diff --git a/README.md b/README.md index 580b86a62bd21e8027b46f93eb5796c28044a591..722c681ecb08e86864b89891601e19394bd0d714 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,10 @@ 整理自《图解 HTTP》 +> [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md) + +整理自《Unix 网络编程》 + ## 面向对象 :couple: @@ -63,6 +67,10 @@ 整理自《SQL 必知必会》 +> [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md) + +Leetcode 上数据库题目的解题记录。 + > [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md) 整理自《高性能 MySQL》 @@ -73,13 +81,17 @@ ## Java :coffee: +> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) + +整理了一些常见考点。 + > [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md) 整理自《深入理解 Java 虚拟机》 > [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md) -只整理了一些比较基础的概念,之后会继续添加更多内容。 +整理了一些并发的基本概念。 > [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md) @@ -89,10 +101,6 @@ File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO -> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) - -整理了一些常见考点。 - > [JDK 中的设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/JDK%20中的设计模式.md) 对每种设计模式做了一个总结,并给出在 JDK 中的使用实例。 @@ -103,10 +111,6 @@ File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO 整理自《大规模分布式存储系统》 -> [一致性协议](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/一致性协议.md) - -两阶段提交、Paxos、Raft、拜占庭将军问题。 - > [分布式问题分析](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式问题分析.md) 分布式事务、负载均衡算法与实现、分布式锁、分布式 Session、分库分表的分布式困境与应对之策。 diff --git a/notes/Git.md b/notes/Git.md index 1695faadade0acd17b0ef52a411ead3e913d8e90..4bb0a54352b6d64397da88779fd31d29c57ba7da 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/HTTP.md b/notes/HTTP.md index 1c70e89da8ed3df90116a11e65e4fa3cb0ab54ce..11045573a9a8c2504fb1050bc50b5fc49b014b6b 100644 --- a/notes/HTTP.md +++ b/notes/HTTP.md @@ -27,34 +27,38 @@ * [五、具体应用](#五具体应用) * [Cookie](#cookie) * [缓存](#缓存) - * [持久连接](#持久连接) - * [管线化处理](#管线化处理) - * [编码](#编码) + * [连接管理](#连接管理) + * [内容协商](#内容协商) + * [内容编码](#内容编码) + * [范围请求](#范围请求) * [分块传输编码](#分块传输编码) * [多部分对象集合](#多部分对象集合) - * [范围请求](#范围请求) - * [内容协商](#内容协商) * [虚拟主机](#虚拟主机) * [通信数据转发](#通信数据转发) * [六、HTTPs](#六https) * [加密](#加密) * [认证](#认证) - * [完整性](#完整性) + * [完整性保护](#完整性保护) + * [HTTPs 的缺点](#https-的缺点) + * [配置 HTTPs](#配置-https) * [七、Web 攻击技术](#七web-攻击技术) - * [攻击模式](#攻击模式) * [跨站脚本攻击](#跨站脚本攻击) - * [跨站点请求伪造](#跨站点请求伪造) + * [跨站请求伪造](#跨站请求伪造) * [SQL 注入攻击](#sql-注入攻击) * [拒绝服务攻击](#拒绝服务攻击) * [八、GET 和 POST 的区别](#八get-和-post-的区别) + * [作用](#作用) * [参数](#参数) * [安全](#安全) * [幂等性](#幂等性) * [可缓存](#可缓存) * [XMLHttpRequest](#xmlhttprequest) -* [九、各版本比较](#九各版本比较) - * [HTTP/1.0 与 HTTP/1.1 的区别](#http10-与-http11-的区别) - * [HTTP/1.1 与 HTTP/2.0 的区别](#http11-与-http20-的区别) +* [九、HTTP/1.0 与 HTTP/1.1 的区别](#九http10-与-http11-的区别) +* [十、HTTP/2.0](#十http20) + * [HTTP/1.x 缺陷](#http1x-缺陷) + * [二进制分帧层](#二进制分帧层) + * [服务端推送](#服务端推送) + * [首部压缩](#首部压缩) * [参考资料](#参考资料) @@ -63,8 +67,9 @@ ## Web 基础 -- HTTP(HyperText Transfer Protocol,超文本传输协议) - WWW(World Wide Web)的三种技术:HTML、HTTP、URL +- HTML(HyperText Markup Language,超文本标记语言) +- HTTP(HyperText Transfer Protocol,超文本传输协议) - RFC(Request for Comments,征求修正意见书),互联网的设计文档。 ## URL @@ -218,7 +223,7 @@ CONNECT www.example.com:443 HTTP/1.1 - 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。 -- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-ModifiedSince,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 +- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。 - **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。 @@ -317,11 +322,19 @@ CONNECT www.example.com:443 HTTP/1.1 HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 -Cookie 是服务器发送给客户端的数据,该数据会被保存在浏览器中,并且客户端的下一次请求报文会包含该数据。通过 Cookie 可以让服务器知道两个请求是否来自于同一个客户端,从而实现保持登录状态等功能。 +Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。它用于告知服务端两个请求是否来自同一浏览器,并保持用户的登录状态。 -### 1. 创建过程 +### 1. 用途 -服务器发送的响应报文包含 Set-Cookie 字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 +- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息) +- 个性化设置(如用户自定义设置、主题等) +- 浏览器行为跟踪(如跟踪分析用户行为等) + +Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。 + +### 2. 创建过程 + +服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 ```html HTTP/1.0 200 OK @@ -332,7 +345,7 @@ Set-Cookie: tasty_cookie=strawberry [page content] ``` -客户端之后发送请求时,会从浏览器中读出 Cookie 值,在请求报文中包含 Cookie 字段。 +客户端之后对同一个服务器发送请求时,会从浏览器中读出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。 ```html GET /sample_page.html HTTP/1.1 @@ -340,7 +353,7 @@ Host: www.example.org Cookie: yummy_cookie=choco; tasty_cookie=strawberry ``` -### 2. 分类 +### 3. 分类 - 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。 - 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(Max-Age)之后就成为了持久性的 Cookie。 @@ -349,117 +362,207 @@ Cookie: yummy_cookie=choco; tasty_cookie=strawberry Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; ``` -### 3. Set-Cookie +### 4. JavaScript 获取 Cookie + +通过 `Document.cookie` 属性可创建新的 Cookie,也可通过该属性访问非 HttpOnly 标记的 Cookie。 + +```html +document.cookie = "yummy_cookie=choco"; +document.cookie = "tasty_cookie=strawberry"; +console.log(document.cookie); +``` + +### 5. Secure 和 HttpOnly + +标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 + +标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。因为跨域脚本 (XSS) 攻击常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 + +```html +Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly +``` + +### 6. 作用域 + +Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。 -| 属性 | 说明 | -| :--: | -- | -| NAME=VALUE | 赋予 Cookie 的名称和其值(必需项) | -| expires=DATE | Cookie 的有效期(若不明确指定则默认为浏览器关闭前为止) | -| path=PATH | 将服务器上的文件目录作为 Cookie 的适用对象(若不指定则默认为文档所在的文件目录) | -| domain=域名 | 作为 Cookie 适用对象的域名(若不指定则默认为创建 Cookie 的服务器的域名) | -| Secure | 仅在 HTTPs 安全通信时才会发送 Cookie | -| HttpOnly | 加以限制,使 Cookie 不能被 JavaScript 脚本访问 | +Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配: -### 4. Session 和 Cookie 区别 +- /docs +- /docs/Web/ +- /docs/Web/HTTP -Session 是服务器用来跟踪用户的一种手段,每个 Session 都有一个唯一标识:Session ID。当服务器创建了一个 Session 时,给客户端发送的响应报文包含了 Set-Cookie 字段,其中有一个名为 sid 的键值对,这个键值对就是 Session ID。客户端收到后就把 Cookie 保存在浏览器中,并且之后发送的请求报文都包含 Session ID。HTTP 就是通过 Session 和 Cookie 这两种方式一起合作来实现跟踪用户状态的,Session 用于服务器端,Cookie 用于客户端。 +### 7. Session -### 5. 浏览器禁用 Cookie 的情况 +除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 -会使用 URL 重写技术,在 URL 后面加上 sid=xxx 。 +Session 可以存储在服务器上的文件、数据库或者内存中,现在最常见的是将 Session 存储在内存型数据库中,比如 Redis。 -### 6. 使用 Cookie 实现用户名和密码的自动填写 +使用 Session 维护用户登录的过程如下: -网站脚本会自动从保存在浏览器中的 Cookie 读取用户名和密码,从而实现自动填写。 +- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; +- 服务器验证该用户名和密码; +- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID; +- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; +- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之后的业务操作。 -但是如果 Set-Cookie 指定了 HttpOnly 属性,就无法通过 Javascript 脚本获取 Cookie 信息,这是出于安全性考虑。 +应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 + +### 8. 浏览器禁用 Cookie + +此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。 + +### 9. Cookie 与 Session 选择 + +- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时 首选 Session; +- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; +- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 ## 缓存 ### 1. 优点 -1. 降低服务器的负担; -2. 提高响应速度(缓存资源比服务器上的资源离客户端更近)。 +- 缓解服务器压力; +- 减低客户端获取资源的延迟(缓存资源比服务器上的资源离客户端更近)。 ### 2. 实现方法 -1. 让代理服务器进行缓存; -2. 让客户端浏览器进行缓存。 +- 让代理服务器进行缓存; +- 让客户端浏览器进行缓存。 + +### 3. Cache-Control + +HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 + +**(一)禁止进行缓存** + +no-store 指令规定不能对请求或响应的任何一部分进行缓存。 + +```html +Cache-Control: no-store +``` -### 3. Cache-Control 字段 +**(二)强制确认缓存** -HTTP 通过 Cache-Control 首部字段来控制缓存。 +no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。 ```html -Cache-Control: private, max-age=0, no-cache +Cache-Control: no-cache ``` -### 4. no-cache 指令 +**(三)私有缓存和公共缓存** -该指令出现在请求报文的 Cache-Control 字段中,表示缓存服务器需要先向原服务器验证缓存资源是否过期; +private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。 -该指令出现在响应报文的 Cache-Control 字段中,表示缓存服务器在进行缓存之前需要先验证缓存资源的有效性。 +```html +Cache-Control: private +``` -### 5. no-store 指令 +public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。 -该指令表示缓存服务器不能对请求或响应的任何一部分进行缓存。 +```html +Cache-Control: public +``` -no-cache 不表示不缓存,而是缓存之前需要先进行验证,no-store 才是不进行缓存。 +**(四)缓存过期机制** -### 6. max-age 指令 +max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 -该指令出现在请求报文的 Cache-Control 字段中,如果缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 +max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。 -该指令出现在响应报文的 Cache-Control 字段中,表示缓存资源在缓存服务器中保存的时间。 +```html +Cache-Control: max-age=31536000 +``` Expires 字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中,Cache-Control : max-age 指令会被忽略掉。 -## 持久连接 +```html +Expires: Wed, 04 Jul 2012 08:26:05 GMT +``` + +### 4. 缓存验证 -当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。持久连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 +需要先了解 ETag 首部字段的含义,它是资源的唯一表示。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一表示。 -

+```html +ETag: "82e22293907ce725faf67773957acd12" +``` -持久连接需要使用 Connection 首部字段进行管理。HTTP/1.1 开始 HTTP 默认是持久化连接的,如果要断开 TCP 连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是非持久化连接的,如果要维持持续连接,需要使用 Connection : Keep-Alive。 +可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。 -## 管线化处理 +```html +If-None-Match: "82e22293907ce725faf67773957acd12" +``` -HTTP/1.1 支持管线化处理,可以同时发送多个请求和响应,而不需要发送一个请求然后等待响应之后再发下一个请求。 +Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应, -## 编码 +```html +Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT +``` -编码(Encoding)主要是为了对实体进行压缩。常用的编码有:gzip、compress、deflate、identity,其中 identity 表示不执行压缩的编码格式。 +```html +If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT +``` -## 分块传输编码 +## 连接管理 -Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。 +

-## 多部分对象集合 +### 1. 短连接与长连接 -一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。 +当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 -例如,上传多个表单时可以使用如下方式: +HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。 -```html -Content-Type: multipart/form-data; boundary=AaB03x +### 2. 流水线 ---AaB03x -Content-Disposition: form-data; name="submit-name" +默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 -Larry ---AaB03x -Content-Disposition: form-data; name="files"; filename="file1.txt" -Content-Type: text/plain +流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。 -... contents of file1.txt ... ---AaB03x-- +## 内容协商 + +通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 + +### 1. 类型 + +**(一)服务端驱动型内容协商** + +客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag,服务器根据这些字段返回特定的资源。 + +它存在以下问题: + +- 服务器很难知道客户端浏览器的全部信息; +- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术)。 +- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 + +**(二)代理驱动型协商** + +服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。 + +### 2. Vary + +```html +Vary: Accept-Language ``` +在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。 + +例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。 + +## 内容编码 + +内容编码将实体主体进行压缩,从而减少传输的数据量。常用的内容编码有:gzip、compress、deflate、identity。 + +浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中,Vary 首部中至少要包含 Content-Encoding,这样的话,缓存服务器就可以对资源的不同展现形式进行缓存。 + ## 范围请求 -如果网络出现中断,服务器只发送了一部分数据,范围请求使得客户端能够只请求未发送的那部分数据,从而避免服务器端重新发送所有数据。 +如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。 -在请求报文首部中添加 Range 字段指定请求的范围,请求成功的话服务器发送 206 Partial Content 状态。 +### 1. Range + +在请求报文中添加 Range 首部字段指定请求的范围。 ```html GET /z4d4kWk.jpg HTTP/1.1 @@ -467,6 +570,8 @@ Host: i.imgur.com Range: bytes=0-1023 ``` +请求成功的话服务器返回的响应包含 206 Partial Content 状态码。 + ```html HTTP/1.1 206 Partial Content Content-Range: bytes 0-1023/146515 @@ -475,25 +580,60 @@ Content-Length: 1024 (binary content) ``` -## 内容协商 +### 2. Accept-Ranges -通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 +响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。 + +```html +Accept-Ranges: bytes +``` + +### 3. 响应状态码 + +- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。 +- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。 +- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。 + +## 分块传输编码 -涉及以下首部字段:Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Language。 +Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。 + +## 多部分对象集合 + +一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。 + +例如,上传多个表单时可以使用如下方式: + +```html +Content-Type: multipart/form-data; boundary=AaB03x + +--AaB03x +Content-Disposition: form-data; name="submit-name" + +Larry +--AaB03x +Content-Disposition: form-data; name="files"; filename="file1.txt" +Content-Type: text/plain + +... contents of file1.txt ... +--AaB03x-- +``` ## 虚拟主机 HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。 -使用 Host 首部字段进行处理。 - ## 通信数据转发 ### 1. 代理 代理服务器接受客户端的请求,并且转发给其它服务器。 -使用代理的主要目的是:缓存、网络访问控制以及访问日志记录。 +使用代理的主要目的是: + +- 缓存 +- 网络访问控制 +- 访问日志记录 代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在,而反向代理一般位于内部网络中,用户察觉不到。 @@ -513,13 +653,13 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名, HTTP 有以下安全性问题: -1. 使用明文进行通信,内容可能会被窃听; -2. 不验证通信方的身份,通信方的身份有可能遭遇伪装; -3. 无法证明报文的完整性,报文有可能遭篡改。 +- 使用明文进行通信,内容可能会被窃听; +- 不验证通信方的身份,通信方的身份有可能遭遇伪装; +- 无法证明报文的完整性,报文有可能遭篡改。 -HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。 +HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。 -通过使用 SSL,HTTPs 具有了加密、认证和完整性保护。 +通过使用 SSL,HTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。

@@ -545,7 +685,7 @@ HTTPs 并不是新协议,而是 HTTP 先和 SSL(Secure Sockets Layer)通 ### 3. HTTPs 采用的加密方式 -HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥,之后使用对称密钥加密进行通信。(下图中的 Session Key 就是对称密钥) +HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥来保证安全性,之后使用对称密钥加密进行通信来保证效率。(下图中的 Session Key 就是对称密钥)

@@ -553,59 +693,66 @@ HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称 通过使用 **证书** 来对通信方进行认证。 -数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 +数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。 -进行 HTTPs 通信时,服务器会把证书发送给客户端,客户端取得其中的公开密钥之后,先进行验证,如果验证通过,就可以开始通信。 +服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。 -

+进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 -使用 OpenSSL 这套开源程序,每个人都可以构建一套属于自己的认证机构,从而自己给自己颁发服务器证书。浏览器在访问该服务器时,会显示“无法确认连接安全性”或“该网站的安全证书存在问题”等警告消息。 +

-## 完整性 +## 完整性保护 -SSL 提供报文摘要功能来验证完整性。 +SSL 提供报文摘要功能来进行完整性保护。 -# 七、Web 攻击技术 +HTTP 也提供了 MD5 报文摘要功能,但是却不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生篡改。 -## 攻击模式 +HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 -### 1. 主动攻击 +## HTTPs 的缺点 -直接攻击服务器,具有代表性的有 SQL 注入和 OS 命令注入。 +- 因为需要进行加密解密等过程,因此速度会更慢; +- 需要支付证书授权的高费用。 -### 2. 被动攻击 +## 配置 HTTPs -设下圈套,让用户发送有攻击代码的 HTTP 请求,用户会泄露 Cookie 等个人信息,具有代表性的有跨站脚本攻击和跨站请求伪造。 +[Nginx 配置 HTTPS 服务器](https://aotu.io/notes/2016/08/16/nginx-https/index.html) + +# 七、Web 攻击技术 ## 跨站脚本攻击 ### 1. 概念 -跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。利用网页开发时留下的漏洞,通过巧妙的方法注入恶意指令代码到网页,使用户加载并执行攻击者恶意制造的网页程序。攻击成功后,攻击者可能得到更高的权限(如执行一些操作)、私密网页内容、会话和 Cookie 等各种内容。 +跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。 -例如有一个论坛网站,攻击者可以在上面发表以下内容: +例如有一个论坛网站,攻击者可以在上面发布以下内容: -``` +```html ``` 之后该内容可能会被渲染成以下形式: -``` +```html

``` -另一个用户浏览了含有这个内容的页面将会跳往 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 +另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 ### 2. 危害 -- 伪造虚假的输入表单骗取个人信息 - 窃取用户的 Cookie 值 +- 伪造虚假的输入表单骗取个人信息 - 显示伪造的文章或者图片 ### 3. 防范手段 -(一)过滤特殊字符 +**(一)设置 Cookie 为 HttpOnly** + +设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,在一定程度上可以防止 XSS 窃取用户的 Cookie 信息。 + +**(二)过滤特殊字符** 许多语言都提供了对 HTML 的过滤: @@ -614,23 +761,83 @@ SSL 提供报文摘要功能来验证完整性。 - Java 的 xssprotect (Open Source Library)。 - Node.js 的 node-validator。 -(二)指定 HTTP 的 Content-Type +例如 htmlspecialchars() 可以将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。 + +**(三)富文本编辑器的处理** -通过这种方式,可以避免内容被当成 HTML 解析,比如 PHP 语言可以使用以下代码: +富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。 + +富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。 + +以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。 + +[XSS 过滤在线测试](http://jsxss.com/zh/try.html) + +```html +

XSS Demo

-```php - +

+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist. +

+ +
+ + +
+ +
hello
+ +

+ http +

+ +

Features:

+ + + ``` -## 跨站点请求伪造 +```html +

XSS Demo

+ +

+Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist. +

+ +<form> + <input type="text" name="q" value="test"> + <button id="submit">Submit</button> +</form> + +
hello
+ +

+ http +

+ +

Features:

+ + +<script type="text/javascript"> +alert(/xss/); +</script> +``` + +## 跨站请求伪造 ### 1. 概念 -跨站点请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 +跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。 -XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。 +XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。 假如一家银行用以执行转账操作的 URL 地址如下: @@ -652,21 +859,23 @@ http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName ### 2. 防范手段 -(一)检查 Referer 字段 +**(一)检查 Referer 字段** -HTTP 头中有一个 Referer 字段,这个字段用以标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。 +HTTP 头中有一个 Referer 字段,这个字段用于标明请求来源于哪个地址。在处理敏感数据请求时,通常来说,Referer 字段应和请求的地址位于同一域名下。 这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 -(二)添加校验 Token +**(二)添加校验 Token** 由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。 +也可以要求用户输入验证码来进行校验。 + ## SQL 注入攻击 ### 1. 概念 -服务器上的数据库运行非法的 SQL 语句。 +服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。 ### 2. 攻击原理 @@ -695,26 +904,24 @@ strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1' strSQL = "SELECT * FROM users;" ``` -### 3. 危害 +### 3. 防范手段 -- 数据表中的数据外泄,例如个人机密数据,账户数据,密码等。 -- 数据结构被黑客探知,得以做进一步攻击(例如 SELECT * FROM sys.tables)。 -- 数据库服务器被攻击,系统管理员账户被窜改(例如 ALTER LOGIN sa WITH PASSWORD='xxxxxx')。 -- 获取系统较高权限后,有可能得以在网页加入恶意链接、恶意代码以及 XSS 等。 -- 经由数据库服务器提供的操作系统支持,让黑客得以修改或控制操作系统(例如 xp_cmdshell "net stop iisadmin" 可停止服务器的 IIS 服务)。 -- 破坏硬盘数据,瘫痪全系统(例如 xp_cmdshell "FORMAT C:")。 +**(一)使用参数化查询** -### 4. 防范手段 +以下以 Java 中的 PreparedStatement 为例,它是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。 -- 在设计应用程序时,完全使用参数化查询(Parameterized Query)来设计数据访问功能。 -- 在组合 SQL 字符串时,先针对所传入的参数作字符取代(将单引号字符取代为连续 2 个单引号字符)。 -- 如果使用 PHP 开发网页程序的话,亦可打开 PHP 的魔术引号(Magic quote)功能(自动将所有的网页传入参数,将单引号字符取代为连续 2 个单引号字符)。 -- 其他,使用其他更安全的方式连接 SQL 数据库。例如已修正过 SQL 注入问题的数据库连接组件,例如 ASP.NET 的 SqlDataSource 对象或是 LINQ to SQL。 -- 使用 SQL 防注入系统。 +```java +PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?"); +stmt.setString(1, userid); +stmt.setString(2, password); +ResultSet rs = stmt.executeQuery(); +``` -## 拒绝服务攻击 +**(二)单引号转换** -### 1. 概念 +将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。 + +## 拒绝服务攻击 拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 @@ -724,11 +931,13 @@ strSQL = "SELECT * FROM users;" # 八、GET 和 POST 的区别 -## 参数 +## 作用 -GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在内容实体中。 +GET 用于获取资源,而 POST 用于传输实体主体。 -GET 的传参方式相比于 POST 安全性较差,因为 GET 传的参数在 URL 中是可见的,可能会泄露私密信息。并且 GET 只支持 ASCII 字符,如果参数为中文则可能会出现乱码,而 POST 支持标准字符集。 +## 参数 + +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。 ``` GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 @@ -740,6 +949,10 @@ Host: w3schools.com name1=value1&name2=value2 ``` +不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 + +因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。 + ## 安全 安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 @@ -771,7 +984,7 @@ POST /add_row HTTP/1.1 -> Adds a 2nd row POST /add_row HTTP/1.1 -> Adds a 3rd row ``` -DELETE /idX/delete HTTP/1.1 是幂等的,即便是不同请求之间接收到的状态码不一样: +DELETE /idX/delete HTTP/1.1 是幂等的,即便不同的请求接收到的状态码不一样: ``` DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists @@ -783,9 +996,9 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 如果要对响应进行缓存,需要满足以下条件: -1. 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 -2. 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。 -3. 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 +- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。 +- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。 +- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。 ## XMLHttpRequest @@ -793,45 +1006,57 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 > XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 -在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 +在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。而 GET 方法 Header 和 Data 会一起发送。 + +# 九、HTTP/1.0 与 HTTP/1.1 的区别 + +- HTTP/1.1 默认是持久连接 +- HTTP/1.1 支持管线化处理 +- HTTP/1.1 支持虚拟主机 +- HTTP/1.1 新增状态码 100 +- HTTP/1.1 支持分块传输编码 +- HTTP/1.1 新增缓存处理指令 max-age -# 九、各版本比较 +# 十、HTTP/2.0 -## HTTP/1.0 与 HTTP/1.1 的区别 +## HTTP/1.x 缺陷 -1. HTTP/1.1 默认是持久连接 -2. HTTP/1.1 支持管线化处理 -3. HTTP/1.1 支持虚拟主机 -4. HTTP/1.1 新增状态码 100 -5. HTTP/1.1 支持分块传输编码 -6. HTTP/1.1 新增缓存处理指令 max-age + HTTP/1.x 实现简单是以牺牲应用性能为代价的: -具体内容见上文 +- 客户端需要使用多个连接才能实现并发和缩短延迟; +- 不会压缩请求和响应首部,从而导致不必要的网络流量; +- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 -## HTTP/1.1 与 HTTP/2.0 的区别 +## 二进制分帧层 -> [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) +HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 -### 1. 多路复用 +

-HTTP/2.0 使用多路复用技术,同一个 TCP 连接可以处理多个请求。 +在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 -### 2. 首部压缩 +

-HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求通讯双方各自缓存一份首部字段表,从而避免了重复传输。 +## 服务端推送 -### 3. 服务端推送 +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 -HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 index.html 页面,服务端就把 index.js 一起发给客户端。 +

-### 4. 二进制格式 +## 首部压缩 -HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 +HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 + +

# 参考资料 - 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014. - [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) +- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) +- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php) +- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement) +- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html) - [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/) - [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html) - [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/) @@ -842,6 +1067,10 @@ HTTP/1.1 的解析是基于文本的,而 HTTP/2.0 采用二进制格式。 - [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html) - [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication) - [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/) +- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment) +- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827) +- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html) +- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/) - [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC) - [维基百科:SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A) - [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0) diff --git "a/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" index 92f53cb3b92726f2cf058483de88e7b8f2729758..0d858e4bd7f695d8bda1801d2df1d6226e9949c8 100644 --- "a/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/notes/JDK \344\270\255\347\232\204\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -138,7 +138,7 @@ java.util.Enumeration ## 5. 中间人模式 -使用中间人对象来封装对象之间的交互。中间人模式可以让降低交互对象之间的耦合程度。 +使用中间人对象来封装对象之间的交互。中间人模式可以降低交互对象之间的耦合程度。 ```java java.util.Timer diff --git a/notes/Java IO.md b/notes/Java IO.md index b48c90c3041e46d49b470ecd7e2423b63bf092fa..885ecbe1b0eef98e705fcf2e03a5981f36f99089 100644 --- a/notes/Java IO.md +++ b/notes/Java IO.md @@ -14,6 +14,7 @@ * [通道与缓冲区](#通道与缓冲区) * [缓冲区状态变量](#缓冲区状态变量) * [文件 NIO 实例](#文件-nio-实例) + * [选择器](#选择器) * [套接字 NIO 实例](#套接字-nio-实例) * [内存映射文件](#内存映射文件) * [对比](#对比) @@ -74,10 +75,16 @@ byte[] bytes = str.getBytes(encoding); // 编码 String str = new String(bytes, encoding); // 解码 ``` -GBK 编码中,中文占 2 个字节,英文占 1 个字节;UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;Java 使用双字节编码 UTF-16be,中文和英文都占 2 个字节。 - 如果编码和解码过程使用不同的编码方式那么就出现了乱码。 +- GBK 编码中,中文占 2 个字节,英文占 1 个字节; +- UTF-8 编码中,中文占 3 个字节,英文占 1 个字节; +- UTF-16be 编码中,中文和英文都占 2 个字节。 + +UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。 + +Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码正是为了让一个中文或者一个英文都能使用一个 char 来存储。 + # 五、对象操作 序列化就是将一个对象转换成字节序列,方便存储和传输。 @@ -148,15 +155,19 @@ is.close(); # 七、NIO -新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,它在标准 Java 代码中提供了高速的、面向块的 I/O。 +- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html) +- [Java NIO 浅析](https://tech.meituan.com/nio.html) +- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html) + +新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 ## 流与块 I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 -面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责单个复杂处理机制的一部分,这样也是相对简单的。不利的一面是,面向流的 I/O 通常相当慢。 +面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 -一个面向块的 I/O 系统以块的形式处理数据,一次处理一个数据块。按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 +面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 @@ -177,7 +188,7 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 ### 2. 缓冲区 -发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 +发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。 缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 @@ -203,11 +214,11 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重

-② 从输入通道中读取 3 个字节数据写入缓冲区中,此时 position 移动设为 3,limit 保持不变。 +② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5,limit 保持不变。 -

+

-③ 以下图例为已经从输入通道读取了 5 个字节数据写入缓冲区中。在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。 +③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。

@@ -221,153 +232,206 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 ## 文件 NIO 实例 -① 为要读取的文件创建 FileInputStream,之后通过 FileInputStream 获取输入 FileChannel; +以下展示了使用 NIO 快速复制文件的实例: ```java -FileInputStream fin = new FileInputStream("readandshow.txt"); -FileChannel fic = fin.getChannel(); -``` - -② 创建一个容量为 1024 的 Buffer; - -```java -ByteBuffer buffer = ByteBuffer.allocate(1024); -``` - -③ 将数据从输入 FileChannel 写入到 Buffer 中,如果没有数据的话,read() 方法会返回 -1; - -```java -int r = fcin.read(buffer); -if (r == -1) { - break; +public class FastCopyFile { + public static void main(String args[]) throws Exception { + + String inFile = "data/abc.txt"; + String outFile = "data/abc-copy.txt"; + + // 获得源文件的输入字节流 + FileInputStream fin = new FileInputStream(inFile); + // 获取输入字节流的文件通道 + FileChannel fcin = fin.getChannel(); + + // 获取目标文件的输出字节流 + FileOutputStream fout = new FileOutputStream(outFile); + // 获取输出字节流的通道 + FileChannel fcout = fout.getChannel(); + + // 为缓冲区分配 1024 个字节 + ByteBuffer buffer = ByteBuffer.allocateDirect(1024); + + while (true) { + // 从输入通道中读取数据到缓冲区中 + int r = fcin.read(buffer); + // read() 返回 -1 表示 EOF + if (r == -1) + break; + // 切换读写 + buffer.flip(); + // 把缓冲区的内容写入输出文件中 + fcout.write(buffer); + // 清空缓冲区 + buffer.clear(); + } + } } ``` -④ 为要写入的文件创建 FileOutputStream,之后通过 FileOutputStream 获取输出 FileChannel +## 选择器 -```java -FileOutputStream fout = new FileOutputStream("writesomebytes.txt"); -FileChannel foc = fout.getChannel(); -``` +一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去检查多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。 -⑤ 调用 flip() 切换读写 +因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。 -```java -buffer.flip(); -``` +

-⑥ 把 Buffer 中的数据读取到输出 FileChannel 中 +### 1. 创建选择器 ```java -foc.write(buffer); +Selector selector = Selector.open(); ``` -⑦ 最后调用 clear() 重置缓冲区 +### 2. 将通道注册到选择器上 ```java -buffer.clear(); +ServerSocketChannel ssChannel = ServerSocketChannel.open(); +ssChannel.configureBlocking(false); +ssChannel.register(selector, SelectionKey.OP_ACCEPT); ``` -## 套接字 NIO 实例 +通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它时间,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。 -### 1. ServerSocketChannel +在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类: -每一个监听端口都需要有一个 ServerSocketChannel 用来监听连接。 +- SelectionKey.OP_CONNECT +- SelectionKey.OP_ACCEPT +- SelectionKey.OP_READ +- SelectionKey.OP_WRITE -```java -ServerSocketChannel ssc = ServerSocketChannel.open(); -ssc.configureBlocking(false); // 设置为非阻塞 +它们在 SelectionKey 的定义如下: -ServerSocket ss = ssc.socket(); -InetSocketAddress address = new InetSocketAddress(ports[i]); -ss.bind(address); // 绑定端口号 +```java +public static final int OP_READ = 1 << 0; +public static final int OP_WRITE = 1 << 2; +public static final int OP_CONNECT = 1 << 3; +public static final int OP_ACCEPT = 1 << 4; ``` -### 2. Selectors - -异步 I/O 通过 Selector 注册对特定 I/O 事件的兴趣 ― 可读的数据的到达、新的套接字连接等等,在发生这样的事件时,系统将会发送通知。 - -创建 Selectors 之后,就可以对不同的通道对象调用 register() 方法。register() 的第一个参数总是这个 Selector。第二个参数是 OP_ACCEPT,这里它指定我们想要监听 ACCEPT 事件,也就是在新的连接建立时所发生的事件。 - -SelectionKey 代表这个通道在此 Selector 上的这个注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。 +可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如: ```java -Selector selector = Selector.open(); -SelectionKey key = ssc.register(selector, SelectionKey.OP_ACCEPT); +int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE; ``` -### 3. 主循环 - -首先,我们调用 Selector 的 select() 方法。这个方法会阻塞,直到至少有一个已注册的事件发生。当一个或者更多的事件发生时,select() 方法将返回所发生的事件的数量。 - -接下来,我们调用 Selector 的 selectedKeys() 方法,它返回发生了事件的 SelectionKey 对象的一个集合。 - -我们通过迭代 SelectionKeys 并依次处理每个 SelectionKey 来处理事件。对于每一个 SelectionKey,您必须确定发生的是什么 I/O 事件,以及这个事件影响哪些 I/O 对象。 +### 3. 监听事件 ```java int num = selector.select(); - -Set selectedKeys = selector.selectedKeys(); -Iterator it = selectedKeys.iterator(); - -while (it.hasNext()) { - SelectionKey key = (SelectionKey)it.next(); - // ... deal with I/O event ... -} ``` -### 4. 监听新连接 +使用 select() 来监听事件到达,它会一直阻塞直到有至少一个事件到达。 -程序执行到这里,我们仅注册了 ServerSocketChannel,并且仅注册它们“接收”事件。为确认这一点,我们对 SelectionKey 调用 readyOps() 方法,并检查发生了什么类型的事件: +### 4. 获取到达的事件 ```java -if ((key.readyOps() & SelectionKey.OP_ACCEPT) - == SelectionKey.OP_ACCEPT) { - // Accept the new connection - // ... +Set keys = selector.selectedKeys(); +Iterator keyIterator = keys.iterator(); +while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isAcceptable()) { + // ... + } else if (key.isReadable()) { + // ... + } + keyIterator.remove(); } ``` -可以肯定地说,readOps() 方法告诉我们该事件是新的连接。 - -### 5. 接受新的连接 - -因为我们知道这个服务器套接字上有一个传入连接在等待,所以可以安全地接受它;也就是说,不用担心 accept() 操作会阻塞: +### 5. 事件循环 -```java -ServerSocketChannel ssc = (ServerSocketChannel)key.channel(); -SocketChannel sc = ssc.accept(); -``` - -下一步是将新连接的 SocketChannel 配置为非阻塞的。而且由于接受这个连接的目的是为了读取来自套接字的数据,所以我们还必须将 SocketChannel 注册到 Selector 上,如下所示: +因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。 ```java -sc.configureBlocking(false); -SelectionKey newKey = sc.register(selector, SelectionKey.OP_READ); +while (true) { + int num = selector.select(); + Set keys = selector.selectedKeys(); + Iterator keyIterator = keys.iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isAcceptable()) { + // ... + } else if (key.isReadable()) { + // ... + } + keyIterator.remove(); + } +} ``` -注意我们使用 register() 的 OP_READ 参数,将 SocketChannel 注册用于读取而不是接受新连接。 - -### 6. 删除处理过的 SelectionKey - -在处理 SelectionKey 之后,我们几乎可以返回主循环了。但是我们必须首先将处理过的 SelectionKey 从选定的键集合中删除。如果我们没有删除处理过的键,那么它仍然会在主集合中以一个激活的键出现,这会导致我们尝试再次处理它。我们调用迭代器的 remove() 方法来删除处理过的 SelectionKey: +## 套接字 NIO 实例 ```java -it.remove(); +public class NIOServer { + + public static void main(String[] args) throws IOException { + Selector selector = Selector.open(); + + ServerSocketChannel ssChannel = ServerSocketChannel.open(); + ssChannel.configureBlocking(false); + ssChannel.register(selector, SelectionKey.OP_ACCEPT); + + ServerSocket serverSocket = ssChannel.socket(); + InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888); + serverSocket.bind(address); + + while (true) { + selector.select(); + Set keys = selector.selectedKeys(); + Iterator keyIterator = keys.iterator(); + while (keyIterator.hasNext()) { + SelectionKey key = keyIterator.next(); + if (key.isAcceptable()) { + ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); + // 服务器会为每个新连接创建一个 SocketChannel + SocketChannel sChannel = ssChannel1.accept(); + sChannel.configureBlocking(false); + // 这个新连接主要用于从客户端读取数据 + sChannel.register(selector, SelectionKey.OP_READ); + } else if (key.isReadable()) { + SocketChannel sChannel = (SocketChannel) key.channel(); + System.out.println(readDataFromSocketChannel(sChannel)); + sChannel.close(); + } + keyIterator.remove(); + } + } + } + + private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { + ByteBuffer buffer = ByteBuffer.allocate(1024); + StringBuffer data = new StringBuffer(); + while (true) { + buffer.clear(); + int n = sChannel.read(buffer); + if (n == -1) + break; + buffer.flip(); + int limit = buffer.limit(); + char[] dst = new char[limit]; + for (int i = 0; i < limit; i++) + dst[i] = (char) buffer.get(i); + data.append(dst); + buffer.clear(); + } + return data.toString(); + } +} ``` -现在我们可以返回主循环并接受从一个套接字中传入的数据 (或者一个传入的 I/O 事件) 了。 - -### 7. 传入的 I/O - -当来自一个套接字的数据到达时,它会触发一个 I/O 事件。这会导致在主循环中调用 Selector.select(),并返回一个或者多个 I/O 事件。这一次, SelectionKey 将被标记为 OP_READ 事件,如下所示: - ```java -} else if ((key.readyOps() & SelectionKey.OP_READ) - == SelectionKey.OP_READ) { - // Read the data - SocketChannel sc = (SocketChannel)key.channel(); - // ... +public class NIOClient { + + public static void main(String[] args) throws IOException { + Socket socket = new Socket("127.0.0.1", 8888); + OutputStream out = socket.getOutputStream(); + String s = "hello world"; + out.write(s.getBytes()); + out.close(); + } } ``` diff --git "a/notes/Java \345\237\272\347\241\200.md" "b/notes/Java \345\237\272\347\241\200.md" index 34bd383e848b0d4f4966435cf64ffe72f5d9b811..6424600aad576316e2533e7c4aedfaab0eed3088 100644 --- "a/notes/Java \345\237\272\347\241\200.md" +++ "b/notes/Java \345\237\272\347\241\200.md" @@ -27,7 +27,7 @@ * [十一、特性](#十一特性) * [面向对象三大特性](#面向对象三大特性) * [Java 各版本的新特性](#java-各版本的新特性) - * [Java 与 C++ 的区别](#java-与-c++-的区别) + * [Java 与 C++ 的区别](#java-与-c-的区别) * [JRE or JDK](#jre-or-jdk) * [参考资料](#参考资料) @@ -51,7 +51,7 @@ final A y = new A(); y.a = 1; ``` -**2. 方法**
+**2. 方法** 声明方法不能被子类覆盖。 @@ -79,7 +79,7 @@ public class A { **2. 静态方法** -静态方法在类加载的时候就存在了,它不依赖于任何实例,所以 static 方法必须实现,也就是说它不能是抽象方法(abstract)。 +静态方法在类加载的时候就存在了,它不依赖于任何实例,所以静态方法必须有实现,也就是说它不能是抽象方法(abstract)。 **3. 静态语句块** @@ -87,11 +87,11 @@ public class A { **4. 静态内部类** -内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非 static 变量和方法。 +内部类的一种,静态内部类不依赖外部类,且不能访问外部类的非静态的变量和方法。 **5. 静态导包** -```source-java +```java import static com.xxx.ClassName.* ``` @@ -131,12 +131,12 @@ public InitialOrderTest() { 存在继承的情况下,初始化顺序为: -1. 父类(静态变量、静态语句块) -2. 子类(静态变量、静态语句块) -3. 父类(实例变量、普通语句块) -4. 父类(构造函数) -5. 子类(实例变量、普通语句块) -6. 子类(构造函数) +- 父类(静态变量、静态语句块) +- 子类(静态变量、静态语句块) +- 父类(实例变量、普通语句块) +- 父类(构造函数) +- 子类(实例变量、普通语句块) +- 子类(构造函数) # 二、Object 通用方法 @@ -191,15 +191,14 @@ x.equals(x); // true (二)对称性 ```java -x.equals(y) == y.equals(x) // true +x.equals(y) == y.equals(x); // true ``` (三)传递性 ```java -if(x.equals(y) && y.equals(z)) { +if (x.equals(y) && y.equals(z)) x.equals(z); // true; -} ``` (四)一致性 @@ -255,7 +254,7 @@ public class EqualExample { hasCode() 返回散列值,而 equals() 是用来判断两个实例是否等价。等价的两个实例散列值一定要相同,但是散列值相同的两个实例不一定等价。 -在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证相等的两个实例散列值也等价。 +在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个实例散列值也相等。 下面的代码中,新建了两个等价的实例,并将它们添加到 HashSet 中。我们希望将这两个实例当成一样的,只在集合中添加一个实例,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个实例的散列值是不同的,最终导致集合添加了两个等价的实例。 @@ -271,7 +270,7 @@ System.out.println(set.size()); // 2 理想的散列函数应当具有均匀性,即不相等的实例应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。 -一个数与 31 相乘可以转换成移位和减法:31\*x == (x<<5)-x。 +一个数与 31 相乘可以转换成移位和减法:`31\*x == (x<<5)-x`,编译器会自动进行这个优化。 ```java @Override @@ -593,9 +592,9 @@ ac2.func1(); 从 Java 8 开始,接口也可以拥有默认的方法实现,这是因为不支持默认方法的接口的维护成本太高了。在 Java 8 之前,如果一个接口想要添加新的方法,那么要修改所有实现了该接口的类。 -接口也可以包含字段,并且这些字段隐式都是 static 和 final 的。 +接口的成员(字段 + 方法)默认都是 public 的,并且不允许定义为 private 或者 protected。 -接口中的方法默认都是 public 的,并且不允许定义为 private 或者 protected。 +接口的字段默认都是 static 和 final 的。 ```java public interface InterfaceExample { @@ -606,7 +605,7 @@ public interface InterfaceExample { } int x = 123; - //int y; // Variable 'y' might not have been initialized + // int y; // Variable 'y' might not have been initialized public int z = 0; // Modifier 'public' is redundant for interface fields // private int k = 0; // Modifier 'private' not allowed here // protected int l = 0; // Modifier 'protected' not allowed here @@ -892,15 +891,15 @@ switch (s) { switch 不支持 long,是因为 swicth 的设计初衷是为那些只需要对少数的几个值进行等值判断,如果值过于复杂,那么还是用 if 比较合适。 ```java -// long x = 111; -// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum' -// case 111: -// System.out.println(111); -// break; -// case 222: -// System.out.println(222); -// break; -// } +// long x = 111; +// switch (x) { // Incompatible types. Found: 'long', required: 'char, byte, short, int, Character, Byte, Short, Integer, String, or an enum' +// case 111: +// System.out.println(111); +// break; +// case 222: +// System.out.println(222); +// break; +// } ``` > [Why can't your switch statement data type be long, Java?](https://stackoverflow.com/questions/2676210/why-cant-your-switch-statement-data-type-be-long-java) @@ -959,7 +958,7 @@ public class Box { } ``` -> [Java 泛型详解](https://www.ziwenxie.site/2017/03/01/java-generic/)
[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) +> [Java 泛型详解](http://www.importnew.com/24029.html)
[10 道 Java 泛型面试题](https://cloud.tencent.com/developer/article/1033693) # 十、注解 diff --git "a/notes/Java \345\256\271\345\231\250.md" "b/notes/Java \345\256\271\345\231\250.md" index 6791e2ed8ffb92eeec6fdb6401b72d6a48631b5d..03130b849be94e4a786e566a0981cb21fd4886a1 100644 --- "a/notes/Java \345\256\271\345\231\250.md" +++ "b/notes/Java \345\256\271\345\231\250.md" @@ -9,11 +9,10 @@ * [ArrayList](#arraylist) * [Vector](#vector) * [LinkedList](#linkedlist) + * [HashMap](#hashmap) + * [ConcurrentHashMap](#concurrenthashmap) * [LinkedHashMap](#linkedhashmap) * [TreeMap](#treemap) - * [HashMap](#hashmap) - * [ConcurrentHashMap - JDK 1.7](#concurrenthashmap---jdk-17) - * [ConcurrentHashMap - JDK 1.8](#concurrenthashmap---jdk-18) * [参考资料](#参考资料) @@ -28,7 +27,7 @@ ### 1. Set -- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 +- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的; - TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN); @@ -40,7 +39,7 @@ - Vector:和 ArrayList 类似,但它是线程安全的; -- LinkedList:基于双向循环链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 +- LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 ### 3. Queue @@ -122,39 +121,41 @@ public class ArrayList extends AbstractList implements List, RandomAccess, Cloneable, java.io.Serializable ``` -基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 - -```java -transient Object[] elementData; // non-private to simplify nested class access -``` - 数组的默认大小为 10。 ```java private static final int DEFAULT_CAPACITY = 10; ``` -删除元素时需要调用 System.arraycopy() 对元素进行复制,因此删除操作成本很高。 +### 2. 序列化 + +基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 ```java -public E remove(int index) { - rangeCheck(index); +transient Object[] elementData; // non-private to simplify nested class access +``` - modCount++; - E oldValue = elementData(index); +### 3. 扩容 - int numMoved = size - index - 1; - if (numMoved > 0) - System.arraycopy(elementData, index+1, elementData, index, numMoved); - elementData[--size] = null; // clear to let GC do its work +添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。 - return oldValue; +扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 + +```java +public boolean add(E e) { + ensureCapacityInternal(size + 1); // Increments modCount!! + elementData[size++] = e; + return true; } -``` -添加元素时使用 ensureCapacity() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,使得新容量为旧容量的 1.5 倍(oldCapacity + (oldCapacity >> 1))。扩容操作需要把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 +private void ensureCapacityInternal(int minCapacity) { + if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { + minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); + } + + ensureExplicitCapacity(minCapacity); +} -```java private void ensureExplicitCapacity(int minCapacity) { modCount++; @@ -176,7 +177,27 @@ private void grow(int minCapacity) { } ``` -### 2. Fail-Fast +### 4. 删除元素 + +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,复制的代价很高。 + +```java +public E remove(int index) { + rangeCheck(index); + + modCount++; + E oldValue = elementData(index); + + int numMoved = size - index - 1; + if (numMoved > 0) + System.arraycopy(elementData, index+1, elementData, index, numMoved); + elementData[--size] = null; // clear to let GC do its work + + return oldValue; +} +``` + +### 5. Fail-Fast modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 @@ -203,39 +224,85 @@ private void writeObject(java.io.ObjectOutputStream s) } ``` -### 3. 和 Vector 的区别 +## Vector + +[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java) + +### 1. 同步 + +它的实现与 ArrayList 类似,但是使用了 synchronized 进行同步。 + +```java +public synchronized boolean add(E e) { + modCount++; + ensureCapacityHelper(elementCount + 1); + elementData[elementCount++] = e; + return true; +} + +public synchronized E get(int index) { + if (index >= elementCount) + throw new ArrayIndexOutOfBoundsException(index); + + return elementData(index); +} +``` + +### 2. ArrayList 与 Vector - Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制; - Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。 -为了获得线程安全的 ArrayList,可以调用 Collections.synchronizedList(new ArrayList<>()); 返回一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类; +### 3. Vector 替代方案 -### 4. 和 LinkedList 的区别 +为了获得线程安全的 ArrayList,可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类; -- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现; -- ArrayList 支持随机访问,LinkedList 不支持; -- LinkedList 在任意位置添加删除元素更快。 - -## Vector +```java +List list = new ArrayList<>(); +List synList = Collections.synchronizedList(list); +``` -[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java) +```java +List list = new CopyOnWriteArrayList(); +``` ## LinkedList [LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java) -## LinkedHashMap +### 1. 概览 -[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) +基于双向链表实现,内部使用 Node 来存储链表节点信息。 -## TreeMap +```java +private static class Node { + E item; + Node next; + Node prev; +} +``` -[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) +每个链表存储了 Head 和 Tail 指针: + +```java +transient Node first; +transient Node last; +``` + +

+ +### 2. ArrayList 与 LinkedList + +- ArrayList 基于动态数组实现,LinkedList 基于双向链表实现; +- ArrayList 支持随机访问,LinkedList 不支持; +- LinkedList 在任意位置添加删除元素更快。 ## HashMap [HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) +为了便于理解,以下内容以 JDK 1.7 为主。 + ### 1. 存储结构 使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶。 @@ -248,28 +315,26 @@ transient Entry[] table;

-JDK 1.8 使用 Node 类型存储一个键值对,它依然继承自 Entry,因此可以按照上面的存储结构来理解。 - ```java -static class Node implements Map.Entry { - final int hash; +static class Entry implements Map.Entry { final K key; V value; - Node next; - - Node(int hash, K key, V value, Node next) { - this.hash = hash; - this.key = key; - this.value = value; - this.next = next; + Entry next; + int hash; + + Entry(int h, K k, V v, Entry n) { + value = v; + next = n; + key = k; + hash = h; } - public final K getKey() { return key; } - public final V getValue() { return value; } - public final String toString() { return key + "=" + value; } + public final K getKey() { + return key; + } - public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); + public final V getValue() { + return value; } public final V setValue(V newValue) { @@ -279,16 +344,42 @@ static class Node implements Map.Entry { } public final boolean equals(Object o) { - if (o == this) - return true; - if (o instanceof Map.Entry) { - Map.Entry e = (Map.Entry)o; - if (Objects.equals(key, e.getKey()) && - Objects.equals(value, e.getValue())) + if (!(o instanceof Map.Entry)) + return false; + Map.Entry e = (Map.Entry)o; + Object k1 = getKey(); + Object k2 = e.getKey(); + if (k1 == k2 || (k1 != null && k1.equals(k2))) { + Object v1 = getValue(); + Object v2 = e.getValue(); + if (v1 == v2 || (v1 != null && v1.equals(v2))) return true; } return false; } + + public final int hashCode() { + return Objects.hashCode(getKey()) ^ Objects.hashCode(getValue()); + } + + public final String toString() { + return getKey() + "=" + getValue(); + } + + /** + * This method is invoked whenever the value in an entry is + * overwritten by an invocation of put(k,v) for a key k that's already + * in the HashMap. + */ + void recordAccess(HashMap m) { + } + + /** + * This method is invoked whenever the entry is + * removed from the table. + */ + void recordRemoval(HashMap m) { + } } ``` @@ -304,22 +395,166 @@ map.put("K3", "V3"); - 新建一个 HashMap,默认大小为 16; - 插入 <K1,V1> 键值对,先计算 K1 的 hashCode 为 115,使用除留余数法得到所在的桶下标 115%16=3。 - 插入 <K2,V2> 键值对,先计算 K2 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6。 -- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 后面。 +- 插入 <K3,V3> 键值对,先计算 K3 的 hashCode 为 118,使用除留余数法得到所在的桶下标 118%16=6,插在 <K2,V2> 前面。 -

+应该注意到链表的插入是以头插法方式进行的,例如上面的 不是插在 后面,而是插入在链表头部。 查找需要分成两步进行: - 计算键值对所在的桶; - 在链表上顺序查找,时间复杂度显然和链表的长度成正比。 -### 3. 链表转红黑树 +

-应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。 +### 3. put 操作 + +```java +public V put(K key, V value) { + if (table == EMPTY_TABLE) { + inflateTable(threshold); + } + // 键为 null 单独处理 + if (key == null) + return putForNullKey(value); + int hash = hash(key); + // 确定桶下标 + int i = indexFor(hash, table.length); + // 先找出是否已经存在键位 key 的键值对,如果存在的话就更新这个键值对的值为 value + for (Entry e = table[i]; e != null; e = e.next) { + Object k; + if (e.hash == hash && ((k = e.key) == key || key.equals(k))) { + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + + modCount++; + // 插入新键值对 + addEntry(hash, key, value, i); + return null; +} +``` + +HashMap 允许插入键位 null 的键值对,因为无法调用 null 的 hashCode(),也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 + +```java +private V putForNullKey(V value) { + for (Entry e = table[0]; e != null; e = e.next) { + if (e.key == null) { + V oldValue = e.value; + e.value = value; + e.recordAccess(this); + return oldValue; + } + } + modCount++; + addEntry(0, null, value, 0); + return null; +} +``` + +使用链表的头插法,也就是新的键值对插在链表的头部,而不是链表的尾部。 + +```java +void addEntry(int hash, K key, V value, int bucketIndex) { + if ((size >= threshold) && (null != table[bucketIndex])) { + resize(2 * table.length); + hash = (null != key) ? hash(key) : 0; + bucketIndex = indexFor(hash, table.length); + } + + createEntry(hash, key, value, bucketIndex); +} + +void createEntry(int hash, K key, V value, int bucketIndex) { + Entry e = table[bucketIndex]; + // 头插法,链表头部指向新的键值对 + table[bucketIndex] = new Entry<>(hash, key, value, e); + size++; +} +``` + +```java +Entry(int h, K k, V v, Entry n) { + value = v; + next = n; + key = k; + hash = h; +} +``` -### 4. 扩容 +### 4. 确定桶下标 -因为从 JDK 1.8 开始引入了红黑树,因此扩容操作较为复杂,为了便于理解,以下内容使用 JDK 1.7 的内容。 +很多操作都需要先确定一个键值对所在的桶下标。 + +```java +int hash = hash(key); +int i = indexFor(hash, table.length); +``` + +(一)计算 hash 值 + +```java +final int hash(Object k) { + int h = hashSeed; + if (0 != h && k instanceof String) { + return sun.misc.Hashing.stringHash32((String) k); + } + + h ^= k.hashCode(); + + // This function ensures that hashCodes that differ only by + // constant multiples at each bit position have a bounded + // number of collisions (approximately 8 at default load factor). + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); +} +``` + +```java +public final int hashCode() { + return Objects.hashCode(key) ^ Objects.hashCode(value); +} +``` + +(二)取模 + +令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: + +``` +x : 00010000 +x-1 : 00001111 +``` + +令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数: + +``` +y : 10110010 +x-1 : 00001111 +y&(x-1) : 00000010 +``` + +这个性质和 y 对 x 取模效果是一样的: + +``` +x : 00010000 +y : 10110010 +y%x : 00000010 +``` + +我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。 + +确定桶下标的最后一步是将 key 的 hash 值对桶个数取模:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 + +```java +static int indexFor(int h, int length) { + return h & (length-1); +} +``` + +### 5. 扩容-基本原理 设 HashMap 的 table 长度为 M,需要存储的键值对数量为 N,如果哈希函数满足均匀性的要求,那么每条链表的长度大约为 N/M,因此平均查找次数的复杂度为 O(N/M)。 @@ -399,69 +634,9 @@ void transfer(Entry[] newTable) { } ``` -### 5. 确定桶下标 - -很多操作都需要先确定一个键值对所在的桶下标,这个操作需要分三步进行。 - -(一)调用 hashCode() - -```java -public final int hashCode() { - return Objects.hashCode(key) ^ Objects.hashCode(value); -} -``` - -(二)高位运算 - -将 hashCode 的高 16 位和低 16 位进行异或操作,使得在数组比较小时,也能保证高低位都参与到了哈希计算中。 - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -(三)除留余数 - -令 x = 1<<4,即 x 为 2 的 4 次方,它具有以下性质: - -``` -x : 00010000 -x-1 : 00001111 -``` - -令一个数 y 与 x-1 做与运算,可以去除 y 位级表示的第 4 位以上数: - -``` -y : 10110010 -x-1 : 00001111 -y&(x-1) : 00000010 -``` - -这个性质和 y 对 x 取模效果是一样的: - -``` -x : 00010000 -y : 10110010 -y%x : 00000010 -``` - -我们知道,位运算的代价比求模运算小的多,因此在进行这种计算时能用位运算的话能带来更高的性能。 - -拉链法需要使用除留余数法来得到桶下标,也就是需要进行以下计算:hash%capacity,如果能保证 capacity 为 2 的幂次方,那么就可以将这个操作转换位位运算。 - -以下操作在 JDK 1.8 中没有,但是原理上相同。 - -```java -static int indexFor(int h, int length) { - return h & (length-1); -} -``` - ### 6. 扩容-重新计算桶下标 -在进行扩容时,需要把 Node 重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。 +在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。 假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16: @@ -470,7 +645,7 @@ capacity : 00010000 new capacity : 00100000 ``` -对于一个 Key,它的 hashCode 如果在第 6 位上为 0,那么除留余数得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。 +对于一个 Key,它的 hash 如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。 ### 7. 扩容-计算数组容量 @@ -505,11 +680,11 @@ static final int tableSizeFor(int cap) { } ``` -### 7. null 值 +### 8. 链表转红黑树 -HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 个桶的位置,因为这个 Key 无法计算 hashCode(),因此只能规定一个桶让它存放。 +应该注意到,从 JDK 1.8 开始,一个桶存储的链表长度大于 8 时会将链表转换为红黑树。 -### 8. 与 HashTable 的区别 +### 9. HashMap 与 HashTable - HashTable 是同步的,它使用了 synchronized 来进行同步。它也是线程安全的,多个线程可以共享同一个 HashTable。HashMap 不是同步的,但是可以使用 ConcurrentHashMap,它是 HashTable 的替代,而且比 HashTable 可扩展性更好。 - HashMap 可以插入键为 null 的 Entry。 @@ -517,18 +692,12 @@ HashMap 允许有一个 Node 的 Key 为 null,该 Node 一定会放在第 0 - 由于 Hashtable 是线程安全的也是 synchronized,所以在单线程环境下它比 HashMap 要慢。 - HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的。 -## ConcurrentHashMap - JDK 1.7 +## ConcurrentHashMap [ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java) -ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶。 - -相比于 HashTable 和用同步包装器包装的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 拥有更高的并发性。在 HashTable 和由同步包装器包装的 HashMap 中,使用一个全局的锁来同步不同线程间的并发访问。同一时间点,只能有一个线程持有锁,也就是说在同一时间点,只能有一个线程能访问容器。这虽然保证多线程间的安全并发访问,但同时也导致对容器的访问变成串行化的了。 - ### 1. 存储结构 -和 HashMap 类似。 - ```java static final class HashEntry { final int hash; @@ -540,6 +709,8 @@ static final class HashEntry { Segment 继承自 ReentrantLock,每个 Segment 维护着多个 HashEntry。 +ConcurrentHashMap 和 HashMap 实现上类似,最主要的差别是 ConcurrentHashMap 采用了分段锁,每个分段锁维护着几个桶,多个线程可以同时访问不同分段锁上的桶,从而使其并发度更高(并发度就是 Segment 的个数)。 + ```java static final class Segment extends ReentrantLock implements Serializable { @@ -572,100 +743,97 @@ static final int DEFAULT_CONCURRENCY_LEVEL = 16;

-### 2. HashEntry 的不可变性 - -HashEntry 类的 value 域被声明为 Volatile 型,Java 的内存模型可以保证:某个写线程对 value 域的写入马上可以被后续的某个读线程 “看” 到。在 ConcurrentHashMap 中,不允许用 null 作为键和值,当读线程读到某个 HashEntry 的 value 域的值为 null 时,便知道产生了冲突——发生了重排序现象,需要加锁后重新读入这个 value 值。这些特性互相配合,使得读线程即使在不加锁状态下,也能正确访问 ConcurrentHashMap。 - -非结构性修改操作只是更改某个 HashEntry 的 value 域的值。由于对 Volatile 变量的写入操作将与随后对这个变量的读操作进行同步。当一个写线程修改了某个 HashEntry 的 value 域后,另一个读线程读这个值域,Java 内存模型能够保证读线程读取的一定是更新后的值。所以,写线程对链表的非结构性修改能够被后续不加锁的读线程 “看到”。 - -对 ConcurrentHashMap 做结构性修改,实质上是对某个桶指向的链表做结构性修改。如果能够确保:在读线程遍历一个链表期间,写线程对这个链表所做的结构性修改不影响读线程继续正常遍历这个链表。那么读 / 写线程之间就可以安全并发访问这个 ConcurrentHashMap。 - -结构性修改操作包括 put,remove,clear。下面我们分别分析这三个操作。 - -clear 操作只是把 ConcurrentHashMap 中所有的桶 “置空”,每个桶之前引用的链表依然存在,只是桶不再引用到这些链表(所有链表的结构并没有被修改)。正在遍历某个链表的读线程依然可以正常执行对该链表的遍历。 - -put 操作如果需要插入一个新节点到链表中时 , 会在链表头部插入这个新节点。此时,链表中的原有节点的链接并没有被修改。也就是说:插入新健 / 值对到链表中的操作不会影响读线程正常遍历这个链表。 - -在以下链表中删除 C 节点,C 节点之后的所有节点都原样保留,C 节点之前的所有节点都被克隆到新的链表中,并且顺序被反转。可以注意到,在执行 remove 操作时,原始链表并没有被修改,也就是说,读线程不会受到执行 remove 操作的并发写线程的干扰。 +### 2. size 操作 -

+每个 Segment 维护了一个 count 变量来统计该 Segment 中的键值对个数。 -

- -综上,可以得出一个结论:写线程对某个链表的结构性修改不会影响其他的并发读线程对这个链表的遍历访问。 - -### 3. Volatile 变量 - -由于内存可见性问题,未正确同步的情况下,写线程写入的值可能并不为后续的读线程可见。 - -下面以写线程 M 和读线程 N 来说明 ConcurrentHashMap 如何协调读 / 写线程间的内存可见性问题。 - -

- -假设线程 M 在写入了 volatile 型变量 count 后,线程 N 读取了这个 volatile 型变量 count。 - -根据 happens-before 关系法则中的程序次序法则,A appens-before 于 B,C happens-before D。 +```java +/** + * The number of elements. Accessed only either within locks + * or among other volatile reads that maintain visibility. + */ +transient int count; +``` -根据 Volatile 变量法则,B happens-before C。 +在执行 size 操作时,需要遍历所有 Segment 然后把 count 累计起来。 -根据传递性,连接上面三个 happens-before 关系得到:A appens-before 于 B; B appens-before C;C happens-before D。也就是说:写线程 M 对链表做的结构性修改,在读线程 N 读取了同一个 volatile 变量后,对线程 N 也是可见的了。 +ConcurrentHashMap 在执行 size 操作时先尝试不加锁,如果连续两次不加锁操作得到的结果一致,那么可以认为这个结果是正确的。 -虽然线程 N 是在未加锁的情况下访问链表。Java 的内存模型可以保证:只要之前对链表做结构性修改操作的写线程 M 在退出写方法前写 volatile 型变量 count,读线程 N 在读取这个 volatile 型变量 count 后,就一定能 “看到” 这些修改。 +尝试次数使用 RETRIES_BEFORE_LOCK 定义,该值为 2,retries 初始值为 -1,因此尝试次数为 3。 -ConcurrentHashMap 中,每个 Segment 都有一个变量 count。它用来统计 Segment 中的 HashEntry 的个数。这个变量被声明为 volatile。 +如果尝试的次数超过 3 次,就需要对每个 Segment 加锁。 ```java -transient volatile int count; -``` - -所有不加锁读方法,在进入读方法时,首先都会去读这个 count 变量。比如下面的 get 方法: -```java -V get(Object key, int hash) { - if(count != 0) { // 首先读 count 变量 - HashEntry e = getFirst(hash); - while(e != null) { - if(e.hash == hash && key.equals(e.key)) { - V v = e.value; - if(v != null) - return v; - // 如果读到 value 域为 null,说明发生了重排序,加锁后重新读取 - return readValueUnderLock(e); - } - e = e.next; - } - } - return null; +/** + * Number of unsynchronized retries in size and containsValue + * methods before resorting to locking. This is used to avoid + * unbounded retries if tables undergo continuous modification + * which would make it impossible to obtain an accurate result. + */ +static final int RETRIES_BEFORE_LOCK = 2; + +public int size() { + // Try a few times to get accurate count. On failure due to + // continuous async changes in table, resort to locking. + final Segment[] segments = this.segments; + int size; + boolean overflow; // true if size overflows 32 bits + long sum; // sum of modCounts + long last = 0L; // previous sum + int retries = -1; // first iteration isn't retry + try { + for (;;) { + // 超过尝试次数,则对每个 Segment 加锁 + if (retries++ == RETRIES_BEFORE_LOCK) { + for (int j = 0; j < segments.length; ++j) + ensureSegment(j).lock(); // force creation + } + sum = 0L; + size = 0; + overflow = false; + for (int j = 0; j < segments.length; ++j) { + Segment seg = segmentAt(segments, j); + if (seg != null) { + sum += seg.modCount; + int c = seg.count; + if (c < 0 || (size += c) < 0) + overflow = true; + } + } + // 连续两次得到的结果一致,则认为这个结果是正确的 + if (sum == last) + break; + last = sum; + } + } finally { + if (retries > RETRIES_BEFORE_LOCK) { + for (int j = 0; j < segments.length; ++j) + segmentAt(segments, j).unlock(); + } + } + return overflow ? Integer.MAX_VALUE : size; } ``` -在 ConcurrentHashMap 中,所有执行写操作的方法(put, remove, clear),在对链表做结构性修改之后,在退出写方法前都会去写这个 count 变量。所有未加锁的读操作(get, contains, containsKey)在读方法中,都会首先去读取这个 count 变量。 - -根据 Java 内存模型,对同一个 volatile 变量的写 / 读操作可以确保:写线程写入的值,能够被之后未加锁的读线程 “看到”。 - -这个特性和前面介绍的 HashEntry 对象的不变性相结合,使得在 ConcurrentHashMap 中,读线程在读取散列表时,基本不需要加锁就能成功获得需要的值。这两个特性相配合,不仅减少了请求同一个锁的频率(读操作一般不需要加锁就能够成功获得值),也减少了持有同一个锁的时间(只有读到 value 域的值为 null 时 ,读线程才需要加锁后重读)。 -### 4. 小结 +### 3. JDK 1.8 的改动 -ConcurrentHashMap 的高并发性主要来自于三个方面: +[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java) -- 用分离锁实现多个线程间的更深层次的共享访问。 -- 用 HashEntery 对象的不变性来降低执行读操作的线程在遍历链表期间对加锁的需求。 -- 通过对同一个 Volatile 变量的写 / 读访问,协调不同线程间读 / 写操作的内存可见性。 +JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发程度与 Segment 数量相等。 -## ConcurrentHashMap - JDK 1.8 +JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。 -[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java) +并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 -JDK 1.7 分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock。 +## LinkedHashMap -JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。而是使用了内置锁 synchronized,主要是出于以下考虑: +[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) -1. synchronized 的锁粒度更低; -2. synchronized 优化空间更大; -3. 在大量数据操作的情况下,ReentrantLock 会开销更多的内存。 +## TreeMap -并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 +[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) # 参考资料 @@ -679,4 +847,5 @@ JDK 1.8 的实现不是用了 Segment,Segment 属于重入锁 ReentrantLock。 - [探索 ConcurrentHashMap 高并发性的实现机制](https://www.ibm.com/developerworks/cn/java/java-lo-concurrenthashmap/) - [HashMap 相关面试题及其解答](https://www.jianshu.com/p/75adf47958a7) - [Java 集合细节(二):asList 的缺陷](http://wiki.jikexueyuan.com/project/java-enhancement/java-thirtysix.html) +- [Java Collection Framework – The LinkedList Class](http://javaconceptoftheday.com/java-collection-framework-linkedlist-class/) diff --git "a/notes/Java \345\271\266\345\217\221.md" "b/notes/Java \345\271\266\345\217\221.md" index 61c9fdb8b2db16ba5a7189f039b438a12958cc88..8a2c8fcf5cd4ada1bbd8e2300d4705183d04db74 100644 --- "a/notes/Java \345\271\266\345\217\221.md" +++ "b/notes/Java \345\271\266\345\217\221.md" @@ -47,7 +47,7 @@ * [线程安全分类](#线程安全分类) * [线程安全的实现方法](#线程安全的实现方法) * [十二、锁优化](#十二锁优化) - * [自旋锁与自适应自旋](#自旋锁与自适应自旋) + * [自旋锁](#自旋锁) * [锁消除](#锁消除) * [锁粗化](#锁粗化) * [轻量级锁](#轻量级锁) @@ -93,6 +93,10 @@ 调用 Object.wait() 方法使线程进入限期等待或者无限期等待时,常常用“挂起一个线程”进行描述。 +睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。 + +阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁;而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。 + | 进入方法 | 退出方法 | | --- | --- | | Thread.sleep() 方法 | 时间结束 | @@ -109,9 +113,9 @@ 有三种使用线程的方法: -1. 实现 Runnable 接口; -2. 实现 Callable 接口; -3. 继承 Thread 类。 +- 实现 Runnable 接口; +- 实现 Callable 接口; +- 继承 Thread 类。 实现 Runnable 和 Callable 接口的类只能当做一个可以在线程中运行的任务,不是真正意义上的线程,因此最后还需要通过 Thread 来调用。可以说任务是通过线程驱动从而执行的。 @@ -182,8 +186,8 @@ public static void main(String[] args) { 实现接口会更好一些,因为: -1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; -2. 类可能只要求可执行就行,继承整个 Thread 类开销会过大。 +- Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口; +- 类可能只要求可执行就行,继承整个 Thread 类开销过大。 # 三、基础线程机制 @@ -193,9 +197,9 @@ Executor 管理多个异步任务的执行,而无需程序员显式地管理 主要有三种 Executor: -1. CachedThreadPool:一个任务创建一个线程; -2. FixedThreadPool:所有任务只能使用固定大小的线程; -3. SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 +- CachedThreadPool:一个任务创建一个线程; +- FixedThreadPool:所有任务只能使用固定大小的线程; +- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。 ```java public static void main(String[] args) { @@ -258,16 +262,10 @@ public void run() { 通过调用一个线程的 interrupt() 来中断该线程,如果该线程处于阻塞、限期等待或者无限期等待状态,那么就会抛出 InterruptedException,从而提前结束该线程。但是不能中断 I/O 阻塞和 synchronized 锁阻塞。 -对于以下代码,在 Main 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。 +对于以下代码,在 main() 中启动一个线程之后再中断它,由于线程中调用了 Thread.sleep() 方法,因此会抛出一个 InterruptedException,从而提前结束线程,不执行之后的语句。 ```java public class InterruptExample { - public static void main(String[] args) throws InterruptedException { - Thread thread1 = new MyThread1(); - thread1.start(); - thread1.interrupt(); - System.out.println("Main run"); - } private static class MyThread1 extends Thread { @Override @@ -281,7 +279,15 @@ public class InterruptExample { } } } +``` +```java +public static void main(String[] args) throws InterruptedException { + Thread thread1 = new MyThread1(); + thread1.start(); + thread1.interrupt(); + System.out.println("Main run"); +} ``` ```html @@ -301,11 +307,6 @@ java.lang.InterruptedException: sleep interrupted ```java public class InterruptExample { - public static void main(String[] args) throws InterruptedException { - Thread thread2 = new MyThread2(); - thread2.start(); - thread2.interrupt(); - } private static class MyThread2 extends Thread { @Override @@ -319,6 +320,14 @@ public class InterruptExample { } ``` +```java +public static void main(String[] args) throws InterruptedException { + Thread thread2 = new MyThread2(); + thread2.start(); + thread2.interrupt(); +} +``` + ```html Thread end ``` @@ -330,20 +339,18 @@ Thread end 以下使用 Lambda 创建线程,相当于创建了一个匿名内部线程。 ```java -public class ExecutorInterruptExample { - public static void main(String[] args) { - ExecutorService executorService = Executors.newCachedThreadPool(); - executorService.execute(() -> { - try { - Thread.sleep(2000); - System.out.println("Thread run"); - } catch (InterruptedException e) { - e.printStackTrace(); - } - }); - executorService.shutdownNow(); - System.out.println("Main run"); - } +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> { + try { + Thread.sleep(2000); + System.out.println("Thread run"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + }); + executorService.shutdownNow(); + System.out.println("Main run"); } ``` @@ -385,7 +392,7 @@ public void func () { 它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。 -对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步语句块,因此这两个线程就需要进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。 +对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。 ```java public class SynchronizedExample { @@ -397,13 +404,15 @@ public class SynchronizedExample { } } } +} +``` - public static void main(String[] args) { - SynchronizedExample e1 = new SynchronizedExample(); - ExecutorService executorService = Executors.newCachedThreadPool(); - executorService.execute(() -> e1.func1()); - executorService.execute(() -> e1.func1()); - } +```java +public static void main(String[] args) { + SynchronizedExample e1 = new SynchronizedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> e1.func1()); + executorService.execute(() -> e1.func1()); } ``` @@ -460,14 +469,16 @@ public class SynchronizedExample { } } } +} +``` - public static void main(String[] args) { - SynchronizedExample e1 = new SynchronizedExample(); - SynchronizedExample e2 = new SynchronizedExample(); - ExecutorService executorService = Executors.newCachedThreadPool(); - executorService.execute(() -> e1.func2()); - executorService.execute(() -> e2.func2()); - } +```java +public static void main(String[] args) { + SynchronizedExample e1 = new SynchronizedExample(); + SynchronizedExample e2 = new SynchronizedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> e1.func2()); + executorService.execute(() -> e2.func2()); } ``` @@ -518,7 +529,7 @@ public static void main(String[] args) { 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 ``` -ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了一些高级功能: +ReentrantLock 是 java.util.concurrent(J.U.C)包中的锁,相比于 synchronized,它多了以下高级功能: **1. 等待可中断** @@ -540,7 +551,7 @@ synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。 **2. 性能** -从性能上来看,在新版本的 JDK 中对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由,而且 synchronized 有更大的优化空间,因此优先考虑 synchronized。 +从性能上来看,新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized。 **3. 功能** @@ -556,7 +567,7 @@ ReentrantLock 多了一些高级功能。 ## join() -在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。 +在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。 对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先与 b 线程的输出。 @@ -595,11 +606,13 @@ public class JoinExample { b.start(); a.start(); } +} +``` - public static void main(String[] args) { - JoinExample example = new JoinExample(); - example.test(); - } +```java +public static void main(String[] args) { + JoinExample example = new JoinExample(); + example.test(); } ``` @@ -633,13 +646,15 @@ public class WaitNotifyExample { } System.out.println("after"); } +} +``` - public static void main(String[] args) { - ExecutorService executorService = Executors.newCachedThreadPool(); - WaitNotifyExample example = new WaitNotifyExample(); - executorService.execute(() -> example.after()); - executorService.execute(() -> example.before()); - } +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + WaitNotifyExample example = new WaitNotifyExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); } ``` @@ -685,13 +700,15 @@ public class AwaitSignalExample { lock.unlock(); } } +} +``` - public static void main(String[] args) { - ExecutorService executorService = Executors.newCachedThreadPool(); - AwaitSignalExample example = new AwaitSignalExample(); - executorService.execute(() -> example.after()); - executorService.execute(() -> example.before()); - } +```java +public static void main(String[] args) { + ExecutorService executorService = Executors.newCachedThreadPool(); + AwaitSignalExample example = new AwaitSignalExample(); + executorService.execute(() -> example.after()); + executorService.execute(() -> example.before()); } ``` @@ -875,9 +892,6 @@ java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: **使用 BlockingQueue 实现生产者消费者问题** ```java -import java.util.concurrent.ArrayBlockingQueue; -import java.util.concurrent.BlockingQueue; - public class ProducerConsumer { private static BlockingQueue queue = new ArrayBlockingQueue<>(5); @@ -906,20 +920,22 @@ public class ProducerConsumer { System.out.print("consume.."); } } +} +``` - public static void main(String[] args) { - for (int i = 0; i < 2; i++) { - Producer producer = new Producer(); - producer.start(); - } - for (int i = 0; i < 5; i++) { - Consumer consumer = new Consumer(); - consumer.start(); - } - for (int i = 0; i < 3; i++) { - Producer producer = new Producer(); - producer.start(); - } +```java +public static void main(String[] args) { + for (int i = 0; i < 2; i++) { + Producer producer = new Producer(); + producer.start(); + } + for (int i = 0; i < 5; i++) { + Consumer consumer = new Consumer(); + consumer.start(); + } + for (int i = 0; i < 3; i++) { + Producer producer = new Producer(); + producer.start(); } } ``` @@ -1002,22 +1018,24 @@ public class ThreadUnsafeExample { public int get() { return cnt; } +} +``` - public static void main(String[] args) throws InterruptedException { - final int threadSize = 1000; - ThreadUnsafeExample example = new ThreadUnsafeExample(); - final CountDownLatch countDownLatch = new CountDownLatch(threadSize); - ExecutorService executorService = Executors.newCachedThreadPool(); - for (int i = 0; i < threadSize; i++) { - executorService.execute(() -> { - example.add(); - countDownLatch.countDown(); - }); - } - countDownLatch.await(); - executorService.shutdown(); - System.out.println(example.get()); +```java +public static void main(String[] args) throws InterruptedException { + final int threadSize = 1000; + ThreadUnsafeExample example = new ThreadUnsafeExample(); + final CountDownLatch countDownLatch = new CountDownLatch(threadSize); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < threadSize; i++) { + executorService.execute(() -> { + example.add(); + countDownLatch.countDown(); + }); } + countDownLatch.await(); + executorService.shutdown(); + System.out.println(example.get()); } ``` @@ -1089,24 +1107,25 @@ public class AtomicExample { public int get() { return cnt.get(); } +} +``` - public static void main(String[] args) throws InterruptedException { - final int threadSize = 1000; - AtomicExample example = new AtomicExample(); - final CountDownLatch countDownLatch = new CountDownLatch(threadSize); - ExecutorService executorService = Executors.newCachedThreadPool(); - for (int i = 0; i < threadSize; i++) { - executorService.execute(() -> { - example.add(); - countDownLatch.countDown(); - }); - } - countDownLatch.await(); - executorService.shutdown(); - System.out.println(example.get()); +```java +public static void main(String[] args) throws InterruptedException { + final int threadSize = 1000; + AtomicExample example = new AtomicExample(); // 只修改这条语句 + final CountDownLatch countDownLatch = new CountDownLatch(threadSize); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < threadSize; i++) { + executorService.execute(() -> { + example.add(); + countDownLatch.countDown(); + }); } + countDownLatch.await(); + executorService.shutdown(); + System.out.println(example.get()); } - ``` ```html @@ -1126,22 +1145,24 @@ public class AtomicSynchronizedExample { public synchronized int get() { return cnt; } +} +``` - public static void main(String[] args) throws InterruptedException { - final int threadSize = 1000; - AtomicSynchronizedExample example = new AtomicSynchronizedExample(); - final CountDownLatch countDownLatch = new CountDownLatch(threadSize); - ExecutorService executorService = Executors.newCachedThreadPool(); - for (int i = 0; i < threadSize; i++) { - executorService.execute(() -> { - example.add(); - countDownLatch.countDown(); - }); - } - countDownLatch.await(); - executorService.shutdown(); - System.out.println(example.get()); +```java +public static void main(String[] args) throws InterruptedException { + final int threadSize = 1000; + AtomicSynchronizedExample example = new AtomicSynchronizedExample(); + final CountDownLatch countDownLatch = new CountDownLatch(threadSize); + ExecutorService executorService = Executors.newCachedThreadPool(); + for (int i = 0; i < threadSize; i++) { + executorService.execute(() -> { + example.add(); + countDownLatch.countDown(); + }); } + countDownLatch.await(); + executorService.shutdown(); + System.out.println(example.get()); } ``` @@ -1155,9 +1176,7 @@ public class AtomicSynchronizedExample { volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 -对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题。因为 volatile 并不能保证操作的原子性。 - -// TODO:volatile 不能解决线程不安全问题的示例代码。 +对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。 ### 3. 有序性 @@ -1404,7 +1423,7 @@ public final int getAndAddInt(Object var1, long var2, int var4) { } ``` -ABA :如果一个变量 V 初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 +ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 ### 3. 无同步方案 @@ -1432,14 +1451,16 @@ public class StackClosedExample { } System.out.println(cnt); } +} +``` - public static void main(String[] args) { - StackClosedExample example = new StackClosedExample(); - ExecutorService executorService = Executors.newCachedThreadPool(); - executorService.execute(() -> example.add100()); - executorService.execute(() -> example.add100()); - executorService.shutdown(); - } +```java +public static void main(String[] args) { + StackClosedExample example = new StackClosedExample(); + ExecutorService executorService = Executors.newCachedThreadPool(); + executorService.execute(() -> example.add100()); + executorService.execute(() -> example.add100()); + executorService.shutdown(); } ``` @@ -1554,21 +1575,23 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因 # 十二、锁优化 -高效并发是从 JDK 1.5 到 JDK 1.6 的一个重要改进,HotSpot 虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning)、锁消除(Lock Elimination)、锁粗化(Lock Coarsening)、轻量级锁(Lightweight Locking)和偏向锁(Biased Locking)等。这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率。 +这里的锁优化主要是指虚拟机对 synchronized 的优化。 -## 自旋锁与自适应自旋 +## 自旋锁 -前面我们讨论互斥同步的时候,提到了互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程的操作都需要转入内核态完成,这些操作给系统的并发性能带来了很大的压力。同时,虚拟机的开发团队也注意到在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段时间去挂起和恢复线程并不值得。如果物理机器有一个以上的处理器,能让两个或以上的线程同时并行执行,我们就可以让后面请求锁的那个线程 “稍等一下”,但不放弃处理器的执行时间,看看持有锁的线程是否很快就会释放锁。为了让线程等待,我们只需让线程执行一个忙循环(自旋),这项技术就是所谓的自旋锁。 +互斥同步的进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 -自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好。反之,如果锁被占用的时候很长,那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是 10 次,用户可以使用参数 -XX:PreBlockSpin 来更改。 +自选锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。自旋次数的默认值是 10 次,用户可以使用虚拟机参数 -XX:PreBlockSpin 来更改。 -在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如 100 个循环。另外,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能省略掉自旋过程,以避免浪费处理器资源。有了自适应自旋,随着程序运行和性能监控信息的不断完善,虚拟机对程序锁的状况预测就会越来越准确,虚拟机就会变得越来越“聪明”了。 +在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。 ## 锁消除 -锁消除是指虚拟机即时编译器在运行时,对一些代码上要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除。锁消除的主要判定依据来源于逃逸分析的数据支持,如果判定在一段代码中,堆上的所有数据都不会逃逸出去从而被其他线程访问到,那就可以把他们当做栈上数据对待,认为它们是线程私有的,同步加锁自然就无须进行。 +锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。 -也许读者会有疑问,变量是否逃逸,对于虚拟机来说需要使用数据流分析来确定,但是程序自己应该是很清楚的,怎么会在明知道不存在数据争用的情况下要求同步呢?答案是有许多同步措施并不是程序员自己加入的。同步的代码在 Java 程序中的普遍程度也许超过了大部分读者的想象。下面段非常简单的代码仅仅是输出 3 个字符串相加的结果,无论是源码字面上还是程序语义上都没有同步。 +锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们上的锁进行消除。 + +对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁: ```java public static String concatString(String s1, String s2, String s3) { @@ -1576,7 +1599,7 @@ public static String concatString(String s1, String s2, String s3) { } ``` -我们也知道,由于 String 是一个不可变的类,对字符串的连接操作总是通过生成新的 String 对象来进行的,因此 Javac 编译器会对 String 连接做自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子: +String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子: ```java public static String concatString(String s1, String s2, String s3) { @@ -1587,46 +1610,47 @@ public static String concatString(String s1, String s2, String s3) { return sb.toString(); } ``` -每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会 “逃逸” 到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉,在即时编译之后,这段代码就会忽略掉所有的同步而直接执行了。 -## 锁粗化 +每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说,sb 的所有引用永远不会“逃逸”到 concatString() 方法之外,其他线程无法访问到它。因此,虽然这里有锁,但是可以被安全地消除掉。 -原则上,我们在编写代码的时候,总是推荐将同步块的作用范围限制得尽量小:只在共享数据的实际作用域中才进行同步。这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待锁的线程也能尽快拿到锁。 +## 锁粗化 -大部分情况下,上面的原则都是正确的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。 +如果一系列的连续操作都对同一个对象反复加锁和解锁,频繁的加锁操作就会导致性能损耗。 -上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。 +上一节的示例代码中连续的 append() 方法就属于这类情况。如果虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部。对于上一节的示例代码就是扩展到第一个 append() 操作之前直至最后一个 append() 操作之后,这样只需要加锁一次就可以了。 ## 轻量级锁 -轻量级锁是 JDK 1.6 之中加入的新型锁机制,它名字中的“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的,因此传统的锁机制就称为“重量级”锁。首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。 +JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:无锁状态(unlocked)、偏向锁状态(biasble)、轻量级锁状态(lightweight locked)和重量级锁状态(inflated)。 + +以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。 -要理解轻量级锁,以及后面会讲到的偏向锁的原理和运作过程,必须从 HotSpot 虚拟机的对象(对象头部分)的内存布局开始介绍。HotSpot 虚拟机的对象头(Object Header)分为两部分信息,第一部分用于存储对象自身的运行时数据,如哈希码(HashCode)、GC 分代年龄(Generational GC Age)等,这部分数据是长度在 32 位和 64 位的虚拟机中分别为 32 bit 和 64 bit,官方称它为“Mark Word”,它是实现轻量级锁和偏向锁的关键。另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象的话,还会有一个额外的部分用于存储数组长度。 +

-简单地介绍了对象的内存布局后,我们把话题返回到轻量级锁的执行过程上。在代码进入同步块的时候,如果此同步对象没有被锁定(锁标志位为 “01” 状态)虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝(官方把这份拷贝加上了一个 Displaced 前缀,即 Displaced Mark Word)。然后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针。如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位(Mark Word 的最后 2bit)将转变为 “00”,即表示此对象处于轻量级锁定状态。 +下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象,包含了 Mark Word 和其它信息。 -

+

-如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是的话只说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁,所标志的状态变为“10”,Mark Word 中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。 +轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。 -上面描述的是轻量级锁的加锁过程,它的解锁过程也是通过 CAS 操作来进行的,如果对象的 Mark Word 仍然指向着线程的锁记录,那就用 CAS 操作把对象当前的 Mark Word 和线程中复制的 Displaced Mark Word 替换回来,如果替换成功,整个同步过程就完成了。如果替换失败,说明有其他线程尝试过获取该锁,那就要释放锁的同时,唤醒被挂起的线程。 +当尝试获取一个锁对象时,如果锁对象标记为 0 01,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。 -轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用 CAS 操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了 CAS 操作,因此在有竞争的情况下,轻量级锁会比传统的重量级锁更慢。 +

+ +如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。 ## 偏向锁 -偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。 +偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。 -偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。 +可以使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。 -假设当前虚拟机启用了偏向锁(启用参数 -XX:+UseBiasedLocking,这是 JDK 1.6 的默认值),那么,当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设为“01”,即偏向模式。同时使用 CAS 操作把获取到这个锁的线程 ID 记录在对象的 Mark Word 之中,如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行如何同步操作(例如 Locking、Unlocking 及对 Mark Word 的 Update 等)。 +当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。 -当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。根据锁对象目前是否处于被锁定的状态,撤销偏向(Revoke Bias)后恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就如上面介绍的轻量级锁那样执行。偏向锁、轻量级锁的状态转换及对象 Mark Word 的关系如图 13-5 所示。 +当有另外一个线程去尝试获取这个锁对象时,偏向状态就宣告结束,此时撤销偏向(Revoke Bias)后恢复到未锁定状态或者轻量级锁状态。

-偏向锁可以提高带有同步但无竞争的程序性能。它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说,它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数 -XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。 - # 十三、多线程开发良好的实践 - 给线程起个有意义的名字,这样可以方便找 Bug。 @@ -1659,3 +1683,4 @@ public static String concatString(String s1, String s2, String s3) { - [Concurrent](https://sites.google.com/site/webdevelopart/21-compile/06-java/javase/concurrent?tmpl=%2Fsystem%2Fapp%2Ftemplates%2Fprint%2F&showPrintDialog=1) - [JAVA FORK JOIN EXAMPLE](http://www.javacreed.com/java-fork-join-example/ "Java Fork Join Example") - [聊聊并发(八)——Fork/Join 框架介绍](http://ifeve.com/talk-concurrency-forkjoin/) +- [Eliminating SynchronizationRelated Atomic Operations with Biased Locking and Bulk Rebiasing](http://www.oracle.com/technetwork/java/javase/tech/biasedlocking-oopsla2006-preso-150106.pdf) diff --git "a/notes/Java \350\231\232\346\213\237\346\234\272.md" "b/notes/Java \350\231\232\346\213\237\346\234\272.md" index 4b72b865bc918e58171a03e742c8f49b1936c830..6f157ce8931cf352272411db082370383b745519 100644 --- "a/notes/Java \350\231\232\346\213\237\346\234\272.md" +++ "b/notes/Java \350\231\232\346\213\237\346\234\272.md" @@ -12,7 +12,6 @@ * [垃圾收集算法](#垃圾收集算法) * [垃圾收集器](#垃圾收集器) * [内存分配与回收策略](#内存分配与回收策略) - * [Full GC 的触发条件](#full-gc-的触发条件) * [三、类加载机制](#三类加载机制) * [类的生命周期](#类的生命周期) * [类初始化时机](#类初始化时机) @@ -47,8 +46,8 @@ java -Xss=512M HackTheJava 该区域可能抛出以下异常: -1. 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常; -2. 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。 +- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常; +- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。 ## 本地方法栈 @@ -90,7 +89,9 @@ java -Xms=1M -Xmx=2M HackTheJava 和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 -对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现,HotSpot 虚拟机把它当成永久代来进行垃圾回收。 +对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 + +JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,JDK 1.8 之后,取消了永久代,用 metaspace(元数据)区替代。 ## 运行时常量池 @@ -110,39 +111,49 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用 ## 判断一个对象是否可回收 -### 1. 引用计数 +### 1. 引用计数算法 给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 ```java -objA.instance = objB; -objB.instance = objA; +public class ReferenceCountingGC { + public Object instance = null; + + public static void main(String[] args) { + ReferenceCountingGC objectA = new ReferenceCountingGC(); + ReferenceCountingGC objectB = new ReferenceCountingGC(); + objectA.instance = objectB; + objectB.instance = objectA; + } +} ``` -### 2. 可达性 +正因为循环引用的存在,因此 Java 虚拟机不适用引用计数算法。 -通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是可用的,不可达的对象可被回收。 +### 2. 可达性分析算法 + +通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。

-GC Roots 一般包含以下内容: +Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容: -1. 虚拟机栈中引用的对象 -2. 方法区中类静态属性引用的对象 -3. 方法区中的常量引用的对象 -4. 本地方法栈中引用的对象 +- 虚拟机栈中引用的对象 +- 本地方法栈中引用的对象 +- 方法区中类静态属性引用的对象 +- 方法区中的常量引用的对象 ### 3. 引用类型 -无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否存活都与引用有关。 +无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。 -Java 对引用的概念进行了扩充,引入四种强度不同的引用类型。 +Java 具有四种强度不同的引用类型。 **(一)强引用** -只要强引用存在,垃圾回收器永远不会回收被引用的对象。 +被强引用关联的对象不会被垃圾收集器回收。 使用 new 一个新对象的方式来创建强引用。 @@ -152,28 +163,69 @@ Object obj = new Object(); **(二)软引用** -用来描述一些还有用但是并非必需的对象。 - -在系统将要发生内存溢出异常之前,会将这些对象列进回收范围之中进行第二次回收。 - -软引用主要用来实现类似缓存的功能,在内存足够的情况下直接通过软引用取值,无需从繁忙的真实来源获取数据,提升速度;当内存不足时,自动删除这部分缓存数据,从真正的来源获取这些数据。 +被软引用关联的对象,只有在内存不够的情况下才会被回收。 -使用 SoftReference 类来实现软引用。 +使用 SoftReference 类来创建软引用。 ```java Object obj = new Object(); SoftReference sf = new SoftReference(obj); +obj = null; // 使对象只被软引用关联 ``` **(三)弱引用** -只能生存到下一次垃圾收集发生之前,当垃圾收集器工作时,无论当前内存是否足够,都会被回收。 +被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。 使用 WeakReference 类来实现弱引用。 ```java Object obj = new Object(); WeakReference wf = new WeakReference(obj); +obj = null; +``` + +WeakHashMap 的 Entry 继承自 WeakReference,主要用来实现缓存。 + +```java +private static class Entry extends WeakReference implements Map.Entry +``` + +Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现,longterm 使用 WeakHashMap,保证了不常使用的对象容易被回收。 + +```java +public final class ConcurrentCache { + + private final int size; + + private final Map eden; + + private final Map longterm; + + public ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K k) { + V v = this.eden.get(k); + if (v == null) { + v = this.longterm.get(k); + if (v != null) + this.eden.put(k, v); + } + return v; + } + + public void put(K k, V v) { + if (this.eden.size() >= size) { + this.longterm.putAll(this.eden); + this.eden.clear(); + } + this.eden.put(k, v); + } +} ``` **(四)虚引用** @@ -187,6 +239,7 @@ WeakReference wf = new WeakReference(obj); ```java Object obj = new Object(); PhantomReference pf = new PhantomReference(obj); +obj = null; ``` ### 4. 方法区的回收 @@ -197,13 +250,13 @@ PhantomReference pf = new PhantomReference(obj); 类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载: -1. 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 -2. 加载该类的 ClassLoader 已经被回收。 -3. 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 +- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 +- 加载该类的 ClassLoader 已经被回收。 +- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。 -在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGo 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 +在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 ### 5. finalize() @@ -217,12 +270,12 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。

-将需要回收的对象进行标记,然后清除。 +将需要存活的对象进行标记,然后清理掉未被标记的对象。 不足: -1. 标记和清除过程效率都不高; -2. 会产生大量碎片,内存碎片过多可能导致无法给大对象分配内存。 +- 标记和清除过程效率都不高; +- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。 ### 2. 标记 - 整理 @@ -238,7 +291,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 主要不足是只使用了内存的一半。 -现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和 使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间。 +现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90 %。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。 ### 4. 分代收集 @@ -246,8 +299,8 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 一般将 Java 堆分为新生代和老年代。 -1. 新生代使用:复制算法 -2. 老年代使用:标记 - 清理 或者 标记 - 整理 算法 +- 新生代使用:复制算法 +- 老年代使用:标记 - 清理 或者 标记 - 整理 算法 ## 垃圾收集器 @@ -259,11 +312,13 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。

-它是单线程的收集器,不仅意味着只会使用一个线程进行垃圾收集工作,更重要的是它在进行垃圾收集时,必须暂停所有其他工作线程,往往造成过长的等待时间。 +Serial 翻译为串行,可以理解为垃圾收集和用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序。除了 CMS 和 G1 之外,其它收集器都是以串行的方式执行。CMS 和 G1 可以使得垃圾收集和用户程序同时执行,被称为并发执行。 + +它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 -在 Client 应用场景中,分配给虚拟机管理的内存一般来说不会很大,该收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 +它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 ### 2. ParNew 收集器 @@ -277,7 +332,7 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 ### 3. Parallel Scavenge 收集器 -是并行的多线程收集器。 +与 ParNew 一样是并行的多线程收集器。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 @@ -285,16 +340,16 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 -还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。自适应调节策略也是它与 ParNew 收集器的一个重要区别。 +还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。 ### 4. Serial Old 收集器

-Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途: +是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途: -1. 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 -2. 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 +- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。 +- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。 ### 5. Parallel Old 收集器 @@ -308,117 +363,127 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下

-CMS(Concurrent Mark Sweep),从 Mark Sweep 可以知道它是基于标记 - 清除算法实现的。 +CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 -特点:并发收集、低停顿。 +特点:并发收集、低停顿。并发指的是用户线程和 GC 线程同时运行。 分为以下四个流程: -1. 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 -2. 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 -3. 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 -4. 并发清除:不需要停顿。 +- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 +- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。 +- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。 +- 并发清除:不需要停顿。 在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。 具有以下缺点: -1. 对 CPU 资源敏感。CMS 默认启动的回收线程数是 (CPU 数量 + 3) / 4,当 CPU 不足 4 个时,CMS 对用户程序的影响就可能变得很大,如果本来 CPU 负载就比较大,还要分出一半的运算能力去执行收集器线程,就可能导致用户程序的执行速度忽然降低了 50%,其实也让人无法接受。并且低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率变低。 - -2. 无法处理浮动垃圾。由于并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生。这一部分垃圾出现在标记过程之后,CMS 无法在当次收集中处理掉它们,只好留到下一次 GC 时再清理掉,这一部分垃圾就被称为“浮动垃圾”。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此它不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。可以使用 -XX:CMSInitiatingOccupancyFraction 的值来改变触发收集器工作的内存占用百分比,JDK 1.5 默认设置下该值为 68,也就是当老年代使用了 68% 的空间之后会触发收集器工作。如果该值设置的太高,导致浮动垃圾无法保存,那么就会出现 Concurrent Mode Failure,此时虚拟机将启动后备预案:临时启用 Serial Old 收集器来重新进行老年代的垃圾收集。 - -3. 标记 - 清除算法导致的空间碎片,给大对象分配带来很大麻烦,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 +- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 +- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。可以使用 -XX:CMSInitiatingOccupancyFraction 来改变触发 CMS 收集器工作的内存占用百分,如果这个值设置的太大,导致预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。 +- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 ### 7. G1 收集器 -

+G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 -G1(Garbage-First)收集器是当今收集器技术发展最前沿的成果之一,它是一款面向服务端应用的垃圾收集器,HotSpot 开发团队赋予它的使命是(在比较长期的)未来可以替换掉 JDK 1.5 中发布的 CMS 收集器。 +Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老生代,而 G1 可以直接对新生代和永久代一起回收。 -具备如下特点: +

-- 并行与并发:能充分利用多 CPU 环境下的硬件优势,使用多个 CPU 来缩短停顿时间。 -- 分代收集:分代概念依然得以保留,虽然它不需要其它收集器配合就能独立管理整个 GC 堆,但它能够采用不同方式去处理新创建的对象和已存活一段时间、熬过多次 GC 的旧对象来获取更好的收集效果。 -- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 -- 可预测的停顿:这是它相对 CMS 的一大优势,降低停顿时间是 G1 和 CMS 共同的关注点,但 G1 除了降低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒,这几乎已经是实时 Java(RTSJ)的垃圾收集器的特征了。 +G1 把新生代和老年代划分成多个大小相等的独立区域(Region),新生代和永久代不再物理隔离。 + +

-在 G1 之前的其他收集器进行收集的范围都是整个新生代或者老生代,而 G1 不再是这样,Java 堆的内存布局与其他收集器有很大区别,将整个 Java 堆划分为多个大小相等的独立区域(Region)。虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,而都是一部分 Region(不需要连续)的集合。 +通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 记录垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。 -之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个 Java 堆中进行全区域的垃圾收集。它跟踪各个 Region 里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region(这也就是 Garbage-First 名称的来由)。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了它在有限的时间内可以获取尽可能高的收集效率。 +每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。 -Region 不可能是孤立的,一个对象分配在某个 Region 中,可以与整个 Java 堆任意的对象发生引用关系。在做可达性分析确定对象是否存活的时候,需要扫描整个 Java 堆才能保证准确性,这显然是对 GC 效率的极大伤害。为了避免全堆扫描的发生,每个 Region 都维护了一个与之对应的 Remembered Set。虚拟机发现程序在对 Reference 类型的数据进行写操作时,会产生一个 Write Barrier 暂时中断写操作,检查 Reference 引用的对象是否处于不同的 Region 之中,如果是,便通过 CardTable 把相关引用信息记录到被引用对象所属的 Region 的 Remembered Set 之中。当进行内存回收时,在 GC 根节点的枚举范围中加入 Remembered Set 即可保证不对全堆扫描也不会有遗漏。 +

如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤: -1. 初始标记 -2. 并发标记 -3. 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 -4. 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 +- 初始标记 +- 并发标记 +- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 +- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。 + +具备如下特点: + +- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 +- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。 + +更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html) -### 8. 七种垃圾收集器的比较 +### 8. 比较 -| 收集器 | 串行、并行 or 并发 | 新生代 / 老年代 | 算法 | 目标 | 适用场景 | +| 收集器 | 串行/并行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 | | :---: | :---: | :---: | :---: | :---: | :---: | -| **Serial** | 串行 | 新生代 | 复制算法 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | +| **Serial** | 串行 | 新生代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | | **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 | -| **ParNew** | 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | -| **Parallel Scavenge** | 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **Parallel Old** | 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **CMS** | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | -| **G1** | 并发 | both | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS | +| **ParNew** | 串行 + 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | +| **Parallel Scavenge** | 串行 + 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | +| **Parallel Old** | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | +| **CMS** | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | +| **G1** | 并行 + 并发 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS | ## 内存分配与回收策略 对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。 -### 1. 优先在 Eden 分配 +### 1. Minor GC 和 Full GC + +- Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 +- Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。 + +### 2. 内存分配策略 + +**(一)对象优先在 Eden 分配** 大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。 -关于 Minor GC 和 Full GC: +**(二)大对象直接进入老年代** -- Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 -- Full GC:发生在老年代上,老年代对象和新生代的相反,其存活时间长,因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。 +大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 -### 2. 大对象直接进入老年代 +经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 -大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。 +-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 -提供 -XX:PretenureSizeThreshold 参数,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。 +**(三)长期存活的对象进入老年代** -### 3. 长期存活的对象进入老年代 +为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。 -JVM 为对象定义年龄计数器,经过 Minor GC 依然存活,并且能被 Survivor 区容纳的,移被移到 Survivor 区,年龄就增加 1 岁,增加到一定年龄则移动到老年代中(默认 15 岁,通过 -XX:MaxTenuringThreshold 设置)。 +-XX:MaxTenuringThreshold 用来定义年龄的阈值。 -### 4. 动态对象年龄判定 +**(四)动态对象年龄判定** -JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等待 MaxTenuringThreshold 中要求的年龄。 +虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 区中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 -### 5. 空间分配担保 +**(五)空间分配担保** -在发生 Minor GC 之前,JVM 先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话 JVM 会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。 +在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC,尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC。 -## Full GC 的触发条件 +### 3. Full GC 的触发条件 对于 Minor GC,其触发条件非常简单,当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件: -### 1. 调用 System.gc() +**(一)调用 System.gc()** -此方法的调用是建议 JVM 进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:+ DisableExplicitGC 来禁止 RMI 调用 System.gc()。 +此方法的调用是建议虚拟机进行 Full GC,虽然只是建议而非一定,但很多情况下它会触发 Full GC,从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:DisableExplicitGC 来禁止 RMI 调用 System.gc()。 -### 2. 老年代空间不足 +**(二)老年代空间不足** 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC,调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。 -### 3. 空间分配担保失败 +**(三)空间分配担保失败** 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC。 -### 4. JDK 1.7 及以前的永久代空间不足 +**(四)JDK 1.7 及以前的永久代空间不足** -在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么 JVM 会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 +在 JDK 1.7 及以前,HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError,为避免以上原因引起的 Full GC,可采用的方法为增大永久代空间或转为使用 CMS GC。 -### 5. Concurrent Mode Failure +**(五)Concurrent Mode Failure** 执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC),便会报 Concurrent Mode Failure 错误,并触发 Full GC。 @@ -446,15 +511,15 @@ JVM 并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才 虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生): -1. 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。 +- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。 -2. 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。 +- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。 -3. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 +- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。 -4. 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类; +- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类; -5. 当使用 JDK.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化; +- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化; 以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括: @@ -486,9 +551,9 @@ System.out.println(ConstClass.HELLOWORLD); 加载过程完成以下三件事: -1. 通过一个类的全限定名来获取定义此类的二进制字节流。 -2. 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。 -3. 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。 +- 通过一个类的全限定名来获取定义此类的二进制字节流。 +- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。 +- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。 其中二进制字节流可以从以下方式中获取: @@ -503,29 +568,16 @@ System.out.println(ConstClass.HELLOWORLD); 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 -主要有以下 4 个阶段: - -(一)文件格式验证 - -验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。 - -(二)元数据验证 - -对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。 - -(三)字节码验证 - -通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。 - -(四)符号引用验证 - -发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。 +- 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。 +- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。 +- 字节码验证:通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。 +- 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。 ### 3. 准备 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 -实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。 +实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次) 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。 @@ -580,7 +632,7 @@ static class Sub extends Parent { } public static void main(String[] args) { - System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值 ,也就是 2。 + System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值,也就是 2。 } ``` @@ -588,15 +640,17 @@ public static void main(String[] args) { - 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。 -- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个进程阻塞,在实际过程中此种阻塞很隐蔽。 +- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。 ## 类加载器 -虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流 ( 即字节码 )”这个动作放到 Java 虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。实现这个动作的代码模块称为“类加载器”。 +实现类的加载动作。在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。 ### 1. 类与类加载器 -对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在 Java 虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。通俗而言:比较两个类是否“相等”(这里所指的“相等”,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况),只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。 +两个类相等:类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。 + +这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true,也包括使用 instanceof 关键字做对象所属关系判定结果为 true。 ### 2. 类加载器分类 @@ -608,7 +662,7 @@ public static void main(String[] args) { 从 Java 开发人员的角度看,类加载器可以划分得更细致一些: -- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 +- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 - 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 @@ -616,43 +670,116 @@ public static void main(String[] args) { ### 3. 双亲委派模型 -应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器,这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。 +应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。 + +下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。

**(一)工作过程** -如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载,而是把这个请求委派给父类加载器,每一个层次的加载器都是如此,依次递归。因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成此加载请求(它搜索范围中没有找到所需类)时,子加载器才会尝试自己加载。 +一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。 **(二)好处** -使用双亲委派模型来组织类加载器之间的关系,使得 Java 类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类 java.lang.Object,它存放在 rt.jar 中,无论哪个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器进行加载,因此 Object 类在程序的各种类加载器环境中都是同一个类。相反,如果没有双亲委派模型,由各个类加载器自行加载的话,如果用户编写了一个称为java.lang.Object 的类,并放在程序的 ClassPath 中,那系统中将会出现多个不同的 Object 类,程序将变得一片混乱。如果开发者尝试编写一个与 rt.jar 类库中已有类重名的 Java 类,将会发现可以正常编译,但是永远无法被加载运行。 +使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而是的基础类得到统一。 + +例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。 **(三)实现** +以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。 + ```java -protected synchronized Class loadClass(String name, boolean resolve) throws ClassNotFoundException{ - // 先检查请求的类是否已经被加载过了 - Class c = findLoadedClass(name); - if(c == null) { - try{ - if(parent != null) { - c = parent.loadClass(name, false); - } else{ - c = findBootstrapClassOrNull(name); +public abstract class ClassLoader { + // The parent class loader for delegation + private final ClassLoader parent; + + public Class loadClass(String name) throws ClassNotFoundException { + return loadClass(name, false); + } + + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + // First, check if the class has already been loaded + Class c = findLoadedClass(name); + if (c == null) { + try { + if (parent != null) { + c = parent.loadClass(name, false); + } else { + c = findBootstrapClassOrNull(name); + } + } catch (ClassNotFoundException e) { + // ClassNotFoundException thrown if class not found + // from the non-null parent class loader + } + + if (c == null) { + // If still not found, then invoke findClass in order + // to find the class. + c = findClass(name); + } } - } catch(ClassNotFoundException e) { - // 如果父类加载器抛出 ClassNotFoundException,说明父类加载器无法完成加载请求 + if (resolve) { + resolveClass(c); + } + return c; + } + } + + protected Class findClass(String name) throws ClassNotFoundException { + throw new ClassNotFoundException(name); + } +} +``` + +### 4. 自定义类加载器实现 + +FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。 + +java.lang.ClassLoader 类的方法 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,而是通过重写 findClass() 方法。 + +```java +public class FileSystemClassLoader extends ClassLoader { + + private String rootDir; + + public FileSystemClassLoader(String rootDir) { + this.rootDir = rootDir; + } + + protected Class findClass(String name) throws ClassNotFoundException { + byte[] classData = getClassData(name); + if (classData == null) { + throw new ClassNotFoundException(); + } else { + return defineClass(name, classData, 0, classData.length); } - if(c == null) { - // 如果父类加载器无法完成加载请求,再调用自身的 findClass() 来进行加载 - c = findClass(name); + } + + private byte[] getClassData(String className) { + String path = classNameToPath(className); + try { + InputStream ins = new FileInputStream(path); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + int bufferSize = 4096; + byte[] buffer = new byte[bufferSize]; + int bytesNumRead; + while ((bytesNumRead = ins.read(buffer)) != -1) { + baos.write(buffer, 0, bytesNumRead); + } + return baos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); } + return null; } - if(resolve) { - resolveClass(c); + + private String classNameToPath(String className) { + return rootDir + File.separatorChar + + className.replace('.', File.separatorChar) + ".class"; } - return c; } ``` @@ -692,3 +819,6 @@ java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC - [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/) - [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/) - [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html) +- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6) +- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap) +- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml) diff --git "a/notes/Leetcode \351\242\230\350\247\243.md" "b/notes/Leetcode \351\242\230\350\247\243.md" index e96fa6f97f4fa0e8022c0889ffc33c8dc6fa28b7..a4bb7a80e7b93ab546596bffca08e3a764c7f2ea 100644 --- "a/notes/Leetcode \351\242\230\350\247\243.md" +++ "b/notes/Leetcode \351\242\230\350\247\243.md" @@ -2208,9 +2208,8 @@ Output : [0, 2] ```java public List diffWaysToCompute(String input) { - int n = input.length(); List ways = new ArrayList<>(); - for (int i = 0; i < n; i++) { + for (int i = 0; i < input.length(); i++) { char c = input.charAt(i); if (c == '+' || c == '-' || c == '*') { List left = diffWaysToCompute(input.substring(0, i)); @@ -2232,9 +2231,10 @@ public List diffWaysToCompute(String input) { } } } - if (ways.size() == 0) { + + if (ways.size() == 0) ways.add(Integer.valueOf(input)); - } + return ways; } ``` @@ -2365,7 +2365,7 @@ private int rob(int[] nums, int first, int last) { 定义一个数组 dp 存储错误方式数量,dp[i] 表示 i 个信和信封的错误方式数量。假设第 i 个信装到第 j 个信封里面,而第 j 个信装到第 k 个信封里面。根据 i 和 k 是否相等,有两种情况: - i==k,交换 i 和 k 的信后,它们的信和信封在正确的位置,但是其余 i-2 封信有 dp[i-2] 种错误装信的方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-2] 种错误装信方式。 -- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (n-1)\*dp[i-1] 种错误装信方式。 +- i != k,交换 i 和 j 的信后,第 i 个信和信封在正确的位置,其余 i-1 封信有 dp[i-1] 种错误装信方式。由于 j 有 i-1 种取值,因此共有 (i-1)\*dp[i-1] 种错误装信方式。 综上所述,错误装信数量方式数量为: @@ -6197,20 +6197,19 @@ We cannot find a way to divide the set of nodes into two independent subsets. public boolean isBipartite(int[][] graph) { int[] colors = new int[graph.length]; Arrays.fill(colors, -1); - - for (int i = 0; i < graph.length; i++) - if (colors[i] != -1 && !isBipartite(graph, i, 0, colors)) + for (int i = 0; i < graph.length; i++) { + if (colors[i] == -1 && !isBipartite(graph, i, 0, colors)) return false; - + } return true; } -private boolean isBipartite(int[][] graph, int cur, int color, int[] colors) { - if (colors[cur] != -1) - return colors[cur] == color; +private boolean isBipartite(int[][] graph, int node, int color, int[] colors) { + if (colors[node] != -1) + return colors[node] == color; - colors[cur] = color; - for (int next : graph[cur]) + colors[node] = color; + for (int next : graph[node]) if (!isBipartite(graph, next, 1 - color, colors)) return false; diff --git "a/notes/Leetcode-Database \351\242\230\350\247\243.md" "b/notes/Leetcode-Database \351\242\230\350\247\243.md" new file mode 100644 index 0000000000000000000000000000000000000000..a38deb95220934fab6e7682589e42572a534e206 --- /dev/null +++ "b/notes/Leetcode-Database \351\242\230\350\247\243.md" @@ -0,0 +1,267 @@ + +* [175. Combine Two Tables](#175-combine-two-tables) +* [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers) +* [183. Customers Who Never Order](#183-customers-who-never-order) +* [184. Department Highest Salary](#184-department-highest-salary) +* [未完待续...](#未完待续) + + + +# 175. Combine Two Tables + +https://leetcode.com/problems/combine-two-tables/description/ + +## Description + +Person 表: + +```html ++-------------+---------+ +| Column Name | Type | ++-------------+---------+ +| PersonId | int | +| FirstName | varchar | +| LastName | varchar | ++-------------+---------+ +PersonId is the primary key column for this table. +``` + +Address 表: + +```html ++-------------+---------+ +| Column Name | Type | ++-------------+---------+ +| AddressId | int | +| PersonId | int | +| City | varchar | +| State | varchar | ++-------------+---------+ +AddressId is the primary key column for this table. +``` + +查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。 + +## SQL Schema + +```sql +DROP TABLE Person; +CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) ); +DROP TABLE Address; +CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) ); +INSERT INTO Person ( PersonId, LastName, FirstName ) +VALUES + ( 1, 'Wang', 'Allen' ); +INSERT INTO Address ( AddressId, PersonId, City, State ) +VALUES + ( 1, 2, 'New York City', 'New York' ); +``` + +## Solution + +使用左外连接。 + +```sql +SELECT FirstName, LastName, City, State +FROM Person AS P LEFT JOIN Address AS A +ON P.PersonId = A.PersonId; +``` + +# 181. Employees Earning More Than Their Managers + +https://leetcode.com/problems/employees-earning-more-than-their-managers/description/ + +## Description + +Employee 表: + +```html ++----+-------+--------+-----------+ +| Id | Name | Salary | ManagerId | ++----+-------+--------+-----------+ +| 1 | Joe | 70000 | 3 | +| 2 | Henry | 80000 | 4 | +| 3 | Sam | 60000 | NULL | +| 4 | Max | 90000 | NULL | ++----+-------+--------+-----------+ +``` + +查找所有员工,它们的薪资大于其经理薪资。 + +## SQL Schema + +```sql +DROP TABLE Employee; +CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT ); +INSERT INTO Employee ( Id, NAME, Salary, ManagerId ) +VALUES + ( '1', 'Joe', '70000', '3' ), + ( '2', 'Henry', '80000', '4' ), + ( '3', 'Sam', '60000', NULL ), + ( '4', 'Max', '90000', NULL ); +``` + +## Solution + +```sql +SELECT E1.Name AS Employee +FROM Employee AS E1, Employee AS E2 +WHERE E1.ManagerId = E2.Id AND E1.Salary > E2.Salary; +``` + +# 183. Customers Who Never Order + +https://leetcode.com/problems/customers-who-never-order/description/ + +## Description + +Curstomers 表: + +```html ++----+-------+ +| Id | Name | ++----+-------+ +| 1 | Joe | +| 2 | Henry | +| 3 | Sam | +| 4 | Max | ++----+-------+ +``` + +Orders 表: + +```html ++----+------------+ +| Id | CustomerId | ++----+------------+ +| 1 | 3 | +| 2 | 1 | ++----+------------+ +``` + +查找没有订单的顾客信息: + +```html ++-----------+ +| Customers | ++-----------+ +| Henry | +| Max | ++-----------+ +``` + +## SQL Schema + +```sql +DROP TABLE Customers; +CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) ); +DROP TABLE Orders; +CREATE TABLE Orders ( Id INT, CustomerId INT ); +INSERT INTO Customers ( Id, NAME ) +VALUES + ( '1', 'Joe' ), + ( '2', 'Henry' ), + ( '3', 'Sam' ), + ( '4', 'Max' ); +INSERT INTO Orders ( Id, CustomerId ) +VALUES + ( '1', '3' ), + ( '2', '1' ); +``` + +## Solution + +左外链接 + +```sql +SELECT C.Name AS Customers +FROM Customers AS C LEFT JOIN Orders AS O +ON C.Id = O.CustomerId +WHERE O.CustomerId IS NULL; +``` + +子查询 + +```sql +SELECT C.Name AS Customers +FROM Customers AS C +WHERE C.Id NOT IN ( + SELECT CustomerId + FROM Orders +); +``` + +# 184. Department Highest Salary + +https://leetcode.com/problems/department-highest-salary/description/ + +## Description + +Employee 表: + +```html ++----+-------+--------+--------------+ +| Id | Name | Salary | DepartmentId | ++----+-------+--------+--------------+ +| 1 | Joe | 70000 | 1 | +| 2 | Henry | 80000 | 2 | +| 3 | Sam | 60000 | 2 | +| 4 | Max | 90000 | 1 | ++----+-------+--------+--------------+ +``` + +Department 表: + +```html ++----+----------+ +| Id | Name | ++----+----------+ +| 1 | IT | +| 2 | Sales | ++----+----------+ +``` + +查找一个 Department 中收入最高者的信息: + +```html ++------------+----------+--------+ +| Department | Employee | Salary | ++------------+----------+--------+ +| IT | Max | 90000 | +| Sales | Henry | 80000 | ++------------+----------+--------+ +``` + +## SQL Schema + +```sql +DROP TABLE Employee; +CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT ); +DROP TABLE Department; +CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) ); +INSERT INTO Employee ( Id, NAME, Salary, DepartmentId ) +VALUES + ( 1, 'Joe', 70000, 1 ), + ( 2, 'Henry', 80000, 2 ), + ( 3, 'Sam', 60000, 2 ), + ( 4, 'Max', 90000, 1 ); +INSERT INTO Department ( Id, NAME ) +VALUES + ( 1, 'IT' ), + ( 2, 'Sales' ); +``` + +## Solution + +```sql +SELECT D.Name AS Department, E.Name AS Employee, E.Salary +FROM Employee AS E, Department AS D, + (SELECT DepartmentId, MAX(Salary) AS Salary + FROM Employee + GROUP BY DepartmentId) AS M +WHERE E.DepartmentId = D.Id + AND E.DepartmentId = M.DepartmentId + AND E.Salary = M.Salary; +``` + +# 未完待续... diff --git a/notes/Linux.md b/notes/Linux.md index 135467a760fdcc955d4e948b54347cf086caf166..f19f3e11bca10e3c1bf437e8c1a3ae3397a72505 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -1,53 +1,62 @@ * [一、常用操作以及概念](#一常用操作以及概念) + * [快捷键](#快捷键) * [求助](#求助) * [关机](#关机) * [PATH](#path) - * [运行等级](#运行等级) * [sudo](#sudo) - * [GNU](#gnu) * [包管理工具](#包管理工具) * [发行版](#发行版) * [VIM 三个模式](#vim-三个模式) -* [二、分区](#二分区) + * [GNU](#gnu) + * [开源协议](#开源协议) +* [二、磁盘](#二磁盘) + * [HDD](#hdd) + * [磁盘接口](#磁盘接口) * [磁盘的文件名](#磁盘的文件名) +* [三、分区](#三分区) * [分区表](#分区表) * [开机检测程序](#开机检测程序) +* [四、文件系统](#四文件系统) + * [分区与文件系统](#分区与文件系统) + * [组成](#组成) + * [文件读取](#文件读取) + * [磁盘碎片](#磁盘碎片) + * [block](#block) + * [inode](#inode) + * [目录](#目录) + * [日志](#日志) * [挂载](#挂载) -* [三、文件](#三文件) - * [文件权限概念](#文件权限概念) - * [文件属性以及权限的修改](#文件属性以及权限的修改) - * [目录的权限](#目录的权限) - * [文件默认权限](#文件默认权限) * [目录配置](#目录配置) - * [文件时间](#文件时间) +* [五、文件](#五文件) + * [文件属性](#文件属性) * [文件与目录的基本操作](#文件与目录的基本操作) + * [修改权限](#修改权限) + * [文件默认权限](#文件默认权限) + * [目录的权限](#目录的权限) + * [链接](#链接) * [获取文件内容](#获取文件内容) * [指令与文件搜索](#指令与文件搜索) -* [四、磁盘与文件系统](#四磁盘与文件系统) - * [文件系统的组成](#文件系统的组成) - * [inode](#inode) - * [目录的 inode 与 block](#目录的-inode-与-block) - * [实体链接与符号链接](#实体链接与符号链接) -* [五、压缩与打包](#五压缩与打包) - * [压缩](#压缩) +* [六、压缩与打包](#六压缩与打包) + * [压缩文件名](#压缩文件名) + * [压缩指令](#压缩指令) * [打包](#打包) -* [六、Bash](#六bash) +* [七、Bash](#七bash) * [特性](#特性) * [变量操作](#变量操作) * [指令搜索顺序](#指令搜索顺序) * [数据流重定向](#数据流重定向) -* [七、管线指令](#七管线指令) +* [八、管线指令](#八管线指令) * [提取指令](#提取指令) * [排序指令](#排序指令) * [双向输出重定向](#双向输出重定向) * [字符转换指令](#字符转换指令) * [分区指令](#分区指令) -* [八、正则表达式](#八正则表达式) +* [九、正则表达式](#九正则表达式) * [grep](#grep) * [printf](#printf) * [awk](#awk) -* [九、进程管理](#九进程管理) +* [十、进程管理](#十进程管理) * [查看进程](#查看进程) * [进程状态](#进程状态) * [SIGCHLD](#sigchld) @@ -55,19 +64,18 @@ * [waitpid()](#waitpid) * [孤儿进程](#孤儿进程) * [僵死进程](#僵死进程) -* [十、I/O 复用](#十io-复用) - * [概念理解](#概念理解) - * [I/O 模型](#io-模型) - * [select poll epoll](#select-poll-epoll) - * [select 和 poll 比较](#select-和-poll-比较) - * [eopll 工作模式](#eopll-工作模式) - * [select poll epoll 应用场景](#select-poll-epoll-应用场景) * [参考资料](#参考资料) # 一、常用操作以及概念 +## 快捷键 + +- Tab:命令和文件名补全; +- Ctrl+C:中断正在运行的程序; +- Ctrl+D:结束键盘输入(End Of File,EOF) + ## 求助 ### 1. --help @@ -78,7 +86,7 @@ man 是 manual 的缩写,将指令的具体信息显示出来。 -当执行 man date 时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下: +当执行`man date`时,有 DATE(1) 出现,其中的数字代表指令的类型,常用的数字及其类型如下: | 代号 | 类型 | | :--: | -- | @@ -90,26 +98,30 @@ man 是 manual 的缩写,将指令的具体信息显示出来。 info 与 man 类似,但是 info 将文档分成一个个页面,每个页面可以进行跳转。 +### 4. doc + +/usr/share/doc 存放着软件的一整套说明文件。 + ## 关机 -### 1. sync +### 1. who + +在关机前需要先使用 who 命令查看有没有其它用户在线。 + +### 2. sync 为了加快对磁盘文件的读写速度,位于内存中的文件数据不会立即同步到磁盘上,因此关机之前需要先进行 sync 同步操作。 -### 2. shutdown +### 3. shutdown ```html -# /sbin/shutdown [-krhc] [时间] [警告讯息] --k : 不会关机,只是发送警告讯息,通知所有在线的用户 +# shutdown [-krhc] 时间 [信息] +-k : 不会关机,只是发送警告信息,通知所有在线的用户 -r : 将系统的服务停掉后就重新启动 -h : 将系统的服务停掉后就立即关机 -c : 取消已经在进行的 shutdown 指令内容 ``` -### 3. 其它关机指令 - -reboot、halt、poweroff。 - ## PATH 可以在环境变量 PATH 中声明可执行文件的路径,路径之间用 : 分隔。 @@ -118,46 +130,28 @@ reboot、halt、poweroff。 /usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/dmtsai/.local/bin:/home/dmtsai/bin ``` -## 运行等级 - -- 0:关机模式 -- 1:单用户模式(可用于破解 root 密码) -- 2:无网络支持的多用户模式 -- 3:有网络支持的多用户模式(文本模式,工作中最常用的模式) -- 4:保留,未使用 -- 5:有网络支持的 X-windows 多用户模式(桌面) -- 6:重新引导系统,即重启 - ## sudo -使用 sudo 允许一般用户使用 root 可执行的命令,只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。 - -## GNU - -GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容: - -- 以任何目的运行此程序的自由; -- 再复制的自由; -- 改进此程序,并公开发布改进的自由。 +sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sudoers 配置文件中添加的用户才能使用该指令。 ## 包管理工具 -RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具- DPKG,全称为 Debian Package,功能方面与 RPM 相似。 +RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager,最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具 DPKG,全称为 Debian Package,功能方面与 RPM 相似。 -YUM 基于 RPM 包管理工具,具有依赖管理功能,并具有软件升级的功能。 +YUM 基于 RPM,具有依赖管理功能,并具有软件升级的功能。 ## 发行版 Linux 发行版是 Linux 内核及各种应用软件的集成版本。 | 基于的包管理工具 | 商业发行版 | 社区发行版 | -| --- | --- | --- | -| DPKG | Ubuntu | Debian | +| :--: | :--: | :--: | | RPM | Red Hat | Fedora / CentOS | +| DPKG | Ubuntu | Debian | ## VIM 三个模式 -- 一般指令模式(Command mode):进入 VIM 的默认模式,可以用于移动游标查看内容; +- 一般指令模式(Command mode):VIM 的默认模式,可以用于移动游标查看内容; - 编辑模式(Insert mode):按下 "i" 等按键之后进入,可以对文本进行编辑; - 指令列模式(Bottom-line mode):按下 ":" 按键之后进入,用于保存退出等操作。 @@ -166,7 +160,7 @@ Linux 发行版是 Linux 内核及各种应用软件的集成版本。 在指令列模式下,有以下命令用于离开或者保存文件。 | 命令 | 作用 | -| :--: | -- | +| :--: | :--: | | :w | 写入磁盘| | :w! | 当文件为只读时,强制写入磁盘。到底能不能写入,与用户对该文件的权限有关 | | :q | 离开 | @@ -174,16 +168,72 @@ Linux 发行版是 Linux 内核及各种应用软件的集成版本。 | :wq | 写入磁盘后离开 | | :wq!| 强制写入磁盘后离开 | -# 二、分区 +## GNU + +GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操作系统,称为 GNU,其内容软件完全以 GPL 方式发布。其中 GPL 全称为 GNU 通用公共许可协议,包含了以下内容: + +- 以任何目的运行此程序的自由; +- 再复制的自由; +- 改进此程序,并公开发布改进的自由。 + +## 开源协议 + +- [Choose an open source license](https://choosealicense.com/) +- [如何选择开源许可证?](http://www.ruanyifeng.com/blog/2011/05/how_to_choose_free_software_licenses.html) + +# 二、磁盘 + +## HDD + +[Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) + +Hard Disk Drives(HDD) 俗称硬盘,具有以下结构: + +- 盘面(Platter):一个硬盘有多个盘面; +- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道; +- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小; +- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写); +- 制动手臂(Actuator arm):用于在磁道之间移动磁头; +- 主轴(Spindle):使整个盘面转动。 + +

+ +## 磁盘接口 + +### 1. IDE + +IDE(ATA)全称 Advanced Technology Attachment,接口速度最大为 133MB/s,因为并口线的抗干扰性太差,且排线占用空间较大,不利电脑内部散热,已逐渐被 SATA 所取代。 + +

+ +### 2. SATA + +SATA 全称 Serial ATA,也就是使用串口的 ATA 接口,因抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能,SATA-II 的接口速度为 300MiB/s,而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。 + +

+ +### 3. SCSI + +SCSI 全称是 Small Computer System Interface(小型机系统接口),经历多代的发展,从早期的 SCSI-II,到目前的 Ultra320 SCSI 以及 Fiber-Channel(光纤通道),接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且资料传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。 + +

+ +### 4. SAS + +SAS(Serial Attached SCSI)是新一代的 SCSI 技术,和 SATA 硬盘相同,都是采取序列式技术以获得更高的传输速度,可达到 6Gb/s。此外也透过缩小连接线改善系统内部空间等。 + +

## 磁盘的文件名 -Linux 中每个硬件都被当做一个文件,包括磁盘。常见磁盘的文件名如下: +Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘接口类型进行命名,常见磁盘的文件名如下: -- SCSI/SATA/USB 磁盘:/dev/sd[a-p] - IDE 磁盘:/dev/hd[a-d] +- SATA/SCSI/SAS 磁盘:/dev/sd[a-p] -其中文件名后面的序号的确定与磁盘插入的顺序有关,而与磁盘所插入的插槽位置无关。 +其中文件名后面的序号的确定与系统侦测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。 + +# 三、分区 ## 分区表 @@ -193,17 +243,19 @@ Linux 中每个硬件都被当做一个文件,包括磁盘。常见磁盘的 MBR 中,第一个扇区最重要,里面有主要开机记录(Master boot record, MBR)及分区表(partition table),其中 MBR 占 446 bytes,分区表占 64 bytes。 -分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它空间用来记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 +分区表只有 64 bytes,最多只能存储 4 个分区,这 4 个分区为主分区(Primary)和扩展分区(Extended)。其中扩展分区只有一个,它将其它扇区用来记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意,逻辑分区的编号从 5 开始。 ### 2. GPT -不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA)。 +不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘,在定义扇区上使用逻辑区块地址(Logical Block Address, LBA),LBA 默认大小为 512 bytes。 + +GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。 -GPT 第 1 个区块记录了 MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。 +GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。 -GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区。 +MBR 不支持 2.2 TB 以上的硬盘,GPT 则最多支持到 233 TB = 8 ZB。

@@ -211,29 +263,120 @@ GPT 没有扩展分区概念,都是主分区,最多可以分 128 个分区 ### 1. BIOS +BIOS(Basic Input/Output System,基本输入输出系统),它是一个固件(嵌入在硬件中的软件),BIOS 程序存放在断电后内容不会丢失的只读内存中。 + BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的 MBR,由 MBR 执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。 -MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 +

-安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR,而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 +MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统。 + +下图中,第一扇区的 MBR 中的开机管理程序提供了两个选单:M1、M2,M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。

+安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR,而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 + ### 2. UEFI -UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 +BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 + +# 四、文件系统 + +## 分区与文件系统 + +对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统。 + +## 组成 + +

+ +最主要的几个组成部分如下: + +- inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号; +- block:记录文件的内容,文件太大时,会占用多个 block。 + +除此之外还包括: + +- superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; +- block bitmap:记录 block 是否被使用的位域; + +## 文件读取 + +对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 + +

+ +而对于 FAT 文件系统,它没有 inode,每个 block 中存储着下一个 block 的编号。 + +

+ +## 磁盘碎片 + +指一个文件内容所在的 block 过于分散。 + +## block + +在 Ext2 文件系统中所支持的 block 大小有 1K,2K 及 4K 三种,不同的大小限制了单个文件和文件系统的最大大小。 + +| 大小 | 1KB | 2KB | 4KB | +| :---: | :---: | :---: | :---: | +| 最大单一文件 | 16GB | 256GB | 2TB | +| 最大文件系统 | 2TB | 8TB | 16TB | + +一个 block 只能被一个文件所使用,未使用的部分直接浪费了。因此如果需要存储大量的小文件,那么最好选用比较小的 block。 + +## inode + +inode 具体包含以下信息: + +- 权限 (read/write/excute); +- 拥有者与群组 (owner/group); +- 容量; +- 建立或状态改变的时间 (ctime); +- 最近一次的读取时间 (atime); +- 最近修改的时间 (mtime); +- 定义文件特性的旗标 (flag),如 SetUID...; +- 该文件真正内容的指向 (pointer)。 + +inode 具有以下特点: + +- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes); +- 每个文件都仅会占用一个 inode。 + +inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块当成 inode 用来记录引用信息。 + +

+ +## 目录 + +建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 + +## 日志 + +如果突然断电,那么文件系统会发生错误,例如断电前只修改了 block bitmap,而还没有将数据真正写入 block 中。 + +ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件系统。 ## 挂载 -挂载利用目录作为分区的进入点,也就是说,进入目录之后就可以读取分区的数据。 +挂载利用目录作为文件系统的进入点,也就是说,进入目录之后就可以读取文件系统的数据。 -

+## 目录配置 -# 三、文件 +为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下: -## 文件权限概念 +- / (root, 根目录) +- /usr (unix software resource):所有系统默认软件都会安装到这个目录; +- /var (variable):存放系统或程序运行过程中的数据文件。 + +

+ +# 五、文件 + +## 文件属性 -把用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 +用户分为三种:文件拥有者、群组以及其它人,对不同的用户有不同的文件权限。 使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下: @@ -253,24 +396,102 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 -## 文件属性以及权限的修改 +文件时间有以下三种: + +- modification time (mtime):文件的内容更新就会更新; +- status time (ctime):文件的状态(权限、属性)更新就会更新; +- access time (atime):读取文件时就会更新。 + +## 文件与目录的基本操作 + +### 1. ls + +列出文件或者目录的信息,目录的信息就是其中包含的文件。 + +```html +# ls [-aAdfFhilnrRSt] file|dir +-a :列出全部的文件 +-d :仅列出目录本身 +-l :以长数据串行列出,包含文件的属性与权限等等数据 +``` + +### 2. cd + +更换当前目录。 + +``` +cd [相对路径或绝对路径] +``` + +### 3. mkdir + +创建目录。 + +``` +# mkdir [-mp] 目录名称 +-m :配置目录权限 +-p :递归创建目录 +``` + +### 4. rmdir + +删除目录,必须为空。 + +```html +rmdir [-p] 目录名称 +-p :递归删除目录 +``` + +### 5. touch + +更新文件时间或者建立新文件。 + +```html +# touch [-acdmt] filename +-a : 更新 atime +-c : 更新 ctime,若该文件不存在则不建立新文件 +-m : 更新 mtime +-d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" +-t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] +``` + +### 6. cp + +复制文件。 + +如果源文件有两个以上,则目的文件一定要是目录才行。 + +```html +cp [-adfilprsu] source destination +-a :相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明 +-d :若来源文件为链接文件,则复制链接文件属性而非文件本身 +-i :若目标文件已经存在时,在覆盖前会先询问 +-p :连同文件的属性一起复制过去 +-r :递归持续复制 +-u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 +--preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 +``` -### 1. 修改文件所属群组 +### 7. rm + +删除文件。 ```html -# chgrp [-R] groupname dirname/filename --R:递归修改 +# rm [-fir] 文件或目录 +-r :递归删除 ``` -### 2. 修改文件拥有者 +### 8. mv -不仅可以修改文件拥有者,也可以修改文件所属群组。 +移动文件。 ```html -# chown [-R] 用户名:群组名 dirname/filename +# mv [-fiu] source destination +# mv [options] source1 source2 source3 .... directory +-f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 ``` -### 3. 修改权限 +## 修改权限 可以将一组权限用数字来表示,此时一组权限的 3 个位当做二进制数字的位,从左到右每个位的权值为 4、2、1,即每个权限对应的数字权值为 r : 4、w : 2、x : 1。 @@ -303,82 +524,54 @@ UEFI 相比于 BIOS 来说功能更为全面,也更为安全。 # chmod a+w .bashrc ``` -## 目录的权限 - -文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 - -目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 - ## 文件默认权限 - 文件默认权限:文件默认没有可执行权限,因此为 666,也就是 -rw-rw-rw- 。 - 目录默认权限:目录必须要能够进入,也就是必须拥有可执行权限,因此为 777 ,也就是 drwxrwxrwx。 -可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r-- 。 - -## 目录配置 - -为了使不同 Linux 发行版本的目录结构保持一致性,Filesystem Hierarchy Standard (FHS) 规定了 Linux 的目录结构。最基础的三个目录如下: - -- / (root, 根目录) -- /usr (unix software resource):所有系统默认软件都会安装到这个目录; -- /var (variable):存放系统或程序运行过程中的数据文件。 - -

- -## 文件时间 +可以通过 umask 设置或者查看文件的默认权限,通常以掩码的形式来表示,例如 002 表示其它用户的权限去除了一个 2 的权限,也就是写权限,因此建立新文件时默认的权限为 -rw-rw-r--。 -1. modification time (mtime):文件的内容更新就会更新; -2. status time (ctime):文件的状态(权限、属性)更新就会更新; -3. access time (atime):读取文件时就会更新。 +## 目录的权限 -## 文件与目录的基本操作 +文件名不是存储在一个文件的内容中,而是存储在一个文件所在的目录中。因此,拥有文件的 w 权限并不能对文件名进行修改。 -### 1. ls +目录存储文件列表,一个目录的权限也就是对其文件列表的权限。因此,目录的 r 权限表示可以读取文件列表;w 权限表示可以修改文件列表,具体来说,就是添加删除文件,对文件名进行修改;x 权限可以让该目录成为工作目录,x 权限是 r 和 w 权限的基础,如果不能使一个目录成为工作目录,也就没办法读取文件列表以及对文件列表进行修改了。 -列出文件或者目录的信息,目录的信息就是其中包含的文件。 +## 链接 ```html -# ls [-aAdfFhilnrRSt] file|dir --a :列出全部的文件 --d :仅列出目录本身 --l :以长数据串行列出,包含文件的属性与权限等等数据 +# ln [-sf] source_filename dist_filename +-s :默认是 hard link,加 -s 为 symbolic link +-f :如果目标文件存在时,先删除目标文件 ``` -### 2. cp +### 1. 实体链接 -复制操作。 +它和普通文件类似,实体链接文件的 inode 都指向源文件所在的 block 上,也就是说读取文件直接从源文件的 block 上读取。 -如果源文件有两个以上,则目的文件一定要是目录才行。 +删除任意一个条目,文件还是存在,只要引用数量不为 0。 + +有以下限制:不能跨越 File System、不能对目录进行链接。 ```html -cp [-adfilprsu] source destination --a :相当于 -dr --preserve=all 的意思,至于 dr 请参考下列说明 --d :若来源文件为链接文件,则复制链接文件属性而非文件本身 --i :若目标文件已经存在时,在覆盖前会先询问 --p :连同文件的属性一起复制过去 --r :递归持续复制 --u :destination 比 source 旧才更新 destination,或 destination 不存在的情况下才复制 ---preserve=all :除了 -p 的权限相关参数外,还加入 SELinux 的属性, links, xattr 等也复制了 +# ln /etc/crontab . +# ll -i /etc/crontab crontab +34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab +34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab ``` -### 3. rm - -移除操作。 +### 2. 符号链接 -```html -# rm [-fir] 文件或目录 --r :递归删除 -``` +符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。 -### 4. mv +当源文件被删除了,链接文件就打不开了。 -移动操作。 +可以为目录建立链接。 ```html -# mv [-fiu] source destination -# mv [options] source1 source2 source3 .... directory --f : force 强制的意思,如果目标文件已经存在,不会询问而直接覆盖 +# ll -i /etc/crontab /root/crontab2 +34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab +53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab ``` ## 获取文件内容 @@ -398,15 +591,15 @@ cp [-adfilprsu] source destination ### 3. more -可以一页一页查看文件内容,和文本编辑器类似。 +和 cat 不同的是它可以一页一页查看文件内容,比较适合大文件的查看。 ### 4. less -和 more 类似。 +和 more 类似,但是多了一个向前翻页的功能。 ### 5. head -可以取得文件前几行。 +取得文件前几行。 ```html # head [-n number] filename @@ -419,20 +612,7 @@ cp [-adfilprsu] source destination ### 7. od -可以以字符或者十六进制的形式显示二进制文件。 - -### 8. touch - -修改文件时间或者建立新文件。 - -```html -# touch [-acdmt] filename --a : 更新 atime --c : 更新 ctime,若该文件不存在则不建立新文件 --m : 更新 mtime --d : 后面可以接更新日期而不使用当前日期,也可以使用 --date="日期或时间" --t : 后面可以接更新时间而不使用当前时间,格式为[YYYYMMDDhhmm] -``` +以字符或者十六进制的形式显示二进制文件。 ## 指令与文件搜索 @@ -447,7 +627,7 @@ cp [-adfilprsu] source destination ### 2. whereis -whereis 搜索文件的速度比较快,因为它只搜索几个特定的目录。 +文件搜索。速度比较快,因为它只搜索几个特定的目录。 ```html # whereis [-bmsu] dirname/filename @@ -455,18 +635,18 @@ whereis 搜索文件的速度比较快,因为它只搜索几个特定的目录 ### 3. locate -locate 可以用关键字或者正则表达式进行搜索。 +文件搜索。可以用关键字或者正则表达式进行搜索。 locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内存中,并且每天更新一次,所以无法用 locate 搜索新建的文件。可以使用 updatedb 来立即更新数据库。 ```html # locate [-ir] keyword --r:接正则表达式 +-r:正则表达式 ``` ### 4. find -find 可以使用文件的属性和权限进行搜索。 +文件搜索。可以使用文件的属性和权限进行搜索。 ```html # find [basedir] [option] @@ -508,89 +688,11 @@ example: find . -name "shadow*" -perm /mode :搜索权限包含任一 mode 的文件 ``` -# 四、磁盘与文件系统 - -## 文件系统的组成 - -对分区进行格式化是为了在分区上建立文件系统。一个分区通常只能格式化为一个文件系统,但是磁盘阵列等技术可以将一个分区格式化为多个文件系统,因此只有文件系统能被挂载,而分区不能被挂载。 - -文件系统有以下三个结构: - -1. superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; -2. inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 号码; -3. block:记录文件的内容,文件太大时,会占用多个 block。 - -

- -当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 - -磁盘碎片是指一个文件内容所在的 block 过于分散。 - -## inode - -Ext2 文件系统支持的 block 大小有 1k、2k 和 4k 三种,不同的 block 大小限制了单一文件的大小。而每个 inode 大小是固定为 128 bytes。 - -inode 中记录了文件内容所在的 block,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块当成 inode 用来记录引用信息。 - -

- -inode 具体包含以下信息: - -- 该文件的存取模式 (read/write/excute); -- 该文件的拥有者与群组 (owner/group); -- 该文件的容量; -- 该文件建立或状态改变的时间 (ctime); -- 最近一次的读取时间 (atime); -- 最近修改的时间 (mtime); -- 定义文件特性的旗标 (flag),如 SetUID...; -- 该文件真正内容的指向 (pointer)。 - -## 目录的 inode 与 block - -建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 - -## 实体链接与符号链接 - -```html -# ln [-sf] source_filename dist_filename --s :默认是 hard link,加 -s 为 symbolic link --f :如果目标文件存在时,先删除目标文件 -``` - -### 1. 实体链接 - -它和普通文件类似,实体链接文件的 inode 都指向源文件所在的 block 上,也就是说读取文件直接从源文件的 block 上读取。 - -删除任意一个条目,文件还是存在,只要引用数量不为 0。 - -有以下限制:不能跨越 File System、不能对目录进行链接。 - -```html -# ln /etc/crontab . -# ll -i /etc/crontab crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 crontab -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -``` - -### 2. 符号链接 - -符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 Windows 的快捷方式。 - -当源文件被删除了,链接文件就打不开了。 - -可以为目录建立链接。 - -```html -# ll -i /etc/crontab /root/crontab2 -34474855 -rw-r--r--. 2 root root 451 Jun 10 2014 /etc/crontab -53745909 lrwxrwxrwx. 1 root root 12 Jun 23 22:31 /root/crontab2 -> /etc/crontab -``` - -# 五、压缩与打包 +# 六、压缩与打包 -## 压缩 +## 压缩文件名 -Linux 底下有很多压缩文件的扩展名,常见的如下: +Linux 底下有很多压缩文件名,常见的如下: | 扩展名 | 压缩程序 | | -- | -- | @@ -604,6 +706,8 @@ Linux 底下有很多压缩文件的扩展名,常见的如下: |\*.tar.bz2 | tar 程序打包的文件,经过 bzip2 的压缩 | |\*.tar.xz | tar 程序打包的文件,经过 xz 的压缩 | +## 压缩指令 + ### 1. gzip gzip 是 Linux 使用最广的压缩指令,可以解开 compress、zip 与 gzip 所压缩的文件。 @@ -666,32 +770,34 @@ $ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩 ``` | 使用方式 | 命令 | -| --- | --- | +| :---: | --- | | 打包压缩 | tar -jcv -f filename.tar.bz2 要被压缩的文件或目录名称 | | 查 看 | tar -jtv -f filename.tar.bz2 | | 解压缩 | tar -jxv -f filename.tar.bz2 -C 要解压缩的目录 | -# 六、Bash +# 七、Bash 可以通过 Shell 请求内核提供服务,Bash 正是 Shell 的一种。 ## 特性 -1. 命令历史:记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中,\~/.bash_history 文件中记录的是前一次登录所执行过的命令。 +- 命令历史:记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中,\~/.bash_history 文件中记录的是前一次登录所执行过的命令。 -2. 命令与文件补全:快捷键:tab。 +- 命令与文件补全:快捷键:tab。 -3. 命名别名:例如 lm 是 ls -al 的别名。 +- 命名别名:例如 lm 是 ls -al 的别名。 -4. shell scripts。 +- shell scripts。 -5. 通配符:例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。 +- 通配符:例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。 ## 变量操作 -- 对一个变量赋值直接使用 = ; -- 对变量取用需要在变量前加上 \$ ,也可以用 \${} 的形式; -- 输出变量使用 echo 命令。 +对一个变量赋值直接使用 =。 + +对变量取用需要在变量前加上 \$ ,也可以用 \${} 的形式; + +输出变量使用 echo 命令。 ```bash $ var=abc @@ -699,7 +805,7 @@ $ echo $var $ echo ${var} ``` -变量内容如果有空格,必须需要使用双引号或者单引号。 +变量内容如果有空格,必须使用双引号或者单引号。 - 双引号内的特殊字符可以保留原本特性,例如 var="lang is \$LANG",则 var 的值为 lang is zh_TW.UTF-8; - 单引号内的特殊字符就是特殊字符本身,例如 var='lang is \$LANG',则 var 的值为 lang is \$LANG。 @@ -728,10 +834,10 @@ $ echo ${array[1]} ## 指令搜索顺序 -1. 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ; -2. 由别名找到该指令来执行; -3. 由 Bash 内建的指令来执行; -4. 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。 +- 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls ; +- 由别名找到该指令来执行; +- 由 Bash 内建的指令来执行; +- 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。 ## 数据流重定向 @@ -740,7 +846,7 @@ $ echo ${array[1]} | 1 | 代码 | 运算符 | | :---: | :---: | :---:| | 标准输入 (stdin) | 0 | < 或 << | -| 标准输出 (stdout) | 1 | > 或 >> | +| 标准输出 (stdout) | 1 | > 或 >> | | 标准错误输出 (stderr) | 2 | 2> 或 2>> | 其中,有一个箭头的表示以覆盖的方式重定向,而有两个箭头的表示以追加的方式重定向。 @@ -753,7 +859,7 @@ $ echo ${array[1]} $ find /home -name .bashrc > list 2>&1 ``` -# 七、管线指令 +# 八、管线指令 管线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线。在命令之间使用 | 分隔各个管线命令。 @@ -763,7 +869,7 @@ $ ls -al /etc | less ## 提取指令 -cut 对数据进行切分,取出想要的部分。提取过程一行一行地进行。 +cut 对数据进行切分,取出想要的部分。切分过程一行一行地进行。 ```html $ cut @@ -812,7 +918,7 @@ $ sort [-fbMnrtuk] [file or stdin] -k :指定排序的区间 ``` -范例:/etc/passwd 内容是以 : 来分隔的,以第三栏来排序。 +范例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。 ```html $ cat /etc/passwd | sort -t ':' -k 3 @@ -907,7 +1013,7 @@ $ split [-bl] file PREFIX - PREFIX :分区文件的前导名称 ``` -# 八、正则表达式 +# 九、正则表达式 ## grep @@ -958,7 +1064,7 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) 可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。 ```html -$ awk ' 条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename +$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename ``` awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。 @@ -1006,7 +1112,7 @@ bin 1 daemon 2 ``` -# 九、进程管理 +# 十、进程管理 ## 查看进程 @@ -1095,7 +1201,7 @@ pid_t wait(int *status) 如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 - 1,同时 errno 被置为 ECHILD。 -参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为 NULL: +参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,以设定这个参数为 NULL: ```c pid = wait(NULL); @@ -1104,12 +1210,12 @@ pid = wait(NULL); ## waitpid() ```c -pid_t waitpid(pid_t pid,int *status,int options) +pid_t waitpid(pid_t pid, int *status, int options) ``` 作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。 -pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么贺 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 +pid 参数指示一个子进程的 ID,表示只关心这个子进程的退出 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。 @@ -1129,299 +1235,22 @@ options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 w 要消灭系统中大量的僵死进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵死进程。 -# 十、I/O 复用 - -## 概念理解 - -I/O Multiplexing 又被称为 Event Driven I/O,它可以让单个进程具有处理多个 I/O 事件的能力。 - -当某个 I/O 事件条件满足时,进程会收到通知。 - -如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时连接几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。 - -## I/O 模型 - -- 阻塞(Blocking) -- 非阻塞(Non-blocking) -- 同步(Synchronous) -- 异步(Asynchronous) - -阻塞非阻塞是等待 I/O 完成的方式,阻塞要求用户程序停止执行,直到 I/O 完成,而非阻塞在 I/O 完成之前还可以继续执行。 - -同步异步是获知 I/O 完成的方式,同步需要时刻关心 I/O 是否已经完成,异步无需主动关心,在 I/O 完成时它会收到通知。 - -

- - -### 1. 同步-阻塞 - -这是最常见的一种模型,用户程序在使用 read() 时会执行系统调用从而陷入内核,之后就被阻塞直到系统调用完成。 - -应该注意到,在阻塞的过程中,其他程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。 - -

- -### 2. 同步-非阻塞 - -非阻塞意味着用户程序在执行系统调用后还可以继续执行,内核并不是马上执行完 I/O,而是以一个错误码来告知用户程序 I/O 还未完成。为了获得 I/O 完成事件,用户程序必须调用多次系统调用去询问内核,甚至是忙等,也就是在一个循环里面一直询问并等待。 - -由于 CPU 要处理更多的用户程序的询问,因此这种模型的效率是比较低的。 - -

- -### 3. 异步 - -该模式下,I/O 操作会立即返回,之后可以处理其它操作,并且在 I/O 完成时会收到一个通知,此时会中断正在处理的操作,然后继续之前的操作。 - -

- -## select poll epoll - -这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。 - -### 1. select - -```c -int select (int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); -``` - -- fd_set 表示描述符集合; -- readset、writeset 和 exceptset 这三个参数指定让操作系统内核测试读、写和异常条件的描述符; -- timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间; -- 成功调用返回结果大于 0;出错返回结果为 -1;超时返回结果为 0。 - -```c -fd_set fd_in, fd_out; -struct timeval tv; - -// Reset the sets -FD_ZERO( &fd_in ); -FD_ZERO( &fd_out ); - -// Monitor sock1 for input events -FD_SET( sock1, &fd_in ); - -// Monitor sock2 for output events -FD_SET( sock2, &fd_out ); - -// Find out which socket has the largest numeric value as select requires it -int largest_sock = sock1 > sock2 ? sock1 : sock2; - -// Wait up to 10 seconds -tv.tv_sec = 10; -tv.tv_usec = 0; - -// Call the select -int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv ); - -// Check if select actually succeed -if ( ret == -1 ) - // report error and abort -else if ( ret == 0 ) - // timeout; no event detected -else -{ - if ( FD_ISSET( sock1, &fd_in ) ) - // input event on sock1 - - if ( FD_ISSET( sock2, &fd_out ) ) - // output event on sock2 -} -``` - -每次调用 select() 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从用户进程内存中复制到操作系统内核中,内核需要将所有 fd_set 遍历一遍,这个过程非常低效。 - -返回结果中内核并没有声明哪些 fd_set 已经准备好了,所以如果返回值大于 0 时,程序需要遍历所有的 fd_set 判断哪个 I/O 已经准备好。 - -在 Linux 中 select 最多支持 1024 个 fd_set 同时轮询,其中 1024 由 Linux 内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE,然后重新编译内核。 - -### 2. poll - -```c -int poll (struct pollfd *fds, unsigned int nfds, int timeout); -``` - -```c -struct pollfd { - int fd; //文件描述符 - short events; //监视的请求事件 - short revents; //已发生的事件 -}; -``` - -```c -// The structure for two events -struct pollfd fds[2]; - -// Monitor sock1 for input -fds[0].fd = sock1; -fds[0].events = POLLIN; - -// Monitor sock2 for output -fds[1].fd = sock2; -fds[1].events = POLLOUT; - -// Wait 10 seconds -int ret = poll( &fds, 2, 10000 ); -// Check if poll actually succeed -if ( ret == -1 ) - // report error and abort -else if ( ret == 0 ) - // timeout; no event detected -else -{ - // If we detect the event, zero it out so we can reuse the structure - if ( pfd[0].revents & POLLIN ) - pfd[0].revents = 0; - // input event on sock1 - - if ( pfd[1].revents & POLLOUT ) - pfd[1].revents = 0; - // output event on sock2 -} -``` - -它和 select() 功能基本相同。同样需要每次将 struct pollfd \*fds 复制到内核,返回后同样需要进行轮询每一个 pollfd 是否已经 I/O 准备好。poll() 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。poll() 在 pollfd 的重复利用上比 select() 的 fd_set 会更好。 - -如果在多线程下,如果一个线程对某个描述符调用了 poll() 系统调用,但是另一个线程关闭了该描述符,会导致 poll() 调用结果不确定,该问题同样出现在 select() 中。 - -### 3. epoll - -```c -int epoll_create(int size); -int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); -int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); -``` - -```c -// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets. -// The function argument is ignored (it was not before, but now it is), so put your favorite number here -int pollingfd = epoll_create( 0xCAFE ); - -if ( pollingfd < 0 ) - // report error - -// Initialize the epoll structure in case more members are added in future -struct epoll_event ev = { 0 }; - -// Associate the connection class instance with the event. You can associate anything -// you want, epoll does not use this information. We store a connection class pointer, pConnection1 -ev.data.ptr = pConnection1; - -// Monitor for input, and do not automatically rearm the descriptor after the event -ev.events = EPOLLIN | EPOLLONESHOT; -// Add the descriptor into the monitoring list. We can do it even if another thread is -// waiting in epoll_wait - the descriptor will be properly added -if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 ) - // report error - -// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen) -struct epoll_event pevents[ 20 ]; - -// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array -int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); -// Check if epoll actually succeed -if ( ret == -1 ) - // report error and abort -else if ( ret == 0 ) - // timeout; no event detected -else -{ - // Check if any events detected - for ( int i = 0; i < ret; i++ ) - { - if ( pevents[i].events & EPOLLIN ) - { - // Get back our connection pointer - Connection * c = (Connection*) pevents[i].data.ptr; - c->handleReadEvent(); - } - } -} -``` - -epoll 仅仅适用于 Linux OS。 - -它是 select 和 poll 的增强版,更加灵活而且没有描述符限制。它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次。 - -select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。 - -新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size,不保证服务质量。 - -epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。 - -epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到程序中,不需要像 select/poll 对注册的所有描述符遍历一遍。 - -epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。 - -## select 和 poll 比较 - -### 1. 功能 - -它们提供了几乎相同的功能,但是在一些细节上有所不同: - -- select 会修改 fd_set 参数,而 poll 不会; -- select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译; -- poll 提供了更多的事件类型。 - -### 2. 速度 - -poll 和 select 在速度上都很慢。 - -- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢; -- select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。 - -### 3. 可移植性 - -几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。 - -## eopll 工作模式 - -epoll_event 有两种触发模式:LT(level trigger)和 ET(edge trigger)。 - -### 1. LT 模式 - -当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。 - -### 2. ET 模式 - -当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 时,不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。 - -## select poll epoll 应用场景 - -很容易产生一种错觉认为只要用 epoll 就可以了,select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。 - -### 1. select 应用场景 - -select() poll() epoll_wait() 都有一个 timeout 参数,在 select() 中 timeout 的精确度为 1ns,而 poll() 和 epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求更高的场景,比如核反应堆的控制。 - -select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。 - -### 2. poll 应用场景 - -poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select。 - -需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。 - -需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。 - -### 3. epoll 应用场景 - -程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。 - -### 4. 性能对比 - -> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html) - # 参考资料 - 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009. - [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html) -- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/) -- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx) -- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859) -- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html) - [Linux 之守护进程、僵死进程与孤儿进程](http://liubigbin.github.io/2016/03/11/Linux-%E4%B9%8B%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E3%80%81%E5%83%B5%E6%AD%BB%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%AD%A4%E5%84%BF%E8%BF%9B%E7%A8%8B/) - [Linux process states](https://idea.popcount.org/2012-12-11-linux-process-states/) - [GUID Partition Table](https://en.wikipedia.org/wiki/GUID_Partition_Table) - [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719) +- [IDE、SATA、SCSI、SAS、FC、SSD 硬盘类型介绍](https://blog.csdn.net/tianlesoftware/article/details/6009110) +- [Akai IB-301S SCSI Interface for S2800,S3000](http://www.mpchunter.com/s3000/akai-ib-301s-scsi-interface-for-s2800s3000/) +- [Parallel ATA](https://en.wikipedia.org/wiki/Parallel_ATA) +- [ADATA XPG SX900 256GB SATA 3 SSD Review – Expanded Capacity and SandForce Driven Speed](http://www.thessdreview.com/our-reviews/adata-xpg-sx900-256gb-sata-3-ssd-review-expanded-capacity-and-sandforce-driven-speed/4/) +- [Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) +- [硬盘](https://zh.wikipedia.org/wiki/%E7%A1%AC%E7%9B%98) +- [Difference between SAS and SATA](http://www.differencebetween.info/difference-between-sas-and-sata) +- [BIOS](https://zh.wikipedia.org/wiki/BIOS) +- [File system design case studies](https://www.cs.rutgers.edu/\~pxk/416/notes/13-fs-studies.html) +- [Programming Project #4](https://classes.soe.ucsc.edu/cmps111/Fall08/proj4.shtml) +- [FILE SYSTEM DESIGN](http://web.cs.ucla.edu/classes/fall14/cs111/scribe/11a/index.html) diff --git a/notes/MySQL.md b/notes/MySQL.md index 4d38566f68da310e0014ac708067b493482c5cd0..0e39a69c578d106e9ee57029581ba5a0ca74f0c4 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -9,17 +9,19 @@ * [字符串](#字符串) * [时间和日期](#时间和日期) * [三、索引](#三索引) + * [B-Tree 和 B+Tree 原理](#b-tree-和-btree-原理) * [索引分类](#索引分类) * [索引的优点](#索引的优点) * [索引优化](#索引优化) - * [B-Tree 和 B+Tree 原理](#b-tree-和-b+tree-原理) * [四、查询性能优化](#四查询性能优化) + * [使用 Explain 进行分析](#使用-explain-进行分析) + * [优化数据访问](#优化数据访问) + * [重构查询方式](#重构查询方式) * [五、切分](#五切分) - * [垂直切分](#垂直切分) * [水平切分](#水平切分) - * [切分的选择](#切分的选择) - * [存在的问题](#存在的问题) -* [六、故障转移和故障恢复](#六故障转移和故障恢复) + * [垂直切分](#垂直切分) + * [Sharding 策略](#sharding-策略) + * [Sharding 存在的问题](#sharding-存在的问题) * [参考资料](#参考资料) @@ -32,7 +34,7 @@ InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支 采用 MVCC 来支持高并发,并且实现了四个标准的隔离级别,默认级别是可重复读(REPEATABLE READ),并且通过间隙锁(next-key locking)策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入。 -表是基于聚簇索引建立的,它对主键的查询性能有很高的提升。 +表是基于聚簇索引建立的,它对主键的查询性能有很大的提升。 内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等。 @@ -40,7 +42,7 @@ InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支 ## MyISAM -MyISAM 提供了大量的特性,包括全文索引、压缩表、空间数据索引等。应该注意的是,MySQL 5.6.4 也添加了对 InnoDB 存储引擎的全文索引支持。 +MyISAM 提供了大量的特性,包括压缩表、空间数据索引等。 不支持事务。 @@ -54,11 +56,15 @@ MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者 ## 比较 -1. 事务:InnoDB 是事务型的。 -2. 备份:InnoDB 支持在线热备份。 -3. 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 -4. 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 -5. 其它特性:MyISAM 支持压缩表和空间数据索引。 +- 事务:InnoDB 是事务型的。 + +- 备份:InnoDB 支持在线热备份。 + +- 崩溃恢复:MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。 + +- 并发:MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 + +- 其它特性:MyISAM 支持压缩表和空间数据索引。 # 二、数据类型 @@ -98,7 +104,7 @@ MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。 和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。 -它和时区有关。 +它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。 MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。 @@ -114,23 +120,71 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和使用索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。 +## B-Tree 和 B+Tree 原理 + +### 1. B-Tree + +

+ +定义一条数据记录为一个二元组 [key, data],B-Tree 是满足下列条件的数据结构: + +- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的; +- 一个节点中的 key 从左到右非递减排列; +- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 + +查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。 + +由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。 + +### 2. B+Tree + +

+ +与 B-Tree 相比,B+Tree 有以下不同点: + +- 每个节点的指针上限为 2d 而不是 2d+1(d 为节点的出度); +- 内节点不存储 data,只存储 key; +- 叶子节点不存储指针。 + +### 3. 顺序访问指针 + +

+ +一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。 + +### 4. B+Tree 和 B-Tree 优势 + +红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+Tree 和 B-Tree 作为索引结构,主要有以下两个原因: + +**(一)更少的检索次数** + +平衡树检索数据的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。 + +红黑树的出度为 2,而 B+Tree 与 B-Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B+Tree 和 B-Tree 大非常多,因此检索的次数也就更多。 + +B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高。 + +**(二)利用计算机预读特性** + +为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,因此速度会非常快。 + +操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,临近的节点也能够被预先载入。 + +更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) + ## 索引分类 ### 1. B+Tree 索引

-《高性能 MySQL》一书使用 B-Tree 进行描述,其实从技术上来说这种索引是 B+Tree。 +《高性能 MySQL》一书使用 B-Tree 进行描述,其实从技术上来说这种索引是 B+Tree,因为只有叶子节点存储数据值。 B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型。 -因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。 - -可以指定多个列作为索引列,多个索引列共同组成键。B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。 +因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组。 -除了用于查找,还可以用于排序和分组。 - -如果不是按照索引列的顺序进行查找,则无法使用索引。 +可以指定多个列作为索引列,多个索引列共同组成键。B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。 ### 2. 哈希索引 @@ -143,23 +197,23 @@ InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个 限制: - 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显; -- 无法用于分组与排序; +- 无法用于排序与分组; - 只支持精确查找,无法用于部分查找和范围查找; - 如果哈希冲突很多,查找速度会变得很慢。 ### 3. 空间数据索引(R-Tree) -MyISAM 存储引擎支持空间索引,可以用于地理数据存储。 +MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。 -空间索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 +空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。 必须使用 GIS 相关的函数来维护数据。 ### 4. 全文索引 -MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较索引中的值。 +MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比值是否相等。查找条件使用 MATCH AGAINST,而不是普通的 WHERE。 -使用 MATCH AGAINST,而不是普通的 WHERE。 +InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 ## 索引的优点 @@ -167,7 +221,7 @@ MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而 - 帮助服务器避免进行排序和创建临时表(B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作); -- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的列值都存储在一起)。 +- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,也就将相邻的数据都存储在一起)。 ## 索引优化 @@ -181,13 +235,7 @@ MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而 SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5; ``` -### 2. 前缀索引 - -对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。 - -对于前缀长度的选取需要根据 **索引选择性** 来确定:不重复的索引值和记录总数的比值。选择性越高,查询效率也越高。最大值为 1,此时每个记录都有唯一的索引与其对应。 - -### 3. 多列索引 +### 2. 多列索引 在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。 @@ -196,9 +244,11 @@ SELECT film_id, actor_ id FROM sakila.film_actor WhERE actor_id = 1 AND film_id = 1; ``` -### 4. 索引列的顺序 +### 3. 索引列的顺序 + +让选择性最强的索引列放在前面,索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。 -让选择性最强的索引列放在前面,例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 +例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 ```sql SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity, @@ -213,115 +263,84 @@ customer_id_selectivity: 0.0373 COUNT(*): 16049 ``` -### 5. 聚簇索引 - -

- -聚簇索引并不是一种索引类型,而是一种数据存储方式。 - -术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行。 - -因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 +### 4. 前缀索引 -**优点** - -1. 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。 -2. 数据访问更快。 - -**缺点** +对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。 -1. 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。 -2. 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。 -3. 更新操作代价很高,因为每个被更新的行都会移动到新的位置。 -4. 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。 -5. 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。 +对于前缀长度的选取需要根据索引选择性来确定。 -### 6. 覆盖索引 +### 5. 覆盖索引 索引包含所有需要查询的字段的值。 **优点** -1. 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。 -2. 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。 -3. 对于 InnoDB 引擎,若二级索引能够覆盖查询,则无需访问聚簇索引。 - -## B-Tree 和 B+Tree 原理 - -### 1. B-Tree - -

- -为了描述 B-Tree,首先定义一条数据记录为一个二元组 [key, data]。 - -B-Tree 是满足下列条件的数据结构: - -- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的; -- 一个节点中的 key 从左到右非递减排列; -- 如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1。 - -在 B-Tree 中按 key 检索数据的算法非常直观:首先在根节点进行二分查找,如果找到则返回对应节点的 data,否则在相应区间的指针指向的节点递归进行查找。 +- 因为索引条目通常远小于数据行的大小,所以若只读取索引,能大大减少数据访问量。 +- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。 +- 对于 InnoDB 引擎,若二级索引能够覆盖查询,则无需访问聚簇索引。 -由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。 - -### 2. B+Tree +### 6. 聚簇索引 -

+

-与 B-Tree 相比,B+Tree 有以下不同点: +聚簇索引并不是一种索引类型,而是一种数据存储方式。 -- 每个节点的指针上限为 2d 而不是 2d+1; -- 内节点不存储 data,只存储 key,叶子节点不存储指针。 +术语“聚簇”表示数据行和相邻的键值紧密地存储在一起,InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行。 -### 3. 带有顺序访问指针的 B+Tree +因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 -

+**优点** -一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。 +- 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。 +- 数据访问更快。 -### 4. 为什么使用 B-Tree 和 B+Tree +**缺点** -红黑树等数据结构也可以用来实现索引,但是文件系统及数据库系统普遍采用 B-/+Tree 作为索引结构。 +- 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。 +- 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。 +- 更新操作代价很高,因为每个被更新的行都会移动到新的位置。 +- 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。 +- 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。 -页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为 4k),主存和磁盘以页为单位交换数据。 +# 四、查询性能优化 -一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理:当一个数据被用到时,其附近的数据也通常会马上被使用。数据库系统的设计者巧妙利用了磁盘预读原理,将一个节点的大小设为等于一个页,这样每个节点只需要一次 I/O 就可以完全载入。 +## 使用 Explain 进行分析 -B-Tree 中一次检索最多需要 h-1 次 I/O(根节点常驻内存),渐进复杂度为 O(h)=O(logdN)。一般实际应用中,出度 d 是非常大的数字,通常超过 100,因此 h 非常小(通常不超过 3)。而红黑树这种结构,h 明显要深的多。并且于逻辑上很近的节点(父子)物理上可能很远,无法利用局部性,效率明显比 B-Tree 差很多。 +Explain 用来分析 SELECT 查询语句,开发人员可以通过分析结果来优化查询语句。 -B+Tree 更适合外存索引,原因和内节点出度 d 有关。由于 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,拥有更好的性能。 +比较重要的字段有: -# 四、查询性能优化 +- select_type : 查询类型,有简单查询、联合查询、子查询等 +- key : 使用的索引 +- rows : 扫描的行数 -### Explain +更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735) -用来分析 SQL 语句,分析结果中比较重要的字段有: +## 优化数据访问 -- select_type : 查询类型,有简单查询、联合查询和子查询 +### 1. 减少请求的数据量 -- key : 使用的索引 +**(一)只返回必要的列** -- rows : 扫描的行数 +最好不要使用 SELECT * 语句。 -### 减少返回的列 +**(二)只返回必要的行** -慢查询主要是因为访问了过多数据,除了访问过多行之外,也包括访问过多列。 +使用 WHERE 语句进行查询过滤,有时候也需要使用 LIMIT 语句来限制返回的数据。 -最好不要使用 SELECT * 语句,要根据需要选择查询的列。 +**(三)缓存重复查询的数据** -### 减少返回的行 +使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。 -最好使用 LIMIT 语句来取出想要的那些行。 +### 2. 减少服务器端扫描的行数 -还可以建立索引来减少条件语句的全表扫描。例如对于下面的语句,不使用索引的情况下需要进行全表扫描,而使用索引只需要扫描几行记录即可,使用 Explain 语句可以通过观察 rows 字段来看出这种差异。 +最有效的方式是使用索引来覆盖查询。 -```sql -SELECT * FROM sakila.film_actor WHERE film_id = 1; -``` +## 重构查询方式 -### 拆分大的 DELETE 或 INSERT 语句 +### 1. 切分大查询 -如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。 +一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。 ```sql DELEFT FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH); @@ -335,64 +354,78 @@ do { } while rows_affected > 0 ``` -# 五、切分 - - -随着时间和业务的发展,数据库中的表会越来越多,并且表中的数据量也会越来越大,那么读写操作的开销也会随着增大。 +### 2. 分解大连接查询 -## 垂直切分 - -将表按功能模块、关系密切程度划分出来,部署到不同的库上。例如,我们会建立商品数据库 payDB、用户数据库 userDB 等,分别用来存储项目与商品有关的表和与用户有关的表。 +将一个大连接查询(JOIN)分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有: -## 水平切分 +- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。 +- 减少锁竞争; +- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可扩展。 +- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。 +- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。 -把表中的数据按照某种规则存储到多个结构相同的表中,例如按 id 的散列值、性别等进行划分。 +```sql +SELECT * FROM tab +JOIN tag_post ON tag_post.tag_id=tag.id +JOIN post ON tag_post.post_id=post.id +WHERE tag.tag='mysql'; +``` -## 切分的选择 +```sql +SELECT * FROM tag WHERE tag='mysql'; +SELECT * FROM tag_post WHERE tag_id=1234; +SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); +``` -如果数据库中的表太多,并且项目各项业务逻辑清晰,那么垂直切分是首选。 +# 五、切分 -如果数据库的表不多,但是单表的数据量很大,应该选择水平切分。 +## 水平切分 -## 存在的问题 +

-### 1. 事务问题 +水平切分就是就是常见的 Sharding,它是将同一个表中的记录拆分到多个结构相同的表中。 -在执行分库分表之后,由于数据存储到了不同的库上,数据库事务管理出现了困难。如果依赖数据库本身的分布式事务管理功能去执行事务,将付出高昂的性能代价;如果由应用程序去协助控制,形成程序逻辑上的事务,又会造成编程方面的负担。 +当一个表的数据不断增多时,Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 -### 2. 跨库跨表连接问题 +## 垂直切分 -在执行了分库分表之后,难以避免会将原本逻辑关联性很强的数据划分到不同的表、不同的库上。这时,表的连接操作将受到限制,我们无法连接位于不同分库的表,也无法连接分表粒度不同的表,导致原本只需要一次查询就能够完成的业务需要进行多次才能完成。 +

-### 3. 额外的数据管理负担和数据运算压力 +垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分。也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。 -最显而易见的就是数据的定位问题和数据的增删改查的重复执行问题,这些都可以通过应用程序解决,但必然引起额外的逻辑运算。 +也可以在数据库的层面使用垂直切分,它按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userBD 等。 +## Sharding 策略 -# 六、故障转移和故障恢复 +- 哈希取模:hash(key) % NUM_DB +- 范围:可以是 ID 范围也可以是时间范围 +- 映射表:使用单独的一个数据库来存储映射关系 -故障转移也叫做切换,当主库出现故障时就切换到备库,使备库成为主库。故障恢复顾名思义就是从故障中恢复过来,并且保证数据的正确性。 +## Sharding 存在的问题 -### 提升备库或切换角色 +### 1. 事务问题 -提升一台备库为主库,或者在一个主-主复制结构中调整主动和被动角色。 +使用分布式事务。 -### 虚拟 IP 地址和 IP 托管 +### 2. JOIN -为 MySQL 实例指定一个逻辑 IP 地址,当 MySQL 实例失效时,可以将 IP 地址转移到另一台 MySQL 服务器上。 +将原来的 JOIN 查询分解成多个单表查询,然后在用户程序中进行 JOIN。 -### 中间件解决方案 +### 3. ID 唯一性 -通过代理,可以路由流量到可以使用的服务器上。 +- 使用全局唯一 ID:GUID。 +- 为每个分片指定一个 ID 范围。 +- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)。 -### 在应用中处理故障转移 +更多内容请参考: -将故障转移整合到应用中可能导致应用变得太过笨拙。 +- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) +- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html) # 参考资料 - BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013. -- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) -- [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) +- [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html) +- [服务端指南 数据存储篇 | 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) +- [SQL Azure Federation – Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.") diff --git a/notes/Redis.md b/notes/Redis.md index b92fc7aecb51df4551d0d6537a9dd81b3e764795..b276275fa308f07e4699ce0da0239df8e00ca8c0 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -1,39 +1,42 @@ -* [一、Redis 是什么](#一redis-是什么) -* [二、五种基本类型](#二五种基本类型) - * [1. STRING](#1-string) - * [2. LIST](#2-list) - * [3. SET](#3-set) - * [4. HASH](#4-hash) - * [5. ZSET](#5-zset) -* [三、键的过期时间](#三键的过期时间) -* [四、发布与订阅](#四发布与订阅) -* [五、事务](#五事务) -* [六、持久化](#六持久化) - * [1. 快照持久化](#1-快照持久化) - * [2. AOF 持久化](#2-aof-持久化) -* [七、复制](#七复制) - * [从服务器连接主服务器的过程](#从服务器连接主服务器的过程) - * [主从链](#主从链) -* [八、处理故障](#八处理故障) -* [九、分片](#九分片) - * [1. 客户端分片](#1-客户端分片) - * [2. 代理分片](#2-代理分片) - * [3. 服务器分片](#3-服务器分片) -* [十、事件](#十事件) - * [事件类型](#事件类型) - * [事件的调度与执行](#事件的调度与执行) -* [十一、Redis 与 Memcached 的区别](#十一redis-与-memcached-的区别) +* [一、概述](#一概述) +* [二、数据类型](#二数据类型) + * [STRING](#string) + * [LIST](#list) + * [SET](#set) + * [HASH](#hash) + * [ZSET](#zset) +* [三、使用场景](#三使用场景) + * [缓存](#缓存) + * [计数器](#计数器) + * [应用限流](#应用限流) + * [消息队列](#消息队列) + * [查找表](#查找表) + * [交集运算](#交集运算) + * [排行榜](#排行榜) + * [分布式 Session](#分布式-session) + * [分布式锁](#分布式锁) +* [四、Redis 与 Memcached](#四redis-与-memcached) * [数据类型](#数据类型) * [数据持久化](#数据持久化) * [分布式](#分布式) * [内存管理机制](#内存管理机制) -* [十二、Redis 适用场景](#十二redis-适用场景) - * [缓存](#缓存) - * [消息队列](#消息队列) - * [计数器](#计数器) - * [好友关系](#好友关系) -* [十三、数据淘汰策略](#十三数据淘汰策略) +* [五、键的过期时间](#五键的过期时间) +* [六、数据淘汰策略](#六数据淘汰策略) +* [七、持久化](#七持久化) + * [快照持久化](#快照持久化) + * [AOF 持久化](#aof-持久化) +* [八、发布与订阅](#八发布与订阅) +* [九、事务](#九事务) +* [十、事件](#十事件) + * [文件事件](#文件事件) + * [时间事件](#时间事件) + * [事件的调度与执行](#事件的调度与执行) +* [十一、复制](#十一复制) + * [连接过程](#连接过程) + * [主从链](#主从链) +* [十二、Sentinel](#十二sentinel) +* [十三、分片](#十三分片) * [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) * [文章信息](#文章信息) * [点赞功能](#点赞功能) @@ -42,27 +45,27 @@ -# 一、Redis 是什么 +# 一、概述 Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。 -五种类型数据类型为:字符串、列表、集合、有序集合、散列表。 +键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。 Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 -# 二、五种基本类型 +# 二、数据类型 | 数据类型 | 可以存储的值 | 操作 | | :--: | :--: | :--: | | STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
对整数和浮点数执行自增或者自减操作 | -| LIST | 链表 | 从两端压入或者弹出元素
读取单个或者多个元素
进行修剪,只保留一个范围内的元素 | +| LIST | 列表 | 从两端压入或者弹出元素
读取单个或者多个元素
进行修剪,只保留一个范围内的元素 | | SET | 无序集合 | 添加、获取、移除单个元素
检查一个元素是否存在于集合中
计算交集、并集、差集
从集合里面随机获取元素 | | HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对
获取所有键值对
检查某个键是否存在| | ZSET | 有序集合 | 添加、获取、删除元素
根据分值范围或者成员来获取元素
计算一个键的排名 | > [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) -## 1. STRING +## STRING

@@ -77,7 +80,7 @@ OK (nil) ``` -## 2. LIST +## LIST

@@ -105,7 +108,7 @@ OK 2) "item" ``` -## 3. SET +## SET

@@ -139,7 +142,7 @@ OK 2) "item3" ``` -## 4. HASH +## HASH

@@ -170,7 +173,7 @@ OK 2) "value1" ``` -## 5. ZSET +## ZSET

@@ -202,41 +205,98 @@ OK 2) "982" ``` -# 三、键的过期时间 +# 三、使用场景 -Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 +## 缓存 -对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 +将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率。 -过期时间对于清理缓存数据非常有用。 +## 计数器 -# 四、发布与订阅 +Redis 这种内存数据库能支持计数器频繁的读写操作。 -订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 +## 应用限流 -发布与订阅模式和观察者模式有以下不同: +限制一个网站访问流量。 -- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 -- 观察者模式是同步的,当事件触发时,主题会去调度观察者的方法;而发布与订阅模式是异步的; +## 消息队列 -

+使用 List 数据类型,它是双向链表。 -发布与订阅有一些问题,很少使用它,而是使用替代的解决方案。问题如下: +## 查找表 -1. 如果订阅者读取消息的速度很慢,会使得消息不断积压在发布者的输出缓存区中,造成内存占用过多; -2. 如果订阅者在执行订阅的过程中网络出现问题,那么就会丢失断线期间发送的所有消息。 +使用 HASH 数据类型。 -# 五、事务 +## 交集运算 -Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 +使用 SET 类型,例如求两个用户的共同好友。 + +## 排行榜 + +使用 ZSET 数据类型。 + +## 分布式 Session + +多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。 + +## 分布式锁 + +除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。 + + +# 四、Redis 与 Memcached + +两者都是非关系型内存键值数据库。有以下主要不同: + +## 数据类型 + +Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 + +## 数据持久化 + +Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。 + +## 分布式 + +Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 + +Redis Cluster 实现了分布式的支持。 + +## 内存管理机制 + +在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。 + +Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 + + +# 五、键的过期时间 + +Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 + +对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 + +# 六、数据淘汰策略 + +可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 + +| 策略 | 描述 | +| :--: | :--: | +| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | +| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | +|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | +| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | +| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | +| noeviction | 禁止驱逐数据 | + +如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 -MULTI 和 EXEC 中的操作将会一次性发送给服务器,而不是一条一条发送,这种方式称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。 +作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key,抽样数量可通过 maxmemory-samples 配置。 -# 六、持久化 +# 七、持久化 Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 -## 1. 快照持久化 +## 快照持久化 将某个时间点的所有数据都存放到硬盘上。 @@ -246,13 +306,13 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 如果数据量很大,保存快照的时间会很长。 -## 2. AOF 持久化 +## AOF 持久化 将写命令添加到 AOF 文件(Append Only File)的末尾。 -对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。 +对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。 -将写命令添加到 AOF 文件时,要根据需求来保证何时将添加的数据同步到硬盘上,有以下同步选项: +有以下同步选项: | 选项 | 同步频率 | | :--: | :--: | @@ -260,67 +320,55 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 | everysec | 每秒同步一次 | | no | 让操作系统来决定何时同步 | -always 选项会严重减低服务器的性能;everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。 - -随着服务器写请求的增多,AOF 文件会越来越大;Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 - -# 七、复制 - -通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 - -一个从服务器只能有一个主服务器,并且不支持主主复制。 - -## 从服务器连接主服务器的过程 +- always 选项会严重减低服务器的性能; +- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; +- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。 -1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令; +随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 -2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令; - -3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。 - -## 主从链 - -随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。 +# 八、发布与订阅 -

+订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 -# 八、处理故障 +某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。 -要用到持久化文件来恢复服务器的数据。 +发布与订阅模式和观察者模式有以下不同: -持久化文件可能因为服务器出错也有错误,因此要先对持久化文件进行验证和修复。对 AOF 文件就行验证和修复很容易,修复操作将第一个出错命令和其后的所有命令都删除;但是只能验证快照文件,无法对快照文件进行修复,因为快照文件进行了压缩,出现在快照文件中间的错误可能会导致整个快照文件的剩余部分无法读取。 +- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 +- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的; -当主服务器出现故障时,Redis 常用的做法是新开一台服务器作为主服务器,具体步骤如下:假设 A 为主服务器,B 为从服务器,当 A 出现故障时,让 B 生成一个快照文件,将快照文件发送给 C,并让 C 恢复快照文件的数据。最后,让 B 成为 C 的从服务器。 +

-# 九、分片 +# 九、事务 -Redis 中的分片类似于 MySQL 的分表操作,分片是将数据划分为多个部分的方法,对数据的划分可以基于键包含的 ID、基于键的哈希值,或者基于以上两者的某种组合。通过对数据进行分片,用户可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 +一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 -假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 +事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。 -## 1. 客户端分片 +Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 -客户端使用一致性哈希等算法决定键应当分布到哪个节点。 +# 十、事件 -## 2. 代理分片 +Redis 服务器是一个事件驱动程序。 -将客户端请求发送到代理上,由代理转发请求到正确的节点上。 +## 文件事件 -## 3. 服务器分片 +服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。 -Redis Cluster。 +Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用响应的时间处理器。 -# 十、事件 +

-## 事件类型 +## 时间事件 -### 1. 文件事件 +服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。 -服务器有许多套接字,事件产生时会对这些套接字进行操作,服务器通过监听套接字来处理事件。常见的文件事件有:客户端的连接事件;客户端的命令请求事件;服务器向客户端返回命令结果的事件; +时间事件又分为: -### 2. 时间事件 +- 定时事件:是让一段程序在指定的时间之内执行一次; +- 周期性事件:是让一段程序每隔指定时间就执行一次。 -又分为两类:定时事件是让一段程序在指定的时间之内执行一次;周期性事件是让一段程序每隔指定时间就执行一次。 +Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。 ## 事件的调度与执行 @@ -374,64 +422,41 @@ def main():

-# 十一、Redis 与 Memcached 的区别 - -两者都是非关系型内存键值数据库。有以下主要不同: - -## 数据类型 - -Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 - -## 数据持久化 - -Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。 - -## 分布式 - -Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 - -Redis Cluster 实现了分布式的支持。 - -## 内存管理机制 +# 十一、复制 -在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。 +通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 -Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 +一个从服务器只能有一个主服务器,并且不支持主主复制。 -# 十二、Redis 适用场景 +## 连接过程 -## 缓存 +1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令; -将热点数据放到内存中。 +2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令; -## 消息队列 +3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。 -List 类型是双向链表,很适合用于消息队列。 +## 主从链 -## 计数器 +随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。 -Redis 这种内存数据库能支持计数器频繁的读写操作。 +

-## 好友关系 +# 十二、Sentinel -使用 Set 类型的交集操作很容易就可以知道两个用户的共同好友。 +Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 -# 十三、数据淘汰策略 +# 十三、分片 -可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 +分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 -| 策略 | 描述 | -| :--: | :--: | -| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 | -| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 | -|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 | -| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 | -| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | -| noeviction | 禁止驱逐数据 | +假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 -如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 +主要有三种分片方式: -作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法(LRU、TTL)实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key。抽样数量可通过 maxmemory-samples 配置。 +- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。 +- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 +- 服务器分片:Redis Cluster。 # 十四、一个简单的论坛系统分析 diff --git a/notes/SQL.md b/notes/SQL.md index 2eb67e58bd65c20d3ce6d58ff5bc08957c9185fe..d1fb3e276ea7baa7548171c85d519910ccff90a5 100644 --- a/notes/SQL.md +++ b/notes/SQL.md @@ -46,6 +46,13 @@ FROM mytable; -- 注释 注释2 */ ``` +数据库创建与使用: + +```sql +CREATE DATABASE test; +USE test; +``` + # 二、创建表 ```sql @@ -120,6 +127,10 @@ WHERE id = 1; **TRUNCATE TABLE** 可以清空表,也就是删除所有行。 +```sql +TRUNCATE TABLE mytable; +``` + 使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。 # 七、查询 @@ -187,16 +198,16 @@ WHERE col IS NULL; | 操作符 | 说明 | | ------------ | ------------ | -| = < > | 等于 小于 大于 | -| <> != | 不等于 | -| <= !> | 小于等于 | -| >= !< | 大于等于 | -| BETWEEN | 在两个值之间 | -| IS NULL | 为NULL值 | +| `=` `<` `>` | 等于 小于 大于 | +| `<>` `!=` | 不等于 | +| `<=` `!>` | 小于等于 | +| `>=` `!<` | 大于等于 | +| `BETWEEN` | 在两个值之间 | +| `IS NULL` | 为 NULL 值 | -应该注意到,NULL 与 0 、空字符串都不同。 +应该注意到,NULL 与 0、空字符串都不同。 -**AND OR** 用于连接多个过滤条件,优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。 +**AND 和 OR** 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。 **IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。 @@ -217,8 +228,9 @@ WHERE col IS NULL; ```sql SELECT * FROM mytable -WHERE col LIKE '[^AB]%' -- 不以 A 和 B 开头的任意文本 +WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本 ``` + 不要滥用通配符,通配符位于开头处匹配会非常慢。 # 十一、计算字段 @@ -228,15 +240,15 @@ WHERE col LIKE '[^AB]%' -- 不以 A 和 B 开头的任意文本 计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。 ```sql -SELECT col1*col2 AS alias -FROM mytable +SELECT col1 * col2 AS alias +FROM mytable; ``` **CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。 ```sql -SELECT CONCAT(TRIM(col1), ' (', TRIM(col2), ')') -FROM mytable +SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col +FROM mytable; ``` # 十二、函数 @@ -247,11 +259,11 @@ FROM mytable | 函数 | 说明 | | :---: | :---: | -| LEFT() RIGHT() | 左边或者右边的字符 | -| LOWER() UPPER() | 转换为小写或者大写 | -| LTRIM() RTIM() | 去除左边或者右边的空格 | -| LENGTH() | 长度 | -| SOUNDEX() | 转换为语音值 | +| `LEFT()` `RIGHT()` | 左边或者右边的字符 | +| `LOWER()` `UPPER()` | 转换为小写或者大写 | +| `LTRIM()` `RTIM()` | 去除左边或者右边的空格 | +| `LENGTH()` | 长度 | +| `SOUNDEX()` | 转换为语音值 | 其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。 @@ -268,23 +280,23 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple') |函 数 | 说 明| | :---: | :---: | -| AddDate() | 增加一个日期(天、周等)| -| AddTime() | 增加一个时间(时、分等)| -| CurDate() | 返回当前日期 | -| CurTime() | 返回当前时间 | -| Date() |返回日期时间的日期部分| -| DateDiff() |计算两个日期之差| -| Date_Add() |高度灵活的日期运算函数| -| Date_Format() |返回一个格式化的日期或时间串| -| Day()| 返回一个日期的天数部分| -| DayOfWeek() |对于一个日期,返回对应的星期几| -| Hour() |返回一个时间的小时部分| -| Minute() |返回一个时间的分钟部分| -| Month() |返回一个日期的月份部分| -| Now() |返回当前日期和时间| -| Second() |返回一个时间的秒部分| -| Time() |返回一个日期时间的时间部分| -| Year() |返回一个日期的年份部分| +| `AddDate()` | 增加一个日期(天、周等)| +| `AddTime()` | 增加一个时间(时、分等)| +| `CurDate()` | 返回当前日期 | +| `CurTime()` | 返回当前时间 | +| `Date()` |返回日期时间的日期部分| +| `DateDiff()` |计算两个日期之差| +| `Date_Add()` |高度灵活的日期运算函数| +| `Date_Format()` |返回一个格式化的日期或时间串| +| `Day()`| 返回一个日期的天数部分| +| `DayOfWeek()` |对于一个日期,返回对应的星期几| +| `Hour()` |返回一个时间的小时部分| +| `Minute()` |返回一个时间的分钟部分| +| `Month()` |返回一个日期的月份部分| +| `Now()` |返回当前日期和时间| +| `Second()` |返回一个时间的秒部分| +| `Time()` |返回一个日期时间的时间部分| +| `Year()` |返回一个日期的年份部分| ```sql mysql> SELECT NOW(); @@ -298,25 +310,25 @@ mysql> SELECT NOW(); | 函数 | 说明 | | :---: | :---: | -| SIN() | 正弦 | -| COS() | 余弦 | -| TAN() | 正切 | -| ABS() | 绝对值 | -| SQRT() | 平方根 | -| MOD() | 余数 | -| EXP() | 指数 | -| PI() | 圆周率 | -| RAND() | 随机数 | +| `SIN()` | 正弦 | +| `COS()` | 余弦 | +| `TAN()` | 正切 | +| `ABS()` | 绝对值 | +| `SQRT()` | 平方根 | +| `MOD()` | 余数 | +| `EXP()` | 指数 | +| `PI()` | 圆周率 | +| `RAND()` | 随机数 | ## 汇总 |函 数 |说 明| | :---: | :---: | -| AVG() | 返回某列的平均值 | -| COUNT() | 返回某列的行数 | -| MAX() | 返回某列的最大值 | -| MIN() | 返回某列的最小值 | -| SUM() |返回某列值之和 | +| `AVG()` | 返回某列的平均值 | +| `COUNT()` | 返回某列的行数 | +| `MAX()` | 返回某列的最大值 | +| `MIN()` | 返回某列的最小值 | +| `SUM()` |返回某列值之和 | AVG() 会忽略 NULL 行。 @@ -333,7 +345,7 @@ FROM mytable 可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。 -指定的分组字段除了能按该字段进行分组,也会自动按按该字段进行排序。 +指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。 ```sql SELECT col, COUNT(*) AS num @@ -341,7 +353,7 @@ FROM mytable GROUP BY col; ``` -GROUP BY 按分组字段进行排序,ORDER BY 也可以以汇总字段来进行排序。 +GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。 ```sql SELECT col, COUNT(*) AS num @@ -362,10 +374,10 @@ HAVING num >= 2; 分组规定: -1. GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前; -2. 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出; -3. NULL 的行会单独分为一组; -4. 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。 +- GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前; +- 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出; +- NULL 的行会单独分为一组; +- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。 # 十四、子查询 @@ -404,17 +416,17 @@ ORDER BY cust_name; 内连接又称等值连接,使用 INNER JOIN 关键字。 ```sql -select a, b, c -from A inner join B -on A.key = B.key +SELECT a, b, c +FROM A INNER JOIN B +ON A.key = B.key; ``` 可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。 ```sql -select a, b, c -from A, B -where A.key = B.key +SELECT a, b, c +FROM A, B +WHERE A.key = B.key; ``` 在没有条件语句的情况下返回笛卡尔积。 @@ -428,21 +440,21 @@ where A.key = B.key 子查询版本 ```sql -select name -from employee -where department = ( - select department - from employee - where name = "Jim"); +SELECT name +FROM employee +WHERE department = ( + SELECT department + FROM employee + WHERE name = "Jim"); ``` 自连接版本 ```sql -select e1.name -from employee as e1, employee as e2 -where e1.department = e2.department - and e2.name = "Jim"; +SELECT e1.name +FROM employee AS e1, employee AS e2 +WHERE e1.department = e2.department + AND e2.name = "Jim"; ``` 连接一般比子查询的效率高。 @@ -454,8 +466,8 @@ where e1.department = e2.department 内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。 ```sql -select * -from employee natural join department; +SELECT * +FROM employee NATURAL JOIN department; ``` ## 外连接 @@ -465,19 +477,19 @@ from employee natural join department; 检索所有顾客的订单信息,包括还没有订单信息的顾客。 ```sql -select Customers.cust_id, Orders.order_num -from Customers left outer join Orders -on Customers.cust_id = Orders.cust_id; +SELECT Customers.cust_id, Orders.order_num +FROM Customers LEFT OUTER JOIN Orders +ON Customers.cust_id = Orders.cust_id; ``` 如果需要统计顾客的订单数,使用聚集函数。 ```sql -select Customers.cust_id, - COUNT(Orders.order_num) as num_ord -from Customers left outer join Orders -on Customers.cust_id = Orders.cust_id -group by Customers.cust_id; +SELECT Customers.cust_id, + COUNT(Orders.order_num) AS num_ord +FROM Customers LEFT OUTER JOIN Orders +ON Customers.cust_id = Orders.cust_id +GROUP BY Customers.cust_id; ``` # 十六、组合查询 @@ -502,18 +514,20 @@ WHERE col =2; # 十七、视图 -视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。 +视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。 + +对视图的操作和对普通表的操作一样。 视图具有如下好处: -1. 简化复杂的 SQL 操作,比如复杂的联结; -2. 只使用实际表的一部分数据; -3. 通过只给用户访问视图的权限,保证数据的安全性; -4. 更改数据格式和表示。 +- 简化复杂的 SQL 操作,比如复杂的连接; +- 只使用实际表的一部分数据; +- 通过只给用户访问视图的权限,保证数据的安全性; +- 更改数据格式和表示。 ```sql CREATE VIEW myview AS -SELECT Concat(col1, col2) AS concat_col, col3*col4 AS count_col +SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col FROM mytable WHERE col5 = val; ``` @@ -522,13 +536,11 @@ WHERE col5 = val; 存储过程可以看成是对一系列 SQL 操作的批处理; -## 使用存储过程的好处 - -1. 代码封装,保证了一定的安全性; -2. 代码复用; -3. 由于是预先编译,因此具有很高的性能。 +使用存储过程的好处: -## 创建存储过程 +- 代码封装,保证了一定的安全性; +- 代码复用; +- 由于是预先编译,因此具有很高的性能。 命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。 @@ -549,6 +561,7 @@ create procedure myprocedure( out ret int ) into y; select y*y into ret; end // + delimiter ; ``` @@ -578,7 +591,7 @@ create procedure myprocedure(out ret int) declare mycursor cursor for select col1 from mytable; - # 定义了一个continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1 + # 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1 declare continue handler for sqlstate '02000' set done = 1; open mycursor; @@ -595,7 +608,7 @@ create procedure myprocedure(out ret int) # 二十、触发器 -触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE +触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE。 触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化。 @@ -603,7 +616,9 @@ INSERT 触发器包含一个名为 NEW 的虚拟表。 ```sql CREATE TRIGGER mytrigger AFTER INSERT ON mytable -FOR EACH ROW SELECT NEW.col; +FOR EACH ROW SELECT NEW.col into @result; + +SELECT @result; -- 获取结果 ``` DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。 @@ -612,16 +627,16 @@ UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。 -MySQL 不允许在触发器中使用 CALL 语句 ,也就是不能调用存储过程。 +MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。 # 二十一、事务处理 基本术语: -1. 事务(transaction)指一组 SQL 语句; -2. 回退(rollback)指撤销指定 SQL 语句的过程; -3. 提交(commit)指将未存储的 SQL 语句结果写入数据库表; -4. 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。 +- 事务(transaction)指一组 SQL 语句; +- 回退(rollback)指撤销指定 SQL 语句的过程; +- 提交(commit)指将未存储的 SQL 语句结果写入数据库表; +- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。 不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。 @@ -645,9 +660,9 @@ COMMIT 基本术语: -1. 字符集为字母和符号的集合; -2. 编码为某个字符集成员的内部表示; -3. 校对字符指定如何比较,主要用于排序和分组。 +- 字符集为字母和符号的集合; +- 编码为某个字符集成员的内部表示; +- 校对字符指定如何比较,主要用于排序和分组。 除了给表指定字符集和校对外,也可以给列指定: @@ -674,7 +689,7 @@ USE mysql; SELECT user FROM user; ``` -## 创建账户 +**创建账户** ```sql CREATE USER myuser IDENTIFIED BY 'mypassword'; @@ -682,25 +697,25 @@ CREATE USER myuser IDENTIFIED BY 'mypassword'; 新创建的账户没有任何权限。 -## 修改账户名 +**修改账户名** ```sql RENAME myuser TO newuser; ``` -## 删除账户 +**删除账户** ```sql DROP USER myuser; ``` -## 查看权限 +**查看权限** ```sql SHOW GRANTS FOR myuser; ``` -## 授予权限 +**授予权限** ```sql GRANT SELECT, INSERT ON mydatabase.* TO myuser; @@ -708,7 +723,7 @@ GRANT SELECT, INSERT ON mydatabase.* TO myuser; 账户用 username@host 的形式定义,username@% 使用的是默认主机名。 -## 删除权限 +**删除权限** ```sql REVOKE SELECT, INSERT ON mydatabase.* FROM myuser; @@ -722,12 +737,12 @@ GRANT 和 REVOKE 可在几个层次上控制访问权限: - 特定的列; - 特定的存储过程。 -## 更改密码 +**更改密码** 必须使用 Password() 函数 ```sql -SET PASSWROD FOR myuser = Password('newpassword'); +SET PASSWROD FOR myuser = Password('new_password'); ``` # 参考资料 diff --git a/notes/Socket.md b/notes/Socket.md new file mode 100644 index 0000000000000000000000000000000000000000..1243e6f59d5a7b263a42cbd0547506428a9c0630 --- /dev/null +++ b/notes/Socket.md @@ -0,0 +1,343 @@ + +* [一、I/O 复用](#一io-复用) + * [I/O 模型](#io-模型) + * [select/poll/epoll](#selectpollepoll) + * [select 和 poll 比较](#select-和-poll-比较) + * [eopll 工作模式](#eopll-工作模式) + * [select poll epoll 应用场景](#select-poll-epoll-应用场景) +* [参考资料](#参考资料) + + + +# 一、I/O 复用 + +## I/O 模型 + +一个输入操作通常包括两个阶段: + +- 等待数据准备好 +- 从内核向进程复制数据 + +对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。 + +Unix 下有五种 I/O 模型: + +- 阻塞式 I/O +- 非阻塞式 I/O +- I/O 复用(select 和 poll) +- 信号驱动式 I/O(SIGIO) +- 异步 I/O(AIO) + +### 1. 阻塞式 I/O + +应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。 + +应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。 + +下图中,recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。 + +```c +ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen); +``` + +

+ +### 2. 非阻塞式 I/O + +应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式成为轮询(polling)。 + +由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。 + +

+ +### 3. I/O 复用 + +使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。 + +它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。 + +如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。并且相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。 + +

+ +### 4. 信号驱动 I/O + +应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。 + +相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。 + +

+ +### 5. 异步 I/O + +进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 + +异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。 + +

+ +### 6. 同步 I/O 与异步 I/O + +- 同步 I/O:应用进程在调用 recvfrom 操作时会阻塞。 +- 异步 I/O:不会阻塞。 + +阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O,虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。 + +### 7. 五大 I/O 模型比较 + +前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。 + +

+ +## select/poll/epoll + +这三个都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。 + +### 1. select + +```c +int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); +``` + +fd_set 表示描述符集合类型,有三个参数:readset、writeset 和 exceptset,分别对应读、写、异常条件的描述符集合。 + +timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间; + +成功调用返回结果大于 0;出错返回结果为 -1;超时返回结果为 0。 + +每次调用 select 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从应用进程缓冲复制到内核缓冲。 + +返回结果中内核并没有声明 fd_set 中哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程需要遍历所有的 fd_set。 + +select 最多支持 1024 个描述符,其中 1024 由内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE,然后重新编译内核。 + +```c +fd_set fd_in, fd_out; +struct timeval tv; + +// Reset the sets +FD_ZERO( &fd_in ); +FD_ZERO( &fd_out ); + +// Monitor sock1 for input events +FD_SET( sock1, &fd_in ); + +// Monitor sock2 for output events +FD_SET( sock2, &fd_out ); + +// Find out which socket has the largest numeric value as select requires it +int largest_sock = sock1 > sock2 ? sock1 : sock2; + +// Wait up to 10 seconds +tv.tv_sec = 10; +tv.tv_usec = 0; + +// Call the select +int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv ); + +// Check if select actually succeed +if ( ret == -1 ) + // report error and abort +else if ( ret == 0 ) + // timeout; no event detected +else +{ + if ( FD_ISSET( sock1, &fd_in ) ) + // input event on sock1 + + if ( FD_ISSET( sock2, &fd_out ) ) + // output event on sock2 +} +``` + +### 2. poll + +```c +int poll(struct pollfd *fds, unsigned int nfds, int timeout); +``` + +```c +struct pollfd { + int fd; //文件描述符 + short events; //监视的请求事件 + short revents; //已发生的事件 +}; +``` + +它和 select 功能基本相同。同样需要每次将描述符从应用进程复制到内核,poll 调用返回后同样需要进行轮询才能知道哪些描述符已经准备好。 + +poll 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。 + +poll 在描述符的重复利用上比 select 的 fd_set 会更好。 + +如果在多线程下,如果一个线程对某个描述符调用了 poll 系统调用,但是另一个线程关闭了该描述符,会导致 poll 调用结果不确定,该问题同样出现在 select 中。 + +```c +// The structure for two events +struct pollfd fds[2]; + +// Monitor sock1 for input +fds[0].fd = sock1; +fds[0].events = POLLIN; + +// Monitor sock2 for output +fds[1].fd = sock2; +fds[1].events = POLLOUT; + +// Wait 10 seconds +int ret = poll( &fds, 2, 10000 ); +// Check if poll actually succeed +if ( ret == -1 ) + // report error and abort +else if ( ret == 0 ) + // timeout; no event detected +else +{ + // If we detect the event, zero it out so we can reuse the structure + if ( pfd[0].revents & POLLIN ) + pfd[0].revents = 0; + // input event on sock1 + + if ( pfd[1].revents & POLLOUT ) + pfd[1].revents = 0; + // output event on sock2 +} +``` + +### 3. epoll + +```c +int epoll_create(int size); +int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); +int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); +``` + +epoll 仅仅适用于 Linux OS。 + +它是 select 和 poll 的增强版,更加灵活而且没有描述符数量限制。 + +它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次。 + +select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。 + +新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size,不保证服务质量。 + +epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。 + +epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到应用进程中,不需要像 select/poll 对注册的所有描述符遍历一遍。 + +epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait() 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。 + +```c +// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets. +// The function argument is ignored (it was not before, but now it is), so put your favorite number here +int pollingfd = epoll_create( 0xCAFE ); + +if ( pollingfd < 0 ) + // report error + +// Initialize the epoll structure in case more members are added in future +struct epoll_event ev = { 0 }; + +// Associate the connection class instance with the event. You can associate anything +// you want, epoll does not use this information. We store a connection class pointer, pConnection1 +ev.data.ptr = pConnection1; + +// Monitor for input, and do not automatically rearm the descriptor after the event +ev.events = EPOLLIN | EPOLLONESHOT; +// Add the descriptor into the monitoring list. We can do it even if another thread is +// waiting in epoll_wait - the descriptor will be properly added +if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 ) + // report error + +// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen) +struct epoll_event pevents[ 20 ]; + +// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array +int ready = epoll_wait( pollingfd, pevents, 20, 10000 ); +// Check if epoll actually succeed +if ( ret == -1 ) + // report error and abort +else if ( ret == 0 ) + // timeout; no event detected +else +{ + // Check if any events detected + for ( int i = 0; i < ret; i++ ) + { + if ( pevents[i].events & EPOLLIN ) + { + // Get back our connection pointer + Connection * c = (Connection*) pevents[i].data.ptr; + c->handleReadEvent(); + } + } +} +``` + +## select 和 poll 比较 + +### 1. 功能 + +它们提供了几乎相同的功能,但是在一些细节上有所不同: + +- select 会修改 fd_set 参数,而 poll 不会; +- select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译; +- poll 提供了更多的事件类型。 + +### 2. 速度 + +poll 和 select 在速度上都很慢。 + +- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢; +- select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。 + +### 3. 可移植性 + +几乎所有的系统都支持 select,但是只有比较新的系统支持 poll。 + +## eopll 工作模式 + +epoll_event 有两种触发模式:LT(level trigger)和 ET(edge trigger)。 + +### 1. LT 模式 + +当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。 + +### 2. ET 模式 + +当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 时,不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。 + +## select poll epoll 应用场景 + +很容易产生一种错觉认为只要用 epoll 就可以了,select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。 + +### 1. select 应用场景 + +select() poll() epoll_wait() 都有一个 timeout 参数,在 select() 中 timeout 的精确度为 1ns,而 poll() 和 epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求更高的场景,比如核反应堆的控制。 + +select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。 + +### 2. poll 应用场景 + +poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select。 + +需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。 + +需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。 + +### 3. epoll 应用场景 + +程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。 + +### 4. 性能对比 + +> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html) + +# 参考资料 + +- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004. +- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/) +- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx) +- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859) +- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html) diff --git "a/notes/\344\270\200\350\207\264\346\200\247\345\215\217\350\256\256.md" "b/notes/\344\270\200\350\207\264\346\200\247\345\215\217\350\256\256.md" deleted file mode 100644 index ecac4a1e7d37e0cd573e974013c63372c7902f9d..0000000000000000000000000000000000000000 --- "a/notes/\344\270\200\350\207\264\346\200\247\345\215\217\350\256\256.md" +++ /dev/null @@ -1,157 +0,0 @@ - -* [一、两阶段提交协议](#一两阶段提交协议) -* [二、Paxos 协议](#二paxos-协议) -* [三、Raft 协议](#三raft-协议) -* [四、拜占庭将军问题](#四拜占庭将军问题) -* [五、参考资料](#五参考资料) - - - -# 一、两阶段提交协议 - -Two-phase Commit(2PC)。 - -可以保证一个事务跨越多个节点时保持 ACID 特性。 - -两类节点:协调者(Coordinator)和参与者(Participants),协调者只有一个,参与者可以有多个。 - -## 运行过程 - -1. 准备阶段:协调者询问参与者事务是否执行成功; - -2. 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 - -

- - -需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 - -## 存在的问题 - -- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。 - -- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。 - -# 二、Paxos 协议 - -用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。 - -主要有三类节点: - -1. 提议者(Proposer):提议一个值; -2. 接受者(Acceptor):对每个提议进行投票; -3. 告知者(Learner):被告知投票的结果,不参与投票过程。 - -

- -## 执行过程 - -规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。 - -下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。 - -

- -当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。 - -如下图,Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,并且设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。 - -

- -如果 Acceptor 接收到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2,那么就丢弃该提议请求;否则,发送提议响应,该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收到的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。 - -如下图,Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的提议请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2,因此就抛弃该提议请求;Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的提议请求,因为之前接收到的提议为 [n=2, v=8],并且 2 <= 4,因此就发送 [n=2, v=8] 的提议响应,设置当前接收到的提议为 [n=4, v=5],并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。 - -

- -当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。 - -Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。 - -Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。 - -

- -Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。 - -

- -## 约束条件 - -### 1. 正确性 - -指只有一个提议值会生效。 - -因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。 - -### 2. 可终止性 - -指最后总会有一个提议生效。 - -Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。 - -# 三、Raft 协议 - -Raft 和 Paxos 类似,但是更容易理解,也更容易实现。 - -Raft 主要是用来竞选主节点。 - -## 单个 Candidate 的竞选 - -有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。 - -① 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 - -

- -② 此时 A 发送投票请求给其它所有节点。 - -

- -③ 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 - -

- -④ 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 - -

- -## 多个 Candidate 竞选 - -① 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 - -

- -② 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 - -

- -## 日志复制 - -① 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 - -

- -② Leader 会把修改复制到所有 Follower。 - -

- -③ Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 - -

- -④ 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 - -

- -# 四、拜占庭将军问题 - -> [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun) - -# 五、参考资料 - -- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013. -- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details) -- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/) -- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) -- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/) diff --git "a/notes/\344\273\243\347\240\201\345\217\257\350\257\273\346\200\247.md" "b/notes/\344\273\243\347\240\201\345\217\257\350\257\273\346\200\247.md" index 744b8bab8933c5d8d2068635bbd4812cc49a1ede..4ba087cbba4e3ec33d9d4a75102d2f4909d69a58 100644 --- "a/notes/\344\273\243\347\240\201\345\217\257\350\257\273\346\200\247.md" +++ "b/notes/\344\273\243\347\240\201\345\217\257\350\257\273\346\200\247.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 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。 为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。 @@ -95,14 +95,14 @@ Map scoreMap = new HashMap<>(); ``` ```java -// Student' name -> Student's score +// Student's name -> Student's score Map 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/\345\210\206\345\270\203\345\274\217\345\237\272\347\241\200.md" "b/notes/\345\210\206\345\270\203\345\274\217\345\237\272\347\241\200.md" index f7f774989350b1fddb6c61d7f7f55f581d8b71e2..f1858594a973fa20cd304c579c3fe5a1aa334dff 100644 --- "a/notes/\345\210\206\345\270\203\345\274\217\345\237\272\347\241\200.md" +++ "b/notes/\345\210\206\345\270\203\345\274\217\345\237\272\347\241\200.md" @@ -6,19 +6,21 @@ * [二、数据分布](#二数据分布) * [哈希分布](#哈希分布) * [顺序分布](#顺序分布) -* [三、负载均衡](#三负载均衡) -* [四、复制](#四复制) - * [强同步复制协议](#强同步复制协议) - * [异步复制协议](#异步复制协议) -* [五、CAP](#五cap) -* [六、BASE](#六base) - * [基本可用](#基本可用) - * [软状态](#软状态) - * [最终一致性](#最终一致性) -* [七、容错](#七容错) + * [负载均衡](#负载均衡) +* [三、复制](#三复制) + * [复制原理](#复制原理) + * [复制协议](#复制协议) + * [CAP](#cap) + * [BASE](#base) +* [四、容错](#四容错) * [故障检测](#故障检测) * [故障恢复](#故障恢复) -* [八、CDN 架构](#八cdn-架构) +* [五、一致性协议](#五一致性协议) + * [两阶段提交协议](#两阶段提交协议) + * [Paxos 协议](#paxos-协议) + * [Raft 协议](#raft-协议) + * [拜占庭将军问题](#拜占庭将军问题) +* [六、CDN 架构](#六cdn-架构) * [参考资料](#参考资料) @@ -73,19 +75,15 @@ 可以从两个角度理解一致性:从客户端的角度,读写操作是否满足某种特性;从服务器的角度,多个数据副本之间是否一致。 -有以下三种一致性模型: - -1. 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值; -2. 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值; -3. 最终一致性:新数据写入之后,只能保证过了一个时间窗口后才能在数据副本上读取到最新值; - ### 4. 可扩展性 指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线性增加。 # 二、数据分布 -分布式系统的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。 +分布式存储系统的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。 + +数据库的水平切分(Sharding)也是一种分布式存储方法,下面的数据分布方法同样适用于 Sharding。 ## 哈希分布 @@ -99,7 +97,7 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n-1],将

-一致性哈希的优点是在加入或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。 +一致性哈希的优点是在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。

@@ -107,31 +105,41 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n-1],将 哈希分布式破坏了数据的有序性,顺序分布则不会。 -顺序分布的数据划分为多个连续的部分,按一定策略分布到不同节点上。例如下图中,User 表的主键范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。 +顺序分布的数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如下图中,User 表的 ID 范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。 -引入 Meta 表是为了支持更大的集群规模,它将原来的一层索引结分成两层,Meta 维护着 User 子表所在的节点,从而减轻 Root 节点的负担。 +顺序分布的优点是可以充分利用每个节点的空间,而哈希分布很难控制一个节点存储多少数据。 + +但是顺序分布需要使用一个映射表来存储数据到节点的映射,这个映射表通常使用单独的节点来存储。当数据量非常大时,映射表也随着变大,那么一个节点就可能无法存放下整个映射表。并且单个节点维护着整个映射表的开销很大,查找速度也会变慢。为了解决以上问题,引入了一个中间层,也就是 Meta 表,从而分担映射表的维护工作。

-# 三、负载均衡 +## 负载均衡 + +衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。 -衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。分布式系统应当能够自动负载均衡,当某个节点的负载较高,将它的部分数据迁移到其它节点。 +分布式系统存储应当能够自动负载均衡,当某个节点的负载较高,将它的部分数据迁移到其它节点。 每个集群都有一个总控节点,其它节点为工作节点,由总控节点根据全局负载信息进行整体调度,工作节点定时发送心跳包(Heartbeat)将节点负载相关的信息发送给总控节点。 一个新上线的工作节点,由于其负载较低,如果不加控制,总控节点会将大量数据同时迁移到该节点上,造成该节点一段时间内无法工作。因此负载均衡操作需要平滑进行,新加入的节点需要较长的一段时间来达到比较均衡的状态。 -# 四、复制 +# 三、复制 + +## 复制原理 复制是保证分布式系统高可用的基础,让一个数据存储多个副本,当某个副本所在的节点出现故障时,能够自动切换到其它副本上,从而实现故障恢复。 -多个副本通常有一个为主副本,其它为备副本。主副本用来处理写请求,备副本主要用来处理读请求,实现读写分离。主副本将同步操作日志发送给备副本,备副本通过回放操作日志获取最新修改。 +多个副本通常有一个为主副本,其它为备副本。主副本用来处理写请求,备副本主要用来处理读请求,实现读写分离。 + +主副本将同步操作日志发送给备副本,备副本通过回放操作日志获取最新修改。

