diff --git a/.gitignore b/.gitignore index af58e420..60e0fba5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .DS_Store +<<<<<<< HEAD # idea code/.idea @@ -13,4 +14,7 @@ code/out # gradle code/.gradle -!code/gradle/wrapper/gradle-wrapper.jar \ No newline at end of file +!code/gradle/wrapper/gradle-wrapper.jar +======= +*.txt +>>>>>>> 4770ba68eceb376456a49d8e08f5e74780bbda20 diff --git a/BOOKLIST.md b/BOOKLIST.md index bdd8eb92..4805a7dc 100644 --- a/BOOKLIST.md +++ b/BOOKLIST.md @@ -79,4 +79,4 @@ - [JavaScript 语言精粹](https://book.douban.com/subject/3590768/) - [利用 Python 进行数据分析](https://book.douban.com/subject/25779298/) -- [概率论与数理统计](https://book.douban.com/subject/2201479/) \ No newline at end of file +- [概率论与数理统计](https://book.douban.com/subject/2201479/) diff --git a/README.md b/README.md index 1a58bebb..6b76988d 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@

- +
@@ -21,59 +21,59 @@ > [剑指 Offer 题解](notes/剑指%20offer%20题解.md) -目录根据原书第二版进行编排。 +目录根据原书第二版进行编排,代码和原书有所不同,尽量比原书更简洁。 > [Leetcode 题解](notes/Leetcode%20题解.md) -做了一个大致分类,并对每种分类题型的解题思路做了总结。 +对题目做了一个大致分类,并对每种题型的解题思路做了总结。 > [算法](notes/算法.md) -主要参考 Robert Sedgewick 的算法书进行实现,源代码以及测试代码可在另一个仓库获取。 +排序、并查集、栈和队列、红黑树、散列表。 ## 操作系统 :computer: > [计算机操作系统](notes/计算机操作系统.md) -参考 现代操作系统、Unix 环境高级编程、深入理解计算机系统。 +进程管理、内存管理、设备管理、链接。 > [Linux](notes/Linux.md) -参考 鸟哥的 Linux 私房菜。 +基本实现原理以及基本操作。 ## 网络 :cloud: > [计算机网络](notes/计算机网络.md) -参考 谢希仁的计算机网络、计算机网络 自顶向下方法、TCP/IP 详解。 +物理层、链路层、网络层、运输层、应用层。 > [HTTP](notes/HTTP.md) -参考 图解 HTTP,更多的是参考网上的文档,比如 MDN、维基百科等。 +方法、状态码、Cookie、缓存、连接管理、HTTPs、HTTP 2.0。 > [Socket](notes/Socket.md) -参考 Unix 网络编程。 +I/O 模型、I/O 多路复用。 ## 面向对象 :couple: > [设计模式](notes/设计模式.md) -参考 Head First 设计模式、设计模式 可复用面向对象软件的基础,实现了 Gof 的 23 种设计模式。 +实现了 Gof 的 23 种设计模式。 > [面向对象思想](notes/面向对象思想.md) -内容包括三大原则(继承、封装、多态)、类图、设计原则。 +三大原则(继承、封装、多态)、类图、设计原则。 ## 数据库 :floppy_disk: > [数据库系统原理](notes/数据库系统原理.md) -参考 数据库系统原理。 +事务、锁、隔离级别、MVCC、间隙锁、范式。 > [SQL](notes/SQL.md) -参考 SQL 必知必会。 +SQL 基本语法。 > [Leetcode-Database 题解](notes/Leetcode-Database%20题解.md) @@ -81,43 +81,59 @@ Leetcode 上数据库题目的解题记录。 > [MySQL](notes/MySQL.md) -参考 高性能 MySQL。 +存储引擎、索引、查询优化、切分、复制。 > [Redis](notes/Redis.md) -参考 Redis 设计与实现、Redis 实战。 +五种数据类型、字典和跳跃表数据结构、使用场景、和 Memcache 的比较、淘汰策略、持久化、文件事件的 Reactor 模式、复制。 ## Java :coffee: > [Java 基础](notes/Java%20基础.md) -参考 Effective Java、Java 编程思想,也有部分内容参考官方文档以及 StackOverflow。 +不会涉及很多基本语法介绍,主要是一些实现原理以及关键特性。 -> [Java 虚拟机](notes/Java%20虚拟机.md) +> [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md) -参考 深入理解 Java 虚拟机。 +源码分析:ArrayList、Vector、CopyOnWriteArrayList、LinkedList、HashMap、ConcurrentHashMap、LinkedHashMap、WeekHashMap。 > [Java 并发](notes/Java%20并发.md) -参考 Java 编程思想、深入理解 Java 虚拟机。 +线程使用方式、两种互斥同步方法、线程协作、JUC、线程安全、内存模型、锁优化。 -> [Java 容器](notes/Java%20容器.md) +> [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md) -包含容器源码的分析。 +运行时数据区域、垃圾收集、类加载。 > [Java I/O](notes/Java%20IO.md) -包含 NIO 的原理以及实例。 +NIO 的原理以及实例。 ## 系统设计 :bulb: -> [一致性](notes/一致性.md) +> [系统设计基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/系统设计基础.md) -CAP、BASE、Paxos、Raft +性能、伸缩性、扩展性、可用性、安全性 ->[分布式问题分析](notes/分布式问题分析.md) +> [分布式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式.md) -分布式事务、分布式锁、分布式 Session、负载均衡 +分布式锁、分布式事务、CAP、BASE、Paxos、Raft + +> [集群](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/集群.md) + +负载均衡、Session 管理 + +> [攻击技术](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/攻击技术.md) + +XSS、CSRF、SQL 注入、DDoS + +> [缓存](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/缓存.md) + +缓存特征、缓存位置、缓存问题、数据分布、一致性哈希、LRU、CDN + +> [消息队列](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/消息队列.md) + +消息处理模型、使用场景、可靠性 ## 工具 :hammer: @@ -125,9 +141,17 @@ CAP、BASE、Paxos、Raft 一些 Git 的使用和概念。 -> [正则表达式](notes/正则表达式.md) +> [Docker](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Docker.md) -参考 正则表达式必知必会。 +Docker 基本原理。 + +> [正则表达式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/正则表达式.md) + +正则表达式基本语法。 + +> [构建工具](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/构建工具.md) + +构建工具的基本概念、主流构建工具介绍。 ## 编码实践 :speak_no_evil: @@ -157,7 +181,9 @@ Google 开源项目的代码风格规范。 笔记内容是笔者一个字一个字打上去的,难免会有一些笔误,如果发现笔误可直接在相应文档进行编辑修改。 -欢迎提交对本仓库的改进建议~ +如果想要提交一个仓库现在还没有的全新内容,可以先将相应的文档放到 other 目录下。 + +欢迎在 Issue 中提交对本仓库的改进建议~ **Authorization** @@ -165,21 +191,21 @@ Google 开源项目的代码风格规范。 转载使用请注明出处,谢谢! -**Uploading** - -笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上,实现了一整套自动化上传方案,包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。 - -进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。 - -这里提供了笔者实现的 GFM 文档转换工具的链接:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 - **Typesetting** 笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/) 进行排版,以保证内容的可读性。 笔记不使用 `![]()` 这种方式来引用图片,而是用 `` 标签。一方面是为了能够控制图片以合适的大小显示,另一方面是因为 GFM 不支持 `
![]()
` 让图片居中显示,只能使用 `
` 达到居中的效果。 -这里提供了笔者实现的中英混排文档在线排版工具的链接:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。 +笔者将自己实现的文档排版功能提取出来,放在 Github Page 中,无需下载安装即可免费使用:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。 + +**Uploading** + +笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上,实现了一整套自动化上传方案,包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。 + +进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。 + +笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github,或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 **Statement** diff --git a/SUMMARY.md b/SUMMARY.md index e60f018e..69b6103c 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -1,3 +1,5 @@ +This file used to generate gitbook catalogue. + # Summary * 算法 @@ -32,5 +34,3 @@ - - diff --git a/notes/Docker.md b/notes/Docker.md new file mode 100644 index 00000000..f230f762 --- /dev/null +++ b/notes/Docker.md @@ -0,0 +1,102 @@ + +* [一、解决的问题](#一解决的问题) +* [二、与虚拟机的比较](#二与虚拟机的比较) +* [三、优势](#三优势) +* [四、使用场景](#四使用场景) +* [五、镜像与容器](#五镜像与容器) + + + +

+ +# 一、解决的问题 + +由于不同的机器有不同的操作系统,以及不同的库和组件,在将一个应用部署到多台机器上需要进行大量的环境配置操作。 + +Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其他机器中。 + +参考资料: + +- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/) +- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html) + +# 二、与虚拟机的比较 + +虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。 + +

+ +

+ +## 启动速度 + +启动虚拟机需要启动虚拟机的操作系统,再启动相应的应用,这个过程会非常慢; + +而启动 Docker 相当于启动宿主操作系统上的一个进程。 + +## 占用资源 + +虚拟机是一个完整的操作系统,需要占用大量的磁盘空间、内存和 CPU,一台机器只能开启几十个的虚拟机。 + +而 Docker 只是一个进程,只需要将应用以及相应的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。 + +参考资料: + +- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php) + +# 三、优势 + +除了启动速度快以及占用资源少之外,Docker 具有以下优势: + +## 更容易迁移 + +Docker 可以提供一致性的运行环境,可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。 + +## 更容易维护 + +Docker 使用分层技术和镜像,使得应用可以更容易复用重复部分。复用程度越高,维护工作也越容易。 + +## 更容易扩展 + +可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像得到我们想要的镜像非常容易。 + +参考资料: + +- [为什么要使用 Docker?](https://yeasy.gitbooks.io/docker_practice/introduction/why.html) + +# 四、使用场景 + +## 持续集成 + +持续集成指的是频繁地将代码集成到主干上,这样能够更快地发现错误。 + +Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。 + +## 提供可伸缩的云服务 + +根据应用的负载情况,可以很容易地增加或者减少 Docker。 + +## 搭建微服务架构 + +Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。 + +参考资料: + +- [What is Docker](https://www.docker.com/what-docker) +- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html) + +# 五、镜像与容器 + +镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。 + +镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。 + +在构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。 + +

+ +参考资料: + +- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/) +- [理解 Docker(2):Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html) + diff --git a/notes/HTTP.md b/notes/HTTP.md index 915e19ee..243dc283 100644 --- a/notes/HTTP.md +++ b/notes/HTTP.md @@ -40,11 +40,11 @@ * [完整性保护](#完整性保护) * [HTTPs 的缺点](#https-的缺点) * [配置 HTTPs](#配置-https) -* [七、Web 攻击技术](#七web-攻击技术) - * [跨站脚本攻击](#跨站脚本攻击) - * [跨站请求伪造](#跨站请求伪造) - * [SQL 注入攻击](#sql-注入攻击) - * [拒绝服务攻击](#拒绝服务攻击) +* [七、HTTP/2.0](#七http20) + * [HTTP/1.x 缺陷](#http1x-缺陷) + * [二进制分帧层](#二进制分帧层) + * [服务端推送](#服务端推送) + * [首部压缩](#首部压缩) * [八、GET 和 POST 的区别](#八get-和-post-的区别) * [作用](#作用) * [参数](#参数) @@ -53,11 +53,6 @@ * [可缓存](#可缓存) * [XMLHttpRequest](#xmlhttprequest) * [九、HTTP/1.0 与 HTTP/1.1 的区别](#九http10-与-http11-的区别) -* [十、HTTP/2.0](#十http20) - * [HTTP/1.x 缺陷](#http1x-缺陷) - * [二进制分帧层](#二进制分帧层) - * [服务端推送](#服务端推送) - * [首部压缩](#首部压缩) * [参考资料](#参考资料) @@ -365,9 +360,9 @@ console.log(document.cookie); ### 5. Secure 和 HttpOnly -标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 +标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性,Secure 标记也无法提供确实的安全保障。 -标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。因为跨站脚本攻击 (XSS) 常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。 +标记为 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 @@ -387,15 +382,15 @@ Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径 除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 -Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在内存型数据库中,比如 Redis。 +Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在内存型数据库中,比如 Redis,效率会更高。 使用 Session 维护用户登录的过程如下: - 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; - 服务器验证该用户名和密码; -- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID; +- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID; - 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中; -- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之后的业务操作。 +- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。 应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 @@ -414,7 +409,7 @@ Session 可以存储在服务器上的文件、数据库或者内存中。也可 ### 1. 优点 - 缓解服务器压力; -- 降低客户端获取资源的延迟(缓存资源比服务器上的资源离客户端更近)。 +- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。 ### 2. 实现方法 @@ -465,7 +460,10 @@ max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中 Cache-Control: max-age=31536000 ``` -Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中,Cache-Control : max-age 指令会被忽略掉。 +Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。 + +- 在 HTTP/1.1 中,会优先处理 max-age 指令; +- 在 HTTP/1.0 中,max-age 指令会被忽略掉。 ```html Expires: Wed, 04 Jul 2012 08:26:05 GMT @@ -501,13 +499,16 @@ If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT ### 1. 短连接与长连接 -当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 +当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。 -从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close;而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。 +长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。 + +- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`; +- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`。 ### 2. 流水线 -默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到相应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 +默认情况下,HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。 流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。 @@ -517,17 +518,17 @@ If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT ### 1. 类型 -**(一)服务端驱动型内容协商** +**(一)服务端驱动型** 客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag,服务器根据这些字段返回特定的资源。 它存在以下问题: - 服务器很难知道客户端浏览器的全部信息; -- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术)。 +- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术); - 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 -**(二)代理驱动型协商** +**(二)代理驱动型** 服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。 @@ -543,9 +544,11 @@ Vary: Accept-Language ## 内容编码 -内容编码将实体主体进行压缩,从而减少传输的数据量。常用的内容编码有:gzip、compress、deflate、identity。 +内容编码将实体主体进行压缩,从而减少传输的数据量。 -浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级,服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应中,Vary 首部中至少要包含 Content-Encoding,这样的话,缓存服务器就可以对资源的不同展现形式进行缓存。 +常用的内容编码有:gzip、compress、deflate、identity。 + +浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。 ## 范围请求 @@ -627,10 +630,14 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名, - 网络访问控制 - 访问日志记录 -代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在;而反向代理一般位于内部网络中,用户察觉不到。 +代理服务器分为正向代理和反向代理两种: + +- 用户察觉得到正向代理的存在。

+- 而反向代理一般位于内部网络中,用户察觉不到。 +

### 2. 网关 @@ -639,7 +646,7 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名, ### 3. 隧道 -使用 SSL 等加密手段,为客户端和服务器之间建立一条安全的通信线路。 +使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。 # 六、HTTPs @@ -649,7 +656,7 @@ HTTP 有以下安全性问题: - 不验证通信方的身份,通信方的身份有可能遭遇伪装; - 无法证明报文的完整性,报文有可能遭篡改。 -HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信。也就是说 HTTPs 使用了隧道进行通信。 +HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPs 使用了隧道进行通信。 通过使用 SSL,HTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 @@ -716,213 +723,45 @@ HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认 [Nginx 配置 HTTPS 服务器](https://aotu.io/notes/2016/08/16/nginx-https/index.html) -# 七、Web 攻击技术 +# 七、HTTP/2.0 -## 跨站脚本攻击 +## HTTP/1.x 缺陷 -### 1. 概念 + HTTP/1.x 实现简单是以牺牲应用性能为代价的: -跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。 +- 客户端需要使用多个连接才能实现并发和缩短延迟; +- 不会压缩请求和响应首部,从而导致不必要的网络流量; +- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 -例如有一个论坛网站,攻击者可以在上面发布以下内容: +## 二进制分帧层 -```html - -``` +HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 -之后该内容可能会被渲染成以下形式: +

-```html -

-``` +在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。 -另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 +- 一个数据流都有一个唯一标识符和可选的优先级信息,用于承载双向信息。 +- 消息(Message)是与逻辑请求或响应消息对应的完整的一系列帧。 +- 帧(Fram)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。 -### 2. 危害 +

-- 窃取用户的 Cookie 值 -- 伪造虚假的输入表单骗取个人信息 -- 显示伪造的文章或者图片 +## 服务端推送 -### 3. 防范手段 +HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 -**(一)设置 Cookie 为 HttpOnly** +

-设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。 +## 首部压缩 -**(二)过滤特殊字符** +HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。 -例如将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。 +HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。 -**(三)富文本编辑器的处理** +不仅如此,HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。 -富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。 - -富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。 - -以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。 - -[XSS 过滤在线测试](http://jsxss.com/zh/try.html) - -```html -

XSS Demo

- -

-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),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。 - -XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。 - -假如一家银行用以执行转账操作的 URL 地址如下: - -``` -http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。 -``` - -那么,一个恶意攻击者可以在另一个网站上放置如下代码: - -``` -。 -``` - -如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 - -这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 - -透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 - -### 2. 防范手段 - -**(一)检查 Referer 首部字段** - -Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 XSRF 攻击。 - -这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 - -**(二)添加校验 Token** - -在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。 - -**(三)输入验证码** - -因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。 - -也可以要求用户输入验证码来进行校验。 - -## SQL 注入攻击 - -### 1. 概念 - -服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。 - -### 2. 攻击原理 - -例如一个网站登录验证的 SQL 查询代码为: - -```sql -strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');" -``` - -如果填入以下内容: - -```sql -userName = "1' OR '1'='1"; -passWord = "1' OR '1'='1"; -``` - -那么 SQL 查询字符串为: - -```sql -strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');" -``` - -此时无需验证通过就能执行以下查询: - -```sql -strSQL = "SELECT * FROM users;" -``` - -### 3. 防范手段 - -**(一)使用参数化查询** - -以下以 Java 中的 PreparedStatement 为例,它是预先编译的 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(); -``` - -**(二)单引号转换** - -将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。 - -## 拒绝服务攻击 - -拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 - -分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 - -> [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A) +

