fix
@ -1,84 +0,0 @@
|
||||
| Ⅰ | Ⅱ | Ⅲ | Ⅳ | Ⅴ | Ⅵ | Ⅶ | Ⅷ | Ⅸ | Ⅹ |
|
||||
| :----------------------------: | :--------------------------------------: | :------------------------: | :----------------------------------: | :----------------------------------------: | :---------------------------: | :------------------------------: | :--------------------------: | :------------------------------------------------: | :----------------------: |
|
||||
| 算法[: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-后记) |
|
||||
|
||||
|
||||
#### About
|
||||
|
||||
本仓库主要是根据计算机经典书籍以及官方技术文档进行总结的学习笔记,希望对大家有所帮助。
|
||||
|
||||
学习笔记不是从网上到处拼凑而来,除了少部分引用书上和技术文档的原文,其余都是笔者的原创。在您引用本仓库内容或者对内容进行修改演绎时,请遵循文末的开源协议,谢谢。
|
||||
|
||||
#### How To Contribute
|
||||
|
||||
笔记内容是笔者一个字一个字打上去的,难免会有一些笔误,如果发现笔误可直接对相应文档进行编辑修改。
|
||||
|
||||
如果想要提交一个仓库现在还没有的全新内容,可以先将相应的文档放到 other 目录下。
|
||||
|
||||
欢迎在 Issue 中提交对本仓库的改进建议~
|
||||
|
||||
#### BookList
|
||||
|
||||
本仓库参考的书目:[BOOKLIST](https://github.com/CyC2018/Interview-Notebook/blob/master/BOOKLIST.md)。
|
||||
|
||||
#### Typesetting
|
||||
|
||||
笔记内容按照 [中文文案排版指北](https://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 同步。
|
||||
|
||||
进行 Markdown 文档转换是因为 Github 使用的 GFM 不支持 MathJax 公式和 TOC 标记,所以需要替换 MathJax 公式为 CodeCogs 的云服务和重新生成 TOC 目录。
|
||||
|
||||
笔者将自己实现文档转换功能提取出来,方便大家在需要将本地 Markdown 上传到 Github,或者制作项目 README 文档时生成目录时使用:[GFM-Converter](https://github.com/CyC2018/GFM-Converter)。
|
||||
|
||||
#### Logo
|
||||
|
||||
Power by [logomakr](https://logomakr.com/).
|
||||
|
||||
#### Statement
|
||||
|
||||
本仓库不参与商业行为,不向读者收取任何费用。(This repository is not engaging in business activities, and does not charge readers any fee.)
|
||||
|
||||
#### Acknowledgements
|
||||
|
||||
感谢以下人员对本仓库做出的贡献,当然不仅仅只有这些贡献者,这里就不一一列举了。如果你希望被添加到这个名单中,并且提交过 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>
|
||||
<a href="https://github.com/yanglbme">
|
||||
<img src="https://avatars1.githubusercontent.com/u/21008209?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>
|
@ -1,15 +0,0 @@
|
||||
<img width="150px" src="_media/LogoMakr_1J56bI.png">
|
||||
|
||||
# CS-Notes
|
||||
|
||||
> Computer Sicence Learning Notes.
|
||||
|
||||
- Algorithm
|
||||
- Computer Networks
|
||||
- Operating System
|
||||
- ...
|
||||
|
||||
[GitHub](https://github.com/CyC2018/CS-Notes)
|
||||
|
||||
<!-- ![color](#107ea485) -->
|
||||
|
Before Width: | Height: | Size: 56 KiB |
Before Width: | Height: | Size: 2.3 KiB |
@ -1,6 +0,0 @@
|
||||
- Translations
|
||||
- [:uk: English](/)
|
||||
- [:cn: 中文](/zh-cn/)
|
||||
- [:de: Deutsch](/de-de/)
|
||||
- [:es: Spanish](/es/)
|
||||
- [:ru: Russian](/ru/)
|
@ -1,53 +0,0 @@
|
||||
- :pencil2: 算法
|
||||
|
||||
- [剑指 Offer 题解](notes/剑指%20offer%20题解.md)
|
||||
- [Leetcode 题解](notes/Leetcode%20题解)
|
||||
- [算法](notes/算法.md)
|
||||
|
||||
- :computer: 操作系统
|
||||
|
||||
- [计算机操作系统](notes/计算机操作系统.md)
|
||||
- [Linux](notes/Linux.md)
|
||||
|
||||
- :cloud: 网络
|
||||
|
||||
- [计算机网络](notes/计算机网络.md)
|
||||
- [HTTP](notes/HTTP.md)
|
||||
- [Sockets](notes/Sockets.md)
|
||||
|
||||
- :couple: 面向对象
|
||||
|
||||
- [设计模式](notes/设计模式.md)
|
||||
- [面向对象思想](notes/面向对象思想.md)
|
||||
|
||||
- :floppy_disk: 数据库
|
||||
|
||||
- [数据库系统原理](notes/数据库系统原理.md)
|
||||
- [SQL](notes/SQL.md)
|
||||
- [Leetcode-Database 题解](notes/Leetcode-Database%20题解.md)
|
||||
- [MySQL](notes/MySQL.md)
|
||||
- [Redis](notes/Redis.md)
|
||||
|
||||
- :coffee: Java
|
||||
|
||||
- [Java 基础](notes/Java%20基础.md)
|
||||
- [Java 容器](notes/Java%20容器.md)
|
||||
- [Java 并发](notes/Java%20并发.md)
|
||||
- [Java 虚拟机](notes/Java%20虚拟机.md)
|
||||
- [Java I/O](notes/Java%20IO.md)
|
||||
|
||||
- :bulb: 系统设计
|
||||
|
||||
- [系统设计基础](notes/系统设计基础.md)
|
||||
- [分布式](notes/分布式.md)
|
||||
- [集群](notes/集群.md)
|
||||
- [攻击技术](notes/攻击技术.md)
|
||||
- [缓存](notes/缓存.md)
|
||||
- [消息队列](notes/消息队列.md)
|
||||
|
||||
- :hammer:工具
|
||||
|
||||
- [Git](notes/Git.md)
|
||||
- [Docker](notes/Docker.md)
|
||||
- [正则表达式](notes/正则表达式.md)
|
||||
- [构建工具](notes/构建工具.md)
|
@ -1 +0,0 @@
|
||||
# test
|
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Document</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||
<meta name="description" content="Description">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
|
||||
<link rel="stylesheet" href="//unpkg.com/docsify/lib/themes/vue.css">
|
||||
<link rel="stylesheet" href="//unpkg.com/gitalk/dist/gitalk.css">
|
||||
<script src="//unpkg.com/docsify/lib/plugins/gitalk.min.js"></script>
|
||||
<script src="//unpkg.com/gitalk/dist/gitalk.min.js"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav>
|
||||
<a href="#/">Github</a>
|
||||
</nav>
|
||||
<div id="app"></div>
|
||||
<script>
|
||||
window.$docsify = {
|
||||
name: 'CS-Notes',
|
||||
repo: '',
|
||||
loadSidebar: true,
|
||||
subMaxLevel: 1,
|
||||
coverpage: true,
|
||||
auto2top: true,
|
||||
// logo: '/_media/logomakr-3sxxzw-128x128.png'
|
||||
}
|
||||
// const gitalk = new Gitalk({
|
||||
// clientID: '0a3e1a4c29a4778f35f2',
|
||||
// clientSecret: '5bf2890d68acc32cbf8327924805a0b48e15f64c',
|
||||
// repo: 'test',
|
||||
// owner: 'CyC2018',
|
||||
// admin: ['CyC2018'],
|
||||
// distractionFreeMode: false
|
||||
// })
|
||||
</script>
|
||||
<script src="//unpkg.com/docsify/lib/docsify.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
@ -1,91 +0,0 @@
|
||||
<!-- 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/)
|
||||
- [理解 Docker(2):Docker 镜像](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)
|
||||
|
@ -1,158 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [集中式与分布式](#集中式与分布式)
|
||||
* [中心服务器](#中心服务器)
|
||||
* [工作流](#工作流)
|
||||
* [分支实现](#分支实现)
|
||||
* [冲突](#冲突)
|
||||
* [Fast forward](#fast-forward)
|
||||
* [分支管理策略](#分支管理策略)
|
||||
* [储藏(Stashing)](#储藏stashing)
|
||||
* [SSH 传输设置](#ssh-传输设置)
|
||||
* [.gitignore 文件](#gitignore-文件)
|
||||
* [Git 命令一览](#git-命令一览)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 集中式与分布式
|
||||
|
||||
Git 属于分布式版本控制系统,而 SVN 属于集中式。
|
||||
|
||||
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
|
||||
|
||||
集中式版本控制有安全性问题,当中心服务器挂了所有人都没办法工作了。
|
||||
|
||||
集中式版本控制需要连网才能工作,如果网速过慢,那么提交一个文件的会慢的无法让人忍受。而分布式版本控制不需要连网就能工作。
|
||||
|
||||
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
|
||||
|
||||
# 中心服务器
|
||||
|
||||
中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
|
||||
|
||||
Github 就是一个中心服务器。
|
||||
|
||||
# 工作流
|
||||
|
||||
<div align="center"> <img src="../pics//a1198642-9159-4d88-8aec-c3b04e7a2563.jpg"/> </div><br>
|
||||
|
||||
新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git,它属于 Git 的版本库。
|
||||
|
||||
Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master 分支以及指向分支的 HEAD 指针。
|
||||
|
||||
<div align="center"> <img src="../pics//46f66e88-e65a-4ad0-a060-3c63fe22947c.png"/> </div><br>
|
||||
|
||||
- git add files 把文件的修改添加到暂存区
|
||||
- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
|
||||
- git reset -- files 使用当前分支上的修改覆盖暂存区,用来撤销最后一次 git add files
|
||||
- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
|
||||
|
||||
<div align="center"> <img src="../pics//17976404-95f5-480e-9cb4-250e6aa1d55f.png"/> </div><br>
|
||||
|
||||
可以跳过暂存区域直接从分支中取出修改,或者直接提交修改到分支中。
|
||||
|
||||
- git commit -a 直接把所有文件的修改添加到暂存区然后执行提交
|
||||
- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
|
||||
|
||||
# 分支实现
|
||||
|
||||
使用指针将每个提交连接成一条时间线,HEAD 指针指向当前分支指针。
|
||||
|
||||
<div align="center"> <img src="../pics//fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg"/> </div><br>
|
||||
|
||||
新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
|
||||
|
||||
<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//1164a71f-413d-494a-9cc8-679fb6a2613d.jpg"/> </div><br>
|
||||
|
||||
# 冲突
|
||||
|
||||
当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。
|
||||
|
||||
<div align="center"> <img src="../pics//58e57a21-6b6b-40b6-af85-956dd4e0f55a.jpg"/> </div><br>
|
||||
|
||||
Git 会使用 <<<<<<< ,======= ,>>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
|
||||
|
||||
```
|
||||
<<<<<<< HEAD
|
||||
Creating a new branch is quick & simple.
|
||||
=======
|
||||
Creating a new branch is quick AND simple.
|
||||
>>>>>>> feature1
|
||||
```
|
||||
|
||||
# Fast forward
|
||||
|
||||
"快进式合并"(fast-farward merge),会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
|
||||
|
||||
可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
|
||||
|
||||
```
|
||||
$ git merge --no-ff -m "merge with no-ff" dev
|
||||
```
|
||||
|
||||
<div align="center"> <img src="../pics//dd78a1fe-1ff3-4bcf-a56f-8c003995beb6.jpg"/> </div><br>
|
||||
|
||||
# 分支管理策略
|
||||
|
||||
master 分支应该是非常稳定的,只用来发布新版本;
|
||||
|
||||
日常开发在开发分支 dev 上进行。
|
||||
|
||||
<div align="center"> <img src="../pics//245fd2fb-209c-4ad5-bc5e-eb5664966a0e.jpg"/> </div><br>
|
||||
|
||||
# 储藏(Stashing)
|
||||
|
||||
在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。
|
||||
|
||||
可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
|
||||
|
||||
```
|
||||
$ git stash
|
||||
Saved working directory and index state \ "WIP on master: 049d078 added the index file"
|
||||
HEAD is now at 049d078 added the index file (To restore them type "git stash apply")
|
||||
```
|
||||
|
||||
该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
|
||||
|
||||
# SSH 传输设置
|
||||
|
||||
Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
|
||||
|
||||
如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key:
|
||||
|
||||
```
|
||||
$ ssh-keygen -t rsa -C "youremail@example.com"
|
||||
```
|
||||
|
||||
然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
|
||||
|
||||
# .gitignore 文件
|
||||
|
||||
忽略以下文件:
|
||||
|
||||
- 操作系统自动生成的文件,比如缩略图;
|
||||
- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
|
||||
- 自己的敏感信息,比如存放口令的配置文件。
|
||||
|
||||
不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
|
||||
|
||||
# Git 命令一览
|
||||
|
||||
<div align="center"> <img src="../pics//7a29acce-f243-4914-9f00-f2988c528412.jpg"/> </div><br>
|
||||
|
||||
比较详细的地址: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/)
|
@ -1,891 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一 、基础概念](#一-基础概念)
|
||||
* [URL](#url)
|
||||
* [请求和响应报文](#请求和响应报文)
|
||||
* [二、HTTP 方法](#二http-方法)
|
||||
* [GET](#get)
|
||||
* [HEAD](#head)
|
||||
* [POST](#post)
|
||||
* [PUT](#put)
|
||||
* [PATCH](#patch)
|
||||
* [DELETE](#delete)
|
||||
* [OPTIONS](#options)
|
||||
* [CONNECT](#connect)
|
||||
* [TRACE](#trace)
|
||||
* [三、HTTP 状态码](#三http-状态码)
|
||||
* [1XX 信息](#1xx-信息)
|
||||
* [2XX 成功](#2xx-成功)
|
||||
* [3XX 重定向](#3xx-重定向)
|
||||
* [4XX 客户端错误](#4xx-客户端错误)
|
||||
* [5XX 服务器错误](#5xx-服务器错误)
|
||||
* [四、HTTP 首部](#四http-首部)
|
||||
* [通用首部字段](#通用首部字段)
|
||||
* [请求首部字段](#请求首部字段)
|
||||
* [响应首部字段](#响应首部字段)
|
||||
* [实体首部字段](#实体首部字段)
|
||||
* [五、具体应用](#五具体应用)
|
||||
* [连接管理](#连接管理)
|
||||
* [Cookie](#cookie)
|
||||
* [缓存](#缓存)
|
||||
* [内容协商](#内容协商)
|
||||
* [内容编码](#内容编码)
|
||||
* [范围请求](#范围请求)
|
||||
* [分块传输编码](#分块传输编码)
|
||||
* [多部分对象集合](#多部分对象集合)
|
||||
* [虚拟主机](#虚拟主机)
|
||||
* [通信数据转发](#通信数据转发)
|
||||
* [六、HTTPs](#六https)
|
||||
* [加密](#加密)
|
||||
* [认证](#认证)
|
||||
* [完整性保护](#完整性保护)
|
||||
* [HTTPs 的缺点](#https-的缺点)
|
||||
* [七、HTTP/2.0](#七http20)
|
||||
* [HTTP/1.x 缺陷](#http1x-缺陷)
|
||||
* [二进制分帧层](#二进制分帧层)
|
||||
* [服务端推送](#服务端推送)
|
||||
* [首部压缩](#首部压缩)
|
||||
* [八、HTTP/1.1 新特性](#八http11-新特性)
|
||||
* [九、GET 和 POST 比较](#九get-和-post-比较)
|
||||
* [作用](#作用)
|
||||
* [参数](#参数)
|
||||
* [安全](#安全)
|
||||
* [幂等性](#幂等性)
|
||||
* [可缓存](#可缓存)
|
||||
* [XMLHttpRequest](#xmlhttprequest)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一 、基础概念
|
||||
|
||||
## URL
|
||||
|
||||
URI 包含 URL 和 URN,目前 WEB 只有 URL 比较流行,所以见到的基本都是 URL。
|
||||
|
||||
- URI(Uniform Resource Identifier,统一资源标识符)
|
||||
- URL(Uniform Resource Locator,统一资源定位符)
|
||||
- URN(Uniform Resource Name,统一资源名称)
|
||||
|
||||
<div align="center"> <img src="../pics//urlnuri.jpg" width="600"/> </div><br>
|
||||
|
||||
## 请求和响应报文
|
||||
|
||||
### 1. 请求报文
|
||||
|
||||
<div align="center"> <img src="../pics//HTTP_RequestMessageExample.png" width=""/> </div><br>
|
||||
|
||||
### 2. 响应报文
|
||||
|
||||
<div align="center"> <img src="../pics//HTTP_ResponseMessageExample.png" width=""/> </div><br>
|
||||
|
||||
# 二、HTTP 方法
|
||||
|
||||
客户端发送的 **请求报文** 第一行为请求行,包含了方法字段。
|
||||
|
||||
## GET
|
||||
|
||||
> 获取资源
|
||||
|
||||
当前网络请求中,绝大部分使用的是 GET 方法。
|
||||
|
||||
## HEAD
|
||||
|
||||
> 获取报文首部
|
||||
|
||||
和 GET 方法一样,但是不返回报文实体主体部分。
|
||||
|
||||
主要用于确认 URL 的有效性以及资源更新的日期时间等。
|
||||
|
||||
## POST
|
||||
|
||||
> 传输实体主体
|
||||
|
||||
POST 主要用来传输数据,而 GET 主要用来获取资源。
|
||||
|
||||
更多 POST 与 GET 的比较请见第九章。
|
||||
|
||||
## PUT
|
||||
|
||||
> 上传文件
|
||||
|
||||
由于自身不带验证机制,任何人都可以上传文件,因此存在安全性问题,一般不使用该方法。
|
||||
|
||||
```html
|
||||
PUT /new.html HTTP/1.1
|
||||
Host: example.com
|
||||
Content-type: text/html
|
||||
Content-length: 16
|
||||
|
||||
<p>New File</p>
|
||||
```
|
||||
|
||||
## PATCH
|
||||
|
||||
> 对资源进行部分修改
|
||||
|
||||
PUT 也可以用于修改资源,但是只能完全替代原始资源,PATCH 允许部分修改。
|
||||
|
||||
```html
|
||||
PATCH /file.txt HTTP/1.1
|
||||
Host: www.example.com
|
||||
Content-Type: application/example
|
||||
If-Match: "e0023aa4e"
|
||||
Content-Length: 100
|
||||
|
||||
[description of changes]
|
||||
```
|
||||
|
||||
## DELETE
|
||||
|
||||
> 删除文件
|
||||
|
||||
与 PUT 功能相反,并且同样不带验证机制。
|
||||
|
||||
```html
|
||||
DELETE /file.html HTTP/1.1
|
||||
```
|
||||
|
||||
## OPTIONS
|
||||
|
||||
> 查询支持的方法
|
||||
|
||||
查询指定的 URL 能够支持的方法。
|
||||
|
||||
会返回 Allow: GET, POST, HEAD, OPTIONS 这样的内容。
|
||||
|
||||
## CONNECT
|
||||
|
||||
> 要求在与代理服务器通信时建立隧道
|
||||
|
||||
使用 SSL(Secure Sockets Layer,安全套接层)和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。
|
||||
|
||||
```html
|
||||
CONNECT www.example.com:443 HTTP/1.1
|
||||
```
|
||||
|
||||
<div align="center"> <img src="../pics//dc00f70e-c5c8-4d20-baf1-2d70014a97e3.jpg" width=""/> </div><br>
|
||||
|
||||
## TRACE
|
||||
|
||||
> 追踪路径
|
||||
|
||||
服务器会将通信路径返回给客户端。
|
||||
|
||||
发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器就会减 1,当数值为 0 时就停止传输。
|
||||
|
||||
通常不会使用 TRACE,并且它容易受到 XST 攻击(Cross-Site Tracing,跨站追踪)。
|
||||
# 三、HTTP 状态码
|
||||
|
||||
服务器返回的 **响应报文** 中第一行为状态行,包含了状态码以及原因短语,用来告知客户端请求的结果。
|
||||
|
||||
| 状态码 | 类别 | 原因短语 |
|
||||
| :---: | :---: | :---: |
|
||||
| 1XX | Informational(信息性状态码) | 接收的请求正在处理 |
|
||||
| 2XX | Success(成功状态码) | 请求正常处理完毕 |
|
||||
| 3XX | Redirection(重定向状态码) | 需要进行附加操作以完成请求 |
|
||||
| 4XX | Client Error(客户端错误状态码) | 服务器无法处理请求 |
|
||||
| 5XX | Server Error(服务器错误状态码) | 服务器处理请求出错 |
|
||||
|
||||
## 1XX 信息
|
||||
|
||||
- **100 Continue** :表明到目前为止都很正常,客户端可以继续发送请求或者忽略这个响应。
|
||||
|
||||
## 2XX 成功
|
||||
|
||||
- **200 OK**
|
||||
|
||||
- **204 No Content** :请求已经成功处理,但是返回的响应报文不包含实体的主体部分。一般在只需要从客户端往服务器发送信息,而不需要返回数据时使用。
|
||||
|
||||
- **206 Partial Content** :表示客户端进行了范围请求,响应报文包含由 Content-Range 指定范围的实体内容。
|
||||
|
||||
## 3XX 重定向
|
||||
|
||||
- **301 Moved Permanently** :永久性重定向
|
||||
|
||||
- **302 Found** :临时性重定向
|
||||
|
||||
- **303 See Other** :和 302 有着相同的功能,但是 303 明确要求客户端应该采用 GET 方法获取资源。
|
||||
|
||||
- 注:虽然 HTTP 协议规定 301、302 状态下重定向时不允许把 POST 方法改成 GET 方法,但是大多数浏览器都会在 301、302 和 303 状态下的重定向把 POST 方法改成 GET 方法。
|
||||
|
||||
- **304 Not Modified** :如果请求报文首部包含一些条件,例如:If-Match,If-Modified-Since,If-None-Match,If-Range,If-Unmodified-Since,如果不满足条件,则服务器会返回 304 状态码。
|
||||
|
||||
- **307 Temporary Redirect** :临时重定向,与 302 的含义类似,但是 307 要求浏览器不会把重定向请求的 POST 方法改成 GET 方法。
|
||||
|
||||
## 4XX 客户端错误
|
||||
|
||||
- **400 Bad Request** :请求报文中存在语法错误。
|
||||
|
||||
- **401 Unauthorized** :该状态码表示发送的请求需要有认证信息(BASIC 认证、DIGEST 认证)。如果之前已进行过一次请求,则表示用户认证失败。
|
||||
|
||||
- **403 Forbidden** :请求被拒绝。
|
||||
|
||||
- **404 Not Found**
|
||||
|
||||
## 5XX 服务器错误
|
||||
|
||||
- **500 Internal Server Error** :服务器正在执行请求时发生错误。
|
||||
|
||||
- **503 Service Unavailable** :服务器暂时处于超负载或正在进行停机维护,现在无法处理请求。
|
||||
|
||||
# 四、HTTP 首部
|
||||
|
||||
有 4 种类型的首部字段:通用首部字段、请求首部字段、响应首部字段和实体首部字段。
|
||||
|
||||
各种首部字段及其含义如下(不需要全记,仅供查阅):
|
||||
|
||||
## 通用首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| :--: | :--: |
|
||||
| Cache-Control | 控制缓存的行为 |
|
||||
| Connection | 控制不再转发给代理的首部字段、管理持久连接|
|
||||
| Date | 创建报文的日期时间 |
|
||||
| Pragma | 报文指令 |
|
||||
| Trailer | 报文末端的首部一览 |
|
||||
| Transfer-Encoding | 指定报文主体的传输编码方式 |
|
||||
| Upgrade | 升级为其他协议 |
|
||||
| Via | 代理服务器的相关信息 |
|
||||
| Warning | 错误通知 |
|
||||
|
||||
## 请求首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| :--: | :--: |
|
||||
| Accept | 用户代理可处理的媒体类型 |
|
||||
| Accept-Charset | 优先的字符集 |
|
||||
| Accept-Encoding | 优先的内容编码 |
|
||||
| Accept-Language | 优先的语言(自然语言) |
|
||||
| Authorization | Web 认证信息 |
|
||||
| Expect | 期待服务器的特定行为 |
|
||||
| From | 用户的电子邮箱地址 |
|
||||
| Host | 请求资源所在服务器 |
|
||||
| If-Match | 比较实体标记(ETag) |
|
||||
| If-Modified-Since | 比较资源的更新时间 |
|
||||
| If-None-Match | 比较实体标记(与 If-Match 相反) |
|
||||
| If-Range | 资源未更新时发送实体 Byte 的范围请求 |
|
||||
| If-Unmodified-Since | 比较资源的更新时间(与 If-Modified-Since 相反) |
|
||||
| Max-Forwards | 最大传输逐跳数 |
|
||||
| Proxy-Authorization | 代理服务器要求客户端的认证信息 |
|
||||
| Range | 实体的字节范围请求 |
|
||||
| Referer | 对请求中 URI 的原始获取方 |
|
||||
| TE | 传输编码的优先级 |
|
||||
| User-Agent | HTTP 客户端程序的信息 |
|
||||
|
||||
## 响应首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| :--: | :--: |
|
||||
| Accept-Ranges | 是否接受字节范围请求 |
|
||||
| Age | 推算资源创建经过时间 |
|
||||
| ETag | 资源的匹配信息 |
|
||||
| Location | 令客户端重定向至指定 URI |
|
||||
| Proxy-Authenticate | 代理服务器对客户端的认证信息 |
|
||||
| Retry-After | 对再次发起请求的时机要求 |
|
||||
| Server | HTTP 服务器的安装信息 |
|
||||
| Vary | 代理服务器缓存的管理信息 |
|
||||
| WWW-Authenticate | 服务器对客户端的认证信息 |
|
||||
|
||||
## 实体首部字段
|
||||
|
||||
| 首部字段名 | 说明 |
|
||||
| :--: | :--: |
|
||||
| Allow | 资源可支持的 HTTP 方法 |
|
||||
| Content-Encoding | 实体主体适用的编码方式 |
|
||||
| Content-Language | 实体主体的自然语言 |
|
||||
| Content-Length | 实体主体的大小 |
|
||||
| Content-Location | 替代对应资源的 URI |
|
||||
| Content-MD5 | 实体主体的报文摘要 |
|
||||
| Content-Range | 实体主体的位置范围 |
|
||||
| Content-Type | 实体主体的媒体类型 |
|
||||
| Expires | 实体主体过期的日期时间 |
|
||||
| Last-Modified | 资源的最后修改日期时间 |
|
||||
|
||||
# 五、具体应用
|
||||
|
||||
## 连接管理
|
||||
|
||||
<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
|
||||
|
||||
HTTP 协议是无状态的,主要是为了让 HTTP 协议尽可能简单,使得它能够处理大量事务。HTTP/1.1 引入 Cookie 来保存状态信息。
|
||||
|
||||
Cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器之后向同一服务器再次发起请求时被携带上,用于告知服务端两个请求是否来自同一浏览器。由于之后每次请求都会需要携带 Cookie 数据,因此会带来额外的性能开销(尤其是在移动环境下)。
|
||||
|
||||
Cookie 曾一度用于客户端数据的存储,因为当时并没有其它合适的存储办法而作为唯一的存储手段,但现在随着现代浏览器开始支持各种各样的存储方式,Cookie 渐渐被淘汰。新的浏览器 API 已经允许开发者直接将数据存储到本地,如使用 Web storage API(本地存储和会话存储)或 IndexedDB。
|
||||
|
||||
### 1. 用途
|
||||
|
||||
- 会话状态管理(如用户登录状态、购物车、游戏分数或其它需要记录的信息)
|
||||
- 个性化设置(如用户自定义设置、主题等)
|
||||
- 浏览器行为跟踪(如跟踪分析用户行为等)
|
||||
|
||||
### 2. 创建过程
|
||||
|
||||
服务器发送的响应报文包含 Set-Cookie 首部字段,客户端得到响应报文后把 Cookie 内容保存到浏览器中。
|
||||
|
||||
```html
|
||||
HTTP/1.0 200 OK
|
||||
Content-type: text/html
|
||||
Set-Cookie: yummy_cookie=choco
|
||||
Set-Cookie: tasty_cookie=strawberry
|
||||
|
||||
[page content]
|
||||
```
|
||||
|
||||
客户端之后对同一个服务器发送请求时,会从浏览器中取出 Cookie 信息并通过 Cookie 请求首部字段发送给服务器。
|
||||
|
||||
```html
|
||||
GET /sample_page.html HTTP/1.1
|
||||
Host: www.example.org
|
||||
Cookie: yummy_cookie=choco; tasty_cookie=strawberry
|
||||
```
|
||||
|
||||
### 3. 分类
|
||||
|
||||
- 会话期 Cookie:浏览器关闭之后它会被自动删除,也就是说它仅在会话期内有效。
|
||||
- 持久性 Cookie:指定一个特定的过期时间(Expires)或有效期(max-age)之后就成为了持久性的 Cookie。
|
||||
|
||||
```html
|
||||
Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
|
||||
```
|
||||
|
||||
### 4. 作用域
|
||||
|
||||
Domain 标识指定了哪些主机可以接受 Cookie。如果不指定,默认为当前文档的主机(不包含子域名)。如果指定了 Domain,则一般包含子域名。例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如 developer.mozilla.org)。
|
||||
|
||||
Path 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F ("/") 作为路径分隔符,子路径也会被匹配。例如,设置 Path=/docs,则以下地址都会匹配:
|
||||
|
||||
- /docs
|
||||
- /docs/Web/
|
||||
- /docs/Web/HTTP
|
||||
|
||||
### 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 存储在服务器端,存储在服务器端的信息更加安全。
|
||||
|
||||
Session 可以存储在服务器上的文件、数据库或者内存中。也可以将 Session 存储在 Redis 这种内存型数据库中,效率会更高。
|
||||
|
||||
使用 Session 维护用户登录状态的过程如下:
|
||||
|
||||
- 用户进行登录时,用户提交包含用户名和密码的表单,放入 HTTP 请求报文中;
|
||||
- 服务器验证该用户名和密码,如果正确则把用户信息存储到 Redis 中,它在 Redis 中的 Key 称为 Session ID;
|
||||
- 服务器返回的响应报文的 Set-Cookie 首部字段包含了这个 Session ID,客户端收到响应报文之后将该 Cookie 值存入浏览器中;
|
||||
- 客户端之后对同一个服务器进行请求时会包含该 Cookie 值,服务器收到之后提取出 Session ID,从 Redis 中取出用户信息,继续之前的业务操作。
|
||||
|
||||
应该注意 Session ID 的安全性问题,不能让它被恶意攻击者轻易获取,那么就不能产生一个容易被猜到的 Session ID 值。此外,还需要经常重新生成 Session ID。在对安全性要求极高的场景下,例如转账等操作,除了使用 Session 管理用户状态之外,还需要对用户进行重新验证,比如重新输入密码,或者使用短信验证码等方式。
|
||||
|
||||
### 9. 浏览器禁用 Cookie
|
||||
|
||||
此时无法使用 Cookie 来保存用户信息,只能使用 Session。除此之外,不能再将 Session ID 存放到 Cookie 中,而是使用 URL 重写技术,将 Session ID 作为 URL 的参数进行传递。
|
||||
|
||||
### 10. Cookie 与 Session 选择
|
||||
|
||||
- Cookie 只能存储 ASCII 码字符串,而 Session 则可以存取任何类型的数据,因此在考虑数据复杂性时首选 Session;
|
||||
- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
|
||||
- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。
|
||||
|
||||
## 缓存
|
||||
|
||||
### 1. 优点
|
||||
|
||||
- 缓解服务器压力;
|
||||
- 降低客户端获取资源的延迟:缓存通常位于内存中,读取缓存的速度更快。并且缓存在地理位置上也有可能比源服务器来得近,例如浏览器缓存。
|
||||
|
||||
### 2. 实现方法
|
||||
|
||||
- 让代理服务器进行缓存;
|
||||
- 让客户端浏览器进行缓存。
|
||||
|
||||
### 3. Cache-Control
|
||||
|
||||
HTTP/1.1 通过 Cache-Control 首部字段来控制缓存。
|
||||
|
||||
**3.1 禁止进行缓存**
|
||||
|
||||
no-store 指令规定不能对请求或响应的任何一部分进行缓存。
|
||||
|
||||
```html
|
||||
Cache-Control: no-store
|
||||
```
|
||||
|
||||
**3.2 强制确认缓存**
|
||||
|
||||
no-cache 指令规定缓存服务器需要先向源服务器验证缓存资源的有效性,只有当缓存资源有效才将能使用该缓存对客户端的请求进行响应。
|
||||
|
||||
```html
|
||||
Cache-Control: no-cache
|
||||
```
|
||||
|
||||
**3.3 私有缓存和公共缓存**
|
||||
|
||||
private 指令规定了将资源作为私有缓存,只能被单独用户所使用,一般存储在用户浏览器中。
|
||||
|
||||
```html
|
||||
Cache-Control: private
|
||||
```
|
||||
|
||||
public 指令规定了将资源作为公共缓存,可以被多个用户所使用,一般存储在代理服务器中。
|
||||
|
||||
```html
|
||||
Cache-Control: public
|
||||
```
|
||||
|
||||
**3.4 缓存过期机制**
|
||||
|
||||
max-age 指令出现在请求报文中,并且缓存资源的缓存时间小于该指令指定的时间,那么就能接受该缓存。
|
||||
|
||||
max-age 指令出现在响应报文中,表示缓存资源在缓存服务器中保存的时间。
|
||||
|
||||
```html
|
||||
Cache-Control: max-age=31536000
|
||||
```
|
||||
|
||||
Expires 首部字段也可以用于告知缓存服务器该资源什么时候会过期。
|
||||
|
||||
```html
|
||||
Expires: Wed, 04 Jul 2012 08:26:05 GMT
|
||||
```
|
||||
|
||||
- 在 HTTP/1.1 中,会优先处理 max-age 指令;
|
||||
- 在 HTTP/1.0 中,max-age 指令会被忽略掉。
|
||||
|
||||
### 4. 缓存验证
|
||||
|
||||
需要先了解 ETag 首部字段的含义,它是资源的唯一标识。URL 不能唯一表示资源,例如 `http://www.google.com/` 有中文和英文两个资源,只有 ETag 才能对这两个资源进行唯一标识。
|
||||
|
||||
```html
|
||||
ETag: "82e22293907ce725faf67773957acd12"
|
||||
```
|
||||
|
||||
可以将缓存资源的 ETag 值放入 If-None-Match 首部,服务器收到该请求后,判断缓存资源的 ETag 值和资源的最新 ETag 值是否一致,如果一致则表示缓存资源有效,返回 304 Not Modified。
|
||||
|
||||
```html
|
||||
If-None-Match: "82e22293907ce725faf67773957acd12"
|
||||
```
|
||||
|
||||
Last-Modified 首部字段也可以用于缓存验证,它包含在源服务器发送的响应报文中,指示源服务器对资源的最后修改时间。但是它是一种弱校验器,因为只能精确到一秒,所以它通常作为 ETag 的备用方案。如果响应首部字段里含有这个信息,客户端可以在后续的请求中带上 If-Modified-Since 来验证缓存。服务器只在所请求的资源在给定的日期时间之后对内容进行过修改的情况下才会将资源返回,状态码为 200 OK。如果请求的资源从那时起未经修改,那么返回一个不带有消息主体的 304 Not Modified 响应。
|
||||
|
||||
```html
|
||||
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
|
||||
```
|
||||
|
||||
```html
|
||||
If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
|
||||
```
|
||||
|
||||
## 内容协商
|
||||
|
||||
通过内容协商返回最合适的内容,例如根据浏览器的默认语言选择返回中文界面还是英文界面。
|
||||
|
||||
### 1. 类型
|
||||
|
||||
**1.1 服务端驱动型**
|
||||
|
||||
客户端设置特定的 HTTP 首部字段,例如 Accept、Accept-Charset、Accept-Encoding、Accept-Language,服务器根据这些字段返回特定的资源。
|
||||
|
||||
它存在以下问题:
|
||||
|
||||
- 服务器很难知道客户端浏览器的全部信息;
|
||||
- 客户端提供的信息相当冗长(HTTP/2 协议的首部压缩机制缓解了这个问题),并且存在隐私风险(HTTP 指纹识别技术);
|
||||
- 给定的资源需要返回不同的展现形式,共享缓存的效率会降低,而服务器端的实现会越来越复杂。
|
||||
|
||||
**1.2 代理驱动型**
|
||||
|
||||
服务器返回 300 Multiple Choices 或者 406 Not Acceptable,客户端从中选出最合适的那个资源。
|
||||
|
||||
### 2. Vary
|
||||
|
||||
```html
|
||||
Vary: Accept-Language
|
||||
```
|
||||
|
||||
在使用内容协商的情况下,只有当缓存服务器中的缓存满足内容协商条件时,才能使用该缓存,否则应该向源服务器请求该资源。
|
||||
|
||||
例如,一个客户端发送了一个包含 Accept-Language 首部字段的请求之后,源服务器返回的响应包含 `Vary: Accept-Language` 内容,缓存服务器对这个响应进行缓存之后,在客户端下一次访问同一个 URL 资源,并且 Accept-Language 与缓存中的对应的值相同时才会返回该缓存。
|
||||
|
||||
## 内容编码
|
||||
|
||||
内容编码将实体主体进行压缩,从而减少传输的数据量。
|
||||
|
||||
常用的内容编码有:gzip、compress、deflate、identity。
|
||||
|
||||
浏览器发送 Accept-Encoding 首部,其中包含有它所支持的压缩算法,以及各自的优先级。服务器则从中选择一种,使用该算法对响应的消息主体进行压缩,并且发送 Content-Encoding 首部来告知浏览器它选择了哪一种算法。由于该内容协商过程是基于编码类型来选择资源的展现形式的,在响应的 Vary 首部至少要包含 Content-Encoding。
|
||||
|
||||
## 范围请求
|
||||
|
||||
如果网络出现中断,服务器只发送了一部分数据,范围请求可以使得客户端只请求服务器未发送的那部分数据,从而避免服务器重新发送所有数据。
|
||||
|
||||
### 1. Range
|
||||
|
||||
在请求报文中添加 Range 首部字段指定请求的范围。
|
||||
|
||||
```html
|
||||
GET /z4d4kWk.jpg HTTP/1.1
|
||||
Host: i.imgur.com
|
||||
Range: bytes=0-1023
|
||||
```
|
||||
|
||||
请求成功的话服务器返回的响应包含 206 Partial Content 状态码。
|
||||
|
||||
```html
|
||||
HTTP/1.1 206 Partial Content
|
||||
Content-Range: bytes 0-1023/146515
|
||||
Content-Length: 1024
|
||||
...
|
||||
(binary content)
|
||||
```
|
||||
|
||||
### 2. Accept-Ranges
|
||||
|
||||
响应首部字段 Accept-Ranges 用于告知客户端是否能处理范围请求,可以处理使用 bytes,否则使用 none。
|
||||
|
||||
```html
|
||||
Accept-Ranges: bytes
|
||||
```
|
||||
|
||||
### 3. 响应状态码
|
||||
|
||||
- 在请求成功的情况下,服务器会返回 206 Partial Content 状态码。
|
||||
- 在请求的范围越界的情况下,服务器会返回 416 Requested Range Not Satisfiable 状态码。
|
||||
- 在不支持范围请求的情况下,服务器会返回 200 OK 状态码。
|
||||
|
||||
## 分块传输编码
|
||||
|
||||
Chunked Transfer Coding,可以把数据分割成多块,让浏览器逐步显示页面。
|
||||
|
||||
## 多部分对象集合
|
||||
|
||||
一份报文主体内可含有多种类型的实体同时发送,每个部分之间用 boundary 字段定义的分隔符进行分隔,每个部分都可以有首部字段。
|
||||
|
||||
例如,上传多个表单时可以使用如下方式:
|
||||
|
||||
```html
|
||||
Content-Type: multipart/form-data; boundary=AaB03x
|
||||
|
||||
--AaB03x
|
||||
Content-Disposition: form-data; name="submit-name"
|
||||
|
||||
Larry
|
||||
--AaB03x
|
||||
Content-Disposition: form-data; name="files"; filename="file1.txt"
|
||||
Content-Type: text/plain
|
||||
|
||||
... contents of file1.txt ...
|
||||
--AaB03x--
|
||||
```
|
||||
|
||||
## 虚拟主机
|
||||
|
||||
HTTP/1.1 使用虚拟主机技术,使得一台服务器拥有多个域名,并且在逻辑上可以看成多个服务器。
|
||||
|
||||
## 通信数据转发
|
||||
|
||||
### 1. 代理
|
||||
|
||||
代理服务器接受客户端的请求,并且转发给其它服务器。
|
||||
|
||||
使用代理的主要目的是:
|
||||
|
||||
- 缓存
|
||||
- 负载均衡
|
||||
- 网络访问控制
|
||||
- 访问日志记录
|
||||
|
||||
代理服务器分为正向代理和反向代理两种:
|
||||
|
||||
- 用户察觉得到正向代理的存在。
|
||||
|
||||
<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>
|
||||
|
||||
### 2. 网关
|
||||
|
||||
与代理服务器不同的是,网关服务器会将 HTTP 转化为其它协议进行通信,从而请求其它非 HTTP 服务器的服务。
|
||||
|
||||
### 3. 隧道
|
||||
|
||||
使用 SSL 等加密手段,在客户端和服务器之间建立一条安全的通信线路。
|
||||
|
||||
# 六、HTTPs
|
||||
|
||||
HTTP 有以下安全性问题:
|
||||
|
||||
- 使用明文进行通信,内容可能会被窃听;
|
||||
- 不验证通信方的身份,通信方的身份有可能遭遇伪装;
|
||||
- 无法证明报文的完整性,报文有可能遭篡改。
|
||||
|
||||
HTTPs 并不是新协议,而是让 HTTP 先和 SSL(Secure Sockets Layer)通信,再由 SSL 和 TCP 通信,也就是说 HTTPs 使用了隧道进行通信。
|
||||
|
||||
通过使用 SSL,HTTPs 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
|
||||
|
||||
<div align="center"> <img src="../pics//ssl-offloading.jpg" width="700"/> </div><br>
|
||||
|
||||
## 加密
|
||||
|
||||
### 1. 对称密钥加密
|
||||
|
||||
对称密钥加密(Symmetric-Key Encryption),加密和解密使用同一密钥。
|
||||
|
||||
- 优点:运算速度快;
|
||||
- 缺点:无法安全地将密钥传输给通信方。
|
||||
|
||||
<div align="center"> <img src="../pics//7fffa4b8-b36d-471f-ad0c-a88ee763bb76.png" width="600"/> </div><br>
|
||||
|
||||
### 2.非对称密钥加密
|
||||
|
||||
非对称密钥加密,又称公开密钥加密(Public-Key Encryption),加密和解密使用不同的密钥。
|
||||
|
||||
公开密钥所有人都可以获得,通信发送方获得接收方的公开密钥之后,就可以使用公开密钥进行加密,接收方收到通信内容后使用私有密钥解密。
|
||||
|
||||
非对称密钥除了用来加密,还可以用来进行签名。因为私有密钥无法被其他人获取,因此通信发送方使用其私有密钥进行签名,通信接收方使用发送方的公开密钥对签名进行解密,就能判断这个签名是否正确。
|
||||
|
||||
- 优点:可以更安全地将公开密钥传输给通信发送方;
|
||||
- 缺点:运算速度慢。
|
||||
|
||||
<div align="center"> <img src="../pics//39ccb299-ee99-4dd1-b8b4-2f9ec9495cb4.png" width="600"/> </div><br>
|
||||
|
||||
### 3. HTTPs 采用的加密方式
|
||||
|
||||
HTTPs 采用混合的加密机制,使用非对称密钥加密用于传输对称密钥来保证传输过程的安全性,之后使用对称密钥加密进行通信来保证通信过程的效率。(下图中的 Session Key 就是对称密钥)
|
||||
|
||||
<div align="center"> <img src="../pics//How-HTTPS-Works.png" width="600"/> </div><br>
|
||||
|
||||
## 认证
|
||||
|
||||
通过使用 **证书** 来对通信方进行认证。
|
||||
|
||||
数字证书认证机构(CA,Certificate Authority)是客户端与服务器双方都可信赖的第三方机构。
|
||||
|
||||
服务器的运营人员向 CA 提出公开密钥的申请,CA 在判明提出申请者的身份之后,会对已申请的公开密钥做数字签名,然后分配这个已签名的公开密钥,并将该公开密钥放入公开密钥证书后绑定在一起。
|
||||
|
||||
进行 HTTPs 通信时,服务器会把证书发送给客户端。客户端取得其中的公开密钥之后,先使用数字签名进行验证,如果验证通过,就可以开始通信了。
|
||||
|
||||
通信开始时,客户端需要使用服务器的公开密钥将自己的私有密钥传输给服务器,之后再进行对称密钥加密。
|
||||
|
||||
<div align="center"> <img src="../pics//2017-06-11-ca.png" width=""/> </div><br>
|
||||
|
||||
## 完整性保护
|
||||
|
||||
SSL 提供报文摘要功能来进行完整性保护。
|
||||
|
||||
HTTP 也提供了 MD5 报文摘要功能,但不是安全的。例如报文内容被篡改之后,同时重新计算 MD5 的值,通信接收方是无法意识到发生了篡改。
|
||||
|
||||
HTTPs 的报文摘要功能之所以安全,是因为它结合了加密和认证这两个操作。试想一下,加密之后的报文,遭到篡改之后,也很难重新计算报文摘要,因为无法轻易获取明文。
|
||||
|
||||
## HTTPs 的缺点
|
||||
|
||||
- 因为需要进行加密解密等过程,因此速度会更慢;
|
||||
- 需要支付证书授权的高额费用。
|
||||
# 七、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)。
|
||||
|
||||
- 一个数据流(Stream)都有一个唯一标识符和可选的优先级信息,用于承载双向信息。
|
||||
- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧。
|
||||
- 帧(Frame)是最小的通信单位,来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。
|
||||
|
||||
<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>
|
||||
|
||||
# 八、HTTP/1.1 新特性
|
||||
|
||||
详细内容请见上文
|
||||
|
||||
- 默认是长连接
|
||||
|
||||
- 支持流水线
|
||||
|
||||
- 支持同时打开多个 TCP 连接
|
||||
|
||||
- 支持虚拟主机
|
||||
|
||||
- 新增状态码 100
|
||||
|
||||
- 支持分块传输编码
|
||||
|
||||
- 新增缓存处理指令 max-age
|
||||
|
||||
# 九、GET 和 POST 比较
|
||||
|
||||
## 作用
|
||||
|
||||
GET 用于获取资源,而 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
|
||||
```
|
||||
|
||||
```
|
||||
POST /test/demo_form.asp HTTP/1.1
|
||||
Host: w3schools.com
|
||||
name1=value1&name2=value2
|
||||
```
|
||||
|
||||
## 安全
|
||||
|
||||
安全的 HTTP 方法不会改变服务器状态,也就是说它只是可读的。
|
||||
|
||||
GET 方法是安全的,而 POST 却不是,因为 POST 的目的是传送实体主体内容,这个内容可能是用户上传的表单数据,上传成功之后,服务器可能把这个数据存储到数据库中,因此状态也就发生了改变。
|
||||
|
||||
安全的方法除了 GET 之外还有:HEAD、OPTIONS。
|
||||
|
||||
不安全的方法除了 POST 之外还有 PUT、DELETE。
|
||||
|
||||
## 幂等性
|
||||
|
||||
幂等的 HTTP 方法,同样的请求被执行一次与连续执行多次的效果是一样的,服务器的状态也是一样的。换句话说就是,幂等方法不应该具有副作用(统计用途除外)。
|
||||
|
||||
所有的安全方法也都是幂等的。
|
||||
|
||||
在正确实现的条件下,GET,HEAD,PUT 和 DELETE 等方法都是幂等的,而 POST 方法不是。
|
||||
|
||||
GET /pageX HTTP/1.1 是幂等的,连续调用多次,客户端接收到的结果都是一样的:
|
||||
|
||||
```
|
||||
GET /pageX HTTP/1.1
|
||||
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 -> Adds a 1nd row
|
||||
POST /add_row HTTP/1.1 -> Adds a 2nd row
|
||||
POST /add_row HTTP/1.1 -> Adds a 3rd row
|
||||
```
|
||||
|
||||
DELETE /idX/delete HTTP/1.1 是幂等的,即便不同的请求接收到的状态码不一样:
|
||||
|
||||
```
|
||||
DELETE /idX/delete HTTP/1.1 -> Returns 200 if idX exists
|
||||
DELETE /idX/delete HTTP/1.1 -> Returns 404 as it just got deleted
|
||||
DELETE /idX/delete HTTP/1.1 -> Returns 404
|
||||
```
|
||||
|
||||
## 可缓存
|
||||
|
||||
如果要对响应进行缓存,需要满足以下条件:
|
||||
|
||||
- 请求报文的 HTTP 方法本身是可缓存的,包括 GET 和 HEAD,但是 PUT 和 DELETE 不可缓存,POST 在多数情况下不可缓存的。
|
||||
- 响应报文的状态码是可缓存的,包括:200, 203, 204, 206, 300, 301, 404, 405, 410, 414, and 501。
|
||||
- 响应报文的 Cache-Control 首部字段没有指定不进行缓存。
|
||||
|
||||
## XMLHttpRequest
|
||||
|
||||
为了阐述 POST 和 GET 的另一个区别,需要先了解 XMLHttpRequest:
|
||||
|
||||
> XMLHttpRequest 是一个 API,它为客户端提供了在客户端和服务器之间传输数据的功能。它提供了一个通过 URL 来获取数据的简单方式,并且不会使整个页面刷新。这使得网页只更新一部分页面而不会打扰到用户。XMLHttpRequest 在 AJAX 中被大量使用。
|
||||
|
||||
- 在使用 XMLHttpRequest 的 POST 方法时,浏览器会先发送 Header 再发送 Data。但并不是所有浏览器会这么做,例如火狐就不会。
|
||||
- 而 GET 方法 Header 和 Data 会一起发送。
|
||||
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
|
||||
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
|
||||
- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
|
||||
- [htmlspecialchars](http://php.net/manual/zh/function.htmlspecialchars.php)
|
||||
- [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)
|
||||
- [浅谈 HTTP 中 Get 与 Post 的区别](https://www.cnblogs.com/hyddd/archive/2009/03/31/1426026.html)
|
||||
- [Are http:// and www really necessary?](https://www.webdancers.com/are-http-and-www-necesary/)
|
||||
- [HTTP (HyperText Transfer Protocol)](https://www.ntu.edu.sg/home/ehchua/programming/webprogramming/HTTP_Basics.html)
|
||||
- [Web-VPN: Secure Proxies with SPDY & Chrome](https://www.igvita.com/2011/12/01/web-vpn-secure-proxies-with-spdy-chrome/)
|
||||
- [File:HTTP persistent connection.svg](http://en.wikipedia.org/wiki/File:HTTP_persistent_connection.svg)
|
||||
- [Proxy server](https://en.wikipedia.org/wiki/Proxy_server)
|
||||
- [What Is This HTTPS/SSL Thing And Why Should You Care?](https://www.x-cart.com/blog/what-is-https-and-ssl.html)
|
||||
- [What is SSL Offloading?](https://securebox.comodo.com/ssl-sniffing/ssl-offloading/)
|
||||
- [Sun Directory Server Enterprise Edition 7.0 Reference - Key Encryption](https://docs.oracle.com/cd/E19424-01/820-4811/6ng8i26bn/index.html)
|
||||
- [An Introduction to Mutual SSL Authentication](https://www.codeproject.com/Articles/326574/An-Introduction-to-Mutual-SSL-Authentication)
|
||||
- [The Difference Between URLs and URIs](https://danielmiessler.com/study/url-uri/)
|
||||
- [Cookie 与 Session 的区别](https://juejin.im/entry/5766c29d6be3ff006a31b84e#comment)
|
||||
- [COOKIE 和 SESSION 有什么区别](https://www.zhihu.com/question/19786827)
|
||||
- [Cookie/Session 的机制与安全](https://harttle.land/2015/08/10/cookie-session.html)
|
||||
- [HTTPS 证书原理](https://shijianan.com/2017/06/11/https/)
|
||||
- [What is the difference between a URI, a URL and a URN?](https://stackoverflow.com/questions/176264/what-is-the-difference-between-a-uri-a-url-and-a-urn)
|
||||
- [XMLHttpRequest](https://developer.mozilla.org/zh-CN/docs/Web/API/XMLHttpRequest)
|
||||
- [XMLHttpRequest (XHR) Uses Multiple Packets for HTTP POST?](https://blog.josephscott.org/2009/08/27/xmlhttprequest-xhr-uses-multiple-packets-for-http-post/)
|
||||
- [Symmetric vs. Asymmetric Encryption – What are differences?](https://www.ssl2buy.com/wiki/symmetric-vs-asymmetric-encryption-what-are-differences)
|
||||
- [Web 性能优化与 HTTP/2](https://www.kancloud.cn/digest/web-performance-http2)
|
||||
- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
|
@ -1,620 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、概览](#一概览)
|
||||
* [二、磁盘操作](#二磁盘操作)
|
||||
* [三、字节操作](#三字节操作)
|
||||
* [实现文件复制](#实现文件复制)
|
||||
* [装饰者模式](#装饰者模式)
|
||||
* [四、字符操作](#四字符操作)
|
||||
* [编码与解码](#编码与解码)
|
||||
* [String 的编码方式](#string-的编码方式)
|
||||
* [Reader 与 Writer](#reader-与-writer)
|
||||
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
|
||||
* [五、对象操作](#五对象操作)
|
||||
* [序列化](#序列化)
|
||||
* [Serializable](#serializable)
|
||||
* [transient](#transient)
|
||||
* [六、网络操作](#六网络操作)
|
||||
* [InetAddress](#inetaddress)
|
||||
* [URL](#url)
|
||||
* [Sockets](#sockets)
|
||||
* [Datagram](#datagram)
|
||||
* [七、NIO](#七nio)
|
||||
* [流与块](#流与块)
|
||||
* [通道与缓冲区](#通道与缓冲区)
|
||||
* [缓冲区状态变量](#缓冲区状态变量)
|
||||
* [文件 NIO 实例](#文件-nio-实例)
|
||||
* [选择器](#选择器)
|
||||
* [套接字 NIO 实例](#套接字-nio-实例)
|
||||
* [内存映射文件](#内存映射文件)
|
||||
* [对比](#对比)
|
||||
* [八、参考资料](#八参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、概览
|
||||
|
||||
Java 的 I/O 大概可以分成以下几类:
|
||||
|
||||
- 磁盘操作:File
|
||||
- 字节操作:InputStream 和 OutputStream
|
||||
- 字符操作:Reader 和 Writer
|
||||
- 对象操作:Serializable
|
||||
- 网络操作:Socket
|
||||
- 新的输入/输出:NIO
|
||||
|
||||
# 二、磁盘操作
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
从 Java7 开始,可以使用 Paths 和 Files 代替 File。
|
||||
|
||||
# 三、字节操作
|
||||
|
||||
## 实现文件复制
|
||||
|
||||
```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 对象即可。
|
||||
|
||||
```java
|
||||
FileInputStream fileInputStream = new FileInputStream(filePath);
|
||||
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
|
||||
```
|
||||
|
||||
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
|
||||
|
||||
# 四、字符操作
|
||||
|
||||
## 编码与解码
|
||||
|
||||
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
|
||||
|
||||
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
|
||||
|
||||
- GBK 编码中,中文字符占 2 个字节,英文字符占 1 个字节;
|
||||
- UTF-8 编码中,中文字符占 3 个字节,英文字符占 1 个字节;
|
||||
- UTF-16be 编码中,中文字符和英文字符都占 2 个字节。
|
||||
|
||||
UTF-16be 中的 be 指的是 Big Endian,也就是大端。相应地也有 UTF-16le,le 指的是 Little Endian,也就是小端。
|
||||
|
||||
Java 的内存编码使用双字节编码 UTF-16be,这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位,也就是两个字节,Java 使用这种双字节编码是为了让一个中文或者一个英文都能使用一个 char 来存储。
|
||||
|
||||
## 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()
|
||||
- 反序列化:ObjectInputStream.readObject()
|
||||
|
||||
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
|
||||
|
||||
## 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 关键字可以使一些属性不会被序列化。
|
||||
|
||||
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
|
||||
|
||||
```java
|
||||
private transient Object[] elementData;
|
||||
```
|
||||
|
||||
# 六、网络操作
|
||||
|
||||
Java 中的网络支持:
|
||||
|
||||
- InetAddress:用于表示网络上的硬件资源,即 IP 地址;
|
||||
- URL:统一资源定位符;
|
||||
- Sockets:使用 TCP 协议实现网络通信;
|
||||
- Datagram:使用 UDP 协议实现网络通信。
|
||||
|
||||
## InetAddress
|
||||
|
||||
没有公有的构造函数,只能通过静态方法来创建实例。
|
||||
|
||||
```java
|
||||
InetAddress.getByName(String host);
|
||||
InetAddress.getByAddress(byte[] address);
|
||||
```
|
||||
|
||||
## URL
|
||||
|
||||
可以直接从 URL 中读取字节流数据。
|
||||
|
||||
```java
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
URL url = new URL("http://www.baidu.com");
|
||||
|
||||
/* 字节流 */
|
||||
InputStream is = url.openStream();
|
||||
|
||||
/* 字符流 */
|
||||
InputStreamReader isr = new InputStreamReader(is, "utf-8");
|
||||
|
||||
/* 提供缓存功能 */
|
||||
BufferedReader br = new BufferedReader(isr);
|
||||
|
||||
String line;
|
||||
while ((line = br.readLine()) != null) {
|
||||
System.out.println(line);
|
||||
}
|
||||
|
||||
br.close();
|
||||
}
|
||||
```
|
||||
|
||||
## Sockets
|
||||
|
||||
- ServerSocket:服务器端类
|
||||
- Socket:客户端类
|
||||
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
|
||||
|
||||
<div align="center"> <img src="../pics//ClienteServidorSockets1521731145260.jpg"/> </div><br>
|
||||
|
||||
## Datagram
|
||||
|
||||
- DatagramSocket:通信类
|
||||
- DatagramPacket:数据包类
|
||||
|
||||
# 七、NIO
|
||||
|
||||
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
|
||||
|
||||
## 流与块
|
||||
|
||||
I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
|
||||
|
||||
面向流的 I/O 一次处理一个字节数据:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
|
||||
|
||||
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
|
||||
|
||||
I/O 包和 NIO 已经很好地集成了,java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
|
||||
|
||||
## 通道与缓冲区
|
||||
|
||||
### 1. 通道
|
||||
|
||||
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
|
||||
|
||||
通道与流的不同之处在于,流只能在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
|
||||
|
||||
通道包括以下类型:
|
||||
|
||||
- FileChannel:从文件中读写数据;
|
||||
- DatagramChannel:通过 UDP 读写网络中数据;
|
||||
- SocketChannel:通过 TCP 读写网络中数据;
|
||||
- ServerSocketChannel:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
|
||||
|
||||
### 2. 缓冲区
|
||||
|
||||
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
|
||||
|
||||
缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。
|
||||
|
||||
缓冲区包括以下类型:
|
||||
|
||||
- ByteBuffer
|
||||
- CharBuffer
|
||||
- ShortBuffer
|
||||
- IntBuffer
|
||||
- LongBuffer
|
||||
- FloatBuffer
|
||||
- DoubleBuffer
|
||||
|
||||
## 缓冲区状态变量
|
||||
|
||||
- capacity:最大容量;
|
||||
- position:当前已经读写的字节数;
|
||||
- limit:还可以读写的字节数。
|
||||
|
||||
状态变量的改变过程举例:
|
||||
|
||||
① 新建一个大小为 8 个字节的缓冲区,此时 position 为 0,而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
|
||||
|
||||
<div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
|
||||
|
||||
② 从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 为 5,limit 保持不变。
|
||||
|
||||
<div align="center"> <img src="../pics//80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br>
|
||||
|
||||
③ 在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position,并将 position 设置为 0。
|
||||
|
||||
<div align="center"> <img src="../pics//952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
|
||||
|
||||
④ 从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
|
||||
|
||||
<div align="center"> <img src="../pics//b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
|
||||
|
||||
⑤ 最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
|
||||
|
||||
<div align="center"> <img src="../pics//67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
|
||||
|
||||
## 文件 NIO 实例
|
||||
|
||||
以下展示了使用 NIO 快速复制文件的实例:
|
||||
|
||||
```java
|
||||
public static void fastCopy(String src, String dist) throws IOException {
|
||||
|
||||
/* 获得源文件的输入字节流 */
|
||||
FileInputStream fin = new FileInputStream(src);
|
||||
|
||||
/* 获取输入字节流的文件通道 */
|
||||
FileChannel fcin = fin.getChannel();
|
||||
|
||||
/* 获取目标文件的输出字节流 */
|
||||
FileOutputStream fout = new FileOutputStream(dist);
|
||||
|
||||
/* 获取输出字节流的文件通道 */
|
||||
FileChannel fcout = fout.getChannel();
|
||||
|
||||
/* 为缓冲区分配 1024 个字节 */
|
||||
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
|
||||
|
||||
while (true) {
|
||||
|
||||
/* 从输入通道中读取数据到缓冲区中 */
|
||||
int r = fcin.read(buffer);
|
||||
|
||||
/* read() 返回 -1 表示 EOF */
|
||||
if (r == -1) {
|
||||
break;
|
||||
}
|
||||
|
||||
/* 切换读写 */
|
||||
buffer.flip();
|
||||
|
||||
/* 把缓冲区的内容写入输出文件中 */
|
||||
fcout.write(buffer);
|
||||
|
||||
/* 清空缓冲区 */
|
||||
buffer.clear();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 选择器
|
||||
|
||||
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>
|
||||
|
||||
### 1. 创建选择器
|
||||
|
||||
```java
|
||||
Selector selector = Selector.open();
|
||||
```
|
||||
|
||||
### 2. 将通道注册到选择器上
|
||||
|
||||
```java
|
||||
ServerSocketChannel ssChannel = ServerSocketChannel.open();
|
||||
ssChannel.configureBlocking(false);
|
||||
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
```
|
||||
|
||||
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
|
||||
|
||||
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
|
||||
|
||||
- SelectionKey.OP_CONNECT
|
||||
- SelectionKey.OP_ACCEPT
|
||||
- SelectionKey.OP_READ
|
||||
- SelectionKey.OP_WRITE
|
||||
|
||||
它们在 SelectionKey 的定义如下:
|
||||
|
||||
```java
|
||||
public static final int OP_READ = 1 << 0;
|
||||
public static final int OP_WRITE = 1 << 2;
|
||||
public static final int OP_CONNECT = 1 << 3;
|
||||
public static final int OP_ACCEPT = 1 << 4;
|
||||
```
|
||||
|
||||
可以看出每个事件可以被当成一个位域,从而组成事件集整数。例如:
|
||||
|
||||
```java
|
||||
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
|
||||
```
|
||||
|
||||
### 3. 监听事件
|
||||
|
||||
```java
|
||||
int num = selector.select();
|
||||
```
|
||||
|
||||
使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
|
||||
|
||||
### 4. 获取到达的事件
|
||||
|
||||
```java
|
||||
Set<SelectionKey> keys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = keys.iterator();
|
||||
while (keyIterator.hasNext()) {
|
||||
SelectionKey key = keyIterator.next();
|
||||
if (key.isAcceptable()) {
|
||||
// ...
|
||||
} else if (key.isReadable()) {
|
||||
// ...
|
||||
}
|
||||
keyIterator.remove();
|
||||
}
|
||||
```
|
||||
|
||||
### 5. 事件循环
|
||||
|
||||
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
|
||||
|
||||
```java
|
||||
while (true) {
|
||||
int num = selector.select();
|
||||
Set<SelectionKey> keys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = keys.iterator();
|
||||
while (keyIterator.hasNext()) {
|
||||
SelectionKey key = keyIterator.next();
|
||||
if (key.isAcceptable()) {
|
||||
// ...
|
||||
} else if (key.isReadable()) {
|
||||
// ...
|
||||
}
|
||||
keyIterator.remove();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 套接字 NIO 实例
|
||||
|
||||
```java
|
||||
public class NIOServer {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
|
||||
Selector selector = Selector.open();
|
||||
|
||||
ServerSocketChannel ssChannel = ServerSocketChannel.open();
|
||||
ssChannel.configureBlocking(false);
|
||||
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
|
||||
|
||||
ServerSocket serverSocket = ssChannel.socket();
|
||||
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
|
||||
serverSocket.bind(address);
|
||||
|
||||
while (true) {
|
||||
|
||||
selector.select();
|
||||
Set<SelectionKey> keys = selector.selectedKeys();
|
||||
Iterator<SelectionKey> keyIterator = keys.iterator();
|
||||
|
||||
while (keyIterator.hasNext()) {
|
||||
|
||||
SelectionKey key = keyIterator.next();
|
||||
|
||||
if (key.isAcceptable()) {
|
||||
|
||||
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
|
||||
|
||||
// 服务器会为每个新连接创建一个 SocketChannel
|
||||
SocketChannel sChannel = ssChannel1.accept();
|
||||
sChannel.configureBlocking(false);
|
||||
|
||||
// 这个新连接主要用于从客户端读取数据
|
||||
sChannel.register(selector, SelectionKey.OP_READ);
|
||||
|
||||
} else if (key.isReadable()) {
|
||||
|
||||
SocketChannel sChannel = (SocketChannel) key.channel();
|
||||
System.out.println(readDataFromSocketChannel(sChannel));
|
||||
sChannel.close();
|
||||
}
|
||||
|
||||
keyIterator.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
|
||||
|
||||
ByteBuffer buffer = ByteBuffer.allocate(1024);
|
||||
StringBuilder data = new StringBuilder();
|
||||
|
||||
while (true) {
|
||||
|
||||
buffer.clear();
|
||||
int n = sChannel.read(buffer);
|
||||
if (n == -1) {
|
||||
break;
|
||||
}
|
||||
buffer.flip();
|
||||
int limit = buffer.limit();
|
||||
char[] dst = new char[limit];
|
||||
for (int i = 0; i < limit; i++) {
|
||||
dst[i] = (char) buffer.get(i);
|
||||
}
|
||||
data.append(dst);
|
||||
buffer.clear();
|
||||
}
|
||||
return data.toString();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
public class NIOClient {
|
||||
|
||||
public static void main(String[] args) throws IOException {
|
||||
Socket socket = new Socket("127.0.0.1", 8888);
|
||||
OutputStream out = socket.getOutputStream();
|
||||
String s = "hello world";
|
||||
out.write(s.getBytes());
|
||||
out.close();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 内存映射文件
|
||||
|
||||
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
|
||||
|
||||
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
|
||||
|
||||
下面代码行将文件的前 1024 个字节映射到内存中,map() 方法返回一个 MappedByteBuffer,它是 ByteBuffer 的子类。因此,可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
|
||||
|
||||
```java
|
||||
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
|
||||
```
|
||||
|
||||
## 对比
|
||||
|
||||
NIO 与普通 I/O 的区别主要有以下两点:
|
||||
|
||||
- NIO 是非阻塞的;
|
||||
- NIO 面向块,I/O 面向流。
|
||||
|
||||
# 八、参考资料
|
||||
|
||||
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
|
||||
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.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.html)
|
||||
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
|
||||
- [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)
|
||||
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
|
@ -1,749 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、运行时数据区域](#一运行时数据区域)
|
||||
* [程序计数器](#程序计数器)
|
||||
* [Java 虚拟机栈](#java-虚拟机栈)
|
||||
* [本地方法栈](#本地方法栈)
|
||||
* [堆](#堆)
|
||||
* [方法区](#方法区)
|
||||
* [运行时常量池](#运行时常量池)
|
||||
* [直接内存](#直接内存)
|
||||
* [二、垃圾收集](#二垃圾收集)
|
||||
* [判断一个对象是否可被回收](#判断一个对象是否可被回收)
|
||||
* [引用类型](#引用类型)
|
||||
* [垃圾收集算法](#垃圾收集算法)
|
||||
* [垃圾收集器](#垃圾收集器)
|
||||
* [三、内存分配与回收策略](#三内存分配与回收策略)
|
||||
* [Minor GC 和 Full GC](#minor-gc-和-full-gc)
|
||||
* [内存分配策略](#内存分配策略)
|
||||
* [Full GC 的触发条件](#full-gc-的触发条件)
|
||||
* [四、类加载机制](#四类加载机制)
|
||||
* [类的生命周期](#类的生命周期)
|
||||
* [类加载过程](#类加载过程)
|
||||
* [类初始化时机](#类初始化时机)
|
||||
* [类与类加载器](#类与类加载器)
|
||||
* [类加载器分类](#类加载器分类)
|
||||
* [双亲委派模型](#双亲委派模型)
|
||||
* [自定义类加载器实现](#自定义类加载器实现)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、运行时数据区域
|
||||
|
||||
<div align="center"> <img src="../pics//85370d54-40d1-4912-bcbe-37a2481c861d.png" width="450"/> </div><br>
|
||||
|
||||
## 程序计数器
|
||||
|
||||
记录正在执行的虚拟机字节码指令的地址(如果正在执行的是本地方法则为空)。
|
||||
|
||||
## Java 虚拟机栈
|
||||
|
||||
每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
|
||||
|
||||
<div align="center"> <img src="../pics//28ab96b4-82ea-4d99-99fb-b320f60d0a58.png" width="500"/> </div><br>
|
||||
|
||||
可以通过 -Xss 这个虚拟机参数来指定每个线程的 Java 虚拟机栈内存大小:
|
||||
|
||||
```java
|
||||
java -Xss512M HackTheJava
|
||||
```
|
||||
|
||||
该区域可能抛出以下异常:
|
||||
|
||||
- 当线程请求的栈深度超过最大值,会抛出 StackOverflowError 异常;
|
||||
- 栈进行动态扩展时如果无法申请到足够内存,会抛出 OutOfMemoryError 异常。
|
||||
|
||||
## 本地方法栈
|
||||
|
||||
本地方法栈与 Java 虚拟机栈类似,它们之间的区别只不过是本地方法栈为本地方法服务。
|
||||
|
||||
本地方法一般是用其它语言(C、C++ 或汇编语言等)编写的,并且被编译为基于本机硬件和操作系统的程序,对待这些方法需要特别处理。
|
||||
|
||||
<div align="center"> <img src="../pics//JNI-Java-Native-Interface.jpg" width="350"/> </div><br>
|
||||
|
||||
## 堆
|
||||
|
||||
所有对象都在这里分配内存,是垃圾收集的主要区域("GC 堆")。
|
||||
|
||||
现代的垃圾收集器基本都是采用分代收集算法,其主要的思想是针对不同类型的对象采取不同的垃圾回收算法,可以将堆分成两块:
|
||||
|
||||
- 新生代(Young Generation)
|
||||
- 老年代(Old Generation)
|
||||
|
||||
堆不需要连续内存,并且可以动态增加其内存,增加失败会抛出 OutOfMemoryError 异常。
|
||||
|
||||
可以通过 -Xms 和 -Xmx 两个虚拟机参数来指定一个程序的堆内存大小,第一个参数设置初始值,第二个参数设置最大值。
|
||||
|
||||
```java
|
||||
java -Xms1M -Xmx2M HackTheJava
|
||||
```
|
||||
|
||||
## 方法区
|
||||
|
||||
用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
|
||||
|
||||
和堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 OutOfMemoryError 异常。
|
||||
|
||||
对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。
|
||||
|
||||
HotSpot 虚拟机把它当成永久代来进行垃圾回收。但是很难确定永久代的大小,因为它受到很多因素影响,并且每次 Full GC 之后永久代的大小都会改变,所以经常会抛出 OutOfMemoryError 异常。为了更容易管理方法区,从 JDK 1.8 开始,移除永久代,并把方法区移至元空间,它位于本地内存中,而不是虚拟机内存中。
|
||||
|
||||
## 运行时常量池
|
||||
|
||||
运行时常量池是方法区的一部分。
|
||||
|
||||
Class 文件中的常量池(编译器生成的各种字面量和符号引用)会在类加载后被放入这个区域。
|
||||
|
||||
除了在编译期生成的常量,还允许动态生成,例如 String 类的 intern()。
|
||||
|
||||
## 直接内存
|
||||
|
||||
在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存(Native 堆),然后通过一个存储在 Java 堆里的 DirectByteBuffer 对象作为这块内存的引用进行操作。
|
||||
|
||||
这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。
|
||||
|
||||
# 二、垃圾收集
|
||||
|
||||
垃圾收集主要是针对堆和方法区进行。
|
||||
|
||||
程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。
|
||||
|
||||
## 判断一个对象是否可被回收
|
||||
|
||||
### 1. 引用计数算法
|
||||
|
||||
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。
|
||||
|
||||
两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。
|
||||
|
||||
正因为循环引用的存在,因此 Java 虚拟机不使用引用计数算法。
|
||||
|
||||
```java
|
||||
public class ReferenceCountingGC {
|
||||
|
||||
public Object instance = null;
|
||||
|
||||
public static void main(String[] args) {
|
||||
ReferenceCountingGC objectA = new ReferenceCountingGC();
|
||||
ReferenceCountingGC objectB = new ReferenceCountingGC();
|
||||
objectA.instance = objectB;
|
||||
objectB.instance = objectA;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 可达性分析算法
|
||||
|
||||
通过 GC Roots 作为起始点进行搜索,能够到达到的对象都是存活的,不可达的对象可被回收。
|
||||
|
||||
Java 虚拟机使用该算法来判断对象是否可被回收,在 Java 中 GC Roots 一般包含以下内容:
|
||||
|
||||
- 虚拟机栈中局部变量表中引用的对象
|
||||
- 本地方法栈中 JNI 中引用的对象
|
||||
- 方法区中类静态属性引用的对象
|
||||
- 方法区中的常量引用的对象
|
||||
|
||||
<div align="center"> <img src="../pics//0635cbe8.png" width=""/> </div><br>
|
||||
|
||||
### 3. 方法区的回收
|
||||
|
||||
因为方法区主要存放永久代对象,而永久代对象的回收率比新生代低很多,因此在方法区上进行回收性价比不高。
|
||||
|
||||
主要是对常量池的回收和对类的卸载。
|
||||
|
||||
在大量使用反射、动态代理、CGLib 等 ByteCode 框架、动态生成 JSP 以及 OSGi 这类频繁自定义 ClassLoader 的场景都需要虚拟机具备类卸载功能,以保证不会出现内存溢出。
|
||||
|
||||
类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载:
|
||||
|
||||
- 该类所有的实例都已经被回收,也就是堆中不存在该类的任何实例。
|
||||
- 加载该类的 ClassLoader 已经被回收。
|
||||
- 该类对应的 Class 对象没有在任何地方被引用,也就无法在任何地方通过反射访问该类方法。
|
||||
|
||||
可以通过 -Xnoclassgc 参数来控制是否对类进行卸载。
|
||||
|
||||
### 4. finalize()
|
||||
|
||||
finalize() 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不要使用。
|
||||
|
||||
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
|
||||
|
||||
## 引用类型
|
||||
|
||||
无论是通过引用计数算法判断对象的引用数量,还是通过可达性分析算法判断对象是否可达,判定对象是否可被回收都与引用有关。
|
||||
|
||||
Java 提供了四种强度不同的引用类型。
|
||||
|
||||
### 1. 强引用
|
||||
|
||||
被强引用关联的对象不会被回收。
|
||||
|
||||
使用 new 一个新对象的方式来创建强引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
```
|
||||
|
||||
### 2. 软引用
|
||||
|
||||
被软引用关联的对象只有在内存不够的情况下才会被回收。
|
||||
|
||||
使用 SoftReference 类来创建软引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
SoftReference<Object> sf = new SoftReference<Object>(obj);
|
||||
obj = null; // 使对象只被软引用关联
|
||||
```
|
||||
|
||||
### 3. 弱引用
|
||||
|
||||
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
|
||||
|
||||
使用 WeakReference 类来实现弱引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
WeakReference<Object> wf = new WeakReference<Object>(obj);
|
||||
obj = null;
|
||||
```
|
||||
|
||||
### 4. 虚引用
|
||||
|
||||
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
|
||||
|
||||
为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。
|
||||
|
||||
使用 PhantomReference 来实现虚引用。
|
||||
|
||||
```java
|
||||
Object obj = new Object();
|
||||
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
|
||||
obj = null;
|
||||
```
|
||||
|
||||
## 垃圾收集算法
|
||||
|
||||
### 1. 标记 - 清除
|
||||
|
||||
<div align="center"> <img src="../pics//a4248c4b-6c1d-4fb8-a557-86da92d3a294.jpg" width=""/> </div><br>
|
||||
|
||||
标记要回收的对象,然后清除。
|
||||
|
||||
不足:
|
||||
|
||||
- 标记和清除过程效率都不高;
|
||||
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。
|
||||
|
||||
### 2. 标记 - 整理
|
||||
|
||||
<div align="center"> <img src="../pics//902b83ab-8054-4bd2-898f-9a4a0fe52830.jpg" width=""/> </div><br>
|
||||
|
||||
让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
|
||||
|
||||
### 3. 复制
|
||||
|
||||
<div align="center"> <img src="../pics//e6b733ad-606d-4028-b3e8-83c3a73a3797.jpg" width=""/> </div><br>
|
||||
|
||||
将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
|
||||
|
||||
主要不足是只使用了内存的一半。
|
||||
|
||||
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
|
||||
|
||||
HotSpot 虚拟机的 Eden 和 Survivor 的大小比例默认为 8:1,保证了内存的利用率达到 90%。如果每次回收有多于 10% 的对象存活,那么一块 Survivor 空间就不够用了,此时需要依赖于老年代进行分配担保,也就是借用老年代的空间存储放不下的对象。
|
||||
|
||||
### 4. 分代收集
|
||||
|
||||
现在的商业虚拟机采用分代收集算法,它根据对象存活周期将内存划分为几块,不同块采用适当的收集算法。
|
||||
|
||||
一般将堆分为新生代和老年代。
|
||||
|
||||
- 新生代使用:复制算法
|
||||
- 老年代使用:标记 - 清除 或者 标记 - 整理 算法
|
||||
|
||||
## 垃圾收集器
|
||||
|
||||
<div align="center"> <img src="../pics//c625baa0-dde6-449e-93df-c3a67f2f430f.jpg" width=""/> </div><br>
|
||||
|
||||
以上是 HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。
|
||||
|
||||
- 单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;
|
||||
- 串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。
|
||||
|
||||
### 1. Serial 收集器
|
||||
|
||||
<div align="center"> <img src="../pics//22fda4ae-4dd5-489d-ab10-9ebfdad22ae0.jpg" width=""/> </div><br>
|
||||
|
||||
Serial 翻译为串行,也就是说它以串行的方式执行。
|
||||
|
||||
它是单线程的收集器,只会使用一个线程进行垃圾收集工作。
|
||||
|
||||
它的优点是简单高效,对于单个 CPU 环境来说,由于没有线程交互的开销,因此拥有最高的单线程收集效率。
|
||||
|
||||
它是 Client 模式下的默认新生代收集器,因为在该应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。
|
||||
|
||||
### 2. ParNew 收集器
|
||||
|
||||
<div align="center"> <img src="../pics//81538cd5-1bcf-4e31-86e5-e198df1e013b.jpg" width=""/> </div><br>
|
||||
|
||||
它是 Serial 收集器的多线程版本。
|
||||
|
||||
是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
|
||||
|
||||
默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。
|
||||
|
||||
### 3. Parallel Scavenge 收集器
|
||||
|
||||
与 ParNew 一样是多线程收集器。
|
||||
|
||||
其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量,它被称为“吞吐量优先”收集器。这里的吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。
|
||||
|
||||
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。
|
||||
|
||||
缩短停顿时间是以牺牲吞吐量和新生代空间来换取的:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。
|
||||
|
||||
可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。
|
||||
|
||||
### 4. Serial Old 收集器
|
||||
|
||||
<div align="center"> <img src="../pics//08f32fd3-f736-4a67-81ca-295b2a7972f2.jpg" width=""/> </div><br>
|
||||
|
||||
是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:
|
||||
|
||||
- 在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。
|
||||
- 作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。
|
||||
|
||||
### 5. Parallel Old 收集器
|
||||
|
||||
<div align="center"> <img src="../pics//278fe431-af88-4a95-a895-9c3b80117de3.jpg" width=""/> </div><br>
|
||||
|
||||
是 Parallel Scavenge 收集器的老年代版本。
|
||||
|
||||
在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。
|
||||
|
||||
### 6. CMS 收集器
|
||||
|
||||
<div align="center"> <img src="../pics//62e77997-6957-4b68-8d12-bfd609bb2c68.jpg" width=""/> </div><br>
|
||||
|
||||
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。
|
||||
|
||||
分为以下四个流程:
|
||||
|
||||
- 初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。
|
||||
- 并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。
|
||||
- 重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。
|
||||
- 并发清除:不需要停顿。
|
||||
|
||||
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
|
||||
|
||||
具有以下缺点:
|
||||
|
||||
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
|
||||
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
|
||||
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
|
||||
|
||||
### 7. G1 收集器
|
||||
|
||||
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能。HotSpot 开发团队赋予它的使命是未来可以替换掉 CMS 收集器。
|
||||
|
||||
堆被分为新生代和老年代,其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收。
|
||||
|
||||
<div align="center"> <img src="../pics//4cf711a8-7ab2-4152-b85c-d5c226733807.png" width="600"/> </div><br>
|
||||
|
||||
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
|
||||
|
||||
<div align="center"> <img src="../pics//9bbddeeb-e939-41f0-8e8e-2b1a0aa7e0a7.png" width="600"/> </div><br>
|
||||
|
||||
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
|
||||
|
||||
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
|
||||
|
||||
<div align="center"> <img src="../pics//f99ee771-c56f-47fb-9148-c0036695b5fe.jpg" width=""/> </div><br>
|
||||
|
||||
如果不计算维护 Remembered Set 的操作,G1 收集器的运作大致可划分为以下几个步骤:
|
||||
|
||||
- 初始标记
|
||||
- 并发标记
|
||||
- 最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。
|
||||
- 筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
|
||||
|
||||
具备如下特点:
|
||||
|
||||
- 空间整合:整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。
|
||||
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。
|
||||
|
||||
# 三、内存分配与回收策略
|
||||
|
||||
## Minor GC 和 Full GC
|
||||
|
||||
- Minor GC:发生在新生代上,因为新生代对象存活时间很短,因此 Minor GC 会频繁执行,执行的速度一般也会比较快。
|
||||
|
||||
- Full GC:发生在老年代上,老年代对象其存活时间长,因此 Full GC 很少执行,执行速度会比 Minor GC 慢很多。
|
||||
|
||||
## 内存分配策略
|
||||
|
||||
### 1. 对象优先在 Eden 分配
|
||||
|
||||
大多数情况下,对象在新生代 Eden 区分配,当 Eden 区空间不够时,发起 Minor GC。
|
||||
|
||||
### 2. 大对象直接进入老年代
|
||||
|
||||
大对象是指需要连续内存空间的对象,最典型的大对象是那种很长的字符串以及数组。
|
||||
|
||||
经常出现大对象会提前触发垃圾收集以获取足够的连续空间分配给大对象。
|
||||
|
||||
-XX:PretenureSizeThreshold,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
|
||||
|
||||
### 3. 长期存活的对象进入老年代
|
||||
|
||||
为对象定义年龄计数器,对象在 Eden 出生并经过 Minor GC 依然存活,将移动到 Survivor 中,年龄就增加 1 岁,增加到一定年龄则移动到老年代中。
|
||||
|
||||
-XX:MaxTenuringThreshold 用来定义年龄的阈值。
|
||||
|
||||
### 4. 动态对象年龄判定
|
||||
|
||||
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄。
|
||||
|
||||
### 5. 空间分配担保
|
||||
|
||||
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的。
|
||||
|
||||
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC。
|
||||
|
||||
## Full GC 的触发条件
|
||||
|
||||
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件:
|
||||
|
||||
### 1. 调用 System.gc()
|
||||
|
||||
只是建议虚拟机执行 Full GC,但是虚拟机不一定真正去执行。不建议使用这种方式,而是让虚拟机管理内存。
|
||||
|
||||
### 2. 老年代空间不足
|
||||
|
||||
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等。
|
||||
|
||||
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间。
|
||||
|
||||
### 3. 空间分配担保失败
|
||||
|
||||
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC。具体内容请参考上面的第五小节。
|
||||
|
||||
### 4. JDK 1.7 及以前的永久代空间不足
|
||||
|
||||
在 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。
|
||||
|
||||
# 四、类加载机制
|
||||
|
||||
类是在运行期间第一次使用时动态加载的,而不是编译时期一次性加载。因为如果在编译时期一次性加载,那么会占用很多的内存。
|
||||
|
||||
## 类的生命周期
|
||||
|
||||
<div align="center"> <img src="../pics//32b8374a-e822-4720-af0b-c0f485095ea2.jpg" width=""/> </div><br>
|
||||
|
||||
包括以下 7 个阶段:
|
||||
|
||||
- **加载(Loading)**
|
||||
- **验证(Verification)**
|
||||
- **准备(Preparation)**
|
||||
- **解析(Resolution)**
|
||||
- **初始化(Initialization)**
|
||||
- 使用(Using)
|
||||
- 卸载(Unloading)
|
||||
|
||||
## 类加载过程
|
||||
|
||||
包含了加载、验证、准备、解析和初始化这 5 个阶段。
|
||||
|
||||
### 1. 加载
|
||||
|
||||
加载是类加载的一个阶段,注意不要混淆。
|
||||
|
||||
加载过程完成以下三件事:
|
||||
|
||||
- 通过一个类的全限定名来获取定义此类的二进制字节流。
|
||||
- 将这个字节流所代表的静态存储结构转化为方法区的运行时存储结构。
|
||||
- 在内存中生成一个代表这个类的 Class 对象,作为方法区这个类的各种数据的访问入口。
|
||||
|
||||
其中二进制字节流可以从以下方式中获取:
|
||||
|
||||
- 从 ZIP 包读取,成为 JAR、EAR、WAR 格式的基础。
|
||||
- 从网络中获取,最典型的应用是 Applet。
|
||||
- 运行时计算生成,例如动态代理技术,在 java.lang.reflect.Proxy 使用 ProxyGenerator.generateProxyClass 的代理类的二进制字节流。
|
||||
- 由其他文件生成,例如由 JSP 文件生成对应的 Class 类。
|
||||
|
||||
### 2. 验证
|
||||
|
||||
确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
|
||||
|
||||
### 3. 准备
|
||||
|
||||
类变量是被 static 修饰的变量,准备阶段为类变量分配内存并设置初始值,使用的是方法区的内存。
|
||||
|
||||
实例变量不会在这阶段分配内存,它将会在对象实例化时随着对象一起分配在堆中。
|
||||
|
||||
注意,实例化不是类加载的一个过程,类加载发生在所有实例化操作之前,并且类加载只进行一次,实例化可以进行多次。
|
||||
|
||||
初始值一般为 0 值,例如下面的类变量 value 被初始化为 0 而不是 123。
|
||||
|
||||
```java
|
||||
public static int value = 123;
|
||||
```
|
||||
|
||||
如果类变量是常量,那么会按照表达式来进行初始化,而不是赋值为 0。
|
||||
|
||||
```java
|
||||
public static final int value = 123;
|
||||
```
|
||||
|
||||
### 4. 解析
|
||||
|
||||
将常量池的符号引用替换为直接引用的过程。
|
||||
|
||||
其中解析过程在某些情况下可以在初始化阶段之后再开始,这是为了支持 Java 的动态绑定。
|
||||
|
||||
### 5. 初始化
|
||||
|
||||
初始化阶段才真正开始执行类中定义的 Java 程序代码。初始化阶段即虚拟机执行类构造器 <clinit>() 方法的过程。
|
||||
|
||||
在准备阶段,类变量已经赋过一次系统要求的初始值,而在初始化阶段,根据程序员通过程序制定的主观计划去初始化类变量和其它资源。
|
||||
|
||||
<clinit>() 方法具有以下特点:
|
||||
|
||||
- 是由编译器自动收集类中所有类变量的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序由语句在源文件中出现的顺序决定。特别注意的是,静态语句块只能访问到定义在它之前的类变量,定义在它之后的类变量只能赋值,不能访问。例如以下代码:
|
||||
|
||||
```java
|
||||
public class Test {
|
||||
static {
|
||||
i = 0; // 给变量赋值可以正常编译通过
|
||||
System.out.print(i); // 这句编译器会提示“非法向前引用”
|
||||
}
|
||||
static int i = 1;
|
||||
}
|
||||
```
|
||||
|
||||
- 与类的构造函数(或者说实例构造器 <init>())不同,不需要显式的调用父类的构造器。虚拟机会自动保证在子类的 <clinit>() 方法运行之前,父类的 <clinit>() 方法已经执行结束。因此虚拟机中第一个执行 <clinit>() 方法的类肯定为 java.lang.Object。
|
||||
|
||||
- 由于父类的 <clinit>() 方法先执行,也就意味着父类中定义的静态语句块的执行要优先于子类。例如以下代码:
|
||||
|
||||
```java
|
||||
static class Parent {
|
||||
public static int A = 1;
|
||||
static {
|
||||
A = 2;
|
||||
}
|
||||
}
|
||||
|
||||
static class Sub extends Parent {
|
||||
public static int B = A;
|
||||
}
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.out.println(Sub.B); // 2
|
||||
}
|
||||
```
|
||||
|
||||
- <clinit>() 方法对于类或接口不是必须的,如果一个类中不包含静态语句块,也没有对类变量的赋值操作,编译器可以不为该类生成 <clinit>() 方法。
|
||||
|
||||
- 接口中不可以使用静态语句块,但仍然有类变量初始化的赋值操作,因此接口与类一样都会生成 <clinit>() 方法。但接口与类不同的是,执行接口的 <clinit>() 方法不需要先执行父接口的 <clinit>() 方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的 <clinit>() 方法。
|
||||
|
||||
- 虚拟机会保证一个类的 <clinit>() 方法在多线程环境下被正确的加锁和同步,如果多个线程同时初始化一个类,只会有一个线程执行这个类的 <clinit>() 方法,其它线程都会阻塞等待,直到活动线程执行 <clinit>() 方法完毕。如果在一个类的 <clinit>() 方法中有耗时的操作,就可能造成多个线程阻塞,在实际过程中此种阻塞很隐蔽。
|
||||
|
||||
## 类初始化时机
|
||||
|
||||
### 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。
|
||||
|
||||
## 类加载器分类
|
||||
|
||||
从 Java 虚拟机的角度来讲,只存在以下两种不同的类加载器:
|
||||
|
||||
- 启动类加载器(Bootstrap ClassLoader),这个类加载器用 C++ 实现,是虚拟机自身的一部分;
|
||||
|
||||
- 所有其他类的加载器,这些类由 Java 实现,独立于虚拟机外部,并且全都继承自抽象类 java.lang.ClassLoader。
|
||||
|
||||
从 Java 开发人员的角度看,类加载器可以划分得更细致一些:
|
||||
|
||||
- 启动类加载器(Bootstrap ClassLoader)此类加载器负责将存放在 <JRE_HOME>\lib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被 Java 程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用 null 代替即可。
|
||||
|
||||
- 扩展类加载器(Extension ClassLoader)这个类加载器是由 ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将 <JAVA_HOME>/lib/ext 或者被 java.ext.dir 系统变量所指定路径中的所有类库加载到内存中,开发者可以直接使用扩展类加载器。
|
||||
|
||||
- 应用程序类加载器(Application ClassLoader)这个类加载器是由 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。由于这个类加载器是 ClassLoader 中的 getSystemClassLoader() 方法的返回值,因此一般称为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
|
||||
|
||||
## 双亲委派模型
|
||||
|
||||
应用程序都是由三种类加载器相互配合进行加载的,如果有必要,还可以加入自己定义的类加载器。
|
||||
|
||||
下图展示的类加载器之间的层次关系,称为类加载器的双亲委派模型(Parents Delegation Model)。该模型要求除了顶层的启动类加载器外,其余的类加载器都应有自己的父类加载器。这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现。
|
||||
|
||||
<div align="center"> <img src="../pics//class_loader_hierarchy.png" width="600"/> </div><br>
|
||||
|
||||
### 1. 工作过程
|
||||
|
||||
一个类加载器首先将类加载请求传送到父类加载器,只有当父类加载器无法完成类加载请求时才尝试加载。
|
||||
|
||||
### 2. 好处
|
||||
|
||||
使得 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。
|
||||
|
||||
### 3. 实现
|
||||
|
||||
以下是抽象类 java.lang.ClassLoader 的代码片段,其中的 loadClass() 方法运行过程如下:先检查类是否已经加载过,如果没有则让父类加载器去加载。当父类加载器加载失败时抛出 ClassNotFoundException,此时尝试自己去加载。
|
||||
|
||||
```java
|
||||
public abstract class ClassLoader {
|
||||
// The parent class loader for delegation
|
||||
private final ClassLoader parent;
|
||||
|
||||
public Class<?> loadClass(String name) throws ClassNotFoundException {
|
||||
return loadClass(name, false);
|
||||
}
|
||||
|
||||
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
|
||||
synchronized (getClassLoadingLock(name)) {
|
||||
// First, check if the class has already been loaded
|
||||
Class<?> c = findLoadedClass(name);
|
||||
if (c == null) {
|
||||
try {
|
||||
if (parent != null) {
|
||||
c = parent.loadClass(name, false);
|
||||
} else {
|
||||
c = findBootstrapClassOrNull(name);
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
// ClassNotFoundException thrown if class not found
|
||||
// from the non-null parent class loader
|
||||
}
|
||||
|
||||
if (c == null) {
|
||||
// If still not found, then invoke findClass in order
|
||||
// to find the class.
|
||||
c = findClass(name);
|
||||
}
|
||||
}
|
||||
if (resolve) {
|
||||
resolveClass(c);
|
||||
}
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
throw new ClassNotFoundException(name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 自定义类加载器实现
|
||||
|
||||
FileSystemClassLoader 是自定义类加载器,继承自 java.lang.ClassLoader,用于加载文件系统上的类。它首先根据类的全名在文件系统上查找类的字节代码文件(.class 文件),然后读取该文件内容,最后通过 defineClass() 方法来把这些字节代码转换成 java.lang.Class 类的实例。
|
||||
|
||||
java.lang.ClassLoader 的 loadClass() 实现了双亲委派模型的逻辑,因此自定义类加载器一般不去重写它,但是需要重写 findClass() 方法。
|
||||
|
||||
```java
|
||||
public class FileSystemClassLoader extends ClassLoader {
|
||||
|
||||
private String rootDir;
|
||||
|
||||
public FileSystemClassLoader(String rootDir) {
|
||||
this.rootDir = rootDir;
|
||||
}
|
||||
|
||||
protected Class<?> findClass(String name) throws ClassNotFoundException {
|
||||
byte[] classData = getClassData(name);
|
||||
if (classData == null) {
|
||||
throw new ClassNotFoundException();
|
||||
} else {
|
||||
return defineClass(name, classData, 0, classData.length);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] getClassData(String className) {
|
||||
String path = classNameToPath(className);
|
||||
try {
|
||||
InputStream ins = new FileInputStream(path);
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
int bufferSize = 4096;
|
||||
byte[] buffer = new byte[bufferSize];
|
||||
int bytesNumRead;
|
||||
while ((bytesNumRead = ins.read(buffer)) != -1) {
|
||||
baos.write(buffer, 0, bytesNumRead);
|
||||
}
|
||||
return baos.toByteArray();
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String classNameToPath(String className) {
|
||||
return rootDir + File.separatorChar
|
||||
+ className.replace('.', File.separatorChar) + ".class";
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 周志明. 深入理解 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)
|
||||
[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/)
|
||||
- [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)
|
||||
- [深入理解 JVM(2)——GC 算法与内存分配策略](https://crowhawk.github.io/2017/08/10/jvm_2/)
|
||||
- [深入理解 JVM(3)——7 种垃圾收集器](https://crowhawk.github.io/2017/08/15/jvm_3/)
|
||||
- [JVM Internals](http://blog.jamesdbloom.com/JVMInternals.html)
|
||||
- [深入探讨 Java 类加载器](https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html#code6)
|
||||
- [Guide to WeakHashMap in Java](http://www.baeldung.com/java-weakhashmap)
|
||||
- [Tomcat example source code file (ConcurrentCache.java)](https://alvinalexander.com/java/jwarehouse/apache-tomcat-6.0.16/java/org/apache/el/util/ConcurrentCache.java.shtml)
|
@ -1,950 +0,0 @@
|
||||
<!-- 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)
|
||||
* [181. Employees Earning More Than Their Managers](#181-employees-earning-more-than-their-managers)
|
||||
* [183. Customers Who Never Order](#183-customers-who-never-order)
|
||||
* [184. Department Highest Salary](#184-department-highest-salary)
|
||||
* [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 -->
|
||||
|
||||
|
||||
# 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
|
||||
|
||||
https://leetcode.com/problems/combine-two-tables/description/
|
||||
|
||||
## Description
|
||||
|
||||
Person 表:
|
||||
|
||||
```html
|
||||
+-------------+---------+
|
||||
| Column Name | Type |
|
||||
+-------------+---------+
|
||||
| PersonId | int |
|
||||
| FirstName | varchar |
|
||||
| LastName | varchar |
|
||||
+-------------+---------+
|
||||
PersonId is the primary key column for this table.
|
||||
```
|
||||
|
||||
Address 表:
|
||||
|
||||
```html
|
||||
+-------------+---------+
|
||||
| Column Name | Type |
|
||||
+-------------+---------+
|
||||
| AddressId | int |
|
||||
| PersonId | int |
|
||||
| City | varchar |
|
||||
| State | varchar |
|
||||
+-------------+---------+
|
||||
AddressId is the primary key column for this table.
|
||||
```
|
||||
|
||||
查找 FirstName, LastName, City, State 数据,而不管一个用户有没有填地址信息。
|
||||
|
||||
## SQL Schema
|
||||
|
||||
```sql
|
||||
DROP TABLE
|
||||
IF
|
||||
EXISTS Person;
|
||||
CREATE TABLE Person ( PersonId INT, FirstName VARCHAR ( 255 ), LastName VARCHAR ( 255 ) );
|
||||
DROP TABLE
|
||||
IF
|
||||
EXISTS Address;
|
||||
CREATE TABLE Address ( AddressId INT, PersonId INT, City VARCHAR ( 255 ), State VARCHAR ( 255 ) );
|
||||
INSERT INTO Person ( PersonId, LastName, FirstName )
|
||||
VALUES
|
||||
( 1, 'Wang', 'Allen' );
|
||||
INSERT INTO Address ( AddressId, PersonId, City, State )
|
||||
VALUES
|
||||
( 1, 2, 'New York City', 'New York' );
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
使用左外连接。
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
FirstName,
|
||||
LastName,
|
||||
City,
|
||||
State
|
||||
FROM
|
||||
Person P
|
||||
LEFT JOIN Address A
|
||||
ON P.PersonId = A.PersonId;
|
||||
```
|
||||
|
||||
# 181. Employees Earning More Than Their Managers
|
||||
|
||||
https://leetcode.com/problems/employees-earning-more-than-their-managers/description/
|
||||
|
||||
## Description
|
||||
|
||||
Employee 表:
|
||||
|
||||
```html
|
||||
+----+-------+--------+-----------+
|
||||
| Id | Name | Salary | ManagerId |
|
||||
+----+-------+--------+-----------+
|
||||
| 1 | Joe | 70000 | 3 |
|
||||
| 2 | Henry | 80000 | 4 |
|
||||
| 3 | Sam | 60000 | NULL |
|
||||
| 4 | Max | 90000 | NULL |
|
||||
+----+-------+--------+-----------+
|
||||
```
|
||||
|
||||
查找薪资大于其经理薪资的员工信息。
|
||||
|
||||
## SQL Schema
|
||||
|
||||
```sql
|
||||
DROP TABLE
|
||||
IF
|
||||
EXISTS Employee;
|
||||
CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, ManagerId INT );
|
||||
INSERT INTO Employee ( Id, NAME, Salary, ManagerId )
|
||||
VALUES
|
||||
( 1, 'Joe', 70000, 3 ),
|
||||
( 2, 'Henry', 80000, 4 ),
|
||||
( 3, 'Sam', 60000, NULL ),
|
||||
( 4, 'Max', 90000, NULL );
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
E1.NAME AS Employee
|
||||
FROM
|
||||
Employee E1
|
||||
INNER JOIN Employee E2
|
||||
ON E1.ManagerId = E2.Id
|
||||
AND E1.Salary > E2.Salary;
|
||||
```
|
||||
|
||||
# 183. Customers Who Never Order
|
||||
|
||||
https://leetcode.com/problems/customers-who-never-order/description/
|
||||
|
||||
## Description
|
||||
|
||||
Curstomers 表:
|
||||
|
||||
```html
|
||||
+----+-------+
|
||||
| Id | Name |
|
||||
+----+-------+
|
||||
| 1 | Joe |
|
||||
| 2 | Henry |
|
||||
| 3 | Sam |
|
||||
| 4 | Max |
|
||||
+----+-------+
|
||||
```
|
||||
|
||||
Orders 表:
|
||||
|
||||
```html
|
||||
+----+------------+
|
||||
| Id | CustomerId |
|
||||
+----+------------+
|
||||
| 1 | 3 |
|
||||
| 2 | 1 |
|
||||
+----+------------+
|
||||
```
|
||||
|
||||
查找没有订单的顾客信息:
|
||||
|
||||
```html
|
||||
+-----------+
|
||||
| Customers |
|
||||
+-----------+
|
||||
| Henry |
|
||||
| Max |
|
||||
+-----------+
|
||||
```
|
||||
|
||||
## SQL Schema
|
||||
|
||||
```sql
|
||||
DROP TABLE
|
||||
IF
|
||||
EXISTS Customers;
|
||||
CREATE TABLE Customers ( Id INT, NAME VARCHAR ( 255 ) );
|
||||
DROP TABLE
|
||||
IF
|
||||
EXISTS Orders;
|
||||
CREATE TABLE Orders ( Id INT, CustomerId INT );
|
||||
INSERT INTO Customers ( Id, NAME )
|
||||
VALUES
|
||||
( 1, 'Joe' ),
|
||||
( 2, 'Henry' ),
|
||||
( 3, 'Sam' ),
|
||||
( 4, 'Max' );
|
||||
INSERT INTO Orders ( Id, CustomerId )
|
||||
VALUES
|
||||
( 1, 3 ),
|
||||
( 2, 1 );
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
左外链接
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
C.Name AS Customers
|
||||
FROM
|
||||
Customers C
|
||||
LEFT JOIN Orders O
|
||||
ON C.Id = O.CustomerId
|
||||
WHERE
|
||||
O.CustomerId IS NULL;
|
||||
```
|
||||
|
||||
子查询
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
Name AS Customers
|
||||
FROM
|
||||
Customers
|
||||
WHERE
|
||||
Id NOT IN ( SELECT CustomerId FROM Orders );
|
||||
```
|
||||
|
||||
# 184. Department Highest Salary
|
||||
|
||||
https://leetcode.com/problems/department-highest-salary/description/
|
||||
|
||||
## Description
|
||||
|
||||
Employee 表:
|
||||
|
||||
```html
|
||||
+----+-------+--------+--------------+
|
||||
| Id | Name | Salary | DepartmentId |
|
||||
+----+-------+--------+--------------+
|
||||
| 1 | Joe | 70000 | 1 |
|
||||
| 2 | Henry | 80000 | 2 |
|
||||
| 3 | Sam | 60000 | 2 |
|
||||
| 4 | Max | 90000 | 1 |
|
||||
+----+-------+--------+--------------+
|
||||
```
|
||||
|
||||
Department 表:
|
||||
|
||||
```html
|
||||
+----+----------+
|
||||
| Id | Name |
|
||||
+----+----------+
|
||||
| 1 | IT |
|
||||
| 2 | Sales |
|
||||
+----+----------+
|
||||
```
|
||||
|
||||
查找一个 Department 中收入最高者的信息:
|
||||
|
||||
```html
|
||||
+------------+----------+--------+
|
||||
| Department | Employee | Salary |
|
||||
+------------+----------+--------+
|
||||
| IT | Max | 90000 |
|
||||
| Sales | Henry | 80000 |
|
||||
+------------+----------+--------+
|
||||
```
|
||||
|
||||
## SQL Schema
|
||||
|
||||
```sql
|
||||
DROP TABLE IF EXISTS Employee;
|
||||
CREATE TABLE Employee ( Id INT, NAME VARCHAR ( 255 ), Salary INT, DepartmentId INT );
|
||||
DROP TABLE IF EXISTS Department;
|
||||
CREATE TABLE Department ( Id INT, NAME VARCHAR ( 255 ) );
|
||||
INSERT INTO Employee ( Id, NAME, Salary, DepartmentId )
|
||||
VALUES
|
||||
( 1, 'Joe', 70000, 1 ),
|
||||
( 2, 'Henry', 80000, 2 ),
|
||||
( 3, 'Sam', 60000, 2 ),
|
||||
( 4, 'Max', 90000, 1 );
|
||||
INSERT INTO Department ( Id, NAME )
|
||||
VALUES
|
||||
( 1, 'IT' ),
|
||||
( 2, 'Sales' );
|
||||
```
|
||||
|
||||
## Solution
|
||||
|
||||
创建一个临时表,包含了部门员工的最大薪资。可以对部门进行分组,然后使用 MAX() 汇总函数取得最大薪资。
|
||||
|
||||
之后使用连接找到一个部门中薪资等于临时表中最大薪资的员工。
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
D.NAME Department,
|
||||
E.NAME Employee,
|
||||
E.Salary
|
||||
FROM
|
||||
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.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;
|
||||
```
|
@ -1,422 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、索引](#一索引)
|
||||
* [B+ Tree 原理](#b-tree-原理)
|
||||
* [MySQL 索引](#mysql-索引)
|
||||
* [索引优化](#索引优化)
|
||||
* [索引的优点](#索引的优点)
|
||||
* [索引的使用条件](#索引的使用条件)
|
||||
* [二、查询性能优化](#二查询性能优化)
|
||||
* [使用 Explain 进行分析](#使用-explain-进行分析)
|
||||
* [优化数据访问](#优化数据访问)
|
||||
* [重构查询方式](#重构查询方式)
|
||||
* [三、存储引擎](#三存储引擎)
|
||||
* [InnoDB](#innodb)
|
||||
* [MyISAM](#myisam)
|
||||
* [比较](#比较)
|
||||
* [四、数据类型](#四数据类型)
|
||||
* [整型](#整型)
|
||||
* [浮点数](#浮点数)
|
||||
* [字符串](#字符串)
|
||||
* [时间和日期](#时间和日期)
|
||||
* [五、切分](#五切分)
|
||||
* [水平切分](#水平切分)
|
||||
* [垂直切分](#垂直切分)
|
||||
* [Sharding 策略](#sharding-策略)
|
||||
* [Sharding 存在的问题](#sharding-存在的问题)
|
||||
* [六、复制](#六复制)
|
||||
* [主从复制](#主从复制)
|
||||
* [读写分离](#读写分离)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、索引
|
||||
|
||||
## B+ Tree 原理
|
||||
|
||||
### 1. 数据结构
|
||||
|
||||
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>
|
||||
|
||||
### 2. 操作
|
||||
|
||||
进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。
|
||||
|
||||
插入删除操作会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。
|
||||
|
||||
### 3. 与红黑树的比较
|
||||
|
||||
红黑树等平衡树也可以用来实现索引,但是文件系统及数据库系统普遍采用 B+ Tree 作为索引结构,主要有以下两个原因:
|
||||
|
||||
(一)更少的查找次数
|
||||
|
||||
平衡树查找操作的时间复杂度等于树高 h,而树高大致为 O(h)=O(log<sub>d</sub>N),其中 d 为每个节点的出度。
|
||||
|
||||
红黑树的出度为 2,而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,查找的次数也就更多。
|
||||
|
||||
(二)利用磁盘预读特性
|
||||
|
||||
为了减少磁盘 I/O,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,速度会非常快。
|
||||
|
||||
操作系统一般将内存和磁盘分割成固态大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性,相邻的节点也能够被预先载入。
|
||||
|
||||
## MySQL 索引
|
||||
|
||||
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
|
||||
|
||||
### 1. B+Tree 索引
|
||||
|
||||
是大多数 MySQL 存储引擎的默认索引类型。
|
||||
|
||||
因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。
|
||||
|
||||
除了用于查找,还可以用于排序和分组。
|
||||
|
||||
可以指定多个列作为索引列,多个索引列共同组成键。
|
||||
|
||||
适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
|
||||
|
||||
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. 哈希索引
|
||||
|
||||
哈希索引能以 O(1) 时间进行查找,但是失去了有序性:
|
||||
|
||||
- 无法用于排序与分组;
|
||||
- 只支持精确查找,无法用于部分查找和范围查找。
|
||||
|
||||
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
|
||||
|
||||
### 3. 全文索引
|
||||
|
||||
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。
|
||||
|
||||
查找条件使用 MATCH AGAINST,而不是普通的 WHERE。
|
||||
|
||||
全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。
|
||||
|
||||
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
|
||||
|
||||
### 4. 空间数据索引
|
||||
|
||||
MyISAM 存储引擎支持空间数据索引(R-Tree),可以用于地理数据存储。空间数据索引会从所有维度来索引数据,可以有效地使用任意维度来进行组合查询。
|
||||
|
||||
必须使用 GIS 相关的函数来维护数据。
|
||||
|
||||
## 索引优化
|
||||
|
||||
### 1. 独立的列
|
||||
|
||||
在进行查询时,索引列不能是表达式的一部分,也不能是函数的参数,否则无法使用索引。
|
||||
|
||||
例如下面的查询不能使用 actor_id 列的索引:
|
||||
|
||||
```sql
|
||||
SELECT actor_id FROM sakila.actor WHERE actor_id + 1 = 5;
|
||||
```
|
||||
|
||||
### 2. 多列索引
|
||||
|
||||
在需要使用多个列作为条件进行查询时,使用多列索引比使用多个单列索引性能更好。例如下面的语句中,最好把 actor_id 和 film_id 设置为多列索引。
|
||||
|
||||
```sql
|
||||
SELECT film_id, actor_ id FROM sakila.film_actor
|
||||
WHERE actor_id = 1 AND film_id = 1;
|
||||
```
|
||||
|
||||
### 3. 索引列的顺序
|
||||
|
||||
让选择性最强的索引列放在前面。
|
||||
|
||||
索引的选择性是指:不重复的索引值和记录总数的比值。最大值为 1,此时每个记录都有唯一的索引与其对应。选择性越高,查询效率也越高。
|
||||
|
||||
例如下面显示的结果中 customer_id 的选择性比 staff_id 更高,因此最好把 customer_id 列放在多列索引的前面。
|
||||
|
||||
```sql
|
||||
SELECT COUNT(DISTINCT staff_id)/COUNT(*) AS staff_id_selectivity,
|
||||
COUNT(DISTINCT customer_id)/COUNT(*) AS customer_id_selectivity,
|
||||
COUNT(*)
|
||||
FROM payment;
|
||||
```
|
||||
|
||||
```html
|
||||
staff_id_selectivity: 0.0001
|
||||
customer_id_selectivity: 0.0373
|
||||
COUNT(*): 16049
|
||||
```
|
||||
|
||||
### 4. 前缀索引
|
||||
|
||||
对于 BLOB、TEXT 和 VARCHAR 类型的列,必须使用前缀索引,只索引开始的部分字符。
|
||||
|
||||
对于前缀长度的选取需要根据索引选择性来确定。
|
||||
|
||||
### 5. 覆盖索引
|
||||
|
||||
索引包含所有需要查询的字段的值。
|
||||
|
||||
具有以下优点:
|
||||
|
||||
- 索引通常远小于数据行的大小,只读取索引能大大减少数据访问量。
|
||||
- 一些存储引擎(例如 MyISAM)在内存中只缓存索引,而数据依赖于操作系统来缓存。因此,只访问索引可以不使用系统调用(通常比较费时)。
|
||||
- 对于 InnoDB 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
|
||||
|
||||
## 索引的优点
|
||||
|
||||
- 大大减少了服务器需要扫描的数据行数。
|
||||
|
||||
- 帮助服务器避免进行排序和分组,以及避免创建临时表(B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。
|
||||
|
||||
- 将随机 I/O 变为顺序 I/O(B+Tree 索引是有序的,会将相邻的数据都存储在一起)。
|
||||
|
||||
## 索引的使用条件
|
||||
|
||||
- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
|
||||
|
||||
- 对于中到大型的表,索引就非常有效;
|
||||
|
||||
- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
|
||||
|
||||
# 二、查询性能优化
|
||||
|
||||
## 使用 Explain 进行分析
|
||||
|
||||
Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
|
||||
|
||||
比较重要的字段有:
|
||||
|
||||
- select_type : 查询类型,有简单查询、联合查询、子查询等
|
||||
- key : 使用的索引
|
||||
- rows : 扫描的行数
|
||||
|
||||
## 优化数据访问
|
||||
|
||||
### 1. 减少请求的数据量
|
||||
|
||||
- 只返回必要的列:最好不要使用 SELECT * 语句。
|
||||
- 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
|
||||
- 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。
|
||||
|
||||
### 2. 减少服务器端扫描的行数
|
||||
|
||||
最有效的方式是使用索引来覆盖查询。
|
||||
|
||||
## 重构查询方式
|
||||
|
||||
### 1. 切分大查询
|
||||
|
||||
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
|
||||
|
||||
```sql
|
||||
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
|
||||
```
|
||||
|
||||
```sql
|
||||
rows_affected = 0
|
||||
do {
|
||||
rows_affected = do_query(
|
||||
"DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
|
||||
} while rows_affected > 0
|
||||
```
|
||||
|
||||
### 2. 分解大连接查询
|
||||
|
||||
将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:
|
||||
|
||||
- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
|
||||
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
|
||||
- 减少锁竞争;
|
||||
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
|
||||
- 查询本身效率也可能会有所提升。例如下面的例子中,使用 IN() 代替连接查询,可以让 MySQL 按照 ID 顺序进行查询,这可能比随机的连接要更高效。
|
||||
|
||||
```sql
|
||||
SELECT * FROM tab
|
||||
JOIN tag_post ON tag_post.tag_id=tag.id
|
||||
JOIN post ON tag_post.post_id=post.id
|
||||
WHERE tag.tag='mysql';
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT * FROM tag WHERE tag='mysql';
|
||||
SELECT * FROM tag_post WHERE tag_id=1234;
|
||||
SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
|
||||
```
|
||||
|
||||
# 三、存储引擎
|
||||
|
||||
## 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 空间效率更高。
|
||||
|
||||
# 五、切分
|
||||
|
||||
## 水平切分
|
||||
|
||||
水平切分又称为 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>
|
||||
|
||||
## Sharding 策略
|
||||
|
||||
- 哈希取模:hash(key) % N;
|
||||
- 范围:可以是 ID 范围也可以是时间范围;
|
||||
- 映射表:使用单独的一个数据库来存储映射关系。
|
||||
|
||||
## Sharding 存在的问题
|
||||
|
||||
### 1. 事务问题
|
||||
|
||||
使用分布式事务来解决,比如 XA 接口。
|
||||
|
||||
### 2. 连接
|
||||
|
||||
可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。
|
||||
|
||||
### 3. ID 唯一性
|
||||
|
||||
- 使用全局唯一 ID(GUID)
|
||||
- 为每个分片指定一个 ID 范围
|
||||
- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)
|
||||
|
||||
# 六、复制
|
||||
|
||||
## 主从复制
|
||||
|
||||
主要涉及三个线程: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.
|
||||
- 姜承尧. MySQL 技术内幕: InnoDB 存储引擎 [M]. 机械工业出版社, 2011.
|
||||
- [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
|
||||
- [服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策](http://blog.720ui.com/2017/mysql_core_09_multi_db_table2/ "服务端指南 数据存储篇 | MySQL(09) 分库与分表带来的分布式困境与应对之策")
|
||||
- [How to create unique row ID in sharded databases?](https://stackoverflow.com/questions/788829/how-to-create-unique-row-id-in-sharded-databases)
|
||||
- [SQL Azure Federation – Introduction](http://geekswithblogs.net/shaunxu/archive/2012/01/07/sql-azure-federation-ndash-introduction.aspx "Title of this entry.")
|
||||
- [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)
|
@ -1,608 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、概述](#一概述)
|
||||
* [二、数据类型](#二数据类型)
|
||||
* [STRING](#string)
|
||||
* [LIST](#list)
|
||||
* [SET](#set)
|
||||
* [HASH](#hash)
|
||||
* [ZSET](#zset)
|
||||
* [三、数据结构](#三数据结构)
|
||||
* [字典](#字典)
|
||||
* [跳跃表](#跳跃表)
|
||||
* [四、使用场景](#四使用场景)
|
||||
* [计数器](#计数器)
|
||||
* [缓存](#缓存)
|
||||
* [查找表](#查找表)
|
||||
* [消息队列](#消息队列)
|
||||
* [会话缓存](#会话缓存)
|
||||
* [分布式锁实现](#分布式锁实现)
|
||||
* [其它](#其它)
|
||||
* [五、Redis 与 Memcached](#五redis-与-memcached)
|
||||
* [数据类型](#数据类型)
|
||||
* [数据持久化](#数据持久化)
|
||||
* [分布式](#分布式)
|
||||
* [内存管理机制](#内存管理机制)
|
||||
* [六、键的过期时间](#六键的过期时间)
|
||||
* [七、数据淘汰策略](#七数据淘汰策略)
|
||||
* [八、持久化](#八持久化)
|
||||
* [RDB 持久化](#rdb-持久化)
|
||||
* [AOF 持久化](#aof-持久化)
|
||||
* [九、事务](#九事务)
|
||||
* [十、事件](#十事件)
|
||||
* [文件事件](#文件事件)
|
||||
* [时间事件](#时间事件)
|
||||
* [事件的调度与执行](#事件的调度与执行)
|
||||
* [十一、复制](#十一复制)
|
||||
* [连接过程](#连接过程)
|
||||
* [主从链](#主从链)
|
||||
* [十二、Sentinel](#十二sentinel)
|
||||
* [十三、分片](#十三分片)
|
||||
* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
|
||||
* [文章信息](#文章信息)
|
||||
* [点赞功能](#点赞功能)
|
||||
* [对文章进行排序](#对文章进行排序)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、概述
|
||||
|
||||
Redis 是速度非常快的非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。
|
||||
|
||||
键的类型只能为字符串,值支持五种数据类型:字符串、列表、集合、散列表、有序集合。
|
||||
|
||||
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
|
||||
|
||||
# 二、数据类型
|
||||
|
||||
| 数据类型 | 可以存储的值 | 操作 |
|
||||
| :--: | :--: | :--: |
|
||||
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
|
||||
| LIST | 列表 | 从两端压入或者弹出元素 </br> 对单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
|
||||
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
|
||||
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
|
||||
| ZSET | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
|
||||
|
||||
> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/)
|
||||
|
||||
## STRING
|
||||
|
||||
<div align="center"> <img src="../pics//6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/> </div><br>
|
||||
|
||||
```html
|
||||
> set hello world
|
||||
OK
|
||||
> get hello
|
||||
"world"
|
||||
> del hello
|
||||
(integer) 1
|
||||
> get hello
|
||||
(nil)
|
||||
```
|
||||
|
||||
## LIST
|
||||
|
||||
<div align="center"> <img src="../pics//fb327611-7e2b-4f2f-9f5b-38592d408f07.png" width="400"/> </div><br>
|
||||
|
||||
```html
|
||||
> rpush list-key item
|
||||
(integer) 1
|
||||
> rpush list-key item2
|
||||
(integer) 2
|
||||
> rpush list-key item
|
||||
(integer) 3
|
||||
|
||||
> lrange list-key 0 -1
|
||||
1) "item"
|
||||
2) "item2"
|
||||
3) "item"
|
||||
|
||||
> lindex list-key 1
|
||||
"item2"
|
||||
|
||||
> lpop list-key
|
||||
"item"
|
||||
|
||||
> lrange list-key 0 -1
|
||||
1) "item2"
|
||||
2) "item"
|
||||
```
|
||||
|
||||
## SET
|
||||
|
||||
<div align="center"> <img src="../pics//cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png" width="400"/> </div><br>
|
||||
|
||||
```html
|
||||
> sadd set-key item
|
||||
(integer) 1
|
||||
> sadd set-key item2
|
||||
(integer) 1
|
||||
> sadd set-key item3
|
||||
(integer) 1
|
||||
> sadd set-key item
|
||||
(integer) 0
|
||||
|
||||
> smembers set-key
|
||||
1) "item"
|
||||
2) "item2"
|
||||
3) "item3"
|
||||
|
||||
> sismember set-key item4
|
||||
(integer) 0
|
||||
> sismember set-key item
|
||||
(integer) 1
|
||||
|
||||
> srem set-key item2
|
||||
(integer) 1
|
||||
> srem set-key item2
|
||||
(integer) 0
|
||||
|
||||
> smembers set-key
|
||||
1) "item"
|
||||
2) "item3"
|
||||
```
|
||||
|
||||
## HASH
|
||||
|
||||
<div align="center"> <img src="../pics//7bd202a7-93d4-4f3a-a878-af68ae25539a.png" width="400"/> </div><br>
|
||||
|
||||
```html
|
||||
> hset hash-key sub-key1 value1
|
||||
(integer) 1
|
||||
> hset hash-key sub-key2 value2
|
||||
(integer) 1
|
||||
> hset hash-key sub-key1 value1
|
||||
(integer) 0
|
||||
|
||||
> hgetall hash-key
|
||||
1) "sub-key1"
|
||||
2) "value1"
|
||||
3) "sub-key2"
|
||||
4) "value2"
|
||||
|
||||
> hdel hash-key sub-key2
|
||||
(integer) 1
|
||||
> hdel hash-key sub-key2
|
||||
(integer) 0
|
||||
|
||||
> hget hash-key sub-key1
|
||||
"value1"
|
||||
|
||||
> hgetall hash-key
|
||||
1) "sub-key1"
|
||||
2) "value1"
|
||||
```
|
||||
|
||||
## ZSET
|
||||
|
||||
<div align="center"> <img src="../pics//1202b2d6-9469-4251-bd47-ca6034fb6116.png" width="400"/> </div><br>
|
||||
|
||||
```html
|
||||
> zadd zset-key 728 member1
|
||||
(integer) 1
|
||||
> zadd zset-key 982 member0
|
||||
(integer) 1
|
||||
> zadd zset-key 982 member0
|
||||
(integer) 0
|
||||
|
||||
> zrange zset-key 0 -1 withscores
|
||||
1) "member1"
|
||||
2) "728"
|
||||
3) "member0"
|
||||
4) "982"
|
||||
|
||||
> zrangebyscore zset-key 0 800 withscores
|
||||
1) "member1"
|
||||
2) "728"
|
||||
|
||||
> zrem zset-key member1
|
||||
(integer) 1
|
||||
> zrem zset-key member1
|
||||
(integer) 0
|
||||
|
||||
> zrange zset-key 0 -1 withscores
|
||||
1) "member0"
|
||||
2) "982"
|
||||
```
|
||||
|
||||
# 三、数据结构
|
||||
|
||||
## 字典
|
||||
|
||||
dictht 是一个散列表结构,使用拉链法保存哈希冲突。
|
||||
|
||||
```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>
|
||||
|
||||
与红黑树等平衡树相比,跳跃表具有以下优点:
|
||||
|
||||
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
|
||||
- 更容易实现;
|
||||
- 支持无锁操作。
|
||||
|
||||
# 四、使用场景
|
||||
|
||||
## 计数器
|
||||
|
||||
可以对 String 进行自增自减运算,从而实现计数器功能。
|
||||
|
||||
Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
|
||||
|
||||
## 缓存
|
||||
|
||||
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
|
||||
|
||||
## 查找表
|
||||
|
||||
例如 DNS 记录就很适合使用 Redis 进行存储。
|
||||
|
||||
查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
|
||||
|
||||
## 消息队列
|
||||
|
||||
List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息。
|
||||
|
||||
不过最好使用 Kafka、RabbitMQ 等消息中间件。
|
||||
|
||||
## 会话缓存
|
||||
|
||||
可以使用 Redis 来统一存储多台应用服务器的会话信息。
|
||||
|
||||
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
|
||||
|
||||
## 分布式锁实现
|
||||
|
||||
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
|
||||
|
||||
可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
|
||||
|
||||
## 其它
|
||||
|
||||
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
|
||||
|
||||
ZSet 可以实现有序性操作,从而实现排行榜等功能。
|
||||
|
||||
# 五、Redis 与 Memcached
|
||||
|
||||
两者都是非关系型内存键值数据库,主要有以下不同:
|
||||
|
||||
## 数据类型
|
||||
|
||||
Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。
|
||||
|
||||
## 数据持久化
|
||||
|
||||
Redis 支持两种持久化策略:RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
|
||||
|
||||
## 分布式
|
||||
|
||||
Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
|
||||
|
||||
Redis Cluster 实现了分布式的支持。
|
||||
|
||||
## 内存管理机制
|
||||
|
||||
- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。
|
||||
|
||||
- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题。但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
|
||||
|
||||
# 六、键的过期时间
|
||||
|
||||
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
|
||||
|
||||
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
|
||||
|
||||
# 七、数据淘汰策略
|
||||
|
||||
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
|
||||
|
||||
Reids 具体有 6 种淘汰策略:
|
||||
|
||||
| 策略 | 描述 |
|
||||
| :--: | :--: |
|
||||
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
|
||||
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|
||||
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
|
||||
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
|
||||
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
|
||||
| noeviction | 禁止驱逐数据 |
|
||||
|
||||
作为内存数据库,出于对性能和内存消耗的考虑,Redis 的淘汰算法实际实现上并非针对所有 key,而是抽样一小部分并且从中选出被淘汰的 key。
|
||||
|
||||
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
|
||||
|
||||
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
|
||||
|
||||
# 八、持久化
|
||||
|
||||
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
|
||||
|
||||
## RDB 持久化
|
||||
|
||||
将某个时间点的所有数据都存放到硬盘上。
|
||||
|
||||
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
|
||||
|
||||
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
|
||||
|
||||
如果数据量很大,保存快照的时间会很长。
|
||||
|
||||
## AOF 持久化
|
||||
|
||||
将写命令添加到 AOF 文件(Append Only File)的末尾。
|
||||
|
||||
使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
|
||||
|
||||
| 选项 | 同步频率 |
|
||||
| :--: | :--: |
|
||||
| always | 每个写命令都同步 |
|
||||
| everysec | 每秒同步一次 |
|
||||
| no | 让操作系统来决定何时同步 |
|
||||
|
||||
- always 选项会严重减低服务器的性能;
|
||||
- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
|
||||
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
|
||||
|
||||
随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
|
||||
|
||||
# 九、事务
|
||||
|
||||
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
|
||||
|
||||
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
|
||||
|
||||
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
|
||||
|
||||
# 十、事件
|
||||
|
||||
Redis 服务器是一个事件驱动程序。
|
||||
|
||||
## 文件事件
|
||||
|
||||
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
|
||||
|
||||
Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。
|
||||
|
||||
<div align="center"> <img src="../pics//9ea86eb5-000a-4281-b948-7b567bd6f1d8.png"/> </div><br>
|
||||
|
||||
## 时间事件
|
||||
|
||||
服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
|
||||
|
||||
时间事件又分为:
|
||||
|
||||
- 定时事件:是让一段程序在指定的时间之内执行一次;
|
||||
- 周期性事件:是让一段程序每隔指定时间就执行一次。
|
||||
|
||||
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。
|
||||
|
||||
## 事件的调度与执行
|
||||
|
||||
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
|
||||
|
||||
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
|
||||
|
||||
```python
|
||||
def aeProcessEvents():
|
||||
# 获取到达时间离当前时间最接近的时间事件
|
||||
time_event = aeSearchNearestTimer()
|
||||
# 计算最接近的时间事件距离到达还有多少毫秒
|
||||
remaind_ms = time_event.when - unix_ts_now()
|
||||
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
|
||||
if remaind_ms < 0:
|
||||
remaind_ms = 0
|
||||
# 根据 remaind_ms 的值,创建 timeval
|
||||
timeval = create_timeval_with_ms(remaind_ms)
|
||||
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
|
||||
aeApiPoll(timeval)
|
||||
# 处理所有已产生的文件事件
|
||||
procesFileEvents()
|
||||
# 处理所有已到达的时间事件
|
||||
processTimeEvents()
|
||||
```
|
||||
|
||||
将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
|
||||
|
||||
```python
|
||||
def main():
|
||||
# 初始化服务器
|
||||
init_server()
|
||||
# 一直处理事件,直到服务器关闭为止
|
||||
while server_is_not_shutdown():
|
||||
aeProcessEvents()
|
||||
# 服务器关闭,执行清理操作
|
||||
clean_server()
|
||||
```
|
||||
|
||||
从事件处理的角度来看,服务器运行流程如下:
|
||||
|
||||
<div align="center"> <img src="../pics//c0a9fa91-da2e-4892-8c9f-80206a6f7047.png" width="400"/> </div><br>
|
||||
|
||||
# 十一、复制
|
||||
|
||||
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
|
||||
|
||||
一个从服务器只能有一个主服务器,并且不支持主主复制。
|
||||
|
||||
## 连接过程
|
||||
|
||||
1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
|
||||
|
||||
2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
|
||||
|
||||
3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
|
||||
|
||||
## 主从链
|
||||
|
||||
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
|
||||
|
||||
<div align="center"> <img src="../pics//395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/> </div><br>
|
||||
|
||||
# 十二、Sentinel
|
||||
|
||||
Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。
|
||||
|
||||
# 十三、分片
|
||||
|
||||
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。
|
||||
|
||||
假设有 4 个 Reids 实例 R0,R1,R2,R3,还有很多表示用户的键 user:1,user:2,... ,有不同的方式来选择一个指定的键存储在哪个实例中。
|
||||
|
||||
- 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
|
||||
- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
|
||||
|
||||
根据执行分片的位置,可以分为三种分片方式:
|
||||
|
||||
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
|
||||
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
|
||||
- 服务器分片:Redis Cluster。
|
||||
|
||||
# 十四、一个简单的论坛系统分析
|
||||
|
||||
该论坛系统功能如下:
|
||||
|
||||
- 可以发布文章;
|
||||
- 可以对文章进行点赞;
|
||||
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示。
|
||||
|
||||
## 文章信息
|
||||
|
||||
文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
|
||||
|
||||
Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID,通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617,其中 article 为命名空间,ID 为 92617。
|
||||
|
||||
<div align="center"> <img src="../pics//7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/> </div><br>
|
||||
|
||||
## 点赞功能
|
||||
|
||||
当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。
|
||||
|
||||
为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。
|
||||
|
||||
<div align="center"> <img src="../pics//485fdf34-ccf8-4185-97c6-17374ee719a0.png" width="400"/> </div><br>
|
||||
|
||||
## 对文章进行排序
|
||||
|
||||
为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
|
||||
|
||||
<div align="center"> <img src="../pics//f7d170a3-e446-4a64-ac2d-cb95028f81a8.png" width="800"/> </div><br>
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
|
||||
- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
|
||||
- [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 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
|
||||
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
|
||||
- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache)
|
@ -1,767 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、基础](#一基础)
|
||||
* [二、创建表](#二创建表)
|
||||
* [三、修改表](#三修改表)
|
||||
* [四、插入](#四插入)
|
||||
* [五、更新](#五更新)
|
||||
* [六、删除](#六删除)
|
||||
* [七、查询](#七查询)
|
||||
* [八、排序](#八排序)
|
||||
* [九、过滤](#九过滤)
|
||||
* [十、通配符](#十通配符)
|
||||
* [十一、计算字段](#十一计算字段)
|
||||
* [十二、函数](#十二函数)
|
||||
* [十三、分组](#十三分组)
|
||||
* [十四、子查询](#十四子查询)
|
||||
* [十五、连接](#十五连接)
|
||||
* [十六、组合查询](#十六组合查询)
|
||||
* [十七、视图](#十七视图)
|
||||
* [十八、存储过程](#十八存储过程)
|
||||
* [十九、游标](#十九游标)
|
||||
* [二十、触发器](#二十触发器)
|
||||
* [二十一、事务管理](#二十一事务管理)
|
||||
* [二十二、字符集](#二十二字符集)
|
||||
* [二十三、权限管理](#二十三权限管理)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、基础
|
||||
|
||||
模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
|
||||
|
||||
主键的值不允许修改,也不允许复用(不能使用已经删除的主键值赋给新数据行的主键)。
|
||||
|
||||
SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
|
||||
|
||||
SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
|
||||
|
||||
SQL 支持以下三种注释:
|
||||
|
||||
```sql
|
||||
# 注释
|
||||
SELECT *
|
||||
FROM mytable; -- 注释
|
||||
/* 注释1
|
||||
注释2 */
|
||||
```
|
||||
|
||||
数据库创建与使用:
|
||||
|
||||
```sql
|
||||
CREATE DATABASE test;
|
||||
USE test;
|
||||
```
|
||||
|
||||
# 二、创建表
|
||||
|
||||
```sql
|
||||
CREATE TABLE mytable (
|
||||
id INT NOT NULL AUTO_INCREMENT,
|
||||
col1 INT NOT NULL DEFAULT 1,
|
||||
col2 VARCHAR(45) NULL,
|
||||
col3 DATE NULL,
|
||||
PRIMARY KEY (`id`));
|
||||
```
|
||||
|
||||
# 三、修改表
|
||||
|
||||
添加列
|
||||
|
||||
```sql
|
||||
ALTER TABLE mytable
|
||||
ADD col CHAR(20);
|
||||
```
|
||||
|
||||
删除列
|
||||
|
||||
```sql
|
||||
ALTER TABLE mytable
|
||||
DROP COLUMN col;
|
||||
```
|
||||
|
||||
删除表
|
||||
|
||||
```sql
|
||||
DROP TABLE mytable;
|
||||
```
|
||||
|
||||
# 四、插入
|
||||
|
||||
普通插入
|
||||
|
||||
```sql
|
||||
INSERT INTO mytable(col1, col2)
|
||||
VALUES(val1, val2);
|
||||
```
|
||||
|
||||
插入检索出来的数据
|
||||
|
||||
```sql
|
||||
INSERT INTO mytable1(col1, col2)
|
||||
SELECT col1, col2
|
||||
FROM mytable2;
|
||||
```
|
||||
|
||||
将一个表的内容插入到一个新表
|
||||
|
||||
```sql
|
||||
CREATE TABLE newtable AS
|
||||
SELECT * FROM mytable;
|
||||
```
|
||||
|
||||
# 五、更新
|
||||
|
||||
```sql
|
||||
UPDATE mytable
|
||||
SET col = val
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
# 六、删除
|
||||
|
||||
```sql
|
||||
DELETE FROM mytable
|
||||
WHERE id = 1;
|
||||
```
|
||||
|
||||
**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
|
||||
|
||||
```sql
|
||||
TRUNCATE TABLE mytable;
|
||||
```
|
||||
|
||||
使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
|
||||
|
||||
# 七、查询
|
||||
|
||||
## DISTINCT
|
||||
|
||||
相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。
|
||||
|
||||
```sql
|
||||
SELECT DISTINCT col1, col2
|
||||
FROM mytable;
|
||||
```
|
||||
|
||||
## LIMIT
|
||||
|
||||
限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
|
||||
|
||||
返回前 5 行:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
LIMIT 5;
|
||||
```
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
LIMIT 0, 5;
|
||||
```
|
||||
|
||||
返回第 3 \~ 5 行:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
LIMIT 2, 3;
|
||||
```
|
||||
|
||||
# 八、排序
|
||||
|
||||
- **ASC** :升序(默认)
|
||||
- **DESC** :降序
|
||||
|
||||
可以按多个列进行排序,并且为每个列指定不同的排序方式:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
ORDER BY col1 DESC, col2 ASC;
|
||||
```
|
||||
|
||||
# 九、过滤
|
||||
|
||||
不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
WHERE col IS NULL;
|
||||
```
|
||||
|
||||
下表显示了 WHERE 子句可用的操作符
|
||||
|
||||
| 操作符 | 说明 |
|
||||
| :---: | :---: |
|
||||
| = | 等于 |
|
||||
| < | 小于 |
|
||||
| > | 大于 |
|
||||
| <> != | 不等于 |
|
||||
| <= !> | 小于等于 |
|
||||
| >= !< | 大于等于 |
|
||||
| BETWEEN | 在两个值之间 |
|
||||
| IS NULL | 为 NULL 值 |
|
||||
|
||||
应该注意到,NULL 与 0、空字符串都不同。
|
||||
|
||||
**AND 和 OR** 用于连接多个过滤条件。优先处理 AND,当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。
|
||||
|
||||
**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
|
||||
|
||||
**NOT** 操作符用于否定一个条件。
|
||||
|
||||
# 十、通配符
|
||||
|
||||
通配符也是用在过滤语句中,但它只能用于文本字段。
|
||||
|
||||
- **%** 匹配 >=0 个任意字符;
|
||||
|
||||
- **\_** 匹配 ==1 个任意字符;
|
||||
|
||||
- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
|
||||
|
||||
使用 Like 来进行通配符匹配。
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本
|
||||
```
|
||||
|
||||
不要滥用通配符,通配符位于开头处匹配会非常慢。
|
||||
|
||||
# 十一、计算字段
|
||||
|
||||
在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。
|
||||
|
||||
计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
|
||||
|
||||
```sql
|
||||
SELECT col1 * col2 AS alias
|
||||
FROM mytable;
|
||||
```
|
||||
|
||||
**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
|
||||
|
||||
```sql
|
||||
SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col
|
||||
FROM mytable;
|
||||
```
|
||||
|
||||
# 十二、函数
|
||||
|
||||
各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。
|
||||
|
||||
## 汇总
|
||||
|
||||
|函 数 |说 明|
|
||||
| :---: | :---: |
|
||||
| AVG() | 返回某列的平均值 |
|
||||
| COUNT() | 返回某列的行数 |
|
||||
| MAX() | 返回某列的最大值 |
|
||||
| MIN() | 返回某列的最小值 |
|
||||
| SUM() |返回某列值之和 |
|
||||
|
||||
AVG() 会忽略 NULL 行。
|
||||
|
||||
使用 DISTINCT 可以让汇总函数值汇总不同的值。
|
||||
|
||||
```sql
|
||||
SELECT AVG(DISTINCT col1) AS avg_col
|
||||
FROM mytable;
|
||||
```
|
||||
|
||||
## 文本处理
|
||||
|
||||
| 函数 | 说明 |
|
||||
| :---: | :---: |
|
||||
| LEFT() | 左边的字符 |
|
||||
| RIGHT() | 右边的字符 |
|
||||
| LOWER() | 转换为小写字符 |
|
||||
| UPPER() | 转换为大写字符 |
|
||||
| LTRIM() | 去除左边的空格 |
|
||||
| RTRIM() | 去除右边的空格 |
|
||||
| LENGTH() | 长度 |
|
||||
| SOUNDEX() | 转换为语音值 |
|
||||
|
||||
其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
WHERE SOUNDEX(col1) = SOUNDEX('apple')
|
||||
```
|
||||
|
||||
## 日期和时间处理
|
||||
|
||||
- 日期格式:YYYY-MM-DD
|
||||
- 时间格式:HH:MM:SS
|
||||
|
||||
|函 数 | 说 明|
|
||||
| :---: | :---: |
|
||||
| AddDate() | 增加一个日期(天、周等)|
|
||||
| AddTime() | 增加一个时间(时、分等)|
|
||||
| CurDate() | 返回当前日期 |
|
||||
| CurTime() | 返回当前时间 |
|
||||
| Date() |返回日期时间的日期部分|
|
||||
| DateDiff() |计算两个日期之差|
|
||||
| Date_Add() |高度灵活的日期运算函数|
|
||||
| Date_Format() |返回一个格式化的日期或时间串|
|
||||
| Day()| 返回一个日期的天数部分|
|
||||
| DayOfWeek() |对于一个日期,返回对应的星期几|
|
||||
| Hour() |返回一个时间的小时部分|
|
||||
| Minute() |返回一个时间的分钟部分|
|
||||
| Month() |返回一个日期的月份部分|
|
||||
| Now() |返回当前日期和时间|
|
||||
| Second() |返回一个时间的秒部分|
|
||||
| Time() |返回一个日期时间的时间部分|
|
||||
| Year() |返回一个日期的年份部分|
|
||||
|
||||
```sql
|
||||
mysql> SELECT NOW();
|
||||
```
|
||||
|
||||
```
|
||||
2018-4-14 20:25:11
|
||||
```
|
||||
|
||||
## 数值处理
|
||||
|
||||
| 函数 | 说明 |
|
||||
| :---: | :---: |
|
||||
| SIN() | 正弦 |
|
||||
| COS() | 余弦 |
|
||||
| TAN() | 正切 |
|
||||
| ABS() | 绝对值 |
|
||||
| SQRT() | 平方根 |
|
||||
| MOD() | 余数 |
|
||||
| EXP() | 指数 |
|
||||
| PI() | 圆周率 |
|
||||
| RAND() | 随机数 |
|
||||
|
||||
# 十三、分组
|
||||
|
||||
分组就是把具有相同的数据值的行放在同一组中。
|
||||
|
||||
可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。
|
||||
|
||||
指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。
|
||||
|
||||
```sql
|
||||
SELECT col, COUNT(*) AS num
|
||||
FROM mytable
|
||||
GROUP BY col;
|
||||
```
|
||||
|
||||
GROUP BY 自动按分组字段进行排序,ORDER BY 也可以按汇总字段来进行排序。
|
||||
|
||||
```sql
|
||||
SELECT col, COUNT(*) AS num
|
||||
FROM mytable
|
||||
GROUP BY col
|
||||
ORDER BY num;
|
||||
```
|
||||
|
||||
WHERE 过滤行,HAVING 过滤分组,行过滤应当先于分组过滤。
|
||||
|
||||
```sql
|
||||
SELECT col, COUNT(*) AS num
|
||||
FROM mytable
|
||||
WHERE col > 2
|
||||
GROUP BY col
|
||||
HAVING num >= 2;
|
||||
```
|
||||
|
||||
分组规定:
|
||||
|
||||
- GROUP BY 子句出现在 WHERE 子句之后,ORDER BY 子句之前;
|
||||
- 除了汇总字段外,SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
|
||||
- NULL 的行会单独分为一组;
|
||||
- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
|
||||
|
||||
# 十四、子查询
|
||||
|
||||
子查询中只能返回一个字段的数据。
|
||||
|
||||
可以将子查询的结果作为 WHRER 语句的过滤条件:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable1
|
||||
WHERE col1 IN (SELECT col2
|
||||
FROM mytable2);
|
||||
```
|
||||
|
||||
下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:
|
||||
|
||||
```sql
|
||||
SELECT cust_name, (SELECT COUNT(*)
|
||||
FROM Orders
|
||||
WHERE Orders.cust_id = Customers.cust_id)
|
||||
AS orders_num
|
||||
FROM Customers
|
||||
ORDER BY cust_name;
|
||||
```
|
||||
|
||||
# 十五、连接
|
||||
|
||||
连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。
|
||||
|
||||
连接可以替换子查询,并且比子查询的效率一般会更快。
|
||||
|
||||
可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
|
||||
|
||||
## 内连接
|
||||
|
||||
内连接又称等值连接,使用 INNER JOIN 关键字。
|
||||
|
||||
```sql
|
||||
SELECT A.value, B.value
|
||||
FROM tablea AS A INNER JOIN tableb AS B
|
||||
ON A.key = B.key;
|
||||
```
|
||||
|
||||
可以不明确使用 INNER JOIN,而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
|
||||
|
||||
```sql
|
||||
SELECT A.value, B.value
|
||||
FROM tablea AS A, tableb AS B
|
||||
WHERE A.key = B.key;
|
||||
```
|
||||
|
||||
在没有条件语句的情况下返回笛卡尔积。
|
||||
|
||||
## 自连接
|
||||
|
||||
自连接可以看成内连接的一种,只是连接的表是自身而已。
|
||||
|
||||
一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
|
||||
|
||||
子查询版本
|
||||
|
||||
```sql
|
||||
SELECT name
|
||||
FROM employee
|
||||
WHERE department = (
|
||||
SELECT department
|
||||
FROM employee
|
||||
WHERE name = "Jim");
|
||||
```
|
||||
|
||||
自连接版本
|
||||
|
||||
```sql
|
||||
SELECT e1.name
|
||||
FROM employee AS e1 INNER JOIN employee AS e2
|
||||
ON e1.department = e2.department
|
||||
AND e2.name = "Jim";
|
||||
```
|
||||
|
||||
## 自然连接
|
||||
|
||||
自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
|
||||
|
||||
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
|
||||
|
||||
```sql
|
||||
SELECT A.value, B.value
|
||||
FROM tablea AS A NATURAL JOIN tableb AS B;
|
||||
```
|
||||
|
||||
## 外连接
|
||||
|
||||
外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。
|
||||
|
||||
检索所有顾客的订单信息,包括还没有订单信息的顾客。
|
||||
|
||||
```sql
|
||||
SELECT Customers.cust_id, Orders.order_num
|
||||
FROM Customers LEFT OUTER JOIN Orders
|
||||
ON Customers.cust_id = Orders.cust_id;
|
||||
```
|
||||
|
||||
customers 表:
|
||||
|
||||
| cust_id | cust_name |
|
||||
| :---: | :---: |
|
||||
| 1 | a |
|
||||
| 2 | b |
|
||||
| 3 | c |
|
||||
|
||||
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 |
|
||||
|
||||
# 十六、组合查询
|
||||
|
||||
使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
|
||||
|
||||
每个查询必须包含相同的列、表达式和聚集函数。
|
||||
|
||||
默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
|
||||
|
||||
只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
|
||||
|
||||
```sql
|
||||
SELECT col
|
||||
FROM mytable
|
||||
WHERE col = 1
|
||||
UNION
|
||||
SELECT col
|
||||
FROM mytable
|
||||
WHERE col =2;
|
||||
```
|
||||
|
||||
# 十七、视图
|
||||
|
||||
视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。
|
||||
|
||||
对视图的操作和对普通表的操作一样。
|
||||
|
||||
视图具有如下好处:
|
||||
|
||||
- 简化复杂的 SQL 操作,比如复杂的连接;
|
||||
- 只使用实际表的一部分数据;
|
||||
- 通过只给用户访问视图的权限,保证数据的安全性;
|
||||
- 更改数据格式和表示。
|
||||
|
||||
```sql
|
||||
CREATE VIEW myview AS
|
||||
SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col
|
||||
FROM mytable
|
||||
WHERE col5 = val;
|
||||
```
|
||||
|
||||
# 十八、存储过程
|
||||
|
||||
存储过程可以看成是对一系列 SQL 操作的批处理。
|
||||
|
||||
使用存储过程的好处:
|
||||
|
||||
- 代码封装,保证了一定的安全性;
|
||||
- 代码复用;
|
||||
- 由于是预先编译,因此具有很高的性能。
|
||||
|
||||
命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
|
||||
|
||||
包含 in、out 和 inout 三种参数。
|
||||
|
||||
给变量赋值都需要用 select into 语句。
|
||||
|
||||
每次只能给一个变量赋值,不支持集合的操作。
|
||||
|
||||
```sql
|
||||
delimiter //
|
||||
|
||||
create procedure myprocedure( out ret int )
|
||||
begin
|
||||
declare y int;
|
||||
select sum(col1)
|
||||
from mytable
|
||||
into y;
|
||||
select y*y into ret;
|
||||
end //
|
||||
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
```sql
|
||||
call myprocedure(@ret);
|
||||
select @ret;
|
||||
```
|
||||
|
||||
# 十九、游标
|
||||
|
||||
在存储过程中使用游标可以对一个结果集进行移动遍历。
|
||||
|
||||
游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。
|
||||
|
||||
使用游标的四个步骤:
|
||||
|
||||
1. 声明游标,这个过程没有实际检索出数据;
|
||||
2. 打开游标;
|
||||
3. 取出数据;
|
||||
4. 关闭游标;
|
||||
|
||||
```sql
|
||||
delimiter //
|
||||
create procedure myprocedure(out ret int)
|
||||
begin
|
||||
declare done boolean default 0;
|
||||
|
||||
declare mycursor cursor for
|
||||
select col1 from mytable;
|
||||
# 定义了一个 continue handler,当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
|
||||
declare continue handler for sqlstate '02000' set done = 1;
|
||||
|
||||
open mycursor;
|
||||
|
||||
repeat
|
||||
fetch mycursor into ret;
|
||||
select ret;
|
||||
until done end repeat;
|
||||
|
||||
close mycursor;
|
||||
end //
|
||||
delimiter ;
|
||||
```
|
||||
|
||||
# 二十、触发器
|
||||
|
||||
触发器会在某个表执行以下语句时而自动执行:DELETE、INSERT、UPDATE。
|
||||
|
||||
触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化,AFTER 用于审计跟踪,将修改记录到另外一张表中。
|
||||
|
||||
INSERT 触发器包含一个名为 NEW 的虚拟表。
|
||||
|
||||
```sql
|
||||
CREATE TRIGGER mytrigger AFTER INSERT ON mytable
|
||||
FOR EACH ROW SELECT NEW.col into @result;
|
||||
|
||||
SELECT @result; -- 获取结果
|
||||
```
|
||||
|
||||
DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
|
||||
|
||||
UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改的,而 OLD 是只读的。
|
||||
|
||||
MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
|
||||
|
||||
# 二十一、事务管理
|
||||
|
||||
基本术语:
|
||||
|
||||
- 事务(transaction)指一组 SQL 语句;
|
||||
- 回退(rollback)指撤销指定 SQL 语句的过程;
|
||||
- 提交(commit)指将未存储的 SQL 语句结果写入数据库表;
|
||||
- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),你可以对它发布回退(与回退整个事务处理不同)。
|
||||
|
||||
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
|
||||
|
||||
MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
|
||||
|
||||
通过设置 autocommit 为 0 可以取消自动提交;autocommit 标记是针对每个连接而不是针对服务器的。
|
||||
|
||||
如果没有设置保留点,ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
|
||||
|
||||
```sql
|
||||
START TRANSACTION
|
||||
// ...
|
||||
SAVEPOINT delete1
|
||||
// ...
|
||||
ROLLBACK TO delete1
|
||||
// ...
|
||||
COMMIT
|
||||
```
|
||||
|
||||
# 二十二、字符集
|
||||
|
||||
基本术语:
|
||||
|
||||
- 字符集为字母和符号的集合;
|
||||
- 编码为某个字符集成员的内部表示;
|
||||
- 校对字符指定如何比较,主要用于排序和分组。
|
||||
|
||||
除了给表指定字符集和校对外,也可以给列指定:
|
||||
|
||||
```sql
|
||||
CREATE TABLE mytable
|
||||
(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
|
||||
DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
|
||||
```
|
||||
|
||||
可以在排序、分组时指定校对:
|
||||
|
||||
```sql
|
||||
SELECT *
|
||||
FROM mytable
|
||||
ORDER BY col COLLATE latin1_general_ci;
|
||||
```
|
||||
|
||||
# 二十三、权限管理
|
||||
|
||||
MySQL 的账户信息保存在 mysql 这个数据库中。
|
||||
|
||||
```sql
|
||||
USE mysql;
|
||||
SELECT user FROM user;
|
||||
```
|
||||
|
||||
**创建账户**
|
||||
|
||||
新创建的账户没有任何权限。
|
||||
|
||||
```sql
|
||||
CREATE USER myuser IDENTIFIED BY 'mypassword';
|
||||
```
|
||||
|
||||
**修改账户名**
|
||||
|
||||
```sql
|
||||
RENAME myuser TO newuser;
|
||||
```
|
||||
|
||||
**删除账户**
|
||||
|
||||
```sql
|
||||
DROP USER myuser;
|
||||
```
|
||||
|
||||
**查看权限**
|
||||
|
||||
```sql
|
||||
SHOW GRANTS FOR myuser;
|
||||
```
|
||||
|
||||
**授予权限**
|
||||
|
||||
账户用 username@host 的形式定义,username@% 使用的是默认主机名。
|
||||
|
||||
```sql
|
||||
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
|
||||
```
|
||||
|
||||
**删除权限**
|
||||
|
||||
GRANT 和 REVOKE 可在几个层次上控制访问权限:
|
||||
|
||||
- 整个服务器,使用 GRANT ALL 和 REVOKE ALL;
|
||||
- 整个数据库,使用 ON database.\*;
|
||||
- 特定的表,使用 ON database.table;
|
||||
- 特定的列;
|
||||
- 特定的存储过程。
|
||||
|
||||
```sql
|
||||
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
|
||||
```
|
||||
|
||||
**更改密码**
|
||||
|
||||
必须使用 Password() 函数
|
||||
|
||||
```sql
|
||||
SET PASSWROD FOR myuser = Password('new_password');
|
||||
```
|
||||
|
||||
# 参考资料
|
||||
|
||||
- BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.
|
@ -1,323 +0,0 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、I/O 模型](#一io-模型)
|
||||
* [阻塞式 I/O](#阻塞式-io)
|
||||
* [非阻塞式 I/O](#非阻塞式-io)
|
||||
* [I/O 复用](#io-复用)
|
||||
* [信号驱动 I/O](#信号驱动-io)
|
||||
* [异步 I/O](#异步-io)
|
||||
* [五大 I/O 模型比较](#五大-io-模型比较)
|
||||
* [二、I/O 复用](#二io-复用)
|
||||
* [select](#select)
|
||||
* [poll](#poll)
|
||||
* [比较](#比较)
|
||||
* [epoll](#epoll)
|
||||
* [工作模式](#工作模式)
|
||||
* [应用场景](#应用场景)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、I/O 模型
|
||||
|
||||
一个输入操作通常包括两个阶段:
|
||||
|
||||
- 等待数据准备好
|
||||
- 从内核向进程复制数据
|
||||
|
||||
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
|
||||
|
||||
Unix 有五种 I/O 模型:
|
||||
|
||||
- 阻塞式 I/O
|
||||
- 非阻塞式 I/O
|
||||
- I/O 复用(select 和 poll)
|
||||
- 信号驱动式 I/O(SIGIO)
|
||||
- 异步 I/O(AIO)
|
||||
|
||||
## 阻塞式 I/O
|
||||
|
||||
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
|
||||
|
||||
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,所以不消耗 CPU 时间,这种模型的 CPU 利用率效率会比较高。
|
||||
|
||||
下图中,recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
|
||||
|
||||
```c
|
||||
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
|
||||
```
|
||||
|
||||
<div align="center"> <img src="../pics//1492928416812_4.png"/> </div><br>
|
||||
|
||||
## 非阻塞式 I/O
|
||||
|
||||
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询(polling)。
|
||||
|
||||
由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率是比较低的。
|
||||
|
||||
<div align="center"> <img src="../pics//1492929000361_5.png"/> </div><br>
|
||||
|
||||
## I/O 复用
|
||||
|
||||
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
|
||||
|
||||
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O,即事件驱动 I/O。
|
||||
|
||||
如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接,那么就需要创建相同数量的线程。相比于多进程和多线程技术,I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
|
||||
|
||||
<div align="center"> <img src="../pics//1492929444818_6.png"/> </div><br>
|
||||
|
||||
## 信号驱动 I/O
|
||||
|
||||
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
|
||||
|
||||
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
|
||||
|
||||
<div align="center"> <img src="../pics//1492929553651_7.png"/> </div><br>
|
||||
|
||||
## 异步 I/O
|
||||
|
||||
应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
|
||||
|
||||
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
|
||||
|
||||
<div align="center"> <img src="../pics//1492930243286_8.png"/> </div><br>
|
||||
|
||||
## 五大 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//1492928105791_3.png"/> </div><br>
|
||||
|
||||
# 二、I/O 复用
|
||||
|
||||
select/poll/epoll 都是 I/O 多路复用的具体实现,select 出现的最早,之后是 poll,再是 epoll。
|
||||
|
||||
## select
|
||||
|
||||
```c
|
||||
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
|
||||
```
|
||||
|
||||
有三种类型的描述符类型:readset、writeset、exceptset,分别对应读、写、异常条件的描述符集合。fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义。
|
||||
|
||||
timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
|
||||
|
||||
成功调用返回结果大于 0,出错返回结果为 -1,超时返回结果为 0。
|
||||
|
||||
```c
|
||||
fd_set fd_in, fd_out;
|
||||
struct timeval tv;
|
||||
|
||||
// Reset the sets
|
||||
FD_ZERO( &fd_in );
|
||||
FD_ZERO( &fd_out );
|
||||
|
||||
// Monitor sock1 for input events
|
||||
FD_SET( sock1, &fd_in );
|
||||
|
||||
// Monitor sock2 for output events
|
||||
FD_SET( sock2, &fd_out );
|
||||
|
||||
// Find out which socket has the largest numeric value as select requires it
|
||||
int largest_sock = sock1 > sock2 ? sock1 : sock2;
|
||||
|
||||
// Wait up to 10 seconds
|
||||
tv.tv_sec = 10;
|
||||
tv.tv_usec = 0;
|
||||
|
||||
// Call the select
|
||||
int ret = select( largest_sock + 1, &fd_in, &fd_out, NULL, &tv );
|
||||
|
||||
// Check if select actually succeed
|
||||
if ( ret == -1 )
|
||||
// report error and abort
|
||||
else if ( ret == 0 )
|
||||
// timeout; no event detected
|
||||
else
|
||||
{
|
||||
if ( FD_ISSET( sock1, &fd_in ) )
|
||||
// input event on sock1
|
||||
|
||||
if ( FD_ISSET( sock2, &fd_out ) )
|
||||
// output event on sock2
|
||||
}
|
||||
```
|
||||
|
||||
## poll
|
||||
|
||||
```c
|
||||
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
|
||||
```
|
||||
|
||||
pollfd 使用链表实现。
|
||||
|
||||
```c
|
||||
// The structure for two events
|
||||
struct pollfd fds[2];
|
||||
|
||||
// Monitor sock1 for input
|
||||
fds[0].fd = sock1;
|
||||
fds[0].events = POLLIN;
|
||||
|
||||
// Monitor sock2 for output
|
||||
fds[1].fd = sock2;
|
||||
fds[1].events = POLLOUT;
|
||||
|
||||
// Wait 10 seconds
|
||||
int ret = poll( &fds, 2, 10000 );
|
||||
// Check if poll actually succeed
|
||||
if ( ret == -1 )
|
||||
// report error and abort
|
||||
else if ( ret == 0 )
|
||||
// timeout; no event detected
|
||||
else
|
||||
{
|
||||
// If we detect the event, zero it out so we can reuse the structure
|
||||
if ( fds[0].revents & POLLIN )
|
||||
fds[0].revents = 0;
|
||||
// input event on sock1
|
||||
|
||||
if ( fds[1].revents & POLLOUT )
|
||||
fds[1].revents = 0;
|
||||
// output event on sock2
|
||||
}
|
||||
```
|
||||
|
||||
## 比较
|
||||
|
||||
### 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
|
||||
int epoll_create(int size);
|
||||
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
|
||||
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
|
||||
```
|
||||
|
||||
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。
|
||||
|
||||
从上面的描述可以看出,epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。
|
||||
|
||||
epoll 仅适用于 Linux OS。
|
||||
|
||||
epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。
|
||||
|
||||
epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。
|
||||
|
||||
```c
|
||||
// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
|
||||
// The function argument is ignored (it was not before, but now it is), so put your favorite number here
|
||||
int pollingfd = epoll_create( 0xCAFE );
|
||||
|
||||
if ( pollingfd < 0 )
|
||||
// report error
|
||||
|
||||
// Initialize the epoll structure in case more members are added in future
|
||||
struct epoll_event ev = { 0 };
|
||||
|
||||
// Associate the connection class instance with the event. You can associate anything
|
||||
// you want, epoll does not use this information. We store a connection class pointer, pConnection1
|
||||
ev.data.ptr = pConnection1;
|
||||
|
||||
// Monitor for input, and do not automatically rearm the descriptor after the event
|
||||
ev.events = EPOLLIN | EPOLLONESHOT;
|
||||
// Add the descriptor into the monitoring list. We can do it even if another thread is
|
||||
// waiting in epoll_wait - the descriptor will be properly added
|
||||
if ( epoll_ctl( epollfd, EPOLL_CTL_ADD, pConnection1->getSocket(), &ev ) != 0 )
|
||||
// report error
|
||||
|
||||
// Wait for up to 20 events (assuming we have added maybe 200 sockets before that it may happen)
|
||||
struct epoll_event pevents[ 20 ];
|
||||
|
||||
// Wait for 10 seconds, and retrieve less than 20 epoll_event and store them into epoll_event array
|
||||
int ready = epoll_wait( pollingfd, pevents, 20, 10000 );
|
||||
// Check if epoll actually succeed
|
||||
if ( ret == -1 )
|
||||
// report error and abort
|
||||
else if ( ret == 0 )
|
||||
// timeout; no event detected
|
||||
else
|
||||
{
|
||||
// Check if any events detected
|
||||
for ( int i = 0; i < ret; i++ )
|
||||
{
|
||||
if ( pevents[i].events & EPOLLIN )
|
||||
{
|
||||
// Get back our connection pointer
|
||||
Connection * c = (Connection*) pevents[i].data.ptr;
|
||||
c->handleReadEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## 工作模式
|
||||
|
||||
epoll 的描述符事件有两种触发模式:LT(level trigger)和 ET(edge trigger)。
|
||||
|
||||
### 1. LT 模式
|
||||
|
||||
当 epoll_wait() 检测到描述符事件到达时,将此事件通知进程,进程可以不立即处理该事件,下次调用 epoll_wait() 会再次通知进程。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
|
||||
|
||||
### 2. ET 模式
|
||||
|
||||
和 LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
|
||||
|
||||
很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
|
||||
|
||||
## 应用场景
|
||||
|
||||
很容易产生一种错觉认为只要用 epoll 就可以了,select 和 poll 都已经过时了,其实它们都有各自的使用场景。
|
||||
|
||||
### 1. select 应用场景
|
||||
|
||||
select 的 timeout 参数精度为 1ns,而 poll 和 epoll 为 1ms,因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。
|
||||
|
||||
select 可移植性更好,几乎被所有主流平台所支持。
|
||||
|
||||
### 2. poll 应用场景
|
||||
|
||||
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
|
||||
|
||||
### 3. epoll 应用场景
|
||||
|
||||
只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
|
||||
|
||||
需要同时监控小于 1000 个描述符,就没有必要使用 epoll,因为这个应用场景下并不能体现 epoll 的优势。
|
||||
|
||||
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
|
||||
- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/)
|
||||
- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
|
||||
- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
|
||||
- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
|
||||
- [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/)
|
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 409 KiB |
Before Width: | Height: | Size: 37 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 9.0 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 129 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 7.0 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 87 KiB |
Before Width: | Height: | Size: 286 KiB |
Before Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 146 KiB |
Before Width: | Height: | Size: 185 KiB |
Before Width: | Height: | Size: 214 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 5.0 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 47 KiB |
Before Width: | Height: | Size: 43 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 430 KiB |
Before Width: | Height: | Size: 45 KiB |
Before Width: | Height: | Size: 44 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 8.0 KiB |
Before Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 41 KiB |
Before Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 6.0 KiB |
Before Width: | Height: | Size: 32 KiB |