+## 复制协议 + 主备副本之间有两种复制协议,一种是强同步复制协议,一种是异步复制协议。 -## 强同步复制协议 +### 1. 强同步复制协议 要求主副本将同步操作日志发给备副本之后进行等待,要求至少一个备副本返回成功后,才开始修改主副本,修改完成之后通知客户端操作成功。 @@ -139,7 +147,7 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n-1],将 缺点:可用性差,因为主副本需要等待,那么整个分布式系统的可用时间就会降低。 -## 异步复制协议 +### 2. 异步复制协议 主副本将同步操作日志发给备副本之后不需要进行等待,直接修改主副本并通知客户端操作成功。 @@ -147,41 +155,45 @@ Distributed Hash Table(DHT):对于哈希空间 [0, 2n-1],将 缺点:一致性差。 -# 五、CAP +## CAP -分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition tolerance),最多只能同时满足其中两项。这三个概念上文中已经提到。 +分布式存储系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition tolerance),最多只能同时满足其中两项。 在设计分布式系统时,需要根据实际需求弱化某一要求。因此就有了下图中的三种设计:CA、CP 和 AP。 -

+需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的,并且系统需要能够自动容错,因此实际上设计分布式存储系统需要在一致性和可用性之间做权衡。上一节介绍的强同步协议和异步复制协议就是在一致性和可用性做权衡得到的结果。 -需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此实际上设计分布式系统需要在一致性和可用性之间做权衡。 +

