auto commit

This commit is contained in:
CyC2018 2018-06-04 14:29:04 +08:00
parent 4bfd44a5ca
commit b0ba55fa31
28 changed files with 11805 additions and 11162 deletions

View File

@ -1,13 +1,29 @@
# 学习资料
<!-- GFM-TOC -->
* [学习资料](#学习资料)
* [集中式与分布式](#集中式与分布式)
* [Git 的中心服务器](#git-的中心服务器)
* [Git 工作流](#git-工作流)
* [分支实现](#分支实现)
* [冲突](#冲突)
* [Fast forward](#fast-forward)
* [分支管理策略](#分支管理策略)
* [储藏Stashing](#储藏stashing)
* [SSH 传输设置](#ssh-传输设置)
* [.gitignore 文件](#gitignore-文件)
* [Git 命令一览](#git-命令一览)
<!-- GFM-TOC -->
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)
# 集中式与分布式
# 学习资料
Git 属于分布式版本控制系统 SVN 属于集中式。
- [Git - 简明指南](http://rogerdudler.github.io/git-guide/index.zh.html)
- [图解 Git](http://marklodato.github.io/visual-git-guide/index-zh-cn.html)
- [廖雪峰 : Git 教程](https://www.liaoxuefeng.com/wiki/0013739516305929606dd18361248578c67b8067c8c017b000)
- [Learn Git Branching](https://learngitbranching.js.org/)
# 集中式与分布式
Git 属于分布式版本控制系统,而 SVN 属于集中式。
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
@ -17,125 +33,125 @@ Git 属于分布式版本控制系统 SVN 属于集中式。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
# Git 的中心服务器
# Git 的中心服务器
Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。
Git 的中心服务器用来交换每个用户的修改。没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态这样就能更方便的交换修改。Github 就是一种 Git 中心服务器。
# Git 工作流
# Git 工作流
![](index_files/a1198642-9159-4d88-8aec-c3b04e7a2563.jpg)
<div align="center"> <img src="../pics//a1198642-9159-4d88-8aec-c3b04e7a2563.jpg"/> </div><br>
新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git它属于 Git 的版本库。
新建一个仓库之后,当前目录就成为了工作区,工作区下有一个隐藏目录 .git它属于 Git 的版本库。
Git 版本库有一个称为 stage 的暂存区还有自动创建的 master 分支以及指向分支的 HEAD 指针。
Git 版本库有一个称为 stage 的暂存区,还有自动创建的 master 分支以及指向分支的 HEAD 指针。
![](index_files/46f66e88-e65a-4ad0-a060-3c63fe22947c.png)
<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 使用暂存区的修改覆盖工作目录,用来撤销本地修改
- git add files 把文件的修改添加到暂存区
- git commit 把暂存区的修改提交到当前分支,提交之后暂存区就被清空了
- git reset -- files 使用当前分支上的修改覆盖暂缓区,用来撤销最后一次 git add files
- git checkout -- files 使用暂存区的修改覆盖工作目录,用来撤销本地修改
![](index_files/17976404-95f5-480e-9cb4-250e6aa1d55f.png)
<div align="center"> <img src="../pics//17976404-95f5-480e-9cb4-250e6aa1d55f.png"/> </div><br>
可以跳过暂存区域直接从分支中取出修改或者直接提交修改到分支中
- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交
- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
- git commit -a 直接把所有文件的修改添加到暂缓区然后执行提交
- git checkout HEAD -- files 取出最后一次修改,可以用来进行回滚操作
# 分支实现
# 分支实现
Git 把每次提交都连成一条时间线。分支使用指针来实现例如 master 分支指针指向时间线的最后一个节点也就是最后一次提交。HEAD 指针指向的是当前分支。
Git 把每次提交都连成一条时间线。分支使用指针来实现,例如 master 分支指针指向时间线的最后一个节点也就是最后一次提交。HEAD 指针指向的是当前分支。
![](index_files/fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg)
<div align="center"> <img src="../pics//fb546e12-e1fb-4b72-a1fb-8a7f5000dce6.jpg"/> </div><br>
新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
新建分支是新建一个指针指向时间线的最后一个节点,并让 HEAD 指针指向新分支表示新分支成为当前分支。
![](index_files/bc775758-89ab-4805-9f9c-78b8739cf780.jpg)
<div align="center"> <img src="../pics//bc775758-89ab-4805-9f9c-78b8739cf780.jpg"/> </div><br>
每次提交只会让当前分支向前移动,而其它分支不会移动。
![](index_files/5292faa6-0141-4638-bf0f-bb95b081dcba.jpg)
<div align="center"> <img src="../pics//5292faa6-0141-4638-bf0f-bb95b081dcba.jpg"/> </div><br>
合并分支也只需要改变指针即可。
![](index_files/1164a71f-413d-494a-9cc8-679fb6a2613d.jpg)
<div align="center"> <img src="../pics//1164a71f-413d-494a-9cc8-679fb6a2613d.jpg"/> </div><br>
# 冲突
# 冲突
当两个分支都对同一个文件的同一行进行了修改,在分支合并时就会产生冲突。
![](index_files/58e57a21-6b6b-40b6-af85-956dd4e0f55a.jpg)
<div align="center"> <img src="../pics//58e57a21-6b6b-40b6-af85-956dd4e0f55a.jpg"/> </div><br>
Git 会使用 <<<<<<< ======= >>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
Git 会使用 <<<<<<< ======= >>>>>>> 标记出不同分支的内容,只需要把不同分支中冲突部分修改成一样就能解决冲突。
```
<<<<<<< HEAD
Creating a new branch is quick & simple.
<<<<<<< HEAD
Creating a new branch is quick & simple.
=======
Creating a new branch is quick AND simple.
>>>>>>> feature1
Creating a new branch is quick AND simple.
>>>>>>> feature1
```
# Fast forward
# Fast forward
"快进式合并"fast-farward merge会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
"快进式合并"fast-farward merge会直接将 master 分支指向合并的分支,这种模式下进行分支合并会丢失分支信息,也就不能在分支历史上看出分支信息。
可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式并且加上 -m 参数让合并时产生一个新的 commit。
可以在合并时加上 --no-ff 参数来禁用 Fast forward 模式,并且加上 -m 参数让合并时产生一个新的 commit。
```
$ git merge --no-ff -m "merge with no-ff" dev
$ git merge --no-ff -m "merge with no-ff" dev
```
![](index_files/dd78a1fe-1ff3-4bcf-a56f-8c003995beb6.jpg)
<div align="center"> <img src="../pics//dd78a1fe-1ff3-4bcf-a56f-8c003995beb6.jpg"/> </div><br>
# 分支管理策略
# 分支管理策略
master 分支应该是非常稳定的,只用来发布新版本;
master 分支应该是非常稳定的,只用来发布新版本;
日常开发在开发分支 dev 上进行。
日常开发在开发分支 dev 上进行。
![](index_files/245fd2fb-209c-4ad5-bc5e-eb5664966a0e.jpg)
<div align="center"> <img src="../pics//245fd2fb-209c-4ad5-bc5e-eb5664966a0e.jpg"/> </div><br>
# 储藏Stashing
# 储藏Stashing
在一个分支上操作之后,如果还没有将修改提交到分支上,此时进行切换分支,那么另一个分支上也能看到新的修改。这是因为所有分支都共用一个工作区的缘故。
可以使用 git stash 将当前分支的修改储藏起来,此时当前工作区的所有修改都会被存到栈上,也就是说当前工作区是干净的,没有任何未提交的修改。此时就可以安全的切换到其它分支上了。
可以使用 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")
$ 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 分支的未提交修改储藏起来。
该功能可以用于 bug 分支的实现。如果当前正在 dev 分支上进行开发,但是此时 master 上有个 bug 需要修复,但是 dev 分支上的开发还未完成,不想立即提交。在新建 bug 分支并切换到 bug 分支之前就需要使用 git stash 将 dev 分支的未提交修改储藏起来。
# SSH 传输设置
# SSH 传输设置
Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
Git 仓库和 Github 中心仓库之间的传输是通过 SSH 加密。
如果工作区下没有 .ssh 目录或者该目录下没有 id_rsa  id_rsa.pub 这两个文件可以通过以下命令来创建 SSH Key
如果工作区下没有 .ssh 目录,或者该目录下没有 id_rsa 和 id_rsa.pub 这两个文件,可以通过以下命令来创建 SSH Key
```
$ ssh-keygen -t rsa -C "youremail@example.com"
$ ssh-keygen -t rsa -C "youremail@example.com"
```
然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings"  SSH Keys 中。
然后把公钥 id_rsa.pub 的内容复制到 Github "Account settings" 的 SSH Keys 中。
# .gitignore 文件
# .gitignore 文件
忽略以下文件:
- 操作系统自动生成的文件,比如缩略图;
- 编译生成的中间文件比如 Java 编译产生的 .class 文件;
- 自己的敏感信息,比如存放口令的配置文件。
- 操作系统自动生成的文件,比如缩略图;
- 编译生成的中间文件,比如 Java 编译产生的 .class 文件;
- 自己的敏感信息,比如存放口令的配置文件。
不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
不需要全部自己编写,可以到 [https://github.com/github/gitignore](https://github.com/github/gitignore) 中进行查询。
# Git 命令一览
# Git 命令一览
![](index_files/7a29acce-f243-4914-9f00-f2988c528412.jpg)
<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

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +1,91 @@
# 一、概览
<!-- GFM-TOC -->
* [一、概览](#一概览)
* [二、磁盘操作](#二磁盘操作)
* [三、字节操作](#三字节操作)
* [四、字符操作](#四字符操作)
* [五、对象操作](#五对象操作)
* [六、网络操作](#六网络操作)
* [InetAddress](#inetaddress)
* [URL](#url)
* [Sockets](#sockets)
* [Datagram](#datagram)
* [七、NIO](#七nio)
* [流与块](#流与块)
* [通道与缓冲区](#通道与缓冲区)
* [缓冲区状态变量](#缓冲区状态变量)
* [文件 NIO 实例](#文件-nio-实例)
* [选择器](#选择器)
* [套接字 NIO 实例](#套接字-nio-实例)
* [内存映射文件](#内存映射文件)
* [对比](#对比)
* [八、参考资料](#八参考资料)
<!-- GFM-TOC -->
Java  I/O 大概可以分成以下几类
1. 磁盘操作File
2. 字节操作InputStream  OutputStream
3. 字符操作Reader  Writer
4. 对象操作Serializable
5. 网络操作Socket
6. 新的输入/输出NIO
# 一、概览
# 二、磁盘操作
Java 的 I/O 大概可以分成以下几类:
File 类可以用于表示文件和目录但是它只用于表示文件的信息而不表示文件的内容。
1. 磁盘操作File
2. 字节操作InputStream 和 OutputStream
3. 字符操作Reader 和 Writer
4. 对象操作Serializable
5. 网络操作Socket
6. 新的输入/输出NIO
# 三、字节操作
# 二、磁盘操作
<img src="index_files/DP-Decorator-java.io.png" width="500"/>
File 类可以用于表示文件和目录,但是它只用于表示文件的信息,而不表示文件的内容。
Java I/O 使用了装饰者模式来实现。以 InputStream 为例InputStream 是抽象组件FileInputStream  InputStream 的子类属于具体组件提供了字节流的输入操作。FilterInputStream 属于抽象装饰者装饰者用于装饰组件为组件提供额外的功能例如 BufferedInputStream  FileInputStream 提供缓存的功能。
# 三、字节操作
实例化一个具有缓存功能的字节流对象时只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
<div align="center"> <img src="../pics//DP-Decorator-java.io.png" width="500"/> </div><br>
Java I/O 使用了装饰者模式来实现。以 InputStream 为例InputStream 是抽象组件FileInputStream 是 InputStream 的子类属于具体组件提供了字节流的输入操作。FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能,例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file));
```
DataInputStream 装饰者提供了对更多数据类型进行输入的操作比如 int、double 等基本类型。
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
批量读入文件内容到字节数组:
```java
byte[] buf = new byte[20*1024];
int bytes = 0;
// 最多读取 buf.length 个字节返回的是实际读取的个数返回 -1 的时候表示读到 eof即文件尾
while((bytes = in.read(buf, 0 , buf.length)) != -1) {
    // ...
byte[] buf = new byte[20*1024];
int bytes = 0;
// 最多读取 buf.length 个字节,返回的是实际读取的个数,返回 -1 的时候表示读到 eof即文件尾
while((bytes = in.read(buf, 0 , buf.length)) != -1) {
// ...
}
```
# 四、字符操作
# 四、字符操作
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
InputStreamReader 实现从文本文件的字节流解码成字符流OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader  Writer。
InputStreamReader 实现从文本文件的字节流解码成字符流OutputStreamWriter 实现字符流编码成为文本文件的字节流。它们继承自 Reader 和 Writer。
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
```java
byte[] bytes = str.getBytes(encoding);     // 编码
String str = new String(bytes, encoding) // 解码
byte[] bytes = str.getBytes(encoding); // 编码
String str = new String(bytes, encoding) // 解码
```
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
- GBK 编码中中文占 2 个字节英文占 1 个字节;
- UTF-8 编码中中文占 3 个字节英文占 1 个字节;
- UTF-16be 编码中中文和英文都占 2 个字节。
- GBK 编码中,中文占 2 个字节,英文占 1 个字节;
- UTF-8 编码中,中文占 3 个字节,英文占 1 个字节;
- UTF-16be 编码中,中文和英文都占 2 个字节。
UTF-16be 中的 be 指的是 Big Endian也就是大端。相应地也有 UTF-16lele 指的是 Little Endian也就是小端。
UTF-16be 中的 be 指的是 Big Endian也就是大端。相应地也有 UTF-16lele 指的是 Little Endian也就是小端。
Java 使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 也就是两个字节Java 使用这种双字节编码正是为了让一个中文或者一个英文都能使用一个 char 来存储。
Java 使用双字节编码 UTF-16be这不是指 Java 只支持这一种编码方式,而是说 char 这种类型使用 UTF-16be 进行编码。char 类型占 16 位也就是两个字节Java 使用这种双字节编码正是为了让一个中文或者一个英文都能使用一个 char 来存储。
# 五、对象操作
# 五、对象操作
序列化就是将一个对象转换成字节序列,方便存储和传输。
@ -69,100 +93,100 @@ Java 使用双字节编码 UTF-16be这不是指 Java 只支持这一种
反序列化ObjectInputStream.readObject()
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现。
transient 关键字可以使一些属性不会被序列化。
transient 关键字可以使一些属性不会被序列化。
**ArrayList 序列化和反序列化的实现**ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
**ArrayList 序列化和反序列化的实现** ArrayList 中存储数据的数组是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java
private transient Object[] elementData;
private transient Object[] elementData;
```
# 六、网络操作
# 六、网络操作
Java 中的网络支持:
Java 中的网络支持:
1. InetAddress用于表示网络上的硬件资源 IP 地址;
2. URL统一资源定位符通过 URL 可以直接读取或者写入网络上的数据;
3. Sockets使用 TCP 协议实现网络通信;
4. Datagram使用 UDP 协议实现网络通信。
1. InetAddress用于表示网络上的硬件资源即 IP 地址;
2. URL统一资源定位符通过 URL 可以直接读取或者写入网络上的数据;
3. Sockets使用 TCP 协议实现网络通信;
4. Datagram使用 UDP 协议实现网络通信。
## InetAddress
## InetAddress
没有公有构造函数,只能通过静态方法来创建实例。
```java
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] addr);
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] addr);
```
## URL
## URL
可以直接从 URL 中读取字节流数据
可以直接从 URL 中读取字节流数据
```java
URL url = new URL("http://www.baidu.com");
InputStream is = url.openStream();                           // 字节流
InputStreamReader isr = new InputStreamReader(is, "utf-8");  // 字符流
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null) {
    System.out.println(line);
    line = br.readLine();
URL url = new URL("http://www.baidu.com");
InputStream is = url.openStream(); // 字节流
InputStreamReader isr = new InputStreamReader(is, "utf-8"); // 字符流
BufferedReader br = new BufferedReader(isr);
String line = br.readLine();
while (line != null) {
System.out.println(line);
line = br.readLine();
}
br.close();
isr.close();
is.close();
```
## Sockets
## Sockets
- ServerSocket服务器端类
- Socket客户端类
- 服务器和客户端通过 InputStream  OutputStream 进行输入输出。
- ServerSocket服务器端类
- Socket客户端类
- 服务器和客户端通过 InputStream 和 OutputStream 进行输入输出。
![](index_files/ClienteServidorSockets1521731145260.jpg)
<div align="center"> <img src="../pics//ClienteServidorSockets1521731145260.jpg"/> </div><br>
## Datagram
## Datagram
- DatagramPacket数据包类
- DatagramSocket通信类
- DatagramPacket数据包类
- DatagramSocket通信类
# 七、NIO
# 七、NIO
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足提供了高速的、面向块的 I/O。
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的。NIO 弥补了原来的 I/O 的不足,提供了高速的、面向块的 I/O。
## 流与块
## 流与块
I/O  NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据 NIO 以块的方式处理数据。
I/O 与 NIO 最重要的区别是数据打包和传输的方式I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
面向流的 I/O 一次处理一个字节数据,一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了所以现在它可以利用 NIO 的一些特性。例如java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如java.io.\* 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
## 通道与缓冲区
## 通道与缓冲区
### 1. 通道
### 1. 通道
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道 Channel 是对原 I/O 包中的流的模拟,可以通过它读取和写入数据。
通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道与流的不同之处在于,流只能在一个方向上移动,(一个流必须是 InputStream 或者 OutputStream 的子类),而通道是双向的,可以用于读、写或者同时用于读写。
通道包括以下类型:
- FileChannel从文件中读写数据
- DatagramChannel通过 UDP 读写网络中数据;
- SocketChannel通过 TCP 读写网络中数据;
- ServerSocketChannel可以监听新进来的 TCP 连接对每一个新进来的连接都会创建一个 SocketChannel。
- FileChannel从文件中读写数据
- DatagramChannel通过 UDP 读写网络中数据;
- SocketChannel通过 TCP 读写网络中数据;
- ServerSocketChannel可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。
### 2. 缓冲区
### 2. 缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
@ -170,283 +194,275 @@ I/O 包和 NIO 已经很好地集成了java.io.\* 已经以 NIO 为基
缓冲区包括以下类型:
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- ByteBuffer
- CharBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
## 缓冲区状态变量
## 缓冲区状态变量
- capacity最大容量
- position当前已经读写的字节数
- limit还可以读写的字节数。
- capacity最大容量
- position当前已经读写的字节数
- limit还可以读写的字节数。
状态变量的改变过程举例:
 新建一个大小为 8 个字节的缓冲区此时 position  0 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
新建一个大小为 8 个字节的缓冲区,此时 position 为 0而 limit = capacity = 8。capacity 变量不会改变,下面的讨论会忽略它。
![](index_files/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png)
<div align="center"> <img src="../pics//1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png"/> </div><br>
 从输入通道中读取 5 个字节数据写入缓冲区中此时 position 移动设置为 5limit 保持不变。
从输入通道中读取 5 个字节数据写入缓冲区中,此时 position 移动设置为 5limit 保持不变。
![](index_files/80804f52-8815-4096-b506-48eef3eed5c6.png)
<div align="center"> <img src="../pics//80804f52-8815-4096-b506-48eef3eed5c6.png"/> </div><br>
 在将缓冲区的数据写到输出通道之前需要先调用 flip() 方法这个方法将 limit 设置为当前 position并将 position 设置为 0。
在将缓冲区的数据写到输出通道之前,需要先调用 flip() 方法,这个方法将 limit 设置为当前 position并将 position 设置为 0。
![](index_files/952e06bd-5a65-4cab-82e4-dd1536462f38.png)
<div align="center"> <img src="../pics//952e06bd-5a65-4cab-82e4-dd1536462f38.png"/> </div><br>
 从缓冲区中取 4 个字节到输出缓冲中此时 position 设为 4。
从缓冲区中取 4 个字节到输出缓冲中,此时 position 设为 4。
![](index_files/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png)
<div align="center"> <img src="../pics//b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png"/> </div><br>
 最后需要调用 clear() 方法来清空缓冲区此时 position  limit 都被设置为最初位置。
最后需要调用 clear() 方法来清空缓冲区,此时 position 和 limit 都被设置为最初位置。
![](index_files/67bf5487-c45d-49b6-b9c0-a058d8c68902.png)
<div align="center"> <img src="../pics//67bf5487-c45d-49b6-b9c0-a058d8c68902.png"/> </div><br>
## 文件 NIO 实例
## 文件 NIO 实例
以下展示了使用 NIO 快速复制文件的实例:
以下展示了使用 NIO 快速复制文件的实例:
```java
public class FastCopyFile {
    public static void main(String args[]) throws Exception {
public class FastCopyFile {
public static void main(String args[]) throws Exception {
        String inFile = "data/abc.txt";
        String outFile = "data/abc-copy.txt";
String inFile = "data/abc.txt";
String outFile = "data/abc-copy.txt";
        // 获得源文件的输入字节流
        FileInputStream fin = new FileInputStream(inFile);
        // 获取输入字节流的文件通道
        FileChannel fcin = fin.getChannel();
// 获得源文件的输入字节流
FileInputStream fin = new FileInputStream(inFile);
// 获取输入字节流的文件通道
FileChannel fcin = fin.getChannel();
        // 获取目标文件的输出字节流
        FileOutputStream fout = new FileOutputStream(outFile);
        // 获取输出字节流的通道
        FileChannel fcout = fout.getChannel();
// 获取目标文件的输出字节流
FileOutputStream fout = new FileOutputStream(outFile);
// 获取输出字节流的通道
FileChannel fcout = fout.getChannel();
        // 为缓冲区分配 1024 个字节
        ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
// 为缓冲区分配 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();
        }
    }
while (true) {
// 从输入通道中读取数据到缓冲区中
int r = fcin.read(buffer);
// read() 返回 -1 表示 EOF
if (r == -1)
break;
// 切换读写
buffer.flip();
// 把缓冲区的内容写入输出文件中
fcout.write(buffer);
// 清空缓冲区
buffer.clear();
}
}
}
```
## 选择器
## 选择器
一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去检查多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去检查多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件具有更好的性能。
![](index_files/4d930e22-f493-49ae-8dff-ea21cd6895dc.png)
<div align="center"> <img src="../pics//4d930e22-f493-49ae-8dff-ea21cd6895dc.png"/> </div><br>
### 1. 创建选择器
### 1. 创建选择器
```java
Selector selector = Selector.open();
Selector selector = Selector.open();
```
### 2. 将通道注册到选择器上
### 2. 将通道注册到选择器上
```java
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
```
通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道在某个事件上被阻塞,那么服务器就不能响应其它时间,必须等待这个事件处理完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
它们在 SelectionKey 的定义如下:
它们在 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;
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;
int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
```
### 3. 监听事件
### 3. 监听事件
```java
int num = selector.select();
int num = selector.select();
```
使用 select() 来监听事件到达,它会一直阻塞直到有至少一个事件到达。
使用 select() 来监听事件到达,它会一直阻塞直到有至少一个事件到达。
### 4. 获取到达的事件
### 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();
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. 事件循环
### 5. 事件循环
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内。
因为一次 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();
    }
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 实例
## 套接字 NIO 实例
```java
public class NIOServer {
public class NIOServer {
    public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
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);
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);
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();
            }
        }
    }
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);
        StringBuffer data = new StringBuffer();
        while (true) {
            buffer.clear();
            int n = sChannel.read(buffer);
            if (n == -1)
                break;
            buffer.flip();
            int limit = buffer.limit();
            char[] dst = new char[limit];
            for (int i = 0; i < limit; i++)
                dst[i] = (char) buffer.get(i);
            data.append(dst);
            buffer.clear();
        }
        return data.toString();
    }
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuffer data = new StringBuffer();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1)
break;
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++)
dst[i] = (char) buffer.get(i);
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
```
```java
public class NIOClient {
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();
    }
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 快得多。
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。
只有文件中实际读取或者写入的部分才会映射到内存中。
现代操作系统一般会根据需要将文件的部分映射为内存的部分从而实现文件系统。Java 内存映射机制只不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
现代操作系统一般会根据需要将文件的部分映射为内存的部分从而实现文件系统。Java 内存映射机制只不过是在底层操作系统中可以采用这种机制时,提供了对该机制的访问。
向内存映射文件写入可能是危险的,仅只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
下面代码行将文件的前 1024 个字节映射到内存中map() 方法返回一个 MappedByteBuffer它是 ByteBuffer 的子类。因此,您可以像使用其他任何 ByteBuffer 一样使用新映射的缓冲区,操作系统会在需要时负责执行映射。
```java
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024);
```
## 对比
## 对比
NIO 与普通 I/O 的区别主要有以下两点:
NIO 与普通 I/O 的区别主要有以下两点:
- NIO 是非阻塞的。应当注意FileChannel 不能切换到非阻塞模式套接字 Channel 可以。
- NIO 面向块I/O 面向流。
- NIO 是非阻塞的。应当注意FileChannel 不能切换到非阻塞模式,套接字 Channel 可以。
- NIO 面向块I/O 面向流。
# 八、参考资料
# 八、参考资料
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [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)
---bottom---CyC---
![](index_files/DP-Decorator-java.io.png)
![](index_files/ClienteServidorSockets1521731145260.jpg)
![](index_files/1bea398f-17a7-4f67-a90b-9e2d243eaa9a.png)
![](index_files/4628274c-25b6-4053-97cf-d1239b44c43d.png)
![](index_files/952e06bd-5a65-4cab-82e4-dd1536462f38.png)
![](index_files/b5bdcbe2-b958-4aef-9151-6ad963cb28b4.png)
![](index_files/67bf5487-c45d-49b6-b9c0-a058d8c68902.png)
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [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)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,58 @@
<!-- GFM-TOC -->
* [算法思想](#算法思想)
* [二分查找](#二分查找)
* [贪心思想](#贪心思想)
* [双指针](#双指针)
* [排序](#排序)
* [快速选择](#快速选择)
* [堆排序](#堆排序)
* [桶排序](#桶排序)
* [荷兰国旗问题](#荷兰国旗问题)
* [搜索](#搜索)
* [BFS](#bfs)
* [DFS](#dfs)
* [Backtracking](#backtracking)
* [分治](#分治)
* [动态规划](#动态规划)
* [斐波那契数列](#斐波那契数列)
* [最长递增子序列](#最长递增子序列)
* [最长公共子序列](#最长公共子序列)
* [0-1 背包](#0-1-背包)
* [数组区间](#数组区间)
* [字符串编辑](#字符串编辑)
* [分割整数](#分割整数)
* [矩阵路径](#矩阵路径)
* [其它问题](#其它问题)
* [数学](#数学)
* [素数](#素数)
* [最大公约数](#最大公约数)
* [进制转换](#进制转换)
* [阶乘](#阶乘)
* [字符串加法减法](#字符串加法减法)
* [相遇问题](#相遇问题)
* [多数投票问题](#多数投票问题)
* [其它](#其它)
* [数据结构相关](#数据结构相关)
* [栈和队列](#栈和队列)
* [哈希表](#哈希表)
* [字符串](#字符串)
* [数组与矩阵](#数组与矩阵)
* [链表](#链表)
* [](#树)
* [递归](#递归)
* [层次遍历](#层次遍历)
* [前中后序遍历](#前中后序遍历)
* [BST](#bst)
* [Trie](#trie)
* [](#图)
* [二分图](#二分图)
* [拓扑排序](#拓扑排序)
* [并查集](#并查集)
* [位运算](#位运算)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 算法思想
## 二分查找
@ -93,7 +148,7 @@ Output: 2
Explanation: The square root of 8 is 2.82842..., and since we want to return an integer, the decimal part will be truncated.
```
一个数 x 的开方 sqrt 一定在 0 ~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 ~ x 之间查找 sqrt。
一个数 x 的开方 sqrt 一定在 0 \~ x 之间,并且满足 sqrt == x / sqrt。可以利用二分查找在 0 \~ x 之间查找 sqrt。
对于 x = 8它的开方是 2.82842...,最后应该返回 2 而不是 3。在循环条件为 l <= h 并且循环退出时h 总是比 l 小 1也就是说 h = 2l = 3因此最后的返回值应该为 h 而不是 l。
@ -363,7 +418,7 @@ Explanation: You could modify the first 4 to 1 to get a non-decreasing array.
题目描述:判断一个数组能不能只修改一个数就成为非递减数组。
在出现 nums[i] < nums[i - 1] 需要考虑的是应该修改数组的哪个数使得本次修改能使 i 之前的数组成为非递减数组并且 **不影响后续的操作**优先考虑令 nums[i - 1] = nums[i]因为如果修改 nums[i] = nums[i - 1] 的话那么 nums[i] 这个数会变大就有可能比 nums[i + 1] 从而影响了后续操作还有一个比较特别的情况就是 nums[i] < nums[i - 2]只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组只能修改 nums[i] = nums[i - 1]。
在出现 nums[i] < nums[i - 1] 需要考虑的是应该修改数组的哪个数使得本次修改能使 i 之前的数组成为非递减数组并且 **不影响后续的操作** 优先考虑令 nums[i - 1] = nums[i]因为如果修改 nums[i] = nums[i - 1] 的话那么 nums[i] 这个数会变大就有可能比 nums[i + 1] 从而影响了后续操作还有一个比较特别的情况就是 nums[i] < nums[i - 2]只修改 nums[i - 1] = nums[i] 不能使数组成为非递减数组只能修改 nums[i] = nums[i - 1]。
```java
public boolean checkPossibility(int[] nums) {
@ -785,7 +840,7 @@ private boolean isValid(String s, String target) {
[215. Kth Largest Element in an Array (Medium)](https://leetcode.com/problems/kth-largest-element-in-an-array/description/)
**排序**:时间复杂度 O(NlogN),空间复杂度 O(1)
**排序** :时间复杂度 O(NlogN),空间复杂度 O(1)
```java
public int findKthLargest(int[] nums, int k) {
@ -794,7 +849,7 @@ public int findKthLargest(int[] nums, int k) {
}
```
**堆排序**:时间复杂度 O(NlogK),空间复杂度 O(K)。
**堆排序** :时间复杂度 O(NlogK),空间复杂度 O(K)。
```java
public int findKthLargest(int[] nums, int k) {
@ -808,7 +863,7 @@ public int findKthLargest(int[] nums, int k) {
}
```
**快速选择**:时间复杂度 O(N),空间复杂度 O(1)
**快速选择** :时间复杂度 O(N),空间复杂度 O(1)
```java
public int findKthLargest(int[] nums, int k) {
@ -928,7 +983,7 @@ public String frequencySort(String s) {
它其实是三向切分快速排序的一种变种,在三向切分快速排序中,每次切分都将数组分成三个区间:小于切分元素、等于切分元素、大于切分元素,而该算法是将数组分成三个区间:等于红色、等于白色、等于蓝色。
![](index_files/3b49dd67-2c40-4b81-8ad2-7bbb1fe2fcbd.png)
<div align="center"> <img src="../pics//3b49dd67-2c40-4b81-8ad2-7bbb1fe2fcbd.png"/> </div><br>
**按颜色进行排序**
@ -967,7 +1022,7 @@ private void swap(int[] nums, int i, int j) {
### BFS
![](index_files/4ff355cf-9a7f-4468-af43-e5b02038facc.jpg)
<div align="center"> <img src="../pics//4ff355cf-9a7f-4468-af43-e5b02038facc.jpg"/> </div><br>
广度优先搜索的搜索过程有点像一层一层地进行遍历,每层遍历都以上一层遍历的结果作为起点,遍历一个距离能访问到的所有节点。需要注意的是,遍历过的节点不能再次被遍历。
@ -1180,7 +1235,7 @@ private int getShortestPath(List<Integer>[] graphic, int start, int end) {
### DFS
![](index_files/f7f7e3e5-7dd4-4173-9999-576b9e2ac0a2.png)
<div align="center"> <img src="../pics//f7f7e3e5-7dd4-4173-9999-576b9e2ac0a2.png"/> </div><br>
广度优先搜索一层一层遍历,每一层得到的所有新节点,要用队列存储起来以备下一层遍历的时候再遍历。
@ -1461,7 +1516,7 @@ private void dfs(int r, int c, boolean[][] canReach) {
Backtracking回溯属于 DFS。
- 普通 DFS 主要用在 **可达性问题**,这种问题只需要执行到特点的位置然后返回即可。
- 普通 DFS 主要用在 **可达性问题** ,这种问题只需要执行到特点的位置然后返回即可。
- 而 Backtracking 主要用于求解 **排列组合** 问题,例如有 { 'a','b','c' } 三个字符,求解所有由这三个字符排列得到的字符串,这种问题在执行到特定的位置返回之后还会继续执行求解过程。
因为 Backtracking 不是立即就返回,而要继续求解,因此在程序实现时,需要注意对元素的标记问题:
@ -1473,7 +1528,7 @@ Backtracking回溯属于 DFS。
[17. Letter Combinations of a Phone Number (Medium)](https://leetcode.com/problems/letter-combinations-of-a-phone-number/description/)
![](index_files/a3f34241-bb80-4879-8ec9-dff2d81b514e.jpg)
<div align="center"> <img src="../pics//a3f34241-bb80-4879-8ec9-dff2d81b514e.jpg"/> </div><br>
```html
Input:Digit string "23"
@ -2020,7 +2075,7 @@ private boolean isPalindrome(String s, int begin, int end) {
[37. Sudoku Solver (Hard)](https://leetcode.com/problems/sudoku-solver/description/)
![](index_files/1ca52246-c443-48ae-b1f8-1cafc09ec75c.png)
<div align="center"> <img src="../pics//1ca52246-c443-48ae-b1f8-1cafc09ec75c.png"/> </div><br>
```java
private boolean[][] rowsUsed = new boolean[9][10];
@ -2078,7 +2133,7 @@ private int cubeNum(int i, int j) {
[51. N-Queens (Hard)](https://leetcode.com/problems/n-queens/description/)
![](index_files/1f080e53-4758-406c-bb5f-dbedf89b63ce.jpg)
<div align="center"> <img src="../pics//1f080e53-4758-406c-bb5f-dbedf89b63ce.jpg"/> </div><br>
题目描述:在 n\*n 的矩阵中摆放 n 个皇后,并且每个皇后不能在同一行,同一列,同一对角线上,求所有的 n 皇后的解。
@ -2086,11 +2141,11 @@ private int cubeNum(int i, int j) {
45 度对角线标记数组的维度为 2 \* n - 1通过下图可以明确 (r, c) 的位置所在的数组下标为 r + c。
![](index_files/85583359-1b45-45f2-9811-4f7bb9a64db7.jpg)
<div align="center"> <img src="../pics//85583359-1b45-45f2-9811-4f7bb9a64db7.jpg"/> </div><br>
135 度对角线标记数组的维度也是 2 \* n - 1(r, c) 的位置所在的数组下标为 n - 1 - (r - c)。
![](index_files/9e80f75a-b12b-4344-80c8-1f9ccc2d5246.jpg)
<div align="center"> <img src="../pics//9e80f75a-b12b-4344-80c8-1f9ccc2d5246.jpg"/> </div><br>
```java
private List<List<String>> ret;
@ -2198,7 +2253,7 @@ public List<Integer> diffWaysToCompute(String input) {
定义一个数组 dp 存储上楼梯的方法数(为了方便讨论,数组下标从 1 开始dp[i] 表示走到第 i 个楼梯的方法数目。第 i 个楼梯可以从第 i-1 和 i-2 个楼梯再走一步到达,走到第 i 个楼梯的方法数为走到第 i-1 和第 i-2 个楼梯的方法数之和。
$$dp[i] = dp[i-1] + dp[i-2]$$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[i]=dp[i-1]+dp[i-2]"/></div> <br>
dp[N] 即为所求。
@ -2225,7 +2280,7 @@ public int climbStairs(int n) {
第 i 年成熟的牛的数量为:
$$dp[i] = dp[i-1] + dp[i-3]$$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[i]=dp[i-1]+dp[i-3]"/></div> <br>
**强盗抢劫**
@ -2235,7 +2290,7 @@ $$dp[i] = dp[i-1] + dp[i-3]$$
定义 dp 数组用来存储最大的抢劫量,其中 dp[i] 表示抢到第 i 个住户时的最大抢劫量。由于不能抢劫邻近住户,因此如果抢劫了第 i 个住户那么只能抢劫 i - 2 和 i - 3 的住户,所以
$$dp[i] = max(dp[i - 2], dp[i - 3]) + nums[i]$$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[i]=max(dp[i-2],dp[i-3])+nums[i]"/></div> <br>
O(n) 空间复杂度实现方法:
@ -2314,7 +2369,7 @@ private int rob(int[] nums, int first, int last) {
综上所述,错误装信数量方式数量为:
$$ dp[i] = (i-1) * dp[i-2] + (i-1) * dp[i-1]$$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[i]=(i-1)*dp[i-2]+(i-1)*dp[i-1]"/></div> <br>
dp[N] 即为所求。
@ -2322,15 +2377,15 @@ dp[N] 即为所求。
### 最长递增子序列
已知一个序列 {S<sub>1</sub>, S<sub>2</sub>,...,S<sub>n</sub>} ,取出若干数组成新的序列 {S<sub>i1</sub>, S<sub>i2</sub>,..., S<sub>im</sub>},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个**子序列**。
已知一个序列 {S<sub>1</sub>, S<sub>2</sub>,...,S<sub>n</sub>} ,取出若干数组成新的序列 {S<sub>i1</sub>, S<sub>i2</sub>,..., S<sub>im</sub>},其中 i1、i2 ... im 保持递增,即新序列中各个数仍然保持原数列中的先后顺序,称新序列为原序列的一个 **子序列**
如果在子序列中,当下标 ix > iy 时S<sub>ix</sub> > S<sub>iy</sub>,称子序列为原序列的一个**递增子序列**。
如果在子序列中,当下标 ix > iy 时S<sub>ix</sub> > S<sub>iy</sub>,称子序列为原序列的一个 **递增子序列**
定义一个数组 dp 存储最长递增子序列的长度dp[n] 表示以 S<sub>n</sub> 结尾的序列的最长递增子序列长度。对于一个递增子序列 {S<sub>i1</sub>, S<sub>i2</sub>,...,S<sub>im</sub>},如果 im < n 并且 S<sub>im</sub> < S<sub>n</sub> ,此时 {S<sub>i1</sub>, S<sub>i2</sub>,..., S<sub>im</sub>, S<sub>n</sub>} 为一个递增子序列,递增子序列的长度增加 1。满足上述条件的递增子序列中长度最长的那个递增子序列就是要找的在长度最长的递增子序列上加上 S<sub>n</sub> 就构成了以 S<sub>n</sub> 为结尾的最长递增子序列。因此 dp[n] = max{ dp[i]+1 | S<sub>i</sub> < S<sub>n</sub> && i < n}
因为在求 dp[n] 时可能无法找到一个满足条件的递增子序列,此时 {S<sub>n</sub>} 就构成了递增子序列,需要对前面的求解方程做修改,令 dp[n] 最小为 1
$$ dp[n] = max\{ 1, dp[i]+1 | S_i < S_n \&\& i < n \} $$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[n]=max\{1,dp[i]+1|S_i<S_n\&\&i<n\}"/></div> <br>
对于一个长度为 N 的序列,最长递增子序列并不一定会以 S<sub>N</sub> 为结尾,因此 dp[N] 不是序列的最长递增子序列的长度,需要遍历 dp 数组找出最大值才是所要的结果,即 max{ dp[i] | 1 <= i <= N} 即为所求。
@ -2481,7 +2536,7 @@ public int wiggleMaxLength(int[] nums) {
综上,最长公共子序列的状态转移方程为:
$$ dp[i][j]=\left\{ \begin{array}{rcl} dp[i-1][j-1] && { S1_i==S2_j }\\ max(dp[i-1][j], dp[i][j-1]) &&{ S1_i <> S2_j } \end{array} \right. $$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[i][j]=\left\{\begin{array}{rcl}dp[i-1][j-1]&&{S1_i==S2_j}\\max(dp[i-1][j],dp[i][j-1])&&{S1_i<>S2_j}\end{array}\right."/></div> <br>
对于长度为 N 的序列 S<sub>1</sub> 和 长度为 M 的序列 S<sub>2</sub>dp[N][M] 就是序列 S<sub>1</sub> 和序列 S<sub>2</sub> 的最长公共子序列长度。
@ -2518,7 +2573,7 @@ public int lengthOfLCS(int[] nums1, int[] nums2) {
综上0-1 背包的状态转移方程为:
$$ dp[i][j] = max(dp[i-1][j], dp[i-1][j-w] + v) $$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[i][j]=max(dp[i-1][j],dp[i-1][j-w]+v)"/></div> <br>
```java
public int knapsack(int W, int N, int[] weights, int[] values) {
@ -2541,7 +2596,7 @@ public int knapsack(int W, int N, int[] weights, int[] values) {
在程序实现时可以对 0-1 背包做优化。观察状态转移方程可以知道,前 i 件物品的状态仅由前 i-1 件物品的状态有关,因此可以将 dp 定义为一维数组,其中 dp[j] 既可以表示 dp[i-1][j] 也可以表示 dp[i][j]。此时,
$$ dp[j] = max ( dp[j], dp[j-w] + v ) $$
<div align="center"><img src="https://latex.codecogs.com/gif.latex?dp[j]=max(dp[j],dp[j-w]+v)"/></div> <br>
因为 dp[j-w] 表示 dp[i-1][j-w],因此不能先求 dp[i][j-w],以防止将 dp[i-1][j-w] 覆盖。也就是说要先计算 dp[i][j] 再计算 dp[i][j-w],在程序实现时需要按倒序来循环求解。
@ -2878,7 +2933,7 @@ sumRange(2, 5) -> -1
sumRange(0, 5) -> -3
```
求区间 i ~ j 的和,可以转换为 sum[j] - sum[i-1],其中 sum[i] 为 0 ~ i 的和。
求区间 i \~ j 的和,可以转换为 sum[j] - sum[i-1],其中 sum[i] 为 0 \~ i 的和。
```java
class NumArray {
@ -3115,7 +3170,7 @@ public int numDecodings(String s) {
题目描述:统计从矩阵左上角到右下角的路径总数,每次只能向右或者向下移动。
![](index_files/7c98e1b6-c446-4cde-8513-5c11b9f52aea.jpg)
<div align="center"> <img src="../pics//7c98e1b6-c446-4cde-8513-5c11b9f52aea.jpg"/> </div><br>
```java
public int uniquePaths(int m, int n) {
@ -3181,7 +3236,7 @@ public int minPathSum(int[][] grid) {
题目描述:交易之后需要有一天的冷却时间。
![](index_files/a3da4342-078b-43e2-b748-7e71bec50dc4.png)
<div align="center"> <img src="../pics//a3da4342-078b-43e2-b748-7e71bec50dc4.png"/> </div><br>
```java
public int maxProfit(int[] prices) {
@ -3220,7 +3275,7 @@ The total profit is ((8 - 1) - 2) + ((9 - 4) - 2) = 8.
题目描述:每交易一次,都要支付一定的费用。
![](index_files/61942711-45a0-4e11-bbc9-434e31436f33.png)
<div align="center"> <img src="../pics//61942711-45a0-4e11-bbc9-434e31436f33.png"/> </div><br>
```java
public int maxProfit(int[] prices, int fee) {
@ -4560,7 +4615,7 @@ Output: [1, 3, 2]
Explanation: The [1, 3, 2] has three different positive integers ranging from 1 to 3, and the [2, 1] has exactly 2 distinct integers: 1 and 2.
```
题目描述:数组元素为 1~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。
题目描述:数组元素为 1\~n 的整数,要求构建数组,使得相邻元素的差值不相同的个数为 k。
让前 k+1 个元素构建出 k 个不相同的差值序列为1 k+1 2 k 3 k-1 ... k/2 k/2+1.
@ -5335,7 +5390,7 @@ Output:
二叉查找树BST根节点大于等于左子树所有节点小于等于右子树所有节点。
只保留值在 L ~ R 之间的节点
只保留值在 L \~ R 之间的节点
```java
public TreeNode trimBST(TreeNode root, int L, int R) {
@ -5972,7 +6027,7 @@ private int size(ListNode node) {
### Trie
![](index_files/5c638d59-d4ae-4ba4-ad44-80bdc30f38dd.jpg)
<div align="center"> <img src="../pics//5c638d59-d4ae-4ba4-ad44-80bdc30f38dd.jpg"/> </div><br>
Trie又称前缀树或字典树用于判断字符串是否存在或者是否具有某种字符串前缀。
@ -6350,15 +6405,15 @@ x ^ 1s = ~x x & 1s = x x | 1s = 1s
x ^ x = 0 x & x = x x | x = x
```
- 利用 x ^ 1s = ~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。
- 利用 x ^ 1s = \~x 的特点,可以将位级表示翻转;利用 x ^ x = 0 的特点,可以将三个数中重复的两个数去除,只留下另一个数。
- 利用 x & 0s = 0 和 x & 1s = x 的特点,可以实现掩码操作。一个数 num 与 mask 00111100 进行位与操作,只保留 num 中与 mask 的 1 部分相对应的位。
- 利用 x | 0s = x 和 x | 1s = 1s 的特点,可以实现设值操作。一个数 num 与 mask00111100 进行位或操作,将 num 中与 mask 的 1 部分相对应的位都设置为 1。
位与运算技巧:
- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110**100**,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。
- n-n&(~n+1) 去除 n 的位级表示中最高的那一位。
- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1对于二进制表示 10110**100**-n 得到 01001**100**,相与得到 00000**100**
- n&(n-1) 去除 n 的位级表示中最低的那一位。例如对于二进制表示 10110 **100** ,减去 1 得到 10110**011**,这两个数相与得到 10110**000**。
- n-n&(\~n+1) 去除 n 的位级表示中最高的那一位。
- n&(-n) 得到 n 的位级表示中最低的那一位。-n 得到 n 的反码加 1对于二进制表示 10110 **100** -n 得到 01001**100**,相与得到 00000**100**
移位运算:
@ -6368,13 +6423,13 @@ x ^ x = 0 x & x = x x | x = x
**2. mask 计算**
要获取 111111111将 0 取反即可,~0。
要获取 111111111将 0 取反即可,\~0。
要得到只有第 i 位为 1 的 mask将 1 向左移动 i-1 位即可1&lt;&lt;(i-1) 。例如 1&lt;&lt;4 得到只有第 5 位为 1 的 mask 00010000。
要得到 1 到 i 位为 1 的 mask1&lt;&lt;(i+1)-1 即可,例如将 1&lt;&lt;(4+1)-1 = 00010000-1 = 00001111。
要得到 1 到 i 位为 0 的 mask只需将 1 到 i 位为 1 的 mask 取反,即 ~(1&lt;&lt;(i+1)-1)。
要得到 1 到 i 位为 0 的 mask只需将 1 到 i 位为 1 的 mask 取反,即 \~(1&lt;&lt;(i+1)-1)。
**3. Java 中的位操作**
@ -6729,7 +6784,7 @@ public int maxProduct(String[] words) {
}
```
**统计从 0 ~ n 每个数的二进制表示中 1 的个数**
**统计从 0 \~ n 每个数的二进制表示中 1 的个数**
[338. Counting Bits (Medium)](https://leetcode.com/problems/counting-bits/description/)

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,255 +1,302 @@
# 一、概述
<!-- GFM-TOC -->
* [一、概述](#一概述)
* [二、数据类型](#二数据类型)
* [STRING](#string)
* [LIST](#list)
* [SET](#set)
* [HASH](#hash)
* [ZSET](#zset)
* [三、使用场景](#三使用场景)
* [缓存](#缓存)
* [计数器](#计数器)
* [应用限流](#应用限流)
* [消息队列](#消息队列)
* [查找表](#查找表)
* [交集运算](#交集运算)
* [排行榜](#排行榜)
* [分布式 Session](#分布式-session)
* [分布式锁](#分布式锁)
* [四、Redis 与 Memcached](#四redis-与-memcached)
* [数据类型](#数据类型)
* [数据持久化](#数据持久化)
* [分布式](#分布式)
* [内存管理机制](#内存管理机制)
* [五、键的过期时间](#五键的过期时间)
* [六、数据淘汰策略](#六数据淘汰策略)
* [七、持久化](#七持久化)
* [快照持久化](#快照持久化)
* [AOF 持久化](#aof-持久化)
* [八、发布与订阅](#八发布与订阅)
* [九、事务](#九事务)
* [十、事件](#十事件)
* [文件事件](#文件事件)
* [时间事件](#时间事件)
* [事件的调度与执行](#事件的调度与执行)
* [十一、复制](#十一复制)
* [连接过程](#连接过程)
* [主从链](#主从链)
* [十二、Sentinel](#十二sentinel)
* [十三、分片](#十三分片)
* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
* [文章信息](#文章信息)
* [点赞功能](#点赞功能)
* [对文章进行排序](#对文章进行排序)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
# 一、概述
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。
Redis 支持很多特性例如将内存中的数据持久化到硬盘中使用复制来扩展读性能使用分片来扩展写性能。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
# 二、数据类型
# 二、数据类型
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| LIST | 列表 | 从两端压入或者弹出元素</br> 读取单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| ZSET | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
| 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/)
> [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
## STRING
<img src="index_files/6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/>
<div align="center"> <img src="../pics//6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/> </div><br>
```html
> set hello world
> set hello world
OK
> get hello
> get hello
"world"
> del hello
(integer) 1
> get hello
> del hello
(integer) 1
> get hello
(nil)
```
## LIST
## LIST
<img src="index_files/fb327611-7e2b-4f2f-9f5b-38592d408f07.png" width="400"/>
<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
> 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"
> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
> lindex list-key 1
> lindex list-key 1
"item2"
> lpop list-key
> lpop list-key
"item"
> lrange list-key 0 -1
1) "item2"
2) "item"
> lrange list-key 0 -1
1) "item2"
2) "item"
```
## SET
## SET
<img src="index_files/cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png" width="400"/>
<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
> 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"
> smembers set-key
1) "item"
2) "item2"
3) "item3"
> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1
> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1
> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0
> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0
> smembers set-key
1) "item"
2) "item3"
> smembers set-key
1) "item"
2) "item3"
```
## HASH
## HASH
<img src="index_files/7bd202a7-93d4-4f3a-a878-af68ae25539a.png" width="400"/>
<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
> 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"
> 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
> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0
> hget hash-key sub-key1
> hget hash-key sub-key1
"value1"
> hgetall hash-key
1) "sub-key1"
2) "value1"
> hgetall hash-key
1) "sub-key1"
2) "value1"
```
## ZSET
## ZSET
<img src="index_files/1202b2d6-9469-4251-bd47-ca6034fb6116.png" width="400"/>
<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
> 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"
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
```
# 三、使用场景
# 三、使用场景
## 缓存
## 缓存
将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率。
## 计数器
## 计数器
Redis 这种内存数据库能支持计数器频繁的读写操作。
Redis 这种内存数据库能支持计数器频繁的读写操作。
## 应用限流
## 应用限流
限制一个网站访问流量。
## 消息队列
## 消息队列
使用 List 数据类型,它是双向链表。
使用 List 数据类型,它是双向链表。
## 查找表
## 查找表
使用 HASH 数据类型。
使用 HASH 数据类型。
## 交集运算
## 交集运算
使用 SET 类型,例如求两个用户的共同好友。
使用 SET 类型,例如求两个用户的共同好友。
## 排行榜
## 排行榜
使用 ZSET 数据类型。
使用 ZSET 数据类型。
## 分布式 Session
## 分布式 Session
多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。
多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。
## 分布式锁
## 分布式锁
除了可以使用 SETNX 实现分布式锁之外还可以使用官方提供的 RedLock 分布式锁实现。
除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。
# 四、Redis  Memcached
# 四、Redis 与 Memcached
两者都是非关系型内存键值数据库。有以下主要不同:
## 数据类型
## 数据类型
Memcached 仅支持字符串类型 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
Memcached 仅支持字符串类型,而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。
## 数据持久化
## 数据持久化
Redis 支持两种持久化策略RDB 快照和 AOF 日志 Memcached 不支持持久化。
Redis 支持两种持久化策略RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
## 分布式
## 分布式
Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
Redis Cluster 实现了分布式的支持。
Redis Cluster 实现了分布式的支持。
## 内存管理机制
## 内存管理机制
 Redis 并不是所有数据都一直存储在内存中可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据那么剩下的 28 bytes 就浪费掉了。
Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
# 五、键的过期时间
# 五、键的过期时间
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
# 六、数据淘汰策略
# 六、数据淘汰策略
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
可以设置内存最大使用量,当内存使用量超过时施行淘汰策略,具体有 6 种淘汰策略。
| 策略 | 描述 |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |
| 策略 | 描述 |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
如果使用 Redis 来缓存数据时,要保证所有数据都是热点数据,可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法LRU、TTL实际实现上并非针对所有 key而是抽样一小部分 key 从中选出被淘汰 key抽样数量可通过 maxmemory-samples 配置。
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法LRU、TTL实际实现上并非针对所有 key而是抽样一小部分 key 从中选出被淘汰 key抽样数量可通过 maxmemory-samples 配置。
# 七、持久化
# 七、持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
## 快照持久化
## 快照持久化
将某个时间点的所有数据都存放到硬盘上。
@ -259,206 +306,194 @@ Redis 是内存型数据库为了保证数据在断电后不会丢失
如果数据量很大,保存快照的时间会很长。
## AOF 持久化
## AOF 持久化
将写命令添加到 AOF 文件Append Only File的末尾。
将写命令添加到 AOF 文件Append Only File的末尾。
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。
对硬盘的文件进行写入时,写入的内容首先会被存储到缓冲区,然后由操作系统决定什么时候将该内容同步到硬盘,用户可以调用 file.flush() 方法请求操作系统尽快将缓冲区存储的数据同步到硬盘。可以看出写入文件的数据不会立即同步到硬盘上,在将写命令添加到 AOF 文件时,要根据需求来保证何时同步到硬盘上。
有以下同步选项:
| 选项 | 同步频率 |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
| 选项 | 同步频率 |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适可以保证系统奔溃时只会丢失一秒左右的数据并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适,可以保证系统奔溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统奔溃时数据丢失的数量。
随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性能够去除 AOF 文件中的冗余写命令。
随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
# 八、发布与订阅
# 八、发布与订阅
订阅者订阅了频道之后,发布者向频道发送字符串消息会被所有订阅者接收到。
某个客户端使用 SUBSCRIBE 订阅一个频道其它客户端可以使用 PUBLISH 向这个频道发送消息。
某个客户端使用 SUBSCRIBE 订阅一个频道,其它客户端可以使用 PUBLISH 向这个频道发送消息。
发布与订阅模式和观察者模式有以下不同:
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的;
- 观察者模式中,观察者和主题都知道对方的存在;而在发布与订阅模式中,发布者与订阅者不知道对方的存在,它们之间通过频道进行通信。
- 观察者模式是同步的,当事件触发时,主题会去调用观察者的方法;而发布与订阅模式是异步的;
<img src="index_files/bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg" width="400"/>
<div align="center"> <img src="../pics//bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg" width="400"/> </div><br>
# 九、事务
# 九、事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
Redis 最简单的事务实现方式是使用 MULTI  EXEC 命令将事务操作包围起来。
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
# 十、事件
# 十、事件
Redis 服务器是一个事件驱动程序。
Redis 服务器是一个事件驱动程序。
## 文件事件
## 文件事件
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
Redis 基于 Reactor 模式开发了自己的网络时间处理器使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用响应的时间处理器。
Redis 基于 Reactor 模式开发了自己的网络时间处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的时间传送给文件事件分派器,分派器会根据套接字产生的事件类型调用响应的时间处理器。
![](index_files/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png)
<div align="center"> <img src="../pics//9ea86eb5-000a-4281-b948-7b567bd6f1d8.png"/> </div><br>
## 时间事件
## 时间事件
服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
时间事件又分为:
- 定时事件:是让一段程序在指定的时间之内执行一次;
- 周期性事件:是让一段程序每隔指定时间就执行一次。
- 定时事件:是让一段程序在指定的时间之内执行一次;
- 周期性事件:是让一段程序每隔指定时间就执行一次。
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用响应的事件处理器。
## 事件的调度与执行
## 事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能监听太久,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
```python
def aeProcessEvents():
def aeProcessEvents():
    # 获取到达时间离当前时间最接近的时间事件
    time_event = aeSearchNearestTimer()
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
    # 计算最接近的时间事件距离到达还有多少毫秒
    remaind_ms = time_event.when - unix_ts_now()
# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
    # 如果事件已到达那么 remaind_ms 的值可能为负数将它设为 0
    if remaind_ms < 0:
        remaind_ms = 0
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0
    # 根据 remaind_ms 的值创建 timeval
    timeval = create_timeval_with_ms(remaind_ms)
# 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms)
    # 阻塞并等待文件事件产生最大阻塞时间由传入的 timeval 决定
    aeApiPoll(timeval)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)
    # 处理所有已产生的文件事件
    procesFileEvents()
# 处理所有已产生的文件事件
procesFileEvents()
    # 处理所有已到达的时间事件
    processTimeEvents()
# 处理所有已到达的时间事件
processTimeEvents()
```
 aeProcessEvents 函数置于一个循环里面加上初始化和清理函数就构成了 Redis 服务器的主函数,伪代码如下:
aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
```python
def main():
def main():
    # 初始化服务器
    init_server()
# 初始化服务器
init_server()
    # 一直处理事件,直到服务器关闭为止
    while server_is_not_shutdown():
        aeProcessEvents()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
    # 服务器关闭,执行清理操作
    clean_server()
# 服务器关闭,执行清理操作
clean_server()
```
从事件处理的角度来看,服务器运行流程如下:
<img src="index_files/dda1608d-26e0-4f10-8327-a459969b150a.png" width=""/>
<div align="center"> <img src="../pics//dda1608d-26e0-4f10-8327-a459969b150a.png" width=""/> </div><br>
# 十一、复制
# 十一、复制
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
一个从服务器只能有一个主服务器,并且不支持主主复制。
## 连接过程
## 连接过程
1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
## 主从链
## 主从链
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
<img src="index_files/395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/>
<div align="center"> <img src="../pics//395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/> </div><br>
# 十二、Sentinel
# 十二、Sentinel
Sentinel哨兵可以监听主服务器并在主服务器进入下线状态时自动从从服务器中选举出新的主服务器。
# 十三、分片
# 十三、分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,也可以从多台机器里面获取数据,这种方法在解决某些问题时可以获得线性级别的性能提升。
假设有 4  Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id  0~1000 的存储到实例 R0 用户 id  1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
主要有三种分片方式:
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
- 服务器分片Redis Cluster。
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
- 服务器分片Redis Cluster。
# 十四、一个简单的论坛系统分析
# 十四、一个简单的论坛系统分析
该论坛系统功能如下:
- 可以发布文章;
- 可以对文章进行点赞;
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示;
- 可以发布文章;
- 可以对文章进行点赞;
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示;
## 文章信息
## 文章信息
文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
Redis 没有关系型数据库中的表这一概念来将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617其中 article 为命名空间ID  92617。
Redis 没有关系型数据库中的表这一概念来将同类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617其中 article 为命名空间ID 为 92617。
<img src="index_files/7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/>
<div align="center"> <img src="../pics//7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/> </div><br>
## 点赞功能
## 点赞功能
当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作还必须记录该用户已经对该文章进行了点赞防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。
当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。
为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。
<img src="index_files/485fdf34-ccf8-4185-97c6-17374ee719a0.png" width="400"/>
<div align="center"> <img src="../pics//485fdf34-ccf8-4185-97c6-17374ee719a0.png" width="400"/> </div><br>
## 对文章进行排序
## 对文章进行排序
为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
<img src="index_files/f7d170a3-e446-4a64-ac2d-cb95028f81a8.png" width="800"/>
<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/)
- [论述 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/)
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
---bottom---CyC---
![](index_files/6019b2db-bc3e-4408-b6d8-96025f4481d6.png)
![](index_files/fb327611-7e2b-4f2f-9f5b-38592d408f07.png)
![](index_files/cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png)
![](index_files/7bd202a7-93d4-4f3a-a878-af68ae25539a.png)
![](index_files/1202b2d6-9469-4251-bd47-ca6034fb6116.png)
![](index_files/bee1ff1d-c80f-4b3c-b58c-7073a8896ab2.jpg)
![](index_files/395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png)
![](index_files/dda1608d-26e0-4f10-8327-a459969b150a.png)
![](index_files/7c54de21-e2ff-402e-bc42-4037de1c1592.png)
![](index_files/485fdf34-ccf8-4185-97c6-17374ee719a0.png)
![](index_files/f7d170a3-e446-4a64-ac2d-cb95028f81a8.png)
- 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/)
- [论述 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/)
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)

File diff suppressed because it is too large Load Diff

View File

@ -1,332 +1,343 @@
# 一、I/O 复用
<!-- GFM-TOC -->
* [一、I/O 复用](#一io-复用)
* [I/O 模型](#io-模型)
* [select/poll/epoll](#selectpollepoll)
* [select 和 poll 比较](#select-和-poll-比较)
* [eopll 工作模式](#eopll-工作模式)
* [select poll epoll 应用场景](#select-poll-epoll-应用场景)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## I/O 模型
# 一、I/O 复用
## I/O 模型
一个输入操作通常包括两个阶段:
- 等待数据准备好
- 从内核向进程复制数据
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
Unix 下有五种 I/O 模型:
Unix 下有五种 I/O 模型:
- 阻塞式 I/O
- 非阻塞式 I/O
- I/O 复用select  poll
- 信号驱动式 I/OSIGIO
- 异步 I/OAIO
- 阻塞式 I/O
- 非阻塞式 I/O
- I/O 复用select 和 poll
- 信号驱动式 I/OSIGIO
- 异步 I/OAIO
### 1. 阻塞式 I/O
### 1. 阻塞式 I/O
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。
下图中recvfrom 用于接收 Socket 传来的数据并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
下图中recvfrom 用于接收 Socket 传来的数据,并复制到应用进程的缓冲区 buf 中。这里把 recvfrom() 当成系统调用。
```c
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags, struct sockaddr *src_addr, socklen_t *addrlen);
```
![](index_files/1492928416812_4.png)
<div align="center"> <img src="../pics//1492928416812_4.png"/> </div><br>
### 2. 非阻塞式 I/O
### 2. 非阻塞式 I/O
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成这种方式成为轮询polling
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成这种方式成为轮询polling
由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。
由于 CPU 要处理更多的系统调用,因此这种模型是比较低效的。
![](index_files/1492929000361_5.png)
<div align="center"> <img src="../pics//1492929000361_5.png"/> </div><br>
### 3. I/O 复用
### 3. I/O 复用
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读,这一过程会被阻塞,当某一个套接字可读时返回。之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。
如果一个 Web 服务器没有 I/O 复用那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接那么就需要创建相同数量的线程。并且相比于多进程和多线程技术I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
如果一个 Web 服务器没有 I/O 复用,那么每一个 Socket 连接都需要创建一个线程去处理。如果同时有几万个连接那么就需要创建相同数量的线程。并且相比于多进程和多线程技术I/O 复用不需要进程线程创建和切换的开销,系统开销更小。
![](index_files/1492929444818_6.png)
<div align="center"> <img src="../pics//1492929444818_6.png"/> </div><br>
### 4. 信号驱动 I/O
### 4. 信号驱动 I/O
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。
相比于非阻塞式 I/O 的轮询方式信号驱动 I/O  CPU 利用率更高。
相比于非阻塞式 I/O 的轮询方式,信号驱动 I/O 的 CPU 利用率更高。
![](index_files/1492929553651_7.png)
<div align="center"> <img src="../pics//1492929553651_7.png"/> </div><br>
### 5. 异步 I/O
### 5. 异步 I/O
进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
进行 aio_read 系统调用会立即返回,应用进程继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
异步 I/O 与信号驱动 I/O 的区别在于异步 I/O 的信号是通知应用进程 I/O 完成而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
![](index_files/1492930243286_8.png)
<div align="center"> <img src="../pics//1492930243286_8.png"/> </div><br>
### 6. 同步 I/O 与异步 I/O
### 6. 同步 I/O 与异步 I/O
- 同步 I/O应用进程在调用 recvfrom 操作时会阻塞。
- 异步 I/O不会阻塞。
- 同步 I/O应用进程在调用 recvfrom 操作时会阻塞。
- 异步 I/O不会阻塞。
阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。
阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O虽然非阻塞式 I/O 和信号驱动 I/O 在等待数据阶段不会阻塞,但是在之后的将数据从内核复制到应用进程这个操作会阻塞。
### 7. 五大 I/O 模型比较
### 7. 五大 I/O 模型比较
前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。
前四种 I/O 模型的主要区别在于第一个阶段,而第二个阶段是一样的:将数据从内核复制到应用进程过程中,应用进程会被阻塞。
![](index_files/1492928105791_3.png)
<div align="center"> <img src="../pics//1492928105791_3.png"/> </div><br>
## select/poll/epoll
## select/poll/epoll
这三个都是 I/O 多路复用的具体实现select 出现的最早之后是 poll再是 epoll。
这三个都是 I/O 多路复用的具体实现select 出现的最早,之后是 poll再是 epoll。
### 1. select
### 1. select
```c
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
int select(int n, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
```
fd_set 表示描述符集合类型有三个参数readset、writeset  exceptset分别对应读、写、异常条件的描述符集合。
fd_set 表示描述符集合类型有三个参数readset、writeset 和 exceptset分别对应读、写、异常条件的描述符集合。
timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间;
timeout 参数告知内核等待所指定描述符中的任何一个就绪可花多少时间;
成功调用返回结果大于 0出错返回结果为 -1超时返回结果为 0。
成功调用返回结果大于 0出错返回结果为 -1超时返回结果为 0。
每次调用 select 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从应用进程缓冲复制到内核缓冲。
每次调用 select 都需要将 fd_set \*readfds, fd_set \*writefds, fd_set \*exceptfds 链表内容全部从应用进程缓冲复制到内核缓冲。
返回结果中内核并没有声明 fd_set 中哪些描述符已经准备好所以如果返回值大于 0 应用进程需要遍历所有的 fd_set。
返回结果中内核并没有声明 fd_set 中哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程需要遍历所有的 fd_set。
select 最多支持 1024 个描述符其中 1024 由内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE然后重新编译内核。
select 最多支持 1024 个描述符,其中 1024 由内核的 FD_SETSIZE 决定。如果需要打破该限制可以修改 FD_SETSIZE然后重新编译内核。
```c
fd_set fd_in, fd_out;
struct timeval tv;
fd_set fd_in, fd_out;
struct timeval tv;
// Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );
// Reset the sets
FD_ZERO( &fd_in );
FD_ZERO( &fd_out );
// Monitor sock1 for input events
FD_SET( sock1, &fd_in );
// Monitor sock1 for input events
FD_SET( sock1, &fd_in );
// Monitor sock2 for output events
FD_SET( sock2, &fd_out );
// 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;
// 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;
// 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 );
// 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
// 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( sock1, &fd_in ) )
// input event on sock1
    if ( FD_ISSET( sock2, &fd_out ) )
        // output event on sock2
if ( FD_ISSET( sock2, &fd_out ) )
// output event on sock2
}
```
### 2. poll
### 2. poll
```c
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
```
```c
struct pollfd {
    int fd;       //文件描述符
    short events; //监视的请求事件
    short revents; //已发生的事件
struct pollfd {
int fd; //文件描述符
short events; //监视的请求事件
short revents; //已发生的事件
};
```
它和 select 功能基本相同。同样需要每次将描述符从应用进程复制到内核poll 调用返回后同样需要进行轮询才能知道哪些描述符已经准备好。
它和 select 功能基本相同。同样需要每次将描述符从应用进程复制到内核poll 调用返回后同样需要进行轮询才能知道哪些描述符已经准备好。
poll 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。
poll 取消了 1024 个描述符数量上限,但是数量太大以后不能保证执行效率,因为复制大量内存到内核十分低效,所需时间与描述符数量成正比。
poll 在描述符的重复利用上比 select  fd_set 会更好。
poll 在描述符的重复利用上比 select 的 fd_set 会更好。
如果在多线程下,如果一个线程对某个描述符调用了 poll 系统调用但是另一个线程关闭了该描述符会导致 poll 调用结果不确定该问题同样出现在 select 中。
如果在多线程下,如果一个线程对某个描述符调用了 poll 系统调用,但是另一个线程关闭了该描述符,会导致 poll 调用结果不确定,该问题同样出现在 select 中。
```c
// The structure for two events
struct pollfd fds[2];
// The structure for two events
struct pollfd fds[2];
// Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN;
// Monitor sock1 for input
fds[0].fd = sock1;
fds[0].events = POLLIN;
// Monitor sock2 for output
fds[1].fd = sock2;
fds[1].events = POLLOUT;
// 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
// Wait 10 seconds
int ret = poll( &fds, 2, 10000 );
// Check if poll actually succeed
if ( ret == -1 )
// report error and abort
else if ( ret == 0 )
// timeout; no event detected
else
{
    // If we detect the event, zero it out so we can reuse the structure
    if ( pfd[0].revents & POLLIN )
        pfd[0].revents = 0;
        // input event on sock1
// If we detect the event, zero it out so we can reuse the structure
if ( pfd[0].revents & POLLIN )
pfd[0].revents = 0;
// input event on sock1
    if ( pfd[1].revents & POLLOUT )
        pfd[1].revents = 0;
        // output event on sock2
if ( pfd[1].revents & POLLOUT )
pfd[1].revents = 0;
// output event on sock2
}
```
### 3. epoll
### 3. epoll
```c
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
```
epoll 仅仅适用于 Linux OS。
epoll 仅仅适用于 Linux OS。
它是 select  poll 的增强版,更加灵活而且没有描述符数量限制。
它是 select 和 poll 的增强版,更加灵活而且没有描述符数量限制。
它将用户关心的描述符放到内核的一个事件表中,从而只需要在用户空间和内核空间拷贝一次。
select  poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符一旦基于某个描述符就绪时内核会采用类似 callback 的回调机制迅速激活这个描述符当进程调用 epoll_wait() 时便得到通知。
select 和 poll 方式中,进程只有在调用一定的方法后,内核才对所有监视的描述符进行扫描。而 epoll 事先通过 epoll_ctl() 来注册描述符,一旦基于某个描述符就绪时,内核会采用类似 callback 的回调机制,迅速激活这个描述符,当进程调用 epoll_wait() 时便得到通知。
新版本的 epoll_create(int size) 参数 size 不起任何作用在旧版本的 epoll 中如果描述符的数量大于 size不保证服务质量。
新版本的 epoll_create(int size) 参数 size 不起任何作用,在旧版本的 epoll 中如果描述符的数量大于 size不保证服务质量。
epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。
epoll_ctl() 执行一次系统调用,用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理。
epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符将他们从内核复制到应用进程中不需要像 select/poll 对注册的所有描述符遍历一遍。
epoll_wait() 取出在内核中通过链表维护的 I/O 准备好的描述符,将他们从内核复制到应用进程中,不需要像 select/poll 对注册的所有描述符遍历一遍。
epoll 对多线程编程更有友好同时多个线程对同一个描述符调用了 epoll_wait() 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。
epoll 对多线程编程更有友好,同时多个线程对同一个描述符调用了 epoll_wait() 也不会产生像 select/poll 的不确定情况。或者一个线程调用了 epoll_wait 另一个线程关闭了同一个描述符也不会产生不确定情况。
```c
// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
// The function argument is ignored (it was not before, but now it is), so put your favorite number here
int pollingfd = epoll_create( 0xCAFE );
// 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
if ( pollingfd < 0 )
// report error
// Initialize the epoll structure in case more members are added in future
struct epoll_event ev = { 0 };
// 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;
// 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
// 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 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
// 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();
         }
    }
// Check if any events detected
for ( int i = 0; i < ret; i++ )
{
if ( pevents[i].events & EPOLLIN )
{
// Get back our connection pointer
Connection * c = (Connection*) pevents[i].data.ptr;
c->handleReadEvent();
}
}
}
```
## select  poll 比较
## select 和 poll 比较
### 1. 功能
### 1. 功能
它们提供了几乎相同的功能,但是在一些细节上有所不同:
- select 会修改 fd_set 参数 poll 不会;
- select 默认只能监听 1024 个描述符如果要监听更多的话需要修改 FD_SETSIZE 之后重新编译;
- poll 提供了更多的事件类型。
- select 会修改 fd_set 参数,而 poll 不会;
- select 默认只能监听 1024 个描述符,如果要监听更多的话,需要修改 FD_SETSIZE 之后重新编译;
- poll 提供了更多的事件类型。
### 2. 速度
### 2. 速度
poll  select 在速度上都很慢。
poll 和 select 在速度上都很慢。
- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢;
- select 只使用每个描述符的 3  poll 通常需要使用 64 因此 poll 需要复制更多的内核空间。
- 它们都采取轮询的方式来找到 I/O 完成的描述符,如果描述符很多,那么速度就会很慢;
- select 只使用每个描述符的 3 位,而 poll 通常需要使用 64 位,因此 poll 需要复制更多的内核空间。
### 3. 可移植性
### 3. 可移植性
几乎所有的系统都支持 select但是只有比较新的系统支持 poll。
几乎所有的系统都支持 select但是只有比较新的系统支持 poll。
## eopll 工作模式
## eopll 工作模式
epoll_event 有两种触发模式LTlevel trigger ETedge trigger
epoll_event 有两种触发模式LTlevel trigger和 ETedge trigger
### 1. LT 模式
### 1. LT 模式
 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 会再次响应应用程序并通知此事件。是默认的一种模式并且同时支持 Blocking  No-Blocking。
epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用 epoll_wait() 时,会再次响应应用程序并通知此事件。是默认的一种模式,并且同时支持 Blocking 和 No-Blocking。
### 2. ET 模式
### 2. ET 模式
 epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数因此效率要比 LT 模式高。只支持 No-Blocking以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
epoll_wait() 检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用 epoll_wait() 时,不会再次响应应用程序并通知此事件。很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
## select poll epoll 应用场景
## select poll epoll 应用场景
很容易产生一种错觉认为只要用 epoll 就可以了select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。
很容易产生一种错觉认为只要用 epoll 就可以了select poll 都是历史遗留问题,并没有什么应用场景,其实并不是这样的。
### 1. select 应用场景
### 1. select 应用场景
select() poll() epoll_wait() 都有一个 timeout 参数 select()  timeout 的精确度为 1ns poll()  epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求更高的场景,比如核反应堆的控制。
select() poll() epoll_wait() 都有一个 timeout 参数,在 select() 中 timeout 的精确度为 1ns而 poll() 和 epoll_wait() 中则为 1ms。所以 select 更加适用于实时要求更高的场景,比如核反应堆的控制。
select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。
select 历史更加悠久,它的可移植性更好,几乎被所有主流平台所支持。
### 2. poll 应用场景
### 2. poll 应用场景
poll 没有最大描述符数量的限制如果平台支持应该采用 poll 且对实时性要求并不是十分严格而不是 select。
poll 没有最大描述符数量的限制,如果平台支持应该采用 poll 且对实时性要求并不是十分严格,而不是 select。
需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll因为这个应用场景下并不能体现 epoll 的优势。
需要同时监控小于 1000 个描述符。那么也没有必要使用 epoll因为这个应用场景下并不能体现 epoll 的优势。
需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。
需要监控的描述符状态变化多,而且都是非常短暂的。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用频繁系统调用降低效率。epoll 的描述符存储在内核,不容易调试。
### 3. epoll 应用场景
### 3. epoll 应用场景
程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。
程序只需要运行在 Linux 平台上,有非常大量的描述符需要同时轮询,而且这些连接最好是长连接。
### 4. 性能对比
### 4. 性能对比
> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html)
> [epoll Scalability Web Page](http://lse.sourceforge.net/epoll/index.html)
# 参考资料
# 参考资料
- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/)
- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
- 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)

View File

@ -1,4 +1,22 @@
# 一、可读性的重要性
<!-- GFM-TOC -->
* [一、可读性的重要性](#一可读性的重要性)
* [二、用名字表达代码含义](#二用名字表达代码含义)
* [三、名字不能带来歧义](#三名字不能带来歧义)
* [四、良好的代码风格](#四良好的代码风格)
* [五、编写注释](#五编写注释)
* [六、如何编写注释](#六如何编写注释)
* [七、提高控制流的可读性](#七提高控制流的可读性)
* [八、拆分长表达式](#八拆分长表达式)
* [九、变量与可读性](#九变量与可读性)
* [十、抽取函数](#十抽取函数)
* [十一、一次只做一件事](#十一一次只做一件事)
* [十二、用自然语言表述代码](#十二用自然语言表述代码)
* [十三、减少代码量](#十三减少代码量)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、可读性的重要性
编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。
@ -6,50 +24,50 @@
只有在核心领域为了效率才可以放弃可读性,否则可读性是第一位。
# 二、用名字表达代码含义
# 二、用名字表达代码含义
一些比较有表达力的单词:
|  单词 |  可替代单词 |
| :---: | --- |
|  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 |
| 单词 | 可替代单词 |
| :---: | --- |
| send | deliver、dispatch、announce、distribute、route |
| find | search、extract、locate、recover |
| start| launch、create、begin、open|
| make | create、set up、build、generate、compose、add、new |
使用 i、j、k 作为循环迭代器的名字过于简单user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。
使用 i、j、k 作为循环迭代器的名字过于简单user_i、member_i 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。
为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。
# 三、名字不能带来歧义
# 三、名字不能带来歧义
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
 min、max 表示数量范围 first、last 表示访问空间的包含范围begin、end 表示访问空间的排除范围 end 不包含尾部。
min、max 表示数量范围;用 first、last 表示访问空间的包含范围begin、end 表示访问空间的排除范围,即 end 不包含尾部。
![](index_files/05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg)
<div align="center"> <img src="../pics//05907ab4-42c5-4b5e-9388-6617f6c97bea.jpg"/> </div><br>
布尔相关的命名加上 is、can、should、has 等前缀。
布尔相关的命名加上 is、can、should、has 等前缀。
# 四、良好的代码风格
# 四、良好的代码风格
适当的空行和缩进。
排列整齐的注释:
```java
int a = 1;   // 注释
int b = 11;  // 注释
int c = 111; // 注释
int a = 1; // 注释
int b = 11; // 注释
int c = 111; // 注释
```
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致;
语句顺序不能随意,比如与 html 表单相关联的变量的赋值应该和表单在 html 中的顺序一致;
把相关的代码按块组织起来放在一起。
# 五、编写注释
# 五、编写注释
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter  setter 方法,为这些方法写注释反而让代码可读性更差。
阅读代码首先会注意到注释,如果注释没太大作用,那么就会浪费代码阅读的时间。那些能直接看出含义的代码不需要写注释,特别是并不需要为每个方法都加上注释,比如那些简单的 getter 和 setter 方法,为这些方法写注释反而让代码可读性更差。
不能因为有注释就随便起个名字,而是争取起个好名字而不写注释。
@ -57,155 +75,155 @@ int c = 111; // 注释
注释用来提醒一些特殊情况。
 TODO 等做标记:
TODO 等做标记:
| 标记 | 用法 |
| 标记 | 用法 |
|---|---|
|TODO| 待做 |
|FIXME| 待修复 |
|HACH| 粗糙的解决方案 |
|XXX| 危险!这里有重要的问题 |
|TODO| 待做 |
|FIXME| 待修复 |
|HACH| 粗糙的解决方案 |
|XXX| 危险!这里有重要的问题 |
# 六、如何编写注释
# 六、如何编写注释
尽量简洁明了:
```java
// The first String is student's name
// The Second Integer is student's score
Map<String, Integer> scoreMap = new HashMap<>();
// 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<>();
// 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;
// ...
// Example: add(1, 2), return 3
int add(int x, int y) {
return x + y;
}
```
在很复杂的函数调用中对每个参数标上名字:
```java
int a = 1;
int b = 2;
int num = add(\* x = *\ a, \* y = *\ b);
int a = 1;
int b = 2;
int num = add(\* x = *\ a, \* y = *\ b);
```
使用专业名词来缩短概念上的解释,比如用设计模式名来说明代码。
# 七、提高控制流的可读性
# 七、提高控制流的可读性
条件表达式中,左侧是变量,右侧是常数。比如下面第一个语句正确:
```java
if (len < 10)
if (10 > len)
if (len < 10)
if (10 > len)
```
if / else 条件语句逻辑的处理顺序为 正逻辑 关键逻辑 简单逻辑。
if / else 条件语句,逻辑的处理顺序为:① 正逻辑;② 关键逻辑;③ 简单逻辑。
```java
if (a == b) {
    // 正逻辑
} else{
    // 反逻辑
if (a == b) {
// 正逻辑
} else{
// 反逻辑
}
```
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑否则应该拆分成 if / else
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑,否则应该拆分成 if / else
do / while 的条件放在后面不够简单明了并且会有一些迷惑的地方最好使用 while 来代替。
do / while 的条件放在后面,不够简单明了,并且会有一些迷惑的地方,最好使用 while 来代替。
如果只有一个 goto 目标那么 goto 尚且还能接受但是过于复杂的 goto 会让代码可读性特别差应该避免使用 goto。
如果只有一个 goto 目标,那么 goto 尚且还能接受,但是过于复杂的 goto 会让代码可读性特别差,应该避免使用 goto。
在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
在嵌套的循环中,用一些 return 语句往往能减少嵌套的层数。
# 八、拆分长表达式
# 八、拆分长表达式
长表达式的可读性很差,可以引入一些解释变量从而拆分表达式:
```python
if line.split(':')[0].strip() == "root":
    ...
if line.split(':')[0].strip() == "root":
...
```
```python
username = line.split(':')[0].strip()
if username == "root":
    ...
username = line.split(':')[0].strip()
if username == "root":
...
```
使用摩根定理简化一些逻辑表达式:
```java
if (!a && !b) {
    ...
if (!a && !b) {
...
}
```
```java
if (!(a || b)) {
    ...
if (!(a || b)) {
...
}
```
# 九、变量与可读性
# 九、变量与可读性
**去除控制流变量**。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。
**去除控制流变量** 。在循环中通过使用 break 或者 return 可以减少控制流变量的使用。
```java
boolean done = false;
while (/* condition */ && !done) {
    ...
    if ( ... ) {
        done = true;
        continue;
    }
boolean done = false;
while (/* condition */ && !done) {
...
if ( ... ) {
done = true;
continue;
}
}
```
```java
while(/* condition */) {
    ...
    if ( ... ) {
        break;
    }
while(/* condition */) {
...
if ( ... ) {
break;
}
}
```
**减小变量作用域**。作用域越小,越容易定位到变量所有使用的地方。
**减小变量作用域** 。作用域越小,越容易定位到变量所有使用的地方。
JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
JavaScript 可以用闭包减小作用域。以下代码中 submit_form 是函数变量submitted 变量控制函数不会被提交两次。第一个实现中 submitted 是全局变量,第二个实现把 submitted 放到匿名函数中,从而限制了起作用域范围。
```js
submitted = false;
var submit_form = function(form_name) {
    if (submitted) {
        return;
    }
    submitted = true;
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;
    }
}());  // () 使得外层匿名函数立即执行
var submit_form = (function() {
var submitted = false;
return function(form_name) {
if(submitted) {
return;
}
submitted = true;
}
}()); // () 使得外层匿名函数立即执行
```
JavaScript 中没有用 var 声明的变量都是全局变量而全局变量很容易造成迷惑因此应当总是用 var 来声明变量。
JavaScript 中没有用 var 声明的变量都是全局变量,而全局变量很容易造成迷惑,因此应当总是用 var 来声明变量。
变量定义的位置应当离它使用的位置最近。
@ -214,54 +232,54 @@ JavaScript 中没有用 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 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 字段中,初始实现如下:
现在要接受一个字符串并把它放到第一个空的 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;
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 循环;
- 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;
        }
    }
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;
}
}
};
```
# 十、抽取函数
# 十、抽取函数
工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。
@ -270,38 +288,38 @@ var setFirstEmptyInput = function(new_value) {
介绍性的代码:
```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;
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;
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;
}
```
@ -309,22 +327,22 @@ public int findClostElement(int[] arr) {
函数抽取也用于减小代码的冗余。
# 十一、一次只做一件事
# 十一、一次只做一件事
只做一件事的代码很容易让人知道其要做的事;
基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。
# 十二、用自然语言表述代码
# 十二、用自然语言表述代码
先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。
# 十三、减少代码量
# 十三、减少代码量
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
多用标准库实现。
# 参考资料
# 参考资料
- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.
- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.

View File

@ -1,14 +1,21 @@
# Google Java Style Guide
<!-- GFM-TOC -->
* [Google Java Style Guide](#google-java-style-guide)
* [Google C++ Style Guide](#google-c-style-guide)
* [Google Python Style Guide](#google-python-style-guide)
<!-- GFM-TOC -->
- http://www.hawstein.com/posts/google-java-style.html
- http://google.github.io/styleguide/javaguide.html
# Google C++ Style Guide
# Google Java Style Guide
- http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/
- http://google.github.io/styleguide/cppguide.html
- http://www.hawstein.com/posts/google-java-style.html
- http://google.github.io/styleguide/javaguide.html
# Google Python Style Guide
# Google C++ Style Guide
- http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/
- http://google.github.io/styleguide/pyguide.html
- http://zh-google-styleguide.readthedocs.io/en/latest/google-cpp-styleguide/contents/
- http://google.github.io/styleguide/cppguide.html
# Google Python Style Guide
- http://zh-google-styleguide.readthedocs.io/en/latest/google-python-styleguide/contents/
- http://google.github.io/styleguide/pyguide.html

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,30 +1,72 @@
# 一、事务
<!-- GFM-TOC -->
* [一、事务](#一事务)
* [概念](#概念)
* [ACID](#acid)
* [AUTOCOMMIT](#autocommit)
* [二、并发一致性问题](#二并发一致性问题)
* [丢失修改](#丢失修改)
* [读脏数据](#读脏数据)
* [不可重复读](#不可重复读)
* [幻影读](#幻影读)
* [三、封锁](#三封锁)
* [封锁粒度](#封锁粒度)
* [封锁类型](#封锁类型)
* [封锁协议](#封锁协议)
* [MySQL 隐式与显示锁定](#mysql-隐式与显示锁定)
* [四、隔离级别](#四隔离级别)
* [未提交读READ UNCOMMITTED](#未提交读read-uncommitted)
* [提交读READ COMMITTED](#提交读read-committed)
* [可重复读REPEATABLE READ](#可重复读repeatable-read)
* [可串行化SERIALIXABLE](#可串行化serialixable)
* [五、多版本并发控制](#五多版本并发控制)
* [版本号](#版本号)
* [Undo 日志](#undo-日志)
* [实现过程](#实现过程)
* [快照读与当前读](#快照读与当前读)
* [六、Next-Key Locks](#六next-key-locks)
* [Record Locks](#record-locks)
* [Grap Locks](#grap-locks)
* [Next-Key Locks](#next-key-locks)
* [七、关系数据库设计理论](#七关系数据库设计理论)
* [函数依赖](#函数依赖)
* [异常](#异常)
* [范式](#范式)
* [八、ER 图](#八er-图)
* [实体的三种联系](#实体的三种联系)
* [表示出现多次的关系](#表示出现多次的关系)
* [联系的多向性](#联系的多向性)
* [表示子类](#表示子类)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 概念
![](index_files/185b9c49-4c13-4241-a848-fbff85c03a64.png)
# 一、事务
事务指的是满足 ACID 特性的一组操作可以通过 Commit 提交一个事务也可以使用 Rollback 进行回滚。
## 概念
## ACID
<div align="center"> <img src="../pics//185b9c49-4c13-4241-a848-fbff85c03a64.png"/> </div><br>
### 1. 原子性Atomicity
事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
## ACID
### 1. 原子性Atomicity
事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时方向执行这些修改操作即可。
### 2. 一致性Consistency
### 2. 一致性Consistency
数据库在事务执行前后都保持一致性状态。
在一致性状态下,所有事务对一个数据的读取结果都是相同的。
### 3. 隔离性Isolation
### 3. 隔离性Isolation
一个事务所做的修改在最终提交以前,对其它事务是不可见的。
### 4. 持久性Durability
### 4. 持久性Durability
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
@ -32,58 +74,58 @@
----
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时要只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并发执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库奔溃的情况。
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时要只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并发执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库奔溃的情况。
![](index_files/35650b4b-efa1-49ba-9680-19837027cfc9.png)
<div align="center"> <img src="../pics//35650b4b-efa1-49ba-9680-19837027cfc9.png"/> </div><br>
## AUTOCOMMIT
## AUTOCOMMIT
MySQL 默认采用自动提交模式。也就是说,如果不显式使用`START TRANSACTION`语句来开始一个事务,那么每个查询都会被当做一个事务自动提交。
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> 的修改。
T<sub>1</sub> 和 T<sub>2</sub> 两个事务都对一个数据进行修改T<sub>1</sub> 先修改T<sub>2</sub> 随后修改T<sub>2</sub> 的修改覆盖了 T<sub>1</sub> 的修改。
![](index_files/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png)
<div align="center"> <img src="../pics//88ff46b3-028a-4dbb-a572-1f062b8b96d3.png"/> </div><br>
## 读脏数据
## 读脏数据
T<sub>1</sub> 修改一个数据T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改那么 T<sub>2</sub> 读取的数据是脏数据。
T<sub>1</sub> 修改一个数据T<sub>2</sub> 随后读取这个数据。如果 T<sub>1</sub> 撤销了这次修改,那么 T<sub>2</sub> 读取的数据是脏数据。
![](index_files/dd782132-d830-4c55-9884-cfac0a541b8e.png)
<div align="center"> <img src="../pics//dd782132-d830-4c55-9884-cfac0a541b8e.png"/> </div><br>
## 不可重复读
## 不可重复读
T<sub>2</sub> 读取一个数据T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
T<sub>2</sub> 读取一个数据T<sub>1</sub> 对该数据做了修改。如果 T<sub>2</sub> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
![](index_files/c8d18ca9-0b09-441a-9a0c-fb063630d708.png)
<div align="center"> <img src="../pics//c8d18ca9-0b09-441a-9a0c-fb063630d708.png"/> </div><br>
## 幻影读
## 幻影读
T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插入新的数据T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
T<sub>1</sub> 读取某个范围的数据T<sub>2</sub> 在这个范围内插入新的数据T<sub>1</sub> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
![](index_files/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png)
<div align="center"> <img src="../pics//72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png"/> </div><br>
----
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
# 三、封锁
# 三、封锁
## 封锁粒度
## 封锁粒度
<img src="index_files/1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/>
<div align="center"> <img src="../pics//1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg" width="300"/> </div><br>
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
@ -91,120 +133,120 @@ MySQL 中提供了两种封锁粒度行级锁以及表级锁。
在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。
## 封锁类型
## 封锁类型
### 1. 读写锁
### 1. 读写锁
- 排它锁Exclusive简写为 X 锁,又称写锁。
- 共享锁Shared简写为 S 锁,又称读锁。
- 排它锁Exclusive简写为 X 锁,又称写锁。
- 共享锁Shared简写为 S 锁,又称读锁。
有以下两个规定:
- 一个事务对数据对象 A 加了 X 就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
- 一个事务对数据对象 A 加了 S 可以对 A 进行读取操作但是不能进行更新操作。加锁期间其它事务能对 A  S 但是不能加 X 锁。
- 一个事务对数据对象 A 加了 X 锁,就可以对 A 进行读取和更新。加锁期间其它事务不能对 A 加任何锁。
- 一个事务对数据对象 A 加了 S 锁,可以对 A 进行读取操作,但是不能进行更新操作。加锁期间其它事务能对 A 加 S 锁,但是不能加 X 锁。
锁的兼容关系如下:
| - | X | S |
| :--: | :--: | :--: |
| - | X | S |
| :--: | :--: | :--: |
|X|NO|NO|
|S|NO|YES|
### 2. 意向锁
### 2. 意向锁
使用意向锁Intention Locks可以更容易地支持多粒度封锁。
使用意向锁Intention Locks可以更容易地支持多粒度封锁。
在存在行级锁和表级锁的情况下,事务 T 想要对表 A  X 就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁那么就需要对表 A 的每一行都检测一次,这是非常耗时的。
在存在行级锁和表级锁的情况下,事务 T 想要对表 A 加 X 锁,就需要先检测是否有其它事务对表 A 或者表 A 中的任意一行加了锁,那么就需要对表 A 的每一行都检测一次,这是非常耗时的。
意向锁在原来的 X/S 锁之上引入了 IX/ISIX/IS 都是表锁用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:
意向锁在原来的 X/S 锁之上引入了 IX/ISIX/IS 都是表锁,用来表示一个事务想要在表中的某个数据行上加 X 锁或 S 锁。有以下两个规定:
- 一个事务在获得某个数据行对象的 S 锁之前必须先获得表的 IS 锁或者更强的锁;
- 一个事务在获得某个数据行对象的 X 锁之前必须先获得表的 IX 锁。
- 一个事务在获得某个数据行对象的 S 锁之前,必须先获得表的 IS 锁或者更强的锁;
- 一个事务在获得某个数据行对象的 X 锁之前,必须先获得表的 IX 锁。
通过引入意向锁,事务 T 想要对表 A  X 只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T  X 锁失败。
通过引入意向锁,事务 T 想要对表 A 加 X 锁,只需要先检测是否有其它事务对表 A 加了 X/IX/S/IS 锁,如果加了就表示有其它事务正在使用这个表或者表中某一行的锁,因此事务 T 加 X 锁失败。
各种锁的兼容关系如下:
| - | X | IX | S | IS |
| :--: | :--: | :--: | :--: | :--: |
|X     |NO    |NO    |NO   | NO|
|IX    |NO    |YES   |NO   | YES|
|S     |NO    |NO    |YES  | YES|
|IS    |NO    |YES  |YES  | YES|
| - | X | IX | S | IS |
| :--: | :--: | :--: | :--: | :--: |
|X |NO |NO |NO | NO|
|IX |NO |YES |NO | YES|
|S |NO |NO |YES | YES|
|IS |NO |YES |YES | YES|
解释如下:
- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁;
- S 锁只与 S 锁和 IS 锁兼容也就是说事务 T 想要对数据行加 S 其它事务可以已经获得对表或者表中的行的 S 锁。
- 任意 IS/IX 锁之间都是兼容的,因为它们只是表示想要对表加锁,而不是真正加锁;
- S 锁只与 S 锁和 IS 锁兼容,也就是说事务 T 想要对数据行加 S 锁,其它事务可以已经获得对表或者表中的行的 S 锁。
## 封锁协议
## 封锁协议
### 1. 三级封锁协议
### 1. 三级封锁协议
**一级封锁协议**
事务 T 要修改数据 A 时必须加 X 直到 T 结束才释放锁。
事务 T 要修改数据 A 时必须加 X 锁,直到 T 结束才释放锁。
可以解决丢失修改问题,因为不能同时有两个事务对同一个数据进行修改,那么事务的修改就不会被覆盖。
| T<sub>1</sub> | T<sub>1</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)|
| T<sub>1</sub> | T<sub>1</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 时必须加 S 锁,读取完马上释放 S 锁。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改根据 1 级封锁协议会加 X 那么就不能再加 S 锁了,也就是不会读入数据。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
| T<sub>1</sub> | T<sub>1</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| write A=19 | |
| | lock-s(A) |
|  | wait |
| rollback | .|
| A=20 |. |
| unlock-x(A) |. |
| | obtain |
| | read A=20 |
| | commit |
| | unlock-s(A)|
| T<sub>1</sub> | T<sub>1</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| write A=19 | |
| | lock-s(A) |
| | wait |
| rollback | .|
| A=20 |. |
| unlock-x(A) |. |
| | obtain |
| | read A=20 |
| | commit |
| | unlock-s(A)|
**三级封锁协议**
在二级的基础上,要求读取数据 A 时必须加 S 直到事务结束了才能释放 S 锁。
在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
可以解决不可重复读的问题,因为读 A 其它事务不能对 A  X 锁,从而避免了在读的期间数据发生改变。
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
| T<sub>1</sub> | T<sub>1</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)|
| T<sub>1</sub> | T<sub>1</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. 两段锁协议
### 2. 两段锁协议
加锁和解锁分为两个阶段进行。
@ -222,332 +264,317 @@ lock-x(A)...lock-s(B)...lock-s(C)...unlock(A)...unlock(C)...unlock(B)
lock-x(A)...unlock(A)...lock-s(B)...unlock(B)...lock-s(C)...unlock(C)
```
## MySQL 隐式与显示锁定
## MySQL 隐式与显示锁定
MySQL  InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。
MySQL 的 InnoDB 存储引擎采用两段锁协议,会根据隔离级别在需要的时候自动加锁,并且所有的锁都是在同一时刻被释放,这被称为隐式锁定。
InnoDB 也可以使用特定的语句进行显示锁定:
InnoDB 也可以使用特定的语句进行显示锁定:
```sql
SELECT ... LOCK In SHARE MODE;
SELECT ... FOR UPDATE;
SELECT ... LOCK In SHARE MODE;
SELECT ... FOR UPDATE;
```
# 四、隔离级别
# 四、隔离级别
## 未提交读READ UNCOMMITTED
## 未提交读READ UNCOMMITTED
事务中的修改,即使没有提交,对其它事务也是可见的。
## 提交读READ COMMITTED
## 提交读READ COMMITTED
一个事务只能读取已经提交的事务所做的修改。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。
## 可重复读REPEATABLE READ
## 可重复读REPEATABLE READ
保证在同一个事务中多次读取同样数据的结果是一样的。
## 可串行化SERIALIXABLE
## 可串行化SERIALIXABLE
强制事务串行执行。
----
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
| :---: | :---: | :---:| :---: |
| 未提交读 | YES | YES | YES |
| 提交读 | NO | YES | YES |
| 可重复读 | NO | NO | YES |
| 可串行化 | NO | NO | NO |
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 |
| :---: | :---: | :---:| :---: |
| 未提交读 | YES | YES | YES |
| 提交读 | NO | YES | YES |
| 可重复读 | NO | NO | YES |
| 可串行化 | NO | NO | NO |
# 五、多版本并发控制
# 五、多版本并发控制
多版本并发控制Multi-Version Concurrency Control, MVCC MySQL  InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
多版本并发控制Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
## 版本号
## 版本号
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。
InooDB  MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
InooDB 的 MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
- 创建版本号:指示创建一个数据行的快照时的系统版本号;
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
- 创建版本号:指示创建一个数据行的快照时的系统版本号;
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
## Undo 日志
## Undo 日志
InnoDB  MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
InnoDB 的 MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
![](index_files/e41405a8-7c05-4f70-8092-e961e28d3112.jpg)
<div align="center"> <img src="../pics//e41405a8-7c05-4f70-8092-e961e28d3112.jpg"/> </div><br>
## 实现过程
## 实现过程
以下实现过程针对可重复读隔离级别。
### 1. SELECT
### 1. SELECT
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号,因为如果大于或者等于 T 的版本号,那么表示该数据行快照是其它事务的最新修改,因此不能去读取它。
除了上面的要求T 所要读取的数据行快照的删除版本号必须大于 T 的版本号因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
除了上面的要求T 所要读取的数据行快照的删除版本号必须大于 T 的版本号,因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
### 2. INSERT
### 2. INSERT
将当前系统版本号作为数据行快照的创建版本号。
### 3. DELETE
### 3. DELETE
将当前系统版本号作为数据行快照的删除版本号。
### 4. UPDATE
### 4. UPDATE
将当前系统版本号作为更新后的数据行快照的创建版本号,同时将当前系统版本号作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。
将当前系统版本号作为更新后的数据行快照的创建版本号,同时将当前系统版本号作为更新前的数据行快照的删除版本号。可以理解为先执行 DELETE 后执行 INSERT。
## 快照读与当前读
## 快照读与当前读
### 1. 快照读
### 1. 快照读
使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。
使用 MVCC 读取的是快照中的数据,这样可以减少加锁所带来的开销。
```sql
select * from table ...;
select * from table ...;
```
### 2. 当前读
### 2. 当前读
读取的是最新的数据,需要加锁。以下第一个语句需要加 S 其它都需要加 X 锁。
读取的是最新的数据,需要加锁。以下第一个语句需要加 S 锁,其它都需要加 X 锁。
```sql
select * from table where ? lock in share mode;
select * from table where ? for update;
select * from table where ? lock in share mode;
select * from table where ? for update;
insert;
update;
delete;
```
# 六、Next-Key Locks
# 六、Next-Key Locks
Next-Key Locks 也是 MySQL  InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题Next-Key Locks 就是为了解决这个问题而存在的。在可重复读REPEATABLE READ隔离级别下使用 MVCC + Next-Key Locks 可以解决幻读问题。
Next-Key Locks 也是 MySQL 的 InnoDB 存储引擎的一种锁实现。MVCC 不能解决幻读的问题Next-Key Locks 就是为了解决这个问题而存在的。在可重复读REPEATABLE READ隔离级别下使用 MVCC + Next-Key Locks 可以解决幻读问题。
## Record Locks
## Record Locks
锁定的对象是索引而不是数据。如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚集索引因此 Record Locks 依然可以使用。
锁定的对象是索引而不是数据。如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚集索引,因此 Record Locks 依然可以使用。
## Grap Locks
## Grap Locks
锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
锁定一个范围内的索引,例如当一个事务执行以下语句,其它事务就不能在 t.c 中插入 15。
```sql
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
```
## Next-Key Locks
## Next-Key Locks
它是 Record Locks  Gap Locks 的结合。在 user 中有以下记录:
它是 Record Locks 和 Gap Locks 的结合。在 user 中有以下记录:
```sql
|   id | last_name   | first_name   |   age |
| id | last_name | first_name | age |
|------|-------------|--------------|-------|
|    4 | stark       | tony         |    21 |
|    1 | tom         | hiddleston   |    30 |
|    3 | morgan      | freeman      |    40 |
|    5 | jeff        | dean         |    50 |
|    2 | donald      | trump        |    80 |
| 4 | stark | tony | 21 |
| 1 | tom | hiddleston | 30 |
| 3 | morgan | freeman | 40 |
| 5 | jeff | dean | 50 |
| 2 | donald | trump | 80 |
+------|-------------|--------------|-------+
```
那么就需要锁定以下范围:
```sql
(-∞, 21]
(21, 30]
(30, 40]
(40, 50]
(50, 80]
(80, ∞)
(-∞, 21]
(21, 30]
(30, 40]
(40, 50]
(50, 80]
(80, ∞)
```
# 七、关系数据库设计理论
# 七、关系数据库设计理论
## 函数依赖
## 函数依赖
 A->B 表示 A 函数决定 B也可以说 B 函数依赖于 A。
A->B 表示 A 函数决定 B也可以说 B 函数依赖于 A。
如果 {A1A2... An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
如果 {A1A2... An} 是关系的一个或多个属性的集合,该集合函数决定了关系的其它所有属性并且是最小的,那么该集合就称为键码。
对于 A->B如果能找到 A 的真子集 A'使得 A'-> B那么 A->B 就是部分函数依赖,否则就是完全函数依赖;
对于 A->B如果能找到 A 的真子集 A',使得 A'-> B那么 A->B 就是部分函数依赖,否则就是完全函数依赖;
对于 A->BB->C A->C 是一个传递依赖。
对于 A->BB->C则 A->C 是一个传递依赖。
## 异常
## 异常
以下的学生课程关系的函数依赖为 Sno, Cname -> Sname, Sdept, Mname, Grade键码为 {Sno, Cname}。也就是说,确定学生和课程之后,就能确定其它信息。
以下的学生课程关系的函数依赖为 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 |
| 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 的信息就会丢失。
- 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
- 冗余数据:例如 学生-2 出现了两次。
- 修改异常:修改了一个记录中的信息,但是另一个记录中相同的信息却没有被修改。
- 删除异常:删除一个信息,那么也会丢失其它信息。例如如果删除了 课程-1需要删除第一行和第三行那么 学生-1 的信息就会丢失。
- 插入异常,例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
## 范式
## 范式
范式理论是为了解决以上提到四种异常。高级别范式的依赖于低级别的范式。
<img src="index_files/c2d343f7-604c-4856-9a3c-c71d6f67fecc.png" width="300"/>
<div align="center"> <img src="../pics//c2d343f7-604c-4856-9a3c-c71d6f67fecc.png" width="300"/> </div><br>
### 1. 第一范式 (1NF)
### 1. 第一范式 (1NF)
属性不可分;
### 2. 第二范式 (2NF)
### 2. 第二范式 (2NF)
每个非主属性完全函数依赖于键码。
可以通过分解来满足。
<font size=4>**分解前**</font><br>
<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 | 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, Cname} 为键码,有如下函数依赖:
- Sno -> Sname, Sdept
- Sdept -> Mname
- Sno, Cname-> Grade
- Sno -> Sname, Sdept
- Sdept -> Mname
- Sno, Cname-> Grade
Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。
Grade 完全函数依赖于键码,它没有任何冗余数据,每个学生的每门课都有特定的成绩。
Sname, Sdept  Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。
Sname, Sdept 和 Mname 都部分依赖于键码,当一个学生选修了多门课时,这些数据就会出现多次,造成大量冗余数据。
<font size=4>**分解后**</font><br>
<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 | Mname |
| :---: | :---: | :---: | :---: |
| 1 | 学生-1 | 学院-1 | 院长-1 |
| 2 | 学生-2 | 学院-2 | 院长-2 |
| 3 | 学生-3 | 学院-2 | 院长-2 |
有以下函数依赖:
- Sno -> Sname, Sdept, Mname
- Sdept -> Mname
- Sno -> Sname, Sdept, Mname
- Sdept -> Mname
关系-2
| Sno | Cname | Grade |
| :---: | :---: |:---:|
| 1 | 课程-1 | 90 |
| 2 | 课程-2 | 80 |
| 2 | 课程-1 | 100 |
| 3 | 课程-2 | 95 |
| Sno | Cname | Grade |
| :---: | :---: |:---:|
| 1 | 课程-1 | 90 |
| 2 | 课程-2 | 80 |
| 2 | 课程-1 | 100 |
| 3 | 课程-2 | 95 |
有以下函数依赖:
- Sno, Cname ->  Grade
- Sno, Cname -> Grade
### 3. 第三范式 (3NF)
### 3. 第三范式 (3NF)
非主属性不传递依赖于键码。
上面的 关系-1 中存在以下传递依赖Sno -> Sdept -> Mname可以进行以下分解
上面的 关系-1 中存在以下传递依赖Sno -> Sdept -> Mname可以进行以下分解
关系-11
| Sno | Sname | Sdept |
| :---: | :---: | :---: |
| 1 | 学生-1 | 学院-1 |
| 2 | 学生-2 | 学院-2 |
| 3 | 学生-3 | 学院-2 |
| Sno | Sname | Sdept |
| :---: | :---: | :---: |
| 1 | 学生-1 | 学院-1 |
| 2 | 学生-2 | 学院-2 |
| 3 | 学生-3 | 学院-2 |
关系-12
| Sdept | Mname |
| :---: | :---: |
| 学院-1 | 院长-1 |
| 学院-2 | 院长-2 |
| Sdept | Mname |
| :---: | :---: |
| 学院-1 | 院长-1 |
| 学院-2 | 院长-2 |
# 八、ER 
# 八、ER
Entity-Relationship有三个组成部分实体、属性、联系。
用来进行数据库系统的概念设计。
## 实体的三种联系
## 实体的三种联系
包含一对一,一对多,多对多三种。
如果 A  B 是一对多关系那么画个带箭头的线段指向 B如果是一对一画两个带箭头的线段如果是多对多画两个不带箭头的线段。下图的 Course  Student 是一对多的关系。
如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B如果是一对一画两个带箭头的线段如果是多对多画两个不带箭头的线段。下图的 Course 和 Student 是一对多的关系。
![](index_files/292b4a35-4507-4256-84ff-c218f108ee31.jpg)
<div align="center"> <img src="../pics//292b4a35-4507-4256-84ff-c218f108ee31.jpg"/> </div><br>
## 表示出现多次的关系
## 表示出现多次的关系
一个实体在联系出现几次,就要用几条线连接。下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
一个实体在联系出现几次,就要用几条线连接。下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
![](index_files/8b798007-e0fb-420c-b981-ead215692417.jpg)
<div align="center"> <img src="../pics//8b798007-e0fb-420c-b981-ead215692417.jpg"/> </div><br>
## 联系的多向性
## 联系的多向性
虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。
![](index_files/423f2a40-bee1-488e-b460-8e76c48ee560.png)
<div align="center"> <img src="../pics//423f2a40-bee1-488e-b460-8e76c48ee560.png"/> </div><br>
一般只使用二元联系,可以把多元关系转换为二元关系。
![](index_files/de9b9ea0-1327-4865-93e5-6f805c48bc9e.png)
<div align="center"> <img src="../pics//de9b9ea0-1327-4865-93e5-6f805c48bc9e.png"/> </div><br>
## 表示子类
## 表示子类
用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。
![](index_files/7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg)
<div align="center"> <img src="../pics//7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg"/> </div><br>
# 参考资料
# 参考资料
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- AbrahamSilberschatz, HenryF.Korth, S.Sudarshan, 等. 数据库系统概念 [M]. 机械工业出版社, 2006.
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
- [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)
- [三级模式与两级映像](http://blog.csdn.net/d2457638978/article/details/48783923)
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
- [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)
---bottom---CyC---
![](index_files/185b9c49-4c13-4241-a848-fbff85c03a64.png)
![](index_files/7b48448f-cbe3-4287-9041-f56566b9d0b4.png)
![](index_files/88ff46b3-028a-4dbb-a572-1f062b8b96d3.png)
![](index_files/dd782132-d830-4c55-9884-cfac0a541b8e.png)
![](index_files/c8d18ca9-0b09-441a-9a0c-fb063630d708.png)
![](index_files/72fe492e-f1cb-4cfc-92f8-412fb3ae6fec.png)
![](index_files/1a851e90-0d5c-4d4f-ac54-34c20ecfb903.jpg)
![](index_files/e41405a8-7c05-4f70-8092-e961e28d3112.jpg)
![](index_files/c2d343f7-604c-4856-9a3c-c71d6f67fecc.png)
![](index_files/292b4a35-4507-4256-84ff-c218f108ee31.jpg)
![](index_files/8b798007-e0fb-420c-b981-ead215692417.jpg)
![](index_files/423f2a40-bee1-488e-b460-8e76c48ee560.png)
![](index_files/de9b9ea0-1327-4865-93e5-6f805c48bc9e.png)
![](index_files/7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg)
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- AbrahamSilberschatz, HenryF.Korth, S.Sudarshan, 等. 数据库系统概念 [M]. 机械工业出版社, 2006.
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
- [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)
- [三级模式与两级映像](http://blog.csdn.net/d2457638978/article/details/48783923)
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
- [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)

View File

@ -1,4 +1,19 @@
# 一、概述
<!-- GFM-TOC -->
* [一、概述](#一概述)
* [二、匹配单个字符](#二匹配单个字符)
* [三、匹配一组字符](#三匹配一组字符)
* [四、使用元字符](#四使用元字符)
* [五、重复匹配](#五重复匹配)
* [六、位置匹配](#六位置匹配)
* [七、使用子表达式](#七使用子表达式)
* [八、回溯引用](#八回溯引用)
* [九、前后查找](#九前后查找)
* [十、嵌入条件](#十嵌入条件)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、概述
正则表达式用于文本内容的查找和替换。
@ -6,13 +21,13 @@
[正则表达式在线工具](https://regexr.com/)
# 二、匹配单个字符
# 二、匹配单个字符
正则表达式一般是区分大小写的,但是也有些实现是不区分。
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
**\\** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 
**\\** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \
**正则表达式**
@ -22,21 +37,21 @@ nam.
**匹配结果**
My **name** is Zheng.
My **name** is Zheng.
# 三、匹配一组字符
# 三、匹配一组字符
**[ ]** 定义一个字符集合;
**[ ]** 定义一个字符集合;
0-9、a-z 定义了一个字符区间区间使用 ASCII 码来确定字符区间只能用在 [ ] 之间。
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间只能用在 [ ] 之间。
**-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**-** 元字符只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**^** 在 [ ] 字符集合中是取非操作。
**^** 在 [ ] 字符集合中是取非操作。
**应用**
匹配以 abc 为开头,并且最后一个字母不为数字的字符串:
匹配以 abc 为开头,并且最后一个字母不为数字的字符串:
**正则表达式**
@ -46,55 +61,55 @@ abc[^0-9]
**匹配结果**
1. **abcd**
2. abc1
3. abc2
1. **abcd**
2. abc1
3. abc2
# 四、使用元字符
# 四、使用元字符
## 匹配空白字符
## 匹配空白字符
|  元字符 | 说明  |
| :---: | :---: |
|  [\b] | 回退(删除)一个字符   |
|  \f |  换页符 |
|  \n |  换行符 |
|  \r |  回车符 |
|  \t |  制表符 |
|  \v |  垂直制表符 |
| 元字符 | 说明 |
| :---: | :---: |
| [\b] | 回退(删除)一个字符 |
| \f | 换页符 |
| \n | 换行符 |
| \r | 回车符 |
| \t | 制表符 |
| \v | 垂直制表符 |
\r\n  Windows 中的文本行结束标签 Unix/Linux 则是 \n \r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
\r\n 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n \r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
. 是元字符前提是没有对它们进行转义f  n 也是元字符,但是前提是对它们进行了转义。
. 是元字符前提是没有对它们进行转义f 和 n 也是元字符,但是前提是对它们进行了转义。
## 匹配特定的字符类别
## 匹配特定的字符类别
### 1. 数字元字符
### 1. 数字元字符
|  元字符 | 说明  |
| :---: | :---: |
| \d  | 数字字符,等价于 [0-9]  |
| \D  | 非数字字符,等价于 [^0-9]   |
| 元字符 | 说明 |
| :---: | :---: |
| \d | 数字字符,等价于 [0-9] |
| \D | 非数字字符,等价于 [^0-9] |
### 2. 字母数字元字符
### 2. 字母数字元字符
|  元字符 | 说明  |
| :---: | :---: |
| \w  |  大小写字母,下划线和数字,等价于 [a-zA-Z0-9\_] |
|  \W |  对 \w 取非 |
| 元字符 | 说明 |
| :---: | :---: |
| \w | 大小写字母,下划线和数字,等价于 [a-zA-Z0-9\_] |
| \W | 对 \w 取非 |
### 3. 空白字符元字符
### 3. 空白字符元字符
| 元字符  | 说明  |
| :---: | :---: |
|  \s | 任何一个空白字符,等价于 [\f\n\r\t\v]  |
| \S  |  对 \s 取非  |
| 元字符 | 说明 |
| :---: | :---: |
| \s | 任何一个空白字符,等价于 [\f\n\r\t\v] |
| \S | 对 \s 取非 |
\x 匹配十六进制字符,\0 匹配八进制例如 \x0A 对应 ASCII 字符 10 等价于 \n也就是它会匹配 \n 
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10 ,等价于 \n也就是它会匹配 \n
# 五、重复匹配
# 五、重复匹配
**\+** 匹配 1 个或者多个字符 **\*** 匹配 0 个或者多个**?** 匹配 0 个或者 1 个。
**\+** 匹配 1 个或者多个字符, **\*** 匹配 0 个或者多个,**?** 匹配 0 个或者 1 个。
**应用**
@ -106,22 +121,22 @@ abc[^0-9]
[\w.]+@\w+\.\w+
```
[\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符;
[\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符;
**匹配结果**
**abc.def<span>@</span>qq.com**
为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。
为了可读性,常常把转义的字符放到字符集合 [ ] 中,但是含义是相同的。
```
[\w.]+@\w+\.\w+
[\w.]+@[\w]+[\.][\w]+
```
**{n}** 匹配 n 个字符**{m, n}** 匹配 m~n 个字符**{m,}** 至少匹配 m 个字符;
**{n}** 匹配 n 个字符,**{m, n}** 匹配 m\~n 个字符,**{m,}** 至少匹配 m 个字符;
\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 
\* 和 + 都是贪婪型元字符,会匹配最多的内容,在元字符后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}?
**正则表达式**
@ -129,31 +144,31 @@ abc[^0-9]
a.+c
```
由于 + 是贪婪型的,因此 .+ 会匹配更可能多的内容所以会把整个 abcabcabc 文本都匹配而不是只匹配前面的 abc 文本。用懒惰型可以实现匹配前面的。
由于 + 是贪婪型的,因此 .+ 会匹配更可能多的内容,所以会把整个 abcabcabc 文本都匹配,而不是只匹配前面的 abc 文本。用懒惰型可以实现匹配前面的。
**匹配结果**
**abcabcabc**
# 六、位置匹配
# 六、位置匹配
## 单词边界
## 单词边界
**\b** 可以匹配一个单词的边界,边界是指位于 \w  \W 之间的位置**\B** 匹配一个不是单词边界的位置。
**\b** 可以匹配一个单词的边界,边界是指位于 \w 和 \W 之间的位置;**\B** 匹配一个不是单词边界的位置。
\b 只匹配位置,不匹配字符,因此 \babc\b 匹配出来的结果为 3 个字符。
\b 只匹配位置,不匹配字符,因此 \babc\b 匹配出来的结果为 3 个字符。
## 字符串边界
## 字符串边界
**^** 匹配整个字符串的开头,**$** 匹配结尾。
**^** 匹配整个字符串的开头,**$** 匹配结尾。
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
分行匹配模式multiline换行被当做字符串的边界。
**应用**
匹配代码中以 // 开始的注释行
匹配代码中以 // 开始的注释行
**正则表达式**
@ -161,21 +176,21 @@ a.+c
^\s*\/\/.*$
```
![](index_files/600e9c75-5033-4dad-ae2b-930957db638e.png)
<div align="center"> <img src="../pics//600e9c75-5033-4dad-ae2b-930957db638e.png"/> </div><br>
**匹配结果**
1. public void fun() {
2. &nbsp;&nbsp;&nbsp;&nbsp;    **// 注释 1**
3. &nbsp;&nbsp;&nbsp;&nbsp;    int a = 1;
4. &nbsp;&nbsp;&nbsp;&nbsp;    int b = 2;
5. &nbsp;&nbsp;&nbsp;&nbsp;    **// 注释 2**
6. &nbsp;&nbsp;&nbsp;&nbsp;    int c = a + b;
7. }
1. public void fun() {
2. &nbsp;&nbsp;&nbsp;&nbsp; **// 注释 1**
3. &nbsp;&nbsp;&nbsp;&nbsp; int a = 1;
4. &nbsp;&nbsp;&nbsp;&nbsp; int b = 2;
5. &nbsp;&nbsp;&nbsp;&nbsp; **// 注释 2**
6. &nbsp;&nbsp;&nbsp;&nbsp; int c = a + b;
7. }
# 七、使用子表达式
# 七、使用子表达式
使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。
使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。
子表达式可以嵌套,但是嵌套层次过深会变得很难理解。
@ -189,7 +204,7 @@ a.+c
**ababab**
**|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。
**|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。
**正则表达式**
@ -199,19 +214,19 @@ a.+c
**匹配结果**
1. **1900**
2. **2010**
3. 1020
1. **1900**
2. **2010**
3. 1020
**应用**
匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
匹配 IP 地址。IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
- 一位数字
- 不以 0 开头的两位数字
- 1 开头的三位数
- 2 开头 2 位是 0-4 的三位数
- 25 开头 3 位是 0-5 的三位数
- 一位数字
- 不以 0 开头的两位数字
- 1 开头的三位数
- 2 开头,第 2 位是 0-4 的三位数
- 25 开头,第 3 位是 0-5 的三位数
**正则表达式**
@ -221,21 +236,21 @@ a.+c
**匹配结果**
1. **192.168.0.1**
2. 00.00.00.00
3. 555.555.555.555
1. **192.168.0.1**
2. 00.00.00.00
3. 555.555.555.555
# 八、回溯引用
# 八、回溯引用
回溯引用使用 **\n** 来引用某个子表达式其中 n 代表的是子表达式的序号 1 开始。它和子表达式匹配的内容一致比如子表达式匹配到 abc那么回溯引用部分也需要匹配 abc 
回溯引用使用 **\n** 来引用某个子表达式,其中 n 代表的是子表达式的序号,从 1 开始。它和子表达式匹配的内容一致,比如子表达式匹配到 abc那么回溯引用部分也需要匹配 abc
**应用**
匹配 HTML 中合法的标题元素。
匹配 HTML 中合法的标题元素。
**正则表达式**
\1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。
\1 将回溯引用子表达式 (h[1-6]) 匹配的内容,也就是说必须和子表达式匹配的内容一致。
```
<(h[1-6])>\w*?<\/\1>
@ -243,11 +258,11 @@ a.+c
**匹配结果**
1. **&lt;h1>x&lt;/h1>**
2. **&lt;h2>x&lt;/h2>**
3. &lt;h3>x&lt;/h1>
1. **&lt;h1>x&lt;/h1>**
2. **&lt;h2>x&lt;/h2>**
3. &lt;h3>x&lt;/h1>
## 替换
## 替换
需要用到两个正则表达式。
@ -267,25 +282,25 @@ a.+c
**替换正则表达式**
在第一个子表达式查找的结果加上 () ,然后加一个空格,在第三个和第五个字表达式查找的结果中间加上 - 进行分隔。
在第一个子表达式查找的结果加上 () ,然后加一个空格,在第三个和第五个字表达式查找的结果中间加上 - 进行分隔。
```
($1) $3-$5
($1) $3-$5
```
**结果**
(313) 555-1234
(313) 555-1234
## 大小写转换
## 大小写转换
|  元字符 | 说明  |
| :---: | :---: |
|  \l | 把下个字符转换为小写  |
|   \u| 把下个字符转换为大写  |
|  \L | 把\L 和\E 之间的字符全部转换为小写  |
|  \U | 把\U 和\E 之间的字符全部转换为大写  |
|  \E | 结束\L 或者\U  |
| 元字符 | 说明 |
| :---: | :---: |
| \l | 把下个字符转换为小写 |
| \u| 把下个字符转换为大写 |
| \L | 把\L 和\E 之间的字符全部转换为小写 |
| \U | 把\U 和\E 之间的字符全部转换为大写 |
| \E | 结束\L 或者\U |
**应用**
@ -311,13 +326,13 @@ $1\U$2\E$3
aBCd
# 九、前后查找
# 九、前后查找
前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义(注: javaScript 不支持向后匹配, java 对其支持也不完善)。
前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义(注: javaScript 不支持向后匹配, java 对其支持也不完善)。
**应用**
查找出邮件地址 @ 字符前面的部分。
查找出邮件地址 @ 字符前面的部分。
**正则表达式**
@ -327,19 +342,19 @@ aBCd
**结果**
**abc**@qq.com
**abc** @qq.com
对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。
对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。
# 十、嵌入条件
# 十、嵌入条件
## 回溯引用条件
## 回溯引用条件
条件判断为某个子表达式是否匹配,如果匹配则需要继续匹配条件表达式后面的内容。
**正则表达式**
子表达式 (\\() 匹配一个左括号,其后的 ? 表示匹配 0 个或者 1 个。 ?(1) 为条件当子表达式 1 匹配时条件成立需要执行 \) 匹配,也就是匹配右括号。
子表达式 (\\() 匹配一个左括号,其后的 ? 表示匹配 0 个或者 1 个。 ?(1) 为条件,当子表达式 1 匹配时条件成立,需要执行 \) 匹配,也就是匹配右括号。
```
(\()?abc(?(1)\))
@ -347,17 +362,17 @@ aBCd
**结果**
1. **(abc)**
2. **abc**
3. (abc
1. **(abc)**
2. **abc**
3. (abc
## 前后查找条件
## 前后查找条件
条件为定义的首尾是否匹配,如果匹配,则继续执行后面的匹配。注意,首尾不包含在匹配的内容中。
**正则表达式**
 ?(?=-) 为前向查找条件,只有在以 - 为前向查找的结尾能匹配 \d{5} ,才继续匹配 -\d{4} 
?(?=-) 为前向查找条件,只有在以 - 为前向查找的结尾能匹配 \d{5} ,才继续匹配 -\d{4}
```
\d{5}(?(?=-)-\d{4})
@ -365,10 +380,10 @@ aBCd
**结果**
1. **11111**
2. 22222-
3. **33333-4444**
1. **11111**
2. 22222-
3. **33333-4444**
# 参考资料
# 参考资料
- BenForta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007.
- BenForta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007.

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,235 +1,254 @@
# 一、三大特性
<!-- GFM-TOC -->
* [一、三大特性](#一三大特性)
* [封装](#封装)
* [继承](#继承)
* [多态](#多态)
* [二、设计原则](#二设计原则)
* [S.O.L.I.D](#solid)
* [其他常见原则](#其他常见原则)
* [三、类图](#三类图)
* [泛化关系 (Generalization)](#泛化关系-generalization)
* [实现关系 (Realization)](#实现关系-realization)
* [聚合关系 (Aggregation)](#聚合关系-aggregation)
* [组合关系 (Composition)](#组合关系-composition)
* [关联关系 (Association)](#关联关系-association)
* [依赖关系 (Dependency)](#依赖关系-dependency)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 封装
# 一、三大特性
## 封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
以下 Person 类封装 name、gender、age 等属性外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性而无法获取 age 属性但是 age 属性可以供 work() 方法使用。
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时也可以在不影响客户端代码的情况下进行。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
```java
public class Person {
    private String name;
    private int gender;
    private int age;
public class Person {
private String name;
private int gender;
private int age;
    public String getName() {
        return name;
    }
public String getName() {
return name;
}
    public String getGender() {
        return gender == 0 ? "man" : "woman";
    }
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!");
        }
    }
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 的属性和方法。
继承实现了 **IS-A** 关系,例如 Cat 和 Animal 就是一种 IS-A 关系,因此 Cat 可以继承自 Animal从而获得 Animal 非 private 的属性和方法。
Cat 可以当做 Animal 来使用也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型**
Cat 可以当做 Animal 来使用,也就是说可以使用 Animal 引用 Cat 对象。父类引用指向子类对象称为 **向上转型**
```java
Animal animal = new Cat();
Animal animal = new Cat();
```
继承应该遵循里氏替换原则,子类对象必须能够替换掉所有父类对象。
## 多态
## 多态
多态分为编译时多态和运行时多态。编译时多态主要指方法的重载,运行时多态指程序中定义的对象引用所指向的具体类型在运行期间才确定。
运行时多态有三个条件:
- 继承
- 覆盖(重写)
- 向上转型
- 继承
- 覆盖(重写)
- 向上转型
下面的代码中乐器类Instrument有两个子类Wind  Percussion它们都覆盖了父类的 play() 方法并且在 main() 方法中使用父类 Instrument 来引用 Wind  Percussion 对象。在 Instrument 引用调用 play() 方法时会执行实际引用对象所在类的 play() 方法而不是 Instrument 类的方法。
下面的代码中乐器类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 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 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 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();
        }
    }
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();
}
}
}
```
# 二、设计原则
# 二、设计原则
## S.O.L.I.D
## 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     | 依赖倒置原则 |
| 简写 | 全拼 | 中文翻译 |
| :--: | :--: | :--: |
| 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. 单一责任原则
### 1. 单一责任原则
> 修改一个类的原因应该只有一个。
> 修改一个类的原因应该只有一个。
换句话说就是让一个类只负责一件事,当这个类需要做过多事情的时候,就需要分解这个类。
如果一个类承担的职责过多,就等于把这些职责耦合在了一起,一个职责的变化可能会削弱这个类完成其它职责的能力。
### 2. 开放封闭原则
### 2. 开放封闭原则
> 类应该对扩展开放,对修改关闭。
> 类应该对扩展开放,对修改关闭。
扩展就是添加新功能的意思,因此该原则要求在添加新功能时不需要修改代码。
符合开闭原则最典型的设计模式是装饰者模式,它可以动态地将责任附加到对象上,而不用去修改类的代码。
### 3. 里氏替换原则
### 3. 里氏替换原则
> 子类对象必须能够替换掉所有父类对象。
> 子类对象必须能够替换掉所有父类对象。
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
继承是一种 IS-A 关系,子类需要能够当成父类来使用,并且需要比父类更特殊。
如果不满足这个原则,那么各个子类的行为上就会有很大差异,增加继承体系的复杂度。
### 4. 接口分离原则
### 4. 接口分离原则
> 不应该强迫客户依赖于它们不用的方法。
> 不应该强迫客户依赖于它们不用的方法。
因此使用多个专门的接口比使用单一的总接口要好。
### 5. 依赖倒置原则
### 5. 依赖倒置原则
> 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;</br>抽象不应该依赖于细节,细节应该依赖于抽象。
> 高层模块不应该依赖于低层模块,二者都应该依赖于抽象;</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    | 稳定依赖原则 |
| 简写 | 全拼 | 中文翻译 |
| :--: | :--: | :--: |
|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. 迪米特法则
### 1. 迪米特法则
迪米特法则又叫作最少知识原则Least Knowledge Principle简写 LKP就是说一个对象应当对其他对象有尽可能少的了解不和陌生人说话。
迪米特法则又叫作最少知识原则Least Knowledge Principle简写 LKP就是说一个对象应当对其他对象有尽可能少的了解不和陌生人说话。
### 2. 合成复用原则
### 2. 合成复用原则
尽量使用对象组合,而不是继承来达到复用的目的。
### 3. 共同封闭原则
### 3. 共同封闭原则
一起修改的类,应该组合在一起(同一个包里)。如果必须修改应用程序里的代码,我们希望所有的修改都发生在一个包里(修改关闭),而不是遍布在很多包里。
### 4. 稳定抽象原则
### 4. 稳定抽象原则
最稳定的包应该是最抽象的包,不稳定的包应该是具体的包,即包的抽象程度跟它的稳定性成正比。
### 5. 稳定依赖原则
### 5. 稳定依赖原则
包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。
# 三、类图
# 三、类图
## 泛化关系 (Generalization)
## 泛化关系 (Generalization)
用来描述继承关系,在 Java 中使用 extends 关键字。
用来描述继承关系,在 Java 中使用 extends 关键字。
![](index_files/5341d726-ffde-4d2a-a000-46597bcc9c5a.png)
<div align="center"> <img src="../pics//5341d726-ffde-4d2a-a000-46597bcc9c5a.png"/> </div><br>
## 实现关系 (Realization)
## 实现关系 (Realization)
用来实现一个接口,在 Java 中使用 implement 关键字。
用来实现一个接口,在 Java 中使用 implement 关键字。
![](index_files/123bdf81-1ef5-48a9-a08c-2db97057b4d2.png)
<div align="center"> <img src="../pics//123bdf81-1ef5-48a9-a08c-2db97057b4d2.png"/> </div><br>
## 聚合关系 (Aggregation)
## 聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
![](index_files/1be8b4b0-cc7a-44d7-9c77-85be37b76f7d.png)
<div align="center"> <img src="../pics//1be8b4b0-cc7a-44d7-9c77-85be37b76f7d.png"/> </div><br>
## 组合关系 (Composition)
## 组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
![](index_files/eb4a7007-d437-4740-865d-672973effe25.png)
<div align="center"> <img src="../pics//eb4a7007-d437-4740-865d-672973effe25.png"/> </div><br>
## 关联关系 (Association)
## 关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1  1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
![](index_files/518f16f2-a9f7-499a-98e1-f1dbb37b5a9a.png)
<div align="center"> <img src="../pics//518f16f2-a9f7-499a-98e1-f1dbb37b5a9a.png"/> </div><br>
## 依赖关系 (Dependency)
## 依赖关系 (Dependency)
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类中的(某中方法的)局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息从而影响 B 类发生变化;
- A 类是 B 类中的(某中方法的)局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化;
![](index_files/c7d4956c-9988-4a10-a704-28fdae7f3d28.png)
<div align="center"> <img src="../pics//c7d4956c-9988-4a10-a704-28fdae7f3d28.png"/> </div><br>
# 参考资料
# 参考资料
- 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)
- 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)

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB