Merge branch 'master' of github.com:heibaiying/Full-Stack-Notes

This commit is contained in:
luoxiang 2020-05-21 07:28:04 +08:00
commit f039b42ea7
116 changed files with 4096 additions and 117 deletions

223
CREADME.md Normal file
View File

@ -0,0 +1,223 @@
# Full-Stack-Notes
<div align="center"> <img width="380px" src="pictures/full-stack-notes-logo.png"/> </div>
<br/>
<div align="center">
<img src="pictures/芽.png"/>
<strong>一个处于萌芽阶段的知识库,用于持续分享自己的所见、所学、所思!</strong>
</div>
## :coffee: JAVA
1. [Java 反射与注解](notes/Java_反射与注解.md)
反射机制、自定义注解、@Target@Retention、注解的继承
2. [Java 并发编程](notes/Java_并发编程.md)
非原子性协定、计算机多级高速缓存、缓存一致性协议、写缓冲与无效化队列、内存屏障、锁机制、无锁 CAS、线程池
3. [Java NIO 核心组件详解](notes/Java_NIO.md)
缓冲区 Buffer、通道 Channel、选择器 Selector、实现多人聊天室
4. [Java 函数式编程](notes/Java_函数式编程.md)
Lambda 表达式、函数式接口、流、收集器、并行流
5. [Java 设计模式](notes/Java_设计模式.md)
软件设计原则、单例模式(使用序列化和反射破坏单例、防御序列化和反射攻击、枚举类单例)等 23 种设计模式
6. [Java 虚拟机](notes/Java_虚拟机.md)
Java 内存区域、垃圾收集算法、经典垃圾收集器、双亲委派模型、分层编译、热点代码探测、方法内联、逃逸分析
7. [JVM 性能监控之命令行工具](notes/JVM_性能监控之命令行工具.md)
jps 命令、jstat 命令、jinfo 命令、jmap 命令、jhat 命令、jstack 命令
8. [JVM 性能监控之可视化工具](notes/JVM_性能监控之可视化工具.md)
JConsole、VisualVM 、监控本地进程、监控远程进程
9. [Tomcat 架构解析](notes/Tomcat_架构解析.md)
核心组件、连接器、多层容器、请求处理流程、程序启动过程、类加载器
10. Java 集合类源码解析
<br/>
## :globe_with_meridians: 网络基础
1. [计算机网络模型](notes/计算机网络.md)
四层、五层、七层计算机网络模型、信道复用、PPP 协议、ARP 协议、划分子网与构成超网、TCP 三次握手与四次挥手
3. [HTTPS 协议详解](notes/HTTPS.md)
3. [使用 Wireshark 分析 HTTPS 握手过程](notes/WireShark_HTTPS.md)
<br/>
## :computer: 前端基础
1. [JavaScript 基础](notes/JavaScript_基础.md)
基本数据类型、引用类型、内置对象Global 与 window、作用域与闭包、对象设计
2. [ECMAScript 6.0 基础](notes/ES6_基础.md)
变量声明、对象字面量、对象解构、Symbol、迭代器与生成器、类、代理与反射、模块化
4. JavaScript 设计模式
<br/>
## :dolphin: 数据库
### MySQL
1. [MySQL 核心概念](notes/MySQL_基础.md)
B+ Tree 树、聚集索引和非聚集索引、共享锁与排他锁、意向共享锁与意向排它锁、一致性锁定读与一致性非锁定读
2. [MySQL 备份详解](notes/MySQL_备份.md)
备份类型、mysqldump 备份、mysqlpump 备份、Xtrabackup 备份、二进制日志备份
3. [MySQL 复制详解](notes/MySQL_复制.md)
基于二进制日志的复制、基于 GTID 的复制、半同步复制、高可用架构 MMM 和 MHA
4. [MySQL 高可用架构之 PXC 集群](notes/MySQL_PXC集群.md)
5. [MyCat 读写分离与分库分表](notes/MySQL_Mycat中间件.md)
6. [MySQL 查询性能分析之 Explain](notes/MySQL_EXPLAIN.md)
### Redis
1. [Redis 基本数据类型和常用命令](notes/Redis_数据类型和常用命令.md)
2. [Redis AOF 和 RDB 持久化策略原理](notes/Redis_持久化.md)
3. [Redis 哨兵模式](notes/Redis_哨兵模式.md)
复制机制、哨兵模式架构说明、哨兵模式搭建
4. [Redis 集群模式](notes/Redis_集群模式.md)
数据分区、节点通信、请求路由、故障发现与恢复、集群扩容与缩容
5. [Redis 分布式锁原理](notes/Redis_分布式锁原理.md)
分布式锁原理、单机模式下的分布式锁、集群模式下的分布式锁、RedLock 原理、Redisson
### MongoDB
1. [MongoDB 基础](notes/MongoDB_基础.md)
2. [MongoDB 索引](notes/MongoDB_索引.md)
单字段索引、复合索引、多键索引、哈希所有、地理空间索引、文本索引唯一索引、稀疏索引、部分索引、TTL 索引
3. [MongoDB 聚合](notes/MongoDB_聚合.md)
常用聚合管道、单用途聚合方法、MapReduce
4. [MongoDB 复制](notes/MongoDB_复制.md)
复制功能、故障发现、优先选举、投票成员、副本集搭建
5. [MongoDB 分片](notes/MongoDB_分片.md)
分片副本集配置、配置副本集配置、路由服务配置
<br/>
## :whale: 系统与容器
1. [Linux 常用 Shell 命令](notes/Linux_常用Shell命令.md)
2. [Sehll 脚本编程基础](notes/Shell_基础.md)
创建脚本、分支语句、循环语句、处理用户输入、处理用户输出、创建函数、处理信号、定时作业
3. [Docker 基础](notes/Docker_基础.md)
核心概念镜像、容器、仓库、Docker 常用命令、DockerFile 常用指令
<br/>
## :package: 常用技术栈
### RabbitMQ
1. [RabbitMQ 核心概念](notes/RabbitMQ_基础.md)
2. [RabbitMQ 客户端开发](notes/RabbitMQ_客户端开发.md)
3. [HAProxy + KeepAlived 搭建 RabbitMQ 高可用集群](notes/RabbitMQ_高可用集群架构.md)
### Nginx
1. [Nginx 基础之静态网站部署,负载均衡,动静分离](notes/Nginx_基础.md)
2. HTTP 模块详解
### Kafka
1. [Kafka 简介](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka简介.md)
2. [基于 Zookeeper 搭建 Kafka 高可用集群](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/基于Zookeeper搭建Kafka高可用集群.md)
3. [Kafka 生产者详解](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka生产者详解.md)
4. [Kafka 消费者详解](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka消费者详解.md)
5. [深入理解 Kafka 副本机制](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Kafka深入理解分区副本机制.md)
### ZooKeeper
1. [ZooKeeper 简介及核心概念](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper简介及核心概念.md)
2. [ZooKeeper 单机环境和集群环境搭建](https://github.com/heibaiying/BigData-Notes/blob/master/notes/installation/Zookeeper单机环境和集群环境搭建.md)
3. [ZooKeeper 常用 Shell 命令](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper常用Shell命令.md)
4. [ZooKeeper Java 客户端](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper_Java客户端Curator.md)
5. [ZooKeeper ACL 权限控制](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper_ACL权限控制.md)
6. [ZooKeeper 分布式锁原理](notes/ZooKeeper_分布式锁原理.md)
## ElasticSearch
TODO
<br/>
## :bullettrain_side: 微服务与分布式
1. [CAP 理论 和 BASE 理论](notes/CAP理论和BASE理论.md)
<br/>
## :hammer_and_wrench: 常用软件安装
1. [Redis 单机环境安装](notes/installation/Redis单机环境搭建.md)
2. [RabbitMQ 单机环境安装](notes/installation/RabbitMQ单机环境搭建.md)
3. [Nginx 单机环境安装](notes/installation/Nginx编译方式安装.md)
4. [MySQL 单机环境安装](notes/installation/MySQL单机环境搭建.md)
5. [MongoDB 单机环境安装](notes/installation/MongoDB单机环境搭建.md)
6. [ElasticSearch + Kibana 单机环境安装](notes/installation/ElasticSearch+Kibana单机环境搭建.md)
<br>
<div align="center"> <img width="200px" src="pictures/blog-logo.png"/> </div>
<div align="center"> <a href = "https://blog.csdn.net/m0_37809146"> 欢迎关注我的博客https://blog.csdn.net/m0_37809146</a> </div>

135
README.md
View File

@ -8,73 +8,40 @@
</div>
## :coffee: JAVA
1. [Java 反射与注解](notes/Java_反射与注解.md)
反射机制、自定义注解、@Target@Retention、注解的继承
2. [Java 并发编程](notes/Java_并发编程.md)
非原子性协定、计算机多级高速缓存、缓存一致性协议、写缓冲与无效化队列、内存屏障、锁机制、无锁 CAS、线程池
3. [Java 设计模式](notes/Java_设计模式.md)
软件设计原则、单例模式(使用序列化和反射破坏单例、防御序列化和反射攻击、枚举类单例)等 23 种设计模式
4. [Java 虚拟机](notes/Java_虚拟机.md)
Java 内存区域、垃圾收集算法、经典垃圾收集器、双亲委派模型、分层编译、热点代码探测、方法内联、逃逸分析
5. Jvm 监控工具
6. Java 8 函数式编程
7. 静态代理与动态代理
8. IO、NIO、AIO
9. Socket 网络编程系列
10. 高性能网络框架 Netty
11. Java 集合类源码解析
12. Tomcat 架构解析
<br/>
## :globe_with_meridians: 计算机与网络基础
<p align="right"><a href="../master/CREADME.md">点击切换详细目录</a></p>
## :coffee: JAVA
1. [Java 反射与注解](notes/Java_反射与注解.md)
2. [Java 并发编程](notes/Java_并发编程.md)
3. [Java NIO 核心组件详解](notes/Java_NIO.md)
4. [Java 函数式编程](notes/Java_函数式编程.md)
5. [Java 设计模式](notes/Java_设计模式.md)
6. [Java 虚拟机](notes/Java_虚拟机.md)
7. [JVM 性能监控之命令行工具](notes/JVM_性能监控之命令行工具.md)
8. [JVM 性能监控之可视化工具](notes/JVM_性能监控之可视化工具.md)
9. [Tomcat 架构解析](notes/Tomcat_架构解析.md)
10. Java 集合类源码解析
<br/>
## :globe_with_meridians: 网络基础
1. [计算机网络模型](notes/计算机网络.md)
2. [HTTPS 协议详解](notes/HTTPS.md)
3. [使用 Wireshark 分析 HTTPS 握手过程](notes/WireShark_HTTPS.md)
四层、五层、七层计算机网络模型、信道复用、PPP 协议、ARP 协议、划分子网与构成超网、TCP 三次握手与四次挥手
2. HTTP 协议详解
3. HTTPS 协议详解
4. 抓包神器 Wireshark
5. 计算机组成原理
<br/>
## :computer: 前端基础
1. [JavaScript 基础](notes/JavaScript_基础.md)
基本数据类型、引用类型、内置对象Global 与 window、作用域与闭包、对象设计
2. [ECMAScript 6.0 基础](notes/ES6_基础.md)
变量声明、对象字面量、对象解构、Symbol、迭代器与生成器、类、代理与反射、模块化
3. CSS 基础
选择器、非局部样式、布局样式、效果属性、CSS 动画
4. JavaScript 设计模式
3. JavaScript 设计模式
<br/>
@ -83,74 +50,37 @@
### MySQL
1. [MySQL 核心概念](notes/MySQL_基础.md)
B+ Tree 树、聚集索引和非聚集索引、共享锁与排他锁、意向共享锁与意向排它锁、一致性锁定读与一致性非锁定读
2. [MySQL 备份详解](notes/MySQL_备份.md)
备份类型、mysqldump 备份、mysqlpump 备份、Xtrabackup 备份、二进制日志备份
3. [MySQL 复制详解](notes/MySQL_复制.md)
基于二进制日志的复制、基于 GTID 的复制、半同步复制、高可用架构 MMM 和 MHA
4. [MySQL 高可用架构之 PXC 集群](notes/MySQL_PXC集群.md)
5. [MyCat 读写分离与分库分表](notes/MySQL_Mycat中间件.md)
6. [MySQL 查询性能分析之 Explain](notes/MySQL_EXPLAIN.md)
### Redis
1. [Redis 基本数据类型和常用命令](notes/Redis_数据类型和常用命令.md)
2. [Redis AOF 和 RDB 持久化策略原理](notes/Redis_持久化.md)
3. [Redis 哨兵模式](notes/Redis_哨兵模式.md)
复制机制、哨兵模式架构说明、哨兵模式搭建
4. [Redis 集群模式](notes/Redis_集群模式.md)
数据分区、节点通信、请求路由、故障发现与恢复、集群扩容与缩容
5. 使用 Redis 实现分布式锁
5. [Redis 分布式锁原理](notes/Redis_分布式锁原理.md)
### MongoDB
1. [MongoDB 基础](notes/MongoDB_基础.md)
2. [MongoDB 索引](notes/MongoDB_索引.md)
单字段索引、复合索引、多键索引、哈希所有、地理空间索引、文本索引唯一索引、稀疏索引、部分索引、TTL 索引
3. [MongoDB 聚合](notes/MongoDB_聚合.md)
常用聚合管道、单用途聚合方法、MapReduce
4. [MongoDB 复制](notes/MongoDB_复制.md)
复制功能、故障发现、优先选举、投票成员、副本集搭建
5. [MongoDB 分片](notes/MongoDB_分片.md)
分片副本集配置、配置副本集配置、路由服务配置
<br/>
## :whale: 系统与容器
1. [Linux 常用 Shell 命令](notes/Linux_常用Shell命令.md)
2. [Sehll 脚本编程基础](notes/Shell_基础.md)
创建脚本、分支语句、循环语句、处理用户输入、处理用户输出、创建函数、处理信号、定时作业
3. [Docker 基础](notes/Docker_基础.md)
核心概念镜像、容器、仓库、Docker 常用命令、DockerFile 常用指令
<br/>
## :package: 常用技术栈
@ -159,16 +89,13 @@
### RabbitMQ
1. [RabbitMQ 核心概念](notes/RabbitMQ_基础.md)
2. [RabbitMQ 客户端开发](notes/RabbitMQ_客户端开发.md)
3. [HAProxy + KeepAlived 搭建 RabbitMQ 高可用集群](notes/RabbitMQ_高可用集群架构.md)
### Nginx
1. [Nginx 基础之静态网站部署,负载均衡,动静分离](notes/Nginx_基础.md)
2. HTTP 模块详解
3. Nginx 性能优化
### Kafka
@ -187,29 +114,23 @@
3. [ZooKeeper 常用 Shell 命令](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper常用Shell命令.md)
4. [ZooKeeper Java 客户端](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper_Java客户端Curator.md)
5. [ZooKeeper ACL 权限控制](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper_ACL权限控制.md)
6. 使用 ZooKeeper 实现分布式锁
6. [ZooKeeper 分布式锁原理](notes/ZooKeeper_分布式锁原理.md)
## ElasticSearch
TODO
+ TODO
<br/>
## :rocket: 测试与运维
1. 性能测试之 Jmeter
2. 性能测试之 LoadRunner
3. Jenkins 持续交付与自动化部署
<br/>
## :bullettrain_side: 微服务与分布式
1. 分布式锁的实现
2. 分布式选举算法
3. 分布式事务实现原理
4. 分布式全局 ID 的生成
5. CAP 理论和 BASE 理论
1. [CAP 理论 和 BASE 理论](notes/CAP理论和BASE理论.md)
<br/>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>nio-tutorial</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<target>1.8</target>
<source>1.8</source>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,133 @@
package chat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.Set;
public class ChatClient {
private String hostname;
private int port;
private Selector selector;
private final ByteBuffer rBuffer = ByteBuffer.allocate(1024);
private final ByteBuffer wBuffer = ByteBuffer.allocate(1024);
ChatClient(String hostname, int port) {
this.hostname = hostname;
this.port = port;
}
public void start() {
try {
// 创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(hostname, port));
// 创建selector
selector = Selector.open();
// 注册监听CONNECT事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 持续调用select
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
// 处理Connect事件
if (selectionKey.isConnectable()) {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
// 有可能正处于连接中的状态
if (clientChannel.isConnectionPending()) {
// 等待连接完成
clientChannel.finishConnect();
// 开始监听用户输入
inputListening(clientChannel);
// 为clientChannel注册上Read
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("尊敬的用户" + clientChannel.socket().getLocalPort() + ", 你已成功加入群聊!");
}
// 处理Read事件
} else if (selectionKey.isReadable()) {
handleMessage(selectionKey);
}
}
}
} catch (ClosedSelectorException e) {
// 主动关闭客户端不做任何处理
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 监听用户来自控制台的输入
*
* @param clientChannel 客户端Channel
*/
private void inputListening(SocketChannel clientChannel) {
// 要持续监听用户输入但又不能阻塞主线程所以需要一个单独的线程来完成
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
try {
while (scanner.hasNextLine()) {
String input = scanner.nextLine();
if (!input.isEmpty()) {
if ("exit".equals(input)) {
selector.close();
return;
}
wBuffer.put(input.getBytes());
wBuffer.flip();
while (wBuffer.hasRemaining()) {
clientChannel.write(wBuffer);
}
wBuffer.clear();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
/**
* 处理来自其他客户端的消息
*
* @param selectionKey 处于Read状态的selectionKey
*/
private void handleMessage(SelectionKey selectionKey) {
try {
SocketChannel channel = (SocketChannel) selectionKey.channel();
StringBuilder buffer = new StringBuilder();
while (channel.read(rBuffer) > 0) {
rBuffer.flip();
buffer.append(StandardCharsets.UTF_8.decode(rBuffer));
rBuffer.clear();
}
System.out.println(buffer.toString());
} catch (IOException e) {
selectionKey.cancel();
selector.wakeup();
System.out.println("聊天室服务器已关闭!");
}
}
public static void main(String[] args) {
new ChatClient("127.0.0.1", 8888).start();
}
}

View File

@ -0,0 +1,134 @@
package chat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Set;
public class ChatServer {
private String hostname;
private int port;
private Selector selector;
private final ByteBuffer rBuffer = ByteBuffer.allocate(1024);
private final ByteBuffer wBuffer = ByteBuffer.allocate(1024);
ChatServer(int port) {
this("127.0.0.1", port);
}
ChatServer(String hostname, int port) {
this.hostname = hostname;
this.port = port;
}
public void start() {
try {
// 创建ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(hostname, port));
// 创建selector
selector = Selector.open();
// 注册监听CONNECT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 持续调用select()
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
// 处理Accept事件
if (selectionKey.isAcceptable()) {
// 获取ServerSocketChannel
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
// 通过ServerSocketChannel获取SocketChannel
SocketChannel clientChannel = server.accept();
clientChannel.configureBlocking(false);
// 注册Read事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("欢迎" + clientChannel.socket().getPort() + "加入聊天室!");
// 处理Readable事件
} else if (selectionKey.isReadable()) {
handleMessage(selectionKey);
}
}
selectionKeys.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭selector后与之相关的所有资源都会被释放
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理客户端消息
*
* @param selectionKey 处于Read状态的SelectionKey
*/
private void handleMessage(SelectionKey selectionKey) {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
try {
// 获取来自客户端的消息
StringBuilder buffer = new StringBuilder();
while (clientChannel.read(rBuffer) > 0) {
rBuffer.flip();
buffer.append(StandardCharsets.UTF_8.decode(rBuffer));
rBuffer.clear();
}
String msg = buffer.toString();
// 将消息发送给其他客户端
broadcastMessage(clientChannel, msg);
} catch (IOException e) {
// 该异常由clientChannel.read(rBuffer)方法抛出如果出现该异常则说明clientChannel已经关闭
// 此时需要调用cancel()取消注册在selector上的事件
selectionKey.cancel();
// 在多线程环境下如果另一个线程正在阻塞地调用select()因为事件集已经改变因此需要通过wakeup()让其立刻返回并重新select()
selector.wakeup();
System.out.println("用户" + clientChannel.socket().getPort() + "退出聊天室!");
}
}
/**
* 将客户端发来的消息广播给其他客户端
*
* @param clientChannel 消息源
* @param msg 消息
*/
private void broadcastMessage(SocketChannel clientChannel, String msg) {
try {
// 遍历当前selector上所有channel
for (SelectionKey selectionKey : selector.keys()) {
SelectableChannel channel = selectionKey.channel();
// 消息不需要转发给ServerSocketChannel和当前客户端自己
if (selectionKey.isValid() && !(channel instanceof ServerSocketChannel) && !channel.equals(clientChannel)) {
SocketChannel otherClient = (SocketChannel) channel;
wBuffer.put(("用户" + clientChannel.socket().getPort() + ": " + msg).getBytes());
while (wBuffer.hasRemaining()) {
wBuffer.flip();
otherClient.write(wBuffer);
}
wBuffer.clear();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ChatServer(8888).start();
}
}

View File

@ -0,0 +1,82 @@
# CAP 理论 和 BASE 理论
## 一、CAP 理论
### 1.1 基本概念
#### 1. 一致性
在分布式环境中,一致性是指数据在多个节点之间能够保持一致的特性。如果在某个节点上执行变更操作后,用户可以立即从其他任意节点上读取到变更后的数据,那么就认为这样的系统具备强一致性。
#### 2. 可用性
可以性是指系统提供的服务必须一直处于可用状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。它主要强调以下两点:
+ **有限的时间内**:对于用户的一个请求操作,系统必须要在指定的时间内返回处理结果,如果超过这个时间,那么系统就被认为是不可用的。
+ **返回结果**:不论成功或者失败,都需要明确地返回响应结果。
#### 3. 分区容错性
分区容错性指定是分布式系统在遇到网络分区时,仍需要能够对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。
这里的网络分区指的是:在分布式系统中,由于不同的节点会分布在不同子网中(不同机房或异地网络等),由于一些特殊的原因,可能会出现子网内部是正常的,但子网彼此之间却无法正常通信,从而导致整个系统的网络被切分成若干个独立的区域,这就是网络分区。
### 1.2 CAP 理论
CAP 理论强调一个分布式系统不可能同时满足一致性CConsistency、可用性AAvailability和分区容错性PPartition tolerance这三个需求最多只能同时满足其中的两个。这里我们来进行一下解释说明
首先对于一个分布式系统而言,网络分区是不可避免的,不可能永远不出现网络故障,所以分区容错性 P 必须要保证。假设一个分布式系统中出现了网络分区,如下:
<div align="center"> <img src="../pictures/cap_示例.png"/> </div>
假设用户 1 向节点 1 上增加了 10 个数据,但节点 1 和节点 2 之间因为网络分区而无法进行数据同步,碰巧用户 2 此时发起了查询请求,此时有两种处理方案:
+ **放弃 A保证 C**:即对于用户 2 的查询返回失败,直至节点 1 上的数据同步至节点 2两者的数据都变为 60 为止;
+ **放弃 C保证 A**:对于本次的查询直接返回原来的数据 50此时放弃了一致性但保证了可用性。待网络恢复后仍然需要将节点 1 上的数据同步至节点 2。
可以看到无论如何,都是无法既保证 A ,又保证 C 的。
### 1.3 选择策略
因为 CAP 理论不能将一致性、可用性和分区容错性都满足,所以需要根据不同系统的特性进行取舍,主要分为以下三种情况:
+ **保证 AC ,放弃 P**:这种情况下可以将所有数据(或者是与事务相关的数据)都放在一个分布式节点上,这样可以避免网络分区带来的影响,但同时也意味着放弃了系统的可扩展性,如单机版本的 MySQL、Oracle 等。
+ **保证 CP ,放弃 A**:这种情况下如果发生了网络分区故障,此时节点间的数据就无法同步。因此在故障修复前都需要放弃对外提供服务,直至网络恢复,数据到达一致为止。
+ **保证 AP ,放弃 C**:这种情况相当于放弃一致性。具体而言,是放弃数据的强一致性,但保证数据的最终一致性。因为不论是什么系统,数据最终都需要保持一致,否则整个系统就无法使用。在这种策略下,在某个短暂的时间窗口内会存在数据不一致的情况。
<div align="center"> <img src="../pictures/cap理论.jpg"/> </div>
## 二、BASE 理论
BASE是对基本可用Basically Available、软状态 Soft State、最终一致性 Eventually Consistent三个短语的简写它是对 CAP 理论中 AP 策略的延伸。其核心是即便无法做到强一致性,但每个系统应用都应该根据自身业务的特点,采取适当的方式来保证系统的最终一致性,而具体的方案就体现在这三个短语上:
#### 1. 基本可用
基本可用是指分布式系统在出现不可预知的故障时,允许损失部分可用性,例如:
+ 延长响应时间:比如原来的的查询只需要 0.5 秒,现在延长到 1 2 秒;
+ 服务降级:比如在商品秒杀时,部分用户会被引导到一个降级页面。
#### 2. 软状态
软状态也称为弱状态,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统整体的可用性,即允许不同节点间的数据同步存在延时。
#### 3. 最终一致性
最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终需要达到一致的状态。
## 参考资料
主要摘录自:倪超 . 从 Paxos 到 Zookeeper——分布式一致性原理与实践 . 电子工业出版社 . 2015-02-01

209
notes/HTTPS.md Normal file
View File

@ -0,0 +1,209 @@
# HTTPS
<nav>
<a href="#一基本概念">一、基本概念</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-SSL">1.1 SSL</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-TLS">1.2 TLS</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#13-OpenSSL">1.3 OpenSSL</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#14-HTTPS">1.4 HTTPS</a><br/>
<a href="#二数据安全">二、数据安全</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-非对称加密">2.1 非对称加密</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22-对称加密">2.2 对称加密</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#23-摘要算法">2.3 摘要算法</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#24-数字签名">2.4 数字签名</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#25-中间人攻击">2.5 中间人攻击</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#26-CA-证书">2.6 CA 证书</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#27-PKI-体系">2.7 PKI 体系</a><br/>
<a href="#三握手过程">三、握手过程</a><br/>
</nav>
## 一、基本概念
### 1.1 SSL
SSLSecure Sockets Layer安全套接层是一种安全协议目的是为互联网通信提供安全保障最早由网景公司Netscape推出。SSL 协议有三个版本,分别是 SSL v1、SSL v2、SSL v3
- v1 版本因为存在严重的安全漏洞,所以从未公开过。
- v2 版本在1995 年 2 月发布,但因为存在多个严重的安全漏洞而被 v3 版本替代。
- v3 版本在 1996 年发布,由网景公司全部进行重新设计。
### 1.2 TLS
1966 年IETFInternet Engineering Task Force国际互联网工程任务组在 SSL v3 的基础进一步进行了标准化,微软为这个新的协议取名为 TLS v1.0 ,这也就是 TLSTransport Layer Security安全传输层协议的由来。之后 TLS 继续发布了 v1.1v1.2v1.3 版本,当前主流的版本为 v1.2 。
### 1.3 OpenSSL
OpenSSL 是一个开源的底层密码库,封装了所有的密码学算法,并为 TLS/SSL 协议提供了完善的工具库,可以把它看做是 TLS/SSL 协议的具体实现。
### 1.4 HTTPS
HTTPS Hyper Text Transfer Protocol over SecureSocket Layer超文本传输安全协议是在 HTTP 的基础上通过 SSL/TLS 层来进行传输加密和身份认证,从而保证通讯的安全性。除此之外它的报文结构、请求方法等都完全沿用 HTTP 原有的模式,因此可以很方便地将原有 HTTP 服务转换为 HTTPS 服务。
![http+tls](../pictures/http+tls.png)
## 二、数据安全
具体而言HTTPS 主要通过以下几种方式来保证通信的安全:
### 2.1 非对称加密
非对称加密算法需要一对密钥公开密钥public key简称公钥和私有密钥private key简称私钥来进行加解密。用公钥加密的数据只有对应的私钥才能解密反之亦然。因为加密和解密使用的是两个不同的密钥从而叫作非对称加密算法。
HTTPS 的公钥可以由任意的客户端持有,但私钥只能由服务器持有,因此所有经过公钥加密的数据只能由对应的服务器解密。
<div align="center"> <img src="../pictures/https_非对称加密.png"/> </div>
### 2.2 对称加密
非对称加密算法通常是基于一些复杂的数学问题来实现的因此其加解密过程需要大量的运算对应的加解密时间也比较长。为了兼顾性能与安全两方面的考虑HTTPS 还引入了对称加密算法。
对称加密算法采用同一个密钥来进行信息的加密和解密因此也被称为单密钥加密算法其加解密速度都比较快。HTTPS 会在握手阶段采用非对称加密算法来生成对称加密所需的密钥,而之后的过程,因为要涉及到大量业务信息的传输,故都采用对称加密来提升性能。
<div align="center"> <img src="../pictures/https_对称加密.png"/> </div>
### 2.3 摘要算法
通过以上两步,在网络中传输的数据已经是密文,此时即便数据被窃取,窃取者也无法解析。但是窃取者仍然可以对密文进行截取和重发,进而改变用户预期的行为,此时需要引入摘要算法来对数据的完整性进行校验。
摘要算法是一种单向的,不需要密钥的算法,它可以将任意长度的输入转换为固定长度的输出。并且能够保证对于任意相同的输入在同样的摘要算法下总是输出相同的结果;反之,如果输入不相同,则采用相同摘要算法得到的结果必然不相同。
HTTPS 会为需要传输的数据生成摘要信息,然后将经过会话密钥加密后的摘要和数据传送给服务器。服务器进行解密后,首先需要使用同样的摘要算法为数据生成摘要,并与客户端传送过来的摘要进行比较;如果一致,则证明数据没有被篡改。
<div align="center"> <img src="../pictures/https_摘要会话密钥加密.png"/> </div>
### 2.4 数字签名
当服务器处理完客户端的请求后,需要将处理结果返回给客户端。此时服务器需要证明这些返回数据是由真正持有私钥的服务器返回的,而不是其它伪装服务器返回的。想要保证这一点,需要使用服务器独有的私钥对数据进行加密,这种行为也称为数字签名。
由于非对称性加密比较耗时,因此只会对返回数据的摘要进行加密。当客户端收到服务器的响应时,首先尝试使用公钥对加密后的摘要进行解密,如果解密失败,则表示是其他伪装服务器返回的;如果解密成功,则继续对数据的完整性进行校验。
<div align="center"> <img src="../pictures/https_摘要私钥加密.png"/> </div>
### 2.5 中间人攻击
经过以上步骤,数据的传输已经相当安全了,但此时还是无法抵御中间人攻击:
<div align="center"> <img src="../pictures/https_中间人攻击.png"/> </div>
简单来说,中间人攻击就是服务器传递给客户端的公钥可能被攻击者替换,进而丧失数据安全的一种攻击方式:
+ 首先,中间攻击者将服务器发送给客户端的公钥替换为自己的公钥;
+ 假设没有任何认证机制,那么此时客户端并不知道公钥被替换,仍然使用攻击者的公钥对数据进行加密;
+ 攻击者接收到加密数据后,使用攻击者自己的私钥进行解密,然后将解密后的数据用真正的公钥进行加密后发送给服务器,并接收响应。
想要解决中间人攻击,客户端首先需要保证自己拿到的公钥是正确的公钥,此时需要借助 CA 证书和 PKI 体系。
### 2.6 CA 证书
CACertification Authority是证书的签发机构它是公钥基础设施Public Key InfrastructurePKI的核心负责签发证书、认证证书和管理已颁发证书。如果用户想得到一份属于自己的证书他可以向 CA 机构提出申请。在 CA 判明申请者的身份后,便为他分配一个公钥,并将该公钥与申请者的身份信息绑在一起,然后为其签名,形成证书后颁发给申请者。
顶级的 CA 机构有 Symantec、Comodo、GeoTrust、DigiCert但是通常它们并不支持给厂商颁发证书而是授权给中间厂商由中间厂商再颁发给应用厂商。从而形成 CA 认证链:
<div align="center"> <img src="../pictures/https_证书链.png"/> </div>
就 CA 证书类型而言,其分为以下三类:
- **DV (Domain Validation)**:最基础的域名型证书,只需要验证域名的所有权,特点是颁发速度快,但不知道所属者的身份。
- **OV (Organization Validation)**:面向企业用户,需要验证网站所属企业的真实身份。
- **EV (Extended Validation)**:是目前最高信任级别的 SSL 证书。按照国际规范审查网站企业身份和域名所有权,确保网站身份的真实可靠。
通过点击浏览器上锁图标可以查看对应网站的证书状态,示例如下:
<div align="center"> <img src="../pictures/http_baidu_ov.png"/> </div>
<div align="center"> <img src="../pictures/http_baidu_ov_证书.png"/> </div>
除了以上三种证书外,你还可以使用 Openssl 生成自签名证书,但由于自签名证书并不在整个认证链上,所以其通常不被浏览器所信任,访问时会出现以下警告:
<div align="center"> <img src="../pictures/https_非安全的连接.png"/> </div>
### 2.7 PKI 体系
PKIPublic Key Infrastructure公开密钥基础建设系统由数字证书、认证中心CA、证书资料库、证书吊销系统、密钥备份及恢复系统、PKI 应用接口系统等多个部分共同组成,其目的在于创造、管理、分配、使用、存储以及撤销数字证书。
以 CRLCertificate revocation list证书吊销列表为例它主要用于列出所有不再被信任的证书。如果一个证书所属单位不再被信任则该证书将被撤销并添加到 CRL 中。CRL 由 CA 生成并定期更新,以保证整个证书认证体系的持续可靠。
## 三、握手过程
`TLS v1.2` 版本中采用 ECDHE 算法的握手过程为例,其具体流程如下:
<div align="center"> <img src="../pictures/https_tsl_v1.2_ECDHE.jpg"/> </div>
1. TLS/SSL 协议是基于 TCP/IP 协议的,因此需要先等待 TCP 三次握手完成;
2. TCP 握手完成后,客户端首先发起一个 `Client Hello` 请求,里面包含客户端版本号、所有支持的密码套件、以及一个随机数 `Client Random`;
3. 服务器收到该请求后,会返回一个 `Server Hello` 消息,里面包含选中的用于本次通信的密码套件,以及一个随机数 `Server Random`
4. 紧接着,为了证明自己的身份,服务器需要将证书发送给客户端。客户端收到证书后,沿着证书链逐级向上验证,确保拿到的是有效的证书;
5. 之后服务器还会发送一个 `Server Key Exchange` 消息,里面包含了进行 ECDHE 算法所需的各种参数 `Server Params`
<div align="center"> <img src="../pictures/https_server_key_exchange.png"/> </div>
6. 客户端按照密码套件的要求,也生成一个 `Client Params`,并通过 `Client Key Exchange` 消息发送给服务器;
<div align="center"> <img src="../pictures/https_client_key_exchange.png"/> </div>
7. 经过以上步骤的交换,服务器和客户端都有了 `Client Params``Server Params`它们分别根据这两个参数进行计算ECDHE 算法能够保证客户端和服务器算出来的值是一样的,这个算出来的值就是预主密钥 `Pre-Master`
8. 最后再利用预主秘钥,客户端随机数,服务器随机数共同算出真正的主密钥 `Master_Secret`,算法如下:
```shell
master_secret = PRF(pre_master_secret,
"master secret",
ClientHello.random + ServerHello.random)
```
这里的 PRF 是一个伪随机函数,用于增强 master_secret 的随机性。主密钥 `Master_Secret` 的长度固定为 48 个字节,从中可以推导出以下四个密钥:
+ client_write_MAC_key
+ server_write_MAC_key
+ client_write_key
+ server_write_key
`client_write_MAC_key``server_write_MAC_key` 主要用于身份验证和完整性校验,而 `client_write_key``server_write_key` 则是对称加密中真正使用的会话密钥:
+ 客户端使用 `client_write_key` 加密,服务器对称地使用 `client_write_key` 解密;
+ 服务器使用 `server_write_key` 加密,客户端对称地使用 `server_write_key` 解密。
9. 有了会话密钥后,客户端会发送一个 `Change Cipher Spec` 请求,告知服务器将加密方式由非对称加密转换为对称加密;
10. 紧接着为了验证对称加密客户端会将所有的握手消息Handshake Message进行加密通过一个 `Encrypted Handshake Message` 请求发送给服务器;
11. 服务器解密并进行验证,如果验证通过,则也返回给客户端 `Change Cipher Spec``Encrypted Handshake Message` 消息,代表双方共同达成协议,之后的通信都采用对称加密的方式。
12. 除此之外,服务器还会返回一个 `New Session Ticket` 消息,其内容如下:
<div align="center"> <img src="../pictures/https_new_session_ticket.png"/> </div>
`New Session Ticket` 是一种优化连接的方式:服务器将会话信息加密后以 Ticket票据的方式发送给客户端 ,服务器本身并不存储会话信息。客户端接收到 Ticket 后,将其储到内存中,如果想恢复会话,则下次连接的时候将票据发送给服务器端;服务器端进行解密,如果确认无误则表示可以进行会话恢复,这样就简化了以上复杂的握手过程。
关于使用 WireShark 分析 HTTPS 握手的详细步骤,可以参考:[使用 WireShark 分析 HTTPS 握手过程](https://github.com/heibaiying/Full-Stack-Notes/blob/master/notes/WireShark_HTTPS.md)
## 参考资料
+ 虞卫东 . 《深入浅出 HTTPS从原理到实战》 . 电子工业出版社 . 2018-06

View File

@ -0,0 +1,247 @@
# JVM 性能监控之可视化工具
<nav>
<a href="#一简介">一、简介</a><br/>
<a href="#二JConsole">二、JConsole</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-简介">2.1 简介</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22-使用">2.2 使用</a><br/>
<a href="#三VisualVM">三、VisualVM</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#31-简介">3.1 简介</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#32-使用">3.2 使用</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#33-安装插件">3.3 安装插件</a><br/>
<a href="#四连接远程进程">四、连接远程进程</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#41-不使用安全凭证">4.1 不使用安全凭证</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#42-使用安全凭证">4.2 使用安全凭证</a><br/>
</nav>
## 一、简介
在 JDK 安装目录的 `bin` 文件夹下,除了提供有命令行监控工具外,还提供了几种可视化的监控工具,以方便用户直观地了解虚拟机的运行状态。常用的可视化监控工具如下:
## 二、JConsole
### 2.1 简介
JConsoleJava Monitoring and Management Console是一款基于 JMXJava Manage-ment Extensions的可视化监视工具。它的主要功能是通过 JMX 的 MBeanManaged Bean对系统信息进行收集和动态调整系统参数。JMXJava Management Extensions是一个为应用程序、设备、系统等植入管理功能的框架通常用于监控系统的运行状态或管理系统的部分功能。
### 2.2 使用
打开位于 bin 目录下的 `jconsole` 程序后,它会自动扫描当前主机上的所有 JVM 进程:
<div align="center"> <img src="..\pictures\jconsole-start.png"/> </div>
选中需要监控的进程后,点击连接,即可进入监控界面。监控界面包含了 *概览*、*内存*、*线程*、*类*、*VM 概要*、*MBean* 六个选项卡。其中概览界面显示的是 *内存*、*线程*、*类* 等三个选项卡界面的概览信息,如下所示:
<div align="center"> <img src="..\pictures\jconsole-概览.png"/> </div>
而内存界面主要用于显示堆和非堆上各个区域的使用量:
<div align="center"> <img src="..\pictures\jconsole-内存.png"/> </div>
线程界面内主要显示各个线程的堆栈信息,最下角有一个 **检测死锁** 按钮,点击后如果检测到死锁存在,则在下部的线程选项卡旁边会出现死锁选项卡:
<div align="center"> <img src="..\pictures\jconsole-检测死锁.png"/> </div>
点击死锁选项卡则可以看到造成死锁的线程:
<div align="center"> <img src="..\pictures\jconsole-死锁.png"/> </div>
最后的 **类** 选项卡主要用于显示当前已加载和已卸载的类的数量。而 **VM 概要** 选项卡则主要用于显示虚拟机的相关参数,如下所示:
<div align="center"> <img src="..\pictures\jconsole-概要.png"/> </div>
## 三、VisualVM
### 3.1 简介
VisualVMAll-in-One Java Troubleshooting Tool是 Oracle 提供的功能最强大的运行监视和故障处理程序之一, 它除了支持常规的运行监视、故障处理等功能外还能用于性能分析Profiling。同时因为 VisualVM 是基于 NetBeans 平台的开发工具所以它还支持通过插件来进行功能的拓展。VisualVM 的主要功能如下:
- 显示虚拟机进程及其配置信息、环境信息(与 jps、jinfo 功能类似);
- 监视应用程序的处理器、垃圾收集、堆、方法区以及线程的信息(与 jstat、jstack 功能类似);
- dump以及分析堆转储快照与 jmap、jhat 功能类似);
- 方法级的程序运行性能分析,找出被调用最多、运行时间最长的方法;
- 离线程序快照:可以收集程序的运行时配置、线程 dump、内存 dump 等信息来建立快照。
### 3.2 使用
打开位于 bin 目录下的 `jvisualvm` 程序, 它会自动扫描当前主机上的所有 JVM 进程:
<div align="center"> <img src="..\pictures\jvisual.png"/> </div>
点击需要监控的进程后,右侧即会显示相关的监控信息:
<div align="center"> <img src="..\pictures\jvisual-监视.png"/> </div>
**1. 堆 Dump**
在监控界面点击按钮可以 **执行垃圾回收** 或者 **堆 Dump** 。进行堆 Dump 后,还会显示其分析结果:
<div align="center"> <img src="..\pictures\jvisual-堆dump.png"/> </div>
**2. 线程 Dump**
在线程界面可以查看所有线程的状态,如果出现死锁,该界面还会进行提示:
<div align="center"> <img src="..\pictures\jvisual-线程.png"/> </div>
此时可以进行 **线程 Dump** 来获取具体的线程信息,效果和 jstack 命令类似:
<div align="center"> <img src="..\pictures\jvisual-dump.png"/> </div>
**3. 性能分析**
在 Profiler 界面,可以进行 CPU 和 内存的性能分析。要开始性能分析,需要先选择 **CPU****内存** 按钮中的一个VisualVM 将会开始记录应用程序执行过的所有方法:如果是进行的是 CPU 执行时间分析,将会统计每个方法的执行次数、执行耗时;如果是内存分析,则会统计每个方法关联的对象数以及这些对象所占的空间。想要结束性能分析,点击停止按钮即可:
<div align="center"> <img src="..\pictures\jvisual-性能分析.png"/> </div>
**4. Visual GC**
Visual GC 面板默认是不显示的,需要通过插件进行扩展。它会实时监控虚拟机的状态,在功能上类似于 jstat 命令:
<div align="center"> <img src="..\pictures\jvisual-gc.png"/> </div>
### 3.3 安装插件
在主界面,点击 **工具 => 插件** ,可以打开插件面板。右击插件选项或者点击安装按钮即可完成对应插件的安装:
<div align="center"> <img src="..\pictures\jvisual-插件安装.png"/> </div>
需要注意的是,安装插件前需要按照自己 JVM 的版本来配置插件中心,否则会抛出 ”无法连接到插件中心“ 的异常。每个版本对应的插件中心可以在该网址上查看https://visualvm.github.io/pluginscenters.html界面如下
<div align="center"> <img src="..\pictures\jvisual-插件中心.png"/> </div>
之后需要将正确的插件中心的地址配置到程序中:
<div align="center"> <img src="..\pictures\jvisual-配置插件中心.png"/> </div>
## 四、连接远程进程
以上演示 JConsole 和 VisualVM 时,我们都是用的本地进程,但在实际开发中,我们更多需要监控的是服务器上的远程进程。想要监控远程主机上的进程,需要进行 JMX 的相关配置,根据连接时是否需要用户名和密码,可以分为以下两种配置方式:
### 4.1 不使用安全凭证
启动服务器上的 Java 进程时增加以下参数:
```shell
java -Dcom.sun.management.jmxremote.port=12345 #jmx远程连接的端口号
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=false
-jar springboot.jar
```
此时只需要知道主机地址和端口号就可以连接,不需要使用用户名和密码,所以安全性比较低。
### 4.2 使用安全凭证
启动服务器上的 Java 进程时增加以下参数:
```shell
java -Dcom.sun.management.jmxremote.port=12345
-Dcom.sun.management.jmxremote.ssl=false
-Dcom.sun.management.jmxremote.authenticate=true
-Dcom.sun.management.jmxremote.access.file=/usr/local/jmxremote.access
-Dcom.sun.management.jmxremote.password.file=/usr/local/jmxremote.password
-jar springboot.jar
```
其中 `jmxremote.access ` 的内容如下,其中 admin 为用户名readwrite 表示可读可写,也可以设置为 readonly只读
```shell
admin readwrite
```
`jmxremote.password` 的内容如下,其中 admin 为用户名123456 为密码:
```shell
admin 123456
```
两个文件创建好后,还需要赋予其执行权限:
```shell
chmod 600 /usr/local/jmxremote.access
chmod 600 /usr/local/jmxremote.password
chown root:root /usr/local/jmxremote.access
chown root:root /usr/local/jmxremote.password
```
之后在使用 VisualVM 进行远程连接时,配置如下:
<div align="center"> <img src="..\pictures\jvisual-连接远程主机.png"/> </div>
需要注意的是这里的端口号是配置的 `Dcom.sun.management.jmxremote.port` 的值,而不是 Java 程序的端口号。连接完成后,即可查看到对应进程的监控状态。
## 参考资料
1. 主要参考自:周志明 . 深入理解Java虚拟机第3版. 机械工业出版社 , 2019-12 ,想要深入了解虚拟机的话,推荐阅读原书。
2. visualvm 官方文档https://visualvm.github.io/documentation.html
3. [Java_jvisualvm使用JMX连接远程机器实践](https://www.cnblogs.com/gossip/p/6141941.html)
4. [使用JMX透过防火墙远程监控tomcat服务](https://my.oschina.net/mye/blog/64879)

View File

@ -0,0 +1,263 @@
# JVM 性能监控之命令行工具
<nav>
<a href="#一简介">一、简介</a><br/>
<a href="#二jps">二、jps</a><br/>
<a href="#三jstat">三、jstat</a><br/>
<a href="#四jinfo">四、jinfo</a><br/>
<a href="#五jmap">五、jmap</a><br/>
<a href="#六jhat">六、jhat</a><br/>
<a href="#七jstack">七、jstack</a><br/>
</nav>
## 一、简介
在 JDK 安装目录的 `bin` 文件夹下,除了提供有 `javac``java` 这两个常用的编译和运行工具外,还提供了一系列命令行工具用于 JVM 的性能监控和故障诊断,常用的命令如下:
## 二、jps
jpsJVM Process Status Tool用于列出正在运行的虚拟机进程的主类名称和 LVMIDLocal Virtual Machine Identifier本地虚拟机唯一标识这里得到的 LVMID 是进行后续其它查询的基础。示例如下:
```shell
C:\Users>jps
10848 Main
14560 Jps
7040 Launcher
11572
9492 DeadLockTest
7868 JConsole
```
可选参数有 `-v` ,用于输出虚拟机进程启动时的 JVM 参数。
## 三、jstat
jstatJVM Statistics Monitoring Tool用于监视虚拟机的运行状态。使用格式如下
```shell
jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
```
其中 `option` 的所有可选值如下:
| 选项 | 作用 |
| ----------------- | ------------------------------------------------------------ |
| -class | 监视类加载、卸载数量、总空间以及类装载所耗费的时间 |
| -gc | 监视 Java 堆状况,包括 Eden 区、2 个 Survivor 区、老年代的容量、已用空间、垃圾收集时间等信息 |
| -gccapacity | 与 -gc 基本相同,但主要关注的是 Java 堆各个区域使用到的最大、最小空间 |
| -gcutil | 与 -gc 基本相同,但主要关注的是已使用空间占总空间的百分比 |
| -gccause | 与 -gcutil 基本相同,但是会额外输出上一次垃圾回收的原因 |
| -gcnew | 监视新生代垃圾回收的状况 |
| -gcnewcapacity | 与 -gcnew 基本相同,但主要关注的是使用到的最大、最小空间 |
| -gcold | 监视老年代垃圾回收的状况 |
| -gcoldcapacity | 与 -gcold 基本相同,但主要关注的是使用到的最大、最小空间 |
| -compiler | 输出即时编译器编译过的方法、耗时等信息 |
| -printcompilation | 输出已经被即时编译的方法 |
命令行中的 `interval` 表示监控的时间间隔,`count` 表示监控次数。示例如下:
```shell
jstat -gc 9492 3s 5 # 每3s输出一次一共输出5次
```
<div align="center"> <img src="..\pictures\jstat_gc.png"/> </div>
输出信息中各个参数含义分别如下:
- **S0C**survivor 0 的容量大小,单位 kB
- **S1C**survivor 1 的容量大小,单位 kB
- **S0U**survivor 0 已使用的空间大小,单位 kB
- **S1U**survivor 1 已使用的空间大小,单位 kB
- **EC**Eden 区的容量大小,单位 kB
- **EU**Eden 区已使用的空间大小,单位 kB
- **OC**:老年代的容量大小,单位 kB
- **OU**:老年代已使用的空间大小,单位 kB
- **MC**Metaspace 容量大小,单位 kB
- **MU**Metaspace 已使用的空间大小,单位 kB
- **CCSC**:压缩类的空间大小,单位 kB
- **CCSU**:压缩类已使用的空间大小,单位 kB
- **YGC**:年轻代垃圾回收的次数;
- **YGCT** 年轻代垃圾回收所消耗的时间;
- **FGC**:老年代垃圾回收的次数;
- **FGCT**:老年代垃圾回收所消耗的时间;
- **GCT**:垃圾回收所消耗的总时间。
以上是 option 为 `-gc` 时的输出结果,不同 option 的输出结果是不同的,所有输出结果及其参数解释可以参考官方文章: https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html
## 四、jinfo
jinfoConfiguration Info for Java的作用是实时查看和调整虚拟机的各项参数。使用格式如下
```shell
jinfo [option] <pid>
```
其中 `option ` 支持以下可选项:
- **-flag name** :输出指定的虚拟机参数的值;
- **-flag [+|-]name** :启用或禁用指定名称的虚拟机参数;
- **-flag name=value** :设置虚拟机参数的值;
- **-flags** :以键值对的方式输出 JVM 的相关属性;
- **-sysprops**:以键值对的方式输出 Java 相关的系统属性。
示例如下:
```java
jinfo -flags 13604
jinfo -flag CMSInitiatingOccupancyFraction 13604
```
<div align="center"> <img src="..\pictures\jinfo.png"/> </div>
## 五、jmap
jmapMemory Map for Java命令主要用于生成堆转储快照一般称为 heapdump 或 dump文件。除此之外它还可以用来查询 finalize 执行队列、Java 堆和方法区的详细信息,如空间使用率、当前使用的收集器等。 使用格式如下:
```shell
jmap [option] <pid>
```
其中 `option` 支持以下可选项:
| 选项 | 作用 |
| --------------------------- | ---- |
| -dump:[live,]format=b,file= | 生成 Java 堆转储快照,其中 live 用于指明是否只 dump 出存活的对象 |
| -finalizerinfo | 显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象。只在 Linux/Solaris 平台下有效 |
| -heap | 显示 Java 堆详细信息,如使用哪种回收器、参数配置、分代状况等。只在 Linux/Solaris 平台下有效 |
| -histo[:live] | 显示堆中对象的统计信息,包括类、实例数量、合计容量 |
| -permstat | 以 ClassLoader 为统计口径显示永久代内存状态。只在 Linux/Solaris 平台下有效 |
| -F | 当虚拟机进程堆 -dump 选项没有响应时,可使用这个选项强制生成 dump 快照。<br/>只在 Linux/Solaris 平台下有效 |
示例如下:
```shell
jmap -dump:format=b,file=test.bin 3260
```
<div align="center"> <img src="..\pictures\jmap.png"/> </div>
## 六、jhat
jhatJVM Heap Analysis Tool命令主要用来分析 jmap 生成的堆转储快照。 假设我们有如下一段程序:
```java
public class StackOverFlowTest {
private static List<StackOverFlowTest> list = new ArrayList<>();
public static void main(String[] args) throws InterruptedException {
while (true) {
list.add(new StackOverFlowTest());
Thread.sleep(10); //因为只是演示,所以休眠一下,避免生成的堆转储文件过大,导致分析时间过长
}
}
}
```
其最终会抛出 `java.lang.OutOfMemoryError: Java heap space` 异常,意味着在 JVM 堆上发生了内存溢出。在程序运行期间,我们可以使用上面的 jmap 命令生成堆转储快照,并使用 jhat 命令进行分析:
<div align="center"> <img src="..\pictures\jhat.png"/> </div>
jhat 命令最终的分析结果会以网页的方式进行提供,端口为 7000界面如下
<div align="center"> <img src="..\pictures\jhat_web.png"/> </div>
jhat 分析的结果并不够直观,因此我们还可以借助第三方工具来分析堆转储快照,这里以 JProfiler 为例,该软件可以直接从[官网](https://www.ej-technologies.com/products/jprofiler/overview.htm)下载并安装,安装完成后,点击 `session` 选项卡,并使用 `Open Snapshot` 打开 jmap 命令生成的堆转储快照:
<div align="center"> <img src="..\pictures\jprofiler-1.png"/> </div>
之后程序会自动进行分析,分析结果如下:
<div align="center"> <img src="..\pictures\jprofiler-2.png"/> </div>
通过以上可视化的统计结果,我们就可以很快定位到导致内存溢出的原因。
## 七、jstack
jstackStack Trace for Java命令用于生成虚拟机的线程快照一般称为 threaddump 或者 javacore 文件)。线程快照就是每一条线程正在执行的方法堆栈的集合,线程快照可以用于定位线程长时间停顿的原因,如死锁、死循环和长时间挂起等。其使用格式如下:
```shell
jstack -F [-m] [-l] <pid>
```
各选项的作用如下:
| 选项 | 作用 |
| ---- | ----------------------------------------------- |
| -F | 当正常输出的请求不被响应时,强制输出线程堆栈 |
| -m | 除堆栈外,显示关于锁的附加信息 |
| -l | 如果有调用本地方法的话,则可以显示 C/C++ 的堆栈 |
假设我们的程序中存在如下死锁:
```java
public class DeadLockTest {
private static final String a = "a";
private static final String b = "b";
public static void main(String[] args) {
new DeadLockTest().deadlock();
}
private void deadlock() {
new Thread(() -> {
synchronized (a) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (b) {
}
}
}).start();
new Thread(() -> {
synchronized (b) {
synchronized (a) {
}
}
}).start();
}
}
```
此时使用 jstack 分析就能很快的定位到问题所在,示例如下:
```shell
jstack 8112
```
输出结果如下:
<div align="center"> <img src="..\pictures\jstack.png"/> </div>
从输出中结果中可以看出,出现了一个死锁,该死锁由线程 Thread-0 和 Thread-1 导致,原因是 Thread-0 锁住了对象 `<0x00000000d6d8d610>` ,并尝试获取 `<0x00000000d6d8d640>` 对象的锁;但是 Thread-0 却恰恰相反,锁住了对象 `<0x00000000d6d8d640>` ,并尝试获取 `<0x00000000d6d8d610>` 对象的锁,由此导致死锁。
## 参考资料
+ 主要参考自:周志明 . 深入理解Java虚拟机第3版. 机械工业出版社 , 2019-12 ,想要深入了解虚拟机的话,推荐阅读原书。
+ 官方文档https://docs.oracle.com/javase/8/docs/technotes/tools/unix/s11-troubleshooting_tools.html#sthref327

921
notes/Java_NIO.md Normal file
View File

@ -0,0 +1,921 @@
# Java NIO
<nav>
<a href="#一简介">一、简介</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-面向缓冲">1.1 面向缓冲</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-同步非阻塞">1.2 同步非阻塞</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#13-多路复用">1.3 多路复用</a><br/>
<a href="#二Buffer">二、Buffer</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-缓冲区属性">2.1 缓冲区属性</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22-创建缓冲区">2.2 创建缓冲区</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#23-操作缓冲区">2.3 操作缓冲区</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#24-复制缓冲区">2.4 复制缓冲区</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#25-直接缓冲区">2.5 直接缓冲区</a><br/>
<a href="#三Channel">三、Channel</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#31-通道基础">3.1 通道基础</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#32-文件通道">3.2 文件通道</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#33-Channel-To-Channel">3.3 Channel To Channel</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#34-ScatterGather">3.4 Scatter/Gather</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#35-Pipe">3.5 Pipe</a><br/>
<a href="#四Selector">四、Selector</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#41-创建选择器">4.1 创建选择器</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#42-注册通道">4.2 注册通道</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#43-select">4.3 select</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#44-SelectionKey">4.4 SelectionKey</a><br/>
<a href="#五聊天室实例">五、聊天室实例</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#51-群聊服务器">5.1 群聊服务器</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#52-客户端实现">5.2 客户端实现</a><br/>
</nav>
## 一、简介
Java NIO 是 JDK 1.4 中引入的新的 IO 方式,它主要包含 Buffer、Channel、Selector 这三个核心的组件,它与传统 IO 的区别如下:
<table>
<tr>
<th width='430px'>NIO</th>
<th width='430px'>IO</th>
</tr>
<tr>
<td align="center">面向缓冲</td>
<td align="center">面向流</td>
</tr>
<tr>
<td align="center">同步非阻塞</td>
<td align="center">同步阻塞</td>
</tr>
<tr>
<td align="center">多路复用(选择器)</td>
<td align="center"></td>
</tr>
</table>
### 1.1 面向缓冲
传统的 IO 是面向流的,传统 IO 每次从流中读取一个或者多个字节,直至读取完所有的字节。而 NIO 是面向缓冲区的,所有的读写操作都需要通过 Buffer 来完成,数据会被先写入 Buffer 中然后再进行处理Buffer 提供了多种方法用于操纵其中的数据,因此其在操作上更加灵活,读取速度也更加快。
### 1.2 同步非阻塞
传统 IO 的流都是单向的,因此它们需要分为 Input Stream 和 Output Stream。而 NIO 中的 Channel 则是双向的,数据可以从 Channel 读到 Buffer 中,也可以从 Buffer 写到 Channel
<div align="center"> <img src="../pictures/nio_channel_buffer.png"/> </div>
> 注意:从 Channel 写入到 Buffer 执行的是 read 方法,而从 Buffer 写出到 Channel 执行的是 write 方法。
Channel 可以设置为非阻塞模式,此时当 Channel 从 Buffer 中读取数据时,如果有待读取的数据则返回该数据;如果没有待读取的数据,对应的方法也不会阻塞,而是直接返回。
### 1.3 多路复用
Java NIO 通过 Reactor 模型实现了 IO 的多路复用可以在一个线程上通过一个选择Selector使用轮询的方式去监听多个通道 Channel 上注册的事件,从而在一个线程上就能实现对多个 Channel 的处理:
<div align="center"> <img src="../pictures/nio_selector.png"/> </div>
## 二、Buffer
### 2.1 缓冲区属性
<div align="center"> <img src="../pictures/nio_buffer.png"/> </div>
所有缓冲区ByteBuffer、FloatBuffer、IntBuffer、DoubleBuffer、ShortBuffer、LongBuffer、CharBuffer、MappedByteBuffer都直接或间接继承自 Buffer 抽象类Buffer 中定义了缓冲区的四个基本属性:
```java
public abstract class Buffer {
private int capacity;
private int limit;
private int position = 0;
private int mark = -1;
...
}
```
+ **容量 (Capacity)** :缓冲区所能容纳元素的个数。
+ **上界 (Limit)**:缓冲区中现存元素的个数。
+ **位置 (Position)**:下一个待操作元素的索引。
+ **标记 (Mark)**:标记位置。通过 `mark()` 方法可以让 mark 等于当前 position之后通过 `reset()` 方法可以让 position 恢复到标记位置。
### 2.2 创建缓冲区
通常可以通过以下两种方法来创建缓冲区:
+ **allocate()**:通过指定缓冲区的容量大小来创建:
```java
CharBuffer buffer = CharBuffer.allocate(100);
```
+ **wrap()**:通过为缓冲区指定初始化数组来创建:
```java
char[] chars = new char[100];
CharBuffer buffer = CharBuffer.wrap(chars);
```
实际上,在缓冲区内部就是通过数组来存储元素,以 CharBuffer 为例,它的内部维持有一个名为 `hb` 的数组,用来存放实际的元素:
```java
public abstract class CharBuffer extends Buffer implements Comparable<CharBuffer>, Appendable, CharSequence, Readable
{
final char[] hb;
...
}
```
缓冲区创建完成后,它处于以下初始状态:
<div align="center"> <img src="../pictures/buffer_init.png"/> </div>
### 2.3 操作缓冲区
**1. put()**
用于向缓冲区中填充数据。以 CharBuffer 为例,有以下四个常用的重载方法:
```java
// 使用字符串填充数据
put(String src)
// 使用字符串填充数据start为字符串的开始位置end为字符串的结束位置不包含
put(String src, int start, int end)
// 使用数组填充数据
put(char[] src)
// 使用数组填充数据offset为数组填充的开始位置length为填充的长度,不允许越界
put(char[] src, int offset, int length)
```
当我们向 Buffer 中添加数据后position 属性也会随之变动:
<div align="center"> <img src="../pictures/buffer_put.png"/> </div>
**2. get()**
用于读取缓冲区中的数据。以 CharBuffer 为例,有以下四个常用的重载方法:
```java
// 获取当前位置postion的数据
char get();
// 获取指定位置的数据
char get(int index);
// 获取数据并填充到数组中
CharBuffer get(char[] dst)
// 获取数据并填充到数据中offset为数组填充的开始位置length为填充的长度,不允许越界
CharBuffer get(char[] dst, int offset, int length)
```
**3. flip()**
该方法会将 position 的值赋给 limit然后将 position 设置为 0从而可以由写模式切换到读模式。无论任何情况只要由写操作转换到读操作都需要先执行该方法。示例如下
```java
CharBuffer buffer = CharBuffer.allocate(100);
buffer.put("hello");
buffer.flip(); //由写模式切换到读模式
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
buffer.clear();
```
当使用 `filp()` 将 Buffer 由写模式切换到读模式后position 属性会恢复到初始位置代表从此处开始读取数据limit 属性也会随之变动,代表我们所能读取数据的上界:
<div align="center"> <img src="../pictures/buffer_flip.png"/> </div>
当我们再通过 `get()` 方法进行读取时position 属性会随之移动position 和 limit 之间就是待处理的数据:
<div align="center"> <img src="../pictures/buffer_get.png"/> </div>
**4. hasRemaining()**
`hasRemaining()` 用于判断当前的 position 是否小于 limit如果 position 小于 limit则返回 true代表仍有待处理的数据。
**5. clear()**
`clear()` 并不会真的清除缓冲区中的数据,它只是将 position 设置为 0并将 limit 设置为 capacity 的大小,从而让缓冲区恢复到初始状态:
<div align="center"> <img src="../pictures/buffer_clear.png"/> </div>
当有新的数据写入时,新的数据会覆盖原有位置上的数据。
**6. compact()**
用于压缩缓冲区,即将数组中待处理的数据复制到头部。如下所示,会将未读取的 `LL0` 复制到头部:
<div align="center"> <img src="../pictures/buffer_compact.png"/> </div>
需要注意的是这里执行的是复制操作,而不是移动操作,底层调用的是 `System.arraycopy` 方法,因此原有位置上的数据依然存在。但由于 position 会移动到未处理数据的下一个位置上,所以不用担心原有位置上的数据会被读取到,原因是你切换到读模式时,原有的 `LO` 数据仍处于 limit 之后:
<div align="center"> <img src="../pictures/buffer_compact_flip.png"/> </div>
**7. mark()**
用于设置标志位,设置好后可以使用 `reset()` 将 position 恢复到该位置:
```java
buffer.position(2).mark().position(5).reset().position(); //从位置2移动到位置5之后又恢复到位置2
```
### 2.4 复制缓冲区
如果想要对一个已有的缓冲区进行复制,可以有以下三种方法:
```java
public abstract CharBuffer duplicate();
public abstract CharBuffer asReadOnlyBuffer();
public abstract CharBuffer slice();
```
使用 `duplicate()` 复制的缓冲区具有以下特性:
- 与原缓冲区共享相同的数据元素,这意味着对原缓冲区数据的修改也会影响复制缓冲区;
- 复制缓冲区的 mark、position、limit、capcaity 属性的初始值与复制时原缓冲区的 mark、position、limit、capcaity 的值相同,但这些属性与原缓冲区的属性相互独立,创建后就不再受原有缓冲区的影响;
- 如果原缓冲区是只读缓冲区或直接缓冲区,则复制缓冲区也将继承这些属性。
<div align="center"> <img src="../pictures/buffer_duplicate.png"/> </div>
`asReadOnlyBuffer()``duplicate()` 类似,但创建的复制缓冲区为只读缓冲区。
`slice()` 也与 `duplicate()` 类似,但创建的复制缓冲区与原缓冲区只共享部分数据元素,并且所有标志位都处于原始状态:
<div align="center"> <img src="../pictures/buffer_slice.png"/> </div>
使用示例如下:
```java
CharBuffer buffer = CharBuffer.allocate(100);
buffer.put("helloworld");
buffer.position(2).limit(5);
CharBuffer duplicate = buffer.duplicate();
buffer.position(3).limit(6);
CharBuffer slice = buffer.slice();
System.out.println("buffer:" + buffer.position() + "," + buffer.limit()); // buffer:3,6
System.out.println("duplicate:" + duplicate.position() + "," + duplicate.limit()); // duplicate:2,5
System.out.println("slice:" + slice.position() + "," + slice.limit()); //slice:0,3
```
### 2.5 直接缓冲区
ByteBuffer 支持使用 `allocateDirect()` 方法来创建直接缓冲区,示例如下:
```java
ByteBuffer byteBuffer = ByteBuffer.allocate(100);
```
## 三、Channel
### 3.1 通道基础
Channel 接口的定义如下,其中定义了两个基础方法:
+ **isOpen()**:判断当前 Channel 是否处于打开状态;
+ **close()**:关闭当前 Channel 。Channel 关闭后,就不能在其上再进行任何 IO 操作,否则将抛出 ClosedChannelException 异常。
```java
public interface Channel extends Closeable {
public boolean isOpen();
public void close() throws IOException;
}
```
对于常见的文件操作和网络操作都可以直接获取到其对应的 Channel
```java
// 获取serverSocketChannel
ServerSocket serverSocket = new ServerSocket(8888);
ServerSocketChannel serverSocketChannel = serverSocket.getChannel();
// 获取SocketChannel
Socket socket = new Socket("127.0.0.1", 8888);
SocketChannel socketChannel = socket.getChannel();
// 获取FileChannel
FileInputStream fileInputStream = new FileInputStream(new File("path"));
FileChannel fileChannel = fileInputStream.getChannel();
```
### 3.2 文件通道
FileChannel 是一个连接到文件的通道,通过该通道可以完成文件的读写。另外 FileChannel 无法设置为非阻塞模式因为对文件读写操作设置非阻塞并没有什么意义。FileChannel 的使用示例如下:
```java
// 示例:文件拷贝
try {
FileInputStream inputStream = new FileInputStream(new File("D:\\a.png"));
FileOutputStream outputStream = new FileOutputStream(new File("D:\\b.png"));
FileChannel inChannel = inputStream.getChannel();
FileChannel outChannel = outputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(256);
// 从输入channel中读取数据到buffer中
while (inChannel.read(buffer) > 0) {
// 由写模式切换到读模式
buffer.flip();
while (buffer.hasRemaining()) {
//将buffer中的数据写出到输出channel
outChannel.write(buffer);
}
buffer.clear();
}
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
```
这里的最后我们只需要关闭 Stream 即可,其上的 Channel 也会被关闭,源码如下:
```java
public void close() throws IOException {
synchronized (closeLock) {
if (closed) {
return;
}
closed = true;
}
// 如果channel不为空则关闭
if (channel != null) {
channel.close();
}
fd.closeAll(new Closeable() {
public void close() throws IOException {
close0();
}
});
}
```
### 3.3 Channel To Channel
在 Java NIO 中,如果两个 Channel 中有一个是 FileChannel那么可以直接将数据从一个 Channel 传输到另外一个 Channel
```java
// 将该通道上的数据直接传送到目标通道
transferTo(long position, long count, WritableByteChannel target) ;
// 将原通道上的数据直接传送到该通道
transferFrom(ReadableByteChannel src, long position, long count)
```
还是以文件拷贝为例,使用示例如下:
```java
try {
FileInputStream inputStream = new FileInputStream(new File("D:\\a.png"));
FileOutputStream outputStream = new FileOutputStream(new File("D:\\b.png"));
FileChannel inChannel = inputStream.getChannel();
FileChannel outChannel = outputStream.getChannel();
inChannel.transferTo(0,inChannel.size(),outChannel); //使用transferTo实现
// outChannel.transferFrom(inChannel, 0, inChannel.size()); //使用transferFrom实现
inputStream.close();
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
```
### 3.4 Scatter/Gather
Java NIO 支持 scatter 和 gather 操作:
- **分散 (scatter)**:把 Channel 中的数据依次写入到多个 Buffer 上。示例如下:
```java
ByteBuffer buffer01 = ByteBuffer.allocate(32);
ByteBuffer buffer02 = ByteBuffer.allocate(64);
ByteBuffer buffer03 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = new ByteBuffer[]{buffer01, buffer02, buffer03};
fileInputStream.getChannel().read(buffers);
```
此时 Channel 中的数据会依次写入到 Buffer01 Buffer02 Buffer03 上。Scatter 通常用于固定长度数据的处理,假设一个数据单元由 headerbodyfooter 三部分组成,并且每部分的长度都是固定的,此时通过 Scatter 操作,每一组数据的 headerbodyfooter 都会分别固定地写到 Buffer01 Buffer02 Buffer03 上,此时就可以对每个 Buffer 应用不同的处理逻辑:
<div align="center"> <img src="../pictures/nio_scatter.png"/> </div>
+ **聚集 (gather)**:将多个 Buffer 中的数据依次写入到同一个 Channel 上。示例如下:
```java
ByteBuffer buffer01 = ByteBuffer.allocate(32);
ByteBuffer buffer02 = ByteBuffer.allocate(64);
ByteBuffer buffer03 = ByteBuffer.allocate(128);
ByteBuffer[] buffers = new ByteBuffer[]{buffer01, buffer02, buffer03};
fileInputStream.getChannel().read(buffers);
```
<div align="center"> <img src="../pictures/nio_gather.png"/> </div>
### 3.5 Pipe
Java NIO 还提供了 Pipe 管道用于在不同线程之间传递数据:
<div align="center"> <img src="../pictures/nio_pipe.png"/> </div>
Pipe 管道可以通过 Pipe 类的静态方法 `open()` 来创建:
```java
Pipe pipe = Pipe.open();
```
创建完成后可以通过其 `sink()``source()` 方法来创建对应的 SinkChannel 和 SourceChannel
```java
Pipe.SinkChannel sinkChannel = pipe.sink();
Pipe.SourceChannel sourceChannel = pipe.source();
```
SinkChannel 和 SourceChannel 的使用与基本的 Channel 类似,示例如下:
```java
Pipe pipe = Pipe.open();
new Thread(() -> {
try {
Pipe.SinkChannel sink = pipe.sink();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("HelloWorld".getBytes());
buffer.flip();
while (buffer.hasRemaining()) {
// 将数据写入SinkChannel
sink.write(buffer);
}
sink.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
try {
Pipe.SourceChannel source = pipe.source();
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取SourceChannel中的数据
while (source.read(buffer) > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get()); //输出HelloWorld
}
buffer.clear();
}
source.close();
} catch (IOException e) {
e.printStackTrace();
}
}).start();
```
## 四、Selector
### 4.1 创建选择器
想要创建一个选择器,可以通过 Selector 类的静态方法 `open()` 来实现:
```java
Selector selector = Selector.open();
```
### 4.2 注册通道
之后须要通过 `register()` 方法将 Channel 注册到 Selector 上,示例如下:
```java
// 创建ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 与 Selector一起使用的Channel必须处于非阻塞模式下
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(hostname, port));
// 注册监听CONNECT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
```
`register()` 方法的第二个参数表示需要监听的事件,它有以下四个可选值:
```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;
```
除此之外,你还可以在注册时通过调用 register 的另外一个重载方法来指定附加信息:
```java
register(Selector sel, int ops, Object att)
```
这个附加信息可以在事件触发时通过 SelectionKey 对象再次得到。
### 4.3 select
当你在 Selector 上注册好通道后,就可以使用 `select()` 方法来获取处于就绪状态的事件的集合。示例如下:
```java
int select = selector.select();
```
select 有以下三个重载方法:
+ **select()**:持续阻塞,直到至少有一个通道在其注册的事件上处于就绪状态;
+ **select(long timeout)**:与 `select()` 类似,但最长只阻塞 timout 毫秒;
+ **selectNow()**:不会阻塞,如果不存在就绪事件,则直接返回 0。
需要注意的是如果是 Ready 操作集发生了变化select 操作的返回值也可能是 0。这意味着如果某个通道注册的是 `OP_READ` 事件那么该通道在第一次收到消息时select 操作返回的值是 1但是之后收到消息时select 的返回值却可能是 0。因此在循环获取消息时对于 select 返回值的判断应该加上为 0 的情况:
```java
// 如果选择器上存在就绪事件,则进行处理
while (selector.select() >= 0) {
....
}
```
### 4.4 SelectionKey
当注册的事件处于就绪状态时,可以通过 Selector 的 `selectedKeys()` 方法来获取处于就绪状态的事件信息:
```java
Set<SelectionKey> selectionKeys = selector.selectedKeys();
```
其返回的是 SelectionKey 的集合SelectionKey 是对多个属性的综合封装:
```java
public abstract class SelectionKey {
// SelectionKey对应的channel
public abstract SelectableChannel channel();
// SelectionKey对应的选择器
public abstract Selector selector();
// 当前SelectionKey是否有效
public abstract boolean isValid();
// 取消channel在selector上注册的事件
public abstract void cancel();
// 当前channel注册的事件的合集
public abstract int interestOps();
// 当前channel是否对指定的事件感兴趣
public abstract SelectionKey interestOps(int ops);
// 处于就绪状态的事件的合集
public abstract int readyOps();
// Read事件是否就绪
public final boolean isReadable() {return (readyOps() & OP_READ) != 0;}
// Write事件是否就绪
public final boolean isWritable() {return (readyOps() & OP_WRITE) != 0;}
// Connect事件是否就绪
public final boolean isConnectable() {return (readyOps() & OP_CONNECT) != 0;}
// Accept事件是否就绪
public final boolean isAcceptable() {return (readyOps() & OP_ACCEPT) != 0;}
// 为SelectionKey指定附加属性也可以在注册时通过register方法指定
public final Object attach(Object ob) {return attachmentUpdater.getAndSet(this, ob);}
// 获取附加属性
public final Object attachment() { return attachment;}
}
```
## 五、聊天室实例
下面以一个群聊的聊天室为例,来展示 Java NIO 三大组件的综合使用,效果如下:
<div align="center"> <img src="../pictures/nio_chat_group.png"/> </div>
### 5.1 群聊服务器
群聊服务器的实现如下:
```java
package chat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.StandardCharsets;
import java.util.Set;
public class ChatServer {
private String hostname;
private int port;
private Selector selector;
private final ByteBuffer rBuffer = ByteBuffer.allocate(1024);
private final ByteBuffer wBuffer = ByteBuffer.allocate(1024);
ChatServer(int port) {
this("127.0.0.1", port);
}
ChatServer(String hostname, int port) {
this.hostname = hostname;
this.port = port;
}
public void start() {
try {
// 创建ServerSocketChannel
ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 设置为非阻塞模式
serverChannel.configureBlocking(false);
serverChannel.bind(new InetSocketAddress(hostname, port));
// 创建selector
selector = Selector.open();
// 注册监听CONNECT事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
// 持续调用select()
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
// 处理Accept事件
if (selectionKey.isAcceptable()) {
// 获取ServerSocketChannel
ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
// 通过ServerSocketChannel获取SocketChannel
SocketChannel clientChannel = server.accept();
clientChannel.configureBlocking(false);
// 注册Read事件
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("欢迎" + clientChannel.socket().getPort() + "加入聊天室!");
// 处理Readable事件
} else if (selectionKey.isReadable()) {
handleMessage(selectionKey);
}
}
selectionKeys.clear();
}
} catch (IOException e) {
e.printStackTrace();
} finally {
// 关闭selector后与之相关的所有资源都会被释放
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 处理客户端消息
*
* @param selectionKey 处于Read状态的SelectionKey
*/
private void handleMessage(SelectionKey selectionKey) {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
try {
// 获取来自客户端的消息
StringBuilder buffer = new StringBuilder();
while (clientChannel.read(rBuffer) > 0) {
rBuffer.flip();
buffer.append(StandardCharsets.UTF_8.decode(rBuffer));
rBuffer.clear();
}
String msg = buffer.toString();
// 将消息发送给其他客户端
broadcastMessage(clientChannel, msg);
} catch (IOException e) {
// 该异常由clientChannel.read(rBuffer)方法抛出如果出现该异常则说明clientChannel已经关闭
// 此时需要调用cancel()取消注册在selector上的事件
selectionKey.cancel();
// 在多线程环境下如果另一个线程正在阻塞地调用select(),因为事件集已经改变,
// 因此需要通过wakeup()让其立刻返回并重新select()
selector.wakeup();
System.out.println("用户" + clientChannel.socket().getPort() + "退出聊天室!");
}
}
/**
* 将客户端发来的消息广播给其他客户端
*
* @param clientChannel 消息源
* @param msg 消息
*/
private void broadcastMessage(SocketChannel clientChannel, String msg) {
try {
// 遍历当前selector上所有channel
for (SelectionKey selectionKey : selector.keys()) {
SelectableChannel channel = selectionKey.channel();
// 消息不需要转发给ServerSocketChannel和当前客户端自己
if (selectionKey.isValid() && !(channel instanceof ServerSocketChannel)
&& !channel.equals(clientChannel)) {
SocketChannel otherClient = (SocketChannel) channel;
wBuffer.put(("用户" + clientChannel.socket().getPort() + ": " + msg).getBytes());
while (wBuffer.hasRemaining()) {
wBuffer.flip();
otherClient.write(wBuffer);
}
wBuffer.clear();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new ChatServer(8888).start();
}
}
```
### 5.2 客户端实现
客户端的实现如下:
```java
package chat;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedSelectorException;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
import java.util.Set;
public class ChatClient {
private String hostname;
private int port;
private Selector selector;
private final ByteBuffer rBuffer = ByteBuffer.allocate(1024);
private final ByteBuffer wBuffer = ByteBuffer.allocate(1024);
ChatClient(String hostname, int port) {
this.hostname = hostname;
this.port = port;
}
public void start() {
try {
// 创建SocketChannel
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
socketChannel.connect(new InetSocketAddress(hostname, port));
// 创建selector
selector = Selector.open();
// 注册监听CONNECT事件
socketChannel.register(selector, SelectionKey.OP_CONNECT);
// 持续调用select
while (true) {
selector.select();
Set<SelectionKey> selectionKeys = selector.selectedKeys();
for (SelectionKey selectionKey : selectionKeys) {
// 处理Connect事件
if (selectionKey.isConnectable()) {
SocketChannel clientChannel = (SocketChannel) selectionKey.channel();
// 有可能正处于连接中的状态
if (clientChannel.isConnectionPending()) {
// 等待连接完成
clientChannel.finishConnect();
// 开始监听用户输入
inputListening(clientChannel);
// 为clientChannel注册上Read
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("尊敬的用户" + clientChannel.socket().getLocalPort()
+ ", 你已成功加入群聊!");
}
// 处理Read事件
} else if (selectionKey.isReadable()) {
handleMessage(selectionKey);
}
}
}
} catch (ClosedSelectorException e) {
// 主动关闭客户端,不做任何处理
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
selector.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 监听用户来自控制台的输入
*
* @param clientChannel 客户端Channel
*/
private void inputListening(SocketChannel clientChannel) {
// 要持续监听用户输入,但又不能阻塞主线程,所以需要一个单独的线程来完成
new Thread(() -> {
Scanner scanner = new Scanner(System.in);
try {
while (scanner.hasNextLine()) {
String input = scanner.nextLine();
if (!input.isEmpty()) {
if ("exit".equals(input)) {
selector.close();
return;
}
wBuffer.put(input.getBytes());
wBuffer.flip();
while (wBuffer.hasRemaining()) {
clientChannel.write(wBuffer);
}
wBuffer.clear();
}
}
} catch (IOException e) {
e.printStackTrace();
}
}).start();
}
/**
* 处理来自其他客户端的消息
*
* @param selectionKey 处于Read状态的selectionKey
*/
private void handleMessage(SelectionKey selectionKey) {
try {
SocketChannel channel = (SocketChannel) selectionKey.channel();
StringBuilder buffer = new StringBuilder();
while (channel.read(rBuffer) > 0) {
rBuffer.flip();
buffer.append(StandardCharsets.UTF_8.decode(rBuffer));
rBuffer.clear();
}
System.out.println(buffer.toString());
} catch (IOException e) {
selectionKey.cancel();
selector.wakeup();
System.out.println("聊天室服务器已关闭!");
}
}
public static void main(String[] args) {
new ChatClient("127.0.0.1", 8888).start();
}
}
```
## 参考资料
+ [Ron Hitchens . Java NIO . O'Reilly Media . 2002-08-15](https://book.douban.com/subject/1433583/)
+ [Java NIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html)
+ [一文读懂 Java NIO 和 IO 的不同](https://baijiahao.baidu.com/s?id=1632673729522644150&wfr=spider&for=pc)

View File

@ -0,0 +1,519 @@
# Java 函数式编程
<nav>
<a href="#一Lambda">一、Lambda</a><br/>
<a href="#二函数式接口">二、函数式接口</a><br/>
<a href="#三创建流">三、创建流</a><br/>
<a href="#四操作流">四、操作流</a><br/>
<a href="#五收集器">五、收集器</a><br/>
<a href="#六并行流">六、并行流</a><br/>
</nav>
## 一、Lambda
### 1.1 格式
JDK 从 1.8 版本开始支持 Lambda 表达式,通过 Lambda 表达式我们可以将一个函数作为参数传入方法中。在 JDK 1.8 之前,我们只能通过匿名表达式来完成类似的功能,但是匿名表达式比较繁琐,存在大量的模板代码,不利于将行为参数化,而采用 Lamdba 则能很好的解决这个问题。Lambda 表达式的基本语法如下:
```java
(parameters) -> expression
```
或采用花括号的形式:
```java
(parameters) -> { statements; }
```
Lambda 表达式具有如下特点:
- **可选的参数:**不需要声明参数类型,编译器会依靠上下文进行自动推断;
- **可选的参数圆括号:**当且仅当只有一个参数时,包裹参数的圆括号可以省略;
- **可选的花括号:**如果主体只有一个表达式,则无需使用花括号;
- **可选的返回关键字:**如果主体只有一个表达式,则该表达式的值就是整个 Lambda 表达式的返回值,此时不需要使用 return 关键字进行显式的返回。
### 1.2 行为参数化
上面我们说过Lambda 表达式主要解决的是行为参数化的问题,而什么是行为参数化?下面给出一个具体的示例:
```java
/**
* 定义函数式接口
* @param <T> 参数类型
*/
@FunctionalInterface
public interface CustomPredicate<T> {
boolean test(T t);
}
```
```java
/**
* 集合过滤
* @param list 待过滤的集合
* @param predicate 函数式接口
* @param <T> 集合中元素的类型
* @return 满足条件的元素的集合
*/
public static <T> List<T> filter(List<T> list, CustomPredicate<T> predicate) {
ArrayList<T> result = new ArrayList<>();
for (T t : list) {
// 将满足条件的元素添加到返回集合中
if (predicate.test(t)) result.add(t);
}
return result;
}
```
针对不同类型的集合,我们可以通过传入不同的 Lambda 表达式作为参数来表达不同的过滤行为,这就是行为参数化:
```java
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
filter(integers, x -> x % 2 == 0); // 过滤出所有偶数
List<Employee> employees = Arrays.asList(
new Employee("张某", 21, true),
new Employee("李某", 30, true),
new Employee("王某", 45, false));
filter(employees, employee -> employee.getAge() > 25); // 过滤出所有年龄大于25的员工
```
需要注意的是上面我们声明接口时,使用了 `@FunctionalInterface` 注解,它表示当前的接口是一个函数式接口。函数式接口就是只含有一个抽象方法的接口;即一个接口不论含有多少个默认方法和静态方法,只要它只有一个抽象方法,它就是一个函数式接口。使用 `@FunctionalInterface` 修饰后,当该接口有一个以上的抽象方法时,编译器就会进行提醒。
任何使用到函数式接口的地方,都可以使用 Lambda 表达式进行简写。例如 Runnable 接口就是一个函数式接口,我们可以使用 Lambda 表达式对其进行简写:
```java
new Thread(() -> {
System.out.println("hello");
});
```
### 1.3 方法引用和构造器引用
紧接上面的例子,如果我们需要过滤出所有的正式员工,除了可以写成下面的形式外:
```java
filter(employees, employee -> employee.isOfficial());
```
还可以使用方法引用的形式进行简写:
```java
filter(employees, Employee::isOfficial);
```
除了方法引用外,还可以对构造器进行引用,示例如下:
```java
Stream<Integer> stream = Stream.of(1, 3, 5, 2, 4);
stream.collect(Collectors.toCollection(ArrayList::new)); //等价于 toCollection(()->new ArrayList<>())
```
方法引用和构造器引用的目的都是为了让代码更加的简洁。
## 二、函数式接口
通常我们不需要自定义函数式接口JDK 中内置了大量函数式接口,基本可以满足大多数场景下的使用需求,最基本的四种如下:
**1. Consumer\<T>**:消费型接口,消费输入的变量,没有返回值:
```java
@FunctionalInterface
public interface Consumer<T> {
void accept(T t);
...
}
```
**2. Consumer\<T>**:供给型接口,供给变量:
```java
@FunctionalInterface
public interface Supplier<T> {
T get();
}
```
**3. Function<T, R>**:对输入类型为 T 的变量执行特定的转换操作,并返回类型为 R 的返回值:
```java
@FunctionalInterface
public interface Function<T, R> {
R apply(T t);
...
}
```
**4. Predicate\<T>**:判断类型为 T 的变量是否满足特定的条件,如果满足则返回 true否则返回 flase
```java
@FunctionalInterface
public interface Predicate<T> {
boolean test(T t);
...
}
```
其他函数式接口都是这四种基本类型的扩展和延伸。以 BiFunction 和 BinaryOperator 接口为例:
+ **BiFunction<T, U, R>**:是函数型接口 Function<T, R> 的扩展Function 只能接收一个入参;而 BiFunction 可以用于接收两个不同类型的入参;
+ **BinaryOperator\<T>**:是 BiFunction 的一种特殊化情况,即两个入参和返回值的类型均相同,通常用于二元运算。定义如下:
```java
@FunctionalInterface
public interface BiFunction<T, U, R> {
R apply(T t, U u);
}
@FunctionalInterface
public interface BinaryOperator<T> extends BiFunction<T,T,T> {
....
}
```
下面演示一下 BinaryOperator 的用法:
```java
/**
* 执行归约操作
*/
public static <T> T reduce(List<T> list, T initValue, BinaryOperator<T> binaryOperator) {
for (T t : list) {
initValue = binaryOperator.apply(initValue, t);
}
return initValue;
}
public static void main(String[] args) {
List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
reduce(integers, 0, (a, b) -> a + b); // 求和 输出15
reduce(integers, 1, (a, b) -> a * b); // 求积 输出120
}
```
## 三、创建流
JDK 1.8 中另一个大的改进是引入了流通过流、Lamda 表达式以及函数式接口,可以高效地完成数据的处理。创建流通常有以下四种方法:
**1. 由值创建**
使用静态方法 `Stream.of()` 由指定的值进行创建:
```java
Stream<String> stream = Stream.of("a", "b ", "c", "d");
```
**2. 由集合或数组创建**
使用静态方法 `Arrays.stream()` 由指定的数组进行创建:
```java
String[] strings={"a", "b ", "c", "d"};
Stream<String> stream = Arrays.stream(strings);
```
调用集合类的 `stream()` 方法进行创建:
```shell
List<String> strings = Arrays.asList("a", "b ", "c", "d");
Stream<String> stream = strings.stream();
```
`stream()` 方法定义在 `Collection` 接口中,它是一个默认方法,因此大多数的集合都可以通过该方法来创建流:
```java
public interface Collection<E> extends Iterable<E> {
default Stream<E> stream() {
return StreamSupport.stream(spliterator(), false);
}
}
```
**3. 由文件创建**
```java
try (Stream<String> lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.UTF_8)) {
lines.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
```
**4. 由函数创建**
除了以上方法外,还可以通过 `Stream.iterate()``Stream.generate()` 方法来来创建无限流:
+ `Stream.iterate()` 接受两个参数:第一个是初始值;第二个参数是一个输入值和输出值相同的函数型接口,主要用于迭代式地产生新的元素,示例如下:
```java
// 依次输出0到9
Stream.iterate(0, x -> x + 1).limit(10).forEach(System.out::print);
```
+ `Stream.generate()` 接收一个供应型函数作为参数,用于按照该函数产生新的元素:
```java
// 依次输出随机数
Stream.generate(Math::random).limit(10).forEach(System.out::print);
```
## 四、操作流
### 4.1 基本操作
当流创建后,便可以利用 Stream 类上的各种方法对流中的数据进行处理,常用的方法如下:
| 操作 | 作用 | 返回类型 | 使用的类型/函数式接口 |
| --------- | ---------------------------------- | ------------ | ---------------------- |
| filter | 过滤符合条件的元素 | Stream\<T> | Predicate\<T> |
| distinct | 过滤重复元素 | Stream\<T> | |
| skip | 跳过指定数量的元素 | Stream\<T> | long |
| limit | 限制元素的数量 | Stream\<T> | long |
| map | 对元素执行特定转换操作 | Stream\<T> | Function<T,R> |
| flatMap | 将元素扁平化后执行特定转换操作 | Stream\<T> | Function<T,Stream\<R>> |
| sorted | 对元素进行排序 | Stream\<T> | Comparator\<T> |
| anyMatch | 是否存在任意一个元素能满足指定条件 | boolean | Predicate\<T> |
| noneMatch | 是否所有元素都不满足指定条件 | boolean | Predicate\<T> |
| allMatch | 是否所有元素都满足指定条件 | boolean | Predicate\<T> |
| findAny | 返回任意一个满足指定条件的元素 | Optional\<T> | |
| findFirst | 返回第一个满足指定条件的元素 | Optional\<T> | |
| forEach | 对所有元素执行特定的操作 | void | Cosumer\<T> |
| collect | 使用收集器 | R | Collector<T, A, R> |
| reduce | 执行归约操作 | Optional\<T> | BinaryOperator\<T> |
| count | 计算流中元素的数量 | long | |
> 注:上表中返回类型为 Stream\<T> 的操作都是中间操作,代表还可以继续调用其它方法对流进行处理。返回类型为其它的操作都是终止操作,代表处理过程到此为止。
使用示例如下:
```java
Stream.iterate(0, x -> x + 1) // 构建流
.limit(20) // 限制元素的个数
.skip(10) // 跳过前10个元素
.filter(x -> x % 2 == 0) // 过滤出所有偶数
.map(x -> "偶数:" + x) // 对元素执行转换操作
.forEach(System.out::println); // 打印出所有元素
```
输出结果如下:
```shell
偶数:10
偶数:12
偶数:14
偶数:16
偶数:18
```
上表的 `flatMap()` 方法接收一个参数,该参数是一个函数型接口 `Function<? super T, ? extends Stream<? extends R>> mapper`,主要用于将流中的元素转换为 `Stream` ,从而可以将原有的元素进行扁平化,示例如下:
```java
String[] strings = {"hello", "world"};
Arrays.stream(strings)
.map(x -> x.split("")) // 拆分得到: ['h','e','l','l','o'],['w','o','r','l','d']
.flatMap(x -> Arrays.stream(x)) // 将每个数组进行扁平化处理得到:'h','e','l','l','o','w','o','r','l','d'
.forEach(System.out::println);
```
而上表的 `reduce()` 方法则接收两个参数:第一个参数表示执行归约操作的初始值;第二个参数是上文我们介绍过的函数式接口 `BinaryOperator<T>` ,使用示例如下:
```java
Stream.iterate(0, x -> x + 1).limit(10)
.reduce(0, (a, b) -> a + b); //进行求和操作
```
### 4.2 数值流
上面的代码等效于对 Stream 中的所有元素执行了求和操作,因此我们还可以调用简便方法 `sum()` 来进行实现,但是需要注意的是 `Stream.iterate()` 生成流中的元素类型都是包装类型:
```java
Stream<Integer> stream = Stream.iterate(0, x -> x + 1); //包装类型Integer
```
`sum()` 方法则是定义在 IntStream 上,此时需要将流转换为具体的数值流,对应的方法是 `mapToInt()`
````java
Stream.iterate(0, x -> x + 1).limit(10).mapToInt(x -> x).sum();
````
类似的方法还有 `mapToLong()``mapToDouble()` 。如果你想要将数值流转换为原有的流,相当于对其中的元素进行装箱操作,此时可以调用 `boxed()` 方法:
```java
IntStream intStream = Stream.iterate(0, x -> x + 1).limit(10).mapToInt(x -> x);
Stream<Integer> boxed = intStream.boxed();
```
## 五、收集器
Stream 中最强大一个终止操作是 `collect()` ,它接收一个收集器 Collector 作为参数可以将流中的元素收集到集合中或进行分组、分区等操作。Java 中内置了多种收集器的实现,可以通过 Collectors 类的静态方法进行调用,常用的收集器如下:
| 工厂方法 | 返回类型 | 用于 |
| ----------------- | --------------------- | ------------------------------------------------------------ |
| toList | List\<T> | 把流中所有元素收集到 List 中 |
| toSet | Set\<T> | 把流中所有元素收集到 Set 中 |
| toCollection | Collection\<T> | 把流中所有元素收集到指定的集合中 |
| counting | Long | 计算流中所有元素的个数 |
| summingInt | Integer | 将流中所有元素转换为整数,并计算其总和 |
| averagingInt | Double | 将流中所有元素转换为整数,并计算其平均值 |
| summarizingInt | IntSummaryStatistics | 将流中所有元素转换为整数,并返回统计结果,包含最大值、最小值、<br/>总和与平均值等信息 |
| joining | String | 将流中所有元素转换为字符串,并使用给定连接符进行连接 |
| maxBy | Optional\<T> | 查找流中最大元素的 Optional |
| minBy | Optional\<T> | 查找流中最小元素的 Optional |
| reducing | 规约操作产生的类型 | 对流中所有元素执行归约操作 |
| collectingAndThen | 转换返回的类型 | 先把流中所有元素收集到指定的集合中,再对集合执行特定的操作 |
| groupingBy | Map<K,List\<T>> | 对流中所有元素执行分组操作 |
| partitionBy | Map<Boolean,List\<T>> | 对流中所有元素执行分区操作 |
使用示例如下:
```java
Stream<Integer> stream = Stream.of(1, 2, 3, 4, 4, 5, 6);
stream.collect(Collectors.toSet()); // [1, 2, 3, 4, 5, 6]
stream.collect(Collectors.toList()); // [1, 2, 3, 4, 4, 5, 6]
stream.collect(Collectors.toCollection(ArrayList::new)); // [1, 2, 3, 4, 4, 5, 6]
stream.collect(Collectors.counting()); // 7 等效于 stream.count();
stream.collect(Collectors.summarizingInt(x -> x)); // IntSummaryStatistics{count=7, sum=25, min=1, average=3.571429, max=6}
stream.collect(Collectors.maxBy((Integer::compareTo))); // Optional[6]
stream.collect(Collectors.reducing(1, (a, b) -> a * b)); // 等效于 stream.reduce(1, (a, b) -> a * b);
collect(Collectors.collectingAndThen(Collectors.toSet(), Set::size)); // 先把所有元素收集到Set中再计算Set的大小
```
> 注意:以上每个终止操作只能单独演示,因为对一个流只能执行一次终止操作。并且执行完终止操作后,就不能再对这个流进行任何操作,否则将抛出 `java.lang.IllegalStateException: stream has already been operated upon or closed` 的异常。
### 5.2 分组
分组收集器可以实现类似数据库 groupBy 子句的功能。假设存在如下员工信息:
```java
Stream<Employee> stream = Stream.of(new Employee("张某", "男", "A公司", 20),
new Employee("李某", "女", "A公司", 30),
new Employee("王某", "男", "B公司", 40),
new Employee("田某", "女", "B公司", 50));
```
```java
public class Employee {
private String name;
private String gender;
private String company;
private int age;
@Override
public String toString() {return "Employee{" + "name='" + name + '\'' + '}';
}
}
```
此时如果需要按照公司进行分组,则可以使用 `groupingBy()` 收集器:
```java
stream.collect(Collectors.groupingBy(Employee::getCompany));
对应的分组结果如下:
{
B公司=[Employee{name='王某'}, Employee{name='田某'}],
A公司=[Employee{name='张某'}, Employee{name='李某'}]
}
```
如果想要计算分组后每家公司的人数,还可以为 `groupingBy()` 传递一个收集器 Collector 作为其第二个参数,调用其重载方法:
```java
stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.counting()));
对应的结果如下:
{
B公司=2,
A公司=2
}
```
因为第二个参数是一个 Collector这意味着你可以再传入一个分组收集器来完成多级分组示例如下
```java
stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.groupingBy(Employee::getGender)));
先按照公司分组,再按照性别分组,结果如下:
{
B公司={女=[Employee{name='田某'}], 男=[Employee{name='王某'}]},
A公司={女=[Employee{name='李某'}], 男=[Employee{name='张某'}]}
}
```
除此之外,也可以通过代码块来自定义分组条件,示例如下:
```java
Map<String, List<Employee>> collect = stream.collect(Collectors.groupingBy(employee -> {
if (employee.getAge() <= 30) {
return "青年员工";
} else if (employee.getAge() < 50) {
return "中年员工";
} else {
return "老年员工";
}
}));
对应的分组结果如下:
{
中年员工=[Employee{name='王某'}],
青年员工=[Employee{name='张某'}, Employee{name='李某'}],
老年员工=[Employee{name='田某'}]
}
```
### 5.3 分区
分区是分组的一种特殊情况,即将满足指定条件的元素分为一组,将不满足指定条件的元素分为另一组,两者在使用上基本类似,示例如下:
```java
stream.collect(Collectors.partitioningBy(x -> "A公司".equals(x.getCompany())));
对应的分区结果如下:
{
false=[Employee{name='王某'}, Employee{name='田某'}],
true=[Employee{name='张某'}, Employee{name='李某'}]
}
```
## 六、并行流
想要将普通流转换为并行流非常简单,只需要调用 Stream 的 `parallel()` 方法即可:
```java
stream.parallel();
```
此时流中的所有元素会被均匀的分配到多个线程上进行处理。并行流内部使用的是 ForkJoinPool 线程池,它默认的线程数量就是处理器数量,可以通过 `Runtime.getRuntime().availableProcessors()` 来查看该值,通常不需要更改。
当前也没有办法为某个具体的流指定线程数量,只能通过修改系统属性 `java.util.concurrent.ForkJoinPool.common.parallelism` 的值来改变所有并行流使用的线程数量,示例如下:
```java
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
```
如果想将并行流改回普通的串行流,则只需要调用 Stream 的 `sequential()` 方法即可:
```she
stream.sequential();
```
## 参考资料
厄马(Raoul-Gabriel Urma) / 弗斯科(Mario Fusco) / 米克罗夫特(Alan Mycroft) .**《Java 8实战》**. 人民邮电出版社 . 2016-04-01

View File

@ -33,7 +33,7 @@
| Undefined | “undefined” | 该数据类型已经废弃 |
| ObjectId | “objectId” | 对象 id |
| Boolean | “bool” | 布尔值 |
| Date | “date” | 日类型 |
| Date | “date” | 日类型 |
| Null | “null” | 空 |
| Regular Expression | “regex” | 正则表达式 |
| DBPointer | “dbPointer” | 该数据类型已经废弃 |
@ -348,4 +348,4 @@ db.user.deleteOne(
+ 官方文档:[MongoDB CRUD Operations](https://docs.mongodb.com/manual/crud/)
+ Kristina Chodorow . MongoDB权威指南第2版. 人民邮件出版社 . 2014-01
+ Kristina Chodorow . MongoDB权威指南第2版. 人民邮件出版社 . 2014-01

View File

@ -27,6 +27,8 @@ Mycat 是一个开源的数据库中间件,可以解决分布式数据库环
- 支持使用 zookeeper 来协调主从切换、统一管理配置数据以及使用 zookeeper 来生成全局唯一 ID。( 1.6+ )
- 提供了 Web 监控界面,来对 Mycat、MySQL 以及服务器的状态进行监控。
## 二、Mycat 核心概念
在引入 Mycat 后,所有的客户端请求需要经过中间件进行转发上,此时客户端直接面向的是 Mycat 上的逻辑库或逻辑表:
@ -48,8 +50,26 @@ Mycat 是一个开源的数据库中间件,可以解决分布式数据库环
将表按照分片键进行分片后,一个表中的所有数据就会被分发到不同的数据库上,这些数据库节点就称为分片节点。
## 三、Mycat 安装
Mycat 所有版本的安装包都可以从 http://dl.mycat.io/ 上进行下载,下载后进行解压即可,其主要文件目录如下:
```shell
mycat
├── bin
│   ├── mycat #mycat程序的执行脚本,如 mycat start 用于启动mycat服务mycat stop用于停止mycat服务
├── conf
│   ├── rule.xml
│   ├── schema.xml
│   ├── server.xml
├── logs #日志存放目录
└── version.txt #版本信息
```
## 四、Mycat 基本配置
在 Mycat 的安装目录的 `conf` 目录下,有以下三个核心配置文件:
@ -187,6 +207,8 @@ rule.xml 文件中定义的是分片规则,主要包含以下标签:
Mycat 内置支持十几种分片算法,如 取模分片,枚举分片,范围分片,字符串 hash 分片,一致性 hash 分片,日期分片等。关于这些分片算法的详细说明可以参考官方文档:[Mycat 官方指南](http://www.mycat.io/document/mycat-definitive-guide.pdf)
## 五、Mycat 读写分离
Mycat 读写分离的配置非常简单,只需要通过配置 balancewriteHost 和 readHost 就可以实现,示例如下:
@ -226,6 +248,8 @@ Mycat 读写分离的配置非常简单,只需要通过配置 balancewriteH
基于以上两个原因,如果想要实现高可用,并不建议配置多个 writeHost ,而是配置一个 writeHost ,但其指向的是虚拟的读 IP 地址,此时复制架构由 MMM 或者 MHA 架构来实现,并由它们来提供虚拟机的读 IP。
## 六、Mycat 分库分表
综合以上全部内容,这里给出一个分库分表的示例,其架构如下:

View File

@ -0,0 +1,344 @@
# Redis 分布式锁
<nav>
<a href="#一实现原理">一、实现原理</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-基本原理">1.1 基本原理</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-官方推荐">1.2 官方推荐</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#13--延长锁时效">1.3 延长锁时效</a><br/>
<a href="#二哨兵模式与分布式锁">二、哨兵模式与分布式锁</a><br/>
<a href="#三集群模式与分布式锁">三、集群模式与分布式锁</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#31-RedLock-方案">3.1 RedLock 方案</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#32-低延迟通讯">3.2 低延迟通讯</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#33-持久化与高可用">3.3 持久化与高可用</a><br/>
<a href="#四Redisson">四、Redisson</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#41-分布式锁">4.1 分布式锁</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#42-RedLock">4.2 RedLock</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#43-延长锁时效">4.3 延长锁时效</a><br/>
</nav>
## 一、实现原理
### 1.1 基本原理
JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源JDK 原生的锁就无能为力了。此时可以使用 Redis 来实现分布式锁。
Redis 实现分布式锁的核心命令如下:
```shell
SETNX key value
```
SETNX 命令的作用是:如果指定的 key 不存在,则创建并为其设置值,然后返回状态码 1如果指定的 key 存在,则直接返回 0。如果返回值为 1代表获得该锁此时其他进程再次尝试创建时由于 key 已经存在,则都会返回 0 ,代表锁已经被占用。
当获得锁的进程处理完成业务后,再通过 `del` 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。
通常为了避免死锁,我们会为锁设置一个超时时间,在 Redis 中可以通过 `expire` 命令来进行实现:
```shell
EXPIRE key seconds
```
这里我们将两者结合起来,并使用 Jedis 客户端来进行实现,其代码如下:
```java
Long result = jedis.setnx("lockKey", "lockValue");
if (result == 1) {
// 如果此处程序被异常终止如直接kill -9进程则设置超时的操作就无法进行该锁就会出现死锁
jedis.expire("lockKey", 3);
}
```
上面的代码存在原子性问题,即 setnx + expire 操作是非原子性的,如果在设置超时时间前,程序被异常终止,则程序就会出现死锁。此时可以将 SETNX 和 EXPIRE 两个命令写在同一个 Lua 脚本中,然后通过调用 Jedis 的 `eval()` 方法来执行,并由 Redis 来保证整个 Lua 脚本操作的原子性。这种方式实现比较繁琐,因此官方文档中推荐了另外一种更加优雅的实现方法:
### 1.2 官方推荐
[官方文档]( Distributed locks with Redis) 中推荐直接使用 set 命令来进行实现:
```shell
SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
```
这里我们主要关注以下四个参数:
- **EX** :设置超时时间,单位是秒;
- **PX** :设置超时时间,单位是毫秒;
- **NX** :当且仅当对应的 Key 不存在时才进行设置;
- **XX**:当且仅当对应的 Key 存在时才进行设置。
这四个参数从 Redis 2.6.12 版本开始支持,因为当前大多数在用的 Redis 都已经高于这个版本,所以推荐直接使用该命令来实现分布式锁。对应的 Jedis 代码如下:
```java
jedis.set("lockKey", "lockValue", SetParams.setParams().nx().ex(3));
```
此时一条命令就可以完成值和超时时间的设置,并且因为只有一条命令,因此其原子性也得到了保证。但因为引入了超时时间来避免死锁,同时也引出了其它两个问题:
<div align="center"> <img src="../pictures/redis_分布式锁原理.png"/> </div>
+ **问题一**:当业务处理的时间超过过期时间后(图中进程 A由于锁已经被释放此时其他进程就可以获得该锁图中进程 B这意味着有两个进程A 和 B同时进入了临界区此时分布式锁就失效了
+ **问题二**:如上图所示,当进程 A 业务处理完成后,此时删除的是进程 B 的锁,进而导致分布式锁又一次失效,让进程 B 和 进程 C 同时进入了临界区。
针对问题二,我们可以在创建锁时为其指定一个唯一的标识作为 Key 的 Value这里假设我们采用 `UUID + 线程ID` 来作为唯一标识:
```java
String identifier = UUID.randomUUID() + ":" + Thread.currentThread().getId();
jedis.set("LockKey", identifier, SetParams.setParams().nx().ex(3));
```
然后在删除锁前,先将该唯一标识与锁的 Value 值进行比较,如果不相等,证明该锁不属于当前的操作对象,此时不执行删除操作。为保证判断操作和删除操作整体的原子性,这里需要使用 Lua 脚本来执行:
```shell
if redis.call("get",KEYS[1]) == ARGV[1] then
return redis.call("del",KEYS[1])
else
return 0
end
```
这段脚本的意思是如果 value 的值与给定的值相同,则执行删除命令,否则直接返回状态码 0 。对应使用 Jedis 实现的代码如下:
```java
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
jedis.eval(script,
Collections.singletonList("LockKey"), // keys的集合
Collections.singletonList(identifier) // args的集合
);
```
接着再看问题一,问题一最简单的解决方法是:你可以估计业务的最大处理时间,然后保证设置的过期时间大于最大处理时间。但是由于业务会面临各种复杂的情况,因此可能无法保证业务每一次都能在规定的过期时间内处理完成,此时可以使用延长锁时效的策略。
### 1.3 延长锁时效
延长锁时效的方案如下:假设锁超时时间是 30 秒,此时程序需要每隔一段时间去扫描一下该锁是否还存在,扫描时间需要小于超时时间,通常可以设置为超时时间的 1/3在这里也就是 10 秒扫描一次。如果锁还存在,则重置其超时时间恢复到 30 秒。通过这种方案,只要业务还没有处理完成,锁就会一直有效;而当业务一旦处理完成,程序也会马上删除该锁。
Redis 的 Java 客户端 Redisson 提供的分布式锁就支持类似的延长锁时效的策略,称为 WatchDog直译过来就是 “看门狗” 机制。
以上讨论的都是单机环境下的 Redis 分布式锁,而想要保证 Redis 分布式锁是高可用,首先 Redis 得是高可用的Redis 的高可用模式主要有两种:哨兵模式和集群模式。以下分别进行讨论:
## 二、哨兵模式与分布式锁
哨兵模式是主从模式的升级版,能够在故障发生时自动进行故障切换,选举出新的主节点。但由于 Redis 的复制机制是异步的,因此在哨兵模式下实现的分布式锁是不可靠的,原因如下:
+ 由于主从之间的复制操作是异步的,当主节点上创建好锁后,此时从节点上的锁可能尚未创建。而如果此时主节点发生了宕机,从节点上将不会创建该分布式锁;
+ 从节点晋升为主节点后,其他进程(或线程)仍然可以在该新主节点创建分布式锁,此时就存在多个进程(或线程)同时进入了临界区,分布式锁就失效了。
因此在哨兵模式下,无法避免锁失效的问题。因此想要实现高可用的分布式锁,可以采取 Redis 的另一个高可用方案 —— Redis 集群模式。
## 三、集群模式与分布式锁
### 3.1 RedLock 方案
想要在集群模式下实现分布式锁Redis 提供了一种称为 RedLock 的方案,假设我们有 N 个 Redis 实例,此时客户端的执行过程如下:
+ 以毫秒为单位记录当前的时间,作为开始时间;
+ 接着采用和单机版相同的方式,依次尝试在每个实例上创建锁。为了避免客户端长时间与某个故障的 Redis 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为 10 秒,则访问每个 Redis 实例的超时时间可能在 5 到 50 毫秒之间,如果在这个时间内还没有建立通信,则尝试连接下一个实例;
+ 如果在至少 N/2+1 个实例上都成功创建了锁。并且 `当前时间 - 开始时间 < 锁的超时时间` ,则认为已经获取了锁,锁的有效时间等于 `超时时间 - 花费时间`(如果考虑不同 Redis 实例所在服务器的时钟漂移,则还需要减去时钟漂移);
+ 如果少于 N/2+1 个实例,则认为创建分布式锁失败,此时需要删除这些实例上已创建的锁,以便其他客户端进行创建。
+ 该客户端在失败后,可以等待一个随机的时间后,再次进行重试。
以上就是 RedLock 的实现方案,可以看到主要是由客户端来实现的,并不真正涉及到 Redis 集群相关的功能。因此这里的 N 个 Redis 实例并不要求是一个真正的 Redis 集群,它们彼此之间可以是完全独立的,但由于只需要半数节点获得锁就能真正获得锁,因此其仍然具备容错性和高可用性。后面使用 Redisson 来演示 RedLock 时会再次验证这一点。
### 3.2 低延迟通讯
另外实现 RedLock 方案的客户端与所有 Redis 实例进行通讯时,必须要保证低延迟,而且最好能使用多路复用技术来保证一次性将 SET 命令发送到所有 Redis 节点上,并获取到对应的执行结果。如果网络延迟较高,假设客户端 A 和 B 都在尝试创建锁:
```shell
SET key 随机数A EX 3 NX #A客户端
SET key 随机数B EX 3 NX #B客户端
```
此时可能客户端 A 在一半节点上创建了锁,而客户端 B 在另外一半节点上创建了锁,那么两个客户端都将无法获取到锁。如果并发很高,则可能存在多个客户端分别在部分节点上创建了锁,而没有一个客户端的数量超过 N/2+1。这也就是上面过程的最后一步中强调一旦客户端失败后需要等待一个随机时间后再进行重试的原因如果是一个固定时间则所有失败的客户端又同时发起重试情况就还是一样。
因此最佳的实现就是客户端的 SET 命令能几乎同时到达所有节点,并几乎同时接受到所有执行结果。 想要保证这一点,低延迟的网络通信极为关键,下文介绍的 Redisson 就采用 Netty 框架来保证这一功能的实现。
### 3.3 持久化与高可用
为了保证高可用,所有 Redis 节点还需要开启持久化。假设不开启持久化,假设进程 A 获得锁后正在处理业务逻辑,此时节点宕机重启,因为锁数据丢失了,其他进程便可以再次创建该锁,因此所有 Redis 节点都需要开启 AOF 的持久化方式。
AOF 默认的同步机制为 `everysec`,即每秒进程一次持久化,此时能够兼顾性能与数据安全,发生意外宕机的时,最多会丢失一秒的数据。但如果碰巧就是在这一秒的时间内进程 A 创建了锁,并由于宕机而导致数据丢失。此时其他进程还可以创建该锁,锁的互斥性也就失效了。想要解决这个问题有两种方式:
+ **方式一**:修改 Redis.conf 中 `appendfsync` 的值为 `always`,即每次命令后都进行持久化,此时会降低 Redis 性能,进而也会降低分布式锁的性能,但锁的互斥性得到了绝对的保证;
+ **方式二**:一旦节点宕机了,需要等到锁的超时时间过了之后才进行重启,此时相当于原有锁自然失效(但你首先需要保证业务能在设定的超时时间内完成),这种方案也称为延时重启。
## 四、Redisson
Redisson 是 Redis 的 Java 客户端,它提供了各种的 Redis 分布式锁的实现如可重入锁、公平锁、RedLock、读写锁等等并且在实现上考虑得也更加全面适用于生产环境下使用。
### 4.1 分布式锁
使用 Redisson 来创建单机版本分布式锁非常简单,示例如下:
```java
// 1.创建RedissonClient,如果与spring集成可以将RedissonClient声明为Bean,在使用时注入即可
Config config = new Config();
config.useSingleServer().setAddress("redis://192.168.0.100:6379");
RedissonClient redissonClient = Redisson.create(config);
// 2.创建锁实例
RLock lock = redissonClient.getLock("myLock");
try {
//3.尝试获取分布式锁,第一个参数为等待时间,第二个参数为锁过期时间
boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLock) {
// 4.模拟业务处理
System.out.println("处理业务逻辑");
Thread.sleep(20 * 1000);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.释放锁
lock.unlock();
}
redissonClient.shutdown();
```
此时对应在 Redis 中的数据结构如下:
<div align="center"> <img src="../pictures/redis_分布式锁_cli1.png"/> </div>
可以看到 key 就是代码中设置的锁名,而 value 值的类型是 hash其中键 `9280e909-c86b-43ec-b11d-6e5a7745e2e9:13` 的格式为 `UUID + 线程ID` ;键对应的值为 1代表加锁的次数。之所以要采用 hash 这种格式,主要是因为 Redisson 创建的锁是具有重入性的,即你可以多次进行加锁:
```java
boolean isLock1 = lock.tryLock(0, 30, TimeUnit.SECONDS);
boolean isLock2 = lock.tryLock(0, 30, TimeUnit.SECONDS);
```
此时对应的值就会变成 2代表加了两次锁
<div align="center"> <img src="../pictures/redis_分布式锁_cli2.png"/> </div>
当然和其他重入锁一样,需要保证解锁的次数和加锁的次数一样,才能完全解锁:
```java
lock.unlock();
lock.unlock();
```
### 4.2 RedLock
Redisson 也实现了 Redis 官方推荐的 RedLock 方案,这里我们启动三个 Redis 实例进行演示,它们彼此之间可以是完全独立的,并不需要进行集群的相关配置:
```shell
$ ./redis-server ../redis.conf
$ ./redis-server ../redis.conf --port 6380
$ ./redis-server ../redis.conf --port 6381
```
对应的代码示例如下:
```java
// 1.创建RedissonClient
Config config01 = new Config();
config01.useSingleServer().setAddress("redis://192.168.0.100:6379");
RedissonClient redissonClient01 = Redisson.create(config01);
Config config02 = new Config();
config02.useSingleServer().setAddress("redis://192.168.0.100:6380");
RedissonClient redissonClient02 = Redisson.create(config02);
Config config03 = new Config();
config03.useSingleServer().setAddress("redis://192.168.0.100:6381");
RedissonClient redissonClient03 = Redisson.create(config03);
// 2.创建锁实例
String lockName = "myLock";
RLock lock01 = redissonClient01.getLock(lockName);
RLock lock02 = redissonClient02.getLock(lockName);
RLock lock03 = redissonClient03.getLock(lockName);
// 3. 创建 RedissonRedLock
RedissonRedLock redLock = new RedissonRedLock(lock01, lock02, lock03);
try {
boolean isLock = redLock.tryLock(10, 300, TimeUnit.SECONDS);
if (isLock) {
// 4.模拟业务处理
System.out.println("处理业务逻辑");
Thread.sleep(200 * 1000);
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.释放锁
redLock.unlock();
}
redissonClient01.shutdown();
redissonClient02.shutdown();
redissonClient03.shutdown();
```
此时每个 Redis 实例上锁的情况如下:
<div align="center"> <img src="../pictures/redis_分布式锁_cli3.png"/> </div>
可以看到每个实例上都获得了锁。
### 4.3 延长锁时效
最后,介绍一下 Redisson 的 WatchDog 机制,它可以用来延长锁时效,示例如下:
```java
Config config = new Config();
// 1.设置WatchdogTimeout
config.setLockWatchdogTimeout(30 * 1000);
config.useSingleServer().setAddress("redis://192.168.0.100:6379");
RedissonClient redissonClient = Redisson.create(config);
// 2.创建锁实例
RLock lock = redissonClient.getLock("myLock");
try {
//3.尝试获取分布式锁,第一个参数为等待时间
boolean isLock = lock.tryLock(0, TimeUnit.SECONDS);
if (isLock) {
// 4.模拟业务处理
System.out.println("处理业务逻辑");
Thread.sleep(60 * 1000);
System.out.println("锁剩余的生存时间:" + lock.remainTimeToLive());
}
} catch (Exception e) {
e.printStackTrace();
} finally {
//5.释放锁
lock.unlock();
}
redissonClient.shutdown();
```
首先 Redisson 的 WatchDog 机制只会对那些没有设置锁超时时间的锁生效,所以我们这里调用的是两个参数的 `tryLock()` 方法:
```java
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
```
而不是包含超时时间的三个参数的 `tryLock()` 方法:
```java
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
```
其次我们通过 `config.setLockWatchdogTimeout(30 * 1000)` 将 lockWatchdogTimeout 的值设置为 30000 毫秒(默认值也是 30000 毫秒)。此时 Redisson 的 WatchDog 机制会以 lockWatchdogTimeout 的 1/3 时长为周期(在这里就是 10 秒对所有未设置超时时间的锁进行检查如果业务尚未处理完成也就是锁还没有被程序主动删除Redisson 就会将锁的超时时间重置为 lockWatchdogTimeout 指定的值(在这里就是设置的 30 秒),直到锁被程序主动删除位置。因此在上面的例子中可以看到,不论将模拟业务的睡眠时间设置为多长,其锁都会存在一定的剩余生存时间,直至业务处理完成。
反之,如果明确的指定了锁的超时时间 leaseTime则以 leaseTime 的时间为准,因为 WatchDog 机制对明确指定超时时间的锁不会生效。
## 参考资料
+ [Distributed locks with Redis](https://redis.io/topics/distlock)
+ [Redisson Distributed locks and synchronizers](https://github.com/redisson/redisson/wiki/8.-distributed-locks-and-synchronizers)

View File

@ -34,10 +34,10 @@ RDB 机制是以指定的时间间隔将 Redis 中的数据生成快照并保存
除了手动使用命令触发外,在某些场景下也会自动触发 Redis 的 RDB 机制:
+ 在 `redis.conf` 中配置了 `save m n` ,表示如果在 m 秒内存在了 n 次修改操作时,则自动触发`bgsave`;
+ 如果从节点执行全量复制操作,则主节点自动执行`bgsave`,并将生成的 RDB 文件发送给从节点;
+ 执行 `debug reload` 命令重新加载 Redis 时,会触发`save`操作;
+ 执行 `shutdown` 命令时候,如果没有启用 AOF 持久化则默认采用`bgsave`进行持久化。
+ 在 `redis.conf` 中配置了 `save m n` ,表示如果在 m 秒内存在了 n 次修改操作时,则自动触发 `bgsave`;
+ 如果从节点执行全量复制操作,则主节点自动执行 `bgsave`,并将生成的 RDB 文件发送给从节点;
+ 执行 `debug reload` 命令重新加载 Redis 时,会触发 `save` 操作;
+ 执行 `shutdown` 命令时候,如果没有启用 AOF 持久化则默认采用 `bgsave ` 进行持久化。
### 2.3 相关配置
@ -45,7 +45,7 @@ RDB 机制是以指定的时间间隔将 Redis 中的数据生成快照并保存
RDB 文件默认保存在 Redis 的工作目录下,默认文件名为 `dump.rdb`,可以通过静态或动态方式修改:
+ 静态配置:通过修改 `redis.conf` 中的工作目录`dir`和数据库存储文件名`dbfilename`两个配置;
+ 静态配置:通过修改 `redis.conf` 中的工作目录 `dir` 和数据库存储文件名 `dbfilename` 两个配置;
+ 动态修改:通过在命令行中执行以下命令:
@ -72,7 +72,7 @@ AOF 是 Redis 提供的另外一种持久化的方式,它以独立日志的方
### 3.2 同步策略
Redis 提供了三种同步策略,用于控制 AOF 缓冲区同步数据到磁盘上的行为,由参数`appendfsync`控制:
Redis 提供了三种同步策略,用于控制 AOF 缓冲区同步数据到磁盘上的行为,由参数 `appendfsync` 控制:
| 可选配置 | 说明 |
| -------- | ------------------------------------------------------------ |
@ -85,11 +85,11 @@ write 和 fsync 操作说明:
- write 操作会触发延迟写机制Linux 在内核提供页缓冲区用来提高硬盘的 IO 性能write 操作在写入系统缓冲区后直接返回。同步操作依赖于系统调度机制,例如缓冲区页空间写满或达到特定时间周期。 同步文件之前,如果此时系统故障宕机,缓冲区内数据将丢失。
- fsync 针对单个文件操作做强制硬盘同步fsync 操作将阻塞直到写入硬盘完成后返回,它保证了数据持久化的安全。
Redis 默认的同步机制为`everysec`,此时能够兼顾性能和保证数据安全,在发生意外宕机的时,最多会丢失一秒的数据。
Redis 默认的同步机制为 `everysec`,此时能够兼顾性能和保证数据安全,在发生意外宕机的时,最多会丢失一秒的数据。
### 3.3 相关配置
想要使用 AOF 功能,需要配置 `appendonly `的值为`yes`,默认值为`no`。默认 AOF 的文件名为 `appendonly.aof`, 可以通过修改`appendfilename`的值进行修改,和 RDB 文件的保存位置一样,默认保存在 Redis 的工作目录下。
想要使用 AOF 功能,需要配置 `appendonly ` 的值为 `yes`,默认值为 `no`。默认 AOF 的文件名为 `appendonly.aof`, 可以通过修改`appendfilename` 的值进行修改,和 RDB 文件的保存位置一样,默认保存在 Redis 的工作目录下。
## 四、对比分析

View File

@ -0,0 +1,342 @@
# Tomcat 架构解析
<nav>
<a href="#一Tomcat-简介">一、Tomcat 简介</a><br/>
<a href="#二Tomcat-架构">二、Tomcat 架构</a><br/>
<a href="#三连接器">三、连接器</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#31-ProtocolHandler">3.1 ProtocolHandler</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#32-Adapter">3.2 Adapter</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#33-Mapper-和-MapperListener">3.3 Mapper 和 MapperListener</a><br/>
<a href="#四容器">四、容器</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#41-Container-和-Lifecycle">4.1 Container 和 Lifecycle</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#42-分层结构">4.2 分层结构</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#43-Pipeline-和-Valve">4.3 Pipeline 和 Valve</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#44-FilterChain">4.4 FilterChain</a><br/>
<a href="#五请求流程">五、请求流程</a><br/>
<a href="#六启动流程">六、启动流程</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#1-startupsh--catalinash">1. startup.sh & catalina.sh</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#2-Bootstrap">2. Bootstrap</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#3-Catalina">3. Catalina</a><br/>
<a href="#七类加载器">七、类加载器</a><br/>
</nav>
## 一、Tomcat 简介
Tomcat 是目前主流的基于 Java 语言的轻量级应用服务器,它是对是 Java ServletJavaServer PagesJSPJava Expression LanguageEL 表达式)和 Java WebSocket 技术的开源实现。当前 Tomcat 共有四个版本:
+ **Tomcat 7**:支持 Servlet 3.0JSP 2.2EL 2.2 和 WebSocket 1.1 规范。
+ **Tomcat 8.5**:支持 Servlet 3.1JSP 2.3EL 3.0 和 WebSocket 1.1 规范,并可以通过安装 Tomcat 原生库来支持 HTTP/2 。当前 Tomcat 8.5 已经完全取代了 Tomcat 8Tomcat 8 已停止维护,并不再提供下载。
+ **Tomcat 9**:是当前主要的发行版;它建立在 Tomcat 8.0.x 和 8.5.x 之上,并实现了 Servlet 4.0JSP 2.3EL 3.0WebSocket 1.1 和 JASPIC 1.1 规范。
+ **Tomcat 10 (alpha)** :是当前主要的开发版;它实现了 Servlet 5.0JSP 3.0EL 4.0 和 WebSocket 2.0 规范。
## 二、Tomcat 架构
Tomcat 的整体架构如下:
<div align="center"> <img src="..\pictures\tomcat_架构.png"/> </div>
+ **Server**:表示整个 Servlet 容器,在整个 Tomcat 运行环境中只有唯一一个 Server 实例。一个 Server 包含多个 Service每个 Service 互相独立,但共享一个 JVM 以及系统类库。
+ **Service**:一个 Service 负责维护多个 Connector 和一个 Engine。其中 Connector 负责开启 Socket 并监听客户端请求返回响应数据Engine 负责具体的请求处理。
+ **Connector**:连接器,用于监听并转换来自客户端 Socket 请求,然后将 Socket 请求交由 Container 处理,支持不同协议以及不同的 I/O 方式。
+ **Engine**:表示整个 Servlet 引擎,在 Tomcat 中Engine 为最高层级的容器对象。
+ **Host**:表示 Engine 中的虚拟机,通常与一个服务器的网络名有关,如域名等。
+ **Context**:表示 ServletContext ,在 Servlet 规范中,一个 ServletContext 即表示一个独立的 Web 应用。
+ **Wrapper**:是对标准 Servlet 的封装。
以上各组件的详细介绍如下:
## 三、连接器
连接器的主要功能是将 Socket 的输入转换为 Request 对象,并交由容器进行处理;之后再将容器处理完成的 Response 对象写到输出流。连接器的内部组件如下:
<div align="center"> <img src="..\pictures\tomcat连接器组件.png"/> </div>
### 3.1 ProtocolHandler
**1. Endpoint**
EndPoint 会启动线程来监听服务器端口,并负责处理来自客户端的 Socket 请求,是对传输层的抽象。它支持以下 IO 方式:
+ **BIO**:即最传统的 I/O 方式;
+ **NIO**:采用 Java NIO 类库进行实现Tomcat 8 之后默认该 I/O 方式,以替换原来的 BIO
+ **NIO2**:采用 JDK 7 最新的 NIO2 类库进行实现;
+ **APR**:采用 APR (Apache 可移植运行库) 实现APR 是使用 C/C++ 编写的本地库,需要单独进行安装。
**2. Processor**
负责构造 Request 和 Response 对象,并通过 Adapter 提交到容器进行处理,是对应用层的抽象。它支持以下应用层协议:
+ **HTTP / 1.1 协议**
+ **HTTP / 2.0 协议**:自 Tomcat 8.5 以及 9.0 版本后开始支持;
+ **AJP 协议**:即定向包协议。
**3. ProtocolHandler**
ProtocolHandler 通过组合不同类型的 Endpoint 和 Processor 来实现针对具体协议的处理功能。按照不同的协议HTTP 和 AJP和不同的 I/O 方式NIONIO2AJP进行组合其有以下六个具体的实现类
<div align="center"> <img src="..\pictures\tomcat_AbstractProtocol.png"/> </div>
**4. 协议升级**
可以看到上面的 ProtocolHandler 只有对 HTTP 1.1 协议进行处理的实现类,并没有对 HTTP 2.0 进行处理的实现类,想要对 HTTP 2.0 进行处理,需要使用到协议升级:当 ProtocolHandler 收到的是一个 HTTP 2.0 请求时,它会根据请求创建一个用于升级处理的令牌 UpgradeToken该令牌中包含了升级处理器 HttpUpgradeHandler接口对于 HTTP 2.0 而言,其实现类是 Http2UpgradeHandler。
### 3.2 Adapter
Tomcat 设计者希望连接器是一个单独的组件,能够脱离 Servlet 规范而独立存在,以便增加其使用场景,因此 Process 对输入流封装后得到的 Request 不是一个 Servlet Request该 Request 的全限定命名为org.apache.coyote.Request 。因此在这里需要使用适配器模式(具体实现类是 CoyoteAdapter将其转换为 org.apache.catalina.connector.Request它才是标准的 ServletRequest 的实现:
<div align="center"> <img src="..\pictures\tomcat_request.png"/> </div>
### 3.3 Mapper 和 MapperListener
由 Socket 输入流构建好标准的 ServletRequest 后,连接器还需要知道将 Request 发往哪一个容器,这需要通过 Mapper 来实现。Mapper 维护了请求路径与容器之间的映射信息。在 Tomcat 7 及之前的版本中 Mapper 由连接器自身维护,在 Tomcat 8 之后的版本中Mapper 由 Service 进行维护。
MapperListener 实现了 ContainerListener 和 LifecycleListener 接口,用于在容器组件状态发生变更时,注册或取消对应容器的映射关系,这么做主要是为了支持 Tomcat 的热部署功能。
## 四、容器
### 4.1 Container 和 Lifecycle
Tomcat 中的所有容器都实现了 Container 接口,它定义了容器共同的属性与方法,而 Container 接口则继承自 Lifecycle 接口。Tomcat 中的大多数组件都实现了 Lifecycle 接口,它定义了与组件生命周期相关的公共方法,如 `init()``start()``stop()``destroy()` :
<div align="center"> <img src="..\pictures\tomcat_container.png"/> </div>
### 4.2 分层结构
Tomcat 之所以采用分层的结构,主要是为了更好的灵活性和可扩展性:
+ **Engine**:最顶层的容器,一个 Service 中只有一个 Engine
+ **Host**:代表一个虚拟主机,一个 Engine 可以包含多个虚拟主机;
+ **Context**:表示一个具体的 Web 应用程序,一个虚拟主机可以包含多个 Context
+ **Wrapper**:是 Tomcat 对 Servlet 的包装,一个 Context 中可以有多个 Wrapper。
<div align="center"> <img src="..\pictures\tomcat_分层结构.png"/> </div>
Tomcat 容器的分层结构在其 conf 目录下的 `server.xml` 配置文件中也有体现:
```xml
<Server port="8005" shutdown="SHUTDOWN">
<Service name="Catalina">
<Connector port="8080" protocol="HTTP/1.1"connectionTimeout="20000"redirectPort="8443" />
<Engine name="Catalina" defaultHost="localhost">
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
</Engine>
</Service>
</Server>
```
这里的 appBase 代表我们应用程序所在父目录,我们部署的每一个应用程序就是一个独立的 Context 。
### 4.3 Pipeline 和 Valve
<div align="center"> <img src="..\pictures\tomcat_多层容器.jpg"/> </div>
由连接器发过来的请求会最先发送到 Engine最终逐层传递直至我们编写的 Servlet这种传递主要通过 Pipeline 和 Valve 来实现。每层容器都有自己的 PipelinePipeline 相当于处理管道;每个 Pipeline 中有一个 Valve 链,每个 Valve 可以看做一个独立的处理单元,用于对请求进行处理。最基础的 Valve 叫做 Basic Valve新增的 Valve 会位于已有的 Valve 之前。Pipeline 和 Valve 的接口定义如下:
```java
public interface Pipeline extends Contained {
public Valve getBasic(); // 获得Basic Valve
public void setBasic(Valve valve); // 设置Basic Valve
public void addValve(Valve valve); // 新增Valve
public Valve[] getValves(); // 获取所有Valve
public void removeValve(Valve valve);// 移除Valve
public Valve getFirst(); //获取第一个 Valve
public boolean isAsyncSupported();
public void findNonAsyncValves(Set<String> result);
}
```
```java
public interface Valve {
public Valve getNext();
// 每一个Valve都持有其下一个Valve,这是标准的责任链模式
public void setNext(Valve valve);
// 对请求进行检查、处理或增强
public void invoke(Request request, Response response) throws IOException, ServletException;
public void backgroundProcess();
public boolean isAsyncSupported();
}
```
通过 Pipeline 的 Valve 责任链模式,每一层容器都可以很方便地进行功能的扩展,来对请求进行检查、处理或增强。每一层处理完成后,就会传递到下一层的 First Valve由下一层进行处理。以 Engine 容器为例,其实现类为 StandardEngine
```java
public class StandardEngine extends ContainerBase implements Engine {
public StandardEngine() {
super();
pipeline.setBasic(new StandardEngineValve());
....
}
}
```
在 StandardEngine 创建时就会为其 Pipeline 设置上一个名为 StandardEngineValve 的 Basic ValveStandardEngineValve 的实现如下:
```java
final class StandardEngineValve extends ValveBase {
public StandardEngineValve() {super(true);}
@Override
public final void invoke(Request request, Response response) throws IOException, ServletException {
// 获取当前请求的Host
Host host = request.getHost();
if (host == null) {
return;
}
if (request.isAsyncSupported()) {
request.setAsyncSupported(host.getPipeline().isAsyncSupported());
}
// 将请求传递给host的Pipeline的第一个Valve
host.getPipeline().getFirst().invoke(request, response);
}
}
```
Engine 的 Basic Valve即最后一个 Valve`invoke` 方法中会获取到下一级容器Host的第一个 Valve从而完成首尾相接。
### 4.4 FilterChain
通过 Pipeline 和 Valve 的传递,请求最终会传递到最内层容器 Wrapper 的 Basic Valve其实现类为 StandardWrapperValve 。StandardWrapperValve 会在 `invoke` 方法中为该请求创建 FilterChain依次执行请求对应的过滤器
```java
// 为该请求创建Filter Chain
ApplicationFilterChain filterChain = ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
.....
// 调用Filter Chain的doFilter方法
filterChain.doFilter(request.getRequest(), response.getResponse());
```
当到达执行链的末端后,会执行 servlet 的 service 方法:
```java
servlet.service(request, response);
```
以我们最常使用的 HttpServlet 为例,其最终的 service 方法如下:
```shell
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String method = req.getMethod();
if (method.equals(METHOD_GET)) {
long lastModified = getLastModified(req);
if (lastModified == -1) {
.....
doGet(req, resp);
} else {
......
}
} else if (method.equals(METHOD_HEAD)) {
long lastModified = getLastModified(req);
maybeSetLastModified(resp, lastModified);
doHead(req, resp);
} else if (method.equals(METHOD_POST)) {
doPost(req, resp);
} else if (method.equals(METHOD_PUT)) {
doPut(req, resp);
} else if (method.equals(METHOD_DELETE)) {
doDelete(req, resp);
} else if (method.equals(METHOD_OPTIONS)) {
doOptions(req,resp);
} else if (method.equals(METHOD_TRACE)) {
doTrace(req,resp);
} ......
}
```
至此,来自客户端的请求就逐步传递到我们编写的 doGet 或者 doPost 方法中。
## 五、请求流程
这里对前面的连接器和容器章节进行总结Tomcat 对客户端请求的完整处理流程如下:
<div align="center"> <img src="..\pictures\tomcat启动请求处理流程.jpg"/> </div>
## 六、启动流程
Tomcat 整体的启动流程如下图所示:
<div align="center"> <img src="..\pictures\tomcat启动流程.png"/> </div>
#### 1. startup.sh & catalina.sh
`startup.sh` 是对 `catalina.sh` 的一层薄封装,主要用于检查 `catalina.sh` 是否存在以及调用它。 `catalina.sh` 负责启动一个 JVM 来运行 Tomcat 的启动类 Bootstrap 。
#### 2. Bootstrap
Bootstrap 独立于 Tomcat 结构之外,它以 JAR 包的形式存在于 `bin` 目录下,主要负责初始化 Tomcat 的类加载器,并通过反射来创建 Catalina。
#### 3. Catalina
Catalina 通过 Digester 解析 server.xml 来创建所有的服务组件。Digester 是一款能将 XML 转换为 Java 对象的事件驱动型工具,简而言之,它通过流读取 XML 文件,当识别出特定 XML 节点后,就会创建对应的组件。
## 七、类加载器
Tomcat 并没有完全沿用 JVM 默认的类加载机制,为了保证 Web 应用之间的隔离性和加载的灵活性,其采用了下图所示的类加载机制:
<div align="center"> <img width="600px" src="..\pictures\tomcat_类加载器.jpg"/> </div>
#### 1. Web App Class Loader
负责加载 `/WEB-INF/classes` 目录下的未压缩的 Class 和资源文件,以及 `/WEB-INF/lib` 目录下的 Jar 包。它只对当前的 Web 应用可见,对其它 Web 应用均不可见,因此它可以保证 Web 应用之间的彼此隔离。
#### 2. Shared Class Loader
是所有 Web 应用的父类加载器,它负责加载 Web 应用之间共享的类,从而避免资源的重复加载。
#### 3. Catalina Class Loader
用于加载 Tomcat 应用服务器的类加载器,从而保证 Tomcat 与 Web 应用程序之间的隔离。
#### 4. Common Class Loader
其作用和 Shared Class Loader 类似,当 Tomcat 与 Web 应用程序之间存在共同依赖时,可以使用其进行加载。再往上,流程就与 JVM 类加载的流程一致了。
## 参考资料
+ 刘光瑞 . Tomcat架构解析 . 人民邮电出版社 . 2017-05

213
notes/WireShark_HTTPS.md Normal file
View File

@ -0,0 +1,213 @@
# 使用 WireShark 分析 HTTPS 握手过程
<nav>
<a href="#一WireShark">一、WireShark</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-下载安装">1.1 下载安装</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-握手流程回顾">1.2 握手流程回顾</a><br/>
<a href="#二TCP-握手过程">二、TCP 握手过程</a><br/>
<a href="#三TLSSSL-握手过程">三、TLS/SSL 握手过程</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#31-Client-Hello">3.1 Client Hello</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#32-Server-Hello">3.2 Server Hello</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#33-Server-Hello-的详细过程">3.3 Server Hello 的详细过程</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#34-Client-Key-Exchange">3.4 Client Key Exchange</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#35-计算预主密钥和主密钥">3.5 计算预主密钥和主密钥</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#36-Change-Cipher-Spec">3.6 Change Cipher Spec</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#37-Encrypted-Handshake-Message">3.7 Encrypted Handshake Message</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#38-New-Session-Ticket">3.8 New Session Ticket</a><br/>
</nav>
## 一、WireShark
这篇文章是对上一篇文章的补充,主要是使用 WireShark 来进行抓包分析,以更加直观的方式,帮助大家理解 HTTPS 的整个握手过程。
### 1.1 下载安装
Wireshark 是一个网络抓包分析软件下载地址为https://www.wireshark.org/index.html#download ,下载后直接安装即可。启动后,进入主页面,需要先选择对应的抓包网络:
<div align="center"> <img src="../pictures/wireshark_主界面.png"/> </div>
这里我使用的是无线网络,因此选择 **WLAN**,双击后即可进入抓包页面。下面我们以 CSDN 的首页 `https://www.csdn.net` 为例,我们首先通过 ping 命令获取其 IP 地址:
<div align="center"> <img src="../pictures/wireshark_ping.png"/> </div>
可以看到,其 IP 地址为 `47.95.164.112` 。为了避免浏览器上其他网站的干扰,我们先通过该 IP 地址进行过滤:
<div align="center"> <img src="../pictures/wireshark_捕获信息.png"/> </div>
输入过滤条件 `ip.addr == 47.95.164.112` 后,敲击回车键即可。可以看到 TCP 和 TLS 的握手消息都已经被过滤出来。
### 1.2 握手流程回顾
这里首先以图片的方式回顾整个 HTTPS 的握手过程:
<div align="center"> <img src="../pictures/https_tsl_v1.2_ECDHE.jpg"/> </div>
然后我们再逐步通过 WireShark 进行分析:
## 二、TCP 握手过程
首先 TLS/SSL 协议是基于 TCP/IP 协议的,因此需要先等待 TCP 三次握手完成:
<div align="center"> <img src="../pictures/wireshark_三次握手.png"/> </div>
从上图被被捕获的记录中,可以很直观的看出整个握手过程与下图是完全匹配的:
<div align="center"> <img src="../pictures/三次握手.png"/> </div>
该过程比较简单,这里就不再进行赘述了。
## 三、TLS/SSL 握手过程
整个 TLS/ SSL 的握手过程如下:
<div align="center"> <img src="../pictures/wireshark_密钥交换过程.png"/> </div>
### 3.1 Client Hello
TCP 握手完成后,客户端首先发起一个 `Client Hello` 请求,里面包含客户端版本号、所有支持的密码套件、以及一个随机数 `Client Random`
<div align="center"> <img src="../pictures/wireshark_client_hello.png"/> </div>
### 3.2 Server Hello
服务器收到该请求后,会返回一个 `Server Hello` 消息,里面包含选中的用于本次通信的密码套件,以及一个随机数 `Server Random`
<div align="center"> <img src="../pictures/wireshark_server_hello.png"/> </div>
### 3.3 Server Hello 的详细过程
接着服务器会在一次返回里面返回多组消息:
<div align="center"> <img src="../pictures/wireshark_server_hello_done.png"/> </div>
这里分别对其进行解释:
#### 1. Certificate
这里面包含的是证书信息:
<div align="center"> <img src="../pictures/wireshark_certificate.png"/> </div>
这里我将里面的部分内容复制了出来:
```shell
Certificate: 308206873082056fa00302010202100b038a343b5b17ae92… (id-at-commonName=*.csdn.net,id-at-organizationalUnitName=IT,id-at-organizationName=北京创新乐知信息技术有限公司,id-at-localityName=北京市,id-at-countryName=CN
```
可以看到,这就是在浏览器中使用的证书的:
<div align="center"> <img src="../pictures/wireshark_csdn_证书.png"/> </div>
#### 2. Certificate Status
这一步主要是依据在线证书状态协议OCSPOnline Certificate Status Protocol对当前证书状态进行查询
<div align="center"> <img src="../pictures/wireshark_certifcate_status.png"/> </div>
#### 3. Server Key Exchange
这里面包含了进行 ECDHE 算法所需的各种参数 `Server Params`
<div align="center"> <img src="../pictures/wireshark_server_key_exchange.png"/> </div>
#### 4. Server Hello Done
这个就是用于告知客户端服务器的整个 Hello 过程已经结束,并不包含任何内容:
<div align="center"> <img src="../pictures/wireshark_server_hello_done_protocol.png"/> </div>
### 3.4 Client Key Exchange
之后,客户端按照密码套件的要求,也生成一个 `Client Params`,并通过 `Client Key Exchange` 消息发送给服务器;
<div align="center"> <img src="../pictures/wireshark_client_key_exchange.png"/> </div>
`Client Key Exchange` 具体内容如下:
<div align="center"> <img src="../pictures/wireshark_client_key_exchange_protocol.png"/> </div>
### 3.5 计算预主密钥和主密钥
1. 经过以上步骤的交换,服务器和客户端都有了 `Client Params``Server Params`它们分别根据这两个参数进行计算ECDHE 算法能够保证客户端和服务器算出来的值是一样的,这个算出来的值就是预主密钥 `Pre-Master`
2. 最后再利用预主秘钥,客户端随机数,服务器随机数共同算出真正的主密钥 `Master_Secret`,算法如下:
```shell
master_secret = PRF(pre_master_secret,
"master secret",
ClientHello.random + ServerHello.random)
```
这里的 PRF 是一个伪随机函数,用于增强 master_secret 的随机性。主密钥 `Master_Secret` 的长度固定为 48 个字节,从中可以推导出以下四个密钥:
+ client_write_MAC_key
+ server_write_MAC_key
+ client_write_key
+ server_write_key
`client_write_MAC_key``server_write_MAC_key` 主要用于身份验证和完整性校验,而 `client_write_key``server_write_key` 则是对称加密中真正使用的会话密钥:
+ 客户端使用 `client_write_key` 加密,服务器对称地使用 `client_write_key` 解密;
+ 服务器使用 `server_write_key` 加密,客户端对称地使用 `server_write_key` 解密。
### 3.6 Change Cipher Spec
有了会话密钥后,客户端会发送一个 `Change Cipher Spec` 请求,告知服务器将加密方式由非对称加密转换为对称加密;
<div align="center"> <img src="../pictures/wireshark_change_cipher_spec.png"/> </div>
### 3.7 Encrypted Handshake Message
紧接着为了验证对称加密客户端会将所有的握手消息Handshake Message进行加密通过一个 `Encrypted Handshake Message` 请求发送给服务器;
<div align="center"> <img src="../pictures/wireshark_handshake_message.png"/> </div>
### 3.8 New Session Ticket
1. 服务器解密并进行验证,如果验证通过,则也返回给客户端 `Change Cipher Spec``Encrypted Handshake Message` 消息,代表双方共同达成协议,之后的通信都采用对称加密的方式。
2. 除此之外,服务器还会返回一个 `New Session Ticket` 消息。 `New Session Ticket` 是一种优化连接的方式:服务器将会话信息加密后以 Ticket票据的方式发送给客户端 ,服务器本身并不存储会话信息。客户端接收到 Ticket 后,将其储到内存中,如果想恢复会话,则下次连接的时候将票据发送给服务器端;服务器端进行解密,如果确认无误则表示可以进行会话恢复,这样就简化了以上复杂的握手过程。
<div align="center"> <img src="../pictures/wireshark_new_session_ticket.png"/> </div>
至此,握手过程全部完成。

View File

@ -0,0 +1,380 @@
# ZooKeeper 分布式锁原理
<nav>
<a href="#一实现原理">一、实现原理</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#11-临时节点方案">1.1 临时节点方案</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#12-临时有序节点方案">1.2 临时有序节点方案</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#13-读写锁">1.3 读写锁</a><br/>
<a href="#二-Apache-Curator">二、 Apache Curator</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#21-基本使用">2.1 基本使用</a><br/>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;<a href="#22-源码解析">2.2 源码解析</a><br/>
</nav>
## 一、实现原理
JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源JDK 原生的锁就无能为力了。此时可以使用 Zookeeper 来实现分布式锁。具体分为以下两种方案:
### 1.1 临时节点方案
<div align="center"> <img src="../pictures/zookeeper_分布式锁_临时节点方法.png"/> </div>
临时节点方案的原理如下:
+ 让多个进程(或线程)竞争性地去创建同一个临时节点,由于 ZooKeeper 不允许存在两个完全相同节点,因此必然只有一个进程能够抢先创建成功
+ 假设是进程 A 成功创建了节点,则它获得该分布式锁。此时其他进程需要在 parent_node 上注册监听,监听其下所有子节点的变化,并挂起当前线程;
+ 当 parent_node 下有子节点发生变化时候,它会通知所有在其上注册了监听的进程。这些进程需要判断是否是对应的锁节点上的删除事件。如果是,则让挂起的线程继续执行,并尝试再次获取锁。
这里之所以使用临时节点是为了避免死锁:进程 A 正常执行完业务逻辑后,会主动地去删除该节点,释放锁。但如果进程 A 意外宕机了,由于声明的是临时节点,因此该节点也会被移除,进而避免死锁。
临时节点方案的实现比较简单,但是其缺点也比较明显:
+ **缺点一**:当 parent_node 下其他锁变动或者被删除时,进程 BCD 也会收到通知,但是显然它们并不关心其他锁的释放情况。如果 parent_node 下存在大量的锁,并且程序处于高并发状态下,则 ZooKeeper 集群就需要频繁地通知客户端,这会带来大量的网络开销;
+ **缺点二**:采用临时节点方案创建的锁是非公平的,也就是说在进程 A 释放锁后,进程 BCD 发起重试的顺序与其收到通知的时间有关,而与其第一次尝试获取锁的时间无关,即与等待时间的长短无关。
当程序并发量不高时,可以采用该方案来实现,因为其实现比较简单。而如果程序并发量很高,则需要采用下面的临时有序节点方案:
### 1.2 临时有序节点方案
<div align="center"> <img src="../pictures/zookeeper_分布式锁_临时有序节点方案.png"/> </div>
采用临时有序节点时,对应的流程如下:
+ 每个进程(或线程)都会尝试在 parent_node 下创建临时有序节点,根据临时有序节点的特性,所有的进程都会创建成功;
+ 然后每个进程需要获取当前 parent_node 下该锁的所有临时节点的信息,并判断自己是否是最小的一个节点,如果是,则代表获得该锁;
+ 如果不是,则挂起当前线程。并对其前一个节点注册监听(这里可以通过 exists 方法传入需要触发 Watch 事件);
+ 如上图所示,当进程 A 处理完成后,会触发进程 B 注册的 Watch 事件,此时进程 B 就知道自己获得了锁,从而可以将挂起的线程继续,并开始业务的处理。
这里需要注意的是一种特殊的情况,其过程如下:
+ 如果进程 B 创建了临时节点,并且通过比较后知道自己不是最小的一个节点,但还没有注册监听;
+ 而 A 进程此时恰好处理完成并删除了 01 节点;
+ 接着进程 B 再调用 exist 方法注册监听就会抛出 IllegalArgumentException 异常。这虽然是一个异常,通常代表前一个节点已经不存在了。
在这种情况下进程 B 应该再次尝试获取锁,如果获取到锁,则就可以开始业务的处理。下文讲解 Apache Curator 源码时也会再次说明这一点。
通过上面对的介绍,可以看出来临时有序节点方案正好解决了临时节点方案的两个缺点:
+ 每个临时有序节点只需要关心它的上一个节点,而不需要关心其他的额外节点和额外事件;
+ 实现的锁是公平的,先到达的进程创建的临时有序节点的值越小,因此能更快地获得锁。
临时有序节点方案的另外一个优点是其能够实现共享锁,比如读写锁中的读锁。
### 1.3 读写锁
如下图所示,可以将临时有序节点分为读锁节点和写锁节点:
+ 对于读锁节点而言,其只需要关心前一个写锁节点的释放。如果前一个写锁释放了,则多个读锁节点对应的线程可以并发地读取数据;
+ 对于写锁节点而言,其只需要关心前一个节点的释放,而不需要关心前一个节点是写锁节点还是读锁节点。因为为了保证有序性,写操作必须要等待前面的读操作或者写操作执行完成。
<div align="center"> <img src="../pictures/zookeeper_分布式读写锁.png"/> </div>
## 二、 Apache Curator
### 2.1 基本使用
Apache Curator 是 ZooKeeper 的 Java 客户端,它基于临时有序节点方案实现了分布式锁、分布式读写锁等功能。使用前需要先导入 Apache Curator 和 ZooKeeper 相关的依赖,并保证 ZooKeeper 版本与服务器上 ZooKeeper 的版本一致:
```xml
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-framework</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.curator</groupId>
<artifactId>curator-recipes</artifactId>
<version>4.3.0</version>
</dependency>
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.14</version>
</dependency>
```
基本使用如下:
```java
RetryPolicy retryPolicy = new RetryNTimes(3, 5000);
CuratorFramework client = CuratorFrameworkFactory.builder()
.connectString("192.168.0.105:2181")
.sessionTimeoutMs(10000).retryPolicy(retryPolicy)
.namespace("mySpace").build();
client.start();
// 1. 创建分布式锁
InterProcessMutex lock = new InterProcessMutex(client, "/distributed/myLock");
// 2.尝试获取分布式锁
if (lock.acquire(10, TimeUnit.SECONDS)) {
try {
System.out.println("模拟业务耗时");
Thread.sleep(3 * 1000);
} finally {
// 3.释放锁
lock.release();
}
}
client.close();
```
之后就可以启动多个程序进程来进行测试,此时 ZooKeeper 上的数据结构如下:
<div align="center"> <img src="../pictures/zookeeper_分布式锁_cli.png"/> </div>
在我们指定的路径下,会依次创建多个临时有序节点,而当业务逻辑处理完成后,这些节点就会被移除。这里我们使用的是单机版本的 ZooKeeper ,而集群环境下也是一样,和 Redis 主从模式下的延迟复制会导致数据不一致的情况不同ZooKeeper 集群各个节点上的数据一致性可以由其自身来进行保证。
### 2.2 源码解析
Apache Curator 底层采用的是临时有序节点的实现方案,下面我们来看一下其源码中具体是如何实现的:
#### 1. 获取锁源码解析
上面最核心的方法是获取锁的 `acquire()` 方法 ,其定义如下:
```java
@Override
public boolean acquire(long time, TimeUnit unit) throws Exception{
return internalLock(time, unit);
}
```
可以看到,它在内部调用了 `internalLock()` 方法internalLock 方法的源码如下:
```java
// threadData是一个线程安全的Map其中Thread是持有锁的线程,LockData是锁数据
private final ConcurrentMap<Thread, LockData> threadData = Maps.newConcurrentMap();
private boolean internalLock(long time, TimeUnit unit) throws Exception{
Thread currentThread = Thread.currentThread();
// 首先查看threadData中是否已经有当前线程对应的锁
LockData lockData = threadData.get(currentThread);
if ( lockData != null ){
//如果锁已存在则将其计数器加1这一步是为了实现可重入锁
lockData.lockCount.incrementAndGet();
return true;
}
// 【核心方法:尝试获取锁】
String lockPath = internals.attemptLock(time, unit, getLockNodeBytes());
// 如果获取到锁则将其添加到threadData中
if ( lockPath != null ){
LockData newLockData = new LockData(currentThread, lockPath);
threadData.put(currentThread, newLockData);
return true;
}
return false;
}
```
上面真正去尝试获取锁的方法是 `attemptLock()`
```java
String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Exception{
final long startMillis = System.currentTimeMillis();
final Long millisToWait = (unit != null) ? unit.toMillis(time) : null;
final byte[] localLockNodeBytes = (revocable.get() != null) ? new byte[0] : lockNodeBytes;
int retryCount = 0; // 重试次数
String ourPath = null;
boolean hasTheLock = false;
boolean isDone = false;
// 当出现NoNodeException异常时候依靠该循环进行重试
while ( !isDone ){
isDone = true;
try{
// 【核心方法:根据锁路径来创建对应的节点】
ourPath = driver.createsTheLock(client, path, localLockNodeBytes);
// 【核心方法:获取锁】
hasTheLock = internalLockLoop(startMillis, millisToWait, ourPath);
}
catch ( KeeperException.NoNodeException e ){
// 如果出现异常并且还没有到达给ZooKeeper配置的最大重试时间或最大重试次数则循环继续并再次尝试获取锁
if ( client.getZookeeperClient().getRetryPolicy()
.allowRetry(retryCount++,System.currentTimeMillis() - startMillis,
RetryLoop.getDefaultRetrySleeper()) ){
isDone = false;
}else{
throw e;
}
}
}
// 如果获取到锁,则跳出循环,并返回锁的路径
if ( hasTheLock ){
return ourPath;
}
return null;
}
```
这里两个核心的方法是 `createsTheLock()``internalLockLoop()` 。createsTheLock 的实现比较简单,就是根据我们指定的路径来创建临时节点有序节点:
```java
@Override
public String createsTheLock(CuratorFramework client, String path, byte[] lockNodeBytes) throws Exception{
String ourPath;
// 如果lockNodeBytes不为空则创建一个含数据的临时有序节点
if ( lockNodeBytes != null ){
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().
withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path, lockNodeBytes);
}else{
//否则则创建一个空的临时有序节点
ourPath = client.create().creatingParentContainersIfNeeded().withProtection().
withMode(CreateMode.EPHEMERAL_SEQUENTIAL).forPath(path);
}
// 返回创建好的节点路径
return ourPath;
}
```
这里返回的临时有序节点的路径会作为参数传递给 `internalLockLoop()` 方法。在文章开头介绍原理时,我们说过每个线程创建好临时有序节点后,还需要判断它所创建的临时有序节点是否是当前最小的节点,`internalLockLoop()` 方法主要做的就是这事:
```java
private boolean internalLockLoop ( long startMillis, Long millisToWait, String ourPath) throws Exception {
// 是否持有锁
boolean haveTheLock = false;
boolean doDelete = false;
try {
if (revocable.get() != null) {
client.getData().usingWatcher(revocableWatcher).forPath(ourPath);
}
// 如果连接ZooKeeper客户端处于启动状态也就是想要获取锁的进程仍然处于运行状态并且还没有获取到锁则循环继续
while ((client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock) {
// 对所当前所有的子节点按照从小到大进行排序
List<String> children = getSortedChildren();
// 将createsTheLock方法获得的临时有序节点的路径进行截取只保留节点名的部分
String sequenceNodeName = ourPath.substring(basePath.length() + 1);
// 判断当前节点是否是最小的一个节点
PredicateResults predicateResults = driver.
getsTheLock(client, children, sequenceNodeName, maxLeases);
// 如果当前节点是最小的一个节点(排他锁情况),则此时就获得了锁
if (predicateResults.getsTheLock()) {
haveTheLock = true;
} else {
// 如果当前节点不是最小的一个节点,先拼接并获取前一个节点完整的路径
String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch();
synchronized (this) {
try {
// 然后对前一个节点进行监听
client.getData().usingWatcher(watcher).forPath(previousSequencePath);
// 如果设置了等待时间
if (millisToWait != null) {
// 将等待时间减去到目前为止所耗费的时间
millisToWait -= (System.currentTimeMillis() - startMillis);
startMillis = System.currentTimeMillis();
// 如果等待时间小于0则说明我们耗费的时间已经超过了等待时间此时获取的锁无效需要删除它
if (millisToWait <= 0) {
//设置删除标志位,并退出循环
doDelete = true;
break;
}
// 如果还有剩余时间,则在剩余时间内继续等待获取锁
wait(millisToWait);
} else {
// 如果没有设置等待时间,则持续等待获取锁
wait();
}
} catch (KeeperException.NoNodeException e) {
// 这个异常抛出时,代表对前一个节点设置监听时,前一个节点已经不存在(被释放),此时捕获该异常,
// 但不需要进行任何额外操作,因为循环会继续,就可以再次尝试获取锁
}
}
}
}
} catch (Exception e) {
ThreadUtils.checkInterrupted(e);
doDelete = true;
throw e;
} finally {
// 如果抛出了异常或者超时,则代表该进程创建的锁无效,需要将已创建的锁删除。以便后面的进程继续尝试创建锁
if (doDelete) {
deleteOurPath(ourPath);
}
}
return haveTheLock;
}
```
这里对上面判断当前节点是否是持有锁的节点的 getsTheLock 方法进行一下说明:
```java
PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases);
```
和上文介绍的一样,判断当前节点是否是持有锁的节点,在不同锁类型(如读写锁和互斥锁)的判断是不同的,因此 getsTheLock 方法有着不同的实现。这里以StandardLockInternalsDriver 为例,它使用的是互斥锁的判断规则:即只要当前节点是最小的一个节点,就能持有锁:
```java
public PredicateResults getsTheLock(CuratorFramework client, List<String> children,
String sequenceNodeName, int maxLeases) throws Exception {
// 获取当前节点在已经排好序的节点中的下标index
int ourIndex = children.indexOf(sequenceNodeName);
// 如果ourIndex小于0则抛出NoNodeException的异常
validateOurIndex(sequenceNodeName, ourIndex);
// 如果ourIndex小于maxLeases(默认值是1)则代表它就是0也就是从小到大排好序的集合中的第一个也就是最小的一个
boolean getsTheLock = ourIndex < maxLeases;
// 如果是最小的一个,此时就已经获取到锁,不需要返回前一个节点的名称,否则需要返回前一个节点的名称,用于后续的监听操作
String pathToWatch = getsTheLock ? null : children.get(ourIndex - maxLeases);
return new PredicateResults(pathToWatch, getsTheLock);
}
```
这里解释一下 maxLease 这个参数的意义:默认值为 1就是互斥锁如果默认值大于 1假设 maxLease 的值是 5则最小的 5 个临时有序节点都可以认为是能持有锁的节点,此时最多可以有 5 个线程并发访问临界区, 在功能上类似于 Java 中Semaphore信号量机制 。
#### 2. 释放锁源码解析
以上就是所有获取锁的源码解析,而释放锁的过程就比较简单了。`release()` 方法的源码如下:
```java
public void release() throws Exception {
Thread currentThread = Thread.currentThread();
// 根据当前线程来获取锁信息
InterProcessMutex.LockData lockData = threadData.get(currentThread);
// 如果获取不到,则当前线程不是锁的持有者,此时抛出异常
if (lockData == null) {
throw new IllegalMonitorStateException("You do not own the lock: " + basePath);
}
// 因为Zookeeper实现的锁具有重入性所以将其计数器减少1
int newLockCount = lockData.lockCount.decrementAndGet();
if (newLockCount > 0) {
return;
}
// 如果计数器的值小于0代表解锁次数大于加锁次数此时抛出异常
if (newLockCount < 0) {
throw new IllegalMonitorStateException("Lock count has gone negative for lock: " + basePath);
}
try {
// 如果到达这一步则说明计数器的值正好等于0此时可以将节点真正的删除释放锁
internals.releaseLock(lockData.lockPath);
} finally {
// 将锁信息从threadData移除
threadData.remove(currentThread);
}
}
```
真正删除锁节点的方法存在于 `releaseLock()` 中,其源码如下:
```java
final void releaseLock(String lockPath) throws Exception{
client.removeWatchers();
revocable.set(null);
deleteOurPath(lockPath); //删除ZooKeeper上对应的节点
}
```
## 参考资料
+ 倪超 . 从 Paxos 到 Zookeeper——分布式一致性原理与实践 . 电子工业出版社 . 2015-02-01
+ https://curator.apache.org/curator-recipes/index.html

BIN
pictures/HTTPS.pptx Normal file

Binary file not shown.

BIN
pictures/buffer_clear.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

BIN
pictures/buffer_compact.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

BIN
pictures/buffer_flip.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

BIN
pictures/buffer_get.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
pictures/buffer_init.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

BIN
pictures/buffer_put.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

BIN
pictures/buffer_slice.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.9 KiB

BIN
pictures/cap_示例.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.5 KiB

BIN
pictures/cap理论.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
pictures/http+tls.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.4 KiB

BIN
pictures/http_baidu_ov.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 385 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
pictures/java_nio.pptx Normal file

Binary file not shown.

BIN
pictures/jconsole-start.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
pictures/jconsole-类.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
pictures/jhat.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
pictures/jhat_web.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

BIN
pictures/jinfo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

BIN
pictures/jmap.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

BIN
pictures/jprofiler-1.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
pictures/jprofiler-2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
pictures/jstack.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

BIN
pictures/jstat_gc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
pictures/jvisual-dump.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
pictures/jvisual-gc.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

BIN
pictures/jvisual-监视.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

BIN
pictures/jvisual-线程.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

BIN
pictures/jvisual.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
pictures/nio_buffer.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

BIN
pictures/nio_chat_group.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

BIN
pictures/nio_gather.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

BIN
pictures/nio_pipe.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
pictures/nio_scatter.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

BIN
pictures/nio_selector.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.5 KiB

BIN
pictures/tomcat_request.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

BIN
pictures/tomcat_架构.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Some files were not shown because too many files have changed in this diff Show More