CAP理论和BASE理论

This commit is contained in:
罗祥 2020-05-06 17:43:59 +08:00
parent 29a6cc946b
commit 6ba5a17420
14 changed files with 503 additions and 654 deletions

View File

@ -2,43 +2,64 @@
<div align="center"> <img width="380px" src="pictures/full-stack-notes-logo.png"/> </div> <div align="center"> <img width="380px" src="pictures/full-stack-notes-logo.png"/> </div>
<br/> <br/>
<div align="center"> <div align="center">
<img src="pictures/芽.png"/> <img src="pictures/芽.png"/>
<strong>一个处于萌芽阶段的知识库,用于持续分享自己的所见、所学、所思!</strong> <strong>一个处于萌芽阶段的知识库,用于持续分享自己的所见、所学、所思!</strong>
</div> </div>
<br/>
<div align="right">
<a href="">点击切换详细目录</a>
</div>
## :coffee: JAVA ## :coffee: JAVA
1. [Java 反射与注解](notes/Java_反射与注解.md) 1. [Java 反射与注解](notes/Java_反射与注解.md)
反射机制、自定义注解、@Target@Retention、注解的继承
2. [Java 并发编程](notes/Java_并发编程.md) 2. [Java 并发编程](notes/Java_并发编程.md)
3. [Java 设计模式](notes/Java_设计模式.md)
4. [Java 虚拟机](notes/Java_虚拟机.md) 非原子性协定、计算机多级高速缓存、缓存一致性协议、写缓冲与无效化队列、内存屏障、锁机制、无锁 CAS、线程池
5. [JVM 性能监控之命令行工具](notes/JVM_性能监控之命令行工具.md)
6. [JVM 性能监控之可视化工具](notes/JVM_性能监控之可视化工具.md) 3. [Java NIO 核心组件详解](notes/Java_NIO.md)
7. [Java NIO 核心组件详解](notes/Java_NIO.md)
8. 函数式编程 缓冲区 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) 9. [Tomcat 架构解析](notes/Tomcat_架构解析.md)
核心组件、连接器、多层容器、请求处理流程、程序启动过程、类加载器
10. Java 集合类源码解析 10. Java 集合类源码解析
<br/> <br/>
## :globe_with_meridians: 计算机与网络基础 ## :globe_with_meridians: 网络基础
1. [计算机网络模型](notes/计算机网络.md) 1. [计算机网络模型](notes/计算机网络.md)
2. HTTP 协议详解 四层、五层、七层计算机网络模型、信道复用、PPP 协议、ARP 协议、划分子网与构成超网、TCP 三次握手与四次挥手
3. HTTPS 协议详解 3. HTTPS 与 通信安全
4. 抓包神器 Wireshark 4. 抓包神器 Wireshark
5. 计算机组成原理
<br/> <br/>
@ -46,10 +67,16 @@
1. [JavaScript 基础](notes/JavaScript_基础.md) 1. [JavaScript 基础](notes/JavaScript_基础.md)
基本数据类型、引用类型、内置对象Global 与 window、作用域与闭包、对象设计
2. [ECMAScript 6.0 基础](notes/ES6_基础.md) 2. [ECMAScript 6.0 基础](notes/ES6_基础.md)
变量声明、对象字面量、对象解构、Symbol、迭代器与生成器、类、代理与反射、模块化
3. CSS 基础 3. CSS 基础
选择器、非局部样式、布局样式、效果属性、CSS 动画
4. JavaScript 设计模式 4. JavaScript 设计模式
<br/> <br/>
@ -60,10 +87,16 @@
1. [MySQL 核心概念](notes/MySQL_基础.md) 1. [MySQL 核心概念](notes/MySQL_基础.md)
B+ Tree 树、聚集索引和非聚集索引、共享锁与排他锁、意向共享锁与意向排它锁、一致性锁定读与一致性非锁定读
2. [MySQL 备份详解](notes/MySQL_备份.md) 2. [MySQL 备份详解](notes/MySQL_备份.md)
备份类型、mysqldump 备份、mysqlpump 备份、Xtrabackup 备份、二进制日志备份
3. [MySQL 复制详解](notes/MySQL_复制.md) 3. [MySQL 复制详解](notes/MySQL_复制.md)
基于二进制日志的复制、基于 GTID 的复制、半同步复制、高可用架构 MMM 和 MHA
4. [MySQL 高可用架构之 PXC 集群](notes/MySQL_PXC集群.md) 4. [MySQL 高可用架构之 PXC 集群](notes/MySQL_PXC集群.md)
5. [MyCat 读写分离与分库分表](notes/MySQL_Mycat中间件.md) 5. [MyCat 读写分离与分库分表](notes/MySQL_Mycat中间件.md)
@ -78,9 +111,15 @@
3. [Redis 哨兵模式](notes/Redis_哨兵模式.md) 3. [Redis 哨兵模式](notes/Redis_哨兵模式.md)
复制机制、哨兵模式架构说明、哨兵模式搭建
4. [Redis 集群模式](notes/Redis_集群模式.md) 4. [Redis 集群模式](notes/Redis_集群模式.md)
5. 使用 Redis 实现分布式锁 数据分区、节点通信、请求路由、故障发现与恢复、集群扩容与缩容
5. [Redis 分布式锁原理](notes/Redis_分布式锁原理.md)
分布式锁原理、单机模式下的分布式锁、集群模式下的分布式锁、RedLock 原理、Redisson
### MongoDB ### MongoDB
@ -89,12 +128,20 @@
2. [MongoDB 索引](notes/MongoDB_索引.md) 2. [MongoDB 索引](notes/MongoDB_索引.md)
单字段索引、复合索引、多键索引、哈希所有、地理空间索引、文本索引唯一索引、稀疏索引、部分索引、TTL 索引
3. [MongoDB 聚合](notes/MongoDB_聚合.md) 3. [MongoDB 聚合](notes/MongoDB_聚合.md)
常用聚合管道、单用途聚合方法、MapReduce
4. [MongoDB 复制](notes/MongoDB_复制.md) 4. [MongoDB 复制](notes/MongoDB_复制.md)
复制功能、故障发现、优先选举、投票成员、副本集搭建
5. [MongoDB 分片](notes/MongoDB_分片.md) 5. [MongoDB 分片](notes/MongoDB_分片.md)
分片副本集配置、配置副本集配置、路由服务配置
<br/> <br/>
## :whale: 系统与容器 ## :whale: 系统与容器
@ -103,8 +150,12 @@
2. [Sehll 脚本编程基础](notes/Shell_基础.md) 2. [Sehll 脚本编程基础](notes/Shell_基础.md)
创建脚本、分支语句、循环语句、处理用户输入、处理用户输出、创建函数、处理信号、定时作业
3. [Docker 基础](notes/Docker_基础.md) 3. [Docker 基础](notes/Docker_基础.md)
核心概念镜像、容器、仓库、Docker 常用命令、DockerFile 常用指令
<br/> <br/>
## :package: 常用技术栈 ## :package: 常用技术栈
@ -122,7 +173,6 @@
1. [Nginx 基础之静态网站部署,负载均衡,动静分离](notes/Nginx_基础.md) 1. [Nginx 基础之静态网站部署,负载均衡,动静分离](notes/Nginx_基础.md)
2. HTTP 模块详解 2. HTTP 模块详解
3. Nginx 性能优化
### Kafka ### Kafka
@ -141,29 +191,23 @@
3. [ZooKeeper 常用 Shell 命令](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper常用Shell命令.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) 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) 5. [ZooKeeper ACL 权限控制](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper_ACL权限控制.md)
6. 使用 ZooKeeper 实现分布式锁 6. [ZooKeeper 分布式锁原理](notes/ZooKeeper_分布式锁原理.md)
## ElasticSearch ## ElasticSearch
TODO TODO
<br/>
## :rocket: 测试与运维
1. 性能测试之 Jmeter
2. 性能测试之 LoadRunner
3. Jenkins 持续交付与自动化部署
<br/> <br/>
## :bullettrain_side: 微服务与分布式 ## :bullettrain_side: 微服务与分布式
1. 分布式锁的实现 1. [CAP 理论 和 BASE 理论](notes/CAP理论和BASE理论.md)
2. 分布式选举算法
3. 分布式事务实现原理
4. 分布式全局 ID 的生成
5. CAP 理论和 BASE 理论
<br/> <br/>

