This commit is contained in:
CyC2018 2018-08-07 12:38:40 +08:00
commit f591af33a0
18 changed files with 6988 additions and 2 deletions

View File

@ -6,7 +6,7 @@
<div align="center">
<img src="other/LogoMakr_0zpEzN.png" width="150px">
<br>
<a href="other/Group.md"> <img src="https://img.shields.io/badge/>-wechat-4ab8a1.svg"></a> <a href="https://legacy.gitbook.com/book/cyc2018/interview-notebook/details"> <img src="https://img.shields.io/badge/_-gitbook-4ab8a1.svg"></a>
<a href="other/Group.md"> <img src="https://img.shields.io/badge/>-group-4ab8a1.svg"></a> <a href="https://legacy.gitbook.com/book/cyc2018/interview-notebook/details"> <img src="https://img.shields.io/badge/_-gitbook-4ab8a1.svg"></a>
</div>
<!-- [![](https://img.shields.io/badge/>-gitter-blue.svg)](https://gitter.im/CyC2018-Interview-Notebook/Lobby?utm_source=share-link&utm_medium=link&utm_campaign=share-link) [![](https://img.shields.io/badge/_-gitbook-4ab8a1.svg)](https://legacy.gitbook.com/book/cyc2018/interview-notebook/details) -->
@ -89,7 +89,7 @@ Leetcode 上数据库题目的解题记录。
> [Java 容器](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20容器.md)
源码分析ArrayListVector、CopyOnWriteArrayList、LinkedList、HashMap、ConcurrentHashMap、LinkedHashMap、WeekHashMap。
源码分析ArrayListVector、CopyOnWriteArrayList、LinkedList、HashMap、ConcurrentHashMap、LinkedHashMap、WeekHashMap。
> [Java 并发](https://github.com/CyC2018/InnterviewNotes/blob/master/notes/Java%20并发.md)

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -2,3 +2,568 @@
<!-- GFM-TOC -->
<<<<<<< HEAD
=======
# 一、概述
Redis 是速度非常快的非关系型NoSQL内存键值数据库可以存储键和五种不同类型的值之间的映射。
键的类型只能为字符串,值支持的五种类型数据类型为:字符串、列表、集合、有序集合、散列表。
Redis 支持很多特性,例如将内存中的数据持久化到硬盘中,使用复制来扩展读性能,使用分片来扩展写性能。
# 二、数据类型
| 数据类型 | 可以存储的值 | 操作 |
| :--: | :--: | :--: |
| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作</br> 对整数和浮点数执行自增或者自减操作 |
| LIST | 列表 | 从两端压入或者弹出元素</br> 读取单个或者多个元素</br> 进行修剪,只保留一个范围内的元素 |
| SET | 无序集合 | 添加、获取、移除单个元素</br> 检查一个元素是否存在于集合中</br> 计算交集、并集、差集</br> 从集合里面随机获取元素 |
| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对</br> 获取所有键值对</br> 检查某个键是否存在|
| ZSET | 有序集合 | 添加、获取、删除元素</br> 根据分值范围或者成员来获取元素</br> 计算一个键的排名 |
> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/)
## STRING
<div align="center"> <img src="../pics//6019b2db-bc3e-4408-b6d8-96025f4481d6.png" width="400"/> </div><br>
```html
> set hello world
OK
> get hello
"world"
> del hello
(integer) 1
> get hello
(nil)
```
## LIST
<div align="center"> <img src="../pics//fb327611-7e2b-4f2f-9f5b-38592d408f07.png" width="400"/> </div><br>
```html
> rpush list-key item
(integer) 1
> rpush list-key item2
(integer) 2
> rpush list-key item
(integer) 3
> lrange list-key 0 -1
1) "item"
2) "item2"
3) "item"
> lindex list-key 1
"item2"
> lpop list-key
"item"
> lrange list-key 0 -1
1) "item2"
2) "item"
```
## SET
<div align="center"> <img src="../pics//cd5fbcff-3f35-43a6-8ffa-082a93ce0f0e.png" width="400"/> </div><br>
```html
> sadd set-key item
(integer) 1
> sadd set-key item2
(integer) 1
> sadd set-key item3
(integer) 1
> sadd set-key item
(integer) 0
> smembers set-key
1) "item"
2) "item2"
3) "item3"
> sismember set-key item4
(integer) 0
> sismember set-key item
(integer) 1
> srem set-key item2
(integer) 1
> srem set-key item2
(integer) 0
> smembers set-key
1) "item"
2) "item3"
```
## HASH
<div align="center"> <img src="../pics//7bd202a7-93d4-4f3a-a878-af68ae25539a.png" width="400"/> </div><br>
```html
> hset hash-key sub-key1 value1
(integer) 1
> hset hash-key sub-key2 value2
(integer) 1
> hset hash-key sub-key1 value1
(integer) 0
> hgetall hash-key
1) "sub-key1"
2) "value1"
3) "sub-key2"
4) "value2"
> hdel hash-key sub-key2
(integer) 1
> hdel hash-key sub-key2
(integer) 0
> hget hash-key sub-key1
"value1"
> hgetall hash-key
1) "sub-key1"
2) "value1"
```
## ZSET
<div align="center"> <img src="../pics//1202b2d6-9469-4251-bd47-ca6034fb6116.png" width="400"/> </div><br>
```html
> zadd zset-key 728 member1
(integer) 1
> zadd zset-key 982 member0
(integer) 1
> zadd zset-key 982 member0
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member1"
2) "728"
3) "member0"
4) "982"
> zrangebyscore zset-key 0 800 withscores
1) "member1"
2) "728"
> zrem zset-key member1
(integer) 1
> zrem zset-key member1
(integer) 0
> zrange zset-key 0 -1 withscores
1) "member0"
2) "982"
```
# 三、数据结构
## 字典
dictht 是一个散列表结构,使用拉链法保存哈希冲突的 dictEntry。
```c
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
```
```c
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
```
Redis 的字典 dict 中包含两个哈希表 dictht这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。
```c
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
```
rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。
渐进式 rehash 通过记录 dict 的 rehashidx 完成,它从 0 开始,然后每执行一次 rehash 都会递增。例如在一次 rehash 中,要把 dict[0] rehash 到 dict[1],这一次会把 dict[0] 上 table[rehashidx] 的键值对 rehash 到 dict[1] 上dict[0] 的 table[rehashidx] 指向 null并令 rehashidx++。
在 rehash 期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式 rehash。
采用渐进式 rehash 会导致字典中的数据分散在两个 dictht 上,因此对字典的操作也需要到对应的 dictht 去执行。
```c
/* Performs N steps of incremental rehashing. Returns 1 if there are still
* keys to move from the old to the new hash table, otherwise 0 is returned.
*
* Note that a rehashing step consists in moving a bucket (that may have more
* than one key as we use chaining) from the old to the new hash table, however
* since part of the hash table may be composed of empty spaces, it is not
* guaranteed that this function will rehash even a single bucket, since it
* will visit at max N*10 empty buckets in total, otherwise the amount of
* work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
int empty_visits = n * 10; /* Max number of empty buckets to visit. */
if (!dictIsRehashing(d)) return 0;
while (n-- && d->ht[0].used != 0) {
dictEntry *de, *nextde;
/* Note that rehashidx can't overflow as we are sure there are more
* elements because ht[0].used != 0 */
assert(d->ht[0].size > (unsigned long) d->rehashidx);
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}
de = d->ht[0].table[d->rehashidx];
/* Move all the keys in this bucket from the old to the new hash HT */
while (de) {
uint64_t h;
nextde = de->next;
/* Get the index in the new hash table */
h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}
/* Check if we already rehashed the whole table... */
if (d->ht[0].used == 0) {
zfree(d->ht[0].table);
d->ht[0] = d->ht[1];
_dictReset(&d->ht[1]);
d->rehashidx = -1;
return 0;
}
/* More to rehash... */
return 1;
}
```
## 跳跃表
是有序集合的底层实现之一。
跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。
<div align="center"> <img src="../pics//beba612e-dc5b-4fc2-869d-0b23408ac90a.png"/> </div><br>
在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。下图演示了查找 22 的过程。
<div align="center"> <img src="../pics//0ea37ee2-c224-4c79-b895-e131c6805c40.png"/> </div><br>
与红黑树等平衡树相比,跳跃表具有以下优点:
- 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
- 更容易实现;
- 支持无锁操作。
# 四、使用场景
## 计数器
可以对 String 进行自增自减运算,从而实现计数器功能。
Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
## 缓存
将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
## 查找表
例如 DNS 记录就很适合使用 Redis 进行存储。
查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源。
## 消息队列
List 是一个双向链表,可以通过 lpop 和 lpush 写入和读取消息。
不过最好使用 Kafka、RabbitMQ 等消息中间件。
## 会话缓存
在分布式场景下具有多个应用服务器,可以使用 Redis 来统一存储这些应用服务器的会话信息。
当应用服务器不再存储用户的会话信息,也就不再具有状态,一个用户可以请求任意一个应用服务器。
## 分布式锁实现
在分布式场景下,无法使用单机环境下的锁来对多个节点上的进程进行同步。
可以使用 Reids 自带的 SETNX 命令实现分布式锁,除此之外,还可以使用官方提供的 RedLock 分布式锁实现。
## 其它
Set 可以实现交集、并集等操作,从而实现共同好友等功能。
ZSet 可以实现有序性操作,从而实现排行榜等功能。
# 五、Redis 与 Memcached
两者都是非关系型内存键值数据库,主要有以下不同:
## 数据类型
Memcached 仅支持字符串类型,而 Redis 支持五种不同的数据类型,可以更灵活地解决问题。
## 数据持久化
Redis 支持两种持久化策略RDB 快照和 AOF 日志,而 Memcached 不支持持久化。
## 分布式
Memcached 不支持分布式,只能通过在客户端使用一致性哈希来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。
Redis Cluster 实现了分布式的支持。
## 内存管理机制
- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘,而 Memcached 的数据则会一直在内存中。
- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。
# 六、键的过期时间
Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。
对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。
# 七、数据淘汰策略
可以设置内存最大使用量,当内存使用量超出时,会施行数据淘汰策略。
Reids 具体有 6 种淘汰策略:
| 策略 | 描述 |
| :--: | :--: |
| volatile-lru | 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰 |
| volatile-ttl | 从已设置过期时间的数据集中挑选将要过期的数据淘汰 |
|volatile-random | 从已设置过期时间的数据集中任意选择数据淘汰 |
| allkeys-lru | 从所有数据集中挑选最近最少使用的数据淘汰 |
| allkeys-random | 从所有数据集中任意选择数据进行淘汰 |
| noeviction | 禁止驱逐数据 |
作为内存数据库出于对性能和内存消耗的考虑Redis 的淘汰算法实际实现上并非针对所有 key而是抽样一小部分并且从中选出被淘汰的 key。
使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。
Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。
# 八、持久化
Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。
## RDB 持久化
将某个时间点的所有数据都存放到硬盘上。
可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。
如果系统发生故障,将会丢失最后一次创建快照之后的数据。
如果数据量很大,保存快照的时间会很长。
## AOF 持久化
将写命令添加到 AOF 文件Append Only File的末尾。
使用 AOF 持久化需要设置同步选项,从而确保写命令什么时候会同步到磁盘文件上。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:
| 选项 | 同步频率 |
| :--: | :--: |
| always | 每个写命令都同步 |
| everysec | 每秒同步一次 |
| no | 让操作系统来决定何时同步 |
- always 选项会严重减低服务器的性能;
- everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
- no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。
随着服务器写请求的增多AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。
# 九、事务
一个事务包含了多个命令,服务器在执行事务期间,不会改去执行其它客户端的命令请求。
事务中的多个命令被一次性发送给服务器,而不是一条一条发送,这种方式被称为流水线,它可以减少客户端与服务器之间的网络通信次数从而提升性能。
Redis 最简单的事务实现方式是使用 MULTI 和 EXEC 命令将事务操作包围起来。
# 十、事件
Redis 服务器是一个事件驱动程序。
## 文件事件
服务器通过套接字与客户端或者其它服务器进行通信,文件事件就是对套接字操作的抽象。
Redis 基于 Reactor 模式开发了自己的网络事件处理器,使用 I/O 多路复用程序来同时监听多个套接字,并将到达的事件传送给文件事件分派器,分派器会根据套接字产生的事件类型调用相应的事件处理器。
<div align="center"> <img src="../pics//9ea86eb5-000a-4281-b948-7b567bd6f1d8.png"/> </div><br>
## 时间事件
服务器有一些操作需要在给定的时间点执行,时间事件是对这类定时操作的抽象。
时间事件又分为:
- 定时事件:是让一段程序在指定的时间之内执行一次;
- 周期性事件:是让一段程序每隔指定时间就执行一次。
Redis 将所有时间事件都放在一个无序链表中,通过遍历整个链表查找出已到达的时间事件,并调用相应的事件处理器。
## 事件的调度与执行
服务器需要不断监听文件事件的套接字才能得到待处理的文件事件,但是不能一直监听,否则时间事件无法在规定的时间内执行,因此监听时间应该根据距离现在最近的时间事件来决定。
事件调度与执行由 aeProcessEvents 函数负责,伪代码如下:
```python
def aeProcessEvents():
# 获取到达时间离当前时间最接近的时间事件
time_event = aeSearchNearestTimer()
# 计算最接近的时间事件距离到达还有多少毫秒
remaind_ms = time_event.when - unix_ts_now()
# 如果事件已到达,那么 remaind_ms 的值可能为负数,将它设为 0
if remaind_ms < 0:
remaind_ms = 0
# 根据 remaind_ms 的值,创建 timeval
timeval = create_timeval_with_ms(remaind_ms)
# 阻塞并等待文件事件产生,最大阻塞时间由传入的 timeval 决定
aeApiPoll(timeval)
# 处理所有已产生的文件事件
procesFileEvents()
# 处理所有已到达的时间事件
processTimeEvents()
```
将 aeProcessEvents 函数置于一个循环里面,加上初始化和清理函数,就构成了 Redis 服务器的主函数,伪代码如下:
```python
def main():
# 初始化服务器
init_server()
# 一直处理事件,直到服务器关闭为止
while server_is_not_shutdown():
aeProcessEvents()
# 服务器关闭,执行清理操作
clean_server()
```
从事件处理的角度来看,服务器运行流程如下:
<div align="center"> <img src="../pics//c0a9fa91-da2e-4892-8c9f-80206a6f7047.png" width="400"/> </div><br>
# 十一、复制
通过使用 slaveof host port 命令来让一个服务器成为另一个服务器的从服务器。
一个从服务器只能有一个主服务器,并且不支持主主复制。
## 连接过程
1. 主服务器创建快照文件,发送给从服务器,并在发送期间使用缓冲区记录执行的写命令。快照文件发送完毕之后,开始向从服务器发送存储在缓冲区中的写命令;
2. 从服务器丢弃所有旧数据,载入主服务器发来的快照文件,之后从服务器开始接受主服务器发来的写命令;
3. 主服务器每执行一次写命令,就向从服务器发送相同的写命令。
## 主从链
随着负载不断上升,主服务器可能无法很快地更新所有从服务器,或者重新连接和重新同步从服务器将导致系统超载。为了解决这个问题,可以创建一个中间层来分担主服务器的复制工作。中间层的服务器是最上层服务器的从服务器,又是最下层服务器的主服务器。
<div align="center"> <img src="../pics//395a9e83-b1a1-4a1d-b170-d081e7bb5bab.png" width="600"/> </div><br>
# 十二、Sentinel
Sentinel哨兵可以监听主服务器并在主服务器进入下线状态时自动从从服务器中选举出新的主服务器。
# 十三、分片
分片是将数据划分为多个部分的方法,可以将数据存储到多台机器里面,这种方法在解决某些问题时可以获得线性级别的性能提升。
假设有 4 个 Reids 实例 R0R1R2R3还有很多表示用户的键 user:1user:2... 等等,有不同的方式来选择一个指定的键存储在哪个实例中。
- 最简单的方式是范围分片,例如用户 id 从 0\~1000 的存储到实例 R0 中,用户 id 从 1001\~2000 的存储到实例 R1 中,等等。但是这样需要维护一张映射范围表,维护操作代价很高。
- 还有一种方式是哈希分片,使用 CRC32 哈希函数将键转换为一个数字,再对实例数量求模就能知道应该存储的实例。
根据执行分片的位置,可以分为三种分片方式:
- 客户端分片:客户端使用一致性哈希等算法决定键应当分布到哪个节点。
- 代理分片:将客户端请求发送到代理上,由代理转发请求到正确的节点上。
- 服务器分片Redis Cluster。
# 十四、一个简单的论坛系统分析
该论坛系统功能如下:
- 可以发布文章;
- 可以对文章进行点赞;
- 在首页可以按文章的发布时间或者文章的点赞数进行排序显示。
## 文章信息
文章包括标题、作者、赞数等信息,在关系型数据库中很容易构建一张表来存储这些信息,在 Redis 中可以使用 HASH 来存储每种信息以及其对应的值的映射。
Redis 没有关系型数据库中的表这一概念来将同种类型的数据存放在一起,而是使用命名空间的方式来实现这一功能。键名的前面部分存储命名空间,后面部分的内容存储 ID通常使用 : 来进行分隔。例如下面的 HASH 的键名为 article:92617其中 article 为命名空间ID 为 92617。
<div align="center"> <img src="../pics//7c54de21-e2ff-402e-bc42-4037de1c1592.png" width="400"/> </div><br>
## 点赞功能
当有用户为一篇文章点赞时,除了要对该文章的 votes 字段进行加 1 操作,还必须记录该用户已经对该文章进行了点赞,防止用户点赞次数超过 1。可以建立文章的已投票用户集合来进行记录。
为了节约内存,规定一篇文章发布满一周之后,就不能再对它进行投票,而文章的已投票集合也会被删除,可以为文章的已投票集合设置一个一周的过期时间就能实现这个规定。
<div align="center"> <img src="../pics//485fdf34-ccf8-4185-97c6-17374ee719a0.png" width="400"/> </div><br>
## 对文章进行排序
为了按发布时间和点赞数进行排序,可以建立一个文章发布时间的有序集合和一个文章点赞数的有序集合。(下图中的 score 就是这里所说的点赞数;下面所示的有序集合分值并不直接是时间和点赞数,而是根据时间和点赞数间接计算出来的)
<div align="center"> <img src="../pics//f7d170a3-e446-4a64-ac2d-cb95028f81a8.png" width="800"/> </div><br>
# 参考资料
- Carlson J L. Redis in Action[J]. Media.johnwiley.com.au, 2013.
- [黄健宏. Redis 设计与实现 [M]. 机械工业出版社, 2014.](http://redisbook.com/index.html)
- [REDIS IN ACTION](https://redislabs.com/ebook/foreword/)
- [Skip Lists: Done Right](http://ticki.github.io/blog/skip-lists-done-right/)
- [论述 Redis 和 Memcached 的差异](http://www.cnblogs.com/loveincode/p/7411911.html)
- [Redis 3.0 中文版- 分片](http://wiki.jikexueyuan.com/project/redis-guide)
- [Redis 应用场景](http://www.scienjus.com/redis-use-case/)
- [Observer vs Pub-Sub](http://developers-club.com/posts/270339/)
- [Using Redis as an LRU cache](https://redis.io/topics/lru-cache)
>>>>>>> 8c67483fe9ac599aa6f6358fb32a541d7e279666

View File

@ -1,4 +1,773 @@
<!-- GFM-TOC -->
<<<<<<< HEAD
<!-- GFM-TOC -->
=======
* [一、基础](#一基础)
* [二、创建表](#二创建表)
* [三、修改表](#三修改表)
* [四、插入](#四插入)
* [五、更新](#五更新)
* [六、删除](#六删除)
* [七、查询](#七查询)
* [八、排序](#八排序)
* [九、过滤](#九过滤)
* [十、通配符](#十通配符)
* [十一、计算字段](#十一计算字段)
* [十二、函数](#十二函数)
* [十三、分组](#十三分组)
* [十四、子查询](#十四子查询)
* [十五、连接](#十五连接)
* [十六、组合查询](#十六组合查询)
* [十七、视图](#十七视图)
* [十八、存储过程](#十八存储过程)
* [十九、游标](#十九游标)
* [二十、触发器](#二十触发器)
* [二十一、事务管理](#二十一事务管理)
* [二十二、字符集](#二十二字符集)
* [二十三、权限管理](#二十三权限管理)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、基础
模式定义了数据如何存储、存储什么样的数据以及数据如何分解等信息,数据库和表都有模式。
主键的值不允许修改,也不允许复用(不能使用已经删除的主键值赋给新数据行的主键)。
SQLStructured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。
SQL 语句不区分大小写,但是数据库表名、列名和值是否区分依赖于具体的 DBMS 以及配置。
SQL 支持以下三种注释:
```sql
# 注释
SELECT *
FROM mytable; -- 注释
/* 注释1
注释2 */
```
数据库创建与使用:
```sql
CREATE DATABASE test;
USE test;
```
# 二、创建表
```sql
CREATE TABLE mytable (
id INT NOT NULL AUTO_INCREMENT,
col1 INT NOT NULL DEFAULT 1,
col2 VARCHAR(45) NULL,
col3 DATE NULL,
PRIMARY KEY (`id`));
```
# 三、修改表
添加列
```sql
ALTER TABLE mytable
ADD col CHAR(20);
```
删除列
```sql
ALTER TABLE mytable
DROP COLUMN col;
```
删除表
```sql
DROP TABLE mytable;
```
# 四、插入
普通插入
```sql
INSERT INTO mytable(col1, col2)
VALUES(val1, val2);
```
插入检索出来的数据
```sql
INSERT INTO mytable1(col1, col2)
SELECT col1, col2
FROM mytable2;
```
将一个表的内容插入到一个新表
```sql
CREATE TABLE newtable AS
SELECT * FROM mytable;
```
# 五、更新
```sql
UPDATE mytable
SET col = val
WHERE id = 1;
```
# 六、删除
```sql
DELETE FROM mytable
WHERE id = 1;
```
**TRUNCATE TABLE** 可以清空表,也就是删除所有行。
```sql
TRUNCATE TABLE mytable;
```
使用更新和删除操作时一定要用 WHERE 子句,不然会把整张表的数据都破坏。可以先用 SELECT 语句进行测试,防止错误删除。
# 七、查询
## DISTINCT
相同值只会出现一次。它作用于所有列,也就是说所有列的值都相同才算相同。
```sql
SELECT DISTINCT col1, col2
FROM mytable;
```
## LIMIT
限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。
返回前 5 行:
```sql
SELECT *
FROM mytable
LIMIT 5;
```
```sql
SELECT *
FROM mytable
LIMIT 0, 5;
```
返回第 3 \~ 5 行:
```sql
SELECT *
FROM mytable
LIMIT 2, 3;
```
# 八、排序
- **ASC** :升序(默认)
- **DESC** :降序
可以按多个列进行排序,并且为每个列指定不同的排序方式:
```sql
SELECT *
FROM mytable
ORDER BY col1 DESC, col2 ASC;
```
# 九、过滤
不进行过滤的数据非常大,导致通过网络传输了多余的数据,从而浪费了网络带宽。因此尽量使用 SQL 语句来过滤不必要的数据,而不是传输所有的数据到客户端中然后由客户端进行过滤。
```sql
SELECT *
FROM mytable
WHERE col IS NULL;
```
下表显示了 WHERE 子句可用的操作符
| 操作符 | 说明 |
| :---: | :---: |
| = | 等于 |
| &lt; | 小于 |
| &gt; | 大于 |
| &lt;&gt; != | 不等于 |
| &lt;= !&gt; | 小于等于 |
| &gt;= !&lt; | 大于等于 |
| BETWEEN | 在两个值之间 |
| IS NULL | 为 NULL 值 |
应该注意到NULL 与 0、空字符串都不同。
**AND 和 OR** 用于连接多个过滤条件。优先处理 AND当一个过滤表达式涉及到多个 AND 和 OR 时,可以使用 () 来决定优先级,使得优先级关系更清晰。
**IN** 操作符用于匹配一组值,其后也可以接一个 SELECT 子句,从而匹配子查询得到的一组值。
**NOT** 操作符用于否定一个条件。
# 十、通配符
通配符也是用在过滤语句中,但它只能用于文本字段。
- **%** 匹配 >=0 个任意字符;
- **\_** 匹配 ==1 个任意字符;
- **[ ]** 可以匹配集合内的字符,例如 [ab] 将匹配字符 a 或者 b。用脱字符 ^ 可以对其进行否定,也就是不匹配集合内的字符。
使用 Like 来进行通配符匹配。
```sql
SELECT *
FROM mytable
WHERE col LIKE '[^AB]%'; -- 不以 A 和 B 开头的任意文本
```
不要滥用通配符,通配符位于开头处匹配会非常慢。
# 十一、计算字段
在数据库服务器上完成数据的转换和格式化的工作往往比客户端上快得多,并且转换和格式化后的数据量更少的话可以减少网络通信量。
计算字段通常需要使用 **AS** 来取别名,否则输出的时候字段名为计算表达式。
```sql
SELECT col1 * col2 AS alias
FROM mytable;
```
**CONCAT()** 用于连接两个字段。许多数据库会使用空格把一个值填充为列宽,因此连接的结果会出现一些不必要的空格,使用 **TRIM()** 可以去除首尾空格。
```sql
SELECT CONCAT(TRIM(col1), '(', TRIM(col2), ')') AS concat_col
FROM mytable;
```
# 十二、函数
各个 DBMS 的函数都是不相同的,因此不可移植,以下主要是 MySQL 的函数。
## 汇总
|函 数 |说 明|
| :---: | :---: |
| AVG() | 返回某列的平均值 |
| COUNT() | 返回某列的行数 |
| MAX() | 返回某列的最大值 |
| MIN() | 返回某列的最小值 |
| SUM() |返回某列值之和 |
AVG() 会忽略 NULL 行。
使用 DISTINCT 可以让汇总函数值汇总不同的值。
```sql
SELECT AVG(DISTINCT col1) AS avg_col
FROM mytable;
```
## 文本处理
| 函数 | 说明 |
| :---: | :---: |
| LEFT() | 左边的字符 |
| RIGHT() | 右边的字符 |
| LOWER() | 转换为小写字符 |
| UPPER() | 转换为大写字符 |
| LTRIM() | 去除左边的空格 |
| RTRIM() | 去除右边的空格 |
| LENGTH() | 长度 |
| SOUNDEX() | 转换为语音值 |
其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。
```sql
SELECT *
FROM mytable
WHERE SOUNDEX(col1) = SOUNDEX('apple')
```
## 日期和时间处理
- 日期格式YYYY-MM-DD
- 时间格式HH:MM:SS
|函 数 | 说 明|
| :---: | :---: |
| AddDate() | 增加一个日期(天、周等)|
| AddTime() | 增加一个时间(时、分等)|
| CurDate() | 返回当前日期 |
| CurTime() | 返回当前时间 |
| Date() |返回日期时间的日期部分|
| DateDiff() |计算两个日期之差|
| Date_Add() |高度灵活的日期运算函数|
| Date_Format() |返回一个格式化的日期或时间串|
| Day()| 返回一个日期的天数部分|
| DayOfWeek() |对于一个日期,返回对应的星期几|
| Hour() |返回一个时间的小时部分|
| Minute() |返回一个时间的分钟部分|
| Month() |返回一个日期的月份部分|
| Now() |返回当前日期和时间|
| Second() |返回一个时间的秒部分|
| Time() |返回一个日期时间的时间部分|
| Year() |返回一个日期的年份部分|
```sql
mysql> SELECT NOW();
```
```
2018-4-14 20:25:11
```
## 数值处理
| 函数 | 说明 |
| :---: | :---: |
| SIN() | 正弦 |
| COS() | 余弦 |
| TAN() | 正切 |
| ABS() | 绝对值 |
| SQRT() | 平方根 |
| MOD() | 余数 |
| EXP() | 指数 |
| PI() | 圆周率 |
| RAND() | 随机数 |
# 十三、分组
分组就是把具有相同的数据值的行放在同一组中。
可以对同一分组数据使用汇总函数进行处理,例如求分组数据的平均值等。
指定的分组字段除了能按该字段进行分组,也会自动按该字段进行排序。
```sql
SELECT col, COUNT(*) AS num
FROM mytable
GROUP BY col;
```
GROUP BY 自动按分组字段进行排序ORDER BY 也可以按汇总字段来进行排序。
```sql
SELECT col, COUNT(*) AS num
FROM mytable
GROUP BY col
ORDER BY num;
```
WHERE 过滤行HAVING 过滤分组,行过滤应当先于分组过滤。
```sql
SELECT col, COUNT(*) AS num
FROM mytable
WHERE col > 2
GROUP BY col
HAVING num >= 2;
```
分组规定:
- GROUP BY 子句出现在 WHERE 子句之后ORDER BY 子句之前;
- 除了汇总字段外SELECT 语句中的每一字段都必须在 GROUP BY 子句中给出;
- NULL 的行会单独分为一组;
- 大多数 SQL 实现不支持 GROUP BY 列具有可变长度的数据类型。
# 十四、子查询
子查询中只能返回一个字段的数据。
可以将子查询的结果作为 WHRER 语句的过滤条件:
```sql
SELECT *
FROM mytable1
WHERE col1 IN (SELECT col2
FROM mytable2);
```
下面的语句可以检索出客户的订单数量,子查询语句会对第一个查询检索出的每个客户执行一次:
```sql
SELECT cust_name, (SELECT COUNT(*)
FROM Orders
WHERE Orders.cust_id = Customers.cust_id)
AS orders_num
FROM Customers
ORDER BY cust_name;
```
# 十五、连接
连接用于连接多个表,使用 JOIN 关键字,并且条件语句使用 ON 而不是 WHERE。
连接可以替换子查询,并且比子查询的效率一般会更快。
可以用 AS 给列名、计算字段和表名取别名,给表名取别名是为了简化 SQL 语句以及连接相同表。
## 内连接
内连接又称等值连接,使用 INNER JOIN 关键字。
```sql
SELECT A.value, B.value
FROM tablea AS A INNER JOIN tableb AS B
ON A.key = B.key;
```
可以不明确使用 INNER JOIN而使用普通查询并在 WHERE 中将两个表中要连接的列用等值方法连接起来。
```sql
SELECT A.value, B.value
FROM tablea AS A, tableb AS B
WHERE A.key = B.key;
```
在没有条件语句的情况下返回笛卡尔积。
## 自连接
自连接可以看成内连接的一种,只是连接的表是自身而已。
一张员工表,包含员工姓名和员工所属部门,要找出与 Jim 处在同一部门的所有员工姓名。
子查询版本
```sql
SELECT name
FROM employee
WHERE department = (
SELECT department
FROM employee
WHERE name = "Jim");
```
自连接版本
```sql
SELECT e1.name
FROM employee AS e1 INNER JOIN employee AS e2
ON e1.department = e2.department
AND e2.name = "Jim";
```
## 自然连接
自然连接是把同名列通过等值测试连接起来的,同名列可以有多个。
内连接和自然连接的区别:内连接提供连接的列,而自然连接自动连接所有同名列。
```sql
SELECT A.value, B.value
FROM tablea AS A NATURAL JOIN tableb AS B;
```
## 外连接
外连接保留了没有关联的那些行。分为左外连接,右外连接以及全外连接,左外连接就是保留左表没有关联的行。
检索所有顾客的订单信息,包括还没有订单信息的顾客。
```sql
SELECT Customers.cust_id, Orders.order_num
FROM Customers LEFT OUTER JOIN Orders
ON Customers.cust_id = Orders.cust_id;
```
customers 表:
| cust_id | cust_name |
| :---: | :---: |
| 1 | a |
| 2 | b |
| 3 | c |
orders 表:
| order_id | cust_id |
| :---: | :---: |
|1 | 1 |
|2 | 1 |
|3 | 3 |
|4 | 3 |
结果:
| cust_id | cust_name | order_id |
| :---: | :---: | :---: |
| 1 | a | 1 |
| 1 | a | 2 |
| 3 | c | 3 |
| 3 | c | 4 |
| 2 | b | Null |
# 十六、组合查询
使用 **UNION** 来组合两个查询,如果第一个查询返回 M 行,第二个查询返回 N 行,那么组合查询的结果一般为 M+N 行。
每个查询必须包含相同的列、表达式和聚集函数。
默认会去除相同行,如果需要保留相同行,使用 UNION ALL。
只能包含一个 ORDER BY 子句,并且必须位于语句的最后。
```sql
SELECT col
FROM mytable
WHERE col = 1
UNION
SELECT col
FROM mytable
WHERE col =2;
```
# 十七、视图
视图是虚拟的表,本身不包含数据,也就不能对其进行索引操作。
对视图的操作和对普通表的操作一样。
视图具有如下好处:
- 简化复杂的 SQL 操作,比如复杂的连接;
- 只使用实际表的一部分数据;
- 通过只给用户访问视图的权限,保证数据的安全性;
- 更改数据格式和表示。
```sql
CREATE VIEW myview AS
SELECT Concat(col1, col2) AS concat_col, col3*col4 AS compute_col
FROM mytable
WHERE col5 = val;
```
# 十八、存储过程
存储过程可以看成是对一系列 SQL 操作的批处理。
使用存储过程的好处:
- 代码封装,保证了一定的安全性;
- 代码复用;
- 由于是预先编译,因此具有很高的性能。
命令行中创建存储过程需要自定义分隔符,因为命令行是以 ; 为结束符,而存储过程中也包含了分号,因此会错误把这部分分号当成是结束符,造成语法错误。
包含 in、out 和 inout 三种参数。
给变量赋值都需要用 select into 语句。
每次只能给一个变量赋值,不支持集合的操作。
```sql
delimiter //
create procedure myprocedure( out ret int )
begin
declare y int;
select sum(col1)
from mytable
into y;
select y*y into ret;
end //
delimiter ;
```
```sql
call myprocedure(@ret);
select @ret;
```
# 十九、游标
在存储过程中使用游标可以对一个结果集进行移动遍历。
游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。
使用游标的四个步骤:
1. 声明游标,这个过程没有实际检索出数据;
2. 打开游标;
3. 取出数据;
4. 关闭游标;
```sql
delimiter //
create procedure myprocedure(out ret int)
begin
declare done boolean default 0;
declare mycursor cursor for
select col1 from mytable;
# 定义了一个 continue handler当 sqlstate '02000' 这个条件出现时,会执行 set done = 1
declare continue handler for sqlstate '02000' set done = 1;
open mycursor;
repeat
fetch mycursor into ret;
select ret;
until done end repeat;
close mycursor;
end //
delimiter ;
```
# 二十、触发器
触发器会在某个表执行以下语句时而自动执行DELETE、INSERT、UPDATE。
触发器必须指定在语句执行之前还是之后自动执行,之前执行使用 BEFORE 关键字,之后执行使用 AFTER 关键字。BEFORE 用于数据验证和净化AFTER 用于审计跟踪,将修改记录到另外一张表中。
INSERT 触发器包含一个名为 NEW 的虚拟表。
```sql
CREATE TRIGGER mytrigger AFTER INSERT ON mytable
FOR EACH ROW SELECT NEW.col into @result;
SELECT @result; -- 获取结果
```
DELETE 触发器包含一个名为 OLD 的虚拟表,并且是只读的。
UPDATE 触发器包含一个名为 NEW 和一个名为 OLD 的虚拟表,其中 NEW 是可以被修改的,而 OLD 是只读的。
MySQL 不允许在触发器中使用 CALL 语句,也就是不能调用存储过程。
# 二十一、事务管理
基本术语:
- 事务transaction指一组 SQL 语句;
- 回退rollback指撤销指定 SQL 语句的过程;
- 提交commit指将未存储的 SQL 语句结果写入数据库表;
- 保留点savepoint指事务处理中设置的临时占位符placeholder你可以对它发布回退与回退整个事务处理不同
不能回退 SELECT 语句,回退 SELECT 语句也没意义;也不能回退 CREATE 和 DROP 语句。
MySQL 的事务提交默认是隐式提交,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 START TRANSACTION 语句时,会关闭隐式提交;当 COMMIT 或 ROLLBACK 语句执行后,事务会自动关闭,重新恢复隐式提交。
通过设置 autocommit 为 0 可以取消自动提交autocommit 标记是针对每个连接而不是针对服务器的。
如果没有设置保留点ROLLBACK 会回退到 START TRANSACTION 语句处;如果设置了保留点,并且在 ROLLBACK 中指定该保留点,则会回退到该保留点。
```sql
START TRANSACTION
// ...
SAVEPOINT delete1
// ...
ROLLBACK TO delete1
// ...
COMMIT
```
# 二十二、字符集
基本术语:
- 字符集为字母和符号的集合;
- 编码为某个字符集成员的内部表示;
- 校对字符指定如何比较,主要用于排序和分组。
除了给表指定字符集和校对外,也可以给列指定:
```sql
CREATE TABLE mytable
(col VARCHAR(10) CHARACTER SET latin COLLATE latin1_general_ci )
DEFAULT CHARACTER SET hebrew COLLATE hebrew_general_ci;
```
可以在排序、分组时指定校对:
```sql
SELECT *
FROM mytable
ORDER BY col COLLATE latin1_general_ci;
```
# 二十三、权限管理
MySQL 的账户信息保存在 mysql 这个数据库中。
```sql
USE mysql;
SELECT user FROM user;
```
**创建账户**
新创建的账户没有任何权限。
```sql
CREATE USER myuser IDENTIFIED BY 'mypassword';
```
**修改账户名**
```sql
RENAME myuser TO newuser;
```
**删除账户**
```sql
DROP USER myuser;
```
**查看权限**
```sql
SHOW GRANTS FOR myuser;
```
**授予权限**
账户用 username@host 的形式定义username@% 使用的是默认主机名。
```sql
GRANT SELECT, INSERT ON mydatabase.* TO myuser;
```
**删除权限**
GRANT 和 REVOKE 可在几个层次上控制访问权限:
- 整个服务器,使用 GRANT ALL 和 REVOKE ALL
- 整个数据库,使用 ON database.\*
- 特定的表,使用 ON database.table
- 特定的列;
- 特定的存储过程。
```sql
REVOKE SELECT, INSERT ON mydatabase.* FROM myuser;
```
**更改密码**
必须使用 Password() 函数
```sql
SET PASSWROD FOR myuser = Password('new_password');
```
# 参考资料
- BenForta. SQL 必知必会 [M]. 人民邮电出版社, 2013.
>>>>>>> 8c67483fe9ac599aa6f6358fb32a541d7e279666

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,908 @@
<!-- GFM-TOC -->
<<<<<<< HEAD
<!-- GFM-TOC -->
=======
* [一、概述](#一概述)
* [网络的网络](#网络的网络)
* [ISP](#isp)
* [主机之间的通信方式](#主机之间的通信方式)
* [电路交换与分组交换](#电路交换与分组交换)
* [时延](#时延)
* [计算机网络体系结构*](#计算机网络体系结构)
* [二、物理层](#二物理层)
* [通信方式](#通信方式)
* [带通调制](#带通调制)
* [三、数据链路层](#三数据链路层)
* [基本问题](#基本问题)
* [信道分类](#信道分类)
* [信道复用技术](#信道复用技术)
* [CSMA/CD 协议*](#csmacd-协议)
* [PPP 协议](#ppp-协议)
* [MAC 地址](#mac-地址)
* [局域网](#局域网)
* [以太网*](#以太网)
* [交换机*](#交换机)
* [虚拟局域网](#虚拟局域网)
* [四、网络层*](#四网络层)
* [概述](#概述)
* [IP 数据报格式](#ip-数据报格式)
* [IP 地址编址方式](#ip-地址编址方式)
* [地址解析协议 ARP](#地址解析协议-arp)
* [网际控制报文协议 ICMP](#网际控制报文协议-icmp)
* [虚拟专用网 VPN](#虚拟专用网-vpn)
* [网络地址转换 NAT](#网络地址转换-nat)
* [路由器的结构](#路由器的结构)
* [路由器分组转发流程](#路由器分组转发流程)
* [路由选择协议](#路由选择协议)
* [五、运输层*](#五运输层)
* [UDP 和 TCP 的特点](#udp-和-tcp-的特点)
* [UDP 首部格式](#udp-首部格式)
* [TCP 首部格式](#tcp-首部格式)
* [TCP 的三次握手](#tcp-的三次握手)
* [TCP 的四次挥手](#tcp-的四次挥手)
* [TCP 可靠传输](#tcp-可靠传输)
* [TCP 滑动窗口](#tcp-滑动窗口)
* [TCP 流量控制](#tcp-流量控制)
* [TCP 拥塞控制](#tcp-拥塞控制)
* [六、应用层](#六应用层)
* [域名系统](#域名系统)
* [文件传送协议](#文件传送协议)
* [动态主机配置协议](#动态主机配置协议)
* [远程登录协议](#远程登录协议)
* [电子邮件协议](#电子邮件协议)
* [常用端口](#常用端口)
* [Web 页面请求过程](#web-页面请求过程)
* [参考资料](#参考资料)
<!-- GFM-TOC -->
# 一、概述
## 网络的网络
网络把主机连接起来,而互联网是把多种不同的网络连接起来,因此互联网是网络的网络。
<div align="center"> <img src="../pics//network-of-networks.gif" width="500"/> </div><br>
## ISP
互联网服务提供商 ISP 可以从互联网管理机构获得许多 IP 地址,同时拥有通信线路以及路由器等联网设备,个人或机构向 ISP 缴纳一定的费用就可以接入互联网。
<div align="center"> <img src="../pics//46cec213-3048-4a80-aded-fdd577542801.jpg" width="500"/> </div><br>
目前的互联网是一种多层次 ISP 结构ISP 根据覆盖面积的大小分为第一层 ISP、区域 ISP 和接入 ISP。
互联网交换点 IXP 允许两个 ISP 直接相连而不用经过第三个 ISP。
<div align="center"> <img src="../pics//168e893c-e4a0-4ba4-b81f-9d993483abd0.jpg" width="500"/> </div><br>
## 主机之间的通信方式
- 客户-服务器C/S客户是服务的请求方服务器是服务的提供方。
- 对等P2P不区分客户和服务器。
<div align="center"> <img src="../pics//2ad244f5-939c-49fa-9385-69bc688677ab.jpg" width=""/> </div><br>
## 电路交换与分组交换
### 1. 电路交换
电路交换用于电话通信系统,两个用户要通信之前需要建立一条专用的物理链路,并且在整个通信过程中始终占用该链路。由于通信的过程中不可能一直在使用传输线路,因此电路交换对线路的利用率很低,往往不到 10%。
### 2. 分组交换
每个分组都有首部和尾部,包含了源地址和目的地址等控制信息,在同一个传输线路上同时传输多个分组互相不会影响,因此在同一条传输线路上允许同时传输多个分组,也就是说分组交换不需要占用传输线路。
在一个邮局通信系统中,邮局收到一份邮件之后,先存储下来,然后把相同目的地的邮件一起转发到下一个目的地,这个过程就是存储转发过程,分组交换也使用了存储转发过程。
## 时延
总时延 = 传输时延 + 传播时延 + 处理时延 + 排队时延
<div align="center"> <img src="../pics//3939369b-3a4a-48a0-b9eb-3efae26dd400.png" width="800"/> </div><br>
### 1. 传输时延
主机或路由器传输数据帧所需要的时间。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(bit)}{v(bit/s)}"/></div> <br>
其中 l 表示数据帧的长度v 表示传输速率。
### 2. 传播时延
电磁波在信道中传播所需要花费的时间,电磁波传播的速度接近光速。
<div align="center"><img src="https://latex.codecogs.com/gif.latex?delay=\frac{l(m)}{v(m/s)}"/></div> <br>
其中 l 表示信道长度v 表示电磁波在信道上的传播速度。
### 3. 处理时延
主机或路由器收到分组时进行处理所需要的时间,例如分析首部、从分组中提取数据、进行差错检验或查找适当的路由等。
### 4. 排队时延
分组在路由器的输入队列和输出队列中排队等待的时间,取决于网络当前的通信量。
## 计算机网络体系结构*
<div align="center"> <img src="../pics//426df589-6f97-4622-b74d-4a81fcb1da8e.png" width="800"/> </div><br>
### 1. 五层协议
- **应用层** :为特定应用程序提供数据传输服务,例如 HTTP、DNS 等。数据单位为报文。
- **运输层** :提供的是进程间的通用数据传输服务。由于应用层协议很多,定义通用的运输层协议就可以支持不断增多的应用层协议。运输层包括两种协议:传输控制协议 TCP提供面向连接、可靠的数据传输服务数据单位为报文段用户数据报协议 UDP提供无连接、尽最大努力的数据传输服务数据单位为用户数据报。TCP 主要提供完整性服务UDP 主要提供及时性服务。
- **网络层** :为主机间提供数据传输服务,而运输层协议是为主机中的进程提供服务。网络层把运输层传递下来的报文段或者用户数据报封装成分组。
- **数据链路层** :网络层针对的还是主机之间的数据传输服务,而主机之间可以有很多链路,链路层协议就是为同一链路的主机提供服务。数据链路层把网络层传下来的分组封装成帧。
- **物理层** :考虑的是怎样在传输媒体上传输数据比特流,而不是指具体的传输媒体。物理层的作用是尽可能屏蔽传输媒体和通信手段的差异,使数据链路层感觉不到这些差异。
### 2. OSI
其中表示层和会话层用途如下:
- **表示层** :数据压缩、加密以及数据描述,这使得应用程序不必担心在各台主机中数据内部格式不同的问题。
- **会话层** :建立及管理会话。
五层协议没有表示层和会话层,而是将这些功能留给应用程序开发者处理。
### 3. TCP/IP
它只有四层,相当于五层协议中数据链路层和物理层合并为网络接口层。
TCP/IP 体系结构不严格遵循 OSI 分层概念,应用层可能会直接使用 IP 层或者网络接口层。
<div align="center"> <img src="../pics//45e0e0bf-386d-4280-a341-a0b9496c7674.png" width="400"/> </div><br>
TCP/IP 协议族是一种沙漏形状中间小两边大IP 协议在其中占用举足轻重的地位。
<div align="center"> <img src="../pics//d4eef1e2-5703-4ca4-82ab-8dda93d6b81f.png" width="500"/> </div><br>
### 4. 数据在各层之间的传递过程
在向下的过程中,需要添加下层协议所需要的首部或者尾部,而在向上的过程中不断拆开首部和尾部。
路由器只有下面三层协议,因为路由器位于网络核心中,不需要为进程或者应用程序提供服务,因此也就不需要运输层和应用层。
<div align="center"> <img src="../pics//ac106e7e-489a-4082-abd9-dabebe48394c.jpg" width="800"/> </div><br>
# 二、物理层
## 通信方式
根据信息在传输线上的传送方向,分为以下三种通信方式:
- 单工通信:单向传输
- 半双工通信:双向交替传输
- 全双工通信:双向同时传输
## 带通调制
模拟信号是连续的信号,数字信号是离散的信号。带通调制把数字信号转换为模拟信号。
<div align="center"> <img src="../pics//f0a31c04-6e26-408c-8395-88f4e2ae928b.jpg"/> </div><br>
# 三、数据链路层
## 基本问题
### 1. 封装成帧
将网络层传下来的分组添加首部和尾部,用于标记帧的开始和结束。
<div align="center"> <img src="../pics//ea5f3efe-d5e6-499b-b278-9e898af61257.jpg" width="500"/> </div><br>
### 2. 透明传输
透明表示一个实际存在的事物看起来好像不存在一样。
帧使用首部和尾部进行定界,如果帧的数据部分含有和首部尾部相同的内容,那么帧的开始和结束位置就会被错误的判定。需要在数据部分出现首部尾部相同的内容前面插入转义字符。如果数据部分出现转义字符,那么就在转义字符前面再加个转义字符。在接收端进行处理之后可以还原出原始数据。这个过程透明传输的内容是转义字符,用户察觉不到转义字符的存在。
<div align="center"> <img src="../pics//c5022dd3-be22-4250-b9f6-38ae984a04d7.jpg" width="600"/> </div><br>
### 3. 差错检测
目前数据链路层广泛使用了循环冗余检验CRC来检查比特差错。
## 信道分类
### 1. 广播信道
一对多通信,一个节点发送的数据能够被广播信道上所有的节点接收到。
所有的节点都在同一个广播信道上发送数据,因此需要有专门的控制方法进行协调,避免发生冲突(冲突也叫碰撞)。
主要有两种控制方法进行协调,一个是使用信道复用技术,一是使用 CSMA/CD 协议。
### 2. 点对点信道
一对一通信。
因为不会发生碰撞,因此也比较简单,使用 PPP 协议进行控制。
## 信道复用技术
### 1. 频分复用
频分复用的所有主机在相同的时间占用不同的频率带宽资源。
<div align="center"> <img src="../pics//c4c14368-519c-4a0e-8331-0a553715e3e7.jpg"/> </div><br>
### 2. 时分复用
时分复用的所有主机在不同的时间占用相同的频率带宽资源。
<div align="center"> <img src="../pics//fa2273c3-1b5f-48ce-8e8b-441a4116c1c4.jpg"/> </div><br>
使用频分复用和时分复用进行通信,在通信的过程中主机会一直占用一部分信道资源。但是由于计算机数据的突发性质,通信过程没必要一直占用信道资源而不让出给其它用户使用,因此这两种方式对信道的利用率都不高。
### 3. 统计时分复用
是对时分复用的一种改进,不固定每个用户在时分复用帧中的位置,只要有数据就集中起来组成统计时分复用帧然后发送。
<div align="center"> <img src="../pics//5999e5de-7c16-4b52-b3aa-6dc7b58c7894.png" width="700"/> </div><br>
### 4. 波分复用
光的频分复用。由于光的频率很高,因此习惯上用波长而不是频率来表示所使用的光载波。
<div align="center"> <img src="../pics//21041ec2-babb-483f-bf47-8b8148eec162.png" width="700"/> </div><br>
### 5. 码分复用
为每个用户分配 m bit 的码片,并且所有的码片正交,对于任意两个码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/><img src="https://latex.codecogs.com/gif.latex?\vec{T}"/>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{T}=0"/></div> <br>
为了讨论方便,取 m=8设码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 为 00011011。在拥有该码片的用户发送比特 1 时就发送该码片,发送比特 0 时就发送该码片的反码 11100100。
在计算时将 00011011 记作 (-1 -1 -1 +1 +1 -1 +1 +1),可以得到
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S}=1"/></div> <br>
<div align="center"><img src="https://latex.codecogs.com/gif.latex?\frac{1}{m}\vec{S}\cdot\vec{S'}=-1"/></div> <br>
其中 <img src="https://latex.codecogs.com/gif.latex?\vec{S'}"/><img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 的反码。
利用上面的式子我们知道,当接收端使用码片 <img src="https://latex.codecogs.com/gif.latex?\vec{S}"/> 对接收到的数据进行内积运算时,结果为 0 的是其它用户发送的数据,结果为 1 的是用户发送的比特 1结果为 -1 的是用户发送的比特 0。
码分复用需要发送的数据量为原先的 m 倍。
<div align="center"> <img src="../pics//92ad9bae-7d02-43ba-8115-a9d6f530ca28.png" width="600"/> </div><br>
## CSMA/CD 协议*
CSMA/CD 表示载波监听多点接入 / 碰撞检测。
- **多点接入** :说明这是总线型网络,许多主机以多点的方式连接到总线上。
- **载波监听** :每个主机都必须不停地监听信道。在发送前,如果监听到信道正在使用,就必须等待。
- **碰撞检测** :在发送中,如果监听到信道已有其它主机正在发送数据,就表示发生了碰撞。虽然每个主机在发送数据之前都已经监听到信道为空闲,但是由于电磁波的传播时延的存在,还是有可能会发生碰撞。
记端到端的传播时延为 τ,最先发送的站点最多经过 2τ 就可以知道是否发生了碰撞,称 2τ 为 **争用期** 。只有经过争用期之后还没有检测到碰撞,才能肯定这次发送不会发生碰撞。
当发生碰撞时,站点要停止发送,等待一段时间再发送。这个时间采用 **截断二进制指数退避算法** 来确定。从离散的整数集合 {0, 1, .., (2<sup>k</sup>-1)} 中随机取出一个数,记作 r然后取 r 倍的争用期作为重传等待时间。
<div align="center"> <img src="../pics//5aa82b89-f266-44da-887d-18f31f01d8ef.png" width="600"/> </div><br>
## PPP 协议
互联网用户通常需要连接到某个 ISP 之后才能接入到互联网PPP 协议是用户计算机和 ISP 进行通信时所使用的数据链路层协议。
<div align="center"> <img src="../pics//ddcf2327-8d84-425d-8535-121a94bcb88d.jpg" width="600"/> </div><br>
PPP 的帧格式:
- F 字段为帧的定界符
- A 和 C 字段暂时没有意义
- FCS 字段是使用 CRC 的检验序列
- 信息部分的长度不超过 1500
<div align="center"> <img src="../pics//69f16984-a66f-4288-82e4-79b4aa43e835.jpg" width="500"/> </div><br>
## MAC 地址
MAC 地址是链路层地址,长度为 6 字节48 位),用于唯一标识网络适配器(网卡)。
一台主机拥有多少个适配器就有多少个 MAC 地址。例如笔记本电脑普遍存在无线网络适配器和有线网络适配器,因此就有两个 MAC 地址。
## 局域网
局域网是一种典型的广播信道,主要特点是网络为一个单位所拥有,且地理范围和站点数目均有限。
主要有以太网、令牌环网、FDDI 和 ATM 等局域网技术,目前以太网占领着有线局域网市场。
可以按照网络拓扑结构对局域网进行分类:
<div align="center"> <img src="../pics//a6026bb4-3daf-439f-b1ec-a5a24e19d2fb.jpg" width="600"/> </div><br>
## 以太网*
以太网是一种星型拓扑结构局域网。
早期使用集线器进行连接,集线器是一种物理层设备,作用于比特而不是帧,当一个比特到达接口时,集线器重新生成这个比特,并将其能量强度放大,从而扩大网络的传输距离,之后再将这个比特发送到其它所有接口。如果集线器同时收到同时从两个不同接口的帧,那么就发生了碰撞。
目前以太网使用交换机替代了集线器,交换机是一种链路层设备,它不会发生碰撞,能根据 MAC 地址进行存储转发。
以太网帧格式:
- **类型** :标记上层使用的协议;
- **数据** :长度在 46-1500 之间,如果太小则需要填充;
- **FCS** :帧检验序列,使用的是 CRC 检验方法;
- **前同步码** :只是为了计算 FCS 临时加入的,计算结束之后会丢弃。
<div align="center"> <img src="../pics//50d38e84-238f-4081-8876-14ef6d7938b5.jpg" width="600"/> </div><br>
## 交换机*
交换机具有自学习能力,学习的是交换表的内容,交换表中存储着 MAC 地址到接口的映射。
正是由于这种自学习能力,因此交换机是一种即插即用设备,不需要网络管理员手动配置交换表内容。
下图中,交换机有 4 个接口,主机 A 向主机 B 发送数据帧时,交换机把主机 A 到接口 1 的映射写入交换表中。为了发送数据帧到 B先查交换表此时没有主机 B 的表项,那么主机 A 就发送广播帧,主机 C 和主机 D 会丢弃该帧。主机 B 收下之后,查找交换表得到主机 A 映射的接口为 1就发送数据帧到接口 1同时交换机添加主机 B 到接口 3 的映射。
<div align="center"> <img src="../pics//c9cfcd20-c901-435f-9a07-3e46830c359f.jpg" width="800"/> </div><br>
## 虚拟局域网
虚拟局域网可以建立与物理位置无关的逻辑组,只有在同一个虚拟局域网中的成员才会收到链路层广播信息。
例如下图中 (A1, A2, A3, A4) 属于一个虚拟局域网A1 发送的广播会被 A2、A3、A4 收到,而其它站点收不到。
使用 VLAN 干线连接来建立虚拟局域网,每台交换机上的一个特殊接口被设置为干线接口,以互连 VLAN 交换机。IEEE 定义了一种扩展的以太网帧格式 802.1Q,它在标准以太网帧上加进了 4 字节首部 VLAN 标签,用于表示该帧属于哪一个虚拟局域网。
<div align="center"> <img src="../pics//a74b70ac-323a-4b31-b4d5-90569b8a944b.png" width="500"/> </div><br>
# 四、网络层*
## 概述
因为网络层是整个互联网的核心,因此应当让网络层尽可能简单。网络层向上只提供简单灵活的、无连接的、尽最大努力交互的数据报服务。
使用 IP 协议,可以把异构的物理网络连接起来,使得在网络层看起来好像是一个统一的网络。
<div align="center"> <img src="../pics//7b038838-c75b-4538-ae84-6299386704e5.jpg" width="500"/> </div><br>
与 IP 协议配套使用的还有三个协议:
- 地址解析协议 ARPAddress Resolution Protocol
- 网际控制报文协议 ICMPInternet Control Message Protocol
- 网际组管理协议 IGMPInternet Group Management Protocol
<div align="center"> <img src="../pics//0a9f4125-b6ab-4e94-a807-fd7070ae726a.png" width="350"/> </div><br>
## IP 数据报格式
<div align="center"> <img src="../pics//85c05fb1-5546-4c50-9221-21f231cdc8c5.jpg" width="700"/> </div><br>
- **版本** : 有 4IPv4和 6IPv6两个值
- **首部长度** : 占 4 位,因此最大值为 15。值为 1 表示的是 1 个 32 位字的长度,也就是 4 字节。因为首部固定长度为 20 字节,因此该值最小为 5。如果可选字段的长度不是 4 字节的整数倍,就用尾部的填充部分来填充。
- **区分服务** : 用来获得更好的服务,一般情况下不使用。
- **总长度** : 包括首部长度和数据部分长度。
- **生存时间** TTL它的存在是为了防止无法交付的数据报在互联网中不断兜圈子。以路由器跳数为单位当 TTL 为 0 时就丢弃数据报。
- **协议** :指出携带的数据应该上交给哪个协议进行处理,例如 ICMP、TCP、UDP 等。
- **首部检验和** :因为数据报每经过一个路由器,都要重新计算检验和,因此检验和不包含数据部分可以减少计算的工作量。
- **标识** : 在数据报长度过长从而发生分片的情况下,相同数据报的不同分片具有相同的标识符。
- **片偏移** : 和标识符一起,用于发生分片的情况。片偏移的单位为 8 字节。
<div align="center"> <img src="../pics//23ba890e-e11c-45e2-a20c-64d217f83430.png" width="700"/> </div><br>
## IP 地址编址方式
IP 地址的编址方式经历了三个历史阶段:
- 分类
- 子网划分
- 无分类
### 1. 分类
由两部分组成,网络号和主机号,其中不同分类具有不同的网络号长度,并且是固定的。
IP 地址 ::= {< 网络号 >, < 主机号 >}
<div align="center"> <img src="../pics//cbf50eb8-22b4-4528-a2e7-d187143d57f7.png" width="500"/> </div><br>
### 2. 子网划分
通过在主机号字段中拿一部分作为子网号,把两级 IP 地址划分为三级 IP 地址。
IP 地址 ::= {< 网络号 >, < 子网号 >, < 主机号 >}
要使用子网,必须配置子网掩码。一个 B 类地址的默认子网掩码为 255.255.0.0,如果 B 类地址的子网占两个比特,那么子网掩码为 11111111 11111111 11000000 00000000也就是 255.255.192.0。
注意,外部网络看不到子网的存在。
### 3. 无分类
无分类编址 CIDR 消除了传统 A 类、B 类和 C 类地址以及划分子网的概念,使用网络前缀和主机号来对 IP 地址进行编码,网络前缀的长度可以根据需要变化。
IP 地址 ::= {< 网络前缀号 >, < 主机号 >}
CIDR 的记法上采用在 IP 地址后面加上网络前缀长度的方法,例如 128.14.35.7/20 表示前 20 位为网络前缀。
CIDR 的地址掩码可以继续称为子网掩码,子网掩码首 1 长度为网络前缀的长度。
一个 CIDR 地址块中有很多地址,一个 CIDR 表示的网络就可以表示原来的很多个网络,并且在路由表中只需要一个路由就可以代替原来的多个路由,减少了路由表项的数量。把这种通过使用网络前缀来减少路由表项的方式称为路由聚合,也称为 **构成超网**
在路由表中的项目由“网络前缀”和“下一跳地址”组成,在查找时可能会得到不止一个匹配结果,应当采用最长前缀匹配来确定应该匹配哪一个。
## 地址解析协议 ARP
网络层实现主机之间的通信而链路层实现具体每段链路之间的通信。因此在通信过程中IP 数据报的源地址和目的地址始终不变,而 MAC 地址随着链路的改变而改变。
<div align="center"> <img src="../pics//66192382-558b-4b05-a35d-ac4a2b1a9811.jpg" width="700"/> </div><br>
ARP 实现由 IP 地址得到 MAC 地址。
<div align="center"> <img src="../pics//b9d79a5a-e7af-499b-b989-f10483e71b8b.jpg" width="500"/> </div><br>
每个主机都有一个 ARP 高速缓存,里面有本局域网上的各主机和路由器的 IP 地址到 MAC 地址的映射表。
如果主机 A 知道主机 B 的 IP 地址,但是 ARP 高速缓存中没有该 IP 地址到 MAC 地址的映射,此时主机 A 通过广播的方式发送 ARP 请求分组,主机 B 收到该请求后会发送 ARP 响应分组给主机 A 告知其 MAC 地址,随后主机 A 向其高速缓存中写入主机 B 的 IP 地址到 MAC 地址的映射。
<div align="center"> <img src="../pics//8006a450-6c2f-498c-a928-c927f758b1d0.png" width="700"/> </div><br>
## 网际控制报文协议 ICMP
ICMP 是为了更有效地转发 IP 数据报和提高交付成功的机会。它封装在 IP 数据报中,但是不属于高层协议。
<div align="center"> <img src="../pics//e3124763-f75e-46c3-ba82-341e6c98d862.jpg" width="500"/> </div><br>
ICMP 报文分为差错报告报文和询问报文。
<div align="center"> <img src="../pics//aa29cc88-7256-4399-8c7f-3cf4a6489559.png" width="600"/> </div><br>
### 1. Ping
Ping 是 ICMP 的一个重要应用,主要用来测试两台主机之间的连通性。
Ping 的原理是通过向目的主机发送 ICMP Echo 请求报文,目的主机收到之后会发送 Echo 回答报文。Ping 会根据时间和成功响应的次数估算出数据包往返时间以及丢包率。
### 2. Traceroute
Traceroute 是 ICMP 的另一个应用,用来跟踪一个分组从源点到终点的路径。
Traceroute 发送的 IP 数据报封装的是无法交付的 UDP 用户数据报,并由目的主机发送终点不可达差错报告报文。
- 源主机向目的主机发送一连串的 IP 数据报。第一个数据报 P1 的生存时间 TTL 设置为 1当 P1 到达路径上的第一个路由器 R1 时R1 收下它并把 TTL 减 1此时 TTL 等于 0R1 就把 P1 丢弃,并向源主机发送一个 ICMP 时间超过差错报告报文;
- 源主机接着发送第二个数据报 P2并把 TTL 设置为 2。P2 先到达 R1R1 收下后把 TTL 减 1 再转发给 R2R2 收下后也把 TTL 减 1由于此时 TTL 等于 0R2 就丢弃 P2并向源主机发送一个 ICMP 时间超过差错报文。
- 不断执行这样的步骤,直到最后一个数据报刚刚到达目的主机,主机不转发数据报,也不把 TTL 值减 1。但是因为数据报封装的是无法交付的 UDP因此目的主机要向源主机发送 ICMP 终点不可达差错报告报文。
- 之后源主机知道了到达目的主机所经过的路由器 IP 地址以及到达每个路由器的往返时间。
## 虚拟专用网 VPN
由于 IP 地址的紧缺,一个机构能申请到的 IP 地址数往往远小于本机构所拥有的主机数。并且一个机构并不需要把所有的主机接入到外部的互联网中,机构内的计算机可以使用仅在本机构有效的 IP 地址(专用地址)。
有三个专用地址块:
- 10.0.0.0 \~ 10.255.255.255
- 172.16.0.0 \~ 172.31.255.255
- 192.168.0.0 \~ 192.168.255.255
VPN 使用公用的互联网作为本机构各专用网之间的通信载体。专用指机构内的主机只与本机构内的其它主机通信;虚拟指“好像是”,而实际上并不是,它有经过公用的互联网。
下图中,场所 A 和 B 的通信经过互联网,如果场所 A 的主机 X 要和另一个场所 B 的主机 Y 通信IP 数据报的源地址是 10.1.0.1,目的地址是 10.2.0.3。数据报先发送到与互联网相连的路由器 R1R1 对内部数据进行加密,然后重新加上数据报的首部,源地址是路由器 R1 的全球地址 125.1.2.3,目的地址是路由器 R2 的全球地址 194.4.5.6。路由器 R2 收到数据报后将数据部分进行解密,恢复原来的数据报,此时目的地址为 10.2.0.3,就交付给 Y。
<div align="center"> <img src="../pics//1556770b-8c01-4681-af10-46f1df69202c.jpg" width="800"/> </div><br>
## 网络地址转换 NAT
专用网内部的主机使用本地 IP 地址又想和互联网上的主机通信时,可以使用 NAT 来将本地 IP 转换为全球 IP。
在以前NAT 将本地 IP 和全球 IP 一一对应,这种方式下拥有 n 个全球 IP 地址的专用网内最多只可以同时有 n 台主机接入互联网。为了更有效地利用全球 IP 地址,现在常用的 NAT 转换表把运输层的端口号也用上了,使得多个专用网内部的主机共用一个全球 IP 地址。使用端口号的 NAT 也叫做网络地址与端口转换 NAPT。
<div align="center"> <img src="../pics//2719067e-b299-4639-9065-bed6729dbf0b.png" width=""/> </div><br>
## 路由器的结构
路由器从功能上可以划分为:路由选择和分组转发。
分组转发结构由三个部分组成:交换结构、一组输入端口和一组输出端口。
<div align="center"> <img src="../pics//c3369072-c740-43b0-b276-202bd1d3960d.jpg" width="600"/> </div><br>
## 路由器分组转发流程
- 从数据报的首部提取目的主机的 IP 地址 D得到目的网络地址 N。
- 若 N 就是与此路由器直接相连的某个网络地址,则进行直接交付;
- 若路由表中有目的地址为 D 的特定主机路由,则把数据报传送给表中所指明的下一跳路由器;
- 若路由表中有到达网络 N 的路由,则把数据报传送给路由表中所指明的下一跳路由器;
- 若路由表中有一个默认路由,则把数据报传送给路由表中所指明的默认路由器;
- 报告转发分组出错。
<div align="center"> <img src="../pics//1ab49e39-012b-4383-8284-26570987e3c4.jpg" width="800"/> </div><br>
## 路由选择协议
路由选择协议都是自适应的,能随着网络通信量和拓扑结构的变化而自适应地进行调整。
互联网可以划分为许多较小的自治系统 AS一个 AS 可以使用一种和别的 AS 不同的路由选择协议。
可以把路由选择协议划分为两大类:
- 自治系统内部的路由选择RIP 和 OSPF
- 自治系统间的路由选择BGP
### 1. 内部网关协议 RIP
RIP 是一种基于距离向量的路由选择协议。距离是指跳数,直接相连的路由器跳数为 1。跳数最多为 15超过 15 表示不可达。
RIP 按固定的时间间隔仅和相邻路由器交换自己的路由表,经过若干次交换之后,所有路由器最终会知道到达本自治系统中任何一个网络的最短距离和下一跳路由器地址。
距离向量算法:
- 对地址为 X 的相邻路由器发来的 RIP 报文,先修改报文中的所有项目,把下一跳字段中的地址改为 X并把所有的距离字段加 1
- 对修改后的 RIP 报文中的每一个项目,进行以下步骤:
- 若原来的路由表中没有目的网络 N则把该项目添加到路由表中
- 否则:若下一跳路由器地址是 X则把收到的项目替换原来路由表中的项目否则若收到的项目中的距离 d 小于路由表中的距离,则进行更新(例如原始路由表项为 Net2, 5, P新表项为 Net2, 4, X则更新否则什么也不做。
- 若 3 分钟还没有收到相邻路由器的更新路由表,则把该相邻路由器标为不可达,即把距离置为 16。
RIP 协议实现简单,开销小。但是 RIP 能使用的最大距离为 15限制了网络的规模。并且当网络出现故障时要经过比较长的时间才能将此消息传送到所有路由器。
### 2. 内部网关协议 OSPF
开放最短路径优先 OSPF是为了克服 RIP 的缺点而开发出来的。
开放表示 OSPF 不受某一家厂商控制,而是公开发表的;最短路径优先表示使用了 Dijkstra 提出的最短路径算法 SPF。
OSPF 具有以下特点:
- 向本自治系统中的所有路由器发送信息,这种方法是洪泛法。
- 发送的信息就是与相邻路由器的链路状态,链路状态包括与哪些路由器相连以及链路的度量,度量用费用、距离、时延、带宽等来表示。
- 只有当链路状态发生变化时,路由器才会发送信息。
所有路由器都具有全网的拓扑结构图,并且是一致的。相比于 RIPOSPF 的更新过程收敛的很快。
### 3. 外部网关协议 BGP
BGPBorder Gateway Protocol边界网关协议
AS 之间的路由选择很困难,主要是由于:
- 互联网规模很大;
- 各个 AS 内部使用不同的路由选择协议,无法准确定义路径的度量;
- AS 之间的路由选择必须考虑有关的策略,比如有些 AS 不愿意让其它 AS 经过。
BGP 只能寻找一条比较好的路由,而不是最佳路由。
每个 AS 都必须配置 BGP 发言人,通过在两个相邻 BGP 发言人之间建立 TCP 连接来交换路由信息。
<div align="center"> <img src="../pics//9cd0ae20-4fb5-4017-a000-f7d3a0eb3529.png" width="600"/> </div><br>
# 五、运输层*
网络层只把分组发送到目的主机,但是真正通信的并不是主机而是主机中的进程。运输层提供了进程间的逻辑通信,运输层向高层用户屏蔽了下面网络层的核心细节,使应用程序看起来像是在两个运输层实体之间有一条端到端的逻辑通信信道。
## UDP 和 TCP 的特点
- 用户数据报协议 UDPUser Datagram Protocol是无连接的尽最大可能交付没有拥塞控制面向报文对于应用程序传下来的报文不合并也不拆分只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。
- 传输控制协议 TCPTransmission Control Protocol是面向连接的提供可靠交付有流量控制拥塞控制提供全双工通信面向字节流把应用层传下来的报文看成字节流把字节流组织成大小不等的数据块每一条 TCP 连接只能是点对点的(一对一)。
## UDP 首部格式
<div align="center"> <img src="../pics//d4c3a4a1-0846-46ec-9cc3-eaddfca71254.jpg" width="600"/> </div><br>
首部字段只有 8 个字节包括源端口、目的端口、长度、检验和。12 字节的伪首部是为了计算检验和临时添加的。
## TCP 首部格式
<div align="center"> <img src="../pics//55dc4e84-573d-4c13-a765-52ed1dd251f9.png" width="700"/> </div><br>
- **序号** :用于对字节流进行编号,例如序号为 301表示第一个字节的编号为 301如果携带的数据长度为 100 字节,那么下一个报文段的序号应为 401。
- **确认号** :期望收到的下一个报文段的序号。例如 B 正确收到 A 发送来的一个报文段,序号为 501携带的数据长度为 200 字节,因此 B 期望下一个报文段的序号为 701B 发送给 A 的确认报文段中确认号就为 701。
- **数据偏移** :指的是数据部分距离报文段起始处的偏移量,实际上指的是首部的长度。
- **确认 ACK** :当 ACK=1 时确认号字段有效否则无效。TCP 规定,在连接建立后所有传送的报文段都必须把 ACK 置 1。
- **同步 SYN** :在连接建立时用来同步序号。当 SYN=1ACK=0 时表示这是一个连接请求报文段。若对方同意建立连接,则响应报文中 SYN=1ACK=1。
- **终止 FIN** :用来释放一个连接,当 FIN=1 时,表示此报文段的发送方的数据已发送完毕,并要求释放连接。
- **窗口** :窗口值作为接收方让发送方设置其发送窗口的依据。之所以要有这个限制,是因为接收方的数据缓存空间是有限的。
## TCP 的三次握手
<div align="center"> <img src="../pics//e92d0ebc-7d46-413b-aec1-34a39602f787.png" width="600"/> </div><br>
假设 A 为客户端B 为服务器端。
- 首先 B 处于 LISTEN监听状态等待客户的连接请求。
- A 向 B 发送连接请求报文SYN=1ACK=0选择一个初始的序号 x。
- B 收到连接请求报文,如果同意建立连接,则向 A 发送连接确认报文SYN=1ACK=1确认号为 x+1同时也选择一个初始的序号 y。
- A 收到 B 的连接确认报文后,还要向 B 发出确认,确认号为 y+1序号为 x+1。
- B 收到 A 的确认后,连接建立。
**三次握手的原因**
第三次握手是为了防止失效的连接请求到达服务器,让服务器错误打开连接。
客户端发送的连接请求如果在网络中滞留,那么就会隔很长一段时间才能收到服务器端发回的连接确认。客户端等待一个超时重传时间之后,就会重新请求连接。但是这个滞留的连接请求最后还是会到达服务器,如果不进行三次握手,那么服务器就会打开两个连接。如果有第三次握手,客户端会忽略服务器之后发送的对滞留连接请求的连接确认,不进行第三次握手,因此就不会再次打开连接。
## TCP 的四次挥手
<div align="center"> <img src="../pics//f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg" width="600"/> </div><br>
以下描述不讨论序号和确认号,因为序号和确认号的规则比较简单。并且不讨论 ACK因为 ACK 在连接建立之后都为 1。
- A 发送连接释放报文FIN=1。
- B 收到之后发出确认,此时 TCP 属于半关闭状态B 能向 A 发送数据但是 A 不能向 B 发送数据。
- 当 B 不再需要连接时发送连接释放报文FIN=1。
- A 收到后发出确认,进入 TIME-WAIT 状态,等待 2 MSL最大报文存活时间后释放连接。
- B 收到 A 的确认后释放连接。
**四次挥手的原因**
客户端发送了 FIN 连接释放报文之后,服务器收到了这个报文,就进入了 CLOSE-WAIT 状态。这个状态是为了让服务器端发送还未传送完毕的数据,传送完毕之后,服务器会发送 FIN 连接释放报文。
**TIME_WAIT**
客户端接收到服务器端的 FIN 报文后进入此状态,此时并不是直接进入 CLOSED 状态,还需要等待一个时间计时器设置的时间 2MSL。这么做有两个理由
- 确保最后一个确认报文能够到达。如果 B 没收到 A 发送来的确认报文那么就会重新发送连接释放请求报文A 等待一段时间就是为了处理这种情况的发生。
- 等待一段时间是为了让本连接持续时间内所产生的所有报文都从网络中消失,使得下一个新的连接不会出现旧的连接请求报文。
## TCP 可靠传输
TCP 使用超时重传来实现可靠传输:如果一个已经发送的报文段在超时时间内没有收到确认,那么就重传这个报文段。
一个报文段从发送再到接收到确认所经过的时间称为往返时间 RTT加权平均往返时间 RTTs 计算如下:
<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTTs=(1-a)*(RTTs)+a*RTT"/></div> <br>
超时时间 RTO 应该略大于 RTTsTCP 使用的超时时间计算如下:
<div align="center"><img src="https://latex.codecogs.com/gif.latex?RTO=RTTs+4*RTT_d"/></div> <br>
其中 RTT<sub>d</sub> 为偏差。
## TCP 滑动窗口
窗口是缓存的一部分,用来暂时存放字节流。发送方和接收方各有一个窗口,接收方通过 TCP 报文段中的窗口字段告诉发送方自己的窗口大小,发送方根据这个值和其它信息设置自己的窗口大小。
发送窗口内的字节都允许被发送,接收窗口内的字节都允许被接收。如果发送窗口左部的字节已经发送并且收到了确认,那么就将发送窗口向右滑动一定距离,直到左部第一个字节不是已发送并且已确认的状态;接收窗口的滑动类似,接收窗口左部字节已经发送确认并交付主机,就向右滑动接收窗口。
接收窗口只会对窗口内最后一个按序到达的字节进行确认,例如接收窗口已经收到的字节为 {31, 34, 35},其中 {31} 按序到达,而 {34, 35} 就不是,因此只对字节 31 进行确认。发送方得到一个字节的确认之后,就知道这个字节之前的所有字节都已经被接收。
<div align="center"> <img src="../pics//a3253deb-8d21-40a1-aae4-7d178e4aa319.jpg" width="800"/> </div><br>
## TCP 流量控制
流量控制是为了控制发送方发送速率,保证接收方来得及接收。
接收方发送的确认报文中的窗口字段可以用来控制发送方窗口大小,从而影响发送方的发送速率。将窗口字段设置为 0则发送方不能发送数据。
## TCP 拥塞控制
如果网络出现拥塞,分组将会丢失,此时发送方会继续重传,从而导致网络拥塞程度更高。因此当出现拥塞时,应当控制发送方的速率。这一点和流量控制很像,但是出发点不同。流量控制是为了让接收方能来得及接收,而拥塞控制是为了降低整个网络的拥塞程度。
<div align="center"> <img src="../pics//51e2ed95-65b8-4ae9-8af3-65602d452a25.jpg" width="500"/> </div><br>
TCP 主要通过四种算法来进行拥塞控制:慢开始、拥塞避免、快重传、快恢复。
发送方需要维护一个叫做拥塞窗口cwnd的状态变量注意拥塞窗口与发送方窗口的区别拥塞窗口只是一个状态变量实际决定发送方能发送多少数据的是发送方窗口。
为了便于讨论,做如下假设:
- 接收方有足够大的接收缓存,因此不会发生流量控制;
- 虽然 TCP 的窗口基于字节,但是这里设窗口的大小单位为报文段。
<div align="center"> <img src="../pics//910f613f-514f-4534-87dd-9b4699d59d31.png" width="800"/> </div><br>
### 1. 慢开始与拥塞避免
发送的最初执行慢开始,令 cwnd=1发送方只能发送 1 个报文段;当收到确认后,将 cwnd 加倍因此之后发送方能够发送的报文段数量为2、4、8 ...
注意到慢开始每个轮次都将 cwnd 加倍,这样会让 cwnd 增长速度非常快,从而使得发送方发送的速度增长速度过快,网络拥塞的可能也就更高。设置一个慢开始门限 ssthresh当 cwnd >= ssthresh 时,进入拥塞避免,每个轮次只将 cwnd 加 1。
如果出现了超时,则令 ssthresh = cwnd/2然后重新执行慢开始。
### 2. 快重传与快恢复
在接收方,要求每次接收到报文段都应该对最后一个已收到的有序报文段进行确认。例如已经接收到 M<sub>1</sub> 和 M<sub>2</sub>,此时收到 M<sub>4</sub>,应当发送对 M<sub>2</sub> 的确认。
在发送方,如果收到三个重复确认,那么可以知道下一个报文段丢失,此时执行快重传,立即重传下一个报文段。例如收到三个 M<sub>2</sub>,则 M<sub>3</sub> 丢失,立即重传 M<sub>3</sub>
在这种情况下,只是丢失个别报文段,而不是网络拥塞。因此执行快恢复,令 ssthresh = cwnd/2 cwnd = ssthresh注意到此时直接进入拥塞避免。
慢开始和快恢复的快慢指的是 cwnd 的设定值,而不是 cwnd 的增长速率。慢开始 cwnd 设定为 1而快恢复 cwnd 设定为 ssthresh。
<div align="center"> <img src="../pics//f61b5419-c94a-4df1-8d4d-aed9ae8cc6d5.png" width="600"/> </div><br>
# 六、应用层
## 域名系统
DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。
域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。
<div align="center"> <img src="../pics//b54eeb16-0b0e-484c-be62-306f57c40d77.jpg"/> </div><br>
DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传来保证可靠性。在两种情况下会使用 TCP 进行传输:
- 如果返回的响应超过的 512 字节就改用 TCP 进行传输UDP 最大只支持 512 字节的数据)。
- 区域传送需要使用 TCP 进行传输(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。
## 文件传送协议
FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:
- 控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。
- 数据连接:用来传送一个文件数据。
根据数据连接是否是服务器端主动建立FTP 有主动和被动两种模式:
- 主动模式:服务器端主动建立数据连接,其中服务器端的端口号为 20客户端的端口号随机但是必须大于 1024因为 0\~1023 是熟知端口号。
<div align="center"> <img src="../pics//03f47940-3843-4b51-9e42-5dcaff44858b.jpg"/> </div><br>
- 被动模式:客户端主动建立数据连接,其中客户端的端口号由客户端自己指定,服务器端的端口号随机。
<div align="center"> <img src="../pics//be5c2c61-86d2-4dba-a289-b48ea23219de.jpg"/> </div><br>
主动模式要求客户端开放端口号给服务器端,需要去配置客户端的防火墙。被动模式只需要服务器端开放端口号即可,无需客户端配置防火墙。但是被动模式会导致服务器端的安全性减弱,因为开放了过多的端口号。
## 动态主机配置协议
DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要去手动配置 IP 地址等信息。
DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。
DHCP 工作过程如下:
1. 客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67源地址为 0.0.0.0:68被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。
2. DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。
3. 如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。
4. DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。
<div align="center"> <img src="../pics//bf16c541-0717-473b-b75d-4115864f4fbf.jpg"/> </div><br>
## 远程登录协议
TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。
TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。
## 电子邮件协议
一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。
邮件协议包含发送协议和读取协议,发送协议常用 SMTP读取协议常用 POP3 和 IMAP。
<div align="center"> <img src="../pics//7b3efa99-d306-4982-8cfb-e7153c33aab4.png" width="700"/> </div><br>
### 1. SMTP
SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP而是增加邮件主体的结构定义了非 ASCII 码的编码规则。
<div align="center"> <img src="../pics//ed5522bb-3a60-481c-8654-43e7195a48fe.png" width=""/> </div><br>
### 2. POP3
POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。
### 3. IMAP
IMAP 协议中客户端和服务器上的邮件保持同步如果不去手动删除邮件那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。
## 常用端口
|应用| 应用层协议 | 端口号 | 运输层协议 | 备注 |
| :---: | :--: | :--: | :--: | :--:
| 域名解析 | DNS | 53 | UDP/TCP | 长度超过 512 字节时使用 TCP |
| 动态主机配置协议 | DHCP | 67/68 | UDP | |
| 简单网络管理协议 | SNMP | 161/162 | UDP | |
| 文件传送协议 | FTP | 20/21 | TCP | 控制连接 21数据连接 20
| 远程终端协议 | TELNET | 23 | TCP | |
| 超文本传送协议 | HTTP | 80 | TCP | |
| 简单邮件传送协议 | SMTP | 25 | TCP | |
| 邮件读取协议 | POP3 | 110 | TCP | |
| 网际报文存取协议 | IMAP | 143 | TCP | |
## Web 页面请求过程
### 1. DHCP 配置主机信息
- 假设主机最开始没有 IP 地址以及其它信息,那么就需要先使用 DHCP 来获取。
- 主机生成一个 DHCP 请求报文,并将这个报文放入具有目的端口 67 和源端口 68 的 UDP 报文段中。
- 该报文段则被放入在一个具有广播 IP 目的地址(255.255.255.255) 和源 IP 地址0.0.0.0)的 IP 数据报中。
- 该数据报则被放置在 MAC 帧中,该帧具有目的地址 FF:FF:FF:FF:FF:FF将广播到与交换机连接的所有设备。
- 连接在交换机的 DHCP 服务器收到广播帧之后,不断地向上分解得到 IP 数据报、UDP 报文段、DHCP 请求报文,之后生成 DHCP ACK 报文该报文包含以下信息IP 地址、DNS 服务器的 IP 地址、默认网关路由器的 IP 地址和子网掩码。该报文被放入 UDP 报文段中UDP 报文段有被放入 IP 数据报中,最后放入 MAC 帧中。
- 该帧的目的地址是请求主机的 MAC 地址,因为交换机具有自学习能力,之前主机发送了广播帧之后就记录了 MAC 地址到其转发接口的交换表项,因此现在交换机就可以直接知道应该向哪个接口发送该帧。
- 主机收到该帧后,不断分解得到 DHCP 报文。之后就配置它的 IP 地址、子网掩码和 DNS 服务器的 IP 地址,并在其 IP 转发表中安装默认网关。
### 2. ARP 解析 MAC 地址
- 主机通过浏览器生成一个 TCP 套接字,套接字向 HTTP 服务器发送 HTTP 请求。为了生成该套接字,主机需要知道网站的域名对应的 IP 地址。
- 主机生成一个 DNS 查询报文,该报文具有 53 号端口,因为 DNS 服务器的端口号是 53。
- 该 DNS 查询报文被放入目的地址为 DNS 服务器 IP 地址的 IP 数据报中。
- 该 IP 数据报被放入一个以太网帧中,该帧将发送到网关路由器。
- DHCP 过程只知道网关路由器的 IP 地址,为了获取网关路由器的 MAC 地址,需要使用 ARP 协议。
- 主机生成一个包含目的地址为网关路由器 IP 地址的 ARP 查询报文,将该 ARP 查询报文放入一个具有广播目的地址FF:FF:FF:FF:FF:FF的以太网帧中并向交换机发送该以太网帧交换机将该帧转发给所有的连接设备包括网关路由器。
- 网关路由器接收到该帧后,不断向上分解得到 ARP 报文,发现其中的 IP 地址与其接口的 IP 地址匹配,因此就发送一个 ARP 回答报文,包含了它的 MAC 地址,发回给主机。
### 3. DNS 解析域名
- 知道了网关路由器的 MAC 地址之后,就可以继续 DNS 的解析过程了。
- 网关路由器接收到包含 DNS 查询报文的以太网帧后,抽取出 IP 数据报,并根据转发表决定该 IP 数据报应该转发的路由器。
- 因为路由器具有内部网关协议RIP、OSPF和外部网关协议BGP这两种路由选择协议因此路由表中已经配置了网关路由器到达 DNS 服务器的路由表项。
- 到达 DNS 服务器之后DNS 服务器抽取出 DNS 查询报文,并在 DNS 数据库中查找待解析的域名。
- 找到 DNS 记录之后,发送 DNS 回答报文,将该回答报文放入 UDP 报文段中,然后放入 IP 数据报中,通过路由器反向转发回网关路由器,并经过以太网交换机到达主机。
### 4. HTTP 请求页面
- 有了 HTTP 服务器的 IP 地址之后,主机就能够生成 TCP 套接字,该套接字将用于向 Web 服务器发送 HTTP GET 报文。
- 在生成 TCP 套接字之前,必须先与 HTTP 服务器进行三次握手来建立连接。生成一个具有目的端口 80 的 TCP SYN 报文段,并向 HTTP 服务器发送该报文段。
- HTTP 服务器收到该报文段之后,生成 TCP SYN ACK 报文段,发回给主机。
- 连接建立之后,浏览器生成 HTTP GET 报文,并交付给 HTTP 服务器。
- HTTP 服务器从 TCP 套接字读取 HTTP GET 报文,生成一个 HTTP 响应报文,将 Web 页面内容放入报文主体中,发回给主机。
- 浏览器收到 HTTP 响应报文后,抽取出 Web 页面内容,之后进行渲染,显示 Web 页面。
# 参考资料
- 计算机网络, 谢希仁
- JamesF.Kurose, KeithW.Ross, 库罗斯, 等. 计算机网络: 自顶向下方法 [M]. 机械工业出版社, 2014.
- W.RichardStevens. TCP/IP 详解. 卷 1, 协议 [M]. 机械工业出版社, 2006.
- [Active vs Passive FTP Mode: Which One is More Secure?](https://securitywing.com/active-vs-passive-ftp-mode/)
- [Active and Passive FTP Transfers Defined - KB Article #1138](http://www.serv-u.com/kb/1138/active-and-passive-ftp-transfers-defined)
- [Traceroute](https://zh.wikipedia.org/wiki/Traceroute)
- [ping](https://zh.wikipedia.org/wiki/Ping)
- [How DHCP works and DHCP Interview Questions and Answers](http://webcache.googleusercontent.com/search?q=cache:http://anandgiria.blogspot.com/2013/09/windows-dhcp-interview-questions-and.html)
- [What is process of DORA in DHCP?](https://www.quora.com/What-is-process-of-DORA-in-DHCP)
- [What is DHCP Server ?](https://tecadmin.net/what-is-dhcp-server/)
- [Tackling emissions targets in Tokyo](http://www.climatechangenews.com/2011/html/university-tokyo.html)
- [What does my ISP know when I use Tor?](http://www.climatechangenews.com/2011/html/university-tokyo.html)
- [Technology-Computer Networking[1]-Computer Networks and the Internet](http://www.linyibin.cn/2017/02/12/technology-ComputerNetworking-Internet/)
- [P2P 网络概述.](http://slidesplayer.com/slide/11616167/)
- [Circuit Switching (a) Circuit switching. (b) Packet switching.](http://slideplayer.com/slide/5115386/)
>>>>>>> 8c67483fe9ac599aa6f6358fb32a541d7e279666

File diff suppressed because it is too large Load Diff

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 83 KiB

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

@ -0,0 +1,37 @@
# LeetCode 面试必备
- 💪 就是干如果你觉得有帮助请点个star谢谢
> **欢迎任何人参与和完善:一个人可以走的很快,但是一群人却可以走的更远**
## LeetCode 习题集合
* [LeetCode 解题集合](https://github.com/apachecn/leetcode/docs/Leetcode_Solutions)
## 模版要求
> 提交PR基本要求满足任意一种即可
* 1. 不一样的思路
* 2. 优化时间复杂度和空间复杂度或者解决题目的Follow up
* 3. 有意义的简化代码
* 4. 未提交过的题目
> **案例模版**
[模版007. Reverse Integer 反转整数](https://github.com/apachecn/leetcode/docs/Leetcode_Solutions/007._Reverse_Integer.md)
## 项目贡献者
> 项目发起人
* [@Lisanaaa](https://github.com/Lisanaaa)
* [@片刻](https://github.com/jiangzhonglian)
> 贡献者(欢迎大家来追加)
* [@Lisanaaa](https://github.com/Lisanaaa)
* [@片刻](https://github.com/jiangzhonglian)
* [@小瑶](https://github.com/chenyyx)

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB