From 6ba5a174202549bed36a811ab71972474e1f2727 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BD=97=E7=A5=A5?= <1366971433@qq.com> Date: Wed, 6 May 2020 17:43:59 +0800 Subject: [PATCH] =?UTF-8?q?CAP=E7=90=86=E8=AE=BA=E5=92=8CBASE=E7=90=86?= =?UTF-8?q?=E8=AE=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SREADME.md => CREADME.md | 100 +++++-- README.md | 97 ++----- code/Java/stream-tutorial/pom.xml | 54 ---- .../java/com/heibaiying/DistributedLock.java | 41 --- .../main/java/com/heibaiying/Employee.java | 70 ----- .../main/java/com/heibaiying/StreamTest.java | 62 ---- .../src/main/resources/log4j.properties | 7 - notes/CAP理论和BASE理论.md | 82 ++++++ notes/Https.md | 40 --- notes/Java_函数式编程.md | 189 ++++++------ notes/Redis_分布式锁原理.md | 144 ++++++---- notes/ZooKeeper_分布式锁原理.md | 271 ++++++++++-------- pictures/cap_示例.png | Bin 0 -> 8711 bytes pictures/cap理论.jpg | Bin 0 -> 36545 bytes 14 files changed, 503 insertions(+), 654 deletions(-) rename SREADME.md => CREADME.md (58%) delete mode 100644 code/Java/stream-tutorial/pom.xml delete mode 100644 code/Java/stream-tutorial/src/main/java/com/heibaiying/DistributedLock.java delete mode 100644 code/Java/stream-tutorial/src/main/java/com/heibaiying/Employee.java delete mode 100644 code/Java/stream-tutorial/src/main/java/com/heibaiying/StreamTest.java delete mode 100644 code/Java/stream-tutorial/src/main/resources/log4j.properties create mode 100644 notes/CAP理论和BASE理论.md delete mode 100644 notes/Https.md create mode 100644 pictures/cap_示例.png create mode 100644 pictures/cap理论.jpg diff --git a/SREADME.md b/CREADME.md similarity index 58% rename from SREADME.md rename to CREADME.md index 4e67999..9485d9d 100644 --- a/SREADME.md +++ b/CREADME.md @@ -2,43 +2,64 @@

-
一个处于萌芽阶段的知识库,用于持续分享自己的所见、所学、所思!
-
-
- 点击切换详细目录 -
## :coffee: JAVA 1. [Java 反射与注解](notes/Java_反射与注解.md) + + 反射机制、自定义注解、@Target 与 @Retention、注解的继承 + 2. [Java 并发编程](notes/Java_并发编程.md) -3. [Java 设计模式](notes/Java_设计模式.md) -4. [Java 虚拟机](notes/Java_虚拟机.md) -5. [JVM 性能监控之命令行工具](notes/JVM_性能监控之命令行工具.md) -6. [JVM 性能监控之可视化工具](notes/JVM_性能监控之可视化工具.md) -7. [Java NIO 核心组件详解](notes/Java_NIO.md) -8. 函数式编程 + + 非原子性协定、计算机多级高速缓存、缓存一致性协议、写缓冲与无效化队列、内存屏障、锁机制、无锁 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 集合类源码解析
-## :globe_with_meridians: 计算机与网络基础 +## :globe_with_meridians: 网络基础 1. [计算机网络模型](notes/计算机网络.md) -2. HTTP 协议详解 + 四层、五层、七层计算机网络模型、信道复用、PPP 协议、ARP 协议、划分子网与构成超网、TCP 三次握手与四次挥手 -3. HTTPS 协议详解 +3. HTTPS 与 通信安全 4. 抓包神器 Wireshark -5. 计算机组成原理
@@ -46,10 +67,16 @@ 1. [JavaScript 基础](notes/JavaScript_基础.md) + 基本数据类型、引用类型、内置对象(Global 与 window)、作用域与闭包、对象设计 + 2. [ECMAScript 6.0 基础](notes/ES6_基础.md) + 变量声明、对象字面量、对象解构、Symbol、迭代器与生成器、类、代理与反射、模块化 + 3. CSS 基础 + 选择器、非局部样式、布局样式、效果属性、CSS 动画 + 4. JavaScript 设计模式
@@ -60,10 +87,16 @@ 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) @@ -78,9 +111,15 @@ 3. [Redis 哨兵模式](notes/Redis_哨兵模式.md) + 复制机制、哨兵模式架构说明、哨兵模式搭建 + 4. [Redis 集群模式](notes/Redis_集群模式.md) -5. 使用 Redis 实现分布式锁 + 数据分区、节点通信、请求路由、故障发现与恢复、集群扩容与缩容 + +5. [Redis 分布式锁原理](notes/Redis_分布式锁原理.md) + + 分布式锁原理、单机模式下的分布式锁、集群模式下的分布式锁、RedLock 原理、Redisson ### MongoDB @@ -89,12 +128,20 @@ 2. [MongoDB 索引](notes/MongoDB_索引.md) + 单字段索引、复合索引、多键索引、哈希所有、地理空间索引、文本索引;唯一索引、稀疏索引、部分索引、TTL 索引 + 3. [MongoDB 聚合](notes/MongoDB_聚合.md) + 常用聚合管道、单用途聚合方法、MapReduce + 4. [MongoDB 复制](notes/MongoDB_复制.md) + 复制功能、故障发现、优先选举、投票成员、副本集搭建 + 5. [MongoDB 分片](notes/MongoDB_分片.md) + 分片副本集配置、配置副本集配置、路由服务配置 +
## :whale: 系统与容器 @@ -103,8 +150,12 @@ 2. [Sehll 脚本编程基础](notes/Shell_基础.md) + 创建脚本、分支语句、循环语句、处理用户输入、处理用户输出、创建函数、处理信号、定时作业 + 3. [Docker 基础](notes/Docker_基础.md) + 核心概念(镜像、容器、仓库)、Docker 常用命令、DockerFile 常用指令 +
## :package: 常用技术栈 @@ -122,7 +173,6 @@ 1. [Nginx 基础之静态网站部署,负载均衡,动静分离](notes/Nginx_基础.md) 2. HTTP 模块详解 -3. Nginx 性能优化 ### Kafka @@ -141,29 +191,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 -
-## :rocket: 测试与运维 -1. 性能测试之 Jmeter -2. 性能测试之 LoadRunner -3. Jenkins 持续交付与自动化部署 +
## :bullettrain_side: 微服务与分布式 -1. 分布式锁的实现 -2. 分布式选举算法 -3. 分布式事务实现原理 -4. 分布式全局 ID 的生成 -5. CAP 理论和 BASE 理论 +1. [CAP 理论 和 BASE 理论](notes/CAP理论和BASE理论.md) + +
diff --git a/README.md b/README.md index 93601c2..9a7d06f 100644 --- a/README.md +++ b/README.md @@ -8,61 +8,43 @@ + +
+ +

点击切换详细目录

+ ## :coffee: JAVA 1. [Java 反射与注解](notes/Java_反射与注解.md) - 反射机制、自定义注解、@Target 与 @Retention、注解的继承 - 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 内存区域、垃圾收集算法、经典垃圾收集器、双亲委派模型、分层编译、热点代码探测、方法内联、逃逸分析 - -5. [JVM 性能监控之命令行工具](notes/JVM_性能监控之命令行工具.md) +7. [JVM 性能监控之命令行工具](notes/JVM_性能监控之命令行工具.md) - jps 命令、jstat 命令、jinfo 命令、jmap 命令、jhat 命令、jstack 命令 - -6. [JVM 性能监控之可视化工具](notes/JVM_性能监控之可视化工具.md) - - JConsole、VisualVM 、监控本地进程、监控远程进程 - -7. [Java NIO 核心组件详解](notes/Java_NIO.md) - - 缓冲区 Buffer、通道 Channel、选择器 Selector、实现多人聊天室 - -8. 高性能网络框架 Netty +8. [JVM 性能监控之可视化工具](notes/JVM_性能监控之可视化工具.md) 9. [Tomcat 架构解析](notes/Tomcat_架构解析.md) - 核心组件、连接器、多层容器、请求处理流程、程序启动过程、类加载器 - 10. Java 集合类源码解析 -11. 函数式编程 -
-## :globe_with_meridians: 计算机与网络基础 +## :globe_with_meridians: 网络基础 1. [计算机网络模型](notes/计算机网络.md) - 四层、五层、七层计算机网络模型、信道复用、PPP 协议、ARP 协议、划分子网与构成超网、TCP 三次握手与四次挥手 - -2. HTTP 协议详解 - -3. HTTPS 协议详解 +3. HTTPS 与 通信安全 4. 抓包神器 Wireshark -5. 计算机组成原理
@@ -70,16 +52,10 @@ 1. [JavaScript 基础](notes/JavaScript_基础.md) - 基本数据类型、引用类型、内置对象(Global 与 window)、作用域与闭包、对象设计 - 2. [ECMAScript 6.0 基础](notes/ES6_基础.md) - 变量声明、对象字面量、对象解构、Symbol、迭代器与生成器、类、代理与反射、模块化 - 3. CSS 基础 - 选择器、非局部样式、布局样式、效果属性、CSS 动画 - 4. JavaScript 设计模式
@@ -90,16 +66,10 @@ 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) @@ -114,35 +84,19 @@ 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) - 分片副本集配置、配置副本集配置、路由服务配置 -
## :whale: 系统与容器 @@ -151,12 +105,8 @@ 2. [Sehll 脚本编程基础](notes/Shell_基础.md) - 创建脚本、分支语句、循环语句、处理用户输入、处理用户输出、创建函数、处理信号、定时作业 - 3. [Docker 基础](notes/Docker_基础.md) - 核心概念(镜像、容器、仓库)、Docker 常用命令、DockerFile 常用指令 -
## :package: 常用技术栈 @@ -174,7 +124,6 @@ 1. [Nginx 基础之静态网站部署,负载均衡,动静分离](notes/Nginx_基础.md) 2. HTTP 模块详解 -3. Nginx 性能优化 ### Kafka @@ -193,29 +142,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 + -
-## :rocket: 测试与运维 -1. 性能测试之 Jmeter -2. 性能测试之 LoadRunner -3. Jenkins 持续交付与自动化部署
## :bullettrain_side: 微服务与分布式 -1. 分布式锁的实现 -2. 分布式选举算法 -3. 分布式事务实现原理 -4. 分布式全局 ID 的生成 -5. CAP 理论和 BASE 理论 +1. [CAP 理论 和 BASE 理论](notes/CAP理论和BASE理论.md) + +
diff --git a/code/Java/stream-tutorial/pom.xml b/code/Java/stream-tutorial/pom.xml deleted file mode 100644 index 9824945..0000000 --- a/code/Java/stream-tutorial/pom.xml +++ /dev/null @@ -1,54 +0,0 @@ - - - 4.0.0 - - org.example - stream-tutorial - 1.0-SNAPSHOT - - - - org.apache.maven.plugins - maven-compiler-plugin - - 8 - 8 - - - - - - - - - - org.redisson - redisson - 3.12.5 - - - - redis.clients - jedis - 3.2.0 - - - org.apache.curator - curator-framework - 4.3.0 - - - org.apache.curator - curator-recipes - 4.3.0 - - - org.apache.zookeeper - zookeeper - 3.4.14 - - - - \ No newline at end of file diff --git a/code/Java/stream-tutorial/src/main/java/com/heibaiying/DistributedLock.java b/code/Java/stream-tutorial/src/main/java/com/heibaiying/DistributedLock.java deleted file mode 100644 index 81bf86a..0000000 --- a/code/Java/stream-tutorial/src/main/java/com/heibaiying/DistributedLock.java +++ /dev/null @@ -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(); - } - -} diff --git a/code/Java/stream-tutorial/src/main/java/com/heibaiying/Employee.java b/code/Java/stream-tutorial/src/main/java/com/heibaiying/Employee.java deleted file mode 100644 index 773ebca..0000000 --- a/code/Java/stream-tutorial/src/main/java/com/heibaiying/Employee.java +++ /dev/null @@ -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; - } - -} diff --git a/code/Java/stream-tutorial/src/main/java/com/heibaiying/StreamTest.java b/code/Java/stream-tutorial/src/main/java/com/heibaiying/StreamTest.java deleted file mode 100644 index dad7c13..0000000 --- a/code/Java/stream-tutorial/src/main/java/com/heibaiying/StreamTest.java +++ /dev/null @@ -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 - * @return - */ - public static T reduce(List list, T initValue, BinaryOperator binaryOperator) { - for (T t : list) { - initValue = binaryOperator.apply(initValue, t); - } - return initValue; - } - - /** - * 集合过滤 - * - * @param list 待过滤的集合 - * @param predicate 函数式接口 - * @param 集合中元素的类型 - * @return 满足条件的元素的集合 - */ - public static List filter(List list, CustomPredicate predicate) { - ArrayList result = new ArrayList<>(); - for (T t : list) { - if (predicate.test(t)) result.add(t); - } - return result; - } - - - /** - * 定义接口 - * - * @param 参数类型 - */ - @FunctionalInterface - public interface CustomPredicate { - // 判断是否满足过滤标准 - boolean test(T t); - } - -} diff --git a/code/Java/stream-tutorial/src/main/resources/log4j.properties b/code/Java/stream-tutorial/src/main/resources/log4j.properties deleted file mode 100644 index b5451a7..0000000 --- a/code/Java/stream-tutorial/src/main/resources/log4j.properties +++ /dev/null @@ -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 \ No newline at end of file diff --git a/notes/CAP理论和BASE理论.md b/notes/CAP理论和BASE理论.md new file mode 100644 index 0000000..277515b --- /dev/null +++ b/notes/CAP理论和BASE理论.md @@ -0,0 +1,82 @@ +# CAP 理论 和 BASE 理论 + + + +## 一、CAP 理论 + +### 1.1 基本概念 + +#### 1. 一致性 + +在分布式环境中,一致性是指数据在多个节点之间能够保持一致的特性。如果在某个节点上执行变更操作后,用户可以立即从其他任意节点上读取到变更后的数据,那么就认为这样的系统具备强一致性。 + +#### 2. 可用性 + +可以性是指系统提供的服务必须一直处于可用状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。它主要强调以下两点: + ++ **有限的时间内**:对于用户的一个请求操作,系统必须要在指定的时间内返回处理结果,如果超过这个时间,那么系统就被认为是不可用的。 ++ **返回结果**:不论成功或者失败,都需要明确地返回响应结果。 + +#### 3. 分区容错性 + +分区容错性指定是分布式系统在遇到网络分区时,仍需要能够对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障。 + +这里的网络分区指的是:在分布式系统中,由于不同的节点会分布在不同子网中(不同机房或异地网络等),由于一些特殊的原因,可能会出现子网内部是正常的,但子网彼此之间却无法正常通信,从而导致整个系统的网络被切分成若干个独立的区域,这就是网络分区。 + + + +### 1.2 CAP 理论 + +CAP 理论强调:一个分布式系统不可能同时满足一致性(C:Consistency)、可用性(A:Availability)和分区容错性(P:Partition tolerance)这三个需求,最多只能同时满足其中的两个。这里我们来进行一下解释说明: + +首先对于一个分布式系统而言,网络分区是不可避免的,不可能永远不出现网络故障,所以分区容错性 P 必须要保证。假设一个分布式系统中出现了网络分区,如下: + +
+ + +假设用户 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**:这种情况相当于放弃一致性。具体而言,是放弃数据的强一致性,但保证数据的最终一致性。因为不论是什么系统,数据最终都需要保持一致,否则整个系统就无法使用。在这种策略下,在某个短暂的时间窗口内会存在数据不一致的情况。 + +
+ + +## 二、BASE 理论 + +BASE是对基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventually Consistent)三个短语的简写,它是对 CAP 理论中 AP 策略的延伸。其核心是即便无法做到强一致性,但每个系统应用都应该根据自身业务的特点,采取适当的方式来保证系统的最终一致性,而具体的方案就体现在这三个短语上: + +#### 1. 基本可用 + +基本可用是指分布式系统在出现不可预知的故障时,允许损失部分可用性,例如: + ++ 延长响应时间:比如原来的的查询只需要 0.5 秒,现在延长到 1~ 2 秒; ++ 服务降级:比如在商品秒杀时,部分用户会被引导到一个降级页面。 + +#### 2. 软状态 + +软状态也称为弱状态,是指允许系统中的数据存在中间状态,并认为该中间状态的存在不会影响系统整体的可用性,即允许不同节点间的数据同步存在延时。 + +#### 3. 最终一致性 + +最终一致性强调的是系统中所有的数据副本,在经过一段时间的同步后,最终需要达到一致的状态。 + + + + + +## 参考资料 + +主要摘录自:倪超 . 从 Paxos 到 Zookeeper——分布式一致性原理与实践 . 电子工业出版社 . 2015-02-01 \ No newline at end of file diff --git a/notes/Https.md b/notes/Https.md deleted file mode 100644 index 2317720..0000000 --- a/notes/Https.md +++ /dev/null @@ -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 年,TETF(Internet Engineering Task Force)组织在 SLL v3 的基础进一步进行了标准化,微软为这个新的协议取名为 TLS v1.0,这也就是TLS(Transport Layer Security)的由来。之后 TLS 继续发布了 v1.1,v1.2,v1.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 对称加密 - - - -## 三、握手过程 - diff --git a/notes/Java_函数式编程.md b/notes/Java_函数式编程.md index 44a0aa7..9c33548 100644 --- a/notes/Java_函数式编程.md +++ b/notes/Java_函数式编程.md @@ -1,10 +1,20 @@ # Java 函数式编程 + + + ## 一、Lambda ### 1.1 格式 -Java 从 1.8 版本开始支持 Lambda 表达式,通过 Lambda 表达式我们可以将一个函数作为参数传入方法中。在 JDK 1.8 之前,我们只能通过匿名表达式来完成类似的功能,但是匿名表达式比较繁琐,存在大量的冗余代码,不利于将行为参数化,而采用 Lamdba 则能很好的解决这个问题。Lambda 表达式的基本语法如下: +JDK 从 1.8 版本开始支持 Lambda 表达式,通过 Lambda 表达式我们可以将一个函数作为参数传入方法中。在 JDK 1.8 之前,我们只能通过匿名表达式来完成类似的功能,但是匿名表达式比较繁琐,存在大量的模板代码,不利于将行为参数化,而采用 Lamdba 则能很好的解决这个问题。Lambda 表达式的基本语法如下: ```java (parameters) -> expression @@ -18,8 +28,8 @@ Java 从 1.8 版本开始支持 Lambda 表达式,通过 Lambda 表达式我们 Lambda 表达式具有如下特点: -- **可选的参数:**不需要声明参数类型,编译器会从上下文自动进行推断; -- **可选的参数圆括号:**当且仅当只有一个参数时,圆括号可以省略; +- **可选的参数:**不需要声明参数类型,编译器会依靠上下文进行自动推断; +- **可选的参数圆括号:**当且仅当只有一个参数时,包裹参数的圆括号可以省略; - **可选的花括号:**如果主体只有一个表达式,则无需使用花括号; - **可选的返回关键字:**如果主体只有一个表达式,则该表达式的值就是整个 Lambda 表达式的返回值,此时不需要使用 return 关键字进行显式的返回。 @@ -34,7 +44,7 @@ Lambda 表达式具有如下特点: */ @FunctionalInterface public interface CustomPredicate { - boolean test(T t); + boolean test(T t); } ``` @@ -47,12 +57,12 @@ public interface CustomPredicate { * @return 满足条件的元素的集合 */ public static List filter(List list, CustomPredicate predicate) { - ArrayList result = new ArrayList<>(); - for (T t : list) { + ArrayList result = new ArrayList<>(); + for (T t : list) { // 将满足条件的元素添加到返回集合中 - if (predicate.test(t)) result.add(t); - } - return result; + if (predicate.test(t)) result.add(t); + } + return result; } ``` @@ -64,9 +74,9 @@ List integers = Arrays.asList(1, 2, 3, 4, 5); filter(integers, x -> x % 2 == 0); // 过滤出所有偶数 List employees = Arrays.asList( - new Employee("张某", 21, true), - new Employee("李某", 30, true), - new Employee("王某", 45, false)); + new Employee("张某", 21, true), + new Employee("李某", 30, true), + new Employee("王某", 45, false)); filter(employees, employee -> employee.getAge() > 25); // 过滤出所有年龄大于25的员工 ``` @@ -76,7 +86,7 @@ filter(employees, employee -> employee.getAge() > 25); // 过滤出所有年龄 ```java new Thread(() -> { - System.out.println("hello"); + System.out.println("hello"); }); ``` @@ -148,10 +158,10 @@ public interface Predicate { } ``` -其他函数式接口都是这四种基本类型的延伸和扩展。以 BiFunction 和 BinaryOperator 接口为例: +其他函数式接口都是这四种基本类型的扩展和延伸。以 BiFunction 和 BinaryOperator 接口为例: + **BiFunction**:是函数型接口 Function 的扩展,Function 只能接收一个入参;而 BiFunction 可以用于接收两个不同类型的入参; -+ **BinaryOperator\**:是 BiFunction 的一种特殊化情况,即两个入参和返回值的类型均相同,通常用于二元运算: ++ **BinaryOperator\**:是 BiFunction 的一种特殊化情况,即两个入参和返回值的类型均相同,通常用于二元运算。定义如下: ```java @FunctionalInterface @@ -165,24 +175,23 @@ public interface BinaryOperator extends BiFunction { } ``` -使用示例如下: +下面演示一下 BinaryOperator 的用法: ```java -public static void main(String[] args) { - List 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 reduce(List list, T initValue, BinaryOperator binaryOperator) { - for (T t : list) { - initValue = binaryOperator.apply(initValue, t); - } - return initValue; + for (T t : list) { + initValue = binaryOperator.apply(initValue, t); + } + return initValue; +} + +public static void main(String[] args) { + List 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 reduce(List list, T initValue, BinaryOperator binaryOp ## 三、创建流 -JDK 1.8 中最主要的变化是引入了流,通过流、Lamda 表达式以及函数式接口,可以高效地完成数据的处理。创建流通常有以下四种方法: +JDK 1.8 中另一个大的改进是引入了流,通过流、Lamda 表达式以及函数式接口,可以高效地完成数据的处理。创建流通常有以下四种方法: **1. 由值创建** @@ -216,7 +225,7 @@ List strings = Arrays.asList("a", "b ", "c", "d"); Stream stream = strings.stream(); ``` -`stream()` 方法定义在 `Collection` 接口中,它是一个默认方法,因此大多数的集合都可以通过该方法转换为流: +`stream()` 方法定义在 `Collection` 接口中,它是一个默认方法,因此大多数的集合都可以通过该方法来创建流: ```java public interface Collection extends Iterable { @@ -230,9 +239,9 @@ public interface Collection extends Iterable { ```java try (Stream lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.UTF_8)) { - lines.forEach(System.out::println); + lines.forEach(System.out::println); } catch (IOException e) { - e.printStackTrace(); + e.printStackTrace(); } ``` @@ -240,10 +249,10 @@ try (Stream lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.U 除了以上方法外,还可以通过 `Stream.iterate()` 和 `Stream.generate()` 方法来来创建无限流: -+ `Stream.iterate()` 接受两个参数:第一个是初始值,第二个参数是一个输入值和输出值相同的函数型接口。它主要用于迭代式的产生新的元素,示例如下: ++ `Stream.iterate()` 接受两个参数:第一个是初始值;第二个参数是一个输入值和输出值相同的函数型接口,主要用于迭代式地产生新的元素,示例如下: ```java - // 依次输出1到9 + // 依次输出0到9 Stream.iterate(0, x -> x + 1).limit(10).forEach(System.out::print); ``` @@ -258,26 +267,26 @@ try (Stream lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.U ### 4.1 基本操作 -当流创建后,便可以利用 Stream 类的各种方法对其上数据进行各种处理,常用的方法如下: +当流创建后,便可以利用 Stream 类上的各种方法对流中的数据进行处理,常用的方法如下: -| 操作 | 作用 | 返回类型 | 使用的类型/函数式接口 | -| --------- | ------------------------------ | ------------ | ---------------------- | -| filter | 过滤符合条件的元素 | Stream\ | Predicate\ | -| distinct | 过滤重复元素 | Stream\ | | -| skip | 跳过指定数量的元素 | Stream\ | long | -| limit | 限制元素的数量 | Stream\ | long | -| map | 对元素执行特定转换操作 | Stream\ | Function | -| flatMap | 将元素扁平化后执行特定转换操作 | Stream\ | Function> | -| sorted | 对元素进行排序 | Stream\ | Comparator\ | -| anyMatch | 是否存在指定元素满足特定条件 | boolean | Predicate\ | -| noneMatch | 是否所有元素都不满足特定条件 | boolean | Predicate\ | -| allMatch | 是否所有元素都满足特定条件 | boolean | Predicate\ | -| findAny | 返回任意一个满足指定条件的元素 | Optional\ | | -| findFirst | 返回第一个满足指定条件的元素 | Optional\ | | -| forEach | 对所有元素执行特定的操作 | void | Cosumer\ | -| collect | 对所有元素指定特定的收集操作 | R | Collector | -| reduce | 对元素依次执行归约操作 | Optional\ | BinaryOperator\ | -| count | 计算流中元素的数量 | long | | +| 操作 | 作用 | 返回类型 | 使用的类型/函数式接口 | +| --------- | ---------------------------------- | ------------ | ---------------------- | +| filter | 过滤符合条件的元素 | Stream\ | Predicate\ | +| distinct | 过滤重复元素 | Stream\ | | +| skip | 跳过指定数量的元素 | Stream\ | long | +| limit | 限制元素的数量 | Stream\ | long | +| map | 对元素执行特定转换操作 | Stream\ | Function | +| flatMap | 将元素扁平化后执行特定转换操作 | Stream\ | Function> | +| sorted | 对元素进行排序 | Stream\ | Comparator\ | +| anyMatch | 是否存在任意一个元素能满足指定条件 | boolean | Predicate\ | +| noneMatch | 是否所有元素都不满足指定条件 | boolean | Predicate\ | +| allMatch | 是否所有元素都满足指定条件 | boolean | Predicate\ | +| findAny | 返回任意一个满足指定条件的元素 | Optional\ | | +| findFirst | 返回第一个满足指定条件的元素 | Optional\ | | +| forEach | 对所有元素执行特定的操作 | void | Cosumer\ | +| collect | 使用收集器 | R | Collector | +| reduce | 执行归约操作 | Optional\ | BinaryOperator\ | +| count | 计算流中元素的数量 | long | | > 注:上表中返回类型为 Stream\ 的操作都是中间操作,代表还可以继续调用其它方法对流进行处理。返回类型为其它的操作都是终止操作,代表处理过程到此为止。 @@ -285,11 +294,11 @@ try (Stream lines = Files.lines(Paths.get("pom.xml"), StandardCharsets.U ```java Stream.iterate(0, x -> x + 1) // 构建流 - .limit(20) // 限制元素的个数 - .skip(10) // 跳过前10个元素 - .filter(x -> x % 2 == 0) // 过滤出所有偶数 - .map(x -> "偶数:" + x) // 对元素执行转换操作 - .forEach(System.out::println); // 打印出所有元素 + .limit(20) // 限制元素的个数 + .skip(10) // 跳过前10个元素 + .filter(x -> x % 2 == 0) // 过滤出所有偶数 + .map(x -> "偶数:" + x) // 对元素执行转换操作 + .forEach(System.out::println); // 打印出所有元素 ``` 输出结果如下: @@ -302,18 +311,18 @@ Stream.iterate(0, x -> x + 1) // 构建流 偶数:18 ``` - 上表的 `flatMap()` 方法接收一个参数,它是一个函数型接口 `Function> mapper`,该接口用于将流中的元素转换为 `Stream` ,从而可以将原有的元素进行扁平化: + 上表的 `flatMap()` 方法接收一个参数,该参数是一个函数型接口 `Function> 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); + .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` ,使用示例如下: +而上表的 `reduce()` 方法则接收两个参数:第一个参数表示执行归约操作的初始值;第二个参数是上文我们介绍过的函数式接口 `BinaryOperator` ,使用示例如下: ```java Stream.iterate(0, x -> x + 1).limit(10) @@ -322,7 +331,7 @@ Stream.iterate(0, x -> x + 1).limit(10) ### 4.2 数值流 -上面的代码等效于对 Stream 中的所有元素执行了求和操作,因此我们还可以调用简便方法 `sum()` 来进行实现,但是需要注意的是上面 `Stream.iterate()` 生成流中的元素类型都是包装类型: +上面的代码等效于对 Stream 中的所有元素执行了求和操作,因此我们还可以调用简便方法 `sum()` 来进行实现,但是需要注意的是 `Stream.iterate()` 生成流中的元素类型都是包装类型: ```java Stream stream = Stream.iterate(0, x -> x + 1); //包装类型Integer @@ -345,7 +354,7 @@ Stream boxed = intStream.boxed(); ## 五、收集器 -Stream 中最强大一个终止操作是 `collect()` ,它接收一个收集器 Collector 作为参数,可以将流中的元素收集到集合中,或进行分组、分区等操作。Java 中内置了多种收集器的实现,可以通过 Collectors 类的静态方法进行调用,常用的如下: +Stream 中最强大一个终止操作是 `collect()` ,它接收一个收集器 Collector 作为参数,可以将流中的元素收集到集合中,或进行分组、分区等操作。Java 中内置了多种收集器的实现,可以通过 Collectors 类的静态方法进行调用,常用的收集器如下: | 工厂方法 | 返回类型 | 用于 | | ----------------- | --------------------- | ------------------------------------------------------------ | @@ -355,12 +364,12 @@ Stream 中最强大一个终止操作是 `collect()` ,它接收一个收集器 | counting | Long | 计算流中所有元素的个数 | | summingInt | Integer | 将流中所有元素转换为整数,并计算其总和 | | averagingInt | Double | 将流中所有元素转换为整数,并计算其平均值 | -| summarizingInt | IntSummaryStatistics | 将流中所有元素转换为整数,并返回值统计值,包含最大值、最小值、
总和与平均值等信息 | +| summarizingInt | IntSummaryStatistics | 将流中所有元素转换为整数,并返回统计结果,包含最大值、最小值、
总和与平均值等信息 | | joining | String | 将流中所有元素转换为字符串,并使用给定连接符进行连接 | | maxBy | Optional\ | 查找流中最大元素的 Optional | | minBy | Optional\ | 查找流中最小元素的 Optional | | reducing | 规约操作产生的类型 | 对流中所有元素执行归约操作 | -| collectingAndThen | 转换返回的类型 | 把流中所有元素收集到指定的集合中,再对集合执行特定转换操作 | +| collectingAndThen | 转换返回的类型 | 先把流中所有元素收集到指定的集合中,再对集合执行特定的操作 | | groupingBy | Map> | 对流中所有元素执行分组操作 | | partitionBy | Map> | 对流中所有元素执行分区操作 | @@ -379,7 +388,7 @@ stream.collect(Collectors.reducing(1, (a, b) -> a * b)); // 等效于 stream.red 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 分组 @@ -387,22 +396,22 @@ collect(Collectors.collectingAndThen(Collectors.toSet(), Set::size)); // 先把 ```java Stream stream = Stream.of(new Employee("张某", "男", "A公司", 20), - new Employee("李某", "女", "A公司", 30), - new Employee("王某", "男", "B公司", 40), - new Employee("田某", "女", "B公司", 50)); + 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 + '\'' + '}'; - } + + private String name; + private String gender; + private String company; + private int age; + + @Override + public String toString() {return "Employee{" + "name='" + name + '\'' + '}'; + } } ``` @@ -435,7 +444,7 @@ stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.counting() ```java stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.groupingBy(Employee::getGender))); -对应的分组结果如下: +先按照公司分组,再按照性别分组,结果如下: { B公司={女=[Employee{name='田某'}], 男=[Employee{name='王某'}]}, A公司={女=[Employee{name='李某'}], 男=[Employee{name='张某'}]} @@ -446,13 +455,13 @@ stream.collect(Collectors.groupingBy(Employee::getCompany, Collectors.groupingBy ```java Map> collect = stream.collect(Collectors.groupingBy(employee -> { - if (employee.getAge() <= 30) { - return "青年员工"; - } else if (employee.getAge() < 50) { - return "中年员工"; - } else { - return "老年员工"; - } + if (employee.getAge() <= 30) { + return "青年员工"; + } else if (employee.getAge() < 50) { + return "中年员工"; + } else { + return "老年员工"; + } })); 对应的分组结果如下: @@ -465,7 +474,7 @@ Map> collect = stream.collect(Collectors.groupingBy(emplo ### 5.3 分区 -分区是分组的一种特殊情况,即将满足指定条件的分为一组,将不满足指定条件的分为另外一组,两者在使用上基本类似,示例如下: +分区是分组的一种特殊情况,即将满足指定条件的元素分为一组,将不满足指定条件的元素分为另一组,两者在使用上基本类似,示例如下: ```java stream.collect(Collectors.partitioningBy(x -> "A公司".equals(x.getCompany()))); @@ -489,7 +498,7 @@ stream.parallel(); 此时流中的所有元素会被均匀的分配到多个线程上进行处理。并行流内部使用的是 ForkJoinPool 线程池,它默认的线程数量就是处理器数量,可以通过 `Runtime.getRuntime().availableProcessors()` 来查看该值,通常不需要更改。 -同时当前也无法为某个具体的流指定线程数量,只能通过修改系统属性 `java.util.concurrent.ForkJoinPool.common.parallelism` 的值来改变线程池大小,进而改变所有并行流的线程大小,示例如下: +当前也没有办法为某个具体的流指定线程数量,只能通过修改系统属性 `java.util.concurrent.ForkJoinPool.common.parallelism` 的值来改变所有并行流使用的线程数量,示例如下: ```java 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 \ No newline at end of file diff --git a/notes/Redis_分布式锁原理.md b/notes/Redis_分布式锁原理.md index 03130a3..4484ead 100644 --- a/notes/Redis_分布式锁原理.md +++ b/notes/Redis_分布式锁原理.md @@ -1,10 +1,26 @@ # Redis 分布式锁 + + ## 一、实现原理 ### 1.1 基本原理 -JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源,JDK 原生的锁就无能为力。此时可以使用 Redis 或 Zookeeper 来实现分布式锁。 +JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源,JDK 原生的锁就无能为力了。此时可以使用 Redis 来实现分布式锁。 Redis 实现分布式锁的核心命令如下: @@ -12,7 +28,9 @@ Redis 实现分布式锁的核心命令如下: SETNX key value ``` -SETNX 命令的作用是如果指定的 key 不存在,则创建并将为其设置值,此时返回状态码为 1,否则为 0。如果状态码为 1,代表获得该锁;此时其他进程再次尝试创建时,都回返回 0 ,代表锁已经被占用。当获得锁的进程处理完成业务后,再通过 `del` 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。 +SETNX 命令的作用是:如果指定的 key 不存在,则创建并为其设置值,然后返回状态码 1;如果指定的 key 存在,则直接返回 0。如果返回值为 1,代表获得该锁;此时其他进程再次尝试创建时,由于 key 已经存在,则都会返回 0 ,代表锁已经被占用。 + +当获得锁的进程处理完成业务后,再通过 `del` 命令将该 key 删除,其他进程就可以再次竞争性地进行创建,获得该锁。 通常为了避免死锁,我们会为锁设置一个超时时间,在 Redis 中可以通过 `expire` 命令来进行实现: @@ -25,8 +43,8 @@ EXPIRE key seconds ```java Long result = jedis.setnx("lockKey", "lockValue"); if (result == 1) { - // 如果此处程序被异常终止(如直接kill -9进程),则设置超时的操作就无法进行,该锁就会出现死锁 - jedis.expire("lockKey", 3); + // 如果此处程序被异常终止(如直接kill -9进程),则设置超时的操作就无法进行,该锁就会出现死锁 + jedis.expire("lockKey", 3); } ``` @@ -44,8 +62,8 @@ SET key value [EX seconds|PX milliseconds] [NX|XX] [KEEPTTL] - **EX** :设置超时时间,单位是秒; - **PX** :设置超时时间,单位是毫秒; -- **NX** :当且仅当对应的 Key 不存在时候才进行设置; -- **XX**:当且仅当对应的 Key 不存在时候才进行设置。 +- **NX** :当且仅当对应的 Key 不存在时才进行设置; +- **XX**:当且仅当对应的 Key 存在时才进行设置。 这四个参数从 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)); ``` -此时一条命令就可以完成值和超时时间的设置,并且因为只有一条命令,因此其原子性也得到了保证。但因为引入了超时时间来避免死锁,同时也存在了两个其他问题: +此时一条命令就可以完成值和超时时间的设置,并且因为只有一条命令,因此其原子性也得到了保证。但因为引入了超时时间来避免死锁,同时也引出了其它两个问题: -![redis_分布式锁原理](../pictures/redis_分布式锁原理.png) +
-+ **问题一**:当业务处理的时间超过过期时间后,由于锁已经被释放,此时其他进程就可以获得该锁,这意味着同时有两个进程进入了临界区,此时分布式锁就失效了; -+ **问题二**:如上图所示,当进程 A 业务处理完成后,此时删除的是进程 B 的锁,进而导致分布式锁又一次失效,进程 B 和 进程 C 同时进入了临界区。 + ++ **问题一**:当业务处理的时间超过过期时间后(图中进程 A),由于锁已经被释放,此时其他进程就可以获得该锁(图中进程 B),这意味着有两个进程(A 和 B)同时进入了临界区,此时分布式锁就失效了; ++ **问题二**:如上图所示,当进程 A 业务处理完成后,此时删除的是进程 B 的锁,进而导致分布式锁又一次失效,让进程 B 和 进程 C 同时进入了临界区。 针对问题二,我们可以在创建锁时为其指定一个唯一的标识作为 Key 的 Value,这里假设我们采用 `UUID + 线程ID` 来作为唯一标识: @@ -87,15 +106,15 @@ jedis.eval(script, ); ``` -接着再看问题一,问题一最简单的解决方法是:你可以估计业务的最大处理时间,然后保证设置的过期时间大于最大处理时间。但是由于业务需要面临各种复杂的情况,因此可能无法保证业务每一次都能在规定的过期时间内处理完成,此时可以使用延长锁时效的策略。 +接着再看问题一,问题一最简单的解决方法是:你可以估计业务的最大处理时间,然后保证设置的过期时间大于最大处理时间。但是由于业务会面临各种复杂的情况,因此可能无法保证业务每一次都能在规定的过期时间内处理完成,此时可以使用延长锁时效的策略。 ### 1.3 延长锁时效 延长锁时效的方案如下:假设锁超时时间是 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 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为 10 秒,则访问每个 Redis 实例的超时时间可能在 5 到 50 毫秒之间,如果在这个时间内还没有建立通信,则尝试下一个实例; -+ 如果在至少 N/2+1 个实例上都成功创建了锁。并且 `当前时间 - 开始时间 < 锁的超时时间` ,则认为已经获取了锁,锁的有效时间等于 `超时时间 - 花费时间`(如果考虑到不同 Redis 实例所在的服务器存在时钟漂移,则还需要减去时钟漂移); ++ 接着采用和单机版相同的方式,依次尝试在每个实例上创建锁。为了避免客户端长时间与某个故障的 Redis 节点通讯而导致阻塞,这里采用快速轮询的方式:假设创建锁时设置的超时时间为 10 秒,则访问每个 Redis 实例的超时时间可能在 5 到 50 毫秒之间,如果在这个时间内还没有建立通信,则尝试连接下一个实例; ++ 如果在至少 N/2+1 个实例上都成功创建了锁。并且 `当前时间 - 开始时间 < 锁的超时时间` ,则认为已经获取了锁,锁的有效时间等于 `超时时间 - 花费时间`(如果考虑不同 Redis 实例所在服务器的时钟漂移,则还需要减去时钟漂移); + 如果少于 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 SET key 随机数A EX 3 NX #A客户端 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 持久化与高可用 -为了保证高可用,所有 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.创建锁实例 RLock lock = redissonClient.getLock("myLock"); try { - //3.尝试获取分布式锁,第一个参数为等待时间,第二个参数为锁过期时间 - boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS); - if (isLock) { - // 4.模拟业务处理 + //3.尝试获取分布式锁,第一个参数为等待时间,第二个参数为锁过期时间 + boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS); + if (isLock) { + // 4.模拟业务处理 System.out.println("处理业务逻辑"); - Thread.sleep(20 * 1000); - } + Thread.sleep(20 * 1000); + } } catch (Exception e) { - e.printStackTrace(); + e.printStackTrace(); } finally { - //5.释放锁 - lock.unlock(); + //5.释放锁 + lock.unlock(); } redissonClient.shutdown(); ``` 此时对应在 Redis 中的数据结构如下: -![redis_分布式锁_cli1](../pictures/redis_分布式锁_cli1.png) +
-可以看到 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 boolean isLock1 = lock.tryLock(0, 30, TimeUnit.SECONDS); @@ -198,9 +218,10 @@ boolean isLock2 = lock.tryLock(0, 30, TimeUnit.SECONDS); 此时对应的值就会变成 2,代表加了两次锁: -![redis_分布式锁_cli2](../pictures/redis_分布式锁_cli2.png) +
-当然和其他重入锁一样,需要保证加锁的次数和解锁的次数一样,才能完全解锁: + +当然和其他重入锁一样,需要保证解锁的次数和加锁的次数一样,才能完全解锁: ```java lock.unlock(); @@ -243,17 +264,17 @@ RLock lock03 = redissonClient03.getLock(lockName); 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); - } + boolean isLock = redLock.tryLock(10, 300, TimeUnit.SECONDS); + if (isLock) { + // 4.模拟业务处理 + System.out.println("处理业务逻辑"); + Thread.sleep(200 * 1000); + } } catch (Exception e) { - e.printStackTrace(); + e.printStackTrace(); } finally { - //5.释放锁 - redLock.unlock(); + //5.释放锁 + redLock.unlock(); } redissonClient01.shutdown(); @@ -263,7 +284,8 @@ redissonClient03.shutdown(); 此时每个 Redis 实例上锁的情况如下: -![redis_分布式锁_cli3](../pictures/redis_分布式锁_cli3.png) +
+ 可以看到每个实例上都获得了锁。 @@ -281,24 +303,24 @@ 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()); - } + //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(); + e.printStackTrace(); } finally { - //5.释放锁 - lock.unlock(); + //5.释放锁 + lock.unlock(); } redissonClient.shutdown(); ``` -这里我们通过 `config.setLockWatchdogTimeout(30 * 1000)` 将 lockWatchdogTimeout 的值设置为 30000 毫秒(默认值也是 30000 毫秒)。lockWatchdogTimeout 只会对那些没有设置锁超时时间的锁生效,所以我们这里调用的是两个参数的 `tryLock()` 方法: +首先 Redisson 的 WatchDog 机制只会对那些没有设置锁超时时间的锁生效,所以我们这里调用的是两个参数的 `tryLock()` 方法: ```java 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; ``` -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 机制对明确指定超时时间的锁不会生效。 diff --git a/notes/ZooKeeper_分布式锁原理.md b/notes/ZooKeeper_分布式锁原理.md index 87a1c8c..1940e31 100644 --- a/notes/ZooKeeper_分布式锁原理.md +++ b/notes/ZooKeeper_分布式锁原理.md @@ -1,43 +1,64 @@ # ZooKeeper 分布式锁原理 + + + ## 一、实现原理 -JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源,JDK 原生的锁就无能为力。此时可以使用 Redis 或 Zookeeper 来实现分布式锁。 +JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享资源,但如果想要在不同**进程**之间以互斥的方式来访问共享资源,JDK 原生的锁就无能为力了。此时可以使用 Zookeeper 来实现分布式锁。具体分为以下两种方案: ### 1.1 临时节点方案 -![zookeeper_分布式锁_临时节点方法](../pictures/zookeeper_分布式锁_临时节点方法.png) +
+ 临时节点方案的原理如下: -+ 让多个进程(或线程)竞争性地去创建同一个临时节点,由于 ZooKeeper 不允许存在两个完全相同节点,因此必然只有一个进程能够抢先够创建成功 ; -+ 假设进程 A 成功创建,则它获得了该分布式锁。此时其他进程需要在 parent_node 上注册监听,监听其下所有子节点的变化,并挂起当前线程; ++ 让多个进程(或线程)竞争性地去创建同一个临时节点,由于 ZooKeeper 不允许存在两个完全相同节点,因此必然只有一个进程能够抢先创建成功 ; ++ 假设是进程 A 成功创建了节点,则它获得该分布式锁。此时其他进程需要在 parent_node 上注册监听,监听其下所有子节点的变化,并挂起当前线程; + 当 parent_node 下有子节点发生变化时候,它会通知所有在其上注册了监听的进程。这些进程需要判断是否是对应的锁节点上的删除事件。如果是,则让挂起的线程继续执行,并尝试再次获取锁。 -这里之所以使用临时节点是为了避免死锁:进程 A 正常执行完业务逻辑后,会主动地去删除该节点,释放锁。但如果进程 A 意外宕机了,由于声明的是临时节点,因此该节点也会被移除,从而避免死锁。 +这里之所以使用临时节点是为了避免死锁:进程 A 正常执行完业务逻辑后,会主动地去删除该节点,释放锁。但如果进程 A 意外宕机了,由于声明的是临时节点,因此该节点也会被移除,进而避免死锁。 临时节点方案的实现比较简单,但是其缺点也比较明显: -+ **缺点一**:当 parent_node 下其他锁变动或者被删除时,进程 B,C,D 也会收到通知,但是显然它们并不关心其他锁的释放情况。如果 parent_node 下存在大量的锁,并且程序处于高并发状态下,则 ZooKeeper 集群就需要频繁地通知客户端进程,这会带来大量的网络开销; ++ **缺点一**:当 parent_node 下其他锁变动或者被删除时,进程 B,C,D 也会收到通知,但是显然它们并不关心其他锁的释放情况。如果 parent_node 下存在大量的锁,并且程序处于高并发状态下,则 ZooKeeper 集群就需要频繁地通知客户端,这会带来大量的网络开销; + **缺点二**:采用临时节点方案创建的锁是非公平的,也就是说在进程 A 释放锁后,进程 B,C,D 发起重试的顺序与其收到通知的时间有关,而与其第一次尝试获取锁的时间无关,即与等待时间的长短无关。 +当程序并发量不高时,可以采用该方案来实现,因为其实现比较简单。而如果程序并发量很高,则需要采用下面的临时有序节点方案: + ### 1.2 临时有序节点方案 -![zookeeper_分布式锁_临时有序节点方案](../pictures/zookeeper_分布式锁_临时有序节点方案.png) +
+ 采用临时有序节点时,对应的流程如下: + 每个进程(或线程)都会尝试在 parent_node 下创建临时有序节点,根据临时有序节点的特性,所有的进程都会创建成功; -+ 然后每个进程需要获取 parent_node 下该锁的所有临时节点的信息,并判断自己是否是最小的一个节点,如果是,则代表获得该锁; ++ 然后每个进程需要获取当前 parent_node 下该锁的所有临时节点的信息,并判断自己是否是最小的一个节点,如果是,则代表获得该锁; + 如果不是,则挂起当前线程。并对其前一个节点注册监听(这里可以通过 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) +
+ @@ -56,56 +78,57 @@ JDK 原生的锁可以让不同**线程**之间以互斥的方式来访问共享 ### 2.1 基本使用 - Apache Curator 是 ZooKeeper 的 Java 客户端,它基于临时有序节点方案实现了分布式锁、分布式读写锁等功能。基本使用如下: +Apache Curator 是 ZooKeeper 的 Java 客户端,它基于临时有序节点方案实现了分布式锁、分布式读写锁等功能。使用前需要先导入 Apache Curator 和 ZooKeeper 相关的依赖,并保证 ZooKeeper 版本与服务器上 ZooKeeper 的版本一致: + +```xml + + org.apache.curator + curator-framework + 4.3.0 + + + org.apache.curator + curator-recipes + 4.3.0 + + + org.apache.zookeeper + zookeeper + 3.4.14 + +``` + +基本使用如下: ```java RetryPolicy retryPolicy = new RetryNTimes(3, 5000); CuratorFramework client = CuratorFrameworkFactory.builder() - .connectString("192.168.0.105:2181") - .sessionTimeoutMs(10000).retryPolicy(retryPolicy) - .namespace("mySpace").build(); + .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(); - } + try { + System.out.println("模拟业务耗时"); + Thread.sleep(3 * 1000); + } finally { + // 3.释放锁 + lock.release(); + } } client.close(); ``` -这里需要事先导入 Apache Curator 和 ZooKeeper 相关的依赖,并保证 ZooKeeper 版本与服务器上 ZooKeeper 的版本一致: +之后就可以启动多个程序进程来进行测试,此时 ZooKeeper 上的数据结构如下: -```xml - - org.apache.curator - curator-framework - 4.3.0 - - - org.apache.curator - curator-recipes - 4.3.0 - - - org.apache.zookeeper - zookeeper - 3.4.14 - -``` +
-之后就可以启动多个程序进程来进程测试,ZooKeeper 上的数据结构如下: -![zookeeper_分布式锁_cli](../pictures/zookeeper_分布式锁_cli.png) - -在我们指定的路径下,会依次创建多个临时有序节点,而当业务逻辑处理完成后,这些节点就会被移除。这里我们使用的是单机版本的 ZooKeeper ,而集群环境下也是一样,和 Redis 主从模式下的延迟复制会导致数据不一致的情况不同,ZooKeeper 各个节点上的数据一致性可以由其自身来进行保证。 +在我们指定的路径下,会依次创建多个临时有序节点,而当业务逻辑处理完成后,这些节点就会被移除。这里我们使用的是单机版本的 ZooKeeper ,而集群环境下也是一样,和 Redis 主从模式下的延迟复制会导致数据不一致的情况不同,ZooKeeper 集群各个节点上的数据一致性可以由其自身来进行保证。 @@ -115,7 +138,7 @@ Apache Curator 底层采用的是临时有序节点的实现方案,下面我 #### 1. 获取锁源码解析 -上面最核心的获取锁的方法 `acquire()` ,其定义如下: +上面最核心的方法是获取锁的 `acquire()` 方法 ,其定义如下: ```java @Override @@ -124,7 +147,7 @@ public boolean acquire(long time, TimeUnit unit) throws Exception{ } ``` -它在内部调用了 `internalLock()` 方法: +可以看到,它在内部调用了 `internalLock()` 方法,internalLock 方法的源码如下: ```java // threadData是一个线程安全的Map,其中Thread是持有锁的线程,LockData是锁数据 @@ -151,7 +174,7 @@ private boolean internalLock(long time, TimeUnit unit) throws Exception{ } ``` -这里面真正去尝试创建锁的方法是 `attemptLock()`: +上面真正去尝试获取锁的方法是 `attemptLock()`: ```java 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 isDone = false; - // 当出现NoNodeException异常时候依靠该循环进行重试 + // 当出现NoNodeException异常时候依靠该循环进行重试 while ( !isDone ){ isDone = true; try{ @@ -184,7 +207,7 @@ String attemptLock(long time, TimeUnit unit, byte[] lockNodeBytes) throws Except } } - // 如果获取到锁,则跳出循环,并返回锁的路径 + // 如果获取到锁,则跳出循环,并返回锁的路径 if ( hasTheLock ){ return ourPath; } @@ -212,71 +235,71 @@ public String createsTheLock(CuratorFramework client, String path, byte[] lockNo } ``` -这里创建好的临时节点的路径会作为参数传递给 `internalLockLoop()` 方法。在文章开头介绍原理时候,我们说过每个线程创建好临时有序节点后,还需要判断它所创建的临时有序节点是否是当前最小的节点,`internalLockLoop()` 方法主要做的就是这事: +这里返回的临时有序节点的路径会作为参数传递给 `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); - } + boolean doDelete = false; + try { + if (revocable.get() != null) { + client.getData().usingWatcher(revocableWatcher).forPath(ourPath); + } // 如果连接ZooKeeper客户端处于启动状态,也就是想要获取锁的进程仍然处于运行状态,并且还没有获取到锁,则循环继续 - while ((client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock) { + while ((client.getState() == CuratorFrameworkState.STARTED) && !haveTheLock) { // 对所当前所有的子节点按照从小到大进行排序 - List children = getSortedChildren(); + List children = getSortedChildren(); // 将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); - // 如果当前节点就是最小的一个节点(排他锁情况),则此时就获得了锁 - if (predicateResults.getsTheLock()) { - haveTheLock = true; - } else { + // 如果当前节点是最小的一个节点(排他锁情况),则此时就获得了锁 + if (predicateResults.getsTheLock()) { + haveTheLock = true; + } else { // 如果当前节点不是最小的一个节点,先拼接并获取前一个节点完整的路径 - String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch(); - synchronized (this) { - try { + String previousSequencePath = basePath + "/" + predicateResults.getPathToWatch(); + synchronized (this) { + try { // 然后对前一个节点进行监听 - client.getData().usingWatcher(watcher).forPath(previousSequencePath); + client.getData().usingWatcher(watcher).forPath(previousSequencePath); // 如果设置了等待时间 - if (millisToWait != null) { + if (millisToWait != null) { // 将等待时间减去到目前为止所耗费的时间 - millisToWait -= (System.currentTimeMillis() - startMillis); - startMillis = System.currentTimeMillis(); + millisToWait -= (System.currentTimeMillis() - startMillis); + startMillis = System.currentTimeMillis(); // 如果等待时间小于0,则说明我们耗费的时间已经超过了等待时间,此时获取的锁无效,需要删除它 - if (millisToWait <= 0) { + if (millisToWait <= 0) { //设置删除标志位,并退出循环 - doDelete = true; - break; - } - // 如果还有剩余时间,则等待获取锁 - wait(millisToWait); - } else { + doDelete = true; + break; + } + // 如果还有剩余时间,则在剩余时间内继续等待获取锁 + wait(millisToWait); + } else { // 如果没有设置等待时间,则持续等待获取锁 - wait(); - } - } catch (KeeperException.NoNodeException e) { - // 这个异常抛出时,代表对前一个节点设置监听时,前一个节点已经不存在(被释放),此时捕获该异常, + wait(); + } + } catch (KeeperException.NoNodeException e) { + // 这个异常抛出时,代表对前一个节点设置监听时,前一个节点已经不存在(被释放),此时捕获该异常, // 但不需要进行任何额外操作,因为循环会继续,就可以再次尝试获取锁 - } - } - } - } - } catch (Exception e) { - ThreadUtils.checkInterrupted(e); - doDelete = true; - throw e; - } finally { - // 如果抛出了异常或者超时了,都删除掉上一个方法createsTheLock创建的临时有序节点,以便后面的进程进行锁的获取 - if (doDelete) { - deleteOurPath(ourPath); - } - } - return haveTheLock; + } + } + } + } + } catch (Exception e) { + ThreadUtils.checkInterrupted(e); + doDelete = true; + throw e; + } finally { + // 如果抛出了异常或者超时,则代表该进程创建的锁无效,需要将已创建的锁删除。以便后面的进程继续尝试创建锁 + if (doDelete) { + deleteOurPath(ourPath); + } + } + return haveTheLock; } ``` @@ -286,24 +309,24 @@ private boolean internalLockLoop ( long startMillis, Long millisToWait, String o PredicateResults predicateResults = driver.getsTheLock(client, children, sequenceNodeName, maxLeases); ``` -和上文介绍的一样,判断当前节点是否是持有锁的节点,在不同锁类型(如读写锁和互斥锁)的判断是不同的,因此 getsTheLock 方法有着不同的实现。这里以StandardLockInternalsDriver 为例,它使用的是互斥锁的判断规则:只要当前节点是最小的一个节点,就能持有锁: +和上文介绍的一样,判断当前节点是否是持有锁的节点,在不同锁类型(如读写锁和互斥锁)的判断是不同的,因此 getsTheLock 方法有着不同的实现。这里以StandardLockInternalsDriver 为例,它使用的是互斥锁的判断规则:即只要当前节点是最小的一个节点,就能持有锁: ```java public PredicateResults getsTheLock(CuratorFramework client, List children, String sequenceNodeName, int maxLeases) throws Exception { - // 获取当前节点在已经排好序的节点中的下标index + // 获取当前节点在已经排好序的节点中的下标index int ourIndex = children.indexOf(sequenceNodeName); - // 如果ourIndexx小于0,则抛出NoNodeException的异常 + // 如果ourIndex小于0,则抛出NoNodeException的异常 validateOurIndex(sequenceNodeName, ourIndex); - // 如果ourIndex小于maxLeases(默认值是1),则代表它就是0,也就是从小到大排好序的集合中的第一个,也就是最小的一个 + // 如果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(信号量)机制 。 +这里解释一下 maxLease 这个参数的意义:默认值为 1,就是互斥锁;如果默认值大于 1,假设 maxLease 的值是 5,则最小的 5 个临时有序节点都可以认为是能持有锁的节点,此时最多可以有 5 个线程并发访问临界区, 在功能上类似于 Java 中Semaphore(信号量)机制 。 @@ -313,29 +336,29 @@ PredicateResults predicateResults = driver.getsTheLock(client, children, sequenc ```java public void release() throws Exception { - Thread currentThread = Thread.currentThread(); + Thread currentThread = Thread.currentThread(); // 根据当前线程来获取锁信息 - InterProcessMutex.LockData lockData = threadData.get(currentThread); - // 如果获取不到,则当前线程不是锁的持有者,此时抛出异常 - if (lockData == null) { - throw new IllegalMonitorStateException("You do not own the lock: " + basePath); - } + 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; - } + 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 { + 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); - } + threadData.remove(currentThread); + } } ``` diff --git a/pictures/cap_示例.png b/pictures/cap_示例.png new file mode 100644 index 0000000000000000000000000000000000000000..33773e13bed88041ed8df5f701a2de9172b1e06f GIT binary patch literal 8711 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 Sru0C5gA`t>LQ0>T2L3N;V92%r literal 0 HcmV?d00001 diff --git a/pictures/cap理论.jpg b/pictures/cap理论.jpg new file mode 100644 index 0000000000000000000000000000000000000000..068f6008b8b411d3a29f00b3cb5821e53d988657 GIT binary patch literal 36545 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 bzu`~)kBI;2f&b}&|LKAMA9~=UH$(pmr`Bd+ literal 0 HcmV?d00001