View File

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

View File

@ -1,54 +0,0 @@
<?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>stream-tutorial</artifactId>
<version>1.0-SNAPSHOT</version>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>8</source>
<target>8</target>
</configuration>
</plugin>
</plugins>
</build>
<dependencies>
<!-- https://mvnrepository.com/artifact/org.redisson/redisson -->
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.12.5</version>
</dependency>
<!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>3.2.0</version>
</dependency>
<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>
</dependencies>
</project>

View File

@ -1,41 +0,0 @@
package com.heibaiying;
import org.apache.curator.RetryPolicy;
import org.apache.curator.framework.CuratorFramework;
import org.apache.curator.framework.CuratorFrameworkFactory;
import org.apache.curator.framework.recipes.locks.InterProcessMutex;
import org.apache.curator.retry.RetryNTimes;
import java.util.concurrent.TimeUnit;
public class DistributedLock {
public static void main(String[] args) throws Exception {
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();
}
}

View File

@ -1,70 +0,0 @@
package com.heibaiying;
public class Employee {
private String name;
private String gender;
private String company;
private int age;
private boolean isOfficial;
public Employee(String name, String gender, String company, int age) {
this.name = name;
this.gender = gender;
this.company = company;
this.age = age;
}
Employee(String name, int age,boolean isOfficial) {
this.name = name;
this.age = age;
this.isOfficial = isOfficial;
}
@Override
public String toString() {
return "Employee{" +
"name='" + name + '\'' +
'}';
}
public boolean isOfficial() {
return isOfficial;
}
public void setOfficial(boolean official) {
isOfficial = official;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getCompany() {
return company;
}
public void setCompany(String company) {
this.company = company;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}

View File

@ -1,62 +0,0 @@
package com.heibaiying;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
import java.util.function.BinaryOperator;
import java.util.function.IntConsumer;
import java.util.stream.Collectors;
import java.util.stream.Stream;
public class StreamTest {
public static void main(String[] args) {
System.out.println(UUID.randomUUID() + ":" + Thread.currentThread().getId());
}
/**
* 进行求和
*
* @param list
* @param initValue
* @param binaryOperator
* @param <T>
* @return
*/
public static <T> T reduce(List<T> list, T initValue, BinaryOperator<T> binaryOperator) {
for (T t : list) {
initValue = binaryOperator.apply(initValue, t);
}
return initValue;
}
/**
* 集合过滤
*
* @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;
}
/**
* 定义接口
*
* @param <T> 参数类型
*/
@FunctionalInterface
public interface CustomPredicate<T> {
// 判断是否满足过滤标准
boolean test(T t);
}
}

View File

@ -1,7 +0,0 @@
log4j.rootLogger=INFO, SYSLOG
log4j.appender.SYSLOG=org.apache.log4j.net.SyslogAppender
log4j.appender.SYSLOG.syslogHost=127.0.0.1
log4j.appender.SYSLOG.layout=org.apache.log4j.PatternLayout
log4j.appender.SYSLOG.layout.conversionPattern=%d{ISO8601} %-5p [%t] %c{2} %x - %m%n
log4j.appender.SYSLOG.Facility=LOCAL1

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

View File

@ -1,40 +0,0 @@
# HTTPS
## 一、核心概念
### 1.1 SSL
安全套接层英语Secure Sockets Layer缩写SSL是一种安全协议目的是为互联网通信提供安全保障最早由网景公司Netscape推出。SSL 协议有三个版本,分别是 SSL v1、SSL v2、SSL v3
- v1 版本从未公开过,因为存在严重的安全漏洞。
- v2 版本在1995年2月发布但因为存在多个严重的安全漏洞而被 v3 版本替代。
- v3 版本在1996年发布是由网景公司完全重新设计的。
### 1.2 TLS
1966 年TETFInternet Engineering Task Force组织在 SLL 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 服务。
## 二、数据安全
HTTPS 的数据安全主要是通过 SSL/TLS 协议来进行实现的SSL/TLS 则主要采用了以下方式来保证传输的安全:
### 2.1 非对称加密
### 2.2 对称加密
## 三、握手过程

View File

@ -1,10 +1,20 @@
# Java 函数式编程 # 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 ## 一、Lambda
### 1.1 格式 ### 1.1 格式
Java 从 1.8 版本开始支持 Lambda 表达式,通过 Lambda 表达式我们可以将一个函数作为参数传入方法中。在 JDK 1.8 之前,我们只能通过匿名表达式来完成类似的功能,但是匿名表达式比较繁琐,存在大量的冗余代码,不利于将行为参数化,而采用 Lamdba 则能很好的解决这个问题。Lambda 表达式的基本语法如下: JDK 从 1.8 版本开始支持 Lambda 表达式,通过 Lambda 表达式我们可以将一个函数作为参数传入方法中。在 JDK 1.8 之前,我们只能通过匿名表达式来完成类似的功能,但是匿名表达式比较繁琐,存在大量的模板代码,不利于将行为参数化,而采用 Lamdba 则能很好的解决这个问题。Lambda 表达式的基本语法如下:
```java ```java
(parameters) -> expression (parameters) -> expression
@ -18,8 +28,8 @@ Java 从 1.8 版本开始支持 Lambda 表达式,通过 Lambda 表达式我们
Lambda 表达式具有如下特点: Lambda 表达式具有如下特点:
- **可选的参数:**不需要声明参数类型,编译器会从上下文自动进行推断; - **可选的参数:**不需要声明参数类型,编译器会依靠上下文进行自动推断;
- **可选的参数圆括号:**当且仅当只有一个参数时,圆括号可以省略; - **可选的参数圆括号:**当且仅当只有一个参数时,包裹参数的圆括号可以省略;
- **可选的花括号:**如果主体只有一个表达式,则无需使用花括号; - **可选的花括号:**如果主体只有一个表达式,则无需使用花括号;
- **可选的返回关键字:**如果主体只有一个表达式,则该表达式的值就是整个 Lambda 表达式的返回值,此时不需要使用 return 关键字进行显式的返回。 - **可选的返回关键字:**如果主体只有一个表达式,则该表达式的值就是整个 Lambda 表达式的返回值,此时不需要使用 return 关键字进行显式的返回。
@ -34,7 +44,7 @@ Lambda 表达式具有如下特点:
*/ */
@FunctionalInterface @FunctionalInterface
public interface CustomPredicate<T> { public interface CustomPredicate<T> {
boolean test(T t); boolean test(T t);
} }
``` ```
@ -47,12 +57,12 @@ public interface CustomPredicate<T> {
* @return 满足条件的元素的集合 * @return 满足条件的元素的集合
*/ */
public static <T> List<T> filter(List<T> list, CustomPredicate<T> predicate) { public static <T> List<T> filter(List<T> list, CustomPredicate<T> predicate) {
ArrayList<T> result = new ArrayList<>(); ArrayList<T> result = new ArrayList<>();
for (T t : list) { for (T t : list) {
// 将满足条件的元素添加到返回集合中 // 将满足条件的元素添加到返回集合中
if (predicate.test(t)) result.add(t); if (predicate.test(t)) result.add(t);
} }
return result; return result;
} }
``` ```
@ -64,9 +74,9 @@ List<Integer> integers = Arrays.asList(1, 2, 3, 4, 5);
filter(integers, x -> x % 2 == 0); // 过滤出所有偶数 filter(integers, x -> x % 2 == 0); // 过滤出所有偶数
List<Employee> employees = Arrays.asList( List<Employee> employees = Arrays.asList(
new Employee("张某", 21, true), new Employee("张某", 21, true),
new Employee("李某", 30, true), new Employee("李某", 30, true),
new Employee("王某", 45, false)); new Employee("王某", 45, false));
filter(employees, employee -> employee.getAge() > 25); // 过滤出所有年龄大于25的员工 filter(employees, employee -> employee.getAge() > 25); // 过滤出所有年龄大于25的员工
``` ```
@ -76,7 +86,7 @@ filter(employees, employee -> employee.getAge() > 25); // 过滤出所有年龄
```java ```java
new Thread(() -> { new Thread(() -> {
System.out.println("hello"); System.out.println("hello");
}); });
``` ```
@ -148,10 +158,10 @@ public interface Predicate<T> {
} }
``` ```
其他函数式接口都是这四种基本类型的延伸和扩展。以 BiFunction 和 BinaryOperator 接口为例: 其他函数式接口都是这四种基本类型的扩展和延伸。以 BiFunction 和 BinaryOperator 接口为例:
+ **BiFunction<T, U, R>**:是函数型接口 Function<T, R> 的扩展Function 只能接收一个入参;而 BiFunction 可以用于接收两个不同类型的入参; + **BiFunction<T, U, R>**:是函数型接口 Function<T, R> 的扩展Function 只能接收一个入参;而 BiFunction 可以用于接收两个不同类型的入参;
+ **BinaryOperator\<T>**:是 BiFunction 的一种特殊化情况,即两个入参和返回值的类型均相同,通常用于二元运算: + **BinaryOperator\<T>**:是 BiFunction 的一种特殊化情况,即两个入参和返回值的类型均相同,通常用于二元运算。定义如下
```java ```java
@FunctionalInterface @FunctionalInterface
@ -165,24 +175,23 @@ public interface BinaryOperator<T> extends BiFunction<T,T,T> {
} }
``` ```
使用示例如下 下面演示一下 BinaryOperator 的用法
```java ```java
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
}
/** /**
* 执行归约操作 * 执行归约操作
*/ */
public static <T> T reduce(List<T> list, T initValue, BinaryOperator<T> binaryOperator) { public static <T> T reduce(List<T> list, T initValue, BinaryOperator<T> binaryOperator) {
for (T t : list) { for (T t : list) {
initValue = binaryOperator.apply(initValue, t); initValue = binaryOperator.apply(initValue, t);
} }
return initValue; 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
} }
``` ```
@ -190,7 +199,7 @@ public static <T> T reduce(List<T> list, T initValue, BinaryOperator<T> binaryOp
## 三、创建流 ## 三、创建流
JDK 1.8 中最主要的变化是引入了流通过流、Lamda 表达式以及函数式接口,可以高效地完成数据的处理。创建流通常有以下四种方法: JDK 1.8 中另一个大的改进是引入了流通过流、Lamda 表达式以及函数式接口,可以高效地完成数据的处理。创建流通常有以下四种方法:
**1. 由值创建** **1. 由值创建**
@ -216,7 +225,7 @@ List<String> strings = Arrays.asList("a", "b ", "c", "d");
Stream<String> stream = strings.stream(); Stream<String> stream = strings.stream();
``` ```
`stream()` 方法定义在 `Collection` 接口中,它是一个默认方法,因此大多数的集合都可以通过该方法转换为流: `stream()` 方法定义在 `Collection` 接口中,它是一个默认方法,因此大多数的集合都可以通过该方法来创建流:
```java ```java
public interface Collection<E> extends Iterable<E> { public interface Collection<E> extends Iterable<E> {
@ -230,9 +239,9 @@ public interface Collection<E> extends Iterable<E> {
```java ```java
try (Stream<String> lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.UTF_8)) { try (Stream<String> lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.UTF_8)) {
lines.forEach(System.out::println); lines.forEach(System.out::println);
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace(); e.printStackTrace();
} }
``` ```
@ -240,10 +249,10 @@ try (Stream<String> lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.U
除了以上方法外,还可以通过 `Stream.iterate()``Stream.generate()` 方法来来创建无限流: 除了以上方法外,还可以通过 `Stream.iterate()``Stream.generate()` 方法来来创建无限流:
+ `Stream.iterate()` 接受两个参数:第一个是初始值,第二个参数是一个输入值和输出值相同的函数型接口。它主要用于迭代式的产生新的元素,示例如下: + `Stream.iterate()` 接受两个参数:第一个是初始值;第二个参数是一个输入值和输出值相同的函数型接口,主要用于迭代式地产生新的元素,示例如下:
```java ```java
// 依次输出1到9 // 依次输出0到9
Stream.iterate(0, x -> x + 1).limit(10).forEach(System.out::print); Stream.iterate(0, x -> x + 1).limit(10).forEach(System.out::print);
``` ```
@ -258,26 +267,26 @@ try (Stream<String> lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.U
### 4.1 基本操作 ### 4.1 基本操作
当流创建后,便可以利用 Stream 类的各种方法对其上数据进行各种处理,常用的方法如下: 当流创建后,便可以利用 Stream 类上的各种方法对流中的数据进行处理,常用的方法如下:
| 操作 | 作用 | 返回类型 | 使用的类型/函数式接口 | | 操作 | 作用 | 返回类型 | 使用的类型/函数式接口 |
| --------- | ------------------------------ | ------------ | ---------------------- | | --------- | ---------------------------------- | ------------ | ---------------------- |
| filter | 过滤符合条件的元素 | Stream\<T> | Predicate\<T> | | filter | 过滤符合条件的元素 | Stream\<T> | Predicate\<T> |
| distinct | 过滤重复元素 | Stream\<T> | | | distinct | 过滤重复元素 | Stream\<T> | |
| skip | 跳过指定数量的元素 | Stream\<T> | long | | skip | 跳过指定数量的元素 | Stream\<T> | long |
| limit | 限制元素的数量 | Stream\<T> | long | | limit | 限制元素的数量 | Stream\<T> | long |
| map | 对元素执行特定转换操作 | Stream\<T> | Function<T,R> | | map | 对元素执行特定转换操作 | Stream\<T> | Function<T,R> |
| flatMap | 将元素扁平化后执行特定转换操作 | Stream\<T> | Function<T,Stream\<R>> | | flatMap | 将元素扁平化后执行特定转换操作 | Stream\<T> | Function<T,Stream\<R>> |
| sorted | 对元素进行排序 | Stream\<T> | Comparator\<T> | | sorted | 对元素进行排序 | Stream\<T> | Comparator\<T> |
| anyMatch | 是否存在指定元素满足特定条件 | boolean | Predicate\<T> | | anyMatch | 是否存在任意一个元素能满足指定条件 | boolean | Predicate\<T> |
| noneMatch | 是否所有元素都不满足特定条件 | boolean | Predicate\<T> | | noneMatch | 是否所有元素都不满足指定条件 | boolean | Predicate\<T> |
| allMatch | 是否所有元素都满足特定条件 | boolean | Predicate\<T> | | allMatch | 是否所有元素都满足指定条件 | boolean | Predicate\<T> |
| findAny | 返回任意一个满足指定条件的元素 | Optional\<T> | | | findAny | 返回任意一个满足指定条件的元素 | Optional\<T> | |
| findFirst | 返回第一个满足指定条件的元素 | Optional\<T> | | | findFirst | 返回第一个满足指定条件的元素 | Optional\<T> | |
| forEach | 对所有元素执行特定的操作 | void | Cosumer\<T> | | forEach | 对所有元素执行特定的操作 | void | Cosumer\<T> |
| collect | 对所有元素指定特定的收集操作 | R | Collector<T, A, R> | | collect | 使用收集器 | R | Collector<T, A, R> |
| reduce | 对元素依次执行归约操作 | Optional\<T> | BinaryOperator\<T> | | reduce | 执行归约操作 | Optional\<T> | BinaryOperator\<T> |
| count | 计算流中元素的数量 | long | | | count | 计算流中元素的数量 | long | |
> 注:上表中返回类型为 Stream\<T> 的操作都是中间操作,代表还可以继续调用其它方法对流进行处理。返回类型为其它的操作都是终止操作,代表处理过程到此为止。 > 注:上表中返回类型为 Stream\<T> 的操作都是中间操作,代表还可以继续调用其它方法对流进行处理。返回类型为其它的操作都是终止操作,代表处理过程到此为止。
@ -285,11 +294,11 @@ try (Stream<String> lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.U
```java ```java
Stream.iterate(0, x -> x + 1) // 构建流 Stream.iterate(0, x -> x + 1) // 构建流
.limit(20) // 限制元素的个数 .limit(20) // 限制元素的个数
.skip(10) // 跳过前10个元素 .skip(10) // 跳过前10个元素
.filter(x -> x % 2 == 0) // 过滤出所有偶数 .filter(x -> x % 2 == 0) // 过滤出所有偶数
.map(x -> "偶数:" + x) // 对元素执行转换操作 .map(x -> "偶数:" + x) // 对元素执行转换操作
.forEach(System.out::println); // 打印出所有元素 .forEach(System.out::println); // 打印出所有元素
``` ```
输出结果如下: 输出结果如下:
@ -302,18 +311,18 @@ Stream.iterate(0, x -> x + 1) // 构建流
偶数:18 偶数:18
``` ```
上表的 `flatMap()` 方法接收一个参数,是一个函数型接口 `Function<? super T, ? extends Stream<? extends R>> mapper`该接口用于将流中的元素转换为 `Stream` ,从而可以将原有的元素进行扁平化: 上表的 `flatMap()` 方法接收一个参数,该参数是一个函数型接口 `Function<? super T, ? extends Stream<? extends R>> mapper`主要用于将流中的元素转换为 `Stream` ,从而可以将原有的元素进行扁平化,示例如下
```java ```java
String[] strings = {"hello", "world"}; String[] strings = {"hello", "world"};
Arrays.stream(strings) Arrays.stream(strings)
.map(x -> x.split("")) // 拆分得到: ['h','e','l','l','o'],['w','o','r','l','d'] .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' .flatMap(x -> Arrays.stream(x)) // 将每个数组进行扁平化处理得到:'h','e','l','l','o','w','o','r','l','d'
.forEach(System.out::println); .forEach(System.out::println);
``` ```
上表的 `reduce()` 方法接收两个参数:第一个参数表示执行归约操作的初始值;第二个参数是上文我们介绍过的函数式接口 `BinaryOperator<T>` ,使用示例如下: 上表的 `reduce()` 方法接收两个参数:第一个参数表示执行归约操作的初始值;第二个参数是上文我们介绍过的函数式接口 `BinaryOperator<T>` ,使用示例如下:
```java ```java
Stream.iterate(0, x -> x + 1).limit(10) Stream.iterate(0, x -> x + 1).limit(10)
@ -322,7 +331,7 @@ Stream.iterate(0, x -> x + 1).limit(10)
### 4.2 数值流 ### 4.2 数值流
上面的代码等效于对 Stream 中的所有元素执行了求和操作,因此我们还可以调用简便方法 `sum()` 来进行实现,但是需要注意的是上面 `Stream.iterate()` 生成流中的元素类型都是包装类型: 上面的代码等效于对 Stream 中的所有元素执行了求和操作,因此我们还可以调用简便方法 `sum()` 来进行实现,但是需要注意的是 `Stream.iterate()` 生成流中的元素类型都是包装类型:
```java ```java
Stream<Integer> stream = Stream.iterate(0, x -> x + 1); //包装类型Integer Stream<Integer> stream = Stream.iterate(0, x -> x + 1); //包装类型Integer
@ -345,7 +354,7 @@ Stream<Integer> boxed = intStream.boxed();
## 五、收集器 ## 五、收集器
Stream 中最强大一个终止操作是 `collect()` ,它接收一个收集器 Collector 作为参数可以将流中的元素收集到集合中或进行分组、分区等操作。Java 中内置了多种收集器的实现,可以通过 Collectors 类的静态方法进行调用,常用的如下: Stream 中最强大一个终止操作是 `collect()` ,它接收一个收集器 Collector 作为参数可以将流中的元素收集到集合中或进行分组、分区等操作。Java 中内置了多种收集器的实现,可以通过 Collectors 类的静态方法进行调用,常用的收集器如下:
| 工厂方法 | 返回类型 | 用于 | | 工厂方法 | 返回类型 | 用于 |
| ----------------- | --------------------- | ------------------------------------------------------------ | | ----------------- | --------------------- | ------------------------------------------------------------ |
@ -355,12 +364,12 @@ Stream 中最强大一个终止操作是 `collect()` ,它接收一个收集器
| counting | Long | 计算流中所有元素的个数 | | counting | Long | 计算流中所有元素的个数 |
| summingInt | Integer | 将流中所有元素转换为整数,并计算其总和 | | summingInt | Integer | 将流中所有元素转换为整数,并计算其总和 |
| averagingInt | Double | 将流中所有元素转换为整数,并计算其平均值 | | averagingInt | Double | 将流中所有元素转换为整数,并计算其平均值 |
| summarizingInt | IntSummaryStatistics | 将流中所有元素转换为整数,并返回值统计值,包含最大值、最小值、<br/>总和与平均值等信息 | | summarizingInt | IntSummaryStatistics | 将流中所有元素转换为整数,并返回统计结果,包含最大值、最小值、<br/>总和与平均值等信息 |
| joining | String | 将流中所有元素转换为字符串,并使用给定连接符进行连接 | | joining | String | 将流中所有元素转换为字符串,并使用给定连接符进行连接 |
| maxBy | Optional\<T> | 查找流中最大元素的 Optional | | maxBy | Optional\<T> | 查找流中最大元素的 Optional |
| minBy | Optional\<T> | 查找流中最小元素的 Optional | | minBy | Optional\<T> | 查找流中最小元素的 Optional |
| reducing | 规约操作产生的类型 | 对流中所有元素执行归约操作 | | reducing | 规约操作产生的类型 | 对流中所有元素执行归约操作 |
| collectingAndThen | 转换返回的类型 | 把流中所有元素收集到指定的集合中,再对集合执行特定转换操作 | | collectingAndThen | 转换返回的类型 | 把流中所有元素收集到指定的集合中,再对集合执行特定操作 |
| groupingBy | Map<K,List\<T>> | 对流中所有元素执行分组操作 | | groupingBy | Map<K,List\<T>> | 对流中所有元素执行分组操作 |
| partitionBy | Map<Boolean,List\<T>> | 对流中所有元素执行分区操作 | | partitionBy | Map<Boolean,List\<T>> | 对流中所有元素执行分区操作 |
@ -379,7 +388,7 @@ stream.collect(Collectors.reducing(1, (a, b) -> a * b)); // 等效于 stream.red
collect(Collectors.collectingAndThen(Collectors.toSet(), Set::size)); // 先把所有元素收集到Set中再计算Set的大小 collect(Collectors.collectingAndThen(Collectors.toSet(), Set::size)); // 先把所有元素收集到Set中再计算Set的大小
``` ```
> 注意:以上每个终止操作只能单独演示,因为对一个流只能执行一次终止操作。并且执行完终止操作后,就不能再对这个流行任何操作,否则将抛出 `java.lang.IllegalStateException: stream has already been operated upon or closed` 异常。 > 注意:以上每个终止操作只能单独演示,因为对一个流只能执行一次终止操作。并且执行完终止操作后,就不能再对这个流行任何操作,否则将抛出 `java.lang.IllegalStateException: stream has already been operated upon or closed` 异常。
### 5.2 分组 ### 5.2 分组
@ -387,22 +396,22 @@ collect(Collectors.collectingAndThen(Collectors.toSet(), Set::size)); // 先把
```java ```java
Stream<Employee> stream = Stream.of(new Employee("张某", "男", "A公司", 20), Stream<Employee> stream = Stream.of(new Employee("张某", "男", "A公司", 20),
new Employee("李某", "女", "A公司", 30), new Employee("李某", "女", "A公司", 30),
new Employee("王某", "男", "B公司", 40), new Employee("王某", "男", "B公司", 40),
new Employee("田某", "女", "B公司", 50)); new Employee("田某", "女", "B公司", 50));
``` ```
```java ```java
public class Employee { public class Employee {
private String name; private String name;
private String gender; private String gender;
private String company; private String company;
private int age; private int age;
@Override @Override
public String toString() {return "Employee{" + "name='" + name + '\'' + '}'; public String toString() {return "Employee{" + "name='" + name + '\'' + '}';
} }
} }
``` ```
@ -435,7 +444,7 @@ stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.counting()
```java ```java
stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.groupingBy(Employee::getGender))); stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.groupingBy(Employee::getGender)));
对应的分组结果如下: 先按照公司分组,再按照性别分组,结果如下:
{ {
B公司={女=[Employee{name='田某'}], 男=[Employee{name='王某'}]}, B公司={女=[Employee{name='田某'}], 男=[Employee{name='王某'}]},
A公司={女=[Employee{name='李某'}], 男=[Employee{name='张某'}]} A公司={女=[Employee{name='李某'}], 男=[Employee{name='张某'}]}
@ -446,13 +455,13 @@ stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.groupingBy
```java ```java
Map<String, List<Employee>> collect = stream.collect(Collectors.groupingBy(employee -> { Map<String, List<Employee>> collect = stream.collect(Collectors.groupingBy(employee -> {
if (employee.getAge() <= 30) { if (employee.getAge() <= 30) {
return "青年员工"; return "青年员工";
} else if (employee.getAge() < 50) { } else if (employee.getAge() < 50) {
return "中年员工"; return "中年员工";
} else { } else {
return "老年员工"; return "老年员工";
} }
})); }));
对应的分组结果如下: 对应的分组结果如下:
@ -465,7 +474,7 @@ Map<String, List<Employee>> collect = stream.collect(Collectors.groupingBy(emplo
### 5.3 分区 ### 5.3 分区
分区是分组的一种特殊情况,即将满足指定条件的分为一组,将不满足指定条件的分为另一组,两者在使用上基本类似,示例如下: 分区是分组的一种特殊情况,即将满足指定条件的元素分为一组,将不满足指定条件的元素分为另一组,两者在使用上基本类似,示例如下:
```java ```java
stream.collect(Collectors.partitioningBy(x -> "A公司".equals(x.getCompany()))); stream.collect(Collectors.partitioningBy(x -> "A公司".equals(x.getCompany())));
@ -489,7 +498,7 @@ stream.parallel();
此时流中的所有元素会被均匀的分配到多个线程上进行处理。并行流内部使用的是 ForkJoinPool 线程池,它默认的线程数量就是处理器数量,可以通过 `Runtime.getRuntime().availableProcessors()` 来查看该值,通常不需要更改。 此时流中的所有元素会被均匀的分配到多个线程上进行处理。并行流内部使用的是 ForkJoinPool 线程池,它默认的线程数量就是处理器数量,可以通过 `Runtime.getRuntime().availableProcessors()` 来查看该值,通常不需要更改。
同时当前也无法为某个具体的流指定线程数量,只能通过修改系统属性 `java.util.concurrent.ForkJoinPool.common.parallelism` 的值来改变线程池大小,进而改变所有并行流的线程大小,示例如下: 当前也没有办法为某个具体的流指定线程数量,只能通过修改系统属性 `java.util.concurrent.ForkJoinPool.common.parallelism` 的值来改变所有并行流使用的线程数量,示例如下:
```java ```java
System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12"); System.setProperty("java.util.concurrent.ForkJoinPool.common.parallelism","12");
@ -505,6 +514,6 @@ stream.sequential();
## 参考文档 ## 参考资料
厄马(Raoul-Gabriel Urma) / 弗斯科(Mario Fusco) / 米克罗夫特(Alan Mycroft) .**《Java 8实战》**. 人民邮电出版社 . 2016-04-01 厄马(Raoul-Gabriel Urma) / 弗斯科(Mario Fusco) / 米克罗夫特(Alan Mycroft) .**《Java 8实战》**. 人民邮电出版社 . 2016-04-01

View File

@ -1,10 +1,26 @@
# Redis 分布式锁 # 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 基本原理 ### 1.1 基本原理
JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源JDK 原生的锁就无能为力。此时可以使用 Redis 或 Zookeeper 来实现分布式锁。 JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源JDK 原生的锁就无能为力。此时可以使用 Redis 来实现分布式锁。
Redis 实现分布式锁的核心命令如下: Redis 实现分布式锁的核心命令如下:
@ -12,7 +28,9 @@ Redis 实现分布式锁的核心命令如下:
SETNX key value SETNX key value
``` ```
SETNX 命令的作用是如果指定的 key 不存在,则创建并将为其设置值,此时返回状态码为 1否则为 0。如果状态码为 1代表获得该锁此时其他进程再次尝试创建时都回返回 0 ,代表锁已经被占用。当获得锁的进程处理完成业务后,再通过 `del` 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。 SETNX 命令的作用是:如果指定的 key 不存在,则创建并为其设置值,然后返回状态码 1如果指定的 key 存在,则直接返回 0。如果返回值为 1代表获得该锁此时其他进程再次尝试创建时由于 key 已经存在,则都会返回 0 ,代表锁已经被占用。
当获得锁的进程处理完成业务后,再通过 `del` 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。
通常为了避免死锁,我们会为锁设置一个超时时间,在 Redis 中可以通过 `expire` 命令来进行实现: 通常为了避免死锁,我们会为锁设置一个超时时间,在 Redis 中可以通过 `expire` 命令来进行实现:
@ -25,8 +43,8 @@ EXPIRE key seconds
```java ```java
Long result = jedis.setnx("lockKey", "lockValue"); Long result = jedis.setnx("lockKey", "lockValue");
if (result == 1) { if (result == 1) {
// 如果此处程序被异常终止如直接kill -9进程则设置超时的操作就无法进行该锁就会出现死锁 // 如果此处程序被异常终止如直接kill -9进程则设置超时的操作就无法进行该锁就会出现死锁
jedis.expire("lockKey", 3); jedis.expire("lockKey", 3);
} }
``` ```
@ -44,8 +62,8 @@ SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
- **EX** :设置超时时间,单位是秒; - **EX** :设置超时时间,单位是秒;
- **PX** :设置超时时间,单位是毫秒; - **PX** :设置超时时间,单位是毫秒;
- **NX** :当且仅当对应的 Key 不存在时才进行设置; - **NX** :当且仅当对应的 Key 不存在时才进行设置;
- **XX**:当且仅当对应的 Key 存在时才进行设置。 - **XX**:当且仅当对应的 Key 存在时才进行设置。
这四个参数从 Redis 2.6.12 版本开始支持,因为当前大多数在用的 Redis 都已经高于这个版本,所以推荐直接使用该命令来实现分布式锁。对应的 Jedis 代码如下: 这四个参数从 Redis 2.6.12 版本开始支持,因为当前大多数在用的 Redis 都已经高于这个版本,所以推荐直接使用该命令来实现分布式锁。对应的 Jedis 代码如下:
@ -53,12 +71,13 @@ SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL]
jedis.set("lockKey", "lockValue", SetParams.setParams().nx().ex(3)); jedis.set("lockKey", "lockValue", SetParams.setParams().nx().ex(3));
``` ```
此时一条命令就可以完成值和超时时间的设置,并且因为只有一条命令,因此其原子性也得到了保证。但因为引入了超时时间来避免死锁,同时也存在了两个其他问题: 此时一条命令就可以完成值和超时时间的设置,并且因为只有一条命令,因此其原子性也得到了保证。但因为引入了超时时间来避免死锁,同时也引出了其它两个问题:
![redis_分布式锁原理](../pictures/redis_分布式锁原理.png) <div align="center"> <img src="../pictures/redis_分布式锁原理.png"/> </div>
+ **问题一**:当业务处理的时间超过过期时间后,由于锁已经被释放,此时其他进程就可以获得该锁,这意味着同时有两个进程进入了临界区,此时分布式锁就失效了;
+ **问题二**:如上图所示,当进程 A 业务处理完成后,此时删除的是进程 B 的锁,进而导致分布式锁又一次失效,进程 B 和 进程 C 同时进入了临界区。 + **问题一**:当业务处理的时间超过过期时间后(图中进程 A由于锁已经被释放此时其他进程就可以获得该锁图中进程 B这意味着有两个进程A 和 B同时进入了临界区此时分布式锁就失效了
+ **问题二**:如上图所示,当进程 A 业务处理完成后,此时删除的是进程 B 的锁,进而导致分布式锁又一次失效,让进程 B 和 进程 C 同时进入了临界区。
针对问题二,我们可以在创建锁时为其指定一个唯一的标识作为 Key 的 Value这里假设我们采用 `UUID + 线程ID` 来作为唯一标识: 针对问题二,我们可以在创建锁时为其指定一个唯一的标识作为 Key 的 Value这里假设我们采用 `UUID + 线程ID` 来作为唯一标识:
@ -87,15 +106,15 @@ jedis.eval(script,
); );
``` ```
接着再看问题一,问题一最简单的解决方法是:你可以估计业务的最大处理时间,然后保证设置的过期时间大于最大处理时间。但是由于业务需要面临各种复杂的情况,因此可能无法保证业务每一次都能在规定的过期时间内处理完成,此时可以使用延长锁时效的策略。 接着再看问题一,问题一最简单的解决方法是:你可以估计业务的最大处理时间,然后保证设置的过期时间大于最大处理时间。但是由于业务面临各种复杂的情况,因此可能无法保证业务每一次都能在规定的过期时间内处理完成,此时可以使用延长锁时效的策略。
### 1.3 延长锁时效 ### 1.3 延长锁时效
延长锁时效的方案如下:假设锁超时时间是 30 秒,此时程序需要每隔一段时间去扫描一下该锁是否还存在,扫描时间需要小于超时时间,通常可以设置为超时时间的 1/3在这里也就是 10 秒扫描一次。如果锁还存在,则重置其超时时间恢复到 30 秒。通过这种方案,只要业务还没有处理完成,锁就会一直有效;而当业务一旦处理完成,程序也会马上删除该锁。 延长锁时效的方案如下:假设锁超时时间是 30 秒,此时程序需要每隔一段时间去扫描一下该锁是否还存在,扫描时间需要小于超时时间,通常可以设置为超时时间的 1/3在这里也就是 10 秒扫描一次。如果锁还存在,则重置其超时时间恢复到 30 秒。通过这种方案,只要业务还没有处理完成,锁就会一直有效;而当业务一旦处理完成,程序也会马上删除该锁。
Redis 的 Java 客户端 Redisson 提供的分布式锁就支持延长锁时效的机制,称为 WatchDog直译过来就是 “看门狗” 机制。 Redis 的 Java 客户端 Redisson 提供的分布式锁就支持类似的延长锁时效的策略,称为 WatchDog直译过来就是 “看门狗” 机制。
以上讨论的都是单机环境下的 Redis 分布式锁,而想要保证 Redis 分布式锁是高可用,首先 Redis 得是高可用的Redis 的高可用模式主要有两种:哨兵模式和集群模式。 以上讨论的都是单机环境下的 Redis 分布式锁,而想要保证 Redis 分布式锁是高可用,首先 Redis 得是高可用的Redis 的高可用模式主要有两种:哨兵模式和集群模式。以下分别进行讨论:
@ -117,38 +136,38 @@ Redis 的 Java 客户端 Redisson 提供的分布式锁就支持延长锁时效
想要在集群模式下实现分布式锁Redis 提供了一种称为 RedLock 的方案,假设我们有 N 个 Redis 实例,此时客户端的执行过程如下: 想要在集群模式下实现分布式锁Redis 提供了一种称为 RedLock 的方案,假设我们有 N 个 Redis 实例,此时客户端的执行过程如下:
+ 以毫秒为单位记录当前的时间,作为开始时间; + 以毫秒为单位记录当前的时间,作为开始时间;
+ 接着采用和单机版相同的方式,依次尝试在每个实例上创建锁。为了避免客户端长时间与某个故障的 Redis 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为 10 秒,则访问每个 Redis 实例的超时时间可能在 5 到 50 毫秒之间,如果在这个时间内还没有建立通信,则尝试下一个实例; + 接着采用和单机版相同的方式,依次尝试在每个实例上创建锁。为了避免客户端长时间与某个故障的 Redis 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为 10 秒,则访问每个 Redis 实例的超时时间可能在 5 到 50 毫秒之间,如果在这个时间内还没有建立通信,则尝试连接下一个实例;
+ 如果在至少 N/2+1 个实例上都成功创建了锁。并且 `当前时间 - 开始时间 < 锁的超时时间` ,则认为已经获取了锁,锁的有效时间等于 `超时时间 - 花费时间`(如果考虑到不同 Redis 实例所在的服务器存在时钟漂移,则还需要减去时钟漂移); + 如果在至少 N/2+1 个实例上都成功创建了锁。并且 `当前时间 - 开始时间 < 锁的超时时间` ,则认为已经获取了锁,锁的有效时间等于 `超时时间 - 花费时间`(如果考虑不同 Redis 实例所在服务器的时钟漂移,则还需要减去时钟漂移);
+ 如果少于 N/2+1 个实例,则认为创建分布式锁失败,此时需要删除这些实例上已创建的锁,以便其他客户端进行创建。 + 如果少于 N/2+1 个实例,则认为创建分布式锁失败,此时需要删除这些实例上已创建的锁,以便其他客户端进行创建。
+ 该客户端在失败后,可以等待一个随机的时间后,再次进行重试。 + 该客户端在失败后,可以等待一个随机的时间后,再次进行重试。
以上就是 RedLock 的实现方案,可以看到主要是由客户端来实现的,并不真正涉及到 Redis 集群相关的功能。因此这里的 N 个 Redis 实例并不要求是一个真正的 Redis 集群,它们彼此之间可以是完全独立的,但由于只需要半数节点获得锁就能真正获得锁,因此对于分布式锁功能而言,其仍然是高可用的。后面使用 Redisson 来演示 RedLock 时会再次验证这一点。 以上就是 RedLock 的实现方案,可以看到主要是由客户端来实现的,并不真正涉及到 Redis 集群相关的功能。因此这里的 N 个 Redis 实例并不要求是一个真正的 Redis 集群,它们彼此之间可以是完全独立的,但由于只需要半数节点获得锁就能真正获得锁,因此其仍然具备容错性和高可用性。后面使用 Redisson 来演示 RedLock 时会再次验证这一点。
### 3.2 低延迟通讯和多路复用 ### 3.2 低延迟通讯
实现 RedLock 方案的客户端与所有 Redis 实例进行通讯时,必须要保证低延迟,而且最好能使用多路复用技术来保证一次性将 SET 命令发送到所有 Redis 节点上,并获取到对应的执行结果。假设网络延迟高,此时客户端 A 和 B 分别尝试创建锁: 另外实现 RedLock 方案的客户端与所有 Redis 实例进行通讯时,必须要保证低延迟,而且最好能使用多路复用技术来保证一次性将 SET 命令发送到所有 Redis 节点上,并获取到对应的执行结果。如果网络延迟较高,假设客户端 A 和 B 都在尝试创建锁:
```shell ```shell
SET key 随机数A EX 3 NX #A客户端 SET key 随机数A EX 3 NX #A客户端
SET key 随机数B EX 3 NX #B客户端 SET key 随机数B EX 3 NX #B客户端
``` ```
假设客户端 A 在一半节点上创建了锁,而客户端 B 在另外一半节点上创建了锁,此时两个客户端都无法获取到锁。如果并发很高,则可能存在多个客户端分别在部分节点上创建了锁,而没有一个客户端的数量超过 N/2+1。这也就是上面过程的最后一步中强调一旦客户端失败后需要等待一个随机时间后再进行重试的原因如果是一个固定时间则所有失败的客户端又同时发起重试情况就还是一样。 此时可能客户端 A 在一半节点上创建了锁,而客户端 B 在另外一半节点上创建了锁,那么两个客户端都将无法获取到锁。如果并发很高,则可能存在多个客户端分别在部分节点上创建了锁,而没有一个客户端的数量超过 N/2+1。这也就是上面过程的最后一步中强调一旦客户端失败后需要等待一个随机时间后再进行重试的原因如果是一个固定时间则所有失败的客户端又同时发起重试情况就还是一样。
因此最佳的实现就是客户端的 SET 命令能几乎同时到达所有节点,并几乎同时接受到所有执行结果。 想要保证这一点,低延迟的网络通信极为关键,下文介绍的 Redisson 就采用 Netty 来实现了这一功能 因此最佳的实现就是客户端的 SET 命令能几乎同时到达所有节点,并几乎同时接受到所有执行结果。 想要保证这一点,低延迟的网络通信极为关键,下文介绍的 Redisson 就采用 Netty 框架来保证这一功能的实现
### 3.3 持久化与高可用 ### 3.3 持久化与高可用
为了保证高可用,所有 Redis 节点需要开启持久化。假设不开启持久化,假设进程 A 获得锁后正在处理业务逻辑,此时节点宕机重启,因为锁数据丢失了,其他进程便可以再次获得该锁,因此所有 Redis 节点都需要开启 AOF 持久化方式。 为了保证高可用,所有 Redis 节点需要开启持久化。假设不开启持久化,假设进程 A 获得锁后正在处理业务逻辑,此时节点宕机重启,因为锁数据丢失了,其他进程便可以再次创建该锁,因此所有 Redis 节点都需要开启 AOF 持久化方式。
AOF 默认的同步机制为 `everysec`,即每秒进程一次持久化,此时能够兼顾性能与数据安全,发生意外宕机的时,最多会丢失一秒的数据。但如果碰巧就是在这一秒的时间内进程 A 创建了锁,此时其他进程也可以获得该锁,锁的互斥性也就失效了。要解决这个问题有两种方式: AOF 默认的同步机制为 `everysec`,即每秒进程一次持久化,此时能够兼顾性能与数据安全,发生意外宕机的时,最多会丢失一秒的数据。但如果碰巧就是在这一秒的时间内进程 A 创建了锁,并由于宕机而导致数据丢失。此时其他进程还可以创建该锁,锁的互斥性也就失效了。想要解决这个问题有两种方式:
+ **方式一**:修改 Redis.conf 中 `appendfsync` 的值为 always即每次命令后都进行持久化此时降低 Redis 性能,进而也会降低分布式锁的性能,但锁的互斥性得到了绝对的保证; + **方式一**:修改 Redis.conf 中 `appendfsync` 的值为 `always`,即每次命令后都进行持久化,此时降低 Redis 性能,进而也会降低分布式锁的性能,但锁的互斥性得到了绝对的保证;
+ **方式二**:一旦节点宕机了,等到锁的超时时间过了之后才进行重启,此时相当于原有锁自然失效(你需要保证业务在自己设定的超时时间内完成),这种方案称为延时重启。 + **方式二**:一旦节点宕机了,需要等到锁的超时时间过了之后才进行重启,此时相当于原有锁自然失效(首先需要保证业务在设定的超时时间内完成),这种方案称为延时重启。
@ -169,27 +188,28 @@ RedissonClient redissonClient = Redisson.create(config);
// 2.创建锁实例 // 2.创建锁实例
RLock lock = redissonClient.getLock("myLock"); RLock lock = redissonClient.getLock("myLock");
try { try {
//3.尝试获取分布式锁,第一个参数为等待时间,第二个参数为锁过期时间 //3.尝试获取分布式锁,第一个参数为等待时间,第二个参数为锁过期时间
boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS); boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
if (isLock) { if (isLock) {
// 4.模拟业务处理 // 4.模拟业务处理
System.out.println("处理业务逻辑"); System.out.println("处理业务逻辑");
Thread.sleep(20 * 1000); Thread.sleep(20 * 1000);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
//5.释放锁 //5.释放锁
lock.unlock(); lock.unlock();
} }
redissonClient.shutdown(); redissonClient.shutdown();
``` ```
此时对应在 Redis 中的数据结构如下: 此时对应在 Redis 中的数据结构如下:
![redis_分布式锁_cli1](../pictures/redis_分布式锁_cli1.png) <div align="center"> <img src="../pictures/redis_分布式锁_cli1.png"/> </div>
可以看到 key 就是代码中设置的锁名,而 value 值的类型是 hash其中键 `9280e909-c86b-43ec-b11d-6e5a7745e2e9:13` 的格式为 `UUID + 线程ID`,键对应的值为 1代表加锁的次数。之所以要采用 hash 这种格式,主要是因为 Redisson 创建的锁是具有重入性的,即你可以多次进行加锁:
可以看到 key 就是代码中设置的锁名,而 value 值的类型是 hash其中键 `9280e909-c86b-43ec-b11d-6e5a7745e2e9:13` 的格式为 `UUID + 线程ID` ;键对应的值为 1代表加锁的次数。之所以要采用 hash 这种格式,主要是因为 Redisson 创建的锁是具有重入性的,即你可以多次进行加锁:
```java ```java
boolean isLock1 = lock.tryLock(0, 30, TimeUnit.SECONDS); boolean isLock1 = lock.tryLock(0, 30, TimeUnit.SECONDS);
@ -198,9 +218,10 @@ boolean isLock2 = lock.tryLock(0, 30, TimeUnit.SECONDS);
此时对应的值就会变成 2代表加了两次锁 此时对应的值就会变成 2代表加了两次锁
![redis_分布式锁_cli2](../pictures/redis_分布式锁_cli2.png) <div align="center"> <img src="../pictures/redis_分布式锁_cli2.png"/> </div>
当然和其他重入锁一样,需要保证加锁的次数和解锁的次数一样,才能完全解锁:
当然和其他重入锁一样,需要保证解锁的次数和加锁的次数一样,才能完全解锁:
```java ```java
lock.unlock(); lock.unlock();
@ -243,17 +264,17 @@ RLock lock03 = redissonClient03.getLock(lockName);
RedissonRedLock redLock = new RedissonRedLock(lock01, lock02, lock03); RedissonRedLock redLock = new RedissonRedLock(lock01, lock02, lock03);
try { try {
boolean isLock = redLock.tryLock(10, 300, TimeUnit.SECONDS); boolean isLock = redLock.tryLock(10, 300, TimeUnit.SECONDS);
if (isLock) { if (isLock) {
// 4.模拟业务处理 // 4.模拟业务处理
System.out.println("处理业务逻辑"); System.out.println("处理业务逻辑");
Thread.sleep(200 * 1000); Thread.sleep(200 * 1000);
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
//5.释放锁 //5.释放锁
redLock.unlock(); redLock.unlock();
} }
redissonClient01.shutdown(); redissonClient01.shutdown();
@ -263,7 +284,8 @@ redissonClient03.shutdown();
此时每个 Redis 实例上锁的情况如下: 此时每个 Redis 实例上锁的情况如下:
![redis_分布式锁_cli3](../pictures/redis_分布式锁_cli3.png) <div align="center"> <img src="../pictures/redis_分布式锁_cli3.png"/> </div>
可以看到每个实例上都获得了锁。 可以看到每个实例上都获得了锁。
@ -281,24 +303,24 @@ RedissonClient redissonClient = Redisson.create(config);
// 2.创建锁实例 // 2.创建锁实例
RLock lock = redissonClient.getLock("myLock"); RLock lock = redissonClient.getLock("myLock");
try { try {
//3.尝试获取分布式锁,第一个参数为等待时间 //3.尝试获取分布式锁,第一个参数为等待时间
boolean isLock = lock.tryLock(0, TimeUnit.SECONDS); boolean isLock = lock.tryLock(0, TimeUnit.SECONDS);
if (isLock) { if (isLock) {
// 4.模拟业务处理 // 4.模拟业务处理
System.out.println("处理业务逻辑"); System.out.println("处理业务逻辑");
Thread.sleep(60 * 1000); Thread.sleep(60 * 1000);
System.out.println("锁剩余的生存时间:" + lock.remainTimeToLive()); System.out.println("锁剩余的生存时间:" + lock.remainTimeToLive());
} }
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} finally { } finally {
//5.释放锁 //5.释放锁
lock.unlock(); lock.unlock();
} }
redissonClient.shutdown(); redissonClient.shutdown();
``` ```
这里我们通过 `config.setLockWatchdogTimeout(30 * 1000)` 将 lockWatchdogTimeout 的值设置为 30000 毫秒(默认值也是 30000 毫秒。lockWatchdogTimeout 只会对那些没有设置锁超时时间的锁生效,所以我们这里调用的是两个参数的 `tryLock()` 方法: 首先 Redisson 的 WatchDog 机制只会对那些没有设置锁超时时间的锁生效,所以我们这里调用的是两个参数的 `tryLock()` 方法:
```java ```java
boolean tryLock(long time, TimeUnit unit) throws InterruptedException; boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
@ -310,9 +332,9 @@ boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException; boolean tryLock(long waitTime, long leaseTime, TimeUnit unit) throws InterruptedException;
``` ```
Redisson 的 WatchDog 机制会以 lockWatchdogTimeout 的 1/3 时长为周期(在这里就是 10 秒对所有未设置超时时间的锁进行检查如果业务尚未处理完成也就是锁还没有被程序主动删除Redisson 就会将锁的超时时间重置为 lockWatchdogTimeout 指定的值(在这里就是设置的 30 秒),直到锁被程序主动删除。因此在上面的例子中可以看到,不论将模拟业务的睡眠时间设置为多长,其锁都会存在一定的剩余生存时间,直至业务处理完成。 其次我们通过 `config.setLockWatchdogTimeout(30 * 1000)` 将 lockWatchdogTimeout 的值设置为 30000 毫秒(默认值也是 30000 毫秒)。此时 Redisson 的 WatchDog 机制会以 lockWatchdogTimeout 的 1/3 时长为周期(在这里就是 10 秒对所有未设置超时时间的锁进行检查如果业务尚未处理完成也就是锁还没有被程序主动删除Redisson 就会将锁的超时时间重置为 lockWatchdogTimeout 指定的值(在这里就是设置的 30 秒),直到锁被程序主动删除位置。因此在上面的例子中可以看到,不论将模拟业务的睡眠时间设置为多长,其锁都会存在一定的剩余生存时间,直至业务处理完成。
反之,如果明确的指定了锁的超时时间 leaseTime则以 leaseTime 的时间为准WatchDog 机制对明确指定超时时间的锁不会生效。 反之,如果明确的指定了锁的超时时间 leaseTime则以 leaseTime 的时间为准,因为 WatchDog 机制对明确指定超时时间的锁不会生效。

View File

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

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