# 八、GET 和 POST 的区别 @@ -932,7 +771,9 @@ GET 用于获取资源,而 POST 用于传输实体主体。 ## 参数 -GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。 +GET 和 POST 的请求都能使用额外的参数,但是 GET 的参数是以查询字符串出现在 URL 中,而 POST 的参数存储在实体主体中。不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 + +因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码。例如 `中文` 会转换为 `%E4%B8%AD%E6%96%87`,而空格会转换为 `%20`。POST 参考支持标准字符集。 ``` GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1 @@ -944,10 +785,6 @@ Host: w3schools.com name1=value1&name2=value2 ``` -不能因为 POST 参数存储在实体主体中就认为它的安全性更高,因为照样可以通过一些抓包工具(Fiddler)查看。 - -因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。 - ## 安全 安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 @@ -960,9 +797,13 @@ GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实 ## 幂等性 -幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。 +幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。 -GET /pageX HTTP/1.1 是幂等的。连续调用多次,客户端接收到的结果都是一样的: +所有的安全方法也都是幂等的。 + +在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。 + +GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的: ``` GET /pageX HTTP/1.1 @@ -971,7 +812,7 @@ GET /pageX HTTP/1.1 GET /pageX HTTP/1.1 ``` -POST /add_row HTTP/1.1 不是幂等的。如果调用多次,就会增加多行记录: +POST /add_row HTTP/1.1 不是幂等的,如果调用多次,就会增加多行记录: ``` POST /add_row HTTP/1.1 -> Adds a 1nd row @@ -1001,7 +842,8 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 > XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 -在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。而 GET 方法 Header 和 Data 会一起发送。 +- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。 +- 而 GET 方法 Header 和 Data 会一起发送。 # 九、HTTP/1.0 与 HTTP/1.1 的区别 @@ -1021,37 +863,6 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404 - HTTP/1.1 新增缓存处理指令 max-age -# 十、HTTP/2.0 - -## HTTP/1.x 缺陷 - - HTTP/1.x 实现简单是以牺牲应用性能为代价的: - -- 客户端需要使用多个连接才能实现并发和缩短延迟; -- 不会压缩请求和响应首部,从而导致不必要的网络流量; -- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。 - -## 二进制分帧层 - -HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。 - -

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

- -## 服务端推送 - -HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。 - -

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

# 参考资料 @@ -1075,10 +886,6 @@ HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。HTTP/2.0 - [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) -- [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A) - [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn) - [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest) - [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/) diff --git a/notes/Java IO.md b/notes/Java IO.md index 08dc80f4..1e3aedcd 100644 --- a/notes/Java IO.md +++ b/notes/Java IO.md @@ -40,8 +40,7 @@ File 类可以用于表示文件和目录的信息,但是它不表示文件的 递归地输出一个目录下所有文件: ```java -public static void listAllFiles(File dir) -{ +public static void listAllFiles(File dir) { if (dir == null || !dir.exists()) { return; } @@ -60,17 +59,19 @@ public static void listAllFiles(File dir) 使用字节流操作进行文件复制: ```java -public static void copyFile(String src, String dist) throws IOException -{ +public static void copyFile(String src, String dist) throws IOException { + FileInputStream in = new FileInputStream(src); FileOutputStream out = new FileOutputStream(dist); byte[] buffer = new byte[20 * 1024]; + // read() 最多读取 buffer.length 个字节 // 返回的是实际读取的个数 // 返回 -1 的时候表示读到 eof,即文件尾 while (in.read(buffer, 0, buffer.length) != -1) { out.write(buffer); } + in.close(); out.close(); } @@ -78,7 +79,11 @@ public static void copyFile(String src, String dist) throws IOException

-Java I/O 使用了装饰者模式来实现。以 InputStream 为例,InputStream 是抽象组件,FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 +Java I/O 使用了装饰者模式来实现。以 InputStream 为例, + +- InputStream 是抽象组件; +- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作; +- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 @@ -99,8 +104,7 @@ DataInputStream 装饰者提供了对更多数据类型进行输入的操作, 逐行输出文本文件的内容: ```java -public static void readFileContent(String filePath) throws IOException -{ +public static void readFileContent(String filePath) throws IOException { FileReader fileReader = new FileReader(filePath); BufferedReader bufferedReader = new BufferedReader(fileReader); String line; @@ -126,7 +130,7 @@ UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF- Java 使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 -String 可以看成一个字符序列,可以指定一个编码方式将它转换为字节序列,也可以指定一个编码方式将一个字节序列转换为 String。 +String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。 ```java String str1 = "中文"; @@ -151,8 +155,7 @@ byte[] bytes = str1.getBytes(); 序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。 ```java -public static void main(String[] args) throws IOException, ClassNotFoundException -{ +public static void main(String[] args) throws IOException, ClassNotFoundException { A a1 = new A(123, "abc"); String objectFile = "file/a1"; ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile)); @@ -165,20 +168,17 @@ public static void main(String[] args) throws IOException, ClassNotFoundExceptio System.out.println(a2); } -private static class A implements Serializable -{ +private static class A implements Serializable { private int x; private String y; - A(int x, String y) - { + A(int x, String y) { this.x = x; this.y = y; } @Override - public String toString() - { + public String toString() { return "x = " + x + " " + "y = " + y; } } @@ -188,7 +188,7 @@ private static class A implements Serializable transient 关键字可以使一些属性不会被序列化。 -**ArrayList 序列化和反序列化的实现** :ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 +ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 ```java private transient Object[] elementData; @@ -205,7 +205,7 @@ Java 中的网络支持: ## InetAddress -没有公有构造函数,只能通过静态方法来创建实例。 +没有公有的构造函数,只能通过静态方法来创建实例。 ```java InetAddress.getByName(String host); @@ -217,19 +217,24 @@ InetAddress.getByAddress(byte[] address); 可以直接从 URL 中读取字节流数据。 ```java -public static void main(String[] args) throws IOException -{ +public static void main(String[] args) throws IOException { + URL url = new URL("http://www.baidu.com"); - // 字节流 + + /* 字节流 */ InputStream is = url.openStream(); - // 字符流 + + /* 字符流 */ InputStreamReader isr = new InputStreamReader(is, "utf-8"); + + /* 提供缓存功能 */ BufferedReader br = new BufferedReader(isr); - String line = br.readLine(); - while (line != null) { + + String line; + while ((line = br.readLine()) != null) { System.out.println(line); - line = br.readLine(); } + br.close(); } ``` @@ -253,7 +258,7 @@ public static void main(String[] args) throws IOException - [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。 +新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。 ## 流与块 @@ -271,7 +276,7 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 -通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 +通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 通道包括以下类型: @@ -329,21 +334,41 @@ I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重 以下展示了使用 NIO 快速复制文件的实例: ```java -public static void fastCopy(String src, String dist) throws IOException -{ - FileInputStream fin = new FileInputStream(src); /* 获取源文件的输入字节流 */ - FileChannel fcin = fin.getChannel(); /* 获取输入字节流的文件通道 */ - FileOutputStream fout = new FileOutputStream(dist); /* 获取目标文件的输出字节流 */ - FileChannel fcout = fout.getChannel(); /* 获取输出字节流的通道 */ - ByteBuffer buffer = ByteBuffer.allocateDirect(1024); /* 为缓冲区分配 1024 个字节 */ +public static void fastCopy(String src, String dist) throws IOException { + + /* 获得源文件的输入字节流 */ + FileInputStream fin = new FileInputStream(src); + + /* 获取输入字节流的文件通道 */ + FileChannel fcin = fin.getChannel(); + + /* 获取目标文件的输出字节流 */ + FileOutputStream fout = new FileOutputStream(dist); + + /* 获取输出字节流的通道 */ + FileChannel fcout = fout.getChannel(); + + /* 为缓冲区分配 1024 个字节 */ + ByteBuffer buffer = ByteBuffer.allocateDirect(1024); + while (true) { - int r = fcin.read(buffer); /* 从输入通道中读取数据到缓冲区中 */ - if (r == -1) { /* read() 返回 -1 表示 EOF */ + + /* 从输入通道中读取数据到缓冲区中 */ + int r = fcin.read(buffer); + + /* read() 返回 -1 表示 EOF */ + if (r == -1) { break; } - buffer.flip(); /* 切换读写 */ - fcout.write(buffer); /* 把缓冲区的内容写入输出文件中 */ - buffer.clear(); /* 清空缓冲区 */ + + /* 切换读写 */ + buffer.flip(); + + /* 把缓冲区的内容写入输出文件中 */ + fcout.write(buffer); + + /* 清空缓冲区 */ + buffer.clear(); } } ``` @@ -448,10 +473,10 @@ while (true) { ## 套接字 NIO 实例 ```java -public class NIOServer -{ - public static void main(String[] args) throws IOException - { +public class NIOServer { + + public static void main(String[] args) throws IOException { + Selector selector = Selector.open(); ServerSocketChannel ssChannel = ServerSocketChannel.open(); @@ -463,32 +488,45 @@ public class NIOServer 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); StringBuilder data = new StringBuilder(); + while (true) { + buffer.clear(); int n = sChannel.read(buffer); if (n == -1) { @@ -509,10 +547,9 @@ public class NIOServer ``` ```java -public class NIOClient -{ - public static void main(String[] args) throws IOException - { +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"; @@ -526,9 +563,9 @@ public class NIOClient 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 -向内存映射文件写入可能是危险的,仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 +向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。 -下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 +下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。 ```java MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); diff --git a/notes/Java 基础.md b/notes/Java 基础.md index 907eea00..bcb94acc 100644 --- a/notes/Java 基础.md +++ b/notes/Java 基础.md @@ -4,7 +4,7 @@ * [缓存池](#缓存池) * [二、String](#二string) * [概览](#概览) - * [String 不可变的好处](#string-不可变的好处) + * [不可变的好处](#不可变的好处) * [String, StringBuffer and StringBuilder](#string,-stringbuffer-and-stringbuilder) * [String.intern()](#stringintern) * [三、运算](#三运算) @@ -153,7 +153,7 @@ public final class String private final char value[]; ``` -## String 不可变的好处 +## 不可变的好处 **1. 可以缓存 hash 值** @@ -212,7 +212,7 @@ String s5 = "bbb"; System.out.println(s4 == s5); // true ``` -在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被放在堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 +在 Java 7 之前,字符串常量池被放在运行时常量池中,它属于永久代。而在 Java 7,字符串常量池被移到 Native Method 中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。 - [StackOverflow : What is String interning?](https://stackoverflow.com/questions/10578984/what-is-string-interning) - [深入解析 String#intern](https://tech.meituan.com/in_depth_understanding_string_intern.html) @@ -223,7 +223,7 @@ System.out.println(s4 == s5); // true Java 的参数是以值传递的形式传入方法中,而不是引用传递。 -Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。 +以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。 ```java public class Dog { @@ -549,7 +549,7 @@ SuperExtendExample.func() ## 重写与重载 -- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法; +- 重写(Override)存在于继承体系中,指子类实现了一个与父类在方法声明上完全相同的一个方法,子类的返回值类型要等于或者小于父类的返回值; - 重载(Overload)存在于同一个类中,指一个方法与已经存在的方法名称上相同,但是参数类型、个数、顺序至少有一个不同。应该注意的是,返回值不同,其它都相同不算是重载。 @@ -583,19 +583,7 @@ protected void finalize() throws Throwable {} ## equals() -**1. equals() 与 == 的区别** - -- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 -- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价。 - -```java -Integer x = new Integer(1); -Integer y = new Integer(1); -System.out.println(x.equals(y)); // true -System.out.println(x == y); // false -``` - -**2. 等价关系** +**1. 等价关系** (一)自反性 @@ -632,6 +620,18 @@ x.equals(y) == x.equals(y); // true x.equals(null); // false; ``` +**2. equals() 与 ==** + +- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。 +- 对于引用类型,== 判断两个实例是否引用同一个对象,而 equals() 判断引用的对象是否等价,根据引用对象 equals() 方法的具体实现来进行比较。 + +```java +Integer x = new Integer(1); +Integer y = new Integer(1); +System.out.println(x.equals(y)); // true +System.out.println(x == y); // false +``` + **3. 实现** - 检查是否为同一个对象的引用,如果是直接返回 true; @@ -1005,7 +1005,7 @@ public class A { **4. 静态内部类** -非静态内部类依赖于需要外部类的实例,而静态内部类不需要。 +非静态内部类依赖于外部类的实例,而静态内部类不需要。 ```java public class OuterClass { diff --git a/notes/Java 容器.md b/notes/Java 容器.md index 9e6e2ac3..bf1921fc 100644 --- a/notes/Java 容器.md +++ b/notes/Java 容器.md @@ -8,9 +8,13 @@ * [三、源码分析](#三源码分析) * [ArrayList](#arraylist) * [Vector](#vector) + * [CopyOnWriteArrayList](#copyonwritearraylist) * [LinkedList](#linkedlist) * [HashMap](#hashmap) * [ConcurrentHashMap](#concurrenthashmap) + * [LinkedHashMap](#linkedhashmap) + * [WeekHashMap](#weekhashmap) +* [附录](#附录) * [参考资料](#参考资料) @@ -21,39 +25,39 @@ ## Collection -

+

### 1. Set -- HashSet:基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的; +- HashSet:基于哈希表实现,支持快速查找。但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。 -- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN); +- TreeSet:基于红黑树实现,支持有序性操作,但是查找效率不如 HashSet,HashSet 查找时间复杂度为 O(1),TreeSet 则为 O(logN)。 -- LinkedHashSet:具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。 +- LinkedHashSet:具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。 ### 2. List -- ArrayList:基于动态数组实现,支持随机访问; +- ArrayList:基于动态数组实现,支持随机访问。 -- Vector:和 ArrayList 类似,但它是线程安全的; +- Vector:和 ArrayList 类似,但它是线程安全的。 - LinkedList:基于双向链表实现,只能顺序访问,但是可以快速地在链表中间插入和删除元素。不仅如此,LinkedList 还可以用作栈、队列和双向队列。 ### 3. Queue -- LinkedList:可以用它来支持双向队列; +- LinkedList:可以用它来实现双向队列。 - PriorityQueue:基于堆结构实现,可以用它来实现优先队列。 ## Map -

+

-- HashMap:基于哈希实现; +- HashMap:基于哈希表实现; - HashTable:和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 -- LinkedHashMap:使用链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 +- LinkedHashMap:使用双向链表来维护元素的顺序,顺序为插入顺序或者最近最少使用(LRU)顺序。 - TreeMap:基于红黑树实现。 @@ -61,7 +65,7 @@ ## 迭代器模式 -

+

Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 @@ -85,14 +89,14 @@ java.util.Arrays#asList() 可以把数组类型转换为 List 类型。 public static List asList(T... a) ``` -如果要将数组类型转换为 List 类型,应该注意的是 asList() 的参数为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 +应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 ```java Integer[] arr = {1, 2, 3}; List list = Arrays.asList(arr); ``` -也可以使用以下方式生成 List。 +也可以使用以下方式调用 asList(): ```java List list = Arrays.asList(1,2,3); @@ -108,7 +112,7 @@ List list = Arrays.asList(1,2,3); ### 1. 概览 -实现了 RandomAccess 接口,因此支持随机访问,这是理所当然的,因为 ArrayList 是基于数组实现的。 +实现了 RandomAccess 接口,因此支持随机访问。这是理所当然的,因为 ArrayList 是基于数组实现的。 ```java public class ArrayList extends AbstractList @@ -123,7 +127,9 @@ private static final int DEFAULT_CAPACITY = 10; ### 2. 序列化 -基于数组实现,保存元素的数组使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 +ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。 + +保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。 ```java transient Object[] elementData; // non-private to simplify nested class access @@ -171,7 +177,7 @@ private void grow(int minCapacity) { ### 4. 删除元素 -需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上。 +需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的。 ```java public E remove(int index) { @@ -235,12 +241,12 @@ public synchronized E get(int index) { } ``` -### 2. ArrayList 与 Vector +### 2. 与 ArrayList 的区别 - Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector,因为同步操作完全可以由程序员自己来控制; - Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。 -### 3. Vector 替代方案 +### 3. 替代方案 为了获得线程安全的 ArrayList,可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList。 @@ -255,7 +261,15 @@ List synList = Collections.synchronizedList(list); List list = new CopyOnWriteArrayList<>(); ``` -CopyOnWriteArrayList 是一种 CopyOnWrite 容器,从以下源码看出:读取元素是从原数组读取;添加元素是在复制的新数组上。读写分离,因而可以在并发条件下进行不加锁的读取,读取效率高,适用于读操作远大于写操作的场景。 +## CopyOnWriteArrayList + +### 读写分离 + +写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。 + +写操作需要加锁,防止同时并发写入时导致的写入数据丢失。 + +写操作结束之后需要把原始数组指向新的复制数组。 ```java public boolean add(E e) { @@ -264,7 +278,7 @@ public boolean add(E e) { try { Object[] elements = getArray(); int len = elements.length; - Object[] newElements = Arrays.copyOf(elements, len + 1); + Object[] newElements = Arrays.copyOf(elements, len + 1); newElements[len] = e; setArray(newElements); return true; @@ -276,18 +290,31 @@ public boolean add(E e) { final void setArray(Object[] a) { array = a; } +``` +```java @SuppressWarnings("unchecked") private E get(Object[] a, int index) { return (E) a[index]; } ``` +### 适用场景 + +CopyOnWriteArrayList 在写操作的同时允许读操作,大大提高了读操作的性能,因此很适合读多写少的应用场景。 + +但是 CopyOnWriteArrayList 有其缺陷: + +- 内存占用:在写操作时需要复制一个新的数组,使得内存占用为原来的两倍左右; +- 数据不一致:读操作不能读取实时性的数据,因为部分写操作的数据还未同步到读数组中。 + +所以 CopyOnWriteArrayList 不适合内存敏感以及对实时性要求很高的场景。 + ## LinkedList ### 1. 概览 -基于双向链表实现,内部使用 Node 来存储链表节点信息。 +基于双向链表实现,使用 Node 存储链表节点信息。 ```java private static class Node { @@ -297,14 +324,14 @@ private static class Node { } ``` -每个链表存储了 Head 和 Tail 指针: +每个链表存储了 first 和 last 指针: ```java transient Node first; transient Node last; ``` -

+