-# 六、BASE +## BASE BASE 是 Basically Available(基本可用)、Soft State(软状态)和 Eventually Consistent(最终一致性)三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。

-## 基本可用 +### 1. 基本可用 指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。 例如,电商在做促销时,服务层可能只提供降级服务,部分用户可能会被引导到降级页面上。 -## 软状态 +### 2. 软状态 指允许系统存在中间状态,而该中间状态不会影响系统整体可用性,即不同节点的数据副本之间进行同步的过程允许存在延时。 -## 最终一致性 +### 3. 最终一致性 -指所有的数据副本,在经过一段时间的同步之后,最终都能够达到一致的状态。 +一致性模型包含以下三种: -强一致性需要保证数据副本实时一致,而最终一致性只需要保证过一段时间是一致的。 +- 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值; +- 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值; +- 最终一致性:新数据写入之后,只能保证过了一个时间窗口后才能在数据副本上读取到最新值; -ACID 是传统数据库系统常用的设计理论,追求强一致性模型。BASE 常用于大型分布式系统,只需要保证最终一致性。在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 +强一致性通常运用在需要满足 ACID 的传统数据库系统上,而最终一致性通常运用在大型分布式系统中。应该注意的是,上面介绍的强同步复制协议和异步复制协议都不能保证强一致性,因为它们是分布式系统的复制协议。这两种复制协议如果要满足最终一致性,还需要多加一些控制。 -# 七、容错 +在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 + +# 四、容错 分布式系统故障发生的概率很大,为了实现高可用以及减少人工运维成本,需要实现自动化容错。 @@ -195,7 +207,147 @@ ACID 是传统数据库系统常用的设计理论,追求强一致性模型。 当某个节点故障时,就将它上面的服务迁移到其它节点。 -# 八、CDN 架构 +# 五、一致性协议 + +## 两阶段提交协议 + +Two-phase Commit(2PC)。 + +可以保证一个事务跨越多个节点时保持 ACID 特性,也就是说它可以用来实现分布式事务。 + +两类节点:协调者(Coordinator)和参与者(Participants),协调者只有一个,参与者可以有多个。 + +### 1. 运行过程 + +1. 准备阶段:协调者询问参与者事务是否执行成功; +2. 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 + +需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 + +

