diff --git a/notes/Git.md b/notes/Git.md index 1695faad..4bb0a543 100644 --- a/notes/Git.md +++ b/notes/Git.md @@ -129,7 +129,7 @@ HEAD is now at 049d078 added the index file (To restore them type "git stash app # SSH 传输设置 -Git 仓库和 Github 中心仓库之间是通过 SSH 加密。 +Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。 如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key: @@ -143,9 +143,9 @@ $ ssh-keygen -t rsa -C "youremail@example.com" 忽略以下文件: -1. 操作系统自动生成的文件,比如缩略图; -2. 编译生成的中间文件,比如 Java 编译产生的 .class 文件; -3. 自己的敏感信息,比如存放口令的配置文件。 +- 操作系统自动生成的文件,比如缩略图; +- 编译生成的中间文件,比如 Java 编译产生的 .class 文件; +- 自己的敏感信息,比如存放口令的配置文件。 不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。 diff --git a/notes/JDK 中的设计模式.md b/notes/JDK 中的设计模式.md index 92f53cb3..0d858e4b 100644 --- a/notes/JDK 中的设计模式.md +++ b/notes/JDK 中的设计模式.md @@ -138,7 +138,7 @@ java.util.Enumeration ## 5. 中间人模式 -使用中间人对象来封装对象之间的交互。中间人模式可以让降低交互对象之间的耦合程度。 +使用中间人对象来封装对象之间的交互。中间人模式可以降低交互对象之间的耦合程度。 ```java java.util.Timer diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 2012896d..0edaab90 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -28,7 +28,7 @@ ### 1. Set -- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 +- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的; - TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN); diff --git a/notes/MySQL.md b/notes/MySQL.md index e4bff4c2..7f2421b7 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -19,6 +19,10 @@ * [水平切分](#水平切分) * [切分的选择](#切分的选择) * [存在的问题](#存在的问题) +* [六、分库与分表带来的分布式困境与应对之策](#六分库与分表带来的分布式困境与应对之策) + * [事务问题](#事务问题) + * [查询问题](#查询问题) + * [ID 唯一性](#id-唯一性) * [六、故障转移和故障恢复](#六故障转移和故障恢复) * [参考资料](#参考资料) <!-- GFM-TOC --> @@ -368,6 +372,24 @@ do { 最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。 +# 六、分库与分表带来的分布式困境与应对之策 + +<div align="center"> <img src="../pics//c81af7d8-3128-4a3c-a9c9-3e0f5b87ab22.jpg"/> </div><br> + +## 事务问题 + +使用分布式事务。 + +## 查询问题 + +使用汇总表。 + +## ID 唯一性 + +- 使用全局唯一 ID:GUID。 +- 为每个分片指定一个 ID 范围。 +- 分布式 ID 生成器 (如 Twitter 的 [Snowflake](https://twitter.github.io/twitter-server/) 算法)。 + # 六、故障转移和故障恢复 @@ -396,3 +418,6 @@ do { - [MySQL 索引背后的数据结构及算法原理 ](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) - [20+ 条 MySQL 性能优化的最佳经验 ](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html) - [数据库为什么分库分表?mysql的分库分表方案](https://www.i3geek.com/archives/1108) +- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) +- [服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策") +- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases) diff --git a/notes/一致性协议.md b/notes/一致性协议.md index ecac4a1e..4d14be8f 100644 --- a/notes/一致性协议.md +++ b/notes/一致性协议.md @@ -17,9 +17,9 @@ Two-phase Commit(2PC)。 ## 运行过程 -1. 准备阶段:协调者询问参与者事务是否执行成功; +- 准备阶段:协调者询问参与者事务是否执行成功; -2. 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 +- 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 <div align="center"> <img src="../pics//07717718-1230-4347-aa18-2041c315e670.jpg"/> </div><br> @@ -38,9 +38,9 @@ Two-phase Commit(2PC)。 主要有三类节点: -1. 提议者(Proposer):提议一个值; -2. 接受者(Acceptor):对每个提议进行投票; -3. 告知者(Learner):被告知投票的结果,不参与投票过程。 +- 提议者(Proposer):提议一个值; +- 接受者(Acceptor):对每个提议进行投票; +- 告知者(Learner):被告知投票的结果,不参与投票过程。 <div align="center"> <img src="../pics//0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg"/> </div><br> @@ -100,47 +100,47 @@ Raft 主要是用来竞选主节点。 有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。 -① 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 +- 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 <div align="center"> <img src="../pics//111521118015898.gif"/> </div><br> -② 此时 A 发送投票请求给其它所有节点。 +- 此时 A 发送投票请求给其它所有节点。 <div align="center"> <img src="../pics//111521118445538.gif"/> </div><br> -③ 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 +- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 <div align="center"> <img src="../pics//111521118483039.gif"/> </div><br> -④ 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 +- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 <div align="center"> <img src="../pics//111521118640738.gif"/> </div><br> ## 多个 Candidate 竞选 -① 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 +- 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 <div align="center"> <img src="../pics//111521119203347.gif"/> </div><br> -② 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 +- 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 <div align="center"> <img src="../pics//111521119368714.gif"/> </div><br> ## 日志复制 -① 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 +- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 <div align="center"> <img src="../pics//7.gif"/> </div><br> -② Leader 会把修改复制到所有 Follower。 +- Leader 会把修改复制到所有 Follower。 <div align="center"> <img src="../pics//9.gif"/> </div><br> -③ Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 +- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 <div align="center"> <img src="../pics//10.gif"/> </div><br> -④ 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 +- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 <div align="center"> <img src="../pics//11.gif"/> </div><br> diff --git a/notes/代码可读性.md b/notes/代码可读性.md index fdc7e2c6..4ba087cb 100644 --- a/notes/代码可读性.md +++ b/notes/代码可读性.md @@ -29,13 +29,13 @@ 一些比较有表达力的单词: | 单词 | 可替代单词 | -| --- | --- | +| :---: | --- | | send | deliver、dispatch、announce、distribute、route | | find | search、extract、locate、recover | | start| launch、create、begin、open| | make | create、set up、build、generate、compose、add、new | -使用 i、j、k 作为循环迭代器的名字过于简单,user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高 +使用 i、j、k 作为循环迭代器的名字过于简单,user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。 为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。 @@ -102,7 +102,7 @@ Map<String, Integer> scoreMap = new HashMap<>(); 添加测试用例来说明: ```java -//... +// ... // Example: add(1, 2), return 3 int add(int x, int y) { return x + y; @@ -124,14 +124,14 @@ int num = add(\* x = *\ a, \* y = *\ b); 条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确: ```java -if(len < 10) -if(10 > len) +if (len < 10) +if (10 > len) ``` if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。 ```java -if(a == b) { +if (a == b) { // 正逻辑 } else{ // 反逻辑 @@ -163,12 +163,12 @@ if username == "root": 使用摩根定理简化一些逻辑表达式: ```java -if(!a && !b) { +if (!a && !b) { ... } ``` ```java -if(!(a || b)) { +if (!(a || b)) { ... } ``` @@ -179,18 +179,19 @@ if(!(a || b)) { ```java boolean done = false; -while(/* condition */ && !done) { +while (/* condition */ && !done) { ... - if(...) { + if ( ... ) { done = true; continue; } } ``` -``` + +```java while(/* condition */) { ... - if(...) { + if ( ... ) { break; } } @@ -203,7 +204,7 @@ JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函 ```js submitted = false; var submit_form = function(form_name) { - if(submitted) { + if (submitted) { return; } submitted = true; @@ -244,15 +245,15 @@ var setFirstEmptyInput = function(new_alue) { var found = false; var i = 1; var elem = document.getElementById('input' + i); - while(elem != null) { - if(elem.value === '') { + while (elem != null) { + if (elem.value === '') { found = true; break; } i++; elem = document.getElementById('input' + i); } - if(found) elem.value = new_value; + if (found) elem.value = new_value; return elem; } ``` @@ -265,12 +266,12 @@ var setFirstEmptyInput = function(new_alue) { ```js var setFirstEmptyInput = function(new_value) { - for(var i = 1; true; i++) { + for (var i = 1; true; i++) { var elem = document.getElementById('input' + i); - if(elem === null) { + if (elem === null) { return null; } - if(elem.value === '') { + if (elem.value === '') { elem.value = new_value; return elem; } @@ -290,13 +291,13 @@ var setFirstEmptyInput = function(new_value) { int findClostElement(int[] arr) { int clostIdx; int clostDist = Interger.MAX_VALUE; - for(int i = 0; i < arr.length; i++) { + for (int i = 0; i < arr.length; i++) { int x = ...; int y = ...; int z = ...; int value = x * y * z; int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2)); - if(dist < clostDist) { + if (dist < clostDist) { clostIdx = i; clostDist = value; } @@ -311,9 +312,9 @@ int findClostElement(int[] arr) { public int findClostElement(int[] arr) { int clostIdx; int clostDist = Interger.MAX_VALUE; - for(int i = 0; i < arr.length; i++) { + for (int i = 0; i < arr.length; i++) { int dist = computDist(arr, i); - if(dist < clostDist) { + if (dist < clostDist) { clostIdx = i; clostDist = value; } diff --git a/notes/分布式基础.md b/notes/分布式基础.md index f7f77498..920f1e15 100644 --- a/notes/分布式基础.md +++ b/notes/分布式基础.md @@ -75,9 +75,9 @@ 有以下三种一致性模型: -1. 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值; -2. 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值; -3. 最终一致性:新数据写入之后,只能保证过了一个时间窗口后才能在数据副本上读取到最新值; +- 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值; +- 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值; +- 最终一致性:新数据写入之后,只能保证过了一个时间窗口后才能在数据副本上读取到最新值; ### 4. 可扩展性 @@ -99,7 +99,7 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2<sup>n</sup>-1],将 <div align="center"> <img src="../pics//d2d34239-e7c1-482b-b33e-3170c5943556.jpg"/> </div><br> -一致性哈希的优点是在加入或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。 +一致性哈希的优点是在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。 <div align="center"> <img src="../pics//91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg"/> </div><br> diff --git a/notes/分布式问题分析.md b/notes/分布式问题分析.md index 60ed87cd..99e9c04d 100644 --- a/notes/分布式问题分析.md +++ b/notes/分布式问题分析.md @@ -11,14 +11,6 @@ * [使用场景](#使用场景) * [实现方式](#实现方式) * [五、分布式 Session](#五分布式-session) - * [1. Sticky Sessions](#1-sticky-sessions) - * [2. Session Replication](#2-session-replication) - * [3. Persistent DataStore](#3-persistent-datastore) - * [4. In-Memory DataStore](#4-in-memory-datastore) -* [六、分库与分表带来的分布式困境与应对之策](#六分库与分表带来的分布式困境与应对之策) - * [事务问题](#事务问题) - * [查询问题](#查询问题) - * [ID 唯一性](#id-唯一性) * [参考资料](#参考资料) <!-- GFM-TOC --> @@ -59,24 +51,24 @@ #### 2.1 消息处理模型 -(一)消息队列 +**(一)消息队列** <div align="center"> <img src="../pics//96b63e13-e2d8-4ddb-9aa1-a38959ca96e5.jpg" width="700"/> </div><br> -(二)发布/订阅 +**(二)发布/订阅** <div align="center"> <img src="../pics//654acfed-a6a5-4fc7-8f40-3fdcae57bae8.jpg" width="700"/> </div><br> #### 2.2 消息的可靠性 -(一)发送端的可靠性 +**(一)发送端的可靠性** 发送端完成操作后一定能将消息成功发送到消息系统。 实现方法:在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。 -(二)接收端的可靠性 +**(二)接收端的可靠性** 接收端能够从消息中间件成功消费一次消息。 @@ -115,9 +107,9 @@ <div align="center"> <img src="../pics//1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg"/> </div><br> -### 4. 加权最小连接(Weighted Least Connection) +### 4. 加权最少连接(Weighted Least Connection) -在最小连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。 +在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。 <div align="center"> <img src="../pics//44edefb7-4b58-4519-b8ee-4aca01697b78.jpg"/> </div><br> @@ -200,9 +192,9 @@ Java 提供了两种内置的锁的实现,一种是由 JVM 实现的 synchroni 这种方式存在以下几个问题: -1. 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。 -2. 只能是非阻塞锁,插入失败直接就报错了,无法重试。 -3. 不可重入,同一线程在没有释放锁之前无法再获得锁。 +- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。 +- 只能是非阻塞锁,插入失败直接就报错了,无法重试。 +- 不可重入,同一线程在没有释放锁之前无法再获得锁。 **(二)采用乐观锁增加版本号** @@ -218,11 +210,11 @@ EXPIRE 可以为一个键值对设置一个过期时间,从而避免了死锁 **(二)RedLock 算法** -RedLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时还可用。 +RedLock 算法使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。 -1. 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。 -2. 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。 -3. 如果锁获取失败,会到每个实例上释放锁。 +- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。 +- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。 +- 如果锁获取失败,会到每个实例上释放锁。 ### 3. Zookeeper 分布式锁 @@ -246,10 +238,10 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示 **(四)分布式锁实现** -1. 创建一个锁目录 /lock。 -1. 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推。 -2. 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁; -3. 执行业务代码,完成后,删除对应的子节点。 +- 创建一个锁目录 /lock; +- 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000,第二个为 /lock/lock-0000000001,以此类推; +- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁,否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁; +- 执行业务代码,完成后,删除对应的子节点。 **(五)会话超时** @@ -257,7 +249,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示 **(六)羊群效应** -在步骤二,一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 +一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 # 五、分布式 Session @@ -265,7 +257,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示 <div align="center"> <img src="../pics//cookiedata.png"/> </div><br> -## 1. Sticky Sessions +### 1. Sticky Sessions 需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。 @@ -273,7 +265,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示 <div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br> -## 2. Session Replication +### 2. Session Replication 在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。 @@ -281,7 +273,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示 <div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br> -## 3. Persistent DataStore +### 3. Persistent DataStore 将 Session 信息持久化到一个数据库中。 @@ -289,28 +281,10 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示 <div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br> -## 4. In-Memory DataStore +### 4. In-Memory DataStore 可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。 -# 六、分库与分表带来的分布式困境与应对之策 - -<div align="center"> <img src="../pics//f3d3e072-e947-43e9-b999-22385fd569a0.jpg"/> </div><br> - -## 事务问题 - -使用分布式事务。 - -## 查询问题 - -使用汇总表。 - -## ID 唯一性 - -- 使用全局唯一 ID:GUID。 -- 为每个分片指定一个 ID 范围。 -- 分布式 ID 生成器 (如 Twitter 的 [Snowflake](https://twitter.github.io/twitter-server/) 算法)。 - # 参考资料 - [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms) @@ -324,6 +298,3 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示 - [分布式系统的事务处理](https://coolshell.cn/articles/10910.html) - [关于分布式事务](http://blog.csdn.net/suifeng3051/article/details/52691210) - [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html) -- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) -- [服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策") -- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases) diff --git a/notes/正则表达式.md b/notes/正则表达式.md index 38e8b14c..38ce62f2 100644 --- a/notes/正则表达式.md +++ b/notes/正则表达式.md @@ -222,11 +222,11 @@ a.+c 匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: -1. 一位数字 -2. 不以 0 开头的两位数字 -3. 1 开头的三位数 -4. 2 开头,第 2 位是 0-4 的三位数 -5. 25 开头,第 3 位是 0-5 的三位数 +- 一位数字 +- 不以 0 开头的两位数字 +- 1 开头的三位数 +- 2 开头,第 2 位是 0-4 的三位数 +- 25 开头,第 3 位是 0-5 的三位数 **正则表达式** diff --git a/notes/计算机操作系统.md b/notes/计算机操作系统.md index bd907ada..b68c1037 100644 --- a/notes/计算机操作系统.md +++ b/notes/计算机操作系统.md @@ -17,9 +17,11 @@ * [死锁的处理方法](#死锁的处理方法) * [四、内存管理](#四内存管理) * [虚拟内存](#虚拟内存) - * [分页与分段](#分页与分段) * [分页系统地址映射](#分页系统地址映射) * [页面置换算法](#页面置换算法) + * [分段](#分段) + * [段页式](#段页式) + * [分页与分段的比较](#分页与分段的比较) * [五、设备管理](#五设备管理) * [磁盘调度算法](#磁盘调度算法) * [六、链接](#六链接) @@ -69,7 +71,7 @@ ### 2. 内存管理 -内存分配、地址映射、内存保护与共享、内存扩充等。 +内存分配、地址映射、内存保护与共享、虚拟内存等。 ### 3. 文件管理 @@ -183,29 +185,23 @@ Linux 的系统调用主要有以下这些: ### 1. 批处理系统中的调度 -#### 1.1 先来先服务 - -> first-come first-serverd(FCFS) +**(一)先来先服务 first-come first-serverd(FCFS)** 调度最先进入就绪队列的作业。 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 -#### 1.2 短作业优先 - -> shortest job first(SJF) +**(二)短作业优先 shortest job first(SJF)** 调度估计运行时间最短的作业。 长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 -#### 1.3 最短剩余时间优先 - -> shortest remaining time next(SRTN) +**(三)最短剩余时间优先 shortest remaining time next(SRTN)** ### 2. 交互式系统中的调度 -#### 2.1 优先级调度 +**(一)优先级调度** 除了可以手动赋予优先权之外,还可以把响应比作为优先权,这种调度方式叫做高响应比优先调度算法。 @@ -213,13 +209,13 @@ Linux 的系统调用主要有以下这些: 这种调度算法主要是为了解决短作业优先调度算法长作业可能会饿死的问题,因为随着等待时间的增长,响应比也会越来越高。 -#### 2.2 时间片轮转 +**(二)时间片轮转** 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 -#### 2.3 多级反馈队列 +**(三)多级反馈队列** <div align="center"> <img src="../pics//042cf928-3c8e-4815-ae9c-f2780202c68f.png"/> </div><br> @@ -457,8 +453,8 @@ void philosopher(int i) { 为了防止死锁的发生,可以设置两个条件: -1. 必须同时拿起左右两根筷子; -2. 只有在两个邻居都没有进餐的情况下才允许进餐。 +- 必须同时拿起左右两根筷子; +- 只有在两个邻居都没有进餐的情况下才允许进餐。 ```c #define N 5 @@ -507,24 +503,24 @@ void test(i) { // 尝试拿起两把筷子 ## 进程通信 -### 1. 进程同步与进程通信的区别 +进程同步与进程通信很容易混淆,它们的区别在于: - 进程同步:控制多个进程按一定顺序执行; - 进程通信:进程间传输信息。 进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。 +### 1. 信号量 + 在进程同步中介绍的信号量也属于进程通信的一种方式,但是属于低级别的进程通信,因为它传输的信息非常小。 -### 2. 进程通信方式 - -#### 2.1 消息传递 +### 2. 消息传递 操作系统提供了用于通信的通道(Channel),进程可以通过读写这个通道进行通信。 <div align="center"> <img src="../pics//037c3a0b-332d-434d-a374-f343ef72c8e1.jpg" width="400"/> </div><br> -<font size=3> **(一)管道** </font></br> +**(一)管道** 写进程在管道的尾端写入数据,读进程在管道的首端读出数据。管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。 @@ -532,25 +528,25 @@ Linux 中管道通过空文件实现。 管道有三种: -1. 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用; -2. 流管道:去除第一个限制,支持双向传输; -3. 命名管道:去除第二个限制,可以在不相关进程之间进行通信。 +- 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用; +- 流管道:去除第一个限制,支持双向传输; +- 命名管道:去除第二个限制,可以在不相关进程之间进行通信。 <div align="center"> <img src="../pics//7f642a65-b167-4c8f-b382-8322c6322b2c.jpg" width="400"/> </div><br> -<font size=3> **(二)消息队列** </font></br> +**(二)消息队列** -消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 +消息队列克服了信号量传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 <div align="center"> <img src="../pics//d49466db-fdd3-4d36-8a86-47dc45c07a1e.jpg" width="400"/> </div><br> -<font size=3> **(三)套接字** </font></br> +**(三)套接字** 套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。 <div align="center"> <img src="../pics//699b4f96-d63f-46ea-a581-2b3d95eceb6a.jpg" width="400"/> </div><br> -#### 2.2 共享内存 +### 3. 共享内存 操作系统建立一块共享内存,并将其映射到每个进程的地址空间上,进程就可以直接对这块共享内存进行读写。 @@ -564,10 +560,10 @@ Linux 中管道通过空文件实现。 <div align="center"> <img src="../pics//c037c901-7eae-4e31-a1e4-9d41329e5c3e.png"/> </div><br> -1. 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 -2. 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 -3. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 -4. 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 +- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 +- 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 +- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 +- 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 ## 死锁的处理方法 @@ -672,60 +668,24 @@ Linux 中管道通过空文件实现。 - 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。 - 重复以上两步,直到所有进程都标记为终止,则状态时安全的。 -如果一个状态不是安全的,也需要拒绝进入这个状态。 +如果一个状态不是安全的,需要拒绝进入这个状态。 # 四、内存管理 ## 虚拟内存 -每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 +虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 -## 分页与分段 +为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 -### 1. 分页 - -大部分虚拟内存系统都使用分页技术。把由程序产生的地址称为虚拟地址,它们构成了一个虚拟地址空间。例如有一台计算机可以产生 16 位地址,它的虚拟地址空间为 0\~64K,然而计算机只有 32KB 的物理内存,因此虽然可以编写 64KB 的程序,但它们不能被完全调入内存运行。 +从上面的描述中可以看出,虚拟内存允许程序地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。 <div align="center"> <img src="../pics//7b281b1e-0595-402b-ae35-8c91084c33c1.png"/> </div><br> -虚拟地址空间划分成固定大小的页,在物理内存中对应的单元称为页框,页和页框大小通常相同,它们之间通过页表进行映射。 - -程序最开始只将一部分页调入页框中,当程序引用到没有在页框的页时,产生缺页中断,进行页面置换,按一定的原则将一部分页框换出,并将页调入。 - -### 2. 分段 - -<div align="center"> <img src="../pics//22de0538-7c6e-4365-bd3b-8ce3c5900216.png"/> </div><br> - -上图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。 - -<div align="center"> <img src="../pics//e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png"/> </div><br> - -分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 - -每个段都需要程序员来划分。 - -### 3. 段页式 - -用分段方法来分配和管理虚拟存储器。程序的地址空间按逻辑单位分成基本独立的段,而每一段有自己的段名,再把每段分成固定大小的若干页。 - -用分页方法来分配和管理实存。即把整个主存分成与上述页大小相等的存储块,可装入作业的任何一页。 - -程序对内存的调入或调出是按页进行的,但它又可按段实现共享和保护。 - -### 4. 分页与分段区别 - -- 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。 - -- 地址空间的维度:分页是一维地址空间,分段是二维的。 - -- 大小是否可以改变:页的大小不可变,段的大小可以动态改变。 - -- 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 - ## 分页系统地址映射 -- 内存管理单元(MMU):管理着虚拟地址空间和物理内存的转换。 -- 页表(Page table):页(虚拟地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。 +- 内存管理单元(MMU):管理着地址空间和物理内存的转换。 +- 页表(Page table):页(地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。 下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。因此对于虚拟地址(0010 000000000100),前 4 位是用来存储页面号,而后 12 位存储在页中的偏移量。 @@ -783,6 +743,32 @@ Linux 中管道通过空文件实现。 <div align="center"> <img src="../pics//5f5ef0b6-98ea-497c-a007-f6c55288eab1.png"/> </div><br> +## 分段 + +虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。 + +下图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。 + +<div align="center"> <img src="../pics//22de0538-7c6e-4365-bd3b-8ce3c5900216.png"/> </div><br> + +分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 + +<div align="center"> <img src="../pics//e0900bb2-220a-43b7-9aa9-1d5cd55ff56e.png"/> </div><br> + +## 段页式 + +程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。 + +## 分页与分段的比较 + +- 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。 + +- 地址空间的维度:分页是一维地址空间,分段是二维的。 + +- 大小是否可以改变:页的大小不可变,段的大小可以动态改变。 + +- 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 + # 五、设备管理 ## 磁盘调度算法 @@ -845,23 +831,23 @@ gcc -o hello hello.c <div align="center"> <img src="../pics//b396d726-b75f-4a32-89a2-03a7b6e19f6f.jpg" width="800"/> </div><br> -1. 预处理阶段:处理以 # 开头的预处理命令; -2. 编译阶段:翻译成汇编程序; -3. 汇编阶段:将汇编程序翻译可重定向目标程序,它是二进制的; -4. 链接阶段:将可重定向目标程序和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标程序。 +- 预处理阶段:处理以 # 开头的预处理命令; +- 编译阶段:翻译成汇编文件; +- 汇编阶段:将汇编文件翻译成可重定向目标文件,它是二进制的; +- 链接阶段:将可重定向目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。 ## 目标文件 -1. 可执行目标文件:可以直接在内存中执行; -2. 可重定向目标文件:可与其他可重定向目标文件在链接阶段合并,创建一个可执行目标文件; -3. 共享目标文件:可以在运行时被动态加载进内存并链接; +- 可执行目标文件:可以直接在内存中执行; +- 可重定向目标文件:可与其它可重定向目标文件在链接阶段合并,创建一个可执行目标文件; +- 共享目标文件:可以在运行时被动态加载进内存并链接; ## 静态链接 静态连接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务: -1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。 -2. 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。 +- 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。 +- 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。 <div align="center"> <img src="../pics//47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg"/> </div><br> @@ -874,8 +860,8 @@ gcc -o hello hello.c 共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点: -1. 在给定的文件系统中一个库只有一个 .so 文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; -2. 在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。 +- 在给定的文件系统中一个库只有一个 .so 文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; +- 在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。 <div align="center"> <img src="../pics//76dc7769-1aac-4888-9bea-064f1caa8e77.jpg"/> </div><br> diff --git a/notes/计算机网络.md b/notes/计算机网络.md index 955150e7..53187e1c 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -130,7 +130,7 @@ ### 3. 处理时延 -主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据部、进行差错检验或查找适当的路由等。 +主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据、进行差错检验或查找适当的路由等。 ### 4. 排队时延 @@ -142,22 +142,22 @@ ### 1. 五层协议 -1. 应用层:为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。 +- 应用层:为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。 -2. 运输层:提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 +- 运输层:提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。 -3. 网络层:为主机之间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。 +- 网络层:为主机之间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。 -4. 数据链路层:网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的结点提供服务。数据链路层把网络层传来的分组封装成帧。 +- 数据链路层:网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的节点提供服务。数据链路层把网络层传来的分组封装成帧。 -5. 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 +- 物理层:考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 ### 2. 七层协议 其中表示层和会话层用途如下: -1. 表示层:数据压缩、加密以及数据描述。这使得应用程序不必担心在各台主机中表示/存储的内部格式不同的问题。 -2. 会话层:建立及管理会话。 +- 表示层:数据压缩、加密以及数据描述。这使得应用程序不必担心在各台主机中表示/存储的内部格式不同的问题。 +- 会话层:建立及管理会话。 五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。 @@ -185,9 +185,9 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中 ## 通信方式 -1. 单向通信,又称为单工通信; -2. 双向交替通信,又称为半双工通信; -3. 双向同时通信,又称为全双工通信。 +- 单向通信,又称为单工通信; +- 双向交替通信,又称为半双工通信; +- 双向同时通信,又称为全双工通信。 ## 带通调制 @@ -243,8 +243,8 @@ TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中 ## 信道分类 -1. 点对点信道:一对一通信方式; -2. 广播信道:一对多通信方式。 +- 点对点信道:一对一通信方式; +- 广播信道:一对多通信方式。 ## 三个基本问题 @@ -362,9 +362,9 @@ MAC 地址是 6 字节(48 位)的地址,用于唯一标识网络适配器 与 IP 协议配套使用的还有三个协议: -1. 地址解析协议 ARP(Address Resolution Protocol) -2. 网际控制报文协议 ICMP(Internet Control Message Protocol) -3. 网际组管理协议 IGMP(Internet Group Management Protocol) +- 地址解析协议 ARP(Address Resolution Protocol) +- 网际控制报文协议 ICMP(Internet Control Message Protocol) +- 网际组管理协议 IGMP(Internet Group Management Protocol) <div align="center"> <img src="../pics//0a9f4125-b6ab-4e94-a807-fd7070ae726a.png" width="400"/> </div><br> @@ -396,9 +396,9 @@ MAC 地址是 6 字节(48 位)的地址,用于唯一标识网络适配器 IP 地址的编址方式经历了三个历史阶段: -1. 分类 -2. 子网划分 -3. 无分类 +- 分类 +- 子网划分 +- 无分类 ### 1. 分类 @@ -444,7 +444,7 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为 每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。 -如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到硬件地址的映射。 +如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。 <div align="center"> <img src="../pics//8006a450-6c2f-498c-a928-c927f758b1d0.png" width="700"/> </div><br> @@ -458,12 +458,12 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为 ## 路由器分组转发流程 -1. 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。 -2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; -3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; -4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; -5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; -6. 报告转发分组出错。 +- 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。 +- 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; +- 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; +- 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; +- 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; +- 报告转发分组出错。 <div align="center"> <img src="../pics//1ab49e39-012b-4383-8284-26570987e3c4.jpg" width="800"/> </div><br> @@ -475,8 +475,8 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为 可以把路由选择协议划分为两大类: -1. 内部网关协议 IGP(Interior Gateway Protocol):在 AS 内部使用,如 RIP 和 OSPF。 -2. 外部网关协议 EGP(External Gateway Protocol):在 AS 之间使用,如 BGP。 +- 内部网关协议 IGP(Interior Gateway Protocol):在 AS 内部使用,如 RIP 和 OSPF。 +- 外部网关协议 EGP(External Gateway Protocol):在 AS 之间使用,如 BGP。 <div align="center"> <img src="../pics//276c31df-3b28-4ac2-b006-1e80fc86a64f.jpg" width="600"/> </div><br> @@ -488,11 +488,11 @@ RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经 距离向量算法: -1. 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1; -2. 对修改后的 RIP 报文中的每一个项目,进行以下步骤: +- 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1; +- 对修改后的 RIP 报文中的每一个项目,进行以下步骤: - 若原来的路由表中没有目的网络 N,则把该项目添加到路由表中; - 否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。 -3. 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。 +- 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。 RIP 协议实现简单,开销小,但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。 @@ -512,7 +512,7 @@ OSPF 具有以下特点: ### 3. 外部网关协议 BGP -AS 之间的路由选择很困难,主要是互联网规模很大。并且各个 AS 内部使用不同的路由选择协议,就无法准确定义路径的度量。并且 AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。 +AS 之间的路由选择很困难,主要是因为互联网规模很大。并且各个 AS 内部使用不同的路由选择协议,就无法准确定义路径的度量。并且 AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。 BGP 只能寻找一条比较好的路由,而不是最佳路由。它采用路径向量路由选择协议。 @@ -540,10 +540,10 @@ Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报。 Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。 -1. 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,但 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; -2. 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 -3. 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 -4. 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 +- 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文; +- 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。 +- 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。 +- 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。 ## 虚拟专用网 VPN @@ -551,9 +551,9 @@ Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终 有三个专用地址块: -1. 10.0.0.0 \~ 10.255.255.255 -2. 172.16.0.0 \~ 172.31.255.255 -3. 192.168.0.0 \~ 192.168.255.255 +- 10.0.0.0 \~ 10.255.255.255 +- 172.16.0.0 \~ 172.31.255.255 +- 192.168.0.0 \~ 192.168.255.255 VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。 @@ -599,7 +599,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 - **同步 SYN** :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。 -- **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放运输连接。 +- **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。 - **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。 @@ -609,15 +609,15 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 假设 A 为客户端,B 为服务器端。 -1. 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 +- 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 -2. A 向 B 发送连接请求报文段,SYN=1,ACK=0,选择一个初始的序号 x。 +- A 向 B 发送连接请求报文段,SYN=1,ACK=0,选择一个初始的序号 x。 -3. B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 +- B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 -4. A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 +- A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 -5. B 收到 A 的确认后,连接建立。 +- B 收到 A 的确认后,连接建立。 **三次握手的原因** @@ -631,15 +631,15 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 -1. A 发送连接释放报文段,FIN=1。 +- A 发送连接释放报文段,FIN=1。 -2. B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。 +- B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。 -3. 当 B 要不再需要连接时,发送连接释放请求报文段,FIN=1。 +- 当 B 不再需要连接时,发送连接释放请求报文段,FIN=1。 -4. A 收到后发出确认,进入 TIME-WAIT 状态,等待 2MSL 时间后释放连接。 +- A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL 时间后释放连接。 -5. B 收到 A 的确认后释放连接。 +- B 收到 A 的确认后释放连接。 **四次挥手的原因** @@ -649,9 +649,9 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由: -1. 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段,那么就会重新发送连接释放请求报文段,A 等待一段时间就是为了处理这种情况的发生。 +- 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段,那么就会重新发送连接释放请求报文段,A 等待一段时间就是为了处理这种情况的发生。 -2. 等待一段时间是为了让本连接持续时间内所产生的所有报文段都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文段。 +- 等待一段时间是为了让本连接持续时间内所产生的所有报文段都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文段。 ## TCP 滑动窗口 @@ -661,7 +661,7 @@ VPN 使用公用的互联网作为本机构各专用网之间的通信载体。 发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 -接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 32, 34, 35},其中 {31, 32} 按序到达,而 {34, 35} 就不是,因此只对字节 32 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 +接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 33, 34},其中 {31} 按序到达,而 {32, 33} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 ## TCP 可靠传输 @@ -689,12 +689,14 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文 <div align="center"> <img src="../pics//51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br> -TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量。注意拥塞窗口与发送方窗口的区别,拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 +TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 + +发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 为了便于讨论,做如下假设: -1. 接收方有足够大的接收缓存,因此不会发生流量控制; -2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。 +- 接收方有足够大的接收缓存,因此不会发生流量控制; +- 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。 <div align="center"> <img src="../pics//910f613f-514f-4534-87dd-9b4699d59d31.png" width="800"/> </div><br> @@ -734,10 +736,10 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 域名服务器可以分为以下四类: -1. 根域名服务器:解析顶级域名; -2. 顶级域名服务器:解析二级域名; -3. 权限域名服务器:解析区内的域名; -4. 本地域名服务器:也称为默认域名服务器。可以在其中配置高速缓存。 +- 根域名服务器:解析顶级域名; +- 顶级域名服务器:解析二级域名; +- 权限域名服务器:解析区内的域名; +- 本地域名服务器:也称为默认域名服务器。可以在其中配置高速缓存。 区和域的概念不同,可以在一个域中划分多个区。图 b 在域 abc.com 中划分了两个区:abc.com 和 y.abc.com @@ -787,7 +789,7 @@ IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动 ### 3. SMTP -SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主题的结构,定义了非 ASCII 码的编码规则。 +SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。 <div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png" width=""/> </div><br> @@ -817,61 +819,61 @@ P2P 是一个分布式系统,任何时候都有对等方加入或者退出。 ### 1. DHCP 配置主机信息 -1. 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。 +- 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。 -2. 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。 +- 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。 -3. 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。 +- 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。 -4. 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。 +- 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。 -5. 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。 +- 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。 -8. 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。 +- 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。 -9. 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。 +- 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。 ### 2. ARP 解析 MAC 地址 -1. 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。 +- 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。 -2. 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。 +- 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。 -3. 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。 +- 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。 -4. 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。 +- 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。 -5. DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。 +- DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。 -6. 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。 +- 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。 -7. 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。 +- 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。 ### 3. DNS 解析域名 -1. 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。 +- 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。 -2. 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。 +- 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。 -3. 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。 +- 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。 -4. 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。 +- 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。 -5. 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。 +- 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。 ### 4. HTTP 请求页面 -1. 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。 +- 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。 -2. 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。 +- 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。 -3. HTTP 服务器收到该报文段之后,生成 TCP SYNACK 报文段,发回给主机。 +- HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。 -4. 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。 +- 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。 -5. HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。 +- HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。 -6. 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。 +- 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。 ## 常用端口 diff --git a/notes/重构.md b/notes/重构.md index e74d3cc5..947a5455 100644 --- a/notes/重构.md +++ b/notes/重构.md @@ -125,53 +125,115 @@ 包括三个类:Movie、Rental 和 Customer,Rental 包含租赁的 Movie 以及天数。 -<div align="center"> <img src="../pics//25d6d3d4-4726-47b1-a9cb-3316d1ff5dd5.png"/> </div><br> +<div align="center"> <img src="../pics//c2f0c8e2-da66-498c-a38f-e1176abee29e.png"/> </div><br> 最开始的实现是把所有的计费代码都放在 Customer 类中。 可以发现,该代码没有使用 Customer 类中的任何信息,更多的是使用 Rental 类的信息,因此第一个可以重构的点就是把具体计费的代码移到 Rental 类中,然后 Customer 类的 getTotalCharge() 方法只需要调用 Rental 类中的计费方法即可。 ```java -class Customer... -double getTotalCharge() { - while (rentals.hasMoreElements()) { - double thisAmount = 0; - Rental each = (Rental) rentals.nextElement(); - switch (each.getMovie().getPriceCode()) { - case Movie.REGULAR: - thisAmount += 2; - if (each.getDaysRented() > 2) - thisAmount += (each.getDaysRented() - 2) * 1.5; - break; - case Movie.NEW_RELEASE: - thisAmount += each.getDaysRented() * 3; - break; - case Movie.CHILDRENS: - thisAmount += 1.5; - if (each.getDaysRented() > 3) - thisAmount += (each.getDaysRented() - 3) * 1.5; - break; +public class Customer { + + private List<Rental> rentals = new ArrayList<>(); + + public void addRental(Rental rental) { + rentals.add(rental); + } + + public double getTotalCharge() { + double totalCharge = 0.0; + for (Rental rental : rentals) { + switch (rental.getMovie().getMovieType()) { + case Movie.Type1: + totalCharge += rental.getDaysRented(); + break; + case Movie.Type2: + totalCharge += rental.getDaysRented() * 2; + break; + case Movie.Type3: + totalCharge += 1.5; + totalCharge += rental.getDaysRented() * 3; + break; + } + } + return totalCharge; + } +} + +``` + +```java +public class Rental { + private int daysRented; + + private Movie movie; + + public Rental(int daysRented, Movie movie) { + this.daysRented = daysRented; + this.movie = movie; + } + + public Movie getMovie() { + return movie; + } + + public int getDaysRented() { + return daysRented; } } ``` -使用 switch 的准则是:只能在对象自己的数据上使用,而不能在另一个对象的数据基础上使用。解释如下:switch 使用的数据通常是一组相关的数据,例如上面的代码使用了 Movie 的多种类别数据。当这组类别的数据发生改变时,例如上面的代码中增加 Movie 的类别或者修改一种 Movie 类别的计费方法,就需要修改 switch 代码。如果允许违反了准则,就会有多个地方的 switch 使用了这部分的数据,那么需要打开所有的 switch 代码进行修改。 +```java +public class Movie { + + public static final int Type1 = 0, Type2 = 1, Type3 = 2; + + private int type; + + public Movie(int type) { + this.type = type; + } + + public int getMovieType() { + return type; + } +} +``` + +```java +public class App { + public static void main(String[] args) { + Customer customer = new Customer(); + Rental rental1 = new Rental(1, new Movie(Movie.Type1)); + Rental rental2 = new Rental(2, new Movie(Movie.Type2)); + customer.addRental(rental1); + customer.addRental(rental2); + System.out.println(customer.getTotalCharge()); + } +} +``` + +```html +5 +``` + +使用 switch 的准则是:只使用 switch 所在类的数据。解释如下:switch 使用的数据通常是一组相关的数据,例如 getTotalCharge() 代码使用了 Movie 的多种类别数据。当这组类别的数据发生改变时,例如增加 Movie 的类别或者修改一种 Movie 类别的计费方法,就需要修改 switch 代码。如果违反了准则,就会有多个地方的 switch 使用了这部分的数据,那么这些 swtich 都需要进行修改,这些代码可能遍布在各个地方,修改工作往往会很难进行。上面的实现违反了这一准则,因此需要重构。 以下是继承 Movie 的多态解决方案,这种方案可以解决上述的 switch 问题,因为每种电影类别的计费方式都被放到了对应 Movie 子类中,当变化发生时,只需要去修改对应子类中的代码即可。 -<div align="center"> <img src="../pics//76b48b4c-8999-4967-893b-832602e73285.png"/> </div><br> +<div align="center"> <img src="../pics//41026c79-dfc1-40f7-85ae-062910fd272b.png"/> </div><br> -但是由于 Movie 可以在其生命周期内修改自己的类别,一个对象却不能在生命周期内修改自己所属的类,因此这种方案不可行。可以使用策略模式来解决这个问题(原书写的是使用状态模式,但是这里应该为策略模式,具体可以参考设计模式内容)。 +但是我们需要允许一部影片可以在运行过程中改变其所属的分类,但是上述的继承方案却不可行,因为一个对象所属的类在编译过程就确定了。 -下图中,Price 有多种实现,Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。 +为了解决上述的问题,需要使用策略模式。引入 Price 类,它有多种实现。Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。 -<div align="center"> <img src="../pics//2a842a14-e4ab-4f37-83fa-f82c206fe426.png"/> </div><br> +<div align="center"> <img src="../pics//8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png"/> </div><br> 重构后整体的类图和时序图如下: -<div align="center"> <img src="../pics//9d549816-60b7-4899-9877-23b01503ab13.png"/> </div><br> +<div align="center"> <img src="../pics//5b910141-08b6-442d-a4bc-a1608458c636.png"/> </div><br> -<div align="center"> <img src="../pics//2c8a7a87-1bf1-4d66-9ba9-225a1add0a51.png"/> </div><br> +<div align="center"> <img src="../pics//3ca58a41-8794-49c1-992e-de5d579a50d1.png"/> </div><br> # 二、重构原则 @@ -197,7 +259,7 @@ double getTotalCharge() { - 允许逻辑共享 - 分开解释意图和实现 - 隔离变化 -- 封装条件逻辑。 +- 封装条件逻辑 重构可以理解为在适当的位置插入间接层以及在不需要时移除间接层。 @@ -227,14 +289,12 @@ double getTotalCharge() { 在编写代码时,不用对性能过多关注,只有在最后性能优化阶段再考虑性能问题。 -应当只关注关键代码的性能,因为只有一小部分的代码是关键代码。 +应当只关注关键代码的性能,并且只有一小部分的代码是关键代码。 # 三、代码的坏味道 本章主要介绍一些不好的代码,也就是说这些代码应该被重构。 -文中提到的具体重构原则可以先忽略。 - ## 1. 重复代码 > Duplicated Code @@ -273,7 +333,7 @@ Extract Method 会把很多参数和临时变量都当做参数,可以用 Repl 太长的参数列表往往会造成前后不一致,不易使用。 -面向对象程序中,函数所需要的数据通常内在宿主类中找到。 +面向对象程序中,函数所需要的数据通常能在宿主类中找到。 ## 5. 发散式变化 @@ -287,7 +347,7 @@ Extract Method 会把很多参数和临时变量都当做参数,可以用 Repl > Shotgun Surgery -一个变化引起多个类修改; +一个变化引起多个类修改。 使用 Move Method 和 Move Field 把所有需要修改的代码放到同一个类中。 @@ -452,18 +512,20 @@ return anOrder.basePrice() > 1000; > Replace Temp with Query -以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。 +以临时变量保存某一表达式的运算结果,将这个表达式提炼到一个独立函数中,将所有对临时变量的引用点替换为对新函数的调用。 + +Replace Temp with Query 往往是 Extract Method 之前必不可少的一个步骤,因为局部变量会使代码难以提炼。 ```java double basePrice = quantity * itemPrice; -if(basePrice > 1000) +if (basePrice > 1000) return basePrice * 0.95; else return basePrice * 0.98; ``` ```java -if(basePrice() > 1000) +if (basePrice() > 1000) return basePrice() * 0.95; else return basePrice() * 0.98; @@ -478,10 +540,10 @@ double basePrice(){ > Introduce Explaining Variable -将复杂表达式(或其中一部分)的结果放进一个临时变量,以此变量名称来解释表达式用途。 +将复杂表达式(或其中一部分)的结果放进一个临时变量, 以此变量名称来解释表达式用途。 ```java -if((platform.toUpperCase().indexOf("MAC") > -1) && +if ((platform.toUpperCase().indexOf("MAC") > -1) && (browser.toUpperCase().indexOf("IE") > -1) && wasInitialized() && resize > 0) { // do something @@ -493,7 +555,7 @@ final boolean isMacOS = platform.toUpperCase().indexOf("MAC") > -1; final boolean isIEBrower = browser.toUpperCase().indexOf("IE") > -1; final boolean wasResized = resize > 0; -if(isMacOS && isIEBrower && wasInitialized() && wasResized) { +if (isMacOS && isIEBrower && wasInitialized() && wasResized) { // do something } ``` @@ -513,14 +575,18 @@ if(isMacOS && isIEBrower && wasInitialized() && wasResized) { 以一个临时变量取代对该参数的赋值。 ```java -int discount (int inputVal, int quentity, int yearToDate){ +int discount (int inputVal, int quentity, int yearToDate) { if (inputVal > 50) inputVal -= 2; + ... +} ``` ```java -int discount (int inputVal, int quentity, int yearToDate){ +int discount (int inputVal, int quentity, int yearToDate) { int result = inputVal; if (inputVal > 50) result -= 2; + ... +} ``` ## 8. 以函数对象取代函数 @@ -572,18 +638,18 @@ int discount (int inputVal, int quentity, int yearToDate){ 建立所需的函数,隐藏委托关系。 ```java -class Person{ +class Person { Department department; - public Department getDepartment(){ + public Department getDepartment() { return department; } } -class Department{ +class Department { private Person manager; - public Person getManager(){ + public Person getManager() { return manager; } } @@ -598,7 +664,7 @@ Person manager = john.getDepartment().getManager(); 通过为 Peron 建立一个函数来隐藏这种委托关系。 ```java -public Person getManager(){ +public Person getManager() { return department.getManager(); } ``` @@ -651,7 +717,7 @@ Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受 以 Change Value to Reference 相反。值对象有个非常重要的特性:它是不可变的,不可变表示如果要改变这个对象,必须用一个新的对象来替换旧对象,而不是修改旧对象。 -需要为值对象实现 equals() 和 hashCode() 方法 +需要为值对象实现 equals() 和 hashCode() 方法。 ## 5. 以对象取代数组 @@ -667,7 +733,7 @@ Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受 一些领域数据置身于 GUI 控件中,而领域函数需要访问这些数据。 -将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用以同步领域对象和 GUI 对象内的重复数据。 +将该数据赋值到一个领域对象中,建立一个 Oberver 模式,用于同步领域对象和 GUI 对象内的重复数据。 <div align="center"> <img src="../pics//e024bd7e-fb4e-4239-9451-9a6227f50b00.jpg" width=""/> </div><br> @@ -680,10 +746,10 @@ Hide Delegate 有很大好处,但是它的代价是:每当客户要使用受 有两个类,分别为订单 Order 和客户 Customer,Order 引用了 Customer,Customer 也需要引用 Order 来查看其所有订单详情。 ```java -class Order{ +class Order { private Customer customer; - public void setCustomer(Customer customer){ - if(this.customer != null) + public void setCustomer(Customer customer) { + if (this.customer != null) this.customer.removeOrder(this); this.customer = customer; this.customer.add(this); @@ -691,12 +757,12 @@ class Order{ } ``` ```java -class Curstomer{ +class Curstomer { private Set<Order> orders = new HashSet<>(); - public void removeOrder(Order order){ + public void removeOrder(Order order) { orders.remove(order); } - public void addOrder(Order order){ + public void addOrder(Order order) { orders.add(order); } } @@ -716,7 +782,7 @@ class Curstomer{ > Replace Magic Number with Symbolic Constant -创建一个常量,根据其意义为它命名,并将字面常量换位这个常量。 +创建一个常量,根据其意义为它命名,并将字面常量换为这个常量。 ## 10. 封装字段 @@ -777,15 +843,17 @@ public 字段应当改为 private,并提供相应的访问函数。 对于一个复杂的条件语句,可以从 if、then、else 三个段落中分别提炼出独立函数。 ```java -if(data.befor(SUMMER_START) || data.after(SUMMER_END)) +if (data.befor(SUMMER_START) || data.after(SUMMER_END)) charge = quantity * winterRate + winterServiceCharge; -else charge = quantity * summerRate; +else + charge = quantity * summerRate; ``` ```java -if(notSummer(date)) +if (notSummer(date)) charge = winterCharge(quantity); -else charge = summerCharge(quantity); +else + charge = summerCharge(quantity); ``` ## 2. 合并条件表达式 @@ -797,7 +865,7 @@ else charge = summerCharge(quantity); 将这些测试合并为一个条件表达式,并将这个条件表达式提炼成为一个独立函数。 ```java -double disabilityAmount(){ +double disabilityAmount() { if (seniority < 2) return 0; if (monthsDisabled > 12 ) return 0; if (isPartTime) return 0; @@ -805,7 +873,7 @@ double disabilityAmount(){ } ``` ```java -double disabilityAmount(){ +double disabilityAmount() { if (isNotEligibleForDisability()) return 0; // ... } @@ -820,7 +888,7 @@ double disabilityAmount(){ 将这段重复代码搬移到条件表达式之外。 ```java -if (isSpecialDeal()){ +if (isSpecialDeal()) { total = price * 0.95; send(); } else { @@ -844,7 +912,7 @@ send(); 在一系列布尔表达式中,某个变量带有“控制标记”的作用。 -用 break 语 句或 return 语句来取代控制标记。 +用 break 语句或 return 语句来取代控制标记。 ## 5. 以卫语句取代嵌套条件表达式 @@ -1027,7 +1095,7 @@ void setWidth(int arg){ ```java int low = daysTempRange().getLow(); int high = daysTempRange().getHigh(); -withinPlan = plan.withinRange(low,high); +withinPlan = plan.withinRange(low, high); ``` ```java @@ -1096,12 +1164,12 @@ double finalPrice = discountedPrice (basePrice); 将向下转型动作移到函数中。 ```java -Object lastReading(){ +Object lastReading() { return readings.lastElement(); } ``` ```java -Reading lastReading(){ +Reading lastReading() { return (Reading)readings.lastElement(); } ``` diff --git a/pics/3ca58a41-8794-49c1-992e-de5d579a50d1.png b/pics/3ca58a41-8794-49c1-992e-de5d579a50d1.png new file mode 100644 index 00000000..725f2f29 Binary files /dev/null and b/pics/3ca58a41-8794-49c1-992e-de5d579a50d1.png differ diff --git a/pics/41026c79-dfc1-40f7-85ae-062910fd272b.png b/pics/41026c79-dfc1-40f7-85ae-062910fd272b.png new file mode 100644 index 00000000..a60891bb Binary files /dev/null and b/pics/41026c79-dfc1-40f7-85ae-062910fd272b.png differ diff --git a/pics/5b910141-08b6-442d-a4bc-a1608458c636.png b/pics/5b910141-08b6-442d-a4bc-a1608458c636.png new file mode 100644 index 00000000..67134656 Binary files /dev/null and b/pics/5b910141-08b6-442d-a4bc-a1608458c636.png differ diff --git a/pics/8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png b/pics/8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png new file mode 100644 index 00000000..86b3b637 Binary files /dev/null and b/pics/8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png differ diff --git a/pics/c2f0c8e2-da66-498c-a38f-e1176abee29e.png b/pics/c2f0c8e2-da66-498c-a38f-e1176abee29e.png new file mode 100644 index 00000000..781c0eff Binary files /dev/null and b/pics/c2f0c8e2-da66-498c-a38f-e1176abee29e.png differ diff --git a/pics/c81af7d8-3128-4a3c-a9c9-3e0f5b87ab22.jpg b/pics/c81af7d8-3128-4a3c-a9c9-3e0f5b87ab22.jpg new file mode 100644 index 00000000..c967b300 Binary files /dev/null and b/pics/c81af7d8-3128-4a3c-a9c9-3e0f5b87ab22.jpg differ