### 2. ArrayList 与 LinkedList @@ -450,7 +477,7 @@ public V put(K key, V value) { } ``` -HashMap 允许插入键为 null 的键值对。因为无法调用 null 的 hashCode(),也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 +HashMap 允许插入键为 null 的键值对。但是因为无法调用 null 的 hashCode() 方法,也就无法确定该键值对的桶下标,只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 ```java private V putForNullKey(V value) { @@ -577,10 +604,10 @@ static int indexFor(int h, int length) { | 参数 | 含义 | | :--: | :-- | -| capacity | table 的容量大小,默认为 16,需要注意的是 capacity 必须保证为 2 的 n 次方。| +| capacity | table 的容量大小,默认为 16。需要注意的是 capacity 必须保证为 2 的 n 次方。| | size | table 的实际使用量。 | | threshold | size 的临界值,size 必须小于 threshold,如果大于等于,就必须进行扩容操作。 | -| load_factor | 装载因子,table 能够使用的比例,threshold = capacity * load_factor。| +| loadFactor | 装载因子,table 能够使用的比例,threshold = capacity * loadFactor。| ```java static final int DEFAULT_INITIAL_CAPACITY = 16; @@ -650,14 +677,14 @@ void transfer(Entry[] newTable) { 在进行扩容时,需要把键值对重新放到对应的桶上。HashMap 使用了一个特殊的机制,可以降低重新计算桶下标的操作。 -假设原数组长度 capacity 为 8,扩容之后 new capacity 为 16: +假设原数组长度 capacity 为 16,扩容之后 new capacity 为 32: ```html capacity : 00010000 new capacity : 00100000 ``` -对于一个 Key,它的哈希值如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 + 8。 +对于一个 Key,它的哈希值如果在第 6 位上为 0,那么取模得到的结果和之前一样;如果为 1,那么得到的结果为原来的结果 +16。 ### 7. 扩容-计算数组容量 @@ -825,15 +852,287 @@ public int size() { } ``` - ### 3. JDK 1.8 的改动 -JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发程度与 Segment 数量相等。 +JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment,它继承自重入锁 ReentrantLock,并发度与 Segment 数量相等。 JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。 并且 JDK 1.8 的实现也在链表过长时会转换为红黑树。 +## LinkedHashMap + +### 存储结构 + +继承自 HashMap,因此具有和 HashMap 一样的快速查找特性。 + +```java +public class LinkedHashMap extends HashMap implements Map +``` + +内存维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。 + +```java +/** + * The head (eldest) of the doubly linked list. + */ +transient LinkedHashMap.Entry head; + +/** + * The tail (youngest) of the doubly linked list. + */ +transient LinkedHashMap.Entry tail; +``` + +accessOrder 决定了顺序,默认为 false,此时使用的是插入顺序。 + +```java +final boolean accessOrder; +``` + +LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。 + +```java +void afterNodeAccess(Node p) { } +void afterNodeInsertion(boolean evict) { } +``` + +### afterNodeAccess() + +当一个节点被访问时,如果 accessOrder 为 true,则会将 该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。 + +```java +void afterNodeAccess(Node e) { // move node to last + LinkedHashMap.Entry last; + if (accessOrder && (last = tail) != e) { + LinkedHashMap.Entry p = + (LinkedHashMap.Entry)e, b = p.before, a = p.after; + p.after = null; + if (b == null) + head = a; + else + b.after = a; + if (a != null) + a.before = b; + else + last = b; + if (last == null) + head = p; + else { + p.before = last; + last.after = p; + } + tail = p; + ++modCount; + } +} +``` + +### afterNodeInsertion() + +在 put 等操作之后执行,当 removeEldestEntry() 方法返回 ture 时会移除最晚的节点,也就是链表首部节点 first。 + +evict 只有在构建 Map 的时候才为 false,在这里为 true。 + +```java +void afterNodeInsertion(boolean evict) { // possibly remove eldest + LinkedHashMap.Entry first; + if (evict && (first = head) != null && removeEldestEntry(first)) { + K key = first.key; + removeNode(hash(key), key, null, false, true); + } +} +``` + +removeEldestEntry() 默认为 false,如果需要让它为 true,需要继承 LinkedHashMap 并且覆盖这个方法的实现,这在实现 LRU 的缓存中特别有用,通过移除最近最久未使用的节点,从而保证缓存空间足够,并且缓存的数据都是热点数据。 + +```java +protected boolean removeEldestEntry(Map.Entry eldest) { + return false; + } +``` + +### LRU 缓存 + +以下是使用 LinkedHashMap 实现的一个 LRU 缓存: + +- 设定最大缓存空间 MAX_ENTRIES 为 3; +- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true,开启 LUR 顺序; +- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。 + +```java +class LRUCache extends LinkedHashMap { + private static final int MAX_ENTRIES = 3; + + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_ENTRIES; + } + + LRUCache() { + super(MAX_ENTRIES, 0.75f, true); + } +} +``` + +```java +public static void main(String[] args) { + LRUCache cache = new LRUCache<>(); + cache.put(1, "a"); + cache.put(2, "b"); + cache.put(3, "c"); + cache.get(1); + cache.put(4, "d"); + System.out.println(cache.keySet()); +} +``` + +```html +[3, 1, 4] +``` + +## WeekHashMap + +### 存储结构 + +WeakHashMap 的 Entry 继承自 WeakReference,被 WeakReference 关联的对象在下一次垃圾回收时会被回收。 + +WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。 + +```java +private static class Entry extends WeakReference implements Map.Entry +``` + +### ConcurrentCache + +Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。 + +ConcurrentCache 采取的是分代缓存: + +- 经常使用的对象放入 eden 中,eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园); +- 不常用的对象放入 longterm,longterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。 +- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,保证频繁被访问的节点不容易被回收。 +- 当调用 put() 方法时,如果缓存当前容量大小超过了 size,那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。 + +```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); + } +} +``` + +# 附录 + +Collection 绘图源码: + +``` +@startuml + +interface Collection +interface Set +interface List +interface Queue +interface SortSet + +class HashSet +class LinkedHashSet +class TreeSet +class ArrayList +class Vector +class LinkedList +class PriorityQueue + + +Collection <|-- Set +Collection <|-- List +Collection <|-- Queue +Set <|-- SortSet + +Set <|.. HashSet +Set <|.. LinkedHashSet +SortSet <|.. TreeSet +List <|.. ArrayList +List <|.. Vector +List <|.. LinkeList +Queue <|.. LinkedList +Queue <|.. PriorityQueue + +@enduml +``` + +Map 绘图源码 + +``` +@startuml + +interface Map +interface SortMap + +class HashTable +class LinkedHashMap +class HashMap +class TreeMap + +Map <|.. HashTable +Map <|.. LinkedHashMap +Map <|.. HashMap +Map <|-- SortMap +SortMap <|.. TreeMap + +@enduml +``` + +迭代器类图 + +``` +@startuml + +interface Iterable +interface Collection +interface List +interface Set +interface Queue +interface Iterator +interface ListIterator + +Iterable <|-- Collection +Collection <|.. List +Collection <|.. Set +Collection <|-- Queue +Iterator <-- Iterable +Iterator <|.. ListIterator +ListIterator <-- List + +@enduml +``` + # 参考资料 - Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002. diff --git a/notes/Java 并发.md b/notes/Java 并发.md index ab3528f2..89ab7cac 100644 --- a/notes/Java 并发.md +++ b/notes/Java 并发.md @@ -1419,7 +1419,9 @@ public final int incrementAndGet() { } ``` -以下代码是 getAndAddInt() 源码,var1 指示内存地址,var2 指示旧值,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2==var5,那么就更新内存地址为 var1 的变量为 var5+var4。可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。 +以下代码是 getAndAddInt() 源码,var1 指示对象内存地址,var2 指示该字段相对对象内存地址的偏移,var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值 ==var5,那么就更新内存地址为 var1+var2 的变量为 var5+var4。 + +可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。 ```java public final int getAndAddInt(Object var1, long var2, int var4) { diff --git a/notes/Java 虚拟机.md b/notes/Java 虚拟机.md index 552a5e94..136ca5ad 100644 --- a/notes/Java 虚拟机.md +++ b/notes/Java 虚拟机.md @@ -1,7 +1,7 @@ * [一、运行时数据区域](#一运行时数据区域) * [程序计数器](#程序计数器) - * [虚拟机栈](#虚拟机栈) + * [Java 虚拟机栈](#java-虚拟机栈) * [本地方法栈](#本地方法栈) * [堆](#堆) * [方法区](#方法区) @@ -23,19 +23,19 @@ # 一、运行时数据区域 -

+

## 程序计数器 记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。 -## 虚拟机栈 +## Java 虚拟机栈 -每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 +每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息,从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 -

+

-可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小: +可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小: ```java java -Xss=512M HackTheJava @@ -52,31 +52,28 @@ java -Xss=512M HackTheJava 与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。 -

+本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的, 并且被编译为基于本机硬件和操作系统的程序。 + +

## 堆 -所有对象实例都在这里分配内存。 +所有对象实例都在这里分配内存,是垃圾收集的主要区域("GC 堆")。 -是垃圾收集的主要区域("GC 堆")。现代的垃圾收集器基本都是采用分代收集算法,主要思想是针对不同的对象采取不同的垃圾回收算法。虚拟机把 Java 堆分成以下三块: +现代的垃圾收集器基本都是采用分代收集算法,针对不同的对象采取不同的垃圾回收算法,可以将堆分成两块: - 新生代(Young Generation) - 老年代(Old Generation) -- 永久代(Permanent Generation) -当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。 - -新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间: +新生代可以继续划分成以下三个空间: - Eden(伊甸园) - From Survivor(幸存者) - To Survivor -

+堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 -Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。 - -可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 +可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 ```java java -Xms=1M -Xmx=2M HackTheJava @@ -86,11 +83,13 @@ java -Xms=1M -Xmx=2M HackTheJava 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 -和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 +和堆一样不需要连续的内存,并且可以动态扩展。 + +动态扩展失败一样会抛出 OutOfMemoryError 异常。 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 -JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,JDK 1.8 之后,取消了永久代,用 metaspace(元数据)区替代。 +JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是从 JDK 1.7 开始,已经把原本放在永久代的字符串常量池移到 Native Method 中。 ## 运行时常量池 @@ -102,11 +101,15 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用 ## 直接内存 -在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 +在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。 + +这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 # 二、垃圾收集 -程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。 +垃圾回收主要是针对堆和方法区进行。 + +程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。 ## 判断一个对象是否存活 @@ -116,6 +119,8 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用 两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 +正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。 + ```java public class ReferenceCountingGC { public Object instance = null; @@ -129,8 +134,6 @@ public class ReferenceCountingGC { } ``` -正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。 - ### 2. 可达性分析算法 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。 @@ -152,7 +155,7 @@ Java 具有四种强度不同的引用类型。 **(一)强引用** -被强引用关联的对象不会被垃圾收集器回收。 +被强引用关联的对象不会被回收。 使用 new 一个新对象的方式来创建强引用。 @@ -162,7 +165,7 @@ Object obj = new Object(); **(二)软引用** -被软引用关联的对象,只有在内存不够的情况下才会被回收。 +被软引用关联的对象只有在内存不够的情况下才会被回收。 使用 SoftReference 类来创建软引用。 @@ -174,7 +177,7 @@ obj = null; // 使对象只被软引用关联 **(三)弱引用** -被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集。 +被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾收集。 使用 WeakReference 类来实现弱引用。 @@ -184,54 +187,11 @@ 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); - } -} -``` - **(四)虚引用** 又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。 -为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。 +为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。 使用 PhantomReference 来实现虚引用。 @@ -243,20 +203,20 @@ obj = null; ### 4. 方法区的回收 -因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。 +因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。 主要是对常量池的回收和对类的卸载。 +在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 + 类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载: -- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。 +- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。 - 加载该类的 ClassLoader 已经被回收。 -- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 +- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。 可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。 -在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。 - ### 5. finalize() finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。 @@ -290,16 +250,18 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 主要不足是只使用了内存的一半。 -现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。 +现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。 + +HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。 ### 4. 分代收集 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 -一般将 Java 堆分为新生代和老年代。 +一般将堆分为新生代和老年代。 - 新生代使用:复制算法 -- 老年代使用:标记 - 清理 或者 标记 - 整理 算法 +- 老年代使用:标记 - 清除 或者 标记 - 整理 算法 ## 垃圾收集器 @@ -307,8 +269,8 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。 以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 -- 单线程与并行(多线程):单线程指的是垃圾收集器只使用一个线程进行收集,而并行使用多个线程。 -- 串行与并发:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并发指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 +- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程; +- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并形指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。 ### 1. Serial 收集器 @@ -334,15 +296,15 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 ### 3. Parallel Scavenge 收集器 -与 ParNew 一样是并行的多线程收集器。 +与 ParNew 一样是多线程收集器。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 -提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间 -XX:MaxGCPauseMillis 参数以及直接设置吞吐量大小的 -XX:GCTimeRatio 参数(值为大于 0 且小于 100 的整数)。缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 +缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。 -还提供了一个参数 -XX:+UseAdaptiveSizePolicy,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 GC 自适应的调节策略(GC Ergonomics)。 +可以通过一个开关参数打卡 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为 。 ### 4. Serial Old 收集器 @@ -367,8 +329,6 @@ Serial 翻译为串行,也就是说它以串行的方式执行。 CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 -特点:并发收集、低停顿。 - 分为以下四个流程: - 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 @@ -388,7 +348,7 @@ CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。 G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 -Java 堆被分为新生代、老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。 +堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。

@@ -416,18 +376,6 @@ G1 把堆划分成多个大小相等的独立区域(Region),新生代和 更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html) -### 8. 比较 - -| 收集器 | 单线程/并行 | 串行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 | -| :---: | :---: | :---: | :---: | :---: | :---: | :---: | -| **Serial** | 单线程 | 串行 | 新生代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | -| **Serial Old** | 单线程 | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 | -| **ParNew** | 并行 |串行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | -| **Parallel Scavenge** | 并行 | 串行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **Parallel Old** | 并行 | 串行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **CMS** | 并行 | 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | -| **G1** | 并行 | 并发 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS | - ## 内存分配与回收策略 ### 1. Minor GC 和 Full GC @@ -670,7 +618,7 @@ public static void main(String[] args) { 从 Java 开发人员的角度看,类加载器可以划分得更细致一些: -- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 +- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 - 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 @@ -794,7 +742,9 @@ public class FileSystemClassLoader extends ClassLoader { # 参考资料 - 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011. +- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4) - [Jvm memory](https://www.slideshare.net/benewu/jvm-memory) +- [JNI Part1: Java Native Interface Introduction and “Hello World” application](http://electrofriends.com/articles/jni/jni-part1-java-native-interface/) - [Memory Architecture Of JVM(Runtime Data Areas)](https://hackthejava.wordpress.com/2015/01/09/memory-architecture-by-jvmruntime-data-areas/) - [JVM Run-Time Data Areas](https://www.programcreek.com/2013/04/jvm-run-time-data-areas/) - [Android on x86: Java Native Interface and the Android Native Development Kit](http://www.drdobbs.com/architecture-and-design/android-on-x86-java-native-interface-and/240166271) diff --git a/notes/Leetcode 题解.md b/notes/Leetcode 题解.md index 293f4328..2813ce5d 100644 --- a/notes/Leetcode 题解.md +++ b/notes/Leetcode 题解.md @@ -2371,7 +2371,9 @@ public List diffWaysToCompute(String input) { 题目描述:有 N 阶楼梯,每次可以上一阶或者两阶,求有多少种上楼梯的方法。 -定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。 +定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始),dp[i] 表示走到第 i 个楼梯的方法数目。 + +第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。

@@ -2400,7 +2402,9 @@ public int climbStairs(int n) { 题目描述:抢劫一排住户,但是不能抢邻近的住户,求最大抢劫量。 -定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 或者 i - 3 的住户,所以 +定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。 + +由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 或者 i - 3 的住户,所以

diff --git a/notes/Linux.md b/notes/Linux.md index 27094e5c..14d3b317 100644 --- a/notes/Linux.md +++ b/notes/Linux.md @@ -11,7 +11,6 @@ * [GNU](#gnu) * [开源协议](#开源协议) * [二、磁盘](#二磁盘) - * [HDD](#hdd) * [磁盘接口](#磁盘接口) * [磁盘的文件名](#磁盘的文件名) * [三、分区](#三分区) @@ -185,21 +184,6 @@ GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操 # 二、磁盘 -## HDD - -Hard Disk Drives(HDD) 俗称硬盘,具有以下结构: - -- 盘面(Platter):一个硬盘有多个盘面; -- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道; -- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小; -- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写); -- 制动手臂(Actuator arm):用于在磁道之间移动磁头; -- 主轴(Spindle):使整个盘面转动。 - -

- -[Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) - ## 磁盘接口 ### 1. IDE @@ -291,8 +275,6 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 ## 组成 -

- 最主要的几个组成部分如下: - inode:一个文件占用一个 inode,记录文件的属性,同时记录此文件的内容所在的 block 编号; @@ -303,6 +285,9 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。 - superblock:记录文件系统的整体信息,包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; - block bitmap:记录 block 是否被使用的位域; +

+ + ## 文件读取 对于 Ext2 文件系统,当要读取一个文件的内容时,先在 inode 中去查找文件内容所在的所有 block,然后把所有 block 的内容读出来。 @@ -352,7 +337,9 @@ inode 中记录了文件内容所在的 block 编号,但是每个 block 非常 ## 目录 -建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 +建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。 + +可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 ## 日志 @@ -871,7 +858,9 @@ $ ls -al /etc | less ## 提取指令 -cut 对数据进行切分,取出想要的部分。切分过程一行一行地进行。 +cut 对数据进行切分,取出想要的部分。 + +切分过程一行一行地进行。 ```html $ cut @@ -891,7 +880,7 @@ root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16) $ last | cut -d ' ' -f 1 ``` -示例 2:将 export 输出的讯息,取出第 12 字符以后的所有字符串。 +示例 2:将 export 输出的信息,取出第 12 字符以后的所有字符串。 ```html $ export @@ -901,12 +890,12 @@ declare -x HOME="/home/dmtsai" declare -x HOSTNAME="study.centos.vbird" .....(其他省略)..... -$ export | cut -c 12 +$ export | cut -c 12- ``` ## 排序指令 -**sort** 进行排序。 +**sort** 用于排序。 ```html $ sort [-fbMnrtuk] [file or stdin] @@ -1023,10 +1012,10 @@ g/re/p(globally search a regular expression and print),使用正则表示式 ```html $ grep [-acinv] [--color=auto] 搜寻字符串 filename --c : 计算找到个数 +-c : 统计个数 -i : 忽略大小写 -n : 输出行号 --v : 反向选择,亦即显示出没有 搜寻字符串 内容的那一行 +-v : 反向选择,也就是显示出没有 搜寻字符串 内容的那一行 --color=auto :找到的关键字加颜色显示 ``` @@ -1066,7 +1055,7 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$n,n 为字段号,从 1 开始,\$0 表示一整行。 -示例 1:取出登录用户的用户名和 ip +示例:取出登录用户的用户名和 IP ```html $ last -n 5 @@ -1075,8 +1064,10 @@ dmtsai pts/0 192.168.1.100 Thu Jul 9 23:36 - 02:58 (03:22) dmtsai pts/0 192.168.1.100 Thu Jul 9 17:23 - 23:36 (06:12) dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14) dmtsai tty1 Fri May 29 11:55 - 12:11 (00:15) +``` -$ last -n 5 | awk '{print $1 "\t" $3} +```html +$ last -n 5 | awk '{print $1 "\t" $3}' ``` 可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。 @@ -1085,7 +1076,7 @@ $ last -n 5 | awk '{print $1 "\t" $3} $ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename ``` -示例 2:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 +示例:/etc/passwd 文件第三个字段为 UID,对 UID 小于 10 的数据进行处理。 ```text $ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}' @@ -1102,7 +1093,7 @@ awk 变量: | NR | 目前所处理的是第几行数据 | | FS | 目前的分隔字符,默认是空格键 | -示例 3:输出正在处理的行号,并显示每一行有多少字段 +示例:显示正在处理的行号以及每一行有多少字段 ```html $ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}' @@ -1123,19 +1114,19 @@ dmtsai lines: 5 columns: 9 示例一:查看自己的进程 -``` +```sh # ps -l ``` 示例二:查看系统所有进程 -``` +```sh # ps aux ``` 示例三:查看特定的进程 -``` +```sh # ps aux | grep threadx ``` @@ -1145,7 +1136,7 @@ dmtsai lines: 5 columns: 9 示例:两秒钟刷新一次 -``` +```sh # top -d 2 ``` @@ -1155,7 +1146,7 @@ dmtsai lines: 5 columns: 9 示例:查看所有进程树 -``` +```sh # pstree -A ``` @@ -1165,14 +1156,12 @@ dmtsai lines: 5 columns: 9 示例:查看特定端口的进程 -``` +```sh # netstat -anp | grep port ``` ## 进程状态 -

- | 状态 | 说明 | | :---: | --- | | R | running or runnable (on run queue) | @@ -1181,9 +1170,11 @@ dmtsai lines: 5 columns: 9 | Z | zombie (terminated but not reaped by its parent) | | T | stopped (either by a job control signal or because it is being traced) | +

+ ## SIGCHLD -当一个子进程改变了它的状态时:停止运行,继续运行或者退出,有两件事会发生在父进程中: +当一个子进程改变了它的状态时(停止运行,继续运行或者退出),有两件事会发生在父进程中: - 得到 SIGCHLD 信号; - waitpid() 或者 wait() 调用会返回。 @@ -1192,7 +1183,7 @@ dmtsai lines: 5 columns: 9 其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。 -在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息。父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 +在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息,父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 ## wait() @@ -1204,11 +1195,7 @@ pid_t wait(int *status) 如果成功,返回被收集的子进程的进程 ID;如果调用进程没有子进程,调用就会失败,此时返回 -1,同时 errno 被置为 ECHILD。 -参数 status 用来保存被收集的子进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL: - -```c -pid = wait(NULL); -``` +参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个子进程消灭掉,可以设置这个参数为 NULL。 ## waitpid() @@ -1218,7 +1205,7 @@ 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() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。 @@ -1236,9 +1223,9 @@ options 参数主要有 WNOHANG 和 WUNTRACED 两个选项,WNOHANG 可以使 w 僵尸进程通过 ps 命令显示出来的状态为 Z(zombie)。 -系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 +系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。 -要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时所有的僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵死进程所占有的资源,从而结束僵尸进程。 +要消灭系统中大量的僵尸进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵尸进程所占有的资源,从而结束僵尸进程。 # 参考资料 diff --git a/notes/MySQL.md b/notes/MySQL.md index d49fc952..690425db 100644 --- a/notes/MySQL.md +++ b/notes/MySQL.md @@ -9,7 +9,7 @@ * [字符串](#字符串) * [时间和日期](#时间和日期) * [三、索引](#三索引) - * [B Tree 原理](#b-tree-原理) + * [B+ Tree 原理](#b-tree-原理) * [索引分类](#索引分类) * [索引的优点](#索引的优点) * [索引优化](#索引优化) @@ -95,15 +95,15 @@ VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。 ## 时间和日期 -MySQL 提供了两种相似的日期时间类型:DATATIME 和 TIMESTAMP。 +MySQL 提供了两种相似的日期时间类型:DATETIME 和 TIMESTAMP。 -### 1. DATATIME +### 1. DATETIME 能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。 它与时区无关。 -默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 +默认情况下,MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值,例如“2008-01-16 22:37:08”,这是 ANSI 标准定义的日期和时间表示方法。 ### 2. TIMESTAMP @@ -125,49 +125,33 @@ MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提 索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 -## B Tree 原理 +## B+ Tree 原理 -### 1. B-Tree +### 1. 数据结构 -

+B Tree 指的是 Balance Tree,也就是平衡树。平衡树时一颗查找树,并且所有叶子节点位于同一层。 -定义一条数据记录为一个二元组 [key, data],B-Tree 是满足下列条件的数据结构: +B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 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 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 keyi 和 keyi+1,且不为 null,则该指针指向节点的所有 key 大于等于 keyi 且小于等于 keyi+1

-一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。 +### 2. 操作 -### 4. 优势 +进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。 -红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B Tree 作为索引结构,主要有以下两个原因: +插入删除操作记录会破坏平衡树的平衡性,因此在插入删除时,需要对树进行一个分裂、合并、旋转等操作。 + +### 3. 与红黑树的比较 + +红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因: (一)更少的检索次数 平衡树检索数据的时间复杂度等于树高 h,而树高大致为 O(h)=O(logdN),其中 d 为每个节点的出度。 -红黑树的出度为 2,而 B Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B Tree 大非常多,因此检索的次数也就更多。 - -B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高。 +红黑树的出度为 2,而 B+ Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B+ Tree 大非常多,因此检索的次数也就更多。 (二)利用计算机预读特性 @@ -175,8 +159,6 @@ B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,相邻的节点也能够被预先载入。 -更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) - ## 索引分类 ### 1. B+Tree 索引 @@ -442,3 +424,4 @@ MySQL 读写分离能提高性能的原因在于: - [服务端指南 数据存储篇 | 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.") +- [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html) diff --git a/notes/Redis.md b/notes/Redis.md index 21840399..0981abf0 100644 --- a/notes/Redis.md +++ b/notes/Redis.md @@ -27,18 +27,17 @@ * [八、持久化](#八持久化) * [RDB 持久化](#rdb-持久化) * [AOF 持久化](#aof-持久化) -* [九、发布与订阅](#九发布与订阅) -* [十、事务](#十事务) -* [十一、事件](#十一事件) +* [九、事务](#九事务) +* [十、事件](#十事件) * [文件事件](#文件事件) * [时间事件](#时间事件) * [事件的调度与执行](#事件的调度与执行) -* [十二、复制](#十二复制) +* [十一、复制](#十一复制) * [连接过程](#连接过程) * [主从链](#主从链) -* [十三、Sentinel](#十三sentinel) -* [十四、分片](#十四分片) -* [十五、一个简单的论坛系统分析](#十五一个简单的论坛系统分析) +* [十二、Sentinel](#十二sentinel) +* [十三、分片](#十三分片) +* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析) * [文章信息](#文章信息) * [点赞功能](#点赞功能) * [对文章进行排序](#对文章进行排序) @@ -417,10 +416,12 @@ Reids 具体有 6 种淘汰策略: | allkeys-random | 从所有数据集中任意选择数据进行淘汰 | | noeviction | 禁止驱逐数据 | -作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分 key 从中选出被淘汰 key。 +作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。 使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。 +Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。 + # 八、持久化 Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 @@ -439,7 +440,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 将写命令添加到 AOF 文件(Append Only File)的末尾。 -使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对硬盘的文件进行写入并不会马上将内容同步到磁盘文件上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到硬盘。有以下同步选项: +使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项: | 选项 | 同步频率 | | :--: | :--: | @@ -448,25 +449,12 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 | no | 让操作系统来决定何时同步 | - always 选项会严重减低服务器的性能; -- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; -- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。 +- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; +- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。 随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 -# 九、发布与订阅 - -订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。 - -某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。 - -发布与订阅模式和观察者模式有以下不同: - -- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 -- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息。 - -

- -# 十、事务 +# 九、事务 一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 @@ -474,7 +462,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需 Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。 -# 十一、事件 +# 十、事件 Redis 服务器是一个事件驱动程序。 @@ -539,7 +527,7 @@ def main():

-# 十二、复制 +# 十一、复制 通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。 @@ -559,15 +547,18 @@ def main():

-# 十三、Sentinel +# 十二、Sentinel Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 -# 十四、分片 +# 十三、分片 -分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 +分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。 -假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 +假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。 + +- 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。 +- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 根据执行分片的位置,可以分为三种分片方式: @@ -575,7 +566,7 @@ Sentinel(哨兵)可以监听主服务器,并在主服务器进入下线状 - 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 - 服务器分片:Redis Cluster。 -# 十五、一个简单的论坛系统分析 +# 十四、一个简单的论坛系统分析 该论坛系统功能如下: @@ -615,3 +606,4 @@ Redis 没有关系型数据库中的表这一概念来将同种类型的数据 - [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide) - [Redis 应用场景](http://www.scienjus.com/redis-use-case/) - [Observer vs Pub-Sub](http://developers-club.com/posts/270339/) +- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache) diff --git a/notes/SQL.md b/notes/SQL.md index 6dbb82ea..93e15072 100644 --- a/notes/SQL.md +++ b/notes/SQL.md @@ -19,7 +19,7 @@ * [十八、存储过程](#十八存储过程) * [十九、游标](#十九游标) * [二十、触发器](#二十触发器) -* [二十一、事务处理](#二十一事务处理) +* [二十一、事务管理](#二十一事务管理) * [二十二、字符集](#二十二字符集) * [二十三、权限管理](#二十三权限管理) * [参考资料](#参考资料) @@ -559,7 +559,7 @@ WHERE col5 = val; # 十八、存储过程 -存储过程可以看成是对一系列 SQL 操作的批处理; +存储过程可以看成是对一系列 SQL 操作的批处理。 使用存储过程的好处: @@ -648,11 +648,11 @@ SELECT @result; -- 获取结果 DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。 -UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改地,而 OLD 是只读的。 +UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改的,而 OLD 是只读的。 MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。 -# 二十一、事务处理 +# 二十一、事务管理 基本术语: @@ -714,12 +714,12 @@ SELECT user FROM user; **创建账户** +新创建的账户没有任何权限。 + ```sql CREATE USER myuser IDENTIFIED BY 'mypassword'; ``` -新创建的账户没有任何权限。 - **修改账户名** ```sql @@ -740,18 +740,14 @@ SHOW GRANTS FOR myuser; **授予权限** +账户用 username@host 的形式定义,username@% 使用的是默认主机名。 + ```sql GRANT SELECT, INSERT ON mydatabase.* TO myuser; ``` -账户用 username@host 的形式定义,username@% 使用的是默认主机名。 - **删除权限** -```sql -REVOKE SELECT, INSERT ON mydatabase.* FROM myuser; -``` - GRANT 和 REVOKE 可在几个层次上控制访问权限: - 整个服务器,使用 GRANT ALL 和 REVOKE ALL; @@ -760,6 +756,10 @@ GRANT 和 REVOKE 可在几个层次上控制访问权限: - 特定的列; - 特定的存储过程。 +```sql +REVOKE SELECT, INSERT ON mydatabase.* FROM myuser; +``` + **更改密码** 必须使用 Password() 函数 diff --git a/notes/一致性.md b/notes/分布式.md similarity index 52% rename from notes/一致性.md rename to notes/分布式.md index 7209dccf..c048b43e 100644 --- a/notes/一致性.md +++ b/notes/分布式.md @@ -1,32 +1,176 @@ -* [一、CAP](#一cap) +* [一、分布式锁](#一分布式锁) + * [数据库的唯一索引](#数据库的唯一索引) + * [Redis 的 SETNX 指令](#redis-的-setnx-指令) + * [Redis 的 RedLock 算法](#redis-的-redlock-算法) + * [Zookeeper 的有序节点](#zookeeper-的有序节点) +* [二、分布式事务](#二分布式事务) + * [本地消息表](#本地消息表) + * [2PC](#2pc) +* [三、CAP](#三cap) * [一致性](#一致性) * [可用性](#可用性) * [分区容忍性](#分区容忍性) * [权衡](#权衡) -* [二、BASE](#二base) +* [四、BASE](#四base) * [基本可用](#基本可用) * [软状态](#软状态) * [最终一致性](#最终一致性) -* [三、2PC](#三2pc) - * [运行过程](#运行过程) - * [存在的问题](#存在的问题) -* [四、Paxos](#四paxos) +* [五、Paxos](#五paxos) * [执行过程](#执行过程) * [约束条件](#约束条件) * [五、Raft](#五raft) * [单个 Candidate 的竞选](#单个-candidate-的竞选) * [多个 Candidate 竞选](#多个-candidate-竞选) * [日志复制](#日志复制) -* [参考资料](#参考资料) -# 一、CAP +# 一、分布式锁 + +在单机场景下,可以使用 Java 提供的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。 + +阻塞锁通常使用互斥量来实现: + +- 互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态; +- 互斥量为 0 表示未锁定状态。 + +1 和 0 可以用一个整型值表示,也可以用某个数据存在或者不存在表示,存在表示互斥量为 1。 + +## 数据库的唯一索引 + +当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。 + +存在以下几个问题: + +- 锁没有失效时间,解锁失败的话其它进程无法再获得锁。 +- 只能是非阻塞锁,插入失败直接就报错了,无法重试。 +- 不可重入,已经获得锁的进程也必须重新获取锁。 + +## Redis 的 SETNX 指令 + +使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。 + +SETNX 指令和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。 + +EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。 + +## Redis 的 RedLock 算法 + +使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。 + +- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个; +- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,那么就认为锁获取成功了; +- 如果锁获取失败,就到每个实例上释放锁。 + +## Zookeeper 的有序节点 + +### 1. Zookeeper 抽象模型 + +Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。 + +

+ +### 2. 节点类型 + +- 永久节点:不会因为会话结束或者超时而消失; +- 临时节点:如果会话结束或者超时就会消失; +- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推。 + +### 3. 监听器 + +为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。 + +### 4. 分布式锁实现 + +- 创建一个锁目录 /lock; +- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点; +- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁; +- 执行业务代码,完成后,删除对应的子节点。 + +### 5. 会话超时 + +如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的释放锁失败问题。 + +### 6. 羊群效应 + +一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 + +参考: + +- [Distributed locks with Redis](https://redis.io/topics/distlock) +- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023) +- [基于 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) + +# 二、分布式事务 + +指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。 + +## 本地消息表 + +### 1. 原理 + +本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。 + +1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 +2. 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 +3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。 + +

+ +### 2. 分析 + +本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。 + +## 2PC + +两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。 + +### 1. 运行过程 + +(一)准备阶段 + +协调者询问参与者事务是否执行成功,参与者发回事务执行结果。 + +

+ +(二)提交阶段 + +如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 + +需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 + +

+ +### 2. 存在的问题 + +(一)同步阻塞 + +所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。 + +(二)单点问题 + +协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。 + +(三)数据不一致 + +在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。 + +(四)太过保守 + +任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 + +参考: + +- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) +- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html) +- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be) + +# 三、CAP 分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。 -

+

## 一致性 @@ -54,18 +198,23 @@ 可用性和一致性往往是冲突的,很难都使它们同时满足。在多个节点之间进行数据同步时, -- 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成; -- 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。 +* 为了保证一致性(CP),就需要让所有节点下线成为不可用的状态,等待同步完成; +* 为了保证可用性(AP),在同步过程中允许读取所有节点的数据,但是数据可能不一致。 -

+

-# 二、BASE +参考: + +- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015. +- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/) + +# 四、BASE BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。 -BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 +BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 -

+

## 基本可用 @@ -85,49 +234,7 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 -# 三、2PC - -两阶段提交(Two-phase Commit,2PC) - -主要用于实现分布式事务,分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。 - -通过引入协调者(Coordinator)来调度参与者的行为,,并最终决定这些参与者是否要真正执行事务。 - -## 运行过程 - -### 1. 准备阶段 - -协调者询问参与者事务是否执行成功,参与者发回事务执行结果。 - -

- -### 2. 提交阶段 - -如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。 - -

- -需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。 - -## 存在的问题 - -### 1. 同步阻塞 - -所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。 - -### 2. 单点问题 - -协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响,特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。 - -### 3. 数据不一致 - -在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。 - -### 4. 太过保守 - -任意一个节点失败就会导致整个事务失败,没有完善的容错机制。 - -# 四、Paxos +# 五、Paxos 用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。 @@ -137,7 +244,7 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE - 接受者(Acceptor):对每个提议进行投票; - 告知者(Learner):被告知投票的结果,不参与投票过程。 -

+

## 执行过程 @@ -145,19 +252,19 @@ ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 下图演示了两个 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 的提议响应时,就可以发送接受请求。 @@ -165,26 +272,31 @@ Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求 Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。 -

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

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

+

-# 参考资料 +参考: -- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015. -- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/) -- [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/分布式问题分析.md b/notes/分布式问题分析.md deleted file mode 100644 index 4f019525..00000000 --- a/notes/分布式问题分析.md +++ /dev/null @@ -1,255 +0,0 @@ - -* [一、分布式锁](#一分布式锁) - * [数据库的唯一索引](#数据库的唯一索引) - * [Redis 的 SETNX 指令](#redis-的-setnx-指令) - * [Redis 的 RedLock 算法](#redis-的-redlock-算法) - * [Zookeeper 的有序节点](#zookeeper-的有序节点) -* [二、分布式事务](#二分布式事务) - * [本地消息表](#本地消息表) - * [两阶段提交协议](#两阶段提交协议) -* [三、分布式 Session](#三分布式-session) - * [Sticky Sessions](#sticky-sessions) - * [Session Replication](#session-replication) - * [Session Server](#session-server) -* [四、负载均衡](#四负载均衡) - * [算法](#算法) - * [实现](#实现) - - - -# 一、分布式锁 - -在单机场景下,可以使用 Java 提供的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。 - -阻塞锁通常使用互斥量来实现,互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态,互斥量为 0 表示未锁定状态。1 和 0 可以用一个整型值来存储,也可以用某个数据存在或者不存在来存储,某个数据存在表示互斥量为 1。 - -## 数据库的唯一索引 - -当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。 - -存在以下几个问题: - -- 锁没有失效时间,解锁失败的话其他线程无法再获得锁。 -- 只能是非阻塞锁,插入失败直接就报错了,无法重试。 -- 不可重入,已经获得锁的进程也必须重新获取锁。 - -## Redis 的 SETNX 指令 - -使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。 - -SETNX 指令和数据库的唯一索引类似,可以保证只存在一个 Key 的键值对,可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。 - -EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。 - -## Redis 的 RedLock 算法 - -使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。 - -- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。 -- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N/2+1)实例上获取了锁,那么就认为锁获取成功了。 -- 如果锁获取失败,会到每个实例上释放锁。 - -## Zookeeper 的有序节点 - -### 1. Zookeeper 抽象模型 - -Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。 - -

- -### 2. 节点类型 - -- 永久节点:不会因为会话结束或者超时而消失; -- 临时节点:如果会话结束或者超时就会消失; -- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推。 - -### 3. 监听器 - -为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。 - -### 4. 分布式锁实现 - -- 创建一个锁目录 /lock; -- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点; -- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁; -- 执行业务代码,完成后,删除对应的子节点。 - -### 5. 会话超时 - -如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的释放锁失败问题。 - -### 6. 羊群效应 - -一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。 - -参考: - -- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023) -- [Distributed locks with Redis](https://redis.io/topics/distlock) -- [基于 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) - -# 二、分布式事务 - -指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。 - -## 本地消息表 - -### 1. 原理 - -本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。 - -1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。 -2. 之后将本地消息表中的消息转发到 Kafka 等消息队列(MQ)中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。 -3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。 - -

- -### 2. 分析 - -本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。 - -## 两阶段提交协议 - -[CyC2018/Interview-Notebook/一致性.md/2PC](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E4%B8%80%E8%87%B4%E6%80%A7.md) - -参考: - -- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) -- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html) -- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be) - -# 三、分布式 Session - -在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。 - -## Sticky Sessions - -需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。 - -缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。 - -

- -## Session Replication - -在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。 - -缺点:占用过多内存;同步过程占用网络带宽以及服务器处理器时间。 - -

- -## Session Server - -使用一个单独的服务器存储 Session 数据,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。 - -缺点:需要去实现存取 Session 的代码。 - -

- -参考: - -- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/) - -# 四、负载均衡 - -## 算法 - -### 1. 轮询(Round Robin) - -轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。 - -

- -该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。 - -

- -### 2. 加权轮询(Weighted Round Robbin) - -加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。 - -

- -### 3. 最少连接(least Connections) - -由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。 - -

- -最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。 - -

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

- -### 5. 随机算法(Random) - -把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。 - -

- -### 6. 源地址哈希法 (IP Hash) - -源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。 - -- 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。 -- 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。 - -

- -## 实现 - -### 1. HTTP 重定向 - -HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。 - -缺点: - -- 用户访问的延迟会增加; -- 如果负载均衡器宕机,就无法访问该站点。 - -

- -### 2. DNS 重定向 - -使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。 - -缺点: - -- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。 - -

- -### 3. 修改 MAC 地址 - -使用 LVS(Linux Virtual Server)这种链路层负载均衡器,根据负载情况修改请求的 MAC 地址。 - -

- -### 4. 修改 IP 地址 - -在网络层修改请求的目的 IP 地址。 - -

- -### 5. 代理自动配置 - -正向代理与反向代理的区别: - -- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。 -- 反向代理:发生在服务器端,用户不知道代理的存在。 - -PAC 服务器是用来判断一个请求是否要经过代理。 - -

- -参考: - -- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms) -- [负载均衡算法及手段](https://segmentfault.com/a/1190000004492447) -- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#) - diff --git a/notes/剑指 offer 题解.md b/notes/剑指 offer 题解.md index 6f786cbe..47d5d94e 100644 --- a/notes/剑指 offer 题解.md +++ b/notes/剑指 offer 题解.md @@ -1,4 +1,5 @@ +* [1. 前言](#1-前言) * [2. 实现 Singleton](#2-实现-singleton) * [3. 数组中重复的数字](#3-数组中重复的数字) * [4. 二维数组中的查找](#4-二维数组中的查找) @@ -80,6 +81,13 @@ +# 1. 前言 + +本文的绘图可通过以下途径免费获得并使用: + +- [ProcessOn](https://www.processon.com/view/5a3e4c7be4b0909c1aa18b49) +- [DrawIO](https://drive.google.com/file/d/1nSSCpPUC05MFoeFuf_aeTtkm7dG5-bJ1/view?usp=sharing) + # 2. 实现 Singleton [单例模式](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md) @@ -90,12 +98,20 @@ ## 题目描述 -在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。例如,如果输入长度为 7 的数组 {2, 3, 1, 0, 2, 5},那么对应的输出是第一个重复的数字 2。 +在一个长度为 n 的数组里的所有数字都在 0 到 n-1 的范围内。数组中某些数字是重复的,但不知道有几个数字是重复的,也不知道每个数字重复几次。请找出数组中任意一个重复的数字。 -要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。 +```html +Input: +{2, 3, 1, 0, 2, 5} + +Output: +2 +``` ## 解题思路 +要求复杂度为 O(N) + O(1),也就是时间复杂度 O(N),空间复杂度 O(1)。因此不能使用排序的方法,也不能使用额外的标记数组。牛客网讨论区这一题的首票答案使用 nums[i] + length 来将元素标记,这么做会有加法溢出问题。 + 这种数组元素在 [0, n-1] 范围内的问题,可以将值为 i 的元素放到第 i 个位置上。 以 (2, 3, 1, 0, 2, 5) 为例: @@ -158,7 +174,11 @@ Given target = 20, return false. ## 解题思路 -从右上角开始查找。因为矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。 +从右上角开始查找。矩阵中的一个数,它左边的数都比它小,下边的数都比它大。因此,从右上角开始查找,就可以根据 target 和当前元素的大小关系来缩小查找区间。 + +当前元素的查找区间为左下角的所有元素,例如元素 12 的查找区间如下: + +

复杂度:O(M + N) + O(1) @@ -261,25 +281,14 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` -### 使用 Collections.reverse() - -```java -public ArrayList printListFromTailToHead(ListNode listNode) { - ArrayList ret = new ArrayList<>(); - while (listNode != null) { - ret.add(listNode.val); - listNode = listNode.next; - } - Collections.reverse(ret); - return ret; -} -``` - ### 使用头插法 利用链表头插法为逆序的特点。 -头结点和第一个节点的区别:头结点是在头插法中使用的一个额外节点,这个节点不存储值;第一个节点就是链表的第一个真正存储值的节点。 +头结点和第一个节点的区别: + +- 头结点是在头插法中使用的一个额外节点,这个节点不存储值; +- 第一个节点就是链表的第一个真正存储值的节点。 ```java public ArrayList printListFromTailToHead(ListNode listNode) { @@ -302,6 +311,20 @@ public ArrayList printListFromTailToHead(ListNode listNode) { } ``` +### 使用 Collections.reverse() + +```java +public ArrayList printListFromTailToHead(ListNode listNode) { + ArrayList ret = new ArrayList<>(); + while (listNode != null) { + ret.add(listNode.val); + listNode = listNode.next; + } + Collections.reverse(ret); + return ret; +} +``` + # 7. 重建二叉树 [NowCoder](https://www.nowcoder.com/practice/8a19cbe657394eeaac2f6ea9b0f6fcf6?tpId=13&tqId=11157&tPage=1&rp=1&ru=/ta/coding-interviews&qru=/ta/coding-interviews/question-ranking) @@ -322,22 +345,23 @@ inorder = [9,3,15,20,7] 前序遍历的第一个值为根节点的值,使用这个值将中序遍历结果分成两部分,左部分为树的左子树中序遍历结果,右部分为树的右子树中序遍历的结果。 ```java -private Map inOrderNumsIndexs = new HashMap<>(); // 缓存中序遍历数组的每个值对应的索引 +// 缓存中序遍历数组的每个值对应的索引 +private Map inOrderNumsIndexs = new HashMap<>(); public TreeNode reConstructBinaryTree(int[] pre, int[] in) { for (int i = 0; i < in.length; i++) inOrderNumsIndexs.put(in[i], i); - return reConstructBinaryTree(pre, 0, pre.length - 1, in, 0, in.length - 1); + return reConstructBinaryTree(pre, 0, pre.length - 1, 0, in.length - 1); } -private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, int inL, int inR) { +private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int inL, int inR) { if (preL > preR) return null; TreeNode root = new TreeNode(pre[preL]); int inIndex = inOrderNumsIndexs.get(root.val); int leftTreeSize = inIndex - inL; - root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, in, inL, inL + leftTreeSize - 1); - root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, in, inL + leftTreeSize + 1, inR); + root.left = reConstructBinaryTree(pre, preL + 1, preL + leftTreeSize, inL, inL + leftTreeSize - 1); + root.right = reConstructBinaryTree(pre, preL + leftTreeSize + 1, preR, inL + leftTreeSize + 1, inR); return root; } ``` @@ -350,16 +374,6 @@ private TreeNode reConstructBinaryTree(int[] pre, int preL, int preR, int[] in, 给定一个二叉树和其中的一个结点,请找出中序遍历顺序的下一个结点并且返回。注意,树中的结点不仅包含左右子结点,同时包含指向父结点的指针。 -## 解题思路 - -① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; - -

- -② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 - -

- ```java public class TreeLinkNode { int val; @@ -373,6 +387,16 @@ public class TreeLinkNode { } ``` +## 解题思路 + +① 如果一个节点的右子树不为空,那么该节点的下一个节点是右子树的最左节点; + +

+ +② 否则,向上找第一个左链接指向的树包含该节点的祖先节点。 + +

+ ```java public TreeLinkNode GetNext(TreeLinkNode pNode) { if (pNode.right != null) { @@ -398,7 +422,7 @@ public TreeLinkNode GetNext(TreeLinkNode pNode) { ## 题目描述 -用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。队列中的元素为 int 类型。 +用两个栈来实现一个队列,完成队列的 Push 和 Pop 操作。 ## 解题思路 @@ -440,9 +464,9 @@ public int pop() throws Exception { 如果使用递归求解,会重复计算一些子问题。例如,计算 f(10) 需要计算 f(9) 和 f(8),计算 f(9) 需要计算 f(8) 和 f(7),可以看到 f(8) 被重复计算了。 -

+

-递归方法是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,避免重复求解子问题。 +递归是将一个问题划分成多个子问题求解,动态规划也是如此,但是动态规划会把子问题的解缓存起来,从而避免重复求解子问题。 ```java public int Fibonacci(int n) { @@ -521,11 +545,11 @@ public int JumpFloor(int n) { ```java public int JumpFloor(int n) { - if (n <= 1) + if (n <= 2) return n; - int pre2 = 0, pre1 = 1; - int result = 0; - for (int i = 1; i <= n; i++) { + int pre2 = 1, pre1 = 2; + int result = 1; + for (int i = 2; i < n; i++) { result = pre2 + pre1; pre2 = pre1; pre1 = result; @@ -882,7 +906,7 @@ public double Power(double base, int exponent) { ## 题目描述 -输入数字 n,按顺序打印出从 1 最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。 +输入数字 n,按顺序打印出从 1 到最大的 n 位十进制数。比如输入 3,则打印出 1、2、3 一直到最大的 3 位数即 999。 ## 解题思路 @@ -895,16 +919,16 @@ public void print1ToMaxOfNDigits(int n) { if (n <= 0) return; char[] number = new char[n]; - print1ToMaxOfNDigits(number, -1); + print1ToMaxOfNDigits(number, 0); } private void print1ToMaxOfNDigits(char[] number, int digit) { - if (digit == number.length - 1) { + if (digit == number.length) { printNumber(number); return; } for (int i = 0; i < 10; i++) { - number[digit + 1] = (char) (i + '0'); + number[digit] = (char) (i + '0'); print1ToMaxOfNDigits(number, digit + 1); } } @@ -925,11 +949,11 @@ private void printNumber(char[] number) { ① 如果该节点不是尾节点,那么可以直接将下一个节点的值赋给该节点,然后令该节点指向下下个节点,再删除下一个节点,时间复杂度为 O(1)。 -

+

② 否则,就需要先遍历链表,找到节点的前一个节点,然后让前一个节点指向 null,时间复杂度为 O(N)。 -

+

综上,如果进行 N 次操作,那么大约需要操作节点的次数为 N-1+N=2N-1,其中 N-1 表示 N-1 个不是尾节点的每个节点以 O(1) 的时间复杂度操作节点的总次数,N 表示 1 个尾节点以 O(N) 的时间复杂度操作节点的总次数。(2N-1)/N \~ 2,因此该算法的平均时间复杂度为 O(1)。 @@ -1034,13 +1058,13 @@ public boolean match(char[] str, char[] pattern) { ```html [] : 字符集合 -() : 分组,在这里是为了让表达式更清晰 +() : 分组 ? : 重复 0 ~ 1 + : 重复 1 ~ n * : 重复 0 ~ n . : 任意字符 \\. : 转义后的 . -\\d : 任意数字 +\\d : 数字 ``` ```java @@ -1087,7 +1111,7 @@ public void reOrderArray(int[] nums) { 设链表的长度为 N。设两个指针 P1 和 P2,先让 P1 移动 K 个节点,则还有 N - K 个节点可以移动。此时让 P1 和 P2 同时移动,可以知道当 P1 移动到链表结尾时,P2 移动到 N - K 个节点处,该位置就是倒数第 K 个节点。 -

+

```java public ListNode FindKthToTail(ListNode head, int k) @@ -1124,7 +1148,7 @@ public ListNode FindKthToTail(ListNode head, int k) 在相遇点,slow 要到环的入口点还需要移动 z 个节点,如果让 fast 重新从头开始移动,并且速度变为每次移动一个节点,那么它到环入口点还需要移动 x 个节点。在上面已经推导出 x=z,因此 fast 和 slow 将在环入口点相遇。 -

+

```java public ListNode EntryNodeOfLoop(ListNode pHead) @@ -1330,7 +1354,7 @@ boolean isSymmetrical(TreeNode t1, TreeNode t2) 下图的矩阵顺时针打印结果为:1, 2, 3, 4, 8, 12, 16, 15, 14, 13, 9, 5, 6, 7, 11, 10 -

+

## 解题思路 @@ -1616,6 +1640,19 @@ private void backtracking(TreeNode node, int target, ArrayList path) 输入一个复杂链表(每个节点中有节点值,以及两个指针,一个指向下一个节点,另一个特殊指针指向任意一个节点),返回结果为复制后复杂链表的 head。 +```java +public class RandomListNode +{ + int label; + RandomListNode next = null; + RandomListNode random = null; + + RandomListNode(int label) { + this.label = label; + } +} +``` +

## 解题思路 @@ -1796,7 +1833,7 @@ private void backtracking(char[] chars, boolean[] hasUsed, StringBuilder s) 多数投票问题,可以利用 Boyer-Moore Majority Vote Algorithm 来解决这个问题,使得时间复杂度为 O(N)。 -使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0 ,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。 +使用 cnt 来统计一个元素出现的次数,当遍历到的元素和统计元素不相等时,令 cnt--。如果前面查找了 i 个元素,且 cnt == 0,说明前 i 个元素没有 majority,或者有 majority,但是出现的次数少于 i / 2 ,因为如果多于 i / 2 的话 cnt 就一定不会为 0 。此时剩下的 n - i 个元素中,majority 的数目依然多于 (n - i) / 2,因此继续查找就能找出 majority。 ```java public int MoreThanHalfNum_Solution(int[] nums) @@ -1900,7 +1937,7 @@ public ArrayList GetLeastNumbers_Solution(int[] nums, int k) if (maxHeap.size() > k) maxHeap.poll(); } - return new ArrayList<>(maxHeap) ; + return new ArrayList<>(maxHeap); } ``` @@ -1981,7 +2018,7 @@ public char FirstAppearingOnce() ## 题目描述 -{6,-3,-2,7,-15,1,2,2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 +{6, -3, -2, 7, -15, 1, 2, 2},连续子数组的最大和为 8(从第 0 个开始,到第 3 个为止)。 ## 解题思路 diff --git a/notes/攻击技术.md b/notes/攻击技术.md new file mode 100644 index 00000000..c99071a0 --- /dev/null +++ b/notes/攻击技术.md @@ -0,0 +1,219 @@ + +* [一、跨站脚本攻击](#一跨站脚本攻击) +* [二、跨站请求伪造](#二跨站请求伪造) +* [三、SQL 注入攻击](#三sql-注入攻击) +* [四、拒绝服务攻击](#四拒绝服务攻击) +* [参考资料](#参考资料) + + + +# 一、跨站脚本攻击 + +## 概念 + +跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。 + +例如有一个论坛网站,攻击者可以在上面发布以下内容: + +```html + +``` + +之后该内容可能会被渲染成以下形式: + +```html +

+``` + +另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。 + +## 危害 + +- 窃取用户的 Cookie 值 +- 伪造虚假的输入表单骗取个人信息 +- 显示伪造的文章或者图片 + +## 防范手段 + +### 1. 设置 Cookie 为 HttpOnly + +设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。 + +### 2. 过滤特殊字符 + +例如将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。 + +## 富文本编辑器 + +富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。 + +富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。 + +以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。 + +> [XSS 过滤在线测试](http://jsxss.com/zh/try.html) + +```html +

XSS Demo

+ +

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

+ +
+ + +
+ +
hello
+ +

+ http +

+ +

Features:

+
    +
  • Specifies HTML tags and their attributes allowed with whitelist
  • +
  • Handle any tags or attributes using custom function
  • +
+ + +``` + +```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:

+
    +
  • Specifies HTML tags and their attributes allowed with whitelist
  • +
  • Handle any tags or attributes using custom function
  • +
+ +<script type="text/javascript"> +alert(/xss/); +</script> +``` + +# 二、跨站请求伪造 + +## 概念 + +跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。 + +XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。 + +假如一家银行用以执行转账操作的 URL 地址如下: + +``` +http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。 +``` + +那么,一个恶意攻击者可以在另一个网站上放置如下代码: + +``` +。 +``` + +如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。 + +这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。 + +透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。 + +## 防范手段 + +### 1. 检查 Referer 首部字段 + +Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 CSRF 攻击。 + +这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。 + +### 2. 添加校验 Token + +在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。 + +### 3. 输入验证码 + +因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。 + +也可以要求用户输入验证码来进行校验。 + +# 三、SQL 注入攻击 + +## 概念 + +服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。 + +## 攻击原理 + +例如一个网站登录验证的 SQL 查询代码为: + +```sql +strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');" +``` + +如果填入以下内容: + +```sql +userName = "1' OR '1'='1"; +passWord = "1' OR '1'='1"; +``` + +那么 SQL 查询字符串为: + +```sql +strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');" +``` + +此时无需验证通过就能执行以下查询: + +```sql +strSQL = "SELECT * FROM users;" +``` + +## 防范手段 + +### 1. 使用参数化查询 + +Java 中的 PreparedStatement 是预先编译的 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(); +``` + +### 2. 单引号转换 + +将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。 + +# 四、拒绝服务攻击 + +拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。 + +分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。 + +# 参考资料 + +- [维基百科:跨站脚本](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) +- [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A) diff --git a/notes/数据库系统原理.md b/notes/数据库系统原理.md index 87d210a9..151ffc0f 100644 --- a/notes/数据库系统原理.md +++ b/notes/数据库系统原理.md @@ -44,10 +44,10 @@ ## 概念 -

- 事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 +

+ ## ACID ### 1. 原子性(Atomicity) @@ -70,18 +70,18 @@ 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 -可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 +可以通过数据库备份和恢复来实现,在系统发生崩溃时,使用备份的数据库进行数据恢复。 ---- 事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系: - 只有满足一致性,事务的执行结果才是正确的。 -- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时要只要能满足原子性,就一定能满足一致性。 +- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 - 在并发的情况下,多个事务并发执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 -- 事务满足持久化是为了能应对数据库奔溃的情况。 +- 事务满足持久化是为了能应对数据库崩溃的情况。 -

+

## AUTOCOMMIT @@ -123,7 +123,6 @@ T1 读取某个范围的数据,T2 在这个范围内插 ## 封锁粒度 -

MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 @@ -133,6 +132,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。 +

+ ## 封锁类型 ### 1. 读写锁 @@ -304,7 +305,11 @@ SELECT ... FOR UPDATE; # 五、多版本并发控制 -多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC;可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 +多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。 + +而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。 + +可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 ## 版本号 @@ -372,11 +377,15 @@ delete; # 六、Next-Key Locks -Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 +Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。 + +MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。 ## Record Locks -锁定整个记录(行)。锁定的对象是记录的索引,而不是记录本身。如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。 +锁定的对象是记录的索引,而不是记录本身。 + +如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。 ## Gap Locks @@ -420,7 +429,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; 如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 -对于 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖; +对于 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖。 对于 A->B,B->C,则 A->C 是一个传递函数依赖。 @@ -452,7 +461,7 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; ### 1. 第一范式 (1NF) -属性不可分; +属性不可分。 ### 2. 第二范式 (2NF) diff --git a/notes/构建工具.md b/notes/构建工具.md new file mode 100644 index 00000000..c0e4acd5 --- /dev/null +++ b/notes/构建工具.md @@ -0,0 +1,147 @@ + +* [一、什么是构建工具](#一什么是构建工具) +* [二、Java 主流构建工具](#二java-主流构建工具) +* [三、Maven](#三maven) + + + +# 一、什么是构建工具 + +构建工具是用于构建项目的自动化工具,主要包含以下工作: + +## 依赖管理 + +不再需要手动导入 Jar 依赖包,并且可以自动处理依赖关系,也就是说某个依赖如果依赖于其它依赖,构建工具可以帮助我们自动处理这种依赖管理。 + +## 运行单元测试 + +不再需要在项目代码中添加测试代码,从而污染项目代码。 + +## 将源代码转化为可执行文件 + +包含预处理、编译、汇编、链接等步骤。 + +## 将可执行文件进行打包 + +不再需要使用 IDE 将应用程序打包成 Jar 包。 + +## 发布到生产服务器上 + +不再需要通过 FTP 将 Jar 包上传到服务器上。 + +参考资料: + +- [What is a build tool?](https://stackoverflow.com/questions/7249871/what-is-a-build-tool) + +# 二、Java 主流构建工具 + +主要包括 Ant、Maven 和 Gradle。 + +

+ +Gradle 和 Maven 的区别是,它使用 Groovy 这种特定领域语言(DSL)来管理构建脚本,而不再使用 XML 这种标记性语言。因为项目如果庞大的话,XML 很容易就变得臃肿。 + +例如要在项目中引入 Junit,Maven 的代码如下: + +```xml + + + 4.0.0 + + jizg.study.maven.hello + hello-first + 0.0.1-SNAPSHOT + + + + junit + junit + 4.10 + test + + + +``` + +而 Gradle 只需要几行代码: + +```java +dependencies { + testCompile "junit:junit:4.10" +} +``` + +参考资料: + +- [Java Build Tools Comparisons: Ant vs Maven vs Gradle](https://programmingmitra.blogspot.com/2016/05/java-build-tools-comparisons-ant-vs.html) +- [maven 2 gradle](http://sagioto.github.io/maven2gradle/) +- [新一代构建工具 gradle](https://www.imooc.com/learn/833) + +# 三、Maven + +## 概述 + +提供了项目对象模型(POM)文件来管理项目的构建。 + +## 仓库 + +仓库的搜索顺序为:本地仓库、中央仓库、远程仓库。 + +- 本地仓库用来存储项目的依赖库; +- 中央仓库是下载依赖库的默认位置; +- 远程仓库,因为并非所有的库存储在中央仓库,或者中央仓库访问速度很慢,远程仓库是中央仓库的补充。 + +## POM + +POM 代表项目对象模型,它是一个 XML 文件,保存在项目根目录的 pom.xml 文件中。 + +```xml + + junit + junit + 4.12 + test + +``` + +[groupId, artifactId, version, packaging, classfier] 称为一个项目的坐标,其中 groupId、artifactId、version 必须定义,packaging 可选(默认为 Jar),classfier 不能直接定义的,需要结合插件使用。 + +- groupId:项目组 Id,必须全球唯一; +- artifactId:项目 Id,即项目名; +- version:项目版本; +- packaging:项目打包方式。 + +## 依赖原则 + +### 依赖路径最短优先原则 + +```html +A -> B -> C -> X(1.0) +A -> D -> X(2.0) +``` +由于 X(2.0) 路径最短,所以使用 X(2.0)。 + +### 声明顺序优先原则 + +```html +A -> B -> X(1.0) +A -> C -> X(2.0) +``` + +在 POM 中最先声明的优先,上面的两个依赖如果先声明 B,那么最后使用 X(1.0)。 + +### 覆写优先原则 + +子 POM 内声明的依赖优先于父 POM 中声明的依赖。 + +## 解决依赖冲突 + +找到 Maven 加载的 Jar 包版本,使用 `mvn dependency:tree` 查看依赖树,根据依赖原则来调整依赖在 POM 文件的声明顺序。 + +参考资料: + +- [POM Reference](http://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification) + + + diff --git a/notes/正则表达式.md b/notes/正则表达式.md index c6c21f2c..c713da63 100644 --- a/notes/正则表达式.md +++ b/notes/正则表达式.md @@ -23,11 +23,11 @@ # 二、匹配单个字符 -正则表达式一般是区分大小写的,但是也有些实现是不区分。 - **.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符; -**\\** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。 +**.** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。 + +正则表达式一般是区分大小写的,但是也有些实现是不区分。 **正则表达式** @@ -43,11 +43,11 @@ My **name** is Zheng. **[ ]** 定义一个字符集合; -0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间只能用在 [ ] 之间。 +0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间在 [ ] 中使用。 -**-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符; +**-** 只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符; -**^** 在 [ ] 字符集合中是取非操作。 +**^** 在 [ ] 中是取非操作。 **应用** @@ -78,9 +78,9 @@ abc[^0-9] | \t | 制表符 | | \v | 垂直制表符 | -\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n ;\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行; +\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n。 -. 是元字符,前提是没有对它们进行转义;f 和 n 也是元字符,但是前提是对它们进行了转义。 +\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行; ## 匹配特定的字符类别 @@ -105,11 +105,13 @@ abc[^0-9] | \s | 任何一个空白字符,等价于 [\f\n\r\t\v] | | \S | 对 \s 取非 | -\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10 ,等价于 \n,也就是它会匹配 \n 。 +\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10,等价于 \n。 # 五、重复匹配 -**\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。 +- **\+** 匹配 1 个或者多个字符 +- **\** * 匹配 0 个或者多个 +- **?** 匹配 0 个或者 1 个 **应用** @@ -127,16 +129,11 @@ abc[^0-9] **abc.def@qq.com** -为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。 +- **{n}** 匹配 n 个字符 +- **{m, n}** 匹配 m\~n 个字符 +- **{m,}** 至少匹配 m 个字符 -``` -[\w.]+@\w+\.\w+ -[\w.]+@[\w]+[\.][\w]+ -``` - -**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符; - -\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。 +\* 和 + 都是贪婪型元字符,会匹配最多的内容。在后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。 **正则表达式** @@ -220,7 +217,9 @@ a.+c **应用** -匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: +匹配 IP 地址。 + +IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: - 一位数字 - 不以 0 开头的两位数字 diff --git a/notes/消息队列.md b/notes/消息队列.md new file mode 100644 index 00000000..74155802 --- /dev/null +++ b/notes/消息队列.md @@ -0,0 +1,80 @@ + +* [一、消息模型](#一消息模型) + * [点对点](#点对点) + * [发布/订阅](#发布订阅) +* [二、使用场景](#二使用场景) + * [异步处理](#异步处理) + * [流量削锋](#流量削锋) + * [应用解耦](#应用解耦) +* [三、可靠性](#三可靠性) + * [发送端的可靠性](#发送端的可靠性) + * [接收端的可靠性](#接收端的可靠性) + + + +# 一、消息模型 + +## 点对点 + +消息生产者向消息队列中发送了一个消息之后,只能被一个消费者消费一次。 + +

+ +## 发布/订阅 + +消息生产者向频道发送一个消息之后,多个消费者可以从该频道订阅到这条消息并消费。 + +

+ +发布与订阅模式和观察者模式有以下不同: + +- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。 +- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息。 + +

+ +参考: + +- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/) +- [消息队列中点对点与发布订阅区别](https://blog.csdn.net/lizhitao/article/details/47723105) + +# 二、使用场景 + +## 异步处理 + +发送者将消息发送给消息队列之后,不需要同步等待消息接收者处理完毕,而是立即返回进行其它操作。消息接收者从消息队列中订阅消息之后异步处理。 + +例如在注册流程中通常需要发送验证邮件来确保注册用户的身份合法,可以使用消息队列使发送验证邮件的操作异步处理,用户在填写完注册信息之后就可以完成注册,而将发送验证邮件这一消息发送到消息队列中。 + +只有在业务流程允许异步处理的情况下才能这么做,例如上面的注册流程中,如果要求用户对验证邮件进行点击之后才能完成注册的话,就不能再使用消息队列。 + +## 流量削锋 + +在高并发的场景下,如果短时间有大量的请求到达会压垮服务器。 + +可以将请求发送到消息队列中,服务器按照其处理能力从消息队列中订阅消息进行处理。 + +## 应用解耦 + +如果模块之间不直接进行调用,模块之间耦合度就会很低,那么修改一个模块或者新增一个模块对其它模块的影响会很小,从而实现可扩展性。 + +通过使用消息队列,一个模块只需要向消息队列中发送消息,其它模块可以选择性地从消息队列中订阅消息从而完成调用。 + +# 三、可靠性 + +## 发送端的可靠性 + +发送端完成操作后一定能将消息成功发送到消息队列中。 + +实现方法: + +- 在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息队列中,若转移消息成功则删除消息表中的数据,否则继续重传。 + +## 接收端的可靠性 + +接收端能够从消息中间件成功消费一次消息。 + +实现方法: + +- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。 +- 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。 diff --git a/notes/算法.md b/notes/算法.md index df5a8049..fb878742 100644 --- a/notes/算法.md +++ b/notes/算法.md @@ -2050,7 +2050,7 @@ public class SparseVector { 如果只有一个圆盘,那么只需要进行一次移动操作。 -从上面的讨论可以知道,n 圆盘需要移动 (n-1)+1+(n-1) = 2n-1 次。 +从上面的讨论可以知道,an = 2 * an-1 + 1,显然 an = 2n - 1,n 个圆盘需要移动 2n - 1 次。

diff --git a/notes/系统设计基础.md b/notes/系统设计基础.md new file mode 100644 index 00000000..40f0ca9d --- /dev/null +++ b/notes/系统设计基础.md @@ -0,0 +1,104 @@ + +* [一、性能](#一性能) +* [二、伸缩性](#二伸缩性) +* [三、扩展性](#三扩展性) +* [四、可用性](#四可用性) +* [五、安全性](#五安全性) + + + +# 一、性能 + +## 性能指标 + +### 1. 响应时间 + +指从某个请求从发出到接收到响应消耗的时间。 + +在对响应时间进行测试时,通常采用重复请求方式,然后计算平均响应时间。 + +### 2. 吞吐量 + +指系统在单位时间内可以处理的请求数量,通常使用每秒的请求数来衡量。 + +### 3. 并发用户数 + +指系统能同时处理的并发用户请求数量。 + +在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s,那么平均响应时间应该为 0.01s。 + +目前的大型系统都支持多线程来处理并发请求,多线程能够提高吞吐量以及缩短响应时间,主要有两个原因: + +- 多 CPU +- IO 等待时间 + +使用 IO 多路复用等方式,系统在等待一个 IO 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。 + +并发用户数不是越高越好,因为如果并发用户数太高,系统来不及处理这么多的请求,会使得过多的请求需要等待,那么响应时间就会大大提高。 + +## 性能优化 + +### 1. 集群 + +将多台服务器组成集群,使用负载均衡将请求转发到集群中,避免单一服务器的负载压力过大导致性能降低。 + +### 2. 缓存 + +缓存能够提高性能的原因如下: + +- 缓存数据通常位于内存等介质中,这种介质对于读操作特别快; +- 缓存数据可以位于靠近用户的地理位置上; +- 可以将计算结果进行缓存,从而避免重复计算。 + +### 3. 异步 + +某些流程可以将操作转换为消息,将消息发送到消息队列之后立即返回,之后这个操作会被异步处理。 + +# 二、伸缩性 + +指不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。 + +## 伸缩性与性能 + +如果系统存在性能问题,那么单个用户的请求总是很慢的; + +如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。 + +## 实现伸缩性 + +应用服务器只要不具有状态,那么就可以很容易地通过负载均衡器向集群中添加新的服务器。 + +关系型数据库的伸缩性通过 Sharding 来实现,将数据按一定的规则分布到不同的节点上,从而解决单台存储服务器存储空间限制。 + +对于非关系型数据库,它们天生就是为海量数据而诞生,对伸缩性的支持特别好。 + +# 三、扩展性 + +指的是添加新功能时对现有系统的其它应用无影响,这就要求不同应用具备低耦合的特点。 + +实现可扩展主要有两种方式: + +- 使用消息队列进行解耦,应用之间通过消息传递的方式进行通信; +- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以用过调用可复用的服务来实现业务逻辑,对其它产品没有影响。 + +# 四、可用性 + +## 冗余 + +保证高可用的主要手段是使用冗余,当某个服务器故障时就请求其它服务器。 + +应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上不会对用户有任何影响。 + +存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器,这个过程称为切换。 + +## 监控 + +对 CPU、内存、磁盘、网络等系统负载信息进行监控,当某个数据达到一定阈值时通知运维人员,从而在系统发生故障之前及时发现问题。 + +## 服务降级 + +服务器降级是系统为了应对大量的请求,主动关闭部分功能,从而保证核心功能可用。 + +# 五、安全性 + +要求系统的应对各种攻击手段时能够有可靠的应对措施。 diff --git a/notes/缓存.md b/notes/缓存.md new file mode 100644 index 00000000..449598ff --- /dev/null +++ b/notes/缓存.md @@ -0,0 +1,270 @@ + +* [一、缓存特征](#一缓存特征) +* [二、LRU](#二lru) +* [三、缓存位置](#三缓存位置) +* [四、CDN](#四cdn) +* [五、缓存问题](#五缓存问题) +* [六、数据分布](#六数据分布) +* [七、一致性哈希](#七一致性哈希) + + + +# 一、缓存特征 + +## 命中率 + +当某个请求能够通过访问缓存而得到响应时,称为缓存命中。 + +缓存命中率越高,缓存的利用率也就越高。 + +## 最大空间 + +缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。 + +当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。 + +## 淘汰策略 + +- FIFO(First In First Out):先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使最先进入的数据(最晚的数据)被淘汰。 + +- LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最远的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。 + +参考资料: + +- [缓存那些事](https://tech.meituan.com/cache_about.html) + +# 二、LRU + +以下是一个基于 双向队列 + HashMap 的 LRU 算法实现,对算法的解释如下: + +- 最基本的思路是当访问某个节点时,将其从原来的位置删除,并重新插入到链表头部,这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就删除链表尾部的节点。 +- 为了使删除操作时间复杂度为 O(1),那么就不能采用遍历的方式找到某个节点。HashMap 存储这 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。 + +```java +public class LRU implements Iterable { + + private Node head; + private Node tail; + private HashMap map; + private int maxSize; + + private class Node { + + Node pre; + Node next; + K k; + V v; + + public Node(K k, V v) { + this.k = k; + this.v = v; + } + } + + public LRU(int maxSize) { + + this.maxSize = maxSize; + this.map = new HashMap<>(maxSize * 4 / 3); + + head = new Node(null, null); + tail = new Node(null, null); + + head.next = tail; + tail.pre = head; + } + + public V get(K key) { + + if (!map.containsKey(key)) { + return null; + } + + Node node = map.get(key); + unlink(node); + appendHead(node); + + return node.v; + } + + public void put(K key, V value) { + + if (map.containsKey(key)) { + Node node = map.get(key); + unlink(node); + } + + Node node = new Node(key, value); + map.put(key, node); + appendHead(node); + + if (map.size() > maxSize) { + Node toRemove = removeTail(); + map.remove(toRemove); + } + } + + private void unlink(Node node) { + Node pre = node.pre; + node.pre = node.next; + node.next = pre; + } + + private void appendHead(Node node) { + node.next = head.next; + head.next = node; + } + + private Node removeTail() { + Node node = tail.pre; + node.pre = tail; + return node; + } + + @Override + public Iterator iterator() { + + return new Iterator() { + + private Node cur = head.next; + + @Override + public boolean hasNext() { + return cur != tail; + } + + @Override + public K next() { + Node node = cur; + cur = cur.next; + return node.k; + } + }; + } +} +``` + +# 三、缓存位置 + +## 浏览器 + +当 HTTP 响应允许进行缓存时,浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存。 + +## ISP + +网络服务提供商(ISP)是网络访问的第一跳,通过将数据缓存在 ISP 中能够大大提高用户的访问速度。 + +## 反向代理 + +反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求时就可以直接使用缓存进行响应。 + +## 本地缓存 + +使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。 + +## 分布式缓存 + +使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。 + +相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。 + +不仅如此,服务器集群都可以访问分布式缓存。而本地缓存需要在服务器集群之间进行同步,实现和性能开销上都非常大。 + +## 数据库缓存 + +MySQL 等数据库管理系统具有自己的查询缓存机制来提高 SQL 查询效率。 + +# 四、CDN + +内容分发网络(Content distribution network,CDN)是一种通过互连的网络系统,利用更靠近用户的服务器更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。 + +CDN 主要有以下优点: + +- 更快地将数据分发给用户; +- 通过部署多台服务器,从而提高系统整体的带宽性能; +- 多台服务器可以看成是一种冗余机制,从而具有高可用性。 + +

+ +参考资料: + +- [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF) +- [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/) + +# 五、缓存问题 + +## 缓存穿透 + +指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。 + +解决方案: + +- 对这些不存在的数据缓存一个空数据; +- 对这类请求进行过滤。 + +## 缓存雪崩 + +指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都去到达数据库。 + +在存在缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。 + +解决方案: + +- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现; +- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。 +- 也可以在进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。 + +## 缓存一致性 + +缓存一致性要求数据更新的同时缓存数据也能够实时更新。 + +解决方案: + +- 在数据更新的同时立即去更新缓存; +- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。 + +要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。 + +# 六、数据分布 + +## 哈希分布 + +哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。 + +传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。 + +## 顺序分布 + +将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。 + +顺序分布相比于哈希分布的主要优点如下: + +- 能保持数据原有的顺序; +- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。 + +参考资料: + +- 大规模分布式存储系统 + +# 七、一致性哈希 + +Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据失效的问题。 + +## 基本原理 + +将哈希空间 [0, 2n-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。 + +

+ +一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。 + +

+ +## 虚拟节点 + +上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。 + +数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得大,那么虚拟节点在哈希环上分布的均匀性就会比原来的真是节点好,从而使得数据分布也更加均匀。 + +参考资料: + +- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849) diff --git a/notes/计算机操作系统.md b/notes/计算机操作系统.md index 52c74dd7..6bfb559c 100644 --- a/notes/计算机操作系统.md +++ b/notes/计算机操作系统.md @@ -1,7 +1,7 @@ * [一、概述](#一概述) - * [操作系统基本特征](#操作系统基本特征) - * [操作系统基本功能](#操作系统基本功能) + * [基本特征](#基本特征) + * [基本功能](#基本功能) * [系统调用](#系统调用) * [大内核和微内核](#大内核和微内核) * [中断分类](#中断分类) @@ -23,6 +23,7 @@ * [段页式](#段页式) * [分页与分段的比较](#分页与分段的比较) * [五、设备管理](#五设备管理) + * [磁盘结构](#磁盘结构) * [磁盘调度算法](#磁盘调度算法) * [六、链接](#六链接) * [编译系统](#编译系统) @@ -35,7 +36,7 @@ # 一、概述 -## 操作系统基本特征 +## 基本特征 ### 1. 并发 @@ -63,7 +64,7 @@ 异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 -## 操作系统基本功能 +## 基本功能 ### 1. 进程管理 @@ -152,19 +153,27 @@ Linux 的系统调用主要有以下这些: 一个进程中可以有多个线程,它们共享进程资源。 +QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 +

### 3. 区别 -- 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 +(一)拥有资源 -- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。 +进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 -- 系统开销:由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 +(二)调度 -- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。 +线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换。 -举例:QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 +(三)系统开销 + +由于创建或撤销进程时,系统都要为之分配或回收资源,如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 + +(四)通信方面 + +进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信。 ## 进程状态的切换 @@ -211,7 +220,10 @@ Linux 的系统调用主要有以下这些: 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 -时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 +时间片轮转算法的效率和时间片的大小有很大关系: + +- 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 +- 而如果时间片过长,那么实时性就不能得到保证。

@@ -293,7 +305,7 @@ void P2() { 为了同步生产者和消费者的行为,需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计,这里需要使用两个信号量:empty 记录空缓冲区的数量,full 记录满缓冲区的数量。其中,empty 信号量是在生产者进程中使用,当 empty 不为 0 时,生产者才可以放入物品;full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。 -注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,也就无法执行 up(empty) 操作,empty 永远都为 0,那么生产者和消费者就会一直等待下去,造成死锁。 +注意,不能先对缓冲区进行加锁,再测试信号量。也就是说,不能先执行 down(mutex) 再执行 down(empty)。如果这么做了,那么可能会出现这种情况:生产者对缓冲区加锁后,执行 down(empty) 操作,发现 empty = 0,此时生产者睡眠。消费者不能进入临界区,因为生产者对缓冲区加锁了,消费者就无法执行 up(empty) 操作,empty 永远都为 0,导致生产者永远等待下,不会释放锁,消费者因此也会永远等待下去。 ```c #define N 100 @@ -303,7 +315,7 @@ semaphore empty = N; semaphore full = 0; void producer() { - while(TRUE){ + while(TRUE) { int item = produce_item(); down(&empty); down(&mutex); @@ -314,7 +326,7 @@ void producer() { } void consumer() { - while(TRUE){ + while(TRUE) { down(&full); down(&mutex); int item = remove_item(); @@ -531,7 +543,7 @@ int pipe(int fd[2]); 它具有以下限制: -- 只支持半双工通信(单向传输); +- 只支持半双工通信(单向交替传输); - 只能在父子进程中使用。

@@ -696,7 +708,7 @@ FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 -为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 +为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的页时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。 @@ -704,12 +716,11 @@ FIFO 常用于客户-服务器应用程序中,FIFO 用作汇聚点,在客户 ## 分页系统地址映射 -- 内存管理单元(MMU):管理着地址空间和物理内存的转换。 -- 页表(Page table):页(地址空间)和页框(物理内存空间)的映射表。例如下图中,页表的第 0 个表项为 010,表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。 +内存管理单元(MMU)管理着地址空间和物理内存的转换,其中的页表(Page table)存储着页(程序地址空间)和页框(物理内存空间)的映射表。 -下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位。因此对于虚拟地址(0010 000000000100),前 4 位是用来存储页面号,而后 12 位存储在页中的偏移量。 +下图的页表存放着 16 个页,这 16 个页需要用 4 个比特位来进行索引定位,也就是存储页面号,剩下 12 个比特位存储偏移量。 -(0010 000000000100)根据前 4 位得到页号为 2,读取表项内容为(110 1),它的前 3 为为页框号,最后 1 位表示该页在内存中。最后映射得到物理内存地址为(110 000000000100)。 +例如对于虚拟地址(0010 000000000100),前 4 位是存储页面号 2,读取表项内容为(110 1)。该页在内存中,并且页框的地址为 (110 000000000100)。

@@ -817,11 +828,22 @@ FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问 # 五、设备管理 +## 磁盘结构 + +- 盘面(Platter):一个磁盘有多个盘面; +- 磁道(Track):盘面上的圆形带状区域,一个盘面可以有多个磁道; +- 扇区(Track Sector):磁道上的一个弧段,一个磁道可以有多个扇区,它是最小的物理储存单位,目前主要有 512 bytes 与 4 K 两种大小; +- 磁头(Head):与盘面非常接近,能够将盘面上的磁场转换为电信号(读),或者将电信号转换为盘面的磁场(写); +- 制动手臂(Actuator arm):用于在磁道之间移动磁头; +- 主轴(Spindle):使整个盘面转动。 + +

+ ## 磁盘调度算法 读写一个磁盘块的时间的影响因素有: -- 旋转时间(主轴旋转磁盘,使得磁头移动到适当的扇区上) +- 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上) - 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上) - 实际的数据传输时间 @@ -922,8 +944,9 @@ gcc -o hello hello.c - Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014. - 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001. - Bryant, R. E., & O’Hallaron, D. R. (2004). 深入理解计算机系统. +- 史蒂文斯. UNIX 环境高级编程 [M]. 人民邮电出版社, 2014. - [Operating System Notes](https://applied-programming.github.io/Operating-Systems-Notes/) -- [进程间的几种通信方式](http://blog.csdn.net/yufaw/article/details/7409596) - [Operating-System Structures](https://www.cs.uic.edu/\~jbell/CourseNotes/OperatingSystems/2_Structures.html) - [Processes](http://cse.csusb.edu/tongyu/courses/cs460/notes/process.php) - [Inter Process Communication Presentation[1]](https://www.slideshare.net/rkolahalam/inter-process-communication-presentation1) +- [Decoding UCS Invicta – Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1) diff --git a/notes/计算机网络.md b/notes/计算机网络.md index f1460844..e80ba7f8 100644 --- a/notes/计算机网络.md +++ b/notes/计算机网络.md @@ -37,16 +37,16 @@ * [TCP 首部格式](#tcp-首部格式) * [TCP 的三次握手](#tcp-的三次握手) * [TCP 的四次挥手](#tcp-的四次挥手) - * [TCP 滑动窗口](#tcp-滑动窗口) * [TCP 可靠传输](#tcp-可靠传输) + * [TCP 滑动窗口](#tcp-滑动窗口) * [TCP 流量控制](#tcp-流量控制) * [TCP 拥塞控制](#tcp-拥塞控制) * [六、应用层](#六应用层) * [域名系统](#域名系统) * [文件传送协议](#文件传送协议) + * [动态主机配置协议](#动态主机配置协议) * [远程登录协议](#远程登录协议) * [电子邮件协议](#电子邮件协议) - * [动态主机配置协议](#动态主机配置协议) * [常用端口](#常用端口) * [Web 页面请求过程](#web-页面请求过程) * [参考资料](#参考资料) @@ -91,7 +91,7 @@ 每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。 -考虑在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。 +在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。 ## 时延 @@ -109,11 +109,11 @@ ### 2. 传播时延 -电磁波在信道中传播一定的距离需要花费的时间,电磁波传播速度接近光速。 +电磁波在信道中传播所需要花费的时间,电磁波传播的速度接近光速。

-其中 l 表示信道长度,v 表示电磁波在信道上的传播速率。 +其中 l 表示信道长度,v 表示电磁波在信道上的传播速度。 ### 3. 处理时延 @@ -135,7 +135,7 @@ - **网络层** :为主机间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。 -- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的节点提供服务。数据链路层把网络层传来的分组封装成帧。 +- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供服务。数据链路层把网络层传下来的分组封装成帧。 - **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。 @@ -153,7 +153,7 @@ 它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。 -现在的 TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。 +TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。

@@ -314,7 +314,7 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。 -可以按照网络拓结构扑对局域网进行分类: +可以按照网络拓扑结构对局域网进行分类:

@@ -322,9 +322,9 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 以太网是一种星型拓扑结构局域网。 -早期使用集线器进行连接。集线器是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离。之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。 +早期使用集线器进行连接,集线器是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。 -目前以太网使用交换机替代了集线器。交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。 +目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。 以太网帧格式: @@ -337,7 +337,9 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 ## 交换机* -交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。正是由于这种自学习能力,因此交换机是一种即插即可即用设备,不需要网络管理员手动配置交换表内容。 +交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。 + +正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。 下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧。主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1,就发送数据帧到接口 1,同时交换机添加主机 B 到接口 3 的映射。 @@ -345,9 +347,11 @@ MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标 ## 虚拟局域网 -虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 +虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。 -使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊端口被设置为干线端口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。 +例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。 + +使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。

@@ -411,12 +415,14 @@ IP 地址 ::= {< 网络号 >, < 主机号 >} ### 2. 子网划分 -通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。注意,外部网络看不到子网的存在。 +通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。 IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >} 要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。 +注意,外部网络看不到子网的存在。 + ### 3. 无分类 无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。 @@ -441,7 +447,7 @@ ARP 实现由 IP 地址得到 MAC 地址。

-每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到硬件地址的映射表。 +每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。 如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。 @@ -461,12 +467,14 @@ ICMP 报文分为差错报告报文和询问报文。 Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。 -Ping 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报。 +Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。 ### 2. Traceroute Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。 +Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。 + - 源主机向目的主机发送一连串的 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 终点不可达差错报告报文。 @@ -614,11 +622,11 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。 - 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。 -- A 向 B 发送连接请求报文段,SYN=1,ACK=0,选择一个初始的序号 x。 +- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。 -- B 收到连接请求报文段,如果同意建立连接,则向 A 发送连接确认报文段,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 +- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。 -- A 收到 B 的连接确认报文段后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 +- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。 - B 收到 A 的确认后,连接建立。 @@ -634,11 +642,11 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。 以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。 -- A 发送连接释放报文段,FIN=1。 +- A 发送连接释放报文,FIN=1。 - B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。 -- 当 B 不再需要连接时,发送连接释放请求报文段,FIN=1。 +- 当 B 不再需要连接时,发送连接释放报文,FIN=1。 - A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。 @@ -652,19 +660,9 @@ BGP 只能寻找一条比较好的路由,而不是最佳路由。 客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由: -- 确保最后一个确认报文段能够到达。如果 B 没收到 A 发送来的确认报文段,那么就会重新发送连接释放请求报文段,A 等待一段时间就是为了处理这种情况的发生。 +- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。 -- 等待一段时间是为了让本连接持续时间内所产生的所有报文段都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文段。 - -## TCP 滑动窗口 - -

- -窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 - -发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 - -接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 +- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。 ## TCP 可靠传输 @@ -680,6 +678,16 @@ TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文 其中 RTTd 为偏差。 +## TCP 滑动窗口 + +窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。 + +发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。 + +接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。 + +

+ ## TCP 流量控制 流量控制是为了控制发送方发送速率,保证接收方来得及接收。 @@ -717,7 +725,9 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M2,则 M3 丢失,立即重传 M3。 -在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。 +在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。 + +慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。

@@ -725,23 +735,23 @@ TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、 ## 域名系统 -DNS 是一个分布式数据库,提供了主机名和 IP 地址之间的转换。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。 +DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。 -域名具有层次结构,从上到下依次为:根域名、顶级域名、第二级域名。 +域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。

DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输: -- 因为 UDP 最大只支持 512 字节的数据,如果返回的响应超过的 512 字节就改用 TCP 进行传输。 -- 区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据,区域传送需要使用 TCP 进行传输。 +- 如果返回的响应超过的 512 字节就改用 TCP 进行传输(UDP 最大只支持 512 字节的数据)。 +- 区域传送需要使用 TCP 进行传输(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。 ## 文件传送协议 FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件: -- 控制连接:服务器以打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。 -- 数据连接:用来传送一个文件。 +- 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。 +- 数据连接:用来传送一个文件数据。 根据数据连接是否是服务器端主动建立,FTP 有主动和被动两种模式: @@ -755,6 +765,21 @@ FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件: 主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。 +## 动态主机配置协议 + +DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。 + +DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。 + +DHCP 工作过程如下: + +1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。 +2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。 +3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。 +4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。 + +

+ ## 远程登录协议 TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。 @@ -769,34 +794,19 @@ TELNET 可以适应许多计算机和操作系统的差异,例如不同操作

-### 1. POP3 - -POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 - -### 2. IMAP - -IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 - -### 3. SMTP +### 1. SMTP SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。

-## 动态主机配置协议 +### 2. POP3 -DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。 +POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。 -DHCP 工作过程如下: +### 3. IMAP -1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。 -2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。 -3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。 -4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。 - -

- -如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。 +IMAP 协议中客户端和服务器上的邮件保持同步,如果不去手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。 ## 常用端口 @@ -879,7 +889,9 @@ DHCP 工作过程如下: - JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014. - W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006. - [Active vs Passive FTP Mode: Which One is More Secure?](https://securitywing.com/active-vs-passive-ftp-mode/) -- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined) +- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined) +- [Traceroute](https://zh.wikipedia.org/wiki/Traceroute) +- [ping](https://zh.wikipedia.org/wiki/Ping) - [How DHCP works and DHCP Interview Questions and Answers](http://webcache.googleusercontent.com/search?q=cache:http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html) - [What is process of DORA in DHCP?](https://www.quora.com/What-is-process-of-DORA-in-DHCP) - [What is DHCP Server ?](https://tecadmin.net/what-is-dhcp-server/) diff --git a/notes/重构.md b/notes/重构.md index 9a3242ba..b5a834e0 100644 --- a/notes/重构.md +++ b/notes/重构.md @@ -107,6 +107,7 @@ * [10. 塑造模板函数](#10-塑造模板函数) * [11. 以委托取代继承](#11-以委托取代继承) * [12. 以继承取代委托](#12-以继承取代委托) +* [重构练习](#重构练习) * [参考资料](#参考资料) @@ -1415,6 +1416,10 @@ public Manager(String name, String id, int grade) { 让委托类继承受托类。 +# 重构练习 + +- [Refactoring Kata](https://github.com/aikin/refactoring-kata) + # 参考资料 - MartinFowler, 福勒, 贝克, 等. 重构: 改善既有代码的设计 [M]. 电子工业出版社, 2011. diff --git a/notes/集群.md b/notes/集群.md new file mode 100644 index 00000000..717ae637 --- /dev/null +++ b/notes/集群.md @@ -0,0 +1,206 @@ + +* [一、负载均衡](#一负载均衡) + * [算法实现](#算法实现) + * [转发实现](#转发实现) +* [二、集群下的 Session 管理](#二集群下的-session-管理) + * [Sticky Session](#sticky-session) + * [Session Replication](#session-replication) + * [Session Server](#session-server) + + + +# 一、负载均衡 + +集群中的应用服务器通常被设计成无状态,用户可以请求任何一个节点(应用服务器)。 + +负载均衡器会根据集群中每个节点的负载情况,将用户请求转发到合适的节点上。 + +负载均衡器可以用来实现高可用以及伸缩性: + +- 高可用:当某个节点故障时,负载均衡器不会将用户请求转发到该节点上,从而保证所有服务持续可用; +- 伸缩性:可以很容易地添加移除节点。 + +负载均衡运行过程包含两个部分: + +1. 根据负载均衡算法得到请求转发的节点; +2. 将请求进行转发; + +## 算法实现 + +### 1. 轮询(Round Robin) + +轮询算法把每个请求轮流发送到每个服务器上。 + +下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。 + +

+ +该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。 + +

+ +### 2. 加权轮询(Weighted Round Robbin) + +加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值,性能高的服务器分配更高的权值。 + +例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。 + +

+ +### 3. 最少连接(least Connections) + +由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。 + +例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。 + +

+ +最少连接算法就是将请求发送给当前最少连接数的服务器上。 + +例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。 + +

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

+ +### 5. 随机算法(Random) + +把请求随机发送到服务器上。 + +和轮询算法类似,该算法比较适合服务器性能差不多的场景。 + +

+ +### 6. 源地址哈希法 (IP Hash) + +源地址哈希通过对客户端 IP 计算哈希值之后,再对服务器数量取模得到目标服务器的序号。 + +可以保证同一 IP 的客户端的请求会转发到同一台服务器上,用来实现会话粘滞(Sticky Session) + +

+ +## 转发实现 + +### 1. HTTP 重定向 + +HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服务器的 IP 地址之后,将该地址写入 HTTP 重定向报文中,状态码为 302。客户端收到重定向报文之后,需要重新向服务器发起请求。 + +缺点: + +- 需要两次请求,因此访问延迟比较高; +- HTTP 负载均衡器处理能力有限,会限制集群的规模。 + +该负载均衡转发的缺点比较明显,实际场景中很少使用它。 + +

+ +### 2. DNS 域名解析 + +在 DNS 解析域名的同时使用负载均衡算法计算服务器地址。 + +优点: + +- DNS 能够根据地理位置进行域名解析,返回离用户最近的服务器 IP 地址。 + +缺点: + +- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效。 + +大型网站基本使用了 DNS 做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。也就是说,域名解析的结果为内部的负载均衡服务器 IP 地址。 + +

+ +### 3. 反向代理服务器 + +首先了解一下正向代理与反向代理的区别: + +- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端; +- 反向代理:发生在服务器端,用户不知道代理的存在。 + +反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来做为负载均衡服务器。 + +在这种负载均衡转发方式下,客户端不直接请求源服务器,因此源服务器不需要外部 IP 地址,而反向代理需要配置内部和外部两套 IP 地址。 + +优点: + +- 与其它功能集成在一起,部署简单。 + +缺点: + +- 所有请求和响应都需要经过反向代理服务器,它可能会成为性能瓶颈。 + +### 4. 网络层 + +在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址,并修改请求数据包的目的 IP 地址,最后进行转发。 + +源服务器返回的响应也需要经过负载均衡服务器,通常是让负载均衡服务器同时作为集群的网关服务器来实现。 + +优点: + +- 在内核进程中进行处理,性能比较高。 + +缺点: + +- 和反向代理一样,所有的请求和响应都经过负载均衡服务器,会成为性能瓶颈。 + +### 5. 链路层 + +在链路层根据负载均衡算法计算源服务器的 MAC 地址,并修改请求数据包的目的 MAC 地址,并进行转发。 + +通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,直接转发给客户端,避免了负载均衡服务器的成为瓶颈。这是一种三角传输模式,被称为直接路由,对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。 + +这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVS(Linux Virtual Server)。 + +参考: + +- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms) +- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#) + +# 二、集群下的 Session 管理 + +一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作。 + +## Sticky Session + +需要配置负载均衡器,使得一个用户的所有请求都路由到同一个服务器,这样就可以把用户的 Session 存放在该服务器中。 + +缺点: + +- 当服务器宕机时,将丢失该服务器上的所有 Session。 + +

+ +## Session Replication + +在服务器之间进行 Session 同步操作,每个服务器都有所有用户的 Session 信息,因此用户可以向任何一个服务器进行请求。 + +缺点: + +- 占用过多内存; +- 同步过程占用网络带宽以及服务器处理器时间。 + + +

+ +## Session Server + +使用一个单独的服务器存储 Session 数据,可以使用 MySQL,也使用 Redis 或者 Memcached 这种内存型数据库。 + +优点: + +- 为了使得大型网站具有伸缩性,集群中的应用服务器通常需要保持无状态,那么应用服务器不能存储用户的会话信息。Session Server 将用户的会话信息单独进行存储,从而保证了应用服务器的无状态。 + +缺点: + +- 需要去实现存取 Session 的代码。 + +

+ +参考: + +- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/) + diff --git a/other/Group.md b/other/Group.md new file mode 100644 index 00000000..fee7b8bf --- /dev/null +++ b/other/Group.md @@ -0,0 +1,15 @@ +创建交流群的主要目的是为了给大家提供一个交流平台,方便大家在学习的过程中互相讨论。 + +这个交流群不是一个笔者的问题回答群,我更希望大家能够愿意积极回答,我相信提问和回答的过程都可以帮助大家对知识的掌握程度。 + +因为笔者白天要上班,因此不能及时进行回复,大部分时间会处于潜水状态。 + +至于交流群和 Issue 有什么区别,主要是两方面:一是交流群实时性高一些,二是交流群会更活跃一些。 + +另外,Issue 主要是用来发布一些项目中的错误和一些改进建议,当然也可以发布一些可以讨论的问题。 + +交流群可以讨论的内容比较广,例如在阅读本项目过程中不理解的地方可以在交流群中寻求别人的帮助、新技术的讨论、招聘信息、学习和工作的感受等等。 + +交流群不讨论政治,不讨论有争议性的话题,不发表仇视言论,不传播谣言,不发布广告(招聘信息之类的可以)。 + +

diff --git a/other/README.md b/other/README.md new file mode 100644 index 00000000..39876f02 --- /dev/null +++ b/other/README.md @@ -0,0 +1,3 @@ +- 其他人添加的全新内容 +- 主页 README 引用的图片 +- 微信群描述文件 \ No newline at end of file diff --git a/other/group.jpg b/other/group.jpg new file mode 100644 index 00000000..af212841 Binary files /dev/null and b/other/group.jpg differ diff --git a/other/leetcode 总结.md b/other/leetcode 总结.md new file mode 100644 index 00000000..82ff61c7 --- /dev/null +++ b/other/leetcode 总结.md @@ -0,0 +1,37 @@ +# LeetCode 面试必备 + - 💪 就是干!如果你觉得有帮助请点个star,谢谢! + +> **欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远** + +## LeetCode 习题集合 + +* [LeetCode 解题集合](https://github.com/apachecn/LeetCode/tree/master/docs/Leetcode_Solutions) + + +## 模版要求 + +> 提交PR基本要求(满足任意一种即可) + +* 1. 不一样的思路 +* 2. 优化时间复杂度和空间复杂度,或者解决题目的Follow up +* 3. 有意义的简化代码 +* 4. 未提交过的题目 + +> **案例模版** + +[模版:007. Reverse Integer 反转整数](https://github.com/apachecn/LeetCode/tree/master/docs/Leetcode_Solutions/007._Reverse_Integer.md) + + +## 项目贡献者 + +> 项目发起人 + +* [@Lisanaaa](https://github.com/Lisanaaa) +* [@片刻](https://github.com/jiangzhonglian) + +> 贡献者(欢迎大家来追加) + +* [@Lisanaaa](https://github.com/Lisanaaa) +* [@片刻](https://github.com/jiangzhonglian) +* [@小瑶](https://github.com/chenyyx) + diff --git a/other/sql 经典练习题.sql b/other/sql 经典练习题.sql index 72965fa2..a4b4f964 100644 --- a/other/sql 经典练习题.sql +++ b/other/sql 经典练习题.sql @@ -430,3 +430,54 @@ where SCORE.SNO = STUDENT.SNO and SSEX = '男' and CNO = ( +-- 46、使用游标方式来同时查询每位同学的名字,他所选课程及成绩。 + +declare + cursor student_cursor is + select S.SNO,S.SNAME,C.CNAME,SC.DEGREE as DEGREE + from STUDENT S, COURSE C, SCORE SC + where S.SNO=SC.SNO + and SC.CNO=C.CNO; + + student_row student_cursor%ROWTYPE; + +begin + open student_cursor; + loop + fetch student_cursor INTO student_row; + exit when student_cursor%NOTFOUND; + dbms_output.put_line( student_row.SNO || '' || + +student_row.SNAME|| '' || student_row.CNAME || '' || + +student_row.DEGREE); + end loop; + close student_cursor; +END; +/ + + +-- 47、 声明触发器指令,每当有同学转换班级时执行触发器显示当前和之前所在班级。 + +CREATE OR REPLACE TRIGGER display_class_changes +AFTER DELETE OR INSERT OR UPDATE ON student +FOR EACH ROW +WHEN (NEW.sno > 0) + +BEGIN + + dbms_output.put_line('Old class: ' || :OLD.class); + dbms_output.put_line('New class: ' || :NEW.class); +END; +/ + + +Update student +set class=95031 +where sno=109; + + +-- 48、 删除已设置的触发器指令 + +DROP TRIGGER display_class_changes; + diff --git a/pics/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png b/pics/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png new file mode 100644 index 00000000..0e56341c Binary files /dev/null and b/pics/011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png differ diff --git a/pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png b/pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png new file mode 100644 index 00000000..26d62519 Binary files /dev/null and b/pics/026d3cb4-67f7-4a83-884d-8032f57ec446.png differ diff --git a/pics/09b52bcb-88ba-4e36-8244-b375f16ad116.jpg b/pics/09b52bcb-88ba-4e36-8244-b375f16ad116.jpg new file mode 100644 index 00000000..146336aa Binary files /dev/null and b/pics/09b52bcb-88ba-4e36-8244-b375f16ad116.jpg differ diff --git a/pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg b/pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg new file mode 100644 index 00000000..cbba7f36 Binary files /dev/null and b/pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg differ diff --git a/pics/27ff9548-edb6-4465-92c8-7e6386e0b185.png b/pics/27ff9548-edb6-4465-92c8-7e6386e0b185.png new file mode 100644 index 00000000..1aee414c Binary files /dev/null and b/pics/27ff9548-edb6-4465-92c8-7e6386e0b185.png differ diff --git a/pics/280f7728-594f-4811-a03a-fa8d32c013da.png b/pics/280f7728-594f-4811-a03a-fa8d32c013da.png new file mode 100644 index 00000000..526b6847 Binary files /dev/null and b/pics/280f7728-594f-4811-a03a-fa8d32c013da.png differ diff --git a/pics/2858f8ad-aedb-45a5-a706-e98c96d690fa.jpg b/pics/2858f8ad-aedb-45a5-a706-e98c96d690fa.jpg new file mode 100644 index 00000000..d4171d08 Binary files /dev/null and b/pics/2858f8ad-aedb-45a5-a706-e98c96d690fa.jpg differ diff --git a/pics/49495c95-52e5-4c9a-b27b-92cf235ff5ec.png b/pics/49495c95-52e5-4c9a-b27b-92cf235ff5ec.png new file mode 100644 index 00000000..662d99c3 Binary files /dev/null and b/pics/49495c95-52e5-4c9a-b27b-92cf235ff5ec.png differ diff --git a/pics/6539b9a4-2b24-4d10-8c94-2eb5aba1e296.png b/pics/6539b9a4-2b24-4d10-8c94-2eb5aba1e296.png new file mode 100644 index 00000000..053a3dc6 Binary files /dev/null and b/pics/6539b9a4-2b24-4d10-8c94-2eb5aba1e296.png differ diff --git a/pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg b/pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg new file mode 100644 index 00000000..fc86a236 Binary files /dev/null and b/pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg differ diff --git a/pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg b/pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg new file mode 100644 index 00000000..d82f46eb Binary files /dev/null and b/pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg differ diff --git a/pics/71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png b/pics/71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png new file mode 100644 index 00000000..4e99159f Binary files /dev/null and b/pics/71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png differ diff --git a/pics/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg b/pics/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg new file mode 100644 index 00000000..fd13a137 Binary files /dev/null and b/pics/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg differ diff --git a/pics/7e873b60-44dc-4911-b080-defd5b8f0b49.png b/pics/7e873b60-44dc-4911-b080-defd5b8f0b49.png new file mode 100644 index 00000000..66e26e80 Binary files /dev/null and b/pics/7e873b60-44dc-4911-b080-defd5b8f0b49.png differ diff --git a/pics/897503d0-59e3-4752-903d-529fbdb72fee.jpg b/pics/897503d0-59e3-4752-903d-529fbdb72fee.jpg new file mode 100644 index 00000000..e0935e57 Binary files /dev/null and b/pics/897503d0-59e3-4752-903d-529fbdb72fee.jpg differ diff --git a/pics/926c7438-c5e1-4b94-840a-dcb24ff1dafe.png b/pics/926c7438-c5e1-4b94-840a-dcb24ff1dafe.png new file mode 100644 index 00000000..c6cdfd77 Binary files /dev/null and b/pics/926c7438-c5e1-4b94-840a-dcb24ff1dafe.png differ diff --git a/pics/JNI-Java-Native-Interface.jpg b/pics/JNI-Java-Native-Interface.jpg new file mode 100644 index 00000000..28b45e2e Binary files /dev/null and b/pics/JNI-Java-Native-Interface.jpg differ diff --git a/pics/NP4z3i8m38Ntd28NQ4_0KCJ2q044Oez.png b/pics/NP4z3i8m38Ntd28NQ4_0KCJ2q044Oez.png new file mode 100644 index 00000000..3819cda2 Binary files /dev/null and b/pics/NP4z3i8m38Ntd28NQ4_0KCJ2q044Oez.png differ diff --git a/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png new file mode 100644 index 00000000..124ec977 Binary files /dev/null and b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png differ diff --git a/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png new file mode 100644 index 00000000..7a202d35 Binary files /dev/null and b/pics/SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png differ diff --git a/pics/a58e294a-615d-4ea0-9fbf-064a6daec4b2.png b/pics/a58e294a-615d-4ea0-9fbf-064a6daec4b2.png new file mode 100644 index 00000000..fdefb823 Binary files /dev/null and b/pics/a58e294a-615d-4ea0-9fbf-064a6daec4b2.png differ diff --git a/pics/c5f611f0-fd5c-4158-9003-278141136e6e.jpg b/pics/c5f611f0-fd5c-4158-9003-278141136e6e.jpg new file mode 100644 index 00000000..473091be Binary files /dev/null and b/pics/c5f611f0-fd5c-4158-9003-278141136e6e.jpg differ diff --git a/pics/c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png b/pics/c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png new file mode 100644 index 00000000..fea5821b Binary files /dev/null and b/pics/c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png differ diff --git a/pics/ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg b/pics/ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg new file mode 100644 index 00000000..73b3d739 Binary files /dev/null and b/pics/ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg differ diff --git a/pics/docker-filesystems-busyboxrw.png b/pics/docker-filesystems-busyboxrw.png new file mode 100644 index 00000000..c12710af Binary files /dev/null and b/pics/docker-filesystems-busyboxrw.png differ diff --git a/pics/ea2304ce-268b-4238-9486-4d8f8aea8ca4.png b/pics/ea2304ce-268b-4238-9486-4d8f8aea8ca4.png new file mode 100644 index 00000000..59b54d2f Binary files /dev/null and b/pics/ea2304ce-268b-4238-9486-4d8f8aea8ca4.png differ diff --git a/pics/f94389e9-55b1-4f49-9d37-00ed05900ae0.png b/pics/f94389e9-55b1-4f49-9d37-00ed05900ae0.png new file mode 100644 index 00000000..49817697 Binary files /dev/null and b/pics/f94389e9-55b1-4f49-9d37-00ed05900ae0.png differ diff --git a/pics/faecea49-9974-40db-9821-c8636137df61.jpg b/pics/faecea49-9974-40db-9821-c8636137df61.jpg new file mode 100644 index 00000000..1b236b6e Binary files /dev/null and b/pics/faecea49-9974-40db-9821-c8636137df61.jpg differ