+ +### 2. 存在的问题 + +- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。 +- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。 + +## Paxos 协议 + +用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。 + +主要有三类节点: + +- 提议者(Proposer):提议一个值; +- 接受者(Acceptor):对每个提议进行投票; +- 告知者(Learner):被告知投票的结果,不参与投票过程。 + +

+ +### 1. 执行过程 + +规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。 + +下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。 + +

+ +当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。 + +如下图,Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,并且设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。 + +

+ +如果 Acceptor 接收到一个提议请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2,那么就丢弃该提议请求;否则,发送提议响应,该提议响应包含之前已经接收过的提议 [n1, v1],设置当前接收到的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。 + +如下图,Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的提议请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2,因此就抛弃该提议请求;Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的提议请求,因为之前接收到的提议为 [n=2, v=8],并且 2 <= 4,因此就发送 [n=2, v=8] 的提议响应,设置当前接收到的提议为 [n=4, v=5],并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。 + +

+ +当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。 + +Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。 + +Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。 + +

+ +Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。 + +

+ +### 2. 约束条件 + +**(一)正确性** + +指只有一个提议值会生效。 + +因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。 + +**(二)可终止性** + +指最后总会有一个提议生效。 + +Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。 + +## Raft 协议 + +Raft 和 Paxos 类似,但是更容易理解,也更容易实现。 + +Raft 主要是用来竞选主节点。 + +### 1. 单个 Candidate 的竞选 + +有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。 + +- 下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。 + +

+ +- 此时 A 发送投票请求给其它所有节点。 + +

+ +- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。 + +

+ +- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。 + +

+ +### 2. 多个 Candidate 竞选 + +* 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票,例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。 + +

+ +* 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 + +

+ +### 3. 日志复制 + +- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 + +

+ +- Leader 会把修改复制到所有 Follower。 + +

+ +- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 + +

+ +- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 + +

+ +## 拜占庭将军问题 + +> [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun) + +# 六、CDN 架构 通过将内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。不仅可以减轻服务器的负担,也可以提高用户的访问速度。 @@ -206,3 +358,8 @@ ACID 是传统数据库系统常用的设计理论,追求强一致性模型。 # 参考资料 - 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013. +- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013. +- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details) +- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/) +- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) +- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/) diff --git "a/notes/\345\210\206\345\270\203\345\274\217\351\227\256\351\242\230\345\210\206\346\236\220.md" "b/notes/\345\210\206\345\270\203\345\274\217\351\227\256\351\242\230\345\210\206\346\236\220.md" index 60ed87cd933bd1b237ee29a3cc5cd9f0a2d71d02..99e9c04dced6d34ec4b652da39778246ed6da5f0 100644 --- "a/notes/\345\210\206\345\270\203\345\274\217\351\227\256\351\242\230\345\210\206\346\236\220.md" +++ "b/notes/\345\210\206\345\270\203\345\274\217\351\227\256\351\242\230\345\210\206\346\236\220.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-唯一性) * [参考资料](#参考资料) @@ -59,24 +51,24 @@ #### 2.1 消息处理模型 -(一)消息队列 +**(一)消息队列**

-(二)发布/订阅 +**(二)发布/订阅**

#### 2.2 消息的可靠性 -(一)发送端的可靠性 +**(一)发送端的可靠性** 发送端完成操作后一定能将消息成功发送到消息系统。 实现方法:在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息中间件,若转移消息成功则删除消息表中的数据,否则继续重传。 -(二)接收端的可靠性 +**(二)接收端的可靠性** 接收端能够从消息中间件成功消费一次消息。 @@ -115,9 +107,9 @@

-### 4. 加权最小连接(Weighted Least Connection) +### 4. 加权最少连接(Weighted Least Connection) -在最小连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。 +在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。

@@ -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 节点表示

-## 1. Sticky Sessions +### 1. Sticky Sessions 需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。 @@ -273,7 +265,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示

-## 2. Session Replication +### 2. Session Replication 在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。 @@ -281,7 +273,7 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示

-## 3. Persistent DataStore +### 3. Persistent DataStore 将 Session 信息持久化到一个数据库中。 @@ -289,28 +281,10 @@ Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示

-## 4. In-Memory DataStore +### 4. In-Memory DataStore 可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。 -# 六、分库与分表带来的分布式困境与应对之策 - -

- -## 事务问题 - -使用分布式事务。 - -## 查询问题 - -使用汇总表。 - -## 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/\345\211\221\346\214\207 offer \351\242\230\350\247\243.md" "b/notes/\345\211\221\346\214\207 offer \351\242\230\350\247\243.md" index 5e2b25536f013d74c6704ddda0dce8999a6fc964..f4927da897e792da03ff5c0368b4565da3eac7c3 100644 --- "a/notes/\345\211\221\346\214\207 offer \351\242\230\350\247\243.md" +++ "b/notes/\345\211\221\346\214\207 offer \351\242\230\350\247\243.md" @@ -71,7 +71,7 @@ * [61. 扑克牌顺子](#61-扑克牌顺子) * [62. 圆圈中最后剩下的数](#62-圆圈中最后剩下的数) * [63. 股票的最大利润](#63-股票的最大利润) -* [64. 求 1+2+3+...+n](#64-求-1+2+3++n) +* [64. 求 1+2+3+...+n](#64-求-123n) * [65. 不用加减乘除做加法](#65-不用加减乘除做加法) * [66. 构建乘积数组](#66-构建乘积数组) * [67. 把字符串转换成整数](#67-把字符串转换成整数) @@ -192,9 +192,9 @@ public boolean Find(int target, int[][] matrix) { 在字符串尾部填充任意字符,使得字符串的长度等于字符串替换之后的长度。因为一个空格要替换成三个字符(%20),因此当遍历到一个空格时,需要在尾部填充两个任意字符。 -令 idxOfOld 指向字符串原来的末尾位置,idxOfNew 指向字符串现在的末尾位置。idxOfOld 和 idxOfNew 从后向前遍历,当 idxOfOld 遍历到一个空格时,就需要令 idxOfNew 指向的位置依次填充 02%(注意是逆序的),否则就填充上 idxOfOld 指向字符的值。 +令 P1 指向字符串原来的末尾位置,P2 指向字符串现在的末尾位置。P1 和 P2从后向前遍历,当 P1 遍历到一个空格时,就需要令 P2 指向的位置依次填充 02%(注意是逆序的),否则就填充上 P1 指向字符的值。 -从后向前遍是为了在改变 idxOfNew 所指向的内容时,不会影响到 idxOfOld 遍历原来字符串的内容。 +从后向前遍是为了在改变 P2 所指向的内容时,不会影响到 P1 遍历原来字符串的内容。 复杂度:O(N) + O(1) @@ -839,7 +839,7 @@ public int NumberOf1(int n) {

-因为 (x\*x)n/2 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logn)。 +因为 (x\*x)n/2 可以通过递归求解,并且每递归一次,n 都减小一半,因此整个算法的时间复杂度为 O(logN)。 ```java public double Power(double base, int exponent) { @@ -976,8 +976,8 @@ if p.charAt(j) == s.charAt(i) : then dp[i][j] = dp[i-1][j-1]; if p.charAt(j) == '.' : then dp[i][j] = dp[i-1][j-1]; if p.charAt(j) == '*' : if p.charAt(j-1) != s.charAt(i) : then dp[i][j] = dp[i][j-2] // a* only counts as empty - if p.charAt(j-1) == s.charAt(i) - or p.charAt(i-1) == '.' : + if p.charAt(j-1) == s.charAt(i) or + p.charAt(i-1) == '.' : then dp[i][j] = dp[i-1][j] // a* counts as multiple a or dp[i][j] = dp[i][j-1] // a* counts as single a or dp[i][j] = dp[i][j-2] // a* counts as empty @@ -1017,6 +1017,8 @@ public boolean match(char[] str, char[] pattern) { ```java public boolean isNumeric(char[] str) { + if (str == null) + return false; return new String(str).matches("[+-]?\\d*(\\.\\d+)?([eE][+-]?\\d+)?"); } ``` @@ -1083,7 +1085,7 @@ public ListNode FindKthToTail(ListNode head, int k) { ## 解题思路 -使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。 +使用双指针,一个指针 fast 每次移动两个节点,一个指针 slow 每次移动一个节点。因为存在环,所以两个指针必定相遇在环中的某个节点上。假设相遇点在下图的 z1 位置,此时 fast 移动的节点数为 x+2y+z,slow 为 x+y,由于 fast 速度比 slow 快一倍,因此 x+2y+z=2(x+y),得到 x=z。 在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。 @@ -1091,22 +1093,21 @@ public ListNode FindKthToTail(ListNode head, int k) { ```java public ListNode EntryNodeOfLoop(ListNode pHead) { - if (pHead == null) + if (pHead == null || pHead.next == null) return null; ListNode slow = pHead, fast = pHead; while (fast != null && fast.next != null) { fast = fast.next.next; slow = slow.next; - if (slow == fast) { - fast = pHead; - while (slow != fast) { - slow = slow.next; - fast = fast.next; - } - return slow; - } + if (slow == fast) + break; } - return null; + fast = pHead; + while (slow != fast) { + slow = slow.next; + fast = fast.next; + } + return slow; } ``` @@ -1774,17 +1775,15 @@ public int MoreThanHalfNum_Solution(int[] nums) { 快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。可以利用这个特性找出数组的第 K 个元素,这种找第 K 个元素的算法称为快速选择算法。 -找到第 K 个元素之后,就可以再遍历一次数组,所有小于等于该元素的数组元素都是最小的 K 个数。 - ```java public ArrayList GetLeastNumbers_Solution(int[] nums, int k) { + ArrayList ret = new ArrayList<>(); if (k > nums.length || k <= 0) - return new ArrayList<>(); + return ret; int kthSmallest = findKthSmallest(nums, k - 1); - ArrayList ret = new ArrayList<>(); - for (int val : nums) - if (val <= kthSmallest && ret.size() < k) - ret.add(val); + // findKthSmallest 会改变数组,使得前 k 个数都是最小的 k 个数 + for (int i = 0; i < k; i++) + ret.add(nums[i]); return ret; } @@ -1803,10 +1802,12 @@ public int findKthSmallest(int[] nums, int k) { } private int partition(int[] nums, int l, int h) { + // 切分元素 + int parti = nums[l]; int i = l, j = h + 1; while (true) { - while (i < h && nums[++i] < nums[l]) ; - while (j > l && nums[l] < nums[--j]) ; + while (i != h && nums[++i] < parti) ; + while (j != l && nums[--j] > parti) ; if (i >= j) break; swap(nums, i, j); @@ -1816,7 +1817,9 @@ private int partition(int[] nums, int l, int h) { } private void swap(int[] nums, int i, int j) { - int t = nums[i]; nums[i] = nums[j]; nums[j] = t; + int t = nums[i]; + nums[i] = nums[j]; + nums[j] = t; } ``` @@ -1898,22 +1901,18 @@ public class Solution { ## 解题思路 ```java -public class Solution { - private int[] cnts = new int[256]; - private Queue queue = new LinkedList<>(); +private int[] cnts = new int[256]; +private Queue queue = new LinkedList<>(); - public void Insert(char ch) { - cnts[ch]++; - queue.add(ch); - while (!queue.isEmpty() && cnts[queue.peek()] > 1) - queue.poll(); - } +public void Insert(char ch) { + cnts[ch]++; + queue.add(ch); + while (!queue.isEmpty() && cnts[queue.peek()] > 1) + queue.poll(); +} - public char FirstAppearingOnce() { - if (queue.isEmpty()) - return '#'; - return queue.peek(); - } +public char FirstAppearingOnce() { + return queue.isEmpty() ? '#' : queue.peek(); } ``` @@ -1928,17 +1927,17 @@ public class Solution { ## 解题思路 ```java - public int FindGreatestSumOfSubArray(int[] nums) { - if (nums.length == 0) - return 0; - int ret = Integer.MIN_VALUE; - int sum = 0; - for (int val : nums) { - sum = sum <= 0 ? val : sum + val; - ret = Math.max(ret, sum); - } - return ret; - } +public int FindGreatestSumOfSubArray(int[] nums) { + if (nums == null || nums.length == 0) + return 0; + int ret = Integer.MIN_VALUE; + int sum = 0; + for (int val : nums) { + sum = sum <= 0 ? val : sum + val; + ret = Math.max(ret, sum); + } + return ret; +} ``` # 43. 从 1 到 n 整数中 1 出现的次数 @@ -2027,6 +2026,8 @@ private int beginNumber(int digit) { ```java public String PrintMinNumber(int[] numbers) { + if (numbers == null || numbers.length == 0) + return ""; int n = numbers.length; String[] nums = new String[n]; for (int i = 0; i < n; i++) @@ -2192,7 +2193,7 @@ public int FirstNotRepeatingChar(String str) { } ``` -以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么我们只需要统计的次数信息只有 0,1,更大,那么使用两个比特位就能存储这些信息。 +以上实现的空间复杂度还不是最优的。考虑到只需要找到只出现一次的字符,那么我们只需要统计的次数信息只有 0,1,更大,使用两个比特位就能存储这些信息。 ```java public int FirstNotRepeatingChar(String str) { @@ -2739,7 +2740,7 @@ public int LastRemaining_Solution(int n, int m) { ## 解题思路 -使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该是 i 之前并且价格最低。 +使用贪心策略,假设第 i 轮进行卖出操作,买入操作价格应该在 i 之前并且价格最低。 ```java public int maxProfit(int[] prices) { @@ -2795,7 +2796,7 @@ a ^ b 表示没有考虑进位的情况下两数的和,(a & b) << 1 就是进 递归会终止的原因是 (a & b) << 1 最右边会多一个 0,那么继续递归,进位最右边的 0 会慢慢增多,最后进位会变为 0,递归终止。 ```java -public int Add(int num1,int num2) { +public int Add(int num1, int num2) { return num2 == 0 ? num1 : Add(num1 ^ num2, (num1 & num2) << 1); } ``` @@ -2844,17 +2845,17 @@ Output: ```java public int StrToInt(String str) { - if (str.length() == 0) + if (str == null || str.length() == 0) return 0; - char[] chars = str.toCharArray(); - boolean isNegative = chars[0] == '-'; + boolean isNegative = str.charAt(0) == '-'; int ret = 0; - for (int i = 0; i < chars.length; i++) { - if (i == 0 && (chars[i] == '+' || chars[i] == '-')) + for (int i = 0; i < str.length(); i++) { + char c = str.charAt(i); + if (i == 0 && (c == '+' || c == '-')) continue; - if (chars[i] < '0' || chars[i] > '9') + if (c < '0' || c > '9') return 0; // 非法输入 - ret = ret * 10 + (chars[i] - '0'); + ret = ret * 10 + (c - '0'); } return isNegative ? -ret : ret; } @@ -2870,7 +2871,7 @@ public int StrToInt(String str) { [Leetcode : 235. Lowest Common Ancestor of a Binary Search Tree](https://leetcode.com/problems/lowest-common-ancestor-of-a-binary-search-tree/description/) -二叉查找树中,两个节点 p, q 的公共祖先 root 满足 p.val <= root.val && root.val <= q.val,只要找到满足这个条件的最低层节点即可。换句话说,应该先考虑子树的解而不是根节点的解,二叉树的后序遍历操作满足这个特性。在本题中我们可以利用后序遍历的特性,先在左右子树中查找解,最后再考虑根节点的解。 +二叉查找树中,两个节点 p, q 的公共祖先 root 满足 root.val >= p.val && root.val <= q.val。 ```java public TreeNode lowestCommonAncestor(TreeNode root, TreeNode p, TreeNode q) { diff --git "a/notes/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" "b/notes/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" index 050b5af0086a030315afba3517abfdd97f1de4b8..343c23d86594cec1ff4457e58e4cdfa244f89952 100644 --- "a/notes/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" +++ "b/notes/\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\345\216\237\347\220\206.md" @@ -2,6 +2,7 @@ * [一、事务](#一事务) * [概念](#概念) * [四大特性](#四大特性) + * [AUTOCOMMIT](#autocommit) * [二、并发一致性问题](#二并发一致性问题) * [问题](#问题) * [解决方法](#解决方法) @@ -9,6 +10,7 @@ * [封锁粒度](#封锁粒度) * [封锁类型](#封锁类型) * [封锁协议](#封锁协议) + * [MySQL 隐式与显示锁定](#mysql-隐式与显示锁定) * [四、隔离级别](#四隔离级别) * [五、多版本并发控制](#五多版本并发控制) * [版本号](#版本号) @@ -48,15 +50,13 @@ ## 四大特性 -

- ### 1. 原子性(Atomicity) -事务被视为不可分割的最小单元,要么全部提交成功,要么全部失败回滚。 +事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。 ### 2. 一致性(Consistency) -事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。 +数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。 ### 3. 隔离性(Isolation) @@ -66,6 +66,10 @@ 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。可以通过数据库备份和恢复来保证持久性。 +## AUTOCOMMIT + +MySQL 默认采用自动提交模式。也就是说,如果不显式使用`START TRANSACTION`语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。 + # 二、并发一致性问题 在并发环境下,一个事务如果受到另一个事务的影响,那么事务操作就无法满足一致性条件。 @@ -120,40 +124,49 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 ## 封锁类型 -### 1. 排它锁与共享锁 +### 1. 读写锁 - 排它锁(Exclusive),简写为 X 锁,又称写锁。 - 共享锁(Shared),简写为 S 锁,又称读锁。 有以下两个规定: -1. 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。 -2. 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。 +- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。 +- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。 锁的兼容关系如下: | - | X | S | | :--: | :--: | :--: | -|X|No|No| -|S|No|Yes| +|X|NO|NO| +|S|NO|YES| ### 2. 意向锁 -意向锁(Intention Locks)可以支持多粒度封锁。它本身是一个表锁,通过在原来的 X/S 锁之上引入了 IX/IS,用来表示一个事务想要在某个数据行上加 X 锁或 S 锁。 +使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。 -有以下两个规定: +在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。 -1. 一个事务在获得某个数据行对象的 S 锁之前,必须先获得 IS 锁或者更强的锁; -2. 一个事务在获得某个数据行对象的 X 锁之前,必须先获得 IX 锁。 +意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定: + +- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁; +- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。 + +通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。 各种锁的兼容关系如下: | - | X | IX | S | IS | | :--: | :--: | :--: | :--: | :--: | -|X |No |No |No | No| -|IX |No |Yes|No | Yes| -|S |No |No |Yes| Yes| -|IS |No |Yes|Yes| Yes| +|X |NO |NO |NO | NO| +|IX |NO |YES |NO | YES| +|S |NO |NO |YES | YES| +|IS |NO |YES |YES | YES| + +解释如下: + +- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁; +- S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。 ## 封锁协议 @@ -171,9 +184,9 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 | read A=20 | | | | lock-x(A) | | | wait | -| write A=19 | | -| commit | | -| unlock-x(A) | | +| write A=19 |. | +| commit |. | +| unlock-x(A) |. | | | obtain | | | read A=19 | | | write A=21 | @@ -193,9 +206,9 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 | write A=19 | | | | lock-s(A) | | | wait | -| rollback | | -| A=20 | | -| unlock-x(A) | | +| rollback | .| +| A=20 |. | +| unlock-x(A) |. | | | obtain | | | read A=20 | | | commit | @@ -213,9 +226,9 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 | read A=20 | | | |lock-x(A) | | | wait | -| read A=20| | -| commit | | -| unlock-s(A) | | +| read A=20| . | +| commit | .| +| unlock-s(A) |. | | | obtain | | | read A=20 | | | write A=19| @@ -224,7 +237,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 ### 2. 两段锁协议 -加锁和解锁分为两个阶段进行,事务 T 对数据 A 进行读或者写操作之前,必须先获得对 A 的封锁,并且在释放一个封锁之后,T 不能再获得任何的其它锁。 +加锁和解锁分为两个阶段进行。事务 T 对数据 A 进行读或者写操作之前,必须先获得对 A 的封锁,并且在释放一个封锁之后,T 不能再获得任何的其它锁。 事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。 @@ -238,6 +251,17 @@ lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B) lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C) ``` +## MySQL 隐式与显示锁定 + +MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。 + +InnoDB 也可以使用特定的语句进行显示锁定: + +```sql +SELECT ... LOCK In SHARE MODE; +SELECT ... FOR UPDATE; +``` + # 四、隔离级别 **1. 未提交读(READ UNCOMMITTED)**
@@ -267,7 +291,7 @@ lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C) # 五、多版本并发控制 -(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC;可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 +多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC;可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 ## 版本号 @@ -287,7 +311,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 ## 实现过程 -以下过程针对可重复读隔离级别。 +以下过程针对可重复读(REPEATABLE READ)隔离级别。 ### 1. SELECT @@ -295,7 +319,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 -把没对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 +把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 除了上面的要求,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。 @@ -315,7 +339,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回 ### 1. 快照读 -读取快照中的数据,可以减少加锁所带来的开销。 +使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。 ```sql select * from table ...; @@ -323,7 +347,7 @@ select * from table ...; ### 2. 当前读 -读取最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。 +读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。 ```sql select * from table where ? lock in share mode; @@ -335,7 +359,7 @@ delete; # 六、Next-Key Locks -Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 +Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 ## Record Locks @@ -394,13 +418,14 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; | 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 | | 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 | | 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 | +| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 | 不符合范式的关系,会产生很多异常,主要有以下四种异常: -1. 冗余数据:例如 学生-2 出现了两次。 -2. 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 -3. 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。 -4. 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 +- 冗余数据:例如 学生-2 出现了两次。 +- 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 +- 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1,需要删除第一行和第三行,那么 学生-1 的信息就会丢失。 +- 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 ## 范式 @@ -425,6 +450,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; | 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 | | 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 | | 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 | +| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 | 以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖: @@ -444,6 +470,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门 | :---: | :---: | :---: | :---: | | 1 | 学生-1 | 学院-1 | 院长-1 | | 2 | 学生-2 | 学院-2 | 院长-2 | +| 3 | 学生-3 | 学院-2 | 院长-2 | 有以下函数依赖: @@ -457,6 +484,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门 | 1 | 课程-1 | 90 | | 2 | 课程-2 | 80 | | 2 | 课程-1 | 100 | +| 3 | 课程-2 | 95 | 有以下函数依赖: @@ -474,6 +502,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门 | :---: | :---: | :---: | | 1 | 学生-1 | 学院-1 | | 2 | 学生-2 | 学院-2 | +| 3 | 学生-3 | 学院-2 | 关系-12 @@ -615,4 +644,4 @@ Entity-Relationship,有三个组成部分:实体、属性、联系。 - [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/) - [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/) - [浅入浅出 MySQL 和 InnoDB](https://draveness.me/mysql-innodb) -- [fd945daf-4a6c-4f20-b9c2-5390f5955ce5.jpg](https://tech.meituan.com/innodb-lock.html) +- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/innodb-lock.html) diff --git "a/notes/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" "b/notes/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" index 38e8b14cac9074b22f11e49f172a1338b94869c5..c6c21f2c765e72ea5c2a2b6292e8c2a450ef0326 100644 --- "a/notes/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" +++ "b/notes/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" @@ -47,7 +47,7 @@ My **name** is Zheng. **-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符; -**^** 是取非操作,必须在 [ ] 字符集合中使用; +**^** 在 [ ] 字符集合中是取非操作。 **应用** @@ -131,7 +131,7 @@ abc[^0-9] ``` [\w.]+@\w+\.\w+ -[\w.]+@[\w]+\.[\w]+ +[\w.]+@[\w]+[\.][\w]+ ``` **{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符; @@ -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/\347\256\227\346\263\225.md" "b/notes/\347\256\227\346\263\225.md" index b6faef125b0be68269df4bc7ff700d4beedab734..0669b9104425aa81a044cfe1d54a5f7194883f3b 100644 --- "a/notes/\347\256\227\346\263\225.md" +++ "b/notes/\347\256\227\346\263\225.md" @@ -7,27 +7,32 @@ * [二、栈和队列](#二栈和队列) * [栈](#栈) * [队列](#队列) -* [三、union-find](#三union-find) - * [quick-find](#quick-find) - * [quick-union](#quick-union) - * [加权 quick-union](#加权-quick-union) - * [路径压缩的加权 quick-union](#路径压缩的加权-quick-union) - * [各种 union-find 算法的比较](#各种-union-find-算法的比较) -* [四、排序](#四排序) +* [三、排序](#三排序) * [选择排序](#选择排序) + * [冒泡排序](#冒泡排序) * [插入排序](#插入排序) * [希尔排序](#希尔排序) * [归并排序](#归并排序) * [快速排序](#快速排序) - * [优先队列](#优先队列) - * [应用](#应用) -* [五、查找](#五查找) + * [堆排序](#堆排序) + * [桶排序](#桶排序) + * [基数排序](#基数排序) + * [外部排序](#外部排序) + * [排序算法的比较](#排序算法的比较) + * [Java 的排序算法实现](#java-的排序算法实现) +* [四、查找](#四查找) * [二分查找实现有序符号表](#二分查找实现有序符号表) * [二叉查找树](#二叉查找树) * [2-3 查找树](#2-3-查找树) * [红黑二叉查找树](#红黑二叉查找树) * [散列表](#散列表) * [应用](#应用) +* [五、union-find](#五union-find) + * [quick-find](#quick-find) + * [quick-union](#quick-union) + * [加权 quick-union](#加权-quick-union) + * [路径压缩的加权 quick-union](#路径压缩的加权-quick-union) + * [各种 union-find 算法的比较](#各种-union-find-算法的比较) * [参考资料](#参考资料) @@ -54,28 +59,25 @@ N3/6-N2/2+N/3 的增长数量级为 O(N3)。增 ## ThreeSum -ThreeSum 用于统计一个数组中三元组的和为 0 的数量。 +ThreeSum 用于统计一个数组中和为 0 的三元组数量。 ```java public class ThreeSum { - public int count(int[] nums) { + public static int count(int[] nums) { int N = nums.length; int cnt = 0; - for (int i = 0; i < N; i++) { - for (int j = i + 1; j < N; j++) { - for (int k = j + 1; k < N; k++) { - if (nums[i] + nums[j] + nums[k] == 0) { + for (int i = 0; i < N; i++) + for (int j = i + 1; j < N; j++) + for (int k = j + 1; k < N; k++) + if (nums[i] + nums[j] + nums[k] == 0) cnt++; - } - } - } - } + return cnt; } } ``` -该算法的内循环为 if(a[i]+a[j]+a[k]==0) 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 O(N3)。 +该算法的内循环为 `if (nums[i] + nums[j] + nums[k] == 0)` 语句,总共执行的次数为 N(N-1)(N-2) = N3/6-N2/2+N/3,因此它的近似执行次数为 \~N3/6,增长数量级为 O(N3)。 **改进**
@@ -85,28 +87,34 @@ public class ThreeSum { ```java public class ThreeSumFast { - public int count(int[] nums) { + public static int count(int[] nums) { Arrays.sort(nums); int N = nums.length; int cnt = 0; - for (int i = 0; i < N; i++) { + for (int i = 0; i < N; i++) for (int j = i + 1; j < N; j++) { + int target = -nums[i] - nums[j]; + int index = binarySearch(nums, target); // 应该注意这里的下标必须大于 j,否则会重复统计。 - if (binarySearch(nums, -nums[i] - nums[j]) > j) { + if (index <= j) + continue; + while (index < N && nums[index++] == target) cnt++; - } } - } + return cnt; } - private int binarySearch(int[] nums, int target) { + private static int binarySearch(int[] nums, int target) { int l = 0, h = nums.length - 1; while (l <= h) { int m = l + (h - l) / 2; - if (target == nums[m]) return m; - else if (target > nums[m]) l = m + 1; - else h = m - 1; + if (target == nums[m]) + return m; + else if (target > nums[m]) + l = m + 1; + else + h = m - 1; } return -1; } @@ -117,19 +125,40 @@ public class ThreeSumFast { 如果 T(N) \~ aNblogN,那么 T(2N)/T(N) \~ 2b。 -例如对于暴力方法的 ThreeSum 算法,近似时间为 \~N3/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果: +例如对于暴力的 ThreeSum 算法,近似时间为 \~N3/6。进行如下实验:多次运行该算法,每次取的 N 值为前一次的两倍,统计每次执行的时间,并统计本次运行时间与前一次运行时间的比值,得到如下结果: -| N | Time | Ratio | +| N | Time(ms) | Ratio | | :---: | :---: | :---: | -| 250 | 0.0 | 2.7 | -| 500 | 0.0 | 4.8 | -| 1000 | 0.1 | 6.9 | -| 2000 | 0.8 | 7.7 | -| 4000 | 6.4 | 8.0 | -| 8000 | 51.1 | 8.0 | +| 500 | 48 | / | +| 1000 | 320 | 6.7 | +| 2000 | 555 | 1.7 | +| 4000 | 4105 | 7.4 | +| 8000 | 33575 | 8.2 | +| 16000 | 268909 | 8.0 | 可以看到,T(2N)/T(N) \~ 23,因此可以确定 T(N) \~ aN3logN。 +```java +public class RatioTest { + public static void main(String[] args) { + int N = 500; + int K = 7; + long preTime = -1; + while (K-- > 0) { + int[] nums = new int[N]; + long startTime = System.currentTimeMillis(); + int cnt = ThreeSum.count(nums); + long endTime = System.currentTimeMillis(); + long time = endTime - startTime; + double ratio = preTime == -1 ? 0 : (double) time / preTime; + System.out.println(N + " " + time + " " + ratio); + preTime = time; + N *= 2; + } + } +} +``` + ## 注意事项 ### 1. 大常数 @@ -156,36 +185,47 @@ public class ThreeSumFast { ## 栈 -> First-In-Last-Out +First-In-Last-Out - **1. 数组实现**
+### 1. 数组实现 ```java -public class ResizeArrayStack implements Iterable { - private Item[] a = (Item[]) new Object[1]; +import java.util.Iterator; + +public class ResizingArrayStack implements Iterable { + // 栈元素数组 + private Item[] a = (Item[]) new Object[1]; // 只能通过转型来创建泛型数组 + // 元素数量 private int N = 0; public void push(Item item) { - if (N >= a.length) { - resize(2 * a.length); - } + check(); a[N++] = item; } - public Item pop() { + public Item pop() throws Exception { + if (isEmpty()) + throw new Exception("stack is empty"); Item item = a[--N]; - if (N <= a.length / 4) { - resize(a.length / 2); - } + check(); + a[N] = null; // 避免对象游离 return item; } - // 调整数组大小,使得栈具有伸缩性 + private void check() { + if (N >= a.length) + resize(2 * a.length); + else if (N > 0 && N <= a.length / 4) + resize(a.length / 2); + } + + /** + * 调整数组大小,使得栈具有伸缩性 + */ private void resize(int size) { Item[] tmp = (Item[]) new Object[size]; - for (int i = 0; i < N; i++) { + for (int i = 0; i < N; i++) tmp[i] = a[i]; - } a = tmp; } @@ -199,7 +239,7 @@ public class ResizeArrayStack implements Iterable { @Override public Iterator iterator() { - // 需要返回逆序遍历的迭代器 + // 返回逆序遍历的迭代器 return new ReverseArrayIterator(); } @@ -219,12 +259,37 @@ public class ResizeArrayStack implements Iterable { } ``` - **2. 链表实现**
+```java +public static void main(String[] args) throws Exception { + ResizingArrayStack stack = new ResizingArrayStack(); + stack.push(1); + stack.push(2); + stack.push(3); + stack.push(4); + System.out.println(stack.isEmpty()); + System.out.println(stack.size()); + System.out.println(stack.pop()); + System.out.println(stack.pop()); + for (Integer item : stack) + System.out.println(item); +} +``` + +```html +false +4 +4 +3 +2 +1 +``` + +### 2. 链表实现 需要使用链表的头插法来实现,因为头插法中最后压入栈的元素在链表的开头,它的 next 指针指向前一个压入栈的元素,在弹出元素使就可以通过 next 指针遍历到前一个压入栈的元素从而让这个元素称为新的栈顶元素。 ```java -public class Stack { +public class ListStack { private Node top = null; private int N = 0; @@ -250,7 +315,9 @@ public class Stack { N++; } - public Item pop() { + public Item pop() throws Exception { + if (isEmpty()) + throw new Exception("stack is empty"); Item item = top.item; top = top.next; N--; @@ -259,9 +326,30 @@ public class Stack { } ``` +```java +public static void main(String[] args) throws Exception { + ListStack stack = new ListStack(); + stack.push(1); + stack.push(2); + stack.push(3); + stack.push(4); + System.out.println(stack.isEmpty()); + System.out.println(stack.size()); + System.out.println(stack.pop()); + System.out.println(stack.pop()); +} +``` + +```html +false +4 +4 +3 +``` + ## 队列 -> First-In-First-Out +First-In-First-Out 下面是队列的链表实现,需要维护 first 和 last 节点指针,分别指向队首和队尾。 @@ -272,203 +360,80 @@ public class Queue { private Node first; private Node last; int N = 0; - private class Node{ + + private class Node { Item item; Node next; } - public boolean isEmpty(){ + public boolean isEmpty() { return N == 0; } - public int size(){ + public int size() { return N; } - // 入队列 - public void enqueue(Item item){ + public void add(Item item) { Node newNode = new Node(); newNode.item = item; newNode.next = null; - if(isEmpty()){ + if (isEmpty()) { last = newNode; first = newNode; - } else{ + } else { last.next = newNode; last = newNode; } N++; } - // 出队列 - public Item dequeue() { - if (isEmpty()) return null; + public Item remove() throws Exception { + if (isEmpty()) + throw new Exception("queue is empty"); Node node = first; first = first.next; N--; - if (isEmpty()) last = null; + if (isEmpty()) + last = null; return node.item; } } ``` -# 三、union-find - - -用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 - -

- - -| 方法 | 描述 | -| :---: | :---: | -| UF(int N) | 构造一个大小为 N 的并查集 | -| void union(int p, int q) | 连接 p 和 q 节点 | -| int find(int p) | 查找 p 所在的连通分量 | -| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 | - -```java -public class UF { - private int[] id; - - public UF(int N) { - id = new int[N]; - for (int i = 0; i < N; i++) { - id[i] = i; - } - } - - public boolean connected(int p, int q) { - return find(p) == find(q); - } -} -``` - -## quick-find - -可以快速进行 find 操作,即可以快速判断两个节点是否连通。 - -同一连通分量的所有节点的 id 值相等。 - -但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 - -

- ```java -public int find(int p) { - return id[p]; -} -public void union(int p, int q) { - int pID = find(p); - int qID = find(q); - - if (pID == qID) return; - for (int i = 0; i < id.length; i++) { - if (id[i] == pID) id[i] = qID; - } +public static void main(String[] args) throws Exception { + Queue queue = new Queue(); + queue.add(1); + queue.add(2); + System.out.println(queue.remove()); + System.out.println(queue.remove()); + queue.add(3); + queue.add(4); + System.out.println(queue.size()); } ``` -## quick-union - -可以快速进行 union 操作,只需要修改一个节点的 id 值即可。 - -但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同,id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。 - -

- -```java - public int find(int p) { - while (p != id[p]) p = id[p]; - return p; - } - - public void union(int p, int q) { - int pRoot = find(p); - int qRoot = find(q); - if (pRoot == qRoot) return; - id[pRoot] = qRoot; - } -``` - -这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 - -

- -## 加权 quick-union - -为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 - -理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 - -

- -```java -public class WeightedQuickUnionUF { - private int[] id; - // 保存节点的数量信息 - private int[] sz; - - public WeightedQuickUnionUF(int N) { - id = new int[N]; - sz = new int[N]; - for (int i = 0; i < N; i++) { - id[i] = i; - sz[i] = 1; - } - } - - public boolean connected(int p, int q) { - return find(p) == find(q); - } - - public int find(int p) { - while (p != id[p]) p = id[p]; - return p; - } - - public void union(int p, int q) { - int i = find(p); - int j = find(q); - if (i == j) return; - if (sz[i] < sz[j]) { - id[i] = j; - sz[j] += sz[i]; - } else { - id[j] = i; - sz[i] += sz[j]; - } - } -} +```html +1 +2 +2 ``` -## 路径压缩的加权 quick-union - -在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 - -## 各种 union-find 算法的比较 - -| 算法 | union | find | -| :---: | :---: | :---: | -| quick-find | N | 1 | -| quick-union | 树高 | 树高 | -| 加权 quick-union | logN | logN | -| 路径压缩的加权 quick-union | 非常接近 1 | 非常接近 1 | - -# 四、排序 +# 三、排序 待排序的元素需要实现 Java 的 Comparable 接口,该接口有 compareTo() 方法,可以用它来判断两个元素的大小关系。 研究排序算法的成本模型时,计算的是比较和交换的次数。 -使用辅助函数 less() 和 exch() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。 +使用辅助函数 less() 和 swap() 来进行比较和交换的操作,使得代码的可读性和可移植性更好。 ```java -private boolean less(Comparable v, Comparable w) { +private static boolean less(Comparable v, Comparable w) { return v.compareTo(w) < 0; } -private void exch(Comparable[] a, int i, int j) { +private static void swap(Comparable[] a, int i, int j) { Comparable t = a[i]; a[i] = a[j]; a[j] = t; @@ -477,20 +442,20 @@ private void exch(Comparable[] a, int i, int j) { ## 选择排序 -找到数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中找到最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 +选择出数组中的最小元素,将它与数组的第一个元素交换位置。再从剩下的元素中选择出最小的元素,将它与数组的第二个元素交换位置。不断进行这样的操作,直到将整个数组排序。 -

+

```java public class Selection { - public void sort(Comparable[] a) { + public static void sort(Comparable[] a) { int N = a.length; for (int i = 0; i < N; i++) { int min = i; - for (int j = i + 1; j < N; j++) { - if (less(a[j], a[min])) min = j; - } - exch(a, i, min); + for (int j = i + 1; j < N; j++) + if (less(a[j], a[min])) + min = j; + swap(a, i, min); } } } @@ -498,57 +463,79 @@ public class Selection { 选择排序需要 \~N2/2 次比较和 \~N 次交换,它的运行时间与输入无关,这个特点使得它对一个已经排序的数组也需要这么多的比较和交换操作。 -## 插入排序 +## 冒泡排序 -插入排序从左到右进行,每次都将当前元素插入到左部已经排序的数组中,使得插入之后左部数组依然有序。 +通过从左到右不断交换相邻逆序的相邻元素,在一轮的交换之后,可以让未排序的元素上浮到最右侧,是的右侧是已排序的。 -

+在一轮交换中,如果没有发生交换,就说明数组已经是有序的,此时可以直接退出。 ```java -public class Insertion { - public void sort(Comparable[] a) { +public class Bubble { + public static void sort(Comparable[] a) { int N = a.length; - for (int i = 1; i < N; i++) { - for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) { - exch(a, j, j - 1); + boolean hasSorted = false; + for (int i = 0; i < N && !hasSorted; i++) { + hasSorted = true; + for (int j = 0; j < N - i - 1; j++) { + if (less(a[j + 1], a[j])) { + hasSorted = false; + swap(a, j, j + 1); + } } } } } ``` -插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,那么插入排序会很快。 +## 插入排序 + +插入排序从左到右进行,每次都将当前元素插入到左侧已经排序的数组中,使得插入之后左部数组依然有序。 + +第 j 元素是通过不断向左比较并交换来实现插入过程:当第 j 元素小于第 j - 1 元素,就将它们的位置交换,然后令 j 指针向左移动一个位置,不断进行以上操作。 + +

+ +```java +public class Insertion { + public static void sort(Comparable[] a) { + int N = a.length; + for (int i = 1; i < N; i++) + for (int j = i; j > 0 && less(a[j], a[j - 1]); j--) + swap(a, j, j - 1); + } +} +``` + +对于数组 {3, 5, 2, 4, 1},它具有以下逆序:(3, 2), (3, 1), (5, 2), (5, 4), (5, 1), (2, 1), (4, 1),插入排序每次只能交换相邻元素,令逆序数量减少 1,因此插入排序需要交换的次数为逆序数量。 + +插入排序的复杂度取决于数组的初始顺序,如果数组已经部分有序了,逆序较少,那么插入排序会很快。 - 平均情况下插入排序需要 \~N2/4 比较以及 \~N2/4 次交换; -- 最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是逆序的; +- 最坏的情况下需要 \~N2/2 比较以及 \~N2/2 次交换,最坏的情况是数组是倒序的; - 最好的情况下需要 N-1 次比较和 0 次交换,最好的情况就是数组已经有序了。 -插入排序对于部分有序数组和小规模数组特别高效。 - ## 希尔排序 -对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,如果要把元素从一端移到另一端,就需要很多次操作。 +对于大规模的数组,插入排序很慢,因为它只能交换相邻的元素,每次只能将逆序数量减少 1。 -希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,使得元素更快的移到正确的位置上。 +希尔排序的出现就是为了改进插入排序的这种局限性,它通过交换不相邻的元素,每次可以将逆序数量减少大于 1。 -希尔排序使用插入排序对间隔 h 的序列进行排序,如果 h 很大,那么元素就能很快的移到很远的地方。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。 +希尔排序使用插入排序对间隔 h 的序列进行排序。通过不断减小 h,最后令 h=1,就可以使得整个数组是有序的。

```java public class Shell { - public void sort(Comparable[] a) { + public static void sort(Comparable[] a) { int N = a.length; int h = 1; - while (h < N / 3) { + while (h < N / 3) h = 3 * h + 1; // 1, 4, 13, 40, ... - } + while (h >= 1) { - for (int i = h; i < N; i++) { - for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) { - exch(a, j, j - h); - } - } + for (int i = h; i < N; i++) + for (int j = i; j >= h && less(a[j], a[j - h]); j -= h) + swap(a, j, j - h); h = h / 3; } } @@ -569,20 +556,23 @@ public class Shell { ```java public class MergeSort { - private Comparable[] aux; + private static Comparable[] aux; - private void merge(Comparable[] a, int lo, int mid, int hi) { - int i = lo, j = mid + 1; + private static void merge(Comparable[] a, int l, int m, int h) { + int i = l, j = m + 1; - for (int k = lo; k <= hi; k++) { + for (int k = l; k <= h; k++) aux[k] = a[k]; // 将数据复制到辅助数组 - } - for (int k = lo; k <= hi; k++) { - if (i > mid) a[k] = aux[j++]; - else if (j > hi) a[k] = aux[i++]; - else if (aux[i].compareTo(a[j]) <= 0) a[k] = aux[i++]; // 先进行这一步,保证稳定性 - else a[k] = aux[j++]; + for (int k = l; k <= h; k++) { + if (i > m) + a[k] = aux[j++]; + else if (j > h) + a[k] = aux[i++]; + else if (aux[i].compareTo(a[j]) <= 0) + a[k] = aux[i++]; // 先进行这一步,保证稳定性 + else + a[k] = aux[j++]; } } } @@ -594,38 +584,37 @@ public class MergeSort { ```java -public void sort(Comparable[] a) { +public static void sort(Comparable[] a) { aux = new Comparable[a.length]; sort(a, 0, a.length - 1); } -private void sort(Comparable[] a, int lo, int hi) { - if (hi <= lo) return; - int mid = lo + (hi - lo) / 2; - sort(a, lo, mid); - sort(a, mid + 1, hi); - merge(a, lo, mid, hi); +private static void sort(Comparable[] a, int l, int h) { + if (h <= l) + return; + int mid = l + (h - l) / 2; + sort(a, l, mid); + sort(a, mid + 1, h); + merge(a, l, mid, h); } ``` 因为每次都将问题对半分成两个子问题,而这种对半分的算法复杂度一般为 O(NlogN),因此该归并排序方法的时间复杂度也为 O(NlogN)。 -小数组的递归操作会过于频繁,可以在数组过小时切换到插入排序来提高性能。 - ### 3. 自底向上归并排序 -先归并那些微型数组,然后成对归并得到的子数组。 +先归并那些微型数组,然后成对归并得到的微型数组。 ```java -public void busort(Comparable[] a) { - int N = a.length; - aux = new Comparable[N]; - for (int sz = 1; sz < N; sz += sz) { - for (int lo = 0; lo < N - sz; lo += sz + sz) { - merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); - } - } -} + public static void sort(Comparable[] a) { + int N = a.length; + aux = new Comparable[N]; + for (int sz = 1; sz < N; sz += sz) { + for (int lo = 0; lo < N - sz; lo += sz + sz) { + merge(a, lo, lo + sz - 1, Math.min(lo + sz + sz - 1, N - 1)); + } + } + } ``` ## 快速排序 @@ -639,19 +628,20 @@ public void busort(Comparable[] a) { ```java public class QuickSort { - public void sort(Comparable[] a) { + public static void sort(Comparable[] a) { shuffle(a); sort(a, 0, a.length - 1); } - private void sort(Comparable[] a, int lo, int hi) { - if (hi <= lo) return; - int j = partition(a, lo, hi); - sort(a, lo, j - 1); - sort(a, j + 1, hi); + private static void sort(Comparable[] a, int l, int h) { + if (h <= l) + return; + int j = partition(a, l, h); + sort(a, l, j - 1); + sort(a, j + 1, h); } - private void shuffle(Comparable[] array) { + private static void shuffle(Comparable[] array) { List list = Arrays.asList(array); Collections.shuffle(list); list.toArray(array); @@ -663,19 +653,20 @@ public class QuickSort { 取 a[lo] 作为切分元素,然后从数组的左端向右扫描直到找到第一个大于等于它的元素,再从数组的右端向左扫描找到第一个小于等于它的元素,交换这两个元素,并不断进行这个过程,就可以保证左指针 i 的左侧元素都不大于切分元素,右指针 j 的右侧元素都不小于切分元素。当两个指针相遇时,将切分元素 a[lo] 和 a[j] 交换位置。 -

+

```java -private int partition(Comparable[] a, int lo, int hi) { - int i = lo, j = hi + 1; - Comparable v = a[lo]; +private static int partition(Comparable[] a, int l, int h) { + int i = l, j = h + 1; + Comparable v = a[l]; while (true) { - while (less(a[++i], v)) if (i == hi) break; - while (less(v, a[--j])) if (j == lo) break; - if (i >= j) break; - exch(a, i, j); + while (less(a[++i], v) && i != h) ; + while (less(v, a[--j]) && j != l) ; + if (i >= j) + break; + swap(a, i, j); } - exch(a, lo, j); + swap(a, l, j); return j; } ``` @@ -692,9 +683,9 @@ private int partition(Comparable[] a, int lo, int hi) { (一)切换到插入排序 -因为快速排序在小数组中也会调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 +因为快速排序在小数组中也会递归调用自己,对于小数组,插入排序比快速排序的性能更好,因此在小数组中可以切换到插入排序。 -(二)三取样 +(二)三数取中 最好的情况下是每次都能取数组的中位数作为切分元素,但是计算中位数的代价很高。人们发现取 3 个元素并将大小居中的元素作为切分元素的效果最好。 @@ -706,42 +697,72 @@ private int partition(Comparable[] a, int lo, int hi) { ```java public class Quick3Way { - public void sort(Comparable[] a, int lo, int hi) { - if (hi <= lo) return; - int lt = lo, i = lo + 1, gt = hi; - Comparable v = a[lo]; + public static void sort(Comparable[] a) { + sort(a, 0, a.length - 1); + } + + private static void sort(Comparable[] a, int l, int h) { + if (h <= l) + return; + int lt = l, i = l + 1, gt = h; + Comparable v = a[l]; while (i <= gt) { int cmp = a[i].compareTo(v); - if (cmp < 0) exch(a, lt++, i++); - else if (cmp > 0) exch(a, i, gt--); - else i++; + if (cmp < 0) + swap(a, lt++, i++); + else if (cmp > 0) + swap(a, i, gt--); + else + i++; } - sort(a, lo, lt - 1); - sort(a, gt + 1, hi); + sort(a, l, lt - 1); + sort(a, gt + 1, h); + } +} +``` + +### 5. 基于切分的快速选择算法 + +快速排序的 partition() 方法,会返回一个整数 j 使得 a[l..j-1] 小于等于 a[j],且 a[j+1..h] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。 + +可以利用这个特性找出数组的第 k 个元素。 + +```java +public static Comparable select(Comparable[] a, int k) { + int l = 0, h = a.length - 1; + while (h > l) { + int j = partition(a, l, h); + if (j == k) + return a[k]; + else if (j > k) + h = j - 1; + else + l = j + 1; } + return a[k]; } ``` -## 优先队列 +该算法是线性级别的,因为每次正好将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 -优先队列主要用于处理最大元素。 +## 堆排序 ### 1. 堆 堆的某个节点的值总是大于等于子节点的值,并且堆是一颗完全二叉树。 - -堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里我们不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。 +堆可以用数组来表示,因为堆是一种完全二叉树,而完全二叉树很容易就存储在数组中。位置 k 的节点的父节点位置为 k/2,而它的两个子节点的位置分别为 2k 和 2k+1。这里不使用数组索引为 0 的位置,是为了更清晰地描述节点的位置关系。

```java -public class MaxPQ { - private Key[] pq; +public class Heap { + private Comparable[] heap; private int N = 0; - public MaxPQ(int maxN) { - pq = (Key[]) new Comparable[maxN + 1]; + public Heap(int maxN) { + heap = new Comparable[maxN + 1]; + N = maxN; } public boolean isEmpty() { @@ -753,33 +774,33 @@ public class MaxPQ { } private boolean less(int i, int j) { - return pq[i].compareTo(pq[j]) < 0; + return heap[i].compareTo(heap[j]) < 0; } - private void exch(int i, int j) { - Key t = pq[i]; - pq[i] = pq[j]; - pq[j] = t; + private void swap(int i, int j) { + Comparable t = heap[i]; + heap[i] = heap[j]; + heap[j] = t; } } ``` ### 2. 上浮和下沉 -在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作。把这种操作称为上浮。 +在堆中,当一个节点比父节点大,那么需要交换这个两个节点。交换后还可能比它新的父节点大,因此需要不断地进行比较和交换操作,把这种操作称为上浮。

```java private void swim(int k) { while (k > 1 && less(k / 2, k)) { - exch(k / 2, k); + swap(k / 2, k); k = k / 2; } } ``` -类似地,当一个节点比子节点来得小,也需要不断的向下比较和交换操作,把这种操作称为下沉。一个节点有两个子节点,应当与两个子节点中最大那么节点进行交换。 +类似地,当一个节点比子节点来得小,也需要不断地向下进行比较和交换操作,把这种操作称为下沉。一个节点有两个子节点,应当与两个子节点中最大那么节点进行交换。

@@ -787,9 +808,11 @@ private void swim(int k) { private void sink(int k) { while (2 * k <= N) { int j = 2 * k; - if (j < N && less(j, j + 1)) j++; - if (!less(k, j)) break; - exch(k, j); + if (j < N && less(j, j + 1)) + j++; + if (!less(k, j)) + break; + swap(k, j); k = j; } } @@ -800,8 +823,8 @@ private void sink(int k) { 将新元素放到数组末尾,然后上浮到合适的位置。 ```java -public void insert(Key v) { - pq[++N] = v; +public void insert(Comparable v) { + heap[++N] = v; swim(N); } ``` @@ -811,10 +834,10 @@ public void insert(Key v) { 从数组顶端删除最大的元素,并将数组的最后一个元素放到顶端,并让这个元素下沉到合适的位置。 ```java -public Key delMax() { - Key max = pq[1]; - exch(1, N--); - pq[N + 1] = null; +public Comparable delMax() { + Comparable max = heap[1]; + swap(1, N--); + heap[N + 1] = null; sink(1); return max; } @@ -834,19 +857,34 @@ public Key delMax() { 交换之后需要进行下沉操作维持堆的有序状态。 -

+


```java -public static void sort(Comparable[] a){ - int N = a.length; - for(int k = N/2; k >= 1; k--){ - sink(a, k, N); +public class HeapSort { + public static void sort(Comparable[] a) { // 数组第 0 个位置不能有元素 + int N = a.length - 1; + for (int k = N / 2; k >= 0; k--) + sink(a, k, N); + + while (N > 1) { + swap(a, 1, N--); + sink(a, 1, N); + } } - while(N > 1){ - exch(a, 1, N--); - sink(a, 1, N); + + + private static void sink(Comparable[] a, int k, int N) { + while (2 * k <= N) { + int j = 2 * k; + if (j < N && less(a, j, j + 1)) + j++; + if (!less(a, k, j)) + break; + swap(a, k, j); + k = j; + } } } ``` @@ -861,9 +899,13 @@ public static void sort(Comparable[] a){ 现代操作系统很少使用堆排序,因为它无法利用缓存,也就是数组元素很少和相邻的元素进行比较。 -## 应用 +## 桶排序 + +## 基数排序 + +## 外部排序 -### 1. 排序算法的比较 +## 排序算法的比较 | 算法 | 稳定 | 原地排序 | 时间复杂度 | 空间复杂度 | 备注 | | :---: | :---: | :---: | :---: | :---: | :---: | @@ -875,34 +917,13 @@ public static void sort(Comparable[] a){ | 归并排序 | yes | no | NlogN | N | | | 堆排序 | no | yes | NlogN | 1 | | | -快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分之后,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 - -### 2. Java 的排序算法实现 - -Java 系统库中的主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 - -### 3. 基于切分的快速选择算法 - -快速排序的 partition() 方法,会返回一个整数 j 使得 a[lo..j-1] 小于等于 a[j],且 a[j+1..hi] 大于等于 a[j],此时 a[j] 就是数组的第 j 大元素。 - -可以利用这个特性找出数组的第 k 个元素。 +快速排序是最快的通用排序算法,它的内循环的指令很少,而且它还能利用缓存,因为它总是顺序地访问数据。它的运行时间近似为 \~cNlogN,这里的 c 比其他线性对数级别的排序算法都要小。使用三向切分快速排序,实际应用中可能出现的某些分布的输入能够达到线性级别,而其它排序算法仍然需要线性对数时间。 -```java -public static Comparable select(Comparable[] a, int k) { - int lo = 0, hi = a.length - 1; - while (hi > lo) { - int j = partion(a, lo, hi); - if (j == k) return a[k]; - else if (j > k) hi = j - 1; - else lo = j + 1; - } - return a[k]; -} -``` +## Java 的排序算法实现 -该算法是线性级别的,因为每次正好将数组二分,那么比较的总次数为 (N+N/2+N/4+..),直到找到第 k 个元素,这个和显然小于 2N。 +Java 主要排序方法为 java.util.Arrays.sort(),对于原始数据类型使用三向切分的快速排序,对于引用类型使用归并排序。 -# 五、查找 +# 四、查找 符号表是一种存储键值对的数据结构,主要支持两种操作:插入一个新的键值对、根据给定键得到值。 @@ -1612,6 +1633,149 @@ public class SparseVector { } ``` +# 五、union-find + + +用于解决动态连通性问题,能动态连接两个点,并且判断两个点是否连通。 + +

+ + +| 方法 | 描述 | +| :---: | :---: | +| UF(int N) | 构造一个大小为 N 的并查集 | +| void union(int p, int q) | 连接 p 和 q 节点 | +| int find(int p) | 查找 p 所在的连通分量 | +| boolean connected(int p, int q) | 判断 p 和 q 节点是否连通 | + +```java +public class UF { + private int[] id; + + public UF(int N) { + id = new int[N]; + for (int i = 0; i < N; i++) { + id[i] = i; + } + } + + public boolean connected(int p, int q) { + return find(p) == find(q); + } +} +``` + +## quick-find + +可以快速进行 find 操作,即可以快速判断两个节点是否连通。 + +同一连通分量的所有节点的 id 值相等。 + +但是 union 操作代价却很高,需要将其中一个连通分量中的所有节点 id 值都修改为另一个节点的 id 值。 + +

+ +```java +public int find(int p) { + return id[p]; +} +public void union(int p, int q) { + int pID = find(p); + int qID = find(q); + + if (pID == qID) return; + for (int i = 0; i < id.length; i++) { + if (id[i] == pID) id[i] = qID; + } +} +``` + +## quick-union + +可以快速进行 union 操作,只需要修改一个节点的 id 值即可。 + +但是 find 操作开销很大,因为同一个连通分量的节点 id 值不同,id 值只是用来指向另一个节点。因此需要一直向上查找操作,直到找到最上层的节点。 + +

+ +```java + public int find(int p) { + while (p != id[p]) p = id[p]; + return p; + } + + public void union(int p, int q) { + int pRoot = find(p); + int qRoot = find(q); + if (pRoot == qRoot) return; + id[pRoot] = qRoot; + } +``` + +这种方法可以快速进行 union 操作,但是 find 操作和树高成正比,最坏的情况下树的高度为触点的数目。 + +

+ +## 加权 quick-union + +为了解决 quick-union 的树通常会很高的问题,加权 quick-union 在 union 操作时会让较小的树连接较大的树上面。 + +理论研究证明,加权 quick-union 算法构造的树深度最多不超过 logN。 + +

+ +```java +public class WeightedQuickUnionUF { + private int[] id; + // 保存节点的数量信息 + private int[] sz; + + public WeightedQuickUnionUF(int N) { + id = new int[N]; + sz = new int[N]; + for (int i = 0; i < N; i++) { + id[i] = i; + sz[i] = 1; + } + } + + public boolean connected(int p, int q) { + return find(p) == find(q); + } + + public int find(int p) { + while (p != id[p]) p = id[p]; + return p; + } + + public void union(int p, int q) { + int i = find(p); + int j = find(q); + if (i == j) return; + if (sz[i] < sz[j]) { + id[i] = j; + sz[j] += sz[i]; + } else { + id[j] = i; + sz[i] += sz[j]; + } + } +} +``` + +## 路径压缩的加权 quick-union + +在检查节点的同时将它们直接链接到根节点,只需要在 find 中添加一个循环即可。 + +## 各种 union-find 算法的比较 + +| 算法 | union | find | +| :---: | :---: | :---: | +| quick-find | N | 1 | +| quick-union | 树高 | 树高 | +| 加权 quick-union | logN | logN | +| 路径压缩的加权 quick-union | 非常接近 1 | 非常接近 1 | + # 参考资料 - Sedgewick, Robert, and Kevin Wayne. _Algorithms_. Addison-Wesley Professional, 2011. diff --git "a/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237.md" "b/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237.md" index bd907ada0ae572b9beec2595b7c3344dcc25c7d2..59584ad3baf791d5761f824fb2a9ffdb098e0771 100644 --- "a/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237.md" +++ "b/notes/\350\256\241\347\256\227\346\234\272\346\223\215\344\275\234\347\263\273\347\273\237.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 多级反馈队列 +**(三)多级反馈队列**

@@ -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),进程可以通过读写这个通道进行通信。

- **(一)管道**
+**(一)管道** 写进程在管道的尾端写入数据,读进程在管道的首端读出数据。管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。 @@ -532,25 +528,25 @@ Linux 中管道通过空文件实现。 管道有三种: -1. 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用; -2. 流管道:去除第一个限制,支持双向传输; -3. 命名管道:去除第二个限制,可以在不相关进程之间进行通信。 +- 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用; +- 流管道:去除第一个限制,支持双向传输; +- 命名管道:去除第二个限制,可以在不相关进程之间进行通信。

- **(二)消息队列**
+**(二)消息队列** -消息队列克服了信号传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 +消息队列克服了信号量传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。

- **(三)套接字**
+**(三)套接字** 套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信。

-#### 2.2 共享内存 +### 3. 共享内存 操作系统建立一块共享内存,并将其映射到每个进程的地址空间上,进程就可以直接对这块共享内存进行读写。 @@ -564,10 +560,10 @@ Linux 中管道通过空文件实现。

-1. 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 -2. 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 -3. 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 -4. 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 +- 互斥:每个资源要么已经分配给了一个进程,要么就是可用的。 +- 占有和等待:已经得到了某个资源的进程可以再请求新的资源。 +- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 +- 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 ## 死锁的处理方法 @@ -672,60 +668,24 @@ Linux 中管道通过空文件实现。 - 假若找到这样一行,将该进程标记为终止,并将其已分配资源加到 A 中。 - 重复以上两步,直到所有进程都标记为终止,则状态时安全的。 -如果一个状态不是安全的,也需要拒绝进入这个状态。 +如果一个状态不是安全的,需要拒绝进入这个状态。 # 四、内存管理 ## 虚拟内存 -每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 - -## 分页与分段 +虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 -### 1. 分页 +为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 -大部分虚拟内存系统都使用分页技术。把由程序产生的地址称为虚拟地址,它们构成了一个虚拟地址空间。例如有一台计算机可以产生 16 位地址,它的虚拟地址空间为 0\~64K,然而计算机只有 32KB 的物理内存,因此虽然可以编写 64KB 的程序,但它们不能被完全调入内存运行。 +从上面的描述中可以看出,虚拟内存允许程序地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。

-虚拟地址空间划分成固定大小的页,在物理内存中对应的单元称为页框,页和页框大小通常相同,它们之间通过页表进行映射。 - -程序最开始只将一部分页调入页框中,当程序引用到没有在页框的页时,产生缺页中断,进行页面置换,按一定的原则将一部分页框换出,并将页调入。 - -### 2. 分段 - -

- -上图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。 - -

- -分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 - -每个段都需要程序员来划分。 - -### 3. 段页式 - -用分段方法来分配和管理虚拟存储器。程序的地址空间按逻辑单位分成基本独立的段,而每一段有自己的段名,再把每段分成固定大小的若干页。 - -用分页方法来分配和管理实存。即把整个主存分成与上述页大小相等的存储块,可装入作业的任何一页。 - -程序对内存的调入或调出是按页进行的,但它又可按段实现共享和保护。 - -### 4. 分页与分段区别 - -- 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。 - -- 地址空间的维度:分页是一维地址空间,分段是二维的。 - -- 大小是否可以改变:页的大小不可变,段的大小可以动态改变。 - -- 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 - ## 分页系统地址映射 -- 内存管理单元(MMU):管理着虚拟地址空间和物理内存的转换。 -- 页表(Page table):页(虚拟地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。 +- 内存管理单元(MMU):管理着地址空间和物理内存的转换。 +- 页表(Page table):页(地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。 下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。因此对于虚拟地址(0010 000000000100),前 4 位是用来存储页面号,而后 12 位存储在页中的偏移量。 @@ -761,7 +721,24 @@ Linux 中管道通过空文件实现。 该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。 -### 3. 最近最久未使用 +### 3. 最近未使用 + +> NRU, Not Recently Used + +首先,系统为毎一页面设置了两个状态位。当页面被访问 (读或写) 时设置 R 位; 当页面 (即修改页面) 被写入时设置 M 位。当启动一个进程时,它的所有页面的两个位都由操作系统设置成 0,R 位被定期地 (比如在每次时钟中断时) 清零,以区别最近没有被访问的页面和被访问的页面。 + +当发生缺页中断时,操作系统检査所有的页面并根据它们当前的 R 位和 M 位的值,把它们分为 4 类: + +* 第 0 类: 没有被访问,没有被修改 +* 第 1 类: 没有被访问,已被修改 +* 第 2 类: 已被访问,没有被修改 +* 第 3 类: 已被访问,已被修改 + +NRU 算法随机地从类编号最小的非空类中挑选一个页面淘汰之。 + +算法隐含的意思是,在最近一个时钟滴答中 (典型的时间是大约 20ms) 淘汰一个没有被访问的已修改页面要比一个被频繁使用的 “十净” 页面好。NRU 主要优点是易于理解和能够有效地被实现,虽然它的性能不是最好的,但是已经够用了。 + +### 4. 最近最久未使用 > LRU, Least Recently Used @@ -773,7 +750,17 @@ Linux 中管道通过空文件实现。

-### 4. 时钟 +### 5. 第二次机会算法 + +FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改: + +当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候,检查最老页面的 R 位。如果 R 位是 0,那么这个页面既老又没有被使用,可以立刻置换掉; 如果是 1,就将 R 位清 0,并把该页面放到链表的尾端,修改它的装入时间使它就像刚装入的一样,然后继续从链表的头部开始搜索 + +

+ +第二次机会算法就是寻找一个最近的时钟间隔以来没有被访问过的页面。如果所有的页面都被访问过了,该算法就简化为纯粹的 FIFO 算法。 + +### 6. 时钟 > Clock @@ -783,6 +770,32 @@ Linux 中管道通过空文件实现。

+## 分段 + +虚拟内存采用的是分页技术,也就是将地址空间划分成固定大小的页,每一页再与内存进行映射。 + +下图为一个编译器在编译过程中建立的多个表,有 4 个表是动态增长的,如果使用分页系统的一维地址空间,动态增长的特点会导致覆盖问题的出现。 + +

+ +分段的做法是把每个表分成段,一个段构成一个独立的地址空间。每个段的长度可以不同,并且可以动态增长。 + +

+ +## 段页式 + +程序的地址空间划分成多个拥有独立地址空间的段,每个段上的地址空间划分成大小相同的页。这样既拥有分段系统的共享和保护,又拥有分页系统的虚拟内存功能。 + +## 分页与分段的比较 + +- 对程序员的透明性:分页透明,但是分段需要程序员显示划分每个段。 + +- 地址空间的维度:分页是一维地址空间,分段是二维的。 + +- 大小是否可以改变:页的大小不可变,段的大小可以动态改变。 + +- 出现的原因:分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。 + # 五、设备管理 ## 磁盘调度算法 @@ -845,23 +858,23 @@ gcc -o hello hello.c

-1. 预处理阶段:处理以 # 开头的预处理命令; -2. 编译阶段:翻译成汇编程序; -3. 汇编阶段:将汇编程序翻译可重定向目标程序,它是二进制的; -4. 链接阶段:将可重定向目标程序和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标程序。 +- 预处理阶段:处理以 # 开头的预处理命令; +- 编译阶段:翻译成汇编文件; +- 汇编阶段:将汇编文件翻译成可重定向目标文件,它是二进制的; +- 链接阶段:将可重定向目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。 ## 目标文件 -1. 可执行目标文件:可以直接在内存中执行; -2. 可重定向目标文件:可与其他可重定向目标文件在链接阶段合并,创建一个可执行目标文件; -3. 共享目标文件:可以在运行时被动态加载进内存并链接; +- 可执行目标文件:可以直接在内存中执行; +- 可重定向目标文件:可与其它可重定向目标文件在链接阶段合并,创建一个可执行目标文件; +- 共享目标文件:可以在运行时被动态加载进内存并链接; ## 静态链接 静态连接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务: -1. 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。 -2. 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。 +- 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。 +- 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。

@@ -874,8 +887,8 @@ gcc -o hello hello.c 共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示,Windows 系统上它们被称为 DLL。它具有以下特点: -1. 在给定的文件系统中一个库只有一个 .so 文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; -2. 在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。 +- 在给定的文件系统中一个库只有一个 .so 文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; +- 在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。

diff --git "a/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" "b/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" index 955150e740079f29cf7e4ab2d8a3d4211cc2bb5f..15a6866d9be0f1b8fe2d6c6393b918357222d18c 100644 --- "a/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" +++ "b/notes/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234.md" @@ -78,9 +78,9 @@ ## 主机之间的通信方式 -1. 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。 +- 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。 -2. 对等(P2P):不区分客户和服务器。 +- 对等(P2P):不区分客户和服务器。

@@ -102,7 +102,7 @@ 分组交换也使用了存储转发,但是转发的是分组而不是报文。把整块数据称为一个报文,由于一个报文可能很长,需要先进行切分,来满足分组能处理的大小。在每个切分的数据前面加上首部之后就成为了分组,首部包含了目的地址和源地址等控制信息。 -存储转发允许在一条传输线路上传送多个主机的分组,也就是说两个用户之间的通信不需要占用端到端的线路资源。 +分组交换允许在一条传输线路上传送多个主机的分组,也就是说两个用户之间的通信不需要占用端到端的线路资源。 相比于报文交换,由于分组比报文更小,因此分组交换的存储转发速度更加快速。 @@ -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)

@@ -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 地址的映射。

@@ -458,12 +458,12 @@ CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为 ## 路由器分组转发流程 -1. 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。 -2. 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; -3. 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; -4. 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; -5. 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; -6. 报告转发分组出错。 +- 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。 +- 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付; +- 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器; +- 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器; +- 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器; +- 报告转发分组出错。

@@ -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。

@@ -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 使用超时重传来实现可靠传输:如果一个已经发送的报文

-TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量。注意拥塞窗口与发送方窗口的区别,拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 +TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。 + +发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。 为了便于讨论,做如下假设: -1. 接收方有足够大的接收缓存,因此不会发生流量控制; -2. 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。 +- 接收方有足够大的接收缓存,因此不会发生流量控制; +- 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。

@@ -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 码的编码规则。

@@ -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/\350\256\276\350\256\241\346\250\241\345\274\217.md" "b/notes/\350\256\276\350\256\241\346\250\241\345\274\217.md" index 400ede5eb3572ebf49e2a7c769bca889f3e4b0f6..0fbd186ea2662df04ebb1de6a4e83ff69f8b04cd 100644 --- "a/notes/\350\256\276\350\256\241\346\250\241\345\274\217.md" +++ "b/notes/\350\256\276\350\256\241\346\250\241\345\274\217.md" @@ -47,7 +47,7 @@ 以下实现中,私有静态变量 uniqueInstance 被延迟化实例化,这样做的好处是,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。 -这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if(uniqueInstance == null) ,那么就会多次实例化 uniqueInstance。 +这个实现在多线程环境下是不安全的,如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么多个线程会执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance。 ```java public class Singleton { @@ -350,8 +350,8 @@ public class Client { public static void main(String[] args) { AbstractFactory abstractFactory = new ConcreteFactory1(); AbstractProductA productA = abstractFactory.createProductA(); - abstractFactory = new ConcreteFactory2(); - productA = abstractFactory.createProductA(); + AbstractProductB productB = abstractFactory.createProductB(); + // do something with productA and productB } } ``` diff --git "a/notes/\351\207\215\346\236\204.md" "b/notes/\351\207\215\346\236\204.md" index e74d3cc51ad7b8d99142081ed2edc0fbcb904442..947a5455518741c626c1cc87cbce17f8e8981183 100644 --- "a/notes/\351\207\215\346\236\204.md" +++ "b/notes/\351\207\215\346\236\204.md" @@ -125,53 +125,115 @@ 包括三个类:Movie、Rental 和 Customer,Rental 包含租赁的 Movie 以及天数。 -

+

最开始的实现是把所有的计费代码都放在 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 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; + } +} +``` + +```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()); } } ``` -使用 switch 的准则是:只能在对象自己的数据上使用,而不能在另一个对象的数据基础上使用。解释如下:switch 使用的数据通常是一组相关的数据,例如上面的代码使用了 Movie 的多种类别数据。当这组类别的数据发生改变时,例如上面的代码中增加 Movie 的类别或者修改一种 Movie 类别的计费方法,就需要修改 switch 代码。如果允许违反了准则,就会有多个地方的 switch 使用了这部分的数据,那么需要打开所有的 switch 代码进行修改。 +```html +5 +``` + +使用 switch 的准则是:只使用 switch 所在类的数据。解释如下:switch 使用的数据通常是一组相关的数据,例如 getTotalCharge() 代码使用了 Movie 的多种类别数据。当这组类别的数据发生改变时,例如增加 Movie 的类别或者修改一种 Movie 类别的计费方法,就需要修改 switch 代码。如果违反了准则,就会有多个地方的 switch 使用了这部分的数据,那么这些 swtich 都需要进行修改,这些代码可能遍布在各个地方,修改工作往往会很难进行。上面的实现违反了这一准则,因此需要重构。 以下是继承 Movie 的多态解决方案,这种方案可以解决上述的 switch 问题,因为每种电影类别的计费方式都被放到了对应 Movie 子类中,当变化发生时,只需要去修改对应子类中的代码即可。 -

+

-但是由于 Movie 可以在其生命周期内修改自己的类别,一个对象却不能在生命周期内修改自己所属的类,因此这种方案不可行。可以使用策略模式来解决这个问题(原书写的是使用状态模式,但是这里应该为策略模式,具体可以参考设计模式内容)。 +但是我们需要允许一部影片可以在运行过程中改变其所属的分类,但是上述的继承方案却不可行,因为一个对象所属的类在编译过程就确定了。 -下图中,Price 有多种实现,Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。 +为了解决上述的问题,需要使用策略模式。引入 Price 类,它有多种实现。Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。 -

+

重构后整体的类图和时序图如下: -

+

-

+

# 二、重构原则 @@ -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 对象内的重复数据。

@@ -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 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/notes/\351\235\242\345\220\221\345\257\271\350\261\241\346\200\235\346\203\263.md" "b/notes/\351\235\242\345\220\221\345\257\271\350\261\241\346\200\235\346\203\263.md" index 8cd324cb20a1d6962aaf95f9241c68f3ff80a371..a25a8b00e09101d88837d1ff24a41a6ca6789399 100644 --- "a/notes/\351\235\242\345\220\221\345\257\271\350\261\241\346\200\235\346\203\263.md" +++ "b/notes/\351\235\242\345\220\221\345\257\271\350\261\241\346\200\235\346\203\263.md" @@ -136,7 +136,7 @@ public class Person { } public void work() { - if(18 <= age && age <= 50) { + if (18 <= age && age <= 50) { System.out.println(name + " is working very hard!"); } else { System.out.println(name + " can't work any more!"); @@ -163,9 +163,9 @@ Animal animal = new Cat(); 运行时多态有三个条件: -1. 继承 -2. 覆盖 -3. 向上转型 +- 继承 +- 覆盖(重写) +- 向上转型 下面的代码中,乐器类(Instrument)有两个子类:Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。 @@ -236,9 +236,9 @@ public class Music { 和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式: -1. A 类是 B 类中的(某中方法的)局部变量; -2. A 类是 B 类方法当中的一个参数; -3. A 类向 B 类发送消息,从而影响 B 类发生变化; +- A 类是 B 类中的(某中方法的)局部变量; +- A 类是 B 类方法当中的一个参数; +- A 类向 B 类发送消息,从而影响 B 类发生变化;

diff --git a/pics/014fbc4d-d873-4a12-b160-867ddaed9807.jpg b/pics/014fbc4d-d873-4a12-b160-867ddaed9807.jpg new file mode 100644 index 0000000000000000000000000000000000000000..39c003ce56f5b81c3b610ef2b8b43140cc2c8ed0 Binary files /dev/null and b/pics/014fbc4d-d873-4a12-b160-867ddaed9807.jpg differ diff --git a/pics/051e436c-0e46-4c59-8f67-52d89d656182.png b/pics/051e436c-0e46-4c59-8f67-52d89d656182.png new file mode 100644 index 0000000000000000000000000000000000000000..e3054539f4044e0e4ed9cf61a6f445def05e0450 Binary files /dev/null and b/pics/051e436c-0e46-4c59-8f67-52d89d656182.png differ diff --git a/pics/075e1977-7846-4928-96c8-bb5b0268693c.jpg b/pics/075e1977-7846-4928-96c8-bb5b0268693c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b8b100a01747e73c5c0dbd9d661a65eff0de85f8 Binary files /dev/null and b/pics/075e1977-7846-4928-96c8-bb5b0268693c.jpg differ diff --git a/pics/1492928105791_3.png b/pics/1492928105791_3.png new file mode 100644 index 0000000000000000000000000000000000000000..d18fc1cae99fe250d8473bf5737ef1f0d4e3159f Binary files /dev/null and b/pics/1492928105791_3.png differ diff --git a/pics/1492928416812_4.png b/pics/1492928416812_4.png new file mode 100644 index 0000000000000000000000000000000000000000..a43a731b5e0404d75801afb9e5111bca3d1e0df6 Binary files /dev/null and b/pics/1492928416812_4.png differ diff --git a/pics/1492929000361_5.png b/pics/1492929000361_5.png new file mode 100644 index 0000000000000000000000000000000000000000..919d1222cf3aa94fdde35b5db0afff111f2f3868 Binary files /dev/null and b/pics/1492929000361_5.png differ diff --git a/pics/1492929444818_6.png b/pics/1492929444818_6.png new file mode 100644 index 0000000000000000000000000000000000000000..0aea3f9afe872d62ec5f6b45491f4cd3ec386a7f Binary files /dev/null and b/pics/1492929444818_6.png differ diff --git a/pics/1492929553651_7.png b/pics/1492929553651_7.png new file mode 100644 index 0000000000000000000000000000000000000000..13cf0b4eec326e0dbdbb8b40a5c6785d68f34950 Binary files /dev/null and b/pics/1492929553651_7.png differ diff --git a/pics/1492930243286_8.png b/pics/1492930243286_8.png new file mode 100644 index 0000000000000000000000000000000000000000..6ee721ff784b6ea4cdb8732b7cf0e688031b90af Binary files /dev/null and b/pics/1492930243286_8.png differ diff --git a/pics/2017-06-11-ca.png b/pics/2017-06-11-ca.png new file mode 100644 index 0000000000000000000000000000000000000000..550292c1060326c5e102d916f7e509a5b4b604f4 Binary files /dev/null and b/pics/2017-06-11-ca.png differ diff --git a/pics/2018-05-29-15275543393037.jpg b/pics/2018-05-29-15275543393037.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4af67ef08b31b9a8a1a357580303c042d96e195b Binary files /dev/null and b/pics/2018-05-29-15275543393037.jpg differ diff --git a/pics/3ca58a41-8794-49c1-992e-de5d579a50d1.png b/pics/3ca58a41-8794-49c1-992e-de5d579a50d1.png new file mode 100644 index 0000000000000000000000000000000000000000..725f2f296b3f6c5ec596e1cfecc59df3cc858328 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 0000000000000000000000000000000000000000..a60891bb9aeae6dd7ac165003e855a4eaa5558a1 Binary files /dev/null and b/pics/41026c79-dfc1-40f7-85ae-062910fd272b.png differ diff --git a/pics/49d6de7b-0d0d-425c-9e49-a1559dc23b10.png b/pics/49d6de7b-0d0d-425c-9e49-a1559dc23b10.png new file mode 100644 index 0000000000000000000000000000000000000000..eea3c6b4039c5af7a69653eee2478cf8d42cc0b9 Binary files /dev/null and b/pics/49d6de7b-0d0d-425c-9e49-a1559dc23b10.png differ diff --git a/pics/4cf711a8-7ab2-4152-b85c-d5c226733807.png b/pics/4cf711a8-7ab2-4152-b85c-d5c226733807.png new file mode 100644 index 0000000000000000000000000000000000000000..25ac4f70b3993346c47372db025a1a089de3012e Binary files /dev/null and b/pics/4cf711a8-7ab2-4152-b85c-d5c226733807.png differ diff --git a/pics/4d930e22-f493-49ae-8dff-ea21cd6895dc.png b/pics/4d930e22-f493-49ae-8dff-ea21cd6895dc.png new file mode 100644 index 0000000000000000000000000000000000000000..7ee2ffb4d2c9623943c4d9ef8811d7518cdeb0af Binary files /dev/null and b/pics/4d930e22-f493-49ae-8dff-ea21cd6895dc.png differ diff --git a/pics/50831a6f-2777-46ea-a571-29f23c85cc21.jpg b/pics/50831a6f-2777-46ea-a571-29f23c85cc21.jpg new file mode 100644 index 0000000000000000000000000000000000000000..4a3f798bca221896646342f83552377f82ceb852 Binary files /dev/null and b/pics/50831a6f-2777-46ea-a571-29f23c85cc21.jpg differ diff --git a/pics/5b910141-08b6-442d-a4bc-a1608458c636.png b/pics/5b910141-08b6-442d-a4bc-a1608458c636.png new file mode 100644 index 0000000000000000000000000000000000000000..671346563899e95c755d32c8fa0167aeff62b268 Binary files /dev/null and b/pics/5b910141-08b6-442d-a4bc-a1608458c636.png differ diff --git a/pics/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg b/pics/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg new file mode 100644 index 0000000000000000000000000000000000000000..d6471621610e63c095cd5e01cf9b1bbc0a504b0c Binary files /dev/null and b/pics/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg differ diff --git a/pics/6729baa0-57d7-4817-b3aa-518cbccf824c.jpg b/pics/6729baa0-57d7-4817-b3aa-518cbccf824c.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7035f001076218e1311f886e41a18265f6d2eba6 Binary files /dev/null and b/pics/6729baa0-57d7-4817-b3aa-518cbccf824c.jpg differ diff --git a/pics/80804f52-8815-4096-b506-48eef3eed5c6.png b/pics/80804f52-8815-4096-b506-48eef3eed5c6.png new file mode 100644 index 0000000000000000000000000000000000000000..06c8760b9d580d016f3871f37f5b8307cc6ca698 Binary files /dev/null and b/pics/80804f52-8815-4096-b506-48eef3eed5c6.png differ diff --git a/pics/83185315-793a-453a-a927-5e8d92b5c0ef.jpg b/pics/83185315-793a-453a-a927-5e8d92b5c0ef.jpg new file mode 100644 index 0000000000000000000000000000000000000000..cb83edeaf33d5a543e9cdeacd0a7c768f32944c7 Binary files /dev/null and b/pics/83185315-793a-453a-a927-5e8d92b5c0ef.jpg differ diff --git a/pics/86e6a91d-a285-447a-9345-c5484b8d0c47.png b/pics/86e6a91d-a285-447a-9345-c5484b8d0c47.png new file mode 100644 index 0000000000000000000000000000000000000000..56f83eafa3eb2adf563ae19ceefbcc3d36485785 Binary files /dev/null and b/pics/86e6a91d-a285-447a-9345-c5484b8d0c47.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 0000000000000000000000000000000000000000..86b3b637dbf1c6afb4844011c89523a23bc26dff Binary files /dev/null and b/pics/8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png differ diff --git a/pics/924914c0-660c-4e4a-bbc0-1df1146e7516.jpg b/pics/924914c0-660c-4e4a-bbc0-1df1146e7516.jpg new file mode 100644 index 0000000000000000000000000000000000000000..5bb29598b22e99cb40e6511faf4ea89f61e5371c Binary files /dev/null and b/pics/924914c0-660c-4e4a-bbc0-1df1146e7516.jpg differ diff --git a/pics/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png b/pics/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png new file mode 100644 index 0000000000000000000000000000000000000000..26ac536e067c0093baeaa3d1c521520906fbb186 Binary files /dev/null and b/pics/9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png differ diff --git a/pics/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png b/pics/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png new file mode 100644 index 0000000000000000000000000000000000000000..ba045e207619b632cdd5007f92b64a450b4a65d6 Binary files /dev/null and b/pics/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png differ diff --git a/pics/BSD_disk.png b/pics/BSD_disk.png new file mode 100644 index 0000000000000000000000000000000000000000..48b0e0e82cddff22b31fee0adfb94bca1f85785a Binary files /dev/null and b/pics/BSD_disk.png differ diff --git a/pics/HTTP1_x_Connections.png b/pics/HTTP1_x_Connections.png new file mode 100644 index 0000000000000000000000000000000000000000..d8c18a3cb707e49994a3701557d58046c562814b Binary files /dev/null and b/pics/HTTP1_x_Connections.png differ diff --git a/pics/HowLinkedListWorks.png b/pics/HowLinkedListWorks.png new file mode 100644 index 0000000000000000000000000000000000000000..2640d80d7bbe101b1d6b396413f9bf15d27c4a18 Binary files /dev/null and b/pics/HowLinkedListWorks.png differ diff --git a/pics/_u4E0B_u8F7D.png b/pics/_u4E0B_u8F7D.png new file mode 100644 index 0000000000000000000000000000000000000000..9da973302d7e9b169ca97fa03fffeaf745accf97 Binary files /dev/null and b/pics/_u4E0B_u8F7D.png differ diff --git a/pics/af198da1-2480-4043-b07f-a3b91a88b815.png b/pics/af198da1-2480-4043-b07f-a3b91a88b815.png new file mode 100644 index 0000000000000000000000000000000000000000..34d7a286a596776b4c9a27590c9e2ec6a04972e3 Binary files /dev/null and b/pics/af198da1-2480-4043-b07f-a3b91a88b815.png differ diff --git a/pics/baaa681f-7c52-4198-a5ae-303b9386cf47.png b/pics/baaa681f-7c52-4198-a5ae-303b9386cf47.png new file mode 100644 index 0000000000000000000000000000000000000000..6664d39875b1ee30c7bd706e08d9921f78d65b4a Binary files /dev/null and b/pics/baaa681f-7c52-4198-a5ae-303b9386cf47.png differ diff --git a/pics/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png b/pics/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png new file mode 100644 index 0000000000000000000000000000000000000000..07d8692d3c70562dc9cfda2b9a22d8a2832414c1 Binary files /dev/null and b/pics/bb6a49be-00f2-4f27-a0ce-4ed764bc605c.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 0000000000000000000000000000000000000000..781c0eff2cbd69bd623b505f98ca4f8caf8577bd 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 0000000000000000000000000000000000000000..c967b300c4316da192d6772e611110bc512f9864 Binary files /dev/null and b/pics/c81af7d8-3128-4a3c-a9c9-3e0f5b87ab22.jpg differ diff --git a/pics/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg b/pics/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg new file mode 100644 index 0000000000000000000000000000000000000000..0968e1bc2dd7c64c736cc057c421e3ca3ea613f3 Binary files /dev/null and b/pics/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg differ diff --git a/pics/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png b/pics/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png new file mode 100644 index 0000000000000000000000000000000000000000..105916c6a61e23cb4c46347ad4e519794cb360e5 Binary files /dev/null and b/pics/e3f1657c-80fc-4dfa-9643-bf51abd201c6.png differ diff --git a/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png b/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png new file mode 100644 index 0000000000000000000000000000000000000000..25ed74973e2b42d02d84c0a2444e5bbcdf3d38df Binary files /dev/null and b/pics/ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png differ diff --git a/pics/f0574025-c514-49f5-a591-6d6a71f271f7.jpg b/pics/f0574025-c514-49f5-a591-6d6a71f271f7.jpg new file mode 100644 index 0000000000000000000000000000000000000000..66a2ecbe205aaf2988c667145936eb92eb88054f Binary files /dev/null and b/pics/f0574025-c514-49f5-a591-6d6a71f271f7.jpg differ diff --git a/pics/f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg b/pics/f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg new file mode 100644 index 0000000000000000000000000000000000000000..b6a0ba7900d83fd47f5d9a5b99dd91acc523a92c Binary files /dev/null and b/pics/f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg differ diff --git a/pics/inode_with_signatures.jpg b/pics/inode_with_signatures.jpg new file mode 100644 index 0000000000000000000000000000000000000000..518ba5ac845b8472326fce69862b2c8a8b85583d Binary files /dev/null and b/pics/inode_with_signatures.jpg differ diff --git "a/pics/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\350\204\221\345\233\276.png" "b/pics/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\350\204\221\345\233\276.png" new file mode 100644 index 0000000000000000000000000000000000000000..ba92ffbb32541a3695b21d42be90a2468f1e547f Binary files /dev/null and "b/pics/\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\350\204\221\345\233\276.png" differ