diff --git a/README.md b/README.md index 6853cf5586..e1f0a29c59 100644 --- a/README.md +++ b/README.md @@ -33,83 +33,71 @@ ## Java -> [Java 面试总结](source/_posts/01.Java/01.JavaSE/99.Java面试.md) 💯 - -### JavaSE - -#### [Java 基础特性](source/_posts/01.Java/01.JavaSE/01.基础特性) - -- [Java 开发环境](source/_posts/01.Java/01.JavaSE/01.基础特性/00.Java开发环境.md) -- [Java 基础语法特性](source/_posts/01.Java/01.JavaSE/01.基础特性/01.Java基础语法.md) -- [Java 基本数据类型](source/_posts/01.Java/01.JavaSE/01.基础特性/02.Java基本数据类型.md) -- [Java 面向对象](source/_posts/01.Java/01.JavaSE/01.基础特性/03.Java面向对象.md) -- [Java 方法](source/_posts/01.Java/01.JavaSE/01.基础特性/04.Java方法.md) -- [Java 数组](source/_posts/01.Java/01.JavaSE/01.基础特性/05.Java数组.md) -- [Java 枚举](source/_posts/01.Java/01.JavaSE/01.基础特性/06.Java枚举.md) -- [Java 控制语句](source/_posts/01.Java/01.JavaSE/01.基础特性/07.Java控制语句.md) -- [Java 异常](source/_posts/01.Java/01.JavaSE/01.基础特性/08.Java异常.md) -- [Java 泛型](source/_posts/01.Java/01.JavaSE/01.基础特性/09.Java泛型.md) -- [Java 反射](source/_posts/01.Java/01.JavaSE/01.基础特性/10.Java反射.md) -- [Java 注解](source/_posts/01.Java/01.JavaSE/01.基础特性/11.Java注解.md) -- [Java String 类型](source/_posts/01.Java/01.JavaSE/01.基础特性/42.JavaString类型.md) - -#### [Java 高级特性](source/_posts/01.Java/01.JavaSE/02.高级特性) - -- [Java 正则从入门到精通](source/_posts/01.Java/01.JavaSE/02.高级特性/01.Java正则.md) - 关键词:`Pattern`、`Matcher`、`捕获与非捕获`、`反向引用`、`零宽断言`、`贪婪与懒惰`、`元字符`、`DFA`、`NFA` -- [Java 编码和加密](source/_posts/01.Java/01.JavaSE/02.高级特性/02.Java编码和加密.md) - 关键词:`Base64`、`消息摘要`、`数字签名`、`对称加密`、`非对称加密`、`MD5`、`SHA`、`HMAC`、`AES`、`DES`、`DESede`、`RSA` -- [Java 国际化](source/_posts/01.Java/01.JavaSE/02.高级特性/03.Java国际化.md) - 关键词:`Locale`、`ResourceBundle`、`NumberFormat`、`DateFormat`、`MessageFormat` -- [Java JDK8](source/_posts/01.Java/01.JavaSE/02.高级特性/04.JDK8.md) - 关键词:`Stream`、`lambda`、`Optional`、`@FunctionalInterface` -- [Java SPI](source/_posts/01.Java/01.JavaSE/02.高级特性/05.JavaSPI.md) - 关键词:`SPI`、`ClassLoader` - -#### [Java 容器](source/_posts/01.Java/01.JavaSE/03.容器) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221175550.png) - -- [Java 容器简介](source/_posts/01.Java/01.JavaSE/03.容器/01.Java容器简介.md) - 关键词:`Collection`、`泛型`、`Iterable`、`Iterator`、`Comparable`、`Comparator`、`Cloneable`、`fail-fast` -- [Java 容器之 List](source/_posts/01.Java/01.JavaSE/03.容器/02.Java容器之List.md) - 关键词:`List`、`ArrayList`、`LinkedList` -- [Java 容器之 Map](source/_posts/01.Java/01.JavaSE/03.容器/03.Java容器之Map.md) - 关键词:`Map`、`HashMap`、`TreeMap`、`LinkedHashMap`、`WeakHashMap` -- [Java 容器之 Set](source/_posts/01.Java/01.JavaSE/03.容器/04.Java容器之Set.md) - 关键词:`Set`、`HashSet`、`TreeSet`、`LinkedHashSet`、`EmumSet` -- [Java 容器之 Queue](source/_posts/01.Java/01.JavaSE/03.容器/05.Java容器之Queue.md) - 关键词:`Queue`、`Deque`、`ArrayDeque`、`LinkedList`、`PriorityQueue` -- [Java 容器之 Stream](source/_posts/01.Java/01.JavaSE/03.容器/06.Java容器之Stream.md) - -#### [Java IO](source/_posts/01.Java/01.JavaSE/04.IO) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630205329.png) - -- [Java IO 模型](source/_posts/01.Java/01.JavaSE/04.IO/01.JavaIO模型.md) - 关键词:`InputStream`、`OutputStream`、`Reader`、`Writer`、`阻塞` -- [Java NIO](source/_posts/01.Java/01.JavaSE/04.IO/02.JavaNIO.md) - 关键词:`Channel`、`Buffer`、`Selector`、`非阻塞`、`多路复用` -- [Java 序列化](source/_posts/01.Java/01.JavaSE/04.IO/03.Java序列化.md) - 关键词:`Serializable`、`serialVersionUID`、`transient`、`Externalizable`、`writeObject`、`readObject` -- [Java 网络编程](source/_posts/01.Java/01.JavaSE/04.IO/04.Java网络编程.md) - 关键词:`Socket`、`ServerSocket`、`DatagramPacket`、`DatagramSocket` -- [Java IO 工具类](source/_posts/01.Java/01.JavaSE/04.IO/05.JavaIO工具类.md) - 关键词:`File`、`RandomAccessFile`、`System`、`Scanner` - -#### [Java 并发](source/_posts/01.Java/01.JavaSE/05.并发) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221175827.png) - -- [Java 并发简介](source/_posts/01.Java/01.JavaSE/05.并发/01.Java并发简介.md) - 关键词:`进程`、`线程`、`安全性`、`活跃性`、`性能`、`死锁`、`饥饿`、`上下文切换` -- [Java 线程基础](source/_posts/01.Java/01.JavaSE/05.并发/02.Java线程基础.md) - 关键词:`Thread`、`Runnable`、`Callable`、`Future`、`wait`、`notify`、`notifyAll`、`join`、`sleep`、`yeild`、`线程状态`、`线程通信` -- [Java 并发核心机制](source/_posts/01.Java/01.JavaSE/05.并发/03.Java并发核心机制.md) - 关键词:`synchronized`、`volatile`、`CAS`、`ThreadLocal` -- [Java 并发锁](source/_posts/01.Java/01.JavaSE/05.并发/04.Java锁.md) - 关键词:`AQS`、`ReentrantLock`、`ReentrantReadWriteLock`、`Condition` -- [Java 原子类](source/_posts/01.Java/01.JavaSE/05.并发/05.Java原子类.md) - 关键词:`CAS`、`Atomic` -- [Java 并发容器](source/_posts/01.Java/01.JavaSE/05.并发/06.Java并发和容器.md) - 关键词:`ConcurrentHashMap`、`CopyOnWriteArrayList` -- [Java 线程池](source/_posts/01.Java/01.JavaSE/05.并发/07.Java线程池.md) - 关键词:`Executor`、`ExecutorService`、`ThreadPoolExecutor`、`Executors` -- [Java 并发工具类](source/_posts/01.Java/01.JavaSE/05.并发/08.Java并发工具类.md) - 关键词:`CountDownLatch`、`CyclicBarrier`、`Semaphore` -- [Java 内存模型](source/_posts/01.Java/01.JavaSE/05.并发/09.Java内存模型.md) - 关键词:`JMM`、`volatile`、`synchronized`、`final`、`Happens-Before`、`内存屏障` -- [ForkJoin 框架](source/_posts/01.Java/01.JavaSE/05.并发/10.ForkJoin框架.md) - -#### [Java 虚拟机](source/_posts/01.Java/01.JavaSE/06.JVM) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200628154803.png) - -- [JVM 体系结构](source/_posts/01.Java/01.JavaSE/06.JVM/01.JVM体系结构.md) -- [JVM 内存区域](source/_posts/01.Java/01.JavaSE/06.JVM/02.JVM内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` -- [JVM 垃圾收集](source/_posts/01.Java/01.JavaSE/06.JVM/03.JVM垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` -- [JVM 字节码](source/_posts/01.Java/01.JavaSE/06.JVM/05.JVM字节码.md) - 关键词:`bytecode`、`asm`、`javassist` -- [JVM 类加载](source/_posts/01.Java/01.JavaSE/06.JVM/04.JVM类加载.md) - 关键词:`ClassLoader`、`双亲委派` -- [JVM 命令行工具](source/_posts/01.Java/01.JavaSE/06.JVM/11.JVM命令行工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo` -- [JVM GUI 工具](source/_posts/01.Java/01.JavaSE/06.JVM/12.JVM_GUI工具.md) - 关键词:`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` -- [JVM 实战](source/_posts/01.Java/01.JavaSE/06.JVM/21.JVM实战.md) - 关键词:`配置`、`调优` -- [Java 故障诊断](source/_posts/01.Java/01.JavaSE/06.JVM/22.Java故障诊断.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` +### JavaCore + +#### [Java 基础特性](source/_posts/01.Java/01.JavaCore/01.基础特性) + +- [Java 基础语法特性](source/_posts/01.Java/01.JavaCore/01.基础特性/Java基础语法.md) +- [Java 基本数据类型](source/_posts/01.Java/01.JavaCore/01.基础特性/Java基本数据类型.md) +- [Java 面向对象](source/_posts/01.Java/01.JavaCore/01.基础特性/Java面向对象.md) +- [Java 方法](source/_posts/01.Java/01.JavaCore/01.基础特性/Java方法.md) +- [Java 数组](source/_posts/01.Java/01.JavaCore/01.基础特性/Java数组.md) +- [Java 枚举](source/_posts/01.Java/01.JavaCore/01.基础特性/Java枚举.md) +- [Java 控制语句](source/_posts/01.Java/01.JavaCore/01.基础特性/Java控制语句.md) +- [Java 异常](source/_posts/01.Java/01.JavaCore/01.基础特性/Java异常.md) +- [Java 泛型](source/_posts/01.Java/01.JavaCore/01.基础特性/Java泛型.md) +- [Java 反射](source/_posts/01.Java/01.JavaCore/01.基础特性/Java反射.md) +- [Java 注解](source/_posts/01.Java/01.JavaCore/01.基础特性/Java注解.md) +- [Java String 类型](source/_posts/01.Java/01.JavaCore/01.基础特性/JavaString类型.md) + +#### [Java 高级特性](source/_posts/01.Java/01.JavaCore/02.高级特性) + +- [Java 正则](source/_posts/01.Java/01.JavaCore/02.高级特性/Java正则.md) - 关键词:Pattern、Matcher、捕获与非捕获、反向引用、零宽断言、贪婪与懒惰、元字符、DFA、NFA +- [Java 编码和加密](source/_posts/01.Java/01.JavaCore/02.高级特性/Java编码和加密.md) - 关键词:Base64、消息摘要、数字签名、对称加密、非对称加密、MD5、SHA、HMAC、AES、DES、DESede、RSA +- [Java 国际化](source/_posts/01.Java/01.JavaCore/02.高级特性/Java国际化.md) - 关键词:Locale、ResourceBundle、NumberFormat、DateFormat、MessageFormat +- [Java JDK8](source/_posts/01.Java/01.JavaCore/02.高级特性/JDK8特性.md) - 关键词:Stream、lambda、Optional、@FunctionalInterface +- [Java SPI](source/_posts/01.Java/01.JavaCore/02.高级特性/JavaSPI.md) - 关键词:SPI、ClassLoader +- [JavaAgent](source/_posts/01.Java/01.JavaCore/02.高级特性/JavaAgent.md) + +#### [Java 容器](source/_posts/01.Java/01.JavaCore/03.容器) + +- [Java 容器简介](source/_posts/01.Java/01.JavaCore/03.容器/Java容器简介.md) - 关键词:泛型、Iterable、Iterator、Comparable、Comparator、Cloneable、fail-fast +- [Java 容器之 List](source/_posts/01.Java/01.JavaCore/03.容器/Java容器之List.md) - 关键词:List、ArrayList、LinkedList +- [Java 容器之 Map](source/_posts/01.Java/01.JavaCore/03.容器/Java容器之Map.md) - 关键词:Map、HashMap、TreeMap、LinkedHashMap、WeakHashMap +- [Java 容器之 Set](source/_posts/01.Java/01.JavaCore/03.容器/Java容器之Set.md) - 关键词:Set、HashSet、TreeSet、LinkedHashSet、EmumSet +- [Java 容器之 Queue](source/_posts/01.Java/01.JavaCore/03.容器/Java容器之Queue.md) - 关键词:Queue、Deque、ArrayDeque、LinkedList、PriorityQueue +- [Java 容器之 Stream](source/_posts/01.Java/01.JavaCore/03.容器/Java容器之Stream.md) + +#### [Java IO](source/_posts/01.Java/01.JavaCore/04.IO) + +- [Java I/O 之 简介](source/_posts/01.Java/01.JavaCore/04.IO/JavaIO简介.md) - 关键词:BIO、NIO、AIO +- [Java I/O 之 BIO](source/_posts/01.Java/01.JavaCore/04.IO/JavaIO之BIO.md) - 关键词:BIO、InputStream、OutputStream、Reader、Writer、File、Socket、ServerSocket +- [Java I/O 之 NIO](source/_posts/01.Java/01.JavaCore/04.IO/JavaIO之NIO.md) - 关键词:NIO、Channel、Buffer、Selector、多路复用 +- [Java I/O 之序列化](source/_posts/01.Java/01.JavaCore/04.IO/JavaIO之序列化.md) - 关键词:Serializable、serialVersionUID、transient、Externalizable + +#### [Java 并发](source/_posts/01.Java/01.JavaCore/05.并发) + +- [Java 并发简介](source/_posts/01.Java/01.JavaCore/05.并发/Java并发简介.md) - 关键词:并发、线程、安全性、活跃性、性能、死锁、活锁 +- [Java 并发之内存模型](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之内存模型.md) - 关键词:JMM、Happens-Before、内存屏障、volatile、synchronized、final、指令重排序 +- [Java 并发之线程](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之线程.md) - 关键词:Thread、Runnable、Callable、Future、FutureTask、线程生命周期 +- [Java 并发之锁](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之锁.md) - 关键词:锁、Lock、Condition、ReentrantLock、ReentrantReadWriteLock、StampedLock +- [Java 并发之无锁](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之无锁.md) - 关键词:CAS、ThreadLocal、Immutability、Copy-on-Write +- [Java 并发之 AQS](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之AQS.md) - 关键词:AQS、独占锁、共享锁 +- [Java 并发之容器](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之容器.md) - 关键词:ConcurrentHashMap、CopyOnWriteArrayList +- [Java 并发之线程池](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之线程池.md) - 关键词:Executor、ExecutorService、ThreadPoolExecutor、Executors +- [Java 并发之同步工具](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之同步工具.md) - 关键词:Semaphore、CountDownLatch、CyclicBarrier +- [Java 并发之分工工具](source/_posts/01.Java/01.JavaCore/05.并发/Java并发之分工工具.md) - 关键词:CompletableFuture、CompletionStage、ForkJoinPool + +#### [Java 虚拟机](source/_posts/01.Java/01.JavaCore/06.JVM) + +- [Java 虚拟机简介](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机简介.md) +- [Java 虚拟机之内存区域](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机之内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` +- [Java 虚拟机之垃圾收集](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机之垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` +- [Java 虚拟机之字节码](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机之字节码.md) - 关键词:`bytecode`、`asm`、`javassist` +- [Java 虚拟机之类加载](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机之类加载.md) - 关键词:`ClassLoader`、`双亲委派` +- [Java 虚拟机之工具](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机之工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo`、`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` +- [Java 虚拟机之故障处理](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机之故障处理.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` +- [Java 虚拟机之调优](source/_posts/01.Java/01.JavaCore/06.JVM/Java虚拟机之调优.md) - 关键词:`配置`、`调优` ### JavaEE @@ -151,7 +139,7 @@ - [Maven 实战问题和最佳实践](source/_posts/01.Java/11.软件/01.构建/01.Maven/04.Maven实战问题和最佳实践.md) - [Maven 教程之发布 jar 到私服或中央仓库](source/_posts/01.Java/11.软件/01.构建/01.Maven/05.Maven教程之发布jar到私服或中央仓库.md) - [Maven 插件之代码检查](source/_posts/01.Java/11.软件/01.构建/01.Maven/06.Maven插件之代码检查.md) -- [Ant 简易教程](source/_posts/01.Java/11.软件/01.构建/03.Ant.md) +- [Ant 简易教程](source/_posts/01.Java/11.软件/01.构建/02.Ant.md) #### Java IDE @@ -259,7 +247,12 @@ ##### Web -- [Spring WebMvc](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/01.SpringWebMvc.md) +- [SpringWeb 综述](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/01.SpringWeb综述.md) +- [SpringWeb 应用](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/02.SpringWeb应用.md) +- [DispatcherServlet](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/03.DispatcherServlet.md) +- [Spring 过滤器](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/04.Spring过滤器.md) +- [Spring 跨域](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/05.Spring跨域.md) +- [Spring 视图](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/06.Spring视图.md) - [SpringBoot 之应用 EasyUI](source/_posts/01.Java/13.框架/01.Spring/03.SpringWeb/21.SpringBoot之应用EasyUI.md) ##### IO @@ -308,9 +301,7 @@ > > 如果想深入学习缓存,建议先了解一下 [缓存基本原理](https://dunwu.github.io/design/distributed/分布式缓存.html),有助于理解缓存的特性、原理,使用缓存常见的问题及解决方案。 -- [缓存面试题](source/_posts/01.Java/14.中间件/02.缓存/01.缓存面试题.md) - [Java 缓存中间件](source/_posts/01.Java/14.中间件/02.缓存/02.Java缓存中间件.md) -- [Memcached 快速入门](source/_posts/01.Java/14.中间件/02.缓存/03.Memcached.md) - [Ehcache 快速入门](source/_posts/01.Java/14.中间件/02.缓存/04.Ehcache.md) - [Java 进程内缓存](source/_posts/01.Java/14.中间件/02.缓存/05.Java进程内缓存.md) - [Http 缓存](source/_posts/01.Java/14.中间件/02.缓存/06.Http缓存.md) @@ -319,9 +310,7 @@ - [Hystrix](source/_posts/01.Java/14.中间件/03.流量控制/01.Hystrix.md) -## 计算机科学 - -### 数据结构和算法 +## 数据结构和算法 - **综合** - [数据结构和算法指南](source/_posts/11.数据结构和算法/00.综合/01.数据结构和算法指南.md) @@ -342,54 +331,41 @@ - [跳表](source/_posts/11.数据结构和算法/04.跳表.md) - 关键词:**`多级索引`** - [图](source/_posts/11.数据结构和算法/05.图.md) -### 数据库 +## 数据库 -#### 数据库综合 +### 数据库综合 - [Nosql 技术选型](source/_posts/12.数据库/01.数据库综合/01.Nosql技术选型.md) - [数据结构与数据库索引](source/_posts/12.数据库/01.数据库综合/02.数据结构与数据库索引.md) -#### 数据库中间件 +### 数据库中间件 - [ShardingSphere 简介](source/_posts/12.数据库/02.数据库中间件/01.Shardingsphere/01.ShardingSphere简介.md) - [ShardingSphere Jdbc](source/_posts/12.数据库/02.数据库中间件/01.Shardingsphere/02.ShardingSphereJdbc.md) - [版本管理中间件 Flyway](source/_posts/12.数据库/02.数据库中间件/02.Flyway.md) -#### 关系型数据库 +### 关系型数据库 > [关系型数据库](source/_posts/12.数据库/03.关系型数据库) 整理主流关系型数据库知识点。 -##### 公共知识 - -- [关系型数据库面试总结](source/_posts/12.数据库/03.关系型数据库/01.综合/01.关系型数据库面试.md) 💯 -- [SQL 语法基础特性](source/_posts/12.数据库/03.关系型数据库/01.综合/02.SQL语法基础特性.md) -- [SQL 语法高级特性](source/_posts/12.数据库/03.关系型数据库/01.综合/03.SQL语法高级特性.md) -- [扩展 SQL](source/_posts/12.数据库/03.关系型数据库/01.综合/03.扩展SQL.md) -- [SQL Cheat Sheet](source/_posts/12.数据库/03.关系型数据库/01.综合/99.SqlCheatSheet.md) +#### 公共知识 -##### Mysql +- [关系数据库简介](source/_posts/12.数据库/03.关系型数据库/01.综合/关系数据库简介.md) +- [SQL 语法](source/_posts/12.数据库/03.关系型数据库/01.综合/SQL语法.md) -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200716103611.png) +#### [Mysql 教程](source/_posts/12.数据库/03.关系型数据库/02.Mysql) -- [Mysql 应用指南](source/_posts/12.数据库/03.关系型数据库/02.Mysql/01.Mysql应用指南.md) ⚡ -- [Mysql 工作流](source/_posts/12.数据库/03.关系型数据库/02.Mysql/02.MySQL工作流.md) - 关键词:`连接`、`缓存`、`语法分析`、`优化`、`执行引擎`、`redo log`、`bin log`、`两阶段提交` -- [Mysql 事务](source/_posts/12.数据库/03.关系型数据库/02.Mysql/03.Mysql事务.md) - 关键词:`ACID`、`AUTOCOMMIT`、`事务隔离级别`、`死锁`、`分布式事务` -- [Mysql 锁](source/_posts/12.数据库/03.关系型数据库/02.Mysql/04.Mysql锁.md) - 关键词:`乐观锁`、`表级锁`、`行级锁`、`意向锁`、`MVCC`、`Next-key 锁` -- [Mysql 索引](source/_posts/12.数据库/03.关系型数据库/02.Mysql/05.Mysql索引.md) - 关键词:`Hash`、`B 树`、`聚簇索引`、`回表` -- [Mysql 性能优化](source/_posts/12.数据库/03.关系型数据库/02.Mysql/06.Mysql性能优化.md) -- [Mysql 运维](source/_posts/12.数据库/03.关系型数据库/02.Mysql/20.Mysql运维.md) 🔨 -- [Mysql 配置](source/_posts/12.数据库/03.关系型数据库/02.Mysql/21.Mysql配置.md) 🔨 -- [Mysql 问题](source/_posts/12.数据库/03.关系型数据库/02.Mysql/99.Mysql常见问题.md) +[Mysql 架构](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_架构)、[Mysql 存储引擎](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_存储引擎)、[Mysql 索引](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_索引)、[Mysql 事务](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_事务)、[Mysql 锁](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_锁)、[Mysql 高可用](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_高可用)、[Mysql 优化](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_优化)、[Mysql 运维](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_运维)、[Mysql 面试](source/_posts/12.数据库/03.关系型数据库/02.Mysql/Mysql_面试) -##### 其他 +#### 其他 - [PostgreSQL 应用指南](source/_posts/12.数据库/03.关系型数据库/99.其他/01.PostgreSQL.md) - [H2 应用指南](source/_posts/12.数据库/03.关系型数据库/99.其他/02.H2.md) - [SqLite 应用指南](source/_posts/12.数据库/03.关系型数据库/99.其他/03.Sqlite.md) -#### 文档数据库 +### 文档数据库 -##### MongoDB +#### MongoDB > MongoDB 是一个基于文档的分布式数据库,由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。 > @@ -397,36 +373,42 @@ > > MongoDB 最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。 -- [MongoDB 应用指南](source/_posts/12.数据库/04.文档数据库/01.MongoDB/01.MongoDB应用指南.md) -- [MongoDB 的 CRUD 操作](source/_posts/12.数据库/04.文档数据库/01.MongoDB/02.MongoDB的CRUD操作.md) -- [MongoDB 聚合操作](source/_posts/12.数据库/04.文档数据库/01.MongoDB/03.MongoDB的聚合操作.md) -- [MongoDB 事务](source/_posts/12.数据库/04.文档数据库/01.MongoDB/04.MongoDB事务.md) -- [MongoDB 建模](source/_posts/12.数据库/04.文档数据库/01.MongoDB/05.MongoDB建模.md) -- [MongoDB 建模示例](source/_posts/12.数据库/04.文档数据库/01.MongoDB/06.MongoDB建模示例.md) -- [MongoDB 索引](source/_posts/12.数据库/04.文档数据库/01.MongoDB/07.MongoDB索引.md) -- [MongoDB 复制](source/_posts/12.数据库/04.文档数据库/01.MongoDB/08.MongoDB复制.md) -- [MongoDB 分片](source/_posts/12.数据库/04.文档数据库/01.MongoDB/09.MongoDB分片.md) -- [MongoDB 运维](source/_posts/12.数据库/04.文档数据库/01.MongoDB/20.MongoDB运维.md) - -#### KV 数据库 - -##### Redis - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200713105627.png) - -- [Redis 面试总结](source/_posts/12.数据库/05.KV数据库/01.Redis/01.Redis面试总结.md) 💯 -- [Redis 应用指南](source/_posts/12.数据库/05.KV数据库/01.Redis/02.Redis应用指南.md) ⚡ - 关键词:`内存淘汰`、`事件`、`事务`、`管道`、`发布与订阅` -- [Redis 数据类型和应用](source/_posts/12.数据库/05.KV数据库/01.Redis/03.Redis数据类型和应用.md) - 关键词:`STRING`、`HASH`、`LIST`、`SET`、`ZSET`、`BitMap`、`HyperLogLog`、`Geo` -- [Redis 持久化](source/_posts/12.数据库/05.KV数据库/01.Redis/04.Redis持久化.md) - 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync` -- [Redis 复制](source/_posts/12.数据库/05.KV数据库/01.Redis/05.Redis复制.md) - 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`REPLCONF ACK` -- [Redis 哨兵](source/_posts/12.数据库/05.KV数据库/01.Redis/06.Redis哨兵.md) - 关键词:`Sentinel`、`PING`、`INFO`、`Raft` -- [Redis 集群](source/_posts/12.数据库/05.KV数据库/01.Redis/07.Redis集群.md) - 关键词:`CLUSTER MEET`、`Hash slot`、`MOVED`、`ASK`、`SLAVEOF no one`、`redis-trib` -- [Redis 实战](source/_posts/12.数据库/05.KV数据库/01.Redis/08.Redis实战.md) - 关键词:`缓存`、`分布式锁`、`布隆过滤器` -- [Redis 运维](source/_posts/12.数据库/05.KV数据库/01.Redis/20.Redis运维.md) 🔨 - 关键词:`安装`、`命令`、`集群`、`客户端` - -#### 列式数据库 - -##### HBase +- [MongoDB 简介](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_简介.md) +- [MongoDB CRUD](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_CRUD.md) +- [MongoDB 聚合](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_聚合.md) +- [MongoDB 事务](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_事务.md) +- [MongoDB 建模](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_建模.md) +- [MongoDB 索引](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_索引.md) +- [MongoDB 复制](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_复制.md) +- [MongoDB 分片](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_分片.md) +- [MongoDB 运维](source/_posts/12.数据库/04.文档数据库/01.MongoDB/MongoDB_运维.md) + +### KV 数据库 + +#### [Redis](source/_posts/12.数据库/05.KV数据库/01.Redis) + +- [Redis 基本数据类型](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_数据类型.md) - 关键词:`String`、`Hash`、`List`、`Set`、`Zset` +- [Redis 高级数据类型](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_数据类型二.md) - 关键词:`BitMap`、`HyperLogLog`、`Geo`、`Stream` +- [Redis 数据结构](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_数据结构.md) - 关键词:`对象`、`SDS`、`链表`、`字典`、`跳表`、`整数集合`、`压缩列表` +- [Redis 内存管理](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_内存管理.md) - 关键词:`定时删除`、`惰性删除`、`定期删除`、`LRU`、`LFU` +- [Redis 持久化](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_持久化.md) - 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync` +- [Redis 事件](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_事件.md) - 关键词:`文件事件`、`时间事件` +- [Redis 复制](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_复制.md) - 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`命令传播`、`心跳` +- [Redis 哨兵](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_哨兵.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`Raft` +- [Redis 集群](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_集群.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`分区`、`Raft`、`Gossip` +- [Redis 订阅](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_订阅.md) - 关键词:`订阅`、`SUBSCRIBE`、`PSUBSCRIBE`、`PUBLISH`、`观察者模式` +- [Redis 独立功能](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_事务.md) - 关键词:`事务`、`ACID`、`MULTI`、`EXEC`、`DISCARD`、`WATCH` +- [Redis 管道](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_管道.md) - 关键词:`Pipeline` +- [Redis 脚本](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_脚本.md) - 关键词:`Lua` +- [Redis 运维](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_运维.md) - 关键词:`安装`、`配置`、`命令`、`集群`、`客户端` +- [Redis 实战](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_实战.md) - 关键词:`缓存`、`分布式锁`、`布隆过滤器` +- [Redis 面试](source/_posts/12.数据库/05.KV数据库/01.Redis/Redis_面试.md) - 关键词:`面试` + +#### [Redis](source/_posts/12.数据库/05.KV数据库/02.Memcached.md) + +### 列式数据库 + +#### HBase - [HBase 快速入门](source/_posts/12.数据库/06.列式数据库/01.HBase/01.HBase快速入门.md) - [HBase 数据模型](source/_posts/12.数据库/06.列式数据库/01.HBase/02.HBase数据模型.md) @@ -439,28 +421,26 @@ - [HBase 运维](source/_posts/12.数据库/06.列式数据库/01.HBase/21.HBase运维.md) - [HBase 命令](source/_posts/12.数据库/06.列式数据库/01.HBase/22.HBase命令.md) -#### 搜索引擎数据库 +### 搜索引擎数据库 -##### Elasticsearch +#### Elasticsearch > Elasticsearch 是一个基于 Lucene 的搜索和数据分析工具,它提供了一个分布式服务。Elasticsearch 是遵从 Apache 开源条款的一款开源产品,是当前主流的企业级搜索引擎。 -- [Elasticsearch 面试总结](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/01.Elasticsearch面试总结.md) 💯 -- [Elasticsearch 快速入门](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/02.Elasticsearch快速入门.md) -- [Elasticsearch 简介](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/03.Elasticsearch简介.md) -- [Elasticsearch 索引](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/04.Elasticsearch索引.md) -- [Elasticsearch 查询](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/05.Elasticsearch查询.md) -- [Elasticsearch 高亮](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/06.Elasticsearch高亮.md) -- [Elasticsearch 排序](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/07.Elasticsearch排序.md) -- [Elasticsearch 聚合](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/08.Elasticsearch聚合.md) -- [Elasticsearch 分析器](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/09.Elasticsearch分析器.md) -- [Elasticsearch 性能优化](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/10.Elasticsearch性能优化.md) -- [Elasticsearch Rest API](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/11.ElasticsearchRestApi.md) -- [ElasticSearch Java API 之 High Level REST Client](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/12.ElasticsearchHighLevelRestJavaApi.md) -- [Elasticsearch 集群和分片](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/13.Elasticsearch集群和分片.md) -- [Elasticsearch 运维](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/20.Elasticsearch运维.md) - -##### Elastic +- [Elasticsearch 简介](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_简介.md) +- [Elasticsearch 存储](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_存储.md) +- [Elasticsearch 搜索(上)](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_搜索上.md) +- [Elasticsearch 搜索(下)](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_搜索下.md) +- [Elasticsearch 聚合](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_聚合.md) +- [Elasticsearch 分析](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_分析.md) +- [Elasticsearch 集群](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_集群.md) +- [Elasticsearch 优化](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_优化.md) +- [Elasticsearch 运维](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_运维.md) +- [Elasticsearch API](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_API.md) +- [ElasticSearch API 之 High Level REST Client](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_API_HighLevelRest.md) +- [Elasticsearch 面试](source/_posts/12.数据库/07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_面试.md) 💯 + +#### Elastic - [Elastic 快速入门](source/_posts/12.数据库/07.搜索引擎数据库/02.Elastic/01.Elastic快速入门.md) - [Elastic 技术栈之 Filebeat](source/_posts/12.数据库/07.搜索引擎数据库/02.Elastic/02.Elastic技术栈之Filebeat.md) @@ -470,11 +450,11 @@ - [Elastic 技术栈之 Logstash](source/_posts/12.数据库/07.搜索引擎数据库/02.Elastic/06.Elastic技术栈之Logstash.md) - [Logstash 运维](source/_posts/12.数据库/07.搜索引擎数据库/02.Elastic/07.Logstash运维.md) -### 网络 +## 网络 > 如果你是做通信领域的开发,或者是 Web 应用的开发,那就或多或少需要了解一些计算机网络的知识 。 -#### 网络综合 +### 网络综合 > 理解计算机网络,首先需要从宏观层面了解计算机网络通信的分层结构。最有代表性的是 OSI 七层结构模型,但现实中更流行的是五层结构模型。 > @@ -489,7 +469,7 @@ - [计算机网络之传输层](source/_posts/13.网络/01.网络综合/14.传输层.md) - 关键词:`UDP`、`TCP`、滑动窗口、拥塞控制、三次握手 - [计算机网络之应用层](source/_posts/13.网络/01.网络综合/15.应用层.md) - 关键词:`HTTP`、`DNS`、`FTP`、`TELNET`、`DHCP` -#### 网络协议 +### 网络协议 - [超文本传输协议 HTTP](source/_posts/13.网络/02.网络协议/01.HTTP.md) - [域名系统协议 DNS](source/_posts/13.网络/02.网络协议/02.DNS) @@ -497,34 +477,34 @@ - [用户数据报协议 UDP](source/_posts/13.网络/02.网络协议/04.UDP.md) - [ICMP](source/_posts/13.网络/02.网络协议/05.ICMP.md) -#### 网络技术 +### 网络技术 - [WebSocket](source/_posts/13.网络/03.网络技术/01.WebSocket.md) - [CDN](source/_posts/13.网络/03.网络技术/02.CDN.md) - [VPN](source/_posts/13.网络/03.网络技术/03.VPN.md) -### 分布式 +## 分布式 -#### 分布式综合 +### [分布式综合](source/_posts/15.分布式/00.分布式综合) -- [分布式面试总结](source/_posts/15.分布式/00.分布式综合/99.分布式面试.md) +- [逻辑时钟](source/_posts/15.分布式/00.分布式综合/逻辑时钟.md) - 关键词:`逻辑时钟`、`向量时钟`、`版本时钟`、`全序`、`偏序` +- [CAP 和 BASE](source/_posts/15.分布式/00.分布式综合/CAP&BASE.md) - 关键词:`ACID`、`CAP`、`BASE`、`一致性` +- [拜占庭将军问题](source/_posts/15.分布式/00.分布式综合/拜占庭将军问题.md) - 关键词:`共识` +- [分布式算法 Paxos](source/_posts/15.分布式/00.分布式综合/Paxos.md) - 关键词:`共识`、`Paxos` +- [分布式算法 Raft](source/_posts/15.分布式/00.分布式综合/Raft.md) - 关键词:`共识`、`Raft` +- [分布式算法 Gossip](source/_posts/15.分布式/00.分布式综合/Gossip.md) - 关键词:`Gossip` +- [ZAB 协议](source/_posts/15.分布式/00.分布式综合/Zab.md) - 关键词:`共识`、`ZAB`、`ZooKeeper` +- [分布式综合面试](source/_posts/15.分布式/00.分布式综合/分布式综合面试.md) -#### 分布式理论 - -- [分布式理论](source/_posts/15.分布式/01.分布式理论/01.分布式基础理论.md) - 关键词:`拜占庭将军`、`CAP`、`BASE`、`错误的分布式假设` -- [分布式算法 Paxos](source/_posts/15.分布式/01.分布式理论/11.Paxos算法.md) - 关键词:`共识性算法` -- [分布式算法 Raft](source/_posts/15.分布式/01.分布式理论/12.Raft算法.md) - 关键词:`共识性算法` -- [分布式算法 Gossip](source/_posts/15.分布式/01.分布式理论/13.Gossip算法.md) - 关键词:`数据传播` - -#### 分布式协同 +### [分布式协同](source/_posts/15.分布式/11.分布式协同) - **分布式协同综合** - - 集群 - - [分布式复制](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/02.分布式复制.md) - - 分区 - - 选主 - - [分布式事务](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/05.分布式事务.md) - 关键词:`2PC`、`3PC`、`TCC`、`本地消息表`、`MQ 消息`、`SAGA` - - [分布式锁](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/06.分布式锁.md) - 关键词:`数据库`、`Redis`、`ZooKeeper`、`互斥`、`可重入`、`死锁`、`容错`、`自旋尝试` + - [分布式复制](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/分布式复制.md) - 关键词:`主从`、`多主`、`无主` + - [分布式分区](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/分布式分区.md) - 关键词:`分区再均衡`、`路由` + - [分布式共识](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/分布式共识.md) - 关键词:`共识`、`广播`、`epoch`、`quorum` + - [分布式事务](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/分布式事务.md) - 关键词:`2PC`、`3PC`、`TCC`、`本地消息表`、`消息事务`、`SAGA` + - [分布式锁](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/分布式锁.md) - 关键词:`互斥`、`可重入`、`死锁`、`容错`、`自旋尝试`、`公平性` + - [分布式 ID](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/分布式ID.md) - 关键词:`UUID`、`自增序列`、`雪花算法`、`Leaf` - **ZooKeeper** - [ZooKeeper 原理](source/_posts/15.分布式/11.分布式协同/02.ZooKeeper/01.ZooKeeper原理.md) - [ZooKeeper Java Api](source/_posts/15.分布式/11.分布式协同/02.ZooKeeper/02.ZooKeeperJavaApi.md) @@ -532,62 +512,56 @@ - [ZooKeeper 运维](source/_posts/15.分布式/11.分布式协同/02.ZooKeeper/04.ZooKeeper运维.md) - [ZooKeeper Acl](source/_posts/15.分布式/11.分布式协同/02.ZooKeeper/05.ZooKeeperAcl.md) -#### 分布式调度 - -- [流量控制](source/_posts/15.分布式/12.分布式调度/03.流量控制.md) - 关键词:`限流`、`熔断`、`降级`、`计数器法`、`时间窗口法`、`令牌桶法`、`漏桶法` -- [负载均衡](source/_posts/15.分布式/12.分布式调度/02.负载均衡.md) - 关键词:`轮询`、`随机`、`最少连接`、`源地址哈希`、`一致性哈希`、`虚拟 hash 槽` -- [服务路由](source/_posts/15.分布式/12.分布式调度/01.服务路由.md) - 关键词:`路由`、`条件路由`、`脚本路由`、`标签路由` -- [分布式会话](source/_posts/15.分布式/12.分布式调度/10.分布式会话.md) - 关键词:`粘性 Session`、`Session 复制共享`、`基于缓存的 session 共享` -- [分布式 ID](source/_posts/15.分布式/12.分布式调度/04.分布式ID.md) - 关键词:`UUID`、`自增序列`、`雪花算法`、`Leaf` +### [分布式调度](source/_posts/15.分布式/12.分布式调度) -#### 分布式高可用 +- [服务注册和发现](source/_posts/15.分布式/12.分布式调度/服务注册和发现.md) - 关键词:`服务注册`、`服务发现`、`元数据` +- [负载均衡](source/_posts/15.分布式/12.分布式调度/负载均衡.md) - 关键词:`轮询`、`随机`、`最少连接`、`源地址哈希`、`一致性哈希`、`虚拟 hash 槽` +- [流量控制](source/_posts/15.分布式/12.分布式调度/流量控制.md) - 关键词:`限流`、`熔断`、`降级`、`计数器法`、`时间窗口法`、`令牌桶法`、`漏桶法` +- [路由和网关](source/_posts/15.分布式/12.分布式调度/网关路由.md) - 关键词:`路由`、`条件路由`、`脚本路由`、`标签路由` -- [服务容错](source/_posts/15.分布式/13.分布式高可用/02.服务容错.md) +### 分布式高可用 -#### 分布式通信 +- [服务容错](source/_posts/15.分布式/11.分布式协同/01.分布式协同综合/服务容错.md) -#### RPC +### [分布式通信](source/_posts/15.分布式/21.分布式通信) -##### RPC 综合 +#### [RPC](source/_posts/15.分布式/21.分布式通信/01.RPC) -- [RPC 基础](source/_posts/15.分布式/21.分布式通信/01.RPC/00.RPC综合/01.RPC基础.md) -- [RPC 进阶](source/_posts/15.分布式/21.分布式通信/01.RPC/00.RPC综合/02.RPC进阶.md) -- [RPC 高级](source/_posts/15.分布式/21.分布式通信/01.RPC/00.RPC综合/03.RPC高级.md) -- [服务注册和发现](source/_posts/15.分布式/21.分布式通信/01.RPC/00.RPC综合/11.服务注册和发现.md) +- [Dubbo 面试](source/_posts/15.分布式/21.分布式通信/01.RPC/Dubbo面试.md) +- [RPC 面试](source/_posts/15.分布式/21.分布式通信/01.RPC/RPC面试.md) -#### MQ +#### [MQ](source/_posts/15.分布式/21.分布式通信/02.MQ) -##### MQ 综合 +##### [MQ 综合](source/_posts/15.分布式/21.分布式通信/02.MQ/00.MQ综合) -- [消息队列面试](source/_posts/15.分布式/21.分布式通信/02.MQ/00.MQ综合/01.消息队列面试.md) -- [消息队列基本原理](source/_posts/15.分布式/21.分布式通信/02.MQ/00.MQ综合/02.消息队列基本原理.md) +- [MQ 面试](source/_posts/15.分布式/21.分布式通信/02.MQ/00.MQ综合/MQ面试.md) -##### Kafka +##### [Kafka](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka) -- [Kafka 快速入门](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/01.Kafka快速入门.md) -- [Kafka 生产者](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/02.Kafka生产者.md) -- [Kafka 消费者](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/03.Kafka消费者.md) -- [Kafka 集群](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/04.Kafka集群.md) -- [Kafka 可靠传输](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/05.Kafka可靠传输.md) -- [Kafka 存储](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/06.Kafka存储.md) -- [Kafka 流式处理](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/07.Kafka流式处理.md) -- [Kafka 运维](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/08.Kafka运维.md) +- [Kafka 快速入门](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka快速入门.md) +- [Kafka 生产](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka生产.md) +- [Kafka 消费](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka消费.md) +- [Kafka 集群](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka集群.md) +- [Kafka 可靠传输](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka可靠传输.md) +- [Kafka 存储](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka存储.md) +- [Kafka 流式处理](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka流式处理.md) +- [Kafka 运维](source/_posts/15.分布式/21.分布式通信/02.MQ/01.Kafka/Kafka运维.md) -##### RocketMQ +##### [RocketMQ](source/_posts/15.分布式/21.分布式通信/02.MQ/02.RocketMQ) -- [RocketMQ 快速入门](source/_posts/15.分布式/21.分布式通信/02.MQ/02.RocketMQ/01.RocketMQ快速入门.md) -- [RocketMQ 基本原理](source/_posts/15.分布式/21.分布式通信/02.MQ/02.RocketMQ/02.RocketMQ基本原理.md) -- [RocketMQ Faq](source/_posts/15.分布式/21.分布式通信/02.MQ/02.RocketMQ/99.RocketMQFaq.md) +- [RocketMQ 快速入门](source/_posts/15.分布式/21.分布式通信/02.MQ/02.RocketMQ/RocketMQ快速入门.md) +- [RocketMQ 基本原理](source/_posts/15.分布式/21.分布式通信/02.MQ/02.RocketMQ/RocketMQ基本原理.md) +- [RocketMQ Faq](source/_posts/15.分布式/21.分布式通信/02.MQ/02.RocketMQ/RocketMQFaq.md) ##### 其他 MQ -- [ActiveMQ](source/_posts/15.分布式/21.分布式通信/02.MQ/99.其他MQ/01.ActiveMQ.md) +- [ActiveMQ](source/_posts/15.分布式/21.分布式通信/02.MQ/99.其他MQ/ActiveMQ.md) -#### 分布式存储 +### [分布式存储](source/_posts/15.分布式/22.分布式存储) -- [数据缓存](source/_posts/15.分布式/22.分布式存储/01.数据缓存.md) - 关键词:`进程内缓存`、`分布式缓存`、`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` -- [读写分离](source/_posts/15.分布式/22.分布式存储/02.读写分离.md) -- [分库分表](source/_posts/15.分布式/22.分布式存储/03.分库分表.md) - 关键词:`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` +- [分布式缓存](source/_posts/15.分布式/22.分布式存储/分布式缓存.md) - 关键词:`进程内缓存`、`分布式缓存`、`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` +- [读写分离](source/_posts/15.分布式/22.分布式存储/读写分离.md) +- [分库分表](source/_posts/15.分布式/22.分布式存储/分库分表.md) - 关键词:`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` ## 编程 @@ -620,8 +594,10 @@ #### 微服务 -- [微服务简介](source/_posts/03.设计/01.架构/01.微服务/01.微服务简介.md) -- [微服务基本原理](source/_posts/03.设计/01.架构/01.微服务/02.微服务基本原理.md) +- [微服务简介](source/_posts/03.设计/01.架构/01.微服务/01.微服务简介.md) - 关键词:`定义`、`演进`、`利弊`、`如何拆分`、`容量规划`、`核心组件` +- [微服务之注册和发现](source/_posts/03.设计/01.架构/01.微服务/02.微服务之注册和发现.md) - 关键词:`服务定义`、`注册中心`、`元数据`、`健康检查`、`服务订阅`、`一致性` +- [微服务之服务调用](source/_posts/03.设计/01.架构/01.微服务/03.微服务之服务调用.md) - 关键词:`RPC`、`通信协议`、`传输方式`、`序列化` +- [微服务基本原理](source/_posts/03.设计/01.架构/01.微服务/10.微服务基本原理.md) - 关键词:`微服务`、`序列化`、`动态代理`、`通信`、`服务注册发现`、`健康检查`、`路由`、`负载均衡`、`容错处理`、`优雅上线下线`、`限流`、`熔断`、`业务分组` #### 安全 @@ -685,9 +661,9 @@ ### UML -- [UML 快速入门](source/_posts/03.设计/11.UML/01.UML快速入门.md) -- [UML 结构建模图](source/_posts/03.设计/11.UML/02.UML结构建模图.md) -- [UML 行为建模图](source/_posts/03.设计/11.UML/03.UML行为建模图.md) +- [UML 快速入门](source/_posts/03.设计/05.UML/01.UML快速入门.md) +- [UML 结构建模图](source/_posts/03.设计/05.UML/02.UML结构建模图.md) +- [UML 行为建模图](source/_posts/03.设计/05.UML/03.UML行为建模图.md) ## DevOps diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/42.JavaString\347\261\273\345\236\213.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/JavaString\347\261\273\345\236\213.md" similarity index 96% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/42.JavaString\347\261\273\345\236\213.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/JavaString\347\261\273\345\236\213.md" index b3c2b58975..238d74a8e3 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/42.JavaString\347\261\273\345\236\213.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/JavaString\347\261\273\345\236\213.md" @@ -4,14 +4,14 @@ date: 2020-12-25 18:43:11 order: 42 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 工具类 - 字符串 -permalink: /pages/bc583c/ +permalink: /pages/89de55cf/ --- # 深入理解 Java String 类型 @@ -131,7 +131,7 @@ sharedLocation.setRegion(messageInfo.getCountryCode().intern()); - [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) - [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) -- [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/82) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) - [Java 基本数据类型和引用类型](https://juejin.im/post/59cd71835188255d3448faf6) -- [深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) \ No newline at end of file +- [深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/10.Java\345\217\215\345\260\204.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\217\215\345\260\204.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/10.Java\345\217\215\345\260\204.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\217\215\345\260\204.md" index 109f0a5234..d7ec84d970 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/10.Java\345\217\215\345\260\204.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\217\215\345\260\204.md" @@ -4,14 +4,14 @@ date: 2020-06-04 13:51:01 order: 10 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 反射 - 动态代理 -permalink: /pages/0d066a/ +permalink: /pages/31248a53/ --- # 深入理解 Java 反射和动态代理 @@ -845,11 +845,11 @@ CGLIB 动态代理特点: - [Java 编程思想](https://book.douban.com/subject/2130190/) - [Java 核心技术(卷 1)](https://book.douban.com/subject/3146174/) -- [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) +- [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - [深入解析 Java 反射(1) - 基础](https://www.sczyh30.com/posts/Java/java-reflection-1/) - [Java 基础之—反射(非常重要)](https://blog.csdn.net/sinat_38259539/article/details/71799078) - [官方 Reflection API 文档](https://docs.oracle.com/javase/tutorial/reflect/index.html) - [Java 的动态代理机制详解](https://www.cnblogs.com/xiaoluo501395377/p/3383130.html) - [Java 动态代理机制详解(JDK 和 CGLIB,Javassist,ASM)](https://blog.csdn.net/luanlouis/article/details/24589193) - [深入理解 JDK 动态代理机制](https://www.jianshu.com/p/471c80a7e831) -- [深入理解 CGLIB 动态代理机制](https://www.jianshu.com/p/9a61af393e41) \ No newline at end of file +- [深入理解 CGLIB 动态代理机制](https://www.jianshu.com/p/9a61af393e41) diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/02.Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" similarity index 77% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/02.Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" index ea9aaa1a80..9bb00b8695 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/02.Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -4,13 +4,13 @@ date: 2019-05-06 15:02:02 order: 02 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 数据类型 -permalink: /pages/55d693/ +permalink: /pages/3f3649ee/ --- # 深入理解 Java 基本数据类型 @@ -22,105 +22,39 @@ permalink: /pages/55d693/ Java 中的数据类型有两类: - 值类型(又叫内置数据类型,基本数据类型) -- 引用类型(除值类型以外,都是引用类型,包括 `String`、数组) +- 引用类型(除值类型以外,都是引用类型,包括 `String`、数组等) ### 值类型 -Java 语言提供了 **8** 种基本类型,大致分为 **4** 类 +Java 语言提供了 **8** 种基本类型,大致分为 **4** 类:布尔型、字符型、整数型、浮点型。 -| 基本数据类型 | 分类 | 比特数 | 默认值 | 取值范围 | 说明 | -| ------------ | ---------- | ------ | ---------- | ----------------------------- | --------------------------------- | -| `boolean` | **布尔型** | 8 位 | `false` | {false, true} | | -| `char` | **字符型** | 16 位 | `'\u0000'` | [0, $2^{16} - 1$] | 存储 Unicode 码,用单引号赋值 | -| `byte` | **整数型** | 8 位 | `0` | [-$2^7$, $2^7 - 1$] | | -| `short` | **整数型** | 16 位 | `0` | [-$2^{15}$, $2^{15} - 1$] | | -| `int` | **整数型** | 32 位 | `0` | [-$2^{31}$, $2^{31} - 1$] | | -| `long` | **整数型** | 64 位 | `0L` | [-$2^{63}$, $2^{63} - 1$] | 赋值时一般在数字后加上 `l` 或 `L` | -| `float` | **浮点型** | 32 位 | `+0.0F` | [$2^{-149}$, $2^{128} - 1$] | 赋值时必须在数字后加上 `f` 或 `F` | -| `double` | **浮点型** | 64 位 | `+0.0D` | [$2^{-1074}$, $2^{1024} - 1$] | 赋值时一般在数字后加 `d` 或 `D` | +| 基本数据类型 | 分类 | 大小 | 默认值 | 取值范围 | 包装类 | 说明 | +| ------------ | ---------- | ------ | --------- | --------------------------------------- | --------- | ------------------------------------------- | +| `boolean` | **布尔型** | - | `false` | false、true | Boolean | `boolean` 的大小,是由具体的JVM实现来决定的 | +| `char` | **字符型** | 16 bit | `'u0000'` | 0 ~ 65535($2^{16} - 1$) | Character | 存储 Unicode 码,用单引号赋值 | +| `byte` | **整数型** | 8 bit | `0` | -128(-$2^7$) ~ 127($2^7 - 1$) | Byte | | +| `short` | **整数型** | 16 bit | `0` | -32768(-$2^{15}$) ~ 32767($2^{15} - 1$) | Short | | +| `int` | **整数型** | 32 bit | `0` | -$2^{31}$ ~ $2^{31} - 1$ | Integer | | +| `long` | **整数型** | 64 bit | `0L` | -$2^{63}$ ~ $2^{63} - 1$ | Long | 赋值时一般在数字后加上 `l` 或 `L` | +| `float` | **浮点型** | 32 bit | `0.0f` | 1.4e-45f ~ 3.4028235e+38f | Float | 赋值时必须在数字后加上 `f` 或 `F` | +| `double` | **浮点型** | 64 bit | `0.0d` | 4.9e-324 ~ 1.7976931348623157e+308 | Double | 赋值时一般在数字后加 `d` 或 `D` | -尽管各种数据类型的默认值看起来不一样,但在内存中都是 0。 - -在这些基本类型中,`boolean` 和 `char` 是唯二的无符号类型。 +`byte`、`short`、`int`、`long` 的最高比特位都用于表示正负(0 为正,-1 为负)。 ### 值类型和引用类型的区别 -- 从概念方面来说 - - 基本类型:变量名指向具体的数值。 - - 引用类型:变量名指向存数据对象的内存地址。 -- 从内存方面来说 - - 基本类型:变量在声明之后,Java 就会立刻分配给他内存空间。 - - 引用类型:它以特殊的方式(类似 C 指针)向对象实体(具体的值),这类变量声明时不会分配内存,只是存储了一个内存地址。 -- 从使用方面来说 - - 基本类型:使用时需要赋具体值,判断时使用 `==` 号。 - - 引用类型:使用时可以赋 null,判断时使用 `equals` 方法。 +| | 值类型 | 引用类型 | +| -------- | ------------------------------------------------------------ | ------------------------------------------------------- | +| 用途 | 一般用于常量和局部变量;不可用于泛型 | 可用于泛型 | +| 存储方式 | 值类型的局部变量存放在 JVM 中的局部变量表中;值类型的成员变量(未被 `static` 修饰 )存放在 JVM 中堆中 | 几乎所有引用类型的对象实例都存在于堆中 | +| 默认值 | 有默认值且不为 `null` | 默认值是 `null` | +| 比较方式 | `==` 比较的是值 | `==` 比较的是对象的内存地址;使用 `equals()` 才是比较值 | -> 👉 扩展阅读:[Java 基本数据类型和引用类型](https://juejin.im/post/59cd71835188255d3448faf6) +> 为什么说几乎所有引用类型的对象实例都存在于堆中? > -> 这篇文章对于基本数据类型和引用类型的内存存储讲述比较生动。 - -## 数据转换 - -Java 中,数据类型转换有两种方式: - -- 自动转换 -- 强制转换 - -### 自动转换 - -一般情况下,定义了某数据类型的变量,就不能再随意转换。但是 JAVA 允许用户对基本类型做**有限度**的类型转换。 - -如果符合以下条件,则 JAVA 将会自动做类型转换: - -- **由小数据转换为大数据** - - 显而易见的是,“小”数据类型的数值表示范围小于“大”数据类型的数值表示范围,即精度小于“大”数据类型。 - - 所以,如果“大”数据向“小”数据转换,会丢失数据精度。比如:long 转为 int,则超出 int 表示范围的数据将会丢失,导致结果的不确定性。 - - 反之,“小”数据向“大”数据转换,则不会存在数据丢失情况。由于这个原因,这种类型转换也称为**扩大转换**。 - - 这些类型由“小”到“大”分别为:(byte,short,char) < int < long < float < double。 - - 这里我们所说的“大”与“小”,并不是指占用字节的多少,而是指表示值的范围的大小。 - -- **转换前后的数据类型要兼容** - - 由于 boolean 类型只能存放 true 或 false,这与整数或字符是不兼容的,因此不可以做类型转换。 - -- **整型类型和浮点型进行计算后,结果会转为浮点类型** - -示例: - -```java -long x = 30; -float y = 14.3f; -System.out.println("x/y = " + x/y); -``` - -输出: - -``` -x/y = 1.9607843 -``` - -可见 long 虽然精度大于 float 类型,但是结果为浮点数类型。 - -### 强制转换 - -在不符合自动转换条件时或者根据用户的需要,可以对数据类型做强制的转换。 - -**强制转换使用括号 `()` 。** +> 因为 HotSpot 虚拟机引入了 JIT 优化之后,会对对象进行逃逸分析:如果发现某一个对象并没有逃逸到方法外部,那么就可能通过标量替换来实现栈上分配,而避免堆上分配内存。 -引用类型也可以使用强制转换。 - -示例: - -```java -float f = 25.5f; -int x = (int)f; -System.out.println("x = " + x); -``` +> 👉 扩展阅读:[Java 基本数据类型和引用类型](https://juejin.im/post/59cd71835188255d3448faf6) ## 装箱和拆箱 @@ -141,46 +75,13 @@ Boolean <-> boolean **引入包装类的目的**就是:提供一种机制,使得**基本数据类型可以与引用类型互相转换**。 -基本数据类型与包装类的转换被称为`装箱`和`拆箱`。 +基本数据类型与包装类的转换被称为装箱和拆箱。 -- **`装箱`(boxing)是将值类型转换为引用类型**。例如:`int` 转 `Integer` +- **装箱(boxing)是将值类型转换为引用类型**。例如:`int` 转 `Integer` - 装箱过程是通过调用包装类的 `valueOf` 方法实现的。 -- **`拆箱`(unboxing)是将引用类型转换为值类型**。例如:`Integer` 转 `int` +- **拆箱(unboxing)是将引用类型转换为值类型**。例如:`Integer` 转 `int` - 拆箱过程是通过调用包装类的 `xxxValue` 方法实现的。(xxx 代表对应的基本数据类型)。 -### 自动装箱、自动拆箱 - -基本数据(Primitive)型的自动装箱(boxing)拆箱(unboxing)自 JDK 5 开始提供的功能。 - -自动装箱与拆箱的机制可以让我们在 Java 的变量赋值或者是方法调用等情况下使用原始类型或者对象类型更加简单直接。 -因为自动装箱会隐式地创建对象,如果在一个循环体中,会创建无用的中间对象,这样会增加 GC 压力,拉低程序的性能。所以在写循环时一定要注意代码,避免引入不必要的自动装箱操作。 - -JDK 5 之前的形式: - -```java -Integer i1 = new Integer(10); // 非自动装箱 -``` - -JDK 5 之后: - -```java -Integer i2 = 10; // 自动装箱 -``` - -Java 对于自动装箱和拆箱的设计,依赖于一种叫做享元模式的设计模式(有兴趣的朋友可以去了解一下源码,这里不对设计模式展开详述)。 - -> 👉 扩展阅读:[深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) -> -> 结合示例,一步步阐述装箱和拆箱原理。 - -### 装箱、拆箱的应用和注意点 - -#### 装箱、拆箱应用场景 - -- 一种最普通的场景是:调用一个**含类型为 `Object` 参数的方法**,该 `Object` 可支持任意类型(因为 `Object` 是所有类的父类),以便通用。当你需要将一个值类型(如 int)传入时,需要使用 `Integer` 装箱。 -- 另一种用法是:一个**非泛型的容器**,同样是为了保证通用,而将元素类型定义为 `Object`。于是,要将值类型数据加入容器时,需要装箱。 -- 当 `==` 运算符的两个操作,一个操作数是包装类,另一个操作数是表达式(即包含算术运算)则比较的是数值(即会触发自动拆箱的过程)。 - 【示例】装箱、拆箱示例 ```java @@ -208,50 +109,119 @@ System.out.println("i1 == i4 is [" + (i1 == i4) + "]"); // 自动拆箱 【说明】 -上面的例子,虽然简单,但却隐藏了自动装箱、拆箱和非自动装箱、拆箱的应用。从例子中可以看到,明明所有变量都初始化为数值 10 了,但为何会出现 `i1 == i2 is [false` 而 `i1 == i4 is [true]` ? +上面的例子,虽然简单,但却隐藏了自动装箱、拆箱和非自动装箱、拆箱的应用。从例子中可以看到,明明所有变量都初始化为数值 10 了,但为何会出现 `i1 == i2 is [false]` 而 `i1 == i4 is [true]` ? 原因在于: - i1、i2 都是包装类,使用 `==` 时,Java 将它们当做两个对象,而非两个 int 值来比较,所以两个对象自然是不相等的。正确的比较操作应该使用 `equals` 方法。 - i1 是包装类,i4 是基础数据类型,使用 `==` 时,Java 会将两个 i1 这个包装类对象自动拆箱为一个 `int` 值,再代入到 `==` 运算表达式中计算;最终,相当于两个 `int` 进行比较,由于值相同,所以结果相等。 -【示例】包装类判等问题 +## 包装类的缓存机制 + +Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。 + +`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `True` or `False`。 + +Long 缓存源码: ```java -Integer a = 127; //Integer.valueOf(127) -Integer b = 127; //Integer.valueOf(127) -log.info("\nInteger a = 127;\nInteger b = 127;\na == b ? {}", a == b); // true +public static Long valueOf(long l) { + final int offset = 128; + if (l >= -128 && l <= 127) { // will cache + return LongCache.cache[(int)l + offset]; + } + return new Long(l); +} -Integer c = 128; //Integer.valueOf(128) -Integer d = 128; //Integer.valueOf(128) -log.info("\nInteger c = 128;\nInteger d = 128;\nc == d ? {}", c == d); //false -//设置-XX:AutoBoxCacheMax=1000再试试 +private static class LongCache { + private LongCache(){} -Integer e = 127; //Integer.valueOf(127) -Integer f = new Integer(127); //new instance -log.info("\nInteger e = 127;\nInteger f = new Integer(127);\ne == f ? {}", e == f); //false + static final Long cache[] = new Long[-(-128) + 127 + 1]; -Integer g = new Integer(127); //new instance -Integer h = new Integer(127); //new instance -log.info("\nInteger g = new Integer(127);\nInteger h = new Integer(127);\ng == h ? {}", g == h); //false + static { + for(int i = 0; i < cache.length; i++) + cache[i] = new Long(i - 128); + } +} +``` -Integer i = 128; //unbox -int j = 128; -log.info("\nInteger i = 128;\nint j = 128;\ni == j ? {}", i == j); //true +从以上代码可知:装箱时,若数值不在包装类缓存范围内,就会创建一个新的包装类实例。由此,我们不难进一步得出以下结论: + +1. 装箱操作可能会创建新对象,**频繁的装箱操作会造成不必要的内存消耗,影响性能**。 +2. **基础数据类型的比较操作使用 `==`,包装类的比较操作使用 `equals` 方法**。 + +### 自动装箱/拆箱 + +JDK5 开始,支持自动装箱/拆箱功能机制。 + +**自动装箱/拆箱是一种简化程序代码的语法糖**,使得值类型和包装类之间的转换更加直接。 + +JDK 5 之前的形式: + +```java +Integer i1 = new Integer(10); // 非自动装箱 +``` + +JDK 5 之后: + +```java +Integer i = 10; // 自动装箱 +int n = i; // 自动拆箱 ``` -通过运行结果可以看到,虽然看起来永远是在对 127 和 127、128 和 128 判等,但 == 却并非总是返回 true。 +上面这两行代码对应的字节码为: + +``` + L1 + + LINENUMBER 8 L1 + + ALOAD 0 + + BIPUSH 10 + + INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; + + PUTFIELD AutoBoxTest.i : Ljava/lang/Integer; + + L2 + + LINENUMBER 9 L2 + + ALOAD 0 + + ALOAD 0 -#### 装箱、拆箱应用注意点 + GETFIELD AutoBoxTest.i : Ljava/lang/Integer; -1. 装箱操作会创建对象,频繁的装箱操作会造成不必要的内存消耗,影响性能。所以**应该尽量避免装箱。** -2. 基础数据类型的比较操作使用 `==`,包装类的比较操作使用 `equals` 方法。 + INVOKEVIRTUAL java/lang/Integer.intValue ()I + + PUTFIELD AutoBoxTest.n : I + + RETURN +``` + +从字节码示例,可以发现: + +- 自动装箱过程是通过调用包装类的 `valueOf` 方法实现的。 +- 自动拆箱过程是通过调用包装类的 `xxxValue` 方法实现的。 + +因此, + +- `Integer i = 10` 等价于 `Integer i = Integer.valueOf(10)` +- `int n = i` 等价于 `int n = i.intValue()`; + +Java 对于自动装箱和拆箱的设计,依赖于一种叫做享元模式的设计模式(有兴趣的朋友可以去了解一下源码,这里不对设计模式展开详述)。 + +> 👉 扩展阅读:[深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) +> +> 结合示例,一步步阐述装箱和拆箱原理。 ## 判等问题 Java 中,通常使用 `equals` 或 `==` 进行判等操作。`equals` 是方法而 `==` 是操作符。此外,二者使用也是有区别的: -- 对**基本类型**,比如 `int`、`long`,进行判等,**只能使用 `==`,比较的是字面值**。因为基本类型的值就是其数值。 +- 对**值类型**,比如 `int`、`long`,进行判等,**只能使用 `==`,比较的是字面值**。因为基本类型的值就是其数值。 - 对**引用类型**,比如 `Integer`、`Long` 和 `String`,进行判等,**需要使用 `equals` 进行内容判等**。因为引用类型的直接值是指针,使用 `==` 的话,比较的是指针,也就是两个对象在内存中的地址,即比较它们是不是同一个对象,而不是比较对象的内容。 ### 包装类的判等 @@ -535,9 +505,72 @@ Lombok 自动生成的方法可能就不是我们期望的了。 @EqualsAndHashCode 默认实现没有使用父类属性。为解决这个问题,我们可以手动设置 callSuper 开关为 true,来覆盖这种默认行为。 -## 数值计算 +## 数据转换 + +Java 中,数据类型转换有两种方式: + +- 自动转换 +- 强制转换 + +### 自动转换 + +一般情况下,定义了某数据类型的变量,就不能再随意转换。但是 JAVA 允许用户对基本类型做**有限度**的类型转换。 + +如果符合以下条件,则 JAVA 将会自动做类型转换: + +- **由小数据转换为大数据** + + 显而易见的是,“小”数据类型的数值表示范围小于“大”数据类型的数值表示范围,即精度小于“大”数据类型。 + + 所以,如果“大”数据向“小”数据转换,会丢失数据精度。比如:long 转为 int,则超出 int 表示范围的数据将会丢失,导致结果的不确定性。 + + 反之,“小”数据向“大”数据转换,则不会存在数据丢失情况。由于这个原因,这种类型转换也称为**扩大转换**。 + + 这些类型由“小”到“大”分别为:(byte,short,char) < int < long < float < double。 + + 这里我们所说的“大”与“小”,并不是指占用字节的多少,而是指表示值的范围的大小。 + +- **转换前后的数据类型要兼容** + + 由于 boolean 类型只能存放 true 或 false,这与整数或字符是不兼容的,因此不可以做类型转换。 + +- **整型类型和浮点型进行计算后,结果会转为浮点类型** + +示例: + +```java +long x = 30; +float y = 14.3f; +System.out.println("x/y = " + x/y); +``` -### 浮点数计算问题 +输出: + +``` +x/y = 1.9607843 +``` + +可见 long 虽然精度大于 float 类型,但是结果为浮点数类型。 + +### 强制转换 + +在不符合自动转换条件时或者根据用户的需要,可以对数据类型做强制的转换。 + +**强制转换使用括号 `()` 。** + +引用类型也可以使用强制转换。 + +示例: + +```java +float f = 25.5f; +int x = (int)f; +System.out.println("x = " + x); +``` + +## 丢失精度和数据溢出 + +### 为什么浮点数计算存在丢失精度的风险 计算机是把数值保存在了变量中,不同类型的数值变量能保存的数值范围不同,当数值超过类型能表达的数值上限则会发生溢出问题。 @@ -557,6 +590,8 @@ System.out.println(amount1 - amount2); // 1.0499999999999998 比如,0.1 的二进制表示为 0.0 0011 0011 0011… (0011 无限循环),再转换为十进制就是 0.1000000000000000055511151231257827021181583404541015625。对于计算机而言,0.1 无法精确表达,这是浮点数计算造成精度损失的根源。 +### 如何解决浮点数丢失精度的问题 + **浮点数无法精确表达和运算的场景,一定要使用 BigDecimal 类型**。 使用 BigDecimal 时,有个细节要格外注意。让我们来看一段代码: @@ -579,9 +614,7 @@ System.out.println(new BigDecimal(123.3).divide(new BigDecimal(100))); **使用 BigDecimal 表示和计算浮点数,且务必使用字符串的构造方法来初始化 BigDecimal**。 -### 浮点数精度和格式化 - -**浮点数的字符串格式化也要通过 BigDecimal 进行**。 +【示例】**浮点数的字符串格式化也要通过 BigDecimal 进行**。 ```java private static void wrong1() { @@ -632,8 +665,6 @@ BigDecimal 的 equals 和 hashCode 方法会同时考虑 value 和 scale,如 Set hashSet1 = new HashSet<>(); hashSet1.add(new BigDecimal("1.0")); System.out.println(hashSet1.contains(new BigDecimal("1")));//返回false - - ``` 解决办法有两个: @@ -654,7 +685,7 @@ System.out.println(treeSet.contains(new BigDecimal("1")));//返回true ### 数值溢出 -数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的基本数值类型都有超出表达范围的可能性。 +数值计算还有一个要小心的点是溢出,不管是 int 还是 long,所有的值类型都有超出表达范围的可能性。 ```java long l = Long.MAX_VALUE; @@ -675,7 +706,11 @@ try { } ``` -方法二是,使用大数类 BigInteger。BigDecimal 是处理浮点数的专家,而 BigInteger 则是对大数进行科学计算的专家。 +方法二是,使用 BigInteger类。 + +`BigInteger` 内部使用 `int[]` 数组来存储任意大小的整形数据。 + +> BigDecimal 是处理浮点数的专家;而 BigInteger 则是对大数进行科学计算的专家。 ```java BigInteger i = new BigInteger(String.valueOf(Long.MAX_VALUE)); @@ -692,6 +727,6 @@ try { - [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) - [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) -- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) - [Java 基本数据类型和引用类型](https://juejin.im/post/59cd71835188255d3448faf6) -- [深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) \ No newline at end of file +- [深入剖析 Java 中的装箱和拆箱](https://www.cnblogs.com/dolphin0520/p/3780005.html) diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/01.Java\345\237\272\347\241\200\350\257\255\346\263\225.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\237\272\347\241\200\350\257\255\346\263\225.md" similarity index 95% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/01.Java\345\237\272\347\241\200\350\257\255\346\263\225.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\237\272\347\241\200\350\257\255\346\263\225.md" index 494adc56b8..ef6c6f6c70 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/01.Java\345\237\272\347\241\200\350\257\255\346\263\225.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\237\272\347\241\200\350\257\255\346\263\225.md" @@ -4,12 +4,12 @@ date: 2022-01-25 07:31:16 order: 01 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE -permalink: /pages/2950ba/ + - JavaCore +permalink: /pages/2d948841/ --- # Java 基础语法特性 @@ -40,7 +40,7 @@ public class HelloWorld { ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java基本数据类型.svg) -> 👉 扩展阅读:[深入理解 Java 基本数据类型](https://dunwu.github.io/waterdrop/pages/55d693/) +> 👉 扩展阅读:[深入理解 Java 基本数据类型](https://dunwu.github.io/waterdrop/pages/e1e559ed/) ## 变量和常量 @@ -77,13 +77,13 @@ Java 支持的变量类型有: ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java数组.svg) -> 👉 扩展阅读:[深入理解 Java 数组](https://dunwu.github.io/waterdrop/pages/155518/) +> 👉 扩展阅读:[深入理解 Java 数组](https://dunwu.github.io/waterdrop/pages/ae0740ef/) ## 枚举 ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java枚举.svg) -> 👉 扩展阅读:[深入理解 Java 枚举](https://dunwu.github.io/waterdrop/pages/979887/) +> 👉 扩展阅读:[深入理解 Java 枚举](https://dunwu.github.io/waterdrop/pages/2f0a1ca4/) ## 操作符 @@ -97,13 +97,13 @@ Java 中支持的操作符类型如下: ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220125072221.png) -> 👉 扩展阅读:[深入理解 Java 方法](https://dunwu.github.io/waterdrop/pages/7a3ffc/) +> 👉 扩展阅读:[深入理解 Java 方法](https://dunwu.github.io/waterdrop/pages/e70c4bf9/) ## 控制语句 ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java控制语句.svg) -> 👉 扩展阅读:[Java 控制语句](https://dunwu.github.io/waterdrop/pages/fb4f8c/) +> 👉 扩展阅读:[Java 控制语句](https://dunwu.github.io/waterdrop/pages/36fd1ce8/) ## 异常 @@ -111,13 +111,13 @@ Java 中支持的操作符类型如下: ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java异常.svg) -> 👉 扩展阅读:[深入理解 Java 异常](https://dunwu.github.io/waterdrop/pages/37415c/) +> 👉 扩展阅读:[深入理解 Java 异常](https://dunwu.github.io/waterdrop/pages/07ac0613/) ## 泛型 ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java泛型.svg) -> 👉 扩展阅读:[深入理解 Java 泛型](https://dunwu.github.io/waterdrop/pages/33a820/) +> 👉 扩展阅读:[深入理解 Java 泛型](https://dunwu.github.io/waterdrop/pages/ddc68bb5/) ## 反射 @@ -125,7 +125,7 @@ Java 中支持的操作符类型如下: ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java代理.svg) -> 👉 扩展阅读:[深入理解 Java 反射和动态代理](https://dunwu.github.io/waterdrop/pages/0d066a/) +> 👉 扩展阅读:[深入理解 Java 反射和动态代理](https://dunwu.github.io/waterdrop/pages/6ef470ed/) ## 注解 @@ -137,10 +137,10 @@ Java 中支持的操作符类型如下: ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/自定义注解.svg) -> 👉 扩展阅读:[深入理解 Java 注解](https://dunwu.github.io/waterdrop/pages/ecc011/) +> 👉 扩展阅读:[深入理解 Java 注解](https://dunwu.github.io/waterdrop/pages/56a4a49d/) ## 序列化 ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/xmind/Java序列化.svg) -> 👉 扩展阅读:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) +> 👉 扩展阅读:[Java 序列化](https://dunwu.github.io/waterdrop/pages/76ab164b/) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/08.Java\345\274\202\345\270\270.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\274\202\345\270\270.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/08.Java\345\274\202\345\270\270.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\274\202\345\270\270.md" index 7962559380..e80b731d72 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/08.Java\345\274\202\345\270\270.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\345\274\202\345\270\270.md" @@ -4,13 +4,13 @@ date: 2019-05-06 15:02:02 order: 08 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 异常 -permalink: /pages/37415c/ +permalink: /pages/f328743a/ --- # 深入理解 Java 异常 @@ -462,4 +462,4 @@ public class ExceptionOverrideDemo { - [优雅的处理你的 Java 异常](https://my.oschina.net/c5ms/blog/1827907) - https://juejin.im/post/5b6d61e55188251b38129f9a#heading-17 - https://www.cnblogs.com/skywang12345/p/3544168.html -- http://www.importnew.com/26613.html +- http://www.importnew.com/26613.html \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/07.Java\346\216\247\345\210\266\350\257\255\345\217\245.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\216\247\345\210\266\350\257\255\345\217\245.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/07.Java\346\216\247\345\210\266\350\257\255\345\217\245.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\216\247\345\210\266\350\257\255\345\217\245.md" index dd3530490d..00db0d3dd0 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/07.Java\346\216\247\345\210\266\350\257\255\345\217\245.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\216\247\345\210\266\350\257\255\345\217\245.md" @@ -4,13 +4,13 @@ date: 2020-10-17 19:13:25 order: 07 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 控制语句 -permalink: /pages/fb4f8c/ +permalink: /pages/2357b621/ --- # Java 控制语句 diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/05.Java\346\225\260\347\273\204.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\225\260\347\273\204.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/05.Java\346\225\260\347\273\204.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\225\260\347\273\204.md" index 7d1c14b53a..b14cc9a80c 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/05.Java\346\225\260\347\273\204.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\225\260\347\273\204.md" @@ -4,13 +4,13 @@ date: 2019-05-06 15:02:02 order: 05 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 数组 -permalink: /pages/155518/ +permalink: /pages/00d8985a/ --- # 深入理解 Java 数组 diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/04.Java\346\226\271\346\263\225.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\226\271\346\263\225.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/04.Java\346\226\271\346\263\225.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\226\271\346\263\225.md" index 3d24d5c97e..2f917b43bd 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/04.Java\346\226\271\346\263\225.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\226\271\346\263\225.md" @@ -4,13 +4,13 @@ date: 2019-05-06 15:02:02 order: 04 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 方法 -permalink: /pages/7a3ffc/ +permalink: /pages/46cba1d9/ --- # 深入理解 Java 方法 diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/06.Java\346\236\232\344\270\276.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\236\232\344\270\276.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/06.Java\346\236\232\344\270\276.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\236\232\344\270\276.md" index 54854d93a3..88ed842b11 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/06.Java\346\236\232\344\270\276.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\236\232\344\270\276.md" @@ -4,13 +4,13 @@ date: 2019-05-06 15:02:02 order: 06 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 枚举 -permalink: /pages/979887/ +permalink: /pages/4e47d423/ --- # 深入理解 Java 枚举 diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/09.Java\346\263\233\345\236\213.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\263\233\345\236\213.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/09.Java\346\263\233\345\236\213.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\263\233\345\236\213.md" index d7270eeea1..60a7745330 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/09.Java\346\263\233\345\236\213.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\263\233\345\236\213.md" @@ -4,13 +4,13 @@ date: 2020-10-17 19:13:25 order: 09 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 泛型 -permalink: /pages/33a820/ +permalink: /pages/4c266ac0/ --- # 深入理解 Java 泛型 @@ -669,4 +669,4 @@ Java 泛型有一些约定俗成的命名: - [Java 核心技术(卷 1)](https://book.douban.com/subject/3146174/) - [Effective java](https://book.douban.com/subject/3360807/) - [Oracle 泛型文档](https://docs.oracle.com/javase/tutorial/java/generics/index.html) -- [Java 泛型详解](https://juejin.im/post/584d36f161ff4b006cccdb82) +- [Java 泛型详解](https://juejin.im/post/584d36f161ff4b006cccdb82) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/11.Java\346\263\250\350\247\243.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\263\250\350\247\243.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/11.Java\346\263\250\350\247\243.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\263\250\350\247\243.md" index c65e9b89ad..b635e3e9e2 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/11.Java\346\263\250\350\247\243.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\346\263\250\350\247\243.md" @@ -4,13 +4,13 @@ date: 2019-05-06 15:02:02 order: 11 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 注解 -permalink: /pages/ecc011/ +permalink: /pages/77a42c51/ --- # 深入理解 Java 注解 diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/03.Java\351\235\242\345\220\221\345\257\271\350\261\241.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\351\235\242\345\220\221\345\257\271\350\261\241.md" similarity index 97% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/03.Java\351\235\242\345\220\221\345\257\271\350\261\241.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\351\235\242\345\220\221\345\257\271\350\261\241.md" index 59871ff0fe..0fe26a52bb 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/03.Java\351\235\242\345\220\221\345\257\271\350\261\241.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/Java\351\235\242\345\220\221\345\257\271\350\261\241.md" @@ -4,31 +4,31 @@ date: 2020-08-06 18:20:39 order: 03 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE + - JavaCore - 面向对象 -permalink: /pages/3e1661/ +permalink: /pages/e5542773/ --- # Java 面向对象 -> 在[Java 基本数据类型](02.Java基本数据类型.md) 中我们了解 Java 中支持的基本数据类型(值类型)。本文开始讲解 Java 中重要的引用类型——类。 +> 在[Java 基本数据类型](Java基本数据类型.md) 中我们了解 Java 中支持的基本数据类型(值类型)。本文开始讲解 Java 中重要的引用类型——类。 ## 面向对象 每种编程语言,都有自己的操纵内存中元素的方式。 -Java 中提供了[基本数据类型](https://dunwu.github.io/waterdrop/pages/55d693/),但这还不能满足编写程序时,需要抽象更加复杂数据类型的需要。因此,Java 中,允许开发者通过类(类的机制下面会讲到)创建自定义类型。 +Java 中提供了[基本数据类型](https://dunwu.github.io/waterdrop/pages/e1e559ed/),但这还不能满足编写程序时,需要抽象更加复杂数据类型的需要。因此,Java 中,允许开发者通过类(类的机制下面会讲到)创建自定义类型。 有了自定义类型,那么数据类型自然会千变万化,所以,必须要有一定的机制,使得它们仍然保持一些必要的、通用的特性。 Java 世界有一句名言:一切皆为对象。这句话,你可能第一天学 Java 时,就听过了。这不仅仅是一句口号,也体现在 Java 的设计上。 - 首先,所有 Java 类都继承自 `Object` 类(从这个名字,就可见一斑)。 -- 几乎所有 Java 对象初始化时,都要使用 `new` 创建对象([基本数据类型](https://dunwu.github.io/waterdrop/pages/55d693/)、String、枚举特殊处理),对象存储在堆中。 +- 几乎所有 Java 对象初始化时,都要使用 `new` 创建对象([基本数据类型](https://dunwu.github.io/waterdrop/pages/e1e559ed/)、String、枚举特殊处理),对象存储在堆中。 ```java // 下面两 @@ -377,4 +377,4 @@ Java 标准库中,比如 `collection` 框架,很多通用部分就被抽取 - [Head First Java](https://book.douban.com/subject/4496038/) - 文章 - [面向对象编程的弊端是什么? - invalid s 的回答](https://www.zhihu.com/question/20275578/answer/26577791) - - https://www.cnblogs.com/swiftma/p/5628762.html \ No newline at end of file + - https://www.cnblogs.com/swiftma/p/5628762.html diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/README.md" "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/README.md" similarity index 64% rename from "source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/README.md" rename to "source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/README.md" index 16f8b899f3..4cf1199df3 100644 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/README.md" +++ "b/source/_posts/01.Java/01.JavaCore/01.\345\237\272\347\241\200\347\211\271\346\200\247/README.md" @@ -1,16 +1,19 @@ --- -title: Java 基础特性 +title: Java 基础 date: 2020-06-04 13:51:01 categories: - Java - - JavaSE + - JavaCore - 基础特性 tags: - Java - - JavaSE -permalink: /pages/8ea213/ + - JavaCore +permalink: /pages/a7aa142d/ hidden: true index: false +dir: + order: 1 + link: true --- # Java 基础特性 @@ -19,19 +22,18 @@ index: false ## 📖 内容 -- [Java 开发环境](00.Java开发环境.md) -- [Java 基础语法特性](01.Java基础语法.md) -- [Java 基本数据类型](02.Java基本数据类型.md) -- [Java 面向对象](03.Java面向对象.md) -- [Java 方法](04.Java方法.md) -- [Java 数组](05.Java数组.md) -- [Java 枚举](06.Java枚举.md) -- [Java 控制语句](07.Java控制语句.md) -- [Java 异常](08.Java异常.md) -- [Java 泛型](09.Java泛型.md) -- [Java 反射](10.Java反射.md) -- [Java 注解](11.Java注解.md) -- [Java String 类型](42.JavaString类型.md) +- [Java 基础语法特性](Java基础语法.md) +- [Java 基本数据类型](Java基本数据类型.md) +- [Java 面向对象](Java面向对象.md) +- [Java 方法](Java方法.md) +- [Java 数组](Java数组.md) +- [Java 枚举](Java枚举.md) +- [Java 控制语句](Java控制语句.md) +- [Java 异常](Java异常.md) +- [Java 泛型](Java泛型.md) +- [Java 反射](Java反射.md) +- [Java 注解](Java注解.md) +- [Java String 类型](JavaString类型.md) ## 📚 资料 @@ -58,11 +60,11 @@ index: false - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) - [java-design-patterns](https://github.com/iluwatar/java-design-patterns) - [Java](https://github.com/TheAlgorithms/Java) - - [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/82) - - [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) - - [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) - - [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - - [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) + - [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) + - [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) + - [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) + - [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) + - [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) - **面试** - [CS-Notes](https://github.com/CyC2018/CS-Notes) - [JavaGuide](https://github.com/Snailclimb/JavaGuide) @@ -70,4 +72,4 @@ index: false ## 🚪 传送 -◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/04.JDK8.md" "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JDK8\347\211\271\346\200\247.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/04.JDK8.md" rename to "source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JDK8\347\211\271\346\200\247.md" index ca89cdfb75..3379a08611 100644 --- "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/04.JDK8.md" +++ "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JDK8\347\211\271\346\200\247.md" @@ -4,13 +4,13 @@ date: 2019-05-06 15:02:02 order: 04 categories: - Java - - JavaSE + - JavaCore - 高级特性 tags: - Java - - JavaSE + - JavaCore - JDK8 -permalink: /pages/ad1cce/ +permalink: /pages/f47a45e7/ --- # JDK8 入门指南 diff --git a/source/_posts/01.Java/01.JavaSE/06.JVM/08.JavaAgent.md "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JavaAgent.md" similarity index 99% rename from source/_posts/01.Java/01.JavaSE/06.JVM/08.JavaAgent.md rename to "source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JavaAgent.md" index 928b3d88aa..c06488195c 100644 --- a/source/_posts/01.Java/01.JavaSE/06.JVM/08.JavaAgent.md +++ "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JavaAgent.md" @@ -4,14 +4,13 @@ date: 2022-04-08 17:29:48 order: 08 categories: - Java - - JavaSE - - JVM + - JavaCore + - 高级特性 tags: - Java - - JavaSE - - JVM + - JavaCore - JavaAgent -permalink: /pages/16e728/ +permalink: /pages/9d88f435/ --- # JavaAgent @@ -371,4 +370,4 @@ public class APPMain { ## 参考资料 -- [Java Agent 探针技术](https://juejin.cn/post/7086026013498408973) \ No newline at end of file +- [Java Agent 探针技术](https://juejin.cn/post/7086026013498408973) diff --git "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/05.JavaSPI.md" "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JavaSPI.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/05.JavaSPI.md" rename to "source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JavaSPI.md" index c55e5ae675..7d537f38f8 100644 --- "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/05.JavaSPI.md" +++ "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/JavaSPI.md" @@ -4,17 +4,17 @@ date: 2022-04-26 19:11:59 order: 05 categories: - Java - - JavaSE + - JavaCore - 高级特性 tags: - Java - - JavaSE + - JavaCore - SPI - Dubbo - Spring Boot - common-logging - JDBC -permalink: /pages/496a7e/ +permalink: /pages/2131c240/ --- # 源码级深度理解 Java SPI diff --git "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/03.Java\345\233\275\351\231\205\345\214\226.md" "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\345\233\275\351\231\205\345\214\226.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/03.Java\345\233\275\351\231\205\345\214\226.md" rename to "source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\345\233\275\351\231\205\345\214\226.md" index 1d7264d8a3..6cfd0c6504 100644 --- "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/03.Java\345\233\275\351\231\205\345\214\226.md" +++ "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\345\233\275\351\231\205\345\214\226.md" @@ -4,13 +4,13 @@ date: 2017-11-08 14:38:26 order: 03 categories: - Java - - JavaSE + - JavaCore - 高级特性 tags: - Java - - JavaSE + - JavaCore - 国际化 -permalink: /pages/57003e/ +permalink: /pages/a33565c3/ --- # Java 国际化 diff --git "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/01.Java\346\255\243\345\210\231.md" "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\346\255\243\345\210\231.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/01.Java\346\255\243\345\210\231.md" rename to "source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\346\255\243\345\210\231.md" index 8108839c28..815825b386 100644 --- "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/01.Java\346\255\243\345\210\231.md" +++ "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\346\255\243\345\210\231.md" @@ -4,13 +4,13 @@ date: 2020-12-25 18:43:11 order: 01 categories: - Java - - JavaSE + - JavaCore - 高级特性 tags: - Java - - JavaSE + - JavaCore - 正则 -permalink: /pages/4c1dd4/ +permalink: /pages/f09a35e3/ --- # Java 正则从入门到精通 @@ -1395,4 +1395,4 @@ test - [msdn 正则表达式教程]() - [正则应用之——日期正则表达式](http://blog.csdn.net/lxcnn/article/details/4362500) - [http://www.regexlib.com/](http://www.regexlib.com/) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) \ No newline at end of file +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) diff --git "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/02.Java\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/02.Java\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" rename to "source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" index 2a2c987776..09386b8738 100644 --- "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/02.Java\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" +++ "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/Java\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" @@ -4,14 +4,14 @@ date: 2021-05-24 15:41:47 order: 02 categories: - Java - - JavaSE + - JavaCore - 高级特性 tags: - Java - - JavaSE + - JavaCore - 编码 - 加密 -permalink: /pages/a249ff/ +permalink: /pages/f4cf5533/ --- # Java 编码和加密 diff --git "a/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/README.md" "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/README.md" new file mode 100644 index 0000000000..92ca0bfb54 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/02.\351\253\230\347\272\247\347\211\271\346\200\247/README.md" @@ -0,0 +1,51 @@ +--- +title: Java 高级 +date: 2020-06-04 13:51:01 +categories: + - Java + - JavaCore + - 高级特性 +tags: + - Java + - JavaCore +permalink: /pages/515e9ef6/ +hidden: true +index: false +dir: + order: 2 + link: true +--- + +# Java 高级特性 + +> Java 高级总结 Java 的一些高级特性。 + +## 📖 内容 + +- [Java 正则](Java正则.md) - 关键词:Pattern、Matcher、捕获与非捕获、反向引用、零宽断言、贪婪与懒惰、元字符、DFA、NFA +- [Java 编码和加密](Java编码和加密.md) - 关键词:Base64、消息摘要、数字签名、对称加密、非对称加密、MD5、SHA、HMAC、AES、DES、DESede、RSA +- [Java 国际化](Java国际化.md) - 关键词:Locale、ResourceBundle、NumberFormat、DateFormat、MessageFormat +- [Java JDK8](JDK8特性.md) - 关键词:Stream、lambda、Optional、@FunctionalInterface +- [Java SPI](JavaSPI.md) - 关键词:SPI、ClassLoader +- [JavaAgent](JavaAgent.md) + +## 📚 资料 + +- Java 综合 + - [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) - 极客时间教程——基于 Java 生产环境的真实案例,讲解“避坑”的手段,很硬核 + - [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - 极客时间教程——覆盖 80% 以上 Java 应用调优场景 + - [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) - 极客时间教程——从面试官视角梳理如何解答常见 Java 面试问题 + - [CS-Notes](https://github.com/CyC2018/CS-Notes) - Github 上的 Java 基础级面试教程,行文清晰简洁 + - [JavaGuide](https://github.com/Snailclimb/JavaGuide) - Github 上的 Java 面试教程,Java 基础部分讲解较为细致 + - [advanced-java](https://github.com/doocs/advanced-java) - Github 上的 Java 面试教程,分布式部分从面试官视角讲解核心考察点 +- Java 基础 + - [《Java 编程思想》](https://book.douban.com/subject/2130190/) - Thinking in java,典中典!由于成书较早,部分内容已经多少有点过时 + - [《Java 核心技术 卷 I 开发基础》](https://book.douban.com/subject/35920145/) - 第 12 版,涵盖 Java 17 的新特性 + - [《Java 核心技术 卷 II 高级特性》](https://book.douban.com/subject/36337685/) - 第 12 版,涵盖 Java 17 的新特性 + - [《Head First Java》](https://book.douban.com/subject/2000732/) - 图文并茂,对新手非常友好的入门级教程 + - [《疯狂 Java 讲义》](https://book.douban.com/subject/3246499/) - 入门级教程 + - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) - 入门级在线教程 + +## 🚪 传送 + +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/02.Java\345\256\271\345\231\250\344\271\213List.md" "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213List.md" similarity index 86% rename from "source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/02.Java\345\256\271\345\231\250\344\271\213List.md" rename to "source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213List.md" index e2e9597be4..4223a242b5 100644 --- "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/02.Java\345\256\271\345\231\250\344\271\213List.md" +++ "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213List.md" @@ -4,13 +4,16 @@ date: 2018-06-27 23:12:18 order: 02 categories: - Java - - JavaSE + - JavaCore - 容器 tags: - Java - - JavaSE + - JavaCore - 容器 -permalink: /pages/69deb2/ + - List + - ArrayList + - LinkedList +permalink: /pages/c7adc138/ --- # Java 容器之 List @@ -42,12 +45,6 @@ permalink: /pages/69deb2/ ## ArrayList -> ArrayList 从数据结构角度来看,可以视为支持动态扩容的线性表。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220529190340.png) - -### ArrayList 要点 - `ArrayList` 是一个数组队列,相当于**动态数组**。**`ArrayList` 默认初始容量大小为 `10` ,添加元素时,如果发现容量已满,会自动扩容为原始大小的 1.5 倍**。因此,应该尽量在初始化 `ArrayList` 时,为其指定合适的初始化容量大小,减少扩容操作产生的性能开销。 `ArrayList` 定义: @@ -65,9 +62,7 @@ public class ArrayList extends AbstractList - `ArrayList` 实现了 `Serializable` 接口,**支持序列化**,能通过序列化方式传输。 - `ArrayList` 是**非线程安全**的。 -### ArrayList 原理 - -#### ArrayList 的数据结构 +### ArrayList 的数据结构 ArrayList 包含了两个重要的元素:`elementData` 和 `size`。 @@ -80,19 +75,10 @@ transient Object[] elementData; private int size; ``` -- `size` - 是动态数组的实际大小。 -- `elementData` - 是一个 `Object` 数组,用于保存添加到 `ArrayList` 中的元素。 +- `size` - 是动态数组的实际大小,默认初始容量大小为 10。 +- `elementData` - 是一个 `Object` 数组,用于保存添加到 `ArrayList` 中的元素。正是由于实际存储元素的是 `Object` 数组,所以其天然支持随机访问。 -#### ArrayList 的序列化 - -`ArrayList` 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。为此,`ArrayList` 定制了其序列化方式。具体做法是: - -- 存储元素的 `Object` 数组(即 `elementData`)使用 `transient` 修饰,使得它可以被 Java 序列化所忽略。 -- `ArrayList` 重写了 `writeObject()` 和 `readObject()` 来控制序列化数组中有元素填充那部分内容。 - -> :bulb: 不了解 Java 序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) - -#### ArrayList 构造方法 +### ArrayList 构造方法 ArrayList 类实现了三个构造函数: @@ -122,7 +108,16 @@ public ArrayList(int initialCapacity) { } ``` -#### ArrayList 访问元素 +### ArrayList 定制序列化 + +`ArrayList` 具有动态扩容特性,因此保存元素的数组不一定都会被使用,那么就没必要全部进行序列化。为此,`ArrayList` 定制了其序列化方式。具体做法是: + +- 存储元素的 `Object` 数组(即 `elementData`)使用 `transient` 修饰,使得它可以被 Java 序列化所忽略。 +- `ArrayList` 重写了 `writeObject()` 和 `readObject()` 来控制序列化数组中有元素填充那部分内容。 + +> :bulb: 不了解 Java 序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/dc9f1331/) + +### ArrayList 访问元素 `ArrayList` 访问元素的实现主要基于以下关键性源码: @@ -140,7 +135,7 @@ E elementData(int index) { 实现非常简单,其实就是**通过数组下标访问数组元素,其时间复杂度为 O(1)**,所以很快。 -#### ArrayList 添加元素 +### ArrayList 添加元素 `ArrayList` 添加元素有两种方法:一种是添加元素到数组末尾,另外一种是添加元素到任意位置。 @@ -208,7 +203,7 @@ private void grow(int minCapacity) { - 如果容量足够时,将数据作为数组中 `size+1` 位置上的元素写入,并将 `size` 自增 1。 - 如果容量不够时,需要使用 `grow` 方法进行扩容数组,新容量的大小为 `oldCapacity + (oldCapacity >> 1)`,也就是旧容量的 1.5 倍。扩容操作实际上是**调用 `Arrays.copyOf()` 把原数组拷贝为一个新数组**,因此最好在创建 `ArrayList` 对象时就指定大概的容量大小,减少扩容操作的次数。 -#### ArrayList 删除元素 +### ArrayList 删除元素 `ArrayList` 的删除方法和添加元素到任意位置方法有些相似。 @@ -230,7 +225,7 @@ public E remove(int index) { } ``` -#### ArrayList 的 Fail-Fast +### ArrayList 的 fail-fast `ArrayList` 使用 `modCount` 来记录结构发生变化的次数。结构发生变化是指添加或者删除至少一个元素的所有操作,或者是调整内部数组的大小,仅仅只是设置元素的值不算结构发生变化。 @@ -259,12 +254,6 @@ private void writeObject(java.io.ObjectOutputStream s) ## LinkedList -> LinkedList 从数据结构角度来看,可以视为双链表。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220529190416.png) - -### LinkedList 要点 - `LinkedList` 基于双链表结构实现。由于是双链表,所以**顺序访问会非常高效,而随机访问效率比较低。** `LinkedList` 定义: @@ -283,9 +272,7 @@ public class LinkedList - `LinkedList` 实现了 `Serializable` 接口,**支持序列化**。 - `LinkedList` 是**非线程安全**的。 -### LinkedList 原理 - -#### LinkedList 的数据结构 +### LinkedList 的数据结构 **`LinkedList` 内部维护了一个双链表**。 @@ -318,14 +305,14 @@ private static class Node { } ``` -#### LinkedList 的序列化 +### LinkedList 的序列化 `LinkedList` 与 `ArrayList` 一样也定制了自身的序列化方式。具体做法是: - 将 `size` (双链表容量大小)、`first` 和`last` (双链表的头尾节点)修饰为 `transient`,使得它们可以被 Java 序列化所忽略。 - 重写了 `writeObject()` 和 `readObject()` 来控制序列化时,只处理双链表中能被头节点链式引用的节点元素。 -#### LinkedList 访问元素 +### LinkedList 访问元素 `LinkedList` 访问元素的实现主要基于以下关键性源码: @@ -361,7 +348,7 @@ Node node(int index) { **推荐使用迭代器遍历 `LinkedList` ,不要使用传统的 `for` 循环**。注:foreach 语法会被编译器转换成迭代器遍历,但是它的遍历过程中不允许修改 `List` 长度,即不能进行增删操作。 -#### LinkedList 添加元素 +### LinkedList 添加元素 `LinkedList` 有多种添加元素方法: @@ -441,7 +428,7 @@ void linkBefore(E e, Node succ) { - 如果往头部添加元素,将头指针 `first` 指向新的 `Node`,之前的 `first` 对象的 `prev` 指向新的 `Node`。 - 如果是向尾部添加元素,则将尾指针 `last` 指向新的 `Node`,之前的 `last` 对象的 `next` 指向新的 `Node`。 -#### LinkedList 删除元素 +### LinkedList 删除元素 `LinkedList` 删除元素的实现主要基于以下关键性源码: @@ -501,6 +488,18 @@ E unlink(Node x) { - 如果当前节点有前驱节点,则让前驱节点指向当前节点的下一个节点;否则,让双链表头指针指向下一个节点。 - 如果当前节点有后继节点,则让后继节点指向当前节点的前一个节点;否则,让双链表尾指针指向上一个节点。 +## ArrayList vs. LinkedList + +- **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; +- **底层数据结构:** `ArrayList` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) +- 插入和删除是否受元素位置的影响: + - `ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 + - `LinkedList` 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(`add(E e)`、`addFirst(E e)`、`addLast(E e)`、`removeFirst()`、 `removeLast()`),时间复杂度为 O(1),如果是要在指定位置 `i` 插入和删除元素的话(`add(int index, E element)`,`remove(Object o)`,`remove(int index)`), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。 +- **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList`(实现了 `RandomAccess` 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 +- **内存空间占用:** `ArrayList` 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 + +我们在项目中一般是不会使用到 `LinkedList` 的,需要用到 `LinkedList` 的场景几乎都可以使用 `ArrayList` 来代替,并且,性能通常会更好!就连 `LinkedList` 的作者约书亚 · 布洛克(Josh Bloch)自己都说从来不会使用 `LinkedList` 。 + ## List 常见问题 ### Arrays.asList 问题点 @@ -618,7 +617,7 @@ log.info("arr:{} list:{}", Arrays.toString(arr), list); ### List.subList 问题点 -List.subList 直接引用了原始的 List,也可以认为是共享“存储”,而且对原始 List 直接进行结构性修改会导致 SubList 出现异常。 +`List.subList` 直接引用了原始的 `List`,也可以认为是共享“存储”,而且对原始 `List` 直接进行结构性修改会导致 `SubList` 出现异常。 ```java private static List> data = new ArrayList<>(); @@ -631,7 +630,7 @@ private static void oom() { } ``` -出现 OOM 的原因是,循环中的 1000 个具有 10 万个元素的 List 始终得不到回收,因为它始终被 subList 方法返回的 List 强引用。 +出现 OOM 的原因是,循环中的 1000 个具有 10 万个元素的 List 始终得不到回收,因为它始终被 `subList` 方法返回的 `List` 强引用。 解决方法是: @@ -681,4 +680,4 @@ List subList = list.stream().skip(1).limit(3).collect(Collectors.toList - [Java 编程思想(第 4 版)](https://item.jd.com/10058164.html) - https://www.cnblogs.com/skywang12345/p/3308556.html -- http://www.cnblogs.com/skywang12345/p/3308807.html \ No newline at end of file +- http://www.cnblogs.com/skywang12345/p/3308807.html diff --git "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/03.Java\345\256\271\345\231\250\344\271\213Map.md" "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Map.md" similarity index 89% rename from "source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/03.Java\345\256\271\345\231\250\344\271\213Map.md" rename to "source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Map.md" index c16207d133..1371eaf45b 100644 --- "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/03.Java\345\256\271\345\231\250\344\271\213Map.md" +++ "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Map.md" @@ -4,13 +4,13 @@ date: 2019-12-29 21:49:58 order: 03 categories: - Java - - JavaSE + - JavaCore - 容器 tags: - Java - - JavaSE + - JavaCore - 容器 -permalink: /pages/385755/ +permalink: /pages/de2c0744/ --- # Java 容器之 Map @@ -129,34 +129,36 @@ public abstract class Dictionary {} ## HashMap 类 -`HashMap` 类是最常用的 `Map`。 +从 `HashMap` 的命名,也可以看出:**`HashMap` 以散列方式存储键值对**。`HashMap` 是非线程安全的。 -### HashMap 要点 +**`HashMap` 允许使用空值和空键**,但 null 作为键只能有一个,null 作为值可以有多个。 -从 `HashMap` 的命名,也可以看出:**`HashMap` 以散列方式存储键值对**。 +(`HashMap` 类大致等同于 `Hashtable`,除了它是不同步的并且允许为空值。)这个类不保序;特别是,它的元素顺序可能会随着时间的推移变化。 -**`HashMap` 允许使用空值和空键**。(`HashMap` 类大致等同于 `Hashtable`,除了它是不同步的并且允许为空值。)这个类不保序;特别是,它的元素顺序可能会随着时间的推移变化。 +JDK1.8 之前 HashMap 由 数组+链表 组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。 JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于等于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 -**`HashMap` 有两个影响其性能的参数:初始容量和负载因子**。 +`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。并且, `HashMap` 总是使用 2 的幂作为哈希表的大小。 -- 容量是哈希表中桶的数量,初始容量就是哈希表创建时的容量。 -- 加载因子是散列表在其容量自动扩容之前被允许的最大饱和量。当哈希表中的 entry 数量超过负载因子和当前容量的乘积时,散列表就会被重新映射(即重建内部数据结构),一般散列表大约是存储桶数量的两倍。 +### JDK8 之前 HashMap 数据结构 -通常,默认加载因子(0.75)在时间和空间成本之间提供了良好的平衡。较高的值会减少空间开销,但会增加查找成本(反映在大部分 `HashMap` 类的操作中,包括 `get` 和 `put`)。在设置初始容量时,应考虑映射中的条目数量及其负载因子,以尽量减少重新运行操作的次数。如果初始容量大于最大入口数除以负载因子,则不会发生重新刷新操作。 +之前 HashMap 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。 -如果许多映射要存储在 `HashMap` 实例中,使用足够大的容量创建映射将允许映射存储的效率高于根据需要执行自动重新散列以增长表。请注意,使用多个具有相同 `hashCode()` 的密钥是降低任何散列表性能的一个可靠方法。为了改善影响,当键是 `Comparable` 时,该类可以使用键之间的比较顺序来帮助断开关系。 +HashMap 通过 key 的 hashCode 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 -`HashMap` 不是线程安全的。 +所谓扰动函数指的就是 HashMap 的 hash 方法。使用 hash 方法也就是扰动函数是为了防止一些实现比较差的 hashCode() 方法 换句话说使用扰动函数之后可以减少碰撞。 -### HashMap 原理 +JDK7 的 HashMap 的 hash 方法: -#### HashMap 数据结构 +```java +static int hash(int h) { + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); +} +``` -`HashMap` 的核心字段: +### JDK8 之后 HashMap 数据结构 -- `table` - `HashMap` 使用一个 `Node[]` 类型的数组 `table` 来储存元素。 -- `size` - 初始容量。 初始为 16,每次容量不够自动扩容 -- `loadFactor` - 负载因子。自动扩容之前被允许的最大饱和量,默认 0.75。 +`HashMap` 的主要字段定义如下: ```java public class HashMap extends AbstractMap @@ -170,23 +172,85 @@ public class HashMap extends AbstractMap transient int size; // 这个HashMap被结构修改的次数结构修改是那些改变HashMap中的映射数量或者修改其内部结构(例如,重新散列)的修改。 transient int modCount; - // 下一个调整大小的值(容量*加载因子)。 + // 下一个调整大小的值(容量*负载因子)。 int threshold; - // 散列表的加载因子 + // 散列表的负载因子 final float loadFactor; } ``` -#### HashMap 构造方法 +**`HashMap` 有两个影响其性能的参数:初始容量和负载因子**。 + +- `size` - 初始容量。默认为 16,每次容量不够自动扩容。容量是哈希表中桶的数量,初始容量就是哈希表创建时的容量。 +- `loadFactor` - 负载因子。自动扩容之前被允许的最大饱和量,默认 0.75。负载因子是散列表在其容量自动扩容之前被允许的最大饱和量。当哈希表中的 entry 数量超过负载因子和当前容量的乘积时,散列表就会被重新映射(即重建内部数据结构),一般散列表大约是存储桶数量的两倍。 + +通常,默认负载因子(0.75)在时间和空间成本之间提供了良好的平衡。较高的值会减少空间开销,但会增加查找成本(反映在大部分 `HashMap` 类的操作中,包括 `get` 和 `put`)。在设置初始容量时,应考虑映射中的条目数量及其负载因子,以尽量减少重新运行操作的次数。如果初始容量大于最大入口数除以负载因子,则不会发生重新刷新操作。 + +如果许多映射要存储在 `HashMap` 实例中,使用足够大的容量创建映射将允许映射存储的效率高于根据需要执行自动重新散列以增长表。请注意,使用多个具有相同 `hashCode()` 的密钥是降低任何散列表性能的一个可靠方法。为了改善影响,当键是 `Comparable` 时,该类可以使用键之间的比较顺序来帮助断开关系。 + +JDK8 的 HashMap 的 hash 方法: + +```java +static final int hash(Object key) { + int h; + // key.hashCode():返回散列值也就是hashcode + // ^:按位异或 + // >>>:无符号右移,忽略符号位,空位都以0补齐 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } +``` + +在 `get` 和 `put` 的过程中,计算下标时,先对 `hashCode` 进行 `hash` 操作,然后再通过 `hash` 值进一步计算下标,如下图所示: + +
+ +
+ +在对 `hashCode()` 计算 hash 时具体实现是这样的: + +```java +static final int hash(Object key) { + int h; + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); +} +``` + +可以看到这个方法大概的作用就是:高 16bit 不变,低 16bit 和高 16bit 做了一个异或。 + +在设计 hash 方法时,因为目前的 table 长度 n 为 2 的幂,而计算下标的时候,是这样实现的(使用 `&` 位操作,而非 `%` 求余): + +```java +(n - 1) & hash +``` + +设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在 n - 1 为 15(0x1111) 时,其实散列真正生效的只是低 4bit 的有效位,当然容易碰撞了。 + +因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高 16bit 和低 16bit 异或了一下。设计者还解释到因为现在大多数的 hashCode 的分布已经很不错了,就算是发生了碰撞也用 O(logn)的 tree 去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table 长度比较小时),从而引起的碰撞。 + +如果还是产生了频繁的碰撞,会发生什么问题呢?作者注释说,他们使用树来处理频繁的碰撞(we use trees to handle large sets of collisions in bins),在 [JEP-180](http://openjdk.java.net/jeps/180) 中,描述了这个问题: + +> Improve the performance of java.util.HashMap under high hash-collision conditions by using balanced trees rather than linked lists to store map entries. Implement the same improvement in the LinkedHashMap class. + +之前已经提过,在获取 HashMap 的元素时,基本分两步: + +1. 首先根据 hashCode() 做 hash,然后确定 bucket 的 index; + +2. 如果 bucket 的节点的 key 不是我们需要的,则通过 keys.equals()在链中找。 + +在 JDK8 之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行 get 时,两步的时间复杂度是 O(1)+O(n)。因此,当碰撞很厉害的时候 n 很大,O(n)的速度显然是影响速度的。 + +因此在 JDK8 中,利用红黑树替换链表,这样复杂度就变成了 O(1)+O(logn)了,这样在 n 很大的时候,能够比较理想的解决这个问题,在 JDK8:HashMap 的性能提升一文中有性能测试的结果。 + +### HashMap 构造方法 ```java -public HashMap(); // 默认加载因子0.75 -public HashMap(int initialCapacity); // 默认加载因子0.75;以 initialCapacity 初始化容量 -public HashMap(int initialCapacity, float loadFactor); // 以 initialCapacity 初始化容量;以 loadFactor 初始化加载因子 -public HashMap(Map m) // 默认加载因子0.75 +public HashMap(); // 默认负载因子0.75 +public HashMap(int initialCapacity); // 默认负载因子0.75;以 initialCapacity 初始化容量 +public HashMap(int initialCapacity, float loadFactor); // 以 initialCapacity 初始化容量;以 loadFactor 初始化负载因子 +public HashMap(Map m) // 默认负载因子0.75 ``` -#### put 方法的实现 +### put 方法的实现 put 方法大致的思路为: @@ -265,7 +329,7 @@ final V putVal(int hash, K key, V value, boolean onlyIfAbsent, 但如果我们将 hashCode 值右移 16 位(h >>> 16 代表无符号右移 16 位),也就是取 int 类型的一半,刚好可以将该二进制数对半切开,并且使用位异或运算(如果两个数对应的位置相反,则结果为 1,反之为 0),这样的话,就能避免上面的情况发生。这就是 hash() 方法的具体实现方式。**简而言之,就是尽量打乱 hashCode 真正参与运算的低 16 位。** -#### get 方法的实现 +### get 方法的实现 在理解了 put 之后,get 就很简单了。大致思路如下: @@ -310,54 +374,7 @@ final Node getNode(int hash, Object key) { } ``` -#### hash 方法的实现 - -HashMap **计算桶下标(index)公式:`key.hashCode() ^ (h >>> 16)`**。 - -下面针对这个公式来详细讲解。 - -在 `get` 和 `put` 的过程中,计算下标时,先对 `hashCode` 进行 `hash` 操作,然后再通过 `hash` 值进一步计算下标,如下图所示: - -
- -
- -在对 `hashCode()` 计算 hash 时具体实现是这样的: - -```java -static final int hash(Object key) { - int h; - return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); -} -``` - -可以看到这个方法大概的作用就是:高 16bit 不变,低 16bit 和高 16bit 做了一个异或。 - -在设计 hash 方法时,因为目前的 table 长度 n 为 2 的幂,而计算下标的时候,是这样实现的(使用 `&` 位操作,而非 `%` 求余): - -```java -(n - 1) & hash -``` - -设计者认为这方法很容易发生碰撞。为什么这么说呢?不妨思考一下,在 n - 1 为 15(0x1111) 时,其实散列真正生效的只是低 4bit 的有效位,当然容易碰撞了。 - -因此,设计者想了一个顾全大局的方法(综合考虑了速度、作用、质量),就是把高 16bit 和低 16bit 异或了一下。设计者还解释到因为现在大多数的 hashCode 的分布已经很不错了,就算是发生了碰撞也用 O(logn)的 tree 去做了。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table 长度比较小时),从而引起的碰撞。 - -如果还是产生了频繁的碰撞,会发生什么问题呢?作者注释说,他们使用树来处理频繁的碰撞(we use trees to handle large sets of collisions in bins),在 [JEP-180](http://openjdk.java.net/jeps/180) 中,描述了这个问题: - -> Improve the performance of java.util.HashMap under high hash-collision conditions by using balanced trees rather than linked lists to store map entries. Implement the same improvement in the LinkedHashMap class. - -之前已经提过,在获取 HashMap 的元素时,基本分两步: - -1. 首先根据 hashCode()做 hash,然后确定 bucket 的 index; - -2. 如果 bucket 的节点的 key 不是我们需要的,则通过 keys.equals()在链中找。 - -在 JDK8 之前的实现中是用链表解决冲突的,在产生碰撞的情况下,进行 get 时,两步的时间复杂度是 O(1)+O(n)。因此,当碰撞很厉害的时候 n 很大,O(n)的速度显然是影响速度的。 - -因此在 JDK8 中,利用红黑树替换链表,这样复杂度就变成了 O(1)+O(logn)了,这样在 n 很大的时候,能够比较理想的解决这个问题,在 JDK8:HashMap 的性能提升一文中有性能测试的结果。 - -#### resize 的实现 +### resize 的实现 当 `put` 时,如果发现目前的 bucket 占用程度已经超过了 Load Factor 所希望的比例,那么就会发生 resize。在 resize 的过程,简单的说就是把 bucket 扩充为 2 倍,之后重新计算 index,把节点再放到新的 bucket 中。 @@ -481,9 +498,7 @@ final Node[] resize() { | 是否有序 | 按照元素插入顺序存储 | | 是否线程安全 | 非线程安全 | -### LinkedHashMap 要点 - -#### LinkedHashMap 数据结构 +### LinkedHashMap 数据结构 **`LinkedHashMap` 通过维护一对 `LinkedHashMap.Entry` 类型的头尾指针,以双链表形式,保存所有数据**。 @@ -647,7 +662,7 @@ private void deleteEntry(Entry p) { root = replacement; else if (p == p.parent.left) p.parent.left = replacement; - else + else p.parent.right = replacement; // Null out links so they are OK to use by fixAfterDeletion. @@ -751,4 +766,4 @@ WeakHashMap 的 key 是**弱键**,即是 WeakReference 类型的;ReferenceQu - [Java-HashMap 工作原理及实现](https://yikun.github.io/2015/04/01/Java-HashMap工作原理及实现) - [Map 综述(二):彻头彻尾理解 LinkedHashMap](https://blog.csdn.net/justloveyou_/article/details/71713781) - [Java 集合系列 09 之 Map 架构](http://www.cnblogs.com/skywang12345/p/3308931.html) -- [Java 集合系列 13 之 WeakHashMap 详细介绍(源码解析)和使用示例](http://www.cnblogs.com/skywang12345/p/3311092.html) \ No newline at end of file +- [Java 集合系列 13 之 WeakHashMap 详细介绍(源码解析)和使用示例](http://www.cnblogs.com/skywang12345/p/3311092.html) diff --git "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/05.Java\345\256\271\345\231\250\344\271\213Queue.md" "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Queue.md" similarity index 55% rename from "source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/05.Java\345\256\271\345\231\250\344\271\213Queue.md" rename to "source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Queue.md" index 5573baa923..bafd6dd2f9 100644 --- "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/05.Java\345\256\271\345\231\250\344\271\213Queue.md" +++ "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Queue.md" @@ -4,13 +4,13 @@ date: 2020-02-21 16:26:21 order: 05 categories: - Java - - JavaSE + - JavaCore - 容器 tags: - Java - - JavaSE + - JavaCore - 容器 -permalink: /pages/ffa963/ +permalink: /pages/798a4c63/ --- # Java 容器之 Queue @@ -29,6 +29,16 @@ permalink: /pages/ffa963/ public interface Queue extends Collection {} ``` +`Queue` 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 **先进先出(FIFO)** 规则。 + +`Queue` 扩展了 `Collection` 的接口,根据 **因为容量问题而导致操作失败后处理方式的不同** 可以分为两类方法: 一种在操作失败后会抛出异常,另一种则会返回特殊值。 + +| `Queue` 接口 | 抛出异常 | 返回特殊值 | +| ------------ | --------- | ---------- | +| 插入队尾 | add(E e) | offer(E e) | +| 删除队首 | remove() | poll() | +| 查询队首元素 | element() | peek() | + ### AbstractQueue 抽象类 **`AbstractQueue` 类提供 `Queue` 接口的核心实现**,以最大限度地减少实现 `Queue` 接口所需的工作。 @@ -52,6 +62,19 @@ Deque 接口是 double ended queue 的缩写,即**双端队列**。Deque 继 大多数的实现对元素的数量没有限制,但这个接口既支持有容量限制的 deque,也支持没有固定大小限制的。 +`Deque` 扩展了 `Queue` 的接口, 增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类: + +| `Deque` 接口 | 抛出异常 | 返回特殊值 | +| ------------ | ------------- | --------------- | +| 插入队首 | addFirst(E e) | offerFirst(E e) | +| 插入队尾 | addLast(E e) | offerLast(E e) | +| 删除队首 | removeFirst() | pollFirst() | +| 删除队尾 | removeLast() | pollLast() | +| 查询队首元素 | getFirst() | peekFirst() | +| 查询队尾元素 | getLast() | peekLast() | + +事实上,`Deque` 还提供有 `push()` 和 `pop()` 等其他方法,可用于模拟栈。 + ## ArrayDeque `ArrayDeque` 是 `Deque` 的顺序表实现。 @@ -97,8 +120,19 @@ public class LinkedListQueueDemo { } ``` +`ArrayDeque` 和 `LinkedList` 都实现了 `Deque` 接口,两者都具有队列的功能,但两者有什么区别呢? + +- `ArrayDeque` 是基于可变长的数组和双指针来实现,而 `LinkedList` 则通过链表来实现。 +- `ArrayDeque` 不支持存储 `NULL` 数据,但 `LinkedList` 支持。 +- `ArrayDeque` 是在 JDK1.6 才被引入的,而`LinkedList` 早在 JDK1.2 时就已经存在。 +- `ArrayDeque` 插入时可能存在扩容过程, 不过均摊后的插入操作依然为 O(1)。虽然 `LinkedList` 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。 + +从性能的角度上,选用 `ArrayDeque` 来实现队列要比 `LinkedList` 更好。此外,`ArrayDeque` 也可以用于实现栈。 + ## PriorityQueue +`PriorityQueue` 是在 JDK1.5 中被引入的, 其与 `Queue` 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。 + `PriorityQueue` 类定义如下: ```java @@ -113,6 +147,9 @@ public class PriorityQueue extends AbstractQueue - `PriorityQueue` 中的元素根据自然顺序或 `Comparator` 提供的顺序排序。 - `PriorityQueue` 不接受 null 值元素。 - `PriorityQueue` 不是线程安全的。 +- `PriorityQueue` 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据 +- `PriorityQueue` 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。 +- `PriorityQueue` 默认是小顶堆,但可以接收一个 `Comparator` 作为构造参数,从而来自定义元素优先级的先后。 ## 参考资料 diff --git "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/04.Java\345\256\271\345\231\250\344\271\213Set.md" "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Set.md" similarity index 88% rename from "source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/04.Java\345\256\271\345\231\250\344\271\213Set.md" rename to "source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Set.md" index 1586caffb7..3c2c6665df 100644 --- "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/04.Java\345\256\271\345\231\250\344\271\213Set.md" +++ "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Set.md" @@ -4,22 +4,24 @@ date: 2019-12-29 21:49:58 order: 04 categories: - Java - - JavaSE + - JavaCore - 容器 tags: - Java - - JavaSE + - JavaCore - 容器 -permalink: /pages/794c6b/ + - Set + - HashSet + - TreeSet + - LinkedHashSet +permalink: /pages/da7e5eeb/ --- # Java 容器之 Set ## Set 简介 -
- -
+![](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/container/Set-diagrams.png) Set 家族成员简介: @@ -237,10 +239,14 @@ public abstract class EnumSet> extends AbstractSet - `EnumSet` 是有序的。以枚举值在 `EnumSet` 类中的定义顺序来决定集合元素的顺序。 - `EnumSet` 不是线程安全的。 -## 要点总结 +## HashSet vs. LinkedHashSet vs. TreeSet -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221190717.png) +`HashSet`、`LinkedHashSet` 和 `TreeSet` 都是 `Set` 接口的实现类,都能保证元素唯一,并且都不是线程安全的。 + +`HashSet`、`LinkedHashSet` 和 `TreeSet` 的主要区别在于底层数据结构不同。`HashSet` 的底层数据结构是哈希表(基于 `HashMap` 实现)。`LinkedHashSet` 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。`TreeSet` 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 + +底层数据结构不同又导致这三者的应用场景不同。`HashSet` 用于不需要保证元素插入和取出顺序的场景,`LinkedHashSet` 用于保证元素的插入和取出顺序满足 FIFO 的场景,`TreeSet` 用于支持对元素自定义排序规则的场景。 ## 参考资料 -- [Java 编程思想(Thinking in java)](https://item.jd.com/10058164.html) \ No newline at end of file +- [Java 编程思想(Thinking in java)](https://item.jd.com/10058164.html) diff --git "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/06.Java\345\256\271\345\231\250\344\271\213Stream.md" "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Stream.md" similarity index 98% rename from "source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/06.Java\345\256\271\345\231\250\344\271\213Stream.md" rename to "source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Stream.md" index 072e85eff3..2291438905 100644 --- "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/06.Java\345\256\271\345\231\250\344\271\213Stream.md" +++ "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\344\271\213Stream.md" @@ -4,13 +4,13 @@ date: 2020-12-05 18:30:22 order: 06 categories: - Java - - JavaSE + - JavaCore - 容器 tags: - Java - - JavaSE + - JavaCore - 容器 -permalink: /pages/529fad/ +permalink: /pages/4f97753f/ --- # Java 容器之 Stream diff --git "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/01.Java\345\256\271\345\231\250\347\256\200\344\273\213.md" "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\347\256\200\344\273\213.md" similarity index 51% rename from "source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/01.Java\345\256\271\345\231\250\347\256\200\344\273\213.md" rename to "source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\347\256\200\344\273\213.md" index f6e17b1d17..2995260c71 100644 --- "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/01.Java\345\256\271\345\231\250\347\256\200\344\273\213.md" +++ "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/Java\345\256\271\345\231\250\347\256\200\344\273\213.md" @@ -4,19 +4,24 @@ date: 2019-12-29 21:49:58 order: 01 categories: - Java - - JavaSE + - JavaCore - 容器 tags: - Java - - JavaSE + - JavaCore - 容器 -permalink: /pages/1cadba/ + - 泛型 + - Iterable + - Iterator + - Comparable + - Comparator + - Cloneable + - fail-fast +permalink: /pages/1bacccd8/ --- # Java 容器简介 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221175550.png) - ## 容器简介 ### 数组与容器 @@ -30,7 +35,7 @@ Java 中常用的存储容器就是数组和容器,二者有以下区别: - **数组可以存储基本数据类型,也可以存储引用数据类型**; - **容器只能存储引用数据类型**,基本数据类型的变量要转换成对应的包装类才能放入容器类中。 -> :bulb: 不了解什么是基本数据类型、引用数据类型、包装类这些概念,可以参考:[Java 基本数据类型](https://dunwu.github.io/waterdrop/pages/55d693/) +> :bulb: 不了解什么是基本数据类型、引用数据类型、包装类这些概念,可以参考:[Java 基本数据类型](https://dunwu.github.io/waterdrop/pages/3f3649ee/) ### 容器框架 @@ -39,10 +44,25 @@ Java 中常用的存储容器就是数组和容器,二者有以下区别: Java 容器框架主要分为 `Collection` 和 `Map` 两种。其中,`Collection` 又分为 `List`、`Set` 以及 `Queue`。 - `Collection` - 一个独立元素的序列,这些元素都服从一条或者多条规则。 - - `List` - 必须按照插入的顺序保存元素。 - - `Set` - 不能有重复的元素。 - - `Queue` - 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。 -- `Map` - 一组成对的“键值对”对象,允许你使用键来查找值。 + - `List` - 必须按照插入的顺序保存元素。常见 `List` 容器有: + - `ArrayList` - `Object[]` 数组。 + - `LinkedList` - 双链表 (JDK1.6 之前为循环链表,JDK1.7 取消了循环)。 + - `Vector` - 通过 `synchronized` 修饰读写方法来保证并发安全。 + - `Vector` - `Object[]` 数组,通过 `synchronized` 修饰读写方法来保证并发安全。 + - `Set` - 不能有重复的元素。常见 `Set` 容器有: + - `HashSet` - 无序,内部基于 `HashMap` 来实现的。 + - `LinkedHashSet` - 保证插入顺序,内部基于 `LinkedHashMap` 来实现的。 + - `TreeSet` - 保证自然序或用户指定的比较器顺序,内部基于红黑树实现。 + - `Queue` - 按照排队规则来确定对象产生的顺序。 + - `PriorityQueue` - 基于 `Object[]` 数组来实现小顶堆 + - `DelayQueue` - 延迟队列。 + - `ArrayQueue` - `ArrayDeque` 是 `Deque` 的顺序表实现。基于动态数组实现了栈和队列所需的所有操作。 + - `LinkedList` - `LinkedList` 是 `Deque` 的链表实现。 +- `Map` - 一组成对的“键值对”对象,允许你使用键来查找值。常见的 Map 容器有: + - `HashMap`:JDK1.8 之前 `HashMap` 由数组+链表组成的,数组是 `HashMap` 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 + - `LinkedHashMap`:`LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 + - `Hashtable`:数组+链表组成的,数组是 `Hashtable` 的主体,链表则是主要为了解决哈希冲突而存在的。 + - `TreeMap`:红黑树(自平衡的排序二叉树)。 ## 容器的基本机制 @@ -64,13 +84,13 @@ list.add(123); 如果没有泛型技术,如示例中的代码那样,容器中就可能存储任意数据类型,这是很危险的行为。 -``` +```java List list = new ArrayList(); list.add("123"); list.add(123); ``` -> :bulb: 想深入了解 Java 泛型技术的用法和原理可以参考:[深入理解 Java 泛型](https://dunwu.github.io/waterdrop/pages/33a820/) +> :bulb: 想深入了解 Java 泛型技术的用法和原理可以参考:[深入理解 Java 泛型](https://dunwu.github.io/waterdrop/pages/4c266ac0/) ### Iterable 和 Iterator @@ -146,11 +166,47 @@ public class IteratorDemo { } ``` +《阿里巴巴 Java 开发手册》的描述如下: + +> **不要在 foreach 循环里进行元素的 `remove/add` 操作。remove 元素请使用 `Iterator` 方式,如果并发操作,需要对 `Iterator` 对象加锁。** + +通过反编译你会发现 foreach 语法底层其实还是依赖 `Iterator` 。不过, `remove/add` 操作直接调用的是集合自己的方法,而不是 `Iterator` 的 `remove/add`方法 + +这就导致 `Iterator` 莫名其妙地发现自己有元素被 `remove/add` ,然后,它就会抛出一个 `ConcurrentModificationException` 来提示用户发生了并发修改异常。这就是单线程状态下产生的 **fail-fast 机制**。 + +> **fail-fast 机制**:多个线程对 fail-fast 集合进行修改的时候,可能会抛出`ConcurrentModificationException`。 即使是单线程下也有可能会出现这种情况,上面已经提到过。 +> +> 相关阅读:[什么是 fail-fastopen in new window](https://www.cnblogs.com/54chensongxia/p/12470446.html) 。 + +Java8 开始,可以使用 `Collection#removeIf()`方法删除满足特定条件的元素,如: + +```java +List list = new ArrayList<>(); +for (int i = 1; i <= 10; ++i) { + list.add(i); +} +list.removeIf(filter -> filter % 2 == 0); /* 删除list中的所有偶数 */ +System.out.println(list); /* [1, 3, 5, 7, 9] */ +``` + +除了上面介绍的直接使用 `Iterator` 进行遍历操作之外,你还可以: + +- 使用普通的 for 循环 +- 使用 fail-safe 的集合类。`java.util`包下面的所有的集合类都是 fail-fast 的,而`java.util.concurrent`包下面的所有的类都是 fail-safe 的。 +- ... ... + ### Comparable 和 Comparator -`Comparable` 是排序接口。若一个类实现了 `Comparable` 接口,表示该类的实例可以比较,也就意味着支持排序。实现了 `Comparable` 接口的类的对象的列表或数组可以通过 `Collections.sort` 或 `Arrays.sort` 进行自动排序。 +`Comparable` 接口和 `Comparator` 接口一般用于实现容器中元素的比较及排序: + +- `Comparable` 接口实际上是出自 `java.lang` 包 它有一个 `compareTo(Object obj) `方法用来排序 +- `Comparator ` 接口实际上是出自 `java.util` 包它有一个 `compare(Object obj1, Object obj2)` 方法用来排序 + +::: tabs#Comparable和Comparator接口定义 + +@tab `Comparable` 接口定义 -`Comparable` 接口定义: +`Comparable` 接口定义 ```java public interface Comparable { @@ -158,9 +214,9 @@ public interface Comparable { } ``` -`Comparator` 是比较接口,我们如果需要控制某个类的次序,而该类本身不支持排序(即没有实现 `Comparable` 接口),那么我们就可以建立一个“该类的比较器”来进行排序,这个“比较器”只需要实现 `Comparator` 接口即可。也就是说,我们可以通过实现 `Comparator` 来新建一个比较器,然后通过这个比较器对类进行排序。 +@tab `Comparator` 接口定义 -`Comparator` 接口定义: +`Comparator` 接口定义 ```java @FunctionalInterface @@ -189,7 +245,126 @@ public interface Comparator { } ``` -在 Java 容器中,一些可以排序的容器,如 `TreeMap`、`TreeSet`,都可以通过传入 `Comparator`,来定义内部元素的排序规则。 +::: + +假设,有一个 `List` 容器,存储的是 `User` 类型对象。现在要根据 `User` 中的 `age` 属性进行排序。 + +User 定义如下: + +```java +public class User { + + private String name; + private int age; + + public User(String name, int age) { + this.age = age; + this.name = name; + } + // getter、setter 略 +} +``` + +我们分别通过 `Comparable` 和 `Comparator` 来实现比较、排序,体会一下有何差异。 + +::: tabs#Comparable和Comparator使用示例 + +@tab `Comparable` 接口使用示例 + +`Comparable` 接口使用示例 + +```java +public class ComparableDemo { + + public static void main(String[] args) { + User a = new User("A", 18); + User b = new User("B", 17); + User c = new User("C", 20); + List list = new ArrayList<>(Arrays.asList(a, b, c)); + Collections.sort(list); + list.forEach(System.out::println); + } + // 输出: + // User{age=17, name='B'} + // User{age=18, name='A'} + // User{age=20, name='C'} + + // 需要对被比较、排序的类进行改造,实现 Comparable 接口 + static class User implements Comparable { + + private String name; + private int age; + + public User(String name, int age) { + this.age = age; + this.name = name; + } + + // getter、setter 略 + + @Override + public int compareTo(User o) { + return this.age - o.age; + } + + @Override + public String toString() { + return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; + } + } +} +``` + +从上例可以看出,使用 `Comparable` 接口,被排序对象类必须实现 `Comparable` 接口;并在类中定义 `compareTo` 方法的实现,即排序逻辑必须置于被排序对象类中。 + +@tab `Comparator` 接口使用示例 + +`Comparator` 接口使用示例 + +```java +public class ComparatorDemo { + + public static void main(String[] args) { + User a = new User("A", 18); + User b = new User("B", 17); + User c = new User("C", 20); + List list = new ArrayList<>(Arrays.asList(a, b, c)); + Collections.sort(list, new Comparator() { + @Override + public int compare(User o1, User o2) { + return o1.age - o2.age; + } + }); + list.forEach(System.out::println); + } + // 输出: + // User{age=17, name='B'} + // User{age=18, name='A'} + // User{age=20, name='C'} + + static class User { + + private String name; + private int age; + + public User(String name, int age) { + this.age = age; + this.name = name; + } + + // getter、setter 略 + + @Override + public String toString() { + return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; + } + } +} +``` + +从上例可以看出,使用 `Comparator` 接口和 `Comparable` 接口的不同点在于:被排序的对象类无需实现 `Comparator` 接口,排序逻辑置于被排序对象类的外部。 + +::: ### Cloneable @@ -240,7 +415,7 @@ public class FailFastDemo { Iterator iterator = list.iterator(); while (iterator.hasNext()) { int i = iterator.next(); - System.out.println("MyThreadA 访问元素:" + i); + System.out.println("MyThreadA 访问元素:" + i); try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) { @@ -284,12 +459,12 @@ fail-fast 有两种解决方案: 为了在并发环境下安全地使用容器,Java 提供了同步容器和并发容器。 -> 同步容器和并发容器详情请参考:[Java 并发容器](https://dunwu.github.io/waterdrop/pages/b067d6/) +> 同步容器和并发容器详情请参考:[Java 并发之容器](https://dunwu.github.io/waterdrop/pages/6fd8d836/) ## 参考资料 - [Java 编程思想(第 4 版)](https://item.jd.com/10058164.html) -- [由浅入深理解 java 集合(一)——集合框架 Collection、Map](https://www.jianshu.com/p/589d58033841) -- [由浅入深理解 java 集合(二)——集合 Set](https://www.jianshu.com/p/9081017a2d67) +- [由浅入深理解 java 集合(一)——集合框架 Collection、Map](https://www.jianshu.com/p/589d58033841) +- [由浅入深理解 java 集合(二)——集合 Set](https://www.jianshu.com/p/9081017a2d67) - [Java 提高篇(三十)-----Iterator](https://www.cnblogs.com/chenssy/p/3821328.html) -- [Java 提高篇(三四)-----fail-fast 机制](https://blog.csdn.net/chenssy/article/details/38151189) \ No newline at end of file +- [Java 提高篇(三四)-----fail-fast 机制](https://blog.csdn.net/chenssy/article/details/38151189) diff --git "a/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/README.md" "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/README.md" new file mode 100644 index 0000000000..37c5a57a96 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/03.\345\256\271\345\231\250/README.md" @@ -0,0 +1,61 @@ +--- +title: Java 容器 +date: 2020-06-04 13:51:01 +categories: + - Java + - JavaCore + - 容器 +tags: + - Java + - JavaCore + - 容器 +permalink: /pages/e335dfc2/ +hidden: true +index: false +dir: + order: 3 + link: true +--- + +# Java 容器 + +> Java 容器涉及许多数据结构知识点,所以设立专题进行总结。 + +## 📖 内容 + +- [Java 容器简介](Java容器简介.md) - 关键词:`泛型`、`Iterable`、`Iterator`、`Comparable`、`Comparator`、`Cloneable`、`fail-fast` +- [Java 容器之 List](Java容器之List.md) - 关键词:`List`、`ArrayList`、`LinkedList` +- [Java 容器之 Map](Java容器之Map.md) - 关键词:`Map`、`HashMap`、`TreeMap`、`LinkedHashMap`、`WeakHashMap` +- [Java 容器之 Set](Java容器之Set.md) - 关键词:`Set`、`HashSet`、`TreeSet`、`LinkedHashSet`、`EmumSet` +- [Java 容器之 Queue](Java容器之Queue.md) - 关键词:`Queue`、`Deque`、`ArrayDeque`、`LinkedList`、`PriorityQueue` +- [Java 容器之 Stream](Java容器之Stream.md) + +## 📚 资料 + +- **书籍** + - Java 四大名著 + - [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) + - [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) + - [《Java 核心技术 卷 II 高级特性》](https://book.douban.com/subject/27165931/) + - [《Effective Java》](https://book.douban.com/subject/30412517/) + - Java 入门 + - [《O'Reilly:Head First Java》](https://book.douban.com/subject/2000732/) + - [《Java 从入门到精通》](https://item.jd.com/12555860.html) + - [《疯狂 Java 讲义》](https://book.douban.com/subject/3246499/) +- **教程、社区** + - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) + - [java-design-patterns](https://github.com/iluwatar/java-design-patterns) + - [Java](https://github.com/TheAlgorithms/Java) + - [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) + - [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) + - [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) + - [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) + - [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) +- **面试** + - [CS-Notes](https://github.com/CyC2018/CS-Notes) + - [JavaGuide](https://github.com/Snailclimb/JavaGuide) + - [advanced-java](https://github.com/doocs/advanced-java) + +## 🚪 传送 + +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore/) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213BIO.md" "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213BIO.md" new file mode 100644 index 0000000000..d53ed45a9e --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213BIO.md" @@ -0,0 +1,1016 @@ +--- +title: Java I/O 之工具类 +date: 2020-06-30 21:34:59 +order: 05 +categories: + - Java + - JavaCore + - IO +tags: + - Java + - JavaCore + - IO + - BIO + - InputStream + - OutputStream + - Reader + - Writer +permalink: /pages/a4dba16e/ +--- + +# Java I/O 之 BIO + +## BIO + +BIO(blocking IO) 即阻塞 IO。指的主要是传统的 `java.io` 包,它基于流模型实现。流从概念上来说是一个连续的数据流。当程序需要读数据的时候就需要使用输入流读取数据,当需要往外写数据的时候就需要输出流。 + +`java.io` 包提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。很多时候,人们也把 java.net 下面提供的部分网络 API,比如 `Socket`、`ServerSocket`、`HttpURLConnection` 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。 + +BIO 中操作的流主要有两大类,字节流和字符流,两类根据流的方向都可以分为输入流和输出流。 + +- **字节流** + - 输入字节流:`InputStream` + - 输出字节流:`OutputStream` +- **字符流** + - 输入字符流:`Reader` + - 输出字符流:`Writer` + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200219130627.png) + +### 字节流 + +字节流主要操作字节数据或二进制对象。 + +字节流有两个核心抽象类:`InputStream` 和 `OutputStream`。所有的字节流类都继承自这两个抽象类。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200219133627.png) + +#### InputStream + +`InputStream`用于从源头(通常是文件)读取数据(字节信息)到内存中,`java.io.InputStream`抽象类是所有字节输入流的父类。 + +`InputStream` 常用方法: + +- `read()`:返回输入流中下一个字节的数据。返回的值介于 0 到 255 之间。如果未读取任何字节,则代码返回 `-1` ,表示文件结束。 +- `read(byte b[ ])` : 从输入流中读取一些字节存储到数组 `b` 中。如果数组 `b` 的长度为零,则不读取。如果没有可用字节读取,返回 `-1`。如果有可用字节读取,则最多读取的字节数最多等于 `b.length` , 返回读取的字节数。这个方法等价于 `read(b, 0, b.length)`。 +- `read(byte b[], int off, int len)`:在`read(byte b[ ])` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 +- `skip(long n)`:忽略输入流中的 n 个字节 , 返回实际忽略的字节数。 +- `available()`:返回输入流中可以读取的字节数。 +- `close()`:关闭输入流释放相关的系统资源。 + +从 Java 9 开始,`InputStream` 新增加了多个实用的方法: + +- `readAllBytes()`:读取输入流中的所有字节,返回字节数组。 +- `readNBytes(byte[] b, int off, int len)`:阻塞直到读取 `len` 个字节。 +- `transferTo(OutputStream out)`:将所有字节从一个输入流传递到一个输出流。 + +#### OutputStream + +`OutputStream` 用于将数据(字节信息)写入到目的地(通常是文件),`java.io.OutputStream`抽象类是所有字节输出流的父类。 + +`OutputStream` 常用方法: + +- `write(int b)`:将特定字节写入输出流。 +- `write(byte b[ ])` : 将数组`b` 写入到输出流,等价于 `write(b, 0, b.length)` 。 +- `write(byte[] b, int off, int len)` : 在`write(byte b[ ])` 方法的基础上增加了 `off` 参数(偏移量)和 `len` 参数(要读取的最大字节数)。 +- `flush()`:刷新此输出流并强制写出所有缓冲的输出字节。 +- `close()`:关闭输出流释放相关的系统资源。 + +#### 文件字节流 + +`FileOutputStream` 和 `FileInputStream` 提供了读写字节到文件的能力。 + +文件流操作一般步骤: + +1. 使用 `File` 类绑定一个文件。 +2. 把 `File` 对象绑定到流对象上。 +3. 进行读或写操作。 +4. 关闭流 + +`FileOutputStream` 和 `FileInputStream` 示例: + +```java +public class FileStreamDemo { + + private static final String FILEPATH = "temp.log"; + + public static void main(String[] args) throws Exception { + write(FILEPATH); + read(FILEPATH); + } + + public static void write(String filepath) throws IOException { + // 第 1 步、使用 File 类找到一个文件 + File f = new File(filepath); + + // 第 2 步、通过子类实例化父类对象 + OutputStream out = new FileOutputStream(f); + // 实例化时,默认为覆盖原文件内容方式;如果添加 true 参数,则变为对原文件追加内容的方式。 + // OutputStream out = new FileOutputStream(f, true); + + // 第 3 步、进行写操作 + String str = "Hello World\n"; + byte[] bytes = str.getBytes(); + out.write(bytes); + + // 第 4 步、关闭输出流 + out.close(); + } + + public static void read(String filepath) throws IOException { + // 第 1 步、使用 File 类找到一个文件 + File f = new File(filepath); + + // 第 2 步、通过子类实例化父类对象 + InputStream input = new FileInputStream(f); + + // 第 3 步、进行读操作 + // 有三种读取方式,体会其差异 + byte[] bytes = new byte[(int) f.length()]; + int len = input.read(bytes); // 读取内容 + System.out.println("读入数据的长度:" + len); + + // 第 4 步、关闭输入流 + input.close(); + System.out.println("内容为:\n" + new String(bytes)); + } + +} +``` + +#### 内存字节流 + +`ByteArrayInputStream` 和 `ByteArrayOutputStream` 是用来完成内存的输入和输出功能。 + +内存操作流一般在生成一些临时信息时才使用。 如果临时信息保存在文件中,还需要在有效期过后删除文件,这样比较麻烦。 + +`ByteArrayInputStream` 和 `ByteArrayOutputStream` 示例: + +```java +public class ByteArrayStreamDemo { + + public static void main(String[] args) { + String str = "HELLOWORLD"; // 定义一个字符串,全部由大写字母组成 + ByteArrayInputStream bis = new ByteArrayInputStream(str.getBytes()); + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + // 准备从内存 ByteArrayInputStream 中读取内容 + int temp = 0; + while ((temp = bis.read()) != -1) { + char c = (char) temp; // 读取的数字变为字符 + bos.write(Character.toLowerCase(c)); // 将字符变为小写 + } + // 所有的数据就全部都在 ByteArrayOutputStream 中 + String newStr = bos.toString(); // 取出内容 + try { + bis.close(); + bos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + System.out.println(newStr); + } + +} +``` + +#### 管道流 + +管道流的主要作用是可以进行两个线程间的通信。 + +如果要进行管道通信,则必须把 `PipedOutputStream` 连接在 `PipedInputStream` 上。为此,`PipedOutputStream` 中提供了 `connect()` 方法。 + +```java +public class PipedStreamDemo { + + public static void main(String[] args) { + Send s = new Send(); + Receive r = new Receive(); + try { + s.getPos().connect(r.getPis()); // 连接管道 + } catch (IOException e) { + e.printStackTrace(); + } + new Thread(s).start(); // 启动线程 + new Thread(r).start(); // 启动线程 + } + + static class Send implements Runnable { + + private PipedOutputStream pos = null; + + Send() { + pos = new PipedOutputStream(); // 实例化输出流 + } + + @Override + public void run() { + String str = "Hello World!!!"; + try { + pos.write(str.getBytes()); + } catch (IOException e) { + e.printStackTrace(); + } + try { + pos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * 得到此线程的管道输出流 + */ + PipedOutputStream getPos() { + return pos; + } + + } + + static class Receive implements Runnable { + + private PipedInputStream pis = null; + + Receive() { + pis = new PipedInputStream(); + } + + @Override + public void run() { + byte[] b = new byte[1024]; + int len = 0; + try { + len = pis.read(b); + } catch (IOException e) { + e.printStackTrace(); + } + try { + pis.close(); + } catch (IOException e) { + e.printStackTrace(); + } + System.out.println("接收的内容为:" + new String(b, 0, len)); + } + + /** + * 得到此线程的管道输入流 + */ + PipedInputStream getPis() { + return pis; + } + + } + +} +``` + +#### 对象字节流 + +**ObjectInputStream 和 ObjectOutputStream 是对象输入输出流,一般用于对象序列化。** + +这里不展开叙述,想了解详细内容和示例可以参考:[Java 序列化](03.Java 序列化。md) + +#### 数据操作流 + +数据操作流提供了格式化读入和输出数据的方法,分别为 `DataInputStream` 和 `DataOutputStream`。 + +`DataInputStream` 和 `DataOutputStream` 格式化读写数据示例: + +```java +public class DataStreamDemo { + + public static final String FILEPATH = "temp.log"; + + public static void main(String[] args) throws IOException { + write(FILEPATH); + read(FILEPATH); + } + + private static void write(String filepath) throws IOException { + // 1. 使用 File 类绑定一个文件 + File f = new File(filepath); + + // 2. 把 File 对象绑定到流对象上 + DataOutputStream dos = new DataOutputStream(new FileOutputStream(f)); + + // 3. 进行读或写操作 + String[] names = { "衬衣", "手套", "围巾" }; + float[] prices = { 98.3f, 30.3f, 50.5f }; + int[] nums = { 3, 2, 1 }; + for (int i = 0; i < names.length; i++) { + dos.writeChars(names[i]); + dos.writeChar('\t'); + dos.writeFloat(prices[i]); + dos.writeChar('\t'); + dos.writeInt(nums[i]); + dos.writeChar('\n'); + } + + // 4. 关闭流 + dos.close(); + } + + private static void read(String filepath) throws IOException { + // 1. 使用 File 类绑定一个文件 + File f = new File(filepath); + + // 2. 把 File 对象绑定到流对象上 + DataInputStream dis = new DataInputStream(new FileInputStream(f)); + + // 3. 进行读或写操作 + String name = null; // 接收名称 + float price = 0.0f; // 接收价格 + int num = 0; // 接收数量 + char[] temp = null; // 接收商品名称 + int len = 0; // 保存读取数据的个数 + char c = 0; // '\u0000' + try { + while (true) { + temp = new char[200]; // 开辟空间 + len = 0; + while ((c = dis.readChar()) != '\t') { // 接收内容 + temp[len] = c; + len++; // 读取长度加 1 + } + name = new String(temp, 0, len); // 将字符数组变为 String + price = dis.readFloat(); // 读取价格 + dis.readChar(); // 读取、t + num = dis.readInt(); // 读取 int + dis.readChar(); // 读取、n + System.out.printf("名称:%s;价格:%5.2f;数量:%d\n", name, price, num); + } + } catch (EOFException e) { + System.out.println("结束"); + } catch (IOException e) { + e.printStackTrace(); + } + + // 4. 关闭流 + dis.close(); + } + +} +``` + +#### 合并流 + +合并流的主要功能是将多个 `InputStream` 合并为一个 `InputStream` 流。合并流的功能由 `SequenceInputStream` 完成。 + +```java +public class SequenceInputStreamDemo { + + public static void main(String[] args) throws Exception { + + InputStream is1 = new FileInputStream("temp1.log"); + InputStream is2 = new FileInputStream("temp2.log"); + SequenceInputStream sis = new SequenceInputStream(is1, is2); + + int temp = 0; // 接收内容 + OutputStream os = new FileOutputStream("temp3.logt"); + while ((temp = sis.read()) != -1) { // 循环输出 + os.write(temp); // 保存内容 + } + + sis.close(); // 关闭合并流 + is1.close(); // 关闭输入流 1 + is2.close(); // 关闭输入流 2 + os.close(); // 关闭输出流 + } + +} +``` + +### 字符流 + +字符流主要操作字符,一般用于处理文本数据。 + +字符流有两个核心类:`Reader` 类和 `Writer` 。所有的字符流类都继承自这两个抽象类。 + +#### 文件字符流 + +文件字符流 `FileReader` 和 `FileWriter` 可以向文件读写文本数据。 + +`FileReader` 和 `FileWriter` 读写文件示例: + +```java +public class FileReadWriteDemo { + + private static final String FILEPATH = "temp.log"; + + public static void main(String[] args) throws IOException { + write(FILEPATH); + System.out.println("内容为:" + new String(read(FILEPATH))); + } + + public static void write(String filepath) throws IOException { + // 1. 使用 File 类绑定一个文件 + File f = new File(filepath); + + // 2. 把 File 对象绑定到流对象上 + Writer out = new FileWriter(f); + // Writer out = new FileWriter(f, true); // 追加内容方式 + + // 3. 进行读或写操作 + String str = "Hello World!!!\r\n"; + out.write(str); + + // 4. 关闭流 + // 字符流操作时使用了缓冲区,并在关闭字符流时会强制将缓冲区内容输出 + // 如果不关闭流,则缓冲区的内容是无法输出的 + // 如果想在不关闭流时,将缓冲区内容输出,可以使用 flush 强制清空缓冲区 + out.flush(); + out.close(); + } + + public static char[] read(String filepath) throws IOException { + // 1. 使用 File 类绑定一个文件 + File f = new File(filepath); + + // 2. 把 File 对象绑定到流对象上 + Reader input = new FileReader(f); + + // 3. 进行读或写操作 + int temp = 0; // 接收每一个内容 + int len = 0; // 读取内容 + char[] c = new char[1024]; + while ((temp = input.read()) != -1) { + // 如果不是-1 就表示还有内容,可以继续读取 + c[len] = (char) temp; + len++; + } + System.out.println("文件字符数为:" + len); + + // 4. 关闭流 + input.close(); + + return c; + } + +} +``` + +#### 字节流转换字符流 + +我们可以在程序中通过 `InputStream` 和 `Reader` 从数据源中读取数据,然后也可以在程序中将数据通过 `OutputStream` 和 `Writer` 输出到目标媒介中 + +使用 `InputStreamReader` 可以将输入字节流转化为输入字符流;使用`OutputStreamWriter`可以将输出字节流转化为输出字符流。 + +`OutputStreamWriter` 示例: + +```java +public class OutputStreamWriterDemo { + + public static void main(String[] args) throws IOException { + File f = new File("temp.log"); + Writer out = new OutputStreamWriter(new FileOutputStream(f)); + out.write("hello world!!"); + out.close(); + } + +} +``` + +`InputStreamReader` 示例: + +```java +public class InputStreamReaderDemo { + + public static void main(String[] args) throws IOException { + File f = new File("temp.log"); + Reader reader = new InputStreamReader(new FileInputStream(f)); + char[] c = new char[1024]; + int len = reader.read(c); + reader.close(); + System.out.println(new String(c, 0, len)); + } + +} +``` + +### 字节流 vs. 字符流 + +相同点: + +字节流和字符流都有 `read()`、`write()`、`flush()`、`close()` 这样的方法,这决定了它们的操作方式近似。 + +不同点: + +- **数据类型** + - 字节流的数据是字节(二进制对象)。主要核心类是 `InputStream` 类和 `OutputStream` 类。 + - 字符流的数据是字符。主要核心类是 `Reader` 类和 `Writer` 类。 +- **缓冲区** + - 字节流在操作时本身不会用到缓冲区(内存),是文件直接操作的。 + - 字符流在操作时是使用了缓冲区,通过缓冲区再操作文件。 + +选择: + +所有的文件在硬盘或传输时都是以字节方式保存的,例如图片,影音文件等都是按字节方式存储的。字符流无法读写这些文件。 + +所以,除了纯文本数据文件使用字符流以外,其他文件类型都应该使用字节流方式。 + +## I/O 工具类 + +### File + +`File` 类是 `java.io` 包中唯一对文件本身进行操作的类。它可以对文件、目录进行增删查操作。 + +#### createNewFille + +**可以使用 `createNewFille()` 方法创建一个新文件**。 + +注: + +Windows 中使用反斜杠表示目录的分隔符 `\`。~~~~~~~~ + +Linux 中使用正斜杠表示目录的分隔符 `/`。 + +最好的做法是使用 `File.separator` 静态常量,可以根据所在操作系统选取对应的分隔符。 + +【示例】创建文件 + +```java +File f = new File(filename); +boolean flag = f.createNewFile(); +``` + +#### mkdir + +**可以使用 `mkdir()` 来创建文件夹**,但是如果要创建的目录的父路径不存在,则无法创建成功。 + +如果要解决这个问题,可以使用 `mkdirs()`,当父路径不存在时,会连同上级目录都一并创建。 + +【示例】创建目录 + +```java +File f = new File(filename); +boolean flag = f.mkdir(); +``` + +#### delete + +**可以使用 `delete()` 来删除文件或目录**。 + +需要注意的是,如果删除的是目录,且目录不为空,直接用 `delete()` 删除会失败。 + +【示例】删除文件或目录 + +```java +File f = new File(filename); +boolean flag = f.delete(); +``` + +#### list 和 listFiles + +`File` 中给出了两种列出文件夹内容的方法: + +- **`list()`: 列出全部名称,返回一个字符串数组**。 +- **`listFiles()`: 列出完整的路径,返回一个 `File` 对象数组**。 + +`list()` 示例: + +```java +File f = new File(filename); +String str[] = f.list(); +``` + +`listFiles()` 示例: + +```java +File f = new File(filename); +File files[] = f.listFiles(); +``` + +### RandomAccessFile + +> 注:`RandomAccessFile` 类虽然可以实现对文件内容的读写操作,但是比较复杂。所以一般操作文件内容往往会使用字节流或字符流方式。 + +`RandomAccessFile` 类是随机读取类,它是一个完全独立的类。 + +它适用于由大小已知的记录组成的文件,所以我们可以使用 `seek()` 将记录从一处转移到另一处,然后读取或者修改记录。 + +文件中记录的大小不一定都相同,只要能够确定哪些记录有多大以及它们在文件中的位置即可。 + +#### RandomAccessFile 写操作 + +当用 `rw` 方式声明 `RandomAccessFile` 对象时,如果要写入的文件不存在,系统将自行创建。 + +`r` 为只读;`w` 为只写;`rw` 为读写。 + +【示例】文件随机读写 + +```java +public class RandomAccessFileDemo01 { + + public static void main(String args[]) throws IOException { + File f = new File("d:" + File.separator + "test.txt"); // 指定要操作的文件 + RandomAccessFile rdf = null; // 声明 RandomAccessFile 类的对象 + rdf = new RandomAccessFile(f, "rw");// 读写模式,如果文件不存在,会自动创建 + String name = null; + int age = 0; + name = "zhangsan"; // 字符串长度为 8 + age = 30; // 数字的长度为 4 + rdf.writeBytes(name); // 将姓名写入文件之中 + rdf.writeInt(age); // 将年龄写入文件之中 + name = "lisi "; // 字符串长度为 8 + age = 31; // 数字的长度为 4 + rdf.writeBytes(name); // 将姓名写入文件之中 + rdf.writeInt(age); // 将年龄写入文件之中 + name = "wangwu "; // 字符串长度为 8 + age = 32; // 数字的长度为 4 + rdf.writeBytes(name); // 将姓名写入文件之中 + rdf.writeInt(age); // 将年龄写入文件之中 + rdf.close(); // 关闭 + } +} +``` + +#### RandomAccessFile 读操作 + +读取是直接使用 `r` 的模式即可,以只读的方式打开文件。 + +读取时所有的字符串只能按照 byte 数组方式读取出来,而且长度必须和写入时的固定大小相匹配。 + +```java +public class RandomAccessFileDemo02 { + + public static void main(String args[]) throws IOException { + File f = new File("d:" + File.separator + "test.txt"); // 指定要操作的文件 + RandomAccessFile rdf = null; // 声明 RandomAccessFile 类的对象 + rdf = new RandomAccessFile(f, "r");// 以只读的方式打开文件 + String name = null; + int age = 0; + byte b[] = new byte[8]; // 开辟 byte 数组 + // 读取第二个人的信息,意味着要空出第一个人的信息 + rdf.skipBytes(12); // 跳过第一个人的信息 + for (int i = 0; i < b.length; i++) { + b[i] = rdf.readByte(); // 读取一个字节 + } + name = new String(b); // 将读取出来的 byte 数组变为字符串 + age = rdf.readInt(); // 读取数字 + System.out.println("第二个人的信息 --> 姓名:" + name + ";年龄:" + age); + // 读取第一个人的信息 + rdf.seek(0); // 指针回到文件的开头 + for (int i = 0; i < b.length; i++) { + b[i] = rdf.readByte(); // 读取一个字节 + } + name = new String(b); // 将读取出来的 byte 数组变为字符串 + age = rdf.readInt(); // 读取数字 + System.out.println("第一个人的信息 --> 姓名:" + name + ";年龄:" + age); + rdf.skipBytes(12); // 空出第二个人的信息 + for (int i = 0; i < b.length; i++) { + b[i] = rdf.readByte(); // 读取一个字节 + } + name = new String(b); // 将读取出来的 byte 数组变为字符串 + age = rdf.readInt(); // 读取数字 + System.out.println("第三个人的信息 --> 姓名:" + name + ";年龄:" + age); + rdf.close(); // 关闭 + } +} +``` + +### System + +`System` 类中提供了大量的静态方法,可以获取系统相关的信息或系统级操作,其中提供了三个常用于 IO 的静态成员: + +- `System.out` - 一个 PrintStream 流。System.out 一般会把你写到其中的数据输出到控制台上。System.out 通常仅用在类似命令行工具的控制台程序上。System.out 也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。 +- `System.err` - 一个 PrintStream 流。System.err 与 System.out 的运行方式类似,但它更多的是用于打印错误文本。一些类似 Eclipse 的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过 System.err 输出到控制台上。 +- `System.in` - 一个典型的连接控制台程序和键盘输入的 InputStream 流。通常当数据通过命令行参数或者配置文件传递给命令行 Java 程序的时候,System.in 并不是很常用。图形界面程序通过界面传递参数给程序,这是一块单独的 Java IO 输入机制。 + +【示例】重定向 `System.out` 输出流 + +```java +import java.io.*; +public class SystemOutDemo { + + public static void main(String args[]) throws Exception { + OutputStream out = new FileOutputStream("d:\\test.txt"); + PrintStream ps = new PrintStream(out); + System.setOut(ps); + System.out.println("人生若只如初见,何事秋风悲画扇"); + ps.close(); + out.close(); + } +} +``` + +【示例】重定向 `System.err` 输出流 + +```java +public class SystemErrDemo { + + public static void main(String args[]) throws IOException { + OutputStream bos = new ByteArrayOutputStream(); // 实例化 + PrintStream ps = new PrintStream(bos); // 实例化 + System.setErr(ps); // 输出重定向 + System.err.print("此处有误"); + System.out.println(bos); // 输出内存中的数据 + } +} +``` + +【示例】`System.in` 接受控制台输入信息 + +```java +import java.io.*; +public class SystemInDemo { + + public static void main(String args[]) throws IOException { + InputStream input = System.in; + StringBuffer buf = new StringBuffer(); + System.out.print("请输入内容:"); + int temp = 0; + while ((temp = input.read()) != -1) { + char c = (char) temp; + if (c == '\n') { + break; + } + buf.append(c); + } + System.out.println("输入的内容为:" + buf); + input.close(); + } +} +``` + +### Scanner + +**`Scanner` 可以获取用户的输入,并对数据进行校验**。 + +【示例】校验输入数据是否格式正确 + +```java +import java.io.*; +public class ScannerDemo { + + public static void main(String args[]) { + Scanner scan = new Scanner(System.in); // 从键盘接收数据 + int i = 0; + float f = 0.0f; + System.out.print("输入整数:"); + if (scan.hasNextInt()) { // 判断输入的是否是整数 + i = scan.nextInt(); // 接收整数 + System.out.println("整数数据:" + i); + } else { + System.out.println("输入的不是整数!"); + } + + System.out.print("输入小数:"); + if (scan.hasNextFloat()) { // 判断输入的是否是小数 + f = scan.nextFloat(); // 接收小数 + System.out.println("小数数据:" + f); + } else { + System.out.println("输入的不是小数!"); + } + + Date date = null; + String str = null; + System.out.print("输入日期(yyyy-MM-dd):"); + if (scan.hasNext("^\\d{4}-\\d{2}-\\d{2}$")) { // 判断 + str = scan.next("^\\d{4}-\\d{2}-\\d{2}$"); // 接收 + try { + date = new SimpleDateFormat("yyyy-MM-dd").parse(str); + } catch (Exception e) {} + } else { + System.out.println("输入的日期格式错误!"); + } + System.out.println(date); + } +} +``` + +输出: + +``` +输入整数:20 +整数数据:20 +输入小数:3.2 +小数数据:3.2 +输入日期(yyyy-MM-dd):1988-13-1 +输入的日期格式错误! +null +``` + +## 网络编程 + +> **_关键词:`Socket`、`ServerSocket`、`DatagramPacket`、`DatagramSocket`_** +> +> 网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。 +> +> `java.net` 包中提供了低层次的网络通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。 +> +> java.net 包中提供了两种常见的网络协议的支持: +> +> - **TCP** - TCP 是传输控制协议的缩写,它保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP/ IP。 +> - **UDP** - UDP 是用户数据报协议的缩写,一个无连接的协议。提供了应用程序之间要发送的数据的数据包。 + +### Socket 和 ServerSocket + +套接字(Socket)使用 TCP 提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。 + +**Java 通过 Socket 和 ServerSocket 实现对 TCP 的支持**。Java 中的 Socket 通信可以简单理解为:**`java.net.Socket` 代表客户端,`java.net.ServerSocket` 代表服务端**,二者可以建立连接,然后通信。 + +以下为 Socket 通信中建立建立的基本流程: + +- 服务器实例化一个 `ServerSocket` 对象,表示服务器绑定一个端口。 +- 服务器调用 `ServerSocket` 的 `accept()` 方法,该方法将一直等待,直到客户端连接到服务器的绑定端口(即监听端口)。 +- 服务器监听端口时,客户端实例化一个 `Socket` 对象,指定服务器名称和端口号来请求连接。 +- `Socket` 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。 +- 在服务器端,`accept()` 方法返回服务器上一个新的 `Socket` 引用,该引用连接到客户端的 `Socket` 。 + +连接建立后,可以通过使用 IO 流进行通信。每一个 `Socket` 都有一个输出流和一个输入流。客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。 + +TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送,以下是一些类提供的一套完整的有用的方法来实现 sockets。 + +#### ServerSocket + +服务器程序通过使用 `java.net.ServerSocket` 类以获取一个端口,并且监听客户端请求连接此端口的请求。 + +##### ServerSocket 构造方法 + +`ServerSocket` 有多个构造方法: + +| **方法** | **描述** | +| ---------------------------------------------------------- | ------------------------------------------------------------------- | +| `ServerSocket()` | 创建非绑定服务器套接字。 | +| `ServerSocket(int port)` | 创建绑定到特定端口的服务器套接字。 | +| `ServerSocket(int port, int backlog)` | 利用指定的 `backlog` 创建服务器套接字并将其绑定到指定的本地端口号。 | +| `ServerSocket(int port, int backlog, InetAddress address)` | 使用指定的端口、监听 `backlog` 和要绑定到的本地 IP 地址创建服务器。 | + +##### ServerSocket 常用方法 + +创建非绑定服务器套接字。 如果 `ServerSocket` 构造方法没有抛出异常,就意味着你的应用程序已经成功绑定到指定的端口,并且侦听客户端请求。 + +这里有一些 `ServerSocket` 类的常用方法: + +| **方法** | **描述** | +| -------------------------------------------- | ----------------------------------------------------- | +| `int getLocalPort()` | 返回此套接字在其上侦听的端口。 | +| `Socket accept()` | 监听并接受到此套接字的连接。 | +| `void setSoTimeout(int timeout)` | 通过指定超时值启用/禁用 `SO_TIMEOUT`,以毫秒为单位。 | +| `void bind(SocketAddress host, int backlog)` | 将 `ServerSocket` 绑定到特定地址(IP 地址和端口号)。 | + +#### Socket + +`java.net.Socket` 类代表客户端和服务器都用来互相沟通的套接字。客户端要获取一个 `Socket` 对象通过实例化 ,而 服务器获得一个 `Socket` 对象则通过 `accept()` 方法 a 的返回值。 + +##### Socket 构造方法 + +`Socket` 类有 5 个构造方法: + +| **方法** | **描述** | +| ----------------------------------------------------------------------------- | -------------------------------------------------------- | +| `Socket()` | 通过系统默认类型的 `SocketImpl` 创建未连接套接字 | +| `Socket(String host, int port)` | 创建一个流套接字并将其连接到指定主机上的指定端口号。 | +| `Socket(InetAddress host, int port)` | 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。 | +| `Socket(String host, int port, InetAddress localAddress, int localPort)` | 创建一个套接字并将其连接到指定远程主机上的指定远程端口。 | +| `Socket(InetAddress host, int port, InetAddress localAddress, int localPort)` | 创建一个套接字并将其连接到指定远程地址上的指定远程端口。 | + +当 Socket 构造方法返回,并没有简单的实例化了一个 Socket 对象,它实际上会尝试连接到指定的服务器和端口。 + +##### Socket 常用方法 + +下面列出了一些感兴趣的方法,注意客户端和服务器端都有一个 Socket 对象,所以无论客户端还是服务端都能够调用这些方法。 + +| **方法** | **描述** | +| ----------------------------------------------- | ----------------------------------------------------- | +| `void connect(SocketAddress host, int timeout)` | 将此套接字连接到服务器,并指定一个超时值。 | +| `InetAddress getInetAddress()` | 返回套接字连接的地址。 | +| `int getPort()` | 返回此套接字连接到的远程端口。 | +| `int getLocalPort()` | 返回此套接字绑定到的本地端口。 | +| `SocketAddress getRemoteSocketAddress()` | 返回此套接字连接的端点的地址,如果未连接则返回 null。 | +| `InputStream getInputStream()` | 返回此套接字的输入流。 | +| `OutputStream getOutputStream()` | 返回此套接字的输出流。 | +| `void close()` | 关闭此套接字。 | + +#### Socket 通信示例 + +服务端示例: + +```java +public class HelloServer { + + public static void main(String[] args) throws Exception { + // Socket 服务端 + // 服务器在 8888 端口上监听 + ServerSocket server = new ServerSocket(8888); + System.out.println("服务器运行中,等待客户端连接。"); + // 得到连接,程序进入到阻塞状态 + Socket client = server.accept(); + // 打印流输出最方便 + PrintStream out = new PrintStream(client.getOutputStream()); + // 向客户端输出信息 + out.println("hello world"); + client.close(); + server.close(); + System.out.println("服务器已向客户端发送消息,退出。"); + } + +} +``` + +客户端示例: + +```java +public class HelloClient { + + public static void main(String[] args) throws Exception { + // Socket 客户端 + Socket client = new Socket("localhost", 8888); + InputStreamReader inputStreamReader = new InputStreamReader(client.getInputStream()); + // 一次性接收完成 + BufferedReader buf = new BufferedReader(inputStreamReader); + String str = buf.readLine(); + buf.close(); + client.close(); + System.out.println("客户端接收到服务器消息:" + str + ",退出"); + } + +} +``` + +### DatagramSocket 和 DatagramPacket + +Java 通过 `DatagramSocket` 和 `DatagramPacket` 实现对 UDP 协议的支持。 + +- `DatagramPacket`:数据包类 +- `DatagramSocket`:通信类 + +UDP 服务端示例: + +```java +public class UDPServer { + + public static void main(String[] args) throws Exception { // 所有异常抛出 + String str = "hello World!!!"; + DatagramSocket ds = new DatagramSocket(3000); // 服务端在 3000 端口上等待服务器发送信息 + DatagramPacket dp = + new DatagramPacket(str.getBytes(), str.length(), InetAddress.getByName("localhost"), 9000); // 所有的信息使用 buf 保存 + System.out.println("发送信息。"); + ds.send(dp); // 发送信息出去 + ds.close(); + } + +} +``` + +UDP 客户端示例: + +```java +public class UDPClient { + + public static void main(String[] args) throws Exception { // 所有异常抛出 + byte[] buf = new byte[1024]; // 开辟空间,以接收数据 + DatagramSocket ds = new DatagramSocket(9000); // 客户端在 9000 端口上等待服务器发送信息 + DatagramPacket dp = new DatagramPacket(buf, 1024); // 所有的信息使用 buf 保存 + ds.receive(dp); // 接收数据 + String str = new String(dp.getData(), 0, dp.getLength()) + "from " + dp.getAddress().getHostAddress() + ":" + + dp.getPort(); + System.out.println(str); // 输出内容 + } + +} +``` + +### InetAddress + +`InetAddress` 类表示互联网协议 (IP) 地址。 + +没有公有的构造函数,只能通过静态方法来创建实例。 + +```java +InetAddress.getByName(String host); +InetAddress.getByAddress(byte[] address); +``` + +### URL + +可以直接从 URL 中读取字节流数据。 + +```java +public static void main(String[] args) throws IOException { + + URL url = new URL("http://www.baidu.com"); + + /* 字节流 */ + InputStream is = url.openStream(); + + /* 字符流 */ + InputStreamReader isr = new InputStreamReader(is, "utf-8"); + + /* 提供缓存功能 */ + BufferedReader br = new BufferedReader(isr); + + String line; + while ((line = br.readLine()) != null) { + System.out.println(line); + } + + br.close(); +} +``` + +## 参考资料 + +- [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) +- [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) +- [System 官方 API 手册](https://docs.oracle.com/javase/7/docs/api/java/lang/System.html) +- [Java 网络编程](https://www.runoob.com/java/java-networking.html) diff --git a/source/_posts/01.Java/01.JavaSE/04.IO/02.JavaNIO.md "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213NIO.md" similarity index 77% rename from source/_posts/01.Java/01.JavaSE/04.IO/02.JavaNIO.md rename to "source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213NIO.md" index 2002864db1..8c06cb18d1 100644 --- a/source/_posts/01.Java/01.JavaSE/04.IO/02.JavaNIO.md +++ "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213NIO.md" @@ -4,57 +4,31 @@ date: 2020-02-19 18:54:21 order: 02 categories: - Java - - JavaSE + - JavaCore - IO tags: - Java - - JavaSE + - JavaCore - IO - NIO -permalink: /pages/6912a8/ + - Channel + - Buffer + - Selector + - 多路复用 +permalink: /pages/291c7061/ --- # Java NIO -> 关键词:`Channel`、`Buffer`、`Selector`、`非阻塞`、`多路复用` - ## NIO 简介 -NIO 是一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入了 NIO 框架,对应 `java.nio` 包,提供了 `Channel` 、`Selector`、`Buffer` 等抽象。 - -NIO 中的 N 可以理解为 Non-blocking,不单纯是 New。它支持面向缓冲的,基于通道的 I/O 操作方法。 NIO 提供了与传统 BIO 模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 - -### NIO 和 BIO 的区别 - -#### Non-blocking IO(非阻塞) - -**BIO 是阻塞的,NIO 是非阻塞的**。 - -BIO 的各种流是阻塞的。这意味着,当一个线程调用 `read()` 或 `write()` 时,该线程被阻塞,直到有一些数据被读取,或数据完全写入。在此期间,该线程不能再干其他任何事。 - -NIO 使我们可以进行非阻塞 IO 操作。比如说,单线程中从通道读取数据到 buffer,同时可以继续做别的事情,当数据读取到 buffer 中后,线程再继续处理数据。写数据也是一样的。另外,非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。 - -#### Buffer(缓冲区) +在传统的 Java I/O 模型(BIO)中,I/O 操作是以阻塞的方式进行的。也就是说,当一个线程执行一个 I/O 操作时,它会被阻塞直到操作完成。这种阻塞模型在处理多个并发连接时可能会导致性能瓶颈,因为需要为每个连接创建一个线程,而线程的创建和切换都是有开销的。 -**BIO 面向流(Stream oriented),而 NIO 面向缓冲区(Buffer oriented)**。 +为了解决此问题,在 Java 1.4 中引入了非阻塞的 I/O 模型——NIO(New IO,也称为 Non-blocking IO)。NIO 对应 `java.nio` 包,提供了 `Channel` 、`Selector`、`Buffer` 等抽象。它支持面向缓冲的,基于通道的 I/O 操作方法。 -Buffer 是一个对象,它包含一些要写入或者要读出的数据。在 NIO 类库中加入 Buffer 对象,体现了 NIO 与 BIO 的一个重要区别。在面向流的 BIO 中可以将数据直接写入或者将数据直接读到 Stream 对象中。虽然 Stream 中也有 Buffer 开头的扩展类,但只是流的包装类,还是从流读到缓冲区,而 NIO 却是直接读到 Buffer 中进行操作。 +NIO 提供了与传统 BIO 模型中的 `Socket` 和 `ServerSocket` 相对应的 `SocketChannel` 和 `ServerSocketChannel` 两种不同的套接字通道实现,两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持一样,比较简单,但是性能和可靠性都不好;非阻塞模式正好与之相反。对于低负载、低并发的应用程序,可以使用同步阻塞 I/O 来提升开发速率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。 -在 NIO 厍中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读缓冲区中的数据; 在写入数据时,写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。 - -最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了 ByteBuffer,还有其他的一些缓冲区,事实上,每一种 Java 基本类型(除了 Boolean 类型)都对应有一种缓冲区。 - -#### Channel (通道) - -NIO 通过 Channel(通道) 进行读写。 - -通道是双向的,可读也可写,而流的读写是单向的。无论读写,通道只能和 Buffer 交互。因为 Buffer,通道可以异步地读写。 - -#### Selector (选择器) - -NIO 有选择器,而 IO 没有。 - -选择器用于使用单个线程处理多个通道。因此,它需要较少的线程来处理这些通道。线程之间的切换对于操作系统来说是昂贵的。 因此,为了提高系统效率选择器是有用的。 +> 注意:使用 NIO 并不一定意味着高性能,它的性能优势主要体现在高并发和高延迟的网络环境下。当连接数较少、并发程度较低或者网络传输速度较快时,NIO 的性能并不一定优于传统的 BIO 。 ### NIO 的基本流程 @@ -67,11 +41,11 @@ NIO 有选择器,而 IO 没有。 NIO 包含下面几个核心的组件: -- **Channel(通道)** -- **Buffer(缓冲区)** -- **Selector(选择器)** +- **Channel(通道)** - Channel 是一个双向的、可读可写的数据传输通道,NIO 通过 Channel 来实现数据的输入输出。通道是一个抽象的概念,它可以代表文件、套接字或者其他数据源之间的连接。 +- **Buffer(缓冲区)** - NIO 读写数据都是通过缓冲区进行操作的。读操作的时候将 Channel 中的数据填充到 Buffer 中,而写操作时将 Buffer 中的数据写入到 Channel 中。 +- **Selector(选择器)** - 允许一个线程处理多个 Channel,基于事件驱动的 I/O 多路复用模型。所有的 Channel 都可以注册到 Selector 上,由 Selector 来分配线程来处理事件。 -## Channel(通道) +## Channel(通道) 通道(`Channel`)是对 BIO 中的流的模拟,可以通过它读写数据。 @@ -91,15 +65,17 @@ File 或者 Socket,通常被认为是比较高层次的抽象,而 Channel - `SocketChannel`:通过 TCP 读写网络中数据; - `ServerSocketChannel`:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel。 -## Buffer(缓冲区) +## Buffer(缓冲区) + +**BIO 面向流 (Stream oriented),而 NIO 面向缓冲区 (Buffer oriented)**。 -NIO 与传统 I/O 不同,它是基于块(Block)的,它以块为基本单位处理数据。`Buffer` 是一块连续的内存块,是 NIO 读写数据的缓冲。`Buffer` 可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。 +在 NIO 中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读缓冲区中的数据;在写入数据时,写入到缓冲区中。任何时候访问 NIO 中的数据,都是通过缓冲区进行操作。 **向 `Channel` 读写的数据都必须先置于缓冲区中**。也就是说,不会直接对通道进行读写数据,而是要先经过缓冲区。缓冲区实质上是一个数组,但它不仅仅是一个数组。缓冲区提供了对数据的结构化访问,而且还可以跟踪系统的读/写进程。 BIO 和 NIO 已经很好地集成了,`java.io.*` 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,`java.io.*` 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。 -缓冲区包括以下类型: +事实上,每一种 Java 基本类型(除了 Boolean 类型)都对应有一种缓冲区: - `ByteBuffer` - `CharBuffer` @@ -177,7 +153,7 @@ NIO 还提供了一个可以直接访问物理内存的类 `DirectBuffer`。普 这里拓展一点,由于 `DirectBuffer` 申请的是非 JVM 的物理内存,所以创建和销毁的代价很高。`DirectBuffer` 申请的内存并不是直接由 JVM 负责垃圾回收,但在 `DirectBuffer` 包装类被回收时,会通过 Java 引用机制来释放该内存块。 -## Selector(选择器) +## Selector(选择器) NIO 常常被叫做非阻塞 IO,主要是因为 NIO 在网络通信中的非阻塞特性被广泛使用。 @@ -380,7 +356,7 @@ MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, 0, 1024); ## NIO vs. BIO -BIO 与 NIO 最重要的区别是数据打包和传输的方式:**BIO 以流的方式处理数据,而 NIO 以块的方式处理数据**。 +BIO 与 NIO 最重要的区别是数据打包和传输的方式。**BIO 面向流 (Stream oriented),而 NIO 面向缓冲区 (Buffer oriented)**。 - **面向流的 BIO 一次处理一个字节数据**:一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。 - **面向块的 NIO 一次处理一个数据块**,按块处理数据比按流处理数据要快得多。但是面向块的 NIO 缺少一些面向流的 BIO 所具有的优雅性和简单性。 @@ -398,4 +374,4 @@ NIO 模式: - [BIO,NIO,AIO 总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/BIO-NIO-AIO.md) - [Java NIO 浅析](https://zhuanlan.zhihu.com/p/23488863) - [JavaNIO Tutorial](http://tutorials.jenkov.com/java-nio/index.html) -- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html) \ No newline at end of file +- [IBM: NIO 入门](https://www.ibm.com/developerworks/cn/education/java/j-nio/j-nio.html) diff --git "a/source/_posts/01.Java/01.JavaSE/04.IO/03.Java\345\272\217\345\210\227\345\214\226.md" "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213\345\272\217\345\210\227\345\214\226.md" similarity index 94% rename from "source/_posts/01.Java/01.JavaSE/04.IO/03.Java\345\272\217\345\210\227\345\214\226.md" rename to "source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213\345\272\217\345\210\227\345\214\226.md" index 294464fb8e..8fd3dfc2e0 100644 --- "a/source/_posts/01.Java/01.JavaSE/04.IO/03.Java\345\272\217\345\210\227\345\214\226.md" +++ "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\344\271\213\345\272\217\345\210\227\345\214\226.md" @@ -4,20 +4,21 @@ date: 2019-05-09 19:06:05 order: 03 categories: - Java - - JavaSE + - JavaCore - IO tags: - Java - - JavaSE + - JavaCore - IO - 序列化 -permalink: /pages/2b2f0f/ + - Serializable + - Externalizable + - transient +permalink: /pages/dc9f1331/ --- # 深入理解 Java 序列化 -> **_关键词:`Serializable`、`serialVersionUID`、`transient`、`Externalizable`、`writeObject`、`readObject`_** - ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220626163533.png) ## 序列化简介 @@ -33,7 +34,7 @@ permalink: /pages/2b2f0f/ - 序列化可以将对象的字节序列持久化——保存在内存、文件、数据库中。 - 在网络上传送对象的字节序列。 -- RMI(远程方法调用) +- RMI(远程方法调用) ## JDK 序列化 @@ -55,7 +56,6 @@ public class SerializeDemo01 { FEMALE } - static class Person implements Serializable { private static final long serialVersionUID = 1L; private String name = null; @@ -462,7 +462,7 @@ public class SerializeDemo05 { ### JDK 序列化的问题 - **无法跨语言**:JDK 序列化目前只适用基于 Java 语言实现的框架,其它语言大部分都没有使用 Java 的序列化框架,也没有实现 JDK 序列化这套协议。因此,如果是两个基于不同语言编写的应用程序相互通信,则无法实现两个应用服务之间传输对象的序列化与反序列化。 -- **容易被攻击**:对象是通过在 `ObjectInputStream` 上调用 `readObject()` 方法进行反序列化的,它可以将类路径上几乎所有实现了 `Serializable` 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 `hashCode` 方法被调用次数呈次方爆发式增长, 从而引发栈溢出异常。例如下面这个案例就可以很好地说明。 +- **容易被攻击**:对象是通过在 `ObjectInputStream` 上调用 `readObject()` 方法进行反序列化的,它可以将类路径上几乎所有实现了 `Serializable` 接口的对象都实例化。这意味着,在反序列化字节流的过程中,该方法可以执行任意类型的代码,这是非常危险的。对于需要长时间进行反序列化的对象,不需要执行任何代码,也可以发起一次攻击。攻击者可以创建循环对象链,然后将序列化后的对象传输到程序中反序列化,这种情况会导致 `hashCode` 方法被调用次数呈次方爆发式增长,从而引发栈溢出异常。例如下面这个案例就可以很好地说明。 - **序列化后的流太大**:JDK 序列化中使用了 `ObjectOutputStream` 来实现对象转二进制编码,编码后的数组很大,非常影响存储和传输效率。 - **序列化性能太差**:Java 的序列化耗时比较大。序列化的速度也是体现序列化性能的重要指标,如果序列化的速度慢,就会影响网络通信的效率,从而增加系统的响应时间。 - **序列化编程限制**: @@ -542,7 +542,7 @@ Hessian 本身也有问题,官方版本对 Java 里面一些常见对象的类 ### JSON 是什么 -JSON 起源于 1999 年的 [JS 语言规范 ECMA262 的一个子集](http://javascript.crockford.com/)(即 15.12 章节描述了格式与解析),后来 2003 年作为一个数据格式[ECMA404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf)(很囧的序号有不有?)发布。 +JSON 起源于 1999 年的 [JS 语言规范 ECMA262 的一个子集](http://javascript.crockford.com/)(即 15.12 章节描述了格式与解析),后来 2003 年作为一个数据格式 [ECMA404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf)(很囧的序号有不有?)发布。 2006 年,作为 [rfc4627](http://www.ietf.org/rfc/rfc4627.txt) 发布,这时规范增加到 18 页,去掉没用的部分,十页不到。 JSON 的应用很广泛,这里有超过 100 种语言下的 JSON 库:[json.org](http://www.json.org/)。 @@ -591,7 +591,7 @@ JSON 优点 JSON 缺点 - 性能一般,文本表示的数据一般来说比二进制大得多,在数据传输上和解析处理上都要更影响性能。 -- 缺乏 schema,跟同是文本数据格式的 XML 比,在类型的严格性和丰富性上要差很多。XML 可以借由 XSD 或 DTD 来定义复杂的格式,并由此来验证 XML 文档是否符合格式要求,甚至进一步的,可以基于 XSD 来生成具体语言的操作代码,例如 apache xmlbeans。并且这些工具组合到一起,形成一套庞大的生态,例如基于 XML 可以实现 SOAP 和 WSDL,一系列的 ws-\*规范。但是我们也可以看到 JSON 在缺乏规范的情况下,实际上有更大一些的灵活性,特别是近年来 REST 的快速发展,已经有一些 schema 相关的发展(例如[理解 JSON Schema](https://spacetelescope.github.io/understanding-json-schema/index.html),[使用 JSON Schema](http://usingjsonschema.com/downloads/), [在线 schema 测试](http://azimi.me/json-schema-view/demo/demo.html)),也有类似于 WSDL 的[WADL](https://www.w3.org/Submission/wadl/)出现。 +- 缺乏 schema,跟同是文本数据格式的 XML 比,在类型的严格性和丰富性上要差很多。XML 可以借由 XSD 或 DTD 来定义复杂的格式,并由此来验证 XML 文档是否符合格式要求,甚至进一步的,可以基于 XSD 来生成具体语言的操作代码,例如 apache xmlbeans。并且这些工具组合到一起,形成一套庞大的生态,例如基于 XML 可以实现 SOAP 和 WSDL,一系列的 ws-\*规范。但是我们也可以看到 JSON 在缺乏规范的情况下,实际上有更大一些的灵活性,特别是近年来 REST 的快速发展,已经有一些 schema 相关的发展(例如 [理解 JSON Schema](https://spacetelescope.github.io/understanding-json-schema/index.html),[使用 JSON Schema](http://usingjsonschema.com/downloads/), [在线 schema 测试](http://azimi.me/json-schema-view/demo/demo.html)),也有类似于 WSDL 的 [WADL](https://www.w3.org/Submission/wadl/) 出现。 ### JSON 库 @@ -607,8 +607,8 @@ Java 中比较流行的 JSON 库有: > 遵循好的设计与编码风格,能提前解决 80%的问题,个人推荐 Google JSON 风格指南。 > -> - 英文版[Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml): -> - 中文版[Google JSON 风格指南](https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md): +> - 英文版 [Google JSON Style Guide](https://google.github.io/styleguide/jsoncstyleguide.xml): +> - 中文版 [Google JSON 风格指南](https://github.com/darcyliu/google-styleguide/blob/master/JSONStyleGuide.md): 简单摘录如下: @@ -624,15 +624,15 @@ Java 中比较流行的 JSON 库有: - 设计好通用的分页参数 - 设计好异常处理 -[JSON API](http://jsonapi.org.cn/format/)与 Google JSON 风格指南有很多可以相互参照之处。 +[JSON API](http://jsonapi.org.cn/format/) 与 Google JSON 风格指南有很多可以相互参照之处。 -[JSON API](http://jsonapi.org.cn/format/)是数据交互规范,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。 +[JSON API](http://jsonapi.org.cn/format/) 是数据交互规范,用以定义客户端如何获取与修改资源,以及服务器如何响应对应请求。 JSON API 设计用来最小化请求的数量,以及客户端与服务器间传输的数据量。在高效实现的同时,无需牺牲可读性、灵活性和可发现性。 ## 序列化技术选型 -市面上有如此多的序列化技术,那么我们在应用时如何选择呢? +市面上有如此多的序列化技术,那么我们在应用时如何选择呢? 序列化技术选型,需要考量的维度,根据重要性从高到低,依次有: @@ -654,9 +654,9 @@ JSON API 设计用来最小化请求的数量,以及客户端与服务器间 - [Java 编程思想](https://book.douban.com/subject/2130190/) - [Java 核心技术(卷 1)](https://book.douban.com/subject/3146174/) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - [JDK 序列化的高级认识](https://www.ibm.com/developerworks/cn/java/j-lo-serial/index.html) - http://www.hollischuang.com/archives/1140 - http://www.codenuclear.com/serialization-deserialization-java/ - http://www.blogjava.net/jiangshachina/archive/2012/02/13/369898.html -- https://agapple.iteye.com/blog/859052 \ No newline at end of file +- https://agapple.iteye.com/blog/859052 diff --git "a/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\347\256\200\344\273\213.md" "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\347\256\200\344\273\213.md" new file mode 100644 index 0000000000..c4ba3d565b --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/04.IO/JavaIO\347\256\200\344\273\213.md" @@ -0,0 +1,110 @@ +--- +title: Java I/O 之 简介 +date: 2020-11-21 16:36:40 +order: 01 +categories: + - Java + - JavaCore + - IO +tags: + - Java + - JavaCore + - IO +permalink: /pages/11934510/ +--- + +# Java I/O 之 简介 + +IO 即 `Input/Output`(输入和输出),指的是:**计算机内存与外部设备之间拷贝数据的过程**。由于 CPU 访问内存的速度远远高于外部设备,因此 CPU 是先把外部设备的数据读到内存里,然后再进行处理。 + +## UNIX I/O 模型 + +UNIX 系统下的 I/O 模型有 5 种: + +- 同步阻塞 I/O +- 同步非阻塞 I/O +- I/O 多路复用 +- 信号驱动 I/O +- 异步 I/O + +如何去理解 UNIX I/O 模型,大致有以下两个维度: + +- 区分同步或异步(synchronous/asynchronous)。简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。 +- 区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 ServerSocket 新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管 IO 操作是否结束,直接返回,相应操作在后台继续处理。 + +不能一概而论认为同步或阻塞就是低效,具体还要看应用和系统特征。 + +对于一个网络 I/O 通信过程,比如网络数据读取,会涉及两个对象,一个是调用这个 I/O 操作的用户线程,另外一个就是操作系统内核。一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。 + +当用户线程发起 I/O 操作后,网络数据读取操作会经历两个步骤: + +- **用户线程等待内核将数据从网卡拷贝到内核空间。** +- **内核将数据从内核空间拷贝到用户空间。** + +各种 I/O 模型的区别就是:它们实现这两个步骤的方式是不一样的。 + +### 同步阻塞 I/O + +用户线程发起 read 调用后就阻塞了,让出 CPU。内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163321.jpg) + +### 同步非阻塞 I/O + +用户线程不断的发起 read 调用,数据没到内核空间时,每次都返回失败,直到数据到了内核空间,这一次 read 调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163344.jpg) + +### I/O 多路复用 + +用户线程的读取操作分成两步了,线程先发起 select 调用,目的是问内核数据准备好了吗?等内核把数据准备好了,用户线程再发起 read 调用。在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的。那为什么叫 I/O 多路复用呢?因为一次 select 调用可以向内核查多个数据通道(Channel)的状态,所以叫多路复用。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163408.jpg) + +### 信号驱动 I/O + +首先开启 Socket 的信号驱动 I/O 功能,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。**信号驱动式 I/O 模型的优点是我们在数据报到达期间进程不会被阻塞,我们只要等待信号处理函数的通知即可** + +### 异步 I/O + +用户线程发起 read 调用的同时注册一个回调函数,read 立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163428.jpg) + +## Java I/O 模型 + +在 Java 中,主要支持三种 IO 模型: + +- BIO(blocking IO) +- NIO(non-blocking IO) +- AIO(Asynchronous IO) + +### BIO(blocking IO) + +BIO(blocking IO) 是同步阻塞 IO 模型。指的主要是传统的 `java.io` 包,它基于流模型实现。BIO 的数据传输采用同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里。 + +如果要让 **BIO 通信模型** 能够同时处理多个客户端请求,就必须使用多线程(主要原因是`socket.accept()`、`socket.read()`、`socket.write()` 涉及的三个主要函数都是同步阻塞的),但会造成不必要的线程开销。不过可以通过 **线程池机制** 改善,线程池还可以让线程的创建和回收成本相对较低。 + +**即使可以用线程池略微优化,但是会消耗宝贵的线程资源,并且在百万级并发场景下也撑不住**。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。 + +### NIO(non-blocking IO) + +JDK 4 引入了 NIO,源码在 `java.nio` 包中。NIO(non-blocking IO) 属于 I/O 多路复用模型。NIO 提供了 `Channel`、`Selector`、`Buffer` 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。 + +NIO 具有以下优点: + +- 使用缓冲区优化读写流 - NIO 与传统 I/O 不同,它是基于块(Block)的,它以块为基本单位处理数据。在 NIO 中,最为重要的两个组件是缓冲区(`Buffer`)和通道(`Channel`)。`Buffer` 是一块连续的内存块,是 NIO 读写数据的缓冲。`Buffer` 可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。`Channel` 表示缓冲数据的源头或者目的地,它用于读取缓冲或者写入数据,是访问缓冲的接口。 +- 使用 DirectBuffer 减少内存复制 - NIO 还提供了一个可以直接访问物理内存的类 `DirectBuffer`。普通的 `Buffer` 分配的是 JVM 堆内存,而 `DirectBuffer` 是直接分配物理内存。数据要输出到外部设备,必须先从用户空间复制到内核空间,再复制到输出设备,而 `DirectBuffer` 则是直接将步骤简化为从内核空间复制到外部设备,减少了数据拷贝。 +- 优化 I/O,避免阻塞 - 传统 I/O 的数据读写是在用户空间和内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。NIO 的 `Channel` 有自己的处理器,可以完成内核空间和磁盘之间的 I/O 操作。在 NIO 中,我们读取和写入数据都要通过 `Channel`,由于 `Channel` 是双向的,所以读、写可以同时进行。 + +### AIO(Asynchronous IO) + +AIO(Asynchronous IO) 即异步非阻塞 IO,指的是 JDK7 中,对 NIO 有了进一步的改进,也称为 NIO2,引入了异步非阻塞 IO 方式。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。 + +## 参考资料 + +- [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) +- [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) +- [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) +- [BIO,NIO,AIO 总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/BIO-NIO-AIO.md) +- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) diff --git a/source/_posts/01.Java/01.JavaCore/04.IO/README.md b/source/_posts/01.Java/01.JavaCore/04.IO/README.md new file mode 100644 index 0000000000..c0a73ff8d6 --- /dev/null +++ b/source/_posts/01.Java/01.JavaCore/04.IO/README.md @@ -0,0 +1,37 @@ +--- +title: Java IO +date: 2020-06-04 13:51:01 +categories: + - Java + - JavaCore + - IO +tags: + - Java + - JavaCore + - IO +permalink: /pages/29b38dd2/ +hidden: true +index: false +dir: + order: 4 + link: true +--- + +# Java IO + +## 📖 内容 + +- [Java I/O 之 简介](JavaIO简介.md) - 关键词:BIO、NIO、AIO +- [Java I/O 之 BIO](JavaIO之BIO.md) - 关键词:BIO、InputStream、OutputStream、Reader、Writer、File、Socket、ServerSocket +- [Java I/O 之 NIO](JavaIO之NIO.md) - 关键词:NIO、Channel、Buffer、Selector、多路复用 +- [Java I/O 之序列化](JavaIO之序列化.md) - 关键词:Serializable、serialVersionUID、transient、Externalizable + +## 📚 资料 + +- [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) +- [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) +- [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) + +## 🚪 传送 + +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore/) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213AQS.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213AQS.md" new file mode 100644 index 0000000000..c68bf35d39 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213AQS.md" @@ -0,0 +1,326 @@ +--- +title: Java 并发之 AQS +date: 2019-12-26 23:11:52 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 + - AQS + - 独占锁 + - 共享锁 +permalink: /pages/3c5ce4e8/ +--- + +# Java 并发之 AQS + +## AQS 简介 + +**AQS** 是 `AbstractQueuedSynchronizer` 的缩写,即 **队列同步器**,顾名思义,其主要作用是处理同步。它是并发锁和很多同步工具类的实现基石(如 `ReentrantLock`、`ReentrantReadWriteLock`、`CountDownLatch`、`Semaphore`、`FutureTask` 等)。 + +**AQS 提供了对锁和同步器的通用能力支持 **。在 `java.util.concurrent.locks` 包中的相关锁(常用的有 `ReentrantLock`、 `ThreadPoolExecutor`)都是基于 AQS 来实现。这些锁都没有直接继承 AQS,而是定义了一个 `Sync` 类去继承 AQS。为什么要这样呢?因为锁面向的是使用用户,而同步器面向的则是线程控制,那么在锁的实现中聚合同步器而不是直接继承 AQS 就可以很好的隔离二者所关注的事情。 + +## AQS 的应用 + +AQS 定义两种资源共享方式:`Exclusive`(独占,只有一个线程能执行,如 `ReentrantLock`)和 `Share`(共享,多个线程可同时执行,如 `Semaphore` / `CountDownLatch`)。 + +### 独占锁 API + +获取、释放独占锁的主要 API 如下: + +```java +public final void acquire(int arg) +public final void acquireInterruptibly(int arg) +public final boolean tryAcquireNanos(int arg, long nanosTimeout) +public final boolean release(int arg) +``` + +- `acquire` - 获取独占锁。 +- `acquireInterruptibly` - 获取可中断的独占锁。 +- `tryAcquireNanos` - 尝试在指定时间内获取可中断的独占锁。在以下三种情况下回返回: + - 在超时时间内,当前线程成功获取了锁; + - 当前线程在超时时间内被中断; + - 超时时间结束,仍未获得锁返回 false。 +- `release` - 释放独占锁。 + +### 共享锁 API + +获取、释放共享锁的主要 API 如下: + +```java +public final void acquireShared(int arg) +public final void acquireSharedInterruptibly(int arg) +public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) +public final boolean releaseShared(int arg) +``` + +- `acquireShared` - 获取共享锁。 +- `acquireSharedInterruptibly` - 获取可中断的共享锁。 +- `tryAcquireSharedNanos` - 尝试在指定时间内获取可中断的共享锁。 +- `release` - 释放共享锁。 + +## AQS 的原理 + +AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制。这个机制是基于 **CLH 锁** (Craig, Landin, and Hagersten locks) 的变体实现的,将暂时获取不到锁的线程加入到队列中。 + +CLH 本是一个单向队列,AQS 中的队列采用了 CLH 的变体,是一个虚拟的 FIFO 双向队列(虚拟的双向队列,是指不存在结点实例,仅存在结点之间的关联关系),暂时获取不到锁的线程将被加入到该队列中。AQS 将每条请求共享资源的线程封装成一个 CLH 队列锁的一个结点(Node)来实现锁的分配。在 CLH 队列锁中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409120729373.png) + +AQS 的核心原理图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409120729594.png) + +### AQS 的数据结构 + +先看一下 `AbstractQueuedSynchronizer` 的定义: + +```java +public abstract class AbstractQueuedSynchronizer + extends AbstractOwnableSynchronizer + implements java.io.Serializable { + + /** 等待队列的队头,懒加载。只能通过 setHead 方法修改。 */ + private transient volatile Node head; + /** 等待队列的队尾,懒加载。只能通过 enq 方法添加新的等待节点。*/ + private transient volatile Node tail; + /** 同步状态 */ + private volatile int state; +} +``` + +阅读 AQS 的源码,可以发现:AQS 继承自 `AbstractOwnableSynchronize`,它有以下核心属性: + +- `state` - AQS 使用一个整型的 `volatile` 变量来 **维护同步状态**。这个整数状态的意义由子类来赋予,如 `ReentrantLock` 中该状态值表示所有者线程已经重复获取该锁的次数;`Semaphore` 中该状态值表示剩余的许可数量。 +- `head` 和 `tail` - AQS **维护了一个 `Node` 类型(AQS 的内部类)的双向队列来完成同步状态的管理 **。这个双向队列是一个双向的 FIFO 队列,通过 `head` 和 `tail` 指针进行访问。当 **有线程获取锁失败后,就被添加到队列末尾 **。 + +再来看一下 `Node` 的源码,很显然,Node 是一个双向队列结构: + +```java +static final class Node { + /** 该等待同步的节点处于共享模式 */ + static final Node SHARED = new Node(); + /** 该等待同步的节点处于独占模式 */ + static final Node EXCLUSIVE = null; + + /** 线程等待状态,状态值有:0、1、-1、-2、-3 */ + volatile int waitStatus; + static final int CANCELLED = 1; + static final int SIGNAL = -1; + static final int CONDITION = -2; + static final int PROPAGATE = -3; + + /** 前驱节点 */ + volatile Node prev; + /** 后继节点 */ + volatile Node next; + /** 等待锁的线程 */ + volatile Thread thread; + + /** 和节点是否共享有关 */ + Node nextWaiter; +} +``` + +属性说明: + +| 方法和属性值 | 含义 | +| :----------- | :--------------------- | +| waitStatus | 当前节点在队列中的状态 | +| thread | 表示处于该节点的线程 | +| prev | 前驱指针 | +| next | 后继指针 | + +`waitStatus` 是一个整型的 `volatile` 变量,用来维护 AQS 同步队列中线程节点的状态。`waitStatus` 有五个状态值: + +- 0 - 一个 Node 被初始化的时候的默认值 +- `CANCELLED(1)` - 表示线程获取锁的请求已经取消了 +- `SIGNAL(-1)` - 表示线程已经准备好了,就等资源释放了 +- `CONDITION(-2)` - 表示节点在等待队列中,节点线程等待唤醒 +- `PROPAGATE(-3)` - 当前线程处在 SHARED 情况下,该字段才会使用 + +### 独占锁的获取和释放 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409120730774.png) + +#### 获取独占锁 + +AQS 中使用 `acquire(int arg)` 方法获取独占锁的相关源码如下: + +```java +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} + +// 利用 CAS 操作将当前线程加入等待队列队尾 +private Node addWaiter(Node mode) { + Node node = new Node(Thread.currentThread(), mode); + Node pred = tail; + if (pred != null) { + node.prev = pred; + if (compareAndSetTail(pred, node)) { + pred.next = node; + return node; + } + } + enq(node); + return node; +} + +// 自旋尝试为等待队列中的线程节点获取独占锁 +final boolean acquireQueued(final Node node, int arg) { + boolean failed = true; + try { + boolean interrupted = false; + for (;;) { + final Node p = node.predecessor(); + // 获取锁成功,退出 + if (p == head && tryAcquire(arg)) { + setHead(node); + p.next = null; // help GC + failed = false; + return interrupted; + } + // 线程中断 + if (shouldParkAfterFailedAcquire(p, node) && + parkAndCheckInterrupt()) + interrupted = true; + } + } finally { + if (failed) + cancelAcquire(node); + } +} +``` + +其大致流程如下: + +1. 先通过 `tryAcquire` 尝试获取同步状态,如果获取同步状态成功,则结束方法,直接返回。 +2. 若不成功,调用 `addWaiter` 方法,利用 CAS 操作将当前线程加入等待队列队尾。 +3. 接着,自旋尝试为等待队列中的线程节点获取独占锁,直到获取成功或线程中断。 + +#### 释放独占锁 + +AQS 中使用 `acquire(int arg)` 方法获取独占锁的相关源码如下: + +```java +public final boolean release(int arg) { + // 尝试释放锁 + if (tryRelease(arg)) { + Node h = head; + // 如果队列不为空,唤醒下一个节点中的线程 + if (h != null && h.waitStatus != 0) + unparkSuccessor(h); + return true; + } + return false; +} + +private void unparkSuccessor(Node node) { + + int ws = node.waitStatus; + if (ws < 0) + compareAndSetWaitStatus(node, ws, 0); + + Node s = node.next; + if (s == null || s.waitStatus > 0) { + s = null; + for (Node t = tail; t != null && t != node; t = t.prev) + if (t.waitStatus <= 0) + s = t; + } + if (s != null) + LockSupport.unpark(s.thread); +} +``` + +1. 先尝试获取解锁线程的同步状态,如果获取同步状态不成功,则结束方法,直接返回。 +2. 如果获取同步状态成功且队列不为空,AQS 会尝试唤醒下一个节点中的线程。 + +#### 获取可中断的独占锁 + +AQS 中使用 `acquireInterruptibly(int arg)` 方法获取可中断的独占锁。 + +`acquireInterruptibly(int arg)` 实现方式 **相较于获取独占锁方法( `acquire`)非常相似**,区别仅在于它会 **通过 `Thread.interrupted` 检测当前线程是否被中断**,如果是,则立即抛出中断异常(`InterruptedException`)。 + +#### 限时获取独占锁 + +AQS 中使用 `tryAcquireNanos(int arg)` 方法获取超时等待的独占锁。 + +doAcquireNanos 的实现方式 **相较于获取独占锁方法( `acquire`)非常相似**,区别在于它会根据超时时间和当前时间计算出截止时间。在获取锁的流程中,会不断判断是否超时,如果超时,直接返回 false;如果没超时,则用 `LockSupport.parkNanos` 来阻塞当前线程。 + +### 共享锁的获取和释放 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409120732865.png) + +#### 获取共享锁 + +AQS 中使用 `acquireShared(int arg)` 方法获取共享锁。 + +`acquireShared` 方法和 `acquire` 方法的逻辑很相似,区别仅在于自旋的条件以及节点出队的操作有所不同。 + +成功获得共享锁的条件如下: + +- `tryAcquireShared(arg)` 返回值大于等于 0 (这意味着共享锁的 permit 还没有用完)。 +- 当前节点的前驱节点是头结点。 + +#### 释放共享锁 + +AQS 中使用 `releaseShared(int arg)` 方法释放共享锁。 + +`releaseShared` 首先会尝试释放同步状态,如果成功,则解锁一个或多个后继线程节点。释放共享锁和释放独占锁流程大体相似,区别在于: + +对于独占模式,如果需要 SIGNAL,释放仅相当于调用头节点的 `unparkSuccessor`。 + +#### 获取可中断的共享锁 + +AQS 中使用 `acquireSharedInterruptibly(int arg)` 方法获取可中断的共享锁。 + +`acquireSharedInterruptibly` 方法与 `acquireInterruptibly` 几乎一致,不再赘述。 + +#### 限时获取共享锁 + +AQS 中使用 `tryAcquireSharedNanos(int arg)` 方法获取超时等待式的共享锁。 + +`tryAcquireSharedNanos` 方法与 `tryAcquireNanos` 几乎一致,不再赘述。 + +## 自定义同步器 + +同步器的设计是基于模板方法模式的,如果需要自定义同步器一般的方式是这样(模板方法模式很经典的一个应用): + +1. 使用者继承 `AbstractQueuedSynchronizer` 并重写指定的方法。 +2. 将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法。 + +这和我们以往通过实现接口的方式有很大区别,这是模板方法模式很经典的一个运用。 + +**AQS 使用了模板方法模式,自定义同步器时需要重写下面几个 AQS 提供的钩子方法:** + +```java +// 独占方式。尝试获取资源,成功则返回 true,失败则返回 false。 +protected boolean tryAcquire(int) +// 独占方式。尝试释放资源,成功则返回 true,失败则返回 false。 +protected boolean tryRelease(int) +// 共享方式。尝试获取资源。负数表示失败;0 表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 +protected int tryAcquireShared(int) +// 共享方式。尝试释放资源,成功则返回 true,失败则返回 false。 +protected boolean tryReleaseShared(int) +// 该线程是否正在独占资源。只有用到 condition 才需要去实现它。 +protected boolean isHeldExclusively() +``` + +**什么是钩子方法呢?** 钩子方法是一种被声明在抽象类中的方法,一般使用 `protected` 关键字修饰,它可以是空方法(由子类实现),也可以是默认实现的方法。模板设计模式通过钩子方法控制固定步骤的实现。 + +## 参考资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) +- [Java 并发编程:Lock](https://www.cnblogs.com/dolphin0520/p/3923167.html) +- [深入学习 java 同步器 AQS](https://zhuanlan.zhihu.com/p/27134110) +- [AbstractQueuedSynchronizer 框架](https://t.hao0.me/java/2016/04/01/aqs.html) +- [Java 中的锁分类](https://www.cnblogs.com/qifengshi/p/6831055.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\206\205\345\255\230\346\250\241\345\236\213.md" new file mode 100644 index 0000000000..9ebbdd0603 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\206\205\345\255\230\346\250\241\345\236\213.md" @@ -0,0 +1,1055 @@ +--- +title: Java 并发之内存模型 +date: 2020-12-25 18:43:11 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 + - JMM + - Happens-Before + - 内存屏障 + - volatile + - synchronized + - final + - 指令重排序 +permalink: /pages/3bafe85f/ +--- + +# Java 并发之内存模型 + +Java 内存模型(Java Memory Model),简称 **JMM**。Java 内存模型的目标是为了解决由可见性和有序性导致的并发安全问题。Java 内存模型通过 **屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果**。 + +## 物理内存模型 + +物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。 + +### 硬件处理效率存在很大差异 + +技术在进步,CPU、内存、I/O 设备的性能也在不断提高。但是,始终存在一个核心矛盾:**CPU、内存、I/O 设备存在很大的速度差异** - CPU 远快于内存,内存远快于 I/O 设备。木桶短板理论告诉我们:一只木桶能装多少水,取决于最短的那块木板。同理,程序整体性能取决于最慢的操作(即 I/O 操作),所以单方面提高 CPU、内存的性能是无效的。 + +为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为: + +- **CPU 增加了缓存**,以均衡与 CPU 内存的速度差异; +- **编译程序优化指令执行次序**,使得缓存能够得到更加合理地利用。 +- **操作系统增加了进程、线程**,以分时复用 CPU,进而均衡 CPU 与 I/O 的速度差异; + +**缓存**导致的可见性问题,**编译优化**带来的有序性问题,**线程切换**带来的原子性问题。 + +### 缓存一致性 + +高速缓存解决了 **硬件效率问题**,但是引入了一个新的问题:**缓存一致性(Cache Coherence)**。 + +在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。 + +为了解决缓存一致性问题,**需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408290755550.png) + +### 指令重排序 + +为了使缓存得到更加合理地使用,计算机在执行程序代码的时候,会对指令进行重排序。 + +**什么是指令重排序?** 简单来说就是系统在执行代码的时候并不一定是按照你写的代码的顺序依次执行。 + +常见的指令重排序有下面 2 种情况: + +- **编译器优化重排**:编译器(包括 JVM、JIT 编译器等)在不改变单线程程序语义的前提下,重新安排语句的执行顺序。 +- **指令并行重排**:现代处理器采用了指令级并行技术 (Instruction-Level Parallelism,ILP) 来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。 + +另外,内存系统也会有“重排序”,但又不是真正意义上的重排序。在 JMM 里表现为主存和本地内存的内容可能不一致,进而导致程序在多线程下执行可能出现问题。 + +Java 源代码会经历 **编译器优化重排 —> 指令并行重排 —> 内存系统重排** 的过程,最终才变成操作系统可执行的指令序列。 + +**指令重排序可以保证串行语义一致,但是没有义务保证多线程间的语义也一致** ,所以在多线程下,指令重排序可能会导致一些问题。 + +对于编译器优化重排和处理器的指令重排序(指令并行重排和内存系统重排都属于是处理器级别的指令重排序),处理该问题的方式不一样。 + +- 对于编译器,通过禁止特定类型的编译器重排序的方式来禁止重排序。 +- 对于处理器,通过插入内存屏障(Memory Barrier,或有时叫做内存栅栏,Memory Fence)的方式来禁止特定类型的处理器重排序。 + +## Java 内存模型 + +**内存模型** 这个概念。我们可以理解为:**在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象**。不同架构的物理计算机可以有不一样的内存模型,JVM 也有自己的内存模型。 + +JVM 中试图定义一种 Java 内存模型(Java Memory Model, JMM)来**屏蔽各种硬件和操作系统的内存访问差异**,以实现让 Java 程序 **在各种平台下都能达到一致的内存访问效果**。 + +在 [Java 并发简介](https://dunwu.github.io/waterdrop/pages/97c73d27/) 中已经介绍了,并发安全需要满足可见性、有序性、原子性。其中,导致可见性的原因是缓存,导致有序性的原因是编译优化。那解决可见性、有序性最直接的办法就是**禁用缓存和编译优化** 。但这么做,性能就堪忧了。 + +合理的方案应该是**按需禁用缓存以及编译优化**。那么,如何做到呢?,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 **volatile**、**synchronized** 和 **final** 三个关键字,以及 **Happens-Before 规则**。 + +### 主内存和工作内存 + +JMM 的主要目标是 **定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节**。此处的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数值对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,JMM 并没有限制执行引擎使用处理器的特定寄存器或缓存来和主存进行交互,也没有限制即使编译器进行调整代码执行顺序这类优化措施。 + +JMM 规定了**所有的变量都存储在主内存(Main Memory)中**。 + +每条线程还有自己的工作内存(Working Memory),**工作内存中保留了该线程使用到的变量的主内存的副本**。工作内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 + +线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程间也无法直接访问对方工作内存中的变量,**线程间变量值的传递均需要通过主内存来完成**。 + +> 说明: +> +> 这里说的主内存、工作内存与 Java 内存区域中的堆、栈、方法区等不是同一个层次的内存划分。 + +### JMM 内存操作的问题 + +类似于物理内存模型面临的问题,JMM 存在以下两个问题: + +- **工作内存数据一致性** - 各个线程操作数据时会保存使用到的主内存中的共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致各自的的共享变量副本不一致。如果真的发生这种情况,数据同步回主内存以谁的副本数据为准? Java 内存模型主要通过一系列的数据同步协议、规则来保证数据的一致性。 +- **指令重排序优化** - Java 中重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:**编译期重排序和运行期重排序**,分别对应编译时和运行时环境。 同样的,指令重排序不是随意重排序,它需要满足以下两个条件: + - 在单线程环境下不能改变程序运行的结果。即时编译器(和处理器)需要保证程序能够遵守 `as-if-serial` 属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。 + - 存在数据依赖关系的不允许重排序。 + - 多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同。 + +### 内存间交互操作 + +JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。JVM 实现时必须保证下面介绍的每种操作都是 **原子的**(对于 double 和 long 型的变量来说,load、store、read、和 write 操作在某些平台上允许有例外 )。 + +- `lock` (锁定) - 作用于**主内存**的变量,它把一个变量标识为一条线程独占的状态。 +- `unlock` (解锁) - 作用于**主内存**的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 +- `read` (读取) - 作用于**主内存**的变量,它把一个变量的值从主内存**传输**到线程的工作内存中,以便随后的 `load` 动作使用。 +- `write` (写入) - 作用于**主内存**的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。 +- `load` (载入) - 作用于**工作内存**的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。 +- `use` (使用) - 作用于**工作内存**的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。 +- `assign` (赋值) - 作用于**工作内存**的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 +- `store` (存储) - 作用于**工作内存**的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 `write` 操作使用。 + +如果要把一个变量从主内存中复制到工作内存,就**需要按序执行 `read` 和 `load` 操作**;如果把变量从工作内存中同步回主内存中,就**需要按序执行 `store` 和 `write` 操作**。但 Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。 + +JMM 还规定了上述 8 种基本操作,需要满足以下规则: + +- **read 和 load 必须成对出现**;**store 和 write 必须成对出现**。即不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写了但主内存不接受的情况出现。 +- **不允许一个线程丢弃它的最近 assign 的操作**,即变量在工作内存中改变了之后必须把变化同步到主内存中。 +- **不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中**。 +- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。换句话说,就是对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。 +- 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 **lock 和 unlock 必须成对出现**。 +- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。 +- 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。 +- 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408290758072.png) + +### 并发安全特性 + +并发最重要的问题是并发安全问题。所谓**并发安全**,是指保证程序的正确性,使得并发处理结果符合预期。 + +并发安全需要保证几个基本特性: + +- **可见性** - 是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,`volatile` 就是负责保证可见性的。 +- **有序性** - 是保证线程内串行语义,避免指令重排等。 +- **原子性** - 简单说就是相关操作不会中途被其他线程干扰,一般通过互斥机制(加锁:`sychronized`、`Lock`)实现。 + +而这三大特性,归根结底,是为了实现多线程的 **数据一致性**,使得程序在多线程并发,指令重排序优化的环境中能如预期运行。上文介绍了 Java 内存交互的 8 种基本操作,它们都保证可见性、有序性、原子性。 + +#### 原子性 + +**原子性即一个操作或者多个操作,要么全部执行(执行的过程不会被任何因素打断),要么就都不执行**。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。 + +在 Java 中,为了保证原子性,提供了两个高级的字节码指令 `monitorenter` 和 `monitorexit`。这两个字节码,在 Java 中对应的关键字就是 `synchronized`。 + +因此,在 Java 中可以使用 `synchronized` 来保证方法和代码块内的操作是原子性的。 + +#### 可见性 + +**可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值**。 + +JMM 是通过 **"变量修改后将新值同步回主内存**, **变量读取前从主内存刷新变量值"** 这种依赖主内存作为传递媒介的方式来实现的。 + +Java 实现多线程可见性的方式有: + +- `volatile` +- `synchronized` +- `final` + +#### 有序性 + +有序性规则表现在以下两种场景:线程内和线程间 + +- 线程内 - 从某个线程的角度看方法的执行,指令会按照一种叫“串行”(`as-if-serial`)的方式执行,此种方式已经应用于顺序编程语言。 +- 线程间 - 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(`synchronized` 关键字修饰)以及 `volatile` 字段的操作仍维持相对有序。 + +在 Java 中,可以使用 `synchronized` 和 `volatile` 来保证多线程之间操作的有序性。实现方式有所区别: + +- `volatile` 关键字会禁止指令重排序。 +- `synchronized` 关键字通过互斥保证同一时刻只允许一条线程操作。 + +## Happens-Before + +JMM 为程序中所有的操作定义了一个偏序关系,称之为 **`先行发生原则(Happens-Before)`**。**Happens-Before** 是指 **前面一个操作的结果对后续操作是可见的**。 + +> 1978 年,Lamport 在论文 [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf) ([**译文**](https://cloud.tencent.com/developer/article/1163428),[**解读**](https://zhuanlan.zhihu.com/p/56146800) )中第一次提出了 Happens-Before,阐述了偏序关系(partial ordering)、逻辑时钟(Logical Clocks)概念,提出解决分布式系统中区分事件发生的时序问题的方法。Happens-Before 的语义是一种因果关系:如果 A 事件是导致 B 事件的起因,那么 A 事件一定是先于(Happens-Before)B 事件发生的。 + +**Happens-Before** 非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作间是否可能存在冲突的所有问题。 + +- **程序顺序规则** - 在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。 +- **锁定规则** - 一个 `unLock` 操作 Happens-Before 于后面对同一个锁的 `lock` 操作。 +- **volatile 变量规则** - 对一个 `volatile` 变量的写操作 Happens-Before 于后面对这个变量的读操作。 +- **线程启动规则** - `Thread` 对象的 `start()` 方法 Happens-Before 于此线程的每个一个动作。 +- **线程终止规则** - 线程中所有的操作都 Happens-Before 于线程的终止检测,我们可以通过 `Thread.join()` 方法是否结束、`Thread.isAlive()` 的返回值手段检测到线程已经终止执行。 +- **线程中断规则** - 对线程 `interrupt()` 方法的调用 Happens-Before 于被中断线程的代码检测到中断事件的发生,可以通过 `Thread.interrupted()` 方法检测到是否有中断发生。 +- **对象终结规则** - 一个对象的初始化完成 Happens-Before 于它的 `finalize()` 方法的开始。 +- **传递性** - 如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。 + +## 内存屏障 + +Java 中如何保证底层操作的有序性和可见性?可以通过内存屏障(memory barrier)。 + +内存屏障是被插入两个 CPU 指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障**有序性**的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障**可见性**。 + +举个例子: + +``` +Store1; +Store2; +Load1; +StoreLoad; //内存屏障 +Store3; +Load2; +Load3; +复制代码 +``` + +对于上面的一组 CPU 指令(Store 表示写入指令,Load 表示读取指令),StoreLoad 屏障之前的 Store 指令无法与 StoreLoad 屏障之后的 Load 指令进行交换位置,即**重排序**。但是 StoreLoad 屏障之前和之后的指令是可以互换位置的,即 Store1 可以和 Store2 互换,Load2 可以和 Load3 互换。 + +常见有 4 种屏障 + +- `LoadLoad` 屏障 - 对于这样的语句 `Load1; LoadLoad; Load2`,在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。 +- `StoreStore` 屏障 - 对于这样的语句 `Store1; StoreStore; Store2`,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。 +- `LoadStore` 屏障 - 对于这样的语句 `Load1; LoadStore; Store2`,在 Store2 及后续写入操作被执行前,保证 Load1 要读取的数据被读取完毕。 +- `StoreLoad` 屏障 - 对于这样的语句 `Store1; StoreLoad; Load2`,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 + +Java 中对内存屏障的使用在一般的代码中不太容易见到,常见的有 `volatile` 和 `synchronized` 关键字修饰的代码块(后面再展开介绍),还可以通过 `Unsafe` 这个类来使用内存屏障。 + +![img](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC83LzE1LzE3MzUwODViNjA2M2Q0NGY?x-oss-process=image/format,png) + +## Synchronized 内存语义 + +`synchronized` 是 Java 中的关键字,是 **利用锁的机制来实现互斥同步的**。 + +**`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 + +- **线程释放锁时内存语义:JMM 会把该线程对应的工作内存中的共享变量刷新到主内存中** +- **线程获取锁时内存语义:JMM 会把该线程对应的工作内存置为无效** + +如果不需要 `Lock` 、`ReadWriteLock` 所提供的高级同步特性,应该优先考虑使用 `synchronized` ,理由如下: + +- Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平。从趋势来看,Java 未来仍将继续优化 `synchronized` ,而不是 `ReentrantLock` 。 +- `ReentrantLock` 是 Oracle JDK 的 API,在其他版本的 JDK 中不一定支持;而 `synchronized` 是 JVM 的内置特性,所有 JDK 版本都提供支持。 + +### synchronized 的应用 + +`synchronized` 有 3 种应用方式: + +- **同步实例方法** - 对于普通同步方法,锁是当前实例对象 +- **同步静态方法** - 对于静态同步方法,锁是当前类的 `Class` 对象 +- **同步代码块** - 对于同步方法块,锁是 `synchonized` 括号里配置的对象 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409090719904.png) + +【示例】`synchronized` 的使用语法 + +```java +class Test { + + // 修饰成员方法 + synchronized void sync1() { + // 临界区 + } + + // 修饰静态方法 + synchronized static void sync2() { + // 临界区 + } + + // 对象锁 + Object obj = new Object(); + // 修饰代码块,使用对象锁 + void sync3() { + synchronized (obj) { + // 临界区 + } + } + + // 修饰代码块,使用类锁(Class) + void sync4() { + synchronized (Test.class) { + //临界区 + } + } + +} +``` + +#### 用 synchronized 实现线程安全的计数器 + +我们先来看一个简单的示例,这段代码维护了一个计数器变量 `count`,并通过 get() 和 add() 分别实现了读写方法。 + +```java +@NotThreadSafe +public class NotThreadSafeCounter { + + private static int count = 0; + + public int get() { + return count; + } + + public void add() { + count++; + } + + public static void main(String[] args) throws InterruptedException { + final int MAX = 100000; + NotThreadSafeCounter instance = new NotThreadSafeCounter(); + Thread t1 = new Thread(() -> { + for (int i = 0; i < MAX; i++) { + instance.add(); + } + }); + Thread t2 = new Thread(() -> { + for (int i = 0; i < MAX; i++) { + instance.add(); + } + }); + t1.start(); + t2.start(); + t1.join(); + t2.join(); + System.out.println("count = " + instance.get()); + } + +} +// 输出: +// count = 117626 +// +``` + +启动两个线程并行执行,期望最终值为 200000,但实际值为小于 200000 的随机数字。显然,上面的示例是线程不安全的。究其原因,在于 count++ 不是原子操作,不满足并发安全的原子性要求。 + +要解决此问题,可以用 `synchronized` 修饰方法, `synchronized` 可以保证同一时刻只有一个线程执行临界区的代码。 + +我们针对上面的示例来进行改造,将 `add()` 方法用 `synchronized` 修饰,如下所示。这下是不是就可以高枕无忧了呢? + +```java +@NotThreadSafe +public class NotThreadSafeCounter2 { + + private static int count = 0; + + public int get() { + return count; + } + + public synchronized void add() { + count++; + } +} +``` + +首先,`add()` 方法本身是线程安全的。但是,这个示例忽略了 `get()` 方法。因为 `get()` 方法未加锁,一个线程调用 `add()` 方法后,无法保证另一个线程调用 `get()` 时能立刻获取到更新后的结果,不满足并发安全的可见性要求。 + +如何彻底解决 get() 并发不安全的问题呢?很简单,就是 `get()` 方法也用 `synchronized` 修饰一下。最终的线程安全示例如下: + +```java +@ThreadSafe +public class ThreadSafeCounter { + + private int count = 0; + + public synchronized long get() { + return count; + } + + public synchronized void add() { + count++; + } +} +``` + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409090720289.png) + +#### 静态 `synchronized` 方法和非静态 `synchronized` 是否互斥 + +静态方法的同步是指同步在该方法所在的类对象上。因为在 JVM 中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。 + +静态 `synchronized` 方法和非静态 `synchronized` 方法之间的调用互斥么? + +答案是:不互斥,但可能存在并发问题!如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法;而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁。 + +```java +@ThreadSafe +public class ThreadSafeCounter2 { + + private static int count = 0; + + public synchronized long get() { + return count; + } + + public synchronized static void add() { + count++; + } +} +``` + +上面这段代码实际上是用两个锁保护同一个资源。这个受保护的资源就是静态变量 count,两个锁分别是 this 和 ThreadSafeCounter2.class。我们可以用下面这幅图来形象描述这个关系。由于临界区 get() 和 add() 是用两个锁保护的,因此这两个临界区没有互斥关系,临界区 add() 对 value 的修改对临界区 get() 也没有可见性保证,这就导致并发问题了。 + +#### 用 synchronized 保护多个资源 + +【示例】错误示例 + +```java +class Account { + private int balance; + // 转账 + synchronized void transfer( + Account target, int amt){ + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } +} +``` + +在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁 this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢? + +问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409060808648.png) + +应该保证使用的**锁能覆盖所有受保护资源**。 + +【示例】正确姿势 + +```java +class Account { + private Object lock; + private int balance; + private Account(); + // 创建 Account 时传入同一个 lock 对象 + public Account(Object lock) { + this.lock = lock; + } + // 转账 + void transfer(Account target, int amt){ + // 此处检查所有对象共享的锁 + synchronized(lock) { + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } + } +} +``` + +这个办法确实能解决问题,但是有点小瑕疵,它要求在创建 Account 对象的时候必须传入同一个对象,如果创建 Account 对象时,传入的 lock 不是同一个对象,那可就惨了,会出现锁自家门来保护他家资产的荒唐事。在真实的项目场景中,创建 Account 对象的代码很可能分散在多个工程中,传入共享的 lock 真的很难。 + +上面的方案缺乏实践的可行性,我们需要更好的方案。还真有,就是**用 Account.class 作为共享的锁**。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。使用 Account.class 作为共享的锁,我们就无需在创建 Account 对象时传入了,代码更简单。 + +【示例】正确姿势 + +```java +class Account { + private int balance; + // 转账 + void transfer(Account target, int amt){ + synchronized(Account.class) { + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } + } +} +``` + +### synchronized 的原理 + +**`synchronized` 代码块是由一对 `monitorenter` 和 `monitorexit` 指令实现的,`Monitor` 对象是同步的基本实现单元**。在 Java 6 之前,`Monitor` 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。 + +如果 `synchronized` 明确制定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据 `synchronized` 修饰的是实例方法还是静态方法,去对对应的对象实例或 `Class` 对象来作为锁对象。 + +`synchronized` 同步块对同一线程来说是可重入的,不会出现锁死问题。 + +`synchronized` 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。 + +```java +public void foo(Object lock) { + synchronized (lock) { + lock.hashCode(); + } + } + // 上面的 Java 代码将编译为下面的字节码 + public void foo(java.lang.Object); + Code: + 0: aload_1 + 1: dup + 2: astore_2 + 3: monitorenter + 4: aload_1 + 5: invokevirtual java/lang/Object.hashCode:()I + 8: pop + 9: aload_2 + 10: monitorexit + 11: goto 19 + 14: astore_3 + 15: aload_2 + 16: monitorexit + 17: aload_3 + 18: athrow + 19: return + Exception table: + from to target type + 4 11 14 any + 14 17 14 any + +``` + +`synchronized` 在修饰同步代码块时,是由 `monitorenter` 和 `monitorexit` 指令来实现同步的。进入 `monitorenter` 指令后,线程将持有 `Monitor` 对象,退出 `monitorenter` 指令后,线程将释放该 `Monitor` 对象。 + +`synchronized` 修饰同步方法时,会设置一个 `ACC_SYNCHRONIZED` 标志。当方法调用时,调用指令将会检查该方法是否被设置 `ACC_SYNCHRONIZED` 访问标志。如果设置了该标志,执行线程将先持有 `Monitor` 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 `Mointor` 对象,当方法执行完成后,再释放该 `Monitor` 对象。 + +每个对象实例都会有一个 `Monitor`,`Monitor` 可以和对象一起创建、销毁。`Monitor` 是由 `ObjectMonitor` 实现,而 `ObjectMonitor` 是由 C++ 的 `ObjectMonitor.hpp` 文件实现。 + +当多个线程同时访问一段同步代码时,多个线程会先被存放在 EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。 + +如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。 + +### synchronized 的优化 + +**Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平**。 + +在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。 + +Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200629191250.png) + +锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,`synchronized` 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。 + +Java 1.6 引入了偏向锁和轻量级锁,从而让 `synchronized` 拥有了四个状态: + +- **无锁状态(unlocked)** +- **偏向锁状态(biasble)** +- **轻量级锁状态(lightweight locked)** +- **重量级锁状态(inflated)** + +当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现。 + +当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。 + +如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。 + +#### 偏向锁 + +偏向锁的思想是偏向于**第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200604105151.png) + +#### 轻量级锁 + +**轻量级锁**是相对于传统的重量级锁而言,它 **使用 CAS 操作来避免重量级锁使用互斥量的开销**。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。 + +当尝试获取一个锁对象时,如果锁对象标记为 `0|01`,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200604105248.png) + +#### 锁消除 / 锁粗化 + +除了锁升级优化,Java 还使用了编译器对锁进行优化。 + +##### 锁消除 + +**锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除**。 + +JIT 编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步块使用的锁对象是否只能够被一个线程访问,而没有被发布到其它线程。 + +确认是的话,那么 JIT 编译器在编译这个同步块的时候不会生成 synchronized 所表示的锁的申请与释放的机器码,即消除了锁的使用。在 Java7 之后的版本就不需要手动配置了,该操作可以自动实现。 + +对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁: + +```java +public static String concatString(String s1, String s2, String s3) { + return s1 + s2 + s3; +} +``` + +`String` 是一个不可变的类,编译器会对 String 的拼接自动优化。在 Java 1.5 之前,会转化为 `StringBuffer` 对象的连续 `append()` 操作: + +```java +public static String concatString(String s1, String s2, String s3) { + StringBuffer sb = new StringBuffer(); + sb.append(s1); + sb.append(s2); + sb.append(s3); + return sb.toString(); +} +``` + +每个 `append()` 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 `concatString()` 方法内部。也就是说,sb 的所有引用永远不会逃逸到 `concatString()` 方法之外,其他线程无法访问到它,因此可以进行消除。 + +##### 锁粗化 + +锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。 + +如果**一系列的连续操作都对同一个对象反复加锁和解锁**,频繁的加锁操作就会导致性能损耗。 + +上一节的示例代码中连续的 `append()` 方法就属于这类情况。如果**虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部**。对于上一节的示例代码就是扩展到第一个 `append()` 操作之前直至最后一个 `append()` 操作之后,这样只需要加锁一次就可以了。 + +#### 自旋锁 + +互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 + +自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。 + +在 Java 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。 + +### synchronized 的误区 + +> 示例摘自:[极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) + +#### synchronized 使用范围不当导致的错误 + +```java +public class Interesting { + + volatile int a = 1; + volatile int b = 1; + + public static void main(String[] args) { + Interesting interesting = new Interesting(); + new Thread(() -> interesting.add()).start(); + new Thread(() -> interesting.compare()).start(); + } + + public synchronized void add() { + log.info("add start"); + for (int i = 0; i < 10000; i++) { + a++; + b++; + } + log.info("add done"); + } + + public void compare() { + log.info("compare start"); + for (int i = 0; i < 10000; i++) { + //a 始终等于 b 吗? + if (a < b) { + log.info("a:{},b:{},{}", a, b, a > b); + //最后的 a>b 应该始终是 false 吗? + } + } + log.info("compare done"); + } + +} +``` + +【输出】 + +``` +16:05:25.541 [Thread-0] INFO io.github.dunwu.javacore.concurrent.sync.synchronized 使用范围不当 - add start +16:05:25.544 [Thread-0] INFO io.github.dunwu.javacore.concurrent.sync.synchronized 使用范围不当 - add done +16:05:25.544 [Thread-1] INFO io.github.dunwu.javacore.concurrent.sync.synchronized 使用范围不当 - compare start +16:05:25.544 [Thread-1] INFO io.github.dunwu.javacore.concurrent.sync.synchronized 使用范围不当 - compare done +``` + +之所以出现这种错乱,是因为两个线程是交错执行 add 和 compare 方法中的业务逻辑,而且这些业务逻辑不是原子性的:a++ 和 b++ 操作中可以穿插在 compare 方法的比较代码中;更需要注意的是,a new Data().wrong()); + return Data.getCounter(); + } + + public int right(int count) { + Data.reset(); + IntStream.rangeClosed(1, count).parallel().forEach(i -> new Data().right()); + return Data.getCounter(); + } + + private static class Data { + + @Getter + private static int counter = 0; + private static Object locker = new Object(); + + public static int reset() { + counter = 0; + return counter; + } + + public synchronized void wrong() { + counter++; + } + + public void right() { + synchronized (locker) { + counter++; + } + } + + } + +} +``` + +wrong 方法中试图对一个静态对象加对象级别的 synchronized 锁,并不能保证线程安全。 + +#### 锁粒度导致的问题 + +要尽可能的缩小加锁的范围,这可以提高并发吞吐。 + +如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。 + +```java +public class synchronized 锁粒度不当 { + + public static void main(String[] args) { + Demo demo = new Demo(); + demo.wrong(); + demo.right(); + } + + private static class Demo { + + private List data = new ArrayList<>(); + + private void slow() { + try { + TimeUnit.MILLISECONDS.sleep(10); + } catch (InterruptedException e) { + } + } + + public int wrong() { + long begin = System.currentTimeMillis(); + IntStream.rangeClosed(1, 1000).parallel().forEach(i -> { + synchronized (this) { + slow(); + data.add(i); + } + }); + log.info("took:{}", System.currentTimeMillis() - begin); + return data.size(); + } + + public int right() { + long begin = System.currentTimeMillis(); + IntStream.rangeClosed(1, 1000).parallel().forEach(i -> { + slow(); + synchronized (data) { + data.add(i); + } + }); + log.info("took:{}", System.currentTimeMillis() - begin); + return data.size(); + } + + } + +} +``` + +## volatile 内存语义 + +`volatile` 是 JVM 提供的 **最轻量级的同步机制**。 + +被 `volatile` 修饰的变量,具备以下特性: + +- **线程可见性** +- **禁止指令重排序** +- **不保证原子性** + +线程安全需要具备:可见性、有序性、原子性。然而,`volatile` 不保证原子性,因此它不能彻底地保证线程安全。这也正如其命名,`volatile` 的中文意思是不稳定的,易变的。 + +### 保证线程可见性 + +这里的**可见性是指当一条线程修改了 volatile 变量的值,新值对于其他线程来说是可以立即得知的**。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。 + +**线程写 volatile 变量的过程:** + +1. 改变线程工作内存中 volatile 变量副本的值 +2. 将改变后的副本的值从工作内存刷新到主内存 + +**线程读 volatile 变量的过程:** + +1. 从主内存中读取 volatile 变量的最新值到线程的工作内存中 +2. 从工作内存中读取 volatile 变量的副本 + +> 注意:**保证可见性不等同于 volatile 变量保证并发操作的安全性** +> +> 在不符合以下两点的场景中,仍然要通过枷锁来保证原子性: +> +> - 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。 +> - 变量不需要与其他状态变量共同参与不变约束。 + +但是如果多个线程同时把更新后的变量值同时刷新回主内存,可能导致得到的值不是预期结果: + +举个例子: 定义 `volatile int count = 0`,2 个线程同时执行 count++ 操作,每个线程都执行 500 次,最终结果小于 1000,原因是每个线程执行 count++ 需要以下 3 个步骤: + +1. 线程从主内存读取最新的 count 的值 +2. 执行引擎把 count 值加 1,并赋值给线程工作内存 +3. 线程工作内存把 count 值保存到主内存 有可能某一时刻 2 个线程在步骤 1 读取到的值都是 100,执行完步骤 2 得到的值都是 101,最后刷新了 2 次 101 保存到主内存。 + +### 禁止指令重排序 + +观察加入 `volatile` 关键字和没有加入 `volatile` 关键字时所生成的汇编代码发现,**加入 `volatile` 关键字时,会多出一个 `lock` 前缀指令**。 + +**`lock` 前缀指令实际上相当于一个内存屏障**(也成内存栅栏),内存屏障会提供 3 个功能: + +- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; +- 它会强制将对缓存的修改操作立即写入主存; +- 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。 + +在 Java 中,`Unsafe` 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异: + +```java +public native void loadFence(); +public native void storeFence(); +public native void fullFence(); +``` + +理论上来说,你通过这个三个方法也可以实现和 `volatile` 禁止重排序一样的效果,只是会麻烦一些。 + +下面我以一个常见的面试题为例讲解一下 `volatile` 关键字禁止指令重排序的效果。 + +【示例】双重校验锁实现单例模式 + +```java +public class Singleton { + + private volatile static Singleton instance; + + private Singleton() { } + + public static Singleton getInstance() { + if (instance == null) { + synchronized (Singleton.class) { + if (instance == null) { + instance = new Singleton(); + } + } + } + return instance; + } + +} +``` + +`instance` 采用 `volatile` 关键字修饰也是很有必要的, `instance = new Singleton();` 这段代码其实是分为三步执行: + +1. 为 `instance` 分配内存空间 +2. 初始化 `instance` +3. 将 `instance` 指向分配的内存地址 + +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getInstance`() 后发现 `instance` 不为空,因此返回 `instance`,但此时 `instance` 还未被初始化。 + +### volatile 不保证原子性 + +线程安全需要具备:可见性、有序性、原子性。然而,**`volatile` 不保证原子性,所以决定了它不能彻底地保证线程安全**。 + +我们通过下面的代码即可证明: + +```java +public class VolatileAtomicityDemo { + public volatile static int inc = 0; + + public void increase() { + inc++; + } + + public static void main(String[] args) throws InterruptedException { + ExecutorService threadPool = Executors.newFixedThreadPool(5); + VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo(); + for (int i = 0; i < 5; i++) { + threadPool.execute(() -> { + for (int j = 0; j < 500; j++) { + volatileAtomicityDemo.increase(); + } + }); + } + // 等待 1.5 秒,保证上面程序执行完成 + Thread.sleep(1500); + System.out.println(inc); + threadPool.shutdown(); + } +} +``` + +正常情况下,运行上面的代码理应输出 `2500`。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 `2500`。 + +为什么会出现这种情况呢?不是说好了,`volatile` 可以保证变量的可见性嘛! + +也就是说,如果 `volatile` 能保证 `inc++` 操作的原子性的话。每个线程中对 `inc` 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5\*500=2500。 + +很多人会误认为自增操作 `inc++` 是原子性的,实际上,`inc++` 其实是一个复合操作,包括三步: + +1. 读取 inc 的值。 +2. 对 inc 加 1。 +3. 将 inc 的值写回内存。 + +`volatile` 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现: + +1. 线程 1 对 `inc` 进行读取操作之后,还未对其进行修改。线程 2 又读取了 `inc` 的值并对其进行修改(+1),再将 `inc` 的值写回内存。 +2. 线程 2 操作完毕后,线程 1 对 `inc` 的值进行修改(+1),再将 `inc` 的值写回内存。 + +这也就导致两个线程分别对 `inc` 进行了一次自增操作后,`inc` 实际上只增加了 1。 + +其实,如果想要保证上面的代码运行正确也非常简单,利用 `synchronized`、`Lock` 或者 `AtomicInteger` 都可以。 + +::: tabs#线程安全的计数器 + +@tab `synchronized` 改进 + +使用 `synchronized` 改进: + +```java +public synchronized void increase() { + inc++; +} +``` + +@tab `AtomicInteger` 改进 + +使用 `AtomicInteger` 改进: + +```java +public AtomicInteger inc = new AtomicInteger(); + +public void increase() { + inc.getAndIncrement(); +} +``` + +@tab `ReentrantLock` 改进 + +使用 `ReentrantLock` 改进: + +```java +Lock lock = new ReentrantLock(); +public void increase() { + lock.lock(); + try { + inc++; + } finally { + lock.unlock(); + } +} +``` + +::: + +### volatile 的应用 + +如果 `volatile` 变量修饰符使用恰当的话,它比 `synchronized` 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。但是,**`volatile` 无法替代 `synchronized` ,因为 `volatile` 无法保证操作的原子性**。 + +`volatile` 和 `synchronized` 的区别在于: + +- `volatile` 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;`synchronized` 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 +- `volatile` 仅能修饰变量;`synchronized` 可以修饰方法和代码块。 +- `volatile` 仅能实现变量的修改可见性,不能保证原子性;而 `synchronized` 则可以保证变量的修改可见性和原子性 +- `volatile` 不会造成线程的阻塞;`synchronized` 可能会造成线程的阻塞。 +- `volatile` 标记的变量不会被编译器优化;`synchronized` 标记的变量可以被编译器优化。 + +通常来说,**使用 `volatile` 必须具备以下 2 个条件**: + +- **对变量的写操作不依赖于当前值** +- **该变量没有包含在具有其他变量的表达式中** + +::: tabs#volatile 的应用 + +@tab 状态标记量 + +【示例】状态标记量 + +```java +volatile boolean flag = false; + +while(!flag) { + doSomething(); +} + +public void setFlag() { + flag = true; +} +``` + +@tab 双重锁实现线程安全的单例模式 + +【示例】双重锁实现线程安全的单例模式 + +```java +class Singleton { + private volatile static Singleton instance = null; + + private Singleton() {} + + public static Singleton getInstance() { + if(instance==null) { + synchronized (Singleton.class) { + if(instance==null) + instance = new Singleton(); + } + } + return instance; + } +} +``` + +::: + +## final 内存语义 + +我们知道,`final` 成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。 `final` 关键字的可见性是指:**final 字段一旦在声明时或构造器中初始化完成,则其他线程无需同步就能正确看见字段值**。这是因为一旦初始化完成,final 变量的值立刻回写到主内存。 + +## long 和 double 的特殊规则 + +JMM 要求 `lock`、`unlock`、`read`、`load`、`assign`、`use`、`store`、`write` 这 8 种操作都具有原子性,但是对于 64 位的数据类型(long 和 double),在模型中特别定义相对宽松的规定:**允许虚拟机将没有被 `volatile` 修饰的 64 位数据的读写操作分为 2 次 32 位的操作来进行**,即允许虚拟机可选择不保证 64 位数据类型的 `load`、`store`、`read` 和 `write` 这 4 个操作的原子性。由于这种非原子性,有可能导致其他线程读到同步未完成的“32 位的半个变量”的值。 + +不过实际开发中,Java 内存模型强烈建议虚拟机把 64 位数据的读写实现为具有原子性,目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的 `long` 和 `double` 变量专门声明为 `volatile`。 + +## 参考资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) +- [理解 Java 内存模型](https://juejin.im/post/5bf2977751882505d840321d) +- [Java 并发编程:volatile 关键字解析](http://www.cnblogs.com/dolphin0520/p/3920373.html) +- [Java 并发编程:synchronized](http://www.cnblogs.com/dolphin0520/p/3923737.html) +- [深入理解 Java 并发之 synchronized 实现原理](https://blog.csdn.net/javazejian/article/details/72828483) +- [synchronized 实现原理及锁优化](https://nicky-chen.github.io/2018/05/14/synchronized-principle/) +- [Time, Clocks, and the Ordering of Events in a Distributed System](https://lamport.azurewebsites.net/pubs/time-clocks.pdf),[译文](https://cloud.tencent.com/developer/article/1163428),[解读](https://zhuanlan.zhihu.com/p/56146800) - Lamport 介绍 happened before、偏序关系(partial ordering)、逻辑时钟(Logical Clocks)概念,提出解决分布式系统中区分事件发生的时序问题的方法。 diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\210\206\345\267\245\345\267\245\345\205\267.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\210\206\345\267\245\345\267\245\345\205\267.md" new file mode 100644 index 0000000000..32bcdfb12d --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\210\206\345\267\245\345\267\245\345\205\267.md" @@ -0,0 +1,429 @@ +--- +title: Java 并发之分工工具 +date: 2020-07-14 15:27:46 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 + - FutureTask + - CompletableFuture + - CompletionStage + - CompletionService + - ForkJoinPool +permalink: /pages/5420c8d3/ +--- + +# Java 并发之分工工具 + +**对于简单的并行任务,你可以通过“线程池 + Future”的方案来解决;如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;而批量的并行任务,则可以通过 CompletionService 来解决。** + +## FutureTask + +FutureTask 有两个构造函数: + +```java +FutureTask(Callable callable); +FutureTask(Runnable runnable, V result); +``` + +`FutureTask` 实现了 `Runnable` 和 `Future` 接口。由于实现了 `Runnable` 接口,所以可以将 `FutureTask` 对象作为任务提交给 `ThreadPoolExecutor` 去执行,也可以直接被 `Thread` 执行;又因为实现了 `Future` 接口,所以也能用来获得任务的执行结果。 + +下面,通过一组示例来展示 FutureTask 如何分别交给线程池、线程执行。 + +::: tabs#创建 FutureTask 示例 + +@tab `FutureTask` 交给线程池执行 + +【示例】`FutureTask` 交给线程池执行 + +```java +public class FutureTaskDemo { + + public static void main(String[] args) throws ExecutionException, InterruptedException { + // 创建 FutureTask + Task task = new Task(); + FutureTask f1 = new FutureTask<>(task); + FutureTask f2 = new FutureTask<>(task); + + // 创建线程池 + ExecutorService executor = Executors.newCachedThreadPool(); + executor.submit(f1); + executor.submit(f2); + System.out.println(f1.get()); + System.out.println(f2.get()); + executor.shutdown(); + } + + static class Task implements Callable { + + @Override + public String call() { + return Thread.currentThread().getName() + " 执行成功!"; + } + + } + +} +// 输出 +// pool-1-thread-1 执行成功! +// pool-1-thread-2 执行成功! +``` + +@tab `FutureTask` 交给线程执行 + +【示例】`FutureTask` 交给线程执行 + +```java +public class FutureTaskDemo2 { + + public static void main(String[] args) throws InterruptedException, ExecutionException { + + // 创建 FutureTask + Task task = new Task(); + FutureTask f1 = new FutureTask<>(task); + FutureTask f2 = new FutureTask<>(task); + + // 创建线程 + new Thread(f1).start(); + new Thread(f2).start(); + System.out.println(f1.get()); + System.out.println(f2.get()); + } + + static class Task implements Callable { + + @Override + public String call() { + return Thread.currentThread().getName() + " 执行成功!"; + } + + } + +} +// 输出 +// Thread-0 执行成功! +// Thread-1 执行成功! +``` + +@tab 用 `FutureTask` 完成并行计算 + +【示例】用 `FutureTask` 完成并行计算 + +```java +public class FutureTaskDemo3 { + + public static void main(String[] args) throws InterruptedException, ExecutionException { + + // 创建一个线程池来执行任务 + ExecutorService executor = Executors.newFixedThreadPool(2); + + // 创建两个 Callable 对象 + Callable t1 = () -> { + int result = 0; + for (int i = 1; i <= 100; i++) { + result += i; + } + return result; + }; + Callable t2 = () -> { + int result = 0; + for (int i = 101; i <= 200; i++) { + result += i; + } + return result; + }; + + // 创建两个 FutureTask 对象 + FutureTask f1 = new FutureTask<>(t1); + FutureTask f2 = new FutureTask<>(t2); + + // 提交任务到线程池执行 + executor.execute(f1); + executor.execute(f2); + + // 获取任务的结果 + Integer value1 = f1.get(); + Integer value2 = f2.get(); + System.out.println("total = " + value1 + value2); + + // 关闭线程池 + executor.shutdown(); + } + +} +``` + +::: + +## CompletableFuture + +JDK8 提供了 CompletableFuture 来支持异步编程。 + +CompletableFuture 提供了四个静态方法来创建一个异步操作。 + +```java +// 使用默认线程池 +public static CompletableFuture runAsync(Runnable runnable) { // 省略 } +public static CompletableFuture supplyAsync(Supplier supplier) { // 省略 } + +// 使用自定义线程池 +public static CompletableFuture runAsync(Runnable runnable, Executor executor) { // 省略 } +public static CompletableFuture supplyAsync(Supplier supplier, Executor executor) { // 省略 } +``` + +上面的 4 个静态方法中,有 2 个 `runAsync` 方法,2 个 `supplyAsync` 方法,它们的区别是: + +- `runAsync` 方法没有返回值。 +- `supplyAsync` 方法有返回值。 + +默认情况下 `CompletableFuture` 会使用公共的 `ForkJoinPool` 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option: `-Djava.util.concurrent.ForkJoinPool.common.parallelism` 来设置 `ForkJoinPool` 线程池的线程数)。如果所有 `CompletableFuture` 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要**根据不同的业务类型创建不同的线程池,以避免互相干扰**。 + +## CompletionStage + +CompletionStage 接口可以清晰地描述任务之间的时序关系,如**串行关系、并行关系、汇聚关系**等。 + +### 串行关系 + +CompletionStage 接口里面描述串行关系,主要是 `thenApply`、`thenAccept`、`thenRun` 和 `thenCompose` 这四个系列的接口。 + +`thenApply` 系列函数里参数 `fn` 的类型是接口 `Function`,这个接口里与 `CompletionStage` 相关的方法是 `R apply(T t)`,这个方法既能接收参数也支持返回值,所以 `thenApply` 系列方法返回的是`CompletionStage`。 + +`thenAccept` 系列方法里参数 `consumer` 的类型是接口 `Consumer`,这个接口里与 `CompletionStage` 相关的方法是 `void accept(T t)`,这个方法虽然支持参数,但却不支持回值,所以 `thenAccept` 系列方法返回的是`CompletionStage`。 + +`thenRun` 系列方法里 `action` 的参数是 `Runnable`,所以 `action` 既不能接收参数也不支持返回值,所以 thenRun 系列方法返回的也是 `CompletionStage`。 + +这些方法里面 Async 代表的是异步执行 `fn`、`consumer` 或者 `action`。其中,需要你注意的是 `thenCompose` 系列方法,这个系列的方法会新创建出一个子流程,最终结果和 `thenApply` 系列是相同的。 + +```java +CompletionStage thenApply(fn); +CompletionStage thenApplyAsync(fn); +CompletionStage thenAccept(consumer); +CompletionStage thenAcceptAsync(consumer); +CompletionStage thenRun(action); +CompletionStage thenRunAsync(action); +CompletionStage thenCompose(fn); +CompletionStage thenComposeAsync(fn); +``` + +### 描述 AND 汇聚关系 + +`CompletionStage` 接口里面描述 AND 汇聚关系,主要是 `thenCombine`、`thenAcceptBoth` 和 `runAfterBoth` 系列的接口,这些接口的区别也是源自 `fn`、`consumer`、`action` 这三个核心参数不同。 + +```java +CompletionStage thenCombine(other, fn); +CompletionStage thenCombineAsync(other, fn); +CompletionStage thenAcceptBoth(other, consumer); +CompletionStage thenAcceptBothAsync(other, consumer); +CompletionStage runAfterBoth(other, action); +CompletionStage runAfterBothAsync(other, action); +``` + +### 描述 OR 汇聚关系 + +`CompletionStage` 接口里面描述 OR 汇聚关系,主要是 `applyToEither`、`acceptEither` 和 `runAfterEither` 系列的接口,这些接口的区别也是源自 `fn`、`consumer`、`action` 这三个核心参数不同。 + +```java +CompletionStage applyToEither(other, fn); +CompletionStage applyToEitherAsync(other, fn); +CompletionStage acceptEither(other, consumer); +CompletionStage acceptEitherAsync(other, consumer); +CompletionStage runAfterEither(other, action); +CompletionStage runAfterEitherAsync(other, action); +``` + +下面的示例代码展示了如何使用 applyToEither() 方法来描述一个 OR 汇聚关系。 + +```java +CompletableFuture f1 = CompletableFuture.supplyAsync(() -> { + int t = getRandom(5, 10); + sleep(t, TimeUnit.SECONDS); + return String.valueOf(t); +}); + +CompletableFuture f2 = CompletableFuture.supplyAsync(() -> { + int t = getRandom(5, 10); + sleep(t, TimeUnit.SECONDS); + return String.valueOf(t); +}); + +CompletableFuture f3 = f1.applyToEither(f2, s -> s); +System.out.println(f3.join()); +``` + +### 异常处理 + +虽然上面我们提到的 `fn`、`consumer`、`action` 它们的核心方法都**不允许抛出可检查异常,但是却无法限制它们抛出运行时异常**,例如下面的代码,执行 `7/0` 就会出现除零错误这个运行时异常。非异步编程里面,我们可以使用 `try {} catch {}` 来捕获并处理异常,那在异步编程里面,异常该如何处理呢? + +```java +CompletableFuture f = CompletableFuture.supplyAsync(() -> (7 / 0)) + .thenApply(r -> r * 10); +System.out.println(f.join()); +``` + +`CompletionStage` 接口给我们提供的方案非常简单,比 `try {} catch {}` 还要简单,下面是相关的方法,使用这些方法进行异常处理和串行操作是一样的,都支持链式编程方式。 + +```java +CompletionStage exceptionally(fn); +CompletionStage whenComplete(consumer); +CompletionStage whenCompleteAsync(consumer); +CompletionStage handle(fn); +CompletionStage handleAsync(fn); +``` + +下面的示例代码展示了如何使用 `exceptionally()` 方法来处理异常,`exceptionally()` 的使用非常类似于 `try {} catch {}` 中的 `catch {}`,但是由于支持链式编程方式,所以相对更简单。既然有 `try {} catch {}`,那就一定还有 `try {} catch {}`,`whenComplete()` 和 `handle()` 系列方法就类似于 `try {} catch {}` 中的 `finally {}`,无论是否发生异常都会执行 `whenComplete()` 中的回调函数 `consumer` 和 `handle()` 中的回调函数 `fn`。`whenComplete()` 和 `handle()` 的区别在于 `whenComplete()` 不支持返回结果,而 `handle()` 是支持返回结果的。 + +```java +CompletableFuture f = CompletableFuture.supplyAsync(() -> 7 / 0) + .thenApply(r -> r * 10) + .exceptionally(e -> 0); +System.out.println(f.join()); +``` + +## CompletionService + +`CompletionService` 接口的实现类是 `ExecutorCompletionService`,这个实现类的构造方法有两个,分别是: + +1. `ExecutorCompletionService(Executor executor)`; +2. `ExecutorCompletionService(Executor executor, BlockingQueue> completionQueue)`。 + +这两个构造方法都需要传入一个线程池,如果不指定 `completionQueue`,那么默认会使用无界的 `LinkedBlockingQueue`。任务执行结果的 `Future` 对象就是加入到 `completionQueue` 中。 + +下面的示例代码完整地展示了如何利用 `CompletionService` 来实现高性能的询价系统。其中,我们没有指定 `completionQueue`,因此默认使用无界的 `LinkedBlockingQueue`。之后通过 `CompletionService` 接口提供的 `submit()` 方法提交了三个询价操作,这三个询价操作将会被 `CompletionService` 异步执行。最后,我们通过 CompletionService 接口提供的 `take()` 方法获取一个 `Future` 对象(前面我们提到过,加入到阻塞队列中的是任务执行结果的 `Future` 对象),调用 `Future` 对象的 `get()` 方法就能返回询价操作的执行结果了。 + +```java +// 创建线程池 +ExecutorService executor = Executors.newFixedThreadPool(3); +// 创建 CompletionService +CompletionService cs = new ExecutorCompletionService<>(executor); +// 异步向电商 S1 询价 +cs.submit(()->getPriceByS1()); +// 异步向电商 S2 询价 +cs.submit(()->getPriceByS2()); +// 异步向电商 S3 询价 +cs.submit(()->getPriceByS3()); +// 将询价结果异步保存到数据库 +for (int i=0; i<3; i++) { + Integer r = cs.take().get(); + executor.execute(()->save(r)); +} +``` + +CompletionService 接口提供的方法有 5 个,这 5 个方法的方法签名如下所示。 + +其中,submit() 相关的方法有两个。一个方法参数是`Callable task`,前面利用 CompletionService 实现询价系统的示例代码中,我们提交任务就是用的它。另外一个方法有两个参数,分别是`Runnable task`和`V result`,这个方法类似于 ThreadPoolExecutor 的 ` Future submit(Runnable task, T result)` ,这个方法在 [《23 | Future:如何用多线程实现最优的“烧水泡茶”程序?》](https://time.geekbang.org/column/article/91292) 中我们已详细介绍过,这里不再赘述。 + +CompletionService 接口其余的 3 个方法,都是和阻塞队列相关的,take()、poll() 都是从阻塞队列中获取并移除一个元素;它们的区别在于如果阻塞队列是空的,那么调用 take() 方法的线程会被阻塞,而 poll() 方法会返回 null 值。 `poll(long timeout, TimeUnit unit)` 方法支持以超时的方式获取并移除阻塞队列头部的一个元素,如果等待了 timeout unit 时间,阻塞队列还是空的,那么该方法会返回 null 值。 + +```java +Future submit(Callable task); +Future submit(Runnable task, V result); +Future take() throws InterruptedException; +Future poll(); +Future poll(long timeout, TimeUnit unit) throws InterruptedException; +``` + +当需要批量提交异步任务的时候建议你使用 CompletionService。CompletionService 将线程池 Executor 和阻塞队列 BlockingQueue 的功能融合在了一起,能够让批量异步任务的管理更简单。除此之外,CompletionService 能够让异步任务的执行结果有序化,先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如 Forking Cluster 这样的需求。 + +CompletionService 的实现类 ExecutorCompletionService,需要你自己创建线程池,虽看上去有些啰嗦,但好处是你可以让多个 ExecutorCompletionService 的线程池隔离,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险。 + +## ForkJoinPool + +Fork/Join 是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的** Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并**。Fork/Join 计算框架主要包含两部分,一部分是**分治任务的线程池 ForkJoinPool**,另一部分是**分治任务 ForkJoinTask**。这两部分的关系类似于 ThreadPoolExecutor 和 Runnable 的关系,都可以理解为提交任务到线程池,只不过分治任务有自己独特类型 ForkJoinTask。 + +ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,其中 fork() 方法会异步地执行一个子任务,而 join() 方法则会阻塞当前线程来等待子任务的执行结果。ForkJoinTask 有两个子类——RecursiveAction 和 RecursiveTask,通过名字你就应该能知道,它们都是用递归的方式来处理分治任务的。这两个子类都定义了抽象方法 compute(),不过区别是 RecursiveAction 定义的 compute() 没有返回值,而 RecursiveTask 定义的 compute() 方法是有返回值的。这两个子类也是抽象类,在使用的时候,需要你定义子类去扩展。 + +Fork/Join 并行计算的核心组件是 ForkJoinPool,所以下面我们就来简单介绍一下 ForkJoinPool 的工作原理。 + +ForkJoinPool 本质上也是一个生产者 - 消费者的实现,但是更加智能,你可以参考下面的 ForkJoinPool 工作原理图来理解其原理。ThreadPoolExecutor 内部只有一个任务队列,而 ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。 + +如果工作线程对应的任务队列空了,是不是就没活儿干了呢?不是的,ForkJoinPool 支持一种叫做“**任务窃取**”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务,例如下图中,线程 T2 对应的任务队列已经空了,它可以“窃取”线程 T1 对应的任务队列的任务。如此一来,所有的工作线程都不会闲下来了。 + +ForkJoinPool 中的任务队列采用的是双端队列,工作线程正常获取任务和“窃取任务”分别是从任务队列不同的端消费,这样能避免很多不必要的数据竞争。我们这里介绍的仅仅是简化后的原理,ForkJoinPool 的实现远比我们这里介绍的复杂,如果你感兴趣,建议去看它的源码。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200703141326.png) + +【示例】模拟 MapReduce 统计单词数量 + +```java + +static void main(String[] args) { + String[] fc = { "hello world", + "hello me", + "hello fork", + "hello join", + "fork join in world" }; + //创建 ForkJoin 线程池 + ForkJoinPool fjp = new ForkJoinPool(3); + //创建任务 + MR mr = new MR(fc, 0, fc.length); + //启动任务 + Map result = fjp.invoke(mr); + //输出结果 + result.forEach((k, v) -> System.out.println(k + ":" + v)); +} + +//MR 模拟类 +static class MR extends RecursiveTask> { + + private String[] fc; + private int start, end; + + //构造函数 + MR(String[] fc, int fr, int to) { + this.fc = fc; + this.start = fr; + this.end = to; + } + + @Override + protected Map compute() { + if (end - start == 1) { + return calc(fc[start]); + } else { + int mid = (start + end) / 2; + MR mr1 = new MR(fc, start, mid); + mr1.fork(); + MR mr2 = new MR(fc, mid, end); + //计算子任务,并返回合并的结果 + return merge(mr2.compute(), + mr1.join()); + } + } + + //合并结果 + private Map merge(Map r1, Map r2) { + Map result = new HashMap<>(); + result.putAll(r1); + //合并结果 + r2.forEach((k, v) -> { + Long c = result.get(k); + if (c != null) { result.put(k, c + v); } else { result.put(k, v); } + }); + return result; + } + + //统计单词数量 + private Map calc(String line) { + Map result = new HashMap<>(); + //分割单词 + String[] words = line.split("\\s+"); + //统计单词数量 + for (String w : words) { + Long v = result.get(w); + if (v != null) { result.put(w, v + 1); } else { result.put(w, 1L); } + } + return result; + } +} +``` + +## 参考资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) +- [CompletableFuture 使用详解](https://www.jianshu.com/p/6bac52527ca4) diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\220\214\346\255\245\345\267\245\345\205\267.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\220\214\346\255\245\345\267\245\345\205\267.md" new file mode 100644 index 0000000000..f4c56f43aa --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\220\214\346\255\245\345\267\245\345\205\267.md" @@ -0,0 +1,394 @@ +--- +title: Java 并发之同步工具 +date: 2019-12-24 23:52:25 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 + - CountDownLatch + - CyclicBarrier + - Semaphore +permalink: /pages/8e4455a2/ +--- + +# Java 并发之同步工具 + +## Semaphore + +**`Semaphore` 译为信号量,是一种同步机制,用于控制多线程对共享资源的访问**。信号量是由计算机科学家 Edsger Dijkstra 于 1965 年提出的,用于解决所谓的“临界区”问题,即多个进程或线程试图同时访问共享资源(如打印机、内存缓冲区等)时可能出现的问题。 + +### 信号量模型 + +信号量模型还是很简单的,可以简单概括为:**一个计数器,一个等待队列,三个方法**。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Java%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%e5%ae%9e%e6%88%98/assets/6dfeeb9180ff3e038478f2a7dccc9b5c.png) + +- 这三个方法详细的语义具体如下所示。 + + - init():设置计数器的初始值。 + - down():计数器的值减 1;如果此时计数器的值小于 0,则当前线程将被阻塞,否则当前线程可以继续执行。 + - up():计数器的值加 1;如果此时计数器的值小于或者等于 0,则唤醒等待队列中的一个线程,并将其从等待队列中移除。 + + 这里提到的 init()、down() 和 up() 三个方法都是原子性的,并且这个原子性是由信号量模型的实现方保证的。在 Java 中,信号量模型是由 java.util.concurrent.Semaphore 实现的,Semaphore 这个类能够保证这三个方法都是原子操作。 + + 信号量模型里面,down()、up() 这两个操作历史上最早称为 P 操作和 V 操作,所以信号量模型也被称为 PV 原语。 + +### Semaphore 使用 + +`Semaphore` 提供了 2 个构造方法: + +```java +// 参数 permits 表示许可数目,即同时可以允许多少线程进行访问 +public Semaphore(int permits) {} +// 参数 fair 表示是否是公平的,即等待时间越久的越先获取许可 +public Semaphore(int permits, boolean fair) {} +``` + +说明: + +- `permits` - 初始化固定数量的 permit。 +- `fair` - 设置是否为公平模式。所谓公平,是指等待久的优先获取 permit。 + +`Semaphore` 的重要方法: + +```java +// 获取 1 个许可 +public void acquire() throws InterruptedException {} +//获取 permits 个许可 +public void acquire(int permits) throws InterruptedException {} +// 释放 1 个许可 +public void release() {} +//释放 permits 个许可 +public void release(int permits) {} +``` + +说明: + +- `acquire()` - 获取 1 个 permit。 +- `acquire(int permits)` - 获取 permits 数量的 permit。 +- `release()` - 释放 1 个 permit。 +- `release(int permits)` - 释放 permits 数量的 permit。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/Semaphore.png) + +【示例】Semaphore 使用示例 + +```java +public class SemaphoreDemo { + + private static final int THREAD_COUNT = 30; + + private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT); + + private static Semaphore semaphore = new Semaphore(10); + + public static void main(String[] args) { + for (int i = 0; i < THREAD_COUNT; i++) { + threadPool.execute(new Runnable() { + @Override + public void run() { + try { + semaphore.acquire(); + System.out.println("save data"); + semaphore.release(); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }); + } + + threadPool.shutdown(); + } + +} +``` + +### Semaphore 原理 + +`Semaphore` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `permits`,你可以将 `permits` 的值理解为许可证的数量,只有拿到许可证的线程才能执行。 + +调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state >= 0` 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果 `state<0` 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。 + +```java +/** + * 获取 1 个许可证 + */ +public void acquire() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} +/** + * 共享模式下获取许可证,获取成功则返回,失败则加入阻塞队列,挂起线程 + */ +public final void acquireSharedInterruptibly(int arg) + throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); + // 尝试获取许可证,arg 为获取许可证个数,当可用许可证数减当前获取的许可证数结果小于 0, 则创建一个节点加入阻塞队列,挂起当前线程。 + if (tryAcquireShared(arg) < 0) + doAcquireSharedInterruptibly(arg); +} +``` + +调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state>=0` 则获取令牌成功,否则重新进入阻塞队列,挂起线程。 + +```java +// 释放一个许可证 +public void release() { + sync.releaseShared(1); +} + +// 释放共享锁,同时会唤醒同步队列中的一个线程。 +public final boolean releaseShared(int arg) { + //释放共享锁 + if (tryReleaseShared(arg)) { + //唤醒同步队列中的一个线程 + doReleaseShared(); + return true; + } + return false; +} +``` + +### 实现一个限流器 + +Semaphore 最重要的特性是:**Semaphore 可以允许多个线程访问一个临界区**。 + +Semaphore 在现实中有很多应用场景: + +- 各种池化资源,例如连接池、对象池、线程池等; +- 信号量限流(例如 Hystrix 就支持信号量限流模式); + +【示例】一个基于信号量实现的简单对象限流器 + +```java +public class SemaphoreRateLimit { + + public static void main(String[] args) { + // 创建对象池,大小为 10 + ObjectPool pool = new ObjectPool<>(10, 2L); + // 通过对象池获取 t,之后执行 + pool.exec(t -> { + System.out.println(t); + return t.toString(); + }); + } + + static class ObjectPool { + + final List pool; + // 用信号量实现限流器 + final Semaphore sem; + + // 构造函数 + ObjectPool(int size, T t) { + pool = new Vector() { }; + for (int i = 0; i < size; i++) { + pool.add(t); + } + sem = new Semaphore(size); + } + + // 利用对象池的对象,调用 func + R exec(Function func) { + T t = null; + try { + sem.acquire(); + t = pool.remove(0); + return func.apply(t); + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + pool.add(t); + sem.release(); + return null; + } + } + + } + +} +``` + +在这个方法里面,我们首先调用 acquire() 方法(与之匹配的是在 finally 里面调用 release() 方法),假设对象池的大小是 10,信号量的计数器初始化为 10,那么前 10 个线程调用 acquire() 方法,都能继续执行,相当于通过了信号量,而其他线程则会阻塞在 acquire() 方法上。对于通过信号量的线程,我们为每个线程分配了一个对象 t(这个分配工作是通过 pool.remove(0) 实现的),分配完之后会执行一个回调函数 func,而函数的参数正是前面分配的对象 t ;执行完回调函数之后,它们就会释放对象(这个释放工作是通过 pool.add(t) 实现的),同时调用 release() 方法来更新信号量的计数器。如果此时信号量里计数器的值小于等于 0,那么说明有线程在等待,此时会自动唤醒等待的线程。 + +## CountDownLatch + +**`CountDownLatch`** 字面意思为递减计数锁。用于**控制一个线程等待多个线程**。 + +`CountDownLatch` 内部维护了一个计数器,表示需要等待的事件数量。`countDown` 方法递减计数器,表示有一个事件已经发生。调用 `await` 方法的线程会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。`CountDownLatch` 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 `CountDownLatch` 使用完毕后,它不能再次被使用。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/CountDownLatch.png) + +`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。直到`count` 个线程调用了`countDown()`使 state 值被减为 0,或者调用`await()`的线程被中断,该线程才会从阻塞中被唤醒,`await()` 方法之后的语句得到执行。 + +`CountDownLatch` 唯一的构造方法: + +```java +// 初始化计数器 +public CountDownLatch(int count) {}; +``` + +`CountDownLatch` 的重要方法: + +```java +public void await() throws InterruptedException { }; +public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; +public void countDown() { }; +``` + +说明: + +- `await()` - 调用 `await()` 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行。 +- `await(long timeout, TimeUnit unit)` - 和 `await()` 类似,只不过等待一定的时间后 count 值还没变为 0 的话就会继续执行 +- `countDown()` - 将统计值 count 减 1 + +【示例】CountDownLatch 使用示例 + +```java +public class CountDownLatchDemo { + + public static void main(String[] args) { + final CountDownLatch latch = new CountDownLatch(2); + + new Thread(new MyThread(latch)).start(); + + try { + System.out.println("等待 2 个子线程执行完毕。.."); + latch.await(); + System.out.println("2 个子线程已经执行完毕"); + System.out.println("继续执行主线程"); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + static class MyThread implements Runnable { + + private CountDownLatch latch; + + public MyThread(CountDownLatch latch) { + this.latch = latch; + } + + @Override + public void run() { + System.out.println("子线程" + Thread.currentThread().getName() + "正在执行"); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕"); + latch.countDown(); + } + + } + +} +``` + +## CyclicBarrier + +`CyclicBarrier` 字面意思是循环栅栏。**`CyclicBarrier` 可以让一组线程等待至某个状态(遵循字面意思,不妨称这个状态为栅栏)之后再全部同时执行**。之所以叫循环栅栏是因为:**当所有等待线程都被释放以后,`CyclicBarrier` 可以被重用**。 + +`CyclicBarrier` 是基于 `ReentrantLock` (`ReentrantLock` 底层也是基于 AQS 实现的)和 `Condition` 实现的。`CyclicBarrier` 内部维护一个计数器,每次执行 `await` 方法之后,计数器加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。 + +`CyclicBarrier` 在并行迭代算法中非常有用。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/CyclicBarrier.png) + +`CyclicBarrier` 提供了 2 个构造方法 + +```java +public CyclicBarrier(int parties) {} +public CyclicBarrier(int parties, Runnable barrierAction) {} +``` + +说明: + +- `parties` - `parties` 数相当于一个阈值,当有 `parties` 数量的线程在等待时, `CyclicBarrier` 处于栅栏状态。 +- `barrierAction` - 当 `CyclicBarrier` 处于栅栏状态时执行的动作。 + +`CyclicBarrier` 的重要方法: + +```java +public int await() throws InterruptedException, BrokenBarrierException {} +public int await(long timeout, TimeUnit unit) + throws InterruptedException, + BrokenBarrierException, + TimeoutException {} +// 将屏障重置为初始状态 +public void reset() {} +``` + +说明: + +- `await()` - 等待调用 `await()` 的线程数达到屏障数。如果当前线程是最后一个到达的线程,并且在构造函数中提供了非空屏障操作,则当前线程在允许其他线程继续之前运行该操作。如果在屏障动作期间发生异常,那么该异常将在当前线程中传播并且屏障被置于断开状态。 +- `await(long timeout, TimeUnit unit)` - 相比于 `await()` 方法,这个方法让这些线程等待至一定的时间,如果还有线程没有到达栅栏状态就直接让到达栅栏状态的线程执行后续任务。 +- `reset()` - 将屏障重置为初始状态。 + +【示例】CyclicBarrier 使用示例 + +```java +public class CyclicBarrierDemo { + + final static int N = 4; + + public static void main(String[] args) { + CyclicBarrier barrier = new CyclicBarrier(N, + new Runnable() { + @Override + public void run() { + System.out.println("当前线程" + Thread.currentThread().getName()); + } + }); + + for (int i = 0; i < N; i++) { + MyThread myThread = new MyThread(barrier); + new Thread(myThread).start(); + } + } + + static class MyThread implements Runnable { + + private CyclicBarrier cyclicBarrier; + + MyThread(CyclicBarrier cyclicBarrier) { + this.cyclicBarrier = cyclicBarrier; + } + + @Override + public void run() { + System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据。.."); + try { + Thread.sleep(3000); // 以睡眠来模拟写入数据操作 + System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕"); + cyclicBarrier.await(); + } catch (InterruptedException | BrokenBarrierException e) { + e.printStackTrace(); + } + } + + } + +} +``` + +## 小结 + +- `CountDownLatch` 和 `CyclicBarrier` 都能够实现线程之间的等待,只不过它们侧重点不同: + - `CountDownLatch` 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行; + - `CyclicBarrier` 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行; + - 另外,`CountDownLatch` 是不可以重用的,而 `CyclicBarrier` 是可以重用的。 +- `Semaphore` 其实和锁有点类似,它一般用于控制对某组资源的访问权限。 + +## 参考资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [Java 并发编程:CountDownLatch、CyclicBarrier 和 Semaphore](https://www.cnblogs.com/dolphin0520/p/3920397.html) diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/06.Java\345\271\266\345\217\221\345\222\214\345\256\271\345\231\250.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\256\271\345\231\250.md" similarity index 91% rename from "source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/06.Java\345\271\266\345\217\221\345\222\214\345\256\271\345\231\250.md" rename to "source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\256\271\345\231\250.md" index a560174c41..312ccbe499 100644 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/06.Java\345\271\266\345\217\221\345\222\214\345\256\271\345\231\250.md" +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\345\256\271\345\231\250.md" @@ -1,20 +1,21 @@ --- -title: Java并发和容器 +title: Java 并发之容器 date: 2020-02-02 17:54:36 -order: 06 categories: - Java - - JavaSE + - JavaCore - 并发 tags: - Java - - JavaSE + - JavaCore - 并发 - 容器 -permalink: /pages/b067d6/ + - synchronized + - AQS +permalink: /pages/6fd8d836/ --- -# Java 并发和容器 +# Java 并发之容器 ## 同步容器 @@ -32,7 +33,7 @@ permalink: /pages/b067d6/ 同步容器的同步原理就是在其 `get`、`set`、`size` 等主要方法上用 `synchronized` 修饰。 **`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 -> 想详细了解 `synchronized` 用法和原理可以参考:[Java 并发核心机制](https://dunwu.github.io/waterdrop/pages/2c6488/) +> 想详细了解 `synchronized` 用法和原理可以参考:[Java 并发核心机制](https://dunwu.github.io/waterdrop/pages/25767945/) #### 性能问题 @@ -443,7 +444,7 @@ final V putVal(K key, V value, boolean onlyIfAbsent) { #### ConcurrentHashMap 的实战 -> 示例摘自:[《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) +> 示例摘自:[极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) ##### ConcurrentHashMap 错误示例 @@ -1027,65 +1028,8 @@ private final ReentrantLock putLock = new ReentrantLock(); private final Condition notFull = putLock.newCondition(); ``` -这里用了两对 `Lock` 和 `Condition`,简单介绍如下: - -- `takeLock` 和 `notEmpty` 搭配:如果要获取(take)一个元素,需要获取 `takeLock` 锁,但是获取了锁还不够,如果队列此时为空,还需要队列不为空(`notEmpty`)这个条件(`Condition`)。 -- `putLock` 需要和 `notFull` 搭配:如果要插入(put)一个元素,需要获取 `putLock` 锁,但是获取了锁还不够,如果队列此时已满,还需要队列不是满的(notFull)这个条件(`Condition`)。 - -### SynchronousQueue 类 - -SynchronousQueue 是**不存储元素的阻塞队列**。每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是 1 吗?其实不是的,其内部容量是 0。 - -`SynchronousQueue` 定义如下: - -```java -public class SynchronousQueue extends AbstractQueue - implements BlockingQueue, java.io.Serializable {} -``` - -`SynchronousQueue` 这个类,在线程池的实现类 `ScheduledThreadPoolExecutor` 中得到了应用。 - -`SynchronousQueue` 的队列其实是虚的,即队列容量为 0。数据必须从某个写线程交给某个读线程,而不是写到某个队列中等待被消费。 - -`SynchronousQueue` 中不能使用 peek 方法(在这里这个方法直接返回 null),peek 方法的语义是只读取不移除,显然,这个方法的语义是不符合 SynchronousQueue 的特征的。 - -`SynchronousQueue` 也不能被迭代,因为根本就没有元素可以拿来迭代的。 - -虽然 `SynchronousQueue` 间接地实现了 Collection 接口,但是如果你将其当做 Collection 来用的话,那么集合是空的。 - -当然,`SynchronousQueue` 也不允许传递 null 值的(并发包中的容器类好像都不支持插入 null 值,因为 null 值往往用作其他用途,比如用于方法的返回值代表操作失败)。 - -### ConcurrentLinkedDeque 类 - -`Deque` 的侧重点是支持对队列头尾都进行插入和删除,所以提供了特定的方法,如: - -- 尾部插入时需要的 `addLast(e)`、`offerLast(e)`。 -- 尾部删除所需要的 `removeLast()`、`pollLast()`。 - -### Queue 的并发应用 - -Queue 被广泛使用在生产者 - 消费者场景。而在并发场景,利用 `BlockingQueue` 的阻塞机制,可以减少很多并发协调工作。 - -这么多并发 Queue 的实现,如何选择呢? - -- 考虑应用场景中对队列边界的要求。`ArrayBlockingQueue` 是有明确的容量限制的,而 `LinkedBlockingQueue` 则取决于我们是否在创建时指定,`SynchronousQueue` 则干脆不能缓存任何元素。 -- 从空间利用角度,数组结构的 `ArrayBlockingQueue` 要比 `LinkedBlockingQueue` 紧凑,因为其不需要创建所谓节点,但是其初始分配阶段就需要一段连续的空间,所以初始内存需求更大。 -- 通用场景中,`LinkedBlockingQueue` 的吞吐量一般优于 `ArrayBlockingQueue`,因为它实现了更加细粒度的锁操作。 -- `ArrayBlockingQueue` 实现比较简单,性能更好预测,属于表现稳定的“选手”。 -- 可能令人意外的是,很多时候 `SynchronousQueue` 的性能表现,往往大大超过其他实现,尤其是在队列元素较小的场景。 - ## 参考资料 - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- https://blog.csdn.net/u010425776/article/details/54890215 -- https://blog.csdn.net/wangxiaotongfan/article/details/52074160 -- https://my.oschina.net/hosee/blog/675884 -- https://www.jianshu.com/p/c0642afe03e0 -- https://www.jianshu.com/p/f6730d5784ad -- http://www.javarticles.com/2012/06/copyonwritearraylist.html -- https://www.cnblogs.com/xrq730/p/5020760.html -- https://www.cnblogs.com/leesf456/p/5547853.html -- http://www.cnblogs.com/chengxiao/p/6881974.html -- http://www.cnblogs.com/dolphin0520/p/3933404.html -- [HashMap? ConcurrentHashMap? 相信看完这篇没人能难住你!](https://juejin.im/post/5b551e8df265da0f84562403) \ No newline at end of file +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\346\227\240\351\224\201.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\346\227\240\351\224\201.md" new file mode 100644 index 0000000000..6b52375b7e --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\346\227\240\351\224\201.md" @@ -0,0 +1,1193 @@ +--- +title: Java 并发之无锁 +date: 2019-12-25 22:19:09 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 + - CAS + - 原子类 + - ThreadLocal + - Immutability + - Copy-on-Write +permalink: /pages/56a038ac/ +--- + +# Java 并发之无锁 + +并发安全需要保证几个基本特性: + +- **可见性** - 是一个线程修改了某个共享变量,其状态能够立即被其他线程知晓,通常被解释为将线程本地状态反映到主内存上,`volatile` 就是负责保证可见性的。 +- **有序性** - 是保证线程内串行语义,避免指令重排等。 +- **原子性** - 简单说就是相关操作不会中途被其他线程干扰,一般通过互斥机制(加锁:`sychronized`、`Lock`)实现。 + +互斥同步是最常见的原子性保障手段。**互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题**。因此,互斥同步也被称为阻塞同步。互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 + +解决并发安全问题,还可以采用无锁方案。无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性。 + +Java 中的无锁技术有: + +- CAS +- 原子类 +- ThreadLocal +- Copy-on-Write +- 不变模式 + +## CAS + +### CAS 的要点 + +**CAS(Compare and Swap),字面意思为比较并交换。** + +CAS 涉及三个操作数: + +- V:需要读写的内存位置 +- A:进行比较的值 +- B:拟写入的新值 + +**当且仅当 V 的值等于 A 时,才会通过原子方式用新值 B 来更新 A 的值,否则什么都不做**。 + +CAS 实际是乐观锁的一种实现方式,因此,**CAS 只适用于线程冲突较少的情况**。 + +### CAS 的应用 + +CAS 的典型应用场景是: + +- 原子类 +- 自旋锁 + +#### 原子类 + +> 原子类是 CAS 在 Java 中最典型的应用。 + +我们先来看一个常见的代码片段。 + +```Java +if(a==b) { + a++; +} +``` + +如果 `a++` 执行前, a 的值被修改了怎么办?还能得到预期值吗?出现该问题的原因是在并发环境下,以上代码片段不是原子操作,随时可能被其他线程所篡改。 + +解决这种问题的最经典方式是应用原子类的 `incrementAndGet` 方法。 + +```Java +public class AtomicIntegerDemo { + + public static void main(String[] args) throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(3); + final AtomicInteger count = new AtomicInteger(0); + for (int i = 0; i < 10; i++) { + executorService.execute(new Runnable() { + @Override + public void run() { + count.incrementAndGet(); + } + }); + } + + executorService.shutdown(); + executorService.awaitTermination(3, TimeUnit.SECONDS); + System.out.println("Final Count is : " + count.get()); + } + +} +``` + +J.U.C 包中提供了 `AtomicBoolean`、`AtomicInteger`、`AtomicLong` 分别针对 `Boolean`、`Integer`、`Long` 执行原子操作,操作和上面的示例大体相似,不做赘述。 + +#### 自旋锁 + +利用原子类(本质上是 CAS),可以实现自旋锁。 + +所谓自旋锁,是指线程反复检查锁变量是否可用,直到成功为止。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 + +示例:非线程安全示例 + +```java +public class AtomicReferenceDemo { + + private static int ticket = 10; + + public static void main(String[] args) { + ExecutorService executorService = Executors.newFixedThreadPool(3); + for (int i = 0; i < 5; i++) { + executorService.execute(new MyThread()); + } + executorService.shutdown(); + } + + static class MyThread implements Runnable { + + @Override + public void run() { + while (ticket > 0) { + System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); + ticket--; + } + } + + } + +} +``` + +输出结果: + +``` +pool-1-thread-2 卖出了第 10 张票 +pool-1-thread-1 卖出了第 10 张票 +pool-1-thread-3 卖出了第 10 张票 +pool-1-thread-1 卖出了第 8 张票 +pool-1-thread-2 卖出了第 9 张票 +pool-1-thread-1 卖出了第 6 张票 +pool-1-thread-3 卖出了第 7 张票 +pool-1-thread-1 卖出了第 4 张票 +pool-1-thread-2 卖出了第 5 张票 +pool-1-thread-1 卖出了第 2 张票 +pool-1-thread-3 卖出了第 3 张票 +pool-1-thread-2 卖出了第 1 张票 +``` + +很明显,出现了重复售票的情况。 + +【示例】使用自旋锁来保证线程安全 + +可以通过自旋锁这种非阻塞同步来保证线程安全,下面使用 `AtomicReference` 来实现一个自旋锁。 + +```java +public class AtomicReferenceDemo2 { + + private static int ticket = 10; + + public static void main(String[] args) { + threadSafeDemo(); + } + + private static void threadSafeDemo() { + SpinLock lock = new SpinLock(); + ExecutorService executorService = Executors.newFixedThreadPool(3); + for (int i = 0; i < 5; i++) { + executorService.execute(new MyThread(lock)); + } + executorService.shutdown(); + } + + static class SpinLock { + + private AtomicReference atomicReference = new AtomicReference<>(); + + public void lock() { + Thread current = Thread.currentThread(); + while (!atomicReference.compareAndSet(null, current)) {} + } + + public void unlock() { + Thread current = Thread.currentThread(); + atomicReference.compareAndSet(current, null); + } + + } + + static class MyThread implements Runnable { + + private SpinLock lock; + + public MyThread(SpinLock lock) { + this.lock = lock; + } + + @Override + public void run() { + while (ticket > 0) { + lock.lock(); + if (ticket > 0) { + System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); + ticket--; + } + lock.unlock(); + } + } + + } + +} +``` + +输出结果: + +``` +pool-1-thread-2 卖出了第 10 张票 +pool-1-thread-1 卖出了第 9 张票 +pool-1-thread-3 卖出了第 8 张票 +pool-1-thread-2 卖出了第 7 张票 +pool-1-thread-3 卖出了第 6 张票 +pool-1-thread-1 卖出了第 5 张票 +pool-1-thread-2 卖出了第 4 张票 +pool-1-thread-1 卖出了第 3 张票 +pool-1-thread-3 卖出了第 2 张票 +pool-1-thread-1 卖出了第 1 张票 +``` + +### CAS 的原理 + +**在 Java 中,主要利用 `Unsafe` 这个类实现 CAS**。 + +`Unsafe` 类位于 `sun.misc` 包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。 + +`Unsafe` 类提供了 `compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong`方法来实现的对 `Object`、`int`、`long ` 类型的 CAS 操作: + +```java +/** + * 以原子方式更新对象字段的值。 + */ +boolean compareAndSwapObject(Object o, long offset, Object expected, Object x); + +/** + * 以原子方式更新 int 类型的对象字段的值。 + */ +boolean compareAndSwapInt(Object o, long offset, int expected, int x); + +/** + * 以原子方式更新 long 类型的对象字段的值。 + */ +boolean compareAndSwapLong(Object o, long offset, long expected, long x); +``` + +`Unsafe` 类中的 CAS 方法是 `native` 方法。`native `关键字表明这些方法是用本地代码(通常是 C 或 C++)实现的,而不是用 Java 实现的。这些方法直接调用底层的、具有原子性的 CPU 指令来实现。 + +由于 CAS 操作可能会因为并发冲突而失败,因此通常会伴随着自旋,而所谓自旋,其实就是循环尝试。 + +`Unsafe#getAndAddInt` 源码: + +```java +// 原子地获取并增加整数值 +public final int getAndAddInt(Object o, long offset, int delta) { + int v; + do { + // 以 volatile 方式获取对象 o 在内存偏移量 offset 处的整数值 + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, v + delta)); + // 返回旧值 + return v; +} +``` + +### CAS 的问题 + +一般情况下,**CAS 比锁性能更高**。因为 CAS 是一种非阻塞算法,所以其避免了线程阻塞和唤醒的等待时间。但是,事物总会有利有弊,CAS 也存在三大问题: + +- **ABA 问题** +- **循环时间长开销大** +- **只能保证一个共享变量的原子性** + +#### ABA 问题 + +如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过,这就是 **ABA 问题**。 + +ABA 问题的解决思路是在变量前面追加上**版本号或者时间戳**。J.U.C 包提供了一个带有标记的**原子引用类 `AtomicStampedReference` 来解决这个问题**,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用**传统的互斥同步可能会比原子类更高效**。 + +#### 循环时间长开销大 + +**自旋 CAS (不断尝试,直到成功为止)如果长时间不成功,会给 CPU 带来非常大的执行开销**。 + +如果 JVM 能支持处理器提供的 `pause` 指令那么效率会有一定的提升,`pause` 指令有两个作用: + +- 它可以延迟流水线执行指令(de-pipeline), 使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 +- 它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。 + +比较花费 CPU 资源,即使没有任何用也会做一些无用功。 + +#### 只能保证一个共享变量的原子性 + +当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。 + +或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 `i = 2, j = a`,合并一下 `ij=2a`,然后用 CAS 来操作 `ij`。从 Java 1.5 开始 JDK 提供了 `AtomicReference` 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。 + +## 原子类 + +### 原子类简介 + +原子性是确保并发安全三大特性之一。为了兼顾原子性以及锁带来的性能问题,Java 引入了 CAS (主要体现在 `Unsafe` 类)来实现非阻塞同步(也叫乐观锁),CAS 底层基于 CPU 指令(硬件支持)支持,具有原子性。并基于 CAS ,提供了一套原子工具类。 + +原子类**比锁的粒度更细,更轻量级**,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上。 + +原子类相当于一种泛化的 `volatile` 变量,能够**支持原子的、有条件的读/改/写操**作。 + +原子类可以分为 5 个类别,这 5 个类别提供的方法基本上是相似的: + +- **基本数据类型** + - `AtomicBoolean` - 布尔类型原子类 + - `AtomicInteger` - 整型原子类 + - `AtomicLong` - 长整型原子类 +- **引用数据类型** + - `AtomicReference` - 引用类型原子类 + - `AtomicMarkableReference` - 带有标记位的引用类型原子类 + - `AtomicStampedReference` - 带有版本号的引用类型原子类 +- **数组数据类型** + - `AtomicIntegerArray` - 整形数组原子类 + - `AtomicLongArray` - 长整型数组原子类 + - `AtomicReferenceArray` - 引用类型数组原子类 +- **属性更新器类型** + - `AtomicIntegerFieldUpdater` - 整型字段的原子更新器 + - `AtomicLongFieldUpdater` - 长整型字段的原子更新器 + - `AtomicReferenceFieldUpdater` - 原子更新引用类型里的字段 +- 累加器 + - `DoubleAdder` - 浮点型原子累加器 + - `LongAdder` - 长整型原子累加器 + - `DoubleAccumulator` - 更复杂的浮点型原子累加器 + - `LongAccumulator` - 更复杂的长整型原子累加器 + +### 原子类之基本数据类型 + +**基本数据类型原子类针对 Java 基本类型提供原子操作**。 + +- `AtomicBoolean` - 布尔类型原子类 +- `AtomicInteger` - 整型原子类 +- `AtomicLong` - 长整型原子类 + +以上类都支持 CAS([compare-and-swap](https://en.wikipedia.org/wiki/Compare-and-swap))技术,此外,`AtomicInteger`、`AtomicLong` 还支持算术运算。 + +> :bulb: 提示: +> +> 虽然 Java 只提供了 `AtomicBoolean` 、`AtomicInteger`、`AtomicLong`,但是可以模拟其他基本类型的原子变量。要想模拟其他基本类型的原子变量,可以将 `short` 或 `byte` 等类型与 `int` 类型进行转换,以及使用 `Float.floatToIntBits` 、`Double.doubleToLongBits` 来转换浮点数。 +> +> 由于 `AtomicBoolean`、`AtomicInteger`、`AtomicLong` 实现方式、使用方式都相近,所以本文仅针对 `AtomicInteger` 进行介绍。 + +#### **`AtomicInteger` 用法** + +```java +public final int get() // 获取当前值 +public final int getAndSet(int newValue) // 获取当前值,并设置新值 +public final int getAndIncrement()// 获取当前值,并自增 +public final int getAndDecrement() // 获取当前值,并自减 +public final int getAndAdd(int delta) // 获取当前值,并加上预期值 +boolean compareAndSet(int expect, int update) // 如果输入值(update)等于预期值,将该值设置为输入值 +public final void lazySet(int newValue) // 最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 +``` + +`AtomicInteger` 使用示例: + +```java +public class AtomicIntegerDemo { + + public static void main(String[] args) throws InterruptedException { + ExecutorService executorService = Executors.newFixedThreadPool(5); + AtomicInteger count = new AtomicInteger(0); + for (int i = 0; i < 1000; i++) { + executorService.submit((Runnable) () -> { + System.out.println(Thread.currentThread().getName() + " count=" + count.get()); + count.incrementAndGet(); + }); + } + + executorService.shutdown(); + executorService.awaitTermination(30, TimeUnit.SECONDS); + System.out.println("Final Count is : " + count.get()); + } +} +``` + +#### **`AtomicInteger` 实现** + +阅读 `AtomicInteger` 源码,可以看到如下定义: + +```java +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; + +static { + try { + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } +} + +private volatile int value; +``` + +> 说明: +> +> - `value` - value 属性使用 `volatile` 修饰,使得对 value 的修改在并发环境下对所有线程可见。 +> - `valueOffset` - value 属性的偏移量,通过这个偏移量可以快速定位到 value 字段,这个是实现 AtomicInteger 的关键。 +> - `unsafe` - Unsafe 类型的属性,它为 `AtomicInteger` 提供了 CAS 操作。 + +### 原子类之引用数据类型 + +Java 数据类型分为 **基本数据类型** 和 **引用数据类型** 两大类(不了解 Java 数据类型划分可以参考: [Java 基本数据类型](https://dunwu.github.io/waterdrop/pages/3f3649ee/) )。 + +上一节中提到了针对基本数据类型的原子类,那么如果想针对引用类型做原子操作怎么办?Java 也提供了相关的原子类: + +- `AtomicReference` - 引用类型原子类 +- `AtomicMarkableReference` - 带有标记位的引用类型原子类 +- `AtomicStampedReference` - 带有版本号的引用类型原子类 + +> `AtomicStampedReference` 类在引用类型原子类中,彻底地解决了 ABA 问题,其它的 CAS 能力与另外两个类相近,所以最具代表性。因此,本节只针对 `AtomicStampedReference` 进行说明。 + +::: tabs#原子类之引用类型示例 + +@tab `AtomicReference` 使用示例 + +【示例】基于 `AtomicReference` 实现一个简单的自旋锁 + +```java +public class AtomicReferenceDemo2 { + + private static int ticket = 10; + + public static void main(String[] args) { + threadSafeDemo(); + } + + private static void threadSafeDemo() { + SpinLock lock = new SpinLock(); + ExecutorService executorService = Executors.newFixedThreadPool(3); + for (int i = 0; i < 5; i++) { + executorService.execute(new MyThread(lock)); + } + executorService.shutdown(); + } + + /** + * 基于 {@link AtomicReference} 实现的简单自旋锁 + */ + static class SpinLock { + + private AtomicReference atomicReference = new AtomicReference<>(); + + public void lock() { + Thread current = Thread.currentThread(); + while (!atomicReference.compareAndSet(null, current)) {} + } + + public void unlock() { + Thread current = Thread.currentThread(); + atomicReference.compareAndSet(current, null); + } + + } + + /** + * 利用自旋锁 {@link SpinLock} 并发处理数据 + */ + static class MyThread implements Runnable { + + private SpinLock lock; + + public MyThread(SpinLock lock) { + this.lock = lock; + } + + @Override + public void run() { + while (ticket > 0) { + lock.lock(); + if (ticket > 0) { + System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); + ticket--; + } + lock.unlock(); + } + } + + } + +} +``` + +@tab `AtomicMarkableReference` 使用示例 + +【示例】`AtomicMarkableReference` 使用示例(解决 ABA 问题) + +原子类的实现基于 CAS 机制,而 CAS 存在 ABA 问题(不了解 ABA 问题,可以参考:[Java 并发基础机制 - CAS 的问题](https://dunwu.github.io/waterdrop/pages/25767945/#cas-%E7%9A%84%E9%97%AE%E9%A2%98))。正是为了解决 ABA 问题,才有了 `AtomicMarkableReference` 和 `AtomicStampedReference`。 + +`AtomicMarkableReference` 使用一个布尔值作为标记,修改时在 true / false 之间切换。这种策略不能根本上解决 ABA 问题,但是可以降低 ABA 发生的几率。常用于缓存或者状态描述这样的场景。 + +```java +public class AtomicMarkableReferenceDemo { + + private final static String INIT_TEXT = "abc"; + + public static void main(String[] args) throws InterruptedException { + + final AtomicMarkableReference amr = new AtomicMarkableReference<>(INIT_TEXT, false); + + ExecutorService executorService = Executors.newFixedThreadPool(3); + for (int i = 0; i < 10; i++) { + executorService.submit(new Runnable() { + @Override + public void run() { + try { + Thread.sleep(Math.abs((int) (Math.random() * 100))); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + String name = Thread.currentThread().getName(); + if (amr.compareAndSet(INIT_TEXT, name, amr.isMarked(), !amr.isMarked())) { + System.out.println(Thread.currentThread().getName() + " 修改了对象!"); + System.out.println("新的对象为:" + amr.getReference()); + } + } + }); + } + + executorService.shutdown(); + executorService.awaitTermination(3, TimeUnit.SECONDS); + } + +} +``` + +@tab `AtomicStampedReference` 使用示例 + +【示例】`AtomicStampedReference` 使用示例 + +**`AtomicStampedReference` 使用一个整型值做为版本号,每次更新前先比较版本号,如果一致,才进行修改**。通过这种策略,可以根本上解决 ABA 问题。 + +```java +public class AtomicStampedReferenceDemo { + + private final static String INIT_REF = "pool-1-thread-3"; + + private final static AtomicStampedReference asr = new AtomicStampedReference<>(INIT_REF, 0); + + public static void main(String[] args) throws InterruptedException { + + System.out.println("初始对象为:" + asr.getReference()); + + ExecutorService executorService = Executors.newFixedThreadPool(3); + for (int i = 0; i < 3; i++) { + executorService.execute(new MyThread()); + } + + executorService.shutdown(); + executorService.awaitTermination(3, TimeUnit.SECONDS); + } + + static class MyThread implements Runnable { + + @Override + public void run() { + try { + Thread.sleep(Math.abs((int) (Math.random() * 100))); + } catch (InterruptedException e) { + e.printStackTrace(); + } + + final int stamp = asr.getStamp(); + if (asr.compareAndSet(INIT_REF, Thread.currentThread().getName(), stamp, stamp + 1)) { + System.out.println(Thread.currentThread().getName() + " 修改了对象!"); + System.out.println("新的对象为:" + asr.getReference()); + } + } + + } + +} +``` + +::: + +### 原子类之数组数据类型 + +Java 提供了以下针对数组的原子类: + +- `AtomicIntegerArray` - 整形数组原子类 +- `AtomicLongArray` - 长整型数组原子类 +- `AtomicReferenceArray` - 引用类型数组原子类 + +已经有了针对基本类型和引用类型的原子类,为什么还要提供针对数组的原子类呢? + +**数组类型的原子类为数组元素提供了 `volatile` 类型的访问语义**,这是普通数组所不具备的特性——**`volatile` 类型的数组仅在数组引用上具有 `volatile` 语义**。 + +【示例】`AtomicIntegerArray` 使用示例(`AtomicLongArray` 、`AtomicReferenceArray` 使用方式也类似) + +```java +public class AtomicIntegerArrayDemo { + + private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10); + + public static void main(final String[] arguments) throws InterruptedException { + + System.out.println("Init Values: "); + for (int i = 0; i < atomicIntegerArray.length(); i++) { + atomicIntegerArray.set(i, i); + System.out.print(atomicIntegerArray.get(i) + " "); + } + System.out.println(); + + Thread t1 = new Thread(new Increment()); + Thread t2 = new Thread(new Compare()); + t1.start(); + t2.start(); + + t1.join(); + t2.join(); + + System.out.println("Final Values: "); + for (int i = 0; i < atomicIntegerArray.length(); i++) { + System.out.print(atomicIntegerArray.get(i) + " "); + } + System.out.println(); + } + + static class Increment implements Runnable { + + @Override + public void run() { + + for (int i = 0; i < atomicIntegerArray.length(); i++) { + int value = atomicIntegerArray.incrementAndGet(i); + System.out.println(Thread.currentThread().getName() + ", index = " + i + ", value = " + value); + } + } + + } + + static class Compare implements Runnable { + + @Override + public void run() { + for (int i = 0; i < atomicIntegerArray.length(); i++) { + boolean swapped = atomicIntegerArray.compareAndSet(i, 2, 3); + if (swapped) { + System.out.println(Thread.currentThread().getName() + " swapped, index = " + i + ", value = 3"); + } + } + } + + } + +} +``` + +### 原子类之属性更新器 + +**属性更新器支持基于反射机制的更新字段值的原子操作**。 + +- `AtomicIntegerFieldUpdater` - 整型字段的原子更新器。 +- `AtomicLongFieldUpdater` - 长整型字段的原子更新器。 +- `AtomicReferenceFieldUpdater` - 原子更新引用类型里的字段。 + +这些类的使用有一定限制: + +- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 `newUpdater()` 创建一个更新器,并且需要设置想要更新的类和属性。 +- 字段必须是 `volatile` 类型的; +- 不能作用于静态变量(`static`); +- 不能作用于常量(`final`); + +【示例】`AtomicReferenceFieldUpdater` 使用示例 + +```java +public class AtomicReferenceFieldUpdaterDemo { + + static User user = new User("begin"); + + static AtomicReferenceFieldUpdater updater = + AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name"); + + public static void main(String[] args) { + ExecutorService executorService = Executors.newFixedThreadPool(3); + for (int i = 0; i < 5; i++) { + executorService.execute(new MyThread()); + } + executorService.shutdown(); + } + + static class MyThread implements Runnable { + + @Override + public void run() { + if (updater.compareAndSet(user, "begin", "end")) { + try { + TimeUnit.SECONDS.sleep(1); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + " 已修改 name = " + user.getName()); + } else { + System.out.println(Thread.currentThread().getName() + " 已被其他线程修改"); + } + } + + } + + static class User { + + volatile String name; + + public User(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public User setName(String name) { + this.name = name; + return this; + } + + } + +} +``` + +### 原子类之累加器 + +`DoubleAccumulator`、`DoubleAdder`、`LongAccumulator` 和 `LongAdder`,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 `compareAndSet()` 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好,代价就是会消耗更多的内存空间。 + +`LongAdder` 内部由一个 `base` 变量和一个 `cell[]` 数组组成。 + +- 当只有一个写线程,没有竞争的情况下,`LongAdder` 会直接使用 `base` 变量作为原子操作变量,通过 CAS 操作修改变量; +- 当有多个写线程竞争的情况下,除了占用 `base` 变量的一个写线程之外,其它各个线程会将修改的变量写入到自己的槽 `cell[]` 数组中。 + +我们可以发现,`LongAdder` 在操作后的返回值只是一个近似准确的数值,但是 `LongAdder` 最终返回的是一个准确的数值, 所以在一些对实时性要求比较高的场景下,`LongAdder` 并不能取代 `AtomicInteger` 或 `AtomicLong`。 + +## ThreadLocal + +在多线程环境下,共享变量存在并发安全问题。换个思路,如果变量非共享,而是各个线程独享,就不会有并发安全问题。这种思想有个术语叫**线程封闭**,其本质上就是避免共享。没有共享,自然也就没有并发安全问题。在 Java 中,`ThreadLocal` 正是根据这个思路而设计的。 + +**`ThreadLocal` 为每个线程都创建了一个本地副本**,这个副本只能被当前线程访问,其他线程无法访问,那么自然是线程安全的。 + +### ThreadLocal 的应用 + +`ThreadLocal` 的方法: + +```java +public class ThreadLocal { + public T get() {} + public void set(T value) {} + public void remove() {} + public static ThreadLocal withInitial(Supplier supplier) {} +} +``` + +> 说明: +> +> - `get` - 用于获取 `ThreadLocal` 在当前线程中保存的变量副本。 +> - `set` - 用于设置当前线程中变量的副本。 +> - `remove` - 用于删除当前线程中变量的副本。如果此线程局部变量随后被当前线程读取,则其值将通过调用其 `initialValue` 方法重新初始化,除非其值由中间线程中的当前线程设置。 这可能会导致当前线程中多次调用 `initialValue` 方法。 +> - `initialValue` - 为 ThreadLocal 设置默认的 `get` 初始值,需要重写 `initialValue` 方法 。 + +`ThreadLocal` 常用于防止对可变的单例(Singleton)变量或全局变量进行共享。典型应用场景有:管理数据库连接、Session 管理等。 + +::: tabs#ThreadLocal 应用示例 + +@tab 数据库连接 + +【示例】数据库连接 + +```java +private static ThreadLocal connectionHolder = new ThreadLocal() { + @Override + public Connection initialValue() { + return DriverManager.getConnection(DB_URL); + } +}; + +public static Connection getConnection() { + return connectionHolder.get(); +} +``` + +@tab Session 管理 + +【示例】Session 管理 + +```java +private static final ThreadLocal sessionHolder = new ThreadLocal<>(); + +public static Session getSession() { + Session session = (Session) sessionHolder.get(); + try { + if (session == null) { + session = createSession(); + sessionHolder.set(session); + } + } catch (Exception e) { + e.printStackTrace(); + } + return session; +} +``` + +@tab 线程安全的 `SimpleDateFormat` + +【示例】线程安全的 `SimpleDateFormat` + +SimpleDateFormat 不是线程安全的,如果要保证并发安全,可以使用 ThreadLocal 来解决。 + +```java +public class SafeDateFormat { + + //定义 ThreadLocal 变量 + static final ThreadLocal + tl = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); + + static DateFormat get() { + return tl.get(); + } + + public static void main(String[] args) { + //不同线程执行下面代码 + //返回的 df 是不同的 + DateFormat df = SafeDateFormat.get(); + } + +} +``` + +@tab 完整使用 `ThreadLocal` 示例 + +【示例】完整使用 `ThreadLocal` 示例 + +```java +public class ThreadLocalDemo { + + private static ThreadLocal threadLocal = new ThreadLocal() { + @Override + protected Integer initialValue() { + return 0; + } + }; + + public static void main(String[] args) { + ExecutorService executorService = Executors.newFixedThreadPool(10); + for (int i = 0; i < 10; i++) { + executorService.execute(new MyThread()); + } + executorService.shutdown(); + } + + static class MyThread implements Runnable { + + @Override + public void run() { + int count = threadLocal.get(); + for (int i = 0; i < 10; i++) { + try { + count++; + Thread.sleep(100); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + threadLocal.set(count); + threadLocal.remove(); + System.out.println(Thread.currentThread().getName() + " : " + count); + } + + } + +} +``` + +::: + +### ThreadLocal 的原理 + +#### 存储结构 + +**`Thread` 类中维护着 2 个 `ThreadLocal.ThreadLocalMap` 类型的成员** `threadLocals` 和 inheritableThreadLocals 。这 2 个成员就是用来存储当前线程独占的变量副本。 + +`ThreadLocalMap` 是 `ThreadLocal` 的内部类,它维护着一个 `Entry` 数组,**`Entry` 继承了 `WeakReference`** ,所以是弱引用。 `Entry` 用于保存键值对,其中: + +- `key` 是 `ThreadLocal` 对象 +- `value` 是传递进来的对象(变量副本) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409110720703.png) + +`ThreadLocal` 关键源码如下: + +```java +public class Thread implements Runnable { + // ... + ThreadLocal.ThreadLocalMap threadLocals = null; + // ... + ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; +} + +static class ThreadLocalMap { + // ... + static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } + } + // ... +} +``` + +#### 如何解决 Hash 冲突 + +`ThreadLocalMap` 虽然是类似 `Map` 结构的数据结构,但它并没有实现 `Map` 接口。它不支持 `Map` 接口中的 `next` 方法,这意味着 `ThreadLocalMap` 中解决 Hash 冲突的方式并非 **拉链表** 方式。 + +实际上,**`ThreadLocalMap` 采用线性探测的方式来解决 Hash 冲突**。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。 + +#### 内存泄漏问题 + +`ThreadLocal` 仅仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 `Thread` 里面,这样的设计容易理解。 + +当然还有一个更加深层次的原因,那就是**不容易产生内存泄露**。如果 `ThreadLocal` 和实际实现反其道而行之:将 `Thread` 的引用维护在一个 `Map` 中,就会出现这种情况——只要 `ThreadLocal` 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。而 `ThreadLocal` 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 `Thread` 持有 `ThreadLocalMap`,而且 `ThreadLocalMap` 里对 `ThreadLocal` 的引用还是弱引用(`WeakReference`),所以只要 `Thread` 对象可以被回收,那么 `ThreadLocalMap` 就能被回收。Java 的这种实现方案虽然看上去复杂一些,但是更加安全。 + +`ThreadLocalMap` 的 `Entry` 继承了 `WeakReference`,所以它的 **key (`ThreadLocal` 对象)是弱引用,而 value (变量副本)是强引用**。如果 `ThreadLocal` 对象没有外部强引用来引用它,那么 `ThreadLocal` 对象会在下次 GC 时被回收。此时,`Entry` 中的 key 已经被回收,但是 value 由于是强引用不会被垃圾收集器回收。如果创建 `ThreadLocal` 的线程一直持续运行,那么 value 就会一直得不到回收,从而导致**内存泄露**。 + +那么如何避免内存泄漏呢?方法就是:**使用 `ThreadLocal` 的 `set` 方法后,在 `try {} finally {} ` 中显示的调用 `remove` 方法** 。 + +```java +ExecutorService es; +ThreadLocal tl; +es.execute(() -> { + //ThreadLocal 增加变量 + tl.set(obj); + try { + // 省略业务逻辑代码 + } finally { + //手动清理 ThreadLocal + tl.remove(); + } +}); +``` + +### ThreadLocal 的误区 + +> 示例摘自:[极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) + +ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景。 + +前文提到,ThreadLocal 是线程隔离的,那么是不是使用 ThreadLocal 就一定高枕无忧呢? + +#### ThreadLocal 错误案例 + +使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,来暂且代表需要在线程中保存的用户信息,这个值初始是 null。 + +```java + private ThreadLocal currentUser = ThreadLocal.withInitial(() -> null); + + @GetMapping("wrong") + public Map wrong(@RequestParam("id") Integer userId) { + //设置用户信息之前先查询一次 ThreadLocal 中的用户信息 + String before = Thread.currentThread().getName() + ":" + currentUser.get(); + //设置用户信息到 ThreadLocal + currentUser.set(userId); + //设置用户信息之后再查询一次 ThreadLocal 中的用户信息 + String after = Thread.currentThread().getName() + ":" + currentUser.get(); + //汇总输出两次查询结果 + Map result = new HashMap<>(); + result.put("before", before); + result.put("after", after); + return result; + } +``` + +【预期】从代码逻辑来看,我们预期第一次获取的值始终应该是 null。 + +【实际】 + +为了方便复现,将 Tomcat 工作线程设为 1: + +``` +server.tomcat.max-threads=1 +``` + +当访问 id = 1 时,符合预期 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200731111854.png) + +当访问 id = 2 时,before 的应答不是 null,而是 1,不符合预期。 + +【分析】实际情况和预期存在偏差。Spring Boot 程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的。**线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从** +**ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息**。 + +**并不能认为没有显式开启多线程就不会有线程安全问题**。使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。 + +#### ThreadLocal 错误案例修正 + +```java + @GetMapping("right") + public Map right(@RequestParam("id") Integer userId) { + String before = Thread.currentThread().getName() + ":" + currentUser.get(); + currentUser.set(userId); + try { + String after = Thread.currentThread().getName() + ":" + currentUser.get(); + Map result = new HashMap<>(); + result.put("before", before); + result.put("after", after); + return result; + } finally { + //在 finally 代码块中删除 ThreadLocal 中的数据,确保数据不串 + currentUser.remove(); + } + } +``` + +### InheritableThreadLocal + +通过 `ThreadLocal` 创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过 `ThreadLocal` 创建了线程变量 V,而后该线程创建了子线程,你在子线程中是无法通过 `ThreadLocal` 来访问父线程的线程变量 V 的。 + +如果你需要子线程继承父线程的线程变量,那该怎么办呢?其实很简单,Java 提供了 `InheritableThreadLocal` 来支持这种特性,`InheritableThreadLocal` 是 `ThreadLocal` 子类,所以用法和 `ThreadLocal` 相同。与 `ThreadLocal` 不同的是,`InheritableThreadLocal` 允许一个线程以及该线程创建的所有子线程都可以访问它保存的数据。 + +不过,完全不建议你在线程池中使用 `InheritableThreadLocal`,不仅仅是因为它具有 `ThreadLocal` 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 `InheritableThreadLocal`,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命。 + +> 原理参考:[Java 多线程:InheritableThreadLocal 实现原理](https://blog.csdn.net/ni357103403/article/details/51970748) + +## Immutability 模式 + +解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:**不变性(Immutability)模式**。所谓**不变性,是指:一旦创建,状态不再变化**。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。 + +### 快速实现具备不可变性的类 + +**将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了**。更严格的做法是**这个类本身也是 final 的**,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性,所以推荐你在实际工作中,使用这种更严格的做法。 + +在 Java 中,经常用到的 `String` 和 `Long`、`Integer`、`Double` 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。如果你仔细翻看这些类的声明、属性和方法,你会发现它们都严格遵守不可变类的三点要求:**类和属性都是 final 的,所有方法均是只读的**。 + +`String` 这个类虽然有替换操作,但实际仍是只读的。阅读 `String` 源码可以发现:`String` 这个类以及它的属性 `value[]` 都是 `final` 的;而 `replace()` 方法的实现,就的确没有修改 `value[]`,而是将替换后的字符串作为返回值返回了。 + +```java +public final class String { + private final char value[]; + // 字符替换 + String replace(char oldChar, + char newChar) { + //无需替换,直接返回 this + if (oldChar == newChar){ + return this; + } + + int len = value.length; + int i = -1; + /* avoid getfield opcode */ + char[] val = value; + //定位到需要替换的字符位置 + while (++i < len) { + if (val[i] == oldChar) { + break; + } + } + //未找到 oldChar,无需替换 + if (i >= len) { + return this; + } + //创建一个 buf[],这是关键 + //用来保存替换后的字符串 + char buf[] = new char[len]; + for (int j = 0; j < i; j++) { + buf[j] = val[j]; + } + while (i < len) { + char c = val[i]; + buf[i] = (c == oldChar) ? + newChar : c; + i++; + } + //创建一个新的字符串返回 + //原字符串不会发生任何变化 + return new String(buf, true); + } +} +``` + +通过分析 `String` 的实现,你可能已经发现了,如果具备不可变性的类,需要提供类似修改的功能,具体该怎么操作呢?做法很简单,那就是**创建一个新的不可变对象**,这是与可变对象的一个重要区别,可变对象往往是修改自己的属性。 + +### 使用 Immutability 模式的注意事项 + +在使用 Immutability 模式的时候,需要注意以下两点: + +1. 对象的所有属性都是 final 的,并不能保证不可变性; +2. 不可变对象也需要正确发布。 + +在 Java 语言中,final 修饰的属性一旦被赋值,就不可以再修改,但是如果属性的类型是普通对象,那么这个普通对象的属性是可以被修改的。例如下面的代码中,Bar 的属性 foo 虽然是 final 的,依然可以通过 setAge() 方法来设置 foo 的属性 age。所以,**在使用 Immutability 模式的时候一定要确认保持不变性的边界在哪里,是否要求属性对象也具备不可变性**。 + +```java +class Foo{ + int age=0; + int name="abc"; +} +final class Bar { + final Foo foo; + void setAge(int a){ + foo.age=a; + } +} +``` + +不可变对象虽然是线程安全的,但是并不意味着引用这些不可变对象的对象就是线程安全的。例如在下面的代码中,Foo 具备不可变性,线程安全,但是类 Bar 并不是线程安全的,类 Bar 中持有对 Foo 的引用 foo,对 foo 这个引用的修改在多线程中并不能保证可见性和原子性。 + +```java +//Foo 线程安全 +final class Foo{ + final int age=0; + final int name="abc"; +} +//Bar 线程不安全 +class Bar { + Foo foo; + void setFoo(Foo f){ + this.foo=f; + } +} +``` + +如果你的程序仅仅需要 foo 保持可见性,无需保证原子性,那么可以将 foo 声明为 volatile 变量,这样就能保证可见性。如果你的程序需要保证原子性,那么可以通过原子类来实现。下面的示例代码是合理库存的原子化实现,你应该很熟悉了,其中就是用原子类解决了不可变对象引用的原子性问题。 + +```java +public class SafeWM { + + class WMRange { + final int upper; + final int lower; + WMRange(int upper, int lower) { + //省略构造函数实现 + } + } + + final AtomicReference rf = new AtomicReference<>(new WMRange(0, 0)); + + // 设置库存上限 + void setUpper(int v) { + while (true) { + WMRange or = rf.get(); + // 检查参数合法性 + if (v < or.lower) { + throw new IllegalArgumentException(); + } + WMRange nr = new WMRange(v, or.lower); + if (rf.compareAndSet(or, nr)) { + return; + } + } + } +} +``` + +## Copy-on-Write 模式 + +所谓 Copy-on-Write,经常被缩写为 CoW,顾名思义就是**写时复制**。 + +Java 支持 `CopyOnWriteArrayList` 和 `CopyOnWriteArraySet` 两种并发容器,其设计思想就是 CoW;通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。 + +CoW 是一项非常通用的技术方案,在很多领域都有着广泛的应用。不过,它也有缺点的,那就是消耗内存,每次修改都需要复制一个新的副本出来。 + +## 参考资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) +- [Non-blocking Algorithms](http://tutorials.jenkov.com/java-concurrency/non-blocking-algorithms.html) +- [Java CAS 完全解读](https://www.jianshu.com/p/473e14d5ab2d) +- [Java 中 CAS 详解](https://blog.csdn.net/ls5718/article/details/52563959) +- [JUC 中的原子类](http://www.itsoku.com/article/182) +- [ThreadLocal 终极篇](https://juejin.im/post/5a64a581f265da3e3b7aa02d) diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\347\272\277\347\250\213.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\347\272\277\347\250\213.md" new file mode 100644 index 0000000000..93eb5c569d --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\347\272\277\347\250\213.md" @@ -0,0 +1,943 @@ +--- +title: Java 并发之线程 +date: 2019-12-24 23:52:25 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 + - 线程 + - Thread + - Runnable + - Callable + - Future + - FutureTask + - 线程生命周期 +permalink: /pages/162ef13a/ +--- + +# Java 并发之线程 + +## 线程简介 + +- **进程(Process)** - 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。**进程可视为一个正在运行的程序**。 +- **线程(Thread)** - **线程是操作系统进行调度的基本单位**。 +- **管程(Monitor)** - **管程是指管理共享变量以及对共享变量的操作过程,让他们支持并发**。 + - Java 通过 synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法来实现管程技术。 + - **管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程**。 +- **协程(Coroutine)** - **协程可以理解为一种轻量级的线程**。 + - 从操作系统的角度来看,线程是在内核态中调度的,而协程是在用户态调度的,所以相对于线程来说,协程切换的成本更低。 + - 协程虽然也有自己的栈,但是相比线程栈要小得多,典型的线程栈大小差不多有 1M,而协程栈的大小往往只有几 K 或者几十 K。所以,无论是从时间维度还是空间维度来看,协程都比线程轻量得多。 + - Go、Python、Lua、Kotlin 等语言都支持协程;Java OpenSDK 中的 Loom 项目目标就是支持协程。 + +进程和线程的差异: + +- 一个程序至少有一个进程,一个进程至少有一个线程。 +- 线程比进程划分更细,所以执行开销更小,并发性更高 +- 进程是一个实体,拥有独立的资源;而同一个进程中的多个线程共享进程的资源。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/processes-vs-threads.jpg) + +JVM 在单个进程中运行,JVM 中的线程共享属于该进程的堆。这就是为什么几个线程可以访问同一个对象。线程共享堆并拥有自己的堆栈空间。这是一个线程如何调用一个方法以及它的局部变量是如何保持线程安全的。但是堆不是线程安全的并且为了线程安全必须进行同步。 + +## 线程创建 + +一般来说,创建线程有很多种方式,例如: + +- 实现 `Runnable` 接口 +- 实现 `Callable` 接口 +- 继承 `Thread` 类 +- 通过线程池创建线程 +- 使用 `CompletableFuture` 创建线程 +- ... + +下面是几种创建线程的示例: + +::: tabs#创建线程 + +@tab Thread + +### Thread + +【示例】继承 `Thread` 类创建线程 + +1. 定义 `Thread` 类的子类,并覆写该类的 `run` 方法。`run` 方法的方法体就代表了线程要完成的任务,因此把 `run` 方法称为执行体。 +2. 创建 `Thread` 子类的实例,即创建了线程对象。 +3. 调用线程对象的 `start` 方法来启动该线程。 + +```java +public class ThreadDemo { + + public static void main(String[] args) { + // 实例化对象 + MyThread tA = new MyThread("Thread 线程-A"); + MyThread tB = new MyThread("Thread 线程-B"); + // 调用线程主体 + tA.start(); + tB.start(); + } + + static class MyThread extends Thread { + + private int ticket = 5; + + MyThread(String name) { + super(name); + } + + @Override + public void run() { + while (ticket > 0) { + System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); + ticket--; + } + } + + } + +} +``` + +@tab Runnable + +### Runnable + +**实现 `Runnable` 接口优于继承 `Thread` 类**,因为: + +- Java 不支持多重继承,所有的类都只允许继承一个父类,但可以实现多个接口。如果继承了 `Thread` 类就无法继承其它类,这不利于扩展。 +- 类可能只要求可执行就行,继承整个 `Thread` 类开销过大。 + +【示例】实现 `Runnable` 接口创建线程 + +1. 定义 `Runnable` 接口的实现类,并覆写该接口的 `run` 方法。该 `run` 方法的方法体同样是该线程的线程执行体。 +2. 创建 `Runnable` 实现类的实例,并以此实例作为 `Thread` 的 target 来创建 `Thread` 对象,该 `Thread` 对象才是真正的线程对象。 +3. 调用线程对象的 `start` 方法来启动该线程。 + +```java +public class RunnableDemo { + + public static void main(String[] args) { + // 实例化对象 + Thread tA = new Thread(new MyThread(), "Runnable 线程-A"); + Thread tB = new Thread(new MyThread(), "Runnable 线程-B"); + // 调用线程主体 + tA.start(); + tB.start(); + } + + static class MyThread implements Runnable { + + private int ticket = 5; + + @Override + public void run() { + while (ticket > 0) { + System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); + ticket--; + } + } + + } + +} +``` + +@tab Callable + +### Callable、Future、FutureTask + +**继承 Thread 类和实现 Runnable 接口这两种创建线程的方式都没有返回值**。所以,线程执行完后,无法得到执行结果。但如果期望得到执行结果该怎么做? + +为了解决这个问题,Java 1.5 后,提供了 `Callable` 接口和 `Future` 接口,通过它们,可以在线程执行结束后,返回执行结果。 + +#### Callable + +`Callable` 接口只声明了一个 `call` 方法: + +```java +public interface Callable { + /** + * Computes a result, or throws an exception if unable to do so. + * + * @return computed result + * @throws Exception if unable to compute a result + */ + V call() throws Exception; +} +``` + +那么怎么使用 `Callable` 呢?一般情况下是配合 `ExecutorService` 来使用的,在 `ExecutorService` 接口中声明了若干个 `submit` 方法的重载版本: + +```java + Future submit(Callable task); + Future submit(Runnable task, T result); +Future submit(Runnable task); +``` + +第一个 `submit` 方法里面的参数类型就是 `Callable`。 + +#### Future + +`Future` 就是对于具体的 `Callable` 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 `get` 方法获取执行结果,该方法会阻塞直到任务返回结果。 + +```java +public interface Future { + boolean cancel(boolean mayInterruptIfRunning); + boolean isCancelled(); + boolean isDone(); + V get() throws InterruptedException, ExecutionException; + V get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException; +} +``` + +#### FutureTask + +`FutureTask` 类实现了 `RunnableFuture` 接口,`RunnableFuture` 继承了 `Runnable` 接口和 `Future` 接口。 + +所以,`FutureTask` 既可以作为 `Runnable` 被线程执行,又可以作为 `Future` 得到 `Callable` 的返回值。 + +```java +public class FutureTask implements RunnableFuture { + // ... + public FutureTask(Callable callable) {} + public FutureTask(Runnable runnable, V result) {} +} + +public interface RunnableFuture extends Runnable, Future { + void run(); +} +``` + +事实上,`FutureTask` 是 `Future` 接口的一个唯一实现类。 + +#### Callable + Future + FutureTask 示例 + +通过实现 `Callable` 接口创建线程的步骤: + +1. 创建 `Callable` 接口的实现类,并实现 `call` 方法。该 `call` 方法将作为线程执行体,并且有返回值。 +2. 创建 `Callable` 实现类的实例,使用 `FutureTask` 类来包装 `Callable` 对象,该 `FutureTask` 对象封装了该 `Callable` 对象的 `call` 方法的返回值。 +3. 使用 `FutureTask` 对象作为 `Thread` 对象的 target 创建并启动新线程。 +4. 调用 `FutureTask` 对象的 `get` 方法来获得线程执行结束后的返回值。 + +```java +public class CallableDemo { + + public static void main(String[] args) { + Callable callable = new MyThread(); + FutureTask future = new FutureTask<>(callable); + new Thread(future, "Callable 线程").start(); + try { + System.out.println("任务耗时:" + (future.get() / 1000000) + "毫秒"); + } catch (InterruptedException | ExecutionException e) { + e.printStackTrace(); + } + } + + static class MyThread implements Callable { + + private int ticket = 10000; + + @Override + public Long call() { + long begin = System.nanoTime(); + while (ticket > 0) { + System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); + ticket--; + } + + long end = System.nanoTime(); + return (end - begin); + } + + } + +} +``` + +::: + +虽然,看似有多种多样的创建线程方式。但是,**从本质上来说,Java 就只有一种方式可以创建线程,那就是通过 `new Thread().start() ` 创建**。不管是哪种方式,最终还是依赖于 `new Thread().start()`。 + +> 👉 扩展阅读:[大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!](https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g)。 + +## 线程终止 + +### 如何正确停止线程 + +通常情况下,我们不会手动停止一个线程,而是允许线程运行到结束,然后让它自然停止。但是依然会有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等。 + +**对于 Java 而言,最正确的停止线程的方式是:通过 `Thread.interrupt` 和 `Thread.isInterrupted` 配合来控制线程终止**。但 `Thread.interrupt` 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。 + +事实上,Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。 + +一旦调用某个线程的 `Thread.interrupt` 之后,这个线程的中断标记位就会被设置成 `true`。每个线程都有这样的标记位,当线程执行时,应该定期检查这个标记位,如果标记位被设置成 `true`,就说明有程序想终止该线程。回到源码,可以看到在 `while` 循环体判断语句中,首先通过 `Thread.currentThread().isInterrupt()` 判断线程是否被中断,随后检查是否还有工作要做。&& 逻辑表示只有当两个判断条件同时满足的情况下,才会去执行下面的工作。 + +需要留意一个特殊场景:**`Thread.sleep` 后,线程依然可以感知 `Thread.interrupt`**。 + +【示例】正确停止线程的方式——`Thread.interrupt` + +```java +public class ThreadStopDemo { + + public static void main(String[] args) throws Exception { + Thread thread = new Thread(new MyTask(), "MyTask"); + thread.start(); + TimeUnit.MILLISECONDS.sleep(10); + thread.interrupt(); + } + + private static class MyTask implements Runnable { + + private long count = 0L; + + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " 线程启动"); + // 通过 Thread.interrupted 和 interrupt 配合来控制线程终止 + while (!Thread.currentThread().isInterrupted() && count < 10000) { + System.out.println("count = " + count++); + } + System.out.println(Thread.currentThread().getName() + " 线程终止"); + } + + } + +} +// 输出(count 未到 10000,线程就主动结束): +// MyTask 线程启动 +// count = 0 +// count = 1 +// ... +// count = 840 +// count = 841 +// count = 842 +// MyTask 线程终止 +``` + +### 可以使用 `Thread.stop`,`Thread.suspend` 和 `Thread.resume` 停止线程吗? + +`Thread.stop`,`Thread.suspend` 和 `Thread.resume` 方法已经被 Java 标记为 `@Deprecated`。为什么废弃呢? + +- **`Thread.stop` 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题**。 +- 而对于`Thread.suspend` 和 `Thread.resume` 而言,它们的问题在于:**如果线程调用 `Thread.suspend`,它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题**。因为这把锁在线程被 `Thread.resume` 之前,是不会被释放的。假设线程 A 调用了 `Thread.suspend` 方法让线程 B 挂起,线程 B 进入休眠,而线程 B 又刚好持有一把锁,此时假设线程 A 想访问线程 B 持有的锁,但由于线程 B 并没有释放锁就进入休眠了,所以对于线程 A 而言,此时拿不到锁,也会陷入阻塞,那么线程 A 和线程 B 就都无法继续向下执行。 + +【示例】`Thread.stop` 终止线程,导致线程任务戛然而止 + +```java +public class ThreadStopErrorDemo { + + public static void main(String[] args) { + MyTask thread = new MyTask(); + thread.start(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 终止线程 + thread.stop(); + // 确保线程终止后,才执行下面的代码 + while (thread.isAlive()) { } + // 输出两个计数器的最终状态 + thread.print(); + } + + /** + * 持有两个计数器,run 方法中每次执行都会使计数器自增 + */ + private static class MyTask extends Thread { + + private int i = 0; + + private int j = 0; + + @Override + public void run() { + synchronized (this) { + ++i; + try { + // 模拟耗时操作 + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + ++j; + } + } + + public void print() { + System.out.println("i=" + i + " j=" + j); + } + + } + +} +``` + +### 使用 `volatile` 标记方式停止线程正确吗? + +使用 `volatile` 标记方式停止线程并不总是正确的。虽然 `volatile` 变量可以确保可见性,即当一个线程修改了 `volatile` 变量的值,其他线程能够立即看到最新的值,但它并不能保证原子性,也就是说并不能保证多个线程对 `volatile` 变量的操作是互斥的。 + +当我们使用 `volatile` 变量来控制线程的停止,通常是通过设置一个 `volatile` 标志位来告诉线程停止执行。例如: + +```java +public class MyTask extends Thread { + private volatile boolean canceled = false; + + public void run() { + while (!canceled) { + // 执行任务 + } + } + + public void stopTask() { + canceled = true; + } +} +``` + +在上述例子中,`canceled` 是一个 `volatile` 变量,用来控制线程的停止。虽然这种方式在某些情况下可以工作,但它并**不是一个可靠的停止线程的方式,因为在多线程环境中,其他线程修改 `canceled` 的值时,可能会出现竞态条件,导致线程无法正确停止**。 + +## 线程基本方法 + +线程(`Thread`)基本方法清单: + +| 方法 | 描述 | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `run` | 线程的执行实体。 | +| `start` | 线程的启动方法。 | +| `currentThread` | 返回对当前正在执行的线程对象的引用。 | +| `setName` | 设置线程名称。 | +| `getName` | 获取线程名称。 | +| `setPriority` | 设置线程优先级。Java 中的线程优先级的范围是 [1,10],一般来说,高优先级的线程在运行时会具有优先权。可以通过 `thread.setPriority(Thread.MAX_PRIORITY)` 的方式设置,默认优先级为 5。 | +| `getPriority` | 获取线程优先级。 | +| `setDaemon` | 设置线程为守护线程。 | +| `isDaemon` | 判断线程是否为守护线程。 | +| `isAlive` | 判断线程是否启动。 | +| `interrupt` | 中断另一个线程的运行状态。 | +| `interrupted` | 测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。换句话说,如果要连续调用此方法两次,则第二次调用将返回 false(除非当前线程在第一次调用清除其中断状态之后且在第二次调用检查其状态之前再次中断)。 | +| `join` | 可以使一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。 | +| `Thread.sleep` | 静态方法。将当前正在执行的线程休眠。 | +| `Thread.yield` | 静态方法。将当前正在执行的线程暂停,让其他线程执行。 | + +### 线程休眠 + +**使用 `Thread.sleep` 方法可以使得当前正在执行的线程进入休眠状态。** + +使用 `Thread.sleep` 需要向其传入一个整数值,这个值表示线程将要休眠的毫秒数。 + +`Thread.sleep` 方法可能会抛出 `InterruptedException`,因为异常不能跨线程传播回 `main` 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 + +```java +public class ThreadSleepDemo { + + public static void main(String[] args) { + new Thread(new MyThread("线程 A", 500)).start(); + new Thread(new MyThread("线程 B", 1000)).start(); + new Thread(new MyThread("线程 C", 1500)).start(); + } + + static class MyThread implements Runnable { + + /** 线程名称 */ + private String name; + + /** 休眠时间 */ + private int time; + + private MyThread(String name, int time) { + this.name = name; + this.time = time; + } + + @Override + public void run() { + try { + // 休眠指定的时间 + Thread.sleep(this.time); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(this.name + "休眠" + this.time + "毫秒。"); + } + + } + +} +``` + +### 线程礼让 + +`Thread.yield` 方法的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行 。该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。 + +```java +public class ThreadYieldDemo { + + public static void main(String[] args) { + MyThread t = new MyThread(); + new Thread(t, "线程 A").start(); + new Thread(t, "线程 B").start(); + } + + static class MyThread implements Runnable { + + @Override + public void run() { + for (int i = 0; i < 5; i++) { + try { + Thread.sleep(1000); + } catch (Exception e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + "运行,i = " + i); + if (i == 2) { + System.out.print("线程礼让:"); + Thread.yield(); + } + } + } + } +} +``` + +### 守护线程 + +什么是守护线程? + +- **守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程**。**当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程**。 +- 与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。 + +为什么需要守护线程? + +- 守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。 + +如何使用守护线程? + +- 可以使用 `isDaemon` 方法判断线程是否为守护线程。 +- 可以使用 `setDaemon` 方法设置线程为守护线程。 + - 正在运行的用户线程无法设置为守护线程,所以 `setDaemon` 必须在 `thread.start` 方法之前设置,否则会抛出 `llegalThreadStateException` 异常; + - 一个守护线程创建的子线程依然是守护线程。 + - 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。 + +```java +public class ThreadDaemonDemo { + + public static void main(String[] args) { + Thread t = new Thread(new MyThread(), "线程"); + t.setDaemon(true); // 此线程在后台运行 + System.out.println("线程 t 是否是守护进程:" + t.isDaemon()); + t.start(); // 启动线程 + } + + static class MyThread implements Runnable { + + @Override + public void run() { + while (true) { + System.out.println(Thread.currentThread().getName() + "在运行。"); + } + } + } +} +``` + +> 参考阅读:[Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) + +## 线程通信 + +> 当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。 + +### wait/notify/notifyAll + +- `wait` - `wait` 会自动释放当前线程占有的对象锁,并请求操作系统挂起当前线程,**让线程从 `RUNNING` 状态转入 `WAITING` 状态**,等待 `notify` / `notifyAll` 来唤醒。如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 `notify` 或者 `notifyAll` 来唤醒挂起的线程,造成死锁。 +- `notify` - 唤醒一个正在 `WAITING` 状态的线程,并让它拿到对象锁,具体唤醒哪一个线程由 JVM 控制 。 +- `notifyAll` - 唤醒所有正在 `WAITING` 状态的线程,接下来它们需要竞争对象锁。 + +> 注意: +> +> - **`wait`、`notify`、`notifyAll` 都是 `Object` 类中的方法**,而非 `Thread`。 +> - **`wait`、`notify`、`notifyAll` 只能用在 `synchronized` 方法或者 `synchronized` 代码块中使用,否则会在运行时抛出 `IllegalMonitorStateException`**。 + +生产者、消费者模式是 `wait`、`notify`、`notifyAll` 的一个经典使用案例: + +```java +public class ThreadWaitNotifyDemo02 { + + private static final int QUEUE_SIZE = 10; + private static final PriorityQueue queue = new PriorityQueue<>(QUEUE_SIZE); + + public static void main(String[] args) { + new Producer("生产者 A").start(); + new Producer("生产者 B").start(); + new Consumer("消费者 A").start(); + new Consumer("消费者 B").start(); + } + + static class Consumer extends Thread { + + Consumer(String name) { + super(name); + } + + @Override + public void run() { + while (true) { + synchronized (queue) { + while (queue.size() == 0) { + try { + System.out.println("队列空,等待数据"); + queue.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + queue.notifyAll(); + } + } + queue.poll(); // 每次移走队首元素 + queue.notifyAll(); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + " 从队列取走一个元素,队列当前有:" + queue.size() + "个元素"); + } + } + } + } + + static class Producer extends Thread { + + Producer(String name) { + super(name); + } + + @Override + public void run() { + while (true) { + synchronized (queue) { + while (queue.size() == QUEUE_SIZE) { + try { + System.out.println("队列满,等待有空余空间"); + queue.wait(); + } catch (InterruptedException e) { + e.printStackTrace(); + queue.notifyAll(); + } + } + queue.offer(1); // 每次插入一个元素 + queue.notifyAll(); + try { + Thread.sleep(500); + } catch (InterruptedException e) { + e.printStackTrace(); + } + System.out.println(Thread.currentThread().getName() + " 向队列取中插入一个元素,队列当前有:" + queue.size() + "个元素"); + } + } + } + } +} +``` + +### join + +在线程操作中,可以使用 `join` 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。 + +```java +public class ThreadJoinDemo { + + public static void main(String[] args) { + MyThread mt = new MyThread(); // 实例化 Runnable 子类对象 + Thread t = new Thread(mt, "mythread"); // 实例化 Thread 对象 + t.start(); // 启动线程 + for (int i = 0; i < 50; i++) { + if (i > 10) { + try { + t.join(); // 线程强制运行 + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + System.out.println("Main 线程运行 --> " + i); + } + } + + static class MyThread implements Runnable { + + @Override + public void run() { + for (int i = 0; i < 50; i++) { + System.out.println(Thread.currentThread().getName() + " 运行,i = " + i); // 取得当前线程的名字 + } + } + } +} +``` + +### 管道 + +管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。 +管道输入/输出流主要包括了如下 4 种具体实现:`PipedOutputStream`、`PipedInputStream`、`PipedReader` 和 `PipedWriter`,前两种面向字节,而后两种面向字符。 + +```java +public class Piped { + + public static void main(String[] args) throws Exception { + PipedWriter out = new PipedWriter(); + PipedReader in = new PipedReader(); + // 将输出流和输入流进行连接,否则在使用时会抛出 IOException + out.connect(in); + Thread printThread = new Thread(new Print(in), "PrintThread"); + printThread.start(); + int receive = 0; + try { + while ((receive = System.in.read()) != -1) { + out.write(receive); + } + } finally { + out.close(); + } + } + + static class Print implements Runnable { + + private PipedReader in; + + Print(PipedReader in) { + this.in = in; + } + + public void run() { + int receive = 0; + try { + while ((receive = in.read()) != -1) { + System.out.print((char) receive); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + } +} +``` + +## 线程生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408290809602.png) + +`java.lang.Thread.State` 中定义了 **6** 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。 + +以下是各状态的说明,以及状态间的联系: + +- **开始(NEW)** - 尚未调用 `start` 方法的线程处于此状态。此状态意味着:**创建的线程尚未启动**。 +- **可运行(RUNNABLE)** - 已经调用了 `start` 方法的线程处于此状态。此状态意味着,**线程已经准备好了**,一旦被线程调度器分配了 CPU 时间片,就可以运行线程。 + - 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。 +- **阻塞(BLOCKED)** - 此状态意味着:**线程处于被阻塞状态**。表示线程在等待 `synchronized` 的隐式锁(Monitor lock)。`synchronized` 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,即处于阻塞状态。当占用 `synchronized` 隐式锁的线程释放锁,并且等待的线程获得 `synchronized` 隐式锁时,就又会从 `BLOCKED` 转换到 `RUNNABLE` 状态。 +- **等待(WAITING)** - 此状态意味着:**线程无限期等待,直到被其他线程显式地唤醒**。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 `synchronized` 的隐式锁。而等待是主动的,通过调用 `Object.wait` 等方法进入。 + - 进入:`Object.wait()`;退出:`Object.notify` / `Object.notifyAll` + - 进入:`Thread.join()`;退出:被调用的线程执行完毕 + - 进入:`LockSupport.park()`;退出:`LockSupport.unpark` +- **定时等待(TIMED_WAITING)** - 等待指定时间的状态。一个线程处于定时等待状态,是由于执行了以下方法中的任意方法: + - 进入:`Thread.sleep(long)`;退出:时间结束 + - 进入:`Object.wait(long)`;退出:时间结束 / `Object.notify` / `Object.notifyAll` + - 进入:`Thread.join(long)`;退出:时间结束 / 被调用的线程执行完毕 + - 进入:`LockSupport.parkNanos(long)`;退出:`LockSupport.unpark` + - 进入:`LockSupport.parkUntil(long)`;退出:`LockSupport.unpark` +- **终止 (TERMINATED)** - 线程 `run()` 方法执行结束,或者因异常退出了 `run()` 方法,则该线程结束生命周期。死亡的线程不可再次复生。 + +> 👉 扩展阅读: +> +> - [Java Thread Methods and Thread States](https://www.w3resource.com/java-tutorial/java-threadclass-methods-and-threadstates.php) +> - [Java 线程的 5 种状态及切换(透彻讲解)](https://blog.csdn.net/pange1991/article/details/53860651) +> - [Java 线程运行怎么有第六种状态? - Dawell 的回答](https://www.zhihu.com/question/56494969/answer/154053599) + +## 线程常见问题 + +### 线程启动 + +**典型问题** + +(1)`Thread.start()` 和 `Thread.run()` 有什么区别? + +(2)可以直接调用 `Thread.run()` 方法么? + +(3)一个线程两次调用 `Thread.start()` 方法会怎样 + +**知识点** + +(1)`Thread.start()` 和 `Thread.run()` 的区别: + +- `run()` 方法是线程的执行体。 +- `start()` 方法负责启动线程,然后 JVM 会让这个线程去执行 `run()` 方法。 + +(2)可以直接调用 `Thread.run()` 方法,但是它的行为和普通方法一样,不会启动新线程去执行。**调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** + +(3)Java 的线程是不允许启动两次的,第二次调用必然会抛出 `IllegalThreadStateException`。 + +### 线程等待 + +**典型问题** + +(1)`Thread.sleep()`、`Thread.yield()`、`Thread.join()`、`Object.wait()` 方法有什么区别? + +(2)为什么 `Thread.sleep()`、`Thread.yield()` 设计为静态方法? + +**知识点** + +(1)`Thread.sleep()`、`Thread.yield()`、`Thread.join()` 方法的区别: + +- `Thread.sleep()` + - `Thread.sleep()` 方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入 **TIMED_WAITING** 状态。 + - 该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。 + - 但是,`Thread.sleep()` 方法不会释放“锁标志”,也就是说如果有 `synchronized` 同步块,其他线程仍然不能访问共享数据。 +- `Thread.yield()` + - `Thread.yield()` 方法可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程从 **RUNNING** 状态转入 **RUNNABLE** 状态。 + - 当某个线程调用了 `Thread.yield()` 方法暂停之后,只有优先级大于等于当前线程的处于就绪状态的线程才会获得执行的机会。 +- `Thread.join()` + - `Thread.join()` 方法会使当前线程转入 **WAITING** 或 **TIMED_WAITING** 状态,等待调用 `Thread.join()` 方法的线程结束后才能继续执行。 +- `Object.wait()` + - `Object.wait()` 用于使当前线程等待,直到其他线程调用相同对象的 `Object.notify()` 或 `Object.notifyAll()` 方法唤醒它。 + - 调用 `Object.wait()` 时,线程会释放对象锁,并进入等待状态。 + +(2)为什么 `Thread.sleep()`、`Thread.yield()` 设计为静态方法? + +`Thread.sleep()`、`Thread.yield()` 针对的是 **RUNNING** 状态的线程,也就是说在非 **RUNNING** 状态的线程上执行这两个方法没有意义。这就是为什么这两个方法被设计为静态的。它们只针对正在 **RUNNING** 状态的线程工作,避免程序员错误的认为可以在其他非 **RUNNING** 状态线程上调用。 + +> 👉 扩展阅读:[Java 线程中 yield 与 join 方法的区别](http://www.importnew.com/14958.html) +> 👉 扩展阅读:[sleep(),wait(),yield() 和 join() 方法的区别](https://blog.csdn.net/xiangwanpeng/article/details/54972952) + +### 线程通信 + +线程间通信是线程间共享资源的一种方式。`Object.wait()`, `Object.notify()` 和 `Object.notifyAll()` 是用于线程之间协作和通信的方法,它们通常与`synchronized` 关键字一起使用来实现线程的同步。 + +**典型问题** + +(1)为什么线程通信的方法 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 被定义在 `Object` 类里? + +(2)为什么 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 必须在 `synchronized` 方法/块中被调用? + +(3) `Object.wait()` 和 `Thread.sleep` 有什么区别? + +**知识点** + +(1)为什么线程通信的方法 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 被定义在 `Object` 类里? + +Java 的每个对象中都有一个称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。 + +如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。 + +- `Object.wait()` + - `Object.wait()` 方法用于使当前线程进入等待状态,直到其他线程调用相同对象的 `notify()` 或 `notifyAll()` 方法唤醒它。 + - 在调用 `wait()` 方法时,线程会释放对象的锁,并进入等待状态。通常在使用 `wait()` 方法时需要放在一个循环中,以避免虚假唤醒(spurious wakeups)。 +- `Object.notify()` + - `Object.notify()` 方法用于唤醒正在等待该对象的锁的一个线程。 + - 被唤醒的线程将会尝试重新获取对象的锁,一旦获取到锁,它将继续执行。 +- `Object.notifyAll()` + - `Object.notifyAll()` 方法用于唤醒正在等待该对象的锁的所有线程。 + - 所有被唤醒的线程将会竞争对象的锁,一旦获取到锁,它们将继续执行。 + +(2)为什么 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 必须在 `synchronized` 方法/块中被调用? + +当一个线程需要调用对象的 `wait()` 方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 `notify()` 方法。同样的,当一个线程需要调用对象的 `notify()` 方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。 + +由于所有的这些方法都需要线程持有对象的锁,这样就只能通过 `synchronized` 来实现,所以他们只能在 `synchronized` 方法/块中被调用。 + +(3) `Object.wait()` 和 `Thread.sleep` 有什么区别? + +相同点: + +1. 它们都可以让线程阻塞。 +2. 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。 + +不同点: + +1. wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。 +2. 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。 +3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。 +4. wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。 + +> 👉 扩展阅读:[Java 并发编程:线程间协作的两种方式:wait、notify、notifyAll 和 Condition](http://www.cnblogs.com/dolphin0520/p/3920385.html) + +### 线程优先级 + +**典型问题** + +(1)Java 的线程优先级如何控制? + +(2)高优先级的 Java 线程一定先执行吗? + +**知识点** + +(1)Java 中的线程优先级的范围是 `[1,10]`,一般来说,高优先级的线程在运行时会具有优先权。可以通过 `thread.setPriority(Thread.MAX_PRIORITY)` 的方式设置,默认优先级为 `5`。 + +(2)即使设置了线程的优先级,也**无法保证高优先级的线程一定先执行**。 + +这是因为 **Java 线程优先级依赖于操作系统的支持**,然而,不同的操作系统支持的线程优先级并不相同,不能很好的和 Java 中线程优先级一一对应。因此,Java 线程优先级控制并不可靠。 + +### 守护线程 + +**典型问题** + +(1)什么是守护线程? + +(2)如何创建守护线程? + +**知识点** + +(1)什么是守护线程? + +守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程。与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。 + +守护线程的优先级比较低,一般用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。 + +(2)创建守护线程的方式: + +- 使用 `thread.setDaemon(true)` 可以设置 thread 线程为守护线程。 +- 正在运行的用户线程无法设置为守护线程,所以 `thread.setDaemon(true)` 必须在 `thread.start()` 之前设置,否则会抛出 `llegalThreadStateException` 异常; +- 一个守护线程创建的子线程依然是守护线程。 +- 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。 + +> 👉 扩展阅读:[Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) + +### 线程数 + +**典型问题** + +(1)线程数是不是越多越好? + +(2)创建多少线程才合适? + +**知识点** + +使用多线程,初衷是为了提升程序性能。度量性能的核心指标是**延迟**和**吞吐量**。所谓提升性能,从度量的角度,主要是**降低延迟,提高吞吐量**。在并发编程领域,提升性能本质上就是提升硬件的利用率,再具体点来说,就是提升 I/O 的利用率和 CPU 的利用率。 + +多线程并非越多越好,过多的线程可能会导致过多的上下文切换,反而降低系统性能。 通常需要根据服务器硬件资源和预期负载来合理设定线程数大小。 + +程序一般都是 CPU 计算和 I/O 操作交叉执行的,由于 I/O 设备的速度相对于 CPU 来说都很慢,所以大部分情况下,I/O 操作执行的时间相对于 CPU 计算来说都非常长,这种场景我们一般都称为 I/O 密集型计算;和 I/O 密集型计算相对的就是 CPU 密集型计算了,CPU 密集型计算大部分场景下都是纯 CPU 计算。I/O 密集型程序和 CPU 密集型程序,计算最佳线程数的方法是不同的。 + +**对于 CPU 密集型的计算场景,理论上“线程的数量=CPU 核数”就是最合适的**。不过在工程上,**线程的数量一般会设置为“CPU 核数+1”**,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。 + +对于 I/O 密集型计算场景,最佳的线程数是与程序中 CPU 计算和 I/O 操作的耗时比相关的,我们可以总结出这样一个公式: + +> 最佳线程数=1 +(I/O 耗时 / CPU 耗时) + +## 参考资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [进程和线程关系及区别](https://blog.csdn.net/yaosiming2011/article/details/44280797) +- [Java 线程中 yield 与 join 方法的区别](http://www.importnew.com/14958.html) +- [sleep(),wait(),yield() 和 join() 方法的区别](https://blog.csdn.net/xiangwanpeng/article/details/54972952) +- [Java 并发编程:线程间协作的两种方式:wait、notify、notifyAll 和 Condition](https://www.cnblogs.com/dolphin0520/p/3920385.html) +- [Java 并发编程:Callable、Future 和 FutureTask](https://www.cnblogs.com/dolphin0520/p/3949310.html) +- [StackOverflow VisualVM - Thread States](https://stackoverflow.com/questions/27406200/visualvm-thread-states) +- [Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) +- [Java 并发](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md) +- [Why must wait() always be in synchronized block](https://stackoverflow.com/questions/2779484/why-must-wait-always-be-in-synchronized-block) +- [Java Thread Methods and Thread States](https://www.w3resource.com/java-tutorial/java-threadclass-methods-and-threadstates.php) +- [Java 线程的 5 种状态及切换(透彻讲解)](https://blog.csdn.net/pange1991/article/details/53860651) +- [Java 线程运行怎么有第六种状态? - Dawell 的回答](https://www.zhihu.com/question/56494969/answer/154053599) diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\347\272\277\347\250\213\346\261\240.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\347\272\277\347\250\213\346\261\240.md" new file mode 100644 index 0000000000..db0ce1a141 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\347\272\277\347\250\213\346\261\240.md" @@ -0,0 +1,714 @@ +--- +title: Java 并发之线程池 +date: 2019-12-24 23:52:25 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 + - 线程池 +permalink: /pages/123b90fb/ +--- + +# Java 并发之线程池 + +## 线程池简介 + +线程池就是管理一系列线程的资源池,其提供了一种限制和管理线程资源的方式。每个线程池还维护一些基本统计信息,例如已完成任务的数量。 + +如果并发请求数量很多,但每个线程执行的时间很短,就会出现频繁的创建和销毁线程。如此一来,会大大降低系统的效率,可能频繁创建和销毁线程的时间、资源开销要大于实际工作的所需。 + +使用 **线程池的好处** 有以下几点: + +- **降低资源消耗** - 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度** - 当任务到达时,任务可以不需要等到线程创建就能立即执行。 +- **提高线程的可管理性** - 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +## Executor 框架 + +Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架,目的是提供一种将”任务提交”与”任务如何运行”分离开来的机制。 + +通过 `Executor` 来启动线程比使用 `Thread` 的 `start` 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。 + +> this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用,调用尚未构造完全的对象的方法可能引发令人疑惑的错误。 + +### 核心 API 概述 + +Executor 框架核心 API 如下: + +- `Executor` - 运行任务的接口。 +- `ExecutorService` - 扩展了 `Executor` 接口。扩展能力: + - 支持有返回值的线程; + - 支持管理线程的生命周期。 +- `ScheduledExecutorService` - 扩展了 `ExecutorService` 接口,支持定时调度任务。 +- `AbstractExecutorService` - `ExecutorService` 接口的默认实现。 +- `ThreadPoolExecutor` - Executor 框架最核心的类,它继承了 `AbstractExecutorService` 类。 +- `ScheduledThreadPoolExecutor` - `ScheduledExecutorService` 接口的实现,一个可定时调度任务的线程池。 +- `Executors` - 可以通过调用 `Executors` 的静态工厂方法来创建线程池并返回一个 `ExecutorService` 对象。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/exexctor-uml.png) + +### Executor + +`Executor` 接口中只定义了一个 `execute` 方法,用于接收一个 `Runnable` 对象。 + +```java +public interface Executor { + void execute(Runnable command); +} +``` + +### ExecutorService + +`ExecutorService` 接口继承了 `Executor` 接口,它还提供了 `invokeAll`、`invokeAny`、`shutdown`、`submit` 等方法。 + +```java +public interface ExecutorService extends Executor { + + void shutdown(); + + List shutdownNow(); + + boolean isShutdown(); + + boolean isTerminated(); + + boolean awaitTermination(long timeout, TimeUnit unit) + throws InterruptedException; + + Future submit(Callable task); + + Future submit(Runnable task, T result); + + Future submit(Runnable task); + + List> invokeAll(Collection> tasks) + throws InterruptedException; + + List> invokeAll(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException; + + T invokeAny(Collection> tasks) + throws InterruptedException, ExecutionException; + + T invokeAny(Collection> tasks, + long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException; +} +``` + +从其支持的方法定义,不难看出:相比于 `Executor` 接口,`ExecutorService` 接口主要的扩展是: + +- 支持有返回值的线程 - `sumbit`、`invokeAll`、`invokeAny` 方法中都支持传入`Callable` 对象。 +- 支持管理线程生命周期 - `shutdown`、`shutdownNow`、`isShutdown` 等方法。 + +### ScheduledExecutorService + +`ScheduledExecutorService` 接口扩展了 `ExecutorService` 接口。 + +它除了支持前面两个接口的所有能力以外,还支持定时调度线程。 + +```java +public interface ScheduledExecutorService extends ExecutorService { + + public ScheduledFuture schedule(Runnable command, + long delay, TimeUnit unit); + + public ScheduledFuture schedule(Callable callable, + long delay, TimeUnit unit); + + public ScheduledFuture scheduleAtFixedRate(Runnable command, + long initialDelay, + long period, + TimeUnit unit); + + public ScheduledFuture scheduleWithFixedDelay(Runnable command, + long initialDelay, + long delay, + TimeUnit unit); + +} +``` + +其扩展的接口提供以下能力: + +- `schedule` 方法可以在指定的延时后执行一个 `Runnable` 或者 `Callable` 任务。 +- `scheduleAtFixedRate` 方法和 `scheduleWithFixedDelay` 方法可以按照指定时间间隔,定期执行任务。 + +## ThreadPoolExecutor + +`java.uitl.concurrent.ThreadPoolExecutor` 类是 `Executor` 框架中最核心的类。 + +### 构造方法 + +`ThreadPoolExecutor` 有四个构造方法,前三个都是基于第四个实现。第四个构造方法定义如下: + +```java +public ThreadPoolExecutor(int corePoolSize,// 线程池的核心线程数量 + int maximumPoolSize,// 线程池的最大线程数 + long keepAliveTime,// 当线程数大于核心线程数时,多余的空闲线程存活的最长时间 + TimeUnit unit,// 时间单位 + BlockingQueue workQueue,// 任务队列,用来储存等待执行任务的队列 + ThreadFactory threadFactory,// 线程工厂,用来创建线程,一般默认即可 + RejectedExecutionHandler handler// 拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 +) {// 略} +``` + +参数说明: + +- **`corePoolSize`** - **表示线程池保有的最小线程数**。 +- **`maximumPoolSize`** - **表示线程池允许创建的最大线程数**。 + - 如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。 + - 值得注意的是:如果使用了无界的任务队列这个参数就没什么效果。 +- **`keepAliveTime & unit`** - **表示线程保持活动的时间**。如果一个线程空闲了`keepAliveTime & unit` 这么久,而且线程池的线程数大于 `corePoolSize` ,那么这个空闲的线程就要被回收了。 +- **`workQueue`** - **等待执行的任务队列**。用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。 + - **`ArrayBlockingQueue`** - **有界阻塞队列**。 + - **`LinkedBlockingQueue`** - **无界阻塞队列**。 + - **`SynchronousQueue`** - **不会保存提交的任务,而是将直接新建一个线程来执行新来的任务**。 + - **`DelayedWorkQueue`** - 延迟阻塞队列。 + - **`PriorityBlockingQueue`** - **具有优先级的无界阻塞队列**。 +- **`threadFactory`** - **线程工厂**。线程工程用于自定义如何创建线程。 +- **`handler`** - **拒绝策略**。它是 `RejectedExecutionHandler` 类型的变量。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。线程池支持以下策略: + - **`AbortPolicy`** - **丢弃任务并抛出异常**。这也是默认策略,会抛出 `RejectedExecutionException`。 + - **`DiscardPolicy`** - **丢弃任务但不抛出异常**。 + - **`DiscardOldestPolicy`** - **丢弃队列最老的任务**,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。 + - **`CallerRunsPolicy`** - 提交任务的线程自己去执行该任务。 + - 如果以上策略都不能满足需要,也可以通过实现 `RejectedExecutionHandler` 接口来定制处理策略。如记录日志或持久化不能处理的任务。 + +### 重要字段 + +`ThreadPoolExecutor` 有以下重要字段: + +```java +private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); +private static final int COUNT_BITS = Integer.SIZE - 3; +private static final int CAPACITY = (1 << COUNT_BITS) - 1; +// runState is stored in the high-order bits +private static final int RUNNING = -1 << COUNT_BITS; +private static final int SHUTDOWN = 0 << COUNT_BITS; +private static final int STOP = 1 << COUNT_BITS; +private static final int TIDYING = 2 << COUNT_BITS; +private static final int TERMINATED = 3 << COUNT_BITS; +``` + +参数说明: + +- `ctl` - **用于控制线程池的运行状态和线程池中的有效线程数量**。它包含两部分的信息: + - 线程池的运行状态 (`runState`) + - 线程池内有效线程的数量 (`workerCount`) + - 可以看到,`ctl` 使用了 `Integer` 类型来保存,高 3 位保存 `runState`,低 29 位保存 `workerCount`。`COUNT_BITS` 就是 29,`CAPACITY` 就是 1 左移 29 位减 1(29 个 1),这个常量表示 `workerCount` 的上限值,大约是 5 亿。 +- 运行状态 - 线程池一共有五种运行状态: + - `RUNNING` - **运行状态**。接受新任务,并且也能处理阻塞队列中的任务。 + - `SHUTDOWN` - **关闭状态**。不接受新任务,但可以处理阻塞队列中的任务。 + - 在线程池处于 `RUNNING` 状态时,调用 `shutdown` 方法会使线程池进入到该状态。 + - `finalize` 方法在执行过程中也会调用 `shutdown` 方法进入该状态。 + - `STOP` - **停止状态**。不接受新任务,也不处理队列中的任务。会中断正在处理任务的线程。在线程池处于 `RUNNING` 或 `SHUTDOWN` 状态时,调用 `shutdownNow` 方法会使线程池进入到该状态。 + - `TIDYING` - **整理状态**。如果所有的任务都已终止了,`workerCount` (有效线程数) 为 0,线程池进入该状态后会调用 `terminated` 方法进入 `TERMINATED` 状态。 + - `TERMINATED` - **已终止状态**。在 `terminated` 方法执行完后进入该状态。默认 `terminated` 方法中什么也没有做。进入 `TERMINATED` 的条件如下: + - 线程池不是 `RUNNING` 状态; + - 线程池状态不是 `TIDYING` 状态或 `TERMINATED` 状态; + - 如果线程池状态是 `SHUTDOWN` 并且 `workerQueue` 为空; + - `workerCount` 为 0; + - 设置 `TIDYING` 状态成功。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409190729946.png) + +### 其他重要方法 + +在 `ThreadPoolExecutor` 类中还有一些重要的方法: + +- `submit` - 类似于 `execute`,但是针对的是有返回值的线程。`submit` 方法是在 `ExecutorService` 中声明的方法,在 `AbstractExecutorService` 就已经有了具体的实现。`ThreadPoolExecutor` 直接复用 `AbstractExecutorService` 的 `submit` 方法。 +- `shutdown` - 不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。 + - 将线程池切换到 `SHUTDOWN` 状态; + - 并调用 `interruptIdleWorkers` 方法请求中断所有空闲的 worker; + - 最后调用 `tryTerminate` 尝试结束线程池。 +- `shutdownNow` - 立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。与 `shutdown` 方法类似,不同的地方在于: + - 设置状态为 `STOP`; + - 中断所有工作线程,无论是否是空闲的; + - 取出阻塞队列中没有被执行的任务并返回。 +- `isShutdown` - 调用了 `shutdown` 或 `shutdownNow` 方法后,`isShutdown` 方法就会返回 true。 +- `isTerminaed` - 当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 `isTerminaed` 方法会返回 true。 +- `setCorePoolSize` - 设置核心线程数大小。 +- `setMaximumPoolSize` - 设置最大线程数大小。 +- `getTaskCount` - 线程池已经执行的和未执行的任务总数; +- `getCompletedTaskCount` - 线程池已完成的任务数量,该值小于等于 `taskCount`; +- `getLargestPoolSize` - 线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了 `maximumPoolSize`; +- `getPoolSize` - 线程池当前的线程数量; +- `getActiveCount` - 当前线程池中正在执行任务的线程数量。 + +### 使用示例 + +```java +public class ThreadPoolExecutorDemo { + + public static void main(String[] args) { + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 500, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + Executors.defaultThreadFactory(), + new ThreadPoolExecutor.AbortPolicy()); + + for (int i = 0; i < 100; i++) { + threadPoolExecutor.execute(new MyThread()); + String info = String.format("线程池中线程数目:%s,队列中等待执行的任务数目:%s,已执行玩别的任务数目:%s", + threadPoolExecutor.getPoolSize(), + threadPoolExecutor.getQueue().size(), + threadPoolExecutor.getCompletedTaskCount()); + System.out.println(info); + } + threadPoolExecutor.shutdown(); + } + + static class MyThread implements Runnable { + + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " 执行"); + } + + } + +} +``` + +## 线程池原理 + +默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。提交任务可以使用 `execute` 方法,它是 `ThreadPoolExecutor` 的核心方法,通过这个方法可以**向线程池提交一个任务,交由线程池去执行**。 + +```java +// 用于控制线程池的运行状态和线程池中的有效线程数量 +private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); + +public void execute(Runnable command) { + if (command == null) + throw new NullPointerException(); + + // 获取 ctl 中存储的线程池状态信息 + int c = ctl.get(); + + // 线程池执行可以分为 3 个步骤 + // 1. 若工作线程数小于核心线程数,则尝试启动一个新的线程来执行任务 + if (workerCountOf(c) < corePoolSize) { + if (addWorker(command, true)) + return; + c = ctl.get(); + } + + // 2. 如果任务可以成功地加入队列,还需要再次确认是否需要添加新的线程(因为可能自从上次检查以来已经有线程死亡)或者检查线程池是否已经关闭 + // -> 如果是后者,则可能需要回滚入队操作; + // -> 如果是前者,则可能需要启动新的线程 + if (isRunning(c) && workQueue.offer(command)) { + int recheck = ctl.get(); + if (!isRunning(recheck) && remove(command)) + reject(command); + else if (workerCountOf(recheck) == 0) + addWorker(null, false); + } + // 如果任务无法加入队列,则尝试添加一个新的线程 + // 如果添加新线程失败,说明线程池已经关闭或者达到了容量上限,此时将拒绝该任务 + else if (!addWorker(command, false)) + reject(command); +} +``` + +`execute` 方法工作流程如下: + +1. 如果 `workerCount < corePoolSize`,则创建并启动一个线程来执行新提交的任务; +2. 如果 `workerCount >= corePoolSize`,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中; +3. 如果 `workerCount >= corePoolSize && workerCount < maximumPoolSize`,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务; +4. 如果`workerCount >= maximumPoolSize`,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务,默认的处理方式是直接抛异常。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409190726019.png) + +在 `execute` 方法中,多次调用 `addWorker` 方法。`addWorker` 这个方法主要用来创建新的工作线程,如果返回 true 说明创建和启动工作线程成功,否则的话返回的就是 false。 + +```java +// 全局锁,并发操作必备 +private final ReentrantLock mainLock = new ReentrantLock(); +// 跟踪线程池的最大大小,只有在持有全局锁 mainLock 的前提下才能访问此集合 +private int largestPoolSize; +// 工作线程集合,存放线程池中所有的(活跃的)工作线程,只有在持有全局锁 mainLock 的前提下才能访问此集合 +private final HashSet workers = new HashSet<>(); +//获取线程池状态 +private static int runStateOf(int c) { return c & ~CAPACITY; } +//判断线程池的状态是否为 Running +private static boolean isRunning(int c) { + return c < SHUTDOWN; +} + +/** + * 添加新的工作线程到线程池 + * @param firstTask 要执行 + * @param core 参数为 true 的话表示使用线程池的基本大小,为 false 使用线程池最大大小 + * @return 添加成功就返回 true 否则返回 false + */ +private boolean addWorker(Runnable firstTask, boolean core) { + retry: + for (;;) { + //这两句用来获取线程池的状态 + int c = ctl.get(); + int rs = runStateOf(c); + + // Check if queue empty only if necessary. + if (rs >= SHUTDOWN && + ! (rs == SHUTDOWN && + firstTask == null && + ! workQueue.isEmpty())) + return false; + + for (;;) { + //获取线程池中工作的线程的数量 + int wc = workerCountOf(c); + // core 参数为 false 的话表明队列也满了,线程池大小变为 maximumPoolSize + if (wc >= CAPACITY || + wc >= (core ? corePoolSize : maximumPoolSize)) + return false; + //原子操作将 workcount 的数量加 1 + if (compareAndIncrementWorkerCount(c)) + break retry; + // 如果线程的状态改变了就再次执行上述操作 + c = ctl.get(); + if (runStateOf(c) != rs) + continue retry; + // else CAS failed due to workerCount change; retry inner loop + } + } + // 标记工作线程是否启动成功 + boolean workerStarted = false; + // 标记工作线程是否创建成功 + boolean workerAdded = false; + Worker w = null; + try { + + w = new Worker(firstTask); + final Thread t = w.thread; + if (t != null) { + // 加锁 + final ReentrantLock mainLock = this.mainLock; + mainLock.lock(); + try { + //获取线程池状态 + int rs = runStateOf(ctl.get()); + //rs < SHUTDOWN 如果线程池状态依然为 RUNNING, 并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中 + //(rs=SHUTDOWN && firstTask == null) 如果线程池状态小于 STOP,也就是 RUNNING 或者 SHUTDOWN 状态下,同时传入的任务实例 firstTask 为 null,则需要添加到工作线程集合和启动新的 Worker + // firstTask == null 证明只新建线程而不执行任务 + if (rs < SHUTDOWN || + (rs == SHUTDOWN && firstTask == null)) { + if (t.isAlive()) // precheck that t is startable + throw new IllegalThreadStateException(); + workers.add(w); + //更新当前工作线程的最大容量 + int s = workers.size(); + if (s > largestPoolSize) + largestPoolSize = s; + // 工作线程是否启动成功 + workerAdded = true; + } + } finally { + // 释放锁 + mainLock.unlock(); + } + //// 如果成功添加工作线程,则调用 Worker 内部的线程实例 t 的 Thread#start() 方法启动真实的线程实例 + if (workerAdded) { + t.start(); + /// 标记线程启动成功 + workerStarted = true; + } + } + } finally { + // 线程启动失败,需要从工作线程中移除对应的 Worker + if (! workerStarted) + addWorkerFailed(w); + } + return workerStarted; +} +``` + +## Executors + +`Executors` 类中提供了几种内置的 `ThreadPoolExecutor` 实现: + +- `FixedThreadPool`:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 +- `SingleThreadExecutor`: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 +- `CachedThreadPool`: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 +- `ScheduledThreadPool`:给定的延迟后运行任务或者定期执行任务的线程池。 + +> 注意: +> +> 《阿里巴巴 Java 开发手册》中明确要求不要使用 `Executors` 中的内置化线程池。 +> +> 【强制】线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。 +> +> 说明:`Executors` 返回的线程池对象的弊端如下: +> +> 1. `FixedThreadPool` 和 `SingleThreadPool`: 允许的请求队列长度为 `Integer.MAX_VALUE`,可能会堆积大量的请求,从而导致 OOM。 +> 2. `CachedThreadPool`:允许的创建线程数量为 `Integer.MAX_VALUE`,可能会创建大量的线程,从而导致 OOM。 +> 3. `ScheduledThreadPool`: 允许的请求队列长度为 `Integer.MAX_VALUE`,可能会堆积大量的请求,从而导致 OOM。 + +### FixedThreadPool + +**FixedThreadPool 是一个可重用的、线程数固定的线程池**。`Executors` 类中的相关源码: + +```java +public static ExecutorService newFixedThreadPool(int nThreads) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue()); +} + +public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) { + return new ThreadPoolExecutor(nThreads, nThreads, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory); +} +``` + +`FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 都被设置为 `nThreads`,这个 `nThreads` 参数是我们使用的时候自己传递的。 + +即使 `maximumPoolSize` 的值比 `corePoolSize` 大,也至多只会创建 `corePoolSize` 个线程。这是因为`FixedThreadPool` 使用的是容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列),队列永远不会被放满。 + +FixedThreadPool 的问题: + +`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`(队列的容量为 Integer.MAX_VALUE)作为线程池的工作队列会对线程池带来如下影响: + +1. 当线程池中的线程数达到 `corePoolSize` 后,新任务将在无界队列中等待,因此线程池中的线程数不会超过 `corePoolSize`; +2. 由于使用无界队列时 `maximumPoolSize` 将是一个无效参数,因为不可能存在任务队列满的情况。所以,通过创建 `FixedThreadPool`的源码可以看出创建的 `FixedThreadPool` 的 `corePoolSize` 和 `maximumPoolSize` 被设置为同一个值。 +3. 由于 1 和 2,使用无界队列时 `keepAliveTime` 将是一个无效参数; +4. 运行中的 `FixedThreadPool`(未执行 `shutdown()`或 `shutdownNow()`)不会拒绝任务,在任务比较多的时候会导致 OOM(内存溢出)。 + +### SingleThreadExecutor + +**`SingleThreadExecutor` 是只有一个线程的线程池**。SingleThreadExecutor 只会创建唯一的工作线程来执行任务,保证所有任务按照指定顺序 (FIFO, LIFO, 优先级)执行。 **如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它** 。 + +`Executors` 类中的相关源码: + +```java +public static ExecutorService newSingleThreadExecutor() { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue())); +} + +public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) { + return new FinalizableDelegatedExecutorService + (new ThreadPoolExecutor(1, 1, + 0L, TimeUnit.MILLISECONDS, + new LinkedBlockingQueue(), + threadFactory)); +} +``` + +`SingleThreadExecutor` 的问题: + +`SingleThreadExecutor` 和 `FixedThreadPool` 一样,使用的都是容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(无界队列)作为线程池的工作队列。`SingleThreadExecutor` 使用无界队列作为线程池的工作队列会对线程池带来的影响与 `FixedThreadPool` 相同。说简单点,就是可能会导致 OOM。 + +### CachedThreadPool + +`CachedThreadPool` 是一个会根据需要创建新线程的线程池。 + +- 如果线程池大小超过处理任务所需要的线程数,就会回收部分空闲的线程; +- 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为 1 分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 +- 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。 因此,使用 `CachedThreadPool` 时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。 + +```java +public static ExecutorService newCachedThreadPool() { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue()); +} + +public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) { + return new ThreadPoolExecutor(0, Integer.MAX_VALUE, + 60L, TimeUnit.SECONDS, + new SynchronousQueue(), + threadFactory); +} +``` + +`CachedThreadPool` 的`corePoolSize` 被设置为空(0),`maximumPoolSize`被设置为 `Integer.MAX.VALUE`,即它是无界的,这也就意味着如果主线程提交任务的速度高于 `maximumPool` 中线程处理任务的速度时,`CachedThreadPool` 会不断创建新的线程。极端情况下,这样会导致耗尽 cpu 和内存资源。 + +`CachedThreadPool` 的执行流程: + +1. 首先执行 `SynchronousQueue.offer(Runnable task)` 提交任务到任务队列。如果当前 `maximumPool` 中有闲线程正在执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`,那么主线程执行 offer 操作与空闲线程执行的 `poll` 操作配对成功,主线程把任务交给空闲线程执行,`execute()`方法执行完成,否则执行下面的步骤 2; +2. 当初始 `maximumPool` 为空,或者 `maximumPool` 中没有空闲线程时,将没有线程执行 `SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)`。这种情况下,步骤 1 将失败,此时 `CachedThreadPool` 会创建新线程执行任务,execute 方法执行完成; + +`CachedThreadPool` 的问题: + +`CachedThreadPool` 使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,可能会创建大量线程,从而导致 OOM。 + +### ScheduleThreadPool + +`ScheduledThreadPool` 用来在给定的延迟后运行任务或者定期执行任务。这个在实际项目中基本不会被用到,也不推荐使用。 + +```java +public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return new ScheduledThreadPoolExecutor(corePoolSize); +} + +public ScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue()); +} +``` + +`ScheduledThreadPool` 是通过 `ScheduledThreadPoolExecutor` 创建的,使用的`DelayedWorkQueue`(延迟阻塞队列)作为线程池的任务队列。 + +`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容原来容量的 1/2,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 + +`ScheduledThreadPoolExecutor` 继承了 `ThreadPoolExecutor`,所以创建 `ScheduledThreadExecutor` 本质也是创建一个 `ThreadPoolExecutor` 线程池,只是传入的参数不相同。 + +#### ScheduledThreadPoolExecutor 和 Timer 对比 + +- `Timer` 对系统时钟的变化敏感,`ScheduledThreadPoolExecutor`不是; +- `Timer` 只有一个执行线程,因此长时间运行的任务可以延迟其他任务。 `ScheduledThreadPoolExecutor` 可以配置任意数量的线程。 此外,如果你想(通过提供 `ThreadFactory`),你可以完全控制创建的线程; +- 在`TimerTask` 中抛出的运行时异常会杀死一个线程,从而导致 `Timer` 死机即计划任务将不再运行。`ScheduledThreadExecutor` 不仅捕获运行时异常,还允许您在需要时处理它们(通过重写 `afterExecute` 方法`ThreadPoolExecutor`)。抛出异常的任务将被取消,但其他任务将继续运行。 + +### WorkStealingPool + +> WorkStealingPool 是 JDK8 才引入的。 + +其内部会构建 `ForkJoinPool`,利用 [Work-Stealing](https://en.wikipedia.org/wiki/Work_stealing) 算法,并行地处理任务,不保证处理顺序。 + +## 线程池最佳实践 + +### 计算线程数量 + +一般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。 + +**CPU 密集型任务:**这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 + +**I/O 密集型任务:**这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 + +### 建议使用有界阻塞队列 + +不建议使用 `Executors` 的最重要的原因是:`Executors` 提供的很多方法默认使用的都是无界的 `LinkedBlockingQueue`,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以**强烈建议使用有界队列**。 + +《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 `new ThreadPoolExecutor` 来创建线程池。制订这条规则是因为容易导致生产事故,最典型的就是 `newFixedThreadPool` 和 `newCachedThreadPool`,可能因为资源耗尽导致 OOM 问题。 + +【示例】`newFixedThreadPool` OOM + +```java +ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); +printStats(threadPool); +for (int i = 0; i < 100000000; i++) { + threadPool.execute(() -> { + String payload = IntStream.rangeClosed(1, 1000000) + .mapToObj(__ -> "a") + .collect(Collectors.joining("")) + UUID.randomUUID().toString(); + try { + TimeUnit.HOURS.sleep(1); + } catch (InterruptedException e) { + } + log.info(payload); + }); +} + +threadPool.shutdown(); +threadPool.awaitTermination(1, TimeUnit.HOURS); +``` + +`newFixedThreadPool` 使用的工作队列是 `LinkedBlockingQueue` ,而默认构造方法的 `LinkedBlockingQueue` 是一个 `Integer.MAX_VALUE` 长度的队列,可以认为是无界的。如果任务较多并且执行较慢的话,队列可能会快速积压,撑爆内存导致 OOM。 + +【示例】`newCachedThreadPool` OOM + +```java +ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool(); +printStats(threadPool); +for (int i = 0; i < 100000000; i++) { + threadPool.execute(() -> { + String payload = UUID.randomUUID().toString(); + try { + TimeUnit.HOURS.sleep(1); + } catch (InterruptedException e) { + } + log.info(payload); + }); +} +threadPool.shutdown(); +threadPool.awaitTermination(1, TimeUnit.HOURS); +``` + +`newCachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE`,可以认为是没有上限的,而其工作队列 `SynchronousQueue` 是一个没有存储空间的阻塞队列。这意味着,只要有请求到来,就必须找到一条工作线程来处理,如果当前没有空闲的线程就再创建一条新的。 + +如果大量的任务进来后会创建大量的线程。我们知道线程是需要分配一定的内存空间作为线程栈的,比如 1MB,因此无限制创建线程必然会导致 OOM。 + +### 监测线程池运行状态 + +可以通过一些手段来检测线程池的运行状态比如 SpringBoot 中的 Actuator 组件。 + +除此之外,我们还可以利用 `ThreadPoolExecutor` 的相关 API 做一个简陋的监控。从下图可以看出, `ThreadPoolExecutor`提供了获取线程池当前的线程数和活跃线程数、已经执行完成的任务数、正在排队中的任务数等等。 + +下面是一个简单的 Demo。`printThreadPoolStatus()`会每隔一秒打印出线程池的线程数、活跃线程数、完成的任务数、以及队列中的任务数。 + +```java +public static void printThreadPoolStatus(ThreadPoolExecutor threadPool) { + ScheduledExecutorService scheduledExecutorService = new ScheduledThreadPoolExecutor(1, createThreadFactory("print-images/thread-pool-status", false)); + scheduledExecutorService.scheduleAtFixedRate(() -> { + log.info("========================="); + log.info("ThreadPool Size: [{}]", threadPool.getPoolSize()); + log.info("Active Threads: {}", threadPool.getActiveCount()); + log.info("Number of Tasks : {}", threadPool.getCompletedTaskCount()); + log.info("Number of Tasks in Queue: {}", threadPool.getQueue().size()); + log.info("========================="); + }, 0, 1, TimeUnit.SECONDS); +} +``` + +### 线程池和 ThreadLocal + +线程池和 `ThreadLocal`共用,可能会导致线程从`ThreadLocal`获取到的是旧值/脏数据。这是因为线程池会复用线程对象,与线程对象绑定的类的静态属性 `ThreadLocal` 变量也会被重用,这就导致一个线程可能获取到其他线程的`ThreadLocal` 值。 + +不要以为代码中没有显示使用线程池就不存在线程池了,像常用的 Web 服务器 Tomcat 处理任务为了提高并发量,就使用到了线程池,并且使用的是基于原生 Java 线程池改进完善得到的自定义线程池。 + +当然了,你可以将 Tomcat 设置为单线程处理任务。不过,这并不合适,会严重影响其处理任务的速度。 + +```properties +server.tomcat.max-threads=1 +``` + +解决上述问题比较建议的办法是使用阿里巴巴开源的 `TransmittableThreadLocal`(`TTL`)。`TransmittableThreadLocal`类继承并加强了 JDK 内置的`InheritableThreadLocal`类,在使用线程池等会池化复用线程的执行组件情况下,提供`ThreadLocal`值的传递功能,解决异步执行时上下文传递的问题。 + +`TransmittableThreadLocal` 项目地址:[https://github.com/alibaba/transmittable-thread-localopen in new window](https://github.com/alibaba/transmittable-thread-local) 。 + +### 重要任务应该自定义拒绝策略 + +使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw `RejectedExecutionException` 这是个运行时异常,对于运行时异常编译器并不强制 `catch` 它,所以开发人员很容易忽略。因此**默认拒绝策略要慎重使用**。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。 + +### 动态线程池 + +美团技术团队在 [《Java 线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html) 这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。 + +美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是: + +- **`corePoolSize`** - 核心线程数线程数定义了最小可以同时运行的线程数量。 +- **`maximumPoolSize`** - 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- **`workQueue`** - 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +JDK 原生线程池 `ThreadPoolExecutor` 提供了如下几个 public 的 setter 方法,如下图所示: + +![图 19 JDK 线程池参数设置接口](https://p1.meituan.net/travelcube/efd32f1211e9cf0a3ca9d35b0dc5de8588353.png) + +JDK 允许线程池使用方通过 `ThreadPoolExecutor` 的实例来动态设置线程池的核心策略。 + +重点是基于这几个 `public` 方法,我们只需要维护 `ThreadPoolExecutor` 的实例,并且在需要修改的时候拿到实例修改其参数即可。基于以上的思路,美团实现了线程池参数的动态化、线程池参数在管理平台可配置可修改,其效果图如下图所示: + +![图 21 可动态修改线程池参数](https://p0.meituan.net/travelcube/414ba7f3abd11e5f805c58635ae10988166121.png) + +如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目: + +- **[Hippo4jopen](https://github.com/opengoofy/hippo4j)** - 异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 +- **[Dynamic TPopen](https://github.com/dromara/dynamic-tp)** - 轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 + +## 参考资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) +- [深入理解 Java 线程池:ThreadPoolExecutor](https://www.jianshu.com/p/d2729853c4da) +- [java 并发编程--Executor 框架](https://www.cnblogs.com/MOBIN/p/5436482.html) +- [Java 线程池实现原理及其在美团业务中的实践](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html) diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/04.Java\351\224\201.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\351\224\201.md" similarity index 73% rename from "source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/04.Java\351\224\201.md" rename to "source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\351\224\201.md" index d2a439d28f..592e2c9022 100644 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/04.Java\351\224\201.md" +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\344\271\213\351\224\201.md" @@ -1,25 +1,27 @@ --- -title: Java锁 +title: Java 并发之锁 date: 2019-12-26 23:11:52 -order: 04 categories: - Java - - JavaSE + - JavaCore - 并发 tags: - Java - - JavaSE + - JavaCore - 并发 - 锁 -permalink: /pages/e2e047/ + - Lock + - Condition + - ReentrantLock + - ReentrantReadWriteLock + - StampedLock +permalink: /pages/2061f1f6/ --- -# 深入理解 Java 并发锁 +# Java 并发之锁 > 本文先阐述 Java 中各种锁的概念。 > -> 然后,介绍锁的核心实现 AQS。 -> > 然后,重点介绍 Lock 和 Condition 两个接口及其实现。并发编程有两个核心问题:同步和互斥。 > > **互斥**,即同一时刻只允许一个线程访问共享资源; @@ -109,30 +111,82 @@ class Task { - **`synchronized` 只支持非公平锁**。 - **`ReentrantLock` 、`ReentrantReadWriteLock`,默认是非公平锁,但支持公平锁**。 -### 独享锁与共享锁 +### 独占锁与共享锁 -独享锁与共享锁是一种广义上的说法,从实际用途上来看,也常被称为互斥锁与读写锁。 +独占锁与共享锁是一种广义上的说法,从实际用途上来看,也常被称为互斥锁与读写锁。 -- **独享锁** - 独享锁是指 **锁一次只能被一个线程所持有**。 +- **独占锁** - 独占锁是指 **锁一次只能被一个线程所持有**。 - **共享锁** - 共享锁是指 **锁可被多个线程所持有**。 -独享锁与共享锁在 Java 中的典型实现: +独占锁与共享锁在 Java 中的典型实现: -- **`synchronized` 、`ReentrantLock` 只支持独享锁**。 -- **`ReentrantReadWriteLock` 其写锁是独享锁,其读锁是共享锁**。读锁是共享锁使得并发读是非常高效的,读写,写读 ,写写的过程是互斥的。 +- **`synchronized` 、`ReentrantLock` 只支持独占锁**。 +- **`ReentrantReadWriteLock` 其写锁是独占锁,其读锁是共享锁**。读锁是共享锁使得并发读是非常高效的,读写,写读 ,写写的过程是互斥的。 ### 悲观锁与乐观锁 乐观锁与悲观锁不是指具体的什么类型的锁,而是**处理并发同步的策略**。 -- **悲观锁** - 悲观锁对于并发采取悲观的态度,认为:**不加锁的并发操作一定会出问题**。**悲观锁适合写操作频繁的场景**。 -- **乐观锁** - 乐观锁对于并发采取乐观的态度,认为:**不加锁的并发操作也没什么问题。对于同一个数据的并发操作,是不会发生修改的**。在更新数据的时候,会采用不断尝试更新的方式更新数据。**乐观锁适合读多写少的场景**。 - -悲观锁与乐观锁在 Java 中的典型实现: +#### 悲观锁(Pessimistic Lock) +- 总是假设最坏的情况,认为:**不加锁的并发操作一定会出问题**。 - 悲观锁在 Java 中的应用就是通过使用 `synchronized` 和 `Lock` 显示加锁来进行互斥同步,这是一种阻塞同步。 +- **悲观锁适合写操作频繁的场景**。高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。 + +【示例】悲观锁示例 + +```java +public void syncTask() { + synchronized (this) { + // 需要同步的操作 + } +} + +private Lock lock = new ReentrantLock(); +lock.lock(); +try { + // 需要同步的操作 +} finally { + lock.unlock(); +} +``` + +#### 乐观锁(OptimisticLock) + +- 乐观锁总是假设最好的情况,认为:**不加锁的并发操作也没什么问题**。每次访问数据时,都假设数据不会被其他线程修改,不必加锁。虽然不加锁,但不意味着什么都不做,而是在更新的时候,判断一下在此期间是否有其他线程更新该数据。 +- 乐观锁最常见的实现方式,是使用版本号机制或 CAS 算法(Compare And Swap)去实现。Java 中的原子类就是基于 CAS 实现的。 +- 乐观锁的**优点**是:减少锁竞争,提高并发度。 +- 乐观锁的**缺点**是: + - **存在 ABA 问题**。所谓的 ABA 问题是指在并发编程中,如果一个变量初次读取的时候是 A 值,它的值被改成了 B,然后又其他线程把 B 值改成了 A,而另一个早期线程在对比值时会误以为此值没有发生改变,但其实已经发生变化了 + - 如果乐观锁所检查的数据存在大量锁竞争,会由于**不断循环重试,产生大量的 CPU 开销**。 +- **乐观锁适合读多写少的场景**。高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。 + +【示例】乐观锁示例 -- 乐观锁在 Java 中的应用就是采用 `CAS` 机制(`CAS` 操作通过 `Unsafe` 类提供,但这个类不直接暴露为 API,所以都是间接使用,如各种原子类)。 +```java +// AtomicInteger 的 getAndAccumulate 方法采用了自旋 + CAS 的乐观锁模式 +public final int getAndAccumulate(int x, + IntBinaryOperator accumulatorFunction) { + int prev, next; + do { + prev = get(); + next = accumulatorFunction.applyAsInt(prev, x); + } while (!compareAndSet(prev, next)); + return prev; +} +``` + +乐观锁也是一种通用的锁机制,不仅在 Java 中,在其他很多软件领域,也存在乐观锁机制。比如下面的示例是 MySQL 中的乐观锁示例。 + +假设,order 表中有一个字段 status,表示订单状态:status 为 1 代表订单未支付;status 为 2 代表订单已支付。现在,要将 id 为 1 的订单状态置为已支付,则操作如下: + +```sql +select status, version from order where id=#{id} + +update order +set status=2, version=version+1 +where id=#{id} and version=#{version}; +``` ### 偏向锁、轻量级锁、重量级锁 @@ -144,12 +198,11 @@ Java 1.6 以后,针对 `synchronized` 做了大量优化,引入 4 种锁状 - **偏向锁** - 偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。 - **轻量级锁** - 是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。 - - **重量级锁** - 是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。 ### 分段锁 -分段锁其实是一种锁的设计,并不是具体的一种锁。所谓分段锁,就是把锁的对象分成多段,每段独立控制,使得锁粒度更细,减少阻塞开销,从而提高并发性。这其实很好理解,就像高速公路上的收费站,如果只有一个收费口,那所有的车只能排成一条队缴费;如果有多个收费口,就可以分流了。 +分段锁其实是一种锁的设计,并不是具体的一种锁。所谓**分段锁,就是把锁的对象分成多段,每段独立控制,使得锁粒度更细,减少阻塞开销,从而提高并发性**。这其实很好理解,就像高速公路上的收费站,如果只有一个收费口,那所有的车只能排成一条队缴费;如果有多个收费口,就可以分流了。 `Hashtable` 使用 `synchronized` 修饰方法来保证线程安全性,那么面对线程的访问,Hashtable 就会锁住整个对象,所有的其它线程只能等待,这种阻塞方式的吞吐量显然很低。 @@ -161,15 +214,15 @@ final Segment[] segments; 当有线程访问 `ConcurrentHashMap` 的数据时,`ConcurrentHashMap` 会先根据 hashCode 计算出数据在哪个桶(即哪个 Segment),然后锁住这个 `Segment`。 -### 显示锁和内置锁 +### 内置锁和显示锁 Java 1.5 之前,协调对共享对象的访问时可以使用的机制只有 `synchronized` 和 `volatile`。这两个都属于内置锁,即锁的申请和释放都是由 JVM 所控制。 Java 1.5 之后,增加了新的机制:`ReentrantLock`、`ReentrantReadWriteLock` ,这类锁的申请和释放都可以由程序所控制,所以常被称为显示锁。 -> 💡 `synchronized` 的用法和原理可以参考:[Java 并发基础机制 - synchronized](https://dunwu.github.io/waterdrop/pages/2c6488/#%E4%BA%8Csynchronized) 。 +> 💡 `synchronized` 的用法和原理可以参考:[Java 并发基础机制 - synchronized](https://dunwu.github.io/waterdrop/pages/25767945/#%E4%BA%8Csynchronized) 。 > -> :bell: 注意:如果不需要 `ReentrantLock`、`ReentrantReadWriteLock` 所提供的高级同步特性,**应该优先考虑使用 `synchronized`** 。理由如下: +> :bell: 注意:如果不需要 `ReentrantLock`、`ReentrantReadWriteLock` 所提供的高级同步特性,**应该优先考虑使用 `synchronized`**。理由如下: > > - Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `ReentrantLock`、`ReentrantReadWriteLock` 基本上持平。 > - 从趋势来看,Java 未来更可能会优化 `synchronized` ,而不是 `ReentrantLock`、`ReentrantReadWriteLock` ,因为 `synchronized` 是 JVM 内置属性,它能执行一些优化。 @@ -204,9 +257,7 @@ Java 1.5 之后,增加了新的机制:`ReentrantLock`、`ReentrantReadWriteL synchronized 是管程的一种实现,既然如此,何必再提供 Lock 和 Condition。 -JDK 1.6 以前,synchronized 还没有做优化,性能远低于 Lock。但是,性能不是引入 Lock 的最重要因素。真正关键在于:synchronized 使用不当,可能会出现死锁。 - -synchronized 无法通过**破坏不可抢占条件**来避免死锁。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。 +JDK 1.6 以前,synchronized 还没有做优化,性能远低于 Lock。但是,性能不是引入 Lock 的最重要因素。真正关键在于:synchronized 使用不当,可能会出现死锁。synchronized 无法通过**破坏不可抢占条件**来避免死锁。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。 与内置锁 `synchronized` 不同的是,**`Lock` 提供了一组无条件的、可轮询的、定时的以及可中断的锁操作**,所有获取锁、释放锁的操作都是显式的操作。 @@ -244,9 +295,7 @@ public interface Lock { 在单线程中,一段代码的执行可能依赖于某个状态,如果不满足状态条件,代码就不会被执行(典型的场景,如:`if ... else ...`)。在并发环境中,当一个线程判断某个状态条件时,其状态可能是由于其他线程的操作而改变,这时就需要有一定的协调机制来确保在同一时刻,数据只能被一个线程锁修改,且修改的数据状态被所有线程所感知。 -Java 1.5 之前,主要是利用 `Object` 类中的 `wait`、`notify`、`notifyAll` 配合 `synchronized` 来进行线程间通信(如果不了解其特性,可以参考:[Java 线程基础 - wait/notify/notifyAll](https://dunwu.github.io/javacore/#/concurrent/java-thread?id=waitnotifynotifyall))。 - -`wait`、`notify`、`notifyAll` 需要配合 `synchronized` 使用,不适用于 `Lock`。而使用 `Lock` 的线程,彼此间通信应该使用 `Condition` 。这可以理解为,什么样的锁配什么样的钥匙。**内置锁(`synchronized`)配合内置条件队列(`wait`、`notify`、`notifyAll` ),显式锁(`Lock`)配合显式条件队列(`Condition` )**。 +Java 1.5 之前,主要是利用 `Object` 类中的 `wait`、`notify`、`notifyAll` 配合 `synchronized` 来进行线程间通信。`wait`、`notify`、`notifyAll` 需要配合 `synchronized` 使用,不适用于 `Lock`。而使用 `Lock` 的线程,彼此间通信应该使用 `Condition` 。这可以理解为,什么样的锁配什么样的钥匙。**内置锁(`synchronized`)配合内置条件队列(`wait`、`notify`、`notifyAll` ),显式锁(`Lock`)配合显式条件队列(`Condition` )**。 #### Condition 的特性 @@ -417,13 +466,11 @@ public class LockConditionDemo { `ReentrantLock` 类是 `Lock` 接口的具体实现,与内置锁 `synchronized` 相同的是,它是一个**可重入锁**。 -### ReentrantLock 的特性 - `ReentrantLock` 的特性如下: - **`ReentrantLock` 提供了与 `synchronized` 相同的互斥性、内存可见性和可重入性**。 - `ReentrantLock` **支持公平锁和非公平锁**(默认)两种模式。 -- `ReentrantLock` 实现了 `Lock` 接口,支持了 `synchronized` 所不具备的**灵活性**。 +- `ReentrantLock` 实现了 `Lock` 接口,支持了 `synchronized` 所不具备的**灵活性**,增加了轮询、超时、中断等功能。 - `synchronized` 无法中断一个正在等待获取锁的线程 - `synchronized` 无法在请求获取一个锁时无休止地等待 @@ -556,7 +603,7 @@ public void execute() { if (lock.tryLock()) { try { for (int i = 0; i < 3; i++) { - // 略... + // 略。.. } } finally { lock.unlock(); @@ -577,7 +624,7 @@ public void execute() { if (lock.tryLock(2, TimeUnit.SECONDS)) { try { for (int i = 0; i < 3; i++) { - // 略... + // 略。.. } } finally { lock.unlock(); @@ -609,7 +656,7 @@ public void execute() { lock.lockInterruptibly(); for (int i = 0; i < 3; i++) { - // 略... + // 略。.. } } catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + "被中断"); @@ -622,7 +669,7 @@ public void execute() { #### newCondition 方法 -`newCondition()` - 返回一个绑定到 `Lock` 对象上的 `Condition` 实例。`Condition` 的特性和具体方法请阅读下文 [`Condition`](#五condition)。 +`newCondition()` - 返回一个绑定到 `Lock` 对象上的 `Condition` 实例。`Condition` 的特性和具体方法请阅读下文 [`Condition`](#五 condition)。 ### ReentrantLock 的原理 @@ -673,11 +720,11 @@ ReentrantLock 获取锁和释放锁的接口,从表象看,是调用 `Reentra 仔细阅读源码很容易发现: - `void lock()` 调用 Sync 的 lock() 方法。 -- `void lockInterruptibly()` 直接调用 AQS 的 [获取可中断的独占锁](#获取可中断的独占锁) 方法 `lockInterruptibly()`。 +- `void lockInterruptibly()` 直接调用 AQS 的 [获取可中断的独占锁](#获取可中断的独占锁) 方法 `lockInterruptibly()`。 - `boolean tryLock()` 调用 Sync 的 `nonfairTryAcquire()` 。 -- `boolean tryLock(long time, TimeUnit unit)` 直接调用 AQS 的 [获取超时等待式的独占锁](#获取超时等待式的独占锁) 方法 `tryAcquireNanos(int arg, long nanosTimeout)`。 -- `void unlock()` 直接调用 AQS 的 [释放独占锁](#释放独占锁) 方法 `release(int arg)` 。 +- `boolean tryLock(long time, TimeUnit unit)` 直接调用 AQS 的 [获取超时等待式的独占锁](#获取超时等待式的独占锁) 方法 `tryAcquireNanos(int arg, long nanosTimeout)`。 +- `void unlock()` 直接调用 AQS 的 [释放独占锁](#释放独占锁) 方法 `release(int arg)` 。 直接调用 AQS 接口的方法就不再赘述了,其原理在 [AQS 的原理](#AQS 的原理) 中已经用很大篇幅进行过讲解。 @@ -690,7 +737,7 @@ final boolean nonfairTryAcquire(int acquires) { int c = getState(); if (c == 0) { if (compareAndSetState(0, acquires)) { - // 如果同步状态为0,将其设为 acquires,并设置当前线程为排它线程 + // 如果同步状态为 0,将其设为 acquires,并设置当前线程为排它线程 setExclusiveOwnerThread(current); return true; } @@ -726,7 +773,7 @@ lock 方法在公平锁和非公平锁中的实现: // 非公平锁实现 final void lock() { if (compareAndSetState(0, 1)) - // 如果同步状态为0,将其设为1,并设置当前线程为排它线程 + // 如果同步状态为 0,将其设为 1,并设置当前线程为排它线程 setExclusiveOwnerThread(Thread.currentThread()); else // 调用 AQS 获取独占锁方法 acquire @@ -801,7 +848,7 @@ public ReentrantReadWriteLock(boolean fair) {} #### ReentrantReadWriteLock 的使用实例 -在 [`ReentrantReadWriteLock` 的特性](#reentrantreadwritelock-的特性) 中已经介绍过,`ReentrantReadWriteLock` 的读写锁(`ReadLock`、`WriteLock`)都实现了 `Lock` 接口,所以其各自独立的使用方式与 `ReentrantLock` 一样,这里不再赘述。 +在 [`ReentrantReadWriteLock` 的特性](#reentrantreadwritelock-的特性) 中已经介绍过,`ReentrantReadWriteLock` 的读写锁(`ReadLock`、`WriteLock`) 都实现了 `Lock` 接口,所以其各自独立的使用方式与 `ReentrantLock` 一样,这里不再赘述。 `ReentrantReadWriteLock` 与 `ReentrantLock` 用法上的差异,主要在于读写锁的配合使用。本文以一个典型使用场景来进行讲解。 @@ -924,7 +971,6 @@ pool-1-thread-2 写入数据 0:21 pool-1-thread-2 写入数据 1:41 pool-1-thread-2 写入数据 2:63 main 读数据 0:21 -main 读数据 0:21 // ... ``` @@ -993,7 +1039,7 @@ StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 **Stamp - ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞; - 而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。 -对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是**StampedLock 的功能仅仅是 ReadWriteLock 的子集**,在使用的时候,还是有几个地方需要注意一下。 +对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是,**StampedLock 的功能仅仅是 ReadWriteLock 的子集**,在使用的时候,还是有几个地方需要注意一下。 - **StampedLock 不支持重入** - StampedLock 的悲观读锁、写锁都不支持条件变量。 @@ -1002,20 +1048,19 @@ StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 **Stamp 【示例】StampedLock 阻塞时,调用 interrupt() 导致 CPU 飙升 ```java -final StampedLock lock - = new StampedLock(); -Thread T1 = new Thread(()->{ - // 获取写锁 - lock.writeLock(); - // 永远阻塞在此处,不释放写锁 - LockSupport.park(); +final StampedLock lock = new StampedLock(); +Thread T1 = new Thread(() -> { + // 获取写锁 + lock.writeLock(); + // 永远阻塞在此处,不释放写锁 + LockSupport.park(); }); T1.start(); // 保证 T1 获取写锁 Thread.sleep(100); -Thread T2 = new Thread(()-> - // 阻塞在悲观读锁 - lock.readLock() +Thread T2 = new Thread(() -> + // 阻塞在悲观读锁 + lock.readLock() ); T2.start(); // 保证 T2 阻塞在读锁 @@ -1029,28 +1074,26 @@ T2.join(); 【示例】StampedLock 读模板: ```java -final StampedLock sl = - new StampedLock(); +final StampedLock sl = new StampedLock(); // 乐观读 -long stamp = - sl.tryOptimisticRead(); +long stamp = sl.tryOptimisticRead(); // 读入方法局部变量 -...... +// ...... // 校验 stamp -if (!sl.validate(stamp)){ - // 升级为悲观读锁 - stamp = sl.readLock(); - try { - // 读入方法局部变量 - ..... - } finally { - // 释放悲观读锁 - sl.unlockRead(stamp); - } +if (!sl.validate(stamp)) { + // 升级为悲观读锁 + stamp = sl.readLock(); + try { + // 读入方法局部变量 + // ..... + } finally { + // 释放悲观读锁 + sl.unlockRead(stamp); + } } // 使用方法局部变量执行业务操作 -...... +// ...... ``` 【示例】StampedLock 写模板: @@ -1065,238 +1108,12 @@ try { } ``` -## AQS - -> `AbstractQueuedSynchronizer`(简称 **AQS**)是**队列同步器**,顾名思义,其主要作用是处理同步。它是并发锁和很多同步工具类的实现基石(如 `ReentrantLock`、`ReentrantReadWriteLock`、`CountDownLatch`、`Semaphore`、`FutureTask` 等)。 - -### AQS 的要点 - -**AQS 提供了对独享锁与共享锁的支持**。 - -在 `java.util.concurrent.locks` 包中的相关锁(常用的有 `ReentrantLock`、 `ReadWriteLock`)都是基于 AQS 来实现。这些锁都没有直接继承 AQS,而是定义了一个 `Sync` 类去继承 AQS。为什么要这样呢?因为锁面向的是使用用户,而同步器面向的则是线程控制,那么在锁的实现中聚合同步器而不是直接继承 AQS 就可以很好的隔离二者所关注的事情。 - -### AQS 的应用 - -**AQS 提供了对独享锁与共享锁的支持**。 - -#### 独享锁 API - -获取、释放独享锁的主要 API 如下: - -```java -public final void acquire(int arg) -public final void acquireInterruptibly(int arg) -public final boolean tryAcquireNanos(int arg, long nanosTimeout) -public final boolean release(int arg) -``` - -- `acquire` - 获取独占锁。 -- `acquireInterruptibly` - 获取可中断的独占锁。 -- `tryAcquireNanos` - 尝试在指定时间内获取可中断的独占锁。在以下三种情况下回返回: - - 在超时时间内,当前线程成功获取了锁; - - 当前线程在超时时间内被中断; - - 超时时间结束,仍未获得锁返回 false。 -- `release` - 释放独占锁。 - -#### 共享锁 API - -获取、释放共享锁的主要 API 如下: - -```java -public final void acquireShared(int arg) -public final void acquireSharedInterruptibly(int arg) -public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) -public final boolean releaseShared(int arg) -``` - -- `acquireShared` - 获取共享锁。 -- `acquireSharedInterruptibly` - 获取可中断的共享锁。 -- `tryAcquireSharedNanos` - 尝试在指定时间内获取可中断的共享锁。 -- `release` - 释放共享锁。 - -### AQS 的原理 - -> ASQ 原理要点: -> -> - AQS 使用一个整型的 `volatile` 变量来 **维护同步状态**。状态的意义由子类赋予。 -> - AQS 维护了一个 FIFO 的双链表,用来存储获取锁失败的线程。 -> -> AQS 围绕同步状态提供两种基本操作“获取”和“释放”,并提供一系列判断和处理方法,简单说几点: -> -> - state 是独占的,还是共享的; -> - state 被获取后,其他线程需要等待; -> - state 被释放后,唤醒等待线程; -> - 线程等不及时,如何退出等待。 -> -> 至于线程是否可以获得 state,如何释放 state,就不是 AQS 关心的了,要由子类具体实现。 - -#### AQS 的数据结构 - -阅读 AQS 的源码,可以发现:AQS 继承自 `AbstractOwnableSynchronize`。 - -```java -public abstract class AbstractQueuedSynchronizer - extends AbstractOwnableSynchronizer - implements java.io.Serializable { - - /** 等待队列的队头,懒加载。只能通过 setHead 方法修改。 */ - private transient volatile Node head; - /** 等待队列的队尾,懒加载。只能通过 enq 方法添加新的等待节点。*/ - private transient volatile Node tail; - /** 同步状态 */ - private volatile int state; -} -``` - -- `state` - AQS 使用一个整型的 `volatile` 变量来 **维护同步状态**。 - - 这个整数状态的意义由子类来赋予,如`ReentrantLock` 中该状态值表示所有者线程已经重复获取该锁的次数,`Semaphore` 中该状态值表示剩余的许可数量。 -- `head` 和 `tail` - AQS **维护了一个 `Node` 类型(AQS 的内部类)的双链表来完成同步状态的管理**。这个双链表是一个双向的 FIFO 队列,通过 `head` 和 `tail` 指针进行访问。当 **有线程获取锁失败后,就被添加到队列末尾**。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/aqs_1.png) - -再来看一下 `Node` 的源码 - -```java -static final class Node { - /** 该等待同步的节点处于共享模式 */ - static final Node SHARED = new Node(); - /** 该等待同步的节点处于独占模式 */ - static final Node EXCLUSIVE = null; - - /** 线程等待状态,状态值有: 0、1、-1、-2、-3 */ - volatile int waitStatus; - static final int CANCELLED = 1; - static final int SIGNAL = -1; - static final int CONDITION = -2; - static final int PROPAGATE = -3; - - /** 前驱节点 */ - volatile Node prev; - /** 后继节点 */ - volatile Node next; - /** 等待锁的线程 */ - volatile Thread thread; - - /** 和节点是否共享有关 */ - Node nextWaiter; -} -``` - -很显然,Node 是一个双链表结构。 - -- `waitStatus` - `Node` 使用一个整型的 `volatile` 变量来 维护 AQS 同步队列中线程节点的状态。`waitStatus` 有五个状态值: - - `CANCELLED(1)` - 此状态表示:该节点的线程可能由于超时或被中断而 **处于被取消(作废)状态**,一旦处于这个状态,表示这个节点应该从等待队列中移除。 - - `SIGNAL(-1)` - 此状态表示:**后继节点会被挂起**,因此在当前节点释放锁或被取消之后,必须唤醒(`unparking`)其后继结点。 - - `CONDITION(-2)` - 此状态表示:该节点的线程 **处于等待条件状态**,不会被当作是同步队列上的节点,直到被唤醒(`signal`),设置其值为 0,再重新进入阻塞状态。 - - `PROPAGATE(-3)` - 此状态表示:下一个 `acquireShared` 应无条件传播。 - - 0 - 非以上状态。 - -#### 独占锁的获取和释放 - -##### 获取独占锁 - -AQS 中使用 `acquire(int arg)` 方法获取独占锁,其大致流程如下: - -1. 先尝试获取同步状态,如果获取同步状态成功,则结束方法,直接返回。 -2. 如果获取同步状态不成功,AQS 会不断尝试利用 CAS 操作将当前线程插入等待同步队列的队尾,直到成功为止。 -3. 接着,不断尝试为等待队列中的线程节点获取独占锁。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/aqs_2.png) - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/aqs_3.png) - -详细流程可以用下图来表示,请结合源码来理解(一图胜千言): - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/aqs_4.png) - -##### 释放独占锁 - -AQS 中使用 `release(int arg)` 方法释放独占锁,其大致流程如下: - -1. 先尝试获取解锁线程的同步状态,如果获取同步状态不成功,则结束方法,直接返回。 -2. 如果获取同步状态成功,AQS 会尝试唤醒当前线程节点的后继节点。 - -##### 获取可中断的独占锁 - -AQS 中使用 `acquireInterruptibly(int arg)` 方法获取可中断的独占锁。 - -`acquireInterruptibly(int arg)` 实现方式**相较于获取独占锁方法( `acquire`)非常相似**,区别仅在于它会**通过 `Thread.interrupted` 检测当前线程是否被中断**,如果是,则立即抛出中断异常(`InterruptedException`)。 - -##### 获取超时等待式的独占锁 - -AQS 中使用 `tryAcquireNanos(int arg)` 方法获取超时等待的独占锁。 - -doAcquireNanos 的实现方式 **相较于获取独占锁方法( `acquire`)非常相似**,区别在于它会根据超时时间和当前时间计算出截止时间。在获取锁的流程中,会不断判断是否超时,如果超时,直接返回 false;如果没超时,则用 `LockSupport.parkNanos` 来阻塞当前线程。 - -#### 共享锁的获取和释放 - -##### 获取共享锁 - -AQS 中使用 `acquireShared(int arg)` 方法获取共享锁。 - -`acquireShared` 方法和 `acquire` 方法的逻辑很相似,区别仅在于自旋的条件以及节点出队的操作有所不同。 - -成功获得共享锁的条件如下: - -- `tryAcquireShared(arg)` 返回值大于等于 0 (这意味着共享锁的 permit 还没有用完)。 -- 当前节点的前驱节点是头结点。 - -##### 释放共享锁 - -AQS 中使用 `releaseShared(int arg)` 方法释放共享锁。 - -`releaseShared` 首先会尝试释放同步状态,如果成功,则解锁一个或多个后继线程节点。释放共享锁和释放独享锁流程大体相似,区别在于: - -对于独享模式,如果需要 SIGNAL,释放仅相当于调用头节点的 `unparkSuccessor`。 - -##### 获取可中断的共享锁 - -AQS 中使用 `acquireSharedInterruptibly(int arg)` 方法获取可中断的共享锁。 - -`acquireSharedInterruptibly` 方法与 `acquireInterruptibly` 几乎一致,不再赘述。 - -##### 获取超时等待式的共享锁 - -AQS 中使用 `tryAcquireSharedNanos(int arg)` 方法获取超时等待式的共享锁。 - -`tryAcquireSharedNanos` 方法与 `tryAcquireNanos` 几乎一致,不再赘述。 - -## 死锁 - -### 什么是死锁 - -死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也 -可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。 - -### 如何定位死锁 - -定位死锁最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测。 - -如果我们是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用 Java 提供的标准管理 API,`ThreadMXBean`,其直接就提供了 `findDeadlockedThreads()` 方法用于定位。 - -### 如何避免死锁 - -基本上死锁的发生是因为: - -- 互斥,类似 Java 中 Monitor 都是独占的。 -- 长期保持互斥,在使用结束之前,不会释放,也不能被其他线程抢占。 -- 循环依赖,多个个体之间出现了锁的循环依赖,彼此依赖上一环释放锁。 - -由此,我们可以分析出避免死锁的思路和方法。 - -(1)避免一个线程同时获取多个锁。 - -避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源。 - -尝试使用定时锁 `lock.tryLock(timeout)`,避免锁一直不能释放。 - -对于数据库锁,加锁和解锁必须在一个数据库连接中里,否则会出现解锁失败的情况。 - ## 参考资料 - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) - [Java 并发编程:Lock](https://www.cnblogs.com/dolphin0520/p/3923167.html) - [深入学习 java 同步器 AQS](https://zhuanlan.zhihu.com/p/27134110) - [AbstractQueuedSynchronizer 框架](https://t.hao0.me/java/2016/04/01/aqs.html) -- [Java 中的锁分类](https://www.cnblogs.com/qifengshi/p/6831055.html) \ No newline at end of file +- [Java 中的锁分类](https://www.cnblogs.com/qifengshi/p/6831055.html) diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/01.Java\345\271\266\345\217\221\347\256\200\344\273\213.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\347\256\200\344\273\213.md" similarity index 51% rename from "source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/01.Java\345\271\266\345\217\221\347\256\200\344\273\213.md" rename to "source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\347\256\200\344\273\213.md" index 662dc1fa3b..955d4e6fa9 100644 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/01.Java\345\271\266\345\217\221\347\256\200\344\273\213.md" +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/Java\345\271\266\345\217\221\347\256\200\344\273\213.md" @@ -1,111 +1,42 @@ --- -title: Java并发简介 +title: Java 并发简介 date: 2019-05-06 15:33:13 -order: 01 categories: - Java - - JavaSE + - JavaCore - 并发 tags: - Java - - JavaSE + - JavaCore - 并发 -permalink: /pages/f6b642/ + - 线程 + - 安全性 + - 活跃性 + - 性能 + - 死锁 + - 活锁 +permalink: /pages/97c73d27/ --- # Java 并发简介 -> **关键词**:`进程`、`线程`、`安全性`、`活跃性`、`性能`、`死锁`、`饥饿`、`上下文切换` -> -> **摘要**:并发编程并非 Java 语言所独有,而是一种成熟的编程范式,Java 只是用自己的方式实现了并发工作模型。学习 Java 并发编程,应该先熟悉并发的基本概念,然后进一步了解并发的特性以及其特性所面临的问题。掌握了这些,当学习 Java 并发工具时,才会明白它们各自是为了解决什么问题,为什么要这样设计。通过这样由点到面的学习方式,更容易融会贯通,将并发知识形成体系化。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701113445.png) - -## 并发概念 - -并发编程中有很多术语概念相近,容易让人混淆。本节内容通过对比分析,力求让读者清晰理解其概念以及差异。 - -### 并发和并行 - -并发和并行是最容易让新手费解的概念,那么如何理解二者呢?其最关键的差异在于:是否是**同时**发生: - -- **并发**:是指具备处理多个任务的能力,但不一定要同时。 -- **并行**:是指具备同时处理多个任务的能力。 - -下面是我见过最生动的说明,摘自 [并发与并行的区别是什么?——知乎的高票答案](https://www.zhihu.com/question/33515481/answer/58849148): - -- 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 -- 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 -- 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 - -### 同步和异步 - -- **同步**:是指在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。 -- **异步**:则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。 - -举例来说明: - -- 同步就像是打电话:不挂电话,通话不会结束。 -- 异步就像是发短信:发完短信后,就可以做其他事;当收到回复短信时,手机会通过铃声或振动来提醒。 - -### 阻塞和非阻塞 - -阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态: - -- **阻塞**:是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 -- **非阻塞**:是指在不能立刻得到结果之前,该调用不会阻塞当前线程。 - -举例来说明: - -- 阻塞调用就像是打电话,通话不结束,不能放下。 -- 非阻塞调用就像是发短信,发完短信后,就可以做其他事,短信来了,手机会提醒。 - -### 进程和线程 - -- **进程**:进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。进程可视为一个正在运行的程序。 -- **线程**:线程是操作系统进行调度的基本单位。 - -进程和线程的差异: - -- 一个程序至少有一个进程,一个进程至少有一个线程。 -- 线程比进程划分更细,所以执行开销更小,并发性更高 -- 进程是一个实体,拥有独立的资源;而同一个进程中的多个线程共享进程的资源。 - -

- -

- -JVM 在单个进程中运行,JVM 中的线程共享属于该进程的堆。这就是为什么几个线程可以访问同一个对象。线程共享堆并拥有自己的堆栈空间。这是一个线程如何调用一个方法以及它的局部变量是如何保持线程安全的。但是堆不是线程安全的并且为了线程安全必须进行同步。 - -### 竞态条件和临界区 - -- **竞态条件(Race Condition)**:当两个线程竞争同一资源时,如果对资源的访问顺序敏感,就称存在竞态条件。 - -- **临界区(Critical Sections)**:导致竞态条件发生的代码区称作临界区。 +> **摘要** - 并发编程并非 Java 语言所独有,而是一种成熟的编程范式,Java 只是用自己的方式实现了并发工作模型。学习 Java 并发编程,应该先熟悉并发的基本概念,然后进一步了解并发的特性以及其特性所面临的问题。掌握了这些,当学习 Java 并发工具时,才会明白它们各自是为了解决什么问题,为什么要这样设计。通过这样由点到面的学习方式,更容易融会贯通,将并发知识形成体系化。 -### 管程 +## 什么是并发 -管程(Monitor),是指管理共享变量以及对共享变量的操作过程,让他们支持并发。 - -Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而**管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程**。 - -## 并发的特点 - -技术在进步,CPU、内存、I/O 设备的性能也在不断提高。但是,始终存在一个核心矛盾:**CPU、内存、I/O 设备存在速度差异**。CPU 远快于内存,内存远快于 I/O 设备。 - -木桶短板理论告诉我们:一只木桶能装多少水,取决于最短的那块木板。同理,程序整体性能取决于最慢的操作(即 I/O 操作),所以单方面提高 CPU、内存的性能是无效的。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201225170052.jpg) +技术在进步,CPU、内存、I/O 设备的性能也在不断提高。但是,始终存在一个核心矛盾:**CPU、内存、I/O 设备存在很大的速度差异** - CPU 远快于内存,内存远快于 I/O 设备。木桶短板理论告诉我们:一只木桶能装多少水,取决于最短的那块木板。同理,程序整体性能取决于最慢的操作(即 I/O 操作),所以单方面提高 CPU、内存的性能是无效的。 为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都做出了贡献,主要体现为: -- **CPU 增加了缓存**,以均衡与内存的速度差异; -- **操作系统增加了进程、线程**,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异; +- **CPU 增加了缓存**,以均衡与 CPU 内存的速度差异; +- **操作系统增加了进程、线程**,以分时复用 CPU,进而均衡 CPU 与 I/O 的速度差异; - **编译程序优化指令执行次序**,使得缓存能够得到更加合理地利用。 -其中,进程、线程使得计算机、程序有了并发处理任务的能力。 +其中,进程、线程使得计算机、程序有了**并发**处理任务的能力。**并发**是指具备处理多个任务的能力,但不一定要同时。 + +## 并发的优点 -并发的优点在于: +并发所带来的好处有: - 提升资源利用率 - 程序响应更快 @@ -115,22 +46,22 @@ Java 采用的是管程技术,synchronized 关键字及 wait()、notify()、no 想象一下,一个应用程序需要从本地文件系统中读取和处理文件的情景。比方说,从磁盘读取一个文件需要 5 秒,处理一个文件需要 2 秒。处理两个文件则需要: ``` -5秒读取文件A -2秒处理文件A -5秒读取文件B -2秒处理文件B +5 秒读取文件 A +2 秒处理文件 A +5 秒读取文件 B +2 秒处理文件 B --------------------- -总共需要14秒 +总共需要 14 秒 ``` 从磁盘中读取文件的时候,大部分的 CPU 时间用于等待磁盘去读取数据。在这段时间里,CPU 非常的空闲。它可以做一些别的事情。通过改变操作的顺序,就能够更好的使用 CPU 资源。看下面的顺序: ``` -5秒读取文件A -5秒读取文件B + 2秒处理文件A -2秒处理文件B +5 秒读取文件 A +5 秒读取文件 B + 2 秒处理文件 A +2 秒处理文件 B --------------------- -总共需要12秒 +总共需要 12 秒 ``` CPU 等待第一个文件被读取完。然后开始读取第二个文件。当第二文件在被读取的时候,CPU 会去处理第一个文件。记住,在等待磁盘读取文件的时候,CPU 大 部分时间是空闲的。 @@ -150,7 +81,7 @@ while(server is active) { } ``` -如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。另一种设计是,监听线程把请求传递给工作者线程(worker thread),然后立刻返回去监听。而工作者线程则能够处理这个请求并发送一个回复给客户端。这种设计如下所述: +如果一个请求需要占用大量的时间来处理,在这段时间内新的客户端就无法发送请求给服务端。只有服务器在监听的时候,请求才能被接收。另一种设计是,监听线程把请求传递给工作者线程 (worker thread),然后立刻返回去监听。而工作者线程则能够处理这个请求并发送一个回复给客户端。这种设计如下所述: ```java while(server is active) { @@ -163,11 +94,7 @@ while(server is active) { 桌面应用也是同样如此。如果你点击一个按钮开始运行一个耗时的任务,这个线程既要执行任务又要更新窗口和按钮,那么在任务执行的过程中,这个应用程序看起来好像没有反应一样。相反,任务可以传递给工作者线程(worker thread)。当工作者线程在繁忙地处理任务的时候,窗口线程可以自由地响应其他用户的请求。当工作者线程完成任务的时候,它发送信号给窗口线程。窗口线程便可以更新应用程序窗口,并显示任务的结果。对用户而言,这种具有工作者线程设计的程序显得响应速度更快。 -### 并发的问题 - -任何事物都有利弊,并发也不例外。 - -我们知道了并发带来的好处:提升资源利用率、程序响应更快,同时也要认识到并发带来的问题,主要有: +任何事物都有利弊,并发也不例外。我们知道了并发带来的好处:提升资源利用率、程序响应更快,同时也要认识到并发带来的问题,主要有: - 安全性问题 - 活跃性问题 @@ -177,9 +104,7 @@ while(server is active) { ## 安全性问题 -并发最重要的问题是并发安全问题。 - -**并发安全**:是指保证程序的正确性,使得并发处理结果符合预期。 +并发最重要的问题是并发安全问题。所谓**并发安全**,是指保证程序的正确性,使得并发处理结果符合预期。 并发安全需要保证几个基本特性: @@ -189,84 +114,107 @@ while(server is active) { ### 缓存导致的可见性问题 -> 一个线程对共享变量的修改,另外一个线程能够立刻看到,称为 **可见性**。 +一个线程对共享变量的修改,另外一个线程能够立刻看到,称为 **可见性**。 在单核时代,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。因为所有线程都是操作同一个 CPU 的缓存,一个线程对缓存的写,对另外一个线程来说一定是可见的。例如在下面的图中,线程 A 和线程 B 都是操作同一个 CPU 里面的缓存,所以线程 A 更新了变量 V 的值,那么线程 B 之后再访问变量 V,得到的一定是 V 的最新值(线程 A 写过的值)。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701110313.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409042331169.png) 多核时代,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言就不具备可见性了。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701110431.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409042332517.png) -【示例】线程不安全的示例 +::: tabs#计数器示例 -下面我们再用一段代码来验证一下多核场景下的可见性问题。下面的代码,每执行一次 add10K() 方法,都会循环 10000 次 count+=1 操作。在 calc() 方法中我们创建了两个线程,每个线程调用一次 add10K() 方法,我们来想一想执行 calc() 方法得到的结果应该是多少呢? +@tab 线程不安全的计数器 + +【示例】线程不安全的计数器示例 ❌ ```java -public class Test { - private long count = 0; - private void add10K() { - int idx = 0; - while(idx++ < 10000) { - count += 1; +@NotThreadSafe +public class NotThreadSafeCounter { + + private static long count = 0; + + private void add() { + int cnt = 0; + while (cnt++ < 100000) { + count += 1; + } } - } - public static long calc() { - final Test test = new Test(); - // 创建两个线程,执行 add() 操作 - Thread th1 = new Thread(()->{ - test.add10K(); - }); - Thread th2 = new Thread(()->{ - test.add10K(); - }); - // 启动两个线程 - th1.start(); - th2.start(); - // 等待两个线程执行结束 - th1.join(); - th2.join(); - return count; - } + + public static void main(String[] args) throws InterruptedException { + final NotThreadSafeCounter demo = new NotThreadSafeCounter(); + // 创建两个线程,执行 add() 操作 + Thread t1 = new Thread(() -> { + demo.add(); + }); + Thread t2 = new Thread(() -> { + demo.add(); + }); + // 启动两个线程 + t1.start(); + t2.start(); + // 等待两个线程执行结束 + t1.join(); + t2.join(); + System.out.println("count = " + count); + } + } +// 输出: +// count = 156602 +// 实际结果总是会小于预期值 200000 ``` -直觉告诉我们应该是 20000,因为在单线程里调用两次 add10K() 方法,count 的值就是 20000,但实际上 calc() 的执行结果是个 10000 到 20000 之间的随机数。为什么呢? +这段程序的目的是将 count 变量累加到 100000,两个线程执行,则应该累加到 200000,但实际结果总是会小于预期值 200000。 -我们假设线程 A 和线程 B 同时开始执行,那么第一次都会将 count=0 读到各自的 CPU 缓存里,执行完 count+=1 之后,各自 CPU 缓存里的值都是 1,同时写入内存后,我们会发现内存中是 1,而不是我们期望的 2。之后由于各自的 CPU 缓存里都有了 count 的值,两个线程都是基于 CPU 缓存里的 count 值来计算,所以导致最终 count 的值都是小于 20000 的。这就是缓存的可见性问题。 +假设线程 A 和线程 B 同时开始执行,那么第一次都会将 count=0 读到各自的 CPU 缓存里,执行完 count+=1 之后,各自 CPU 缓存里的值都是 1,同时写入内存后,我们会发现内存中是 1,而不是我们期望的 2。之后由于各自的 CPU 缓存里都有了 count 的值,两个线程都是基于 CPU 缓存里的 count 值来计算,所以导致最终 count 的值都是小于 20000 的。这就是缓存的可见性问题。 -循环 10000 次 count+=1 操作如果改为循环 1 亿次,你会发现效果更明显,最终 count 的值接近 1 亿,而不是 2 亿。如果循环 10000 次,count 的值接近 20000,原因是两个线程不是同时启动的,有一个时差。 +@tab 线程安全的计数器 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701110615.png) +【示例】线程安全的计数器示例 ✔ -### 线程切换带来的原子性问题 +针对上面线程不安全的计数器最简单的改造方法就是在 add() 方法上增加 `synchronized` 锁,如下所示: -由于 IO 太慢,早期的操作系统就发明了多进程,操作系统允许某个进程执行一小段时间(称为 **时间片**)。 +```java +@ThreadSafe +public class ThreadSafeCounter { + private synchronized void add() { + int cnt = 0; + while (cnt++ < 100000) { + count += 1; + } + } + // 省略 +} +``` -在一个时间片内,如果一个进程进行一个 IO 操作,例如读个文件,这个时候该进程可以把自己标记为“休眠状态”并出让 CPU 的使用权,待文件读进内存,操作系统会把这个休眠的进程唤醒,唤醒后的进程就有机会重新获得 CPU 的使用权了。 +::: + +### 线程切换带来的原子性问题 -这里的进程在等待 IO 时之所以会释放 CPU 使用权,是为了让 CPU 在这段等待时间里可以做别的事情,这样一来 CPU 的使用率就上来了;此外,如果这时有另外一个进程也读文件,读文件的操作就会排队,磁盘驱动在完成一个进程的读操作后,发现有排队的任务,就会立即启动下一个读操作,这样 IO 的使用率也上来了。 +由于 IO 太慢,早期的操作系统就发明了多进程。CPU 会给各个程序分配一个允许执行时间段,即**时间片**。从表面上看,各程序是同时运行的;实际上, 如果在时间片结束时进程还在运行,则 CPU 将被剥夺并分配给另一个进程。 如果进程在时间片结束前阻塞或结束,则 CPU 当即进行切换(称为“任务切换”)。 -早期的操作系统基于进程来调度 CPU,不同进程间是不共享内存空间的,所以进程要做任务切换就要切换内存映射地址,而一个进程创建的所有线程,都是共享一个内存空间的,所以线程做任务切换成本就很低了。现代的操作系统都基于更轻量的线程来调度,现在我们提到的“任务切换”都是指“线程切换”。 +Java 的并发也是基于任务切换。Java 中,即使是一条语句,也可能需要执行多条 CPU 指令。**一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性**。 -Java 并发程序都是基于多线程的,自然也会涉及到任务切换,也许你想不到,任务切换竟然也是并发编程里诡异 Bug 的源头之一。任务切换的时机大多数是在时间片结束的时候,我们现在基本都使用高级语言编程,高级语言里一条语句往往需要多条 CPU 指令完成,例如上面代码中的 `count += 1`,至少需要三条 CPU 指令。 +CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符。违背直觉的是,高级语言里一条语句往往需要多条 CPU 指令完成,例如上面代码中的`count += 1`,至少需要三条 CPU 指令。 - 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器; -- 指令 2:之后,在寄存器中执行 +1 操作; +- 指令 2:之后,在寄存器中执行+1 操作; - 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。 -操作系统做任务切换,可以发生在任何一条**CPU 指令**执行完,是的,是 CPU 指令,而不是高级语言里的一条语句。对于上面的三条指令来说,我们假设 count=0,如果线程 A 在指令 1 执行完后做线程切换,线程 A 和线程 B 按照下图的序列执行,那么我们会发现两个线程都执行了 count+=1 的操作,但是得到的结果不是我们期望的 2,而是 1。 +因此,执行 `count += 1` 不是原子操作。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701110946.png) - -我们潜意识里面觉得 count+=1 这个操作是一个不可分割的整体,就像一个原子一样,线程的切换可以发生在 count+=1 之前,也可以发生在 count+=1 之后,但就是不会发生在中间。**我们把一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性**。CPU 能保证的原子操作是 CPU 指令级别的,而不是高级语言的操作符,这是违背我们直觉的地方。因此,很多时候我们需要在高级语言层面保证操作的原子性。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409042334004.png) ### 编译优化带来的有序性问题 -那并发编程里还有没有其他有违直觉容易导致诡异 Bug 的技术呢?有的,就是有序性。顾名思义,有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:“a=6;b=7;”编译器优化后可能变成“b=7;a=6;”,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。 +有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序,例如程序中:`a=6; b=7;` 编译器优化后可能变成 `b=7; a=6;`,在这个例子中,编译器调整了语句的顺序,但是不影响程序的最终结果。不过有时候编译器及解释器的优化可能导致意想不到的 Bug。 + +在 Java 领域一个经典的案例就是利用双重检查创建单例对象。 -在 Java 领域一个经典的案例就是利用双重检查创建单例对象,例如下面的代码:在获取实例 getInstance() 的方法中,我们首先判断 instance 是否为空,如果为空,则锁定 Singleton.class 并再次检查 instance 是否为空,如果还为空则创建 Singleton 的一个实例。 +【示例】双重检查创建单例对象 ```java public class Singleton { @@ -299,8 +247,6 @@ public class Singleton { 优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 `instance != null` ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701111050.png) - ### 保证并发安全的思路 #### 互斥同步(阻塞同步) @@ -342,44 +288,69 @@ Java 中的 **无同步方案** 有: ## 活跃性问题 -### 死锁(Deadlock) +程序运行时,当某个操作无法继续执行下去时,就会产生活跃性问题。 -#### 什么是死锁 +对于串行程序,活跃性问题的常见形式是无意中造成的死循环,使得循环之后的代码无法执行。 + +对于并发程序,会有一些其他的活跃性问题,常见形式有: -多个线程互相等待对方释放锁。 +- 死锁 +- 活锁 +- 饥饿 -死锁是当线程进入无限期等待状态时发生的情况,因为所请求的锁被另一个线程持有,而另一个线程又等待第一个线程持有的另一个锁。 +### 死锁(Deadlock) -

- -

+#### 什么是死锁 -#### 避免死锁 +**死锁**:**一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象**。 -(1)按序加锁 +死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。 -当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409050712813.png) -如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 +【示例】存在死锁的示例 + +```java +class Account { + private int balance; + // 转账 + void transfer(Account target, int amt){ + // 锁定转出账户 + synchronized(this) { + // 锁定转入账户 + synchronized(target) { + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } + } + } +} +``` -按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。 +### 如何定位死锁 -(2)超时释放锁 +定位死锁最常见的方式就是利用 jstack 等工具获取线程栈,然后定位互相之间的依赖关系,进而找到死锁。如果是比较明显的死锁,往往 jstack 等就能直接定位,类似 JConsole 甚至可以在图形界面进行有限的死锁检测。 -另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。 +如果我们是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用 Java 提供的标准管理 API,`ThreadMXBean`,其直接就提供了 `findDeadlockedThreads()` 方法用于定位。 -(3)死锁检测 +#### 如何避免死锁 -死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。 +只有以下这四个条件都发生时才会出现死锁: -每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph 等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。 +- **互斥**,共享资源 X 和 Y 只能被一个线程占用; +- **占有且等待**,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; +- **不可抢占**,其他线程不能强行抢占线程 T1 占有的资源; +- **循环等待**,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 -当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。 +**也就是说只要破坏任意一个,就可以避免死锁的发生**。 -如果检测出死锁,有两种处理手段: +其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢? -- 释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。 -- 一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。 +1. 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。 +2. 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。超时释放锁 +3. 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。 ### 活锁(Livelock) @@ -389,9 +360,7 @@ Java 中的 **无同步方案** 有: 想象这样一个例子:两个人在狭窄的走廊里相遇,二者都很礼貌,试图移到旁边让对方先通过。但是他们最终在没有取得任何进展的情况下左右摇摆,因为他们都在同一时间向相同的方向移动。 -

- -

+![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409050740102.png) 如图所示:两个线程想要通过一个 Worker 对象访问共享公共资源的情况,但是当他们看到另一个 Worker(在另一个线程上调用)也是“活动的”时,它们会尝试将该资源交给其他工作者并等待为它完成。如果最初我们让两名工作人员都活跃起来,他们将会面临活锁问题。 @@ -405,13 +374,11 @@ Java 中的 **无同步方案** 有: - 高优先级线程吞噬所有的低优先级线程的 CPU 时间。 - 线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前持续地对该同步块进行访问。 -- 线程在等待一个本身(在其上调用 wait())也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。 +- 线程在等待一个本身(在其上调用 wait()) 也处于永久等待完成的对象,因为其他线程总是被持续地获得唤醒。 -

- -

+![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409050752194.png) -饥饿问题最经典的例子就是哲学家问题。如图所示:有五个哲学家用餐,每个人要获得两把叉子才可以就餐。当 2、4 就餐时,1、3、5 永远无法就餐,只能看着盘中的美食饥饿的等待着。 +饥饿问题最经典的例子就是哲学家问题。如图所示:有五个哲学家用餐,每个人要获得两支筷子才可以就餐。当 2、4 就餐时,1、3、5 永远无法就餐,只能看着盘中的美食饥饿的等待着。 #### 解决饥饿 @@ -431,15 +398,15 @@ Java 不可能实现 100% 的公平性,我们依然可以通过同步结构在 并发执行一定比串行执行快吗?线程越多执行越快吗? -答案是:**并发不一定比串行快**。因为有创建线程和线程上下文切换的开销。 +答案是:**并发不一定比串行快**。因为并发过程中,有创建线程和线程上下文切换的开销。 ### 上下文切换 -#### 什么是上下文切换? +当 CPU 从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等。这个开关被称为**上下文切换(Context Switch)**。 -当 CPU 从执行一个线程切换到执行另一个线程时,CPU 需要保存当前线程的本地数据,程序指针等状态,并加载下一个要执行的线程的本地数据,程序指针等。这个开关被称为“上下文切换”。 +如果频繁地出现上下文切换,将带来极大的开销:恢复执行上下文,丢失局部性,并且 CPU 时间将更多地花在线程调度而不是线程运行上。当线程共享数据时,必须使用同步机制,而这些机制往往会抑制某些编译器优化,使内存缓存区中的数据无效,以及增加共享内存总线的同步流量。所有这些因素都会产生额外的性能开销。 -#### 减少上下文切换的方法 +减少上下文切换的方法: - 无锁并发编程 - 多线程竞争锁时,会引起上下文切换,所以多线程处理数据时,可以用一些办法来避免使用锁,如将数据的 ID 按照 Hash 算法取模分段,不同的线程处理不同段的数据。 - CAS 算法 - Java 的 Atomic 包使用 CAS 算法来更新数据,而不需要加锁。 @@ -448,38 +415,112 @@ Java 不可能实现 100% 的公平性,我们依然可以通过同步结构在 ### 资源限制 -#### 什么是资源限制 +程序的执行速度受限于计算机硬件资源或软件资源。在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行。但是,如果将某段串行的代码并发执行,因为受限于资源仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。 -资源限制是指在进行并发编程时,程序的执行速度受限于计算机硬件资源或软件资源。 +如何解决资源限制的问题呢?在资源受限的情况下,可以根据不同的资源限制调整程序的并发度: -#### 资源限制引发的问题 +- 对于硬件资源限制,可以考虑使用集群并行执行程序。 +- 对于软件资源限制,可以考虑使用资源池将资源复用。 -在并发编程中,将代码执行速度加快的原则是将代码中串行执行的部分变成并发执行,但是如果将某段串行的代码并发执行,因为受限于资源,仍然在串行执行,这时候程序不仅不会加快执行,反而会更慢,因为增加了上下文切换和资源调度的时间。 +## 并发编程 -#### 如何解决资源限制的问题 +并发编程可以抽象成三个核心问题:分工、同步、互斥。 -在资源限制情况下进行并发编程,根据不同的资源限制调整程序的并发度。 +- **分工** - 是指如何高效地拆解任务并分配给线程。 +- **同步** - 是指线程之间如何协作。 +- **互斥** - 是指保证同一时刻只允许一个线程访问共享资源。 -- 对于硬件资源限制,可以考虑使用集群并行执行程序。 -- 对于软件资源限制,可以考虑使用资源池将资源复用。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409042338029.png) + +## J.U.C 简介 + +Java 的 `java.util.concurrent` 包(简称 J.U.C)中提供了大量并发工具类,是 Java 并发能力的主要体现(注意,不是全部,有部分并发能力的支持在其他包中)。从功能上,大致可以分为: + +- **原子类** - 如:`AtomicInteger`、`AtomicIntegerArray`、`AtomicReference`、`AtomicStampedReference` 等。 +- **锁** - 如:`ReentrantLock`、`ReentrantReadWriteLock` 等。 +- **并发容器** - 如:`ConcurrentHashMap`、`CopyOnWriteArrayList`、`CopyOnWriteArraySet` 等。 +- **阻塞队列** - 如:`ArrayBlockingQueue`、`LinkedBlockingQueue` 等。 +- **非阻塞队列** - 如: `ConcurrentLinkedQueue` 、`LinkedTransferQueue` 等。 +- **线程池** - 如:`ThreadPoolExecutor`、`Executors` 等。 + +J.U.C 包中的工具类是基于 `synchronized`、`volatile`、`CAS`、`ThreadLocal` 这样的并发核心机制打造的。所以,要想深入理解 J.U.C 工具类的特性、为什么具有这样那样的特性,就必须先理解这些核心机制。 + +## 并发术语 + +并发编程中有很多术语概念相近,容易让人混淆。本节内容通过对比分析,力求让读者清晰理解其概念以及差异。 -## 小结 +### 串行、并行、并发 -并发编程可以总结为三个核心问题:分工、同步、互斥。 +并发和并行是最容易让新手费解的概念,那么如何理解二者呢?其最关键的差异在于:是否是**同时**发生: + +- **串行** - 是指**任务按照顺序依次执行**,每个任务在前一个任务完成后才能开始执行。 +- **并行** - 是指**具备同时处理多个任务的能力**。 +- **并发** - 是指**具备处理多个任务的能力,但不一定要同时**。 + +> 下面是我见过最生动的说明,摘自 [并发与并行的区别是什么?——知乎的高票答案](https://www.zhihu.com/question/33515481/answer/58849148): +> +> - 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 +> - 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 +> - 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 + +### 同步和异步 + +- **同步** - 是指在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。 +- **异步** - 则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。 + +> 举例来说明: +> +> - 同步就像是打电话:不挂电话,通话不会结束。 +> - 异步就像是发短信:发完短信后,就可以做其他事;当收到回复短信时,手机会通过铃声或振动来提醒。 + +### 阻塞和非阻塞 + +阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态: + +- **阻塞** - 是指**调用结果返回之前,当前线程会被挂起**。调用线程只有在得到结果之后才会返回。 +- **非阻塞** - 是指在不能立刻得到结果之前,该**调用不会阻塞当前线程**。 + +> 举例来说明: +> +> - 阻塞调用就像是打电话,通话不结束,不能放下。 +> - 非阻塞调用就像是发短信,发完短信后,就可以做其他事,短信来了,手机会提醒。 + +### 进程、线程、管程、协程 + +- **进程(Process)** - 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。**进程可视为一个正在运行的程序**。 +- **线程(Thread)** - **线程是操作系统进行调度的基本单位**。 +- **管程(Monitor)** - **管程是指管理共享变量以及对共享变量的操作过程,让他们支持并发**。 + - Java 通过 synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法来实现管程技术。 + - **管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程**。 +- **协程(Coroutine)** - **协程可以理解为一种轻量级的线程**。 + - 从操作系统的角度来看,线程是在内核态中调度的,而协程是在用户态调度的,所以相对于线程来说,协程切换的成本更低。 + - 协程虽然也有自己的栈,但是相比线程栈要小得多,典型的线程栈大小差不多有 1M,而协程栈的大小往往只有几 K 或者几十 K。所以,无论是从时间维度还是空间维度来看,协程都比线程轻量得多。 + - Go、Python、Lua、Kotlin 等语言都支持协程;Java OpenSDK 中的 Loom 项目目标就是支持协程。 + +进程和线程的差异: + +- 一个程序至少有一个进程,一个进程至少有一个线程。 +- 线程比进程划分更细,所以执行开销更小,并发性更高 +- 进程是一个实体,拥有独立的资源;而同一个进程中的多个线程共享进程的资源。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/processes-vs-threads.jpg) + +JVM 在单个进程中运行,JVM 中的线程共享属于该进程的堆。这就是为什么几个线程可以访问同一个对象。线程共享堆并拥有自己的堆栈空间。这是一个线程如何调用一个方法以及它的局部变量是如何保持线程安全的。但是堆不是线程安全的并且为了线程安全必须进行同步。 + +### 竞态条件和临界区 -- **分工**:是指如何高效地拆解任务并分配给线程。 -- **同步**:是指线程之间如何协作。 -- **互斥**:是指保证同一时刻只允许一个线程访问共享资源。 +- **竞态条件(Race Condition)** - 程序的执行结果依赖多线程执行的顺序。通俗的说,即**多个线程竞争访问同一个资源**。 +- **临界区(Critical Sections)** - 指的是**访问共享资源的程序片段**。 ## 参考资料 - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) - http://tutorials.jenkov.com/java-concurrency/benefits.html - https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/thread-deadlock.html - https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/thread-livelock.html - https://www.logicbig.com/tutorials/core-java-tutorial/java-multi-threading/thread-starvation.html - https://www.zhihu.com/question/33515481 -- https://blog.csdn.net/yaosiming2011/article/details/44280797 \ No newline at end of file +- https://blog.csdn.net/yaosiming2011/article/details/44280797 diff --git "a/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/README.md" "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/README.md" new file mode 100644 index 0000000000..f6669a584b --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/05.\345\271\266\345\217\221/README.md" @@ -0,0 +1,52 @@ +--- +title: Java 并发 +date: 2020-06-04 13:51:01 +categories: + - Java + - JavaCore + - 并发 +tags: + - Java + - JavaCore + - 并发 +permalink: /pages/6615822b/ +hidden: true +index: false +dir: + order: 5 + link: true +--- + +# Java 并发 + +> Java 并发总结、整理 Java 并发编程相关知识点。 +> +> 并发编程并非 Java 语言所独有,而是一种成熟的编程范式,Java 只是用自己的方式实现了并发工作模型。学习 Java 并发编程,应该先熟悉并发的基本概念,然后进一步了解并发的特性以及其特性所面临的问题。掌握了这些,当学习 Java 并发工具时,才会明白它们各自是为了解决什么问题,为什么要这样设计。通过这样由点到面的学习方式,更容易融会贯通,将并发知识形成体系化。 + +## 📖 内容 + +- [Java 并发简介](Java并发简介.md) - 关键词:并发、线程、安全性、活跃性、性能、死锁、活锁 +- [Java 并发之内存模型](Java并发之内存模型.md) - 关键词:JMM、Happens-Before、内存屏障、volatile、synchronized、final、指令重排序 +- [Java 并发之线程](Java并发之线程.md) - 关键词:Thread、Runnable、Callable、Future、FutureTask、线程生命周期 +- [Java 并发之锁](Java并发之锁.md) - 关键词:锁、Lock、Condition、ReentrantLock、ReentrantReadWriteLock、StampedLock +- [Java 并发之无锁](Java并发之无锁.md) - 关键词:CAS、ThreadLocal、Immutability、Copy-on-Write +- [Java 并发之 AQS](Java并发之AQS.md) - 关键词:AQS、独占锁、共享锁 +- [Java 并发之容器](Java并发之容器.md) - 关键词:ConcurrentHashMap、CopyOnWriteArrayList +- [Java 并发之线程池](Java并发之线程池.md) - 关键词:Executor、ExecutorService、ThreadPoolExecutor、Executors +- [Java 并发之同步工具](Java并发之同步工具.md) - 关键词:Semaphore、CountDownLatch、CyclicBarrier +- [Java 并发之分工工具](Java并发之分工工具.md) - 关键词:FutureTask、CompletableFuture、CompletionStage、CompletionService、ForkJoinPool + +## 📚 资料 + +- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) +- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) +- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) +- [《Effective Java》](https://book.douban.com/subject/30412517/) +- [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) + +## 🚪 传送 + +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore/) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\206\205\345\255\230\345\214\272\345\237\237.md" new file mode 100644 index 0000000000..bb9b66ad96 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\206\205\345\255\230\345\214\272\345\237\237.md" @@ -0,0 +1,851 @@ +--- +title: Java 虚拟机之内存区域 +date: 2020-06-28 16:19:00 +categories: + - Java + - JavaCore + - JVM +tags: + - Java + - JavaCore + - JVM +permalink: /pages/682c2df6/ +--- + +# Java 虚拟机之内存区域 + +## 运行时数据区域 + +JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408130820873.png) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408130821056.png) + +### 程序计数器 + +**程序计数器(Program Counter Register)** 是一块较小的内存空间,它可以看做是**当前线程所执行的字节码的行号指示器**。字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。 + +由于 Java 虚拟机的多线程是通过线程轮流切换、分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。 + +如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地(Native)方法,这个计数器值则应为空(Undefined)。 + +> 🔔 注意:程序计数器是 JVM 中没有规定任何 `OutOfMemoryError` 情况的唯一区域。 + +### Java 虚拟机栈 + +**Java 虚拟机栈(Java Virtual Machine Stacks)** 也是线程私有的,它的生命周期与线程相同。**Java 虚拟机栈以方法作为最基本的执行单元,描述的是 Java 方法执行的线程内存模型**。**每个方法被执行的时候,JVM 都会同步创建一个栈帧(Stack Frame),栈帧是用于支持虚拟机进行方法调用和方法执行背后的数据结构。栈帧存储了局部变量表、操作数栈、动态连接、方法返回地址等信息**。每一个方法从调用开始至执行结束的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。 + +一个线程中的方法调用链可能会很长,以 Java 程序的角度来看,同一时刻、同一条线程里面,在 调用堆栈的所有方法都同时处于执行状态。而对于执行引擎来讲,在活动线程中,只有位于栈顶的方 法才是在运行的,只有位于栈顶的栈帧才是生效的,其被称为“当前栈帧”(Current Stack Frame),与 这个栈帧所关联的方法被称为“当前方法”(Current Method)。执行引擎所运行的所有字节码指令都只 针对当前栈帧进行操作。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408130821241.png) + +- **局部变量表** - 用于存放方法参数和方法内部定义的局部变量。 +- **操作数栈** - 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。 +- **动态连接** - 用于一个方法调用其他方法的场景。Class 文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为**静态解析**;另一部分将在每一次的运行期间转化为直接应用,这部分称为**动态连接**。 +- **方法返回地址** - 用于返回方法被调用的位置,恢复上层方法的局部变量和操作数栈。Java 方法有两种返回方式,一种是 `return` 语句正常返回,一种是抛出异常。无论采用何种退出方式,都会导致栈帧被弹出。也就是说,栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。 + +> 🔔 注意: +> +> 该区域可能抛出以下异常: +> +> - 如果线程请求的栈深度大于虚拟机所允许的深度,将抛出 `StackOverflowError` 异常; +> - 如果虚拟机栈进行动态扩展时,无法申请到足够内存,就会抛出 `OutOfMemoryError` 异常。 +> +> 💡 提示: +> +> 可以通过 `-Xss` 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小: +> +> ```java +> java -Xss=512M HackTheJava +> ``` + +#### 局部变量表 + +**局部变量表(Local Variables Table)**是一组变量值的存储空间,**用于存放方法参数和方法内部定义的局部变量**。在 Java 程序被编译为 Class 文件时,就在方法的 Code 属性的 max_locals 数据项中确定了该方法所需分配的局部变量表的最大容量。 + +局部变量表存放了编译期可知的各种 Java 虚拟机基本数据类型、对象引用(`reference` 类型,它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)和 `returnAddress` 类型(指向了一条字节码指令的地址)。这些数据类型在局部变量表中的存储空间以局部变量槽(Variable Slot)来表示,其中 64 位长度的 `long` 和 `double` 类型的数据会占用两个变量槽,其余的数据类型只占用一个。局部变量表所需的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在栈帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。 + +#### 操作数栈 + +**操作数栈(Operand Stack)**也常被称为操作栈,它是一个后入先出(Last In First Out,LIFO) 栈。**操作数栈主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果**。另外,计算过程中产生的临时变量也会放在操作数栈中。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408130822600.png) + +当一个方法刚刚开始执行的时候,这个方法的操作数栈是空的,在方法的执行过程中,会有各种字节码指令往操作数栈中写入和提取内容,也就是出栈和入栈操作。譬如在做算术运算的时候是通过将运算涉及的操作数栈压入栈顶后调用运算指令来进行的,又譬如在调用其他方法的时候是通过操作数栈来进行方法参数的传递。 + +操作数栈中元素的数据类型必须与字节码指令的序列严格匹配,在编译程序代码的时候,编译器 必须要严格保证这一点,在类校验阶段的数据流分析中还要再次验证这一点。 + +另外在概念模型中,两个不同栈帧作为不同方法的虚拟机栈的元素,是完全相互独立的。但是在大多虚拟机的实现里都会进行一些优化处理,令两个栈帧出现一部分重叠。让下面栈帧的部分操作数栈与上面栈帧的部分局部变量表重叠在一起,这样做不仅节约了一些空间,更重要的是在进行方法调用时就可以直接共用一部分数据,无须进行额外的参数复制传递了。 + +#### 动态连接 + +用于一个方法调用其他方法的场景。Class 文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为**静态解析**;另一部分将在每一次的运行期间转化为直接应用,这部分称为**动态连接**。 + +每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方 法调用过程中的**动态连接(Dynamic Linking)**。通过第 6 章的讲解,我们知道 Class 文件的常量池中存 有大量的符号引用,字节码中的方法调用指令就以常量池里指向方法的符号引用作为参数。这些符号 引用一部分会在类加载阶段或者第一次使用的时候就被转化为直接引用,这种转化被称为静态解析。 另外一部分将在每一次运行期间都转化为直接引用,这部分就称为动态连接。关于这两个转化过程的 具体过程,将在 8.3 节中再详细讲解。 + +#### 方法返回地址 + +方法返回地址用于返回方法被调用的位置,恢复上层方法的局部变量和操作数栈。 + +Java 方法有两种返回方式,一种是 `return` 语句正常返回,这时候可能会有返回值传递给上层的方法调用者,方法是否有返回值以及返回值的类型将根据遇到何种方法返回指令来决定;一种是遇到了异常,并且这个异常没有在方法体内得到妥善处理。无论采用何种退出方式,都会导致栈帧被弹出。也就是说,栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。 + +方法返回时可能需要在栈帧中保存一些信息,用来帮助恢复它的上层主调方法的执行状态。 一般来说,方法正常退出时,主调方法的 PC 计数器的值就可以作为返回地址,栈帧中很可能会保存这个计数器值。而方法异常退出时,返回地址是要通过异常处理器表来确定的,栈帧中就一般不会保存这部分信息。方法退出的过程实际上等同于把当前栈帧出栈,因此退出时可能执行的操作有:恢复上层方法的局部变量表和操作数栈,把返回值(如果有的话)压入调用者栈帧的操作数栈中,调整 PC 计数器的值以指向方法调用指令后面的一条指令等。 + +### 本地方法栈 + +**本地方法栈(Native Method Stack)** 与虚拟机栈的作用非常相似,二者区别仅在于:**虚拟机栈为 Java 方法服务;本地方法栈为 Native 方法服务**。 + +> 🔔 注意:本地方法栈也会在栈深度溢出或者栈扩展失败时分别抛出 `StackOverflowError` 和 `OutOfMemoryError` 异常。 + +### Java 堆 + +**Java 堆(Java Heap) 的作用就是存放对象实例,几乎所有的对象实例都是在这里分配内存**。 + +> 注:由于即时编译技术的进步,尤其是逃逸分析技术的日渐强大,栈上分配、标量替换优化手段已经导致一些微妙的变化悄然发生,所以说 Java 对象实例都分配在堆上也渐渐变得不是那么绝对了。 + +Java 堆是垃圾收集器管理的内存区域(因此也被叫做"GC 堆")。现代的垃圾收集器大部分都是采用**分代收集理论**设计的,该力量的思想是针对不同的对象采取不同的垃圾回收算法。 + +在 JDK 7 及之前版本,堆内存被通常分为下面三部分: + +- **`新生代(Young Generation)`** +- **`老年代(Old Generation)`** +- **`永久代(Permanent Generation)`** + +**JDK 8 版本之后 PermGen(永久代) 已被 Metaspace(元空间) 取代,元空间使用的是本地内存**。 + +大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 S0 或者 S1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。不过,设置的值应该在 0-15,否则会爆出以下错误: + +```bash +MaxTenuringThreshold of 20 is invalid; must be between 0 and 15 +``` + +**为什么年龄只能是 0-15?** + +因为记录年龄的区域在对象头中,这个区域的大小通常是 4 位。这 4 位可以表示的最大二进制数字是 1111,即十进制的 15。因此,对象的年龄被限制为 0 到 15。 + +这里我们简单结合对象布局来详细介绍一下。 + +在 HotSpot 虚拟机中,对象在内存中存储的布局可以分为 3 块区域:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。其中,对象头包括两部分:标记字段(Mark Word)和类型指针(Klass Word)。关于对象内存布局的详细介绍,后文会介绍到,这里就不重复提了。 + +这个年龄信息就是在标记字段中存放的(标记字段还存放了对象自身的其他信息比如哈希码、锁状态信息等等)。 + +如果从分配内存的角度看,所有线程共享的 Java 堆中可以划分出多个线程私有的分配缓冲区 (Thread Local Allocation Buffer,TLAB),以提升对象分配效率。不过无论从什么角度,无论如何划分,都不会改变 Java 堆中存储内容的共性,无论是哪个区域,存储的都只能是对象的实例,将 Java 堆细分的目的只是为了更好地回收内存,或者更快地分配内存。 + +> 🔔 注意:Java 堆既可以被实现成固定大小的,也可以是可扩展的,不过当前主流的 Java 虚拟机都是按照可扩展来实现的,扩展失败会抛出 `OutOfMemoryError` 异常。 +> +> 可以通过 `-Xms` 和 `-Xmx` 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 +> +> ```java +> java -Xms=1M -Xmx=2M HackTheJava +> ``` + +### 方法区 + +方法区(Method Area)是各个线程共享的内存区域。**方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据**。 + +在 JDK8 以前,方法区常被称为**永久代**,但这种说法是不准确的:仅仅是因为当时的 HotSpot 虚拟机使用永久代来实现方法区而已。对于其他虚拟机而言,是不存在永久代概念的。**永久代这种设计,导致了 Java 应用更容易遇到内存溢出的问题**(永久代有 `-XX:MaxPermSize` 的上限,即使不设置也有默认大小)。 + +- JDK7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收,可通过参数 `-XX:PermSize` 和 `-XX:MaxPermSize` 设置。 +- JDK8 之后,取消了永久代,用 **`metaspace(元空间)`**替代,可通过参数 `-XX:MaxMetaspaceSize` 设置。 + +方法区的内存回收目标主要是针对常量池的回收和对类型的卸载,一般来说这个区域的回收效果比较难令人满意,尤其是类型的卸载,条件相当苛刻。 + +> 🔔 注意:方法区和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 `OutOfMemoryError` 异常。 + +### 运行时常量池 + +**`运行时常量池(Runtime Constant Pool)` 是方法区的一部分**,Class 文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池表(Constant Pool Table),**用于存放编译器生成的各种字面量和符号引用**,这部分内容将在类加载后写入。 + +- **字面量** - 文本字符串、声明为 `final` 的常量值等。 +- **符号引用** - 类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。 + +运行时常量池相对于 Class 文件常量池的另外一个重要特征是具备动态性,Java 语言并不要求常量 一定只有编译期才能产生,也就是说,并非预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可以将新的常量放入池中,这种特性被开发人员利用得比较多的便是 `String` 类的 `intern()` 方法。 + +> 🔔 注意:当常量池无法再申请到内存时会抛出 `OutOfMemoryError` 异常。 + +### 直接内存 + +直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。 + +JDK4 中新加入了 NIO,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 `DirectByteBuffer` 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 + +直接内存容量可通过 `-XX:MaxDirectMemorySize` 指定,如果不指定,则默认与 Java 堆最大值(`-Xmx` 指定)一样。 + +> 🔔 注意:直接内存这部分也被频繁的使用,且也可能导致 `OutOfMemoryError` 异常。 + +### Java 内存区域对比 + +| 内存区域 | 内存作用范围 | 异常 | +| ------------- | -------------- | ------------------------------------------ | +| 程序计数器 | 线程私有 | 无 | +| Java 虚拟机栈 | 线程私有 | `StackOverflowError` 和 `OutOfMemoryError` | +| 本地方法栈 | 线程私有 | `StackOverflowError` 和 `OutOfMemoryError` | +| Java 堆 | 线程共享 | `OutOfMemoryError` | +| 方法区 | 线程共享 | `OutOfMemoryError` | +| 运行时常量池 | 线程共享 | `OutOfMemoryError` | +| 直接内存 | 非运行时数据区 | `OutOfMemoryError` | + +## 虚拟机对象 + +### 对象的创建 + +当 Java 虚拟机**遇到一条字节码 `new` 指令时,首先在常量池中尝试定位类的符号引用,并检查这个类是否已被类加载,如果没有,则必须先执行相应的类加载过程**。 + +在类加载检查通过后,**接下来虚拟机将为新生对象分配内存**。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务实际上便等同于把一块确定大小的内存块从 Java 堆中划分出来。分配对象内存有两种方式: + +**指针碰撞(Bump The Pointer)** - 如果 Java 堆中**内存是规整的**,所有被使用过的内存都被放在一 边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间方向挪动一段与对象大小相等的距离。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408140753480.png) + +**空闲列表(Free List)** - 如果 Java 堆中的**内存是不规整的**,已被使用的内存和空闲的内存相互交错在一起,那就没有办法简单地进行指针碰撞了,虚拟机就必须维护一个列表,记录上哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408140753926.png) + +选择哪种分配方式由 Java 堆是否规整决定,而 Java 堆是否规整又由所采用的垃圾收集器是否采用**标记-压缩算法**决定。因此,当使用 Serial、ParNew 等带压缩整理过程的收集器时,系统采用的分配算法是指针碰撞,既简单又高效;而当使用 CMS 这种基于清除 (Sweep)算法的收集器时,理论上就只能采用较为复杂的空闲列表来分配内存。 + +对象创建在虚拟机中是非常频繁的行为,因此还需要考虑分配内存空间的并发安全问题。一般有两种方案: + +- CAS 同步 - 对分配内存空间的动作进行同步处理——实际上虚拟机是采用 CAS 配上失败 重试的方式保证更新操作的原子性; +- TLAB - 另外一种是把内存分配的动作按照线程划分在不同的空间之中进行,即每个线程在 Java 堆中预先分配一小块内存,称为**本地线程分配缓冲(Thread Local Allocation Buffer,TLAB)**,哪个线程要分配内存,就在哪个线程的本地缓冲区中分配,只有本地缓冲区用完了,分配新的缓存区时才需要同步锁定。 + +接下来,需要执行类的构造函数(即 `()` 方法)对对象进行初始化。 + +### 对象的内存布局 + +在 HotSpot 虚拟机里,对象在堆内存中的存储布局可以划分为三个部分: + +- **对象头(Header)** - HotSpot 虚拟机对象的对象头部分包括两类信息。 + - **Mark Word** - **用于存储对象自身的运行时数据**。如哈 希码(HashCode)、GC 分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等,这部分数据的长度在 32 位和 64 位的虚拟机(未开启压缩指针)中分别为 32 个比特和 64 个比特。 + - **类型指针** - **对象指向它的类型元数据的指针**,Java 虚拟机通过这个指针来确定该对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说,查找对象的元数据信息并不一定要经过对象本身。此外,如果对象是一个 Java 数组,那在对象头中还必须有一块用于记录数组长度的数据,因为虚拟机可以通过普通 Java 对象的元数据信息确定 Java 对象的大小,但是如果数组的长度是不确定的,将无法通过元数据中的信息推断出数组的大小。 +- **实例数据(Instance Data)** - **对象真正存储的有效信息**,即我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的字段都必须记录起来。 +- **对齐填充(Padding)** - 并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。 + +### 对象的访问定位 + +Java 程序会通过栈上的 reference 数据来操作堆上的具体对象。主流的对象访问方式主要有使用句柄和直接指针两种:使用句柄访问和使用直接指针访问。 + +#### 使用句柄访问 + +Java 堆中将可能会划分出一块内存来作为句柄池,reference 中存储的就是对象的句柄地址,而句柄中包含了对象实例数据与类型数据各自具体的地址信息。使用句柄来访问的最大好处就是 reference 中存储的是稳定句柄地 址,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时只会改变句柄中的实例数据指针,而 reference 本身不需要被修改。 + +#### 使用直接指针访问 + +Java 堆中对象的内存布局就必须考虑如何放置访问类型数据的相关信息,reference 中存储的直接就是对象地址,如果只是访问对象本身的话,就不需要多一次间接访问的开销。使用直接指针来访问最大的好处就是速度更快,它节省了一次指针定位的时间开销。HotSpot 主要使用第二种方式进行对象访问。 + +## 内存分配 + +```java +public class JVMCase { + + // 常量 + public final static String MAN_SEX_TYPE = "man"; + + // 静态变量 + public static String WOMAN_SEX_TYPE = "woman"; + + public static void main(String[] args) { + + Student stu = new Student(); + stu.setName("nick"); + stu.setSexType(MAN_SEX_TYPE); + stu.setAge(20); + + JVMCase jvmcase = new JVMCase(); + + // 调用静态方法 + print(stu); + // 调用非静态方法 + jvmcase.sayHello(stu); + } + + // 常规静态方法 + public static void print(Student stu) { + System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge()); + } + + // 非静态方法 + public void sayHello(Student stu) { + System.out.println(stu.getName() + "say: hello"); + } +} + +class Student{ + String name; + String sexType; + int age; + + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + + public String getSexType() { + return sexType; + } + public void setSexType(String sexType) { + this.sexType = sexType; + } + public int getAge() { + return age; + } + public void setAge(int age) { + this.age = age; + } +} +``` + +运行以上代码时,JVM 处理过程如下: + +(1)JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。 + +(2)JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。 + +(3)class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值(这部分我在第 21 讲还会详细介绍)。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094250.png) + +(4)完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器 `` 方法,编译器会在 `.java` 文件被编译成 `.class` 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 `()` 方法。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094329.png) + +(5)执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 student 对象,对象引用 student 就存放在栈中。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094651.png) + +(6)此时再次创建一个 JVMCase 对象,调用 sayHello 非静态方法,sayHello 方法属于对象 JVMCase,此时 sayHello 方法入栈,并通过栈中的 student 引用调用堆中的 Student 对象;之后,调用静态方法 print,print 静态方法属于 JVMCase 类,是从静态方法中获取,之后放入到栈中,也是通过 student 引用调用堆中的 student 对象。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094714.png) + +## 内存溢出 + +### OutOfMemoryError + +`OutOfMemoryError` 简称为 OOM。Java 中对 OOM 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。通俗的解释是:JVM 内存不足了。 + +在 JVM 规范中,**除了程序计数器区域外,其他运行时区域都可能发生 `OutOfMemoryError` 异常(简称 OOM)**。 + +下面逐一介绍 OOM 发生场景。 + +#### 堆空间溢出 + +**java.lang.OutOfMemoryError: Java heap space 意味着:堆空间溢出**。 + +更细致的说法是:Java 堆内存已经达到 `-Xmx` 设置的最大值。Java 堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾收集器回收这些对象,那么当堆空间到达最大容量限制后就会产生 OOM。 + +堆空间溢出有可能是**内存泄漏(Memory Leak)** 或 **内存溢出(Memory Overflow)** 。 + +##### Java heap space 分析步骤 + +1. 使用 `jmap` 或 `-XX:+HeapDumpOnOutOfMemoryError` 获取堆快照。 +2. 使用内存分析工具(VisualVM、MAT、JProfile 等)对堆快照文件进行分析。 +3. 根据分析图,重点是确认内存中的对象是否是必要的,分清究竟是是内存泄漏还是内存溢出。 + +##### 内存泄漏 + +**内存泄漏(Memory Leak)是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况**。 + +内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏随着被执行的次数不断增加,最终会导致内存溢出。 + +内存泄漏常见场景: + +- 静态容器 + - 声明为静态(`static`)的 `HashMap`、`Vector` 等集合 + - 通俗来讲 A 中有 B,当前只把 B 设置为空,A 没有设置为空,回收时 B 无法回收。因为被 A 引用。 +- 监听器 + - 监听器被注册后释放对象时没有删除监听器 +- 物理连接 + - 各种连接池建立了连接,必须通过 `close()` 关闭链接 +- 内部类和外部模块等的引用 + - 发现它的方式同内存溢出,可再加个实时观察 + - `jstat -gcutil 7362 2500 70` + +重点关注: + +- `FGC` — 从应用程序启动到采样时发生 Full GC 的次数。 +- `FGCT` — 从应用程序启动到采样时 Full GC 所用的时间(单位秒)。 +- `FGC` 次数越多,`FGCT` 所需时间越多,越有可能存在内存泄漏。 + +如果是内存泄漏,可以进一步查看泄漏对象到 GC Roots 的对象引用链。这样就能找到泄漏对象是怎样与 GC Roots 关联并导致 GC 无法回收它们的。掌握了这些原因,就可以较准确的定位出引起内存泄漏的代码。 + +导致内存泄漏的常见原因是使用容器,且不断向容器中添加元素,但没有清理,导致容器内存不断膨胀。 + +【示例】 + +```java +/** + * 内存泄漏示例 + * 错误现象:java.lang.OutOfMemoryError: Java heap space + * VM Args:-verbose:gc -Xms10M -Xmx10M -XX:+HeapDumpOnOutOfMemoryError + */ +public class HeapMemoryLeakOOM { + + public static void main(String[] args) { + List list = new ArrayList<>(); + while (true) { + list.add(new OomObject()); + } + } + + static class OomObject {} + +} +``` + +##### 内存溢出 + +如果不存在内存泄漏,即内存中的对象确实都必须存活着,则应当检查虚拟机的堆参数(`-Xmx` 和 `-Xms`),与机器物理内存进行对比,看看是否可以调大。并从代码上检查是否存在某些对象生命周期过长、持有时间过长的情况,尝试减少程序运行期的内存消耗。 + +【示例】 + +```java +/** + * 堆溢出示例 + *

+ * 错误现象:java.lang.OutOfMemoryError: Java heap space + *

+ * VM Args:-verbose:gc -Xms10M -Xmx10M + */ +public class HeapOutOfMemoryOOM { + + public static void main(String[] args) { + Double[] array = new Double[999999999]; + System.out.println("array length = [" + array.length + "]"); + } + +} +``` + +上面的例子是一个极端的例子,试图创建一个维度很大的数组,堆内存无法分配这么大的内存,从而报错:`Java heap space`。 + +但如果在现实中,代码并没有问题,仅仅是因为堆内存不足,可以通过 `-Xms` 和 `-Xmx` 适当调整堆内存大小。 + +#### GC 开销超过限制 + +`java.lang.OutOfMemoryError: GC overhead limit exceeded` 这个错误,官方给出的定义是:**超过 `98%` 的时间用来做 GC 并且回收了不到 `2%` 的堆内存时会抛出此异常**。这意味着,发生在 GC 占用大量时间为释放很小空间的时候发生的,是一种保护机制。导致异常的原因:一般是因为堆太小,没有足够的内存。 + +【示例】 + +```java +/** + * GC overhead limit exceeded 示例 + * 错误现象:java.lang.OutOfMemoryError: GC overhead limit exceeded + * 发生在 GC 占用大量时间为释放很小空间的时候发生的,是一种保护机制。导致异常的原因:一般是因为堆太小,没有足够的内存。 + * 官方对此的定义:超过 98%的时间用来做 GC 并且回收了不到 2%的堆内存时会抛出此异常。 + * VM Args: -Xms10M -Xmx10M + */ +public class GcOverheadLimitExceededOOM { + + public static void main(String[] args) { + List list = new ArrayList<>(); + double d = 0.0; + while (true) { + list.add(d++); + } + } + +} +``` + +【处理】 + +与 **Java heap space** 错误处理方法类似,先判断是否存在内存泄漏。如果有,则修正代码;如果没有,则通过 `-Xms` 和 `-Xmx` 适当调整堆内存大小。 + +#### 永久代空间不足 + +【错误】 + +``` +java.lang.OutOfMemoryError: PermGen space +``` + +【原因】 + +Perm (永久代)空间主要用于存放 `Class` 和 Meta 信息,包括类的名称和字段,带有方法字节码的方法,常量池信息,与类关联的对象数组和类型数组以及即时编译器优化。GC 在主程序运行期间不会对永久代空间进行清理,默认是 64M 大小。 + +根据上面的定义,可以得出 **PermGen 大小要求取决于加载的类的数量以及此类声明的大小**。因此,可以说造成该错误的主要原因是永久代中装入了太多的类或太大的类。 + +在 JDK8 之前的版本中,可以通过 `-XX:PermSize` 和 `-XX:MaxPermSize` 设置永久代空间大小,从而限制方法区大小,并间接限制其中常量池的容量。 + +##### 初始化时永久代空间不足 + +【示例】 + +```java +/** + * 永久代内存空间不足示例 + *

+ * 错误现象: + *

    + *
  • java.lang.OutOfMemoryError: PermGen space (JDK8 以前版本)
  • + *
  • java.lang.OutOfMemoryError: Metaspace (JDK8 及以后版本)
  • + *
+ * VM Args: + *
    + *
  • -Xmx100M -XX:MaxPermSize=16M (JDK8 以前版本)
  • + *
  • -Xmx100M -XX:MaxMetaspaceSize=16M (JDK8 及以后版本)
  • + *
+ */ +public class PermGenSpaceOOM { + + public static void main(String[] args) throws Exception { + for (int i = 0; i < 100_000_000; i++) { + generate("eu.plumbr.demo.Generated" + i); + } + } + + public static Class generate(String name) throws Exception { + ClassPool pool = ClassPool.getDefault(); + return pool.makeClass(name).toClass(); + } + +} +``` + +在此示例中,源代码遍历循环并在运行时生成类。javassist 库正在处理类生成的复杂性。 + +##### 重部署时永久代空间不足 + +对于更复杂,更实际的示例,让我们逐步介绍一下在应用程序重新部署期间发生的 PermGen 空间错误。重新部署应用程序时,你希望垃圾回收会摆脱引用所有先前加载的类的加载器,并被加载新类的类加载器取代。 + +不幸的是,许多第三方库以及对线程,JDBC 驱动程序或文件系统句柄等资源的不良处理使得无法卸载以前使用的类加载器。反过来,这意味着在每次重新部署期间,所有先前版本的类仍将驻留在 PermGen 中,从而在每次重新部署期间生成数十兆的垃圾。 + +让我们想象一个使用 JDBC 驱动程序连接到关系数据库的示例应用程序。启动应用程序时,初始化代码将加载 JDBC 驱动程序以连接到数据库。对应于规范,JDBC 驱动程序向 `java.sql.DriverManager` 进行注册。该注册包括将对驱动程序实例的引用存储在 `DriverManager` 的静态字段中。 + +现在,当从应用程序服务器取消部署应用程序时,`java.sql.DriverManager` 仍将保留该引用。我们最终获得了对驱动程序类的实时引用,而驱动程序类又保留了用于加载应用程序的 `java.lang.Classloader` 实例的引用。反过来,这意味着垃圾回收算法无法回收空间。 + +而且该 `java.lang.ClassLoader` 实例仍引用应用程序的所有类,通常在 PermGen 中占据数十兆字节。这意味着只需少量重新部署即可填充通常大小的 PermGen。 + +##### PermGen space 解决方案 + +(1)解决初始化时的 `OutOfMemoryError` + +在应用程序启动期间触发由于 PermGen 耗尽导致的 `OutOfMemoryError` 时,解决方案很简单。该应用程序仅需要更多空间才能将所有类加载到 PermGen 区域,因此我们只需要增加其大小即可。为此,更改你的应用程序启动配置并添加(或增加,如果存在)`-XX:MaxPermSize` 参数,类似于以下示例: + +``` +java -XX:MaxPermSize=512m com.yourcompany.YourClass +``` + +上面的配置将告诉 JVM,PermGen 可以增长到 512MB。 + +清理应用程序中 `WEB-INF/lib` 下的 jar,用不上的 jar 删除掉,多个应用公共的 jar 移动到 Tomcat 的 lib 目录,减少重复加载。 + +🔔 注意:`-XX:PermSize` 一般设为 64M + +(2)解决重新部署时的 `OutOfMemoryError` + +重新部署应用程序后立即发生 `OutOfMemoryError` 时,应用程序会遭受类加载器泄漏的困扰。在这种情况下,解决问题的最简单,继续进行堆转储分析–使用类似于以下命令的重新部署后进行堆转储: + +``` +jmap -dump:format=b,file=dump.hprof +``` + +然后使用你最喜欢的堆转储分析器打开转储(Eclipse MAT 是一个很好的工具)。在分析器中可以查找重复的类,尤其是那些正在加载应用程序类的类。从那里,你需要进行所有类加载器的查找,以找到当前活动的类加载器。 + +对于非活动类加载器,你需要通过从非活动类加载器收集到 GC 根的最短路径来确定阻止它们被垃圾收集的引用。有了此信息,你将找到根本原因。如果根本原因是在第三方库中,则可以进入 Google/StackOverflow 查看是否是已知问题以获取补丁/解决方法。 + +(3)解决运行时 `OutOfMemoryError` + +第一步是检查是否允许 GC 从 PermGen 卸载类。在这方面,标准的 JVM 相当保守-类是天生的。因此,一旦加载,即使没有代码在使用它们,类也会保留在内存中。当应用程序动态创建许多类并且长时间不需要生成的类时,这可能会成为问题。在这种情况下,允许 JVM 卸载类定义可能会有所帮助。这可以通过在启动脚本中仅添加一个配置参数来实现: + +``` +-XX:+CMSClassUnloadingEnabled +``` + +默认情况下,此选项设置为 false,因此要启用此功能,你需要在 Java 选项中显式设置。如果启用 CMSClassUnloadingEnabled,GC 也会扫描 PermGen 并删除不再使用的类。请记住,只有同时使用 UseConcMarkSweepGC 时此选项才起作用。 + +``` +-XX:+UseConcMarkSweepGC +``` + +在确保可以卸载类并且问题仍然存在之后,你应该继续进行堆转储分析–使用类似于以下命令的方法进行堆转储: + +``` +jmap -dump:file=dump.hprof,format=b +``` + +然后,使用你最喜欢的堆转储分析器(例如 Eclipse MAT)打开转储,然后根据已加载的类数查找最昂贵的类加载器。从此类加载器中,你可以继续提取已加载的类,并按实例对此类进行排序,以使可疑对象排在首位。 + +然后,对于每个可疑者,就需要你手动将根本原因追溯到生成此类的应用程序代码。 + +#### 元数据区空间不足 + +【错误】 + +``` +Exception in thread "main" java.lang.OutOfMemoryError: Metaspace +``` + +【原因】 + +Java8 以后,JVM 内存空间发生了很大的变化。取消了永久代,转而变为元数据区。 + +**元数据区的内存不足,即方法区和运行时常量池的空间不足**。 + +方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。 + +一个类要被垃圾收集器回收,判定条件是比较苛刻的。在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。这类常见除了 CGLib 字节码增强和动态语言以外,常见的还有:大量 JSP 或动态产生 JSP 文件的应用(JSP 第一次运行时需要编译为 Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。 + +【示例】方法区出现 `OutOfMemoryError` + +```java +public class MethodAreaOutOfMemoryDemo { + + public static void main(String[] args) { + while (true) { + Enhancer enhancer = new Enhancer(); + enhancer.setSuperclass(Bean.class); + enhancer.setUseCache(false); + enhancer.setCallback(new MethodInterceptor() { + @Override + public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + return proxy.invokeSuper(obj, args); + } + }); + enhancer.create(); + } + } + + static class Bean {} + +} +``` + +【解决】 + +当由于元空间而面临 `OutOfMemoryError` 时,第一个解决方案应该是显而易见的。如果应用程序耗尽了内存中的 Metaspace 区域,则应增加 Metaspace 的大小。更改应用程序启动配置并增加以下内容: + +``` +-XX:MaxMetaspaceSize=512m +``` + +上面的配置示例告诉 JVM,允许 Metaspace 增长到 512 MB。 + +另一种解决方案甚至更简单。你可以通过删除此参数来完全解除对 Metaspace 大小的限制,JVM 默认对 Metaspace 的大小没有限制。但是请注意以下事实:这样做可能会导致大量交换或达到本机物理内存而分配失败。 + +#### 无法新建本地线程 + +`java.lang.OutOfMemoryError: Unable to create new native thread` 这个错误意味着:**Java 应用程序已达到其可以启动线程数的限制**。 + +【原因】 + +当发起一个线程的创建时,虚拟机会在 JVM 内存创建一个 `Thread` 对象同时创建一个操作系统线程,而这个系统线程的内存用的不是 JVM 内存,而是系统中剩下的内存。 + +那么,究竟能创建多少线程呢?这里有一个公式: + +``` +线程数 = (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) +``` + +【参数】 + +- `MaxProcessMemory` - 一个进程的最大内存 +- `JVMMemory` - JVM 内存 +- `ReservedOsMemory` - 保留的操作系统内存 +- `ThreadStackSize` - 线程栈的大小 + +**给 JVM 分配的内存越多,那么能用来创建系统线程的内存就会越少,越容易发生 `unable to create new native thread`**。所以,JVM 内存不是分配的越大越好。 + +但是,通常导致 `java.lang.OutOfMemoryError` 的情况:无法创建新的本机线程需要经历以下阶段: + +1. JVM 内部运行的应用程序请求新的 Java 线程 +2. JVM 本机代码代理为操作系统创建新本机线程的请求 +3. 操作系统尝试创建一个新的本机线程,该线程需要将内存分配给该线程 +4. 操作系统将拒绝本机内存分配,原因是 32 位 Java 进程大小已耗尽其内存地址空间(例如,已达到(2-4)GB 进程大小限制)或操作系统的虚拟内存已完全耗尽 +5. 引发 `java.lang.OutOfMemoryError: Unable to create new native thread` 错误。 + +【示例】 + +```java +public class UnableCreateNativeThreadOOM { + + public static void main(String[] args) { + while (true) { + new Thread(new Runnable() { + @Override + public void run() { + try { + TimeUnit.MINUTES.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }).start(); + } + } +} +``` + +【处理】 + +可以通过增加操作系统级别的限制来绕过无法创建新的本机线程问题。例如,如果限制了 JVM 可在用户空间中产生的进程数,则应检查出并可能增加该限制: + +```shell +[root@dev ~]# ulimit -a +core file size (blocks, -c) 0 +--- cut for brevity --- +max user processes (-u) 1800 +``` + +通常,`OutOfMemoryError` 对新的本机线程的限制表示编程错误。当应用程序产生数千个线程时,很可能出了一些问题—很少有应用程序可以从如此大量的线程中受益。 + +解决问题的一种方法是开始进行线程转储以了解情况。 + +#### 直接内存溢出 + +直接内存(Direct Memory)的容量大小可通过 `-XX:MaxDirectMemorySize` 参数来指定,如果不指定,则默认与 Java 堆最大值(由 `-Xmx` 指定)一致。 + +由直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的 Dump 文件很小,而程序中又直接或间接使用了 DirectMemory(典型的间接使用就是 NIO),那就可以考虑重点检查一下直接内存方面的原因了。 + +由直接内存导致的内存溢出,一个明显的特征是在 Heapdump 文件中不会看见明显的异常,如果发现 OOM 之后 Dump 文件很小,而程序中又直接或间接使用了 NIO,就可以考虑检查一下是不是这方面的原因。 + +【示例】直接内存 `OutOfMemoryError` + +```java +/** + * 本机直接内存溢出示例 + * 错误现象:java.lang.OutOfMemoryError + * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M + */ +public class DirectOutOfMemoryDemo { + + private static final int _1MB = 1024 * 1024; + + public static void main(String[] args) throws IllegalAccessException { + Field unsafeField = Unsafe.class.getDeclaredFields()[0]; + unsafeField.setAccessible(true); + Unsafe unsafe = (Unsafe) unsafeField.get(null); + while (true) { + unsafe.allocateMemory(_1MB); + } + } + +} +``` + +### StackOverflowError + +HotSpot 虚拟机中并不区分虚拟机栈和本地方法栈。 + +对于 HotSpot 虚拟机来说,栈容量只由 `-Xss` 参数来决定。 + +栈溢出的常见原因: + +- **递归函数调用层数太深** - 线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 `StackOverflowError` 异常。 + +- **大量循环或死循环** - 虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 + + `OutOfMemoryError` 异常。 + +【示例】递归函数调用层数太深导致 `StackOverflowError` + +```java +/** + * 以一个无限递归的示例方法来展示栈溢出 + *

+ * 栈溢出时,Java 会抛出 StackOverflowError ,出现此种情况是因为方法运行的时候栈的大小超过了虚拟机的上限所致。 + *

+ * Java 应用程序唤起一个方法调用时就会在调用栈上分配一个栈帧,这个栈帧包含引用方法的参数,本地参数,以及方法的返回地址。 + *

+ * 这个返回地址是被引用的方法返回后,程序能够继续执行的执行点。 + *

+ * 如果没有一个新的栈帧所需空间,Java 就会抛出 StackOverflowError。 + *

+ * VM 参数: + *

    + *
  • -Xss228k - 设置栈大小为 228k
  • + *
+ *

+ * + */ +public class StackOverflowErrorDemo { + + private int stackLength = 1; + + public static void main(String[] args) { + StackOverflowErrorDemo obj = new StackOverflowErrorDemo(); + try { + obj.recursion(); + } catch (Throwable e) { + System.out.println("栈深度:" + obj.stackLength); + e.printStackTrace(); + } + } + + public void recursion() { + stackLength++; + recursion(); + } + +} +``` + +【示例】大量循环或死循环导致 `StackOverflowError` + +```java +/** + * 类成员循环依赖,导致 StackOverflowError + * + * VM 参数: + * + * -Xss228k - 设置栈大小为 228k + * + * @author Zhang Peng + * @since 2019-06-25 + */ +public class StackOverflowErrorDemo2 { + + public static void main(String[] args) { + A obj = new A(); + System.out.println(obj.toString()); + } + + static class A { + + private int value; + + private B instance; + + public A() { + value = 0; + instance = new B(); + } + + @Override + public String toString() { + return "<" + value + ", " + instance + ">"; + } + + } + + static class B { + + private int value; + + private A instance; + + public B() { + value = 10; + instance = new A(); + } + + @Override + public String toString() { + return "<" + value + ", " + instance + ">"; + } + + } + +} +``` + +## 参考资料 + +- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) +- [从表到里学习 JVM 实现](https://www.douban.com/doulist/2545443/) +- [作为测试你应该知道的 JAVA OOM 及定位分析](https://www.jianshu.com/p/28935cbfbae0) +- [异常、堆内存溢出、OOM 的几种情况](https://blog.csdn.net/sinat_29912455/article/details/51125748) +- [介绍 JVM 中 OOM 的 8 种类型](https://tianmingxing.com/2019/11/17/%E4%BB%8B%E7%BB%8DJVM%E4%B8%ADOOM%E7%9A%848%E7%A7%8D%E7%B1%BB%E5%9E%8B/) diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/03.JVM\345\236\203\345\234\276\346\224\266\351\233\206.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\236\203\345\234\276\346\224\266\351\233\206.md" similarity index 93% rename from "source/_posts/01.Java/01.JavaSE/06.JVM/03.JVM\345\236\203\345\234\276\346\224\266\351\233\206.md" rename to "source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\236\203\345\234\276\346\224\266\351\233\206.md" index c80de69800..2ceba501ec 100644 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/03.JVM\345\236\203\345\234\276\346\224\266\351\233\206.md" +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\236\203\345\234\276\346\224\266\351\233\206.md" @@ -1,30 +1,30 @@ --- -title: JVM 垃圾收集 +title: Java 虚拟机之垃圾收集 date: 2020-06-07 09:21:16 order: 03 categories: - Java - - JavaSE + - JavaCore - JVM tags: - Java - - JavaSE + - JavaCore - JVM - GC -permalink: /pages/c5a5b6/ +permalink: /pages/587898a0/ --- -# JVM 垃圾收集 +# Java 虚拟机之垃圾收集 > 程序计数器、虚拟机栈和本地方法栈这三个区域属于线程私有的,只存在于线程的生命周期内,线程结束之后也会消失,因此不需要对这三个区域进行垃圾回收。**垃圾回收主要是针对 Java 堆和方法区进行**。 -## 对象活着吗 +## 对象是否回收 ### 引用计数算法 -给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。 +引用计数算法(Reference Counting)的原理是:在对象中添加一个引用计数器,每当有一个地方 引用它时,计数器值就加一;当引用失效时,计数器值就减一;任何时刻计数器为零的对象就是不可能再被使用的。 -两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 +引用计数算法简单、高效,但是存在循环引用问题——两个对象出现循环引用的情况下,此时引用计数器永远不为 0,导致无法对它们进行回收。 ```java public class ReferenceCountingGC { @@ -158,7 +158,7 @@ obj = null; 因为方法区主要存放永久代对象,而永久代对象的回收率比年轻代差很多,因此在方法区上进行回收性价比不高。 -主要是对常量池的回收和对类的卸载。 +**方法区的垃圾收集主要回收两部分:废弃的常量和不再使用的类型**。 类的卸载条件很多,需要满足以下三个条件,并且满足了也不一定会被卸载: @@ -172,10 +172,10 @@ obj = null; ### finalize() -`finalize()` 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此**最好不要使用 `finalize()`**。 - 当一个对象可被回收时,如果需要执行该对象的 `finalize()` 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。 +`finalize()` 类似 C++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做的更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此**最好不要使用 `finalize()`**。 + ## 垃圾收集算法 ### 垃圾收集性能 @@ -196,7 +196,7 @@ obj = null; 不足: - 标记和清除过程效率都不高; -- 会产生大量不连续的内存碎片,导致无法给大对象分配内存。 +- 会产生大量不连续的内存碎片,内存碎片过多可能导致无法给大对象分配内存。 ### 标记 - 整理(Mark-Compact) @@ -208,7 +208,7 @@ obj = null; 这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。 -### 复制(Copying) +### 标记 - 复制(Copying)

@@ -320,7 +320,7 @@ Serial Old 是 Serial 收集器的老年代版本,也是给 Client 模式下 - 而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 ``` -吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) +吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) ``` **并行收集器是 server 模式下的默认收集器。** @@ -520,7 +520,7 @@ Java 虚拟机启动时选定区域大小。Java 虚拟机通常会指定 2000 ![img](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/images/slide9.png) 图片中的颜色表明了哪个区域被关联上什么角色。活跃对象从一个区域疏散(复制、移动)到另一个区域。区域被设计为并行的方式收集,可以暂停或者不暂停所有的其它用户线程。 -明显的区域可以被分配成 Eden、Survivor、Old 区域。另外,有第四种类型的区域叫做*极大区域(Humongous regions)*。这些区域被设计成保持标准区域大小的 50%或者更大的对象。它们被保存在一个连续的区域集合里。最后,最后一个类型的区域就是堆空间里没有使用的区域。 +明显的区域可以被分配成 Eden、Survivor、Old 区域。另外,有第四种类型的区域叫做*极大区域 (Humongous regions)*。这些区域被设计成保持标准区域大小的 50%或者更大的对象。它们被保存在一个连续的区域集合里。最后,最后一个类型的区域就是堆空间里没有使用的区域。 **注意:**写作此文章时,收集极大对象时还没有被优化。因此,你应该避免创建这个大小的对象。 @@ -559,12 +559,12 @@ Java 虚拟机启动时选定区域大小。Java 虚拟机通常会指定 2000 **(1)初始标记阶段** -年轻代垃圾收集肩负着活跃对象初始标记的任务。在日志文件中被标为*GC pause (young)(inital-mark)* +年轻代垃圾收集肩负着活跃对象初始标记的任务。在日志文件中被标为* GC pause (young)(inital-mark)* ![img](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/images/slide13.png) **(2)并发标记阶段** -如果发现空区域(“X”标示的),在重新标记阶段它们会被马上清除掉。当然,决定活性的审计信息也在此时被计算。 +如果发现空区域 (“X”标示的),在重新标记阶段它们会被马上清除掉。当然,决定活性的审计信息也在此时被计算。 ![img](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/images/slide14.png) **(3)重新标记阶段** @@ -574,7 +574,7 @@ Java 虚拟机启动时选定区域大小。Java 虚拟机通常会指定 2000 ![img](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/images/slide15.png) **(4)复制/清理阶段** -G1 选择活性最低的区域,这些区域能够以最快的速度回收。然后这些区域会在年轻代垃圾回收过程中被回收。在日志中被指示为*[GC pause (mixed)]*。所以年轻代和年老代在同一时间被回收。 +G1 选择活性最低的区域,这些区域能够以最快的速度回收。然后这些区域会在年轻代垃圾回收过程中被回收。在日志中被指示为* [GC pause (mixed)]*。所以年轻代和年老代在同一时间被回收。 ![img](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/images/slide16.png) **(5)复制/清理阶段之后** @@ -585,15 +585,15 @@ G1 选择活性最低的区域,这些区域能够以最快的速度回收。 ### 总结 -| 收集器 | 串行/并行/并发 | 年轻代/老年代 | 收集算法 | 目标 | 适用场景 | -| :-------------------: | :------------: | :-------------: | :------------------: | :----------: | :-------------------------------------------: | -| **Serial** | 串行 | 年轻代 | 复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | -| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 | -| **ParNew** | 串行 + 并行 | 年轻代 | 复制算法 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | -| **Parallel Scavenge** | 串行 + 并行 | 年轻代 | 复制算法 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **Parallel Old** | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | -| **CMS** | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | -| **G1** | 并行 + 并发 | 年轻代 + 老年代 | 标记-整理 + 复制算法 | 响应速度优先 | 面向服务端应用,将来替换 CMS | +| 收集器 | 串行/并行/并发 | 年轻代/老年代 | 收集算法 | 目标 | 适用场景 | +| :-------------------: | :------------: | :-------------: | :-------------------: | :----------: | :-------------------------------------------: | +| **Serial** | 串行 | 年轻代 | 标记-复制 | 响应速度优先 | 单 CPU 环境下的 Client 模式 | +| **Serial Old** | 串行 | 老年代 | 标记-整理 | 响应速度优先 | 单 CPU 环境下的 Client 模式、CMS 的后备预案 | +| **ParNew** | 串行 + 并行 | 年轻代 | 标记-复制 | 响应速度优先 | 多 CPU 环境时在 Server 模式下与 CMS 配合 | +| **Parallel Scavenge** | 串行 + 并行 | 年轻代 | 标记-复制 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | +| **Parallel Old** | 串行 + 并行 | 老年代 | 标记-整理 | 吞吐量优先 | 在后台运算而不需要太多交互的任务 | +| **CMS** | 并行 + 并发 | 老年代 | 标记-清除 | 响应速度优先 | 集中在互联网站或 B/S 系统服务端上的 Java 应用 | +| **G1** | 并行 + 并发 | 年轻代 + 老年代 | 标记-整理 + 标记-复制 | 响应速度优先 | 面向服务端应用,将来替换 CMS | ## 内存分配与回收策略 diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/05.JVM\345\255\227\350\212\202\347\240\201.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\255\227\350\212\202\347\240\201.md" similarity index 67% rename from "source/_posts/01.Java/01.JavaSE/06.JVM/05.JVM\345\255\227\350\212\202\347\240\201.md" rename to "source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\255\227\350\212\202\347\240\201.md" index 0975dbb3df..e1fb069361 100644 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/05.JVM\345\255\227\350\212\202\347\240\201.md" +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\255\227\350\212\202\347\240\201.md" @@ -1,56 +1,56 @@ --- -title: Java 字节码 +title: Java 虚拟机之字节码 date: 2019-10-28 22:04:39 order: 05 categories: - Java - - JavaSE + - JavaCore - JVM tags: - Java - - JavaSE + - JavaCore - JVM - 字节码 -permalink: /pages/e9eb4b/ +permalink: /pages/885f081c/ --- -# Java 字节码 +# Java 虚拟机之字节码 ## 字节码简介 -### 什么是字节码 +Java 字节码是 Java 虚拟机执行的一种指令格式。之所以被称之为字节码,是因为:**Java 字节码文件(`.class`)是一种以 8 位字节为基础单位的二进制流文件**,各个数据项严格按照顺序紧凑地排列在 .class 文件中,中间没有添加任何分隔符。**整个 .class 文件本质上就是一张表**。 -Java 字节码是Java虚拟机执行的一种指令格式。之所以被称之为字节码,是因为:**Java 字节码文件(`.class`)是一种以 8 位字节为基础单位的二进制流文件**,各个数据项严格按照顺序紧凑地排列在 .class 文件中,中间没有添加任何分隔符。**整个 .class 文件本质上就是一张表**。 +Java 能做到 “**一次编译,到处运行**”,一是因为 JVM 针对各种操作系统、平台都进行了定制;二是因为无论在什么平台,都可以编译生成固定格式的 Java 字节码文件(`.class`)。 -Java 能做到 “**一次编译,到处运行**”,一是因为 JVM 针对各种操作系统、平台都进行了定制;二是因为无论在什么平台,都可以编译生成固定格式的Java 字节码文件(`.class`)。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408200751147.png) -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230419203137.png) - -### 字节码文件结构 +## 类文件结构 一个 Java 类编译后生成的 .class 文件内容如下图所示,是一堆十六进制数。 +Class 文件是一组以 8 个字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在文 件之中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎全部是程序运行的必要数 据,没有空隙存在。 + ![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230419141404.png) 图来自 [字节码增强技术探索](https://tech.meituan.com/2019/09/05/java-bytecode-enhancement.html) 字节码看似杂乱无序,实际上是由严格的格式要求组成的。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230419154033.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408200748424.png) -#### 魔数 +### 魔数 -每个 `.class` 文件的头 4 个字节称为 **`魔数(magic_number)`**,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 `.class` 文件。魔数的固定值为:`0xCAFEBABE`。 +每个 `.class` 文件的头 4 个字节称为 **`魔数(magic_number)`**,它的唯一作用是确定这个文件是否为一个能被虚拟机接收的 `.class` 文件。魔数的固定值为:`0xCAFEBABE`(咖啡宝贝)。 -#### 版本号 +### 版本号 版本号(version)有 4 个字节,**前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)**。 -举例来说,如果版本号为:“00 00 00 34”。那么,次版本号转化为十进制为 0,主版本号转化为十进制为 52,在 Oracle 官网中查询序号 52 对应的主版本号为 1.8,所以编译该文件的 Java 版本号为 1.8.0。 +Java 的版本号是从 45 开始的,JDK 1.1 之后 的每个 JDK 大版本发布主版本号向上加 1。举例来说,如果版本号为:“00 00 00 34”。那么,次版本号转化为十进制为 0,主版本号转化为十进制为 52,在 Oracle 官网中查询序号 52 对应的主版本号为 1.8,所以编译该文件的 Java 版本号为 1.8.0。 -#### 常量池 +### 常量池 -紧接着主版本号之后的字节为常量池(constant_pool),常量池可以理解为 .class 文件中的资源仓库。 +紧接着主版本号之后的字节为常量池(constant_pool),**常量池可以理解为 `.class` 文件中的资源仓库**。 常量池整体上分为两部分:常量池计数器以及常量池数据区 @@ -66,29 +66,31 @@ Java 能做到 “**一次编译,到处运行**”,一是因为 JVM 针对 - 字段的名称和描述符 - 方法的名称和描述符 -#### 访问标志 +### 访问标志 + +紧接着常量池的 2 个字节代表访问标志(access_flags),这个标志**用于识别一些类或者接口的访问信息**,描述该 Class 是类还是接口;以及是否被 `public`、`abstract`、`final` 等修饰符修饰。 -紧接着常量池的 2 个字节代表访问标志(access_flags),这个标志**用于识别一些类或者接口的访问信息**,描述该 Class 是类还是接口,以及是否被 `public`、`abstract`、`final` 等修饰符修饰。 +### 类索引、父类索引、接口索引集合 -#### 类索引、父类索引、接口索引 +类索引(this_class)和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,**Class 文件中由这三项数据来确定该类型的继承关系**。 -类索引(this_class)和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合。.class 文件中由这 3 项数据来确定这个类的继承关系。 +### 字段表集合 -#### 字段表 +字段表(field_info)用于描述类和接口中声明的变量。Java 语言中的“字段”(Field)包括类级变 量以及实例级变量,但不包括在方法内部声明的局部变量。 -字段表用于描述类和接口中声明的变量。包含类级变量以及实例级变量,但是不包含方法内部声明的局部变量。 +字段可以包括的修饰符有字段的作用域(public、private、protected 修饰 符)、是实例变量还是类变量(static 修饰符)、可变性(final)、并发可见性(volatile 修饰符,是否 强制从主内存读写)、可否被序列化(transient 修饰符)、字段数据类型(基本类型、对象、数组)、 字段名称。 -字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息 fields_info。 +### 方法表集合 -#### 方法表 +Class 文件存储 格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依 次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表 集合(attributes)几项 字段表结束后为方法表,方法表的结构如同字段表一样,依次包括了访问标志、名称索引、描述符索引、属性表集合几项。 -#### 属性表集合 +### 属性表集合 -属性表集合存放了在该文件中类或接口所定义属性的基本信息。 +属性表集合(attribute_info)存放了在该文件中类或接口所定义属性的基本信息。 -### 字节码指令 +## 字节码指令 字节码指令由一个字节长度的、代表着某种特定操作含义的数字(称为操作码,Opcode)以及跟随其后的零到多个代表此操作所需参数(Operands)而构成。由于 JVM 采用面向操作数栈架构而不是寄存器架构,所以大多数的指令都不包括操作数,只有一个操作码。 @@ -120,7 +122,7 @@ Asm 有两类 API:核心 API 和树形 API Asm Core API 可以类比解析 XML 文件中的 SAX 方式,不需要把这个类的整个结构读取进来,就可以用流式的方法来处理字节码文件。好处是非常节约内存,但是编程难度较大。然而出于性能考虑,一般情况下编程都使用 Core API。在 Core API 中有以下几个关键类: -- ClassReader:用于读取已经编译好的.class 文件。 +- ClassReader:用于读取已经编译好的。class 文件。 - ClassWriter:用于重新构建编译后的类,如修改类名、属性以及方法,也可以生成新的类的字节码文件。 - 各种 Visitor 类:如上所述,CoreAPI 根据字节码从上到下依次处理,对于字节码文件中不同的区域有不同的 Visitor,比如用于访问方法的 MethodVisitor、用于访问类变量的 FieldVisitor、用于访问注解的 AnnotationVisitor 等。为了实现 AOP,重点要使用的是 MethodVisitor。 @@ -135,13 +137,9 @@ Asm Tree API 可以类比解析 XML 文件中的 DOM 方式,把整个类的结 其中最重要的是 ClassPool、CtClass、CtMethod、CtField 这四个类: - `CtClass(compile-time class)` - 编译时类信息,它是一个 class 文件在代码中的抽象表现形式,可以通过一个类的全限定名来获取一个 CtClass 对象,用来表示这个类文件。 -- `ClassPool` - 从开发视角来看,ClassPool 是一张保存 CtClass 信息的 HashTable,key 为类名,value 为类名对应的 CtClass 对象。当我们需要对某个类进行修改时,就是通过 pool.getCtClass("className")方法从 pool 中获取到相应的 CtClass。 +- `ClassPool` - 从开发视角来看,ClassPool 是一张保存 CtClass 信息的 HashTable,key 为类名,value 为类名对应的 CtClass 对象。当我们需要对某个类进行修改时,就是通过 pool.getCtClass("className") 方法从 pool 中获取到相应的 CtClass。 - `CtMethod`、`CtField` - 这两个比较好理解,对应的是类中的方法和属性。 -## 工具 - -[jclasslib](https://plugins.jetbrains.com/plugin/9248-jclasslib-bytecode-viewer) - IDEA 插件,可以直观查看当前字节码文件的类信息、常量池、方法区等信息。 - ## 参考资料 - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/11.JVM\345\221\275\344\273\244\350\241\214\345\267\245\345\205\267.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\267\245\345\205\267.md" similarity index 63% rename from "source/_posts/01.Java/01.JavaSE/06.JVM/11.JVM\345\221\275\344\273\244\350\241\214\345\267\245\345\205\267.md" rename to "source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\267\245\345\205\267.md" index 9b826f518a..14d5fec0f3 100644 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/11.JVM\345\221\275\344\273\244\350\241\214\345\267\245\345\205\267.md" +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\345\267\245\345\205\267.md" @@ -1,20 +1,22 @@ --- -title: JVM 命令行工具 +title: Java 虚拟机之工具 date: 2020-07-30 17:56:33 order: 11 categories: - Java - - JavaSE + - JavaCore - JVM tags: - Java - - JavaSE + - JavaCore - JVM - 命令行 -permalink: /pages/c590ae/ +permalink: /pages/dd0d3c68/ --- -# JVM 命令行工具 +# Java 虚拟机之工具 + +## JVM 命令行工具 > Java 程序员免不了故障排查工作,所以经常需要使用一些 JVM 工具。 > @@ -24,7 +26,7 @@ permalink: /pages/c590ae/ | 名称 | 描述 | | -------- | ----------------------------------------------------------------------------------------------------------------------- | -| `jps` | JVM 进程状态工具。显示系统内的所有 JVM 进程。 | +| `jps` | 虚拟机进程状况工具。显示系统内的所有 JVM 进程。 | | `jstat` | JVM 统计监控工具。监控虚拟机运行时状态信息,它可以显示出 JVM 进程中的类装载、内存、GC、JIT 编译等运行数据。 | | `jmap` | JVM 堆内存分析工具。用于打印 JVM 进程对象直方图、类加载统计。并且可以生成堆转储快照(一般称为 heapdump 或 dump 文件)。 | | `jstack` | JVM 栈查看工具。用于打印 JVM 进程的线程和锁的情况。并且可以生成线程快照(一般称为 threaddump 或 javacore 文件)。 | @@ -32,20 +34,17 @@ permalink: /pages/c590ae/ | `jinfo` | JVM 信息查看工具。用于实时查看和调整 JVM 进程参数。 | | `jcmd` | JVM 命令行调试 工具。用于向 JVM 进程发送调试命令。 | -## jps +### jps:虚拟机进程状况工具 -> **[jps(JVM Process Status Tool)](https://docs.oracle.com/en/java/javase/11/tools/jps.html#GUID-6EB65B96-F9DD-4356-B825-6146E9EEC81E) 是虚拟机进程状态工具**。它可以显示指定系统内所有的 HotSpot 虚拟机进程状态信息。jps 通过 RMI 协议查询开启了 RMI 服务的远程虚拟机进程状态。 +**[jps(JVM Process Status Tool)](https://docs.oracle.com/en/java/javase/11/tools/jps.html#GUID-6EB65B96-F9DD-4356-B825-6146E9EEC81E) 是虚拟机进程状态工具**。它可以列出正在运行的虚拟机进程,并显示虚拟机执行主类(Main Class,main() 函数所在的类)名称以及这些进程的本地虚拟机唯一 ID(LVMID,Local Virtual Machine Identifier)。对于本地虚拟机进程来说,LVMID 与操作系统的进程 ID(PID,Process Identifier)是一致的。 -### jps 命令用法 +jps 命令格式: ```shell jps [option] [hostid] -jps [-help] ``` -如果不指定 hostid 就默认为当前主机或服务器。 - -常用参数: +jps 还可以通过 RMI 协议查询开启了 RMI 服务的远程虚拟机进程状态,参数 hostid 为 RMI 注册表中 注册的主机名。 - `option` - 选项参数 - `-m` - 输出 JVM 启动时传递给 main() 的参数。 @@ -57,7 +56,11 @@ jps [-help] 其中 `option`、`hostid` 参数也可以不写。 -### jps 使用示例 +jps 使用示例: + +::: tabs#jps 使用示例 + +@tab 列出本地 Java 进程 【示例】列出本地 Java 进程 @@ -68,6 +71,8 @@ $ jps 18005 jstat ``` +@tab 列出本地 Java 进程 ID + 【示例】列出本地 Java 进程 ID ```shell @@ -77,7 +82,11 @@ $ jps -q 5398 ``` -【示例】列出本地 Java 进程 ID,并输出主类的全名,如果进程执行的是 jar 包,输出 jar 路径 +@tab 列出本地 Java 进程 ID,并输出主类的全名 + +【示例】列出本地 Java 进程 ID,并输出主类的全名 + +如果进程执行的是 jar 包,输出 jar 路径 ```shell $ jps -l remote.domain @@ -85,18 +94,24 @@ $ jps -l remote.domain 2857 sun.tools.jstatd.jstatd ``` -## jstat +::: -> **[jstat(JVM statistics Monitoring)](https://docs.oracle.com/en/java/javase/11/tools/jstat.html),是虚拟机统计信息监视工具**。jstat 用于监视虚拟机运行时状态信息,它可以显示出虚拟机进程中的类装载、内存、垃圾收集、JIT 编译等运行数据。 +### jstat:虚拟机统计信息监视工具 -### jstat 命令用法 +**[jstat(JVM statistics Monitoring)](https://docs.oracle.com/en/java/javase/11/tools/jstat.html) 是虚拟机统计信息监视工具**。jstat 用于监视虚拟机各种运行状态信息的命令行工具。它可以显示本地或者远程虚拟机进程中的类加载、内存、垃圾收集、即时编译等运行时数据。 -命令格式: +jstat 命令格式: ```shell jstat [option] VMID [interval] [count] ``` +对于命令格式中的 VMID 与 LVMID 需要特别说明一下:如果是本地虚拟机进程,VMID 与 LVMID 是一致的;如果是远程虚拟机进程,那 VMID 的格式应当是: + +``` +[protocol:][//]lvmid[@hostname[:port]/servername] +``` + 常用参数: - `option` - 选项参数,用于指定用户需要查询的虚拟机信息 @@ -118,7 +133,11 @@ jstat [option] VMID [interval] [count] > 【参考】更详细说明可以参考:[jstat 命令查看 jvm 的 GC 情况](https://www.cnblogs.com/yjd_hycf_space/p/7755633.html) -### jstat 使用示例 +jstat 使用示例: + +::: tabs#jstat 使用示例 + +@tab 类加载统计 #### 类加载统计 @@ -140,6 +159,8 @@ Loaded Bytes Unloaded Bytes Time 26749 50405.3 873 1216.8 19.75 ``` +@tab 编译统计 + #### 编译统计 使用 `jstat -compiler pid` 命令可以查看编译统计信息。 @@ -161,11 +182,13 @@ Compiled Failed Invalid Time FailedType FailedMethod - FailedType - 失败类型 - FailedMethod - 失败的方法 +@tab GC 统计 + #### GC 统计 使用 `jstat -gc pid time` 命令可以查看 GC 统计信息。 -【示例】以 250 毫秒的间隔进行 7 个采样,并显示-gcutil 选项指定的输出。 +【示例】以 250 毫秒的间隔进行 7 次采样,并显示-gcutil 选项指定的输出。 ```shell $ jstat -gcutil 21891 250 7 @@ -176,18 +199,14 @@ $ jstat -gcutil 21891 250 7 91.03 0.00 1.98 68.19 95.89 91.24 8 0.378 0 0.000 0.378 91.03 0.00 15.82 68.19 95.89 91.24 8 0.378 0 0.000 0.378 91.03 0.00 17.80 68.19 95.89 91.24 8 0.378 0 0.000 0.378 - 91.03 0.00 17.80 68.19 95.89 91.24 8 0.378 0 0.000 0.378 ``` -【示例】以 1 秒的间隔进行 4 个采样,并显示-gc 选项指定的输出。 +【示例】以 1 秒的间隔进行 4 次采样,并显示-gc 选项指定的输出。 ```shell $ jstat -gc 25196 1s 4 S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT 20928.0 20928.0 0.0 0.0 167936.0 8880.5 838912.0 80291.2 106668.0 100032.1 12772.0 11602.2 760 14.332 580 656.218 670.550 -20928.0 20928.0 0.0 0.0 167936.0 8880.5 838912.0 80291.2 106668.0 100032.1 12772.0 11602.2 760 14.332 580 656.218 670.550 -20928.0 20928.0 0.0 0.0 167936.0 8880.5 838912.0 80291.2 106668.0 100032.1 12772.0 11602.2 760 14.332 580 656.218 670.550 -20928.0 20928.0 0.0 0.0 167936.0 8880.5 838912.0 80291.2 106668.0 100032.1 12772.0 11602.2 760 14.332 580 656.218 670.550 ``` 参数说明: @@ -210,15 +229,43 @@ $ jstat -gc 25196 1s 4 > 注:更详细的参数含义可以参考官方文档:http://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html -## jmap +::: -> **[jmap(JVM Memory Map)](https://docs.oracle.com/en/java/javase/11/tools/jmap.html) 是 Java 内存映像工具**。jmap 用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。jmap 不仅能生成 dump 文件,还可以查询 `finalize` 执行队列、Java 堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。 -> -> 如果不使用这个命令,还可以使用 `-XX:+HeapDumpOnOutOfMemoryError` 参数来让虚拟机出现 OOM 的时候,自动生成 dump 文件。 +### jinfo:Java 配置信息工具 + +**[jinfo(JVM Configuration info)](https://docs.oracle.com/en/java/javase/11/tools/jinfo.html) 是 Java 配置信息工具**。jinfo 用于实时查看和调整虚拟机运行参数。如传递给 Java 虚拟机的`-X`(即输出中的 jvm_args)、`-XX`参数(即输出中的 VM Flags),以及可在 Java 层面通过`System.getProperty`获取的`-D`参数(即输出中的 System Properties)。 + +之前的 `jps -v` 口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用 jinfo 口令。 + +jinfo 命令格式: + +```shell +jinfo [option] pid +``` + +`option` 选项参数: + +- `-flag` - 输出指定 args 参数的值 +- `-sysprops` - 输出系统属性,等同于 `System.getProperties()` + +【示例】jinfo 使用示例 + +```shell +$ jinfo -sysprops 29527 +Attaching to process ID 29527, please wait... +Debugger attached successfully. +Server compiler detected. +JVM version is 25.222-b10 +... +``` + +### jmap:Java 内存映像工具 + +**[jmap(JVM Memory Map)](https://docs.oracle.com/en/java/javase/11/tools/jmap.html) 是 Java 内存映像工具**。jmap 用于生成堆转储快照(一般称为 heapdump 或 dump 文件)。jmap 不仅能生成 dump 文件,还可以查询 `finalize` 执行队列、Java 堆和永久代的详细信息,如当前使用率、当前使用的是哪种收集器等。 -### jmap 命令用法 +如果不使用这个命令,还可以使用 `-XX:+HeapDumpOnOutOfMemoryError` 参数来让虚拟机出现 OOM 的时候,自动生成 dump 文件。 -命令格式: +jmap 命令格式: ``` jmap [option] pid @@ -233,7 +280,9 @@ jmap [option] pid - `-permstat` - to print permanent generation statistics - `-F` - 当-dump 没有响应时,强制生成 dump 快照 -### jstat 使用示例 +::: tabs#jstat 使用示例 + +@tab 生成 heapdump 快照 #### 生成 heapdump 快照 @@ -245,7 +294,9 @@ Dumping heap to /home/xxx/dump.hprof ... Heap dump file created ``` -dump.hprof 这个后缀是为了后续可以直接用 MAT(Memory Anlysis Tool)等工具打开。 +dump.hprof 这个后缀是为了后续可以直接用 MAT(Memory Anlysis Tool) 等工具打开。 + +@tab 查看实例数最多的类 #### 查看实例数最多的类 @@ -259,6 +310,8 @@ $ jmap -histo 29527 | head -n 6 3: 7382322 347307096 [Ljava.lang.Object; ``` +@tab 查看指定进程的堆信息 + #### 查看指定进程的堆信息 注意:使用 CMS GC 情况下,`jmap -heap PID` 的执行有可能会导致 java 进程挂起。 @@ -314,19 +367,46 @@ PS Perm Generation 97.06831451706046% used ``` -## jstack +::: -> **[jstack(Stack Trace for java)](https://docs.oracle.com/en/java/javase/11/tools/jstack.html) 是 Java 堆栈跟踪工具**。jstack 用来打印目标 Java 进程中各个线程的栈轨迹,以及这些线程所持有的锁,并可以生成 java 虚拟机当前时刻的线程快照(一般称为 threaddump 或 javacore 文件)。 -> -> **线程快照是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等**。 +### jhat:虚拟机堆转储快照分析工具 -`jstack` 通常会结合 `top -Hp pid` 或 `pidstat -p pid -t` 一起查看具体线程的状态,也经常用来排查一些死锁的异常。 +**jhat(JVM Heap Analysis Tool) 是虚拟机堆转储快照分析工具**。jhat 与 jmap 搭配使用,用来分析 jmap 生成的 dump 文件。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 的分析结果后,可以在浏览器中查看。 + +提示:一般来说,使用 jhat 分析 dump 快照不是一个好的选择。因为 jhat 是一个耗时并且耗费硬件资源的过程。而在其他服务器上分析快照,不如使用 VisualVM、Eclipse Memory Analyzer、IBM HeapAnalyzer 等 UI 工具来分析,分析功能更加强大。 + +jhat 命令格式: + +```shell +jhat [dumpfile] +``` + +【示例】使用 jhat 分析 dump 文件 + +```shell +jhat eclipse.bin +Reading from eclipse.bin... +Dump file created Fri Nov 19 22:07:21 CST 2010 +Snapshot read, resolving... +Resolving 1225951 objects... +Chasing references, expect 245 dots.... +Eliminating duplicate references... +Snapshot resolved. +Started HTTP server on port 7000 +Server is ready. +``` -线程出现停顿的时候通过 jstack 来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。 如果 java 程序崩溃生成 core 文件,jstack 工具可以用来获得 core 文件的 java stack 和 native stack 的信息,从而可以轻松地知道 java 程序是如何崩溃和在程序何处发生问题。另外,jstack 工具还可以附属到正在运行的 java 程序中,看到当时运行的 java 程序的 java stack 和 native stack 的信息, 如果现在运行的 java 程序呈现 hung 的状态,jstack 是非常有用的。 +显示“Server is ready.”的提示后,用户在浏览器中输入 http://localhost:7000/ 可以看到分析结果。 -### jstack 命令用法 +### jstack:Java 堆栈跟踪工具 -命令格式: +**[jstack(Stack Trace for java)](https://docs.oracle.com/en/java/javase/11/tools/jstack.html) 是 Java 堆栈跟踪工具**。jstack 用于生成虚拟机当前时刻的线程快照(一般称为 threaddump 或者 javacore 文件)。 + +**线程快照**就是当前虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的 目的通常是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间挂 起等,都是导致线程长时间停顿的常见原因。 + +`jstack` 通常会结合 `top -Hp pid` 或 `pidstat -p pid -t` 一起查看具体线程的状态,也经常用来排查一些死锁的异常。 + +jstack 命令格式: ```shell jstack [option] pid @@ -338,13 +418,294 @@ jstack [option] pid - `-l` - 除堆栈外,显示关于锁的附加信息 - `-m` - 打印 java 和 jni 框架的所有栈信息 -### thread dump 文件 +::: tabs#jstack 使用示例 + +@tab 找出某 Java 进程中最耗费 CPU 的 Java 线程 + +#### 找出某 Java 进程中最耗费 CPU 的 Java 线程 + +(1)找出 Java 进程 + +假设应用名称为 myapp: + +```shell +$ jps | grep myapp +29527 myapp.jar +``` + +得到进程 ID 为 21711 + +(2)找出该进程内最耗费 CPU 的线程,可以使用 `ps -Lfp pid` 或者 `ps -mp pid -o THREAD, tid, time` 或者 `top -Hp pid` + +![img](http://static.oschina.net/uploads/space/2014/0128/170402_A57i_111708.png) + +TIME 列就是各个 Java 线程耗费的 CPU 时间,CPU 时间最长的是线程 ID 为 21742 的线程,用 + +```shell +printf "%x\n" 21742 +``` + +得到 21742 的十六进制值为 54ee,下面会用到。 + +(3)使用 jstack 打印线程堆栈信息 + +下一步终于轮到 jstack 上场了,它用来输出进程 21711 的堆栈信息,然后根据线程 ID 的十六进制值 grep,如下: + +```shell +$ jstack 21711 | grep 54ee +"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000] +``` + +可以看到 CPU 消耗在 `PollIntervalRetrySchedulerThread` 这个类的 `Object.wait()`。 + +> 注:上面的例子中,默认只显示了一行信息,但很多时候我们希望查看更详细的调用栈。可以通过指定 `-A ` 的方式来显示行数。例如:`jstack -l | grep -A 10` + +(4)分析代码 + +我找了下我的代码,定位到下面的代码: + +```java +// Idle wait +getLog().info("Thread [" + getName() + "] is idle waiting..."); +schedulerThreadState = PollTaskSchedulerThreadState.IdleWaiting; +long now = System.currentTimeMillis(); +long waitTime = now + getIdleWaitTime(); +long timeUntilContinue = waitTime - now; +synchronized(sigLock) { + try { + if(!halted.get()) { + sigLock.wait(timeUntilContinue); + } + } + catch (InterruptedException ignore) { + } +} +``` + +它是轮询任务的空闲等待代码,上面的 `sigLock.wait(timeUntilContinue)` 就对应了前面的 `Object.wait()`。 + +@tab 生成 threaddump 文件 + +#### 生成 threaddump 文件 + +可以使用 `jstack -l > ` 命令生成 threaddump 文件 + +【示例】生成进程 ID 为 8841 的 Java 进程的 threaddump 文件。 + +```shell +jstack -l 8841 > /home/threaddump.txt +``` + +::: + +## JVM GUI 工具 + +Java 程序员免不了故障排查工作,所以经常需要使用一些 JVM 工具。 + +JDK 中除了附带大量的命令行工具外,还提供了几个功能集成度更高的可视化工具,用户可以使 用这些可视化工具以更加便捷的方式进行进程故障诊断和调试工作。这类工具主要包括 JConsole、JHSDB、VisualVM 和 JMC 四个。 + +### JHSDB:基于服务性代理的调试工具 + +JDK 中提供了 JCMD 和 JHSDB 两个集成式的多功能工具箱。 + +JHSDB 是一款基于服务性代理(Serviceability Agent,SA)实现的进程外调试工具。服务性代理是 HotSpot 虚拟机中一组用于映射 Java 虚拟机运行信息的、主要基于 Java 语言(含少量 JNI 代码)实现的 API 集合。通过服务性代理的 API,可以在一个独立的 Java 虚拟 机的进程里分析其他 HotSpot 虚拟机的内部数据,或者从 HotSpot 虚拟机进程内存中 dump 出来的转储快 照里还原出它的运行状态细节。 + +### JConsole:基于 JMX 的可视化监视与管理工具 + +**JConsole(Java Monitoring and Management Console) 是一种基于 JMX 的可视化监视与管理工具**。它的主要功能是通过JMX的MBean(Managed Bean)对系统进行信息收集和参数动态调整。由于 MBean 可以使用代码、中间件服务器的管理控制台或所有符合 JMX 规范的软件进行访问。 + +注意:使用 jconsole 的前提是 Java 应用开启 JMX。 + +#### 开启 JMX + +Java 应用开启 JMX 后,可以使用 `jconsole` 或 `jvisualvm` 进行监控 Java 程序的基本信息和运行情况。 + +开启方法是,在 java 指令后,添加以下参数: + +```java +-Dcom.sun.management.jmxremote=true +-Dcom.sun.management.jmxremote.ssl=false +-Dcom.sun.management.jmxremote.authenticate=false +-Djava.rmi.server.hostname=127.0.0.1 +-Dcom.sun.management.jmxremote.port=18888 +``` + +- `-Djava.rmi.server.hostname` - 指定 Java 程序运行的服务器 +- `-Dcom.sun.management.jmxremote.port` - 指定 JMX 服务监听端口 + +#### 连接 jconsole + +如果是本地 Java 进程,jconsole 可以直接绑定连接。 + +如果是远程 Java 进程,需要连接 Java 进程的 JMX 端口。 + +![Connecting to a JMX Agent Using the JMX Service URL](https://docs.oracle.com/javase/8/docs/technotes/guides/management/figures/connectadv.gif) + +#### jconsole 界面 + +进入 jconsole 应用后,可以看到以下 tab 页面。 + +- `概述` - 显示有关 Java VM 和监视值的概述信息。 +- `内存` - 显示有关内存使用的信息。内存页相当于可视化的 `jstat` 命令。 +- `线程` - 显示有关线程使用的信息。 +- `类` - 显示有关类加载的信息。 +- `VM 摘要` - 显示有关 Java VM 的信息。 +- `MBean` - 显示有关 MBean 的信息。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730151422.png) + +### VisualVM + +> jvisualvm 是 JDK 自带的 GUI 工具。**jvisualvm(All-In-One Java Troubleshooting Tool) 是多合一故障处理工具**。它支持运行监视、故障处理、性能分析等功能。 + +个人觉得 jvisualvm 比 jconsole 好用。 + +#### jvisualvm 概述页面 + +jvisualvm 概述页面可以查看当前 Java 进程的基本信息,如:JDK 版本、Java 进程、JVM 参数等。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150147.png) + +#### jvisualvm 监控页面 + +在 jvisualvm 监控页面,可以看到 Java 进程的 CPU、内存、类加载、线程的实时变化。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150254.png) + +#### jvisualvm 线程页面 + +jvisualvm 线程页面展示了当前的线程状态。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150416.png) + +jvisualvm 还可以生成线程 Dump 文件,帮助进一步分析线程栈信息。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150830.png) + +#### jvisualvm 抽样器页面 + +jvisualvm 可以对 CPU、内存进行抽样,帮助我们进行性能分析。 + +### MAT + +[MAT](https://www.eclipse.org/mat/) 即 Eclipse Memory Analyzer Tool 的缩写。 + +MAT 本身也能够获取堆的二进制快照。该功能将借助 `jps` 列出当前正在运行的 Java 进程,以供选择并获取快照。由于 `jps` 会将自己列入其中,因此你会在列表中发现一个已经结束运行的 `jps` 进程。 + +MAT 可以独立安装([官方下载地址](http://www.eclipse.org/mat/downloads.php)),也可以作为 Eclipse IDE 的插件安装。 + +#### MAT 配置 + +MAT 解压后,安装目录下有个 `MemoryAnalyzer.ini` 文件。 + +`MemoryAnalyzer.ini` 中有个重要的参数 `Xmx` 表示最大内存,默认为:`-vmargs -Xmx1024m` + +如果试图用 MAT 导入的 dump 文件超过 1024 M,会报错: + +```shell +An internal error occurred during: "Parsing heap dump from XXX" +``` + +此时,可以适当调整 `Xmx` 大小。如果设置的 `Xmx` 数值过大,本机内存不足以支撑,启动 MAT 会报错: + +``` +Failed to create the Java Virtual Machine +``` + +#### MAT 分析 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200308092746.png) + +点击 Leak Suspects 可以进入内存泄漏页面。 + +(1)首先,可以查看饼图了解内存的整体消耗情况 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200308150556.png) + +(2)缩小范围,寻找问题疑似点 + +![img](https://img-blog.csdn.net/20160223202154818) + +可以点击进入详情页面,在详情页面 Shortest Paths To the Accumulation Point 表示 GC root 到内存消耗聚集点的最短路径,如果某个内存消耗聚集点有路径到达 GC root,则该内存消耗聚集点不会被当做垃圾被回收。 + +为了找到内存泄露,我获取了两个堆转储文件,两个文件获取时间间隔是一天(因为内存只是小幅度增长,短时间很难发现问题)。对比两个文件的对象,通过对比后的结果可以很方便定位内存泄露。 + +MAT 同时打开两个堆转储文件,分别打开 Histogram,如下图。在下图中方框 1 按钮用于对比两个 Histogram,对比后在方框 2 处选择 Group By package,然后对比各对象的变化。不难发现 heap3.hprof 比 heap6.hprof 少了 64 个 eventInfo 对象,如果对代码比较熟悉的话想必这样一个结果是能够给程序员一定的启示的。而我也是根据这个启示差找到了最终内存泄露的位置。 +![img](https://img-blog.csdn.net/20160223203226362) + +### JProfile + +[JProfiler](https://www.ej-technologies.com/products/jprofiler/overview.html) 是一款性能分析工具。 + +由于它是收费的,所以我本人使用较少。但是,它确实功能强大,且方便使用,还可以和 Intellij Idea 集成。 + +### Arthas + +[Arthas](https://github.com/alibaba/arthas) 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。 + +Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 `Tab` 自动补全功能,进一步方便进行问题的定位和诊断。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730145030.png) + +#### Arthas 基础命令 + +- help——查看命令帮助信息 +- [cat](https://alibaba.github.io/arthas/cat.html)——打印文件内容,和 linux 里的 cat 命令类似 +- [echo](https://alibaba.github.io/arthas/echo.html)–打印参数,和 linux 里的 echo 命令类似 +- [grep](https://alibaba.github.io/arthas/grep.html)——匹配查找,和 linux 里的 grep 命令类似 +- [tee](https://alibaba.github.io/arthas/tee.html)——复制标准输入到标准输出和指定的文件,和 linux 里的 tee 命令类似 +- [pwd](https://alibaba.github.io/arthas/pwd.html)——返回当前的工作目录,和 linux 命令类似 +- cls——清空当前屏幕区域 +- session——查看当前会话的信息 +- [reset](https://alibaba.github.io/arthas/reset.html)——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类 +- version——输出当前目标 Java 进程所加载的 Arthas 版本号 +- history——打印命令历史 +- quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响 +- stop——关闭 Arthas 服务端,所有 Arthas 客户端全部退出 +- [keymap](https://alibaba.github.io/arthas/keymap.html)——Arthas 快捷键列表及自定义快捷键 + +#### Arthas jvm 相关命令 + +- [dashboard](https://alibaba.github.io/arthas/dashboard.html)——当前系统的实时数据面板 +- [thread](https://alibaba.github.io/arthas/thread.html)——查看当前 JVM 的线程堆栈信息 +- [jvm](https://alibaba.github.io/arthas/jvm.html)——查看当前 JVM 的信息 +- [sysprop](https://alibaba.github.io/arthas/sysprop.html)——查看和修改 JVM 的系统属性 +- [sysenv](https://alibaba.github.io/arthas/sysenv.html)——查看 JVM 的环境变量 +- [vmoption](https://alibaba.github.io/arthas/vmoption.html)——查看和修改 JVM 里诊断相关的 option +- [perfcounter](https://alibaba.github.io/arthas/perfcounter.html)——查看当前 JVM 的 Perf Counter 信息 +- [logger](https://alibaba.github.io/arthas/logger.html)——查看和修改 logger +- [getstatic](https://alibaba.github.io/arthas/getstatic.html)——查看类的静态属性 +- [ognl](https://alibaba.github.io/arthas/ognl.html)——执行 ognl 表达式 +- [mbean](https://alibaba.github.io/arthas/mbean.html)——查看 Mbean 的信息 +- [heapdump](https://alibaba.github.io/arthas/heapdump.html)——dump java heap, 类似 jmap 命令的 heap dump 功能 + +#### Arthas class/classloader 相关命令 + +- [sc](https://alibaba.github.io/arthas/sc.html)——查看 JVM 已加载的类信息 +- [sm](https://alibaba.github.io/arthas/sm.html)——查看已加载类的方法信息 +- [jad](https://alibaba.github.io/arthas/jad.html)——反编译指定已加载类的源码 +- [mc](https://alibaba.github.io/arthas/mc.html)——内存编译器,内存编译`.java`文件为`.class`文件 +- [redefine](https://alibaba.github.io/arthas/redefine.html)——加载外部的`.class`文件,redefine 到 JVM 里 +- [dump](https://alibaba.github.io/arthas/dump.html)——dump 已加载类的 byte code 到特定目录 +- [classloader](https://alibaba.github.io/arthas/classloader.html)——查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource + +#### Arthas monitor/watch/trace 相关命令 + +> 请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 `stop` 或将增强过的类执行 `reset` 命令。 + +- [monitor](https://alibaba.github.io/arthas/monitor.html)——方法执行监控 +- [watch](https://alibaba.github.io/arthas/watch.html)——方法执行数据观测 +- [trace](https://alibaba.github.io/arthas/trace.html)——方法内部调用路径,并输出方法路径上的每个节点上耗时 +- [stack](https://alibaba.github.io/arthas/stack.html)——输出当前方法被调用的调用路径 +- [tt](https://alibaba.github.io/arthas/tt.html)——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 + +## thread dump 文件 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730112431.png) 一个 Thread Dump 文件大致可以分为五个部分。 -#### 第一部分:Full thread dump identifier +### 第一部分:Full thread dump identifier 这一部分是内容最开始的部分,展示了快照文件的生成时间和 JVM 的版本信息。 @@ -353,7 +714,7 @@ jstack [option] pid Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode): ``` -#### 第二部分:Java EE middleware, third party & custom application Threads +### 第二部分:Java EE middleware, third party & custom application Threads 这是整个文件的核心部分,里面展示了 JavaEE 容器(如 tomcat、resin 等)、自己的程序中所使用的线程信息。 @@ -369,21 +730,21 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode): 参数说明: -- `"resin-22129"` **线程名称:**如果使用 java.lang.Thread 类生成一个线程的时候,线程名称为 Thread-(数字) 的形式,这里是 resin 生成的线程; +- `"resin-22129"` **线程名称:**如果使用 java.lang.Thread 类生成一个线程的时候,线程名称为 Thread-(数字) 的形式,这里是 resin 生成的线程; - `daemon` **线程类型:**线程分为守护线程 (daemon) 和非守护线程 (non-daemon) 两种,通常都是守护线程; - `prio=10` **线程优先级:**默认为 5,数字越大优先级越高; -- `tid=0x00007fbe5c34e000` **JVM 线程的 id:**JVM 内部线程的唯一标识,通过 java.lang.Thread.getId()获取,通常用自增的方式实现; +- `tid=0x00007fbe5c34e000` **JVM 线程的 id:**JVM 内部线程的唯一标识,通过 java.lang.Thread.getId() 获取,通常用自增的方式实现; - `nid=0x4cb1` **系统线程 id:**对应的系统线程 id(Native Thread ID),可以通过 top 命令进行查看,现场 id 是十六进制的形式; - `waiting on condition` **系统线程状态:**这里是系统的线程状态; - `[0x00007fbe4ff7c000]` **起始栈地址:**线程堆栈调用的其实内存地址; - `java.lang.Thread.State: WAITING (parking)` **JVM 线程状态:**这里标明了线程在代码级别的状态。 - **线程调用栈信息:**下面就是当前线程调用的详细栈信息,用于代码的分析。堆栈信息应该从下向上解读,因为程序调用的顺序是从下向上的。 -#### 第三部分:HotSpot VM Thread +### 第三部分:HotSpot VM Thread 这一部分展示了 JVM 内部线程的信息,用于执行内部的原生操作。下面常见的集中内置线程: -##### "Attach Listener" +#### "Attach Listener" 该线程负责接收外部命令,执行该命令并把结果返回给调用者,此种类型的线程通常在桌面程序中出现。 @@ -392,7 +753,7 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode): java.lang.Thread.State: RUNNABLE ``` -##### "DestroyJavaVM" +#### "DestroyJavaVM" 执行 `main()` 的线程在执行完之后调用 JNI 中的 `jni_DestroyJavaVM()` 方法会唤起 `DestroyJavaVM` 线程,处于等待状态,等待其它线程(java 线程和 native 线程)退出时通知它卸载 JVM。 @@ -401,7 +762,7 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode): java.lang.Thread.State: RUNNABLE ``` -##### "Service Thread" +#### "Service Thread" 用于启动服务的线程 @@ -410,7 +771,7 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode): java.lang.Thread.State: RUNNABLE ``` -##### "CompilerThread" +#### "CompilerThread" 用来调用 JITing,实时编译装卸类。通常 JVM 会启动多个线程来处理这部分工作,线程名称后面的数字也会累加,比如 CompilerThread1。 @@ -422,7 +783,7 @@ Full thread dump Java HotSpot(TM) 64-Bit Server VM (24.79-b02 mixed mode): java.lang.Thread.State: RUNNABLE ``` -##### "Signal Dispatcher" +#### "Signal Dispatcher" Attach Listener 线程的职责是接收外部 jvm 命令,当命令接收成功后,会交给 signal dispather 线程去进行分发到各个不同的模块处理命令,并且返回处理结果。 signal dispather 线程也是在第一次接收外部 jvm 命令时,进行初始化工作。 @@ -432,17 +793,17 @@ signal dispather 线程也是在第一次接收外部 jvm 命令时,进行初 java.lang.Thread.State: RUNNABLE ``` -##### "Finalizer" +#### "Finalizer" 这个线程也是在 main 线程之后创建的,其优先级为 10,主要用于在垃圾收集前,调用对象的 `finalize()` 方法;关于 Finalizer 线程的几点: -- 只有当开始一轮垃圾收集时,才会开始调用 finalize()方法;因此并不是所有对象的 finalize()方法都会被执行; -- 该线程也是 daemon 线程,因此如果虚拟机中没有其他非 daemon 线程,不管该线程有没有执行完 finalize()方法,JVM 也会退出; +- 只有当开始一轮垃圾收集时,才会开始调用 finalize() 方法;因此并不是所有对象的 finalize() 方法都会被执行; +- 该线程也是 daemon 线程,因此如果虚拟机中没有其他非 daemon 线程,不管该线程有没有执行完 finalize() 方法,JVM 也会退出; - JVM 在垃圾收集时会将失去引用的对象包装成 Finalizer 对象(Reference 的实现),并放入 ReferenceQueue,由 Finalizer 线程来处理;最后将该 Finalizer 对象的引用置为 null,由垃圾收集器来回收; JVM 为什么要单独用一个线程来执行 `finalize()` 方法呢? -如果 JVM 的垃圾收集线程自己来做,很有可能由于在 finalize()方法中误操作导致 GC 线程停止或不可控,这对 GC 线程来说是一种灾难。 +如果 JVM 的垃圾收集线程自己来做,很有可能由于在 finalize() 方法中误操作导致 GC 线程停止或不可控,这对 GC 线程来说是一种灾难。 ``` "Finalizer" daemon prio=10 tid=0x00007fbea80da000 nid=0x5eb in Object.wait() [0x00007fbeac044000] @@ -454,7 +815,7 @@ JVM 为什么要单独用一个线程来执行 `finalize()` 方法呢? at java.lang.ref.Finalizer$FinalizerThread.run(Finalizer.java:209) ``` -##### "Reference Handler" +#### "Reference Handler" JVM 在创建 main 线程后就创建 Reference Handler 线程,其优先级最高,为 10,它主要用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收问题 。 @@ -467,21 +828,21 @@ JVM 在创建 main 线程后就创建 Reference Handler 线程,其优先级最 - locked <0x00000006d173c1f0> (a java.lang.ref.Reference$Lock) ``` -##### "VM Thread" +#### "VM Thread" JVM 中线程的母体,根据 HotSpot 源码中关于 vmThread.hpp 里面的注释,它是一个单例的对象(最原始的线程)会产生或触发所有其他的线程,这个单例的 VM 线程是会被其他线程所使用来做一些 VM 操作(如清扫垃圾等)。 -在 VM Thread 的结构体里有一个 VMOperationQueue 列队,所有的 VM 线程操作(vm_operation)都会被保存到这个列队当中,VMThread 本身就是一个线程,它的线程负责执行一个自轮询的 loop 函数(具体可以参考:VMThread.cpp 里面的 void VMThread::loop()) ,该 loop 函数从 VMOperationQueue 列队中按照优先级取出当前需要执行的操作对象(VM_Operation),并且调用 VM_Operation->evaluate 函数去执行该操作类型本身的业务逻辑。 +在 VM Thread 的结构体里有一个 VMOperationQueue 列队,所有的 VM 线程操作 (vm_operation) 都会被保存到这个列队当中,VMThread 本身就是一个线程,它的线程负责执行一个自轮询的 loop 函数(具体可以参考:VMThread.cpp 里面的 void VMThread::loop()) ,该 loop 函数从 VMOperationQueue 列队中按照优先级取出当前需要执行的操作对象 (VM_Operation),并且调用 VM_Operation->evaluate 函数去执行该操作类型本身的业务逻辑。 VM 操作类型被定义在 vm_operations.hpp 文件内,列举几个:ThreadStop、ThreadDump、PrintThreads、GenCollectFull、GenCollectFullConcurrent、CMS_Initial_Mark、CMS_Final_Remark….. 有兴趣的同学,可以自己去查看源文件。 ``` "VM Thread" prio=10 tid=0x00007fbea80d3800 nid=0x5e9 runnable ``` -#### 第四部分:HotSpot GC Thread +### 第四部分:HotSpot GC Thread JVM 中用于进行资源回收的线程,包括以下几种类型的线程: -##### "VM Periodic Task Thread" +#### "VM Periodic Task Thread" 该线程是 JVM 周期性任务调度的线程,它由 WatcherThread 创建,是一个单例对象。该线程在 JVM 内使用得比较频繁,比如:定期的内存监控、JVM 运行状况监控。 @@ -492,7 +853,7 @@ JVM 中用于进行资源回收的线程,包括以下几种类型的线程: 可以使用 jstat 命令查看 GC 的情况,比如查看某个进程没有存活必要的引用可以使用命令 `jstat -gcutil 250 7` 参数中 pid 是进程 id,后面的 250 和 7 表示每 250 毫秒打印一次,总共打印 7 次。 这对于防止因为应用代码中直接使用 native 库或者第三方的一些监控工具的内存泄漏有非常大的帮助。 -##### "GC task thread#0 (ParallelGC)" +#### "GC task thread#0 (ParallelGC)" 垃圾回收线程,该线程会负责进行垃圾回收。通常 JVM 会启动多个线程来处理这个工作,线程名称中#后面的数字也会累加。 @@ -508,9 +869,9 @@ JVM 中用于进行资源回收的线程,包括以下几种类型的线程: 如果在 JVM 中增加了 `-XX:+UseConcMarkSweepGC` 参数将会启用 CMS (Concurrent Mark-Sweep)GC Thread 方式,以下是该模式下的线程类型: -##### "Gang worker#0 (Parallel GC Threads)" +#### "Gang worker#0 (Parallel GC Threads)" -原来垃圾回收线程 GC task thread#0 (ParallelGC) 被替换为 Gang worker#0 (Parallel GC Threads)。Gang worker 是 JVM 用于年轻代垃圾回收(minor gc)的线程。 +原来垃圾回收线程 GC task thread#0 (ParallelGC) 被替换为 Gang worker#0 (Parallel GC Threads)。Gang worker 是 JVM 用于年轻代垃圾回收 (minor gc) 的线程。 ``` "Gang worker#0 (Parallel GC Threads)" prio=10 tid=0x00007fbea801b800 nid=0x5e4 runnable @@ -518,7 +879,7 @@ JVM 中用于进行资源回收的线程,包括以下几种类型的线程: "Gang worker#1 (Parallel GC Threads)" prio=10 tid=0x00007fbea801d800 nid=0x5e7 runnable ``` -##### "Concurrent Mark-Sweep GC Thread" +#### "Concurrent Mark-Sweep GC Thread" 并发标记清除垃圾回收器(就是通常所说的 CMS GC)线程, 该线程主要针对于年老代垃圾回收。 @@ -526,7 +887,7 @@ JVM 中用于进行资源回收的线程,包括以下几种类型的线程: "Concurrent Mark-Sweep GC Thread" prio=10 tid=0x00007fbea8073800 nid=0x5e8 runnable ``` -##### "Surrogate Locker Thread (Concurrent GC)" +#### "Surrogate Locker Thread (Concurrent GC)" 此线程主要配合 CMS 垃圾回收器来使用,是一个守护线程,主要负责处理 GC 过程中 Java 层的 Reference(指软引用、弱引用等等)与 jvm 内部层面的对象状态同步。 @@ -537,7 +898,7 @@ JVM 中用于进行资源回收的线程,包括以下几种类型的线程: 这里以 WeakHashMap 为例进行说明,首先是一个关键点: -- WeakHashMap 和 HashMap 一样,内部有一个 Entry[]数组; +- WeakHashMap 和 HashMap 一样,内部有一个 Entry[] 数组; - WeakHashMap 的 Entry 比较特殊,它的继承体系结构为 Entry->WeakReference->Reference; - Reference 里面有一个全局锁对象:Lock,它也被称为 pending_lock,注意:它是静态对象; - Reference 里面有一个静态变量:pending; @@ -545,10 +906,10 @@ JVM 中用于进行资源回收的线程,包括以下几种类型的线程: - WeakHashMap 里面还实例化了一个 ReferenceQueue 列队 假设,WeakHashMap 对象里面已经保存了很多对象的引用,JVM 在进行 CMS GC 的时候会创建一个 ConcurrentMarkSweepThread(简称 CMST)线程去进行 GC。ConcurrentMarkSweepThread 线程被创建的同时会创建一个 SurrogateLockerThread(简称 SLT)线程并且启动它,SLT 启动之后,处于等待阶段。 -CMST 开始 GC 时,会发一个消息给 SLT 让它去获取 Java 层 Reference 对象的全局锁:Lock。直到 CMS GC 完毕之后,JVM 会将 WeakHashMap 中所有被回收的对象所属的 WeakReference 容器对象放入到 Reference 的 pending 属性当中(每次 GC 完毕之后,pending 属性基本上都不会为 null 了),然后通知 SLT 释放并且 notify 全局锁:Lock。此时激活了 ReferenceHandler 线程的 run 方法,使其脱离 wait 状态,开始工作了。 -ReferenceHandler 这个线程会将 pending 中的所有 WeakReference 对象都移动到它们各自的列队当中,比如当前这个 WeakReference 属于某个 WeakHashMap 对象,那么它就会被放入相应的 ReferenceQueue 列队里面(该列队是链表结构)。 当我们下次从 WeakHashMap 对象里面 get、put 数据或者调用 size 方法的时候,WeakHashMap 就会将 ReferenceQueue 列队中的 WeakReference 依依 poll 出来去和 Entry[]数据做比较,如果发现相同的,则说明这个 Entry 所保存的对象已经被 GC 掉了,那么将 Entry[]内的 Entry 对象剔除掉。 +CMST 开始 GC 时,会发一个消息给 SLT 让它去获取 Java 层 Reference 对象的全局锁:Lock。直到 CMS GC 完毕之后,JVM 会将 WeakHashMap 中所有被回收的对象所属的 WeakReference 容器对象放入到 Reference 的 pending 属性当中(每次 GC 完毕之后,pending 属性基本上都不会为 null 了),然后通知 SLT 释放并且 notify 全局锁:Lock。此时激活了 ReferenceHandler 线程的 run 方法,使其脱离 wait 状态,开始工作了。 +ReferenceHandler 这个线程会将 pending 中的所有 WeakReference 对象都移动到它们各自的列队当中,比如当前这个 WeakReference 属于某个 WeakHashMap 对象,那么它就会被放入相应的 ReferenceQueue 列队里面(该列队是链表结构)。 当我们下次从 WeakHashMap 对象里面 get、put 数据或者调用 size 方法的时候,WeakHashMap 就会将 ReferenceQueue 列队中的 WeakReference 依依 poll 出来去和 Entry[] 数据做比较,如果发现相同的,则说明这个 Entry 所保存的对象已经被 GC 掉了,那么将 Entry[] 内的 Entry 对象剔除掉。 -#### 第五部分:JNI global references count +### 第五部分:JNI global references count 这一部分主要回收那些在 native 代码上被引用,但在 java 代码中却没有存活必要的引用,对于防止因为应用代码中直接使用 native 库或第三方的一些监控工具的内存泄漏有非常大的帮助。 @@ -556,13 +917,11 @@ ReferenceHandler 这个线程会将 pending 中的所有 WeakReference 对象都 JNI global references: 830 ``` -下一篇文章将要讲述一个直接找出 CPU 100% 线程的例子。 - -### 系统线程状态 +## 系统线程状态 系统线程有如下状态: -#### deadlock +### deadlock 死锁线程,一般指多个线程调用期间进入了相互资源占用,导致一直等待无法释放的情况。 @@ -603,11 +962,11 @@ JNI global references: 830 - None ``` -#### runnable +### runnable 一般指该线程正在执行状态中,该线程占用了资源,正在处理某个操作,如通过 SQL 语句查询数据库、对某个文件进行写入等。 -#### blocked +### blocked 线程正处于阻塞状态,指当前线程执行过程中,所需要的资源长时间等待却一直未能获取到,被容器的线程管理器标识为阻塞状态,可以理解为等待资源超时的线程。 @@ -636,7 +995,7 @@ JNI global references: 830 - <0x0000000780b0e1b8> (a java.util.concurrent.locks.ReentrantLock$NonfairSync) ``` -#### waiting on condition +### waiting on condition 线程正处于等待资源或等待某个条件的发生,具体的原因需要结合下面堆栈信息进行分析。 @@ -667,7 +1026,7 @@ JNI global references: 830 at java.lang.Thread.run(Thread.java:662) ``` -#### waiting for monitor entry 或 in Object.wait() +### waiting for monitor entry 或 in Object.wait() Moniter 是 Java 中用以实现线程之间的互斥与协作的主要手段,它可以看成是对象或者 class 的锁,每个对象都有,也仅有一个 Monitor。 @@ -693,128 +1052,17 @@ synchronized(obj) { (2)"Wait Set"里面的线程 -当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(通常是被 synchronized 的对象)的 wait()方法,放弃 Monitor,进入 "Wait Set"队列。只有当别的线程在该对象上调用了 notify()或者 notifyAll()方法,"Wait Set"队列中的线程才得到机会去竞争,但是只有一个线程获得对象的 Monitor,恢复到运行态。"Wait Set"中的线程在 Thread Dump 中显示的状态为 in Object.wait()。通常来说,当 CPU 很忙的时候关注 Runnable 状态的线程,反之则关注 waiting for monitor entry 状态的线程。 - -### jstack 使用示例 - -#### 找出某 Java 进程中最耗费 CPU 的 Java 线程 - -(1)找出 Java 进程 - -假设应用名称为 myapp: - -```shell -$ jps | grep myapp -29527 myapp.jar -``` - -得到进程 ID 为 21711 - -(2)找出该进程内最耗费 CPU 的线程,可以使用 `ps -Lfp pid` 或者 `ps -mp pid -o THREAD, tid, time` 或者 `top -Hp pid` - -![img](http://static.oschina.net/uploads/space/2014/0128/170402_A57i_111708.png) -TIME 列就是各个 Java 线程耗费的 CPU 时间,CPU 时间最长的是线程 ID 为 21742 的线程,用 - -```shell -printf "%x\n" 21742 -``` - -得到 21742 的十六进制值为 54ee,下面会用到。 - -(3)使用 jstack 打印线程堆栈信息 - -下一步终于轮到 jstack 上场了,它用来输出进程 21711 的堆栈信息,然后根据线程 ID 的十六进制值 grep,如下: - -```shell -$ jstack 21711 | grep 54ee -"PollIntervalRetrySchedulerThread" prio=10 tid=0x00007f950043e000 nid=0x54ee in Object.wait() [0x00007f94c6eda000] -``` - -可以看到 CPU 消耗在 `PollIntervalRetrySchedulerThread` 这个类的 `Object.wait()`。 - -> 注:上面的例子中,默认只显示了一行信息,但很多时候我们希望查看更详细的调用栈。可以通过指定 `-A ` 的方式来显示行数。例如:`jstack -l | grep -A 10` - -(4)分析代码 - -我找了下我的代码,定位到下面的代码: - -```java -// Idle wait -getLog().info("Thread [" + getName() + "] is idle waiting..."); -schedulerThreadState = PollTaskSchedulerThreadState.IdleWaiting; -long now = System.currentTimeMillis(); -long waitTime = now + getIdleWaitTime(); -long timeUntilContinue = waitTime - now; -synchronized(sigLock) { - try { - if(!halted.get()) { - sigLock.wait(timeUntilContinue); - } - } - catch (InterruptedException ignore) { - } -} -``` - -它是轮询任务的空闲等待代码,上面的 `sigLock.wait(timeUntilContinue)` 就对应了前面的 `Object.wait()`。 - -#### 生成 threaddump 文件 - -可以使用 `jstack -l > ` 命令生成 threaddump 文件 - -【示例】生成进程 ID 为 8841 的 Java 进程的 threaddump 文件。 - -``` -jstack -l 8841 > /home/threaddump.txt -``` - -## jinfo - -> **[jinfo(JVM Configuration info)](https://docs.oracle.com/en/java/javase/11/tools/jinfo.html),是 Java 配置信息工具**。jinfo 用于实时查看和调整虚拟机运行参数。如传递给 Java 虚拟机的`-X`(即输出中的 jvm_args)、`-XX`参数(即输出中的 VM Flags),以及可在 Java 层面通过`System.getProperty`获取的`-D`参数(即输出中的 System Properties)。 - -之前的 `jps -v` 口令只能查看到显示指定的参数,如果想要查看未被显示指定的参数的值就要使用 jinfo 口令。 - -jinfo 命令格式: - -```shell -jinfo [option] pid -``` - -`option` 选项参数: - -- `-flag` - 输出指定 args 参数的值 -- `-sysprops` - 输出系统属性,等同于 `System.getProperties()` - -【示例】jinfo 使用示例 - -```shell -$ jinfo -sysprops 29527 -Attaching to process ID 29527, please wait... -Debugger attached successfully. -Server compiler detected. -JVM version is 25.222-b10 -... -``` - -## jhat - -> **jhat(JVM Heap Analysis Tool),是虚拟机堆转储快照分析工具**。jhat 与 jmap 搭配使用,用来分析 jmap 生成的 dump 文件。jhat 内置了一个微型的 HTTP/HTML 服务器,生成 dump 的分析结果后,可以在浏览器中查看。 -> -> 注意:一般不会直接在服务器上进行分析,因为 jhat 是一个耗时并且耗费硬件资源的过程,一般把服务器生成的 dump 文件,用 jvisualvm 、Eclipse Memory Analyzer、IBM HeapAnalyzer 等工具来分析。 - -命令格式: - -```shell -jhat [dumpfile] -``` +当线程获得了 Monitor,进入了临界区之后,如果发现线程继续运行的条件没有满足,它则调用对象(通常是被 synchronized 的对象)的 wait() 方法,放弃 Monitor,进入 "Wait Set"队列。只有当别的线程在该对象上调用了 notify() 或者 notifyAll() 方法,"Wait Set"队列中的线程才得到机会去竞争,但是只有一个线程获得对象的 Monitor,恢复到运行态。"Wait Set"中的线程在 Thread Dump 中显示的状态为 in Object.wait()。通常来说,当 CPU 很忙的时候关注 Runnable 状态的线程,反之则关注 waiting for monitor entry 状态的线程。 ## 参考资料 - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - [JVM 性能调优监控工具 jps、jstack、jmap、jhat、jstat、hprof 使用详解](https://my.oschina.net/feichexia/blog/196575) +- [JVM 故障分析及性能优化系列之一:使用 jstack 定位线程堆栈信息](https://www.javatang.com/archives/2017/10/19/33151873.html) +- [jstat 命令查看 jvm 的 GC 情况](https://www.cnblogs.com/yjd_hycf_space/p/7755633.html) - [jconsole 官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/management/jconsole.html) - [jconsole 工具使用](https://www.cnblogs.com/kongzhongqijing/articles/3621441.html) -- [jstat 命令查看 jvm 的 GC 情况](https://www.cnblogs.com/yjd_hycf_space/p/7755633.html) +- [jvisualvm 官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/visualvm/index.html) +- [Java jvisualvm 简要说明](https://blog.csdn.net/a19881029/article/details/8432368) - [利用内存分析工具(Memory Analyzer Tool,MAT)分析 java 项目内存泄露](https://blog.csdn.net/wanghuiqi2008/article/details/50724676) -- [JVM 故障分析及性能优化系列之一:使用 jstack 定位线程堆栈信息](https://www.javatang.com/archives/2017/10/19/33151873.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/22.Java\346\225\205\351\232\234\350\257\212\346\226\255.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\346\225\205\351\232\234\345\244\204\347\220\206.md" similarity index 98% rename from "source/_posts/01.Java/01.JavaSE/06.JVM/22.Java\346\225\205\351\232\234\350\257\212\346\226\255.md" rename to "source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\346\225\205\351\232\234\345\244\204\347\220\206.md" index f9499d4d0e..3e14957a34 100644 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/22.Java\346\225\205\351\232\234\350\257\212\346\226\255.md" +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\346\225\205\351\232\234\345\244\204\347\220\206.md" @@ -1,20 +1,20 @@ --- -title: Java 故障诊断 +title: Java 虚拟机之故障处理 date: 2020-07-30 17:56:33 order: 22 categories: - Java - - JavaSE + - JavaCore - JVM tags: - Java - - JavaSE + - JavaCore - JVM - 故障诊断 -permalink: /pages/84f329/ +permalink: /pages/78327955/ --- -# Java 故障诊断 +# Java虚拟机之故障处理 ## 故障定位思路 @@ -318,7 +318,7 @@ ls -l /proc/pid/task | wc -l 查看 GC 日志,如果有明显提示 OOM 问题,那就可以根据提示信息,较为快速的定位问题。 -> OOM 定位可以参考:[JVM 内存区域之 OutOfMemoryError](02.JVM内存区域.md#OutOfMemoryError) +> OOM 定位可以参考:[JVM 内存区域之 OutOfMemoryError](JVM内存区域#OutOfMemoryError) ### Minor GC @@ -391,6 +391,6 @@ vmstat 是一款指定采样周期和次数的功能性监测工具,我们可 ## 参考资料 -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - [JAVA 线上故障诊断全套路](https://fredal.xin/java-error-check) -- [从实际案例聊聊 Java 应用的 GC 优化](https://tech.meituan.com/2017/12/29/jvm-optimize.html) \ No newline at end of file +- [从实际案例聊聊 Java 应用的 GC 优化](https://tech.meituan.com/2017/12/29/jvm-optimize.html) diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/04.JVM\347\261\273\345\212\240\350\275\275.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\347\261\273\345\212\240\350\275\275.md" similarity index 98% rename from "source/_posts/01.Java/01.JavaSE/06.JVM/04.JVM\347\261\273\345\212\240\350\275\275.md" rename to "source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\347\261\273\345\212\240\350\275\275.md" index c3a7ec809c..ed86a5b3f8 100644 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/04.JVM\347\261\273\345\212\240\350\275\275.md" +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\347\261\273\345\212\240\350\275\275.md" @@ -1,21 +1,19 @@ --- -title: JVM 类加载 +title: Java 虚拟机之类加载 date: 2020-06-17 15:06:46 order: 04 categories: - Java - - JavaSE + - JavaCore - JVM tags: - Java - - JavaSE + - JavaCore - JVM -permalink: /pages/17aad9/ +permalink: /pages/3e37ea6e/ --- -# JVM 类加载 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200617145849.png) +# Java 虚拟机之类加载 ## 类加载机制 @@ -27,7 +25,7 @@ permalink: /pages/17aad9/ ## 类的生命周期 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200617115110.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408200752774.png) Java 类的完整生命周期包括以下几个阶段: @@ -357,9 +355,7 @@ null 下图展示的类加载器之间的层次关系,称为类加载器的**双亲委派模型(Parents Delegation Model)**。**该模型要求除了顶层的 Bootstrap ClassLoader 外,其余的类加载器都应有自己的父类加载器**。**这里类加载器之间的父子关系一般通过组合(Composition)关系来实现,而不是通过继承(Inheritance)的关系实现**。 -
- -
+![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408200807340.png) **(1)工作过程** @@ -560,6 +556,6 @@ Exception in thread "main" java.lang.ClassCastException: java.lang.Object cannot ## 参考资料 - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) +- [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - [一篇图文彻底弄懂类加载器与双亲委派机制](https://juejin.im/post/5e479c2cf265da575f4e65e4) -- [Jvm 系列(一):Java 类的加载机制](http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html) \ No newline at end of file +- [Jvm 系列(一):Java 类的加载机制](http://www.ityouknow.com/jvm/2017/08/19/class-loading-principle.html) diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/21.JVM\345\256\236\346\210\230.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\350\260\203\344\274\230.md" similarity index 99% rename from "source/_posts/01.Java/01.JavaSE/06.JVM/21.JVM\345\256\236\346\210\230.md" rename to "source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\350\260\203\344\274\230.md" index a6a13ce5f2..8bba32e550 100644 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/21.JVM\345\256\236\346\210\230.md" +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\344\271\213\350\260\203\344\274\230.md" @@ -1,19 +1,18 @@ --- -title: JVM 实战 +title: Java 虚拟机之调优 date: 2019-10-28 22:04:39 -order: 21 categories: - Java - - JavaSE + - JavaCore - JVM tags: - Java - - JavaSE + - JavaCore - JVM -permalink: /pages/9cb60a/ +permalink: /pages/395de487/ --- -# JVM 实战 +# Java 虚拟机之调优 ## JVM 调优概述 @@ -428,7 +427,7 @@ address 即为远程 debug 的监听端口。 ## 参考资料 - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - [从表到里学习 JVM 实现](https://www.douban.com/doulist/2545443/) - [JVM(4):Jvm 调优-命令篇](http://www.importnew.com/23761.html) - [Java 系列笔记(4) - JVM 监控与调优](https://www.cnblogs.com/zhguang/p/Java-JVM-GC.html) diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/01.JVM\344\275\223\347\263\273\347\273\223\346\236\204.md" "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\347\256\200\344\273\213.md" similarity index 76% rename from "source/_posts/01.Java/01.JavaSE/06.JVM/01.JVM\344\275\223\347\263\273\347\273\223\346\236\204.md" rename to "source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\347\256\200\344\273\213.md" index b773a32098..e5a5fafafb 100644 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/01.JVM\344\275\223\347\263\273\347\273\223\346\236\204.md" +++ "b/source/_posts/01.Java/01.JavaCore/06.JVM/Java\350\231\232\346\213\237\346\234\272\347\256\200\344\273\213.md" @@ -1,19 +1,19 @@ --- -title: JVM 体系结构 +title: Java 虚拟机简介 date: 2021-05-24 15:41:47 order: 01 categories: - Java - - JavaSE + - JavaCore - JVM tags: - Java - - JavaSE + - JavaCore - JVM -permalink: /pages/08f153/ +permalink: /pages/f847eacd/ --- -# JVM 体系结构 +# Java 虚拟机简介 > JVM 能跨平台工作,主要是由于 JVM 屏蔽了与各个计算机平台相关的软件、硬件之间的差异。 @@ -72,6 +72,28 @@ Java 虚拟机的性能指标主要有两点: - 一小时内批处理程序完成的工作数量 - 一小时内数据查询完成的数量 +## JVM 内存简介 + +### 物理内存和虚拟内存 + +所谓物理内存就是通常所说的 RAM(随机存储器)。 + +虚拟内存使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间上共享,在逻辑上彼此仍然是隔离的。 + +### 内核空间和用户空间 + +一个计算通常有固定大小的内存空间,但是程序并不能使用全部的空间。因为这些空间被划分为内核空间和用户空间,而程序只能使用用户空间的内存。 + +### 使用内存的 Java 组件 + +Java 启动后,作为一个进程运行在操作系统中。 + +有哪些 Java 组件需要占用内存呢? + +- 堆内存:Java 堆、类和类加载器 +- 栈内存:线程 +- 本地内存:NIO、JNI + ## 参考资料 -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) \ No newline at end of file +- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) diff --git a/source/_posts/01.Java/01.JavaCore/06.JVM/README.md b/source/_posts/01.Java/01.JavaCore/06.JVM/README.md new file mode 100644 index 0000000000..1962e6d883 --- /dev/null +++ b/source/_posts/01.Java/01.JavaCore/06.JVM/README.md @@ -0,0 +1,46 @@ +--- +title: Java JVM +date: 2020-06-04 13:51:01 +categories: + - Java + - JavaCore + - JVM +tags: + - Java + - JavaCore + - JVM +permalink: /pages/5f839231/ +hidden: true +index: false +dir: + order: 6 + link: true +--- + +# JVM 教程 + +> 【Java 虚拟机】总结、整理了个人对于 JVM 的学习、应用心得。 + +## 📖 内容 + +- [Java 虚拟机简介](Java虚拟机简介.md) +- [Java 虚拟机之内存区域](Java虚拟机之内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` +- [Java 虚拟机之垃圾收集](Java虚拟机之垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` +- [Java 虚拟机之字节码](Java虚拟机之字节码.md) - 关键词:`bytecode`、`asm`、`javassist` +- [Java 虚拟机之类加载](Java虚拟机之类加载.md) - 关键词:`ClassLoader`、`双亲委派` +- [Java 虚拟机之工具](Java虚拟机之工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo`、`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` +- [Java 虚拟机之故障处理](Java虚拟机之故障处理.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` +- [Java 虚拟机之调优](Java虚拟机之调优.md) - 关键词:`配置`、`调优` + +## 📚 资料 + +- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) +- [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) +- [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) +- [从表到里学习 JVM 实现](https://www.douban.com/doulist/2545443/) + +## 🚪 传送 + +◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\270\200.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\270\200.md" new file mode 100644 index 0000000000..8017fae319 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\270\200.md" @@ -0,0 +1,1105 @@ +--- +title: Java 基础面试一 +date: 2024-06-18 22:46:20 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 +permalink: /pages/5fe7db38/ +--- + +# Java 基础面试一 + +## Java 常识 + +### Oracle JDK 和 Open JDK + +**典型问题** + +Oracle JDK 和 Open JDK 有什么区别? + +**知识点** + +| | OpenJDK | Oracle JDK | +| -------- | ------------------------------------------------- | ------------------------------------------------------ | +| 是否开源 | 完全开源 | 闭源 | +| 是否免费 | 完全免费 | JDK8u221 之后存在限制 | +| 更新频率 | 一般每 3 个月发布一个版本;不提供 LTS 服务 | 一般每 6 个月发布一个版本;大概每三年推出一个 LTS 版本 | +| 功能性 | Java 11 之后,OracleJDK 和 OpenJDK 的功能基本一致 | | +| 协议 | GPL v2 | BCL/OTN | + +![img](https://i.sstatic.net/mmVJs.png) + +### Java SE 和 Java EE + +**典型问题** + +Java SE 和 Java EE 有什么区别? + +**知识点** + +Java 技术既是一种编程语言,又是一种平台。Java 编程语言是一种具有特定语法和风格的高级面向对象语言。Java 平台是 Java 编程语言应用程序运行的特定环境。 + +- Java SE(Java Platform, Standard Edition) - Java 平台标准版。Java SE 的 API 提供了 Java 编程语言的核心功能。它定义了从 Java 编程语言的基本类型和对象到用于网络、安全、数据库访问、图形用户界面 (GUI) 开发和 XML 解析的高级类的所有内容。除了核心 API 之外,Java SE 平台还包括虚拟机、开发工具、部署技术以及 Java 技术应用程序中常用的其他类库和工具包。 +- Java EE(Java Platform, Enterprise Edition) - Java 平台企业版。Java EE 构建在 Java SE 基础之上。 Java EE 定义了企业级应用程序开发和部署的标准和规范,如:Servlet、JSP、EJB、JDBC、JPA、JTA、JavaMail、JMS。 + +> 摘自 [**Your First Cup**](https://docs.oracle.com/javaee/6/firstcup/doc/gkhoy.html) + +### JDK、JRE、JVM 之间有什么关系 + +**JVM** - Java Virtual Machine 的缩写,即 Java 虚拟机。JVM 是运行 Java 字节码的虚拟机。JVM 不理解 Java 源代码,这就是为什么要将 `*.java` 文件编译为 JVM 可理解的 `*.class` 文件(字节码)。Java 有一句著名的口号:“Write Once, Run Anywhere(一次编写,随处运行)”,JVM 正是其核心所在。实际上,JVM 针对不同的系统(Windows、Linux、MacOS)有不同的实现,目的在于用相同的字节码执行同样的结果。 + +**JRE** - Java Runtime Environment 的缩写,即 Java 运行时环境。它是运行已编译 Java 程序所需的一切的软件包,主要包括 JVM、Java 类库(Class Library)、Java 命令和其他基础结构。但是,它不能用于创建新程序。 + +**JDK** - Java Development Kit 的缩写,即 Java SDK。它不仅包含 JRE 的所有功能,还包含编译器 (javac) 和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 + +> 总结来说,JDK、JRE、JVM 三者的关系是:JDK > JRE > JVM +> +> **JDK = JRE + 开发/调试工具** +> +> **JRE = JVM + Java 类库 + Java 运行库** +> +> **JVM = 类加载系统 + 运行时内存区域 + 执行引擎** + +![enter image description here](https://i.sstatic.net/CBNux.png) + +> 摘自 [stackoverflow 高票问题 - What is the difference between JDK and JRE?](https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre) + +### 什么是字节码?采用字节码的好处是什么? + +在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 `.class` 的文件),它不面向任何特定的处理器,只面向虚拟机。Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以, Java 程序运行时相对来说还是高效的(不过,和 C、 C++,Rust,Go 等语言还是有一定差距的),而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。 + +我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 **JIT(Just in Time Compilation)** 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 。 + +### Java 是编译型语言还是解释型语言? + +结论:**Java 既是编译型语言,也是解释型语言**。 + +知识点: + +(1)什么是编译型语言?什么是解释型语言? + +- [**编译型语言**](https://zh.wikipedia.org/wiki/編譯語言) - 程序在执行之前需要一个专门的编译过程,把程序编译成为机器语言的文件,运行时不需要重新翻译,直接使用编译的结果就行了。一般情况下,编译型语言的执行速度比较快,开发效率比较低。常见的编译型语言有 C、C++、Go 等。 +- [**解释型语言**](https://zh.wikipedia.org/wiki/直譯語言) - 程序不需要编译,只是在程序运行时通过 [解释器](https://zh.wikipedia.org/wiki/直譯器),将代码一句一句解释为机器代码后再执行。一般情况下,解释型语言的执行速度比较慢,开发效率比较高。常见的解释型语言有 JavaScript、Python、Ruby 等。 + +(2)为什么说 Java 既是编译型语言,也是解释型语言 + +Java 语言既具有编译型语言的特征,也具有解释型语言的特征。因此,我们说 Java 是编译和解释并存的。 + +Java 的源代码,首先通过 Javac 编译成为字节码(bytecode),即 `*.java` 文件转为 `*.class` 文件;然后,在运行时,通过 Java 虚拟机(JVM)内嵌的解释器将字节码转换成为最终的机器码来执行。正是由于 JVM 这套机制,使得 Java 可以“一次编写,到处执行(Write once, run anywhere)”。 + +为了改善解释语言的效率而发展出的 [即时编译](https://zh.wikipedia.org/wiki/即時編譯)技术,已经缩小了这两种语言间的差距。这种技术混合了编译语言与解释型语言的优点,它像编译语言一样,先把程序源代码编译成 [字节码](https://zh.wikipedia.org/wiki/字节码)。到执行期时,再将字节码直译,之后执行。[Java](https://zh.wikipedia.org/wiki/Java) 与 [LLVM](https://zh.wikipedia.org/wiki/LLVM) 是这种技术的代表产物。常见的 JVM(如 Hotspot JVM),都提供了 JIT(Just-In-Time)编译器,JIT 能够在运行时将热点代码编译成机器码,这种情况下部分热点代码就属于**编译执行**,而不是解释执行了。 + +> 扩展阅读:[基本功 | Java 即时编译器原理解析及实践](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html) + +### AOT 有什么优点?为什么不全部使用 AOT 呢? + +JDK 9 引入了一种新的编译模式 **AOT(Ahead of Time Compilation)** 。和 JIT 不同的是,这种编译模式会在程序被执行前就将其编译成机器码,属于静态编译(C、 C++,Rust,Go 等语言就是静态编译)。AOT 避免了 JIT 预热等各方面的开销,可以提高 Java 程序的启动速度,避免预热时间长。并且,AOT 还能减少内存占用和增强 Java 程序的安全性(AOT 编译后的代码不容易被反编译和修改),特别适合云原生场景。 + +AOT 的主要优势在于启动时间、内存占用和打包体积。JIT 的主要优势在于具备更高的极限处理能力,可以降低请求的最大延迟。 + +提到 AOT 就不得不提 [GraalVM](https://www.graalvm.org/) 了!GraalVM 是一种高性能的 JDK(完整的 JDK 发行版本),它可以运行 Java 和其他 JVM 语言,以及 JavaScript、Python 等非 JVM 语言。 GraalVM 不仅能提供 AOT 编译,还能提供 JIT 编译。感兴趣的同学,可以去看看 GraalVM 的官方文档:https://www.graalvm.org/latest/docs/。如果觉得官方文档看着比较难理解的话,也可以找一些文章来看看,比如: + +- [基于静态编译构建微服务应用](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw) +- [走向 Native 化:Spring&Dubbo AOT 技术示例与原理讲解](https://cn.dubbo.apache.org/zh-cn/blog/2023/06/28/走向-native-化 springdubbo-aot-技术示例与原理讲解/) + +**既然 AOT 这么多优点,那为什么不全部使用这种编译方式呢?** + +我们前面也对比过 JIT 与 AOT,两者各有优点,只能说 AOT 更适合当下的云原生场景,对微服务架构的支持也比较友好。除此之外,AOT 编译无法支持 Java 的一些动态特性,如反射、动态代理、动态加载、JNI(Java Native Interface)等。然而,很多框架和库(如 Spring、CGLIB)都用到了这些特性。如果只使用 AOT 编译,那就没办法使用这些框架和库了,或者说需要针对性地去做适配和优化。举个例子,CGLIB 动态代理使用的是 ASM 技术,而这种技术大致原理是运行时直接在内存中生成并加载修改后的字节码文件也就是 `.class` 文件,如果全部使用 AOT 提前编译,也就不能使用 ASM 技术了。为了支持类似的动态特性,所以选择使用 JIT 即时编译器。 + +## 注释 + +### Java 有几种注释形式 + +注释用于在源代码中解释代码的作用,可以增强程序的可读性,可维护性。 空白行,或者注释的内容,都会被 Java 编译器忽略掉。 + +Java 注释主要有三种类型: + +- 单行注释 +- 多行注释 +- 文档注释(JavaDoc) + +```java +public class HelloWorld { + /* + * JavaDoc 注释 + */ + public static void main(String[] args) { + // 单行注释 + /* 多行注释 + 1. 注意点 a + 2. 注意点 b + */ + System.out.println("Hello World"); + } +} +``` + +## 数据类型 + +### Java 有哪些值类型? + +Java 中的数据类型有两类: + +- 值类型(又叫内置数据类型,基本数据类型) +- 引用类型(除值类型以外,都是引用类型,包括 `String`、数组等) + +Java 语言提供了 **8** 种基本类型,大致分为 **4** 类:布尔型、字符型、整数型、浮点型。 + +| 基本数据类型 | 分类 | 大小 | 默认值 | 取值范围 | 包装类 | 说明 | +| ------------ | ---------- | ------ | --------- | ----------------------------- | --------- | ------------------------------------------- | +| `boolean` | **布尔型** | - | `false` | {false, true} | Boolean | `boolean` 的大小,是由具体的 JVM 实现来决定的 | +| `char` | **字符型** | 16 bit | `'u0000'` | [0, $2^{16} - 1$] | Character | 存储 Unicode 码,用单引号赋值 | +| `byte` | **整数型** | 8 bit | `0` | [-$2^7$, $2^7 - 1$] | Byte | | +| `short` | **整数型** | 16 bit | `0` | [-$2^{15}$, $2^{15} - 1$] | Short | | +| `int` | **整数型** | 32 bit | `0` | [-$2^{31}$, $2^{31} - 1$] | Integer | | +| `long` | **整数型** | 64 bit | `0L` | [-$2^{63}$, $2^{63} - 1$] | Long | 赋值时一般在数字后加上 `l` 或 `L` | +| `float` | **浮点型** | 32 bit | `0.0f` | [$2^{-149}$, $2^{128} - 1$] | Float | 赋值时必须在数字后加上 `f` 或 `F` | +| `double` | **浮点型** | 64 bit | `0.0d` | [$2^{-1074}$, $2^{1024} - 1$] | Double | 赋值时一般在数字后加 `d` 或 `D` | + +### 什么是装箱、拆箱? + +Java 中为每一种基本数据类型提供了相应的包装类,如下: + +``` +Byte <-> byte +Short <-> short +Integer <-> int +Long <-> long +Float <-> float +Double <-> double +Character <-> char +Boolean <-> boolean +``` + +**引入包装类的目的**就是:提供一种机制,使得**基本数据类型可以与引用类型互相转换**。 + +基本数据类型与包装类的转换被称为装箱和拆箱。 + +- **装箱(boxing)是将值类型转换为引用类型**。例如:`int` 转 `Integer` + - 装箱过程是通过调用包装类的 `valueOf` 方法实现的。 +- **拆箱(unboxing)是将引用类型转换为值类型**。例如:`Integer` 转 `int` + - 拆箱过程是通过调用包装类的 `xxxValue` 方法实现的。(xxx 代表对应的基本数据类型)。 + +### 包装类型的缓存机制了解么? + +Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。 + +`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `True` or `False`。 + +**Integer 缓存源码:** + +``` +public static Integer valueOf(int i) { + if (i >= IntegerCache.low && i <= IntegerCache.high) + return IntegerCache.cache[i + (-IntegerCache.low)]; + return new Integer(i); +} +private static class IntegerCache { + static final int low = -128; + static final int high; + static { + // high value may be configured by property + int h = 127; + } +} +``` + +**`Character` 缓存源码:** + +``` +public static Character valueOf(char c) { + if (c <= 127) { // must cache + return CharacterCache.cache[(int)c]; + } + return new Character(c); +} + +private static class CharacterCache { + private CharacterCache(){} + static final Character cache[] = new Character[127 + 1]; + static { + for (int i = 0; i < cache.length; i++) + cache[i] = new Character((char)i); + } + +} +``` + +**`Boolean` 缓存源码:** + +``` +public static Boolean valueOf(boolean b) { + return (b ? TRUE : FALSE); +} +``` + +如果超出对应范围仍然会去创建新的对象,缓存的范围区间的大小只是在性能和资源之间的权衡。 + +两种浮点数类型的包装类 `Float`,`Double` 并没有实现缓存机制。 + +``` +Integer i1 = 33; +Integer i2 = 33; +System.out.println(i1 == i2);// 输出 true + +Float i11 = 333f; +Float i22 = 333f; +System.out.println(i11 == i22);// 输出 false + +Double i3 = 1.2; +Double i4 = 1.2; +System.out.println(i3 == i4);// 输出 false +``` + +下面我们来看一个问题:下面的代码的输出结果是 `true` 还是 `false` 呢? + +``` +Integer i1 = 40; +Integer i2 = new Integer(40); +System.out.println(i1==i2); +``` + +`Integer i1=40` 这一行代码会发生装箱,也就是说这行代码等价于 `Integer i1=Integer.valueOf(40)` 。因此,`i1` 直接使用的是缓存中的对象。而`Integer i2 = new Integer(40)` 会直接创建新的对象。 + +因此,答案是 `false` 。你答对了吗? + +记住:**所有整型包装类对象之间值的比较,全部使用 equals 方法比较**。 + +### 自动装箱与拆箱的原理是什么? + +```java +Integer a = 10; //装箱 +int b = a; //拆箱 +``` + +上面这两行代码对应的字节码为: + +```java + L1 + + LINENUMBER 8 L1 + + ALOAD 0 + + BIPUSH 10 + + INVOKESTATIC java/lang/Integer.valueOf (I)Ljava/lang/Integer; + + PUTFIELD AutoBoxTest.i : Ljava/lang/Integer; + + L2 + + LINENUMBER 9 L2 + + ALOAD 0 + + ALOAD 0 + + GETFIELD AutoBoxTest.i : Ljava/lang/Integer; + + INVOKEVIRTUAL java/lang/Integer.intValue ()I + + PUTFIELD AutoBoxTest.n : I + + RETURN +``` + +通过字节码代码,不难发现,装箱其实就是调用了 包装类的 `valueOf()` 方法;而拆箱其实就是调用了 `xxxValue()` 方法。 + +因此, + +- `Integer a = 10` 等价于 `Integer a = Integer.valueOf(10)` +- `int b = a` 等价于 `int b = a.intValue()`; + +### 比较包装类型为什么不能用 ==? + +Java 值类型的包装类大部分都使用了缓存机制来提升性能: + +- `Byte`、`Short`、`Integer`、`Long` 这 4 种包装类,默认都创建了数值在 **[-128,127]** 范围之间的相应类型缓存数据; +- `Character` 创建了数值在 **[0,127]** 范围之间的缓存数据; +- `Boolean` 直接返回 `True` or `False`; + +试图装箱的数值,如果超出缓存范围,则会创建新的对象。 + +以 `Long.valueOf` 方法为例: + +```java +public static Long valueOf(long l) { + final int offset = 128; + if (l >= -128 && l <= 127) { // will cache + return LongCache.cache[(int)l + offset]; + } + return new Long(l); +} +``` + +### 为什么浮点数运算的时候会有精度丢失的风险? + +浮点数运算精度丢失代码演示: + +``` +float a = 2.0f - 1.9f; +float b = 1.8f - 1.7f; +System.out.println(a);// 0.100000024 +System.out.println(b);// 0.099999905 +System.out.println(a == b);// false +``` + +为什么会出现这个问题呢? + +这个和计算机保存浮点数的机制有很大关系。我们知道计算机是二进制的,而且计算机在表示一个数字时,宽度是有限的,无限循环的小数存储在计算机时,只能被截断,所以就会导致小数精度发生损失的情况。这也就是解释了为什么浮点数没有办法用二进制精确表示。 + +就比如说十进制下的 0.2 就没办法精确转换成二进制小数: + +``` +// 0.2 转换为二进制数的过程为,不断乘以 2,直到不存在小数为止, +// 在这个计算过程中,得到的整数部分从上到下排列就是二进制的结果。 +0.2 * 2 = 0.4 -> 0 +0.4 * 2 = 0.8 -> 0 +0.8 * 2 = 1.6 -> 1 +0.6 * 2 = 1.2 -> 1 +0.2 * 2 = 0.4 -> 0(发生循环) +... +``` + +### 如何解决浮点数运算的精度丢失问题? + +`BigDecimal` 可以实现对浮点数的运算,不会造成精度丢失。通常情况下,大部分需要浮点数精确运算结果的业务场景(比如涉及到钱的场景)都是通过 `BigDecimal` 来做的。 + +``` +BigDecimal a = new BigDecimal("1.0"); +BigDecimal b = new BigDecimal("0.9"); +BigDecimal c = new BigDecimal("0.8"); + +BigDecimal x = a.subtract(b); +BigDecimal y = b.subtract(c); + +System.out.println(x); /* 0.1 */ +System.out.println(y); /* 0.1 */ +System.out.println(Objects.equals(x, y)); /* true */ +``` + +### 超过 long 整型的数据应该如何表示? + +基本数值类型都有一个表达范围,如果超过这个范围就会有数值溢出的风险。 + +在 Java 中,64 位 long 整型是最大的整数类型。 + +``` +long l = Long.MAX_VALUE; +System.out.println(l + 1); // -9223372036854775808 +System.out.println(l + 1 == Long.MIN_VALUE); // true +``` + +`BigInteger` 内部使用 `int[]` 数组来存储任意大小的整形数据。 + +相对于常规整数类型的运算来说,`BigInteger` 运算的效率会相对较低。 + +## 标识符 + +### 标识符命名规则 + +Java 所有的组成部分都需要名字。类名、变量名以及方法名都被称为标识符。 + +关于 Java 标识符,有以下几点需要注意: + +- 所有的标识符都应该以字母(A-Z 或者 a-z), 美元符($)、或者下划线(\_)开始 +- 首字符之后可以是字母(A-Z 或者 a-z), 美元符($)、下划线(\_)或数字的任何字符组合 +- 关键字不能用作标识符 +- 标识符是大小写敏感的 +- 合法标识符举例:age、$salary、\_value、\_\_1_value +- 非法标识符举例:123abc、-salary + +在 Java 中,标识符通常遵循 [驼峰命名法](https://zh.wikipedia.org/wiki/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB)。 + +- **类名、接口名一般采用大驼峰式命名法(upper camel case)**,即:每一个单字的首字母都采用大写字母,例如:FirstName、LastName、CamelCase。 +- **方法名、变量名一般采用小驼峰式命名法(lower camel case)**,即:第一个单词以小写字母开始;第二个单词的首字母大写,例如:firstName、lastName。 +- **常量名一般采用全大写的蛇形命名法(snake_case)**,即:单词之间用下划线(`_`)分隔,例如:SCREAMING_SNAKE_CASE。 + +### Java 中有哪些关键字? + +下面列出了 Java 保留字,这些保留字不能用于常量、变量、和任何标识符的名称。 + +| 分类 | 关键字 | +| -------------------- | ------------------------------------------------------------------------------------------------------------------------------ | +| 访问级别修饰符 | private、protected、public、default | +| 类,方法和变量修饰符 | abstract、class、extends、final、implements、interface、native、new、static、strictfp、synchronized、transient、volatile、enum | +| 程序控制语句 | break、continue、return、do、while、if、else、for、instanceof、switch、case | +| 错误处理 | assert、try、catch、throw、throws、finally | +| 包相关 | import、package | +| 数据类型 | boolean、byte、char、short、int、long、float、double、enum | +| 变量引用 | super、this、void | +| 其他保留字 | goto、const | + +> **注意:**Java 的 null 不是关键字,类似于 true 和 false,它是一个字面常量,不允许作为标识符使用。 +> +> **官方文档**:https://docs.oracle.com/javase/tutorial/java/nutsandbolts/_keywords.html + +## 变量 + +Java 支持的变量类型有: + +- `局部变量` - 类方法中的变量。 +- `成员变量(也叫实例变量)` - 类方法外的变量,不过没有 `static` 修饰。 +- `静态变量(也叫类变量)` - 类方法外的变量,用 `static` 修饰。 + +```java +public class VariableDemo { + + // 静态变量 + private static String v1 = "静态变量"; + + // 成员变量 + private String v2 = "成员变量"; + + public void test(String v4) { + // 局部变量 + String v3 = "局部变量"; + System.out.println(v1); + System.out.println(v2); + System.out.println(v3); + System.out.println(v4); + } + + public static void main(String[] args) { + VariableDemo demo = new VariableDemo(); + demo.test("参数变量"); + } + +} +``` + +### 成员变量与局部变量的区别? + +- **语法形式**:从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 `public`,`private`,`static` 等修饰符所修饰,而局部变量不能被访问控制修饰符及 `static` 所修饰;但是,成员变量和局部变量都能被 `final` 所修饰。 +- **存储方式**:从变量在内存中的存储方式来看,如果成员变量是使用 `static` 修饰的,那么这个成员变量是属于类的,如果没有使用 `static` 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。 +- **生存时间**:从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动生成,随着方法的调用结束而消亡。 +- **默认值**:从变量是否有默认值来看,成员变量如果没有被赋初始值,则会自动以类型的默认值而赋值(一种情况例外:被 `final` 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。 + +### 为什么成员变量有默认值? + +1. 先不考虑变量类型,如果没有默认值会怎样?变量存储的是内存地址对应的任意随机值,程序读取该值运行会出现意外。 +2. 默认值有两种设置方式:手动和自动,根据第一点,没有手动赋值一定要自动赋值。成员变量在运行时可借助反射等方法手动赋值,而局部变量不行。 +3. 对于编译器(javac)来说,局部变量没赋值很好判断,可以直接报错。而成员变量可能是运行时赋值,无法判断,误报“没默认值”又会影响用户体验,所以采用自动赋默认值。 + +成员变量与局部变量代码示例: + +``` +public class VariableExample { + + // 成员变量 + private String name; + private int age; + + // 方法中的局部变量 + public void method() { + int num1 = 10; // 栈中分配的局部变量 + String str = "Hello, world!"; // 栈中分配的局部变量 + System.out.println(num1); + System.out.println(str); + } + + // 带参数的方法中的局部变量 + public void method2(int num2) { + int sum = num2 + 10; // 栈中分配的局部变量 + System.out.println(sum); + } + + // 构造方法中的局部变量 + public VariableExample(String name, int age) { + this.name = name; // 对成员变量进行赋值 + this.age = age; // 对成员变量进行赋值 + int num3 = 20; // 栈中分配的局部变量 + String str2 = "Hello, " + this.name + "!"; // 栈中分配的局部变量 + System.out.println(num3); + System.out.println(str2); + } +} +``` + +### 静态变量有什么作用? + +静态变量也就是被 `static` 关键字修饰的变量。它可以被类的所有实例共享,无论一个类创建了多少个对象,它们都共享同一份静态变量。也就是说,静态变量只会被分配一次内存,即使创建多个对象,这样可以节省内存。 + +静态变量是通过类名来访问的,例如`StaticVariableExample.staticVar`(如果被 `private`关键字修饰就无法这样访问了)。 + +``` +public class StaticVariableExample { + // 静态变量 + public static int staticVar = 0; +} +``` + +通常情况下,静态变量会被 `final` 关键字修饰成为常量。 + +``` +public class ConstantVariableExample { + // 常量 + public static final int constantVar = 0; +} +``` + +### 字符型常量和字符串常量的区别? + +- **形式** : 字符常量是单引号引起的一个字符,字符串常量是双引号引起的 0 个或若干个字符。 +- **含义** : 字符常量相当于一个整型值 ( ASCII 值), 可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置)。 +- **占内存大小**:字符常量只占 2 个字节;字符串常量占若干个字节。 + +⚠️ 注意 `char` 在 Java 中占两个字节。 + +字符型常量和字符串常量代码示例: + +``` +public class StringExample { + // 字符型常量 + public static final char LETTER_A = 'A'; + + // 字符串常量 + public static final String GREETING_MESSAGE = "Hello, world!"; + public static void main(String[] args) { + System.out.println("字符型常量占用的字节数为:"+Character.BYTES); + System.out.println("字符串常量占用的字节数为:"+GREETING_MESSAGE.getBytes().length); + } +} +``` + +输出: + +``` +字符型常量占用的字节数为:2 +字符串常量占用的字节数为:13 +``` + +## 操作符 + +### 如果移位的位数超过数值所占有的位数会怎样? + +当 int 类型左移/右移位数大于等于 32 位操作时,会先求余(%)后再进行左移/右移操作。也就是说左移/右移 32 位相当于不进行移位操作(32%32=0),左移/右移 42 位相当于左移/右移 10 位(42%32=10)。当 long 类型进行左移/右移操作时,由于 long 对应的二进制是 64 位,因此求余操作的基数也变成了 64。 + +也就是说:`x<<42`等同于`x<<10`,`x>>42`等同于`x>>10`,`x >>>42`等同于`x >>> 10`。 + +**左移运算符代码示例**: + +``` +int i = -1; +System.out.println("初始数据:" + i); +System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i)); +i <<= 10; +System.out.println("左移 10 位后的数据 " + i); +System.out.println("左移 10 位后的数据对应的二进制字符 " + Integer.toBinaryString(i)); +``` + +输出: + +``` +初始数据:-1 +初始数据对应的二进制字符串:11111111111111111111111111111111 +左移 10 位后的数据 -1024 +左移 10 位后的数据对应的二进制字符 11111111111111111111110000000000 +``` + +由于左移位数大于等于 32 位操作时,会先求余(%)后再进行左移操作,所以下面的代码左移 42 位相当于左移 10 位(42%32=10),输出结果和前面的代码一样。 + +``` +int i = -1; +System.out.println("初始数据:" + i); +System.out.println("初始数据对应的二进制字符串:" + Integer.toBinaryString(i)); +i <<= 42; +System.out.println("左移 10 位后的数据 " + i); +System.out.println("左移 10 位后的数据对应的二进制字符 " + Integer.toBinaryString(i)); +``` + +右移运算符使用类似,篇幅问题,这里就不做演示了。 + +## 方法 + +### 什么是方法的返回值?方法有哪几种类型? + +**方法的返回值** 是指我们获取到的某个方法体中的代码执行后产生的结果!(前提是该方法可能产生结果)。返回值的作用是接收出结果,使得它可以用于其他的操作! + +我们可以按照方法的返回值和参数类型将方法分为下面这几种: + +**1、无参数无返回值的方法** + +``` +public void f1() { + //...... +} +// 下面这个方法也没有返回值,虽然用到了 return +public void f(int a) { + if (...) { + // 表示结束方法的执行,下方的输出语句不会执行 + return; + } + System.out.println(a); +} +``` + +**2、有参数无返回值的方法** + +``` +public void f2(Parameter 1, ..., Parameter n) { + //...... +} +``` + +**3、有返回值无参数的方法** + +``` +public int f3() { + //...... + return x; +} +``` + +**4、有返回值有参数的方法** + +``` +public int f4(int a, int b) { + return a * b; +} +``` + +### 静态方法为什么不能调用非静态成员? + +这个需要结合 JVM 的相关知识,主要原因如下: + +1. 静态方法是属于类的,在类加载的时候就会分配内存,可以通过类名直接访问。而非静态成员属于实例对象,只有在对象实例化之后才存在,需要通过类的实例对象去访问。 +2. 在类的非静态成员不存在的时候静态方法就已经存在了,此时调用在内存中还不存在的非静态成员,属于非法操作。 + +``` +public class Example { + // 定义一个字符型常量 + public static final char LETTER_A = 'A'; + + // 定义一个字符串常量 + public static final String GREETING_MESSAGE = "Hello, world!"; + + public static void main(String[] args) { + // 输出字符型常量的值 + System.out.println("字符型常量的值为:" + LETTER_A); + + // 输出字符串常量的值 + System.out.println("字符串常量的值为:" + GREETING_MESSAGE); + } +} +``` + +### 静态方法和实例方法有何不同? + +**1、调用方式** + +在外部调用静态方法时,可以使用 `类名。方法名` 的方式,也可以使用 `对象。方法名` 的方式,而实例方法只有后面这种方式。也就是说,**调用静态方法可以无需创建对象** 。 + +不过,需要注意的是一般不建议使用 `对象。方法名` 的方式来调用静态方法。这种方式非常容易造成混淆,静态方法不属于类的某个对象而是属于这个类。 + +因此,一般建议使用 `类名。方法名` 的方式来调用静态方法。 + +``` +public class Person { + public void method() { + //...... + } + + public static void staicMethod(){ + //...... + } + public static void main(String[] args) { + Person person = new Person(); + // 调用实例方法 + person.method(); + // 调用静态方法 + Person.staicMethod() + } +} +``` + +**2、访问类成员是否存在限制** + +静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),不允许访问实例成员(即实例成员变量和实例方法),而实例方法不存在这个限制。 + +### 重载和重写有什么区别? + +> 重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理 +> +> 重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法 + +#### 重载 + +发生在同一个类中(或者父类和子类之间),方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。 + +《Java 核心技术》这本书是这样介绍重载的: + +> 如果多个方法(比如 `StringBuilder` 的构造方法)有相同的名字、不同的参数, 便产生了重载。 +> +> ``` +> StringBuilder sb = new StringBuilder(); +> StringBuilder sb2 = new StringBuilder("HelloWorld"); +> ``` +> +> 编译器必须挑选出具体执行哪个方法,它通过用各个方法给出的参数类型与特定方法调用所使用的值类型进行匹配来挑选出相应的方法。 如果编译器找不到匹配的参数, 就会产生编译时错误, 因为根本不存在匹配, 或者没有一个比其他的更好(这个过程被称为重载解析 (overloading resolution))。 +> +> Java 允许重载任何方法, 而不只是构造器方法。 + +综上:重载就是同一个类中多个同名方法根据不同的传参来执行不同的逻辑处理。 + +#### 重写 + +重写发生在运行期,是子类对父类的允许访问的方法的实现过程进行重新编写。 + +1. 方法名、参数列表必须相同,子类方法返回值类型应比父类方法返回值类型更小或相等,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。 +2. 如果父类方法访问修饰符为 `private/final/static` 则子类就不能重写该方法,但是被 `static` 修饰的方法能够被再次声明。 +3. 构造方法无法被重写 + +#### 总结 + +综上:**重写就是子类对父类方法的重新改造,外部样子不能改变,内部逻辑可以改变。** + +| 区别点 | 重载方法 | 重写方法 | +| ---------- | -------- | ---------------------------------------------------------------- | +| 发生范围 | 同一个类 | 子类 | +| 参数列表 | 必须修改 | 一定不能修改 | +| 返回类型 | 可修改 | 子类方法返回值类型应比父类方法返回值类型更小或相等 | +| 异常 | 可修改 | 子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; | +| 访问修饰符 | 可修改 | 一定不能做更严格的限制(可以降低限制) | +| 发生阶段 | 编译期 | 运行期 | + +**方法的重写要遵循“两同两小一大”**(以下内容摘录自《疯狂 Java 讲义》,[issue#892](https://github.com/Snailclimb/JavaGuide/issues/892) ): + +- “两同”即方法名相同、形参列表相同; +- “两小”指的是子类方法返回值类型应比父类方法返回值类型更小或相等,子类方法声明抛出的异常类应比父类方法声明抛出的异常类更小或相等; +- “一大”指的是子类方法的访问权限应比父类方法的访问权限更大或相等。 + +⭐️ 关于 **重写的返回值类型** 这里需要额外多说明一下,上面的表述不太清晰准确:如果方法的返回类型是 void 和基本数据类型,则返回值重写时不可修改。但是如果方法的返回值是引用类型,重写时是可以返回该引用类型的子类的。 + +``` +public class Hero { + public String name() { + return "超级英雄"; + } +} +public class SuperMan extends Hero{ + @Override + public String name() { + return "超人"; + } + public Hero hero() { + return new Hero(); + } +} + +public class SuperSuperMan extends SuperMan { + public String name() { + return "超级超级英雄"; + } + + @Override + public SuperMan hero() { + return new SuperMan(); + } +} +``` + +### 什么是可变长参数? + +从 Java5 开始,Java 支持定义可变长参数,所谓可变长参数就是允许在调用方法时传入不定长度的参数。就比如下面这个方法就可以接受 0 个或者多个参数。 + +``` +public static void method1(String... args) { + //...... +} +``` + +另外,可变参数只能作为函数的最后一个参数,但其前面可以有也可以没有任何其他参数。 + +``` +public static void method2(String arg1, String... args) { + //...... +} +``` + +**遇到方法重载的情况怎么办呢?会优先匹配固定参数还是可变参数的方法呢?** + +答案是会优先匹配固定参数的方法,因为固定参数的方法匹配度更高。 + +我们通过下面这个例子来证明一下。 + +``` +/** + * 微信搜 JavaGuide 回复"面试突击"即可免费领取个人原创的 Java 面试手册 + * + * @author Guide 哥 + * @date 2021/12/13 16:52 + **/ +public class VariableLengthArgument { + + public static void printVariable(String... args) { + for (String s : args) { + System.out.println(s); + } + } + + public static void printVariable(String arg1, String arg2) { + System.out.println(arg1 + arg2); + } + + public static void main(String[] args) { + printVariable("a", "b"); + printVariable("a", "b", "c", "d"); + } +} +``` + +输出: + +``` +ab +a +b +c +d +``` + +另外,Java 的可变参数编译后实际会被转换成一个数组,我们看编译后生成的 `class`文件就可以看出来了。 + +``` +public class VariableLengthArgument { + + public static void printVariable(String... args) { + String[] var1 = args; + int var2 = args.length; + + for(int var3 = 0; var3 < var2; ++var3) { + String s = var1[var3]; + System.out.println(s); + } + + } + // ...... +} +``` + +## 异常 + +![](https://cdn.javarush.com/images/article/2e4a84d4-3d29-41a2-b6f9-32ae87e9ee96/1024.webp) + +### Exception 和 Error 有什么区别? + +在 Java 中,所有的异常都有一个共同的祖先 `java.lang` 包中的 `Throwable` 类。`Throwable` 类有两个重要的子类: + +- **`Exception`** - 程序本身可以处理的异常,可以通过 `catch` 来进行捕获。`Exception` 又分为**检查**(checked)异常和**非检查**(unchecked)异常,检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。 +- **`Error`** - `Error` 属于程序无法处理的错误。例如 Java 虚拟机运行错误(`Virtual MachineError`)、虚拟机内存不够错误(`OutOfMemoryError`)、类定义错误(`NoClassDefFoundError`)等 。这些异常发生时,Java 虚拟机(JVM)一般会选择线程终止。 + +### Checked Exception 和 Unchecked Exception 有什么区别? + +**Checked Exception** 即 受检查异常 ,Java 代码在编译过程中,如果受检查异常没有被 `catch`或者`throws` 关键字处理的话,就没办法通过编译。 + +除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常 。常见的受检查异常有:IO 相关的异常、`ClassNotFoundException`、`SQLException`...。 + +**Unchecked Exception** 即 **不受检查异常** ,Java 代码在编译过程中 ,我们即使不处理不受检查异常也可以正常通过编译。 + +`RuntimeException` 及其子类都统称为非受检查异常,常见的有(建议记下来,日常开发中会经常用到): + +- `NullPointerException`(空指针错误) +- `IllegalArgumentException`(参数错误比如方法入参类型错误) +- `NumberFormatException`(字符串转换为数字格式错误,`IllegalArgumentException`的子类) +- `ArrayIndexOutOfBoundsException`(数组越界错误) +- `ClassCastException`(类型转换错误) +- `ArithmeticException`(算术错误) +- `SecurityException` (安全错误比如权限不够) +- `UnsupportedOperationException`(不支持的操作错误比如重复创建同一用户) +- …… + +[![img](https://camo.githubusercontent.com/2179fd845ca1fc9c022437576fd996dfeddb7722d41b32aa3d799199a2490d81/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f62617369732f756e636865636b65642d657863657074696f6e2e706e67)](https://camo.githubusercontent.com/2179fd845ca1fc9c022437576fd996dfeddb7722d41b32aa3d799199a2490d81/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f62617369732f756e636865636b65642d657863657074696f6e2e706e67) + +### Throwable 类常用方法有哪些? + +- `String getMessage()`: 返回异常发生时的简要描述 +- `String toString()`: 返回异常发生时的详细信息 +- `String getLocalizedMessage()`: 返回异常对象的本地化信息。使用 `Throwable` 的子类覆盖这个方法,可以生成本地化信息。如果子类没有覆盖该方法,则该方法返回的信息与 `getMessage()`返回的结果相同 +- `void printStackTrace()`: 在控制台上打印 `Throwable` 对象封装的异常信息 + +### try-catch-finally 如何使用? + +- `try`块:用于捕获异常。其后可接零个或多个 `catch` 块,如果没有 `catch` 块,则必须跟一个 `finally` 块。 +- `catch`块:用于处理 try 捕获到的异常。 +- `finally` 块:无论是否捕获或处理异常,`finally` 块里的语句都会被执行。当在 `try` 块或 `catch` 块中遇到 `return` 语句时,`finally` 语句块将在方法返回之前被执行。 + +代码示例: + +``` +try { + System.out.println("Try to do something"); + throw new RuntimeException("RuntimeException"); +} catch (Exception e) { + System.out.println("Catch Exception -> " + e.getMessage()); +} finally { + System.out.println("Finally"); +} +``` + +输出: + +``` +Try to do something +Catch Exception -> RuntimeException +Finally +``` + +**注意:不要在 finally 语句块中使用 return!** 当 try 语句和 finally 语句中都有 return 语句时,try 语句块中的 return 语句会被忽略。这是因为 try 语句中的 return 返回值会先被暂存在一个本地变量中,当执行到 finally 语句中的 return 之后,这个本地变量的值就变为了 finally 语句中的 return 返回值。 + +[jvm 官方文档](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.10.2.5) 中有明确提到: + +> If the `try` clause executes a _return_, the compiled code does the following: +> +> 1. Saves the return value (if any) in a local variable. +> 2. Executes a _jsr_ to the code for the `finally` clause. +> 3. Upon return from the `finally` clause, returns the value saved in the local variable. + +代码示例: + +``` +public static void main(String[] args) { + System.out.println(f(2)); +} + +public static int f(int value) { + try { + return value * value; + } finally { + if (value == 2) { + return 0; + } + } +} +``` + +输出: + +``` +0 +``` + +### finally 中的代码一定会执行吗? + +不一定的!在某些情况下,finally 中的代码不会被执行。 + +就比如说 finally 之前虚拟机被终止运行的话,finally 中的代码就不会被执行。 + +``` +try { + System.out.println("Try to do something"); + throw new RuntimeException("RuntimeException"); +} catch (Exception e) { + System.out.println("Catch Exception -> " + e.getMessage()); + // 终止当前正在运行的 Java 虚拟机 + System.exit(1); +} finally { + System.out.println("Finally"); +} +``` + +输出: + +``` +Try to do something +Catch Exception -> RuntimeException +``` + +另外,在以下 2 种特殊情况下,`finally` 块的代码也不会被执行: + +1. 程序所在的线程死亡。 +2. 关闭 CPU。 + +相关 issue:[#190](https://github.com/Snailclimb/JavaGuide/issues/190)。 + +🧗🏻 进阶一下:从字节码角度分析`try catch finally`这个语法糖背后的实现原理。 + +### 如何使用 `try-with-resources` 代替`try-catch-finally`? + +1. **适用范围(资源的定义):** 任何实现 `java.lang.AutoCloseable`或者 `java.io.Closeable` 的对象 +2. **关闭资源和 finally 块的执行顺序:** 在 `try-with-resources` 语句中,任何 catch 或 finally 块在声明的资源关闭后运行 + +《Effective Java》中明确指出: + +> 面对必须要关闭的资源,我们总是应该优先使用 `try-with-resources` 而不是`try-finally`。随之产生的代码更简短,更清晰,产生的异常对我们也更有用。`try-with-resources`语句让我们更容易编写必须要关闭的资源的代码,若采用`try-finally`则几乎做不到这点。 + +Java 中类似于`InputStream`、`OutputStream`、`Scanner`、`PrintWriter`等的资源都需要我们调用`close()`方法来手动关闭,一般情况下我们都是通过`try-catch-finally`语句来实现这个需求,如下: + +``` +//读取文本文件的内容 +Scanner scanner = null; +try { + scanner = new Scanner(new File("D://read.txt")); + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException e) { + e.printStackTrace(); +} finally { + if (scanner != null) { + scanner.close(); + } +} +``` + +使用 Java 7 之后的 `try-with-resources` 语句改造上面的代码: + +``` +try (Scanner scanner = new Scanner(new File("test.txt"))) { + while (scanner.hasNext()) { + System.out.println(scanner.nextLine()); + } +} catch (FileNotFoundException fnfe) { + fnfe.printStackTrace(); +} +``` + +当然多个资源需要关闭的时候,使用 `try-with-resources` 实现起来也非常简单,如果你还是用`try-catch-finally`可能会带来很多问题。 + +通过使用分号分隔,可以在`try-with-resources`块中声明多个资源。 + +``` +try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt"))); + BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) { + int b; + while ((b = bin.read()) != -1) { + bout.write(b); + } +} +catch (IOException e) { + e.printStackTrace(); +} +``` + +### NoClassDefFoundError 和 ClassNotFoundException 有什么区别 + +`NoClassDefFoundError`是一个 Error,而 `ClassNOtFoundException` 是一个 Exception。 + +`ClassNotFoundException` 产生的原因: + +- 使用 `Class.forName`、`ClassLoader.loadClass`、`ClassLOader.findSystemClass` 方法动态加载类,如果这个类没有被找到,那么就会在运行时抛出 `ClassNotFoundException` 异常; +- 当一个类已经被某个类加载器加载到内存中了,此时另一个类加载器又尝试着动态地从同一个包中加载这个类。 + +`NoClassDefFoundError` 产生的原因:当 JVM 或 `ClassLoader` 试图加载类,却找不到类的定义时(编译时存在,运行时找不到),抛出异常。 + +### 异常使用有哪些需要注意的地方? + +- 不要把异常定义为静态变量,因为这样会导致异常栈信息错乱。每次手动抛出异常,我们都需要手动 new 一个异常对象抛出。 +- 抛出的异常信息一定要有意义。 +- 建议抛出更加具体的异常比如字符串转换为数字格式错误的时候应该抛出`NumberFormatException`而不是其父类`IllegalArgumentException`。 +- 避免重复记录日志:如果在捕获异常的地方已经记录了足够的信息(包括异常类型、错误信息和堆栈跟踪等),那么在业务代码中再次抛出这个异常时,就不应该再次记录相同的错误信息。重复记录日志会使得日志文件膨胀,并且可能会掩盖问题的实际原因,使得问题更难以追踪和解决。 +- …… + +## 参考资料 + +- **书籍** + - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) + - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) + - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) + - [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\270\211.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\270\211.md" new file mode 100644 index 0000000000..52c812c2f1 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\270\211.md" @@ -0,0 +1,406 @@ +--- +title: Java 基础面试三 +date: 2024-07-12 08:18:58 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 +permalink: /pages/2fad4724/ +--- + +# Java 基础面试三 + +## 泛型 + +### 什么是泛型?有什么作用? + +**Java 泛型(Generics)** 是 JDK 5 中引入的一个新特性。使用泛型参数,可以增强代码的可读性以及稳定性。 + +编译器可以对泛型参数进行检测,并且通过泛型参数可以指定传入的对象类型。比如 `ArrayList persons = new ArrayList()` 这行代码就指明了该 `ArrayList` 对象只能传入 `Person` 对象,如果传入其他类型的对象就会报错。 + +```java +ArrayList extends AbstractList +``` + +并且,原生 `List` 返回类型是 `Object` ,需要手动转换类型才能使用,使用泛型后编译器自动转换。 + +### 泛型的使用方式有哪几种? + +泛型一般有三种使用方式:**泛型类**、**泛型接口**、**泛型方法**。 + +**1. 泛型类**: + +``` +//此处 T 可以随便写为任意标识,常见的如 T、E、K、V 等形式的参数常用于表示泛型 +//在实例化泛型类时,必须指定 T 的具体类型 +public class Generic{ + + private T key; + + public Generic(T key) { + this.key = key; + } + + public T getKey(){ + return key; + } +} +``` + +如何实例化泛型类: + +``` +Generic genericInteger = new Generic(123456); +``` + +**2. 泛型接口**: + +``` +public interface Generator { + public T method(); +} +``` + +实现泛型接口,不指定类型: + +``` +class GeneratorImpl implements Generator{ + @Override + public T method() { + return null; + } +} +``` + +实现泛型接口,指定类型: + +``` +class GeneratorImpl implements Generator{ + @Override + public String method() { + return "hello"; + } +} +``` + +**3. 泛型方法**: + +``` + public static < E > void printArray( E[] inputArray ) + { + for ( E element : inputArray ){ + System.out.printf( "%s ", element ); + } + System.out.println(); + } +``` + +使用: + +``` +// 创建不同类型数组:Integer, Double 和 Character +Integer[] intArray = { 1, 2, 3 }; +String[] stringArray = { "Hello", "World" }; +printArray( intArray ); +printArray( stringArray ); +``` + +> 注意:`public static < E > void printArray( E[] inputArray )` 一般被称为静态泛型方法;在 java 中泛型只是一个占位符,必须在传递类型后才能使用。类在实例化时才能真正的传递类型参数,由于静态方法的加载先于类的实例化,也就是说类中的泛型还没有传递真正的类型参数,静态的方法的加载就已经完成了,所以静态泛型方法是没有办法使用类上声明的泛型的。只能使用自己声明的 `` + +## 反射 + +### 何谓反射? + +如果说大家研究过框架的底层原理或者咱们自己写过框架的话,一定对反射这个概念不陌生。反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。 + +反射 (Reflection) 是 Java 程序开发语言的特征之一,它允许运行中的 Java 程序获取自身的信息,并且可以操作类或对象的内部属性。 + +**通过反射机制,可以在运行时访问 Java 对象的属性,方法,构造方法等。** + +### 反射的应用场景? + +反射的主要应用场景有: + +- **开发通用框架** - 反射最重要的用途就是开发各种通用框架。很多框架(比如 Spring)都是配置化的(比如通过 XML 文件配置 JavaBean、Filter 等),为了保证框架的通用性,它们可能需要根据配置文件加载不同的对象或类,调用不同的方法,这个时候就必须用到反射——运行时动态加载需要加载的对象。 +- **动态代理** - 在切面编程(AOP)中,需要拦截特定的方法,通常,会选择动态代理方式。这时,就需要反射技术来实现了。 +- **注解** - 注解本身仅仅是起到标记作用,它需要利用反射机制,根据注解标记去调用注解解释器,执行行为。如果没有反射机制,注解并不比注释更有用。 +- **可扩展性功能** - 应用程序可以通过使用完全限定名称创建可扩展性对象实例来使用外部的用户定义类。 + +像咱们平时大部分时候都是在写业务代码,很少会接触到直接使用反射机制的场景。但是!这并不代表反射没有用。相反,正是因为反射,你才能这么轻松地使用各种框架。像 Spring/Spring Boot、MyBatis 等等框架中都大量使用了反射机制。 + +**这些框架中也大量使用了动态代理,而动态代理的实现也依赖反射。** + +比如下面是通过 JDK 实现动态代理的示例代码,其中就使用了反射类 `Method` 来调用指定的方法。 + +``` +public class DebugInvocationHandler implements InvocationHandler { + /** + * 代理类中的真实对象 + */ + private final Object target; + + public DebugInvocationHandler(Object target) { + this.target = target; + } + + public Object invoke(Object proxy, Method method, Object[] args) throws InvocationTargetException, IllegalAccessException { + System.out.println("before method " + method.getName()); + Object result = method.invoke(target, args); + System.out.println("after method " + method.getName()); + return result; + } +} +``` + +另外,像 Java 中的一大利器 **注解** 的实现也用到了反射。 + +为什么你使用 Spring 的时候 ,一个`@Component`注解就声明了一个类为 Spring Bean 呢?为什么你通过一个 `@Value`注解就读取到配置文件中的值呢?究竟是怎么起作用的呢? + +这些都是因为你可以基于反射分析类,然后获取到类/属性/方法/方法的参数上的注解。你获取到注解之后,就可以做进一步的处理。 + +### 反射的优缺点? + +反射可以让我们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利。 + +不过,反射让我们在运行时有了分析操作类的能力的同时,也产生了一些问题: + +- **性能开销** - 由于反射涉及动态解析的类型,因此无法执行某些 Java 虚拟机优化。因此,反射操作的性能要比非反射操作的性能要差,应该在性能敏感的应用程序中频繁调用的代码段中避免。 +- **破坏封装性** - 反射调用方法时可以忽略权限检查,因此可能会破坏封装性而导致安全问题。 +- **内部曝光** - 由于反射允许代码执行在非反射代码中非法的操作,例如访问私有字段和方法,所以反射的使用可能会导致意想不到的副作用,这可能会导致代码功能失常并可能破坏可移植性。反射代码打破了抽象,因此可能会随着平台的升级而改变行为。 + +相关阅读:[Java Reflection: Why is it so slow?](https://stackoverflow.com/questions/1392351/java-reflection-why-is-it-so-slow) 。 + +### ⭐ 创建实例 + +> 反射创建实例有几种方式? + +通过反射来创建实例对象主要有两种方式: + +- 用 `Class` 对象的 `newInstance` 方法。 +- 用 `Constructor` 对象的 `newInstance` 方法。 + +### ⭐ 加载实例 + +> 加载实例有几种方式? +> +> Class.forName("className") 和 ClassLoader.laodClass("className") 有什么区别? + +- `Class.forName("className")` 加载的是已经初始化到 JVM 中的类。 +- `ClassLoader.loadClass("className")` 装载的是还没有初始化到 JVM 中的类。 + +### ⭐⭐ 动态代理 + +> 什么是动态代理?动态代理有几种实现方式?有什么特点? +> +> JDK 动态代理和 CGLIB 动态代理有什么区别? + +(1)什么是动态代理? + +动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。 + +(2)为什么需要动态代理? + +有动态就会有静态,静态代理其实就是指设计模式中的代理模式。代理模式为其他对象提供一种代理以控制对这个对象的访问。 + +静态代理模式在访问无法访问的资源时,虽然可以增强现有的接口功能,但是大量使用这种静态代理,会使我们系统内的类的规模增大,并且不易维护;此外,由于 Proxy 和 RealSubject 的功能本质上是相同的,Proxy 只是起到了中介的作用,这种代理在系统中的存在,导致系统结构比较臃肿和松散。 + +(3)实现动态代理的方式 + +实现动态代理的方式很多,比如 JDK 自身提供的动态代理,就是主要利用了上面提到的反射机制。还有其他的实现方式,比如利用传说中更高性能的字节码操作机制,类似 ASM、cglib(基于 ASM)、Javassist 等。 + +(1)JDK 方式 + +代理类与委托类实现同一接口,主要是通过代理类实现 `InvocationHandler` 并重写 `invoke` 方法来进行动态代理的,在 `invoke` 方法中将对方法进行处理。 + +JDK 动态代理特点: + +- 优点:相对于静态代理模式,不需要硬编码接口,代码复用率高。 +- 缺点:强制要求代理类实现 `InvocationHandler` 接口。 + +(2)CGLIB + +CGLIB 底层,其实是借助了 ASM 这个强大的 Java 字节码框架去进行字节码增强操作。 + +CGLIB 动态代理特点: + +优点:使用字节码增强,比 JDK 动态代理方式性能高。可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口。 + +缺点:不能对 `final` 类以及 `final` 方法进行代理。 + +## 注解 + +### 何谓注解? + +`Annotation` (注解) 是 Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量,提供某些信息供程序在编译或者运行时使用。 + +注解本质是一个继承了`Annotation` 的特殊接口: + +``` +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.SOURCE) +public @interface Override { + +} + +public interface Override extends Annotation{ + +} +``` + +JDK 提供了很多内置的注解(比如 `@Override`、`@Deprecated`),同时,我们还可以自定义注解。 + +### 注解的解析方法有哪几种? + +注解只有被解析之后才会生效,常见的解析方法有两种: + +- **编译期直接扫描**:编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用`@Override` 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。 +- **运行期通过反射处理**:像框架中自带的注解(比如 Spring 框架的 `@Value`、`@Component`) 都是通过反射来进行处理的。 + +## SPI + +### 何谓 SPI? + +SPI 即 Service Provider Interface ,字面意思就是:“服务提供者的接口”,我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。 + +SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 + +很多框架都使用了 Java 的 SPI 机制,比如:Spring 框架、数据库加载驱动、日志接口、以及 Dubbo 的扩展实现等等。 + +### SPI 和 API 有什么区别? + +**SPI** 主要关注于组件之间的松耦合和可插拔性,通过接口的定义和实现分离,提供了一种机制来实现动态加载和替换;**API** 则关注于软件组件之间的交互和集成,提供了一种标准化的方式来使用和操作其他软件组件的功能。 + +## 序列化 + +### 什么是序列化?什么是反序列化? + +如果我们需要持久化 Java 对象比如将 Java 对象保存在文件中,或者在网络传输 Java 对象,这些场景都需要用到序列化。 + +简单来说: + +- **序列化**:将数据结构或对象转换成二进制字节流的过程 +- **反序列化**:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程 + +对于 Java 这种面向对象编程语言来说,我们序列化的都是对象(Object)也就是实例化后的类 (Class),但是在 C++这种半面向对象的语言中,struct(结构体)定义的是数据结构类型,而 class 对应的是对象类型。 + +下面是序列化和反序列化常见应用场景: + +- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化; +- 将对象存储到文件之前需要进行序列化,将对象从文件中读取出来需要进行反序列化; +- 将对象存储到数据库(如 Redis)之前需要用到序列化,将对象从缓存数据库中读取出来需要反序列化; +- 将对象存储到内存之前需要进行序列化,从内存中读取出来之后需要进行反序列化。 + +维基百科是如是介绍序列化的: + +> **序列化**(serialization)在计算机科学的数据处理中,是指将数据结构或对象状态转换成可取用格式(例如存成文件,存于缓冲,或经由网络中发送),以留待后续在相同或另一台计算机环境中,能恢复原先状态的过程。依照序列化格式重新获取字节的结果时,可以利用它来产生与原始对象相同语义的副本。对于许多对象,像是使用大量引用的复杂对象,这种序列化重建的过程并不容易。面向对象中的对象序列化,并不概括之前原始对象所关系的函数。这种过程也称为对象编组(marshalling)。从一系列字节提取数据结构的反向操作,是反序列化(也称为解编组、deserialization、unmarshalling)。 + +综上:**序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。** + +### 如果有些字段不想进行序列化怎么办? + +对于不想进行序列化的变量,使用 `transient` 关键字修饰。 + +`transient` 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 `transient` 修饰的变量值不会被持久化和恢复。 + +关于 `transient` 还有几点注意: + +- `transient` 只能修饰变量,不能修饰类和方法。 +- `transient` 修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰 `int` 类型,那么反序列后结果就是 `0`。 +- `static` 变量因为不属于任何对象 (Object),所以无论有没有 `transient` 关键字修饰,均不会被序列化。 + +### 常见序列化协议有哪些? + +JDK 自带的序列化方式一般不会用 ,因为序列化效率低并且存在安全问题。比较常用的序列化协议有 Hessian、Kryo、Protobuf、ProtoStuff,这些都是基于二进制的序列化协议。 + +像 JSON 和 XML 这种属于文本类序列化方式。虽然可读性比较好,但是性能较差,一般不会选择。 + +### 为什么不推荐使用 JDK 自带的序列化? + +我们很少或者说几乎不会直接使用 JDK 自带的序列化方式,主要原因有下面这些原因: + +- **不支持跨语言调用** : 如果调用的是其他语言开发的服务的时候就不支持了。 +- **性能差**:相比于其他序列化框架性能更低,主要原因是序列化之后的字节数组体积较大,导致传输成本加大。 +- **存在安全问题**:序列化和反序列化本身并不存在问题。但当输入的反序列化的数据可被用户控制,那么攻击者即可通过构造恶意输入,让反序列化产生非预期的对象,在此过程中执行构造的任意代码。相关阅读:[应用安全:JAVA 反序列化漏洞之殇](https://cryin.github.io/blog/secure-development-java-deserialization-vulnerability/) 。 + +## 语法糖 + +### 什么是语法糖? + +**语法糖(Syntactic sugar)** 代指的是编程语言为了方便程序员开发程序而设计的一种特殊语法,这种语法对编程语言的功能并没有影响。实现相同的功能,基于语法糖写出来的代码往往更简单简洁且更易阅读。 + +举个例子,Java 中的 `for-each` 就是一个常用的语法糖,其原理其实就是基于普通的 for 循环和迭代器。 + +```java +String[] strs = {"JavaGuide", "公众号:JavaGuide", "博客:https://javaguide.cn/"}; +for (String s : strs) { + System.out.println(s); +} +``` + +不过,JVM 其实并不能识别语法糖,Java 语法糖要想被正确执行,需要先通过编译器进行解糖,也就是在程序编译阶段将其转换成 JVM 认识的基本语法。这也侧面说明,Java 中真正支持语法糖的是 Java 编译器而不是 JVM。如果你去看`com.sun.tools.javac.main.JavaCompiler`的源码,你会发现在`compile()`中有一个步骤就是调用`desugar()`,这个方法就是负责解语法糖的实现的。 + +### Java 中有哪些常见的语法糖? + +Java 中最常用的语法糖主要有泛型、自动拆装箱、变长参数、枚举、内部类、增强 for 循环、try-with-resources 语法、lambda 表达式等。 + +## IO + +### Java 提供了哪些 IO 方式? + +- **BIO** - 优点是代码比较简单、直观;缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。 + - 传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,如:字节流(InputStream/OutputStream)、字符流(Reader/Writer)、File、RandomAccessFile 等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。 + - 很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。 + - BufferedOutputStream 等带缓冲区的实现,可以避免频繁的磁盘读写,进而提高 IO 处理效率。这种设计利用了缓冲区,将批量数据进行一次操作,但在使用中千万别忘了 flush。 + - 很多 IO 工具类都实现了 Closeable 接口,因为需要进行资源的释放。需要利用 try-with-resources、 try-finally 等机制保证资源被明确关闭,否则将导致资源无法被释放。 +- **NIO** - java.nio 包,提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。 + - Buffer,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的 Buffer 实现。 + - Channel,类似在 Linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支持批量式 IO 操作的一种抽象。 + - File 或者 Socket,通常被认为是比较高层次的抽象,而 Channel 则是更加底层的一种抽象,这也使得 NIO 得以充分利用现代操作系统底层机制,获得特定场景的性能优化,例如,DMA(Direct Memory Access)等。不同层次的抽象是相互关联的,我们可以通过 Socket 获取 Channel,反之亦然。 + - Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。Selector 同样是基于底层操作系统机制,不同模式、不同版本都存在区别。Linux 上依赖于 [epoll](http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java),Windows 上 NIO2(AIO)模式则是依赖于 [iocp](http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java)。 +- **AIO** - 在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。 + +### NIO 如何实现多路复用? + +```java + public class NIOServer extends Thread { + public void run() { + try (Selector selector = Selector.open(); + ServerSocketChannel serverSocket = ServerSocketChannel.open();) {// 创建 Selector 和 Channel + serverSocket.bind(new InetSocketAddress(InetAddress.getLocalHost(), 8888)); + serverSocket.configureBlocking(false); + // 注册到 Selector,并说明关注点 + serverSocket.register(selector, SelectionKey.OP_ACCEPT); + while (true) { + selector.select();// 阻塞等待就绪的 Channel,这是关键点之一 + Set selectedKeys = selector.selectedKeys(); + Iterator iter = selectedKeys.iterator(); + while (iter.hasNext()) { + SelectionKey key = iter.next(); + // 生产系统中一般会额外进行就绪状态检查 + sayHelloWorld((ServerSocketChannel) key.channel()); + iter.remove(); + } + } + } catch (IOException e) { + e.printStackTrace(); + } + } + private void sayHelloWorld(ServerSocketChannel server) throws IOException { + try (SocketChannel client = server.accept();) { client.write(Charset.defaultCharset().encode("Hello world!")); + } + } + // 省略了与前面类似的 main + } +``` + +这个非常精简的样例掀开了 NIO 多路复用的流程 + +- 首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。 +- 然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。 + - **注意**,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。 +- Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。 +- 在 sayHelloWorld 方法中,通过 SocketChannel 和 Buffer 进行数据操作,在本例中是发送了一段字符串。 \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\272\214.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\272\214.md" new file mode 100644 index 0000000000..868ad5bceb --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\237\272\347\241\200\351\235\242\350\257\225\344\272\214.md" @@ -0,0 +1,663 @@ +--- +title: Java 基础面试二 +date: 2024-07-03 07:44:02 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 +permalink: /pages/dd562d6e/ +--- + +# Java 基础面试二 + +## 面向对象 + +### 面向对象和面向过程 + +**典型问题** + +面向对象编程和面向过程编程有什么区别? + +**知识点** + +二者的主要区别在于解决问题的方式不同: + +- 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。 +- 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。 + +另外,面向对象开发的程序一般更易维护、易复用、易扩展。 + +### 类和对象 + +**典型问题** + +(1)什么是对象? + +(2)什么是类? + +(3)对象实体与对象引用有何不同? + +**知识点** + +(1)**对象是用来描述客观事物的一个抽象**。一个对象由一组属性和对这组属性进行操作的一组服务组成。 + +(2)**类是具有相同属性和方法的一组对象的集合**,它为属于该类的所有对象提供了统一的抽象描述,其内部包括属性和方法两个主要部分。 + +(3)对象实体与对象引用的不同之处在于: + +- new 创建对象实例(对象实例在堆内存中),对象引用指向对象实例(对象引用存放在栈内存中) +- 一个对象引用可以指向 0 个或 1 个对象(一根绳子可以不系气球,也可以系一个气球); +- 一个对象可以有 n 个引用指向它(可以用 n 条绳子系住一个气球)。 + +### 构造方法 + +**典型问题** + +(1)构造方法有什么用? + +(2)构造方法有哪些特点? + +(3)如果一个类没有声明构造方法,该程序能正确执行吗? + +(4)构造方法能否可被重写(Override)? + +**知识点** + +(1)构造方法是一种特殊的方法,主要作用是完成对象的初始化工作。 + +(2)构造方法特点如下: + +- 名字与类名相同。 +- 没有返回值,但不能用 void 声明构造函数。 +- 生成类的对象时自动执行,无需调用。 + +(3)如果一个类没有声明构造方法,也可以执行!因为一个类即使没有声明构造方法也会有默认的不带参数的构造方法。如果我们自己添加了类的构造方法(无论是否有参),Java 就不会添加默认的无参数的构造方法了。 + +(4)构造方法不能被重写(Override),但可以重载(Overload)。 + +### 接口和抽象类 + +**典型问题** + +(1)什么是接口?接口有什么特性? + +(2)什么是抽象类?抽象类有什么特性? + +(3)接口和抽象类有什么相同点和不同点? + +(4)类支持多继承吗?接口支持多继承吗? + +**知识点** + +(1)接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。 + +接口的主要特性有: + +- 接口不能实例化。 +- 接口不能包含任何非常量成员,任何字段都隐式的被 `public static final` 修饰。 +- 接口中没有非静态方法,也就是说要么是抽象方法,要么是静态方法。 +- 从 Java8 开始,接口增加了 `default` 方法特性,可以定义方法的默认实现;Java 9 以后,甚至可以定义私有的 `default` 方法。 + +(2)抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取相关 Java 类的共用方法实现或者是共同成员变量,然后通过继承的方式达到代码复用的目的。 + +(3)接口和抽象类有什么相同点和不同点? + +Java 中的类可以实现多个接口。 + +(4)与 C++ 等语言不一样,Java 类不支持多继承。这意味着,Java 不能通过继承多个抽象类来重用逻辑。那么,如何来实现重用呢?Java 的解决方案是:接口支持多继承,准确的说,接口支持扩展多个接口,而接口也支持实现多个接口。 + +### 深拷贝和浅拷贝 + +**典型问题** + +(1)什么是深拷贝?什么是浅拷贝?深拷贝和浅拷贝有什么区别? + +(2)如何实现深拷贝? + +**知识点** + +(1)深拷贝和浅拷贝的区别: + +- **浅拷贝** - 只拷贝栈内存中的数据,不拷贝堆内存中数据。 +- **深拷贝** - 既拷贝栈内存中的数据,又拷贝堆内存中的数据。 + +(2)深拷贝的实现方式 + +- 构造方法 +- 重写 `Cloneable` 接口的 `clone()` 方法 +- Apache Commons Lang 序列化 +- JSON 序列化 + +### 面向对象设计 + +**典型问题** + +(1)面向对象三大特征是什么? + +(2)面向对象的五大原则是什么? + +**知识点** + +(1)封装、继承和多态是面向对象编程的三大特征。 + +- **封装** - **封装**的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。 +- **继承** - **继承**是代码复用的基础机制。当多个类存在相同的属性(变量)和方法时,可以从这些类中**抽象出父类**,在父类中定义**相同的属性和方法**,所有的**子类不需要重新定义这些属性和方法**,只需要通过 extends 关键字来声明继承父类即可。 +- **多态** - 你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的。 + +(2)面向对象的五大原则也就是所谓的 S.O.L.I.D 原则: + +- **单一职责原则(Single Responsibility)** - 类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。 +- **开闭原则(Open-Close)** - 设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。 +- **里氏替换原则(Liskov Substitution)** - 这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。 +- **接口分离原则** - 我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。- 对于这种情况,可以通过拆分成功能单一的多个接口,将行为进行解耦。在未来维护中,如果某个接口设计有变,不会对使用其他接口的子类构成影响。 +- **依赖反转原则** - 实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。 + +### 设计模式 + +**典型问题** + +(1)你知道哪些设计模式? + +(2)你知道哪些设计模式在 Java 源码中的应用案例? + +(3)你知道哪些设计模式在主流框架中的应用案例? + +**知识点** + +(1)23 种经典设计模式分类如下: + +- 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。 +- 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。 +- 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。 + +(2)设计模式在 Java 源码中应用的经典案例: + +InputStream 是一个抽象类,标准类库中提供了 FileInputStream、ByteArrayInputStream 等各种不同的子类,分别从不同角度对 InputStream 进行了功能扩展,这是典型的装饰器模式应用案例。 + +(3)设计模式在主流框架中应用的经典案例: + +如 Spring 等如何在 API 设计中使用设计模式。你至少要有个大体的印象,如: + +- [BeanFactory](https://github.com/spring-projects/spring-framework/blob/master/spring-beans/src/main/java/org/springframework/beans/factory/BeanFactory.java) 和 [ApplicationContext](https://github.com/spring-projects/spring-framework/blob/master/spring-context/src/main/java/org/springframework/context/ApplicationContext.java) 应用了工厂模式。 +- 在 Bean 的创建中,Spring 也为不同 scope 定义的对象,提供了单例和原型等模式实现。 +- Spring Aop 使用了代理模式、装饰器模式、适配器模式等。 +- 各种事件监听器,是观察者模式的典型应用。 +- 类似 JdbcTemplate 等则是应用了模板模式。 + +## Object + +### Object 类的常见方法有哪些? + +Object 类是一个特殊的类,是所有类的父类。它主要提供了以下 11 个方法: + +```java +/** + * native 方法,用于返回当前运行时对象的 Class 对象,使用了 final 关键字修饰,故不允许子类重写。 + */ +public final native Class getClass() +/** + * native 方法,用于返回对象的哈希码,主要使用在哈希表中,比如 JDK 中的 HashMap。 + */ +public native int hashCode() +/** + * 用于比较 2 个对象的内存地址是否相等,String 类对该方法进行了重写以用于比较字符串的值是否相等。 + */ +public boolean equals(Object obj) +/** + * native 方法,用于创建并返回当前对象的一份拷贝。 + */ +protected native Object clone() throws CloneNotSupportedException +/** + * 返回类的名字实例的哈希码的 16 进制的字符串。建议 Object 所有的子类都重写这个方法。 + */ +public String toString() +/** + * native 方法,并且不能重写。唤醒一个在此对象监视器上等待的线程(监视器相当于就是锁的概念)。如果有多个线程在等待只会任意唤醒一个。 + */ +public final native void notify() +/** + * native 方法,并且不能重写。跟 notify 一样,唯一的区别就是会唤醒在此对象监视器上等待的所有线程,而不是一个线程。 + */ +public final native void notifyAll() +/** + * native 方法,并且不能重写。暂停线程的执行。注意:sleep 方法没有释放锁,而 wait 方法释放了锁 ,timeout 是等待时间。 + */ +public final native void wait(long timeout) throws InterruptedException +/** + * 多了 nanos 参数,这个参数表示额外时间(以纳秒为单位,范围是 0-999999)。 所以超时的时间还需要加上 nanos 纳秒。 + */ +public final void wait(long timeout, int nanos) throws InterruptedException +/** + * 跟之前的 2 个 wait 方法一样,只不过该方法一直等待,没有超时时间这个概念 + */ +public final void wait() throws InterruptedException +/** + * 实例被垃圾回收器回收的时候触发的操作 + */ +protected void finalize() throws Throwable { } +``` + +### == 和 equals() 的区别 + +> 有`==`运算符了,为什么还需要 equals 啊? +> +> 说一说你对 java.lang.Object 对象中 hashCode 和 equals 方法的理解。在什么场景下需 +> 要重新实现这两个方法。 +> +> 有没有可能 2 个不相等的对象有相同的 hashcode + +**`==`** 对于基本类型和引用类型的作用效果是不同的: + +- 对于基本数据类型来说,`==` 比较的是值。 +- 对于引用数据类型来说,`==` 比较的是对象的内存地址。 + +> 因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。 + +**`equals()`** 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。`equals()`方法存在于`Object`类中,而`Object`类是所有类的直接或间接父类,因此所有的类都有`equals()`方法。 + +`Object` 类 `equals()` 方法: + +```java +public boolean equals(Object obj) { + return (this == obj); +} +``` + +`equals()` 方法存在两种使用情况: + +- **类没有重写 `equals()`方法**:通过`equals()`比较该类的两个对象时,等价于通过“==”比较这两个对象,使用的默认是 `Object`类`equals()`方法。 +- **类重写了 `equals()`方法**:一般我们都重写 `equals()`方法来比较两个对象中的属性是否相等;若它们的属性相等,则返回 true(即,认为这两个对象相等)。 + +举个例子(这里只是为了举例。实际上,你按照下面这种写法的话,像 IDEA 这种比较智能的 IDE 都会提示你将 `==` 换成 `equals()` ): + +```java +String a = new String("ab"); // a 为一个引用 +String b = new String("ab"); // b 为另一个引用,对象的内容一样 +String aa = "ab"; // 放在常量池中 +String bb = "ab"; // 从常量池中查找 +System.out.println(aa == bb);// true +System.out.println(a == b);// false +System.out.println(a.equals(b));// true +System.out.println(42 == 42.0);// true +``` + +`String` 中的 `equals` 方法是被重写过的,因为 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是对象的值。 + +当创建 `String` 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用。如果没有就在常量池中重新创建一个 `String` 对象。 + +`String`类`equals()`方法: + +```java +public boolean equals(Object anObject) { + if (this == anObject) { + return true; + } + if (anObject instanceof String) { + String anotherString = (String)anObject; + int n = value.length; + if (n == anotherString.value.length) { + char v1[] = value; + char v2[] = anotherString.value; + int i = 0; + while (n-- != 0) { + if (v1[i] != v2[i]) + return false; + i++; + } + return true; + } + } + return false; +} +``` + +### hashCode() 有什么用? + +`hashCode()` 的作用是获取哈希码(`int` 整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。 + +`hashCode()` 定义在 JDK 的 `Object` 类中,这就意味着 Java 中的任何类都包含有 `hashCode()` 函数。另外需要注意的是:`Object` 的 `hashCode()` 方法是本地方法,也就是用 C 语言或 C++ 实现的。 + +> ⚠️ 注意:该方法在 **Oracle OpenJDK8** 中默认是 "使用线程局部状态来实现 Marsaglia's xor-shift 随机数生成", 并不是 "地址" 或者 "地址转换而来", 不同 JDK/VM 可能不同在 **Oracle OpenJDK8** 中有六种生成方式 (其中第五种是返回地址), 通过添加 VM 参数:-XX:hashCode=4 启用第五种。参考源码: +> +> - https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/globals.hpp(1127 行) +> - https://hg.openjdk.org/jdk8u/jdk8u/hotspot/file/87ee5ee27509/src/share/vm/runtime/synchronizer.cpp(537 行开始) + +```java +public native int hashCode(); +``` + +散列表存储的是键值对 (key-value),它的特点是:**能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)** + +### 为什么要有 hashCode? + +我们以“`HashSet` 如何检查重复”为例子来说明为什么要有 `hashCode`? + +下面这段内容摘自我的 Java 启蒙书《Head First Java》: + +> 当你把对象加入 `HashSet` 时,`HashSet` 会先计算对象的 `hashCode` 值来判断对象加入的位置,同时也会与其他已经加入的对象的 `hashCode` 值作比较,如果没有相符的 `hashCode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashCode` 值的对象,这时会调用 `equals()` 方法来检查 `hashCode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 `equals` 的次数,相应就大大提高了执行速度。 + +其实, `hashCode()` 和 `equals()`都是用于比较两个对象是否相等。 + +**那为什么 JDK 还要同时提供这两个方法呢?** + +这是因为在一些容器(比如 `HashMap`、`HashSet`)中,有了 `hashCode()` 之后,判断元素是否在对应容器中的效率会更高(参考添加元素进`HashSet`的过程)! + +我们在前面也提到了添加元素进`HashSet`的过程,如果 `HashSet` 在对比的时候,同样的 `hashCode` 有多个对象,它会继续使用 `equals()` 来判断是否真的相同。也就是说 `hashCode` 帮助我们大大缩小了查找成本。 + +**那为什么不只提供 `hashCode()` 方法呢?** + +这是因为两个对象的`hashCode` 值相等并不代表两个对象就相等。 + +**那为什么两个对象有相同的 `hashCode` 值,它们也不一定是相等的?** + +因为 `hashCode()` 所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的 `hashCode` )。 + +总结下来就是: + +- 如果两个对象的`hashCode` 值相等,那这两个对象不一定相等(哈希碰撞)。 +- 如果两个对象的`hashCode` 值相等并且`equals()`方法也返回 `true`,我们才认为这两个对象相等。 +- 如果两个对象的`hashCode` 值不相等,我们就可以直接认为这两个对象不相等。 + +相信大家看了我前面对 `hashCode()` 和 `equals()` 的介绍之后,下面这个问题已经难不倒你们了。 + +### 为什么重写 equals() 时必须重写 hashCode() 方法? + +因为两个相等的对象的 `hashCode` 值必须是相等。也就是说如果 `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。 + +如果重写 `equals()` 时没有重写 `hashCode()` 方法的话就可能会导致 `equals` 方法判断是相等的两个对象,`hashCode` 值却不相等。 + +**思考**:重写 `equals()` 时没有重写 `hashCode()` 方法的话,使用 `HashMap` 可能会出现什么问题。 + +**总结**: + +- `equals` 方法判断两个对象是相等的,那这两个对象的 `hashCode` 值也要相等。 +- 两个对象有相同的 `hashCode` 值,他们也不一定是相等的(哈希碰撞)。 + +更多关于 `hashCode()` 和 `equals()` 的内容可以查看:[Java hashCode() 和 equals() 的若干问题解答](https://www.cnblogs.com/skywang12345/p/3324958.html) + +### finalize 有什么用? + +首先,不推荐使用 finalize,在 Java 9 中,甚至明确将 Object.finalize() 标记为 deprecated! + +finalize 的目的是保证对象在被垃圾收集前完成特定资源的回收。实际上,无法保证 finalize 什么时候执行,执行的是否符合预期。finalize 使用不当会影响性能,导致程序死锁、挂起等。 + +有什么机制可以替换 finalize 吗? + +Java 目前在逐步使用 java.lang.ref.Cleaner 来替换掉原有的 finalize 实现。Cleaner 的实现利用了幻象引用(PhantomReference),这是一种常见的所谓 post-mortem 清理机制。吸取了 finalize 里的教训,每个 Cleaner 的操作都是独立的,它有自己的运行线程,所以可以避免意外死锁等问题。 + +## String + +### String、StringBuffer、StringBuilder 的区别? + +**可变性** + +`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,是典型的 Immutable 类。由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。 + +`StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串,不过没有使用 `final` 和 `private` 关键字修饰,最关键的是这个 `AbstractStringBuilder` 类还提供了很多修改字符串的方法比如 `append` 方法。 + +``` +abstract class AbstractStringBuilder implements Appendable, CharSequence { + char[] value; + public AbstractStringBuilder append(String str) { + if (str == null) + return appendNull(); + int len = str.length(); + ensureCapacityInternal(count + len); + str.getChars(0, len, value, count); + count += len; + return this; + } + //... +} +``` + +**线程安全性** + +`String` 中的对象是不可变的,也就可以理解为常量,线程安全。`AbstractStringBuilder` 是 `StringBuilder` 与 `StringBuffer` 的公共父类,定义了一些字符串的基本操作,如 `expandCapacity`、`append`、`insert`、`indexOf` 等公共方法。`StringBuffer` 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。`StringBuilder` 并没有对方法进行加同步锁,所以是非线程安全的。 + +**性能** + +每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。`StringBuffer` 每次都会对 `StringBuffer` 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 `StringBuilder` 相比使用 `StringBuffer` 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。 + +**对于三者使用的总结:** + +- 操作少量的数据:适用 `String` +- 单线程操作字符串缓冲区下操作大量数据:适用 `StringBuilder` +- 多线程操作字符串缓冲区下操作大量数据:适用 `StringBuffer` + +### String 为什么是不可变的? + +`String` 类中使用 `final` 关键字修饰字符数组来保存字符串,是典型的 Immutable 类。由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。 + +``` +public final class String implements java.io.Serializable, Comparable, CharSequence { + private final char value[]; + //... +} +``` + +> 🐛 修正:我们知道被 `final` 关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,`final` 关键字修饰的数组保存字符串并不是 `String` 不可变的根本原因,因为这个数组保存的字符串是可变的(`final` 修饰引用类型变量的情况)。 +> +> `String` 真正不可变有下面几点原因: +> +> 1. 保存字符串的数组被 `final` 修饰且为私有的,并且`String` 类没有提供/暴露修改这个字符串的方法。 +> 2. `String` 类被 `final` 修饰导致其不能被继承,进而避免了子类破坏 `String` 不可变。 +> +> 相关阅读:[如何理解 String 类型值的不可变? - 知乎提问](https://www.zhihu.com/question/20618891/answer/114125846) +> +> 补充(来自 [issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,`String`、`StringBuilder` 与 `StringBuffer` 的实现改用 `byte` 数组存储字符串。 +> +> ``` +> public final class String implements java.io.Serializable,Comparable, CharSequence { +> // @Stable 注解表示变量最多被修改一次,称为“稳定的”。 +> @Stable +> private final byte[] value; +> } +> +> abstract class AbstractStringBuilder implements Appendable, CharSequence { +> byte[] value; +> +> } +> ``` +> +> **Java 9 为何要将 `String` 的底层实现由 `char[]` 改成了 `byte[]` ?** +> +> 新版的 String 其实支持两个编码方案:Latin-1 和 UTF-16。如果字符串中包含的汉字没有超过 Latin-1 可表示范围内的字符,那就会使用 Latin-1 作为编码方案。Latin-1 编码方案下,`byte` 占一个字节 (8 位),`char` 占用 2 个字节(16),`byte` 相较 `char` 节省一半的内存空间。 +> +> JDK 官方就说了绝大部分字符串对象只包含 Latin-1 可表示的字符。 +> +> [![img](https://camo.githubusercontent.com/3c9b36a376eac3142dedec2c23fe59867890bafa9edb26450c56cba6c0d40c4a/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a646b392d737472696e672d6c6174696e312e706e67)](https://camo.githubusercontent.com/3c9b36a376eac3142dedec2c23fe59867890bafa9edb26450c56cba6c0d40c4a/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a646b392d737472696e672d6c6174696e312e706e67) +> +> 如果字符串中包含的汉字超过 Latin-1 可表示范围内的字符,`byte` 和 `char` 所占用的空间是一样的。 +> +> 这是官方的介绍:https://openjdk.java.net/jeps/254 。 + +### 字符串拼接用“+” 还是 StringBuilder? + +Java 语言本身并不支持运算符重载,“+”和“+=”是专门为 String 类重载过的运算符,也是 Java 中仅有的两个重载过的运算符。 + +``` +String str1 = "he"; +String str2 = "llo"; +String str3 = "world"; +String str4 = str1 + str2 + str3; +``` + +上面的代码对应的字节码如下: + +[![img](https://camo.githubusercontent.com/605d21c93ba1ea24d5c41fef5a6df008bc7b086def34ed4446aa854fc8a7ea9f/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f696d6167652d32303232303432323136313633373932392e706e67)](https://camo.githubusercontent.com/605d21c93ba1ea24d5c41fef5a6df008bc7b086def34ed4446aa854fc8a7ea9f/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f696d6167652d32303232303432323136313633373932392e706e67) + +可以看出,字符串对象通过“+”的字符串拼接方式,实际上是通过 `StringBuilder` 调用 `append()` 方法实现的,拼接完成之后调用 `toString()` 得到一个 `String` 对象 。 + +不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:**编译器不会创建单个 `StringBuilder` 以复用,会导致创建过多的 `StringBuilder` 对象**。 + +``` +String[] arr = {"he", "llo", "world"}; +String s = ""; +for (int i = 0; i < arr.length; i++) { + s += arr[i]; +} +System.out.println(s); +``` + +`StringBuilder` 对象是在循环内部被创建的,这意味着每循环一次就会创建一个 `StringBuilder` 对象。 + +[![img](https://camo.githubusercontent.com/04e5f5d98c90ab0482c6d5a76c1fa171f85cdc96b4ff1e543ff4e87e6a37cd10/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f696d6167652d32303232303432323136313332303832332e706e67)](https://camo.githubusercontent.com/04e5f5d98c90ab0482c6d5a76c1fa171f85cdc96b4ff1e543ff4e87e6a37cd10/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f696d6167652d32303232303432323136313332303832332e706e67) + +如果直接使用 `StringBuilder` 对象进行字符串拼接的话,就不会存在这个问题了。 + +```java +String[] arr = {"he", "llo", "world"}; +StringBuilder s = new StringBuilder(); +for (String value : arr) { + s.append(value); +} +System.out.println(s); +``` + +[![img](https://camo.githubusercontent.com/4459c04a5826598d584e7b08713a6c58cdbd1dc437e5d2776d7fa7852e7c36d7/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f696d6167652d32303232303432323136323332373431352e706e67)](https://camo.githubusercontent.com/4459c04a5826598d584e7b08713a6c58cdbd1dc437e5d2776d7fa7852e7c36d7/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f696d6167652d32303232303432323136323332373431352e706e67) + +如果你使用 IDEA 的话,IDEA 自带的代码检查机制也会提示你修改代码。 + +不过,使用 “+” 进行字符串拼接会产生大量的临时对象的问题在 JDK9 中得到了解决。在 JDK9 当中,字符串相加 “+” 改为了用动态方法 `makeConcatWithConstants()` 来实现,而不是大量的 `StringBuilder` 了。这个改进是 JDK9 的 [JEP 280](https://openjdk.org/jeps/280) 提出的,这也意味着 JDK 9 之后,你可以放心使用“+” 进行字符串拼接了。关于这部分改进的详细介绍,推荐阅读这篇文章:还在无脑用 [StringBuilder?来重温一下字符串拼接吧](https://juejin.cn/post/7182872058743750715) 。 + +### String#equals() 和 Object#equals() 有何区别? + +`String` 中的 `equals` 方法是被重写过的,比较的是 String 字符串的值是否相等。 `Object` 的 `equals` 方法是比较的对象的内存地址。 + +### 字符串常量池的作用了解吗? + +**字符串常量池** 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。 + +```java +// 在堆中创建字符串对象”ab“ +// 将字符串对象”ab“的引用保存在字符串常量池中 +String aa = "ab"; +// 直接返回字符串常量池中字符串对象”ab“的引用 +String bb = "ab"; +System.out.println(aa==bb);// true +``` + +更多关于字符串常量池的介绍可以看一下 [Java 内存区域详解](https://javaguide.cn/java/jvm/memory-area.html) 这篇文章。 + +### String s1 = new String("abc"); 这句话创建了几个字符串对象? + +会创建 1 或 2 个字符串对象。 + +1、如果字符串常量池中不存在字符串对象“abc”的引用,那么它会在堆上创建两个字符串对象,其中一个字符串对象的引用会被保存在字符串常量池中。 + +示例代码(JDK 1.8): + +```java +String s1 = new String("abc"); +``` + +对应的字节码: + +[![img](https://camo.githubusercontent.com/085d4875332e2a5176d46e9cf6b0f08a2d1f0ddac26ef2ec7bc1a1cbdb221e34/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6f70656e2d736f757263652d70726f6a6563742f696d6167652d32303232303431333137353830393935392e706e67)](https://camo.githubusercontent.com/085d4875332e2a5176d46e9cf6b0f08a2d1f0ddac26ef2ec7bc1a1cbdb221e34/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6f70656e2d736f757263652d70726f6a6563742f696d6167652d32303232303431333137353830393935392e706e67) + +`ldc` 命令用于判断字符串常量池中是否保存了对应的字符串对象的引用,如果保存了的话直接返回,如果没有保存的话,会在堆中创建对应的字符串对象并将该字符串对象的引用保存到字符串常量池中。 + +2、如果字符串常量池中已存在字符串对象“abc”的引用,则只会在堆中创建 1 个字符串对象“abc”。 + +示例代码(JDK 1.8): + +```java +// 字符串常量池中已存在字符串对象“abc”的引用 +String s1 = "abc"; +// 下面这段代码只会在堆中创建 1 个字符串对象“abc” +String s2 = new String("abc"); +``` + +对应的字节码: + +[![img](https://camo.githubusercontent.com/241ade2583eac806483db5ae9ac0b246e85048cc67699b2c00e5fa97d16ec008/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6f70656e2d736f757263652d70726f6a6563742f696d6167652d32303232303431333138303032313037322e706e67)](https://camo.githubusercontent.com/241ade2583eac806483db5ae9ac0b246e85048cc67699b2c00e5fa97d16ec008/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6f70656e2d736f757263652d70726f6a6563742f696d6167652d32303232303431333138303032313037322e706e67) + +这里就不对上面的字节码进行详细注释了,7 这个位置的 `ldc` 命令不会在堆中创建新的字符串对象“abc”,这是因为 0 这个位置已经执行了一次 `ldc` 命令,已经在堆中创建过一次字符串对象“abc”了。7 这个位置执行 `ldc` 命令会直接返回字符串常量池中字符串对象“abc”对应的引用。 + +### String#intern 方法有什么作用? + +String 在 Java 6 以后提供了 intern() 方法,目的是提示 JVM 把相应字符串缓存起来,以备重复使用。在我们创建字符串对象并调用 intern() 方法的时候,如果已经有缓存的字符串,就会返回缓存里的实例,否则将其缓存起来。 + +被缓存的字符串是存在永久代(PermGen)里的,而这个空间是很有限的,也基本不会被 FullGC 之外的垃圾收集照顾到。所以,如果使用不当,就可能产生 OOM。 + +在后续版本中,这个缓存被放置在堆中,这样就极大避免了永久代占满的问题,甚至永久代在 JDK 8 中被 MetaSpace(元数据区)替代了。而且,默认缓存大小也在不断地扩大中,从最初的 1009,到 7u40 以后被修改为 60013。 + +### String 类型的变量和常量做“+”运算时发生了什么? + +先来看字符串不加 `final` 关键字拼接的情况(JDK1.8): + +```java +String str1 = "str"; +String str2 = "ing"; +String str3 = "str" + "ing"; +String str4 = str1 + str2; +String str5 = "string"; +System.out.println(str3 == str4);//false +System.out.println(str3 == str5);//true +System.out.println(str4 == str5);//false +``` + +> **注意**:比较 String 字符串的值是否相等,可以使用 `equals()` 方法。 `String` 中的 `equals` 方法是被重写过的。 `Object` 的 `equals` 方法是比较的对象的内存地址,而 `String` 的 `equals` 方法比较的是字符串的值是否相等。如果你使用 `==` 比较两个字符串是否相等的话,IDEA 还是提示你使用 `equals()` 方法替换。 + +[![img](https://camo.githubusercontent.com/b94bf51c6a148c3b1e87cc6ddc5a150d5fb40b7838ae21754034cca5b100d569/68747470733a2f2f6f73732e6a61766167756964652e636e2f6a6176612d67756964652d626c6f672f696d6167652d32303231303831373132333235323434312e706e67)](https://camo.githubusercontent.com/b94bf51c6a148c3b1e87cc6ddc5a150d5fb40b7838ae21754034cca5b100d569/68747470733a2f2f6f73732e6a61766167756964652e636e2f6a6176612d67756964652d626c6f672f696d6167652d32303231303831373132333235323434312e706e67) + +**对于编译期可以确定值的字符串,也就是常量字符串 ,jvm 会将其存入字符串常量池。并且,字符串常量拼接得到的字符串常量在编译阶段就已经被存放字符串常量池,这个得益于编译器的优化。** + +在编译过程中,Javac 编译器(下文中统称为编译器)会进行一个叫做 **常量折叠 (Constant Folding)** 的代码优化。《深入理解 Java 虚拟机》中是也有介绍到: + +[![img](https://camo.githubusercontent.com/0c22c82bb7b2e76aa639af321b67774bb5a7bed3efb284d7d55aa8bc5337dd2c/68747470733a2f2f6f73732e6a61766167756964652e636e2f6a61766167756964652f696d6167652d32303231303831373134323731353339362e706e67)](https://camo.githubusercontent.com/0c22c82bb7b2e76aa639af321b67774bb5a7bed3efb284d7d55aa8bc5337dd2c/68747470733a2f2f6f73732e6a61766167756964652e636e2f6a61766167756964652f696d6167652d32303231303831373134323731353339362e706e67) + +常量折叠会把常量表达式的值求出来作为常量嵌在最终生成的代码中,这是 Javac 编译器会对源代码做的极少量优化措施之一(代码优化几乎都在即时编译器中进行)。 + +对于 `String str3 = "str" + "ing";` 编译器会给你优化成 `String str3 = "string";` 。 + +并不是所有的常量都会进行折叠,只有编译器在程序编译期就可以确定值的常量才可以: + +- 基本数据类型 ( `byte`、`boolean`、`short`、`char`、`int`、`float`、`long`、`double`) 以及字符串常量。 +- `final` 修饰的基本数据类型和字符串变量 +- 字符串通过 “+”拼接得到的字符串、基本数据类型之间算数运算(加减乘除)、基本数据类型的位运算(<<、>>、>>> ) + +**引用的值在程序编译期是无法确定的,编译器无法对其进行优化。** + +对象引用和“+”的字符串拼接方式,实际上是通过 `StringBuilder` 调用 `append()` 方法实现的,拼接完成之后调用 `toString()` 得到一个 `String` 对象 。 + +```java +String str4 = new StringBuilder().append(str1).append(str2).toString(); +``` + +我们在平时写代码的时候,尽量避免多个字符串对象拼接,因为这样会重新创建对象。如果需要改变字符串的话,可以使用 `StringBuilder` 或者 `StringBuffer`。 + +不过,字符串使用 `final` 关键字声明之后,可以让编译器当做常量来处理。 + +示例代码: + +```java +final String str1 = "str"; +final String str2 = "ing"; +// 下面两个表达式其实是等价的 +String c = "str" + "ing";// 常量池中的对象 +String d = str1 + str2; // 常量池中的对象 +System.out.println(c == d);// true +``` + +被 `final` 关键字修饰之后的 `String` 会被编译器当做常量来处理,编译器在程序编译期就可以确定它的值,其效果就相当于访问常量。 + +如果 ,编译器在运行时才能知道其确切值的话,就无法对其优化。 + +示例代码(`str2` 在运行时才能确定其值): + +```java +final String str1 = "str"; +final String str2 = getStr(); +String c = "str" + "ing";// 常量池中的对象 +String d = str1 + str2; // 在堆上创建的新的对象 +System.out.println(c == d);// false +public static String getStr() { + return "ing"; +} +``` \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\270\200.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\270\200.md" new file mode 100644 index 0000000000..eca4f82d99 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\270\200.md" @@ -0,0 +1,411 @@ +--- +title: Java 容器面试一 +date: 2024-07-03 07:44:02 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 + - 容器 +permalink: /pages/e896e1d0/ +--- + +# Java 容器面试一 + +## Java 容器综合 + +### Java 容器框架概览 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/container/java-container-structure.png) + +Java 容器框架主要分为 `Collection` 和 `Map` 两种。其中,`Collection` 又分为 `List`、`Set` 以及 `Queue`。 + +- `Collection` - 一个独立元素的序列,这些元素都服从一条或者多条规则。 + - `List` - 可以视为有序线性表。 + - `ArrayList` - 数据结构为 `Object[]` 数组。 + - `LinkedList` - 数据结构为双链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)。 + - `Vector` - 数据结构为 `Object[]` 数组。 + - `Set` - 不存储重复的元素。 + - `HashSet` - 基于 `HashMap` 实现,不保证存储元素有序。 + - `LinkedHashSet` - 基于 `LinkedHashMap` 实现,保证元素按插入顺序存储。 + - `TreeSet` - 基于 `TreeMap` 实现,排序根据元素类型的 `Comparator` 而定。 + - `Queue` - 按照排队规则来确定对象产生的顺序(通常与它们被插入的顺序相同)。 + - `ArrayDeque` - 用一个动态数组实现了栈和队列所需的所有操作。 + - `PriorityQueue` - 优先级队列。 +- `Map` - 一组成对的“键值对”对象,允许你使用键来查找值。 + - `HashMap` - 储存无序的键值对,而 `Hash` 也体现了它的查找效率很高。`HashMap` 是使用最广泛的 `Map`。 + - `TreeMap` - 储存有序的键值对,排序根据元素类型的 `Comparator` 而定。 + - `LinkedHashMap` - `LinkedHashMap` 继承了 `HashMap`,并以此为基础,增加了一条双向链表,以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。 + - `Hashtable` - `Hashtable` 在它的主要方法中使用 `synchronized` 关键字修饰,来保证线程安全。但是,由于它的锁粒度太大,非常影响读写速度,所以,现代 Java 程序几乎不会使用。如果需要保证线程安全,一般会用 `ConcurrentHashMap` 来替代。 + +### 为什么要使用容器 + +在 Java 中,存储一组同类型的数据,可以选择数组或容器。 + +**相对于数组,容器更灵活、更便捷**。 + +| | 数组 | 容器 | +| ------------ | -------------------------------------- | ---------------------------------------- | +| 大小 | 存储大小固定,且必须在声明时就指定大小 | 可以根据实际存储数量,动态扩容、缩容 | +| 存储数据类型 | 无限制 | 只能存储引用数据类型 | +| 类型安全 | 不支持 | 基于泛型来确保类型安全 | +| 操作 | 基于数组下标访问 | 基于泛型,支持了丰富的内置算法,操作便捷 | + +## List + +### ArrayList 和 Array(数组)的区别? + +`ArrayList` 内部基于动态数组实现,比 `Array`(静态数组) 使用起来更加灵活: + +- `ArrayList`会根据实际存储的元素动态地扩容或缩容,而 `Array` 被创建之后就不能改变它的长度了。 +- `ArrayList` 允许你使用泛型来确保类型安全,`Array` 则不可以。 +- `ArrayList` 中只能存储对象。对于基本类型数据,需要使用其对应的包装类(如 Integer、Double 等)。`Array` 可以直接存储基本类型数据,也可以存储对象。 +- `ArrayList` 支持插入、删除、遍历等常见操作,并且提供了丰富的 API 操作方法,比如 `add()`、`remove()`等。`Array` 只是一个固定长度的数组,只能按照下标访问其中的元素,不具备动态添加、删除元素的能力。 +- `ArrayList`创建时不需要指定大小,而`Array`创建时必须指定大小。 + +下面是二者使用的简单对比: + +`Array`: + +```java + // 初始化一个 String 类型的数组 + String[] stringArr = new String[]{"hello", "world", "!"}; + // 修改数组元素的值 + stringArr[0] = "goodbye"; + System.out.println(Arrays.toString(stringArr));// [goodbye, world, !] + // 删除数组中的元素,需要手动移动后面的元素 + for (int i = 0; i < stringArr.length - 1; i++) { + stringArr[i] = stringArr[i + 1]; + } + stringArr[stringArr.length - 1] = null; + System.out.println(Arrays.toString(stringArr));// [world, !, null] +``` + +`ArrayList` : + +```java +// 初始化一个 String 类型的 ArrayList + ArrayList stringList = new ArrayList<>(Arrays.asList("hello", "world", "!")); +// 添加元素到 ArrayList 中 + stringList.add("goodbye"); + System.out.println(stringList);// [hello, world, !, goodbye] + // 修改 ArrayList 中的元素 + stringList.set(0, "hi"); + System.out.println(stringList);// [hi, world, !, goodbye] + // 删除 ArrayList 中的元素 + stringList.remove(0); + System.out.println(stringList); // [world, !, goodbye] +``` + +### ArrayList 可以添加 null 值吗? + +`ArrayList` 中可以存储任何类型的对象,包括 `null` 值。不过,不建议向`ArrayList` 中添加 `null` 值, `null` 值无意义,会让代码难以维护比如忘记做判空处理就会导致空指针异常。 + +示例代码: + +``` +ArrayList listOfStrings = new ArrayList<>(); +listOfStrings.add(null); +listOfStrings.add("java"); +System.out.println(listOfStrings); +``` + +输出: + +``` +[null, java] +``` + +### ArrayList 插入和删除元素的时间复杂度? + +对于插入: + +- 头部插入:由于需要将所有元素都依次向后移动一个位置,因此时间复杂度是 O(n)。 +- 尾部插入:当 `ArrayList` 的容量未达到极限时,往列表末尾插入元素的时间复杂度是 O(1),因为它只需要在数组末尾添加一个元素即可;当容量已达到极限并且需要扩容时,则需要执行一次 O(n) 的操作将原数组复制到新的更大的数组中,然后再执行 O(1) 的操作添加元素。 +- 指定位置插入:需要将目标位置之后的所有元素都向后移动一个位置,然后再把新元素放入指定位置。这个过程需要移动平均 n/2 个元素,因此时间复杂度为 O(n)。 + +对于删除: + +- 头部删除:由于需要将所有元素依次向前移动一个位置,因此时间复杂度是 O(n)。 +- 尾部删除:当删除的元素位于列表末尾时,时间复杂度为 O(1)。 +- 指定位置删除:需要将目标元素之后的所有元素向前移动一个位置以填补被删除的空白位置,因此需要移动平均 n/2 个元素,时间复杂度为 O(n)。 + +这里简单列举一个例子: + +``` +// ArrayList 的底层数组大小为 10,此时存储了 7 个元素 ++---+---+---+---+---+---+---+---+---+---+ +| 1 | 2 | 3 | 4 | 5 | 6 | 7 | | | | ++---+---+---+---+---+---+---+---+---+---+ + 0 1 2 3 4 5 6 7 8 9 +// 在索引为 1 的位置插入一个元素 8,该元素后面的所有元素都要向右移动一位 ++---+---+---+---+---+---+---+---+---+---+ +| 1 | 8 | 2 | 3 | 4 | 5 | 6 | 7 | | | ++---+---+---+---+---+---+---+---+---+---+ + 0 1 2 3 4 5 6 7 8 9 +// 删除索引为 1 的位置的元素,该元素后面的所有元素都要向左移动一位 ++---+---+---+---+---+---+---+---+---+---+ +| 1 | 2 | 3 | 4 | 5 | 6 | 7 | | | | ++---+---+---+---+---+---+---+---+---+---+ + 0 1 2 3 4 5 6 7 8 9 +``` + +### LinkedList 插入和删除元素的时间复杂度? + +- 头部插入/删除:只需要修改头结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 +- 尾部插入/删除:只需要修改尾结点的指针即可完成插入/删除操作,因此时间复杂度为 O(1)。 +- 指定位置插入/删除:需要先移动到指定位置,再修改指定节点的指针完成插入/删除,因此需要遍历平均 n/2 个元素,时间复杂度为 O(n)。 + +### ArrayList 和 Vector 的比较 + +- `Vector` 是 Java 早期提供的**线程安全的动态数组**。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。 +- `ArrayList` 是应用更加广泛的**动态数组**实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。 + +### Vector 和 Stack 的比较 + +- `Vector` 和 `Stack` 两者都是线程安全的,都是使用 `synchronized` 关键字进行同步处理。 +- `Stack` 继承自 `Vector`,是一个后进先出的栈,而 `Vector` 是一个列表。 + +随着 Java 并发编程的发展,`Vector` 和 `Stack` 已经被淘汰,推荐使用并发集合类(例如 `ConcurrentHashMap`、`CopyOnWriteArrayList` 等)或者手动实现线程安全的方法来提供安全的多线程操作支持。 + +### ArrayList 与 LinkedList 的比较 + +| | ArrayList | LinkedList | +| ---------------- | ----------- | -------------------------------------------------- | +| 数据结构 | Object 数组 | 双链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环) | +| 是否支持随机访问 | 支持 | 不支持 | +| 线程安全 | 不保证 | 不保证 | + +- **是否保证线程安全:** `ArrayList` 和 `LinkedList` 都是不同步的,也就是不保证线程安全; +- **底层数据结构:** `ArrayList` 底层使用的是 **`Object` 数组**;`LinkedList` 底层使用的是 **双向链表** 数据结构(JDK1.6 之前为循环链表,JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!) +- 插入和删除是否受元素位置的影响: + - `ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`),时间复杂度就为 O(n)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的 (n-i) 个元素都要执行向后位/向前移一位的操作。 + - `LinkedList` 采用链表存储,所以在头尾插入或者删除元素不受元素位置的影响(`add(E e)`、`addFirst(E e)`、`addLast(E e)`、`removeFirst()`、 `removeLast()`),时间复杂度为 O(1),如果是要在指定位置 `i` 插入和删除元素的话(`add(int index, E element)`,`remove(Object o)`,`remove(int index)`), 时间复杂度为 O(n) ,因为需要先移动到指定位置再插入和删除。 +- **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList`(实现了 `RandomAccess` 接口) 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。 +- **内存空间占用:** `ArrayList` 的空间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。 + +## Set + +### Comparable 和 Comparator 的区别 + +`Comparable` 接口和 `Comparator` 接口都是 Java 中用于排序的接口,它们在实现类对象之间比较大小、排序等方面发挥了重要作用: + +- `Comparable` 接口实际上是出自`java.lang`包 它有一个 `compareTo(Object obj)`方法用来排序 +- `Comparator`接口实际上是出自 `java.util` 包它有一个`compare(Object obj1, Object obj2)`方法用来排序 + +一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个 `song` 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的`Comparator`方法或者以两个 `Comparator` 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`. + +#### Comparator 定制排序 + +``` +ArrayList arrayList = new ArrayList(); +arrayList.add(-1); +arrayList.add(3); +arrayList.add(-5); +arrayList.add(7); +arrayList.add(4); +arrayList.add(-9); +arrayList.add(-7); +System.out.println("原始数组:"); +System.out.println(arrayList); +// void reverse(List list):反转 +Collections.reverse(arrayList); +System.out.println("Collections.reverse(arrayList):"); +System.out.println(arrayList); + +// void sort(List list), 按自然排序的升序排序 +Collections.sort(arrayList); +System.out.println("Collections.sort(arrayList):"); +System.out.println(arrayList); +// 定制排序的用法 +Collections.sort(arrayList, new Comparator() { + @Override + public int compare(Integer o1, Integer o2) { + return o2.compareTo(o1); + } +}); +System.out.println("定制排序后:"); +System.out.println(arrayList); +``` + +Output: + +``` +原始数组: +[-1, 3, 3, -5, 7, 4, -9, -7] +Collections.reverse(arrayList): +[-7, -9, 4, 7, -5, 3, 3, -1] +Collections.sort(arrayList): +[-9, -7, -5, -1, 3, 3, 4, 7] +定制排序后: +[7, 4, 3, 3, -1, -5, -7, -9] +``` + +#### 重写 compareTo 方法实现按年龄来排序 + +``` +// person 对象没有实现 Comparable 接口,所以必须实现,这样才不会出错,才可以使 treemap 中的数据按顺序排列 +// 前面一个例子的 String 类已经默认实现了 Comparable 接口,详细可以查看 String 类的 API 文档,另外其他 +// 像 Integer 类等都已经实现了 Comparable 接口,所以不需要另外实现了 +public class Person implements Comparable { + private String name; + private int age; + + public Person(String name, int age) { + super(); + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + /** + * T 重写 compareTo 方法实现按年龄来排序 + */ + @Override + public int compareTo(Person o) { + if (this.age > o.getAge()) { + return 1; + } + if (this.age < o.getAge()) { + return -1; + } + return 0; + } +} +``` + +``` + public static void main(String[] args) { + TreeMap pdata = new TreeMap(); + pdata.put(new Person("张三", 30), "zhangsan"); + pdata.put(new Person("李四", 20), "lisi"); + pdata.put(new Person("王五", 10), "wangwu"); + pdata.put(new Person("小红", 5), "xiaohong"); + // 得到 key 的值的同时得到 key 所对应的值 + Set keys = pdata.keySet(); + for (Person key : keys) { + System.out.println(key.getAge() + "-" + key.getName()); + + } + } +``` + +Output: + +``` +5-小红 +10-王五 +20-李四 +30-张三 +``` + +### 无序性和不可重复性的含义是什么 + +- 无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。 +- 不可重复性是指添加的元素按照 `equals()` 判断时 ,返回 false,需要同时重写 `equals()` 方法和 `hashCode()` 方法。 + +### 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同 + +- `HashSet`、`LinkedHashSet` 和 `TreeSet` 都是 `Set` 接口的实现类,都能保证元素唯一,并且都不是线程安全的。 +- `HashSet`、`LinkedHashSet` 和 `TreeSet` 的主要区别在于底层数据结构不同。`HashSet` 的底层数据结构是哈希表(基于 `HashMap` 实现)。`LinkedHashSet` 的底层数据结构是链表和哈希表,元素的插入和取出顺序满足 FIFO。`TreeSet` 底层数据结构是红黑树,元素是有序的,排序的方式有自然排序和定制排序。 +- 底层数据结构不同又导致这三者的应用场景不同。`HashSet` 用于不需要保证元素插入和取出顺序的场景,`LinkedHashSet` 用于保证元素的插入和取出顺序满足 FIFO 的场景,`TreeSet` 用于支持对元素自定义排序规则的场景。 + +## Queue + +### Queue 与 Deque 的区别 + +`Queue` 是单端队列,只能从一端插入元素,另一端删除元素,实现上一般遵循 **先进先出(FIFO)** 规则。 + +`Queue` 扩展了 `Collection` 的接口,根据 **因为容量问题而导致操作失败后处理方式的不同** 可以分为两类方法:一种在操作失败后会抛出异常,另一种则会返回特殊值。 + +| `Queue` 接口 | 抛出异常 | 返回特殊值 | +| ------------ | --------- | ---------- | +| 插入队尾 | add(E e) | offer(E e) | +| 删除队首 | remove() | poll() | +| 查询队首元素 | element() | peek() | + +`Deque` 是双端队列,在队列的两端均可以插入或删除元素。 + +`Deque` 扩展了 `Queue` 的接口,增加了在队首和队尾进行插入和删除的方法,同样根据失败后处理方式的不同分为两类: + +| `Deque` 接口 | 抛出异常 | 返回特殊值 | +| ------------ | ------------- | --------------- | +| 插入队首 | addFirst(E e) | offerFirst(E e) | +| 插入队尾 | addLast(E e) | offerLast(E e) | +| 删除队首 | removeFirst() | pollFirst() | +| 删除队尾 | removeLast() | pollLast() | +| 查询队首元素 | getFirst() | peekFirst() | +| 查询队尾元素 | getLast() | peekLast() | + +事实上,`Deque` 还提供有 `push()` 和 `pop()` 等其他方法,可用于模拟栈。 + +### ArrayDeque 与 LinkedList 的区别 + +`ArrayDeque` 和 `LinkedList` 都实现了 `Deque` 接口,两者都具有队列的功能,但两者有什么区别呢? + +- `ArrayDeque` 是基于可变长的数组和双指针来实现,而 `LinkedList` 则通过链表来实现。 +- `ArrayDeque` 不支持存储 `NULL` 数据,但 `LinkedList` 支持。 +- `ArrayDeque` 是在 JDK1.6 才被引入的,而`LinkedList` 早在 JDK1.2 时就已经存在。 +- `ArrayDeque` 插入时可能存在扩容过程,不过均摊后的插入操作依然为 O(1)。虽然 `LinkedList` 不需要扩容,但是每次插入数据时均需要申请新的堆空间,均摊性能相比更慢。 + +从性能的角度上,选用 `ArrayDeque` 来实现队列要比 `LinkedList` 更好。此外,`ArrayDeque` 也可以用于实现栈。 + +### 说一说 PriorityQueue + +`PriorityQueue` 是在 JDK1.5 中被引入的,其与 `Queue` 的区别在于元素出队顺序是与优先级相关的,即总是优先级最高的元素先出队。 + +这里列举其相关的一些要点: + +- `PriorityQueue` 利用了二叉堆的数据结构来实现的,底层使用可变长的数组来存储数据 +- `PriorityQueue` 通过堆元素的上浮和下沉,实现了在 O(logn) 的时间复杂度内插入元素和删除堆顶元素。 +- `PriorityQueue` 是非线程安全的,且不支持存储 `NULL` 和 `non-comparable` 的对象。 +- `PriorityQueue` 默认是小顶堆,但可以接收一个 `Comparator` 作为构造参数,从而来自定义元素优先级的先后。 + +`PriorityQueue` 在面试中可能更多的会出现在手撕算法的时候,典型例题包括堆排序、求第 K 大的数、带权图的遍历等,所以需要会熟练使用才行。 + +### BlockingQueue + +`BlockingQueue` (阻塞队列)是一个接口,继承自 `Queue`。`BlockingQueue` 阻塞的原因是其支持当队列没有元素时一直阻塞,直到有元素;还支持如果队列已满,一直等到队列可以放入新元素时再放入。 + +``` +public interface BlockingQueue extends Queue { + // ... +} +``` + +`BlockingQueue` 常用于生产者-消费者模型中,生产者线程会向队列中添加数据,而消费者线程会从队列中取出数据进行处理。 + +Java 中常用的阻塞队列实现类有以下几种: + +- `ArrayBlockingQueue`:使用数组实现的有界阻塞队列。在创建时需要指定容量大小,并支持公平和非公平两种方式的锁访问机制。 +- `LinkedBlockingQueue`:使用单向链表实现的可选有界阻塞队列。在创建时可以指定容量大小,如果不指定则默认为 `Integer.MAX_VALUE`。和 `ArrayBlockingQueue` 不同的是, 它仅支持非公平的锁访问机制。 +- `PriorityBlockingQueue`:支持优先级排序的无界阻塞队列。元素必须实现 `Comparable` 接口或者在构造函数中传入`Comparator` 对象,并且不能插入 null 元素。 +- `SynchronousQueue`:同步队列,是一种不存储元素的阻塞队列。每个插入操作都必须等待对应的删除操作,反之删除操作也必须等待插入操作。因此,`SynchronousQueue` 通常用于线程之间的直接传递数据。 +- `DelayQueue`:延迟队列,其中的元素只有到了其指定的延迟时间,才能够从队列中出队。 + +### ArrayBlockingQueue 和 LinkedBlockingQueue 有什么区别? + +`ArrayBlockingQueue` 和 `LinkedBlockingQueue` 是 Java 并发包中常用的两种阻塞队列实现,它们都是线程安全的。不过,不过它们之间也存在下面这些区别: + +- 底层实现:`ArrayBlockingQueue` 基于数组实现,而 `LinkedBlockingQueue` 基于链表实现。 +- 是否有界:`ArrayBlockingQueue` 是有界队列,必须在创建时指定容量大小。`LinkedBlockingQueue` 创建时可以不指定容量大小,默认是 `Integer.MAX_VALUE`,也就是无界的。但也可以指定队列大小,从而成为有界的。 +- 锁是否分离: `ArrayBlockingQueue`中的锁是没有分离的,即生产和消费用的是同一个锁;`LinkedBlockingQueue`中的锁是分离的,即生产用的是`putLock`,消费是`takeLock`,这样可以防止生产者和消费者线程之间的锁争夺。 +- 内存占用:`ArrayBlockingQueue` 需要提前分配数组内存,而 `LinkedBlockingQueue` 则是动态分配链表节点内存。这意味着,`ArrayBlockingQueue` 在创建时就会占用一定的内存空间,且往往申请的内存比实际所用的内存更大,而`LinkedBlockingQueue` 则是根据元素的增加而逐渐占用内存空间。 \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\270\211.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\270\211.md" new file mode 100644 index 0000000000..5af5ed6e01 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\270\211.md" @@ -0,0 +1,461 @@ +--- +title: Java 容器面试三 +date: 2024-07-03 07:44:02 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 + - 容器 +permalink: /pages/d9229779/ +--- + +# Java 容器面试三 + +## 集合判空 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **判断所有集合内部的元素是否为空,使用 `isEmpty()` 方法,而不是 `size()==0` 的方式。** + +这是因为 `isEmpty()` 方法的可读性更好,并且时间复杂度为 O(1)。 + +绝大部分我们使用的集合的 `size()` 方法的时间复杂度也是 O(1),不过,也有很多复杂度不是 O(1) 的,比如 `java.util.concurrent` 包下的某些集合(`ConcurrentLinkedQueue`、`ConcurrentHashMap`...)。 + +下面是 `ConcurrentHashMap` 的 `size()` 方法和 `isEmpty()` 方法的源码。 + +``` +public int size() { + long n = sumCount(); + return ((n < 0L) ? 0 : + (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE : + (int)n); +} +final long sumCount() { + CounterCell[] as = counterCells; CounterCell a; + long sum = baseCount; + if (as != null) { + for (int i = 0; i < as.length; ++i) { + if ((a = as[i]) != null) + sum += a.value; + } + } + return sum; +} +public boolean isEmpty() { + return sumCount() <= 0L; // ignore transient negative values +} +``` + +## 集合转 Map + +《阿里巴巴 Java 开发手册》的描述如下: + +> **在使用 `java.util.stream.Collectors` 类的 `toMap()` 方法转为 `Map` 集合时,一定要注意当 value 为 null 时会抛 NPE 异常。** + +``` +class Person { + private String name; + private String phoneNumber; + // getters and setters +} + +List bookList = new ArrayList<>(); +bookList.add(new Person("jack","18163138123")); +bookList.add(new Person("martin",null)); +// 空指针异常 +bookList.stream().collect(Collectors.toMap(Person::getName, Person::getPhoneNumber)); +``` + +下面我们来解释一下原因。 + +首先,我们来看 `java.util.stream.Collectors` 类的 `toMap()` 方法 ,可以看到其内部调用了 `Map` 接口的 `merge()` 方法。 + +``` +public static > +Collector toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction, + Supplier mapSupplier) { + BiConsumer accumulator + = (map, element) -> map.merge(keyMapper.apply(element), + valueMapper.apply(element), mergeFunction); + return new CollectorImpl<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID); +} +``` + +`Map` 接口的 `merge()` 方法如下,这个方法是接口中的默认实现。 + +> 如果你还不了解 Java 8 新特性的话,请看这篇文章:[《Java8 新特性总结》](https://mp.weixin.qq.com/s/ojyl7B6PiHaTWADqmUq2rw) 。 + +``` +default V merge(K key, V value, + BiFunction remappingFunction) { + Objects.requireNonNull(remappingFunction); + Objects.requireNonNull(value); + V oldValue = get(key); + V newValue = (oldValue == null) ? value : + remappingFunction.apply(oldValue, value); + if(newValue == null) { + remove(key); + } else { + put(key, newValue); + } + return newValue; +} +``` + +`merge()` 方法会先调用 `Objects.requireNonNull()` 方法判断 value 是否为空。 + +``` +public static T requireNonNull(T obj) { + if (obj == null) + throw new NullPointerException(); + return obj; +} +``` + +## 集合遍历 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **不要在 foreach 循环里进行元素的 `remove/add` 操作。remove 元素请使用 `Iterator` 方式,如果并发操作,需要对 `Iterator` 对象加锁。** + +通过反编译你会发现 foreach 语法底层其实还是依赖 `Iterator` 。不过, `remove/add` 操作直接调用的是集合自己的方法,而不是 `Iterator` 的 `remove/add`方法 + +这就导致 `Iterator` 莫名其妙地发现自己有元素被 `remove/add` ,然后,它就会抛出一个 `ConcurrentModificationException` 来提示用户发生了并发修改异常。这就是单线程状态下产生的 **fail-fast 机制**。 + +> **fail-fast 机制**:多个线程对 fail-fast 集合进行修改的时候,可能会抛出`ConcurrentModificationException`。 即使是单线程下也有可能会出现这种情况,上面已经提到过。 +> +> 相关阅读:[什么是 fail-fast](https://www.cnblogs.com/54chensongxia/p/12470446.html) 。 + +Java8 开始,可以使用 `Collection#removeIf()`方法删除满足特定条件的元素,如 + +``` +List list = new ArrayList<>(); +for (int i = 1; i <= 10; ++i) { + list.add(i); +} +list.removeIf(filter -> filter % 2 == 0); /* 删除 list 中的所有偶数 */ +System.out.println(list); /* [1, 3, 5, 7, 9] */ +``` + +除了上面介绍的直接使用 `Iterator` 进行遍历操作之外,你还可以: + +- 使用普通的 for 循环 +- 使用 fail-safe 的集合类。`java.util`包下面的所有的集合类都是 fail-fast 的,而`java.util.concurrent`包下面的所有的类都是 fail-safe 的。 +- …… + +## 集合去重 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **可以利用 `Set` 元素唯一的特性,可以快速对一个集合进行去重操作,避免使用 `List` 的 `contains()` 进行遍历去重或者判断包含操作。** + +这里我们以 `HashSet` 和 `ArrayList` 为例说明。 + +``` +// Set 去重代码示例 +public static Set removeDuplicateBySet(List data) { + + if (CollectionUtils.isEmpty(data)) { + return new HashSet<>(); + } + return new HashSet<>(data); +} + +// List 去重代码示例 +public static List removeDuplicateByList(List data) { + + if (CollectionUtils.isEmpty(data)) { + return new ArrayList<>(); + + } + List result = new ArrayList<>(data.size()); + for (T current : data) { + if (!result.contains(current)) { + result.add(current); + } + } + return result; +} +``` + +两者的核心差别在于 `contains()` 方法的实现。 + +`HashSet` 的 `contains()` 方法底部依赖的 `HashMap` 的 `containsKey()` 方法,时间复杂度接近于 O(1)(没有出现哈希冲突的时候为 O(1))。 + +``` +private transient HashMap map; +public boolean contains(Object o) { + return map.containsKey(o); +} +``` + +我们有 N 个元素插入进 Set 中,那时间复杂度就接近是 O (n)。 + +`ArrayList` 的 `contains()` 方法是通过遍历所有元素的方法来做的,时间复杂度接近是 O(n)。 + +``` +public boolean contains(Object o) { + return indexOf(o) >= 0; +} +public int indexOf(Object o) { + if (o == null) { + for (int i = 0; i < size; i++) + if (elementData[i]==null) + return i; + } else { + for (int i = 0; i < size; i++) + if (o.equals(elementData[i])) + return i; + } + return -1; +} +``` + +## 集合转数组 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **使用集合转数组的方法,必须使用集合的 `toArray(T[] array)`,传入的是类型完全一致、长度为 0 的空数组。** + +`toArray(T[] array)` 方法的参数是一个泛型数组,如果 `toArray` 方法中没有传递任何参数的话返回的是 `Object`类 型数组。 + +``` +String [] s= new String[]{ + "dog", "lazy", "a", "over", "jumps", "fox", "brown", "quick", "A" +}; +List list = Arrays.asList(s); +Collections.reverse(list); +//没有指定类型的话会报错 +s=list.toArray(new String[0]); +``` + +由于 JVM 优化,`new String[0]`作为`Collection.toArray()`方法的参数现在使用更好,`new String[0]`就是起一个模板的作用,指定了返回数组的类型,0 是为了节省空间,因为它只是为了说明返回的类型。详见:https://shipilev.net/blog/2016/arrays-wisdom-ancients/ + +## 数组转集合 + +《阿里巴巴 Java 开发手册》的描述如下: + +> **使用工具类 `Arrays.asList()` 把数组转换成集合时,不能使用其修改集合相关的方法, 它的 `add/remove/clear` 方法会抛出 `UnsupportedOperationException` 异常。** + +我在之前的一个项目中就遇到一个类似的坑。 + +`Arrays.asList()`在平时开发中还是比较常见的,我们可以使用它将一个数组转换为一个 `List` 集合。 + +``` +String[] myArray = {"Apple", "Banana", "Orange"}; +List myList = Arrays.asList(myArray); +//上面两个语句等价于下面一条语句 +List myList = Arrays.asList("Apple","Banana", "Orange"); +``` + +JDK 源码对于这个方法的说明: + +``` +/** + *返回由指定数组支持的固定大小的列表。此方法作为基于数组和基于集合的 API 之间的桥梁, + * 与 Collection.toArray() 结合使用。返回的 List 是可序列化并实现 RandomAccess 接口。 + */ +public static List asList(T... a) { + return new ArrayList<>(a); +} +``` + +下面我们来总结一下使用注意事项。 + +**问题一、不能直接使用 Arrays.asList 来转换基本类型数组** + +```java +int[] arr = { 1, 2, 3 }; +List list = Arrays.asList(arr); +log.info("list:{} size:{} class:{}", list, list.size(), list.get(0).getClass()); +``` + +在上面的示例中,通过 `Arrays.asList` 将 `int[]` 数组初始化为 `List` 后。这个`List` 包含的其实是一个 `int` 数组,整个 `List` 的元素个数是 1,元素类型是整数数组。 + +其原因是,只能是把 int 装箱为 Integer,不可能把 int 数组装箱为 Integer 数组。我们知 道,Arrays.asList 方法传入的是一个泛型 T 类型可变参数,最终 int 数组整体作为了一个 对象成为了泛型类型 T + +```java +public static List asList(T... a) { + return new ArrayList<>(a); +} +``` + +直接遍历这样的 List 必然会出现 Bug。 + +**问题二、使用集合的修改方法:`add()`、`remove()`、`clear()`会抛出异常。** + +Arrays.asList 返回的 List 并不是我们期望的 java.util.ArrayList,而是 Arrays 的内部类。这个内部类继承自 AbstractList 类,但没有覆写父类的 add、remove、clear 方法,而父类中的这几个方法默认会抛出 UnsupportedOperationException。 + +```java +String[] arr = { "1", "2", "3" }; +List list = Arrays.asList(arr); +list.add(4);//运行时报错:UnsupportedOperationException +list.remove(1);//运行时报错:UnsupportedOperationException +list.clear();//运行时报错:UnsupportedOperationException +``` + +下图是 `java.util.Arrays$ArrayList` 的简易源码,我们可以看到这个类重写的方法有哪些。 + +``` + private static class ArrayList extends AbstractList + implements RandomAccess, java.io.Serializable + { + ... + + @Override + public E get(int index) { + ... + } + + @Override + public E set(int index, E element) { + ... + } + + @Override + public int indexOf(Object o) { + ... + } + + @Override + public boolean contains(Object o) { + ... + } + + @Override + public void forEach(Consumer action) { + ... + } + + @Override + public void replaceAll(UnaryOperator operator) { + ... + } + + @Override + public void sort(Comparator c) { + ... + } + } +``` + +我们再看一下`java.util.AbstractList`的 `add/remove/clear` 方法就知道为什么会抛出 `UnsupportedOperationException` 了。 + +``` +public E remove(int index) { + throw new UnsupportedOperationException(); +} +public boolean add(E e) { + add(size(), e); + return true; +} +public void add(int index, E element) { + throw new UnsupportedOperationException(); +} + +public void clear() { + removeRange(0, size()); +} +protected void removeRange(int fromIndex, int toIndex) { + ListIterator it = listIterator(fromIndex); + for (int i=0, n=toIndex-fromIndex; i List arrayToList(final T[] array) { + final List l = new ArrayList(array.length); + + for (final T s : array) { + l.add(s); + } + return l; +} + +Integer [] myArray = { 1, 2, 3 }; +System.out.println(arrayToList(myArray).getClass());//class java.util.ArrayList +``` + +2、最简便的方法 + +``` +List list = new ArrayList<>(Arrays.asList("a", "b", "c")) +``` + +3、使用 Java8 的 `Stream`(推荐) + +``` +Integer [] myArray = { 1, 2, 3 }; +List myList = Arrays.stream(myArray).collect(Collectors.toList()); +//基本类型也可以实现转换(依赖 boxed 的装箱操作) +int [] myArray2 = { 1, 2, 3 }; +List myList = Arrays.stream(myArray2).boxed().collect(Collectors.toList()); +``` + +4、使用 Guava + +对于不可变集合,你可以使用 [`ImmutableList`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java) 类及其 [`of()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L101) 与 [`copyOf()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/ImmutableList.java#L225) 工厂方法:(参数不能为空) + +``` +List il = ImmutableList.of("string", "elements"); // from varargs +List il = ImmutableList.copyOf(aStringArray); // from array +``` + +对于可变集合,你可以使用 [`Lists`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java) 类及其 [`newArrayList()`](https://github.com/google/guava/blob/master/guava/src/com/google/common/collect/Lists.java#L87) 工厂方法: + +``` +List l1 = Lists.newArrayList(anotherListOrCollection); // from collection +List l2 = Lists.newArrayList(aStringArray); // from array +List l3 = Lists.newArrayList("or", "string", "elements"); // from varargs +``` + +5、使用 Apache Commons Collections + +``` +List list = new ArrayList(); +CollectionUtils.addAll(list, str); +``` + +6、 使用 Java9 的 `List.of()`方法 + +``` +Integer[] array = {1, 2, 3}; +List list = List.of(array); +``` + +## 使用 List.subList 进行切片操作居然会导致 OOM + +List.subList 返回的子 List 不是一个普通的 ArrayList。这个子 List 可以认为是原始 List 的视图,会和原始 List 相互影响。如果不注意,很可能会因此产生 OOM 问题。 + +如下代码所示,定义一个名为 data 的静态 List 来存放 Integer 的 List,[也就是说 data 的成员本身是包含了多个数字的 List。循环 1000 次,每次都从一个具有 10 万个 Integer 的 List 中,使用 subList 方法获得一个只包含一个数字的子 List,并把这个子 List 加入 data 变量: + +```java +private static List> data = new ArrayList<>(); + +private static void oom() { + for (int i = 0; i < 1000; i++) { + List rawList = IntStream.rangeClosed(1, 100000).boxed().collect(Collectors.toList()); + data.add(rawList.subList(0, 1)); + } +} +``` + +出现 OOM 的原因是,循环中的 1000 个具有 10 万个元素的 List 始终得不到回收,因为它始终被 subList 方法返回的 List 强引用。 + +## 参考资料 + +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\272\214.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\272\214.md" new file mode 100644 index 0000000000..e8de54e75f --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\256\271\345\231\250\351\235\242\350\257\225\344\272\214.md" @@ -0,0 +1,520 @@ +--- +title: Java 容器面试二 +date: 2024-07-03 07:44:02 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 + - 容器 +permalink: /pages/e3c58d1f/ +--- + +# Java 容器面试二 + +## Map + +### HashMap 和 Hashtable 的区别 + +Hashtable 是早期 Java 类库提供的一个哈希表实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。 + +HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。 + +二者的主要差别如下: + +| | HashMap | Hashtable | +| ------------ | ----------------------------- | -------------------------------------------- | +| 线程安全 | 非线程安全 | 线程安全(主要方法都用 `synchronized` 修饰) | +| 效率 | 性能好 | 性能差:互斥锁,势必影响性能 | +| 初始化容量 | 初始容量为 16 | 初始容量为 11 | +| 扩容方式 | 2N(N 为当前容量) | 2N + 1 | +| 是否允许空值 | 允许存储 null 的 key 和 value | 不允许存储 null 的 key 和 value | + +- **线程是否安全:** `HashMap` 是非线程安全的,`Hashtable` 是线程安全的,因为 `Hashtable` 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 `ConcurrentHashMap` 吧!); +- **效率:** 因为线程安全的问题,`HashMap` 要比 `Hashtable` 效率高一点。另外,`Hashtable` 基本被淘汰,不要在代码中使用它; +- **对 Null key 和 Null value 的支持:** `HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 `NullPointerException`。 +- **初始容量大小和每次扩充容量大小的不同:** ① 创建时如果不指定容量初始值,`Hashtable` 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。`HashMap` 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 `Hashtable` 会直接使用你给定的大小,而 `HashMap` 会将其扩充为 2 的幂次方大小(`HashMap` 中的`tableSizeFor()`方法保证,下面给出了源代码)。也就是说 `HashMap` 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。 +- **底层数据结构:** JDK1.8 以后的 `HashMap` 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)时,将链表转化为红黑树(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树),以减少搜索时间(后文中我会结合源码对这一过程进行分析)。`Hashtable` 没有这样的机制。 + +**`HashMap` 中带有初始容量的构造函数:** + +``` + public HashMap(int initialCapacity, float loadFactor) { + if (initialCapacity < 0) + throw new IllegalArgumentException("Illegal initial capacity: " + + initialCapacity); + if (initialCapacity > MAXIMUM_CAPACITY) + initialCapacity = MAXIMUM_CAPACITY; + if (loadFactor <= 0 || Float.isNaN(loadFactor)) + throw new IllegalArgumentException("Illegal load factor: " + + loadFactor); + this.loadFactor = loadFactor; + this.threshold = tableSizeFor(initialCapacity); + } + public HashMap(int initialCapacity) { + this(initialCapacity, DEFAULT_LOAD_FACTOR); + } +``` + +下面这个方法保证了 `HashMap` 总是使用 2 的幂作为哈希表的大小。 + +``` + /** + * Returns a power of two size for the given target capacity. + */ + static final int tableSizeFor(int cap) { + int n = cap - 1; + n |= n >>> 1; + n |= n >>> 2; + n |= n >>> 4; + n |= n >>> 8; + n |= n >>> 16; + return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1; + } +``` + +### HashMap 和 HashSet 区别 + +如果你看过 `HashSet` 源码的话就应该知道:`HashSet` 底层就是基于 `HashMap` 实现的。(`HashSet` 的源码非常非常少,因为除了 `clone()`、`writeObject()`、`readObject()`是 `HashSet` 自己不得不实现之外,其他方法都是直接调用 `HashMap` 中的方法。 + +| `HashMap` | `HashSet` | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| 实现了 `Map` 接口 | 实现 `Set` 接口 | +| 存储键值对 | 仅存储对象 | +| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 `Set` 中添加元素 | +| `HashMap` 使用键(Key)计算 `hashcode` | `HashSet` 使用成员对象来计算 `hashcode` 值,对于两个对象来说 `hashcode` 可能相同,所以`equals()`方法用来判断对象的相等性 | + +### HashMap、TreeMap、LinkedHashMap 的区别 + +大部分使用 Map 的场景,通常就是放入、访问或者删除,而对顺序没有特别要求,HashMap 在这种情况下基本是最好的选择。**HashMap 的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定**,比如: + +- equals 相等,hashCode 一定要相等。 +- 重写了 hashCode 也要重写 equals。 +- hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致。 +- equals 的对称、反射、传递等特性。 + +LinkedHashMap 和 TreeMap 都可以保证某种顺序,但二者还是非常不同的。 + +- LinkedHashMap 通常提供的是遍历顺序符合插入顺序,它的实现是通过为条目(键值对)维护一个双向链表。注意,通过特定构造函数,我们可以创建反映访问顺序的实例,所谓的 put、get、compute 等,都算作“访问”。 +- 对于 TreeMap,它的整体顺序是由键的顺序关系决定的,通过 Comparator 或 Comparable(自然顺序)来决定。 + +### HashSet 如何检查重复? + +以下内容摘自我的 Java 启蒙书《Head first java》第二版: + +> 当你把对象加入`HashSet`时,`HashSet` 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 `hashcode` 值作比较,如果没有相符的 `hashcode`,`HashSet` 会假设对象没有重复出现。但是如果发现有相同 `hashcode` 值的对象,这时会调用`equals()`方法来检查 `hashcode` 相等的对象是否真的相同。如果两者相同,`HashSet` 就不会让加入操作成功。 + +在 JDK1.8 中,`HashSet`的`add()`方法只是简单的调用了`HashMap`的`put()`方法,并且判断了一下返回值以确保是否有重复元素。直接看一下`HashSet`中的源码: + +``` +// Returns: true if this set did not already contain the specified element +// 返回值:当 set 中没有包含 add 的元素时返回真 +public boolean add(E e) { + return map.put(e, PRESENT)==null; +} +``` + +而在`HashMap`的`putVal()`方法中也能看到如下说明: + +``` +// Returns : previous value, or null if none +// 返回值:如果插入位置没有元素返回 null,否则返回上一个元素 +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { +... +} +``` + +也就是说,在 JDK1.8 中,实际上无论`HashSet`中是否已经存在了某元素,`HashSet`都会直接插入,只是会在`add()`方法的返回值处告诉我们插入前是否存在相同元素。 + +### HashMap 的底层实现 + +#### JDK1.8 之前 + +JDK1.8 之前 `HashMap` 底层是 **数组和链表** 结合在一起使用也就是 **链表散列**。HashMap 通过 key 的 `hashcode` 经过扰动函数处理过后得到 hash 值,然后通过 `(n - 1) & hash` 判断当前元素存放的位置(这里的 n 指的是数组的长度),如果当前位置存在元素的话,就判断该元素与要存入的元素的 hash 值以及 key 是否相同,如果相同的话,直接覆盖,不相同就通过拉链法解决冲突。 + +所谓扰动函数指的就是 HashMap 的 `hash` 方法。使用 `hash` 方法也就是扰动函数是为了防止一些实现比较差的 `hashCode()` 方法 换句话说使用扰动函数之后可以减少碰撞。 + +**JDK 1.8 HashMap 的 hash 方法源码:** + +JDK 1.8 的 hash 方法相比于 JDK 1.7 hash 方法更加简化,但是原理不变。 + +``` + static final int hash(Object key) { + int h; + // key.hashCode():返回散列值也就是 hashcode + // ^:按位异或 + // >>>: 无符号右移,忽略符号位,空位都以 0 补齐 + return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16); + } +``` + +对比一下 JDK1.7 的 HashMap 的 hash 方法源码。 + +``` +static int hash(int h) { + // This function ensures that hashCodes that differ only by + // constant multiples at each bit position have a bounded + // number of collisions (approximately 8 at default load factor). + + h ^= (h >>> 20) ^ (h >>> 12); + return h ^ (h >>> 7) ^ (h >>> 4); +} +``` + +相比于 JDK1.8 的 hash 方法 ,JDK 1.7 的 hash 方法的性能会稍差一点点,因为毕竟扰动了 4 次。 + +所谓 **“拉链法”** 就是:将链表和数组相结合。也就是说创建一个链表数组,数组中每一格就是一个链表。若遇到哈希冲突,则将冲突的值加到链表中即可。 + +#### JDK1.8 之后 + +相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。 + +> TreeMap、TreeSet 以及 JDK1.8 之后的 HashMap 底层都用到了红黑树。红黑树就是为了解决二叉查找树的缺陷,因为二叉查找树在某些情况下会退化成一个线性结构。 + +我们来结合源码分析一下 `HashMap` 链表到红黑树的转换。 + +**1、 `putVal` 方法中执行链表转红黑树的判断逻辑。** + +链表的长度大于 8 的时候,就执行 `treeifyBin` (转换红黑树)的逻辑。 + +``` +// 遍历链表 +for (int binCount = 0; ; ++binCount) { + // 遍历到链表最后一个节点 + if ((e = p.next) == null) { + p.next = newNode(hash, key, value, null); + // 如果链表元素个数大于 TREEIFY_THRESHOLD(8) + if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st + // 红黑树转换(并不会直接转换成红黑树) + treeifyBin(tab, hash); + break; + } + if (e.hash == hash && + ((k = e.key) == key || (key != null && key.equals(k)))) + break; + p = e; +} +``` + +**2、`treeifyBin` 方法中判断是否真的转换为红黑树。** + +``` +final void treeifyBin(Node[] tab, int hash) { + int n, index; Node e; + // 判断当前数组的长度是否小于 64 + if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY) + // 如果当前数组的长度小于 64,那么会选择先进行数组扩容 + resize(); + else if ((e = tab[index = (n - 1) & hash]) != null) { + // 否则才将列表转换为红黑树 + + TreeNode hd = null, tl = null; + do { + TreeNode p = replacementTreeNode(e, null); + if (tl == null) + hd = p; + else { + p.prev = tl; + tl.next = p; + } + tl = p; + } while ((e = e.next) != null); + if ((tab[index] = hd) != null) + hd.treeify(tab); + } +} +``` + +将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树。 + +### HashMap 的长度为什么是 2 的幂次方 + +为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀。我们上面也讲到了过了,Hash 值的范围值-2147483648 到 2147483647,前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。 + +**这个算法应该如何设计呢?** + +我们首先可能会想到采用 % 取余的操作来实现。但是,重点来了:“**取余 (%) 操作中如果除数是 2 的幂次则等价于与其除数减一的与 (&) 操作**(也就是说 hash%length==hash&(length-1) 的前提是 length 是 2 的 n 次方;)。” 并且 **采用二进制位操作 & 相对于 % 能够提高运算效率**,这就解释了 HashMap 的长度为什么是 2 的幂次方。 + +### HashMap 多线程操作导致死循环问题 + +JDK1.7 及之前版本的 `HashMap` 在多线程环境下扩容操作可能存在死循环问题,这是由于当一个桶位中有多个元素需要进行扩容时,多个线程同时对链表进行操作,头插法可能会导致链表中的节点指向错误的位置,从而形成一个环形链表,进而使得查询元素的操作陷入死循环无法结束。 + +为了解决这个问题,JDK1.8 版本的 HashMap 采用了尾插法而不是头插法来避免链表倒置,使得插入的节点永远都是放在链表的末尾,避免了链表中的环形结构。但是还是不建议在多线程下使用 `HashMap`,因为多线程下使用 `HashMap` 还是会存在数据覆盖的问题。并发环境下,推荐使用 `ConcurrentHashMap` 。 + +一般面试中这样介绍就差不多,不需要记各种细节,个人觉得也没必要记。如果想要详细了解 `HashMap` 扩容导致死循环问题,可以看看耗子叔的这篇文章:[Java HashMap 的死循环](https://coolshell.cn/articles/9606.html)。 + +### HashMap 为什么线程不安全? + +JDK1.7 及之前版本,在多线程环境下,`HashMap` 扩容时会造成死循环和数据丢失的问题。 + +数据丢失这个在 JDK1.7 和 JDK 1.8 中都存在,这里以 JDK 1.8 为例进行介绍。 + +JDK 1.8 后,在 `HashMap` 中,多个键值对可能会被分配到同一个桶(bucket),并以链表或红黑树的形式存储。多个线程对 `HashMap` 的 `put` 操作会导致线程不安全,具体来说会有数据覆盖的风险。 + +举个例子: + +- 两个线程 1,2 同时进行 put 操作,并且发生了哈希冲突(hash 函数计算出的插入下标是相同的)。 +- 不同的线程可能在不同的时间片获得 CPU 执行的机会,当前线程 1 执行完哈希冲突判断后,由于时间片耗尽挂起。线程 2 先完成了插入操作。 +- 随后,线程 1 获得时间片,由于之前已经进行过 hash 碰撞的判断,所有此时会直接进行插入,这就导致线程 2 插入的数据被线程 1 覆盖了。 + +``` +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + // ... + // 判断是否出现 hash 碰撞 + // (n - 1) & hash 确定元素存放在哪个桶中,桶为空,新生成结点放入桶中(此时,这个结点是放在数组中) + if ((p = tab[i = (n - 1) & hash]) == null) + tab[i] = newNode(hash, key, value, null); + // 桶中已经存在元素(处理 hash 冲突) + else { + // ... +} +``` + +还有一种情况是这两个线程同时 `put` 操作导致 `size` 的值不正确,进而导致数据覆盖的问题: + +1. 线程 1 执行 `if(++size > threshold)` 判断时,假设获得 `size` 的值为 10,由于时间片耗尽挂起。 +2. 线程 2 也执行 `if(++size > threshold)` 判断,获得 `size` 的值也为 10,并将元素插入到该桶位中,并将 `size` 的值更新为 11。 +3. 随后,线程 1 获得时间片,它也将元素放入桶位中,并将 size 的值更新为 11。 +4. 线程 1、2 都执行了一次 `put` 操作,但是 `size` 的值只增加了 1,也就导致实际上只有一个元素被添加到了 `HashMap` 中。 + +``` +public V put(K key, V value) { + return putVal(hash(key), key, value, false, true); +} + +final V putVal(int hash, K key, V value, boolean onlyIfAbsent, + boolean evict) { + // ... + // 实际大小大于阈值则扩容 + if (++size > threshold) + resize(); + // 插入后回调 + afterNodeInsertion(evict); + return null; +} +``` + +### ConcurrentHashMap 和 Hashtable 的区别 + +`ConcurrentHashMap` 和 `Hashtable` 的区别主要体现在实现线程安全的方式上不同。 + +- **底层数据结构:** JDK1.7 的 `ConcurrentHashMap` 底层采用 **分段的数组+链表** 实现,JDK1.8 采用的数据结构跟 `HashMap1.8` 的结构一样,数组+链表/红黑二叉树。`Hashtable` 和 JDK1.8 之前的 `HashMap` 的底层数据结构类似都是采用 **数组+链表** 的形式,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的; +- 实现线程安全的方式(重要): + - 在 JDK1.7 的时候,`ConcurrentHashMap` 对整个桶数组进行了分割分段 (`Segment`,分段锁),每一把锁只锁容器其中一部分数据(下面有示意图),多线程访问容器里不同数据段的数据,就不会存在锁竞争,提高并发访问率。 + - 到了 JDK1.8 的时候,`ConcurrentHashMap` 已经摒弃了 `Segment` 的概念,而是直接用 `Node` 数组+链表+红黑树的数据结构来实现,并发控制使用 `synchronized` 和 CAS 来操作。(JDK1.6 以后 `synchronized` 锁做了很多优化) 整个看起来就像是优化过且线程安全的 `HashMap`,虽然在 JDK1.8 中还能看到 `Segment` 的数据结构,但是已经简化了属性,只是为了兼容旧版本; + - **`Hashtable`(同一把锁)** : 使用 `synchronized` 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越来越激烈效率越低。 + +下面,我们再来看看两者底层数据结构的对比图。 + +**Hashtable** : + +[![Hashtable 的内部结构](https://camo.githubusercontent.com/944543a8d68c07837ae4e21d1109bf590808e261cb508bdc7a94f1e7f11d5864/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a646b312e375f686173686d61702e706e67)](https://camo.githubusercontent.com/944543a8d68c07837ae4e21d1109bf590808e261cb508bdc7a94f1e7f11d5864/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a646b312e375f686173686d61702e706e67) + +https://www.cnblogs.com/chengxiao/p/6842045.html%3E + +**JDK1.7 的 ConcurrentHashMap**: + +[![Java7 ConcurrentHashMap 存储结构](https://camo.githubusercontent.com/0645f636024f751eec39d72725429f531081e88caec417355d6b153c8b3362f8/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661375f636f6e63757272656e74686173686d61702e706e67)](https://camo.githubusercontent.com/0645f636024f751eec39d72725429f531081e88caec417355d6b153c8b3362f8/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661375f636f6e63757272656e74686173686d61702e706e67) + +`ConcurrentHashMap` 是由 `Segment` 数组结构和 `HashEntry` 数组结构组成。 + +`Segment` 数组中的每个元素包含一个 `HashEntry` 数组,每个 `HashEntry` 数组属于链表结构。 + +**JDK1.8 的 ConcurrentHashMap**: + +[![Java8 ConcurrentHashMap 存储结构](https://camo.githubusercontent.com/2a0628afc8c086f443e6c989246737ab99c88d774f376b05df2853ed7d48dffe/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661385f636f6e63757272656e74686173686d61702e706e67)](https://camo.githubusercontent.com/2a0628afc8c086f443e6c989246737ab99c88d774f376b05df2853ed7d48dffe/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661385f636f6e63757272656e74686173686d61702e706e67) + +JDK1.8 的 `ConcurrentHashMap` 不再是 **Segment 数组 + HashEntry 数组 + 链表**,而是 **Node 数组 + 链表 / 红黑树**。不过,Node 只能用于链表的情况,红黑树的情况需要使用 **`TreeNode`**。当冲突链表达到一定长度时,链表会转换成红黑树。 + +`TreeNode`是存储红黑树节点,被`TreeBin`包装。`TreeBin`通过`root`属性维护红黑树的根结点,因为红黑树在旋转的时候,根结点可能会被它原来的子节点替换掉,在这个时间点,如果有其他线程要写这棵红黑树就会发生线程不安全问题,所以在 `ConcurrentHashMap` 中`TreeBin`通过`waiter`属性维护当前使用这棵红黑树的线程,来防止其他线程的进入。 + +``` +static final class TreeBin extends Node { + TreeNode root; + volatile TreeNode first; + volatile Thread waiter; + volatile int lockState; + // values for lockState + static final int WRITER = 1; // set while holding write lock + static final int WAITER = 2; // set when waiting for write lock + static final int READER = 4; // increment value for setting read lock +... +} +``` + +### ConcurrentHashMap 线程安全的具体实现方式/底层具体实现 + +#### JDK1.8 之前 + +[![Java7 ConcurrentHashMap 存储结构](https://camo.githubusercontent.com/0645f636024f751eec39d72725429f531081e88caec417355d6b153c8b3362f8/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661375f636f6e63757272656e74686173686d61702e706e67)](https://camo.githubusercontent.com/0645f636024f751eec39d72725429f531081e88caec417355d6b153c8b3362f8/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661375f636f6e63757272656e74686173686d61702e706e67) + +首先将数据分为一段一段(这个“段”就是 `Segment`)的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据时,其他段的数据也能被其他线程访问。 + +**`ConcurrentHashMap` 是由 `Segment` 数组结构和 `HashEntry` 数组结构组成**。 + +`Segment` 继承了 `ReentrantLock`, 所以 `Segment` 是一种可重入锁,扮演锁的角色。`HashEntry` 用于存储键值对数据。 + +``` +static class Segment extends ReentrantLock implements Serializable { +} +``` + +一个 `ConcurrentHashMap` 里包含一个 `Segment` 数组,`Segment` 的个数一旦**初始化就不能改变**。 `Segment` 数组的大小默认是 16,也就是说默认可以同时支持 16 个线程并发写。 + +`Segment` 的结构和 `HashMap` 类似,是一种数组和链表结构,一个 `Segment` 包含一个 `HashEntry` 数组,每个 `HashEntry` 是一个链表结构的元素,每个 `Segment` 守护着一个 `HashEntry` 数组里的元素,当对 `HashEntry` 数组的数据进行修改时,必须首先获得对应的 `Segment` 的锁。也就是说,对同一 `Segment` 的并发写入会被阻塞,不同 `Segment` 的写入是可以并发执行的。 + +#### JDK1.8 之后 + +[![Java8 ConcurrentHashMap 存储结构](https://camo.githubusercontent.com/2a0628afc8c086f443e6c989246737ab99c88d774f376b05df2853ed7d48dffe/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661385f636f6e63757272656e74686173686d61702e706e67)](https://camo.githubusercontent.com/2a0628afc8c086f443e6c989246737ab99c88d774f376b05df2853ed7d48dffe/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6c6c656374696f6e2f6a617661385f636f6e63757272656e74686173686d61702e706e67) + +Java 8 几乎完全重写了 `ConcurrentHashMap`,代码量从原来 Java 7 中的 1000 多行,变成了现在的 6000 多行。 + +`ConcurrentHashMap` 取消了 `Segment` 分段锁,采用 `Node + CAS + synchronized` 来保证并发安全。数据结构跟 `HashMap` 1.8 的结构类似,数组+链表/红黑二叉树。Java 8 在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为 O(N))转换为红黑树(寻址时间复杂度为 O(log(N)))。 + +Java 8 中,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,就不会影响其他 Node 的读写,效率大幅提升。 + +### JDK 1.7 和 JDK 1.8 的 ConcurrentHashMap 实现有什么不同? + +- **线程安全实现方式**:JDK 1.7 采用 `Segment` 分段锁来保证安全, `Segment` 是继承自 `ReentrantLock`。JDK1.8 放弃了 `Segment` 分段锁的设计,采用 `Node + CAS + synchronized` 保证线程安全,锁粒度更细,`synchronized` 只锁定当前链表或红黑二叉树的首节点。 +- **Hash 碰撞解决方法** : JDK 1.7 采用拉链法,JDK1.8 采用拉链法结合红黑树(链表长度超过一定阈值时,将链表转换为红黑树)。 +- **并发度**:JDK 1.7 最大并发度是 Segment 的个数,默认是 16。JDK 1.8 最大并发度是 Node 数组的大小,并发度更大。 + +### ConcurrentHashMap 为什么 key 和 value 不能为 null? + +`ConcurrentHashMap` 的 key 和 value 不能为 null 主要是为了避免二义性。null 是一个特殊的值,表示没有对象或没有引用。如果你用 null 作为键,那么你就无法区分这个键是否存在于 `ConcurrentHashMap` 中,还是根本没有这个键。同样,如果你用 null 作为值,那么你就无法区分这个值是否是真正存储在 `ConcurrentHashMap` 中的,还是因为找不到对应的键而返回的。 + +拿 get 方法取值来说,返回的结果为 null 存在两种情况: + +- 值没有在集合中 ; +- 值本身就是 null。 + +这也就是二义性的由来。 + +具体可以参考 [ConcurrentHashMap 源码分析](https://javaguide.cn/java/collection/concurrent-hash-map-source-code.html) 。 + +多线程环境下,存在一个线程操作该 `ConcurrentHashMap` 时,其他的线程将该 `ConcurrentHashMap` 修改的情况,所以无法通过 `containsKey(key)` 来判断否存在这个键值对,也就没办法解决二义性问题了。 + +与此形成对比的是,`HashMap` 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个。如果传入 null 作为参数,就会返回 hash 值为 0 的位置的值。单线程环境下,不存在一个线程操作该 HashMap 时,其他的线程将该 `HashMap` 修改的情况,所以可以通过 `contains(key)`来做判断是否存在这个键值对,从而做相应的处理,也就不存在二义性问题。 + +也就是说,多线程下无法正确判定键值对是否存在(存在其他线程修改的情况),单线程是可以的(不存在其他线程修改的情况)。 + +如果你确实需要在 ConcurrentHashMap 中使用 null 的话,可以使用一个特殊的静态空对象来代替 null。 + +``` +public static final Object NULL = new Object(); +``` + +最后,再分享一下 `ConcurrentHashMap` 作者本人 (Doug Lea) 对于这个问题的回答: + +> The main reason that nulls aren't allowed in ConcurrentMaps (ConcurrentHashMaps, ConcurrentSkipListMaps) is that ambiguities that may be just barely tolerable in non-concurrent maps can't be accommodated. The main one is that if `map.get(key)` returns `null`, you can't detect whether the key explicitly maps to `null` vs the key isn't mapped. In a non-concurrent map, you can check this via `map.contains(key)`, but in a concurrent one, the map might have changed between calls. + +翻译过来之后的,大致意思还是单线程下可以容忍歧义,而多线程下无法容忍。 + +### ConcurrentHashMap 能保证复合操作的原子性吗? + +`ConcurrentHashMap` 是线程安全的,意味着它可以保证多个线程同时对它进行读写操作时,不会出现数据不一致的情况,也不会导致 JDK1.7 及之前版本的 `HashMap` 多线程操作导致死循环问题。但是,这并不意味着它可以保证所有的复合操作都是原子性的,一定不要搞混了! + +复合操作是指由多个基本操作(如`put`、`get`、`remove`、`containsKey`等)组成的操作,例如先判断某个键是否存在`containsKey(key)`,然后根据结果进行插入或更新`put(key, value)`。这种操作在执行过程中可能会被其他线程打断,导致结果不符合预期。 + +例如,有两个线程 A 和 B 同时对 `ConcurrentHashMap` 进行复合操作,如下: + +``` +// 线程 A +if (!map.containsKey(key)) { +map.put(key, value); +} +// 线程 B +if (!map.containsKey(key)) { +map.put(key, anotherValue); +} +``` + +如果线程 A 和 B 的执行顺序是这样: + +1. 线程 A 判断 map 中不存在 key +2. 线程 B 判断 map 中不存在 key +3. 线程 B 将 (key, anotherValue) 插入 map +4. 线程 A 将 (key, value) 插入 map + +那么最终的结果是 (key, value),而不是预期的 (key, anotherValue)。这就是复合操作的非原子性导致的问题。 + +**那如何保证 `ConcurrentHashMap` 复合操作的原子性呢?** + +`ConcurrentHashMap` 提供了一些原子性的复合操作,如 `putIfAbsent`、`compute`、`computeIfAbsent` 、`computeIfPresent`、`merge`等。这些方法都可以接受一个函数作为参数,根据给定的 key 和 value 来计算一个新的 value,并且将其更新到 map 中。 + +上面的代码可以改写为: + +``` +// 线程 A +map.putIfAbsent(key, value); +// 线程 B +map.putIfAbsent(key, anotherValue); +``` + +或者: + +``` +// 线程 A +map.computeIfAbsent(key, k -> value); +// 线程 B +map.computeIfAbsent(key, k -> anotherValue); +``` + +很多同学可能会说了,这种情况也能加锁同步呀!确实可以,但不建议使用加锁的同步机制,违背了使用 `ConcurrentHashMap` 的初衷。在使用 `ConcurrentHashMap` 的时候,尽量使用这些原子性的复合操作方法来保证原子性。 + +## Collections 工具类(不重要) + +**`Collections` 工具类常用方法**: + +- 排序 +- 查找,替换操作 +- 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合) + +### 排序操作 + +``` +void reverse(List list)//反转 +void shuffle(List list)//随机排序 +void sort(List list)//按自然排序的升序排序 +void sort(List list, Comparator c)//定制排序,由 Comparator 控制排序逻辑 +void swap(List list, int i , int j)//交换两个索引位置的元素 +void rotate(List list, int distance)//旋转。当 distance 为正数时,将 list 后 distance 个元素整体移到前面。当 distance 为负数时,将 list 的前 distance 个元素整体移到后面 +``` + +### 查找,替换操作 + +``` +int binarySearch(List list, Object key)//对 List 进行二分查找,返回索引,注意 List 必须是有序的 +int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比 int min(Collection coll) +int max(Collection coll, Comparator c)//根据定制排序,返回最大元素,排序规则由 Comparatator 类控制。类比 int min(Collection coll, Comparator c) +void fill(List list, Object obj)//用指定的元素代替指定 list 中的所有元素 +int frequency(Collection c, Object o)//统计元素出现次数 +int indexOfSubList(List list, List target)//统计 target 在 list 中第一次出现的索引,找不到则返回-1,类比 int lastIndexOfSubList(List source, list target) +boolean replaceAll(List list, Object oldVal, Object newVal)//用新元素替换旧元素 +``` + +### 同步控制 + +`Collections` 提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。 + +我们知道 `HashSet`,`TreeSet`,`ArrayList`,`LinkedList`,`HashMap`,`TreeMap` 都是线程不安全的。`Collections` 提供了多个静态方法可以把他们包装成线程同步的集合。 + +**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。** + +方法如下: + +``` +synchronizedCollection(Collection c) //返回指定 collection 支持的同步(线程安全的)collection。 +synchronizedList(List list)//返回指定列表支持的同步(线程安全的)List。 +synchronizedMap(Map m) //返回由指定映射支持的同步(线程安全的)Map。 +synchronizedSet(Set s) //返回指定 set 支持的同步(线程安全的)set。 +``` \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\270\200.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\270\200.md" new file mode 100644 index 0000000000..9105a7d511 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\270\200.md" @@ -0,0 +1,1215 @@ +--- +title: Java 并发面试一 +date: 2020-06-04 13:51:00 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 + - 并发 +permalink: /pages/bbf8a81d/ +--- + +# Java 并发面试一 + +## 并发术语 + +### 并发和并行 + +**典型问题** + +- 什么是并发? +- 什么是并行? +- 并发和并行有什么区别? + +**知识点** + +并发和并行是最容易让新手费解的概念,那么如何理解二者呢?其最关键的差异在于:是否是**同时**发生: + +- **并发**:是指具备处理多个任务的能力,但不一定要同时。 +- **并行**:是指具备同时处理多个任务的能力。 + +下面是我见过最生动的说明,摘自 [并发与并行的区别是什么?——知乎的高票答案](https://www.zhihu.com/question/33515481/answer/58849148) + +- 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 +- 你吃饭吃到一半,电话来了,你停了下来接了电话,接完后继续吃饭,这说明你支持并发。 +- 你吃饭吃到一半,电话来了,你一边打电话一边吃饭,这说明你支持并行。 + +### 同步和异步 + +**典型问题** + +- 什么是同步? +- 什么是异步? +- 同步和异步有什么区别? + +**知识点** + +- **同步**:是指在发出一个调用时,在没有得到结果之前,该调用就不返回。但是一旦调用返回,就得到返回值了。 +- **异步**:则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果。换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果。而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用。 + +举例来说明: + +- 同步就像是打电话:不挂电话,通话不会结束。 +- 异步就像是发短信:发完短信后,就可以做其他事;当收到回复短信时,手机会通过铃声或振动来提醒。 + +### 阻塞和非阻塞 + +**典型问题** + +- 什么是阻塞? +- 阻塞和非阻塞有什么区别? + +**知识点** + +阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态: + +- **阻塞**:是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回。 +- **非阻塞**:是指在不能立刻得到结果之前,该调用不会阻塞当前线程。 + +举例来说明: + +- 阻塞调用就像是打电话,通话不结束,不能放下。 +- 非阻塞调用就像是发短信,发完短信后,就可以做其他事,短信来了,手机会提醒。 + +### 进程、线程、协程、管程 + +**典型问题** + +- 什么是进程? +- 什么是线程? +- 什么是协程? +- 什么是管程? +- 进程和线程有什么区别? + +**知识点** + +- **进程(Process)** - 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。**进程可视为一个正在运行的程序**。 +- **线程(Thread)** - **线程是操作系统进行调度的基本单位**。 +- **管程(Monitor)** - **管程是指管理共享变量以及对共享变量的操作过程,让他们支持并发**。 + - Java 通过 synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法来实现管程技术。 + - **管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程**。 +- **协程(Coroutine)** - **协程可以理解为一种轻量级的线程**。 + - 从操作系统的角度来看,线程是在内核态中调度的,而协程是在用户态调度的,所以相对于线程来说,协程切换的成本更低。 + - 协程虽然也有自己的栈,但是相比线程栈要小得多,典型的线程栈大小差不多有 1M,而协程栈的大小往往只有几 K 或者几十 K。所以,无论是从时间维度还是空间维度来看,协程都比线程轻量得多。 + - Go、Python、Lua、Kotlin 等语言都支持协程;Java OpenSDK 中的 Loom 项目目标就是支持协程。 + +进程和线程的差异: + +- 一个程序至少有一个进程,一个进程至少有一个线程。 +- 线程比进程划分更细,所以执行开销更小,并发性更高 +- 进程是一个实体,拥有独立的资源;而同一个进程中的多个线程共享进程的资源。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/processes-vs-threads.jpg) + +JVM 在单个进程中运行,JVM 中的线程共享属于该进程的堆。这就是为什么几个线程可以访问同一个对象。线程共享堆并拥有自己的堆栈空间。这是一个线程如何调用一个方法以及它的局部变量是如何保持线程安全的。但是堆不是线程安全的并且为了线程安全必须进行同步。 + +#### 程序计数器为什么是私有的? + +程序计数器主要有下面两个作用: + +1. 字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。 +2. 在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。 + +需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。 + +所以,程序计数器私有主要是为了**线程切换后能恢复到正确的执行位置**。 + +#### 虚拟机栈和本地方法栈为什么是私有的? + +- **虚拟机栈:** 每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 +- **本地方法栈:** 和虚拟机栈所发挥的作用非常相似,区别是:**虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。** 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。 + +所以,为了**保证线程中的局部变量不被别的线程访问到**,虚拟机栈和本地方法栈是线程私有的。 + +#### 一句话简单了解堆和方法区 + +堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (几乎所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 + +## 并发概念 + +### Java 线程和操作系统的线程有啥区别? + +JDK 1.2 之前,Java 线程是基于绿色线程(Green Threads)实现的,这是一种用户级线程(用户线程),也就是说 JVM 自己模拟了多线程的运行,而不依赖于操作系统。由于绿色线程和原生线程比起来在使用时有一些限制(比如绿色线程不能直接使用操作系统提供的功能如异步 I/O、只能在一个内核线程上运行无法利用多核),在 JDK 1.2 及以后,Java 线程改为基于原生线程(Native Threads)实现,也就是说 JVM 直接使用操作系统原生的内核级线程(内核线程)来实现 Java 线程,由操作系统内核进行线程的调度和管理。 + +我们上面提到了用户线程和内核线程,考虑到很多读者不太了解二者的区别,这里简单介绍一下: + +- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。 +- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)。 + +顺便简单总结一下用户线程和内核线程的区别和特点:用户线程创建和切换成本低,但不可以利用多核。内核态线程,创建和切换成本高,可以利用多核。 + +一句话概括 Java 线程和操作系统线程的关系:**现在的 Java 线程的本质其实就是操作系统的线程**。 + +线程模型是用户线程和内核线程之间的关联方式,常见的线程模型有这三种: + +1. 一对一(一个用户线程对应一个内核线程) +2. 多对一(多个用户线程映射到一个内核线程) +3. 多对多(多个用户线程映射到多个内核线程) + +在 Windows 和 Linux 等主流操作系统中,Java 线程采用的是一对一的线程模型,也就是一个 Java 线程对应一个系统内核线程。Solaris 系统是一个特例(Solaris 系统本身就支持多对多的线程模型),HotSpot VM 在 Solaris 上支持多对多和一对一。具体可以参考 R 大的回答:[JVM 中的线程模型是用户级的么?](https://www.zhihu.com/question/23096638/answer/29617153)。 + +虚拟线程在 JDK 21 顺利转正,关于虚拟线程、平台线程(也就是我们上面提到的 Java 线程)和内核线程三者的关系可以阅读我写的这篇文章:[Java 20 新特性概览](https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/new-features/java20.md)。 + +### 并发(多线程)编程的好处是什么? + +- 更有效率的利用多处理器核心 +- 更快的响应时间 +- 更好的编程模型 + +### 单核 CPU 支持 Java 多线程吗? + +单核 CPU 是支持 Java 多线程的。操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。 + +这里顺带提一下 Java 使用的线程调度方式。 + +操作系统主要通过两种线程调度方式来管理多线程的执行: + +- **抢占式调度(Preemptive Scheduling)**:操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。这种方式存在上下文切换开销,但公平性和 CPU 资源利用率较好,不易阻塞。 +- **协同式调度(Cooperative Scheduling)**:线程执行完毕后,主动通知系统切换到另一个线程。这种方式可以减少上下文切换带来的性能开销,但公平性较差,容易阻塞。 + +Java 使用的线程调度是抢占式的。也就是说,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多。 + +### 并发和性能 + +**典型问题** + +**并发一定比串行更快吗?** + +**知识点** + +**并发不一定比串行更快!** + +对于多线程而言,它不仅可能会带来线程安全问题,还有可能会带来性能问题。 + +多线程会产生部分额外的开销: + +- **线程调度** + - **上下文切换** - 在实际开发中,线程数往往是大于 CPU 核心数的,比如 CPU 核心数可能是 8 核、16 核,等等,但线程数可能达到成百上千个。这种情况下,操作系统就会按照一定的调度算法,给每个线程分配时间片,让每个线程都有机会得到运行。而在进行调度时就会引起上下文切换,上下文切换会挂起当前正在执行的线程并保存当前的状态,然后寻找下一处即将恢复执行的代码,唤醒下一个线程,以此类推,反复执行。但上下文切换带来的开销是比较大的,假设我们的任务内容非常短,比如只进行简单的计算,那么就有可能发生我们上下文切换带来的性能开销比执行线程本身内容带来的开销还要大的情况。 + - **缓存失效** - 由于程序有很大概率会再次访问刚才访问过的数据,所以为了加速整个程序的运行,会使用缓存,这样我们在使用相同数据时就可以很快地获取数据。可一旦进行了线程调度,切换到其他线程,CPU就会去执行不同的代码,原有的缓存就很可能失效了,需要重新缓存新的数据,这也会造成一定的开销,所以线程调度器为了避免频繁地发生上下文切换,通常会给被调度到的线程设置最小的执行时间,也就是只有执行完这段时间之后,才可能进行下一次的调度,由此减少上下文切换的次数。 +- **线程协作** - 因为线程之间如果有共享数据,为了避免数据错乱,为了保证线程安全,就有可能禁止编译器和 CPU 对其进行重排序等优化,也可能出于同步的目的,反复把线程工作内存的数据 flush 到主存中,然后再从主内存 refresh 到其他线程的工作内存中,等等。这些问题在单线程中并不存在,但在多线程中为了确保数据的正确性,就不得不采取上述方法,因为线程安全的优先级要比性能优先级更高,这也间接降低了我们的性能。 + +### 并发安全 + +**经典问题** + +(1)有哪些线程不安全的情况 + +(2)哪些场景需要额外注意线程安全问题? + +**知识点** + +(1)有哪些线程不安全的情况 + +1. 运行结果错误; +2. 发布和初始化导致线程安全问题; +3. 活跃性问题。典型的有:死锁、活锁和饥饿 + 1. 死锁是指两个以上的线程永远相互阻塞的情况。 + 2. 活锁 - 活锁是指两个或多个线程在执行各自的逻辑时,相互之间不断地做出反应和改变状态,从而陷入无限循环,而这种循环不会导致任何线程向前推进。 + 3. 饥饿 - 饥饿是指线程需要某些资源时始终得不到,尤其是 CPU 资源,就会导致线程一直不能运行而产生的问题。 + +【示例】运行结果错误 + +```java +public class WrongResult { + + volatile static int i; + + public static void main(String[] args) throws InterruptedException { + + Runnable r = new Runnable() { + + @Override + + public void run() { + + for (int j = 0; j < 10000; j++) { + + i++; + + } + + } + + }; + + Thread thread1 = new Thread(r); + + thread1.start(); + + Thread thread2 = new Thread(r); + + thread2.start(); + + thread1.join(); + + thread2.join(); + + System.out.println(i); + + } + +} +``` + +启动两个线程,分别对变量 i 进行 10000 次 i++ 操作。理论上得到的结果应该是 20000,但实际结果却远小于理论结果。这是因为 i 变量虽然被修饰为 volatile,但由于 i++ 不是原子操作,而 volatile 无法保证原子性,这就导致两个线程在循环 ++ 操作时,无法及时感知 i 的数值变化,最终导致累加数值远小于预期值。 + +【示例】发布和初始化导致线程安全问题 + +```java +public class WrongInit { + + private Map students; + + public WrongInit() { + new Thread(() -> { + students = new HashMap<>(); + students.put(1, "王小美"); + students.put(2, "钱二宝"); + students.put(3, "周三"); + students.put(4, "赵四"); + }).start(); + } + + public Map getStudents() { + return students; + } + + public static void main(String[] args) throws InterruptedException { + WrongInit demo = new WrongInit(); + System.out.println(demo.getStudents().get(1)); + } + +} +``` + +(2)哪些场景需要额外注意线程安全问题? + +- **访问共享变量或资源** - 典型的场景有访问共享对象的属性,访问 static 静态变量,访问共享的缓存,等等。因为这些信息不仅会被一个线程访问到,还有可能被多个线程同时访问,那么就有可能在并发读写的情况下发生线程安全问题。 +- **依赖时序的操作** - 如果我们操作的正确性是依赖时序的,而在多线程的情况下又不能保障执行的顺序和我们预想的一致,这个时候就会发生线程安全问题。 +- **不同数据之间存在绑定关系** - 有时候,不同数据之间是成组出现的,存在着相互对应或绑定的关系,最典型的就是 IP 和端口号。有时候我们更换了 IP,往往需要同时更换端口号,如果没有把这两个操作绑定在一起,就有可能出现单独更换了 IP 或端口号的情况,而此时信息如果已经对外发布,信息获取方就有可能获取一个错误的 IP 与端口绑定情况,这时就发生了线程安全问题。 +- **对方没有声明自己是线程安全的** - 在我们使用其他类时,如果对方没有声明自己是线程安全的,那么这种情况下对其他类进行多线程的并发操作,就有可能会发生线程安全问题。举个例子,比如说我们定义了 ArrayList,它本身并不是线程安全的,如果此时多个线程同时对 ArrayList 进行并发读/写,那么就有可能会产生线程安全问题,造成数据出错,而这个责任并不在 ArrayList,因为它本身并不是并发安全的。 + +### 死锁 + +**典型问题** + +(1)什么是死锁? + +(2)如何预防和避免线程死锁? + +**知识点** + +(1)什么是死锁? + +**死锁是指两个以上的线程永远相互阻塞的情况**。产生死锁的四个必要条件: + +1. 互斥条件:该资源任意一个时刻只由一个线程占用。 +2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。 +3. 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。 +4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。 + +(2)如何预防和避免线程死锁? + +**如何预防死锁?** 破坏死锁的产生的必要条件即可: + +1. **破坏请求与保持条件**:一次性申请所有的资源。 +2. **破坏不剥夺条件**:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。 +3. **破坏循环等待条件**:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。 + +**如何避免死锁?** + +避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。 + +> **安全状态** 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 `` 序列为安全序列。 + +## 线程基础 + +### 线程生命周期 + +**典型问题** + +Java 线程生命周期中有哪些状态?各状态之间如何切换? + +**知识点** + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202407211143123.png) + +`java.lang.Thread.State` 中定义了 **6** 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。 + +以下是各状态的说明,以及状态间的联系: + +- **开始(NEW)** - 尚未调用 `start` 方法的线程处于此状态。此状态意味着:**创建的线程尚未启动**。 +- **可运行(RUNNABLE)** - 已经调用了 `start` 方法的线程处于此状态。此状态意味着,**线程已经准备好了**,一旦被线程调度器分配了 CPU 时间片,就可以运行线程。 + - 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。 +- **阻塞(BLOCKED)** - 此状态意味着:**线程处于被阻塞状态**。表示线程在等待 `synchronized` 的隐式锁(Monitor lock)。`synchronized` 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,即处于阻塞状态。当占用 `synchronized` 隐式锁的线程释放锁,并且等待的线程获得 `synchronized` 隐式锁时,就又会从 `BLOCKED` 转换到 `RUNNABLE` 状态。 +- **等待(WAITING)** - 此状态意味着:**线程无限期等待,直到被其他线程显式地唤醒**。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 `synchronized` 的隐式锁。而等待是主动的,通过调用 `Object.wait` 等方法进入。 + - 进入:`Object.wait()`;退出:`Object.notify` / `Object.notifyAll` + - 进入:`Thread.join()`;退出:被调用的线程执行完毕 + - 进入:`LockSupport.park()`;退出:`LockSupport.unpark` +- **定时等待(TIMED_WAITING)** - 等待指定时间的状态。一个线程处于定时等待状态,是由于执行了以下方法中的任意方法: + - 进入:`Thread.sleep(long)`;退出:时间结束 + - 进入:`Object.wait(long)`;退出:时间结束 / `Object.notify` / `Object.notifyAll` + - 进入:`Thread.join(long)`;退出:时间结束 / 被调用的线程执行完毕 + - 进入:`LockSupport.parkNanos(long)`;退出:`LockSupport.unpark` + - 进入:`LockSupport.parkUntil(long)`;退出:`LockSupport.unpark` +- **终止 (TERMINATED)** - 线程 `run()` 方法执行结束,或者因异常退出了 `run()` 方法,则该线程结束生命周期。死亡的线程不可再次复生。 + +> 👉 扩展阅读: +> +> - [Java Thread Methods and Thread States](https://www.w3resource.com/java-tutorial/java-threadclass-methods-and-threadstates.php) +> - [Java 线程的 5 种状态及切换(透彻讲解)](https://blog.csdn.net/pange1991/article/details/53860651) +> - [Java 线程运行怎么有第六种状态? - Dawell 的回答](https://www.zhihu.com/question/56494969/answer/154053599) + +### 线程创建 + +**典型问题** + +- Java 中,如何创建线程? +- Java 中,创建线程有几种方式? + +**知识点** + +一般来说,创建线程有很多种方式,例如: + +- 实现 `Runnable` 接口 +- 实现 `Callable` 接口 +- 继承 `Thread` 类 +- 通过线程池创建线程 +- 使用 `CompletableFuture` 创建线程 +- ... + +虽然,看似有多种多样的创建线程方式。但是,从本质上来说,Java 就只有一种方式可以创建线程,那就是通过 `new Thread().start() ` 创建。不管是哪种方式,最终还是依赖于 `new Thread().start()`。 + +> 👉 扩展阅读:[大家都说 Java 有三种创建线程的方式!并发编程中的惊天骗局!](https://mp.weixin.qq.com/s/NspUsyhEmKnJ-4OprRFp9g)。 + +### 线程启动 + +**典型问题** + +(1)`Thread.start()` 和 `Thread.run()` 有什么区别? + +(2)可以直接调用 `Thread.run()` 方法么? + +(3)一个线程两次调用 `Thread.start()` 方法会怎样 + +**知识点** + +(1)`Thread.start()` 和 `Thread.run()` 的区别: + +- `run()` 方法是线程的执行体。 +- `start()` 方法负责启动线程,然后 JVM 会让这个线程去执行 `run()` 方法。 + +(2)可以直接调用 `Thread.run()` 方法,但是它的行为和普通方法一样,不会启动新线程去执行。**调用 `start()` 方法方可启动线程并使线程进入就绪状态,直接执行 `run()` 方法的话不会以多线程的方式执行。** + +(3)Java 的线程是不允许启动两次的,第二次调用必然会抛出 `IllegalThreadStateException`。 + +### 线程等待 + +**典型问题** + +(1)`Thread.sleep()`、`Thread.yield()`、`Thread.join()`、`Object.wait()` 方法有什么区别? + +(2)为什么 `Thread.sleep()`、`Thread.yield()` 设计为静态方法? + +**知识点** + +(1)`Thread.sleep()`、`Thread.yield()`、`Thread.join()` 方法的区别: + +- `Thread.sleep()` + - `Thread.sleep()` 方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入 **Blocked** 状态。 + - 该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。 + - 但是,`Thread.sleep()` 方法不会释放“锁标志”,也就是说如果有 `synchronized` 同步块,其他线程仍然不能访问共享数据。 +- `Thread.yield()` + - `Thread.yield()` 方法可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程从 **Running** 状态转入 **Runnable** 状态。 + - 当某个线程调用了 `Thread.yield()` 方法暂停之后,只有优先级大于等于当前线程的处于就绪状态的线程才会获得执行的机会。 +- `Thread.join()` + - `Thread.join()` 方法会使当前线程转入 **Blocked** 状态,等待调用 `Thread.join()` 方法的线程结束后才能继续执行。 +- `Object.wait()` + - `Object.wait()` 用于使当前线程等待,直到其他线程调用相同对象的 `Object.notify()` 或 `Object.notifyAll()` 方法唤醒它。 + - 调用 `Object.wait()` 时,线程会释放对象锁,并进入等待状态。 + +(2)为什么 `Thread.sleep()`、`Thread.yield()` 设计为静态方法? + +`Thread.sleep()`、`Thread.yield()` 针对的是 **Running** 状态的线程,也就是说在非 **Running** 状态的线程上执行这两个方法没有意义。这就是为什么这两个方法被设计为静态的。它们只针对正在 **Running** 状态的线程工作,避免程序员错误的认为可以在其他非 **Running** 状态线程上调用。 + +> 👉 扩展阅读:[Java 线程中 yield 与 join 方法的区别](http://www.importnew.com/14958.html) +> 👉 扩展阅读:[sleep(),wait(),yield() 和 join() 方法的区别](https://blog.csdn.net/xiangwanpeng/article/details/54972952) + +### 线程通信 + +线程间通信是线程间共享资源的一种方式。`Object.wait()`, `Object.notify()` 和 `Object.notifyAll()` 是用于线程之间协作和通信的方法,它们通常与`synchronized` 关键字一起使用来实现线程的同步。 + +**典型问题** + +(1)为什么线程通信的方法 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 被定义在 `Object` 类里? + +(2)为什么 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 必须在 `synchronized` 方法/块中被调用? + +(3) `Object.wait()` 和 `Thread.sleep` 有什么区别? + +**知识点** + +(1)为什么线程通信的方法 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 被定义在 `Object` 类里? + +Java 的每个对象中都有一个称之为 monitor 监视器的锁,由于每个对象都可以上锁,这就要求在对象头中有一个用来保存锁信息的位置。这个锁是对象级别的,而非线程级别的,wait/notify/notifyAll 也都是锁级别的操作,它们的锁属于对象,所以把它们定义在 Object 类中是最合适,因为 Object 类是所有对象的父类。 + +如果把 wait/notify/notifyAll 方法定义在 Thread 类中,会带来很大的局限性,比如一个线程可能持有多把锁,以便实现相互配合的复杂逻辑,假设此时 wait 方法定义在 Thread 类中,如何实现让一个线程持有多把锁呢?又如何明确线程等待的是哪把锁呢?既然我们是让当前线程去等待某个对象的锁,自然应该通过操作对象来实现,而不是操作线程。 + +- `Object.wait()` + - `Object.wait()` 方法用于使当前线程进入等待状态,直到其他线程调用相同对象的 `notify()` 或 `notifyAll()` 方法唤醒它。 + - 在调用 `wait()` 方法时,线程会释放对象的锁,并进入等待状态。通常在使用 `wait()` 方法时需要放在一个循环中,以避免虚假唤醒(spurious wakeups)。 +- `Object.notify()` + - `Object.notify()` 方法用于唤醒正在等待该对象的锁的一个线程。 + - 被唤醒的线程将会尝试重新获取对象的锁,一旦获取到锁,它将继续执行。 +- `Object.notifyAll()` + - `Object.notifyAll()` 方法用于唤醒正在等待该对象的锁的所有线程。 + - 所有被唤醒的线程将会竞争对象的锁,一旦获取到锁,它们将继续执行。 + +(2)为什么 `Object.wait()`、`Object.notify()` 和 `Object.notifyAll()` 必须在 `synchronized` 方法/块中被调用? + +当一个线程需要调用对象的 `wait()` 方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 `notify()` 方法。同样的,当一个线程需要调用对象的 `notify()` 方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。 + +由于所有的这些方法都需要线程持有对象的锁,这样就只能通过 `synchronized` 来实现,所以他们只能在 `synchronized` 方法/块中被调用。 + +(3) `Object.wait()` 和 `Thread.sleep` 有什么区别? + +相同点: + +1. 它们都可以让线程阻塞。 +2. 它们都可以响应 interrupt 中断:在等待的过程中如果收到中断信号,都可以进行响应,并抛出 InterruptedException 异常。 + +不同点: + +1. wait 方法必须在 synchronized 保护的代码中使用,而 sleep 方法并没有这个要求。 +2. 在同步代码中执行 sleep 方法时,并不会释放 monitor 锁,但执行 wait 方法时会主动释放 monitor 锁。 +3. sleep 方法中会要求必须定义一个时间,时间到期后会主动恢复,而对于没有参数的 wait 方法而言,意味着永久等待,直到被中断或被唤醒才能恢复,它并不会主动恢复。 +4. wait/notify 是 Object 类的方法,而 sleep 是 Thread 类的方法。 + +> 👉 扩展阅读:[Java 并发编程:线程间协作的两种方式:wait、notify、notifyAll 和 Condition](http://www.cnblogs.com/dolphin0520/p/3920385.html) + +### 线程终止 + +**经典问题** + +(1)如何正确停止线程? + +(2)可以使用 `Thread.stop`,`Thread.suspend` 和 `Thread.resume` 停止线程吗?为什么? + +(3)使用 `volatile` 标记方式停止线程正确吗? + +**知识点** + +(1)如何正确停止线程? + +通常情况下,我们不会手动停止一个线程,而是允许线程运行到结束,然后让它自然停止。但是依然会有许多特殊的情况需要我们提前停止线程,比如:用户突然关闭程序,或程序运行出错重启等。 + +**对于 Java 而言,最正确的停止线程的方式是:通过 `Thread.interrupt` 和 `Thread.isInterrupted` 配合来控制线程终止**。但 `Thread.interrupt` 仅仅起到通知被停止线程的作用。而对于被停止的线程而言,它拥有完全的自主权,它既可以选择立即停止,也可以选择一段时间后停止,也可以选择压根不停止。 + +事实上,Java 希望程序间能够相互通知、相互协作地管理线程,因为如果不了解对方正在做的工作,贸然强制停止线程就可能会造成一些安全的问题,为了避免造成问题就需要给对方一定的时间来整理收尾工作。比如:线程正在写入一个文件,这时收到终止信号,它就需要根据自身业务判断,是选择立即停止,还是将整个文件写入成功后停止,而如果选择立即停止就可能造成数据不完整,不管是中断命令发起者,还是接收者都不希望数据出现问题。 + +一旦调用某个线程的 `Thread.interrupt` 之后,这个线程的中断标记位就会被设置成 `true`。每个线程都有这样的标记位,当线程执行时,应该定期检查这个标记位,如果标记位被设置成 `true`,就说明有程序想终止该线程。回到源码,可以看到在 `while` 循环体判断语句中,首先通过 `Thread.currentThread().isInterrupt()` 判断线程是否被中断,随后检查是否还有工作要做。&& 逻辑表示只有当两个判断条件同时满足的情况下,才会去执行下面的工作。 + +需要留意一个特殊场景:**`Thread.sleep` 后,线程依然可以感知 `Thread.interrupt`**。 + +【示例】正确停止线程的方式——`Thread.interrupt` + +```java +public class ThreadStopDemo { + + public static void main(String[] args) throws Exception { + Thread thread = new Thread(new MyTask(), "MyTask"); + thread.start(); + TimeUnit.MILLISECONDS.sleep(10); + thread.interrupt(); + } + + private static class MyTask implements Runnable { + + private long count = 0L; + + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + " 线程启动"); + // 通过 Thread.interrupted 和 interrupt 配合来控制线程终止 + while (!Thread.currentThread().isInterrupted() && count < 10000) { + System.out.println("count = " + count++); + } + System.out.println(Thread.currentThread().getName() + " 线程终止"); + } + + } + +} +// 输出(count 未到 10000,线程就主动结束): +// MyTask 线程启动 +// count = 0 +// count = 1 +// ... +// count = 840 +// count = 841 +// count = 842 +// MyTask 线程终止 +``` + +(2)可以使用 `Thread.stop`,`Thread.suspend` 和 `Thread.resume` 停止线程吗?为什么? + +`Thread.stop`,`Thread.suspend` 和 `Thread.resume` 方法已经被 Java 标记为 `@Deprecated`。为什么废弃呢? + +- **`Thread.stop` 会直接把线程停止,这样就没有给线程足够的时间来处理想要在停止前保存数据的逻辑,任务戛然而止,会导致出现数据完整性等问题**。 +- 而对于`Thread.suspend` 和 `Thread.resume` 而言,它们的问题在于:**如果线程调用 `Thread.suspend`,它并不会释放锁,就开始进入休眠,但此时有可能仍持有锁,这样就容易导致死锁问题**。因为这把锁在线程被 `Thread.resume` 之前,是不会被释放的。假设线程 A 调用了 `Thread.suspend` 方法让线程 B 挂起,线程 B 进入休眠,而线程 B 又刚好持有一把锁,此时假设线程 A 想访问线程 B 持有的锁,但由于线程 B 并没有释放锁就进入休眠了,所以对于线程 A 而言,此时拿不到锁,也会陷入阻塞,那么线程 A 和线程 B 就都无法继续向下执行。 + +【示例】`Thread.stop` 终止线程,导致线程任务戛然而止 + +```java +public class ThreadStopErrorDemo { + + public static void main(String[] args) { + MyTask thread = new MyTask(); + thread.start(); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + // 终止线程 + thread.stop(); + // 确保线程终止后,才执行下面的代码 + while (thread.isAlive()) { } + // 输出两个计数器的最终状态 + thread.print(); + } + + /** + * 持有两个计数器,run 方法中每次执行都会使计数器自增 + */ + private static class MyTask extends Thread { + + private int i = 0; + + private int j = 0; + + @Override + public void run() { + synchronized (this) { + ++i; + try { + // 模拟耗时操作 + TimeUnit.SECONDS.sleep(5); + } catch (InterruptedException e) { + e.printStackTrace(); + } + ++j; + } + } + + public void print() { + System.out.println("i=" + i + " j=" + j); + } + + } + +} +``` + +(3)使用 `volatile` 标记方式停止线程正确吗? + +使用 `volatile` 标记方式停止线程并不总是正确的。虽然 `volatile` 变量可以确保可见性,即当一个线程修改了 `volatile` 变量的值,其他线程能够立即看到最新的值,但它并不能保证原子性,也就是说并不能保证多个线程对 `volatile` 变量的操作是互斥的。 + +当我们使用 `volatile` 变量来控制线程的停止,通常是通过设置一个 `volatile` 标志位来告诉线程停止执行。例如: + +```java +public class MyTask extends Thread { + private volatile boolean canceled = false; + + public void run() { + while (!canceled) { + // 执行任务 + } + } + + public void stopTask() { + canceled = true; + } +} +``` + +在上述例子中,`canceled` 是一个 `volatile` 变量,用来控制线程的停止。虽然这种方式在某些情况下可以工作,但它并不是一个可靠的停止线程的方式,因为在多线程环境中,其他线程修改 `canceled` 的值时,可能会出现竞态条件,导致线程无法正确停止。 + +### 线程优先级 + +**典型问题** + +(1)Java 的线程优先级如何控制? + +(2)高优先级的 Java 线程一定先执行吗? + +**知识点** + +(1)Java 中的线程优先级的范围是 `[1,10]`,一般来说,高优先级的线程在运行时会具有优先权。可以通过 `thread.setPriority(Thread.MAX_PRIORITY)` 的方式设置,默认优先级为 `5`。 + +(2)即使设置了线程的优先级,也**无法保证高优先级的线程一定先执行**。 + +这是因为 **Java 线程优先级依赖于操作系统的支持**,然而,不同的操作系统支持的线程优先级并不相同,不能很好的和 Java 中线程优先级一一对应。因此,Java 线程优先级控制并不可靠。 + +### 守护线程 + +**典型问题** + +(1)什么是守护线程? + +(2)如何创建守护线程? + +**知识点** + +(1)什么是守护线程? + +守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程。与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。 + +守护线程的优先级比较低,一般用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。 + +(2)创建守护线程的方式: + +- 使用 `thread.setDaemon(true)` 可以设置 thread 线程为守护线程。 +- 正在运行的用户线程无法设置为守护线程,所以 `thread.setDaemon(true)` 必须在 `thread.start()` 之前设置,否则会抛出 `llegalThreadStateException` 异常; +- 一个守护线程创建的子线程依然是守护线程。 +- 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。 + +> 👉 扩展阅读:[Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) + +## volatile + +被 `volatile` 关键字修饰的变量有两层含义: + +- **保证变量的可见性** +- **防止 JVM 的指令重排序** + +### volatile 保证线程可见性 + +**典型问题** + +- `volatile` 有什么作用? +- Java 中,如何保证变量的可见性? + +**知识点** + +**在 Java 并发场景中,`volatile` 可以保证线程可见性**。保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个共享变量,另外一个线程能读到这个修改的值。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102230327.png) + +`volatile` 关键字其实并非是 Java 语言特有的,在 C 语言里也有,它最原始的意义就是禁用 CPU 缓存。如果我们将一个变量使用 `volatile` 修饰,这就指示 编译器,这个变量是共享且不稳定的,每次使用它都到主存中进行读取。 + +`volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。 + +### volatile 防止 JVM 的指令重排序 + +**典型问题** + +- `volatile` 有什么作用? +- Java 中,如何防止 JVM 的指令重排序? + +**知识点** + +观察加入 `volatile` 关键字和没有加入 `volatile` 关键字时所生成的汇编代码发现,**加入 `volatile` 关键字时,会多出一个 `lock` 前缀指令**。 + +**`lock` 前缀指令实际上相当于一个内存屏障**(也成内存栅栏),内存屏障会提供 3 个功能: + +- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; +- 它会强制将对缓存的修改操作立即写入主存; +- 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。 + +在 Java 中,`Unsafe` 类提供了三个开箱即用的内存屏障相关的方法,屏蔽了操作系统底层的差异: + +```java +public native void loadFence(); +public native void storeFence(); +public native void fullFence(); +``` + +理论上来说,你通过这个三个方法也可以实现和 `volatile` 禁止重排序一样的效果,只是会麻烦一些。 + +下面我以一个常见的面试题为例讲解一下 `volatile` 关键字禁止指令重排序的效果。 + +面试中面试官经常会说:“单例模式了解吗?来给我手写一下!给我解释一下双重检验锁方式实现单例模式的原理呗!” + +**双重校验锁实现对象单例(线程安全)**: + +```java +public class Singleton { + + private volatile static Singleton uniqueInstance; + + private Singleton() { + } + + public static Singleton getUniqueInstance() { + // 先判断对象是否已经实例过,没有实例化过才进入加锁代码 + if (uniqueInstance == null) { + // 类对象加锁 + synchronized (Singleton.class) { + if (uniqueInstance == null) { + uniqueInstance = new Singleton(); + } + } + } + return uniqueInstance; + } +} +``` + +`uniqueInstance` 采用 `volatile` 关键字修饰也是很有必要的, `uniqueInstance = new Singleton();` 这段代码其实是分为三步执行: + +1. 为 `uniqueInstance` 分配内存空间 +2. 初始化 `uniqueInstance` +3. 将 `uniqueInstance` 指向分配的内存地址 + +但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 `getUniqueInstance`() 后发现 `uniqueInstance` 不为空,因此返回 `uniqueInstance`,但此时 `uniqueInstance` 还未被初始化。 + +### volatile 不保证原子性 + +**问题点** + +- volatile 能保证原子性吗? +- volatile 能完全保证并发安全吗? + +**知识点** + +线程安全需要具备:可见性、原子性、顺序性。**`volatile` 不保证原子性,所以决定了它不能彻底地保证线程安全**。 + +我们通过下面的代码即可证明: + +```java +public class VolatileAtomicityDemo { + public volatile static int inc = 0; + + public void increase() { + inc++; + } + + public static void main(String[] args) throws InterruptedException { + ExecutorService threadPool = Executors.newFixedThreadPool(5); + VolatileAtomicityDemo volatileAtomicityDemo = new VolatileAtomicityDemo(); + for (int i = 0; i < 5; i++) { + threadPool.execute(() -> { + for (int j = 0; j < 500; j++) { + volatileAtomicityDemo.increase(); + } + }); + } + // 等待 1.5 秒,保证上面程序执行完成 + Thread.sleep(1500); + System.out.println(inc); + threadPool.shutdown(); + } +} +``` + +正常情况下,运行上面的代码理应输出 `2500`。但你真正运行了上面的代码之后,你会发现每次输出结果都小于 `2500`。 + +为什么会出现这种情况呢?不是说好了,`volatile` 可以保证变量的可见性嘛! + +也就是说,如果 `volatile` 能保证 `inc++` 操作的原子性的话。每个线程中对 `inc` 变量自增完之后,其他线程可以立即看到修改后的值。5 个线程分别进行了 500 次操作,那么最终 inc 的值应该是 5\*500=2500。 + +很多人会误认为自增操作 `inc++` 是原子性的,实际上,`inc++` 其实是一个复合操作,包括三步: + +1. 读取 inc 的值。 +2. 对 inc 加 1。 +3. 将 inc 的值写回内存。 + +`volatile` 是无法保证这三个操作是具有原子性的,有可能导致下面这种情况出现: + +1. 线程 1 对 `inc` 进行读取操作之后,还未对其进行修改。线程 2 又读取了 `inc` 的值并对其进行修改(+1),再将 `inc` 的值写回内存。 +2. 线程 2 操作完毕后,线程 1 对 `inc` 的值进行修改(+1),再将 `inc` 的值写回内存。 + +这也就导致两个线程分别对 `inc` 进行了一次自增操作后,`inc` 实际上只增加了 1。 + +其实,如果想要保证上面的代码运行正确也非常简单,利用 `synchronized`、`Lock` 或者 `AtomicInteger` 都可以。 + +使用 `synchronized` 改进: + +```java +public synchronized void increase() { + inc++; +} +``` + +使用 `AtomicInteger` 改进: + +```java +public AtomicInteger inc = new AtomicInteger(); + +public void increase() { + inc.getAndIncrement(); +} +``` + +使用 `ReentrantLock` 改进: + +```java +Lock lock = new ReentrantLock(); +public void increase() { + lock.lock(); + try { + inc++; + } finally { + lock.unlock(); + } +} +``` + +### volatile 和 synchronized + +**典型问题** + +`volatile` 和 `synchronized` 有什么区别?`volatile` 能替代 `synchronized` ? + +**知识点** + +**`volatile` 无法替代 `synchronized` ,因为 `volatile` 无法保证操作的原子性**。 + +- `volatile` 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;`synchronized` 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 +- `volatile` 仅能修饰变量;`synchronized` 可以修饰方法和代码块。 +- `volatile` 仅能实现变量的修改可见性,不能保证原子性;而 `synchronized` 则可以保证变量的修改可见性和原子性 +- `volatile` 不会造成线程的阻塞;`synchronized` 可能会造成线程的阻塞。 +- `volatile` 标记的变量不会被编译器优化;`synchronized` 标记的变量可以被编译器优化。 + +## synchronized + +`synchronized` 有 3 种应用方式: + +- **同步实例方法** - 对于普通同步方法,锁是当前实例对象 +- **同步静态方法** - 对于静态同步方法,锁是当前类的 `Class` 对象 +- **同步代码块** - 对于同步方法块,锁是 `synchonized` 括号里配置的对象 + +**原理** + +`synchronized` 经过编译后,会在同步块的前后分别形成 `monitorenter` 和 `monitorexit` 这两个字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。如果 `synchronized` 明确制定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据 `synchronized` 修饰的是实例方法还是静态方法,去对对应的对象实例或 `Class` 对象来作为锁对象。 + +`synchronized` 同步块对同一线程来说是可重入的,不会出现锁死问题。 + +`synchronized` 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。 + +**优化** + +Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平。 + +`synchronized` 的优化是将锁粒度分为不同级别,`synchronized` 会根据运行状态动态的由低到高调整锁级别(**偏向锁** -> **轻量级锁** -> **重量级锁**),以减少阻塞。 + +**同步方法 or 同步块?** + +- 同步块是更好的选择。 +- 因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。 + +### synchronized 作用 + +**典型问题** + +`synchronized` 有什么作用? + +**知识点** + +**`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 + +`synchronized` 同步块对同一线程来说是可重入的,不会出现锁死问题。 + +`synchronized` 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。 + +在 Java 早期版本中,`synchronized` 属于 **重量级锁**,效率低下。这是因为监视器锁(monitor)是依赖于底层的操作系统的 `Mutex Lock` 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高。 + +不过,在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多。因此, `synchronized` 还是可以在实际项目中使用的,像 JDK 源码、很多开源框架都大量使用了 `synchronized` 。 + +关于偏向锁多补充一点:由于偏向锁增加了 JVM 的复杂性,同时也并没有为所有应用都带来性能提升。因此,在 JDK15 中,偏向锁被默认关闭(仍然可以使用 `-XX:+UseBiasedLocking` 启用偏向锁),在 JDK18 中,偏向锁已经被彻底废弃(无法通过命令行打开)。 + +### synchronized 用法 + +**典型问题** + +- synchronized 可以用在哪些场景? +- synchronized 如何使用? + +**知识点** + +`synchronized` 关键字的使用方式主要有下面 3 种: + +1. 修饰实例方法 +2. 修饰静态方法 +3. 修饰代码块 + +**1、修饰实例方法** (锁当前对象实例) + +给当前对象实例加锁,进入同步代码前要获得 **当前对象实例的锁** 。 + +``` +synchronized void method() { + // 业务代码 +} +``` + +**2、修饰静态方法** (锁当前类) + +给当前类加锁,会作用于类的所有对象实例 ,进入同步代码前要获得 **当前 class 的锁**。 + +这是因为静态成员不属于任何一个实例对象,归整个类所有,不依赖于类的特定实例,被类的所有实例共享。 + +``` +synchronized static void method() { + // 业务代码 +} +``` + +静态 `synchronized` 方法和非静态 `synchronized` 方法之间的调用互斥么?不互斥!如果一个线程 A 调用一个实例对象的非静态 `synchronized` 方法,而线程 B 需要调用这个实例对象所属类的静态 `synchronized` 方法,是允许的,不会发生互斥现象,因为访问静态 `synchronized` 方法占用的锁是当前类的锁,而访问非静态 `synchronized` 方法占用的锁是当前实例对象锁。 + +**3、修饰代码块** (锁指定对象 / 类) + +对括号里指定的对象 / 类加锁: + +- `synchronized(object)` 表示进入同步代码库前要获得 **给定对象的锁**。 +- `synchronized(类。class)` 表示进入同步代码前要获得 **给定 Class 的锁** + +``` +synchronized(this) { + // 业务代码 +} +``` + +**总结:** + +- `synchronized` 关键字加到 `static` 静态方法和 `synchronized(class)` 代码块上都是是给 Class 类上锁; +- `synchronized` 关键字加到实例方法上是给对象实例上锁; +- 尽量不要使用 `synchronized(String a)` 因为 JVM 中,字符串常量池具有缓存功能。 + +### 构造方法可以用 synchronized 修饰么? + +构造方法不能使用 synchronized 关键字修饰。不过,可以在构造方法内部使用 synchronized 代码块。 + +另外,构造方法本身是线程安全的,但如果在构造方法中涉及到共享资源的操作,就需要采取适当的同步措施来保证整个构造过程的线程安全。 + +### synchronized 底层原理了解吗? + +`synchronized` 经过编译后,会在同步块的前后分别形成 `monitorenter` 和 `monitorexit` 这两个字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。如果 `synchronized` 明确制定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据 `synchronized` 修饰的是实例方法还是静态方法,去对对应的对象实例或 `Class` 对象来作为锁对象。 + +synchronized 关键字底层原理属于 JVM 层面的东西。 + +#### synchronized 同步语句块的情况 + +``` +public class SynchronizedDemo { + public void method() { + synchronized (this) { + System.out.println("synchronized 代码块"); + } + } +} +``` + +通过 JDK 自带的 `javap` 命令查看 `SynchronizedDemo` 类的相关字节码信息:首先切换到类的对应目录执行 `javac SynchronizedDemo.java` 命令生成编译后的 .class 文件,然后执行 `javap -c -s -v -l SynchronizedDemo.class`。 + +[![synchronized 关键字原理](https://camo.githubusercontent.com/669b67b48f1e58c37ac12eb80239cc5df7df55d7d75f9187e1622ee401a0c230/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f73796e6368726f6e697a65642d7072696e6369706c652e706e67)](https://camo.githubusercontent.com/669b67b48f1e58c37ac12eb80239cc5df7df55d7d75f9187e1622ee401a0c230/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f73796e6368726f6e697a65642d7072696e6369706c652e706e67) + +从上面我们可以看出:**`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。** + +上面的字节码中包含一个 `monitorenter` 指令以及两个 `monitorexit` 指令,这是为了保证锁在同步代码块代码正常执行以及出现异常的这两种情况下都能被正确释放。 + +当执行 `monitorenter` 指令时,线程试图获取锁也就是获取 **对象监视器 `monitor`** 的持有权。 + +> 在 Java 虚拟机 (HotSpot) 中,Monitor 是基于 C++ 实现的,由 [ObjectMonitor](https://github.com/openjdk-mirror/jdk7u-hotspot/blob/50bdefc3afe944ca74c3093e7448d6b889cd20d1/src/share/vm/runtime/objectMonitor.cpp) 实现的。每个对象中都内置了一个 `ObjectMonitor` 对象。 +> +> 另外,`wait/notify` 等方法也依赖于 `monitor` 对象,这就是为什么只有在同步的块或者方法中才能调用 `wait/notify` 等方法,否则会抛出 `java.lang.IllegalMonitorStateException` 的异常的原因。 + +在执行 `monitorenter` 时,会尝试获取对象的锁,如果锁的计数器为 0 则表示锁可以被获取,获取后将锁计数器设为 1 也就是加 1。 + +[![ 执行 monitorenter 获取锁](https://camo.githubusercontent.com/9b5986778b36cc58ea99abe6df0a892dc46acae65bbb73fba6b6dcfc4834da6b/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f73796e6368726f6e697a65642d6765742d6c6f636b2d636f64652d626c6f636b2e706e67)](https://camo.githubusercontent.com/9b5986778b36cc58ea99abe6df0a892dc46acae65bbb73fba6b6dcfc4834da6b/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f73796e6368726f6e697a65642d6765742d6c6f636b2d636f64652d626c6f636b2e706e67) + +对象锁的的拥有者线程才可以执行 `monitorexit` 指令来释放锁。在执行 `monitorexit` 指令后,将锁计数器设为 0,表明锁被释放,其他线程可以尝试获取锁。 + +[![ 执行 monitorexit 释放锁](https://camo.githubusercontent.com/ff0fb002626c445b1adc69507f430bc0ffd1202c9e0decfc58749f71c8183587/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f73796e6368726f6e697a65642d72656c656173652d6c6f636b2d626c6f636b2e706e67)](https://camo.githubusercontent.com/ff0fb002626c445b1adc69507f430bc0ffd1202c9e0decfc58749f71c8183587/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f73796e6368726f6e697a65642d72656c656173652d6c6f636b2d626c6f636b2e706e67) + +如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。 + +#### synchronized 修饰方法的的情况 + +``` +public class SynchronizedDemo2 { + public synchronized void method() { + System.out.println("synchronized 方法"); + } +} +``` + +[![synchronized 关键字原理](https://camo.githubusercontent.com/0ac6ee1ed5d3ca201bd9243767f5a3d239419b6381c9053c7ccfba00890bd4b7/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f73796e6368726f6e697a6564254535253835254233254539253934254145254535254144253937254535253845253946254537253930253836322e706e67)](https://camo.githubusercontent.com/0ac6ee1ed5d3ca201bd9243767f5a3d239419b6381c9053c7ccfba00890bd4b7/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f73796e6368726f6e697a6564254535253835254233254539253934254145254535254144253937254535253845253946254537253930253836322e706e67) + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。JVM 通过该 `ACC_SYNCHRONIZED` 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。 + +如果是实例方法,JVM 会尝试获取实例对象的锁。如果是静态方法,JVM 会尝试获取当前 class 的锁。 + +#### 总结 + +`synchronized` 同步语句块的实现使用的是 `monitorenter` 和 `monitorexit` 指令,其中 `monitorenter` 指令指向同步代码块的开始位置,`monitorexit` 指令则指明同步代码块的结束位置。 + +`synchronized` 修饰的方法并没有 `monitorenter` 指令和 `monitorexit` 指令,取得代之的确实是 `ACC_SYNCHRONIZED` 标识,该标识指明了该方法是一个同步方法。 + +**不过两者的本质都是对对象监视器 monitor 的获取。** + +相关推荐:[Java 锁与线程的那些事 - 有赞技术团队](https://tech.youzan.com/javasuo-yu-xian-cheng-de-na-xie-shi/) 。 + +🧗🏻 进阶一下:学有余力的小伙伴可以抽时间详细研究一下对象监视器 `monitor`。 + +### JDK1.6 之后的 synchronized 底层做了哪些优化?锁升级原理了解吗? + +在 Java 6 之后, `synchronized` 引入了大量的优化如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销,这些优化让 `synchronized` 锁的效率提升了很多(JDK18 中,偏向锁已经被彻底废弃,前面已经提到过了)。 + +锁主要存在四种状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。 + +`synchronized` 锁升级是一个比较复杂的过程,面试也很少问到,如果你想要详细了解的话,可以看看这篇文章:[浅析 synchronized 锁升级的原理与实现](https://www.cnblogs.com/star95/p/17542850.html)。 + +### synchronized 和 volatile 有什么区别? + +`synchronized` 关键字和 `volatile` 关键字是两个互补的存在,而不是对立的存在! + +- `volatile` 关键字是线程同步的轻量级实现,所以 `volatile` 性能肯定比 `synchronized` 关键字要好 。但是 `volatile` 关键字只能用于变量而 `synchronized` 关键字可以修饰方法以及代码块 。 +- `volatile` 关键字能保证数据的可见性,但不能保证数据的原子性。`synchronized` 关键字两者都能保证。 +- `volatile` 关键字主要用于解决变量在多个线程之间的可见性,而 `synchronized` 关键字解决的是多个线程之间访问资源的同步性。 + +## CAS + +> 什么是 CAS? +> +> CAS 有什么作用? +> +> CAS 的原理是什么? +> +> CAS 的三大问题? + +**作用** + +**CAS(Compare and Swap)**,字面意思为**比较并交换**。CAS 有 3 个操作数,分别是:内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。 + +**原理** + +Java 主要利用 `Unsafe` 这个类提供的 CAS 操作。`Unsafe` 的 CAS 依赖的是 JV M 针对不同的操作系统实现的 `Atomic::cmpxchg` 指令。 + +**三大问题** + +1. **ABA 问题**:因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A。 +2. **循环时间长开销大**。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline), 使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。 +3. **只能保证一个共享变量的原子操作**。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i = 2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。 + +## ThreadLocal + +> `ThreadLocal` 有什么作用? +> +> `ThreadLocal` 的原理是什么? +> +> 如何解决 `ThreadLocal` 内存泄漏问题? + +**作用** + +**`ThreadLocal` 是一个存储线程本地副本的工具类**。 + +**原理** + +`Thread` 类中维护着一个 `ThreadLocal.ThreadLocalMap` 类型的成员 `threadLocals`。这个成员就是用来存储当前线程独占的变量副本。 + +`ThreadLocalMap` 是 `ThreadLocal` 的内部类,它维护着一个 `Entry` 数组, `Entry` 用于保存键值对,其 key 是 `ThreadLocal` 对象,value 是传递进来的对象(变量副本)。 `Entry` 继承了 `WeakReference` ,所以是弱引用。 + +**内存泄漏问题** + +ThreadLocalMap 的 `Entry` 继承了 `WeakReference`,所以它的 key (`ThreadLocal` 对象)是弱引用,而 value (变量副本)是强引用。 + +- 如果 `ThreadLocal` 对象没有外部强引用来引用它,那么 `ThreadLocal` 对象会在下次 GC 时被回收。 +- 此时,`Entry` 中的 key 已经被回收,但是 value 由于是强引用不会被垃圾收集器回收。如果创建 `ThreadLocal` 的线程一直持续运行,那么 value 就会一直得不到回收,产生内存泄露。 + +那么如何避免内存泄漏呢?方法就是:**使用 `ThreadLocal` 的 `set` 方法后,显示的调用 `remove` 方法** 。 + +## 内存模型 + +### 什么是 Java 内存模型 + +- Java 内存模型即 Java Memory Model,简称 JMM。JMM 定义了 JVM 在计算机内存 (RAM) 中的工作方式。JMM 是隶属于 JVM 的。 +- 并发编程领域两个关键问题:线程间通信和线程间同步 +- 线程间通信机制 + - 共享内存 - 线程间通过写-读内存中的公共状态来隐式进行通信。 + - 消息传递 - java 中典型的消息传递方式就是 wait() 和 notify()。 +- 线程间同步机制 + - 在共享内存模型中,必须显示指定某个方法或某段代码在线程间互斥地执行。 + - 在消息传递模型中,由于发送消息必须在接收消息之前,因此同步是隐式进行的。 +- Java 的并发采用的是共享内存模型 +- JMM 决定一个线程对共享变量的写入何时对另一个线程可见。 +- 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。 +- JMM 把内存分成了两部分:线程栈区和堆区 + - 线程栈 + - JVM 中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。 + - 线程栈还包含了当前方法的所有本地变量信息。线程中的本地变量对其它线程是不可见的。 + - 堆区 + - 堆区包含了 Java 应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如 Byte、Integer、Long 等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。 + - 一个本地变量如果是原始类型,那么它会被完全存储到栈区。 + - 一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。 + - 对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。 + - 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-memory-model_3.png) + +> 👉 扩展阅读:[全面理解 Java 内存模型](https://blog.csdn.net/suifeng3051/article/details/52611310) + +## 同步容器和并发容器 + +> 👉 扩展阅读:[Java 并发容器](https://dunwu.github.io/waterdrop/pages/6fd8d836/) + +### ⭐ 同步容器 + +> 什么是同步容器? +> +> 有哪些常见同步容器? +> +> 它们是如何实现线程安全的? +> +> 同步容器真的线程安全吗? + +**类型** + +`Vector`、`Stack`、`Hashtable` + +**作用/原理** + +同步容器的同步原理就是在方法上用 `synchronized` 修饰。 **`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 + +`synchronized` 的互斥同步会产生阻塞和唤醒线程的开销。显然,这种方式比没有使用 `synchronized` 的容器性能要差。 + +**线程安全** + +同步容器真的绝对安全吗? + +其实也未必。在做复合操作(非原子操作)时,仍然需要加锁来保护。常见复合操作如下: + +- **迭代**:反复访问元素,直到遍历完全部元素; +- **跳转**:根据指定顺序寻找当前元素的下一个(下 n 个)元素; +- **条件运算**:例如若没有则添加等; + +### ⭐⭐⭐ ConcurrentHashMap + +> 请描述 ConcurrentHashMap 的实现原理? +> +> ConcurrentHashMap 为什么放弃了分段锁? + +基础数据结构原理和 `HashMap` 一样,JDK 1.7 采用 数组+单链表;JDK 1.8 采用数组+单链表+红黑树。 + +并发安全特性的实现: + +JDK 1.7: + +- 使用分段锁,设计思路是缩小锁粒度,提高并发吞吐。也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。 +- 写数据时,会使用可重入锁去锁住分段(segment):HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。 + +JDK 1.8: + +- 取消分段锁,直接采用 `transient volatile HashEntry[] table` 保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。 +- 写数据时,使用是 CAS + `synchronized`。 + - 根据 key 计算出 hashcode 。 + - 判断是否需要进行初始化。 + - `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 + - 如果当前位置的 `hashcode == MOVED == -1`, 则需要进行扩容。 + - 如果都不满足,则利用 synchronized 锁写入数据。 + - 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。 diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\270\211.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\270\211.md" new file mode 100644 index 0000000000..3718f5d330 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\270\211.md" @@ -0,0 +1,1414 @@ +--- +title: Java 并发面试三 +date: 2024-07-23 07:21:03 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 + - 并发 +permalink: /pages/8ede3b07/ +--- + +# Java 并发面试三 + +## ThreadLocal + +### ThreadLocal 有什么用? + +通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。**如果想实现每一个线程都有自己的专属本地变量该如何解决呢?** + +JDK 中自带的`ThreadLocal`类正是为了解决这样的问题。 **`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。** + +如果你创建了一个`ThreadLocal`变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是`ThreadLocal`变量名的由来。他们可以使用 `get()` 和 `set()` 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题。 + +再举个简单的例子:两个人去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配一个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。 + +### 如何使用 ThreadLocal? + +相信看了上面的解释,大家已经搞懂 `ThreadLocal` 类是个什么东西了。下面简单演示一下如何在项目中实际使用 `ThreadLocal` 。 + +``` +import java.text.SimpleDateFormat; +import java.util.Random; + +public class ThreadLocalExample implements Runnable{ + + // SimpleDateFormat 不是线程安全的,所以每个线程都要有自己独立的副本 + private static final ThreadLocal formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm")); + + public static void main(String[] args) throws InterruptedException { + ThreadLocalExample obj = new ThreadLocalExample(); + for(int i=0 ; i<10; i++){ + Thread t = new Thread(obj, ""+i); + Thread.sleep(new Random().nextInt(1000)); + t.start(); + } + } + + @Override + public void run() { + System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern()); + try { + Thread.sleep(new Random().nextInt(1000)); + } catch (InterruptedException e) { + e.printStackTrace(); + } + //formatter pattern is changed here by thread, but it won't reflect to other threads + formatter.set(new SimpleDateFormat()); + + System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern()); + } + +} +``` + +输出结果 : + +``` +Thread Name= 0 default Formatter = yyyyMMdd HHmm +Thread Name= 0 formatter = yy-M-d ah:mm +Thread Name= 1 default Formatter = yyyyMMdd HHmm +Thread Name= 2 default Formatter = yyyyMMdd HHmm +Thread Name= 1 formatter = yy-M-d ah:mm +Thread Name= 3 default Formatter = yyyyMMdd HHmm +Thread Name= 2 formatter = yy-M-d ah:mm +Thread Name= 4 default Formatter = yyyyMMdd HHmm +Thread Name= 3 formatter = yy-M-d ah:mm +Thread Name= 4 formatter = yy-M-d ah:mm +Thread Name= 5 default Formatter = yyyyMMdd HHmm +Thread Name= 5 formatter = yy-M-d ah:mm +Thread Name= 6 default Formatter = yyyyMMdd HHmm +Thread Name= 6 formatter = yy-M-d ah:mm +Thread Name= 7 default Formatter = yyyyMMdd HHmm +Thread Name= 7 formatter = yy-M-d ah:mm +Thread Name= 8 default Formatter = yyyyMMdd HHmm +Thread Name= 9 default Formatter = yyyyMMdd HHmm +Thread Name= 8 formatter = yy-M-d ah:mm +Thread Name= 9 formatter = yy-M-d ah:mm +``` + +从输出中可以看出,虽然 `Thread-0` 已经改变了 `formatter` 的值,但 `Thread-1` 默认格式化值与初始化值相同,其他线程也一样。 + +上面有一段代码用到了创建 `ThreadLocal` 变量的那段代码用到了 Java8 的知识,它等于下面这段代码,如果你写了下面这段代码的话,IDEA 会提示你转换为 Java8 的格式 (IDEA 真的不错!)。因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法`withInitial()`,将 Supplier 功能接口作为参数。 + +``` +private static final ThreadLocal formatter = new ThreadLocal(){ + @Override + protected SimpleDateFormat initialValue(){ + return new SimpleDateFormat("yyyyMMdd HHmm"); + } +}; +``` + +### ThreadLocal 原理了解吗? + +从 `Thread`类源代码入手。 + +``` +public class Thread implements Runnable { + //...... + //与此线程有关的 ThreadLocal 值。由 ThreadLocal 类维护 + ThreadLocal.ThreadLocalMap threadLocals = null; + + //与此线程有关的 InheritableThreadLocal 值。由 InheritableThreadLocal 类维护 + ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; + //...... +} +``` + +从上面`Thread`类 源代码可以看出`Thread` 类中有一个 `threadLocals` 和 一个 `inheritableThreadLocals` 变量,它们都是 `ThreadLocalMap` 类型的变量,我们可以把 `ThreadLocalMap` 理解为`ThreadLocal` 类实现的定制化的 `HashMap`。默认情况下这两个变量都是 null,只有当前线程调用 `ThreadLocal` 类的 `set`或`get`方法时才创建它们,实际上调用这两个方法的时候,我们调用的是`ThreadLocalMap`类对应的 `get()`、`set()`方法。 + +`ThreadLocal`类的`set()`方法 + +``` +public void set(T value) { + //获取当前请求的线程 + Thread t = Thread.currentThread(); + //取出 Thread 类内部的 threadLocals 变量(哈希表结构) + ThreadLocalMap map = getMap(t); + if (map != null) + // 将需要存储的值放入到这个哈希表中 + map.set(this, value); + else + createMap(t, value); +} +ThreadLocalMap getMap(Thread t) { + return t.threadLocals; +} +``` + +通过上面这些内容,我们足以通过猜测得出结论:**最终的变量是放在了当前线程的 `ThreadLocalMap` 中,并不是存在 `ThreadLocal` 上,`ThreadLocal` 可以理解为只是`ThreadLocalMap`的封装,传递了变量值。** `ThrealLocal` 类中可以通过`Thread.currentThread()`获取到当前线程对象后,直接通过`getMap(Thread t)`可以访问到该线程的`ThreadLocalMap`对象。 + +**每个`Thread`中都具备一个`ThreadLocalMap`,而`ThreadLocalMap`可以存储以`ThreadLocal`为 key ,Object 对象为 value 的键值对。** + +``` +ThreadLocalMap(ThreadLocal firstKey, Object firstValue) { + //...... +} +``` + +比如我们在同一个线程中声明了两个 `ThreadLocal` 对象的话, `Thread`内部都是使用仅有的那个`ThreadLocalMap` 存放数据的,`ThreadLocalMap`的 key 就是 `ThreadLocal`对象,value 就是 `ThreadLocal` 对象调用`set`方法设置的值。 + +`ThreadLocal` 数据结构如下图所示: + +[![ThreadLocal 数据结构](https://camo.githubusercontent.com/1819d183385b93268378cf890d4b70cbc31c12bbe8e153deb4f3f8337bc0ebd7/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7468726561646c6f63616c2d646174612d7374727563747572652e706e67)](https://camo.githubusercontent.com/1819d183385b93268378cf890d4b70cbc31c12bbe8e153deb4f3f8337bc0ebd7/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7468726561646c6f63616c2d646174612d7374727563747572652e706e67) + +`ThreadLocalMap`是`ThreadLocal`的静态内部类。 + +[![ThreadLocal 内部类](https://camo.githubusercontent.com/c8e18827ce59c5dfed521f3c5a1d7fcacf559da6b472b4f782013b362c513795/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7468726561642d6c6f63616c2d696e6e65722d636c6173732e706e67)](https://camo.githubusercontent.com/c8e18827ce59c5dfed521f3c5a1d7fcacf559da6b472b4f782013b362c513795/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7468726561642d6c6f63616c2d696e6e65722d636c6173732e706e67) + +### ThreadLocal 内存泄露问题是怎么导致的? + +`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。 + +这样一来,`ThreadLocalMap` 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。`ThreadLocalMap` 实现中已经考虑了这种情况,在调用 `set()`、`get()`、`remove()` 方法的时候,会清理掉 key 为 null 的记录。使用完 `ThreadLocal`方法后最好手动调用`remove()`方法 + +``` +static class Entry extends WeakReference> { + /** The value associated with this ThreadLocal. */ + Object value; + + Entry(ThreadLocal k, Object v) { + super(k); + value = v; + } +} +``` + +**弱引用介绍:** + +> 如果一个对象只具有弱引用,那就类似于**可有可无的生活用品**。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。 +> +> 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java 虚拟机就会把这个弱引用加入到与之关联的引用队列中。 + +## 线程池 + +### 什么是线程池? + +顾名思义,线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。 + +### 为什么要用线程池? + +池化技术想必大家已经屡见不鲜了,线程池、数据库连接池、HTTP 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。 + +**线程池**提供了一种限制和管理资源(包括执行一个任务)的方式。 每个**线程池**还维护一些基本统计信息,例如已完成任务的数量。 + +这里借用《Java 并发编程的艺术》提到的来说一下**使用线程池的好处**: + +- **降低资源消耗**。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 +- **提高响应速度**。当任务到达时,任务可以不需要等到线程创建就能立即执行。 +- **提高线程的可管理性**。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。 + +### 如何创建线程池? + +**方式一:通过`ThreadPoolExecutor`构造函数来创建(推荐)。** + +[![通过构造方法实现](https://github.com/Snailclimb/JavaGuide/raw/main/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0.png)](https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/concurrent/images/java-thread-pool-summary/threadpoolexecutor 构造函数。png) + +**方式二:通过 `Executor` 框架的工具类 `Executors` 来创建。** + +`Executors`工具类提供的创建线程池的方法如下图所示: + +[![img](https://camo.githubusercontent.com/bdfb503aac4225d5cf902e997970b57cdacf3fbce9a2b0d3d111870046fff376/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f6578656375746f72732d6e65772d7468726561642d706f6f6c2d6d6574686f64732e706e67)](https://camo.githubusercontent.com/bdfb503aac4225d5cf902e997970b57cdacf3fbce9a2b0d3d111870046fff376/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f6578656375746f72732d6e65772d7468726561642d706f6f6c2d6d6574686f64732e706e67) + +可以看出,通过`Executors`工具类可以创建多种类型的线程池,包括: + +- `FixedThreadPool`:固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。 +- `SingleThreadExecutor`: 只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。 +- `CachedThreadPool`: 可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。 +- `ScheduledThreadPool`:给定的延迟后运行任务或者定期执行任务的线程池。 + +### 为什么不推荐使用内置线程池? + +在《阿里巴巴 Java 开发手册》“并发处理”这一章节,明确指出线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。 + +**为什么呢?** + +> 使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。 + +另外,《阿里巴巴 Java 开发手册》中强制线程池不允许使用 `Executors` 去创建,而是通过 `ThreadPoolExecutor` 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险 + +`Executors` 返回线程池对象的弊端如下: + +- `FixedThreadPool` 和 `SingleThreadExecutor`: 使用的是有界阻塞队列是 `LinkedBlockingQueue` ,其任务队列的最大长度为 `Integer.MAX_VALUE` ,可能堆积大量的请求,从而导致 OOM。 +- `CachedThreadPool`: 使用的是同步队列 `SynchronousQueue`, 允许创建的线程数量为 `Integer.MAX_VALUE` ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。 +- `ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` : 使用的无界的延迟阻塞队列 `DelayedWorkQueue` ,任务队列最大长度为 `Integer.MAX_VALUE` ,可能堆积大量的请求,从而导致 OOM。 + +``` +// 有界队列 LinkedBlockingQueue +public static ExecutorService newFixedThreadPool(int nThreads) { + + return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue()); + +} + +// 无界队列 LinkedBlockingQueue +public static ExecutorService newSingleThreadExecutor() { + + return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue())); + +} + +// 同步队列 SynchronousQueue,没有容量,最大线程数是 Integer.MAX_VALUE` +public static ExecutorService newCachedThreadPool() { + + return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue()); + +} + +// DelayedWorkQueue(延迟阻塞队列) +public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) { + return new ScheduledThreadPoolExecutor(corePoolSize); +} +public ScheduledThreadPoolExecutor(int corePoolSize) { + super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, + new DelayedWorkQueue()); +} +``` + +### 线程池常见参数有哪些?如何解释? + +``` + /** + * 用给定的初始参数创建一个新的 ThreadPoolExecutor。 + */ + public ThreadPoolExecutor(int corePoolSize,//线程池的核心线程数量 + int maximumPoolSize,//线程池的最大线程数 + long keepAliveTime,//当线程数大于核心线程数时,多余的空闲线程存活的最长时间 + TimeUnit unit,//时间单位 + BlockingQueue workQueue,//任务队列,用来储存等待执行任务的队列 + ThreadFactory threadFactory,//线程工厂,用来创建线程,一般默认即可 + RejectedExecutionHandler handler//拒绝策略,当提交的任务过多而不能及时处理时,我们可以定制策略来处理任务 + ) { + if (corePoolSize < 0 || + maximumPoolSize <= 0 || + maximumPoolSize < corePoolSize || + keepAliveTime < 0) + throw new IllegalArgumentException(); + if (workQueue == null || threadFactory == null || handler == null) + throw new NullPointerException(); + this.corePoolSize = corePoolSize; + this.maximumPoolSize = maximumPoolSize; + this.workQueue = workQueue; + this.keepAliveTime = unit.toNanos(keepAliveTime); + this.threadFactory = threadFactory; + this.handler = handler; + } +``` + +`ThreadPoolExecutor` 3 个最重要的参数: + +- `corePoolSize` : 任务队列未达到队列容量时,最大可以同时运行的线程数量。 +- `maximumPoolSize` : 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- `workQueue`: 新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +`ThreadPoolExecutor`其他常见参数 : + +- `keepAliveTime`: 线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`才会被回收销毁。 +- `unit` : `keepAliveTime` 参数的时间单位。 +- `threadFactory` :executor 创建新线程的时候会用到。 +- `handler` : 拒绝策略(后面会单独详细介绍一下)。 + +下面这张图可以加深你对线程池中各个参数的相互关系的理解(图片来源:《Java 性能调优实战》): + +[![线程池各个参数的关系](https://camo.githubusercontent.com/5defc281ae4c3f7786c0dd58b398ddd7a1193f436ce3e3b6502a50f0b2df9b7b/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f72656c6174696f6e736869702d6265747765656e2d7468726561642d706f6f6c2d706172616d65746572732e706e67)](https://camo.githubusercontent.com/5defc281ae4c3f7786c0dd58b398ddd7a1193f436ce3e3b6502a50f0b2df9b7b/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f72656c6174696f6e736869702d6265747765656e2d7468726561642d706f6f6c2d706172616d65746572732e706e67) + +### 线程池的核心线程会被回收吗? + +`ThreadPoolExecutor` 默认不会回收核心线程,即使它们已经空闲了。这是为了减少创建线程的开销,因为核心线程通常是要长期保持活跃的。但是,如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 `allowCoreThreadTimeOut(boolean value)` 方法的参数设置为 `true`,这样就会回收空闲(时间间隔由 `keepAliveTime` 指定)的核心线程了。 + +``` + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(4, 6, 6, TimeUnit.SECONDS, new SynchronousQueue<>()); + threadPoolExecutor.allowCoreThreadTimeOut(true); +``` + +### 线程池的拒绝策略有哪些? + +如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,`ThreadPoolExecutor` 定义一些策略: + +- `ThreadPoolExecutor.AbortPolicy`:抛出 `RejectedExecutionException`来拒绝新任务的处理。 +- `ThreadPoolExecutor.CallerRunsPolicy`:调用执行自己的线程运行任务,也就是直接在调用`execute`方法的线程中运行 (`run`) 被拒绝的任务,如果执行程序已关闭,则会丢弃该任务。因此这种策略会降低对于新任务提交速度,影响程序的整体性能。如果你的应用程序可以承受此延迟并且你要求任何一个任务请求都要被执行的话,你可以选择这个策略。 +- `ThreadPoolExecutor.DiscardPolicy`:不处理新任务,直接丢弃掉。 +- `ThreadPoolExecutor.DiscardOldestPolicy`:此策略将丢弃最早的未处理的任务请求。 + +举个例子:Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 拒绝策略来配置线程池的时候,默认使用的是 `AbortPolicy`。在这种拒绝策略下,如果队列满了,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 异常来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。如果不想丢弃任务的话,可以使用`CallerRunsPolicy`。`CallerRunsPolicy` 和其他的几个策略不同,它既不会抛弃任务,也不会抛出异常,而是将任务回退给调用者,使用调用者的线程来执行任务。 + +``` +public static class CallerRunsPolicy implements RejectedExecutionHandler { + + public CallerRunsPolicy() { } + + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + if (!e.isShutdown()) { + // 直接主线程执行,而不是线程池中的线程执行 + r.run(); + } + } + } +``` + +### 如果不允许丢弃任务任务,应该选择哪个拒绝策略? + +根据上面对线程池拒绝策略的介绍,相信大家很容易能够得出答案是:`CallerRunsPolicy` 。 + +这里我们再来结合`CallerRunsPolicy` 的源码来看看: + +``` +public static class CallerRunsPolicy implements RejectedExecutionHandler { + + public CallerRunsPolicy() { } + + public void rejectedExecution(Runnable r, ThreadPoolExecutor e) { + //只要当前程序没有关闭,就用执行 execute 方法的线程执行该任务 + if (!e.isShutdown()) { + + r.run(); + } + } + } +``` + +从源码可以看出,只要当前程序不关闭就会使用执行`execute`方法的线程执行该任务。 + +### CallerRunsPolicy 拒绝策略有什么风险?如何解决? + +我们上面也提到了:如果想要保证任何一个任务请求都要被执行的话,那选择 `CallerRunsPolicy` 拒绝策略更合适一些。 + +不过,如果走到`CallerRunsPolicy`的任务是个非常耗时的任务,且处理提交任务的线程是主线程,可能会导致主线程阻塞,影响程序的正常运行。 + +这里简单举一个例子,该线程池限定了最大线程数为 2,阻塞队列大小为 1(这意味着第 4 个任务就会走到拒绝策略),`ThreadUtil`为 Hutool 提供的工具类: + +``` +public class ThreadPoolTest { + + private static final Logger log = LoggerFactory.getLogger(ThreadPoolTest.class); + + public static void main(String[] args) { + // 创建一个线程池,核心线程数为 1,最大线程数为 2 + // 当线程数大于核心线程数时,多余的空闲线程存活的最长时间为 60 秒, + // 任务队列为容量为 1 的 ArrayBlockingQueue,饱和策略为 CallerRunsPolicy。 + ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1, + 2, + 60, + TimeUnit.SECONDS, + new ArrayBlockingQueue<>(1), + new ThreadPoolExecutor.CallerRunsPolicy()); + + // 提交第一个任务,由核心线程执行 + threadPoolExecutor.execute(() -> { + log.info("核心线程执行第一个任务"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + // 提交第二个任务,由于核心线程被占用,任务将进入队列等待 + threadPoolExecutor.execute(() -> { + log.info("非核心线程处理入队的第二个任务"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + // 提交第三个任务,由于核心线程被占用且队列已满,创建非核心线程处理 + threadPoolExecutor.execute(() -> { + log.info("非核心线程处理第三个任务"); + ThreadUtil.sleep(1, TimeUnit.MINUTES); + }); + + // 提交第四个任务,由于核心线程和非核心线程都被占用,队列也满了,根据 CallerRunsPolicy 策略,任务将由提交任务的线程(即主线程)来执行 + threadPoolExecutor.execute(() -> { + log.info("主线程处理第四个任务"); + ThreadUtil.sleep(2, TimeUnit.MINUTES); + }); + + // 提交第五个任务,主线程被第四个任务卡住,该任务必须等到主线程执行完才能提交 + threadPoolExecutor.execute(() -> { + log.info("核心线程执行第五个任务"); + }); + + // 关闭线程池 + threadPoolExecutor.shutdown(); + } +} +``` + +输出: + +``` +18:19:48.203 INFO [pool-1-thread-1] c.j.concurrent.ThreadPoolTest - 核心线程执行第一个任务 +18:19:48.203 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理第三个任务 +18:19:48.203 INFO [main] c.j.concurrent.ThreadPoolTest - 主线程处理第四个任务 +18:20:48.212 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 非核心线程处理入队的第二个任务 +18:21:48.219 INFO [pool-1-thread-2] c.j.concurrent.ThreadPoolTest - 核心线程执行第五个任务 +``` + +从输出结果可以看出,因为`CallerRunsPolicy`这个拒绝策略,导致耗时的任务用了主线程执行,导致线程池阻塞,进而导致后续任务无法及时执行,严重的情况下很可能导致 OOM。 + +我们从问题的本质入手,调用者采用`CallerRunsPolicy`是希望所有的任务都能够被执行,暂时无法处理的任务又被保存在阻塞队列`BlockingQueue`中。这样的话,在内存允许的情况下,我们可以增加阻塞队列`BlockingQueue`的大小并调整堆内存以容纳更多的任务,确保任务能够被准确执行。 + +为了充分利用 CPU,我们还可以调整线程池的`maximumPoolSize` (最大线程数)参数,这样可以提高任务处理速度,避免累计在 `BlockingQueue`的任务过多导致内存用完。 + +[![调整阻塞队列大小和最大线程数](https://camo.githubusercontent.com/2b37716a74a10dfa6c633720c4d9238b3bace9563fbc6a7c9a7f8131e6b0c080/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f746872656164706f6f6c2d72656a6563742d322d746872656164706f6f6c2d72656a6563742d30312e706e67)](https://camo.githubusercontent.com/2b37716a74a10dfa6c633720c4d9238b3bace9563fbc6a7c9a7f8131e6b0c080/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f746872656164706f6f6c2d72656a6563742d322d746872656164706f6f6c2d72656a6563742d30312e706e67) + +如果服务器资源以达到可利用的极限,这就意味我们要在设计策略上改变线程池的调度了,我们都知道,导致主线程卡死的本质就是因为我们不希望任何一个任务被丢弃。换个思路,有没有办法既能保证任务不被丢弃且在服务器有余力时及时处理呢? + +这里提供的一种**任务持久化**的思路,这里所谓的任务持久化,包括但不限于: + +1. 设计一张任务表将任务存储到 MySQL 数据库中。 +2. Redis 缓存任务。 +3. 将任务提交到消息队列中。 + +这里以方案一为例,简单介绍一下实现逻辑: + +1. 实现`RejectedExecutionHandler`接口自定义拒绝策略,自定义拒绝策略负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库(保存到 MySQL 中)。注意:线程池暂时无法处理的任务会先被放在阻塞队列中,阻塞队列满了才会触发拒绝策略。 +2. 继承`BlockingQueue`实现一个混合式阻塞队列,该队列包含 JDK 自带的`ArrayBlockingQueue`。另外,该混合式阻塞队列需要修改取任务处理的逻辑,也就是重写`take()`方法,取任务时优先从数据库中读取最早的任务,数据库中无任务时再从 `ArrayBlockingQueue`中去取任务。 + +[![将一部分任务保存到 MySQL 中](https://camo.githubusercontent.com/41dfd7d44a77e0bd8153fc4b1f4deedf77349625f6f58e401bbcaf1caf6d9f0a/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f746872656164706f6f6c2d72656a6563742d322d746872656164706f6f6c2d72656a6563742d30322e706e67)](https://camo.githubusercontent.com/41dfd7d44a77e0bd8153fc4b1f4deedf77349625f6f58e401bbcaf1caf6d9f0a/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f746872656164706f6f6c2d72656a6563742d322d746872656164706f6f6c2d72656a6563742d30322e706e67) + +整个实现逻辑还是比较简单的,核心在于自定义拒绝策略和阻塞队列。如此一来,一旦我们的线程池中线程以达到满载时,我们就可以通过拒绝策略将最新任务持久化到 MySQL 数据库中,等到线程池有了有余力处理所有任务时,让其优先处理数据库中的任务以避免"饥饿"问题。 + +当然,对于这个问题,我们也可以参考其他主流框架的做法,以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控: + +``` +private static final class NewThreadRunsPolicy implements RejectedExecutionHandler { + NewThreadRunsPolicy() { + super(); + } + public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { + try { + //创建一个临时线程处理任务 + final Thread t = new Thread(r, "Temporary task executor"); + t.start(); + } catch (Throwable e) { + throw new RejectedExecutionException( + "Failed to start a new thread", e); + } + } +} +``` + +ActiveMQ 则是尝试在指定的时效内尽可能的争取将任务入队,以保证最大交付: + +``` +new RejectedExecutionHandler() { + @Override + public void rejectedExecution(final Runnable r, final ThreadPoolExecutor executor) { + try { + //限时阻塞等待,实现尽可能交付 + executor.getQueue().offer(r, 60, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RejectedExecutionException("Interrupted waiting for BrokerService.worker"); + } + throw new RejectedExecutionException("Timed Out while attempting to enqueue Task."); + } + }); +``` + +### 线程池常用的阻塞队列有哪些? + +新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +不同的线程池会选用不同的阻塞队列,我们可以结合内置线程池来分析。 + +- 容量为 `Integer.MAX_VALUE` 的 `LinkedBlockingQueue`(有界阻塞队列):`FixedThreadPool` 和 `SingleThreadExecutor` 。`FixedThreadPool`最多只能创建核心线程数的线程(核心线程数和最大线程数相等),`SingleThreadExecutor`只能创建一个线程(核心线程数和最大线程数都是 1),二者的任务队列永远不会被放满。 +- `SynchronousQueue`(同步队列):`CachedThreadPool` 。`SynchronousQueue` 没有容量,不存储元素,目的是保证对于提交的任务,如果有空闲线程,则使用空闲线程来处理;否则新建一个线程来处理任务。也就是说,`CachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE` ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。 +- `DelayedWorkQueue`(延迟队列):`ScheduledThreadPool` 和 `SingleThreadScheduledExecutor` 。`DelayedWorkQueue` 的内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。`DelayedWorkQueue` 添加元素满了之后会自动扩容,增加原来容量的 50%,即永远不会阻塞,最大扩容可达 `Integer.MAX_VALUE`,所以最多只能创建核心线程数的线程。 +- `ArrayBlockingQueue`(有界阻塞队列):底层由数组实现,容量一旦创建,就不能修改。 + +### 线程池处理任务的流程了解吗? + +[![图解线程池实现原理](https://camo.githubusercontent.com/4cac69de4a9742c9357feacd6ad9cbe10afebd668ff9f7d2e86e13de65cac1a9/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7468726561642d706f6f6c2d7072696e6369706c652e706e67)](https://camo.githubusercontent.com/4cac69de4a9742c9357feacd6ad9cbe10afebd668ff9f7d2e86e13de65cac1a9/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7468726561642d706f6f6c2d7072696e6369706c652e706e67) + +1. 如果当前运行的线程数小于核心线程数,那么就会新建一个线程来执行任务。 +2. 如果当前运行的线程数等于或大于核心线程数,但是小于最大线程数,那么就把该任务放入到任务队列里等待执行。 +3. 如果向任务队列投放任务失败(任务队列已经满了),但是当前运行的线程数是小于最大线程数的,就新建一个线程来执行任务。 +4. 如果当前运行的线程数已经等同于最大线程数了,新建线程将会使当前运行的线程超出最大线程数,那么当前任务会被拒绝,拒绝策略会调用`RejectedExecutionHandler.rejectedExecution()`方法。 + +再提一个有意思的小问题:**线程池在提交任务前,可以提前创建线程吗?** + +答案是可以的!`ThreadPoolExecutor` 提供了两个方法帮助我们在提交任务之前,完成核心线程的创建,从而实现线程池预热的效果: + +- `prestartCoreThread()`: 启动一个线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true; +- `prestartAllCoreThreads()`: 启动所有的核心线程,并返回启动成功的核心线程数。 + +### 线程池中线程异常后,销毁还是复用? + +直接说结论,需要分两种情况: + +- **使用`execute()`提交任务**:当任务通过`execute()`提交到线程池并在执行过程中抛出异常时,如果这个异常没有在任务内被捕获,那么该异常会导致当前线程终止,并且异常会被打印到控制台或日志文件中。线程池会检测到这种线程终止,并创建一个新线程来替换它,从而保持配置的线程数不变。 +- **使用`submit()`提交任务**:对于通过`submit()`提交的任务,如果在任务执行中发生异常,这个异常不会直接打印出来。相反,异常会被封装在由`submit()`返回的`Future`对象中。当调用`Future.get()`方法时,可以捕获到一个`ExecutionException`。在这种情况下,线程不会因为异常而终止,它会继续存在于线程池中,准备执行后续的任务。 + +简单来说:使用`execute()`时,未捕获异常导致线程终止,线程池创建新线程替代;使用`submit()`时,异常被封装在`Future`中,线程继续复用。 + +这种设计允许`submit()`提供更灵活的错误处理机制,因为它允许调用者决定如何处理异常,而`execute()`则适用于那些不需要关注执行结果的场景。 + +具体的源码分析可以参考这篇:[线程池中线程异常后:销毁还是复用? - 京东技术](https://mp.weixin.qq.com/s/9ODjdUU-EwQFF5PrnzOGfw)。 + +### 如何给线程池命名? + +初始化线程池的时候需要显示命名(设置线程池名称前缀),有利于定位问题。 + +默认情况下创建的线程名字类似 `pool-1-thread-n` 这样的,没有业务含义,不利于我们定位问题。 + +给线程池里的线程命名通常有下面两种方式: + +**1、利用 guava 的 `ThreadFactoryBuilder`** + +``` +ThreadFactory threadFactory = new ThreadFactoryBuilder() + .setNameFormat(threadNamePrefix + "-%d") + .setDaemon(true).build(); +ExecutorService threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MINUTES, workQueue, threadFactory); +``` + +**2、自己实现 `ThreadFactory`。** + +``` +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.atomic.AtomicInteger; + +/** + * 线程工厂,它设置线程名称,有利于我们定位问题。 + */ +public final class NamingThreadFactory implements ThreadFactory { + + private final AtomicInteger threadNum = new AtomicInteger(); + private final String name; + + /** + * 创建一个带名字的线程池生产工厂 + */ + public NamingThreadFactory(String name) { + this.name = name; + } + + @Override + public Thread newThread(Runnable r) { + Thread t = new Thread(r); + t.setName(name + " [#" + threadNum.incrementAndGet() + "]"); + return t; + } +} +``` + +### 如何设定线程池的大小? + +很多人甚至可能都会觉得把线程池配置过大一点比较好!我觉得这明显是有问题的。就拿我们生活中非常常见的一例子来说:**并不是人多就能把事情做好,增加了沟通交流成本。你本来一件事情只需要 3 个人做,你硬是拉来了 6 个人,会提升做事效率嘛?我想并不会。** 线程数量过多的影响也是和我们分配多少人做事情一样,对于多线程这个场景来说主要是增加了**上下文切换**成本。不清楚什么是上下文切换的话,可以看我下面的介绍。 + +> 上下文切换: +> +> 多线程编程中一般线程的个数都大于 CPU 核心的个数,而一个 CPU 核心在任意时刻只能被一个线程使用,为了让这些线程都能得到有效执行,CPU 采取的策略是为每个线程分配时间片并轮转的形式。当一个线程的时间片用完的时候就会重新处于就绪状态让给其他线程使用,这个过程就属于一次上下文切换。概括来说就是:当前任务在执行完 CPU 时间片切换到另一个任务之前会先保存自己的状态,以便下次再切换回这个任务时,可以再加载这个任务的状态。**任务从保存到再加载的过程就是一次上下文切换**。 +> +> 上下文切换通常是计算密集型的。也就是说,它需要相当可观的处理器时间,在每秒几十上百次的切换中,每次切换都需要纳秒量级的时间。所以,上下文切换对系统来说意味着消耗大量的 CPU 时间,事实上,可能是操作系统中时间消耗最大的操作。 +> +> Linux 相比与其他操作系统(包括其他类 Unix 系统)有很多的优点,其中有一项就是,其上下文切换和模式切换的时间消耗非常少。 + +类比于实现世界中的人类通过合作做某件事情,我们可以肯定的一点是线程池大小设置过大或者过小都会有问题,合适的才是最好。 + +- 如果我们设置的线程池数量太小的话,如果同一时间有大量任务/请求需要处理,可能会导致大量的请求/任务在任务队列中排队等待执行,甚至会出现任务队列满了之后任务/请求无法处理的情况,或者大量任务堆积在任务队列导致 OOM。这样很明显是有问题的,CPU 根本没有得到充分利用。 +- 如果我们设置线程数量太大,大量线程可能会同时在争取 CPU 资源,这样会导致大量的上下文切换,从而增加线程的执行时间,影响了整体执行效率。 + +有一个简单并且适用面比较广的公式: + +- **CPU 密集型任务 (N+1):** 这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1。比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 +- **I/O 密集型任务 (2N):** 这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 + +**如何判断是 CPU 密集任务还是 IO 密集任务?** + +CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。 + +> 🌈 拓展一下(参见:[issue#1737](https://github.com/Snailclimb/JavaGuide/issues/1737)): +> +> 线程数更严谨的计算的方法应该是:`最佳线程数 = N(CPU 核心数)∗(1+WT(线程等待时间)/ST(线程计算时间))`,其中 `WT(线程等待时间)=线程运行总时间 - ST(线程计算时间)`。 +> +> 线程等待时间所占比例越高,需要越多线程。线程计算时间所占比例越高,需要越少线程。 +> +> 我们可以通过 JDK 自带的工具 VisualVM 来查看 `WT/ST` 比例。 +> +> CPU 密集型任务的 `WT/ST` 接近或者等于 0,因此, 线程数可以设置为 N(CPU 核心数)∗(1+0)= N,和我们上面说的 N(CPU 核心数)+1 差不多。 +> +> IO 密集型任务下,几乎全是线程等待时间,从理论上来说,你就可以将线程数设置为 2N(按道理来说,WT/ST 的结果应该比较大,这里选择 2N 的原因应该是为了避免创建过多线程吧)。 + +公式也只是参考,具体还是要根据项目实际线上运行情况来动态调整。我在后面介绍的美团的线程池参数动态配置这种方案就非常不错,很实用! + +### 如何动态修改线程池的参数? + +美团技术团队在 [《Java 线程池实现原理及其在美团业务中的实践》](https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html) 这篇文章中介绍到对线程池参数实现可自定义配置的思路和方法。 + +美团技术团队的思路是主要对线程池的核心参数实现自定义可配置。这三个核心参数是: + +- **`corePoolSize` :** 核心线程数线程数定义了最小可以同时运行的线程数量。 +- **`maximumPoolSize` :** 当队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。 +- **`workQueue`:** 当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。 + +**为什么是这三个参数?** + +我在 [Java 线程池详解](https://javaguide.cn/java/concurrent/java-thread-pool-summary.html) 这篇文章中就说过这三个参数是 `ThreadPoolExecutor` 最重要的参数,它们基本决定了线程池对于任务的处理策略。 + +**如何支持参数动态配置?** 且看 `ThreadPoolExecutor` 提供的下面这些方法。 + +[![img](https://camo.githubusercontent.com/f80ed7e3dd54a0c6ffaaedeaa37d2232e08f978ead597081df116ed3d6fb7f88/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f746872656164706f6f6c6578656375746f722d6d6574686f64732e706e67)](https://camo.githubusercontent.com/f80ed7e3dd54a0c6ffaaedeaa37d2232e08f978ead597081df116ed3d6fb7f88/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f746872656164706f6f6c6578656375746f722d6d6574686f64732e706e67) + +格外需要注意的是`corePoolSize`, 程序运行期间的时候,我们调用 `setCorePoolSize()`这个方法的话,线程池会首先判断当前工作线程数是否大于`corePoolSize`,如果大于的话就会回收工作线程。 + +另外,你也看到了上面并没有动态指定队列长度的方法,美团的方式是自定义了一个叫做 `ResizableCapacityLinkedBlockIngQueue` 的队列(主要就是把`LinkedBlockingQueue`的 capacity 字段的 final 关键字修饰给去掉了,让它变为可变的)。 + +最终实现的可动态修改线程池参数效果如下。👏👏👏 + +[![动态配置线程池参数最终效果](https://camo.githubusercontent.com/11c8c4dbdde74f94079c90663efa9758932f9b12834b06ef1c7c6fb1dcf23c12/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f6d65697475616e2d64796e616d6963616c6c792d636f6e6669677572696e672d7468726561642d706f6f6c2d706172616d65746572732e706e67)](https://camo.githubusercontent.com/11c8c4dbdde74f94079c90663efa9758932f9b12834b06ef1c7c6fb1dcf23c12/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f6d65697475616e2d64796e616d6963616c6c792d636f6e6669677572696e672d7468726561642d706f6f6c2d706172616d65746572732e706e67) + +还没看够?推荐 why 神的 [如何设置线程池参数?美团给出了一个让面试官虎躯一震的回答。](https://mp.weixin.qq.com/s/9HLuPcoWmTqAeFKa1kj-_A) 这篇文章,深度剖析,很不错哦! + +如果我们的项目也想要实现这种效果的话,可以借助现成的开源项目: + +- **[Hippo4j](https://github.com/opengoofy/hippo4j)**:异步线程池框架,支持线程池动态变更&监控&报警,无需修改代码轻松引入。支持多种使用模式,轻松引入,致力于提高系统运行保障能力。 +- **[Dynamic TP](https://github.com/dromara/dynamic-tp)**:轻量级动态线程池,内置监控告警功能,集成三方中间件线程池管理,基于主流配置中心(已支持 Nacos、Apollo,Zookeeper、Consul、Etcd,可通过 SPI 自定义实现)。 + +### 如何设计一个能够根据任务的优先级来执行的线程池? + +这是一个常见的面试问题,本质其实还是在考察求职者对于线程池以及阻塞队列的掌握。 + +我们上面也提到了,不同的线程池会选用不同的阻塞队列作为任务队列,比如`FixedThreadPool` 使用的是`LinkedBlockingQueue`(有界队列),默认构造器初始的队列长度为 `Integer.MAX_VALUE` ,由于队列永远不会被放满,因此`FixedThreadPool`最多只能创建核心线程数的线程。 + +假如我们需要实现一个优先级任务线程池的话,那可以考虑使用 `PriorityBlockingQueue` (优先级阻塞队列)作为任务队列(`ThreadPoolExecutor` 的构造函数有一个 `workQueue` 参数可以传入任务队列)。 + +[![ThreadPoolExecutor 构造函数](https://camo.githubusercontent.com/1ceea00af5184731f66b95e568be54e6b95d1759d43183ba68562b2dbd371ed1/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f636f6d6d6f6e2d706172616d65746572732d6f662d746872656164706f6f6c2d776f726b71756575652e6a7067)](https://camo.githubusercontent.com/1ceea00af5184731f66b95e568be54e6b95d1759d43183ba68562b2dbd371ed1/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f636f6d6d6f6e2d706172616d65746572732d6f662d746872656164706f6f6c2d776f726b71756575652e6a7067) + +`PriorityBlockingQueue` 是一个支持优先级的无界阻塞队列,可以看作是线程安全的 `PriorityQueue`,两者底层都是使用小顶堆形式的二叉堆,即值最小的元素优先出队。不过,`PriorityQueue` 不支持阻塞操作。 + +要想让 `PriorityBlockingQueue` 实现对任务的排序,传入其中的任务必须是具备排序能力的,方式有两种: + +1. 提交到线程池的任务实现 `Comparable` 接口,并重写 `compareTo` 方法来指定任务之间的优先级比较规则。 +2. 创建 `PriorityBlockingQueue` 时传入一个 `Comparator` 对象来指定任务之间的排序规则(推荐)。 + +不过,这存在一些风险和问题,比如: + +- `PriorityBlockingQueue` 是无界的,可能堆积大量的请求,从而导致 OOM。 +- 可能会导致饥饿问题,即低优先级的任务长时间得不到执行。 +- 由于需要对队列中的元素进行排序操作以及保证线程安全(并发控制采用的是可重入锁 `ReentrantLock`),因此会降低性能。 + +对于 OOM 这个问题的解决比较简单粗暴,就是继承`PriorityBlockingQueue` 并重写一下 `offer` 方法(入队)的逻辑,当插入的元素数量超过指定值就返回 false 。 + +饥饿问题这个可以通过优化设计来解决(比较麻烦),比如等待时间过长的任务会被移除并重新添加到队列中,但是优先级会被提升。 + +对于性能方面的影响,是没办法避免的,毕竟需要对任务进行排序操作。并且,对于大部分业务场景来说,这点性能影响是可以接受的。 + +## Future + +### Future 类有什么用? + +`Future` 类是异步思想的典型运用,主要用在一些需要执行耗时任务的场景,避免程序一直原地等待耗时任务执行完成,执行效率太低。具体来说是这样的:当我们执行某一耗时的任务时,可以将这个耗时任务交给一个子线程去异步执行,同时我们可以干点其他事情,不用傻傻等待耗时任务执行完成。等我们的事情干完后,我们再通过 `Future` 类获取到耗时任务的执行结果。这样一来,程序的执行效率就明显提高了。 + +这其实就是多线程中经典的 **Future 模式**,你可以将其看作是一种设计模式,核心思想是异步调用,主要用在多线程领域,并非 Java 语言独有。 + +在 Java 中,`Future` 类只是一个泛型接口,位于 `java.util.concurrent` 包下,其中定义了 5 个方法,主要包括下面这 4 个功能: + +- 取消任务; +- 判断任务是否被取消; +- 判断任务是否已经执行完成; +- 获取任务执行结果。 + +``` +// V 代表了 Future 执行的任务返回值的类型 +public interface Future { + // 取消任务执行 + // 成功取消返回 true,否则返回 false + boolean cancel(boolean mayInterruptIfRunning); + // 判断任务是否被取消 + boolean isCancelled(); + // 判断任务是否已经执行完成 + boolean isDone(); + // 获取任务执行结果 + V get() throws InterruptedException, ExecutionException; + // 指定时间内没有返回计算结果就抛出 TimeOutException 异常 + V get(long timeout, TimeUnit unit) + + throws InterruptedException, ExecutionException, TimeoutExceptio + +} +``` + +简单理解就是:我有一个任务,提交给了 `Future` 来处理。任务执行期间我自己可以去做任何想做的事情。并且,在这期间我还可以取消任务以及获取任务的执行状态。一段时间之后,我就可以 `Future` 那里直接取出任务执行结果。 + +### Callable 和 Future 有什么关系? + +我们可以通过 `FutureTask` 来理解 `Callable` 和 `Future` 之间的关系。 + +`FutureTask` 提供了 `Future` 接口的基本实现,常用来封装 `Callable` 和 `Runnable`,具有取消任务、查看任务是否执行完成以及获取任务执行结果的方法。`ExecutorService.submit()` 方法返回的其实就是 `Future` 的实现类 `FutureTask` 。 + +``` + Future submit(Callable task); +Future submit(Runnable task); +``` + +`FutureTask` 不光实现了 `Future`接口,还实现了`Runnable` 接口,因此可以作为任务直接被线程执行。 + +[![img](https://camo.githubusercontent.com/11ad130bc21f74bdc678cfb34a8626983cfc186a1a0467137a6f3406dcde715e/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f636f6d706c657461626c656675747572652d636c6173732d6469616772616d2e6a7067)](https://camo.githubusercontent.com/11ad130bc21f74bdc678cfb34a8626983cfc186a1a0467137a6f3406dcde715e/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f636f6d706c657461626c656675747572652d636c6173732d6469616772616d2e6a7067) + +`FutureTask` 有两个构造函数,可传入 `Callable` 或者 `Runnable` 对象。实际上,传入 `Runnable` 对象也会在方法内部转换为`Callable` 对象。 + +``` +public FutureTask(Callable callable) { + if (callable == null) + throw new NullPointerException(); + this.callable = callable; + this.state = NEW; +} +public FutureTask(Runnable runnable, V result) { + // 通过适配器 RunnableAdapter 来将 Runnable 对象 runnable 转换成 Callable 对象 + this.callable = Executors.callable(runnable, result); + this.state = NEW; +} +``` + +`FutureTask`相当于对`Callable` 进行了封装,管理着任务执行的情况,存储了 `Callable` 的 `call` 方法的任务执行结果。 + +### CompletableFuture 类有什么用? + +`Future` 在实际使用过程中存在一些局限性比如不支持异步任务的编排组合、获取计算结果的 `get()` 方法为阻塞调用。 + +Java 8 才被引入`CompletableFuture` 类可以解决`Future` 的这些缺陷。`CompletableFuture` 除了提供了更为好用和强大的 `Future` 特性之外,还提供了函数式编程、异步任务编排组合(可以将多个异步任务串联起来,组成一个完整的链式调用)等能力。 + +下面我们来简单看看 `CompletableFuture` 类的定义。 + +``` +public class CompletableFuture implements Future, CompletionStage { +} +``` + +可以看到,`CompletableFuture` 同时实现了 `Future` 和 `CompletionStage` 接口。 + +[![img](https://camo.githubusercontent.com/11ad130bc21f74bdc678cfb34a8626983cfc186a1a0467137a6f3406dcde715e/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f636f6d706c657461626c656675747572652d636c6173732d6469616772616d2e6a7067)](https://camo.githubusercontent.com/11ad130bc21f74bdc678cfb34a8626983cfc186a1a0467137a6f3406dcde715e/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f636f6d706c657461626c656675747572652d636c6173732d6469616772616d2e6a7067) + +`CompletionStage` 接口描述了一个异步计算的阶段。很多计算可以分成多个阶段或步骤,此时可以通过它将所有步骤组合起来,形成异步计算的流水线。 + +`CompletionStage` 接口中的方法比较多,`CompletableFuture` 的函数式能力就是这个接口赋予的。从这个接口的方法参数你就可以发现其大量使用了 Java8 引入的函数式编程。 + +[![img](https://camo.githubusercontent.com/bae81809cbfbee278ea8553cb9f67f0a656e8ca3c71b21050d5ac13b0d3a89c9/68747470733a2f2f6f73732e6a61766167756964652e636e2f6a61766167756964652f696d6167652d32303231303930323039333032363035392e706e67)](https://camo.githubusercontent.com/bae81809cbfbee278ea8553cb9f67f0a656e8ca3c71b21050d5ac13b0d3a89c9/68747470733a2f2f6f73732e6a61766167756964652e636e2f6a61766167756964652f696d6167652d32303231303930323039333032363035392e706e67) + +## AQS + +### AQS 是什么? + +AQS 的全称为 `AbstractQueuedSynchronizer` ,翻译过来的意思就是抽象队列同步器。这个类在 `java.util.concurrent.locks` 包下面。 + +[![img](https://camo.githubusercontent.com/f005531c065284d60ab560110b41e0d91de345807cfd658e7539602b2a8e916c/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f4151532e706e67)](https://camo.githubusercontent.com/f005531c065284d60ab560110b41e0d91de345807cfd658e7539602b2a8e916c/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f4151532e706e67) + +AQS 就是一个抽象类,主要用来构建锁和同步器。 + +``` +public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable { +} +``` + +AQS 为构建锁和同步器提供了一些通用功能的实现,因此,使用 AQS 能简单且高效地构造出应用广泛的大量的同步器,比如我们提到的 `ReentrantLock`,`Semaphore`,其他的诸如 `ReentrantReadWriteLock`,`SynchronousQueue`等等皆是基于 AQS 的。 + +### AQS 的原理是什么? + +AQS 核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用 **CLH 队列锁** 实现的,即将暂时获取不到锁的线程加入到队列中。 + +CLH(Craig,Landin,and Hagersten) 队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、 当前节点在队列中的状态(waitStatus)、前驱节点(prev)、后继节点(next)。 + +CLH 队列结构如下图所示: + +[![img](https://camo.githubusercontent.com/889c9071ff9311e4692e51259330c5aa977cbe8920e2b3f83b7fcb99e0c6c120/68747470733a2f2f6f73732e6a61766167756964652e636e2f70332d6a75656a696e2f34306362393332613634363934323632393933393037656264613661306266657e74706c762d6b3375316662706663702d7a6f6f6d2d312e706e67)](https://camo.githubusercontent.com/889c9071ff9311e4692e51259330c5aa977cbe8920e2b3f83b7fcb99e0c6c120/68747470733a2f2f6f73732e6a61766167756964652e636e2f70332d6a75656a696e2f34306362393332613634363934323632393933393037656264613661306266657e74706c762d6b3375316662706663702d7a6f6f6d2d312e706e67) + +AQS(`AbstractQueuedSynchronizer`) 的核心原理图(图源 [Java 并发之 AQS 详解](https://www.cnblogs.com/waterystone/p/4920797.html))如下: + +[![img](https://camo.githubusercontent.com/fc9871754fb4d8fec38abcba2837d12765c800432241be11fc8892e5b9f2fe50/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f434c482e706e67)](https://camo.githubusercontent.com/fc9871754fb4d8fec38abcba2837d12765c800432241be11fc8892e5b9f2fe50/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f434c482e706e67) + +AQS 使用 **int 成员变量 `state` 表示同步状态**,通过内置的 **线程等待队列** 来完成获取资源线程的排队工作。 + +`state` 变量由 `volatile` 修饰,用于展示当前临界资源的获锁情况。 + +``` +// 共享变量,使用 volatile 修饰保证线程可见性 +private volatile int state; +``` + +另外,状态信息 `state` 可以通过 `protected` 类型的`getState()`、`setState()`和`compareAndSetState()` 进行操作。并且,这几个方法都是 `final` 修饰的,在子类中无法被重写。 + +``` +//返回同步状态的当前值 +protected final int getState() { + return state; +} + // 设置同步状态的值 +protected final void setState(int newState) { + state = newState; +} +//原子地(CAS 操作)将同步状态值设置为给定值 update 如果当前同步状态的值等于 expect(期望值) +protected final boolean compareAndSetState(int expect, int update) { + return unsafe.compareAndSwapInt(this, stateOffset, expect, update); +} +``` + +以 `ReentrantLock` 为例,`state` 初始值为 0,表示未锁定状态。A 线程 `lock()` 时,会调用 `tryAcquire()` 独占该锁并将 `state+1` 。此后,其他线程再 `tryAcquire()` 时就会失败,直到 A 线程 `unlock()` 到 `state=`0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A 线程自己是可以重复获取此锁的(`state` 会累加),这就是可重入的概念。但要注意,获取多少次就要释放多少次,这样才能保证 state 是能回到零态的。 + +再以 `CountDownLatch` 以例,任务分为 N 个子线程去执行,`state` 也初始化为 N(注意 N 要与线程个数一致)。这 N 个子线程是并行执行的,每个子线程执行完后`countDown()` 一次,state 会 CAS(Compare and Swap) 减 1。等到所有子线程都执行完后(即 `state=0` ),会 `unpark()` 主调用线程,然后主调用线程就会从 `await()` 函数返回,继续后余动作。 + +### Semaphore 有什么用? + +`synchronized` 和 `ReentrantLock` 都是一次只允许一个线程访问某个资源,而`Semaphore`(信号量)可以用来控制同时访问特定资源的线程数量。 + +Semaphore 的使用简单,我们这里假设有 N(N>5) 个线程来获取 `Semaphore` 中的共享资源,下面的代码表示同一时刻 N 个线程中只有 5 个线程能获取到共享资源,其他线程都会阻塞,只有获取到共享资源的线程才能执行。等到有线程释放了共享资源,其他阻塞的线程才能获取到。 + +``` +// 初始共享资源数量 +final Semaphore semaphore = new Semaphore(5); +// 获取 1 个许可 +semaphore.acquire(); +// 释放 1 个许可 +semaphore.release(); +``` + +当初始的资源个数为 1 的时候,`Semaphore` 退化为排他锁。 + +`Semaphore` 有两种模式:。 + +- **公平模式:** 调用 `acquire()` 方法的顺序就是获取许可证的顺序,遵循 FIFO; +- **非公平模式:** 抢占式的。 + +`Semaphore` 对应的两个构造方法如下: + +``` +public Semaphore(int permits) { + sync = new NonfairSync(permits); +} + +public Semaphore(int permits, boolean fair) { + sync = fair ? new FairSync(permits) : new NonfairSync(permits); +} +``` + +**这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** + +`Semaphore` 通常用于那些资源有明确访问数量限制的场景比如限流(仅限于单机模式,实际项目中推荐使用 Redis +Lua 来做限流)。 + +### Semaphore 的原理是什么? + +`Semaphore` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `permits`,你可以将 `permits` 的值理解为许可证的数量,只有拿到许可证的线程才能执行。 + +调用`semaphore.acquire()` ,线程尝试获取许可证,如果 `state >= 0` 的话,则表示可以获取成功。如果获取成功的话,使用 CAS 操作去修改 `state` 的值 `state=state-1`。如果 `state<0` 的话,则表示许可证数量不足。此时会创建一个 Node 节点加入阻塞队列,挂起当前线程。 + +``` +/** + * 获取 1 个许可证 + */ +public void acquire() throws InterruptedException { + sync.acquireSharedInterruptibly(1); +} +/** + * 共享模式下获取许可证,获取成功则返回,失败则加入阻塞队列,挂起线程 + */ +public final void acquireSharedInterruptibly(int arg) + throws InterruptedException { + if (Thread.interrupted()) + throw new InterruptedException(); + // 尝试获取许可证,arg 为获取许可证个数,当可用许可证数减当前获取的许可证数结果小于 0, 则创建一个节点加入阻塞队列,挂起当前线程。 + if (tryAcquireShared(arg) < 0) + doAcquireSharedInterruptibly(arg); +} +``` + +调用`semaphore.release();` ,线程尝试释放许可证,并使用 CAS 操作去修改 `state` 的值 `state=state+1`。释放许可证成功之后,同时会唤醒同步队列中的一个线程。被唤醒的线程会重新尝试去修改 `state` 的值 `state=state-1` ,如果 `state>=0` 则获取令牌成功,否则重新进入阻塞队列,挂起线程。 + +``` +// 释放一个许可证 +public void release() { + sync.releaseShared(1); +} + +// 释放共享锁,同时会唤醒同步队列中的一个线程。 +public final boolean releaseShared(int arg) { + //释放共享锁 + if (tryReleaseShared(arg)) { + //唤醒同步队列中的一个线程 + doReleaseShared(); + return true; + } + return false; +} +``` + +### CountDownLatch 有什么用? + +`CountDownLatch` 允许 `count` 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。 + +`CountDownLatch` 是一次性的,计数器的值只能在构造方法中初始化一次,之后没有任何机制再次对其设置值,当 `CountDownLatch` 使用完毕后,它不能再次被使用。 + +### CountDownLatch 的原理是什么? + +`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`, 直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。直到`count` 个线程调用了`countDown()`使 state 值被减为 0,或者调用`await()`的线程被中断,该线程才会从阻塞中被唤醒,`await()` 方法之后的语句得到执行。 + +### 用过 CountDownLatch 么?什么场景下用的? + +`CountDownLatch` 的作用就是 允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。之前在项目中,有一个使用多线程读取多个文件处理的场景,我用到了 `CountDownLatch` 。具体场景是下面这样的: + +我们要读取处理 6 个文件,这 6 个任务都是没有执行顺序依赖的任务,但是我们需要返回给用户的时候将这几个文件的处理的结果进行统计整理。 + +为此我们定义了一个线程池和 count 为 6 的`CountDownLatch`对象 。使用线程池处理读取任务,每一个线程处理完之后就将 count-1,调用`CountDownLatch`对象的 `await()`方法,直到所有文件读取完之后,才会接着执行后面的逻辑。 + +伪代码是下面这样的: + +``` +public class CountDownLatchExample1 { + // 处理文件的数量 + private static final int threadCount = 6; + + public static void main(String[] args) throws InterruptedException { + // 创建一个具有固定线程数量的线程池对象(推荐使用构造方法创建) + ExecutorService threadPool = Executors.newFixedThreadPool(10); + final CountDownLatch countDownLatch = new CountDownLatch(threadCount); + for (int i = 0; i < threadCount; i++) { + final int threadnum = i; + threadPool.execute(() -> { + try { + //处理文件的业务操作 + //...... + } catch (InterruptedException e) { + e.printStackTrace(); + } finally { + //表示一个文件已经被完成 + countDownLatch.countDown(); + } + + }); + } + countDownLatch.await(); + threadPool.shutdown(); + System.out.println("finish"); + } +} +``` + +**有没有可以改进的地方呢?** + +可以使用 `CompletableFuture` 类来改进!Java8 的 `CompletableFuture` 提供了很多对多线程友好的方法,使用它可以很方便地为我们编写多线程程序,什么异步、串行、并行或者等待所有线程执行完任务什么的都非常方便。 + +``` +CompletableFuture task1 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... +CompletableFuture task6 = + CompletableFuture.supplyAsync(()->{ + //自定义业务操作 + }); +...... +CompletableFuture headerFuture=CompletableFuture.allOf(task1,.....,task6); + +try { + headerFuture.join(); +} catch (Exception ex) { + //...... +} +System.out.println("all done. "); +``` + +上面的代码还可以继续优化,当任务过多的时候,把每一个 task 都列出来不太现实,可以考虑通过循环来添加任务。 + +``` +//文件夹位置 +List filePaths = Arrays.asList(...) +// 异步处理所有文件 +List> fileFutures = filePaths.stream() + .map(filePath -> doSomeThing(filePath)) + .collect(Collectors.toList()); +// 将他们合并起来 +CompletableFuture allFutures = CompletableFuture.allOf( + fileFutures.toArray(new CompletableFuture[fileFutures.size()]) +); +``` + +### CyclicBarrier 有什么用? + +`CyclicBarrier` 和 `CountDownLatch` 非常类似,它也可以实现线程间的技术等待,但是它的功能比 `CountDownLatch` 更加复杂和强大。主要应用场景和 `CountDownLatch` 类似。 + +> `CountDownLatch` 的实现是基于 AQS 的,而 `CycliBarrier` 是基于 `ReentrantLock`(`ReentrantLock` 也属于 AQS 同步器)和 `Condition` 的。 + +`CyclicBarrier` 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是:让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。 + +### CyclicBarrier 的原理是什么? + +`CyclicBarrier` 内部通过一个 `count` 变量作为计数器,`count` 的初始值为 `parties` 属性的初始化值,每当一个线程到了栅栏这里了,那么就将计数器减 1。如果 count 值为 0 了,表示这是这一代最后一个线程到达栅栏,就尝试执行我们构造方法中输入的任务。 + +``` +//每次拦截的线程数 +private final int parties; +//计数器 +private int count; +``` + +下面我们结合源码来简单看看。 + +1、`CyclicBarrier` 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用 `await()` 方法告诉 `CyclicBarrier` 我已经到达了屏障,然后当前线程被阻塞。 + +``` +public CyclicBarrier(int parties) { + this(parties, null); +} + +public CyclicBarrier(int parties, Runnable barrierAction) { + if (parties <= 0) throw new IllegalArgumentException(); + this.parties = parties; + this.count = parties; + this.barrierCommand = barrierAction; +} +``` + +其中,`parties` 就代表了有拦截的线程的数量,当拦截的线程数量达到这个值的时候就打开栅栏,让所有线程通过。 + +2、当调用 `CyclicBarrier` 对象调用 `await()` 方法时,实际上调用的是 `dowait(false, 0L)`方法。 `await()` 方法就像树立起一个栅栏的行为一样,将线程挡住了,当拦住的线程数量达到 `parties` 的值时,栅栏才会打开,线程才得以通过执行。 + +``` +public int await() throws InterruptedException, BrokenBarrierException { + try { + return dowait(false, 0L); + } catch (TimeoutException toe) { + throw new Error(toe); // cannot happen + } +} +``` + +`dowait(false, 0L)`方法源码分析如下: + +``` + // 当线程数量或者请求数量达到 count 时 await 之后的方法才会被执行。上面的示例中 count 的值就为 5。 + private int count; + /** + * Main barrier code, covering the various policies. + */ + private int dowait(boolean timed, long nanos) + throws InterruptedException, BrokenBarrierException, + TimeoutException { + final ReentrantLock lock = this.lock; + // 锁住 + lock.lock(); + try { + final Generation g = generation; + + if (g.broken) + throw new BrokenBarrierException(); + + // 如果线程中断了,抛出异常 + if (Thread.interrupted()) { + breakBarrier(); + throw new InterruptedException(); + } + // cout 减 1 + int index = --count; + // 当 count 数量减为 0 之后说明最后一个线程已经到达栅栏了,也就是达到了可以执行 await 方法之后的条件 + if (index == 0) { // tripped + boolean ranAction = false; + try { + final Runnable command = barrierCommand; + if (command != null) + command.run(); + ranAction = true; + // 将 count 重置为 parties 属性的初始化值 + // 唤醒之前等待的线程 + // 下一波执行开始 + nextGeneration(); + return 0; + } finally { + if (!ranAction) + breakBarrier(); + } + } + + // loop until tripped, broken, interrupted, or timed out + for (;;) { + try { + if (!timed) + trip.await(); + else if (nanos > 0L) + nanos = trip.awaitNanos(nanos); + } catch (InterruptedException ie) { + if (g == generation && ! g.broken) { + breakBarrier(); + throw ie; + } else { + // We're about to finish waiting even if we had not + // been interrupted, so this interrupt is deemed to + // "belong" to subsequent execution. + Thread.currentThread().interrupt(); + } + } + + if (g.broken) + throw new BrokenBarrierException(); + + if (g != generation) + return index; + + if (timed && nanos <= 0L) { + breakBarrier(); + throw new TimeoutException(); + } + } + } finally { + lock.unlock(); + } + } +``` + +## 虚拟线程 + +虚拟线程在 Java 21 正式发布,这是一项重量级的更新。 + +虽然目前面试中问的不多,但还是建议大家去简单了解一下,具体可以阅读这篇文章:[虚拟线程极简入门](https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/concurrent/virtual-thread.md) 。重点搞清楚虚拟线程和平台线程的关系以及虚拟线程的优势即可。 + +## 案例 + +### 生产者消费者模式 + +**经典问题** + +(1)什么是生产者消费者模式 + +(2)Java 中如何实现生产者消费者模式 + +**知识点** + +(1)什么是生产者消费者模式 + +生产者消费者模式是一个经典的并发设计模式。在这个模型中,有一个共享缓冲区;有两个线程,一个负责向缓冲区推数据,另一个负责向缓冲区拉数据。要让两个线程更好的配合,就需要一个阻塞队列作为媒介来进行调度,由此便诞生了生产者消费者模式。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/Java%20%e5%b9%b6%e5%8f%91%e7%bc%96%e7%a8%8b%2078%20%e8%ae%b2-%e5%ae%8c/assets/CgotOV3OJ3iAGcaiAAFrcv5xk9U160.png) + +(2)Java 中如何实现生产者消费者模式 + +在 Java 中,实现生产者消费者模式有 3 种具有代表性的方式: + +- 基于 BlockingQueue 实现 +- 基于 Condition 实现 +- 基于 wait/notify 实现 + +【示例】基于 BlockingQueue 实现生产者消费者模式 + +```java +public class ProducerConsumerDemo01 { + + public static void main(String[] args) throws InterruptedException { + BlockingQueue queue = new ArrayBlockingQueue<>(10); + Thread producer1 = new Thread(new Producer(queue), "producer1"); + Thread producer2 = new Thread(new Producer(queue), "producer2"); + Thread consumer1 = new Thread(new Consumer(queue), "consumer1"); + Thread consumer2 = new Thread(new Consumer(queue), "consumer2"); + producer1.start(); + producer2.start(); + consumer1.start(); + consumer2.start(); + } + + static class Producer implements Runnable { + + private long count = 0L; + private final BlockingQueue queue; + + public Producer(BlockingQueue queue) { + this.queue = queue; + } + + @Override + public void run() { + while (count < 500) { + try { + queue.put(new Object()); + System.out.println(Thread.currentThread().getName() + " 生产1条数据,已生产数据量:" + ++count); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + } + + static class Consumer implements Runnable { + + private long count = 0L; + private final BlockingQueue queue; + + public Consumer(BlockingQueue queue) { + this.queue = queue; + } + + @Override + public void run() { + while (count < 500) { + try { + queue.take(); + System.out.println(Thread.currentThread().getName() + " 消费1条数据,已消费数据量:" + ++count); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } + + } + +} +``` + +【示例】基于 Condition 实现生产者消费者模式 + +```java +public class ProducerConsumerDemo02 { + + public static void main(String[] args) { + + MyBlockingQueue queue = new MyBlockingQueue<>(10); + Runnable producer = () -> { + while (true) { + try { + queue.put(new Object()); + System.out.println("生产1条数据,总数据量:" + queue.size()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + + new Thread(producer).start(); + new Thread(producer).start(); + + Runnable consumer = () -> { + while (true) { + try { + queue.take(); + System.out.println("消费1条数据,总数据量:" + queue.size()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + + new Thread(consumer).start(); + new Thread(consumer).start(); + } + + public static class MyBlockingQueue { + + private final int max; + private final Queue queue; + + private final ReentrantLock lock = new ReentrantLock(); + private final Condition notEmpty = lock.newCondition(); + private final Condition notFull = lock.newCondition(); + + public MyBlockingQueue(int size) { + this.max = size; + queue = new LinkedList<>(); + } + + public void put(T o) throws InterruptedException { + lock.lock(); + try { + while (queue.size() == max) { + notFull.await(); + } + queue.add(o); + notEmpty.signalAll(); + } finally { + lock.unlock(); + } + } + + public T take() throws InterruptedException { + lock.lock(); + try { + while (queue.isEmpty()) { + notEmpty.await(); + } + T o = queue.remove(); + notFull.signalAll(); + return o; + } finally { + lock.unlock(); + } + } + + public int size() { + return queue.size(); + } + + } + +} +``` + +【示例】基于 wait/notify 实现生产者消费者模式 + +```java +public class ProducerConsumerDemo03 { + + public static void main(String[] args) { + + MyBlockingQueue queue = new MyBlockingQueue<>(10); + Runnable producer = () -> { + while (true) { + try { + queue.put(new Object()); + System.out.println("生产1条数据,总数据量:" + queue.size()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + + new Thread(producer).start(); + new Thread(producer).start(); + + Runnable consumer = () -> { + while (true) { + try { + queue.take(); + System.out.println("消费1条数据,总数据量:" + queue.size()); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + }; + + new Thread(consumer).start(); + new Thread(consumer).start(); + } + + public static class MyBlockingQueue { + + private final int max; + private final Queue queue; + + public MyBlockingQueue(int size) { + max = size; + queue = new LinkedList<>(); + } + + public synchronized void put(T o) throws InterruptedException { + while (queue.size() == max) { + wait(); + } + queue.add(o); + notifyAll(); + } + + public synchronized T take() throws InterruptedException { + while (queue.isEmpty()) { + wait(); + } + T o = queue.remove(); + notifyAll(); + return o; + } + + public synchronized int size() { + return queue.size(); + } + + } + +} +``` \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\272\214.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\272\214.md" new file mode 100644 index 0000000000..8f6d816a99 --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\345\271\266\345\217\221\351\235\242\350\257\225\344\272\214.md" @@ -0,0 +1,429 @@ +--- +title: Java 并发面试二 +date: 2024-07-23 07:21:03 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 + - 并发 +permalink: /pages/b1e468f4/ +--- + +# Java 并发面试二 + +## JMM(Java 内存模型) + +JMM(Java 内存模型)相关的问题比较多,也比较重要,于是我单独抽了一篇文章来总结 JMM 相关的知识点和问题:[JMM(Java 内存模型)详解](https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/concurrent/jmm.md) 。 + +## 乐观锁和悲观锁 + +### 什么是悲观锁? + +悲观锁总是假设最坏的情况,认为共享资源每次被访问的时候就会出现问题(比如共享数据被修改),所以每次在获取资源操作的时候都会上锁,这样其他线程想拿到这个资源就会阻塞直到锁被上一个持有者释放。也就是说,**共享资源每次只给一个线程使用,其它线程阻塞,用完后再把资源转让给其它线程**。 + +像 Java 中 `synchronized` 和 `ReentrantLock` 等独占锁就是悲观锁思想的实现。 + +``` +public void performSynchronisedTask() { + synchronized (this) { + // 需要同步的操作 + } +} + +private Lock lock = new ReentrantLock(); +lock.lock(); +try { + // 需要同步的操作 +} finally { + lock.unlock(); +} +``` + +高并发的场景下,激烈的锁竞争会造成线程阻塞,大量阻塞线程会导致系统的上下文切换,增加系统的性能开销。并且,悲观锁还可能会存在死锁问题,影响代码的正常运行。 + +### 什么是乐观锁? + +乐观锁总是假设最好的情况,认为共享资源每次被访问的时候不会出现问题,线程可以不停地执行,无需加锁也无需等待,只是在提交修改的时候去验证对应的资源(也就是数据)是否被其它线程修改了(具体方法可以使用版本号机制或 CAS 算法)。 + +在 Java 中 `java.util.concurrent.atomic` 包下面的原子变量类(比如 `AtomicInteger`、`LongAdder`)就是使用了乐观锁的一种实现方式 **CAS** 实现的。 [![JUC 原子类概览](https://camo.githubusercontent.com/dc483c985184b7b69b8c73f0c98fd522da1c51eccf4638410b86a0b04944c2c8/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f4a55432545352538452539462545352541442539302545372542312542422545362541362538322545382541372538382d32303233303831343030353231313936382e706e67)](https://camo.githubusercontent.com/dc483c985184b7b69b8c73f0c98fd522da1c51eccf4638410b86a0b04944c2c8/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f4a55432545352538452539462545352541442539302545372542312542422545362541362538322545382541372538382d32303233303831343030353231313936382e706e67) + +``` +// LongAdder 在高并发场景下会比 AtomicInteger 和 AtomicLong 的性能更好 +// 代价就是会消耗更多的内存空间(空间换时间) +LongAdder sum = new LongAdder(); +sum.increment(); +``` + +高并发的场景下,乐观锁相比悲观锁来说,不存在锁竞争造成线程阻塞,也不会有死锁的问题,在性能上往往会更胜一筹。但是,如果冲突频繁发生(写占比非常多的情况),会频繁失败和重试,这样同样会非常影响性能,导致 CPU 飙升。 + +不过,大量失败重试的问题也是可以解决的,像我们前面提到的 `LongAdder` 以空间换时间的方式就解决了这个问题。 + +理论上来说: + +- 悲观锁通常多用于写比较多的情况(多写场景,竞争激烈),这样可以避免频繁失败和重试影响性能,悲观锁的开销是固定的。不过,如果乐观锁解决了频繁失败和重试这个问题的话(比如 `LongAdder`),也是可以考虑使用乐观锁的,要视实际情况而定。 +- 乐观锁通常多用于写比较少的情况(多读场景,竞争较少),这样可以避免频繁加锁影响性能。不过,乐观锁主要针对的对象是单个共享变量(参考 `java.util.concurrent.atomic` 包下面的原子变量类)。 + +### 如何实现乐观锁? + +乐观锁一般会使用版本号机制或 CAS 算法实现,CAS 算法相对来说更多一些,这里需要格外注意。 + +#### 版本号机制 + +一般是在数据表中加上一个数据版本号 `version` 字段,表示数据被修改的次数。当数据被修改时,`version` 值会加一。当线程 A 要更新数据值时,在读取数据的同时也会读取 `version` 值,在提交更新时,若刚才读取到的 version 值为当前数据库中的 `version` 值相等时才更新,否则重试更新操作,直到更新成功。 + +**举一个简单的例子**:假设数据库中帐户信息表中有一个 version 字段,当前值为 1 ;而当前帐户余额字段( `balance` )为 $100 。 + +1. 操作员 A 此时将其读出( `version`=1 ),并从其帐户余额中扣除 $50( $100-$50 )。 +2. 在操作员 A 操作的过程中,操作员 B 也读入此用户信息( `version`=1 ),并从其帐户余额中扣除 $20 ( $100-$20 )。 +3. 操作员 A 完成了修改工作,将数据版本号( `version`=1 ),连同帐户扣除后余额( `balance`=$50 ),提交至数据库更新,此时由于提交数据版本等于数据库记录当前版本,数据被更新,数据库记录 `version` 更新为 2 。 +4. 操作员 B 完成了操作,也将版本号( `version`=1 )试图向数据库提交数据( `balance`=$80 ),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 1 ,数据库记录当前版本也为 2 ,不满足 “ 提交版本必须等于当前版本才能执行更新 “ 的乐观锁策略,因此,操作员 B 的提交被驳回。 + +这样就避免了操作员 B 用基于 `version`=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。 + +#### CAS 算法 + +CAS 的全称是 **Compare And Swap(比较与交换)** ,用于实现乐观锁,被广泛应用于各大框架中。CAS 的思想很简单,就是用一个预期值和要更新的变量值进行比较,两值相等才会进行更新。 + +CAS 是一个原子操作,底层依赖于一条 CPU 的原子指令。 + +> ** 原子操作 ** 即最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,直到操作完成。 + +CAS 涉及到三个操作数: + +- **V**:要更新的变量值 (Var) +- **E**:预期值 (Expected) +- **N**:拟写入的新值 (New) + +当且仅当 V 的值等于 E 时,CAS 通过原子方式用新值 N 来更新 V 的值。如果不等,说明已经有其它线程更新了 V,则当前线程放弃更新。 + +**举一个简单的例子**:线程 A 要修改变量 i 的值为 6,i 原值为 1(V = 1,E=1,N=6,假设不存在 ABA 问题)。 + +1. i 与 1 进行比较,如果相等, 则说明没被其他线程修改,可以被设置为 6 。 +2. i 与 1 进行比较,如果不相等,则说明被其他线程修改,当前线程放弃更新,CAS 操作失败。 + +当多个线程同时使用 CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作。 + +Java 语言并没有直接实现 CAS,CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI 调用)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。 + +`sun.misc` 包下的 `Unsafe` 类提供了 `compareAndSwapObject`、`compareAndSwapInt`、`compareAndSwapLong` 方法来实现的对 `Object`、`int`、`long` 类型的 CAS 操作 + +``` +/** + * CAS + * @param o 包含要修改 field 的对象 + * @param offset 对象中某 field 的偏移量 + * @param expected 期望值 + * @param update 更新值 + * @return true | false + */ +public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); + +public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); + +public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update); +``` + +关于 `Unsafe` 类的详细介绍可以看这篇文章:[Java 魔法类 Unsafe 详解 - JavaGuide - 2022](https://javaguide.cn/java/basis/unsafe.html) 。 + +### CAS 算法存在哪些问题? + +ABA 问题是 CAS 算法最常见的问题。 + +#### ABA 问题 + +如果一个变量 V 初次读取的时候是 A 值,并且在准备赋值的时候检查到它仍然是 A 值,那我们就能说明它的值没有被其他线程修改过了吗?很明显是不能的,因为在这段时间它的值可能被改为其他值,然后又改回 A,那 CAS 操作就会误认为它从来没有被修改过。这个问题被称为 CAS 操作的 **"ABA"问题。** + +ABA 问题的解决思路是在变量前面追加上 **版本号或者时间戳**。JDK 1.5 以后的 `AtomicStampedReference` 类就是用来解决 ABA 问题的,其中的 `compareAndSet()` 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。 + +``` +public boolean compareAndSet(V expectedReference, + V newReference, + int expectedStamp, + int newStamp) { + Pair current = pair; + return + expectedReference == current.reference && + expectedStamp == current.stamp && + ((newReference == current.reference && + newStamp == current.stamp) || + casPair(current, Pair.of(newReference, newStamp))); +} +``` + +#### 循环时间长开销大 + +CAS 经常会用到自旋操作来进行重试,也就是不成功就一直循环执行直到成功。如果长时间不成功,会给 CPU 带来非常大的执行开销。 + +如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用: + +1. 可以延迟流水线执行指令,使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 +2. 可以避免在退出循环的时候因内存顺序冲突而引起 CPU 流水线被清空,从而提高 CPU 的执行效率。 + +#### 只能保证一个共享变量的原子操作 + +CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效。但是从 JDK 1.5 开始,提供了 `AtomicReference` 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。所以我们可以使用锁或者利用 `AtomicReference` 类把多个共享变量合并成一个共享变量来操作。 + +## ReentrantLock + +### ReentrantLock 是什么? + +`ReentrantLock` 实现了 `Lock` 接口,是一个可重入且独占式的锁,和 `synchronized` 关键字类似。不过,`ReentrantLock` 更灵活、更强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。 + +``` +public class ReentrantLock implements Lock, java.io.Serializable {} +``` + +`ReentrantLock` 里面有一个内部类 `Sync`,`Sync` 继承 AQS(`AbstractQueuedSynchronizer`),添加锁和释放锁的大部分操作实际上都是在 `Sync` 中实现的。`Sync` 有公平锁 `FairSync` 和非公平锁 `NonfairSync` 两个子类。 + +[![img](https://camo.githubusercontent.com/d08903b8450071ab6280dfd9ff0ed74ebcfd0a3ebba6ba26eee3c596e7f366aa/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7265656e7472616e746c6f636b2d636c6173732d6469616772616d2e706e67)](https://camo.githubusercontent.com/d08903b8450071ab6280dfd9ff0ed74ebcfd0a3ebba6ba26eee3c596e7f366aa/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7265656e7472616e746c6f636b2d636c6173732d6469616772616d2e706e67) + +`ReentrantLock` 默认使用非公平锁,也可以通过构造器来显式的指定使用公平锁。 + +``` +// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁 +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync(): new NonfairSync(); +} +``` + +从上面的内容可以看出, `ReentrantLock` 的底层就是由 AQS 来实现的。关于 AQS 的相关内容推荐阅读 [AQS 详解](https://javaguide.cn/java/concurrent/aqs.html) 这篇文章。 + +### 公平锁和非公平锁有什么区别? + +- **公平锁** : 锁被释放之后,先申请的线程先得到锁。性能较差一些,因为公平锁为了保证时间上的绝对顺序,上下文切换更频繁。 +- **非公平锁**:锁被释放之后,后申请的线程可能会先获取到锁,是随机或者按照其他优先级排序的。性能更好,但可能会导致某些线程永远无法获取到锁。 + +### synchronized 和 ReentrantLock 有什么区别? + +#### 两者都是可重入锁 + +**可重入锁** 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果是不可重入锁的话,就会造成死锁。 + +JDK 提供的所有现成的 `Lock` 实现类,包括 `synchronized` 关键字锁都是可重入的。 + +在下面的代码中,`method1()` 和 `method2()` 都被 `synchronized` 关键字修饰,`method1()` 调用了 `method2()`。 + +``` +public class SynchronizedDemo { + public synchronized void method1() { + System.out.println("方法 1"); + method2(); + } + + public synchronized void method2() { + System.out.println("方法 2"); + } +} +``` + +由于 `synchronized` 锁是可重入的,同一个线程在调用 `method1()` 时可以直接获得当前对象的锁,执行 `method2()` 的时候可以再次获取这个对象的锁,不会产生死锁问题。假如 `synchronized` 是不可重入锁的话,由于该对象的锁已被当前线程所持有且无法释放,这就导致线程在执行 `method2()` 时获取锁失败,会出现死锁问题。 + +#### synchronized 依赖于 JVM 而 ReentrantLock 依赖于 API + +`synchronized` 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 `synchronized` 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。 + +`ReentrantLock` 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock() 方法配合 try/finally 语句块来完成),所以我们可以通过查看它的源代码,来看它是如何实现的。 + +#### ReentrantLock 比 synchronized 增加了一些高级功能 + +相比 `synchronized`,`ReentrantLock` 增加了一些高级功能。主要来说主要有三点: + +- **等待可中断** : `ReentrantLock` 提供了一种能够中断等待锁的线程的机制,通过 `lock.lockInterruptibly()` 来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 +- **可实现公平锁** : `ReentrantLock` 可以指定是公平锁还是非公平锁。而 `synchronized` 只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。`ReentrantLock` 默认情况是非公平的,可以通过 `ReentrantLock` 类的 `ReentrantLock(boolean fair)` 构造方法来指定是否是公平的。 +- **可实现选择性通知(锁可以绑定多个条件)**: `synchronized` 关键字与 `wait()` 和 `notify()`/`notifyAll()` 方法相结合可以实现等待 / 通知机制。`ReentrantLock` 类当然也可以实现,但是需要借助于 `Condition` 接口与 `newCondition()` 方法。 + +如果你想使用上述功能,那么选择 `ReentrantLock` 是一个不错的选择。 + +关于 `Condition` 接口的补充: + +> `Condition` 是 JDK1.5 之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个 `Lock` 对象中可以创建多个 `Condition` 实例(即对象监视器),**线程对象可以注册在指定的 `Condition` 中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用 `notify()/notifyAll()` 方法进行通知时,被通知的线程是由 JVM 选择的,用 `ReentrantLock` 类结合 `Condition` 实例可以实现“选择性通知”** ,这个功能非常重要,而且是 `Condition` 接口默认提供的。而 `synchronized` 关键字就相当于整个 `Lock` 对象中只有一个 `Condition` 实例,所有的线程都注册在它一个身上。如果执行 `notifyAll()` 方法的话就会通知所有处于等待状态的线程,这样会造成很大的效率问题。而 `Condition` 实例的 `signalAll()` 方法,只会唤醒注册在该 `Condition` 实例中的所有等待线程。 + +### 可中断锁和不可中断锁有什么区别? + +- **可中断锁**:获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。`ReentrantLock` 就属于是可中断锁。 +- **不可中断锁**:一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。 `synchronized` 就属于是不可中断锁。 + +## ReentrantReadWriteLock + +`ReentrantReadWriteLock` 在实际项目中使用的并不多,面试中也问的比较少,简单了解即可。JDK 1.8 引入了性能更好的读写锁 `StampedLock` 。 + +### ReentrantReadWriteLock 是什么? + +`ReentrantReadWriteLock` 实现了 `ReadWriteLock` ,是一个可重入的读写锁,既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。 + +``` +public class ReentrantReadWriteLock + implements ReadWriteLock, java.io.Serializable{ +} +public interface ReadWriteLock { + Lock readLock(); + Lock writeLock(); +} +``` + +- 一般锁进行并发控制的规则:读读互斥、读写互斥、写写互斥。 +- 读写锁进行并发控制的规则:读读不互斥、读写互斥、写写互斥(只有读读不互斥)。 + +`ReentrantReadWriteLock` 其实是两把锁,一把是 `WriteLock` (写锁),一把是 `ReadLock`(读锁) 。读锁是共享锁,写锁是独占锁。读锁可以被同时读,可以同时被多个线程持有,而写锁最多只能同时被一个线程持有。 + +和 `ReentrantLock` 一样,`ReentrantReadWriteLock` 底层也是基于 AQS 实现的。 + +[![img](https://camo.githubusercontent.com/0105b54599441118d430cf64703e34eb8fb9a1cd29de1b8e08d3a2485935a482/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7265656e7472616e747265616477726974656c6f636b2d636c6173732d6469616772616d2e706e67)](https://camo.githubusercontent.com/0105b54599441118d430cf64703e34eb8fb9a1cd29de1b8e08d3a2485935a482/68747470733a2f2f6f73732e6a61766167756964652e636e2f6769746875622f6a61766167756964652f6a6176612f636f6e63757272656e742f7265656e7472616e747265616477726974656c6f636b2d636c6173732d6469616772616d2e706e67) + +`ReentrantReadWriteLock` 也支持公平锁和非公平锁,默认使用非公平锁,可以通过构造器来显示的指定。 + +``` +// 传入一个 boolean 值,true 时为公平锁,false 时为非公平锁 +public ReentrantReadWriteLock(boolean fair) { + sync = fair ? new FairSync(): new NonfairSync(); + readerLock = new ReadLock(this); + writerLock = new WriteLock(this); +} +``` + +### ReentrantReadWriteLock 适合什么场景? + +由于 `ReentrantReadWriteLock` 既可以保证多个线程同时读的效率,同时又可以保证有写入操作时的线程安全。因此,在读多写少的情况下,使用 `ReentrantReadWriteLock` 能够明显提升系统性能。 + +### 共享锁和独占锁有什么区别? + +- **共享锁**:一把锁可以被多个线程同时获得。 +- **独占锁**:一把锁只能被一个线程获得。 + +### 线程持有读锁还能获取写锁吗? + +- 在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。 +- 在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)。 + +读写锁的源码分析,推荐阅读 [聊聊 Java 的几把 JVM 级锁 - 阿里巴巴中间件](https://mp.weixin.qq.com/s/h3VIUyH9L0v14MrQJiiDbw) 这篇文章,写的很不错。 + +### 读锁为什么不能升级为写锁? + +写锁可以降级为读锁,但是读锁却不能升级为写锁。这是因为读锁升级为写锁会引起线程的争夺,毕竟写锁属于是独占锁,这样的话,会影响性能。 + +另外,还可能会有死锁问题发生。举个例子:假设两个线程的读锁都想升级写锁,则需要对方都释放自己锁,而双方都不释放,就会产生死锁。 + +## StampedLock + +`StampedLock` 面试中问的比较少,不是很重要,简单了解即可。 + +### StampedLock 是什么? + +`StampedLock` 是 JDK 1.8 引入的性能更好的读写锁,不可重入且不支持条件变量 `Condition`。 + +不同于一般的 `Lock` 类,`StampedLock` 并不是直接实现 `Lock` 或 `ReadWriteLock` 接口,而是基于 **CLH 锁** 独立实现的(AQS 也是基于这玩意)。 + +``` +public class StampedLock implements java.io.Serializable { +} +``` + +`StampedLock` 提供了三种模式的读写控制模式:读锁、写锁和乐观读。 + +- **写锁**:独占锁,一把锁只能被一个线程获得。当一个线程获取写锁后,其他请求读锁和写锁的线程必须等待。类似于 `ReentrantReadWriteLock` 的写锁,不过这里的写锁是不可重入的。 +- **读锁** (悲观读):共享锁,没有线程获取写锁的情况下,多个线程可以同时持有读锁。如果己经有线程持有写锁,则其他线程请求获取该读锁会被阻塞。类似于 `ReentrantReadWriteLock` 的读锁,不过这里的读锁是不可重入的。 +- **乐观读**:允许多个线程获取乐观读以及读锁。同时允许一个写线程获取写锁。 + +另外,`StampedLock` 还支持这三种锁在一定条件下进行相互转换 。 + +``` +long tryConvertToWriteLock(long stamp){} +long tryConvertToReadLock(long stamp){} +long tryConvertToOptimisticRead(long stamp){} +``` + +`StampedLock` 在获取锁的时候会返回一个 long 型的数据戳,该数据戳用于稍后的锁释放参数,如果返回的数据戳为 0 则表示锁获取失败。当前线程持有了锁再次获取锁还是会返回一个新的数据戳,这也是 `StampedLock` 不可重入的原因。 + +``` +// 写锁 +public long writeLock() { + long s, next; // bypass acquireWrite in fully unlocked case only + return ((((s = state) & ABITS) == 0L && + U.compareAndSwapLong(this, STATE, s, next = s + WBIT)) ? + next : acquireWrite(false, 0L)); +} +// 读锁 +public long readLock() { + long s = state, next; // bypass acquireRead on common uncontended case + return ((whead == wtail && (s & ABITS) < RFULL && + U.compareAndSwapLong(this, STATE, s, next = s + RUNIT)) ? + next : acquireRead(false, 0L)); +} +// 乐观读 +public long tryOptimisticRead() { + long s; + return (((s = state) & WBIT)== 0L) ? (s & SBITS) : 0L; +} +``` + +### StampedLock 的性能为什么更好? + +相比于传统读写锁多出来的乐观读是 `StampedLock` 比 `ReadWriteLock` 性能更好的关键原因。`StampedLock` 的乐观读允许一个写线程获取写锁,所以不会导致所有写线程阻塞,也就是当读多写少的时候,写线程有机会获取写锁,减少了线程饥饿的问题,吞吐量大大提高。 + +### StampedLock 适合什么场景? + +和 `ReentrantReadWriteLock` 一样,`StampedLock` 同样适合读多写少的业务场景,可以作为 `ReentrantReadWriteLock` 的替代品,性能更好。 + +不过,需要注意的是 `StampedLock` 不可重入,不支持条件变量 `Condition`,对中断操作支持也不友好(使用不当容易导致 CPU 飙升)。如果你需要用到 `ReentrantLock` 的一些高级性能,就不太建议使用 `StampedLock` 了。 + +另外,`StampedLock` 性能虽好,但使用起来相对比较麻烦,一旦使用不当,就会出现生产问题。强烈建议你在使用 `StampedLock` 之前,看看 [StampedLock 官方文档中的案例](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html)。 + +### StampedLock 的底层原理了解吗? + +`StampedLock` 不是直接实现 `Lock` 或 `ReadWriteLock` 接口,而是基于 **CLH 锁** 实现的(AQS 也是基于这玩意),CLH 锁是对自旋锁的一种改良,是一种隐式的链表队列。`StampedLock` 通过 CLH 队列进行线程的管理,通过同步状态值 `state` 来表示锁的状态和类型。 + +`StampedLock` 的原理和 AQS 原理比较类似,这里就不详细介绍了,感兴趣的可以看看下面这两篇文章: + +- [AQS 详解](https://javaguide.cn/java/concurrent/aqs.html) +- [StampedLock 底层原理分析](https://segmentfault.com/a/1190000015808032) + +如果你只是准备面试的话,建议多花点精力搞懂 AQS 原理即可,`StampedLock` 底层原理在面试中遇到的概率非常小。 + +## Atomic 原子类 + +Atomic 原子类部分的内容我单独写了一篇文章来总结:[Atomic 原子类总结](https://github.com/Snailclimb/JavaGuide/blob/main/docs/java/concurrent/atomic-classes.md) 。 + + + +## 死锁(Deadlock) + +### 什么是死锁 + +多个线程互相等待对方释放锁。 + +死锁是当线程进入无限期等待状态时发生的情况,因为所请求的锁被另一个线程持有,而另一个线程又等待第一个线程持有的另一个锁。 + +

+ +

+ +### 避免死锁 + +(1)按序加锁 + +当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。 + +如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。 + +按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。 + +(2)超时释放锁 + +另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。 + +(3)死锁检测 + +死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。 + +每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph 等等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。 + +当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。 + +如果检测出死锁,有两种处理手段: + +- 释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。 +- 一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。 \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225\344\270\200.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225\344\270\200.md" new file mode 100644 index 0000000000..4de71c124d --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/Java\350\231\232\346\213\237\346\234\272\351\235\242\350\257\225\344\270\200.md" @@ -0,0 +1,120 @@ +--- +title: Java 虚拟机面试一 +date: 2024-07-03 07:44:02 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 + - 并发 +permalink: /pages/46c1e340/ +--- + +# Java 虚拟机面试一 + +## 引用类型 + +### Java 支持哪些引用类型?分别用于什么场景? + +无论是通过引用计算算法判断对象的引用数量,还是通过可达性分析算法判断对象的引用链是否可达,判定对象是否可被回收都与引用有关。 + +Java 具有四种强度不同的引用类型: + +- 强引用(Strong Reference) +- 软引用(Soft Reference) +- 弱引用(Weak Reference) +- 虚引用 + +**(1)强引用** + +**被强引用(Strong Reference)关联的对象不会被垃圾收集器回收。** + +使用 `new` 一个新对象的方式来创建强引用。 + +```java +Object obj = new Object(); +``` + +**(2)软引用** + +**被软引用(Soft Reference)关联的对象,只有在 JVM 内存不够的情况下才会被回收。**JVM 会确保在抛出 `OutOfMemoryError` 之前,清理软引用指向的对象。软引用通常用来实现内存敏感的缓存,如果还有空闲内存,就可以暂时保留缓存,当内存不足时清理掉,这样就保证了使用缓存的同时,不会耗尽内存。 + +使用 `SoftReference` 类来创建软引用。 + +```java +Object obj = new Object(); +SoftReference sf = new SoftReference(obj); +obj = null; // 使对象只被软引用关联 +``` + +**(3)弱引用** + +**被弱引用(Weak Reference)关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。** + +使用 `WeakReference` 类来实现弱引用。 + +```java +Object obj = new Object(); +WeakReference wf = new WeakReference(obj); +obj = null; +``` + +`WeakHashMap` 的 `Entry` 继承自 `WeakReference`,主要用来实现缓存。 + +```java +private static class Entry extends WeakReference implements Map.Entry +``` + +Tomcat 中的 `ConcurrentCache` 就使用了 `WeakHashMap` 来实现缓存功能。`ConcurrentCache` 采取的是分代缓存,经常使用的对象放入 eden 中,而不常用的对象放入 longterm。eden 使用 `ConcurrentHashMap` 实现,longterm 使用 `WeakHashMap`,保证了不常使用的对象容易被回收。 + +```java +public final class ConcurrentCache { + + private final int size; + + private final Map eden; + + private final Map longterm; + + public ConcurrentCache(int size) { + this.size = size; + this.eden = new ConcurrentHashMap<>(size); + this.longterm = new WeakHashMap<>(size); + } + + public V get(K k) { + V v = this.eden.get(k); + if (v == null) { + v = this.longterm.get(k); + if (v != null) + this.eden.put(k, v); + } + return v; + } + + public void put(K k, V v) { + if (this.eden.size() >= size) { + this.longterm.putAll(this.eden); + this.eden.clear(); + } + this.eden.put(k, v); + } +} +``` + +**(4)虚引用** + +又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。 + +**为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。** + +使用 `PhantomReference` 来实现虚引用。 + +```java +Object obj = new Object(); +PhantomReference pf = new PhantomReference(obj); +obj = null; +``` \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/README.md" "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/README.md" new file mode 100644 index 0000000000..3f3472428b --- /dev/null +++ "b/source/_posts/01.Java/01.JavaCore/99.\351\235\242\350\257\225/README.md" @@ -0,0 +1,35 @@ +--- +title: Java 面试 +date: 2020-06-04 13:51:00 +categories: + - Java + - JavaCore + - 面试 +tags: + - Java + - JavaSE + - 面试 +permalink: /pages/1b5f74fb/ +hidden: true +index: false +dir: + order: 99 + link: true +--- + +# Java 面试 + +## 📖 内容 + +## 📚 资料 + +- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) +- [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) +- [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) +- [从表到里学习 JVM 实现](https://www.douban.com/doulist/2545443/) + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git a/source/_posts/01.Java/01.JavaCore/README.md b/source/_posts/01.Java/01.JavaCore/README.md new file mode 100644 index 0000000000..3f358cd739 --- /dev/null +++ b/source/_posts/01.Java/01.JavaCore/README.md @@ -0,0 +1,122 @@ +--- +title: JavaCore +date: 2022-05-06 09:19:33 +categories: + - Java + - JavaCore +tags: + - Java + - JavaCore +permalink: /pages/bc8e1129/ +hidden: true +index: false +--- + +# JavaCore + +## 📖 内容 + +### [Java 基础特性](01.基础特性) + +- [Java 基础语法特性](01.基础特性/Java基础语法.md) +- [Java 基本数据类型](01.基础特性/Java基本数据类型.md) +- [Java 面向对象](01.基础特性/Java面向对象.md) +- [Java 方法](01.基础特性/Java方法.md) +- [Java 数组](01.基础特性/Java数组.md) +- [Java 枚举](01.基础特性/Java枚举.md) +- [Java 控制语句](01.基础特性/Java控制语句.md) +- [Java 异常](01.基础特性/Java异常.md) +- [Java 泛型](01.基础特性/Java泛型.md) +- [Java 反射](01.基础特性/Java反射.md) +- [Java 注解](01.基础特性/Java注解.md) +- [Java String 类型](01.基础特性/JavaString类型.md) + +### [Java 高级特性](02.高级特性) + +- [Java 正则](02.高级特性/Java正则.md) - 关键词:Pattern、Matcher、捕获与非捕获、反向引用、零宽断言、贪婪与懒惰、元字符、DFA、NFA +- [Java 编码和加密](02.高级特性/Java编码和加密.md) - 关键词:Base64、消息摘要、数字签名、对称加密、非对称加密、MD5、SHA、HMAC、AES、DES、DESede、RSA +- [Java 国际化](02.高级特性/Java国际化.md) - 关键词:Locale、ResourceBundle、NumberFormat、DateFormat、MessageFormat +- [Java JDK8](02.高级特性/JDK8特性.md) - 关键词:Stream、lambda、Optional、@FunctionalInterface +- [Java SPI](02.高级特性/JavaSPI.md) - 关键词:SPI、ClassLoader +- [JavaAgent](02.高级特性/JavaAgent.md) + +### [Java 容器](03.容器) + +- [Java 容器简介](03.容器/Java容器简介.md) - 关键词:泛型、Iterable、Iterator、Comparable、Comparator、Cloneable、fail-fast +- [Java 容器之 List](03.容器/Java容器之List.md) - 关键词:List、ArrayList、LinkedList +- [Java 容器之 Map](03.容器/Java容器之Map.md) - 关键词:Map、HashMap、TreeMap、LinkedHashMap、WeakHashMap +- [Java 容器之 Set](03.容器/Java容器之Set.md) - 关键词:Set、HashSet、TreeSet、LinkedHashSet、EmumSet +- [Java 容器之 Queue](03.容器/Java容器之Queue.md) - 关键词:Queue、Deque、ArrayDeque、LinkedList、PriorityQueue +- [Java 容器之 Stream](03.容器/Java容器之Stream.md) + +### [Java IO](04.IO) + +- [Java I/O 之 简介](04.IO/JavaIO简介.md) - 关键词:BIO、NIO、AIO +- [Java I/O 之 BIO](04.IO/JavaIO之BIO.md) - 关键词:BIO、InputStream、OutputStream、Reader、Writer、File、Socket、ServerSocket +- [Java I/O 之 NIO](04.IO/JavaIO之NIO.md) - 关键词:NIO、Channel、Buffer、Selector、多路复用 +- [Java I/O 之序列化](04.IO/JavaIO之序列化.md) - 关键词:Serializable、serialVersionUID、transient、Externalizable + +### [Java 并发](05.并发) + +- [Java 并发简介](05.并发/Java并发简介.md) - 关键词:并发、线程、安全性、活跃性、性能、死锁、活锁 +- [Java 并发之内存模型](05.并发/Java并发之内存模型.md) - 关键词:JMM、Happens-Before、内存屏障、volatile、synchronized、final、指令重排序 +- [Java 并发之线程](05.并发/Java并发之线程.md) - 关键词:Thread、Runnable、Callable、Future、FutureTask、线程生命周期 +- [Java 并发之锁](05.并发/Java并发之锁.md) - 关键词:锁、Lock、Condition、ReentrantLock、ReentrantReadWriteLock、StampedLock +- [Java 并发之无锁](05.并发/Java并发之无锁.md) - 关键词:CAS、ThreadLocal、Immutability、Copy-on-Write +- [Java 并发之 AQS](05.并发/Java并发之AQS.md) - 关键词:AQS、独占锁、共享锁 +- [Java 并发之容器](05.并发/Java并发之容器.md) - 关键词:ConcurrentHashMap、CopyOnWriteArrayList +- [Java 并发之线程池](05.并发/Java并发之线程池.md) - 关键词:Executor、ExecutorService、ThreadPoolExecutor、Executors +- [Java 并发之同步工具](05.并发/Java并发之同步工具.md) - 关键词:Semaphore、CountDownLatch、CyclicBarrier +- [Java 并发之分工工具](05.并发/Java并发之分工工具.md) - 关键词:CompletableFuture、CompletionStage、ForkJoinPool + +### [Java 虚拟机](06.JVM) + +- [Java 虚拟机简介](06.JVM/Java虚拟机简介.md) +- [Java 虚拟机之内存区域](06.JVM/Java虚拟机之内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` +- [Java 虚拟机之垃圾收集](06.JVM/Java虚拟机之垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` +- [Java 虚拟机之字节码](06.JVM/Java虚拟机之字节码.md) - 关键词:`bytecode`、`asm`、`javassist` +- [Java 虚拟机之类加载](06.JVM/Java虚拟机之类加载.md) - 关键词:`ClassLoader`、`双亲委派` +- [Java 虚拟机之工具](06.JVM/Java虚拟机之工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo`、`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` +- [Java 虚拟机之故障处理](06.JVM/Java虚拟机之故障处理.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` +- [Java 虚拟机之调优](06.JVM/Java虚拟机之调优.md) - 关键词:`配置`、`调优` + +## 📚 资料 + +- Java 综合 + - [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) - 极客时间教程——基于 Java 生产环境的真实案例,讲解“避坑”的手段,很硬核 + - [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - 极客时间教程——覆盖 80% 以上 Java 应用调优场景 + - [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) - 极客时间教程——从面试官视角梳理如何解答常见 Java 面试问题 + - [CS-Notes](https://github.com/CyC2018/CS-Notes) - Github 上的 Java 基础级面试教程,行文清晰简洁 + - [JavaGuide](https://github.com/Snailclimb/JavaGuide) - Github 上的 Java 面试教程,Java 基础部分讲解较为细致 + - [advanced-java](https://github.com/doocs/advanced-java) - Github 上的 Java 面试教程,分布式部分从面试官视角讲解核心考察点 +- Java 基础 + - [《Java 编程思想》](https://book.douban.com/subject/2130190/) - Thinking in java,典中典!由于成书较早,部分内容已经多少有点过时 + - [《Java 核心技术 卷 I 开发基础》](https://book.douban.com/subject/35920145/) - 第 12 版,涵盖 Java 17 的新特性 + - [《Java 核心技术 卷 II 高级特性》](https://book.douban.com/subject/36337685/) - 第 12 版,涵盖 Java 17 的新特性 + - [《Head First Java》](https://book.douban.com/subject/2000732/) - 图文并茂,对新手非常友好的入门级教程 + - [《疯狂 Java 讲义》](https://book.douban.com/subject/3246499/) - 入门级教程 + - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) - 入门级在线教程 +- Java 并发 + - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - 深入浅出地介绍 Java 线程和并发 + - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) + - [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) - 极客时间教程——图文并茂,系统性讲解并发编程知识 + - [拉勾教育教程 - Java 并发编程 78 讲](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16) - 拉勾教育教程——针对并发场景问题,讲解的通俗易懂 +- Java 虚拟机 + - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) - 第 3 版,国内最好的 JVM 书籍 + - [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - 极客时间教程 +- Java IO + - [《Netty 实战》](https://book.douban.com/subject/27038538/) +- Java 编程规范 + - [《Effective Java》](https://book.douban.com/subject/36818907/) - 第 3 版,涵盖 Java 9 的新特性 + - [《阿里巴巴 Java 开发手册》](https://github.com/alibaba/p3c/blob/master/阿里巴巴Java开发手册(详尽版).pdf) + - [Google Java 编程指南](https://google.github.io/styleguide/javaguide.html) +- 其他 + - [《Head First 设计模式》](https://book.douban.com/subject/2243615/) + - [《Java 网络编程》](https://book.douban.com/subject/1438754/) + - [《Java 加密与解密的艺术》](https://book.douban.com/subject/25861566/) + - [java-design-patterns](https://github.com/iluwatar/java-design-patterns) - Github 上的 Java 版设计模式教程 + - [Java](https://github.com/TheAlgorithms/Java) - Github 上的 Java 算法教程 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/00.Java\345\274\200\345\217\221\347\216\257\345\242\203.md" "b/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/00.Java\345\274\200\345\217\221\347\216\257\345\242\203.md" deleted file mode 100644 index 0bff5680ac..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/00.Java\345\274\200\345\217\221\347\216\257\345\242\203.md" +++ /dev/null @@ -1,90 +0,0 @@ ---- -title: Java 开发环境 -date: 2018-08-29 17:28:34 -order: 00 -categories: - - Java - - JavaSE - - 基础特性 -tags: - - Java - - JavaSE -permalink: /pages/7daf0d/ ---- - -# Java 开发环境 - -> 📌 **关键词:** JAVA_HOME、CLASSPATH、Path、环境变量、IDE - -## 下载 - -进入 [JDK 官方下载地址](http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html) ,根据自己的环境选择下载所需版本。 - -## 安装 - -windows 环境的 jdk 包是 exe 安装文件,启动后根据安装向导安装即可。 - -Linux 环境的 jdk 包,解压到本地即可。 - -## 环境变量 - -### Windows - -计算机 > 属性 > 高级系统设置 > 环境变量 - -添加以下环境变量: - -`JAVA_HOME`:`C:\Program Files (x86)\Java\jdk1.8.0_91` (根据自己的实际路径配置) - -`CLASSPATH`:`.;%JAVA_HOME%\lib\dt.jar;%JAVA_HOME%\lib\tools.jar;` (注意前面有个".") - -`Path`:`%JAVA_HOME%\bin;%JAVA_HOME%\jre\bin;` - -### Linux - -执行 `vi /etc/profile` ,编辑环境变量文件 - -添加两行: - -```shell -export JAVA_HOME=path/to/java -export PATH=JAVA_HOME/bin:JAVA_HOME/jre/bin: -``` - -执行 `source /etc/profile` ,立即生效。 - -## 测试安装成功 - -执行命令 `java -version` ,如果安装成功,会打印当前 java 的版本信息。 - -## 开发工具 - -工欲善其事,必先利其器。编写 Java 程序,当然有必要选择一个合适的 IDE。 - -IDE(Integrated Development Environment,即集成开发环境)是用于提供程序开发环境的应用程序,一般包括代码编辑器、编译器、调试器和图形用户界面等工具。 - -常见的 Java IDE 如下: - -- Eclipse - 一个开放源代码的、基于 Java 的可扩展开发平台。 -- NetBeans - 开放源码的 Java 集成开发环境,适用于各种客户机和 Web 应用。 -- IntelliJ IDEA - 在代码自动提示、代码分析等方面的具有很好的功能。 -- MyEclipse - 由 Genuitec 公司开发的一款商业化软件,是应用比较广泛的 Java 应用程序集成开发环境。 -- EditPlus - 如果正确配置 Java 的编译器“Javac”以及解释器“Java”后,可直接使用 EditPlus 编译执行 Java 程序。 - -## 第一个程序:Hello World - -添加 HelloWorld.java 文件,内容如下: - -```java -public class HelloWorld { - public static void main(String[] args) { - System.out.println("Hello World"); - } -} -``` - -执行后,控制台输出: - -``` -Hello World -``` \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/41.Java\345\270\270\347\224\250\345\267\245\345\205\267\347\261\273.md" "b/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/41.Java\345\270\270\347\224\250\345\267\245\345\205\267\347\261\273.md" deleted file mode 100644 index f6b21af1e6..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/01.\345\237\272\347\241\200\347\211\271\346\200\247/41.Java\345\270\270\347\224\250\345\267\245\345\205\267\347\261\273.md" +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: Java 常用工具类 -date: 2019-12-16 16:59:15 -order: 41 -categories: - - Java - - JavaSE - - 基础特性 -tags: - - Java - - JavaSE - - 工具类 -permalink: /pages/71bfcd/ ---- - -# Java 常用工具类 - -> 并发、IO、容器的工具类不会在本文提及,后面会有专题一一道来。 - -## 字符串 - -### String - -### StringBuffer - -### StringBuilder - -## 日期时间 - -### Date - -### SimpleDateFormat - -### Calendar - -## 数学 - -### Number - -### Math - -## 参考资料 - -- [Java 编程思想](https://book.douban.com/subject/2130190/) -- [Java 核心技术(卷 1)](https://book.douban.com/subject/3146174/) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/99.Java\347\274\226\347\250\213\350\247\204\350\214\203.md" "b/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/99.Java\347\274\226\347\250\213\350\247\204\350\214\203.md" deleted file mode 100644 index 55e9628bb1..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/99.Java\347\274\226\347\250\213\350\247\204\350\214\203.md" +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Java 编程规范 -date: 2019-05-06 15:02:02 -order: 99 -categories: - - Java - - JavaSE - - 高级特性 -tags: - - Java - - JavaSE -permalink: /pages/d71f2c/ ---- - -# Java 编程规范 - -> 编程规范就是 Java 开发的最佳实践。帮助开发人员少走弯路。 - -## Effective Java - -- 第 2 章 创建、销毁对象 - - 第 1 条:考虑用静态工厂方法代替构造器 - - 第 2 条:遇到多个构造器参数时要考虑用构建器 - - 第 3 条:用私有构造器或者枚举类型强化 Singleton 属性 - - 第 4 条:通过私有构造器强化不可实例化的能力 - - 第 5 条:避免创建不必要的对象 - - 第 6 条:消除过期的对象引用 - - 第 7 条:避免使用终结方法 -- 第 3 章 对于所有对象都通用的方法 - - 第 8 条:覆盖 equals 时请遵守通用约定 - - 第 9 条:覆盖 equals 时总要覆盖 hashCode - - 第 10 条:始终要覆盖 toString - - 第 11 条:谨慎地覆盖 clone - - 第 12 条:考虑实现 Comparable 接口 -- 第 4 章 类和接口 - - 第 13 条:使类和成员的可访问性最小化 - - 第 14 条:在公有类中使用访问方法而非公有域 - - 第 15 条:使可变性最小化 - - 第 16 条:复合优先于继承 - - 第 17 条:要么为继承而设计,并提供文档说明,要么就禁止继承 - - 第 18 条:接口优于抽象类 - - 第 19 条:接口只用于定义类型 - - 第 20 条:类层次优于标签类 - - 第 21 条:用函数对象表示策略 - - 第 22 条:优先考虑静态成员类 -- 第 5 章 泛型 - - 第 23 条:请不要在新代码中使用原生态类型 - - 第 24 条:消除非受检警告 - - 第 25 条:列表优先于数组 - - 第 26 条:优先考虑泛型 - - 第 27 条:优先考虑泛型方法 - - 第 28 条:利用有限制通配符来提升 API 的灵活性 - - 第 29 条:优先考虑类型安全的异构容器 -- 第 6 章 枚举和注解 - - 第 30 条:用 enum 代替 int 常量 - - 第 31 条:用实例域代替序数 - - 第 32 条:用 EnumSet 代替位域 - - 第 33 条:用 EnumMap 代替序数索引 - - 第 34 条:用接口模拟可伸缩的枚举 - - 第 35 条:注解优先于命名模式 - - 第 36 条:坚持使用 Override 注解 - - 第 37 条:用标记接口定义类型 -- 第 7 章 方法 - - 第 38 条:检查参数的有效性 - - 第 39 条:必要时进行保护性拷贝 - - 第 40 条:谨慎设计方法签名 - - 第 41 条:慎用重载 - - 第 42 条:慎用可变参数 - - 第 43 条:返回零长度的数组或者集合,而不是:null - - 第 44 条:为所有导出的 API 元素编写文档注释 -- 第 8 章 通用程序设计 - - 第 45 条:将局部变量的作用域最小化 - - 第 46 条:for-each 循环优先于传统的 for 循环 - - 第 47 条:了解和使用类库 - - 第 48 条:如果需要精确的答案,请避免使用 float 和 double - - 第 49 条:基本类型优先于装箱基本类型 - - 第 50 条:如果其他类型更适合,则尽量避免使用字符串 - - 第 51 条:当心字符串连接的性能 - - 第 52 条:通过接口引用对象 - - 第 53 条:接口优先于反射机制 - - 第 54 条:谨慎地使用本地方法 - - 第 55 条:谨慎地进行优化 - - 第 56 条:遵守普遍接受的命名惯例 -- 第 9 章 异常 - - 第 57 条:只针对异常的情况才使用异常 - - 第 58 条:对可恢复的情况使用受检异常,对编程错误使用运行时异常 - - 第 59 条:避免不必要地使用受检的异常 - - 第 60 条:优先使用标准的异常 - - 第 61 条:抛出与抽象相对应的异常 - - 第 62 条:每个方法抛出的异常都要有文档 - - 第 63 条:在细节消息中包含能捕获失败的信息 - - 第 64 条:努力使失败保持原子性 - - 第 65 条:不要忽略异常 -- 第 10 章 并发 - - 第 66 条:同步访问共享的可变数据 - - 第 67 条:避免过度同步 - - 第 68 条:executor 和 task 优先干线程 - - 第 69 条:并发工具优先于 wait 和 notify - - 第 70 条:线程安全性的文档化 - - 第 71 条:慎用延迟初始化 - - 第 72 条:不要依赖于线程调度器 - - 第 73 条:避免使用线程组 -- 第 11 章 序列化 - - 第 74 条:谨慎地实现 Serializable 接口 - - 第 75 条:考虑使用自定义的序列化形式 - - 第 76 条:保护性地编写 readObject 方法 - - 第 77 条:对于实例控制,枚举类型优先于 readResolve - - 第 78 条:考虑用序列化代理代替序列化实例 - -## 资源 - -- [Effective Java](https://book.douban.com/subject/3360807/) -- [阿里巴巴 Java 开发手册](https://github.com/alibaba/p3c/blob/master/阿里巴巴Java开发手册(详尽版).pdf) -- [Google Java 编程指南](https://google.github.io/styleguide/javaguide.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/README.md" "b/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/README.md" deleted file mode 100644 index 00a1b36f49..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/02.\351\253\230\347\272\247\347\211\271\346\200\247/README.md" +++ /dev/null @@ -1,65 +0,0 @@ ---- -title: Java 高级特性 -date: 2020-06-04 13:51:01 -categories: - - Java - - JavaSE - - 高级特性 -tags: - - Java - - JavaSE -permalink: /pages/016137/ -hidden: true -index: false ---- - -# Java 高级特性 - -> Java 高级总结 Java 的一些高级特性。 - -## 📖 内容 - -- [Java 正则从入门到精通](01.Java正则.md) - 关键词:`Pattern`、`Matcher`、`捕获与非捕获`、`反向引用`、`零宽断言`、`贪婪与懒惰`、`元字符`、`DFA`、`NFA` -- [Java 编码和加密](02.Java编码和加密.md) - 关键词:`Base64`、`消息摘要`、`数字签名`、`对称加密`、`非对称加密`、`MD5`、`SHA`、`HMAC`、`AES`、`DES`、`DESede`、`RSA` -- [Java 国际化](03.Java国际化.md) - 关键词:`Locale`、`ResourceBundle`、`NumberFormat`、`DateFormat`、`MessageFormat` -- [Java JDK8](04.JDK8.md) - 关键词:`Stream`、`lambda`、`Optional`、`@FunctionalInterface` -- [Java SPI](05.JavaSPI.md) - 关键词:`SPI`、`ClassLoader` - -## 📚 资料 - -- **书籍** - - Java 四大名著 - - [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) - - [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) - - [《Java 核心技术 卷 II 高级特性》](https://book.douban.com/subject/27165931/) - - [《Effective Java》](https://book.douban.com/subject/30412517/) - - Java 并发 - - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) - - Java 虚拟机 - - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) - - Java 入门 - - [《O'Reilly:Head First Java》](https://book.douban.com/subject/2000732/) - - [《疯狂 Java 讲义》](https://book.douban.com/subject/3246499/) - - 其他 - - [《Head First 设计模式》](https://book.douban.com/subject/2243615/) - - [《Java 网络编程》](https://book.douban.com/subject/1438754/) - - [《Java 加密与解密的艺术》](https://book.douban.com/subject/25861566/) - - [《阿里巴巴 Java 开发手册》](https://book.douban.com/subject/27605355/) -- **教程、社区** - - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) - - [java-design-patterns](https://github.com/iluwatar/java-design-patterns) - - [Java](https://github.com/TheAlgorithms/Java) - - [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/82) - - [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) - - [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) - - [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - - [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) -- **面试** - - [CS-Notes](https://github.com/CyC2018/CS-Notes) - - [JavaGuide](https://github.com/Snailclimb/JavaGuide) - - [advanced-java](https://github.com/doocs/advanced-java) - -## 🚪 传送 - -◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/README.md" "b/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/README.md" deleted file mode 100644 index 11aa4da034..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/03.\345\256\271\345\231\250/README.md" +++ /dev/null @@ -1,58 +0,0 @@ ---- -title: Java 容器 -date: 2020-06-04 13:51:01 -categories: - - Java - - JavaSE - - 容器 -tags: - - Java - - JavaSE - - 容器 -permalink: /pages/9eb49b/ -hidden: true -index: false ---- - -# Java 容器 - -> Java 容器涉及许多数据结构知识点,所以设立专题进行总结。 - -## 📖 内容 - -- [Java 容器简介](01.Java容器简介.md) - 关键词:`Collection`、`泛型`、`Iterable`、`Iterator`、`Comparable`、`Comparator`、`Cloneable`、`fail-fast` -- [Java 容器之 List](02.Java容器之List.md) - 关键词:`List`、`ArrayList`、`LinkedList` -- [Java 容器之 Map](03.Java容器之Map.md) - 关键词:`Map`、`HashMap`、`TreeMap`、`LinkedHashMap`、`WeakHashMap` -- [Java 容器之 Set](04.Java容器之Set.md) - 关键词:`Set`、`HashSet`、`TreeSet`、`LinkedHashSet`、`EmumSet` -- [Java 容器之 Queue](05.Java容器之Queue.md) - 关键词:`Queue`、`Deque`、`ArrayDeque`、`LinkedList`、`PriorityQueue` -- [Java 容器之 Stream](06.Java容器之Stream.md) - -## 📚 资料 - -- **书籍** - - Java 四大名著 - - [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) - - [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) - - [《Java 核心技术 卷 II 高级特性》](https://book.douban.com/subject/27165931/) - - [《Effective Java》](https://book.douban.com/subject/30412517/) - - Java 入门 - - [《O'Reilly:Head First Java》](https://book.douban.com/subject/2000732/) - - [《Java 从入门到精通》](https://item.jd.com/12555860.html) - - [《疯狂 Java 讲义》](https://book.douban.com/subject/3246499/) -- **教程、社区** - - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) - - [java-design-patterns](https://github.com/iluwatar/java-design-patterns) - - [Java](https://github.com/TheAlgorithms/Java) - - [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/82) - - [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) - - [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) - - [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - - [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) -- **面试** - - [CS-Notes](https://github.com/CyC2018/CS-Notes) - - [JavaGuide](https://github.com/Snailclimb/JavaGuide) - - [advanced-java](https://github.com/doocs/advanced-java) - -## 🚪 传送 - -◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/04.IO/01.JavaIO\346\250\241\345\236\213.md" "b/source/_posts/01.Java/01.JavaSE/04.IO/01.JavaIO\346\250\241\345\236\213.md" deleted file mode 100644 index 8b28621582..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/04.IO/01.JavaIO\346\250\241\345\236\213.md" +++ /dev/null @@ -1,599 +0,0 @@ ---- -title: Java IO 模型 -date: 2020-11-21 16:36:40 -order: 01 -categories: - - Java - - JavaSE - - IO -tags: - - Java - - JavaSE - - IO -permalink: /pages/b165ad/ ---- - -# Java IO 模型 - -> 所谓的**I/O,就是计算机内存与外部设备之间拷贝数据的过程**。由于 CPU 访问内存的速度远远高于外部设备,因此 CPU 是先把外部设备的数据读到内存里,然后再进行处理。 -> -> **关键词:`InputStream`、`OutputStream`、`Reader`、`Writer`** - -## UNIX I/O 模型 - -UNIX 系统下的 I/O 模型有 5 种: - -- 同步阻塞 I/O -- 同步非阻塞 I/O -- I/O 多路复用 -- 信号驱动 I/O -- 异步 I/O - -如何去理解 UNIX I/O 模型,大致有以下两个维度: - -- 区分同步或异步(synchronous/asynchronous)。简单来说,同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系。 -- 区分阻塞与非阻塞(blocking/non-blocking)。在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如 ServerSocket 新连接建立完毕,或数据读取、写入操作完成;而非阻塞则是不管 IO 操作是否结束,直接返回,相应操作在后台继续处理。 - -不能一概而论认为同步或阻塞就是低效,具体还要看应用和系统特征。 - -对于一个网络 I/O 通信过程,比如网络数据读取,会涉及两个对象,一个是调用这个 I/O 操作的用户线程,另外一个就是操作系统内核。一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。 - -当用户线程发起 I/O 操作后,网络数据读取操作会经历两个步骤: - -- **用户线程等待内核将数据从网卡拷贝到内核空间。** -- **内核将数据从内核空间拷贝到用户空间。** - -各种 I/O 模型的区别就是:它们实现这两个步骤的方式是不一样的。 - -### 同步阻塞 I/O - -用户线程发起 read 调用后就阻塞了,让出 CPU。内核等待网卡数据到来,把数据从网卡拷贝到内核空间,接着把数据拷贝到用户空间,再把用户线程叫醒。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163321.jpg) - -### 同步非阻塞 I/O - -用户线程不断的发起 read 调用,数据没到内核空间时,每次都返回失败,直到数据到了内核空间,这一次 read 调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程叫醒。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163344.jpg) - -### I/O 多路复用 - -用户线程的读取操作分成两步了,线程先发起 select 调用,目的是问内核数据准备好了吗?等内核把数据准备好了,用户线程再发起 read 调用。在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的。那为什么叫 I/O 多路复用呢?因为一次 select 调用可以向内核查多个数据通道(Channel)的状态,所以叫多路复用。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163408.jpg) - -### 信号驱动 I/O - -首先开启 Socket 的信号驱动 I/O 功能,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个 SIGIO 信号,可以在信号处理函数中调用 I/O 操作函数处理数据。**信号驱动式 I/O 模型的优点是我们在数据报到达期间进程不会被阻塞,我们只要等待信号处理函数的通知即可** - -### 异步 I/O - -用户线程发起 read 调用的同时注册一个回调函数,read 立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20201121163428.jpg) - -## Java I/O 模型 - -### BIO - -> BIO(blocking IO) 即阻塞 IO。指的主要是传统的 `java.io` 包,它基于流模型实现。 - -#### BIO 简介 - -`java.io` 包提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。 - -很多时候,人们也把 java.net 下面提供的部分网络 API,比如 `Socket`、`ServerSocket`、`HttpURLConnection` 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。 - -BIO 的优点是代码比较简单、直观;缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。 - -#### BIO 的性能缺陷 - -**BIO 会阻塞进程,不适合高并发场景**。 - -采用 BIO 的服务端,通常由一个独立的 Acceptor 线程负责监听客户端连接。服务端一般在`while(true)` 循环中调用 `accept()` 方法等待客户端的连接请求,一旦接收到一个连接请求,就可以建立 Socket,并基于这个 Socket 进行读写操作。此时,不能再接收其他客户端连接请求,只能等待当前连接的操作执行完成。 - -如果要让 **BIO 通信模型** 能够同时处理多个客户端请求,就必须使用多线程(主要原因是`socket.accept()`、`socket.read()`、`socket.write()` 涉及的三个主要函数都是同步阻塞的),但会造成不必要的线程开销。不过可以通过 **线程池机制** 改善,线程池还可以让线程的创建和回收成本相对较低。 - -**即使可以用线程池略微优化,但是会消耗宝贵的线程资源,并且在百万级并发场景下也撑不住**。如果并发访问量增加会导致线程数急剧膨胀可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。 - -### NIO - -> NIO(non-blocking IO) 即非阻塞 IO。指的是 Java 1.4 中引入的 `java.nio` 包。 - -为了解决 BIO 的性能问题, Java 1.4 中引入的 `java.nio` 包。NIO 优化了内存复制以及阻塞导致的严重性能问题。 - -`java.nio` 包提供了 `Channel`、`Selector`、`Buffer` 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。 - -NIO 有哪些性能优化点呢? - -#### 使用缓冲区优化读写流 - -NIO 与传统 I/O 不同,它是基于块(Block)的,它以块为基本单位处理数据。在 NIO 中,最为重要的两个组件是缓冲区(`Buffer`)和通道(`Channel`)。 - -`Buffer` 是一块连续的内存块,是 NIO 读写数据的缓冲。`Buffer` 可以将文件一次性读入内存再做后续处理,而传统的方式是边读文件边处理数据。`Channel` 表示缓冲数据的源头或者目的地,它用于读取缓冲或者写入数据,是访问缓冲的接口。 - -#### 使用 DirectBuffer 减少内存复制 - -NIO 还提供了一个可以直接访问物理内存的类 `DirectBuffer`。普通的 `Buffer` 分配的是 JVM 堆内存,而 `DirectBuffer` 是直接分配物理内存。 - -数据要输出到外部设备,必须先从用户空间复制到内核空间,再复制到输出设备,而 `DirectBuffer` 则是直接将步骤简化为从内核空间复制到外部设备,减少了数据拷贝。 - -这里拓展一点,由于 `DirectBuffer` 申请的是非 JVM 的物理内存,所以创建和销毁的代价很高。`DirectBuffer` 申请的内存并不是直接由 JVM 负责垃圾回收,但在 `DirectBuffer` 包装类被回收时,会通过 Java 引用机制来释放该内存块。 - -#### 优化 I/O,避免阻塞 - -传统 I/O 的数据读写是在用户空间和内核空间来回复制,而内核空间的数据是通过操作系统层面的 I/O 接口从磁盘读取或写入。 - -NIO 的 `Channel` 有自己的处理器,可以完成内核空间和磁盘之间的 I/O 操作。在 NIO 中,我们读取和写入数据都要通过 `Channel`,由于 `Channel` 是双向的,所以读、写可以同时进行。 - -### AIO - -> AIO(Asynchronous IO) 即异步非阻塞 IO,指的是 Java 7 中,对 NIO 有了进一步的改进,也称为 NIO2,引入了异步非阻塞 IO 方式。 - -在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。 - -## 传统 IO 流 - -流从概念上来说是一个连续的数据流。当程序需要读数据的时候就需要使用输入流读取数据,当需要往外写数据的时候就需要输出流。 - -BIO 中操作的流主要有两大类,字节流和字符流,两类根据流的方向都可以分为输入流和输出流。 - -- **字节流** - - 输入字节流:`InputStream` - - 输出字节流:`OutputStream` -- **字符流** - - 输入字符流:`Reader` - - 输出字符流:`Writer` - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200219130627.png) - -### 字节流 - -字节流主要操作字节数据或二进制对象。 - -字节流有两个核心抽象类:`InputStream` 和 `OutputStream`。所有的字节流类都继承自这两个抽象类。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200219133627.png) - -#### 文件字节流 - -`FileOutputStream` 和 `FileInputStream` 提供了读写字节到文件的能力。 - -文件流操作一般步骤: - -1. 使用 `File` 类绑定一个文件。 -2. 把 `File` 对象绑定到流对象上。 -3. 进行读或写操作。 -4. 关闭流 - -`FileOutputStream` 和 `FileInputStream` 示例: - -```java -public class FileStreamDemo { - - private static final String FILEPATH = "temp.log"; - - public static void main(String[] args) throws Exception { - write(FILEPATH); - read(FILEPATH); - } - - public static void write(String filepath) throws IOException { - // 第1步、使用File类找到一个文件 - File f = new File(filepath); - - // 第2步、通过子类实例化父类对象 - OutputStream out = new FileOutputStream(f); - // 实例化时,默认为覆盖原文件内容方式;如果添加true参数,则变为对原文件追加内容的方式。 - // OutputStream out = new FileOutputStream(f, true); - - // 第3步、进行写操作 - String str = "Hello World\n"; - byte[] bytes = str.getBytes(); - out.write(bytes); - - // 第4步、关闭输出流 - out.close(); - } - - public static void read(String filepath) throws IOException { - // 第1步、使用File类找到一个文件 - File f = new File(filepath); - - // 第2步、通过子类实例化父类对象 - InputStream input = new FileInputStream(f); - - // 第3步、进行读操作 - // 有三种读取方式,体会其差异 - byte[] bytes = new byte[(int) f.length()]; - int len = input.read(bytes); // 读取内容 - System.out.println("读入数据的长度:" + len); - - // 第4步、关闭输入流 - input.close(); - System.out.println("内容为:\n" + new String(bytes)); - } - -} -``` - -#### 内存字节流 - -`ByteArrayInputStream` 和 `ByteArrayOutputStream` 是用来完成内存的输入和输出功能。 - -内存操作流一般在生成一些临时信息时才使用。 如果临时信息保存在文件中,还需要在有效期过后删除文件,这样比较麻烦。 - -`ByteArrayInputStream` 和 `ByteArrayOutputStream` 示例: - -```java -public class ByteArrayStreamDemo { - - public static void main(String[] args) { - String str = "HELLOWORLD"; // 定义一个字符串,全部由大写字母组成 - ByteArrayInputStream bis = new ByteArrayInputStream(str.getBytes()); - ByteArrayOutputStream bos = new ByteArrayOutputStream(); - // 准备从内存ByteArrayInputStream中读取内容 - int temp = 0; - while ((temp = bis.read()) != -1) { - char c = (char) temp; // 读取的数字变为字符 - bos.write(Character.toLowerCase(c)); // 将字符变为小写 - } - // 所有的数据就全部都在ByteArrayOutputStream中 - String newStr = bos.toString(); // 取出内容 - try { - bis.close(); - bos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - System.out.println(newStr); - } - -} -``` - -#### 管道流 - -管道流的主要作用是可以进行两个线程间的通信。 - -如果要进行管道通信,则必须把 `PipedOutputStream` 连接在 `PipedInputStream` 上。为此,`PipedOutputStream` 中提供了 `connect()` 方法。 - -```java -public class PipedStreamDemo { - - public static void main(String[] args) { - Send s = new Send(); - Receive r = new Receive(); - try { - s.getPos().connect(r.getPis()); // 连接管道 - } catch (IOException e) { - e.printStackTrace(); - } - new Thread(s).start(); // 启动线程 - new Thread(r).start(); // 启动线程 - } - - static class Send implements Runnable { - - private PipedOutputStream pos = null; - - Send() { - pos = new PipedOutputStream(); // 实例化输出流 - } - - @Override - public void run() { - String str = "Hello World!!!"; - try { - pos.write(str.getBytes()); - } catch (IOException e) { - e.printStackTrace(); - } - try { - pos.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - - /** - * 得到此线程的管道输出流 - */ - PipedOutputStream getPos() { - return pos; - } - - } - - static class Receive implements Runnable { - - private PipedInputStream pis = null; - - Receive() { - pis = new PipedInputStream(); - } - - @Override - public void run() { - byte[] b = new byte[1024]; - int len = 0; - try { - len = pis.read(b); - } catch (IOException e) { - e.printStackTrace(); - } - try { - pis.close(); - } catch (IOException e) { - e.printStackTrace(); - } - System.out.println("接收的内容为:" + new String(b, 0, len)); - } - - /** - * 得到此线程的管道输入流 - */ - PipedInputStream getPis() { - return pis; - } - - } - -} -``` - -#### 对象字节流 - -**ObjectInputStream 和 ObjectOutputStream 是对象输入输出流,一般用于对象序列化。** - -这里不展开叙述,想了解详细内容和示例可以参考:[Java 序列化](03.Java序列化.md) - -#### 数据操作流 - -数据操作流提供了格式化读入和输出数据的方法,分别为 `DataInputStream` 和 `DataOutputStream`。 - -`DataInputStream` 和 `DataOutputStream` 格式化读写数据示例: - -```java -public class DataStreamDemo { - - public static final String FILEPATH = "temp.log"; - - public static void main(String[] args) throws IOException { - write(FILEPATH); - read(FILEPATH); - } - - private static void write(String filepath) throws IOException { - // 1.使用 File 类绑定一个文件 - File f = new File(filepath); - - // 2.把 File 对象绑定到流对象上 - DataOutputStream dos = new DataOutputStream(new FileOutputStream(f)); - - // 3.进行读或写操作 - String[] names = { "衬衣", "手套", "围巾" }; - float[] prices = { 98.3f, 30.3f, 50.5f }; - int[] nums = { 3, 2, 1 }; - for (int i = 0; i < names.length; i++) { - dos.writeChars(names[i]); - dos.writeChar('\t'); - dos.writeFloat(prices[i]); - dos.writeChar('\t'); - dos.writeInt(nums[i]); - dos.writeChar('\n'); - } - - // 4.关闭流 - dos.close(); - } - - private static void read(String filepath) throws IOException { - // 1.使用 File 类绑定一个文件 - File f = new File(filepath); - - // 2.把 File 对象绑定到流对象上 - DataInputStream dis = new DataInputStream(new FileInputStream(f)); - - // 3.进行读或写操作 - String name = null; // 接收名称 - float price = 0.0f; // 接收价格 - int num = 0; // 接收数量 - char[] temp = null; // 接收商品名称 - int len = 0; // 保存读取数据的个数 - char c = 0; // '\u0000' - try { - while (true) { - temp = new char[200]; // 开辟空间 - len = 0; - while ((c = dis.readChar()) != '\t') { // 接收内容 - temp[len] = c; - len++; // 读取长度加1 - } - name = new String(temp, 0, len); // 将字符数组变为String - price = dis.readFloat(); // 读取价格 - dis.readChar(); // 读取\t - num = dis.readInt(); // 读取int - dis.readChar(); // 读取\n - System.out.printf("名称:%s;价格:%5.2f;数量:%d\n", name, price, num); - } - } catch (EOFException e) { - System.out.println("结束"); - } catch (IOException e) { - e.printStackTrace(); - } - - // 4.关闭流 - dis.close(); - } - -} -``` - -#### 合并流 - -合并流的主要功能是将多个 `InputStream` 合并为一个 `InputStream` 流。合并流的功能由 `SequenceInputStream` 完成。 - -```java -public class SequenceInputStreamDemo { - - public static void main(String[] args) throws Exception { - - InputStream is1 = new FileInputStream("temp1.log"); - InputStream is2 = new FileInputStream("temp2.log"); - SequenceInputStream sis = new SequenceInputStream(is1, is2); - - int temp = 0; // 接收内容 - OutputStream os = new FileOutputStream("temp3.logt"); - while ((temp = sis.read()) != -1) { // 循环输出 - os.write(temp); // 保存内容 - } - - sis.close(); // 关闭合并流 - is1.close(); // 关闭输入流1 - is2.close(); // 关闭输入流2 - os.close(); // 关闭输出流 - } - -} -``` - -### 字符流 - -字符流主要操作字符,一个字符等于两个字节。 - -字符流有两个核心类:`Reader` 类和 `Writer` 。所有的字符流类都继承自这两个抽象类。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200219133648.png) - -#### 文件字符流 - -文件字符流 `FileReader` 和 `FileWriter` 可以向文件读写文本数据。 - -`FileReader` 和 `FileWriter` 读写文件示例: - -```java -public class FileReadWriteDemo { - - private static final String FILEPATH = "temp.log"; - - public static void main(String[] args) throws IOException { - write(FILEPATH); - System.out.println("内容为:" + new String(read(FILEPATH))); - } - - public static void write(String filepath) throws IOException { - // 1.使用 File 类绑定一个文件 - File f = new File(filepath); - - // 2.把 File 对象绑定到流对象上 - Writer out = new FileWriter(f); - // Writer out = new FileWriter(f, true); // 追加内容方式 - - // 3.进行读或写操作 - String str = "Hello World!!!\r\n"; - out.write(str); - - // 4.关闭流 - // 字符流操作时使用了缓冲区,并在关闭字符流时会强制将缓冲区内容输出 - // 如果不关闭流,则缓冲区的内容是无法输出的 - // 如果想在不关闭流时,将缓冲区内容输出,可以使用 flush 强制清空缓冲区 - out.flush(); - out.close(); - } - - public static char[] read(String filepath) throws IOException { - // 1.使用 File 类绑定一个文件 - File f = new File(filepath); - - // 2.把 File 对象绑定到流对象上 - Reader input = new FileReader(f); - - // 3.进行读或写操作 - int temp = 0; // 接收每一个内容 - int len = 0; // 读取内容 - char[] c = new char[1024]; - while ((temp = input.read()) != -1) { - // 如果不是-1就表示还有内容,可以继续读取 - c[len] = (char) temp; - len++; - } - System.out.println("文件字符数为:" + len); - - // 4.关闭流 - input.close(); - - return c; - } - -} -``` - -#### 字节流转换字符流 - -我们可以在程序中通过 `InputStream` 和 `Reader` 从数据源中读取数据,然后也可以在程序中将数据通过 `OutputStream` 和 `Writer` 输出到目标媒介中 - -使用 `InputStreamReader` 可以将输入字节流转化为输入字符流;使用`OutputStreamWriter`可以将输出字节流转化为输出字符流。 - -`OutputStreamWriter` 示例: - -```java -public class OutputStreamWriterDemo { - - public static void main(String[] args) throws IOException { - File f = new File("temp.log"); - Writer out = new OutputStreamWriter(new FileOutputStream(f)); - out.write("hello world!!"); - out.close(); - } - -} -``` - -`InputStreamReader` 示例: - -```java -public class InputStreamReaderDemo { - - public static void main(String[] args) throws IOException { - File f = new File("temp.log"); - Reader reader = new InputStreamReader(new FileInputStream(f)); - char[] c = new char[1024]; - int len = reader.read(c); - reader.close(); - System.out.println(new String(c, 0, len)); - } - -} -``` - -### 字节流 vs. 字符流 - -相同点: - -字节流和字符流都有 `read()`、`write()`、`flush()`、`close()` 这样的方法,这决定了它们的操作方式近似。 - -不同点: - -- **数据类型** - - 字节流的数据是字节(二进制对象)。主要核心类是 `InputStream` 类和 `OutputStream` 类。 - - 字符流的数据是字符,一个字符等于两个字节。主要核心类是 `Reader` 类和 `Writer` 类。 -- **缓冲区** - - 字节流在操作时本身不会用到缓冲区(内存),是文件直接操作的。 - - 字符流在操作时是使用了缓冲区,通过缓冲区再操作文件。 - -选择: - -所有的文件在硬盘或传输时都是以字节方式保存的,例如图片,影音文件等都是按字节方式存储的。字符流无法读写这些文件。 - -所以,除了纯文本数据文件使用字符流以外,其他文件类型都应该使用字节流方式。 - -## 参考资料 - -- [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) -- [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) -- [《Java 从入门到精通》](https://item.jd.com/12555860.html) -- [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/100006701) -- [BIO,NIO,AIO 总结](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/BIO-NIO-AIO.md) -- [深入拆解 Tomcat & Jetty](https://time.geekbang.org/column/intro/100027701) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/04.IO/04.Java\347\275\221\347\273\234\347\274\226\347\250\213.md" "b/source/_posts/01.Java/01.JavaSE/04.IO/04.Java\347\275\221\347\273\234\347\274\226\347\250\213.md" deleted file mode 100644 index 7453b04638..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/04.IO/04.Java\347\275\221\347\273\234\347\274\226\347\250\213.md" +++ /dev/null @@ -1,238 +0,0 @@ ---- -title: Java 网络编程 -date: 2020-02-19 18:54:21 -order: 04 -categories: - - Java - - JavaSE - - IO -tags: - - Java - - JavaSE - - IO - - 网络 -permalink: /pages/e4c818/ ---- - -# Java 网络编程 - -> **_关键词:`Socket`、`ServerSocket`、`DatagramPacket`、`DatagramSocket`_** -> -> 网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。 -> -> `java.net` 包中提供了低层次的网络通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。 -> -> java.net 包中提供了两种常见的网络协议的支持: -> -> - **TCP** - TCP 是传输控制协议的缩写,它保障了两个应用程序之间的可靠通信。通常用于互联网协议,被称 TCP/ IP。 -> - **UDP** - UDP 是用户数据报协议的缩写,一个无连接的协议。提供了应用程序之间要发送的数据的数据包。 - -## Socket 和 ServerSocket - -套接字(Socket)使用 TCP 提供了两台计算机之间的通信机制。 客户端程序创建一个套接字,并尝试连接服务器的套接字。 - -**Java 通过 Socket 和 ServerSocket 实现对 TCP 的支持**。Java 中的 Socket 通信可以简单理解为:**`java.net.Socket` 代表客户端,`java.net.ServerSocket` 代表服务端**,二者可以建立连接,然后通信。 - -以下为 Socket 通信中建立建立的基本流程: - -- 服务器实例化一个 `ServerSocket` 对象,表示服务器绑定一个端口。 -- 服务器调用 `ServerSocket` 的 `accept()` 方法,该方法将一直等待,直到客户端连接到服务器的绑定端口(即监听端口)。 -- 服务器监听端口时,客户端实例化一个 `Socket` 对象,指定服务器名称和端口号来请求连接。 -- `Socket` 类的构造函数试图将客户端连接到指定的服务器和端口号。如果通信被建立,则在客户端创建一个 Socket 对象能够与服务器进行通信。 -- 在服务器端,`accept()` 方法返回服务器上一个新的 `Socket` 引用,该引用连接到客户端的 `Socket` 。 - -连接建立后,可以通过使用 IO 流进行通信。每一个 `Socket` 都有一个输出流和一个输入流。客户端的输出流连接到服务器端的输入流,而客户端的输入流连接到服务器端的输出流。 - -TCP 是一个双向的通信协议,因此数据可以通过两个数据流在同一时间发送,以下是一些类提供的一套完整的有用的方法来实现 sockets。 - -### ServerSocket - -服务器程序通过使用 `java.net.ServerSocket` 类以获取一个端口,并且监听客户端请求连接此端口的请求。 - -#### ServerSocket 构造方法 - -`ServerSocket` 有多个构造方法: - -| **方法** | **描述** | -| ---------------------------------------------------------- | ------------------------------------------------------------------- | -| `ServerSocket()` | 创建非绑定服务器套接字。 | -| `ServerSocket(int port)` | 创建绑定到特定端口的服务器套接字。 | -| `ServerSocket(int port, int backlog)` | 利用指定的 `backlog` 创建服务器套接字并将其绑定到指定的本地端口号。 | -| `ServerSocket(int port, int backlog, InetAddress address)` | 使用指定的端口、监听 `backlog` 和要绑定到的本地 IP 地址创建服务器。 | - -#### ServerSocket 常用方法 - -创建非绑定服务器套接字。 如果 `ServerSocket` 构造方法没有抛出异常,就意味着你的应用程序已经成功绑定到指定的端口,并且侦听客户端请求。 - -这里有一些 `ServerSocket` 类的常用方法: - -| **方法** | **描述** | -| -------------------------------------------- | ----------------------------------------------------- | -| `int getLocalPort()` | 返回此套接字在其上侦听的端口。 | -| `Socket accept()` | 监听并接受到此套接字的连接。 | -| `void setSoTimeout(int timeout)` | 通过指定超时值启用/禁用 `SO_TIMEOUT`,以毫秒为单位。 | -| `void bind(SocketAddress host, int backlog)` | 将 `ServerSocket` 绑定到特定地址(IP 地址和端口号)。 | - -### Socket - -`java.net.Socket` 类代表客户端和服务器都用来互相沟通的套接字。客户端要获取一个 `Socket` 对象通过实例化 ,而 服务器获得一个 `Socket` 对象则通过 `accept()` 方法 a 的返回值。 - -#### Socket 构造方法 - -`Socket` 类有 5 个构造方法: - -| **方法** | **描述** | -| ----------------------------------------------------------------------------- | -------------------------------------------------------- | -| `Socket()` | 通过系统默认类型的 `SocketImpl` 创建未连接套接字 | -| `Socket(String host, int port)` | 创建一个流套接字并将其连接到指定主机上的指定端口号。 | -| `Socket(InetAddress host, int port)` | 创建一个流套接字并将其连接到指定 IP 地址的指定端口号。 | -| `Socket(String host, int port, InetAddress localAddress, int localPort)` | 创建一个套接字并将其连接到指定远程主机上的指定远程端口。 | -| `Socket(InetAddress host, int port, InetAddress localAddress, int localPort)` | 创建一个套接字并将其连接到指定远程地址上的指定远程端口。 | - -当 Socket 构造方法返回,并没有简单的实例化了一个 Socket 对象,它实际上会尝试连接到指定的服务器和端口。 - -#### Socket 常用方法 - -下面列出了一些感兴趣的方法,注意客户端和服务器端都有一个 Socket 对象,所以无论客户端还是服务端都能够调用这些方法。 - -| **方法** | **描述** | -| ----------------------------------------------- | ----------------------------------------------------- | -| `void connect(SocketAddress host, int timeout)` | 将此套接字连接到服务器,并指定一个超时值。 | -| `InetAddress getInetAddress()` | 返回套接字连接的地址。 | -| `int getPort()` | 返回此套接字连接到的远程端口。 | -| `int getLocalPort()` | 返回此套接字绑定到的本地端口。 | -| `SocketAddress getRemoteSocketAddress()` | 返回此套接字连接的端点的地址,如果未连接则返回 null。 | -| `InputStream getInputStream()` | 返回此套接字的输入流。 | -| `OutputStream getOutputStream()` | 返回此套接字的输出流。 | -| `void close()` | 关闭此套接字。 | - -### Socket 通信示例 - -服务端示例: - -```java -public class HelloServer { - - public static void main(String[] args) throws Exception { - // Socket 服务端 - // 服务器在8888端口上监听 - ServerSocket server = new ServerSocket(8888); - System.out.println("服务器运行中,等待客户端连接。"); - // 得到连接,程序进入到阻塞状态 - Socket client = server.accept(); - // 打印流输出最方便 - PrintStream out = new PrintStream(client.getOutputStream()); - // 向客户端输出信息 - out.println("hello world"); - client.close(); - server.close(); - System.out.println("服务器已向客户端发送消息,退出。"); - } - -} -``` - -客户端示例: - -```java -public class HelloClient { - - public static void main(String[] args) throws Exception { - // Socket 客户端 - Socket client = new Socket("localhost", 8888); - InputStreamReader inputStreamReader = new InputStreamReader(client.getInputStream()); - // 一次性接收完成 - BufferedReader buf = new BufferedReader(inputStreamReader); - String str = buf.readLine(); - buf.close(); - client.close(); - System.out.println("客户端接收到服务器消息:" + str + ",退出"); - } - -} -``` - -## DatagramSocket 和 DatagramPacket - -Java 通过 `DatagramSocket` 和 `DatagramPacket` 实现对 UDP 协议的支持。 - -- `DatagramPacket`:数据包类 -- `DatagramSocket`:通信类 - -UDP 服务端示例: - -```java -public class UDPServer { - - public static void main(String[] args) throws Exception { // 所有异常抛出 - String str = "hello World!!!"; - DatagramSocket ds = new DatagramSocket(3000); // 服务端在3000端口上等待服务器发送信息 - DatagramPacket dp = - new DatagramPacket(str.getBytes(), str.length(), InetAddress.getByName("localhost"), 9000); // 所有的信息使用buf保存 - System.out.println("发送信息。"); - ds.send(dp); // 发送信息出去 - ds.close(); - } - -} -``` - -UDP 客户端示例: - -```java -public class UDPClient { - - public static void main(String[] args) throws Exception { // 所有异常抛出 - byte[] buf = new byte[1024]; // 开辟空间,以接收数据 - DatagramSocket ds = new DatagramSocket(9000); // 客户端在9000端口上等待服务器发送信息 - DatagramPacket dp = new DatagramPacket(buf, 1024); // 所有的信息使用buf保存 - ds.receive(dp); // 接收数据 - String str = new String(dp.getData(), 0, dp.getLength()) + "from " + dp.getAddress().getHostAddress() + ":" - + dp.getPort(); - System.out.println(str); // 输出内容 - } - -} -``` - -## InetAddress - -`InetAddress` 类表示互联网协议(IP)地址。 - -没有公有的构造函数,只能通过静态方法来创建实例。 - -```java -InetAddress.getByName(String host); -InetAddress.getByAddress(byte[] address); -``` - -## URL - -可以直接从 URL 中读取字节流数据。 - -```java -public static void main(String[] args) throws IOException { - - URL url = new URL("http://www.baidu.com"); - - /* 字节流 */ - InputStream is = url.openStream(); - - /* 字符流 */ - InputStreamReader isr = new InputStreamReader(is, "utf-8"); - - /* 提供缓存功能 */ - BufferedReader br = new BufferedReader(isr); - - String line; - while ((line = br.readLine()) != null) { - System.out.println(line); - } - - br.close(); -} -``` - -## 参考资料 - -- [Java 网络编程](https://www.runoob.com/java/java-networking.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/04.IO/05.JavaIO\345\267\245\345\205\267\347\261\273.md" "b/source/_posts/01.Java/01.JavaSE/04.IO/05.JavaIO\345\267\245\345\205\267\347\261\273.md" deleted file mode 100644 index b819f6fad3..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/04.IO/05.JavaIO\345\267\245\345\205\267\347\261\273.md" +++ /dev/null @@ -1,307 +0,0 @@ ---- -title: Java IO 工具类 -date: 2020-06-30 21:34:59 -order: 05 -categories: - - Java - - JavaSE - - IO -tags: - - Java - - JavaSE - - IO - - 工具类 -permalink: /pages/e25d81/ ---- - -# Java IO 工具类 - -> **_关键词:`File`、`RandomAccessFile`、`System`、`Scanner`_** -> -> 本文介绍 Java IO 的一些常见工具类的用法和特性。 - -## File - -`File` 类是 `java.io` 包中唯一对文件本身进行操作的类。它可以对文件、目录进行增删查操作。 - -### createNewFille - -**可以使用 `createNewFille()` 方法创建一个新文件**。 - -注: - -Windows 中使用反斜杠表示目录的分隔符 `\`。~~~~~~~~ - -Linux 中使用正斜杠表示目录的分隔符 `/`。 - -最好的做法是使用 `File.separator` 静态常量,可以根据所在操作系统选取对应的分隔符。 - -【示例】创建文件 - -```java -File f = new File(filename); -boolean flag = f.createNewFile(); -``` - -### mkdir - -**可以使用 `mkdir()` 来创建文件夹**,但是如果要创建的目录的父路径不存在,则无法创建成功。 - -如果要解决这个问题,可以使用 `mkdirs()`,当父路径不存在时,会连同上级目录都一并创建。 - -【示例】创建目录 - -```java -File f = new File(filename); -boolean flag = f.mkdir(); -``` - -### delete - -**可以使用 `delete()` 来删除文件或目录**。 - -需要注意的是,如果删除的是目录,且目录不为空,直接用 `delete()` 删除会失败。 - -【示例】删除文件或目录 - -```java -File f = new File(filename); -boolean flag = f.delete(); -``` - -### list 和 listFiles - -`File` 中给出了两种列出文件夹内容的方法: - -- **`list()`: 列出全部名称,返回一个字符串数组**。 -- **`listFiles()`: 列出完整的路径,返回一个 `File` 对象数组**。 - -`list()` 示例: - -```java -File f = new File(filename); -String str[] = f.list(); -``` - -`listFiles()` 示例: - -```java -File f = new File(filename); -File files[] = f.listFiles(); -``` - -## RandomAccessFile - -> 注:`RandomAccessFile` 类虽然可以实现对文件内容的读写操作,但是比较复杂。所以一般操作文件内容往往会使用字节流或字符流方式。 - -`RandomAccessFile` 类是随机读取类,它是一个完全独立的类。 - -它适用于由大小已知的记录组成的文件,所以我们可以使用 `seek()` 将记录从一处转移到另一处,然后读取或者修改记录。 - -文件中记录的大小不一定都相同,只要能够确定哪些记录有多大以及它们在文件中的位置即可。 - -### RandomAccessFile 写操作 - -当用 `rw` 方式声明 `RandomAccessFile` 对象时,如果要写入的文件不存在,系统将自行创建。 - -`r` 为只读;`w` 为只写;`rw` 为读写。 - -【示例】文件随机读写 - -```java -public class RandomAccessFileDemo01 { - - public static void main(String args[]) throws IOException { - File f = new File("d:" + File.separator + "test.txt"); // 指定要操作的文件 - RandomAccessFile rdf = null; // 声明RandomAccessFile类的对象 - rdf = new RandomAccessFile(f, "rw");// 读写模式,如果文件不存在,会自动创建 - String name = null; - int age = 0; - name = "zhangsan"; // 字符串长度为8 - age = 30; // 数字的长度为4 - rdf.writeBytes(name); // 将姓名写入文件之中 - rdf.writeInt(age); // 将年龄写入文件之中 - name = "lisi "; // 字符串长度为8 - age = 31; // 数字的长度为4 - rdf.writeBytes(name); // 将姓名写入文件之中 - rdf.writeInt(age); // 将年龄写入文件之中 - name = "wangwu "; // 字符串长度为8 - age = 32; // 数字的长度为4 - rdf.writeBytes(name); // 将姓名写入文件之中 - rdf.writeInt(age); // 将年龄写入文件之中 - rdf.close(); // 关闭 - } -} -``` - -### RandomAccessFile 读操作 - -读取是直接使用 `r` 的模式即可,以只读的方式打开文件。 - -读取时所有的字符串只能按照 byte 数组方式读取出来,而且长度必须和写入时的固定大小相匹配。 - -```java -public class RandomAccessFileDemo02 { - - public static void main(String args[]) throws IOException { - File f = new File("d:" + File.separator + "test.txt"); // 指定要操作的文件 - RandomAccessFile rdf = null; // 声明RandomAccessFile类的对象 - rdf = new RandomAccessFile(f, "r");// 以只读的方式打开文件 - String name = null; - int age = 0; - byte b[] = new byte[8]; // 开辟byte数组 - // 读取第二个人的信息,意味着要空出第一个人的信息 - rdf.skipBytes(12); // 跳过第一个人的信息 - for (int i = 0; i < b.length; i++) { - b[i] = rdf.readByte(); // 读取一个字节 - } - name = new String(b); // 将读取出来的byte数组变为字符串 - age = rdf.readInt(); // 读取数字 - System.out.println("第二个人的信息 --> 姓名:" + name + ";年龄:" + age); - // 读取第一个人的信息 - rdf.seek(0); // 指针回到文件的开头 - for (int i = 0; i < b.length; i++) { - b[i] = rdf.readByte(); // 读取一个字节 - } - name = new String(b); // 将读取出来的byte数组变为字符串 - age = rdf.readInt(); // 读取数字 - System.out.println("第一个人的信息 --> 姓名:" + name + ";年龄:" + age); - rdf.skipBytes(12); // 空出第二个人的信息 - for (int i = 0; i < b.length; i++) { - b[i] = rdf.readByte(); // 读取一个字节 - } - name = new String(b); // 将读取出来的byte数组变为字符串 - age = rdf.readInt(); // 读取数字 - System.out.println("第三个人的信息 --> 姓名:" + name + ";年龄:" + age); - rdf.close(); // 关闭 - } -} -``` - -## System - -`System` 类中提供了大量的静态方法,可以获取系统相关的信息或系统级操作,其中提供了三个常用于 IO 的静态成员: - -- `System.out` - 一个 PrintStream 流。System.out 一般会把你写到其中的数据输出到控制台上。System.out 通常仅用在类似命令行工具的控制台程序上。System.out 也经常用于打印程序的调试信息(尽管它可能并不是获取程序调试信息的最佳方式)。 -- `System.err` - 一个 PrintStream 流。System.err 与 System.out 的运行方式类似,但它更多的是用于打印错误文本。一些类似 Eclipse 的程序,为了让错误信息更加显眼,会将错误信息以红色文本的形式通过 System.err 输出到控制台上。 -- `System.in` - 一个典型的连接控制台程序和键盘输入的 InputStream 流。通常当数据通过命令行参数或者配置文件传递给命令行 Java 程序的时候,System.in 并不是很常用。图形界面程序通过界面传递参数给程序,这是一块单独的 Java IO 输入机制。 - -【示例】重定向 `System.out` 输出流 - -```java -import java.io.*; -public class SystemOutDemo { - - public static void main(String args[]) throws Exception { - OutputStream out = new FileOutputStream("d:\\test.txt"); - PrintStream ps = new PrintStream(out); - System.setOut(ps); - System.out.println("人生若只如初见,何事秋风悲画扇"); - ps.close(); - out.close(); - } -} -``` - -【示例】重定向 `System.err` 输出流 - -```java -public class SystemErrDemo { - - public static void main(String args[]) throws IOException { - OutputStream bos = new ByteArrayOutputStream(); // 实例化 - PrintStream ps = new PrintStream(bos); // 实例化 - System.setErr(ps); // 输出重定向 - System.err.print("此处有误"); - System.out.println(bos); // 输出内存中的数据 - } -} -``` - -【示例】`System.in` 接受控制台输入信息 - -```java -import java.io.*; -public class SystemInDemo { - - public static void main(String args[]) throws IOException { - InputStream input = System.in; - StringBuffer buf = new StringBuffer(); - System.out.print("请输入内容:"); - int temp = 0; - while ((temp = input.read()) != -1) { - char c = (char) temp; - if (c == '\n') { - break; - } - buf.append(c); - } - System.out.println("输入的内容为:" + buf); - input.close(); - } -} -``` - -## Scanner - -**`Scanner` 可以获取用户的输入,并对数据进行校验**。 - -【示例】校验输入数据是否格式正确 - -```java -import java.io.*; -public class ScannerDemo { - - public static void main(String args[]) { - Scanner scan = new Scanner(System.in); // 从键盘接收数据 - int i = 0; - float f = 0.0f; - System.out.print("输入整数:"); - if (scan.hasNextInt()) { // 判断输入的是否是整数 - i = scan.nextInt(); // 接收整数 - System.out.println("整数数据:" + i); - } else { - System.out.println("输入的不是整数!"); - } - - System.out.print("输入小数:"); - if (scan.hasNextFloat()) { // 判断输入的是否是小数 - f = scan.nextFloat(); // 接收小数 - System.out.println("小数数据:" + f); - } else { - System.out.println("输入的不是小数!"); - } - - Date date = null; - String str = null; - System.out.print("输入日期(yyyy-MM-dd):"); - if (scan.hasNext("^\\d{4}-\\d{2}-\\d{2}$")) { // 判断 - str = scan.next("^\\d{4}-\\d{2}-\\d{2}$"); // 接收 - try { - date = new SimpleDateFormat("yyyy-MM-dd").parse(str); - } catch (Exception e) {} - } else { - System.out.println("输入的日期格式错误!"); - } - System.out.println(date); - } -} -``` - -输出: - -``` -输入整数:20 -整数数据:20 -输入小数:3.2 -小数数据:3.2 -输入日期(yyyy-MM-dd):1988-13-1 -输入的日期格式错误! -null -``` - -## 参考资料 - -- [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) -- [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) -- [System 官方 API 手册](https://docs.oracle.com/javase/7/docs/api/java/lang/System.html) \ No newline at end of file diff --git a/source/_posts/01.Java/01.JavaSE/04.IO/README.md b/source/_posts/01.Java/01.JavaSE/04.IO/README.md deleted file mode 100644 index 762a012e80..0000000000 --- a/source/_posts/01.Java/01.JavaSE/04.IO/README.md +++ /dev/null @@ -1,54 +0,0 @@ ---- -title: Java IO -date: 2020-06-04 13:51:01 -categories: - - Java - - JavaSE - - IO -tags: - - Java - - JavaSE - - IO -permalink: /pages/e285c8/ -hidden: true -index: false ---- - -# Java IO - -## 📖 内容 - -### [Java IO 模型](01.JavaIO模型.md) - -> 关键词:`InputStream`、`OutputStream`、`Reader`、`Writer` - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630202823.png) - -### [Java NIO](02.JavaNIO.md) - -> 关键词:`Channel`、`Buffer`、`Selector`、`非阻塞`、`多路复用` - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630203739.png) - -### [Java 序列化](03.Java序列化.md) - -> 关键词:`Serializable`、`serialVersionUID`、`transient`、`Externalizable`、`writeObject`、`readObject` - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630204142.png) - -### [Java 网络编程](04.Java网络编程.md) - -> 关键词:`Socket`、`ServerSocket`、`DatagramPacket`、`DatagramSocket` - -### [Java IO 工具类](05.JavaIO工具类.md) - -> 关键词:`File`、`RandomAccessFile`、`System`、`Scanner` - -## 📚 资料 - -- [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) -- [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) - -## 🚪 传送 - -◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/02.Java\347\272\277\347\250\213\345\237\272\347\241\200.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/02.Java\347\272\277\347\250\213\345\237\272\347\241\200.md" deleted file mode 100644 index dd06476e3e..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/02.Java\347\272\277\347\250\213\345\237\272\347\241\200.md" +++ /dev/null @@ -1,791 +0,0 @@ ---- -title: Java线程基础 -date: 2019-12-24 23:52:25 -order: 02 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 - - 线程 -permalink: /pages/fee2cc/ ---- - -# Java 线程基础 - -> **关键词:`Thread`、`Runnable`、`Callable`、`Future`、`wait`、`notify`、`notifyAll`、`join`、`sleep`、`yeild`、`线程状态`、`线程通信`** - -## 线程简介 - -### 什么是进程 - -简言之,**进程可视为一个正在运行的程序**。它是系统运行程序的基本单位,因此进程是动态的。进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。 - -### 什么是线程 - -线程是操作系统进行调度的基本单位。线程也叫轻量级进程(Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。 - -### 进程和线程的区别 - -- 一个程序至少有一个进程,一个进程至少有一个线程。 -- 线程比进程划分更细,所以执行开销更小,并发性更高。 -- 进程是一个实体,拥有独立的资源;而同一个进程中的多个线程共享进程的资源。 - -## 创建线程 - -创建线程有三种方式: - -- 继承 `Thread` 类 -- 实现 `Runnable` 接口 -- 实现 `Callable` 接口 - -### Thread - -通过继承 `Thread` 类创建线程的步骤: - -1. 定义 `Thread` 类的子类,并覆写该类的 `run` 方法。`run` 方法的方法体就代表了线程要完成的任务,因此把 `run` 方法称为执行体。 -2. 创建 `Thread` 子类的实例,即创建了线程对象。 -3. 调用线程对象的 `start` 方法来启动该线程。 - -```java -public class ThreadDemo { - - public static void main(String[] args) { - // 实例化对象 - MyThread tA = new MyThread("Thread 线程-A"); - MyThread tB = new MyThread("Thread 线程-B"); - // 调用线程主体 - tA.start(); - tB.start(); - } - - static class MyThread extends Thread { - - private int ticket = 5; - - MyThread(String name) { - super(name); - } - - @Override - public void run() { - while (ticket > 0) { - System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); - ticket--; - } - } - - } - -} -``` - -### Runnable - -**实现 `Runnable` 接口优于继承 `Thread` 类**,因为: - -- Java 不支持多重继承,所有的类都只允许继承一个父类,但可以实现多个接口。如果继承了 `Thread` 类就无法继承其它类,这不利于扩展。 -- 类可能只要求可执行就行,继承整个 `Thread` 类开销过大。 - -通过实现 `Runnable` 接口创建线程的步骤: - -1. 定义 `Runnable` 接口的实现类,并覆写该接口的 `run` 方法。该 `run` 方法的方法体同样是该线程的线程执行体。 -2. 创建 `Runnable` 实现类的实例,并以此实例作为 `Thread` 的 target 来创建 `Thread` 对象,该 `Thread` 对象才是真正的线程对象。 -3. 调用线程对象的 `start` 方法来启动该线程。 - -```java -public class RunnableDemo { - - public static void main(String[] args) { - // 实例化对象 - Thread tA = new Thread(new MyThread(), "Runnable 线程-A"); - Thread tB = new Thread(new MyThread(), "Runnable 线程-B"); - // 调用线程主体 - tA.start(); - tB.start(); - } - - static class MyThread implements Runnable { - - private int ticket = 5; - - @Override - public void run() { - while (ticket > 0) { - System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); - ticket--; - } - } - - } - -} -``` - -### Callable、Future、FutureTask - -**继承 Thread 类和实现 Runnable 接口这两种创建线程的方式都没有返回值**。所以,线程执行完后,无法得到执行结果。但如果期望得到执行结果该怎么做? - -为了解决这个问题,Java 1.5 后,提供了 `Callable` 接口和 `Future` 接口,通过它们,可以在线程执行结束后,返回执行结果。 - -#### Callable - -Callable 接口只声明了一个方法,这个方法叫做 call(): - -```java -public interface Callable { - /** - * Computes a result, or throws an exception if unable to do so. - * - * @return computed result - * @throws Exception if unable to compute a result - */ - V call() throws Exception; -} -``` - -那么怎么使用 Callable 呢?一般情况下是配合 ExecutorService 来使用的,在 ExecutorService 接口中声明了若干个 submit 方法的重载版本: - -```java - Future submit(Callable task); - Future submit(Runnable task, T result); -Future submit(Runnable task); -``` - -第一个 submit 方法里面的参数类型就是 Callable。 - -#### Future - -Future 就是对于具体的 Callable 任务的执行结果进行取消、查询是否完成、获取结果。必要时可以通过 get 方法获取执行结果,该方法会阻塞直到任务返回结果。 - -```java -public interface Future { - boolean cancel(boolean mayInterruptIfRunning); - boolean isCancelled(); - boolean isDone(); - V get() throws InterruptedException, ExecutionException; - V get(long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException; -} -``` - -#### FutureTask - -FutureTask 类实现了 RunnableFuture 接口,RunnableFuture 继承了 Runnable 接口和 Future 接口。 - -所以,FutureTask 既可以作为 Runnable 被线程执行,又可以作为 Future 得到 Callable 的返回值。 - -```java -public class FutureTask implements RunnableFuture { - // ... - public FutureTask(Callable callable) {} - public FutureTask(Runnable runnable, V result) {} -} - -public interface RunnableFuture extends Runnable, Future { - void run(); -} -``` - -事实上,FutureTask 是 Future 接口的一个唯一实现类。 - -#### Callable + Future + FutureTask 示例 - -通过实现 `Callable` 接口创建线程的步骤: - -1. 创建 `Callable` 接口的实现类,并实现 `call` 方法。该 `call` 方法将作为线程执行体,并且有返回值。 -2. 创建 `Callable` 实现类的实例,使用 `FutureTask` 类来包装 `Callable` 对象,该 `FutureTask` 对象封装了该 `Callable` 对象的 `call` 方法的返回值。 -3. 使用 `FutureTask` 对象作为 `Thread` 对象的 target 创建并启动新线程。 -4. 调用 `FutureTask` 对象的 `get` 方法来获得线程执行结束后的返回值。 - -```java -public class CallableDemo { - - public static void main(String[] args) { - Callable callable = new MyThread(); - FutureTask future = new FutureTask<>(callable); - new Thread(future, "Callable 线程").start(); - try { - System.out.println("任务耗时:" + (future.get() / 1000000) + "毫秒"); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - } - - static class MyThread implements Callable { - - private int ticket = 10000; - - @Override - public Long call() { - long begin = System.nanoTime(); - while (ticket > 0) { - System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); - ticket--; - } - - long end = System.nanoTime(); - return (end - begin); - } - - } - -} -``` - -## 线程基本用法 - -线程(`Thread`)基本方法清单: - -| 方法 | 描述 | -| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `run` | 线程的执行实体。 | -| `start` | 线程的启动方法。 | -| `currentThread` | 返回对当前正在执行的线程对象的引用。 | -| `setName` | 设置线程名称。 | -| `getName` | 获取线程名称。 | -| `setPriority` | 设置线程优先级。Java 中的线程优先级的范围是 [1,10],一般来说,高优先级的线程在运行时会具有优先权。可以通过 `thread.setPriority(Thread.MAX_PRIORITY)` 的方式设置,默认优先级为 5。 | -| `getPriority` | 获取线程优先级。 | -| `setDaemon` | 设置线程为守护线程。 | -| `isDaemon` | 判断线程是否为守护线程。 | -| `isAlive` | 判断线程是否启动。 | -| `interrupt` | 中断另一个线程的运行状态。 | -| `interrupted` | 测试当前线程是否已被中断。通过此方法可以清除线程的中断状态。换句话说,如果要连续调用此方法两次,则第二次调用将返回 false(除非当前线程在第一次调用清除其中断状态之后且在第二次调用检查其状态之前再次中断)。 | -| `join` | 可以使一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。 | -| `Thread.sleep` | 静态方法。将当前正在执行的线程休眠。 | -| `Thread.yield` | 静态方法。将当前正在执行的线程暂停,让其他线程执行。 | - -### 线程休眠 - -**使用 `Thread.sleep` 方法可以使得当前正在执行的线程进入休眠状态。** - -使用 `Thread.sleep` 需要向其传入一个整数值,这个值表示线程将要休眠的毫秒数。 - -`Thread.sleep` 方法可能会抛出 `InterruptedException`,因为异常不能跨线程传播回 `main` 中,因此必须在本地进行处理。线程中抛出的其它异常也同样需要在本地进行处理。 - -```java -public class ThreadSleepDemo { - - public static void main(String[] args) { - new Thread(new MyThread("线程A", 500)).start(); - new Thread(new MyThread("线程B", 1000)).start(); - new Thread(new MyThread("线程C", 1500)).start(); - } - - static class MyThread implements Runnable { - - /** 线程名称 */ - private String name; - - /** 休眠时间 */ - private int time; - - private MyThread(String name, int time) { - this.name = name; - this.time = time; - } - - @Override - public void run() { - try { - // 休眠指定的时间 - Thread.sleep(this.time); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(this.name + "休眠" + this.time + "毫秒。"); - } - - } - -} -``` - -### 线程礼让 - -`Thread.yield` 方法的调用声明了当前线程已经完成了生命周期中最重要的部分,可以切换给其它线程来执行 。 - -该方法只是对线程调度器的一个建议,而且也只是建议具有相同优先级的其它线程可以运行。 - -```java -public class ThreadYieldDemo { - - public static void main(String[] args) { - MyThread t = new MyThread(); - new Thread(t, "线程A").start(); - new Thread(t, "线程B").start(); - } - - static class MyThread implements Runnable { - - @Override - public void run() { - for (int i = 0; i < 5; i++) { - try { - Thread.sleep(1000); - } catch (Exception e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread().getName() + "运行,i = " + i); - if (i == 2) { - System.out.print("线程礼让:"); - Thread.yield(); - } - } - } - } -} -``` - -### 终止线程 - -> **`Thread` 中的 `stop` 方法有缺陷,已废弃**。 -> -> 使用 `Thread.stop` 停止线程会导致它解锁所有已锁定的监视器(由于未经检查的 `ThreadDeath` 异常会在堆栈中传播,这是自然的结果)。 如果先前由这些监视器保护的任何对象处于不一致状态,则损坏的对象将对其他线程可见,从而可能导致任意行为。 -> -> stop() 方法会真的杀死线程,不给线程喘息的机会,如果线程持有 ReentrantLock 锁,被 stop() 的线程并不会自动调用 ReentrantLock 的 unlock() 去释放锁,那其他线程就再也没机会获得 ReentrantLock 锁,这实在是太危险了。所以该方法就不建议使用了,类似的方法还有 suspend() 和 resume() 方法,这两个方法同样也都不建议使用了,所以这里也就不多介绍了。`Thread.stop` 的许多用法应由仅修改某些变量以指示目标线程应停止运行的代码代替。 目标线程应定期检查此变量,如果该变量指示要停止运行,则应按有序方式从其运行方法返回。如果目标线程等待很长时间(例如,在条件变量上),则应使用中断方法来中断等待。 - -当一个线程运行时,另一个线程可以直接通过 `interrupt` 方法中断其运行状态。 - -```java -public class ThreadInterruptDemo { - - public static void main(String[] args) { - MyThread mt = new MyThread(); // 实例化Runnable子类对象 - Thread t = new Thread(mt, "线程"); // 实例化Thread对象 - t.start(); // 启动线程 - try { - Thread.sleep(2000); // 线程休眠2秒 - } catch (InterruptedException e) { - System.out.println("3、main线程休眠被终止"); - } - t.interrupt(); // 中断线程执行 - } - - static class MyThread implements Runnable { - - @Override - public void run() { - System.out.println("1、进入run()方法"); - try { - Thread.sleep(10000); // 线程休眠10秒 - System.out.println("2、已经完成了休眠"); - } catch (InterruptedException e) { - System.out.println("3、MyThread线程休眠被终止"); - return; // 返回调用处 - } - System.out.println("4、run()方法正常结束"); - } - } -} -``` - -如果一个线程的 `run` 方法执行一个无限循环,并且没有执行 `sleep` 等会抛出 `InterruptedException` 的操作,那么调用线程的 `interrupt` 方法就无法使线程提前结束。 - -但是调用 `interrupt` 方法会设置线程的中断标记,此时调用 `interrupted` 方法会返回 `true`。因此可以在循环体中使用 `interrupted` 方法来判断线程是否处于中断状态,从而提前结束线程。 - -安全地终止线程有两种方法: - -- 定义 `volatile` 标志位,在 `run` 方法中使用标志位控制线程终止 -- 使用 `interrupt` 方法和 `Thread.interrupted` 方法配合使用来控制线程终止 - -【示例】使用 `volatile` 标志位控制线程终止 - -```java -public class ThreadStopDemo2 { - - public static void main(String[] args) throws Exception { - MyTask task = new MyTask(); - Thread thread = new Thread(task, "MyTask"); - thread.start(); - TimeUnit.MILLISECONDS.sleep(50); - task.cancel(); - } - - private static class MyTask implements Runnable { - - private volatile boolean flag = true; - - private volatile long count = 0L; - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 线程启动"); - while (flag) { - System.out.println(count++); - } - System.out.println(Thread.currentThread().getName() + " 线程终止"); - } - - /** - * 通过 volatile 标志位来控制线程终止 - */ - public void cancel() { - flag = false; - } - - } - -} -``` - -【示例】使用 `interrupt` 方法和 `Thread.interrupted` 方法配合使用来控制线程终止 - -```java -public class ThreadStopDemo3 { - - public static void main(String[] args) throws Exception { - MyTask task = new MyTask(); - Thread thread = new Thread(task, "MyTask"); - thread.start(); - TimeUnit.MILLISECONDS.sleep(50); - thread.interrupt(); - } - - private static class MyTask implements Runnable { - - private volatile long count = 0L; - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 线程启动"); - // 通过 Thread.interrupted 和 interrupt 配合来控制线程终止 - while (!Thread.interrupted()) { - System.out.println(count++); - } - System.out.println(Thread.currentThread().getName() + " 线程终止"); - } - } -} -``` - -### 守护线程 - -什么是守护线程? - -- **守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程**。**当所有非守护线程结束时,程序也就终止,同时会杀死所有守护线程**。 -- 与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。 - -为什么需要守护线程? - -- 守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。 - -如何使用守护线程? - -- 可以使用 `isDaemon` 方法判断线程是否为守护线程。 -- 可以使用 `setDaemon` 方法设置线程为守护线程。 - - 正在运行的用户线程无法设置为守护线程,所以 `setDaemon` 必须在 `thread.start` 方法之前设置,否则会抛出 `llegalThreadStateException` 异常; - - 一个守护线程创建的子线程依然是守护线程。 - - 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。 - -```java -public class ThreadDaemonDemo { - - public static void main(String[] args) { - Thread t = new Thread(new MyThread(), "线程"); - t.setDaemon(true); // 此线程在后台运行 - System.out.println("线程 t 是否是守护进程:" + t.isDaemon()); - t.start(); // 启动线程 - } - - static class MyThread implements Runnable { - - @Override - public void run() { - while (true) { - System.out.println(Thread.currentThread().getName() + "在运行。"); - } - } - } -} -``` - -> 参考阅读:[Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) - -## 线程通信 - -> 当多个线程可以一起工作去解决某个问题时,如果某些部分必须在其它部分之前完成,那么就需要对线程进行协调。 - -### wait/notify/notifyAll - -- `wait` - `wait` 会自动释放当前线程占有的对象锁,并请求操作系统挂起当前线程,**让线程从 `Running` 状态转入 `Waiting` 状态**,等待 `notify` / `notifyAll` 来唤醒。如果没有释放锁,那么其它线程就无法进入对象的同步方法或者同步控制块中,那么就无法执行 `notify` 或者 `notifyAll` 来唤醒挂起的线程,造成死锁。 -- `notify` - 唤醒一个正在 `Waiting` 状态的线程,并让它拿到对象锁,具体唤醒哪一个线程由 JVM 控制 。 -- `notifyAll` - 唤醒所有正在 `Waiting` 状态的线程,接下来它们需要竞争对象锁。 - -> 注意: -> -> - **`wait`、`notify`、`notifyAll` 都是 `Object` 类中的方法**,而非 `Thread`。 -> - **`wait`、`notify`、`notifyAll` 只能用在 `synchronized` 方法或者 `synchronized` 代码块中使用,否则会在运行时抛出 `IllegalMonitorStateException`**。 -> -> 为什么 `wait`、`notify`、`notifyAll` 不定义在 `Thread` 中?为什么 `wait`、`notify`、`notifyAll` 要配合 `synchronized` 使用? -> -> 首先,需要了解几个基本知识点: -> -> - 每一个 Java 对象都有一个与之对应的 **监视器(monitor)** -> - 每一个监视器里面都有一个 **对象锁** 、一个 **等待队列**、一个 **同步队列** -> -> 了解了以上概念,我们回过头来理解前面两个问题。 -> -> 为什么这几个方法不定义在 `Thread` 中? -> -> 由于每个对象都拥有对象锁,让当前线程等待某个对象锁,自然应该基于这个对象(`Object`)来操作,而非使用当前线程(`Thread`)来操作。因为当前线程可能会等待多个线程的锁,如果基于线程(`Thread`)来操作,就非常复杂了。 -> -> 为什么 `wait`、`notify`、`notifyAll` 要配合 `synchronized` 使用? -> -> 如果调用某个对象的 `wait` 方法,当前线程必须拥有这个对象的对象锁,因此调用 `wait` 方法必须在 `synchronized` 方法和 `synchronized` 代码块中。 - -生产者、消费者模式是 `wait`、`notify`、`notifyAll` 的一个经典使用案例: - -```java -public class ThreadWaitNotifyDemo02 { - - private static final int QUEUE_SIZE = 10; - private static final PriorityQueue queue = new PriorityQueue<>(QUEUE_SIZE); - - public static void main(String[] args) { - new Producer("生产者A").start(); - new Producer("生产者B").start(); - new Consumer("消费者A").start(); - new Consumer("消费者B").start(); - } - - static class Consumer extends Thread { - - Consumer(String name) { - super(name); - } - - @Override - public void run() { - while (true) { - synchronized (queue) { - while (queue.size() == 0) { - try { - System.out.println("队列空,等待数据"); - queue.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - queue.notifyAll(); - } - } - queue.poll(); // 每次移走队首元素 - queue.notifyAll(); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread().getName() + " 从队列取走一个元素,队列当前有:" + queue.size() + "个元素"); - } - } - } - } - - static class Producer extends Thread { - - Producer(String name) { - super(name); - } - - @Override - public void run() { - while (true) { - synchronized (queue) { - while (queue.size() == QUEUE_SIZE) { - try { - System.out.println("队列满,等待有空余空间"); - queue.wait(); - } catch (InterruptedException e) { - e.printStackTrace(); - queue.notifyAll(); - } - } - queue.offer(1); // 每次插入一个元素 - queue.notifyAll(); - try { - Thread.sleep(500); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread().getName() + " 向队列取中插入一个元素,队列当前有:" + queue.size() + "个元素"); - } - } - } - } -} -``` - -### join - -在线程操作中,可以使用 `join` 方法让一个线程强制运行,线程强制运行期间,其他线程无法运行,必须等待此线程完成之后才可以继续执行。 - -```java -public class ThreadJoinDemo { - - public static void main(String[] args) { - MyThread mt = new MyThread(); // 实例化Runnable子类对象 - Thread t = new Thread(mt, "mythread"); // 实例化Thread对象 - t.start(); // 启动线程 - for (int i = 0; i < 50; i++) { - if (i > 10) { - try { - t.join(); // 线程强制运行 - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - System.out.println("Main 线程运行 --> " + i); - } - } - - static class MyThread implements Runnable { - - @Override - public void run() { - for (int i = 0; i < 50; i++) { - System.out.println(Thread.currentThread().getName() + " 运行,i = " + i); // 取得当前线程的名字 - } - } - } -} -``` - -### 管道 - -管道输入/输出流和普通的文件输入/输出流或者网络输入/输出流不同之处在于,它主要用于线程之间的数据传输,而传输的媒介为内存。 -管道输入/输出流主要包括了如下 4 种具体实现:`PipedOutputStream`、`PipedInputStream`、`PipedReader` 和 `PipedWriter`,前两种面向字节,而后两种面向字符。 - -```java -public class Piped { - - public static void main(String[] args) throws Exception { - PipedWriter out = new PipedWriter(); - PipedReader in = new PipedReader(); - // 将输出流和输入流进行连接,否则在使用时会抛出IOException - out.connect(in); - Thread printThread = new Thread(new Print(in), "PrintThread"); - printThread.start(); - int receive = 0; - try { - while ((receive = System.in.read()) != -1) { - out.write(receive); - } - } finally { - out.close(); - } - } - - static class Print implements Runnable { - - private PipedReader in; - - Print(PipedReader in) { - this.in = in; - } - - public void run() { - int receive = 0; - try { - while ((receive = in.read()) != -1) { - System.out.print((char) receive); - } - } catch (IOException e) { - e.printStackTrace(); - } - } - } -} -``` - -## 线程生命周期 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102103928.png) - -`java.lang.Thread.State` 中定义了 **6** 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。 - -以下是各状态的说明,以及状态间的联系: - -- **新建(New)** - 尚未调用 `start` 方法的线程处于此状态。此状态意味着:**创建的线程尚未启动**。 - -- **就绪(Runnable)** - 已经调用了 `start` 方法的线程处于此状态。此状态意味着:**线程已经在 JVM 中运行**。但是在操作系统层面,它可能处于运行状态,也可能等待资源调度(例如处理器资源),资源调度完成就进入运行状态。所以该状态的可运行是指可以被运行,具体有没有运行要看底层操作系统的资源调度。 - -- **阻塞(Blocked)** - 此状态意味着:**线程处于被阻塞状态**。表示线程在等待 `synchronized` 的隐式锁(Monitor lock)。`synchronized` 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,即处于阻塞状态。当占用 `synchronized` 隐式锁的线程释放锁,并且等待的线程获得 `synchronized` 隐式锁时,就又会从 `BLOCKED` 转换到 `RUNNABLE` 状态。 - -- **等待(Waiting)** - 此状态意味着:**线程无限期等待,直到被其他线程显式地唤醒**。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 `synchronized` 的隐式锁。而等待是主动的,通过调用 `Object.wait` 等方法进入。 - - | 进入方法 | 退出方法 | - | -------------------------------------------------------------- | ------------------------------------ | - | 没有设置 Timeout 参数的 `Object.wait` 方法 | `Object.notify` / `Object.notifyAll` | - | 没有设置 Timeout 参数的 `Thread.join` 方法 | 被调用的线程执行完毕 | - | `LockSupport.park` 方法(Java 并发包中的锁,都是基于它实现的) | `LockSupport.unpark` | - -- **定时等待(Timed waiting)** - 此状态意味着:**无需等待其它线程显式地唤醒,在一定时间之后会被系统自动唤醒**。 - - | 进入方法 | 退出方法 | - | ------------------------------------------------------------------------------ | ----------------------------------------------- | - | `Thread.sleep` 方法 | 时间结束 | - | 获得 `synchronized` 隐式锁的线程,调用设置了 Timeout 参数的 `Object.wait` 方法 | 时间结束 / `Object.notify` / `Object.notifyAll` | - | 设置了 Timeout 参数的 `Thread.join` 方法 | 时间结束 / 被调用的线程执行完毕 | - | `LockSupport.parkNanos` 方法 | `LockSupport.unpark` | - | `LockSupport.parkUntil` 方法 | `LockSupport.unpark` | - -- **终止(Terminated)** - 线程执行完 `run` 方法,或者因异常退出了 `run` 方法。此状态意味着:线程结束了生命周期。 - -## 线程常见问题 - -### sleep、yield、join 方法有什么区别 - -- `yield` 方法 - - `yield` 方法会 **让线程从 `Running` 状态转入 `Runnable` 状态**。 - - 当调用了 `yield` 方法后,只有**与当前线程相同或更高优先级的`Runnable` 状态线程才会获得执行的机会**。 -- `sleep` 方法 - - `sleep` 方法会 **让线程从 `Running` 状态转入 `Waiting` 状态**。 - - `sleep` 方法需要指定等待的时间,**超过等待时间后,JVM 会将线程从 `Waiting` 状态转入 `Runnable` 状态**。 - - 当调用了 `sleep` 方法后,**无论什么优先级的线程都可以得到执行机会**。 - - `sleep` 方法不会释放“锁标志”,也就是说如果有 `synchronized` 同步块,其他线程仍然不能访问共享数据。 -- `join` - - `join` 方法会 **让线程从 `Running` 状态转入 `Waiting` 状态**。 - - 当调用了 `join` 方法后,**当前线程必须等待调用 `join` 方法的线程结束后才能继续执行**。 - -### 为什么 sleep 和 yield 方法是静态的 - -`Thread` 类的 `sleep` 和 `yield` 方法将处理 `Running` 状态的线程。 - -所以在其他处于非 `Running` 状态的线程上执行这两个方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 - -### Java 线程是否按照线程优先级严格执行 - -即使设置了线程的优先级,也**无法保证高优先级的线程一定先执行**。 - -原因在于线程优先级依赖于操作系统的支持,然而,不同的操作系统支持的线程优先级并不相同,不能很好的和 Java 中线程优先级一一对应。 - -### 一个线程两次调用 start()方法会怎样 - -Java 的线程是不允许启动两次的,第二次调用必然会抛出 IllegalThreadStateException,这是一种运行时异常,多次调用 start 被认为是编程错误。 - -### `start` 和 `run` 方法有什么区别 - -- `run` 方法是线程的执行体。 -- `start` 方法会启动线程,然后 JVM 会让这个线程去执行 `run` 方法。 - -### 可以直接调用 `Thread` 类的 `run` 方法么 - -- 可以。但是如果直接调用 `Thread` 的 `run` 方法,它的行为就会和普通的方法一样。 -- 为了在新的线程中执行我们的代码,必须使用 `Thread` 的 `start` 方法。 - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [进程和线程关系及区别](https://blog.csdn.net/yaosiming2011/article/details/44280797) -- [Java 线程中 yield 与 join 方法的区别](http://www.importnew.com/14958.html) -- [sleep(),wait(),yield()和 join()方法的区别](https://blog.csdn.net/xiangwanpeng/article/details/54972952) -- [Java 并发编程:线程间协作的两种方式:wait、notify、notifyAll 和 Condition](https://www.cnblogs.com/dolphin0520/p/3920385.html) -- [Java 并发编程:Callable、Future 和 FutureTask](https://www.cnblogs.com/dolphin0520/p/3949310.html) -- [StackOverflow VisualVM - Thread States](https://stackoverflow.com/questions/27406200/visualvm-thread-states) -- [Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) -- [Java 并发](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20%E5%B9%B6%E5%8F%91.md) -- [Why must wait() always be in synchronized block](https://stackoverflow.com/questions/2779484/why-must-wait-always-be-in-synchronized-block) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/03.Java\345\271\266\345\217\221\346\240\270\345\277\203\346\234\272\345\210\266.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/03.Java\345\271\266\345\217\221\346\240\270\345\277\203\346\234\272\345\210\266.md" deleted file mode 100644 index 34b9cad8f7..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/03.Java\345\271\266\345\217\221\346\240\270\345\277\203\346\234\272\345\210\266.md" +++ /dev/null @@ -1,1225 +0,0 @@ ---- -title: Java并发核心机制 -date: 2019-12-25 22:19:09 -order: 03 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 -permalink: /pages/2c6488/ ---- - -# Java 并发核心机制 - -> Java 对于并发的支持主要汇聚在 `java.util.concurrent`,即 J.U.C。而 J.U.C 的核心是 `AQS`。 - -## J.U.C 简介 - -Java 的 `java.util.concurrent` 包(简称 J.U.C)中提供了大量并发工具类,是 Java 并发能力的主要体现(注意,不是全部,有部分并发能力的支持在其他包中)。从功能上,大致可以分为: - -- 原子类 - 如:`AtomicInteger`、`AtomicIntegerArray`、`AtomicReference`、`AtomicStampedReference` 等。 -- 锁 - 如:`ReentrantLock`、`ReentrantReadWriteLock` 等。 -- 并发容器 - 如:`ConcurrentHashMap`、`CopyOnWriteArrayList`、`CopyOnWriteArraySet` 等。 -- 阻塞队列 - 如:`ArrayBlockingQueue`、`LinkedBlockingQueue` 等。 -- 非阻塞队列 - 如: `ConcurrentLinkedQueue` 、`LinkedTransferQueue` 等。 -- `Executor` 框架(线程池)- 如:`ThreadPoolExecutor`、`Executors` 等。 - -我个人理解,Java 并发框架可以分为以下层次。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-concurrent-basic-mechanism.png) - -由 Java 并发框架图不难看出,J.U.C 包中的工具类是基于 `synchronized`、`volatile`、`CAS`、`ThreadLocal` 这样的并发核心机制打造的。所以,要想深入理解 J.U.C 工具类的特性、为什么具有这样那样的特性,就必须先理解这些核心机制。 - -## synchronized - -> `synchronized` 是 Java 中的关键字,是 **利用锁的机制来实现互斥同步的**。 -> -> **`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 -> -> 如果不需要 `Lock` 、`ReadWriteLock` 所提供的高级同步特性,应该优先考虑使用 `synchronized` ,理由如下: -> -> - Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平。从趋势来看,Java 未来仍将继续优化 `synchronized` ,而不是 `ReentrantLock` 。 -> - `ReentrantLock` 是 Oracle JDK 的 API,在其他版本的 JDK 中不一定支持;而 `synchronized` 是 JVM 的内置特性,所有 JDK 版本都提供支持。 - -### synchronized 的应用 - -`synchronized` 有 3 种应用方式: - -- **同步实例方法** - 对于普通同步方法,锁是当前实例对象 -- **同步静态方法** - 对于静态同步方法,锁是当前类的 `Class` 对象 -- **同步代码块** - 对于同步方法块,锁是 `synchonized` 括号里配置的对象 - -> 说明: -> -> 类似 `Vector`、`Hashtable` 这类同步类,就是使用 `synchonized` 修饰其重要方法,来保证其线程安全。 -> -> 事实上,这类同步容器也非绝对的线程安全,当执行迭代器遍历,根据条件删除元素这种场景下,就可能出现线程不安全的情况。此外,Java 1.6 针对 `synchonized` 进行优化前,由于阻塞,其性能不高。 -> -> 综上,这类同步容器,在现代 Java 程序中,已经渐渐不用了。 - -#### 同步实例方法 - -❌ 错误示例 - 未同步的示例 - -```java -public class NoSynchronizedDemo implements Runnable { - - public static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - NoSynchronizedDemo instance = new NoSynchronizedDemo(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - public void increase() { - count++; - } - -} -// 输出结果: 小于 200000 的随机数字 -``` - -Java 实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。 - -```java -public class SynchronizedDemo implements Runnable { - - private static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - SynchronizedDemo instance = new SynchronizedDemo(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - /** - * synchronized 修饰普通方法 - */ - public synchronized void increase() { - count++; - } - -} -``` - -【示例】错误示例 - -```java -class Account { - private int balance; - // 转账 - synchronized void transfer( - Account target, int amt){ - if (this.balance > amt) { - this.balance -= amt; - target.balance += amt; - } - } -} -``` - -在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁 this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢? - -问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701135257.png) - -应该保证使用的**锁能覆盖所有受保护资源**。 - -【示例】正确姿势 - -```java -class Account { - private Object lock; - private int balance; - private Account(); - // 创建 Account 时传入同一个 lock 对象 - public Account(Object lock) { - this.lock = lock; - } - // 转账 - void transfer(Account target, int amt){ - // 此处检查所有对象共享的锁 - synchronized(lock) { - if (this.balance > amt) { - this.balance -= amt; - target.balance += amt; - } - } - } -} -``` - -这个办法确实能解决问题,但是有点小瑕疵,它要求在创建 Account 对象的时候必须传入同一个对象,如果创建 Account 对象时,传入的 lock 不是同一个对象,那可就惨了,会出现锁自家门来保护他家资产的荒唐事。在真实的项目场景中,创建 Account 对象的代码很可能分散在多个工程中,传入共享的 lock 真的很难。 - -上面的方案缺乏实践的可行性,我们需要更好的方案。还真有,就是**用 Account.class 作为共享的锁**。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。使用 Account.class 作为共享的锁,我们就无需在创建 Account 对象时传入了,代码更简单。 - -【示例】正确姿势 - -```java -class Account { - private int balance; - // 转账 - void transfer(Account target, int amt){ - synchronized(Account.class) { - if (this.balance > amt) { - this.balance -= amt; - target.balance += amt; - } - } - } -} -``` - -#### 同步静态方法 - -静态方法的同步是指同步在该方法所在的类对象上。因为在 JVM 中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。 - -对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。不管类中的哪个静态同步方法被调用,一个类只能由一个线程同时执行。 - -```java -public class SynchronizedDemo2 implements Runnable { - - private static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - SynchronizedDemo2 instance = new SynchronizedDemo2(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - /** - * synchronized 修饰静态方法 - */ - public synchronized static void increase() { - count++; - } - -} -``` - -#### 同步代码块 - -有时你不需要同步整个方法,而是同步方法中的一部分。Java 可以对方法的一部分进行同步。 - -```java -@ThreadSafe -public class SynchronizedDemo05 implements Runnable { - - private static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - SynchronizedDemo05 instance = new SynchronizedDemo05(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - /** - * synchronized 修饰代码块 - */ - public void increase() { - synchronized (this) { - count++; - } - } - -} -``` - -注意 Java 同步块构造器用括号将对象括起来。在上例中,使用了 `this`,即为调用 `increase` 方法的实例本身。用括号括起来的对象叫做监视器对象。一次只有一个线程能够在同步于同一个监视器对象的 Java 方法内执行。 - -如果是静态方法,就不能用 this 对象作为监视器对象了,而是使用 `Class` 对象,如下: - -```java -public class SynchronizedDemo3 implements Runnable { - - private static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - SynchronizedDemo3 instance = new SynchronizedDemo3(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - /** - * synchronized 修饰代码块 - */ - public static void increase() { - synchronized (SynchronizedDemo3.class) { - count++; - } - } - -} -``` - -### synchronized 的原理 - -**`synchronized` 代码块是由一对 `monitorenter` 和 `monitorexit` 指令实现的,`Monitor` 对象是同步的基本实现单元**。在 Java 6 之前,`Monitor` 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。 - -如果 `synchronized` 明确制定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据 `synchronized` 修饰的是实例方法还是静态方法,去对对应的对象实例或 `Class` 对象来作为锁对象。 - -`synchronized` 同步块对同一线程来说是可重入的,不会出现锁死问题。 - -`synchronized` 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。 - -【示例】 - -```java -public void foo(Object lock) { - synchronized (lock) { - lock.hashCode(); - } - } - // 上面的 Java 代码将编译为下面的字节码 - public void foo(java.lang.Object); - Code: - 0: aload_1 - 1: dup - 2: astore_2 - 3: monitorenter - 4: aload_1 - 5: invokevirtual java/lang/Object.hashCode:()I - 8: pop - 9: aload_2 - 10: monitorexit - 11: goto 19 - 14: astore_3 - 15: aload_2 - 16: monitorexit - 17: aload_3 - 18: athrow - 19: return - Exception table: - from to target type - 4 11 14 any - 14 17 14 any - -``` - -#### 同步代码块 - -`synchronized` 在修饰同步代码块时,是由 `monitorenter` 和 `monitorexit` 指令来实现同步的。进入 `monitorenter` 指令后,线程将持有 `Monitor` 对象,退出 `monitorenter` 指令后,线程将释放该 `Monitor` 对象。 - -#### 同步方法 - -`synchronized` 修饰同步方法时,会设置一个 `ACC_SYNCHRONIZED` 标志。当方法调用时,调用指令将会检查该方法是否被设置 `ACC_SYNCHRONIZED` 访问标志。如果设置了该标志,执行线程将先持有 `Monitor` 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 `Mointor` 对象,当方法执行完成后,再释放该 `Monitor` 对象。 - -#### Monitor - -每个对象实例都会有一个 `Monitor`,`Monitor` 可以和对象一起创建、销毁。`Monitor` 是由 `ObjectMonitor` 实现,而 `ObjectMonitor` 是由 C++ 的 `ObjectMonitor.hpp` 文件实现。 - -当多个线程同时访问一段同步代码时,多个线程会先被存放在 EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。 - -如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。 - -### synchronized 的优化 - -> **Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平**。 - -#### Java 对象头 - -在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。 - -Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200629191250.png) - -锁升级功能主要依赖于 Mark Word 中的锁标志位和是否偏向锁标志位,`synchronized` 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。 - -Java 1.6 引入了偏向锁和轻量级锁,从而让 `synchronized` 拥有了四个状态: - -- **无锁状态(unlocked)** -- **偏向锁状态(biasble)** -- **轻量级锁状态(lightweight locked)** -- **重量级锁状态(inflated)** - -当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现。 - -当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。 - -如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。 - -#### 偏向锁 - -偏向锁的思想是偏向于**第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要**。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200604105151.png) - -#### 轻量级锁 - -**轻量级锁**是相对于传统的重量级锁而言,它 **使用 CAS 操作来避免重量级锁使用互斥量的开销**。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。 - -当尝试获取一个锁对象时,如果锁对象标记为 `0|01`,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200604105248.png) - -#### 锁消除 / 锁粗化 - -除了锁升级优化,Java 还使用了编译器对锁进行优化。 - -**(1)锁消除** - -**锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除**。 - -JIT 编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步块使用的锁对象是否只能够被一个线程访问,而没有被发布到其它线程。 - -确认是的话,那么 JIT 编译器在编译这个同步块的时候不会生成 synchronized 所表示的锁的申请与释放的机器码,即消除了锁的使用。在 Java7 之后的版本就不需要手动配置了,该操作可以自动实现。 - -对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁: - -```java -public static String concatString(String s1, String s2, String s3) { - return s1 + s2 + s3; -} -``` - -`String` 是一个不可变的类,编译器会对 String 的拼接自动优化。在 Java 1.5 之前,会转化为 `StringBuffer` 对象的连续 `append()` 操作: - -```java -public static String concatString(String s1, String s2, String s3) { - StringBuffer sb = new StringBuffer(); - sb.append(s1); - sb.append(s2); - sb.append(s3); - return sb.toString(); -} -``` - -每个 `append()` 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 `concatString()` 方法内部。也就是说,sb 的所有引用永远不会逃逸到 `concatString()` 方法之外,其他线程无法访问到它,因此可以进行消除。 - -**(2)锁粗化** - -锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。 - -如果**一系列的连续操作都对同一个对象反复加锁和解锁**,频繁的加锁操作就会导致性能损耗。 - -上一节的示例代码中连续的 `append()` 方法就属于这类情况。如果**虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部**。对于上一节的示例代码就是扩展到第一个 `append()` 操作之前直至最后一个 `append()` 操作之后,这样只需要加锁一次就可以了。 - -#### 自旋锁 - -互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 - -自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。 - -在 Java 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。 - -### synchronized 的误区 - -> 示例摘自:[《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) - -#### synchronized 使用范围不当导致的错误 - -```java -public class Interesting { - - volatile int a = 1; - volatile int b = 1; - - public static void main(String[] args) { - Interesting interesting = new Interesting(); - new Thread(() -> interesting.add()).start(); - new Thread(() -> interesting.compare()).start(); - } - - public synchronized void add() { - log.info("add start"); - for (int i = 0; i < 10000; i++) { - a++; - b++; - } - log.info("add done"); - } - - public void compare() { - log.info("compare start"); - for (int i = 0; i < 10000; i++) { - //a始终等于b吗? - if (a < b) { - log.info("a:{},b:{},{}", a, b, a > b); - //最后的a>b应该始终是false吗? - } - } - log.info("compare done"); - } - -} -``` - -【输出】 - -``` -16:05:25.541 [Thread-0] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - add start -16:05:25.544 [Thread-0] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - add done -16:05:25.544 [Thread-1] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - compare start -16:05:25.544 [Thread-1] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - compare done -``` - -之所以出现这种错乱,是因为两个线程是交错执行 add 和 compare 方法中的业务逻辑,而且这些业务逻辑不是原子性的:a++ 和 b++ 操作中可以穿插在 compare 方法的比较代码中;更需要注意的是,a new Data().wrong()); - return Data.getCounter(); - } - - public int right(int count) { - Data.reset(); - IntStream.rangeClosed(1, count).parallel().forEach(i -> new Data().right()); - return Data.getCounter(); - } - - private static class Data { - - @Getter - private static int counter = 0; - private static Object locker = new Object(); - - public static int reset() { - counter = 0; - return counter; - } - - public synchronized void wrong() { - counter++; - } - - public void right() { - synchronized (locker) { - counter++; - } - } - - } - -} -``` - -wrong 方法中试图对一个静态对象加对象级别的 synchronized 锁,并不能保证线程安全。 - -#### 锁粒度导致的问题 - -要尽可能的缩小加锁的范围,这可以提高并发吞吐。 - -如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。 - -```java -public class synchronized锁粒度不当 { - - public static void main(String[] args) { - Demo demo = new Demo(); - demo.wrong(); - demo.right(); - } - - private static class Demo { - - private List data = new ArrayList<>(); - - private void slow() { - try { - TimeUnit.MILLISECONDS.sleep(10); - } catch (InterruptedException e) { - } - } - - public int wrong() { - long begin = System.currentTimeMillis(); - IntStream.rangeClosed(1, 1000).parallel().forEach(i -> { - synchronized (this) { - slow(); - data.add(i); - } - }); - log.info("took:{}", System.currentTimeMillis() - begin); - return data.size(); - } - - public int right() { - long begin = System.currentTimeMillis(); - IntStream.rangeClosed(1, 1000).parallel().forEach(i -> { - slow(); - synchronized (data) { - data.add(i); - } - }); - log.info("took:{}", System.currentTimeMillis() - begin); - return data.size(); - } - - } - -} -``` - -## volatile - -### volatile 的要点 - -`volatile` 是轻量级的 `synchronized`,它在多处理器开发中保证了共享变量的“可见性”。 - -被 `volatile` 修饰的变量,具备以下特性: - -- **线程可见性** - 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个共享变量,另外一个线程能读到这个修改的值。 -- **禁止指令重排序** -- **不保证原子性** - -我们知道,线程安全需要具备:可见性、原子性、顺序性。`volatile` 不保证原子性,所以决定了它不能彻底地保证线程安全。 - -### volatile 的应用 - -如果 `volatile` 变量修饰符使用恰当的话,它比 `synchronized` 的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。但是,**`volatile` 无法替代 `synchronized` ,因为 `volatile` 无法保证操作的原子性**。 - -通常来说,**使用 `volatile` 必须具备以下 2 个条件**: - -- **对变量的写操作不依赖于当前值** -- **该变量没有包含在具有其他变量的表达式中** - -【示例】状态标记量 - -```java -volatile boolean flag = false; - -while(!flag) { - doSomething(); -} - -public void setFlag() { - flag = true; -} -``` - -【示例】双重锁实现线程安全的单例模式 - -```java -class Singleton { - private volatile static Singleton instance = null; - - private Singleton() {} - - public static Singleton getInstance() { - if(instance==null) { - synchronized (Singleton.class) { - if(instance==null) - instance = new Singleton(); - } - } - return instance; - } -} -``` - -### volatile 的原理 - -观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,**加入 `volatile` 关键字时,会多出一个 `lock` 前缀指令**。**`lock` 前缀指令实际上相当于一个内存屏障**(也成内存栅栏),内存屏障会提供 3 个功能: - -- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; -- 它会强制将对缓存的修改操作立即写入主存; -- 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。 - -### volatile 的问题 - -`volatile` 的要点中,已经提到,**`volatile` 不保证原子性,所以 volatile 并不能保证线程安全**。 - -那么,如何做到线程安全呢?有两种方案: - -- `volatile` + `synchronized` - 可以参考:【示例】双重锁实现线程安全的单例模式 -- 使用原子类替代 `volatile` - -## CAS - -### CAS 的要点 - -互斥同步是最常见的并发正确性保障手段。 - -**互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题**,因此互斥同步也被称为阻塞同步。互斥同步属于一种悲观的并发策略,总是认为只要不去做正确的同步措施,那就肯定会出现问题。无论共享数据是否真的会出现竞争,它都要进行加锁(这里讨论的是概念模型,实际上虚拟机会优化掉很大一部分不必要的加锁)、用户态核心态转换、维护锁计数器和检查是否有被阻塞的线程需要唤醒等操作。 - -随着硬件指令集的发展,我们可以使用基于冲突检测的乐观并发策略:先进行操作,如果没有其它线程争用共享数据,那操作就成功了,否则采取补偿措施(不断地重试,直到成功为止)。这种乐观的并发策略的许多实现都不需要将线程阻塞,因此这种同步操作称为非阻塞同步。 - -为什么说乐观锁需要 **硬件指令集的发展** 才能进行?因为需要操作和冲突检测这两个步骤具备原子性。而这点是由硬件来完成,如果再使用互斥同步来保证就失去意义了。硬件支持的原子性操作最典型的是:CAS。 - -**CAS(Compare and Swap),字面意思为比较并交换。CAS 有 3 个操作数,分别是:内存值 M,期望值 E,更新值 U。当且仅当内存值 M 和期望值 E 相等时,将内存值 M 修改为 U,否则什么都不做**。 - -### CAS 的应用 - -**CAS 只适用于线程冲突较少的情况**。 - -CAS 的典型应用场景是: - -- 原子类 -- 自旋锁 - -#### 原子类 - -> 原子类是 CAS 在 Java 中最典型的应用。 - -我们先来看一个常见的代码片段。 - -```Java -if(a==b) { - a++; -} -``` - -如果 `a++` 执行前, a 的值被修改了怎么办?还能得到预期值吗?出现该问题的原因是在并发环境下,以上代码片段不是原子操作,随时可能被其他线程所篡改。 - -解决这种问题的最经典方式是应用原子类的 `incrementAndGet` 方法。 - -```Java -public class AtomicIntegerDemo { - - public static void main(String[] args) throws InterruptedException { - ExecutorService executorService = Executors.newFixedThreadPool(3); - final AtomicInteger count = new AtomicInteger(0); - for (int i = 0; i < 10; i++) { - executorService.execute(new Runnable() { - @Override - public void run() { - count.incrementAndGet(); - } - }); - } - - executorService.shutdown(); - executorService.awaitTermination(3, TimeUnit.SECONDS); - System.out.println("Final Count is : " + count.get()); - } - -} -``` - -J.U.C 包中提供了 `AtomicBoolean`、`AtomicInteger`、`AtomicLong` 分别针对 `Boolean`、`Integer`、`Long` 执行原子操作,操作和上面的示例大体相似,不做赘述。 - -#### 自旋锁 - -利用原子类(本质上是 CAS),可以实现自旋锁。 - -所谓自旋锁,是指线程反复检查锁变量是否可用,直到成功为止。由于线程在这一过程中保持执行,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 - -示例:非线程安全示例 - -```java -public class AtomicReferenceDemo { - - private static int ticket = 10; - - public static void main(String[] args) { - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0; i < 5; i++) { - executorService.execute(new MyThread()); - } - executorService.shutdown(); - } - - static class MyThread implements Runnable { - - @Override - public void run() { - while (ticket > 0) { - System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); - ticket--; - } - } - - } - -} -``` - -输出结果: - -``` -pool-1-thread-2 卖出了第 10 张票 -pool-1-thread-1 卖出了第 10 张票 -pool-1-thread-3 卖出了第 10 张票 -pool-1-thread-1 卖出了第 8 张票 -pool-1-thread-2 卖出了第 9 张票 -pool-1-thread-1 卖出了第 6 张票 -pool-1-thread-3 卖出了第 7 张票 -pool-1-thread-1 卖出了第 4 张票 -pool-1-thread-2 卖出了第 5 张票 -pool-1-thread-1 卖出了第 2 张票 -pool-1-thread-3 卖出了第 3 张票 -pool-1-thread-2 卖出了第 1 张票 -``` - -很明显,出现了重复售票的情况。 - -【示例】使用自旋锁来保证线程安全 - -可以通过自旋锁这种非阻塞同步来保证线程安全,下面使用 `AtomicReference` 来实现一个自旋锁。 - -```java -public class AtomicReferenceDemo2 { - - private static int ticket = 10; - - public static void main(String[] args) { - threadSafeDemo(); - } - - private static void threadSafeDemo() { - SpinLock lock = new SpinLock(); - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0; i < 5; i++) { - executorService.execute(new MyThread(lock)); - } - executorService.shutdown(); - } - - static class SpinLock { - - private AtomicReference atomicReference = new AtomicReference<>(); - - public void lock() { - Thread current = Thread.currentThread(); - while (!atomicReference.compareAndSet(null, current)) {} - } - - public void unlock() { - Thread current = Thread.currentThread(); - atomicReference.compareAndSet(current, null); - } - - } - - static class MyThread implements Runnable { - - private SpinLock lock; - - public MyThread(SpinLock lock) { - this.lock = lock; - } - - @Override - public void run() { - while (ticket > 0) { - lock.lock(); - if (ticket > 0) { - System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); - ticket--; - } - lock.unlock(); - } - } - - } - -} -``` - -输出结果: - -``` -pool-1-thread-2 卖出了第 10 张票 -pool-1-thread-1 卖出了第 9 张票 -pool-1-thread-3 卖出了第 8 张票 -pool-1-thread-2 卖出了第 7 张票 -pool-1-thread-3 卖出了第 6 张票 -pool-1-thread-1 卖出了第 5 张票 -pool-1-thread-2 卖出了第 4 张票 -pool-1-thread-1 卖出了第 3 张票 -pool-1-thread-3 卖出了第 2 张票 -pool-1-thread-1 卖出了第 1 张票 -``` - -### CAS 的原理 - -Java 主要利用 `Unsafe` 这个类提供的 CAS 操作。`Unsafe` 的 CAS 依赖的是 JVM 针对不同的操作系统实现的硬件指令 **`Atomic::cmpxchg`**。`Atomic::cmpxchg` 的实现使用了汇编的 CAS 操作,并使用 CPU 提供的 `lock` 信号保证其原子性。 - -### CAS 的问题 - -一般情况下,CAS 比锁性能更高。因为 CAS 是一种非阻塞算法,所以其避免了线程阻塞和唤醒的等待时间。 - -但是,事物总会有利有弊,CAS 也存在三大问题: - -- ABA 问题 -- 循环时间长开销大 -- 只能保证一个共享变量的原子性 - -#### ABA 问题 - -**如果一个变量初次读取的时候是 A 值,它的值被改成了 B,后来又被改回为 A,那 CAS 操作就会误认为它从来没有被改变过**。 - -J.U.C 包提供了一个带有标记的**原子引用类 `AtomicStampedReference` 来解决这个问题**,它可以通过控制变量值的版本来保证 CAS 的正确性。大部分情况下 ABA 问题不会影响程序并发的正确性,如果需要解决 ABA 问题,改用**传统的互斥同步可能会比原子类更高效**。 - -#### 循环时间长开销大 - -**自旋 CAS (不断尝试,直到成功为止)如果长时间不成功,会给 CPU 带来非常大的执行开销**。 - -如果 JVM 能支持处理器提供的 `pause` 指令那么效率会有一定的提升,`pause` 指令有两个作用: - -- 它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。 -- 它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。 - -比较花费 CPU 资源,即使没有任何用也会做一些无用功。 - -#### 只能保证一个共享变量的原子性 - -当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁。 - -或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 `i = 2, j = a`,合并一下 `ij=2a`,然后用 CAS 来操作 `ij`。从 Java 1.5 开始 JDK 提供了 `AtomicReference` 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。 - -## ThreadLocal - -> **`ThreadLocal` 是一个存储线程本地副本的工具类**。 -> -> 要保证线程安全,不一定非要进行同步。同步只是保证共享数据争用时的正确性,如果一个方法本来就不涉及共享数据,那么自然无须同步。 -> -> Java 中的 **无同步方案** 有: -> -> - **可重入代码** - 也叫纯代码。如果一个方法,它的 **返回结果是可以预测的**,即只要输入了相同的数据,就能返回相同的结果,那它就满足可重入性,当然也是线程安全的。 -> - **线程本地存储** - 使用 **`ThreadLocal` 为共享变量在每个线程中都创建了一个本地副本**,这个副本只能被当前线程访问,其他线程无法访问,那么自然是线程安全的。 - -### ThreadLocal 的应用 - -`ThreadLocal` 的方法: - -```java -public class ThreadLocal { - public T get() {} - public void set(T value) {} - public void remove() {} - public static ThreadLocal withInitial(Supplier supplier) {} -} -``` - -> 说明: -> -> - `get` - 用于获取 `ThreadLocal` 在当前线程中保存的变量副本。 -> - `set` - 用于设置当前线程中变量的副本。 -> - `remove` - 用于删除当前线程中变量的副本。如果此线程局部变量随后被当前线程读取,则其值将通过调用其 `initialValue` 方法重新初始化,除非其值由中间线程中的当前线程设置。 这可能会导致当前线程中多次调用 `initialValue` 方法。 -> - `initialValue` - 为 ThreadLocal 设置默认的 `get` 初始值,需要重写 `initialValue` 方法 。 - -`ThreadLocal` 常用于防止对可变的单例(Singleton)变量或全局变量进行共享。典型应用场景有:管理数据库连接、Session。 - -【示例】数据库连接 - -```java -private static ThreadLocal connectionHolder = new ThreadLocal() { - @Override - public Connection initialValue() { - return DriverManager.getConnection(DB_URL); - } -}; - -public static Connection getConnection() { - return connectionHolder.get(); -} -``` - -【示例】Session 管理 - -```java -private static final ThreadLocal sessionHolder = new ThreadLocal<>(); - -public static Session getSession() { - Session session = (Session) sessionHolder.get(); - try { - if (session == null) { - session = createSession(); - sessionHolder.set(session); - } - } catch (Exception e) { - e.printStackTrace(); - } - return session; -} -``` - -【示例】完整使用 `ThreadLocal` 示例 - -```java -public class ThreadLocalDemo { - - private static ThreadLocal threadLocal = new ThreadLocal() { - @Override - protected Integer initialValue() { - return 0; - } - }; - - public static void main(String[] args) { - ExecutorService executorService = Executors.newFixedThreadPool(10); - for (int i = 0; i < 10; i++) { - executorService.execute(new MyThread()); - } - executorService.shutdown(); - } - - static class MyThread implements Runnable { - - @Override - public void run() { - int count = threadLocal.get(); - for (int i = 0; i < 10; i++) { - try { - count++; - Thread.sleep(100); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - threadLocal.set(count); - threadLocal.remove(); - System.out.println(Thread.currentThread().getName() + " : " + count); - } - - } - -} -``` - -全部输出 count = 10 - -### ThreadLocal 的原理 - -#### 存储结构 - -**`Thread` 类中维护着一个 `ThreadLocal.ThreadLocalMap` 类型的成员** `threadLocals`。这个成员就是用来存储当前线程独占的变量副本。 - -`ThreadLocalMap` 是 `ThreadLocal` 的内部类,它维护着一个 `Entry` 数组,**`Entry` 继承了 `WeakReference`** ,所以是弱引用。 `Entry` 用于保存键值对,其中: - -- `key` 是 `ThreadLocal` 对象; -- `value` 是传递进来的对象(变量副本)。 - -```java -public class Thread implements Runnable { - // ... - ThreadLocal.ThreadLocalMap threadLocals = null; - // ... -} - -static class ThreadLocalMap { - // ... - static class Entry extends WeakReference> { - /** The value associated with this ThreadLocal. */ - Object value; - - Entry(ThreadLocal k, Object v) { - super(k); - value = v; - } - } - // ... -} -``` - -#### 如何解决 Hash 冲突 - -`ThreadLocalMap` 虽然是类似 `Map` 结构的数据结构,但它并没有实现 `Map` 接口。它不支持 `Map` 接口中的 `next` 方法,这意味着 `ThreadLocalMap` 中解决 Hash 冲突的方式并非 **拉链表** 方式。 - -实际上,**`ThreadLocalMap` 采用线性探测的方式来解决 Hash 冲突**。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在 table 数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。 - -#### 内存泄漏问题 - -`ThreadLocalMap` 的 `Entry` 继承了 `WeakReference`,所以它的 **key (`ThreadLocal` 对象)是弱引用,而 value (变量副本)是强引用**。 - -- 如果 `ThreadLocal` 对象没有外部强引用来引用它,那么 `ThreadLocal` 对象会在下次 GC 时被回收。 -- 此时,`Entry` 中的 key 已经被回收,但是 value 由于是强引用不会被垃圾收集器回收。如果创建 `ThreadLocal` 的线程一直持续运行,那么 value 就会一直得不到回收,产生**内存泄露**。 - -那么如何避免内存泄漏呢?方法就是:**使用 `ThreadLocal` 的 `set` 方法后,显示的调用 `remove` 方法** 。 - -```java -ThreadLocal threadLocal = new ThreadLocal(); -try { - threadLocal.set("xxx"); - // ... -} finally { - threadLocal.remove(); -} -``` - -### ThreadLocal 的误区 - -> 示例摘自:[《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) - -ThreadLocal 适用于变量在线程间隔离,而在方法或类间共享的场景。 - -前文提到,ThreadLocal 是线程隔离的,那么是不是使用 ThreadLocal 就一定高枕无忧呢? - -#### ThreadLocal 错误案例 - -使用 Spring Boot 创建一个 Web 应用程序,使用 ThreadLocal 存放一个 Integer 的值,来暂且代表需要在线程中保存的用户信息,这个值初始是 null。 - -```java - private ThreadLocal currentUser = ThreadLocal.withInitial(() -> null); - - @GetMapping("wrong") - public Map wrong(@RequestParam("id") Integer userId) { - //设置用户信息之前先查询一次ThreadLocal中的用户信息 - String before = Thread.currentThread().getName() + ":" + currentUser.get(); - //设置用户信息到ThreadLocal - currentUser.set(userId); - //设置用户信息之后再查询一次ThreadLocal中的用户信息 - String after = Thread.currentThread().getName() + ":" + currentUser.get(); - //汇总输出两次查询结果 - Map result = new HashMap<>(); - result.put("before", before); - result.put("after", after); - return result; - } -``` - -【预期】从代码逻辑来看,我们预期第一次获取的值始终应该是 null。 - -【实际】 - -为了方便复现,将 Tomcat 工作线程设为 1: - -``` -server.tomcat.max-threads=1 -``` - -当访问 id = 1 时,符合预期 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200731111854.png) - -当访问 id = 2 时,before 的应答不是 null,而是 1,不符合预期。 - -【分析】实际情况和预期存在偏差。Spring Boot 程序运行在 Tomcat 中,执行程序的线程是 Tomcat 的工作线程,而 Tomcat 的工作线程是基于线程池的。**线程池会重用固定的几个线程,一旦线程重用,那么很可能首次从** -**ThreadLocal 获取的值是之前其他用户的请求遗留的值。这时,ThreadLocal 中的用户信息就是其他用户的信息**。 - -**并不能认为没有显式开启多线程就不会有线程安全问题**。使用类似 ThreadLocal 工具来存放一些数据时,需要特别注意在代码运行完后,显式地去清空设置的数据。 - -#### ThreadLocal 错误案例修正 - -```java - @GetMapping("right") - public Map right(@RequestParam("id") Integer userId) { - String before = Thread.currentThread().getName() + ":" + currentUser.get(); - currentUser.set(userId); - try { - String after = Thread.currentThread().getName() + ":" + currentUser.get(); - Map result = new HashMap<>(); - result.put("before", before); - result.put("after", after); - return result; - } finally { - //在finally代码块中删除ThreadLocal中的数据,确保数据不串 - currentUser.remove(); - } - } -``` - -### InheritableThreadLocal - -`InheritableThreadLocal` 类是 `ThreadLocal` 类的子类。 - -`ThreadLocal` 中每个线程拥有它自己独占的数据。与 `ThreadLocal` 不同的是,`InheritableThreadLocal` 允许一个线程以及该线程创建的所有子线程都可以访问它保存的数据。 - -> 原理参考:[Java 多线程:InheritableThreadLocal 实现原理](https://blog.csdn.net/ni357103403/article/details/51970748) - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) -- [Java 并发编程:volatile 关键字解析](http://www.cnblogs.com/dolphin0520/p/3920373.html) -- [Java 并发编程:synchronized](http://www.cnblogs.com/dolphin0520/p/3923737.html) -- [深入理解 Java 并发之 synchronized 实现原理](https://blog.csdn.net/javazejian/article/details/72828483) -- [Java CAS 完全解读](https://www.jianshu.com/p/473e14d5ab2d) -- [Java 中 CAS 详解](https://blog.csdn.net/ls5718/article/details/52563959) -- [ThreadLocal 终极篇](https://juejin.im/post/5a64a581f265da3e3b7aa02d) -- [synchronized 实现原理及锁优化](https://nicky-chen.github.io/2018/05/14/synchronized-principle/) -- [Non-blocking Algorithms](http://tutorials.jenkov.com/java-concurrency/non-blocking-algorithms.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/05.Java\345\216\237\345\255\220\347\261\273.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/05.Java\345\216\237\345\255\220\347\261\273.md" deleted file mode 100644 index 3c56191adf..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/05.Java\345\216\237\345\255\220\347\261\273.md" +++ /dev/null @@ -1,458 +0,0 @@ ---- -title: Java原子类 -date: 2019-12-26 23:11:52 -order: 05 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 - - 原子类 -permalink: /pages/25f78a/ ---- - -# Java 原子变量类 - -## 原子变量类简介 - -### 为何需要原子变量类 - -保证线程安全是 Java 并发编程必须要解决的重要问题。Java 从原子性、可见性、有序性这三大特性入手,确保多线程的数据一致性。 - -- 确保线程安全最常见的做法是利用锁机制(`Lock`、`sychronized`)来对共享数据做互斥同步,这样在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,那么操作必然是原子性的,线程安全的。互斥同步最主要的问题是线程阻塞和唤醒所带来的性能问题。 -- `volatile` 是轻量级的锁(自然比普通锁性能要好),它保证了共享变量在多线程中的可见性,但无法保证原子性。所以,它只能在一些特定场景下使用。 -- 为了兼顾原子性以及锁带来的性能问题,Java 引入了 CAS (主要体现在 `Unsafe` 类)来实现非阻塞同步(也叫乐观锁)。并基于 CAS ,提供了一套原子工具类。 - -### 原子变量类的作用 - -原子变量类 **比锁的粒度更细,更轻量级**,并且对于在多处理器系统上实现高性能的并发代码来说是非常关键的。原子变量将发生竞争的范围缩小到单个变量上。 - -原子变量类相当于一种泛化的 `volatile` 变量,能够**支持原子的、有条件的读/改/写操**作。 - -原子类在内部使用 CAS 指令(基于硬件的支持)来实现同步。这些指令通常比锁更快。 - -原子变量类可以分为 4 组: - -- 基本类型 - - `AtomicBoolean` - 布尔类型原子类 - - `AtomicInteger` - 整型原子类 - - `AtomicLong` - 长整型原子类 -- 引用类型 - - `AtomicReference` - 引用类型原子类 - - `AtomicMarkableReference` - 带有标记位的引用类型原子类 - - `AtomicStampedReference` - 带有版本号的引用类型原子类 -- 数组类型 - - `AtomicIntegerArray` - 整形数组原子类 - - `AtomicLongArray` - 长整型数组原子类 - - `AtomicReferenceArray` - 引用类型数组原子类 -- 属性更新器类型 - - `AtomicIntegerFieldUpdater` - 整型字段的原子更新器。 - - `AtomicLongFieldUpdater` - 长整型字段的原子更新器。 - - `AtomicReferenceFieldUpdater` - 原子更新引用类型里的字段。 - -> 这里不对 CAS、volatile、互斥同步做深入探讨。如果想了解更多细节,不妨参考:[Java 并发核心机制](https://dunwu.github.io/waterdrop/pages/2c6488/) - -## 基本类型 - -这一类型的原子类是针对 Java 基本类型进行操作。 - -- `AtomicBoolean` - 布尔类型原子类 -- `AtomicInteger` - 整型原子类 -- `AtomicLong` - 长整型原子类 - -以上类都支持 CAS([compare-and-swap](https://en.wikipedia.org/wiki/Compare-and-swap))技术,此外,`AtomicInteger`、`AtomicLong` 还支持算术运算。 - -> :bulb: 提示: -> -> 虽然 Java 只提供了 `AtomicBoolean` 、`AtomicInteger`、`AtomicLong`,但是可以模拟其他基本类型的原子变量。要想模拟其他基本类型的原子变量,可以将 `short` 或 `byte` 等类型与 `int` 类型进行转换,以及使用 `Float.floatToIntBits` 、`Double.doubleToLongBits` 来转换浮点数。 -> -> 由于 `AtomicBoolean`、`AtomicInteger`、`AtomicLong` 实现方式、使用方式都相近,所以本文仅针对 `AtomicInteger` 进行介绍。 - -### **`AtomicInteger` 用法** - -```java -public final int get() // 获取当前值 -public final int getAndSet(int newValue) // 获取当前值,并设置新值 -public final int getAndIncrement()// 获取当前值,并自增 -public final int getAndDecrement() // 获取当前值,并自减 -public final int getAndAdd(int delta) // 获取当前值,并加上预期值 -boolean compareAndSet(int expect, int update) // 如果输入值(update)等于预期值,将该值设置为输入值 -public final void lazySet(int newValue) // 最终设置为 newValue,使用 lazySet 设置之后可能导致其他线程在之后的一小段时间内还是可以读到旧的值。 -``` - -`AtomicInteger` 使用示例: - -```java -public class AtomicIntegerDemo { - - public static void main(String[] args) throws InterruptedException { - ExecutorService executorService = Executors.newFixedThreadPool(5); - AtomicInteger count = new AtomicInteger(0); - for (int i = 0; i < 1000; i++) { - executorService.submit((Runnable) () -> { - System.out.println(Thread.currentThread().getName() + " count=" + count.get()); - count.incrementAndGet(); - }); - } - - executorService.shutdown(); - executorService.awaitTermination(30, TimeUnit.SECONDS); - System.out.println("Final Count is : " + count.get()); - } -} -``` - -### **`AtomicInteger` 实现** - -阅读 `AtomicInteger` 源码,可以看到如下定义: - -```java -private static final Unsafe unsafe = Unsafe.getUnsafe(); -private static final long valueOffset; - -static { - try { - valueOffset = unsafe.objectFieldOffset - (AtomicInteger.class.getDeclaredField("value")); - } catch (Exception ex) { throw new Error(ex); } -} - -private volatile int value; -``` - -> 说明: -> -> - `value` - value 属性使用 `volatile` 修饰,使得对 value 的修改在并发环境下对所有线程可见。 -> - `valueOffset` - value 属性的偏移量,通过这个偏移量可以快速定位到 value 字段,这个是实现 AtomicInteger 的关键。 -> - `unsafe` - Unsafe 类型的属性,它为 `AtomicInteger` 提供了 CAS 操作。 - -## 引用类型 - -Java 数据类型分为 **基本数据类型** 和 **引用数据类型** 两大类(不了解 Java 数据类型划分可以参考: [Java 基本数据类型](https://dunwu.github.io/waterdrop/pages/55d693/) )。 - -上一节中提到了针对基本数据类型的原子类,那么如果想针对引用类型做原子操作怎么办?Java 也提供了相关的原子类: - -- `AtomicReference` - 引用类型原子类 -- `AtomicMarkableReference` - 带有标记位的引用类型原子类 -- `AtomicStampedReference` - 带有版本号的引用类型原子类 - -> `AtomicStampedReference` 类在引用类型原子类中,彻底地解决了 ABA 问题,其它的 CAS 能力与另外两个类相近,所以最具代表性。因此,本节只针对 `AtomicStampedReference` 进行说明。 - -示例:基于 `AtomicReference` 实现一个简单的自旋锁 - -```java -public class AtomicReferenceDemo2 { - - private static int ticket = 10; - - public static void main(String[] args) { - threadSafeDemo(); - } - - private static void threadSafeDemo() { - SpinLock lock = new SpinLock(); - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0; i < 5; i++) { - executorService.execute(new MyThread(lock)); - } - executorService.shutdown(); - } - - /** - * 基于 {@link AtomicReference} 实现的简单自旋锁 - */ - static class SpinLock { - - private AtomicReference atomicReference = new AtomicReference<>(); - - public void lock() { - Thread current = Thread.currentThread(); - while (!atomicReference.compareAndSet(null, current)) {} - } - - public void unlock() { - Thread current = Thread.currentThread(); - atomicReference.compareAndSet(current, null); - } - - } - - /** - * 利用自旋锁 {@link SpinLock} 并发处理数据 - */ - static class MyThread implements Runnable { - - private SpinLock lock; - - public MyThread(SpinLock lock) { - this.lock = lock; - } - - @Override - public void run() { - while (ticket > 0) { - lock.lock(); - if (ticket > 0) { - System.out.println(Thread.currentThread().getName() + " 卖出了第 " + ticket + " 张票"); - ticket--; - } - lock.unlock(); - } - } - - } - -} -``` - -原子类的实现基于 CAS 机制,而 CAS 存在 ABA 问题(不了解 ABA 问题,可以参考:[Java 并发基础机制 - CAS 的问题](https://dunwu.github.io/waterdrop/pages/2c6488/#cas-%E7%9A%84%E9%97%AE%E9%A2%98))。正是为了解决 ABA 问题,才有了 `AtomicMarkableReference` 和 `AtomicStampedReference`。 - -`AtomicMarkableReference` 使用一个布尔值作为标记,修改时在 true / false 之间切换。这种策略不能根本上解决 ABA 问题,但是可以降低 ABA 发生的几率。常用于缓存或者状态描述这样的场景。 - -```java -public class AtomicMarkableReferenceDemo { - - private final static String INIT_TEXT = "abc"; - - public static void main(String[] args) throws InterruptedException { - - final AtomicMarkableReference amr = new AtomicMarkableReference<>(INIT_TEXT, false); - - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0; i < 10; i++) { - executorService.submit(new Runnable() { - @Override - public void run() { - try { - Thread.sleep(Math.abs((int) (Math.random() * 100))); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - String name = Thread.currentThread().getName(); - if (amr.compareAndSet(INIT_TEXT, name, amr.isMarked(), !amr.isMarked())) { - System.out.println(Thread.currentThread().getName() + " 修改了对象!"); - System.out.println("新的对象为:" + amr.getReference()); - } - } - }); - } - - executorService.shutdown(); - executorService.awaitTermination(3, TimeUnit.SECONDS); - } - -} -``` - -**`AtomicStampedReference` 使用一个整型值做为版本号,每次更新前先比较版本号,如果一致,才进行修改**。通过这种策略,可以根本上解决 ABA 问题。 - -```java -public class AtomicStampedReferenceDemo { - - private final static String INIT_REF = "pool-1-thread-3"; - - private final static AtomicStampedReference asr = new AtomicStampedReference<>(INIT_REF, 0); - - public static void main(String[] args) throws InterruptedException { - - System.out.println("初始对象为:" + asr.getReference()); - - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0; i < 3; i++) { - executorService.execute(new MyThread()); - } - - executorService.shutdown(); - executorService.awaitTermination(3, TimeUnit.SECONDS); - } - - static class MyThread implements Runnable { - - @Override - public void run() { - try { - Thread.sleep(Math.abs((int) (Math.random() * 100))); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - final int stamp = asr.getStamp(); - if (asr.compareAndSet(INIT_REF, Thread.currentThread().getName(), stamp, stamp + 1)) { - System.out.println(Thread.currentThread().getName() + " 修改了对象!"); - System.out.println("新的对象为:" + asr.getReference()); - } - } - - } - -} -``` - -## 数组类型 - -Java 提供了以下针对数组的原子类: - -- `AtomicIntegerArray` - 整形数组原子类 -- `AtomicLongArray` - 长整型数组原子类 -- `AtomicReferenceArray` - 引用类型数组原子类 - -已经有了针对基本类型和引用类型的原子类,为什么还要提供针对数组的原子类呢? - -数组类型的原子类为 **数组元素** 提供了 `volatile` 类型的访问语义,这是普通数组所不具备的特性——**`volatile` 类型的数组仅在数组引用上具有 `volatile` 语义**。 - -示例:`AtomicIntegerArray` 使用示例(`AtomicLongArray` 、`AtomicReferenceArray` 使用方式也类似) - -```java -public class AtomicIntegerArrayDemo { - - private static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10); - - public static void main(final String[] arguments) throws InterruptedException { - - System.out.println("Init Values: "); - for (int i = 0; i < atomicIntegerArray.length(); i++) { - atomicIntegerArray.set(i, i); - System.out.print(atomicIntegerArray.get(i) + " "); - } - System.out.println(); - - Thread t1 = new Thread(new Increment()); - Thread t2 = new Thread(new Compare()); - t1.start(); - t2.start(); - - t1.join(); - t2.join(); - - System.out.println("Final Values: "); - for (int i = 0; i < atomicIntegerArray.length(); i++) { - System.out.print(atomicIntegerArray.get(i) + " "); - } - System.out.println(); - } - - static class Increment implements Runnable { - - @Override - public void run() { - - for (int i = 0; i < atomicIntegerArray.length(); i++) { - int value = atomicIntegerArray.incrementAndGet(i); - System.out.println(Thread.currentThread().getName() + ", index = " + i + ", value = " + value); - } - } - - } - - static class Compare implements Runnable { - - @Override - public void run() { - for (int i = 0; i < atomicIntegerArray.length(); i++) { - boolean swapped = atomicIntegerArray.compareAndSet(i, 2, 3); - if (swapped) { - System.out.println(Thread.currentThread().getName() + " swapped, index = " + i + ", value = 3"); - } - } - } - - } - -} -``` - -## 属性更新器类型 - -更新器类支持基于反射机制的更新字段值的原子操作。 - -- `AtomicIntegerFieldUpdater` - 整型字段的原子更新器。 -- `AtomicLongFieldUpdater` - 长整型字段的原子更新器。 -- `AtomicReferenceFieldUpdater` - 原子更新引用类型里的字段。 - -这些类的使用有一定限制: - -- 因为对象的属性修改类型原子类都是抽象类,所以每次使用都必须使用静态方法 `newUpdater()` 创建一个更新器,并且需要设置想要更新的类和属性。 -- 字段必须是 `volatile` 类型的; -- 不能作用于静态变量(`static`); -- 不能作用于常量(`final`); - -```java -public class AtomicReferenceFieldUpdaterDemo { - - static User user = new User("begin"); - - static AtomicReferenceFieldUpdater updater = - AtomicReferenceFieldUpdater.newUpdater(User.class, String.class, "name"); - - public static void main(String[] args) { - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0; i < 5; i++) { - executorService.execute(new MyThread()); - } - executorService.shutdown(); - } - - static class MyThread implements Runnable { - - @Override - public void run() { - if (updater.compareAndSet(user, "begin", "end")) { - try { - TimeUnit.SECONDS.sleep(1); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println(Thread.currentThread().getName() + " 已修改 name = " + user.getName()); - } else { - System.out.println(Thread.currentThread().getName() + " 已被其他线程修改"); - } - } - - } - - static class User { - - volatile String name; - - public User(String name) { - this.name = name; - } - - public String getName() { - return name; - } - - public User setName(String name) { - this.name = name; - return this; - } - - } - -} -``` - -## 原子化的累加器 - -`DoubleAccumulator`、`DoubleAdder`、`LongAccumulator` 和 `LongAdder`,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 `compareAndSet()` 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好,代价就是会消耗更多的内存空间。 - -`LongAdder` 内部由一个 `base` 变量和一个 `cell[]` 数组组成。 - -- 当只有一个写线程,没有竞争的情况下,`LongAdder` 会直接使用 `base` 变量作为原子操作变量,通过 CAS 操作修改变量; -- 当有多个写线程竞争的情况下,除了占用 `base` 变量的一个写线程之外,其它各个线程会将修改的变量写入到自己的槽 `cell[]` 数组中。 - -我们可以发现,`LongAdder` 在操作后的返回值只是一个近似准确的数值,但是 `LongAdder` 最终返回的是一个准确的数值, 所以在一些对实时性要求比较高的场景下,`LongAdder` 并不能取代 `AtomicInteger` 或 `AtomicLong`。 - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [JUC 中的原子类](http://www.itsoku.com/article/182) -- http://tutorials.jenkov.com/java-util-concurrent/atomicinteger.html -- http://tutorials.jenkov.com/java-util-concurrent/atomicintegerarray.html -- http://tutorials.jenkov.com/java-util-concurrent/atomicreference.html -- http://tutorials.jenkov.com/java-util-concurrent/atomicstampedreference.htm \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/07.Java\347\272\277\347\250\213\346\261\240.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/07.Java\347\272\277\347\250\213\346\261\240.md" deleted file mode 100644 index 2ef4609b57..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/07.Java\347\272\277\347\250\213\346\261\240.md" +++ /dev/null @@ -1,521 +0,0 @@ ---- -title: Java线程池 -date: 2019-12-24 23:52:25 -order: 07 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 - - 线程池 -permalink: /pages/ad9680/ ---- - -# Java 线程池 - -## 简介 - -### 什么是线程池 - -线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。 - -### 为什么要用线程池 - -如果并发请求数量很多,但每个线程执行的时间很短,就会出现频繁的创建和销毁线程。如此一来,会大大降低系统的效率,可能频繁创建和销毁线程的时间、资源开销要大于实际工作的所需。 - -正是由于这个问题,所以有必要引入线程池。使用 **线程池的好处** 有以下几点: - -- **降低资源消耗** - 通过重复利用已创建的线程降低线程创建和销毁造成的消耗。 -- **提高响应速度** - 当任务到达时,任务可以不需要等到线程创建就能立即执行。 -- **提高线程的可管理性** - 线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如指掌。 - -## Executor 框架 - -> Executor 框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架,目的是提供一种将”任务提交”与”任务如何运行”分离开来的机制。 - -### 核心 API 概述 - -Executor 框架核心 API 如下: - -- `Executor` - 运行任务的简单接口。 -- `ExecutorService` - 扩展了 `Executor` 接口。扩展能力: - - 支持有返回值的线程; - - 支持管理线程的生命周期。 -- `ScheduledExecutorService` - 扩展了 `ExecutorService` 接口。扩展能力:支持定期执行任务。 -- `AbstractExecutorService` - `ExecutorService` 接口的默认实现。 -- `ThreadPoolExecutor` - Executor 框架最核心的类,它继承了 `AbstractExecutorService` 类。 -- `ScheduledThreadPoolExecutor` - `ScheduledExecutorService` 接口的实现,一个可定时调度任务的线程池。 -- `Executors` - 可以通过调用 `Executors` 的静态工厂方法来创建线程池并返回一个 `ExecutorService` 对象。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/exexctor-uml.png) - -### Executor - -`Executor` 接口中只定义了一个 `execute` 方法,用于接收一个 `Runnable` 对象。 - -```java -public interface Executor { - void execute(Runnable command); -} -``` - -### ExecutorService - -`ExecutorService` 接口继承了 `Executor` 接口,它还提供了 `invokeAll`、`invokeAny`、`shutdown`、`submit` 等方法。 - -```java -public interface ExecutorService extends Executor { - - void shutdown(); - - List shutdownNow(); - - boolean isShutdown(); - - boolean isTerminated(); - - boolean awaitTermination(long timeout, TimeUnit unit) - throws InterruptedException; - - Future submit(Callable task); - - Future submit(Runnable task, T result); - - Future submit(Runnable task); - - List> invokeAll(Collection> tasks) - throws InterruptedException; - - List> invokeAll(Collection> tasks, - long timeout, TimeUnit unit) - throws InterruptedException; - - T invokeAny(Collection> tasks) - throws InterruptedException, ExecutionException; - - T invokeAny(Collection> tasks, - long timeout, TimeUnit unit) - throws InterruptedException, ExecutionException, TimeoutException; -} -``` - -从其支持的方法定义,不难看出:相比于 `Executor` 接口,`ExecutorService` 接口主要的扩展是: - -- 支持有返回值的线程 - `sumbit`、`invokeAll`、`invokeAny` 方法中都支持传入`Callable` 对象。 -- 支持管理线程生命周期 - `shutdown`、`shutdownNow`、`isShutdown` 等方法。 - -### ScheduledExecutorService - -`ScheduledExecutorService` 接口扩展了 `ExecutorService` 接口。 - -它除了支持前面两个接口的所有能力以外,还支持定时调度线程。 - -```java -public interface ScheduledExecutorService extends ExecutorService { - - public ScheduledFuture schedule(Runnable command, - long delay, TimeUnit unit); - - public ScheduledFuture schedule(Callable callable, - long delay, TimeUnit unit); - - public ScheduledFuture scheduleAtFixedRate(Runnable command, - long initialDelay, - long period, - TimeUnit unit); - - public ScheduledFuture scheduleWithFixedDelay(Runnable command, - long initialDelay, - long delay, - TimeUnit unit); - -} -``` - -其扩展的接口提供以下能力: - -- `schedule` 方法可以在指定的延时后执行一个 `Runnable` 或者 `Callable` 任务。 -- `scheduleAtFixedRate` 方法和 `scheduleWithFixedDelay` 方法可以按照指定时间间隔,定期执行任务。 - -## ThreadPoolExecutor - -`java.uitl.concurrent.ThreadPoolExecutor` 类是 `Executor` 框架中最核心的类。所以,本文将着重讲述一下这个类。 - -### 重要字段 - -`ThreadPoolExecutor` 有以下重要字段: - -```java -private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); -private static final int COUNT_BITS = Integer.SIZE - 3; -private static final int CAPACITY = (1 << COUNT_BITS) - 1; -// runState is stored in the high-order bits -private static final int RUNNING = -1 << COUNT_BITS; -private static final int SHUTDOWN = 0 << COUNT_BITS; -private static final int STOP = 1 << COUNT_BITS; -private static final int TIDYING = 2 << COUNT_BITS; -private static final int TERMINATED = 3 << COUNT_BITS; -``` - -参数说明: - -- `ctl` - **用于控制线程池的运行状态和线程池中的有效线程数量**。它包含两部分的信息: - - 线程池的运行状态 (`runState`) - - 线程池内有效线程的数量 (`workerCount`) - - 可以看到,`ctl` 使用了 `Integer` 类型来保存,高 3 位保存 `runState`,低 29 位保存 `workerCount`。`COUNT_BITS` 就是 29,`CAPACITY` 就是 1 左移 29 位减 1(29 个 1),这个常量表示 `workerCount` 的上限值,大约是 5 亿。 -- 运行状态 - 线程池一共有五种运行状态: - - `RUNNING` - **运行状态**。接受新任务,并且也能处理阻塞队列中的任务。 - - `SHUTDOWN` - **关闭状态**。不接受新任务,但可以处理阻塞队列中的任务。 - - 在线程池处于 `RUNNING` 状态时,调用 `shutdown` 方法会使线程池进入到该状态。 - - `finalize` 方法在执行过程中也会调用 `shutdown` 方法进入该状态。 - - `STOP` - **停止状态**。不接受新任务,也不处理队列中的任务。会中断正在处理任务的线程。在线程池处于 `RUNNING` 或 `SHUTDOWN` 状态时,调用 `shutdownNow` 方法会使线程池进入到该状态。 - - `TIDYING` - **整理状态**。如果所有的任务都已终止了,`workerCount` (有效线程数) 为 0,线程池进入该状态后会调用 `terminated` 方法进入 `TERMINATED` 状态。 - - `TERMINATED` - **已终止状态**。在 `terminated` 方法执行完后进入该状态。默认 `terminated` 方法中什么也没有做。进入 `TERMINATED` 的条件如下: - - 线程池不是 `RUNNING` 状态; - - 线程池状态不是 `TIDYING` 状态或 `TERMINATED` 状态; - - 如果线程池状态是 `SHUTDOWN` 并且 `workerQueue` 为空; - - `workerCount` 为 0; - - 设置 `TIDYING` 状态成功。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-thread-pool_2.png) - -### 构造方法 - -`ThreadPoolExecutor` 有四个构造方法,前三个都是基于第四个实现。第四个构造方法定义如下: - -```java -public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) { -``` - -参数说明: - -- `corePoolSize` - **核心线程数量**。当有新任务通过 `execute` 方法提交时 ,线程池会执行以下判断: - - 如果运行的线程数少于 `corePoolSize`,则创建新线程来处理任务,即使线程池中的其他线程是空闲的。 - - 如果线程池中的线程数量大于等于 `corePoolSize` 且小于 `maximumPoolSize`,则只有当 `workQueue` 满时才创建新的线程去处理任务; - - 如果设置的 `corePoolSize` 和 `maximumPoolSize` 相同,则创建的线程池的大小是固定的。这时如果有新任务提交,若 `workQueue` 未满,则将请求放入 `workQueue` 中,等待有空闲的线程去从 `workQueue` 中取任务并处理; - - 如果运行的线程数量大于等于 `maximumPoolSize`,这时如果 `workQueue` 已经满了,则使用 `handler` 所指定的策略来处理任务; - - 所以,任务提交时,判断的顺序为 `corePoolSize` => `workQueue` => `maximumPoolSize`。 -- `maximumPoolSize` - **最大线程数量**。 - - 如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。 - - 值得注意的是:如果使用了无界的任务队列这个参数就没什么效果。 -- `keepAliveTime`:**线程保持活动的时间**。 - - 当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`。 - - 所以,如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。 -- `unit` - **`keepAliveTime` 的时间单位**。有 7 种取值。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。 -- `workQueue` - **等待执行的任务队列**。用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。 - - `ArrayBlockingQueue` - **有界阻塞队列**。 - - 此队列是**基于数组的先进先出队列(FIFO)**。 - - 此队列创建时必须指定大小。 - - `LinkedBlockingQueue` - **无界阻塞队列**。 - - 此队列是**基于链表的先进先出队列(FIFO)**。 - - 如果创建时没有指定此队列大小,则默认为 `Integer.MAX_VALUE`。 - - 吞吐量通常要高于 `ArrayBlockingQueue`。 - - 使用 `LinkedBlockingQueue` 意味着: `maximumPoolSize` 将不起作用,线程池能创建的最大线程数为 `corePoolSize`,因为任务等待队列是无界队列。 - - `Executors.newFixedThreadPool` 使用了这个队列。 - - `SynchronousQueue` - **不会保存提交的任务,而是将直接新建一个线程来执行新来的任务**。 - - 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。 - - 吞吐量通常要高于 `LinkedBlockingQueue`。 - - `Executors.newCachedThreadPool` 使用了这个队列。 - - `PriorityBlockingQueue` - **具有优先级的无界阻塞队列**。 -- `threadFactory` - **线程工厂**。可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 -- `handler` - **饱和策略**。它是 `RejectedExecutionHandler` 类型的变量。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。线程池支持以下策略: - - `AbortPolicy` - 丢弃任务并抛出异常。这也是默认策略。 - - `DiscardPolicy` - 丢弃任务,但不抛出异常。 - - `DiscardOldestPolicy` - 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。 - - `CallerRunsPolicy` - 直接调用 `run` 方法并且阻塞执行。 - - 如果以上策略都不能满足需要,也可以通过实现 `RejectedExecutionHandler` 接口来定制处理策略。如记录日志或持久化不能处理的任务。 - -### execute 方法 - -默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务之后才会创建线程。 - -提交任务可以使用 `execute` 方法,它是 `ThreadPoolExecutor` 的核心方法,通过这个方法可以**向线程池提交一个任务,交由线程池去执行**。 - -`execute` 方法工作流程如下: - -1. 如果 `workerCount < corePoolSize`,则创建并启动一个线程来执行新提交的任务; -2. 如果 `workerCount >= corePoolSize`,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中; -3. 如果 `workerCount >= corePoolSize && workerCount < maximumPoolSize`,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务; -4. 如果`workerCount >= maximumPoolSize`,并且线程池内的阻塞队列已满,则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-thread-pool_1.png) - -### 其他重要方法 - -在 `ThreadPoolExecutor` 类中还有一些重要的方法: - -- `submit` - 类似于 `execute`,但是针对的是有返回值的线程。`submit` 方法是在 `ExecutorService` 中声明的方法,在 `AbstractExecutorService` 就已经有了具体的实现。`ThreadPoolExecutor` 直接复用 `AbstractExecutorService` 的 `submit` 方法。 -- `shutdown` - 不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务。 - - 将线程池切换到 `SHUTDOWN` 状态; - - 并调用 `interruptIdleWorkers` 方法请求中断所有空闲的 worker; - - 最后调用 `tryTerminate` 尝试结束线程池。 -- `shutdownNow` - 立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务。与 `shutdown` 方法类似,不同的地方在于: - - 设置状态为 `STOP`; - - 中断所有工作线程,无论是否是空闲的; - - 取出阻塞队列中没有被执行的任务并返回。 -- `isShutdown` - 调用了 `shutdown` 或 `shutdownNow` 方法后,`isShutdown` 方法就会返回 true。 -- `isTerminaed` - 当所有的任务都已关闭后,才表示线程池关闭成功,这时调用 `isTerminaed` 方法会返回 true。 -- `setCorePoolSize` - 设置核心线程数大小。 -- `setMaximumPoolSize` - 设置最大线程数大小。 -- `getTaskCount` - 线程池已经执行的和未执行的任务总数; -- `getCompletedTaskCount` - 线程池已完成的任务数量,该值小于等于 `taskCount`; -- `getLargestPoolSize` - 线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过,也就是达到了 `maximumPoolSize`; -- `getPoolSize` - 线程池当前的线程数量; -- `getActiveCount` - 当前线程池中正在执行任务的线程数量。 - -### 使用示例 - -```java -public class ThreadPoolExecutorDemo { - - public static void main(String[] args) { - ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 500, TimeUnit.MILLISECONDS, - new LinkedBlockingQueue(), - Executors.defaultThreadFactory(), - new ThreadPoolExecutor.AbortPolicy()); - - for (int i = 0; i < 100; i++) { - threadPoolExecutor.execute(new MyThread()); - String info = String.format("线程池中线程数目:%s,队列中等待执行的任务数目:%s,已执行玩别的任务数目:%s", - threadPoolExecutor.getPoolSize(), - threadPoolExecutor.getQueue().size(), - threadPoolExecutor.getCompletedTaskCount()); - System.out.println(info); - } - threadPoolExecutor.shutdown(); - } - - static class MyThread implements Runnable { - - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 执行"); - } - - } - -} -``` - -## Executors - -JDK 的 `Executors` 类中提供了几种具有代表性的线程池,这些线程池 **都是基于 `ThreadPoolExecutor` 的定制化实现**。 - -在实际使用线程池的场景中,我们往往不是直接使用 `ThreadPoolExecutor` ,而是使用 JDK 中提供的具有代表性的线程池实例。 - -### newSingleThreadExecutor - -**创建一个单线程的线程池**。 - -只会创建唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 **如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它** 。 - -单工作线程最大的特点是:**可保证顺序地执行各个任务**。 - -示例: - -```java -public class SingleThreadExecutorDemo { - - public static void main(String[] args) { - ExecutorService executorService = Executors.newSingleThreadExecutor(); - for (int i = 0; i < 100; i++) { - executorService.execute(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 执行"); - } - }); - } - executorService.shutdown(); - } - -} -``` - -### newFixedThreadPool - -**创建一个固定大小的线程池**。 - -**每次提交一个任务就会新创建一个工作线程,如果工作线程数量达到线程池最大线程数,则将提交的任务存入到阻塞队列中**。 - -`FixedThreadPool` 是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。 - -示例: - -```java -public class FixedThreadPoolDemo { - - public static void main(String[] args) { - ExecutorService executorService = Executors.newFixedThreadPool(3); - for (int i = 0; i < 100; i++) { - executorService.execute(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 执行"); - } - }); - } - executorService.shutdown(); - } - -} -``` - -### newCachedThreadPool - -**创建一个可缓存的线程池**。 - -- 如果线程池大小超过处理任务所需要的线程数,就会回收部分空闲的线程; -- 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为 1 分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 -- 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。 因此,使用 `CachedThreadPool` 时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。 - -示例: - -```java -public class CachedThreadPoolDemo { - - public static void main(String[] args) { - ExecutorService executorService = Executors.newCachedThreadPool(); - for (int i = 0; i < 100; i++) { - executorService.execute(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 执行"); - } - }); - } - executorService.shutdown(); - } - -} -``` - -### newScheduleThreadPool - -创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 - -```java -public class ScheduledThreadPoolDemo { - - public static void main(String[] args) { - schedule(); - scheduleAtFixedRate(); - } - - private static void schedule() { - ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5); - for (int i = 0; i < 100; i++) { - executorService.schedule(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 执行"); - } - }, 1, TimeUnit.SECONDS); - } - executorService.shutdown(); - } - - private static void scheduleAtFixedRate() { - ScheduledExecutorService executorService = Executors.newScheduledThreadPool(5); - for (int i = 0; i < 100; i++) { - executorService.scheduleAtFixedRate(new Runnable() { - @Override - public void run() { - System.out.println(Thread.currentThread().getName() + " 执行"); - } - }, 1, 1, TimeUnit.SECONDS); - } - executorService.shutdown(); - } - -} -``` - -### newWorkStealingPool - -Java 8 才引入。 - -其内部会构建 `ForkJoinPool`,利用 [Work-Stealing](https://en.wikipedia.org/wiki/Work_stealing) 算法,并行地处理任务,不保证处理顺序。 - -## 线程池最佳实践 - -### 计算线程数量 - -一般多线程执行的任务类型可以分为 CPU 密集型和 I/O 密集型,根据不同的任务类型,我们计算线程数的方法也不一样。 - -**CPU 密集型任务:**这种任务消耗的主要是 CPU 资源,可以将线程数设置为 N(CPU 核心数)+1,比 CPU 核心数多出来的一个线程是为了防止线程偶发的缺页中断,或者其它原因导致的任务暂停而带来的影响。一旦任务暂停,CPU 就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用 CPU 的空闲时间。 - -**I/O 密集型任务:**这种任务应用起来,系统会用大部分的时间来处理 I/O 交互,而线程在处理 I/O 的时间段内不会占用 CPU 来处理,这时就可以将 CPU 交出给其它线程使用。因此在 I/O 密集型任务的应用中,我们可以多配置一些线程,具体的计算方法是 2N。 - -### 建议使用有界阻塞队列 - -不建议使用 `Executors` 的最重要的原因是:`Executors` 提供的很多方法默认使用的都是无界的 `LinkedBlockingQueue`,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以**强烈建议使用有界队列**。 - -《阿里巴巴 Java 开发手册》中提到,禁止使用这些方法来创建线程池,而应该手动 `new ThreadPoolExecutor` 来创建线程池。制订这条规则是因为容易导致生产事故,最典型的就是 `newFixedThreadPool` 和 `newCachedThreadPool`,可能因为资源耗尽导致 OOM 问题。 - -【示例】`newFixedThreadPool` OOM - -```java -ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newFixedThreadPool(1); -printStats(threadPool); -for (int i = 0; i < 100000000; i++) { - threadPool.execute(() -> { - String payload = IntStream.rangeClosed(1, 1000000) - .mapToObj(__ -> "a") - .collect(Collectors.joining("")) + UUID.randomUUID().toString(); - try { - TimeUnit.HOURS.sleep(1); - } catch (InterruptedException e) { - } - log.info(payload); - }); -} - -threadPool.shutdown(); -threadPool.awaitTermination(1, TimeUnit.HOURS); -``` - -`newFixedThreadPool` 使用的工作队列是 `LinkedBlockingQueue` ,而默认构造方法的 `LinkedBlockingQueue` 是一个 `Integer.MAX_VALUE` 长度的队列,可以认为是无界的。如果任务较多并且执行较慢的话,队列可能会快速积压,撑爆内存导致 OOM。 - -【示例】`newCachedThreadPool` OOM - -```java -ThreadPoolExecutor threadPool = (ThreadPoolExecutor) Executors.newCachedThreadPool(); -printStats(threadPool); -for (int i = 0; i < 100000000; i++) { - threadPool.execute(() -> { - String payload = UUID.randomUUID().toString(); - try { - TimeUnit.HOURS.sleep(1); - } catch (InterruptedException e) { - } - log.info(payload); - }); -} -threadPool.shutdown(); -threadPool.awaitTermination(1, TimeUnit.HOURS); -``` - -`newCachedThreadPool` 的最大线程数是 `Integer.MAX_VALUE`,可以认为是没有上限的,而其工作队列 `SynchronousQueue` 是一个没有存储空间的阻塞队列。这意味着,只要有请求到来,就必须找到一条工作线程来处理,如果当前没有空闲的线程就再创建一条新的。 - -如果大量的任务进来后会创建大量的线程。我们知道线程是需要分配一定的内存空间作为线程栈的,比如 1MB,因此无限制创建线程必然会导致 OOM。 - -### 重要任务应该自定义拒绝策略 - -使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw `RejectedExecutionException` 这是个运行时异常,对于运行时异常编译器并不强制 `catch` 它,所以开发人员很容易忽略。因此**默认拒绝策略要慎重使用**。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。 - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [深入理解 Java 线程池:ThreadPoolExecutor](https://www.jianshu.com/p/d2729853c4da) -- [java 并发编程--Executor 框架](https://www.cnblogs.com/MOBIN/p/5436482.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/08.Java\345\271\266\345\217\221\345\267\245\345\205\267\347\261\273.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/08.Java\345\271\266\345\217\221\345\267\245\345\205\267\347\261\273.md" deleted file mode 100644 index ddc3c82693..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/08.Java\345\271\266\345\217\221\345\267\245\345\205\267\347\261\273.md" +++ /dev/null @@ -1,282 +0,0 @@ ---- -title: Java并发工具类 -date: 2019-12-24 23:52:25 -order: 08 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 -permalink: /pages/02d274/ ---- - -# Java 并发工具类 - -> JDK 的 `java.util.concurrent` 包(即 J.U.C)中提供了几个非常有用的并发工具类。 - -## CountDownLatch - -> 字面意思为 **递减计数锁**。用于**控制一个线程等待多个线程**。 -> -> `CountDownLatch` 维护一个计数器 count,表示需要等待的事件数量。`countDown` 方法递减计数器,表示有一个事件已经发生。调用 `await` 方法的线程会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/CountDownLatch.png) - -`CountDownLatch` 是基于 AQS(`AbstractQueuedSynchronizer`) 实现的。 - -`CountDownLatch` 唯一的构造方法: - -```java -// 初始化计数器 -public CountDownLatch(int count) {}; -``` - -说明: - -- count 为统计值。 - -`CountDownLatch` 的重要方法: - -```java -public void await() throws InterruptedException { }; -public boolean await(long timeout, TimeUnit unit) throws InterruptedException { }; -public void countDown() { }; -``` - -说明: - -- `await()` - 调用 `await()` 方法的线程会被挂起,它会等待直到 count 值为 0 才继续执行。 -- `await(long timeout, TimeUnit unit)` - 和 `await()` 类似,只不过等待一定的时间后 count 值还没变为 0 的话就会继续执行 -- `countDown()` - 将统计值 count 减 1 - -示例: - -```java -public class CountDownLatchDemo { - - public static void main(String[] args) { - final CountDownLatch latch = new CountDownLatch(2); - - new Thread(new MyThread(latch)).start(); - new Thread(new MyThread(latch)).start(); - - try { - System.out.println("等待2个子线程执行完毕..."); - latch.await(); - System.out.println("2个子线程已经执行完毕"); - System.out.println("继续执行主线程"); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - - static class MyThread implements Runnable { - - private CountDownLatch latch; - - public MyThread(CountDownLatch latch) { - this.latch = latch; - } - - @Override - public void run() { - System.out.println("子线程" + Thread.currentThread().getName() + "正在执行"); - try { - Thread.sleep(1000); - } catch (InterruptedException e) { - e.printStackTrace(); - } - System.out.println("子线程" + Thread.currentThread().getName() + "执行完毕"); - latch.countDown(); - } - - } - -} -``` - -## CyclicBarrier - -> 字面意思是 **循环栅栏**。**`CyclicBarrier` 可以让一组线程等待至某个状态(遵循字面意思,不妨称这个状态为栅栏)之后再全部同时执行**。之所以叫循环栅栏是因为:**当所有等待线程都被释放以后,`CyclicBarrier` 可以被重用**。 -> -> `CyclicBarrier` 维护一个计数器 count。每次执行 `await` 方法之后,count 加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。 - -`CyclicBarrier` 是基于 `ReentrantLock` 和 `Condition` 实现的。 - -`CyclicBarrier` 应用场景:`CyclicBarrier` 在并行迭代算法中非常有用。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/CyclicBarrier.png) - -`CyclicBarrier` 提供了 2 个构造方法 - -```java -public CyclicBarrier(int parties) {} -public CyclicBarrier(int parties, Runnable barrierAction) {} -``` - -> 说明: -> -> - `parties` - `parties` 数相当于一个阈值,当有 `parties` 数量的线程在等待时, `CyclicBarrier` 处于栅栏状态。 -> - `barrierAction` - 当 `CyclicBarrier` 处于栅栏状态时执行的动作。 - -`CyclicBarrier` 的重要方法: - -```java -public int await() throws InterruptedException, BrokenBarrierException {} -public int await(long timeout, TimeUnit unit) - throws InterruptedException, - BrokenBarrierException, - TimeoutException {} -// 将屏障重置为初始状态 -public void reset() {} -``` - -> 说明: -> -> - `await()` - 等待调用 `await()` 的线程数达到屏障数。如果当前线程是最后一个到达的线程,并且在构造函数中提供了非空屏障操作,则当前线程在允许其他线程继续之前运行该操作。如果在屏障动作期间发生异常,那么该异常将在当前线程中传播并且屏障被置于断开状态。 -> - `await(long timeout, TimeUnit unit)` - 相比于 `await()` 方法,这个方法让这些线程等待至一定的时间,如果还有线程没有到达栅栏状态就直接让到达栅栏状态的线程执行后续任务。 -> - `reset()` - 将屏障重置为初始状态。 - -示例: - -```java -public class CyclicBarrierDemo { - - final static int N = 4; - - public static void main(String[] args) { - CyclicBarrier barrier = new CyclicBarrier(N, - new Runnable() { - @Override - public void run() { - System.out.println("当前线程" + Thread.currentThread().getName()); - } - }); - - for (int i = 0; i < N; i++) { - MyThread myThread = new MyThread(barrier); - new Thread(myThread).start(); - } - } - - static class MyThread implements Runnable { - - private CyclicBarrier cyclicBarrier; - - MyThread(CyclicBarrier cyclicBarrier) { - this.cyclicBarrier = cyclicBarrier; - } - - @Override - public void run() { - System.out.println("线程" + Thread.currentThread().getName() + "正在写入数据..."); - try { - Thread.sleep(3000); // 以睡眠来模拟写入数据操作 - System.out.println("线程" + Thread.currentThread().getName() + "写入数据完毕,等待其他线程写入完毕"); - cyclicBarrier.await(); - } catch (InterruptedException | BrokenBarrierException e) { - e.printStackTrace(); - } - } - - } - -} -``` - -## Semaphore - -> 字面意思为 **信号量**。`Semaphore` 用来控制某段代码块的并发数。 -> -> `Semaphore` 管理着一组虚拟的许可(permit),permit 的初始数量可通过构造方法来指定。每次执行 `acquire` 方法可以获取一个 permit,如果没有就等待;而 `release` 方法可以释放一个 permit。 - -`Semaphore` 应用场景: - -- `Semaphore` 可以用于实现资源池,如数据库连接池。 -- `Semaphore` 可以用于将任何一种容器变成有界阻塞容器。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/Semaphore.png) - -`Semaphore` 提供了 2 个构造方法: - -```java -// 参数 permits 表示许可数目,即同时可以允许多少线程进行访问 -public Semaphore(int permits) {} -// 参数 fair 表示是否是公平的,即等待时间越久的越先获取许可 -public Semaphore(int permits, boolean fair) {} -``` - -> 说明: -> -> - `permits` - 初始化固定数量的 permit,并且默认为非公平模式。 -> - `fair` - 设置是否为公平模式。所谓公平,是指等待久的优先获取 permit。 - -`Semaphore`的重要方法: - -```java -// 获取 1 个许可 -public void acquire() throws InterruptedException {} -//获取 permits 个许可 -public void acquire(int permits) throws InterruptedException {} -// 释放 1 个许可 -public void release() {} -//释放 permits 个许可 -public void release(int permits) {} -``` - -说明: - -- `acquire()` - 获取 1 个 permit。 -- `acquire(int permits)` - 获取 permits 数量的 permit。 -- `release()` - 释放 1 个 permit。 -- `release(int permits)` - 释放 permits 数量的 permit。 - -示例: - -```java -public class SemaphoreDemo { - - private static final int THREAD_COUNT = 30; - - private static ExecutorService threadPool = Executors.newFixedThreadPool(THREAD_COUNT); - - private static Semaphore semaphore = new Semaphore(10); - - public static void main(String[] args) { - for (int i = 0; i < THREAD_COUNT; i++) { - threadPool.execute(new Runnable() { - @Override - public void run() { - try { - semaphore.acquire(); - System.out.println("save data"); - semaphore.release(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - }); - } - - threadPool.shutdown(); - } - -} -``` - -## 总结 - -- `CountDownLatch` 和 `CyclicBarrier` 都能够实现线程之间的等待,只不过它们侧重点不同: - - `CountDownLatch` 一般用于某个线程 A 等待若干个其他线程执行完任务之后,它才执行; - - `CyclicBarrier` 一般用于一组线程互相等待至某个状态,然后这一组线程再同时执行; - - 另外,`CountDownLatch` 是不可以重用的,而 `CyclicBarrier` 是可以重用的。 -- `Semaphore` 其实和锁有点类似,它一般用于控制对某组资源的访问权限。 - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [Java 并发编程:CountDownLatch、CyclicBarrier 和 Semaphore](https://www.cnblogs.com/dolphin0520/p/3920397.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/09.Java\345\206\205\345\255\230\346\250\241\345\236\213.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/09.Java\345\206\205\345\255\230\346\250\241\345\236\213.md" deleted file mode 100644 index 780d3af230..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/09.Java\345\206\205\345\255\230\346\250\241\345\236\213.md" +++ /dev/null @@ -1,311 +0,0 @@ ---- -title: Java内存模型 -date: 2020-12-25 18:43:11 -order: 09 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 -permalink: /pages/d4e06f/ ---- - -# Java 内存模型 - -> **关键词**:`JMM`、`volatile`、`synchronized`、`final`、`Happens-Before`、`内存屏障` -> -> **摘要**:Java 内存模型(Java Memory Model),简称 **JMM**。Java 内存模型的目标是为了解决由可见性和有序性导致的并发安全问题。Java 内存模型通过 **屏蔽各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果**。 - -## 物理内存模型 - -物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机对并发的处理方案对于虚拟机的实现也有相当大的参考意义。 - -### 硬件处理效率 - -物理内存的第一个问题是:硬件处理效率。 - -- 绝大多数的运算任务都不可能只靠处理器“计算”就能完成,处理器至少需要与**内存交互**,如读取运算数据、存储运算结果,这个 I/O 操作是很难消除的(无法仅靠寄存器完成所有运算任务)。 -- **由于计算机的存储设备与处理器的运算速度有几个数量级的差距** ,这种速度上的矛盾,会降低硬件的处理效率。所以,现代计算机都不得不 **加入高速缓存(Cache) 来作为内存和处理器之间的缓冲**。将需要用到的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步会内存中,这样处理器就无需等待缓慢的内存读写了。 - -### 缓存一致性 - -高速缓存解决了 **硬件效率问题**,但是引入了一个新的问题:**缓存一致性(Cache Coherence)**。 - -在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。 - -为了解决缓存一致性问题,**需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作**。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102230327.png) - -### 代码乱序执行优化 - -**除了高速缓存以外,为了使得处理器内部的运算单元尽量被充分利用**,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化。处理器会在计算之后将乱序执行的结果重组,**保证该结果与顺序执行的结果是一致的**,但不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102223609.png) - -乱序执行技术是处理器为提高运算速度而做出违背代码原有顺序的优化。 - -- **单核**环境下,处理器保证做出的优化不会导致执行结果远离预期目标,但在多核环境下却并非如此。 -- **多核**环境下, 如果存在一个核的计算任务依赖另一个核的计算任务的中间结果,而且对相关数据读写没做任何防护措施,那么其顺序性并不能靠代码的先后顺序来保证。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102224144.png) - -## Java 内存模型 - -**`内存模型`** 这个概念。我们可以理解为:**在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象**。不同架构的物理计算机可以有不一样的内存模型,JVM 也有自己的内存模型。 - -JVM 中试图定义一种 Java 内存模型(Java Memory Model, JMM)来**屏蔽各种硬件和操作系统的内存访问差异**,以实现让 Java 程序 **在各种平台下都能达到一致的内存访问效果**。 - -在 [Java 并发简介](https://dunwu.github.io/waterdrop/pages/f6b642/) 中已经介绍了,并发安全需要满足可见性、有序性、原子性。其中,导致可见性的原因是缓存,导致有序性的原因是编译优化。那解决可见性、有序性最直接的办法就是**禁用缓存和编译优化** 。但这么做,性能就堪忧了。 - -合理的方案应该是**按需禁用缓存以及编译优化**。那么,如何做到呢?,Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 **volatile**、**synchronized** 和 **final** 三个关键字,以及 **Happens-Before 规则**。 - -### 主内存和工作内存 - -JMM 的主要目标是 **定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节**。此处的变量(Variables)与 Java 编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数值对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,自然就不会存在竞争问题。为了获得较好的执行效能,JMM 并没有限制执行引擎使用处理器的特定寄存器或缓存来和主存进行交互,也没有限制即使编译器进行调整代码执行顺序这类优化措施。 - -JMM 规定了**所有的变量都存储在主内存(Main Memory)中**。 - -每条线程还有自己的工作内存(Working Memory),**工作内存中保留了该线程使用到的变量的主内存的副本**。工作内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102225839.png) - -线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程间也无法直接访问对方工作内存中的变量,**线程间变量值的传递均需要通过主内存来完成**。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102225657.png) - -> 说明: -> -> 这里说的主内存、工作内存与 Java 内存区域中的堆、栈、方法区等不是同一个层次的内存划分。 - -### JMM 内存操作的问题 - -类似于物理内存模型面临的问题,JMM 存在以下两个问题: - -- **工作内存数据一致性** - 各个线程操作数据时会保存使用到的主内存中的共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致各自的的共享变量副本不一致。如果真的发生这种情况,数据同步回主内存以谁的副本数据为准? Java 内存模型主要通过一系列的数据同步协议、规则来保证数据的一致性。 -- **指令重排序优化** - Java 中重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:**编译期重排序和运行期重排序**,分别对应编译时和运行时环境。 同样的,指令重排序不是随意重排序,它需要满足以下两个条件: - - 在单线程环境下不能改变程序运行的结果。即时编译器(和处理器)需要保证程序能够遵守 `as-if-serial` 属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。 - - 存在数据依赖关系的不允许重排序。 - - 多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同。 - -### 内存间交互操作 - -JMM 定义了 8 个操作来完成主内存和工作内存之间的交互操作。JVM 实现时必须保证下面介绍的每种操作都是 **原子的**(对于 double 和 long 型的变量来说,load、store、read、和 write 操作在某些平台上允许有例外 )。 - -- `lock` (锁定) - 作用于**主内存**的变量,它把一个变量标识为一条线程独占的状态。 -- `unlock` (解锁) - 作用于**主内存**的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 -- `read` (读取) - 作用于**主内存**的变量,它把一个变量的值从主内存**传输**到线程的工作内存中,以便随后的 `load` 动作使用。 -- `write` (写入) - 作用于**主内存**的变量,它把 store 操作从工作内存中得到的变量的值放入主内存的变量中。 -- `load` (载入) - 作用于**工作内存**的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。 -- `use` (使用) - 作用于**工作内存**的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值得字节码指令时就会执行这个操作。 -- `assign` (赋值) - 作用于**工作内存**的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 -- `store` (存储) - 作用于**工作内存**的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 `write` 操作使用。 - -如果要把一个变量从主内存中复制到工作内存,就**需要按序执行 `read` 和 `load` 操作**;如果把变量从工作内存中同步回主内存中,就**需要按序执行 `store` 和 `write` 操作**。但 Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。 - -JMM 还规定了上述 8 种基本操作,需要满足以下规则: - -- **read 和 load 必须成对出现**;**store 和 write 必须成对出现**。即不允许一个变量从主内存读取了但工作内存不接受,或从工作内存发起回写了但主内存不接受的情况出现。 -- **不允许一个线程丢弃它的最近 assign 的操作**,即变量在工作内存中改变了之后必须把变化同步到主内存中。 -- **不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中**。 -- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。换句话说,就是对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。 -- 一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 **lock 和 unlock 必须成对出现**。 -- 如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行 load 或 assign 操作初始化变量的值。 -- 如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作,也不允许去 unlock 一个被其他线程锁定的变量。 -- 对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210102230708.png) - -### 并发安全特性 - -上文介绍了 Java 内存交互的 8 种基本操作,它们遵循 Java 内存三大特性:原子性、可见性、有序性。 - -而这三大特性,归根结底,是为了实现多线程的 **数据一致性**,使得程序在多线程并发,指令重排序优化的环境中能如预期运行。 - -#### 原子性 - -**原子性即一个操作或者多个操作,要么全部执行(执行的过程不会被任何因素打断),要么就都不执行**。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。 - -在 Java 中,为了保证原子性,提供了两个高级的字节码指令 `monitorenter` 和 `monitorexit`。这两个字节码,在 Java 中对应的关键字就是 `synchronized`。 - -因此,在 Java 中可以使用 `synchronized` 来保证方法和代码块内的操作是原子性的。 - -#### 可见性 - -**可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值**。 - -JMM 是通过 **"变量修改后将新值同步回主内存**, **变量读取前从主内存刷新变量值"** 这种依赖主内存作为传递媒介的方式来实现的。 - -Java 实现多线程可见性的方式有: - -- `volatile` -- `synchronized` -- `final` - -#### 有序性 - -有序性规则表现在以下两种场景: 线程内和线程间 - -- 线程内 - 从某个线程的角度看方法的执行,指令会按照一种叫“串行”(`as-if-serial`)的方式执行,此种方式已经应用于顺序编程语言。 -- 线程间 - 这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(`synchronized` 关键字修饰)以及 `volatile` 字段的操作仍维持相对有序。 - -在 Java 中,可以使用 `synchronized` 和 `volatile` 来保证多线程之间操作的有序性。实现方式有所区别: - -- `volatile` 关键字会禁止指令重排序。 -- `synchronized` 关键字通过互斥保证同一时刻只允许一条线程操作。 - -## Happens-Before - -JMM 为程序中所有的操作定义了一个偏序关系,称之为 **`先行发生原则(Happens-Before)`**。 - -**Happens-Before** 是指 **前面一个操作的结果对后续操作是可见的**。 - -**Happens-Before** 非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作间是否可能存在冲突的所有问题。 - -- **程序次序规则** - 一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。 -- **锁定规则** - 一个 `unLock` 操作先行发生于后面对同一个锁的 `lock` 操作。 -- **volatile 变量规则** - 对一个 `volatile` 变量的写操作先行发生于后面对这个变量的读操作。 -- **线程启动规则** - `Thread` 对象的 `start()` 方法先行发生于此线程的每个一个动作。 -- **线程终止规则** - 线程中所有的操作都先行发生于线程的终止检测,我们可以通过 `Thread.join()` 方法结束、`Thread.isAlive()` 的返回值手段检测到线程已经终止执行。 -- **线程中断规则** - 对线程 `interrupt()` 方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 `Thread.interrupted()` 方法检测到是否有中断发生。 -- **对象终结规则** - 一个对象的初始化完成先行发生于它的 `finalize()` 方法的开始。 -- **传递性** - 如果操作 A 先行发生于 操作 B,而操作 B 又 先行发生于 操作 C,则可以得出操作 A 先行发生于 操作 C。 - -## 内存屏障 - -Java 中如何保证底层操作的有序性和可见性?可以通过内存屏障(memory barrier)。 - -内存屏障是被插入两个 CPU 指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障**有序性**的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障**可见性**。 - -举个例子: - -``` -Store1; -Store2; -Load1; -StoreLoad; //内存屏障 -Store3; -Load2; -Load3; -复制代码 -``` - -对于上面的一组 CPU 指令(Store 表示写入指令,Load 表示读取指令),StoreLoad 屏障之前的 Store 指令无法与 StoreLoad 屏障之后的 Load 指令进行交换位置,即**重排序**。但是 StoreLoad 屏障之前和之后的指令是可以互换位置的,即 Store1 可以和 Store2 互换,Load2 可以和 Load3 互换。 - -常见有 4 种屏障 - -- `LoadLoad` 屏障 - 对于这样的语句 `Load1; LoadLoad; Load2`,在 Load2 及后续读取操作要读取的数据被访问前,保证 Load1 要读取的数据被读取完毕。 -- `StoreStore` 屏障 - 对于这样的语句 `Store1; StoreStore; Store2`,在 Store2 及后续写入操作执行前,保证 Store1 的写入操作对其它处理器可见。 -- `LoadStore` 屏障 - 对于这样的语句 `Load1; LoadStore; Store2`,在 Store2 及后续写入操作被执行前,保证 Load1 要读取的数据被读取完毕。 -- `StoreLoad` 屏障 - 对于这样的语句 `Store1; StoreLoad; Load2`,在 Load2 及后续所有读取操作执行前,保证 Store1 的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。 - -Java 中对内存屏障的使用在一般的代码中不太容易见到,常见的有 `volatile` 和 `synchronized` 关键字修饰的代码块(后面再展开介绍),还可以通过 `Unsafe` 这个类来使用内存屏障。 - -## volatile - -`volatile` 是 JVM 提供的 **最轻量级的同步机制**。 - -`volatile` 的中文意思是不稳定的,易变的,用 `volatile` 修饰变量是为了保证变量在多线程中的可见性。 - -#### volatile 变量的特性 - -`volatile` 变量具有两种特性: - -- 保证变量对所有线程的可见性。 -- 禁止进行指令重排序 - -##### 保证变量对所有线程的可见性 - -这里的可见性是指当一条线程修改了 volatile 变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。 - -**线程写 volatile 变量的过程:** - -1. 改变线程工作内存中 volatile 变量副本的值 -2. 将改变后的副本的值从工作内存刷新到主内存 - -**线程读 volatile 变量的过程:** - -1. 从主内存中读取 volatile 变量的最新值到线程的工作内存中 -2. 从工作内存中读取 volatile 变量的副本 - -> 注意:**保证可见性不等同于 volatile 变量保证并发操作的安全性** -> -> 在不符合以下两点的场景中,仍然要通过枷锁来保证原子性: -> -> - 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。 -> - 变量不需要与其他状态变量共同参与不变约束。 - -但是如果多个线程同时把更新后的变量值同时刷新回主内存,可能导致得到的值不是预期结果: - -举个例子: 定义 `volatile int count = 0`,2 个线程同时执行 count++ 操作,每个线程都执行 500 次,最终结果小于 1000,原因是每个线程执行 count++ 需要以下 3 个步骤: - -1. 线程从主内存读取最新的 count 的值 -2. 执行引擎把 count 值加 1,并赋值给线程工作内存 -3. 线程工作内存把 count 值保存到主内存 有可能某一时刻 2 个线程在步骤 1 读取到的值都是 100,执行完步骤 2 得到的值都是 101,最后刷新了 2 次 101 保存到主内存。 - -##### 语义 2 禁止进行指令重排序 - -具体一点解释,禁止重排序的规则如下: - -- 当程序执行到 `volatile` 变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行; -- 在进行指令优化时,不能将在对 `volatile` 变量访问的语句放在其后面执行,也不能把 `volatile` 变量后面的语句放到其前面执行。 - -普通的变量仅仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证赋值操作的顺序与程序代码中的执行顺序一致。 - -举个例子: - -```java -volatile boolean initialized = false; - -// 下面代码线程A中执行 -// 读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用 -doSomethingReadConfg(); -initialized = true; - -// 下面代码线程B中执行 -// 等待initialized 为true,代表线程A已经把配置信息初始化完成 -while (!initialized) { - sleep(); -} -// 使用线程A初始化好的配置信息 -doSomethingWithConfig(); -复制代码 -``` - -上面代码中如果定义 initialized 变量时没有使用 volatile 修饰,就有可能会由于指令重排序的优化,导致线程 A 中最后一句代码 "initialized = true" 在 “doSomethingReadConfg()” 之前被执行,这样会导致线程 B 中使用配置信息的代码就可能出现错误,而 volatile 关键字就禁止重排序的语义可以避免此类情况发生。 - -#### volatile 的原理 - -具体实现方式是在编译期生成字节码时,会在指令序列中增加内存屏障来保证,下面是基于保守策略的 JMM 内存屏障插入策略: - -- 在每个 volatile 写操作的前面插入一个 StoreStore 屏障。 该屏障除了保证了屏障之前的写操作和该屏障之后的写操作不能重排序,还会保证了 volatile 写操作之前,任何的读写操作都会先于 volatile 被提交。 -- 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障。 该屏障除了使 volatile 写操作不会与之后的读操作重排序外,还会刷新处理器缓存,使 volatile 变量的写更新对其他线程可见。 -- 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障。 该屏障除了使 volatile 读操作不会与之前的写操作发生重排序外,还会刷新处理器缓存,使 volatile 变量读取的为最新值。 -- 在每个 volatile 读操作的后面插入一个 LoadStore 屏障。 该屏障除了禁止了 volatile 读操作与其之后的任何写操作进行重排序,还会刷新处理器缓存,使其他线程 volatile 变量的写更新对 volatile 读操作的线程可见。 - -#### volatile 的使用场景 - -总结起来,就是“一次写入,到处读取”,某一线程负责更新变量,其他线程只读取变量(不更新变量),并根据变量的新值执行相应逻辑。例如状态标志位更新,观察者模型变量值发布。 - -## synchronized - -### long 和 double 变量的特殊规则 - -JMM 要求 lock、unlock、read、load、assign、use、store、write 这 8 种操作都具有原子性,但是对于 64 位的数据类型(long 和 double),在模型中特别定义相对宽松的规定:允许虚拟机将没有被 `volatile` 修饰的 64 位数据的读写操作分为 2 次 32 位的操作来进行,即允许虚拟机可选择不保证 64 位数据类型的 load、store、read 和 write 这 4 个操作的原子性。由于这种非原子性,有可能导致其他线程读到同步未完成的“32 位的半个变量”的值。 - -不过实际开发中,Java 内存模型强烈建议虚拟机把 64 位数据的读写实现为具有原子性,目前各种平台下的商用虚拟机都选择把 64 位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的 long 和 double 变量专门声明为 volatile。 - -### final 型量的特殊规则 - -我们知道,final 成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。 final 关键字的可见性是指:被 final 修饰的字段在声明时或者构造器中,一旦初始化完成,那么在其他线程无须同步就能正确看见 final 字段的值。这是因为一旦初始化完成,final 变量的值立刻回写到主内存。 - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [理解 Java 内存模型](https://juejin.im/post/5bf2977751882505d840321d) -- [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/10.ForkJoin\346\241\206\346\236\266.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/10.ForkJoin\346\241\206\346\236\266.md" deleted file mode 100644 index 6acee957da..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/10.ForkJoin\346\241\206\346\236\266.md" +++ /dev/null @@ -1,161 +0,0 @@ ---- -title: ForkJoin框架 -date: 2020-07-14 15:27:46 -order: 10 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 -permalink: /pages/edd121/ ---- - -# Java Fork Join 框架 - -**对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;而批量的并行任务,则可以通过 CompletionService 来解决。** - -## CompletableFuture - -### runAsync 和 supplyAsync 方法 - -CompletableFuture 提供了四个静态方法来创建一个异步操作。 - -```java -public static CompletableFuture runAsync(Runnable runnable) -public static CompletableFuture runAsync(Runnable runnable, Executor executor) -public static CompletableFuture supplyAsync(Supplier supplier) -public static CompletableFuture supplyAsync(Supplier supplier, Executor executor) -``` - -没有指定 Executor 的方法会使用 ForkJoinPool.commonPool() 作为它的线程池执行异步代码。如果指定线程池,则使用指定的线程池运行。以下所有的方法都类同。 - -- runAsync 方法不支持返回值。 -- supplyAsync 可以支持返回值。 - -## CompletionStage - -CompletionStage 接口可以清晰地描述任务之间的时序关系,如**串行关系、并行关系、汇聚关系**等。 - -### 串行关系 - -CompletionStage 接口里面描述串行关系,主要是 thenApply、thenAccept、thenRun 和 thenCompose 这四个系列的接口。 - -thenApply 系列函数里参数 fn 的类型是接口 `Function`,这个接口里与 CompletionStage 相关的方法是 `R apply(T t)`,这个方法既能接收参数也支持返回值,所以 thenApply 系列方法返回的是`CompletionStage`。 - -而 thenAccept 系列方法里参数 consumer 的类型是接口 `Consumer`,这个接口里与 CompletionStage 相关的方法是 `void accept(T t)`,这个方法虽然支持参数,但却不支持回值,所以 thenAccept 系列方法返回的是`CompletionStage`。 - -thenRun 系列方法里 action 的参数是 Runnable,所以 action 既不能接收参数也不支持返回值,所以 thenRun 系列方法返回的也是`CompletionStage`。 - -这些方法里面 Async 代表的是异步执行 fn、consumer 或者 action。其中,需要你注意的是 thenCompose 系列方法,这个系列的方法会新创建出一个子流程,最终结果和 thenApply 系列是相同的。 - -### 描述 AND 汇聚关系 - -CompletionStage 接口里面描述 AND 汇聚关系,主要是 thenCombine、thenAcceptBoth 和 runAfterBoth 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。 - -``` -CompletionStage thenCombine(other, fn); -CompletionStage thenCombineAsync(other, fn); -CompletionStage thenAcceptBoth(other, consumer); -CompletionStage thenAcceptBothAsync(other, consumer); -CompletionStage runAfterBoth(other, action); -CompletionStage runAfterBothAsync(other, action); -``` - -### 描述 OR 汇聚关系 - -CompletionStage 接口里面描述 OR 汇聚关系,主要是 applyToEither、acceptEither 和 runAfterEither 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。 - -``` -CompletionStage applyToEither(other, fn); -CompletionStage applyToEitherAsync(other, fn); -CompletionStage acceptEither(other, consumer); -CompletionStage acceptEitherAsync(other, consumer); -CompletionStage runAfterEither(other, action); -CompletionStage runAfterEitherAsync(other, action); -``` - -下面的示例代码展示了如何使用 applyToEither() 方法来描述一个 OR 汇聚关系。 - -``` -CompletableFuture f1 = - CompletableFuture.supplyAsync(()->{ - int t = getRandom(5, 10); - sleep(t, TimeUnit.SECONDS); - return String.valueOf(t); -}); - -CompletableFuture f2 = - CompletableFuture.supplyAsync(()->{ - int t = getRandom(5, 10); - sleep(t, TimeUnit.SECONDS); - return String.valueOf(t); -}); - -CompletableFuture f3 = - f1.applyToEither(f2,s -> s); - -System.out.println(f3.join()); -``` - -### 异常处理 - -虽然上面我们提到的 fn、consumer、action 它们的核心方法都**不允许抛出可检查异常,但是却无法限制它们抛出运行时异常**,例如下面的代码,执行 `7/0` 就会出现除零错误这个运行时异常。非异步编程里面,我们可以使用 try{}catch{} 来捕获并处理异常,那在异步编程里面,异常该如何处理呢? - -``` -CompletableFuture - f0 = CompletableFuture. - .supplyAsync(()->(7/0)) - .thenApply(r->r*10); -System.out.println(f0.join()); -``` - -CompletionStage 接口给我们提供的方案非常简单,比 try{}catch{}还要简单,下面是相关的方法,使用这些方法进行异常处理和串行操作是一样的,都支持链式编程方式。 - -``` -CompletionStage exceptionally(fn); -CompletionStage whenComplete(consumer); -CompletionStage whenCompleteAsync(consumer); -CompletionStage handle(fn); -CompletionStage handleAsync(fn); -``` - -下面的示例代码展示了如何使用 exceptionally() 方法来处理异常,exceptionally() 的使用非常类似于 try{}catch{}中的 catch{},但是由于支持链式编程方式,所以相对更简单。既然有 try{}catch{},那就一定还有 try{}finally{},whenComplete() 和 handle() 系列方法就类似于 try{}finally{}中的 finally{},无论是否发生异常都会执行 whenComplete() 中的回调函数 consumer 和 handle() 中的回调函数 fn。whenComplete() 和 handle() 的区别在于 whenComplete() 不支持返回结果,而 handle() 是支持返回结果的。 - -``` -CompletableFuture - f0 = CompletableFuture - .supplyAsync(()->7/0)) - .thenApply(r->r*10) - .exceptionally(e->0); -System.out.println(f0.join()); -``` - -## Fork/Join - -Fork/Join 是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的**Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并**。Fork/Join 计算框架主要包含两部分,一部分是**分治任务的线程池 ForkJoinPool**,另一部分是**分治任务 ForkJoinTask**。这两部分的关系类似于 ThreadPoolExecutor 和 Runnable 的关系,都可以理解为提交任务到线程池,只不过分治任务有自己独特类型 ForkJoinTask。 - -ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,其中 fork() 方法会异步地执行一个子任务,而 join() 方法则会阻塞当前线程来等待子任务的执行结果。ForkJoinTask 有两个子类——RecursiveAction 和 RecursiveTask,通过名字你就应该能知道,它们都是用递归的方式来处理分治任务的。这两个子类都定义了抽象方法 compute(),不过区别是 RecursiveAction 定义的 compute() 没有返回值,而 RecursiveTask 定义的 compute() 方法是有返回值的。这两个子类也是抽象类,在使用的时候,需要你定义子类去扩展。 - -### ForkJoinPool 工作原理 - -Fork/Join 并行计算的核心组件是 ForkJoinPool,所以下面我们就来简单介绍一下 ForkJoinPool 的工作原理。 - -通过专栏前面文章的学习,你应该已经知道 ThreadPoolExecutor 本质上是一个生产者 - 消费者模式的实现,内部有一个任务队列,这个任务队列是生产者和消费者通信的媒介;ThreadPoolExecutor 可以有多个工作线程,但是这些工作线程都共享一个任务队列。 - -ForkJoinPool 本质上也是一个生产者 - 消费者的实现,但是更加智能,你可以参考下面的 ForkJoinPool 工作原理图来理解其原理。ThreadPoolExecutor 内部只有一个任务队列,而 ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。 - -如果工作线程对应的任务队列空了,是不是就没活儿干了呢?不是的,ForkJoinPool 支持一种叫做“**任务窃取**”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务,例如下图中,线程 T2 对应的任务队列已经空了,它可以“窃取”线程 T1 对应的任务队列的任务。如此一来,所有的工作线程都不会闲下来了。 - -ForkJoinPool 中的任务队列采用的是双端队列,工作线程正常获取任务和“窃取任务”分别是从任务队列不同的端消费,这样能避免很多不必要的数据竞争。我们这里介绍的仅仅是简化后的原理,ForkJoinPool 的实现远比我们这里介绍的复杂,如果你感兴趣,建议去看它的源码。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200703141326.png) - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) -- [CompletableFuture 使用详解](https://www.jianshu.com/p/6bac52527ca4) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/11.Synchronized.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/11.Synchronized.md" deleted file mode 100644 index 66b70287d8..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/11.Synchronized.md" +++ /dev/null @@ -1,610 +0,0 @@ ---- -title: Synchronized -date: 2020-12-25 18:43:11 -order: 11 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 -permalink: /pages/8655a7/ ---- - -# Synchronized - -## synchronized 的简介 - -`synchronized` 是 Java 中的关键字,是 **利用锁的机制来实现互斥同步的**。 - -**`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 - -如果不需要 `Lock` 、`ReadWriteLock` 所提供的高级同步特性,应该优先考虑使用 `synchronized` ,理由如下: - -- Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平。从趋势来看,Java 未来仍将继续优化 `synchronized` ,而不是 `ReentrantLock` 。 -- `ReentrantLock` 是 Oracle JDK 的 API,在其他版本的 JDK 中不一定支持;而 `synchronized` 是 JVM 的内置特性,所有 JDK 版本都提供支持。 - -## synchronized 的应用 - -`synchronized` 有 3 种应用方式: - -- **同步实例方法** - 对于普通同步方法,锁是当前实例对象 -- **同步静态方法** - 对于静态同步方法,锁是当前类的 `Class` 对象 -- **同步代码块** - 对于同步方法块,锁是 `synchonized` 括号里配置的对象 - -> 说明: -> -> 类似 `Vector`、`Hashtable` 这类同步类,就是使用 `synchonized` 修饰其重要方法,来保证其线程安全。 -> -> 事实上,这类同步容器也非绝对的线程安全,当执行迭代器遍历,根据条件删除元素这种场景下,就可能出现线程不安全的情况。此外,Java 1.6 针对 `synchonized` 进行优化前,由于阻塞,其性能不高。 -> -> 综上,这类同步容器,在现代 Java 程序中,已经渐渐不用了。 - -### 同步实例方法 - -❌ 错误示例 - 未同步的示例 - -```java -public class NoSynchronizedDemo implements Runnable { - - public static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - NoSynchronizedDemo instance = new NoSynchronizedDemo(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - public void increase() { - count++; - } - -} -// 输出结果: 小于 200000 的随机数字 -``` - -Java 实例方法同步是同步在拥有该方法的对象上。这样,每个实例其方法同步都同步在不同的对象上,即该方法所属的实例。只有一个线程能够在实例方法同步块中运行。如果有多个实例存在,那么一个线程一次可以在一个实例同步块中执行操作。一个实例一个线程。 - -```java -public class SynchronizedDemo implements Runnable { - - private static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - SynchronizedDemo instance = new SynchronizedDemo(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - /** - * synchronized 修饰普通方法 - */ - public synchronized void increase() { - count++; - } - -} -``` - -【示例】错误示例 - -```java -class Account { - private int balance; - // 转账 - synchronized void transfer( - Account target, int amt){ - if (this.balance > amt) { - this.balance -= amt; - target.balance += amt; - } - } -} -``` - -在这段代码中,临界区内有两个资源,分别是转出账户的余额 this.balance 和转入账户的余额 target.balance,并且用的是一把锁 this,符合我们前面提到的,多个资源可以用一把锁来保护,这看上去完全正确呀。真的是这样吗?可惜,这个方案仅仅是看似正确,为什么呢? - -问题就出在 this 这把锁上,this 这把锁可以保护自己的余额 this.balance,却保护不了别人的余额 target.balance,就像你不能用自家的锁来保护别人家的资产,也不能用自己的票来保护别人的座位一样。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701135257.png) - -应该保证使用的**锁能覆盖所有受保护资源**。 - -【示例】正确姿势 - -```java -class Account { - private Object lock; - private int balance; - private Account(); - // 创建 Account 时传入同一个 lock 对象 - public Account(Object lock) { - this.lock = lock; - } - // 转账 - void transfer(Account target, int amt){ - // 此处检查所有对象共享的锁 - synchronized(lock) { - if (this.balance > amt) { - this.balance -= amt; - target.balance += amt; - } - } - } -} -``` - -这个办法确实能解决问题,但是有点小瑕疵,它要求在创建 Account 对象的时候必须传入同一个对象,如果创建 Account 对象时,传入的 lock 不是同一个对象,那可就惨了,会出现锁自家门来保护他家资产的荒唐事。在真实的项目场景中,创建 Account 对象的代码很可能分散在多个工程中,传入共享的 lock 真的很难。 - -上面的方案缺乏实践的可行性,我们需要更好的方案。还真有,就是**用 Account.class 作为共享的锁**。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以我们不用担心它的唯一性。使用 Account.class 作为共享的锁,我们就无需在创建 Account 对象时传入了,代码更简单。 - -【示例】正确姿势 - -```java -class Account { - private int balance; - // 转账 - void transfer(Account target, int amt){ - synchronized(Account.class) { - if (this.balance > amt) { - this.balance -= amt; - target.balance += amt; - } - } - } -} -``` - -### 同步静态方法 - -静态方法的同步是指同步在该方法所在的类对象上。因为在 JVM 中一个类只能对应一个类对象,所以同时只允许一个线程执行同一个类中的静态同步方法。 - -对于不同类中的静态同步方法,一个线程可以执行每个类中的静态同步方法而无需等待。不管类中的那个静态同步方法被调用,一个类只能由一个线程同时执行。 - -```java -public class SynchronizedDemo2 implements Runnable { - - private static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - SynchronizedDemo2 instance = new SynchronizedDemo2(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - /** - * synchronized 修饰静态方法 - */ - public synchronized static void increase() { - count++; - } - -} -``` - -### 同步代码块 - -有时你不需要同步整个方法,而是同步方法中的一部分。Java 可以对方法的一部分进行同步。 - -注意 Java 同步块构造器用括号将对象括起来。在上例中,使用了 `this`,即为调用 add 方法的实例本身。在同步构造器中用括号括起来的对象叫做监视器对象。上述代码使用监视器对象同步,同步实例方法使用调用方法本身的实例作为监视器对象。 - -一次只有一个线程能够在同步于同一个监视器对象的 Java 方法内执行。 - -```java -public class SynchronizedDemo3 implements Runnable { - - private static final int MAX = 100000; - - private static int count = 0; - - public static void main(String[] args) throws InterruptedException { - SynchronizedDemo3 instance = new SynchronizedDemo3(); - Thread t1 = new Thread(instance); - Thread t2 = new Thread(instance); - t1.start(); - t2.start(); - t1.join(); - t2.join(); - System.out.println(count); - } - - @Override - public void run() { - for (int i = 0; i < MAX; i++) { - increase(); - } - } - - /** - * synchronized 修饰代码块 - */ - public static void increase() { - synchronized (SynchronizedDemo3.class) { - count++; - } - } - -} -``` - -## synchronized 的原理 - -**`synchronized` 代码块是由一对 `monitorenter` 和 `monitorexit` 指令实现的,`Monitor` 对象是同步的基本实现单元**。在 Java 6 之前,`Monitor` 的实现完全是依靠操作系统内部的互斥锁,因为需要进行用户态到内核态的切换,所以同步操作是一个无差别的重量级操作。 - -如果 `synchronized` 明确制定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据 `synchronized` 修饰的是实例方法还是静态方法,去对对应的对象实例或 `Class` 对象来作为锁对象。 - -`synchronized` 同步块对同一线程来说是可重入的,不会出现锁死问题。 - -`synchronized` 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。 - -【示例】 - -```java -public void foo(Object lock) { - synchronized (lock) { - lock.hashCode(); - } - } - // 上面的 Java 代码将编译为下面的字节码 - public void foo(java.lang.Object); - Code: - 0: aload_1 - 1: dup - 2: astore_2 - 3: monitorenter - 4: aload_1 - 5: invokevirtual java/lang/Object.hashCode:()I - 8: pop - 9: aload_2 - 10: monitorexit - 11: goto 19 - 14: astore_3 - 15: aload_2 - 16: monitorexit - 17: aload_3 - 18: athrow - 19: return - Exception table: - from to target type - 4 11 14 any - 14 17 14 any - -``` - -### 同步代码块 - -`synchronized` 在修饰同步代码块时,是由 `monitorenter` 和 `monitorexit` 指令来实现同步的。进入 `monitorenter` 指令后,线程将持有 `Monitor` 对象,退出 `monitorenter` 指令后,线程将释放该 `Monitor` 对象。 - -### 同步方法 - -`synchronized` 修饰同步方法时,会设置一个 `ACC_SYNCHRONIZED` 标志。当方法调用时,调用指令将会检查该方法是否被设置 `ACC_SYNCHRONIZED` 访问标志。如果设置了该标志,执行线程将先持有 `Monitor` 对象,然后再执行方法。在该方法运行期间,其它线程将无法获取到该 `Mointor` 对象,当方法执行完成后,再释放该 `Monitor` 对象。 - -### Monitor - -每个对象实例都会有一个 `Monitor`,`Monitor` 可以和对象一起创建、销毁。`Monitor` 是由 `ObjectMonitor` 实现,而 `ObjectMonitor` 是由 C++ 的 `ObjectMonitor.hpp` 文件实现。 - -当多个线程同时访问一段同步代码时,多个线程会先被存放在 EntryList 集合中,处于 block 状态的线程,都会被加入到该列表。接下来当线程获取到对象的 Monitor 时,Monitor 是依靠底层操作系统的 Mutex Lock 来实现互斥的,线程申请 Mutex 成功,则持有该 Mutex,其它线程将无法获取到该 Mutex。 - -如果线程调用 wait() 方法,就会释放当前持有的 Mutex,并且该线程会进入 WaitSet 集合中,等待下一次被唤醒。如果当前线程顺利执行完方法,也将释放 Mutex。 - -## synchronized 的优化 - -> **Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平**。 - -### Java 对象头 - -在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。 - -Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200629191250.png) - -锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,`synchronized` 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。 - -Java 1.6 引入了偏向锁和轻量级锁,从而让 `synchronized` 拥有了四个状态: - -- **无锁状态(unlocked)** -- **偏向锁状态(biasble)** -- **轻量级锁状态(lightweight locked)** -- **重量级锁状态(inflated)** - -当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现。 - -当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。 - -如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。 - -### 偏向锁 - -偏向锁的思想是偏向于**第一个获取锁对象的线程,这个线程在之后获取该锁就不再需要进行同步操作,甚至连 CAS 操作也不再需要**。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200604105151.png) - -### 轻量级锁 - -**轻量级锁**是相对于传统的重量级锁而言,它 **使用 CAS 操作来避免重量级锁使用互斥量的开销**。对于绝大部分的锁,在整个同步周期内都是不存在竞争的,因此也就不需要都使用互斥量进行同步,可以先采用 CAS 操作进行同步,如果 CAS 失败了再改用互斥量进行同步。 - -当尝试获取一个锁对象时,如果锁对象标记为 `0|01`,说明锁对象的锁未锁定(unlocked)状态。此时虚拟机在当前线程的虚拟机栈中创建 Lock Record,然后使用 CAS 操作将对象的 Mark Word 更新为 Lock Record 指针。如果 CAS 操作成功了,那么线程就获取了该对象上的锁,并且对象的 Mark Word 的锁标记变为 00,表示该对象处于轻量级锁状态。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200604105248.png) - -### 锁消除 / 锁粗化 - -除了锁升级优化,Java 还使用了编译器对锁进行优化。 - -#### 锁消除 - -**锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除**。 - -JIT 编译器在动态编译同步块的时候,借助了一种被称为逃逸分析的技术,来判断同步块使用的锁对象是否只能够被一个线程访问,而没有被发布到其它线程。 - -确认是的话,那么 JIT 编译器在编译这个同步块的时候不会生成 synchronized 所表示的锁的申请与释放的机器码,即消除了锁的使用。在 Java7 之后的版本就不需要手动配置了,该操作可以自动实现。 - -对于一些看起来没有加锁的代码,其实隐式的加了很多锁。例如下面的字符串拼接代码就隐式加了锁: - -```java -public static String concatString(String s1, String s2, String s3) { - return s1 + s2 + s3; -} -``` - -`String` 是一个不可变的类,编译器会对 String 的拼接自动优化。在 Java 1.5 之前,会转化为 `StringBuffer` 对象的连续 `append()` 操作: - -```java -public static String concatString(String s1, String s2, String s3) { - StringBuffer sb = new StringBuffer(); - sb.append(s1); - sb.append(s2); - sb.append(s3); - return sb.toString(); -} -``` - -每个 `append()` 方法中都有一个同步块。虚拟机观察变量 sb,很快就会发现它的动态作用域被限制在 `concatString()` 方法内部。也就是说,sb 的所有引用永远不会逃逸到 `concatString()` 方法之外,其他线程无法访问到它,因此可以进行消除。 - -#### 锁粗化 - -锁粗化同理,就是在 JIT 编译器动态编译时,如果发现几个相邻的同步块使用的是同一个锁实例,那么 JIT 编译器将会把这几个同步块合并为一个大的同步块,从而避免一个线程“反复申请、释放同一个锁“所带来的性能开销。 - -如果**一系列的连续操作都对同一个对象反复加锁和解锁**,频繁的加锁操作就会导致性能损耗。 - -上一节的示例代码中连续的 `append()` 方法就属于这类情况。如果**虚拟机探测到由这样的一串零碎的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部**。对于上一节的示例代码就是扩展到第一个 `append()` 操作之前直至最后一个 `append()` 操作之后,这样只需要加锁一次就可以了。 - -### 自旋锁 - -互斥同步进入阻塞状态的开销都很大,应该尽量避免。在许多应用中,共享数据的锁定状态只会持续很短的一段时间。自旋锁的思想是让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。 - -自旋锁虽然能避免进入阻塞状态从而减少开销,但是它需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。 - -在 Java 1.6 中引入了自适应的自旋锁。自适应意味着自旋的次数不再固定了,而是由前一次在同一个锁上的自旋次数及锁的拥有者的状态来决定。 - -## synchronized 的误区 - -> 示例摘自:[《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) - -### synchronized 使用范围不当导致的错误 - -```java -public class Interesting { - - volatile int a = 1; - volatile int b = 1; - - public static void main(String[] args) { - Interesting interesting = new Interesting(); - new Thread(() -> interesting.add()).start(); - new Thread(() -> interesting.compare()).start(); - } - - public synchronized void add() { - log.info("add start"); - for (int i = 0; i < 10000; i++) { - a++; - b++; - } - log.info("add done"); - } - - public void compare() { - log.info("compare start"); - for (int i = 0; i < 10000; i++) { - //a始终等于b吗? - if (a < b) { - log.info("a:{},b:{},{}", a, b, a > b); - //最后的a>b应该始终是false吗? - } - } - log.info("compare done"); - } - -} -``` - -【输出】 - -``` -16:05:25.541 [Thread-0] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - add start -16:05:25.544 [Thread-0] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - add done -16:05:25.544 [Thread-1] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - compare start -16:05:25.544 [Thread-1] INFO io.github.dunwu.javacore.concurrent.sync.synchronized使用范围不当 - compare done -``` - -之所以出现这种错乱,是因为两个线程是交错执行 add 和 compare 方法中的业务逻辑,而且这些业务逻辑不是原子性的:a++ 和 b++ 操作中可以穿插在 compare 方法的比较代码中;更需要注意的是,a new Data().wrong()); - return Data.getCounter(); - } - - public int right(int count) { - Data.reset(); - IntStream.rangeClosed(1, count).parallel().forEach(i -> new Data().right()); - return Data.getCounter(); - } - - private static class Data { - - @Getter - private static int counter = 0; - private static Object locker = new Object(); - - public static int reset() { - counter = 0; - return counter; - } - - public synchronized void wrong() { - counter++; - } - - public void right() { - synchronized (locker) { - counter++; - } - } - - } - -} -``` - -wrong 方法中试图对一个静态对象加对象级别的 synchronized 锁,并不能保证线程安全。 - -### 锁粒度导致的问题 - -要尽可能的缩小加锁的范围,这可以提高并发吞吐。 - -如果精细化考虑了锁应用范围后,性能还无法满足需求的话,我们就要考虑另一个维度的粒度问题了,即:区分读写场景以及资源的访问冲突,考虑使用悲观方式的锁还是乐观方式的锁。 - -```java -public class synchronized锁粒度不当 { - - public static void main(String[] args) { - Demo demo = new Demo(); - demo.wrong(); - demo.right(); - } - - private static class Demo { - - private List data = new ArrayList<>(); - - private void slow() { - try { - TimeUnit.MILLISECONDS.sleep(10); - } catch (InterruptedException e) { - } - } - - public int wrong() { - long begin = System.currentTimeMillis(); - IntStream.rangeClosed(1, 1000).parallel().forEach(i -> { - synchronized (this) { - slow(); - data.add(i); - } - }); - log.info("took:{}", System.currentTimeMillis() - begin); - return data.size(); - } - - public int right() { - long begin = System.currentTimeMillis(); - IntStream.rangeClosed(1, 1000).parallel().forEach(i -> { - slow(); - synchronized (data) { - data.add(i); - } - }); - log.info("took:{}", System.currentTimeMillis() - begin); - return data.size(); - } - - } - -} -``` - -## 参考资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) -- [Java 并发编程:volatile 关键字解析](http://www.cnblogs.com/dolphin0520/p/3920373.html) -- [Java 并发编程:synchronized](http://www.cnblogs.com/dolphin0520/p/3923737.html) -- [深入理解 Java 并发之 synchronized 实现原理](https://blog.csdn.net/javazejian/article/details/72828483) -- [Java CAS 完全解读](https://www.jianshu.com/p/473e14d5ab2d) -- [Java 中 CAS 详解](https://blog.csdn.net/ls5718/article/details/52563959) -- [ThreadLocal 终极篇](https://juejin.im/post/5a64a581f265da3e3b7aa02d) -- [synchronized 实现原理及锁优化](https://nicky-chen.github.io/2018/05/14/synchronized-principle/) -- [Non-blocking Algorithms](http://tutorials.jenkov.com/java-concurrency/non-blocking-algorithms.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/README.md" "b/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/README.md" deleted file mode 100644 index 00ad309ecc..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/05.\345\271\266\345\217\221/README.md" +++ /dev/null @@ -1,82 +0,0 @@ ---- -title: Java 并发 -date: 2020-06-04 13:51:01 -categories: - - Java - - JavaSE - - 并发 -tags: - - Java - - JavaSE - - 并发 -permalink: /pages/6e5393/ -hidden: true -index: false ---- - -# Java 并发 - -> Java 并发总结、整理 Java 并发编程相关知识点。 -> -> 并发编程并非 Java 语言所独有,而是一种成熟的编程范式,Java 只是用自己的方式实现了并发工作模型。学习 Java 并发编程,应该先熟悉并发的基本概念,然后进一步了解并发的特性以及其特性所面临的问题。掌握了这些,当学习 Java 并发工具时,才会明白它们各自是为了解决什么问题,为什么要这样设计。通过这样由点到面的学习方式,更容易融会贯通,将并发知识形成体系化。 - -## 📖 内容 - -### [Java 并发简介](01.Java并发简介.md) - -> **关键词:`进程`、`线程`、`安全性`、`活跃性`、`性能`、`死锁`、`饥饿`、`上下文切换`** - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200701113445.png) - -### [Java 线程基础](02.Java线程基础.md) - -> **关键词:`Thread`、`Runnable`、`Callable`、`Future`、`wait`、`notify`、`notifyAll`、`join`、`sleep`、`yeild`、`线程状态`、`线程通信`** - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630221707.png) - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-thread_1.png) - -### [Java 并发核心机制](03.Java并发核心机制.md) - -> **关键词:`synchronized`、`volatile`、`CAS`、`ThreadLocal`** - -### [Java 并发锁](04.Java锁.md) - -> **关键词:`AQS`、`ReentrantLock`、`ReentrantReadWriteLock`、`Condition`** - -### [Java 原子类](05.Java原子类.md) - -> **关键词:`CAS`、`Atomic`** - -### [Java 并发容器](06.Java并发和容器.md) - -> **关键词:`ConcurrentHashMap`、`CopyOnWriteArrayList`** - -### [Java 线程池](07.Java线程池.md) - -> **关键词:`Executor`、`ExecutorService`、`ThreadPoolExecutor`、`Executors`** - -### [Java 并发工具类](08.Java并发工具类.md) - -> **关键词:`CountDownLatch`、`CyclicBarrier`、`Semaphore`** - -### [Java 内存模型](09.Java内存模型.md) - -> **关键词:`JMM`、`volatile`、`synchronized`、`final`、`Happens-Before`、`内存屏障`** - -### [ForkJoin 框架](10.ForkJoin框架.md) - -## 📚 资料 - -- [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) -- [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Effective Java》](https://book.douban.com/subject/30412517/) -- [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/82) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) -- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) -- [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) - -## 🚪 传送 - -◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/02.JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" "b/source/_posts/01.Java/01.JavaSE/06.JVM/02.JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" deleted file mode 100644 index 925f65800c..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/02.JVM\345\206\205\345\255\230\345\214\272\345\237\237.md" +++ /dev/null @@ -1,704 +0,0 @@ ---- -title: Java 内存管理 -date: 2020-06-28 16:19:00 -order: 02 -categories: - - Java - - JavaSE - - JVM -tags: - - Java - - JavaSE - - JVM -permalink: /pages/db5b69/ ---- - -# Java 内存管理 - -## 内存简介 - -### 物理内存和虚拟内存 - -所谓物理内存就是通常所说的 RAM(随机存储器)。 - -虚拟内存使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间上共享,在逻辑上彼此仍然是隔离的。 - -### 内核空间和用户空间 - -一个计算通常有固定大小的内存空间,但是程序并不能使用全部的空间。因为这些空间被划分为内核空间和用户空间,而程序只能使用用户空间的内存。 - -### 使用内存的 Java 组件 - -Java 启动后,作为一个进程运行在操作系统中。 - -有哪些 Java 组件需要占用内存呢? - -- 堆内存:Java 堆、类和类加载器 -- 栈内存:线程 -- 本地内存:NIO、JNI - -## 运行时数据区域 - -JVM 在执行 Java 程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域都有各自的用途,以及创建和销毁的时间,有的区域随着虚拟机进程的启动而存在,有些区域则依赖用户线程的启动和结束而建立和销毁。如下图所示: - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/jvm/jvm-memory-runtime-data-area.png) - -### 程序计数器 - -**`程序计数器(Program Counter Register)`** 是一块较小的内存空间,它可以看做是**当前线程所执行的字节码的行号指示器**。例如,分支、循环、跳转、异常、线程恢复等都依赖于计数器。 - -当执行的线程数量超过 CPU 数量时,线程之间会根据时间片轮询争夺 CPU 资源。如果一个线程的时间片用完了,或者是其它原因导致这个线程的 CPU 资源被提前抢夺,那么这个退出的线程就需要单独的一个程序计数器,来记录下一条运行的指令,从而在线程切换后能恢复到正确的执行位置。各条线程间的计数器互不影响,独立存储,我们称这类内存区域为 “线程私有” 的内存。 - -- 如果线程正在执行的是一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址; -- 如果正在执行的是 Native 方法,这个计数器值则为空(Undefined)。 - -> 🔔 注意:此内存区域是唯一一个在 JVM 中没有规定任何 `OutOfMemoryError` 情况的区域。 - -### Java 虚拟机栈 - -**`Java 虚拟机栈(Java Virtual Machine Stacks)`** 也**是线程私有的,它的生命周期与线程相同**。 - -每个 Java 方法在执行的同时都会创建一个栈帧(Stack Frame)用于存储 **局部变量表**、**操作数栈**、**常量池引用** 等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/jvm/jvm-stack.png!w640) - -- **局部变量表** - 32 位变量槽,存放了编译期可知的各种基本数据类型、对象引用、`ReturnAddress` 类型。 -- **操作数栈** - 基于栈的执行引擎,虚拟机把操作数栈作为它的工作区,大多数指令都要从这里弹出数据、执行运算,然后把结果压回操作数栈。 -- **动态链接** - 每个栈帧都包含一个指向运行时常量池(方法区的一部分)中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接。Class 文件的常量池中有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期间转化为直接应用,这部分称为动态链接。 -- **方法出口** - 返回方法被调用的位置,恢复上层方法的局部变量和操作数栈,如果无返回值,则把它压入调用者的操作数栈。 - -> 🔔 注意: -> -> 该区域可能抛出以下异常: -> -> - 如果线程请求的栈深度超过最大值,就会抛出 `StackOverflowError` 异常; -> - 如果虚拟机栈进行动态扩展时,无法申请到足够内存,就会抛出 `OutOfMemoryError` 异常。 -> -> 💡 提示: -> -> 可以通过 `-Xss` 这个虚拟机参数来指定一个程序的 Java 虚拟机栈内存大小: -> -> ```java -> java -Xss=512M HackTheJava -> ``` - -### 本地方法栈 - -**`本地方法栈(Native Method Stack)`** 与虚拟机栈的作用相似。 - -二者的区别在于:**虚拟机栈为 Java 方法服务;本地方法栈为 Native 方法服务**。本地方法并不是用 Java 实现的,而是由 C 语言实现的。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/jvm/jvm-native-method-stack.gif!w640) - -> 🔔 注意:本地方法栈也会抛出 `StackOverflowError` 异常和 `OutOfMemoryError` 异常。 - -### Java 堆 - -**`Java 堆(Java Heap)` 的作用就是存放对象实例,几乎所有的对象实例都是在这里分配内存**。 - -Java 堆是垃圾收集的主要区域(因此也被叫做"GC 堆")。现代的垃圾收集器基本都是采用**分代收集算法**,该算法的思想是针对不同的对象采取不同的垃圾回收算法。 - -因此虚拟机把 Java 堆分成以下三块: - -- **`新生代(Young Generation)`** - - `Eden` - Eden 和 Survivor 的比例为 8:1 - - `From Survivor` - - `To Survivor` -- **`老年代(Old Generation)`** -- **`永久代(Permanent Generation)`** - -当一个对象被创建时,它首先进入新生代,之后有可能被转移到老年代中。新生代存放着大量的生命很短的对象,因此新生代在三个区域中垃圾回收的频率最高。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/jvm/jvm-heap.gif!w640) - -> 🔔 注意:Java 堆不需要连续内存,并且可以动态扩展其内存,扩展失败会抛出 `OutOfMemoryError` 异常。 -> -> 💡 提示:可以通过 `-Xms` 和 `-Xmx` 两个虚拟机参数来指定一个程序的 Java 堆内存大小,第一个参数设置初始值,第二个参数设置最大值。 -> -> ```java -> java -Xms=1M -Xmx=2M HackTheJava -> ``` - -### 方法区 - -方法区(Method Area)也被称为永久代。**方法区用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据**。 - -对这块区域进行垃圾回收的主要目标是对常量池的回收和对类的卸载,但是一般比较难实现。 - -> 🔔 注意:和 Java 堆一样不需要连续的内存,并且可以动态扩展,动态扩展失败一样会抛出 `OutOfMemoryError` 异常。 -> -> 💡 提示: -> -> - JDK 1.7 之前,HotSpot 虚拟机把它当成永久代来进行垃圾回收。可通过参数 `-XX:PermSize` 和 `-XX:MaxPermSize` 设置。 -> - JDK 1.8 之后,取消了永久代,用 **`metaspace(元数据)`**区替代。可通过参数 `-XX:MaxMetaspaceSize` 设置。 - -### 运行时常量池 - -**`运行时常量池(Runtime Constant Pool)` 是方法区的一部分**,Class 文件中除了有类的版本、字段、方法、接口等描述信息,还有一项信息是常量池(Constant Pool Table),**用于存放编译器生成的各种字面量和符号引用**,这部分内容会在类加载后被放入这个区域。 - -- **字面量** - 文本字符串、声明为 `final` 的常量值等。 -- **符号引用** - 类和接口的完全限定名(Fully Qualified Name)、字段的名称和描述符(Descriptor)、方法的名称和描述符。 - -除了在编译期生成的常量,还允许动态生成,例如 `String` 类的 `intern()`。这部分常量也会被放入运行时常量池。 - -> 🔔 注意:当常量池无法再申请到内存时会抛出 `OutOfMemoryError` 异常。 - -### 直接内存 - -直接内存(Direct Memory)并不是虚拟机运行时数据区的一部分,也不是 JVM 规范中定义的内存区域。 - -在 JDK 1.4 中新加入了 NIO 类,它可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里的 `DirectByteBuffer` 对象作为这块内存的引用进行操作。这样能在一些场景中显著提高性能,因为避免了在 Java 堆和 Native 堆中来回复制数据。 - -> 🔔 注意:直接内存这部分也被频繁的使用,且也可能导致 `OutOfMemoryError` 异常。 -> -> 💡 提示:直接内存容量可通过 `-XX:MaxDirectMemorySize` 指定,如果不指定,则默认与 Java 堆最大值(`-Xmx` 指定)一样。 - -### Java 内存区域对比 - -| 内存区域 | 内存作用范围 | 异常 | -| ------------- | -------------- | ------------------------------------------ | -| 程序计数器 | 线程私有 | 无 | -| Java 虚拟机栈 | 线程私有 | `StackOverflowError` 和 `OutOfMemoryError` | -| 本地方法栈 | 线程私有 | `StackOverflowError` 和 `OutOfMemoryError` | -| Java 堆 | 线程共享 | `OutOfMemoryError` | -| 方法区 | 线程共享 | `OutOfMemoryError` | -| 运行时常量池 | 线程共享 | `OutOfMemoryError` | -| 直接内存 | 非运行时数据区 | `OutOfMemoryError` | - -## JVM 运行原理 - -```java -public class JVMCase { - - // 常量 - public final static String MAN_SEX_TYPE = "man"; - - // 静态变量 - public static String WOMAN_SEX_TYPE = "woman"; - - public static void main(String[] args) { - - Student stu = new Student(); - stu.setName("nick"); - stu.setSexType(MAN_SEX_TYPE); - stu.setAge(20); - - JVMCase jvmcase = new JVMCase(); - - // 调用静态方法 - print(stu); - // 调用非静态方法 - jvmcase.sayHello(stu); - } - - - // 常规静态方法 - public static void print(Student stu) { - System.out.println("name: " + stu.getName() + "; sex:" + stu.getSexType() + "; age:" + stu.getAge()); - } - - - // 非静态方法 - public void sayHello(Student stu) { - System.out.println(stu.getName() + "say: hello"); - } -} - -class Student{ - String name; - String sexType; - int age; - - public String getName() { - return name; - } - public void setName(String name) { - this.name = name; - } - - public String getSexType() { - return sexType; - } - public void setSexType(String sexType) { - this.sexType = sexType; - } - public int getAge() { - return age; - } - public void setAge(int age) { - this.age = age; - } -} -``` - -运行以上代码时,JVM 处理过程如下: - -(1)JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。 - -(2)JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。 - -(3)class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值(这部分我在第 21 讲还会详细介绍)。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094250.png) - -(4)完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器 `` 方法,编译器会在 `.java` 文件被编译成 `.class` 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为 `()` 方法。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094329.png) - -(5)执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 student 对象,对象引用 student 就存放在栈中。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094651.png) - -(6)此时再次创建一个 JVMCase 对象,调用 sayHello 非静态方法,sayHello 方法属于对象 JVMCase,此时 sayHello 方法入栈,并通过栈中的 student 引用调用堆中的 Student 对象;之后,调用静态方法 print,print 静态方法属于 JVMCase 类,是从静态方法中获取,之后放入到栈中,也是通过 student 引用调用堆中的 student 对象。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630094714.png) - -## OutOfMemoryError - -### 什么是 OutOfMemoryError - -`OutOfMemoryError` 简称为 OOM。Java 中对 OOM 的解释是,没有空闲内存,并且垃圾收集器也无法提供更多内存。通俗的解释是:JVM 内存不足了。 - -在 JVM 规范中,**除了程序计数器区域外,其他运行时区域都可能发生 `OutOfMemoryError` 异常(简称 OOM)**。 - -下面逐一介绍 OOM 发生场景。 - -### 堆空间溢出 - -`java.lang.OutOfMemoryError: Java heap space` 这个错误意味着:**堆空间溢出**。 - -更细致的说法是:Java 堆内存已经达到 `-Xmx` 设置的最大值。Java 堆用于存储对象实例,只要不断地创建对象,并且保证 GC Roots 到对象之间有可达路径来避免垃圾收集器回收这些对象,那么当堆空间到达最大容量限制后就会产生 OOM。 - -堆空间溢出有可能是**`内存泄漏(Memory Leak)`** 或 **`内存溢出(Memory Overflow)`** 。需要使用 jstack 和 jmap 生成 threaddump 和 heapdump,然后用内存分析工具(如:MAT)进行分析。 - -#### Java heap space 分析步骤 - -1. 使用 `jmap` 或 `-XX:+HeapDumpOnOutOfMemoryError` 获取堆快照。 -2. 使用内存分析工具(visualvm、mat、jProfile 等)对堆快照文件进行分析。 -3. 根据分析图,重点是确认内存中的对象是否是必要的,分清究竟是是内存泄漏(Memory Leak)还是内存溢出(Memory Overflow)。 - -#### 内存泄漏 - -**内存泄漏是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况**。 - -内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。内存泄漏随着被执行的次数不断增加,最终会导致内存溢出。 - -内存泄漏常见场景: - -- 静态容器 - - 声明为静态(`static`)的 `HashMap`、`Vector` 等集合 - - 通俗来讲 A 中有 B,当前只把 B 设置为空,A 没有设置为空,回收时 B 无法回收。因为被 A 引用。 -- 监听器 - - 监听器被注册后释放对象时没有删除监听器 -- 物理连接 - - 各种连接池建立了连接,必须通过 `close()` 关闭链接 -- 内部类和外部模块等的引用 - - 发现它的方式同内存溢出,可再加个实时观察 - - `jstat -gcutil 7362 2500 70` - -重点关注: - -- `FGC` — 从应用程序启动到采样时发生 Full GC 的次数。 -- `FGCT` — 从应用程序启动到采样时 Full GC 所用的时间(单位秒)。 -- `FGC` 次数越多,`FGCT` 所需时间越多,越有可能存在内存泄漏。 - -如果是内存泄漏,可以进一步查看泄漏对象到 GC Roots 的对象引用链。这样就能找到泄漏对象是怎样与 GC Roots 关联并导致 GC 无法回收它们的。掌握了这些原因,就可以较准确的定位出引起内存泄漏的代码。 - -导致内存泄漏的常见原因是使用容器,且不断向容器中添加元素,但没有清理,导致容器内存不断膨胀。 - -【示例】 - -```java -/** - * 内存泄漏示例 - * 错误现象:java.lang.OutOfMemoryError: Java heap space - * VM Args:-verbose:gc -Xms10M -Xmx10M -XX:+HeapDumpOnOutOfMemoryError - */ -public class HeapOutOfMemoryDemo { - - public static void main(String[] args) { - List list = new ArrayList<>(); - while (true) { - list.add(new OomObject()); - } - } - - static class OomObject {} - -} -``` - -#### 内存溢出 - -如果不存在内存泄漏,即内存中的对象确实都必须存活着,则应当检查虚拟机的堆参数(`-Xmx` 和 `-Xms`),与机器物理内存进行对比,看看是否可以调大。并从代码上检查是否存在某些对象生命周期过长、持有时间过长的情况,尝试减少程序运行期的内存消耗。 - -【示例】 - -```java -/** - * 堆溢出示例 - *

- * 错误现象:java.lang.OutOfMemoryError: Java heap space - *

- * VM Args:-verbose:gc -Xms10M -Xmx10M - * - * @author Zhang Peng - * @since 2019-06-25 - */ -public class HeapOutOfMemoryDemo { - - public static void main(String[] args) { - Double[] array = new Double[999999999]; - System.out.println("array length = [" + array.length + "]"); - } - -} -``` - -执行 `java -verbose:gc -Xms10M -Xmx10M -XX:+HeapDumpOnOutOfMemoryError io.github.dunwu.javacore.jvm.memory.HeapMemoryLeakMemoryErrorDemo` - -上面的例子是一个极端的例子,试图创建一个维度很大的数组,堆内存无法分配这么大的内存,从而报错:`Java heap space`。 - -但如果在现实中,代码并没有问题,仅仅是因为堆内存不足,可以通过 `-Xms` 和 `-Xmx` 适当调整堆内存大小。 - -### GC 开销超过限制 - -`java.lang.OutOfMemoryError: GC overhead limit exceeded` 这个错误,官方给出的定义是:**超过 `98%` 的时间用来做 GC 并且回收了不到 `2%` 的堆内存时会抛出此异常**。这意味着,发生在 GC 占用大量时间为释放很小空间的时候发生的,是一种保护机制。导致异常的原因:一般是因为堆太小,没有足够的内存。 - -【示例】 - -```java -/** - * GC overhead limit exceeded 示例 - * 错误现象:java.lang.OutOfMemoryError: GC overhead limit exceeded - * 发生在GC占用大量时间为释放很小空间的时候发生的,是一种保护机制。导致异常的原因:一般是因为堆太小,没有足够的内存。 - * 官方对此的定义:超过98%的时间用来做GC并且回收了不到2%的堆内存时会抛出此异常。 - * VM Args: -Xms10M -Xmx10M - */ -public class GcOverheadLimitExceededDemo { - - public static void main(String[] args) { - List list = new ArrayList<>(); - double d = 0.0; - while (true) { - list.add(d++); - } - } - -} -``` - -【处理】 - -与 **Java heap space** 错误处理方法类似,先判断是否存在内存泄漏。如果有,则修正代码;如果没有,则通过 `-Xms` 和 `-Xmx` 适当调整堆内存大小。 - -### 永久代空间不足 - -【错误】 - -``` -java.lang.OutOfMemoryError: PermGen space -``` - -【原因】 - -Perm (永久代)空间主要用于存放 `Class` 和 Meta 信息,包括类的名称和字段,带有方法字节码的方法,常量池信息,与类关联的对象数组和类型数组以及即时编译器优化。GC 在主程序运行期间不会对永久代空间进行清理,默认是 64M 大小。 - -根据上面的定义,可以得出 **PermGen 大小要求取决于加载的类的数量以及此类声明的大小**。因此,可以说造成该错误的主要原因是永久代中装入了太多的类或太大的类。 - -在 JDK8 之前的版本中,可以通过 `-XX:PermSize` 和 `-XX:MaxPermSize` 设置永久代空间大小,从而限制方法区大小,并间接限制其中常量池的容量。 - -#### 初始化时永久代空间不足 - -【示例】 - -```java -/** - * 永久代内存空间不足示例 - *

- * 错误现象: - *

    - *
  • java.lang.OutOfMemoryError: PermGen space (JDK8 以前版本)
  • - *
  • java.lang.OutOfMemoryError: Metaspace (JDK8 及以后版本)
  • - *
- * VM Args: - *
    - *
  • -Xmx100M -XX:MaxPermSize=16M (JDK8 以前版本)
  • - *
  • -Xmx100M -XX:MaxMetaspaceSize=16M (JDK8 及以后版本)
  • - *
- */ -public class PermOutOfMemoryErrorDemo { - - public static void main(String[] args) throws Exception { - for (int i = 0; i < 100_000_000; i++) { - generate("eu.plumbr.demo.Generated" + i); - } - } - - public static Class generate(String name) throws Exception { - ClassPool pool = ClassPool.getDefault(); - return pool.makeClass(name).toClass(); - } - -} -``` - -在此示例中,源代码遍历循环并在运行时生成类。javassist 库正在处理类生成的复杂性。 - -#### 重部署时永久代空间不足 - -对于更复杂,更实际的示例,让我们逐步介绍一下在应用程序重新部署期间发生的 Permgen 空间错误。重新部署应用程序时,你希望垃圾回收会摆脱引用所有先前加载的类的加载器,并被加载新类的类加载器取代。 - -不幸的是,许多第三方库以及对线程,JDBC 驱动程序或文件系统句柄等资源的不良处理使得无法卸载以前使用的类加载器。反过来,这意味着在每次重新部署期间,所有先前版本的类仍将驻留在 PermGen 中,从而在每次重新部署期间生成数十兆的垃圾。 - -让我们想象一个使用 JDBC 驱动程序连接到关系数据库的示例应用程序。启动应用程序时,初始化代码将加载 JDBC 驱动程序以连接到数据库。对应于规范,JDBC 驱动程序向 java.sql.DriverManager 进行注册。该注册包括将对驱动程序实例的引用存储在 DriverManager 的静态字段中。 - -现在,当从应用程序服务器取消部署应用程序时,java.sql.DriverManager 仍将保留该引用。我们最终获得了对驱动程序类的实时引用,而驱动程序类又保留了用于加载应用程序的 java.lang.Classloader 实例的引用。反过来,这意味着垃圾回收算法无法回收空间。 - -而且该 java.lang.ClassLoader 实例仍引用应用程序的所有类,通常在 PermGen 中占据数十兆字节。这意味着只需少量重新部署即可填充通常大小的 PermGen。 - -#### PermGen space 解决方案 - -(1)解决初始化时的 `OutOfMemoryError` - -在应用程序启动期间触发由于 PermGen 耗尽导致的 `OutOfMemoryError` 时,解决方案很简单。该应用程序仅需要更多空间才能将所有类加载到 PermGen 区域,因此我们只需要增加其大小即可。为此,更改你的应用程序启动配置并添加(或增加,如果存在)`-XX:MaxPermSize` 参数,类似于以下示例: - -``` -java -XX:MaxPermSize=512m com.yourcompany.YourClass -``` - -上面的配置将告诉 JVM,PermGen 可以增长到 512MB。 - -清理应用程序中 `WEB-INF/lib` 下的 jar,用不上的 jar 删除掉,多个应用公共的 jar 移动到 Tomcat 的 lib 目录,减少重复加载。 - -🔔 注意:`-XX:PermSize` 一般设为 64M - -(2)解决重新部署时的 `OutOfMemoryError` - -重新部署应用程序后立即发生 OutOfMemoryError 时,应用程序会遭受类加载器泄漏的困扰。在这种情况下,解决问题的最简单,继续进行堆转储分析–使用类似于以下命令的重新部署后进行堆转储: - -``` -jmap -dump:format=b,file=dump.hprof -``` - -然后使用你最喜欢的堆转储分析器打开转储(Eclipse MAT 是一个很好的工具)。在分析器中可以查找重复的类,尤其是那些正在加载应用程序类的类。从那里,你需要进行所有类加载器的查找,以找到当前活动的类加载器。 - -对于非活动类加载器,你需要通过从非活动类加载器收集到 GC 根的最短路径来确定阻止它们被垃圾收集的引用。有了此信息,你将找到根本原因。如果根本原因是在第三方库中,则可以进入 Google/StackOverflow 查看是否是已知问题以获取补丁/解决方法。 - -(3)解决运行时 `OutOfMemoryError` - -第一步是检查是否允许 GC 从 PermGen 卸载类。在这方面,标准的 JVM 相当保守-类是天生的。因此,一旦加载,即使没有代码在使用它们,类也会保留在内存中。当应用程序动态创建许多类并且长时间不需要生成的类时,这可能会成为问题。在这种情况下,允许 JVM 卸载类定义可能会有所帮助。这可以通过在启动脚本中仅添加一个配置参数来实现: - -``` --XX:+CMSClassUnloadingEnabled -``` - -默认情况下,此选项设置为 false,因此要启用此功能,你需要在 Java 选项中显式设置。如果启用 CMSClassUnloadingEnabled,GC 也会扫描 PermGen 并删除不再使用的类。请记住,只有同时使用 UseConcMarkSweepGC 时此选项才起作用。 - -``` --XX:+UseConcMarkSweepGC -``` - -在确保可以卸载类并且问题仍然存在之后,你应该继续进行堆转储分析–使用类似于以下命令的方法进行堆转储: - -``` -jmap -dump:file=dump.hprof,format=b -``` - -然后,使用你最喜欢的堆转储分析器(例如 Eclipse MAT)打开转储,然后根据已加载的类数查找最昂贵的类加载器。从此类加载器中,你可以继续提取已加载的类,并按实例对此类进行排序,以使可疑对象排在首位。 - -然后,对于每个可疑者,就需要你手动将根本原因追溯到生成此类的应用程序代码。 - -### 元数据区空间不足 - -【错误】 - -``` -Exception in thread "main" java.lang.OutOfMemoryError: Metaspace -``` - -【原因】 - -Java8 以后,JVM 内存空间发生了很大的变化。取消了永久代,转而变为元数据区。 - -**元数据区的内存不足,即方法区和运行时常量池的空间不足**。 - -方法区用于存放 Class 的相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。 - -一个类要被垃圾收集器回收,判定条件是比较苛刻的。在经常动态生成大量 Class 的应用中,需要特别注意类的回收状况。这类常见除了 CGLib 字节码增强和动态语言以外,常见的还有:大量 JSP 或动态产生 JSP 文件的应用(JSP 第一次运行时需要编译为 Java 类)、基于 OSGi 的应用(即使是同一个类文件,被不同的加载器加载也会视为不同的类)等。 - -【示例】方法区出现 `OutOfMemoryError` - -```java -public class MethodAreaOutOfMemoryDemo { - - public static void main(String[] args) { - while (true) { - Enhancer enhancer = new Enhancer(); - enhancer.setSuperclass(Bean.class); - enhancer.setUseCache(false); - enhancer.setCallback(new MethodInterceptor() { - @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { - return proxy.invokeSuper(obj, args); - } - }); - enhancer.create(); - } - } - - static class Bean {} - -} -``` - -【解决】 - -当由于元空间而面临 `OutOfMemoryError` 时,第一个解决方案应该是显而易见的。如果应用程序耗尽了内存中的 Metaspace 区域,则应增加 Metaspace 的大小。更改应用程序启动配置并增加以下内容: - -``` --XX:MaxMetaspaceSize=512m -``` - -上面的配置示例告诉 JVM,允许 Metaspace 增长到 512 MB。 - -另一种解决方案甚至更简单。你可以通过删除此参数来完全解除对 Metaspace 大小的限制,JVM 默认对 Metaspace 的大小没有限制。但是请注意以下事实:这样做可能会导致大量交换或达到本机物理内存而分配失败。 - -### 无法新建本地线程 - -`java.lang.OutOfMemoryError: Unable to create new native thread` 这个错误意味着:**Java 应用程序已达到其可以启动线程数的限制**。 - -【原因】 - -当发起一个线程的创建时,虚拟机会在 JVM 内存创建一个 `Thread` 对象同时创建一个操作系统线程,而这个系统线程的内存用的不是 JVM 内存,而是系统中剩下的内存。 - -那么,究竟能创建多少线程呢?这里有一个公式: - -``` -线程数 = (MaxProcessMemory - JVMMemory - ReservedOsMemory) / (ThreadStackSize) -``` - -【参数】 - -- `MaxProcessMemory` - 一个进程的最大内存 -- `JVMMemory` - JVM 内存 -- `ReservedOsMemory` - 保留的操作系统内存 -- `ThreadStackSize` - 线程栈的大小 - -**给 JVM 分配的内存越多,那么能用来创建系统线程的内存就会越少,越容易发生 `unable to create new native thread`**。所以,JVM 内存不是分配的越大越好。 - -但是,通常导致 `java.lang.OutOfMemoryError` 的情况:无法创建新的本机线程需要经历以下阶段: - -1. JVM 内部运行的应用程序请求新的 Java 线程 -2. JVM 本机代码代理为操作系统创建新本机线程的请求 -3. 操作系统尝试创建一个新的本机线程,该线程需要将内存分配给该线程 -4. 操作系统将拒绝本机内存分配,原因是 32 位 Java 进程大小已耗尽其内存地址空间(例如,已达到(2-4)GB 进程大小限制)或操作系统的虚拟内存已完全耗尽 -5. 引发 `java.lang.OutOfMemoryError: Unable to create new native thread` 错误。 - -【示例】 - -```java -public class UnableCreateNativeThreadErrorDemo { - - public static void main(String[] args) { - while (true) { - new Thread(new Runnable() { - @Override - public void run() { - try { - TimeUnit.MINUTES.sleep(5); - } catch (InterruptedException e) { - e.printStackTrace(); - } - } - }).start(); - } - } -} -``` - -【处理】 - -可以通过增加操作系统级别的限制来绕过无法创建新的本机线程问题。例如,如果限制了 JVM 可在用户空间中产生的进程数,则应检查出并可能增加该限制: - -```shell -[root@dev ~]# ulimit -a -core file size (blocks, -c) 0 ---- cut for brevity --- -max user processes (-u) 1800 -``` - -通常,`OutOfMemoryError` 对新的本机线程的限制表示编程错误。当应用程序产生数千个线程时,很可能出了一些问题—很少有应用程序可以从如此大量的线程中受益。 - -解决问题的一种方法是开始进行线程转储以了解情况。 - -### 直接内存溢出 - -由直接内存导致的内存溢出,一个明显的特征是在 Head Dump 文件中不会看见明显的异常,如果发现 OOM 之后 Dump 文件很小,而程序中又直接或间接使用了 NIO,就可以考虑检查一下是不是这方面的原因。 - -【示例】直接内存 `OutOfMemoryError` - -```java -/** - * 本机直接内存溢出示例 - * 错误现象:java.lang.OutOfMemoryError - * VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M - */ -public class DirectOutOfMemoryDemo { - - private static final int _1MB = 1024 * 1024; - - public static void main(String[] args) throws IllegalAccessException { - Field unsafeField = Unsafe.class.getDeclaredFields()[0]; - unsafeField.setAccessible(true); - Unsafe unsafe = (Unsafe) unsafeField.get(null); - while (true) { - unsafe.allocateMemory(_1MB); - } - } - -} -``` - -## StackOverflowError - -对于 HotSpot 虚拟机来说,栈容量只由 `-Xss` 参数来决定如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 `StackOverflowError` 异常。 - -从实战来说,栈溢出的常见原因: - -- **递归函数调用层数太深** -- **大量循环或死循环** - -【示例】递归函数调用层数太深导致 `StackOverflowError` - -```java -public class StackOverflowDemo { - - private int stackLength = 1; - - public void recursion() { - stackLength++; - recursion(); - } - - public static void main(String[] args) { - StackOverflowDemo obj = new StackOverflowDemo(); - try { - obj.recursion(); - } catch (Throwable e) { - System.out.println("栈深度:" + obj.stackLength); - e.printStackTrace(); - } - } - -} -``` - -## 参考资料 - -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) -- [从表到里学习 JVM 实现](https://www.douban.com/doulist/2545443/) -- [作为测试你应该知道的 JAVA OOM 及定位分析](https://www.jianshu.com/p/28935cbfbae0) -- [异常、堆内存溢出、OOM 的几种情况](https://blog.csdn.net/sinat_29912455/article/details/51125748) -- [介绍 JVM 中 OOM 的 8 种类型](https://tianmingxing.com/2019/11/17/%E4%BB%8B%E7%BB%8DJVM%E4%B8%ADOOM%E7%9A%848%E7%A7%8D%E7%B1%BB%E5%9E%8B/) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/06.JVM/12.JVM_GUI\345\267\245\345\205\267.md" "b/source/_posts/01.Java/01.JavaSE/06.JVM/12.JVM_GUI\345\267\245\345\205\267.md" deleted file mode 100644 index 359aa80a80..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/06.JVM/12.JVM_GUI\345\267\245\345\205\267.md" +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: JVM GUI 工具 -date: 2020-07-30 17:56:33 -order: 12 -categories: - - Java - - JavaSE - - JVM -tags: - - Java - - JavaSE - - JVM - - GUI -permalink: /pages/43a8e5/ ---- - -# JVM GUI 工具 - -> Java 程序员免不了故障排查工作,所以经常需要使用一些 JVM 工具。 -> -> 本文系统性的介绍一下常用的 JVM GUI 工具。 - -## jconsole - -> jconsole 是 JDK 自带的 GUI 工具。**jconsole(Java Monitoring and Management Console) 是一种基于 JMX 的可视化监视与管理工具**。 -> -> jconsole 的管理功能是针对 JMX MBean 进行管理,由于 MBean 可以使用代码、中间件服务器的管理控制台或所有符合 JMX 规范的软件进行访问。 - -注意:使用 jconsole 的前提是 Java 应用开启 JMX。 - -### 开启 JMX - -Java 应用开启 JMX 后,可以使用 `jconsole` 或 `jvisualvm` 进行监控 Java 程序的基本信息和运行情况。 - -开启方法是,在 java 指令后,添加以下参数: - -```java --Dcom.sun.management.jmxremote=true --Dcom.sun.management.jmxremote.ssl=false --Dcom.sun.management.jmxremote.authenticate=false --Djava.rmi.server.hostname=127.0.0.1 --Dcom.sun.management.jmxremote.port=18888 -``` - -- `-Djava.rmi.server.hostname` - 指定 Java 程序运行的服务器 -- `-Dcom.sun.management.jmxremote.port` - 指定 JMX 服务监听端口 - -### 连接 jconsole - -如果是本地 Java 进程,jconsole 可以直接绑定连接。 - -如果是远程 Java 进程,需要连接 Java 进程的 JMX 端口。 - -![Connecting to a JMX Agent Using the JMX Service URL](https://docs.oracle.com/javase/8/docs/technotes/guides/management/figures/connectadv.gif) - -### jconsole 界面 - -进入 jconsole 应用后,可以看到以下 tab 页面。 - -- `概述` - 显示有关 Java VM 和监视值的概述信息。 -- `内存` - 显示有关内存使用的信息。内存页相当于可视化的 `jstat` 命令。 -- `线程` - 显示有关线程使用的信息。 -- `类` - 显示有关类加载的信息。 -- `VM 摘要` - 显示有关 Java VM 的信息。 -- `MBean` - 显示有关 MBean 的信息。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730151422.png) - -## jvisualvm - -> jvisualvm 是 JDK 自带的 GUI 工具。**jvisualvm(All-In-One Java Troubleshooting Tool) 是多合一故障处理工具**。它支持运行监视、故障处理、性能分析等功能。 - -个人觉得 jvisualvm 比 jconsole 好用。 - -### jvisualvm 概述页面 - -jvisualvm 概述页面可以查看当前 Java 进程的基本信息,如:JDK 版本、Java 进程、JVM 参数等。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150147.png) - -### jvisualvm 监控页面 - -在 jvisualvm 监控页面,可以看到 Java 进程的 CPU、内存、类加载、线程的实时变化。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150254.png) - -### jvisualvm 线程页面 - -jvisualvm 线程页面展示了当前的线程状态。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150416.png) - -jvisualvm 还可以生成线程 Dump 文件,帮助进一步分析线程栈信息。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730150830.png) - -### jvisualvm 抽样器页面 - -jvisualvm 可以对 CPU、内存进行抽样,帮助我们进行性能分析。 - -## MAT - -[MAT](https://www.eclipse.org/mat/) 即 Eclipse Memory Analyzer Tool 的缩写。 - -MAT 本身也能够获取堆的二进制快照。该功能将借助 `jps` 列出当前正在运行的 Java 进程,以供选择并获取快照。由于 `jps` 会将自己列入其中,因此你会在列表中发现一个已经结束运行的 `jps` 进程。 - -MAT 可以独立安装([官方下载地址](http://www.eclipse.org/mat/downloads.php)),也可以作为 Eclipse IDE 的插件安装。 - -### MAT 配置 - -MAT 解压后,安装目录下有个 `MemoryAnalyzer.ini` 文件。 - -`MemoryAnalyzer.ini` 中有个重要的参数 `Xmx` 表示最大内存,默认为:`-vmargs -Xmx1024m` - -如果试图用 MAT 导入的 dump 文件超过 1024 M,会报错: - -```shell -An internal error occurred during: "Parsing heap dump from XXX" -``` - -此时,可以适当调整 `Xmx` 大小。如果设置的 `Xmx` 数值过大,本机内存不足以支撑,启动 MAT 会报错: - -``` -Failed to create the Java Virtual Machine -``` - -### MAT 分析 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200308092746.png) - -点击 Leak Suspects 可以进入内存泄漏页面。 - -(1)首先,可以查看饼图了解内存的整体消耗情况 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200308150556.png) - -(2)缩小范围,寻找问题疑似点 - -![img](https://img-blog.csdn.net/20160223202154818) - -可以点击进入详情页面,在详情页面 Shortest Paths To the Accumulation Point 表示 GC root 到内存消耗聚集点的最短路径,如果某个内存消耗聚集点有路径到达 GC root,则该内存消耗聚集点不会被当做垃圾被回收。 - -为了找到内存泄露,我获取了两个堆转储文件,两个文件获取时间间隔是一天(因为内存只是小幅度增长,短时间很难发现问题)。对比两个文件的对象,通过对比后的结果可以很方便定位内存泄露。 - -MAT 同时打开两个堆转储文件,分别打开 Histogram,如下图。在下图中方框 1 按钮用于对比两个 Histogram,对比后在方框 2 处选择 Group By package,然后对比各对象的变化。不难发现 heap3.hprof 比 heap6.hprof 少了 64 个 eventInfo 对象,如果对代码比较熟悉的话想必这样一个结果是能够给程序员一定的启示的。而我也是根据这个启示差找到了最终内存泄露的位置。 -![img](https://img-blog.csdn.net/20160223203226362) - -## JProfile - -[JProfiler](https://www.ej-technologies.com/products/jprofiler/overview.html) 是一款性能分析工具。 - -由于它是收费的,所以我本人使用较少。但是,它确实功能强大,且方便使用,还可以和 Intellij Idea 集成。 - -## Arthas - -[Arthas](https://github.com/alibaba/arthas) 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。在线排查问题,无需重启;动态跟踪 Java 代码;实时监控 JVM 状态。 - -Arthas 支持 JDK 6+,支持 Linux/Mac/Windows,采用命令行交互模式,同时提供丰富的 `Tab` 自动补全功能,进一步方便进行问题的定位和诊断。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200730145030.png) - -### Arthas 基础命令 - -- help——查看命令帮助信息 -- [cat](https://alibaba.github.io/arthas/cat.html)——打印文件内容,和 linux 里的 cat 命令类似 -- [echo](https://alibaba.github.io/arthas/echo.html)–打印参数,和 linux 里的 echo 命令类似 -- [grep](https://alibaba.github.io/arthas/grep.html)——匹配查找,和 linux 里的 grep 命令类似 -- [tee](https://alibaba.github.io/arthas/tee.html)——复制标准输入到标准输出和指定的文件,和 linux 里的 tee 命令类似 -- [pwd](https://alibaba.github.io/arthas/pwd.html)——返回当前的工作目录,和 linux 命令类似 -- cls——清空当前屏幕区域 -- session——查看当前会话的信息 -- [reset](https://alibaba.github.io/arthas/reset.html)——重置增强类,将被 Arthas 增强过的类全部还原,Arthas 服务端关闭时会重置所有增强过的类 -- version——输出当前目标 Java 进程所加载的 Arthas 版本号 -- history——打印命令历史 -- quit——退出当前 Arthas 客户端,其他 Arthas 客户端不受影响 -- stop——关闭 Arthas 服务端,所有 Arthas 客户端全部退出 -- [keymap](https://alibaba.github.io/arthas/keymap.html)——Arthas 快捷键列表及自定义快捷键 - -### Arthas jvm 相关命令 - -- [dashboard](https://alibaba.github.io/arthas/dashboard.html)——当前系统的实时数据面板 -- [thread](https://alibaba.github.io/arthas/thread.html)——查看当前 JVM 的线程堆栈信息 -- [jvm](https://alibaba.github.io/arthas/jvm.html)——查看当前 JVM 的信息 -- [sysprop](https://alibaba.github.io/arthas/sysprop.html)——查看和修改 JVM 的系统属性 -- [sysenv](https://alibaba.github.io/arthas/sysenv.html)——查看 JVM 的环境变量 -- [vmoption](https://alibaba.github.io/arthas/vmoption.html)——查看和修改 JVM 里诊断相关的 option -- [perfcounter](https://alibaba.github.io/arthas/perfcounter.html)——查看当前 JVM 的 Perf Counter 信息 -- [logger](https://alibaba.github.io/arthas/logger.html)——查看和修改 logger -- [getstatic](https://alibaba.github.io/arthas/getstatic.html)——查看类的静态属性 -- [ognl](https://alibaba.github.io/arthas/ognl.html)——执行 ognl 表达式 -- [mbean](https://alibaba.github.io/arthas/mbean.html)——查看 Mbean 的信息 -- [heapdump](https://alibaba.github.io/arthas/heapdump.html)——dump java heap, 类似 jmap 命令的 heap dump 功能 - -### Arthas class/classloader 相关命令 - -- [sc](https://alibaba.github.io/arthas/sc.html)——查看 JVM 已加载的类信息 -- [sm](https://alibaba.github.io/arthas/sm.html)——查看已加载类的方法信息 -- [jad](https://alibaba.github.io/arthas/jad.html)——反编译指定已加载类的源码 -- [mc](https://alibaba.github.io/arthas/mc.html)——内存编译器,内存编译`.java`文件为`.class`文件 -- [redefine](https://alibaba.github.io/arthas/redefine.html)——加载外部的`.class`文件,redefine 到 JVM 里 -- [dump](https://alibaba.github.io/arthas/dump.html)——dump 已加载类的 byte code 到特定目录 -- [classloader](https://alibaba.github.io/arthas/classloader.html)——查看 classloader 的继承树,urls,类加载信息,使用 classloader 去 getResource - -### Arthas monitor/watch/trace 相关命令 - -> 请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 `stop` 或将增强过的类执行 `reset` 命令。 - -- [monitor](https://alibaba.github.io/arthas/monitor.html)——方法执行监控 -- [watch](https://alibaba.github.io/arthas/watch.html)——方法执行数据观测 -- [trace](https://alibaba.github.io/arthas/trace.html)——方法内部调用路径,并输出方法路径上的每个节点上耗时 -- [stack](https://alibaba.github.io/arthas/stack.html)——输出当前方法被调用的调用路径 -- [tt](https://alibaba.github.io/arthas/tt.html)——方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 - -## 参考资料 - -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) -- [jconsole 官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/management/jconsole.html) -- [jconsole 工具使用](https://www.cnblogs.com/kongzhongqijing/articles/3621441.html) -- [jvisualvm 官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/visualvm/index.html) -- [Java jvisualvm 简要说明](https://blog.csdn.net/a19881029/article/details/8432368) -- [利用内存分析工具(Memory Analyzer Tool,MAT)分析 java 项目内存泄露](https://blog.csdn.net/wanghuiqi2008/article/details/50724676) \ No newline at end of file diff --git a/source/_posts/01.Java/01.JavaSE/06.JVM/README.md b/source/_posts/01.Java/01.JavaSE/06.JVM/README.md deleted file mode 100644 index df64d7f65d..0000000000 --- a/source/_posts/01.Java/01.JavaSE/06.JVM/README.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: JVM 教程 -date: 2020-06-04 13:51:01 -categories: - - Java - - JavaSE - - JVM -tags: - - Java - - JavaSE - - JVM -permalink: /pages/51172b/ -hidden: true -index: false ---- - -# JVM 教程 - -> 【Java 虚拟机】总结、整理了个人对于 JVM 的学习、应用心得。 - -## 📖 内容 - -- [JVM 体系结构](01.JVM体系结构.md) -- [JVM 内存区域](02.JVM内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` -- [JVM 垃圾收集](03.JVM垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` -- [JVM 类加载](04.JVM类加载.md) - 关键词:`ClassLoader`、`双亲委派` -- [JVM 字节码](05.JVM字节码.md) - 关键词:`bytecode`、`asm`、`javassist` -- [JVM 命令行工具](11.JVM命令行工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo` -- [JVM GUI 工具](12.JVM_GUI工具.md) - 关键词:`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` -- [JVM 实战](21.JVM实战.md) - 关键词:`配置`、`调优` -- [Java 故障诊断](22.Java故障诊断.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` - -## 📚 资料 - -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/82) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) -- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) -- [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) -- [从表到里学习 JVM 实现](https://www.douban.com/doulist/2545443/) - -## 🚪 传送 - -◾ 🏠 [JAVACORE 首页](https://github.com/dunwu/javacore) ◾ 🎯 [钝悟的博客](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git a/source/_posts/01.Java/01.JavaSE/06.JVM/jvm-and-java.md b/source/_posts/01.Java/01.JavaSE/06.JVM/jvm-and-java.md deleted file mode 100644 index d5c1e26a59..0000000000 --- a/source/_posts/01.Java/01.JavaSE/06.JVM/jvm-and-java.md +++ /dev/null @@ -1,197 +0,0 @@ ---- -title: jvm-and-java -date: 2020-10-16 20:29:24 -categories: - - Java - - JavaSE - - JVM -tags: - - Java - - JavaSE - - JVM -permalink: /pages/ebf8d4/ ---- - -# JVM 和 Java 特性 - -## JVM 如何执行方法调用 - -在 Java 程序里,如果同一个类中出现多个名字相同,并且参数类型相同的方法,那么它无法通过编译。如果我们想要在同一个类中定义名字相同的方法,那么它们的参数类型必须不同。这些方法之间的关系,我们称之为重载。 - -重载的方法在编译过程中即可完成识别。具体到每一个方法调用,Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: - -1. 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法; -2. 如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法; -3. 如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。 - -### 静态绑定和动态绑定 - -Java 虚拟机识别方法的关键在于类名、方法名以及方法描述符(method descriptor)。方法描述符,它是由方法的参数类型以及返回类型所构成。 - -Java 虚拟机中关于方法重写的判定同样基于方法描述符。也就是说,如果子类定义了与父类中非私有、非静态方法同名的方法,那么只有当这两个方法的参数类型以及返回类型一致,Java 虚拟机才会判定为重写。 - -由于对重载方法的区分在编译阶段已经完成,我们可以认为 Java 虚拟机不存在重载这一概念。因此,在某些文章中,重载也被称为**静态绑定(static binding)**,或者编译时多态(compile-time polymorphism);而重写则被称为**动态绑定(dynamic binding)**。 - -确切地说,Java 虚拟机中的静态绑定指的是在解析时便能够直接识别目标方法的情况,而动态绑定则指的是需要在运行过程中根据调用者的动态类型来识别目标方法的情况。 - -具体来说,Java 字节码中与调用相关的指令共有五种。 - -1. `invokestatic`:用于调用静态方法。 -2. `invokespecial`:用于调用私有实例方法、构造器,以及使用 `super` 关键字调用父类的实例方法或构造器,和所实现接口的默认方法。 -3. `invokevirtual`:用于调用非私有实例方法。 -4. `invokeinterface`:用于调用接口方法。 -5. `invokedynamic`:用于调用动态方法。 - -【示例】 - -```java -interface 客户 { - boolean isVIP(); -} - -class 商户 { - public double 折后价格 (double 原价, 客户 某客户) { - return 原价 * 0.8d; - } -} - -class 奸商 extends 商户 { - @Override - public double 折后价格 (double 原价, 客户 某客户) { - if (某客户.isVIP()) { // invokeinterface - return 原价 * 价格歧视 (); // invokestatic - } else { - return super. 折后价格 (原价, 某客户); // invokespecial - } - } - public static double 价格歧视 () { - // 咱们的杀熟算法太粗暴了,应该将客户城市作为随机数生成器的种子。 - return new Random() // invokespecial - .nextDouble() // invokevirtual - + 0.8d; - } -} -``` - -### 调用指令的符号引用 - -在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java 编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。 - -符号引用存储在 class 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。 - -对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。 - -### 虚方法调用 - -Java 里所有非私有实例方法调用都会被编译成 invokevirtual 指令,而接口方法调用都会被编译成 invokeinterface 指令。这两种指令,均属于 Java 虚拟机中的虚方法调用。 - -在 Java 虚拟机中,静态绑定包括用于调用静态方法的 invokestatic 指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的 invokespecial 指令。如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法。 - -### 方法表 - -方法表是 Java 虚拟机实现动态绑定的关键所在。 - -方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。 - -这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。方法表满足两个特质:其一,子类方法表中包含父类方法表中的所有方法;其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。 - -在执行过程中,Java 虚拟机将获取调用者的实际类型,并在该实际类型的虚方法表中,根据索引值获得目标方法。这个过程便是动态绑定。 - -### 内联缓存 - -内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。 - -## JVM 如何处理异常 - -### JVM 如何捕获异常 - -在编译生成的字节码中,每个方法都附带一个异常表。异常表中的每一个条目代表一个异常处理器,并且由 from 指针、to 指针、target 指针以及所捕获的异常类型构成。这些指针的值是字节码索引(bytecode index,bci),用以定位字节码。 - -其中,from 指针和 to 指针标示了该异常处理器所监控的范围,例如 try 代码块所覆盖的范围。target 指针则指向异常处理器的起始位置,例如 catch 代码块的起始位置。 - -```java -public static void main(String[] args) { - try { - mayThrowException(); - } catch (Exception e) { - e.printStackTrace(); - } -} -// 对应的 Java 字节码 -public static void main(java.lang.String[]); - Code: - 0: invokestatic mayThrowException:()V - 3: goto 11 - 6: astore_1 - 7: aload_1 - 8: invokevirtual java.lang.Exception.printStackTrace - 11: return - Exception table: - from to target type - 0 3 6 Class java/lang/Exception // 异常表条目 -``` - -说明:编译过后,该方法的异常表拥有一个条目。其 from 指针和 to 指针分别为 0 和 3,代表它的监控范围从索引为 0 的字节码开始,到索引为 3 的字节码结束(不包括 3)。该条目的 target 指针是 6,代表这个异常处理器从索引为 6 的字节码开始。条目的最后一列,代表该异常处理器所捕获的异常类型正是 Exception。 - -当程序触发异常时,Java 虚拟机会从上至下遍历异常表中的所有条目。当触发异常的字节码的索引值在某个异常表条目的监控范围内,Java 虚拟机会判断所抛出的异常和该条目想要捕获的异常是否匹配。如果匹配,Java 虚拟机会将控制流转移至该条目 target 指针指向的字节码。 - -如果遍历完所有异常表条目,Java 虚拟机仍未匹配到异常处理器,那么它会弹出当前方法对应的 Java 栈帧,并且在调用者(caller)中重复上述操作。在最坏情况下,Java 虚拟机需要遍历当前线程 Java 栈上所有方法的异常表。 - -### Java 7 的 Supressed 异常以及语法糖 - -如果 catch 代码块捕获了异常,并且触发了另一个异常,那么 finally 捕获并且重抛的异常是哪个呢?答案是后者。也就是说原本的异常便会被忽略掉,这对于代码调试来说十分不利。 - -Java 7 引入了 Supressed 异常来解决这个问题。这个新特性允许开发人员将一个异常附于另一个异常之上。因此,抛出的异常可以附带多个异常的信息。 - -Java 7 专门构造了一个名为 try-with-resources 的语法糖,在字节码层面自动使用 Supressed 异常。 - -```java -public class Foo implements AutoCloseable { - private final String name; - public Foo(String name) { this.name = name; } - - @Override - public void close() { - throw new RuntimeException(name); - } - - public static void main(String[] args) { - try (Foo foo0 = new Foo("Foo0"); // try-with-resources - Foo foo1 = new Foo("Foo1"); - Foo foo2 = new Foo("Foo2")) { - throw new RuntimeException("Initial"); - } - } -} - -// 运行结果: -Exception in thread "main" java.lang.RuntimeException: Initial - at Foo.main(Foo.java:18) - Suppressed: java.lang.RuntimeException: Foo2 - at Foo.close(Foo.java:13) - at Foo.main(Foo.java:19) - Suppressed: java.lang.RuntimeException: Foo1 - at Foo.close(Foo.java:13) - at Foo.main(Foo.java:19) - Suppressed: java.lang.RuntimeException: Foo0 - at Foo.close(Foo.java:13) - at Foo.main(Foo.java:19) -``` - -除了 try-with-resources 语法糖之外,Java 7 还支持在同一 catch 代码块中捕获多种异常。实际实现非常简单,生成多个异常表条目即可。 - -``` -// 在同一 catch 代码块中捕获多种异常 -try { - ... -} catch (SomeException | OtherException e) { - ... -} -``` - -## JVM 如何实现反射 - -## 参考资料 - -- [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) \ No newline at end of file diff --git "a/source/_posts/01.Java/01.JavaSE/99.Java\351\235\242\350\257\225.md" "b/source/_posts/01.Java/01.JavaSE/99.Java\351\235\242\350\257\225.md" deleted file mode 100644 index 1444453af5..0000000000 --- "a/source/_posts/01.Java/01.JavaSE/99.Java\351\235\242\350\257\225.md" +++ /dev/null @@ -1,1036 +0,0 @@ ---- -title: Java 面试总结 -date: 2020-06-04 13:51:00 -order: 99 -categories: - - Java - - JavaSE -tags: - - Java - - JavaSE - - 面试 -permalink: /pages/5f886e/ ---- - -# Java 面试总结 - -## 基础 - -### 工具类 - -#### String - -> String 类能被继承吗? -> -> String,StringBuffer,StringBuilder 的区别。 - -String 类不能被继承。因为其被 final 修饰,所以无法被继承。 - -StringBuffer,StringBuilder 拼接字符串,使用 append 比 String 效率高。因为 String 会隐式 new String 对象。 - -StringBuffer 主要方法都用 synchronized 修饰,是线程安全的;而 StringBuilder 不是。 - -### 面向对象 - -> 抽象类和接口的区别? -> -> 类可以继承多个类么?接口可以继承多个接口么?类可以实现多个接口么? - -类只能继承一个类,但是可以实现多个接口。接口可以继承多个接口。 - -> 继承和聚合的区别在哪? - -一般,能用聚合就别用继承。 - -### 反射 - -#### ⭐ 创建实例 - -> 反射创建实例有几种方式? - -通过反射来创建实例对象主要有两种方式: - -- 用 `Class` 对象的 `newInstance` 方法。 -- 用 `Constructor` 对象的 `newInstance` 方法。 - -#### ⭐ 加载实例 - -> 加载实例有几种方式? -> -> Class.forName("className") 和 ClassLoader.laodClass("className") 有什么区别? - -- `Class.forName("className")` 加载的是已经初始化到 JVM 中的类。 -- `ClassLoader.loadClass("className")` 装载的是还没有初始化到 JVM 中的类。 - -#### ⭐⭐ 动态代理 - -> 动态代理有几种实现方式?有什么特点? -> -> JDK 动态代理和 CGLIB 动态代理有什么区别? - -(1)JDK 方式 - -代理类与委托类实现同一接口,主要是通过代理类实现 `InvocationHandler` 并重写 `invoke` 方法来进行动态代理的,在 `invoke` 方法中将对方法进行处理。 - -JDK 动态代理特点: - -- 优点:相对于静态代理模式,不需要硬编码接口,代码复用率高。 -- 缺点:强制要求代理类实现 `InvocationHandler` 接口。 - -(2)CGLIB - -CGLIB 底层,其实是借助了 ASM 这个强大的 Java 字节码框架去进行字节码增强操作。 - -CGLIB 动态代理特点: - -优点:使用字节码增强,比 JDK 动态代理方式性能高。可以在运行时对类或者是接口进行增强操作,且委托类无需实现接口。 - -缺点:不能对 `final` 类以及 `final` 方法进行代理。 - -### JDK8 - -### 其他 - -#### ⭐ hashcode - -> 有`==`运算符了,为什么还需要 equals 啊? -> -> 说一说你对 java.lang.Object 对象中 hashCode 和 equals 方法的理解。在什么场景下需 -> 要重新实现这两个方法。 -> -> 有没有可能 2 个不相等的对象有相同的 hashcode - -(1)有`==`运算符了,为什么还需要 equals 啊? - -equals 等价于`==`,而`==`运算符是判断两个对象是不是同一个对象,即他们的**地址是否相等**。而覆写 equals 更多的是追求两个对象在**逻辑上的相等**,你可以说是**值相等**,也可说是**内容相等**。 - -(2)说一说你对 java.lang.Object 对象中 hashCode 和 equals 方法的理解。在什么场景下需 -要重新实现这两个方法。 - -在集合查找时,hashcode 能大大降低对象比较次数,提高查找效率! - -(3)有没有可能 2 个不相等的对象有相同的 hashcode - -有可能。 - -- 如果两个对象 equals,Java 运行时环境会认为他们的 hashcode 一定相等。 -- 如果两个对象不 equals,他们的 hashcode 有可能相等。 -- 如果两个对象 hashcode 相等,他们不一定 equals。 -- 如果两个对象 hashcode 不相等,他们一定不 equals。 - -## IO - -### NIO - -> 什么是 NIO? -> -> NIO 和 BIO、AIO 有何差别? - -### 序列化 - -#### ⭐ 序列化问题 - -> 序列化、反序列化有哪些问题?如何解决? - -Java 的序列化能保证对象状态的持久保存,但是遇到一些对象结构复杂的情况还是难以处理,这里归纳一下: - -- 当父类继承 `Serializable` 接口时,所有子类都可以被序列化。 -- 子类实现了 `Serializable` 接口,父类没有,则父类的属性不会被序列化(不报错,数据丢失),子类的属性仍可以正确序列化。 -- 如果序列化的属性是对象,则这个对象也必须实现 `Serializable` 接口,否则会报错。 -- 在反序列化时,如果对象的属性有修改或删减,则修改的部分属性会丢失,但不会报错。 -- 在反序列化时,如果 `serialVersionUID` 被修改,则反序列化时会失败。 - -## 容器 - -### List - -#### ArrayList 和 LinkedList 有什么区别? - -ArrayList 是数组链表,访问效率更高。 - -LinkedList 是双链表,数据有序存储。 - -### Map - -请描述 HashMap 的实现原理? - -## 并发 - -### 并发简介 - -#### 什么是进程?什么是线程?进程和线程的区别? - -- 什么是进程? - - 简言之,进程可视为一个正在运行的程序。 - - 进程是具有一定独立功能的程序关于某个数据集合上的一次运行活动。进程是操作系统进行资源分配的基本单位。 -- 什么是线程? - - 线程是操作系统进行调度的基本单位。 -- 进程 vs. 线程 - - 一个程序至少有一个进程,一个进程至少有一个线程。 - - 线程比进程划分更细,所以执行开销更小,并发性更高。 - - 进程是一个实体,拥有独立的资源;而同一个进程中的多个线程共享进程的资源。 - -#### 并发(多线程)编程的好处是什么? - -- 更有效率的利用多处理器核心 -- 更快的响应时间 -- 更好的编程模型 - -#### 并发一定比串行更快吗? - -答:否。 - -要点:**创建线程和线程上下文切换有一定开销**。 - -说明:即使是单核处理器也支持多线程。CPU 通过给每个线程分配时间切片的算法来循环执行任务,当前任务执行一个时间片后会切换到下一个任务。但是,在切换前会保持上一个任务的状态,以便下次切换回这个任务时,可以再加载这个任务的状态。所以**任务从保存到再加载的过程就是一次上下文切换**。 - -引申 - -- 如何减少上下文切换? - - 尽量少用锁 - - CAS 算法 - - 线程数要合理 - - 协程:在单线程中实现多任务调度,并在单线程中维持多个任务的切换 - -#### 如何让正在运行的线程暂停一段时间? - -我们可以使用 `Thread` 类的 Sleep() 方法让线程暂停一段时间。 - -需要注意的是,这并不会让线程终止,一旦从休眠中唤醒线程,线程的状态将会被改变为 Runnable,并且根据线程调度,它将得到执行。 - -#### 什么是线程调度器(Thread Scheduler)和时间分片(Time Slicing)? - -线程调度器是一个操作系统服务,它负责为 `Runnable` 状态的线程分配 CPU 时间。一旦我们创建一个线程并启动它,它的执行便依赖于线程调度器的实现。 - -时间分片是指将可用的 CPU 时间分配给可用的 `Runnable` 线程的过程。 - -分配 CPU 时间可以基于线程优先级或者线程等待的时间。线程调度并不受到 Java 虚拟机控制,所以由应用程序来控制它是更好的选择(也就是说不要让你的程序依赖于线程的优先级)。 - -#### 在多线程中,什么是上下文切换(context-switching)? - -上下文切换是存储和恢复 CPU 状态的过程,它使得线程执行能够从中断点恢复执行。上下文切换是多任务操作系统和多线程环境的基本特征。 - -#### 如何确保线程安全? - -- 原子类(atomic concurrent classes) -- 锁 -- `volatile` 关键字 -- 不变类和线程安全类 - -#### 什么是死锁(Deadlock)?如何分析和避免死锁? - -死锁是指两个以上的线程永远相互阻塞的情况,这种情况产生至少需要两个以上的线程和两个以上的资源。 - -分析死锁,我们需要查看 Java 应用程序的线程转储。我们需要找出那些状态为 BLOCKED 的线程和他们等待的资源。每个资源都有一个唯一的 id,用这个 id 我们可以找出哪些线程已经拥有了它的对象锁。 - -避免嵌套锁,只在需要的地方使用锁和避免无限期等待是避免死锁的通常办法。 - -### 线程基础 - -#### Java 线程生命周期中有哪些状态?各状态之间如何切换? - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-thread_1.png) - -`java.lang.Thread.State` 中定义了 **6** 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。 - -以下是各状态的说明,以及状态间的联系: - -- **开始(New)** - 还没有调用 `start()` 方法的线程处于此状态。 -- **可运行(Runnable)** - 已经调用了 `start()` 方法的线程状态。此状态意味着,线程已经准备好了,一旦被线程调度器分配了 CPU 时间片,就可以运行线程。 -- **阻塞(Blocked)** - 阻塞状态。线程阻塞的线程状态等待监视器锁定。处于阻塞状态的线程正在等待监视器锁定,以便在调用 `Object.wait()` 之后输入同步块/方法或重新输入同步块/方法。 -- **等待(Waiting)** - 等待状态。一个线程处于等待状态,是由于执行了 3 个方法中的任意方法: - - `Object.wait()` - - `Thread.join()` - - `LockSupport.park()` -- **定时等待(Timed waiting)** - 等待指定时间的状态。一个线程处于定时等待状态,是由于执行了以下方法中的任意方法: - - `Thread.sleep(sleeptime)` - - `Object.wait(timeout)` - - `Thread.join(timeout)` - - `LockSupport.parkNanos(timeout)` - - `LockSupport.parkUntil(timeout)` -- **终止(Terminated)** - 线程 `run()` 方法执行结束,或者因异常退出了 `run()` 方法,则该线程结束生命周期。死亡的线程不可再次复生。 - -> 👉 参考阅读:[Java`Thread` Methods and `Thread` States](https://www.w3resource.com/java-tutorial/java-threadclass-methods-and-threadstates.php) -> 👉 参考阅读:[Java 线程的 5 种状态及切换(透彻讲解)](https://blog.csdn.net/pange1991/article/details/53860651) - -#### 创建线程有哪些方式?这些方法各自利弊是什么? - -创建线程主要有三种方式: - -**1. 继承 `Thread` 类** - -- 定义 `Thread` 类的子类,并重写该类的 `run()` 方法,该 `run()` 方法的方法体就代表了线程要完成的任务。因此把 `run()` 方法称为执行体。 -- 创建 `Thread` 子类的实例,即创建了线程对象。 -- 调用线程对象的 `start()` 方法来启动该线程。 - -**2. 实现 `Runnable` 接口** - -- 定义 `Runnable` 接口的实现类,并重写该接口的 `run()` 方法,该 `run()` 方法的方法体同样是该线程的线程执行体。 -- 创建 `Runnable` 实现类的实例,并以此实例作为 `Thread` 对象,该 `Thread` 对象才是真正的线程对象。 -- 调用线程对象的 start() 方法来启动该线程。 - -**3. 通过 `Callable` 接口和 `Future` 接口** - -- 创建 `Callable` 接口的实现类,并实现 `call()` 方法,该 `call()` 方法将作为线程执行体,并且有返回值。 -- 创建 `Callable` 实现类的实例,使用 `FutureTask` 类来包装 `Callable` 对象,该 `FutureTask` 对象封装了该 `Callable` 对象的 `call()` 方法的返回值。 -- 使用 `FutureTask` 对象作为 `Thread` 对象的 target 创建并启动新线程。 -- 调用 `FutureTask` 对象的 `get()` 方法来获得子线程执行结束后的返回值 - -三种创建线程方式对比 - -- 实现 `Runnable` 接口优于继承 `Thread` 类,因为根据开放封闭原则——实现接口更便于扩展; -- 实现 `Runnable` 接口的线程没有返回值;而使用 `Callable` / `Future` 方式可以让线程有返回值。 - -> 👉 参考阅读:[Java 创建线程的三种方式及其对比](https://blog.csdn.net/longshengguoji/article/details/41126119) - -#### 什么是 `Callable` 和 `Future`?什么是 `FutureTask`? - -**什么是 `Callable` 和 `Future`?** - -Java 5 在 concurrency 包中引入了 `Callable` 接口,它和 `Runnable` 接口很相似,但它可以返回一个对象或者抛出一个异常。 - -`Callable` 接口使用泛型去定义它的返回类型。`Executors` 类提供了一些有用的方法去在线程池中执行 `Callable` 内的任务。由于 `Callable` 任务是并行的,我们必须等待它返回的结果。`Future` 对象为我们解决了这个问题。在线程池提交 `Callable` 任务后返回了一个 `Future` 对象,使用它我们可以知道 `Callable` 任务的状态和得到 `Callable` 返回的执行结果。`Future` 提供了 `get()` 方法让我们可以等待 `Callable` 结束并获取它的执行结果。 - -**什么是 `FutureTask`?** - -`FutureTask` 是 `Future` 的一个基础实现,我们可以将它同 `Executors` 使用处理异步任务。通常我们不需要使用 `FutureTask` 类,单当我们打算重写 `Future` 接口的一些方法并保持原来基础的实现是,它就变得非常有用。我们可以仅仅继承于它并重写我们需要的方法。阅读 Java `FutureTask` 例子,学习如何使用它。 - -> 👉 参考阅读:[Java 并发编程:Callable、Future 和 FutureTask](http://www.cnblogs.com/dolphin0520/p/3949310.html) - -#### `start()` 和 `run()` 有什么区别?可以直接调用 `Thread` 类的 `run()` 方法么? - -- `run()` 方法是线程的执行体。 -- `start()` 方法负责启动线程,然后 JVM 会让这个线程去执行 `run()` 方法。 - -可以直接调用 `Thread` 类的 `run()` 方法么? - -- 可以。但是如果直接调用 `Thread` 的 `run()` 方法,它的行为就会和普通的方法一样。 -- 为了在新的线程中执行我们的代码,必须使用 `start()` 方法。 - -#### `sleep()`、`yield()`、`join()` 方法有什么区别?为什么 `sleep()` 和 `yield()` 方法是静态(static)的? - -**`yield()`** - -- `yield()` 方法可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程从 **Running** 状态转入 `Runnable` 状态。 -- 当某个线程调用了 `yield()` 方法暂停之后,只有优先级与当前线程相同,或者优先级比当前线程更高的处于就绪状态的线程才会获得执行的机会。 - -**`sleep()`** - -- `sleep()` 方法需要指定等待的时间,它可以让当前正在执行的线程在指定的时间内暂停执行,进入 **Blocked** 状态。 -- 该方法既可以让其他同优先级或者高优先级的线程得到执行的机会,也可以让低优先级的线程得到执行机会。 -- 但是,`sleep()` 方法不会释放“锁标志”,也就是说如果有 `synchronized` 同步块,其他线程仍然不能访问共享数据。 - -**`join()`** - -- `join()` 方法会使当前线程转入 **Blocked** 状态,等待调用 `join()` 方法的线程结束后才能继续执行。 - -**为什么 `sleep()` 和 `yield()` 方法是静态(static)的?** - -- `Thread` 类的 `sleep()` 和 `yield()` 方法将处理 **Running** 状态的线程。所以在其他处于非 **Running** 状态的线程上执行这两个方法是没有意义的。这就是为什么这些方法是静态的。它们可以在当前正在执行的线程中工作,并避免程序员错误的认为可以在其他非运行线程调用这些方法。 - -> 👉 参考阅读:[Java 线程中 yield 与 join 方法的区别](http://www.importnew.com/14958.html) -> 👉 参考阅读:[sleep(),wait(),yield()和 join()方法的区别](https://blog.csdn.net/xiangwanpeng/article/details/54972952) - -#### Java 的线程优先级如何控制?高优先级的 Java 线程一定先执行吗? - -**Java 中的线程优先级如何控制** - -- Java 中的线程优先级的范围是 `[1,10]`,一般来说,高优先级的线程在运行时会具有优先权。可以通过 `thread.setPriority(Thread.MAX_PRIORITY)` 的方式设置,默认优先级为 `5`。 - -**高优先级的 Java 线程一定先执行吗** - -- 即使设置了线程的优先级,也**无法保证高优先级的线程一定先执行**。 -- 原因:这是因为 **Java 线程优先级依赖于操作系统的支持**,然而,不同的操作系统支持的线程优先级并不相同,不能很好的和 Java 中线程优先级一一对应。 -- 结论:Java 线程优先级控制并不可靠。 - -#### 什么是守护线程?为什么要用守护线程?如何创建守护线程? - -**什么是守护线程** - -- 守护线程(Daemon Thread)是在后台执行并且不会阻止 JVM 终止的线程。 -- 与守护线程(Daemon Thread)相反的,叫用户线程(User Thread),也就是非守护线程。 - -**为什么要用守护线程** - -- 守护线程的优先级比较低,用于为系统中的其它对象和线程提供服务。典型的应用就是垃圾回收器。 - -**如何创建守护线程** - -- 使用 `thread.setDaemon(true)` 可以设置 thread 线程为守护线程。 -- 注意点: - - 正在运行的用户线程无法设置为守护线程,所以 `thread.setDaemon(true)` 必须在 `thread.start()` 之前设置,否则会抛出 `llegalThreadStateException` 异常; - - 一个守护线程创建的子线程依然是守护线程。 - - 不要认为所有的应用都可以分配给守护线程来进行服务,比如读写操作或者计算逻辑。 - -> 👉 参考阅读:[Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) - -#### 线程间是如何通信的? - -当线程间是可以共享资源时,线程间通信是协调它们的重要的手段。`Object` 类中 `wait()`, `notify()` 和 `notifyAll()` 方法可以用于线程间通信关于资源的锁的状态。 - -> 👉 参考阅读:[Java 并发编程:线程间协作的两种方式:wait、notify、notifyAll 和 Condition](http://www.cnblogs.com/dolphin0520/p/3920385.html) - -#### 为什么线程通信的方法 `wait()`, `notify()` 和 `notifyAll()` 被定义在 Object 类里? - -Java 的每个对象中都有一个锁(monitor,也可以成为监视器) 并且 `wait()`、`notify()` 等方法用于等待对象的锁或者通知其他线程对象的监视器可用。在 Java 的线程中并没有可供任何对象使用的锁和同步器。这就是为什么这些方法是 Object 类的一部分,这样 Java 的每一个类都有用于线程间通信的基本方法 - -#### 为什么 `wait()`, `notify()` 和 `notifyAll()` 必须在同步方法或者同步块中被调用? - -当一个线程需要调用对象的 `wait()` 方法的时候,这个线程必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 `notify()` 方法。同样的,当一个线程需要调用对象的 `notify()` 方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。 - -由于所有的这些方法都需要线程持有对象的锁,这样就只能通过同步来实现,所以他们只能在同步方法或者同步块中被调用。 - -### 并发机制的底层实现 - -> 👉 参考阅读:[Java 并发核心机制](https://dunwu.github.io/waterdrop/pages/2c6488/) - -#### ⭐⭐⭐ `synchronized` - -> `synchronized` 有什么作用? -> -> `synchronized` 的原理是什么? -> -> 同步方法和同步块,哪个更好? -> -> JDK1.6 对`synchronized` 做了哪些优化? -> -> 使用 `synchronized` 修饰静态方法和非静态方法有什么区别? - -**作用** - -**`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 - -`synchronized` 有 3 种应用方式: - -- **同步实例方法** - 对于普通同步方法,锁是当前实例对象 -- **同步静态方法** - 对于静态同步方法,锁是当前类的 `Class` 对象 -- **同步代码块** - 对于同步方法块,锁是 `synchonized` 括号里配置的对象 - -**原理** - -`synchronized` 经过编译后,会在同步块的前后分别形成 `monitorenter` 和 `monitorexit` 这两个字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。如果 `synchronized` 明确制定了对象参数,那就是这个对象的引用;如果没有明确指定,那就根据 `synchronized` 修饰的是实例方法还是静态方法,去对对应的对象实例或 `Class` 对象来作为锁对象。 - -`synchronized` 同步块对同一线程来说是可重入的,不会出现锁死问题。 - -`synchronized` 同步块是互斥的,即已进入的线程执行完成前,会阻塞其他试图进入的线程。 - -**优化** - -Java 1.6 以后,`synchronized` 做了大量的优化,其性能已经与 `Lock` 、`ReadWriteLock` 基本上持平。 - -`synchronized` 的优化是将锁粒度分为不同级别,`synchronized` 会根据运行状态动态的由低到高调整锁级别(**偏向锁** -> **轻量级锁** -> **重量级锁**),以减少阻塞。 - -**同步方法 or 同步块?** - -- 同步块是更好的选择。 -- 因为它不会锁住整个对象(当然你也可以让它锁住整个对象)。同步方法会锁住整个对象,哪怕这个类中有多个不相关联的同步块,这通常会导致他们停止执行并需要等待获得这个对象上的锁。 - -#### ⭐ `volatile` - -> `volatile` 有什么作用? -> -> `volatile` 的原理是什么? -> -> `volatile` 能代替锁吗? -> -> `volatile` 和 `synchronized` 的区别? - -**`volatile` 无法替代 `synchronized` ,因为 `volatile` 无法保证操作的原子性**。 - -**作用** - -被 `volatile` 关键字修饰的变量有两层含义: - -- **保证了不同线程对这个变量进行操作时的可见性**,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。 -- **禁止指令重排序**。 - -**原理** - -观察加入 volatile 关键字和没有加入 volatile 关键字时所生成的汇编代码发现,**加入 `volatile` 关键字时,会多出一个 `lock` 前缀指令**。 - -**`lock` 前缀指令实际上相当于一个内存屏障**(也成内存栅栏),内存屏障会提供 3 个功能: - -- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成; -- 它会强制将对缓存的修改操作立即写入主存; -- 如果是写操作,它会导致其他 CPU 中对应的缓存行无效。 - -**`volatile` 和 `synchronized` 的区别?** - -- `volatile` 本质是在告诉 jvm 当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取; `synchronized` 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。 -- `volatile` 仅能使用在变量级别;synchronized 则可以使用在变量、方法、和类级别的。 -- `volatile` 仅能实现变量的修改可见性,不能保证原子性;而 `synchronized` 则可以保证变量的修改可见性和原子性 -- `volatile` 不会造成线程的阻塞;synchronized 可能会造成线程的阻塞。 -- `volatile` 标记的变量不会被编译器优化;synchronized 标记的变量可以被编译器优化。 - -#### ⭐⭐ CAS - -> 什么是 CAS? -> -> CAS 有什么作用? -> -> CAS 的原理是什么? -> -> CAS 的三大问题? - -**作用** - -**CAS(Compare and Swap)**,字面意思为**比较并交换**。CAS 有 3 个操作数,分别是:内存值 V,旧的预期值 A,要修改的新值 B。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B,否则什么都不做。 - -**原理** - -Java 主要利用 `Unsafe` 这个类提供的 CAS 操作。`Unsafe` 的 CAS 依赖的是 JV M 针对不同的操作系统实现的 `Atomic::cmpxchg` 指令。 - -**三大问题** - -1. **ABA 问题**:因为 CAS 需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是 A,变成了 B,又变成了 A,那么使用 CAS 进行检查时会发现它的值没有发生变化,但是实际上却变化了。ABA 问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么 A-B-A 就会变成 1A-2B-3A。 -2. **循环时间长开销大**。自旋 CAS 如果长时间不成功,会给 CPU 带来非常大的执行开销。如果 JVM 能支持处理器提供的 pause 指令那么效率会有一定的提升,pause 指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使 CPU 不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起 CPU 流水线被清空(CPU pipeline flush),从而提高 CPU 的执行效率。 -3. **只能保证一个共享变量的原子操作**。当对一个共享变量执行操作时,我们可以使用循环 CAS 的方式来保证原子操作,但是对多个共享变量操作时,循环 CAS 就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量 i = 2,j=a,合并一下 ij=2a,然后用 CAS 来操作 ij。从 Java1.5 开始 JDK 提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作。 - -#### ⭐ `ThreadLocal` - -> `ThreadLocal` 有什么作用? -> -> `ThreadLocal` 的原理是什么? -> -> 如何解决 `ThreadLocal` 内存泄漏问题? - -**作用** - -**`ThreadLocal` 是一个存储线程本地副本的工具类**。 - -**原理** - -`Thread` 类中维护着一个 `ThreadLocal.ThreadLocalMap` 类型的成员 `threadLocals`。这个成员就是用来存储当前线程独占的变量副本。 - -`ThreadLocalMap` 是 `ThreadLocal` 的内部类,它维护着一个 `Entry` 数组, `Entry` 用于保存键值对,其 key 是 `ThreadLocal` 对象,value 是传递进来的对象(变量副本)。 `Entry` 继承了 `WeakReference` ,所以是弱引用。 - -**内存泄漏问题** - -ThreadLocalMap 的 `Entry` 继承了 `WeakReference`,所以它的 key (`ThreadLocal` 对象)是弱引用,而 value (变量副本)是强引用。 - -- 如果 `ThreadLocal` 对象没有外部强引用来引用它,那么 `ThreadLocal` 对象会在下次 GC 时被回收。 -- 此时,`Entry` 中的 key 已经被回收,但是 value 由于是强引用不会被垃圾收集器回收。如果创建 `ThreadLocal` 的线程一直持续运行,那么 value 就会一直得不到回收,产生内存泄露。 - -那么如何避免内存泄漏呢?方法就是:**使用 `ThreadLocal` 的 `set` 方法后,显示的调用 `remove` 方法** 。 - -### 内存模型 - -#### 什么是 Java 内存模型 - -- Java 内存模型即 Java Memory Model,简称 JMM。JMM 定义了 JVM 在计算机内存(RAM)中的工作方式。JMM 是隶属于 JVM 的。 -- 并发编程领域两个关键问题:线程间通信和线程间同步 -- 线程间通信机制 - - 共享内存 - 线程间通过写-读内存中的公共状态来隐式进行通信。 - - 消息传递 - java 中典型的消息传递方式就是 wait()和 notify()。 -- 线程间同步机制 - - 在共享内存模型中,必须显示指定某个方法或某段代码在线程间互斥地执行。 - - 在消息传递模型中,由于发送消息必须在接收消息之前,因此同步是隐式进行的。 -- Java 的并发采用的是共享内存模型 -- JMM 决定一个线程对共享变量的写入何时对另一个线程可见。 -- 线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。 -- JMM 把内存分成了两部分:线程栈区和堆区 - - 线程栈 - - JVM 中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈。随着代码的不断执行,调用栈会不断变化。 - - 线程栈还包含了当前方法的所有本地变量信息。线程中的本地变量对其它线程是不可见的。 - - 堆区 - - 堆区包含了 Java 应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如 Byte、Integer、Long 等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。 - - 一个本地变量如果是原始类型,那么它会被完全存储到栈区。 - - 一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。 - - 对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。 - - 对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-memory-model_3.png) - -> 👉 参考阅读:[全面理解 Java 内存模型](https://blog.csdn.net/suifeng3051/article/details/52611310) - -### 同步容器和并发容器 - -> 👉 参考阅读:[Java 并发容器](https://dunwu.github.io/waterdrop/pages/b067d6/) - -#### ⭐ 同步容器 - -> 什么是同步容器? -> -> 有哪些常见同步容器? -> -> 它们是如何实现线程安全的? -> -> 同步容器真的线程安全吗? - -**类型** - -`Vector`、`Stack`、`Hashtable` - -**作用/原理** - -同步容器的同步原理就是在方法上用 `synchronized` 修饰。 **`synchronized` 可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块**。 - -`synchronized` 的互斥同步会产生阻塞和唤醒线程的开销。显然,这种方式比没有使用 `synchronized` 的容器性能要差。 - -**线程安全** - -同步容器真的绝对安全吗? - -其实也未必。在做复合操作(非原子操作)时,仍然需要加锁来保护。常见复合操作如下: - -- **迭代**:反复访问元素,直到遍历完全部元素; -- **跳转**:根据指定顺序寻找当前元素的下一个(下 n 个)元素; -- **条件运算**:例如若没有则添加等; - -#### ⭐⭐⭐ ConcurrentHashMap - -> 请描述 ConcurrentHashMap 的实现原理? -> -> ConcurrentHashMap 为什么放弃了分段锁? - -基础数据结构原理和 `HashMap` 一样,JDK 1.7 采用 数组+单链表;JDK 1.8 采用数组+单链表+红黑树。 - -并发安全特性的实现: - -JDK 1.7: - -- 使用分段锁,设计思路是缩小锁粒度,提高并发吞吐。 -- 写数据时,会使用可重入锁去锁住分段(segment)。 - -JDK 1.8: - -- 取消分段锁,直接采用 `transient volatile HashEntry[] table` 保存数据,采用 table 数组元素作为锁,从而实现了对每一行数据进行加锁,进一步减少并发冲突的概率。 -- 写数据时,使用是 CAS + `synchronized`。 - - 根据 key 计算出 hashcode 。 - - 判断是否需要进行初始化。 - - `f` 即为当前 key 定位出的 Node,如果为空表示当前位置可以写入数据,利用 CAS 尝试写入,失败则自旋保证成功。 - - 如果当前位置的 `hashcode == MOVED == -1`,则需要进行扩容。 - - 如果都不满足,则利用 synchronized 锁写入数据。 - - 如果数量大于 `TREEIFY_THRESHOLD` 则要转换为红黑树。 - -#### ⭐⭐ CopyOnWriteArrayList - -> CopyOnWriteArrayList 的作用? -> -> CopyOnWriteArrayList 的原理? - -**作用** - -CopyOnWrite 字面意思为写入时复制。CopyOnWriteArrayList 是线程安全的 ArrayList。 - -**原理** - -- 在 `CopyOnWriteAarrayList` 中,读操作不同步,因为它们在内部数组的快照上工作,所以多个迭代器可以同时遍历而不会相互阻塞(1,2,4)。 -- 所有的写操作都是同步的。他们在备份数组(3)的副本上工作。写操作完成后,后备阵列将被替换为复制的阵列,并释放锁定。支持数组变得易变,所以替换数组的调用是原子(5)。 -- 写操作后创建的迭代器将能够看到修改的结构(6,7)。 -- 写时复制集合返回的迭代器不会抛出 ConcurrentModificationException,因为它们在数组的快照上工作,并且无论后续的修改(2,4)如何,都会像迭代器创建时那样完全返回元素。 - -

- -

- -### 并发锁 - -> 👉 参考阅读:[Java 并发锁](https://dunwu.github.io/waterdrop/pages/e2e047/) - -#### ⭐⭐ 锁类型 - -> Java 中有哪些锁? -> -> 这些锁有什么特性? - -**可重入锁** - -- **`ReentrantLock` 、`ReentrantReadWriteLock` 是可重入锁**。这点,从其命名也不难看出。 -- **`synchronized` 也是一个可重入锁**。 - -**公平锁与非公平锁** - -- **`synchronized` 只支持非公平锁**。 -- **`ReentrantLock` 、`ReentrantReadWriteLock`,默认是非公平锁,但支持公平锁**。 - -**独享锁与共享锁** - -- **`synchronized` 、`ReentrantLock` 只支持独享锁**。 -- **`ReentrantReadWriteLock` 其写锁是独享锁,其读锁是共享锁**。读锁是共享锁使得并发读是非常高效的,读写,写读 ,写写的过程是互斥的。 - -**悲观锁与乐观锁** - -- 悲观锁在 Java 中的应用就是通过使用 `synchronized` 和 `Lock` 显示加锁来进行互斥同步,这是一种阻塞同步。 - -- 乐观锁在 Java 中的应用就是采用 CAS 机制(CAS 操作通过 `Unsafe` 类提供,但这个类不直接暴露为 API,所以都是间接使用,如各种原子类)。 - -**偏向锁、轻量级锁、重量级锁** - -Java 1.6 以前,重量级锁一般指的是 `synchronized` ,而轻量级锁指的是 `volatile`。 - -Java 1.6 以后,针对 `synchronized` 做了大量优化,引入 4 种锁状态: 无锁状态、偏向锁、轻量级锁和重量级锁。锁可以单向的从偏向锁升级到轻量级锁,再从轻量级锁升级到重量级锁 。 - -**分段锁** - -分段锁其实是一种锁的设计,并不是具体的一种锁。典型:JDK1.7 之前的 `ConcurrentHashMap` - -**显示锁和内置锁** - -- 内置锁:`synchronized` -- 显示锁:`ReentrantLock`、`ReentrantReadWriteLock` 等。 - -#### ⭐⭐ AQS - -> 什么是 AQS? -> -> AQS 的作用是什么? -> -> AQS 的原理? - -**作用** - -`AbstractQueuedSynchronizer`(简称 **AQS**)是**队列同步器**,顾名思义,其主要作用是处理同步。它是并发锁和很多同步工具类的实现基石(如 `ReentrantLock`、`ReentrantReadWriteLock`、`Semaphore` 等)。 - -**AQS 提供了对独享锁与共享锁的支持**。 - -**原理** - -(1)数据结构 - -- `state` - AQS 使用一个整型的 `volatile` 变量来 **维护同步状态**。 - - 这个整数状态的意义由子类来赋予,如`ReentrantLock` 中该状态值表示所有者线程已经重复获取该锁的次数,`Semaphore` 中该状态值表示剩余的许可数量。 -- `head` 和 `tail` - AQS **维护了一个 `Node` 类型(AQS 的内部类)的双链表来完成同步状态的管理**。这个双链表是一个双向的 FIFO 队列,通过 `head` 和 `tail` 指针进行访问。当 **有线程获取锁失败后,就被添加到队列末尾**。 - -(2)获取独占锁 - -AQS 中使用 `acquire(int arg)` 方法获取独占锁,其大致流程如下: - -1. 先尝试获取同步状态,如果获取同步状态成功,则结束方法,直接返回。 -2. 如果获取同步状态不成功,AQS 会不断尝试利用 CAS 操作将当前线程插入等待同步队列的队尾,直到成功为止。 -3. 接着,不断尝试为等待队列中的线程节点获取独占锁。 - -(3)释放独占锁 - -AQS 中使用 `release(int arg)` 方法释放独占锁,其大致流程如下: - -1. 先尝试获取解锁线程的同步状态,如果获取同步状态不成功,则结束方法,直接返回。 -2. 如果获取同步状态成功,AQS 会尝试唤醒当前线程节点的后继节点。 - -(4)获取共享锁 - -AQS 中使用 `acquireShared(int arg)` 方法获取共享锁。 - -`acquireShared` 方法和 `acquire` 方法的逻辑很相似,区别仅在于自旋的条件以及节点出队的操作有所不同。 - -成功获得共享锁的条件如下: - -- `tryAcquireShared(arg)` 返回值大于等于 0 (这意味着共享锁的 permit 还没有用完)。 -- 当前节点的前驱节点是头结点。 - -(5)释放共享锁 - -AQS 中使用 `releaseShared(int arg)` 方法释放共享锁。 - -`releaseShared` 首先会尝试释放同步状态,如果成功,则解锁一个或多个后继线程节点。释放共享锁和释放独享锁流程大体相似,区别在于: - -对于独享模式,如果需要 SIGNAL,释放仅相当于调用头节点的 `unparkSuccessor`。 - -#### ⭐⭐ ReentrantLock - -> 什么是 ReentrantLock? -> -> 什么是可重入锁? -> -> ReentrantLock 有什么用? -> -> ReentrantLock 原理? - -**作用** - -**`ReentrantLock` 提供了一组无条件的、可轮询的、定时的以及可中断的锁操作** - -`ReentrantLock` 的特性如下: - -- **`ReentrantLock` 提供了与 `synchronized` 相同的互斥性、内存可见性和可重入性**。 -- `ReentrantLock` 支持公平锁和非公平锁(默认)两种模式。 -- `ReentrantLock` 实现了 `Lock` 接口,支持了 `synchronized` 所不具备的**灵活性**。 - - `synchronized` 无法中断一个正在等待获取锁的线程 - - `synchronized` 无法在请求获取一个锁时无休止地等待 - -**原理** - -`ReentrantLock` 基于其内部类 `ReentrantLock.Sync` 实现,`Sync` 继承自 AQS。它有两个子类: - -- `ReentrantLock.FairSync` - 公平锁。 -- `ReentrantLock.NonfairSync` - 非公平锁。 - -本质上,就是基于 AQS 实现。 - -#### ⭐ ReentrantReadWriteLock - -> ReentrantReadWriteLock 是什么? -> -> ReentrantReadWriteLock 的作用? -> -> ReentrantReadWriteLock 的原理? - -**作用** - -`ReentrantReadWriteLock` 是一个**可重入的读写锁**。**`ReentrantReadWriteLock` 维护了一对读写锁,将读写锁分开,有利于提高并发效率**。 - -**原理** - -`ReentrantReadWriteLock` 本质上也是基于 AQS 实现。有三个核心字段: - -- `sync` - 内部类 `ReentrantReadWriteLock.Sync` 对象。与 `ReentrantLock` 类似,它有两个子类:`ReentrantReadWriteLock.FairSync` 和 `ReentrantReadWriteLock.NonfairSync` ,分别表示公平锁和非公平锁的实现。 -- `readerLock` - 内部类 `ReentrantReadWriteLock.ReadLock` 对象,这是一把读锁。 -- `writerLock` - 内部类 `ReentrantReadWriteLock.WriteLock` 对象,这是一把写锁。 - -#### ⭐ Condition - -> Condition 有什么用? -> -> 使用 Lock 的线程,彼此如何通信? - -**作用** - -可以理解为,什么样的锁配什么样的钥匙。 - -**内置锁(`synchronized`)配合内置条件队列(`wait`、`notify`、`notifyAll` ),显式锁(`Lock`)配合显式条件队列(`Condition` )**。 - -#### ⭐⭐ 死锁 - -> 如何避免死锁? - -- 避免一个线程同时获取多个锁 -- 避免一个线程在锁内同时占用多个资源,尽量保证每个锁只占用一个资源 -- 尝试使用定时锁 lock.tryLock(timeout),避免锁一直不能释放 -- 对于数据库锁,加锁和解锁必须在一个数据库连接中里,否则会出现解锁失败的情况。 - -### 原子变量类 - -> 👉 参考阅读:[Java 原子类](https://dunwu.github.io/waterdrop/pages/25f78a/) - -#### ⭐ 原子类简介 - -> 为什么要用原子类? -> -> 用过哪些原子类? - -**作用** - -常规的锁(`Lock`、`sychronized`)由于是阻塞式的,势必影响并发吞吐量。 - -`volatile` 号称轻量级的锁,但不能保证原子性。 - -为了兼顾原子性和锁的性能问题,所以引入了原子类。 - -**类型** - -原子变量类可以分为 4 组: - -- 基本类型 - - `AtomicBoolean` - 布尔类型原子类 - - `AtomicInteger` - 整型原子类 - - `AtomicLong` - 长整型原子类 -- 引用类型 - - `AtomicReference` - 引用类型原子类 - - `AtomicMarkableReference` - 带有标记位的引用类型原子类 - - `AtomicStampedReference` - 带有版本号的引用类型原子类 -- 数组类型 - - `AtomicIntegerArray` - 整形数组原子类 - - `AtomicLongArray` - 长整型数组原子类 - - `AtomicReferenceArray` - 引用类型数组原子类 -- 属性更新器类型 - - `AtomicIntegerFieldUpdater` - 整型字段的原子更新器。 - - `AtomicLongFieldUpdater` - 长整型字段的原子更新器。 - - `AtomicReferenceFieldUpdater` - 原子更新引用类型里的字段。 - -#### ⭐ 原子类的原理 - -1. 处理器实现原子操作:使用总线锁保证原子性,使用缓存锁保证原子性(修改内存地址,缓存一致性机制:阻止同时修改由 2 个以上的处理器缓存的内存区域数据) -2. JAVA 实现原子操作:循环使用 CAS (自旋 CAS)实现原子操作 - -### 并发工具类 - -> 👉 参考阅读:[Java 并发工具类](https://dunwu.github.io/waterdrop/pages/02d274/) - -#### ⭐ CountDownLatch - -> CountDownLatch 作用? -> -> CountDownLatch 原理? - -**作用** - -字面意思为 **递减计数锁**。用于控制一个或者多个线程等待多个线程。 - -`CountDownLatch` 维护一个计数器 count,表示需要等待的事件数量。`countDown` 方法递减计数器,表示有一个事件已经发生。调用 `await` 方法的线程会一直阻塞直到计数器为零,或者等待中的线程中断,或者等待超时。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/CountDownLatch.png) - -**原理** - -`CountDownLatch` 是基于 AQS(`AbstractQueuedSynchronizer`) 实现的。 - -#### ⭐ CyclicBarrier - -> CyclicBarrier 有什么用? -> -> CyclicBarrier 的原理是什么? -> -> CyclicBarrier 和 CountDownLatch 有什么区别? - -**作用** - -字面意思是 **循环栅栏**。**`CyclicBarrier` 可以让一组线程等待至某个状态(遵循字面意思,不妨称这个状态为栅栏)之后再全部同时执行**。之所以叫循环栅栏是因为:当所有等待线程都被释放以后,`CyclicBarrier` 可以被重用。 - -`CyclicBarrier` 维护一个计数器 count。每次执行 `await` 方法之后,count 加 1,直到计数器的值和设置的值相等,等待的所有线程才会继续执行。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/CyclicBarrier.png) - -**原理** - -`CyclicBarrier` 是基于 `ReentrantLock` 和 `Condition` 实现的。 - -**区别** - -`CyclicBarrier` 和 `CountDownLatch` 都可以用来让一组线程等待其它线程。与 `CyclicBarrier` 不同的是,`CountdownLatch` 不能重用。 - -#### ⭐ Semaphore - -> Semaphore 作用? - -**作用** - -字面意思为 **信号量**。`Semaphore` 用来控制同时访问某个特定资源的操作数量,或者同时执行某个指定操作的数量。 - -`Semaphore` 管理着一组虚拟的许可(permit),permit 的初始数量可通过构造方法来指定。每次执行 `acquire` 方法可以获取一个 permit,如果没有就等待;而 `release` 方法可以释放一个 permit。 - -- `Semaphore` 可以用于实现资源池,如数据库连接池。 -- `Semaphore` 可以用于将任何一种容器变成有界阻塞容器。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/Semaphore.png) - -### 线程池 - -> 👉 参考阅读:[Java 线程池](https://dunwu.github.io/waterdrop/pages/ad9680/) - -#### ⭐⭐ ThreadPoolExecutor - -> `ThreadPoolExecutor` 有哪些参数,各自有什么用? -> -> `ThreadPoolExecutor` 工作原理? - -**原理** - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javacore/concurrent/java-thread-pool_1.png) - -**参数** - -`java.uitl.concurrent.ThreadPoolExecutor` 类是 `Executor` 框架中最核心的一个类。 - -ThreadPoolExecutor 有四个构造方法,前三个都是基于第四个实现。第四个构造方法定义如下: - -```java - public ThreadPoolExecutor(int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler handler) { -``` - -参数说明: - -- `corePoolSize` - **核心线程数量**。当有新任务通过 `execute` 方法提交时 ,线程池会执行以下判断: - - 如果运行的线程数少于 `corePoolSize`,则创建新线程来处理任务,即使线程池中的其他线程是空闲的。 - - 如果线程池中的线程数量大于等于 `corePoolSize` 且小于 `maximumPoolSize`,则只有当 `workQueue` 满时才创建新的线程去处理任务; - - 如果设置的 `corePoolSize` 和 `maximumPoolSize` 相同,则创建的线程池的大小是固定的。这时如果有新任务提交,若 `workQueue` 未满,则将请求放入 `workQueue` 中,等待有空闲的线程去从 `workQueue` 中取任务并处理; - - 如果运行的线程数量大于等于 `maximumPoolSize`,这时如果 `workQueue` 已经满了,则使用 `handler` 所指定的策略来处理任务; - - 所以,任务提交时,判断的顺序为 `corePoolSize` => `workQueue` => `maximumPoolSize`。 -- `maximumPoolSize` - **最大线程数量**。 - - 如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。 - - 值得注意的是:如果使用了无界的任务队列这个参数就没什么效果。 -- `keepAliveTime`:**线程保持活动的时间**。 - - 当线程池中的线程数量大于 `corePoolSize` 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 `keepAliveTime`。 - - 所以,如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。 -- `unit` - **`keepAliveTime` 的时间单位**。有 7 种取值。可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。 -- `workQueue` - **等待执行的任务队列**。用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。 - - `ArrayBlockingQueue` - **有界阻塞队列**。 - - 此队列是**基于数组的先进先出队列(FIFO)**。 - - 此队列创建时必须指定大小。 - - `LinkedBlockingQueue` - **无界阻塞队列**。 - - 此队列是**基于链表的先进先出队列(FIFO)**。 - - 如果创建时没有指定此队列大小,则默认为 `Integer.MAX_VALUE`。 - - 吞吐量通常要高于 `ArrayBlockingQueue`。 - - 使用 `LinkedBlockingQueue` 意味着: `maximumPoolSize` 将不起作用,线程池能创建的最大线程数为 `corePoolSize`,因为任务等待队列是无界队列。 - - `Executors.newFixedThreadPool` 使用了这个队列。 - - `SynchronousQueue` - **不会保存提交的任务,而是将直接新建一个线程来执行新来的任务**。 - - 每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。 - - 吞吐量通常要高于 `LinkedBlockingQueue`。 - - `Executors.newCachedThreadPool` 使用了这个队列。 - - `PriorityBlockingQueue` - **具有优先级的无界阻塞队列**。 -- `threadFactory` - **线程工厂**。可以通过线程工厂给每个创建出来的线程设置更有意义的名字。 -- `handler` - **饱和策略**。它是 `RejectedExecutionHandler` 类型的变量。当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。线程池支持以下策略: - - `AbortPolicy` - 丢弃任务并抛出异常。这也是默认策略。 - - `DiscardPolicy` - 丢弃任务,但不抛出异常。 - - `DiscardOldestPolicy` - 丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。 - - `CallerRunsPolicy` - 只用调用者所在的线程来运行任务。 - - 如果以上策略都不能满足需要,也可以通过实现 `RejectedExecutionHandler` 接口来定制处理策略。如记录日志或持久化不能处理的任务。 - -#### ⭐ Executors - -> Executors 提供了哪些内置的线程池? -> -> 这些线程池各自有什么特性?适合用于什么场景? - -Executors 为 Executor,ExecutorService,ScheduledExecutorService,ThreadFactory 和 `Callable` 类提供了一些工具方法。 - -(1)newSingleThreadExecutor - -**创建一个单线程的线程池**。 - -只会创建唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。 **如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它** 。 - -单工作线程最大的特点是:**可保证顺序地执行各个任务**。 - -(2)newFixedThreadPool - -**创建一个固定大小的线程池**。 - -**每次提交一个任务就会新创建一个工作线程,如果工作线程数量达到线程池最大线程数,则将提交的任务存入到阻塞队列中**。 - -`FixedThreadPool` 是一个典型且优秀的线程池,它具有线程池提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。 - -(3)newCachedThreadPool - -**创建一个可缓存的线程池**。 - -- 如果线程池大小超过处理任务所需要的线程数,就会回收部分空闲的线程; -- 如果长时间没有往线程池中提交任务,即如果工作线程空闲了指定的时间(默认为 1 分钟),则该工作线程将自动终止。终止后,如果你又提交了新的任务,则线程池重新创建一个工作线程。 -- 此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说 JVM)能够创建的最大线程大小。 因此,使用 `CachedThreadPool` 时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统瘫痪。 - -(4)newScheduleThreadPool - -创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。 - -## JVM - -### 内存管理 - -### OOM - -## 参考资料 - -- **书籍** - - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) - - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) -- **文章** - - [Java 线程面试题 Top 50](http://www.importnew.com/12773.html) - - [Java 多线程和并发基础面试问答](http://ifeve.com/java-multi-threading-concurrency-interview-questions-with-answers/) - - [进程和线程关系及区别](https://blog.csdn.net/yaosiming2011/article/details/44280797) - - [JavaThread Methods and Thread States](https://www.w3resource.com/java-tutorial/java-threadclass-methods-and-threadstates.php) - - [Java 线程的 5 种状态及切换(透彻讲解)](https://blog.csdn.net/pange1991/article/details/53860651) - - [Java 中守护线程的总结](https://blog.csdn.net/shimiso/article/details/8964414) - - [Java 创建线程的三种方式及其对比](https://blog.csdn.net/longshengguoji/article/details/41126119) - - [Java 线程的 5 种状态及切换(透彻讲解)](https://blog.csdn.net/pange1991/article/details/53860651) - - [Java 线程方法 join 的简单总结](https://www.cnblogs.com/lcplcpjava/p/6896904.html) - - [Java 并发编程:线程间协作的两种方式:wait、notify、notifyAll 和 Condition](http://www.cnblogs.com/dolphin0520/p/3920385.html) - - [Java 并发编程:volatile 关键字解析](http://www.cnblogs.com/dolphin0520/p/3920373.html) - - [Java 并发编程:Callable、Future 和 FutureTask](http://www.cnblogs.com/dolphin0520/p/3949310.html) - - [Java 并发编程:线程池的使用](http://www.cnblogs.com/dolphin0520/p/3932921.html) - - [Java 并发编程](https://www.jianshu.com/p/0256c2995cec) \ No newline at end of file diff --git a/source/_posts/01.Java/01.JavaSE/README.md b/source/_posts/01.Java/01.JavaSE/README.md deleted file mode 100644 index fb9048c57d..0000000000 --- a/source/_posts/01.Java/01.JavaSE/README.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: JavaSE -date: 2022-05-06 09:19:33 -categories: - - Java - - JavaSE -tags: - - Java - - JavaSE -permalink: /pages/69d2f8/ -hidden: true -index: false ---- - -# JavaSE - -## 📖 内容 - -> [Java 面试总结](99.Java面试.md) 💯 - -### [Java 基础特性](01.基础特性) - -- [Java 开发环境](01.基础特性/00.Java开发环境.md) -- [Java 基础语法特性](01.基础特性/01.Java基础语法.md) -- [Java 基本数据类型](01.基础特性/02.Java基本数据类型.md) -- [Java 面向对象](01.基础特性/03.Java面向对象.md) -- [Java 方法](01.基础特性/04.Java方法.md) -- [Java 数组](01.基础特性/05.Java数组.md) -- [Java 枚举](01.基础特性/06.Java枚举.md) -- [Java 控制语句](01.基础特性/07.Java控制语句.md) -- [Java 异常](01.基础特性/08.Java异常.md) -- [Java 泛型](01.基础特性/09.Java泛型.md) -- [Java 反射](01.基础特性/10.Java反射.md) -- [Java 注解](01.基础特性/11.Java注解.md) -- [Java String 类型](01.基础特性/42.JavaString类型.md) - -### [Java 高级特性](02.高级特性) - -- [Java 正则从入门到精通](02.高级特性/01.Java正则.md) - 关键词:`Pattern`、`Matcher`、`捕获与非捕获`、`反向引用`、`零宽断言`、`贪婪与懒惰`、`元字符`、`DFA`、`NFA` -- [Java 编码和加密](02.高级特性/02.Java编码和加密.md) - 关键词:`Base64`、`消息摘要`、`数字签名`、`对称加密`、`非对称加密`、`MD5`、`SHA`、`HMAC`、`AES`、`DES`、`DESede`、`RSA` -- [Java 国际化](02.高级特性/03.Java国际化.md) - 关键词:`Locale`、`ResourceBundle`、`NumberFormat`、`DateFormat`、`MessageFormat` -- [Java JDK8](02.高级特性/04.JDK8.md) - 关键词:`Stream`、`lambda`、`Optional`、`@FunctionalInterface` -- [Java SPI](02.高级特性/05.JavaSPI.md) - 关键词:`SPI`、`ClassLoader` - -### [Java 容器](03.容器) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221175550.png) - -- [Java 容器简介](03.容器/01.Java容器简介.md) - 关键词:`Collection`、`泛型`、`Iterable`、`Iterator`、`Comparable`、`Comparator`、`Cloneable`、`fail-fast` -- [Java 容器之 List](03.容器/02.Java容器之List.md) - 关键词:`List`、`ArrayList`、`LinkedList` -- [Java 容器之 Map](03.容器/03.Java容器之Map.md) - 关键词:`Map`、`HashMap`、`TreeMap`、`LinkedHashMap`、`WeakHashMap` -- [Java 容器之 Set](03.容器/04.Java容器之Set.md) - 关键词:`Set`、`HashSet`、`TreeSet`、`LinkedHashSet`、`EmumSet` -- [Java 容器之 Queue](03.容器/05.Java容器之Queue.md) - 关键词:`Queue`、`Deque`、`ArrayDeque`、`LinkedList`、`PriorityQueue` -- [Java 容器之 Stream](03.容器/06.Java容器之Stream.md) - -### [Java IO](04.IO) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630205329.png) - -- [Java IO 模型](04.IO/01.JavaIO模型.md) - 关键词:`InputStream`、`OutputStream`、`Reader`、`Writer`、`阻塞` -- [Java NIO](04.IO/02.JavaNIO.md) - 关键词:`Channel`、`Buffer`、`Selector`、`非阻塞`、`多路复用` -- [Java 序列化](04.IO/03.Java序列化.md) - 关键词:`Serializable`、`serialVersionUID`、`transient`、`Externalizable`、`writeObject`、`readObject` -- [Java 网络编程](04.IO/04.Java网络编程.md) - 关键词:`Socket`、`ServerSocket`、`DatagramPacket`、`DatagramSocket` -- [Java IO 工具类](04.IO/05.JavaIO工具类.md) - 关键词:`File`、`RandomAccessFile`、`System`、`Scanner` - -### [Java 并发](05.并发) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221175827.png) - -- [Java 并发简介](05.并发/01.Java并发简介.md) - 关键词:`进程`、`线程`、`安全性`、`活跃性`、`性能`、`死锁`、`饥饿`、`上下文切换` -- [Java 线程基础](05.并发/02.Java线程基础.md) - 关键词:`Thread`、`Runnable`、`Callable`、`Future`、`wait`、`notify`、`notifyAll`、`join`、`sleep`、`yeild`、`线程状态`、`线程通信` -- [Java 并发核心机制](05.并发/03.Java并发核心机制.md) - 关键词:`synchronized`、`volatile`、`CAS`、`ThreadLocal` -- [Java 并发锁](05.并发/04.Java锁.md) - 关键词:`AQS`、`ReentrantLock`、`ReentrantReadWriteLock`、`Condition` -- [Java 原子类](05.并发/05.Java原子类.md) - 关键词:`CAS`、`Atomic` -- [Java 并发容器](05.并发/06.Java并发和容器.md) - 关键词:`ConcurrentHashMap`、`CopyOnWriteArrayList` -- [Java 线程池](05.并发/07.Java线程池.md) - 关键词:`Executor`、`ExecutorService`、`ThreadPoolExecutor`、`Executors` -- [Java 并发工具类](05.并发/08.Java并发工具类.md) - 关键词:`CountDownLatch`、`CyclicBarrier`、`Semaphore` -- [Java 内存模型](05.并发/09.Java内存模型.md) - 关键词:`JMM`、`volatile`、`synchronized`、`final`、`Happens-Before`、`内存屏障` -- [ForkJoin 框架](05.并发/10.ForkJoin框架.md) - -### [Java 虚拟机](06.JVM) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200628154803.png) - -- [JVM 体系结构](06.JVM/01.JVM体系结构.md) -- [JVM 内存区域](06.JVM/02.JVM内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` -- [JVM 垃圾收集](06.JVM/03.JVM垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` -- [JVM 类加载](06.JVM/04.JVM类加载.md) - 关键词:`ClassLoader`、`双亲委派` -- [JVM 字节码](06.JVM/05.JVM字节码.md) - 关键词:`bytecode`、`asm`、`javassist` -- [JVM 命令行工具](06.JVM/11.JVM命令行工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo` -- [JVM GUI 工具](06.JVM/12.JVM_GUI工具.md) - 关键词:`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` -- [JVM 实战](06.JVM/21.JVM实战.md) - 关键词:`配置`、`调优` -- [Java 故障诊断](06.JVM/22.Java故障诊断.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` - -## 📚 资料 - -- **书籍** - - Java 四大名著 - - [《Java 编程思想(Thinking in java)》](https://book.douban.com/subject/2130190/) - - [《Java 核心技术 卷 I 基础知识》](https://book.douban.com/subject/26880667/) - - [《Java 核心技术 卷 II 高级特性》](https://book.douban.com/subject/27165931/) - - [《Effective Java》](https://book.douban.com/subject/30412517/) - - Java 并发 - - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) - - Java 虚拟机 - - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) - - Java 入门 - - [《O'Reilly:Head First Java》](https://book.douban.com/subject/2000732/) - - [《疯狂 Java 讲义》](https://book.douban.com/subject/3246499/) - - 其他 - - [《Head First 设计模式》](https://book.douban.com/subject/2243615/) - - [《Java 网络编程》](https://book.douban.com/subject/1438754/) - - [《Java 加密与解密的艺术》](https://book.douban.com/subject/25861566/) - - [《阿里巴巴 Java 开发手册》](https://book.douban.com/subject/27605355/) -- **教程、社区** - - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) - - [java-design-patterns](https://github.com/iluwatar/java-design-patterns) - - [Java](https://github.com/TheAlgorithms/Java) - - [《Java 核心技术面试精讲》](https://time.geekbang.org/column/intro/82) - - [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) - - [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) - - [深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - - [《Java 并发编程实战》](https://time.geekbang.org/column/intro/100023901) -- **面试** - - [CS-Notes](https://github.com/CyC2018/CS-Notes) - - [JavaGuide](https://github.com/Snailclimb/JavaGuide) - - [advanced-java](https://github.com/doocs/advanced-java) - -## 🚪 传送 - -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" index 2133814eb3..c1f560bf9b 100644 --- "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" +++ "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/01.JavaWeb\344\271\213Servlet\346\214\207\345\215\227.md" @@ -10,7 +10,7 @@ tags: - Java - JavaWeb - Servlet -permalink: /pages/e98894/ +permalink: /pages/99049148/ --- # JavaWeb 之 Servlet 指南 diff --git "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" index 32b8ab7ce8..778a7d4a52 100644 --- "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" +++ "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/02.JavaWeb\344\271\213Jsp\346\214\207\345\215\227.md" @@ -10,7 +10,7 @@ tags: - Java - JavaWeb - JSP -permalink: /pages/8cc787/ +permalink: /pages/b4ab7a47/ --- # JavaWeb 之 Jsp 指南 diff --git "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" index aea602475f..7c8173855c 100644 --- "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" +++ "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/03.JavaWeb\344\271\213Filter\345\222\214Listener.md" @@ -11,7 +11,7 @@ tags: - JavaWeb - Filter - Listener -permalink: /pages/82df5f/ +permalink: /pages/e7b45edd/ --- # JavaWeb 之 Filter 和 Listener diff --git "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" index 3729f4a6b1..cc85d16e1c 100644 --- "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" +++ "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/04.JavaWeb\344\271\213Cookie\345\222\214Session.md" @@ -11,7 +11,7 @@ tags: - JavaWeb - Cookie - Session -permalink: /pages/c46bff/ +permalink: /pages/b535e4c0/ --- # JavaWeb 之 Cookie 和 Session diff --git "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" index 8e803b40d4..425e467325 100644 --- "a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" +++ "b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/99.JavaWeb\351\235\242\347\273\217.md" @@ -10,7 +10,7 @@ tags: - Java - JavaWeb - Servlet -permalink: /pages/e175ce/ +permalink: /pages/07fe1296/ --- # JavaWeb 面经 diff --git a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/README.md b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/README.md index 56a9b42a58..7394b184e5 100644 --- a/source/_posts/01.Java/02.JavaEE/01.JavaWeb/README.md +++ b/source/_posts/01.Java/02.JavaEE/01.JavaWeb/README.md @@ -7,7 +7,7 @@ categories: - JavaWeb tags: - JavaWeb -permalink: /pages/50f49f/ +permalink: /pages/b6e5e1cc/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" index 05822b5e40..1d2fe79808 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/01.Tomcat\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -12,7 +12,7 @@ tags: - JavaWeb - 服务器 - Tomcat -permalink: /pages/4a4c02/ +permalink: /pages/20889acf/ --- # Tomcat 快速入门 diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" index fbf9fe8e86..4508c4ed38 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/02.Tomcat\350\277\236\346\216\245\345\231\250.md" @@ -12,7 +12,7 @@ tags: - JavaWeb - 服务器 - Tomcat -permalink: /pages/13f070/ +permalink: /pages/b3690c15/ --- # Tomcat 连接器 diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" index 0f71d9ce68..1ec8f641a7 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/03.Tomcat\345\256\271\345\231\250.md" @@ -12,7 +12,7 @@ tags: - JavaWeb - 服务器 - Tomcat -permalink: /pages/d5076a/ +permalink: /pages/678f0b0e/ --- # Tomcat 容器 diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" index 3beb96e83a..47dc417207 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/04.Tomcat\344\274\230\345\214\226.md" @@ -12,7 +12,7 @@ tags: - JavaWeb - 服务器 - Tomcat -permalink: /pages/f9e1e6/ +permalink: /pages/a6eabb5c/ --- # Tomcat 优化 diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" index a841530fb4..2acebc1702 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/05.Tomcat\345\222\214Jetty.md" @@ -13,7 +13,7 @@ tags: - 服务器 - Tomcat - Jetty -permalink: /pages/f37326/ +permalink: /pages/9e448b84/ --- ## Tomcat 和 Jetty diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" index 6204b0c8d1..72d86b0875 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/01.Tomcat/README.md" @@ -11,7 +11,7 @@ tags: - JavaWeb - 服务器 - Tomcat -permalink: /pages/33e817/ +permalink: /pages/efbf274f/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" index db02b55cf8..1ed549f95a 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/02.Jetty.md" @@ -11,7 +11,7 @@ tags: - JavaWeb - 服务器 - Jetty -permalink: /pages/ec364e/ +permalink: /pages/63272e2b/ --- # Jetty 快速入门 diff --git "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" index 9e9a4473ed..b61315589e 100644 --- "a/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" +++ "b/source/_posts/01.Java/02.JavaEE/02.\346\234\215\345\212\241\345\231\250/README.md" @@ -9,7 +9,7 @@ tags: - Java - JavaWeb - 服务器 -permalink: /pages/e3f3f3/ +permalink: /pages/0ccb3c71/ hidden: true index: false --- diff --git a/source/_posts/01.Java/02.JavaEE/README.md b/source/_posts/01.Java/02.JavaEE/README.md index 846c3677fe..7e5cfe90e2 100644 --- a/source/_posts/01.Java/02.JavaEE/README.md +++ b/source/_posts/01.Java/02.JavaEE/README.md @@ -7,7 +7,7 @@ categories: tags: - Java - JavaEE -permalink: /pages/80a822/ +permalink: /pages/7e4834de/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" index b2efbdda0e..09da21c8e6 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/01.Maven\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -11,7 +11,7 @@ tags: - Java - 构建 - Maven -permalink: /pages/e5b79f/ +permalink: /pages/8f9cb4f6/ --- # Maven 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" index d19300f4dd..bf51e51fcb 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/02.Maven\346\225\231\347\250\213\344\271\213pom.xml\350\257\246\350\247\243.md" @@ -11,7 +11,7 @@ tags: - Java - 构建 - Maven -permalink: /pages/d893c2/ +permalink: /pages/23a4efe7/ --- # Maven 教程之 pom.xml 详解 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" index 9fce2931ba..de8d8388d9 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/03.Maven\346\225\231\347\250\213\344\271\213settings.xml\350\257\246\350\247\243.md" @@ -11,7 +11,7 @@ tags: - Java - 构建 - Maven -permalink: /pages/1d58f1/ +permalink: /pages/9919f8ec/ --- # Maven 教程之 settings.xml 详解 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" index af20c6e46e..6ff9b43bc2 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/04.Maven\345\256\236\346\210\230\351\227\256\351\242\230\345\222\214\346\234\200\344\275\263\345\256\236\350\267\265.md" @@ -11,7 +11,7 @@ tags: - Java - 构建 - Maven -permalink: /pages/198618/ +permalink: /pages/3dae1bb1/ --- # Maven 实战问题和最佳实践 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" index 2f29b940c2..842c526514 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/05.Maven\346\225\231\347\250\213\344\271\213\345\217\221\345\270\203jar\345\210\260\347\247\201\346\234\215\346\210\226\344\270\255\345\244\256\344\273\223\345\272\223.md" @@ -11,7 +11,7 @@ tags: - Java - 构建 - Maven -permalink: /pages/7bdaf9/ +permalink: /pages/167f6345/ --- # Maven 教程之发布 jar 到私服或中央仓库 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" index 561aa81a22..9aecacf40c 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/06.Maven\346\217\222\344\273\266\344\271\213\344\273\243\347\240\201\346\243\200\346\237\245.md" @@ -11,7 +11,7 @@ tags: - Java - 构建 - Maven -permalink: /pages/370f1d/ +permalink: /pages/3f571b78/ --- # Maven 插件之代码检查 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" index 179cb60411..6d218c99d4 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/01.Maven/README.md" @@ -10,7 +10,7 @@ tags: - Java - 构建 - Maven -permalink: /pages/85f27a/ +permalink: /pages/adb721ae/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" index d0b8d13979..186d1eaf1b 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/02.Ant.md" @@ -10,7 +10,7 @@ tags: - Java - 构建 - Ant -permalink: /pages/0bafae/ +permalink: /pages/8036d4dc/ --- # Ant 简易教程 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" index 08c9c99afc..cdf84ec394 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/01.\346\236\204\345\273\272/README.md" @@ -8,7 +8,7 @@ categories: tags: - Java - 构建 -permalink: /pages/d1859b/ +permalink: /pages/afbb2faf/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" index 85b4f254a7..3778159712 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/01.Intellij.md" @@ -10,7 +10,7 @@ tags: - Java - IDE - Intellij -permalink: /pages/ac5c6a/ +permalink: /pages/b9996d17/ --- # Intellij IDEA 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" index d8f3730d84..92593de510 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/02.Eclipse.md" @@ -9,7 +9,7 @@ categories: tags: - Java - IDE -permalink: /pages/2257c7/ +permalink: /pages/6d6d7990/ --- # Eclipse 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" index 909e80c55f..37841505dd 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/03.VsCode.md" @@ -9,7 +9,7 @@ categories: tags: - Java - IDE -permalink: /pages/0f7153/ +permalink: /pages/f9dd72c7/ --- # Vscode 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" index e487ace7d0..f98c95e673 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/02.IDE/README.md" @@ -8,7 +8,7 @@ categories: tags: - Java - IDE -permalink: /pages/8695a7/ +permalink: /pages/57a22368/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" index 7ccdb46566..75617d4903 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/01.\347\233\221\346\216\247\345\267\245\345\205\267\345\257\271\346\257\224.md" @@ -9,7 +9,7 @@ categories: tags: - Java - 监控 -permalink: /pages/16563a/ +permalink: /pages/60751347/ --- # 监控工具对比 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" index ee099d3686..7febea2ff7 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/02.CAT.md" @@ -10,7 +10,7 @@ tags: - Java - 监控 - CAT -permalink: /pages/821ca3/ +permalink: /pages/48726db7/ --- # CAT 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" index e665495de3..68651ca36f 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/03.Zipkin.md" @@ -10,7 +10,7 @@ tags: - Java - 监控 - Zipkin -permalink: /pages/0a8826/ +permalink: /pages/4ce6aef0/ --- # Zipkin 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" index e357343780..03f1443601 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/04.Skywalking.md" @@ -10,7 +10,7 @@ tags: - Java - 监控 - SkyWalking -permalink: /pages/df7dec/ +permalink: /pages/55de5c6d/ --- # SkyWalking 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" index 7e5726c964..1734180e7a 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/05.Arthas.md" @@ -10,7 +10,7 @@ tags: - Java - 诊断 - Arthas -permalink: /pages/c689d1/ +permalink: /pages/1d699188/ --- # Arthas 快速入门 diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" index a2caaea0a2..b5f5dc6bf1 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/03.\347\233\221\346\216\247\350\257\212\346\226\255/README.md" @@ -9,7 +9,7 @@ tags: - Java - 监控 - 诊断 -permalink: /pages/3d16d3/ +permalink: /pages/bc2f8c2a/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/11.\350\275\257\344\273\266/README.md" "b/source/_posts/01.Java/11.\350\275\257\344\273\266/README.md" index d44fbe38b5..8508e3e231 100644 --- "a/source/_posts/01.Java/11.\350\275\257\344\273\266/README.md" +++ "b/source/_posts/01.Java/11.\350\275\257\344\273\266/README.md" @@ -6,7 +6,7 @@ categories: - 软件 tags: - Java -permalink: /pages/2cb045/ +permalink: /pages/f8ce3054/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" index 2e3cfea855..6e446f7472 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/01.JSON\345\272\217\345\210\227\345\214\226.md" @@ -11,7 +11,7 @@ tags: - IO - 序列化 - JSON -permalink: /pages/4622a6/ +permalink: /pages/6ad4a503/ --- # Java 和 JSON 序列化 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" index 631a5fcbb5..a882a5d920 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/02.\344\272\214\350\277\233\345\210\266\345\272\217\345\210\227\345\214\226.md" @@ -11,7 +11,7 @@ tags: - IO - 序列化 - 二进制 -permalink: /pages/08d872/ +permalink: /pages/0bec8119/ --- # Java 二进制序列化 @@ -22,7 +22,7 @@ permalink: /pages/08d872/ 原因很简单,就是 Java 默认的序列化机制(`ObjectInputStream` 和 `ObjectOutputStream`)具有很多缺点。 -> 不了解 Java 默认的序列化机制,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) +> 不了解 Java 默认的序列化机制,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/76ab164b/) Java 自身的序列化方式具有以下缺点: diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" index 1cd5ba9503..25fed65084 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/01.IO/README.md" @@ -9,14 +9,14 @@ tags: - Java - IO - 序列化 -permalink: /pages/08b504/ +permalink: /pages/b4b6ddc9/ hidden: true index: false --- # Java 序列化工具 -Java 官方的序列化存在许多问题,因此,很多人更愿意使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。 如果想详细了解 Java 自身序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) +Java 官方的序列化存在许多问题,因此,很多人更愿意使用优秀的第三方序列化工具来替代 Java 自身的序列化机制。 如果想详细了解 Java 自身序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/76ab164b/) 序列化库技术选型: diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" index 39b1df3a5d..45e45ea9be 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/01.Lombok.md" @@ -10,7 +10,7 @@ tags: - Java - JavaBean - Lombok -permalink: /pages/eb1d46/ +permalink: /pages/74f4e649/ --- # Lombok 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" index e9a7c23031..4552a306b6 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/02.JavaBean/02.Dozer.md" @@ -10,7 +10,7 @@ tags: - Java - JavaBean - Dozer -permalink: /pages/45e21b/ +permalink: /pages/bdde0815/ --- # Dozer 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" index 8de94dbca1..bd2269afaf 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/01.Freemark.md" @@ -10,7 +10,7 @@ tags: - Java - 模板引擎 - Freemark -permalink: /pages/a60ccf/ +permalink: /pages/a3b0ff35/ --- # Freemark 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" index df2e8fc48f..35dd853ba5 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/02.Thymeleaf.md" @@ -10,7 +10,7 @@ tags: - Java - 模板引擎 - Thymeleaf -permalink: /pages/e7d2ad/ +permalink: /pages/6e1de5aa/ --- # Thymeleaf 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" index ccb6514f34..95c6b27396 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/03.Velocity.md" @@ -10,7 +10,7 @@ tags: - Java - 模板引擎 - Velocity -permalink: /pages/3ba0ff/ +permalink: /pages/4f9c9f68/ --- # Velocity 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" index ffd80ba350..8b525ebd61 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/03.\346\250\241\346\235\277\345\274\225\346\223\216/README.md" @@ -8,7 +8,7 @@ categories: tags: - Java - 模板引擎 -permalink: /pages/9d37fa/ +permalink: /pages/cad15f3b/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" index b1d02cb99f..5825f487b3 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/01.Junit.md" @@ -10,7 +10,7 @@ tags: - Java - 测试 - JUnit -permalink: /pages/b39f47/ +permalink: /pages/86c4aae6/ --- # JUnit5 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" index e33ffaa498..53835938f5 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/02.Mockito.md" @@ -10,7 +10,7 @@ tags: - Java - 测试 - Mockito -permalink: /pages/f2c6f5/ +permalink: /pages/4ab12dfe/ --- # Mockito 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" index 7378e09f5d..cdd0118265 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/03.Jmeter.md" @@ -10,7 +10,7 @@ tags: - Java - 测试 - JMeter -permalink: /pages/0e5ab1/ +permalink: /pages/6e001e03/ --- # JMeter 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" index e5707faf22..09276050f0 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/04.JMH.md" @@ -10,7 +10,7 @@ tags: - Java - 测试 - JUnit -permalink: /pages/9c6402/ +permalink: /pages/b3025684/ --- # JMH 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" index ab247b863e..0a00efefc6 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/04.\346\265\213\350\257\225/README.md" @@ -8,7 +8,7 @@ categories: tags: - Java - 测试 -permalink: /pages/2cecc3/ +permalink: /pages/cffa3952/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" index efdc23d614..c587c8bc6e 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.Java\346\227\245\345\277\227.md" @@ -9,7 +9,7 @@ categories: tags: - Java - 日志 -permalink: /pages/fcc1c4/ +permalink: /pages/02c18ea3/ --- # 细说 Java 主流日志工具库 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" index 192f3c4216..8d27602dcf 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/02.Java\345\267\245\345\205\267\345\214\205.md" @@ -9,7 +9,7 @@ categories: tags: - Java - 工具包 -permalink: /pages/27ad42/ +permalink: /pages/38604da2/ --- # 细说 Java 主流工具包 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" index 6200ebea4f..ca804e2859 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/03.Reflections.md" @@ -10,7 +10,7 @@ tags: - Java - 反射 - Reflections -permalink: /pages/ce6195/ +permalink: /pages/59298a81/ --- # Reflections 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" index dc623bbfc1..7fa0dd2437 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/04.JavaMail.md" @@ -9,7 +9,7 @@ categories: tags: - Java - 邮件 -permalink: /pages/cd38ec/ +permalink: /pages/2c1194a5/ --- # JavaMail 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" index 04f0cbd5a0..c50b1841eb 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/05.Jsoup.md" @@ -10,7 +10,7 @@ tags: - Java - Html - Jsoup -permalink: /pages/5dd78d/ +permalink: /pages/1f697c93/ --- # Jsoup 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" index 7115115416..1e241e8eed 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/06.Thumbnailator.md" @@ -10,7 +10,7 @@ tags: - Java - 图形处理 - Thumbnailator -permalink: /pages/adacc5/ +permalink: /pages/a550913b/ --- # Thumbnailator 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" index d5f691bb90..2a8f75d987 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/99.\345\205\266\344\273\226/07.Zxing.md" @@ -10,7 +10,7 @@ tags: - Java - 条形码 - ZXing -permalink: /pages/b563af/ +permalink: /pages/c0f3f9b8/ --- # ZXing 快速入门 diff --git "a/source/_posts/01.Java/12.\345\267\245\345\205\267/README.md" "b/source/_posts/01.Java/12.\345\267\245\345\205\267/README.md" index dbd1c924e8..ffedb8c5c3 100644 --- "a/source/_posts/01.Java/12.\345\267\245\345\205\267/README.md" +++ "b/source/_posts/01.Java/12.\345\267\245\345\205\267/README.md" @@ -7,7 +7,7 @@ categories: tags: - Java - 工具 -permalink: /pages/1123e1/ +permalink: /pages/c85e7820/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" index 3a202241c6..39ddcf523c 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/01.Spring\346\246\202\350\277\260.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/9d3091/ +permalink: /pages/6193c337/ --- # Spring Framework 综述 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" index 3a5733b4c0..8f672e39ce 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/21.SpringBoot\347\237\245\350\257\206\345\233\276\350\260\261.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/430f53/ +permalink: /pages/fecda6b3/ --- # SpringBoot 知识图谱 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" index a7ee34aeb5..b6bba15d14 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/22.SpringBoot\345\237\272\346\234\254\345\216\237\347\220\206.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/dbf521/ +permalink: /pages/9a2eccb1/ --- # SpringBoot 基本原理 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" index 461f16fb66..eb24ccc3eb 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/99.Spring\351\235\242\350\257\225.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - 面试 -permalink: /pages/db33b0/ +permalink: /pages/ec29aea4/ --- # Spring 面试 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" index cb55bf5800..4e7cc6c7bd 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/00.Spring\347\273\274\345\220\210/README.md" @@ -11,7 +11,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/9e0b67/ +permalink: /pages/32ef3b63/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" index 1df26d7b49..8d126a6843 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/01.SpringBean.md" @@ -13,7 +13,7 @@ tags: - Spring - Bean - BeanDefinition -permalink: /pages/68097d/ +permalink: /pages/90bed6fd/ --- # Spring Bean diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" index 505172fbe8..d716000832 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/02.SpringIoC.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - IOC -permalink: /pages/915530/ +permalink: /pages/7b524787/ --- # Spring IoC diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" index 0d1fa3385f..273dd4058b 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/03.Spring\344\276\235\350\265\226\346\237\245\346\211\276.md" @@ -13,7 +13,7 @@ tags: - Spring - IOC - 依赖查找 -permalink: /pages/9a6f6b/ +permalink: /pages/d605fb0b/ --- # Spring 依赖查找 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" index c44e438f49..3696edff05 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/04.Spring\344\276\235\350\265\226\346\263\250\345\205\245.md" @@ -13,7 +13,7 @@ tags: - Spring - IOC - 依赖注入 -permalink: /pages/f61a1c/ +permalink: /pages/a7ad7991/ --- # Spring 依赖注入 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" index b633b85336..b1abb764ab 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/05.SpringIoC\344\276\235\350\265\226\346\235\245\346\272\220.md" @@ -13,7 +13,7 @@ tags: - Spring - IOC - 依赖注入 -permalink: /pages/a5f257/ +permalink: /pages/dd41e5a4/ --- # Spring IoC 依赖来源 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" index c07dacace0..c7089832db 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/06.SpringBean\344\275\234\347\224\250\345\237\237.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - Bean -permalink: /pages/8289f5/ +permalink: /pages/28438bc8/ --- # Spring Bean 作用域 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" index 4a72cd8c23..189e437a54 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/07.SpringBean\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - Bean -permalink: /pages/4ab176/ +permalink: /pages/a8c1ed61/ --- # Spring Bean 生命周期 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" index 5415034345..ea675d7d7c 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/08.Spring\351\205\215\347\275\256\345\205\203\346\225\260\346\215\256.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - Bean -permalink: /pages/55f315/ +permalink: /pages/2f9554a9/ --- # Spring 配置元数据 @@ -138,8 +138,8 @@ Spring Bean 生命周期回调注解 | Spring 注解 | 场景说明 | 起始版本 | | -------------- | ------------------------------------------------------------- | -------- | -| @PostConstruct | 替换 XML 元素 或 InitializingBean | 2.5 | -| @PreDestroy | 替换 XML 元素 或 DisposableBean | 2.5 | +| @PostConstruct | 替换 XML 元素 `` 或 InitializingBean | 2.5 | +| @PreDestroy | 替换 XML 元素 `` 或 DisposableBean | 2.5 | Spring BeanDefinition 解析与注册 @@ -296,4 +296,4 @@ API 编程 ## 参考资料 - [Spring 官方文档之 Core Technologies](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans) -- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) \ No newline at end of file +- [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" index fea4b39844..4f30187f6f 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/09.Spring\345\272\224\347\224\250\344\270\212\344\270\213\346\226\207\347\224\237\345\221\275\345\221\250\346\234\237.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/ad472e/ +permalink: /pages/bc233194/ --- # Spring 应用上下文生命周期 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" index 1a89468ae4..3211f1a3df 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/10.SpringAop.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - AOP -permalink: /pages/53aedb/ +permalink: /pages/626b2d15/ --- # Spring AOP diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" index b285f66344..98dcf05fb6 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/20.Spring\350\265\204\346\272\220\347\256\241\347\220\206.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - Resource -permalink: /pages/a1549f/ +permalink: /pages/926576ff/ --- # Spring 资源管理 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" index d99a6fcc6d..6960e72773 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/21.Spring\346\240\241\351\252\214.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/fe6aad/ +permalink: /pages/b827ab36/ --- # Spring 校验 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" index 3c5fe26013..4d061171cb 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/22.Spring\346\225\260\346\215\256\347\273\221\345\256\232.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - 数据绑定 -permalink: /pages/267b4c/ +permalink: /pages/750e15b8/ --- # Spring 数据绑定 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" index 2da331a65f..7b2a8ff27f 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/23.Spring\347\261\273\345\236\213\350\275\254\346\215\242.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/6662dc/ +permalink: /pages/0c43a278/ --- # Spring 类型转换 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" index 1e93903878..32d26bc54a 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/24.SpringEL.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/1f743f/ +permalink: /pages/be2542d9/ --- # Spring EL 表达式 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" index 8dc880d04a..0c98801fa8 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/25.Spring\344\272\213\344\273\266.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/cca414/ +permalink: /pages/41814824/ --- # Spring 事件 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" index e07843fd72..c97cc7267c 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/26.Spring\345\233\275\351\231\205\345\214\226.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/b5b8ad/ +permalink: /pages/b2008783/ --- # Spring 国际化 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" index ffc2db656b..d9a38d690b 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/27.Spring\346\263\233\345\236\213\345\244\204\347\220\206.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/175cbd/ +permalink: /pages/6561cf1d/ --- # Spring 泛型处理 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" index e8ce595cfb..d158072a1a 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/28.Spring\346\263\250\350\247\243.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/b6556f/ +permalink: /pages/26e4a88e/ --- # Spring 注解 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" index e95e298af1..3dd9ad6e1c 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/29.SpringEnvironment\346\212\275\350\261\241.md" @@ -11,7 +11,7 @@ tags: - Java - 框架 - Spring -permalink: /pages/03d838/ +permalink: /pages/1e6fd6fc/ --- # Spring Environment 抽象 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" index da73823df5..4a6fc7a67b 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/31.SpringBoot\344\271\213\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/950e4d/ +permalink: /pages/64c58a4c/ --- # SpringBoot 之快速入门 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" index 8a6ab56255..55d00a0849 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/32.SpringBoot\344\271\213\345\261\236\346\200\247\345\212\240\350\275\275.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/0fb992/ +permalink: /pages/7f4217a7/ --- # SpringBoot 之属性加载详解 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" index 667df7385c..3e85ba8e11 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/33.SpringBoot\344\271\213Profile.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/cb598e/ +permalink: /pages/b9fb7b40/ --- # SpringBoot 之 Profile diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" index 544eb1c101..b5f82bf309 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/01.Spring\346\240\270\345\277\203/README.md" @@ -11,7 +11,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/5e7c20/ +permalink: /pages/686be900/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" index 5d01d70337..aa194507c1 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/01.Spring\344\271\213\346\225\260\346\215\256\346\272\220.md" @@ -14,7 +14,7 @@ tags: - SpringBoot - 数据库 - DataSource -permalink: /pages/1b774c/ +permalink: /pages/0d7fe974/ --- # Spring 之数据源 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" index e199039287..9b5f086f24 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/02.Spring\344\271\213JDBC.md" @@ -14,7 +14,7 @@ tags: - SpringBoot - JDBC - JdbcTemplate -permalink: /pages/cf19fd/ +permalink: /pages/75f33649/ --- # Spring 之 JDBC diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" index 5a9b29fec2..cd6dad000a 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/03.Spring\344\271\213\344\272\213\345\212\241.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - 事务 -permalink: /pages/128c54/ +permalink: /pages/79ba0350/ --- # Spring 之事务 @@ -1269,4 +1269,4 @@ public void createSubUserWithExceptionRight(UserEntity entity) { - [Spring 官网](https://spring.io/) - [Spring 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) - [Spring Boot 官方文档](https://docs.spring.io/spring-boot/docs/current/reference/html/data.html) -- [《Java 业务开发常见错误 100 例》](https://time.geekbang.org/column/intro/100047701) \ No newline at end of file +- [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" index 8d36278912..6907937235 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/04.Spring\344\271\213JPA.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - JPA -permalink: /pages/a03d7b/ +permalink: /pages/ffe362a4/ --- # Spring 之 JPA diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" index d20f39b648..45b38a8254 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/10.Spring\351\233\206\346\210\220Mybatis.md" @@ -15,7 +15,7 @@ tags: - MyBatis - PageHelper - Mapper -permalink: /pages/88219e/ +permalink: /pages/613f787a/ --- # Spring 集成 Mybatis diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" index d4120edf29..ee09702ecc 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/20.SpringData\347\273\274\345\220\210.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/191cdb/ +permalink: /pages/f40d72cf/ --- # Spring Data 综合 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" index c8bf04e3a8..4dbd5df97f 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/21.Spring\350\256\277\351\227\256Redis.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - Redis -permalink: /pages/65e4a2/ +permalink: /pages/6bb64355/ --- # Spring 访问 Redis diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" index 03ddf78c7b..c563a33649 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/22.Spring\350\256\277\351\227\256MongoDB.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - MongoDB -permalink: /pages/db2a41/ +permalink: /pages/762be9db/ --- # Spring 访问 MongoDB diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" index 695ae7ebba..4394f0bc77 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/23.Spring\350\256\277\351\227\256Elasticsearch.md" @@ -13,14 +13,14 @@ tags: - Spring - SpringBoot - Elasticsearch -permalink: /pages/fac14c/ +permalink: /pages/fedadf18/ --- # Spring 访问 Elasticsearch ## 简介 -[Elasticsearch](https://www.elastic.co/products/elasticsearch) 是一个开源的、分布式的搜索和分析引擎。 +[Elasticsearch](https://www.elastic.co/elasticsearch) 是一个开源的、分布式的搜索和分析引擎。 ### 通过 REST 客户端连接 Elasticsearch @@ -130,4 +130,4 @@ Spring 和 Elasticsearch 匹配版本: - [Elasticsearch: The Definitive Guide](https://www.elastic.co/guide/en/elasticsearch/guide/master/index.html) - ElasticSearch 官方学习资料 - [Spring Boot 官方文档之 boot-features-elasticsearch](https://docs.spring.io/spring-boot/docs/current/reference/htmlsingle/#boot-features-elasticsearch) - [Spring Data Elasticsearch Github](https://github.com/spring-projects/spring-data-elasticsearch) -- [Spring Data Elasticsearch 官方文档](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/) \ No newline at end of file +- [Spring Data Elasticsearch 官方文档](https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/) diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" index a079aa0219..949c2da7a3 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/02.Spring\346\225\260\346\215\256/README.md" @@ -12,7 +12,7 @@ tags: - Spring - SpringBoot - 数据库 -permalink: /pages/b912d1/ +permalink: /pages/63e37fe3/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" deleted file mode 100644 index 3e8d4b44e6..0000000000 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWebMvc.md" +++ /dev/null @@ -1,37 +0,0 @@ ---- -title: spring-mvc -date: 2017-11-08 16:53:27 -order: 01 -categories: - - Java - - 框架 - - Spring - - SpringWeb -tags: - - Java - - 框架 - - Spring - - Web -permalink: /pages/65351b/ ---- - -# SpringMVC 简介 - -## SpringMVC 工作流程描述 - -Spring MVC 的工作流程可以用一幅图来说明: - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) - -1. 向服务器发送 HTTP 请求,请求被前端控制器 `DispatcherServlet` 捕获。 -2. `DispatcherServlet` 根据 **`-servlet.xml`** 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 -3. `DispatcherServlet` 根据获得的`Handler`,选择一个合适的 `HandlerAdapter`。(附注:如果成功获得`HandlerAdapter`后,此时将开始执行拦截器的 preHandler(...)方法)。 -4. 提取`Request`中的模型数据,填充`Handler`入参,开始执行`Handler`(`Controller`)。 在填充`Handler`的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: - - HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 - - 数据转换:对请求消息进行数据转换。如`String`转换成`Integer`、`Double`等。 - - 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 - - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到`BindingResult`或`Error`中。 -5. Handler(Controller)执行完成后,向 `DispatcherServlet` 返回一个 `ModelAndView` 对象; -6. 根据返回的`ModelAndView`,选择一个适合的 `ViewResolver`(必须是已经注册到 Spring 容器中的`ViewResolver`)返回给`DispatcherServlet`。 -7. `ViewResolver` 结合`Model`和`View`,来渲染视图。 -8. 视图负责将渲染结果返回给客户端。 \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWeb\347\273\274\350\277\260.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWeb\347\273\274\350\277\260.md" new file mode 100644 index 0000000000..3d797f5988 --- /dev/null +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/01.SpringWeb\347\273\274\350\277\260.md" @@ -0,0 +1,120 @@ +--- +title: Spring Web 综述 +date: 2017-11-08 16:53:27 +order: 01 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web +permalink: /pages/83fee2f7/ +--- + +# Spring Web 综述 + +## 快速入门 + +下面,通过一个简单的示例来展示如何通过 Spring 创建一个 Hello World Web 服务。 + +(1)`pom.xml` 中引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-web + +``` + +(2)定义 Controller + +Spring 构建 RESTful 服务的方法,HTTP 请求由 `Controller` 处理。 这些组件由 `@RestController` 注解标识。 + +【示例】下面的示例定义了一个处理 `/greeting` 的 GET 请求 + +```java +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class GreetingController { + + @GetMapping("/greeting") + public String greeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name, + Model model) { + model.addAttribute("name", name); + return "greeting"; + } + +} +``` + +(3)创建启动类 + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HelloWorldApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWorldApplication.class); + } + +} +``` + +(4)启动服务:执行 `HelloWorldApplication.main` 方法启动 web 服务 + +(5)测试 + +打开浏览器,访问 http://localhost:8080/greeting,页面会显示如下内容: + +```json +Hello, World! +``` + +打开浏览器,访问 http://localhost:8080/greeting?name=dunwu,页面会显示如下内容: + +``` +Hello, dunwu! +``` + +## SpringMVC 工作流程 + +Spring MVC 的工作流程可以用一幅图来说明: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) + +1. 向服务器发送 HTTP 请求,请求被前端控制器 `DispatcherServlet` 捕获。 +2. `DispatcherServlet` 根据 **`-servlet.xml`** 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 +3. `DispatcherServlet` 根据获得的`Handler`,选择一个合适的 `HandlerAdapter`。(附注:如果成功获得`HandlerAdapter`后,此时将开始执行拦截器的 preHandler(...)方法)。 +4. 提取`Request`中的模型数据,填充`Handler`入参,开始执行`Handler`(`Controller`)。 在填充`Handler`的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: + - HttpMessageConverter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 + - 数据转换:对请求消息进行数据转换。如`String`转换成`Integer`、`Double`等。 + - 数据根式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 + - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到`BindingResult`或`Error`中。 +5. Handler(Controller)执行完成后,向 `DispatcherServlet` 返回一个 `ModelAndView` 对象; +6. 根据返回的`ModelAndView`,选择一个适合的 `ViewResolver`(必须是已经注册到 Spring 容器中的`ViewResolver`)返回给`DispatcherServlet`。 +7. `ViewResolver` 结合`Model`和`View`,来渲染视图。 +8. 视图负责将渲染结果返回给客户端。 + +## 参考资料 + +- **官方** + - [Spring 官网](https://spring.io/) + - [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) + - [Spring Github](https://github.com/spring-projects/spring-framework) +- **书籍** + - [《Spring In Action》](https://item.jd.com/12622829.html) +- **教程** + - [《小马哥讲 Spring 核心编程思想》](https://time.geekbang.org/course/intro/265) + - [geekbang-lessons](https://github.com/geektime-geekbang/geekbang-lessons) + - [跟我学 Spring3](http://jinnianshilongnian.iteye.com/blog/1482071) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/02.SpringWeb\345\272\224\347\224\250.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/02.SpringWeb\345\272\224\347\224\250.md" new file mode 100644 index 0000000000..870c0c5a61 --- /dev/null +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/02.SpringWeb\345\272\224\347\224\250.md" @@ -0,0 +1,943 @@ +--- +title: Spring Web 应用 +date: 2023-02-14 19:21:22 +order: 02 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web + - Controller +permalink: /pages/1e6e3d1d/ +--- + +# Spring Web 应用 + +Spring MVC 提供了一种基于注解的编程模型,`@Controller` 和 `@RestController` 组件使用注解来表达请求映射、请求输入、异常处理等。注解控制器具有灵活的方法签名,并且不必扩展基类或实现特定接口。以下示例显示了一个由注解定义的控制器: + +```java +@Controller +public class HelloController { + + @GetMapping("/hello") + public String handle(Model model) { + model.addAttribute("message", "Hello World!"); + return "index"; + } +} +``` + +在前面的示例中,该方法接受一个 `Model` 并以 `String` 形式返回一个视图名称,但还存在许多其他选项。 + +## 快速入门 + +下面,通过一个简单的示例来展示如何通过 Spring 创建一个 Hello World Web 服务。 + +(1)`pom.xml` 中引入依赖 + +```xml + + org.springframework.boot + spring-boot-starter-web + +``` + +(2)定义 Controller + +Spring 构建 RESTful 服务的方法,HTTP 请求由 `Controller` 处理。 这些组件由 `@RestController` 注解标识。 + +【示例】下面的示例定义了一个处理 `/greeting` 的 GET 请求 + +```java +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@Controller +public class GreetingController { + + @GetMapping("/greeting") + public String greeting(@RequestParam(name = "name", required = false, defaultValue = "World") String name, + Model model) { + model.addAttribute("name", name); + return "greeting"; + } + +} +``` + +(3)创建启动类 + +```java +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class HelloWorldApplication { + + public static void main(String[] args) { + SpringApplication.run(HelloWorldApplication.class); + } + +} +``` + +(4)启动服务:执行 `HelloWorldApplication.main` 方法启动 web 服务 + +(5)测试 + +打开浏览器,访问 http://localhost:8080/greeting,页面会显示如下内容: + +```json +Hello, World! +``` + +打开浏览器,访问 http://localhost:8080/greeting?name=dunwu,页面会显示如下内容: + +``` +Hello, dunwu! +``` + +## Spring Web 组件 + +### 组件扫描 + +可以使用 Servlet 的 `WebApplicationContext` 中的标准 Spring bean 定义来定义控制器。`@Controller` 构造型允许自动检测,与 Spring 对检测类路径中的 `@Component` 类并为它们自动注册 bean 定义的一般支持保持一致。它还充当带注解类的构造型,表明其作为 Web 组件的角色。 + +要启用此类 `@Controller` 的自动检测,可以将组件扫描添加到您的 Java 配置中,如以下示例所示: + +```java +@Configuration +@ComponentScan("org.example.web") +public class WebConfig { + + // ... +} +``` + +以下示例显示了与上述示例等效的 XML 配置: + +```xml + + + + + + + + +``` + +### AOP 代理 + +在某些情况下,可能需要在运行时使用 AOP 代理装饰控制器。一个例子是,如果选择直接在控制器上使用 `@Transactional` 注解。在这种情况下,特别是对于控制器,建议使用基于类的代理。直接在控制器上使用此类注解会自动出现这种情况。 + +如果控制器实现了一个接口,并且需要 AOP 代理,您可能需要显式配置基于类的代理。例如,对于 `@EnableTransactionManagement` ,可以更改为 `@EnableTransactionManagement(proxyTargetClass = true)`,对于 `` ,您可以更改为 ``。 + +### @Controller + +`@RestController` 是一个[组合注解](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-meta-annotations),它本身使用 `@Controller` 和 `@ResponseBody` 元注解进行标记,以指示控制器的每个方法继承了类型级别的 `@ResponseBody` 注解,因此直接写入响应主体,而不是使用 HTML 模板进行视图解析和渲染。 + +### @RequestMapping + +可以使用 `@RequestMapping` 注解将请求映射到控制器方法。它具有各种属性,可以通过 URL、HTTP 方法、请求参数、标头和媒体类型进行匹配。可以在类级别使用它来表达共享映射,或者在方法级别使用它来缩小到特定端点的映射。 + +`@RequestMapping` 的主要参数: + +- path / method 指定映射路径与方法 +- params / headers 限定映射范围 +- consumes / produces 限定请求与响应格式 + +Spring 还提供了以下 `@RequestMapping` 的变体: + +- `@GetMapping` +- `@PostMapping` +- `@PutMapping` +- `@DeleteMapping` +- `@PatchMapping` + +快捷方式是提供的[自定义注解](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping-composed),因为可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 `@RequestMapping`,默认情况下,它与所有 HTTP 方法匹配。在类级别仍然需要 `@RequestMapping` 来表达共享映射。 + +以下示例具有类型和方法级别的映射: + +```java +@RestController +@RequestMapping("/persons") +class PersonController { + + @GetMapping("/{id}") + public Person getPerson(@PathVariable Long id) { + // ... + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public void add(@RequestBody Person person) { + // ... + } +} +``` + +#### URI 模式 + +`@RequestMapping` 方法可以使用 URL 模式进行映射。有两种选择: + +- `PathPattern` - 与 URL 路径匹配的预解析模式也预解析为 `PathContainer`。该解决方案专为网络使用而设计,可有效处理编码和路径参数,并高效匹配。 +- `AntPathMatcher` - 根据字符串路径匹配字符串模式。这是在 Spring 配置中也使用的原始解决方案,用于在类路径、文件系统和其他位置选择资源。它的效率较低,并且字符串路径输入对于有效处理 URL 的编码和其他问题是一个挑战。 + +`PathPattern` 是 Web 应用程序的推荐解决方案,它是 Spring WebFlux 中的唯一选择。它从 5.3 版开始在 Spring MVC 中使用,从 6.0 版开始默认启用。请参阅 [MVC 配置](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-config-path-matching) 以自定义路径匹配选项。 + +`PathPattern` 支持与 `AntPathMatcher` 相同的模式语法。此外,它还支持捕获模式,例如 `{spring}`,用于匹配路径末尾的 0 个或多个路径段。`PathPattern` 还限制使用 `**` 来匹配多个路径段,这样它只允许出现在模式的末尾。这消除了在为给定请求选择最佳匹配模式时出现的许多歧义。有关完整模式语法,请参阅 [PathPattern](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/web/util/pattern/PathPattern.html) 和 [AntPathMatcher](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/util/AntPathMatcher.html)。 + +一些示例模式: + +- `"/resources/ima?e.png"` -匹配一个字符 +- `"/resources/*.png"` - 匹配零个或多个字符 +- `"/resources/**"` - 匹配多个字符 +- `"/projects/{project}/versions"` - 匹配路径段并将其捕获为变量 +- `"/projects/{project:[a-z]+}/versions"` - 使用正则表达式匹配并捕获变量 + +可以使用 `@PathVariable` 访问捕获的 URI 变量。例如: + +```java +@GetMapping("/owners/{ownerId}/pets/{petId}") +public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { + // ... +} +``` + +可以在类和方法级别声明 URI 变量,如以下示例所示: + +```java +@Controller +@RequestMapping("/owners/{ownerId}") +public class OwnerController { + + @GetMapping("/pets/{petId}") + public Pet findPet(@PathVariable Long ownerId, @PathVariable Long petId) { + // ... + } +} +``` + +URI 变量会自动转换为适当的类型,否则会引发 `TypeMismatchException`。默认支持简单类型(`int`、`long`、`Date` 等),可以注册对任何其他数据类型的支持。请参见[类型转换](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-typeconversion)和 [`DataBinder`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-initbinder)。 + +可以显式命名 URI 变量(例如,`@PathVariable("customId")`),但如果名称相同并且代码是使用 `-parameters` 编译器标志编译的,则可以省略该细节。 + +语法 `{varName:regex}` 使用正则表达式声明一个 URI 变量。例如,给定 URL `"/spring-web-3.0.5.jar"`,以下方法提取名称、版本和文件扩展名: + +```java +@GetMapping("/{name:[a-z-]+}-{version:\\d\\.\\d\\.\\d}{ext:\\.[a-z]+}") +public void handle(@PathVariable String name, @PathVariable String version, @PathVariable String ext) { + // ... +} +``` + +URI 路径模式还可以嵌入 `${…}` 占位符,这些占位符在启动时通过使用 `PropertySourcesPlaceholderConfigurer` 针对本地、系统、环境和其他属性源进行解析。例如,可以使用它来根据某些外部配置参数化基本 URL。 + +#### 模式比较 + +当多个模式匹配一个 URL 时,必须选择最佳匹配。这是通过以下方式之一完成的,具体取决于是否启用了已解析的 `PathPattern` 以供使用: + +- [`PathPattern.SPECIFICITY_COMPARATOR`](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/web/util/pattern/PathPattern.html#SPECIFICITY_COMPARATOR) +- [`AntPathMatcher.getPatternComparator(String path)`](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/util/AntPathMatcher.html#getPatternComparator-java.lang.String-) + +两者都有助于对模式进行排序,更具体的模式位于顶部。如果模式具有较少的 URI 变量(计为 1)、单通配符(计为 1)和双通配符(计为 2),则模式不太具体。如果得分相同,则选择较长的模式。给定相同的分数和长度,选择 URI 变量多于通配符的模式。 + +默认映射模式 (`/**`) 被排除在评分之外并始终排在最后。此外,前缀模式(例如 `/public/**`)被认为不如其他没有双通配符的模式具体。 + +#### 后缀匹配 + +从 5.3 开始,默认情况下 Spring MVC 不再执行 `.*` 后缀模式匹配,其中映射到 `person` 的控制器也隐式映射到 `/person.*`。因此,路径扩展不再用于解释请求的响应内容类型⟩——例如,`/person.pdf`、`/person.xml` 等。 + +当浏览器过去发送难以一致解释的 `Accept` 请求头时,以这种方式使用文件扩展名是必要的。现在,这不再是必需的,使用 `Accept` 请求头应该是首选。 + +随着时间的推移,文件扩展名的使用在很多方面都被证明是有问题的。当使用 URI 变量、路径参数和 URI 编码覆盖时,它可能会导致歧义。关于基于 URL 的授权和安全性的推理也变得更加困难。 + +要在 5.3 之前的版本中完全禁用路径扩展,请设置以下内容: + +- `useSuffixPatternMatching(false)` - 参考:[PathMatchConfigurer](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-config-path-matching) +- `favorPathExtension(false)` - 参考:[ContentNegotiationConfigurer](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-config-content-negotiation) + +除了通过 `Accept` 请求头之外,还有一种请求内容类型的方法仍然有用,例如在浏览器中键入 URL 时。路径扩展的一种安全替代方法是使用查询参数策略。如果您必须使用文件扩展名,请考虑通过 [ContentNegotiationConfigurer](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-config-content-negotiation) 的 `mediaTypes` 属性将它们限制为明确注册的扩展名列表。 + +#### 后缀匹配和 RFD + +反射文件下载 (RFD) 攻击与 XSS 类似,因为它依赖于响应中反映的请求输入(例如,查询参数和 URI 变量)。然而,RFD 攻击不是将 JavaScript 插入 HTML,而是依赖于浏览器切换来执行下载,并在稍后双击时将响应视为可执行脚本。 + +在 Spring MVC 中,`@ResponseBody` 和 `ResponseEntity` 方法存在风险,因为它们可以渲染不同的内容类型,客户端可以通过 URL 路径扩展请求这些内容类型。禁用后缀模式匹配并使用路径扩展进行内容协商可以降低风险,但不足以防止 RFD 攻击。 + +为了防止 RFD 攻击,在渲染响应主体之前,Spring MVC 添加了一个 `Content-Disposition:inline;filename=f.txt` 头以建议一个固定且安全的下载文件。仅当 URL 路径包含的文件扩展名既不安全也不明确注册用于内容协商时,才会执行此操作。但是,当 URL 直接输入浏览器时,它可能会产生副作用。 + +默认情况下,允许许多常见的路径扩展是安全的。具有自定义 `HttpMessageConverter` 实现的应用程序可以显式注册文件扩展名以进行内容协商,以避免为这些扩展名添加 `Content-Disposition` 头。请参阅 [内容类型](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-config-content-negotiation)。 + +关于 RFD 更多细节推荐参考 [CVE-2015-5211](https://pivotal.io/security/cve-2015-5211) + +#### 限定数据类型 + +您可以根据请求的 `Content-Type` 缩小请求映射,如以下示例所示: + +```java +@PostMapping(path = "/pets", consumes = "application/json") +public void addPet(@RequestBody Pet pet) { + // ... +} +``` + +`consumes` 属性还支持否定表达式 - 例如,`!textplain` 表示除 `textplain` 之外的任何内容类型。 + +您可以在类级别声明一个共享的 `consumes` 属性。然而,与大多数其他请求映射属性不同的是,当在类级别使用时,方法级别的 `consumes` 属性会覆盖而不是扩展类级别的声明。 + +#### Producible Media Types + +可以根据 `Accept` 请求头和控制器方法生成的内容类型列表来缩小请求映射,如以下示例所示: + +```java +@GetMapping(path = "/pets/{petId}", produces = "application/json") +@ResponseBody +public Pet getPet(@PathVariable String petId) { + // ... +} +``` + +媒体类型可以指定一个字符集。支持否定表达式——例如,`!textplain` 表示除 "text/plain" 之外的任何内容类型。 + +可以在类级别声明一个共享的 `produces` 属性。然而,与大多数其他请求映射属性不同,当在类级别使用时,方法级别的 `produces` 属性会覆盖而不是扩展类级别的声明。 + +#### 参数、请求头 + +可以根据请求参数条件缩小请求映射范围。可以测试是否存在请求参数 (`myParam`)、是否缺少请求参数 (`!myParam`) 或特定值 (`myParam=myValue`)。以下示例显示如何测试特定值: + +```java +@GetMapping(path = "/pets/{petId}", params = "myParam=myValue") +public void findPet(@PathVariable String petId) { + // ... +} +``` + +还可以使用相同的请求头条件,如以下示例所示: + +```java +@GetMapping(path = "/pets", headers = "myHeader=myValue") +public void findPet(@PathVariable String petId) { + // ... +} +``` + +#### HTTP HEAD, OPTIONS + +`@GetMapping`(和 `@RequestMapping(method=HttpMethod.GET)`)透明地支持 HTTP HEAD 以进行请求映射。控制器方法不需要改变。在 `jakarta.servlet.http.HttpServlet` 中应用的响应包装器确保将 `Content-Length` 头设置为写入的字节数(实际上没有写入响应)。 + +`@GetMapping`(和`@RequestMapping(method=HttpMethod.GET)`)被隐式映射并支持 HTTP HEAD。HTTP HEAD 请求的处理方式就好像它是 HTTP GET 一样,除了不写入正文,而是计算字节数并设置 `Content-Length` 头。 + +默认情况下,通过将 `Allow` 响应头设置为所有具有匹配 URL 模式的 `@RequestMapping` 方法中列出的 HTTP 方法列表来处理 HTTP OPTIONS。 + +对于没有 HTTP 方法声明的 `@RequestMapping` ,`Allow` 头设置为 `GET,HEAD,POST,PUT,PATCH,DELETE,OPTIONS`。控制器方法应始终声明支持的 HTTP 方法(例如,通过使用 HTTP 方法特定变体:`@GetMapping`、`@PostMapping` 等)。 + +You can explicitly map the `@RequestMapping` method to HTTP HEAD and HTTP OPTIONS, but that is not necessary in the common case. + +可以显式地将 `@RequestMapping` 方法映射到 HTTP HEAD 和 HTTP OPTIONS,但在常见情况下这不是必需的。 + +#### 自定义注解 + +Spring MVC 支持使用[组合注解](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-meta-annotations) 进行请求映射。这些注解本身是用 `@RequestMapping` 进行元注解的,并且组合起来重新声明 `@RequestMapping` 属性的一个子集(或全部),具有更明确的目的。 + +`@GetMapping`、`@PostMapping`、`@PutMapping`、`@DeleteMapping` 和 `@PatchMapping` 是组合注解的示例。提供它们是因为,可以说,大多数控制器方法应该映射到特定的 HTTP 方法,而不是使用 `@RequestMapping`,默认情况下,它与所有 HTTP 方法匹配。如果您需要组合注解的示例,请查看这些注解的声明方式。 + +Spring MVC 还支持具有自定义请求匹配逻辑的自定义请求映射属性。这是一个更高级的选项,需要继承 `RequestMappingHandlerMapping` 并覆盖 `getCustomMethodCondition` 方法,您可以在其中检查自定义属性并返回您自己的 `RequestCondition`。 + +#### 显示注册 + +您可以以编程方式注册处理程序方法,您可以将其用于动态注册或高级情况,例如不同 URL 下的同一处理程序的不同实例。以下示例注册了一个处理程序方法 + +```java +@Configuration +public class MyConfig { + + @Autowired + public void setHandlerMapping(RequestMappingHandlerMapping mapping, UserHandler handler) + throws NoSuchMethodException { + + RequestMappingInfo info = RequestMappingInfo + .paths("/user/{id}").methods(RequestMethod.GET).build(); + + Method method = UserHandler.class.getMethod("getUser", Long.class); + + mapping.registerMapping(info, handler, method); + } +} +``` + +1. 为控制器注入目标处理程序和处理程序映射。 + +2. 准备请求映射元数据。 + +3. 获取处理程序方法。 + +4. 添加注册。 + +## 处理方法 + +### 请求数据 + +- `@RequestParam` + +- `@RequestBody` + +- `@PathVariable` + +- `@RequestHeader` + +> 更多 Spring Web 方法参数可以参考: [Method Arguments](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-arguments) + +### 响应数据 + +- `@ResponseBody` +- `@ResponseStatus` +- ResponseEntity + +- HttpEntity + +> 更多 Spring Web 方法返回值可以参考:[Return Values](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-return-types) + +## @ModelAttribute + +可以使用 `@ModelAttribute` 注解: + +- 在 `@RequestMapping` 方法中的方法参数上,用于模型创建或访问对象,并通过 `WebDataBinder` 将其绑定到请求。 +- 作为 `@Controller` 或 `@ControllerAdvice` 类中的方法级注解,有助于在任何 `@RequestMapping` 方法调用之前初始化模型。 +- 在 `@RequestMapping` 方法上标记它的返回值是一个模型属性。 + +本节讨论 `@ModelAttribute` 方法——前面列表中的第二项。一个控制器可以有任意数量的 `@ModelAttribute` 方法。所有这些方法都在同一控制器中的 `@RequestMapping` 方法之前被调用。`@ModelAttribute` 方法也可以通过 `@ControllerAdvice` 在控制器之间共享。 + +`@ModelAttribute` 方法具有灵活的方法签名。它们支持许多与 `@RequestMapping` 方法相同的参数,除了 `@ModelAttribute` 本身或与请求主体相关的任何内容。 + +以下示例显示了 `@ModelAttribute` 方法: + +```java +@ModelAttribute +public void populateModel(@RequestParam String number, Model model) { + model.addAttribute(accountRepository.findAccount(number)); + // add more ... +} +``` + +以下示例仅添加一个属性: + +```java +@ModelAttribute +public Account addAccount(@RequestParam String number) { + return accountRepository.findAccount(number); +} +``` + +还可以将 `@ModelAttribute` 用作 `@RequestMapping` 方法上的方法级注解,在这种情况下,`@RequestMapping` 方法的返回值被解释为模型属性。这通常不是必需的,因为它是 HTML 控制器中的默认行为,除非返回值是一个 String 否则将被解释为视图名称。 `@ModelAttribute` 还可以自定义模型属性名称,如下例所示: + +```java +@GetMapping("/accounts/{id}") +@ModelAttribute("myAccount") +public Account handle() { + // ... + return account; +} +``` + +## @InitBinder + +`@Controller` 或 `@ControllerAdvice` 类可以用 `@InitBinder` 方法来初始化 `WebDataBinder` 的实例,而这些方法又可以: + +- 将请求参数(即表单或查询数据)绑定到模型对象。 +- 将基于字符串的请求值(例如请求参数、路径变量、标头、cookie 等)转换为控制器方法参数的目标类型。 +- 在渲染 HTML 表单时将模型对象值格式化为 `String` 值。 + +`@InitBinder` 方法可以注册指定控制器 `java.beans.PropertyEditor` 或 Spring `Converter` 和 `Formatter` 组件。此外,您可以使用 [MVC 配置](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-config-conversion) 在全局共享的 `FormattingConversionService` 中注册 `Converter` 和 `Formatter` 类型。 + +`@InitBinder` 方法支持许多与 `@RequestMapping` 方法相同的参数,除了 `@ModelAttribute`(命令对象)参数。通常,它们使用 `WebDataBinder` 参数(用于注册)和 `void` 返回值声明。下面展示了一个示例: + +```java +@Controller +public class FormController { + + @InitBinder + public void initBinder(WebDataBinder binder) { + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + dateFormat.setLenient(false); + binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false)); + } + + // ... +} +``` + +或者,当您通过共享的 `FormattingConversionService` 使用基于 `Formatter` 的设置时,您可以重复使用相同的方法并注册指定控制器的 `Formatter` 实现,如以下示例所示: + +```java +@Controller +public class FormController { + + @InitBinder + protected void initBinder(WebDataBinder binder) { + binder.addCustomFormatter(new DateFormatter("yyyy-MM-dd")); + } + + // ... +} +``` + +在 Web 应用程序的上下文中,*数据绑定*涉及将 HTTP 请求参数(即表单数据或查询参数)绑定到模型对象及其嵌套对象中的属性。 + +仅公开遵循 [JavaBeans 命名约定](https://www.oracle.com/java/technologies/javase/javabeans-spec.html) 的 `public` 属性用于数据绑定——例如,`firstName` 属性的 get/set 方法:`public String getFirstName()` 和 `public void setFirstName(String)`。 + +默认情况下,Spring 允许绑定到模型对象图中的所有公共属性。这意味着您需要仔细考虑模型具有哪些公共属性,因为客户端可以将任何公共属性路径作为目标,甚至是一些预计不会针对给定用例的公共属性路径。 + +例如,给定一个 HTTP 表单数据端点,恶意客户端可以为存在于模型对象图中但不属于浏览器中显示的 HTML 表单的属性提供值。这可能导致在模型对象及其任何嵌套对象上设置数据,这些数据预计不会更新。 + +荐的方法是使用一个*专用模型对象*,它只公开与表单提交相关的属性。例如,在用于更改用户电子邮件地址的表单上,模型对象应声明最少的一组属性,例如以下 `ChangeEmailForm`。 + +```java +public class ChangeEmailForm { + + private String oldEmailAddress; + private String newEmailAddress; + + public void setOldEmailAddress(String oldEmailAddress) { + this.oldEmailAddress = oldEmailAddress; + } + + public String getOldEmailAddress() { + return this.oldEmailAddress; + } + + public void setNewEmailAddress(String newEmailAddress) { + this.newEmailAddress = newEmailAddress; + } + + public String getNewEmailAddress() { + return this.newEmailAddress; + } + +} +``` + +如果您不能或不想为每个数据绑定用例使用*专用模型对象*,则必须限制允许用于数据绑定的属性。理想情况下,可以通过 `WebDataBinder` 上的 `setAllowedFields()` 方法注册*允许的字段模式* 来实现这一点。 + +例如,要在您的应用程序中注册允许的字段模式,您可以在 `@Controller` 或 `@ControllerAdvice` 组件中实现 `@InitBinder` 方法,如下所示: + +```java +@Controller +public class ChangeEmailController { + + @InitBinder + void initBinder(WebDataBinder binder) { + binder.setAllowedFields("oldEmailAddress", "newEmailAddress"); + } + + // @RequestMapping methods, etc. + +} +``` + +除了注册允许的模式外,还可以通过 `DataBinder`及其子类中的 `setDisallowedFields()` 方法注册 _允许的字段模式_。但是请注意,“允许列表”比“拒绝列表”更安全。因此,`setAllowedFields()` 应该优于 `setDisallowedFields()`。 + +请注意,匹配允许的字段模式是区分大小写的;然而,与不允许的字段模式匹配是不区分大小写的。此外,匹配不允许的模式的字段将不会被接受,即使它也恰好匹配允许列表中的模式。 + +## 表单处理 + +### 创建处理表单的 Controller + +`GreetingController` 通过返回视图的名称处理 `/greeting` 的 GET 请求,这意味着返回的内容是名为 `greeting.html` 的视图内容。 + +`greetingForm()` 方法是通过使用 `@GetMapping` 专门映射到 GET 请求的,而 `greetingSubmit()` 是通过 `@PostMapping` 映射到 POST 请求的。 + +```java +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +@Controller +public class GreetingController { + + @GetMapping("/greeting") + public String greetingForm(Model model) { + model.addAttribute("greeting", new Greeting()); + return "greeting"; + } + + @PostMapping("/greeting") + public String greetingSubmit(@ModelAttribute Greeting greeting, Model model) { + model.addAttribute("greeting", greeting); + return "result"; + } + +} +``` + +### 定义需要提交的表单实体 + +```java +import lombok.Data; + +@Data +public class Greeting { + + private long id; + + private String content; + +} +``` + +### 提交表单前端代码 + +提交实体的页面必须依赖某种视图技术,通过将视图名称转换为模板进行渲染,从而对HTML进行服务端渲染。在下面的例子中,使用了 Thymeleaf 模板引擎作为视图,它解析 `greeting.html` 的各种模板表达式以渲染表单。 + +```html + + + + Getting Started: Handling Form Submission + + + +

Form

+
+

Id:

+

Message:

+

+
+ + +``` + +## 文件上传 + +### 创建文件上传处理 Controller + +```java +import java.io.IOException; +import java.util.stream.Collectors; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.ui.Model; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.ResponseBody; +import org.springframework.web.multipart.MultipartFile; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import org.springframework.web.servlet.mvc.support.RedirectAttributes; + +import com.example.uploadingfiles.storage.StorageFileNotFoundException; +import com.example.uploadingfiles.storage.StorageService; + +@Controller +public class FileUploadController { + + private final StorageService storageService; + + @Autowired + public FileUploadController(StorageService storageService) { + this.storageService = storageService; + } + + @GetMapping("/") + public String listUploadedFiles(Model model) throws IOException { + + model.addAttribute("files", storageService.loadAll().map( + path -> MvcUriComponentsBuilder.fromMethodName(FileUploadController.class, + "serveFile", path.getFileName().toString()).build().toUri().toString()) + .collect(Collectors.toList())); + + return "uploadForm"; + } + + @GetMapping("/files/{filename:.+}") + @ResponseBody + public ResponseEntity serveFile(@PathVariable String filename) { + + Resource file = storageService.loadAsResource(filename); + return ResponseEntity.ok().header(HttpHeaders.CONTENT_DISPOSITION, + "attachment; filename=\"" + file.getFilename() + "\"").body(file); + } + + @PostMapping("/") + public String handleFileUpload(@RequestParam("file") MultipartFile file, + RedirectAttributes redirectAttributes) { + + storageService.store(file); + redirectAttributes.addFlashAttribute("message", + "You successfully uploaded " + file.getOriginalFilename() + "!"); + + return "redirect:/"; + } + + @ExceptionHandler(StorageFileNotFoundException.class) + public ResponseEntity handleStorageFileNotFound(StorageFileNotFoundException exc) { + return ResponseEntity.notFound().build(); + } + +} +``` + +`FileUploadController` 类使用 `@Controller` 注解,以便 Spring 可以扫描并注册它。 每个方法都标有 `@GetMapping` 或 `@PostMapping` ,将路径和 HTTP 操作映射到指定的控制器。 + +在这种情况下: + +- GET `/`:从 `StorageService` 中查找当前上传文件的列表,并将其加载到 Thymeleaf 模板中。 它使用 `MvcUriComponentsBuilder` 计算指向实际资源的链接。 + +- GET `/files/{filename}`:加载资源(如果存在)并使用 Content-Disposition 响应标头将其发送到浏览器进行下载。 + +- POST `/`:处理一个多部分的消息文件,并将其交给 `StorageService` 进行保存。 + +### 定义存储文件的 Service + +```java +import org.springframework.core.io.Resource; +import org.springframework.web.multipart.MultipartFile; + +import java.nio.file.Path; +import java.util.stream.Stream; + +public interface StorageService { + + void init(); + + void store(MultipartFile file); + + Stream loadAll(); + + Path load(String filename); + + Resource loadAsResource(String filename); + + void deleteAll(); + +} +``` + +一个加单的 `StorageService` 实现: + +```java + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; +import org.springframework.stereotype.Service; +import org.springframework.util.FileSystemUtils; +import org.springframework.util.StringUtils; +import org.springframework.web.multipart.MultipartFile; + +import java.io.IOException; +import java.io.InputStream; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.util.stream.Stream; + +@Service +public class FileSystemStorageServiceImpl implements StorageService { + + private final Path rootLocation; + + @Autowired + public FileSystemStorageServiceImpl(StorageProperties properties) { + this.rootLocation = Paths.get(properties.getLocation()); + } + + @Override + public void deleteAll() { + FileSystemUtils.deleteRecursively(rootLocation.toFile()); + } + + @Override + public void init() { + try { + Files.createDirectories(rootLocation); + } catch (IOException e) { + throw new StorageException("Could not initialize storage", e); + } + } + + @Override + public Path load(String filename) { + return rootLocation.resolve(filename); + } + + @Override + public Stream loadAll() { + try { + return Files.walk(this.rootLocation, 1).filter(path -> !path.equals(this.rootLocation)) + .map(this.rootLocation::relativize); + } catch (IOException e) { + throw new StorageException("Failed to read stored files", e); + } + } + + @Override + public Resource loadAsResource(String filename) { + try { + Path file = load(filename); + Resource resource = new UrlResource(file.toUri()); + if (resource.exists() || resource.isReadable()) { + return resource; + } else { + throw new StorageFileNotFoundException("Could not read file: " + filename); + } + } catch (MalformedURLException e) { + throw new StorageFileNotFoundException("Could not read file: " + filename, e); + } + } + + @Override + public void store(MultipartFile file) { + String filename = StringUtils.cleanPath(file.getOriginalFilename()); + try { + if (file.isEmpty()) { + throw new StorageException("Failed to store empty file " + filename); + } + if (filename.contains("..")) { + // This is a security check + throw new StorageException( + "Cannot store file with relative path outside current directory " + filename); + } + try (InputStream inputStream = file.getInputStream()) { + Files.copy(inputStream, this.rootLocation.resolve(filename), StandardCopyOption.REPLACE_EXISTING); + } + } catch (IOException e) { + throw new StorageException("Failed to store file " + filename, e); + } + } + +} +``` + +### 创建文件上传表单 + +```html + + + +
+

+

+ +
+
+ + + +
File to upload:
+
+
+ + + + + +``` + +### 文件上传限制 + +如果使用 Spring Boot,可以使用一些属性设置来调整其自动配置的 `MultipartConfigElement`。 + +将以下属性添加到现有属性设置中(在 `src/main/resources/application.properties` 中): + +```properties +spring.servlet.multipart.max-file-size=128KB +spring.servlet.multipart.max-request-size=128KB +``` + +- `spring.servlet.multipart.max-file-size` 设置为 128KB,表示总文件大小不能超过 128KB。 +- `spring.servlet.multipart.max-request-size` 设置为 128KB,这意味着 `multipart/form-data` 的总请求大小不能超过 128KB。 + +## 异常处理 + +### @ExceptionHandler + +`@Controller` 和 [@ControllerAdvice](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice) 类可以用 `@ExceptionHandler` 方法来处理来自控制器方法的异常,如以下示例所示: + +```java +@Controller +public class SimpleController { + + // ... + + @ExceptionHandler + public ResponseEntity handle(IOException ex) { + // ... + } +} +``` + +异常可能与正在传播的顶级异常(例如,抛出直接的 `IOException`)或包装器异常中的嵌套原因(例如,包装在 `IllegalStateException` 中的 `IOException`)相匹配。从 5.3 开始,这可以匹配任意原因级别,而以前只考虑直接原因。 + +对于匹配的异常类型,最好将目标异常声明为方法参数,如前面的示例所示。当多个异常方法匹配时,根异常匹配通常优先于原因异常匹配。更具体地说,`ExceptionDepthComparator` 用于根据抛出的异常类型的深度对异常进行排序。 + +或者,注解声明可以缩小要匹配的异常类型,如以下示例所示: + +```java +@ExceptionHandler({FileSystemException.class, RemoteException.class}) +public ResponseEntity handle(IOException ex) { + // ... +} +``` + +您甚至可以使用具有非常通用的参数签名的特定异常类型列表,如以下示例所示: + +```java +@ExceptionHandler({FileSystemException.class, RemoteException.class}) +public ResponseEntity handle(Exception ex) { + // ... +} +``` + +通常建议您在参数签名中尽可能具体,以减少根本和原因异常类型之间不匹配的可能性。考虑将一个多重匹配方法分解为单独的 `@ExceptionHandler` 方法,每个方法通过其签名匹配一个特定的异常类型。 + +在多 `@ControllerAdvice` 安排中,建议在具有相应顺序优先级的 `@ControllerAdvice` 上声明您的主要根异常映射。虽然根异常匹配优于原因,但这是在给定控制器或 `@ControllerAdvice` 类的方法中定义的。这意味着优先级较高的 `@ControllerAdvice` 上的原因匹配优于优先级较低的 `@ControllerAdvice` 上的任何匹配(例如,root)。 + +最后但同样重要的是, `@ExceptionHandler` 方法实现可以选择通过以原始形式重新抛出给定异常实例来退出处理。这在您只对根级匹配或无法静态确定的特定上下文中的匹配感兴趣的情况下很有用。重新抛出的异常通过剩余的解析链传播,就好像给定的 `@ExceptionHandler` 方法一开始就不会匹配一样。 + +Spring MVC 中对 `@ExceptionHandler` 方法的支持建立在 `DispatcherServlet` 级别 [HandlerExceptionResolver](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-exceptionhandlers) 机制上。 + +> 附录: +> +> [`@ExceptionHandler` 支持的参数](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler-args) +> +> [`@ExceptionHandler` 支持返回值](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-exceptionhandler-return-values) + + + +## 参考资料 + +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Framework 官方文档之 Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/03.DispatcherServlet.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/03.DispatcherServlet.md" new file mode 100644 index 0000000000..9ff22b6c2d --- /dev/null +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/03.DispatcherServlet.md" @@ -0,0 +1,747 @@ +--- +title: Spring MVC 之 DispatcherServlet +date: 2023-02-13 09:57:52 +order: 03 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web + - DispatcherServlet +permalink: /pages/1782720c/ +--- + +# Spring MVC 之 DispatcherServlet + +## 简介 + +`DispatcherServlet` 是 Spring MVC 框架的核心组件,负责将**客户端请求映射到相应的控制器,然后调用控制器处理请求并返回响应结果**。 + +### DispatcherServlet 工作原理 + +#### DispatcherServlet 工作流程 + +`DispatcherServlet` 的工作流程大致如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/spring/web/spring-dispatcher-servlet.png) + +1. **接收 Http 请求**:当客户端发送 HTTP 请求时,`DispatcherServlet` 接收该请求并将其传递给 Spring MVC 框架。 +2. **选择 `Handler`**:`DispatcherServlet` 会根据请求的 URL 找到对应的处理器映射器 `HandlerMapping`,该映射器会根据配置文件中的 URL 映射规则找到合适的处理器 `Handler`。 + - **绑定属性**:`DispatcherServlet` 会根据程序的 web 初始化策略关联各种 `Resolver`,如:`LocaleResolver`、`ThemeResolver` 等。 + - `DispatcherServlet` 根据 `-servlet.xml` 中的配置对请求的 URL 进行解析,得到请求资源标识符(URI)。然后根据该 URI,调用 `HandlerMapping` 获得该 `Handler` 配置的所有相关的对象(包括 `Handler` 对象以及 `Handler` 对象对应的拦截器),最后以`HandlerExecutionChain` 对象的形式返回。 + - 将请求映射到处理程序以及用于预处理和后处理的拦截器列表。映射基于一些标准,其细节因 `HandlerMapping` 实现而异。 + - 两个主要的 `HandlerMapping` 实现是 `RequestMappingHandlerMapping`(支持 `@RequestMapping` 注释方法)和 `SimpleUrlHandlerMapping`(维护 URI 路径模式到处理程序的显式注册)。 +3. **选择 `HandlerAdapter`**: `DispatcherServlet` 根据获得的 `Handler`,选择一个合适的 `HandlerAdapter`。 + - `HandlerAdapter` 帮助 `DispatcherServlet` 调用映射到请求的 `Handler`,而不管实际调用 `Handler` 的方式如何。例如,调用带注解的控制器需要解析注解。`HandlerAdapter` 的主要目的是保护 `DispatcherServlet` 免受此类细节的影响。 +4. **`Handler` 处理请求**:`DispatcherServlet` 提取 `Request` 中的模型数据,填充 `Handler` 入参,由 `HandlerAdapter` 负责调用 `Handler`(`Controller`)。 在填充 `Handler` 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作: + - `HttpMessageConverter`: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息。 + - 数据转换:对请求消息进行数据转换。如 `String` 转换成 `Integer`、`Double `等。 + - 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等。 + - 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 `BindingResult` 或 `Error` 中。 +5. **返回 `ModelAndView` 对象**:`Handler` 处理完请求后,会返回一个 `ModelAndView` 对象,其中包含了处理结果(`Model`)和视图(`View`)信息。 +6. **选择 `ViewResolver` 渲染 `ModelAndView`**:根据返回的 `ModelAndView`,选择一个适合的 `ViewResolver`,并将 `ModelAndView` 传递给 `ViewResolver` 进行渲染,最后将渲染后的结果返回给客户端。 + +#### DispatcherServlet 源码解读 + +前面介绍了 `DispatcherServlet` 的工作流程,下面通过核心源码解读,来加深对 `DispatcherServlet` 工作原理的理解 + +(1)`onRefresh` 方法 + +```java + @Override + protected void onRefresh(ApplicationContext context) { + initStrategies(context); + } + + /** + * 初始化此 servlet 使用的策略对象 + * 可以在子类中重写以初始化更多策略对象 + */ + protected void initStrategies(ApplicationContext context) { + initMultipartResolver(context); + initLocaleResolver(context); + initThemeResolver(context); + initHandlerMappings(context); + initHandlerAdapters(context); + initHandlerExceptionResolvers(context); + initRequestToViewNameTranslator(context); + initViewResolvers(context); + initFlashMapManager(context); + } +``` + +(2)`doService` 方法 + +`DispatcherServlet` 的核心方法 `doService` 源码如下: + +```java + @Override + protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception { + logRequest(request); + + // 在包含的情况下保留请求属性的快照,以便能够在包含后恢复原始属性 + Map attributesSnapshot = null; + if (WebUtils.isIncludeRequest(request)) { + attributesSnapshot = new HashMap<>(); + Enumeration attrNames = request.getAttributeNames(); + while (attrNames.hasMoreElements()) { + String attrName = (String) attrNames.nextElement(); + if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) { + attributesSnapshot.put(attrName, request.getAttribute(attrName)); + } + } + } + + // 设置请求属性(绑定各种 Resolver),使框架对象可用于处理程序和视图对象 + request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext()); + request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver); + request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver); + request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource()); + + if (this.flashMapManager != null) { + FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response); + if (inputFlashMap != null) { + request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap)); + } + request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap()); + request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager); + } + + RequestPath previousRequestPath = null; + if (this.parseRequestPath) { + previousRequestPath = (RequestPath) request.getAttribute(ServletRequestPathUtils.PATH_ATTRIBUTE); + ServletRequestPathUtils.parseAndCache(request); + } + + try { + // 请求分发 + doDispatch(request, response); + } + finally { + if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) { + // 恢复原始属性快照,以防包含 + if (attributesSnapshot != null) { + restoreAttributesAfterInclude(request, attributesSnapshot); + } + } + if (this.parseRequestPath) { + ServletRequestPathUtils.setParsedRequestPath(previousRequestPath, request); + } + } + } +``` + +(3)`doDispatch` 方法 + +`doService` 中的核心方法是 `doDispatch`,负责分发请求。 + +```java +protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { + HttpServletRequest processedRequest = request; + HandlerExecutionChain mappedHandler = null; + boolean multipartRequestParsed = false; + + WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); + + try { + ModelAndView mv = null; + Exception dispatchException = null; + + try { + // 检查是否为multipart请求,如果是,则解析参数 + processedRequest = checkMultipart(request); + multipartRequestParsed = (processedRequest != request); + + // 确定适配当前请求的 Handler + mappedHandler = getHandler(processedRequest); + if (mappedHandler == null) { + noHandlerFound(processedRequest, response); + return; + } + + // 确定适配当前请求的 HandlerAdapter + HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); + + // 如果 Handler 支持,则处理 last-modified 头 + String method = request.getMethod(); + boolean isGet = HttpMethod.GET.matches(method); + if (isGet || HttpMethod.HEAD.matches(method)) { + long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); + if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { + return; + } + } + + // 请求的前置处理 + if (!mappedHandler.applyPreHandle(processedRequest, response)) { + return; + } + + // 调用实际的 Handler 处理请求并返回 ModelAndView(有可能为 null) + mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); + + if (asyncManager.isConcurrentHandlingStarted()) { + return; + } + + applyDefaultViewName(processedRequest, mv); + // 请求的后置处理 + mappedHandler.applyPostHandle(processedRequest, response, mv); + } + catch (Exception ex) { + dispatchException = ex; + } + catch (Throwable err) { + // As of 4.3, we're processing Errors thrown from handler methods as well, + // making them available for @ExceptionHandler methods and other scenarios. + dispatchException = new NestedServletException("Handler dispatch failed", err); + } + // 处理响应结果 + processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); + } + catch (Exception ex) { + triggerAfterCompletion(processedRequest, response, mappedHandler, ex); + } + catch (Throwable err) { + triggerAfterCompletion(processedRequest, response, mappedHandler, + new NestedServletException("Handler processing failed", err)); + } + finally { + if (asyncManager.isConcurrentHandlingStarted()) { + // 替代 postHandle 和 afterCompletion + if (mappedHandler != null) { + mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); + } + } + else { + // 清理 multipart 请求所使用的资源 + if (multipartRequestParsed) { + cleanupMultipart(processedRequest); + } + } + } +} +``` + +#### 上下文层次结构 + +`DispatcherServlet` 需要一个 `WebApplicationContext`(`ApplicationContext` 的扩展类)用于它自己的配置。`WebApplicationContext` 有一个指向 `ServletContext` 和与之关联的 `Servlet` 的链接。它还绑定到 `ServletContext`,以便应用程序可以在 `RequestContextUtils` 上使用静态方法来查找 `WebApplicationContext`。 + +对于多数应用程序来说,拥有一个 `WebApplicationContext` 单例就足够。也可以有一个上下文层次结构,其中有一个根 `WebApplicationContext` 在多个 `DispatcherServlet`(或其他 `Servlet`)实例之间共享,每个实例都有自己的子 `WebApplicationContext` 配置。 + +根 `WebApplicationContext` 通常包含基础结构 bean,例如需要跨多个 Servlet 实例共享的数据存储和业务服务。这些 bean 是有效继承的,并且可以在特定 `Servlet` 的子 `WebApplicationContext` 中被覆盖(即重新声明),它通常包含指定 `Servlet` 的本地 bean。下图显示了这种关系: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20230213103223.png) + +【示例】配置 `WebApplicationContext` 层次结构: + +```java +public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return new Class[] { RootConfig.class }; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] { App1Config.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/app1/*" }; + } +} +``` + +【示例】`web.xml` 方式配置 `WebApplicationContext` 层次结构: + +```xml + + + + org.springframework.web.context.ContextLoaderListener + + + + contextConfigLocation + /WEB-INF/root-context.xml + + + + app1 + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + /WEB-INF/app1-context.xml + + 1 + + + + app1 + /app1/* + + + +``` + +## 配置 + +`DispatcherServlet` 与其他 Servlet 一样,需要使用 Java 配置或在 `web.xml` 中根据 Servlet 规范进行声明和映射。也就是说,`DispatcherServlet` 使用 Spring 配置来发现请求映射、视图解析、异常处理等所需的委托组件。 + +可以通过将 Servlet 初始化参数(`init-param` 元素)添加到 `web.xml` 文件中的 Servlet 声明来自定义各个 `DispatcherServlet` 实例。下表列出了支持的参数: + +| 参数 | 说明 | +| :------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `contextClass` | 实现 `ConfigurableWebApplicationContext` 的类,将由此 Servlet 实例化和本地配置。默认情况下,使用 `XmlWebApplicationContext`。 | +| `contextConfigLocation` | 传递给上下文实例(由 `contextClass` 指定)以指示可以在何处找到上下文的字符串。该字符串可能包含多个字符串(使用逗号作为分隔符)以支持多个上下文。在具有两次定义的 bean 的多个上下文位置的情况下,最新的位置优先。 | +| `namespace` | `WebApplicationContext` 的命名空间。默认为 `[servlet-name]-servlet`。 | +| `throwExceptionIfNoHandlerFound` | 当找不到请求的处理程序时是否抛出 `NoHandlerFoundException`。然后可以使用 `HandlerExceptionResolver`(例如,通过使用 `@ExceptionHandler` 控制器方法)捕获异常并像其他任何方法一样处理。默认情况下,它设置为 `false`,在这种情况下,`DispatcherServlet` 设置响应状态为 404 (NOT_FOUND) 而不会引发异常。请注意,如果 [默认 servlet 处理](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc -default-servlet-handler) 也被配置,未解决的请求总是转发到默认的 servlet 并且永远不会引发 404。 | + +应用程序可以声明处理请求所需的特殊 Bean 类型中列出的基础结构 bean。`DispatcherServlet` 检查每个特殊 bean 的 `WebApplicationContext`。如果没有匹配的 bean 类型,它将回退到 `DispatcherServlet.properties` 中列出的默认类型。 + +在大多数情况下,MVC 配置是最好的起点。它以 Java 或 XML 声明所需的 bean,并提供更高级别的配置回调 API 来对其进行自定义。 + +> 注意:Spring Boot 依赖于 MVC Java 配置来配置 Spring MVC,并提供了许多额外的方便选项。 + +在 Servlet 环境中,您可以选择以编程方式配置 Servlet 容器作为替代方案或与 web.xml 文件结合使用。 + +```java +import org.springframework.web.WebApplicationInitializer; + +public class MyWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext container) { + XmlWebApplicationContext appContext = new XmlWebApplicationContext(); + appContext.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); + + ServletRegistration.Dynamic registration = container.addServlet("dispatcher", new DispatcherServlet(appContext)); + registration.setLoadOnStartup(1); + registration.addMapping("/"); + } +} +``` + +`WebApplicationInitializer` 是 Spring MVC 提供的接口,可确保检测到自定义的实现并自动用于初始化任何 Servlet 3 容器。名为 `AbstractDispatcherServletInitializer` 的 `WebApplicationInitializer` 的抽象基类实现通过覆盖方法来指定 servlet 映射和 `DispatcherServlet` 配置的位置,使得注册 `DispatcherServlet` 变得更加容易。 + +对于使用基于 Java 的 Spring 配置的应用程序,建议这样做,如以下示例所示: + +```java +public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected Class[] getRootConfigClasses() { + return null; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] { MyWebConfig.class }; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} +``` + +如果使用基于 XML 的 Spring 配置,则应直接从 AbstractDispatcherServletInitializer 扩展,如以下示例所示: + +```java +public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { + + @Override + protected WebApplicationContext createRootApplicationContext() { + return null; + } + + @Override + protected WebApplicationContext createServletApplicationContext() { + XmlWebApplicationContext cxt = new XmlWebApplicationContext(); + cxt.setConfigLocation("/WEB-INF/spring/dispatcher-config.xml"); + return cxt; + } + + @Override + protected String[] getServletMappings() { + return new String[] { "/" }; + } +} +``` + +`AbstractDispatcherServletInitializer` 还提供了一种方便的方法来添加 Filter 实例并将它们自动映射到 `DispatcherServlet`,如以下示例所示: + +```java +public class MyWebAppInitializer extends AbstractDispatcherServletInitializer { + + // ... + + @Override + protected Filter[] getServletFilters() { + return new Filter[] { + new HiddenHttpMethodFilter(), new CharacterEncodingFilter() }; + } +} +``` + +每个过滤器都根据其具体类型添加一个默认名称,并自动映射到 `DispatcherServlet`。 + +`AbstractDispatcherServletInitializer` 的 `isAsyncSupported` 保护方法提供了一个单独的位置来启用 `DispatcherServlet` 和映射到它的所有过滤器的异步支持。默认情况下,此标志设置为 true。 + +最后,如果需要进一步自定义 `DispatcherServlet` 本身,可以重写 `createDispatcherServlet` 方法。 + +【示例】Java 方式注册并初始化 `DispatcherServlet`,它由 Servlet 容器自动检测(请参阅 Servlet Config): + +```java +public class MyWebApplicationInitializer implements WebApplicationInitializer { + + @Override + public void onStartup(ServletContext servletContext) { + + // Load Spring web application configuration + AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext(); + context.register(AppConfig.class); + + // Create and register the DispatcherServlet + DispatcherServlet servlet = new DispatcherServlet(context); + ServletRegistration.Dynamic registration = servletContext.addServlet("app", servlet); + registration.setLoadOnStartup(1); + registration.addMapping("/app/*"); + } +} +``` + +【示例】web.xml 方式注册并初始化 `DispatcherServlet` + +```xml + + + + org.springframework.web.context.ContextLoaderListener + + + + contextConfigLocation + /WEB-INF/app-context.xml + + + + app + org.springframework.web.servlet.DispatcherServlet + + contextConfigLocation + + + 1 + + + + app + /app/* + + + +``` + +## 路径匹配 + +Servlet API 将完整的请求路径公开为 `requestURI`,并将其进一步细分为 `contextPath`、`servletPath` 和 `pathInfo`,它们的值因 Servlet 的映射方式而异。从这些输入中,Spring MVC 需要确定用于映射处理程序的查找路径,如果适用,它应该排除 `contextPath` 和任何 `servletMapping` 前缀。 + +`servletPath` 和 `pathInfo` 已解码,这使得它们无法直接与完整的 `requestURI` 进行比较以派生 `lookupPath`,因此有必要对 `requestURI` 进行解码。然而,这引入了它自己的问题,因为路径可能包含编码的保留字符,例如 `"/"` 或 `";"` 这反过来又会在解码后改变路径的结构,这也可能导致安全问题。此外,Servlet 容器可能会在不同程度上规范化 `servletPath`,这使得进一步无法对 `requestURI` 执行 `startsWith` 比较。 + +这就是为什么最好避免依赖基于前缀的 `servletPath` 映射类型附带的 `servletPath`。如果 `DispatcherServlet` 被映射为带有 `"/"` 的默认 Servlet,或者没有带 `"/*"` 的前缀,并且 Servlet 容器是 4.0+,则 Spring MVC 能够检测 Servlet 映射类型,并避免使用 `servletPath` 和 `pathInfo`。在 3.1 Servlet 容器上,假设相同的 Servlet 映射类型,可以通过在 MVC 配置中通过路径匹配提供一个带有 `alwaysUseFullPath=true` 的 `UrlPathHelper` 来实现等效。 + +幸运的是,默认的 Servlet 映射 `"/"` 是一个不错的选择。但是,仍然存在一个问题,即需要对 `requestURI` 进行解码才能与控制器映射进行比较。这也是不可取的,因为可能会解码改变路径结构的保留字。如果不需要这样的字符,那么您可以拒绝它们(如 Spring Security HTTP 防火墙),或者您可以使用 `urlDecode=false` 配置 `UrlPathHelper`,但控制器映射需要与编码路径匹配,这可能并不总是有效。此外,有时 `DispatcherServlet` 需要与另一个 Servlet 共享 URL 空间,并且可能需要通过前缀进行映射。 + +在使用 `PathPatternParser` 和解析模式时解决了上述问题,作为使用 `AntPathMatcher` 进行字符串路径匹配的替代方法。`PathPatternParser` 从 5.3 版本开始就可以在 Spring MVC 中使用,并且从 6.0 版本开始默认启用。与需要解码查找路径或编码控制器映射的 `AntPathMatcher` 不同,解析的 `PathPattern` 与称为 `RequestPath` 的路径的解析表示匹配,一次一个路径段。这允许单独解码和清理路径段值,而没有改变路径结构的风险。解析的 `PathPattern` 也支持使用 `servletPath` 前缀映射,只要使用 Servlet 路径映射并且前缀保持简单,即它没有编码字符。 + +## 拦截器 + +所有 `HandlerMapping` 实现都支持处理拦截器,当想要将特定功能应用于某些请求时,这些拦截器很有用——例如,检查主体。拦截器必须使用 `org.springframework.web.servlet` 包中的三个方法实现 `HandlerInterceptor`,这三个方法应该提供足够的灵活性来进行各种预处理和后处理: + +- `preHandle(..)`:在实际 handler 之前执行 +- `postHandle(..)`:handler 之后执行 +- `afterCompletion(..)`:完成请求后执行 + +`preHandle(..)` 方法返回一个布尔值。可以使用此方法中断或继续执行链的处理。当此方法返回 true 时,处理程序执行链将继续。当它返回 false 时,`DispatcherServlet` 假定拦截器本身已经处理请求(并且,例如,呈现适当的视图)并且不会继续执行其他拦截器和执行链中的实际处理程序。 + +有关如何配置拦截器的示例,请参阅 MVC 配置部分中的[拦截器](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-config-interceptors)。还可以通过在各个 `HandlerMapping` 实现上使用 setter 来直接注册它们。 + +`postHandle` 方法对于 `@ResponseBody` 和 `ResponseEntity` 的方法不太有用,它们的响应是在 `HandlerAdapter` 中和 `postHandle` 之前编写和提交的。这意味着对响应进行任何更改都为时已晚,例如添加额外的标头。对于此类场景,您可以实现 `ResponseBodyAdvice` 并将其声明为 [Controller Advice](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-controller-advice) bean 或直接在 `RequestMappingHandlerAdapter` 上进行配置。 + +## 解析器 + +DispatcherServlet 会加载多种解析器来处理请求,比较常见的有以下几个: + +- [`HandlerExceptionResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-exceptionhandlers) - 解决异常的策略,可能将它们映射到处理程序、HTML 错误视图或其他目标。 +- [`ViewResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-viewresolver) - 将从处理程序返回的基于字符串的逻辑视图名称解析为用于呈现响应的实际视图。 +- [`LocaleResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver), [LocaleContextResolver](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-timezone) - 解析用户正在使用的本地化设置,可能还有他们的时区,以便能够提供国际化的视图。 +- [`ThemeResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-themeresolver) - 解析 Web 应用程序可以使用的主题——例如,提供个性化布局。 +- [`MultipartResolver`](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-multipart) - 通过一些 multipart 解析库的帮助解析 multipart 请求(例如,通过浏览器上传文件)。 + +### HandlerExceptionResolver + +在 `WebApplicationContext` 中声明的 `HandlerExceptionResolver` 用于解决请求处理期间抛出的异常。这些异常解析器允许自定义逻辑来解决异常。 + +对于 HTTP 缓存支持,处理程序可以使用 `WebRequest` 的 `checkNotModified` 方法,以及用于控制器的 HTTP 缓存中所述的带注释控制器的更多选项。 + +如果在请求映射期间发生异常或从请求处理程序(例如 `@Controller`)抛出异常,则 `DispatcherServlet` 委托 `HandlerExceptionResolver` 链来解决异常并提供替代处理,这通常是错误响应。 + +下表列出了可用的 `HandlerExceptionResolver` 实现: + +| `HandlerExceptionResolver` | 说明 | +| :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------- | +| `SimpleMappingExceptionResolver` | 异常类名称和错误视图名称之间的映射。用于在浏览器应用程序中呈现错误页面。 | +| [`DefaultHandlerExceptionResolver`](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html) | 解决由 Spring MVC 引发的异常并将它们映射到 HTTP 状态代码。 | +| `ResponseStatusExceptionResolver` | 使用 `@ResponseStatus` 注解解决异常,并根据注解中的值将它们映射到 HTTP 状态代码。 | +| `ExceptionHandlerExceptionResolver` | 通过在 `@Controller` 或 `@ControllerAdvice` 类中调用 `@ExceptionHandler` 方法来解决异常。 | + +#### 解析器链 + +您可以通过在 Spring 配置中声明多个 `HandlerExceptionResolver` bean 并根据需要设置它们的顺序属性来构成异常解析器链。order 属性越高,异常解析器的位置就越靠后。 + +`HandlerExceptionResolver` 的约定使它可以返回以下内容: + +- 指向错误视图的 `ModelAndView`。 + +- 如果异常是在解析器中处理的,则为空的 `ModelAndView`。 + +- 如果异常仍未解决,则为 null,供后续解析器尝试,如果异常仍然存在,则允许向上冒泡到 Servlet 容器。 + +MVC Config 自动为默认的 Spring MVC 异常、`@ResponseStatus` 注释的异常和对 `@ExceptionHandler` 方法的支持声明内置解析器。您可以自定义该列表或替换它。 + +#### 错误页面 + +如果异常仍未被任何 `HandlerExceptionResolver` 处理并因此继续传播,或者如果响应状态设置为错误状态(即 4xx、5xx),Servlet 容器可以在 HTML 中呈现默认错误页面。要自定义容器的默认错误页面,您可以在 `web.xml` 中声明一个错误页面映射。以下示例显示了如何执行此操作: + +```xml + + /error + +``` + +在前面的示例中,当出现异常或响应具有错误状态时,Servlet 容器会在容器内将 ERROR 分派到配置的 URL(例如,`/error`)。然后由 `DispatcherServlet` 处理,可能将其映射到 `@Controller`,后者可以返回带有模型的错误视图名称或呈现 JSON 响应,如以下示例所示: + +```java +@RestController +public class ErrorController { + + @RequestMapping(path = "/error") + public Map handle(HttpServletRequest request) { + Map map = new HashMap(); + map.put("status", request.getAttribute("jakarta.servlet.error.status_code")); + map.put("reason", request.getAttribute("jakarta.servlet.error.message")); + return map; + } +} +``` + +> 提示:Servlet API 不提供在 Java 中创建错误页面映射的方法。但是,您可以同时使用 `WebApplicationInitializer` 和最小的 `web.xml`。 + +### ViewResolver + +Spring MVC 定义了 `ViewResolver` 和 `View` 接口,让用户可以在浏览器中渲染模型,而无需限定于特定的视图技术。`ViewResolver` 提供视图名称和实际视图之间的映射。`View` 解决了在移交给特定视图技术之前准备数据的问题。 + +下表提供了有关 ViewResolver 一些子类: + +| ViewResolver | Description | +| :------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `AbstractCachingViewResolver` | `AbstractCachingViewResolver` 的子类缓存它们解析的视图实例。缓存提高了某些视图技术的性能。您可以通过将 `cache` 属性设置为 `false` 来关闭缓存。此外,如果您必须在运行时刷新某个视图(例如,修改 FreeMarker 模板时),您可以使用 removeFromCache(String viewName, Locale loc) 方法。 | +| `UrlBasedViewResolver` | `ViewResolver` 接口的简单实现,无需显式映射定义即可将逻辑视图名称直接解析为 URL。如果您的逻辑名称以直接的方式匹配您的视图资源的名称,而不需要任意映射,那么这是合适的。 | +| `InternalResourceViewResolver` | `UrlBasedViewResolver` 的子类,支持 `InternalResourceView`(实际上是 Servlet 和 JSP)以及 `JstlView` 和 `TilesView` 等子类。可以使用 `setViewClass(..)` 为该解析器生成的所有视图指定视图类。 | +| `FreeMarkerViewResolver` | `UrlBasedViewResolver` 的子类,支持 `FreeMarkerView` 和它们的自定义子类。 | +| `ContentNegotiatingViewResolver` | `ViewResolver` 接口的实现,该接口根据请求文件名或 `Accept` 标头解析视图。 | +| `BeanNameViewResolver` | 将视图名称解释为当前应用程序上下文中的 bean 名称的 ViewResolver 接口的实现。这是一个非常灵活的变体,允许根据不同的视图名称混合和匹配不同的视图类型。每个这样的“视图”都可以定义为一个 bean,例如 在 XML 或配置类中。 | + +#### 处理 + +可以通过声明多个解析器来构成视图解析器链,如果需要,还可以通过设置 order 属性来指定顺序。顺序属性越高,视图解析器在链中的位置就越靠后。 + +`ViewResolver` 的约定指定它可以返回 null 以指示找不到视图。但是,对于 JSP 和 `InternalResourceViewResolver`,确定 JSP 是否存在的唯一方法是通过 `RequestDispatcher` 执行分派。因此,您必须始终将 `InternalResourceViewResolver` 配置为在视图解析器的整体顺序中排在最后。 + +配置视图解析就像将 `ViewResolver` 添加到 Spring 配置中一样简单。MVC Config 为视图解析器和添加无逻辑视图控制器提供了专用的配置 API,这对于没有控制器逻辑的 HTML 模板渲染很有用。 + +#### 重定向 + +视图名称中的特殊前缀 `redirect:` 可以实现一个重定向。`UrlBasedViewResolver`(及其子类)将此识别为需要重定向的指令。视图名称的其余部分是重定向 URL。 + +最终效果与控制器返回 `RedirectView` 相同,但现在控制器本身可以根据逻辑视图名称进行操作。逻辑视图名称(例如 `redirect:/myapp/some/resource`)相对于当前 Servlet 上下文重定向,而名称(例如 `redirect:https://myhost.com/some/arbitrary/path`)重定向到绝对 URL。 + +请注意,如果使用 `@ResponseStatus` 注解标记控制器方法,则注解值优先于 `RedirectView` 设置的响应状态。 + +#### 转发 + +视图名称中的特殊前缀 `forward:` 可以实现一个转发。这将创建一个 `InternalResourceView`,它执行 `RequestDispatcher.forward()`。因此,此前缀对 `InternalResourceViewResolver` 和 `InternalResourceView`(对于 JSP)没有用,但如果您使用另一种视图技术但仍想强制转发由 Servlet/JSP 引擎处理的资源,它可能会有所帮助。 + +#### 内容协商 + +`ContentNegotiatingViewResolver` 本身不解析视图,而是委托给其他视图解析器并选择类似于客户端请求的表示的视图。可以从 `Accept` 头或查询参数(例如,`"/path?format=pdf"`)确定表示形式。 + +`ContentNegotiatingViewResolver` 通过将请求媒体类型与其每个 `ViewResolver` 关联的 `View` 支持的媒体类型(也称为 `Content-Type`)进行比较,来选择合适的 `View` 来处理请求。列表中第一个具有兼容 `Content-Type` 的视图将处理结果返回给客户端。如果 `ViewResolver` 链无法提供兼容的视图,则会查阅通过 `DefaultViews` 属性指定的视图列表。后一个选项适用于单例视图,它可以呈现当前资源的适当表示,而不管逻辑视图名称如何。`Accept` 标头可以包含通配符(例如 `text/*`),在这种情况下,`Content-Type` 为 `text/xml` 的 View 是兼容的匹配项。 + +### LocaleResolver + +大部分的 Spring 架构都支持国际化,就像 Spring web MVC 框架所做的那样。`DispatcherServlet` 允许您使用客户端的语言环境自动解析消息。这是通过 `LocaleResolver` 对象完成的。 + +当收到请求时,`DispatcherServlet` 会寻找语言环境解析器,如果找到,它会尝试使用它来设置 Locale 环境。通过使用 `RequestContext.getLocale()` 方法,您始终可以检索由 Locale 解析器解析的语言环境。 + +除了自动识别 Locale 环境之外,您还可以为 handle 映射附加拦截器,在特定情况下更改 Locale 环境设置(例如,基于请求中的参数)。 + +Locale 解析器和拦截器在 `org.springframework.web.servlet.i18n` 包中定义,并以正常方式在您的应用程序上下文中配置。Spring 中有以下 Locale 解析器可供选择。 + +- [Time Zone](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-timezone) +- [Header Resolver](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-acceptheader) +- [Cookie Resolver](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-cookie) +- [Session Resolver](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-session) +- [Locale Interceptor](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-localeresolver-interceptor) + +#### LocaleResolver + +除了获取客户端的区域设置外,了解其时区通常也很有用。`LocaleContextResolver` 接口提供了 `LocaleResolver` 的扩展,让解析器提供更丰富的 `LocaleContext`,其中可能包括时区信息。 + +如果可用,可以使用 `RequestContext.getTimeZone()` 方法获取用户的 `TimeZone`。在 Spring 的 `ConversionService` 中注册的任何日期/时间 `Converter` 和 `Formatter` 对象会自动使用时区信息。 + +#### 标头解析器 + +此 Locale 解析器检查客户端(例如网络浏览器)发送的请求中的 `accept-language` 头。通常,此头字段包含客户端操作系统的区域信息。请注意,此解析器不支持时区信息。 + +#### CookieLocaleResolver + +This locale resolver inspects a `Cookie` that might exist on the client to see if a `Locale` or `TimeZone` is specified. If so, it uses the specified details. By using the properties of this locale resolver, you can specify the name of the cookie as well as the maximum age. The following example defines a `CookieLocaleResolver`: + +此 Locale 解析器检查客户端上是否存在 `Cookie`,以查看是否指定了 `Locale` 或 `TimeZone`。如果是,它会使用指定的详细信息。通过使用此 Locale 解析器的属性,可以指定 cookie 的名称以及最长期限。以下示例定义了 `CookieLocaleResolver`: + +```xml + + + + + + + + +``` + +下表描述了 `CookieLocaleResolver` 的属性: + +| 属性 | 默认值 | Description | +| :------------- | :------------------------ | :------------------------------------------------------------------------------------------------------ | +| `cookieName` | 类名 + LOCALE | cookie 名 | +| `cookieMaxAge` | Servlet container default | cookie 在客户端上保留的最长时间。如果指定了“-1”,则不会保留 cookie。它仅在客户端关闭浏览器之前可用。 | +| `cookiePath` | / | 将 cookie 的可见性限制在您网站的特定部分。当指定 `cookiePath` 时,cookie 仅对该路径及其下方的路径可见。 | + +#### SessionLocaleResolver + +`SessionLocaleResolver` 允许您从可能与用户请求相关联的会话中检索 `Locale` 和 `TimeZone`。与 `CookieLocaleResolver` 相比,此策略将本地选择的 locale 设置存储在 Servlet 容器的 `HttpSession` 中。因此,这些设置对于每个会话都是临时的,因此会在每个会话结束时丢失。 + +注意,这与外部会话管理机制(例如 Spring Session 项目)没有直接关系。此 `SessionLocaleResolver` 根据当前 `HttpServletRequest` 评估和修改相应的 `HttpSession` 属性。 + +#### LocaleChangeInterceptor + +可以通过将 `LocaleChangeInterceptor` 添加到一个 `HandlerMapping` 定义来启用区域设置更改。它检测请求中的参数并相应地更改 Locale 环境,在调度程序的应用程序上下文中调用 `LocaleResolver` 上的 `setLocale` 方法。下面的示例显示调用所有包含名为 `siteLanguage` 的参数的 `*.view` 资源,以更改语言环境。因此,例如,对 URL `https://www.sf.net/home.view?siteLanguage=nl` 的请求将站点语言更改为荷兰语。以下示例显示了如何拦截语言环境: + +```xml + + + + + + + + + + + + + + /**/*.view=someController + + +``` + +### ThemeResolver + +您可以应用 Spring Web MVC 框架主题来设置应用程序的整体外观,从而增强用户体验。主题是静态资源的集合,通常是样式表和图像,它们会影响应用程序的视觉风格。 + +要在 Web 应用程序中使用主题,必须设置 `org.springframework.ui.context.ThemeSource` 接口的实现。`WebApplicationContext` 接口扩展了 `ThemeSource` 但将其职责委托给了专门的实现。默认情况下,委托是 `org.springframework.ui.context.support.ResourceBundleThemeSource` ,它从类的根路径加载属性文件。要使用自定义的 `ThemeSource` 实现或配置 `ResourceBundleThemeSource` 的基本名称前缀,您可以在应用程序上下文中使用保留名称 `themeSource` 注册一个 bean。Web 应用程序上下文自动检测具有该名称的 bean 并使用它。 + +当使用 `ResourceBundleThemeSource` 时,主题是在一个简单的属性文件中定义的。属性文件列出了构成主题的资源,如以下示例所示: + +```properties +styleSheet=/themes/cool/style.css +background=/themes/cool/img/coolBg.jpg +``` + +属性的键是从视图代码中引用主题元素的名称。对于 JSP,通常使用 `spring:theme` 自定义标签来执行此操作,它与 `spring:message` 标签非常相似。以下 JSP 片段使用前面示例中定义的主题来自定义外观: + +```xml +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> + + + + + + ... + + +``` + +默认情况下, `ResourceBundleThemeSource` 使用空的基本名称前缀。因此,属性文件是从类路径的根加载的。因此,可以将 `cool.properties` 主题定义放在类路径根目录中(例如,在 `/WEB-INF/classes` 中)。`ResourceBundleThemeSource` 使用标准的 Java 资源包加载机制,允许主题完全国际化。例如,我们可以有一个 `/WEB-INF/classes/cool_nl.properties`,它引用一个带有荷兰语文本的特殊背景图像。 + +定义主题后,可以决定使用哪个要使用的主题。`DispatcherServlet` 查找名为 `themeResolver` 的 bean 以找出要使用的 `ThemeResolver` 实现。主题解析器的工作方式与 `LocaleResolver` 大致相同。它检测用于特定请求的主题,也可以更改请求的主题。下表描述了 Spring 提供的主题解析器: + +| Class | Description | +| :--------------------- | :------------------------------------------------------------------------------------ | +| `FixedThemeResolver` | 选择一个固定的主题,使用 `defaultThemeName` 属性设置。 | +| `SessionThemeResolver` | 主题在用户的 HTTP 会话中维护。 它只需要为每个会话设置一次,但不会在会话之间持续存在。 | +| `CookieThemeResolver` | 所选主题存储在客户端的 cookie 中。 | + +Spring 还提供了一个 `ThemeChangeInterceptor`,它允许使用一个简单的请求参数在每个请求上更改主题。 + +### MultipartResolver + +`org.springframework.web.multipart` 包中的 `MultipartResolver` 是一种解析 multipart 请求(包括文件上传)的策略。 有一个基于容器的 `StandardServletMultipartResolver` 实现,用于 Servlet 多部分请求解析。 请注意,从具有新 Servlet 5.0+ 基线的 Spring Framework 6.0 开始,基于 Apache Commons FileUpload 的过时的 `CommonsMultipartResolver` 不再可用。 + +要启用 multipart 处理,需要在 `DispatcherServlet` Spring 配置中声明一个名为 `multipartResolver` 的 `MultipartResolver`。 `DispatcherServlet` 检测到它并将其应用于传入请求。 当接收到内容类型为 `multipart/form-data` 的 POST 时,解析器解析将当前 `HttpServletRequest` 包装为 `MultipartHttpServletRequest` 的内容,以提供对已解析文件的访问以及将部分作为请求参数公开。 + +Servlet 多部分解析需要通过 Servlet 容器配置启用。 为此: + +- 在 Java 中,在 Servlet 注册上设置一个 `MultipartConfigElement`。 + +- 在 `web.xml` 中,将 `` 部分添加到 servlet 声明。 + +以下示例显示如何在 Servlet 注册上设置 `MultipartConfigElement`: + +```java +public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + // ... + + @Override + protected void customizeRegistration(ServletRegistration.Dynamic registration) { + + // Optionally also set maxFileSize, maxRequestSize, fileSizeThreshold + registration.setMultipartConfig(new MultipartConfigElement("/tmp")); + } + +} +``` + +一旦 Servlet multipart 配置好,就可以添加一个名为 `multipartResolver` 的 `StandardServletMultipartResolver` 类型的 bean。 + +## 参考资料 + +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Framework 官方文档之 Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/04.Spring\350\277\207\346\273\244\345\231\250.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/04.Spring\350\277\207\346\273\244\345\231\250.md" new file mode 100644 index 0000000000..4b67e50d11 --- /dev/null +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/04.Spring\350\277\207\346\273\244\345\231\250.md" @@ -0,0 +1,63 @@ +--- +title: Spring MVC 之过滤器 +date: 2023-02-14 17:44:09 +order: 04 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web + - Filter +permalink: /pages/57b73dc7/ +--- + +# Spring MVC 之过滤器 + +`spring-web` 模块提供了一些有用的 Filter: + +- [Form Data](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-http-put) +- [Forwarded Headers](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-forwarded-headers) +- [Shallow ETag](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-shallow-etag) +- [CORS](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#filters-cors) + +## 表单内容过滤器 + +浏览器只能通过 HTTP GET 或 HTTP POST 提交表单数据,但非浏览器客户端也可以使用 HTTP PUT、PATCH 和 DELETE。 Servlet API 需要 `ServletRequest.getParameter*()` 系列方法来支持仅对 HTTP POST 的表单字段访问。 + +`spring-web` 模块提供了 `FormContentFilter` 来拦截内容类型为 `applicationx-www-form-urlencoded` 的 HTTP PUT、PATCH、DELETE 请求,从请求体中读取表单数据,并包装 `ServletRequest` 通过 `ServletRequest.getParameter()` 系列方法使表单数据可用。 + +## 转发过滤器 + +当请求通过代理(如负载均衡器)时,主机、端口和方案可能会发生变化,这使得从客户端角度创建指向正确主机、端口和方案的链接成为一项挑战。 + +[RFC 7239](https://tools.ietf.org/html/rfc7239) 定义了 `Forwarded` HTTP 头,代理可以使用它来提供有关原始请求的信息。还有其他非标准头,包括 `X-Forwarded-Host`、`X-Forwarded-Port`、`X-Forwarded-Proto`、`X-Forwarded-Ssl` 和 `X-Forwarded-Prefix`。 + +`ForwardedHeaderFilter` 是一个 Servlet 过滤器,它修改请求以便 a) 根据 `Forwarded` 头更改主机、端口和 scheme;b) 删除这些头以消除进一步的影响。该过滤器依赖于包装请求,因此它必须排在其他过滤器之前,例如 `RequestContextFilter`,它应该与修改后的请求一起使用,而不是原始请求。 + +`Forwarded` 头有安全考量,因为应用程序无法知道头是由代理按预期添加的,还是由恶意客户端添加的。这就是为什么应将信任边界处的代理配置为删除来自外部的不受信任的 `Forwarded` 头。还可以使用 `removeOnly=true` 配置 `ForwardedHeaderFilter`,在这种情况下它会删除但不使用头。 + +为了支持[异步请求](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async)和错误分派,此过滤器应使用 `DispatcherType.ASYNC` 和 `DispatcherType.ERROR` 进行映射。如果使用 Spring Framework 的 `AbstractAnnotationConfigDispatcherServletInitializer`(参见 [Servlet Config](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-container-config)),所有过滤器都会自动为所有调度类型注册。但是,如果通过 `web.xml` 或在 Spring Boot 中通过 `FilterRegistrationBean` 注册过滤器,请确保除了 `DispatcherType.REQUEST` 之外还包括 `DispatcherType.ASYNC` 和 `DispatcherType.ERROR`。 + +## ETag 过滤器 + +`ShallowEtagHeaderFilter` 过滤器通过缓存写入响应的内容并从中计算 MD5 哈希来创建“浅”ETag。下次客户端发送时,它会做同样的事情,但它还会将计算值与 `If-None-Match` 请求标头进行比较,如果两者相等,则返回 304 (NOT_MODIFIED)。 + +此策略节省网络带宽但不节省 CPU,因为必须为每个请求计算完整响应。前面描述的控制器级别的其他策略可以避免计算。 + +此过滤器有一个 `writeWeakETag` 参数,该参数将过滤器配置为写入类似于以下内容的弱 ETag:`W"02a2d595e6ed9a0b24f027f2b63b134d6"`(如 [RFC 7232 Section 2.3](https://tools.ietf.org/html/rfc7232#section-2.3) 中所定义)。 + +为了支持[异步请求](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-async),这个过滤器必须用 `DispatcherType.ASYNC` 映射,这样过滤器才能延迟并成功生成一个 ETag 到最后最后一次异步调度。如果使用 Spring Framework 的 `AbstractAnnotationConfigDispatcherServletInitializer`,所有过滤器都会自动为所有调度类型注册。但是,如果通过 `web.xml` 或在 Spring Boot 中通过 `FilterRegistrationBean` 注册过滤器,请确保包含 `DispatcherType.ASYNC`。 + +## 跨域过滤器 + +Spring MVC 通过控制器上的注解为 CORS 配置提供细粒度支持。但是,当与 Spring Security 一起使用时,建议依赖内置的 `CorsFilter`,它必须在 Spring Security 的过滤器链之前订阅。 + +## 参考资料 + +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Framework 官方文档之 Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/05.Spring\350\267\250\345\237\237.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/05.Spring\350\267\250\345\237\237.md" new file mode 100644 index 0000000000..6921e5f5af --- /dev/null +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/05.Spring\350\267\250\345\237\237.md" @@ -0,0 +1,202 @@ +--- +title: Spring MVC 之跨域 +date: 2023-02-16 20:33:26 +order: 05 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web + - CORS +permalink: /pages/a7b9b8e7/ +--- + +# Spring MVC 之跨域 + +Spring MVC 支持跨域处理(CORS)。 + +## 简介 + +出于安全原因,浏览器禁止对当前源之外的资源进行 AJAX 调用。例如,可以在一个选项卡中使用您的银行帐户,而在另一个选项卡中使用 evil.com。来自 evil.com 的脚本不应该能够使用您的凭据向您的银行 API 发出 AJAX 请求——例如从您的账户中取款! + +跨域(CORS)是由 [大多数浏览器](https://caniuse.com/#feat=cors) 实施的 [W3C 规范](https://www.w3.org/TR/cors/),可让您指定哪种跨域请求是授权,而不是使用基于 IFRAME 或 JSONP 的不太安全和不太强大的解决方法。 + +## 处理 + +CORS 规范分为预检请求、简单请求和实际请求。要了解 CORS 的工作原理,可以阅读 [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) 等,或者查看规范了解更多详细信息。 + +Spring MVC `HandlerMapping` 实现提供了对 CORS 的内置支持。成功将请求映射到处理程序后,`HandlerMapping` 实现检查给定请求和处理程序的 CORS 配置并采取进一步的操作。预检请求被直接处理,而简单和实际的 CORS 请求被拦截、验证,并设置了所需的 CORS 响应标头。 + +为了启用跨源请求(即存在 `Origin` 标头并且与请求的主机不同),您需要有一些明确声明的 CORS 配置。如果未找到匹配的 CORS 配置,预检请求将被拒绝。没有 CORS 标头添加到简单和实际 CORS 请求的响应中,因此浏览器会拒绝它们。 + +每个 `HandlerMapping` 都可以[配置](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/web/servlet/handler/AbstractHandlerMapping.html#setCorsConfigurations- java.util.Map-) 单独使用基于 URL 模式的 `CorsConfiguration` 映射。 在大多数情况下,应用程序使用 MVC Java 配置或 XML 命名空间来声明此类映射,这会导致将单个全局映射传递给所有 `HandlerMapping` 实例。 + +可以将 `HandlerMapping` 级别的全局 CORS 配置与更细粒度的处理程序级别的 CORS 配置相结合。 例如,带注释的控制器可以使用类级或方法级的 `@CrossOrigin` 注释(其他处理程序可以实现 `CorsConfigurationSource`)。 + +The rules for combining global and local configuration are generally additive — for example, all global and all local origins. For those attributes where only a single value can be accepted, e.g. `allowCredentials` and `maxAge`, the local overrides the global value. + +结合全局和局部配置的规则通常是附加的⟩——例如,所有全局和所有局部起源。 对于那些只能接受单个值的属性,例如 `allowCredentials` 和 `maxAge`,局部覆盖全局值。 + +## `@CrossOrigin` + +[`@CrossOrigin`](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/web/bind/annotation/CrossOrigin.html) 注解在带注解的 Controller 方法上启用跨源请求,如以下示例所示: + +```java +@RestController +@RequestMapping("/account") +public class AccountController { + + @CrossOrigin + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` + +默认,`@CrossOrigin` 允许访问: + +- 所以 origin +- 所以 header +- 所以 Controller 方法映射到的 HTTP 方法 + +`allowCredentials` 默认情况下不启用,因为它建立了一个信任级别,可以公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且只应在适当的情况下使用。启用时,必须将 `allowOrigins` 设置为一个或多个特定域(但不是特殊值 `"*"`),或者 `allowOriginPatterns` 属性可用于匹配一组动态来源。 + +`maxAge` 单位为分钟 + +`@CrossOrigin` 也支持类级别,并且被所有方继承,如下所示: + +```java +@CrossOrigin(origins = "https://domain2.com", maxAge = 3600) +@RestController +@RequestMapping("/account") +public class AccountController { + + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` + +可以同时在类级别和方法级别上使用 `@CrossOrigin` + +```java +@CrossOrigin(maxAge = 3600) +@RestController +@RequestMapping("/account") +public class AccountController { + + @CrossOrigin("https://domain2.com") + @GetMapping("/{id}") + public Account retrieve(@PathVariable Long id) { + // ... + } + + @DeleteMapping("/{id}") + public void remove(@PathVariable Long id) { + // ... + } +} +``` + +## 全局配置 + +除了细粒度的控制器方法级别配置之外,您可能还想定义一些全局 CORS 配置。您可以在任何 `HandlerMapping` 上单独设置基于 URL 的 `CorsConfiguration` 映射。但是,大多数应用程序使用 MVC Java 配置或 MVC XML 命名空间来执行此操作。 + +默认情况下,全局配置启用以下功能: + +- 所以 origin +- 所以 header +- `GET`、`HEAD` 和 `POST` 方法 + +`allowCredentials` 默认情况下不启用,因为它建立了一个信任级别,可以公开敏感的用户特定信息(例如 cookie 和 CSRF 令牌),并且只应在适当的情况下使用。启用时,必须将 `allowOrigins` 设置为一个或多个特定域(但不是特殊值 `"*"`),或者 `allowOriginPatterns` 属性可用于匹配一组动态来源。 + +`maxAge` 单位为分钟 + +### Java 配置 + +要在 MVC Java 配置中启用 CORS,您可以使用 `CorsRegistry` 回调,如以下示例所示: + +```java +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + + registry.addMapping("/api/**") + .allowedOrigins("https://domain2.com") + .allowedMethods("PUT", "DELETE") + .allowedHeaders("header1", "header2", "header3") + .exposedHeaders("header1", "header2") + .allowCredentials(true).maxAge(3600); + + // Add more mappings... + } +} +``` + +### XML 配置 + +要在 XML 命名空间中启用 CORS,可以使用 `` 元素,如以下示例所示: + +```xml + + + + + + + +``` + +## CORS 过滤器 + +可以通过 Spring 内置的 [`CorsFilter`](https://docs.spring.io/spring-framework/docs/6.0.4/javadoc-api/org/springframework/web/filter/CorsFilter.html) 支持 CORS。 + +要配置过滤器,请将 `CorsConfigurationSource` 传递给它的构造函数,如以下示例所示: + +```java +CorsConfiguration config = new CorsConfiguration(); + +// Possibly... +// config.applyPermitDefaultValues() + +config.setAllowCredentials(true); +config.addAllowedOrigin("https://domain1.com"); +config.addAllowedHeader("*"); +config.addAllowedMethod("*"); + +UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); +source.registerCorsConfiguration("/**", config); + +CorsFilter filter = new CorsFilter(source); +``` + +## 参考资料 + +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Framework 官方文档之 Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/06.Spring\350\247\206\345\233\276.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/06.Spring\350\247\206\345\233\276.md" new file mode 100644 index 0000000000..21fc99eb16 --- /dev/null +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/06.Spring\350\247\206\345\233\276.md" @@ -0,0 +1,700 @@ +--- +title: Spring MVC 之视图技术 +date: 2023-02-17 11:21:25 +order: 06 +categories: + - Java + - 框架 + - Spring + - SpringWeb +tags: + - Java + - 框架 + - Spring + - Web + - View +permalink: /pages/264d3a22/ +--- + +# Spring MVC 之视图技术 + +Spring MVC 中视图技术的使用是可插拔的。无论决定使用 Thymeleaf、Groovy 等模板引擎、JSP 还是其他技术,都可以通过配置来更改。 + +Spring MVC 的视图位于该应用程序的内部信任边界内。 视图可以访问应用程序上下文的所有 bean。 因此,不建议在模板可由外部源编辑的应用程序中使用 Spring MVC 的模板支持,因为这可能会产生安全隐患。 + +## Thymeleaf + +[Thymeleaf](https://www.thymeleaf.org/) 是一个现代服务器端 Java 模板引擎,它强调自然的 HTML 模板,可以通过双击在浏览器中预览,而无需运行服务器,这对于 UI 模板的独立工作(例如,由设计师)非常有帮助。 + +Thymeleaf 与 Spring MVC 的集成由 Thymeleaf 项目管理。 配置涉及一些 bean 声明,例如 `ServletContextTemplateResolver`、`SpringTemplateEngine` 和 `ThymeleafViewResolver`。 有关详细信息,请参阅 [Thymeleaf+Spring](https://www.thymeleaf.org/documentation.html)。 + +## FreeMarker + +[Apache FreeMarker](https://freemarker.apache.org/) 是一个模板引擎,用于生成从 HTML 到电子邮件等任何类型的文本内容。 Spring 框架内置了 Spring MVC 与 FreeMarker 模板结合使用的集成。 + +### 视图配置 + +以下示例显示了如何将 FreeMarker 配置为视图技术: + +```java +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.freeMarker(); + } + + // Configure FreeMarker... + + @Bean + public FreeMarkerConfigurer freeMarkerConfigurer() { + FreeMarkerConfigurer configurer = new FreeMarkerConfigurer(); + configurer.setTemplateLoaderPath("/WEB-INF/freemarker"); + return configurer; + } +} +``` + +以下示例显示了如何在 XML 中配置相同的内容: + +```xml + + + + + + + + + + +``` + +或者,您也可以声明 `FreeMarkerConfigurer` 以完全控制所有属性,如以下示例所示: + +```xml + + + +``` + +您的模板需要存储在前面示例中所示的 `FreeMarkerConfigurer` 指定的目录中。鉴于前面的配置,如果您的控制器返回视图名称 `welcome`,解析器将查找 `/WEB-INF/freemarker/welcome.ftl` 模板。 + +### FreeMarker 配置 + +可以通过在 `FreeMarkerConfigurer` 上设置适当的 bean 属性,将 FreeMarker 'Settings' 和 'SharedVariables' 直接传递给 FreeMarker `Configuration` 对象(由 Spring 管理)。 `freemarkerSettings` 属性需要一个 `java.util.Properties` 对象,`freemarkerVariables` 属性需要一个 `java.util.Map`。 以下示例显示了如何使用 `FreeMarkerConfigurer`: + +```xml + + + + + + + + + + +``` + +有关应用于 `Configuration` 对象的设置和变量的详细信息,请参阅 FreeMarker 文档。 + +### 表单处理 + +Spring 提供了一个用于 JSP 的标记库,其中包含一个 `` 元素。 此元素主要让表单显示来自表单支持对象的值,并显示来自 Web 或业务层中的“验证器”的验证失败的结果。 Spring 还支持 FreeMarker 中的相同功能,以及用于生成表单输入元素的额外便利宏。 + +#### 绑定宏 + +在 FreeMarker 的 `spring-webmvc.jar` 文件中维护了一组标准宏,因此它们始终可用于适当配置的应用程序。 + +Spring 模板库中定义的一些宏被认为是内部的(私有的),但宏定义中不存在这样的范围,这使得所有宏对调用代码和用户模板都是可见的。以下部分仅关注您需要从模板中直接调用的宏。如果您想直接查看宏代码,该文件名为 `spring.ftl` ,位于 `org.springframework.web.servlet.view.freemarker` 包中。 + +#### 简单绑定 + +在基于充当 Spring MVC 控制器表单视图的 FreeMarker 模板的 HTML 表单中,您可以使用类似于下一个示例的代码来绑定到字段值,并以类似于 JSP 等价物的方式为每个输入字段显示错误消息。以下示例显示了一个 personForm 视图: + +```xml + +<#import "/spring.ftl" as spring/> + + ... +
+ Name: + <@spring.bind "personForm.name"/> +
+ <#list spring.status.errorMessages as error> ${error}
+
+ ... + +
+ ... + +``` + +`<@spring.bind>` 需要一个 'path' 参数,它由命令对象的名称(它是 'command',除非您在控制器配置中更改它)组成,在您希望绑定的命令对象后跟一个句点和字段名称。 您还可以使用嵌套字段,例如 `command.address.street`。 `bind` 宏采用 `web.xml` 中的 `ServletContext` 参数 `defaultHtmlEscape` 指定的默认 HTML 转义行为。 + +称为 `<@spring.bindEscaped>` 的宏的另一种形式采用第二个参数,该参数明确指定是否应在状态错误消息或值中使用 HTML 转义。 您可以根据需要将其设置为 `true` 或 `false` 。 附加的表单处理宏简化了 HTML 转义的使用,您应该尽可能使用这些宏。 + +#### 输入宏 + +FreeMarker 的附加便利宏简化了绑定和表单生成(包括验证错误显示)。 永远不需要使用这些宏来生成表单输入字段,您可以将它们与简单的 HTML 混合搭配,或者直接调用我们之前强调的 Spring 绑定宏。 + +下表中的可用宏显示了 FreeMarker 模板 (FTL) 定义和每个采用的参数列表: + +| macro | FTL definition | +| :------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | +| `message` (output a string from a resource bundle based on the code parameter) | <@spring.message code/> | +| `messageText` (output a string from a resource bundle based on the code parameter, falling back to the value of the default parameter) | <@spring.messageText code, text/> | +| `url` (prefix a relative URL with the application’s context root) | <@spring.url relativeUrl/> | +| `formInput` (standard input field for gathering user input) | <@spring.formInput path, attributes, fieldType/> | +| `formHiddenInput` (hidden input field for submitting non-user input) | <@spring.formHiddenInput path, attributes/> | +| `formPasswordInput` (standard input field for gathering passwords. Note that no value is ever populated in fields of this type.) | <@spring.formPasswordInput path, attributes/> | +| `formTextarea` (large text field for gathering long, freeform text input) | <@spring.formTextarea path, attributes/> | +| `formSingleSelect` (drop down box of options that let a single required value be selected) | <@spring.formSingleSelect path, options, attributes/> | +| `formMultiSelect` (a list box of options that let the user select 0 or more values) | <@spring.formMultiSelect path, options, attributes/> | +| `formRadioButtons` (a set of radio buttons that let a single selection be made from the available choices) | <@spring.formRadioButtons path, options separator, attributes/> | +| `formCheckboxes` (a set of checkboxes that let 0 or more values be selected) | <@spring.formCheckboxes path, options, separator, attributes/> | +| `formCheckbox` (a single checkbox) | <@spring.formCheckbox path, attributes/> | +| `showErrors` (simplify display of validation errors for the bound field) | <@spring.showErrors separator, classOrStyle/> | + +上述任何宏的参数具有一致的含义: + +- `path`: 要绑定到的字段的名称(例如,“command.name”) +- `options`: 可在输入字段中选择的所有可用值的 `Map`。映射的键表示从表单回传并绑定到命令对象的值。针对键存储的 map 对象是在表单上显示给用户的标签,可能与表单回传的相应值不同。通常,这样的地图由控制器提供作为参考数据。您可以使用任何 `Map` 实现,具体取决于所需的行为。对于严格排序的映射,您可以使用带有合适的“比较器”的 `SortedMap`(例如 `TreeMap`),对于应按插入顺序返回值的任意映射,使用“LinkedHashMap”或“LinkedMap” `公共收藏`。 +- `separator`: 在多个选项可用作离散元素(单选按钮或复选框)的情况下,用于分隔列表中每个选项的字符序列(例如 `
`)。 +- `attributes`: 要包含在 HTML 标记本身中的任意标记或文本的附加字符串。该字符串按字面意思由宏回显。例如,在 `textarea` 字段中,您可以提供属性(例如“rows="5" cols="60"'),或者您可以传递样式信息,例如 'style="border:1px solid silver"'。 +- `classOrStyle`: 对于 `showErrors` 宏,包装每个错误的 `span` 元素使用的 CSS 类的名称。如果未提供任何信息(或值为空),错误将包含在 `` 标签中。 + +以下部分概述了宏的示例。 + +输入字段 + +`formInput` 宏采用 `path` 参数 (`command.name`) 和一个额外的 `attributes` 参数(在接下来的示例中为空)。该宏与所有其他表单生成宏一起对路径参数执行隐式 Spring 绑定。绑定在新绑定发生之前一直有效,因此 `showErrors` 宏不需要再次传递路径参数——它对上次创建绑定的字段进行操作。 + +`showErrors` 宏接受一个分隔符参数(用于分隔给定字段上的多个错误的字符),还接受第二个参数——这次是类名或样式属性。请注意,FreeMarker 可以为 attributes 参数指定默认值。以下示例显示了如何使用 `formInput` 和 `showErrors` 宏: + +```xml +<@spring.formInput "command.name"/> +<@spring.showErrors "
"/> +``` + +下一个示例显示表单片段的输出,生成名称字段并在表单提交后显示验证错误,该字段中没有任何值。验证通过 Spring 的验证框架进行。 + +生成的 HTML 类似于以下示例: + +```html +Name: + +
+ required +
+
+``` + +`formTextarea` 宏的工作方式与 `formInput` 宏相同,并且接受相同的参数列表。通常,第二个参数 (`attributes`) 用于传递样式信息或 `textarea` 的 `rows` 和 `cols` 属性。 + +选中字段 + +您可以使用四个选择字段宏在 HTML 表单中生成常见的 UI 值选择输入: + +- `formSingleSelect` +- `formMultiSelect` +- `formRadioButtons` +- `formCheckboxes` + +四个宏中的每一个都接受一个“Map”选项,其中包含表单字段的值和与该值对应的标签。值和标签可以相同。 + +下一个例子是 FTL 中的单选按钮。表单支持对象为此字段指定默认值“伦敦”,因此无需验证。渲染表单时,整个可供选择的城市列表作为参考数据提供在模型中,名称为 `cityMap`。以下清单显示了示例: + +```html +... +Town: +<@spring.formRadioButtons "command.address.town", cityMap, ""/>

+``` + +前面的清单呈现一行单选按钮,一个用于 `cityMap` 中的每个值,并使用分隔符 `""`。没有提供额外的属性(缺少宏的最后一个参数)。 `cityMap` 对地图中的每个键值对使用相同的 `String`。地图的键是表单实际作为 POST 请求参数提交的内容。地图值是用户看到的标签。在前面的示例中,给定三个知名城市的列表和表单支持对象中的默认值,HTML 类似于以下内容: + +```html +Town: +London +Paris +New York +``` + +如果您的应用程序希望通过内部代码处理城市(例如),您可以使用合适的键创建代码映射,如以下示例所示: + +```java +protected Map referenceData(HttpServletRequest request) throws Exception { + Map cityMap = new LinkedHashMap<>(); + cityMap.put("LDN", "London"); + cityMap.put("PRS", "Paris"); + cityMap.put("NYC", "New York"); + + Map model = new HashMap<>(); + model.put("cityMap", cityMap); + return model; +} +``` + +代码现在生成输出,其中无线电值是相关代码,但用户仍然看到更用户友好的城市名称,如下所示: + +```html +Town: +London +Paris +New York +``` + +#### HTML 转义 + +前面描述的表单宏的默认使用导致 HTML 元素符合 HTML 4.01,并且使用 `web.xml` 文件中定义的 HTML 转义的默认值,如 Spring 的绑定支持所使用的那样。 要使元素符合 XHTML 或覆盖默认的 HTML 转义值,您可以在模板中指定两个变量(或在模型中,它们对模板可见)。 在模板中指定它们的好处是它们可以在稍后的模板处理中更改为不同的值,以便为表单中的不同字段提供不同的行为。 + +要为您的标签切换到 XHTML 合规性,请为名为 `xhtmlCompliant` 的模型或上下文变量指定 `true` 值,如以下示例所示: + +```html +<#-- for FreeMarker --> +<#assign xhtmlCompliant = true> +``` + +处理此指令后,Spring 宏生成的任何元素现在都符合 XHTML。 + +以类似的方式,您可以为每个字段指定 HTML 转义,如以下示例所示: + +```html +<#-- until this point, default HTML escaping is used --> + +<#assign htmlEscape = true> +<#-- next field will use HTML escaping --> +<@spring.formInput "command.name"/> + +<#assign htmlEscape = false in spring> +<#-- all future fields will be bound with HTML escaping off --> +``` + +## Groovy + +[Groovy 标记模板引擎](https://groovy-lang.org/templating.html#_the_markuptemplateengine) 主要用于生成类似 XML 的标记(XML、XHTML、HTML5 等),但可以使用它来生成任何基于文本的内容。 Spring Framework 具有将 Spring MVC 与 Groovy 标记结合使用的内置集成。 + +### 配置 + +以下示例显示如何配置 Groovy 标记模板引擎: + +```java +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.groovy(); + } + + // Configure the Groovy Markup Template Engine... + + @Bean + public GroovyMarkupConfigurer groovyMarkupConfigurer() { + GroovyMarkupConfigurer configurer = new GroovyMarkupConfigurer(); + configurer.setResourceLoaderPath("/WEB-INF/"); + return configurer; + } +} +``` + +以下示例显示了如何在 XML 中配置相同的内容: + +```xml + + + + + + + + +``` + +### 示例 + +与传统的模板引擎不同,Groovy 标记依赖于使用构建器语法的 DSL。以下示例显示了 HTML 页面的示例模板: + +```groovy +yieldUnescaped '' +html(lang:'en') { + head { + meta('http-equiv':'"Content-Type" content="text/html; charset=utf-8"') + title('My page') + } + body { + p('This is an example of HTML contents') + } +} +``` + +## 脚本视图 + +Spring 有一个内置的集成,可以将 Spring MVC 与任何可以在 [JSR-223](https://www.jcp.org/en/jsr/detail?id=223) 之上运行的模板库一起使用 Java 脚本引擎。 我们在不同的脚本引擎上测试了以下模板库: + +| 脚本库 | 脚本引擎 | +| :--------------------------------------------------------------------------------- | :---------------------------------------------------- | +| [Handlebars](https://handlebarsjs.com/) | [Nashorn](https://openjdk.java.net/projects/nashorn/) | +| [Mustache](https://mustache.github.io/) | [Nashorn](https://openjdk.java.net/projects/nashorn/) | +| [React](https://facebook.github.io/react/) | [Nashorn](https://openjdk.java.net/projects/nashorn/) | +| [EJS](https://www.embeddedjs.com/) | [Nashorn](https://openjdk.java.net/projects/nashorn/) | +| [ERB](https://www.stuartellis.name/articles/erb/) | [JRuby](https://www.jruby.org/) | +| [String templates](https://docs.python.org/2/library/string.html#template-strings) | [Jython](https://www.jython.org/) | +| [Kotlin Script templating](https://github.com/sdeleuze/kotlin-script-templating) | [Kotlin](https://kotlinlang.org/) | + +### 要求 + +需要在类路径中包含脚本引擎,具体细节因脚本引擎而异: + +- The [Nashorn](https://openjdk.java.net/projects/nashorn/) Java 8+ 提供了 JavaScript 引擎。强烈建议使用可用的最新更新版本。 +- [JRuby](https://www.jruby.org/) 应该作为 Ruby 支持的依赖项添加。 +- [Jython](https://www.jython.org/) 应该作为 Python 支持的依赖项添加。 +- `org.jetbrains.kotlin:kotlin-script-util` 依赖项和包含 `org.jetbrains.kotlin.script.jsr223.KotlinJsr223JvmLocalScriptEngineFactory` 行的 `META-INF/services/javax.script.ScriptEngineFactory` 文件应该被添加 Kotlin 脚本支持。 有关详细信息,请参阅[此示例](https://github.com/sdeleuze/kotlin-script-templating)。 + +您需要有脚本模板库。 为 JavaScript 做到这一点的一种方法是通过 [WebJars](https://www.webjars.org/)。 + +### 脚本模板 + +可以声明一个 `ScriptTemplateConfigurer` 来指定要使用的脚本引擎、要加载的脚本文件、调用什么函数来渲染模板等等。 以下示例使用 Mustache 模板和 Nashorn JavaScript 引擎: + +```java +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("mustache.js"); + configurer.setRenderObject("Mustache"); + configurer.setRenderFunction("render"); + return configurer; + } +} +``` + +以下示例显示了 XML 中的相同配置: + +```xml + + + + + + + + + +``` + +对于 Java 和 XML 配置,controller 看起来没有什么不同,如以下示例所示: + +```java +@Controller +public class SampleController { + + @GetMapping("/sample") + public String test(Model model) { + model.addAttribute("title", "Sample title"); + model.addAttribute("body", "Sample body"); + return "template"; + } +} +``` + +以下示例显示了 Mustache 模板: + +```html + + + {{title}} + + +

{{body}}

+ + +``` + +使用以下参数调用渲染函数: + +- `String template`: 模板内容 +- `地图模型`:视图模型 +- `RenderingContext renderingContext`: [`RenderingContext`](https://docs.spring.io/spring-framework/docs/6.0.5/javadoc-api/org/springframework/web/servlet/view/script/RenderingContext.html) 允许访问应用上下文、语言环境、模板加载器和 URL(自 5.0 起) + +如果您的模板技术需要一些自定义,您可以提供一个实现自定义渲染功能的脚本。 例如,[Handlerbars](https://handlebarsjs.com/) 需要在使用之前编译模板,并且需要一个 [polyfill](https://en.wikipedia.org/wiki/Polyfill) 来模拟一些浏览器工具,但在服务器端脚本引擎中不可用。 + +以下示例显示了如何执行此操作: + +```java +@Configuration +@EnableWebMvc +public class WebConfig implements WebMvcConfigurer { + + @Override + public void configureViewResolvers(ViewResolverRegistry registry) { + registry.scriptTemplate(); + } + + @Bean + public ScriptTemplateConfigurer configurer() { + ScriptTemplateConfigurer configurer = new ScriptTemplateConfigurer(); + configurer.setEngineName("nashorn"); + configurer.setScripts("polyfill.js", "handlebars.js", "render.js"); + configurer.setRenderFunction("render"); + configurer.setSharedEngine(false); + return configurer; + } +} +``` + +`polyfill.js` 只定义了 Handlebars 正常运行所需的 `window` 对象,如下: + +```javascript +var window = {} +``` + +这个基本的 `render.js` 实现在使用之前编译模板。 生产就绪的实现还应该存储任何重复使用的缓存模板或预编译模板。 您可以在脚本端这样做(并处理您需要的任何定制——管理模板引擎配置,例如)。 以下示例显示了如何执行此操作: + +```javascript +function render(template, model) { + var compiledTemplate = Handlebars.compile(template) + return compiledTemplate(model) +} +``` + +查看 Spring Framework 单元测试,[Java](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/java/org/springframework/web/servlet/view/script) 和[资源](https://github.com/spring-projects/spring-framework/tree/main/spring-webmvc/src/test/resources/org/springframework/web/servlet/view/script),以获取更多配置示例。 + +## JSP 和 JSTL + +Spring Framework 具有将 Spring MVC 与 JSP 和 JSTL 结合使用的内置集成。 + +> 更多内容详见:[Spring 官方文档之 JSP and JSTL](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-view-jsp) + +## RSS and Atom + +`AbstractAtomFeedView` 和 `AbstractRssFeedView` 都继承自 `AbstractFeedView` 基类,分别用于提供 Atom 和 RSS Feed 视图。 它们基于 [ROME](https://rometools.github.io/rome/) 项目,位于 org.springframework.web.servlet.view.feed 包中。 + +`AbstractAtomFeedView` 要求您实现 `buildFeedEntries()` 方法并可选择覆盖 `buildFeedMetadata()` 方法(默认实现为空)。 以下示例显示了如何执行此操作: + +```java +public class SampleContentAtomView extends AbstractAtomFeedView { + + @Override + protected void buildFeedMetadata(Map model, + Feed feed, HttpServletRequest request) { + // implementation omitted + } + + @Override + protected List buildFeedEntries(Map model, + HttpServletRequest request, HttpServletResponse response) throws Exception { + // implementation omitted + } +} +``` + +类似的要求适用于实现 `AbstractRssFeedView`,如以下示例所示: + +```java +public class SampleContentRssView extends AbstractRssFeedView { + + @Override + protected void buildFeedMetadata(Map model, + Channel feed, HttpServletRequest request) { + // implementation omitted + } + + @Override + protected List buildFeedItems(Map model, + HttpServletRequest request, HttpServletResponse response) throws Exception { + // implementation omitted + } +} +``` + +`buildFeedItems()` 和 `buildFeedEntries()` 方法传入 HTTP 请求,以防您需要访问 Locale。 传入 HTTP 响应仅用于设置 cookie 或其他 HTTP 标头。 方法返回后,提要会自动写入响应对象。 + +有关创建 Atom 视图的示例,请参阅 Alef Arendsen 的 Spring Team 博客 [entry](https://spring.io/blog/2009/03/16/adding-an-atom-view-to-an-application-using-spring-s-rest-support)。 + +## PDF and Excel + +Spring 提供了返回 HTML 以外的输出的方法,包括 PDF 和 Excel 电子表格。 + +### 文档视图简介 + +HTML 页面并不总是用户查看模型输出的最佳方式,Spring 使从模型数据动态生成 PDF 文档或 Excel 电子表格变得简单。 该文档是视图,从服务器流出正确的内容类型,(希望)使客户端 PC 能够运行他们的电子表格或 PDF 查看器应用程序作为响应。 + +为了使用 Excel 视图,您需要将 Apache POI 库添加到类路径中。 对于 PDF 生成,您需要添加(最好)OpenPDF 库。 + +### PDF 视图 + +单词列表的简单 PDF 视图可以扩展 `org.springframework.web.servlet.view.document.AbstractPdfView` 并实现 `buildPdfDocument()` 方法,如以下示例所示: + +```java +public class PdfWordList extends AbstractPdfView { + + protected void buildPdfDocument(Map model, Document doc, PdfWriter writer, + HttpServletRequest request, HttpServletResponse response) throws Exception { + + List words = (List) model.get("wordList"); + for (String word : words) { + doc.add(new Paragraph(word)); + } + } +} +``` + +控制器可以从外部视图定义(按名称引用它)或作为处理程序方法的 `View` 实例返回此类视图。 + +### Excel 视图 + +从 Spring Framework 4.2 开始,`org.springframework.web.servlet.view.document.AbstractXlsView` 作为 Excel 视图的基类提供。 它基于 Apache POI,具有专门的子类(`AbstractXlsxView` 和 `AbstractXlsxStreamingView`),取代了过时的 `AbstractExcelView` 类。 + +编程模型类似于 `AbstractPdfView`,以 `buildExcelDocument()` 作为核心模板方法,控制器能够从外部定义(按名称)或作为处理程序方法的 `View` 实例返回此类视图。 + +## Jackson + +Spring 提供对 Jackson JSON 库的支持。 + +### 基于 Jackson 的 JSON MVC 视图 + +`MappingJackson2JsonView` 使用 Jackson 库的 `ObjectMapper` 将响应内容渲染为 JSON。 默认情况下,模型映射的全部内容(特定于框架的类除外)都编码为 JSON。 对于需要过滤 map 内容的情况,您可以使用 `modelKeys` 属性指定一组特定的模型属性进行编码。 您还可以使用 `extractValueFromSingleKeyModel` 属性直接提取和序列化单键模型中的值,而不是作为模型属性的映射。 + +您可以根据需要使用 Jackson 提供的注释自定义 JSON 映射。 当您需要进一步控制时,您可以通过 `ObjectMapper` 属性注入自定义 `ObjectMapper`,适用于需要为特定类型提供自定义 JSON 序列化器和反序列化器的情况。 + +### 基于 Jackson 的 XML 视图 + +`MappingJackson2XmlView` 使用 [Jackson XML 扩展](https://github.com/FasterXML/jackson-dataformat-xml) `XmlMapper` 将响应内容渲染为 XML。 如果模型包含多个条目,您应该使用 `modelKey` bean 属性显式设置要序列化的对象。 如果模型包含单个条目,它会自动序列化。 + +您可以根据需要使用 JAXB 或 Jackson 提供的注释自定义 XML 映射。当您需要进一步控制时,您可以通过 `ObjectMapper` 属性注入自定义 `XmlMapper`,对于需要为特定类型提供序列化器和反序列化器的自定义 XML 的情况 + +## XML + +`MarshallingView` 使用 XML `Marshaller`(在 `org.springframework.oxm` 包中定义)将响应内容渲染为 XML。 您可以使用 `MarshallingView` 实例的 `modelKey` 属性显式设置要编组的对象。 或者,视图遍历所有模型属性并编组 `Marshaller` 支持的第一个类型。 有关 `org.springframework.oxm` 包中功能的更多信息,请参阅 [Marshalling XML using O/X Mappers](https://docs.spring.io/spring-framework/docs/current/reference/html/data-access.html#oxm)。 + +## XSLT + +XSLT 是 XML 的一种转换语言,作为 Web 应用程序中的一种视图技术很受欢迎。 如果您的应用程序自然地处理 XML,或者如果您的模型可以很容易地转换为 XML,那么 XSLT 作为一种视图技术是一个不错的选择。 以下部分展示了如何生成 XML 文档作为模型数据,并在 Spring Web MVC 应用程序中使用 XSLT 对其进行转换。 + +此示例是一个简单的 Spring 应用程序,它在 `Controller` 中创建关键字列表并将它们添加到模型映射中。 返回映射以及我们的 XSLT 视图的视图名称。 有关 Spring Web MVC 的 `Controller` 接口的详细信息,请参阅 [Annotated Controllers](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-controller)。 XSLT 控制器将单词列表转换为准备转换的简单 XML 文档。 + +### Beans + +配置是一个简单的 Spring Web 应用程序的标准配置:MVC 配置必须定义一个 `XsltViewResolver` 和常规 MVC 注释配置。以下示例显示了如何执行此操作: + +```java +@EnableWebMvc +@ComponentScan +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Bean + public XsltViewResolver xsltViewResolver() { + XsltViewResolver viewResolver = new XsltViewResolver(); + viewResolver.setPrefix("/WEB-INF/xsl/"); + viewResolver.setSuffix(".xslt"); + return viewResolver; + } +} +``` + +### Controller + +我们还需要一个控制器来封装我们的单词生成逻辑。 + +控制器逻辑封装在一个 `@Controller` 类中,处理方法定义如下: + +```java +@Controller +public class XsltController { + + @RequestMapping("/") + public String home(Model model) throws Exception { + Document document = DocumentBuilderFactory.newInstance().newDocumentBuilder().newDocument(); + Element root = document.createElement("wordList"); + + List words = Arrays.asList("Hello", "Spring", "Framework"); + for (String word : words) { + Element wordNode = document.createElement("word"); + Text textNode = document.createTextNode(word); + wordNode.appendChild(textNode); + root.appendChild(wordNode); + } + + model.addAttribute("wordList", root); + return "home"; + } +} +``` + +到目前为止,我们只创建了一个 DOM 文档并将其添加到模型映射中。请注意,您还可以将 XML 文件作为 `Resource` 加载并使用它来代替自定义 DOM 文档。 + +有可用的软件包可以自动 'domify' 一个对象图,但是在 Spring 中,您可以完全灵活地以您选择的任何方式从您的模型创建 DOM。这可以防止 XML 的转换在模型数据的结构中发挥太大作用,这在使用工具管理 DOMification 过程时是一种危险。 + +### Transformation + +最后,`XsltViewResolver` 解析 “home” XSLT 模板文件并将 DOM 文档合并到其中以生成我们的视图。如 `XsltViewResolver` 配置所示,XSLT 模板位于 `WEB-INF/xsl` 目录下的 `war` 文件中,并以 `xslt` 文件扩展名结尾。 + +以下示例显示了 XSLT 转换: + +```xml + + + + + + + + Hello! + +

My First Words

+
    + +
+ + +
+ + +
  • +
    + +
    +``` + +前面的转换渲染为以下 HTML: + +```html + + + + Hello! + + +

    My First Words

    +
      +
    • Hello
    • +
    • Spring
    • +
    • Framework
    • +
    + + +``` + +## 参考资料 + +- [Spring Framework 官方文档](https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/index.html) +- [Spring Framework 官方文档之 Web](https://docs.spring.io/spring-framework/docs/current/reference/html/web.html) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" index ef62068ad7..5c9740d5f7 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/21.SpringBoot\344\271\213\345\272\224\347\224\250EasyUI.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - Web -permalink: /pages/ad0516/ +permalink: /pages/3e39cd2c/ --- # SpringBoot 之应用 EasyUI diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" index 0d69c4ffb2..1cd2492449 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/03.SpringWeb/README.md" @@ -12,7 +12,7 @@ tags: - Spring - SpringBoot - Web -permalink: /pages/e2586a/ +permalink: /pages/f9b6aef8/ hidden: true index: false --- @@ -23,7 +23,12 @@ index: false ## 📖 内容 -- [Spring WebMvc](01.SpringWebMvc.md) +- [SpringWeb 综述](01.SpringWeb综述.md) +- [SpringWeb 应用](02.SpringWeb应用.md) +- [DispatcherServlet](03.DispatcherServlet.md) +- [Spring 过滤器](04.Spring过滤器.md) +- [Spring 跨域](05.Spring跨域.md) +- [Spring 视图](06.Spring视图.md) - [SpringBoot 之应用 EasyUI](21.SpringBoot之应用EasyUI.md) ## 📚 资料 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" index 605bc8adfe..88fb2b8a26 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/01.SpringBoot\344\271\213\345\274\202\346\255\245\350\257\267\346\261\202.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - 异步 -permalink: /pages/92add2/ +permalink: /pages/ba95662f/ --- # SpringBoot 教程之处理异步请求 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" index 0e5da98728..23b61c12f1 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/02.SpringBoot\344\271\213Json.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - JSON -permalink: /pages/676725/ +permalink: /pages/f7d8c4ce/ --- # SpringBoot 之集成 Json diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" index f2c2259cfa..80f4f0c697 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/03.SpringBoot\344\271\213\351\202\256\344\273\266.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - 邮件 -permalink: /pages/2586f1/ +permalink: /pages/21302e7e/ --- # SpringBoot 之发送邮件 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" index 32a77f97aa..eb4fd372da 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/04.SpringIO/README.md" @@ -12,7 +12,7 @@ tags: - Spring - SpringBoot - IO -permalink: /pages/56581b/ +permalink: /pages/c9519a1f/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" index 58c1563200..a7376038c7 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/01.Spring\351\233\206\346\210\220\347\274\223\345\255\230.md" @@ -13,7 +13,7 @@ tags: - Spring - 集成 - 缓存 -permalink: /pages/a311cb/ +permalink: /pages/5cd0d547/ --- # Spring 集成缓存中间件 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" index 2f460e46df..0f0be77c93 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/02.Spring\351\233\206\346\210\220\350\260\203\345\272\246\345\231\250.md" @@ -13,7 +13,7 @@ tags: - Spring - 集成 - 调度器 -permalink: /pages/a187f0/ +permalink: /pages/196faeed/ --- # Spring 集成调度器 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" index a36f1168c8..8625a60bab 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/03.Spring\351\233\206\346\210\220Dubbo.md" @@ -13,7 +13,7 @@ tags: - Spring - 集成 - Dubbo -permalink: /pages/274fd7/ +permalink: /pages/1bbc8647/ --- # Spring 集成 Dubbo diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" index 068cd0a824..4c7978acaf 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/05.Spring\351\233\206\346\210\220/README.md" @@ -12,7 +12,7 @@ tags: - Spring - SpringBoot - 集成 -permalink: /pages/d6025b/ +permalink: /pages/a43c2fcd/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" index 90cd3fd6bf..c79a6376d7 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/10.Spring\345\256\211\345\205\250/01.SpringBoot\344\271\213\345\256\211\345\205\250\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -13,7 +13,7 @@ tags: - Spring - SpringBoot - 安全 -permalink: /pages/568352/ +permalink: /pages/274214a7/ --- # SpringBoot 之安全快速入门 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" index 9864760965..f1140ad4f1 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/01.Spring4\345\215\207\347\272\247.md" @@ -8,8 +8,10 @@ categories: - Spring - Spring其他 tags: - - null -permalink: /pages/752c6a/ + - Java + - 框架 + - Spring +permalink: /pages/ab3b4763/ --- # Spring 4 升级踩雷指南 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" index 09a1e61c6a..f39200f1e2 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/21.SpringBoot\344\271\213banner.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/bac2ce/ +permalink: /pages/8582ef65/ --- # SpringBoot 之 banner 定制 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" index f49e516c34..d3a194176c 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/22.SpringBoot\344\271\213Actuator.md" @@ -12,7 +12,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/c013cc/ +permalink: /pages/892679dd/ --- # SpringBoot Actuator 快速入门 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/README.md" index 3e0c927578..f23fdff704 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/99.Spring\345\205\266\344\273\226/README.md" @@ -11,7 +11,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/6bb8c1/ +permalink: /pages/d2e118ed/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/README.md" index e19a47d0d9..a6a96dad13 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/01.Spring/README.md" @@ -10,7 +10,7 @@ tags: - 框架 - Spring - SpringBoot -permalink: /pages/a1a3d3/ +permalink: /pages/bf8b7d5a/ hidden: true index: false --- @@ -72,7 +72,12 @@ index: false ### Web -- [Spring WebMvc](03.SpringWeb/01.SpringWebMvc.md) +- [SpringWeb 综述](03.SpringWeb/01.SpringWeb综述.md) +- [SpringWeb 应用](03.SpringWeb/02.SpringWeb应用.md) +- [DispatcherServlet](03.SpringWeb/03.DispatcherServlet.md) +- [Spring 过滤器](03.SpringWeb/04.Spring过滤器.md) +- [Spring 跨域](03.SpringWeb/05.Spring跨域.md) +- [Spring 视图](03.SpringWeb/06.Spring视图.md) - [SpringBoot 之应用 EasyUI](03.SpringWeb/21.SpringBoot之应用EasyUI.md) ### IO diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" index c6bbb2789b..53ebeb7f5c 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/01.Mybatis\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -11,7 +11,7 @@ tags: - 框架 - ORM - Mybatis -permalink: /pages/d4e6ee/ +permalink: /pages/5a2da192/ --- # MyBatis 快速入门 @@ -393,4 +393,4 @@ public class ExamplePlugin implements Interceptor { - [mybatis 源码中文注释](https://github.com/tuguangquan/mybatis) - [MyBatis Generator 详解](https://blog.csdn.net/isea533/article/details/42102297) - [Mybatis 常见面试题](https://juejin.im/post/5aa646cdf265da237e095da1) - - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) + - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" index bd8b3760af..1110fcf9f8 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/02.Mybatis\345\216\237\347\220\206.md" @@ -11,7 +11,7 @@ tags: - 框架 - ORM - Mybatis -permalink: /pages/d55184/ +permalink: /pages/20966ea3/ --- # Mybatis 原理 @@ -860,4 +860,4 @@ public List handleResultSets(Statement stmt) throws SQLException { - **文章** - [深入理解 Mybatis 原理](https://blog.csdn.net/luanlouis/article/details/40422941) - [Mybatis 源码中文注释](https://github.com/tuguangquan/Mybatis) - - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) + - [Mybatis 中强大的 resultMap](https://juejin.im/post/5cee8b61e51d455d88219ea4) \ No newline at end of file diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" index 1cb04ab89b..da2398bfcf 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/11.ORM/README.md" @@ -9,7 +9,7 @@ tags: - Java - 框架 - ORM -permalink: /pages/fe879a/ +permalink: /pages/1e4215bc/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" index 70d8f3dda2..22ecb3e85f 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/01.Shiro.md" @@ -11,7 +11,7 @@ tags: - 框架 - 安全 - Shiro -permalink: /pages/3295c4/ +permalink: /pages/312bd026/ --- # Shiro 快速入门 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" index 3434c2dd27..9ff4ea4f47 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/12.\345\256\211\345\205\250/02.SpringSecurity.md" @@ -11,7 +11,7 @@ tags: - 框架 - 安全 - SpringSecurity -permalink: /pages/050cdd/ +permalink: /pages/6425eb64/ --- # Spring Security 快速入门 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" index 8bf182aac1..0d84f57586 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/13.IO/01.Netty.md" @@ -10,7 +10,7 @@ tags: - Java - IO - Netty -permalink: /pages/10bd70/ +permalink: /pages/f2c7208b/ --- # Netty 快速入门 diff --git "a/source/_posts/01.Java/13.\346\241\206\346\236\266/README.md" "b/source/_posts/01.Java/13.\346\241\206\346\236\266/README.md" index 7843b3d3eb..e0e8b7523f 100644 --- "a/source/_posts/01.Java/13.\346\241\206\346\236\266/README.md" +++ "b/source/_posts/01.Java/13.\346\241\206\346\236\266/README.md" @@ -7,7 +7,7 @@ categories: tags: - Java - 框架 -permalink: /pages/e373d7/ +permalink: /pages/4fdc7a8d/ hidden: true index: false --- @@ -63,7 +63,12 @@ index: false #### Web -- [Spring WebMvc](01.Spring/03.SpringWeb/01.SpringWebMvc.md) +- [SpringWeb 综述](01.Spring/03.SpringWeb/01.SpringWeb综述.md) +- [SpringWeb 应用](01.Spring/03.SpringWeb/02.SpringWeb应用.md) +- [DispatcherServlet](01.Spring/03.SpringWeb/03.DispatcherServlet.md) +- [Spring 过滤器](01.Spring/03.SpringWeb/04.Spring过滤器.md) +- [Spring 跨域](01.Spring/03.SpringWeb/05.Spring跨域.md) +- [Spring 视图](01.Spring/03.SpringWeb/06.Spring视图.md) - [SpringBoot 之应用 EasyUI](01.Spring/03.SpringWeb/21.SpringBoot之应用EasyUI.md) #### IO diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" index 9adf2b83c6..6cab7e9ca6 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/02.Java\347\274\223\345\255\230\344\270\255\351\227\264\344\273\266.md" @@ -10,7 +10,7 @@ tags: - Java - 中间件 - 缓存 -permalink: /pages/85460d/ +permalink: /pages/a8658bce/ --- # Java 缓存中间件 diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" index dd76153f46..e2a4949fc4 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/04.Ehcache.md" @@ -11,7 +11,7 @@ tags: - 中间件 - 缓存 - Ehcache -permalink: /pages/5f7893/ +permalink: /pages/14ada432/ --- # Ehcache 快速入门 diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" index 53562aa9ba..aa6f7cf395 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/05.Java\350\277\233\347\250\213\345\206\205\347\274\223\345\255\230.md" @@ -10,7 +10,7 @@ tags: - Java - 中间件 - 缓存 -permalink: /pages/59f078/ +permalink: /pages/45a5db60/ --- # Java 进程内缓存 diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" index b9c26d3455..bf3cfd5257 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/06.Http\347\274\223\345\255\230.md" @@ -9,7 +9,7 @@ categories: tags: - 缓存 - Http -permalink: /pages/30abaa/ +permalink: /pages/f5949220/ --- # Http 缓存 diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" index b5487f648a..e3f08f46e5 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/02.\347\274\223\345\255\230/README.md" @@ -9,7 +9,7 @@ tags: - Java - 中间件 - 缓存 -permalink: /pages/c4efe9/ +permalink: /pages/1420c34e/ hidden: true index: false --- diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" index 1cb5617ae9..19e77b76aa 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/03.\346\265\201\351\207\217\346\216\247\345\210\266/01.Hystrix.md" @@ -11,20 +11,20 @@ tags: - 中间件 - 流量控制 - Hystrix -permalink: /pages/364124/ +permalink: /pages/bef3ae94/ --- # Hystrix 快速入门 -## 一、Hystrix 简介 +## Hystrix 简介 ### Hystrix 是什么 -Hystrix 是 Netflix 开源的一款容错框架,包含常用的容错方法:线程池隔离、信号量隔离、熔断、降级。 +Hystrix 是由 Netflix 开源,用于处理分布式系统的延迟和容错的一个开源组件。在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。Hystrix 采用**断路器模式**来实现服务间的彼此隔离,从而避免级联故障,以提高分布式系统整体的弹性。 -Hystrix 官方宣布**不再发布新版本**。 +“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),**向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常**,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。 -但是 Hystrix 的客户端熔断保护,断路器设计理念,有非常高的学习价值。 +Hystrix 官方已宣布**不再发布新版本**。但是,Hystrix 的断路器设计理念,有非常高的学习价值。 ### 为什么需要 Hystrix @@ -72,26 +72,20 @@ Hystrix 具有以下功能: ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717142842.png) -## Hystrix 核心概念 - -## 二、Hystrix 工作流程 +## Hystrix 原理 如下图所示,Hystrix 的工作流程大致可以分为 9 个步骤。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717143247.png) -### (一)包装命令 - -x 支持资源隔离。 +### (一)构建一个 HystrixCommand 或 HystrixObservableCommand 对象 -资源隔离,就是说,你如果要把对某一个依赖服务的所有调用请求,全部隔离在同一份资源池内,不会去用其它资源了,这就叫资源隔离。哪怕对这个依赖服务,比如说商品服务,现在同时发起的调用量已经到了 1000,但是分配给商品服务线程池内就 10 个线程,最多就只会用这 10 个线程去执行。不会因为对商品服务调用的延迟,将 Tomcat 内部所有的线程资源全部耗尽。 +Hystrix 进行资源隔离,其实是提供了一个抽象,叫做命令模式。这也是 Hystrix 最基本的资源隔离技术。 -Hystrix 进行资源隔离,其实是提供了一个抽象,叫做命令模式。这也是 Hystrix 最最基本的资源隔离技术。 +在使用 Hystrix 的过程中,会对依赖服务的调用请求封装成命令对象,Hystrix 对 命令对象抽象了两个抽象类:`HystrixCommand` 和 `HystrixObservableCommand` 。 -在使用 Hystrix 的过程中,会对**依赖服务**的调用请求封装成**命令对象**,Hystrix 对 **命令对象**抽象了两个抽象类:`HystrixCommand` 和`HystrixObservableCommand` 。 - -- `HystrixCommand` 表示的**命令对象**会返回一个唯一返回值。 -- `HystrixObservableCommand` 表示的**命令对象** 会返回多个返回值。 +- `HystrixCommand` 表示的命令对象会返回一个唯一返回值。 +- `HystrixObservableCommand` 表示的命令对象 会返回多个返回值。 ```java HystrixCommand command = new HystrixCommand(arg1, arg2); @@ -102,14 +96,14 @@ HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2); Hystrix 中共有 4 种方式执行命令,如下所示: -| 执行方式 | 说明 | 可用对象 | -| :------------- | :----------------------------------------------------------------------------------------------------------------------------- | :------------------------- | -| `execute()` | 阻塞式同步执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | -| `queue()` | 基于 Future 的异步方式执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | -| `observe()` | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果,代调用代码先执行(Hot Obserable) | `HystrixObservableCommand` | -| `toObvsevable` | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果,执行代码等到真正订阅的时候才会执行(cold observable) | `HystrixObservableCommand` | +| 执行方式 | 说明 | 可用对象 | +| :------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------ | :------------------------- | +| [`execute()`]() | 阻塞式同步执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`queue()`]() | 异步执行,通过 `Future` 返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`observe()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。代调用代码先执行(Hot Obserable) | `HystrixObservableCommand` | +| [`toObservable()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。执行代码等到真正订阅的时候才会执行(cold observable) | `HystrixObservableCommand` | -这四种命令中,`exeucte()`、`queue()`、`observe()`的表示也是通过`toObservable()`实现的,其转换关系如下图所示: +这四种命令中,`exeucte()`、`queue()`、`observe()` 的表示其实是通过 `toObservable()` 实现的,其转换关系如下图所示: ![img](https:////upload-images.jianshu.io/upload_images/14126519-60964d9fa41614c1.png?imageMogr2/auto-orient/strip|imageView2/2/w/563/format/webp) @@ -136,16 +130,22 @@ Observable ocValue = command.toObservable(); //cold observable,延后订阅 ### (三)是否缓存 -如果当前命令对象配置了允许从`结果缓存`中取返回结果,并且在`结果缓存`中已经缓存了请求结果,则缓存的请求结果会立刻通过 `Observable` 的格式返回。 +如果当前命令对象启用了请求缓存,并且请求的响应存在于缓存中,则缓存的响应会立刻以 `Observable` 的形式返回。 ### (四)是否开启断路器 -如果第三步没有缓存没有命中,则判断一下当前断路器的断路状态是否打开。如果断路器状态为`打开`状态,则 `Hystrix` 将不会执行此 Command 命令,直接执行**步骤 8** 调用 Fallback; +如果第三步没有缓存没有命中,则判断一下当前断路器的断路状态是否打开。如果断路器状态为打开状态,则 Hystrix 将不会执行此 Command 命令,直接执行步骤 8 调用 Fallback; -如果断路器状态是`关闭`,则执行 **步骤 5** 检查是否有足够的资源运行 Command 命令 +如果断路器状态是关闭,则执行步骤 5 检查是否有足够的资源运行 Command 命令 ### (五)信号量、线程池是否拒绝 +当您执行该命令时,Hystrix 会检查断路器以查看电路是否打开。 + +如果电路开路(或“跳闸”),则 Hystrix 将不会执行该命令,而是将流程路由到 (8) 获取回退。 + +如果电路闭合,则流程前进至 (5) 以检查是否有可用容量来运行命令。 + 如果当前要执行的 Command 命令 先关连的线程池 和队列(或者信号量)资源已经满了,Hystrix 将不会运行 Command 命令,直接执行 **步骤 8**的 Fallback 降级处理;如果未满,表示有剩余的资源执行 Command 命令,则执行**步骤 6** ### (六)construct() 或 run() @@ -187,7 +187,7 @@ Hystrix 会统计 Command 命令执行执行过程中的**成功数**、**失败 - `watch()` —订阅 `Observable` 并开始执行命令的流程; 返回一个 `Observable`,当订阅该 `Observable` 时,它会重新通知。 - `toObservable()` —返回不变的 `Observable`; 必须订阅它才能真正开始执行命令的流程。 -## 三、断路器工作原理 +## 断路器工作原理 ![img](https:////upload-images.jianshu.io/upload_images/14126519-dce007513bf90794.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) @@ -238,7 +238,7 @@ Hystrix 对系统指标的统计是基于时间窗模式的: ![img](https:////upload-images.jianshu.io/upload_images/14126519-11710915e1a5dcda.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) -## 四、资源隔离技术 +## 资源隔离技术 ### 线程池隔离 @@ -379,9 +379,7 @@ semaphore > 弊:本质上基于信号量的隔离是同步行为,所以无法做到超时熔断,所以服务方自身要控制住执行时间,避免超时。 > 应用场景:**业务服务上,有并发上限限制时,可以考虑此方式** > `Alibaba Sentinel`开源框架,就是基于信号量的熔断和断路器框架。 -## 五、Hystrix 应用 - -### Spring Cloud + Hystrix +## Hystrix 应用 - **Hystrix 配置无法动态调节生效**。Hystrix 框架本身是使用的[Archaius](https://links.jianshu.com/go?to=https%3A%2F%2Fgithub.com%2FNetflix%2Farchaius)框架完成的配置加载和刷新,但是集成自 Spring Cloud 下,无法有效地根据实时监控结果,动态调整熔断和系统参数 - **线程池和 Command 之间的配置比较复杂**,在 Spring Cloud 在做 feigin-hystrix 集成的时候,还有些 BUG,对 command 的默认配置没有处理好,导致所有 command 占用公共的 command 线程池,没有细粒度控制,还需要做框架适配调整 @@ -466,7 +464,7 @@ public interface SetterFactory { | [`queueSizeRejectionThreshold`](https://github.com/Netflix/Hystrix/wiki/Configuration#queueSizeRejectionThreshold) | 队列大小阈值,超过则拒绝 | 5 | | [`allowMaximumSizeToDivergeFromCoreSize`](https://github.com/Netflix/Hystrix/wiki/Configuration#allowMaximumSizeToDivergeFromCoreSize) | 此属性允许 maximumSize 的配置生效。该值可以等于或大于 coreSize。设置 coreSize other > default** + +#### 根据调用链路入口限流:链路限流 + +`NodeSelectorSlot` 中记录了资源之间的调用链路,这些资源通过调用关系,相互之间构成一棵调用树。这棵树的根节点是一个名字为 `machine-root` 的虚拟节点,调用链的入口都是这个虚节点的子节点。 + +一棵典型的调用树如下图所示: + +``` + machine-root + / \ + / \ + Entrance1 Entrance2 + / \ + / \ + DefaultNode(nodeA) DefaultNode(nodeA) +``` + +上图中来自入口 `Entrance1` 和 `Entrance2` 的请求都调用到了资源 `NodeA`,Sentinel 允许只根据某个入口的统计信息对资源限流。比如我们可以设置 `FlowRule.strategy` 为 `RuleConstant.CHAIN`,同时设置 `FlowRule.ref_identity` 为 `Entrance1` 来表示只有从入口 `Entrance1` 的调用才会记录到 `NodeA` 的限流统计当中,而对来自 `Entrance2` 的调用漠不关心。 + +调用链的入口是通过 API 方法 `ContextUtil.enter(name)` 定义的。 + +#### 具有关系的资源流量控制:关联流量控制 + +当两个资源之间具有资源争抢或者依赖关系的时候,这两个资源便具有了关联。比如对数据库同一个字段的读操作和写操作存在争抢,读的速度过高会影响写得速度,写的速度过高会影响读的速度。如果放任读写操作争抢资源,则争抢本身带来的开销会降低整体的吞吐量。可使用关联限流来避免具有关联关系的资源之间过度的争抢,举例来说,`read_db` 和 `write_db` 这两个资源分别代表数据库读写,我们可以给 `read_db` 设置限流规则来达到写优先的目的:设置 `FlowRule.strategy` 为 `RuleConstant.RELATE` 同时设置 `FlowRule.ref_identity` 为 `write_db`。这样当写库操作过于频繁时,读数据的请求会被限流。 + +## Sentinel 熔断降级 + +除了流量控制以外,对调用链路中不稳定的资源进行熔断降级也是保障高可用的重要措施之一。 + +![chain](https://user-images.githubusercontent.com/9434884/62410811-cd871680-b61d-11e9-9df7-3ee41c618644.png) + +Sentinel 提供以下几种熔断策略: + +- 慢调用比例 (`SLOW_REQUEST_RATIO`):选择以慢调用比例作为阈值,需要设置允许的慢调用 RT(即最大的响应时间),请求的响应时间大于该值则统计为慢调用。当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且慢调用的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求响应时间小于设置的慢调用 RT 则结束熔断,若大于设置的慢调用 RT 则会再次被熔断。 +- 异常比例 (`ERROR_RATIO`):当单位统计时长(`statIntervalMs`)内请求数目大于设置的最小请求数目,并且异常的比例大于阈值,则接下来的熔断时长内请求会自动被熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。异常比率的阈值范围是 `[0.0, 1.0]`,代表 0% - 100%。 +- 异常数 (`ERROR_COUNT`):当单位统计时长内的异常数目超过阈值之后会自动进行熔断。经过熔断时长后熔断器会进入探测恢复状态(HALF-OPEN 状态),若接下来的一个请求成功完成(没有错误)则结束熔断,否则会再次被熔断。 + +注意异常降级**仅针对业务异常**,对 Sentinel 限流降级本身的异常(`BlockException`)不生效。为了统计异常比例或异常数,需要通过 `Tracer.trace(ex)` 记录业务异常。示例: + +```java +Entry entry = null; +try { + entry = SphU.entry(resource); + + // Write your biz code here. + // <> +} catch (Throwable t) { + if (!BlockException.isBlockException(t)) { + Tracer.trace(t); + } +} finally { + if (entry != null) { + entry.exit(); + } +} +``` + +开源整合模块,如 Sentinel Dubbo Adapter, Sentinel Web Servlet Filter 或 `@SentinelResource` 注解会自动统计业务异常,无需手动调用。 + +Sentinel 支持注册自定义的事件监听器监听熔断器状态变换事件(state change event)。示例: + +```java +EventObserverRegistry.getInstance().addStateChangeObserver("logging", + (prevState, newState, rule, snapshotValue) -> { + if (newState == State.OPEN) { + // 变换至 OPEN state 时会携带触发时的值 + System.err.println(String.format("%s -> OPEN at %d, snapshotValue=%.2f", prevState.name(), + TimeUtil.currentTimeMillis(), snapshotValue)); + } else { + System.err.println(String.format("%s -> %s at %d", prevState.name(), newState.name(), + TimeUtil.currentTimeMillis())); + } + }); +``` + +## Sentinel 系统自适应保护 + +Sentinel 系统自适应保护从整体维度对应用入口流量进行控制,结合应用的 Load、总体平均 RT、入口 QPS 和线程数等几个维度的监控指标,让系统的入口流量和系统的负载达到一个平衡,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 + +Sentinel 做系统自适应保护的目的: + +- 保证系统不被拖垮 +- 在系统稳定的前提下,保持系统的吞吐量 + +Sentinel 的设计理念是,根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流。Sentinel 在系统自适应保护的实际做法是,用系统负载作为启动控制流量的值,而允许通过的流量由处理请求的能力,即请求的响应时间以及当前系统正在处理的请求速率来决定。 + +系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,让系统尽可能跑在最大吞吐量的同时保证系统整体的稳定性。 + +系统保护规则是应用整体维度的,而不是资源维度的,并且**仅对入口流量生效**。入口流量指的是进入应用的流量(`EntryType.IN`),比如 Web 服务或 Dubbo 服务端接收的请求,都属于入口流量。 + +系统规则支持以下的阈值类型: + +- **Load**(仅对 Linux/Unix-like 机器生效):当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统的 `maxQps * minRt` 计算得出。设定参考值一般是 `CPU cores * 2.5`。 +- **CPU usage**(1.5.0+ 版本):当系统 CPU 使用率超过阈值即触发系统保护(取值范围 0.0-1.0)。 +- **RT**:当单台机器上所有入口流量的平均 RT 达到阈值即触发系统保护,单位是毫秒。 +- **线程数**:当单台机器上所有入口流量的并发线程数达到阈值即触发系统保护。 +- **入口 QPS**:当单台机器上所有入口流量的 QPS 达到阈值即触发系统保护。 + +> 注:这种系统自适应算法对于低 load 的请求,它的效果是一个“兜底”的角色。**对于不是应用本身造成的 load 高的情况(如其它进程导致的不稳定的情况),效果不明显。** + +## Sentinel 集群流量控制 + +### 集群流量控制简介 + +集群流控可以精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果。 + +集群流控中共有两种身份: + +- Token Client:集群流控客户端,用于向所属 Token Server 通信请求 token。集群限流服务端会返回给客户端结果,决定是否限流。 +- Token Server:即集群流控服务端,处理来自 Token Client 的请求,根据配置的集群规则判断是否应该发放 token(是否允许通过)。 + +Sentinel 1.4.0 开始引入了集群流控模块,主要包含以下几部分: + +- `sentinel-cluster-common-default`: 公共模块,包含公共接口和实体 +- `sentinel-cluster-client-default`: 默认集群流控 client 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展 +- `sentinel-cluster-server-default`: 默认集群流控 server 模块,使用 Netty 进行通信,提供接口方便序列化协议扩展;同时提供扩展接口对接规则判断的具体实现(`TokenService`),默认实现是复用 `sentinel-core` 的相关逻辑 + +### 集群流量控制规则 + +`FlowRule` 添加了两个字段用于集群限流相关配置: + +```java +private boolean clusterMode; // 标识是否为集群限流配置 +private ClusterFlowConfig clusterConfig; // 集群限流相关配置项 +``` + +其中 用一个专门的 `ClusterFlowConfig` 代表集群限流相关配置项,以与现有规则配置项分开: + +```java +// 全局唯一的规则 ID,由集群限流管控端分配. +private Long flowId; + +// 阈值模式,默认(0)为单机均摊,1 为全局阈值. +private int thresholdType = ClusterRuleConstant.FLOW_THRESHOLD_AVG_LOCAL; + +private int strategy = ClusterRuleConstant.FLOW_CLUSTER_STRATEGY_NORMAL; + +// 在 client 连接失败或通信失败时,是否退化到本地的限流模式 +private boolean fallbackToLocalWhenFail = true; +``` + +- `flowId` 代表全局唯一的规则 ID,Sentinel 集群限流服务端通过此 ID 来区分各个规则,因此**务必保持全局唯一**。一般 flowId 由统一的管控端进行分配,或写入至 DB 时生成。 +- `thresholdType` 代表集群限流阈值模式。其中**单机均摊模式**下配置的阈值等同于单机能够承受的限额,token server 会根据客户端对应的 namespace(默认为 `project.name` 定义的应用名)下的连接数来计算总的阈值(比如独立模式下有 3 个 client 连接到了 token server,然后配的单机均摊阈值为 10,则计算出的集群总量就为 30);而全局模式下配置的阈值等同于**整个集群的总阈值**。 + +`ParamFlowRule` 热点参数限流相关的集群配置与 `FlowRule` 相似。 + +## Sentinel 热点参数限流 + +热点即经常访问的数据。很多时候我们希望统计某个热点数据中访问频次最高的 Top K 数据,并对其访问进行限制。比如: + +- 商品 ID 为参数,统计一段时间内最常购买的商品 ID 并进行限制 +- 用户 ID 为参数,针对一段时间内频繁访问的用户 ID 进行限制 + +热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流。热点参数限流可以看做是一种特殊的流量控制,仅对包含热点参数的资源调用生效。 + +![Sentinel Parameter Flow Control](https://github.com/alibaba/Sentinel/wiki/image/sentinel-hot-param-overview-1.png) + +Sentinel 利用 LRU 策略统计最近最常访问的热点参数,结合令牌桶算法来进行参数级别的流控。 + +要使用热点参数限流功能,需要引入以下依赖: + +```xml + + com.alibaba.csp + sentinel-parameter-flow-control + x.y.z + +``` + +然后为对应的资源配置热点参数限流规则,并在 `entry` 的时候传入相应的参数,即可使热点参数限流生效。 + +> 注:若自行扩展并注册了自己实现的 `SlotChainBuilder`,并希望使用热点参数限流功能,则可以在 chain 里面合适的地方插入 `ParamFlowSlot`。 + +那么如何传入对应的参数以便 Sentinel 统计呢?我们可以通过 `SphU` 类里面几个 `entry` 重载方法来传入: + +```java +public static Entry entry(String name, EntryType type, int count, Object... args) throws BlockException + +public static Entry entry(Method method, EntryType type, int count, Object... args) throws BlockException +``` + +其中最后的一串 `args` 就是要传入的参数,有多个就按照次序依次传入。比如要传入两个参数 `paramA` 和 `paramB`,则可以: + +```java +// paramA in index 0, paramB in index 1. +// 若需要配置例外项或者使用集群维度流控,则传入的参数只支持基本类型。 +SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB); +``` + +**注意**:若 entry 的时候传入了热点参数,那么 exit 的时候也一定要带上对应的参数(`exit(count, args)`),否则可能会有统计错误。正确的示例: + +```java +Entry entry = null; +try { + entry = SphU.entry(resourceName, EntryType.IN, 1, paramA, paramB); + // Your logic here. +} catch (BlockException ex) { + // Handle request rejection. +} finally { + if (entry != null) { + entry.exit(1, paramA, paramB); + } +} +``` + +对于 `@SentinelResource` 注解方式定义的资源,若注解作用的方法上有参数,Sentinel 会将它们作为参数传入 `SphU.entry(res, args)`。比如以下的方法里面 `uid` 和 `type` 会分别作为第一个和第二个参数传入 Sentinel API,从而可以用于热点规则判断: + +```java +@SentinelResource("myMethod") +public Result doSomething(String uid, int type) { + // some logic here... +} +``` + +## 来源访问控制(黑白名单) + +很多时候,我们需要根据调用方来限制资源是否通过,这时候可以使用 Sentinel 的黑白名单控制的功能。黑白名单根据资源的请求来源(`origin`)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过。 + +> 调用方信息通过 `ContextUtil.enter(resourceName, origin)` 方法中的 `origin` 参数传入。 + +黑白名单规则(`AuthorityRule`)非常简单,主要有以下配置项: + +- `resource`:资源名,即限流规则的作用对象 +- `limitApp`:对应的黑名单/白名单,不同 origin 用 `,` 分隔,如 `appA,appB` +- `strategy`:限制模式,`AUTHORITY_WHITE` 为白名单模式,`AUTHORITY_BLACK` 为黑名单模式,默认为白名单模式 + +## 注解埋点支持 + +Sentinel 提供了 `@SentinelResource` 注解用于定义资源,并提供了 AspectJ 的扩展用于自动定义资源、处理 `BlockException` 等。使用 [Sentinel Annotation AspectJ Extension](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-annotation-aspectj) 的时候需要引入以下依赖: + +```xml + + com.alibaba.csp + sentinel-annotation-aspectj + x.y.z + +``` + +`@SentinelResource` 用于定义资源,并提供可选的异常处理和 fallback 配置项。 特别地,若 blockHandler 和 fallback 都进行了配置,则被限流降级而抛出 `BlockException` 时只会进入 `blockHandler` 处理逻辑。若未配置 `blockHandler`、`fallback` 和 `defaultFallback`,则被限流降级时会将 `BlockException` **直接抛出**。 + +示例: + +```java +public class TestService { + + // 对应的 `handleException` 函数需要位于 `ExceptionUtil` 类中,并且必须为 static 函数. + @SentinelResource(value = "test", blockHandler = "handleException", blockHandlerClass = {ExceptionUtil.class}) + public void test() { + System.out.println("Test"); + } + + // 原函数 + @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback") + public String hello(long s) { + return String.format("Hello at %d", s); + } + + // Fallback 函数,函数签名与原函数一致或加一个 Throwable 类型的参数. + public String helloFallback(long s) { + return String.format("Halooooo %d", s); + } + + // Block 异常处理函数,参数最后多一个 BlockException,其余与原函数一致. + public String exceptionHandler(long s, BlockException ex) { + // Do some log here. + ex.printStackTrace(); + return "Oops, error occurred at " + s; + } +} +``` + +## 动态规则扩展 + +Sentinel 提供两种方式修改规则: + +- 通过 API 直接修改 (`loadRules`) +- 通过 `DataSource` 适配不同数据源修改 + +通过 API 修改比较直观,可以通过以下几个 API 修改不同的规则: + +```Java +FlowRuleManager.loadRules(List rules); // 修改流控规则 +DegradeRuleManager.loadRules(List rules); // 修改降级规则 +``` + +手动修改规则(硬编码方式)一般仅用于测试和演示,生产上一般通过动态规则源的方式来动态管理规则。 + +上述 `loadRules()` 方法只接受内存态的规则对象,但更多时候规则存储在文件、数据库或者配置中心当中。`DataSource` 接口给我们提供了对接任意配置源的能力。相比直接通过 API 修改规则,实现 `DataSource` 接口是更加可靠的做法。 + +我们推荐**通过控制台设置规则后将规则推送到统一的规则中心,客户端实现** `ReadableDataSource` **接口端监听规则中心实时获取变更**,流程如下: + +![push-rules-from-dashboard-to-config-center](https://user-images.githubusercontent.com/9434884/45406233-645e8380-b698-11e8-8199-0c917403238f.png) + +`DataSource` 扩展常见的实现方式有: + +- **拉模式**:客户端主动向某个规则管理中心定期轮询拉取规则,这个规则中心可以是 RDBMS、文件,甚至是 VCS 等。这样做的方式是简单,缺点是无法及时获取变更; +- **推模式**:规则中心统一推送,客户端通过注册监听器的方式时刻监听变化,比如使用 [Nacos](https://github.com/alibaba/nacos)、Zookeeper 等配置中心。这种方式有更好的实时性和一致性保证。 + +Sentinel 目前支持以下数据源扩展: + +- Pull-based: 动态文件数据源、[Consul](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-consul), [Eureka](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-eureka) +- Push-based: [ZooKeeper](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-zookeeper), [Redis](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-redis), [Nacos](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-nacos), [Apollo](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-apollo), [etcd](https://github.com/alibaba/Sentinel/tree/master/sentinel-extension/sentinel-datasource-etcd) + +流量治理标准数据源:[OpenSergo](https://sentinelguard.io/zh-cn/docs/opensergo-data-source.html) + +### 拉模式扩展 + +实现拉模式的数据源最简单的方式是继承 [`AutoRefreshDataSource`](https://github.com/alibaba/Sentinel/blob/master/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AutoRefreshDataSource.java) 抽象类,然后实现 `readSource()` 方法,在该方法里从指定数据源读取字符串格式的配置数据。比如 [基于文件的数据源](https://github.com/alibaba/Sentinel/blob/master/sentinel-demo/sentinel-demo-dynamic-file-rule/src/main/java/com/alibaba/csp/sentinel/demo/file/rule/FileDataSourceDemo.java)。 + +### 推模式扩展 + +实现推模式的数据源最简单的方式是继承 [`AbstractDataSource`](https://github.com/alibaba/Sentinel/blob/master/sentinel-extension/sentinel-datasource-extension/src/main/java/com/alibaba/csp/sentinel/datasource/AbstractDataSource.java) 抽象类,在其构造方法中添加监听器,并实现 `readSource()` 从指定数据源读取字符串格式的配置数据。比如 [基于 Nacos 的数据源](https://github.com/alibaba/Sentinel/blob/master/sentinel-extension/sentinel-datasource-nacos/src/main/java/com/alibaba/csp/sentinel/datasource/nacos/NacosDataSource.java)。 + +### 注册数据源 + +通常需要调用以下方法将数据源注册至指定的规则管理器中: + +```java +ReadableDataSource> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, parser); +FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); +``` + +若不希望手动注册数据源,可以借助 Sentinel 的 `InitFunc` SPI 扩展接口。只需要实现自己的 `InitFunc` 接口,在 `init` 方法中编写注册数据源的逻辑。比如: + +```java +package com.test.init; + +public class DataSourceInitFunc implements InitFunc { + + @Override + public void init() throws Exception { + final String remoteAddress = "localhost"; + final String groupId = "Sentinel:Demo"; + final String dataId = "com.alibaba.csp.sentinel.demo.flow.rule"; + + ReadableDataSource> flowRuleDataSource = new NacosDataSource<>(remoteAddress, groupId, dataId, + source -> JSON.parseObject(source, new TypeReference>() {})); + FlowRuleManager.register2Property(flowRuleDataSource.getProperty()); + } +} +``` + +接着将对应的类名添加到位于资源目录(通常是 `resource` 目录)下的 `META-INF/services` 目录下的 `com.alibaba.csp.sentinel.init.InitFunc` 文件中,比如: + +``` +com.test.init.DataSourceInitFunc +``` + +这样,当初次访问任意资源的时候,Sentinel 就可以自动去注册对应的数据源了。 + +## 常见问题 + +### 为什么有时候限流不是完全精准 + +滑动时间窗是在固定时间窗算法基础上增加了样本数配置,支持按需配置时间片。sentinel 默认 1 秒两个样本,即 500ms 为一个时间窗口。样本数越多,计算越精确,但性能损耗更多。选择 500ms 是一个性能与精确统计的折中值。对于限流场景来说,性能可能比统计准确更重要,所以 sentinel 不是完全精准按照配置的阈值来限流,当然也不会相差很多。这是为了保证业务服务的高性能,减少限流组件对业务服务的性能影响。 +另外,sentinel 监控数据最小精度是秒级的,但是实际统计窗口是 500ms,所以对某些极端场景的突刺流量在监控上不能很好的展示,这个是监控数据聚合的问题,不是限流不准的问题。 + +## 参考资料 + +- [Sentinel 官网](https://sentinelguard.io/zh-cn/) +- [Sentinel Github](https://github.com/alibaba/Sentinel) \ No newline at end of file diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/99.\345\205\266\344\273\226/01.\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\346\261\240.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/99.\345\205\266\344\273\226/01.\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\346\261\240.md" index f947bed49d..49241a556d 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/99.\345\205\266\344\273\226/01.\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\346\261\240.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/99.\345\205\266\344\273\226/01.\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245\346\261\240.md" @@ -10,7 +10,7 @@ tags: - Java - 中间件 - 数据库连接池 -permalink: /pages/be5227/ +permalink: /pages/a48c4708/ --- # 数据库连接池 diff --git "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" index fd3311e006..23eed400c3 100644 --- "a/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" +++ "b/source/_posts/01.Java/14.\344\270\255\351\227\264\344\273\266/README.md" @@ -8,7 +8,7 @@ tags: - 编程 - Java - 中间件 -permalink: /pages/fe6d83/ +permalink: /pages/f5854602/ hidden: true index: false --- diff --git a/source/_posts/01.Java/README.md b/source/_posts/01.Java/README.md index 51ee60ab50..f36edaeaa0 100644 --- a/source/_posts/01.Java/README.md +++ b/source/_posts/01.Java/README.md @@ -5,7 +5,7 @@ categories: - Java tags: - Java -permalink: /pages/0d2474/ +permalink: /pages/cbf6db8d/ hidden: true index: false --- @@ -50,81 +50,71 @@ index: false ## 📖 内容 -### JavaSE - -#### [Java 基础特性](01.JavaSE/01.基础特性) - -- [Java 开发环境](01.JavaSE/01.基础特性/00.Java开发环境.md) -- [Java 基础语法特性](01.JavaSE/01.基础特性/01.Java基础语法.md) -- [Java 基本数据类型](01.JavaSE/01.基础特性/02.Java基本数据类型.md) -- [Java 面向对象](01.JavaSE/01.基础特性/03.Java面向对象.md) -- [Java 方法](01.JavaSE/01.基础特性/04.Java方法.md) -- [Java 数组](01.JavaSE/01.基础特性/05.Java数组.md) -- [Java 枚举](01.JavaSE/01.基础特性/06.Java枚举.md) -- [Java 控制语句](01.JavaSE/01.基础特性/07.Java控制语句.md) -- [Java 异常](01.JavaSE/01.基础特性/08.Java异常.md) -- [Java 泛型](01.JavaSE/01.基础特性/09.Java泛型.md) -- [Java 反射](01.JavaSE/01.基础特性/10.Java反射.md) -- [Java 注解](01.JavaSE/01.基础特性/11.Java注解.md) -- [Java String 类型](01.JavaSE/01.基础特性/42.JavaString类型.md) - -#### [Java 高级特性](01.JavaSE/02.高级特性) - -- [Java 正则从入门到精通](01.JavaSE/02.高级特性/01.Java正则.md) - 关键词:`Pattern`、`Matcher`、`捕获与非捕获`、`反向引用`、`零宽断言`、`贪婪与懒惰`、`元字符`、`DFA`、`NFA` -- [Java 编码和加密](01.JavaSE/02.高级特性/02.Java编码和加密.md) - 关键词:`Base64`、`消息摘要`、`数字签名`、`对称加密`、`非对称加密`、`MD5`、`SHA`、`HMAC`、`AES`、`DES`、`DESede`、`RSA` -- [Java 国际化](01.JavaSE/02.高级特性/03.Java国际化.md) - 关键词:`Locale`、`ResourceBundle`、`NumberFormat`、`DateFormat`、`MessageFormat` -- [Java JDK8](01.JavaSE/02.高级特性/04.JDK8.md) - 关键词:`Stream`、`lambda`、`Optional`、`@FunctionalInterface` -- [Java SPI](01.JavaSE/02.高级特性/05.JavaSPI.md) - 关键词:`SPI`、`ClassLoader` - -#### [Java 容器](01.JavaSE/03.容器) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221175550.png) - -- [Java 容器简介](01.JavaSE/03.容器/01.Java容器简介.md) - 关键词:`Collection`、`泛型`、`Iterable`、`Iterator`、`Comparable`、`Comparator`、`Cloneable`、`fail-fast` -- [Java 容器之 List](01.JavaSE/03.容器/02.Java容器之List.md) - 关键词:`List`、`ArrayList`、`LinkedList` -- [Java 容器之 Map](01.JavaSE/03.容器/03.Java容器之Map.md) - 关键词:`Map`、`HashMap`、`TreeMap`、`LinkedHashMap`、`WeakHashMap` -- [Java 容器之 Set](01.JavaSE/03.容器/04.Java容器之Set.md) - 关键词:`Set`、`HashSet`、`TreeSet`、`LinkedHashSet`、`EmumSet` -- [Java 容器之 Queue](01.JavaSE/03.容器/05.Java容器之Queue.md) - 关键词:`Queue`、`Deque`、`ArrayDeque`、`LinkedList`、`PriorityQueue` -- [Java 容器之 Stream](01.JavaSE/03.容器/06.Java容器之Stream.md) - -#### [Java IO](01.JavaSE/04.IO) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200630205329.png) - -- [Java IO 模型](01.JavaSE/04.IO/01.JavaIO模型.md) - 关键词:`InputStream`、`OutputStream`、`Reader`、`Writer`、`阻塞` -- [Java NIO](01.JavaSE/04.IO/02.JavaNIO.md) - 关键词:`Channel`、`Buffer`、`Selector`、`非阻塞`、`多路复用` -- [Java 序列化](01.JavaSE/04.IO/03.Java序列化.md) - 关键词:`Serializable`、`serialVersionUID`、`transient`、`Externalizable`、`writeObject`、`readObject` -- [Java 网络编程](01.JavaSE/04.IO/04.Java网络编程.md) - 关键词:`Socket`、`ServerSocket`、`DatagramPacket`、`DatagramSocket` -- [Java IO 工具类](01.JavaSE/04.IO/05.JavaIO工具类.md) - 关键词:`File`、`RandomAccessFile`、`System`、`Scanner` - -#### [Java 并发](01.JavaSE/05.并发) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200221175827.png) - -- [Java 并发简介](01.JavaSE/05.并发/01.Java并发简介.md) - 关键词:`进程`、`线程`、`安全性`、`活跃性`、`性能`、`死锁`、`饥饿`、`上下文切换` -- [Java 线程基础](01.JavaSE/05.并发/02.Java线程基础.md) - 关键词:`Thread`、`Runnable`、`Callable`、`Future`、`wait`、`notify`、`notifyAll`、`join`、`sleep`、`yeild`、`线程状态`、`线程通信` -- [Java 并发核心机制](01.JavaSE/05.并发/03.Java并发核心机制.md) - 关键词:`synchronized`、`volatile`、`CAS`、`ThreadLocal` -- [Java 并发锁](01.JavaSE/05.并发/04.Java锁.md) - 关键词:`AQS`、`ReentrantLock`、`ReentrantReadWriteLock`、`Condition` -- [Java 原子类](01.JavaSE/05.并发/05.Java原子类.md) - 关键词:`CAS`、`Atomic` -- [Java 并发容器](01.JavaSE/05.并发/06.Java并发和容器.md) - 关键词:`ConcurrentHashMap`、`CopyOnWriteArrayList` -- [Java 线程池](01.JavaSE/05.并发/07.Java线程池.md) - 关键词:`Executor`、`ExecutorService`、`ThreadPoolExecutor`、`Executors` -- [Java 并发工具类](01.JavaSE/05.并发/08.Java并发工具类.md) - 关键词:`CountDownLatch`、`CyclicBarrier`、`Semaphore` -- [Java 内存模型](01.JavaSE/05.并发/09.Java内存模型.md) - 关键词:`JMM`、`volatile`、`synchronized`、`final`、`Happens-Before`、`内存屏障` -- [ForkJoin 框架](01.JavaSE/05.并发/10.ForkJoin框架.md) - -#### [Java 虚拟机](01.JavaSE/06.JVM) - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200628154803.png) - -- [JVM 体系结构](01.JavaSE/06.JVM/01.JVM体系结构.md) -- [JVM 内存区域](01.JavaSE/06.JVM/02.JVM内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` -- [JVM 垃圾收集](01.JavaSE/06.JVM/03.JVM垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` -- [JVM 类加载](01.JavaSE/06.JVM/04.JVM类加载.md) - 关键词:`ClassLoader`、`双亲委派` -- [JVM 字节码](01.JavaSE/06.JVM/05.JVM字节码.md) - 关键词:`bytecode`、`asm`、`javassist` -- [JVM 命令行工具](01.JavaSE/06.JVM/11.JVM命令行工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo` -- [JVM GUI 工具](01.JavaSE/06.JVM/12.JVM_GUI工具.md) - 关键词:`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` -- [JVM 实战](01.JavaSE/06.JVM/21.JVM实战.md) - 关键词:`配置`、`调优` -- [Java 故障诊断](01.JavaSE/06.JVM/22.Java故障诊断.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` +### JavaCore + +#### [Java 基础特性](01.JavaCore/01.基础特性) + +- [Java 基础语法特性](01.JavaCore/01.基础特性/Java基础语法.md) +- [Java 基本数据类型](01.JavaCore/01.基础特性/Java基本数据类型.md) +- [Java 面向对象](01.JavaCore/01.基础特性/Java面向对象.md) +- [Java 方法](01.JavaCore/01.基础特性/Java方法.md) +- [Java 数组](01.JavaCore/01.基础特性/Java数组.md) +- [Java 枚举](01.JavaCore/01.基础特性/Java枚举.md) +- [Java 控制语句](01.JavaCore/01.基础特性/Java控制语句.md) +- [Java 异常](01.JavaCore/01.基础特性/Java异常.md) +- [Java 泛型](01.JavaCore/01.基础特性/Java泛型.md) +- [Java 反射](01.JavaCore/01.基础特性/Java反射.md) +- [Java 注解](01.JavaCore/01.基础特性/Java注解.md) +- [Java String 类型](01.JavaCore/01.基础特性/JavaString类型.md) + +#### [Java 高级特性](01.JavaCore/02.高级特性) + +- [Java 正则](01.JavaCore/02.高级特性/Java正则.md) - 关键词:Pattern、Matcher、捕获与非捕获、反向引用、零宽断言、贪婪与懒惰、元字符、DFA、NFA +- [Java 编码和加密](01.JavaCore/02.高级特性/Java编码和加密.md) - 关键词:Base64、消息摘要、数字签名、对称加密、非对称加密、MD5、SHA、HMAC、AES、DES、DESede、RSA +- [Java 国际化](01.JavaCore/02.高级特性/Java国际化.md) - 关键词:Locale、ResourceBundle、NumberFormat、DateFormat、MessageFormat +- [Java JDK8](01.JavaCore/02.高级特性/JDK8特性.md) - 关键词:Stream、lambda、Optional、@FunctionalInterface +- [Java SPI](01.JavaCore/02.高级特性/JavaSPI.md) - 关键词:SPI、ClassLoader +- [JavaAgent](01.JavaCore/02.高级特性/JavaAgent.md) + +#### [Java 容器](01.JavaCore/03.容器) + +- [Java 容器简介](01.JavaCore/03.容器/Java容器简介.md) - 关键词:泛型、Iterable、Iterator、Comparable、Comparator、Cloneable、fail-fast +- [Java 容器之 List](01.JavaCore/03.容器/Java容器之List.md) - 关键词:List、ArrayList、LinkedList +- [Java 容器之 Map](01.JavaCore/03.容器/Java容器之Map.md) - 关键词:Map、HashMap、TreeMap、LinkedHashMap、WeakHashMap +- [Java 容器之 Set](01.JavaCore/03.容器/Java容器之Set.md) - 关键词:Set、HashSet、TreeSet、LinkedHashSet、EmumSet +- [Java 容器之 Queue](01.JavaCore/03.容器/Java容器之Queue.md) - 关键词:Queue、Deque、ArrayDeque、LinkedList、PriorityQueue +- [Java 容器之 Stream](01.JavaCore/03.容器/Java容器之Stream.md) + +#### [Java IO](01.JavaCore/04.IO) + +- [Java I/O 之 简介](01.JavaCore/04.IO/JavaIO简介.md) - 关键词:BIO、NIO、AIO +- [Java I/O 之 BIO](01.JavaCore/04.IO/JavaIO之BIO.md) - 关键词:BIO、InputStream、OutputStream、Reader、Writer、File、Socket、ServerSocket +- [Java I/O 之 NIO](01.JavaCore/04.IO/JavaIO之NIO.md) - 关键词:NIO、Channel、Buffer、Selector、多路复用 +- [Java I/O 之序列化](01.JavaCore/04.IO/JavaIO之序列化.md) - 关键词:Serializable、serialVersionUID、transient、Externalizable + +#### [Java 并发](01.JavaCore/05.并发) + +- [Java 并发简介](01.JavaCore/05.并发/Java并发简介.md) - 关键词:并发、线程、安全性、活跃性、性能、死锁、活锁 +- [Java 并发之内存模型](01.JavaCore/05.并发/Java并发之内存模型.md) - 关键词:JMM、Happens-Before、内存屏障、volatile、synchronized、final、指令重排序 +- [Java 并发之线程](01.JavaCore/05.并发/Java并发之线程.md) - 关键词:Thread、Runnable、Callable、Future、FutureTask、线程生命周期 +- [Java 并发之锁](01.JavaCore/05.并发/Java并发之锁.md) - 关键词:锁、Lock、Condition、ReentrantLock、ReentrantReadWriteLock、StampedLock +- [Java 并发之无锁](01.JavaCore/05.并发/Java并发之无锁.md) - 关键词:CAS、ThreadLocal、Immutability、Copy-on-Write +- [Java 并发之 AQS](01.JavaCore/05.并发/Java并发之AQS.md) - 关键词:AQS、独占锁、共享锁 +- [Java 并发之容器](01.JavaCore/05.并发/Java并发之容器.md) - 关键词:ConcurrentHashMap、CopyOnWriteArrayList +- [Java 并发之线程池](01.JavaCore/05.并发/Java并发之线程池.md) - 关键词:Executor、ExecutorService、ThreadPoolExecutor、Executors +- [Java 并发之同步工具](01.JavaCore/05.并发/Java并发之同步工具.md) - 关键词:Semaphore、CountDownLatch、CyclicBarrier +- [Java 并发之分工工具](01.JavaCore/05.并发/Java并发之分工工具.md) - 关键词:CompletableFuture、CompletionStage、ForkJoinPool + +#### [Java 虚拟机](01.JavaCore/06.JVM) + +- [Java 虚拟机简介](01.JavaCore/06.JVM/Java虚拟机简介.md) +- [Java 虚拟机之内存区域](01.JavaCore/06.JVM/Java虚拟机之内存区域.md) - 关键词:`程序计数器`、`虚拟机栈`、`本地方法栈`、`堆`、`方法区`、`运行时常量池`、`直接内存`、`OutOfMemoryError`、`StackOverflowError` +- [Java 虚拟机之垃圾收集](01.JavaCore/06.JVM/Java虚拟机之垃圾收集.md) - 关键词:`GC Roots`、`Serial`、`Parallel`、`CMS`、`G1`、`Minor GC`、`Full GC` +- [Java 虚拟机之字节码](01.JavaCore/06.JVM/Java虚拟机之字节码.md) - 关键词:`bytecode`、`asm`、`javassist` +- [Java 虚拟机之类加载](01.JavaCore/06.JVM/Java虚拟机之类加载.md) - 关键词:`ClassLoader`、`双亲委派` +- [Java 虚拟机之工具](01.JavaCore/06.JVM/Java虚拟机之工具.md) - 关键词:`jps`、`jstat`、`jmap` 、`jstack`、`jhat`、`jinfo`、`jconsole`、`jvisualvm`、`MAT`、`JProfile`、`Arthas` +- [Java 虚拟机之故障处理](01.JavaCore/06.JVM/Java虚拟机之故障处理.md) - 关键词:`CPU`、`内存`、`磁盘`、`网络`、`GC` +- [Java 虚拟机之调优](01.JavaCore/06.JVM/Java虚拟机之调优.md) - 关键词:`配置`、`调优` ### JavaEE @@ -274,7 +264,12 @@ index: false ##### Web -- [Spring WebMvc](13.框架/01.Spring/03.SpringWeb/01.SpringWebMvc.md) +- [SpringWeb 综述](13.框架/01.Spring/03.SpringWeb/01.SpringWeb综述.md) +- [SpringWeb 应用](13.框架/01.Spring/03.SpringWeb/02.SpringWeb应用.md) +- [DispatcherServlet](13.框架/01.Spring/03.SpringWeb/03.DispatcherServlet.md) +- [Spring 过滤器](13.框架/01.Spring/03.SpringWeb/04.Spring过滤器.md) +- [Spring 跨域](13.框架/01.Spring/03.SpringWeb/05.Spring跨域.md) +- [Spring 视图](13.框架/01.Spring/03.SpringWeb/06.Spring视图.md) - [SpringBoot 之应用 EasyUI](13.框架/01.Spring/03.SpringWeb/21.SpringBoot之应用EasyUI.md) ##### IO @@ -349,19 +344,40 @@ index: false ## 📚 资料 -- Java 经典书籍 - - [《Effective Java 中文版》](https://item.jd.com/12507084.html) - 本书介绍了在 Java 编程中 78 条极具实用价值的经验规则,这些经验规则涵盖了大多数开发人员每天所面临的问题的解决方案。同推荐《重构 : 改善既有代码的设计》、《代码整洁之道》、《代码大全》,有一定的内容重叠。 - - [《Java 并发编程实战》](https://item.jd.com/10922250.html) - 本书深入浅出地介绍了 Java 线程和并发,是一本完美的 Java 并发参考手册。 - - [《深入理解 Java 虚拟机》](https://item.jd.com/11252778.html) - 不去了解 JVM 的工程师,和咸鱼有什么区 - - [《Maven 实战》](https://item.jd.com/10476794.html) - 国内最权威的 Maven 专家的力作,唯一一本哦! -- 其他领域书籍 - - [《Redis 设计与实现》](https://item.jd.com/11486101.html) - 系统而全面地描述了 Redis 内部运行机制。图示丰富,描述清晰,并给出大量参考信息,是 NoSQL 数据库开发人员案头必备。 - - [《鸟哥的 Linux 私房菜 (基础学习篇)》](https://item.jd.com/12443890.html) - 本书是最具知名度的 Linux 入门书《鸟哥的 Linux 私房菜基础学习篇》的最新版,全面而详细地介绍了 Linux 操作系统。内容非常全面,建议挑选和自己实际工作相关度较高的,其他部分有需要再阅读。 - - [《Head First 设计模式》](https://item.jd.com/10100236.html) - 《Head First 设计模式》(中文版)共有 14 章,每章都介绍了几个设计模式,完整地涵盖了四人组版本全部 23 个设计模式。 - - [《HTTP 权威指南》](https://item.jd.com/11056556.html) - 本书尝试着将 HTTP 中一些互相关联且常被误解的规则梳理清楚,并编写了一系列基于各种主题的章节,对 HTTP 各方面的特性进行了介绍。纵观全书,对 HTTP“为什么”这样做进行了详细的解释,而不仅仅停留在它是“怎么做”的。 - - [《TCP/IP 详解 系列》](https://item.jd.com/11966296.html) - 完整而详细的 TCP/IP 协议指南。针对任何希望理解 TCP/IP 协议是如何实现的读者设计。 - - [《剑指 Offer:名企面试官精讲典型编程题》](https://item.jd.com/12163054.html) - 剖析了 80 个典型的编程面试题,系统整理基础知识、代码质量、解题思路、优化效率和综合能力这 5 个面试要点。 +- **书籍** + - Java 基础 + - [《Java 编程思想》](https://book.douban.com/subject/2130190/) - Thinking in java,典中典!由于成书较早,部分内容已经多少有点过时 + - [《Java 核心技术 卷 I 开发基础》](https://book.douban.com/subject/35920145/) - 第 12 版,涵盖 Java 17 的新特性 + - [《Java 核心技术 卷 II 高级特性》](https://book.douban.com/subject/36337685/) - 第 12 版,涵盖 Java 17 的新特性 + - [《Effective Java》](https://book.douban.com/subject/36818907/) - 第 3 版,涵盖 Java 9 的新特性 + - [《Head First Java》](https://book.douban.com/subject/2000732/) - 图文并茂,对新手非常友好的入门级教程 + - [《疯狂 Java 讲义》](https://book.douban.com/subject/3246499/) - 入门级教程 + - Java 并发 + - [《Java 并发编程实战》](https://book.douban.com/subject/10484692/) - 深入浅出地介绍 Java 线程和并发 + - [《Java 并发编程的艺术》](https://book.douban.com/subject/26591326/) + - Java 虚拟机 + - [《深入理解 Java 虚拟机》](https://book.douban.com/subject/34907497/) - 第 3 版,国内最好的 JVM 书籍 + - Java IO + - [《Netty 实战》](https://book.douban.com/subject/27038538/) + - 其他 + - [《Head First 设计模式》](https://book.douban.com/subject/2243615/) + - [《Java 网络编程》](https://book.douban.com/subject/1438754/) + - [《Java 加密与解密的艺术》](https://book.douban.com/subject/25861566/) + - [《阿里巴巴 Java 开发手册》](https://book.douban.com/subject/27605355/) +- **教程、社区** + - [Runoob Java 教程](https://www.runoob.com/java/java-tutorial.html) + - [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) - 极客时间教程——从面试官视角梳理如何解答常见 Java 面试问题 + - [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) - 极客时间教程——图文并茂,系统性讲解并发编程知识 + - [拉勾教育教程 - Java 并发编程 78 讲](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=16) - 拉勾教育教程——针对并发场景问题,讲解的通俗易懂 + - [极客时间教程 - Java 业务开发常见错误 100 例](https://time.geekbang.org/column/intro/100047701) - 极客时间教程——基于 Java 生产环境的真实案例,讲解“避坑”的手段,很硬核 + - [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - 极客时间教程——覆盖 80% 以上 Java 应用调优场景 + - [极客时间教程 - 深入拆解 Java 虚拟机](https://time.geekbang.org/column/intro/100010301) - 极客时间教程 + - [CS-Notes](https://github.com/CyC2018/CS-Notes) - Github 上的 Java 基础级面试教程,行文清晰简洁 + - [JavaGuide](https://github.com/Snailclimb/JavaGuide) - Github 上的 Java 面试教程,Java 基础部分讲解较为细致 + - [advanced-java](https://github.com/doocs/advanced-java) - Github 上的 Java 面试教程,分布式部分从面试官视角讲解核心考察点 + - [java-design-patterns](https://github.com/iluwatar/java-design-patterns) - Github 上的 Java 版设计模式教程 + - [Java](https://github.com/TheAlgorithms/Java) - Github 上的 Java 算法教程 ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\345\246\202\344\275\225\345\255\246\344\271\240\347\274\226\347\250\213\350\257\255\350\250\200.md" "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\345\246\202\344\275\225\345\255\246\344\271\240\347\274\226\347\250\213\350\257\255\350\250\200.md" index 75c3d34204..080bca5890 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\345\246\202\344\275\225\345\255\246\344\271\240\347\274\226\347\250\213\350\257\255\350\250\200.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\345\246\202\344\275\225\345\255\246\344\271\240\347\274\226\347\250\213\350\257\255\350\250\200.md" @@ -7,7 +7,7 @@ categories: - 编程范式 tags: - 编程 -permalink: /pages/1d2aa9/ +permalink: /pages/eeaf22de/ --- # 如何学习编程语言 diff --git "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\351\224\231\350\257\257\345\244\204\347\220\206.md" "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\351\224\231\350\257\257\345\244\204\347\220\206.md" index 6ca3e97c98..01aeaea300 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\351\224\231\350\257\257\345\244\204\347\220\206.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/01.\351\224\231\350\257\257\345\244\204\347\220\206.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 编程范式 -permalink: /pages/e51064/ +permalink: /pages/d220f10c/ --- # 错误处理 diff --git "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/README.md" "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/README.md" index 7b89514f7b..a2174b6b45 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/README.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/README.md" @@ -7,7 +7,7 @@ categories: tags: - 设计 - 编程范式 -permalink: /pages/34f6f0/ +permalink: /pages/7a94ec4e/ hidden: true index: false --- diff --git "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/\345\246\202\344\275\225\351\230\205\350\257\273\346\272\220\347\240\201.md" "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/\345\246\202\344\275\225\351\230\205\350\257\273\346\272\220\347\240\201.md" index 0a2ae2802a..951a7a6e93 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/\345\246\202\344\275\225\351\230\205\350\257\273\346\272\220\347\240\201.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/01.\347\274\226\347\250\213\350\214\203\345\274\217/\345\246\202\344\275\225\351\230\205\350\257\273\346\272\220\347\240\201.md" @@ -5,8 +5,8 @@ categories: - 编程 - 编程范式 tags: - - null -permalink: /pages/40aa36/ + - 编程 +permalink: /pages/04e2841d/ --- # XXX diff --git "a/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/01.python.md" "b/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/01.python.md" index 1078a795d5..f6628f2a60 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/01.python.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/01.python.md" @@ -9,7 +9,7 @@ tags: - 编程 - 编程语言 - python -permalink: /pages/ef501b/ +permalink: /pages/0154b026/ --- # 一篇文章让你掌握 Python diff --git "a/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/02.shell.md" "b/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/02.shell.md" index 9bebc83d75..49361ee878 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/02.shell.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/02.shell.md" @@ -9,7 +9,7 @@ tags: - 编程 - 编程语言 - shell -permalink: /pages/ea6ae1/ +permalink: /pages/8dfbf5ce/ --- # 一篇文章让你彻底掌握 Shell @@ -1897,4 +1897,4 @@ printf "\n" - [Runoob Shell 教程](http://www.runoob.com/linux/linux-shell.html) - [shellcheck](https://github.com/koalaman/shellcheck) - 一个静态 shell 脚本分析工具,本质上是 bash/sh/zsh 的 lint。 -最后,Stack Overflow 上 [bash 标签下](https://stackoverflow.com/questions/tagged/bash)有很多你可以学习的问题,当你遇到问题时,也是一个提问的好地方。 +最后,Stack Overflow 上 [bash 标签下](https://stackoverflow.com/questions/tagged/bash)有很多你可以学习的问题,当你遇到问题时,也是一个提问的好地方。 \ No newline at end of file diff --git "a/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/03.scala.md" "b/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/03.scala.md" index 2ca69327be..c5a2e81a31 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/03.scala.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/02.\347\274\226\347\250\213\350\257\255\350\250\200/03.scala.md" @@ -9,7 +9,7 @@ tags: - 编程 - 编程语言 - scala -permalink: /pages/f4bd32/ +permalink: /pages/49bc50c9/ --- # 一篇文章让你彻底掌握 Scala diff --git "a/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/00.\345\210\235\350\257\206Python.md" "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/00.\345\210\235\350\257\206Python.md" new file mode 100644 index 0000000000..b37d5e05a6 --- /dev/null +++ "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/00.\345\210\235\350\257\206Python.md" @@ -0,0 +1,211 @@ +--- +title: 初识 Python +date: 2024-03-28 08:20:21 +order: 00 +categories: + - 编程 + - Python + - 基础特性 +tags: + - Python +permalink: /pages/83e67c86/ +--- + +# 初识 Python + +## Python 简介 + +Python 是一种广泛使用的解释型、高级和通用的编程语言。Python 支持多种编程范型,包括结构化、过程式、反射式、面向对象和函数式编程。它拥有动态类型系统和垃圾回收功能,能够自动管理内存使用,并且其本身拥有一个巨大而广泛的标准库。 + +### Python 历史 + +1991 年,Python 的第一个解释器诞生。 + +1994 年,Python 1.0 版本发布。它包含了异常处理、函数和模块等基本特性。 + +2000 年,Python 2.0 版本发布。它引入了新的特性,如[列表推导式](https://zh.wikipedia.org/wiki/列表推导式)、垃圾回收机制等。 + +2008 年,Python 3.0 版本发布。它进行了重大修订而不能完全后向兼容。 + +2020 年,Python 2.0 停止更新。 + +### Python 应用 + +Python 在以下领域都有用武之地。 + +- 后端开发 - Python / Java / Go / PHP +- DevOps - Python / Shell / Ruby +- 数据采集 - Python / C++ / Java +- 量化交易 - Python / C++ / R +- 数据科学 - Python / R / Julia / Matlab +- 机器学习 - Python / R / C++ / Julia +- 自动化测试 - Python / Shell + +## Python 开发环境 + +目前,Python 有两个版本,一个是 2.x 版,一个是 3.x 版,这两个版本是不兼容的。由于 3.x 版本越来越普及,所以推荐安装 3.x 版本。 + +### 安装 Python + +#### Linux + +Linux 环境自带了 Python 2.x 版本,但是如果要更新到 3.x 的版本,可以在 [Python 官网](https://www.python.org/) 下载 Python 的源代码并通过源代码构建安装的方式进行安装,具体的步骤如下所示(以 CentOS 为例): + +(1)安装依赖库(因为没有这些依赖库可能在源代码构件安装时因为缺失底层依赖库而失败)。 + +```shell +yum -y install wget gcc zlib-devel bzip2-devel openssl-devel ncurses-devel sqlite-devel readline-devel tk-devel gdbm-devel db4-devel libpcap-devel xz-devel libffi-devel +``` + +(2)下载 Python 源代码并解压缩到指定目录。 + +```shell +wget https://www.python.org/ftp/python/3.7.6/Python-3.7.6.tar.xz +xz -d Python-3.7.6.tar.xz +tar -xvf Python-3.7.6.tar +``` + +(3)切换至 Python 源代码目录并执行下面的命令进行配置和安装。 + +```shell +cd Python-3.7.6 +./configure --prefix=/usr/local/python37 --enable-optimizations +make && make install +``` + +(4)修改 .bash_profile 文件 + +```shell +cd ~ +vim .bash_profile +``` + +配置 PATH 环境变量并使其生效 + +```powershell +# ... 此处省略上面的代码 ... + +export PATH=$PATH:/usr/local/python37/bin + +# ... 此处省略下面的代码 ... +``` + +(5)激活环境变量 + +```shell +source .bash_profile +``` + +#### Mac + +Mac 系统自带的 Python 版本是 2.7。要安装 Python 3.x,有两个方法: + +方法一、从 [Python 官网](https://www.python.org/) 下载 Python 的[安装程序](https://www.python.org/downloads/),下载后双击运行并安装。 + +方法二、如果安装了 [Homebrew](https://brew.sh/),直接通过命令 `brew install python3` 安装即可。 + +#### Windows + +从 [Python 官网](https://www.python.org/) 下载合适的 Windows 安装版本(64 位还是 32 位),下载后双击运行并安装。 + +> 注:要勾选 `Add Python 3.x to PATH` 选项,将安装路径自动添加到环境变量,否则需要自行配置。 + +### 运行 Python + +执行以下命令可以检查 python 版本: + +``` +python --version +``` + +直接执行 python 命令可以进入交互式环境。 + +### 第一个程序 + +新建一个 `hello.py` 文件,内容如下: + +```python +print('hello world') +``` + +在终端执行如下命令: + +```shell +python hello.py +``` + +打印如下内容 + +``` +hello world +``` + +## Python 开发工具 + +### PyCharm + +PyCharm 是由 JetBrains 打造的一款 Python IDE,支持 macOS、 Windows、 Linux 系统。 + +我认为,[PyCharm](https://www.jetbrains.com/pycharm/) 是最好用的 Python IDE,功能丰富,UI 很酷,缺点是正版比较贵。 + +![](https://www.jetbrains.com/pycharm/img/screenshots/code-completion_animation.gif) + +### VSCode + +[VSCode](https://github.com/microsoft/vscode)(全称:Visual Studio Code)是一款由微软开发且跨平台的免费源代码编辑器,VSCode 开发环境非常简单易用。 + +### pip + +pip 是 Python 包管理工具,该工具提供了对 Python 包的查找、下载、安装、卸载的功能。 + +目前最新的 Python 版本已经预装了 pip。 + +**查看是否已经安装 pip**,可以使用以下命令: + +``` +pip --version +``` + +**下载安装包**,可以使用以下命令: + +``` +pip install some-package-name +``` + +**卸载安装包**,可以使用以下命令: + +``` +pip uninstall some-package-name +``` + +**查看已安装的包**,可以使用以下命令: + +``` +pip list +``` + +### IPython + +IPython 是一种基于 Python 的交互式解释器。相较于原生的 Python 交互式环境,IPython 提供了更为强大的编辑和交互功能。可以通过 Python 的包管理工具 pip 安装 IPython,具体的操作如下所示。 + +``` +pip install ipython +``` + +或 + +``` +pip3 install ipython +``` + +### Anaconda + +Anaconda 是一个集成的数据科学和机器学习环境,其中包括了 Python 解释器以及大量常用的数据科学库和工具。Anaconda 发行版包含了 Python。 + +Anaconda 包及其依赖项和环境的管理工具为 conda 命令,与传统的 Python pip 工具相比 Anaconda 的conda 可以更方便地在不同环境之间进行切换,环境管理较为简单。 + +Anaconda详细安装与介绍参考:[Anaconda 教程。](https://www.runoob.com/python-qt/anaconda-tutorial.html) + +## 参考资料 + +- [维基百科-Python](https://zh.wikipedia.org/wiki/Python) \ No newline at end of file diff --git "a/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/01.Python\345\237\272\347\241\200\350\257\255\346\263\225.md" "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/01.Python\345\237\272\347\241\200\350\257\255\346\263\225.md" new file mode 100644 index 0000000000..5c7a766b27 --- /dev/null +++ "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/01.Python\345\237\272\347\241\200\350\257\255\346\263\225.md" @@ -0,0 +1,306 @@ +--- +title: Python 基础语法 +date: 2024-03-28 08:20:21 +order: 01 +categories: + - 编程 + - Python + - 基础特性 +tags: + - Python +permalink: /pages/687291ac/ +--- + +# Python 基础语法 + +## 编码 + +默认情况下,Python 3 源码文件以 **UTF-8** 编码,所有字符串都是 unicode 字符串。 当然你也可以为源码文件指定不同的编码: + +```python +# -*- coding: cp-1252 -*- +``` + +## 注释 + +Python 中的注释有三种形式: + +- **单行注释**以 `#` 开头 +- **多行注释**可以用 `'''` 或 `"""` 标记开始和结尾 + +```python +# 单行注释 + +''' +这是多行注释,用三个单引号 +这是多行注释,用三个单引号 +这是多行注释,用三个单引号 +''' + +""" +这是多行注释,用三个双引号 +这是多行注释,用三个双引号 +这是多行注释,用三个双引号 +""" +``` + +## 保留字 + +Python 保留字意味着,不能将这些关键字用作任何标识符名称。 + +Python 的标准库提供了一个 keyword 模块,可以输出当前版本的所有关键字: + +```python +>>> import keyword +>>> keyword.kwlist +['False', 'None', 'True', 'and', 'as', 'assert', 'break', 'class', 'continue', 'def', 'del', 'elif', 'else', 'except', 'finally', 'for', 'from', 'global', 'if', 'import', 'in', 'is', 'lambda', 'nonlocal', 'not', 'or', 'pass', 'raise', 'return', 'try', 'while', 'with', 'yield'] +``` + +## 变量 + +Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。 + +Python 基本赋值 + +```python +a = 1 +b = 2.0 +c = "test" +print(f'a={a}') +print(f'b={b}') +print(f'c={c}') +# 输出 +# a=1 +# b=2.0 +# c=test +``` + +## 数据类型 + +Python3 中有六个标准的数据类型: + +- **不可变数据(3 个):**Number(数字)、String(字符串)、Tuple(元组); +- **可变数据(3 个):**List(列表)、Dictionary(字典)、Set(集合)。 + +## 操作符 + +Python 语言支持以下类型的运算符: + +- 算术运算符 +- 比较(关系)运算符 +- 赋值运算符 +- 逻辑运算符 +- 位运算符 +- 成员运算符 +- 身份运算符 +- 运算符优先级 + +## 语句 + +python 最具特色的就是使用缩进来表示代码块,不需要使用大括号 `{}` 。 + +缩进的空格数是可变的,但是同一个代码块的语句必须包含相同的缩进空格数。 + +```python +if True: + print ("True") +else: + print ("False") +``` + +以下代码最后一行语句缩进数的空格数不一致,会导致运行错误: + +```python +if True: + print ("Answer") + print ("True") +else: + print ("Answer") + print ("False") # 缩进不一致,会导致运行错误 +``` + +Python 通常是一行写完一条语句,但如果语句很长,我们可以使用反斜杠 `\` 来实现多行语句,例如: + +```python +total = item_one + \ + item_two + \ + item_three +``` + +在 `[]`, `{}`, 或 `()` 中的多行语句,不需要使用反斜杠 `\` ,例如: + +```python +total = ['item_one', 'item_two', 'item_three', + 'item_four', 'item_five'] +``` + +像 `if`、`while`、`def` 和 `class` 这样的复合语句,首行以关键字开始,以冒号( : )结束,该行之后的一行或多行代码构成代码组。 + +我们将首行及后面的代码组称为一个子句(clause)。 + +### 同一行显示多条语句 + +Python 可以在同一行中使用多条语句,语句之间使用分号 **;** 分割,以下是一个简单的实例: + +```python +import sys; x = 'test'; sys.stdout.write(x + '\n') +# 输出 +# test +``` + +使用交互式命令行执行,输出结果为: + +```python +>>> import sys; x = 'test'; sys.stdout.write(x + '\n') +test +5 +``` + +此处的 5 表示字符数,**test** 有 4 个字符,**\n** 表示一个字符,加起来 **5** 个字符。 + +```python +>>> import sys +>>> sys.stdout.write(" hi ") # hi 前后各有 1 个空格 + hi 4 +``` + +### 多个语句构成代码组 + +缩进相同的一组语句构成一个代码块,我们称之代码组。 + +像 if、while、def 和 class 这样的复合语句,首行以关键字开始,以冒号( : )结束,该行之后的一行或多行代码构成代码组。 + +我们将首行及后面的代码组称为一个子句(clause)。 + +如下实例: + +```python +if expression : + suite +elif expression : + suite +else : + suite +``` + +## 控制语句 + +Python 支持三类控制语句: + +- 选择语句 - `if...elif...else`、`match...case`(Python 3.10 新增) +- 循环语句 - `while`、`for` +- 中断语句 - `break`、`continue`、`pass` + +## 函数 + +Python 定义函数使用 def 关键字。 + +语法格式如下: + +```python +def 函数名(参数列表): + # do something +``` + +函数示例: + +```python +# 函数定义 +def hello(): + print("hello world!") + +# 函数调用 +hello() +``` + +## 输入和输出 + +### print 输出 + +`print` 默认输出是换行的,如果要实现不换行需要在变量末尾加上 `end=""`: + +```python +a = "a" +b = "b" +# 换行输出 +print(a) +print(b) + +print('---------') + +# 不换行输出 +print(a, end="") +print(b, end="") +print() + +# 输出 +# a +# b +# --------- +# ab +``` + +### input 输入 + +执行下面的程序在按回车键后就会等待用户输入: + +```python +input("\n按下 enter 键后退出。\n") +``` + +以上代码中,一旦用户按下 **enter** 键时,程序将退出。 + +## 模块 + +在 python 用 `import` 或者 `from...import` 来导入相应的模块。 + +将整个模块(somemodule)导入,格式为: `import somemodule` + +从某个模块中导入某个函数,格式为: `from somemodule import somefunction` + +从某个模块中导入多个函数,格式为: `from somemodule import firstfunc, secondfunc, thirdfunc` + +将某个模块中的全部函数导入,格式为: `from somemodule import *` + +【示例】导入 sys 模块 + +```python +import sys + +print('================Python import mode==========================') +print('命令行参数为:') +for i in sys.argv: + print(i) +print('\n python 路径为', sys.path) +``` + +【示例】导入 sys 模块的 argv,path 成员 + +```python +from sys import argv,path # 导入特定的成员 + +print('================python from import===================================') +print('path:',path) # 因为已经导入path成员,所以此处引用时不需要加sys.path +``` + +## 命令行参数 + +很多程序可以执行一些操作来查看一些基本信息,Python 可以使用 `-h` 参数查看各参数帮助信息: + +```shell +$ python -h +usage: python [option] ... [-c cmd | -m mod | file | -] [arg] ... +Options and arguments (and corresponding environment variables): +-c cmd : program passed in as string (terminates option list) +-d : debug output from parser (also PYTHONDEBUG=x) +-E : ignore environment variables (such as PYTHONPATH) +-h : print this help message and exit + +[ etc. ] +``` + +我们在使用脚本形式执行 Python 时,可以接收命令行输入的参数,具体使用可以参照 [Python 3 命令行参数](https://www.runoob.com/python3/python3-command-line-arguments.html)。 + +## 参考资料 + +- [菜鸟-基础教程](https://www.runoob.com/python/python-tutorial.html) \ No newline at end of file diff --git "a/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/02.Python\345\217\230\351\207\217\345\222\214\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/02.Python\345\217\230\351\207\217\345\222\214\346\225\260\346\215\256\347\261\273\345\236\213.md" new file mode 100644 index 0000000000..722da94584 --- /dev/null +++ "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/02.Python\345\217\230\351\207\217\345\222\214\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -0,0 +1,244 @@ +--- +title: Python 基础语法 +date: 2024-03-28 08:20:21 +order: 02 +categories: + - 编程 + - Python + - 基础特性 +tags: + - Python +permalink: /pages/3246e106/ +--- + +# Python 变量和数据类型 + +## 变量 + +### 变量简介 + +Python 中的变量不需要声明。每个变量在使用前都必须赋值,变量赋值以后该变量才会被创建。 + +Python 基本赋值 + +```python +a = 1 +b = 2.0 +c = "test" +print(f'a={a}') +print(f'b={b}') +print(f'c={c}') +# 输出 +# a=1 +# b=2.0 +# c=test +``` + +Python 允许多个变量同时赋值 + +```python +a = b = c = 1 +print(f'a={a}') +print(f'b={b}') +print(f'c={c}') +# 输出 +# a=1 +# b=1 +# c=1 +``` + +Python 允许为多个变量同时赋不同的值 + +```python +a, b, c = 1, 2.0, "test" +print(f'a={a}') +print(f'b={b}') +print(f'c={c}') +# 输出 +# a=1 +# b=2.0 +# c=test +``` + +### 变量命名规则 + +- 第一个字符必须是字母表中字母或下划线 `_` 。 +- 标识符的其他的部分由字母、数字和下划线组成。 +- 标识符对大小写敏感。 + +在 Python 3 中,可以用中文作为变量名,非 ASCII 标识符也是允许的了。 + +## 数据类型 + +在 Python 中,变量就是变量,它没有类型,我们所说的"类型"是变量所指的内存中对象的类型。 + +Python3 中有六个标准的数据类型: + +- **不可变数据(3 个):**Number(数字)、String(字符串)、Tuple(元组); +- **可变数据(3 个):**List(列表)、Dictionary(字典)、Set(集合)。 + +Python 内置的 `type()` 函数可以用来查询变量所指的对象类型 + +```python +a, b, c, d = 1, 2.0, True, 3.14j +print(f'a={type(a)}') +print(f'b={type(b)}') +print(f'c={type(c)}') +print(f'd={type(d)}') +# 输出 +# a= +# b= +# c= +# d= +``` + +### Number + +在 Python 中,Number 数据类型用于存储数值。 + +数据类型是不允许改变的,这就意味着如果改变 Number 数据类型的值,将重新分配内存空间。 + +Python 中数学运算常用的函数基本都在 math 模块、cmath 模块中。 + +Python math 模块提供了许多对浮点数的数学运算函数。 + +Python cmath 模块包含了一些用于复数运算的函数。 + +cmath 模块的函数跟 math 模块函数基本一致,区别是 cmath 模块运算的是复数,math 模块运算的是数学运算。 + +### 字符串 + +字符串是 Python 中最常用的数据类型。可以使用引号 ( **'** 或 **"** ) 来创建字符串。 + +Python 中单引号 `'` 和双引号 `"` 使用完全相同。 + +使用三引号(`'''` 或 `"""`)可以指定一个多行字符串。 + +转义符 `\`。 + +反斜杠可以用来转义,使用 **r** 可以让反斜杠不发生转义。 如 **r"this is a line with \n"** 则 **\n** 会显示,并不是换行。 + +按字面意义级联字符串,如 **"this " "is " "string"** 会被自动转换为 **this is string**。 + +字符串可以用 **+** 运算符连接在一起,用 ***** 运算符重复。 + +Python 中的字符串有两种索引方式,从左往右以 **0** 开始,从右往左以 **-1** 开始。 + +Python 中的字符串不能改变。 + +Python 没有单独的字符类型,一个字符就是长度为 1 的字符串。 + +字符串切片 `str[start:end]`,其中 start 是切片开始的索引,end 是切片结束的索引(但不包括该索引指向的字符)。 + +字符串的切片可以加上步长参数 step,语法格式如下:`str[start:end:step]` + +字符串的截取的语法格式如下:**变量[头下标:尾下标:步长]** + +```python +str='123456789' + +print(str) # 输出字符串 +print(str[0:-1]) # 输出第一个到倒数第二个的所有字符 +print(str[0]) # 输出字符串第一个字符 +print(str[2:5]) # 输出从第三个开始到第六个的字符(不包含) +print(str[2:]) # 输出从第三个开始后的所有字符 +print(str[1:5:2]) # 输出从第二个开始到第五个且每隔一个的字符(步长为2) +print(str * 2) # 输出字符串两次 +print(str + '你好') # 连接字符串 + +print('------------------------------') + +print('hello\nrunoob') # 使用反斜杠(\)+n转义特殊字符 +print(r'hello\nrunoob') # 在字符串前面添加一个 r,表示原始字符串,不会发生转义 +``` + +### 列表 + +序列是 Python 中最基本的数据结构。序列中的每个元素都分配一个数字 - 它的位置,或索引,第一个索引是 0,第二个索引是 1,依此类推。 + +Python 有 6 个序列的内置类型,但最常见的是列表和元组。 + +序列都可以进行的操作包括索引,切片,加,乘,检查成员。 + +列表的数据项不需要具有相同的类型。创建一个列表,只要把逗号分隔的不同的数据项使用方括号括起来即可。 + +```python +list1 = ['physics', 'chemistry', 1997, 2000] +list2 = [1, 2, 3, 4, 5 ] +list3 = ["a", "b", "c", "d"] +``` + +### 元祖 + +Python 的元组与列表类似,不同之处在于元组的元素不能修改。 + +元组使用小括号,列表使用方括号。 + +元组创建很简单,只需要在括号中添加元素,并使用逗号隔开即可。 + +```python +tup1 = ('physics', 'chemistry', 1997, 2000) +tup2 = (1, 2, 3, 4, 5 ) +tup3 = "a", "b", "c", "d" +``` + +### 字典 + +字典是另一种可变容器模型,且可存储任意类型对象。 + +字典的每个键值 **key:value** 对用冒号 **:** 分割,每个键值对之间用逗号 **,** 分割,整个字典包括在花括号 **{}** 中。 + +```python +tinydict = {'Alice': '2341', 'Beth': '9102', 'Cecil': '3258'} +tinydict1 = { 'abc': 456 } +tinydict2 = { 'abc': 123, 98.6: 37 } +``` + +## 数据类型转换 + +Python 数据类型转换可以分为两种: + +- 隐式类型转换 +- 显式类型转换 + +隐式类型转换示例 + +```python +num_int = 1 +num_float = 2.0 +num_new = num_int + num_float +print("num_int 数据类型为:", type(num_int)) +print("num_float 数据类型为:", type(num_float)) +print("num_new 值为:", num_new) +print("num_new 数据类型为:", type(num_new)) +# 输出 +# num_int 数据类型为: +# num_float 数据类型为: +# num_new 值为: 3.0 +# num_new 数据类型为: +``` + +显示类型转换方法: + +- `int()` - 将指定的数值或字符串转换成整数,可以指定进制。 +- `float()` - 将指定的字符串转换成浮点数。 +- `str()` - 将指定的对象转换成字符串,可以指定编码。 +- `chr()` - 将指定的整数转换成该编码对应的字符。 +- `ord()` - 将指定的字符转换成对应的编码(整数)。 + +```python +a = int("100") +b = float(2) +c = str(3.0) +print(f'a={a}, type={type(a)}') +print(f'b={b}, type={type(b)}') +print(f'c={c}, type={type(c)}') +# 输出 +# a=100, type= +# b=2.0, type= +# c=3.0, type= +``` + +## 参考资料 + +- [菜鸟-基础教程](https://www.runoob.com/python/python-tutorial.html) \ No newline at end of file diff --git "a/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/03.Python\346\223\215\344\275\234\347\254\246.md" "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/03.Python\346\223\215\344\275\234\347\254\246.md" new file mode 100644 index 0000000000..c2a942fa82 --- /dev/null +++ "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/03.Python\346\223\215\344\275\234\347\254\246.md" @@ -0,0 +1,123 @@ +--- +title: Python 操作符 +date: 2024-03-28 08:20:21 +order: 03 +categories: + - 编程 + - Python + - 基础特性 +tags: + - Python +permalink: /pages/91f621ce/ +--- + +# Python 操作符 + +Python 语言支持以下类型的运算符: + +- 算术运算符 +- 比较(关系)运算符 +- 赋值运算符 +- 逻辑运算符 +- 位运算符 +- 成员运算符 +- 身份运算符 +- 运算符优先级 + +## 算术运算符 + +假设变量: **a=10,b=20** + +| 运算符 | 描述 | 实例 | +| ------ | ----------------------------------------------- | -------------------------------------------------------- | +| `+` | 加 - 两个对象相加 | `a + b` 输出结果 30 | +| `-` | 减 - 得到负数或是一个数减去另一个数 | `a - b` 输出结果 -10 | +| `*` | 乘 - 两个数相乘或是返回一个被重复若干次的字符串 | `a * b` 输出结果 200 | +| `/` | 除 - x 除以 y | `b / a` 输出结果 2 | +| `%` | 取模 - 返回除法的余数 | `b % a` 输出结果 0 | +| `**` | 幂 - 返回 x 的 y 次幂 | `a**b` 为 10 的 20 次方, 输出结果 100000000000000000000 | +| `//` | 取整除 - 返回商的整数部分 | `9//2` 输出结果 4 , `9.0//2.0` 输出结果 4.0 | + +## 比较运算符 + +假设变量: **a=10,b=20** + +| 运算符 | 描述 | 实例 | +| :----- | :------------------------------------------------------------------------------------------------------------ | :----------------------------------------- | +| `==` | 等于 - 比较对象是否相等 | `(a == b)` 返回 False。 | +| `!=` | 不等于 - 比较两个对象是否不相等 | (a != b) 返回 True。 | +| `<>` | 不等于 - 比较两个对象是否不相等。**python3 已废弃。** | `(a <> b)` 返回 True。这个运算符类似 != 。 | +| `>` | 大于 - 返回 x 是否大于 y | `(a > b)` 返回 False。 | +| `<` | 小于 - 返回 x 是否小于 y。所有比较运算符返回 1 表示真,返回 0 表示假。这分别与特殊的变量 True 和 False 等价。 | `(a < b)` 返回 True。 | +| `>=` | 大于等于 - 返回 x 是否大于等于 y。 | `(a >= b)` 返回 False。 | +| `<=` | 小于等于 - 返回 x 是否小于等于 y。 | `(a <= b)` 返回 True。 | + +## 赋值运算符 + +假设变量: **a=10,b=20** + +| 运算符 | 描述 | 实例 | +| :----- | :--------------- | :------------------------------------ | +| `=` | 简单的赋值运算符 | c = a + b 将 a + b 的运算结果赋值为 c | +| `+=` | 加法赋值运算符 | `c += a` 等效于 `c = c + a` | +| `-=` | 减法赋值运算符 | `c -= a` 等效于 `c = c - a` | +| `*=` | 乘法赋值运算符 | `c *= a` 等效于 `c = c * a` | +| `/=` | 除法赋值运算符 | `c /= a` 等效于 `c = c / a` | +| `%=` | 取模赋值运算符 | `c %= a` 等效于 `c = c % a` | +| `**=` | 幂赋值运算符 | `c **= a` 等效于 `c = c ** a` | +| `//=` | 取整除赋值运算符 | `c //= a` 等效于 `c = c // a` | + +## 位运算符 + +| 运算符 | 描述 | 实例 | +| ------ | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------- | +| & | 按位与运算符:参与运算的两个值,如果两个相应位都为 1,则该位的结果为 1,否则为 0 | (a & b) 输出结果 12 ,二进制解释: 0000 1100 | +| \| | 按位或运算符:只要对应的二个二进位有一个为 1 时,结果位就为 1。 | (a \| b) 输出结果 61 ,二进制解释: 0011 1101 | +| ^ | 按位异或运算符:当两对应的二进位相异时,结果为 1 | (a ^ b) 输出结果 49 ,二进制解释: 0011 0001 | +| \~ | 按位取反运算符:对数据的每个二进制位取反,即把 1 变为 0,把 0 变为 1 | (\~a ) 输出结果 -61 ,二进制解释: 1100 0011, 在一个有符号二进制数的补码形式。 | +| << | 左移动运算符:运算数的各二进位全部左移若干位,由"<<"右边的数指定移动的位数,高位丢弃,低位补 0。 | a << 2 输出结果 240 ,二进制解释: 1111 0000 | +| >> | 右移动运算符:把">>"左边的运算数的各二进位全部右移若干位,">>"右边的数指定移动的位数 | a >> 2 输出结果 15 ,二进制解释: 0000 1111 | + +## 逻辑运算符 + +| 运算符 | 逻辑表达式 | 描述 | 实例 | +| ------ | ---------- | ----------------------------------------------------------------------- | ----------------------- | +| and | x and y | 布尔"与" - 如果 x 为 False,x and y 返回 False,否则它返回 y 的计算值。 | (a and b) 返回 20。 | +| or | x or y | 布尔"或" - 如果 x 是 True,它返回 x 的值,否则它返回 y 的计算值。 | (a or b) 返回 10。 | +| not | not x | 布尔"非" - 如果 x 为 True,返回 False 。如果 x 为 False,它返回 True。 | not(a and b) 返回 False | + +## 成员运算符 + +| 运算符 | 描述 | 实例 | +| ------ | ------------------------------------------------------- | ------------------------------------------------- | +| in | 如果在指定的序列中找到值返回 True,否则返回 False。 | x 在 y 序列中 , 如果 x 在 y 序列中返回 True。 | +| not in | 如果在指定的序列中没有找到值返回 True,否则返回 False。 | x 不在 y 序列中 , 如果 x 不在 y 序列中返回 True。 | + +## 身份运算符 + +| 运算符 | 描述 | 实例 | +| ------ | ------------------------------------------- | ---------------------------------------------------------- | +| is | is 是判断两个标识符是不是引用自一个对象 | x is y, 如果 id(x) 等于 id(y) , **is** 返回结果 1 | +| is not | is not 是判断两个标识符是不是引用自不同对象 | x is not y, 如果 id(x) 不等于 id(y). **is not** 返回结果 1 | + +## 运算符优先级 + +| 运算符 | 描述 | +| --------------------------- | ------------------------------------------------------ | +| \*\* | 指数 (最高优先级) | +| \~ + - | 按位翻转, 一元加号和减号 (最后两个的方法名为 +@ 和 -@) | +| \* / % // | 乘,除,取模和取整除 | +| + - | 加法减法 | +| >> << | 右移,左移运算符 | +| & | 位 'AND' | +| ^ \| | 位运算符 | +| <= < > >= | 比较运算符 | +| <> == != | 等于运算符 | +| = %= /= //= -= += \*= \*\*= | 赋值运算符 | +| is is not | 身份运算符 | +| in not in | 成员运算符 | +| not or and | 逻辑运算符 | + +## 参考资料 + +- [菜鸟-基础教程](https://www.runoob.com/python/python-tutorial.html) \ No newline at end of file diff --git "a/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/04.Python\346\216\247\345\210\266\350\257\255\345\217\245.md" "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/04.Python\346\216\247\345\210\266\350\257\255\345\217\245.md" new file mode 100644 index 0000000000..41f57017e2 --- /dev/null +++ "b/source/_posts/02.\347\274\226\347\250\213/03.Python/01.\345\237\272\347\241\200\347\211\271\346\200\247/04.Python\346\216\247\345\210\266\350\257\255\345\217\245.md" @@ -0,0 +1,162 @@ +--- +title: Python 控制语句 +date: 2024-03-28 08:20:21 +order: 04 +categories: + - 编程 + - Python + - 基础特性 +tags: + - Python +permalink: /pages/ae1e5524/ +--- + +# Python 控制语句 + +## 选择语句 + +Python 的选择语句的语法格式为:`if...elif...else` 语句。 + +- `if` 语句至多有 1 个 `else` 语句,`else` 语句在所有的 `elif` 语句之后。 +- `if` 语句可以有若干个 `elif` 语句,它们必须在 `else` 语句之前。 +- 一旦其中一个 `elif` 语句检测为 `true`,其他的 `elif` 以及 `else` 语句都将跳过执行。 + +```python +code = 3 +if code == 0: + print("code == 0") +elif code == 1: + print("code == 1") +else: + print("code != 0 && code != 1") +# 输出 +# code != 0 && code != 1 +``` + +## 循环语句 + +### while 循环 + +只要布尔表达式为 `true`,`while` 循环体会一直执行下去。 + +```python +count = 1 +while (count <= 5): + print('count = ', count) + count = count + 1 +# 输出 +# count = 1 +# count = 2 +# count = 3 +# count = 4 +# count = 5 +``` + +### for 循环 + +for 循环可以遍历任何的序列对象或可迭代对象。 + +【示例】遍历字符串字符 + +```python +for letter in 'python': + print("char: %s" % letter) +# 输出 +# char: p +# char: y +# char: t +# char: h +# char: o +# char: n +``` + +【示例】遍历数组 + +```python +colors = ['red', 'yellow', 'blue'] +for color in colors: + print('color: %s' % color) +# 输出 +# color: red +# color: yellow +# color: blue +``` + +【示例】遍历指定整数范围 + +```python +for num in range(1, 10): + if num % 2 == 0: + print('num = ', num) +# 输出 +# num = 2 +# num = 4 +# num = 6 +# num = 8 +``` + +## 中断语句 + +### break 语句 + +`break` 语句用来终止循环语句,即循环条件没有 False 条件或者序列还没被完全递归完,也会停止执行循环语句。 + +`break` 语句用在 `while` 和 `for` 循环中。 + +【示例】遍历字符串,找到指定字母的位置后退出 + +```python +pos = 0 +for letter in 'python': + if letter == 'h': + print('h pos: ', pos) + break + else: + pos += 1 +# 输出 +# h pos: 3 +``` + +### continue 语句 + +使用 `continue` 语句意味着跳过当前循环的剩余语句,然后继续进行下一轮循环。 + +`continue` 语句用在 `while` 和 `for` 循环中。 + +```python +num = 1 +for num in range(1, 10): + if num % 2 == 0: + continue + else: + print(f'num = {num}') +# 输出 +# num = 1 +# num = 3 +# num = 5 +# num = 7 +# num = 9 +``` + +### pass 语句 + +Python pass 是空语句,是为了保持程序结构的完整性。 + +`pass` 不做任何事情,一般用做占位语句。 + +```python +# pass 语句 +age = 65 +if age < 18: + print("未成年") +elif age >= 18 and age < 30: + print("成年人") +elif age >= 30 and age < 65: + pass +else: + print("老年人") +``` + +## 参考资料 + +- [菜鸟-基础教程](https://www.runoob.com/python/python-tutorial.html) \ No newline at end of file diff --git "a/source/_posts/02.\347\274\226\347\250\213/03.Python/README.md" "b/source/_posts/02.\347\274\226\347\250\213/03.Python/README.md" new file mode 100644 index 0000000000..9b6e3ff993 --- /dev/null +++ "b/source/_posts/02.\347\274\226\347\250\213/03.Python/README.md" @@ -0,0 +1,33 @@ +--- +title: Python +date: 2024-03-28 08:20:21 +categories: + - 编程 + - Python +tags: + - Python +permalink: /pages/3c3ccf24/ +hidden: true +index: false +--- + +# Python 教程 + +[初识 Python](01.基础特性/00.初识Python.md) + +## 📚 资料 + +- **书籍** + - [《Python Cookbook》](https://book.douban.com/subject/26381341/) + - [《流畅的 Python》](https://book.douban.com/subject/27028517/) + - [《Python 编程》](https://book.douban.com/subject/36365320/) +- **教程、社区** + - [awesome-python](https://github.com/vinta/awesome-python) - Python 资源大全 + - [awesome-python-cn](https://github.com/jobbole/awesome-python-cn) - Python 资源大全中文版 + - [Python-100-Days](https://github.com/jackfrued/Python-100-Days) - Github Python 渐进式教程 + - [python-guide](https://github.com/realpython/python-guide) - Python 最佳实践 + - [python-patterns](https://github.com/faif/python-patterns) - Python 设计模式 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/02.\347\274\226\347\250\213/README.md" "b/source/_posts/02.\347\274\226\347\250\213/README.md" index 155fd79de1..5f72e9f72b 100644 --- "a/source/_posts/02.\347\274\226\347\250\213/README.md" +++ "b/source/_posts/02.\347\274\226\347\250\213/README.md" @@ -5,7 +5,7 @@ categories: - 编程 tags: - 编程 -permalink: /pages/f85bac/ +permalink: /pages/47a444a4/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/00.\345\246\202\344\275\225\350\256\276\350\256\241\347\263\273\347\273\237.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/00.\345\246\202\344\275\225\350\256\276\350\256\241\347\263\273\347\273\237.md" index df351d5d59..7ef96e1780 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/00.\345\246\202\344\275\225\350\256\276\350\256\241\347\263\273\347\273\237.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/00.\345\246\202\344\275\225\350\256\276\350\256\241\347\263\273\347\273\237.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 设计 -permalink: /pages/0a89f3/ +permalink: /pages/5f2fa796/ --- # 如何设计系统 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/01.\347\263\273\347\273\237\346\236\266\346\236\204\351\235\242\350\257\225.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/01.\347\263\273\347\273\237\346\236\266\346\236\204\351\235\242\350\257\225.md" index 09d91f393f..803a13aa33 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/01.\347\263\273\347\273\237\346\236\266\346\236\204\351\235\242\350\257\225.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/01.\347\263\273\347\273\237\346\236\266\346\236\204\351\235\242\350\257\225.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 面试 -permalink: /pages/000a7b/ +permalink: /pages/59eefc2d/ --- # 系统架构面试 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/02.\347\263\273\347\273\237\346\236\266\346\236\204\346\246\202\350\277\260.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/02.\347\263\273\347\273\237\346\236\266\346\236\204\346\246\202\350\277\260.md" index 8d34e2806c..7ab9135389 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/02.\347\263\273\347\273\237\346\236\266\346\236\204\346\246\202\350\277\260.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/02.\347\263\273\347\273\237\346\236\266\346\236\204\346\246\202\350\277\260.md" @@ -10,7 +10,7 @@ tags: - 架构 - 分布式 - 微服务 -permalink: /pages/db2390/ +permalink: /pages/2c4f8405/ --- # 系统架构概述 @@ -479,4 +479,4 @@ permalink: /pages/db2390/ - [《大型网站技术架构:核心原理与案例分析》](https://item.jd.com/11322972.html) - 《从 0 开始学架构》 -- [软件架构入门- 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2016/09/software-architecture.html) +- [软件架构入门- 阮一峰的网络日志](http://www.ruanyifeng.com/blog/2016/09/software-architecture.html) \ No newline at end of file diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/03.\347\263\273\347\273\237\351\253\230\346\200\247\350\203\275\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/03.\347\263\273\347\273\237\351\253\230\346\200\247\350\203\275\346\236\266\346\236\204.md" index 0c3f7948de..e2c669b482 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/03.\347\263\273\347\273\237\351\253\230\346\200\247\350\203\275\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/03.\347\263\273\347\273\237\351\253\230\346\200\247\350\203\275\346\236\266\346\236\204.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 性能 -permalink: /pages/a49605/ +permalink: /pages/bf101e07/ --- # 系统高性能架构 @@ -259,7 +259,7 @@ Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池 **高性能集群的复杂性主要体现在需要增加一个任务分配器,以及为任务选择一个合适的任务分配算法**。 -> 缓存解决方案请参考:[负载均衡](https://dunwu.github.io/waterdrop/pages/98a1c1/) +> 缓存解决方案请参考:[负载均衡](https://dunwu.github.io/waterdrop/pages/bcf0fb8c/) ### 代码优化 @@ -310,13 +310,13 @@ Reactor 模式的核心组成部分包括 Reactor 和处理资源池(进程池 **读写分离的基本原理是将数据库读写操作分散到不同的节点上** -> 详细解决方案参考:[读写分离](https://dunwu.github.io/waterdrop/pages/3faf18/) +> 详细解决方案参考:[读写分离](https://dunwu.github.io/waterdrop/pages/b2caa509/) #### 数据库分库分表 **数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果**。 -> 详细解决方案参考:[分库分表](https://dunwu.github.io/waterdrop/pages/e1046e/) +> 详细解决方案参考:[分库分表](https://dunwu.github.io/waterdrop/pages/6634a2b3/) #### Nosql @@ -401,4 +401,4 @@ CDN 的本质仍然是一个缓存,而且将数据缓存在离用户最近的 ## 参考资料 - [《大型网站技术架构:核心原理与案例分析》](https://item.jd.com/11322972.html) -- [Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) \ No newline at end of file +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/04.\347\263\273\347\273\237\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/04.\347\263\273\347\273\237\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204.md" index f2e7f1186b..c70c37706b 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/04.\347\263\273\347\273\237\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/04.\347\263\273\347\273\237\351\253\230\345\217\257\347\224\250\346\236\266\346\236\204.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 高可用 -permalink: /pages/9a462f/ +permalink: /pages/ce132f76/ --- # 系统高可用架构 @@ -78,15 +78,15 @@ permalink: /pages/9a462f/ 学习高可用架构,首先需要了解分布式基础理论:CAP 和 BASE。 -然后,很多著名的分布式系统,都利用选举机制,来保证主节点宕机时的故障恢复。如果要深入理解选举机制,有必要了解:[Paxos 算法](https://dunwu.github.io/waterdrop/pages/0276bb/) 和 [Raft 算法](https://dunwu.github.io/waterdrop/pages/4907dc/)。Paxos 和 Raft 是为了实现分布式系统中高可用架构而提出的共识性算法,已经成为业界标准。 +然后,很多著名的分布式系统,都利用选举机制,来保证主节点宕机时的故障恢复。如果要深入理解选举机制,有必要了解:[Paxos 算法](https://dunwu.github.io/waterdrop/pages/ea903d16/) 和 [Raft 算法](https://dunwu.github.io/waterdrop/pages/9386474c/)。Paxos 和 Raft 是为了实现分布式系统中高可用架构而提出的共识性算法,已经成为业界标准。 CAP 定理又称为 CAP 原则,指的是:**在一个分布式系统中, `一致性(C:Consistency)`、`可用性(A:Availability)` 和 `分区容忍性(P:Partition Tolerance)`,最多只能同时满足其中两项**。 BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State)`** 和 **`最终一致性(Eventually Consistent)`** 三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 -> CAP 和 BASE 理论的详细说明请参考:[分布式一致性](https://dunwu.github.io/waterdrop/pages/dac0e2/) +> CAP 和 BASE 理论的详细说明请参考:[分布式一致性](https://dunwu.github.io/waterdrop/pages/1c5cc28c/) > -> Paxos 和 Raft 的详细说明请参考:[Paxos 算法](https://dunwu.github.io/waterdrop/pages/0276bb/) 和 [Raft 算法](https://dunwu.github.io/waterdrop/pages/4907dc/) +> Paxos 和 Raft 的详细说明请参考:[Paxos 算法](https://dunwu.github.io/waterdrop/pages/ea903d16/) 和 [Raft 算法](https://dunwu.github.io/waterdrop/pages/9386474c/) ## 架构模式 @@ -172,7 +172,7 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State 无状态的应用实现高可用架构十分简单,由于服务器不保存请求状态,那么所有服务器完全对等,在任意节点执行同样的请求,结果总是一致的。这种情况下,最简单的高可用方案就是使用负载均衡。 -> 负载均衡原理可以参考:[负载均衡基本原理](https://dunwu.github.io/waterdrop/pages/98a1c1/) +> 负载均衡原理可以参考:[负载均衡基本原理](https://dunwu.github.io/waterdrop/pages/bcf0fb8c/) ### 分布式 Session @@ -186,7 +186,7 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State - 应用服务器间的 session 复制共享 - 基于缓存的 session 共享 ✅ -> 分布式会话原理可以参考:[分布式会话基本原理](https://dunwu.github.io/waterdrop/pages/95e45f/) +> 分布式会话原理可以参考:[分布式会话基本原理](https://dunwu.github.io/waterdrop/pages/9f390e41/) ## 高可用的服务 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/05.\347\263\273\347\273\237\344\274\270\347\274\251\346\200\247\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/05.\347\263\273\347\273\237\344\274\270\347\274\251\346\200\247\346\236\266\346\236\204.md" index 496e546c77..312957d5e8 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/05.\347\263\273\347\273\237\344\274\270\347\274\251\346\200\247\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/05.\347\263\273\347\273\237\344\274\270\347\274\251\346\200\247\346\236\266\346\236\204.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 伸缩性 -permalink: /pages/1e5251/ +permalink: /pages/43727b77/ --- # 系统伸缩性架构 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/06.\347\263\273\347\273\237\346\211\251\345\261\225\346\200\247\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/06.\347\263\273\347\273\237\346\211\251\345\261\225\346\200\247\346\236\266\346\236\204.md" index d4c24e08bc..2971f7b9f2 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/06.\347\263\273\347\273\237\346\211\251\345\261\225\346\200\247\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/06.\347\263\273\347\273\237\346\211\251\345\261\225\346\200\247\346\236\266\346\236\204.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 扩展性 -permalink: /pages/943670/ +permalink: /pages/f3a87538/ --- # 系统扩展性架构 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/07.\347\263\273\347\273\237\345\256\211\345\205\250\346\200\247\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/07.\347\263\273\347\273\237\345\256\211\345\205\250\346\200\247\346\236\266\346\236\204.md" index 0bd1de17ff..240db04b25 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/07.\347\263\273\347\273\237\345\256\211\345\205\250\346\200\247\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/07.\347\263\273\347\273\237\345\256\211\345\205\250\346\200\247\346\236\266\346\236\204.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 安全 -permalink: /pages/a1adcf/ +permalink: /pages/90e6cebe/ --- # 系统安全性架构 @@ -769,7 +769,7 @@ MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统 应用场景:将用户密码以消息摘要形式保存到数据库中。 -> :point_right: 参考阅读: [Java 编码和加密](https://dunwu.github.io/waterdrop/pages/a249ff/) +> :point_right: 参考阅读: [Java 编码和加密](https://dunwu.github.io/waterdrop/pages/9c7d7935/) ### 加密算法 @@ -789,7 +789,7 @@ MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统 应用场景:HTTPS 传输中浏览器使用的数字证书实质上是经过权威机构认证的非对称加密公钥。 -> :point_right: 参考阅读: [Java 编码和加密](https://dunwu.github.io/waterdrop/pages/a249ff/) +> :point_right: 参考阅读: [Java 编码和加密](https://dunwu.github.io/waterdrop/pages/9c7d7935/) #### 密钥安全管理 @@ -908,4 +908,4 @@ SSL/TLS 协议的基本过程是这样的: - http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html - [CAS 实现 SSO 单点登录原理](http://www.coin163.com/java/cas/cas.html) - [权限系统设计模型分析(DAC,MAC,RBAC,ABAC)](https://www.jianshu.com/p/ce0944b4a903) -- [RBAC 模型:基于用户-角色-权限控制的一些思考](http://www.woshipm.com/pd/1150093.html) +- [RBAC 模型:基于用户-角色-权限控制的一些思考](http://www.woshipm.com/pd/1150093.html) \ No newline at end of file diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/08.\345\244\247\345\236\213\347\263\273\347\273\237\346\240\270\345\277\203\346\212\200\346\234\257.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/08.\345\244\247\345\236\213\347\263\273\347\273\237\346\240\270\345\277\203\346\212\200\346\234\257.md" index 0e4acf8dd7..531b195b2f 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/08.\345\244\247\345\236\213\347\263\273\347\273\237\346\240\270\345\277\203\346\212\200\346\234\257.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/08.\345\244\247\345\236\213\347\263\273\347\273\237\346\240\270\345\277\203\346\212\200\346\234\257.md" @@ -9,7 +9,7 @@ categories: tags: - 架构 - 分布式 -permalink: /pages/8cbae8/ +permalink: /pages/53415678/ --- # 大型系统核心技术 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/09.\347\263\273\347\273\237\346\265\213\350\257\225\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/09.\347\263\273\347\273\237\346\265\213\350\257\225\346\236\266\346\236\204.md" index a0ddefed06..1fa5e31f4c 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/09.\347\263\273\347\273\237\346\265\213\350\257\225\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/09.\347\263\273\347\273\237\346\265\213\350\257\225\346\236\266\346\236\204.md" @@ -10,7 +10,7 @@ tags: - 架构 - 设计 - 测试 -permalink: /pages/641e5c/ +permalink: /pages/a32bd53e/ --- # 系统测试架构 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/README.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/README.md" index 41b0bbff7b..c92de72cbf 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/00.\347\273\274\345\220\210/README.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 架构 -permalink: /pages/f3d238/ +permalink: /pages/ae754879/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/01.\345\276\256\346\234\215\345\212\241\347\256\200\344\273\213.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/01.\345\276\256\346\234\215\345\212\241\347\256\200\344\273\213.md" index 64e9316636..5945d294b8 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/01.\345\276\256\346\234\215\345\212\241\347\256\200\344\273\213.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/01.\345\276\256\346\234\215\345\212\241\347\256\200\344\273\213.md" @@ -11,7 +11,7 @@ tags: - 架构 - 微服务 - 分布式 -permalink: /pages/012075/ +permalink: /pages/70a8b0e2/ --- # 微服务简介 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/02.\345\276\256\346\234\215\345\212\241\344\271\213\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/02.\345\276\256\346\234\215\345\212\241\344\271\213\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" index 7e2ec5ebbf..7a0f41e6bd 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/02.\345\276\256\346\234\215\345\212\241\344\271\213\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/02.\345\276\256\346\234\215\345\212\241\344\271\213\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" @@ -12,7 +12,7 @@ tags: - 微服务 - 分布式 - 注册中心 -permalink: /pages/44b4c3/ +permalink: /pages/1233ca70/ --- # 微服务之注册和发现 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/03.\345\276\256\346\234\215\345\212\241\344\271\213\346\234\215\345\212\241\350\260\203\347\224\250.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/03.\345\276\256\346\234\215\345\212\241\344\271\213\346\234\215\345\212\241\350\260\203\347\224\250.md" index 1f7dd53d4b..ea2414e5c9 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/03.\345\276\256\346\234\215\345\212\241\344\271\213\346\234\215\345\212\241\350\260\203\347\224\250.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/03.\345\276\256\346\234\215\345\212\241\344\271\213\346\234\215\345\212\241\350\260\203\347\224\250.md" @@ -12,7 +12,7 @@ tags: - 微服务 - 分布式 - RPC -permalink: /pages/c1c7b2/ +permalink: /pages/c89c8052/ --- # 微服务之服务调用 @@ -92,7 +92,7 @@ TCP 通信的过程分为四个步骤:**服务器监听**、**客户端请求* ## 序列化 -> 有兴趣深入了解 JDK 序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) +> 有兴趣深入了解 JDK 序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/76ab164b/) 由于,网络传输的数据必须是二进制数据,而调用方请求的出参、入参都是对象。因此,必须将对象转换可传输的二进制,并且要求转换算法是可逆的。 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/10.\345\276\256\346\234\215\345\212\241\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/10.\345\276\256\346\234\215\345\212\241\345\237\272\346\234\254\345\216\237\347\220\206.md" index 881571a41b..857fa88ec4 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/10.\345\276\256\346\234\215\345\212\241\345\237\272\346\234\254\345\216\237\347\220\206.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/10.\345\276\256\346\234\215\345\212\241\345\237\272\346\234\254\345\216\237\347\220\206.md" @@ -11,7 +11,7 @@ tags: - 架构 - 微服务 - 分布式 -permalink: /pages/aa7497/ +permalink: /pages/214c6d9d/ --- # 微服务基本原理 @@ -50,7 +50,7 @@ permalink: /pages/aa7497/ - 跨语言支持 - 性能 -> 👉 参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) +> 👉 参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/76ab164b/) ### 通信协议 @@ -166,7 +166,7 @@ API 网关方式的核心要点是,所有的客户端和消费端都通过统 ## 负载均衡 -> 参考:[负载均衡基本原理](https://dunwu.github.io/waterdrop/pages/98a1c1/) +> 参考:[负载均衡基本原理](https://dunwu.github.io/waterdrop/pages/bcf0fb8c/) ## 服务路由 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/README.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/README.md" index 45a1509214..c4602f5075 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/01.\345\276\256\346\234\215\345\212\241/README.md" @@ -10,7 +10,7 @@ tags: - 架构 - 微服务 - 分布式 -permalink: /pages/559360/ +permalink: /pages/76c00a1f/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/01.\347\273\274\350\277\260.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/01.\347\273\274\350\277\260.md" index 78be3bc08d..9e3a8801d0 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/01.\347\273\274\350\277\260.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/01.\347\273\274\350\277\260.md" @@ -11,7 +11,7 @@ tags: - 安全 - 认证 - 授权 -permalink: /pages/7ac4c5/ +permalink: /pages/3298ba00/ --- # 权限认证综述 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/02.\350\256\244\350\257\201.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/02.\350\256\244\350\257\201.md" index f430c4daf8..e117a0e1de 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/02.\350\256\244\350\257\201.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/02.\350\256\244\350\257\201.md" @@ -10,7 +10,7 @@ tags: - 架构 - 安全 - 认证 -permalink: /pages/6236e0/ +permalink: /pages/6b3a7519/ --- # 认证设计 @@ -47,8 +47,8 @@ Cookie 实际上是存储在客户端上的文本信息,并保留了各种跟 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。如果说 Cookie 机制是通过检查客户身上的“通行证”来确定客户身份的话,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份。 -- [Cookie 和 Session](https://dunwu.github.io/waterdrop/pages/c46bff/) -- [分布式会话](https://dunwu.github.io/waterdrop/pages/95e45f/) +- [Cookie 和 Session](https://dunwu.github.io/waterdrop/pages/b535e4c0/) +- [分布式会话](https://dunwu.github.io/waterdrop/pages/9f390e41/) ## 单点登录 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/03.\346\216\210\346\235\203.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/03.\346\216\210\346\235\203.md" index a634e8f5a7..632ccbab5e 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/03.\346\216\210\346\235\203.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/03.\346\216\210\346\235\203.md" @@ -10,7 +10,7 @@ tags: - 架构 - 安全 - 授权 -permalink: /pages/05473f/ +permalink: /pages/6aab33f7/ --- # 授权设计 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/05.\345\256\211\345\205\250\346\274\217\346\264\236.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/05.\345\256\211\345\205\250\346\274\217\346\264\236.md" index eb1a57775a..0536602a83 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/05.\345\256\211\345\205\250\346\274\217\346\264\236.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/05.\345\256\211\345\205\250\346\274\217\346\264\236.md" @@ -10,7 +10,7 @@ tags: - 架构 - 安全 - 漏洞 -permalink: /pages/3633eb/ +permalink: /pages/71e0bc17/ --- # 安全漏洞防护 @@ -188,4 +188,4 @@ MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统 > :point_right: 参考阅读: > -> - [拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A) +> - [拒绝服务攻击](https://zh.wikipedia.org/wiki/%E9%98%BB%E6%96%B7%E6%9C%8D%E5%8B%99%E6%94%BB%E6%93%8A) \ No newline at end of file diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/06.\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/06.\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" index 34708eee2a..19b8b3fd19 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/06.\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/06.\347\274\226\347\240\201\345\222\214\345\212\240\345\257\206.md" @@ -20,7 +20,7 @@ tags: - AES - DES - RSA -permalink: /pages/a4db83/ +permalink: /pages/7684cf16/ --- # 编码和加密 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/README.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/README.md" index f9fbb6898a..9e87e0eef1 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/02.\345\256\211\345\205\250/README.md" @@ -9,7 +9,7 @@ tags: - 设计 - 架构 - 安全 -permalink: /pages/056621/ +permalink: /pages/18c5e27a/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/Cinchcast\347\232\204\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/Cinchcast\347\232\204\346\236\266\346\236\204.md" index 47878c9668..c764381b5e 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/Cinchcast\347\232\204\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/Cinchcast\347\232\204\346\236\266\346\236\204.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/3f25aa/ +permalink: /pages/c0063bdf/ --- # Cinchcast 的架构 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/README.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/README.md" index cdf364e636..77c947bac9 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/README.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/c38eff/ +permalink: /pages/efd318eb/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\272\232\351\251\254\351\200\212\347\232\204\346\236\266\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\272\232\351\251\254\351\200\212\347\232\204\346\236\266\346\236\204.md" index 332f5f8c8d..de21a0a5d7 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\272\232\351\251\254\351\200\212\347\232\204\346\236\266\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\272\232\351\251\254\351\200\212\347\232\204\346\236\266\346\236\204.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/940342/ +permalink: /pages/d3934133/ --- # 亚马逊的架构 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\275\216\344\273\243\347\240\201\345\271\263\345\217\260.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\275\216\344\273\243\347\240\201\345\271\263\345\217\260.md" index b2ae309e5b..191f4d3c84 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\275\216\344\273\243\347\240\201\345\271\263\345\217\260.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\344\275\216\344\273\243\347\240\201\345\271\263\345\217\260.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/f90553/ +permalink: /pages/3a4a05d9/ --- # 设计一个低代码平台 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\346\265\267\351\207\217\346\225\260\346\215\256\345\244\204\347\220\206.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\346\265\267\351\207\217\346\225\260\346\215\256\345\244\204\347\220\206.md" index 51ffac1db4..561934da8b 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\346\265\267\351\207\217\346\225\260\346\215\256\345\244\204\347\220\206.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\346\265\267\351\207\217\346\225\260\346\215\256\345\244\204\347\220\206.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/d63886/ +permalink: /pages/dfa3503c/ --- # 海量数据处理 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\224\265\345\225\206.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\224\265\345\225\206.md" index 363dfdbb8a..a1a4efd15d 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\224\265\345\225\206.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\224\265\345\225\206.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/4ae6a4/ +permalink: /pages/5b7e99de/ --- # 电商 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\237\255\345\234\260\345\235\200\346\234\215\345\212\241.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\237\255\345\234\260\345\235\200\346\234\215\345\212\241.md" index c0af454d85..44829f742b 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\237\255\345\234\260\345\235\200\346\234\215\345\212\241.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\237\255\345\234\260\345\235\200\346\234\215\345\212\241.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/c72587/ +permalink: /pages/d56f7960/ --- # 设计 Pastebin.com (或者 Bit.ly) diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\247\222\346\235\200\347\263\273\347\273\237\350\256\276\350\256\241.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\247\222\346\235\200\347\263\273\347\273\237\350\256\276\350\256\241.md" index 0dfda59fb5..5b1c2e22ff 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\247\222\346\235\200\347\263\273\347\273\237\350\256\276\350\256\241.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/99.\350\247\243\345\206\263\346\226\271\346\241\210/\347\247\222\346\235\200\347\263\273\347\273\237\350\256\276\350\256\241.md" @@ -8,7 +8,7 @@ categories: tags: - 架构 - 解决方案 -permalink: /pages/a963f0/ +permalink: /pages/0e6f048d/ --- # 秒杀系统设计 diff --git "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/README.md" "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/README.md" index 16373dd19c..4d89384654 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/01.\346\236\266\346\236\204/README.md" @@ -6,7 +6,7 @@ categories: - 架构 tags: - 架构 -permalink: /pages/d9e5d2/ +permalink: /pages/bad4b2c4/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/00.\350\256\276\350\256\241\346\250\241\345\274\217\346\246\202\350\277\260.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/00.\350\256\276\350\256\241\346\250\241\345\274\217\346\246\202\350\277\260.md" index 7fd0e9b752..d57026b9e5 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/00.\350\256\276\350\256\241\346\250\241\345\274\217\346\246\202\350\277\260.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/00.\350\256\276\350\256\241\346\250\241\345\274\217\346\246\202\350\277\260.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/9a2452/ +permalink: /pages/5bc54df9/ --- # 设计模式概述 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/01.\347\256\200\345\215\225\345\267\245\345\216\202\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/01.\347\256\200\345\215\225\345\267\245\345\216\202\346\250\241\345\274\217.md" index 3d164420c8..cb812d1225 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/01.\347\256\200\345\215\225\345\267\245\345\216\202\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/01.\347\256\200\345\215\225\345\267\245\345\216\202\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/ff930b/ +permalink: /pages/b27e3e42/ --- # 设计模式之简单工厂模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/02.\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/02.\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" index a5008e3d70..6ba3372117 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/02.\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/02.\345\267\245\345\216\202\346\226\271\346\263\225\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/65724c/ +permalink: /pages/615dc76d/ --- # 设计模式之工厂方法模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/03.\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/03.\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217.md" index db4f5e2f69..b5427abd40 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/03.\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/03.\346\212\275\350\261\241\345\267\245\345\216\202\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/340aa0/ +permalink: /pages/5b12dab7/ --- # 设计模式之抽象工厂模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/04.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/04.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" index b4c7e7d8b4..9b5d19c7db 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/04.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/04.\345\273\272\351\200\240\350\200\205\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/bf03f3/ +permalink: /pages/bc487370/ --- # 设计模式之建造者模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/05.\345\216\237\345\236\213\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/05.\345\216\237\345\236\213\346\250\241\345\274\217.md" index 80701866b2..19edbfd7b9 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/05.\345\216\237\345\236\213\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/05.\345\216\237\345\236\213\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/1af8ee/ +permalink: /pages/1204b863/ --- # 设计模式之原型模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/06.\345\215\225\344\276\213\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/06.\345\215\225\344\276\213\346\250\241\345\274\217.md" index 2b4a7fefa2..f28d32384d 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/06.\345\215\225\344\276\213\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/06.\345\215\225\344\276\213\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/cf046f/ +permalink: /pages/1a7f3b3a/ --- # 设计模式之单例模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/07.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/07.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" index 223b80b90c..db1e4a0f2b 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/07.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/07.\351\200\202\351\205\215\345\231\250\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/2115cf/ +permalink: /pages/b1816a12/ --- # 设计模式之适配器模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/08.\346\241\245\346\216\245\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/08.\346\241\245\346\216\245\346\250\241\345\274\217.md" index 7f46cbee0e..8ff5b6a6c7 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/08.\346\241\245\346\216\245\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/08.\346\241\245\346\216\245\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/b05f5f/ +permalink: /pages/ec8559df/ --- # 设计模式之桥接模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/09.\347\273\204\345\220\210\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/09.\347\273\204\345\220\210\346\250\241\345\274\217.md" index 05d6e33065..95013ab8dd 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/09.\347\273\204\345\220\210\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/09.\347\273\204\345\220\210\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/85c0a3/ +permalink: /pages/7ccde2a4/ --- # 设计模式之组合模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/10.\350\243\205\351\245\260\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/10.\350\243\205\351\245\260\346\250\241\345\274\217.md" index e4d9022f0c..162a81600c 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/10.\350\243\205\351\245\260\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/10.\350\243\205\351\245\260\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/2e24a8/ +permalink: /pages/c6884edf/ --- # 设计模式之装饰模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/11.\345\244\226\350\247\202\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/11.\345\244\226\350\247\202\346\250\241\345\274\217.md" index 14a49defe3..753596a6c4 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/11.\345\244\226\350\247\202\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/11.\345\244\226\350\247\202\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/ea331b/ +permalink: /pages/9a395a72/ --- # 设计模式之外观模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/12.\344\272\253\345\205\203\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/12.\344\272\253\345\205\203\346\250\241\345\274\217.md" index 648f1579b6..1f8099464c 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/12.\344\272\253\345\205\203\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/12.\344\272\253\345\205\203\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/9147e7/ +permalink: /pages/5882258a/ --- # 设计模式之享元模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/13.\344\273\243\347\220\206\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/13.\344\273\243\347\220\206\346\250\241\345\274\217.md" index f9c5b51401..1dcbfe607f 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/13.\344\273\243\347\220\206\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/13.\344\273\243\347\220\206\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/5a865c/ +permalink: /pages/77da042a/ --- # 设计模式之代理模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/14.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/14.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" index 191ae0f4d6..23d6d950ec 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/14.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/14.\346\250\241\346\235\277\346\226\271\346\263\225\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/6eaeb4/ +permalink: /pages/e0f9d2ec/ --- # 设计模式之模板方法模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/15.\345\221\275\344\273\244\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/15.\345\221\275\344\273\244\346\250\241\345\274\217.md" index fb373942c3..4f2a259650 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/15.\345\221\275\344\273\244\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/15.\345\221\275\344\273\244\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/22353c/ +permalink: /pages/25f68410/ --- # 设计模式之命令模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/16.\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/16.\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" index 5af30b2cc1..ae8119ed44 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/16.\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/16.\350\277\255\344\273\243\345\231\250\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/09d5af/ +permalink: /pages/69095ee8/ --- # 设计模式之迭代器模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/17.\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/17.\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" index 39742d74e4..12e72b1aeb 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/17.\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/17.\350\247\202\345\257\237\350\200\205\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/056e1d/ +permalink: /pages/6268bcb4/ --- # 设计模式之观察者模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/18.\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/18.\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217.md" index 4820713836..86cafba3c5 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/18.\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/18.\350\247\243\351\207\212\345\231\250\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/48e5aa/ +permalink: /pages/707ae8fa/ --- # 设计模式之解释器模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/19.\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/19.\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" index 76d4d270c3..ac90966ec9 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/19.\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/19.\344\270\255\344\273\213\350\200\205\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/3b1f47/ +permalink: /pages/b1e11f2f/ --- # 设计模式之中介者模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/20.\350\201\214\350\264\243\351\223\276\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/20.\350\201\214\350\264\243\351\223\276\346\250\241\345\274\217.md" index 44677d3ec1..dc16003058 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/20.\350\201\214\350\264\243\351\223\276\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/20.\350\201\214\350\264\243\351\223\276\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/b25735/ +permalink: /pages/8fa92451/ --- # 设计模式之职责链模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/21.\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/21.\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" index 0f5433221a..3cf9b1d70e 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/21.\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/21.\345\244\207\345\277\230\345\275\225\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/5ae0d5/ +permalink: /pages/d757bde0/ --- # 设计模式之备忘录模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/22.\347\255\226\347\225\245\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/22.\347\255\226\347\225\245\346\250\241\345\274\217.md" index ca0817d2e5..985df5fa94 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/22.\347\255\226\347\225\245\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/22.\347\255\226\347\225\245\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/dc8ecd/ +permalink: /pages/45f74367/ --- # 设计模式之策略模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/23.\350\256\277\351\227\256\350\200\205\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/23.\350\256\277\351\227\256\350\200\205\346\250\241\345\274\217.md" index db02cbbb7c..458a39c37d 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/23.\350\256\277\351\227\256\350\200\205\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/23.\350\256\277\351\227\256\350\200\205\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/671352/ +permalink: /pages/6e042c1b/ --- # 设计模式之访问者模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/24.\347\212\266\346\200\201\346\250\241\345\274\217.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/24.\347\212\266\346\200\201\346\250\241\345\274\217.md" index 25f8e108d8..99709fd510 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/24.\347\212\266\346\200\201\346\250\241\345\274\217.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/24.\347\212\266\346\200\201\346\250\241\345\274\217.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/d77095/ +permalink: /pages/051b3740/ --- # 设计模式之状态模式 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/25.\351\235\242\345\220\221\345\257\271\350\261\241\345\216\237\345\210\231.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/25.\351\235\242\345\220\221\345\257\271\350\261\241\345\216\237\345\210\231.md" index 25a6712726..b311edd7d3 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/25.\351\235\242\345\220\221\345\257\271\350\261\241\345\216\237\345\210\231.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/25.\351\235\242\345\220\221\345\257\271\350\261\241\345\216\237\345\210\231.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/9703b1/ +permalink: /pages/3610064d/ --- # 面向对象设计六大原则 diff --git "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/README.md" "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/README.md" index ea0f06ef5e..42c77c03e7 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/02.\350\256\276\350\256\241\346\250\241\345\274\217/README.md" @@ -7,7 +7,7 @@ categories: tags: - 设计 - 设计模式 -permalink: /pages/81b0f2/ +permalink: /pages/fe8df8f0/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/01.\344\273\243\347\240\201\347\232\204\345\235\217\345\221\263\351\201\223\345\222\214\351\207\215\346\236\204.md" "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/01.\344\273\243\347\240\201\347\232\204\345\235\217\345\221\263\351\201\223\345\222\214\351\207\215\346\236\204.md" index 44a37c0bf9..3f4be4635f 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/01.\344\273\243\347\240\201\347\232\204\345\235\217\345\221\263\351\201\223\345\222\214\351\207\215\346\236\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/01.\344\273\243\347\240\201\347\232\204\345\235\217\345\221\263\351\201\223\345\222\214\351\207\215\346\236\204.md" @@ -9,7 +9,7 @@ tags: - 设计 - 重构 - 代码的坏味道 -permalink: /pages/d86872/ +permalink: /pages/b14a7acd/ --- 第一次读《重构:改善既有代码的设计》时,我曾整理过一个简单的笔记。最近,因为参与一个重构项目,再一次温习了《重构:改善既有代码的设计》。过程中,萌发了认真总结、整理重构方法的冲动,于是有了这系列文字。 diff --git "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/02.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\344\273\243\347\240\201\350\207\203\350\202\277.md" "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/02.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\344\273\243\347\240\201\350\207\203\350\202\277.md" index 50ae55dab2..2f72591a31 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/02.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\344\273\243\347\240\201\350\207\203\350\202\277.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/02.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\344\273\243\347\240\201\350\207\203\350\202\277.md" @@ -9,7 +9,7 @@ tags: - 设计 - 重构 - 代码的坏味道 -permalink: /pages/49d5ae/ +permalink: /pages/7804288f/ --- > 翻译自:https://sourcemaking.com/refactoring/smells/bloaters diff --git "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/03.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\346\273\245\347\224\250\351\235\242\345\220\221\345\257\271\350\261\241.md" "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/03.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\346\273\245\347\224\250\351\235\242\345\220\221\345\257\271\350\261\241.md" index 2bcfe63b35..159bb482d1 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/03.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\346\273\245\347\224\250\351\235\242\345\220\221\345\257\271\350\261\241.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/03.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\346\273\245\347\224\250\351\235\242\345\220\221\345\257\271\350\261\241.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - 重构 -permalink: /pages/65ee05/ +permalink: /pages/3ba0474c/ --- > 翻译自:https://sourcemaking.com/refactoring/smells/oo-abusers diff --git "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/04.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\345\217\230\351\235\251\347\232\204\351\232\234\347\242\215.md" "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/04.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\345\217\230\351\235\251\347\232\204\351\232\234\347\242\215.md" index 3f42411d65..98f21d4fcb 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/04.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\345\217\230\351\235\251\347\232\204\351\232\234\347\242\215.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/04.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\345\217\230\351\235\251\347\232\204\351\232\234\347\242\215.md" @@ -9,7 +9,7 @@ tags: - 设计 - 重构 - 代码的坏味道 -permalink: /pages/56ca63/ +permalink: /pages/69705a6c/ --- > 翻译自:https://sourcemaking.com/refactoring/smells/change-preventers diff --git "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/05.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\351\235\236\345\277\205\350\246\201\347\232\204.md" "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/05.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\351\235\236\345\277\205\350\246\201\347\232\204.md" index 35724f1751..d1b3041415 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/05.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\351\235\236\345\277\205\350\246\201\347\232\204.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/05.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\351\235\236\345\277\205\350\246\201\347\232\204.md" @@ -9,7 +9,7 @@ tags: - 设计 - 重构 - 代码的坏味道 -permalink: /pages/47acb5/ +permalink: /pages/af6a80d7/ --- > 翻译自:https://sourcemaking.com/refactoring/smells/dispensables diff --git "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/06.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\350\200\246\345\220\210.md" "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/06.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\350\200\246\345\220\210.md" index d846c8bd25..44707528ed 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/06.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\350\200\246\345\220\210.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/06.\344\273\243\347\240\201\345\235\217\345\221\263\351\201\223\344\271\213\350\200\246\345\220\210.md" @@ -9,7 +9,7 @@ tags: - 设计 - 重构 - 代码的坏味道 -permalink: /pages/630e7a/ +permalink: /pages/fffe5913/ --- > 翻译自:https://sourcemaking.com/refactoring/smells/couplers diff --git "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/README.md" "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/README.md" index fb07281a76..7b090f2a49 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/03.\351\207\215\346\236\204/README.md" @@ -7,7 +7,7 @@ categories: tags: - 设计 - 重构 -permalink: /pages/d200c3/ +permalink: /pages/7f65f5c2/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/04.DDD/01.\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\256\200\344\273\213.md" "b/source/_posts/03.\350\256\276\350\256\241/04.DDD/01.\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\256\200\344\273\213.md" index 8372f826bf..792853b837 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/04.DDD/01.\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\256\200\344\273\213.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/04.DDD/01.\351\242\206\345\237\237\351\251\261\345\212\250\350\256\276\350\256\241\347\256\200\344\273\213.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - DDD -permalink: /pages/86db92/ +permalink: /pages/04e4f5dd/ --- # 领域驱动设计简介 diff --git "a/source/_posts/03.\350\256\276\350\256\241/04.DDD/README.md" "b/source/_posts/03.\350\256\276\350\256\241/04.DDD/README.md" index 8a8dba0537..6233f23845 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/04.DDD/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/04.DDD/README.md" @@ -7,7 +7,7 @@ categories: tags: - 设计 - DDD -permalink: /pages/833925/ +permalink: /pages/53db812a/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/05.UML/01.UML\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/03.\350\256\276\350\256\241/05.UML/01.UML\345\277\253\351\200\237\345\205\245\351\227\250.md" index 57692eb8b5..22482a531a 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/05.UML/01.UML\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/05.UML/01.UML\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - UML -permalink: /pages/ae1396/ +permalink: /pages/88d3215a/ --- # UML 快速入门 diff --git "a/source/_posts/03.\350\256\276\350\256\241/05.UML/02.UML\347\273\223\346\236\204\345\273\272\346\250\241\345\233\276.md" "b/source/_posts/03.\350\256\276\350\256\241/05.UML/02.UML\347\273\223\346\236\204\345\273\272\346\250\241\345\233\276.md" index c77daea0b8..e885b70701 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/05.UML/02.UML\347\273\223\346\236\204\345\273\272\346\250\241\345\233\276.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/05.UML/02.UML\347\273\223\346\236\204\345\273\272\346\250\241\345\233\276.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - UML -permalink: /pages/dd5922/ +permalink: /pages/1a7d64e7/ --- # UML 结构建模图 diff --git "a/source/_posts/03.\350\256\276\350\256\241/05.UML/03.UML\350\241\214\344\270\272\345\273\272\346\250\241\345\233\276.md" "b/source/_posts/03.\350\256\276\350\256\241/05.UML/03.UML\350\241\214\344\270\272\345\273\272\346\250\241\345\233\276.md" index 055cf7dba1..4042f6689b 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/05.UML/03.UML\350\241\214\344\270\272\345\273\272\346\250\241\345\233\276.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/05.UML/03.UML\350\241\214\344\270\272\345\273\272\346\250\241\345\233\276.md" @@ -8,7 +8,7 @@ categories: tags: - 设计 - UML -permalink: /pages/0b8e4b/ +permalink: /pages/bdc54514/ --- # UML 行为建模图 diff --git "a/source/_posts/03.\350\256\276\350\256\241/05.UML/README.md" "b/source/_posts/03.\350\256\276\350\256\241/05.UML/README.md" index 4b260cffdf..fc75c38163 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/05.UML/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/05.UML/README.md" @@ -7,7 +7,7 @@ categories: tags: - 设计 - UML -permalink: /pages/13ccb0/ +permalink: /pages/845dbfe4/ hidden: true index: false --- diff --git "a/source/_posts/03.\350\256\276\350\256\241/README.md" "b/source/_posts/03.\350\256\276\350\256\241/README.md" index 3bd1d954c3..5e1fa86657 100644 --- "a/source/_posts/03.\350\256\276\350\256\241/README.md" +++ "b/source/_posts/03.\350\256\276\350\256\241/README.md" @@ -5,7 +5,7 @@ categories: - 设计 tags: - 编程 -permalink: /pages/8ea43c/ +permalink: /pages/71f30cda/ hidden: true index: false --- diff --git "a/source/_posts/04.DevOps/00.\347\273\274\345\220\210/01.DevOps\347\256\200\344\273\213.md" "b/source/_posts/04.DevOps/00.\347\273\274\345\220\210/01.DevOps\347\256\200\344\273\213.md" index 1b40fcb87a..cf66181810 100644 --- "a/source/_posts/04.DevOps/00.\347\273\274\345\220\210/01.DevOps\347\256\200\344\273\213.md" +++ "b/source/_posts/04.DevOps/00.\347\273\274\345\220\210/01.DevOps\347\256\200\344\273\213.md" @@ -7,7 +7,7 @@ categories: - 综合 tags: - DevOps -permalink: /pages/b09613/ +permalink: /pages/3a9971fd/ --- # DevOps 简介 diff --git "a/source/_posts/04.DevOps/03.\347\233\221\346\216\247/01.\347\233\221\346\216\247\344\275\223\347\263\273.md" "b/source/_posts/04.DevOps/03.\347\233\221\346\216\247/01.\347\233\221\346\216\247\344\275\223\347\263\273.md" index 5d784e66fa..dd1d5bb82f 100644 --- "a/source/_posts/04.DevOps/03.\347\233\221\346\216\247/01.\347\233\221\346\216\247\344\275\223\347\263\273.md" +++ "b/source/_posts/04.DevOps/03.\347\233\221\346\216\247/01.\347\233\221\346\216\247\344\275\223\347\263\273.md" @@ -8,7 +8,7 @@ categories: tags: - DevOps - 监控 -permalink: /pages/e593a4/ +permalink: /pages/783bf95a/ --- # 如何建设监控体系 diff --git "a/source/_posts/04.DevOps/03.\347\233\221\346\216\247/02.\351\223\276\350\267\257\350\277\275\350\270\252.md" "b/source/_posts/04.DevOps/03.\347\233\221\346\216\247/02.\351\223\276\350\267\257\350\277\275\350\270\252.md" index ac615f1f4e..92daf5a478 100644 --- "a/source/_posts/04.DevOps/03.\347\233\221\346\216\247/02.\351\223\276\350\267\257\350\277\275\350\270\252.md" +++ "b/source/_posts/04.DevOps/03.\347\233\221\346\216\247/02.\351\223\276\350\267\257\350\277\275\350\270\252.md" @@ -10,7 +10,7 @@ tags: - 监控 - APM - 链路追踪 -permalink: /pages/b46249/ +permalink: /pages/3da6a371/ --- # 链路追踪 diff --git "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/01.\345\246\202\344\275\225\344\274\230\351\233\205\347\232\204\347\216\251\350\275\254Git.md" "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/01.\345\246\202\344\275\225\344\274\230\351\233\205\347\232\204\347\216\251\350\275\254Git.md" index ebb09c4eff..149ca93ed8 100644 --- "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/01.\345\246\202\344\275\225\344\274\230\351\233\205\347\232\204\347\216\251\350\275\254Git.md" +++ "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/01.\345\246\202\344\275\225\344\274\230\351\233\205\347\232\204\347\216\251\350\275\254Git.md" @@ -9,7 +9,7 @@ categories: tags: - DevOps - Git -permalink: /pages/2fc8b1/ +permalink: /pages/88c215b7/ --- # 如何优雅的玩转 Git diff --git "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/02.Git\345\270\256\345\212\251\346\211\213\345\206\214.md" "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/02.Git\345\270\256\345\212\251\346\211\213\345\206\214.md" index 5b407e753f..d813230478 100644 --- "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/02.Git\345\270\256\345\212\251\346\211\213\345\206\214.md" +++ "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/02.Git\345\270\256\345\212\251\346\211\213\345\206\214.md" @@ -9,7 +9,7 @@ categories: tags: - DevOps - Git -permalink: /pages/09397d/ +permalink: /pages/b97c8100/ --- # Git 帮助手册 diff --git "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/README.md" "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/README.md" index a9d7f3dd1d..f01c6a6285 100644 --- "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/README.md" +++ "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/01.Git/README.md" @@ -8,7 +8,7 @@ categories: tags: - DevOps - Git -permalink: /pages/d107ad/ +permalink: /pages/7f7a45cf/ hidden: true index: false --- diff --git "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" index a0caae6299..f27c1a2527 100644 --- "a/source/_posts/04.DevOps/99.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" +++ "b/source/_posts/04.DevOps/99.\345\267\245\345\205\267/99.\345\205\266\344\273\226/01.\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217.md" @@ -7,8 +7,9 @@ categories: - 工具 - 其他 tags: - - null -permalink: /pages/0ba465/ + - 工具 + - 正则 +permalink: /pages/620ec459/ --- # 正则表达式极简教程 diff --git a/source/_posts/04.DevOps/README.md b/source/_posts/04.DevOps/README.md index 404265574d..3b41e92c8d 100644 --- a/source/_posts/04.DevOps/README.md +++ b/source/_posts/04.DevOps/README.md @@ -5,7 +5,7 @@ categories: - DevOps tags: - DevOps -permalink: /pages/1883b8/ +permalink: /pages/3559bef4/ hidden: true index: false --- diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/01.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\346\214\207\345\215\227.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/01.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\346\214\207\345\215\227.md" index 40802bb740..c4ac2ae135 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/01.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\346\214\207\345\215\227.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/01.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225\346\214\207\345\215\227.md" @@ -8,7 +8,7 @@ categories: tags: - 数据结构 - 算法 -permalink: /pages/241e98/ +permalink: /pages/6ac253d5/ --- # 数据结构和算法指南 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/02.\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/02.\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" index de87d309a5..a9239e92b4 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/02.\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/00.\347\273\274\345\220\210/02.\345\244\215\346\235\202\345\272\246\345\210\206\346\236\220.md" @@ -8,7 +8,7 @@ categories: tags: - 数据结构 - 算法 -permalink: /pages/cba821/ +permalink: /pages/69ee68d5/ --- # 复杂度分析 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/01.\346\225\260\347\273\204\345\222\214\351\223\276\350\241\250.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/01.\346\225\260\347\273\204\345\222\214\351\223\276\350\241\250.md" index 9d1d22889b..1437322de6 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/01.\346\225\260\347\273\204\345\222\214\351\223\276\350\241\250.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/01.\346\225\260\347\273\204\345\222\214\351\223\276\350\241\250.md" @@ -10,7 +10,7 @@ tags: - 线性表 - 数组 - 链表 -permalink: /pages/6c31ed/ +permalink: /pages/5e38c911/ --- # 数组和链表 @@ -437,4 +437,4 @@ public DListNode find(E value) { - [数据结构(C 语言版)](https://item.jd.com/12407475.html) - [数据结构(C++语言版)](https://book.douban.com/subject/25859528/) - [Leetcode:数组和字符串](https://leetcode-cn.com/leetbook/detail/array-and-string/) -- [Leetcode:链表](https://leetcode-cn.com/tag/linked-list/) +- [Leetcode:链表](https://leetcode-cn.com/tag/linked-list/) \ No newline at end of file diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/02.\346\240\210\345\222\214\351\230\237\345\210\227.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/02.\346\240\210\345\222\214\351\230\237\345\210\227.md" index ee3ec7b672..7279edc142 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/02.\346\240\210\345\222\214\351\230\237\345\210\227.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/02.\346\240\210\345\222\214\351\230\237\345\210\227.md" @@ -10,7 +10,7 @@ tags: - 线性表 - 栈 - 队列 -permalink: /pages/dd3588/ +permalink: /pages/ad285d47/ --- # 栈和队列 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/11.\347\272\277\346\200\247\350\241\250\347\232\204\346\237\245\346\211\276.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/11.\347\272\277\346\200\247\350\241\250\347\232\204\346\237\245\346\211\276.md" index 91aecffee1..ac6c5c3ce7 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/11.\347\272\277\346\200\247\350\241\250\347\232\204\346\237\245\346\211\276.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/11.\347\272\277\346\200\247\350\241\250\347\232\204\346\237\245\346\211\276.md" @@ -9,7 +9,7 @@ tags: - 数据结构 - 线性表 - 查找 -permalink: /pages/b14afb/ +permalink: /pages/55029754/ --- # 线性表的查找 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/12.\347\272\277\346\200\247\350\241\250\347\232\204\346\216\222\345\272\217.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/12.\347\272\277\346\200\247\350\241\250\347\232\204\346\216\222\345\272\217.md" index 493ded5741..9689579084 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/12.\347\272\277\346\200\247\350\241\250\347\232\204\346\216\222\345\272\217.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/01.\347\272\277\346\200\247\350\241\250/12.\347\272\277\346\200\247\350\241\250\347\232\204\346\216\222\345\272\217.md" @@ -9,7 +9,7 @@ tags: - 数据结构 - 线性表 - 排序 -permalink: /pages/3bac06/ +permalink: /pages/9204f157/ --- # 线性表的排序 @@ -890,4 +890,4 @@ public int[] sort(int[] list) { [我的 Github 测试例](https://github.com/dunwu/algorithm-tutorial/blob/master/codes/algorithm/src/test/java/io/github/dunwu/algorithm/sort/SortStrategyTest.java) -样本包含:数组个数为奇数、偶数的情况;元素重复或不重复的情况。且样本均为随机样本,实测有效。 +样本包含:数组个数为奇数、偶数的情况;元素重复或不重复的情况。且样本均为随机样本,实测有效。 \ No newline at end of file diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/01.\346\240\221\345\222\214\344\272\214\345\217\211\346\240\221.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/01.\346\240\221\345\222\214\344\272\214\345\217\211\346\240\221.md" index 251f18a490..6130907d16 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/01.\346\240\221\345\222\214\344\272\214\345\217\211\346\240\221.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/01.\346\240\221\345\222\214\344\272\214\345\217\211\346\240\221.md" @@ -10,7 +10,7 @@ tags: - 树 - 二叉树 - 完全二叉树 -permalink: /pages/133326/ +permalink: /pages/25822f3b/ --- # 树和二叉树 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/02.\345\240\206.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/02.\345\240\206.md" index 49963229b3..b438ab8bd5 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/02.\345\240\206.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/02.\345\240\206.md" @@ -10,7 +10,7 @@ tags: - 树 - 二叉树 - 堆 -permalink: /pages/99ac45/ +permalink: /pages/120cb51f/ --- # 堆 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/03.B+\346\240\221.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/03.B+\346\240\221.md" index eca2e1c406..fcbaff3433 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/03.B+\346\240\221.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/03.B+\346\240\221.md" @@ -10,7 +10,7 @@ tags: - 树 - 二叉树 - B+ 树 -permalink: /pages/2ba2ac/ +permalink: /pages/e9e0fd6b/ --- # B+树 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/04.LSM\346\240\221.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/04.LSM\346\240\221.md" index b8593b1d50..f517442d50 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/04.LSM\346\240\221.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/04.LSM\346\240\221.md" @@ -9,7 +9,7 @@ tags: - 数据结构 - 树 - LSM 树 -permalink: /pages/899690/ +permalink: /pages/65965411/ --- # LSM 树 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/05.\345\255\227\345\205\270\346\240\221.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/05.\345\255\227\345\205\270\346\240\221.md" index 656ce7e80d..519ce97f3a 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/05.\345\255\227\345\205\270\346\240\221.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/05.\345\255\227\345\205\270\346\240\221.md" @@ -9,7 +9,7 @@ tags: - 数据结构 - 树 - 字典树 -permalink: /pages/eec931/ +permalink: /pages/3e77e25c/ --- # 字典树 @@ -95,4 +95,4 @@ Trie 树可通过剪枝搜索空间来高效解决 Boggle 单词游戏 ## 参考资料 - [数据结构与算法之美](https://time.geekbang.org/column/intro/100017301) -- https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/shi-xian-trie-qian-zhui-shu-by-leetcode/ +- https://leetcode-cn.com/problems/implement-trie-prefix-tree/solution/shi-xian-trie-qian-zhui-shu-by-leetcode/ \ No newline at end of file diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/06.\347\272\242\351\273\221\346\240\221.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/06.\347\272\242\351\273\221\346\240\221.md" index 7d6477fa09..c2716a9250 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/06.\347\272\242\351\273\221\346\240\221.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/02.\346\240\221/06.\347\272\242\351\273\221\346\240\221.md" @@ -10,7 +10,7 @@ tags: - 树 - 二叉树 - 红黑树 -permalink: /pages/0966fa/ +permalink: /pages/830b2e9c/ --- # 红黑树 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/03.\345\223\210\345\270\214\350\241\250.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/03.\345\223\210\345\270\214\350\241\250.md" index 505309d98e..9c6751eba0 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/03.\345\223\210\345\270\214\350\241\250.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/03.\345\223\210\345\270\214\350\241\250.md" @@ -7,7 +7,7 @@ categories: tags: - 数据结构和算法 - 哈希表 -permalink: /pages/be34fc/ +permalink: /pages/9dc5bb26/ --- # 哈希表 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/04.\350\267\263\350\241\250.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/04.\350\267\263\350\241\250.md" index 713dbbe8bf..22a48a2f0f 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/04.\350\267\263\350\241\250.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/04.\350\267\263\350\241\250.md" @@ -7,7 +7,7 @@ categories: tags: - 数据结构和算法 - 跳表 -permalink: /pages/42aedd/ +permalink: /pages/ca033e45/ --- # 跳表 @@ -101,4 +101,4 @@ Redis 中的有序集合支持的核心操作主要有下面这几个: ## 参考资料 -- [数据结构与算法之美](https://time.geekbang.org/column/intro/100017301) +- [数据结构与算法之美](https://time.geekbang.org/column/intro/100017301) \ No newline at end of file diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/05.\345\233\276.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/05.\345\233\276.md" index b2d9e6c3df..7baaec1d4b 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/05.\345\233\276.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/05.\345\233\276.md" @@ -7,7 +7,7 @@ categories: tags: - 数据结构和算法 - 图 -permalink: /pages/5dd75b/ +permalink: /pages/a5aec614/ --- # 图 diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/README.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/README.md" index 0351ca5036..ef998701a0 100644 --- "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/README.md" +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/README.md" @@ -5,7 +5,7 @@ categories: - 数据结构和算法 tags: - 数据结构和算法 -permalink: /pages/3ccbd4/ +permalink: /pages/3ecafbc9/ hidden: true index: false --- diff --git "a/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\210\267\351\242\230.md" "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\210\267\351\242\230.md" new file mode 100644 index 0000000000..7ccb8918a0 --- /dev/null +++ "b/source/_posts/11.\346\225\260\346\215\256\347\273\223\346\236\204\345\222\214\347\256\227\346\263\225/\345\210\267\351\242\230.md" @@ -0,0 +1,74 @@ +--- +title: 刷题 +categories: + - 数据结构和算法 +tags: + - 数据结构 + - 算法 +--- + +# 刷题 + +## 经典数据结构 + +### 数组 + +| 题目 | 难度 | 状态 | +| ------------------------------------------------------------ | ---- | ------ | +| [1. 两数之和](https://leetcode.cn/problems/two-sum/) | 简单 | 通过 | +| [167. 两数之和 II - 输入有序数组](https://leetcode.cn/problems/two-sum-ii-input-array-is-sorted/) | 中等 | 通过 | +| [剑指 Offer II 006. 排序数组中两个数字之和](https://leetcode.cn/problems/kLl5u1/) | 简单 | 通过 | +| [剑指 Offer 57. 和为 s 的两个数字](https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/) | 简单 | 通过 | +| [136. 只出现一次的数字](https://leetcode.cn/problems/single-number/) | 简单 | 通过 | +| [217. 存在重复元素](https://leetcode.cn/problems/contains-duplicate/) | 简单 | 通过 | +| [2073. 买票需要的时间](https://leetcode.cn/problems/time-needed-to-buy-tickets/) | 简单 | 通过 | +| [26. 删除有序数组中的重复项](https://leetcode.cn/problems/remove-duplicates-from-sorted-array/) | 简单 | 未通过 | +| [27. 移除元素](https://leetcode.cn/problems/remove-element/) | | | +| [283. 移动零](https://leetcode.cn/problems/move-zeroes/) | | | +| [344. 反转字符串](https://leetcode.cn/problems/reverse-string/) | | | +| [5. 最长回文子串](https://leetcode.cn/problems/longest-palindromic-substring/) | | | +| [263. 丑数](https://leetcode.cn/problems/ugly-number/) | 简单 | 未通过 | +| [264. 丑数 II](https://labuladong.online/algo/problem-set/linkedlist-two-pointers/#slug_ugly-number-ii) | 中等 | 未通过 | +| [1201. 丑数 III](https://leetcode.cn/problems/ugly-number-iii/) | 中等 | 未通过 | +| [313. 超级丑数](https://leetcode.cn/problems/super-ugly-number/) | 中等 | 未通过 | +| [373. 查找和最小的 K 对数字](https://leetcode.cn/problems/find-k-pairs-with-smallest-sums/) | | | +| | | | + +### 链表 + +| 题目 | 难度 | 通关 | +| ------------------------------------------------------------ | ---- | ------ | +| [19. 删除链表的倒数第 N 个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/) | 中等 | 通过 | +| [21. 合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/) | 简单 | 通过 | +| [23. 合并 K 个升序链表](https://leetcode.cn/problems/merge-k-sorted-lists/) | 困难 | 通过 | +| [83. 删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/) | 简单 | 通过 | +| [82. 删除排序链表中的重复元素 II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/description/) | 中等 | 半通过 | +| [86. 分隔链表](https://leetcode.cn/problems/partition-list/) | 简单 | 通过 | +| [876. 链表的中间结点](https://leetcode.cn/problems/middle-of-the-linked-list/) | 简单 | 通过 | +| [剑指 Offer 22. 链表中倒数第 k 个节点](https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/) | 简单 | 通过 | +| [141. 环形链表](https://leetcode.cn/problems/linked-list-cycle/) | 简单 | 通过 | +| [142. 环形链表 II](https://leetcode.cn/problems/linked-list-cycle-ii/) | 中等 | 未通过 | +| [160. 相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/) | 简单 | 半通过 | +| [1836. 从未排序的链表中移除重复元素](https://labuladong.online/algo/problem-set/linkedlist-two-pointers/#slug_remove-duplicates-from-an-unsorted-linked-list) | 中等 | 半通过 | +| | | | +| | | | + +### 栈 + +| 题目 | 难度 | | +| ----------------------------------------------------------------- | ---- | --- | +| [20. 有效的括号](https://leetcode.cn/problems/valid-parentheses/) | 简单 | | +| | | | + +## 解题套路 + +### 滑动窗口 + +本文讲解的例题 + +| 力扣题目 | 难度 | 状态 | +| :------------------------------------------------------------------------------------------------------ | :--: | ---- | +| [3. 无重复字符的最长子串](https://leetcode.cn/problems/longest-substring-without-repeating-characters/) | 中等 | | +| [438. 找到字符串中所有字母异位词](https://leetcode.cn/problems/find-all-anagrams-in-a-string/) | 中等 | 半通过 | +| [567. 字符串的排列](https://leetcode.cn/problems/permutation-in-string/) | 中等 | 未通过 | +| [76. 最小覆盖子串](https://leetcode.cn/problems/minimum-window-substring/) | 困难 | 未通过 | diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/01.Nosql\346\212\200\346\234\257\351\200\211\345\236\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/01.Nosql\346\212\200\346\234\257\351\200\211\345\236\213.md" index 149e889e8d..2c2c1d9ae8 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/01.Nosql\346\212\200\346\234\257\351\200\211\345\236\213.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/01.Nosql\346\212\200\346\234\257\351\200\211\345\236\213.md" @@ -9,7 +9,7 @@ tags: - 数据库 - 综合 - Nosql -permalink: /pages/0e1012/ +permalink: /pages/d94c7395/ --- # Nosql 技术选型 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/02.\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/02.\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" index cc851ee7f0..c5aa9d7e93 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/02.\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/02.\346\225\260\346\215\256\347\273\223\346\236\204\344\270\216\346\225\260\346\215\256\345\272\223\347\264\242\345\274\225.md" @@ -10,7 +10,7 @@ tags: - 综合 - 数据结构 - 索引 -permalink: /pages/d7cd88/ +permalink: /pages/5bfaa21b/ --- # 数据结构与数据库索引 @@ -215,4 +215,4 @@ LSM 树的这些特点,使得它相对于 B+ 树,在写入性能上有大幅 - [数据结构与算法之美](https://time.geekbang.org/column/intro/100017301) - [检索技术核心 20 讲](https://time.geekbang.org/column/intro/100048401) - [Data Structures for Databases](https://www.cise.ufl.edu/~mschneid/Research/papers/HS05BoCh.pdf) -- [Data Structures and Algorithms for Big Databases](https://people.csail.mit.edu/bradley/BenderKuszmaul-tutorial-xldb12.pdf) +- [Data Structures and Algorithms for Big Databases](https://people.csail.mit.edu/bradley/BenderKuszmaul-tutorial-xldb12.pdf) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/README.md" index 42a7a801e6..bddffda09f 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/01.\346\225\260\346\215\256\345\272\223\347\273\274\345\220\210/README.md" @@ -7,7 +7,7 @@ categories: tags: - 数据库 - 综合 -permalink: /pages/3c3c45/ +permalink: /pages/2cf6ceb4/ hidden: true index: false --- diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/01.ShardingSphere\347\256\200\344\273\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/01.ShardingSphere\347\256\200\344\273\213.md" index 17a90468dc..7d6f9ad242 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/01.ShardingSphere\347\256\200\344\273\213.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/01.ShardingSphere\347\256\200\344\273\213.md" @@ -10,7 +10,7 @@ tags: - 数据库 - 中间件 - 分库分表 -permalink: /pages/5ed2a2/ +permalink: /pages/b854a36a/ --- # ShardingSphere 简介 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/02.ShardingSphereJdbc.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/02.ShardingSphereJdbc.md" index 55202fc0d4..e889ea0e78 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/02.ShardingSphereJdbc.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/01.Shardingsphere/02.ShardingSphereJdbc.md" @@ -10,7 +10,7 @@ tags: - 数据库 - 中间件 - 分库分表 -permalink: /pages/8448de/ +permalink: /pages/1cb73343/ --- # ShardingSphere Jdbc diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/02.Flyway.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/02.Flyway.md" index 7dc02e4c59..0cf0f3a679 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/02.Flyway.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/02.Flyway.md" @@ -9,7 +9,7 @@ tags: - 数据库 - 中间件 - 版本管理 -permalink: /pages/e2648c/ +permalink: /pages/f8de06ee/ --- # 版本管理中间件 Flyway diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/README.md" index f3f49ff801..151e59bddf 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/02.\346\225\260\346\215\256\345\272\223\344\270\255\351\227\264\344\273\266/README.md" @@ -7,7 +7,7 @@ categories: tags: - 数据库 - 中间件 -permalink: /pages/addb05/ +permalink: /pages/a5124f55/ hidden: true index: false --- diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/02.SQL\350\257\255\346\263\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/02.SQL\350\257\255\346\263\225.md" deleted file mode 100644 index 7c2412634c..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/02.SQL\350\257\255\346\263\225.md" +++ /dev/null @@ -1,1188 +0,0 @@ ---- -title: SQL 语法速成 -cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202310011053288.png -date: 2018-06-15 16:07:17 -order: 02 -categories: - - 数据库 - - 关系型数据库 - - 综合 -tags: - - 数据库 - - 关系型数据库 - - SQL -permalink: /pages/b71c9e/ ---- - -# SQL 语法速成 - -> 本文针对关系型数据库的基本语法。限于篇幅,本文侧重说明用法,不会展开讲解特性、原理。 -> -> 本文语法主要针对 Mysql,但大部分的语法对其他关系型数据库也适用。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310011053288.png) - -## SQL 简介 - -### 数据库术语 - -- **数据库(database)** - 保存有组织的数据的容器(通常是一个文件或一组文件)。 -- **数据表(table)** - 某种特定类型数据的结构化清单。 -- **模式(schema)** - 关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息。数据库和表都有模式。 -- **行(row)** - 表中的一条记录。 -- **列(column)** - 表中的一个字段。所有表都是由一个或多个列组成的。 -- **主键(primary key)** - 一列(或一组列),其值能够唯一标识表中每一行。 - -### SQL 语法 - -> SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。 - -#### SQL 语法结构 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/database/mysql/sql-syntax.png) - -SQL 语法结构包括: - -- **子句** - 是语句和查询的组成成分。(在某些情况下,这些都是可选的。) -- **表达式** - 可以产生任何标量值,或由列和行的数据库表 -- **谓词** - 给需要评估的 SQL 三值逻辑(3VL)(true/false/unknown)或布尔真值指定条件,并限制语句和查询的效果,或改变程序流程。 -- **查询** - 基于特定条件检索数据。这是 SQL 的一个重要组成部分。 -- **语句** - 可以持久地影响纲要和数据,也可以控制数据库事务、程序流程、连接、会话或诊断。 - -#### SQL 语法要点 - -- **SQL 语句不区分大小写**,但是数据库表名、列名和值是否区分,依赖于具体的 DBMS 以及配置。 - -例如:`SELECT` 与 `select` 、`Select` 是相同的。 - -- **多条 SQL 语句必须以分号(`;`)分隔**。 - -- 处理 SQL 语句时,**所有空格都被忽略**。SQL 语句可以写成一行,也可以分写为多行。 - -```sql --- 一行 SQL 语句 -UPDATE user SET username='robot', password='robot' WHERE username = 'root'; - --- 多行 SQL 语句 -UPDATE user -SET username='robot', password='robot' -WHERE username = 'root'; -``` - -- SQL 支持三种注释 - -```sql -## 注释1 --- 注释2 -/* 注释3 */ -``` - -#### SQL 分类 - -##### DDL - -**DDL**,英文叫做 Data Definition Language,即**“数据定义语言”**。**DDL 用于定义数据库对象**。 - -DDL 定义操作包括创建(`CREATE`)、删除(`DROP`)、修改(`ALTER`);而被操作的对象包括:数据库、数据表和列、视图、索引。 - -##### DML - -**DML**,英文叫做 Data Manipulation Language,即**“数据操作语言”**。**DML 用于访问数据库的数据**。 - -DML 访问操作包括插入(`INSERT`)、删除(`DELETE`)、修改(`UPDATE`)、查询(`SELECT`)。这四个指令合称 **CRUD**,英文单词为 Create, Read, Update, Delete,即增删改查。 - -##### TCL - -**TCL**,英文叫做 Transaction Control Language,即**“事务控制语言”**。**TCL 用于管理数据库中的事务**,实际上就是用于管理由 DML 语句所产生的数据变更,它还允许将语句分组为逻辑事务。 - -TCL 的核心指令是 `COMMIT`、`ROLLBACK`。 - -##### DCL - -**DCL**,英文叫做 Data Control Language,即**“数据控制语言”**。DCL 用于对数据访问权进行控制,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。 - -DCL 的核心指令是 `GRANT`、`REVOKE`。 - -DCL 以**控制用户的访问权限**为主,因此其指令作法并不复杂,可利用 DCL 控制的权限有:`CONNECT`、`SELECT`、`INSERT`、`UPDATE`、`DELETE`、`EXECUTE`、`USAGE`、`REFERENCES`。根据不同的 DBMS 以及不同的安全性实体,其支持的权限控制也有所不同。 - -## 数据定义(CREATE、ALTER、DROP) - -> DDL 的主要功能是定义数据库对象(如:数据库、数据表、视图、索引等)。 - -### 数据库(DATABASE) - -#### 创建数据库 - -```sql -CREATE DATABASE IF NOT EXISTS db_tutorial; -``` - -#### 删除数据库 - -```sql -DROP DATABASE IF EXISTS db_tutorial; -``` - -#### 选择数据库 - -```sql -USE db_tutorial; -``` - -### 数据表(TABLE) - -#### 创建数据表 - -**普通创建** - -```sql -CREATE TABLE user ( - id INT(10) UNSIGNED NOT NULL COMMENT 'Id', - username VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '用户名', - password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '密码', - email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '邮箱' -) COMMENT ='用户表'; -``` - -**根据已有的表创建新表** - -```sql -CREATE TABLE vip_user AS -SELECT * FROM user; -``` - -#### 修改数据表 - -##### 添加列 - -```sql -ALTER TABLE user -ADD age int(3); -``` - -##### 删除列 - -```sql -ALTER TABLE user -DROP COLUMN age; -``` - -##### 修改列 - -```sql -ALTER TABLE `user` -MODIFY COLUMN age tinyint; -``` - -#### 删除数据表 - -```sql -DROP TABLE IF EXISTS user; -DROP TABLE IF EXISTS vip_user; -``` - -### 视图(VIEW) - -**“视图”是基于 SQL 语句的结果集的可视化的表**。视图是虚拟的表,本身不存储数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。 - -视图的作用: - -- 简化复杂的 SQL 操作,比如复杂的连接。 -- 只使用实际表的一部分数据。 -- 通过只给用户访问视图的权限,保证数据的安全性。 -- 更改数据格式和表示。 - -#### 创建视图 - -```sql -CREATE VIEW top_10_user_view AS -SELECT id, username FROM user -WHERE id < 10; -``` - -#### 删除视图 - -```sql -DROP VIEW top_10_user_view; -``` - -### 索引(INDEX) - -**“索引”是数据库为了提高查找效率的一种数据结构**。 - -日常生活中,我们可以通过检索目录,来快速定位书本中的内容。索引和数据表,就好比目录和书,想要高效查询数据表,索引至关重要。在数据量小且负载较低时,不恰当的索引对于性能的影响可能还不明显;但随着数据量逐渐增大,性能则会急剧下降。因此,**设置合理的索引是数据库查询性能优化的最有效手段**。 - -更新一个包含索引的表需要比更新一个没有索引的表花费更多的时间,这是由于索引本身也需要更新。因此,理想的做法是仅仅在常常被搜索的列(以及表)上面创建索引。 - -“唯一索引”表明此索引的每一个索引值只对应唯一的数据记录。 - -#### 创建索引 - -```sql -CREATE INDEX idx_email - ON user(email); -``` - -#### 创建唯一索引 - -```sql -CREATE UNIQUE INDEX uniq_name - ON user(name); -``` - -#### 删除索引 - -```sql -ALTER TABLE user -DROP INDEX idx_email; -ALTER TABLE user -DROP INDEX uniq_name; -``` - -#### 添加主键 - -```sql -ALTER TABLE user -ADD PRIMARY KEY (id); -``` - -#### 删除主键 - -```sql -ALTER TABLE user -DROP PRIMARY KEY; -``` - -### 约束 - -> SQL 约束用于规定表中的数据规则。 - -如果存在违反约束的数据行为,行为会被约束终止。约束可以在创建表时规定(通过 `CREATE TABLE` 语句),或者在表创建之后规定(通过 `ALTER TABLE` 语句)。 - -约束类型 -- `NOT NULL` - 指示字段不能存储 `NULL` 值。 -- `UNIQUE` - 保证字段的每行必须有唯一的值。 -- `PRIMARY KEY` - PRIMARY KEY 的作用是唯一标识一条记录,不能重复,不能为空,即相当于 `NOT NULL` + `UNIQUE`。确保字段(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。 -- `FOREIGN KEY` - 保证一个表中的数据匹配另一个表中的值的参照完整性。 -- `CHECK` - 用于检查字段取值范围的有效性。 -- `DEFAULT` - 表明字段的默认值。如果插入数据时,该字段没有赋值,就会被设置为默认值。 - -创建表时使用约束条件: - -```sql -CREATE TABLE Users ( - Id INT(10) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增Id', - Username VARCHAR(64) NOT NULL UNIQUE DEFAULT 'default' COMMENT '用户名', - Password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '密码', - Email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '邮箱地址', - Enabled TINYINT(4) DEFAULT NULL COMMENT '是否有效', - PRIMARY KEY (Id) -) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -``` - -## 增删改查(CRUD) - -增删改查,又称为 **`CRUD`**,是数据库基本操作中的基本操作。 - -### 插入数据 - -> - `INSERT INTO` 语句用于向表中插入新记录。 - -#### 插入完整的行 - -```sql -INSERT INTO user -VALUES (10, 'root', 'root', 'xxxx@163.com'); -``` - -#### 插入行的一部分 - -```sql -INSERT INTO user(username, password, email) -VALUES ('admin', 'admin', 'xxxx@163.com'); -``` - -#### 插入查询出来的数据 - -```sql -INSERT INTO user(username) -SELECT name -FROM account; -``` - -### 更新数据 - -> - `UPDATE` 语句用于更新表中的记录。 - -```sql -UPDATE user -SET username='robot', password='robot' -WHERE username = 'root'; -``` - -### 删除数据 - -> - `DELETE` 语句用于删除表中的记录。 -> - `TRUNCATE TABLE` 可以清空表,也就是删除所有行。 - -#### 删除表中的指定数据 - -```sql -DELETE FROM user WHERE username = 'robot'; -``` - -#### 清空表中的数据 - -```sql -TRUNCATE TABLE user; -``` - -### 查询数据 - -> - `SELECT` 语句用于从数据库中查询数据。 -> - `DISTINCT` 用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。 -> - `LIMIT` 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。 -> - `ASC` :升序(默认) -> - `DESC` :降序 - -#### 查询单列 - -```sql -SELECT prod_name FROM products; -``` - -#### 查询多列 - -```sql -SELECT prod_id, prod_name, prod_price FROM products; -``` - -#### 查询所有列 - -```sql -SELECT * FROM products; -``` - -#### 查询不同的值 - -```sql -SELECT DISTINCT vend_id FROM products; -``` - -#### 限制查询数量 - -```sql --- 返回前 5 行 -SELECT * FROM products LIMIT 5; -SELECT * FROM products LIMIT 0, 5; --- 返回第 3 ~ 5 行 -SELECT * FROM products LIMIT 2, 3; -``` - -## 过滤数据(WHERE) - -子查询是嵌套在较大查询中的 SQL 查询。子查询也称为**内部查询**或**内部选择**,而包含子查询的语句也称为**外部查询**或**外部选择**。 - -- 子查询可以嵌套在 `SELECT`,`INSERT`,`UPDATE` 或 `DELETE` 语句内或另一个子查询中。 - -- 子查询通常会在另一个 `SELECT` 语句的 `WHERE` 子句中添加。 - -- 您可以使用比较运算符,如 `>`,`<`,或 `=`。比较运算符也可以是多行运算符,如 `IN`,`ANY` 或 `ALL`。 - -- 子查询必须被圆括号 `()` 括起来。 - -- 内部查询首先在其父查询之前执行,以便可以将内部查询的结果传递给外部查询。执行过程可以参考下图: - -

    - sql-subqueries -

    - -**子查询的子查询** - -```sql -SELECT cust_name, cust_contact -FROM customers -WHERE cust_id IN (SELECT cust_id - FROM orders - WHERE order_num IN (SELECT order_num - FROM orderitems - WHERE prod_id = 'RGAN01')); -``` - -### WHERE 子句 - -在 SQL 语句中,数据根据 `WHERE` 子句中指定的搜索条件进行过滤。 - -`WHERE` 子句的基本格式如下: - -```sql -SELECT ……(列名) FROM ……(表名) WHERE ……(子句条件) -``` - -`WHERE` 子句用于过滤记录,即缩小访问数据的范围。`WHERE` 后跟一个返回 `true` 或 `false` 的条件。 - -`WHERE` 可以与 `SELECT`,`UPDATE` 和 `DELETE` 一起使用。 - -**`SELECT` 语句中的 `WHERE` 子句** - -```sql -SELECT * FROM Customers -WHERE cust_name = 'Kids Place'; -``` - -**`UPDATE` 语句中的 `WHERE` 子句** - -```sql -UPDATE Customers -SET cust_name = 'Jack Jones' -WHERE cust_name = 'Kids Place'; -``` - -**`DELETE` 语句中的 `WHERE` 子句** - -```sql -DELETE FROM Customers -WHERE cust_name = 'Kids Place'; -``` - -可以在 `WHERE` 子句中使用的操作符: - -### 比较操作符 - -| 运算符 | 描述 | -| ------ | ------------------------------------------------------ | -| `=` | 等于 | -| `<>` | 不等于。注释:在 SQL 的一些版本中,该操作符可被写成 != | -| `>` | 大于 | -| `<` | 小于 | -| `>=` | 大于等于 | -| `<=` | 小于等于 | - -### 范围操作符 - -| 运算符 | 描述 | -| --------- | -------------------------- | -| `BETWEEN` | 在某个范围内 | -| `IN` | 指定针对某个列的多个可能值 | - -- `IN` 操作符在 `WHERE` 子句中使用,作用是在指定的几个特定值中任选一个值。 - -- `BETWEEN` 操作符在 `WHERE` 子句中使用,作用是选取介于某个范围内的值。 - -**IN 示例** - -```sql -SELECT * -FROM products -WHERE vend_id IN ('DLL01', 'BRS01'); -``` - -**BETWEEN 示例** - -```sql -SELECT * -FROM products -WHERE prod_price BETWEEN 3 AND 5; -``` - -### 逻辑操作符 - -| 运算符 | 描述 | -| ------ | ---------- | -| `AND` | 并且(与) | -| `OR` | 或者(或) | -| `NOT` | 否定(非) | - -`AND`、`OR`、`NOT` 是用于对过滤条件的逻辑处理指令。 - -- `AND` 优先级高于 `OR`,为了明确处理顺序,可以使用 `()`。`AND` 操作符表示左右条件都要满足。 -- `OR` 操作符表示左右条件满足任意一个即可。 - -- `NOT` 操作符用于否定一个条件。 - -**AND 示例** - -```sql -SELECT prod_id, prod_name, prod_price -FROM products -WHERE vend_id = 'DLL01' AND prod_price <= 4; -``` - -**OR 示例** - -```sql -SELECT prod_id, prod_name, prod_price -FROM products -WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'; -``` - -**NOT 示例** - -```sql -SELECT * -FROM products -WHERE prod_price NOT BETWEEN 3 AND 5; -``` - -### 通配符 - -| 运算符 | 描述 | -| ------ | -------------------------- | -| `LIKE` | 搜索某种模式 | -| `%` | 表示任意字符出现任意次数 | -| `_` | 表示任意字符出现一次 | -| `[]` | 必须匹配指定位置的一个字符 | - -`LIKE` 操作符在 `WHERE` 子句中使用,作用是确定字符串是否匹配模式。只有字段是文本值时才使用 `LIKE`。 - -`LIKE` 支持以下通配符匹配选项: - -- `%` 表示任何字符出现任意次数。 -- `_` 表示任何字符出现一次。 -- `[]` 必须匹配指定位置的一个字符。 - -> 注意:**不要滥用通配符,通配符位于开头处匹配会非常慢**。 - -`%` 示例: - -```sql -SELECT prod_id, prod_name, prod_price -FROM products -WHERE prod_name LIKE '%bean bag%'; -``` - -`_` 示例: - -```sql -SELECT prod_id, prod_name, prod_price -FROM products -WHERE prod_name LIKE '__ inch teddy bear'; -``` - -## 排序和分组 - -### ORDER BY - -> `ORDER BY` 用于对结果集进行排序。 - -`ORDER BY` 有两种排序模式: - -- `ASC` :升序(默认) -- `DESC` :降序 - -可以按多个列进行排序,并且为每个列指定不同的排序方式。 - -指定多个列的排序示例: - -```sql -SELECT * FROM products -ORDER BY prod_price DESC, prod_name ASC; -``` - -### GROUP BY - -> `GROUP BY` 子句将记录分组到汇总行中,`GROUP BY` 为每个组返回一个记录。 - -`GROUP BY` 可以按一列或多列进行分组。 - -`GROUP BY` 通常还涉及聚合函数:COUNT,MAX,SUM,AVG 等。 - -`GROUP BY` 按分组字段进行排序后,`ORDER BY` 可以以汇总字段来进行排序。 - -分组示例: - -```sql -SELECT cust_name, COUNT(cust_address) AS addr_num -FROM Customers GROUP BY cust_name; -``` - -分组后排序示例: - -```sql -SELECT cust_name, COUNT(cust_address) AS addr_num -FROM Customers GROUP BY cust_name -ORDER BY cust_name DESC; -``` - -### HAVING - -> `HAVING` 用于对汇总的 `GROUP BY` 结果进行过滤。`HAVING` 要求存在一个 `GROUP BY` 子句。 - -`WHERE` 和 `HAVING` 可以在相同的查询中。 - -`HAVING` vs `WHERE`: - -- `WHERE` 和 `HAVING` 都是用于过滤。 -- `HAVING` 适用于汇总的组记录;而 `WHERE` 适用于单个记录。 - -使用 `WHERE` 和 `HAVING` 过滤数据示例: - -```sql -SELECT cust_name, COUNT(*) AS num -FROM Customers -WHERE cust_email IS NOT NULL -GROUP BY cust_name -HAVING COUNT(*) >= 1; -``` - -## 连接和组合 - -### 连接(JOIN) - -**在 SELECT, UPDATE 和 DELETE 语句中,“连接”可以用于联合多表查询。连接使用 `JOIN` 关键字,并且条件语句使用 `ON` 而不是 `WHERE`**。 - -**连接可以替换子查询,并且一般比子查询的效率更快**。 - -`JOIN` 有以下类型: - -- 内连接 - 内连接又称等值连接,用于获取两个表中字段匹配关系的记录,**使用 `INNER JOIN` 关键字**。在没有条件语句的情况下**返回笛卡尔积**。 - - 笛卡尔积 - **“笛卡尔积”也称为交叉连接(`CROSS JOIN`),它的作用就是可以把任意表进行连接,即使这两张表不相关**。 - - 自连接(=) - **“自连接(=)”可以看成内连接的一种,只是连接的表是自身而已**。 - - 自然连接(NATURAL JOIN) - **“自然连接”会自动连接所有同名列**。自然连接使用 `NATURAL JOIN` 关键字。 -- 外连接 - - 左连接(LEFT JOIN) - **“左外连接”会获取左表所有记录,即使右表没有对应匹配的记录**。左外连接使用 `LEFT JOIN` 关键字。 - - 右连接(RIGHT JOIN) - **“右外连接”会获取右表所有记录,即使左表没有对应匹配的记录**。右外连接使用 `RIGHT JOIN` 关键字。 - -
    - sql-join -
    -#### 内连接(INNER JOIN) - -内连接又称等值连接,用于获取两个表中字段匹配关系的记录,**使用 `INNER JOIN` 关键字**。在没有条件语句的情况下**返回笛卡尔积**。 - -```sql -SELECT vend_name, prod_name, prod_price -FROM vendors INNER JOIN products -ON vendors.vend_id = products.vend_id; - --- 也可以省略 INNER 使用 JOIN,与上面一句效果一样 -SELECT vend_name, prod_name, prod_price -FROM vendors JOIN products -ON vendors.vend_id = products.vend_id; -``` - -##### 笛卡尔积 - -**“笛卡尔积”也称为交叉连接(`CROSS JOIN`),它的作用就是可以把任意表进行连接,即使这两张表不相关**。但通常进行连接还是需要筛选的,因此需要在连接后面加上 `WHERE` 子句,也就是作为过滤条件对连接数据进行筛选。 - -笛卡尔积是一个数学运算。假设我有两个集合 X 和 Y,那么 X 和 Y 的笛卡尔积就是 X 和 Y 的所有可能组合,也就是第一个对象来自于 X,第二个对象来自于 Y 的所有可能。 - -【示例】求 t1 和 t2 两张表的笛卡尔积 - -```sql --- 以下两条 SQL,执行结果相同 -SELECT * FROM t1, t2; -SELECT * FROM t1 CROSS JOIN t2; -``` - -##### 自连接(=) - -**“自连接”可以看成内连接的一种,只是连接的表是自身而已**。 - -```sql -SELECT c1.cust_id, c1.cust_name, c1.cust_contact -FROM customers c1, customers c2 -WHERE c1.cust_name = c2.cust_name -AND c2.cust_contact = 'Jim Jones'; -``` - -##### 自然连接(NATURAL JOIN) - -**“自然连接”会自动连接所有同名列**。自然连接使用 `NATURAL JOIN` 关键字。 - -```sql -SELECT * -FROM Products -NATURAL JOIN Customers; -``` - -#### 外连接(OUTER JOIN) - -外连接返回一个表中的所有行,并且仅返回来自此表中满足连接条件的那些行,即两个表中的列是相等的。外连接分为左外连接、右外连接、全外连接(Mysql 不支持)。 - -##### 左连接(LEFT JOIN) - -**“左外连接”会获取左表所有记录,即使右表没有对应匹配的记录**。左外连接使用 `LEFT JOIN` 关键字。 - -```sql -SELECT customers.cust_id, orders.order_num -FROM customers LEFT JOIN orders -ON customers.cust_id = orders.cust_id; -``` - -##### 右连接(RIGHT JOIN) - -**“右外连接”会获取右表所有记录,即使左表没有对应匹配的记录**。右外连接使用 `RIGHT JOIN` 关键字。 - -```sql -SELECT customers.cust_id, orders.order_num -FROM customers RIGHT JOIN orders -ON customers.cust_id = orders.cust_id; -``` - -### 组合(UNION) - -> `UNION` 运算符**将两个或更多查询的结果组合起来,并生成一个结果集**,其中包含来自 `UNION` 中参与查询的提取行。 - -`UNION` 基本规则: - -- 所有查询的列数和列顺序必须相同。 -- 每个查询中涉及表的列的数据类型必须相同或兼容。 -- 通常返回的列名取自第一个查询。 - -默认会去除相同行,如果需要保留相同行,使用 `UNION ALL`。 - -只能包含一个 `ORDER BY` 子句,并且必须位于语句的最后。 - -应用场景: - -- 在一个查询中从不同的表返回结构数据。 -- 对一个表执行多个查询,按一个查询返回数据。 - -组合查询示例: - -```sql -SELECT cust_name, cust_contact, cust_email -FROM customers -WHERE cust_state IN ('IL', 'IN', 'MI') -UNION -SELECT cust_name, cust_contact, cust_email -FROM customers -WHERE cust_name = 'Fun4All'; -``` - -### JOIN vs UNION - -- `JOIN` 中连接表的列可能不同,但在 `UNION` 中,所有查询的列数和列顺序必须相同。 -- `UNION` 将查询之后的行放在一起(垂直放置),但 `JOIN` 将查询之后的列放在一起(水平放置),即它构成一个笛卡尔积。 - -## 函数 - -> 🔔 注意:不同数据库的函数往往各不相同,因此不可移植。本节主要以 Mysql 的函数为例。 - -### 字符串函数 - -| 函数 | 说明 | -| :------------------: | :--------------------: | -| `CONCAT()` | 合并字符串 | -| `LEFT()`、`RIGHT()` | 左边或者右边的字符 | -| `LOWER()`、`UPPER()` | 转换为小写或者大写 | -| `LTRIM()`、`RTIM()` | 去除左边或者右边的空格 | -| `LENGTH()` | 长度 | -| `SOUNDEX()` | 转换为语音值 | - -其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。 - -```sql -SELECT * -FROM mytable -WHERE SOUNDEX(col1) = SOUNDEX('apple') -``` - -### 时间函数 - -- 日期格式:`YYYY-MM-DD` -- 时间格式:`HH:MM:SS` - -| 函 数 | 说 明 | -| :--------------: | :----------------------------: | -| `ADDDATE()` | 增加一个日期(天、周等) | -| `ADDTIME()` | 增加一个时间(时、分等) | -| `CURRENT_DATE()` | 返回当前日期 | -| `CURRENT_TIME()` | 返回当前时间 | -| `DATE()` | 返回日期时间的日期部分 | -| `DATEDIFF()` | 计算两个日期之差 | -| `DATE_ADD()` | 高度灵活的日期运算函数 | -| `DATE_FORMAT()` | 返回一个格式化的日期或时间串 | -| `DAY()` | 返回一个日期的天数部分 | -| `DAYOFWEEK()` | 对于一个日期,返回对应的星期几 | -| `HOUR()` | 返回一个时间的小时部分 | -| `MINUTE()` | 返回一个时间的分钟部分 | -| `MONTH()` | 返回一个日期的月份部分 | -| `NOW()` | 返回当前日期和时间 | -| `SECOND()` | 返回一个时间的秒部分 | -| `TIME()` | 返回一个日期时间的时间部分 | -| `YEAR()` | 返回一个日期的年份部分 | - -```sql -mysql> SELECT NOW(); -2018-4-14 20:25:11 -``` - -### 数学函数 - -常见 Mysql 数学函数: - -| 函数 | 说明 | -| :-------: | :------: | -| `ABS()` | 取绝对值 | -| `MOD()` | 取余 | -| `ROUND()` | 四舍五入 | -| `...` | | - -### 聚合函数 - -| 函 数 | 说 明 | -| :-------: | :--------------: | -| `AVG()` | 返回某列的平均值 | -| `COUNT()` | 返回某列的行数 | -| `MAX()` | 返回某列的最大值 | -| `MIN()` | 返回某列的最小值 | -| `SUM()` | 返回某列值之和 | - -`AVG()` 会忽略 NULL 行。 - -使用 DISTINCT 可以让汇总函数值汇总不同的值。 - -```sql -SELECT AVG(DISTINCT col1) AS avg_col -FROM mytable -``` - -### 转换函数 - -| 函 数 | 说 明 | 示例 | -| :------: | :----------: | -------------------------------------------------- | -| `CAST()` | 转换数据类型 | `SELECT CAST("2017-08-29" AS DATE); -> 2017-08-29` | - -## 事务 - -不能回退 `SELECT` 语句,回退 `SELECT` 语句也没意义;也不能回退 `CREATE` 和 `DROP` 语句。 - -**MySQL 默认采用隐式提交策略(`autocommit`)**,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 `START TRANSACTION` 语句时,会关闭隐式提交;当 `COMMIT` 或 `ROLLBACK` 语句执行后,事务会自动关闭,重新恢复隐式提交。 - -通过 `set autocommit=0` 可以取消自动提交,直到 `set autocommit=1` 才会提交;`autocommit` 标记是针对每个连接而不是针对服务器的。 - -事务处理指令: - -- `START TRANSACTION` - 指令用于标记事务的起始点。 -- `SAVEPOINT` - 指令用于创建保留点。 -- `ROLLBACK TO` - 指令用于回滚到指定的保留点;如果没有设置保留点,则回退到 `START TRANSACTION` 语句处。 -- `COMMIT` - 提交事务。 -- `RELEASE SAVEPOINT`:删除某个保存点。 -- `SET TRANSACTION`:设置事务的隔离级别。 - -事务处理示例: - -```sql --- 开始事务 -START TRANSACTION; - --- 插入操作 A -INSERT INTO `user` -VALUES (1, 'root1', 'root1', 'xxxx@163.com'); - --- 创建保留点 updateA -SAVEPOINT updateA; - --- 插入操作 B -INSERT INTO `user` -VALUES (2, 'root2', 'root2', 'xxxx@163.com'); - --- 回滚到保留点 updateA -ROLLBACK TO updateA; - --- 提交事务,只有操作 A 生效 -COMMIT; -``` - -### ACID - -### 事务隔离级别 - ---- - -**(以下为 DCL 语句用法)** - -## 权限控制 - -`GRANT` 和 `REVOKE` 可在几个层次上控制访问权限: - -- 整个服务器,使用 `GRANT ALL` 和 `REVOKE ALL`; -- 整个数据库,使用 ON database.\*; -- 特定的表,使用 ON database.table; -- 特定的列; -- 特定的存储过程。 - -新创建的账户没有任何权限。 - -账户用 `username@host` 的形式定义,`username@%` 使用的是默认主机名。 - -MySQL 的账户信息保存在 mysql 这个数据库中。 - -```sql -USE mysql; -SELECT user FROM user; -``` - -### 创建账户 - -```sql -CREATE USER myuser IDENTIFIED BY 'mypassword'; -``` - -### 修改账户名 - -```sql -UPDATE user SET user='newuser' WHERE user='myuser'; -FLUSH PRIVILEGES; -``` - -### 删除账户 - -```sql -DROP USER myuser; -``` - -### 查看权限 - -```sql -SHOW GRANTS FOR myuser; -``` - -### 授予权限 - -```sql -GRANT SELECT, INSERT ON *.* TO myuser; -``` - -### 删除权限 - -```sql -REVOKE SELECT, INSERT ON *.* FROM myuser; -``` - -### 更改密码 - -```sql -SET PASSWORD FOR myuser = 'mypass'; -``` - -## 存储过程 - -存储过程的英文是 Stored Procedure。它可以视为一组 SQL 语句的批处理。一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。 - -定义存储过程的语法格式: - -```sql -CREATE PROCEDURE 存储过程名称 ([参数列表]) -BEGIN - 需要执行的语句 -END -``` - -存储过程定义语句类型: - -- `CREATE PROCEDURE` 用于创建存储过程 -- `DROP PROCEDURE` 用于删除存储过程 -- `ALTER PROCEDURE` 用于修改存储过程 - -### 使用存储过程 - -创建存储过程的要点: - -- `DELIMITER` 用于定义语句的结束符 -- 存储过程的 3 种参数类型: - - `IN`:存储过程的入参 - - `OUT`:存储过程的出参 - - `INPUT`:既是存储过程的入参,也是存储过程的出参 -- 流控制语句: - - `BEGIN…END`:`BEGIN…END` 中间包含了多个语句,每个语句都以(`;`)号为结束符。 - - `DECLARE`:`DECLARE` 用来声明变量,使用的位置在于 `BEGIN…END` 语句中间,而且需要在其他语句使用之前进行变量的声明。 - - `SET`:赋值语句,用于对变量进行赋值。 - - `SELECT…INTO`:把从数据表中查询的结果存放到变量中,也就是为变量赋值。每次只能给一个变量赋值,不支持集合的操作。 - - `IF…THEN…ENDIF`:条件判断语句,可以在 `IF…THEN…ENDIF` 中使用 `ELSE` 和 `ELSEIF` 来进行条件判断。 - - `CASE`:`CASE` 语句用于多条件的分支判断。 - -创建存储过程示例: - -```sql -DROP PROCEDURE IF EXISTS `proc_adder`; -DELIMITER ;; -CREATE DEFINER=`root`@`localhost` PROCEDURE `proc_adder`(IN a int, IN b int, OUT sum int) -BEGIN - DECLARE c int; - if a is null then set a = 0; - end if; - - if b is null then set b = 0; - end if; - - set sum = a + b; -END -;; -DELIMITER ; -``` - -使用存储过程示例: - -```sql -set @b=5; -call proc_adder(2,@b,@s); -select @s as sum; -``` - -### 存储过程的利弊 - -存储过程的优点: - -- **执行效率高**:一次编译多次使用。 -- **安全性强**:在设定存储过程的时候可以设置对用户的使用权限,这样就和视图一样具有较强的安全性。 -- **可复用**:将代码封装,可以提高代码复用。 -- **性能好** - - 由于是预先编译,因此具有很高的性能。 - - 一个存储过程替代大量 T_SQL 语句 ,可以降低网络通信量,提高通信速率。 - -存储过程的缺点: - -- **可移植性差**:存储过程不能跨数据库移植。由于不同数据库的存储过程语法几乎都不一样,十分难以维护(不通用)。 -- **调试困难**:只有少数 DBMS 支持存储过程的调试。对于复杂的存储过程来说,开发和维护都不容易。 -- **版本管理困难**:比如数据表索引发生变化了,可能会导致存储过程失效。我们在开发软件的时候往往需要进行版本管理,但是存储过程本身没有版本控制,版本迭代更新的时候很麻烦。 -- **不适合高并发的场景**:高并发的场景需要减少数据库的压力,有时数据库会采用分库分表的方式,而且对可扩展性要求很高,在这种情况下,存储过程会变得难以维护,增加数据库的压力,显然就不适用了。 - -> _综上,存储过程的优缺点都非常突出,是否使用一定要慎重,需要根据具体应用场景来权衡_。 - -### 触发器 - -> 触发器可以视为一种特殊的存储过程。 -> -> 触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。 - -#### 触发器特性 - -可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。 - -MySQL 不允许在触发器中使用 `CALL` 语句 ,也就是不能调用存储过程。 - -**`BEGIN` 和 `END`** - -当触发器的触发条件满足时,将会执行 `BEGIN` 和 `END` 之间的触发器执行动作。 - -> 🔔 注意:在 MySQL 中,分号 `;` 是语句结束的标识符,遇到分号表示该段语句已经结束,MySQL 可以开始执行了。因此,解释器遇到触发器执行动作中的分号后就开始执行,然后会报错,因为没有找到和 BEGIN 匹配的 END。 -> -> 这时就会用到 `DELIMITER` 命令(`DELIMITER` 是定界符,分隔符的意思)。它是一条命令,不需要语句结束标识,语法为:`DELIMITER new_delemiter`。`new_delemiter` 可以设为 1 个或多个长度的符号,默认的是分号 `;`,我们可以把它修改为其他符号,如 `$` - `DELIMITER $` 。在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了 `$`,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来。 - -**`NEW` 和 `OLD`** - -- MySQL 中定义了 `NEW` 和 `OLD` 关键字,用来表示触发器的所在表中,触发了触发器的那一行数据。 -- 在 `INSERT` 型触发器中,`NEW` 用来表示将要(`BEFORE`)或已经(`AFTER`)插入的新数据; -- 在 `UPDATE` 型触发器中,`OLD` 用来表示将要或已经被修改的原数据,`NEW` 用来表示将要或已经修改为的新数据; -- 在 `DELETE` 型触发器中,`OLD` 用来表示将要或已经被删除的原数据; -- 使用方法: `NEW.columnName` (columnName 为相应数据表某一列名) - -#### 触发器指令 - -> 提示:为了理解触发器的要点,有必要先了解一下创建触发器的指令。 - -`CREATE TRIGGER` 指令用于创建触发器。 - -语法: - -```sql -CREATE TRIGGER trigger_name -trigger_time -trigger_event -ON table_name -FOR EACH ROW -BEGIN - trigger_statements -END; -``` - -说明: - -- trigger_name:触发器名 -- trigger_time: 触发器的触发时机。取值为 `BEFORE` 或 `AFTER`。 -- trigger_event: 触发器的监听事件。取值为 `INSERT`、`UPDATE` 或 `DELETE`。 -- table_name: 触发器的监听目标。指定在哪张表上建立触发器。 -- FOR EACH ROW: 行级监视,Mysql 固定写法,其他 DBMS 不同。 -- trigger_statements: 触发器执行动作。是一条或多条 SQL 语句的列表,列表内的每条语句都必须用分号 `;` 来结尾。 - -创建触发器示例: - -```sql -DELIMITER $ -CREATE TRIGGER `trigger_insert_user` -AFTER INSERT ON `user` -FOR EACH ROW -BEGIN - INSERT INTO `user_history`(user_id, operate_type, operate_time) - VALUES (NEW.id, 'add a user', now()); -END $ -DELIMITER ; -``` - -查看触发器示例: - -```sql -SHOW TRIGGERS; -``` - -删除触发器示例: - -```sql -DROP TRIGGER IF EXISTS trigger_insert_user; -``` - -## 游标 - -> 游标(CURSOR)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 `SELECT` 语句,而是被该语句检索出来的结果集。在存储过程中使用游标可以对一个结果集进行移动遍历。 - -游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。 - -使用游标的步骤: - -1. **定义游标**:通过 `DECLARE cursor_name CURSOR FOR <语句>` 定义游标。这个过程没有实际检索出数据。 -2. **打开游标**:通过 `OPEN cursor_name` 打开游标。 -3. **取出数据**:通过 `FETCH cursor_name INTO var_name ...` 获取数据。 -4. **关闭游标**:通过 `CLOSE cursor_name` 关闭游标。 -5. **释放游标**:通过 `DEALLOCATE PREPARE` 释放游标。 - -游标使用示例: - -```sql -DELIMITER $ -CREATE PROCEDURE getTotal() -BEGIN - DECLARE total INT; - -- 创建接收游标数据的变量 - DECLARE sid INT; - DECLARE sname VARCHAR(10); - -- 创建总数变量 - DECLARE sage INT; - -- 创建结束标志变量 - DECLARE done INT DEFAULT false; - -- 创建游标 - DECLARE cur CURSOR FOR SELECT id,name,age from cursor_table where age>30; - -- 指定游标循环结束时的返回值 - DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true; - SET total = 0; - OPEN cur; - FETCH cur INTO sid, sname, sage; - WHILE(NOT done) - DO - SET total = total + 1; - FETCH cur INTO sid, sname, sage; - END WHILE; - - CLOSE cur; - SELECT total; -END $ -DELIMITER ; - --- 调用存储过程 -call getTotal(); -``` - -## 参考资料 - -- [《SQL 必知必会》](https://book.douban.com/subject/35167240/) -- [“浅入深出”MySQL 中事务的实现](https://draveness.me/mysql-transaction) -- [MySQL 的学习--触发器](https://www.cnblogs.com/CraryPrimitiveMan/p/4206942.html) -- [维基百科词条 - SQL](https://zh.wikipedia.org/wiki/SQL) -- [https://www.sitesbay.com/sql/index](https://www.sitesbay.com/sql/index) -- [SQL Subqueries](https://www.w3resource.com/sql/subqueries/understanding-sql-subqueries.php) -- [Quick breakdown of the types of joins](https://stackoverflow.com/questions/6294778/mysql-quick-breakdown-of-the-types-of-joins) -- [SQL UNION](https://www.w3resource.com/sql/sql-union.php) -- [SQL database security](https://www.w3resource.com/sql/database-security/create-users.php) -- [Mysql 中的存储过程](https://www.cnblogs.com/chenpi/p/5136483.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/03.\346\211\251\345\261\225SQL.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/03.\346\211\251\345\261\225SQL.md" deleted file mode 100644 index c6a9bbdfbd..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/03.\346\211\251\345\261\225SQL.md" +++ /dev/null @@ -1,86 +0,0 @@ ---- -title: 扩展 SQL -date: 2020-10-10 19:03:05 -order: 03 -categories: - - 数据库 - - 关系型数据库 - - 综合 -tags: - - 数据库 - - 关系型数据库 - - SQL -permalink: /pages/55e9a7/ ---- - -# 扩展 SQL - -## 数据库 - -## 表 - -### 查看表的基本信息 - -```sql -SELECT * FROM information_schema.tables -WHERE table_schema = 'test' AND table_name = 'user'; -``` - -### 查看表的列信息 - -```sql -SELECT * FROM information_schema.columns -WHERE table_schema = 'test' AND table_name = 'user'; -``` - -### 如何批量删除大量数据 - -如果要根据时间范围批量删除大量数据,最简单的语句如下: - -```sql -delete from orders -where timestamp < SUBDATE(CURDATE(),INTERVAL 3 month); -``` - -上面的语句,大概率执行会报错,提示删除失败,因为需要删除的数据量太大了,所以需要分批删除。 - -可以先通过一次查询,找到符合条件的历史订单中最大的那个订单 ID,然后在删除语句中把删除的条件转换成按主键删除。 - -```sql -select max(id) from orders -where timestamp < SUBDATE(CURDATE(),INTERVAL 3 month); - --- 分批删除,? 填上一条语句查到的最大 ID -delete from orders -where id <= ? -order by id limit 1000; -``` - -### 修改表的编码格式 - -utf8mb4 编码是 utf8 编码的超集,兼容 utf8,并且能存储 4 字节的表情字符。如果表的编码指定为 utf8,在保存 emoji 字段时会报错。 - -```sql -ALTER TABLE CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; -``` - -## 其他 - -### 显示哪些线程正在运行 - -```sql -mysql> show processlist; -+----+-----------------+-----------------+------+---------+-------+------------------------+------------------+ -| Id | User | Host | db | Command | Time | State | Info | -+----+-----------------+-----------------+------+---------+-------+------------------------+------------------+ -| 5 | event_scheduler | localhost | NULL | Daemon | 40230 | Waiting on empty queue | NULL | -| 10 | root | localhost:10120 | NULL | Query | 0 | init | show processlist | -+----+-----------------+-----------------+------+---------+-------+------------------------+------------------+ -2 rows in set (0.00 sec) -``` - -Mysql 连接完成后,如果你没有后续的动作,这个连接就处于空闲状态,你可以在 `show processlist` 命令中看到它。其中的 Command 列显示为“Sleep”的这一行,就表示现在系统里面有一个空闲连接。客户端如果太长时间没动静,连接器就会自动将它断开。这个时间是由参数 wait_timeout 控制的,默认值是 8 小时。 - -## 参考资料 - -- [《SQL 必知必会》](https://book.douban.com/subject/35167240/) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/99.SqlCheatSheet.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/99.SqlCheatSheet.md" deleted file mode 100644 index 2b1362fea0..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/99.SqlCheatSheet.md" +++ /dev/null @@ -1,193 +0,0 @@ ---- -title: SQL Cheat Sheet -date: 2022-07-16 14:17:08 -order: 99 -categories: - - 数据库 - - 关系型数据库 - - 综合 -tags: - - 数据库 - - 关系型数据库 - - SQL -permalink: /pages/e438a7/ ---- - -# SQL Cheat Sheet - -## 查找数据的查询 - -### **SELECT**: 用于从数据库中选择数据 - -- `SELECT` \* `FROM` table_name; - -### **DISTINCT**: 用于过滤掉重复的值并返回指定列的行 - -- `SELECT DISTINCT` column_name; - -### **WHERE**: 用于过滤记录/行 - -- `SELECT` column1, column2 `FROM` table_name `WHERE` condition; -- `SELECT` \* `FROM` table_name `WHERE` condition1 `AND` condition2; -- `SELECT` \* `FROM` table_name `WHERE` condition1 `OR` condition2; -- `SELECT` \* `FROM` table_name `WHERE NOT` condition; -- `SELECT` \* `FROM` table_name `WHERE` condition1 `AND` (condition2 `OR` condition3); -- `SELECT` \* `FROM` table_name `WHERE EXISTS` (`SELECT` column_name `FROM` table_name `WHERE` condition); - -### **ORDER BY**: 用于结果集的排序,升序(ASC)或者降序(DESC) - -- `SELECT` \* `FROM` table_name `ORDER BY` column; -- `SELECT` \* `FROM` table_name `ORDER BY` column `DESC`; -- `SELECT` \* `FROM` table_name `ORDER BY` column1 `ASC`, column2 `DESC`; - -### **SELECT TOP**: 用于指定从表顶部返回的记录数 - -- `SELECT TOP` number columns_names `FROM` table_name `WHERE` condition; -- `SELECT TOP` percent columns_names `FROM` table_name `WHERE` condition; -- 并非所有数据库系统都支持`SELECT TOP`。 MySQL 中是`LIMIT`子句 -- `SELECT` column_names `FROM` table_name `LIMIT` offset, count; - -### **LIKE**: 用于搜索列中的特定模式,WHERE 子句中使用的运算符 - -- % (percent sign) 是一个表示零个,一个或多个字符的通配符 -- \_ (underscore) 是一个表示单个字符通配符 -- `SELECT` column_names `FROM` table_name `WHERE` column_name `LIKE` pattern; -- `LIKE` ‘a%’ (查找任何以“a”开头的值) -- `LIKE` ‘%a’ (查找任何以“a”结尾的值) -- `LIKE` ‘%or%’ (查找任何包含“or”的值) -- `LIKE` ‘\_r%’ (查找任何第二位是“r”的值) -- `LIKE` ‘a*%*%’ (查找任何以“a”开头且长度至少为 3 的值) -- `LIKE` ‘[a-c]%’(查找任何以“a”或“b”或“c”开头的值) - -### **IN**: 用于在 WHERE 子句中指定多个值的运算符 - -- 本质上,IN 运算符是多个 OR 条件的简写 -- `SELECT` column_names `FROM` table_name `WHERE` column_name `IN` (value1, value2, …); -- `SELECT` column_names `FROM` table_name `WHERE` column_name `IN` (`SELECT STATEMENT`); - -### **BETWEEN**: 用于过滤给定范围的值的运算符 - -- `SELECT` column_names `FROM` table_name `WHERE` column_name `BETWEEN` value1 `AND` value2; -- `SELECT` \* `FROM` Products `WHERE` (column_name `BETWEEN` value1 `AND` value2) `AND NOT` column_name2 `IN` (value3, value4); -- `SELECT` \* `FROM` Products `WHERE` column_name `BETWEEN` #01/07/1999# AND #03/12/1999#; - -### **NULL**: 代表一个字段没有值 - -- `SELECT` \* `FROM` table_name `WHERE` column_name `IS NULL`; -- `SELECT` \* `FROM` table_name `WHERE` column_name `IS NOT NULL`; - -### **AS**: 用于给表或者列分配别名 - -- `SELECT` column_name `AS` alias_name `FROM` table_name; -- `SELECT` column_name `FROM` table_name `AS` alias_name; -- `SELECT` column_name `AS` alias_name1, column_name2 `AS` alias_name2; -- `SELECT` column_name1, column_name2 + ‘, ‘ + column_name3 `AS` alias_name; - -### **UNION**: 用于组合两个或者多个 SELECT 语句的结果集的运算符 - -- 每个 SELECT 语句必须拥有相同的列数 -- 列必须拥有相似的数据类型 -- 每个 SELECT 语句中的列也必须具有相同的顺序 -- `SELECT` columns_names `FROM` table1 `UNION SELECT` column_name `FROM` table2; -- `UNION` 仅允许选择不同的值, `UNION ALL` 允许重复 - -### **ANY|ALL**: 用于检查 WHERE 或 HAVING 子句中使用的子查询条件的运算符 - -- `ANY` 如果任何子查询值满足条件,则返回 true。 -- `ALL` 如果所有子查询值都满足条件,则返回 true。 -- `SELECT` columns_names `FROM` table1 `WHERE` column_name operator (`ANY`|`ALL`) (`SELECT` column_name `FROM` table_name `WHERE` condition); - -### **GROUP BY**: 通常与聚合函数(COUNT,MAX,MIN,SUM,AVG)一起使用,用于将结果集分组为一列或多列 - -- `SELECT` column_name1, COUNT(column_name2) `FROM` table_name `WHERE` condition `GROUP BY` column_name1 `ORDER BY` COUNT(column_name2) DESC; - -### **HAVING**: HAVING 子句指定 SELECT 语句应仅返回聚合值满足指定条件的行。它被添加到 SQL 语言中,因为 WHERE 关键字不能与聚合函数一起使用。 - -- `SELECT` `COUNT`(column_name1), column_name2 `FROM` table `GROUP BY` column_name2 `HAVING` `COUNT(`column_name1`)` > 5; - -## 修改数据的查询 - -### **INSERT INTO**: 用于在表中插入新记录/行 - -- `INSERT INTO` table_name (column1, column2) `VALUES` (value1, value2); -- `INSERT INTO` table_name `VALUES` (value1, value2 …); - -### **UPDATE**: 用于修改表中的现有记录/行 - -- `UPDATE` table_name `SET` column1 = value1, column2 = value2 `WHERE` condition; -- `UPDATE` table_name `SET` column_name = value; - -### **DELETE**: 用于删除表中的现有记录/行 - -- `DELETE FROM` table_name `WHERE` condition; -- `DELETE` \* `FROM` table_name; - -## 聚合查询 - -### **COUNT**: 返回出现次数 - -- `SELECT COUNT (DISTINCT` column_name`)`; - -### **MIN() and MAX()**: 返回所选列的最小/最大值 - -- `SELECT MIN (`column_names`) FROM` table_name `WHERE` condition; -- `SELECT MAX (`column_names`) FROM` table_name `WHERE` condition; - -### **AVG()**: 返回数字列的平均值 - -- `SELECT AVG (`column_name`) FROM` table_name `WHERE` condition; - -### **SUM()**: 返回数值列的总和 - -- `SELECT SUM (`column_name`) FROM` table_name `WHERE` condition; - -## 连接查询 - -### **INNER JOIN**: 内连接,返回在两张表中具有匹配值的记录 - -- `SELECT` column_names `FROM` table1 `INNER JOIN` table2 `ON` table1.column_name=table2.column_name; -- `SELECT` table1.column_name1, table2.column_name2, table3.column_name3 `FROM` ((table1 `INNER JOIN` table2 `ON` relationship) `INNER JOIN` table3 `ON` relationship); - -### **LEFT (OUTER) JOIN**: 左外连接,返回左表(table1)中的所有记录,以及右表中的匹配记录(table2) - -- `SELECT` column_names `FROM` table1 `LEFT JOIN` table2 `ON` table1.column_name=table2.column_name; - -### **RIGHT (OUTER) JOIN**: 右外连接,返回右表(table2)中的所有记录,以及左表(table1)中匹配的记录 - -- `SELECT` column_names `FROM` table1 `RIGHT JOIN` table2 `ON` table1.column_name=table2.column_name; - -### **FULL (OUTER) JOIN**: 全外连接,全连接是左右外连接的并集. 连接表包含被连接的表的所有记录, 如果缺少匹配的记录, 以 NULL 填充。 - -- `SELECT` column_names `FROM` table1 `FULL OUTER JOIN` table2 `ON` table1.column_name=table2.column_name; - -### **Self JOIN**: 自连接,表自身连接 - -- `SELECT` column_names `FROM` table1 T1, table1 T2 `WHERE` condition; - -## 视图查询 - -### **CREATE**: 创建视图 - -- `CREATE VIEW` view_name `AS SELECT` column1, column2 `FROM` table_name `WHERE` condition; - -### **SELECT**: 检索视图 - -- `SELECT` \* `FROM` view_name; - -### **DROP**: 删除视图 - -- `DROP VIEW` view_name; - -## 修改表的查询 - -### **ADD**: 添加字段 - -- `ALTER TABLE` table_name `ADD` column_name column_definition; - -### **MODIFY**: 修改字段数据类型 - -- `ALTER TABLE` table_name `MODIFY` column_name column_type; - -### **DROP**: 删除字段 - -- `ALTER TABLE` table_name `DROP COLUMN` column_name; \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/README.md" index c1a0b8fd3a..3e8c76e55d 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/README.md" @@ -8,7 +8,7 @@ categories: tags: - 数据库 - 关系型数据库 -permalink: /pages/22f2e3/ +permalink: /pages/853cc908/ hidden: true index: false --- @@ -17,13 +17,8 @@ index: false ## 📖 内容 -### [数据库系统概论](01.数据库系统概论.md) - -### [SQL 语法速成](02.SQL语法.md) - -### [扩展 SQL](03.扩展SQL.md) - -### [SQL Cheat Sheet](99.SqlCheatSheet.md) +- [关系数据库简介](关系数据库简介.md) +- [SQL 语法](SQL语法.md) ## 📚 资料 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/SQL\350\257\255\346\263\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/SQL\350\257\255\346\263\225.md" new file mode 100644 index 0000000000..f87b8d52a5 --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/SQL\350\257\255\346\263\225.md" @@ -0,0 +1,1866 @@ +--- +title: SQL 语法必知必会 +cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202410022047122.png +date: 2018-06-15 16:07:17 +order: 02 +categories: + - 数据库 + - 关系型数据库 + - 综合 +tags: + - 数据库 + - 关系型数据库 + - SQL +permalink: /pages/cd3ae5de/ +--- + +# SQL 语法必知必会 + +> 本文针对关系型数据库的基本语法。限于篇幅,本文侧重说明用法,不会展开讲解特性、原理。 +> +> 本文语法主要针对 Mysql,但大部分的语法对其他关系型数据库也适用。 + +## SQL 简介 + +### 数据库术语 + +- **数据库(database)** - 保存有组织的数据的容器(通常是一个文件或一组文件)。 +- **数据表(table)** - 某种特定类型数据的结构化清单。 +- **模式(schema)** - 关于数据库和表的布局及特性的信息。模式定义了数据在表中如何存储,包含存储什么样的数据,数据如何分解,各部分信息如何命名等信息。数据库和表都有模式。 +- **行(row)** - 表中的一条记录。 +- **列(column)** - 表中的一个字段。所有表都是由一个或多个列组成的。 +- **主键(primary key)** - 一列(或一组列),其值能够唯一标识表中每一行。 + +### SQL 语法 + +> SQL(Structured Query Language),标准 SQL 由 ANSI 标准委员会管理,从而称为 ANSI SQL。各个 DBMS 都有自己的实现,如 PL/SQL、Transact-SQL 等。 + +#### SQL 语法结构 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/database/mysql/sql-syntax.png) + +SQL 语法结构包括: + +- **子句** - 是语句和查询的组成成分。(在某些情况下,这些都是可选的。) +- **表达式** - 可以产生任何标量值,或由列和行的数据库表 +- **谓词** - 给需要评估的 SQL 三值逻辑(3VL)(true/false/unknown)或布尔真值指定条件,并限制语句和查询的效果,或改变程序流程。 +- **查询** - 基于特定条件检索数据。这是 SQL 的一个重要组成部分。 +- **语句** - 可以持久地影响纲要和数据,也可以控制数据库事务、程序流程、连接、会话或诊断。 + +#### SQL 语法要点 + +- **SQL 语句不区分大小写**,但是数据库表名、列名和值是否区分,依赖于具体的 DBMS 以及配置。 + +例如:`SELECT` 与 `select` 、`Select` 是相同的。 + +- **多条 SQL 语句必须以分号(`;`)分隔**。 + +- 处理 SQL 语句时,**所有空格都被忽略**。SQL 语句可以写成一行,也可以分写为多行。 + +```sql +-- 一行 SQL 语句 +UPDATE user SET username='robot', password='robot' WHERE username = 'root'; + +-- 多行 SQL 语句 +UPDATE user +SET username='robot', password='robot' +WHERE username = 'root'; +``` + +- SQL 支持三种注释 + +```sql +SELECT prod_name -- 这是一条注释 +FROM Products; + +# 这是一条注释 +SELECT prod_name +FROM Products; + +/* SELECT prod_name, vend_id +FROM Products; */ +SELECT prod_name +FROM Products; +``` + +#### SQL 分类 + +- DDL - **DDL**,英文叫做 Data Definition Language,即**“数据定义语言”**。 + - **DDL 用于定义数据库对象**。 + - DDL 定义操作包括创建(`CREATE`)、删除(`DROP`)、修改(`ALTER`);而被操作的对象包括:数据库、数据表和列、视图、索引。 +- DML - **DML**,英文叫做 Data Manipulation Language,即**“数据操作语言”**。 + - **DML 用于访问数据库的数据**。 + - DML 访问操作包括插入(`INSERT`)、删除(`DELETE`)、修改(`UPDATE`)、查询(`SELECT`)。这四个指令合称 **CRUD**,英文单词为 Create, Read, Update, Delete,即增删改查。 +- TCL - **TCL**,英文叫做 Transaction Control Language,即**“事务控制语言”**。 + - **TCL 用于管理数据库中的事务**,实际上就是用于管理由 DML 语句所产生的数据变更,它还允许将语句分组为逻辑事务。 + - TCL 的核心指令是 `COMMIT`、`ROLLBACK`。 +- DCL - **DCL**,英文叫做 Data Control Language,即**“数据控制语言”**。 + - **DCL 用于对数据访问权限进行控制**,它可以控制特定用户账户对数据表、查看表、预存程序、用户自定义函数等数据库对象的控制权。 + - DCL 的核心指令是 `GRANT`、`REVOKE`。 + - DCL 以**控制用户的访问权限**为主,因此其指令作法并不复杂,可利用 DCL 控制的权限有:`CONNECT`、`SELECT`、`INSERT`、`UPDATE`、`DELETE`、`EXECUTE`、`USAGE`、`REFERENCES`。 + - 根据不同的 DBMS 以及不同的安全性实体,其支持的权限控制也有所不同。 + +## 数据定义(CREATE、ALTER、DROP) + +DDL 的主要功能是定义数据库对象(如:数据库、数据表、视图、索引等)。 + +### 数据库(DATABASE) + +以下为数据库定义示例: + +::: tabs#数据库定义 + +@tab 创建数据库 + +```sql +CREATE DATABASE IF NOT EXISTS db_tutorial; +``` + +@tab 删除数据库 + +```sql +DROP DATABASE IF EXISTS db_tutorial; +``` + +@tab 选择数据库 + +```sql +USE db_tutorial; +``` + +::: + +### 数据表(TABLE) + +以下为数据表定义示例: + +::: tabs#数据表定义 + +@tab 创建数据表 + +利用 `CREATE TABLE` 创建表,必须给出下列信息: + +- 新表的名字,在关键字 `CREATE TABLE` 之后给出; +- 表列的名字和定义,用逗号分隔; +- 有的 DBMS 还要求指定表的位置。 + +```sql +CREATE TABLE user ( + id INT(10) UNSIGNED NOT NULL COMMENT 'Id', + username VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '用户名', + password VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '密码', + email VARCHAR(64) NOT NULL DEFAULT 'default' COMMENT '邮箱' +) COMMENT ='用户表'; +``` + +@tab 删除数据表 + +```sql +DROP TABLE IF EXISTS user; +DROP TABLE CustCopy; +``` + +@tab 复制表 + +```sql +CREATE TABLE vip_user AS +SELECT * FROM user; +``` + +@tab 数据表添加列 + +```sql +ALTER TABLE user +ADD age int(3); +``` + +@tab 数据表删除列 + +```sql +ALTER TABLE user +DROP COLUMN age; +``` + +@tab 数据表修改列 + +```sql +ALTER TABLE user +MODIFY COLUMN age tinyint; +``` + +@tab 修改表的编码格式 + +utf8mb4 编码是 utf8 编码的超集,兼容 utf8,并且能存储 4 字节的表情字符。如果表的编码指定为 utf8,在保存 emoji 字段时会报错。 + +```sql +ALTER TABLE user CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; +``` + +::: + +以下为数据表信息查看示例: + +::: tabs#数据表查看 + +@tab 查看表的基本信息 + +```sql +SELECT * FROM information_schema.tables +WHERE table_schema = 'test' AND table_name = 'user'; +``` + +@tab 查看表的列信息 + +```sql +SELECT * FROM information_schema.columns +WHERE table_schema = 'test' AND table_name = 'user'; +``` + +::: + +### 视图(VIEW) + +**“视图”是基于 SQL 语句的结果集的可视化的表**。视图是虚拟的表,本身不存储数据,也就不能对其进行索引操作。对视图的操作和对普通表的操作一样。 + +视图的作用: + +- 简化复杂的 SQL 操作,比如复杂的连接。 +- 只使用实际表的一部分数据。 +- 通过只给用户访问视图的权限,保证数据的安全性。 +- 更改数据格式和表示。 + +以下为视图定义示例: + +::: tabs#视图定义 + +@tab 创建视图 + +创建一个名为 ProductCustomers 的视图,它联结三个表,返回已订购了任意产品的所有顾客的列表。 + +```sql +CREATE VIEW ProductCustomers AS +SELECT cust_name, cust_contact, prod_id +FROM Customers, Orders, OrderItems +WHERE Customers.cust_id = Orders.cust_id +AND OrderItems.order_num = Orders.order_num; +``` + +检索订购了产品 RGAN01 的顾客 + +```sql +SELECT cust_name, cust_contact +FROM ProductCustomers +WHERE prod_id = 'RGAN01'; +``` + +@tab 删除视图 + +```sql +DROP VIEW top_10_user_view; +``` + +::: + +### 索引(INDEX) + +**“索引”是数据库为了提高查找效率的一种数据结构**。 + +日常生活中,我们可以通过检索目录,来快速定位书本中的内容。索引和数据表,就好比目录和书,想要高效查询数据表,索引至关重要。在数据量小且负载较低时,不恰当的索引对于性能的影响可能还不明显;但随着数据量逐渐增大,性能则会急剧下降。因此,**设置合理的索引是数据库查询性能优化的最有效手段**。 + +更新一个包含索引的表需要比更新一个没有索引的表花费更多的时间,这是由于索引本身也需要更新。因此,理想的做法是仅仅在常常被搜索的列(以及表)上面创建索引。 + +“唯一索引”表明此索引的每一个索引值只对应唯一的数据记录。 + +以下为视图定义示例: + +::: tabs#索引定义 + +@tab 创建索引 + +```sql +CREATE INDEX idx_email ON user(email); +``` + +@tab 创建唯一索引 + +```sql +CREATE UNIQUE INDEX uniq_name ON user(name); +``` + +@tab 删除索引 + +```sql +ALTER TABLE user DROP INDEX idx_email; +ALTER TABLE user DROP INDEX uniq_name; +``` + +@tab 添加主键 + +```sql +ALTER TABLE user ADD PRIMARY KEY (id); +``` + +@tab 删除主键 + +```sql +ALTER TABLE user DROP PRIMARY KEY; +``` + +::: + +### 约束(CONSTRAINT) + +约束(constraint)管理如何插入或处理数据库数据的规则。 + +如果存在违反约束的数据行为,行为会被约束终止。约束可以在创建表时规定(通过 `CREATE TABLE` 语句),或者在表创建之后规定(通过 `ALTER TABLE` 语句)。 + +定义约束的语法: + +```sql +CREATE TABLE table_name ( + column_name1 data_type(size) constraint_name, + column_name2 data_type(size) constraint_name, + column_name3 data_type(size) constraint_name, + .... +); +``` + +约束类型 + +- `NOT NULL` - 指示字段不能存储 `NULL` 值。 +- `UNIQUE KEY` - 保证字段的每行必须有唯一的值。 +- `PRIMARY KEY` - PRIMARY KEY 的作用是唯一标识一条记录,不能重复,不能为空,即相当于 `NOT NULL` + `UNIQUE`。确保字段(或两个列多个列的结合)有唯一标识,有助于更容易更快速地找到表中的一个特定的记录。 +- `FOREIGN KEY` - 保证一个表中的数据匹配另一个表中的值的参照完整性。 +- `CHECK` - 用于检查字段取值范围的有效性。 +- `DEFAULT` - 表明字段的默认值。如果插入数据时,该字段没有赋值,就会被设置为默认值。 + +以下为约束定义示例: + +::: tabs#约束定义 + +@tab NOT NULL + +```sql +CREATE TABLE demo ( + id INT UNSIGNED NOT NULL +); +``` + +@tab UNIQUE KEY + +```sql +CREATE TABLE demo2 ( + id INT UNSIGNED NOT NULL, + name VARCHAR(50) NOT NULL UNIQUE KEY +); +``` + +@tab PRIMARY KEY + +```sql +CREATE TABLE demo3 ( + id INT UNSIGNED NOT NULL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE KEY +); +``` + +@tab FOREIGN KEY + +```sql +CREATE TABLE demo4 ( + id INT UNSIGNED NOT NULL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE KEY, + fid INT UNSIGNED, + FOREIGN KEY (fid) REFERENCES demo3(id) +); +``` + +@tab CHECK + +```sql +CREATE TABLE demo5 ( + id INT UNSIGNED NOT NULL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE KEY, + age INT CHECK (age > 0) +); +``` + +@tab DEFAULT + +```sql +CREATE TABLE demo6 ( + id INT UNSIGNED NOT NULL PRIMARY KEY, + name VARCHAR(50) NOT NULL UNIQUE KEY, + age INT DEFAULT 0 +); +``` + +::: + +## 增删改查(CRUD) + +增删改查,又称为 **`CRUD`**,是数据库基本操作中的基本操作。 + +### 插入数据(INSERT) + +`INSERT INTO` 语句用于向表中插入新记录。 + +以下为插入数据示例: + +::: tabs#插入数据 + +@tab 插入完整的行 + +```sql +-- 下面两条 SQL 等价 +INSERT INTO Customers +VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA', NULL, NULL); + +INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email) +VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY','11111', 'USA', NULL, NULL); +``` + +@tab 插入行的一部分 + +```sql +INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country) +VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA'); +``` + +@tab 插入查询出来的数据 + +```sql +INSERT INTO Customers(cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country) +SELECT cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country +FROM CustNew; +``` + +@tab 从一个表复制到另一个表 + +```sql +SELECT * +INTO CustCopy +FROM Customers; + +-- MariaDB、MySQL、Oracle、PostgreSQL 和 SQLite +CREATE TABLE CustCopy AS +SELECT * FROM Customers; +``` + +::: + +### 更新数据(UPDATE) + +`UPDATE` 语句用于更新表中的记录。 + +::: tabs#更新数据 + +@tab 更新单列 + +更新客户 1000000005 的电子邮件地址 + +```sql +UPDATE Customers +SET cust_email = 'kim@thetoystore.com' +WHERE cust_id = '1000000005'; +``` + +@tab 更新多列 + +```sql +UPDATE customers +SET cust_contact = 'Sam Roberts', cust_email = 'sam@toyland.com' +WHERE cust_id = '1000000006'; +``` + +@tab 从表中删除特定的行 + +```sql +DELETE FROM Customers +WHERE cust_id = '1000000006'; +``` + +::: + +### 删除数据(DELETE) + +- `DELETE` 语句用于删除表中的记录。 +- `TRUNCATE TABLE` 可以清空表,也就是删除所有行。 + +以下为删除数据示例: + +::: tabs#删除数据 + +@tab 删除表中的指定数据 + +```sql +DELETE FROM user WHERE username = 'robot'; +``` + +@tab 清空表中的数据 + +```sql +TRUNCATE TABLE user; +``` + +@tab 批量删除大量数据 + +如果要根据时间范围批量删除大量数据,最简单的语句如下: + +```sql +DELETE FROM order +WHERE timestamp < SUBDATE(CURDATE(), INTERVAL 3 MONTH); +``` + +上面的语句,大概率执行会报错,提示删除失败,因为需要删除的数据量太大了,所以需要分批删除。 + +可以先通过一次查询,找到符合条件的历史订单中最大的那个订单 ID,然后在删除语句中把删除的条件转换成按主键删除。 + +```sql +SELECT max(id) FROM order +WHERE timestamp < SUBDATE(CURDATE(), INTERVAL 3 MONTH); + +-- 分批删除,? 填上一条语句查到的最大 ID +DELETE FROM order +WHERE id <= ? ORDER BY id LIMIT 1000; +``` + +::: + +### 查询数据(SELECT) + +- `SELECT` 语句用于从数据库中查询数据。 +- `DISTINCT` 用于返回唯一不同的值。它作用于所有列,也就是说所有列的值都相同才算相同。 +- `LIMIT` 限制返回的行数。可以有两个参数,第一个参数为起始行,从 0 开始;第二个参数为返回的总行数。 + - `ASC` :升序(默认) + - `DESC` :降序 + +#### SELECT 的用法 + +以下为查询数据示例: + +::: tabs#删除数据 + +@tab 查询单列 + +```sql +SELECT prod_name +FROM Products; +``` + +@tab 查询多列 + +```sql +SELECT prod_id, prod_name, prod_price +FROM Products; +``` + +@tab 查询所有列 + +```sql +SELECT * +FROM Products; +``` + +@tab 查询去重 + +```sql +SELECT DISTINCT vend_id +FROM Products; +``` + +@tab 限制查询数量 + +```sql +-- SQL Server 和 Access +SELECT TOP 5 prod_name +FROM Products; + +-- DB2 +SELECT prod_name +FROM Products +FETCH FIRST 5 ROWS ONLY; + +-- Oracle +SELECT prod_name +FROM Products +WHERE ROWNUM <=5; + +-- MySQL、MariaDB、PostgreSQL 或者 SQLite +SELECT prod_name +FROM Products +LIMIT 5; +-- 检索从第 5 行起的 5 行数据 +SELECT prod_name +FROM Products +LIMIT 5 OFFSET 5; +-- MySQL 和 MariaDB 中,上面的示例可以简化如下 +SELECT prod_name +FROM Products +LIMIT 5, 5; +``` + +::: + +#### SELECT 的执行顺序 + +关键字的顺序是不能颠倒的: + +```sql +SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... +``` + +SELECT 语句的执行顺序(在 MySQL 和 Oracle 中,SELECT 执行顺序基本相同): + +```sql +FROM > WHERE > GROUP BY > HAVING > SELECT 的字段 > DISTINCT > ORDER BY > LIMIT +``` + +比如你写了一个 SQL 语句,那么它的关键字顺序和执行顺序是下面这样的: + +```sql +SELECT DISTINCT player_id, player_name, count(*) as num -- 顺序 5 +FROM player JOIN team ON player.team_id = team.team_id -- 顺序 1 +WHERE height > 1.80 -- 顺序 2 +GROUP BY player.team_id -- 顺序 3 +HAVING num > 2 -- 顺序 4 +ORDER BY num DESC -- 顺序 6 +LIMIT 2 -- 顺序 7 +``` + +## 过滤数据(WHERE) + +数据库表一般包含大量的数据,很少需要检索表中的所有行。通常只会根据特定操作或报告的需要提取表数据的子集。只检索所需数据需要指 定搜索条件(search criteria),搜索条件也称为过滤条件(filter condition)。 + +### WHERE + +在 SQL 语句中,数据根据 `WHERE` 子句中指定的搜索条件进行过滤。 + +`WHERE` 子句的基本格式如下: + +```sql +SELECT ……(列名) FROM ……(表名) WHERE ……(子句条件) +``` + +`WHERE` 的常见用法: + +```sql +SELECT column1, column2 FROM table_name WHERE condition; +SELECT * FROM table_name WHERE condition1 AND condition2; +SELECT * FROM table_name WHERE condition1 OR condition2; +SELECT * FROM table_name WHERE NOT condition; +SELECT * FROM table_name WHERE condition1 AND (condition2 OR condition3); +SELECT * FROM table_name WHERE EXISTS (SELECT column_name FROM table_name WHERE condition) +``` + +`WHERE` 可以与 `SELECT`,`UPDATE` 和 `DELETE` 一起使用。 + +::: tabs#WHERE 示例 + +@tab `SELECT` 语句中的 `WHERE` 子句 + +检索所有价格小于 10 美元的产品。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price < 10; +``` + +检索所有不是供应商 DLL01 制造的产品 + +```sql +-- 下面两条查询语句作用相同 + +SELECT vend_id, prod_name +FROM Products +WHERE vend_id <> 'DLL01'; + +SELECT vend_id, prod_name +FROM Products +WHERE vend_id != 'DLL01'; +``` + +检索价格在 5 美元和 10 美元之间的所有产品 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price BETWEEN 5 AND 10; +``` + +检索所有没有邮件地址的顾客 + +```sql +SELECT cust_name +FROM CUSTOMERS +WHERE cust_email IS NULL; +``` + +@tab `UPDATE` 语句中的 `WHERE` 子句 + +```sql +UPDATE Customers +SET cust_name = 'Jack Jones' +WHERE cust_name = 'Kids Place'; +``` + +@tab `DELETE` 语句中的 `WHERE` 子句 + +```sql +DELETE FROM Customers +WHERE cust_name = 'Kids Place'; +``` + +::: + +### 比较操作符 + +| 操作符 | 描述 | +| ------- | ------------------------------------------------------ | +| `=` | 等于 | +| `<>` | 不等于。注释:在 SQL 的一些版本中,该操作符可被写成 != | +| `>` | 大于 | +| `<` | 小于 | +| `>=` | 大于等于 | +| `<=` | 小于等于 | +| IS NULL | 是否为空 | + +【示例】查询所有价格小于 10 美元的产品 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price < 10; +``` + +【示例】查询所有不是供应商 DLL01 制造的产品 + +```sql +SELECT vend_id, prod_name +FROM Products +WHERE vend_id != 'DLL01'; +``` + +【示例】查询邮件地址为空的客户 + +```sql +SELECT cust_name +FROM CUSTOMERS +WHERE cust_email IS NULL; +``` + +### 范围操作符 + +| 操作符 | 描述 | +| --------- | -------------------------- | +| `BETWEEN` | 在某个范围内 | +| `IN` | 指定针对某个列的多个可能值 | + +`BETWEEN` 操作符在 `WHERE` 子句中使用,作用是选取介于某个范围内的值。 + +`IN` 操作符用来指定条件范围,范围中的每个条件都可以进行匹配。`IN` 取一组由逗号分隔、括在圆括号中的合法值。 + +为什么要使用 IN 操作符?其优点如下。 + +- 在有很多合法选项时,IN 操作符的语法更清楚,更直观。 +- 在与其他 AND 和 OR 操作符组合使用 IN 时,求值顺序更容易管理。 +- IN 操作符一般比一组 OR 操作符执行得更快(在上面这个合法选项很 少的例子中,你看不出性能差异)。 +- IN 的最大优点是可以包含其他 SELECT 语句,能够更动态地建立 WHERE 子句。 + +以下为范围操作符使用示例: + +::: tabs#范围操作符 + +@tab IN 示例 + +下面两条 SQL 的语义等价: + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE vend_id IN ( 'DLL01', 'BRS01' ) +ORDER BY prod_name; + +SELECT prod_name, prod_price +FROM Products +WHERE vend_id = 'DLL01' OR vend_id = 'BRS01' +ORDER BY prod_name; +``` + +@tab BETWEEN 示例 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price BETWEEN 5 AND 10; +``` + +::: + +### 逻辑操作符 + +| 操作符 | 描述 | +| ------ | ---------- | +| `AND` | 并且(与) | +| `OR` | 或者(或) | +| `NOT` | 否定(非) | + +`AND`、`OR`、`NOT` 是用于对过滤条件的逻辑处理指令。 + +- `AND` 优先级高于 `OR`,为了明确处理顺序,可以使用 `()`。`AND` 操作符表示左右条件都要满足。 +- `OR` 操作符表示左右条件满足任意一个即可。 + +- `NOT` 操作符用于否定其后条件。 + +以下为逻辑操作符使用示例: + +::: tabs#逻辑操作符 + +@tab `AND` 示例 + +检索由供应商 DLL01 制造且价格小于等于 4 美元的所有产品的名称和价格 + +```sql +SELECT prod_id, prod_price, prod_name +FROM Products +WHERE vend_id = 'DLL01' AND prod_price <= 4; +``` + +@tab `OR` 示例 + +检索由供应商 DLL01 或供应商 BRS01 制造的所有产品的名称和价格 + +```sql +SELECT prod_id, prod_price, prod_name +FROM Products +WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'; +``` + +@tab NOT 示例 + +检索除 DLL01 之外的所有供应商制造的产品 + +```sql +SELECT prod_name +FROM Products +WHERE NOT vend_id = 'DLL01' +ORDER BY prod_name; +``` + +和下面的示例作用相同 + +```sql +SELECT prod_name +FROM Products +WHERE vend_id <> 'DLL01' +ORDER BY prod_name; +``` + +@tab `AND` 和 `OR` 优先级示例 + +SQL 在处理 `OR` 操作符前,优先处理 `AND` 操作符。 + +下面的示例中,SQL 会理解为由供应商 BRS01 制造的价格为 10 美元以上的所有产品,以及由供应商 DLL01 制造的所有产品,而不管其价格如何。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE vend_id = 'DLL01' OR vend_id = 'BRS01' +AND prod_price >= 10; +``` + +任何时候使用具有 AND 和 OR 操作符的 WHERE 子句,都应该使用圆括号明确地分组操作符。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE (vend_id = 'DLL01' OR vend_id = 'BRS01') +AND prod_price >= 10; +``` + +::: + +### 通配符 + +`LIKE` 操作符在 `WHERE` 子句中使用,作用是确定字符串是否匹配模式。只有字段是文本值时才使用 `LIKE`。**不要滥用通配符,通配符位于开头处匹配会非常慢**。 + +`LIKE` 支持以下通配符匹配选项: + +- `%` 表示任何字符出现任意次数。 +- `_` 表示任何字符出现一次。 +- `[]` 必须匹配指定位置的一个字符。 + +> 说明:并不是所有 DBMS 都支持 `[]`。只有微软的 Access 和 SQL Server 支持 `[]`。 + +以下为通配符使用示例: + +::: tabs#逻辑操作符 + +@tab `%` 示例 + +检索所有产品名以 Fish 开头的产品 + +```sql +SELECT prod_id, prod_name +FROM Products +WHERE prod_name LIKE 'Fish%'; +``` + +检索产品名中包含 bean bag 的产品 + +```sql +SELECT prod_id, prod_name +FROM Products +WHERE prod_name LIKE '%bean bag%'; +``` + +检索产品名中以 F 开头,y 结尾的产品 + +```sql +SELECT prod_name +FROM Products +WHERE prod_name LIKE 'F%y'; +``` + +@tab `_` 示例 + +```sql +SELECT * FROM Products +WHERE prod_name LIKE '__ inch teddy bear'; +``` + +@tab `[]` 示例 + +找出所有名字以 J 或 M 开头的联系人: + +```sql +SELECT cust_contact +FROM Customers +WHERE cust_contact LIKE '[JM]%' +ORDER BY cust_contact; +``` + +::: + +### 子查询 + +子查询(subquery),即嵌套在其他查询中的查询。 + +子查询可以分为关联子查询和非关联子查询。 + +- 子查询从数据表中查询了数据结果,如果这个数据结果只执行一次,然后这个数据结果作为主查询的条件进行执行,那么这样的子查询叫做**非关联子查询**。 + +- 如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部,这种嵌套的执行方式就称为**关联子查询**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/database/mysql/sql-subqueries.gif) + +假如需要列出订购物品 RGAN01 的所有顾客,应该怎样检索?下面列出具体的步骤。 + +(1) 检索包含物品 RGAN01 的所有订单的编号。 + +```sql +SELECT order_num +FROM OrderItems +WHERE prod_id = 'RGAN01'; +``` + +输出 + +```text +order_num +----------- +20007 +20008 +``` + +(2) 检索具有前一步骤列出的订单编号的所有顾客的 ID。 + +```sql +SELECT cust_id +FROM Orders +WHERE order_num IN (20007,20008); +``` + +输出 + +```text +cust_id +---------- +1000000004 +1000000005 +``` + +(3) 检索前一步骤返回的所有顾客 ID 的顾客信息。 + +```sql +SELECT cust_name, cust_contact +FROM Customers +WHERE cust_id IN ('1000000004','1000000005'); +``` + +现在,结合这两个查询,把第一个查询(返回订单号的那一个)变为子查询。 + +```sql +SELECT cust_id +FROM orders +WHERE order_num IN (SELECT order_num + FROM orderitems + WHERE prod_id = 'RGAN01'); +``` + +再进一步结合第三个查询 + +```sql +SELECT cust_name, cust_contact +FROM customers +WHERE cust_id IN (SELECT cust_id + FROM orders + WHERE order_num IN (SELECT order_num + FROM orderitems + WHERE prod_id = 'RGAN01')); +``` + +## 排序和分组 + +### ORDER BY + +`ORDER BY` 用于对结果集进行排序。`ORDER BY` 子句取一个或多个列的名字,据此对输出进行排序。`ORDER BY` 支持两种排序方式: + +- `ASC` :升序(默认) +- `DESC` :降序 + +单列排序示例: + +```sql +SELECT prod_name +FROM Products +ORDER BY prod_name; +``` + +可以按多个列进行排序,并且为每个列指定不同的排序方式。 + +多列排序示例: + +```sql +SELECT * FROM Products +ORDER BY prod_price DESC, prod_name ASC; +``` + +按列位置排序(不推荐): + +```sql +SELECT prod_id, prod_price, prod_name +FROM Products +ORDER BY 2, 3; +``` + +### GROUP BY + +`GROUP BY` 子句将记录分组到汇总行中,`GROUP BY` 为每个组返回一个记录。 + +GROUP BY 要点: + +- GROUP BY 子句可以包含任意数目的列,因而可以对分组进行嵌套,更细致地进行数据分组。 +- 如果在 GROUP BY 子句中嵌套了分组,数据将在最后指定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。 +- GROUP BY 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 SELECT 中使用表达式,则必须在 GROUP BY 子句中指定相同的表达式。不能使用别名。 +- 大多数 SQL 实现不允许 GROUP BY 列带有长度可变的数据类型(如文本或备注型字段)。 +- 除聚集计算语句外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出。 +- 如果分组列中包含具有 NULL 值的行,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。 +- GROUP BY 子句必须出现在 WHERE 子句之后,ORDER BY 子句之前。 + +分组示例: + +```sql +SELECT cust_name, COUNT(cust_address) AS addr_num +FROM Customers GROUP BY cust_name; +``` + +分组后排序示例: + +```sql +SELECT cust_name, COUNT(cust_address) AS addr_num +FROM Customers GROUP BY cust_name +ORDER BY cust_name DESC; +``` + +### HAVING + +`HAVING` 用于对汇总的 `GROUP BY` 结果进行过滤。`HAVING` 要求存在一个 `GROUP BY` 子句。 + +`WHERE` 和 `HAVING` 可以在相同的查询中。 + +`HAVING` vs `WHERE`: + +- `HAVING` 非常类似于 `WHERE`。`WHERE` 和 `HAVING` 都是用于过滤。 +- `WHERE` 过滤行,而 `HAVING` 过滤分组。 + +使用 `WHERE` 和 `HAVING` 过滤数据示例: + +过滤两个以上订单的分组 + +```sql +SELECT cust_id, COUNT(*) AS orders +FROM Orders +GROUP BY cust_id +HAVING COUNT(*) >= 2; +``` + +列出具有两个以上产品且其价格大于等于 4 的供应商: + +```sql +SELECT vend_id, COUNT(*) AS num_prods +FROM Products +WHERE prod_price >= 4 +GROUP BY vend_id +HAVING COUNT(*) >= 2; +``` + +检索包含三个或更多物品的订单号和订购物品的数目: + +```sql +SELECT order_num, COUNT(*) AS items +FROM orderitems +GROUP BY order_num +HAVING COUNT(*) >= 3; +``` + +要按订购物品的数目排序输出,需要添加 ORDER BY 子句 + +```sql +SELECT order_num, COUNT(*) AS items +FROM orderitems +GROUP BY order_num +HAVING COUNT(*) >= 3 +ORDER BY items, order_num; +``` + +## 联结和组合 + +### 联结(JOIN) + +**在 SELECT, UPDATE 和 DELETE 语句中,“联结”可以用于联合多表查询。联结使用 `JOIN` 关键字,并且条件语句使用 `ON` 而不是 `WHERE`**。 + +**联结可以替换子查询,并且一般比子查询的效率更快**。 + +`JOIN` 有以下类型: + +- **内联结** - 内联结又称等值联结,用于获取两个表中字段匹配关系的记录,**使用 `INNER JOIN` 关键字**。在没有条件语句的情况下**返回笛卡尔积**。 + - **笛卡尔积** - “笛卡尔积”也称为交叉联结(`CROSS JOIN`)。由没有联结条件的表关系返回的结果为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。 + - **自联结(=)** - “自联结(=)”可以看成内联结的一种,只是联结的表是自身而已。 + - **自然联结(NATURAL JOIN)** - “自然联结”会自动联结所有同名列。自然联结使用 `NATURAL JOIN` 关键字。 +- **外联结** + - **左联结(LEFT JOIN)** - “左外联结”会获取左表所有记录,即使右表没有对应匹配的记录。左外联结使用 `LEFT JOIN` 关键字。 + - **右联结(RIGHT JOIN)** - “右外联结”会获取右表所有记录,即使左表没有对应匹配的记录。右外联结使用 `RIGHT JOIN` 关键字。 + +
    + sql-join +
    +#### 内联结(INNER JOIN) + +内联结又称等值联结,用于获取两个表中字段匹配关系的记录,**使用 `INNER JOIN` 关键字**。在没有条件语句的情况下**返回笛卡尔积**。 + +```sql +SELECT vend_name, prod_name, prod_price +FROM vendors INNER JOIN products +ON vendors.vend_id = products.vend_id; + +-- 也可以省略 INNER 使用 JOIN,与上面一句效果一样 +SELECT vend_name, prod_name, prod_price +FROM vendors JOIN products +ON vendors.vend_id = products.vend_id; +``` + +##### 笛卡尔积 + +**“笛卡尔积”也称为交叉联结(`CROSS JOIN`),它的作用就是可以把任意表进行联结,即使这两张表不相关**。但通常进行联结还是需要筛选的,因此需要在联结后面加上 `WHERE` 子句,也就是作为过滤条件对联结数据进行筛选。 + +笛卡尔积是一个数学运算。假设我有两个集合 X 和 Y,那么 X 和 Y 的笛卡尔积就是 X 和 Y 的所有可能组合,也就是第一个对象来自于 X,第二个对象来自于 Y 的所有可能。 + +【示例】求 t1 和 t2 两张表的笛卡尔积 + +```sql +-- 以下两条 SQL,执行结果相同 +SELECT * FROM t1, t2; +SELECT * FROM t1 CROSS JOIN t2; +``` + +##### 自联结(=) + +**“自联结”可以看成内联结的一种,只是联结的表是自身而已**。 + +给与 Jim Jones 同一公司的所有顾客发送一封信件: + +```sql +-- 子查询方式 +SELECT cust_id, cust_name, cust_contact +FROM customers +WHERE cust_name = (SELECT cust_name + FROM customers + WHERE cust_contact = 'Jim Jones'); + +-- 自联结方式 +SELECT c1.cust_id, c1.cust_name, c1.cust_contact +FROM customers AS c1, customers AS c2 +WHERE c1.cust_name = c2.cust_name AND c2.cust_contact = 'Jim Jones'; +``` + +##### 自然联结(NATURAL JOIN) + +**“自然联结”会自动联结所有同名列**。自然联结使用 `NATURAL JOIN` 关键字。 + +```sql +SELECT * +FROM Products +NATURAL JOIN Customers; +``` + +#### 外联结(OUTER JOIN) + +外联结返回一个表中的所有行,并且仅返回来自此表中满足联结条件的那些行,即两个表中的列是相等的。外联结分为左外联结、右外联结、全外联结(Mysql 不支持)。 + +##### 左联结(LEFT JOIN) + +**“左外联结”会获取左表所有记录,即使右表没有对应匹配的记录**。左外联结使用 `LEFT JOIN` 关键字。 + +```sql +SELECT customers.cust_id, orders.order_num +FROM customers LEFT JOIN orders +ON customers.cust_id = orders.cust_id; +``` + +##### 右联结(RIGHT JOIN) + +**“右外联结”会获取右表所有记录,即使左表没有对应匹配的记录**。右外联结使用 `RIGHT JOIN` 关键字。 + +```sql +SELECT customers.cust_id, orders.order_num +FROM customers RIGHT JOIN orders +ON customers.cust_id = orders.cust_id; +``` + +### 组合(UNION) + +`UNION` 运算符**将两个或更多查询的结果组合起来,并生成一个结果集**,其中包含来自 `UNION` 中参与查询的提取行。 + +`UNION` 基本规则: + +- 所有查询的列数和列顺序必须相同。 +- 每个查询中涉及表的列的数据类型必须相同或兼容。 +- 通常返回的列名取自第一个查询。 + +主要有两种情况需要使用组合查询: + +- 在一个查询中从不同的表返回结构数据; +- 对一个表执行多个查询,按一个查询返回数据。 + +把 Illinois、Indiana、Michigan 等州的缩写传递给 IN 子句,检索出这些州的所有行 + +```sql +SELECT cust_name, cust_contact, cust_email +FROM Customers +WHERE cust_state IN ('IL','IN','MI'); +``` + +找出所有 Fun4All + +```sql +SELECT cust_name, cust_contact, cust_email +FROM Customers +WHERE cust_name = 'Fun4All'; +``` + +组合这两条语句 + +```sql +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_state IN ('IL', 'IN', 'MI') +UNION +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_name = 'Fun4All'; +``` + +`UNION` 默认从查询结果集中自动去除了重复的行;如果想返回所有的匹配行,可使用 `UNION ALL`。 + +```sql +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_state IN ('IL', 'IN', 'MI') +UNION ALL +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_name = 'Fun4All'; +``` + +### JOIN vs UNION + +- `JOIN` 中联结表的列可能不同,但在 `UNION` 中,所有查询的列数和列顺序必须相同。 +- `UNION` 将查询之后的行放在一起(垂直放置),但 `JOIN` 将查询之后的列放在一起(水平放置),即它构成一个笛卡尔积。 + +## 函数 + +> 🔔 注意:不同数据库的函数往往各不相同,因此不可移植。本节主要以 Mysql 的函数为例。 + +### 字符串函数 + +| 函数 | 说明 | +| :------------------: | :--------------------: | +| `CONCAT()` | 合并字符串 | +| `LEFT()`、`RIGHT()` | 左边或者右边的字符 | +| `LOWER()`、`UPPER()` | 转换为小写或者大写 | +| `LTRIM()`、`RTIM()` | 去除左边或者右边的空格 | +| `LENGTH()` | 长度 | +| `SOUNDEX()` | 转换为语音值 | + +其中, **SOUNDEX()** 可以将一个字符串转换为描述其语音表示的字母数字模式。 + +以下为部分字符串函数的使用示例 + +拼接字符串值: + +```sql +-- Access 和 SQL Server +SELECT vend_name + ' (' + vend_country + ')' +FROM Vendors +ORDER BY vend_name; + +-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base +SELECT vend_name || ' (' || vend_country || ')' +FROM Vendors +ORDER BY vend_name; + +-- MySQL 或 MariaDB +SELECT Concat(vend_name, ' (', vend_country, ')') +FROM Vendors +ORDER BY vend_name; +``` + +去除字符串中的空格: + +```sql +-- Access 和 SQL Server +SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')' +FROM Vendors +ORDER BY vend_name; + +-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base +SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')' +FROM Vendors +ORDER BY vend_name; +``` + +### 时间函数 + +- 日期格式:`YYYY-MM-DD` +- 时间格式:`HH:MM:SS` + +| 函 数 | 说 明 | +| :--------------: | :----------------------------: | +| `ADDDATE()` | 增加一个日期(天、周等) | +| `ADDTIME()` | 增加一个时间(时、分等) | +| `CURRENT_DATE()` | 返回当前日期 | +| `CURRENT_TIME()` | 返回当前时间 | +| `DATE()` | 返回日期时间的日期部分 | +| `DATEDIFF()` | 计算两个日期之差 | +| `DATE_ADD()` | 高度灵活的日期运算函数 | +| `DATE_FORMAT()` | 返回一个格式化的日期或时间串 | +| `DAY()` | 返回一个日期的天数部分 | +| `DAYOFWEEK()` | 对于一个日期,返回对应的星期几 | +| `HOUR()` | 返回一个时间的小时部分 | +| `MINUTE()` | 返回一个时间的分钟部分 | +| `MONTH()` | 返回一个日期的月份部分 | +| `NOW()` | 返回当前日期和时间 | +| `SECOND()` | 返回一个时间的秒部分 | +| `TIME()` | 返回一个日期时间的时间部分 | +| `YEAR()` | 返回一个日期的年份部分 | + +部分日期和时间处理函数使用示例: + +```sql +-- SQL Server +SELECT order_num +FROM Orders +WHERE DATEPART(yy, order_date) = 2012; + +-- Access +SELECT order_num +FROM Orders +WHERE DATEPART('yyyy', order_date) = 2012; + +-- PostgreSQL +SELECT order_num +FROM Orders +WHERE DATE_PART('year', order_date) = 2012; + +-- Oracle +SELECT order_num +FROM Orders +WHERE to_number(to_char(order_date, 'YYYY')) = 2012; + +-- MySQL 和 MariaDB +SELECT order_num +FROM Orders +WHERE YEAR(order_date) = 2012; +``` + +### 数学函数 + +常见 Mysql 数学函数: + +| 函数 | 说明 | +| :------: | ------------------ | +| `ABS()` | 返回一个数的绝对值 | +| `COS()` | 返回一个角度的余弦 | +| `EXP()` | 返回一个数的指数值 | +| `PI()` | 返回圆周率 | +| `SIN()` | 返回一个角度的正弦 | +| `SQRT()` | 返回一个数的平方根 | +| `TAN()` | 返回一个角度的正切 | + +### 聚合函数 + +| 函 数 | 说 明 | +| :-------: | :--------------: | +| `AVG()` | 返回某列的平均值 | +| `COUNT()` | 返回某列的行数 | +| `MAX()` | 返回某列的最大值 | +| `MIN()` | 返回某列的最小值 | +| `SUM()` | 返回某列值之和 | + +`AVG()` 通过对表中行数计数并计算其列值之和,求得该列的平均值。 + +使用 DISTINCT 可以让汇总函数值汇总不同的值。 + +::: tabs#聚合函数示例 + +@tab `AVG()` 示例 + +使用 `AVG()` 返回 Products 表中所有产品的平均价格: + +```sql +SELECT AVG(prod_price) AS avg_price +FROM Products; +``` + +@tab `COUNT()` 示例 + +`COUNT()` 函数进行计数。可利用 `COUNT()` 确定表中行的数目或符合特定条件的行的数目。 + +返回 Customers 表中顾客的总数: + +```sql +SELECT COUNT(*) AS num_cust +FROM Customers; +``` + +只对具有电子邮件地址的客户计数: + +```sql +SELECT COUNT(cust_email) AS num_cust +FROM Customers; +``` + +@tab `MAX()` 示例 + +返回 Products 表中最贵物品的价格: + +```sql +SELECT MAX(prod_price) AS max_price +FROM Products; +``` + +@tab `MIN()` 示例 + +返回 Products 表中最便宜物品的价格 + +```sql +SELECT MIN(prod_price) AS min_price +FROM Products; +``` + +@tab `SUM()` 示例 + +返回订单中所有物品数量之和 + +```sql +SELECT SUM(quantity) AS items_ordered +FROM OrderItems +WHERE order_num = 20005; +``` + +::: + +### 转换函数 + +| 函 数 | 说 明 | 示例 | +| :------: | :----------: | -------------------------------------------------- | +| `CAST()` | 转换数据类型 | `SELECT CAST("2017-08-29" AS DATE); -> 2017-08-29` | + +## 事务 + +不能回退 `SELECT` 语句,回退 `SELECT` 语句也没意义;也不能回退 `CREATE` 和 `DROP` 语句。 + +**MySQL 默认采用隐式提交策略(`autocommit`)**,每执行一条语句就把这条语句当成一个事务然后进行提交。当出现 `START TRANSACTION` 语句时,会关闭隐式提交;当 `COMMIT` 或 `ROLLBACK` 语句执行后,事务会自动关闭,重新恢复隐式提交。 + +通过 `set autocommit=0` 可以取消自动提交,直到 `set autocommit=1` 才会提交;`autocommit` 标记是针对每个连接而不是针对服务器的。 + +事务处理指令: + +- `START TRANSACTION` - 指令用于标记事务的起始点。 +- `SAVEPOINT` - 指令用于创建保留点。 +- `ROLLBACK TO` - 指令用于回滚到指定的保留点;如果没有设置保留点,则回退到 `START TRANSACTION` 语句处。 +- `COMMIT` - 提交事务。 +- `RELEASE SAVEPOINT`:删除某个保存点。 +- `SET TRANSACTION`:设置事务的隔离级别。 + +事务处理示例: + +```sql +-- 开始事务 +START TRANSACTION; + +-- 插入操作 A +INSERT INTO `user` +VALUES (1, 'root1', 'root1', 'xxxx@163.com'); + +-- 创建保留点 updateA +SAVEPOINT updateA; + +-- 插入操作 B +INSERT INTO `user` +VALUES (2, 'root2', 'root2', 'xxxx@163.com'); + +-- 回滚到保留点 updateA +ROLLBACK TO updateA; + +-- 提交事务,只有操作 A 生效 +COMMIT; +``` + +### ACID + +### 事务隔离级别 + +--- + +**(以下为 DCL 语句用法)** + +## 权限控制 + +`GRANT` 和 `REVOKE` 可在几个层次上控制访问权限: + +- 整个服务器,使用 `GRANT ALL` 和 `REVOKE ALL`; +- 整个数据库,使用 ON database.\*; +- 特定的表,使用 ON database.table; +- 特定的列; +- 特定的存储过程。 + +新创建的账户没有任何权限。 + +账户用 `username@host` 的形式定义,`username@%` 使用的是默认主机名。 + +MySQL 的账户信息保存在 mysql 这个数据库中。 + +```sql +USE mysql; +SELECT user FROM user; +``` + +### 创建账户 + +```sql +CREATE USER myuser IDENTIFIED BY 'mypassword'; +``` + +### 修改账户名 + +```sql +UPDATE user SET user='newuser' WHERE user='myuser'; +FLUSH PRIVILEGES; +``` + +### 删除账户 + +```sql +DROP USER myuser; +``` + +### 查看权限 + +```sql +SHOW GRANTS FOR myuser; +``` + +### 授予权限 + +```sql +GRANT SELECT, INSERT ON *.* TO myuser; +``` + +### 删除权限 + +```sql +REVOKE SELECT, INSERT ON *.* FROM myuser; +``` + +### 更改密码 + +```sql +SET PASSWORD FOR myuser = 'mypass'; +``` + +## 存储过程 + +存储过程的英文是 Stored Procedure。它可以视为一组 SQL 语句的批处理。一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。 + +定义存储过程的语法格式: + +```sql +CREATE PROCEDURE 存储过程名称 ([参数列表]) +BEGIN + 需要执行的语句 +END +``` + +存储过程定义语句类型: + +- `CREATE PROCEDURE` 用于创建存储过程 +- `DROP PROCEDURE` 用于删除存储过程 +- `ALTER PROCEDURE` 用于修改存储过程 + +### 使用存储过程 + +创建存储过程的要点: + +- `DELIMITER` 用于定义语句的结束符 +- 存储过程的 3 种参数类型: + - `IN`:存储过程的入参 + - `OUT`:存储过程的出参 + - `INPUT`:既是存储过程的入参,也是存储过程的出参 +- 流控制语句: + - `BEGIN…END`:`BEGIN…END` 中间包含了多个语句,每个语句都以(`;`)号为结束符。 + - `DECLARE`:`DECLARE` 用来声明变量,使用的位置在于 `BEGIN…END` 语句中间,而且需要在其他语句使用之前进行变量的声明。 + - `SET`:赋值语句,用于对变量进行赋值。 + - `SELECT…INTO`:把从数据表中查询的结果存放到变量中,也就是为变量赋值。每次只能给一个变量赋值,不支持集合的操作。 + - `IF…THEN…ENDIF`:条件判断语句,可以在 `IF…THEN…ENDIF` 中使用 `ELSE` 和 `ELSEIF` 来进行条件判断。 + - `CASE`:`CASE` 语句用于多条件的分支判断。 + +创建存储过程示例: + +```sql +DROP PROCEDURE IF EXISTS `proc_adder`; +DELIMITER ;; +CREATE DEFINER=`root`@`localhost` PROCEDURE `proc_adder`(IN a int, IN b int, OUT sum int) +BEGIN + DECLARE c int; + if a is null then set a = 0; + end if; + + if b is null then set b = 0; + end if; + + set sum = a + b; +END +;; +DELIMITER ; +``` + +使用存储过程示例: + +```sql +set @b=5; +call proc_adder(2,@b,@s); +select @s as sum; +``` + +### 存储过程的利弊 + +存储过程的优点: + +- **执行效率高**:一次编译多次使用。 +- **安全性强**:在设定存储过程的时候可以设置对用户的使用权限,这样就和视图一样具有较强的安全性。 +- **可复用**:将代码封装,可以提高代码复用。 +- **性能好** + - 由于是预先编译,因此具有很高的性能。 + - 一个存储过程替代大量 T_SQL 语句 ,可以降低网络通信量,提高通信速率。 + +存储过程的缺点: + +- **可移植性差**:存储过程不能跨数据库移植。由于不同数据库的存储过程语法几乎都不一样,十分难以维护(不通用)。 +- **调试困难**:只有少数 DBMS 支持存储过程的调试。对于复杂的存储过程来说,开发和维护都不容易。 +- **版本管理困难**:比如数据表索引发生变化了,可能会导致存储过程失效。我们在开发软件的时候往往需要进行版本管理,但是存储过程本身没有版本控制,版本迭代更新的时候很麻烦。 +- **不适合高并发的场景**:高并发的场景需要减少数据库的压力,有时数据库会采用分库分表的方式,而且对可扩展性要求很高,在这种情况下,存储过程会变得难以维护,增加数据库的压力,显然就不适用了。 + +> _综上,存储过程的优缺点都非常突出,是否使用一定要慎重,需要根据具体应用场景来权衡_。 + +### 触发器 + +触发器是特殊的存储过程,它在特定的数据库活动发生时自动执行。触发器可以与特定表上的 INSERT、UPDATE 和 DELETE 操作(或组合)相关联。 + +触发器是一种与表操作有关的数据库对象,当触发器所在表上出现指定事件时,将调用该对象,即表的操作事件触发表上的触发器的执行。 + +触发器的一些常见用途 + +- 保证数据一致。例如,在 INSERT 或 UPDATE 操作中将所有州名转换为大写。 +- 基于某个表的变动在其他表上执行活动。例如,每当更新或删除一行时将审计跟踪记录写入某个日志表。 +- 进行额外的验证并根据需要回退数据。例如,保证某个顾客的可用资金不超限定,如果已经超出,则阻塞插入。 +- 计算计算列的值或更新时间戳。 + +#### 触发器特性 + +可以使用触发器来进行审计跟踪,把修改记录到另外一张表中。 + +MySQL 不允许在触发器中使用 `CALL` 语句 ,也就是不能调用存储过程。 + +**`BEGIN` 和 `END`** + +当触发器的触发条件满足时,将会执行 `BEGIN` 和 `END` 之间的触发器执行动作。 + +> 🔔 注意:在 MySQL 中,分号 `;` 是语句结束的标识符,遇到分号表示该段语句已经结束,MySQL 可以开始执行了。因此,解释器遇到触发器执行动作中的分号后就开始执行,然后会报错,因为没有找到和 BEGIN 匹配的 END。 +> +> 这时就会用到 `DELIMITER` 命令(`DELIMITER` 是定界符,分隔符的意思)。它是一条命令,不需要语句结束标识,语法为:`DELIMITER new_delemiter`。`new_delemiter` 可以设为 1 个或多个长度的符号,默认的是分号 `;`,我们可以把它修改为其他符号,如 `$` - `DELIMITER $` 。在这之后的语句,以分号结束,解释器不会有什么反应,只有遇到了 `$`,才认为是语句结束。注意,使用完之后,我们还应该记得把它给修改回来。 + +**`NEW` 和 `OLD`** + +- MySQL 中定义了 `NEW` 和 `OLD` 关键字,用来表示触发器的所在表中,触发了触发器的那一行数据。 +- 在 `INSERT` 型触发器中,`NEW` 用来表示将要(`BEFORE`)或已经(`AFTER`)插入的新数据; +- 在 `UPDATE` 型触发器中,`OLD` 用来表示将要或已经被修改的原数据,`NEW` 用来表示将要或已经修改为的新数据; +- 在 `DELETE` 型触发器中,`OLD` 用来表示将要或已经被删除的原数据; +- 使用方法: `NEW.columnName` (columnName 为相应数据表某一列名) + +#### 触发器指令 + +> 提示:为了理解触发器的要点,有必要先了解一下创建触发器的指令。 + +`CREATE TRIGGER` 指令用于创建触发器。 + +语法: + +```sql +CREATE TRIGGER trigger_name +trigger_time +trigger_event +ON table_name +FOR EACH ROW +BEGIN + trigger_statements +END; +``` + +说明: + +- trigger_name:触发器名 +- trigger_time: 触发器的触发时机。取值为 `BEFORE` 或 `AFTER`。 +- trigger_event: 触发器的监听事件。取值为 `INSERT`、`UPDATE` 或 `DELETE`。 +- table_name: 触发器的监听目标。指定在哪张表上建立触发器。 +- FOR EACH ROW: 行级监视,Mysql 固定写法,其他 DBMS 不同。 +- trigger_statements: 触发器执行动作。是一条或多条 SQL 语句的列表,列表内的每条语句都必须用分号 `;` 来结尾。 + +创建触发器示例: + +```sql +-- SQL Server +CREATE TRIGGER customer_state +ON Customers +FOR INSERT, UPDATE +AS +UPDATE Customers +SET cust_state = Upper(cust_state) +WHERE Customers.cust_id = inserted.cust_id; + +-- Oracle 和 PostgreSQL +CREATE TRIGGER customer_state +AFTER INSERT OR UPDATE +FOR EACH ROW +BEGIN +UPDATE Customers +SET cust_state = Upper(cust_state) +WHERE Customers.cust_id = :OLD.cust_id +END; +``` + +查看触发器示例: + +```sql +SHOW TRIGGERS; +``` + +删除触发器示例: + +```sql +DROP TRIGGER IF EXISTS trigger_insert_user; +``` + +## 游标 + +游标(CURSOR)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 `SELECT` 语句,而是被该语句检索出来的结果集。在存储过程中使用游标可以对一个结果集进行移动遍历。 + +游标主要用于交互式应用,其中用户需要对数据集中的任意行进行浏览和修改。 + +游标要点 + +- 能够标记游标为只读,使数据能读取,但不能更新和删除。 +- 能控制可以执行的定向操作(向前、向后、第一、最后、绝对位置、相对位置等)。 +- 能标记某些列为可编辑的,某些列为不可编辑的。 +- 规定范围,使游标对创建它的特定请求(如存储过程)或对所有请求可访问。 +- 指示 DBMS 对检索出的数据(而不是指出表中活动数据)进行复制,使数据在游标打开和访问期间不变化。 + +使用游标的步骤: + +1. **定义游标**:通过 `DECLARE cursor_name CURSOR FOR <语句>` 定义游标。这个过程没有实际检索出数据。 +2. **打开游标**:通过 `OPEN cursor_name` 打开游标。 +3. **取出数据**:通过 `FETCH cursor_name INTO var_name ...` 获取数据。 +4. **关闭游标**:通过 `CLOSE cursor_name` 关闭游标。 +5. **释放游标**:通过 `DEALLOCATE PREPARE` 释放游标。 + +游标使用示例: + +```sql +DELIMITER $ +CREATE PROCEDURE getTotal() +BEGIN + DECLARE total INT; + -- 创建接收游标数据的变量 + DECLARE sid INT; + DECLARE sname VARCHAR(10); + -- 创建总数变量 + DECLARE sage INT; + -- 创建结束标志变量 + DECLARE done INT DEFAULT false; + -- 创建游标 + DECLARE cur CURSOR FOR SELECT id,name,age from cursor_table where age>30; + -- 指定游标循环结束时的返回值 + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = true; + SET total = 0; + -- 打开游标 + OPEN cur; + FETCH cur INTO sid, sname, sage; + WHILE(NOT done) + DO + SET total = total + 1; + FETCH cur INTO sid, sname, sage; + END WHILE; + -- 关闭游标 + CLOSE cur; + SELECT total; +END $ +DELIMITER ; + +-- 调用存储过程 +call getTotal(); +``` + +## 参考资料 + +- [《SQL 必知必会》](https://book.douban.com/subject/35167240/) +- [“浅入深出”MySQL 中事务的实现](https://draveness.me/mysql-transaction) +- [MySQL 的学习--触发器](https://www.cnblogs.com/CraryPrimitiveMan/p/4206942.html) +- [维基百科词条 - SQL](https://zh.wikipedia.org/wiki/SQL) +- [https://www.sitesbay.com/sql/index](https://www.sitesbay.com/sql/index) +- [SQL Subqueries](https://www.w3resource.com/sql/subqueries/understanding-sql-subqueries.php) +- [Quick breakdown of the types of joins](https://stackoverflow.com/questions/6294778/mysql-quick-breakdown-of-the-types-of-joins) +- [SQL UNION](https://www.w3resource.com/sql/sql-union.php) +- [SQL database security](https://www.w3resource.com/sql/database-security/create-users.php) +- [Mysql 中的存储过程](https://www.cnblogs.com/chenpi/p/5136483.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/01.\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\346\246\202\350\256\272.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223\347\256\200\344\273\213.md" similarity index 63% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/01.\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\346\246\202\350\256\272.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223\347\256\200\344\273\213.md" index 007aaae4e2..2142cb214a 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/01.\346\225\260\346\215\256\345\272\223\347\263\273\347\273\237\346\246\202\350\256\272.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/01.\347\273\274\345\220\210/\345\205\263\347\263\273\346\225\260\346\215\256\345\272\223\347\256\200\344\273\213.md" @@ -1,5 +1,5 @@ --- -title: 数据库系统概论 +title: 关系数据库简介 date: 2023-11-01 09:39:59 order: 01 categories: @@ -9,72 +9,45 @@ categories: tags: - 数据库 - 关系型数据库 -permalink: /pages/e1121b/ +permalink: /pages/9b4579ff/ --- -# 数据库系统概论 +# 关系数据库简介 -## 数据库核心术语 - -### 数据 - -数据**是数据库中存储的基本对象**,可以对数据做如下定义:**描述事物的符号称为数据**。 - -描述事物的符号多种多样,所以数据有多种表现形式。 - -数据的表现形式还不能完全地表达其内容,需要经过解释,数据和关于数据的解释是密不可分的,每一个数据都有它的意义,数据的解释指的是对数据含义的说明,**数据的含义称为数据的语义,数据与其语义是不可分的。** - -### 数据库 - -数据库就是存放数据的仓库,只不过这个仓库是在计算机存储设备上,而且数据是按一定格式存放的。 - -严格的来讲,数据库的含义如下: - -- 长期存储在计算机内,有组织的,可共享大量数据的集合 -- 里面的数据按照一定的数据模型组织,描述和储存 -- 具有较小的冗余度,较高的数据独立性和易扩展性,并可为各种用户共享 +## 什么是关系型数据库 -数据库特点: +关系型数据库是指采用了关系模型来组织数据的数据库。关系模型是一种数据模型,它表示数据之间的联系,包括一对一、一对多和多对多的关系。在关系型数据库中,数据以表格的形式存储,每个表格称为一个“关系”,每个关系由行(记录或元组)和列(字段或属性)组成。 -- 永久存储 -- 有组织 -- 可共享 +常见的关系型数据库有:MySQL、Oracle、PostgreSQL、MariaDB、SQL Server、SQLite 等。 -### 数据库管理系统 +## 什么是 SQL -数据库管理系统(Database Management System, DBMS)是一种操纵和管理数据库的大型软件,用于建立、使用和维护数据库。它对数据库进行统一的管理和控制,以保证数据库的安全性和完整性。 用户通过 DBMS 访问数据库中的数据,数据库管理员也通过 DBMS 进行数据库的维护工作。 +SQL 是 Structured Query Language(结构 化查询语言)的缩写。SQL 是一种专门用来与数据库沟通的语言。 -数据库管理系统的功能主要包括以下方面: +SQL 有两个主要的标准,分别是 SQL92 和 SQL99。92 和 99 代表了标准提出的时间,SQL92 就是 92 年提出的标准规范。除了 SQL92 和 SQL99 以外,还存在 SQL-86、SQL-89、SQL:2003、SQL:2008、SQL:2011 和 SQL:2016 等其他的标准。主流 RDBMS,比如 MySQL、Oracle、SQL Sever、DB2、PostgreSQL 等都支持 SQL 语言,也就是说它们的使用符合大部分 SQL 标准,但很难完全符合。 -- 数据定义功能 -- 数据组织,存储和管理 -- 数据操纵功能 -- 数据库的事务管理和运行管理 -- 数据库的建立和维护功能 - - 数据库初始数据输入,转换功能 - - 数据库的转储,恢复功能 - - 数据库的重组织功能 - - 数据库的性能监视,分析功能 -- 其他功能 - - 数据库管理系统与网络中其他软件系统的通信功能 - - 一个数据库管理系统与另一个数据库管理系统或文件系统的数据转换功能 - - 异构数据库之间的互访和互操作功能等 +SQL 语言按照功能可以划分成以下的 4 个部分: -### 数据库系统 +- **DDL** 是 Data Definition Language 的缩写,即数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用 DDL,我们可以创建,删除和修改数据库和表结构。 +- **DML** 是 Data Manipulation Language 的缩写,即数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。 +- **DCL** 是 Data Control Language 的缩写,即数据控制语言,我们用它来定义访问权限和安全级别。 +- **DQL** 是 Data Query Language 的缩写,即数据查询语言,我们用它查询想要的记录,它是 SQL 语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。 -数据库系统是由**数据库,数据库管理系统,应用程序和数据库管理员组成的存储,管理,处理和维护数据的系统**。 - -### OLTP 和 OLAP - -OLTP 和 OLAP 的共性: - -OLTP 和 OLAP 都是用于存储和处理大量数据的数据库管理系统。它们都需要高效可靠的 IT 基础设施才能平稳运行。可以同时使用它们来查询现有数据或存储新数据。两者都支持组织中数据驱动的决策。大多数公司同时使用 OLTP 和 OLAP 系统来满足其商业智能需求。 - -OLTP 和 OLAP 的区别: - -**“联机事务处理 (OLTP) ”系统的主要用途是处理数据库事务**。 +## 数据库核心术语 -**“联机分析处理 (OLAP) ”系统的主要用途是分析聚合数据**。 +- **数据库** - 数据库 (DataBase 简称 DB) 就是信息的集合或者说数据库是由数据库管理系统管理的数据的集合。 +- **数据库管理系统** - 数据库管理系统 (Database Management System 简称 DBMS) 是一种操纵和管理数据库的大型软件,通常用于建立、使用和维护数据库。 +- **数据库系统** - 数据库系统 (Data Base System,简称 DBS) 通常由软件、数据库和数据管理员 (DBA) 组成。 +- **数据库管理员** - 数据库管理员 (Database Administrator, 简称 DBA) 负责全面管理和控制数据库系统。 +- **OLTP** - 联机事务处理 (OLTP) 系统的主要用途是处理数据库事务。 +- **OLAP** - 联机分析处理 (OLAP) 系统的主要用途是分析聚合数据。 +- **元组** - 元组(Tuple)是关系数据库中的基本概念,关系是一张表,表中的每行(即数据库中的每条记录)就是一个元组,每列就是一个属性。 在二维表里,元组也称为行。 +- **码** - 码就是能唯一标识实体的属性,对应表中的列。 +- **候选码** - 若关系中的某一属性或属性组的值能唯一的标识一个元组,而其任何、子集都不能再标识,则称该属性组为候选码。例如:在学生实体中,“学号”是能唯一的区分学生实体的,同时又假设“姓名”、“班级”的属性组合足以区分学生实体,那么{学号}和{姓名,班级}都是候选码。 +- **主码** - 主码也叫主键。主码是从候选码中选出来的。一个实体集中只能有一个主码,但可以有多个候选码。 +- **外码** - 外码也叫外键。如果一个关系中的一个属性是另外一个关系中的主码则这个属性为外码。 +- **主属性** - 候选码中出现过的属性称为主属性。比如关系 工人(工号,身份证号,姓名,性别,部门). 显然工号和身份证号都能够唯一标示这个关系,所以都是候选码。工号、身份证号这两个属性就是主属性。如果主码是一个属性组,那么属性组中的属性都是主属性。 +- **非主属性** - 不包含在任何一个候选码中的属性称为非主属性。比如在关系——学生(学号,姓名,年龄,性别,班级)中,主码是“学号”,那么其他的“姓名”、“年龄”、“性别”、“班级”就都可以称为非主属性。 ## 数据模型 @@ -138,7 +111,7 @@ U = { 学号,姓名,年龄,专业 } 根据约束程度从低到高有:第一范式(1NF)、第二范式(2NF)、第三范式(3NF)、巴斯-科德范式(BCNF)等等。现代数据库设计,一般最多满足 3NF——范式过高,虽然具有对数据关系更好的约束性,但也导致数据关系表增加而令数据库 IO 更繁忙。因此,在实际应用中,本来可以交由数据库处理的关系约束,很多都是在数据库使用程序中完成的。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202311030715177.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410022024207.png) ### 第一范式 (1NF) @@ -150,9 +123,9 @@ U = { 学号,姓名,年龄,专业 } 假设有一张 student 表,结构如下: -``` +```sql -- 学生表 -student(学号、课程号、姓名、学分、成绩) +student(学号、课程号、姓名、学分、成绩) ``` 举例来说,现有一张 student 表,具有学号、课程号、姓名、学分等字段。从中可以看出,表中包含了学生信息和课程信息。由于非主键字段必须依赖主键,这里学分依赖课程号,姓名依赖学号,所以不符合 2NF。 @@ -166,31 +139,31 @@ student(学号、课程号、姓名、学分、成绩) 根据 2NF 可以拆分如下: -``` +```sql -- 学生表 -student(学号、姓名) +student(学号、姓名) -- 课程表 -course(课程号、学分) +course(课程号、学分) -- 学生课程关系表 -student_course(学号、课程号、成绩) +student_course(学号、课程号、成绩) ``` ### 第三范式 (3NF) -**如果一个关系属于第二范式**,并且在**两个(或多个)非主键属性之间不存在函数依赖**(非主键属性之间的函数依赖也称为传递依赖),那么这个关系属于第三范式。 +**如果一个关系属于第二范式**,并且在**两个(或多个)非主键属性之间不存在函数依赖**(非主键属性之间的函数依赖也称为传递依赖),那么这个关系属于第三范式。 3NF 是对字段的**冗余性**,要求任何字段不能由其他字段派生出来,它要求字段没有冗余,即**不存在传递依赖**。 假设有一张 student 表,结构如下: -``` +```sql -- 学生表 -student(学号、姓名、年龄、班级号、班主任) +student(学号、姓名、年龄、班级号、班主任) ``` 上表属于第二范式,因为主键由单个属性组成(学号)。 -因为存在**依赖传递**:(学号) → (学生)→(所在班级) → (班主任) 。 +因为存在**依赖传递**:(学号) → (学生)→(所在班级) → (班主任) 。 **可能会存在问题:** @@ -200,8 +173,8 @@ student(学号、姓名、年龄、班级号、班主任) 可以基于 3NF 拆解: ``` -student(学号、姓名、年龄、所在班级号) -class(班级号、班主任) +student(学号、姓名、年龄、所在班级号) +class(班级号、班主任) ``` ### 反范式 @@ -221,7 +194,7 @@ ER 图中的要素: - **主键** - **在属性下方标记下划线**。在描述实体集的所有属性中,可以唯一标识每个实体的属性称为键。键也是属于实体的属性,作为键的属性取值必须唯一且不能“空置”。 - **联系** - **用菱形表示**。世界上任何事物都不是孤立存在的,事物内部和事物之间都有联系的,实体之间的联系通常有 3 种类型:一对一联系,一对多联系,多对多联系。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202311030715877.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410022024964.png) 绘制 ER 图常用软件: @@ -239,6 +212,6 @@ ER 图中的要素: ## 参考资料 -- [《数据库系统概论(第 5 版)》](https://book.douban.com/subject/26317662/) +- [《关系数据库简介(第 5 版)》](https://book.douban.com/subject/26317662/) - [数据库逻辑设计之三大范式通俗理解,一看就懂,书上说的太晦涩](https://segmentfault.com/a/1190000013695030) - [ER 图(实体关系图)怎么画?](https://www.zhihu.com/tardis/zm/art/270299029?source_id=1003) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/04.Mysql\344\272\213\345\212\241.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\344\272\213\345\212\241.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/04.Mysql\344\272\213\345\212\241.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\344\272\213\345\212\241.md" index 09a55c642b..e597783924 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/04.Mysql\344\272\213\345\212\241.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\344\272\213\345\212\241.md" @@ -3,7 +3,6 @@ icon: logos:mysql title: Mysql 事务 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202310260703504.png date: 2020-06-03 19:32:09 -order: 04 categories: - 数据库 - 关系型数据库 @@ -13,7 +12,7 @@ tags: - 关系型数据库 - Mysql - 事务 -permalink: /pages/00b04d/ +permalink: /pages/04246a4a/ --- # Mysql 事务 @@ -565,7 +564,7 @@ COMMIT; - 本地消息表/MQ 事务 都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。 - Saga 事务 由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。 Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。 -> 分布式事务详细说明、分析请参考:[分布式事务基本原理](https://dunwu.github.io/waterdrop/pages/e1881c/) +> 分布式事务详细说明、分析请参考:[分布式事务基本原理](https://dunwu.github.io/waterdrop/pages/d46468f7/) ## 事务最佳实践 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/07.Mysql\344\274\230\345\214\226.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\344\274\230\345\214\226.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/07.Mysql\344\274\230\345\214\226.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\344\274\230\345\214\226.md" index b11daf843c..16209278bb 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/07.Mysql\344\274\230\345\214\226.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\344\274\230\345\214\226.md" @@ -12,7 +12,7 @@ tags: - 关系型数据库 - Mysql - 优化 -permalink: /pages/396816/ +permalink: /pages/b17b5f4a/ --- # Mysql 优化 @@ -310,7 +310,7 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); 通过索引覆盖查询,可以优化排序、分组。 -详情见 [Mysql 索引](https://dunwu.github.io/waterdrop/pages/fcb19c/) +详情见 [Mysql 索引](https://dunwu.github.io/waterdrop/pages/2ce0ae87/) ## 数据结构优化 @@ -400,7 +400,7 @@ SELECT * FROM post WHERE post.id IN (123,456,567,9098,8904); - [《高性能 MySQL》](https://book.douban.com/subject/23008813/) - [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - [我必须得告诉大家的 MySQL 优化原理](https://www.jianshu.com/p/d7665192aaaf) - [20+ 条 MySQL 性能优化的最佳经验](https://www.jfox.info/20-tiao-mysql-xing-nen-you-hua-de-zui-jia-jing-yan.html) - [Mysql 官方文档之执行计划](https://dev.mysql.com/doc/refman/8.0/en/execution-plan-information.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/02.Mysql\345\255\230\345\202\250\345\274\225\346\223\216.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\345\255\230\345\202\250\345\274\225\346\223\216.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/02.Mysql\345\255\230\345\202\250\345\274\225\346\223\216.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\345\255\230\345\202\250\345\274\225\346\223\216.md" index 74477f9e99..99d7a199cd 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/02.Mysql\345\255\230\345\202\250\345\274\225\346\223\216.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\345\255\230\345\202\250\345\274\225\346\223\216.md" @@ -2,9 +2,8 @@ icon: logos:mysql title: Mysql 存储引擎 date: 2020-07-13 10:08:37 -order: 02 categories: - - 数据库`` + - 数据库 - 关系型数据库 - Mysql tags: @@ -13,7 +12,7 @@ tags: - Mysql - 存储引擎 - InnoDB -permalink: /pages/5fe0f3/ +permalink: /pages/f123189c/ --- # Mysql 存储引擎 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/01.Mysql\346\236\266\346\236\204.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\346\236\266\346\236\204.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/01.Mysql\346\236\266\346\236\204.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\346\236\266\346\236\204.md" index a52d9de056..b498e2dd76 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/01.Mysql\346\236\266\346\236\204.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\346\236\266\346\236\204.md" @@ -1,9 +1,8 @@ --- icon: logos:mysql title: Mysql 架构 -cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202309242206810.png +cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202410022029759.png date: 2020-07-16 11:14:07 -order: 01 categories: - 数据库 - 关系型数据库 @@ -15,7 +14,7 @@ tags: - 日志 - binlog - WAL -permalink: /pages/8262aa/ +permalink: /pages/b322c2bc/ --- # Mysql 架构 @@ -26,7 +25,7 @@ permalink: /pages/8262aa/ **存储引擎层负责数据的存储和提取**。其架构模式是插件式的,支持 InnoDB、MyISAM、Memory 等多个存储引擎。现在最常用的存储引擎是 InnoDB,它从 MySQL 5.5.5 版本开始成为了默认存储引擎。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202311111138178.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410022029759.png) ## Mysql 查询流程 @@ -144,7 +143,7 @@ MySQL 更新过程和 MySQL 查询过程类似,也会将流程走一遍。不 InnoDB 的 redo log 是固定大小的,比如可以配置为一组 4 个文件,每个文件的大小是 1GB,那么总共就可以记录 4GB 的操作。从头开始写,写到末尾就又回到开头循环写,如下面这个图所示。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202311111210060.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410022031931.png) write pos 是当前记录的位置,一边写一边后移,写到第 3 号文件末尾后就回到 0 号文件开头。checkpoint 是当前要擦除的位置,也是往后推移并且循环的,擦除记录前要把记录更新到数据文件。 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/03.Mysql\347\264\242\345\274\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\347\264\242\345\274\225.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/03.Mysql\347\264\242\345\274\225.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\347\264\242\345\274\225.md" index 3c1a7d6fc5..2c5c538936 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/03.Mysql\347\264\242\345\274\225.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\347\264\242\345\274\225.md" @@ -3,7 +3,6 @@ icon: logos:mysql title: Mysql 索引 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202310162333557.png date: 2020-07-16 11:14:07 -order: 03 categories: - 数据库 - 关系型数据库 @@ -13,7 +12,7 @@ tags: - 关系型数据库 - Mysql - 索引 -permalink: /pages/fcb19c/ +permalink: /pages/2ce0ae87/ --- # Mysql 索引 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/20.Mysql\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\350\277\220\347\273\264.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/20.Mysql\350\277\220\347\273\264.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\350\277\220\347\273\264.md" index f90e203bd6..5eb535fbcf 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/20.Mysql\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\350\277\220\347\273\264.md" @@ -2,7 +2,6 @@ icon: logos:mysql title: Mysql 运维 date: 2019-11-26 21:37:17 -order: 20 categories: - 数据库 - 关系型数据库 @@ -12,7 +11,7 @@ tags: - 关系型数据库 - Mysql - 运维 -permalink: /pages/e33b92/ +permalink: /pages/99c68708/ --- # Mysql 运维 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/05.Mysql\351\224\201.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\224\201.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/05.Mysql\351\224\201.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\224\201.md" index 6913503797..4277ec1d00 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/05.Mysql\351\224\201.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\224\201.md" @@ -3,7 +3,6 @@ icon: logos:mysql title: Mysql 锁 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202310162345947.png date: 2020-09-07 07:54:19 -order: 05 categories: - 数据库 - 关系型数据库 @@ -16,7 +15,7 @@ tags: - 读写锁 - 悲观锁 - 乐观锁 -permalink: /pages/f1f151/ +permalink: /pages/a49b76af/ --- # Mysql 锁 @@ -533,7 +532,7 @@ InnoDB 存储引擎的主键索引为聚簇索引,其它索引为辅助索引 - [《高性能 MySQL》](https://book.douban.com/subject/23008813/) - [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139) -- [《Java 性能调优实战》](https://time.geekbang.org/column/intro/100028001) +- [极客时间教程 - Java 性能调优实战](https://time.geekbang.org/column/intro/100028001) - [Mysql 官方文档之 InnoDB Locking](https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html) - [数据库系统原理](https://github.com/CyC2018/Interview-Notebook/blob/master/notes/数据库系统原理.md) - [数据库两大神器【索引和锁】](https://juejin.im/post/5b55b842f265da0f9e589e79) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/99.Mysql\351\235\242\350\257\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\235\242\350\257\225.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/99.Mysql\351\235\242\350\257\225.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\235\242\350\257\225.md" index 979abf49b3..fc84600586 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/99.Mysql\351\235\242\350\257\225.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\235\242\350\257\225.md" @@ -2,7 +2,6 @@ icon: logos:mysql title: Mysql 面试 date: 2020-09-12 10:43:53 -order: 99 categories: - 数据库 - 关系型数据库 @@ -12,7 +11,7 @@ tags: - 关系型数据库 - Mysql - 面试 -permalink: /pages/7b0caf/ +permalink: /pages/cf957091/ --- # Mysql 面试 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/06.Mysql\351\253\230\345\217\257\347\224\250.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\253\230\345\217\257\347\224\250.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/06.Mysql\351\253\230\345\217\257\347\224\250.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\253\230\345\217\257\347\224\250.md" index e05c059454..5c8de65d42 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/06.Mysql\351\253\230\345\217\257\347\224\250.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/Mysql_\351\253\230\345\217\257\347\224\250.md" @@ -2,7 +2,6 @@ icon: logos:mysql title: Mysql 高可用 date: 2023-09-21 21:25:58 -order: 06 categories: - 数据库 - 关系型数据库 @@ -12,7 +11,7 @@ tags: - 关系型数据库 - Mysql - 高可用 -permalink: /pages/083b48/ +permalink: /pages/f3f7b97e/ --- # Mysql 高可用 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/README.md" index 96f5d2a449..9bbcb70017 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/02.Mysql/README.md" @@ -10,7 +10,7 @@ tags: - 数据库 - 关系型数据库 - Mysql -permalink: /pages/a5b63b/ +permalink: /pages/90b1138e/ hidden: true index: false --- @@ -21,23 +21,15 @@ index: false ## 📖 内容 -### [Mysql 架构](01.Mysql架构.md) - -### [Mysql 存储引擎](02.Mysql存储引擎.md) - -### [Mysql 索引](03.Mysql索引.md) - -### [Mysql 事务](04.Mysql事务.md) - -### [Mysql 锁](05.Mysql锁.md) - -### [Mysql 高可用](06.Mysql高可用.md) - -### [Mysql 优化](07.Mysql优化.md) - -### [Mysql 运维](20.Mysql运维.md) - -### [Mysql 面试](99.Mysql面试.md) +- [Mysql 架构](Mysql_架构) +- [Mysql 存储引擎](Mysql_存储引擎) +- [Mysql 索引](Mysql_索引) +- [Mysql 事务](Mysql_事务) +- [Mysql 锁](Mysql_锁) +- [Mysql 高可用](Mysql_高可用) +- [Mysql 优化](Mysql_优化) +- [Mysql 运维](Mysql_运维) +- [Mysql 面试](Mysql_面试) ## 📚 资料 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/01.PostgreSQL.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/01.PostgreSQL.md" index f2dd84c7b8..268246ec67 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/01.PostgreSQL.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/01.PostgreSQL.md" @@ -10,7 +10,7 @@ tags: - 数据库 - 关系型数据库 - PostgreSQL -permalink: /pages/52609d/ +permalink: /pages/31bd28da/ --- # PostgreSQL 应用指南 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/02.H2.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/02.H2.md" index a020720d55..bf88742d60 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/02.H2.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/02.H2.md" @@ -10,7 +10,7 @@ tags: - 数据库 - 关系型数据库 - H2 -permalink: /pages/f27c0c/ +permalink: /pages/906c7a2d/ --- # H2 应用指南 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/03.Sqlite.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/03.Sqlite.md" index d91efa0c22..81b0d7415b 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/03.Sqlite.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/03.Sqlite.md" @@ -10,7 +10,7 @@ tags: - 数据库 - 关系型数据库 - SQLite -permalink: /pages/bdcd7e/ +permalink: /pages/b4ac0b8f/ --- # SQLite diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/README.md" index 17b2e47230..fae253ba52 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/99.\345\205\266\344\273\226/README.md" @@ -8,7 +8,7 @@ categories: tags: - 数据库 - 关系型数据库 -permalink: /pages/ca9888/ +permalink: /pages/052cac2c/ hidden: true index: false --- diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/README.md" index 9a0162063e..cef38a7b11 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/03.\345\205\263\347\263\273\345\236\213\346\225\260\346\215\256\345\272\223/README.md" @@ -7,7 +7,7 @@ categories: tags: - 数据库 - 关系型数据库 -permalink: /pages/bb43eb/ +permalink: /pages/97156163/ hidden: true index: false --- @@ -18,21 +18,20 @@ index: false ### 关系型数据库综合 -- [SQL 语法速成](01.综合/02.SQL语法.md) -- [扩展 SQL](01.综合/03.扩展SQL.md) -- [SQL Cheat Sheet](01.综合/99.SqlCheatSheet.md) +- [关系数据库简介](01.综合/关系数据库简介.md) +- [SQL 语法](01.综合/SQL语法.md) ### Mysql -- [Mysql 架构](02.Mysql/01.Mysql架构.md) -- [Mysql 存储引擎](02.Mysql/02.Mysql存储引擎.md) -- [Mysql 索引](02.Mysql/03.Mysql索引.md) -- [Mysql 事务](02.Mysql/04.Mysql事务.md) -- [Mysql 锁](02.Mysql/05.Mysql锁.md) -- [Mysql 高可用](02.Mysql/06.Mysql高可用.md) -- [Mysql 优化](02.Mysql/07.Mysql优化.md) -- [Mysql 运维](02.Mysql/20.Mysql运维.md) -- [Mysql 面试](02.Mysql/99.Mysql面试.md) +- [Mysql 架构](02.Mysql/Mysql_架构) +- [Mysql 存储引擎](02.Mysql/Mysql_存储引擎) +- [Mysql 索引](02.Mysql/Mysql_索引) +- [Mysql 事务](02.Mysql/Mysql_事务) +- [Mysql 锁](02.Mysql/Mysql_锁) +- [Mysql 高可用](02.Mysql/Mysql_高可用) +- [Mysql 优化](02.Mysql/Mysql_优化) +- [Mysql 运维](02.Mysql/Mysql_运维) +- [Mysql 面试](02.Mysql/Mysql_面试) ### 其他 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/01.MongoDB\345\272\224\347\224\250\346\214\207\345\215\227.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/01.MongoDB\345\272\224\347\224\250\346\214\207\345\215\227.md" deleted file mode 100644 index d7856e889f..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/01.MongoDB\345\272\224\347\224\250\346\214\207\345\215\227.md" +++ /dev/null @@ -1,670 +0,0 @@ ---- -title: MongoDB 应用指南 -date: 2020-09-07 07:54:19 -order: 01 -categories: - - 数据库 - - 文档数据库 - - MongoDB -tags: - - 数据库 - - 文档数据库 - - MongoDB -permalink: /pages/3288f3/ ---- - -# MongoDB 应用指南 - -## 简介 - -MongoDB 是一个基于分布式文件存储的数据库。由 C++ 语言编写。旨在为 WEB 应用提供可扩展的高性能数据存储解决方案。 - -MongoDB 将数据存储为一个文档,数据结构由键值(key=>value)对组成。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档,数组及文档数组。 - -### MongoDB 发展 - -- 1.x - 支持复制和分片 -- 2.x - 更丰富的数据库功能 -- 3.x - WiredTiger 和周边生态 -- 4.x - 支持分布式事务 - -### MongoDB 和 RDBMS - -| 特性 | MongoDB | RDBMS | -| --------- | ------------------------------------------------ | -------- | -| 数据模型 | 文档模型 | 关系型 | -| CRUD 操作 | MQL/SQL | SQL | -| 高可用 | 复制集 | 集群模式 | -| 扩展性 | 支持分片 | 数据分区 | -| 扩繁方式 | 垂直扩展+水平扩展 | 垂直扩展 | -| 索引类型 | B 树、全文索引、地理位置索引、多键索引、TTL 索引 | B 树 | -| 数据容量 | 没有理论上限 | 千万、亿 | - -### MongoDB 特性 - -- 数据是 JSON 结构 - - 支持结构化、半结构化数据模型 - - 可以动态响应结构变化 -- 通过副本机制提供高可用 -- 通过分片提供扩容能力 - -## MongoDB 概念 - -| SQL 术语/概念 | MongoDB 术语/概念 | 解释/说明 | -| :------------ | :---------------- | :------------------------------------- | -| database | database | 数据库 | -| table | collection | 数据库表/集合 | -| row | document | 数据记录行/文档 | -| column | field | 数据字段/域 | -| index | index | 索引 | -| table joins | | 表连接,MongoDB 不支持 | -| primary key | primary key | 主键,MongoDB 自动将\_id 字段设置为主键 | - -### 数据库 - -一个 MongoDB 中可以建立多个数据库。 - -MongoDB 的默认数据库为"db",该数据库存储在 data 目录中。 - -MongoDB 的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。 - -**"show dbs"** 命令可以显示所有数据的列表。 - -```shell -$ ./mongo -MongoDBshell version: 3.0.6 -connecting to: test -> show dbs -local 0.078GB -test 0.078GB -> -``` - -执行 **"db"** 命令可以显示当前数据库对象或集合。 - -```shell -$ ./mongo -MongoDBshell version: 3.0.6 -connecting to: test -> db -test -> -``` - -运行"use"命令,可以连接到一个指定的数据库。 - -```shell -> use local -switched to db local -> db -local -> -``` - -数据库也通过名字来标识。数据库名可以是满足以下条件的任意 UTF-8 字符串。 - -- 不能是空字符串("")。 -- 不得含有 ' '(空格)、`.`、`\$`、`/`、`\`和 `\0` (空字符)。 -- 应全部小写。 -- 最多 64 字节。 - -有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。 - -- **admin**:从权限的角度来看,这是"root"数据库。要是将一个用户添加到这个数据库,这个用户自动继承所有数据库的权限。一些特定的服务器端命令也只能从这个数据库运行,比如列出所有的数据库或者关闭服务器。 -- **local**:这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合 -- **config**:当 Mongo 用于分片设置时,config 数据库在内部使用,用于保存分片的相关信息。 - -### 文档 - -文档是一组键值(key-value)对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。 - -需要注意的是: - -- 文档中的键/值对是有序的。 -- 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。 -- MongoDB 区分类型和大小写。 -- MongoDB 的文档不能有重复的键。 -- 文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。 - -文档键命名规范: - -- 键不能含有 `\0` (空字符)。这个字符用来表示键的结尾。 -- `.` 和 `$` 有特别的意义,只有在特定环境下才能使用。 -- 以下划线 `_` 开头的键是保留的(不是严格要求的)。 - -### 集合 - -集合就是 MongoDB 文档组,类似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表格。 - -集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。 - -合法的集合名: - -- 集合名不能是空字符串""。 -- 集合名不能含有 `\0` 字符(空字符),这个字符表示集合名的结尾。 -- 集合名不能以"system."开头,这是为系统集合保留的前缀。 -- 用户创建的集合名字不能含有保留字符。有些驱动程序的确支持在集合名里面包含,这是因为某些系统生成的集合中包含该字符。除非你要访问这种系统创建的集合,否则千万不要在名字里出现 `$`。 - -### 元数据 - -数据库的信息是存储在集合中。它们使用了系统的命名空间:`dbname.system.*` - -在 MongoDB 数据库中名字空间 `.system.*` 是包含多种系统信息的特殊集合(Collection),如下: - -| 集合命名空间 | 描述 | -| :----------------------- | :---------------------------------------- | -| dbname.system.namespaces | 列出所有名字空间。 | -| dbname.system.indexes | 列出所有索引。 | -| dbname.system.profile | 包含数据库概要(profile)信息。 | -| dbname.system.users | 列出所有可访问数据库的用户。 | -| dbname.local.sources | 包含复制对端(slave)的服务器信息和状态。 | - -对于修改系统集合中的对象有如下限制。 - -在 `system.indexes` 插入数据,可以创建索引。但除此之外该表信息是不可变的(特殊的 drop index 命令将自动更新相关信息)。`system.users` 是可修改的。`system.profile` 是可删除的。 - -## MongoDB 数据类型 - -| 数据类型 | 描述 | -| :----------------- | :--------------------------------------------------------------------------------------------------------- | -| String | 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。 | -| Integer | 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。 | -| Boolean | 布尔值。用于存储布尔值(真/假)。 | -| Double | 双精度浮点值。用于存储浮点值。 | -| Min/Max keys | 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。 | -| Array | 用于将数组或列表或多个值存储为一个键。 | -| Timestamp | 时间戳。记录文档修改或添加的具体时间。 | -| Object | 用于内嵌文档。 | -| Null | 用于创建空值。 | -| Symbol | 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。 | -| Date | 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。 | -| Object ID | 对象 ID。用于创建文档的 ID。 | -| Binary Data | 二进制数据。用于存储二进制数据。 | -| Code | 代码类型。用于在文档中存储 JavaScript 代码。 | -| Regular expression | 正则表达式类型。用于存储正则表达式。 | - -## MongoDB CRUD - -### 数据库操作 - -#### 查看所有数据库 - -```shell -show dbs -``` - -#### 创建数据库 - -```shell -use -``` - -如果数据库不存在,则创建数据库,否则切换到指定数据库。 - -【示例】创建数据库,并插入一条数据 - -刚创建的数据库 test 并不在数据库的列表中, 要显示它,需要插入一些数据 - -```shell -> use test -switched to db test -> -> show dbs -admin 0.000GB -config 0.000GB -local 0.000GB -> db.test.insert({"name":"mongodb"}) -WriteResult({ "nInserted" : 1 }) -> show dbs -admin 0.000GB -config 0.000GB -local 0.000GB -test 0.000GB -``` - -#### 删除数据库 - -删除当前数据库 - -```shell -db.dropDatabase() -``` - -### 集合操作 - -#### 查看集合 - -```shell -show collections -``` - -#### 创建集合 - -```shell -db.createCollection(name, options) -``` - -参数说明: - -- name: 要创建的集合名称 -- options: 可选参数, 指定有关内存大小及索引的选项 - -options 可以是如下参数: - -| 字段 | 类型 | 描述 | -| :---------- | :--- | :------------------------------------------------------------------------------------------------------------------------------------------------------- | -| capped | 布尔 | (可选)如果为 true,则创建固定集合。固定集合是指有着固定大小的集合,当达到最大值时,它会自动覆盖最早的文档。 **当该值为 true 时,必须指定 size 参数。** | -| autoIndexId | 布尔 | 3.2 之后不再支持该参数。(可选)如为 true,自动在 \_id 字段创建索引。默认为 false。 | -| size | 数值 | (可选)为固定集合指定一个最大值,即字节数。 **如果 capped 为 true,也需要指定该字段。** | -| max | 数值 | (可选)指定固定集合中包含文档的最大数量。 | - -在插入文档时,MongoDB 首先检查固定集合的 size 字段,然后检查 max 字段。 - -```shell -> db.createCollection("collection") -{ "ok" : 1 } -> show collections -collection -``` - -#### 删除集合 - -```shell -> db.collection.drop() -true -> show collections -> -``` - -### 插入文档操作 - -MongoDB 使用 insert() 方法完成插入操作。 - -**语法格式** - -```shell -# 插入单条记录 -db.<集合>.insertOne() -# 插入多条记录 -db.<集合>.insertMany([, , ..., ]) -``` - -【示例】insertOne - -```shell -> db.color.insertOne({name: "red"}) -{ - "acknowledged" : true, - "insertedId" : ObjectId("5f533ae4e8f16647950fdf43") -} -``` - -【示例】insertMany - -```shell -> db.color.insertMany([ - { - "name": "yellow" - }, - { - "name": "blue" - } -]) -{ - "acknowledged" : true, - "insertedIds" : [ - ObjectId("5f533bcae8f16647950fdf44"), - ObjectId("5f533bcae8f16647950fdf45") - ] -} -> -``` - -### 查询文档操作 - -MongoDB 使用 `find()` 方法完成查询文档操作。 - -**语法格式** - -```shell -db.<集合>.find() -``` - -查询条件也是 json 形式,如果不设置查询条件,即为全量查询。 - -#### 查询条件 - -| 操作 | 格式 | 范例 | RDBMS 中的类似语句 | -| :---------------------- | :-------------------------------------- | :----------------------------------------- | :-------------------- | -| 等于 | `{:`} | `db.book.find({"pageCount": {$eq: 0}})` | `where pageCount = 0` | -| 不等于 | `{:{$ne:}}` | `db.book.find({"pageCount": {$ne: 0}})` | `where likes != 50` | -| 大于 | `{:{$gt:}}` | `db.book.find({"pageCount": {$gt: 0}})` | `where likes > 50` | -| `{:{$gt:}}` | `db.book.find({"pageCount": {$gt: 0}})` | `where likes > 50` | 大于或等于 | -| 小于 | `{:{$lt:}}` | `db.book.find({"pageCount": {$lt: 200}})` | `where likes < 50` | -| 小于或等于 | `{:{$lte:}}` | `db.book.find({"pageCount": {$lte: 200}})` | `where likes <= 50` | - -> 说明: -> -> ```shell -> $eq -------- equal = -> $ne ----------- not equal != -> $gt -------- greater than > -> $gte --------- gt equal >= -> $lt -------- less than < -> $lte --------- lt equal <= -> ``` - -【示例】 - -```shell - - -# 统计匹配查询条件的记录数 -> db.book.find({"status": "MEAP"}).count() -68 -``` - -#### 查询逻辑条件 - -(1)and 条件 - -MongoDB 的 find() 方法可以传入多个键(key),每个键(key)以逗号隔开,即常规 SQL 的 AND 条件。 - -语法格式如下: - -```shell -> db.col.find({key1:value1, key2:value2}).pretty() -``` - -(2)or 条件 - -MongoDB OR 条件语句使用了关键字 **\$or**,语法格式如下: - -```shell ->db.col.find( - { - $or: [ - {key1: value1}, {key2:value2} - ] - } -).pretty() -``` - -#### 模糊查询 - -查询 title 包含"教"字的文档: - -```shell -db.col.find({ title: /教/ }) -``` - -查询 title 字段以"教"字开头的文档: - -```shell -db.col.find({ title: /^教/ }) -``` - -查询 titl e 字段以"教"字结尾的文档: - -```shell -db.col.find({ title: /教$/ }) -``` - -#### Limit() 方法 - -如果你需要在 MongoDB 中读取指定数量的数据记录,可以使用 MongoDB 的 Limit 方法,limit()方法接受一个数字参数,该参数指定从 MongoDB 中读取的记录条数。 - -limit()方法基本语法如下所示: - -```shell ->db.COLLECTION_NAME.find().limit(NUMBER) -``` - -#### Skip() 方法 - -我们除了可以使用 limit()方法来读取指定数量的数据外,还可以使用 skip()方法来跳过指定数量的数据,skip 方法同样接受一个数字参数作为跳过的记录条数。 - -skip() 方法脚本语法格式如下: - -```shell ->db.COLLECTION_NAME.find().limit(NUMBER).skip(NUMBER) -``` - -#### Sort() 方法 - -在 MongoDB 中使用 sort() 方法对数据进行排序,sort() 方法可以通过参数指定排序的字段,并使用 1 和 -1 来指定排序的方式,其中 1 为升序排列,而 -1 是用于降序排列。 - -sort()方法基本语法如下所示: - -```shell ->db.COLLECTION_NAME.find().sort({KEY:1}) -``` - -> 注意:skip(), limilt(), sort()三个放在一起执行的时候,执行的顺序是先 sort(), 然后是 skip(),最后是显示的 limit()。 - -### 更新文档操作 - -update() 方法用于更新已存在的文档。语法格式如下: - -```shell -db.collection.update( - , - , - { - upsert: , - multi: , - writeConcern: - } -) -``` - -**参数说明:** - -- **query** : update 的查询条件,类似 sql update 查询内 where 后面的。 -- **update** : update 的对象和一些更新的操作符(如$,$inc...)等,也可以理解为 sql update 查询内 set 后面的 -- **upsert** : 可选,这个参数的意思是,如果不存在 update 的记录,是否插入 objNew,true 为插入,默认是 false,不插入。 -- **multi** : 可选,mongodb 默认是 false,只更新找到的第一条记录,如果这个参数为 true,就把按条件查出来多条记录全部更新。 -- **writeConcern** :可选,抛出异常的级别。 - -【示例】更新文档 - -```shell -db.collection.update({ title: 'MongoDB 教程' }, { $set: { title: 'MongoDB' } }) -``` - -【示例】更新多条相同文档 - -以上语句只会修改第一条发现的文档,如果你要修改多条相同的文档,则需要设置 multi 参数为 true。 - -```shell -db.collection.update( - { title: 'MongoDB 教程' }, - { $set: { title: 'MongoDB' } }, - { multi: true } -) -``` - -【示例】更多实例 - -只更新第一条记录: - -```shell -db.collection.update({ count: { $gt: 1 } }, { $set: { test2: 'OK' } }) -``` - -全部更新: - -```shell -db.collection.update( - { count: { $gt: 3 } }, - { $set: { test2: 'OK' } }, - false, - true -) -``` - -只添加第一条: - -```shell -db.collection.update( - { count: { $gt: 4 } }, - { $set: { test5: 'OK' } }, - true, - false -) -``` - -全部添加进去: - -```shell -db.collection.update( - { count: { $gt: 4 } }, - { $set: { test5: 'OK' } }, - true, - false -) -``` - -全部更新: - -```shell -db.collection.update( - { count: { $gt: 4 } }, - { $set: { test5: 'OK' } }, - true, - false -) -``` - -只更新第一条记录: - -```shell -db.collection.update( - { count: { $gt: 4 } }, - { $set: { test5: 'OK' } }, - true, - false -) -``` - -### 删除文档操作 - -官方推荐使用 deleteOne() 和 deleteMany() 方法删除数据。 - -删除 status 等于 A 的全部文档: - -```shell -db.collection.deleteMany({ status: 'A' }) -``` - -删除 status 等于 D 的一个文档: - -```shell -db.collection.deleteOne({ status: 'D' }) -``` - -### 索引操作 - -索引通常能够极大的提高查询的效率,如果没有索引,MongoDB 在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。 - -这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。 - -索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构。 - -MongoDB 使用 createIndex() 方法来创建索引。 - -createIndex()方法基本语法格式如下所示: - -```shell ->db.collection.createIndex(keys, options) -``` - -语法中 Key 值为你要创建的索引字段,1 为指定按升序创建索引,如果你想按降序来创建索引指定为 -1 即可。 - -```shell ->db.col.createIndex({"title":1}) -``` - -createIndex() 方法中你也可以设置使用多个字段创建索引(关系型数据库中称作复合索引)。 - -```shell ->db.col.createIndex({"title":1,"description":-1}) -``` - -createIndex() 接收可选参数,可选参数列表如下: - -| Parameter | Type | Description | -| :----------------- | :------------ | :----------------------------------------------------------------------------------------------------------------------------------------------- | -| background | Boolean | 建索引过程会阻塞其它数据库操作,background 可指定以后台方式创建索引,即增加 "background" 可选参数。 "background" 默认值为**false**。 | -| unique | Boolean | 建立的索引是否唯一。指定为 true 创建唯一索引。默认值为**false**. | -| name | string | 索引的名称。如果未指定,MongoDB 的通过连接索引的字段名和排序顺序生成一个索引名称。 | -| ~~dropDups~~ | ~~Boolean~~ | ~~**3.0+版本已废弃。**在建立唯一索引时是否删除重复记录,指定 true 创建唯一索引。默认值为 **false**。~~ | -| sparse | Boolean | 对文档中不存在的字段数据不启用索引;这个参数需要特别注意,如果设置为 true 的话,在索引字段中不会查询出不包含对应字段的文档.。默认值为 **false**. | -| expireAfterSeconds | integer | 指定一个以秒为单位的数值,完成 TTL 设定,设定集合的生存时间。 | -| v | index version | 索引的版本号。默认的索引版本取决于 mongod 创建索引时运行的版本。 | -| weights | document | 索引权重值,数值在 1 到 99,999 之间,表示该索引相对于其他索引字段的得分权重。 | -| default_language | string | 对于文本索引,该参数决定了停用词及词干和词器的规则的列表。 默认为英语 | -| language_override | string | 对于文本索引,该参数指定了包含在文档中的字段名,语言覆盖默认的 language,默认值为 language. | - -## MongoDB 聚合操作 - -MongoDB 中聚合(aggregate)主要用于处理数据(诸如统计平均值,求和等),并返回计算后的数据结果。有点类似 sql 语句中的 count(\*)。 - -### 管道 - -整个聚合运算过程称为管道,它是由多个步骤组成,每个管道 - -- 接受一系列文档(原始数据); -- 每个步骤对这些文档进行一系列运算; -- 结果文档输出给下一个步骤; - -聚合操作的基本格式 - -```shell -pipeline = [$stage1, $stage1, ..., $stageN]; - -db.<集合>.aggregate(pipeline, {options}); -``` - -### 聚合步骤 - -| 步骤 | 作用 | SQL 等价运算符 | -| -------------------- | -------- | --------------- | -| `$match` | 过滤 | WHERE | -| `$project` | 投影 | AS | -| `$sort` | 排序 | ORDER BY | -| `$group` | 分组 | GROUP BY | -| `$skip` / `$limit` | 结果限制 | SKIP / LIMIT | -| `$lookup` | 左外连接 | LEFT OUTER JOIN | -| `$unwind` | 展开数组 | N/A | -| `$graphLookup` | 图搜索 | N/A | -| `$facet` / `$bucket` | 分面搜索 | N/A | - -【示例】 - -```shell -> db.collection.insertMany([{"title":"MongoDB Overview","description":"MongoDB is no sql database","by_user":"collection","tagsr":["mongodb","database","NoSQL"],"likes":"100"},{"title":"NoSQL Overview","description":"No sql database is very fast","by_user":"collection","tagsr":["mongodb","database","NoSQL"],"likes":"10"},{"title":"Neo4j Overview","description":"Neo4j is no sql database","by_user":"Neo4j","tagsr":["neo4j","database","NoSQL"],"likes":"750"}]) -> db.collection.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : 1}}}]) -{ "_id" : null, "num_tutorial" : 3 } -{ "_id" : "Neo4j", "num_tutorial" : 1 } -{ "_id" : "collection", "num_tutorial" : 2 } -``` - -下表展示了一些聚合的表达式: - -| 表达式 | 描述 | 实例 | -| :---------- | :--------------------------------------------- | :-------------------------------------------------------------------------------------- | -| `$sum` | 计算总和。 | `db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$sum : "$likes"}}}])` | -| `$avg` | 计算平均值 | `db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$avg : "$likes"}}}])` | -| `$min` | 获取集合中所有文档对应值得最小值。 | `db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$min : "$likes"}}}])` | -| `$max` | 获取集合中所有文档对应值得最大值。 | `db.mycol.aggregate([{$group : {_id : "$by_user", num_tutorial : {$max : "$likes"}}}])` | -| `$push` | 在结果文档中插入值到一个数组中。 | `db.mycol.aggregate([{$group : {_id : "$by_user", url : {$push: "$url"}}}])` | -| `$addToSet` | 在结果文档中插入值到一个数组中,但不创建副本。 | `db.mycol.aggregate([{$group : {_id : "$by_user", url : {$addToSet : "$url"}}}])` | -| `$first` | 根据资源文档的排序获取第一个文档数据。 | `db.mycol.aggregate([{$group : {_id : "$by_user", first_url : {$first : "$url"}}}])` | -| `$last` | 根据资源文档的排序获取最后一个文档数据 | `db.mycol.aggregate([{$group : {_id : "$by_user", last_url : {$last : "$url"}}}])` | - -## 参考资料 - -- [MongoDB 官网](https://www.mongodb.com/) -- [MongoDB Github](https://github.com/mongodb/mongo) -- [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/03.MongoDB\347\232\204\350\201\232\345\220\210\346\223\215\344\275\234.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/03.MongoDB\347\232\204\350\201\232\345\220\210\346\223\215\344\275\234.md" deleted file mode 100644 index b699f0e0ff..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/03.MongoDB\347\232\204\350\201\232\345\220\210\346\223\215\344\275\234.md" +++ /dev/null @@ -1,400 +0,0 @@ ---- -title: MongoDB 的聚合操作 -date: 2020-09-21 21:22:57 -order: 03 -categories: - - 数据库 - - 文档数据库 - - MongoDB -tags: - - 数据库 - - 文档数据库 - - MongoDB - - 聚合 -permalink: /pages/75daa5/ ---- - -# MongoDB 的聚合操作 - -聚合操作处理数据记录并返回计算结果。聚合操作将来自多个 document 的值分组,并可以对分组的数据执行各种操作以返回单个结果。 MongoDB 提供了三种执行聚合的方式:聚合管道,map-reduce 函数和单一目的聚合方法。 - -## Pipeline - -### Pipeline 简介 - -MongoDB 的聚合框架以数据处理管道(Pipeline)的概念为模型。 - -**MongoDB 通过 [`db.collection.aggregate()`](https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/#db.collection.aggregate) 方法支持聚合操作**。并提供了 [`aggregate`](https://docs.mongodb.com/manual/reference/command/aggregate/#dbcmd.aggregate) 命令来执行 pipeline。 - -MongoDB Pipeline 由多个阶段([stages](https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/#aggregation-pipeline-operator-reference))组成。每个阶段在 document 通过 pipeline 时都会对其进行转换。pipeline 阶段不需要为每个输入 document 都生成一个输出 document。例如,某些阶段可能会生成新 document 或过滤 document。 - -同一个阶段可以在 pipeline 中出现多次,但 [`$out`](https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out)、[`$merge`](https://docs.mongodb.com/manual/reference/operator/aggregation/merge/#pipe._S_merge),和 [`$geoNear`](https://docs.mongodb.com/manual/reference/operator/aggregation/geoNear/#pipe._S_geoNear) 阶段除外。所有可用 pipeline 阶段可以参考:[Aggregation Pipeline Stages](https://docs.mongodb.com/manual/reference/operator/aggregation-pipeline/#aggregation-pipeline-operator-reference)。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921092725.png) - -- 第一阶段:[`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 阶段按状态字段过滤 document,然后将状态等于“ A”的那些 document 传递到下一阶段。 -- 第二阶段:[`$group`](https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group) 阶段按 cust_id 字段对 document 进行分组,以计算每个唯一 cust_id 的金额总和。 - -最基本的管道阶段提供过滤器,其操作类似于查询和 document 转换(修改输出 document 形式)。 - -其他管道操作提供了用于按特定字段对 document 进行分组和排序的工具,以及用于汇总数组(包括 document 数组)内容的工具。另外,管道阶段可以将运算符用于诸如计算平均值或连接字符串之类的任务。 - -聚合管道也可以在分片 collection 上操作。 - -### Pipeline 优化 - -#### 投影优化 - -Pipeline 可以确定是否仅需要 document 中必填字段即可获得结果。 - -#### Pipeline 串行优化 - -(`$project`、`$unset`、`$addFields`、`$set`) + `$match` 串行优化 - -对于包含投影阶段([`$project`](https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project) 或 [`$unset`](https://docs.mongodb.com/manual/reference/operator/aggregation/unset/#pipe._S_unset) 或 [`$addFields`](https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/#pipe._S_addFields) 或 [`$set`](https://docs.mongodb.com/manual/reference/operator/aggregation/set/#pipe._S_set)),且后续跟随着 [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 阶段的 Pipeline ,MongoDB 会将所有 [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 阶段中不需要在投影阶段中计算出的值的过滤器,移动一个在投影阶段之前的新 [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 阶段。 - -如果 Pipeline 包含多个投影阶段 和 / 或 [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 阶段,则 MongoDB 将为每个 [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 阶段执行此优化,将每个 [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 过滤器移动到该过滤器不依赖的所有投影阶段之前。 - -【示例】Pipeline 串行优化示例 - -优化前: - -```javascript -{ $addFields: { - maxTime: { $max: "$times" }, - minTime: { $min: "$times" } -} }, -{ $project: { - _id: 1, name: 1, times: 1, maxTime: 1, minTime: 1, - avgTime: { $avg: ["$maxTime", "$minTime"] } -} }, -{ $match: { - name: "Joe Schmoe", - maxTime: { $lt: 20 }, - minTime: { $gt: 5 }, - avgTime: { $gt: 7 } -} } -``` - -优化后: - -```javascript -{ $match: { name: "Joe Schmoe" } }, -{ $addFields: { - maxTime: { $max: "$times" }, - minTime: { $min: "$times" } -} }, -{ $match: { maxTime: { $lt: 20 }, minTime: { $gt: 5 } } }, -{ $project: { - _id: 1, name: 1, times: 1, maxTime: 1, minTime: 1, - avgTime: { $avg: ["$maxTime", "$minTime"] } -} }, -{ $match: { avgTime: { $gt: 7 } } } -``` - -说明: - -`{ name: "Joe Schmoe" }` 不需要计算任何投影阶段的值,所以可以放在最前面。 - -`{ avgTime: { $gt: 7 } }` 依赖 [`$project`](https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project) 阶段的 `avgTime` 字段,所以不能移动。 - -`maxTime` 和 `minTime` 字段被 [`$addFields`](https://docs.mongodb.com/manual/reference/operator/aggregation/addFields/#pipe._S_addFields) 阶段所依赖,但自身不依赖其他,所以会新建一个 [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) 阶段,并将其置于 [`$project`](https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project) 阶段之前。 - -#### Pipeline 并行优化 - -如果可能,优化阶段会将 Pipeline 阶段合并到其前身。通常,合并发生在任意序列重新排序优化之后。 - -##### `$sort` + `$limit` - -当 [`$sort`](https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort) 在 [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit) 之前时,如果没有中间阶段修改文档数量(例如 [`$unwind`](https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind)、[`$group`](https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group)),则优化程序可以将 [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit) 合并到 [`$sort`](https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort) 中。如果有管道阶段更改了 [`$sort`](https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort) 和 [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit) 阶段之间的文档数,则 MongoDB 不会将 [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit) 合并到 [`$sort`](https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort) 中。 - -【示例】`$sort` + `$limit` - -优化前: - -```javascript -{ $sort : { age : -1 } }, -{ $project : { age : 1, status : 1, name : 1 } }, -{ $limit: 5 } -``` - -优化后: - -```javascript -{ - "$sort" : { - "sortKey" : { - "age" : -1 - }, - "limit" : NumberLong(5) - } -}, -{ "$project" : { - "age" : 1, - "status" : 1, - "name" : 1 - } -} -``` - -##### `$limit` + `$limit` - -如果一个 [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit) 紧随另一个 [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit),那么它们可以合并为一。 - -优化前: - -```javascript -{ $limit: 100 }, -{ $limit: 10 } -``` - -优化后: - -```javascript -{ - $limit: 10 -} -``` - -##### `$skip` + `$skip` - -如果一个 [`$skip`](https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip) 紧随另一个 [`$skip`](https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip) ,那么它们可以合并为一。 - -优化前: - -```javascript -{ $skip: 5 }, -{ $skip: 2 } -``` - -优化后: - -```javascript -{ - $skip: 7 -} -``` - -##### `$match` + `$match` - -如果一个 [`$skip`](https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip) 紧随另一个 [`$skip`](https://docs.mongodb.com/manual/reference/operator/aggregation/skip/#pipe._S_skip) ,那么它们可以通过 [`$and`](https://docs.mongodb.com/manual/reference/operator/aggregation/and/#exp._S_and) 合并为一。 - -优化前: - -```javascript -{ $match: { year: 2014 } }, -{ $match: { status: "A" } } -``` - -优化后: - -```javascript -{ - $match: { - $and: [{ year: 2014 }, { status: 'A' }] - } -} -``` - -##### `$lookup` + `$unwind` - -如果一个 [`$unwind`](https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind) 紧随另一个 [`$lookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup),并且 [`$unwind`](https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind) 在 [`$lookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup) 的 as 字段上运行时,优化程序可以将 [`$unwind`](https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind) 合并到 [`$lookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup) 阶段。这样可以避免创建较大的中间文档。 - -优化前: - -```javascript -{ - $lookup: { - from: "otherCollection", - as: "resultingArray", - localField: "x", - foreignField: "y" - } -}, -{ $unwind: "$resultingArray"} -``` - -优化后: - -```javascript -{ - $lookup: { - from: "otherCollection", - as: "resultingArray", - localField: "x", - foreignField: "y", - unwinding: { preserveNullAndEmptyArrays: false } - } -} -``` - -### Pipeline 限制 - -结果集中的每个文档均受 BSON 文档大小限制(当前为 16 MB) - -Pipeline 的内存限制为 100 MB。 - -## Map-Reduce - -> 聚合 pipeline 比 map-reduce 提供更好的性能和更一致的接口。 - -Map-reduce 是一种数据处理范式,用于将大量数据汇总为有用的聚合结果。为了执行 map-reduce 操作,MongoDB 提供了 [`mapReduce`](https://docs.mongodb.com/manual/reference/command/mapReduce/#dbcmd.mapReduce) 数据库命令。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921155546.svg) - -在上面的操作中,MongoDB 将 map 阶段应用于每个输入 document(即 collection 中与查询条件匹配的 document)。 map 函数分发出多个键-值对。对于具有多个值的那些键,MongoDB 应用 reduce 阶段,该阶段收集并汇总聚合的数据。然后,MongoDB 将结果存储在 collection 中。可选地,reduce 函数的输出可以通过 finalize 函数来进一步汇总聚合结果。 - -MongoDB 中的所有 map-reduce 函数都是 JavaScript,并在 mongod 进程中运行。 Map-reduce 操作将单个 collection 的 document 作为输入,并且可以在开始 map 阶段之前执行任意排序和限制。 mapReduce 可以将 map-reduce 操作的结果作为 document 返回,也可以将结果写入 collection。 - -## 单一目的聚合方法 - -MongoDB 支持一下单一目的的聚合操作: - -- [`db.collection.estimatedDocumentCount()`](https://docs.mongodb.com/manual/reference/method/db.collection.estimatedDocumentCount/#db.collection.estimatedDocumentCount) -- [`db.collection.count()`](https://docs.mongodb.com/manual/reference/method/db.collection.count/#db.collection.count) -- [`db.collection.distinct()`](https://docs.mongodb.com/manual/reference/method/db.collection.distinct/#db.collection.distinct) - -所有这些操作都汇总了单个 collection 中的 document。尽管这些操作提供了对常见聚合过程的简单访问,但是它们相比聚合 pipeline 和 map-reduce,缺少灵活性和丰富的功能性。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921155935.svg) - -## SQL 和 MongoDB 聚合对比 - -MongoDB pipeline 提供了许多等价于 SQL 中常见聚合语句的操作。 - -下表概述了常见的 SQL 聚合语句或函数和 MongoDB 聚合操作的映射表: - -| SQL Terms, Functions, and Concepts | MongoDB Aggregation Operators | -| :--------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `WHERE` | [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) | -| `GROUP BY` | [`$group`](https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group) | -| `HAVING` | [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) | -| `SELECT` | [`$project`](https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project) | -| `ORDER BY` | [`$sort`](https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort) | -| `LIMIT` | [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit) | -| `SUM()` | [`$sum`](https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum) | -| `COUNT()` | [`$sum`](https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum)[`$sortByCount`](https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/#pipe._S_sortByCount) | -| `JOIN` | [`$lookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup) | -| `SELECT INTO NEW_TABLE` | [`$out`](https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out) | -| `MERGE INTO TABLE` | [`$merge`](https://docs.mongodb.com/manual/reference/operator/aggregation/merge/#pipe._S_merge) (Available starting in MongoDB 4.2) | -| `UNION ALL` | [`$unionWith`](https://docs.mongodb.com/manual/reference/operator/aggregation/unionWith/#pipe._S_unionWith) (Available starting in MongoDB 4.4) | - -【示例】 - -```javascript -db.orders.insertMany([ - { - _id: 1, - cust_id: 'Ant O. Knee', - ord_date: new Date('2020-03-01'), - price: 25, - items: [ - { sku: 'oranges', qty: 5, price: 2.5 }, - { sku: 'apples', qty: 5, price: 2.5 } - ], - status: 'A' - }, - { - _id: 2, - cust_id: 'Ant O. Knee', - ord_date: new Date('2020-03-08'), - price: 70, - items: [ - { sku: 'oranges', qty: 8, price: 2.5 }, - { sku: 'chocolates', qty: 5, price: 10 } - ], - status: 'A' - }, - { - _id: 3, - cust_id: 'Busby Bee', - ord_date: new Date('2020-03-08'), - price: 50, - items: [ - { sku: 'oranges', qty: 10, price: 2.5 }, - { sku: 'pears', qty: 10, price: 2.5 } - ], - status: 'A' - }, - { - _id: 4, - cust_id: 'Busby Bee', - ord_date: new Date('2020-03-18'), - price: 25, - items: [{ sku: 'oranges', qty: 10, price: 2.5 }], - status: 'A' - }, - { - _id: 5, - cust_id: 'Busby Bee', - ord_date: new Date('2020-03-19'), - price: 50, - items: [{ sku: 'chocolates', qty: 5, price: 10 }], - status: 'A' - }, - { - _id: 6, - cust_id: 'Cam Elot', - ord_date: new Date('2020-03-19'), - price: 35, - items: [ - { sku: 'carrots', qty: 10, price: 1.0 }, - { sku: 'apples', qty: 10, price: 2.5 } - ], - status: 'A' - }, - { - _id: 7, - cust_id: 'Cam Elot', - ord_date: new Date('2020-03-20'), - price: 25, - items: [{ sku: 'oranges', qty: 10, price: 2.5 }], - status: 'A' - }, - { - _id: 8, - cust_id: 'Don Quis', - ord_date: new Date('2020-03-20'), - price: 75, - items: [ - { sku: 'chocolates', qty: 5, price: 10 }, - { sku: 'apples', qty: 10, price: 2.5 } - ], - status: 'A' - }, - { - _id: 9, - cust_id: 'Don Quis', - ord_date: new Date('2020-03-20'), - price: 55, - items: [ - { sku: 'carrots', qty: 5, price: 1.0 }, - { sku: 'apples', qty: 10, price: 2.5 }, - { sku: 'oranges', qty: 10, price: 2.5 } - ], - status: 'A' - }, - { - _id: 10, - cust_id: 'Don Quis', - ord_date: new Date('2020-03-23'), - price: 25, - items: [{ sku: 'oranges', qty: 10, price: 2.5 }], - status: 'A' - } -]) -``` - -SQL 和 MongoDB 聚合方式对比: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921200556.png) - -## 参考资料 - -- **官方** - - [MongoDB 官网](https://www.mongodb.com/) - - [MongoDB Github](https://github.com/mongodb/mongo) - - [MongoDB 官方免费教程](https://university.mongodb.com/) -- **教程** - - [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) - - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/04.MongoDB\344\272\213\345\212\241.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/04.MongoDB\344\272\213\345\212\241.md" deleted file mode 100644 index 781b34286a..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/04.MongoDB\344\272\213\345\212\241.md" +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: MongoDB 事务 -date: 2020-09-20 23:12:17 -order: 04 -categories: - - 数据库 - - 文档数据库 - - MongoDB -tags: - - 数据库 - - 文档数据库 - - MongoDB - - 事务 -permalink: /pages/4574fe/ ---- - -# MongoDB 事务 - -writeConcern 可以决定写操作到达多少个节点才算成功。 - -- 默认:多节点复制集不做任何设定,所以是有可能丢失数据。 -- `w: "majority"`:大部分节点确认,就视为写成功 -- `w: "all"`:全部节点确认,才视为写成功 - -journal 则定义如何才算成功。取值包括: - -- `true`:写操作落到 journal 文件中才算成功; -- `false`:写操作达到内存即算作成功。 - -【示例】在集群中使用 writeConcern 参数 - -```javascript -db.transaction.insert({ count: 1 }, { writeConcern: { w: 'majoriy' } }) -db.transaction.insert({ count: 1 }, { writeConcern: { w: '4' } }) -db.transaction.insert({ count: 1 }, { writeConcern: { w: 'all' } }) -``` - -【示例】配置延迟节点,模拟网络延迟 - -``` -conf=rs.conf() -conf.memebers[2].slaveDelay=5 -conf.memebers[2].priority=0 -rs.reconfig(conf) -``` \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/05.MongoDB\345\273\272\346\250\241.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/05.MongoDB\345\273\272\346\250\241.md" deleted file mode 100644 index 0bade5f358..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/05.MongoDB\345\273\272\346\250\241.md" +++ /dev/null @@ -1,388 +0,0 @@ ---- -title: MongoDB 建模 -date: 2020-09-09 20:47:14 -order: 05 -categories: - - 数据库 - - 文档数据库 - - MongoDB -tags: - - 数据库 - - 文档数据库 - - MongoDB - - 建模 -permalink: /pages/562f99/ ---- - -# MongoDB 建模 - -MongoDB 的数据模式是一种灵活模式,关系型数据库要求你在插入数据之前必须先定义好一个表的模式结构,而 MongoDB 的集合则并不限制 document 结构。这种灵活性让对象和数据库文档之间的映射变得很容易。即使数据记录之间有很大的变化,每个文档也可以很好的映射到各条不同的记录。 当然在实际使用中,同一个集合中的文档往往都有一个比较类似的结构。 - -数据模型设计中最具挑战性的是在应用程序需求,数据库引擎性能要求和数据读写模式之间做权衡考量。当设计数据模型的时候,一定要考虑应用程序对数据的使用模式(如查询,更新和处理)以及数据本身的天然结构。 - -## MongoDB 数据建模入门 - -> 参考:https://docs.mongodb.com/guides/server/introduction/#what-you-ll-need - -### (一)定义数据集 - -当需要建立数据存储时,首先应该思考以下问题:需要存储哪些数据?这些字段之间如何关联? - -这是一个数据建模的过程。目标是**将业务需求抽象为逻辑模型**。 - -假设这样一个场景:我们需要建立数据库以跟踪物料及其数量,大小,标签和等级。 - -如果是存储在 RDBMS,可能以下的数据表: - -| name | quantity | size | status | tags | rating | -| :------- | :------- | :---------- | :----- | :----------------------- | :----- | -| journal | 25 | 14x21,cm | A | brown, lined | 9 | -| notebook | 50 | 8.5x11,in | A | college-ruled,perforated | 8 | -| paper | 100 | 8.5x11,in | D | watercolor | 10 | -| planner | 75 | 22.85x30,cm | D | 2019 | 10 | -| postcard | 45 | 10x,cm | D | double-sided,white | 2 | - -### (二)思考 JSON 结构 - -从上例中可以看出,表似乎是存储数据的好地方,但该数据集中的字段需要多个值,如果在单个列中建模,则不容易搜索或显示(对于 例如–大小和标签)。 - -在 SQL 数据库中,您可以通过创建关系表来解决此问题。 - -在 MongoDB 中,数据存储为文档(document)。 这些文档以 JSON(JavaScript 对象表示法)格式存储在 MongoDB 中。 JSON 文档支持嵌入式字段,因此相关数据和数据列表可以与文档一起存储,而不是与外部表一起存储。 - -JSON 格式为键/值对。 在 JSON 文档中,字段名和值用冒号分隔,字段名和值对用逗号分隔,并且字段集封装在“大括号”(`{}`)中。 - -如果要开始对上面的行之一进行建模,例如此行: - -| name | quantity | size | status | tags | rating | -| :------- | :------- | :-------- | :----- | :----------------------- | :----- | -| notebook | 50 | 8.5x11,in | A | college-ruled,perforated | 8 | - -您可以从 name 和 quantity 字段开始。 在 JSON 中,这些字段如下所示: - -```json -{ "name": "notebook", "qty": 50 } -``` - -### (三)确定哪些字段作为嵌入式数据 - -接下来,需要确定哪些字段可能需要多个值。可以考虑将这些字段作为嵌入式文档或嵌入式文档中的 列表/数组 对象。 - -例如,在上面的示例中,size 可能包含三个字段: - -```json -{ "h": 11, "w": 8.5, "uom": "in" } -``` - -And some items have multiple ratings, so `ratings` might be represented as a list of documents containing the field `scores`: - -```json -[{ "score": 8 }, { "score": 9 }] -``` - -And you might need to handle multiple tags per item. So you might store them in a list too. - -```json -["college-ruled", "perforated"] -``` - -Finally, a JSON document that stores an inventory item might look like this: - -```json -{ - "name": "notebook", - "qty": 50, - "rating": [{ "score": 8 }, { "score": 9 }], - "size": { "height": 11, "width": 8.5, "unit": "in" }, - "status": "A", - "tags": ["college-ruled", "perforated"] -} -``` - -This looks very different from the tabular data structure you started with in Step 1. - -## 数据模型简介 - -数据建模中的关键挑战是平衡应用程序的需求、数据库引擎的性能以及数据检索模式。 在设计数据模型时,始终需要考虑数据的应用程序使用情况(即数据的查询,更新和处理)以及数据本身的固有结构。 - -### 灵活的 Schema - -在关系型数据库中,必须在插入数据之前确定并声明表的结构。而 MongoDB 的 collection 默认情况下不需要其文档具有相同的架构。也就是说: - -同一个 collection 中的 document 不需要具有相同的 field 集,并且 field 的数据类型可以在集合中的不同文档之间有所不同。 - -要更改 collection 中的 document 结构,例如添加新 field,删除现有 field 或将 field 值更改为新类型,只需要将文档更新为新结构即可。 - -这种灵活性有助于将 document 映射到实体或对象。每个 document 都可以匹配所表示实体的数据字段,即使该文档与集合中的其他文档有很大的不同。但是,实际上,集合中的文档具有相似的结构,并且您可以在更新和插入操作期间对 collection 强制执行 document 校验规则。 - -### Document 结构 - -#### 嵌入式数据模型 - -嵌入式 document 通过将相关数据存储在单个 document 结构中来捕获数据之间的关系。 MongoDB document 可以将 document 结构嵌入到另一个 document 中的字段或数组中。这些非规范化的数据模型允许应用程序在单个数据库操作中检索和操纵相关数据。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200910193231.png) - -对于 MongoDB 中的很多场景,非规范化数据模型都是最佳的。 - -> 嵌入式 document 有大小限制:必须小于 16 MB。 -> -> 如果是较大的二进制数据,可以考虑 [GridFS](https://docs.mongodb.com/manual/core/gridfs/)。 - -#### 引用式数据模型 - -引用通过包含从一个 document 到另一个 document 的链接或引用来存储数据之间的关系。 应用程序可以解析这些引用以访问相关数据。 广义上讲,这些是规范化的数据模型。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200910193234.png) - -通常,在以下场景使用引用式的数据模型: - -- 嵌入时会导致数据重复,但无法提供足够的读取性能优势,无法胜过重复的含义。 -- 代表更复杂的多对多关系。 -- 为大规模分层数据集建模。 - -为了 join collection,MongoDB 支持聚合 stage: - -- [`$lookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup)(MongoDB 3.2 开始支持) -- [`$graphLookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/graphLookup/#pipe._S_graphLookup)(MongoDB 3.4 开始支持) - -MongoDB 还提供了引用来支持跨集合 join 数据: - -- 引用数据模型示例,参考:[Model One-to-Many Relationships with Document References](https://docs.mongodb.com/manual/tutorial/model-referenced-one-to-many-relationships-between-documents/#data-modeling-publisher-and-books). -- 更多树形模型,参考:[Model Tree Structures](https://docs.mongodb.com/manual/applications/data-models-tree-structures/). - -### 原子写操作 - -#### 单 document 的原子性 - -在 MongoDB 中,针对单个 document 的写操作是原子性的,即使该 document 中嵌入了多个子 document。 具有嵌入数据的非规范化数据模型将所有相关数据合并在一个 document 中,而不是在多个 document 和 collection 中进行规范化。 该数据模型有助于原子操作。 当单个写入操作(例如 [`db.collection.updateMany()`](https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/#db.collection.updateMany))修改多个 document 时,每个 document 的独立修改是原子的,但整个操作不是原子的。 - -#### 多 document 事务 - -对于需要对多个 document(在单个或多个集合中)进行读写原子性的情况,MongoDB 支持多 document 事务。 - -- 在版本 4.0 中,MongoDB 在副本集上支持多 document 事务。 -- 在版本 4.2 中,MongoDB 引入了分布式事务,它增加了对分片群集上多 document 事务的支持,并合并了对副本集上多 document 事务的现有支持。 - -> 在大多数情况下,多 document 事务会比单 document 的写入产生更高的性能消耗,并且多 document 事务的可用性不能替代高效的结构设计。 在许多情况下,非规范化数据模型(嵌入式 document 和数组)仍是最佳选择。 也就是说,合理的数据建模,将最大程度地减少对多 document 事务的需求。 - -### 数据使用和性能 - -在设计数据模型时,请考虑应用程序将如何使用您的数据库。 例如,如果您的应用程序仅使用最近插入的 document,请考虑使用上限集合。 或者,如果您的应用程序主要是对 collection 的读取操作,则添加索引以提高性能。 - -## Schema 校验 - -### 指定校验规则 - -如果创建新 collection 时要指定校验规则,需要在使用 [`db.createCollection()`](https://docs.mongodb.com/manual/reference/method/db.createCollection/#db.createCollection) 时指定 `validator` 选项。 - -如果要将 document 校验添加到现有 collection 中,需要使用带有 `validator` 选项的 [`collMod`](https://docs.mongodb.com/manual/reference/command/collMod/#dbcmd.collMod) 命令。 - -MongoDB 还提供以下相关选项: - -- `validationLevel` 选项(用于确定 MongoDB 在更新过程中,对现有 document 应用校验规则的严格程度) -- `validationAction` 选项(用于确定 MongoDB 发现违反校验规则的 document 时,是选择报错并拒绝,还是接受数据但在日志中告警)。 - -### JSON Schema - -从 3.6 版本开始,MongoDB 开始支持 JSON Schema 校验。 - -可以通过在 validator 表达式中使用 [`$jsonSchema`](https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#op._S_jsonSchema) 操作来指定 JSON Schema 校验。 - -【示例】 - -```javascript -db.createCollection('students', { - validator: { - $jsonSchema: { - bsonType: 'object', - required: ['name', 'year', 'major', 'address'], - properties: { - name: { - bsonType: 'string', - description: 'must be a string and is required' - }, - year: { - bsonType: 'int', - minimum: 2017, - maximum: 3017, - description: 'must be an integer in [ 2017, 3017 ] and is required' - }, - major: { - enum: ['Math', 'English', 'Computer Science', 'History', null], - description: 'can only be one of the enum values and is required' - }, - gpa: { - bsonType: ['double'], - description: 'must be a double if the field exists' - }, - address: { - bsonType: 'object', - required: ['city'], - properties: { - street: { - bsonType: 'string', - description: 'must be a string if the field exists' - }, - city: { - bsonType: 'string', - description: 'must be a string and is required' - } - } - } - } - } - } -}) -``` - -### 其它查询表达式 - -除了使用 [`$jsonSchema`](https://docs.mongodb.com/manual/reference/operator/query/jsonSchema/#op._S_jsonSchema) 查询运算符的 JSON Schema 校验外,MongoDB 还支持其它查询运算符的校验,但以下情况除外: - -- [`$near`](https://docs.mongodb.com/manual/reference/operator/query/near/#op._S_near), -- [`$nearSphere`](https://docs.mongodb.com/manual/reference/operator/query/nearSphere/#op._S_nearSphere), -- [`$text`](https://docs.mongodb.com/manual/reference/operator/query/text/#op._S_text), -- [`$where`](https://docs.mongodb.com/manual/reference/operator/query/where/#op._S_where), and -- 带有 [`$function`](https://docs.mongodb.com/manual/reference/operator/aggregation/function/#exp._S_function) 表达式的 [`$expr`](https://docs.mongodb.com/manual/reference/operator/query/expr/#op._S_expr) - -【示例】查询表达式中指定校验规则 - -```javascript -db.createCollection('contacts', { - validator: { - $or: [ - { phone: { $type: 'string' } }, - { email: { $regex: /@mongodb\.com$/ } }, - { status: { $in: ['Unknown', 'Incomplete'] } } - ] - } -}) -``` - -### 行为 - -校验发生在更新和插入期间。添加校验规则到 collection 时,不会对现有的 document 进行校验,除非发生修改操作。 - -#### 现有的 document - -`validationLevel` 选项确定 MongoDB 进行规则校验时执行的操作: - -- 如果 `validationLevel` 是 strict(严格级别。这是 MongoDB 默认级别),则 MongoDB 将校验规则应用于所有插入和更新。 -- 如果 `validationLevel` 是 moderate(中等级别),则 MongoDB 只对已满足校验条件的现有文档的插入和更新操作进行校验;对不符合校验标准的现有文档的更新操作不进行校验。 - -【示例】 - -下面是一个正常的插入操作: - -```javascript -db.contacts.insert([ - { - _id: 1, - name: 'Anne', - phone: '+1 555 123 456', - city: 'London', - status: 'Complete' - }, - { _id: 2, name: 'Ivan', city: 'Vancouver' } -]) -``` - -在 collection 上配置一个校验规则: - -```javascript -db.runCommand({ - collMod: 'contacts', - validator: { - $jsonSchema: { - bsonType: 'object', - required: ['phone', 'name'], - properties: { - phone: { - bsonType: 'string', - description: 'must be a string and is required' - }, - name: { - bsonType: 'string', - description: 'must be a string and is required' - } - } - } - }, - validationLevel: 'moderate' -}) -``` - -则 `contacts` collection 现在添加了含中等级别(moderate) validationLevel 的 `validator`: - -- 如果尝试更新 `_id`为 1 的文档,则 MongoDB 将应用校验规则,因为现有文档符合条件。 - -- 相反,MongoDB 不会将校验 `_id` 为 2 的文档,因为它不符合校验规则。 - -如果要完全禁用校验,可以将 `validationLevel` 置为 `off`。 - -#### 接受或拒绝无效的 document - -- 如果 validationAction 是 Error(默认),则 MongoDB 拒绝任何违反校验规则的插入或更新。 -- 如果 validationAction 是 Warn,MongoDB 会记录所有的违规,但允许进行插入或更新。 - -【示例】 - -创建集合时,配置 `validationAction` 为 warn。 - -```javascript -db.createCollection('contacts2', { - validator: { - $jsonSchema: { - bsonType: 'object', - required: ['phone'], - properties: { - phone: { - bsonType: 'string', - description: 'must be a string and is required' - }, - email: { - bsonType: 'string', - pattern: '@mongodb.com$', - description: - 'must be a string and match the regular expression pattern' - }, - status: { - enum: ['Unknown', 'Incomplete'], - description: 'can only be one of the enum values' - } - } - } - }, - validationAction: 'warn' -}) -``` - -尝试插入一条违规记录 - -```javascript -> db.contacts2.insert( { name: "Amanda", status: "Updated" } ) -WriteResult({ "nInserted" : 1 }) -``` - -MongoDB 允许这条操作执行,但是服务器会记录下告警信息。 - -``` -{"t":{"$date":"2020-09-11T16:35:57.754+08:00"},"s":"W", "c":"STORAGE", "id":20294, "ctx":"conn14","msg":"Document would fail validation","attr":{"namespace":"test.contacts2","document":{"_id":{"$oid":"5f5b36ed8ea53d62a0b51c4e"},"name":"Amanda","status":"Updated"}}} -``` - -#### 限制 - -不能在 `admin`、`local`、`config` 这几个特殊的数据库中指定校验规则。 - -不能在 `system.*` collection 中指定校验。 - -## 参考资料 - -- **官方** - - [MongoDB 官网](https://www.mongodb.com/) - - [MongoDB Github](https://github.com/mongodb/mongo) - - [MongoDB 官方免费教程](https://university.mongodb.com/) -- **教程** - - [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) - - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/02.MongoDB\347\232\204CRUD\346\223\215\344\275\234.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_CRUD.md" similarity index 56% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/02.MongoDB\347\232\204CRUD\346\223\215\344\275\234.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_CRUD.md" index 3d63b9348f..eddebd5b26 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/02.MongoDB\347\232\204CRUD\346\223\215\344\275\234.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_CRUD.md" @@ -1,7 +1,7 @@ --- +icon: logos:mongodb title: MongoDB 的 CRUD 操作 date: 2020-09-25 21:23:41 -order: 02 categories: - 数据库 - 文档数据库 @@ -10,42 +10,35 @@ tags: - 数据库 - 文档数据库 - MongoDB -permalink: /pages/7efbac/ +permalink: /pages/773f1695/ --- # MongoDB 的 CRUD 操作 -## 一、基本 CRUD 操作 - MongoDB 的 CRUD 操作是针对 document 的读写操作。 -### Create 操作 +MongoDB 中的所有写入操作在单个文档级别上都是**原子**的。 -MongoDB 提供以下操作向一个 collection 插入 document +## 插入操作 -- [`db.collection.insertOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.insertOne/#db.collection.insertOne):插入一条 document -- [`db.collection.insertMany()`](https://docs.mongodb.com/manual/reference/method/db.collection.insertMany/#db.collection.insertMany):插入多条 document +MongoDB 提供了以下方法将文档插入集合: -> 注:以上操作都是原子操作。 +- [`db.collection.insertOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.insertOne/#db.collection.insertOne) +- [`db.collection.insertMany()`](https://docs.mongodb.com/manual/reference/method/db.collection.insertMany/#db.collection.insertMany) ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200924112342.svg) 插入操作的特性: -- MongoDB 中的所有写操作都是单个文档级别的原子操作。 +- MongoDB 中的所有写入操作在单个文档级别上都是原子的。 - 如果要插入的 collection 当前不存在,则插入操作会自动创建 collection。 -- 在 MongoDB 中,存储在集合中的每个文档都需要一个唯一的 [`_id`](https://docs.mongodb.com/manual/reference/glossary/#term-id) 字段作为主键。如果插入的文档省略 `_id` 字段,则 MongoDB 驱动程序会自动为 `_id` 字段生成 ObjectId。 +- 如果文档未指定 `_id` 字段,则 MongoDB 会将 `_id` 字段添加 ObjectId 值到新文档中。 - 可以 MongoDB 写入操作的确认级别来控制写入行为。 【示例】插入一条 document 示例 ```javascript -db.inventory.insertOne({ - item: 'canvas', - qty: 100, - tags: ['cotton'], - size: { h: 28, w: 35.5, uom: 'cm' } -}) +db.inventory.find({ item: 'canvas' }) ``` 【示例】插入多条 document 示例 @@ -73,19 +66,56 @@ db.inventory.insertMany([ ]) ``` -### Read 操作 +> 更多详情参考:https://www.mongodb.com/zh-cn/docs/manual/tutorial/insert-documents/ + +## 查询操作 -MongoDB 提供 [`db.collection.find()`](https://docs.mongodb.com/manual/reference/method/db.collection.find/#db.collection.find) 方法来检索 document。 +MongoDB 提供了 [`db.collection.find()`](https://www.mongodb.com/zh-cn/docs/manual/reference/method/db.collection.find/#mongodb-method-db.collection.find) 方法从集合中查找文档。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200924113832.svg) -### Update 操作 +可以在 `{}` 中指定查询条件来查找要匹配的数据。如果 `{}` 为空,则会返回集合中的所有文档。 + +```javascript +db.inventory.insertMany([ + { item: 'journal', qty: 25, size: { h: 14, w: 21, uom: 'cm' }, status: 'A' }, + { + item: 'notebook', + qty: 50, + size: { h: 8.5, w: 11, uom: 'in' }, + status: 'A' + }, + { item: 'paper', qty: 100, size: { h: 8.5, w: 11, uom: 'in' }, status: 'D' }, + { + item: 'planner', + qty: 75, + size: { h: 22.85, w: 30, uom: 'cm' }, + status: 'D' + }, + { + item: 'postcard', + qty: 45, + size: { h: 10, w: 15.25, uom: 'cm' }, + status: 'A' + } +]) + +// 查询集合中所有文档 +db.inventory.find({}) + +// 查询集合所有 status 等于 "D" 的文档 +db.inventory.find({ status: 'D' }) +``` + +> 更多详情参考:https://www.mongodb.com/zh-cn/docs/manual/tutorial/query-documents/ + +## 更新操作 -MongoDB 提供以下操作来更新 collection 中的 document +MongoDB 提供以下操作来更新集合中的文档: -- [`db.collection.updateOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/#db.collection.updateOne):更新一条 document -- [`db.collection.updateMany()`](https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/#db.collection.updateMany):更新多条 document -- [`db.collection.replaceOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/#db.collection.replaceOne):替换一条 document +- [`db.collection.updateOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.updateOne/#db.collection.updateOne) +- [`db.collection.updateMany()`](https://docs.mongodb.com/manual/reference/method/db.collection.updateMany/#db.collection.updateMany) +- [`db.collection.replaceOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.replaceOne/#db.collection.replaceOne) 语法格式: @@ -188,27 +218,43 @@ db.inventory.replaceOne( 更新操作的特性: -- MongoDB 中的所有写操作都是单个文档级别的原子操作。 +- MongoDB 中的所有写入操作在单个文档级别上都是原子性的。 - 一旦设置了,就无法更新或替换 [`_id`](https://docs.mongodb.com/manual/reference/glossary/#term-id) 字段。 - 除以下情况外,MongoDB 会在执行写操作后保留文档字段的顺序: - `_id` 字段始终是文档中的第一个字段。 - 包括重命名字段名称的更新可能导致文档中字段的重新排序。 - 如果更新操作中包含 `upsert : true` 并且没有 document 匹配过滤器,MongoDB 会新插入一个 document;如果有匹配的 document,MongoDB 会修改或替换这些 document。 -### Delete 操作 +## 删除操作 -MongoDB 提供以下操作来删除 collection 中的 document +MongoDB 提供了以下操作来删除集合中的文档: -- [`db.collection.deleteOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.deleteOne/#db.collection.deleteOne):删除一条 document -- [`db.collection.deleteMany()`](https://docs.mongodb.com/manual/reference/method/db.collection.deleteMany/#db.collection.deleteMany):删除多条 document +- [`db.collection.deleteOne()`](https://docs.mongodb.com/manual/reference/method/db.collection.deleteOne/#db.collection.deleteOne) +- [`db.collection.deleteMany()`](https://docs.mongodb.com/manual/reference/method/db.collection.deleteMany/#db.collection.deleteMany) ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200924120007.svg) -删除操作的特性: +可以指定用于标识要删除的文档的过滤器,这些筛选条件和 find 方法的过滤器语法一致。 + +【示例】删除集合中的所有文档 + +```javascript +db.inventory.deleteMany({}) +``` -- MongoDB 中的所有写操作都是单个文档级别的原子操作。 +【示例】删除 `status` 字段等于 `"A"` 的文档 -## 二、批量写操作 +```javascript +db.inventory.deleteMany({ status: 'A' }) +``` + +【示例】删除 `status` 为 `"D"` 的第一个文档 + +```javascript +db.inventory.deleteOne({ status: 'D' }) +``` + +## 批量写操作 MongoDB 通过 [`db.collection.bulkWrite()`](https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#db.collection.bulkWrite) 方法来支持批量写操作(包括批量插入、更新、删除)。 @@ -238,46 +284,37 @@ MongoDB 通过 [`db.collection.bulkWrite()`](https://docs.mongodb.com/manual/ref 【示例】批量写操作示例 +以下 [`bulkWrite()`](https://www.mongodb.com/zh-cn/docs/manual/reference/method/db.collection.bulkWrite/#mongodb-method-db.collection.bulkWrite) 示例对 `pizzas` 集合运行以下操作: + +- 使用 `insertOne` 添加两个文档。 +- 使用 `updateOne` 更新一个文档。 +- 使用 `deleteOne` 删除文档。 +- 使用 `replaceOne` 替换一个文档。 + ```javascript -try { - db.characters.bulkWrite([ - { - insertOne: { - document: { - _id: 4, - char: 'Dithras', - class: 'barbarian', - lvl: 4 - } - } - }, - { - insertOne: { - document: { - _id: 5, - char: 'Taeln', - class: 'fighter', - lvl: 3 - } - } - }, - { - updateOne: { - filter: { char: 'Eldon' }, - update: { $set: { status: 'Critical Injury' } } - } - }, - { deleteOne: { filter: { char: 'Brisbane' } } }, - { - replaceOne: { - filter: { char: 'Meldane' }, - replacement: { char: 'Tanys', class: 'oracle', lvl: 4 } - } +db.pizzas.bulkWrite([ + { + insertOne: { document: { _id: 3, type: 'beef', size: 'medium', price: 6 } } + }, + { + insertOne: { + document: { _id: 4, type: 'sausage', size: 'large', price: 10 } + } + }, + { + updateOne: { + filter: { type: 'cheese' }, + update: { $set: { price: 8 } } + } + }, + { deleteOne: { filter: { type: 'pepperoni' } } }, + { + replaceOne: { + filter: { type: 'vegan' }, + replacement: { type: 'tofu', size: 'small', price: 4 } } - ]) -} catch (e) { - print(e) -} + } +]) ``` ### 批量写操作策略 @@ -292,33 +329,14 @@ try { 要提高对分片集群的写入性能,请使用 [`bulkWrite()`](https://docs.mongodb.com/manual/reference/method/db.collection.bulkWrite/#db.collection.bulkWrite),并将可选参数顺序设置为 false。[`mongos`](https://docs.mongodb.com/manual/reference/program/mongos/#bin.mongos) 可以尝试同时将写入操作发送到多个分片。对于空集合,首先按照分片群集中的分割 [chunk](https://docs.mongodb.com/manual/reference/glossary/#term-chunk) 中的说明预拆分 collection。 -#### 避免单调节流 - -如果在一次插入操作中,分片 key 单调递增,那么所有的插入数据都会存入 collection 的最后一个 chunk,也就是存入一个分片中。因此,集群的插入容量将永远不会超过该单个分片的插入容量。 - -如果插入量大于单个分片可以处理的插入量,并且无法避免单调递增的分片键,那么请考虑对应用程序进行以下修改: - -- 反转分片密钥的二进制位。这样可以保留信息,并避免将插入顺序与值序列的增加关联起来。 -- 交换第一个和最后一个 16 位字以“随机”插入。 +#### 避免单调限速 -## SQL 和 MongoDB 对比 +如果分片键在插入期间单调增加,则所有已插入数据都会进入集合中的最后一个数据段,该数据段将始终出现在单个分片上。因此,集群的插入容量永远不会超过该单个分片的插入容量。 -### 术语和概念 +如果插入量大于单个分片可以处理的容量,并且无法避免分片键的单调增加,则可以考虑对应用程序进行以下修改: -| SQL 术语和概念 | MongoDB 术语和概念 | -| :-------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| database | [database](https://docs.mongodb.com/manual/reference/glossary/#term-database) | -| table | [collection](https://docs.mongodb.com/manual/reference/glossary/#term-collection) | -| row | [document](https://docs.mongodb.com/manual/reference/glossary/#term-document) 或 [BSON](https://docs.mongodb.com/manual/reference/glossary/#term-bson) | -| column | [field](https://docs.mongodb.com/manual/reference/glossary/#term-field) | -| index | [index](https://docs.mongodb.com/manual/reference/glossary/#term-index) | -| table joins | [`$lookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup)、嵌入式文档 | -| primary key | [primary key](https://docs.mongodb.com/manual/reference/glossary/#term-primary-key)
    MongoDB 中自动设置主键为 [`_id`](https://docs.mongodb.com/manual/reference/glossary/#term-id) 字段 | -| aggregation (e.g. group by) | aggregation pipeline
    参考 [SQL to Aggregation Mapping Chart](https://docs.mongodb.com/manual/reference/sql-aggregation-comparison/). | -| SELECT INTO NEW_TABLE | [`$out`](https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out)
    参考 [SQL to Aggregation Mapping Chart](https://docs.mongodb.com/manual/reference/sql-aggregation-comparison/) | -| MERGE INTO TABLE | [`$merge`](https://docs.mongodb.com/manual/reference/operator/aggregation/merge/#pipe._S_merge) (MongoDB 4.2 开始支持)
    参考 [SQL to Aggregation Mapping Chart](https://docs.mongodb.com/manual/reference/sql-aggregation-comparison/). | -| UNION ALL | [`$unionWith`](https://docs.mongodb.com/manual/reference/operator/aggregation/unionWith/#pipe._S_unionWith) (MongoDB 4.4 开始支持) | -| transactions | [transactions](https://docs.mongodb.com/manual/core/transactions/) | +- 反转分片键的二进制位。这样将保留信息,并避免将插入顺序与递增的值序列相关联。 +- 交换第一个和最后一个 16 位字,“随机打乱”插入。 ## 参考资料 @@ -328,4 +346,4 @@ try { - [MongoDB 官方免费教程](https://university.mongodb.com/) - **教程** - [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) - - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) \ No newline at end of file + - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\344\272\213\345\212\241.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\344\272\213\345\212\241.md" new file mode 100644 index 0000000000..59f2185f4f --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\344\272\213\345\212\241.md" @@ -0,0 +1,109 @@ +--- +icon: logos:mongodb +title: MongoDB 事务 +date: 2020-09-20 23:12:17 +categories: + - 数据库 + - 文档数据库 + - MongoDB +tags: + - 数据库 + - 文档数据库 + - MongoDB + - 事务 +permalink: /pages/69582aae/ +--- + +# MongoDB 事务 + +在 MongoDB 中,**对单个文档的操作是原子的**。由于可以使用嵌入式文档和数组来捕获单个文档结构中数据之间的关系,而不是跨多个文档和集合进行规范化,因此这种单文档原子性在许多实际用例中消除了对分布式事务的需求。 + +对于需要对多个文档(在单个或多个集合中)进行读取和写入原子性的情况,MongoDB 支持分布式事务。借助分布式事务,可以跨多个操作、集合、数据库、文档和分片使用事务。 + +【示例】使用 MongoDB Java Driver 进行事务操作 + +```java +/* + For a replica set, include the replica set name and a seedlist of the members in the URI string; e.g. + String uri = "mongodb://mongodb0.example.com:27017,mongodb1.example.com:27017/admin?replicaSet=myRepl"; + For a sharded cluster, connect to the mongos instances. + For example: + String uri = "mongodb://mongos0.example.com:27017,mongos1.example.com:27017:27017/admin"; + */ +final MongoClient client = MongoClients.create(uri); +/* + Create collections. + */ +client.getDatabase("mydb1").getCollection("foo") + .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("abc", 0)); +client.getDatabase("mydb2").getCollection("bar") + .withWriteConcern(WriteConcern.MAJORITY).insertOne(new Document("xyz", 0)); +/* Step 1: Start a client session. */ +final ClientSession clientSession = client.startSession(); +/* Step 2: Optional. Define options to use for the transaction. */ +TransactionOptions txnOptions = TransactionOptions.builder() + .writeConcern(WriteConcern.MAJORITY) + .build(); +/* Step 3: Define the sequence of operations to perform inside the transactions. */ +TransactionBody txnBody = new TransactionBody() { + public String execute() { + MongoCollection coll1 = client.getDatabase("mydb1").getCollection("foo"); + MongoCollection coll2 = client.getDatabase("mydb2").getCollection("bar"); + /* + Important:: You must pass the session to the operations. + */ + coll1.insertOne(clientSession, new Document("abc", 1)); + coll2.insertOne(clientSession, new Document("xyz", 999)); + return "Inserted into collections in different databases"; + } +}; +try { + /* + Step 4: Use .withTransaction() to start a transaction, + execute the callback, and commit (or abort on error). + */ + clientSession.withTransaction(txnBody, txnOptions); +} catch (RuntimeException e) { + // some error handling +} finally { + clientSession.close(); +} +``` + +writeConcern 可以决定写操作到达多少个节点才算成功。 + +- 默认:多节点复制集不做任何设定,所以是有可能丢失数据。 +- `w: "majority"`:大部分节点确认,就视为写成功 +- `w: "all"`:全部节点确认,才视为写成功 + +journal 则定义如何才算成功。取值包括: + +- `true`:写操作落到 journal 文件中才算成功; +- `false`:写操作达到内存即算作成功。 + +【示例】在集群中使用 writeConcern 参数 + +```javascript +db.transaction.insert({ count: 1 }, { writeConcern: { w: 'majoriy' } }) +db.transaction.insert({ count: 1 }, { writeConcern: { w: '4' } }) +db.transaction.insert({ count: 1 }, { writeConcern: { w: 'all' } }) +``` + +【示例】配置延迟节点,模拟网络延迟 + +``` +conf=rs.conf() +conf.memebers[2].slaveDelay=5 +conf.memebers[2].priority=0 +rs.reconfig(conf) +``` + +MongoDB 事务中不允许以下操作: + +**在跨分片写入事务中创建新集合**。例如,如果写入一个分片中的现有集合,并在另一个分片中隐式创建集合,则 MongoDB 无法在同一事务中执行这两个操作。 + +当使用 [`“local”`](https://www.mongodb.com/zh-cn/docs/manual/reference/read-concern-local/#mongodb-readconcern-readconcern.-local-) 以外的读取关注级别时,[显式创建集合](https://www.mongodb.com/zh-cn/docs/manual/core/transactions-operations/#std-label-transactions-operations-ddl-explicit) - 例如 [`db.createCollection()`](https://www.mongodb.com/zh-cn/docs/manual/reference/method/db.createCollection/#mongodb-method-db.createCollection) 方法;例如 [`db.collection.createIndexes()`](https://www.mongodb.com/zh-cn/docs/manual/reference/method/db.collection.createIndexes/#mongodb-method-db.collection.createIndexes) 和 [`db.collection.createIndex()`](https://www.mongodb.com/zh-cn/docs/manual/reference/method/db.collection.createIndex/#mongodb-method-db.collection.createIndex) 方法。 + +[`listCollections`](https://www.mongodb.com/zh-cn/docs/manual/reference/command/listCollections/#mongodb-dbcommand-dbcmd.listCollections) 和 [`listIndexes`](https://www.mongodb.com/zh-cn/docs/manual/reference/command/listIndexes/#mongodb-dbcommand-dbcmd.listIndexes) 命令及其帮助程序方法。 + +其他非 CRUD 和非信息性操作,例如 [`createUser`](https://www.mongodb.com/zh-cn/docs/manual/reference/command/createUser/#mongodb-dbcommand-dbcmd.createUser)、[`getParameter`](https://www.mongodb.com/zh-cn/docs/manual/reference/command/getParameter/#mongodb-dbcommand-dbcmd.getParameter)、[`count`](https://www.mongodb.com/zh-cn/docs/manual/reference/command/count/#mongodb-dbcommand-dbcmd.count) 等方法。 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/09.MongoDB\345\210\206\347\211\207.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\210\206\347\211\207.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/09.MongoDB\345\210\206\347\211\207.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\210\206\347\211\207.md" index c6b3a8c4c5..02ce5b2c01 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/09.MongoDB\345\210\206\347\211\207.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\210\206\347\211\207.md" @@ -1,7 +1,7 @@ --- +icon: logos:mongodb title: MongoDB 分片 date: 2020-09-20 23:12:17 -order: 09 categories: - 数据库 - 文档数据库 @@ -11,7 +11,7 @@ tags: - 文档数据库 - MongoDB - 分片 -permalink: /pages/ad08f5/ +permalink: /pages/2b3824ce/ --- # MongoDB 分片 @@ -141,4 +141,4 @@ Hash 分片策略会先计算分片 Key 字段值的哈希值;然后,根据 - [MongoDB 官方免费教程](https://university.mongodb.com/) - **教程** - [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) - - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) \ No newline at end of file + - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/08.MongoDB\345\244\215\345\210\266.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\244\215\345\210\266.md" similarity index 74% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/08.MongoDB\345\244\215\345\210\266.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\244\215\345\210\266.md" index 8c5341e2e9..cf480d7630 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/08.MongoDB\345\244\215\345\210\266.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\244\215\345\210\266.md" @@ -1,7 +1,7 @@ --- +icon: logos:mongodb title: MongoDB 复制 date: 2020-09-20 23:12:17 -order: 08 categories: - 数据库 - 文档数据库 @@ -11,26 +11,28 @@ tags: - 文档数据库 - MongoDB - 复制 -permalink: /pages/505407/ +permalink: /pages/57308862/ --- # MongoDB 复制 ## 副本和可用性 -副本可以**提供冗余并提高数据可用性**。在不同数据库服务器上使用多个数据副本,可以提供一定程度的容错能力,以防止单个数据库服务器宕机时,数据丢失。 +MongoDB 中*的副本集*是一组维护相同数据集的 [`mongod`](https://www.mongodb.com/zh-cn/docs/manual/reference/program/mongod/#mongodb-binary-bin.mongod) 进程。复制提供冗余并提高[数据可用性](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-high-availability)。由于数据副本位于不同的数据库服务器上,复制提供了一定程度的容错能力,以防止单个数据库服务器丢失。 -在某些情况下,副本还可以**提供更大的读取吞吐量**。因为客户端可以将读取操作发送到不同的服务器。在不同数据中心中维护数据副本可以提高数据本地性和分布式应用程序的可用性。您还可以维护其他副本以用于专用目的:例如灾难恢复,报告或备份。 +在某些情况下,副本还可以**提供更大的读取吞吐量**。因为客户端可以将读取操作发送到不同的服务器。在不同数据中心中维护数据副本可以提高数据本地性和分布式应用程序的可用性。您还可以维护其他副本以用于专用目的:例如故障恢复,报告或备份。 ## MongoDB 副本 -MongoDB 中的副本集是一组维护相同数据集的 mongod 进程。一个副本集包含多个数据承载节点和一个仲裁器节点(可选)。在数据承载节点中,只有一个成员被视为主要节点,而其他节点则被视为次要节点。 +副本集是一组维护相同数据集的 [`mongod`](https://www.mongodb.com/zh-cn/docs/manual/reference/program/mongod/#mongodb-binary-bin.mongod) 实例。副本集包含多个数据承载节点和一个(可选)仲裁节点。在数据承载节点中,有且只有一个成员被视为主节点,而其他节点被视为辅助节点。 -**主节点负责接收所有写操作**。副本集只能有一个主副本,能够以 [`{ w: "majority" }`](https://docs.mongodb.com/manual/reference/write-concern/#writeconcern."majority") 来确认集群中节点的写操作成功情况;尽管在某些情况下,另一个 MongoDB 实例可能会暂时认为自己也是主要的。主节点在其操作日志(即 [oplog](https://docs.mongodb.com/manual/core/replica-set-oplog/))中记录了对其数据集的所有更改。 +每个副本集节点必须属于且只能属于一个副本集。 + +[主节点](https://www.mongodb.com/zh-cn/docs/manual/core/replica-set-primary/#std-label-replica-set-primary)接收所有写入操作。一个副本集只能有一个主节点,能够通过 [`{ w: “majority” }`](https://www.mongodb.com/zh-cn/docs/manual/reference/write-concern/#mongodb-writeconcern-writeconcern.-majority-) 确认写操作;尽管在某些情况下,另一个 mongod 实例可能会暂时认为自己也是主节点。主节点在其 oplog (即 [oplog](https://www.mongodb.com/zh-cn/docs/manual/core/replica-set-oplog/))中记录对其数据集的所有更改。有关主节点操作的更多信息,请参阅[副本集主节点。](https://www.mongodb.com/zh-cn/docs/manual/core/replica-set-primary/) ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200920165054.svg) -**从节点复制主节点的操作日志,并将操作应用于其数据集**,以便同步主节点的数据。如果主节点不可用,则符合条件的从节点将选举新的主节点。 +[从节点](https://www.mongodb.com/zh-cn/docs/manual/core/replica-set-secondary/#std-label-replica-set-secondary-members-ref)复制主节点的 oplog 并将操作应用于其数据集,以便辅助数据库的数据集反映主数据库的数据集。如果主节点不可用,符合条件的从节点将举行选举,以选举自己为新的主节点。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200920165055.svg) @@ -44,9 +46,9 @@ MongoDB 中的副本集是一组维护相同数据集的 mongod 进程。一个 ### 慢操作 -从节点复制主节点的操作日志,并将操作异步应用于其数据集。通过从节点同步主节点的数据集,即使一个或多个成员失败,副本集(MongoDB 集群)也可以继续运行。 +从节点复制主节点的 oplog ,并将操作异步应用于其数据集。通过从节点同步主节点的数据集,即使一个或多个成员失败,副本集(MongoDB 集群)也可以继续运行。 -从 4.2 版本开始,副本集的从节点记录慢操作(操作时间比设置的阈值长)的日志条目。这些慢操作在 [`REPL`](https://docs.mongodb.com/manual/reference/log-messages/#REPL) 组件下的 [诊断日志](https://docs.mongodb.com/manual/reference/program/mongod/#cmdoption-mongod-logpath) 中记录了日志消息,并使用了文本 `op: ` 花费了 `ms`。这些慢操作日志条目仅取决于慢操作阈值,而不取决于日志级别(在系统级别或组件级别),配置级别或运行缓慢的采样率。探查器不会捕获缓慢的操作日志条目。 +从 4.2 版本开始,副本集的从节点记录慢操作(操作时间比设置的阈值长)的日志条目。这些慢操作在 [`REPL`](https://docs.mongodb.com/manual/reference/log-messages/#REPL) 组件下的 [诊断日志](https://docs.mongodb.com/manual/reference/program/mongod/#cmdoption-mongod-logpath) 中记录了日志消息,并使用了文本 `op: ` 花费了 `ms`。这些慢 oplog 条目仅取决于慢操作阈值,而不取决于日志级别(在系统级别或组件级别),配置级别或运行缓慢的采样率。探查器不会捕获缓慢的 oplog 条目。 ### 复制延迟和流控 @@ -76,7 +78,7 @@ MongoDB 中的副本集是一组维护相同数据集的 mongod 进程。一个 ## 读操作 -### 读优先 +### 读取首选项 默认情况下,客户端从主节点读取数据;但是,客户端可以指定读取首选项,以将读取操作发送到从节点。 @@ -109,4 +111,4 @@ MongoDB 中的副本集是一组维护相同数据集的 mongod 进程。一个 - [MongoDB 官方免费教程](https://university.mongodb.com/) - **教程** - [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) - - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) \ No newline at end of file + - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/06.MongoDB\345\273\272\346\250\241\347\244\272\344\276\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\273\272\346\250\241.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/06.MongoDB\345\273\272\346\250\241\347\244\272\344\276\213.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\273\272\346\250\241.md" index ebc92a3bff..3e4bf824c8 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/06.MongoDB\345\273\272\346\250\241\347\244\272\344\276\213.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\345\273\272\346\250\241.md" @@ -1,7 +1,7 @@ --- +icon: logos:mongodb title: MongoDB 建模示例 date: 2020-09-12 10:43:53 -order: 06 categories: - 数据库 - 文档数据库 @@ -11,7 +11,7 @@ tags: - 文档数据库 - MongoDB - 建模 -permalink: /pages/88c7d3/ +permalink: /pages/20ae84ea/ --- # MongoDB 建模示例 @@ -583,4 +583,4 @@ MongoDB 文档格式非常灵活,势必会带来版本维护上的难度。 ## 参考资料 -- [Data Model Examples and Patterns](https://docs.mongodb.com/manual/applications/data-models/) \ No newline at end of file +- [Data Model Examples and Patterns](https://docs.mongodb.com/manual/applications/data-models/) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\347\256\200\344\273\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\347\256\200\344\273\213.md" new file mode 100644 index 0000000000..71fa54ab1e --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\347\256\200\344\273\213.md" @@ -0,0 +1,250 @@ +--- +icon: logos:mongodb +title: MongoDB 简介 +date: 2020-09-07 07:54:19 +categories: + - 数据库 + - 文档数据库 + - MongoDB +tags: + - 数据库 + - 文档数据库 + - MongoDB + - BSON +permalink: /pages/9eca06f6/ +--- + +# MongoDB 简介 + +## 什么是 MongoDB + +MongoDB 是一个分布式文档数据库,由 C++ 语言编写。 + +面向文档的数据库使用更灵活的“文档”模型取代了“行”的概念。通过嵌入文档和数组,面向文档的方式可以仅用一条记录来表示复杂的层次关系。 + +## 面向文档 + +MongoDB 中的记录是一个文档,它是由字段和值对组成的数据结构。MongoDB 文档类似于 JSON 对象。字段值可以包含其他文档、数组和文档数组。 + +![A MongoDB document.](https://www.mongodb.com/zh-cn/docs/manual/images/crud-annotated-document.bakedsvg.svg) + +MongoDB 中没有预定义模式(predefined schema):文档键值的类型和大小不是固定的。由于没有固定的模式,因此按需添加或删除字段变得更容易。 + +综上,**MongoDB 支持结构化、半结构化数据模型,可以动态响应结构变化**。 + +## 为什么使用 MongoDB + +### 主要功能 + +MongoDB 提供了丰富的功能: + +- [**读写操作 (CRUD)**](https://www.mongodb.com/zh-cn/docs/manual/crud/#std-label-crud) +- [**数据聚合**](https://www.mongodb.com/zh-cn/docs/manual/core/aggregation-pipeline/#std-label-aggregation-pipeline) +- [**文本搜索**](https://www.mongodb.com/zh-cn/docs/manual/text-search/#std-label-text-search) +- [**地理空间搜索**](https://www.mongodb.com/zh-cn/docs/manual/tutorial/geospatial-tutorial/) +- ... + +### 分布式 + +MongoDB 作为分布式存储,自然也具备了分布式的一般特性: + +- **高可用** - 通过**复制**机制实现**高可用**,提供**数据冗余**和**自动故障转移**能力。在 MongoDB 中,这种机制称为**[副本集](https://www.mongodb.com/zh-cn/docs/manual/replication/)**。**[副本集](https://www.mongodb.com/zh-cn/docs/manual/replication/)** 是一组 MongoDB 服务器,它们维护相同的数据集,并可提供冗余和提高数据可用性。 +- **高性能** - 通过**分片**机制提供**水平扩容**能力,以支撑海量数据,海量并发。从 3.4 开始,MongoDB 支持基于[**分片键**](https://www.mongodb.com/zh-cn/docs/manual/core/zone-sharding/#std-label-zone-sharding)创建数据的[**区域**](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-shard-key)。在均衡的集群中,MongoDB 仅将区域覆盖的读写定向到区域内的那些分片。 + +### 存储引擎 + +MongoDB 支持[多种存储引擎:](https://www.mongodb.com/zh-cn/docs/manual/core/storage-engines/) + +- [WiredTiger Storage Engine](https://www.mongodb.com/zh-cn/docs/manual/core/wiredtiger/)(包括对[静态加密](https://www.mongodb.com/zh-cn/docs/manual/core/security-encryption-at-rest/)的支持) +- [用于自我管理部署的内存存储引擎。](https://www.mongodb.com/zh-cn/docs/manual/core/inmemory/) + +此外,MongoDB 还提供可插拔的存储引擎 API,从而允许第三方基于 MongoDB 开发存储引擎。 + +## MongoDB 历史 + +- 1.x - 支持复制和分片 +- 2.x - 更丰富的数据库功能 +- 3.x - WiredTiger 和周边生态 +- 4.x - 支持分布式事务 + +## MongoDB 概念 + +MongoDB 将数据记录存储为 [BSON 文档](https://www.mongodb.com/zh-cn/docs/manual/core/document/#std-label-bson-document-format)。BSON 是 [JSON](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-JSON) 文档的二进制表示形式,尽管它包含的数据类型比 JSON 多。最大 BSON 文档大小为 16 MB。 + +每个MongoDB 文档都需要一个唯一的 [\_id](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-_id) 字段作为[主键](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-primary-key)。如果插入的文档省略了 `_id` 字段,则 MongoDB 驱动程序会自动为 `_id` 字段生成 [ObjectId](https://www.mongodb.com/zh-cn/docs/manual/reference/bson-types/#std-label-objectid)。 + +这些 [MongoDB 文档](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-document)收集在[集合](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-collection)中。[数据库](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-database)存储一个或多个文档集合。 + +为了方便理解MongoDB 概念,下面将MongoDB概念和 RDBM 概念进行对比: + +| RDBM 概念 | MongoDB 概念 | +| :----------------- | :--------------------------------------------------------------------------------- | +| database(数据库) | database(数据库) | +| table(表) | collection(集合) | +| row(行) | document(文档) | +| column(列) | field(字段) | +| index(索引) | index(索引) | +| primary key | [\_id](https://www.mongodb.com/zh-cn/docs/manual/reference/glossary/#std-term-_id) | + +### 数据库 + +一个 MongoDB 中可以建立多个数据库。 + +MongoDB 的默认数据库为"db",该数据库存储在 data 目录中。 + +MongoDB 的单个实例可以容纳多个独立的数据库,每一个都有自己的集合和权限,不同的数据库也放置在不同的文件中。 + +**"show dbs"** 命令可以显示所有数据的列表。 + +```shell +$ ./mongo +MongoDBshell version: 3.0.6 +connecting to: test +> show dbs +local 0.078GB +test 0.078GB +> +``` + +执行 **"db"** 命令可以显示当前数据库对象或集合。 + +```shell +$ ./mongo +MongoDBshell version: 3.0.6 +connecting to: test +> db +test +> +``` + +运行"use"命令,可以连接到一个指定的数据库。 + +```shell +> use local +switched to db local +> db +local +> +``` + +数据库按照名称进行标识的。数据库名称可以是任意 UTF-8 字符串,但有以下限制: + +- 数据库名称不能是空字符串("")。 +- 数据库名称不能包含 `/`、`\`、`.`、`"`、`*`、`<`、`>`、`:`、`|`、`?`、`$`、单一的空格以及 `\0`(空字符),基本上只能使用 ASCII 字母和数字。 +- 数据库名称区分大小写。 +- 数据库名称的长度限制为 64 字节。 + +有一些数据库名是保留的,可以直接访问这些有特殊作用的数据库。 + +- **admin**:admin 数据库会在身份验证和授权时被使用。此外,某些管理操作需要访问此数据库。 +- **local**:这个数据永远不会被复制,可以用来存储限于本地单台服务器的任意集合 +- **config**:当 Mongo 用于分片设置时,config 数据库在内部使用,用于保存分片的相关信息。 + +### 文档 + +文档是 MongoDB 中的基本数据单元。 + +文档是一组有序键值对(即 BSON)。MongoDB 的文档不需要设置相同的字段,并且相同的字段不需要相同的数据类型,这与关系型数据库有很大的区别,也是 MongoDB 非常突出的特点。 + +需要注意的是: + +- 文档中的键/值对是有序的。 + +- 文档的键是字符串。除了少数例外情况,键可以使用任意 UTF-8 字符。 + +- 文档中的值不仅可以是在双引号里面的字符串,还可以是其他几种数据类型(甚至可以是整个嵌入的文档)。 + +- MongoDB 区分类型和大小写。例如,下面这两对文档是不同的: + + ```json + {"count" : 5} + {"count" : "5"} + + {"count" : 5} + {"Count" : 5} + ``` + +- MongoDB 的文档不能有重复的键。例如,下面这个文档是不合法的 + + ```json + {"greeting" : "Hello, world!", "greeting" : "Hello, MongoDB!" + ``` + +文档键命名规范: + +- 键不能含有 `\0` (空字符)。这个字符用来表示键的结尾。 +- `.` 和 `$` 有特别的意义,只有在特定环境下才能使用。 +- 以下划线 `_` 开头的键是保留的(不是严格要求的)。 + +### 集合 + +集合就是 MongoDB 文档组,类似于 RDBMS (关系数据库管理系统:Relational Database Management System)中的表格。 + +集合存在于数据库中,集合没有固定的结构,这意味着你在对集合可以插入不同格式和类型的数据,但通常情况下我们插入集合的数据都会有一定的关联性。 + +使用 `.` 字符分隔不同命名空间的子集合是一种组织集合的惯例。例如,有一个具有博客功能的应用程序,可能包含名为 `blog.posts` 和名为 `blog.authors` 的集合。 + +合法的集合名: + +- 集合名称不能是空字符串("")。 +- 集合名称不能含有 `\0`(空字符),因为这个字符用于表示一个集合名称的结束。 +- 集合名称不能以 `system.` 开头,该前缀是为内部集合保留的。例如,`system.users` 集合中保存着数据库的用户,`system.namespaces` 集合中保存着有关数据库所有集合的信息。 +- 用户创建的集合名称中不应包含保留字符 `$`。许多驱动程序确实支持在集合名称中使用 `$`,这是因为某些由系统生成的集合会包含它,但除非你要访问的是这些集合之一,否则不应在名称中使用 `$` 字符。 + +### 元数据 + +数据库的信息是存储在集合中。它们使用了系统的命名空间:`dbname.system.*` + +在 MongoDB 数据库中名字空间 `.system.*` 是包含多种系统信息的特殊集合(Collection),如下: + +| 集合命名空间 | 描述 | +| :----------------------- | :---------------------------------------- | +| dbname.system.namespaces | 列出所有名字空间。 | +| dbname.system.indexes | 列出所有索引。 | +| dbname.system.profile | 包含数据库概要(profile)信息。 | +| dbname.system.users | 列出所有可访问数据库的用户。 | +| dbname.local.sources | 包含复制对端(slave)的服务器信息和状态。 | + +对于修改系统集合中的对象有如下限制。 + +在 `system.indexes` 插入数据,可以创建索引。但除此之外该表信息是不可变的(特殊的 drop index 命令将自动更新相关信息)。`system.users` 是可修改的。`system.profile` 是可删除的。 + +## BSON 数据类型 + +MongoDB 文档由键值对组成。字段名称是字符串;字段的值可以是任何 [BSON 数据类型](https://www.mongodb.com/zh-cn/docs/manual/reference/bson-types/#std-label-bson-types),包括其他文档、数组和文档数组。 + +| 数据类型 | 描述 | +| :----------------- | :--------------------------------------------------------------------------------------------------------- | +| String | 字符串。存储数据常用的数据类型。在 MongoDB 中,UTF-8 编码的字符串才是合法的。 | +| Integer | 整型数值。用于存储数值。根据你所采用的服务器,可分为 32 位或 64 位。 | +| Boolean | 布尔值。用于存储布尔值(真/假)。 | +| Double | 双精度浮点值。用于存储浮点值。 | +| Min/Max keys | 将一个值与 BSON(二进制的 JSON)元素的最低值和最高值相对比。 | +| Array | 用于将数组或列表或多个值存储为一个键。 | +| Timestamp | 时间戳。记录文档修改或添加的具体时间。 | +| Object | 用于内嵌文档。 | +| Null | 用于创建空值。 | +| Symbol | 符号。该数据类型基本上等同于字符串类型,但不同的是,它一般用于采用特殊符号类型的语言。 | +| Date | 日期时间。用 UNIX 时间格式来存储当前日期或时间。你可以指定自己的日期时间:创建 Date 对象,传入年月日信息。 | +| Object ID | 对象 ID。用于创建文档的 ID。 | +| Binary Data | 二进制数据。用于存储二进制数据。 | +| Code | 代码类型。用于在文档中存储 JavaScript 代码。 | +| Regular expression | 正则表达式类型。用于存储正则表达式。 | + +## MongoDB vs.RDBM + +| 特性 | MongoDB | RDBMS | +| --------- | ------------------------------------------------ | -------- | +| 数据模型 | 文档模型 | 关系型 | +| CRUD 操作 | MQL/SQL | SQL | +| 高可用 | 复制集 | 集群模式 | +| 扩展性 | 支持分片 | 数据分区 | +| 扩繁方式 | 垂直扩展+水平扩展 | 垂直扩展 | +| 索引类型 | B 树、全文索引、地理位置索引、多键索引、TTL 索引 | B 树 | +| 数据容量 | 没有理论上限 | 千万、亿 | + +## 参考资料 + +- [MongoDB 官网](https://www.mongodb.com/) +- [MongoDB Github](https://github.com/mongodb/mongo) +- [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/07.MongoDB\347\264\242\345\274\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\347\264\242\345\274\225.md" similarity index 98% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/07.MongoDB\347\264\242\345\274\225.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\347\264\242\345\274\225.md" index 32dee6ab13..1e1b974447 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/07.MongoDB\347\264\242\345\274\225.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\347\264\242\345\274\225.md" @@ -1,7 +1,7 @@ --- +icon: logos:mongodb title: MongoDB 索引 date: 2020-09-21 21:22:57 -order: 07 categories: - 数据库 - 文档数据库 @@ -11,7 +11,7 @@ tags: - 文档数据库 - MongoDB - 索引 -permalink: /pages/10c674/ +permalink: /pages/567ecac8/ --- # MongoDB 索引 @@ -69,4 +69,4 @@ db.collection.createIndex( { name: -1 } ) - [MongoDB 官方免费教程](https://university.mongodb.com/) - **教程** - [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) - - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) \ No newline at end of file + - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\350\201\232\345\220\210.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\350\201\232\345\220\210.md" new file mode 100644 index 0000000000..ddf805690d --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\350\201\232\345\220\210.md" @@ -0,0 +1,601 @@ +--- +icon: logos:mongodb +title: MongoDB 的聚合操作 +date: 2020-09-21 21:22:57 +categories: + - 数据库 + - 文档数据库 + - MongoDB +tags: + - 数据库 + - 文档数据库 + - MongoDB + - 聚合 +permalink: /pages/02aefb0c/ +--- + +# MongoDB 的聚合操作 + +聚合操作处理多个文档并返回计算结果。可以使用聚合操作来: + +- 将多个文档中的值组合在一起。 +- 对分组数据执行操作,返回单一结果。 +- 分析一段时间内的数据变化。 + +若要执行聚合操作,可以使用: + +- [聚合管道](https://www.mongodb.com/zh-cn/docs/manual/aggregation/#std-label-aggregation-pipeline-intro),这是执行聚合的首选方法。 +- [单一目的聚合方法](https://www.mongodb.com/zh-cn/docs/manual/aggregation/#std-label-single-purpose-agg-methods),这些方法很简单,但缺乏聚合管道的功能。 + +## Pipeline 简介 + +MongoDB 的聚合框架以数据处理管道(Pipeline)的概念为模型。**MongoDB 通过 [`db.collection.aggregate()`](https://docs.mongodb.com/manual/reference/method/db.collection.aggregate/#db.collection.aggregate) 方法支持聚合操作 **。并提供了 [`aggregate`](https://docs.mongodb.com/manual/reference/command/aggregate/#dbcmd.aggregate) 命令来执行 pipeline。 + +聚合管道由一个或多个处理文档的 [阶段](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation-pipeline/#std-label-aggregation-pipeline-operator-reference) 组成: + +- 每个阶段对输入文档执行一个操作。例如,某个阶段可以过滤文档、对文档进行分组并计算值。 +- 从一个阶段输出的文档将传递到下一阶段。 +- 一个聚合管道可以返回针对文档组的结果。例如,返回总值、平均值、最大值和最小值。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921092725.png) + +[阶段](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation-pipeline/#std-label-aggregation-pipeline-operator-reference) 的要点: + +- 阶段不必为每个输入文档输出一个文档。例如,某些阶段可能会产生新文档或过滤掉现有文档。 +- 同一个阶段可以在管道中多次出现,但以下阶段例外:[`$out`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/out/#mongodb-pipeline-pipe.-out)、[`$merge`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/merge/#mongodb-pipeline-pipe.-merge) 和 [`$geoNear`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/geoNear/#mongodb-pipeline-pipe.-geoNear)。 +- 要在阶段中计算平均值和执行其他计算,请使用指定 [聚合操作符](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/#std-label-aggregation-expressions) 的 [聚合表达式](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/#std-label-aggregation-expression-operators)。在下一节中,您将了解有关聚合表达式的更多信息。 + +## Pipeline 示例 + +初始数据: + +```javascript +db.orders.insertMany( [ + { _id: 0, name: "Pepperoni", size: "small", price: 19, + quantity: 10, date: ISODate( "2021-03-13T08:14:30Z" ) }, + { _id: 1, name: "Pepperoni", size: "medium", price: 20, + quantity: 20, date : ISODate( "2021-03-13T09:13:24Z" ) }, + { _id: 2, name: "Pepperoni", size: "large", price: 21, + quantity: 30, date : ISODate( "2021-03-17T09:22:12Z" ) }, + { _id: 3, name: "Cheese", size: "small", price: 12, + quantity: 15, date : ISODate( "2021-03-13T11:21:39.736Z" ) }, + { _id: 4, name: "Cheese", size: "medium", price: 13, + quantity:50, date : ISODate( "2022-01-12T21:23:13.331Z" ) }, + { _id: 5, name: "Cheese", size: "large", price: 14, + quantity: 10, date : ISODate( "2022-01-12T05:08:13Z" ) }, + { _id: 6, name: "Vegan", size: "small", price: 17, + quantity: 10, date : ISODate( "2021-01-13T05:08:13Z" ) }, + { _id: 7, name: "Vegan", size: "medium", price: 18, + quantity: 10, date : ISODate( "2021-01-13T05:10:13Z" ) } +] ) +``` + +【示例】计算总订单数量 + +以下聚合管道示例包含两个 [阶段](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation-pipeline/#std-label-aggregation-pipeline-operator-reference),并返回按披萨名称分组后,各款中号披萨的总订单数量: + +```javascript +db.orders.aggregate( [ + // Stage 1: 根据 size 过滤订单 + { + $match: { size: "medium" } + }, + // Stage 2: 按名称对剩余文档进行分组,并计算总数量 + { + $group: { _id: "$name", totalQuantity: { $sum: "$quantity" } } + } +] ) + +// 输出 +[ + { _id: 'Cheese', totalQuantity: 50 }, + { _id: 'Vegan', totalQuantity: 10 }, + { _id: 'Pepperoni', totalQuantity: 20 } +] +``` + +[`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段: + +- 从披萨订单文档过滤出 `size` 为 `medium` 的披萨。 +- 将剩余文档传递到 [`$group`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/group/#mongodb-pipeline-pipe.-group) 阶段。 + +[`$group`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/group/#mongodb-pipeline-pipe.-group) 阶段: + +- 按披萨 `name` 对剩余文档进行分组。 +- 使用 [`$sum`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sum/#mongodb-group-grp.-sum) 计算每种披萨 `name` 的总订单 `quantity`。总数存储在聚合管道返回的 `totalQuantity` 字段中。 + +【示例】计算订单总值和平均订单数 + +```javascript +db.orders.aggregate( [ + + // Stage 1: 根据 date 过滤订单 + { + $match: + { + "date": { $gte: new ISODate( "2020-01-30" ), $lt: new ISODate( "2022-01-30" ) } + } + }, + + // Stage 2: 按 date 对剩余文档进行分组,并计算 + { + $group: + { + _id: { $dateToString: { format: "%Y-%m-%d", date: "$date" } }, + totalOrderValue: { $sum: { $multiply: [ "$price", "$quantity" ] } }, + averageOrderQuantity: { $avg: "$quantity" } + } + }, + + // Stage 3: 根据 totalOrderValue 倒序排序 + { + $sort: {totalOrderValue: -1} + } + + ] ) + +// 输出 +[ + { _id: '2022-01-12', totalOrderValue: 790, averageOrderQuantity: 30 }, + { _id: '2021-03-13', totalOrderValue: 770, averageOrderQuantity: 15 }, + { _id: '2021-03-17', totalOrderValue: 630, averageOrderQuantity: 30 }, + { _id: '2021-01-13', totalOrderValue: 350, averageOrderQuantity: 10 } +] +``` + +[`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段: + +- 使用 [`$gte`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/gte/#mongodb-expression-exp.-gte) 和 [`$lt`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lt/#mongodb-expression-exp.-lt)[``](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lt/#mongodb-expression-exp.-lt) 将披萨订单文档筛选为指定日期范围内的文档。 +- 将剩余文档传递到 [`$group`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/group/#mongodb-pipeline-pipe.-group) 阶段。 + +[`$group`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/group/#mongodb-pipeline-pipe.-group) 阶段: + +- 使用 [`$dateToString`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/dateToString/#mongodb-expression-exp.-dateToString)[``](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/dateToString/#mongodb-expression-exp.-dateToString) 按日期对文档进行分组。 +- 对于每个群组,计算: + - 使用 [`$sum`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sum/#mongodb-group-grp.-sum) 和 [`$multiply`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/multiply/#mongodb-expression-exp.-multiply)[``](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/multiply/#mongodb-expression-exp.-multiply) 的总订单值。 + - 使用 [`$avg`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/avg/#mongodb-group-grp.-avg)[``](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/avg/#mongodb-group-grp.-avg) 计算平均订单数量。 +- 将分组的文档传递到 [`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort) 阶段。 + +[`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort) 阶段: + +- 按每组的总订单值以降序对文档进行排序 (`-1`)。 +- 返回排序文档。 + +> 更多示例: +> +> - [使用用户偏好数据进行聚合](https://www.mongodb.com/zh-cn/docs/manual/tutorial/aggregation-with-user-preference-data/) +> - [与邮政编码数据集的聚合](https://www.mongodb.com/zh-cn/docs/manual/tutorial/aggregation-zip-code-data-set/) +> - [使用聚合管道进行更新](https://www.mongodb.com/zh-cn/docs/manual/tutorial/update-documents-with-aggregation-pipeline/) + +## Pipeline 优化 + +### 投影优化 + +聚合管道可确定是否只需文档中的部分字段即可获取结果。如果是,管道则仅会使用这些字段,从而减少通过管道传递的数据量。 + +使用 [`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 阶段时,通常应该是管道的最后一个阶段,用于指定要返回给客户端的字段。 + +在管道的开头或中间使用 `$project` 阶段来减少传递到后续管道阶段的字段数量不太可能提高性能,因为数据库会自动执行此优化。 + +### 管道序列优化 + +(`$project`、`$unset`、`$addFields`、`$set`) + `$match` 序列优化 + +如果聚合管道包含投影阶段 ([`$addFields`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/addFields/#mongodb-pipeline-pipe.-addFields)、[`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project)、[`$set`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/set/#mongodb-pipeline-pipe.-set) 或 [`$unset`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unset/#mongodb-pipeline-pipe.-unset)),且其后跟随 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段,MongoDB 会将 `$match` 阶段中无需使用投影阶段计算的值的所有过滤器移动到投影前的新的 `$match` 阶段。 + +如果聚合管道包含多个投影或 `$match` 阶段,MongoDB 会对每个 `$match` 阶段执行此优化,将每个 `$match` 过滤器移到过滤器不依赖的所有投影阶段之前。 + +【示例】管道序列优化示例 + +优化前: + +```javascript +{ + $addFields: { + maxTime: { $max: "$times" }, + minTime: { $min: "$times" } + } +}, +{ + $project: { + _id: 1, + name: 1, + times: 1, + maxTime: 1, + minTime: 1, + avgTime: { $avg: ["$maxTime", "$minTime"] } + } +}, +{ + $match: { + name: "Joe Schmoe", + maxTime: { $lt: 20 }, + minTime: { $gt: 5 }, + avgTime: { $gt: 7 } + } +} +``` + +优化器会将 `$match` 阶段分解为四个单独的过滤器,每个过滤器对应 `$match` 查询文档中的一个键。然后,优化器会将每个过滤器移至尽可能多的投影阶段之前,从而按需创建新的 `$match` 阶段。 + +优化后: + +```javascript +{ $match: { name: "Joe Schmoe" } }, +{ $addFields: { + maxTime: { $max: "$times" }, + minTime: { $min: "$times" } +} }, +{$match: { maxTime: { $lt: 20}, minTime: {$gt: 5} } }, +{ $project: { + _id: 1, name: 1, times: 1, maxTime: 1, minTime: 1, + avgTime: { $avg: ["$maxTime", "$minTime"] } +} }, +{$match: { avgTime: { $gt: 7} } } +``` + +[`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 筛选器 `{ avgTime: { $gt: 7 } }` 依赖 [`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 阶段来计算 `avgTime` 字段。[`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 阶段是该管道中的最后一个投影阶段,因此 `avgTime` 上的 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 筛选器无法移动。 + +`maxTime` 和 `minTime` 字段在 [`$addFields`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/addFields/#mongodb-pipeline-pipe.-addFields) 阶段计算,但不依赖 [`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 阶段。优化器已为这些字段上的筛选器创建一个新的 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段,并将其置于 [`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 阶段之前。 + +[`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 筛选器 `{ name: "Joe Schmoe" }` 不使用在 [`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 或 [`$addFields`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/addFields/#mongodb-pipeline-pipe.-addFields) 阶段计算的任何值,因此它在这两个投影阶段之前移到了新的 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段。 + +优化后,筛选器 `{ name: "Joe Schmoe" }` 在管道开始时会处于 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段。此举还允许聚合在最初查询该集合时使用针对 `name` 字段的索引。 + +#### `$sort` + `$match` 序列优化 + +当序列中的 [`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort) 后面是 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 时,[`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 会在 [`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort) 之前移动,以最大限度地减少要排序的对象数量。例如,如果管道由以下阶段组成: + +``` +{ $sort: { age : -1 } }, +{ $match: { status: 'A' } } +``` + +在优化阶段,优化器会将序列转换为以下内容: + +``` +{ $match: { status: 'A' } }, +{ $sort: { age : -1 } } +``` + +#### `$redact` + `$match` 序列优化 + +如果可能,当管道有 [`$redact`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/redact/#mongodb-pipeline-pipe.-redact) 阶段紧接着 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段时,聚合有时可以在 [`$redact`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段之前添加 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/redact/#mongodb-pipeline-pipe.-redact) 阶段的一部分。如果添加的 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段位于管道的开头,则聚合可以使用索引并查询集合以限制进入管道的文档数量。有关更多信息,请参阅 [使用索引和文档过滤器提高性能](https://www.mongodb.com/zh-cn/docs/manual/core/aggregation-pipeline-optimization/#std-label-aggregation-pipeline-optimization-indexes-and-filters)。 + +例如,如果管道由以下阶段组成: + +``` +{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } }, +{ $match: { year: 2014, category: { $ne: "Z" } } } +``` + +优化器可以在 [`$redact`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段之前添加相同的 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/redact/#mongodb-pipeline-pipe.-redact) 阶段: + +``` +{ $match: { year: 2014 } }, +{ $redact: { $cond: { if: { $eq: [ "$level", 5 ] }, then: "$$PRUNE", else: "$$DESCEND" } } }, +{ $match: { year: 2014, category: { $ne: "Z" } } } +``` + +#### `$project`/`$unset` + `$skip` 序列优化 + +如果序列中的 [`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 或 [`$unset`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unset/#mongodb-pipeline-pipe.-unset) 后面是 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip),则 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip) 在 [`$project`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/project/#mongodb-pipeline-pipe.-project) 之前移动。例如,如果管道由以下阶段组成: + +``` +{ $sort: { age : -1 } }, +{ $project: { status: 1, name: 1 } }, +{ $skip: 5 } +``` + +在优化阶段,优化器会将序列转换为以下内容: + +``` +{ $sort: { age : -1 } }, +{ $skip: 5 }, +{ $project: { status: 1, name: 1 } } +``` + +### 管道合并优化 + +如果可能,优化阶段会将 Pipeline 阶段合并到其前身。通常,合并发生在任意序列重新排序优化之后。 + +#### `$sort` + `$limit` 合并 + +当 [`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort) 在 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 之前时,, the optimizer can coalesce the into the 如果没有干预阶段(例如 [`$unwind`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit)、[`$group`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort))修改文档的数量,则优化器可以将 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unwind/#mongodb-pipeline-pipe.-unwind) 阶段合并到 [`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/group/#mongodb-pipeline-pipe.-group)。如果有管道阶段更改了 [`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 和 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort) 阶段之间的文档数量,则 MongoDB 不会将 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#mongodb-pipeline-pipe.-sort) 合并到 [`$sort`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 中。 + +例如,如果管道由以下阶段组成: + +``` +{ $sort : { age : -1 } }, +{ $project : { age : 1, status : 1, name : 1 } }, +{ $limit: 5 } +``` + +在优化阶段,优化器会将此序列合并为以下内容: + +``` +{ + "$sort" : { + "sortKey" : { + "age" : -1 + }, + "limit" : NumberLong(5) + } +}, +{ "$project" : { + "age" : 1, + "status" : 1, + "name" : 1 + } +} +``` + +此操作可让排序操作在推进时仅维护前 `n` 个结果,其中 `n` 为指定的限制,而 MongoDB 仅需要在内存中存储 `n` 个项目 [[1\]](https://www.mongodb.com/zh-cn/docs/manual/core/aggregation-pipeline-optimization/#footnote-coalescence-allowDiskUse)。有关更多信息,请参阅 [`$sort` 操作符和内存](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/sort/#std-label-sort-and-memory)。 + +#### `$limit` + `$limit` 合并 + +当 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 紧随另一个 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 时,这两个阶段可以合并为一个 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit),以两个初始限额中*较小*的为合并后的限额。例如,一个管道包含以下序列: + +``` +{ $limit: 100 }, +{ $limit: 10 } +``` + +然后第二个 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 阶段可以合并到第一个 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 阶段,形成一个 [`$limit`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/limit/#mongodb-pipeline-pipe.-limit) 阶段,新阶段的限额 `10` 是两个初始限额 `100` 和 `10` 中的较小者。 + +``` +{ $limit: 10 } +``` + +#### `$skip` + `$skip` 合并 + +当 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip) 紧随在另一个 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip) 之后时,这两个阶段可以合并为一个 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip),其中的跳过数量是两个初始跳过数量的*总和*。例如,一个管道包含以下序列: + +``` +{ $skip: 5 }, +{ $skip: 2 } +``` + +然后第二个 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip) 阶段可以合并到第一个 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip) 阶段,形成一个 [`$skip`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/skip/#mongodb-pipeline-pipe.-skip) 阶段,新阶段的跳过数量 `7` 是两个初始限额 `5` 和 `2` 的总和。 + +``` +{ $skip: 7 } +``` + +#### `$match` + `$match` 合并 + +当 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 紧随另一个 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 之后时,这两个阶段可以合并为一个 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match),用 [`$and`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/and/#mongodb-expression-exp.-and) 将条件组合在一起。例如,一个管道包含以下序列: + +``` +{ $match: { year: 2014 } }, +{ $match: { status: "A" } } +``` + +然后第二个 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段可合并到第一个 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段并形成一个 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段 + +``` +{ $match: { $and: [ { "year" : 2014 }, { "status" : "A" } ] } } +``` + +#### `$lookup`、`$unwind` 和 `$match` Coalescence + +当 [`$unwind`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unwind/#mongodb-pipeline-pipe.-unwind) 紧随 [`$lookup`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lookup/#mongodb-pipeline-pipe.-lookup),且 [`$unwind`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unwind/#mongodb-pipeline-pipe.-unwind) 在 [`$lookup`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lookup/#mongodb-pipeline-pipe.-lookup)`` 的 `as`[``](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lookup/#mongodb-pipeline-pipe.-lookup) 字段上运行时,优化器将 [`$unwind`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unwind/#mongodb-pipeline-pipe.-unwind) 合并到 [`$lookup`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lookup/#mongodb-pipeline-pipe.-lookup) 阶段。这样可以避免创建大型中间文档。此外,如果 [`$unwind`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unwind/#mongodb-pipeline-pipe.-unwind) 后接 [`$lookup`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 的任意 `as` 子字段上的 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lookup/#mongodb-pipeline-pipe.-lookup),则优化器也会合并 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match)[``](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match)。 + +例如,一个管道包含以下序列: + +``` +{ + $lookup: { + from: "otherCollection", + as: "resultingArray", + localField: "x", + foreignField: "y" + } +}, +{ $unwind: "$resultingArray" }, +{ $match: { + "resultingArray.foo": "bar" + } +} +``` + +优化器将 [`$unwind`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/unwind/#mongodb-pipeline-pipe.-unwind) 和 [`$match`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/match/#mongodb-pipeline-pipe.-match) 阶段合并到 [`$lookup`](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation/lookup/#mongodb-pipeline-pipe.-lookup) 阶段。如果使用 `explain` 选项运行聚合,`explain` 输出将显示合并阶段: + +``` +{ + $lookup: { + from: "otherCollection", + as: "resultingArray", + localField: "x", + foreignField: "y", + let: {}, + pipeline: [ + { + $match: { + "foo": { + "$eq": "bar" + } + } + } + ], + unwinding: { + "preserveNullAndEmptyArrays": false + } + } +} +``` + +您可以在 [解释计划](https://www.mongodb.com/zh-cn/docs/manual/reference/method/db.collection.aggregate/#std-label-example-aggregate-method-explain-option) 中查看此优化后的管道。 + +## Pipeline 限制 + +- **大小限制** - [`aggregate`](https://www.mongodb.com/zh-cn/docs/manual/reference/command/aggregate/#mongodb-dbcommand-dbcmd.aggregate) 命令既可以返回一个游标,也可以将结果存储在集合中。结果集中的每个文档存在 16 MB 的 [BSON 文档大小限制](https://www.mongodb.com/zh-cn/docs/manual/reference/limits/#mongodb-limit-BSON-Document-Size)。如果任何单个文档超过 [BSON 文档大小限制](https://www.mongodb.com/zh-cn/docs/manual/reference/limits/#mongodb-limit-BSON-Document-Size),则聚合会产生错误。该限制仅适用于返回的文档。在管道处理过程中,文档可能会超过此大小。[`db.collection.aggregate()`](https://www.mongodb.com/zh-cn/docs/manual/reference/method/db.collection.aggregate/#mongodb-method-db.collection.aggregate) 方法默认返回一个游标。 +- **阶段数量限制** - *在 5.0 版中进行了更改*:MongoDB 5.0 将单个管道中允许的 [聚合管道阶段](https://www.mongodb.com/zh-cn/docs/manual/reference/operator/aggregation-pipeline/#std-label-aggregation-pipeline-operator-reference) 限制为 1000 个。 +- **内存限制** - 从 MongoDB 6.0 开始,[`allowDiskUseByDefault`](https://www.mongodb.com/zh-cn/docs/manual/reference/parameters/#mongodb-parameter-param.allowDiskUseByDefault) 参数可控制需要 100 MB 以上内存容量来执行的管道阶段是否默认会将临时文件写入磁盘。 +- 如果将 [`allowDiskUseByDefault`](https://www.mongodb.com/zh-cn/docs/manual/reference/parameters/#mongodb-parameter-param.allowDiskUseByDefault) 设为 `true`,则默认情况下,需要 100 MB 以上内存容量的管道阶段会将临时文件写入磁盘。您可以使用 `{ allowDiskUse: false }` 选项来为特定的 `find` 或 `aggregate` 命令禁用将临时文件写入磁盘的功能。 +- 如果 [`allowDiskUseByDefault`](https://www.mongodb.com/zh-cn/docs/manual/reference/parameters/#mongodb-parameter-param.allowDiskUseByDefault) 设置为 `false`,则默认情况下,需要超过 100 MB 的内存才能执行的管道阶段会引发错误。您可以使用 `{ allowDiskUse: true }` 选项来为特定 `find` 或 `aggregate` 启用向磁盘写入临时文件的功能。 + +## Map-Reduce + +> 聚合 pipeline 比 map-reduce 提供更好的性能和更一致的接口。 + +Map-reduce 是一种数据处理范式,用于将大量数据汇总为有用的聚合结果。为了执行 map-reduce 操作,MongoDB 提供了 [`mapReduce`](https://docs.mongodb.com/manual/reference/command/mapReduce/#dbcmd.mapReduce) 数据库命令。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921155546.svg) + +在上面的操作中,MongoDB 将 map 阶段应用于每个输入 document(即 collection 中与查询条件匹配的 document)。 map 函数分发出多个键 - 值对。对于具有多个值的那些键,MongoDB 应用 reduce 阶段,该阶段收集并汇总聚合的数据。然后,MongoDB 将结果存储在 collection 中。可选地,reduce 函数的输出可以通过 finalize 函数来进一步汇总聚合结果。 + +MongoDB 中的所有 map-reduce 函数都是 JavaScript,并在 mongod 进程中运行。 Map-reduce 操作将单个 collection 的 document 作为输入,并且可以在开始 map 阶段之前执行任意排序和限制。 mapReduce 可以将 map-reduce 操作的结果作为 document 返回,也可以将结果写入 collection。 + +## 单一目的聚合方法 + +MongoDB 支持一下单一目的的聚合操作: + +- [`db.collection.estimatedDocumentCount()`](https://docs.mongodb.com/manual/reference/method/db.collection.estimatedDocumentCount/#db.collection.estimatedDocumentCount) +- [`db.collection.count()`](https://docs.mongodb.com/manual/reference/method/db.collection.count/#db.collection.count) +- [`db.collection.distinct()`](https://docs.mongodb.com/manual/reference/method/db.collection.distinct/#db.collection.distinct) + +所有这些操作都汇总了单个 collection 中的 document。尽管这些操作提供了对常见聚合过程的简单访问,但是它们相比聚合 pipeline 和 map-reduce,缺少灵活性和丰富的功能性。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921155935.svg) + +## RDBM 聚合 vs. MongoDB 聚合 + +MongoDB pipeline 提供了许多等价于 SQL 中常见聚合语句的操作。 + +下表概述了常见的 SQL 聚合语句或函数和 MongoDB 聚合操作的映射表: + +| RDBM 操作 | MongoDB 聚合操作 | +| :---------------------- | :----------------------------------------------------------- | +| `WHERE` | [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) | +| `GROUP BY` | [`$group`](https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group) | +| `HAVING` | [`$match`](https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match) | +| `SELECT` | [`$project`](https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project) | +| `ORDER BY` | [`$sort`](https://docs.mongodb.com/manual/reference/operator/aggregation/sort/#pipe._S_sort) | +| `LIMIT` | [`$limit`](https://docs.mongodb.com/manual/reference/operator/aggregation/limit/#pipe._S_limit) | +| `SUM()` | [`$sum`](https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum) | +| `COUNT()` | [`$sum`](https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum)[`$sortByCount`](https://docs.mongodb.com/manual/reference/operator/aggregation/sortByCount/#pipe._S_sortByCount) | +| `JOIN` | [`$lookup`](https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup) | +| `SELECT INTO NEW_TABLE` | [`$out`](https://docs.mongodb.com/manual/reference/operator/aggregation/out/#pipe._S_out) | +| `MERGE INTO TABLE` | [`$merge`](https://docs.mongodb.com/manual/reference/operator/aggregation/merge/#pipe._S_merge) (Available starting in MongoDB 4.2) | +| `UNION ALL` | [`$unionWith`](https://docs.mongodb.com/manual/reference/operator/aggregation/unionWith/#pipe._S_unionWith) (Available starting in MongoDB 4.4) | + +【示例】 + +```javascript +db.orders.insertMany([ + { + _id: 1, + cust_id: 'Ant O. Knee', + ord_date: new Date('2020-03-01'), + price: 25, + items: [ + { sku: 'oranges', qty: 5, price: 2.5 }, + { sku: 'apples', qty: 5, price: 2.5 } + ], + status: 'A' + }, + { + _id: 2, + cust_id: 'Ant O. Knee', + ord_date: new Date('2020-03-08'), + price: 70, + items: [ + { sku: 'oranges', qty: 8, price: 2.5 }, + { sku: 'chocolates', qty: 5, price: 10 } + ], + status: 'A' + }, + { + _id: 3, + cust_id: 'Busby Bee', + ord_date: new Date('2020-03-08'), + price: 50, + items: [ + { sku: 'oranges', qty: 10, price: 2.5 }, + { sku: 'pears', qty: 10, price: 2.5 } + ], + status: 'A' + }, + { + _id: 4, + cust_id: 'Busby Bee', + ord_date: new Date('2020-03-18'), + price: 25, + items: [{ sku: 'oranges', qty: 10, price: 2.5 }], + status: 'A' + }, + { + _id: 5, + cust_id: 'Busby Bee', + ord_date: new Date('2020-03-19'), + price: 50, + items: [{ sku: 'chocolates', qty: 5, price: 10 }], + status: 'A' + }, + { + _id: 6, + cust_id: 'Cam Elot', + ord_date: new Date('2020-03-19'), + price: 35, + items: [ + { sku: 'carrots', qty: 10, price: 1.0 }, + { sku: 'apples', qty: 10, price: 2.5 } + ], + status: 'A' + }, + { + _id: 7, + cust_id: 'Cam Elot', + ord_date: new Date('2020-03-20'), + price: 25, + items: [{ sku: 'oranges', qty: 10, price: 2.5 }], + status: 'A' + }, + { + _id: 8, + cust_id: 'Don Quis', + ord_date: new Date('2020-03-20'), + price: 75, + items: [ + { sku: 'chocolates', qty: 5, price: 10 }, + { sku: 'apples', qty: 10, price: 2.5 } + ], + status: 'A' + }, + { + _id: 9, + cust_id: 'Don Quis', + ord_date: new Date('2020-03-20'), + price: 55, + items: [ + { sku: 'carrots', qty: 5, price: 1.0 }, + { sku: 'apples', qty: 10, price: 2.5 }, + { sku: 'oranges', qty: 10, price: 2.5 } + ], + status: 'A' + }, + { + _id: 10, + cust_id: 'Don Quis', + ord_date: new Date('2020-03-23'), + price: 25, + items: [{ sku: 'oranges', qty: 10, price: 2.5 }], + status: 'A' + } +]) +``` + +SQL 和 MongoDB 聚合方式对比: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200921200556.png) + +## 参考资料 + +- ** 官方 ** + - [MongoDB 官网](https://www.mongodb.com/) + - [MongoDB Github](https://github.com/mongodb/mongo) + - [MongoDB 官方免费教程](https://university.mongodb.com/) +- ** 教程 ** + - [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) + - [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/20.MongoDB\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\350\277\220\347\273\264.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/20.MongoDB\350\277\220\347\273\264.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\350\277\220\347\273\264.md" index eaaf16f165..721ace78af 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/20.MongoDB\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/MongoDB_\350\277\220\347\273\264.md" @@ -1,7 +1,7 @@ --- +icon: logos:mongodb title: MongoDB 运维 date: 2020-09-09 20:47:14 -order: 20 categories: - 数据库 - 文档数据库 @@ -11,7 +11,7 @@ tags: - 文档数据库 - MongoDB - 运维 -permalink: /pages/5e3c30/ +permalink: /pages/ca200834/ --- # MongoDB 运维 @@ -297,4 +297,4 @@ $ mongoexport -h 127.0.0.1 --port 27017 -d test -c product --type csv -f name,pr - [MongoDB 官网](https://www.mongodb.com/) - [MongoDB Github](https://github.com/mongodb/mongo) - [MongoDB 官方免费教程](https://university.mongodb.com/) -- [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) \ No newline at end of file +- [MongoDB 教程](https://www.runoob.com/mongodb/mongodb-tutorial.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/README.md" index 7cc53846b3..351fdb00c6 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/01.MongoDB/README.md" @@ -9,7 +9,7 @@ tags: - 数据库 - 文档数据库 - MongoDB -permalink: /pages/b1a116/ +permalink: /pages/4b8aff19/ hidden: true index: false --- @@ -24,30 +24,21 @@ index: false ## 📖 内容 -### [MongoDB 应用指南](01.MongoDB应用指南.md) - -### [MongoDB 的 CRUD 操作](02.MongoDB的CRUD操作.md) - -### [MongoDB 聚合操作](03.MongoDB的聚合操作.md) - -### [MongoDB 事务](04.MongoDB事务.md) - -### [MongoDB 建模](05.MongoDB建模.md) - -### [MongoDB 建模示例](06.MongoDB建模示例.md) - -### [MongoDB 索引](07.MongoDB索引.md) - -### [MongoDB 复制](08.MongoDB复制.md) - -### [MongoDB 分片](09.MongoDB分片.md) - -### [MongoDB 运维](20.MongoDB运维.md) +- [MongoDB 简介](MongoDB_简介.md) +- [MongoDB CRUD](MongoDB_CRUD.md) +- [MongoDB 聚合](MongoDB_聚合.md) +- [MongoDB 事务](MongoDB_事务.md) +- [MongoDB 建模](MongoDB_建模.md) +- [MongoDB 索引](MongoDB_索引.md) +- [MongoDB 复制](MongoDB_复制.md) +- [MongoDB 分片](MongoDB_分片.md) +- [MongoDB 运维](MongoDB_运维.md) ## 📚 资料 - **官方** - [MongoDB 官网](https://www.mongodb.com/) + - [MongoDB 官方手册](https://www.mongodb.com/zh-cn/docs/manual/) - [MongoDB Github](https://github.com/mongodb/mongo) - [MongoDB 官方免费教程](https://university.mongodb.com/) - **教程** @@ -60,4 +51,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/README.md" index 753b779233..d354e79039 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/04.\346\226\207\346\241\243\346\225\260\346\215\256\345\272\223/README.md" @@ -7,7 +7,7 @@ categories: tags: - 数据库 - 文档数据库 -permalink: /pages/d1dc5f/ +permalink: /pages/4b2d2f5c/ hidden: true index: false --- @@ -18,25 +18,15 @@ index: false ### MongoDB -#### [MongoDB 应用指南](01.MongoDB/01.MongoDB应用指南.md) - -#### [MongoDB 的 CRUD 操作](01.MongoDB/02.MongoDB的CRUD操作.md) - -#### [MongoDB 聚合操作](01.MongoDB/03.MongoDB的聚合操作.md) - -#### [MongoDB 事务](01.MongoDB/04.MongoDB事务.md) - -#### [MongoDB 建模](01.MongoDB/05.MongoDB建模.md) - -#### [MongoDB 建模示例](01.MongoDB/06.MongoDB建模示例.md) - -#### [MongoDB 索引](01.MongoDB/07.MongoDB索引.md) - -#### [MongoDB 复制](01.MongoDB/08.MongoDB复制.md) - -#### [MongoDB 分片](01.MongoDB/09.MongoDB分片.md) - -#### [MongoDB 运维](01.MongoDB/20.MongoDB运维.md) +- [MongoDB 简介](01.MongoDB/MongoDB_简介.md) +- [MongoDB CRUD](01.MongoDB/MongoDB_CRUD.md) +- [MongoDB 聚合](01.MongoDB/MongoDB_聚合.md) +- [MongoDB 事务](01.MongoDB/MongoDB_事务.md) +- [MongoDB 建模](01.MongoDB/MongoDB_建模.md) +- [MongoDB 索引](01.MongoDB/MongoDB_索引.md) +- [MongoDB 复制](01.MongoDB/MongoDB_复制.md) +- [MongoDB 分片](01.MongoDB/MongoDB_分片.md) +- [MongoDB 运维](01.MongoDB/MongoDB_运维.md) ## 📚 资料 @@ -56,4 +46,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/README.md" index 286bc36c2c..b385182460 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/README.md" @@ -10,7 +10,7 @@ tags: - 数据库 - KV数据库 - Redis -permalink: /pages/83e307/ +permalink: /pages/1cdc3dfb/ hidden: true index: false --- @@ -34,71 +34,26 @@ Redis 有多种高可用方案:**主从复制**模式、**哨兵**模式、** Redis 支持很多丰富的特性,如:**事务** 、**Lua 脚本**、**发布订阅**等等。 -![](https://architecturenotes.co/content/images/size/w2400/2022/08/Redis-v2-01-1.jpg) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411231010326.png) ## 📖 内容 -### [Redis 基本数据类型](01.Redis基本数据类型.md) - -> 关键词:`String`、`Hash`、`List`、`Set`、`Zset` - -### [Redis 高级数据类型](02.Redis高级数据类型.md) - -> 关键词:`BitMap`、`HyperLogLog`、`Geo`、`Stream` - -### [Redis 数据结构](03.Redis数据结构.md) - -> 关键词:`对象`、`SDS`、`链表`、`字典`、`跳表`、`整数集合`、`压缩列表` - -### [Redis 过期删除和内存淘汰](11.Redis过期删除和内存淘汰.md) - -> 关键词:`定时删除`、`惰性删除`、`定期删除`、`LRU`、`LFU` - -### [Redis 持久化](12.Redis持久化.md) - -> 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync` - -### [Redis 事件](13.Redis事件.md) - -> 关键词:`文件事件`、`时间事件` - -### [Redis 复制](21.Redis复制.md) - -> 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`命令传播`、`心跳` - -### [Redis 哨兵](22.Redis哨兵.md) - -> 关键词:`高可用`、`监控`、`选主`、`故障转移`、`Raft` - -### [Redis 集群](23.Redis集群.md) - -> 关键词:`高可用`、`监控`、`选主`、`故障转移`、`分区`、`Raft`、`Gossip` - -### [Redis 发布订阅](31.Redis发布订阅.md) - -> 关键词:`订阅`、`SUBSCRIBE`、`PSUBSCRIBE`、`PUBLISH`、`观察者模式` - -### [Redis 独立功能](32.Redis事务.md) - -> 关键词:`事务`、`ACID`、`MULTI`、`EXEC`、`DISCARD`、`WATCH` - -### [Redis 管道](33.Redis管道.md) - -> 关键词:`Pipeline` - -### [Redis 脚本](34.Redis脚本.md) - -> 关键词:`Lua` - -### [Redis 运维](41.Redis运维.md) - -> 关键词:`安装`、`配置`、`命令`、`集群`、`客户端` - -### [Redis 实战](42.Redis实战.md) - -> 关键词:`缓存`、`分布式锁`、`布隆过滤器` - -### [Redis 面试](99.Redis面试.md) +- [Redis 基本数据类型](Redis_数据类型.md) - 关键词:`String`、`Hash`、`List`、`Set`、`Zset` +- [Redis 高级数据类型](Redis_数据类型二.md) - 关键词:`BitMap`、`HyperLogLog`、`Geo`、`Stream` +- [Redis 数据结构](Redis_数据结构.md) - 关键词:`对象`、`SDS`、`链表`、`字典`、`跳表`、`整数集合`、`压缩列表` +- [Redis 内存管理](Redis_内存管理.md) - 关键词:`定时删除`、`惰性删除`、`定期删除`、`LRU`、`LFU` +- [Redis 持久化](Redis_持久化.md) - 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync` +- [Redis 事件](Redis_事件.md) - 关键词:`文件事件`、`时间事件` +- [Redis 复制](Redis_复制.md) - 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`命令传播`、`心跳` +- [Redis 哨兵](Redis_哨兵.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`Raft` +- [Redis 集群](Redis_集群.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`分区`、`Raft`、`Gossip` +- [Redis 订阅](Redis_订阅.md) - 关键词:`订阅`、`SUBSCRIBE`、`PSUBSCRIBE`、`PUBLISH`、`观察者模式` +- [Redis 独立功能](Redis_事务.md) - 关键词:`事务`、`ACID`、`MULTI`、`EXEC`、`DISCARD`、`WATCH` +- [Redis 管道](Redis_管道.md) - 关键词:`Pipeline` +- [Redis 脚本](Redis_脚本.md) - 关键词:`Lua` +- [Redis 运维](Redis_运维.md) - 关键词:`安装`、`配置`、`命令`、`集群`、`客户端` +- [Redis 实战](Redis_实战.md) - 关键词:`缓存`、`分布式锁`、`布隆过滤器` +- [Redis 面试](Redis_面试.md) - 关键词:`面试` ## 📚 资料 @@ -115,6 +70,7 @@ Redis 支持很多丰富的特性,如:**事务** 、**Lua 脚本**、**发 - **文章** - [Introduction to Redis](https://www.slideshare.net/dvirsky/introduction-to-redis) - [《我们一起进大厂》系列- Redis 基础](https://juejin.im/post/5db66ed9e51d452a2f15d833) + - [Redis Explained](https://architecturenotes.co/p/redis) - **源码** - [《Redis 实战》配套 Python 源码](https://github.com/josiahcarlson/redis-in-action) - **资源汇总** @@ -132,4 +88,4 @@ Redis 支持很多丰富的特性,如:**事务** 、**Lua 脚本**、**发 ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/13.Redis\344\272\213\344\273\266.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\344\272\213\344\273\266.md" similarity index 51% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/13.Redis\344\272\213\344\273\266.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\344\272\213\344\273\266.md" index a65fe0f1f4..218b92fdb6 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/13.Redis\344\272\213\344\273\266.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\344\272\213\344\273\266.md" @@ -11,7 +11,7 @@ tags: - 数据库 - KV数据库 - Redis -permalink: /pages/6e71c3/ +permalink: /pages/83bd4bff/ --- # Redis 事件 @@ -30,9 +30,18 @@ Redis 基于 Reactor 模式开发了自己的网络时间处理器。 - Redis 文件事件处理器使用 I/O 多路复用程序来同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。 - 当被监听的套接字准备好执行连接应答、读取、写入、关闭操作时,与操作相对应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。 -虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字,文件事件处理器实现了高性能的网络通信模型。 +**虽然文件事件处理器以单线程方式运行,但通过使用 I/O 多路复用程序来监听多个套接字**,文件事件处理器既实现了高性能的网络通信模型,又可以很好地与 Redis 服务器中其他同样以单线程方式运行的模块进行对接,这保持了 Redis 内部单线程设计的简单性。 -文件事件处理器有四个组成部分:套接字、I/O 多路复用程序、文件事件分派器、事件处理器。 +Redis 通过 **IO 多路复用程序** 来监听来自客户端的大量连接(或者说是监听多个 socket),它会将感兴趣的事件及类型(读、写)注册到内核中并监听每个事件是否发生。 + +这样的好处非常明显:**I/O 多路复用技术的使用让 Redis 不需要额外创建多余的线程来监听客户端的大量连接,降低了资源的消耗**(和 NIO 中的 `Selector` 组件很像)。 + +文件事件处理器有四个组成部分: + +- 多个 Socket(客户端连接) +- IO 多路复用程序(支持多个客户端连接的关键) +- 文件事件分派器(将 Socket 关联到相应的事件处理器) +- 事件处理器(连接应答处理器、命令请求处理器、命令回复处理器) ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200130172525.png) @@ -98,7 +107,49 @@ def main():
    +## 线程模型 + +虽然说 Redis 是单线程模型,但实际上,**Redis 在 4.0 之后的版本中就已经加入了对多线程的支持。** + +不过,Redis 4.0 增加的多线程主要是针对一些大键值对的删除操作的命令,使用这些命令就会使用主线程之外的其他线程来“异步处理”,从而减少对主线程的影响。 + +为此,Redis 4.0 之后新增了几个异步命令: + +- `UNLINK`:可以看作是 `DEL` 命令的异步版本。 +- `FLUSHALL ASYNC`:用于清空所有数据库的所有键,不限于当前 `SELECT` 的数据库。 +- `FLUSHDB ASYNC`:用于清空当前 `SELECT` 数据库中的所有键。 + +总的来说,直到 Redis 6.0 之前,Redis 的主要操作仍然是单线程处理的。 + +**Redis6.0 之前为什么不使用多线程?** 我觉得主要原因有 3 点: + +- 单线程编程容易并且更容易维护; +- Redis 的性能瓶颈不在 CPU ,主要在内存和网络; +- 多线程就会存在死锁、线程上下文切换等问题,甚至会影响性能。 + +**Redis6.0 引入多线程主要是为了提高网络 IO 读写性能**,因为这个算是 Redis 中的一个性能瓶颈(Redis 的瓶颈主要受限于内存和网络)。 + +虽然,Redis6.0 引入了多线程,但是 Redis 的多线程只是在网络数据的读写这类耗时操作上使用了,执行命令仍然是单线程顺序执行。因此,你也不需要担心线程安全问题。 + +Redis6.0 的多线程默认是禁用的,只使用主线程。如需开启需要设置 IO 线程数 > 1,需要修改 redis 配置文件 `redis.conf`: + +``` +io-threads 4 #设置1的话只会开启主线程,官网建议4核的机器建议设置为2或3个线程,8核的建议设置为6个线程 +``` + +另外: + +- io-threads 的个数一旦设置,不能通过 config 动态设置。 +- 当设置 ssl 后,io-threads 将不工作。 + +开启多线程后,默认只会使用多线程进行 IO 写入 writes,即发送数据给客户端,如果需要开启多线程 IO 读取 reads,同样需要修改 redis 配置文件 `redis.conf` : + +``` +io-threads-do-reads yes +``` + +但是官网描述开启多线程读并不能有太大提升,因此一般情况下并不建议开启 ## 参考资料 -- [《Redis 设计与实现》](https://item.jd.com/11486101.html) \ No newline at end of file +- [《Redis 设计与实现》](https://item.jd.com/11486101.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/32.Redis\344\272\213\345\212\241.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\344\272\213\345\212\241.md" similarity index 51% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/32.Redis\344\272\213\345\212\241.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\344\272\213\345\212\241.md" index 2d206daf41..0687396ee8 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/32.Redis\344\272\213\345\212\241.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\344\272\213\345\212\241.md" @@ -13,7 +13,7 @@ tags: - Redis - 事务 - ACID -permalink: /pages/476a09/ +permalink: /pages/d6a68f2a/ --- # Redis 事务 @@ -40,7 +40,7 @@ ACID 是数据库事务正确执行的四个基本要素。 - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 -**一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易。** +**一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。** - 只有满足一致性,事务的执行结果才是正确的。 - 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 @@ -72,6 +72,8 @@ Redis 官方的[事务特性文档](https://redis.io/docs/interact/transactions/ - 事务是一个单独的隔离操作:事务中的所有命令都会序列化、按顺序地执行。事务在执行的过程中,不会被其他客户端发送来的命令请求所打断。 - 事务是一个原子操作:事务中的命令要么全部被执行,要么全部都不执行。 +Redis 有天然解决这个并发竞争问题的类 CAS 乐观锁方案:每次要**写之前,先判断**一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。 + ### MULTI **[`MULTI`](https://redis.io/commands/multi) 命令用于开启一个事务,它总是返回 OK 。** @@ -170,6 +172,100 @@ ZREM zset element EXEC ``` +## Lua 脚本 + +### 为什么使用 Lua + +前面提到了,**Redis 仅支持“非严格”的事务**。 + +Lua 是一种轻量小巧的脚本语言,用标准 C 语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。 + +Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。 + +**Redis 执行 Lua 是原子操作**。因为 Redis 使用串行化的方式来执行 Redis 命令, 所以在任何特定时间里, 最多都只会有一个脚本能够被放进 Lua 环境里面运行, 因此, 整个 Redis 服务器只需要创建一个 Lua 环境即可。由于,Redis 执行 Lua 具有原子性,所以常被用于需要原子性执行多命令的场景。 + +不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, **严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。** + +如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。 + +另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/manual/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。 + +### Redis 脚本命令 + +| 命令 | 说明 | +| --------------- | ------------------------------------------------------------ | +| `EVAL` | `EVAL` 命令为客户端输入的脚本在 Lua 环境中定义一个函数, 并通过调用这个函数来执行脚本。 | +| `EVALSHA` | `EVALSHA` 命令通过直接调用 Lua 环境中已定义的函数来执行脚本。 | +| `SCRIPT_FLUSH` | `SCRIPT_FLUSH` 命令会清空服务器 `lua_scripts` 字典中保存的脚本, 并重置 Lua 环境。 | +| `SCRIPT_EXISTS` | `SCRIPT_EXISTS` 命令接受一个或多个 SHA1 校验和为参数, 并通过检查 `lua_scripts` 字典来确认校验和对应的脚本是否存在。 | +| `SCRIPT_LOAD` | `SCRIPT_LOAD` 命令接受一个 Lua 脚本为参数, 为该脚本在 Lua 环境中创建函数, 并将脚本保存到 `lua_scripts` 字典中。 | +| `SCRIPT_KILL` | `SCRIPT_KILL` 命令用于停止正在执行的脚本。 | + +### Redis 执行 Lua 的工作流程 + +为了在 Redis 服务器中执行 Lua 脚本, Redis 在服务器内嵌了一个 Lua 环境(environment), 并对这个 Lua 环境进行了一系列修改, 从而确保这个 Lua 环境可以满足 Redis 服务器的需要。 + +Redis 服务器创建并修改 Lua 环境的整个过程由以下步骤组成: + +1. 创建一个基础的 Lua 环境, 之后的所有修改都是针对这个环境进行的。 +2. 载入多个函数库到 Lua 环境里面, 让 Lua 脚本可以使用这些函数库来进行数据操作。 +3. 创建全局表格 `redis` , 这个表格包含了对 Redis 进行操作的函数, 比如用于在 Lua 脚本中执行 Redis 命令的 `redis.call` 函数。 +4. 使用 Redis 自制的随机函数来替换 Lua 原有的带有副作用的随机函数, 从而避免在脚本中引入副作用。 +5. 创建排序辅助函数, Lua 环境使用这个辅佐函数来对一部分 Redis 命令的结果进行排序, 从而消除这些命令的不确定性。 +6. 创建 `redis.pcall` 函数的错误报告辅助函数, 这个函数可以提供更详细的出错信息。 +7. 对 Lua 环境里面的全局环境进行保护, 防止用户在执行 Lua 脚本的过程中, 将额外的全局变量添加到了 Lua 环境里面。 +8. 将完成修改的 Lua 环境保存到服务器状态的 `lua` 属性里面, 等待执行服务器传来的 Lua 脚本。 + +### Redis 执行 Lua 的要点 + +- Redis 服务器专门使用一个伪客户端来执行 Lua 脚本中包含的 Redis 命令。 +- Redis 使用脚本字典来保存所有被 `EVAL` 命令执行过, 或者被 `SCRIPT_LOAD` 命令载入过的 Lua 脚本, 这些脚本可以用于实现 `SCRIPT_EXISTS` 命令, 以及实现脚本复制功能。 +- 服务器在执行脚本之前, 会为 Lua 环境设置一个超时处理钩子, 当脚本出现超时运行情况时, 客户端可以通过向服务器发送 `SCRIPT_KILL` 命令来让钩子停止正在执行的脚本, 或者发送 `SHUTDOWN nosave` 命令来让钩子关闭整个服务器。 +- 主服务器复制 `EVAL` 、 `SCRIPT_FLUSH` 、 `SCRIPT_LOAD` 三个命令的方法和复制普通 Redis 命令一样 —— 只要将相同的命令传播给从服务器就可以了。 +- 主服务器在复制 `EVALSHA` 命令时, 必须确保所有从服务器都已经载入了 `EVALSHA` 命令指定的 SHA1 校验和所对应的 Lua 脚本, 如果不能确保这一点的话, 主服务器会将 `EVALSHA` 命令转换成等效的 `EVAL` 命令, 并通过传播 `EVAL` 命令来获得相同的脚本执行效果。 + +### Redis + Lua 实现分布式锁 + +Redis 应用 Lua 的一个经典使用场景是实现分布式锁。其实现有 3 个重要的考量点: + +1. 互斥(只能有一个客户端获取锁) +2. 不能死锁 +3. 容错(只要大部分 redis 节点创建了这把锁就可以) + +对应的 Redis 指令如下: + +- `setnx` - `setnx key val`:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0。 +- `expire` - `expire key timeout`:为 key 设置一个超时时间,单位为 second,超过这个时间锁会自动释放,避免死锁。 +- `delete` - `delete key`:删除 key + +> 注意: +> +> 不要将 `setnx` 和 `expire` 作为两个命令组合实现加锁,这样就**无法保证原子性**。如果客户端在 `setnx` 之后崩溃,那么将导致锁无法释放。正确的做法应是在 `setnx` 命令中指定 `expire` 时间。 + +(1)申请锁 + +```shell +SET resource_name my_random_value NX PX 30000 +``` + +执行这个命令就 ok。 + +- `NX`:表示只有 `key` 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 `nil`) +- `PX 30000`:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。 + +(2)释放锁 + +释放锁就是删除 key ,但是一般可以用 `lua` 脚本删除,判断 value 一样才删除: + +```python +-- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。 +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + ## 参考资料 -- [《Redis 设计与实现》](https://item.jd.com/11486101.html) +- [《Redis 设计与实现》](https://item.jd.com/11486101.html) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/11.Redis\350\277\207\346\234\237\345\210\240\351\231\244\345\222\214\345\206\205\345\255\230\346\267\230\346\261\260.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\206\205\345\255\230\347\256\241\347\220\206.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/11.Redis\350\277\207\346\234\237\345\210\240\351\231\244\345\222\214\345\206\205\345\255\230\346\267\230\346\261\260.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\206\205\345\255\230\347\256\241\347\220\206.md" index 08cfa0a8a8..da2130f965 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/11.Redis\350\277\207\346\234\237\345\210\240\351\231\244\345\222\214\345\206\205\345\255\230\346\267\230\346\261\260.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\206\205\345\255\230\347\256\241\347\220\206.md" @@ -3,7 +3,6 @@ icon: logos:redis title: Redis 过期删除和内存淘汰 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202309171630222.png date: 2023-08-23 15:14:13 -order: 11 categories: - 数据库 - KV数据库 @@ -14,10 +13,10 @@ tags: - Redis - LRU - LFU -permalink: /pages/ce0453/ +permalink: /pages/e7f8e268/ --- -# Redis 过期删除和内存淘汰 +# Redis 内存管理 > 关键词:`定时删除`、`惰性删除`、`定期删除`、`LRU`、`LFU` diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/22.Redis\345\223\250\345\205\265.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\223\250\345\205\265.md" similarity index 92% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/22.Redis\345\223\250\345\205\265.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\223\250\345\205\265.md" index 35ea8718f1..32706a6967 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/22.Redis\345\223\250\345\205\265.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\223\250\345\205\265.md" @@ -3,7 +3,6 @@ icon: logos:redis title: Redis 哨兵 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202309190748787.png date: 2020-06-24 10:45:38 -order: 22 categories: - 数据库 - KV数据库 @@ -16,7 +15,7 @@ tags: - 选主 - 故障转移 - Raft -permalink: /pages/615afe/ +permalink: /pages/10970a97/ --- # Redis 哨兵 @@ -72,12 +71,12 @@ redis-server /path/to/sentinel.conf --sentinel ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202309190750857.png) -默认情况下, Sentinel 以**“每十秒一次”**的频率向被监视的主服务器和从服务器**发送 `INFO` 命令**,并通过分析 `INFO` 命令的回复来获取服务器的当前信息。 +默认情况下, Sentinel 以“**每十秒一次**”的频率向被监视的主服务器和从服务器**发送 `INFO` 命令**,并通过分析 `INFO` 命令的回复来获取服务器的当前信息。 - 主服务器 - 可以获取主服务器自身信息,以及其所属从服务器的地址信息。 - 从服务器 - 从服务器自身信息,以及其主服务器的了解状态和地址。 -**Sentinel 通过向主服务器发送 `INFO` 命令来获得主服务器属下所有从服务器的地址信息, 并为这些从服务器创建相应的实例结构, 以及连向这些从服务器的“命令连接”和“订阅连接”**。 +**Sentinel 通过向主服务器发送 `INFO` 命令来获得主服务器属下所有从服务器的地址信息, 并为这些从服务器创建相应的实例结构, 以及连向这些从服务器的“命令连接”和“订阅连接**”。 对于监视同一个主服务器和从服务器的多个 Sentinel 来说, 它们会以“每两秒一次”的频率, 通过向被监视服务器的 `__sentinel__:hello` 频道发送消息来向其他 Sentinel 宣告自己的存在。Sentinel 只会与主服务器和从服务器创建命令连接和订阅连接, Sentinel 与 Sentinel 之间则只创建命令连接。 @@ -92,24 +91,24 @@ redis-server /path/to/sentinel.conf --sentinel - “所知”是指,与 Sentinel 创建了命令连接的实例。 - “所有实例”包括了主服务器、从服务器以及其他 Sentinel 实例。 -如果,**某实例在指定的时长( `down-after-milliseconds` 设置的值,单位毫秒)中,未向 Sentinel 发送有效回复, Sentinel 会将该实例判定为“主观下线”**。 +如果,**某实例在指定的时长( `down-after-milliseconds` 设置的值,单位毫秒)中,未向 Sentinel 发送有效回复, Sentinel 会将该实例判定为“主观下线**”。 - 一个有效的 `PING` 回复可以是:`+PONG`、`-LOADING` 或者 `-MASTERDOWN`。如果服务器返回除以上三种回复之外的其他回复,又或者在 **指定时间** 内没有回复 `PING` 命令, 那么 Sentinel 认为服务器返回的回复无效。 - “主观下线”适用于所有主节点和从节点。 #### 客观下线 -当一个**“主服务器”**被 Sentinel 标记为**“主观下线”**后,为了确认其是否真的下线,Sentinel 会向同样监听该主服务器的其他 Sentinel 发起询问。如果有**“足够数量”**的 Sentinel 在指定的时间范围内认为主服务器已下线,那么这个**“主服务器”**被标记为**“客观下线”**。 +当一个“**主服务器**”被 Sentinel 标记为“**主观下线**”后,为了确认其是否真的下线,Sentinel 会向同样监听该主服务器的其他 Sentinel 发起询问。如果有“**足够数量**”的 Sentinel 在指定的时间范围内认为主服务器已下线,那么这个“**主服务器**”被标记为“**客观下线**”。 - Sentinel 节点通过 `sentinel is-master-down-by-addr` 命令,向其它 Sentinel 节点询问对某主服务器的 **状态判断**。 - “足够数量”是指 Sentinel 配置中 `quorum` 参数所设的值。 - 客观下线只适用于主节点。 -注:默认情况下, Sentinel 以**“每十秒一次”**的频率向被监视的主服务器和从服务器**发送 `INFO` 命令**。当一个主服务器被 Sentinel 标记为**“客观下线”**时,Sentinel 向该主服务器的所有从服务器发送 `INFO` 命令的频率,会从**“每十秒一次”**改为**“每秒一次”**。 +注:默认情况下, Sentinel 以“**每十秒一次**”的频率向被监视的主服务器和从服务器**发送 `INFO` 命令**。当一个主服务器被 Sentinel 标记为“**客观下线**”时,Sentinel 向该主服务器的所有从服务器发送 `INFO` 命令的频率,会从“**每十秒一次**”改为“**每秒一次**”。 ## 选主 -> Redis Sentinel 采用 [Raft 协议](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) 实现了其 Sentinel 选主流程。Raft 是一种共识性算法,想了解其原理,可以参考 [深入剖析共识性算法 Raft](https://dunwu.github.io/waterdrop/pages/4907dc/)。 +> Redis Sentinel 采用 [Raft 协议](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) 实现了其 Sentinel 选主流程。Raft 是一种共识性算法,想了解其原理,可以参考 [深入剖析共识性算法 Raft](https://dunwu.github.io/waterdrop/pages/9386474c/)。 **当一个“主服务器”被判断为“客观下线”时,监视该主服务器的各个 Sentinel 会进行“协商”,选举出一个领头的 Sentinel(Leader),并由领头 Sentinel 对下线主服务器执行“故障转移”操作**。 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/21.Redis\345\244\215\345\210\266.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\244\215\345\210\266.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/21.Redis\345\244\215\345\210\266.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\244\215\345\210\266.md" index 28d2e0cb09..6cf26dc5db 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/21.Redis\345\244\215\345\210\266.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\244\215\345\210\266.md" @@ -3,7 +3,6 @@ icon: logos:redis title: Redis 复制 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/20230914071554.png date: 2020-06-24 10:45:38 -order: 21 categories: - 数据库 - KV数据库 @@ -13,7 +12,7 @@ tags: - KV数据库 - Redis - 复制 -permalink: /pages/379cd8/ +permalink: /pages/c7ac6486/ --- # Redis 复制 @@ -291,4 +290,4 @@ REPLCONF ACK ## 参考资料 - [Redis 官方文档之复制](https://redis.io/docs/management/replication/) -- [《Redis 设计与实现》](https://item.jd.com/11486101.html) \ No newline at end of file +- [《Redis 设计与实现》](https://item.jd.com/11486101.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/42.Redis\345\256\236\346\210\230.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\256\236\346\210\230.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/42.Redis\345\256\236\346\210\230.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\256\236\346\210\230.md" index d11963ff2f..c822241870 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/42.Redis\345\256\236\346\210\230.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\345\256\236\346\210\230.md" @@ -2,7 +2,6 @@ icon: logos:redis title: Redis 实战 date: 2020-06-24 10:45:38 -order: 42 categories: - 数据库 - KV数据库 @@ -11,7 +10,7 @@ tags: - 数据库 - KV数据库 - Redis -permalink: /pages/1fc9c4/ +permalink: /pages/b5256e7c/ --- # Redis 实战 @@ -42,7 +41,7 @@ BitMap 和 BloomFilter 都可以用于解决缓存穿透问题。要点在于: - **避免永远不释放锁** - 使用 `expire` 加一个过期时间,避免一直不释放锁,导致阻塞。 - **原子性** - setnx 和 expire 必须合并为一个原子指令,避免 setnx 后,机器崩溃,没来得及设置 expire,从而导致锁永不释放。 -> 更多分布式锁的实现方式及细节,请参考:[分布式锁基本原理](https://dunwu.github.io/waterdrop/pages/40ac64/) +> 更多分布式锁的实现方式及细节,请参考:[分布式锁基本原理](https://dunwu.github.io/waterdrop/pages/0eb5a899/) 根据 Redis 的特性,在实际应用中,存在一些应用小技巧。 @@ -503,4 +502,4 @@ public static class CleanSessionsThread extends Thread { ## 参考资料 - [《Redis 实战》](https://item.jd.com/11791607.html) -- [《Redis 设计与实现》](https://item.jd.com/11486101.html) \ No newline at end of file +- [《Redis 设计与实现》](https://item.jd.com/11486101.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/12.Redis\346\214\201\344\271\205\345\214\226.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\214\201\344\271\205\345\214\226.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/12.Redis\346\214\201\344\271\205\345\214\226.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\214\201\344\271\205\345\214\226.md" index 84881437f8..03b555d5b7 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/12.Redis\346\214\201\344\271\205\345\214\226.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\214\201\344\271\205\345\214\226.md" @@ -3,7 +3,6 @@ icon: logos:redis title: Redis 持久化 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202309150716562.png date: 2020-06-24 10:45:38 -order: 12 categories: - 数据库 - KV数据库 @@ -14,7 +13,7 @@ tags: - Redis - 持久化 - CoW -permalink: /pages/4de901/ +permalink: /pages/2504588d/ --- # Redis 持久化 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/01.Redis\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\261\273\345\236\213.md" similarity index 86% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/01.Redis\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\261\273\345\236\213.md" index 4665da76b0..d0a4b6975a 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/01.Redis\345\237\272\346\234\254\346\225\260\346\215\256\347\261\273\345\236\213.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\261\273\345\236\213.md" @@ -3,17 +3,16 @@ icon: logos:redis title: Redis 基本数据类型 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/20230901071808.png date: 2020-06-24 10:45:38 -order: 01 categories: - 数据库 - KV数据库 - Redis tags: - 数据库 - - KV数据库 + - KV 数据库 - Redis - 数据类型 -permalink: /pages/ed757c/ +permalink: /pages/cbe4e503/ --- # Redis 基本数据类型 @@ -40,9 +39,7 @@ String 类型是**二进制安全**的。二进制安全是指,String 类型 默认情况下,String 类型的值最大可为 **512 MB**。 -
    - -
    +![](https://raw.githubusercontent.com/dunwu/images/master/cs/database/redis/redis-datatype-string.png) ### String 实现 @@ -56,6 +53,12 @@ SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言 **字符串对象的编码可以是 `int` 、 `raw` 或者 `embstr`** 。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100759580.svg) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100759674.svg) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100800212.svg) + 字符串对象保存各类型值的编码方式: | 值 | 编码 | @@ -66,7 +69,7 @@ SDS 和我们认识的 C 字符串不太一样,之所以没有使用 C 语言 如果一个字符串对象保存的是整数值, 并且这个整数值可以用 `long` 类型来表示, 那么字符串对象会将整数值保存在字符串对象结构的 `ptr` 属性里面(将 `void*` 转换成 `long` ), 并将字符串对象的编码设置为 `int` 。 -【示例】 +【示例】set 整数值 ```shell > SET number 10086 @@ -91,7 +94,7 @@ OK 如果字符串对象保存的是一个字符串值, 并且这个字符串值的长度小于等于 `39` 字节, 那么字符串对象将使用 `embstr` 编码的方式来保存这个字符串值。`embstr` 编码是专门用于保存短字符串的一种优化编码方式。 -【示例】 +【示例】set 字符串值 ```c > SET msg "hello" @@ -204,7 +207,7 @@ OK > set user:1 {"name":"dunwu","sex":"man"} ``` -(2)将 key 分离为 user:ID:属性的形式,采用 MSET 存储,用 MGET 获取各属性值 +(2)将 key 分离为 user:ID: 属性的形式,采用 MSET 存储,用 MGET 获取各属性值 ```shell > mset user:1:name dunwu user:1:sex man @@ -295,16 +298,13 @@ end > > 缺点:需要去实现存取 Session 的代码。 -
    - -
    +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/MultiNode-SpringSession.jpg) + ## Hash ### Hash 简介 -
    - -
    +![](https://raw.githubusercontent.com/dunwu/images/master/cs/database/redis/redis-datatype-hash.png) Hash 是一个键值对(key - value)集合,其中 value 的形式如: `value=[{field1,value1},...{fieldN,valueN}]`。Hash 特别适合用于存储对象。 @@ -314,8 +314,14 @@ Hash 是一个键值对(key - value)集合,其中 value 的形式如: `v `ziplist` 编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时, 程序会先将保存了键的压缩列表节点推入到压缩列表表尾, 然后再将保存了值的压缩列表节点推入到压缩列表表尾。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100803215.svg) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100804441.svg) + `hashtable` 编码的哈希对象使用字典作为底层实现, 哈希对象中的每个键值对都使用一个字典键值对来保存。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100805761.svg) + 当哈希对象同时满足以下两个条件时, 使用 `ziplist` 编码;否则,使用 `hashtable` 编码。 1. 哈希对象保存的所有键值对的键和值的字符串长度都小于 `64` 字节(可由 `hash-max-ziplist-value` 配置); @@ -339,24 +345,24 @@ Hash 是一个键值对(key - value)集合,其中 value 的形式如: `v > 更多命令请参考:[Redis Hash 类型官方命令文档](https://redis.io/commands#hash) ```shell -# 存储一个哈希表key的键值 +# 存储一个哈希表 key 的键值 HSET key field value -# 获取哈希表key对应的field键值 +# 获取哈希表 key 对应的 field 键值 HGET key field -# 在一个哈希表key中存储多个键值对 +# 在一个哈希表 key 中存储多个键值对 HMSET key field value [field value...] -# 批量获取哈希表key中多个field键值 +# 批量获取哈希表 key 中多个 field 键值 HMGET key field [field ...] -# 删除哈希表key中的field键值 +# 删除哈希表 key 中的 field 键值 HDEL key field [field ...] -# 返回哈希表key中field的数量 +# 返回哈希表 key 中 field 的数量 HLEN key -# 返回哈希表key中所有的键值 +# 返回哈希表 key 中所有的键值 HGETALL key -# 为哈希表key中field键的值加上增量n +# 为哈希表 key 中 field 键的值加上增量 n HINCRBY key field n ``` @@ -373,13 +379,13 @@ Hash 类型的(key,field,value)的结构与对象的(对象 id,属 我们可以使用如下命令,将用户对象的信息存储到 Hash 类型: ```shell -# 存储一个哈希表uid:1的键值 +# 存储一个哈希表 uid:1 的键值 > HMSET uid:1 name Tom age 15 2 -# 存储一个哈希表uid:2的键值 +# 存储一个哈希表 uid:2 的键值 > HMSET uid:2 name Jerry age 13 2 -# 获取哈希表用户id为1中所有的键值 +# 获取哈希表用户 id 为 1 中所有的键值 > HGETALL uid:1 1) "name" 2) "Tom" @@ -411,10 +417,10 @@ Redis Hash 存储其结构如下图: 维护购物车的常见操作如下: -- 添加商品 - `HSET cart:{session} {商品id} 1` -- 添加数量 - `HINCRBY cart:{session} {商品id} 1` +- 添加商品 - `HSET cart:{session} {商品 id} 1` +- 添加数量 - `HINCRBY cart:{session} {商品 id} 1` - 商品总数 - `HLEN cart:{session}` -- 删除商品 - `HDEL cart:{session} {商品id}` +- 删除商品 - `HDEL cart:{session} {商品 id}` - 获取购物车所有商品 - `HGETALL cart:{session}` 当前仅仅是将商品 ID 存储到了 Redis 中,在回显商品具体信息的时候,还需要拿着商品 id 查询一次数据库,获取完整的商品的信息。 @@ -425,9 +431,7 @@ Redis 中的 List 类型就是有序列表。 ### List 简介 -
    - -
    +![](https://raw.githubusercontent.com/dunwu/images/master/cs/database/redis/redis-datatype-list.png) List 列表是简单的字符串列表,**按照插入顺序排序**,可以从头部或尾部向 List 列表添加元素。 @@ -439,11 +443,11 @@ List 列表是简单的字符串列表,**按照插入顺序排序**,可以 `ziplist` 编码的列表对象使用压缩列表作为底层实现, 每个压缩列表节点(entry)保存了一个列表元素。 -![](http://redisbook.com/_images/graphviz-a8d31075b4c0537f4eb6d84aaba1df928c67c953.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100802398.svg) `inkedlist` 编码的列表对象使用双链表作为底层实现。 -![](http://redisbook.com/_images/graphviz-84c0d231f30c740a431407c7aaf3851b96399590.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100802787.svg) 当列表对象可以同时满足以下两个条件时, 列表对象使用 `ziplist` 编码;否则,使用 `linkedlist` 编码 @@ -470,21 +474,21 @@ List 列表是简单的字符串列表,**按照插入顺序排序**,可以 > 更多命令请参考:[Redis List 类型官方命令文档](https://redis.io/commands#list) ```shell -# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面 +# 将一个或多个值 value 插入到 key 列表的表头(最左边),最后的值在最前面 LPUSH key value [value ...] -# 将一个或多个值value插入到key列表的表尾(最右边) +# 将一个或多个值 value 插入到 key 列表的表尾(最右边) RPUSH key value [value ...] -# 移除并返回key列表的头元素 +# 移除并返回 key 列表的头元素 LPOP key -# 移除并返回key列表的尾元素 +# 移除并返回 key 列表的尾元素 RPOP key -# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始 +# 返回列表 key 中指定区间内的元素,区间以偏移量 start 和 stop 指定,从 0 开始 LRANGE key start stop -# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞 +# 从 key 列表表头弹出一个元素,没有就阻塞 timeout 秒,如果 timeout=0 则一直阻塞 BLPOP key [key ...] timeout -# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞 +# 从 key 列表表尾弹出一个元素,没有就阻塞 timeout 秒,如果 timeout=0 则一直阻塞 BRPOP key [key ...] timeout ``` @@ -597,11 +601,11 @@ Set 类型和 List 类型的区别如下: `intset` 编码的集合对象使用整数集合作为底层实现, 集合对象包含的所有元素都被保存在整数集合里面。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202309241059483.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100806680.svg) `hashtable` 编码的集合对象使用字典作为底层实现, 字典的每个键都是一个字符串对象, 每个字符串对象包含了一个集合元素, 而字典的值则全部被设置为 `NULL` 。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202309241100174.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100806732.svg) 当集合对象可以同时满足以下两个条件时,集合对象使用 `intset` 编码;否则,使用 `hashtable` 编码: @@ -624,21 +628,21 @@ Set 类型和 List 类型的区别如下: Set 常用操作: ```shell -# 往集合key中存入元素,元素存在则忽略,若key不存在则新建 +# 往集合 key 中存入元素,元素存在则忽略,若 key 不存在则新建 SADD key member [member ...] -# 从集合key中删除元素 +# 从集合 key 中删除元素 SREM key member [member ...] -# 获取集合key中所有元素 +# 获取集合 key 中所有元素 SMEMBERS key -# 获取集合key中的元素个数 +# 获取集合 key 中的元素个数 SCARD key -# 判断member元素是否存在于集合key中 +# 判断 member 元素是否存在于集合 key 中 SISMEMBER key member -# 从集合key中随机选出count个元素,元素不从key中删除 +# 从集合 key 中随机选出 count 个元素,元素不从 key 中删除 SRANDMEMBER key [count] -# 从集合key中随机选出count个元素,元素从key中删除 +# 从集合 key 中随机选出 count 个元素,元素从 key 中删除 SPOP key [count] ``` @@ -647,17 +651,17 @@ Set 运算操作: ```shell # 交集运算 SINTER key [key ...] -# 将交集结果存入新集合destination中 +# 将交集结果存入新集合 destination 中 SINTERSTORE destination key [key ...] # 并集运算 SUNION key [key ...] -# 将并集结果存入新集合destination中 +# 将并集结果存入新集合 destination 中 SUNIONSTORE destination key [key ...] # 差集运算 SDIFF key [key ...] -# 将差集结果存入新集合destination中 +# 将差集结果存入新集合 destination 中 SDIFFSTORE destination key [key ...] ``` @@ -715,7 +719,7 @@ Set 类型可以保证一个用户只能点一个赞,这里举例子一个场 ```shell > SISMEMBER article:1 uid:1 -(integer) 0 # 返回0说明没点赞,返回1则说明点赞了 +(integer) 0 # 返回 0 说明没点赞,返回 1 则说明点赞了 ``` #### 共同关注 @@ -757,9 +761,9 @@ key 可以是用户 id,value 则是已关注的公众号的 id。 ```shell > SISMEMBER uid:1 5 -(integer) 1 # 返回1,说明关注了 +(integer) 1 # 返回 1,说明关注了 > SISMEMBER uid:2 5 -(integer) 0 # 返回0,说明没关注 +(integer) 0 # 返回 0,说明没关注 ``` #### 抽奖活动 @@ -769,7 +773,7 @@ key 可以是用户 id,value 则是已关注的公众号的 id。 key 为抽奖活动名,value 为员工名称,把所有员工名称放入抽奖箱: ```shell ->SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark +> SADD lucky Tom Jerry John Sean Marry Lindy Sary Mark (integer) 5 ``` @@ -793,14 +797,14 @@ key 为抽奖活动名,value 为员工名称,把所有员工名称放入抽 如果不允许重复中奖,可以使用 SPOP 命令。 ```shell -# 抽取一等奖1个 +# 抽取一等奖 1 个 > SPOP lucky 1 1) "Sary" -# 抽取二等奖2个 +# 抽取二等奖 2 个 > SPOP lucky 2 1) "Jerry" 2) "Mark" -# 抽取三等奖3个 +# 抽取三等奖 3 个 > SPOP lucky 3 1) "John" 2) "Sean" @@ -827,6 +831,10 @@ Zset 类型(有序集合类型)相比于 Set 类型多了一个排序属性 `ziplist` 编码的有序集合对象使用压缩列表作为底层实现, 每个集合元素使用两个紧挨在一起的压缩列表节点来保存, 第一个节点保存元素的成员(member), 而第二个元素则保存元素的分值(score)。压缩列表内的集合元素按分值从小到大进行排序, 分值较小的元素被放置在靠近表头的方向, 而分值较大的元素则被放置在靠近表尾的方向。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100808991.svg) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100808319.svg) + `skiplist` 编码的有序集合对象使用 `zset` 结构作为底层实现, 一个 `zset` 结构同时包含一个字典和一个跳跃表 ```c @@ -843,9 +851,11 @@ typedef struct zset { 除此之外, `zset` 结构中的 `dict` 字典为有序集合创建了一个从成员到分值的映射, 字典中的每个键值对都保存了一个集合元素: 字典的键保存了元素的成员, 而字典的值则保存了元素的分值。 通过这个字典, 程序可以用 O(1) 复杂度查找给定成员的分值, ZSCORE 命令就是根据这一特性实现的, 而很多其他有序集合命令都在实现的内部用到了这一特性。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100810255.svg) + 有序集合每个元素的成员都是一个字符串对象, 而每个元素的分值都是一个 `double` 类型的浮点数。 值得一提的是, 虽然 `zset` 结构同时使用跳跃表和字典来保存有序集合元素, 但这两种数据结构都会通过指针来共享相同元素的成员和分值, 所以同时使用跳跃表和字典来保存集合元素不会产生任何重复成员或者分值, 也不会因此而浪费额外的内存。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202309241108347.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100812776.svg) 当有序集合对象可以同时满足以下两个条件时,有序集合对象使用 `ziplist` 编码;否则,使用 `skiplist` 编码。 @@ -858,54 +868,54 @@ typedef struct zset { ### Zset 命令 -| 命令 | 行为 | -| ---------------- | ------------------------------------------ | -| `ZADD` | 将一个带有给定分值的成员添加到有序集合里面 | -| `ZRANGE` | 顺序排序,并返回指定排名区间的成员 | -| ZREVRANGE | 反序排序,并返回指定排名区间的成员 | -| `ZRANGEBYSCORE` | 顺序排序,并返回指定排名区间的成员及其分值 | -| ZREVRANGEBYSCORE | 反序排序,并返回指定排名区间的成员及其分值 | -| `ZREM` | 移除指定的成员 | -| `ZSCORE` | 返回指定成员的分值 | -| `ZCARD` | 返回所有成员数 | +| 命令 | 行为 | +| ------------------ | ------------------------------------------ | +| `ZADD` | 将一个带有给定分值的成员添加到有序集合里面 | +| `ZRANGE` | 顺序排序,并返回指定排名区间的成员 | +| `ZREVRANGE` | 反序排序,并返回指定排名区间的成员 | +| `ZRANGEBYSCORE` | 顺序排序,并返回指定排名区间的成员及其分值 | +| `ZREVRANGEBYSCORE` | 反序排序,并返回指定排名区间的成员及其分值 | +| `ZREM` | 移除指定的成员 | +| `ZSCORE` | 返回指定成员的分值 | +| `ZCARD` | 返回所有成员数 | > 更多命令请参考:[Redis ZSet 类型官方命令文档](https://redis.io/commands#sorted_set) Zset 常用操作: ```shell -# 往有序集合key中加入带分值元素 +# 往有序集合 key 中加入带分值元素 ZADD key score member [[score member]...] -# 往有序集合key中删除元素 +# 往有序集合 key 中删除元素 ZREM key member [member...] -# 返回有序集合key中元素member的分值 +# 返回有序集合 key 中元素 member 的分值 ZSCORE key member -# 返回有序集合key中元素个数 +# 返回有序集合 key 中元素个数 ZCARD key -# 为有序集合key中元素member的分值加上increment +# 为有序集合 key 中元素 member 的分值加上 increment ZINCRBY key increment member -# 正序获取有序集合key从start下标到stop下标的元素 +# 正序获取有序集合 key 从 start 下标到 stop 下标的元素 ZRANGE key start stop [WITHSCORES] -# 倒序获取有序集合key从start下标到stop下标的元素 +# 倒序获取有序集合 key 从 start 下标到 stop 下标的元素 ZREVRANGE key start stop [WITHSCORES] # 返回有序集合中指定分数区间内的成员,分数由低到高排序。 ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count] -# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。 +# 返回指定成员区间内的成员,按字典正序排列,分数必须相同。 ZRANGEBYLEX key min max [LIMIT offset count] -# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同 +# 返回指定成员区间内的成员,按字典倒序排列,分数必须相同 ZREVRANGEBYLEX key max min [LIMIT offset count] ``` Zset 运算操作(相比于 Set 类型,ZSet 类型没有支持差集运算): ```shell -# 并集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积 +# 并集计算(相同元素分值相加),numberkeys 一共多少个 key,WEIGHTS 每个 key 对应的分值乘积 ZUNIONSTORE destkey numberkeys key [key...] -# 交集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积 +# 交集计算(相同元素分值相加),numberkeys 一共多少个 key,WEIGHTS 每个 key 对应的分值乘积 ZINTERSTORE destkey numberkeys key [key...] ``` @@ -928,19 +938,19 @@ Zset 类型(Sorted Set,有序集合)可以根据元素的权重来排序 我们以博文点赞排名为例,小林发表了五篇博文,分别获得赞为 200、40、100、50、150。 ```shell -# arcticle:1 文章获得了200个赞 +# arcticle:1 文章获得了 200 个赞 > ZADD user:xiaolin:ranking 200 arcticle:1 (integer) 1 -# arcticle:2 文章获得了40个赞 +# arcticle:2 文章获得了 40 个赞 > ZADD user:xiaolin:ranking 40 arcticle:2 (integer) 1 -# arcticle:3 文章获得了100个赞 +# arcticle:3 文章获得了 100 个赞 > ZADD user:xiaolin:ranking 100 arcticle:3 (integer) 1 -# arcticle:4 文章获得了50个赞 +# arcticle:4 文章获得了 50 个赞 > ZADD user:xiaolin:ranking 50 arcticle:4 (integer) 1 -# arcticle:5 文章获得了150个赞 +# arcticle:5 文章获得了 150 个赞 > ZADD user:xiaolin:ranking 150 arcticle:5 (integer) 1 ``` @@ -1077,7 +1087,7 @@ _2、姓名排序_ ## 总结 -Redis 常见的五种数据类型:**String(字符串),Hash(哈希),List(列表),Set(集合)及 Zset(sorted set:有序集合)**。 +Redis 常见的五种数据类型:**String(字符串),Hash(哈希),List(列表),Set(集合)及 Zset(sorted set:有序集合)**。 这五种数据类型都由多种数据结构实现的,主要是出于时间和空间的考虑,当数据量小的时候使用更简单的数据结构,有利于节省内存,提高性能。 @@ -1088,8 +1098,8 @@ Redis 常见的五种数据类型:**String(字符串),Hash(哈希) Redis 五种数据类型的应用场景: -- String 类型的应用场景:缓存对象、常规计数、分布式锁、共享 session 信息等。 -- List 类型的应用场景:消息队列(有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)等。 +- String 类型的应用场景:缓存对象、分布式锁、共享 session、计数器、限流、分布式 ID 等。 +- List 类型的应用场景:消息队列(有两个问题:1. 生产者需要自行实现全局唯一 ID;2. 不能以消费组形式消费数据)、输入自动补全等。 - Hash 类型:缓存对象、购物车等。 - Set 类型:聚合计算(并集、交集、差集)场景,比如点赞、共同关注、抽奖活动等。 - Zset 类型:排序场景,比如排行榜、电话和姓名排序等。 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/02.Redis\351\253\230\347\272\247\346\225\260\346\215\256\347\261\273\345\236\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\261\273\345\236\213\344\272\214.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/02.Redis\351\253\230\347\272\247\346\225\260\346\215\256\347\261\273\345\236\213.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\261\273\345\236\213\344\272\214.md" index 56734e213e..4cf91c3365 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/02.Redis\351\253\230\347\272\247\346\225\260\346\215\256\347\261\273\345\236\213.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\261\273\345\236\213\344\272\214.md" @@ -3,7 +3,6 @@ icon: logos:redis title: Redis 高级数据类型 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/20230901071808.png date: 2020-06-24 10:45:38 -order: 02 categories: - 数据库 - KV数据库 @@ -13,7 +12,7 @@ tags: - KV数据库 - Redis - 数据类型 -permalink: /pages/518280/ +permalink: /pages/205e0261/ --- # Redis 高级数据类型 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/03.Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\273\223\346\236\204.md" similarity index 97% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/03.Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\273\223\346\236\204.md" index 8a5c0a6152..3fc3948443 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/03.Redis\346\225\260\346\215\256\347\273\223\346\236\204.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\346\225\260\346\215\256\347\273\223\346\236\204.md" @@ -3,20 +3,19 @@ icon: logos:redis title: Redis 数据结构 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/20230901071535.png date: 2023-08-23 15:14:13 -order: 03 categories: - 数据库 - KV数据库 - Redis tags: - 数据库 - - KV数据库 + - KV 数据库 - Redis - 数据结构 - 链表 - 字典 - 跳表 -permalink: /pages/aae60d/ +permalink: /pages/11d1c545/ --- # Redis 数据结构 @@ -61,7 +60,7 @@ struct sdshdr { SDS 遵循 C 字符串以空字符结尾的惯例, 保存空字符的 `1` 字节空间不计算在 SDS 的 `len` 属性里面, 并且为空字符分配额外的 `1` 字节空间, 以及添加空字符到字符串末尾等操作都是由 SDS 函数自动完成的, 所以这个空字符对于 SDS 的使用者来说是完全透明的。 -![](http://redisbook.com/_images/graphviz-72760f6945c3742eca0df91a91cc379168eda82d.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100815385.svg) ### SDS 特性 @@ -128,8 +127,6 @@ typedef struct listNode { 多个 `listNode` 可以通过 `prev` 和 `next` 指针组成双链表。 -![](http://redisbook.com/_images/graphviz-167adfc2e52e078d4c0e3c8a9eddec54551602fb.png) - 虽然仅仅使用多个 `listNode` 结构就可以组成链表, 但使用 `adlist.h/list` 来持有链表的话, 操作起来会更方便: ```c @@ -162,7 +159,7 @@ typedef struct list { - `free` 函数 - 用于释放链表节点所保存的值; - `match` 函数 - 用于对比链表节点所保存的值和另一个输入值是否相等。 -![](http://redisbook.com/_images/graphviz-5f4d8b6177061ac52d0ae05ef357fceb52e9cb90.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100817994.svg) ## 字典 @@ -255,7 +252,7 @@ typedef struct dict { - `ht` 属性是一个包含两个项的数组, 数组中的每个项都是一个 `dictht` 哈希表, 一般情况下, 字典只使用 `ht[0]` 哈希表, `ht[1]` 哈希表只会在对 `ht[0]` 哈希表进行 rehash 时使用。 - `rehashidx` 属性记录了 rehash 目前的进度, 如果目前没有在进行 rehash , 那么它的值为 `-1` 。 -![](http://redisbook.com/_images/graphviz-e73003b166b90094c8c4b7abbc8d59f691f91e27.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100819328.svg) ### 哈希算法 @@ -278,7 +275,7 @@ index = hash & dict->ht[x].sizemask; **Redis 使用链地址法(separate chaining)来解决哈希冲突**: 每个哈希表节点都有一个 `next` 指针, 多个哈希表节点可以用 `next` 指针构成一个单向链表, 被分配到同一个索引上的多个节点可以用这个单向链表连接起来, 这就解决了键冲突的问题。 -![](http://redisbook.com/_images/graphviz-4b52dcf6eb0768750e1c15480be3326ca37e05b3.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410100822993.svg) ### rehash @@ -683,9 +680,9 @@ typedef struct redisObject { } robj; ``` -OBJECT IDLETIME 命令可以打印出给定键的空转时长, 这一空转时长就是通过将当前时间减去键的值对象的 `lru` 时间计算得出的: +`OBJECT IDLETIME` 命令可以打印出给定键的空转时长, 这一空转时长就是通过将当前时间减去键的值对象的 `lru` 时间计算得出的: -``` +```shell > SET msg "hello world" OK @@ -708,10 +705,10 @@ OK > 注意 > -> OBJECT IDLETIME 命令的实现是特殊的, 这个命令在访问键的值对象时, 不会修改值对象的 `lru` 属性。 +> `OBJECT IDLETIME` 命令的实现是特殊的, 这个命令在访问键的值对象时, 不会修改值对象的 `lru` 属性。 除了可以被 OBJECT IDLETIME 命令打印出来之外, 键的空转时长还有另外一项作用: 如果服务器打开了 `maxmemory` 选项, 并且服务器用于回收内存的算法为 `volatile-lru` 或者 `allkeys-lru` , 那么当服务器占用的内存数超过了 `maxmemory` 选项所设置的上限值时, 空转时长较高的那部分键会优先被服务器释放, 从而回收内存。 ## 参考资料 -- [《Redis 设计与实现》](https://item.jd.com/11486101.html) \ No newline at end of file +- [《Redis 设计与实现》](https://item.jd.com/11486101.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/33.Redis\347\256\241\351\201\223.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\347\256\241\351\201\223.md" similarity index 98% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/33.Redis\347\256\241\351\201\223.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\347\256\241\351\201\223.md" index 55c62adfc4..f09b28a55e 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/33.Redis\347\256\241\351\201\223.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\347\256\241\351\201\223.md" @@ -2,7 +2,6 @@ icon: logos:redis title: Redis 管道 date: 2023-09-11 22:22:31 -order: 33 categories: - 数据库 - KV数据库 @@ -12,7 +11,7 @@ tags: - KV数据库 - Redis - Pipeline -permalink: /pages/f1bbae/ +permalink: /pages/85187ea4/ --- # Redis 管道 @@ -124,4 +123,4 @@ public class Demo { ## 参考资料 - [《Redis 设计与实现》](https://item.jd.com/11486101.html) -- [阿里云管道传输](https://help.aliyun.com/zh/redis/use-cases/use-pipelining-to-batch-issue-commands?spm=a2c4g.11186623.0.0.1c193393SEIu92) \ No newline at end of file +- [阿里云管道传输](https://help.aliyun.com/zh/redis/use-cases/use-pipelining-to-batch-issue-commands?spm=a2c4g.11186623.0.0.1c193393SEIu92) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/34.Redis\350\204\232\346\234\254.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\204\232\346\234\254.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/34.Redis\350\204\232\346\234\254.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\204\232\346\234\254.md" index 86e378e02e..509b7b1d54 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/34.Redis\350\204\232\346\234\254.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\204\232\346\234\254.md" @@ -2,7 +2,6 @@ icon: logos:redis title: Redis 脚本 date: 2020-01-30 21:48:57 -order: 34 categories: - 数据库 - KV数据库 @@ -12,7 +11,7 @@ tags: - KV数据库 - Redis - Lua -permalink: /pages/30456b/ +permalink: /pages/f6008d32/ --- # Redis 脚本 @@ -67,4 +66,4 @@ Redis 服务器创建并修改 Lua 环境的整个过程由以下步骤组成: ## 参考资料 -- [《Redis 设计与实现》](https://item.jd.com/11486101.html) \ No newline at end of file +- [《Redis 设计与实现》](https://item.jd.com/11486101.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/31.Redis\345\217\221\345\270\203\350\256\242\351\230\205.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\256\242\351\230\205.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/31.Redis\345\217\221\345\270\203\350\256\242\351\230\205.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\256\242\351\230\205.md" index 989c3e3dee..fc31e56e1d 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/31.Redis\345\217\221\345\270\203\350\256\242\351\230\205.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\256\242\351\230\205.md" @@ -2,7 +2,6 @@ icon: logos:redis title: Redis 发布订阅 date: 2023-09-11 22:22:30 -order: 31 categories: - 数据库 - KV数据库 @@ -13,7 +12,7 @@ tags: - Redis - 订阅 - 观察者模式 -permalink: /pages/a329e5/ +permalink: /pages/c03b7b4f/ --- # Redis 发布订阅 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/41.Redis\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\277\220\347\273\264.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/41.Redis\350\277\220\347\273\264.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\277\220\347\273\264.md" index c6ae9382a8..639a63a237 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/41.Redis\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\350\277\220\347\273\264.md" @@ -2,7 +2,6 @@ icon: logos:redis title: Redis 运维 date: 2020-06-24 10:45:38 -order: 41 categories: - 数据库 - KV数据库 @@ -12,7 +11,7 @@ tags: - KV数据库 - Redis - 运维 -permalink: /pages/537098/ +permalink: /pages/246e9f5c/ --- # Redis 运维 @@ -684,4 +683,4 @@ redis-cli --cluster reshard 172.22.6.3 7001 - **教程** - [Redis 命令参考](http://redisdoc.com/) - **文章** - - [深入剖析 Redis 系列(三) - Redis 集群模式搭建与原理详解](https://juejin.im/post/5b8fc5536fb9a05d2d01fb11) \ No newline at end of file + - [深入剖析 Redis 系列(三) - Redis 集群模式搭建与原理详解](https://juejin.im/post/5b8fc5536fb9a05d2d01fb11) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/23.Redis\351\233\206\347\276\244.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\351\233\206\347\276\244.md" similarity index 98% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/23.Redis\351\233\206\347\276\244.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\351\233\206\347\276\244.md" index 152afe7aea..4d5e6081f9 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/23.Redis\351\233\206\347\276\244.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\351\233\206\347\276\244.md" @@ -3,7 +3,6 @@ icon: logos:redis title: Redis 集群 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/20230914072642.png date: 2020-06-24 10:45:38 -order: 23 categories: - 数据库 - KV数据库 @@ -19,7 +18,7 @@ tags: - 分区 - Raft - Gossip -permalink: /pages/77dfbe/ +permalink: /pages/6b0256da/ --- # Redis 集群 @@ -53,7 +52,7 @@ Redis Cluster 节点分为主节点(master)和从节点(slave): ### 分配 Hash 槽 -分布式存储需要解决的首要问题是把整个数据集按照**“分区规则”** 到**多个节点**,即每个节点负责整体数据的一个 **子集**。 +分布式存储需要解决的首要问题是把整个数据集按照“**分区规则**” 到**多个节点**,即每个节点负责整体数据的一个 **子集**。 **Redis Cluster 将整个数据库规划为 “16384” 个虚拟的哈希槽**,数据库中的每个键都属于其中一个槽。**每个节点都会记录哪些槽指派给了自己, 而哪些槽又被指派给了其他节点**。 @@ -109,7 +108,7 @@ MOVED : 对 Redis Cluster 的重新分片工作是由客户端(redis-trib)执行的, **重新分片的关键是将属于某个槽的所有键值对从一个节点转移至另一个节点**。 -重新分区操作可以**“在线”**进行,在重新分区的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。 +重新分区操作可以“**在线**”进行,在重新分区的过程中,集群不需要下线,并且源节点和目标节点都可以继续处理命令请求。 重新分区的实现原理如下图所示: @@ -174,7 +173,7 @@ Redis Cluster 节点间的复制是“异步”的。 Redis Cluster 采用 Gossip 协议基于两个主要目标:**去中心化**以及**失败检测**。 -Redis Cluster 中,每个节点之间都会同步信息,但是每个节点的信息不保证实时的,即无法保证数据强一致性,但是保证**“数据最终一致性”**——当集群中发生节点增减、故障、主从关系变化、槽信息变更等事件时,通过不断的通信,在经过一段时间后,所有的节点都会同步集群全部节点的最新状态。 +Redis Cluster 中,每个节点之间都会同步信息,但是每个节点的信息不保证实时的,即无法保证数据强一致性,但是保证“**数据最终一致性**”——当集群中发生节点增减、故障、主从关系变化、槽信息变更等事件时,通过不断的通信,在经过一段时间后,所有的节点都会同步集群全部节点的最新状态。 Redis Cluster 节点发送的消息主要有以下五种: diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/99.Redis\351\235\242\350\257\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\351\235\242\350\257\225.md" similarity index 53% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/99.Redis\351\235\242\350\257\225.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\351\235\242\350\257\225.md" index 62a9517cc0..12160eb245 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/99.Redis\351\235\242\350\257\225.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/01.Redis/Redis_\351\235\242\350\257\225.md" @@ -3,17 +3,16 @@ icon: logos:redis title: Redis 面试 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202309231131433.png date: 2020-07-13 17:03:42 -order: 99 categories: - 数据库 - KV数据库 - Redis tags: - 数据库 - - KV数据库 + - KV 数据库 - Redis - 面试 -permalink: /pages/451b73/ +permalink: /pages/4431bbfc/ --- # Redis 面试 @@ -22,43 +21,46 @@ permalink: /pages/451b73/ ### 什么是 Redis -【问题】 +**典型问题** + +(1)什么是 Redis? -- 什么是 Redis? -- Redis 有什么功能和特性? +(2)Redis 有什么功能和特性? -【解答】 +**知识点** -什么是 Redis: +(1)什么是 Redis? -**Redis 是一个开源的“内存”数据库**。由于,Redis 的读写操作都是在内存中完成,因此其**读写速度非常快**。 +**Redis 是一个开源的、数据存于内存中的 K-V 数据库**。由于,Redis 的读写操作都是在内存中完成,因此其**读写速度非常快**。 - **高性能** - 由于,Redis 的读写操作都是在内存中完成,因此性能极高。 - **高并发** - Redis 单机 QPS 能达到 10w+,将近是 Mysql 的 10 倍。 Redis 常被用于**缓存,消息队列、分布式锁等场景**。 +(2)Redis 有什么功能和特性? + Redis 的功能和特性: -- **Redis 支持多种数据类型**。如:String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理空间)、Stream(流)。 -- **Redis 的读写采用“单线程”模型**,因此,其操作天然就具有**原子性**。 +- **Redis 支持多种数据类型**。如:String(字符串)、Hash(哈希)、 List (列表)、Set(集合)、Zset(有序集合)、Bitmaps(位图)、HyperLogLog(基数统计)、GEO(地理空间)、Stream(流)。 +- **Redis 的读写采用“单线程”模型**,因此,其操作天然就具有**原子性**。需要注意的是,Redis 6.0 后在其网络模块中引入了多线程 I/O 机制。 - Redis 支持两种持久化策略:RDB 和 AOF。 - Redis 有多种高可用方案:**主从复制**模式、**哨兵**模式、**集群**模式。 - Redis 支持很多丰富的特性,如:**事务** 、**Lua 脚本**、**发布订阅**、**过期删除**、**内存淘汰**等等。 -![](https://architecturenotes.co/content/images/size/w2400/2022/08/Redis-v2-01-1.jpg) +![](https://substackcdn.com/image/fetch/f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F778a7e21-455b-45f6-8487-63f9eb41e88b_2000x1414.jpeg) -图来自 https://architecturenotes.co/redis/ +_图来自 [Redis Explained](https://architecturenotes.co/p/redis)_ ### Redis 有哪些应用场景 -【问题】 +**典型问题** - Redis 有哪些应用场景? -【解答】 +**知识点** - **缓存** - 将热点数据放到内存中,设置内存的最大使用量以及过期淘汰策略来保证缓存的命中率。 - **计数器** - Redis 这种内存数据库能支持计数器频繁的读写操作。 @@ -70,19 +72,94 @@ Redis 的功能和特性: - **分布式 Session** - 多个应用服务器的 Session 都存储到 Redis 中来保证 Session 的一致性。 - **分布式锁** - 除了可以使用 SETNX 实现分布式锁之外,还可以使用官方提供的 RedLock 分布式锁实现。 +### Redis 为什么快 + +**典型问题** + +- Redis 有多快? +- Redis 为什么这么快? + +**知识点** + +根据 [Redis 官方 Benchmark](https://redis.io/docs/management/optimization/benchmarks/) 文档的描述,Redis 单机 QPS 能达到 10w+,将近是 Mysql 的 10 倍。 + +![Redis 官方 Benchmark QPS 图](https://redis.io/docs/management/optimization/benchmarks/Connections_chart.png) + +Redis 是单线程模型(Redis 6.0 已经支持多线程模型),为什么还能有这么高的并发? + +- **Redis 读写基于内存** +- **IO 多路复用** + **读写单线程模型** + - IO 多路复用是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 + - 单线程模型避免了由于并发而产生的线程切换、锁竞争等开销。 + - 由于,Redis 读写基于内存,性能很高,所以 CPU 并不是制约 Redis 性能表现的瓶颈所在。更多情况下是受到内存大小和网络 I/O 的限制,所以 Redis 核心网络模型使用单线程并没有什么问题。 +- **高效的数据结构** + +![](https://pbs.twimg.com/media/FoYNzdcacAAMjy5?format=jpg&name=4096x4096) + +图来自 [Why is redis so fast?](https://blog.bytebytego.com/p/why-is-redis-so-fast) + +> 扩展阅读: +> +> [【视频】Why is Redis so FAST](https://www.youtube.com/shorts/x8lcdDbKZto) + +### Redis 模块 + +**经典问题** + +什么是 Redis 模块? + +Redis 模块有什么用? + +**经典问题** + +Redis 从 4.0 版本开始,支持通过 Module 来扩展其功能以满足特殊的需求。这些 Module 以动态链接库(so 文件)的形式被加载到 Redis 中,这是一种非常灵活的动态扩展功能的实现方式,值得借鉴学习! + +我们每个人都可以基于 Redis 去定制化开发自己的 Module,比如实现搜索引擎功能、自定义分布式锁和分布式限流。 + +目前,被 Redis 官方推荐的 Module 有: + +- [RediSearch](https://github.com/RediSearch/RediSearch):用于实现搜索引擎的模块。 +- [RedisJSON](https://github.com/RedisJSON/RedisJSON):用于处理 JSON 数据的模块。 +- [RedisGraph](https://github.com/RedisGraph/RedisGraph):用于实现图形数据库的模块。 +- [RedisTimeSeries](https://github.com/RedisTimeSeries/RedisTimeSeries):用于处理时间序列数据的模块。 +- [RedisBloom](https://github.com/RedisBloom/RedisBloom):用于实现布隆过滤器的模块。 +- [RedisAI](https://github.com/RedisAI/RedisAI):用于执行深度学习/机器学习模型并管理其数据的模块。 +- [RedisCell](https://github.com/brandur/redis-cell):用于实现分布式限流的模块。 + +关于 Redis 模块的详细介绍,可以查看官方文档:https://redis.io/modules。 + +### Redis 发展 + +**经典问题** + +- Redis 架构经历了哪些变化? + +**知识点** + +![](https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fac67c4db-99e0-4d6c-a7e4-163d962141ea_1280x1664.gif) + +- Redis 1.0(2010 年) - Redis 1.0 发布,采用单机架构,一般作为业务应用的缓存。但是 Redis 的数据是存在内存中的,重启 Redis 时,数据会全部丢失,流量直接打到数据库。 +- Redis 2.8(2013 年) + - 持久化 - Redis 引入了 RDB 内存快照来持久化数据。它还支持 AOF(仅追加文件),其中每个写入命令都写入 AOF 文件。 + - 复制 - 添加了复制功能以提高可用性。主实例处理实时读写请求,而副本同步主实例的数据。 + - 哨兵 - 引入了 Sentinel 来实时监控 Redis 实例。Sentinel 是一个旨在帮助管理 Redis 实例的系统。它执行以下四个任务:监控、通知、自动故障转移和共享配置。 +- Redis 3.0(2015 年) - 官方提供了 redis-cluster。redis-cluster 是一种分布式数据库解决方案,通过分片管理数据。数据被分成 16384 个槽,每个节点负责槽的一部分。 +- Redis 5.0(2017 年) - 新增 Stream 数据类型。 +- Redis 6.0(2020 年) - 在网络模块中引入了多线程 I/O。Redis 模型分为网络模块和主处理模块。特别注意:Redis 不再完全是单线程架构。 + ### Redis vs. Memcached -【问题】 +**典型问题** - Redis 和 Memcached 有什么相同点? - Redis 和 Memcached 有什么差异? - 分布式缓存技术选型,选 Redis 还是 Memcached,为什么? -【解答】 +**知识点** Redis 与 Memcached 的**共性**: -1. 都是内存数据库,因此性能都很高 +1. 都是内存数据库,因此性能都很高。 2. 都有过期策略。 因为以上两点,所以常被作为缓存使用。 @@ -99,63 +176,38 @@ Redis 与 Memcached 的**差异**: 通过以上分析,可以看出,Redis 在很多方面都占有优势。因此,绝大多数情况下,优先选择 Redis 作为分布式缓存。 -> 参考:[《脚踏两只船的困惑 - Memcached 与 Redis》](www.imooc.com/article/23549) - -### Redis 为什么快 - -【问题】 - -- Redis 有多快? -- Redis 为什么这么快? - -【解答】 - -根据 [Redis 官方 Benchmark](https://redis.io/docs/management/optimization/benchmarks/) 文档的描述,Redis 单机 QPS 能达到 10w+。 - -![Redis 官方 Benchmark QPS 图](https://redis.io/docs/management/optimization/benchmarks/Connections_chart.png) - -Redis 是单线程模型(Redis 6.0 已经支持多线程模型),为什么还能有这么高的并发? - -- **Redis 读写基于内存** -- **IO 多路复用** + **读写单线程模型** - - IO 多路复用是利用 select、poll、epoll 可以同时监察多个流的 I/O 事件的能力,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有 I/O 事件时,就从阻塞态中唤醒,于是程序就会轮询一遍所有的流(epoll 是只轮询那些真正发出了事件的流),并且只依次顺序的处理就绪的流,这种做法就避免了大量的无用操作。 - - 单线程模型避免了由于并发而产生的线程切换、锁竞争等开销。 - - 由于,Redis 读写基于内存,性能很高,所以 CPU 并不是制约 Redis 性能表现的瓶颈所在。更多情况下是受到内存大小和网络 I/O 的限制,所以 Redis 核心网络模型使用单线程并没有什么问题。 -- **高效的数据结构** - -![](https://pbs.twimg.com/media/FoYNzdcacAAMjy5?format=jpg&name=4096x4096) - -图来自 [Why is redis so fast?](https://blog.bytebytego.com/p/why-is-redis-so-fast) +> 参考:[《脚踏两只船的困惑 - Memcached 与 Redis》](https://www.imooc.com/article/23549) ## Redis 数据类型 ### Redis 支持哪些数据类型 -【问题】 +**典型问题** - Redis 支持哪些数据类型? -【解答】 +**知识点** -- Redis 支持五种基本数据类型:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。 +- Redis 支持五种基本数据类型:String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)。 - 随着 Redis 版本升级,又陆续支持以下数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)。 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202309232155082.png) +> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) + ### Redis 各数据类型的应用场景 -【问题】 +**典型问题** Redis 各数据类型有哪些应用场景? -【解答】 +**知识点** - **String(字符串)** - 缓存对象、分布式 Session、分布式锁、计数器、限流器、分布式 ID 等。 - **Hash(哈希)** - 缓存对象、购物车等。 - **List(列表)** - 消息队列 - **Set(集合)** - 聚合计算(并集、交集、差集),如点赞、共同关注、抽奖活动等。 - **Zset(有序集合)** - 排序场景,如排行榜、电话和姓名排序等。 - - **BitMap**(2.2 版新增) - 二值状态统计的场景,比如签到、判断用户登陆状态、连续签到用户总数等; - **HyperLogLog**(2.8 版新增) - 海量数据基数统计的场景,比如百万级网页 UV 计数等; - **GEO**(3.2 版新增) - 存储地理位置信息的场景,比如滴滴叫车; @@ -184,15 +236,16 @@ Redis 各数据类型有哪些应用场景? - 有序集合保存的元素数量小于 `128` 个; - 有序集合保存的所有元素成员的长度都小于 `64` 字节; -## Redis 过期删除和内存淘汰 +## Redis 内存管理 ### Redis 过期删除策略 -【问题】 +**典型问题** - Redis 的过期删除策略是什么? +- 常见的过期策略有哪些,Redis 的选择考量是什么? -【解答】 +**知识点** Redis 采用的过期策略是:**定期删除+惰性删除**。 @@ -228,13 +281,13 @@ AOF 持久化 ### Redis 内存淘汰策略 -【问题】 +**典型问题** - Redis 内存不足时,怎么办? - Redis 有哪些内存淘汰策略? - 如何选择内存淘汰策略? -【解答】 +**知识点** (1)Redis 内存淘汰要点 @@ -245,23 +298,16 @@ AOF 持久化 (2)Redis 内存淘汰策略 - **不淘汰** - - **`noeviction`** - 当内存使用达到阈值的时候,所有引起申请内存的命令会报错。这是 Redis 默认的策略。 - - **在过期键中进行淘汰** - - **`volatile-random`** - 在设置了过期时间的键空间中,随机移除某个 key。 - - **`volatile-ttl`** - 在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除。 - - **`volatile-lru`** - 在设置了过期时间的键空间中,优先移除最近未使用的 key。 - - **`volatile-lfu`** (Redis 4.0 新增)- 淘汰所有设置了过期时间的键值中,最少使用的键值。 - - **在所有键中进行淘汰** - **`allkeys-lru`** - 在主键空间中,优先移除最近未使用的 key。 - **`allkeys-random`** - 在主键空间中,随机移除某个 key。 - - **`allkeys-lfu`** (Redis 4.0 新增) - 淘汰整个键值中最少使用的键值。 + - **`allkeys-lfu`** (Redis 4.0 新增) - 淘汰整个键值中最少使用的键值。 (3)如何选择内存淘汰策略 @@ -274,12 +320,12 @@ AOF 持久化 ### Redis 如何保证数据不丢失 -【问题】 +**典型问题** - Redis 如何保证数据不丢失? - Redis 有几种持久化方式? -【解答】 +**知识点** 为了追求性能,Redis 的读写都是在内存中完成的。一旦重启,内存中的数据就会清空,为了保证数据不丢失,Redis 支持持久化机制。 @@ -289,14 +335,37 @@ Redis 有三种持久化方式 - AOF 日志 - 混合持久化 +### RDB 的实现原理 + +**典型问题** + +- RDB 的实现原理是什么? +- 生成 RDB 快照时,Redis 可以响应请求吗? + +**知识点** + +有两个 Redis 命令可以用于生成 RDB 文件:[**`SAVE`**](https://redis.io/commands/save) 和 [**`BGSAVE`**](https://redis.io/commands/bgsave) 。 + +[**`SAVE`**](https://redis.io/commands/save) 命令由服务器进程直接执行保存操作,直到 RDB 创建完成为止。所以**该命令“会阻塞”服务器**,在阻塞期间,服务器不能响应任何命令请求。 + +[**`BGSAVE`**](https://redis.io/commands/bgsave) 命令会**“派生”**(fork)一个子进程,由子进程负责创建 RDB 文件,服务器进程继续处理命令请求,所以**该命令“不会阻塞”服务器**。 + +![BGSAVE 流程](https://raw.githubusercontent.com/dunwu/images/master/snap/202309172009198.png) + +> 🔔 **【注意】** +> +> `BGSAVE` 命令的实现采用的是写时复制技术(Copy-On-Write,缩写为 CoW)。 +> +> `BGSAVE` 命令执行期间,`SAVE`、`BGSAVE`、`BGREWRITEAOF` 三个命令会被拒绝,以免与当前的 `BGSAVE` 操作产生竞态条件,降低性能。 + ### AOF 的实现原理 -【问题】 +**典型问题** - AOF 的实现原理是什么? - 为什么先执行命令,再把数据写入日志呢? -【解答】 +**知识点** **Redis 命令请求会先保存到 AOF 缓冲区,再定期写入并同步到 AOF 文件**。 @@ -331,13 +400,13 @@ AOF 的实现可以分为命令追加(append)、文件写入、文件同步 ### AOF 重写机制 -【问题】 +**典型问题** - AOF 日志过大时,怎么办? - AOF 重写流程是怎样的? - AOF 重写时,可以处理请求吗? -【解答】 +**知识点** 当 AOF 日志过大时,恢复过程就会很久。为了避免此问题,Redis 提供了 AOF 重写机制,即 AOF 日志大小超过所设阈值后,启动 AOF 重写,压缩 AOF 文件。 @@ -351,19 +420,6 @@ AOF 重写机制是,读取当前数据库中的所有键值对,然后将每 ![BGREWRITEAOF 流程](https://raw.githubusercontent.com/dunwu/images/master/snap/202309171957918.png) -### RDB 的实现原理 - -【问题】 - -- RDB 的实现原理是什么? -- 生成 RDB 快照时,Redis 可以响应请求吗? - -【解答】 - -[**`BGSAVE`**](https://redis.io/commands/bgsave) 命令会**“派生”**(fork)一个子进程,由子进程负责创建 RDB 文件,服务器进程继续处理命令请求,所以**该命令“不会阻塞”服务器**。 - -![BGSAVE 流程](https://raw.githubusercontent.com/dunwu/images/master/snap/202309172009198.png) - ### 为什么会有混合持久化? RDB 优点是数据恢复速度快,但是快照的频率不好把握。频率太低,丢失的数据就会比较多,频率太高,就会影响性能。 @@ -391,19 +447,19 @@ AOF 优点是丢失数据少,但是数据恢复不快。 ## Redis 高可用 -【问题】 +**典型问题** Redis 如何保证高可用? ### Redis 主从复制 -【问题】 +**典型问题** - Redis 复制的工作原理?Redis 旧版复制和新版复制有何不同? - Redis 主从节点间如何复制数据? - Redis 的数据一致性是强一致性吗? -【解答】 +**知识点** (1)旧版复制基于 `SYNC` 命令实现。分为同步(sync)和命令传播(command propagate)两个操作。这种方式存在缺陷:不能高效处理断线重连后的复制情况。 @@ -429,14 +485,14 @@ Redis 如何保证高可用? ### Redis 哨兵 -【问题】 +**典型问题** - Redis 哨兵的功能? - Redis 哨兵的原理? - Redis 哨兵如何选举 Leader? - Redis 如何实现故障转移? -【解答】 +**知识点** (1)Redis 主从复制模式无法自动故障转移,也就是说,一旦主服务器宕机,需要手动恢复。为了解决此问题,Redis 增加了哨兵模式(Sentinel)。 @@ -516,6 +572,7 @@ Redis 并非真的只有单线程。 - Redis 的主要工作包括接收客户端请求、解析请求和进行数据读写等操作,是由单线程来执行的,这也是常说 Redis 是单线程程序的原因。 - Redis 还启动了 3 个线程来执行**文件关闭**、**AOF 同步写**和**惰性删除**等操作。 +- 此外,Redis 6.0 版本之后引入了多线程来处理网络请求(提高网络 IO 读写性能)。 ### Redis 单线程模式是怎样的? @@ -539,42 +596,203 @@ Redis 官方表示,**Redis 6.0 版本引入的多线程 I/O 特性对性能提 ## Redis 事务 -【问题】 +### Redis 事务功能 + +**典型问题** -- Redis 的并发竞争问题是什么?如何解决这个问题? - Redis 支持事务吗? -- Redis 事务是严格意义的事务吗?Redis 为什么不支持回滚。 - Redis 事务如何工作? -- 了解 Redis 事务中的 CAS 行为吗? -【解答】 +**知识点** + +Redis 支持事务。[`MULTI`](https://redis.io/commands/multi)、[`EXEC`](https://redis.io/commands/exec)、[`DISCARD`](https://redis.io/commands/discard) 和 [`WATCH`](https://redis.io/commands/watch) 是 Redis 事务相关的命令。 + +**[`MULTI`](https://redis.io/commands/multi) 命令用于开启一个事务,它总是返回 OK 。**`MULTI` 执行之后, 客户端可以继续向服务器发送任意多条命令, 这些命令不会立即被执行, 而是被放到一个队列中, 当 EXEC 命令被调用时, 所有队列中的命令才会被执行。 -**Redis 提供的不是严格的事务,Redis 只保证串行执行命令,并且能保证全部执行,但是执行命令失败时并不会回滚,而是会继续执行下去**。 +**[`EXEC`](https://redis.io/commands/exec) 命令负责触发并执行事务中的所有命令。** + +- 如果客户端在使用 `MULTI` 开启了一个事务之后,却因为断线而没有成功执行 `EXEC` ,那么事务中的所有命令都不会被执行。 +- 另一方面,如果客户端成功在开启事务之后执行 `EXEC` ,那么事务中的所有命令都会被执行。 + +**当执行 [`DISCARD`](https://redis.io/commands/discard) 命令时, 事务会被放弃, 事务队列会被清空, 并且客户端会从事务状态中退出。** + +**[`WATCH`](https://redis.io/commands/watch) 命令可以为 Redis 事务提供 check-and-set (CAS)行为。**被 `WATCH` 的键会被监视,并会发觉这些键是否被改动过了。 如果有至少一个被监视的键在 `EXEC` 执行之前被修改了, 那么整个事务都会被取消, `EXEC` 返回 `nil-reply` 来表示事务已经失败。 + +`WATCH` 可以用于创建 Redis 没有内置的原子操作。 + +举个例子,以下代码实现了原创的 `ZPOP` 命令,它可以原子地弹出有序集合中分值(`score`)最小的元素: + +```shell +WATCH zset +element = ZRANGE zset 0 0 +MULTI +ZREM zset element +EXEC +``` -Redis 不支持回滚的理由: +### Redis 事务的不足 + +**典型问题** + +Redis 事务是严格意义的事务吗? + +Redis 事务为什么不支持回滚? + +**知识点** + +ACID 是数据库事务正确执行的四个基本要素。 + +- **原子性(Atomicity)** + - 事务被视为不可分割的最小单元,事务中的所有操作**要么全部提交成功,要么全部失败回滚**。 + - 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 +- **一致性(Consistency)** + - 数据库在事务执行前后都保持一致性状态。 + - 在一致性状态下,所有事务对一个数据的读取结果都是相同的。 +- **隔离性(Isolation)** + - 一个事务所做的修改在最终提交以前,对其它事务是不可见的。 +- **持久性(Durability)** + - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 + - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 + +**一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。** + +![ACID](https://raw.githubusercontent.com/dunwu/images/master/cs/database/RDB/数据库ACID.png) + +**Redis 仅支持“非严格”的事务**。所谓“非严格”是指: + +- **Redis 事务保证全部执行命令** - Redis 事务中的多个命令会被打包到事务队列中,然后按先进先出(FIFO)的顺序执行。事务在执行过程中不会被中断,当事务队列中的所有命令都被执行完毕之后,事务才会结束。 +- **Redis 事务不支持回滚** - 如果命令执行失败不会回滚,而是会继续执行下去。 + +Redis 官方的[事务特性文档](https://redis.io/docs/interact/transactions/)给出的不支持回滚的理由是: - Redis 命令只会因为错误的语法而失败,或是命令用在了错误类型的键上面。 - 因为不需要对回滚进行支持,所以 Redis 的内部可以保持简单且快速。 -`MULTI` 、 `EXEC` 、 `DISCARD` 和 `WATCH` 是 Redis 事务相关的命令。 +### Redis 事务的替代 + +Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。 + +一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。 + +不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, **严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。** + +如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。 -Redis 有天然解决这个并发竞争问题的类 CAS 乐观锁方案:每次要**写之前,先判断**一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。如果是的话,那么可以写,否则,就不能用旧的数据覆盖新的数据。 +另外,Redis 7.0 新增了 [Redis functions](https://redis.io/docs/manual/programmability/functions-intro/) 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。 ## Redis 管道 -【问题】 +**典型问题** - 除了事务,还有其他批量执行 Redis 命令的方式吗? -【解答】 +**知识点** Redis 是一种基于 C/S 模型以及请求/响应协议的 TCP 服务。Redis 支持管道技术。管道技术允许请求以异步方式发送,即旧请求的应答还未返回的情况下,允许发送新请求。这种方式可以大大提高传输效率。使用管道发送命令时,Redis Server 会将部分请求放到缓存队列中(占用内存),执行完毕后一次性发送结果。如果需要发送大量的命令,会占用大量的内存,因此应该按照合理数量分批次的处理。 +## Redis 慢查询 + +### 为什么会有慢查询命令? + +我们知道一个 Redis 命令的执行可以简化为以下 4 步: + +1. 发送命令 +2. 命令排队 +3. 命令执行 +4. 返回结果 + +Redis 慢查询统计的是命令执行这一步骤的耗时,慢查询命令也就是那些命令执行时间较长的命令。 + +Redis 为什么会有慢查询命令呢? + +Redis 中的大部分命令都是 O(1)时间复杂度,但也有少部分 O(n) 时间复杂度的命令,例如: + +- `KEYS *`:会返回所有符合规则的 key。 +- `HGETALL`:会返回一个 Hash 中所有的键值对。 +- `LRANGE`:会返回 List 中指定范围内的元素。 +- `SMEMBERS`:返回 Set 中的所有元素。 +- `SINTER`/`SUNION`/`SDIFF`:计算多个 Set 的交集/并集/差集。 +- …… + +由于这些命令时间复杂度是 O(n),有时候也会全表扫描,随着 n 的增大,执行耗时也会越长。不过, 这些命令并不是一定不能使用,但是需要明确 N 的值。另外,有遍历的需求可以使用 `HSCAN`、`SSCAN`、`ZSCAN` 代替。 + +除了这些 O(n)时间复杂度的命令可能会导致慢查询之外, 还有一些时间复杂度可能在 O(N) 以上的命令,例如: + +- `ZRANGE`/`ZREVRANGE`:返回指定 Sorted Set 中指定排名范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 为返回的元素数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 +- `ZREMRANGEBYRANK`/`ZREMRANGEBYSCORE`:移除 Sorted Set 中指定排名范围/指定 score 范围内的所有元素。时间复杂度为 O(log(n)+m),n 为所有元素的数量, m 被删除元素的数量,当 m 和 n 相当大时,O(n) 的时间复杂度更小。 +- …… + +### 如何找到慢查询命令? + +在 `redis.conf` 文件中,我们可以使用 `slowlog-log-slower-than` 参数设置耗时命令的阈值,并使用 `slowlog-max-len` 参数设置耗时命令的最大记录条数。 + +当 Redis 服务器检测到执行时间超过 `slowlog-log-slower-than`阈值的命令时,就会将该命令记录在慢查询日志(slow log) 中,这点和 MySQL 记录慢查询语句类似。当慢查询日志超过设定的最大记录条数之后,Redis 会把最早的执行命令依次舍弃。 + +⚠️注意:由于慢查询日志会占用一定内存空间,如果设置最大记录条数过大,可能会导致内存占用过高的问题。 + +`slowlog-log-slower-than`和`slowlog-max-len`的默认配置如下(可以自行修改): + +``` +# The following time is expressed in microseconds, so 1000000 is equivalent +# to one second. Note that a negative number disables the slow log, while +# a value of zero forces the logging of every command. +slowlog-log-slower-than 10000 + +# There is no limit to this length. Just be aware that it will consume memory. +# You can reclaim memory used by the slow log with SLOWLOG RESET. +slowlog-max-len 128 +``` + +除了修改配置文件之外,你也可以直接通过 `CONFIG` 命令直接设置: + +``` +# 命令执行耗时超过 10000 微妙(即10毫秒)就会被记录 +CONFIG SET slowlog-log-slower-than 10000 +# 只保留最近 128 条耗时命令 +CONFIG SET slowlog-max-len 128 +``` + +获取慢查询日志的内容很简单,直接使用`SLOWLOG GET` 命令即可。 + +``` +127.0.0.1:6379> SLOWLOG GET #慢日志查询 + 1) 1) (integer) 5 + 2) (integer) 1684326682 + 3) (integer) 12000 + 4) 1) "KEYS" + 2) "*" + 5) "172.17.0.1:61152" + 6) "" + // ... +``` + +慢查询日志中的每个条目都由以下六个值组成: + +1. 唯一渐进的日志标识符。 +2. 处理记录命令的 Unix 时间戳。 +3. 执行所需的时间量,以微秒为单位。 +4. 组成命令参数的数组。 +5. 客户端 IP 地址和端口。 +6. 客户端名称。 + +`SLOWLOG GET` 命令默认返回最近 10 条的的慢查询命令,你也自己可以指定返回的慢查询命令的数量 `SLOWLOG GET N`。 + +下面是其他比较常用的慢查询相关的命令: + +``` +# 返回慢查询命令的数量 +127.0.0.1:6379> SLOWLOG LEN +(integer) 128 +# 清空慢查询命令 +127.0.0.1:6379> SLOWLOG RESET +OK +``` + ## Redis 应用 -### 缓存设计 +### 缓存 -【问题】 +**典型问题** 如何避免缓存雪崩、缓存击穿、缓存穿透? @@ -590,6 +808,194 @@ LFU 算法的原理是什么 ### 分布式锁 +### 消息队列 + +**经典问题** + +Redis 可以做消息队列吗? + +Redis 有哪些实现消息队列的方式? + +**知识点** + +先说结论:**可以是可以,但不建议使用 Redis 来做消息队列。和专业的消息队列相比,还是有很多欠缺的地方。** + +**Redis 2.0 之前,如果想要使用 Redis 来做消息队列的话,只能通过 List 来实现。** + +通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`即可实现简易版消息队列: + +```shell +# 生产者生产消息 +> RPUSH myList msg1 msg2 +(integer) 2 +> RPUSH myList msg3 +(integer) 3 +# 消费者消费消息 +> LPOP myList +"msg1" +``` + +不过,通过 `RPUSH/LPOP` 或者 `LPUSH/RPOP`这样的方式存在性能问题,我们需要不断轮询去调用 `RPOP` 或 `LPOP` 来消费消息。当 List 为空时,大部分的轮询的请求都是无效请求,这种方式大量浪费了系统资源。 + +因此,Redis 还提供了 `BLPOP`、`BRPOP` 这种阻塞式读取的命令(带 B-Blocking 的都是阻塞式),并且还支持一个超时参数。如果 List 为空,Redis 服务端不会立刻返回结果,它会等待 List 中有新数据后再返回或者是等待最多一个超时时间后返回空。如果将超时时间设置为 0 时,即可无限等待,直到弹出消息 + +```shell +# 超时时间为 10s +# 如果有数据立刻返回,否则最多等待10秒 +> BRPOP myList 10 +null +``` + +**List 实现消息队列功能太简单,像消息确认机制等功能还需要我们自己实现,最要命的是没有广播机制,消息也只能被消费一次。** + +**Redis 2.0 引入了发布订阅 (pub/sub) 功能,解决了 List 实现消息队列没有广播机制的问题。** + +![Redis 发布订阅 (pub/sub) 功能](https://oss.javaguide.cn/github/javaguide/database/redis/redis-pub-sub.png) + +Redis 发布订阅 (pub/sub) 功能 + +pub/sub 中引入了一个概念叫 **channel(频道)**,发布订阅机制的实现就是基于这个 channel 来做的。 + +pub/sub 涉及发布者(Publisher)和订阅者(Subscriber,也叫消费者)两个角色: + +- 发布者通过 `PUBLISH` 投递消息给指定 channel。 +- 订阅者通过`SUBSCRIBE`订阅它关心的 channel。并且,订阅者可以订阅一个或者多个 channel。 + +我们这里启动 3 个 Redis 客户端来简单演示一下: + +pub/sub 既能单播又能广播,还支持 channel 的简单正则匹配。不过,消息丢失(客户端断开连接或者 Redis 宕机都会导致消息丢失)、消息堆积(发布者发布消息的时候不会管消费者的具体消费能力如何)等问题依然没有一个比较好的解决办法。 + +为此,Redis 5.0 新增加的一个数据结构 `Stream` 来做消息队列。`Stream` 支持: + +- 发布 / 订阅模式 +- 按照消费者组进行消费(借鉴了 Kafka 消费者组的概念) +- 消息持久化( RDB 和 AOF) +- ACK 机制(通过确认机制来告知已经成功处理了消息) +- 阻塞式获取消息 + +`Stream` 的结构如下: + +![img](https://oss.javaguide.cn/github/javaguide/database/redis/redis-stream-structure.png) + +这是一个有序的消息链表,每个消息都有一个唯一的 ID 和对应的内容。ID 是一个时间戳和序列号的组合,用来保证消息的唯一性和递增性。内容是一个或多个键值对(类似 Hash 基本数据类型),用来存储消息的数据。 + +这里再对图中涉及到的一些概念,进行简单解释: + +- `Consumer Group`:消费者组用于组织和管理多个消费者。消费者组本身不处理消息,而是再将消息分发给消费者,由消费者进行真正的消费 +- `last_delivered_id`:标识消费者组当前消费位置的游标,消费者组中任意一个消费者读取了消息都会使 last_delivered_id 往前移动。 +- `pending_ids`:记录已经被客户端消费但没有 ack 的消息的 ID。 + +`Stream` 使用起来相对要麻烦一些,这里就不演示了。 + +总的来说,`Stream` 已经可以满足一个消息队列的基本要求了。不过,`Stream` 在实际使用中依然会有一些小问题不太好解决比如在 Redis 发生故障恢复后不能保证消息至少被消费一次。 + +综上,和专业的消息队列相比,使用 Redis 来实现消息队列还是有很多欠缺的地方比如消息丢失和堆积问题不好解决。因此,我们通常建议不要使用 Redis 来做消息队列,你完全可以选择市面上比较成熟的一些消息队列比如 RocketMQ、Kafka。不过,如果你就是想要用 Redis 来做消息队列的话,那我建议你优先考虑 `Stream`,这是目前相对最优的 Redis 消息队列实现。 + +相关阅读:[Redis 消息队列发展历程 - 阿里开发者 - 2022](https://mp.weixin.qq.com/s/gCUT5TcCQRAxYkTJfTRjJw) + +### 延时任务 + +**经典问题** + +如何基于 Redis 实现延时任务? + +**知识点** + +基于 Redis 实现延时任务的功能无非就下面两种方案: + +1. Redis 过期事件监听 +2. Redisson 内置的延时队列 + +Redis 过期事件监听的存在时效性较差、丢消息、多服务实例下消息重复消费等问题,不被推荐使用。 + +Redisson 内置的延时队列具备下面这些优势: + +1. **减少了丢消息的可能**:DelayedQueue 中的消息会被持久化,即使 Redis 宕机了,根据持久化机制,也只可能丢失一点消息,影响不大。当然了,你也可以使用扫描数据库的方法作为补偿机制。 +2. **消息不存在重复消费问题**:每个客户端都是从同一个目标队列中获取任务的,不存在重复消费的问题。 + +### 批处理 + +一个 Redis 命令的执行可以简化为以下 4 步: + +1. 发送命令 +2. 命令排队 +3. 命令执行 +4. 返回结果 + +其中,第 1 步和第 4 步耗费时间之和称为 **Round Trip Time (RTT,往返时间)** ,也就是数据在网络上传输的时间。 + +使用批量操作可以减少网络传输次数,进而有效减小网络开销,大幅减少 RTT。 + +另外,除了能减少 RTT 之外,发送一次命令的 socket I/O 成本也比较高(涉及上下文切换,存在`read()`和`write()`系统调用),批量操作还可以减少 socket I/O 成本。这个在官方对 pipeline 的介绍中有提到:https://redis.io/docs/manual/pipelining/ 。 + +#### [原生批量操作命令](#原生批量操作命令) + +Redis 中有一些原生支持批量操作的命令,比如: + +- `MGET`(获取一个或多个指定 key 的值)、`MSET`(设置一个或多个指定 key 的值)、 +- `HMGET`(获取指定哈希表中一个或者多个指定字段的值)、`HMSET`(同时将一个或多个 field-value 对设置到指定哈希表中)、 +- `SADD`(向指定集合添加一个或多个元素) +- …… + +不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 `MGET` 无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上,`MGET`可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。 + +整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现): + +1. 找到 key 对应的所有 hash slot; +2. 分别向对应的 Redis 节点发起 `MGET` 请求获取数据; +3. 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。 + +如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。 + +#### [原生批量操作命令](#原生批量操作命令) + +Redis 中有一些原生支持批量操作的命令,比如: + +- `MGET`(获取一个或多个指定 key 的值)、`MSET`(设置一个或多个指定 key 的值)、 +- `HMGET`(获取指定哈希表中一个或者多个指定字段的值)、`HMSET`(同时将一个或多个 field-value 对设置到指定哈希表中)、 +- `SADD`(向指定集合添加一个或多个元素) +- …… + +不过,在 Redis 官方提供的分片集群解决方案 Redis Cluster 下,使用这些原生批量操作命令可能会存在一些小问题需要解决。就比如说 `MGET` 无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上,`MGET`可能还是需要多次网络传输,原子操作也无法保证了。不过,相较于非批量操作,还是可以节省不少网络传输次数。 + +整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现): + +1. 找到 key 对应的所有 hash slot; +2. 分别向对应的 Redis 节点发起 `MGET` 请求获取数据; +3. 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。 + +如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升。 + +#### Pipeline + +对于不支持批量操作的命令,我们可以利用 **pipeline(流水线)** 将一批 Redis 命令封装成一组,这些 Redis 命令会被一次性提交到 Redis 服务器,只需要一次网络传输。不过,需要注意控制一次批量操作的 **元素个数**(例如 500 以内,实际也和元素字节数有关),避免网络传输的数据量过大。 + +与`MGET`、`MSET`等原生批量操作命令一样,pipeline 同样在 Redis Cluster 上使用会存在一些小问题。原因类似,无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上。如果想要使用的话,客户端需要自己维护 key 与 slot 的关系。 + +原生批量操作命令和 pipeline 的是有区别的,使用的时候需要注意: + +- 原生批量操作命令是原子操作,pipeline 是非原子操作。 +- pipeline 可以打包不同的命令,原生批量操作命令不可以。 +- 原生批量操作命令是 Redis 服务端支持实现的,而 pipeline 需要服务端和客户端的共同实现。 + +顺带补充一下 pipeline 和 Redis 事务的对比: + +- 事务是原子操作,pipeline 是非原子操作。两个不同的事务不会同时运行,而 pipeline 可以同时以交错方式执行。 +- Redis 事务中每个命令都需要发送到服务端,而 Pipeline 只需要发送一次,请求次数更少。 + +另外,pipeline 不适用于执行顺序有依赖关系的一批命令。就比如说,你需要将前一个命令的结果给后续的命令使用,pipeline 就没办法满足你的需求了。对于这种需求,我们可以使用 **Lua 脚本** 。 + +#### Lua 脚本 + +Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是 **原子操作** 。也就是说,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。 + +并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。 + +不过, Lua 脚本依然存在下面这些缺陷: + +- 如果 Lua 脚本运行时出错并中途结束,之后的操作不会进行,但是之前已经发生的写操作不会撤销,所以即使使用了 Lua 脚本,也不能实现类似数据库回滚的原子性。 +- Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 **hash slot**(哈希槽)上。 + ### 大 Key 处理 #### 什么是大 Key @@ -632,14 +1038,17 @@ redis-cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys **_2、使用 SCAN 命令查找大 key_** -使用 SCAN 命令对数据库扫描,然后用 TYPE 命令获取返回的每一个 key 的类型。 +`SCAN` 命令可以按照一定的模式和数量返回匹配的 key。获取了 key 之后,可以利用 `STRLEN`、`HLEN`、`LLEN`等命令返回其长度或成员数量。 -对于 String 类型,可以直接使用 STRLEN 命令获取字符串的长度,也就是占用的内存空间字节数。 +| 数据结构 | 命令 | 复杂度 | 结果(对应 key) | +| ---------- | ------ | ------ | ------------------ | +| String | STRLEN | O(1) | 字符串值的长度 | +| Hash | HLEN | O(1) | 哈希表中字段的数量 | +| List | LLEN | O(1) | 列表元素数量 | +| Set | SCARD | O(1) | 集合元素数量 | +| Sorted Set | ZCARD | O(1) | 有序集合的元素数量 | -对于集合类型来说,有两种方法可以获得它占用的内存大小: - -- 如果能够预先从业务层知道集合元素的平均大小,那么,可以使用下面的命令获取集合元素的个数,然后乘以集合元素的平均大小,这样就能获得集合占用的内存大小了。List 类型:`LLEN` 命令;Hash 类型:`HLEN` 命令;Set 类型:`SCARD` 命令;Sorted Set 类型:`ZCARD` 命令; -- 如果不能提前知道写入集合的元素大小,可以使用 `MEMORY USAGE` 命令(需要 Redis 4.0 及以上版本),查询一个键值对占用的内存空间。 +对于集合类型还可以使用 `MEMORY USAGE` 命令(Redis 4.0+),这个命令会返回键值对占用的内存空间。 **_3、使用 RdbTools 工具查找大 key_** @@ -651,97 +1060,86 @@ redis-cli -h 127.0.0.1 -p6379 -a "password" -- bigkeys rdb dump.rdb -c memory --bytes 10240 -f redis.csv ``` -#### 如何删除大 Key +#### 如何处理大 Key -如果大 Key 过大,删除时间过长,会阻塞 Redis 主线程,导致主线程无法及时响应其他请求。因此,删除大 Key 时需要考虑分批、异步处理。 +bigkey 的常见处理以及优化办法如下(这些方法可以配合起来使用): -**_1、分批次删除_** +- **分割 bigkey**:将一个 bigkey 分割为多个小 key。例如,将一个含有上万字段数量的 Hash 按照一定策略(比如二次哈希)拆分为多个 Hash。 +- **手动清理**:Redis 4.0+ 可以使用 `UNLINK` 命令来异步删除一个或多个指定的 key。Redis 4.0 以下可以考虑使用 `SCAN` 命令结合 `DEL` 命令来分批次删除。 +- **采用合适的数据结构**:例如,文件二进制数据不使用 String 保存、使用 HyperLogLog 统计页面 UV、Bitmap 保存状态信息(0/1)。 +- **开启 lazy-free(惰性删除/延迟释放)** :lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。 -对于**删除大 Hash**,使用 `hscan` 命令,每次获取 100 个字段,再用 `hdel` 命令,每次删除 1 个字段。 +### 热点 Key 处理 -Python 代码: +#### 什么是 hotkey? -```python -def del_large_hash(): - r = redis.StrictRedis(host='redis-host1', port=6379) - large_hash_key ="xxx" #要删除的大hash键名 - cursor = '0' - while cursor != 0: - # 使用 hscan 命令,每次获取 100 个字段 - cursor, data = r.hscan(large_hash_key, cursor=cursor, count=100) - for item in data.items(): - # 再用 hdel 命令,每次删除1个字段 - r.hdel(large_hash_key, item[0]) -``` +如果一个 key 的访问次数比较多且明显多于其他 key 的话,那这个 key 就可以看作是 **hotkey(热 Key)**。例如在 Redis 实例的每秒处理请求达到 5000 次,而其中某个 key 的每秒访问量就高达 2000 次,那这个 key 就可以看作是 hotkey。 + +hotkey 出现的原因主要是某个热点数据访问量暴增,如重大的热搜事件、参与秒杀的商品。 + +#### hotkey 有什么危害? + +处理 hotkey 会占用大量的 CPU 和带宽,可能会影响 Redis 实例对其他请求的正常处理。此外,如果突然访问 hotkey 的请求超出了 Redis 的处理能力,Redis 就会直接宕机。这种情况下,大量请求将落到后面的数据库上,可能会导致数据库崩溃。 + +因此,hotkey 很可能成为系统性能的瓶颈点,需要单独对其进行优化,以确保系统的高可用性和稳定性 -对于**删除大 List**,通过 `ltrim` 命令,每次删除少量元素。 +#### [如何发现 hotkey?](#如何发现-hotkey) -Python 代码: +**1、使用 Redis 自带的 `--hotkeys` 参数来查找。** + +Redis 4.0.3 版本中新增了 `hotkeys` 参数,该参数能够返回所有 key 的被访问次数。 + +使用该方案的前提条件是 Redis Server 的 `maxmemory-policy` 参数设置为 LFU 算法,不然就会出现如下所示的错误。 -```python -def del_large_list(): - r = redis.StrictRedis(host='redis-host1', port=6379) - large_list_key = 'xxx' #要删除的大list的键名 - while r.llen(large_list_key)>0: - #每次只删除最右100个元素 - r.ltrim(large_list_key, 0, -101) ``` +# redis-cli -p 6379 --hotkeys -对于**删除大 Set**,使用 `sscan` 命令,每次扫描集合中 100 个元素,再用 `srem` 命令每次删除一个键。 - -Python 代码: - -```python -def del_large_set(): - r = redis.StrictRedis(host='redis-host1', port=6379) - large_set_key = 'xxx' # 要删除的大set的键名 - cursor = '0' - while cursor != 0: - # 使用 sscan 命令,每次扫描集合中 100 个元素 - cursor, data = r.sscan(large_set_key, cursor=cursor, count=100) - for item in data: - # 再用 srem 命令每次删除一个键 - r.srem(large_size_key, item) +# Scanning the entire keyspace to find hot keys as well as +# average sizes per key type. You can use -i 0.1 to sleep 0.1 sec +# per 100 SCAN commands (not usually needed). + +Error: ERR An LFU maxmemory policy is not selected, access frequency not tracked. Please note that when switching between policies at runtime LRU and LFU data will take some time to adjust. ``` -对于**删除大 ZSet**,使用 `zremrangebyrank` 命令,每次删除 top 100 个元素。 +Redis 中有两种 LFU 算法: + +1. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最不经常使用的数据淘汰。 +2. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。 -Python 代码: +以下是配置文件 `redis.conf` 中的示例: -```python -def del_large_sortedset(): - r = redis.StrictRedis(host='large_sortedset_key', port=6379) - large_sortedset_key='xxx' - while r.zcard(large_sortedset_key)>0: - # 使用 zremrangebyrank 命令,每次删除 top 100个元素 - r.zremrangebyrank(large_sortedset_key,0,99) ``` +# 使用 volatile-lfu 策略 +maxmemory-policy volatile-lfu -**_2、异步删除_** +# 或者使用 allkeys-lfu 策略 +maxmemory-policy allkeys-lfu +``` -从 Redis 4.0 版本开始,可以采用**异步删除**法,**用 unlink 命令代替 del 来删除**。 +需要注意的是,`hotkeys` 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。 -这样 Redis 会将这个 key 放入到一个异步线程中进行删除,这样不会阻塞主线程。 +1. **volatile-lfu(least frequently used)**:从已设置过期时间的数据集(`server.db[i].expires`)中挑选最不经常使用的数据淘汰。 +2. **allkeys-lfu(least frequently used)**:当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key。 -除了主动调用 unlink 命令实现异步删除之外,我们还可以通过配置参数,达到某些条件的时候自动进行异步删除。 +以下是配置文件 `redis.conf` 中的示例: -主要有 4 种场景,默认都是关闭的: +``` +# 使用 volatile-lfu 策略 +maxmemory-policy volatile-lfu -```text -lazyfree-lazy-eviction no -lazyfree-lazy-expire no -lazyfree-lazy-server-del -noslave-lazy-flush no +# 或者使用 allkeys-lfu 策略 +maxmemory-policy allkeys-lfu ``` -它们代表的含义如下: +需要注意的是,`hotkeys` 参数命令也会增加 Redis 实例的 CPU 和内存消耗(全局扫描),因此需要谨慎使用。 + +#### 如何解决 hotkey? -- lazyfree-lazy-eviction:表示当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删除; -- lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除; -- lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除; -- slave-lazy-flush:针对 slave (从节点) 进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。 +hotkey 的常见处理以及优化办法如下(这些方法可以配合起来使用): -建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样就可以有效的提高主线程的执行效率。 +- **读写分离**:主节点处理写请求,从节点处理读请求。 +- **使用 Redis Cluster**:将热点数据分散存储在多个 Redis 节点上。 +- **二级缓存**:hotkey 采用二级缓存的方式进行处理,将 hotkey 存放一份到 JVM 本地内存中(可以用 Caffeine)。 ## 参考资料 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/02.Memcached.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/02.Memcached.md" index 9f3cef8eba..9759da3d05 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/02.Memcached.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/02.Memcached.md" @@ -2,7 +2,6 @@ icon: logos:memcached title: Memcached 快速入门 date: 2022-02-17 22:34:30 -order: 02 categories: - 数据库 - KV数据库 @@ -10,7 +9,7 @@ tags: - 数据库 - KV数据库 - Memcached -permalink: /pages/56cf9a/ +permalink: /pages/e52934a7/ --- # Memcached 快速入门 @@ -88,4 +87,4 @@ public class MemcachedJava { - [Memcached 官网](https://memcached.org/) - [Memcached Github](https://github.com/memcached/memcached/) -- [Memcached 教程](https://www.runoob.com/memcached/memcached-tutorial.html) \ No newline at end of file +- [Memcached 教程](https://www.runoob.com/memcached/memcached-tutorial.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/README.md" index 63646933ee..1c8f2e5e88 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/05.KV\346\225\260\346\215\256\345\272\223/README.md" @@ -7,7 +7,7 @@ categories: tags: - 数据库 - KV数据库 -permalink: /pages/85202a/ +permalink: /pages/c6629edb/ hidden: true index: false --- @@ -16,7 +16,26 @@ index: false ## 📖 内容 - +### [Redis](01.Redis) + +- [Redis 基本数据类型](01.Redis/Redis_数据类型.md) - 关键词:`String`、`Hash`、`List`、`Set`、`Zset` +- [Redis 高级数据类型](01.Redis/Redis_数据类型二.md) - 关键词:`BitMap`、`HyperLogLog`、`Geo`、`Stream` +- [Redis 数据结构](01.Redis/Redis_数据结构.md) - 关键词:`对象`、`SDS`、`链表`、`字典`、`跳表`、`整数集合`、`压缩列表` +- [Redis 内存管理](01.Redis/Redis_内存管理.md) - 关键词:`定时删除`、`惰性删除`、`定期删除`、`LRU`、`LFU` +- [Redis 持久化](01.Redis/Redis_持久化.md) - 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync` +- [Redis 事件](01.Redis/Redis_事件.md) - 关键词:`文件事件`、`时间事件` +- [Redis 复制](01.Redis/Redis_复制.md) - 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`命令传播`、`心跳` +- [Redis 哨兵](01.Redis/Redis_哨兵.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`Raft` +- [Redis 集群](01.Redis/Redis_集群.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`分区`、`Raft`、`Gossip` +- [Redis 订阅](01.Redis/Redis_订阅.md) - 关键词:`订阅`、`SUBSCRIBE`、`PSUBSCRIBE`、`PUBLISH`、`观察者模式` +- [Redis 独立功能](01.Redis/Redis_事务.md) - 关键词:`事务`、`ACID`、`MULTI`、`EXEC`、`DISCARD`、`WATCH` +- [Redis 管道](01.Redis/Redis_管道.md) - 关键词:`Pipeline` +- [Redis 脚本](01.Redis/Redis_脚本.md) - 关键词:`Lua` +- [Redis 运维](01.Redis/Redis_运维.md) - 关键词:`安装`、`配置`、`命令`、`集群`、`客户端` +- [Redis 实战](01.Redis/Redis_实战.md) - 关键词:`缓存`、`分布式锁`、`布隆过滤器` +- [Redis 面试](01.Redis/Redis_面试.md) - 关键词:`面试` + +### [Memcached](02.Memcached) ## 📚 资料 @@ -49,4 +68,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/01.HBase\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/01.HBase\345\277\253\351\200\237\345\205\245\351\227\250.md" index f7a3c53c81..eb0ebbbbbc 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/01.HBase\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/01.HBase\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -11,7 +11,7 @@ tags: - 列式数据库 - 大数据 - HBase -permalink: /pages/7ab03c/ +permalink: /pages/87503572/ --- # HBase 快速入门 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/02.HBase\346\225\260\346\215\256\346\250\241\345\236\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/02.HBase\346\225\260\346\215\256\346\250\241\345\236\213.md" index ff19a04eb7..a536069eb7 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/02.HBase\346\225\260\346\215\256\346\250\241\345\236\213.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/02.HBase\346\225\260\346\215\256\346\250\241\345\236\213.md" @@ -11,7 +11,7 @@ tags: - 列式数据库 - 大数据 - HBase -permalink: /pages/c8cfeb/ +permalink: /pages/f033c70f/ --- # HBase 数据模型 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/03.HBaseSchema\350\256\276\350\256\241.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/03.HBaseSchema\350\256\276\350\256\241.md" index 7eb925c752..4d2075621f 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/03.HBaseSchema\350\256\276\350\256\241.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/03.HBaseSchema\350\256\276\350\256\241.md" @@ -9,7 +9,7 @@ categories: tags: - 大数据 - HBase -permalink: /pages/a69528/ +permalink: /pages/2197e992/ --- # HBase Schema 设计 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/04.HBase\346\236\266\346\236\204.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/04.HBase\346\236\266\346\236\204.md" index 7cdfa997cf..fc686996aa 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/04.HBase\346\236\266\346\236\204.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/04.HBase\346\236\266\346\236\204.md" @@ -9,7 +9,7 @@ categories: tags: - 大数据 - HBase -permalink: /pages/62f8d9/ +permalink: /pages/ed3d3fde/ --- # HBase 架构 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/10.HBaseJavaApi\345\237\272\347\241\200\347\211\271\346\200\247.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/10.HBaseJavaApi\345\237\272\347\241\200\347\211\271\346\200\247.md" index 5b69646d5a..4e8a6e1965 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/10.HBaseJavaApi\345\237\272\347\241\200\347\211\271\346\200\247.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/10.HBaseJavaApi\345\237\272\347\241\200\347\211\271\346\200\247.md" @@ -9,7 +9,7 @@ categories: tags: - 大数据 - HBase -permalink: /pages/a8cad3/ +permalink: /pages/f62adb4e/ --- # HBase Java API 基础特性 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/11.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\350\277\207\346\273\244\345\231\250.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/11.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\350\277\207\346\273\244\345\231\250.md" index 0465b1dc81..9419b4a1b1 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/11.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\350\277\207\346\273\244\345\231\250.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/11.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\350\277\207\346\273\244\345\231\250.md" @@ -10,7 +10,7 @@ tags: - 大数据 - HBase - API -permalink: /pages/a3347e/ +permalink: /pages/b83dca7a/ --- # HBase Java API 高级特性之过滤器 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/12.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\345\215\217\345\244\204\347\220\206\345\231\250.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/12.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\345\215\217\345\244\204\347\220\206\345\231\250.md" index bd664f06df..7d08c4c550 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/12.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\345\215\217\345\244\204\347\220\206\345\231\250.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/12.HBaseJavaApi\351\253\230\347\272\247\347\211\271\346\200\247\344\271\213\345\215\217\345\244\204\347\220\206\345\231\250.md" @@ -10,7 +10,7 @@ tags: - 大数据 - HBase - API -permalink: /pages/5f1bc3/ +permalink: /pages/ae009b3b/ --- # HBase Java API 高级特性之协处理器 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/13.HBaseJavaApi\345\205\266\344\273\226\351\253\230\347\272\247\347\211\271\346\200\247.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/13.HBaseJavaApi\345\205\266\344\273\226\351\253\230\347\272\247\347\211\271\346\200\247.md" index 45626d1038..e163884cd4 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/13.HBaseJavaApi\345\205\266\344\273\226\351\253\230\347\272\247\347\211\271\346\200\247.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/13.HBaseJavaApi\345\205\266\344\273\226\351\253\230\347\272\247\347\211\271\346\200\247.md" @@ -10,7 +10,7 @@ tags: - 大数据 - HBase - API -permalink: /pages/ce5ca0/ +permalink: /pages/5c4df258/ --- # HBase Java API 其他高级特性 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/14.HBaseJavaApi\347\256\241\347\220\206\345\212\237\350\203\275.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/14.HBaseJavaApi\347\256\241\347\220\206\345\212\237\350\203\275.md" index 35968b59ec..ff52995773 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/14.HBaseJavaApi\347\256\241\347\220\206\345\212\237\350\203\275.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/14.HBaseJavaApi\347\256\241\347\220\206\345\212\237\350\203\275.md" @@ -10,7 +10,7 @@ tags: - 大数据 - HBase - API -permalink: /pages/b59ba2/ +permalink: /pages/e4d04380/ --- # HBase Java API 管理功能 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/21.HBase\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/21.HBase\350\277\220\347\273\264.md" index b63ad1fa0a..cf27fbc16d 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/21.HBase\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/21.HBase\350\277\220\347\273\264.md" @@ -10,7 +10,7 @@ tags: - 大数据 - HBase - 运维 -permalink: /pages/f808fc/ +permalink: /pages/e1c3bf67/ --- # HBase 运维 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/22.HBase\345\221\275\344\273\244.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/22.HBase\345\221\275\344\273\244.md" index 9adaf918d8..0f792bdf0c 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/22.HBase\345\221\275\344\273\244.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/22.HBase\345\221\275\344\273\244.md" @@ -9,7 +9,7 @@ categories: tags: - 大数据 - HBase -permalink: /pages/263c40/ +permalink: /pages/4eb16c7d/ --- # HBase 命令 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/README.md" index e30929458d..b67c60f117 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/01.HBase/README.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - HBase -permalink: /pages/417be6/ +permalink: /pages/ed5e0d7f/ hidden: true index: false --- diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/02.Cassandra.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/02.Cassandra.md" index dbab0361d2..5c4dbaca8c 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/02.Cassandra.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/02.Cassandra.md" @@ -9,7 +9,7 @@ tags: - 数据库 - 列式数据库 - Cassandra -permalink: /pages/ca3ca5/ +permalink: /pages/d62ca29b/ --- # Cassandra diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/README.md" index c4501ed299..33432761e4 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/06.\345\210\227\345\274\217\346\225\260\346\215\256\345\272\223/README.md" @@ -7,7 +7,7 @@ categories: tags: - 数据库 - 列式数据库 -permalink: /pages/46f339/ +permalink: /pages/e5fd7cac/ hidden: true index: false --- diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/01.Elasticsearch\351\235\242\350\257\225\346\200\273\347\273\223.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/01.Elasticsearch\351\235\242\350\257\225\346\200\273\347\273\223.md" deleted file mode 100644 index be6761cbb3..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/01.Elasticsearch\351\235\242\350\257\225\346\200\273\347\273\223.md" +++ /dev/null @@ -1,647 +0,0 @@ ---- -title: Elasticsearch 面试总结 -date: 2020-06-16 07:10:44 -order: 01 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch - - 面试 -permalink: /pages/0cb563/ ---- - -# Elasticsearch 面试总结 - -## 集群部署 - -ES 部署情况: - -5 节点(配置:8 核 64 G 1T),总计 320 G,5 T。 - -约 10+ 索引,5 分片,每日新增数据量约为 2G,4000w 条。记录保存 30 天。 - -## 性能优化 - -### filesystem cache - -你往 es 里写的数据,实际上都写到磁盘文件里去了,**查询的时候**,操作系统会将磁盘文件里的数据自动缓存到 `filesystem cache` 里面去。 - -[![es-search-process](https://github.com/doocs/advanced-java/raw/main/docs/high-concurrency/images/es-search-process.png)](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/images/es-search-process.png) - -es 的搜索引擎严重依赖于底层的 `filesystem cache` ,你如果给 `filesystem cache` 更多的内存,尽量让内存可以容纳所有的 `idx segment file`索引数据文件,那么你搜索的时候就基本都是走内存的,性能会非常高。 - -性能差距究竟可以有多大?我们之前很多的测试和压测,如果走磁盘一般肯定上秒,搜索性能绝对是秒级别的,1 秒、5 秒、10 秒。但如果是走 `filesystem cache` ,是走纯内存的,那么一般来说性能比走磁盘要高一个数量级,基本上就是毫秒级的,从几毫秒到几百毫秒不等。 - -这里有个真实的案例。某个公司 es 节点有 3 台机器,每台机器看起来内存很多,64G,总内存就是 `64 * 3 = 192G` 。每台机器给 es jvm heap 是 `32G` ,那么剩下来留给 `filesystem cache` 的就是每台机器才 `32G` ,总共集群里给 `filesystem cache` 的就是 `32 * 3 = 96G` 内存。而此时,整个磁盘上索引数据文件,在 3 台机器上一共占用了 `1T` 的磁盘容量,es 数据量是 `1T` ,那么每台机器的数据量是 `300G` 。这样性能好吗? `filesystem cache` 的内存才 100G,十分之一的数据可以放内存,其他的都在磁盘,然后你执行搜索操作,大部分操作都是走磁盘,性能肯定差。 - -归根结底,你要让 es 性能要好,最佳的情况下,就是你的机器的内存,至少可以容纳你的总数据量的一半。 - -根据我们自己的生产环境实践经验,最佳的情况下,是仅仅在 es 中就存少量的数据,就是你要**用来搜索的那些索引**,如果内存留给 `filesystem cache` 的是 100G,那么你就将索引数据控制在 `100G` 以内,这样的话,你的数据几乎全部走内存来搜索,性能非常之高,一般可以在 1 秒以内。 - -比如说你现在有一行数据。 `id,name,age ....` 30 个字段。但是你现在搜索,只需要根据 `id,name,age` 三个字段来搜索。如果你傻乎乎往 es 里写入一行数据所有的字段,就会导致说 `90%` 的数据是不用来搜索的,结果硬是占据了 es 机器上的 `filesystem cache` 的空间,单条数据的数据量越大,就会导致 `filesystem cahce` 能缓存的数据就越少。其实,仅仅写入 es 中要用来检索的**少数几个字段**就可以了,比如说就写入 es `id,name,age` 三个字段,然后你可以把其他的字段数据存在 mysql/hbase 里,我们一般是建议用 `es + hbase` 这么一个架构。 - -hbase 的特点是**适用于海量数据的在线存储**,就是对 hbase 可以写入海量数据,但是不要做复杂的搜索,做很简单的一些根据 id 或者范围进行查询的这么一个操作就可以了。从 es 中根据 name 和 age 去搜索,拿到的结果可能就 20 个 `doc id` ,然后根据 `doc id` 到 hbase 里去查询每个 `doc id` 对应的**完整的数据**,给查出来,再返回给前端。 - -写入 es 的数据最好小于等于,或者是略微大于 es 的 filesystem cache 的内存容量。然后你从 es 检索可能就花费 20ms,然后再根据 es 返回的 id 去 hbase 里查询,查 20 条数据,可能也就耗费个 30ms,可能你原来那么玩儿,1T 数据都放 es,会每次查询都是 5~10s,现在可能性能就会很高,每次查询就是 50ms。 - -### 数据预热 - -假如说,哪怕是你就按照上述的方案去做了,es 集群中每个机器写入的数据量还是超过了 `filesystem cache` 一倍,比如说你写入一台机器 60G 数据,结果 `filesystem cache` 就 30G,还是有 30G 数据留在了磁盘上。 - -其实可以做**数据预热**。 - -举个例子,拿微博来说,你可以把一些大 V,平时看的人很多的数据,你自己提前后台搞个系统,每隔一会儿,自己的后台系统去搜索一下热数据,刷到 `filesystem cache` 里去,后面用户实际上来看这个热数据的时候,他们就是直接从内存里搜索了,很快。 - -或者是电商,你可以将平时查看最多的一些商品,比如说 iphone 8,热数据提前后台搞个程序,每隔 1 分钟自己主动访问一次,刷到 `filesystem cache` 里去。 - -对于那些你觉得比较热的、经常会有人访问的数据,最好**做一个专门的缓存预热子系统**,就是对热数据每隔一段时间,就提前访问一下,让数据进入 `filesystem cache` 里面去。这样下次别人访问的时候,性能一定会好很多。 - -### 冷热分离 - -es 可以做类似于 mysql 的水平拆分,就是说将大量的访问很少、频率很低的数据,单独写一个索引,然后将访问很频繁的热数据单独写一个索引。最好是将**冷数据写入一个索引中,然后热数据写入另外一个索引中**,这样可以确保热数据在被预热之后,尽量都让他们留在 `filesystem os cache` 里,**别让冷数据给冲刷掉**。 - -你看,假设你有 6 台机器,2 个索引,一个放冷数据,一个放热数据,每个索引 3 个 shard。3 台机器放热数据 index,另外 3 台机器放冷数据 index。然后这样的话,你大量的时间是在访问热数据 index,热数据可能就占总数据量的 10%,此时数据量很少,几乎全都保留在 `filesystem cache` 里面了,就可以确保热数据的访问性能是很高的。但是对于冷数据而言,是在别的 index 里的,跟热数据 index 不在相同的机器上,大家互相之间都没什么联系了。如果有人访问冷数据,可能大量数据是在磁盘上的,此时性能差点,就 10% 的人去访问冷数据,90% 的人在访问热数据,也无所谓了。 - -### document 模型设计 - -对于 MySQL,我们经常有一些复杂的关联查询。在 es 里该怎么玩儿,es 里面的复杂的关联查询尽量别用,一旦用了性能一般都不太好。 - -最好是先在 Java 系统里就完成关联,将关联好的数据直接写入 es 中。搜索的时候,就不需要利用 es 的搜索语法来完成 join 之类的关联搜索了。 - -document 模型设计是非常重要的,很多操作,不要在搜索的时候才想去执行各种复杂的乱七八糟的操作。es 能支持的操作就那么多,不要考虑用 es 做一些它不好操作的事情。如果真的有那种操作,尽量在 document 模型设计的时候,写入的时候就完成。另外对于一些太复杂的操作,比如 join/nested/parent-child 搜索都要尽量避免,性能都很差的。 - -### 分页性能优化 - -es 的分页是较坑的,为啥呢?举个例子吧,假如你每页是 10 条数据,你现在要查询第 100 页,实际上是会把每个 shard 上存储的前 1000 条数据都查到一个协调节点上,如果你有个 5 个 shard,那么就有 5000 条数据,接着协调节点对这 5000 条数据进行一些合并、处理,再获取到最终第 100 页的 10 条数据。 - -分布式的,你要查第 100 页的 10 条数据,不可能说从 5 个 shard,每个 shard 就查 2 条数据,最后到协调节点合并成 10 条数据吧?你**必须**得从每个 shard 都查 1000 条数据过来,然后根据你的需求进行排序、筛选等等操作,最后再次分页,拿到里面第 100 页的数据。你翻页的时候,翻的越深,每个 shard 返回的数据就越多,而且协调节点处理的时间越长,非常坑爹。所以用 es 做分页的时候,你会发现越翻到后面,就越是慢。 - -我们之前也是遇到过这个问题,用 es 作分页,前几页就几十毫秒,翻到 10 页或者几十页的时候,基本上就要 5~10 秒才能查出来一页数据了。 - -有什么解决方案吗? - -#### 不允许深度分页(默认深度分页性能很差) - -跟产品经理说,你系统不允许翻那么深的页,默认翻的越深,性能就越差。 - -#### 类似于 app 里的推荐商品不断下拉出来一页一页的 - -类似于微博中,下拉刷微博,刷出来一页一页的,你可以用 `scroll api` ,关于如何使用,自行上网搜索。 - -scroll 会一次性给你生成**所有数据的一个快照**,然后每次滑动向后翻页就是通过**游标** `scroll_id` 移动,获取下一页下一页这样子,性能会比上面说的那种分页性能要高很多很多,基本上都是毫秒级的。 - -但是,唯一的一点就是,这个适合于那种类似微博下拉翻页的,**不能随意跳到任何一页的场景**。也就是说,你不能先进入第 10 页,然后去第 120 页,然后又回到第 58 页,不能随意乱跳页。所以现在很多产品,都是不允许你随意翻页的,app,也有一些网站,做的就是你只能往下拉,一页一页的翻。 - -初始化时必须指定 `scroll` 参数,告诉 es 要保存此次搜索的上下文多长时间。你需要确保用户不会持续不断翻页翻几个小时,否则可能因为超时而失败。 - -除了用 `scroll api` ,你也可以用 `search_after` 来做, `search_after` 的思想是使用前一页的结果来帮助检索下一页的数据,显然,这种方式也不允许你随意翻页,你只能一页页往后翻。初始化时,需要使用一个唯一值的字段作为 sort 字段。 - -**1.1、设计阶段调优** - -(1)根据业务增量需求,采取基于日期模板创建索引,通过 roll over API 滚动索引; - -(2)使用别名进行索引管理; - -(3)每天凌晨定时对索引做 force_merge 操作,以释放空间; - -(4)采取冷热分离机制,热数据存储到 SSD,提高检索效率;冷数据定期进行 shrink 操作,以缩减存储; - -(5)采取 curator 进行索引的生命周期管理; - -(6)仅针对需要分词的字段,合理的设置分词器; - -(7)Mapping 阶段充分结合各个字段的属性,是否需要检索、是否需要存储等。…….. - -**1.2、写入调优** - -(1)写入前副本数设置为 0; - -(2)写入前关闭 refresh_interval 设置为-1,禁用刷新机制; - -(3)写入过程中:采取 bulk 批量写入; - -(4)写入后恢复副本数和刷新间隔; - -(5)尽量使用自动生成的 id。 - -1.3、查询调优 - -(1)禁用 wildcard; - -(2)禁用批量 terms(成百上千的场景); - -(3)充分利用倒排索引机制,能 keyword 类型尽量 keyword; - -(4)数据量大时候,可以先基于时间敲定索引再检索; - -(5)设置合理的路由机制。 - -1.4、其他调优 - -部署调优,业务调优等。 - -上面的提及一部分,面试者就基本对你之前的实践或者运维经验有所评估了。 - -## 工作原理 - -### es 写数据过程 - -- 客户端选择一个 node 发送请求过去,这个 node 就是 `coordinating node` (协调节点)。 -- `coordinating node` 对 document 进行**路由**,将请求转发给对应的 node(有 primary shard)。 -- 实际的 node 上的 `primary shard` 处理请求,然后将数据同步到 `replica node` 。 -- `coordinating node` 如果发现 `primary node` 和所有 `replica node` 都搞定之后,就返回响应结果给客户端。 - -[![es-write](https://github.com/doocs/advanced-java/raw/main/docs/high-concurrency/images/es-write.png)](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/images/es-write.png) - -### es 读数据过程 - -可以通过 `doc id` 来查询,会根据 `doc id` 进行 hash,判断出来当时把 `doc id` 分配到了哪个 shard 上面去,从那个 shard 去查询。 - -- 客户端发送请求到**任意**一个 node,成为 `coordinate node` 。 -- `coordinate node` 对 `doc id` 进行哈希路由,将请求转发到对应的 node,此时会使用 `round-robin` **随机轮询算法**,在 `primary shard` 以及其所有 replica 中随机选择一个,让读请求负载均衡。 -- 接收请求的 node 返回 document 给 `coordinate node` 。 -- `coordinate node` 返回 document 给客户端。 - -### es 搜索数据过程 - -es 最强大的是做全文检索,就是比如你有三条数据: - -``` -java真好玩儿啊 -java好难学啊 -j2ee特别牛 -``` - -你根据 `java` 关键词来搜索,将包含 `java` 的 `document` 给搜索出来。es 就会给你返回:java 真好玩儿啊,java 好难学啊。 - -- 客户端发送请求到一个 `coordinate node` 。 -- 协调节点将搜索请求转发到**所有**的 shard 对应的 `primary shard` 或 `replica shard` ,都可以。 -- query phase:每个 shard 将自己的搜索结果(其实就是一些 `doc id` )返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。 -- fetch phase:接着由协调节点根据 `doc id` 去各个节点上**拉取实际**的 `document` 数据,最终返回给客户端。 - -> 写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。 - -### 写数据底层原理 - -[![es-write-detail](https://github.com/doocs/advanced-java/raw/master/docs/high-concurrency/images/es-write-detail.png)](https://github.com/doocs/advanced-java/blob/main/docs/high-concurrency/images/es-write-detail.png) - -先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。 - -如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 `refresh` 到一个新的 `segment file` 中,但是此时数据不是直接进入 `segment file` 磁盘文件,而是先进入 `os cache` 。这个过程就是 `refresh` 。 - -每隔 1 秒钟,es 将 buffer 中的数据写入一个**新的** `segment file` ,每秒钟会产生一个**新的磁盘文件** `segment file` ,这个 `segment file` 中就存储最近 1 秒内 buffer 中写入的数据。 - -但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。 - -操作系统里面,磁盘文件其实都有一个东西,叫做 `os cache` ,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 `os cache` ,先进入操作系统级别的一个内存缓存中去。只要 `buffer` 中的数据被 refresh 操作刷入 `os cache` 中,这个数据就可以被搜索到了。 - -为什么叫 es 是**准实时**的? `NRT` ,全称 `near real-time` 。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 `restful api` 或者 `java api` ,**手动**执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 `os cache` 中,让数据立马就可以被搜索到。只要数据被输入 `os cache` 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。 - -重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 `buffer` 数据写入一个又一个新的 `segment file` 中去,每次 `refresh` 完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 `commit` 操作。 - -commit 操作发生第一步,就是将 buffer 中现有数据 `refresh` 到 `os cache` 中去,清空 buffer。然后,将一个 `commit point` 写入磁盘文件,里面标识着这个 `commit point` 对应的所有 `segment file` ,同时强行将 `os cache` 中目前所有的数据都 `fsync` 到磁盘文件中去。最后**清空** 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。 - -这个 commit 操作叫做 `flush` 。默认 30 分钟自动执行一次 `flush` ,但如果 translog 过大,也会触发 `flush` 。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。 - -translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 `translog` 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。 - -translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会**丢失** 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 `fsync` 到磁盘,但是性能会差很多。 - -实际上你在这里,如果面试官没有问你 es 丢数据的问题,你可以在这里给面试官炫一把,你说,其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的**数据丢失**。 - -**总结一下**,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。 - -> 数据写入 segment file 之后,同时就建立好了倒排索引。 - -### 删除/更新数据底层原理 - -如果是删除操作,commit 的时候会生成一个 `.del` 文件,里面将某个 doc 标识为 `deleted` 状态,那么搜索的时候根据 `.del` 文件就知道这个 doc 是否被删除了。 - -如果是更新操作,就是将原来的 doc 标识为 `deleted` 状态,然后新写入一条数据。 - -buffer 每 refresh 一次,就会产生一个 `segment file` ,所以默认情况下是 1 秒钟一个 `segment file` ,这样下来 `segment file` 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 `segment file` 合并成一个,同时这里会将标识为 `deleted` 的 doc 给**物理删除掉**,然后将新的 `segment file` 写入磁盘,这里会写一个 `commit point` ,标识所有新的 `segment file` ,然后打开 `segment file` 供搜索使用,同时删除旧的 `segment file` 。 - -### 底层 lucene - -简单来说,lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。 - -通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构。 - -### 倒排索引 - -在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,文档 1 经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。 - -那么,倒排索引就是**关键词到文档** ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。 - -举个栗子。 - -有以下文档: - -| DocId | Doc | -| ----- | ---------------------------------------------- | -| 1 | 谷歌地图之父跳槽 Facebook | -| 2 | 谷歌地图之父加盟 Facebook | -| 3 | 谷歌地图创始人拉斯离开谷歌加盟 Facebook | -| 4 | 谷歌地图之父跳槽 Facebook 与 Wave 项目取消有关 | -| 5 | 谷歌地图之父拉斯加盟社交网站 Facebook | - -对文档进行分词之后,得到以下**倒排索引**。 - -| WordId | Word | DocIds | -| ------ | -------- | ------------- | -| 1 | 谷歌 | 1, 2, 3, 4, 5 | -| 2 | 地图 | 1, 2, 3, 4, 5 | -| 3 | 之父 | 1, 2, 4, 5 | -| 4 | 跳槽 | 1, 4 | -| 5 | Facebook | 1, 2, 3, 4, 5 | -| 6 | 加盟 | 2, 3, 5 | -| 7 | 创始人 | 3 | -| 8 | 拉斯 | 3, 5 | -| 9 | 离开 | 3 | -| 10 | 与 | 4 | -| .. | .. | .. | - -另外,实用的倒排索引还可以记录更多的信息,比如文档频率信息,表示在文档集合中有多少个文档包含某个单词。 - -那么,有了倒排索引,搜索引擎可以很方便地响应用户的查询。比如用户输入查询 `Facebook` ,搜索系统查找倒排索引,从中读出包含这个单词的文档,这些文档就是提供给用户的搜索结果。 - -要注意倒排索引的两个重要细节: - -- 倒排索引中的所有词项对应一个或多个文档; -- 倒排索引中的词项**根据字典顺序升序排列** - -> 上面只是一个简单的栗子,并没有严格按照字典顺序升序排列。 - -## elasticsearch 的倒排索引是什么 - -面试官:想了解你对基础概念的认知。 - -解答:通俗解释一下就可以。 - -传统的我们的检索是通过文章,逐个遍历找到对应关键词的位置。 - -而倒排索引,是通过分词策略,形成了词和文章的映射关系表,这种词典+映射表即为倒排索引。有了倒排索引,就能实现 o(1)时间复杂度的效率检索文章了,极大的提高了检索效率。 - -![img](https://pic3.zhimg.com/80/v2-bf18227dc4554da0dcc7b970dbd582ae_720w.jpg) - -学术的解答方式: - -倒排索引,相反于一篇文章包含了哪些词,它从词出发,记载了这个词在哪些文档中出现过,由两部分组成——词典和倒排表。 - -加分项:倒排索引的底层实现是基于:FST(Finite State Transducer)数据结构。 - -lucene 从 4+版本后开始大量使用的数据结构是 FST。FST 有两个优点: - -(1)空间占用小。通过对词典中单词前缀和后缀的重复利用,压缩了存储空间; - -(2)查询速度快。O(len(str))的查询时间复杂度。 - -## 3、elasticsearch 索引数据多了怎么办,如何调优,部署 - -面试官:想了解大数据量的运维能力。 - -解答:索引数据的规划,应在前期做好规划,正所谓“设计先行,编码在后”,这样才能有效的避免突如其来的数据激增导致集群处理能力不足引发的线上客户检索或者其他业务受到影响。 - -如何调优,正如问题 1 所说,这里细化一下: - -**3.1 动态索引层面** - -基于模板+时间+rollover api 滚动创建索引,举例:设计阶段定义:blog 索引的模板格式为:blog*index*时间戳的形式,每天递增数据。这样做的好处:不至于数据量激增导致单个索引数据量非常大,接近于上线 2 的 32 次幂-1,索引存储达到了 TB+甚至更大。 - -一旦单个索引很大,存储等各种风险也随之而来,所以要提前考虑+及早避免。 - -**3.2 存储层面** - -冷热数据分离存储,热数据(比如最近 3 天或者一周的数据),其余为冷数据。 - -对于冷数据不会再写入新数据,可以考虑定期 force_merge 加 shrink 压缩操作,节省存储空间和检索效率。 - -**3.3 部署层面** - -一旦之前没有规划,这里就属于应急策略。 - -结合 ES 自身的支持动态扩展的特点,动态新增机器的方式可以缓解集群压力,注意:如果之前主节点等规划合理,不需要重启集群也能完成动态新增的。 - -## 4、elasticsearch 是如何实现 master 选举的 - -面试官:想了解 ES 集群的底层原理,不再只关注业务层面了。 - -解答: - -前置前提: - -(1)只有候选主节点(master:true)的节点才能成为主节点。 - -(2)最小主节点数(min_master_nodes)的目的是防止脑裂。 - -核对了一下代码,核心入口为 findMaster,选择主节点成功返回对应 Master,否则返回 null。选举流程大致描述如下: - -第一步:确认候选主节点数达标,elasticsearch.yml 设置的值 - -discovery.zen.minimum_master_nodes; - -第二步:比较:先判定是否具备 master 资格,具备候选主节点资格的优先返回; - -若两节点都为候选主节点,则 id 小的值会主节点。注意这里的 id 为 string 类型。 - -题外话:获取节点 id 的方法。 - -```text -1GET /_cat/nodes?v&h=ip,port,heapPercent,heapMax,id,name - -2ip port heapPercent heapMax id name -``` - -## 详细描述一下 Elasticsearch 索引文档的过程 - -面试官:想了解 ES 的底层原理,不再只关注业务层面了。 - -解答: - -这里的索引文档应该理解为文档写入 ES,创建索引的过程。 - -文档写入包含:单文档写入和批量 bulk 写入,这里只解释一下:单文档写入流程。 - -记住官方文档中的这个图。 - -![img](https://pic3.zhimg.com/80/v2-bf1b23846420eb4fdace5c6415ad7cf2_720w.jpg) - -第一步:客户写集群某节点写入数据,发送请求。(如果没有指定路由/协调节点,请求的节点扮演路由节点的角色。) - -第二步:节点 1 接受到请求后,使用文档\_id 来确定文档属于分片 0。请求会被转到另外的节点,假定节点 3。因此分片 0 的主分片分配到节点 3 上。 - -第三步:节点 3 在主分片上执行写操作,如果成功,则将请求并行转发到节点 1 和节点 2 的副本分片上,等待结果返回。所有的副本分片都报告成功,节点 3 将向协调节点(节点 1)报告成功,节点 1 向请求客户端报告写入成功。 - -如果面试官再问:第二步中的文档获取分片的过程? - -回答:借助路由算法获取,路由算法就是根据路由和文档 id 计算目标的分片 id 的过程。 - -```text -1shard = hash(_routing) % (num_of_primary_shards) -``` - -## 详细描述一下 Elasticsearch 搜索的过程? - -面试官:想了解 ES 搜索的底层原理,不再只关注业务层面了。 - -解答: - -搜索拆解为“query then fetch” 两个阶段。 - -query 阶段的目的:定位到位置,但不取。 - -步骤拆解如下: - -(1)假设一个索引数据有 5 主+1 副本 共 10 分片,一次请求会命中(主或者副本分片中)的一个。 - -(2)每个分片在本地进行查询,结果返回到本地有序的优先队列中。 - -(3)第 2)步骤的结果发送到协调节点,协调节点产生一个全局的排序列表。 - -fetch 阶段的目的:取数据。 - -路由节点获取所有文档,返回给客户端。 - -## Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法 - -面试官:想了解对 ES 集群的运维能力。 - -解答: - -(1)关闭缓存 swap; - -(2)堆内存设置为:Min(节点内存/2, 32GB); - -(3)设置最大文件句柄数; - -(4)线程池+队列大小根据业务需要做调整; - -(5)磁盘存储 raid 方式——存储有条件使用 RAID10,增加单节点性能以及避免单节点存储故障。 - -## lucence 内部结构是什么? - -面试官:想了解你的知识面的广度和深度。 - -解答: - -![img](https://pic1.zhimg.com/80/v2-576954e3b238870ec089d68abe0de1d4_720w.jpg) - -Lucene 是有索引和搜索的两个过程,包含索引创建,索引,搜索三个要点。可以基于这个脉络展开一些。 - -## Elasticsearch 是如何实现 Master 选举的? - -(1)Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块包含一个主机列表以控制哪些节点需要 ping 通)这两部分; - -(2)对所有可以成为 master 的节点(node.master: true)根据 nodeId 字典排序,每次选举每个节点都把自己所知道节点排一次序,然后选出第一个(第 0 位)节点,暂且认为它是 master 节点。 - -(3)如果对某个节点的投票数达到一定的值(可以成为 master 节点数 n/2+1)并且该节点自己也选举自己,那这个节点就是 master。否则重新选举一直到满足上述条件。 - -(4)补充:master 节点的职责主要包括集群、节点和索引的管理,不负责文档级别的管理;data 节点可以关闭 http 功能\*。 - -## 10、Elasticsearch 中的节点(比如共 20 个),其中的 10 个 - -选了一个 master,另外 10 个选了另一个 master,怎么办? - -(1)当集群 master 候选数量不小于 3 个时,可以通过设置最少投票通过数量(discovery.zen.minimum_master_nodes)超过所有候选节点一半以上来解决脑裂问题; - -(3)当候选数量为两个时,只能修改为唯一的一个 master 候选,其他作为 data 节点,避免脑裂问题。 - -## 客户端在和集群连接时,如何选择特定的节点执行请求的? - -TransportClient 利用 transport 模块远程连接一个 elasticsearch 集群。它并不加入到集群中,只是简单的获得一个或者多个初始化的 transport 地址,并以 轮询 的方式与这些地址进行通信。 - -## 详细描述一下 Elasticsearch 索引文档的过程。 - -协调节点默认使用文档 ID 参与计算(也支持通过 routing),以便为路由提供合适的分片。 - -```text -shard = hash(document_id) % (num_of_primary_shards) -``` - -(1)当分片所在的节点接收到来自协调节点的请求后,会将请求写入到 MemoryBuffer,然后定时(默认是每隔 1 秒)写入到 Filesystem Cache,这个从 MomeryBuffer 到 Filesystem Cache 的过程就叫做 refresh; - -(2)当然在某些情况下,存在 Momery Buffer 和 Filesystem Cache 的数据可能会丢失,ES 是通过 translog 的机制来保证数据的可靠性的。其实现机制是接收到请求后,同时也会写入到 translog 中 ,当 Filesystem cache 中的数据写入到磁盘中时,才会清除掉,这个过程叫做 flush; - -(3)在 flush 过程中,内存中的缓冲将被清除,内容被写入一个新段,段的 fsync 将创建一个新的提交点,并将内容刷新到磁盘,旧的 translog 将被删除并开始一个新的 translog。 - -(4)flush 触发的时机是定时触发(默认 30 分钟)或者 translog 变得太大(默认为 512M)时; - -![img](https://pic4.zhimg.com/80/v2-5e0c4bfbd57a4fae4895c480aaaa0a37_720w.jpg) - -补充:关于 Lucene 的 Segement: - -(1)Lucene 索引是由多个段组成,段本身是一个功能齐全的倒排索引。 - -(2)段是不可变的,允许 Lucene 将新的文档增量地添加到索引中,而不用从头重建索引。 - -(3)对于每一个搜索请求而言,索引中的所有段都会被搜索,并且每个段会消耗 CPU 的时钟周、文件句柄和内存。这意味着段的数量越多,搜索性能会越低。 - -(4)为了解决这个问题,Elasticsearch 会合并小段到一个较大的段,提交新的合并段到磁盘,并删除那些旧的小段。 - -## 详细描述一下 Elasticsearch 更新和删除文档的过程。 - -(1)删除和更新也都是写操作,但是 Elasticsearch 中的文档是不可变的,因此不能被删除或者改动以展示其变更; - -(2)磁盘上的每个段都有一个相应的.del 文件。当删除请求发送后,文档并没有真的被删除,而是在.del 文件中被标记为删除。该文档依然能匹配查询,但是会在结果中被过滤掉。当段合并时,在.del 文件中被标记为删除的文档将不会被写入新段。 - -(3)在新的文档被创建时,Elasticsearch 会为该文档指定一个版本号,当执行更新时,旧版本的文档在.del 文件中被标记为删除,新版本的文档被索引到一个新段。旧版本的文档依然能匹配查询,但是会在结果中被过滤掉。 - -## 详细描述一下 Elasticsearch 搜索的过程。 - -(1)搜索被执行成一个两阶段过程,我们称之为 Query Then Fetch; - -(2)在初始查询阶段时,查询会广播到索引中每一个分片拷贝(主分片或者副本分片)。 每个分片在本地执行搜索并构建一个匹配文档的大小为 from + size 的优先队列。 - -PS:在搜索的时候是会查询 Filesystem Cache 的,但是有部分数据还在 MemoryBuffer,所以搜索是近实时的。 - -(3)每个分片返回各自优先队列中 所有文档的 ID 和排序值 给协调节点,它合并这些值到自己的优先队列中来产生一个全局排序后的结果列表。 - -(4)接下来就是 取回阶段,协调节点辨别出哪些文档需要被取回并向相关的分片提交多个 GET 请求。每个分片加载并 丰 富 文档,如果有需要的话,接着返回文档给协调节点。一旦所有的文档都被取回了,协调节点返回结果给客户端。 - -(5)补充:Query Then Fetch 的搜索类型在文档相关性打分的时候参考的是本分片的数据,这样在文档数量较少的时候可能不够准确,DFS Query Then Fetch 增加了一个预查询的处理,询问 Term 和 Document frequency,这个评分更准确,但是性能会变差。\* - -![img](https://pic2.zhimg.com/80/v2-4c25616e623de2aee23bd63ec22a5bfd_720w.jpg) - -## 在 Elasticsearch 中,是怎么根据一个词找到对应的倒排索引的? - -(1)Lucene 的索引过程,就是按照全文检索的基本过程,将倒排表写成此文件格式的过程。 - -(2)Lucene 的搜索过程,就是按照此文件格式将索引进去的信息读出来,然后计算每篇文档打分(score)的过程。 - -## Elasticsearch 在部署时,对 Linux 的设置有哪些优化方法? - -(1)64 GB 内存的机器是非常理想的, 但是 32 GB 和 16 GB 机器也是很常见的。少于 8 GB 会适得其反。 - -(2)如果你要在更快的 CPUs 和更多的核心之间选择,选择更多的核心更好。多个内核提供的额外并发远胜过稍微快一点点的时钟频率。 - -(3)如果你负担得起 SSD,它将远远超出任何旋转介质。 基于 SSD 的节点,查询和索引性能都有提升。如果你负担得起,SSD 是一个好的选择。 - -(4)即使数据中心们近在咫尺,也要避免集群跨越多个数据中心。绝对要避免集群跨越大的地理距离。 - -(5)请确保运行你应用程序的 JVM 和服务器的 JVM 是完全一样的。 在 Elasticsearch 的几个地方,使用 Java 的本地序列化。 - -(6)通过设置 gateway.recover_after_nodes、gateway.expected_nodes、gateway.recover_after_time 可以在集群重启的时候避免过多的分片交换,这可能会让数据恢复从数个小时缩短为几秒钟。 - -(7)Elasticsearch 默认被配置为使用单播发现,以防止节点无意中加入集群。只有在同一台机器上运行的节点才会自动组成集群。最好使用单播代替组播。 - -(8)不要随意修改垃圾回收器(CMS)和各个线程池的大小。 - -(9)把你的内存的(少于)一半给 Lucene(但不要超过 32 GB!),通过 ES_HEAP_SIZE 环境变量设置。 - -(10)内存交换到磁盘对服务器性能来说是致命的。如果内存交换到磁盘上,一个 100 微秒的操作可能变成 10 毫秒。 再想想那么多 10 微秒的操作时延累加起来。 不难看出 swapping 对于性能是多么可怕。 - -(11)Lucene 使用了大 量 的文件。同时,Elasticsearch 在节点和 HTTP 客户端之间进行通信也使用了大量的套接字。 所有这一切都需要足够的文件描述符。你应该增加你的文件描述符,设置一个很大的值,如 64,000。 - -补充:索引阶段性能提升方法 - -(1)使用批量请求并调整其大小:每次批量数据 5–15 MB 大是个不错的起始点。 - -(2)存储:使用 SSD - -(3)段和合并:Elasticsearch 默认值是 20 MB/s,对机械磁盘应该是个不错的设置。如果你用的是 SSD,可以考虑提高到 100–200 MB/s。如果你在做批量导入,完全不在意搜索,你可以彻底关掉合并限流。另外还可以增加 index.translog.flush_threshold_size 设置,从默认的 512 MB 到更大一些的值,比如 1 GB,这可以在一次清空触发的时候在事务日志里积累出更大的段。 - -(4)如果你的搜索结果不需要近实时的准确度,考虑把每个索引的 index.refresh_interval 改到 30s。 - -(5)如果你在做大批量导入,考虑通过设置 index.number_of_replicas: 0 关闭副本。 - -## 对于 GC 方面,在使用 Elasticsearch 时要注意什么? - -(1)倒排词典的索引需要常驻内存,无法 GC,需要监控 data node 上 segmentmemory 增长趋势。 - -(2)各类缓存,field cache, filter cache, indexing cache, bulk queue 等等,要设置合理的大小,并且要应该根据最坏的情况来看 heap 是否够用,也就是各类缓存全部占满的时候,还有 heap 空间可以分配给其他任务吗?避免采用 clear cache 等“自欺欺人”的方式来释放内存。 - -(3)避免返回大量结果集的搜索与聚合。确实需要大量拉取数据的场景,可以采用 scan & scroll api 来实现。 - -(4)cluster stats 驻留内存并无法水平扩展,超大规模集群可以考虑分拆成多个集群通过 tribe node 连接。 - -(5)想知道 heap 够不够,必须结合实际应用场景,并对集群的 heap 使用情况做持续的监控。 - -(6)根据监控数据理解内存需求,合理配置各类 circuit breaker,将内存溢出风险降低到最低 - -## 18、Elasticsearch 对于大数据量(上亿量级)的聚合如何实现? - -Elasticsearch 提供的首个近似聚合是 cardinality 度量。它提供一个字段的基数,即该字段的 distinct 或者 unique 值的数目。它是基于 HLL 算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。 - -## 19、在并发情况下,Elasticsearch 如果保证读写一致? - -(1)可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突; - -(2)另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。 - -(3)对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数\_preference 为 primary 来查询主分片,确保文档是最新版本。 - -## 20、如何监控 Elasticsearch 集群状态? - -Marvel 让你可以很简单的通过 Kibana 监控 Elasticsearch。你可以实时查看你的集群健康状态和性能,也可以分析过去的集群、索引和节点指标。 - -## 21、介绍下你们电商搜索的整体技术架构。 - -![img](https://pic1.zhimg.com/80/v2-5bdbe7ada0ddee9d8b2f03c0a379e0d4_720w.jpg) - -## 介绍一下你们的个性化搜索方案? - -基于 word2vec 和 Elasticsearch 实现个性化搜索 - -(1)基于 word2vec、Elasticsearch 和自定义的脚本插件,我们就实现了一个个性化的搜索服务,相对于原有的实现,新版的点击率和转化率都有大幅的提升; - -(2)基于 word2vec 的商品向量还有一个可用之处,就是可以用来实现相似商品的推荐; - -(3)使用 word2vec 来实现个性化搜索或个性化推荐是有一定局限性的,因为它只能处理用户点击历史这样的时序数据,而无法全面的去考虑用户偏好,这个还是有很大的改进和提升的空间; - -## 是否了解字典树? - -常用字典数据结构如下所示: - -![img](https://pic2.zhimg.com/80/v2-8bb844c5b8fb944111fa8cecdb0e12d5_720w.jpg) - -Trie 的核心思想是空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的。它有 3 个基本性质: - -1)根节点不包含字符,除根节点外每一个节点都只包含一个字符。 - -2)从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。 - -3)每个节点的所有子节点包含的字符都不相同。 - -![img](https://pic4.zhimg.com/80/v2-26a48882a8f09a50dfeb79cc25045fcf_720w.jpg) - -(1)可以看到,trie 树每一层的节点数是 26^i 级别的。所以为了节省空间,我们还可以用动态链表,或者用数组来模拟动态。而空间的花费,不会超过单词数 × 单词长度。 - -(2)实现:对每个结点开一个字母集大小的数组,每个结点挂一个链表,使用左儿子右兄弟表示法记录这棵树; - -(3)对于中文的字典树,每个节点的子节点用一个哈希表存储,这样就不用浪费太大的空间,而且查询速度上可以保留哈希的复杂度 O(1)。 - -## 拼写纠错是如何实现的? - -(1)拼写纠错是基于编辑距离来实现;编辑距离是一种标准的方法,它用来表示经过插入、删除和替换操作从一个字符串转换到另外一个字符串的最小操作步数; - -(2)编辑距离的计算过程:比如要计算 batyu 和 beauty 的编辑距离,先创建一个 7×8 的表(batyu 长度为 5,coffee 长度为 6,各加 2),接着,在如下位置填入黑色数字。其他格的计算过程是取以下三个值的最小值: - -如果最上方的字符等于最左方的字符,则为左上方的数字。否则为左上方的数字+1。(对于 3,3 来说为 0) - -左方数字+1(对于 3,3 格来说为 2) - -上方数字+1(对于 3,3 格来说为 2) - -最终取右下角的值即为编辑距离的值 3。 - -![img](https://pic4.zhimg.com/80/v2-66f01f0d578c83274e90a7ddf704b633_720w.jpg) - -对于拼写纠错,我们考虑构造一个度量空间(Metric Space),该空间内任何关系满足以下三条基本条件: - -d(x,y) = 0 -- 假如 x 与 y 的距离为 0,则 x=y - -d(x,y) = d(y,x) -- x 到 y 的距离等同于 y 到 x 的距离 - -d(x,y) + d(y,z) >= d(x,z) -- 三角不等式 - -(1)根据三角不等式,则满足与 query 距离在 n 范围内的另一个字符转 B,其与 A 的距离最大为 d+n,最小为 d-n。 - -(2)BK 树的构造就过程如下:每个节点有任意个子节点,每条边有个值表示编辑距离。所有子节点到父节点的边上标注 n 表示编辑距离恰好为 n。比如,我们有棵树父节点是”book”和两个子节点”cake”和”books”,”book”到”books”的边标号 1,”book”到”cake”的边上标号 4。从字典里构造好树后,无论何时你想插入新单词时,计算该单词与根节点的编辑距离,并且查找数值为 d(neweord, root)的边。递归得与各子节点进行比较,直到没有子节点,你就可以创建新的子节点并将新单词保存在那。比如,插入”boo”到刚才上述例子的树中,我们先检查根节点,查找 d(“book”, “boo”) = 1 的边,然后检查标号为 1 的边的子节点,得到单词”books”。我们再计算距离 d(“books”, “boo”)=2,则将新单词插在”books”之后,边标号为 2。 - -(3)查询相似词如下:计算单词与根节点的编辑距离 d,然后递归查找每个子节点标号为 d-n 到 d+n(包含)的边。假如被检查的节点与搜索单词的距离 d 小于 n,则返回该节点并继续查询。比如输入 cape 且最大容忍距离为 1,则先计算和根的编辑距离 d(“book”, “cape”)=4,然后接着找和根节点之间编辑距离为 3 到 5 的,这个就找到了 cake 这个节点,计算 d(“cake”, “cape”)=1,满足条件所以返回 cake,然后再找和 cake 节点编辑距离是 0 到 2 的,分别找到 cape 和 cart 节点,这样就得到 cape 这个满足条件的结果。 - -![img](https://pic4.zhimg.com/80/v2-79f2a89041e546d9feccf55e4ff1c0d7_720w.jpg) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/02.Elasticsearch\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/02.Elasticsearch\345\277\253\351\200\237\345\205\245\351\227\250.md" deleted file mode 100644 index 7cc762b32e..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/02.Elasticsearch\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ /dev/null @@ -1,249 +0,0 @@ ---- -title: Elasticsearch 快速入门 -date: 2020-06-16 07:10:44 -order: 02 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -permalink: /pages/98c3a5/ ---- - -# Elasticsearch 快速入门 - -> **[Elasticsearch](https://github.com/elastic/elasticsearch) 是一个分布式、RESTful 风格的搜索和数据分析引擎**,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。 -> -> [Elasticsearch](https://github.com/elastic/elasticsearch) 基于搜索库 [Lucene](https://github.com/apache/lucene-solr) 开发。ElasticSearch 隐藏了 Lucene 的复杂性,提供了简单易用的 REST API / Java API 接口(另外还有其他语言的 API 接口)。 -> -> _以下简称 ES_。 - -## Elasticsearch 简介 - -### 什么是 Elasticsearch - -**[Elasticsearch](https://github.com/elastic/elasticsearch) 是一个分布式、RESTful 风格的搜索和数据分析引擎**,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。 - -[Elasticsearch](https://github.com/elastic/elasticsearch) **基于搜索库 [Lucene](https://github.com/apache/lucene-solr) 开发**。ElasticSearch 隐藏了 Lucene 的复杂性,提供了简单易用的 REST API / Java API 接口(另外还有其他语言的 API 接口)。 - -ElasticSearch 可以视为一个文档存储,它**将复杂数据结构序列化为 JSON 存储**。 - -**ElasticSearch 是近乎于实时的全文搜素**,这是指: - -- 从写入数据到数据可以被搜索,存在较小的延迟(大概是 1s) -- 基于 ES 执行搜索和分析可以达到秒级 - -### 核心概念 - -``` -index -> type -> mapping -> document -> field -``` - -#### Cluster - -集群包含多个节点,每个节点属于哪个集群都是通过一个配置来决定的,对于中小型应用来说,刚开始一个集群就一个节点很正常。 - -#### Node - -Node 是集群中的一个节点,节点也有一个名称,默认是随机分配的。默认节点会去加入一个名称为 `elasticsearch` 的集群。如果直接启动一堆节点,那么它们会自动组成一个 elasticsearch 集群,当然一个节点也可以组成 elasticsearch 集群。 - -#### Index - -**可以认为是文档(document)的优化集合。** - -ES 会为所有字段建立索引,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。 - -所以,ES 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。 - -#### Type - -每个索引里可以有一个或者多个类型(type)。`类型(type)` 是 index 的一个逻辑分类。 - -不同的 Type 应该有相似的结构(schema),举例来说,`id`字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的[一个区别](https://www.elastic.co/guide/en/elasticsearch/guide/current/mapping.html)。性质完全不同的数据(比如`products`和`logs`)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。 - -> 注意:根据[规划](https://www.elastic.co/blog/index-type-parent-child-join-now-future-in-elasticsearch),Elastic 6.x 版只允许每个 Index 包含一个 Type,7.x 版将会彻底移除 Type。 - -#### Document - -Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。 - -每个 **`文档(document)`** 都是字段(field)的集合。 - -Document 使用 JSON 格式表示,下面是一个例子。 - -```javascript -{ -"user": "张三", -"title": "工程师", -"desc": "数据库管理" -} -``` - -同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。 - -#### Field - -**`字段(field)`** 是包含数据的键值对。 - -默认情况下,Elasticsearch 对每个字段中的所有数据建立索引,并且每个索引字段都具有专用的优化数据结构。 - -#### Shard - -当单台机器不足以存储大量数据时,Elasticsearch 可以将一个索引中的数据切分为多个 **`分片(shard)`** 。 **`分片(shard)`** 分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个 shard 都是一个 lucene index。 - -#### Replica - -任何一个服务器随时可能故障或宕机,此时 shard 可能就会丢失,因此可以为每个 shard 创建多个 **`副本(replica)`**。replica 可以在 shard 故障时提供备用服务,保证数据不丢失,多个 replica 还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认 5 个),replica shard(随时修改数量,默认 1 个),默认每个索引 10 个 shard,5 个 primary shard,5 个 replica shard,最小的高可用配置,是 2 台服务器。 - -#### ES 核心概念 vs. DB 核心概念 - -| ES | DB | -| -------- | -------- | -| index | 数据库 | -| type | 数据表 | -| docuemnt | 一行数据 | - -## ElasticSearch 基本原理 - -### ES 写数据过程 - -- 客户端选择一个 node 发送请求过去,这个 node 就是 `coordinating node`(协调节点)。 -- `coordinating node` 对 document 进行**路由**,将请求转发给对应的 node(有 primary shard)。 -- 实际的 node 上的 `primary shard` 处理请求,然后将数据同步到 `replica node`。 -- `coordinating node` 如果发现 `primary node` 和所有 `replica node` 都搞定之后,就返回响应结果给客户端。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210712104055.png) - -### ES 读数据过程 - -可以通过 `doc id` 来查询,会根据 `doc id` 进行 hash,判断出来当时把 `doc id` 分配到了哪个 shard 上面去,从那个 shard 去查询。 - -- 客户端发送请求到**任意**一个 node,成为 `coordinate node`。 -- `coordinate node` 对 `doc id` 进行哈希路由,将请求转发到对应的 node,此时会使用 `round-robin` **轮询算法**,在 `primary shard` 以及其所有 replica 中随机选择一个,让读请求负载均衡。 -- 接收请求的 node 返回 document 给 `coordinate node`。 -- `coordinate node` 返回 document 给客户端。 - -### es 搜索数据过程 - -es 最强大的是做全文检索,就是比如你有三条数据: - -``` -java真好玩儿啊 -java好难学啊 -j2ee特别牛 -``` - -你根据 `java` 关键词来搜索,将包含 `java` 的 `document` 给搜索出来。es 就会给你返回:java 真好玩儿啊,java 好难学啊。 - -- 客户端发送请求到一个 `coordinate node` 。 -- 协调节点将搜索请求转发到**所有**的 shard 对应的 `primary shard` 或 `replica shard` ,都可以。 -- query phase:每个 shard 将自己的搜索结果(其实就是一些 `doc id` )返回给协调节点,由协调节点进行数据的合并、排序、分页等操作,产出最终结果。 -- fetch phase:接着由协调节点根据 `doc id` 去各个节点上**拉取实际**的 `document` 数据,最终返回给客户端。 - -> 写请求是写入 primary shard,然后同步给所有的 replica shard;读请求可以从 primary shard 或 replica shard 读取,采用的是随机轮询算法。 - -### 写数据底层原理 - -[![es-write-detail](https://github.com/doocs/advanced-java/raw/master/docs/high-concurrency/images/es-write-detail.png)](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/images/es-write-detail.png) - -先写入内存 buffer,在 buffer 里的时候数据是搜索不到的;同时将数据写入 translog 日志文件。 - -如果 buffer 快满了,或者到一定时间,就会将内存 buffer 数据 `refresh` 到一个新的 `segment file` 中,但是此时数据不是直接进入 `segment file` 磁盘文件,而是先进入 `os cache` 。这个过程就是 `refresh`。 - -每隔 1 秒钟,es 将 buffer 中的数据写入一个**新的** `segment file`,每秒钟会产生一个**新的磁盘文件** `segment file`,这个 `segment file` 中就存储最近 1 秒内 buffer 中写入的数据。 - -但是如果 buffer 里面此时没有数据,那当然不会执行 refresh 操作,如果 buffer 里面有数据,默认 1 秒钟执行一次 refresh 操作,刷入一个新的 segment file 中。 - -操作系统里面,磁盘文件其实都有一个东西,叫做 `os cache`,即操作系统缓存,就是说数据写入磁盘文件之前,会先进入 `os cache`,先进入操作系统级别的一个内存缓存中去。只要 `buffer` 中的数据被 refresh 操作刷入 `os cache`中,这个数据就可以被搜索到了。 - -为什么叫 es 是**准实时**的? `NRT`,全称 `near real-time`。默认是每隔 1 秒 refresh 一次的,所以 es 是准实时的,因为写入的数据 1 秒之后才能被看到。可以通过 es 的 `restful api` 或者 `java api`,**手动**执行一次 refresh 操作,就是手动将 buffer 中的数据刷入 `os cache`中,让数据立马就可以被搜索到。只要数据被输入 `os cache` 中,buffer 就会被清空了,因为不需要保留 buffer 了,数据在 translog 里面已经持久化到磁盘去一份了。 - -重复上面的步骤,新的数据不断进入 buffer 和 translog,不断将 `buffer` 数据写入一个又一个新的 `segment file` 中去,每次 `refresh` 完 buffer 清空,translog 保留。随着这个过程推进,translog 会变得越来越大。当 translog 达到一定长度的时候,就会触发 `commit` 操作。 - -commit 操作发生第一步,就是将 buffer 中现有数据 `refresh` 到 `os cache` 中去,清空 buffer。然后,将一个 `commit point` 写入磁盘文件,里面标识着这个 `commit point` 对应的所有 `segment file`,同时强行将 `os cache` 中目前所有的数据都 `fsync` 到磁盘文件中去。最后**清空** 现有 translog 日志文件,重启一个 translog,此时 commit 操作完成。 - -这个 commit 操作叫做 `flush`。默认 30 分钟自动执行一次 `flush`,但如果 translog 过大,也会触发 `flush`。flush 操作就对应着 commit 的全过程,我们可以通过 es api,手动执行 flush 操作,手动将 os cache 中的数据 fsync 强刷到磁盘上去。 - -translog 日志文件的作用是什么?你执行 commit 操作之前,数据要么是停留在 buffer 中,要么是停留在 os cache 中,无论是 buffer 还是 os cache 都是内存,一旦这台机器死了,内存中的数据就全丢了。所以需要将数据对应的操作写入一个专门的日志文件 `translog` 中,一旦此时机器宕机,再次重启的时候,es 会自动读取 translog 日志文件中的数据,恢复到内存 buffer 和 os cache 中去。 - -translog 其实也是先写入 os cache 的,默认每隔 5 秒刷一次到磁盘中去,所以默认情况下,可能有 5 秒的数据会仅仅停留在 buffer 或者 translog 文件的 os cache 中,如果此时机器挂了,会**丢失** 5 秒钟的数据。但是这样性能比较好,最多丢 5 秒的数据。也可以将 translog 设置成每次写操作必须是直接 `fsync` 到磁盘,但是性能会差很多。 - -实际上你在这里,如果面试官没有问你 es 丢数据的问题,你可以在这里给面试官炫一把,你说,其实 es 第一是准实时的,数据写入 1 秒后可以搜索到;可能会丢失数据的。有 5 秒的数据,停留在 buffer、translog os cache、segment file os cache 中,而不在磁盘上,此时如果宕机,会导致 5 秒的**数据丢失**。 - -**总结一下**,数据先写入内存 buffer,然后每隔 1s,将数据 refresh 到 os cache,到了 os cache 数据就能被搜索到(所以我们才说 es 从写入到能被搜索到,中间有 1s 的延迟)。每隔 5s,将数据写入 translog 文件(这样如果机器宕机,内存数据全没,最多会有 5s 的数据丢失),translog 大到一定程度,或者默认每隔 30mins,会触发 commit 操作,将缓冲区的数据都 flush 到 segment file 磁盘文件中。 - -> 数据写入 segment file 之后,同时就建立好了倒排索引。 - -### 删除/更新数据底层原理 - -如果是删除操作,commit 的时候会生成一个 `.del` 文件,里面将某个 doc 标识为 `deleted` 状态,那么搜索的时候根据 `.del` 文件就知道这个 doc 是否被删除了。 - -如果是更新操作,就是将原来的 doc 标识为 `deleted` 状态,然后新写入一条数据。 - -buffer 每 refresh 一次,就会产生一个 `segment file`,所以默认情况下是 1 秒钟一个 `segment file`,这样下来 `segment file` 会越来越多,此时会定期执行 merge。每次 merge 的时候,会将多个 `segment file` 合并成一个,同时这里会将标识为 `deleted` 的 doc 给**物理删除掉**,然后将新的 `segment file` 写入磁盘,这里会写一个 `commit point`,标识所有新的 `segment file`,然后打开 `segment file` 供搜索使用,同时删除旧的 `segment file`。 - -### 底层 lucene - -简单来说,lucene 就是一个 jar 包,里面包含了封装好的各种建立倒排索引的算法代码。我们用 Java 开发的时候,引入 lucene jar,然后基于 lucene 的 api 去开发就可以了。 - -通过 lucene,我们可以将已有的数据建立索引,lucene 会在本地磁盘上面,给我们组织索引的数据结构。 - -### 倒排索引 - -在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,文档 1 经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。 - -那么,倒排索引就是**关键词到文档** ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。 - -举个栗子。 - -有以下文档: - -| DocId | Doc | -| ----- | ---------------------------------------------- | -| 1 | 谷歌地图之父跳槽 Facebook | -| 2 | 谷歌地图之父加盟 Facebook | -| 3 | 谷歌地图创始人拉斯离开谷歌加盟 Facebook | -| 4 | 谷歌地图之父跳槽 Facebook 与 Wave 项目取消有关 | -| 5 | 谷歌地图之父拉斯加盟社交网站 Facebook | - -对文档进行分词之后,得到以下**倒排索引**。 - -| WordId | Word | DocIds | -| ------ | -------- | --------- | -| 1 | 谷歌 | 1,2,3,4,5 | -| 2 | 地图 | 1,2,3,4,5 | -| 3 | 之父 | 1,2,4,5 | -| 4 | 跳槽 | 1,4 | -| 5 | Facebook | 1,2,3,4,5 | -| 6 | 加盟 | 2,3,5 | -| 7 | 创始人 | 3 | -| 8 | 拉斯 | 3,5 | -| 9 | 离开 | 3 | -| 10 | 与 | 4 | -| .. | .. | .. | - -另外,实用的倒排索引还可以记录更多的信息,比如文档频率信息,表示在文档集合中有多少个文档包含某个单词。 - -那么,有了倒排索引,搜索引擎可以很方便地响应用户的查询。比如用户输入查询 `Facebook`,搜索系统查找倒排索引,从中读出包含这个单词的文档,这些文档就是提供给用户的搜索结果。 - -要注意倒排索引的两个重要细节: - -- 倒排索引中的所有词项对应一个或多个文档; -- 倒排索引中的词项**根据字典顺序升序排列** - -> 上面只是一个简单的栗子,并没有严格按照字典顺序升序排列。 - -## 参考资料 - -- **官方** - - [Elasticsearch 官网](https://www.elastic.co/cn/products/elasticsearch) - - [Elasticsearch Github](https://github.com/elastic/elasticsearch) - - [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) -- **文章** - - [Install Elasticsearch with RPM](https://www.elastic.co/guide/en/elasticsearch/reference/current/rpm.html#rpm) - - [https://www.ruanyifeng.com/blog/2017/08/elasticsearch.html](https://www.ruanyifeng.com/blog/2017/08/elasticsearch.html) - - [es-introduction](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/es-introduction.md) - - [es-write-query-search](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/es-write-query-search.md) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/03.Elasticsearch\347\256\200\344\273\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/03.Elasticsearch\347\256\200\344\273\213.md" deleted file mode 100644 index ac45741163..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/03.Elasticsearch\347\256\200\344\273\213.md" +++ /dev/null @@ -1,473 +0,0 @@ ---- -title: Elasticsearch 简介 -date: 2022-02-22 21:01:01 -order: 03 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -permalink: /pages/0fb506/ ---- - -# Elasticsearch 简介 - -Elasticsearch 是一个基于 Lucene 的搜索和数据分析工具,它提供了一个分布式服务。Elasticsearch 是遵从 Apache 开源条款的一款开源产品,是当前主流的企业级搜索引擎。 - -它用于全文搜索、结构化搜索、分析以及将这三者混合使用: - -- 维基百科使用 Elasticsearch 提供全文搜索并高亮关键字,以及**输入实时搜索(search-as-you-type)**和**搜索纠错(did-you-mean)**等搜索建议功能。 -- 英国卫报使用 Elasticsearch 结合用户日志和社交网络数据提供给他们的编辑以实时的反馈,以便及时了解公众对新发表的文章的回应。 -- StackOverflow 结合全文搜索与地理位置查询,以及**more-like-this**功能来找到相关的问题和答案。 -- Github 使用 Elasticsearch 检索 1300 亿行的代码。 - -## Elasticsearch 特点 - -- 分布式的实时文件存储,每个字段都被索引并可被搜索; -- 分布式的实时分析搜索引擎; -- 可弹性扩展到上百台服务器规模,处理 PB 级结构化或非结构化数据; -- 开箱即用(安装即可使用),它提供了许多合理的缺省值,并对初学者隐藏了复杂的搜索引擎理论。只需很少的学习既可在生产环境中使用。 - -## Elasticsearch 发展历史 - -- 2010 年 2 月 8 日,Elasticsearch 第一个公开版本发布。 - -- 2010 年 5 月 14 日,发布第一个具有里程碑意义的初始版本 **0.7.0** ,具有如下特征: -- Zen Discovery 自动发现模块; - - 支持 Groovy Client; -- 简单的插件管理机制; - - 更好地支持 icu 分词器; -- 更多的管理 api。 -- 2013 年初,GitHub 抛弃了 Solr,采取 ElasticSearch 来做其 PB 级的搜索。 - -- 2014 年 2 月 14 日,发布 **1.0.0** 版本,增加如下重要特性: -- 支持 Snapshot/Restore API 备份恢复 API; - - 支持聚合分析 Aggregations; -- 支持 cat api; - - 支持断路器; -- 引入 Doc values。 -- 2015 年 10 月 28 日,发布 **2.0.0** 版本,有如下重要特性: -- 增加了 Pipleline Aggregations; - - query/filter 查询合并,都合并到 query 中,根据不同的上下文执行不同的查询; -- 压缩存储可配置; - - Rivers 模块被移除; -- Multicast 组播发现被移除,成为一个插件,生产环境必须配置单播地址。 -- 2016 年 10 月 26 日,发布 **5.0.0** 版本,有如下重大特性变化: -- Lucene 6.x 的支持,磁盘空间少一半;索引时间少一半;查询性能提升 25%;支持 IPV6; - - Internal Engine 级别移除了用于避免同一文档并发更新的竞争锁,带来 15%-20% 的性能提升; -- Shrink API,它可将分片数进行收缩成它的因数,如之前你是 15 个分片,你可以收缩成 5 个或者 3 个又或者 1 个,那么我们就可以想象成这样一种场景,在写入压力非常大的收集阶段,设置足够多的索引,充分利用 shard 的并行写能力,索引写完之后收缩成更少的 shard,提高查询性能; - - 提供了第一个 Java 原生的 REST 客户端 SDK; -- IngestNode,之前如果需要对数据进行加工,都是在索引之前进行处理,比如 logstash 可以对日志进行结构化和转换,现在直接在 es 就可以处理了; - - 提供了 Painless 脚本,代替 Groovy 脚本; - - 移除 site plugins,就是说 head、bigdesk 都不能直接装 es 里面了,不过可以部署独立站点(反正都是静态文件)或开发 kibana 插件; - - 新增 Sliced Scroll 类型,现在 Scroll 接口可以并发来进行数据遍历了。每个 Scroll 请求,可以分成多个 Slice 请求,可以理解为切片,各 Slice 独立并行,利用 Scroll 重建或者遍历要快很多倍; - - 新增了 Profile API; - - 新增了 Rollover API; - - 新增 Reindex; - - 引入新的字段类型 Text/Keyword 来替换 String; - - 限制索引请求大小,避免大量并发请求压垮 ES; - - 限制单个请求的 shards 数量,默认 1000 个。 -- 2017 年 8 月 31 日,发布 **6.0.0** 版本,具有如下重要特性: -- 稀疏性 Doc Values 的支持; - - Index Sorting,即索引阶段的排序; -- 顺序号的支持,每个 es 的操作都有一个顺序编号(类似增量设计); - - 无缝滚动升级; -- 从 6.0 开始不支持一个 index 里面存在多个 type; - - Index-template inheritance,索引版本的继承,目前索引模板是所有匹配的都会合并,这样会造成索引模板有一些冲突问题, 6.0 将会只匹配一个,索引创建时也会进行验证; - - Load aware shard routing, 基于负载的请求路由,目前的搜索请求是全节点轮询,那么性能最慢的节点往往会造成整体的延迟增加,新的实现方式将基于队列的耗费时间自动调节队列长度,负载高的节点的队列长度将减少,让其他节点分摊更多的压力,搜索和索引都将基于这种机制; - - 已经关闭的索引将也支持 replica 的自动处理,确保数据可靠。 -- 2019 年 4 月 10 日,发布 **7.0.0** 版本,具有如下重要特性: -- 集群连接变化:TransportClient 被废弃,es7 的 java 代码,只能使用 restclient;对于 java 编程,建议采用 High-level-rest-client 的方式操作 ES 集群; - - ES 程序包默认打包 jdk:7.x 版本的程序包大小变成 300MB+,对比 6.x,包大了 200MB+,这正是 JDK 的大小; -- 采用基于 Lucene 9.0; - - 正式废除单个索引下多 Type 的支持,es6 时,官方就提到了 es7 会删除 type,并且 es6 时,已经规定每一个 index 只能有一个 type。在 es7 中,使用默认的 \_doc 作为 type,官方说在 8.x 版本会彻底移除 type。api 请求方式也发送变化,如获得某索引的某 ID 的文档:GET index/\_doc/id 其中 index 和 id 为具体的值; -- 引入了真正的内存断路器,它可以更精准地检测出无法处理的请求,并防止它们使单个节点不稳定; - - Zen2 是 Elasticsearch 的全新集群协调层,提高了可靠性、性能和用户体验,变得更快、更安全,并更易于使用。 - -## Elasticsearch 概念 - -下列有一些概念是 Elasticsearch 的核心。从一开始就理解这些概念将极大地帮助简化学习 Elasticsearch 的过程。 - -### 近实时(NRT) - -Elasticsearch 是一个近乎实时的搜索平台。这意味着**从索引文档到可搜索文档的时间有一点延迟**(通常是一秒)。 - -### 索引(Index) - -索引在不同语境,有着不同的含义 - -- 索引(名词):一个 **索引** 类似于传统关系数据库中的一个 **数据库** ,是一个存储关系型文档的容器。 索引 (_index_) 的复数词为 indices 或 indexes 。索引实际上是指向一个或者多个**物理分片**的**逻辑命名空间** 。 -- 索引(动词):索引一个文档 就是存储一个文档到一个 _索引_ (名词)中以便被检索和查询。这非常类似于 SQL 语句中的 `INSERT` 关键词,除了文档已存在时,新文档会替换旧文档情况之外。 -- 倒排索引:关系型数据库通过增加一个索引比如一个 B 树索引到指定的列上,以便提升数据检索速度。Elasticsearch 和 Lucene 使用了一个叫做 **倒排索引** 的结构来达到相同的目的。 - -索引的 Mapping 和 Setting - -- **`Mapping`** 定义文档字段的类型 -- **`Setting`** 定义不同的数据分布 - -示例: - -```json -{ - "settings": { ... any settings ... }, - "mappings": { - "type_one": { ... any mappings ... }, - "type_two": { ... any mappings ... }, - ... - } -} -``` - -#### 倒排索引 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220108215559.PNG) - -#### index template - -**`index template`**(索引模板)帮助用户设定 Mapping 和 Setting,并按照一定的规则,自动匹配到新创建的索引之上。 - -- 模板仅在一个索引被创建时,才会产生作用。修改模板不会影响已创建的索引。 -- 你可以设定多个索引模板,这些设置会被 merge 在一起。 -- 你可以指定 order 的数值,控制 merge 的过程。 - -当新建一个索引时 - -- 应用 ES 默认的 Mapping 和 Setting -- 应用 order 数值低的 index template 中的设定 -- 应用 order 数值高的 index template 中的设定,之前的设定会被覆盖 -- 应用创建索引是,用户所指定的 Mapping 和 Setting,并覆盖之前模板中的设定。 - -示例:创建默认索引模板 - -```bash -PUT _template/template_default -{ - "index_patterns": ["*"], - "order": 0, - "version": 1, - "settings": { - "number_of_shards": 1, - "number_of_replicas": 1 - } -} - -PUT /_template/template_test -{ - "index_patterns": ["test*"], - "order": 1, - "settings": { - "number_of_shards": 1, - "number_of_replicas": 2 - }, - "mappings": { - "date_detection": false, - "numeric_detection": true - } -} - -# 查看索引模板 -GET /_template/template_default -GET /_template/temp* - -#写入新的数据,index以test开头 -PUT testtemplate/_doc/1 -{ - "someNumber": "1", - "someDate": "2019/01/01" -} -GET testtemplate/_mapping -GET testtemplate/_settings - -PUT testmy -{ - "settings":{ - "number_of_replicas":5 - } -} - -PUT testmy/_doc/1 -{ - "key": "value" -} - -GET testmy/_settings -DELETE testmy -DELETE /_template/template_default -DELETE /_template/template_test -``` - -#### dynamic template - -- 根据 ES 识别的数据类型,结合字段名称,来动态设定字段类型 - - 所有的字符串类型都设定成 Keyword,或者关闭 keyword 字段。 - - is 开头的字段都设置成 boolean - - long\_ 开头的都设置成 long 类型 -- dynamic template 是定义在某个索引的 Mapping 中 -- template 有一个名称 -- 匹配规则是一个数组 -- 为匹配到字段设置 Mapping - -示例: - -```bash -#Dynaminc Mapping 根据类型和字段名 -DELETE my_index - -PUT my_index/_doc/1 -{ - "firstName": "Ruan", - "isVIP": "true" -} - -GET my_index/_mapping - -DELETE my_index -PUT my_index -{ - "mappings": { - "dynamic_templates": [ - { - "strings_as_boolean": { - "match_mapping_type": "string", - "match": "is*", - "mapping": { - "type": "boolean" - } - } - }, - { - "strings_as_keywords": { - "match_mapping_type": "string", - "mapping": { - "type": "keyword" - } - } - } - ] - } -} -GET my_index/_mapping - -DELETE my_index -#结合路径 -PUT my_index -{ - "mappings": { - "dynamic_templates": [ - { - "full_name": { - "path_match": "name.*", - "path_unmatch": "*.middle", - "mapping": { - "type": "text", - "copy_to": "full_name" - } - } - } - ] - } -} -GET my_index/_mapping - - -PUT my_index/_doc/1 -{ - "name": { - "first": "John", - "middle": "Winston", - "last": "Lennon" - } -} - -GET my_index/_search?q=full_name:John -DELETE my_index -``` - -### ~~类型(Type)~~ - -~~type 是一个逻辑意义上的分类或者叫分区,允许在同一索引中建立多个 type。本质是相当于一个过滤条件,高版本将会废弃 type 概念。~~ - -> ~~**6.0.0 版本及之后,废弃 type**~~ - -### 文档(Document) - -Elasticsearch 是面向文档的,**文档是所有可搜索数据的最小单位**。 - -Elasticsearch 使用 [_JSON_](http://en.wikipedia.org/wiki/Json) 作为文档的序列化格式。 - -在索引/类型中,可以根据需要存储任意数量的文档。 - -每个文档都有一个 **Unique ID** - -- 用户可以自己指定 -- 或通过 Elasticsearch 自动生成 - -#### 文档的元数据 - -一个文档不仅仅包含它的数据 ,也包含**元数据** —— 有关文档的信息。 - -- `_index`:文档在哪存放 -- `_type`:文档表示的对象类别 -- `_id`:文档唯一标识 -- `_source`:文档的原始 Json 数据 -- `_all`:整合所有字段内容到该字段,已被废除 -- `_version`:文档的版本信息 -- `_score`:相关性打分 - -示例: - -```json -{ - "_index": "megacorp", - "_type": "employee", - "_id": "1", - "_version": 1, - "found": true, - "_source": { - "first_name": "John", - "last_name": "Smith", - "age": 25, - "about": "I love to go rock climbing", - "interests": ["sports", "music"] - } -} -``` - -### 节点(Node) - -#### 节点简介 - -一个运行中的 Elasticsearch 实例称为一个**节点**。 - -Elasticsearch 实例本质上是一个 Java 进程。一台机器上可以运行多个 Elasticsearch 进程,但是生产环境建议一台机器上只运行一个 Elasticsearch 进程 - -每个节点都有名字,通过配置文件配置,或启动时通过 `-E node.name=node1` 指定。 - -每个节点在启动后,会分配一个 UID,保存在 `data` 目录下。 - -#### 节点类型 - -- **主节点(master node)**:每个节点都保存了集群的状态,只有 master 节点才能修改集群的状态信息(保证数据一致性)。**集群状态**,维护了以下信息: - - 所有的节点信息 - - 所有的索引和其相关的 mapping 和 setting 信息 - - 分片的路由信息 -- **候选节点(master eligible node)**:master eligible 节点可以参加选主流程。第一个启动的节点,会将自己选举为 mater 节点。 - - 每个节点启动后,默认为 master eligible 节点,可以通过配置 `node.master: false` 禁止 -- **数据节点(data node)**:负责保存分片数据。 -- **协调节点(coordinating node)**:负责接收客户端的请求,将请求分发到合适的接地那,最终把结果汇集到一起。每个 Elasticsearch 节点默认都是协调节点(coordinating node)。 -- **冷/热节点(warm/hot node)**:针对不同硬件配置的数据节点(data node),用来实现 Hot & Warm 架构,降低集群部署的成本。 -- **机器学习节点(machine learning node)**:负责执行机器学习的 Job,用来做异常检测。 - -#### 节点配置 - -| 配置参数 | 默认值 | 说明 | -| ----------- | ------ | ------------------------------------- | -| node.master | true | 是否为主节点 | -| node.data | true | 是否为数据节点 | -| node.ingest | true | | -| node.ml | true | 是否为机器学习节点(需要开启 x-pack) | - -> **建议** -> -> 开发环境中一个节点可以承担多种角色。但是,在生产环境中,节点应该设置为单一角色。 - -### 集群(Cluster) - -#### 集群简介 - -拥有相同 `cluster.name` 配置的 Elasticsearch 节点组成一个**集群**。 `cluster.name` 默认名为 `elasticsearch`,可以通过配置文件修改,或启动时通过 `-E cluster.name=xxx` 指定。 - -当有节点加入集群中或者从集群中移除节点时,集群将会重新平均分布所有的数据。 - -当一个节点被选举成为主节点时,它将负责管理集群范围内的所有变更,例如增加、删除索引,或者增加、删除节点等。 而主节点并不需要涉及到文档级别的变更和搜索等操作,所以当集群只拥有一个主节点的情况下,即使流量增加,它也不会成为瓶颈。 任何节点都可以成为主节点。 - -作为用户,我们可以将请求发送到集群中的任何节点 ,包括主节点。 每个节点都知道任意文档所处的位置,并且能够将我们的请求直接转发到存储我们所需文档的节点。 无论我们将请求发送到哪个节点,它都能负责从各个包含我们所需文档的节点收集回数据,并将最终结果返回給客户端。 Elasticsearch 对这一切的管理都是透明的。 - -#### 集群健康 - -Elasticsearch 的集群监控信息中包含了许多的统计数据,其中最为重要的一项就是 _集群健康_ , 它在 `status` 字段中展示为 `green` 、 `yellow` 或者 `red` 。 - -在一个不包含任何索引的空集群中,它将会有一个类似于如下所示的返回内容: - -```js -{ - "cluster_name" : "elasticsearch", - "status" : "green", - "timed_out" : false, - "number_of_nodes" : 1, - "number_of_data_nodes" : 1, - "active_primary_shards" : 5, - "active_shards" : 5, - "relocating_shards" : 0, - "initializing_shards" : 0, - "unassigned_shards" : 0, - "delayed_unassigned_shards" : 0, - "number_of_pending_tasks" : 0, - "number_of_in_flight_fetch" : 0, - "task_max_waiting_in_queue_millis" : 0, - "active_shards_percent_as_number" : 100.0 -} -``` - -`status` 字段指示着当前集群在总体上是否工作正常。它的三种颜色含义如下: - -- **`green`**:所有的主分片和副本分片都正常运行。 -- **`yellow`**:所有的主分片都正常运行,但不是所有的副本分片都正常运行。 -- **`red`**:有主分片没能正常运行。 - -### 分片(Shards) - -#### 分片简介 - -索引实际上是指向一个或者多个**物理分片**的**逻辑命名空间** 。 - -一个分片是一个底层的工作单元 ,它仅保存了全部数据中的一部分。一个分片可以视为一个 Lucene 的实例,并且它本身就是一个完整的搜索引擎。 我们的文档被存储和索引到分片内,但是应用程序是直接与索引而不是与分片进行交互。 - -Elasticsearch 是利用分片将数据分发到集群内各处的。分片是数据的容器,文档保存在分片内,分片又被分配到集群内的各个节点里。 当你的集群规模扩大或者缩小时, Elasticsearch 会自动的在各节点中迁移分片,使得数据仍然均匀分布在集群里。 - -#### 主分片和副分片 - -分片分为主分片(Primary Shard)和副分片(Replica Shard)。 - -主分片:用于解决数据水平扩展的问题。通过主分片,可以将数据分布到集群内不同节点上。 - -- 索引内任意一个文档都归属于一个主分片。 -- 主分片数在索引创建时指定,后序不允许修改,除非 Reindex - -副分片(Replica Shard):用于解决数据高可用的问题。副分片是主分片的拷贝。副本分片作为硬件故障时保护数据不丢失的冗余备份,并为搜索和返回文档等读操作提供服务。 - -- 副分片数可以动态调整 -- 增加副本数,还可以在一定程度上提高服务的可用性(读取的吞吐) - -对于生产环境中分片的设定,需要提前做好容量规划 - -分片数过小 - -- 无法水平扩展 -- 单个分片的数量太大,导致数据重新分配耗时 - -分片数过大 - -- 影响搜索结果的相关性打分,影响统计结果的准确性 -- 单节点上过多的分片,会导致资源浪费,同时也会影响性能 - -### 副本(Replicas) - -副本主要是针对主分片(Shards)的复制,Elasticsearch 中主分片可以拥有 0 个或多个的副本。 - -副本分片的主要目的就是为了故障转移。 - -分片副本很重要,主要有两个原因: - -- 它在分片或节点发生故障时提供高可用性。因此,副本分片永远不会在与其复制的主分片相同的节点; -- 副本分片也可以接受搜索的请求,可以并行搜索,从而提高系统的吞吐量。 - -> 每个 Elasticsearch 分片都是 Lucene 索引。单个 Lucene 索引中可以包含最大数量的文档。截止 LUCENE-5843,限制是 2,147,483,519(= `Integer.MAX_VALUE` - 128)文档。您可以使用\_cat/shardsAPI 监控分片大小。 - -## 参考资料 - -- [Elasticsearch 官网](https://www.elastic.co/) -- [Elasticsearch 简介](https://www.knowledgedict.com/tutorial/elasticsearch-intro.html) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/04.Elasticsearch\347\264\242\345\274\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/04.Elasticsearch\347\264\242\345\274\225.md" deleted file mode 100644 index adbf407050..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/04.Elasticsearch\347\264\242\345\274\225.md" +++ /dev/null @@ -1,473 +0,0 @@ ---- -title: Elasticsearch 索引 -date: 2022-02-22 21:01:01 -order: 04 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch - - 索引 -permalink: /pages/293175/ ---- - -# Elasticsearch 索引 - -## 索引管理操作 - -Elasticsearch 索引管理主要包括如何进行索引的创建、索引的删除、副本的更新、索引读写权限、索引别名的配置等等内容。 - -### 索引删除 - -ES 索引删除操作向 ES 集群的 http 接口发送指定索引的 delete http 请求即可,可以通过 curl 命令,具体如下: - -```bash -curl -X DELETE http://{es_host}:{es_http_port}/{index} -``` - -如果删除成功,它会返回如下信息,具体示例如下: - -```bash -curl -X DELETE http://10.10.10.66:9200/my_index?pretty -``` - -为了返回的信息便于读取,增加了 pretty 参数: - -```bash -{ - "acknowledged" : true -} -``` - -### 索引别名 - -ES 的索引别名就是给一个索引或者多个索引起的另一个名字,典型的应用场景是针对索引使用的平滑切换。 - -首先,创建索引 my_index,然后将别名 my_alias 指向它,示例如下: - -```bash -PUT /my_index -PUT /my_index/_alias/my_alias -``` - -也可以通过如下形式: - -```bash -POST /_aliases -{ - "actions": [ - { "add": { "index": "my_index", "alias": "my_alias" }} - ] -} -``` - -也可以在一次请求中增加别名和移除别名混合使用: - -```bash -POST /_aliases -{ - "actions": [ - { "remove": { "index": "my_index", "alias": "my_alias" }} - { "add": { "index": "my_index_v2", "alias": "my_alias" }} - ] -} -``` - -> 需要注意的是,如果别名与索引是一对一的,使用别名索引文档或者查询文档是可以的,但是如果别名和索引是一对多的,使用别名会发生错误,因为 ES 不知道把文档写入哪个索引中去或者从哪个索引中读取文档。 - -ES 索引别名有个典型的应用场景是平滑切换,更多细节可以查看 [Elasticsearch(ES)索引零停机(无需重启)无缝平滑切换的方法](https://www.knowledgedict.com/tutorial/elasticsearch-index-smooth-shift.html)。 - -## Settings 详解 - -Elasticsearch 索引的配置项主要分为**静态配置属性**和**动态配置属性**,静态配置属性是索引创建后不能修改,而动态配置属性则可以随时修改。 - -ES 索引设置的 api 为 **_`_settings`_**,完整的示例如下: - -```bash -PUT /my_index -{ - "settings": { - "index": { - "number_of_shards": "1", - "number_of_replicas": "1", - "refresh_interval": "60s", - "analysis": { - "filter": { - "tsconvert": { - "type": "stconvert", - "convert_type": "t2s", - "delimiter": "," - }, - "synonym": { - "type": "synonym", - "synonyms_path": "analysis/synonyms.txt" - } - }, - "analyzer": { - "ik_max_word_synonym": { - "filter": [ - "synonym", - "tsconvert", - "standard", - "lowercase", - "stop" - ], - "tokenizer": "ik_max_word" - }, - "ik_smart_synonym": { - "filter": [ - "synonym", - "standard", - "lowercase", - "stop" - ], - "tokenizer": "ik_smart" - } - }, - "mapping": { - "coerce": "false", - "ignore_malformed": "false" - }, - "indexing": { - "slowlog": { - "threshold": { - "index": { - "warn": "2s", - "info": "1s" - } - } - } - }, - "provided_name": "hospital_202101070533", - "query": { - "default_field": "timestamp", - "parse": { - "allow_unmapped_fields": "false" - } - }, - "requests": { - "cache": { - "enable": "true" - } - }, - "search": { - "slowlog": { - "threshold": { - "fetch": { - "warn": "1s", - "info": "200ms" - }, - "query": { - "warn": "1s", - "info": "500ms" - } - } - } - } - } - } -} -``` - -### 固定属性 - -- **_`index.creation_date`_**:顾名思义索引的创建时间戳。 -- **_`index.uuid`_**:索引的 uuid 信息。 -- **_`index.version.created`_**:索引的版本号。 - -### 索引静态配置 - -- **_`index.number_of_shards`_**:索引的主分片数,默认值是 **_`5`_**。这个配置在索引创建后不能修改;在 es 层面,可以通过 **_`es.index.max_number_of_shards`_** 属性设置索引最大的分片数,默认为 **_`1024`_**。 -- **_`index.codec`_**:数据存储的压缩算法,默认值为 **_`LZ4`_**,可选择值还有 **_`best_compression`_**,它比 LZ4 可以获得更好的压缩比(即占据较小的磁盘空间,但存储性能比 LZ4 低)。 -- **_`index.routing_partition_size`_**:路由分区数,如果设置了该参数,其路由算法为:`( hash(_routing) + hash(_id) % index.routing_parttion_size ) % number_of_shards`。如果该值不设置,则路由算法为 `hash(_routing) % number_of_shardings`,`_routing` 默认值为 `_id`。 - -静态配置里,有重要的部分是配置分析器(config analyzers)。 - -- **`index.analysis`** - - :分析器最外层的配置项,内部主要分为 char_filter、tokenizer、filter 和 analyzer。 - - - **_`char_filter`_**:定义新的字符过滤器件。 - - **_`tokenizer`_**:定义新的分词器。 - - **_`filter`_**:定义新的 token filter,如同义词 filter。 - - **_`analyzer`_**:配置新的分析器,一般是 char_filter、tokenizer 和一些 token filter 的组合。 - -### 索引动态配置 - -- **_`index.number_of_replicas`_**:索引主分片的副本数,默认值是 **_`1`_**,该值必须大于等于 0,这个配置可以随时修改。 -- **_`index.refresh_interval`_**:执行新索引数据的刷新操作频率,该操作使对索引的最新更改对搜索可见,默认为 **_`1s`_**。也可以设置为 **_`-1`_** 以禁用刷新。更详细信息参考 [Elasticsearch 动态修改 refresh_interval 刷新间隔设置](https://www.knowledgedict.com/tutorial/elasticsearch-refresh_interval-settings.html)。 - -## Mapping 详解 - -在 Elasticsearch 中,**`Mapping`**(映射),用来定义一个文档以及其所包含的字段如何被存储和索引,可以在映射中事先定义字段的数据类型、字段的权重、分词器等属性,就如同在关系型数据库中创建数据表时会设置字段的类型。 - -Mapping 会把 json 文档映射成 Lucene 所需要的扁平格式 - -一个 Mapping 属于一个索引的 Type - -- 每个文档都属于一个 Type -- 一个 Type 有一个 Mapping 定义 -- 7.0 开始,不需要在 Mapping 定义中指定 type 信息 - -### 映射分类 - -在 Elasticsearch 中,映射可分为静态映射和动态映射。在关系型数据库中写入数据之前首先要建表,在建表语句中声明字段的属性,在 Elasticsearch 中,则不必如此,Elasticsearch 最重要的功能之一就是让你尽可能快地开始探索数据,文档写入 Elasticsearch 中,它会根据字段的类型自动识别,这种机制称为**动态映射**,而**静态映射**则是写入数据之前对字段的属性进行手工设置。 - -#### 静态映射 - -**静态映射**是在创建索引时手工指定索引映射。静态映射和 SQL 中在建表语句中指定字段属性类似。相比动态映射,通过静态映射可以添加更详细、更精准的配置信息。 - -如何定义一个 Mapping - -```bash -PUT /books -{ - "mappings": { - "type_one": { ... any mappings ... }, - "type_two": { ... any mappings ... }, - ... - } -} -``` - -#### 动态映射 - -**动态映射**是一种偷懒的方式,可直接创建索引并写入文档,文档中字段的类型是 Elasticsearch **自动识别**的,不需要在创建索引的时候设置字段的类型。在实际项目中,如果遇到的业务在导入数据之前不确定有哪些字段,也不清楚字段的类型是什么,使用动态映射非常合适。当 Elasticsearch 在文档中碰到一个以前没见过的字段时,它会利用动态映射来决定该字段的类型,并自动把该字段添加到映射中,根据字段的取值自动推测字段类型的规则见下表: - -| JSON 格式的数据 | 自动推测的字段类型 | -| :-------------- | :--------------------------------------------------------------------------------- | -| null | 没有字段被添加 | -| true or false | boolean 类型 | -| 浮点类型数字 | float 类型 | -| 数字 | long 类型 | -| JSON 对象 | object 类型 | -| 数组 | 由数组中第一个非空值决定 | -| string | 有可能是 date 类型(若开启日期检测)、double 或 long 类型、text 类型、keyword 类型 | - -下面举一个例子认识动态 mapping,在 Elasticsearch 中创建一个新的索引并查看它的 mapping,命令如下: - -```bash -PUT books -GET books/_mapping -``` - -此时 books 索引的 mapping 是空的,返回结果如下: - -```json -{ - "books": { - "mappings": {} - } -} -``` - -再往 books 索引中写入一条文档,命令如下: - -```bash -PUT books/it/1 -{ - "id": 1, - "publish_date": "2019-11-10", - "name": "master Elasticsearch" -} -``` - -文档写入完成之后,再次查看 mapping,返回结果如下: - -```json -{ - "books": { - "mappings": { - "properties": { - "id": { - "type": "long" - }, - "name": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "publish_date": { - "type": "date" - } - } - } - } -} -``` - -使用动态 mapping 要结合实际业务需求来综合考虑,如果将 Elasticsearch 当作主要的数据存储使用,并且希望出现未知字段时抛出异常来提醒你注意这一问题,那么开启动态 mapping 并不适用。在 mapping 中可以通过 `dynamic` 设置来控制是否自动新增字段,接受以下参数: - -- **`true`**:默认值为 true,自动添加字段。 -- **`false`**:忽略新的字段。 -- **`strict`**:严格模式,发现新的字段抛出异常。 - -### 基础类型 - -| 类型 | 关键字 | -| :--------- | :------------------------------------------------------------------ | -| 字符串类型 | string、text、keyword | -| 数字类型 | long、integer、short、byte、double、float、half_float、scaled_float | -| 日期类型 | date | -| 布尔类型 | boolean | -| 二进制类型 | binary | -| 范围类型 | range | - -### 复杂类型 - -| 类型 | 关键字 | -| :------- | :----- | -| 数组类型 | array | -| 对象类型 | object | -| 嵌套类型 | nested | - -### 特殊类型 - -| 类型 | 关键字 | -| :----------- | :---------- | -| 地理类型 | geo_point | -| 地理图形类型 | geo_shape | -| IP 类型 | ip | -| 范围类型 | completion | -| 令牌计数类型 | token_count | -| 附件类型 | attachment | -| 抽取类型 | percolator | - -### Mapping 属性 - -Elasticsearch 的 mapping 中的字段属性非常多,具体如下表格: - -| 属性名 | 描述 | -| :- | :- | | -| **_`type`_** | 字段类型,常用的有 text、integer 等等。 | -| **_`index`_** | 当前字段是否被作为索引。可选值为 **_`true`_**,默认为 true。 | -| **_`store`_** | 是否存储指定字段,可选值为 **_`true`_** | **_`false`_**,设置 true 意味着需要开辟单独的存储空间为这个字段做存储,而且这个存储是独立于 **_`_source`_** 的存储的。 | -| **_`norms`_** | 是否使用归一化因子,可选值为 **_`true`_** | **_`false`_**,不需要对某字段进行打分排序时,可禁用它,节省空间;_type_ 为 _text_ 时,默认为 _true_;而 _type_ 为 _keyword_ 时,默认为 _false_。 | -| **_`index_options`_** | 索引选项控制添加到倒排索引(Inverted Index)的信息,这些信息用于搜索(Search)和高亮显示:**_`docs`_**:只索引文档编号(Doc Number);**_`freqs`_**:索引文档编号和词频率(term frequency);**_`positions`_**:索引文档编号,词频率和词位置(序号);**_`offsets`_**:索引文档编号,词频率,词偏移量(开始和结束位置)和词位置(序号)。默认情况下,被分析的字符串(analyzed string)字段使用 _positions_,其他字段默认使用 _docs_。此外,需要注意的是 _index_option_ 是 elasticsearch 特有的设置属性;临近搜索和短语查询时,_index_option_ 必须设置为 _offsets_,同时高亮也可使用 postings highlighter。 | -| **_`term_vector`_** | 索引选项控制词向量相关信息:**_`no`_**:默认值,表示不存储词向量相关信息;**_`yes`_**:只存储词向量信息;**_`with_positions`_**:存储词项和词项位置;**_`with_offsets`_**:存储词项和字符偏移位置;**_`with_positions_offsets`_**:存储词项、词项位置、字符偏移位置。_term_vector_ 是 lucene 层面的索引设置。 | -| **_`similarity`_** | 指定文档相似度算法(也可以叫评分模型):**_`BM25`_**:ES 5 之后的默认设置。 | -| **_`copy_to`_** | 复制到自定义 \_all 字段,值是数组形式,即表明可以指定多个自定义的字段。 | -| **_`analyzer`_** | 指定索引和搜索时的分析器,如果同时指定 _search_analyzer_ 则搜索时会优先使用 _search_analyzer_。 | -| **_`search_analyzer`_** | 指定搜索时的分析器,搜索时的优先级最高。 | -| **_`null_value`_** | 用于需要对 Null 值实现搜索的场景,只有 Keyword 类型支持此配置。 | - -## 索引查询 - -### 多个 index、多个 type 查询 - -Elasticsearch 的搜索 api 支持**一个索引(index)的多个类型(type)查询**以及**多个索引(index)**的查询。 - -例如,我们可以搜索 twitter 索引下面所有匹配条件的所有类型中文档,如下: - -```bash -GET /twitter/_search?q=user:shay -``` - -我们也可以搜索一个索引下面指定多个 type 下匹配条件的文档,如下: - -```bash -GET /twitter/tweet,user/_search?q=user:banon -``` - -我们也可以搜索多个索引下匹配条件的文档,如下: - -```bash -GET /twitter,elasticsearch/_search?q=tags:wow -``` - -此外我们也可以搜索所有索引下匹配条件的文档,用\_all 表示所有索引,如下: - -```bash -GET /_all/_search?q=tags:wow -``` - -甚至我们可以搜索所有索引及所有 type 下匹配条件的文档,如下: - -```bash -GET /_search?q=tags:wow -``` - -### URI 搜索 - -Elasticsearch 支持用 uri 搜索,可用 get 请求里面拼接相关的参数,并用 curl 相关的命令就可以进行测试。 - -如下有一个示例: - -```bash -GET twitter/_search?q=user:kimchy -``` - -如下是上一个请求的相应实体: - -```json -{ - "timed_out": false, - "took": 62, - "_shards": { - "total": 1, - "successful": 1, - "skipped": 0, - "failed": 0 - }, - "hits": { - "total": 1, - "max_score": 1.3862944, - "hits": [ - { - "_index": "twitter", - "_type": "_doc", - "_id": "0", - "_score": 1.3862944, - "_source": { - "user": "kimchy", - "date": "2009-11-15T14:12:12", - "message": "trying out Elasticsearch", - "likes": 0 - } - } - ] - } -} -``` - -URI 中允许的参数: - -| 名称 | 描述 | -| :--------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| q | 查询字符串,映射到 query_string 查询 | -| df | 在查询中未定义字段前缀时使用的默认字段 | -| analyzer | 查询字符串时指定的分词器 | -| analyze_wildcard | 是否允许通配符和前缀查询,默认设置为 false | -| batched_reduce_size | 应在协调节点上一次减少的分片结果数。如果请求中潜在的分片数量很大,则应将此值用作保护机制,以减少每个搜索请求的内存开销 | -| default_operator | 默认使用的匹配运算符,可以是*AND*或者*OR*,默认是*OR* | -| lenient | 如果设置为 true,将会忽略由于格式化引起的问题(如向数据字段提供文本),默认为 false | -| explain | 对于每个 hit,包含了具体如何计算得分的解释 | -| \_source | 请求文档内容的参数,默认 true;设置 false 的话,不返回\_source 字段,可以使用**\_source_include**和**\_source_exclude**参数分别指定返回字段和不返回的字段 | -| stored_fields | 指定每个匹配返回的文档中的存储字段,多个用逗号分隔。不指定任何值将导致没有字段返回 | -| sort | 排序方式,可以是*fieldName*、*fieldName:asc*或者*fieldName:desc*的形式。fieldName 可以是文档中的实际字段,也可以是诸如\_score 字段,其表示基于分数的排序。此外可以指定多个 sort 参数(顺序很重要) | -| track_scores | 当排序时,若设置 true,返回每个命中文档的分数 | -| track_total_hits | 是否返回匹配条件命中的总文档数,默认为 true | -| timeout | 设置搜索的超时时间,默认无超时时间 | -| terminate_after | 在达到查询终止条件之前,指定每个分片收集的最大文档数。如果设置,则在响应中多了一个 terminated_early 的布尔字段,以指示查询执行是否实际上已终止。默认为 no terminate_after | -| from | 从第几条(索引以 0 开始)结果开始返回,默认为 0 | -| size | 返回命中的文档数,默认为 10 | -| search_type | 搜索的方式,可以是*dfs_query_then_fetch*或*query_then_fetch*。默认为*query_then_fetch* | -| allow_partial_search_results | 是否可以返回部分结果。如设置为 false,表示如果请求产生部分结果,则设置为返回整体故障;默认为 true,表示允许请求在超时或部分失败的情况下获得部分结果 | - -### 查询流程 - -在 Elasticsearch 中,查询是一个比较复杂的执行模式,因为我们不知道那些 document 会被匹配到,任何一个 shard 上都有可能,所以一个 search 请求必须查询一个索引或多个索引里面的所有 shard 才能完整的查询到我们想要的结果。 - -找到所有匹配的结果是查询的第一步,来自多个 shard 上的数据集在分页返回到客户端之前会被合并到一个排序后的 list 列表,由于需要经过一步取 top N 的操作,所以 search 需要进过两个阶段才能完成,分别是 query 和 fetch。 - -## 参考资料 - -- [Elasticsearch 官网](https://www.elastic.co/) -- [Elasticsearch 索引映射类型及 mapping 属性详解](https://www.knowledgedict.com/tutorial/elasticsearch-index-mapping.html) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/05.Elasticsearch\346\237\245\350\257\242.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/05.Elasticsearch\346\237\245\350\257\242.md" deleted file mode 100644 index c5d158e23f..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/05.Elasticsearch\346\237\245\350\257\242.md" +++ /dev/null @@ -1,1635 +0,0 @@ ---- -title: Elasticsearch 查询 -date: 2022-01-18 08:01:08 -order: 05 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch - - 查询 -permalink: /pages/83bd15/ ---- - -# Elasticsearch 查询 - -Elasticsearch 查询语句采用基于 RESTful 风格的接口封装成 JSON 格式的对象,称之为 Query DSL。Elasticsearch 查询分类大致分为**全文查询**、**词项查询**、**复合查询**、**嵌套查询**、**位置查询**、**特殊查询**。Elasticsearch 查询从机制分为两种,一种是根据用户输入的查询词,通过排序模型计算文档与查询词之间的**相关度**,并根据评分高低排序返回;另一种是**过滤机制**,只根据过滤条件对文档进行过滤,不计算评分,速度相对较快。 - -## 全文查询 - -ES 全文查询主要用于在全文字段上,主要考虑查询词与文档的相关性(Relevance)。 - -### intervals query - -[**`intervals query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-intervals-query.html) 根据匹配词的顺序和近似度返回文档。 - -intervals query 使用**匹配规则**,这些规则应用于指定字段中的 term。 - -示例:下面示例搜索 `query` 字段,搜索值是 `my favorite food`,没有任何间隙;然后是 `my_text` 字段搜索匹配 `hot water`、`cold porridge` 的 term。 - -当 my_text 中的值为 `my favorite food is cold porridge` 时,会匹配成功,但是 `when it's cold my favorite food is porridge` 则匹配失败 - -```bash -POST _search -{ - "query": { - "intervals" : { - "my_text" : { - "all_of" : { - "ordered" : true, - "intervals" : [ - { - "match" : { - "query" : "my favorite food", - "max_gaps" : 0, - "ordered" : true - } - }, - { - "any_of" : { - "intervals" : [ - { "match" : { "query" : "hot water" } }, - { "match" : { "query" : "cold porridge" } } - ] - } - } - ] - } - } - } - } -} -``` - -### match query - -[**`match query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) **用于搜索单个字段**,首先会针对查询语句进行解析(经过 analyzer),主要是对查询语句进行分词,分词后查询语句的任何一个词项被匹配,文档就会被搜到,默认情况下相当于对分词后词项进行 or 匹配操作。 - -[**`match query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) 是执行全文搜索的标准查询,包括模糊匹配选项。 - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "match": { - "customer_full_name": { - "query": "George Hubbard" - } - } - } -} -``` - -等同于 `or` 匹配操作,如下: - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "match": { - "customer_full_name": { - "query": "George Hubbard", - "operator": "or" - } - } - } -} -``` - -#### match query 简写 - -可以通过组合 `` 和 `query` 参数来简化匹配查询语法。 - -示例: - -```bash -GET /_search -{ - "query": { - "match": { - "message": "this is a test" - } - } -} -``` - -#### match query 如何工作 - -匹配查询是布尔类型。这意味着会对提供的文本进行分析,分析过程从提供的文本构造一个布尔查询。 `operator` 参数可以设置为 `or` 或 `and` 来控制布尔子句(默认为 `or`)。可以使用 [`minimum_should_match`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html) 参数设置要匹配的可选 `should` 子句的最小数量。 - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "match": { - "customer_full_name": { - "query": "George Hubbard", - "operator": "and" - } - } - } -} -``` - -可以设置 `analyzer` 来控制哪个分析器将对文本执行分析过程。它默认为字段显式映射定义或默认搜索分析器。 - -`lenient` 参数可以设置为 `true` 以忽略由数据类型不匹配导致的异常,例如尝试使用文本查询字符串查询数字字段。默认为 `false`。 - -#### match query 的模糊查询 - -`fuzziness` 允许基于被查询字段的类型进行模糊匹配。请参阅 [Fuzziness](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness) 的配置。 - -在这种情况下可以设置 `prefix_length` 和 `max_expansions` 来控制模糊匹配。如果设置了模糊选项,查询将使用 `top_terms_blended_freqs_${max_expansions}` 作为其重写方法,`fuzzy_rewrite` 参数允许控制查询将如何被重写。 - -默认情况下允许模糊倒转 (`ab` → `ba`),但可以通过将 `fuzzy_transpositions` 设置为 `false` 来禁用。 - -```bash -GET /_search -{ - "query": { - "match": { - "message": { - "query": "this is a testt", - "fuzziness": "AUTO" - } - } - } -} -``` - -#### zero terms 查询 - -如果使用的分析器像 stop 过滤器一样删除查询中的所有标记,则默认行为是不匹配任何文档。可以使用 `zero_terms_query` 选项来改变默认行为,它接受 `none`(默认)和 `all` (相当于 `match_all` 查询)。 - -```bash -GET /_search -{ - "query": { - "match": { - "message": { - "query": "to be or not to be", - "operator": "and", - "zero_terms_query": "all" - } - } - } -} -``` - -### match_bool_prefix query - -[**`match_bool_prefix query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-bool-prefix-query.html) 分析其输入并根据这些词构造一个布尔查询。除了最后一个术语之外的每个术语都用于术语查询。最后一个词用于 `prefix query`。 - -示例: - -```bash -GET /_search -{ - "query": { - "match_bool_prefix" : { - "message" : "quick brown f" - } - } -} -``` - -等价于 - -```bash -GET /_search -{ - "query": { - "bool" : { - "should": [ - { "term": { "message": "quick" }}, - { "term": { "message": "brown" }}, - { "prefix": { "message": "f"}} - ] - } - } -} -``` - -`match_bool_prefix query` 和 `match_phrase_prefix query` 之间的一个重要区别是:`match_phrase_prefix query` 将其 term 匹配为短语,但 `match_bool_prefix query` 可以在任何位置匹配其 term。 - -上面的示例 `match_bool_prefix query` 查询可以匹配包含 `quick brown fox` 的字段,但它也可以快速匹配 `brown fox`。它还可以匹配包含 `quick`、`brown` 和以 `f` 开头的字段,出现在任何位置。 - -### match_phrase query - -[**`match_phrase query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) 即短语匹配,首先会把 query 内容分词,分词器可以自定义,同时文档还要满足以下两个条件才会被搜索到: - -1. **分词后所有词项都要出现在该字段中(相当于 and 操作)**。 -2. **字段中的词项顺序要一致**。 - -例如,有以下 3 个文档,使用 **`match_phrase`** 查询 "How are you",只有前两个文档会被匹配: - -```bash -PUT demo/_create/1 -{ "desc": "How are you" } - -PUT demo/_create/2 -{ "desc": "How are you, Jack?"} - -PUT demo/_create/3 -{ "desc": "are you"} - -GET demo/_search -{ - "query": { - "match_phrase": { - "desc": "How are you" - } - } -} -``` - -> 说明: -> -> 一个被认定为和短语 How are you 匹配的文档,必须满足以下这些要求: -> -> - How、 are 和 you 需要全部出现在域中。 -> - are 的位置应该比 How 的位置大 1 。 -> - you 的位置应该比 How 的位置大 2 。 - -### match_phrase_prefix query - -[**`match_phrase_prefix query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html) 和 [**`match_phrase query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) 类似,只不过 [**`match_phrase_prefix query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html) 最后一个 term 会被作为前缀匹配。 - -```bash -GET demo/_search -{ - "query": { - "match_phrase_prefix": { - "desc": "are yo" - } - } -} -``` - -### multi_match query - -[**`multi_match query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html) 是 **`match query`** 的升级,**用于搜索多个字段**。 - -示例: - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "multi_match": { - "query": 34.98, - "fields": [ - "taxful_total_price", - "taxless_total_price" - ] - } - } -} -``` - -**`multi_match query`** 的搜索字段可以使用通配符指定,示例如下: - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "multi_match": { - "query": 34.98, - "fields": [ - "taxful_*", - "taxless_total_price" - ] - } - } -} -``` - -同时,也可以用**指数符指定搜索字段的权重**。 - -示例:指定 taxful_total_price 字段的权重是 taxless_total_price 字段的 3 倍,命令如下: - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "multi_match": { - "query": 34.98, - "fields": [ - "taxful_total_price^3", - "taxless_total_price" - ] - } - } -} -``` - -### combined_fields query - -[**`combined_fields query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-combined-fields-query.html) 支持搜索多个文本字段,就好像它们的内容已被索引到一个组合字段中一样。该查询会生成以 term 为中心的输入字符串视图:首先它将查询字符串解析为独立的 term,然后在所有字段中查找每个 term。当匹配结果可能跨越多个文本字段时,此查询特别有用,例如文章的标题、摘要和正文: - -```bash -GET /_search -{ - "query": { - "combined_fields" : { - "query": "database systems", - "fields": [ "title", "abstract", "body"], - "operator": "and" - } - } -} -``` - -#### 字段前缀权重 - -字段前缀权重根据组合字段模型进行计算。例如,如果 title 字段的权重为 2,则匹配度打分时会将 title 中的每个 term 形成的组合字段,按出现两次进行打分。 - -### common_terms query - -> 7.3.0 废弃 - -[**`common_terms query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-common-terms-query.html) 是一种在不牺牲性能的情况下替代停用词提高搜索准确率和召回率的方案。 - -查询中的每个词项都有一定的代价,以搜索“The brown fox”为例,query 会被解析成三个词项“the”“brown”和“fox”,每个词项都会到索引中执行一次查询。很显然包含“the”的文档非常多,相比其他词项,“the”的重要性会低很多。传统的解决方案是把“the”当作停用词处理,去除停用词之后可以减少索引大小,同时在搜索时减少对停用词的收缩。 - -虽然停用词对文档评分影响不大,但是当停用词仍然有重要意义的时候,去除停用词就不是完美的解决方案了。如果去除停用词,就无法区分“happy”和“not happy”, “The”“To be or not to be”就不会在索引中存在,搜索的准确率和召回率就会降低。 - -common_terms query 提供了一种解决方案,它把 query 分词后的词项分成重要词项(低频词项)和不重要的词项(高频词,也就是之前的停用词)。在搜索的时候,首先搜索和重要词项匹配的文档,这些文档是词项出现较少并且词项对其评分影响较大的文档。然后执行第二次查询,搜索对评分影响较小的高频词项,但是不计算所有文档的评分,而是只计算第一次查询已经匹配的文档得分。如果一个查询中只包含高频词,那么会通过 and 连接符执行一个单独的查询,换言之,会搜索所有的词项。 - -词项是高频词还是低频词是通过 cutoff frequency 来设置阀值的,取值可以是绝对频率(频率大于 1)或者相对频率(0 ~ 1)。common_terms query 最有趣之处在于它能自适应特定领域的停用词,例如,在视频托管网站上,诸如“clip”或“video”之类的高频词项将自动表现为停用词,无须保留手动列表。 - -例如,文档频率高于 0.1% 的词项将会被当作高频词项,词频之间可以用 low_freq_operator、high_freq_operator 参数连接。设置低频词操作符为“and”使所有的低频词都是必须搜索的,示例代码如下: - -```bash -GET books/_search -{ - "query": { - "common": { - "body": { - "query": "nelly the elephant as a cartoon", - "cutoff_frequency": 0.001, - "low_freq_operator": "and" - } - } - } -} -``` - -上述操作等价于: - -```bash -GET books/_search -{ - "query": { - "bool": { - "must": [ - { "term": { "body": "nelly" } }, - { "term": { "body": "elephant" } }, - { "term": { "body": "cartoon" } } - ], - "should": [ - { "term": { "body": "the" } }, - { "term": { "body": "as" } }, - { "term": { "body": "a" } } - ] - } - } -} -``` - -### query_string query - -[**`query_string query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html) 是与 Lucene 查询语句的语法结合非常紧密的一种查询,允许在一个查询语句中使用多个特殊条件关键字(如:AND | OR | NOT)对多个字段进行查询,建议熟悉 Lucene 查询语法的用户去使用。 - -用户可以使用 query_string query 来创建包含通配符、跨多个字段的搜索等复杂搜索。虽然通用,但查询是严格的,如果查询字符串包含任何无效语法,则会返回错误。 - -示例: - -```bash -GET /_search -{ - "query": { - "query_string": { - "query": "(new york city) OR (big apple)", - "default_field": "content" - } - } -} -``` - -### simple_query_string query - -[**`simple_query_string query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html) 是一种适合直接暴露给用户,并且具有非常完善的查询语法的查询语句,接受 Lucene 查询语法,解析过程中发生错误不会抛出异常。 - -虽然语法比 [**`query_string query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html) 更严格,但 [**`simple_query_string query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html) 不会返回无效语法的错误。相反,它会忽略查询字符串的任何无效部分。 - -示例: - -```bash -GET /_search -{ - "query": { - "simple_query_string" : { - "query": "\"fried eggs\" +(eggplant | potato) -frittata", - "fields": ["title^5", "body"], - "default_operator": "and" - } - } -} -``` - -#### simple_query_string 语义 - -- `+`:等价于 AND 操作 -- `|`:等价于 OR 操作 -- `-`:相当于 NOT 操作 -- `"`:包装一些标记以表示用于搜索的短语 -- `*`:词尾表示前缀查询 -- `(` and `)`:表示优先级 -- `~N`:词尾表示表示编辑距离(模糊性) -- `~N`:在一个短语之后表示溢出量 - -注意:要使用上面的字符,请使用反斜杠 `/` 对其进行转义。 - -### 全文查询完整示例 - -```bash -#设置 position_increment_gap -DELETE groups -PUT groups -{ - "mappings": { - "properties": { - "names":{ - "type": "text", - "position_increment_gap": 0 - } - } - } -} - -GET groups/_mapping - -POST groups/_doc -{ - "names": [ "John Water", "Water Smith"] -} - -POST groups/_search -{ - "query": { - "match_phrase": { - "names": { - "query": "Water Water", - "slop": 100 - } - } - } -} - -POST groups/_search -{ - "query": { - "match_phrase": { - "names": "Water Smith" - } - } -} - -DELETE groups -``` - -## 词项查询 - -**`Term`(词项)是表达语意的最小单位**。搜索和利用统计语言模型进行自然语言处理都需要处理 Term。 - -全文查询在执行查询之前会分析查询字符串。 - -与全文查询不同,词项查询不会分词,而是将输入作为一个整体,在倒排索引中查找准确的词项。并且使用相关度计算公式为每个包含该词项的文档进行相关度计算。一言以概之:**词项查询是对词项进行精确匹配**。词项查询通常用于结构化数据,如数字、日期和枚举类型。 - -词项查询有以下类型: - -- **[`exists` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html)** -- **[`fuzzy` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html)** -- **[`ids` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html)** -- **[`prefix` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html)** -- **[`range` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html)** -- **[`regexp` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html)** -- **[`term` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html)** -- **[`terms` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html)** -- **[`type` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-type-query.html)** -- **[`wildcard` query](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html)** - -### exists query - -[**`exists query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html) 会返回字段中至少有一个非空值的文档。 - -由于多种原因,文档字段可能不存在索引值: - -- JSON 中的字段为 `null` 或 `[]` -- 该字段在 mapping 中配置了 `"index" : false` -- 字段值的长度超过了 mapping 中的 `ignore_above` 设置 -- 字段值格式错误,并且在 mapping 中定义了 `ignore_malformed` - -示例: - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "exists": { - "field": "email" - } - } -} -``` - -以下文档会匹配上面的查询: - -- `{ "user" : "jane" }` 有 user 字段,且不为空。 -- `{ "user" : "" }` 有 user 字段,值为空字符串。 -- `{ "user" : "-" }` 有 user 字段,值不为空。 -- `{ "user" : [ "jane" ] }` 有 user 字段,值不为空。 -- `{ "user" : [ "jane", null ] }` 有 user 字段,至少一个值不为空即可。 - -下面的文档都不会被匹配: - -- `{ "user" : null }` 虽然有 user 字段,但是值为空。 -- `{ "user" : [] }` 虽然有 user 字段,但是值为空。 -- `{ "user" : [null] }` 虽然有 user 字段,但是值为空。 -- `{ "foo" : "bar" }` 没有 user 字段。 - -### fuzzy query - -[**`fuzzy query`**(模糊查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html)返回包含与搜索词相似的词的文档。ES 使用 [Levenshtein edit distance(Levenshtein 编辑距离)](https://en.wikipedia.org/wiki/Levenshtein_distance)测量相似度或模糊度。 - -编辑距离是将一个术语转换为另一个术语所需的单个字符更改的数量。这些变化可能包括: - -- 改变一个字符:(**b**ox -> **f**ox) -- 删除一个字符:(**b**lack -> lack) -- 插入一个字符:(sic -> sic**k**) -- 反转两个相邻字符:(**ac**t → **ca**t) - -为了找到相似的词条,fuzzy query 会在指定的编辑距离内创建搜索词条的所有可能变体或扩展集。然后返回完全匹配任意扩展的文档。 - -```bash -GET books/_search -{ - "query": { - "fuzzy": { - "user.id": { - "value": "ki", - "fuzziness": "AUTO", - "max_expansions": 50, - "prefix_length": 0, - "transpositions": true, - "rewrite": "constant_score" - } - } - } -} -``` - -注意:如果配置了 [`search.allow_expensive_queries`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html#query-dsl-allow-expensive-queries) ,则 fuzzy query 不能执行。 - -### ids query - -[**`ids query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html) 根据 ID 返回文档。 此查询使用存储在 `_id` 字段中的文档 ID。 - -```bash -GET /_search -{ - "query": { - "ids" : { - "values" : ["1", "4", "100"] - } - } -} -``` - -### prefix query - -[**`prefix query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html#prefix-query-ex-request) 用于查询某个字段中包含指定前缀的文档。 - -比如查询 `user.id` 中含有以 `ki` 为前缀的关键词的文档,那么含有 `kind`、`kid` 等所有以 `ki` 开头关键词的文档都会被匹配。 - -```bash -GET /_search -{ - "query": { - "prefix": { - "user.id": { - "value": "ki" - } - } - } -} -``` - -### range query - -[**`range query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html) 即范围查询,用于匹配在某一范围内的数值型、日期类型或者字符串型字段的文档。比如搜索哪些书籍的价格在 50 到 100 之间、哪些书籍的出版时间在 2015 年到 2019 年之间。**使用 range 查询只能查询一个字段,不能作用在多个字段上**。 - -range 查询支持的参数有以下几种: - -- **`gt`**:大于 - -- **`gte`**:大于等于 - -- **`lt`**:小于 - -- **`lte`**:小于等于 - -- **`format`**:如果字段是 Date 类型,可以设置日期格式化 - -- **`time_zone`**:时区 - -- **`relation`**:指示范围查询如何匹配范围字段的值。 - - - **`INTERSECTS` (Default)**:匹配与查询字段值范围相交的文档。 - - **`CONTAINS`**:匹配完全包含查询字段值的文档。 - - **`WITHIN`**:匹配具有完全在查询范围内的范围字段值的文档。 - -示例:数值范围查询 - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "range": { - "taxful_total_price": { - "gt": 10, - "lte": 50 - } - } - } -} -``` - -示例:日期范围查询 - -```bash -GET kibana_sample_data_ecommerce/_search -{ - "query": { - "range": { - "order_date": { - "time_zone": "+00:00", - "gte": "2018-01-01T00:00:00", - "lte": "now" - } - } - } -} -``` - -### regexp query - -[**`regexp query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html) 返回与正则表达式相匹配的 term 所属的文档。 - -[正则表达式](https://zh.wikipedia.org/zh-hans/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F)是一种使用占位符字符匹配数据模式的方法,称为运算符。 - -示例:以下搜索返回 `user.id` 字段包含任何以 `k` 开头并以 `y` 结尾的文档。 `.*` 运算符匹配任何长度的任何字符,包括无字符。匹配项可以包括 `ky`、`kay` 和 `kimchy`。 - -```bash -GET /_search -{ - "query": { - "regexp": { - "user.id": { - "value": "k.*y", - "flags": "ALL", - "case_insensitive": true, - "max_determinized_states": 10000, - "rewrite": "constant_score" - } - } - } -} -``` - -> 注意:如果配置了[`search.allow_expensive_queries`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html#query-dsl-allow-expensive-queries) ,则 [**`regexp query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html) 会被禁用。 - -### term query - -[**`term query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 用来查找指定字段中包含给定单词的文档,term 查询不被解析,只有查询词和文档中的词精确匹配才会被搜索到,应用场景为查询人名、地名等需要精准匹配的需求。 - -示例: - -```bash -# 1. 创建一个索引 -DELETE my-index-000001 -PUT my-index-000001 -{ - "mappings": { - "properties": { - "full_text": { "type": "text" } - } - } -} - -# 2. 使用 "Quick Brown Foxes!" 关键字查 "full_text" 字段 -PUT my-index-000001/_doc/1 -{ - "full_text": "Quick Brown Foxes!" -} - -# 3. 使用 term 查询 -GET my-index-000001/_search?pretty -{ - "query": { - "term": { - "full_text": "Quick Brown Foxes!" - } - } -} -# 因为 full_text 字段不再包含确切的 Term —— "Quick Brown Foxes!",所以 term query 搜索不到任何结果 - -# 4. 使用 match 查询 -GET my-index-000001/_search?pretty -{ - "query": { - "match": { - "full_text": "Quick Brown Foxes!" - } - } -} - -DELETE my-index-000001 -``` - -> :warning: 注意:应避免 term 查询对 text 字段使用查询。 -> -> 默认情况下,Elasticsearch 针对 text 字段的值进行解析分词,这会使查找 text 字段值的精确匹配变得困难。 -> -> 要搜索 text 字段值,需改用 match 查询。 - -### terms query - -[**`terms query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html) 与 [**`term query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 相同,但可以搜索多个值。 - -terms query 查询参数: - -- **`index`**:索引名 -- **`id`**:文档 ID -- **`path`**:要从中获取字段值的字段的名称,即搜索关键字 -- **`routing`**(选填):要从中获取 term 值的文档的自定义路由值。如果在索引文档时提供了自定义路由值,则此参数是必需的。 - -示例: - -```bash -# 1. 创建一个索引 -DELETE my-index-000001 -PUT my-index-000001 -{ - "mappings": { - "properties": { - "color": { "type": "keyword" } - } - } -} - -# 2. 写入一个文档 -PUT my-index-000001/_doc/1 -{ - "color": [ - "blue", - "green" - ] -} - -# 3. 写入另一个文档 -PUT my-index-000001/_doc/2 -{ - "color": "blue" -} - -# 3. 使用 terms query -GET my-index-000001/_search?pretty -{ - "query": { - "terms": { - "color": { - "index": "my-index-000001", - "id": "2", - "path": "color" - } - } - } -} - -DELETE my-index-000001 -``` - -### type query - -> 7.0.0 后废弃 - -[**`type query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-type-query.html) 用于查询具有指定类型的文档。 - -示例: - -```bash -GET /_search -{ - "query": { - "type": { - "value": "_doc" - } - } -} -``` - -### wildcard query - -[**`wildcard query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html) 即通配符查询,返回与通配符模式匹配的文档。 - -`?` 用来匹配一个任意字符,`*` 用来匹配零个或者多个字符。 - -示例:以下搜索返回 `user.id` 字段包含以 `ki` 开头并以 `y` 结尾的术语的文档。这些匹配项可以包括 `kiy`、`kity` 或 `kimchy`。 - -```bash -GET /_search -{ - "query": { - "wildcard": { - "user.id": { - "value": "ki*y", - "boost": 1.0, - "rewrite": "constant_score" - } - } - } -} -``` - -> 注意:如果配置了[`search.allow_expensive_queries`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html#query-dsl-allow-expensive-queries) ,则[**`wildcard query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html) 会被禁用。 - -### 词项查询完整示例 - -```bash -DELETE products -PUT products -{ - "settings": { - "number_of_shards": 1 - } -} - -POST /products/_bulk -{ "index": { "_id": 1 }} -{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" } -{ "index": { "_id": 2 }} -{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" } -{ "index": { "_id": 3 }} -{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" } - -GET /products - -POST /products/_search -{ - "query": { - "term": { - "desc": { - //"value": "iPhone" - "value":"iphone" - } - } - } -} - -POST /products/_search -{ - "query": { - "term": { - "desc.keyword": { - //"value": "iPhone" - //"value":"iphone" - } - } - } -} - -POST /products/_search -{ - "query": { - "term": { - "productID": { - "value": "XHDK-A-1293-#fJ3" - } - } - } -} - -POST /products/_search -{ - //"explain": true, - "query": { - "term": { - "productID.keyword": { - "value": "XHDK-A-1293-#fJ3" - } - } - } -} - -POST /products/_search -{ - "explain": true, - "query": { - "constant_score": { - "filter": { - "term": { - "productID.keyword": "XHDK-A-1293-#fJ3" - } - } - - } - } -} -``` - -## 复合查询 - -复合查询就是把一些简单查询组合在一起实现更复杂的查询需求,除此之外,复合查询还可以控制另外一个查询的行为。 - -### bool query - -bool 查询可以把任意多个简单查询组合在一起,使用 must、should、must_not、filter 选项来表示简单查询之间的逻辑,每个选项都可以出现 0 次到多次,它们的含义如下: - -- must 文档必须匹配 must 选项下的查询条件,相当于逻辑运算的 AND,且参与文档相关度的评分。 -- should 文档可以匹配 should 选项下的查询条件也可以不匹配,相当于逻辑运算的 OR,且参与文档相关度的评分。 -- must_not 与 must 相反,匹配该选项下的查询条件的文档不会被返回;需要注意的是,**must_not 语句不会影响评分,它的作用只是将不相关的文档排除**。 -- filter 和 must 一样,匹配 filter 选项下的查询条件的文档才会被返回,**但是 filter 不评分,只起到过滤功能,与 must_not 相反**。 - -假设要查询 title 中包含关键词 java,并且 price 不能高于 70,description 可以包含也可以不包含虚拟机的书籍,构造 bool 查询语句如下: - -``` -GET books/_search -{ - "query": { - "bool": { - "filter": { - "term": { - "status": 1 - } - }, - "must_not": { - "range": { - "price": { - "gte": 70 - } - } - }, - "must": { - "match": { - "title": "java" - } - }, - "should": [ - { - "match": { - "description": "虚拟机" - } - } - ], - "minimum_should_match": 1 - } - } -} -``` - -有关布尔查询更详细的信息参考 [bool query(组合查询)详解](https://www.knowledgedict.com/tutorial/elasticsearch-query-bool.html)。 - -### boosting query - -boosting 查询用于需要对两个查询的评分进行调整的场景,boosting 查询会把两个查询封装在一起并降低其中一个查询的评分。 - -boosting 查询包括 positive、negative 和 negative_boost 三个部分,positive 中的查询评分保持不变,negative 中的查询会降低文档评分,negative_boost 指明 negative 中降低的权值。如果我们想对 2015 年之前出版的书降低评分,可以构造一个 boosting 查询,查询语句如下: - -``` -GET books/_search -{ - "query": { - "boosting": { - "positive": { - "match": { - "title": "python" - } - }, - "negative": { - "range": { - "publish_time": { - "lte": "2015-01-01" - } - } - }, - "negative_boost": 0.2 - } - } -} -``` - -boosting 查询中指定了抑制因子为 0.2,publish_time 的值在 2015-01-01 之后的文档得分不变,publish_time 的值在 2015-01-01 之前的文档得分为原得分的 0.2 倍。 - -### constant_score query - -constant*score query 包装一个 filter query,并返回匹配过滤器查询条件的文档,且它们的相关性评分都等于 \_boost* 参数值(可以理解为原有的基于 tf-idf 或 bm25 的相关分固定为 1.0,所以最终评分为 _1.0 \* boost_,即等于 _boost_ 参数值)。下面的查询语句会返回 title 字段中含有关键词 _elasticsearch_ 的文档,所有文档的评分都是 1.8: - -``` -GET books/_search -{ - "query": { - "constant_score": { - "filter": { - "term": { - "title": "elasticsearch" - } - }, - "boost": 1.8 - } - } -} -``` - -### dis_max query - -dis_max query 与 bool query 有一定联系也有一定区别,dis_max query 支持多并发查询,可返回与任意查询条件子句匹配的任何文档类型。与 bool 查询可以将所有匹配查询的分数相结合使用的方式不同,dis_max 查询只使用最佳匹配查询条件的分数。请看下面的例子: - -``` -GET books/_search -{ - "query": { - "dis_max": { - "tie_breaker": 0.7, - "boost": 1.2, - "queries": [{ - "term": { - "age": 34 - } - }, - { - "term": { - "age": 35 - } - } - ] - } - } -} -``` - -### function_score query - -function_score query 可以修改查询的文档得分,这个查询在有些情况下非常有用,比如通过评分函数计算文档得分代价较高,可以改用过滤器加自定义评分函数的方式来取代传统的评分方式。 - -使用 function_score query,用户需要定义一个查询和一至多个评分函数,评分函数会对查询到的每个文档分别计算得分。 - -下面这条查询语句会返回 books 索引中的所有文档,文档的最大得分为 5,每个文档的得分随机生成,权重的计算模式为相乘模式。 - -``` -GET books/_search -{ - "query": { - "function_score": { - "query": { - "match all": {} - }, - "boost": "5", - "random_score": {}, - "boost_mode": "multiply" - } - } -} -``` - -使用脚本自定义评分公式,这里把 price 值的十分之一开方作为每个文档的得分,查询语句如下: - -``` -GET books/_search -{ - "query": { - "function_score": { - "query": { - "match": { - "title": "java" - } - }, - "script_score": { - "inline": "Math.sqrt(doc['price'].value/10)" - } - } - } -} -``` - -关于 function_score 的更多详细内容请查看 [Elasticsearch function_score 查询最强详解](https://www.knowledgedict.com/tutorial/elasticsearch-function_score.html)。 - -### indices query - -indices query 适用于需要在多个索引之间进行查询的场景,它允许指定一个索引名字列表和内部查询。indices query 中有 query 和 no_match_query 两部分,query 中用于搜索指定索引列表中的文档,no_match_query 中的查询条件用于搜索指定索引列表之外的文档。下面的查询语句实现了搜索索引 books、books2 中 title 字段包含关键字 javascript,其他索引中 title 字段包含 basketball 的文档,查询语句如下: - -``` -GET books/_search -{ - "query": { - "indices": { - "indices": ["books", "books2"], - "query": { - "match": { - "title": "javascript" - } - }, - "no_match_query": { - "term": { - "title": "basketball" - } - } - } - } -} -``` - -## 嵌套查询 - -在 Elasticsearch 这样的分布式系统中执行全 SQL 风格的连接查询代价昂贵,是不可行的。相应地,为了实现水平规模地扩展,Elasticsearch 提供了以下两种形式的 join: - -- nested query(嵌套查询) - - 文档中可能包含嵌套类型的字段,这些字段用来索引一些数组对象,每个对象都可以作为一条独立的文档被查询出来。 - -- has_child query(有子查询)和 has_parent query(有父查询) - - 父子关系可以存在单个的索引的两个类型的文档之间。has_child 查询将返回其子文档能满足特定查询的父文档,而 has_parent 则返回其父文档能满足特定查询的子文档。 - -### nested query - -文档中可能包含嵌套类型的字段,这些字段用来索引一些数组对象,每个对象都可以作为一条独立的文档被查询出来(用嵌套查询)。 - -``` -PUT /my_index -{ - "mappings": { - "type1": { - "properties": { - "obj1": { - "type": "nested" - } - } - } - } -} -``` - -### has_child query - -文档的父子关系创建索引时在映射中声明,这里以员工(employee)和工作城市(branch)为例,它们属于不同的类型,相当于数据库中的两张表,如果想把员工和他们工作的城市关联起来,需要告诉 Elasticsearch 文档之间的父子关系,这里 employee 是 child type,branch 是 parent type,在映射中声明,执行命令: - -``` -PUT /company -{ - "mappings": { - "branch": {}, - "employee": { - "parent": { "type": "branch" } - } - } -} -``` - -使用 bulk api 索引 branch 类型下的文档,命令如下: - -``` -POST company/branch/_bulk -{ "index": { "_id": "london" }} -{ "name": "London Westminster","city": "London","country": "UK" } -{ "index": { "_id": "liverpool" }} -{ "name": "Liverpool Central","city": "Liverpool","country": "UK" } -{ "index": { "_id": "paris" }} -{ "name": "Champs Elysees","city": "Paris","country": "France" } -``` - -添加员工数据: - -``` -POST company/employee/_bulk -{ "index": { "_id": 1,"parent":"london" }} -{ "name": "Alice Smith","dob": "1970-10-24","hobby": "hiking" } -{ "index": { "_id": 2,"parent":"london" }} -{ "name": "Mark Tomas","dob": "1982-05-16","hobby": "diving" } -{ "index": { "_id": 3,"parent":"liverpool" }} -{ "name": "Barry Smith","dob": "1979-04-01","hobby": "hiking" } -{ "index": { "_id": 4,"parent":"paris" }} -{ "name": "Adrien Grand","dob": "1987-05-11","hobby": "horses" } -``` - -通过子文档查询父文档要使用 has_child 查询。例如,搜索 1980 年以后出生的员工所在的分支机构,employee 中 1980 年以后出生的有 Mark Thomas 和 Adrien Grand,他们分别在 london 和 paris,执行以下查询命令进行验证: - -``` -GET company/branch/_search -{ - "query": { - "has_child": { - "type": "employee", - "query": { - "range": { "dob": { "gte": "1980-01-01" } } - } - } - } -} -``` - -搜索哪些机构中有名为 “Alice Smith” 的员工,因为使用 match 查询,会解析为 “Alice” 和 “Smith”,所以 Alice Smith 和 Barry Smith 所在的机构会被匹配,执行以下查询命令进行验证: - -``` -GET company/branch/_search -{ - "query": { - "has_child": { - "type": "employee", - "score_mode": "max", - "query": { - "match": { "name": "Alice Smith" } - } - } - } -} -``` - -可以使用 min_children 指定子文档的最小个数。例如,搜索最少含有两个 employee 的机构,查询命令如下: - -``` -GET company/branch/_search?pretty -{ - "query": { - "has_child": { - "type": "employee", - "min_children": 2, - "query": { - "match_all": {} - } - } - } -} -``` - -### has_parent query - -通过父文档查询子文档使用 has_parent 查询。比如,搜索哪些 employee 工作在 UK,查询命令如下: - -``` -GET company/employee/_search -{ - "query": { - "has_parent": { - "parent_type": "branch", - "query": { - "match": { "country": "UK } - } - } - } -} -``` - -## 位置查询 - -Elasticsearch 可以对地理位置点 geo_point 类型和地理位置形状 geo_shape 类型的数据进行搜索。为了学习方便,这里准备一些城市的地理坐标作为测试数据,每一条文档都包含城市名称和地理坐标这两个字段,这里的坐标点取的是各个城市中心的一个位置。首先把下面的内容保存到 geo.json 文件中: - -``` -{"index":{ "_index":"geo","_type":"city","_id":"1" }} -{"name":"北京","location":"39.9088145109,116.3973999023"} -{"index":{ "_index":"geo","_type":"city","_id": "2" }} -{"name":"乌鲁木齐","location":"43.8266300000,87.6168800000"} -{"index":{ "_index":"geo","_type":"city","_id":"3" }} -{"name":"西安","location":"34.3412700000,108.9398400000"} -{"index":{ "_index":"geo","_type":"city","_id":"4" }} -{"name":"郑州","location":"34.7447157466,113.6587142944"} -{"index":{ "_index":"geo","_type":"city","_id":"5" }} -{"name":"杭州","location":"30.2294080260,120.1492309570"} -{"index":{ "_index":"geo","_type":"city","_id":"6" }} -{"name":"济南","location":"36.6518400000,117.1200900000"} -``` - -创建一个索引并设置映射: - -``` -PUT geo -{ - "mappings": { - "city": { - "properties": { - "name": { - "type": "keyword" - }, - "location": { - "type": "geo_point" - } - } - } - } -} -``` - -然后执行批量导入命令: - -``` -curl -XPOST "http://localhost:9200/_bulk?pretty" --data-binary @geo.json -``` - -### geo_distance query - -geo_distance query 可以查找在一个中心点指定范围内的地理点文档。例如,查找距离天津 200km 以内的城市,搜索结果中会返回北京,命令如下: - -``` -GET geo/_search -{ - "query": { - "bool": { - "must": { - "match_all": {} - }, - "filter": { - "geo_distance": { - "distance": "200km", - "location": { - "lat": 39.0851000000, - "lon": 117.1993700000 - } - } - } - } - } -} -``` - -按各城市离北京的距离排序: - -``` -GET geo/_search -{ - "query": { - "match_all": {} - }, - "sort": [{ - "_geo_distance": { - "location": "39.9088145109,116.3973999023", - "unit": "km", - "order": "asc", - "distance_type": "plane" - } - }] -} -``` - -其中 location 对应的经纬度字段;unit 为 `km` 表示将距离以 `km` 为单位写入到每个返回结果的 sort 键中;distance_type 为 `plane` 表示使用快速但精度略差的 `plane` 计算方式。 - -### geo_bounding_box query - -geo_bounding_box query 用于查找落入指定的矩形内的地理坐标。查询中由两个点确定一个矩形,然后在矩形区域内查询匹配的文档。 - -``` -GET geo/_search -{ - "query": { - "bool": { - "must": { - "match_all": {} - }, - "filter": { - "geo_bounding_box": { - "location": { - "top_left": { - "lat": 38.4864400000, - "lon": 106.2324800000 - }, - "bottom_right": { - "lat": 28.6820200000, - "lon": 115.8579400000 - } - } - } - } - } - } -} -``` - -### geo_polygon query - -geo_polygon query 用于查找在指定**多边形**内的地理点。例如,呼和浩特、重庆、上海三地组成一个三角形,查询位置在该三角形区域内的城市,命令如下: - -``` -GET geo/_search -{ - "query": { - "bool": { - "must": { - "match_all": {} - } - }, - "filter": { - "geo_polygon": { - "location": { - "points": [{ - "lat": 40.8414900000, - "lon": 111.7519900000 - }, { - "lat": 29.5647100000, - "lon": 106.5507300000 - }, { - "lat": 31.2303700000, - "lon": 121.4737000000 - }] - } - } - } - } -} -``` - -### geo_shape query - -geo_shape query 用于查询 geo_shape 类型的地理数据,地理形状之间的关系有相交、包含、不相交三种。创建一个新的索引用于测试,其中 location 字段的类型设为 geo_shape 类型。 - -``` -PUT geoshape -{ - "mappings": { - "city": { - "properties": { - "name": { - "type": "keyword" - }, - "location": { - "type": "geo_shape" - } - } - } - } -} -``` - -关于经纬度的顺序这里做一个说明,geo_point 类型的字段纬度在前经度在后,但是对于 geo_shape 类型中的点,是经度在前纬度在后,这一点需要特别注意。 - -把西安和郑州连成的线写入索引: - -``` -POST geoshape/city/1 -{ - "name": "西安-郑州", - "location": { - "type": "linestring", - "coordinates": [ - [108.9398400000, 34.3412700000], - [113.6587142944, 34.7447157466] - ] - } -} -``` - -查询包含在由银川和南昌作为对角线上的点组成的矩形的地理形状,由于西安和郑州组成的直线落在该矩形区域内,因此可以被查询到。命令如下: - -``` -GET geoshape/_search -{ - "query": { - "bool": { - "must": { - "match_all": {} - }, - "filter": { - "geo_shape": { - "location": { - "shape": { - "type": "envelope", - "coordinates": [ - [106.23248, 38.48644], - [115.85794, 28.68202] - ] - }, - "relation": "within" - } - } - } - } - } -} -``` - -## 特殊查询 - -### more_like_this query - -more_like_this query 可以查询和提供文本类似的文档,通常用于近似文本的推荐等场景。查询命令如下: - -``` -GET books/_search -{ - "query": { - "more_like_ this": { - "fields": ["title", "description"], - "like": "java virtual machine", - "min_term_freq": 1, - "max_query_terms": 12 - } - } -} -``` - -可选的参数及取值说明如下: - -- fields 要匹配的字段,默认是 \_all 字段。 -- like 要匹配的文本。 -- min_term_freq 文档中词项的最低频率,默认是 2,低于此频率的文档会被忽略。 -- max_query_terms query 中能包含的最大词项数目,默认为 25。 -- min_doc_freq 最小的文档频率,默认为 5。 -- max_doc_freq 最大文档频率。 -- min_word length 单词的最小长度。 -- max_word length 单词的最大长度。 -- stop_words 停用词列表。 -- analyzer 分词器。 -- minimum_should_match 文档应匹配的最小词项数,默认为 query 分词后词项数的 30%。 -- boost terms 词项的权重。 -- include 是否把输入文档作为结果返回。 -- boost 整个 query 的权重,默认为 1.0。 - -### script query - -Elasticsearch 支持使用脚本进行查询。例如,查询价格大于 180 的文档,命令如下: - -``` -GET books/_search -{ - "query": { - "script": { - "script": { - "inline": "doc['price'].value > 180", - "lang": "painless" - } - } - } -} -``` - -### percolate query - -一般情况下,我们是先把文档写入到 Elasticsearch 中,通过查询语句对文档进行搜索。percolate query 则是反其道而行之的做法,它会先注册查询条件,根据文档来查询 query。例如,在 my-index 索引中有一个 laptop 类型,文档有 price 和 name 两个字段,在映射中声明一个 percolator 类型的 query,命令如下: - -``` -PUT my-index -{ - "mappings": { - "laptop": { - "properties": { - "price": { "type": "long" }, - "name": { "type": "text" } - }, - "queries": { - "properties": { - "query": { "type": "percolator" } - } - } - } - } -} -``` - -注册一个 bool query,bool query 中包含一个 range query,要求 price 字段的取值小于等于 10000,并且 name 字段中含有关键词 macbook: - -``` -PUT /my-index/queries/1?refresh -{ - "query": { - "bool": { - "must": [{ - "range": { "price": { "lte": 10000 } } - }, { - "match": { "name": "macbook" } - }] - } - } -} -``` - -通过文档查询 query: - -``` -GET /my-index/_search -{ - "query": { - "percolate": { - "field": "query", - "document_type": "laptop", - "document": { - "price": 9999, - "name": "macbook pro on sale" - } - } - } -} -``` - -文档符合 query 中的条件,返回结果中可以查到上文中注册的 bool query。percolate query 的这种特性适用于数据分类、数据路由、事件监控和预警等场景。 \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/06.Elasticsearch\351\253\230\344\272\256.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/06.Elasticsearch\351\253\230\344\272\256.md" deleted file mode 100644 index e68927b71c..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/06.Elasticsearch\351\253\230\344\272\256.md" +++ /dev/null @@ -1,130 +0,0 @@ ---- -title: Elasticsearch 高亮搜索及显示 -date: 2022-02-22 21:01:01 -order: 06 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch - - 高亮 -permalink: /pages/e1b769/ ---- - -# Elasticsearch 高亮搜索及显示 - -Elasticsearch 的高亮(highlight)可以让您从搜索结果中的一个或多个字段中获取突出显示的摘要,以便向用户显示查询匹配的位置。当您请求突出显示(即高亮)时,响应结果的 highlight 字段中包括高亮的字段和高亮的片段。Elasticsearch 默认会用 `` 标签标记关键字。 - -## 高亮参数 - -ES 提供了如下高亮参数: - -| 参数 | 说明 | -| :------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `boundary_chars` | 包含每个边界字符的字符串。默认为,! ?\ \ n。 | -| `boundary_max_scan` | 扫描边界字符的距离。默认为 20。 | -| `boundary_scanner` | 指定如何分割突出显示的片段,支持 chars、sentence、word 三种方式。 | -| `boundary_scanner_locale` | 用来设置搜索和确定单词边界的本地化设置,此参数使用语言标记的形式(“en-US”, “fr-FR”, “ja-JP”) | -| `encoder` | 表示代码段应该是 HTML 编码的:默认(无编码)还是 HTML (HTML-转义代码段文本,然后插入高亮标记) | -| `fields` | 指定检索高亮显示的字段。可以使用通配符来指定字段。例如,可以指定 comment*\*来获取以 comment*开头的所有文本和关键字字段的高亮显示。 | -| `force_source` | 根据源高亮显示。默认值为 false。 | -| `fragmenter` | 指定文本应如何在突出显示片段中拆分:支持参数 simple 或者 span。 | -| `fragment_offset` | 控制要开始突出显示的空白。仅在使用 fvh highlighter 时有效。 | -| `fragment_size` | 字符中突出显示的片段的大小。默认为 100。 | -| `highlight_query` | 突出显示搜索查询之外的其他查询的匹配项。这在使用重打分查询时特别有用,因为默认情况下高亮显示不会考虑这些问题。 | -| `matched_fields` | 组合多个匹配结果以突出显示单个字段,对于使用不同方式分析同一字符串的多字段。所有的 matched_fields 必须将 term_vector 设置为 with_positions_offsets,但是只有将匹配项组合到的字段才会被加载,因此只有将 store 设置为 yes 才能使该字段受益。只适用于 fvh highlighter。 | -| `no_match_size` | 如果没有要突出显示的匹配片段,则希望从字段开头返回的文本量。默认为 0(不返回任何内容)。 | -| `number_of_fragments` | 返回的片段的最大数量。如果片段的数量设置为 0,则不会返回任何片段。相反,突出显示并返回整个字段内容。当需要突出显示短文本(如标题或地址),但不需要分段时,使用此配置非常方便。如果 number_of_fragments 为 0,则忽略 fragment_size。默认为 5。 | -| `order` | 设置为 score 时,按分数对突出显示的片段进行排序。默认情况下,片段将按照它们在字段中出现的顺序输出(order:none)。将此选项设置为 score 将首先输出最相关的片段。每个高亮应用自己的逻辑来计算相关性得分。 | -| `phrase_limit` | 控制文档中所考虑的匹配短语的数量。防止 fvh highlighter 分析太多的短语和消耗太多的内存。提高限制会增加查询时间并消耗更多内存。默认为 256。 | -| `pre_tags` | 与 post_tags 一起使用,定义用于突出显示文本的 HTML 标记。默认情况下,突出显示的文本被包装在和标记中。指定为字符串数组。 | -| `post_tags` | 与 pre_tags 一起使用,定义用于突出显示文本的 HTML 标记。默认情况下,突出显示的文本被包装在和标记中。指定为字符串数组。 | -| `require_field_match` | 默认情况下,只突出显示包含查询匹配的字段。将 require_field_match 设置为 false 以突出显示所有字段。默认值为 true。 | -| `tags_schema` | 设置为使用内置标记模式的样式。 | -| `type` | 使用的高亮模式,可选项为**_`unified`_**、**_`plain`_**或**_`fvh`_**。默认为 _`unified`_。 | - -## 自定义高亮片段 - -如果我们想使用自定义标签,在高亮属性中给需要高亮的字段加上 `pre_tags` 和 `post_tags` 即可。例如,搜索 title 字段中包含关键词 javascript 的书籍并使用自定义 HTML 标签高亮关键词,查询语句如下: - -```bash -GET /books/_search -{ - "query": { - "match": { "title": "javascript" } - }, - "highlight": { - "fields": { - "title": { - "pre_tags": [""], - "post_tags": [""] - } - } - } -} -``` - -## 多字段高亮 - -关于搜索高亮,还需要掌握如何设置多字段搜索高亮。比如,搜索 title 字段的时候,我们期望 description 字段中的关键字也可以高亮,这时候就需要把 `require_field_match` 属性的取值设置为 `fasle`。`require_field_match` 的默认值为 `true`,只会高亮匹配的字段。多字段高亮的查询语句如下: - -```bash -GET /books/_search -{ - "query": { - "match": { "title": "javascript" } - }, - "highlight": { - "require_field_match": false, - "fields": { - "title": {}, - "description": {} - } - } -} -``` - -## 高亮性能分析 - -Elasticsearch 提供了三种高亮器,分别是**默认的 highlighter 高亮器**、**postings-highlighter 高亮器**和 **fast-vector-highlighter 高亮器**。 - -默认的 **highlighter** 是最基本的高亮器。highlighter 高亮器实现高亮功能需要对 `_source` 中保存的原始文档进行二次分析,其速度在三种高亮器里最慢,优点是不需要额外的存储空间。 - -**postings-highlighter** 高亮器实现高亮功能不需要二次分析,但是需要在字段的映射中设置 `index_options` 参数的取值为 `offsets`,即保存关键词的偏移量,速度快于默认的 highlighter 高亮器。例如,配置 comment 字段使用 postings-highlighter 高亮器,映射如下: - -```bash -PUT /example -{ - "mappings": { - "doc": { - "properties": { - "comment": { - "type": "text", - "index_options": "offsets" - } - } - } - } -} -``` - -**fast-vector-highlighter** 高亮器实现高亮功能速度最快,但是需要在字段的映射中设置 `term_vector` 参数的取值为 `with_positions_offsets`,即保存关键词的位置和偏移信息,占用的存储空间最大,是典型的空间换时间的做法。例如,配置 comment 字段使用 fast-vector-highlighter 高亮器,映射如下: - -```bash -PUT /example -{ - "mappings": { - "doc": { - "properties": { - "comment": { - "type": "text", - "term_vector": "with_positions_offsets" - } - } - } - } -} -``` \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/07.Elasticsearch\346\216\222\345\272\217.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/07.Elasticsearch\346\216\222\345\272\217.md" deleted file mode 100644 index 7512f40a39..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/07.Elasticsearch\346\216\222\345\272\217.md" +++ /dev/null @@ -1,204 +0,0 @@ ---- -title: Elasticsearch 排序 -date: 2022-01-19 22:49:16 -order: 07 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch - - 排序 -permalink: /pages/24baff/ ---- - -# Elasticsearch 排序 - -在 Elasticsearch 中,默认排序是**按照相关性的评分(\_score)**进行降序排序,也可以按照**字段的值排序**、**多级排序**、**多值字段排序、基于 geo(地理位置)排序以及自定义脚本排序**,除此之外,对于相关性的评分也可以用 rescore 二次、三次打分,它可以限定重新打分的窗口大小(window size),并针对作用范围内的文档修改其得分,从而达到精细化控制结果相关性的目的。 - -## 默认相关性排序 - -在 Elasticsearch 中,默认情况下,文档是按照相关性得分倒序排列的,其对应的相关性得分字段用 `_score` 来表示,它是浮点数类型,`_score` 评分越高,相关性越高。评分模型的选择可以通过 `similarity` 参数在映射中指定。 - -相似度算法可以按字段指定,只需在映射中为不同字段选定即可,如果要修改已有字段的相似度算法,只能通过为数据重新建立索引来达到目的。关于更多 es 相似度算法可以参考 [深入理解 es 相似度算法(相关性得分计算)](https://www.knowledgedict.com/tutorial/elasticsearch-similarity.html)。 - -### TF-IDF 模型 - -Elasticsearch 在 5.4 版本以前,text 类型的字段,默认采用基于 tf-idf 的向量空间模型。 - -在开始计算得分之时,Elasticsearch 使用了被搜索词条的频率以及它有多常见来影响得分。一个简短的解释是,**一个词条出现在某个文档中的次数越多,它就越相关;但是,如果该词条出现在不同的文档的次数越多,它就越不相关**。这一点被称为 TF-IDF,TF 是**词频**(term frequency),IDF 是**逆文档频率**(inverse document frequency)。 - -考虑给一篇文档打分的首要方式,是统计一个词条在文本中出现的次数。举个例子,如果在用户的区域搜索关于 Elasticsearch 的 get-together,用户希望频繁提及 Elasticsearch 的分组被优先展示出来。 - -``` -"We will discuss Elasticsearch at the next Big Data group." -"Tuesday the Elasticsearch team will gather to answer questions about Elasticsearch." -``` - -第一个句子提到 Elasticsearch 一次,而第二个句子提到 Elasticsearch 两次,所以包含第二句话的文档应该比包含第一句话的文档拥有更高的得分。如果我们要按照数量来讨论,第一句话的词频(TF)是 1,而第二句话的词频将是 2。 - -逆文档频率比文档词频稍微复杂一点。这个听上去很酷炫的描述意味着,如果一个分词(通常是单词,但不一定是)在索引的不同文档中出现越多的次数,那么它就越不重要。使用如下例子更容易解释这一点。 - -``` -"We use Elasticsearch to power the search for our website." -"The developers like Elasticsearch so far." -"The scoring of documents is calculated by the scoring formula." -``` - -如上述例子,需要理解以下几点: - -- 词条 “Elasticsearch” 的文档频率是 2(因为它出现在两篇文档中)。文档频率的逆源自得分乘以 1/DF,这里 DF 是该词条的文档频率。这就意味着,由于词条拥有更高的文档频率,它的权重就会降低。 -- 词条 “the” 的文档频率是 3,因为它出现在所有的三篇文档中。请注意,尽管 “the” 在最后一篇文档中出现了两次,它的文档频率还是 3。这是因为,逆文档频率只检查一个词条是否出现在某文档中,而不检查它出现多少次。那个应该是词频所关心的事情。 - -逆文档频率是一个重要的因素,用于平衡词条的词频。举个例子,考虑有一个用户搜索词条 “the score”,单词 the 几乎出现在每个普通的英语文本中,如果它不被均衡一下,单词 the 的频率要完全淹没单词 score 的频率。逆文档频率 IDF 均衡了 the 这种常见词的相关性影响,所以实际的相关性得分将会对查询的词条有一个更准确的描述。 - -一旦词频 TF 和逆文档频率 IDF 计算完成,就可以使用 TF-IDF 公式来计算文档的得分。 - -### BM25 模型 - -Elasticsearch 在 5.4 版本之后,针对 text 类型的字段,默认采用的是 BM25 评分模型,而不是基于 tf-idf 的向量空间模型,评分模型的选择可以通过 `similarity` 参数在映射中指定。 - -## 字段的值排序 - -在 Elasticsearch 中按照字段的值排序,可以利用 `sort` 参数实现。 - -```bash -GET books/_search -{ - "sort": { - "price": { - "order": "desc" - } - } -} -``` - -返回结果如下: - -```json -{ - "took": 132, - "timed_out": false, - "_shards": { - "total": 10, - "successful": 10, - "skipped": 0, - "failed": 0 - }, - "hits": { - "total": 749244, - "max_score": null, - "hits": [ - { - "_index": "books", - "_type": "book", - "_id": "8456479", - "_score": null, - "_source": { - "id": 8456479, - "price": 1580.00, - ... - }, - "sort": [ - 1580.00 - ] - }, - ... - ] - } -} -``` - -从如上返回结果,可以看出,`max_score` 和 `_score` 字段都返回 `null`,返回字段多出 `sort` 字段,包含排序字段的分值。计算 \_`score` 的花销巨大,如果不根据相关性排序,记录 \_`score` 是没有意义的。如果无论如何都要计算 \_`score`,可以将 `track_scores` 参数设置为 `true`。 - -## 多字段排序 - -如果我们想要结合使用 price、date 和 \_score 进行查询,并且匹配的结果首先按照价格排序,然后按照日期排序,最后按照相关性排序,具体示例如下: - -```bash -GET books/_search -{ - "query": { - "bool": { - "must": { - "match": { "content": "java" } - }, - "filter": { - "term": { "user_id": 4868438 } - } - } - }, - "sort": [{ - "price": { - "order": "desc" - } - }, { - "date": { - "order": "desc" - } - }, { - "_score": { - "order": "desc" - } - } - ] -} -``` - -排序条件的顺序是很重要的。结果首先按第一个条件排序,仅当结果集的第一个 `sort` 值完全相同时才会按照第二个条件进行排序,以此类推。 - -多级排序并不一定包含 `_score`。你可以根据一些不同的字段进行排序,如地理距离或是脚本计算的特定值。 - -## 多值字段的排序 - -一种情形是字段有多个值的排序,需要记住这些值并没有固有的顺序;一个多值的字段仅仅是多个值的包装,这时应该选择哪个进行排序呢? - -对于数字或日期,你可以将多值字段减为单值,这可以通过使用 `min`、`max`、`avg` 或是 `sum` 排序模式。例如你可以按照每个 date 字段中的最早日期进行排序,通过以下方法: - -```json -"sort": { - "dates": { - "order": "asc", - "mode": "min" - } -} -``` - -## 地理位置上的距离排序 - -es 的地理位置排序使用 **`_geo_distance`** 来进行距离排序,如下示例: - -```json -{ - "sort" : [ - { - "_geo_distance" : { - "es_location_field" : [116.407526, 39.904030], - "order" : "asc", - "unit" : "km", - "mode" : "min", - "distance_type" : "plane" - } - } - ], - "query" : { - ...... - } -} -``` - -_\_geo_distance_ 的选项具体如下: - -- 如上的 _es_location_field_ 指的是 es 存储经纬度数据的字段名。 -- **_`order`_**:指定按距离升序或降序,分别对应 **_`asc`_** 和 **_`desc`_**。 -- **_`unit`_**:计算距离值的单位,默认是 **_`m`_**,表示米(meters),其它可选项有 **_`mi`_**、**_`cm`_**、**_`mm`_**、**_`NM`_**、**_`km`_**、**_`ft`_**、**_`yd`_** 和 **_`in`_**。 -- **_`mode`_**:针对数组数据(多个值)时,指定的取值模式,可选值有 **_`min`_**、**_`max`_**、**_`sum`_**、**_`avg`_** 和 **_`median`_**,当排序采用升序时,默认为 _min_;排序采用降序时,默认为 _max_。 -- **_`distance_type`_**:用来设置如何计算距离,它的可选项有 **_`sloppy_arc`_**、**_`arc`_** 和 **_`plane`_**,默认为 _sloppy_arc_,_arc_ 它相对更精确些,但速度会明显下降,_plane_ 则是计算快,但是长距离计算相对不准确。 -- **_`ignore_unmapped`_**:未映射字段时,是否忽略处理,可选项有 **_`true`_** 和 **_`false`_**;默认为 _false_,表示如果未映射字段,查询将引发异常;若设置 _true_,将忽略未映射的字段,并且不匹配此查询的任何文档。 -- **_`validation_method`_**:指定检验经纬度数据的方式,可选项有 **_`IGNORE_MALFORMED`_**、**_`COERCE`_** 和 **_`STRICT`_**;_IGNORE_MALFORMED_ 表示可接受纬度或经度无效的地理点,即忽略数据;_COERCE_ 表示另外尝试并推断正确的地理坐标;_STRICT_ 为默认值,表示遇到不正确的地理坐标直接抛出异常。 - -## 参考资料 - -- [Elasticsearch 教程](https://www.knowledgedict.com/tutorial/elasticsearch-intro.html) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/08.Elasticsearch\350\201\232\345\220\210.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/08.Elasticsearch\350\201\232\345\220\210.md" deleted file mode 100644 index 2516e6d0db..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/08.Elasticsearch\350\201\232\345\220\210.md" +++ /dev/null @@ -1,737 +0,0 @@ ---- -title: Elasticsearch 聚合 -date: 2022-01-19 22:49:16 -order: 08 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch - - 聚合 -permalink: /pages/f89f66/ ---- - -# Elasticsearch 聚合 - -Elasticsearch 是一个分布式的全文搜索引擎,索引和搜索是 Elasticsearch 的基本功能。事实上,Elasticsearch 的聚合(Aggregations)功能也十分强大,允许在数据上做复杂的分析统计。Elasticsearch 提供的聚合分析功能主要有**指标聚合(metrics aggregations)**、**桶聚合(bucket aggregations)**、**管道聚合(pipeline aggregations)** 和 **矩阵聚合(matrix aggregations)** 四大类,管道聚合和矩阵聚合官方说明是在试验阶段,后期会完全更改或者移除,这里不再对管道聚合和矩阵聚合进行讲解。 - -## 聚合的具体结构 - -所有的聚合,无论它们是什么类型,都遵从以下的规则。 - -- 使用查询中同样的 JSON 请求来定义它们,而且你是使用键 aggregations 或者是 aggs 来进行标记。需要给每个聚合起一个名字,指定它的类型以及和该类型相关的选项。 -- 它们运行在查询的结果之上。和查询不匹配的文档不会计算在内,除非你使用 global 聚集将不匹配的文档囊括其中。 -- 可以进一步过滤查询的结果,而不影响聚集。 - -以下是聚合的基本结构: - -```json -"aggregations" : { - "" : { - "" : { - - } - [,"meta" : { [] } ]? - [,"aggregations" : { []+ } ]? - } - [,"" : { ... } ]* -} -``` - -- **在最上层有一个 aggregations 的键,可以缩写为 aggs**。 -- 在下面一层,需要为聚合指定一个名字。可以在请求的返回中看到这个名字。在同一个请求中使用多个聚合时,这一点非常有用,它让你可以很容易地理解每组结果的含义。 -- 最后,必须要指定聚合的类型。 - -> 关于聚合分析的值来源,可以**取字段的值**,也可以是**脚本计算的结果**。 -> -> 但是用脚本计算的结果时,需要注意脚本的性能和安全性;尽管多数聚集类型允许使用脚本,但是脚本使得聚集变得缓慢,因为脚本必须在每篇文档上运行。为了避免脚本的运行,可以在索引阶段进行计算。 -> -> 此外,脚本也可以被人可能利用进行恶意代码攻击,尽量使用沙盒(sandbox)内的脚本语言。 - -示例:查询所有球员的平均年龄是多少,并对球员的平均薪水加 188(也可以理解为每名球员加 188 后的平均薪水)。 - -```bash -POST /player/_search?size=0 -{ - "aggs": { - "avg_age": { - "avg": { - "field": "age" - } - }, - "avg_salary_188": { - "avg": { - "script": { - "source": "doc.salary.value + 188" - } - } - } - } -} -``` - -## 指标聚合 - -指标聚合(又称度量聚合)主要从不同文档的分组中提取统计数据,或者,从来自其他聚合的文档桶来提取统计数据。 - -这些统计数据通常来自数值型字段,如最小或者平均价格。用户可以单独获取每项统计数据,或者也可以使用 stats 聚合来同时获取它们。更高级的统计数据,如平方和或者是标准差,可以通过 extended stats 聚合来获取。 - -### Max Aggregation - -Max Aggregation 用于最大值统计。例如,统计 sales 索引中价格最高的是哪本书,并且计算出对应的价格的 2 倍值,查询语句如下: - -``` -GET /sales/_search?size=0 -{ - "aggs" : { - "max_price" : { - "max" : { - "field" : "price" - } - }, - "max_price_2" : { - "max" : { - "field" : "price", - "script": { - "source": "_value * 2.0" - } - } - } - } -} -``` - -**指定的 field,在脚本中可以用 \_value 取字段的值**。 - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "max_price": { - "value": 188.0 - }, - "max_price_2": { - "value": 376.0 - } - } -} -``` - -### Min Aggregation - -Min Aggregation 用于最小值统计。例如,统计 sales 索引中价格最低的是哪本书,查询语句如下: - -``` -GET /sales/_search?size=0 -{ - "aggs" : { - "min_price" : { - "min" : { - "field" : "price" - } - } - } -} -``` - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "min_price": { - "value": 18.0 - } - } -} -``` - -### Avg Aggregation - -Avg Aggregation 用于计算平均值。例如,统计 exams 索引中考试的平均分数,如未存在分数,默认为 60 分,查询语句如下: - -``` -GET /exams/_search?size=0 -{ - "aggs" : { - "avg_grade" : { - "avg" : { - "field" : "grade", - "missing": 60 - } - } - } -} -``` - -**如果指定字段没有值,可以通过 missing 指定默认值;若未指定默认值,缺失该字段值的文档将被忽略(计算)**。 - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "avg_grade": { - "value": 78.0 - } - } -} -``` - -除了常规的平均值聚合计算外,elasticsearch 还提供了加权平均值的聚合计算,详情参见 [Elasticsearch 指标聚合之 Weighted Avg Aggregation](https://www.knowledgedict.com/tutorial/elasticsearch-aggregations-metrics-weighted-avg-aggregation.html)。 - -### Sum Aggregation - -Sum Aggregation 用于计算总和。例如,统计 sales 索引中 type 字段中匹配 hat 的价格总和,查询语句如下: - -``` -GET /exams/_search?size=0 -{ - "query" : { - "constant_score" : { - "filter" : { - "match" : { "type" : "hat" } - } - } - }, - "aggs" : { - "hat_prices" : { - "sum" : { "field" : "price" } - } - } -} -``` - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "hat_prices": { - "value": 567.0 - } - } -} -``` - -### Value Count Aggregation - -Value Count Aggregation 可按字段统计文档数量。例如,统计 books 索引中包含 author 字段的文档数量,查询语句如下: - -``` -GET /books/_search?size=0 -{ - "aggs" : { - "doc_count" : { - "value_count" : { "field" : "author" } - } - } -} -``` - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "doc_count": { - "value": 5 - } - } -} -``` - -### Cardinality Aggregation - -Cardinality Aggregation 用于基数统计,其作用是先执行类似 SQL 中的 distinct 操作,去掉集合中的重复项,然后统计去重后的集合长度。例如,在 books 索引中对 language 字段进行 cardinality 操作可以统计出编程语言的种类数,查询语句如下: - -``` -GET /books/_search?size=0 -{ - "aggs" : { - "all_lan" : { - "cardinality" : { "field" : "language" } - }, - "title_cnt" : { - "cardinality" : { "field" : "title.keyword" } - } - } -} -``` - -**假设 title 字段为文本类型(text),去重时需要指定 keyword,表示把 title 作为整体去重,即不分词统计**。 - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "all_lan": { - "value": 8 - }, - "title_cnt": { - "value": 18 - } - } -} -``` - -### Stats Aggregation - -Stats Aggregation 用于基本统计,会一次返回 count、max、min、avg 和 sum 这 5 个指标。例如,在 exams 索引中对 grade 字段进行分数相关的基本统计,查询语句如下: - -``` -GET /exams/_search?size=0 -{ - "aggs" : { - "grades_stats" : { - "stats" : { "field" : "grade" } - } - } -} -``` - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "grades_stats": { - "count": 2, - "min": 50.0, - "max": 100.0, - "avg": 75.0, - "sum": 150.0 - } - } -} -``` - -### Extended Stats Aggregation - -Extended Stats Aggregation 用于高级统计,和基本统计功能类似,但是会比基本统计多出以下几个统计结果,sum_of_squares(平方和)、variance(方差)、std_deviation(标准差)、std_deviation_bounds(平均值加/减两个标准差的区间)。在 exams 索引中对 grade 字段进行分数相关的高级统计,查询语句如下: - -``` -GET /exams/_search?size=0 -{ - "aggs" : { - "grades_stats" : { - "extended_stats" : { "field" : "grade" } - } - } -} -``` - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "grades_stats": { - "count": 2, - "min": 50.0, - "max": 100.0, - "avg": 75.0, - "sum": 150.0, - "sum_of_squares": 12500.0, - "variance": 625.0, - "std_deviation": 25.0, - "std_deviation_bounds": { - "upper": 125.0, - "lower": 25.0 - } - } - } -} -``` - -### Percentiles Aggregation - -Percentiles Aggregation 用于百分位统计。百分位数是一个统计学术语,如果将一组数据从大到小排序,并计算相应的累计百分位,某一百分位所对应数据的值就称为这一百分位的百分位数。默认情况下,累计百分位为 [ 1, 5, 25, 50, 75, 95, 99 ]。以下例子给出了在 latency 索引中对 load_time 字段进行加载时间的百分位统计,查询语句如下: - -``` -GET latency/_search -{ - "size": 0, - "aggs" : { - "load_time_outlier" : { - "percentiles" : { - "field" : "load_time" - } - } - } -} -``` - -**需要注意的是,如上的 `load_time` 字段必须是数字类型**。 - -聚合结果如下: - -``` -{ - ... - "aggregations": { - "load_time_outlier": { - "values" : { - "1.0": 5.0, - "5.0": 25.0, - "25.0": 165.0, - "50.0": 445.0, - "75.0": 725.0, - "95.0": 945.0, - "99.0": 985.0 - } - } - } -} -``` - -百分位的统计也可以指定 percents 参数指定百分位,如下: - -``` -GET latency/_search -{ - "size": 0, - "aggs" : { - "load_time_outlier" : { - "percentiles" : { - "field" : "load_time", - "percents": [60, 80, 95] - } - } - } -} -``` - -### Percentiles Ranks Aggregation - -Percentiles Ranks Aggregation 与 Percentiles Aggregation 统计恰恰相反,就是想看当前数值处在什么范围内(百分位), 假如你查一下当前值 500 和 600 所处的百分位,发现是 90.01 和 100,那么说明有 90.01 % 的数值都在 500 以内,100 % 的数值在 600 以内。 - -``` -GET latency/_search -{ - "size": 0, - "aggs" : { - "load_time_ranks" : { - "percentile_ranks" : { - "field" : "load_time", - "values" : [500, 600] - } - } - } -} -``` - -**`同样 load_time` 字段必须是数字类型**。 - -返回结果大概类似如下: - -``` -{ - ... - "aggregations": { - "load_time_ranks": { - "values" : { - "500.0": 90.01, - "600.0": 100.0 - } - } - } -} -``` - -可以设置 `keyed` 参数为 `true`,将对应的 values 作为桶 key 一起返回,默认是 `false`。 - -``` -GET latency/_search -{ - "size": 0, - "aggs": { - "load_time_ranks": { - "percentile_ranks": { - "field": "load_time", - "values": [500, 600], - "keyed": true - } - } - } -} -``` - -返回结果如下: - -``` -{ - ... - "aggregations": { - "load_time_ranks": { - "values": [ - { - "key": 500.0, - "value": 90.01 - }, - { - "key": 600.0, - "value": 100.0 - } - ] - } - } -} -``` - -## 桶聚合 - -bucket 可以理解为一个桶,它会遍历文档中的内容,凡是符合某一要求的就放入一个桶中,分桶相当于 SQL 中的 group by。从另外一个角度,可以将指标聚合看成单桶聚合,即把所有文档放到一个桶中,而桶聚合是多桶型聚合,它根据相应的条件进行分组。 - -| 种类 | 描述/场景 | -| :-------------------------------------------- | :--------------------------------------------------------------------------------------------- | -| 词项聚合(Terms Aggregation) | 用于分组聚合,让用户得知文档中每个词项的频率,它返回每个词项出现的次数。 | -| 差异词项聚合(Significant Terms Aggregation) | 它会返回某个词项在整个索引中和在查询结果中的词频差异,这有助于我们发现搜索场景中有意义的词。 | -| 过滤器聚合(Filter Aggregation) | 指定过滤器匹配的所有文档到单个桶(bucket),通常这将用于将当前聚合上下文缩小到一组特定的文档。 | -| 多过滤器聚合(Filters Aggregation) | 指定多个过滤器匹配所有文档到多个桶(bucket)。 | -| 范围聚合(Range Aggregation) | 范围聚合,用于反映数据的分布情况。 | -| 日期范围聚合(Date Range Aggregation) | 专门用于日期类型的范围聚合。 | -| IP 范围聚合(IP Range Aggregation) | 用于对 IP 类型数据范围聚合。 | -| 直方图聚合(Histogram Aggregation) | 可能是数值,或者日期型,和范围聚集类似。 | -| 时间直方图聚合(Date Histogram Aggregation) | 时间直方图聚合,常用于按照日期对文档进行统计并绘制条形图。 | -| 空值聚合(Missing Aggregation) | 空值聚合,可以把文档集中所有缺失字段的文档分到一个桶中。 | -| 地理点范围聚合(Geo Distance Aggregation) | 用于对地理点(geo point)做范围统计。 | - -### Terms Aggregation - -Terms Aggregation 用于词项的分组聚合。最为经典的用例是获取 X 中最频繁(top frequent)的项目,其中 X 是文档中的某个字段,如用户的名称、标签或分类。由于 terms 聚集统计的是每个词条,而不是整个字段值,因此通常需要在一个非分析型的字段上运行这种聚集。原因是, 你期望“big data”作为词组统计,而不是“big”单独统计一次,“data”再单独统计一次。 - -用户可以使用 terms 聚集,从分析型字段(如内容)中抽取最为频繁的词条。还可以使用这种信息来生成一个单词云。 - -``` -{ - "aggs": { - "profit_terms": { - "terms": { // terms 聚合 关键字 - "field": "profit", - ...... - } - } - } -} -``` - -在 terms 分桶的基础上,还可以对每个桶进行指标统计,也可以基于一些指标或字段值进行排序。示例如下: - -``` -{ - "aggs": { - "item_terms": { - "terms": { - "field": "item_id", - "size": 1000, - "order":[{ - "gmv_stat": "desc" - },{ - "gmv_180d": "desc" - }] - }, - "aggs": { - "gmv_stat": { - "sum": { - "field": "gmv" - } - }, - "gmv_180d": { - "sum": { - "script": "doc['gmv_90d'].value*2" - } - } - } - } - } -} -``` - -返回的结果如下: - -``` -{ - ... - "aggregations": { - "hospital_id_agg": { - "doc_count_error_upper_bound": 0, - "sum_other_doc_count": 260, - "buckets": [ - { - "key": 23388, - "doc_count": 18, - "gmv_stat": { - "value": 176220 - }, - "gmv_180d": { - "value": 89732 - } - }, - { - "key": 96117, - "doc_count": 16, - "gmv_stat": { - "value": 129306 - }, - "gmv_180d": { - "value": 56988 - } - }, - ... - ] - } - } -} -``` - -默认情况下返回按文档计数从高到低的前 10 个分组,可以通过 size 参数指定返回的分组数。 - -### Filter Aggregation - -Filter Aggregation 是过滤器聚合,可以把符合过滤器中的条件的文档分到一个桶中,即是单分组聚合。 - -``` -{ - "aggs": { - "age_terms": { - "filter": {"match":{"gender":"F"}}, - "aggs": { - "avg_age": { - "avg": { - "field": "age" - } - } - } - } - } -} -``` - -### Filters Aggregation - -Filters Aggregation 是多过滤器聚合,可以把符合多个过滤条件的文档分到不同的桶中,即每个分组关联一个过滤条件,并收集所有满足自身过滤条件的文档。 - -``` -{ - "size": 0, - "aggs": { - "messages": { - "filters": { - "filters": { - "errors": { "match": { "body": "error" } }, - "warnings": { "match": { "body": "warning" } } - } - } - } - } -} -``` - -在这个例子里,我们分析日志信息。聚合会创建两个关于日志数据的分组,一个收集包含错误信息的文档,另一个收集包含告警信息的文档。而且每个分组会按月份划分。 - -``` -{ - ... - "aggregations": { - "messages": { - "buckets": { - "errors": { - "doc_count": 1 - }, - "warnings": { - "doc_count": 2 - } - } - } - } -} -``` - -### Range Aggregation - -Range Aggregation 范围聚合是一个基于多组值来源的聚合,可以让用户定义一系列范围,每个范围代表一个分组。在聚合执行的过程中,从每个文档提取出来的值都会检查每个分组的范围,并且使相关的文档落入分组中。注意,范围聚合的每个范围内包含 from 值但是排除 to 值。 - -``` -{ - "aggs": { - "age_range": { - "range": { - "field": "age", - "ranges": [{ - "to": 25 - }, - { - "from": 25, - "to": 35 - }, - { - "from": 35 - }] - }, - "aggs": { - "bmax": { - "max": { - "field": "balance" - } - } - } - } - } - } -} -``` - -返回结果如下: - -``` -{ - ... - "aggregations": { - "age_range": { - "buckets": [{ - "key": "*-25.0", - "to": 25, - "doc_count": 225, - "bmax": { - "value": 49587 - } - }, - { - "key": "25.0-35.0", - "from": 25, - "to": 35, - "doc_count": 485, - "bmax": { - "value": 49795 - } - }, - { - "key": "35.0-*", - "from": 35, - "doc_count": 290, - "bmax": { - "value": 49989 - } - }] - } - } -} -``` - -## 参考资料 - -- [Elasticsearch 教程](https://www.knowledgedict.com/tutorial/elasticsearch-intro.html) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/09.Elasticsearch\345\210\206\346\236\220\345\231\250.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/09.Elasticsearch\345\210\206\346\236\220\345\231\250.md" deleted file mode 100644 index 03ccb8edbd..0000000000 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/09.Elasticsearch\345\210\206\346\236\220\345\231\250.md" +++ /dev/null @@ -1,407 +0,0 @@ ---- -title: Elasticsearch 分析器 -date: 2022-02-22 21:01:01 -order: 09 -categories: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch -tags: - - 数据库 - - 搜索引擎数据库 - - Elasticsearch - - 分词 -permalink: /pages/a5a001/ ---- - -# Elasticsearch 分析器 - -文本分析是把全文本转换为一系列单词(term/token)的过程,也叫分词。在 Elasticsearch 中,分词是通过 analyzer(分析器)来完成的,不管是索引还是搜索,都需要使用 analyzer(分析器)。分析器,分为**内置分析器**和**自定义的分析器**。 - -分析器可以进一步细分为**字符过滤器**(**Character Filters**)、**分词器**(**Tokenizer**)和**词元过滤器**(**Token Filters**)三部分。它的执行顺序如下: - -**_character filters_** -> **_tokenizer_** -> **_token filters_** - -## 字符过滤器(Character Filters) - -character filter 的输入是原始的文本 text,如果配置了多个,它会按照配置的顺序执行,目前 ES 自带的 character filter 主要有如下 3 类: - -1. **html strip character filter**:从文本中剥离 HTML 元素,并用其解码值替换 HTML 实体(如,将 **_`&amp;`_** 替换为 **_`&`_**)。 -2. **mapping character filter**:自定义一个 map 映射,可以进行一些自定义的替换,如常用的大写变小写也可以在该环节设置。 -3. **pattern replace character filter**:使用 java 正则表达式来匹配应替换为指定替换字符串的字符,此外,替换字符串可以引用正则表达式中的捕获组。 - -### HTML strip character filter - -HTML strip 如下示例: - -```bash -GET /_analyze -{ - "tokenizer": "keyword", - "char_filter": [ - "html_strip" - ], - "text": "

    I'm so happy!

    " -} -``` - -经过 **_`html_strip`_** 字符过滤器处理后,输出如下: - -``` -[ \nI'm so happy!\n ] -``` - -### Mapping character filter - -Mapping character filter 接收键和值映射(key => value)作为配置参数,每当在预处理过程中遇到与键值映射中的键相同的字符串时,就会使用该键对应的值去替换它。 - -原始文本中的字符串和键值映射中的键的匹配是贪心的,在对给定的文本进行预处理过程中如果配置的键值映射存在包含关系,会优先**匹配最长键**。同样也可以用空字符串进行替换。 - -mapping char_filter 不像 html_strip 那样拆箱即可用,必须先进行配置才能使用,它有两个属性可以配置: - -| 参数名称 | 参数说明 | -| :-------------------- | :--------------------------------------------------------------------------------------------- | -| **_`mappings`_** | 一组映射,每个元素的格式为 _key => value_。 | -| **_`mappings_path`_** | 一个相对或者绝对的文件路径,指向一个每行包含一个 _key =>value_ 映射的 UTF-8 编码文本映射文件。 | - -mapping char_filter 示例如下: - -```bash -GET /_analyze -{ - "tokenizer": "keyword", - "char_filter": [ - { - "type": "mapping", - "mappings": [ - "٠ => 0", - "١ => 1", - "٢ => 2", - "٣ => 3", - "٤ => 4", - "٥ => 5", - "٦ => 6", - "٧ => 7", - "٨ => 8", - "٩ => 9" - ] - } - ], - "text": "My license plate is ٢٥٠١٥" -} -``` - -分析结果如下: - -``` -[ My license plate is 25015 ] -``` - -### Pattern Replace character filter - -Pattern Replace character filter 支持如下三个参数: - -| 参数名称 | 参数说明 | -| :------------------ | :----------------------------------------------------------------------------- | -| **_`pattern`_** | 必填参数,一个 java 的正则表达式。 | -| **_`replacement`_** | 替换字符串,可以使用 **_`$1 ... $9`_** 语法来引用捕获组。 | -| **_`flags`_** | Java 正则表达式的标志,具体参考 java 的 java.util.regex.Pattern 类的标志属性。 | - -如将输入的 text 中大于一个的空格都转变为一个空格,在 settings 时,配置示例如下: - -```bash -"char_filter": { - "multi_space_2_one": { - "pattern": "[ ]+", - "type": "pattern_replace", - "replacement": " " - }, - ... -} -``` - -## 分词器(Tokenizer) - -tokenizer 即分词器,也是 analyzer 最重要的组件,它对文本进行分词;**一个 analyzer 必需且只可包含一个 tokenizer**。 - -ES 自带默认的分词器是 standard tokenizer,标准分词器提供基于语法的分词(基于 Unicode 文本分割算法),并且适用于大多数语言。 - -此外有很多第三方的分词插件,如中文分词界最经典的 ik 分词器,它对应的 tokenizer 分为 ik_smart 和 ik_max_word,一个是智能分词(针对搜索侧),一个是全切分词(针对索引侧)。 - -ES 默认提供的分词器 standard 对中文分词不优化,效果差,一般会安装第三方中文分词插件,通常首先 [elasticsearch-analysis-ik](https://github.com/medcl/elasticsearch-analysis-ik) 插件,它其实是 ik 针对的 ES 的定制版。 - -### elasticsearch-plugin 使用 - -在安装 elasticsearch-analysis-ik 第三方之前,我们首先要了解 es 的插件管理工具 **_`elasticsearch-plugin`_** 的使用。 - -现在的 elasticsearch 安装完后,在安装目录的 bin 目录下会存在 elasticsearch-plugin 命令工具,用它来对 es 插件进行管理。 - -``` -bin/elasticsearch-plugin -``` - -其实该命令的是软连接,原始路径是: - -``` -libexec/bin/elasticsearch-plugin -``` - -再进一步看脚本代码,你会发现,它是通过 **_`elasticsearch-cli`_** 执行 `libexec/lib/tools/plugin-cli/elasticsearch-plugin-cli-x.x.x.jar`。 - -但一般使用者了解 elasticsearch-plugin 命令使用就可: - -```bash -# 安装指定的插件到当前 ES 节点中 -elasticsearch-plugin install {plugin_url} - -# 显示当前 ES 节点已经安装的插件列表 -elasticsearch-plugin list - -# 删除已安装的插件 -elasticsearch-plugin remove {plugin_name} -``` - -> 在安装插件时,要保证安装的插件与 ES 版本一致。 - -### elasticsearch-analysis-ik 安装 - -在确定要安装的 ik 版本之后,执行如下命令: - -```bash -./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v{X.X.X}/elasticsearch-analysis-ik-{X.X.X}.zip -``` - -执行完安装命令后,我们会发现在 plugins 中多了 analysis-ik 目录,这里面主要存放的是源码 jar 包,此外,在 config 文件里也多了 analysis-ik 目录,里面主要是 ik 相关的配置,如 IKAnalyzer.cfg.xml 配置、词典文件等。 - -```bash -# 两个新增目录路径 -libexec/plugins/analysis-ik/ -libexec/config/analysis-ik/ -``` - -### elasticsearch-analysis-ik 使用 - -ES 5.X 版本开始安装完的 elasticsearch-analysis-ik 提供了两个分词器,分别对应名称是 **_ik_max_word_** 和 **_ik_smart_**,ik_max_word 是索引侧的分词器,走全切模式,ik_smart 是搜索侧的分词器,走智能分词,属于搜索模式。 - -#### 索引 mapping 设置 - -安装完 elasticsearch-analysis-ik 后,我们可以指定索引及指定字段设置可用的分析器(analyzer),示例如下: - -```json -{ - "qa": { - "mappings": { - "qa": { - "_all": { - "enabled": false - }, - "properties": { - "question": { - "type": "text", - "store": true, - "similarity": "BM25", - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - }, - "answer": { - "type": "text", - "store": false, - "similarity": "BM25", - "analyzer": "ik_max_word", - "search_analyzer": "ik_smart" - }, - ... - } - } - } - } -} -``` - -如上示例中,analyzer 指定 ik_max_word,即索引侧使用 ik 全切模式,search_analyzer 设置 ik_smart,即搜索侧使用 ik 智能分词模式。 - -#### 查看 ik 分词结果 - -es 提供了查看分词结果的 api **`analyze`**,具体示例如下: - -```bash -GET {index}/_analyze -{ - "analyzer" : "ik_smart", - "text" : "es 中文分词器安装" -} -``` - -输出如下: - -```json -{ - "tokens": [ - { - "token": "es", - "start_offset": 0, - "end_offset": 2, - "type": "CN_WORD", - "position": 0 - }, - { - "token": "中文", - "start_offset": 3, - "end_offset": 5, - "type": "CN_WORD", - "position": 1 - }, - { - "token": "分词器", - "start_offset": 5, - "end_offset": 8, - "type": "CN_WORD", - "position": 2 - }, - { - "token": "安装", - "start_offset": 8, - "end_offset": 10, - "type": "CN_WORD", - "position": 3 - } - ] -} -``` - -#### elasticsearch-analysis-ik 自定义词典 - -elasticsearch-analysis-ik 本质是 ik 分词器,使用者根据实际需求可以扩展自定义的词典,具体主要分为如下 2 大类,每类又分为本地配置和远程配置 2 种: - -1. 自定义扩展词典; -2. 自定义扩展停用词典; - -elasticsearch-analysis-ik 配置文件为 `IKAnalyzer.cfg.xml`,它位于 `libexec/config/analysis-ik` 目录下,具体配置结构如下: - -```xml - - - - IK Analyzer 扩展配置 - - - - - - - - - -``` - -> 当然,如果开发者认为 ik 默认的词表有问题,也可以进行调整,文件都在 `libexec/config/analysis-ik` 下,如 main.dic 为主词典,stopword.dic 为停用词表。 - -## 词元过滤器(Token Filters) - -token filters 叫词元过滤器,或词项过滤器,对 tokenizer 分出的词进行过滤处理。常用的有转小写、停用词处理、同义词处理等等。**一个 analyzer 可包含 0 个或多个词项过滤器,按配置顺序进行过滤**。 - -以同义词过滤器的使用示例,具体如下: - -```bash -PUT /test_index -{ - "settings": { - "index": { - "analysis": { - "analyzer": { - "synonym": { - "tokenizer": "standard", - "filter": [ "my_stop", "synonym" ] - } - }, - "filter": { - "my_stop": { - "type": "stop", - "stopwords": [ "bar" ] - }, - "synonym": { - "type": "synonym", - "lenient": true, - "synonyms": [ "foo, bar => baz" ] - } - } - } - } - } -} -``` - -### 同义词 - -Elasticsearch 同义词通过专有的同义词过滤器(synonym token filter)来进行工作,它允许在分析(analysis)过程中方便地处理同义词,一般是通过配置文件配置同义词。此外,同义词可以再建索引时(index-time synonyms)或者检索时(search-time synonyms)使用。 - -#### 同义词(synonym)配置语法 - -如上例子所示,es 同义词配置的 filter 语法具体如下选项: - -- **_`type`_**:指定 synonym,表示同义词 filter; - -- **_`synonyms_path`_**:指定同义词配置文件路径; - -- **`expand`**:该参数决定映射行为的模式,默认为 true,表示扩展模式,具体示例如下: - - - 当 **`expand == true`** 时, - - ``` - ipod, i-pod, i pod - ``` - - 等价于: - - ``` - ipod, i-pod, i pod => ipod, i-pod, i pod - ``` - - 当 **_`expand == false`_** 时, - - ``` - ipod, i-pod, i pod - ``` - - 仅映射第一个单词,等价于: - - ``` - ipod, i-pod, i pod => ipod - ``` - -- **_`lenient`_**:如果值为 true 时,遇到那些无法解析的同义词规则时,忽略异常。默认为 false。 - -#### 同义词文档格式 - -elasticsearch 的同义词有如下两种形式: - -- 单向同义词: - - ``` - ipod, i-pod, i pod => ipod - ``` - -- 双向同义词: - - ``` - 马铃薯, 土豆, potato - ``` - -单向同义词不管索引还是检索时,箭头左侧的词都会映射成箭头右侧的词; - -双向同义词是索引时,都建立同义词的倒排索引,检索时,同义词之间都会进行倒排索引的匹配。 - -> 同义词的文档化时,需要注意的是,同一个词在不同的同义词关系中出现时,其它同义词之间不具有传递性,这点需要注意。 - -假设如上示例中,如果“马铃薯”和其它两个同义词分成两行写: - -``` -马铃薯,土豆 -马铃薯,potato -``` - -此时,elasticsearch 中不会将“土豆”和“potato”视为同义词关系,所以多个同义词要写在一起,这往往是开发中经常容易疏忽的点。 - -## 参考资料 - -- [Elasticsearch 教程](https://www.knowledgedict.com/tutorial/elasticsearch-intro.html) \ No newline at end of file diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/11.ElasticsearchRestApi.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_API.md" similarity index 85% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/11.ElasticsearchRestApi.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_API.md" index 68b8d2292b..bbc4bf1fa1 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/11.ElasticsearchRestApi.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_API.md" @@ -1,7 +1,7 @@ --- -title: Elasticsearch Rest API +icon: logos:elasticsearch +title: ElasticSearch API date: 2020-06-16 07:10:44 -order: 11 categories: - 数据库 - 搜索引擎数据库 @@ -11,10 +11,10 @@ tags: - 搜索引擎数据库 - Elasticsearch - API -permalink: /pages/4b1907/ +permalink: /pages/24933bd4/ --- -# ElasticSearch Rest API +# ElasticSearch API > **[Elasticsearch](https://github.com/elastic/elasticsearch) 是一个分布式、RESTful 风格的搜索和数据分析引擎**,能够解决不断涌现出的各种用例。 作为 Elastic Stack 的核心,它集中存储您的数据,帮助您发现意料之中以及意料之外的情况。 > @@ -24,7 +24,87 @@ permalink: /pages/4b1907/ > > REST API 最详尽的文档应该参考:[ES 官方 REST API](https://www.elastic.co/guide/en/elasticsearch/reference/current/rest-apis.html) -## ElasticSearch Rest API 语法格式 +## ElasticSearch API 简介 + +Elasticsearch 官方提供了很多版本的 Java 客户端,包含但不限于: + +- [Transport Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html) - 7.0 废弃,8.0 移除。 +- Java REST 客户端 +- [Elasticsearch Java API Client](https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html) - + +如果当前是:8.X 版本,推荐 Elasticsearch `Java API`客户端。 + +如果当前是:7.X 版本且不考虑升级,推荐 `High Level REST`客户端。 + +如果当前是:5.X、6.X 版本,推荐尽早升级集群版本。 + +### Elasticsearch Java API Client 快速入门 + +:::detail 示例 + +```java +//创建一个低级的客户端 +final RestClient restClient = RestClient.builder(new HttpHost("localhost", 9200)).build(); +//创建 JSON 对象映射器 +final RestClientTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper()); +//创建 API 客户端 +final ElasticsearchClient client = new ElasticsearchClient(transport); +//查询所有索引------------------------------------------------------------------------------------- +final GetIndexResponse response = client.indices().get(query -> query.index("_all")); +final IndexState products = response.result().get("products"); +System.out.println(products.toString()); +//关闭 +client.shutdown(); +transport.close(); +restClient.close(); +``` + +::: + +### Transport Client 快速入门 + +`TransportClient` 使用 `transport` 模块远程连接到 Elasticsearch 集群。它不会加入集群,而只是获取一个或多个初始传输地址,并以轮询方式与它们通信。 + +> 扩展:https://www.elastic.co/guide/en/elasticsearch/client/java-api/current/transport-client.html + +:::detail 示例 + +启动客户端: + +```java +// 启动 +TransportClient client = new PreBuiltTransportClient(Settings.EMPTY) + .addTransportAddress(new TransportAddress(InetAddress.getByName("host1"), 9300)) + .addTransportAddress(new TransportAddress(InetAddress.getByName("host2"), 9300)); + +// 关闭 +client.close(); +``` + +配置集群名称 + +注意,如果使用的集群名称与 “elasticsearch” 不同,则必须设置集群名称。 + +```java +Settings settings = Settings.builder() + .put("cluster.name", "myClusterName").build(); +TransportClient client = new PreBuiltTransportClient(settings); +// Add transport addresses and do something with the client... +``` + +启用 sniffing + +```java +Settings settings = Settings.builder() + .put("client.transport.sniff", true).build(); +TransportClient client = new PreBuiltTransportClient(settings); +``` + +::: + +## ElasticSearch Rest + +### ElasticSearch Rest API 语法格式 向 Elasticsearch 发出的请求的组成部分与其它普通的 HTTP 请求是一样的: @@ -35,9 +115,9 @@ curl -X '://:/?' -d '' - `VERB`:HTTP 方法,支持:`GET`, `POST`, `PUT`, `HEAD`, `DELETE` - `PROTOCOL`:http 或者 https 协议(只有在 Elasticsearch 前面有 https 代理的时候可用) - `HOST`:Elasticsearch 集群中的任何一个节点的主机名,如果是在本地的节点,那么就叫 localhost -- `PORT`:Elasticsearch HTTP 服务所在的端口,默认为 9200 PATH API 路径(例如\_count 将返回集群中文档的数量), +- `PORT`:Elasticsearch HTTP 服务所在的端口,默认为 9200 PATH API 路径(例如、\_count 将返回集群中文档的数量), - `PATH`:可以包含多个组件,例如 `_cluster/stats` 或者 `_nodes/stats/jvm` -- `QUERY_STRING`:一些可选的查询请求参数,例如?pretty 参数将使请求返回更加美观易读的 JSON 数据 +- `QUERY_STRING`:一些可选的查询请求参数,例如?pretty 参数将使请求返回更加美观易读的 JSON 数据 - `BODY`:一个 JSON 格式的请求主体(如果请求需要的话) ElasticSearch Rest API 分为两种: @@ -53,7 +133,7 @@ Request Body Search 示例: ![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220530072654.png) -## 索引 API +### 索引 API > 参考资料:[Elasticsearch 官方之 cat 索引 API](https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-indices.html) @@ -127,11 +207,11 @@ GET kibana_sample_data_ecommerce # 查看索引的文档总数 GET kibana_sample_data_ecommerce/_count -# 查看前10条文档,了解文档格式 +# 查看前 10 条文档,了解文档格式 GET kibana_sample_data_ecommerce/_search # _cat indices API -# 查看indices +# 查看 indices GET /_cat/indices/kibana*?v&s=index # 查看状态为绿的索引 @@ -198,7 +278,7 @@ POST kibana_sample_data_ecommerce/_open POST kibana_sample_data_ecommerce/_close ``` -## 文档 +### 文档 ```bash ############Create Document############ @@ -210,7 +290,7 @@ POST users/_doc "message" : "trying out Kibana" } -#create document. 指定Id。如果id已经存在,报错 +#create document. 指定 Id。如果 id 已经存在,报错 PUT users/_doc/1?op_type=create { "user" : "Jack", @@ -230,9 +310,8 @@ PUT users/_create/1 #Get the document by ID GET users/_doc/1 - ### Index & Update -#Update 指定 ID (先删除,在写入) +#Update 指定 ID (先删除,在写入) GET users/_doc/1 PUT users/_doc/1 @@ -241,7 +320,6 @@ PUT users/_doc/1 } - #GET users/_doc/1 #在原文档上增加字段 POST users/_update/1/ @@ -252,17 +330,14 @@ POST users/_update/1/ } } - - ### Delete by Id # 删除文档 DELETE users/_doc/1 - ### Bulk 操作 #执行两次,查看每次的结果 -#执行第1次 +#执行第 1 次 POST _bulk { "index" : { "_index" : "test", "_id" : "1" } } { "field1" : "value1" } @@ -272,8 +347,7 @@ POST _bulk { "update" : {"_id" : "1", "_index" : "test"} } { "doc" : {"field2" : "value2"} } - -#执行第2次 +#执行第 2 次 POST _bulk { "index" : { "_index" : "test", "_id" : "1" } } { "field1" : "value1" } @@ -298,8 +372,7 @@ GET /_mget ] } - -#URI中指定index +#URI 中指定 index GET /test/_mget { "docs" : [ @@ -314,7 +387,6 @@ GET /test/_mget ] } - GET /_mget { "docs" : [ @@ -346,7 +418,6 @@ POST kibana_sample_data_ecommerce/_msearch {"index" : "kibana_sample_data_flights"} {"query" : {"match_all" : {}},"size":2} - ### 清除测试数据 #清除数据 DELETE users @@ -547,7 +618,7 @@ $ curl 'localhost:9200/user/admin/_search?pretty' ### 全文搜索 -ES 的查询非常特别,使用自己的[查询语法](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl.html),要求 GET 请求带有数据体。 +ES 的查询非常特别,使用自己的 [查询语法](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl.html),要求 GET 请求带有数据体。 ```bash $ curl -H 'Content-Type: application/json' 'localhost:9200/user/admin/_search?pretty' -d ' @@ -625,7 +696,7 @@ $ curl 'localhost:9200/user/admin/_search' -d ' 上面代码搜索的是`软件 or 系统`。 -如果要执行多个关键词的`and`搜索,必须使用[布尔查询](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl-bool-query.html)。 +如果要执行多个关键词的`and`搜索,必须使用 [布尔查询](https://www.elastic.co/guide/en/elasticsearch/reference/5.5/query-dsl-bool-query.html)。 ```bash $ curl -H 'Content-Type: application/json' 'localhost:9200/user/admin/_search?pretty' -d ' @@ -815,7 +886,7 @@ GET /movies/_search?q=title:(Beautiful NOT Mind) 示例: ```bash -# 范围查询 ,区间写法 +# 范围查询 , 区间写法 GET /movies/_search?q=title:beautiful AND year:{2010 TO 2018%7D { "profile":"true" @@ -1030,7 +1101,7 @@ POST movies/_search } ``` -## 集群 API +### 集群 API > [Elasticsearch 官方之 Cluster API](https://www.elastic.co/guide/en/elasticsearch/reference/current/cluster.html) @@ -1091,7 +1162,7 @@ GET /_cluster/health/kibana_sample_data_flights?level=shards GET /_cluster/state ``` -## 节点 API +### 节点 API > [Elasticsearch 官方之 cat Nodes API](https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-nodes.html)——返回有关集群节点的信息。 @@ -1102,7 +1173,7 @@ GET /_cat/nodes?v=true GET /_cat/nodes?v=true&h=id,ip,port,v,m ``` -## 分片 API +### 分片 API > [Elasticsearch 官方之 cat Shards API](https://www.elastic.co/guide/en/elasticsearch/reference/current/cat-shards.html)——shards 命令是哪些节点包含哪些分片的详细视图。它会告诉你它是主还是副本、文档数量、它在磁盘上占用的字节数以及它所在的节点。 @@ -1115,7 +1186,7 @@ GET /_cat/shards/my-index-* GET /_cat/shards?h=index,shard,prirep,state,unassigned.reason ``` -## 监控 API +### 监控 API Elasticsearch 中集群相关的健康、统计等相关的信息都是围绕着 `cat` API 进行的。 @@ -1156,7 +1227,5 @@ GET /_cat ## 参考资料 -- **官方** - - [Elasticsearch 官网](https://www.elastic.co/cn/products/elasticsearch) - - [Elasticsearch Github](https://github.com/elastic/elasticsearch) - - [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) \ No newline at end of file +- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) +- https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.clients diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/12.ElasticsearchHighLevelRestJavaApi.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_API_HighLevelRest.md" similarity index 98% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/12.ElasticsearchHighLevelRestJavaApi.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_API_HighLevelRest.md" index 907c670232..6b72ca5ae2 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/12.ElasticsearchHighLevelRestJavaApi.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_API_HighLevelRest.md" @@ -1,7 +1,7 @@ --- -title: ElasticSearch Java API 之 High Level REST Client +icon: logos:elasticsearch +title: ElasticSearch API 之 HighLevelRestClient date: 2022-03-01 18:55:46 -order: 12 categories: - 数据库 - 搜索引擎数据库 @@ -11,10 +11,10 @@ tags: - 搜索引擎数据库 - Elasticsearch - API -permalink: /pages/201e43/ +permalink: /pages/02faef83/ --- -# ElasticSearch Java API 之 High Level REST Client +# ElasticSearch API 之 HighLevelRestClient > Elasticsearch 官方的 High Level REST Client 在 7.1.5.0 版本废弃。所以本文中的 API 不推荐使用。 @@ -379,4 +379,4 @@ public void matchPhraseQuery() throws IOException { ## 参考资料 - **官方** - - [Java High Level REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html) \ No newline at end of file + - [Java High Level REST Client](https://www.elastic.co/guide/en/elasticsearch/client/java-rest/current/java-rest-high.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/10.Elasticsearch\346\200\247\350\203\275\344\274\230\345\214\226.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\344\274\230\345\214\226.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/10.Elasticsearch\346\200\247\350\203\275\344\274\230\345\214\226.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\344\274\230\345\214\226.md" index 718674621c..9d04d06ef1 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/10.Elasticsearch\346\200\247\350\203\275\344\274\230\345\214\226.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\344\274\230\345\214\226.md" @@ -1,7 +1,7 @@ --- -title: Elasticsearch 性能优化 +icon: logos:elasticsearch +title: Elasticsearch 优化 date: 2022-01-21 19:54:43 -order: 10 categories: - 数据库 - 搜索引擎数据库 @@ -11,10 +11,10 @@ tags: - 搜索引擎数据库 - Elasticsearch - 性能 -permalink: /pages/2d95ce/ +permalink: /pages/a363fdc7/ --- -# Elasticsearch 性能优化 +# Elasticsearch 优化 Elasticsearch 是当前流行的企业级搜索引擎,设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。作为一个开箱即用的产品,在生产环境上线之后,我们其实不一定能确保其的性能和稳定性。如何根据实际情况提高服务的性能,其实有很多技巧。这章我们分享从实战经验中总结出来的 elasticsearch 性能优化,主要从硬件配置优化、索引优化设置、查询方面优化、数据结构优化、集群架构优化等方面讲解。 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\345\210\206\346\236\220.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\345\210\206\346\236\220.md" new file mode 100644 index 0000000000..7bcc303606 --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\345\210\206\346\236\220.md" @@ -0,0 +1,346 @@ +--- +icon: logos:elasticsearch +title: Elasticsearch 文本分析 +date: 2022-02-22 21:01:01 +categories: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch + - 分词 +permalink: /pages/6bfb0fbf/ +--- + +# Elasticsearch 文本分析 + +**文本分析**是将非结构化文本转换为针对搜索优化的结构化格式的过程。 + +## 文本分析简介 + +文本分析使 Elasticsearch 能够执行全文搜索,其中搜索返回所有相关结果,而不仅仅是完全匹配。 + +文本分析可以分为两个方面: + +- **Tokenization(分词化)** - 分析通过分词化使全文搜索成为可能:将文本分解成更小的块,称为分词。在大多数情况下,这些标记是单独的 term(词项)。 +- **Normalizeation(标准化)** - 经过分词后的文本只能进行词项匹配,但是无法进行同义词匹配。为解决这个问题,可以将文本进行标准化处理。例如:将 `foxes` 标准化为 `fox`。 + +## Analyzer(分析器) + +文本分析由 [**analyzer(分析器)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html) 执行,分析器是一组控制整个过程的规则。无论是索引还是搜索,都需要使用分析器。 + +[**analyzer(分析器)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html) 由三个组件组成:零个或多个 [Character Filters(字符过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html)、有且仅有一个 [Tokenizer(分词器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html)、零个或多个 [Token Filters(分词过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html)。 + +它的执行顺序如下: + +``` +character filters -> tokenizer -> token filters +``` + +Elasticsearch 内置的分析器: + +- [`standard`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html) - 根据单词边界将文本划分为多个 term,如 Unicode 文本分割算法所定义。它删除了大多数标点符号、小写 term,并支持删除停用词。 +- [`simple`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-simple-analyzer.html) - 遇到非字母字符时将文本划分为多个 term,并将其转为小写。 +- [`whitespace`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-whitespace-analyzer.html) - 遇到任何空格时将文本划分为多个 term,不转换为小写。 +- [`stop`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stop-analyzer.html) - 与 [`simple`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-simple-analyzer.html) 相似,同时支持删除停用词(如:the、a、is)。 +- [`keyword`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-keyword-analyzer.html) - 部分词,直接将输入当做输出。 +- [`pattern`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-pattern-analyzer.html) - 使用正则表达式将文本拆分为 term。它支持小写和非索引字。 +- [`fingerprint`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-fingerprint-analyzer.html) - 可创建用于重复检测的指纹。 +- [语言分析器](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html) - 提供了 30 多种常见语言的分词器。 + +默认情况下,Elasticsearch 使用 [**standard analyzer(标准分析器)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html),它开箱即用,适用于大多数使用场景。Elasticsearch 也允许定制分析器。 + +### 测试分析器 + +[`_analyze` API](https://www.elastic.co/guide/en/elasticsearch/reference/current/indices-analyze.html) 是查看分析器如何分词的工具。 + +::: details 【示例】直接指定 analyzer 进行测试 + +查看不同的 analyzer 的效果 + +```json +GET _analyze +{ + "analyzer": "standard", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +GET _analyze +{ + "analyzer": "simple", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +GET _analyze +{ + "analyzer": "stop", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +GET _analyze +{ + "analyzer": "whitespace", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +GET _analyze +{ + "analyzer": "keyword", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +GET _analyze +{ + "analyzer": "pattern", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} +``` + +::: + +::: details 【示例】自由组合分析器组件进行测试 + +```json +POST _analyze +{ + "tokenizer": "standard", + "filter": [ "lowercase", "asciifolding" ], + "text": "Is this déja vu?" +} +``` + +::: + +### 指定分析器 + +内置分析器可以直接使用,无需任何配置。但是,其中一些支持配置选项来更改其行为。 + +在搜索时,Elasticsearch 通过按顺序检查以下参数来确定要使用的分析器: + +1. 搜索查询中的 [`analyzer`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer.html) 参数。请参阅 [指定查询的搜索分析器](https://www.elastic.co/guide/en/elasticsearch/reference/current/specify-analyzer.html#specify-search-query-analyzer)。 +2. 字段的 [`search_analyzer`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-analyzer.html) 映射参数。请参阅 [为字段指定搜索分析器](https://www.elastic.co/guide/en/elasticsearch/reference/current/specify-analyzer.html#specify-search-field-analyzer)。 +3. `analysis.analyzer.default_search` 索引设置。请参阅 [指定索引的默认搜索分析器](https://www.elastic.co/guide/en/elasticsearch/reference/current/specify-analyzer.html#specify-search-default-analyzer)。 +4. 字段的 [`analyzer`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer.html) mapping 参数。请参阅 [为字段指定分析器](https://www.elastic.co/guide/en/elasticsearch/reference/current/specify-analyzer.html#specify-index-field-analyzer)。 + +如果未指定这些参数,则使用 [`standard`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html) 分析器。 + +::: details 【示例】设置索引的默认分析器 + +将 std_english 分析器定义为基于标准分析器,但配置为删除预定义的英语停用词列表 + +```json +PUT my-index-000001 +{ + "settings": { + "analysis": { + "analyzer": { + "std_english": { + "type": "standard", + "stopwords": "_english_" + } + } + } + }, + "mappings": { + "properties": { + "my_text": { + "type": "text", + "analyzer": "standard", + "fields": { + "english": { + "type": "text", + "analyzer": "std_english" + } + } + } + } + } +} +``` + +::: + +::: details 【示例】设置字段的分析器 + +将字段 title 的分析器设为 `whitespace`: + +```json +PUT my-index-000001 +{ + "mappings": { + "properties": { + "title": { + "type": "text", + "analyzer": "whitespace" + } + } + } +} +``` + +::: + +::: details 【示例】指定查询的搜索分析器 + +```json +GET my-index-000001/_search +{ + "query": { + "match": { + "message": { + "query": "Quick foxes", + "analyzer": "stop" + } + } + } +} +``` + +::: + +::: details 【示例】指定字段的搜索分析器 + +```json +PUT my-index-000001 +{ + "mappings": { + "properties": { + "title": { + "type": "text", + "analyzer": "whitespace", + "search_analyzer": "simple" + } + } + } +} +``` + +::: + +::: details 【示例】指定索引的默认搜索分析器 + +创建索引时,可以使用该 `analysis.analyzer.default_search` 设置设置默认搜索分析器。如果提供了搜索分析器,则还必须使用 `analysis.analyzer.default` 设置指定默认索引分析器。 + +```json +PUT my-index-000001 +{ + "settings": { + "analysis": { + "analyzer": { + "default": { + "type": "simple" + }, + "default_search": { + "type": "whitespace" + } + } + } + } +} +``` + +::: + +### 自定义分析器 + +自定义分析器,需要指定 type 为 `custom` 类型。 + +```json +PUT my-index-000001 +{ + "settings": { + "analysis": { + "analyzer": { + "my_custom_analyzer": { + "type": "custom", + "tokenizer": "standard", + "char_filter": [ + "html_strip" + ], + "filter": [ + "lowercase", + "asciifolding" + ] + } + } + } + } +} +``` + +## 中文分词 + +在英文中,单词有自然的空格作为分隔。 + +在中文中,分词有以下难点: + +- 中文不能根据一个个汉字进行分词 +- 不同于英文可以根据自然的空格进行分词;中文中一般不会有空格。 +- 同一句话,在不同的上下文中,有不同个理解。例如:这个苹果,不大好吃;这个苹果,不大,好吃! + +可以使用一些插件来获得对中文更好的分析能力: + +- [analysis-icu](https://www.elastic.co/guide/en/elasticsearch/plugins/current/analysis-icu.html) - 添加了扩展的 Unicode 支持,包括更好地分析亚洲语言、Unicode 规范化、Unicode 感知大小写折叠、排序规则支持和音译。 +- [elasticsearch-analysis-ik](https://github.com/infinilabs/analysis-ik) - 支持自定义词库,支持热更新分词字典 +- [elasticsearch-thulac-plugin](https://github.com/microbun/elasticsearch-thulac-plugin) - 清华大学自然语言处理和社会人文计算实验室的一套中文分词器。 + +## Character Filters(字符过滤器) + +[Character Filters(字符过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html) 将原始文本作为字符流接收,并可以通过添加、删除或更改字符来转换文本。 + +分析器可以有**零个或多个** [Character Filters(字符过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html),如果配置了多个,它会按照配置的顺序执行。 + +Elasticsearch 内置的字符过滤器: + +- [`html_strip`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-htmlstrip-charfilter.html) - `html_strip`字符过滤器用于去除 HTML 元素(如 ``)并转义 HTML 实体(如 `&`)。 +- [`mapping`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-mapping-charfilter.html) - `mapping` 字符过滤器用于将指定字符串的任何匹配项替换为指定的替换项。 +- [`pattern_replace`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-pattern-replace-charfilter.html) - `pattern_replace` 字符筛选器将匹配正则表达式的任何字符替换为指定的替换。 + +## Tokenizer(分词器) + +[Tokenizer(分词器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html) 接收字符流,将其分解为分词(通常是单个单词),并输出一个分词流。 + +分词器还负责记录每个 term 的顺序或位置,以及该 term 所代表的原始单词的开始和结束字符偏移量。`` + +分析器**有且仅有一个** [Tokenizer(分词器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html)。 + +Elasticsearch 内置的分词器: + +- 面向单词的分词器 + - [`standard`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-tokenizer.html) - 将文本划分为单词边界上的 term,如 Unicode 文本分割算法所定义。它会删除大多数标点符号。它是大多数语言的最佳选择。 + - [`letter`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-letter-tokenizer.html) - 遇到非字母字符时将文本划分为多个 term。 + - [`lowercase`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lowercase-tokenizer.html) - 到非字母字符时将文本划分为多个 term,并将其转为小写。 + - [`whitespace`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-whitespace-tokenizer.html) - 遇到任何空格时将文本划分为多个 term。 + - [`uax_url_email`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-uaxurlemail-tokenizer.html) - 与 [`standard`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-tokenizer.html) 相似,不同之处在于它将 URL 和电子邮件地址识别为单个分词。 + - [`classic`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-classic-tokenizer.html) - 基于语法的英语分词器。 + - [`thai`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-thai-tokenizer.html) - 将泰语文本分割为单词。 +- 部分单词分词器 + - [`n-gram`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-ngram-tokenizer.html) - 遇到指定字符列表(例如空格或标点符号)中的任何一个时,将文本分解为单词,然后返回每个单词的 n-gram:一个连续字母的滑动窗口,例如 `quick`→ `[qu, ui, ic, ck]`。 + - [`edge_n-gram`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-edgengram-tokenizer.html) - 遇到指定字符列表(例如空格或标点符号)中的任何一个时,将文本分解为单词,然后返回锚定到单词开头的每个单词的 n 元语法,例如 `quick` → `[q, qu, qui, quic, quick]`。 +- 结构化文本分词器 + - [`keyword`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-keyword-tokenizer.html) - 接受给定的任何文本,并输出与单个 term 完全相同的文本。它可以与 [`lowercase`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lowercase-tokenfilter.html) 等分词过滤器结合使用,以规范化分析的 term。 + - [`pattern`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-pattern-tokenizer.html) - 使用正则表达式在文本与单词分隔符匹配时将文本拆分为 term,或者将匹配的文本捕获为 term。 + - [`simple_pattern`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-simplepattern-tokenizer.html) - 使用正则表达式将匹配的文本捕获为 term。它使用正则表达式特征的受限子集,并且通常比 [`pattern`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-pattern-tokenizer.html) 更快。 + - [`char_group`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-chargroup-tokenizer.html) - 可以通过要拆分的字符集进行配置,这通常比运行正则表达式代价更小。 + - [`simple_pattern_split`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-simplepatternsplit-tokenizer.html) - 使用与 [`simple_pattern`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-simplepattern-tokenizer.html) 分词器相同的受限正则表达式子集,但在匹配项处拆分输入,而不是将匹配项作为 term 返回。 + - [`path_hierarchy`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-pathhierarchy-tokenizer.html) - 基于文件系统的路径分隔符,进行拆分,例如 `/foo/bar/baz` → `[/foo, /foo/bar, /foo/bar/baz ]` 。 + +## Token Filters(分词过滤器) + +[Token Filters(分词过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html) 接收分词流,并可以添加、删除或更改分词。常用的分词过滤器有: [`lowercase`(小写转换)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lowercase-tokenfilter.html)、[`stop`(停用词处理)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stop-tokenfilter.html)、[`synonym`(同义词处理)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html) 等等。 + +分析器可以有零个或多个 [Token Filters(分词过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html),如果配置了多个,它会按照配置的顺序执行。 + +Elasticsearch 内置了很多分词过滤器,这里列举几个常见的: + +- [`classic`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-classic-tokenfilter.html) - 从单词末尾删除英语所有格 (`'s`),并删除首字母缩略词中的点。它使用 Lucene 的 [ClassicFilter](https://lucene.apache.org/core/9_12_0/analysis/common/org/apache/lucene/analysis/standard/ClassicFilter.html)。 +- [`lowercase`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lowercase-tokenfilter.html) - 将分词转为小写。 +- [`stop`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stop-tokenfilter.html) - 从分词中删除 [stop word(停用词)](https://en.wikipedia.org/wiki/Stop_word)。 +- [`synonym`](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-synonym-tokenfilter.html) - 允许在分析过程中轻松处理 [近义词](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-with-synonyms.html)。 + +## 参考资料 + +- [极客时间教程 - Elasticsearch 核心技术与实战](https://time.geekbang.org/course/detail/100030501-102659) +- [ES 官方文档之 Text Analysis](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/05.Elasticsearch\346\230\240\345\260\204.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\345\255\230\345\202\250.md" similarity index 67% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/05.Elasticsearch\346\230\240\345\260\204.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\345\255\230\345\202\250.md" index dcb818bd8d..aa55f35b20 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/05.Elasticsearch\346\230\240\345\260\204.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\345\255\230\345\202\250.md" @@ -1,7 +1,7 @@ --- -title: Elasticsearch 映射 -date: 2022-05-16 19:54:24 -order: 05 +icon: logos:elasticsearch +title: Elasticsearch 存储 +date: 2022-02-22 21:01:01 categories: - 数据库 - 搜索引擎数据库 @@ -10,15 +10,271 @@ tags: - 数据库 - 搜索引擎数据库 - Elasticsearch + - 存储 - 索引 -permalink: /pages/d1bae4/ +permalink: /pages/646e92be/ --- -# Elasticsearch 映射 +# Elasticsearch 存储 + +## 逻辑存储设计 + +Elasticsearch 的逻辑存储被设计为层级结构,自上而下为: + +``` +index -> type -> mapping -> document -> field +``` + +各层级结构的说明如下: + +### Document(文档) + +Elasticsearch 是面向文档的,这意味着读写数据的最小单位是文档。Elasticsearch 以 JSON 文档的形式序列化和存储数据。文档是一组字段,这些字段是包含数据的键值对。每个文档都有一个唯一的 ID。 + +一个简单的 Elasticsearch 文档可能如下所示: + +```json +{ + "_index": "my-first-elasticsearch-index", + "_id": "DyFpo5EBxE8fzbb95DOa", + "_version": 1, + "_seq_no": 0, + "_primary_term": 1, + "found": true, + "_source": { + "email": "john@smith.com", + "first_name": "John", + "last_name": "Smith", + "info": { + "bio": "Eco-warrior and defender of the weak", + "age": 25, + "interests": ["dolphins", "whales"] + }, + "join_date": "2024/05/01" + } +} +``` + +Elasticsearch 中的 document 是无模式的,也就是并非所有 document 都必须拥有完全相同的字段,它们不受限于同一个模式。 + +### Field(字段) + +field 包含数据的键值对。默认情况下,Elasticsearch 对每个字段中的所有数据建立索引,并且每个索引字段都具有专用的优化数据结构。 + +`document` 包含数据和元数据。[**Metadata Field(元数据字段)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html) 是存储有关文档信息的系统字段。在 Elasticsearch 中,元数据字段都以 `_` 开头。常见的元数据字段有: + +- [`_index`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html) - 文档所属的索引 +- [`_id`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html) - 文档的 ID +- [`_source`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html) - 表示文档原文的 JSON + +### Type(类型) + +在 Elasticsearch 中,**type 是 document 的逻辑分类**。每个 index 里可以有一个或多个 type。 + +不同的 type 应该有相似的结构(schema)。举例来说,`id`字段不能在这个组是字符串,在另一个组是数值。 + +> 注意:Elasticsearch 7.x 版已彻底移除 type。 + +### Index(索引) + +在 Elasticsearch 中,**可以将 index 视为 document 的集合**。每个索引存储在磁盘上的同组文件中;索引存储了所有映射类型的字段,还有一些设置。 + +Elasticsearch 会为所有字段建立索引,经过处理后写入一个倒排索引(Inverted Index)。查找数据的时候,直接查找该索引。 + +所以,Elasticsearch 数据管理的顶层单位就叫做 Index。它是单个数据库的同义词。每个 Index 的名字必须是小写。 + +### Elasticsearch 概念和 RDBM 概念 + +Elasticsearch 概念 vs. RDBM 概念 + +| Elasticsearch 概念 | RDBM 概念 | +| -------------------------------- | ------------------ | +| 索引(index) | 数据库(database) | +| 类型(type,6.0 废弃,7.0 移除) | 数据表(table) | +| 文档(docuemnt) | 行(row) | +| 字符(field) | 列(column) | +| 映射(mapping) | 表结构(schema) | + +## 物理存储设计 + +Elasticsearch 的物理存储,天然使用了分布式设计。 + +每个 Elasticsearch 进程都从属于一个 cluster,一个 cluster 可以有一个或多个 node(即 Elasticsearch 进程)。 + +Elasticsearch 存储会将每个 index 分为多个 shard,而 shard 可以分布在集群中不同节点上。正是由于这个机制,使得 Elasticsearch 有了水平扩展的能力。shard 也是 Elasticsearch 将数据从一个节点迁移到拎一个节点的最小单位。 + +Elasticsearch 的每个 shard 对应一个 Lucene index(一个包含倒排索引的文件目录)。Lucene index 又会被分解为多个 segment。segment 是索引中的内部存储元素,由于写入效率的考虑,所以被设计为不可变更的。segment 会定期 [合并](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html) 较大的 segment,以保持索引大小。简单来说,Lucene 就是一个 jar 包,里面包含了封装好的构建、管理倒排索引的算法代码。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411242138288.png) + +## 倒排索引 + +在搜索引擎中,每个文档都有一个对应的文档 ID,文档内容被表示为一系列关键词的集合。例如,文档 1 经过分词,提取了 20 个关键词,每个关键词都会记录它在文档中出现的次数和出现位置。 + +那么,倒排索引就是**关键词到文档** ID 的映射,每个关键词都对应着一系列的文件,这些文件中都出现了关键词。 + +举个例子,有以下文档: + +| DocId | Doc | +| ----- | ---------------------------------------------- | +| 1 | 谷歌地图之父跳槽 Facebook | +| 2 | 谷歌地图之父加盟 Facebook | +| 3 | 谷歌地图创始人拉斯离开谷歌加盟 Facebook | +| 4 | 谷歌地图之父跳槽 Facebook 与 Wave 项目取消有关 | +| 5 | 谷歌地图之父拉斯加盟社交网站 Facebook | + +对文档进行分词之后,得到以下**倒排索引**。 + +| WordId | Word | DocIds | +| ------ | -------- | --------- | +| 1 | 谷歌 | 1,2,3,4,5 | +| 2 | 地图 | 1,2,3,4,5 | +| 3 | 之父 | 1,2,4,5 | +| 4 | 跳槽 | 1,4 | +| 5 | Facebook | 1,2,3,4,5 | +| 6 | 加盟 | 2,3,5 | +| 7 | 创始人 | 3 | +| 8 | 拉斯 | 3,5 | +| 9 | 离开 | 3 | +| 10 | 与 | 4 | +| .. | .. | .. | + +另外,实用的倒排索引还可以记录更多的信息,比如文档频率信息,表示在文档集合中有多少个文档包含某个单词。 + +那么,有了倒排索引,搜索引擎可以很方便地响应用户的查询。比如用户输入查询 `Facebook`,搜索系统查找倒排索引,从中读出包含这个单词的文档,这些文档就是提供给用户的搜索结果。 + +要注意倒排索引的两个重要细节: + +- 倒排索引中的所有词项对应一个或多个文档; +- 倒排索引中的词项**根据字典顺序升序排列** + +> 上面只是一个简单的栗子,并没有严格按照字典顺序升序排列。 + +## Setting + +Elasticsearch 索引的配置项主要分为**静态配置属性**和**动态配置属性**,静态配置属性是索引创建后不能修改,而动态配置属性则可以随时修改。 + +Elasticsearch 索引设置的 api 为 **_settings_**,完整的示例如下: + +```bash +PUT /my_index +{ + "settings": { + "index": { + "number_of_shards": "1", + "number_of_replicas": "1", + "refresh_interval": "60s", + "analysis": { + "filter": { + "tsconvert": { + "type": "stconvert", + "convert_type": "t2s", + "delimiter": "," + }, + "synonym": { + "type": "synonym", + "synonyms_path": "analysis/synonyms.txt" + } + }, + "analyzer": { + "ik_max_word_synonym": { + "filter": [ + "synonym", + "tsconvert", + "standard", + "lowercase", + "stop" + ], + "tokenizer": "ik_max_word" + }, + "ik_smart_synonym": { + "filter": [ + "synonym", + "standard", + "lowercase", + "stop" + ], + "tokenizer": "ik_smart" + } + }, + "mapping": { + "coerce": "false", + "ignore_malformed": "false" + }, + "indexing": { + "slowlog": { + "threshold": { + "index": { + "warn": "2s", + "info": "1s" + } + } + } + }, + "provided_name": "hospital_202101070533", + "query": { + "default_field": "timestamp", + "parse": { + "allow_unmapped_fields": "false" + } + }, + "requests": { + "cache": { + "enable": "true" + } + }, + "search": { + "slowlog": { + "threshold": { + "fetch": { + "warn": "1s", + "info": "200ms" + }, + "query": { + "warn": "1s", + "info": "500ms" + } + } + } + } + } + } +} +``` + +### 固定属性 + +- **_`index.creation_date`_**:顾名思义索引的创建时间戳。 +- **_`index.uuid`_**:索引的 uuid 信息。 +- **_`index.version.created`_**:索引的版本号。 + +### 索引静态配置 + +- **_`index.number_of_shards`_**:索引的主分片数,默认值是 **_`5`_**。这个配置在索引创建后不能修改;在 Elasticsearch 层面,可以通过 **_`es.index.max_number_of_shards`_** 属性设置索引最大的分片数,默认为 **_`1024`_**。 +- **_`index.codec`_**:数据存储的压缩算法,默认值为 **_`LZ4`_**,可选择值还有 **_`best_compression`_**,它比 LZ4 可以获得更好的压缩比(即占据较小的磁盘空间,但存储性能比 LZ4 低)。 +- **_`index.routing_partition_size`_**:路由分区数,如果设置了该参数,其路由算法为:`( hash(_routing) + hash(_id) % index.routing_parttion_size ) % number_of_shards`。如果该值不设置,则路由算法为 `hash(_routing) % number_of_shardings`,`_routing` 默认值为 `_id`。 + +静态配置里,有重要的部分是配置分析器(config analyzers)。 + +- **`index.analysis`** + + :分析器最外层的配置项,内部主要分为 char_filter、tokenizer、filter 和 analyzer。 + + - **_`char_filter`_**:定义新的字符过滤器件。 + - **_`tokenizer`_**:定义新的分词器。 + - **_`filter`_**:定义新的 token filter,如同义词 filter。 + - **_`analyzer`_**:配置新的分析器,一般是 char_filter、tokenizer 和一些 token filter 的组合。 + +### 索引动态配置 + +- **_`index.number_of_replicas`_**:索引主分片的副本数,默认值是 **_`1`_**,该值必须大于等于 0,这个配置可以随时修改。 +- **_`index.refresh_interval`_**:执行新索引数据的刷新操作频率,该操作使对索引的最新更改对搜索可见,默认为 **_`1s`_**。也可以设置为 **_`-1`_** 以禁用刷新。更详细信息参考 [Elasticsearch 动态修改 refresh_interval 刷新间隔设置](https://www.knowledgedict.com/tutorial/elasticsearch-refresh_interval-settings.html)。 + +## Mapping 在 Elasticsearch 中,**`Mapping`**(映射),用来定义一个文档以及其所包含的字段如何被存储和索引,可以在映射中事先定义字段的数据类型、字段的权重、分词器等属性,就如同在关系型数据库中创建数据表时会设置字段的类型。 -Mapping 会把 JSON 文档映射成 Lucene 所需要的扁平格式 +Mapping 会把 json 文档映射成 Lucene 所需要的扁平格式 一个 Mapping 属于一个索引的 Type @@ -28,13 +284,15 @@ Mapping 会把 JSON 文档映射成 Lucene 所需要的扁平格式 每个 `document` 都是 `field` 的集合,每个 `field` 都有自己的数据类型。映射数据时,可以创建一个 `mapping`,其中包含与 `document` 相关的 `field` 列表。映射定义还包括元数据 `field`,例如 `_source` ,它自定义如何处理 `document` 的关联元数据。 -## 映射方式 +### 映射分类 在 Elasticsearch 中,映射可分为静态映射和动态映射。在关系型数据库中写入数据之前首先要建表,在建表语句中声明字段的属性,在 Elasticsearch 中,则不必如此,Elasticsearch 最重要的功能之一就是让你尽可能快地开始探索数据,文档写入 Elasticsearch 中,它会根据字段的类型自动识别,这种机制称为**动态映射**,而**静态映射**则是写入数据之前对字段的属性进行手工设置。 -### 静态映射 +#### 静态映射 -ES 官方将静态映射称为**显式映射([Explicit mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/explicit-mapping.html))**。**静态映射**是在创建索引时显示的指定索引映射。静态映射和 SQL 中在建表语句中指定字段属性类似。相比动态映射,通过静态映射可以添加更详细、更精准的配置信息。例如: +Elasticsearch 官方将静态映射称为**显式映射([Explicit mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/explicit-mapping.html))**。**静态映射**是在创建索引时手工指定索引映射。静态映射和 SQL 中在建表语句中指定字段属性类似。相比动态映射,通过静态映射可以添加更详细、更精准的配置信息。 + +例如: - 哪些字符串字段应被视为全文字段。 - 哪些字段包含数字、日期或地理位置。 @@ -82,7 +340,7 @@ GET /my-index-000001/_mapping GET /my-index-000001/_mapping/field/employee-id ``` -### 动态映射 +#### 动态映射 动态映射机制,允许用户不手动定义映射,Elasticsearch 会自动识别字段类型。在实际项目中,如果遇到的业务在导入数据之前不确定有哪些字段,也不清楚字段的类型是什么,使用动态映射非常合适。当 Elasticsearch 在文档中碰到一个以前没见过的字段时,它会利用动态映射来决定该字段的类型,并自动把该字段添加到映射中。 @@ -112,17 +370,17 @@ PUT data/_doc/1 启用动态字段映射后,Elasticsearch 使用内置规则来确定如何映射每个字段的数据类型。规则如下: -| **JSON 数据类型** | **`"dynamic":"true"`** | **`"dynamic":"runtime"`** | -| ------------------------------------------------------------------------------------------------------------------------------------ | --------------------------------------- | --------------------------- | -| `null` | 没有字段被添加 | 没有字段被添加 | -| `true` or `false` | `boolean` 类型 | `boolean` 类型 | -| 浮点型数字 | `float` 类型 | `double` 类型 | -| 数字 | 数字型 | `long` 类型 | -| JSON 对象 | `object` 类型 | 没有字段被添加 | -| 数组 | 由数组中第一个非空值决定 | 由数组中第一个非空值决定 | -| 开启[日期检测](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html#date-detection)的字符串 | `date` 类型 | `date` 类型 | -| 开启[数字检测](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html#numeric-detection)的字符串 | `float` 类型或 `long`类型 | `double` 类型或 `long` 类型 | -| 什么也没开启的字符串 | 带有 `.keyword` 子 field 的 `text` 类型 | `keyword` 类型 | +| **JSON 数据类型** | **`"dynamic":"true"`** | **`"dynamic":"runtime"`** | +| -------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | --------------------------- | +| `null` | 没有字段被添加 | 没有字段被添加 | +| `true` or `false` | `boolean` 类型 | `boolean` 类型 | +| 浮点型数字 | `float` 类型 | `double` 类型 | +| 数字 | 数字型 | `long` 类型 | +| JSON 对象 | `object` 类型 | 没有字段被添加 | +| 数组 | 由数组中第一个非空值决定 | 由数组中第一个非空值决定 | +| 开启 [日期检测](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html#date-detection) 的字符串 | `date` 类型 | `date` 类型 | +| 开启 [数字检测](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-field-mapping.html#numeric-detection) 的字符串 | `float` 类型或 `long`类型 | `double` 类型或 `long` 类型 | +| 什么也没开启的字符串 | 带有 `.keyword` 子 field 的 `text` 类型 | `keyword` 类型 | 下面举一个例子认识动态 mapping,在 Elasticsearch 中创建一个新的索引并查看它的 mapping,命令如下: @@ -213,7 +471,7 @@ PUT my-index-000001 } ``` -## 运行时字段 +### 运行时字段 运行时字段是在查询时评估的字段。运行时字段有以下作用: @@ -228,15 +486,13 @@ PUT my-index-000001 运行时字段在处理日志数据时很有用,尤其是当日志是不确定的数据结构时:这种情况下,会降低搜索速度,但您的索引大小要小得多,您可以更快地处理日志,而无需为它们设置索引。 -### 运行时字段的优点 - 因为**运行时字段没有被索引**,所以添加运行时字段不会增加索引大小。用户可以直接在 mapping 中定义运行时字段,从而节省存储成本并提高采集数据的速度。定义了运行时字段后,可以立即在搜索请求、聚合、过滤和排序中使用它。 如果将运行时字段设为索引字段,则无需修改任何引用运行时字段的查询。更好的是,您可以引用字段是运行时字段的一些索引,以及字段是索引字段的其他索引。您可以灵活地选择要索引哪些字段以及保留哪些字段作为运行时字段。 就其核心而言,运行时字段最重要的好处是能够在您提取字段后将字段添加到文档中。此功能简化了映射决策,因为您不必预先决定如何解析数据,并且可以使用运行时字段随时修改映射。使用运行时字段允许更小的索引和更快的摄取时间,这结合使用更少的资源并降低您的运营成本。 -## 字段数据类型 +### 字段数据类型 在 Elasticsearch 中,每个字段都有一个字段数据类型或字段类型,用于指示字段包含的数据类型(例如字符串或布尔值)及其预期用途。字段类型按系列分组。同一族中的类型具有完全相同的搜索行为,但可能具有不同的空间使用或性能特征。 @@ -286,7 +542,7 @@ Elasticsearch 提供了非常丰富的数据类型,官方将其分为以下几 - **其他类型** - [`percolator`](https://www.elastic.co/guide/en/elasticsearch/reference/current/percolator.html):使用 [Query DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) 编写的索引查询 -## 元数据字段 +#### 元数据字段 一个文档中,不仅仅包含数据 ,也包含**元数据**。元数据是用于描述文档的信息。 @@ -307,7 +563,7 @@ Elasticsearch 提供了非常丰富的数据类型,官方将其分为以下几 - [`_meta`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-meta-field.html):应用程序特定的元数据。 - [`_tier`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-tier-field.html):文档所属索引的当前数据层首选项。 -## 映射参数 +### 映射参数 Elasticsearch 提供了以下映射参数: @@ -342,7 +598,7 @@ Elasticsearch 提供了以下映射参数: - 起始和结束字符偏移量,用于将 term 和原始字符串进行映射 - 有效负载(如果可用) - 用户定义的,与 term 位置相关的二进制数据 -## 映射配置 +### 映射配置 - `index.mapping.total_fields.limit`:索引中的最大字段数。字段和对象映射以及字段别名计入此限制。默认值为 `1000`。 - `index.mapping.depth.limit`:字段的最大深度,以内部对象的数量来衡量。例如,如果所有字段都在根对象级别定义,则深度为 `1`。如果有一个对象映射,则深度为 `2`,以此类推。默认值为 `20`。 @@ -352,4 +608,6 @@ Elasticsearch 提供了以下映射参数: ## 参考资料 -- [Elasticsearch 官方文档之 Mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html) \ No newline at end of file +- [Elasticsearch 官网](https://www.elastic.co/) +- [Elasticsearch 官方文档之 Mapping](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html) +- https://blog.devgenius.io/elasticsearch-solution-to-searching-71116220c82f diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\220\234\347\264\242\344\270\212.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\220\234\347\264\242\344\270\212.md" new file mode 100644 index 0000000000..2165a1f7ce --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\220\234\347\264\242\344\270\212.md" @@ -0,0 +1,475 @@ +--- +icon: logos:elasticsearch +title: Elasticsearch 搜索(上) +date: 2024-11-22 07:37:46 +categories: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch + - 搜索 +permalink: /pages/b630d7a6/ +--- + +# Elasticsearch 搜索(上) + +## 搜索简介 + +Elasticsearch 支持多种搜索: + +- [**精确搜索(词项搜索)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/term-level-queries.html):搜索数值、日期、IP 或字符串的精确值或范围。 +- [**全文搜索**](https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html):搜索非结构化文本数据并查找与查询项最匹配的文档。 +- **向量搜索**:存储向量,并使用 ANN 或 [KNN](https://www.elastic.co/guide/en/elasticsearch/reference/current/knn-search.html) 搜索来查找相似的向量,从而支持 [语义搜索](https://www.elastic.co/guide/en/elasticsearch/reference/current/semantic-search.html) 等场景。 + +可以使用 [`_search API`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html) 来搜索和聚合 Elasticsearch 数据流或索引中的数据。API 的 `query` 请求采用 [DSL](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html) 语义来进行查询。 + +Elasticsearch 支持两种搜索方式:URI Query 和 Request Body Query(DSL) + +::: details URI Query 示例 + +```shell +GET /kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie +GET /kibana*/_search?q=customer_first_name:Eddie +GET /_all/_search?q=customer_first_name:Eddie +``` + +::: + +::: details Request Body Query(DSL)示例 + +```shell +POST /kibana_sample_data_ecommerce/_search +{ + "query": { + "match_all": {} + } +} +``` + +::: + +当文档存储在 Elasticsearch 中时,它会在 1 秒内近乎实时地被索引和完全搜索。 + +Elasticsearch 基于 Lucene 开发,并引入了分段搜索的概念。分段类似于倒排索引,但 Lucene 中的单词 `index` 表示“段的集合加上提交点”。提交后,将向提交点添加新分段并清除缓冲区。 + +位于 Elasticsearch 和磁盘之间的是文件系统缓存。内存中索引缓冲区的文档会被写入新的分段,然后写入文件系统缓存,然后才刷新到磁盘。 + +![A Lucene index with new documents in the in-memory buffer](https://www.elastic.co/guide/en/elasticsearch/reference/current/images/lucene-in-memory-buffer.png) + +Lucene 允许写入和打开新分段,使其包含的文档对搜索可见,而无需执行完全提交。这是一个比提交到磁盘要轻松得多的过程,并且可以频繁地完成而不会降低性能。 + +![The buffer contents are written to a segment, which is searchable, but is not yet committed](https://www.elastic.co/guide/en/elasticsearch/reference/current/images/lucene-written-not-committed.png) + +在 Elasticsearch 中,写入和打开新分段的这一过程称为刷新。刷新使自上次刷新以来对索引执行的所有操作都可用于搜索。 + +默认情况下,Elasticsearch 每秒定期刷新一次索引,但仅限于在过去 30 秒内收到一个或多个搜索请求的索引。这就是我们说 Elasticsearch 具有近实时搜索能力的原因:文档更改不会立即对搜索可见,但会在此时间范围内变得可见。 + +## 排序 + +在 Elasticsearch 中,默认排序是**按照相关性的评分(`_score`)**进行降序排序。`_score` 是浮点数类型,`_score` 评分越高,相关性越高。评分模型的选择可以通过 `similarity` 参数在映射中指定。 + +在 5.4 版本以前,默认的相关性算法是 TF-IDF。TF 是**词频**(term frequency),IDF 是**逆文档频率**(inverse document frequency)。一个简短的解释是,一个词条出现在某个文档中的次数越多,它就越相关;但是,如果该词条出现在不同的文档的次数越多,它就越不相关。5.4 版本以后,默认的相关性算法 BM25。 + +此外,也可以通过 `sort` 自定排序规则,如:按照字段的值排序、多级排序、多值字段排序、基于 geo(地理位置)排序以及自定义脚本排序。 + +::: details 排序示例 + +单字段排序 + +```json +POST /kibana_sample_data_ecommerce/_search +{ + "size": 5, + "query": { + "match_all": {} + }, + "sort": [ + {"order_date": {"order": "desc"}} + ] +} +``` + +多字段排序 + +```json +POST /kibana_sample_data_ecommerce/_search +{ + "size": 5, + "query": { + "match_all": {} + }, + "sort": [ + {"order_date": {"order": "desc"}}, + {"_doc":{"order": "asc"}}, + {"_score":{ "order": "desc"}} + ] +} +``` + +::: + +> 详情参考:[Sort search results](https://www.elastic.co/guide/en/elasticsearch/reference/current/sort-search-results.html) + +## 分页 + +默认情况下,Elasticsearch 搜索会返回前 10 个匹配的匹配项。 + +Elasticsearch 支持三种分页查询方式。 + +- from + size +- search after +- scroll + +### from + size + +可以使用 `from` 和 `size` 参数分别指定起始页和每页记录数。 + +当一个查询:from = 990, size = 10,会在每个分片上先获取 1000 个文档。然后,通过协调节点聚合所有结果。最后,再通过排序选取前 1000 个文档。 + +页数越深,占用内存越多。为了避免**深分页**问题,ES 默认限定最多搜索 10000 个文档,可以通过 [`index.max_result_window`](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html#index-max-result-window) 进行设置。 + +::: details from + size 分页查询示例 + +```json +POST /kibana_sample_data_ecommerce/_search +{ + "from": 2, + "size": 5, + "query": { + "match_all": {} + } +} +``` + +::: + +### scroll + +scroll 搜索方式类似于 RDBMS 中的游标,只允许向下翻页。每次下一页查询后,使用返回结果的 scroll id 来作为下一次翻页的标记。 + +scroll 在搜索初始化阶段会生成快照,后续数据的变化无法及时体现在查询结果,因此更加适合一次性批量查询或非实时数据的分页查询。 + +启用游标查询时,需要注意设定期望的过期时间(scroll = 1m),以降低维持游标查询窗口所需消耗的资源。 + +> 注意:Elasticsearch 官方不再建议使用 scroll 查询方式进行深分页,而是推荐使用 [`search_after`](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after) 和时间点(PIT)一起使用。 + +::: details scroll 分页查询示例 + +```json +POST /kibana_sample_data_ecommerce/_search?scroll=1m +{ + "size": 3, + "query": { + "match": { + "currency": "EUR" + } + } +} + +``` + +响应结果 + +```json +{ + "_scroll_id": "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAmTkWRTMzNmxBYmZUbUdsdFNqMnJoTl84Zw==", + "took": 0, + "timed_out": false, + "_shards": { + // 略 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": 1, + "hits": [] // 略 + } +} +``` + +::: + +> 详情参考:[Paginate search results](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html) + +### search after + +search after 搜索方式不支持指定页数,只能向下翻页;并且需要指定 sort,并保证值是唯一的。然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询。 + +search after 实现的思路同 scroll 方式基本一致,通过记录上一次分页的位置标识,来进行下一次分页查询。相比于 scroll 方式,它的优点是可以实时获取数据的变化,解决了查询快照导致的查询结果延迟问题。 + +::: details search after 分页查询示例 + +第一次查询 + +```json + +POST /kibana_sample_data_ecommerce/_search +{ + "size": 5, + "query": { + "match_all": {} + }, + "sort": [ + {"order_date": {"order": "desc"}} + ] +} +``` + +响应结果 + +```json +{ + "took": 2609, + "timed_out": false, + "_shards": { + // 略 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": null, + "hits": [ + // 略多条记录 + // 最后一条记录 + { + // 略 + "sort": [1642893235000] + } + ] + } +} +``` + +从上次查询的响应中获取 `sort` 值,然后将 sort 值插入 search after 数组: + +```json +POST /kibana_sample_data_ecommerce/_search +{ + "size": 5, + "query": { + "match_all": {} + }, + "search_after": [ + 1642893235000 + ], + "sort": [ + { + "order_date": { + "order": "desc" + } + } + ] +} +``` + +::: + +## 限定字段 + +默认情况下,搜索响应中的每个点击都包含 [`_source`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html),该字段保存了原始文本的 JSON 对象。有两种推荐的方法可以从搜索查询中检索所选字段: + +- 使用 [`fields`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#search-fields-param) 选项指定响应结果中返回的值。 +- 如果需要在查询时返回原始文本数据,可以使用 [`_source`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-fields.html#source-filtering) 选项。 + +## 折叠搜索结果 + +Elasticsearch 中,可以通过 collapse 对搜索结果进行分组,且每个分组只显示该分组的一个代表文档。 + +::: details collapse 查询示例 + +```json +POST /kibana_sample_data_ecommerce/_search +{ + "size": 10, + "query": { + "match_all": {} + }, + "collapse": { + "field": "day_of_week" + } +} +``` + +响应结果: + +```json +{ + "took": 106, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 4675, + "relation": "eq" + }, + "max_score": null, + "hits": [ + { + "_index": "kibana_sample_data_ecommerce", + "_type": "_doc", + "_id": "yZUtBX4BU8KXl1YJRBrH", + "_score": 1, + "fields": { + "day_of_week": ["Monday"] + } + }, + { + "_index": "kibana_sample_data_ecommerce", + "_type": "_doc", + "_id": "ypUtBX4BU8KXl1YJRBrH", + "_score": 1, + "fields": { + "day_of_week": ["Sunday"] + } + }, + { + "_index": "kibana_sample_data_ecommerce", + "_type": "_doc", + "_id": "1JUtBX4BU8KXl1YJRBrH", + "_score": 1, + "fields": { + "day_of_week": ["Tuesday"] + } + }, + { + "_index": "kibana_sample_data_ecommerce", + "_type": "_doc", + "_id": "1ZUtBX4BU8KXl1YJRBrH", + "_score": 1, + "fields": { + "day_of_week": ["Wednesday"] + } + }, + { + "_index": "kibana_sample_data_ecommerce", + "_type": "_doc", + "_id": "2JUtBX4BU8KXl1YJRBrH", + "_score": 1, + "fields": { + "day_of_week": ["Saturday"] + } + }, + { + "_index": "kibana_sample_data_ecommerce", + "_type": "_doc", + "_id": "2ZUtBX4BU8KXl1YJRBrH", + "_score": 1, + "fields": { + "day_of_week": ["Thursday"] + } + }, + { + "_index": "kibana_sample_data_ecommerce", + "_type": "_doc", + "_id": "35UtBX4BU8KXl1YJRBrI", + "_score": 1, + "fields": { + "day_of_week": ["Friday"] + } + } + ] + } +} +``` + +::: + +## 过滤搜索结果 + +使用带有 `filter` 子句的布尔查询,可以过滤搜索和聚合的结果。 + +使用 [`post_filter`](https://www.elastic.co/guide/en/elasticsearch/reference/current/filter-search-results.html#post-filter) 可以过滤搜索的结果,但不能过滤聚合结果。 + +:::details filter 示例 + +```json +POST /kibana_sample_data_ecommerce/_search +{ + "size": 10, + "query": { + "bool": { + "filter": { + "range": { + "taxful_total_price": { + "gte": 0, + "lte": 10 + } + } + } + } + } +} +``` + +::: + +## 高亮 + +Elasticsearch 的高亮(highlight)可以从搜索结果中的一个或多个字段中获取突出显示的摘要,以便向用户显示查询匹配的位置。当请求突出显示(即高亮)时,响应结果的 `highlight` 字段中包括高亮的字段和高亮的片段。Elasticsearch 默认会用 `` 标签标记关键字。 + +Elasticsearch 提供了三种高亮器,分别是**默认的 highlighter 高亮器**、**postings-highlighter 高亮器** 和 **fast-vector-highlighter 高亮器**。 + +::: details 高亮结果示例 + +```json +POST /kibana_sample_data_ecommerce/_search +{ + "size": 10, + "query": { + "match_all": {} + }, + "highlight": { + "fields": { + "user": { + "pre_tags": [ + "" + ], + "post_tags": [ + "" + ] + } + } + } +} +``` + +::: + +> 详情参考:[Highlighting](https://www.elastic.co/guide/en/elasticsearch/reference/current/highlighting.html) + +## 分片路由搜索 + +Elasticsearch 可以在多个节点上的多个分片中存储索引数据的副本。在运行搜索请求时,Elasticsearch 会选择包含索引数据副本的节点,并将搜索请求转发到该节点的分片。此过程称为**路由**。 + +默认情况下,Elasticsearch 使用自适应副本选择来路由搜索请求。默认情况下,自适应副本选择从所有符合条件的节点和分片中进行选择。如果要限制符合搜索请求条件的节点和分片集,可以使用 [`preference`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-search.html#search-preference) 查询参数。 + +> 详情参考:[Search shard routing](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-shard-routing.html) + +## 查询规则 + +Elasticsearch 允许自定义查询规则来进行搜索。 + +> 详情参考:[Searching with query rules](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-using-query-rules.html) + +## 搜索模板 + +搜索模板是可以使用不同变量运行的存储搜索。 + +> 详情参考:[Search templates](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-template.html) + +## 参考资料 + +- [极客时间教程 - Elasticsearch 核心技术与实战](https://time.geekbang.org/course/detail/100030501-102659) +- [ES 官方文档之 Search your data](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-with-elasticsearch.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\220\234\347\264\242\344\270\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\220\234\347\264\242\344\270\213.md" new file mode 100644 index 0000000000..99d07bfc97 --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\220\234\347\264\242\344\270\213.md" @@ -0,0 +1,1147 @@ +--- +icon: logos:elasticsearch +title: Elasticsearch 搜索(下) +date: 2022-01-18 08:01:08 +categories: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch + - 查询 + - DSL +permalink: /pages/f8fab8f0/ +--- + +# Elasticsearch 搜索(下) + +Elasticsearch 提供了基于 JSON 的 DSL(Domain Specific Language)来定义查询。 + +可以将 DSL 视为查询的 AST(抽象语法树),由两种类型的子句组成: + +- 叶子查询 - 在指定字段中查找特定值,例如:[`match`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html)、[`term`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 和 [`range`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html)。 +- 组合查询 - 组合其他叶子查询或组合查询,用于以逻辑方式组合多个查询(例如: [`bool`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html)、[`dis_max`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html)),或更改它们的行为(例如:[`constant_score`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html))。 + +查询子句的行为会有所不同,具体取决于它们是在 query content 还是 filter context 中使用。 + +- `query` context - **有相关性计算**,采用相关性算法,计算文档与查询关键词之间的相关度,并根据评分(`_score`)大小排序。 +- `filter` context - **无相关性计算**,可以利用缓存,性能更好。 + +从用法角度,Elasticsearch 查询分类大致分为: + +- [Compound(组合查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html) +- [Term-level(词项查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/term-level-queries.html) +- [Full text(全文查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html) +- [Joining(联结查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/joining-queries.html) +- [Specialized(专用查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/specialized-queries.html) +- [Geo(地理位置查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-queries.html) +- [Span(跨度查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/span-queries.html) +- [Vector(向量查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/vector-queries.html) +- [Shape(形状查询)](https://www.elastic.co/guide/en/elasticsearch/reference/current/shape-queries.html) + +## 全文查询 + +[Full Text Search(全文搜索)](https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html) 支持在非结构化文本数据中搜索与查询关键字最匹配的数据。 + +在 ES 中,支持以下全文搜索方式: + +- [intervals](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-intervals-query.html) - 根据匹配词的顺序和近似度返回文档。 +- [match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) - **匹配查询**,用于执行全文搜索的标准查询,包括模糊匹配和短语或邻近查询。 +- [match_bool_prefix](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-bool-prefix-query.html) - 对检索文本分词,并根据这些分词构造一个布尔查询。除了最后一个分词之外的每个分词都进行 term 查询。最后一个分词用于 `prefix` 查询;其他分词都进行 `term` 查询。 +- [match_phrase](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) - 短语匹配查询。 +- [match_phrase_prefix](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html) - 与 `match_phrase` 查询类似,但对最后一个单词执行通配符搜索。 +- [multi_match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html) 支持多字段 match 查询 +- [combined_fields](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-combined-fields-query.html) - 匹配多个字段,就像它们已索引到一个组合字段中一样。 +- [query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-query-string-query.html) - 支持紧凑的 Lucene [query string(查询字符串)语法](https://www.elastic.co/guide/en/elasticsearch/reference/8.16/query-dsl-query-string-query.html#query-string-syntax),允许指定 `AND|OR|NOT` 条件和单个查询字符串中的多字段搜索。仅适用于专家用户。 +- [simple_query_string](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-simple-query-string-query.html) - 更简单、更健壮的 `query_string` 语法版本,适合直接向用户公开。 + +### match(匹配查询) + +[**`match`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) 查询用于搜索单个字段。首先,会针对检索文本进行解析(分词),分词后的任何一个词项只要被匹配,文档就会被搜到。默认情况下,相当于对分词后词项进行 or 匹配操作。 + +:::details match 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "match": { + "customer_full_name": { + "query": "George Hubbard" + } + } + } +} +``` + +响应结果: + +```json +{ + "took": 891, // 查询使用的毫秒数 + "timed_out": false, // 是否有分片超时,也就是说是否只返回了部分结果 + "_shards": { + // 总分片数、响应成功/失败数量信息 + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + // 搜索结果 + "total": { + // 匹配的总记录数 + "value": 82, + "relation": "eq" + }, + "max_score": 10.018585, // 所有匹配文档中的最大相关性分值 + "hits": [ + // 匹配文档列表 + { + "_index": "kibana_sample_data_ecommerce", // 文档所属索引 + "_type": "_doc", // 文档所属 type + "_id": "2ZUtBX4BU8KXl1YJRBrH", // 文档的唯一性标识 + "_score": 10.018585, // 文档的相关性分值 + "_source": { + // 文档的原始 JSON 对象 + // 略 + } + } + // 省略多条记录 + ] + } +} +``` + +::: + +可以通过组合 `` 和 `query` 参数来简化匹配查询语法。下面是一个简单的示例。 + +:::details match 简写示例 + +下面的查询等价于前面的匹配查询示例: + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "match": { + "customer_full_name": "George Hubbard" + } + } +} +``` + +::: + +在进行全文本字段检索的时候, match API 提供了 operator 和 minimum_should_match 参数: + +- **operator** 参数值可以为 “or” 或者 “and” 来控制检索词项间的关系,默认值为 “or”。所以上面例子中,只要书名中含有 “linux” 或者 “architecture” 的文档都可以匹配上。 +- **[minimum_should_match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-minimum-should-match.html)** 可以指定词项的最少匹配个数,其值可以指定为某个具体的数字,但因为我们无法预估检索内容的词项数量,一般将其设置为一个百分比。 + +:::details minimum_should_match 示例 + +至少有 50% 的词项匹配的文档才会被返回: + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "match": { + "category": { + "query": "Women Clothing Accessories", + "operator": "or", + "minimum_should_match": "50%" + } + } + } +} +``` + +::: + +match 查询提供了 fuzziness 参数,**fuzziness** 允许基于被查询字段的类型进行模糊匹配。请参阅 [Fuzziness](https://www.elastic.co/guide/en/elasticsearch/reference/current/common-options.html#fuzziness) 的配置。 + +在这种情况下可以设置 `prefix_length` 和 `max_expansions` 来控制模糊匹配。如果设置了模糊选项,查询将使用 `top_terms_blended_freqs_${max_expansions}` 作为其重写方法,`fuzzy_rewrite` 参数允许控制查询将如何被重写。 + +默认情况下允许模糊倒转 (`ab` → `ba`),但可以通过将 `fuzzy_transpositions` 设置为 `false` 来禁用。 + +:::details fuzziness 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "match": { + "customer_first_name": { + "query": "Gearge", + "fuzziness": "AUTO" + } + } + } +} +``` + +::: + +如果使用的分析器像 stop 过滤器一样删除查询中的所有标记,则默认行为是不匹配任何文档。可以使用 `zero_terms_query` 选项来改变默认行为,它接受 `none`(默认)和 `all` (相当于 `match_all` 查询)。 + +:::details zero_terms_query 示例 + +```bash +GET kibana_sample_data_logs/_search +{ + "query": { + "match": { + "message": { + "query": "Mozilla Linux", + "operator": "and", + "zero_terms_query": "all" + } + } + } +} +``` + +::: + +### match_phrase(短语匹配查询) + +[**`match_phrase`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) 查询首先会对检索内容进行分词,分词器可以自定义,同时文档还要满足以下两个条件才会被搜索到: + +1. **分词后所有词项都要出现在该字段中(相当于 and 操作)**。 +2. **字段中的词项顺序要一致**。 + +:::details match_phrase 示例 + +```bash +GET kibana_sample_data_logs/_search +{ + "query": { + "match_phrase": { + "agent": { + "query": "Linux x86_64" + } + } + } +} +``` + +::: + +### match_phrase_prefix(短语前缀匹配查询) + +查询和 [**`match_phrase`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) 查询类似,只不过 [**`match_phrase_prefix`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html) 最后一个 term 会被作为前缀匹配。 + +:::details match_phrase_prefix 示例 + +匹配以 `https://www.elastic.co/download` 开头的短语 + +```bash +GET kibana_sample_data_logs/_search +{ + "query": { + "match_phrase_prefix": { + "url": { + "query": "https://www.elastic.co/download" + } + } + } +} +``` + +::: + +### multi_match(多字段匹配查询) + +[**`multi_match`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html) 查询允许对多个字段执行相同的匹配查询。 + +`multi_match` 查询在内部执行的方式取决于 type 参数,可以设置为: + +- [`best_fields`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#type-best-fields) -(默认)将所有与查询匹配的文档作为结果返回,但是只使用评分最高的字段的评分来作为评分结果返回。 +- [`most_fields`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#type-most-fields) - 将所有与查询匹配的文档作为结果返回,并将所有匹配字段的评分累加起来作为评分结果。 +- [`cross_fields`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#type-cross-fields) - 将具有相同分析器的字段视为一个大字段。在每个字段中查找每个单词。例如当需要查询英文人名的时候,可以将 first_name 和 last_name 两个字段组合起来当作 full_name 来查询。 +- [`phrase`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#type-phrase) - 对每个字段运行 `match_phrase` 查询,并将最佳匹配字段的评分作为结果返回。 +- [`phrase_prefix`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#type-phrase) - 对每个字段运行 `match_phrase_prefix` 查询,并将最佳匹配字段的评分作为结果返回。 +- [`bool_prefix`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html#type-bool-prefix) - 在每个字段上创建一个 [match_bool_prefix](https://link.juejin.cn/?target=https%3A%2F%2Fwww.elastic.co%2Fguide%2Fen%2Felasticsearch%2Freference%2F7.13%2Fquery-dsl-match-bool-prefix-query.html) 查询,并且合并每个字段的评分作为评分结果。 + +:::details multi_match 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "multi_match": { + "query": 34.98, + "fields": [ + "taxful_total_price", + "taxless_total_*" # 可以使用通配符 + ] + } + } +} +``` + +::: + +## 词项级别查询 + +**`Term`(词项)是表达语意的最小单位**。搜索和利用统计语言模型进行自然语言处理都需要处理 Term。 + +全文查询在执行查询之前会分析查询字符串。 + +与全文查询不同,**词项级别查询不会分词**,而是将输入作为一个整体,在倒排索引中查找准确的词项。并且使用相关度计算公式为每个包含该词项的文档进行相关度计算。一言以概之:**词项查询是对词项进行精确匹配**。词项查询通常用于结构化数据,如数字、日期和枚举类型。 + +词项查询有以下类型: + +- **[exists](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html)** - 返回在指定字段上有值的文档。 +- **[fuzzy](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html)** - 模糊查询,返回包含与搜索词相似的词的文档。 +- **[ids](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-ids-query.html)** - 根据 ID 返回文档。此查询使用存储在 `_id` 字段中的文档 ID。 +- **[prefix](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html)** - 前缀查询,用于查询某个字段中包含指定前缀的文档。 +- **[range](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html)** - 范围查询,用于匹配在某一范围内的数值型、日期类型或者字符串型字段的文档。 +- **[regexp](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html)** - 正则匹配查询,返回与正则表达式相匹配的词项所属的文档。 +- **[term](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html)** - 用来查找指定字段中包含给定单词的文档。 +- **[terms](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html)** - 与 [**`term`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 相似,但可以搜索多个值。 +- **[terms set](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-set-query.html)** - 与 [**`term`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 相似,但可以定义返回文档所需的匹配词数。 +- **[wildcard](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html)** - 通配符查询,返回与通配符模式匹配的文档。 + +### exists(字段不为空查询) + +[**`exists`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html) 返回在指定字段上有值的文档。 + +由于多种原因,文档字段可能不存在索引值: + +- JSON 中的字段为 `null` 或 `[]` +- 该字段在 mapping 中配置了 `"index" : false` +- 字段值的长度超过了 mapping 中的 `ignore_above` 设置 +- 字段值格式错误,并且在 mapping 中定义了 `ignore_malformed` + +:::details exists 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "exists": { + "field": "email" + } + } +} +``` + +::: + +### fuzzy(模糊查询) + +[**`fuzzy`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html) 返回包含与搜索词相似的词的文档。ES 使用 [Levenshtein edit distance(Levenshtein 编辑距离)](https://en.wikipedia.org/wiki/Levenshtein_distance) 测量相似度或模糊度。 + +编辑距离是将一个术语转换为另一个术语所需的单个字符更改的数量。这些变化可能包括: + +- 改变一个字符:(**b**ox -> **f**ox) +- 删除一个字符:(**b**lack -> lack) +- 插入一个字符:(sic -> sic**k**) +- 反转两个相邻字符:(**ac**t → **ca**t) + +为了找到相似的词条,fuzzy query 会在指定的编辑距离内创建搜索词条的所有可能变体或扩展集。然后返回完全匹配任意扩展的文档。 + +注意:如果配置了 [`search.allow_expensive_queries`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html#query-dsl-allow-expensive-queries) ,则 fuzzy query 不能执行。 + +:::details fuzzy 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "fuzzy": { + "customer_full_name": { + "value": "mary" + } + } + } +} +``` + +::: + +### prefix(前缀查询) + +[**`prefix`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html#prefix-query-ex-request) 用于查询某个字段中包含指定前缀的文档。 + +比如查询 `user.id` 中含有以 `ki` 为前缀的关键词的文档,那么含有 `kind`、`kid` 等所有以 `ki` 开头关键词的文档都会被匹配。 + +:::details prefix 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "prefix": { + "customer_full_name": { + "value": "mar" + } + } + } +} +``` + +::: + +### range(范围查询) + +[**`range`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html) 用于匹配在某一范围内的数值型、日期类型或者字符串型字段的文档。比如搜索哪些书籍的价格在 50 到 100 之间、哪些书籍的出版时间在 2015 年到 2019 年之间。**使用 range 查询只能查询一个字段,不能作用在多个字段上**。 + +`range` 查询支持的参数有以下几种 - + +- **`gt`** - 大于 +- **`gte`** - 大于等于 +- **`lt`** - 小于 +- **`lte`** - 小于等于 +- **`format`** - 如果字段是 Date 类型,可以设置日期格式化 +- **`time_zone`** - 时区 +- **`relation`** - 指示范围查询如何匹配范围字段的值。 + - **`INTERSECTS` (Default)** - 匹配与查询字段值范围相交的文档。 + - **`CONTAINS`** - 匹配完全包含查询字段值的文档。 + - **`WITHIN`** - 匹配具有完全在查询范围内的范围字段值的文档。 + +:::details range 示例 + +数值范围查询示例: + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "range": { + "taxful_total_price": { + "gt": 10, + "lte": 50 + } + } + } +} +``` + +日期范围查询示例: + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "range": { + "order_date": { + "time_zone": "+00:00", + "gte": "2018-01-01T00:00:00", + "lte": "now" + } + } + } +} +``` + +::: + +### regexp(正则匹配查询) + +[**`regexp`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html) 返回与正则表达式相匹配的词项所属的文档。[正则表达式](https://zh.wikipedia.org/zh-hans/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F) 是一种使用占位符字符匹配数据模式的方法,称为运算符。 + +注意:如果配置了 [`search.allow_expensive_queries`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html#query-dsl-allow-expensive-queries) ,则 [**`regexp query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html) 会被禁用。 + +:::details regexp 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "regexp": { + "email": ".*@.*-family.zzz" + } + } +} +``` + +::: + +### term(词项匹配查询) + +[**`term`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 用来查找指定字段中包含给定单词的文档,term 查询不被解析,只有查询词和文档中的词精确匹配才会被搜索到,应用场景为查询人名、地名等需要精准匹配的需求。 + +注意:应避免 term 查询对 text 字段使用查询。默认情况下,Elasticsearch 针对 text 字段的值进行解析分词,这会使查找 text 字段值的精确匹配变得困难。要搜索 text 字段值,需改用 match 查询。 + +:::details term 示例 + +```bash +# 1. 创建一个索引 +DELETE my-index-000001 +PUT my-index-000001 +{ + "mappings": { + "properties": { + "full_text": { "type": "text" } + } + } +} + +# 2. 使用 "Quick Brown Foxes!" 关键字查 "full_text" 字段 +PUT my-index-000001/_doc/1 +{ + "full_text": "Quick Brown Foxes!" +} + +# 3. 使用 term 查询 +GET my-index-000001/_search?pretty +{ + "query": { + "term": { + "full_text": "Quick Brown Foxes!" + } + } +} +# 因为 full_text 字段不再包含确切的 Term —— "Quick Brown Foxes!",所以 term query 搜索不到任何结果 + +# 4. 使用 match 查询 +GET my-index-000001/_search?pretty +{ + "query": { + "match": { + "full_text": "Quick Brown Foxes!" + } + } +} + +DELETE my-index-000001 +``` + +> :warning: 注意:应避免 term 查询对 text 字段使用查询。 +> +> 默认情况下,Elasticsearch 针对 text 字段的值进行解析分词,这会使查找 text 字段值的精确匹配变得困难。 +> +> 要搜索 text 字段值,需改用 match 查询。 + +::: + +### terms(多词项匹配查询) + +[**`terms`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html) 与 [**`term`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html) 相同,但可以搜索多个值。 + +terms query 查询参数: + +- **`index`**:索引名 +- **`id`**:文档 ID +- **`path`**:要从中获取字段值的字段的名称,即搜索关键字 +- **`routing`**(选填):要从中获取 term 值的文档的自定义路由值。如果在索引文档时提供了自定义路由值,则此参数是必需的。 + +:::details terms 示例 + +```bash +# 1. 创建一个索引 +DELETE my-index-000001 +PUT my-index-000001 +{ + "mappings": { + "properties": { + "color": { "type": "keyword" } + } + } +} + +# 2. 写入一个文档 +PUT my-index-000001/_doc/1 +{ + "color": [ + "blue", + "green" + ] +} + +# 3. 写入另一个文档 +PUT my-index-000001/_doc/2 +{ + "color": "blue" +} + +# 3. 使用 terms query +GET my-index-000001/_search?pretty +{ + "query": { + "terms": { + "color": { + "index": "my-index-000001", + "id": "2", + "path": "color" + } + } + } +} + +DELETE my-index-000001 +``` + +::: + +### wildcard(通配符查询) + +[**`wildcard`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html) 即通配符查询,返回与通配符模式匹配的文档。 + +`?` 用来匹配一个任意字符,`*` 用来匹配零个或者多个字符。 + +注意:如果配置了 [`search.allow_expensive_queries`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html#query-dsl-allow-expensive-queries) ,则 [**`wildcard query`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html) 会被禁用。 + +:::details wildcard 示例 + +示例:以下搜索返回 `user.id` 字段包含以 `ki` 开头并以 `y` 结尾的术语的文档。这些匹配项可以包括 `kiy`、`kity` 或 `kimchy`。 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "wildcard": { + "email": { + "value": "*@underwood-family.zzz", + "boost": 1, + "rewrite": "constant_score" + } + } + } +} +``` + +::: + +## 复合查询 + +复合查询就是把一些简单查询组合在一起实现更复杂的查询需求,除此之外,复合查询还可以控制另外一个查询的行为。 + +复合查询有以下类型: + +- [`bool`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html) - 布尔查询,可以组合多个过滤语句来过滤文档。 +- [`boosting`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html) - 提供调整相关性打分的能力,在 `positive` 块中指定匹配文档的语句,同时降低在 `negative` 块中也匹配的文档的得分。 +- [`constant_score`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html) - 使用 `constant_score` 可以将 `query` 转化为 `filter`,filter 可以忽略相关性算分的环节,并且 filter 可以有效利用缓存,从而提高查询的性能。 +- [`dis_max`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html) - 返回匹配了一个或者多个查询语句的文档,但只将最佳匹配的评分作为相关性算分返回。 +- [`function_score`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html) - 支持使用函数来修改查询返回的分数。 + +### bool (布尔查询) + +[`bool`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html) 查询可以把任意多个简单查询组合在一起,使用 must、should、must_not、filter 选项来表示简单查询之间的逻辑,每个选项都可以出现 0 次到多次,它们的含义如下: + +- `must` - 文档必须匹配 must 选项下的查询条件,相当于逻辑运算的 AND,且参与文档相关度的评分。 +- `should` - 文档可以匹配 should 选项下的查询条件也可以不匹配,相当于逻辑运算的 OR,且参与文档相关度的评分。 +- `must_not` - 与 must 相反,匹配该选项下的查询条件的文档不会被返回;需要注意的是,**must_not 语句不会影响评分,它的作用只是将不相关的文档排除**。 +- `filter` - 和 must 一样,匹配 filter 选项下的查询条件的文档才会被返回,**但是 filter 不评分,只起到过滤功能,与 must_not 相反**。 + +:::details bool 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "bool": { + "filter": { + "term": { + "type": "order" + } + }, + "must_not": { + "range": { + "taxful_total_price": { + "gte": 30 + } + } + }, + "should": [ + { + "match": { + "day_of_week": "Sunday" + } + }, + { + "match": { + "category": "Clothing" + } + } + ], + "minimum_should_match": 1 + } + } +} +``` + +::: + +### boosting + +[boosting](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html) 提供了调整相关性打分的能力。 + +boosting 查询包括 `positive`、`negative` 和 `negative_boost` 三个部分。`positive` 中的查询评分保持不变;`negative` 中的查询会降低文档评分;相关性算分降低的程度将由 `negative_boost` 参数决定,其取值范围为:`[0.0, 1.0]`。 + +:::details boosting 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "boosting": { + "positive": { + "term": { + "day_of_week": "Monday" + } + }, + "negative": { + "range": { + "taxful_total_price": { + "gte": "30" + } + } + }, + "negative_boost": 0.2 + } + } +} +``` + +::: + +### constant_score + +使用 [`constant_score`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html) 可以将 `query` 转化为 `filter`,filter 可以忽略相关性算分的环节,并且 filter 可以有效利用缓存,从而提高查询的性能。 + +:::details constant_score 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "constant_score": { + "filter": { + "term": { + "day_of_week": "Monday" + } + }, + "boost": 1.2 + } + } +} +``` + +::: + +### dis_max + +[dis_max](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html) 查询与 bool 查询有一定联系也有一定区别,`dis_max` 查询支持多并发查询,可返回与任意查询条件子句匹配的任何文档类型。与 `bool` 查询可以将所有匹配查询的分数相结合使用的方式不同,`dis_max` 查询只使用最佳匹配查询条件的分数。 + +:::details dis_max 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "dis_max": { + "queries": [ + { + "term": { + "currency": "EUR" + } + }, + { + "term": { + "day_of_week": "Sunday" + } + } + ], + "tie_breaker": 0.7 + } + } +} +``` + +::: + +### function_score + +[function_score](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html) 查询可以修改查询的文档得分,这个查询在有些情况下非常有用,比如通过评分函数计算文档得分代价较高,可以改用过滤器加自定义评分函数的方式来取代传统的评分方式。 + +使用 `function_score` 查询,用户需要定义一个查询和一至多个评分函数,评分函数会对查询到的每个文档分别计算得分。 + +`function_score` 查询提供了以下几种算分函数: + +- [**`script_score`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-script-score) - 利用自定义脚本完全控制算分逻辑。 +- [**`weight`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-weight) - 为每一个文档设置一个简单且不会被规范化的权重。 +- [**`random_score`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-random) - 为每个用户提供一个不同的随机算分,对结果进行排序。 +- [**`field_value_factor`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-field-value-factor) - 使用文档字段的值来影响算分,例如将好评数量这个字段作为考虑因数。 +- [**`decay functions`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html#function-decay) - 衰减函数,以某个字段的值为标准,距离指定值越近,算分就越高。例如我想让书本价格越接近 10 元,算分越高排序越靠前。 + +:::details function_score 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "function_score": { + "query": { "match_all": {} }, + "boost": "5", + "functions": [ + { + "filter": { "match": { "day_of_week": "Sunday" } }, + "random_score": {}, + "weight": 23 + }, + { + "filter": { "match": { "day_of_week": "Monday" } }, + "weight": 42 + } + ], + "max_boost": 42, + "score_mode": "max", + "boost_mode": "multiply", + "min_score": 42 + } + } +} +``` + +::: + +## 推荐搜索 + +ES 通过 [**`Suggester`**](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html) 提供了推荐搜索能力,可以用于文本纠错,文本自动补全等场景。 + +根据使用场景的不同,ES 提供了以下 4 种 Suggester: + +- **Term Suggester** - 基于单词的纠错补全。 +- **Phrase Suggester** - 基于短语的纠错补全。 +- **Completion Suggester** - 自动补全单词,输入词语的前半部分,自动补全单词。 +- **Context Suggester** - 基于上下文的补全提示,可以实现上下文感知推荐。 + +### Term Suggester + +Term Suggester **提供了基于单词的纠错、补全功能,其工作原理是基于编辑距离(edit distance)来运作的,编辑距离的核心思想是一个词需要改变多少个字符就可以和另一个词一致**。 + +:::details term suggester 示例 + +```bash +GET kibana_sample_data_ecommerce/_search +{ + "query": { + "match": { + "day_of_week": "Sund" + } + }, + "suggest": { + "my_suggest": { + "text": "Sund", + "term": { + "suggest_mode": "missing", + "field": "day_of_week" + } + } + } +} +``` + +响应结果: + +```json +{ + "took": 2, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + // 略 + }, + "suggest": { + "my_suggest": [ + { + "text": "Sund", + "offset": 0, + "length": 4, + "options": [ + { + "text": "Sunday", + "score": 0.5, + "freq": 614 + } + ] + } + ] + } +} +``` + +::: + +Term Suggester API 有很多参数,比较常用的有以下几个 - + +- **text** - 指定了需要产生建议的文本,一般是用户的输入内容。 +- **field** - 指定从文档的哪个字段中获取建议。 +- **suggest_mode** - 设置建议的模式。其值有以下几个选项 - + - `missing` - 如果索引中存在就不进行建议,默认的选项。 + - `popular` - 推荐出现频率更高的词。 + - `always` - 不管是否存在,都进行建议。 +- **analyzer** - 指定分词器来对输入文本进行分词,默认与 field 指定的字段设置的分词器一致。 +- **size** - 为每个单词提供的最大建议数量。 +- **sort** - 建议结果排序的方式,有以下两个选项 - + - `score` - 先按相似性得分排序,然后按文档频率排序,最后按词项本身(字母顺序的等)排序。 + - `frequency` - 先按文档频率排序,然后按相似性得分排序,最后按词项本身排序。 + +### Phrase Suggester + +**Phrase Suggester 在 Term Suggester 的基础上增加了一些额外的逻辑,因为是短语形式的建议,所以会考量多个 term 间的关系,比如相邻的程度、词频等**。 + +:::details phrase suggester 示例 + +```bash +GET kibana_sample_data_logs/_search +{ + "suggest": { + "text": "Firefix", + "simple_phrase": { + "phrase": { + "field": "agent", + "direct_generator": [ { + "field": "agent", + "suggest_mode": "always" + } ], + "highlight": { + "pre_tag": "", + "post_tag": "" + } + } + } + } +} +``` + +响应结果: + +```json +{ + "took" : 2, + "timed_out" : false, + "_shards" : // 略 + "hits" : // 略 + "suggest" : { + "simple_phrase" : [ + { + "text" : "Firefix", + "offset" : 0, + "length" : 7, + "options" : [ + { + "text" : "firefox", + "highlighted" : "firefox", + "score" : 0.2000096 + } + ] + } + ] + } +} +``` + +::: + +Phrase Suggester 可用的参数也是比较多的,下面介绍几个用得比较多的参数选项 - + +- `max_error` - 指定最多可以拼写错误的词语的个数。 +- `confidence` - 其作用是用来控制返回结果条数的。如果用户输入的数据(短语)得分为 N,那么返回结果的得分需要大于 `N * confidence`。`confidence` 默认值为 1.0。 +- `highlight` - 高亮被修改后的词语。 + +### Completion Suggester + +**Completion Suggester 提供了自动补全的功能**。 + +**Completion Suggester 在实现的时候会将 analyze(将文本分词,并且去除没用的词语,例如 is、at 这样的词语) 后的数据进行编码,构建为 FST 并且和索引存放在一起**。FST(**[finite-state transducer](https://link.juejin.cn/?target=https%3A%2F%2Fen.wikipedia.org%2Fwiki%2FFinite-state_transducer)**)是一种高效的前缀查询索引。由于 FST 天生为前缀查询而生,所以其非常适合实现自动补全的功能。ES 会将整个 FST 加载到内存中,所以在使用 FST 进行前缀查询的时候效率是非常高效的。 + +在使用 Completion Suggester 前需要定义 Mapping,对应的字段需要使用 “completion” type。 + +:::details completion suggester 示例 + +构造用于自动补全的测试数据: + +```bash +# 先删除原来的索引 +DELETE music + +# 新增 type 字段,类型为 completion,用于自动补全测试 +PUT music +{ + "mappings": { + "properties": { + "suggest": { + "type": "completion" + } + } + } +} + +# 添加推荐 +PUT music/_doc/1?refresh +{ + "suggest" : { + "input": [ "Nevermind", "Nirvana" ], + "weight" : 34 + } +} +PUT music/_doc/1?refresh +{ + "suggest": [ + { + "input": "Nevermind", + "weight": 10 + }, + { + "input": "Nirvana", + "weight": 3 + } + ] +} +``` + +获取推荐: + +```bash +POST music/_search +{ + "suggest": { + "song-suggest": { + "prefix": "nir", + "completion": { + "field": "suggest", + "size": 5, + "skip_duplicates": true + } + } + } +} +``` + +::: + +### Context Suggester + +**Context Suggester 是 Completion Suggester 的扩展,可以实现上下文感知推荐**。 + +ES 支持两种类型的上下文: + +- **Category**:任意字符串的分类。 +- **Geo**:地理位置信息。 + +在使用 Context Suggester 前需要定义 Mapping,然后在数据中加入相关的 Context 信息。 + +:::details context suggester 示例 + +构造用于 Context Suggester 的测试数据: + +```bash +# 删除原来的索引 +DELETE books_context + +# 创建用于测试 Context Suggester 的索引 +PUT books_context +{ + "mappings": { + "properties": { + "book_id": { + "type": "keyword" + }, + "name": { + "type": "text", + "analyzer": "standard" + }, + "name_completion": { + "type": "completion", + "contexts": [ + { + "name": "book_type", + "type": "category" + } + ] + }, + "author": { + "type": "keyword" + }, + "intro": { + "type": "text" + }, + "price": { + "type": "double" + }, + "date": { + "type": "date" + } + } + }, + "settings": { + "number_of_shards": 3, + "number_of_replicas": 1 + } +} + +# 导入测试数据 +PUT books_context/_doc/4 +{ + "book_id": "4ee82465", + "name": "Linux Programming", + "name_completion": { + "input": ["Linux Programming"], + "contexts": { + "book_type": "program" + } + }, + "author": "Richard Stones", + "intro": "Happy to Linux Programming", + "price": 10.9, + "date": "2022-06-01" +} +PUT books_context/_doc/5 +{ + "book_id": "4ee82466", + "name": "Linus Autobiography", + "name_completion": { + "input": ["Linus Autobiography"], + "contexts": { + "book_type": "autobiography" + } + }, + "author": "Linus", + "intro": "Linus Autobiography", + "price": 14.9, + "date": "2012-06-01" +} +``` + +执行 Context Suggester 查询: + +```bash +POST books_context/_search +{ + "suggest": { + "my_suggest": { + "prefix": "linu", + "completion": { + "field": "name_completion", + "contexts": { + "book_type": "autobiography" + } + } + } + } +} +``` + +::: + +## 参考资料 + +- [极客时间教程 - Elasticsearch 核心技术与实战](https://time.geekbang.org/course/detail/100030501-102659) +- [Elasticsearch 从入门到实践](https://www.itshujia.com/books/elasticsearch) +- [Full text queries](https://www.elastic.co/guide/en/elasticsearch/reference/current/full-text-queries.html) +- [Term-level queries](https://www.elastic.co/guide/en/elasticsearch/reference/current/term-level-queries.html) +- [Compound queries](https://www.elastic.co/guide/en/elasticsearch/reference/current/compound-queries.html) +- [Suggesters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-suggesters.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\236\266\346\236\204.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\236\266\346\236\204.md" new file mode 100644 index 0000000000..93d00b55cd --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\346\236\266\346\236\204.md" @@ -0,0 +1,215 @@ +--- +icon: logos:elasticsearch +title: Elasticsearch 架构 +date: 2024-11-25 07:42:18 +categories: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch + - 存储 + - 索引 +permalink: /pages/f648b115/ +--- + +# Elasticsearch 架构 + +## 存储流程 + +ES 存储数据的流程可以从三个角度来阐述: + +- 从**集群**的角度来看,数据写入会先路由到主分片,在主分片上写入成功后,会并发写副本分片,最后响应给客户端。 +- 从**分片**的角度来看,数据到达分片后需要对内容进行格式校验、分词处理然后再索引数据。 +- 从**节点**的角度来看,ES 数据持久化的步骤可归纳为:**Refresh、写 Translog、Flush、Merge。** + +### 文档分布式存储流程 + +ES 的索引有一个或者多个分片,而分片又分为主分片和副本分片两种。将要写入的数据存储在哪个分片是第一个要考虑的问题。 + +首先需要找到存储文档的主分片,并在主分片的节点上写入对应数据,**数据在主分片写入成功后再将数据分发到副分片进行存储**。文档的新增、更新、删除等操作都属于写入操作。 + +从集群层面来看,存储数据的流程如下: + +1. **请求** - 客户端选择一个 node(示例中是 node1)发送请求过去,这个 node 就是 `coordinating node`(协调节点)。 +2. **路由转发** - `coordinating node` 根据文档 ID 或 routing key 计算出文档应该被保存到哪个分片(这里是分片 3),并且从集群状态的路由表信息中获取分片 3 的主分片所在的节点为 node3。`coordinating node` 将请求转发给 node3。 +3. **复制** - node3 存储数据后,将请求并发转发到 分片 3 的所有副本分片,即数据复制。 +4. **响应** - 当所有副分片都写入成功后,node3 会向 `coordinating node` 返回写入成功的消息,`coordinating node` 再将响应返回给客户端。 + +### 数据索引流程 + +文档分布式存储流程中的描述,隐藏了一个细节:如果是全文本数据,ES 需要使用 [**analyzer(分析器)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html) 先对内容进行分析(如果数据是精确值,如实体 ID、日期等,则无需处理)。 + +在 Elasticsearch 中,分析器是用于对文本进行分词的组件。分析器用于将文本分解为更小的单元,称为分词。然后,这些分词用于索引和搜索文本。分析器的主要目标是将原始文本转换为可以有效搜索和分析的结构化格式 (分词)。 + +[**analyzer(分析器)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html) 由三个组件组成:零个或多个 [Character Filters(字符过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html)、有且仅有一个 [Tokenizer(分词器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html)、零个或多个 [Token Filters(分词过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html)。分析的执行顺序为:`character filters -> tokenizer -> token filters`。 + +对全文本数据来说,数据索引时会对文本内容进行分析处理,分析器的处理流程如下: + +1. character flters 先对字符进行过滤,例如:把一些 HTML 元素、转义标签清除; +2. tokenizer 会将字符串按不同的策略进行切分,分割得到的单词称为 token(词条); +3. token filters 对 token 再进行过滤,例如:删除停用词(and、is 等),转换近义词等; + +经过以上一系列处理后,ES 会将数据存储到名为倒排索引的结构中。 + +当需要全文检索存储数据时,需要先使用搜索分析器对搜索内容进行分析,这个处理过程和存储时使用的分析器相似。通过分析得到的分词列表,再去和倒排索引中的数据去进行匹配,最后返回匹配度最高的数据。 + +### 数据持久化流程 + +ES 的数据持久化流程主要有以下几个过程:**Refresh、写 Translog、Flush、Merge。** + +![](https://miro.medium.com/1*mB9Uqv2ECmj-_Rxuw_Mgww.png) + +#### Refresh + +在文档写入的时候,ES 会将文档先写入到 **Index Buffer** 中。 + +当 Index Buffer 大小达到阈值(默认为 JVM 的 10%),或间隔一段时间(默认每秒执行一次,可以通过 `index.refresh_interval` 进行设置),ES 会将 Index Buffer 中的数据写入到一个新的 Segment 文件中。此时的 Segment 文件存在于 OS Cache 中。这个过程称为 **Refresh**。 + +refresh 写完 segment 后,会更新 shard 的 commit point。commit point 在 shard 中以 `segments_xxx` 形式名字的文件存在,用来记录每个 shard 中 segment 相关的信息。 + +此外,ES 也支持通过 API 手动触发 Refresh 操作。 + +Refresh 过程有几点需要注意: + +- 在 Index Buffer 中的数据是搜索不到的;Refresh 后,数据进入 **OS Cache**,这时数据就可以搜索了。由于,刷新默认间隔一秒,写入的数据需要一秒后才可见,因此,ES 被称为近实时搜索数据库。 +- Index Buffer 的设计是为了通过批量写入,提高写入效率。但是,这种设计也带来了新的问题:一旦 ES 节点发生断点,Index Buffer 中的数据就丢失了。为了避免数据丢失,ES 的解决方案就是下文要提到的 **Translog**。 +- Index Buffer 每次 Refresh 时,都会创建一个新的 Segment 文件。随着时间推移,Segment 文件会越来越多。这些 Segment 都要消耗文件句柄和内存,每次搜索都要检查每个 Segment 然后再合并结果。因此,Segment 越多、搜索也越慢。为了减少 Segment 文件数,ES 的解决方案就是下文要提到的 **Merge** 操作。 + +#### 写 Translog + +**ES 通过 Translog(事务日志)来保证数据不丢失**。 + +数据写入 Index Buffer 后,ES 会将数据也写入 Translog,写入完毕后即可以返回客户端写入成功。**Translog 只允许追加写入**,并且默认是调用 fsync 进行刷盘的。**每个分片都会有自己的 Translog,在 Refresh 的时候系统会清空 Index Buffer,但不会清空 Translog**。一旦机器宕机,再次重启的时候, ES 会自动读取 Translog 中的数据,恢复到 Index Buffer 和 OS Cache 中。 + +Translog 其实也是先写入 OS Cache 的,默认每 5 秒刷一次到磁盘中去(由 `index.translog.interval` 控制)。所以,如果机器宕机,可能会丢失 5 秒的数据。这样设计的目的,还是基于写入效率的考虑。如果每条数据都直接写入磁盘,开销是比较高的,所以这里设计为延时批量写入。 + +> 通过 Refresh 和 写 Translog 两节的内容,我们可以总结为: +> +> - ES 之所以被称为**近实时查询**,是由于数据写入后,需要刷新(默认间隔 1 秒)后,才可以搜索到; +> - ES 虽然有 Translog 机制,但依然有丢失数据的风险——有 5 秒的数据,是暂存在 index buffer、translog(os cache)、segment file(os cache) 中,此时尚未保存到磁盘。如果此时发生宕机或断电,会**丢失 5 秒的数据**。 + +#### Flush + +Flush 操作本质上就是 commit 操作,即 ES 的数据持久化操作。 + +1. Flush 操作的第一步,就是将 index buffer 中现有数据 `refresh` 到 `OS Cache` 中去,清空 buffer。 +2. 然后,将一个 `commit point` 写入磁盘文件,里面标识着这个 `commit point` 对应的所有 Segment 文件。同时,强行将 `OS Cache` 中目前所有的数据都 `fsync` 到磁盘中去。 +3. 最后,删除当前的 translog,新建一个 translog,此时 commit 操作完成。 + +以下两个条件满足任意一个,就会触发 Flush 操作: + +- 默认每 30 分钟触发执行一次(由 `index.translog.flush_threshold_period` 控制) +- Translog 写满时触发执行,默认容量为 512M(由 `index.translog.flush_threshold_size` 控制)。 + +#### Merge + +Elasticsearch 的 document 的物理存储是 Luncene segment,而 segment 不允许变更。那么,如何处理删除和更新呢? + +- 如果是删除操作,commit 的时候会生成一个 `.del` 文件,里面将某个 doc 标识为 `deleted` 状态,那么搜索的时候根据 `.del` 文件就知道这个 doc 是否被删除了。 + +- 如果是更新操作,就是将原来的 doc 标识为 `deleted` 状态,然后新写入一条数据。 + +Index Buffer 每次 Refresh 时,都会创建一个新的 Segment 文件。随着时间推移,Segment 文件会越来越多。这些 Segment 都要消耗文件句柄和内存,每次搜索都要检查每个 Segment 然后再合并结果。因此,Segment 越多、搜索也越慢。 + +Elasticsearch 会定期执行 merge 操作,将多个 `segment file` 合并成一个。合并时会将标识为 `deleted` 的 doc 给**物理删除掉**,然后将新的 `segment file` 写入磁盘,这里会写一个 `commit point`,标识所有新的 `segment file`,然后打开 `segment file` 供搜索使用,同时删除旧的 `segment file`。 + +## 搜索流程 + +在 Elasticsearch 中,搜索一般分为两个阶段,query 和 fetch 阶段。可以简单的理解,query 阶段确定要取哪些 doc,fetch 阶段取出具体的 doc。 + +### Query 阶段 + +Query 阶段会根据搜索条件遍历每个分片(主分片或者副分片中的其一)中的数据,返回符合条件的前 N 条数据的 ID 和排序值,然后在协调节点中对所有分片的数据进行排序,获取前 N 条数据的 ID。 + +Query 阶段的流程如下: + +1. 客户端选择一个节点发送请求,这个 node 成为 coordinate node(协调节点)。coordinate node 创建一个大小为 from + size 的优先级队列用来存放结果。 +2. coordinate node 将请求转发到索引的每个主分片或者副分片中。 +3. 每个分片在本地执行搜索请求,并将查询结果打分排序,然后将结果保存到 from + size 大小的有序队列中。 +4. 接着,每个分片将结果返回给 coordinate node,coordinate node 对数据进行汇总处理:合并、排序、分页,将汇总数据存到一个大小为 from + size 的全局有序队列。 + +需要注意的是,在协调节点转发搜索请求的时候,如果有 N 个 Shard 位于同一个节点时,并不会合并这些请求,而是发生 N 次请求! + +### Fetch 阶段 + +在 Fetch 阶段,协调节点会从 Query 阶段产生的全局排序列表中确定需要取回的文档 ID 列表,然后通过路由算法计算出各个文档对应的分片,并且用 multi get 的方式到对应的分片上获取文档数据。 + +Fetch 阶段的流程如下: + +1. coordinate node 确定需要获取哪些文档,然后向相关节点发起 multi get 请求; +2. 分片所在节点读取文档数据,并且进行 `_source` 字段过滤、处理高亮参数等,然后把处理后的文档数据返回给协调节点; +3. coordinate node 汇总所有数据后,返回给客户端。 + +### 深度分页问题 + +在 Elasticsearch 中,支持三种分页查询方式: + +- from + size - 可以使用 `from` 和 `size` 参数分别指定查询的起始页和每页记录数。 +- [`search_after`](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after) - 不支持指定页数,只能向下翻页;并且需要指定 sort,并保证值是唯一的。然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询。 +- [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#scroll-search-results) - 类似于 RDBMS 中的游标,只允许向下翻页。每次下一页查询后,使用返回结果的 scroll id 来作为下一次翻页的标记。scroll 查询会在搜索初始化阶段会生成快照,后续数据的变化无法及时体现在查询结果,因此更加适合一次性批量查询或非实时数据的分页查询。 + +前文中,我们已经了解了 ES 两阶段搜索流程(Query 和 Fetch)。从中不难发现,这种搜索方式在分页查询时会出现以下情况: + +- 每个 shard 要扫描 `from + size` 条数据; +- coordinate node 需要接收并处理 `(from + size) * primary_shard_num` 条数据。 + +**如果 from 或 size 很大,需要处理的数据量也会很大,代价很高,这就是深分页产生的原因**。为了避免深分页,ES 默认限制 `from + size` 不能超过 10000,可以通过 `index.max_result_window` 设置。 + +如何解决 Elasticsearch 深分页问题? + +ES 官方提供了另外两种分页查询方式 [`search_after`](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after) + PIT 和 [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#scroll-search-results)(注意:官方已不再推荐) 来避免深分页问题。 + +### 计算偏差 + +在 ES 中,不仅仅是普通搜索,相关性计算(评分)和聚合计算也是先在每个 shard 的本地进行计算,再由 coordinate node 进行汇总。由于分片的本地计算是独立的,只能基于数据子集来进行计算,所以难免出现数据偏差。 + +解决这个问题的方式也有多种: + +- 当数据量不大的情况下,**设置主分片数为 1**,这意味着在数据全集上进行聚合。 但这种方案不太现实。 +- **设置 [`shard_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-shard-size) 参数**,将计算数据范围变大,**牺牲整体性能,提高精准度**。shard_size 的默认值是 `size * 1.5 + 10`。 +- **使用 DFS Query Then Fetch**, 在 URL 参数中指定:`_search?search_type=dfs_query_then_fetch`。这样设定之后,系统先会把每个分片的词频和文档频率的数据汇总到协调节点进行处理,然后再进行相关性算分。这样的话会消耗更多的 CPU 和内存资源,效率低下! +- 尽量保证数据均匀地分布在各个分片中。 + +### 数据路由 + +为了避免出现数据倾斜,系统需要一种高效的方式把数据均匀分散到各个节点上**存储**,并且**在检索的时候可以快速找到**文档所在的节点与分片。这就需要确立路由算法,使得数据可以映射到指定的节点上。 + +常见的路由方式如下: + +| **算法** | **描述** | +| :------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| 随机算法 | 写数据时,随机写入到一个节点中;读数据时,由于不知道查询数据存在于哪个节点,所以需要遍历所有节点。 | +| 路由表 | 由中心节点统一维护数据的路由表,以保证唯一性;但是,中心化产生了新的问题:单点故障、数据越大,路由表越大、单点容易称为性能瓶颈、数据迁移复杂等。 | +| 哈希取模 | 对 key 值进行哈希计算,然后根据节点数取模,以确定节点。 | + +ES 的数据路由算法是根据文档 ID 和 routing key 来确定 Shard ID 的过程。**默认的情况下 routing key 为文档 ID**,路由算法一般情况下的计算公式如下: + +``` + shard_number = hash(_routing) % numer_of_primary_shards +``` + +也可以在请求中指定 routing key,下面是新增数据的时候指定 routing 的方式: + +```bash +PUT /_doc/?routing=routing_key +{ + "field1": "xxx", + "field2": "xxx" +} +``` + +添加数据时,如果不指定文档 ID,ES 会自动分片一个随机 ID。这种情况下,结合 Hash 算法,可以保证数据被均匀分布到各个分片中。如果指定文档 ID,或指定 routing key,Hash 计算得出的值可能会不够随机,从而导致数据倾斜。 + +**index 一旦设置了主分片数就不能修改,如果要修改就需要 reindex(即数据迁移)**。之所以如此,就是因为:一旦修改了主分片数,即等于修改了原 Hash 计算中的变量,无法再通过 Hash 计算正确路由到数据存储的分片。 + +## 参考资料 + +- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) +- https://www.itshujia.com/read/elasticsearch/359.html +- https://github.com/doocs/advanced-java/blob/main/docs/high-concurrency/es-write-query-search.md +- https://www.elastic.co/blog/found-elasticsearch-top-down +- https://www.elastic.co/guide/en/elasticsearch/reference/current/preload-data-to-file-system-cache.html +- https://blog.devgenius.io/elasticsearch-solution-to-searching-71116220c82f diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\347\256\200\344\273\213.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\347\256\200\344\273\213.md" new file mode 100644 index 0000000000..a99289c4de --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\347\256\200\344\273\213.md" @@ -0,0 +1,549 @@ +--- +icon: logos:elasticsearch +title: Elasticsearch 简介 +cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202411241734774.png +date: 2020-06-16 07:10:44 +categories: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +permalink: /pages/adc985cd/ +--- + +# Elasticsearch 简介 + +## 什么是 Elasticsearch? + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202411241734774.png) + +**[Elasticsearch](https://github.com/elastic/elasticsearch) 是一个开源的分布式搜索和分析引擎**。 + +**[Elasticsearch](https://github.com/elastic/elasticsearch) 基于搜索库 [Lucene](https://github.com/apache/lucene-solr) 开发**。Elasticsearch 隐藏了 Lucene 的复杂性,提供了简单易用的 REST API / Java API 接口(另外还有其他语言的 API 接口)。 + +Elasticsearch 是面向文档的,它**将复杂数据结构序列化为 JSON 形式存储**。 + +**Elasticsearch 是近实时(Near Realtime,缩写 NRT)的全文搜索**。近实时是指: + +- 从写入数据到数据可以被搜索,存在较小的延迟(大概是 1s)。 +- 基于 Elasticsearch 执行搜索和分析可以达到秒级。 + +## 为什么使用 Elasticsearch? + +Elasticsearch 是基于 Lucene 的,那么为什么不是直接使用 Lucene 呢? + +Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库。 + +但是,Lucene 仅仅只是一个库。为了充分发挥其功能,需要使用 Java 并将 Lucene 直接集成到应用程序中。 Lucene 非常复杂,了解其工作原理并不容易。 + +Elasticsearch 也是使用 Java 编写的,它的内部使用 Lucene 做索引与搜索,但是它的目的是使全文检索变得简单,**通过隐藏 Lucene 的复杂性,取而代之的提供一套简单一致的 RESTful API**。 + +然而,Elasticsearch 不仅仅是 Lucene,并且也不仅仅只是一个全文搜索引擎。 它可以被下面这样准确的形容: + +- 一个分布式的实时文档数据库,每个字段可以被索引与搜索。 +- 一个分布式实时分析搜索引擎。 +- 支持扩展为上百个服务节点的集群,并支持 PB 级别的半结构化数据。 + +## Elasticsearch 的应用场景有哪些? + +Elasticsearch 的主要功能如下: + +- **海量数据的分布式存储及集群管理** +- **提供丰富的近实时搜索能力** +- **海量数据的近实时分析(聚合)** + +Elasticsearch 被广泛应用于以下场景: + +- **搜索** + - **全文检索** - Elasticsearch 通过快速搜索大型数据集,使复杂的搜索查询变得更加容易。它对于需要即时和相关搜索结果的网站、应用程序或企业特别有用。 + - **自动补全和拼写纠正** - 可以在用户输入内容时,实时提供自动补全和拼写纠正,以增加用户体验并提高搜索效率。 + - **地理空间搜索** - 使用地理空间查询搜索位置并计算空间关系。 +- **可观测性** + - **日志、指标和链路追踪** - 收集、存储和分析来自应用程序、系统和服务的日志、指标和追踪。 + - **性能监控** - 监控和分析业务关键性能指标。 + - **OpenTelemetry** - 使用 OpenTelemetry 标准,将遥测数据采集到 Elastic Stack。 + +## Elasticsearch 历史 + +- 1.0(2014 年) +- 5.0(2016 年) + - Lucene 6.x + - 默认打分机制从 TD-IDF 改为 BM 25 + - 增加 Keyword 类型 +- 6.0(2017 年) + - Lucene 7.x + - 跨集群复制 + - 索引生命周期管理 + - SQL 的支持 +- 7.0(2019 年) + - Lucene 8.0 + - 移除 Type + - ECK (用于支持 K8S) + - 集群协调 + - High Level Rest Client + - Script Score 查询 +- 8.0(2022 年) + - Lucene 9.0 + - 向量搜索 + - 支持 OpenTelemetry + +## Elasticsearch 概念 + +``` +index -> type -> mapping -> document -> field +``` + +Elasticsearch 集群的核心概念如下: + +- **Cluster(集群)** - **由多个协同工作的 ES 实例组合成的集合称为集群**。集群架构使得 ES 具备了高可用性和可扩展性。 +- **Node(节点)** - **单个 ES 服务实例称为 Node,本质上就是一个 Java 进程**。每个节点都有各自的名字,默认是随机分配的,也可以通过 `node.name` 指定。 +- **Shard(分片)** - 当单台机器不足以存储大量数据时,Elasticsearch 可以将一个索引中的数据切分为多个 **`分片(shard)`** 。 **`分片(shard)`** 分布在多台服务器上存储。有了 shard 就可以横向扩展,存储更多数据,让搜索和分析等操作分布到多台服务器上去执行,提升吞吐量和性能。每个 shard 都是一个 lucene index。 +- **Replica(副本)** - 任何一个服务器随时可能故障或宕机,此时 shard 可能就会丢失,因此可以为每个 shard 创建多个 **`副本(replica)`**。replica 可以在 shard 故障时提供备用服务,保证数据不丢失,多个 replica 还可以提升搜索操作的吞吐量和性能。primary shard(建立索引时一次设置,不能修改,默认 5 个),replica shard(随时修改数量,默认 1 个),默认每个索引 10 个 shard,5 个 primary shard,5 个 replica shard,最小的高可用配置,是 2 台服务器。 + +Elasticsearch 数据的核心概念如下: + +- **Index(索引)** - 在 ES 中,**可以将索引视为文档(document)的集合**。 + - ES 会为所有字段建立索引,经过处理后写入一个倒排索引(Inverted Index)。查找数据的时候,直接查找该索引。 + - 所以,ES 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。 +- **Type(类型)** - 每个索引里可以有一个或者多个类型(type)。`类型(type)` 是 Index 的一个逻辑分类。 + - 不同的 Type 应该有相似的结构(schema),举例来说,`id`字段不能在这个组是字符串,在另一个组是数值。这是与关系型数据库的表的 [一个区别](https://www.elastic.co/guide/en/Elasticsearch/guide/current/mapping.html)。性质完全不同的数据(比如`products`和`logs`)应该存成两个 Index,而不是一个 Index 里面的两个 Type(虽然可以做到)。 + - 注意:ES 7.x 版已彻底移除 Type。 +- **Document(文档)** - Index 里面单条的记录称为 Document。文档是一组字段。每个文档都有一个唯一的 ID。 +- **Field(字段)** - 包含数据的键值对。默认情况下,Elasticsearch 对每个字段中的所有数据建立索引,并且每个索引字段都具有专用的优化数据结构。 +- [**Metadata Field(元数据字段)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html) - 存储有关文档的信息的系统字段。元数据字段都以 `_` 开头。常见元数据字段: + - [`_index`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html) - 文档所属的索引 + - [`_id`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html) - 文档的 ID + - [`_source`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html) - 表示文档原文的 JSON + +ES 核心概念 vs. DB 核心概念: + +| ES | DB | +| -------------------------------- | ------------------ | +| 索引(index) | 数据库(database) | +| 类型(type,6.0 废弃,7.0 移除) | 数据表(table) | +| 文档(docuemnt) | 行(row) | +| 字符(field) | 列(column) | +| 映射(mapping) | 表结构(schema) | + +## Elastic Stack + +Elastic Stack 通常被用来作为日志采集、检索、可视化的解决方案。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202411231210104.png) + +Elastic Stack 也常被称为 ELK,这是 Elastic 公司旗下三款产品 [Elasticsearch](https://www.elastic.co/elasticsearch) 、[Logstash](https://www.elastic.co/products/logstash) 、[Kibana](https://www.elastic.co/kibana) 的首字母组合。 + +- [Elasticsearch](https://www.elastic.co/elasticsearch) 负责存储数据,并提供对数据的检索和分析。 +- [Logstash](https://www.elastic.co/logstash) 传输和处理你的日志、事务或其他数据。 +- [Kibana](https://www.elastic.co/kibana) 将 Elasticsearch 的数据分析并渲染为可视化的报表。 + +Elastic Stack,在 ELK 的基础上扩展了一些新的产品。如:[Beats](https://www.elastic.co/beats),这是针对不同类型数据的轻量级采集器套件。 + +此外,基于 Elastic Stack,其技术生态还可以和一些主流的分布式中间件进行集成,以应对各种不同的场景。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202411231211496.png) + +## Elasticsearch 安装和设置 + +> 参考:[Set up Elasticsearch](https://www.elastic.co/guide/en/elasticsearch/reference/current/setup.html) + +## Elasticsearch 快速入门 + +Elasticsearch 的基本 CRUD 方式如下: + +- **新建文档** - ES 提供了 [Index API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html) 来新建文档。 + - `PUT //_create/<_id>` - 指定 id,如果 id 已存在,会报错 + - `POST //_doc` - 自动生成 `_id` +- **删除文档** - ES 提供了 [Delete API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html) 来删除文档。 + - `DELETE /` +- **更新文档** - ES 提供了 [Update API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html) 来更新文档。 + - `POST //_update/<_id>` +- **查询文档** - ES 提供了 [Get API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html) 来指定 ID 查询文档。 + - `GET /_doc/<_id>` +- **批量写** - ES 提供了 [Bulk API(`_bulk`)](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html) 来执行批量写操作。Bulk API 支持 Index、Create、Update、Delete 四种操作。 +- **批量查** - ES 提供了 [Multi Get API(`_mget`)](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html) 来执行批量指定 ID 查询操作。 + +> 扩展:[Quick starts](https://www.elastic.co/guide/en/elasticsearch/reference/current/quickstart.html) + +### 索引管理 + +:::details 创建索引 + +创建索引 `users`: + +```bash +PUT users +{ + "mappings": { + "properties": { + "name": { + "type": "keyword" + }, + "age": { + "type": "integer" + }, + "message": { + "type": "text" + } + } + }, + "settings": { + "number_of_shards": 1, + "number_of_replicas": 1 + } +} +``` + +响应结果: + +```json +{ + "acknowledged": true, + "shards_acknowledged": true, + "index": "users" +} +``` + +::: + +:::details 删除索引 + +删除索引 `users`: + +```bash +DELETE users +``` + +响应结果: + +```json +{ + "acknowledged": true +} +``` + +::: + +### 新建文档 + +ES 提供了 [Index API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-index_.html) 来新建文档。ES 提供了两种创建文档的方式: + +- `PUT //_create/<_id>` - 指定 id,如果 `_id` 已存在,会报错 +- `POST //_doc` - 自动生成 `_id` + +:::details 不指定 ID 新建文档 + +ES 会自动为新建的文档分片一个 UID。 + +```bash +POST /users/_doc +{ + "user" : "dunwu", + "age" : 20, + "message" : "learning Elasticsearch" +} +``` + +响应结果: + +```json +{ + "_index": "users", + "_type": "_doc", + "_id": "_JVCi5MBf44xQviy3tpW", + "_version": 1, + "result": "created", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 1, + "_primary_term": 1 +} +``` + +::: + +:::details 指定 ID 新建文档 + +```bash +PUT users/_create/2 +{ + "user" : "jason", + "age" : 20, + "message" : "learning Redis" +} +``` + +响应结果: + +```json +{ + "_index": "users", + "_type": "_doc", + "_id": "2", + "_version": 1, + "result": "created", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 1, + "_primary_term": 1 +} +``` + +指定 ID 如果已经存在,返回报错。可以再执行一遍上面的指令,会得到类似下面的错误响应: + +```json +{ + "error": { + "root_cause": [ + { + "type": "version_conflict_engine_exception", + "reason": "[2]: version conflict, document already exists (current version [1])", + "index_uuid": "bkNSOG6RTEet3Q65ynCuBA", + "shard": "0", + "index": "users" + } + ], + "type": "version_conflict_engine_exception", + "reason": "[2]: version conflict, document already exists (current version [1])", + "index_uuid": "bkNSOG6RTEet3Q65ynCuBA", + "shard": "0", + "index": "users" + }, + "status": 409 +} +``` + +::: + +### 删除文档 + +ES 提供了 [Delete API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-delete.html) 来删除文档。 + +:::details 指定 ID 删除文档 + +```bash +DELETE users/_doc/2 +``` + +响应结果: + +```json +{ + "_index": "users", + "_type": "_doc", + "_id": "2", + "_version": 2, + "result": "deleted", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 4, + "_primary_term": 1 +} +``` + +::: + +### 更新文档 + +ES 提供了 [Update API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-update.html) 来更新文档。 + +:::details 指定 ID 查询 + +```bash +POST users/_update/_JVCi5MBf44xQviy3tpW +{ + "doc": { + "message": "learning HBase" + } +} +``` + +响应结果: + +```json +{ + "_index": "users", + "_type": "_doc", + "_id": "_JVCi5MBf44xQviy3tpW", + "_version": 2, + "result": "updated", + "_shards": { + "total": 2, + "successful": 1, + "failed": 0 + }, + "_seq_no": 2, + "_primary_term": 1 +} +``` + +::: + +### 查询文档 + +ES 提供了 [Get API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-get.html) 来指定 ID 查询文档。 + +:::details 指定 ID 查询 + +```bash +GET users/_doc/_JVCi5MBf44xQviy3tpW +``` + +响应结果: + +```json +{ + "_index": "users", + "_type": "_doc", + "_id": "_JVCi5MBf44xQviy3tpW", + "_version": 2, + "_seq_no": 2, + "_primary_term": 1, + "found": true, + "_source": { + "user": "dunwu", + "age": 20, + "message": "learning HBase" + } +} +``` + +::: + +### 批量写 + +ES 提供了 [Bulk API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-bulk.html) 来执行批量写操作。Bulk API 支持 Index、Create、Update、Delete 四种操作。 + +:::details 批量操作 + +执行第 1 次: + +```bash +POST _bulk +{ "index" : { "_index" : "test", "_id" : "1" } } +{ "field1" : "value1" } +{ "delete" : { "_index" : "test", "_id" : "2" } } +{ "create" : { "_index" : "test", "_id" : "3" } } +{ "field1" : "value3" } +{ "update" : {"_id" : "1", "_index" : "test"} } +{ "doc" : {"field2" : "value2"} } +``` + +响应结果: + +```json +{ + "took": 5436, + "errors": false, + "items": [ + // 略 + ] +} +``` + +执行第 2 次: + +```bash +POST _bulk +{ "index" : { "_index" : "test", "_id" : "1" } } +{ "field1" : "value1" } +{ "delete" : { "_index" : "test", "_id" : "2" } } +{ "create" : { "_index" : "test", "_id" : "3" } } +{ "field1" : "value3" } +{ "update" : {"_id" : "1", "_index" : "test"} } +{ "doc" : {"field2" : "value2"} } +``` + +响应结果: + +```json +{ + "took": 1870, + "errors": true, + "items": [ + // 略 + ] +} +``` + +::: + +### 批量查 + +ES 提供了 [Multi Get API](https://www.elastic.co/guide/en/elasticsearch/reference/current/docs-multi-get.html) 来执行批量指定 ID 查询操作。 + +:::details 批量查询 + +```bash +GET /_mget +{ + "docs": [ + { + "_index": "users", + "_id": "_JVCi5MBf44xQviy3tpW" + }, + { + "_index": "users", + "_id": "2" + } + ] +} +``` + +响应结果: + +```json +{ + "docs": [ + { + "_index": "users", + "_type": "_doc", + "_id": "_JVCi5MBf44xQviy3tpW", + "_version": 2, + "_seq_no": 2, + "_primary_term": 1, + "found": true, + "_source": { + "user": "dunwu", + "age": 20, + "message": "learning HBase" + } + }, + { + "_index": "users", + "_type": "_doc", + "_id": "2", + "_version": 1, + "_seq_no": 3, + "_primary_term": 1, + "found": true, + "_source": { + "user": "jason", + "age": 20, + "message": "learning Redis" + } + } + ] +} +``` + +::: + +## 参考资料 + +- [Elasticsearch 官网](https://www.elastic.co/cn/products/Elasticsearch) +- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) +- [What is Elasticsearch?](https://www.elastic.co/guide/en/elasticsearch/reference/current/elasticsearch-intro-what-is-es.html) +- [Elasticsearch 从入门到实践](https://www.itshujia.com/books/elasticsearch) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\350\201\232\345\220\210.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\350\201\232\345\220\210.md" new file mode 100644 index 0000000000..4b533a0682 --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\350\201\232\345\220\210.md" @@ -0,0 +1,1170 @@ +--- +icon: logos:elasticsearch +title: Elasticsearch 聚合 +date: 2022-01-19 22:49:16 +categories: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch + - 聚合 + - 指标 + - 管道 +permalink: /pages/c6244284/ +--- + +# Elasticsearch 聚合 + +聚合将数据汇总为指标、统计数据或其他分析。 + +Elasticsearch 将聚合分为三类: + +| 类型 | 说明 | +| ----------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | +| [**Metric(指标聚合)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html) | 根据字段值进行统计计算 | +| [**Bucket(桶聚合)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket.html) | 根据字段值、范围或其他条件进行分组 | +| [**Pipeline(管道聚合)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline.html) | 根据其他聚合结果进行聚合 | + +## 聚合的用法 + +所有的聚合,无论它们是什么类型,都遵从以下的规则。 + +- 通过 JSON 来定义聚合计算,使用 `aggregations` 或 `aggs` 来标记聚合计算。需要给每个聚合起一个名字,指定它的类型以及和该类型相关的选项。 +- 它们运行在查询的结果之上。和查询不匹配的文档不会计算在内,除非你使用 global 聚集将不匹配的文档囊括其中。 +- 可以进一步过滤查询的结果,而不影响聚集。 + +以下是聚合的基本结构: + +```json +"aggregations" : { + "" : { + "" : { + + } + [,"meta" : { [] } ]? + [,"aggregations" : { []+ } ]? + } + [,"" : { ... } ]* +} +``` + +- **在最上层有一个 aggregations 的键,可以缩写为 aggs**。 +- 在下面一层,需要为聚合指定一个名字。可以在请求的返回中看到这个名字。在同一个请求中使用多个聚合时,这一点非常有用,它让你可以很容易地理解每组结果的含义。 +- 最后,必须要指定聚合的类型。 + +> 关于聚合分析的值来源,可以**取字段的值**,也可以是**脚本计算的结果**。 +> +> 但是用脚本计算的结果时,需要注意脚本的性能和安全性;尽管多数聚集类型允许使用脚本,但是脚本使得聚集变得缓慢,因为脚本必须在每篇文档上运行。为了避免脚本的运行,可以在索引阶段进行计算。 +> +> 此外,脚本也可以被人可能利用进行恶意代码攻击,尽量使用沙盒(sandbox)内的脚本语言。 + +::: details 【示例】根据 my-field 字段进行 terms 聚合计算 + +```json +GET /my-index-000001/_search +{ + "aggs": { + "my-agg-name": { + "terms": { + "field": "my-field" + } + } + } +} +``` + +响应结果: + +```json +{ + "took": 78, + "timed_out": false, + "_shards": { + "total": 1, + "successful": 1, + "skipped": 0, + "failed": 0 + }, + "hits": { + "total": { + "value": 5, + "relation": "eq" + }, + "max_score": 1.0, + "hits": [...] + }, + "aggregations": { + "my-agg-name": { // my-agg-name 聚合计算的结果 + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 0, + "buckets": [] + } + } +} +``` + +::: + +::: details 用于测试的数据 + +为 `/employees` 索引添加测试数据: + +```json +DELETE /employees +PUT /employees/ +{ + "mappings" : { + "properties" : { + "age" : { + "type" : "integer" + }, + "gender" : { + "type" : "keyword" + }, + "job" : { + "type" : "text", + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 50 + } + } + }, + "name" : { + "type" : "keyword" + }, + "salary" : { + "type" : "integer" + } + } + } +} + +PUT /employees/_bulk +{"index":{"_id":"1"}} +{"name":"Emma","age":32,"job":"Product Manager","gender":"female","salary":35000} +{"index":{"_id":"2"}} +{"name":"Underwood","age":41,"job":"Dev Manager","gender":"male","salary":50000} +{"index":{"_id":"3"}} +{"name":"Tran","age":25,"job":"Web Designer","gender":"male","salary":18000} +{"index":{"_id":"4"}} +{"name":"Rivera","age":26,"job":"Web Designer","gender":"female","salary":22000} +{"index":{"_id":"5"}} +{"name":"Rose","age":25,"job":"QA","gender":"female","salary":18000} +{"index":{"_id":"6"}} +{"name":"Lucy","age":31,"job":"QA","gender":"female","salary":25000} +{"index":{"_id":"7"}} +{"name":"Byrd","age":27,"job":"QA","gender":"male","salary":20000} +{"index":{"_id":"8"}} +{"name":"Foster","age":27,"job":"Java Programmer","gender":"male","salary":20000} +{"index":{"_id":"9"}} +{"name":"Gregory","age":32,"job":"Java Programmer","gender":"male","salary":22000} +{"index":{"_id":"10"}} +{"name":"Bryant","age":20,"job":"Java Programmer","gender":"male","salary":9000} +{"index":{"_id":"11"}} +{"name":"Jenny","age":36,"job":"Java Programmer","gender":"female","salary":38000} +{"index":{"_id":"12"}} +{"name":"Mcdonald","age":31,"job":"Java Programmer","gender":"male","salary":32000} +{"index":{"_id":"13"}} +{"name":"Jonthna","age":30,"job":"Java Programmer","gender":"female","salary":30000} +{"index":{"_id":"14"}} +{"name":"Marshall","age":32,"job":"Javascript Programmer","gender":"male","salary":25000} +{"index":{"_id":"15"}} +{"name":"King","age":33,"job":"Java Programmer","gender":"male","salary":28000} +{"index":{"_id":"16"}} +{"name":"Mccarthy","age":21,"job":"Javascript Programmer","gender":"male","salary":16000} +{"index":{"_id":"17"}} +{"name":"Goodwin","age":25,"job":"Javascript Programmer","gender":"male","salary":16000} +{"index":{"_id":"18"}} +{"name":"Catherine","age":29,"job":"Javascript Programmer","gender":"female","salary":20000} +{"index":{"_id":"19"}} +{"name":"Boone","age":30,"job":"DBA","gender":"male","salary":30000} +{"index":{"_id":"20"}} +{"name":"Kathy","age":29,"job":"DBA","gender":"female","salary":20000} +``` + +::: + +### 限定数据范围 + +ES 聚合分析的默认作用范围是 `query` 的查询结果集。 + +同时 ES 还支持以下方式改变聚合的作用范围: + +- `filter` - 只对当前子聚合语句生效 +- `post_filter` - 对聚合分析后的文档进行再次过滤 +- `global` - 无视 query,对全部文档进行统计 + +::: details 【示例】使用 query 限定聚合数据的范围 + +```json +POST /employees/_search +{ + "size": 0, + "query": { + "range": { + "age": { + "gte": 20 + } + } + }, + "aggs": { + "jobs": { + "terms": { + "field":"job.keyword" + + } + } + } +} +``` + +::: + +::: details 【示例】使用 filter 限定聚合数据的范围 + +```json +POST /employees/_search +{ + "size": 0, + "aggs": { + "older_person": { + "filter":{ + "range":{ + "age":{ + "from":35 + } + } + }, + "aggs":{ + "jobs":{ + "terms": { + "field":"job.keyword" + } + }}, + "all_jobs": { + "terms": { + "field":"job.keyword" + + } + } + } +} +``` + +::: + +### 控制返回聚合结果 + +::: details 【示例】仅返回聚合结果 + +使用 `field` 限定聚合返回的展示字段: + +```json +POST /employees/_search +{ + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword" + } + } + } +} + +// 找出所有的 job 类型,还能找到聚合后符合条件的结果 +POST /employees/_search +{ + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword" + } + } + }, + "post_filter": { + "match": { + "job.keyword": "Dev Manager" + } + } +} +``` + +默认情况下,包含聚合的搜索会同时返回搜索命中和聚合结果。要仅返回聚合结果,请将 `size` 设置为 `0`: + +```json +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword" + } + } + } +} +``` + +::: + +### 聚合结果排序 + +::: details 【示例】聚合结果排序 + +指定 `order`,按照 `_count` 和 `_key` 进行排序。 + +```json +POST /employees/_search +{ + "size": 0, + "query": { + "range": { + "age": { + "gte": 20 + } + } + }, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "order": [ + { + "_count": "asc" + }, + { + "_key": "desc" + } + ] + } + } + } +} + +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "order": [ + { + "avg_salary": "desc" + } + ] + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + } + } + } + } +} + +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "order": [ + { + "stats_salary.min": "desc" + } + ] + }, + "aggs": { + "stats_salary": { + "stats": { + "field": "salary" + } + } + } + } + } +} +``` + +::: + +### 运行多个聚合 + +::: details 【示例】运行多个聚合 + +可以在同一请求中指定多个聚合: + +```json +POST /employees/_search +{ + "size": 0, + "query": { + "range": { + "age": { + "gte": 40 + } + } + }, + "aggs": { + "jobs": { + "terms": { + "field":"job.keyword" + + } + }, + + "all":{ + "global":{}, + "aggs":{ + "salary_avg":{ + "avg":{ + "field":"salary" + } + } + } + } + } +} +``` + +::: + +### 运行子聚合 + +::: details 【示例】运行子聚合 + +Bucket 聚合支持 Bucket 或 Metric 子聚合。例如,具有 [avg](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html) 子聚合的 terms 聚合会计算每个桶中文档的平均值。嵌套子聚合没有级别或深度限制。 + +```json +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "size": 10 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + } + } + }, + "min_salary_by_job":{ + "min_bucket": { + "buckets_path": "jobs>avg_salary" + } + } + } +} +``` + +响应将子聚合结果嵌套在其父聚合下: + +```json +{ + // ... + "aggregations": { + "jobs": { + "doc_count_error_upper_bound": 0, + "sum_other_doc_count": 6, + "buckets": [ + { + "key": "Java Programmer", + "doc_count": 7, + "avg_salary": { + "value": 25571.428571428572 + } + }, + { + "key": "Javascript Programmer", + "doc_count": 4, + "avg_salary": { + "value": 19250.0 + } + }, + { + "key": "QA", + "doc_count": 3, + "avg_salary": { + "value": 21000.0 + } + } + ] + }, + "min_salary_by_job": { + "value": 19250.0, + "keys": ["Javascript Programmer"] + } + } +} +``` + +::: + +## Metric(指标聚合) + +指标聚合主要从不同文档的分组中提取统计数据,或者,从来自其他聚合的文档桶来提取统计数据。 + +这些统计数据通常来自数值型字段,如最小或者平均价格。用户可以单独获取每项统计数据,或者也可以使用 stats 聚合来同时获取它们。更高级的统计数据,如平方和或者是标准差,可以通过 extended stats 聚合来获取。 + +ES 支持的指标聚合类型: + +| 类型 | 说明 | +| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | +| [avg](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-avg-aggregation.html) | 平均值聚合 | +| [boxplot](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-boxplot-aggregation.html) | 箱线图聚合 | +| [cardinality](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cardinality-aggregation.html) | 近似计算非重复值 | +| [extended_stats](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-extendedstats-aggregation.html) | 扩展统计聚合 | +| [geo_bounds](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geobounds-aggregation.html) | 地理边界聚合 | +| [geo_centroid](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geocentroid-aggregation.html) | 根据* geo *字段的所有坐标值计算加权质心 | +| [geo_line_geo_line](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-geo-line.html) | 根据地理数据生成可用于线性几何图形展示的数据 | +| [cartesian_bounds](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cartesian-bounds-aggregation.html) | 笛卡尔积边界聚合 | +| [cartesian_centroid](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-cartesian-centroid-aggregation.html) | 计算所有坐标值加权质心 | +| [matrix_stats](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-matrix-stats-aggregation.html) | 矩阵统计聚合 | +| [max](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-max-aggregation.html) | 最大值聚合 | +| [median_absolute_deviation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-median-absolute-deviation-aggregation.html) | 中位数绝对偏差聚合 | +| [min](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-min-aggregation.html) | 最小值聚合 | +| [percentile_ranks](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-rank-aggregation.html) | 百分位排名聚合 | +| [percentiles](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-percentile-aggregation.html) | 百分位聚合 | +| [rate](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-rate-aggregation.html) | 频率聚合 | +| [scripted_metric](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-scripted-metric-aggregation.html) | 脚本化指标聚合 | +| [stats](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-stats-aggregation.html) | 统计聚合 | +| [string_stats](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-string-stats-aggregation.html) | 字符串统计聚合 | +| [sum](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-sum-aggregation.html) | 求和聚合 | +| [t_test](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-ttest-aggregation.html) | 校验聚合 | +| [top_hits](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-hits-aggregation.html) | 热门点击统计 | +| [top_metrics](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-top-metrics.html) | 热门指标聚合 | +| [value_count](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-valuecount-aggregation.html) | 值统计聚合 | +| [weighted_avg](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics-weight-avg-aggregation.html) | 加权平均值聚合 | + +::: details 【示例】指标聚合示例 + +Metric 聚合测试: + +```json +// Metric 聚合,找到最低的工资 +POST /employees/_search +{ + "size": 0, + "aggs": { + "min_salary": { + "min": { + "field":"salary" + } + } + } +} + +// Metric 聚合,找到最高的工资 +POST /employees/_search +{ + "size": 0, + "aggs": { + "max_salary": { + "max": { + "field":"salary" + } + } + } +} + +// 多个 Metric 聚合,找到最低最高和平均工资 +POST /employees/_search +{ + "size": 0, + "aggs": { + "max_salary": { + "max": { + "field": "salary" + } + }, + "min_salary": { + "min": { + "field": "salary" + } + }, + "avg_salary": { + "avg": { + "field": "salary" + } + } + } +} + +// 一个聚合,输出多值 +POST /employees/_search +{ + "size": 0, + "aggs": { + "stats_salary": { + "stats": { + "field": "salary" + } + } + } +} +``` + +::: + +## Bucket(桶聚合) + +桶聚合不会像指标聚合那样计算字段的指标,而是创建文档桶。每个桶都与一个标准(取决于聚合类型)相关联,该标准确定当前上下文中的文档是否“落入”其中。换句话说,桶有效地定义了文档集。除了桶本身之外,`桶`聚合还计算并返回“落入”每个桶的文档数。 + +与`指标`聚合相反,桶聚合可以保存子聚合。这些子聚合将针对其 “父” 桶聚合创建的桶进行聚合。 + +Elasticsearch 中支持的桶聚合类型: + +| 类型 | 说明 | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------- | +| [adjacency_matrix](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-adjacency-matrix-aggregation.html) | 邻接矩阵聚合 | +| [auto_interval_date_histogram](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-autodatehistogram-aggregation.html) | 自动间隔日期直方图聚合 | +| [categorize_text](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-categorize-text-aggregation.html) | 对文本进行分类聚合 | +| [children](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-children-aggregation.html) | 子文档聚合 | +| [composite](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-composite-aggregation.html) | 组合聚合 | +| [date_histogram](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-datehistogram-aggregation.html) | 日期直方图聚合 | +| [date_range](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-daterange-aggregation.html) | 日期范围聚合 | +| [diversified_sampler](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-diversified-sampler-aggregation.html) | 多种采样器聚合 | +| [filter](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filter-aggregation.html) | 过滤器聚合 | +| [filters](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-filters-aggregation.html) | 多过滤器聚合 | +| [geo_distance](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geodistance-aggregation.html) | 地理距离聚合 | +| [geohash_grid](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohashgrid-aggregation.html) | geohash 网格 | +| [geohex_grid](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geohexgrid-aggregation.html) | geohex 网格 | +| [geotile_grid](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-geotilegrid-aggregation.html) | geotile 网格 | +| [global](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-global-aggregation.html) | 全局聚合 | +| [histogram](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-histogram-aggregation.html) | 直方图聚合 | +| [ip_prefix](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-ipprefix-aggregation.html) | IP 前缀聚合 | +| [ip_range](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-iprange-aggregation.html) | IP 范围聚合 | +| [missing](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-missing-aggregation.html) | 空值聚合 | +| [multi_terms](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-multi-terms-aggregation.html) | 多词项分组聚合 | +| [nested](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-nested-aggregation.html) | 嵌套聚合 | +| [parent](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-parent-aggregation.html) | 父文档聚合 | +| [random_sampler](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-random-sampler-aggregation.html) | 随机采样器聚合 | +| [range](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-range-aggregation.html) | 范围聚合 | +| [rare_terms](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-rare-terms-aggregation.html) | 稀有多词项聚合 | +| [reverse_nested](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-reverse-nested-aggregation.html) | 反向嵌套聚合 | +| [sampler](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-sampler-aggregation.html) | 采样器聚合 | +| [significant_terms](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significantterms-aggregation.html) | 重要词项聚合 | +| [significant_text](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-significanttext-aggregation.html) | 重要文本聚合 | +| [terms](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html) | 词项分组聚合 | +| [time_series](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-time-series-aggregation.html) | 时间序列聚合 | +| [variable_width_histogram](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-variablewidthhistogram-aggregation.html) | 可变宽度直方图聚合 | + +::: details 【示例】terms 聚合查询 + +默认,ES 不允许对 Text 字段进行 terms 聚合查询 + +```json +// 对 keword 进行聚合 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field":"job.keyword" + } + } + } +} + +// 对 Text 字段进行 terms 聚合查询,失败 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job" + } + } + } +} + +// 对 Text 字段打开 fielddata,支持 terms aggregation +PUT employees/_mapping +{ + "properties" : { + "job":{ + "type": "text", + "fielddata": true + } + } +} + +// 对 Text 字段进行 terms 分词。分词后的 terms +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field":"job" + } + } + } +} + +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field":"job.keyword" + } + } + } +} +``` + +::: + +::: details 【示例】更多 Bucket 聚合示例 + +```json +// 对 job 进行近似去重统计 +POST /employees/_search +{ + "size": 0, + "aggs": { + "cardinate": { + "cardinality": { + "field": "job" + } + } + } +} + +// 对 gender 进行聚合 +POST /employees/_search +{ + "size": 0, + "aggs": { + "gender": { + "terms": { + "field":"gender" + } + } + } +} + +// 指定 bucket 的 size +POST /employees/_search +{ + "size": 0, + "aggs": { + "ages_5": { + "terms": { + "field":"age", + "size":3 + } + } + } +} + +// 指定 size,不同工种中,年纪最大的 3 个员工的具体信息 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field":"job.keyword" + }, + "aggs":{ + "old_employee":{ + "top_hits":{ + "size":3, + "sort":[ + { + "age":{ + "order":"desc" + } + } + ] + } + } + } + } + } +} + +// Salary Ranges 分桶,可以自己定义 key +POST /employees/_search +{ + "size": 0, + "aggs": { + "salary_range": { + "range": { + "field":"salary", + "ranges":[ + { + "to":10000 + }, + { + "from":10000, + "to":20000 + }, + { + "key":">20000", + "from":20000 + } + ] + } + } + } +} + +// Salary Histogram, 工资 0 到 10 万,以 5000 一个区间进行分桶 +POST /employees/_search +{ + "size": 0, + "aggs": { + "salary_histrogram": { + "histogram": { + "field":"salary", + "interval":5000, + "extended_bounds":{ + "min":0, + "max":100000 + + } + } + } + } +} + +// 嵌套聚合 1,按照工作类型分桶,并统计工资信息 +POST /employees/_search +{ + "size": 0, + "aggs": { + "Job_salary_stats": { + "terms": { + "field": "job.keyword" + }, + "aggs": { + "salary": { + "stats": { + "field": "salary" + } + } + } + } + } +} + +// 多次嵌套。根据工作类型分桶,然后按照性别分桶,计算工资的统计信息 +POST /employees/_search +{ + "size": 0, + "aggs": { + "Job_gender_stats": { + "terms": { + "field": "job.keyword" + }, + "aggs": { + "gender_stats": { + "terms": { + "field": "gender" + }, + "aggs": { + "salary_stats": { + "stats": { + "field": "salary" + } + } + } + } + } + } + } +} +``` + +::: + +## Pipeline(管道聚合) + +管道聚合处理从其他聚合而不是文档集生成的输出,从而将信息添加到输出树中。 + +Pipeline 聚合的分析结果会输出到原结果中,根据位置的不同,分为两类: + +- **sibling** - 结果和现有分析结果同级。例如:[max_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-max-bucket-aggregation.html)、[min_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-min-bucket-aggregation.html)、[avg_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-avg-bucket-aggregation.html)、[sum_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-sum-bucket-aggregation.html)、[stats_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-stats-bucket-aggregation.html)、[extended_stats_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-extended-stats-bucket-aggregation.html)、[percentiles_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-percentiles-bucket-aggregation.html)。 +- **parent** - 结果内嵌到现有的聚合分析结果中。例如:[derivative](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-derivative-aggregation.html)、[cumulative_sum](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-sum-aggregation.html)、[moving_function](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-movfn-aggregation.html)。 + +管道聚合可以通过使用 `buckets_path` 参数来指示所需指标的路径,从而引用执行计算所需的聚合。管道聚合不能具有子聚合,但根据类型,它可以引用`buckets_path`中的另一个管道,从而允许链接管道聚合。 + +以下为 Elasticsearch 支持的管道聚合类型: + +| 类型 | 说明 | +| -------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------- | +| [avg_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-avg-bucket-aggregation.html) | 平均桶 | +| [bucket_script](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-script-aggregation.html) | 桶脚本 | +| [bucket_count_ks_test](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-count-ks-test-aggregation.html) | 桶数 k-s 测试 | +| [bucket_correlation](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-correlation-aggregation.html) | 桶关联 | +| [bucket_selector](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-selector-aggregation.html) | 桶选择器 | +| [bucket_sort](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-bucket-sort-aggregation.html) | 桶排序 | +| [change_point](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-change-point-aggregation.html) | 更改点 | +| [cumulative_cardinality](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-cardinality-aggregation.html) | 累积基数 | +| [cumulative_sum](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-sum-aggregation.html) | 累计总和 | +| [derivative](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-derivative-aggregation.html) | 导数 | +| [extended_stats_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-extended-stats-bucket-aggregation.html) | 扩展的统计信息桶 | +| [inference_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-inference-bucket-aggregation.html) | 推理桶 | +| [max_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-max-bucket-aggregation.html) | 最大桶 | +| [min_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-min-bucket-aggregation.html) | 最小桶 | +| [moving_function](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-movfn-aggregation.html) | 移动功能 | +| [moving_percentiles](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-moving-percentiles-aggregation.html) | 移动百分位数 | +| [normalize](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-normalize-aggregation.html) | 正常化 | +| [percentiles_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-percentiles-bucket-aggregation.html) | 百分位数桶 | +| [serial_differencing](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-serialdiff-aggregation.html) | 序列差分 | +| [stats_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-stats-bucket-aggregation.html) | 统计桶 | +| [sum_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-sum-bucket-aggregation.html) | 总和桶 | + +::: details 【示例】Pipeline 聚合示例 + +```json +// 平均工资最低的工作类型 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "size": 10 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + } + } + }, + "min_salary_by_job":{ + "min_bucket": { + "buckets_path": "jobs>avg_salary" + } + } + } +} + +// 平均工资最高的工作类型 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "size": 10 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + } + } + }, + "max_salary_by_job":{ + "max_bucket": { + "buckets_path": "jobs>avg_salary" + } + } + } +} + +// 平均工资的平均工资 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "size": 10 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + } + } + }, + "avg_salary_by_job":{ + "avg_bucket": { + "buckets_path": "jobs>avg_salary" + } + } + } +} + +// 平均工资的统计分析 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "size": 10 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + } + } + }, + "stats_salary_by_job":{ + "stats_bucket": { + "buckets_path": "jobs>avg_salary" + } + } + } +} + +// 平均工资的百分位数 +POST /employees/_search +{ + "size": 0, + "aggs": { + "jobs": { + "terms": { + "field": "job.keyword", + "size": 10 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + } + } + }, + "percentiles_salary_by_job":{ + "percentiles_bucket": { + "buckets_path": "jobs>avg_salary" + } + } + } +} + +// 按照年龄对平均工资求导 +POST /employees/_search +{ + "size": 0, + "aggs": { + "age": { + "histogram": { + "field": "age", + "min_doc_count": 1, + "interval": 1 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + }, + "derivative_avg_salary":{ + "derivative": { + "buckets_path": "avg_salary" + } + } + } + } + } +} + +// Cumulative_sum +POST /employees/_search +{ + "size": 0, + "aggs": { + "age": { + "histogram": { + "field": "age", + "min_doc_count": 1, + "interval": 1 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + }, + "cumulative_salary":{ + "cumulative_sum": { + "buckets_path": "avg_salary" + } + } + } + } + } +} + +// Moving Function +POST /employees/_search +{ + "size": 0, + "aggs": { + "age": { + "histogram": { + "field": "age", + "min_doc_count": 1, + "interval": 1 + }, + "aggs": { + "avg_salary": { + "avg": { + "field": "salary" + } + }, + "moving_avg_salary":{ + "moving_fn": { + "buckets_path": "avg_salary", + "window":10, + "script": "MovingFunctions.min(values)" + } + } + } + } + } +} +``` + +::: + +## 聚合的执行流程 + +ES 在进行聚合分析时,协调节点会在每个分片的主分片、副分片中选一个,然后在不同分片上分别进行聚合计算,然后将每个分片的聚合结果进行汇总,返回最终结果。 + +由于,并非基于全量数据进行计算,所以聚合结果并非完全准确。 + +要解决聚合准确性问题,有两个解决方案: + +- 解决方案 1:当数据量不大时,设置 Primary Shard 为 1,这意味着在数据全集上进行聚合。 +- 解决方案 2:设置 [`shard_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-shard-size) 参数,将计算数据范围变大,进而使得 ES 的**整体性能变低,精准度变高**。shard_size 值的默认值是 `size * 1.5 + 10`。 + +## 参考资料 + +- [极客时间教程 - Elasticsearch 核心技术与实战](https://time.geekbang.org/course/detail/100030501-102659) +- [ES 官方文档之 Aggregations](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/20.Elasticsearch\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\350\277\220\347\273\264.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/20.Elasticsearch\350\277\220\347\273\264.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\350\277\220\347\273\264.md" index 01ed0ef339..b9d1a999c1 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/20.Elasticsearch\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\350\277\220\347\273\264.md" @@ -1,7 +1,7 @@ --- +icon: logos:elasticsearch title: Elasticsearch 运维 date: 2020-06-16 07:10:44 -order: 20 categories: - 数据库 - 搜索引擎数据库 @@ -11,7 +11,7 @@ tags: - 搜索引擎数据库 - Elasticsearch - 运维 -permalink: /pages/fdaf15/ +permalink: /pages/3ce6521e/ --- # Elasticsearch 运维 @@ -217,4 +217,4 @@ echo "* hard nproc 4096" > /etc/security/limits.conf - [Elasticsearch 官方下载安装说明](https://www.elastic.co/cn/downloads/elasticsearch) - [Install Elasticsearch with RPM](https://www.elastic.co/guide/en/elasticsearch/reference/current/rpm.html#rpm) -- [Elasticsearch 使用积累](http://siye1982.github.io/2015/09/17/es-optimize/) \ No newline at end of file +- [Elasticsearch 使用积累](http://siye1982.github.io/2015/09/17/es-optimize/) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/13.Elasticsearch\351\233\206\347\276\244\345\222\214\345\210\206\347\211\207.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\351\233\206\347\276\244.md" similarity index 99% rename from "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/13.Elasticsearch\351\233\206\347\276\244\345\222\214\345\210\206\347\211\207.md" rename to "source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\351\233\206\347\276\244.md" index a1186b559b..134f38238b 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/13.Elasticsearch\351\233\206\347\276\244\345\222\214\345\210\206\347\211\207.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\351\233\206\347\276\244.md" @@ -1,7 +1,7 @@ --- -title: Elasticsearch 集群和分片 +icon: logos:elasticsearch +title: Elasticsearch 集群 date: 2022-03-01 20:52:25 -order: 13 categories: - 数据库 - 搜索引擎数据库 @@ -12,10 +12,10 @@ tags: - Elasticsearch - 集群 - 分片 -permalink: /pages/9a2546/ +permalink: /pages/e3fe52cc/ --- -# Elasticsearch 集群和分片 +# Elasticsearch 集群 ## 集群 @@ -519,4 +519,4 @@ POST /logstash-2014-10/_optimize?max_num_segments=1 ## 参考资料 -- [Elasticsearch 官方文档之 集群内的原理](https://www.elastic.co/guide/cn/elasticsearch/guide/current/distributed-cluster.html) \ No newline at end of file +- [Elasticsearch 官方文档之 集群内的原理](https://www.elastic.co/guide/cn/elasticsearch/guide/current/distributed-cluster.html) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\351\235\242\350\257\225.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..ac6c459c8d --- /dev/null +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/Elasticsearch_\351\235\242\350\257\225.md" @@ -0,0 +1,854 @@ +--- +icon: logos:elasticsearch +title: Elasticsearch 面试 +date: 2020-06-16 07:10:44 +categories: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch + - 面试 +permalink: /pages/6219b063/ +--- + +# Elasticsearch 面试 + +## Elasticsearch 简介 + +### 【基础】什么是 ES? + +:::details 要点 + +**[Elasticsearch](https://github.com/elastic/elasticsearch) 是一个开源的分布式搜索和分析引擎**。 + +**[Elasticsearch](https://github.com/elastic/elasticsearch) 基于搜索库 [Lucene](https://github.com/apache/lucene-solr) 开发**。Elasticsearch 隐藏了 Lucene 的复杂性,提供了简单易用的 REST API / Java API 接口(另外还有其他语言的 API 接口)。 + +Elasticsearch 是面向文档的,它**将复杂数据结构序列化为 JSON 形式存储**。 + +**Elasticsearch 是近实时(Near Realtime,缩写 NRT)的全文搜索**。近实时是指: + +- 从写入数据到数据可以被搜索,存在较小的延迟(大概是 1s)。 +- 基于 Elasticsearch 执行搜索和分析可以达到秒级。 + +::: + +### 【基础】ES 的应用场景有哪些? + +:::details 要点 + +Elasticsearch 的主要功能如下: + +- **海量数据的分布式存储及集群管理** +- **提供丰富的近实时搜索能力** +- **海量数据的近实时分析(聚合)** + +Elasticsearch 被广泛应用于以下场景: + +- **搜索** + - **全文检索** - Elasticsearch 通过快速搜索大型数据集,使复杂的搜索查询变得更加容易。它对于需要即时和相关搜索结果的网站、应用程序或企业特别有用。 + - **自动补全和拼写纠正** - 可以在用户输入内容时,实时提供自动补全和拼写纠正,以增加用户体验并提高搜索效率。 + - **地理空间搜索** - 使用地理空间查询搜索位置并计算空间关系。 + - 近实时分析 - Elasticsearch 能够进行实时分析,使其适用于追踪实时数据的仪表板,例如用户活动、用户画像等,分析后进行推送。 +- **可观测性** + - **日志、指标和链路追踪** - 收集、存储和分析来自应用程序、系统和服务的日志、指标和追踪。 + - **性能监控** - 监控和分析业务关键性能指标。 + - **OpenTelemetry** - 使用 OpenTelemetry 标准,将遥测数据采集到 Elastic Stack。 + +::: + +### 【基础】如何在 ES 中 CRUD? + +:::details 要点 + +Elasticsearch 的基本 CRUD 方式如下: + +- **添加索引** + - `PUT /_create/` - 指定 id,如果 id 已存在,报错 + - `POST /_doc` - 自动生成 `_id` +- **删除索引** - `DELETE /?pretty` +- **更新索引** - `POST /_update/` +- **查询索引** - `GET /_doc/` +- **批量更新** - `bulk` API 支持 `index/create/update/delete` +- **批量查询** - `_mget` 和 `_msearch` 可以用于批量查询 + +> 扩展:[Quick starts](https://www.elastic.co/guide/en/elasticsearch/reference/current/quickstart.html) + +::: + +### 【中级】什么是 Elasic Stack(ELK)? + +:::details 要点 + +Elastic Stack 通常被用来作为日志采集、检索、可视化的解决方案。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411231210104.png) + +Elastic Stack 也常被称为 ELK,这是 Elastic 公司旗下三款产品 [Elasticsearch](https://www.elastic.co/elasticsearch) 、[Logstash](https://www.elastic.co/products/logstash) 、[Kibana](https://www.elastic.co/kibana) 的首字母组合。 + +- [Elasticsearch](https://www.elastic.co/elasticsearch) 负责存储数据,并提供对数据的检索和分析。 +- [Logstash](https://www.elastic.co/logstash) 传输和处理你的日志、事务或其他数据。 +- [Kibana](https://www.elastic.co/kibana) 将 Elasticsearch 的数据分析并渲染为可视化的报表。 + +Elastic Stack,在 ELK 的基础上扩展了一些新的产品。如:[Beats](https://www.elastic.co/beats),这是针对不同类型数据的轻量级采集器套件。 + +此外,基于 Elastic Stack,其技术生态还可以和一些主流的分布式中间件进行集成,以应对各种不同的场景。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411231211496.png) + +::: + +## Elasticsearch 存储 + +### 【基础】ES 的逻辑存储是怎样设计的? + +:::details 要点 + +Elasticsearch 的逻辑存储被设计为层级结构,自上而下依次为: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411260812733.png) + +各层级结构的说明如下: + +(1)Document(文档) + +Elasticsearch 是面向文档的,这意味着读写数据的最小单位是文档。Elasticsearch 以 JSON 文档的形式序列化和存储数据。文档是一组字段,这些字段是包含数据的键值对。每个文档都有一个唯一的 ID。 + +一个简单的 Elasticsearch 文档可能如下所示: + +```json +{ + "_index": "my-first-elasticsearch-index", + "_id": "DyFpo5EBxE8fzbb95DOa", + "_version": 1, + "_seq_no": 0, + "_primary_term": 1, + "found": true, + "_source": { + "email": "john@smith.com", + "first_name": "John", + "last_name": "Smith", + "info": { + "bio": "Eco-warrior and defender of the weak", + "age": 25, + "interests": ["dolphins", "whales"] + }, + "join_date": "2024/05/01" + } +} +``` + +Elasticsearch 中的 document 是无模式的,也就是并非所有 document 都必须拥有完全相同的字段,它们不受限于同一个模式。 + +(2)Field(字段) + +field 包含数据的键值对。默认情况下,Elasticsearch 对每个字段中的所有数据建立索引,并且每个索引字段都具有专用的优化数据结构。 + +`document` 包含数据和元数据。[**Metadata Field(元数据字段)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-fields.html) 是存储有关文档信息的系统字段。在 Elasticsearch 中,元数据字段都以 `_` 开头。常见的元数据字段有: + +- [`_index`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-index-field.html) - 文档所属的索引 +- [`_id`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-id-field.html) - 文档的 ID +- [`_source`](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-source-field.html) - 表示文档原文的 JSON + +(3)Type(类型) + +在 Elasticsearch 中,**type 是 document 的逻辑分类**。每个 index 里可以有一个或多个 type。 + +不同的 type 应该有相似的结构(schema)。举例来说,`id`字段不能在这个组是字符串,在另一个组是数值。 + +> 注意:Elasticsearch 7.x 版已彻底移除 type。 + +(4)Index(索引) + +在 Elasticsearch 中,**可以将 index 视为 document 的集合**。 + +Elasticsearch 会为所有字段建立索引,经过处理后写入一个倒排索引(Inverted Index)。查找数据的时候,直接查找该索引。 + +所以,Elasticsearch 数据管理的顶层单位就叫做 Index。它是单个数据库的同义词。每个 Index 的名字必须是小写。 + +(5)Elasticsearch 概念和 RDBM 概念 + +| Elasticsearch | DB | +| -------------------------------- | ------------------ | +| 索引(index) | 数据库(database) | +| 类型(type,6.0 废弃,7.0 移除) | 数据表(table) | +| 文档(docuemnt) | 行(row) | +| 字符(field) | 列(column) | +| 映射(mapping) | 表结构(schema) | + +::: + +### 【基础】ES 的物理存储是怎样设计的? + +:::details 要点 + +Elasticsearch 的物理存储,天然使用了分布式设计。 + +每个 Elasticsearch 进程都从属于一个 cluster,一个 cluster 可以有一个或多个 node(即 Elasticsearch 进程)。 + +Elasticsearch 存储会将每个 index 分为多个 shard,而 shard 可以分布在集群中不同节点上。正是由于这个机制,使得 Elasticsearch 有了水平扩展的能力。shard 也是 Elasticsearch 将数据从一个节点迁移到拎一个节点的最小单位。 + +Elasticsearch 的每个 shard 对应一个 Lucene index(一个包含倒排索引的文件目录)。Lucene index 又会被分解为多个 segment。segment 是索引中的内部存储元素,由于写入效率的考虑,所以被设计为不可变更的。segment 会定期 [合并](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html) 较大的 segment,以保持索引大小。简单来说,Lucene 就是一个 jar 包,里面包含了封装好的构建、管理倒排索引的算法代码。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411260815446.png) + +::: + +### 【中级】什么是倒排索引? + +:::details 要点 + +既然有倒排索引,顾名思义,有与之相对的正排索引。这里,以实现一个诗词检索器为例,来说明一下正排索引和倒排索引的区别。 + +**正排索引是 ID 到数据的映射关系**。如下所示,每首诗词用一个 ID 唯一识别。如果,我们要查找诗歌内容中是否包含某个关键字,就不得不在内容的完整文本中进行检索,效率很低。即使针对文档内容创建传统 RDBM 的索引(通常为 B+ 树结构),查找效率依然低下,并且会产生较大的额外存储空间开销。 + +| ID | 文档标题 | 文档内容 | +| --- | ---------- | ------------------------------------------------ | +| 1 | 望月怀远 | 海上生明月,天涯共此时… | +| 2 | 春江花月夜 | 春江潮水连海平,海上明月共潮生… | +| 3 | 静夜思 | 床前明月光,疑是地上霜。举头望明月,低头思故乡。 | +| 4 | 锦瑟 | 沧海月明珠有泪,蓝田日暖玉生烟… | + +倒排索引的实现与正排索引相反。**将文本分词后保存为多个词项,词项到 ID 的映射关系称为倒排索引(Inverted index)**。 + +| 词项 | ID | 词频 | +| ---- | ---------- | ---------------------------------- | +| 月 | 1, 2, 3, 4 | 1:1 次、2:1 次、3:2 次、4:1 次 | +| 明月 | 1, 2, 3 | 1:1 次、2:1 次、3:2 次 | +| 海 | 1, 2, 4 | 1:1 次、2:1 次、4:1 次 | + +除了要保存词项与 ID 的关系外,还需要保存这个词项在对应文档出现的位置、偏移量等信息,这是因为很多检索的场景中还需要判断关键词前后的内容是否符合搜索要求。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411260816781.png) + +::: + +### 【中级】什么是字典树? + +:::details 要点 + +Trie(字典树),也被称为前缀树,是一种树状数据结构,用于有效检索键值对。它通常用于实现字典和自动补全功能,使其成为许多搜索算法的基本组件。 + +Trie 遵循一个规则:如果两个字符串有共同的前缀,那么它们在 Trie 中将具有相同的祖先。 + +Trie 的检索能力也可以使用 Hash 替代,但是 Trie 比 Hash 更高效。此外,Trie 有 Hash 不具备的优势:Trie 支持前缀搜索和排序。Trie 的主要问题是:存储词项需要额外的空间,对于长文本,空间可能会变得很大。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411301547515.png) + +::: + +### 【高级】ES 如何实现倒排索引? + +:::details 要点 + +在 Elasticsearch 中,数据存储、检索实际上是基于 Lucene 实现。 + +一个 Elasticsearch shard 对应一个 Lucene index, + +Elasticsearch 的每个 shard 对应一个 Lucene index(一个包含倒排索引的文件目录)。Lucene index 又会被分解为多个 segment。segment 是索引中的内部存储元素,由于写入效率的考虑,所以被设计为不可变更的。segment 会定期 [合并](https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules-merge.html) 较大的 segment,以保持索引大小。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411260817705.png) + +倒排索引的组成主要有 3 个部分: + +- **Term Dictionary** - **Term Dictionary 用于保存 term(词项)**。由于 ES 会对 document 中的每个 field 都进行分词,所以数据量可能会非常大。 + - Term Dictionary 存储数据时,先将所有的 term 进行排序,然后将 Term Dictionary 中有共同前缀的 term 抽取出来进行分块存储;再对共同前缀做索引,最后通过索引就可以找到公共前缀对应的块在 Term Dictionary 文件中的偏移地址。 + - 由于每个块中都有共同前缀,所有不需要再保存每个 Term 的全部内容,只需要保存其后缀即可,而且这些后缀都是排好序的。 +- **Term Index** - **Term Index 是 Term Dictionary 的索引**。由于 Term Dictionary 存储的 term 可能会非常多,为了提高查询效率,从而设计了 Term Index。 + - 为了提高检索效率以及节省空间,Term Index 只使用公共前缀做索引。 + - **Lucene 中实现 Term Index 采用了 FST 算法**。FST 是一种非常复杂的结构,可以把它简单理解为一个**占用空间小且高效的 KV 数据结构**,有点类似于 Trie(字典树)。FST 有以下的特点: + - 通过对 Term Dictionary 数据的前缀复用,压缩了存储空间; + - 高效的查询性能,`O(len(prefix))` 的复杂度; + - 构建后不可修改,因此 Lucene segment 也不允许修改。 +- **Posting List** - **Posting List 保存着每个 term 的映射信息**。如文档 ID、词频、位置等。Lucene 把这些数据分成 3 个文件进行存储: + - `.doc` 文件,记录了文档 ID 信息和 term 的词频,还额外记录了跳表的信息,用来加速文档 ID 的查询;并且还记录了 term 在 `.pos` 和 `.pay` 文件中的位置,有助于进行快速读取。 + - `.pay` 文件,记录了 payload 信息和 term 在 doc 中的偏移信息; + - `.pos` 文件,记录了 term 在 doc 中的位置信息。 + +> 扩展:https://www.itshujia.com/read/elasticsearch/354.html + +::: + +### 【基础】ES 支持哪些数据类型? + +:::details 要点 + +Elasticsearch 支持丰富的数据类型,常见的有: + +- 文本类型:[`text`](https://www.elastic.co/guide/en/elasticsearch/reference/current/text.html)、[`keyword`](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#keyword-field-type)、[`constant_keyword`](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#constant-keyword-field-type)、 [`wildcard`](https://www.elastic.co/guide/en/elasticsearch/reference/current/keyword.html#wildcard-field-type) +- 二进制类型:[`binary`](https://www.elastic.co/guide/en/elasticsearch/reference/current/binary.html) +- 数值类型:`long`、`float` 等 +- 日期类型:[`date`](https://www.elastic.co/guide/en/elasticsearch/reference/current/date.html) +- 布尔类型:[`boolean`](https://www.elastic.co/guide/en/elasticsearch/reference/current/boolean.html) +- 对象类型:[`object`](https://www.elastic.co/guide/en/elasticsearch/reference/current/object.html)、[`nested`](https://www.elastic.co/guide/en/elasticsearch/reference/current/nested.html) + +> 扩展:[数据类型](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html) + +::: + +### 【基础】ES 如何识别字段的数据类型? + +:::details 要点 + +在 Elasticsearch 中,每个文档都是字段的集合,每个字段都有自己的 [数据类型](https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping-types.html)。**Elasticsearch 通过映射来定义文档及其包含字段的存储和索引方式**。 + +Elasticsearch 映射可分为动态映射和静态映射。 + +在 RDBM 中写入数据之前首先要建表,在建表语句中声明字段的属性,在 Elasticsearch 中,则不必如此。Elasticsearch 最重要的功能之一是:文档写入 Elasticsearch 中,它会自动检测新字段的数据类型,这种机制称为 [**动态映射**](https://www.elastic.co/guide/en/elasticsearch/reference/current/dynamic-mapping.html)。也正是由于这点,所以说 Elasticsearch 是无模式的。 + +例如,执行下面添加文档的操作,Elasticsearch 会自动将 `count` 字段识别为 `long` 类型。 + +```bash +PUT data/_doc/1 +{ "count": 5 } +``` + +Elasticsearch 的动态映射无法保证完全符合预期,因此 Elasticsearch 也提供了显示设置映射规则的方法。[**静态映射(显示映射)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/explicit-mapping.html) 是在创建索引时显示设置索引映射(即设置 mapping)。静态映射和 SQL 中在建表语句中指定字段属性类似。相比动态映射,通过静态映射可以添加更详细、更精准的配置信息。 + +- mapping 是用于定义文档结构的 JSON 对象。它指定文档中允许的字段,以及它们的数据类型和其他属性。mapping 用于控制文档的存储和索引方式,还影响文档的搜索和分析方式。 + +> 扩展: +> +> https://www.elastic.co/guide/en/elasticsearch/reference/current/mapping.html + +::: + +## Elasticsearch 搜索 + +### 【基础】ES 索引别名有什么用? + +:::details 要点 + +Elasticsearch 中的别名可用于更轻松地管理和使用索引。别名允许同时对多个索引执行操作,或者通过隐藏底层索引结构的复杂性来简化索引管理。 + +扩展: + +https://www.elastic.co/guide/en/elasticsearch/reference/current/aliases.html + +::: + +### 【基础】ES 中有哪些全文搜索 API? + +:::details 要点 + +ES 支持全文搜索的 API 主要有以下几个: + +- [match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query.html) - 匹配查询可以处理全文本、精确字段(日期、数字等)。 +- [match_phrase](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase.html) - 短语匹配会将检索内容分词,这些词语必须全部出现在被检索内容中,并且顺序必须一致,默认情况下这些词都必须连续。 +- [match_phrase_prefix](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-match-query-phrase-prefix.html) - 与 match_phrase 类似,但最后一个词项会作为前缀,并且匹配这个词项开头的任何词语。 +- [multi_match](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-multi-match-query.html) - 允许在多个字段上执行相同的查询语句。 + +::: + +### 【基础】ES 中有哪些全文搜索 API? + +:::details 要点 + +**词项查询是对词项进行精确匹配**。词项查询通常用于结构化数据,如数字、日期和枚举类型。 + +ES 支持词项搜索的 API 主要有以下几个: + +- **[exists](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-exists-query.html)** - 返回在指定字段上有值的文档,一般用于过滤没有值的文档。 +- **[fuzzy](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-fuzzy-query.html)** - 模糊查询返回包含与搜索词相似的词的文档。 +- **[prefix](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-prefix-query.html)** - 用于查询某个字段中包含指定前缀的文档。 +- **[range](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-range-query.html)** - 范围查询。 +- **[regexp](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-regexp-query.html)** - 正则查询。 +- **[term](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-term-query.html)** - 返回在指定字段中准确包含了检索内容的文档。 +- **[terms](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-query.html)** - 与 Term 类似,不过可以同时的检索多个词项。 +- **[wildcard](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-wildcard-query.html)** - 通配符查询,返回与通配符模式匹配的文档。 + +::: + +### 【基础】ES 支持哪些组合查询? + +:::details 要点 + +ES 支持以下复合查询: + +- [`bool`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-bool-query.html) - 布尔查询,可以组合多个过滤语句来过滤文档。 +- [`boosting`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-boosting-query.html) - 在 positive 块中指定匹配文档的语句,同时降低在 negative 块中也匹配的文档的得分,提供调整相关性算分的能力。 +- [`constant_score`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-constant-score-query.html) - 包装了一个过滤器查询,不进行算分。 +- [`dis_max`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html) - 返回匹配了一个或者多个查询语句的文档,但只将最佳匹配的评分作为相关性算分返回。 +- [`function_score`](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-function-score-query.html) - 支持使用函数来修改查询返回的分数。 + +::: + +### 【基础】ES 中的 query 和 filter 有什么区别? + +:::details 要点 + +在 Elasticsearch 中,可以在两个不同的上下文中执行查询: + +- `query` context - **有相关性计算**,采用相关性算法,计算文档与查询关键词之间的相关度,并根据评分(`_score`)大小排序。 +- `filter` context - **无相关性计算**,可以利用缓存,性能更好。 + +> 扩展:https://www.elastic.co/guide/en/elasticsearch/reference/current/query-filter-context.html + +::: + +## Elasticsearch 聚合 + +### 【基础】什么是聚合?ES 中有哪些聚合? + +:::details 要点 + +Elasticsearch 中的聚合是一项强大的功能,可让您实时分析、汇总和执行复杂的数据集计算。聚合提供了从索引数据中分组和提取可操作见解的功能,这些数据可用于数据可视化、报告和分析目的。 + +Elasticsearch 中的聚合主要有三种类型: + +- [Bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket.html) - 分组计算 +- [Metric](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-metrics.html) - 统计值计算 +- [Pipeline](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline.html) -在聚合结果的基础上再次聚合,而非直接处理文档数据 + +::: + +### 【中级】ES 如何对海量数据(过亿)进行聚合计算? + +:::details 要点 + +Elasticsearch 提供的首个近似聚合是 cardinality 度量。它提供一个字段的基数,即该字段的 distinct 或者 unique 值的数目。它是基于 HLL 算法的。HLL 会先对我们的输入作哈希运算,然后根据哈希运算的结果中的 bits 做概率估算从而得到基数。其特点是:可配置的精度,用来控制内存的使用(更精确 = 更多内存);小的数据集精度是非常高的;我们可以通过配置参数,来设置去重需要的固定内存使用量。无论数千还是数十亿的唯一值,内存使用量只与你配置的精确度相关。 + +::: + +## Elasticsearch 分析 + +:::details 要点 + +### 【基础】Elasticsearch 中的分析器是什么? + +在 Elasticsearch 中,分析器是用于对文本进行分词的组件。分析器用于将文本分解为更小的单元,称为分词。然后,这些分词用于索引和搜索文本。分析器的主要目标是将原始文本转换为可以有效搜索和分析的结构化格式 (分词)。 + +文本分析由 [**analyzer(分析器)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html) 执行,分析器是一组控制整个过程的规则。无论是索引还是搜索,都需要使用分析器。 + +[**analyzer(分析器)**](https://www.elastic.co/guide/en/elasticsearch/reference/current/analyzer-anatomy.html) 由三个组件组成:零个或多个 [Character Filters(字符过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-charfilters.html)、有且仅有一个 [Tokenizer(分词器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenizers.html)、零个或多个 [Token Filters(分词过滤器)](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-tokenfilters.html)。分析的执行顺序为:`character filters -> tokenizer -> token filters`。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412012129250.png) + +::: + +## Elasticsearch 复制 + +### 【中级】ES 如何保证高可用? + +:::details 要点 + +**ES 通过副本机制实现高可用**。ES 的数据副本模型参考了 [PacificA 算法](https://www.microsoft.com/en-us/research/wp-content/uploads/2008/02/tr-2008-25.pdf)。 + +ES 必须满足以下条件才能运行: + +- 至少需要 [选举一个主节点](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-discovery-quorums.html) +- 每个 [角色](https://www.elastic.co/guide/en/elasticsearch/reference/current/modules-node.html) 至少一个节点 +- 每个 [分片](https://www.elastic.co/guide/en/elasticsearch/reference/current/scalability.html) 至少有一个副本(主副本) + +默认的情况下,ES 的数据写入只需要保证主副本写入了即可,ES 在写上选择的是**可用性优先**,而并不是像 PacificA 协议那样的强一致性。而数据读取方面,ES 可能会读取到没有 commit 的数据,所以 ES 的数据读取可能产生不一致的情况。 + +在数据恢复方面,系统可以借助 GlobalCheckpoint 和 LocalCheckpoint 来加速数据恢复的过程。如果集群中只有旧的副本可用,那么可以使用 **allocate_stale_primary 将一个指定的旧分片分配为主分片,但会造成数据丢失,慎用!** + +> 扩展: +> +> - https://www.elastic.co/guide/en/elasticsearch/reference/current/high-availability.html +> - https://www.itshujia.com/read/elasticsearch/362.html +> - https://www.itshujia.com/read/elasticsearch/363.html + +::: + +### 【中级】ES 是如何实现选主的? + +:::details 要点 + +发起选主流程的条件: + +- 只有 master-eligible 节点(通过 `node.master: true` 设置)才能发起选主流程。 +- 该 master-eligible 节点的当前状态不是 master。 +- 该 master-eligible 节点通过 ZenDiscovery 模块的 ping 操作询问其已知的集群其他节点,没有任何节点连接到 master。 +- 包括本节点在内,当前已有超过 `discovery.zen.minimum_master_nodes` 个节点没有连接到 master。 + +> 一般,应设置 `discovery.zen.minimum_master_nodes` 为 `N / 2 + 1`,以保证各种分布式决议能得到大多数节点认可。当集群由于故障(如:通信失联)被分割成多个子集群时,节点数未达到半数以上的子集群,不允许进行选主。以此,来避免出现**脑裂**问题。 + +选主流程: + +- Elasticsearch 的选主是 ZenDiscovery 模块负责的,主要包含 Ping(节点之间通过这个 RPC 来发现彼此)和 Unicast(单播模块,包含一个主机列表以控制哪些节点需要 ping 通)这两部分; +- 对所有 master-eligible 节点根据 nodeId 字典排序:每次选举时,每个节点都把自己所知道的节点排一次序,然后选出 id 最小的节点,投票该节点为 master 节点。 +- 如果对某个节点的投票数达到一定的值(`投票数 > N / 2 + 1`),并且该节点自己也投票自己,那这个节点就当选 master。否则,重新发起选举,直到满足上述条件。 + +::: + +### 【中级】ES 如何避免脑裂问题? + +:::details 要点 + +ES 集群采用主从架构模式,集群中有且只能有一个 Master 存在。 + +现在假设这样一种场景,ES 集群部署在 2 个不同的机房。若两个机房网络断连,其中没有主节点的机房进行选主,产生了一个新的主节点。这时,就同时存在了两个主节点,它们各自负责处理接收的请求,会存在数据不一致。一旦,两个机房恢复通信,又将以哪个主节点为主,数据不一致问题怎么办,这就是**脑裂**问题。 + +那如何避免产生脑裂呢?**ES 使用了 Quorum 机制来避免脑裂,在进行选主的时候,需要超过半数 Master 候选节点参与选主才行**。假如有 5 个 Master 候选节点,如果要成功选举出 Master,必须有 (5 / 2) + 1 = 3 个 Master 候选节点参与选主才行。 + +在 6.x 及之前的版本使用 Zen Discovery 的集群协调子系统,Zen Discovery 允许用户通过使用 `discovery.zen.minimum_master_nodes` 设置来决定多少个符合主节点条件的节点可以选举出主节点。通常,只有 Master Eligible 节点(Master 候选节点)数大于 Quorum 的时候才能进行选主。计算公式如下: + +``` +Quorum = (Master 候选节点数 / 2) + 1 +``` + +Elasticsearch 7.0 中,重新设计并重建了集群协调子系统: + +- 移除了 `discovery.zen.minimum_master_nodes` 设置,让 Elasticsearch 自己选择可以形成法定数量的节点。 +- 典型的主节点选举只需很短时间就能完成。 +- 集群的扩充和缩减变得更加安全和简单,并且大幅降低了因系统配置不当而可能造成数据丢失的风险。 +- 节点状态记录比以往清晰很多,有助于诊断它们不能加入集群的原因,或者为何不能选举出主节点。 + +::: + +## Elasticsearch 分片 + +### 【中级】ES 是如何实现水平扩展的? + +:::details 要点 + +Elasticsearch 通过分片来实现水平扩展。在 Elasticsearch 中,分片是索引的逻辑划分。索引可以有一个或多个分片,并且每个分片可以存储在集群中的不同节点上。分片用于在多个节点之间分配数据,从而提高性能和可扩展性。 + +Elasticsearch 中有两种类型的分片: + +- primary shard(主分片) - 用于存储原始数据。适当增加主分片数,可以提升 Elasticsearch 集群的吞吐量和整体容量。 +- replica shard(副本分片) - 用于存储数据备份。 + +默认情况下,每个索引都有 1 个主分片(早期版本,默认每个索引有 5 个主分片)。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411221525828.png) + +::: + +### 【中级】ES 如何选择读写数据映射到哪个分片上? + +:::details 要点 + +为了避免出现数据倾斜,系统需要一种高效的方式把数据均匀分散到各个节点上**存储**,并且**在检索的时候可以快速找到**文档所在的节点与分片。这就需要确立路由算法,使得数据可以映射到指定的节点上。 + +常见的路由方式如下: + +| **算法** | **描述** | +| :------- | :----------------------------------------------------------------------------------------------------------------------------------------------- | +| 随机算法 | 写数据时,随机写入到一个节点中;读数据时,由于不知道查询数据存在于哪个节点,所以需要遍历所有节点。 | +| 路由表 | 由中心节点统一维护数据的路由表,以保证唯一性;但是,中心化产生了新的问题:单点故障、数据越大,路由表越大、单点容易称为性能瓶颈、数据迁移复杂等。 | +| 哈希取模 | 对 key 值进行哈希计算,然后根据节点数取模,以确定节点。 | + +ES 的数据路由算法是根据文档 ID 和 routing key 来确定 Shard ID 的过程。**默认的情况下 routing key 为文档 ID**,路由算法一般情况下的计算公式如下: + +``` + shard_number = hash(_routing) % numer_of_primary_shards +``` + +也可以在请求中指定 routing key,下面是新增数据的时候指定 routing 的方式: + +```bash +PUT /_doc/?routing=routing_key +{ + "field1": "xxx", + "field2": "xxx" +} +``` + +添加数据时,如果不指定文档 ID,ES 会自动分片一个随机 ID。这种情况下,结合 Hash 算法,可以保证数据被均匀分布到各个分片中。如果指定文档 ID,或指定 routing key,Hash 计算得出的值可能会不够随机,从而导致数据倾斜。 + +**index 一旦设置了主分片数就不能修改,如果要修改就需要 reindex(即数据迁移)**。之所以如此,就是因为:一旦修改了主分片数,即等于修改了原 Hash 计算中的变量,无法再通过 Hash 计算正确路由到数据存储的分片。 + +::: + +### 【中级】如何合理设置 ES 分片? + +:::details 要点 + +ES 索引设置多分片有以下好处: + +- 多分片如果分布在不同的节点,查询可以在不同分片上并行执行,提升查询速度; +- 数据写入时,会分散在不同节点存储,避免数据倾斜。 + +设置多少分片合适: + +一般,**分片数要大于节点数**,这样可以保证:一旦集群中有新的数据节点加入,ES 会自动对分片数进行再均衡,使得分片尽量在集群中分布均匀。 + +**分片数也不宜设置过多**,这会带来一些问题: + +- 每一个 ES 分片对应一个 Lucene 索引,Lucene 索引存储在一个文件系统的目录中,它又可以分为多个 Segment,每个存储在一个文件中。因此,过多的分片意味着过多的文件,这会导致较大的读写性能开销。 +- 此外,分片的元数据信息由 Master 节点维护,分片过多,会增加管理负担。建议,**集群的总分片数控制在 10w 以内**。 + +单数据节点分片限制: + +- **每个非冻结数据节点 1000 个分片**,通过 `cluster.max_shards_per_node` 控制 +- **每个冻结数据节点 3000 个分片**,通过 `cluster.max_shards_per_node.frozen` 控制 + +此外,分片大小也要有所限制: + +- 理论上,**一个分片最多包含约 20 亿个文档(`Integer.MAX_VALUE - 128`)**。但是,经验表明,**每个分片的文档数量最好保持在 2 亿以下**。 +- **非日志型(搜索型、线上业务型) ES 的单分片容量最好在 [10GB, 30GB] 范围内**; +- **日志型 ES 的单分片容量最好在 [30GB, 30GB] 范围内**; + +分片大小的上下限可以分别通过 `max_primary_shard_size` 和 `min_primary_shard_size` 来控制。 + +> 扩展: +> +> - https://www.elastic.co/cn/blog/how-many-shards-should-i-have-in-my-elasticsearch-cluster +> - https://elastic.ac.cn/guide/en/elasticsearch/reference/current/size-your-shards.html + +::: + +## Elasticsearch 集群 + +### 【中级】Elasticsearch 集群中有哪些不同类型的节点? + +:::details 要点 + +Elasticsearch 中的节点是指集群中的单个 Elasticsearch 进程实例。节点用于存储数据并参与集群的索引和搜索功能。 + +节点间会相互通信以分配数据和工作负载,从而确保集群的平衡和高性能。节点可以配置不同的角色,这些角色决定了它们在集群中的职责。 + +可以通过在 `elasticsearch.yml` 中设置 `node.roles` 来为节点分配角色。 + +ES 中主要有以下节点类型: + +| 节点类型 | 说明 | 配置 | +| :----------------------- | :------------------------------------------------------------------------------------------------------- | :------------------------------------------------ | +| **master eligible node** | **候选主节点**。一旦成为主节点,可以管理整个集群:创建、更新、删除索引;添加或删除节点;为节点分配分片。 | 低配置的 CPU、内存、磁盘 | +| **data node** | **数据节点**。负责数据的存储和读取。 | 高配置的 CPU、内存、磁盘 | +| **coordinating node** | **协调节点**。负责请求的分发,结果的汇总。 | 高配置的 CPU、中等配置的内存、低配置的磁盘 | +| ingest node | **预处理节点**。负责处理数据、数据转换。 | 高配置的 CPU、中等配置的内存、低配置的磁盘 | +| warm & hot node | 存储冷、热数据的数据节点。 | Hot 类型的节点,都是高配配置,Warm 都是中低配即可 | + +::: + +## Elasticsearch 架构 + +### 【高级】ES 存储数据的流程是怎样的? + +:::details 要点 + +![](https://www.elastic.co/guide/en/elasticsearch/reference/current/images/data_processing_flow.png) + +ES 存储数据的流程可以从三个角度来阐述: + +- 从**集群**的角度来看,数据写入会先路由到主分片,在主分片上写入成功后,会并发写副本分片,最后响应给客户端。 + + ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412012126135.png) + +- 从**分片**的角度来看,数据到达分片后需要对内容进行格式校验、分词处理然后再索引数据。 + +- 从**节点**的角度来看,ES 数据持久化的步骤可归纳为:**Refresh、写 Translog、Flush、Merge。** + + ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412012127951.png) + + - 默认,ES 会每秒执行一次 **Refresh** 操作,把 Index Buffer 的数据写入磁盘中,但不会调用 fsync 刷盘。ES 提供近实时搜索的原因是因为数据被 Refresh 后才能被检索出来 。 + - 为了保证数据不丢失,在**写完 Index Buffer 后,ES 还要写 Translog**。Translog 是追加写入的,并且默认是调用 fsync 进行刷盘的。 + - **Flush** 操作会将 Filesystem Cache 中的数据持久化到磁盘中,默认 30 分钟或者在 Translog 写满时(默认 512 MB)触发执行。Flush 将磁盘缓存持久化到磁盘后,会清空 Translog。 + - 最后,ES 和 Lucene 会自动执行 **Merge** 操作,清理过多的 Segment 文件,这个时候被标记为删除的文档会正式被物理删除。 + +> 扩展: +> +> - https://www.itshujia.com/read/elasticsearch/359.html +> - https://github.com/doocs/advanced-java/blob/main/docs/high-concurrency/es-write-query-search.md + +::: + +### 【高级】ES 搜索数据的流程是怎样的? + +:::details 要点 + +在 Elasticsearch 中,搜索一般分为两个阶段,query 和 fetch 阶段。可以简单的理解,query 阶段确定要取哪些 doc,fetch 阶段取出具体的 doc。 + +Query 阶段会根据搜索条件遍历每个分片(主分片或者副分片中的其一)中的数据,返回符合条件的前 N 条数据的 ID 和排序值,然后在协调节点中对所有分片的数据进行排序,获取前 N 条数据的 ID。 + +**Query 阶段的流程**如下: + +1. 客户端发送请求到任意一个节点,这个 node 成为 coordinate node(协调节点)。coordinate node 创建一个大小为 from + size 的优先级队列用来存放结果。 +2. coordinate node 对 document 进行路由,将请求转发到对应的 node,此时会使用 round-robin 随机轮询算法,在 primary shard 以及其所有 replica 中随机选择一个,让读请求负载均衡。 +3. 每个分片在本地执行搜索请求,并将查询结果打分排序,然后将结果保存到 from + size 大小的有序队列中。 +4. 接着,每个分片将结果返回给 coordinate node,coordinate node 对数据进行汇总处理:合并、排序、分页,将汇总数据存到一个大小为 from + size 的全局有序队列。 + +需要注意的是,在协调节点转发搜索请求的时候,如果有 N 个 Shard 位于同一个节点时,并不会合并这些请求,而是发生 N 次请求! + +在 Fetch 阶段,协调节点会从 Query 阶段产生的全局排序列表中确定需要取回的文档 ID 列表,然后通过路由算法计算出各个文档对应的分片,并且用 multi get 的方式到对应的分片上获取文档数据。 + +**Fetch 阶段的流程**如下: + +1. coordinate node 确定需要获取哪些文档,然后向相关节点发起 multi get 请求; +2. 分片所在节点读取文档数据,并且进行 `_source` 字段过滤、处理高亮参数等,然后把处理后的文档数据返回给协调节点; +3. coordinate node 汇总所有数据后,返回给客户端。 + +::: + +### 【高级】ES 为什么会有深分页问题? + +:::details 要点 + +在 Elasticsearch 中,支持三种分页查询方式: + +- from + size - 可以使用 `from` 和 `size` 参数分别指定查询的起始页和每页记录数。 +- [`search_after`](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after) - 不支持指定页数,只能向下翻页;并且需要指定 sort,并保证值是唯一的。然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询。 +- [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#scroll-search-results) - 类似于 RDBMS 中的游标,只允许向下翻页。每次下一页查询后,使用返回结果的 scroll id 来作为下一次翻页的标记。scroll 查询会在搜索初始化阶段会生成快照,后续数据的变化无法及时体现在查询结果,因此更加适合一次性批量查询或非实时数据的分页查询。 + +前文中,我们已经了解了 ES 两阶段搜索流程(Query 和 Fetch)。从中不难发现,这种搜索方式在分页查询时会出现以下情况: + +- 每个 shard 要扫描 `from + size` 条数据; +- coordinate node 需要接收并处理 `(from + size) * primary_shard_num` 条数据。 + +**如果 from 或 size 很大,需要处理的数据量也会很大,代价很高,这就是深分页产生的原因**。为了避免深分页,ES 默认限制 `from + size` 不能超过 10000,可以通过 `index.max_result_window` 设置。 + +如何解决 Elasticsearch 深分页问题? + +ES 官方提供了另外两种分页查询方式 [`search_after`](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#search-after) + PIT 和 [scroll](https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html#scroll-search-results)(注意:官方已不再推荐) 来避免深分页问题。 + +::: + +### 【中级】ES 相关性计算和聚合计算为什么会有计算偏差? + +:::details 要点 + +在 ES 中,不仅仅是普通搜索,相关性计算(评分)和聚合计算也是先在每个 shard 的本地进行计算,再由 coordinate node 进行汇总。由于分片的本地计算是独立的,只能基于数据子集来进行计算,所以难免出现数据偏差。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412012144894.png) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412012145912.png) + +解决这个问题的方式也有多种: + +- 当数据量不大的情况下,**设置主分片数为 1**,这意味着在数据全集上进行聚合。 但这种方案不太现实。 +- **设置 [`shard_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-shard-size) 参数**,将计算数据范围变大,**牺牲整体性能,提高精准度**。shard_size 的默认值是 `size * 1.5 + 10`。 +- **使用 DFS Query Then Fetch**, 在 URL 参数中指定:`_search?search_type=dfs_query_then_fetch`。这样设定之后,ES 先会把每个分片的词频和文档频率的数据汇总到协调节点进行处理,然后再进行相关性算分。这样的话会消耗更多的 CPU 和内存资源,效率低下! +- 尽量保证数据均匀地分布在各个分片中。 + +::: + +### 【高级】ES 如何保证读写一致? + +:::details 要点 + +**乐观锁机制** - 可以通过版本号使用乐观并发控制,以确保新版本不会被旧版本覆盖,由应用层来处理具体的冲突; + +另外对于写操作,一致性级别支持 quorum/one/all,默认为 quorum,即只有当大多数分片可用时才允许写操作。但即使大多数可用,也可能存在因为网络等原因导致写入副本失败,这样该副本被认为故障,分片将会在一个不同的节点上重建。 + +对于读操作,可以设置 replication 为 sync(默认),这使得操作在主分片和副本分片都完成后才会返回;如果设置 replication 为 async 时,也可以通过设置搜索请求参数、\_preference 为 primary 来查询主分片,确保文档是最新版本。 + +::: + +### 【高级】ES 查询速度为什么快? + +:::details 要点 + +- **倒排索引** - Elasticsearch 查询速度快最核心的点在于使用倒排索引。 + - 在 Elasticsearch 中,为了提高查询效率,它对存储的文档进行了分词处理。分词是将连续的文本切分成一个个独立的词项的过程。对文本进行分词后,Elasticsearch 会为每个词项创建一个倒排索引。这样,当用户进行查询时,Elasticsearch 只需要在倒排索引中查找匹配的词项,从而快速地定位到相关的文档。 + - 正向索引的结构是每个文档和关键字做关联,每个文档都有与之对应的关键字,记录关键字在文档中出现的位置和次数;而倒排索引则是将文档中的词项和文档的 ID 进行关联,这样就可以通过词项快速找到包含它的文档。 +- **分片** - Elasticsearch 通过分片,支持分布式存储和搜索,可以实现搜索的并行处理和负载均衡。 + +> 参考:https://cloud.tencent.com/developer/article/1922613 + +::: + +## Elasticsearch 生产环境 + +### 【中级】ES 生产环境部署情况是怎样的? + +:::details 要点 + +**典型问题** + +- 你们的 Elasticsearch 生产环境部署情况是怎样的? +- 你们的 Elasticsearch 生产环境集群规模有多大? +- 你们的 Elasticsearch 生产环境中有多少索引,每个索引大概有多少个分片? + +**知识点** + +根据实际 Elasticsearch 集群情况描述,以下是一个案例: + +- 节点数:19 +- 机器配置:6 核,10G 内存,800G 磁盘 +- 索引数、分片数:1200+ 索引、1.7 万+ 分片 +- 容量:总文档数 150 亿+,总容量 15TB,使用容量 10TB+ +- 日增数据量:约 4 千万条数据,50 GB 增长容量 + +::: + +## Elasticsearch 优化 + +### 【中级】ES 使用有哪些基本规范? + +:::details 要点 + +- 索引数 + - 大索引需要拆分,增强性能,减少风险 + - index 可以按日期拆分为 index_yyyyMMdd,然后用 alias 映射 +- Mapping 设置 + - text 数据类型默认是关闭 fielddate + - 关闭 `_source` 会导致无法使用 reindex + - ES 字段数的最大限制是 1000,但是不建议超过 100 +- Refersh + - 写入时,尽量不要执行 refresh,在并发较大的情况下,ES 负载可能会被打满。 +- 索引别名 + - 尽量使用索引的别名,在类似于进行索引字段类型变更需要进行索引重建的时候会减少很多的问题。 + - 别名的下面可以挂载多个索引,若是索引拆分之后业务验证允许可以这么使用。 + - alias 下面可以挂多个索引,但是需要注意的是每次请求很容易写放大,比如说 alias 挂了 50 个索引,每个索引有 5 个分片,那么从集群的维度来看一共就是 50\*5=250 次 query 和 fetch,很容易导致读放大的情况。 + +::: + +### 【中级】ES JVM 设置需要注意什么? + +:::details 要点 + +ES 实际上是一个 Java 进程,因此也需要考虑 JVM 设置。关于 ES JVM 的设置,有以下几点建议: + +- 从 ES6 开始,支持 64 位的 JVM +- 将内存 `Xms` 和 `Xmx` 设置一样,需要注意过多的堆可能会使垃圾回收停顿时间过长 +- 一般,将 50% 的可用内存分配给 ES +- ES 内存不要超过 32 GB + +> 扩展:https://www.elastic.co/blog/a-heap-of-trouble + +::: + +### 【高级】ES 内存为什么不要超过 32 GB? + +:::details 要点 + +实际上,一般而言,**绝大部分 JVM 内存最好都不要超过 32 GB**,不仅仅是 ES 内存。 + +对于 32 位系统来说,JVM 的对象指针占用 32 位(4 byte),可以表示 2^32 哥内存地址。由于,CPU 寻址的最小单位是 byte,2^32 byte 即 4GB,也就是说 JVM 最大可以支持 4GB。 + +对于 64 位系统来说,如果直接引用,就需要使用 64 位的指针,相比 32 位 指针,多使用了一倍的内存。并且,指针在主内存和各级缓存间移动数据时,会占用更大的带宽。 + +Java 使用了一种叫做 [Compressed oops](https://wiki.openjdk.org/display/HotSpot/CompressedOops) 的技术来进行优化。该技术利用 Java 对象按照 8 字节对齐的机制,让 Java 对象指针指向一个映射地址偏移量(非真实 64 位 地址)。这种方式可以寻址最大位 32 GB 的内存空间。一旦超出 32 GB,就无法利用压缩指针技术,对象指针只能指向真实内存地址,这会造成空间的浪费。 + +> 扩展: +> +> https://wiki.openjdk.org/display/HotSpot/CompressedOops +> +> https://blog.csdn.net/liujianyangbj/article/details/108049482 + +::: + +### 【中级】ES 主机有哪些优化点? + +:::details 要点 + +- 关闭缓存 swap; +- 堆内存设置为:Min(节点内存/2, 32GB); +- 设置最大文件句柄数; +- 线程池+队列大小根据业务需要做调整; +- 磁盘存储 raid 方式——存储有条件使用 RAID10,增加单节点性能以及避免单节点存储故障。 + +::: + +### 【中级】ES 索引数据多,如何优化? + +:::details 要点 + +- **动态索引** - 如果单索引数据量过大,可以创建索引模板,并周期性创建新索引(举例来说,索引名为 blog_yyyyMMdd),实现数据的分解。 +- **冷热数据分离** - 将一定范围(如:一周、一月等)的数据作为热数据,其他数据作为冷数据。针对冷数据,可以考虑定期 force_merge + shrink 进行压缩,以节省存储空间和检索效率。 +- **分区再均衡** - Elasticsearch 集群可以动态根据节点数的变化,调整索引分片在集群上的分布。但需要注意的是,要提前合理规划好索引的分片数:分片数过少,则增加节点也无法水平扩展;分片数过多,影响 Elasticsearch 读写效率。 + +::: + +## 参考资料 + +- [Elasticsearch 官方文档](https://www.elastic.co/guide/en/elasticsearch/reference/current/index.html) +- [Elasticsearch 从入门到实践](https://www.itshujia.com/books/elasticsearch) +- https://www.turing.com/interview-questions/elasticsearch +- https://github.com/rkm-ravi94/awesome-devops-interview/blob/main/elasticsearch.md diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/README.md" index 5f7ee875c0..76e9ee3bb4 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/01.Elasticsearch/README.md" @@ -1,4 +1,5 @@ --- +icon: logos:elasticsearch title: Elasticsearch 教程 date: 2022-04-11 16:52:35 categories: @@ -9,7 +10,7 @@ tags: - 数据库 - 搜索引擎数据库 - Elasticsearch -permalink: /pages/74675e/ +permalink: /pages/f457d8c3/ hidden: true index: false --- @@ -20,35 +21,19 @@ index: false ## 📖 内容 -### [Elasticsearch 面试总结](01.Elasticsearch面试总结.md) 💯 - -### [Elasticsearch 快速入门](02.Elasticsearch快速入门.md) - -### [Elasticsearch 简介](03.Elasticsearch简介.md) - -### [Elasticsearch 索引管理](04.Elasticsearch索引.md) - -### [Elasticsearch 映射](05.Elasticsearch映射.md) - -### [Elasticsearch 查询](05.Elasticsearch查询.md) - -### [Elasticsearch 高亮](06.Elasticsearch高亮.md) - -### [Elasticsearch 排序](07.Elasticsearch排序.md) - -### [Elasticsearch 聚合](08.Elasticsearch聚合.md) - -### [Elasticsearch 分析器](09.Elasticsearch分析器.md) - -### [Elasticsearch 性能优化](10.Elasticsearch性能优化.md) - -### [Elasticsearch Rest API](11.ElasticsearchRestApi.md) - -### [ElasticSearch Java API 之 High Level REST Client](12.ElasticsearchHighLevelRestJavaApi.md) - -### [Elasticsearch 集群和分片](13.Elasticsearch集群和分片.md) - -### [Elasticsearch 运维](20.Elasticsearch运维.md) +- [Elasticsearch 简介](Elasticsearch_简介.md) +- [Elasticsearch 存储](Elasticsearch_存储.md) +- [Elasticsearch 搜索(上)](Elasticsearch_搜索上.md) +- [Elasticsearch 搜索(下)](Elasticsearch_搜索下.md) +- [Elasticsearch 聚合](Elasticsearch_聚合.md) +- [Elasticsearch 分析](Elasticsearch_分析.md) +- [Elasticsearch 集群](Elasticsearch_集群.md) +- [Elasticsearch 架构](Elasticsearch_架构.md) +- [Elasticsearch 优化](Elasticsearch_优化.md) +- [Elasticsearch 运维](Elasticsearch_运维.md) +- [Elasticsearch API](Elasticsearch_API.md) +- [ElasticSearch API 之 High Level REST Client](Elasticsearch_API_HighLevelRest.md) +- [Elasticsearch 面试](Elasticsearch_面试.md) 💯 ## 📚 资料 @@ -61,7 +46,9 @@ index: false - [《Elasticsearch 实战》](https://book.douban.com/subject/30380439/) - **教程** - [ELK Stack 权威指南](https://github.com/chenryn/logstash-best-practice-cn) + - [极客时间教程 - Elasticsearch 核心技术与实战](https://time.geekbang.org/course/detail/100030501-102659) - [Elasticsearch 教程](https://www.knowledgedict.com/tutorial/elasticsearch-intro.html) + - [Elasticsearch 从入门到实践](https://www.itshujia.com/books/elasticsearch) - **文章** - [Elasticsearch+Logstash+Kibana 教程](https://www.cnblogs.com/xing901022/p/4704319.html) - [ELK(Elasticsearch、Logstash、Kibana)安装和配置](https://github.com/judasn/Linux-Tutorial/blob/master/ELK-Install-And-Settings.md) @@ -75,4 +62,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/01.Elastic\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/01.Elastic\345\277\253\351\200\237\345\205\245\351\227\250.md" index d9626e6673..b3b0173da2 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/01.Elastic\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/01.Elastic\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -10,7 +10,7 @@ tags: - 数据库 - 搜索引擎数据库 - Elastic -permalink: /pages/553160/ +permalink: /pages/5ef200a5/ --- # Elastic 快速入门 @@ -47,9 +47,9 @@ ELK 是指 Elastic 公司旗下三款产品 [ElasticSearch](https://www.elastic. > > - [Beats](https://www.elastic.co/products/beats) 是单一用途的数据传输平台,它可以将多台机器的数据发送到 Logstash 或 ElasticSearch。但 Beats 并不是不可或缺的一环,所以本文中暂不介绍。 > - [Logstash](https://www.elastic.co/products/logstash) 是一个动态数据收集管道。支持以 TCP/UDP/HTTP 多种方式收集数据(也可以接受 Beats 传输来的数据),并对数据做进一步丰富或提取字段处理。 -> - [ElasticSearch](https://www.elastic.co/products/elasticsearch) 是一个基于 JSON 的分布式的搜索和分析引擎。作为 ELK 的核心,它集中存储数据。 +> - [ElasticSearch](https://www.elastic.co/elasticsearch) 是一个基于 JSON 的分布式的搜索和分析引擎。作为 ELK 的核心,它集中存储数据。 > -> - [Kibana](https://www.elastic.co/products/kibana) 是 ELK 的用户界面。它将收集的数据进行可视化展示(各种报表、图形化数据),并提供配置、管理 ELK 的界面。 +> - [Kibana](https://www.elastic.co/kibana) 是 ELK 的用户界面。它将收集的数据进行可视化展示(各种报表、图形化数据),并提供配置、管理 ELK 的界面。 ## 2. ElasticSearch @@ -265,7 +265,6 @@ Filebeat 将每个事件的传递状态存储在注册表文件中。所以它 ## 5. 运维 -- [ElasticSearch 运维](../01.Elasticsearch/20.Elasticsearch运维.md) - [Logstash 运维](07.Logstash运维.md) - [Kibana 运维](05.Kibana运维.md) - [Beats 运维](03.Filebeat运维.md) @@ -288,4 +287,4 @@ Filebeat 将每个事件的传递状态存储在注册表文件中。所以它 - **文章** - [什么是 ELK Stack?](https://www.elastic.co/cn/what-is/elk-stack) - [https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/es-introduction.md](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/es-introduction.md) - - [es-write-query-search](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/es-write-query-search.md) \ No newline at end of file + - [es-write-query-search](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/es-write-query-search.md) diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/02.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Filebeat.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/02.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Filebeat.md" index fccaac2c8f..5095c1317f 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/02.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Filebeat.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/02.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Filebeat.md" @@ -11,7 +11,7 @@ tags: - 搜索引擎数据库 - Elastic - Filebeat -permalink: /pages/b7f079/ +permalink: /pages/e2aac1bb/ --- # Elastic 技术栈之 Filebeat diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/03.Filebeat\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/03.Filebeat\350\277\220\347\273\264.md" index 8ee54577e0..dfffb7e515 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/03.Filebeat\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/03.Filebeat\350\277\220\347\273\264.md" @@ -11,7 +11,7 @@ tags: - 搜索引擎数据库 - Elastic - Filebeat -permalink: /pages/7c067f/ +permalink: /pages/004c4aeb/ --- # Filebeat 运维 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/04.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Kibana.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/04.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Kibana.md" index b52a9c05e9..3c58dbb8ce 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/04.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Kibana.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/04.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Kibana.md" @@ -11,7 +11,7 @@ tags: - 搜索引擎数据库 - Elastic - Kibana -permalink: /pages/002159/ +permalink: /pages/aaf079ab/ --- # Elastic 技术栈之 Kibana diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/05.Kibana\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/05.Kibana\350\277\220\347\273\264.md" index 7a159cd138..288cabec57 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/05.Kibana\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/05.Kibana\350\277\220\347\273\264.md" @@ -11,7 +11,7 @@ tags: - 搜索引擎数据库 - Elastic - Kibana -permalink: /pages/fc47af/ +permalink: /pages/b5286ca5/ --- # Kibana 运维 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/06.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Logstash.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/06.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Logstash.md" index a7cc019499..6745cee8f2 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/06.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Logstash.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/06.Elastic\346\212\200\346\234\257\346\240\210\344\271\213Logstash.md" @@ -11,7 +11,7 @@ tags: - 搜索引擎数据库 - Elastic - Logstash -permalink: /pages/55ce99/ +permalink: /pages/c2b201b9/ --- # Elastic 技术栈之 Logstash diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/07.Logstash\350\277\220\347\273\264.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/07.Logstash\350\277\220\347\273\264.md" index 82733fe380..479d0e0e6d 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/07.Logstash\350\277\220\347\273\264.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/07.Logstash\350\277\220\347\273\264.md" @@ -11,7 +11,7 @@ tags: - 搜索引擎数据库 - Elastic - Logstash -permalink: /pages/92df30/ +permalink: /pages/a193983b/ --- # Logstash 运维 diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/README.md" index 9653203411..8b0b62ffbe 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/02.Elastic/README.md" @@ -9,7 +9,7 @@ tags: - 数据库 - 搜索引擎数据库 - Elastic -permalink: /pages/7bf7f7/ +permalink: /pages/a263510e/ hidden: true index: false --- @@ -18,11 +18,11 @@ index: false > **Elastic 技术栈通常被用来作为日志采集、检索、可视化的解决方案。** > -> ELK 是 elastic 公司旗下三款产品 [Elasticsearch](https://www.elastic.co/products/elasticsearch) 、[Logstash](https://www.elastic.co/products/logstash) 、[Kibana](https://www.elastic.co/products/kibana) 的首字母组合。 +> ELK 是 elastic 公司旗下三款产品 [Elasticsearch](https://www.elastic.co/elasticsearch) 、[Logstash](https://www.elastic.co/products/logstash) 、[Kibana](https://www.elastic.co/kibana) 的首字母组合。 > > [Logstash](https://www.elastic.co/products/logstash) 传输和处理你的日志、事务或其他数据。 > -> [Kibana](https://www.elastic.co/products/kibana) 将 Elasticsearch 的数据分析并渲染为可视化的报表。 +> [Kibana](https://www.elastic.co/kibana) 将 Elasticsearch 的数据分析并渲染为可视化的报表。 > > Elastic 技术栈,在 ELK 的基础上扩展了一些新的产品,如:[Beats](https://www.elastic.co/products/beats) 、[X-Pack](https://www.elastic.co/products/x-pack) 。 @@ -56,4 +56,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/README.md" index 4ba20d682b..2908562dc9 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/07.\346\220\234\347\264\242\345\274\225\346\223\216\346\225\260\346\215\256\345\272\223/README.md" @@ -7,7 +7,7 @@ categories: tags: - 数据库 - 搜索引擎数据库 -permalink: /pages/82c9ce/ +permalink: /pages/6436d6bf/ hidden: true index: false --- @@ -20,49 +20,29 @@ index: false > Elasticsearch 是一个基于 Lucene 的搜索和数据分析工具,它提供了一个分布式服务。Elasticsearch 是遵从 Apache 开源条款的一款开源产品,是当前主流的企业级搜索引擎。 -#### [Elasticsearch 面试总结](01.Elasticsearch/01.Elasticsearch面试总结.md) - -#### [Elasticsearch 快速入门](01.Elasticsearch/02.Elasticsearch快速入门.md) - -#### [Elasticsearch 简介](01.Elasticsearch/03.Elasticsearch简介.md) - -#### [Elasticsearch 索引](01.Elasticsearch/04.Elasticsearch索引.md) - -#### [Elasticsearch 查询](01.Elasticsearch/05.Elasticsearch查询.md) - -#### [Elasticsearch 高亮](01.Elasticsearch/06.Elasticsearch高亮.md) - -#### [Elasticsearch 排序](01.Elasticsearch/07.Elasticsearch排序.md) - -#### [Elasticsearch 聚合](01.Elasticsearch/08.Elasticsearch聚合.md) - -#### [Elasticsearch 分析器](01.Elasticsearch/09.Elasticsearch分析器.md) - -#### [Elasticsearch 性能优化](01.Elasticsearch/10.Elasticsearch性能优化.md) - -#### [Elasticsearch Rest API](01.Elasticsearch/11.ElasticsearchRestApi.md) - -#### [ElasticSearch Java API 之 High Level REST Client](01.Elasticsearch/12.ElasticsearchHighLevelRestJavaApi.md) - -#### [Elasticsearch 集群和分片](01.Elasticsearch/13.Elasticsearch集群和分片.md) - -#### [Elasticsearch 运维](01.Elasticsearch/20.Elasticsearch运维.md) +- [Elasticsearch 简介](01.Elasticsearch/Elasticsearch_简介.md) +- [Elasticsearch 存储](01.Elasticsearch/Elasticsearch_存储.md) +- [Elasticsearch 搜索(上)](01.Elasticsearch/Elasticsearch_搜索上.md) +- [Elasticsearch 搜索(下)](01.Elasticsearch/Elasticsearch_搜索下.md) +- [Elasticsearch 聚合](01.Elasticsearch/Elasticsearch_聚合.md) +- [Elasticsearch 分析](01.Elasticsearch/Elasticsearch_分析.md) +- [Elasticsearch DSL](01.Elasticsearch/Elasticsearch_DSL.md) +- [Elasticsearch 集群](01.Elasticsearch/Elasticsearch_集群.md) +- [Elasticsearch 优化](01.Elasticsearch/Elasticsearch_优化.md) +- [Elasticsearch 运维](01.Elasticsearch/Elasticsearch_运维.md) +- [Elasticsearch API](01.Elasticsearch/Elasticsearch_API.md) +- [ElasticSearch API 之 High Level REST Client](01.Elasticsearch/Elasticsearch_API_HighLevelRest.md) +- [Elasticsearch 面试](01.Elasticsearch/Elasticsearch_面试.md) 💯 ### Elastic -#### [Elastic 快速入门](02.Elastic/01.Elastic快速入门.md) - -#### [Elastic 技术栈之 Filebeat](02.Elastic/02.Elastic技术栈之Filebeat.md) - -#### [Filebeat 运维](02.Elastic/03.Filebeat运维.md) - -#### [Elastic 技术栈之 Kibana](02.Elastic/04.Elastic技术栈之Kibana.md) - -#### [Kibana 运维](02.Elastic/05.Kibana运维.md) - -#### [Elastic 技术栈之 Logstash](02.Elastic/06.Elastic技术栈之Logstash.md) - -#### [Logstash 运维](02.Elastic/07.Logstash运维.md) +- [Elastic 快速入门](02.Elastic/01.Elastic快速入门.md) +- [Elastic 技术栈之 Filebeat](02.Elastic/02.Elastic技术栈之Filebeat.md) +- [Filebeat 运维](02.Elastic/03.Filebeat运维.md) +- [Elastic 技术栈之 Kibana](02.Elastic/04.Elastic技术栈之Kibana.md) +- [Kibana 运维](02.Elastic/05.Kibana运维.md) +- [Elastic 技术栈之 Logstash](02.Elastic/06.Elastic技术栈之Logstash.md) +- [Logstash 运维](02.Elastic/07.Logstash运维.md) ## 📚 资料 @@ -109,4 +89,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/README.md" "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/README.md" index 0d773abe68..130ae414e6 100644 --- "a/source/_posts/12.\346\225\260\346\215\256\345\272\223/README.md" +++ "b/source/_posts/12.\346\225\260\346\215\256\345\272\223/README.md" @@ -5,7 +5,7 @@ categories: - 数据库 tags: - 数据库 -permalink: /pages/48b310/ +permalink: /pages/23fe4586/ hidden: true index: false --- @@ -53,11 +53,11 @@ index: false - [`关系型数据库综合知识`](03.关系型数据库/01.综合) - [`扩展 SQL`](03.关系型数据库/01.综合/03.扩展SQL.md)、[`SQL 语法速成`](03.关系型数据库/01.综合/02.SQL语法.md)、[`SQL Cheat Sheet`](03.关系型数据库/01.综合/99.SqlCheatSheet.md) + [关系数据库简介](03.关系型数据库/01.综合/关系数据库简介.md)、[SQL 语法](03.关系型数据库/01.综合/SQL语法.md) - [Mysql 教程](03.关系型数据库/02.Mysql) - [Mysql 架构](03.关系型数据库/02.Mysql/01.Mysql架构.md)、[Mysql 存储引擎](03.关系型数据库/02.Mysql/02.Mysql存储引擎.md)、[Mysql 索引](03.关系型数据库/02.Mysql/03.Mysql索引.md)、[Mysql 事务](03.关系型数据库/02.Mysql/04.Mysql事务.md)、[Mysql 锁](03.关系型数据库/02.Mysql/05.Mysql锁.md)、[Mysql 高可用](03.关系型数据库/02.Mysql/06.Mysql高可用.md)、[Mysql 优化](03.关系型数据库/02.Mysql/07.Mysql优化.md)、[Mysql 运维](03.关系型数据库/02.Mysql/20.Mysql运维.md)、[Mysql 面试](03.关系型数据库/02.Mysql/99.Mysql面试.md) + [Mysql 架构](03.关系型数据库/02.Mysql/Mysql_架构)、[Mysql 存储引擎](03.关系型数据库/02.Mysql/Mysql_存储引擎)、[Mysql 索引](03.关系型数据库/02.Mysql/Mysql_索引)、[Mysql 事务](03.关系型数据库/02.Mysql/Mysql_事务)、[Mysql 锁](03.关系型数据库/02.Mysql/Mysql_锁)、[Mysql 高可用](03.关系型数据库/02.Mysql/Mysql_高可用)、[Mysql 优化](03.关系型数据库/02.Mysql/Mysql_优化)、[Mysql 运维](03.关系型数据库/02.Mysql/Mysql_运维)、[Mysql 面试](03.关系型数据库/02.Mysql/Mysql_面试) ### [列式数据库](06.列式数据库) @@ -81,27 +81,66 @@ index: false ### [搜索引擎数据库](07.搜索引擎数据库) -- [`Elastic 技术栈`](07.搜索引擎数据库/02.Elastic) - - [`Elastic 技术栈之 Filebeat`](07.搜索引擎数据库/02.Elastic/02.Elastic技术栈之Filebeat.md)、[`Elastic 技术栈之 Kibana`](07.搜索引擎数据库/02.Elastic/04.Elastic技术栈之Kibana.md)、[`Elastic 技术栈之 Logstash`](07.搜索引擎数据库/02.Elastic/06.Elastic技术栈之Logstash.md)、[`Elastic 快速入门`](07.搜索引擎数据库/02.Elastic/01.Elastic快速入门.md)、[`Filebeat 运维`](07.搜索引擎数据库/02.Elastic/03.Filebeat运维.md)、[`Kibana 运维`](07.搜索引擎数据库/02.Elastic/05.Kibana运维.md)、[`Logstash 运维`](07.搜索引擎数据库/02.Elastic/07.Logstash运维.md) - -- [`Elasticsearch 教程`](07.搜索引擎数据库/01.Elasticsearch) - - [`Elasticsearch 查询`](07.搜索引擎数据库/01.Elasticsearch/05.Elasticsearch查询.md)、[`Elasticsearch 分析器`](07.搜索引擎数据库/01.Elasticsearch/09.Elasticsearch分析器.md)、[`Elasticsearch 高亮搜索及显示`](07.搜索引擎数据库/01.Elasticsearch/06.Elasticsearch高亮.md)、[`Elasticsearch 集群和分片`](07.搜索引擎数据库/01.Elasticsearch/13.Elasticsearch集群和分片.md)、[`Elasticsearch 简介`](07.搜索引擎数据库/01.Elasticsearch/03.Elasticsearch简介.md)、[`Elasticsearch 聚合`](07.搜索引擎数据库/01.Elasticsearch/08.Elasticsearch聚合.md)、[`Elasticsearch 快速入门`](07.搜索引擎数据库/01.Elasticsearch/02.Elasticsearch快速入门.md)、[`Elasticsearch 面试总结`](07.搜索引擎数据库/01.Elasticsearch/01.Elasticsearch面试总结.md)、[`Elasticsearch 排序`](07.搜索引擎数据库/01.Elasticsearch/07.Elasticsearch排序.md)、[`Elasticsearch 索引`](07.搜索引擎数据库/01.Elasticsearch/04.Elasticsearch索引.md)、[`Elasticsearch 性能优化`](07.搜索引擎数据库/01.Elasticsearch/10.Elasticsearch性能优化.md)、[`Elasticsearch 映射`](07.搜索引擎数据库/01.Elasticsearch/05.Elasticsearch映射.md)、[`Elasticsearch 运维`](07.搜索引擎数据库/01.Elasticsearch/20.Elasticsearch运维.md)、[`ElasticSearch Java API 之 High Level REST Client`](07.搜索引擎数据库/01.Elasticsearch/12.ElasticsearchHighLevelRestJavaApi.md)、[`Elasticsearch Rest API`](07.搜索引擎数据库/01.Elasticsearch/11.ElasticsearchRestApi.md) +#### Elasticsearch + +- [Elasticsearch 简介](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_简介.md) +- [Elasticsearch 存储](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_存储.md) +- [Elasticsearch 搜索(上)](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_搜索上.md) +- [Elasticsearch 搜索(下)](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_搜索下.md) +- [Elasticsearch 聚合](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_聚合.md) +- [Elasticsearch 分析](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_分析.md) +- [Elasticsearch DSL](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_DSL.md) +- [Elasticsearch 集群](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_集群.md) +- [Elasticsearch 优化](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_优化.md) +- [Elasticsearch 运维](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_运维.md) +- [Elasticsearch API](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_API.md) +- [ElasticSearch API 之 High Level REST Client](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_API_HighLevelRest.md) +- [Elasticsearch 面试](07.搜索引擎数据库/01.Elasticsearch/Elasticsearch_面试.md) 💯 + +#### Elastic + +- [Elastic 快速入门](07.搜索引擎数据库/02.Elastic/01.Elastic快速入门.md) +- [Elastic 技术栈之 Filebeat](07.搜索引擎数据库/02.Elastic/02.Elastic技术栈之Filebeat.md) +- [Filebeat 运维](07.搜索引擎数据库/02.Elastic/03.Filebeat运维.md) +- [Elastic 技术栈之 Kibana](07.搜索引擎数据库/02.Elastic/04.Elastic技术栈之Kibana.md) +- [Kibana 运维](07.搜索引擎数据库/02.Elastic/05.Kibana运维.md) +- [Elastic 技术栈之 Logstash](07.搜索引擎数据库/02.Elastic/06.Elastic技术栈之Logstash.md) +- [Logstash 运维](07.搜索引擎数据库/02.Elastic/07.Logstash运维.md) ### [文档数据库](04.文档数据库) -- [`MongoDB 教程`](04.文档数据库/01.MongoDB) - - [`MongoDB 的 CRUD 操作`](04.文档数据库/01.MongoDB/02.MongoDB的CRUD操作.md)、[`MongoDB 的聚合操作`](04.文档数据库/01.MongoDB/03.MongoDB的聚合操作.md)、[`MongoDB 分片`](04.文档数据库/01.MongoDB/09.MongoDB分片.md)、[`MongoDB 复制`](04.文档数据库/01.MongoDB/08.MongoDB复制.md)、[`MongoDB 建模`](04.文档数据库/01.MongoDB/05.MongoDB建模.md)、[`MongoDB 建模示例`](04.文档数据库/01.MongoDB/06.MongoDB建模示例.md)、[`MongoDB 事务`](04.文档数据库/01.MongoDB/04.MongoDB事务.md)、[`MongoDB 索引`](04.文档数据库/01.MongoDB/07.MongoDB索引.md)、[`MongoDB 应用指南`](04.文档数据库/01.MongoDB/01.MongoDB应用指南.md)、[`MongoDB 运维`](04.文档数据库/01.MongoDB/20.MongoDB运维.md) +- [MongoDB 简介](04.文档数据库/01.MongoDB/MongoDB_简介.md) +- [MongoDB CRUD](04.文档数据库/01.MongoDB/MongoDB_CRUD.md) +- [MongoDB 聚合](04.文档数据库/01.MongoDB/MongoDB_聚合.md) +- [MongoDB 事务](04.文档数据库/01.MongoDB/MongoDB_事务.md) +- [MongoDB 建模](04.文档数据库/01.MongoDB/MongoDB_建模.md) +- [MongoDB 索引](04.文档数据库/01.MongoDB/MongoDB_索引.md) +- [MongoDB 复制](04.文档数据库/01.MongoDB/MongoDB_复制.md) +- [MongoDB 分片](04.文档数据库/01.MongoDB/MongoDB_分片.md) +- [MongoDB 运维](04.文档数据库/01.MongoDB/MongoDB_运维.md) ### [KV 数据库](05.KV数据库) -- [**Redis 教程**](05.KV数据库/01.Redis) - - [Redis 基本数据类型](05.KV数据库/01.Redis/01.Redis基本数据类型.md)、[Redis 高级数据类型](05.KV数据库/01.Redis/02.Redis高级数据类型.md)、[Redis 数据结构](05.KV数据库/01.Redis/03.Redis数据结构.md)、[Redis 过期删除和内存淘汰](05.KV数据库/01.Redis/11.Redis过期删除和内存淘汰.md)、[Redis 持久化](05.KV数据库/01.Redis/12.Redis持久化.md)、[Redis 事件](05.KV数据库/01.Redis/13.Redis事件.md)、[Redis 复制](05.KV数据库/01.Redis/21.Redis复制.md)、[Redis 哨兵](05.KV数据库/01.Redis/22.Redis哨兵.md)、[Redis 集群](05.KV数据库/01.Redis/23.Redis集群.md)、[Redis 发布订阅](05.KV数据库/01.Redis/31.Redis发布订阅.md)、[Redis 独立功能](05.KV数据库/01.Redis/32.Redis事务.md)、[Redis 管道](05.KV数据库/01.Redis/33.Redis管道.md)、[Redis 脚本](05.KV数据库/01.Redis/34.Redis脚本.md)、[Redis 运维](05.KV数据库/01.Redis/41.Redis运维.md)、[Redis 实战](05.KV数据库/01.Redis/42.Redis实战.md)、[Redis 面试](05.KV数据库/01.Redis/99.Redis面试.md) - -- [Memcached 快速入门](05.KV数据库/02.Memcached.md) +#### [Redis](05.KV数据库/01.Redis) + +- [Redis 基本数据类型](05.KV数据库/01.Redis/Redis_数据类型.md) - 关键词:`String`、`Hash`、`List`、`Set`、`Zset` +- [Redis 高级数据类型](05.KV数据库/01.Redis/Redis_数据类型二.md) - 关键词:`BitMap`、`HyperLogLog`、`Geo`、`Stream` +- [Redis 数据结构](05.KV数据库/01.Redis/Redis_数据结构.md) - 关键词:`对象`、`SDS`、`链表`、`字典`、`跳表`、`整数集合`、`压缩列表` +- [Redis 内存管理](05.KV数据库/01.Redis/Redis_内存管理.md) - 关键词:`定时删除`、`惰性删除`、`定期删除`、`LRU`、`LFU` +- [Redis 持久化](05.KV数据库/01.Redis/Redis_持久化.md) - 关键词:`RDB`、`AOF`、`SAVE`、`BGSAVE`、`appendfsync` +- [Redis 事件](05.KV数据库/01.Redis/Redis_事件.md) - 关键词:`文件事件`、`时间事件` +- [Redis 复制](05.KV数据库/01.Redis/Redis_复制.md) - 关键词:`SLAVEOF`、`SYNC`、`PSYNC`、`命令传播`、`心跳` +- [Redis 哨兵](05.KV数据库/01.Redis/Redis_哨兵.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`Raft` +- [Redis 集群](05.KV数据库/01.Redis/Redis_集群.md) - 关键词:`高可用`、`监控`、`选主`、`故障转移`、`分区`、`Raft`、`Gossip` +- [Redis 订阅](05.KV数据库/01.Redis/Redis_订阅.md) - 关键词:`订阅`、`SUBSCRIBE`、`PSUBSCRIBE`、`PUBLISH`、`观察者模式` +- [Redis 独立功能](05.KV数据库/01.Redis/Redis_事务.md) - 关键词:`事务`、`ACID`、`MULTI`、`EXEC`、`DISCARD`、`WATCH` +- [Redis 管道](05.KV数据库/01.Redis/Redis_管道.md) - 关键词:`Pipeline` +- [Redis 脚本](05.KV数据库/01.Redis/Redis_脚本.md) - 关键词:`Lua` +- [Redis 运维](05.KV数据库/01.Redis/Redis_运维.md) - 关键词:`安装`、`配置`、`命令`、`集群`、`客户端` +- [Redis 实战](05.KV数据库/01.Redis/Redis_实战.md) - 关键词:`缓存`、`分布式锁`、`布隆过滤器` +- [Redis 面试](05.KV数据库/01.Redis/Redis_面试.md) - 关键词:`面试` + +#### [Redis](05.KV数据库/02.Memcached.md) ## 资料 📚 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225.md" index 854be55fd9..d6f1029c57 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/01.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\351\235\242\350\257\225.md" @@ -8,7 +8,7 @@ categories: tags: - 网络 - 面试 -permalink: /pages/e936ba/ +permalink: /pages/ed55a426/ --- # 计算机网络面试总结 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/02.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\346\214\207\345\215\227.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/02.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\346\214\207\345\215\227.md" index 947790d313..2e52047082 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/02.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\346\214\207\345\215\227.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/02.\350\256\241\347\256\227\346\234\272\347\275\221\347\273\234\346\214\207\345\215\227.md" @@ -7,7 +7,7 @@ categories: - 网络综合 tags: - 网络 -permalink: /pages/847c99/ +permalink: /pages/79c009ec/ --- # 计算机网络指南 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/11.\347\211\251\347\220\206\345\261\202.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/11.\347\211\251\347\220\206\345\261\202.md" index afb2035784..f80fd682e1 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/11.\347\211\251\347\220\206\345\261\202.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/11.\347\211\251\347\220\206\345\261\202.md" @@ -8,7 +8,7 @@ categories: tags: - 网络 - 网络分层 -permalink: /pages/e05ae2/ +permalink: /pages/3461bf57/ --- # 计算机网络之物理层 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/12.\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/12.\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" index 0978ba7a9b..0b99dbbb85 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/12.\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/12.\346\225\260\346\215\256\351\223\276\350\267\257\345\261\202.md" @@ -8,7 +8,7 @@ categories: tags: - 网络 - 网络分层 -permalink: /pages/390718/ +permalink: /pages/ba212929/ --- # 计算机网络之数据链路层 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/13.\347\275\221\347\273\234\345\261\202.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/13.\347\275\221\347\273\234\345\261\202.md" index 22f8076e48..d4523a3126 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/13.\347\275\221\347\273\234\345\261\202.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/13.\347\275\221\347\273\234\345\261\202.md" @@ -8,7 +8,7 @@ categories: tags: - 网络 - 网络分层 -permalink: /pages/42c7a1/ +permalink: /pages/414cba41/ --- # 计算机网络之网络层 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/14.\344\274\240\350\276\223\345\261\202.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/14.\344\274\240\350\276\223\345\261\202.md" index ff4ef6c0ab..6cfb5d8819 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/14.\344\274\240\350\276\223\345\261\202.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/14.\344\274\240\350\276\223\345\261\202.md" @@ -8,7 +8,7 @@ categories: tags: - 网络 - 网络分层 -permalink: /pages/1d6f56/ +permalink: /pages/cf81414e/ --- # 计算机网络之传输层 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/15.\345\272\224\347\224\250\345\261\202.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/15.\345\272\224\347\224\250\345\261\202.md" index 60aa22f3ab..d90d663766 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/15.\345\272\224\347\224\250\345\261\202.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/15.\345\272\224\347\224\250\345\261\202.md" @@ -8,7 +8,7 @@ categories: tags: - 网络 - 网络分层 -permalink: /pages/267818/ +permalink: /pages/94b734fc/ --- # 计算机网络之应用层 diff --git "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/README.md" "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/README.md" index 55f09d58a8..30063a4f87 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/README.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/01.\347\275\221\347\273\234\347\273\274\345\220\210/README.md" @@ -6,7 +6,7 @@ categories: - 网络综合 tags: - 网络 -permalink: /pages/f76ad1/ +permalink: /pages/e8e40dc6/ hidden: true index: false --- diff --git "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/01.HTTP.md" "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/01.HTTP.md" index 4021557a3b..a08c892079 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/01.HTTP.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/01.HTTP.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络协议 - HTTP -permalink: /pages/d58ebc/ +permalink: /pages/3ce07194/ --- # 超文本传输协议 HTTP diff --git "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/02.DNS.md" "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/02.DNS.md" index e2c087d007..0c5b4961d5 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/02.DNS.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/02.DNS.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络协议 - DNS -permalink: /pages/af0e09/ +permalink: /pages/8dae5f42/ --- # 域名解析协议 DNS diff --git "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/03.TCP.md" "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/03.TCP.md" index 79c373c901..c368a23390 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/03.TCP.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/03.TCP.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络协议 - TCP -permalink: /pages/5dec61/ +permalink: /pages/84d17d55/ --- # 传输控制协议 TCP diff --git "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/04.UDP.md" "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/04.UDP.md" index 2402242e1d..30dda3f905 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/04.UDP.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/04.UDP.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络协议 - UDP -permalink: /pages/4eee26/ +permalink: /pages/f372c2b8/ --- # 用户数据报协议 UDP diff --git "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/05.ICMP.md" "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/05.ICMP.md" index 0baa446bb9..d1791c2ccf 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/05.ICMP.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/05.ICMP.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络协议 - ICMP -permalink: /pages/8004e9/ +permalink: /pages/3a73788e/ --- # 网络协议之 ICMP diff --git "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/README.md" "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/README.md" index 75ea12c821..90a930a850 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/README.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/02.\347\275\221\347\273\234\345\215\217\350\256\256/README.md" @@ -7,7 +7,7 @@ categories: tags: - 网络 - 网络协议 -permalink: /pages/b2bc79/ +permalink: /pages/a86f8245/ hidden: true index: false --- diff --git "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/01.WebSocket.md" "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/01.WebSocket.md" index 6cef68aea4..bc8307894b 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/01.WebSocket.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/01.WebSocket.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络技术 - WebSocket -permalink: /pages/b920dc/ +permalink: /pages/c129e3e6/ --- # 网络技术之 WebSocket diff --git "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/02.CDN.md" "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/02.CDN.md" index f90e078e02..3883b06b3b 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/02.CDN.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/02.CDN.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络技术 - CDN -permalink: /pages/a6febf/ +permalink: /pages/ea758828/ --- # 网络技术之 CDN diff --git "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/03.VPN.md" "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/03.VPN.md" index b2b19c92b8..35fc907208 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/03.VPN.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/03.VPN.md" @@ -9,7 +9,7 @@ tags: - 网络 - 网络技术 - VPN -permalink: /pages/9e7fab/ +permalink: /pages/6a49609d/ --- # 网络技术之 VPN diff --git "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/README.md" "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/README.md" index 533fc7a972..f4e80a2ae8 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/README.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/03.\347\275\221\347\273\234\346\212\200\346\234\257/README.md" @@ -6,7 +6,7 @@ categories: - 网络技术 tags: - 网络 -permalink: /pages/75570a/ +permalink: /pages/12e30b8d/ hidden: true index: false --- diff --git "a/source/_posts/13.\347\275\221\347\273\234/README.md" "b/source/_posts/13.\347\275\221\347\273\234/README.md" index c5b20249b2..039fce7339 100644 --- "a/source/_posts/13.\347\275\221\347\273\234/README.md" +++ "b/source/_posts/13.\347\275\221\347\273\234/README.md" @@ -5,7 +5,7 @@ categories: - 网络 tags: - 网络 -permalink: /pages/b39653/ +permalink: /pages/bb9ba184/ hidden: true index: false --- diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/01.Windows\345\256\236\347\224\250\346\212\200\345\267\247.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/01.Windows\345\256\236\347\224\250\346\212\200\345\267\247.md" index 5caa693dd4..fb4d4a19b3 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/01.Windows\345\256\236\347\224\250\346\212\200\345\267\247.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/01.Windows\345\256\236\347\224\250\346\212\200\345\267\247.md" @@ -8,7 +8,7 @@ categories: tags: - 操作系统 - Windows -permalink: /pages/6f05e2/ +permalink: /pages/8aebce55/ --- # Windows 常用技巧总结 diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/02.Mac\345\256\236\347\224\250\346\212\200\345\267\247.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/02.Mac\345\256\236\347\224\250\346\212\200\345\267\247.md" index 646bf5ddc3..0275c56c6a 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/02.Mac\345\256\236\347\224\250\346\212\200\345\267\247.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/01.\346\223\215\344\275\234\347\263\273\347\273\237\345\272\224\347\224\250/02.Mac\345\256\236\347\224\250\346\212\200\345\267\247.md" @@ -8,7 +8,7 @@ categories: tags: - 操作系统 - Mac -permalink: /pages/1b79b0/ +permalink: /pages/7fbef188/ --- ## 基本操作 diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/01.\345\221\275\344\273\244/01.Linux\345\221\275\344\273\244CheatSheet.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/01.\345\221\275\344\273\244/01.Linux\345\221\275\344\273\244CheatSheet.md" index 7ddb3c2e9b..9e78929829 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/01.\345\221\275\344\273\244/01.Linux\345\221\275\344\273\244CheatSheet.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/01.\345\221\275\344\273\244/01.Linux\345\221\275\344\273\244CheatSheet.md" @@ -10,7 +10,7 @@ tags: - 操作系统 - Linux - 命令 -permalink: /pages/af6d52/ +permalink: /pages/ea73272b/ --- # Linux 命令 Cheat Sheet @@ -600,4 +600,4 @@ mkdir empty && rsync -r --delete empty/ some-dir && rmdir some-dir ![img](http://creativecommons.org/licenses/by-sa/4.0/) -本文使用授权协议 [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)。 +本文使用授权协议 [Creative Commons Attribution-ShareAlike 4.0 International License](http://creativecommons.org/licenses/by-sa/4.0/)。 \ No newline at end of file diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/01.network-ops.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/01.network-ops.md" index 96483a4565..e7702faa7d 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/01.network-ops.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/01.network-ops.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - 网络 -permalink: /pages/f7e766/ +permalink: /pages/589b50af/ --- # Linux 典型运维应用 diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/02.samba.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/02.samba.md" index 7aa25ed0c6..7e0ef193f6 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/02.samba.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/02.samba.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - Samba -permalink: /pages/77993f/ +permalink: /pages/9bc4d575/ --- # Samba 应用 diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/03.ntp.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/03.ntp.md" index 92a0bca54e..c0ccf8d468 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/03.ntp.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/03.ntp.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - NTP -permalink: /pages/f50fdb/ +permalink: /pages/7607f70f/ --- # 时间服务器 - NTP diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/04.firewalld.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/04.firewalld.md" index 3cb849c875..4539df5d81 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/04.firewalld.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/04.firewalld.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - 防火墙 -permalink: /pages/0cdbda/ +permalink: /pages/17292e63/ --- # 防火墙 - Firewalld diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/05.iptables.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/05.iptables.md" index a24a38767c..203d054b1e 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/05.iptables.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/05.iptables.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - 防火墙 -permalink: /pages/eb9176/ +permalink: /pages/a2bb9482/ --- # Iptables 应用 diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/06.crontab.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/06.crontab.md" index e9fb04e984..9c6a955dd1 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/06.crontab.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/06.crontab.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - cron -permalink: /pages/a6ec53/ +permalink: /pages/0c6af703/ --- # 定时任务 - crontab diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/07.systemd.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/07.systemd.md" index 80acb3eb66..65e1777b74 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/07.systemd.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/07.systemd.md" @@ -10,7 +10,7 @@ tags: - 操作系统 - Linux - 工具 -permalink: /pages/06c8d6/ +permalink: /pages/36fc34f3/ --- # Systemd 应用 diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/08.vim.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/08.vim.md" index 1040ac93ee..2eb68c1f81 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/08.vim.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/08.vim.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - Vim -permalink: /pages/50ab65/ +permalink: /pages/c166f21d/ --- # Vim 应用 @@ -286,7 +286,7 @@ Vim 是从 vi 发展出来的一个文本编辑器。代码补完、编译及错 #### 可视化选择: `v`,`V`,`` -前面,我们看到了 ``的示例 (在 Windows 下应该是),我们可以使用 `v` 和 `V`。一但被选好了,你可以做下面的事: +前面,我们看到了 ``的示例 (在 Windows 下应该是 ``),我们可以使用 `v` 和 `V`。一但被选好了,你可以做下面的事: - `J` → 把所有的行连接起来(变成一行) - `<` 或 `>` → 左右缩进 @@ -309,7 +309,7 @@ Vim 是从 vi 发展出来的一个文本编辑器。代码补完、编译及错 > - `:split` → 创建分屏 (`:vsplit`创建垂直分屏) > - `` : dir 就是方向,可以是 `hjkl` 或是 ←↓↑→ 中的一个,其用来切换分屏。 -> - `_` (或 `|`) : 最大化尺寸 (| 垂直分屏) +> - `_` (或 `|`) : 最大化尺寸 (``| 垂直分屏) > - `+` (或 `-`) : 增加尺寸 ![img](http://upload-images.jianshu.io/upload_images/3101171-f329d01e299cb366.gif?imageMogr2/auto-orient/strip) diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/09.zsh.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/09.zsh.md" index 816b7cb4be..bb58016ab8 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/09.zsh.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/09.zsh.md" @@ -11,7 +11,7 @@ tags: - Linux - 工具 - Zsh -permalink: /pages/078b3e/ +permalink: /pages/3e7042f9/ --- # oh-my-zsh 应用 diff --git "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/README.md" "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/README.md" index f39e68f12f..7d9ccc7766 100644 --- "a/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/README.md" +++ "b/source/_posts/14.\346\223\215\344\275\234\347\263\273\347\273\237/02.Linux/02.\345\267\245\345\205\267/README.md" @@ -9,7 +9,7 @@ tags: - 操作系统 - Linux - 工具 -permalink: /pages/38874e/ +permalink: /pages/785397b5/ hidden: true index: false --- diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/99.\345\210\206\345\270\203\345\274\217\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/99.\345\210\206\345\270\203\345\274\217\351\235\242\350\257\225.md" deleted file mode 100644 index ad9968bb7a..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/99.\345\210\206\345\270\203\345\274\217\351\235\242\350\257\225.md" +++ /dev/null @@ -1,505 +0,0 @@ ---- -title: 分布式面试总结 -date: 2018-07-10 16:02:00 -order: 99 -categories: - - 分布式 - - 分布式综合 -tags: - - 分布式 - - 分布式综合 - - 面试 -permalink: /pages/f9209d/ ---- - -# 分布式面试总结 - -## 分布式缓存 - -### Redis 有什么数据类型?分别用于什么场景 - -| 数据类型 | 可以存储的值 | 操作 | -| -------- | ---------------------- | ---------------------------------------------------------------------------------------------------------------- | -| STRING | 字符串、整数或者浮点数 | 对整个字符串或者字符串的其中一部分执行操作
    对整数和浮点数执行自增或者自减操作 | -| LIST | 列表 | 从两端压入或者弹出元素
    读取单个或者多个元素
    进行修剪,只保留一个范围内的元素 | -| SET | 无序集合 | 添加、获取、移除单个元素
    检查一个元素是否存在于集合中
    计算交集、并集、差集
    从集合里面随机获取元素 | -| HASH | 包含键值对的无序散列表 | 添加、获取、移除单个键值对
    获取所有键值对
    检查某个键是否存在 | -| ZSET | 有序集合 | 添加、获取、删除元素
    根据分值范围或者成员来获取元素
    计算一个键的排名 | - -> [What Redis data structures look like](https://redislabs.com/ebook/part-1-getting-started/chapter-1-getting-to-know-redis/1-2-what-redis-data-structures-look-like/) - -### Redis 的主从复制是如何实现的 - -1. 从服务器连接主服务器,发送 SYNC 命令; -2. 主服务器接收到 SYNC 命名后,开始执行 BGSAVE 命令生成 RDB 文件并使用缓冲区记录此后执行的所有写命令; -3. 主服务器 BGSAVE 执行完后,向所有从服务器发送快照文件,并在发送期间继续记录被执行的写命令; -4. 从服务器收到快照文件后丢弃所有旧数据,载入收到的快照; -5. 主服务器快照发送完毕后开始向从服务器发送缓冲区中的写命令; -6. 从服务器完成对快照的载入,开始接收命令请求,并执行来自主服务器缓冲区的写命令; - -### Redis 的 key 是如何寻址的 - -#### 背景 - -(1)redis 中的每一个数据库,都由一个 redisDb 的结构存储。其中: - -- redisDb.id 存储着 redis 数据库以整数表示的号码。 -- redisDb.dict 存储着该库所有的键值对数据。 -- redisDb.expires 保存着每一个键的过期时间。 - -(2)当 redis 服务器初始化时,会预先分配 16 个数据库(该数量可以通过配置文件配置),所有数据库保存到结构 redisServer 的一个成员 redisServer.db 数组中。当我们选择数据库 select number 时,程序直接通过 redisServer.db[number] 来切换数据库。有时候当程序需要知道自己是在哪个数据库时,直接读取 redisDb.id 即可。 - -(3)redis 的字典使用哈希表作为其底层实现。dict 类型使用的两个指向哈希表的指针,其中 0 号哈希表(ht[0])主要用于存储数据库的所有键值,而 1 号哈希表主要用于程序对 0 号哈希表进行 rehash 时使用,rehash 一般是在添加新值时会触发,这里不做过多的赘述。所以 redis 中查找一个 key,其实就是对进行该 dict 结构中的 ht[0] 进行查找操作。 - -(4)既然是哈希,那么我们知道就会有哈希碰撞,那么当多个键哈希之后为同一个值怎么办呢?redis 采取链表的方式来存储多个哈希碰撞的键。也就是说,当根据 key 的哈希值找到该列表后,如果列表的长度大于 1,那么我们需要遍历该链表来找到我们所查找的 key。当然,一般情况下链表长度都为是 1,所以时间复杂度可看作 o(1)。 - -#### 寻址 key 的步骤 - -1. 当拿到一个 key 后,redis 先判断当前库的 0 号哈希表是否为空,即:if (dict->ht[0].size == 0)。如果为 true 直接返回 NULL。 -2. 判断该 0 号哈希表是否需要 rehash,因为如果在进行 rehash,那么两个表中者有可能存储该 key。如果正在进行 rehash,将调用一次`_dictRehashStep` 方法,`_dictRehashStep` 用于对数据库字典、以及哈希键的字典进行被动 rehash,这里不作赘述。 -3. 计算哈希表,根据当前字典与 key 进行哈希值的计算。 -4. 根据哈希值与当前字典计算哈希表的索引值。 -5. 根据索引值在哈希表中取出链表,遍历该链表找到 key 的位置。一般情况,该链表长度为 1。 -6. 当 ht[0] 查找完了之后,再进行了次 rehash 判断,如果未在 rehashing,则直接结束,否则对 ht[1]重复 345 步骤。 - -### Redis 的集群模式是如何实现的? - -Redis Cluster 是 Redis 的分布式解决方案,在 Redis 3.0 版本正式推出的。 - -Redis Cluster 去中心化,每个节点保存数据和整个集群状态,每个节点都和其他所有节点连接。 - -#### Redis Cluster 节点分配 - -Redis Cluster 特点: - -1. 所有的 redis 节点彼此互联(PING-PONG 机制),内部使用二进制协议优化传输速度和带宽。 -2. 节点的 fail 是通过集群中超过半数的节点检测失效时才生效。 -3. 客户端与 redis 节点直连,不需要中间 proxy 层。客户端不需要连接集群所有节点,连接集群中任何一个可用节点即可。 -4. redis-cluster 把所有的物理节点映射到[0-16383] 哈希槽 (hash slot)上(不一定是平均分配),cluster 负责维护 node<->slot<->value。 -5. Redis 集群预分好 16384 个桶,当需要在 Redis 集群中放置一个 key-value 时,根据 CRC16(key) mod 16384 的值,决定将一个 key 放到哪个桶中。 - -#### Redis Cluster 主从模式 - -Redis Cluster 为了保证数据的高可用性,加入了主从模式。 - -一个主节点对应一个或多个从节点,主节点提供数据存取,从节点则是从主节点拉取数据备份。当这个主节点挂掉后,就会有这个从节点选取一个来充当主节点,从而保证集群不会挂掉。所以,在集群建立的时候,一定要为每个主节点都添加了从节点。 - -#### Redis Sentinel - -Redis Sentinel 用于管理多个 Redis 服务器,它有三个功能: - -- **监控(Monitoring)** - Sentinel 会不断地检查你的主服务器和从服务器是否运作正常。 -- **提醒(Notification)** - 当被监控的某个 Redis 服务器出现问题时, Sentinel 可以通过 API 向管理员或者其他应用程序发送通知。 -- **自动故障迁移(Automatic failover)** - 当一个主服务器不能正常工作时, Sentinel 会开始一次自动故障迁移操作, 它会将失效主服务器的其中一个从服务器升级为新的主服务器, 并让失效主服务器的其他从服务器改为复制新的主服务器; 当客户端试图连接失效的主服务器时, 集群也会向客户端返回新主服务器的地址, 使得集群可以使用新主服务器代替失效服务器。 - -Redis 集群中应该有奇数个节点,所以至少有三个节点。 - -哨兵监控集群中的主服务器出现故障时,需要根据 quorum 选举出一个哨兵来执行故障转移。选举需要 majority,即大多数哨兵是运行的(2 个哨兵的 majority=2,3 个哨兵的 majority=2,5 个哨兵的 majority=3,4 个哨兵的 majority=2)。 - -假设集群仅仅部署 2 个节点 - -``` -+----+ +----+ -| M1 |---------| R1 | -| S1 | | S2 | -+----+ +----+ -``` - -如果 M1 和 S1 所在服务器宕机,则哨兵只有 1 个,无法满足 majority 来进行选举,就不能执行故障转移。 - -### Redis 如何实现分布式锁?ZooKeeper 如何实现分布式锁?比较二者优劣? - -分布式锁的三种实现: - -- 基于数据库实现分布式锁; -- 基于缓存(Redis 等)实现分布式锁; -- 基于 Zookeeper 实现分布式锁; - -#### 数据库实现 - -#### Redis 实现 - -1. 获取锁的时候,使用 setnx 加锁,并使用 expire 命令为锁添加一个超时时间,超过该时间则自动释放锁,锁的 value 值为一个随机生成的 UUID,通过此在释放锁的时候进行判断。 -2. 获取锁的时候还设置一个获取的超时时间,若超过这个时间则放弃获取锁。 -3. 释放锁的时候,通过 UUID 判断是不是该锁,若是该锁,则执行 delete 进行锁释放。 - -#### ZooKeeper 实现 - -1. 创建一个目录 mylock; -2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点; -3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; -4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; -5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 - -#### 实现对比 - -ZooKeeper 具备高可用、可重入、阻塞锁特性,可解决失效死锁问题。 -但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。 - -### Redis 的持久化方式?有什么优缺点?持久化实现原理? - -#### RDB 快照(snapshot) - -将存在于某一时刻的所有数据都写入到硬盘中。 - -##### 快照的原理 - -在默认情况下,Redis 将数据库快照保存在名字为 dump.rdb 的二进制文件中。你可以对 Redis 进行设置, 让它在“N 秒内数据集至少有 M 个改动”这一条件被满足时, 自动保存一次数据集。你也可以通过调用 SAVE 或者 BGSAVE,手动让 Redis 进行数据集保存操作。这种持久化方式被称为快照。 - -当 Redis 需要保存 dump.rdb 文件时, 服务器执行以下操作: - -- Redis 创建一个子进程。 -- 子进程将数据集写入到一个临时快照文件中。 -- 当子进程完成对新快照文件的写入时,Redis 用新快照文件替换原来的快照文件,并删除旧的快照文件。 - -这种工作方式使得 Redis 可以从写时复制(copy-on-write)机制中获益。 - -##### 快照的优点 - -- 它保存了某个时间点的数据集,非常适用于数据集的备份。 -- 很方便传送到另一个远端数据中心或者亚马逊的 S3(可能加密),非常适用于灾难恢复。 -- 快照在保存 RDB 文件时父进程唯一需要做的就是 fork 出一个子进程,接下来的工作全部由子进程来做,父进程不需要再做其他 IO 操作,所以快照持久化方式可以最大化 redis 的性能。 -- 与 AOF 相比,在恢复大的数据集的时候,DB 方式会更快一些。 - -##### 快照的缺点 - -- 如果你希望在 redis 意外停止工作(例如电源中断)的情况下丢失的数据最少的话,那么快照不适合你。 -- 快照需要经常 fork 子进程来保存数据集到硬盘上。当数据集比较大的时候,fork 的过程是非常耗时的,可能会导致 Redis 在一些毫秒级内不能响应客户端的请求。 - -#### AOF - -AOF 持久化方式记录每次对服务器执行的写操作。当服务器重启的时候会重新执行这些命令来恢复原始的数据。 - -#### AOF 的原理 - -- Redis 创建一个子进程。 -- 子进程开始将新 AOF 文件的内容写入到临时文件。 -- 对于所有新执行的写入命令,父进程一边将它们累积到一个内存缓存中,一边将这些改动追加到现有 AOF 文件的末尾,这样样即使在重写的中途发生停机,现有的 AOF 文件也还是安全的。 -- 当子进程完成重写工作时,它给父进程发送一个信号,父进程在接收到信号之后,将内存缓存中的所有数据追加到新 AOF 文件的末尾。 -- 搞定!现在 Redis 原子地用新文件替换旧文件,之后所有命令都会直接追加到新 AOF 文件的末尾。 - -#### AOF 的优点 - -- 使用默认的每秒 fsync 策略,Redis 的性能依然很好(fsync 是由后台线程进行处理的,主线程会尽力处理客户端请求),一旦出现故障,使用 AOF ,你最多丢失 1 秒的数据。 -- AOF 文件是一个只进行追加的日志文件,所以不需要写入 seek,即使由于某些原因(磁盘空间已满,写的过程中宕机等等)未执行完整的写入命令,你也也可使用 redis-check-aof 工具修复这些问题。 -- Redis 可以在 AOF 文件体积变得过大时,自动地在后台对 AOF 进行重写:重写后的新 AOF 文件包含了恢复当前数据集所需的最小命令集合。整个重写操作是绝对安全的。 -- AOF 文件有序地保存了对数据库执行的所有写入操作,这些写入操作以 Redis 协议的格式保存。因此 AOF 文件的内容非常容易被人读懂,对文件进行分析(parse)也很轻松。 - -#### AOF 的缺点 - -- 对于相同的数据集来说,AOF 文件的体积通常要大于 RDB 文件的体积。 -- 根据所使用的 fsync 策略,AOF 的速度可能会慢于快照。在一般情况下,每秒 fsync 的性能依然非常高,而关闭 fsync 可以让 AOF 的速度和快照一样快,即使在高负荷之下也是如此。不过在处理巨大的写入载入时,快照可以提供更有保证的最大延迟时间(latency)。 - -### Redis 过期策略有哪些? - -- **noeviction** - 当内存使用达到阈值的时候,所有引起申请内存的命令会报错。 -- **allkeys-lru** - 在主键空间中,优先移除最近未使用的 key。 -- **allkeys-random** - 在主键空间中,随机移除某个 key。 -- **volatile-lru** - 在设置了过期时间的键空间中,优先移除最近未使用的 key。 -- **volatile-random** - 在设置了过期时间的键空间中,随机移除某个 key。 -- **volatile-ttl** - 在设置了过期时间的键空间中,具有更早过期时间的 key 优先移除。 - -### Redis 和 Memcached 有什么区别? - -两者都是非关系型内存键值数据库。有以下主要不同: - -**数据类型** - -- Memcached 仅支持字符串类型; -- 而 Redis 支持五种不同种类的数据类型,使得它可以更灵活地解决问题。 - -**数据持久化** - -- Memcached 不支持持久化; -- Redis 支持两种持久化策略:RDB 快照和 AOF 日志。 - -**分布式** - -- Memcached 不支持分布式,只能通过在客户端使用像一致性哈希这样的分布式算法来实现分布式存储,这种方式在存储和查询时都需要先在客户端计算一次数据所在的节点。 -- Redis Cluster 实现了分布式的支持。 - -**内存管理机制** - -- Memcached 将内存分割成特定长度的块来存储数据,以完全解决内存碎片的问题,但是这种方式会使得内存的利用率不高,例如块的大小为 128 bytes,只存储 100 bytes 的数据,那么剩下的 28 bytes 就浪费掉了。 -- 在 Redis 中,并不是所有数据都一直存储在内存中,可以将一些很久没用的 value 交换到磁盘。而 Memcached 的数据则会一直在内存中。 - -### 为什么单线程的 Redis 性能反而优于多线程的 Memcached? - -Redis 快速的原因: - -1. 绝大部分请求是纯粹的内存操作(非常快速) -2. 采用单线程,避免了不必要的上下文切换和竞争条件 -3. 非阻塞 IO - -内部实现采用 epoll,采用了 epoll+自己实现的简单的事件框架。epoll 中的读、写、关闭、连接都转化成了事件,然后利用 epoll 的多路复用特性,绝不在 io 上浪费一点时间。 - -## 分布式消息队列(MQ) - -### 为什么使用 MQ? - -- 异步处理 - 相比于传统的串行、并行方式,提高了系统吞吐量。 -- 应用解耦 - 系统间通过消息通信,不用关心其他系统的处理。 -- 流量削锋 - 可以通过消息队列长度控制请求量;可以缓解短时间内的高并发请求。 -- 日志处理 - 解决大量日志传输。 -- 消息通讯 - 消息队列一般都内置了高效的通信机制,因此也可以用在纯的消息通讯。比如实现点对点消息队列,或者聊天室等。 - -### 如何保证 MQ 的高可用? - -#### 数据复制 - -1. 将所有 Broker 和待分配的 Partition 排序 -2. 将第 i 个 Partition 分配到第(i mod n)个 Broker 上 -3. 将第 i 个 Partition 的第 j 个 Replica 分配到第((i + j) mode n)个 Broker 上 - -#### 选举主服务器 - -### MQ 有哪些常见问题?如何解决这些问题? - -MQ 的常见问题有: - -1. 消息的顺序问题 -2. 消息的重复问题 - -#### 消息的顺序问题 - -消息有序指的是可以按照消息的发送顺序来消费。 - -假如生产者产生了 2 条消息:M1、M2,假定 M1 发送到 S1,M2 发送到 S2,如果要保证 M1 先于 M2 被消费,怎么做? - -
    -解决方案: - -(1)保证生产者 - MQServer - 消费者是一对一对一的关系 - -
    -缺陷: - -- 并行度就会成为消息系统的瓶颈(吞吐量不够) -- 更多的异常处理,比如:只要消费端出现问题,就会导致整个处理流程阻塞,我们不得不花费更多的精力来解决阻塞的问题。 - -(2)通过合理的设计或者将问题分解来规避。 - -- 不关注乱序的应用实际大量存在 -- 队列无序并不意味着消息无序 - -所以从业务层面来保证消息的顺序而不仅仅是依赖于消息系统,是一种更合理的方式。 - -#### 消息的重复问题 - -造成消息重复的根本原因是:网络不可达。 - -所以解决这个问题的办法就是绕过这个问题。那么问题就变成了:如果消费端收到两条一样的消息,应该怎样处理? - -消费端处理消息的业务逻辑保持幂等性。只要保持幂等性,不管来多少条重复消息,最后处理的结果都一样。 -保证每条消息都有唯一编号且保证消息处理成功与去重表的日志同时出现。利用一张日志表来记录已经处理成功的消息的 ID,如果新到的消息 ID 已经在日志表中,那么就不再处理这条消息。 - -### Kafka, ActiveMQ, RabbitMQ, RocketMQ 各有什么优缺点? - -
    -## 分布式服务(RPC) - -### Dubbo 的实现过程? - -
    - -
    - -节点角色: - -| 节点 | 角色说明 | -| --------- | -------------------------------------- | -| Provider | 暴露服务的服务提供方 | -| Consumer | 调用远程服务的服务消费方 | -| Registry | 服务注册与发现的注册中心 | -| Monitor | 统计服务的调用次数和调用时间的监控中心 | -| Container | 服务运行容器 | - -调用关系: - -1. 务容器负责启动,加载,运行服务提供者。 -2. 服务提供者在启动时,向注册中心注册自己提供的服务。 -3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 -4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 -5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 -6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 - -### Dubbo 负载均衡策略有哪些? - -##### Random - -- 随机,按权重设置随机概率。 -- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - -##### RoundRobin - -- 轮循,按公约后的权重设置轮循比率。 -- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - -##### LeastActive - -- 最少活跃调用数,相同活跃数的随机,活跃数指调用前后计数差。 -- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - -##### ConsistentHash - -- 一致性 Hash,相同参数的请求总是发到同一提供者。 -- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 -- 算法参见: -- 缺省只对第一个参数 Hash,如果要修改,请配置 `` -- 缺省用 160 份虚拟节点,如果要修改,请配置 `` - -### Dubbo 集群容错策略 ? - -
    -- **Failover** - 失败自动切换,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 -- **Failfast** - 快速失败,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 -- **Failsafe** - 失败安全,出现异常时,直接忽略。通常用于写入审计日志等操作。 -- **Failback** - 失败自动恢复,后台记录失败请求,定时重发。通常用于消息通知操作。 -- **Forking** - 并行调用多个服务器,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 -- **Broadcast** - 播调用所有提供者,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 - -### 动态代理策略? - -Dubbo 作为 RPC 框架,首先要完成的就是跨系统,跨网络的服务调用。消费方与提供方遵循统一的接口定义,消费方调用接口时,Dubbo 将其转换成统一格式的数据结构,通过网络传输,提供方根据规则找到接口实现,通过反射完成调用。也就是说,消费方获取的是对远程服务的一个代理(Proxy),而提供方因为要支持不同的接口实现,需要一个包装层(Wrapper)。调用的过程大概是这样: - -
    -消费方的 Proxy 和提供方的 Wrapper 得以让 Dubbo 构建出复杂、统一的体系。而这种动态代理与包装也是通过基于 SPI 的插件方式实现的,它的接口就是**ProxyFactory**。 - -``` -@SPI("javassist") -public interface ProxyFactory { - - @Adaptive({Constants.PROXY_KEY}) - T getProxy(Invoker invoker) throws RpcException; - - @Adaptive({Constants.PROXY_KEY}) - Invoker getInvoker(T proxy, Class type, URL url) throws RpcException; - -} -``` - -ProxyFactory 有两种实现方式,一种是基于 JDK 的代理实现,一种是基于 javassist 的实现。ProxyFactory 接口上定义了@SPI("javassist"),默认为 javassist 的实现。 - -### Dubbo 支持哪些序列化协议?Hessian?Hessian 的数据结构? - -1. dubbo 序列化,阿里尚不成熟的 java 序列化实现。 -2. hessian2 序列化:hessian 是一种跨语言的高效二进制的序列化方式,但这里实际不是原生的 hessian2 序列化,而是阿里修改过的 hessian lite,它是 dubbo RPC 默认启用的序列化方式。 -3. json 序列化:目前有两种实现,一种是采用的阿里的 fastjson 库,另一种是采用 dubbo 中自已实现的简单 json 库,一般情况下,json 这种文本序列化性能不如二进制序列化。 -4. java 序列化:主要是采用 JDK 自带的 java 序列化实现,性能很不理想。 -5. Kryo 和 FST:Kryo 和 FST 的性能依然普遍优于 hessian 和 dubbo 序列化。 - -Hessian 序列化与 Java 默认的序列化区别? - -Hessian 是一个轻量级的 remoting on http 工具,采用的是 Binary RPC 协议,所以它很适合于发送二进制数据,同时又具有防火墙穿透能力。 - -1. Hessian 支持跨语言串行 -2. 比 java 序列化具有更好的性能和易用性 -3. 支持的语言比较多 - -### Protoco Buffer 是什么? - -Protocol Buffer 是 Google 出品的一种轻量 & 高效的结构化数据存储格式,性能比 Json、XML 真的强!太!多! - -Protocol Buffer 的序列化 & 反序列化简单 & 速度快的原因是: - -1. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等) -2. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成 - -Protocol Buffer 的数据压缩效果好(即序列化后的数据量体积小)的原因是: - -1. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等 -2. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑 - -### 注册中心挂了可以继续通信吗? - -可以。Dubbo 消费者在应用启动时会从注册中心拉取已注册的生产者的地址接口,并缓存在本地。每次调用时,按照本地存储的地址进行调用。 - -### ZooKeeper 原理是什么?ZooKeeper 有什么用? - -ZooKeeper 是一个分布式应用协调系统,已经用到了许多分布式项目中,用来完成统一命名服务、状态同步服务、集群管理、分布式应用配置项的管理等工作。 - -
    - -
    - -1. 每个 Server 在内存中存储了一份数据; -2. Zookeeper 启动时,将从实例中选举一个 leader(Paxos 协议); -3. Leader 负责处理数据更新等操作(Zab 协议); -4. 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改数据。 - -### Netty 有什么用?NIO/BIO/AIO 有什么用?有什么区别? - -Netty 是一个“网络通讯框架”。 - -Netty 进行事件处理的流程。`Channel`是连接的通道,是 ChannelEvent 的产生者,而`ChannelPipeline`可以理解为 ChannelHandler 的集合。 - -
    -> 参考:https://github.com/code4craft/netty-learning/blob/master/posts/ch1-overview.md - -IO 的方式通常分为几种: - -- 同步阻塞的 BIO -- 同步非阻塞的 NIO -- 异步非阻塞的 AIO - -在使用同步 I/O 的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。 - -NIO 基于 Reactor,当 socket 有流可读或可写入 socket 时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。 - -与 NIO 不同,当进行读写操作时,只须直接调用 API 的 read 或 write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write 方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write 方法都是异步的,完成后会主动调用回调函数。 - -> 参考:https://blog.csdn.net/skiof007/article/details/52873421 - -### 为什么要进行系统拆分?拆分不用 Dubbo 可以吗? - -系统拆分从资源角度分为:应用拆分和数据库拆分。 - -从采用的先后顺序可分为:水平扩展、垂直拆分、业务拆分、水平拆分。 - -
    -是否使用服务依据实际业务场景来决定。 - -当垂直应用越来越多,应用之间交互不可避免,将核心业务抽取出来,作为独立的服务,逐渐形成稳定的服务中心,使前端应用能更快速的响应多变的市场需求。此时,用于提高业务复用及整合的分布式服务框架(RPC)是关键。 - -当服务越来越多,容量的评估,小服务资源的浪费等问题逐渐显现,此时需增加一个调度中心基于访问压力实时管理集群容量,提高集群利用率。此时,用于提高机器利用率的资源调度和治理中心(SOA)是关键。 - -### Dubbo 和 Thrift 有什么区别? - -- Thrift 是跨语言的 RPC 框架。 -- Dubbo 支持服务治理,而 Thrift 不支持。 - -## 分布式锁基本原理 - -> 分布式锁有几种实现方式?实现的要点是什么? -> -> 分布式锁各方案有什么利弊?如何选择方案?为什么? -> -> Redis 分布式锁如何保证可重入性? -> -> 详细内容请参考:[分布式锁](../11.分布式协同/01.分布式协同综合/06.分布式锁.md) - -【答题思路】 - -实现方式一般有: - -- 基于数据库实现: - - 建一张表(t_dlock),关键字段有:`id`、`method_name`、`time`。 - - 向表中插入记录成功,即为获取锁成功。需要注意的是,获取锁一般是通过自旋方式,并设置尝试次数,超过最大尝试次数,才判定获取锁失败。 - - 删除记录,即为释放锁。 - - 因为数据库没有淘汰机制,为了避免获取锁永不释放,应用需要自身实现定期检查,删除过期记录(根据 time 判断)。 -- 基于 Redis 实现 - - 生成一个分布式 ID 作为 key,通过 `setnx` 写入 - - 写入成功,即为获取锁成功。需要注意的是,获取锁一般是通过自旋方式,并设置尝试次数,超过最大尝试次数,才判定获取锁失败。 - - 删除 key,即为获取锁失败。 - - Redis 自身有内存淘汰策略,所以只要设置 expire,就可以让 key 自动过期。 -- 基于 ZooKeeper 实现 - - 创建一个节点,所有节点都 Watch 此节点。 - - 任意节点的任意线程只要向这个节点创建临时子节点成功,即为获取锁成功。 - - 由于创建临时子节点是原子性的,不存在竞态,不需要自旋尝试,性能很好。 - - 因为 ZooKeeper 只要和节点断开会话,就会自动删除临时节点。即为删除锁。所以无需过期机制。 - -从实现方式可以看出,三种方案的对比: - -- Mysql 方案性能最差,并且影响 Mysql 吞吐量。而且还要程序保证容错处理。不建议采用这种方案。 -- Redis 方案需要不断自旋尝试获取锁,应用会消耗一些性能开销。而且为了保证分布式锁的可重入性,需要设置对于所有节点、所有线程都唯一的分布式 ID,生成 ID 也需要一定的 CPU 开销。 -- ZooKeeper 方案实现最简单,最稳定。是推荐的方案。但是它也有一个问题:ZooKeeper 的主从架构,所有写都由 Master 节点负责,所以 ZooKeeper 自身有一定的性能瓶颈。 \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/CAP&BASE.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/CAP&BASE.md" new file mode 100644 index 0000000000..73ea5ebe66 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/CAP&BASE.md" @@ -0,0 +1,140 @@ +--- +title: CAP 和 BASE +date: 2021-11-08 08:15:33 +categories: + - 分布式 + - 分布式综合 +tags: + - 分布式 + - 一致性 + - ACID + - CAP + - BASE +permalink: /pages/1c5cc28c/ +--- + +# CAP 和 BASE + +## 一致性 + +一致性(Consistency)指的是**多个数据副本是否能保持一致**的特性。 + +在一致性的条件下,分布式系统在执行写操作成功后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 + +数据一致性又可以分为以下几点: + +- **强一致性** - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。 +- **弱一致性** - 系统在写入数据成功后,不承诺立即能读到最新的值,也不承诺什么时候能读到,但是过一段时间之后用户可以看到更新后的值。那么用户读不到最新数据的这段时间被称为“不一致窗口时间”。 +- **最终一致性** - 最终一致性作为弱一致性中的特例,强调的是所有数据副本,在经过一段时间的同步后,最终能够到达一致的状态,不需要实时保证系统数据的强一致性。 + +## ACID + +ACID 是数据库事务正确执行的四个基本要素的单词缩写: + +- **原子性(Atomicity)** + - 原子是指不可分解为更小粒度的东西。事务的原子性意味着:**事务中的所有操作要么全部成功,要么全部失败**。 + - 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 +- **一致性(Consistency)** + - 数据库在事务执行前后都保持一致性状态。 + - 在一致性状态下,所有事务对一个数据的读取结果都是相同的。 +- **隔离性(Isolation)** + - 同时运行的事务互不干扰。换句话说,一个事务所做的修改在最终提交以前,对其它事务是不可见的。 +- **持久性(Durability)** + - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 + - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 + +一个支持事务(Transaction)的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。 + +- 只有满足一致性,事务的执行结果才是正确的。 +- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 +- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 +- 事务满足持久化是为了能应对系统崩溃的情况。 + +## CAP 定理 + +### CAP 简介 + +1998 年,Brewer 提出了分布式系统领域大名鼎鼎的 CAP 定理。 + +CAP 定理提出:分布式系统有三个指标,这三个指标不能同时做到: + +- **一致性(Consistency)** - 在任何给定时间,网络中的所有节点都具有完全相同(最近)的值。 +- **可用性(Availability)** - 对网络的每个请求都会返回响应,但不能保证返回的数据是最新的。 +- **分区容错性(Partition Tolerance)** - 即使任意数量的节点出现故障,网络仍会继续运行。 + +CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405160639643.png) + +在分布式系统中,分区容错性是一个既定的事实:因为分布式系统总会出现各种各样的问题,如由于网络原因而导致节点失联;发生机器故障;机器重启或升级等等。因此,**CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡**。 + +### AP 模式 + +对网络的每个请求都会收到响应,即使由于网络分区(故障节点)而无法保证数据一定是最新的。 + +选择 **AP 模式**,偏向于保证服务的高可用性。用户访问系统的时候,都能得到响应数据,不会出现响应错误;但是,当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。 + + + +### CP 模式 + +如果由于网络分区(故障节点)而无法保证特定信息是最新的,则系统将返回错误或超时。 + +选择 **CP 模式**,一旦因为消息丢失、延迟过高发生了网络分区,就会影响用户的体验和业务的可用性。因为为了防止数据不一致,系统将拒绝新数据的写入。 + + + +### CAP 定理的应用 + +CAP 定理在分布式系统设计中,可以被应用与哪些方面? + +一个最具代表性的问题是:服务注册中心应该选择 AP 还是 CP? + +在微服务架构下,服务注册和服务发现机制中主要有三种角色: + +- **服务提供者**(RPC Server / Provider) +- **服务消费者**(RPC Client / Consumer) +- **服务注册中心**(Registry) + +**注册中心**负责协调服务注册和服务发现,显然它是核心中的核心。主流的注册中心有很多,如:ZooKeeper、Nacos、Eureka、Consul、etcd 等。在针对注册中心进行技术选型时,其 CAP 设计也是一个比较的维度。 + +- CP 模型代表:ZooKeeper、etcd。系统强调数据的一致性,当数据一致性无法保证时(如:正在选举主节点),系统拒绝请求。 +- AP 模型代表:Nacos、Eureka。系统强调可用性,牺牲一定的一致性(即服务节点上的数据不保证是最新的),来保证整体服务可用。 + +对于服务注册中心而言,即使不同节点保存的服务注册信息存在差异,也不会造成灾难性的后果,仅仅是信息滞后而已。但是,如果为了追求数据一致性,使得服务发现短时间内不可用,负面影响更严重。所以,对于服务注册中心而言,可用性比一致性更重要,一般应该选择 AP 模型。 + +### CAP 定理的误导 + +CAP 定理在分布式系统领域大名鼎鼎,以至于被很多人视为了真理。然而,CAP 定理真的正确吗? + +网络分区是一种故障,不管喜欢还是不喜欢,它都可能发生,所以无法选择或逃避分区的问题。在网络正常的时候,系统可以同时保证一致性(线性化)和可用性。而一旦发生了网络故障,必须要么选择一致性,要么选择可用性。因此,对 CAP 更准确的理解应该是:**当发生网络分区(P)的情况下,可用性(A)和一致性(C)二者只能选其一**。 + +CAP 定理所描述的模型实际上局限性很大,它只考虑了一种一致性模型和一种故障(网络分区故障),而没有考虑网络延迟、节点失效等情况。因此,它对于指导一个具体的分布式系统设计来说,没有太大的实际价值。 + +值得一提的是,在 CAP 定理提出十二年之后,其提出者也发表了一篇文章 [**CAP Twelve Years Later: How the “Rules” Have Changed**](https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/),来阐述 CAP 定理的局限性。 + +## BASE 定理 + +BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State)`** 和 **`最终一致性(Eventually Consistent)`** 三个短语的缩写。BASE 定理是对 CAP 定理中可用性(A)和一致性(C)权衡的结果。 + +BASE 定理的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +- **基本可用(Basically Available)** - 分布式系统在出现故障的时候,**保证核心可用,允许损失部分可用性**。例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。 +- **软状态(Soft State)** - 指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即**允许系统不同节点的数据副本之间进行同步的过程存在延时**。 +- **最终一致性(Eventually Consistent)** - 强调的是所有数据副本,**在经过一段时间的同步后,最终能够到达一致的状态**,不需要实时保证系统数据的强一致性。 + +## BASE vs. ACID + +BASE 定理的**核心思想**是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过**牺牲强一致性来达到可用性**,通常运用在大型分布式系统中。 + + + +在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 + +## 参考资料 + +- [**Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services**](https://www.comp.nus.edu.sg/~gilbert/pubs/BrewersConjecture-SigAct.pdf),[**解读**](https://juejin.cn/post/6844903936718012430) - 经典的 CAP 定理,即:在一个分布式系统中,当发生网络分区时,那么强一致性和可用性只能二选一。 +- [**CAP Twelve Years Later: How the “Rules” Have Changed**](https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/), [**解读**](https://www.zhihu.com/question/64778723/answer/224266038) - CAP 定理的新解读,并阐述 CAP 定理的一些常见误区。 +- [**BASE: An Acid Alternative**](https://www.semanticscholar.org/paper/BASE%3A-An-Acid-Alternative-Pritchett/2e72e6c022dd33115304ecfcb6dad7ea609534a4),[**译文**](https://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html) - BASE 定理是对 CAP 中一致性和可用性的权衡,提出采用适当的方式来使系统达到最终一致性。 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/13.Gossip\347\256\227\346\263\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Gossip.md" similarity index 92% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/13.Gossip\347\256\227\346\263\225.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Gossip.md" index 3f7cb61418..d2ad248ffc 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/13.Gossip\347\256\227\346\263\225.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Gossip.md" @@ -1,17 +1,14 @@ --- title: 分布式算法 Gossip date: 2021-07-13 09:18:41 -order: 13 categories: - 分布式 - - 分布式理论 + - 分布式综合 tags: - 分布式 - - 理论 - 算法 - - 共识性 - Gossip -permalink: /pages/71539a/ +permalink: /pages/88db2f5d/ --- # 分布式算法 Gossip @@ -24,7 +21,7 @@ Gossip 协议是集群中节点相互通信的内部通信技术。 Gossip 是 ## Gossip 的应用 -在 CASSANDRA 中,节点间使用 Gossip 协议交换信息,因此所有节点都可以快速了解集群中的所有其他节点。 +在 Cassandra 中,节点间使用 Gossip 协议交换信息,因此所有节点都可以快速了解集群中的所有其他节点。 Consul 使用名为 SERF 的 Gossip 协议有两个作用: @@ -54,7 +51,7 @@ Gossip 过程是由种子节点发起,当一个种子节点有状态需要更 Goosip 协议的信息传播和扩散通常需要由种子节点发起。整个传播过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个**最终一致性**协议。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210708234308.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20210708234308.gif) ## Gossip 类型 @@ -73,9 +70,9 @@ Gossip 有两种类型: 在 Gossip 协议下,网络中两个节点之间有三种通信方式: -- Push: 节点 A 将数据 (key,value,version) 及对应的版本号推送给 B 节点,B 节点更新 A 中比自己新的数据 -- Pull:A 仅将数据 key, version 推送给 B,B 将本地比 A 新的数据(Key, value, version)推送给 A,A 更新本地 -- Push/Pull:与 Pull 类似,只是多了一步,A 再将本地比 B 新的数据推送给 B,B 则更新本地 +- **Push** - 节点 A 将数据 (key,value,version) 及对应的版本号推送给 B 节点,B 节点更新 A 中比自己新的数据 +- **Pull** - A 仅将数据 key, version 推送给 B,B 将本地比 A 新的数据(Key, value, version)推送给 A,A 更新本地 +- **Push/Pull** - 与 Pull 类似,只是多了一步,A 再将本地比 B 新的数据推送给 B,B 则更新本地 如果把两个节点数据同步一次定义为一个周期,则在一个周期内,Push 需通信 1 次,Pull 需 2 次,Push/Pull 则需 3 次。虽然消息数增加了,但从效果上来讲,Push/Pull 最好,理论上一个周期内可以使两个节点完全一致。直观上,Push/Pull 的收敛速度也是最快的。 @@ -101,4 +98,4 @@ Gossip 有两种类型: - [Epidemic Algorithms for Replicated Database Maintenance](http://bitsavers.trailing-edge.com/pdf/xerox/parc/techReports/CSL-89-1_Epidemic_Algorithms_for_Replicated_Database_Maintenance.pdf) - [P2P 网络核心技术:Gossip 协议](https://zhuanlan.zhihu.com/p/41228196) - [INTRODUCTION TO GOSSIP](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) -- [Goosip 协议仿真动画](https://flopezluis.github.io/gossip-simulator/) \ No newline at end of file +- [Goosip 协议仿真动画](https://flopezluis.github.io/gossip-simulator/) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/11.Paxos\347\256\227\346\263\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Paxos.md" similarity index 95% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/11.Paxos\347\256\227\346\263\225.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Paxos.md" index 65b2c5e392..d077af11c9 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/11.Paxos\347\256\227\346\263\225.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Paxos.md" @@ -2,26 +2,24 @@ title: 深入剖析共识性算法 Paxos cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202310200757219.png date: 2020-02-02 22:00:00 -order: 11 categories: - 分布式 - - 分布式理论 + - 分布式综合 tags: - 分布式 - - 理论 - 算法 - - 共识性 + - 共识 - Paxos -permalink: /pages/0276bb/ +permalink: /pages/ea903d16/ --- # 深入剖析共识性算法 Paxos -> Paxos 是一种基于消息传递且具有容错性的共识性(consensus)算法。 +> **Paxos 是一种基于消息传递且具有容错性的共识性(consensus)算法**。 > -> Paxos 算法解决的问题正是分布式一致性问题。在一个节点数为 2N+1 的分布式集群中,只要半数以上的节点(N + 1)还正常工作,整个系统仍可以正常工作。 +> **Paxos 算法解决了分布式一致性问题**:在一个节点数为 `2N+1` 的分布式集群中,只要半数以上的节点(`N + 1`)还正常工作,整个系统仍可以正常工作。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200757219.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170823342.png) ## Paxos 背景 @@ -29,22 +27,22 @@ Paxos 是 Leslie Lamport 于 1990 年提出的一种基于消息传递且具有 为描述 Paxos 算法,Lamport 虚拟了一个叫做 Paxos 的希腊城邦,这个岛按照议会民主制的政治模式制订法律,但是没有人愿意将自己的全部时间和精力放在这种事情上。所以无论是议员,议长或者传递纸条的服务员都不能承诺别人需要时一定会出现,也无法承诺批准决议或者传递消息的时间。 +Paxos 算法解决的问题正是分布式共识性问题,即一个分布式系统中的各个节点如何就某个值(决议)达成一致。 + +Paxos 算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。它利用大多数 (Majority) 机制保证了一定的容错能力,即 `N` 个节点的系统最多允许 `N / 2 - 1` 个节点同时出现故障。 + Paxos 算法包含 2 个部分: - **Basic Paxos 算法**:描述的多节点之间如何就某个值达成共识。 - **Multi Paxos 思想**:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。 -Paxos 算法解决的问题正是分布式共识性问题,即一个分布式系统中的各个进程如何就某个值(决议)达成一致。 - -Paxos 算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。它利用大多数 (Majority) 机制保证了 2N+1 的容错能力,即 2N+1 个节点的系统最多允许 N 个节点同时出现故障。 - ## Basic Paxos 算法 ### 角色 Paxos 将分布式系统中的节点分 Proposer、Acceptor、Learner 三种角色。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210528150700.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20210528150700.png) - **提议者(Proposer)**:发出提案(Proposal),用于投票表决。Proposal 信息包括提案编号 (Proposal ID) 和提议的值 (Value)。在绝大多数场景中,集群中收到客户端请求的节点,才是提议者。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑。 - **接受者(Acceptor)**:对每个 Proposal 进行投票,若 Proposal 获得多数 Acceptor 的接受,则称该 Proposal 被批准。一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。 @@ -72,15 +70,11 @@ Paxos 算法流程中的每条消息描述如下: - **Prepare**: Proposer 生成全局唯一且递增的 Proposal ID (可使用时间戳加 Server ID),向所有 Acceptors 发送 Prepare 请求,这里无需携带提案内容,只携带 Proposal ID 即可。 - **Promise**: Acceptors 收到 Prepare 请求后,做出“两个承诺,一个应答”。 - - 两个承诺: - - 不再接受 Proposal ID 小于等于当前请求的 Prepare 请求。 - 不再接受 Proposal ID 小于当前请求的 Propose 请求。 - - 一个应答: - 不违背以前作出的承诺下,回复已经 Accept 过的提案中 Proposal ID 最大的那个提案的 Value 和 Proposal ID,没有则返回空值。 - - **Propose**: Proposer 收到多数 Acceptors 的 Promise 应答后,从应答中选择 Proposal ID 最大的提案的 Value,作为本次要发起的提案。如果所有应答的提案 Value 均为空值,则可以自己随意决定提案 Value。然后携带当前 Proposal ID,向所有 Acceptors 发送 Propose 请求。 - **Accept**: Acceptor 收到 Propose 请求后,在不违背自己之前作出的承诺下,接受并持久化当前 Proposal ID 和提案 Value。 - **Learn**: Proposer 收到多数 Acceptors 的 Accept 后,决议形成,将形成的决议发送给所有 Learners。 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/README.md" new file mode 100644 index 0000000000..c995c3e4a9 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/README.md" @@ -0,0 +1,32 @@ +--- +title: 分布式综合 +date: 2022-06-23 17:17:18 +categories: + - 分布式 + - 分布式综合 +tags: + - 分布式 +permalink: /pages/2716904b/ +hidden: true +index: false +--- + +# 分布式综合 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170821320.png) + +## 📖 内容 + +- [分布式简介](分布式简介.md) +- [逻辑时钟](逻辑时钟.md) - 关键词:`物理时钟`、`逻辑时钟`、`向量时钟`、`全序`、`偏序` +- [CAP 和 BASE](CAP&BASE.md) - 关键词:`ACID`、`CAP`、`BASE`、`一致性` +- [拜占庭将军问题](拜占庭将军问题.md) - 关键词:`共识` +- [分布式算法 Paxos](Paxos.md) - 关键词:`共识`、`Paxos` +- [分布式算法 Raft](Raft.md) - 关键词:`共识`、`Raft` +- [分布式算法 Gossip](Gossip.md) - 关键词:`Gossip` +- [ZAB 协议](Zab.md) - 关键词:`共识`、`ZAB`、`ZooKeeper` +- [分布式综合面试](分布式综合面试.md) 💯 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/12.Raft\347\256\227\346\263\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Raft.md" similarity index 91% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/12.Raft\347\256\227\346\263\225.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Raft.md" index be52a5da11..8ebc8a4f0a 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/12.Raft\347\256\227\346\263\225.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Raft.md" @@ -1,22 +1,20 @@ --- title: 深入剖析共识性算法 Raft date: 2020-02-01 22:07:00 -order: 12 categories: - 分布式 - - 分布式理论 + - 分布式综合 tags: - 分布式 - - 理论 - 算法 - - 共识性 + - 共识 - Raft -permalink: /pages/4907dc/ +permalink: /pages/9386474c/ --- # 深入剖析共识性算法 Raft -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200201221202.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170818218.png) ## Raft 简介 @@ -40,7 +38,7 @@ Paxos 和 Raft 都是分布式共识性算法,这个过程如同投票选举 **`复制状态机(Replicated State Machines)`** 是指一组服务器上的状态机产生相同状态的副本,并且在一些机器宕掉的情况下也可以继续运行。一致性算法管理着来自客户端指令的复制日志。状态机从日志中处理相同顺序的相同指令,所以产生的结果也是相同的。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131233906.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131233906.png) 复制状态机通常都是基于复制日志实现的,如上图。每一个服务器存储一个包含一系列指令的日志,并且按照日志的顺序进行执行。每一个日志都按照相同的顺序包含相同的指令,所以每一个服务器都执行相同的指令序列。因为每个状态机都是确定的,每一次执行操作都产生相同的状态和同样的序列。 @@ -80,7 +78,7 @@ Raft 将一致性问题分解成了三个子问题: - **`Follower`** - 跟随者,**不会发送任何请求**,只是简单的 **响应来自 Leader 或者 Candidate 的请求**。 - **`Candidate`** - 参选者,选举新 Leader 时的临时角色。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131215742.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131215742.png) > :bulb: 图示说明: > @@ -90,7 +88,7 @@ Raft 将一致性问题分解成了三个子问题: ### 任期 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131220742.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131220742.png) Raft 把时间分割成任意长度的 **_`任期(Term)`_**,任期用连续的整数标记。每一段任期从一次**选举**开始。**Raft 保证了在一个给定的任期内,最多只有一个领导者**。 @@ -160,33 +158,33 @@ Raft 算法使用随机选举超时时间的方法来确保很少会发生选票 (1)下图表示一个分布式系统的最初阶段,此时只有 Follower,没有 Leader。Follower A 等待一个随机的选举超时时间之后,没收到 Leader 发来的心跳消息。因此,将 Term 由 0 增加为 1,转换为 Candidate,进入选举状态。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-01.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-01.gif) (2)此时,A 向所有其他节点发送投票请求。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-02.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-02.gif) (3)其它节点会对投票请求进行回复,如果超过半数以上的节点投票了,那么该 Candidate 就会立即变成 Term 为 1 的 Leader。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-03.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-03.gif) (4)Leader 会周期性地发送心跳消息给所有 Follower,Follower 接收到心跳包,会重新开始计时。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-04.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-candidate-04.gif) ### 多 Candidate 选举 (1)如果有多个 Follower 成为 Candidate,并且所获得票数相同,那么就需要重新开始投票。例如下图中 Candidate B 和 Candidate D 都发起 Term 为 4 的选举,且都获得两票,因此需要重新开始投票。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-multi-candidate-01.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-multi-candidate-01.gif) (2)当重新开始投票时,由于每个节点设置的随机竞选超时时间不同,因此能下一次再次出现多个 Candidate 并获得同样票数的概率很低。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-multi-candidate-02.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-multi-candidate-02.gif) ### 小结 -Raft 算法通过:领导者心跳消息、随机选举超时时间、得到大多数选票才通过原则、任期最新者优先、先来先服务的投票原则、等,保证了一个任期只有一位领导,也极大地减少了选举失败的情况。 +Raft 算法通过:领导者心跳消息、随机选举超时时间、得到大多数选票才通过原则、任期最新者优先、先来先服务等投票原则,保证了一个任期只有一位领导,也极大地减少了选举失败的情况。 ## 日志复制 @@ -197,7 +195,7 @@ Raft 算法通过:领导者心跳消息、随机选举超时时间、得到大 - 日志条目中的 Term 号被用来检查是否出现不一致的情况,它实际上是创建这条日志的领导者的任期编号。 - 日志条目中的日志索引用来表明它在日志中的位置,它是一个单调递增的整数。 -![img](https://pic3.zhimg.com/80/v2-ee29a89e4eb63468e142bb6103dbe4de_hd.jpg) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170814527.png) Raft 日志同步保证如下两点: @@ -208,7 +206,7 @@ Raft 日志同步保证如下两点: ### 日志复制流程 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200201115848.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170817072.png) 1. Leader 负责处理所有客户端的请求。 2. Leader 把请求作为日志条目加入到它的日志中,然后并行的向其他服务器发送 `AppendEntries RPC` 请求,要求 Follower 复制日志条目。 @@ -221,19 +219,19 @@ Raft 日志同步保证如下两点: (1)来自客户端的修改都会被传入 Leader。注意该修改还未被提交,只是写入日志中。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-01.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-01.gif) (2)Leader 会把修改复制到所有 Follower。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-02.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-02.gif) (3)Leader 会等待大多数的 Follower 也进行了修改,然后才将修改提交。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-03.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-03.gif) (4)此时 Leader 会通知的所有 Follower 让它们也提交修改,此时所有节点的值达成一致。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-04.gif) +![](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/raft-sync-log-04.gif) ### 日志一致性 @@ -243,7 +241,7 @@ Raft 日志同步保证如下两点: Leader 和 Follower 可能存在多种日志不一致的可能。 -![img](https://pic4.zhimg.com/80/v2-d36c587901391cae50788061f568d24f_hd.jpg) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170815743.png) > :bulb: 图示说明: > @@ -289,7 +287,7 @@ Raft 通过比较两份日志中最后一条日志条目的日志索引和 Term 一个当前 Term 的日志条目被复制到了半数以上的服务器上,Leader 就认为它是可以被提交的。如果这个 Leader 在提交日志条目前就下线了,后续的 Leader 可能会覆盖掉这个日志条目。 -![img](https://pic4.zhimg.com/80/v2-12a5ebab63781f9ec49e14e331775537_hd.jpg) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170816381.png) > 💡 图示说明: > @@ -318,7 +316,7 @@ Raft 通过比较两份日志中最后一条日志条目的日志索引和 Term 当 Leader 要发送某个日志条目,落后太多的 Follower 的日志条目会被丢弃,Leader 会将快照发给 Follower。或者新上线一台机器时,也会发送快照给它。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200201220628.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20200201220628.png) **生成快照的频率要适中**,频率过高会消耗大量 I/O 带宽;频率过低,一旦需要执行恢复操作,会丢失大量数据,影响可用性。推荐当日志达到某个固定的大小时生成快照。 @@ -336,4 +334,4 @@ Raft 通过比较两份日志中最后一条日志条目的日志索引和 Term - [Raft 算法详解](https://zhuanlan.zhihu.com/p/32052223) - [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) - 一个动画教程 - [The Raft Consensus Algorithm](https://raft.github.io/) - 一个交互式动画教程 -- [sofa-jraft](https://github.com/sofastack/sofa-jraft) - 蚂蚁金服的 Raft 算法实现库(Java 版) \ No newline at end of file +- [sofa-jraft](https://github.com/sofastack/sofa-jraft) - 蚂蚁金服的 Raft 算法实现库(Java 版) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Zab.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Zab.md" new file mode 100644 index 0000000000..8d2e1e156a --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/Zab.md" @@ -0,0 +1,113 @@ +--- +title: ZAB 协议 +date: 2024-04-28 16:41:07 +categories: + - 分布式 + - 分布式综合 +tags: + - 分布式 + - 算法 + - 共识 + - ZAB +permalink: /pages/51168337/ +--- + +# ZAB 协议 + +> ZooKeeper 并没有直接采用 Paxos 算法,而是采用了名为 ZAB 的一致性协议。**_ZAB 协议不是 Paxos 算法_**,只是比较类似,二者在操作上并不相同。Multi-Paxos 实现的是一系列值的共识,不关心最终达成共识的值是什么,不关心各值的顺序。而 ZooKeeper 需要确保操作的顺序性。 +> +> ZAB 协议是 Zookeeper 专门设计的一种**支持故障恢复的原子广播协议**。 +> +> ZAB 协议是 ZooKeeper 的数据一致性和高可用解决方案。 + +ZAB 协议定义了两个可以**无限循环**的流程: + +- **`选举 Leader`** - 用于故障恢复,从而保证高可用。 +- **`原子广播`** - 用于主从同步,从而保证数据一致性。 + +## 选举 Leader + +### 故障恢复 + +ZooKeeper 集群采用一主(称为 Leader)多从(称为 Follower)模式,主从节点通过副本机制保证数据一致。 + +- **如果 Follower 节点挂了** - ZooKeeper 集群中的每个节点都会单独在内存中维护自身的状态,并且各节点之间都保持着通讯,**只要集群中有半数机器能够正常工作,那么整个集群就可以正常提供服务**。 +- **如果 Leader 节点挂了** - 如果 Leader 节点挂了,系统就不能正常工作了。此时,需要通过 ZAB 协议的选举 Leader 机制来进行故障恢复。 + +ZAB 协议的选举 Leader 机制简单来说,就是:基于过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出选举 Leader 模式,进入原子广播模式。 + +### 术语 + +- **myid** - 每个 Zookeeper 服务器,都需要在数据文件夹下创建一个名为 myid 的文件,该文件包含整个 Zookeeper 集群唯一的 ID(整数)。 +- **zxid** - 类似于 RDBMS 中的事务 ID,用于标识一次更新操作的 Proposal ID。为了保证顺序性,该 zxid 必须单调递增。因此 Zookeeper 使用一个 64 位的数来表示,高 32 位是 Leader 的 epoch,从 1 开始,每次选出新的 Leader,epoch 加一。低 32 位为该 epoch 内的序号,每次 epoch 变化,都将低 32 位的序号重置。这样保证了 zxid 的全局递增性。 + +### 服务器状态 + +- **_LOOKING_** - 不确定 Leader 状态。该状态下的服务器认为当前集群中没有 Leader,会发起 Leader 选举 +- **_FOLLOWING_** - 跟随者状态。表明当前服务器角色是 Follower,并且它知道 Leader 是谁 +- **_LEADING_** - 领导者状态。表明当前服务器角色是 Leader,它会维护与 Follower 间的心跳 +- **_OBSERVING_** - 观察者状态。表明当前服务器角色是 Observer,与 Folower 唯一的不同在于不参与选举,也不参与集群写操作时的投票 + +### 选票数据结构 + +每个服务器在进行领导选举时,会发送如下关键信息 + +- **_logicClock_** - 每个服务器会维护一个自增的整数,名为 logicClock,它表示这是该服务器发起的第多少轮投票 +- **_state_** - 当前服务器的状态 +- **_self_id_** - 当前服务器的 myid +- **_self_zxid_** - 当前服务器上所保存的数据的最大 zxid +- **_vote_id_** - 被推举的服务器的 myid +- **_vote_zxid_** - 被推举的服务器上所保存的数据的最大 zxid + +### 投票流程 + +(1)**自增选举轮次** - Zookeeper 规定所有有效的投票都必须在同一轮次中。每个服务器在开始新一轮投票时,会先对自己维护的 logicClock 进行自增操作。 + +(2)**初始化选票** - 每个服务器在广播自己的选票前,会将自己的投票箱清空。该投票箱记录了所收到的选票。例:服务器 2 投票给服务器 3,服务器 3 投票给服务器 1,则服务器 1 的投票箱为(2, 3), (3, 1), (1, 1)。票箱中只会记录每一投票者的最后一票,如投票者更新自己的选票,则其它服务器收到该新选票后会在自己票箱中更新该服务器的选票。 + +(3)**发送初始化选票** - 每个服务器最开始都是通过广播把票投给自己。 + +(4)**接收外部投票** - 服务器会尝试从其它服务器获取投票,并记入自己的投票箱内。如果无法获取任何外部投票,则会确认自己是否与集群中其它服务器保持着有效连接。如果是,则再次发送自己的投票;如果否,则马上与之建立连接。 + +(5)**判断选举轮次** - 收到外部投票后,首先会根据投票信息中所包含的 logicClock 来进行不同处理 + +- 外部投票的 logicClock 大于自己的 logicClock。说明该服务器的选举轮次落后于其它服务器的选举轮次,立即清空自己的投票箱并将自己的 logicClock 更新为收到的 logicClock,然后再对比自己之前的投票与收到的投票以确定是否需要变更自己的投票,最终再次将自己的投票广播出去。 +- 外部投票的 logicClock 小于自己的 logicClock。当前服务器直接忽略该投票,继续处理下一个投票。 +- 外部投票的 logickClock 与自己的相等。当时进行选票 PK。 + +(6)**选票 PK** - 选票 PK 是基于`(self_id, self_zxid)` 与 `(vote_id, vote_zxid)` 的对比 + +- 外部投票的 logicClock 大于自己的 logicClock,则将自己的 logicClock 及自己的选票的 logicClock 变更为收到的 logicClock +- 若 logicClock 一致,则对比二者的 vote_zxid,若外部投票的 vote_zxid 比较大,则将自己的票中的 vote_zxid 与 vote_myid 更新为收到的票中的 vote_zxid 与 vote_myid 并广播出去,另外将收到的票及自己更新后的票放入自己的票箱。如果票箱内已存在(self_myid, self_zxid)相同的选票,则直接覆盖 +- 若二者 vote_zxid 一致,则比较二者的 vote_myid,若外部投票的 vote_myid 比较大,则将自己的票中的 vote_myid 更新为收到的票中的 vote_myid 并广播出去,另外将收到的票及自己更新后的票放入自己的票箱 + +(7)**统计选票** - 如果已经确定有过半服务器认可了自己的投票(可能是更新后的投票),则终止投票。否则继续接收其它服务器的投票。 + +(8)**更新服务器状态** - 投票终止后,服务器开始更新自身状态。若过半的票投给了自己,则将自己的服务器状态更新为 LEADING,否则将自己的状态更新为 FOLLOWING + +通过以上流程分析,我们不难看出:要使 Leader 获得多数 Server 的支持,则 **ZooKeeper 集群节点数必须是奇数。且存活的节点数目不得少于 `N + 1`** 。 + +每个 Server 启动后都会重复以上流程。在恢复模式下,如果是刚从崩溃状态恢复的或者刚启动的 server 还会从磁盘快照中恢复数据和会话信息,zk 会记录事务日志并定期进行快照,方便在恢复时进行状态恢复。 + +## 原子广播(Atomic Broadcast) + +**ZooKeeper 通过副本机制来实现高可用**。 + +那么,ZooKeeper 是如何实现副本机制的呢?答案是:ZAB 协议的原子广播。 + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_3.png) + +ZAB 协议的原子广播要求: + +**_所有的写请求都会被转发给 Leader,Leader 会以原子广播的方式通知 Follow。当半数以上的 Follow 已经更新状态持久化后,Leader 才会提交这个更新,然后客户端才会收到一个更新成功的响应_**。这有些类似数据库中的两阶段提交协议。 + +在整个消息的广播过程中,Leader 服务器会每个事务请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播。 + +> ZAB 是通过“一切以领导者为准”的强领导者模型和严格按照顺序提交日志,来实现操作的顺序性的,这一点和 Raft 是一样的。 + +# 参考资料 + +- [**A Simple Totally Ordered Broadcast Protocol**](https://diyhpl.us/~bryan/papers2/distributed/distributed-systems/zab.totally-ordered-broadcast-protocol.2008.pdf) - 概述 ZooKeeper 的全序广播协议(Zab) +- [ZooKeeper 简介及核心概念](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper%E7%AE%80%E4%BB%8B%E5%8F%8A%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5.md) +- [详解分布式协调服务 ZooKeeper](https://draveness.me/zookeeper-chubby) +- [Introduction to Apache ZooKeeper](https://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\347\256\200\344\273\213.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\347\256\200\344\273\213.md" new file mode 100644 index 0000000000..d79e20eb62 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\347\256\200\344\273\213.md" @@ -0,0 +1,245 @@ +--- +title: 分布式简介 +date: 2021-11-08 08:15:33 +categories: + - 分布式 + - 分布式综合 +tags: + - 分布式 + - 指标 + - 挑战 + - 超时检测 + - NTP + - 逻辑时钟 +permalink: /pages/f6860206/ +--- + +# 分布式简介 + +## 分布式系统的发展历程 + +罗马不是一天建成的,同理,现代分布式系统架构也不是一蹴而就的,而是逐步发展的演化过程。随着业务的不断发展,用户体量的增加,系统的复杂度势必不断攀升,最终迫使系统架构进化,以应对挑战。 + +了解分布式系统架构的演化过程,有利于我们了解架构进化的发展规律和业界一些成熟的应对方案。帮助我们在实际工作中,如何去思考架构,如何去凝练解决方案。 + +### 单机架构 + +- **场景**:网站运营初期,访问用户少,一台服务器绰绰有余。 +- **特征**:**应用程序、数据库、文件等所有的资源都在一台服务器上。** +- **描述**:通常服务器操作系统使用 linux,应用程序使用 PHP 开发,然后部署在 Apache 上,数据库使用 Mysql,通俗称为 LAMP。汇集各种免费开源软件以及一台廉价服务器就可以开始系统的发展之路了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200702718.png) + +### 应用服务和数据服务分离 + +- **场景**:越来越多的用户访问导致性能越来越差,越来越多的数据导致存储空间不足,一台服务器已不足以支撑。 +- **特征**:**应用服务器、数据库服务器、文件服务器分别独立部署。** +- **描述**:三台服务器对性能要求各不相同: + - 应用服务器要处理大量业务逻辑,因此需要更快更强大的 CPU; + - 数据库服务器需要快速磁盘检索和数据缓存,因此需要更快的硬盘和更大的内存; + - 文件服务器需要存储大量文件,因此需要更大容量的硬盘。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200703123.png) + +### 使用缓存改善性能 + +- **场景**:随着用户逐渐增多,数据库压力太大导致访问延迟。 +- **特征**:由于网站访问和财富分配一样遵循二八定律:_80% 的业务访问集中在 20% 的数据上_。**将数据库中访问较集中的少部分数据缓存在内存中,可以减少数据库的访问次数,降低数据库的访问压力。** +- **描述**:缓存分为两种:应用服务器上的本地缓存和分布式缓存服务器上的远程缓存。 + - 本地缓存访问速度更快,但缓存数据量有限,同时存在与应用程序争用内存的情况。 + - 分布式缓存可以采用集群方式,理论上可以做到不受内存容量限制的缓存服务。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200705172.png) + +### 负载均衡 + +- **场景**:使用缓存后,数据库访问压力得到有效缓解。但是单一应用服务器能够处理的请求连接有限,在访问高峰期,成为瓶颈。 +- **特征**:**多台服务器通过负载均衡同时向外部提供服务,解决单一服务器处理能力和存储空间不足的问题。** +- **描述**:使用集群是系统解决高并发、海量数据问题的常用手段。通过向集群中追加资源,提升系统的并发处理能力,使得服务器的负载压力不再成为整个系统的瓶颈。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200704005.png) + +### 数据库读写分离 + +- **场景**:网站使用缓存后,使绝大部分数据读操作访问都可以不通过数据库就能完成,但是仍有一部分读操作和全部的写操作需要访问数据库,在网站的用户达到一定规模后,数据库因为负载压力过高而成为网站的瓶颈。 +- **特征**:目前大部分的主流数据库都提供主从热备功能,通过配置两台数据库主从关系,可以将一台数据库服务器的数据更新同步到一台服务器上。**网站利用数据库的主从热备功能,实现数据库读写分离,从而改善数据库负载压力。** +- **描述**:应用服务器在写操作的时候,访问主数据库,主数据库通过主从复制机制将数据更新同步到从数据库。这样当应用服务器在读操作的时候,访问从数据库获得数据。为了便于应用程序访问读写分离后的数据库,通常在应用服务器端使用专门的数据访问模块,使数据库读写分离的对应用透明。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200707199.png) + +### 多级缓存 + +- **场景**:中国网络环境复杂,不同地区的用户访问网站时,速度差别也极大。 +- **特征**:**采用 CDN 和反向代理加快系统的静态资源访问速度。** +- **描述**:CDN 和反向代理的基本原理都是缓存,区别在于: + - CDN 部署在网络提供商的机房,使用户在请求网站服务时,可以从距离自己最近的网络提供商机房获取数据; + - 而反向代理则部署在网站的中心机房,当用户请求到达中心机房后,首先访问的服务器时反向代理服务器,如果反向代理服务器中缓存着用户请求的资源,就将其直接返回给用户。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200710745.png) + +### 业务拆分 + +- **场景**:大型网站的业务场景日益复杂,分为多个产品线。 +- **特征**:采用分而治之的手段将整个网站业务分成不同的产品线。**系统上按照业务进行拆分改造,应用服务器按照业务区分进行分别部署。** +- **描述**:应用之间可以通过超链接建立关系,也可以通过消息队列进行数据分发,当然更多的还是通过访问同一个数据存储系统来构成一个关联的完整系统。 + - **纵向拆分**:**将一个大应用拆分为多个小应用**,如果新业务较为独立,那么就直接将其设计部署为一个独立的 Web 应用系统。纵向拆分相对较为简单,通过梳理业务,将较少相关的业务剥离即可。 + - **横向拆分**:**将复用的业务拆分出来,独立部署为分布式服务**,新增业务只需要调用这些分布式服务横向拆分需要识别可复用的业务,设计服务接口,规范服务依赖关系。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200710835.png) + +### 分库分表 + +- **场景**:随着大型网站业务持续增长,数据库经过读写分离,从一台服务器拆分为两台服务器,依然不能满足需求。 +- **特征**:**数据库采用分布式数据库。** +- **描述**:分布式数据库是数据库拆分的最后方法,只有在单表数据规模非常庞大的时候才使用。不到不得已时,更常用的数据库拆分手段是业务分库,将不同的业务数据库部署在不同的物理服务器上。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200711984.png) + +### 分布式组件 + +- **场景**:随着网站业务越来越复杂,对数据存储和检索的需求也越来越复杂。 +- **特征**:**系统引入 NoSQL 数据库及搜索引擎。** +- **描述**:NoSQL 数据库及搜索引擎对可伸缩的分布式特性具有更好的支持。应用服务器通过统一数据访问模块访问各种数据,减轻应用程序管理诸多数据源的麻烦。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200711267.png) + +### 微服务 + +- **场景**:随着业务越拆越小,存储系统越来越庞大,应用系统整体复杂程度呈指数级上升,部署维护越来越困难。由于所有应用要和所有数据库系统连接,最终导致数据库连接资源不足,拒绝服务。 +- **特征**:**公共业务提取出来,独立部署。由这些可复用的业务连接数据库,通过分布式服务提供共用业务服务。** +- 描述:大型网站的架构演化到这里,基本上大多数的技术问题都得以解决,诸如跨数据中心的实时数据同步和具体网站业务相关的问题也都可以组合改进现有技术架构来解决。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200711681.png) + +## 分布式指标 + +**分布式系统的目标是提升系统的整体性能和吞吐量,另外还要尽量保证分布式系统的容错性**。 + +由分布式系统的目标很容易得出分布式系统的关键指标:性能、可用性、可扩展性。这些指标,正对应着耳熟能详的分布式系统“三高”特性——高并发、高性能、高可用。 + +### 性能(Performance) + +性能用于衡量一个系统处理各种任务的能力。 + +常见的性能指标有: + +- **吞吐量(Throughput)** - 系统在一定时间内可以处理的任务数。常见的吞吐量指标有: + - **QPS** - Queries Per Second 的缩写,即每秒查询数。 + - **TPS** - Transactions Per Second 的缩写,即每秒事务数。 +- **响应时间(Response Time)** - 执行一个请求从开始到最后收到响应数据所花费的总体时间,即从客户端发起请求到收到服务器响应结果的时间。 +- **并发数(Concurrency)** - 并发数是指系统能同时处理请求的数量,这个也反映了系统的负载能力。并发意味着可以同时进行多个处理。并发在现代编程中无处不在,网络中有多台计算机同时存在,一台计算机上同时运行着多个应用程序。 + +以上三个指标的关系大致为: + +``` +QPS(TPS)= 并发数 / 平均响应时间 +并发数 = QPS(TPS) * 平均响应时间 +``` + +### 可用性(Availability) + +**可用性**:指的是系统在面对各种异常时可以正确提供服务的能力。 + +系统的可用性可以用**系统停止服务的时间与总的时间之比衡量。** + +行业内一般用几个 9 表示可用性指标,对应用的可用性程度一般衡量标准有三个 9 到五个 9;一般我们的系统至少要到 4 个 9(99.99%)的可用性才能谈得上高可用。 + +| 可用性 | 年故障时间 | +| :------: | :----------------: | +| 99.9999% | 32 秒 | +| 99.999% | 5 分 15 秒 | +| 99.99% | 52 分 34 秒 | +| 99.9% | 8 小时 46 分 | +| 99% | 3 天 15 小时 36 分 | + +而所谓的高可用,就是:**在任何情况下,让服务尽最大可能对外提供服务**。 + +### 可扩展性(Scalability) + +**可扩展性(Scalability)**指的是分布式系统通过扩展集群机器规模提高系统性能 (吞吐、响应时间、 完成时间)、存储容量、计算能力的特性,是分布式系统的特有性质。 + +系统扩展可以分为垂直扩展、水平扩展。 + +- **垂直扩展**,即**提升单机的硬件处理能力**,比如 CPU 处理能力,内存容量,磁盘等方面。但是,单机是有性能瓶颈的,一旦触及瓶颈,再想提升,付出的成本和代价会极高。通俗来说,就三个字:**得加钱**! +- **水平扩展**:采用分而治之的思想,通过集群来分担吞吐量。集群中的应用机器(节点)通常被设计成无状态,用户可以请求任何一个节点,这些节点共同分担访问压力。水平扩展有两个要点: + - **集群化、分区化**:将一个完整的应用化整为零,如果是无状态应用,可以直接集群化部署;如果是有状态应用,可以将状态数据分区(分片),然后部署到多台机器上。 + - **负载均衡**:集群化、分区化后,要解决的问题是,请求应该被分发(寻址)到哪台机器上。这就需要通过某种策略来控制分发,这种技术就是负载均衡。 + +## 分布式系统分类 + +分布式技术错综复杂、知识庞杂,且各种技术相互耦合,所以不容易划分层次。 + +从应用的维度来看,大致可以将分布式系统分为以下四类: + +- **分布式计算**:解决应用的分布式计算问题。基于分布式计算模式,包括批处理计算、离线计算、在线计算、融合计算等,根据应用类型构建高效智能的分布式计算框架。 +- **分布式存储**:解决数据的分布式和多元化问题。包括分布式数据库、分布式文件系统、分布式缓存等,支持不同类型的数据的存储和管理。 +- **分布式通信**:解决进程间的分布式通信问题。通过消息队列、远程调用等方式,实现简单高效的通信。 +- **分布式资源管理**:解决资源的分布式和异构性问题。将 CPU、内存、IO 等物理资源虚拟化,新城逻辑资源池,以便统一管理。 + +此外,分布式系统都需要面对一些共性问题,可以视为分布式系统技术的基石: + +- **分布式协同** - 解决分布式状态及数据一致性的问题。代表技术:分布式互斥、分布式共识、分布式选举、分布式选举等。 +- **分布式调度** - 解决分布式系统资源、请求分配调度的问题。代表技术:服务注册和发现、服务路由、负载均衡、流量控制等。 +- **分布式容错** - 解决分布式系统中故障分析、处理的问题,保证系统整体可靠性。代表技术:链路追踪、故障隔离、故障转移等。 +- **分布式部署** - 解决分布式系统部署问题。代表技术:CI/CD、容器化等。 + +## 分布式系统的挑战 + +当程序运行在单机上时,通常会以一种可预测的方式运行:要么正常,要么异常。 + +一旦程序运行在多台机器上时,面临的场景就会变得复杂而难以预料。在分布式系统中,系统的某些部分可能会出现不可预知的故障,这被称为**部分失效(partial failure)**。问题的难点就在于部分失效是**不确定性的**。你甚至不确定请求是否成功了,因为消息通过网络传播的时间也是不确定的!这种不确定性和部分失效的可能性,使得分布式系统难以工作。 + +> 扩展阅读:[**The Eight Fallacies of Distributed Computing - Tech Talk**](https://web.archive.org/web/20171107014323/http://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/) 一文中提出了分布式系统新手常有的 8 种误区。 + +为什么我们要深刻地认识这 8 个错误?这是因为,我们需要清楚地认识到——**在分布式系统中,故障是不可避免的**。因此,如果要构建一个可靠的分布式系统,就必须要建立容错机制。很可能大部分组件在大部分时间都正常工作。然而,迟早会有一部分系统出现故障,软件必须以某种方式处理。故障处理必须是软件设计的一部分,并且作为软件的运维,你需要知道在发生故障的情况下,软件可能会表现出怎样的行为。 + +## 不可靠的网络 + +互联网以及大多数数据中心的内部网络(通常是以太网)都是异步网络。当通过网络发送数据包时,数据包可能会丢失或者延迟;同样,回复也可能会丢失或延迟。所以如果没有收到回复,并不能确定消息是否发送成功。传输的过程中,可能有各种各样的问题: + +1. 请求可能已经丢失(可能是被拔掉了网线)。 +2. 请求可能正在某个队列中等待,无法马上发送(可能是网络或接收方已经超负荷)。 +3. 远程接收节点可能已经失效(可能是崩愤或关机)。 +4. 远程节点可能暂时无法响应(例如正在运行长时间的垃圾回收)。 +5. 远程接收节点已经完成了请求处理,但回复却在网络中丢失(例如网络交换机配置错误)。 +6. 远程接收节点已经完成了请求处理,但回复却被延迟处理(例如网络或者发送者的机器过载)。 + +![如果发送请求并没有得到响应,则无法区分(a)请求是否丢失,(b)远程节点是否关闭,或(c)响应是否丢失](https://github.com/Vonng/ddia/raw/master/img/fig8-1.png) + +在大多数情况下,系统并没有准确判断节点是否发生故障的机制。因此,分布式系统中,一般通过**超时检测**来判断远程节点是否可用。但是,超时无法区分网络和节点故障,且可变的网络延迟有时会导致节点被误认为发生崩溃。 + +超时检测的一个关键点是超时大小的设置: + +- 超时时间如果设置过大,意味着等待时间更久,才能判定节点失效(在此期间,用户只能等待或拿到错误信息)。 + +- 超时时间如果设置过小,虽然可以更快检测故障,但增加了误判的可能——节点可能实际上是活着的。当一个节点被宣告为失效,其承担的职责要交给到其他节点,这个过程会给其他节点以及网络带来额外负担,特别是如果此时系统已经处于高负荷状态。 + +对此,可以先设置一个经验值,然后通过实验逐步调整:先在多台机器上,多次测量往返时间,以确定延迟的大概范围;然后结合应用特点,在故障检测与过早超时风险之间选择一个合适的中间值。更好的做法是:持续测量响应时间及其变化(抖动),然后根据最新的响应时间分布来动态调整。 + +## 不可靠的时钟 + +时钟和计时非常重要。有许多应用程序以各种方式依赖于时钟,例如: + +1. 某个请求是否超时了? +2. 某项服务的 99 %的响应时间是多少? +3. 在过去的五分钟内,服务平均每秒处理多少个查询? +4. 用户在我们的网站上浏览花了多段时间? +5. 这篇文章什么时候发表? +6. 在什么时间发送提醒邮件? +7. 这个缓存条目何时过期? +8. 日志文件中错误消息的时间戳是多少? + +在分布式系统中,时间总是件棘手的问题,由于跨节点通信不可能即时完成,消息经由网络从一台机器到另一台机器总是需要花费时间。收到消息的时间应该晚于发送的时间,但是由于网络的不确定延迟,精确测量面临着很多挑战。这些情况使得多节点通信时很难确定事情发生的先后顺序。 + +为了保证每台机器的时间同步,最常用的机制是 **网络时间协议(Network Time Protocol, NTP)**,它可以根据一组专门的时间服务器来调整本地时间。需要注意的是,即使使用了 NTP 进行时间同步,但是依然会存在一些误差:一方面受限于 NTP 本身的同步精度,此外还受限于网络通信的延迟。 + +如果想要保证时序,另一种方案是采用逻辑时钟。**逻辑时钟(logic clock)**是基于递增计数器,对于排序事件来说是更安全的选择。逻辑时钟仅测量事件的相对顺序(无论一个事件发生在另一个事件之前还是之后)。 + +## 参考资料 + +- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 +- [The Eight Fallacies of Distributed Computing - Tech Talk](https://web.archive.org/web/20171107014323/http://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/) - 分布式系统新手常犯的 8 个错误,并探讨了其会带来的影响。 +- [Distributed Systems for Fun and Profit](http://book.mixu.net/distsys/) - 一本学习小册,涵盖了分布式系统中的关键问题,包括时间的作用和不同的复制策略。 +- [A Note on Distributed Systems](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.41.7628&rep=rep1&type=pdf) - 这是一篇经典的论文,讲述了为什么在分布式系统中,远程交互不能像本地对象那样进行。 +- [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf),[**译文**](https://cloud.tencent.com/developer/article/1163428),[**解读**](https://zhuanlan.zhihu.com/p/56146800) - Lamport 介绍 happened before、偏序关系(partial ordering)、逻辑时钟(Logical Clocks)概念,提出解决分布式系统中区分事件发生的时序问题的方法。 +- [**Virtual Time and Global States of Distributed Systems**](http://courses.csail.mit.edu/6.852/01/papers/VirtTime_GlobState.pdf),[**解读**](https://zhuanlan.zhihu.com/p/56886156) - 逻辑时钟无法描述事件的因果关系。本文提出了向量时钟,这种算法利用了向量这种数据结构将全局各个进程的逻辑时间戳广播给各个进程,通过向量时间戳就能够比较任意两个事件的因果关系。 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..42ba8ea1e4 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210\351\235\242\350\257\225.md" @@ -0,0 +1,459 @@ +--- +title: 分布式综合面试 +date: 2021-11-08 08:15:33 +categories: + - 分布式 + - 分布式综合 +tags: + - 分布式 + - 指标 + - 挑战 + - 超时检测 + - NTP + - 逻辑时钟 +permalink: /pages/5b3ad94d/ +--- + +# 分布式综合面试 + +## 逻辑时钟 + +> 扩展: +> +> - [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf),[**译文**](https://cloud.tencent.com/developer/article/1163428),[**解读**](https://zhuanlan.zhihu.com/p/56146800) - Lamport 介绍 happened before、偏序关系(partial ordering)、逻辑时钟(Logical Clocks)概念,提出解决分布式系统中区分事件发生的时序问题的方法。 +> - [**Virtual Time and Global States of Distributed Systems**](http://courses.csail.mit.edu/6.852/01/papers/VirtTime_GlobState.pdf),[**解读**](https://zhuanlan.zhihu.com/p/56886156) - 逻辑时钟无法描述事件的因果关系。本文提出了向量时钟,这种算法利用了向量这种数据结构将全局各个进程的逻辑时间戳广播给各个进程,通过向量时间戳就能够比较任意两个事件的因果关系。 +> - [逻辑时钟 - 如何刻画分布式中的事件顺序](https://writings.sh/post/logical-clocks) + +### 【初级】为什么需要逻辑时钟? + +:::details 要点 + +**经典问题** + +- 为什么需要逻辑时钟? +- 分布式系统中以系统时间来确定事件顺序有什么问题? + +**知识点** + +**不同节点的物理时钟无法完全保持一致**。即使引入一个全局时钟(例如:NTP)来进行校准,由于网络通信延迟的不确定性,以及时钟计时的偏差,无法保证每个节点的时间完全一致。 + +在分布式系统中,由于网络通信延迟的不确定性, **仅仅以接收顺序作为整个分布式系统中事件的发生顺序是不可取的**。 + +::: + +### 【中级】什么是偏序?什么是全序? + +:::details 要点 + +全序和偏序是数学上的术语,按照数学内容阐述比较晦涩,简单来说: + +- **偏序**是部分可比较的有序关系。 +- **全序**是在偏序基础上,要求全部元素必须可比较的有序关系。 + +::: + +### 【高级】什么是逻辑时钟? + +:::details 要点 + +**经典问题** + +- 什么是逻辑时钟? +- 逻辑时钟是如何工作的? + +**知识点** + +1978 年,Lamport 在 [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf) 中提出了逻辑时钟的概念,来解决分布式系统中区分事件发生的时序问题。 + +**逻辑时钟并不度量时间本身,仅区分事件发生的前后顺序**。 + +分布式系统中按是否存在节点交互可分为三类事件,一类发生于节点内部,二是发送事件,三是接收事件。Lamport 时间戳原理如下: + +![Lamport timestamps space time (图片来源: wikipedia)_](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170810350.webp) + +1. 每个事件对应一个 Lamport 计数器,初始值为 0 +2. 如果事件在节点内发生,计数器加 1 +3. 如果事件属于发送事件,计数器加 1 并在消息中带上该计数器 +4. 如果事件属于接收事件,计数器 = Max(本地计数器,消息中的计数器) + 1 + +综上,**Lamport 逻辑时钟构建了一个全序时钟来描述事件顺序**。 + +**Lamport 逻辑时钟的缺陷是无法描述同时发生的事件**。 + +> 扩展: +> +> [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf) + +::: + +### 【高级】什么是向量时钟? + +:::details 要点 + +**向量时钟**其实是在逻辑时钟的基础上进行了演进,算法逻辑类似,只是不仅记录了本节点的时间戳,还记录了其他节点的时间戳。其本质在于**将逻辑时钟的全序计数器改造为向量时钟的偏序大小关系**:向量有序,则事件有序;向量平行,则事件并发。 + +![Vector clock space time (图片来源: wikipedia)](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170811135.webp) + +**向量时钟可以发现数据冲突,但不能解决数据冲突**。 + +::: + +### 【高级】什么是版本向量时钟? + +:::details 要点 + +在向量时钟算法中, 消息传播后,发送方的向量一定会小于接收者的向量, 是因为接收者对齐了发送者的原因。 + +版本向量在此基础上,做了一点加强:消息传播后,发送方也对齐接收者的向量,也就是双向对齐,在版本向量中,叫做**同步**。 + +发送消息和接收消息的时候不再自增向量中的自己的计数器,而是只做双方的向量对齐操作。 也就是,**只有在更新数据的时候做向量自增**。 + +::: + +## 一致性 + +### 【初级】什么是强一致性?什么是弱一致性?什么是最终一致性? + +:::details 要点 + +**一致性(Consistency)**指的是**多个数据副本是否能保持一致**的特性。 + +数据一致性又可以分为以下几点: + +- **强一致性** - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。通俗的说,分布式系统在执行写操作成功后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 +- **弱一致性** - 系统在写入数据成功后,不承诺立即能读到最新的值,也不承诺什么时候能读到,但是过一段时间之后用户可以看到更新后的值。那么用户读不到最新数据的这段时间被称为“不一致窗口时间”。 +- **最终一致性** - 最终一致性作为弱一致性中的特例,强调的是所有数据副本,在经过一段时间的同步后,最终能够到达一致的状态,不需要实时保证系统数据的强一致性。 + +::: + +### 【初级】什么是 ACID? + +:::details 要点 + +那么,什么是 ACID 特性呢?ACID 是数据库事务正确执行的四个基本要素的单词缩写: + +- **原子性(Atomicity)** + - 原子是指不可分解为更小粒度的东西。事务的原子性意味着:**事务中的所有操作要么全部成功,要么全部失败**。 + - 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 + - ACID 中的原子性并不关乎多个操作的并发性,它并没有描述多个线程试图访问相同的数据会发生什么情况,后者其实是由 ACID 的隔离性所定义。 +- **一致性(Consistency)** + - 数据库在事务执行前后都保持一致性状态。 + - 在一致性状态下,所有事务对一个数据的读取结果都是相同的。 + - 一致性本质上要求应用层来维护状态一致(或者恒等),应用程序有责任正确地定义事务来保持一致性。这不是数据库可以保证的事情。 +- **隔离性(Isolation)** + - **同时运行的事务互不干扰**。换句话说,一个事务所做的修改在最终提交以前,对其它事务是不可见的。 +- **持久性(Durability)** + - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 + - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 + +一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性。 + +- 只有满足一致性,事务的执行结果才是正确的。 +- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 +- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 +- 事务满足持久化是为了能应对系统崩溃的情况。 + +::: + +## CAP & BASE + +> 扩展: +> +> - [**Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services**](https://www.comp.nus.edu.sg/~gilbert/pubs/BrewersConjecture-SigAct.pdf),[**解读**](https://juejin.cn/post/6844903936718012430) - 经典的 CAP 定理,即:在一个分布式系统中,当发生网络分区时,那么强一致性和可用性只能二选一。 +> - [**CAP Twelve Years Later: How the “Rules” Have Changed**](https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/), [**解读**](https://www.zhihu.com/question/64778723/answer/224266038) - CAP 定理的新解读,并阐述 CAP 定理的一些常见误区。 +> - [**BASE: An Acid Alternative**](https://www.semanticscholar.org/paper/BASE%3A-An-Acid-Alternative-Pritchett/2e72e6c022dd33115304ecfcb6dad7ea609534a4),[**译文**](https://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html) - BASE 定理是对 CAP 中一致性和可用性的权衡,提出采用适当的方式来使系统达到最终一致性。 + +### 【中级】什么是 CAP 定理? + +:::details 要点 + +CAP 定理提出:分布式系统有三个指标,这三个指标不能同时做到: + +- **一致性(Consistency)** - 在任何给定时间,网络中的所有节点都具有完全相同(最近)的值。 +- **可用性(Availability)** - 对网络的每个请求都会返回响应,但不能保证返回的数据是最新的。 +- **分区容错性(Partition Tolerance)** - 即使任意数量的节点出现故障,网络仍会继续运行。 + +CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405160639643.png) + +在分布式系统中,分区容错性是一个既定的事实:因为分布式系统总会出现各种各样的问题,如由于网络原因而导致节点失联;发生机器故障;机器重启或升级等等。因此,**CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡**。 + +> 扩展:[**Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services**](https://www.comp.nus.edu.sg/~gilbert/pubs/BrewersConjecture-SigAct.pdf),[**解读**](https://juejin.cn/post/6844903936718012430) - 经典的 CAP 定理,即:在一个分布式系统中,当发生网络分区时,那么强一致性和可用性只能二选一。 + +::: + +### 【中级】选择 CP 还是 AP? + +:::details 要点 + +在分布式系统中,分区容错性是一个既定的事实:因为分布式系统总会出现各种各样的问题,如由于网络原因而导致节点失联;发生机器故障;机器重启或升级等等。因此,**CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡**。 + +- 选择 **AP 模式**,偏向于保证服务的高可用性。用户访问系统的时候,都能得到响应数据,不会出现响应错误;但是,当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。 +- 选择 **CP 模式**,一旦因为消息丢失、延迟过高发生了网络分区,就会影响用户的体验和业务的可用性。因为为了防止数据不一致,系统将拒绝新数据的写入。 + +一个最具代表性的问题是:服务注册中心应该选择 AP 还是 CP? + +在微服务架构下,服务注册和服务发现机制中主要有三种角色: + +- **服务提供者**(RPC Server / Provider) +- **服务消费者**(RPC Client / Consumer) +- **服务注册中心**(Registry) + +**注册中心**负责协调服务注册和服务发现,显然它是核心中的核心。主流的注册中心有很多,如:ZooKeeper、Nacos、Eureka、Consul、etcd 等。在针对注册中心进行技术选型时,其 CAP 设计也是一个比较的维度。 + +- CP 模型代表:ZooKeeper、etcd。系统强调数据的一致性,当数据一致性无法保证时(如:正在选举主节点),系统拒绝请求。 +- AP 模型代表:Nacos、Eureka。系统强调可用性,牺牲一定的一致性(即服务节点上的数据不保证是最新的),来保证整体服务可用。 + +对于服务注册中心而言,即使不同节点保存的服务注册信息存在差异,也不会造成灾难性的后果,仅仅是信息滞后而已。但是,如果为了追求数据一致性,使得服务发现短时间内不可用,负面影响更严重。所以,对于服务注册中心而言,可用性比一致性更重要,一般应该选择 AP 模型。 + +::: + +### 【中级】CAP 定理真的正确吗? + +:::details 要点 + +CAP 定理在分布式系统领域大名鼎鼎,以至于被很多人视为了真理。然而,CAP 定理真的正确吗? + +网络分区是一种故障,不管喜欢还是不喜欢,它都可能发生,所以无法选择或逃避分区的问题。在网络正常的时候,系统可以同时保证一致性(线性化)和可用性。而一旦发生了网络故障,必须要么选择一致性,要么选择可用性。因此,对 CAP 更准确的理解应该是:**当发生网络分区(P)的情况下,可用性(A)和一致性(C)二者只能选其一**。 + +CAP 定理所描述的模型实际上局限性很大,它只考虑了一种一致性模型和一种故障(网络分区故障),而没有考虑网络延迟、节点失效等情况。因此,它对于指导一个具体的分布式系统设计来说,没有太大的实际价值。 + +值得一提的是,在 CAP 定理提出十二年之后,其提出者也发表了一篇文章 [**CAP Twelve Years Later: How the “Rules” Have Changed**](https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/),来阐述 CAP 定理的局限性。 + +> 扩展:- [**CAP Twelve Years Later: How the “Rules” Have Changed**](https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/), [**解读**](https://www.zhihu.com/question/64778723/answer/224266038) - CAP 定理的新解读,并阐述 CAP 定理的一些常见误区。 + +::: + +### 【中级】什么是 BASE 定理? + +:::details 要点 + +BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State)`** 和 **`最终一致性(Eventually Consistent)`** 三个短语的缩写。BASE 定理是对 CAP 定理中可用性(A)和一致性(C)权衡的结果。 + +BASE 定理的**核心思想**是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过**牺牲强一致性来达到可用性**,通常运用在大型分布式系统中。 + + + +在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 + +> 扩展:- [**BASE: An Acid Alternative**](https://www.semanticscholar.org/paper/BASE%3A-An-Acid-Alternative-Pritchett/2e72e6c022dd33115304ecfcb6dad7ea609534a4),[**译文**](https://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html) - BASE 定理是对 CAP 中一致性和可用性的权衡,提出采用适当的方式来使系统达到最终一致性。 + +::: + +## Paxos + +> 扩展: +> +> - [Part-time Parliament 论文](https://research.microsoft.com/en-us/um/people/lamport/pubs/lamport-paxos.pdf) +> - [Paxos Made Simple 论文](https://lamport.azurewebsites.net/pubs/paxos-simple.pdf) +> - [Paxos 算法详解](https://zhuanlan.zhihu.com/p/31780743) +> - [Wiki - Paxos 算法](https://zh.wikipedia.org/w/index.php?title=Paxos%E7%AE%97%E6%B3%95) +> - [一致性算法(Paxos、Raft、Zab)](https://www.bilibili.com/video/BV1TW411M7Fx?from=search&seid=11524608198747599965) +> - [Raft 作者讲解 Paxos 视频](https://www.bilibili.com/video/av36556594) +> - [Paxos 算法讲解视频](https://www.youtube.com/watch?v=d7nAGI_NZPk) +> - [深入剖析共识性算法 Paxos](https://dunwu.github.io/waterdrop/pages/ea903d16/) + +### 【高级】Paxos 是怎样工作的? + +:::details 要点 + +**Paxos 是一种基于消息传递且具有容错性的共识性(consensus)算法**。 + +Paxos 算法运行在允许宕机故障的异步系统中,不要求可靠的消息传递,可容忍消息丢失、延迟、乱序以及重复。 + +Paxos 利用多数派 (Majority) 机制保证了一定的容错能力,即 `N` 个节点的系统最多允许 `N / 2 - 1` 个节点同时出现故障。 + +Paxos 算法包含 2 个部分: + +- **Basic Paxos 算法**:描述的是多节点之间如何就某个值达成共识。 +- **Multi Paxos 思想**:描述的是执行多个 Basic Paxos 实例,就一系列值达成共识。 + +#### Basic Paxos 算法 + +**Basic Paxos 是通过二阶段提交的方式来达成共识的**。 + +Paxos 将分布式系统中的节点分 Proposer、Acceptor、Learner 三种角色。 + +- **提议者(Proposer)**:发出提案(Proposal),用于投票表决。Proposal 信息包括提案编号 (Proposal ID) 和提议的值 (Value)。在绝大多数场景中,集群中收到客户端请求的节点,才是提议者。这样做的好处是,对业务代码没有入侵性,也就是说,我们不需要在业务代码中实现算法逻辑。 +- **接受者(Acceptor)**:对每个 Proposal 进行投票,若 Proposal 获得多数 Acceptor 的接受,则称该 Proposal 被批准。一般来说,集群中的所有节点都在扮演接受者的角色,参与共识协商,并接受和存储数据。 +- **学习者(Learner)**:不参与接受,从 Proposers/Acceptors 学习、记录最新达成共识的提案(Value)。一般来说,学习者是数据备份节点,比如主从架构中的从节点,被动地接受数据,容灾备份。 + +Paxos 算法有 3 个阶段,其中,前 2 个阶段负责协商并达成共识: + +1. **准备(Prepare)阶段**:Proposer 向 Acceptors 发出 Prepare 请求,Acceptors 针对收到的 Prepare 请求进行 Promise 承诺。 +2. **接受(Accept)阶段**:Proposer 收到多数 Acceptors 承诺的 Promise 后,向 Acceptors 发出 Propose 请求,Acceptors 针对收到的 Propose 请求进行 Accept 处理。 +3. **学习(Learn)阶段**:Proposer 在收到多数 Acceptors 的 Accept 之后,标志着本次 Accept 成功,决议形成,将形成的决议发送给所有 Learners。 + +#### Multi Paxos 思想 + +Basic Paxos 有以下问题,导致它不能应用于实际: + +- **Basic Paxos 算法只能对一个值形成决议**。 +- **Basic Paxos 算法会消耗大量网络带宽**。Basic Paxos 中,决议的形成至少需要两次网络通信,在高并发情况下可能需要更多的网络通信,极端情况下甚至可能形成活锁。如果想连续确定多个值,Basic Paxos 搞不定了。 + +Multi Paxos 基于 Basic Paxos 做了两点改进: + +- **针对每一个要确定的值,运行一次 Paxos 算法实例(Instance),形成决议**。每一个 Paxos 实例使用唯一的 Instance ID 标识。 +- **在所有 Proposer 中选举一个 Leader,由 Leader 唯一地提交 Proposal 给 Acceptor 进行表决**。这样没有 Proposer 竞争,解决了活锁问题。在系统中仅有一个 Leader 进行 Value 提交的情况下,Prepare 阶段就可以跳过,从而将两阶段变为一阶段,提高效率。 + +::: + +## Raft + +> 扩展: +> +> - [Raft 算法论文](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) +> - [Raft 算法论文译文](https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md) +> - [Raft 作者讲解视频](https://www.youtube.com/watch?v=YbZ3zDzDnrw&feature=youtu.be) +> - [Raft 作者讲解视频对应的 PPT](http://www2.cs.uh.edu/~paris/6360/PowerPoint/Raft.ppt) +> - [分布式系统的 Raft 算法](https://www.jdon.com/artichect/raft.html) +> - [Raft 算法详解](https://zhuanlan.zhihu.com/p/32052223) +> - [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) - 一个动画教程 +> - [The Raft Consensus Algorithm](https://raft.github.io/) - 一个交互式动画教程 +> - [深入剖析共识性算法 Raft](https://dunwu.github.io/waterdrop/pages/9386474c/) + +### 【高级】Raft 是怎样工作的? + +:::details 要点 + +**[Raft](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) 是一种为了管理日志复制的分布式共识性算法**。从本质上说,**Raft 算法是通过一切以领导者为准的方式,实现一系列值的共识和各节点日志的一致**。 + +[Raft](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) 出现之前,Paxos 一直是分布式共识性算法的标准。Paxos **难以理解,更难以实现**。Raft 的设计目标是简化 Paxos,使得算法**既容易理解,也容易实现**。 + +Raft 将一致性问题分解成了三个子问题: + +- **选举 Leader** +- **日志复制** +- **安全性** + +#### Raft 概念 + +**(1)服务器角色** + +在 Raft 中,任何时刻,每个服务器都处于这三个角色之一 : + +- **`Leader`** - 领导者,通常一个系统中是**一主(Leader)多从(Follower)**。Leader **负责处理所有的客户端请求**。 +- **`Follower`** - 跟随者,**不会发送任何请求**,只是简单的 **响应来自 Leader 或者 Candidate 的请求**。 +- **`Candidate`** - 参选者,选举新 Leader 时的临时角色。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131215742.png) + +**(2)任期** + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20200131220742.png) + +Raft 把时间分割成任意长度的 **_`任期(Term)`_**,任期用连续的整数标记。每一段任期从一次**选举**开始。**Raft 保证了在一个给定的任期内,最多只有一个领导者**。 + +**任期在 Raft 算法中充当逻辑时钟的作用,使得服务器节点可以查明一些过期的信息(比如过期的 Leader)。每个服务器节点都会存储一个当前任期号,这一编号在整个时期内单调的增长。当服务器之间通信的时候会交换当前任期号。** + +**(3)选举 Leader** + +**领导者心跳消息**:Raft 使用一种心跳机制来触发 Leader 选举。**Leader 需要周期性的向所有 Follower 发送心跳消息**,以此维持 Leader 身份。 + +**随机的竞选超时时间**:每个 Follower 都设置了一个**随机的竞选超时时间**,一般为 `150ms ~ 300ms`,如果在竞选超时时间内没有收到 Leader 的心跳消息,就会认为当前 Term 没有可用的 Leader,并发起选举来选出新的 Leader。开始一次选举过程,Follower 先要增加自己的当前 Term 号,并**转换为 Candidate**。 + +Candidate 会并行的**向集群中的所有服务器节点发送投票请求(`RequestVote RPC`)**,它会保持当前状态直到以下三件事情之一发生: + +- **自己成为 Leader** +- **其他的服务器成为 Leader** +- **没有任何服务器成为 Leader** + +Raft 算法通过:领导者心跳消息、随机选举超时时间、得到大多数选票才通过原则、任期最新者优先、先来先服务等投票原则,保证了一个任期只有一位领导,也极大地减少了选举失败的情况。 + +#### 日志复制 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170817072.png) + +1. Leader 负责处理所有客户端的请求。 +2. Leader 把请求作为日志条目加入到它的日志中,然后并行的向其他服务器发送 `AppendEntries RPC` 请求,要求 Follower 复制日志条目。 +3. Follower 复制成功后,返回确认消息。 +4. 当这个日志条目被半数以上的服务器复制后,Leader 提交这个日志条目到它的复制状态机,并向客户端返回执行结果。 + +#### 安全性 + +- **选举限制**:拥有最新的已提交的日志条目的 Follower 才有资格成为 Leader。 +- **提交旧任期的日志条目**:**Raft 永远不会通过计算副本数目的方式去提交一个之前 Term 内的日志条目**。 +- **日志压缩**:Raft 采用对整个系统进行快照来解决,快照之前的日志都可以丢弃。以此,避免日志无限膨胀,导致故障恢复过久。 + +::: + +## ZAB + +> 扩展: +> +> - [**A Simple Totally Ordered Broadcast Protocol**](https://diyhpl.us/~bryan/papers2/distributed/distributed-systems/zab.totally-ordered-broadcast-protocol.2008.pdf) - 概述 ZooKeeper 的全序广播协议(Zab) +> - [ZooKeeper 简介及核心概念](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Zookeeper%E7%AE%80%E4%BB%8B%E5%8F%8A%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5.md) +> - [详解分布式协调服务 ZooKeeper](https://draveness.me/zookeeper-chubby) +> - [Introduction to Apache ZooKeeper](https://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper) +> - [ZAB 协议](https://dunwu.github.io/waterdrop/pages/51168337/) + +### 【高级】ZAB 是怎样工作的? + +:::details 要点 + +ZAB 协议是 Zookeeper 专门设计的一种**支持故障恢复的原子广播协议**。 + +ZAB 协议是 ZooKeeper 的数据一致性和高可用解决方案。 + +ZAB 协议定义了两个可以**无限循环**的流程: + +- **`选举 Leader`** - 用于故障恢复,从而保证高可用。 +- **`原子广播`** - 用于主从同步,从而保证数据一致性。 + +#### 选举 Leader + +ZooKeeper 集群采用一主(称为 Leader)多从(称为 Follower)模式,主从节点通过副本机制保证数据一致。 + +- **如果 Follower 节点挂了** - ZooKeeper 集群中的每个节点都会单独在内存中维护自身的状态,并且各节点之间都保持着通讯,**只要集群中有半数机器能够正常工作,那么整个集群就可以正常提供服务**。 +- **如果 Leader 节点挂了** - 如果 Leader 节点挂了,系统就不能正常工作了。此时,需要通过 ZAB 协议的选举 Leader 机制来进行故障恢复。 + +ZAB 协议的选举 Leader 机制简单来说,就是:基于过半选举机制产生新的 Leader,之后其他机器将从新的 Leader 上同步状态,当有过半机器完成状态同步后,就退出选举 Leader 模式,进入原子广播模式。 + +#### 原子广播 + +**ZooKeeper 通过副本机制来实现高可用**。 + +那么,ZooKeeper 是如何实现副本机制的呢?答案是:ZAB 协议的原子广播。 + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_3.png) + +ZAB 协议的原子广播要求: + +**_所有的写请求都会被转发给 Leader,Leader 会以原子广播的方式通知 Follow。当半数以上的 Follow 已经更新状态持久化后,Leader 才会提交这个更新,然后客户端才会收到一个更新成功的响应_**。这有些类似数据库中的两阶段提交协议。 + +在整个消息的广播过程中,Leader 服务器会每个事务请求生成对应的 Proposal,并为其分配一个全局唯一的递增的事务 ID(ZXID),之后再对其进行广播。 + +::: + +## Gossip + +### 【高级】Gossip 是怎样工作的? + +> 扩展: +> +> - [Epidemic Algorithms for Replicated Database Maintenance](http://bitsavers.trailing-edge.com/pdf/xerox/parc/techReports/CSL-89-1_Epidemic_Algorithms_for_Replicated_Database_Maintenance.pdf) +> - [P2P 网络核心技术:Gossip 协议](https://zhuanlan.zhihu.com/p/41228196) +> - [INTRODUCTION TO GOSSIP](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) +> - [Goosip 协议仿真动画](https://flopezluis.github.io/gossip-simulator/) + +:::details 要点 + +Gossip 也叫 Epidemic Protocol (流行病协议),这个协议基于**最终一致性**以及**去中心化**设计思想。主要用于**分布式节点之间进行信息交换和数据同步**,这种场景的一个最大特点就是组成的网络的节点都是对等节点,是非结构化网络(去中心化)。 + +Gossip 过程是由种子节点发起,当一个种子节点有状态需要更新到网络中的其他节点时,它会随机的选择周围几个节点散播消息,收到消息的节点也会重复该过程,直至最终网络中所有的节点都收到了消息。这个过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个最终一致性协议。 + +Gossip 过程是异步的,也就是说发消息的节点不会关注对方是否收到,即不等待响应;不管对方有没有收到,它都会每隔 1 秒向周围节点发消息。**异步是它的优点,而消息冗余则是它的缺点**。 + +Goosip 协议的信息传播和扩散通常需要由种子节点发起。整个传播过程可能需要一定的时间,由于不能保证某个时刻所有节点都收到消息,但是理论上最终所有节点都会收到消息,因此它是一个**最终一致性**协议。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20210708234308.gif) + +Gossip 有两种类型: + +- **Anti-Entropy(反熵)**:**以固定的概率传播所有的数据**。反熵时通讯成本会很高,可以通过引入校验和等机制,降低需要对比的数据量和通讯消息等。反熵不适合动态变化或节点数比较多的分布式环境。 +- **Rumor-Mongering(谣言传播)**:**仅传播新到达的数据**。谣言传播模型指的是当一个节点有了新数据后,这个节点变成活跃状态,并周期性地联系其他节点向其发送新数据,直到所有的节点都存储了该新数据。在谣言传播模型下,消息可以发送得更频繁,因为消息只包含最新 update,体积更小。而且,一个谣言消息在某个时间点之后会被标记为 removed,并且不再被传播,因此,谣言传播模型下,系统有一定的概率会不一致。而由于,谣言传播模型下某个时间点之后消息不再传播,因此消息是有限的,系统开销小。 + +::: \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/10.\346\213\234\345\215\240\345\272\255\345\260\206\345\206\233\351\227\256\351\242\230.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\213\234\345\215\240\345\272\255\345\260\206\345\206\233\351\227\256\351\242\230.md" similarity index 96% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/10.\346\213\234\345\215\240\345\272\255\345\260\206\345\206\233\351\227\256\351\242\230.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\213\234\345\215\240\345\272\255\345\260\206\345\206\233\351\227\256\351\242\230.md" index eeb03d5b89..2c3328597e 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/10.\346\213\234\345\215\240\345\272\255\345\260\206\345\206\233\351\227\256\351\242\230.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\213\234\345\215\240\345\272\255\345\260\206\345\206\233\351\227\256\351\242\230.md" @@ -1,15 +1,13 @@ --- title: 拜占庭将军问题 date: 2021-11-08 08:15:33 -order: 10 categories: - 分布式 - - 分布式理论 + - 分布式综合 tags: - 分布式 - - 理论 - - 共识性 -permalink: /pages/a72fee/ + - 共识 +permalink: /pages/0bbdbc6b/ --- # 拜占庭将军问题 @@ -34,7 +32,7 @@ permalink: /pages/a72fee/ 上述的故事可以映射到分布式系统中,_将军代表分布式系统中的节点;信使代表通信系统;叛徒代表故障或异常_。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210704104211.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20210704104211.png) ## 问题分析 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\351\200\273\350\276\221\346\227\266\351\222\237.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\351\200\273\350\276\221\346\227\266\351\222\237.md" new file mode 100644 index 0000000000..6ad8f39788 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\351\200\273\350\276\221\346\227\266\351\222\237.md" @@ -0,0 +1,133 @@ +--- +title: 逻辑时钟 +date: 2024-04-28 22:02:18 +categories: + - 分布式 + - 分布式综合 +tags: + - 分布式 + - 物理时钟 + - 逻辑时钟 + - 向量时钟 + - 全序 + - 偏序 +permalink: /pages/6a1351bb/ +--- + +# 逻辑时钟 + +## 什么是逻辑时钟 + +1978 年,Lamport 在 [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf) 中提出了逻辑时钟的概念,来解决分布式系统中区分事件发生的时序问题。 + +**逻辑时钟指的是分布式系统中用于区分事件的发生顺序的时间机制**。 + +## 为什么需要逻辑时钟 + +对于程序来说,时间维度非常重要,很多业务逻辑都依赖于时间。常见的场景有: + +- 某个请求是否超时了? +- 某项服务 P99 的响应时间是多少? +- 在过去五分钟,服务平均每秒处理多少个查询? +- 用户在我们的网站上浏览花了多段时间? +- 这篇文章什么时候发表? +- 在什么时间发送提醒邮件? +- 这个缓存条目何时过期? +- 日志文件中错误消息的时间戳是多少? + +分布式系统,意味着整个系统中有多个节点。为了让多节点的系统时间保持同步,需要有一个对表机制,来保证各节点的时间一致。一种常见方法是使用 [NTP](https://en.wikipedia.org/wiki/Network_Time_Protocol),它的工作机制是使用专门的高精度时间服务器来作为基准,调整服务器的本地时间。即使使用了 NTP,也难免存在微小的误差,在有些场景中(如金融)是不能接受的。 + +**在分布式系统中,由于跨节点通信不可能即时完成,因此在多节点上难以确定事件的先后顺序**。而逻辑时钟就是一种定义时序先后顺序的方案。 + +## 全序和偏序 + +全序和偏序是集合论中的概念,用于描述集合中元素之间的关系。 + +### 什么是偏序 + +偏序是指集合中的元素之间存在一种关系,使得任意两个元素之间可能存在比较,但不一定所有元素都可以相互比较。这种关系不一定是传递的或者反对称的。例如,集合中的子集关系就是一个偏序关系,因为不是所有的子集都可以相互比较。 + +设 R 是集合 A 上的一个二元关系,若 R 满足: + +(1)自反性:对任意 `x∈A`,有 `xRx`; + +(2)反对称性(即[反对称关系](https://www.zhihu.com/search?q=反对称关系&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={"sourceType"%3A"answer"%2C"sourceId"%3A555298363})):对任意 `x,y∈A`,若 `xRy`,且 `yRx`,则 `x=y`; + +(3)传递性:对任意 `x,y,z∈A`,若 `xRy`,且 `yRz`,则 `xRz`。 + +则称 R 为 A 上的偏序关系。 + +### 什么是全序 + +全序是指集合中的元素之间存在一种关系,使得任意两个元素都可以进行比较,且这种比较关系是传递的,反对称的。换句话说,任意两个元素都可以比较大小,并且不会出现无法比较的情况。例如,实数集合上的小于等于关系就是一个全序关系。 + +设集合 X 上有一全序关系,如果我们把这种关系用 ≤ 表述,则下列陈述对于 X 中的所有 a, b 和 c 成立: + +如果 a ≤ b 且 b ≤ a 则 a = b([反对称性](https://www.zhihu.com/search?q=反对称性&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={"sourceType"%3A"answer"%2C"sourceId"%3A555298363})) + +如果 a ≤ b 且 b ≤ c 则 a ≤ c([传递性](https://www.zhihu.com/search?q=传递性&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={"sourceType"%3A"answer"%2C"sourceId"%3A555298363})) + +a ≤ b 或 b ≤ a (完全性) + +**注意**: + +完全性本身也包括了[自反性](https://www.zhihu.com/search?q=自反性&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={"sourceType"%3A"answer"%2C"sourceId"%3A555298363})。 所以,全序关系必是[偏序关系](https://www.zhihu.com/search?q=偏序关系&search_source=Entity&hybrid_search_source=Entity&hybrid_search_extra={"sourceType"%3A"answer"%2C"sourceId"%3A555298363})。 + +### 时序的关键 + +**两个事件可以建立因果(时序)关系的前提是:两个事件之间是否发生过信息传递。**在分布式系统中,进程间通信的手段(共享内存、消息发送等)都属于信息传递,如果两个进程间没有任何交互,实际上他们之间内部事件的时序也无关紧要。但是有交互的情况下,特别是多个节点的要保持同一副本的情况下,事件的时序非常重要。 + +## 逻辑时钟 + +分布式系统中按是否存在节点交互可分为三类事件,一类发生于节点内部,二是发送事件,三是接收事件。Lamport 时间戳原理如下: + +![Lamport timestamps space time (图片来源: wikipedia)_](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170810350.webp) + +1. 每个事件对应一个 Lamport 时间戳,初始值为 0 +2. 如果事件在节点内发生,时间戳加 1 +3. 如果事件属于发送事件,时间戳加 1 并在消息中带上该时间戳 +4. 如果事件属于接收事件,时间戳 = Max(本地时间戳,消息中的时间戳) + 1 + +假设有事件 a、b,C(a)、C(b)分别表示事件 a、b 对应的 Lamport 时间戳,如果 a->b,则 C(a) < C(b),a 发生在 b 之前(happened before),例如图 1 中有 C1 -> B1。通过该定义,事件集中 Lamport 时间戳不等的事件可进行比较,我们获得事件的[偏序关系](https://en.wikipedia.org/wiki/Partially_ordered_set#Formal_definition)(partial order)。 + +如果 C(a) = C(b),那 a、b 事件的顺序又是怎样的?假设 a、b 分别在节点 P、Q 上发生,Pi、Qj 分别表示我们给 P、Q 的编号,如果 C(a) = C(b) 并且 Pi < Qj,同样定义为 a 发生在 b 之前,记作 a => b。假如我们对图 1 的 A、B、C 分别编号 Ai = 1、Bj = 2、Ck = 3,因 C(B4) = C(C3) 并且 Bj < Ck,则 B4 => C3。 + +通过以上定义,我们可以对所有事件排序、获得事件的[全序关系](https://en.wikipedia.org/wiki/Total_order)(total order)。上图例子,我们可以从 C1 到 A4 进行排序。 + +## 向量时钟 + +Lamport 时间戳帮助我们得到事件顺序关系,但还有一种顺序关系不能用 Lamport 时间戳很好地表示出来,那就是同时发生关系(concurrent)(4)。例如图 1 中事件 B4 和事件 C3 没有因果关系,属于同时发生事件,但 Lamport 时间戳定义两者有先后顺序。 + +Vector clock 是在 Lamport 时间戳基础上演进的另一种逻辑时钟方法,它通过 vector 结构不但记录本节点的 Lamport 时间戳,同时也记录了其他节点的 Lamport 时间戳(5)(6)。Vector clock 的原理与 Lamport 时间戳类似,使用图例如下: + +![Vector clock space time (图片来源: wikipedia)](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170811135.webp) + +假设有事件 a、b 分别在节点 P、Q 上发生,Vector clock 分别为 Ta、Tb,如果 Tb[Q] > Ta[Q] 并且 Tb[P] >= Ta[P],则 a 发生于 b 之前,记作 a -> b。到目前为止还和 Lamport 时间戳差别不大,那 Vector clock 怎么判别同时发生关系呢? + +如果 Tb[Q] > Ta[Q] 并且 Tb[P] < Ta[P],则认为 a、b 同时发生,记作 a <-> b。例如图 2 中节点 B 上的第 4 个事件 (A:2,B:4,C:1) 与节点 C 上的第 2 个事件 (B:3,C:2) 没有因果关系、属于同时发生事件。 + +## 版本向量时钟 + +基于 Vector clock 我们可以获得任意两个事件的顺序关系,结果或为先后顺序或为同时发生,识别事件顺序在工程实践中有很重要的引申应用,最常见的应用是发现数据冲突(detect conflict)。 + +分布式系统中数据一般存在多个副本(replication),多个副本可能被同时更新,这会引起副本间数据不一致,Version vector 的实现与 Vector clock 非常类似,目的用于发现数据冲突。下面通过一个例子说明 Version vector 的用法: + +![Version Vector Clock](https://raw.githubusercontent.com/dunwu/images/master/snap/202405170812797.png) + +- client 端写入数据,该请求被 Sx 处理并创建相应的 vector ([Sx, 1]),记为数据 D1 +- 第 2 次请求也被 Sx 处理,数据修改为 D2,vector 修改为([Sx, 2]) +- 第 3、第 4 次请求分别被 Sy、Sz 处理,client 端先读取到 D2,然后 D3、D4 被写入 Sy、Sz +- 第 5 次更新时 client 端读取到 D2、D3 和 D4 3 个数据版本,通过类似 Vector clock 判断同时发生关系的方法可判断 D3、D4 存在数据冲突,最终通过一定方法解决数据冲突并写入 D5 + +Vector clock 只用于发现数据冲突,不能解决数据冲突。如何解决数据冲突因场景而异,具体方法有以最后更新为准(last write win),或将冲突的数据交给 client 由 client 端决定如何处理,或通过 quorum 决议事先避免数据冲突的情况发生(11)。 + +由于记录了所有数据在所有节点上的逻辑时钟信息,Vector clock 和 Version vector 在实际应用中可能面临的一个问题是 vector 过大,用于数据管理的元数据(meta data)甚至大于数据本(12)。 + +解决该问题的方法是使用 server id 取代 client id 创建 vector (因为 server 的数量相对 client 稳定),或设定最大的 size、如果超过该 size 值则淘汰最旧的 vector 信息(10)(13)。 + +## 参考资料 + +- [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf),[**译文**](https://cloud.tencent.com/developer/article/1163428),[**解读**](https://zhuanlan.zhihu.com/p/56146800) - Lamport 介绍 happened before、偏序关系(partial ordering)、逻辑时钟(Logical Clocks)概念,提出解决分布式系统中区分事件发生的时序问题的方法。 +- [**Virtual Time and Global States of Distributed Systems**](http://courses.csail.mit.edu/6.852/01/papers/VirtTime_GlobState.pdf),[**解读**](https://zhuanlan.zhihu.com/p/56886156) - 逻辑时钟无法描述事件的因果关系。本文提出了向量时钟,这种算法利用了向量这种数据结构将全局各个进程的逻辑时间戳广播给各个进程,通过向量时间戳就能够比较任意两个事件的因果关系。 +- [**分布式系统理论基础 - 时间、时钟和事件顺序**](https://zhuanlan.zhihu.com/p/23278509) +- https://writings.sh/post/logical-clocks diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/01.\345\210\206\345\270\203\345\274\217\345\237\272\347\241\200\347\220\206\350\256\272.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/01.\345\210\206\345\270\203\345\274\217\345\237\272\347\241\200\347\220\206\350\256\272.md" deleted file mode 100644 index da7c7277ae..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/01.\345\210\206\345\270\203\345\274\217\345\237\272\347\241\200\347\220\206\350\256\272.md" +++ /dev/null @@ -1,64 +0,0 @@ ---- -title: 分布式基础理论 -date: 2021-11-08 08:15:33 -order: 01 -categories: - - 分布式 - - 分布式理论 -tags: - - 分布式 - - 理论 -permalink: /pages/286bb3/ ---- - -# 分布式基础理论 - -## 分布式特性和分类 - -### 分布式特性 - -- **性能**:用于衡量一个系统处理各种任务的能力。 - - **吞吐量**:系统在一定时间内可以处理的任务数。 - - **QPS**,即每秒查询数 - - **TPS**,即每秒事务数 - - **响应时间**:系统响应一个请求或输入需要花费的时间。 -- **可用性**:指的是系统在面对各种异常时可以正确提供服务的能力。系统的可用性可以用系统停止服务的时间与总的时间之比衡量。 -- **可扩展性**:指的是分布式系统通过扩展集群机器规模提高系统性能 (吞吐、响应时间、 完成时间)、存储容量、计算能力的特性,是分布式系统的特有性质。 - -### 分布式分类 - -- **分布式计算**:解决应用的分布式计算问题。基于分布式计算模式,包括批处理计算、离线计算、在线计算、融合计算等,根据应用类型构建高效智能的分布式计算框架。 -- **分布式存储**:解决数据的分布式和多元化问题。包括分布式数据库、分布式文件系统、分布式缓存等,支持不同类型的数据的存储和管理。 -- **分布式通信**:解决进程间的分布式通信问题。通过消息队列、远程调用等方式,实现简单高效的通信。 -- **分布式资源管理**:解决资源的分布式和异构性问题。将 CPU、内存、IO 等物理资源虚拟化,新城逻辑资源池,以便统一管理。 - -## 错误的分布式假设 - -> 内容摘自 [*The Eight Fallacies of Distributed Computing - Tech Talk*](https://web.archive.org/web/20171107014323/http://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/) - -随着时间的推移,每一条都会被证明是错误的,也都会导致严重的问题,以及痛苦的学习体验: - -- 网络是稳定的 -- 网络传输的延迟是零 -- 网络的带宽是无穷大 -- 网络是安全的 -- 网络的拓扑不会改变 -- 只有一个系统管理员 -- 传输数据的成本为零 -- 整个网络是同构的 - -为什么我们要深刻地认识这 8 个错误? - -是因为,这要我们清楚地认识到——**分布式系统中,错误是不可能避免的**。既然错误无可避免,那么,我们应该做的是,将容错也作为功能去实现。 - -## 参考资料 - -- [The Eight Fallacies of Distributed Computing - Tech Talk](https://web.archive.org/web/20171107014323/http://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/) - 分布式系统新手常犯的 8 个错误,并探讨了其会带来的影响。 -- [Distributed Systems for Fun and Profit](http://book.mixu.net/distsys/) - 一本学习小册,涵盖了分布式系统中的关键问题,包括时间的作用和不同的复制策略。 -- [A Note on Distributed Systems](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.41.7628&rep=rep1&type=pdf) - 这是一篇经典的论文,讲述了为什么在分布式系统中,远程交互不能像本地对象那样进行。 -- [Amazon’s Highly Available Key-value Store](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) -- [CAP Theorem](https://cryptographics.info/cryptographics/blockchain/cap-theorem/) -- [CAP twelve years later: How the "rules" have changed](https://www.semanticscholar.org/paper/CAP-twelve-years-later%3A-How-the-%22rules%22-have-Brewer/c9c73f5a1668b8bf12aae2efb6ac5a5a2c34002a) -- [CAP 定理的含义](https://www.ruanyifeng.com/blog/2018/07/cap.html) - by 阮一峰 -- [神一样的 CAP 理论被应用在何方](https://juejin.im/post/5d720e86f265da03cc08de74) -- [BASE: An Acid Alternative](https://queue.acm.org/detail.cfm?id=1394128) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/02.\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/02.\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247.md" deleted file mode 100644 index 9eb24953f2..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/02.\345\210\206\345\270\203\345\274\217\344\270\200\350\207\264\346\200\247.md" +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: 分布式一致性 -date: 2021-11-08 08:15:33 -order: 02 -categories: - - 分布式 - - 分布式理论 -tags: - - 分布式 - - 理论 - - 一致性 - - ACID - - CAP - - BASE -permalink: /pages/dac0e2/ ---- - -# 分布式一致性 - -## ACID 理论 - -ACID 是数据库事务正确执行的四个基本要素。 - -- **原子性(Atomicity)** - - 事务被视为不可分割的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚。 - - 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 -- **一致性(Consistency)** - - 数据库在事务执行前后都保持一致性状态。 - - 在一致性状态下,所有事务对一个数据的读取结果都是相同的。 -- **隔离性(Isolation)** - - 一个事务所做的修改在最终提交以前,对其它事务是不可见的。 -- **持久性(Durability)** - - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 - - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 - -**一个支持事务(Transaction)中的数据库系统,必需要具有这四种特性,否则在事务过程(Transaction processing)当中无法保证数据的正确性,交易过程极可能达不到交易。** - -- 只有满足一致性,事务的执行结果才是正确的。 -- 在无并发的情况下,事务串行执行,隔离性一定能够满足。此时只要能满足原子性,就一定能满足一致性。 -- 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 -- 事务满足持久化是为了能应对系统崩溃的情况。 - -## 本地事务和分布式事务 - -学习分布式之前,先了解一下本地事务的概念。 - -事务简单来说:**一个会话中所进行所有的操作,要么同时成功,要么同时失败**。 - -事务指的是满足 ACID 特性的一组操作,可以通过 `Commit` 提交一个事务,也可以使用 `Rollback` 进行回滚。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310092226555.png) - -**分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性**。 - -分布式事务相比于单机事务,实现复杂度要高很多,主要是因为其存在以下**难点**: - -- **事务的原子性**:事务操作跨不同节点,当多个节点某一节点操作失败时,需要保证多节点操作的**都做或都不做(All or Nothing)**的原子性。 -- **事务的一致性**:当发生网络传输故障或者节点故障,节点间数据复制通道中断,在进行事务操作时需要保证数据一致性,保证事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。 -- **事务的隔离性**:事务隔离性的本质就是如何正确多个并发事务的处理的读写冲突和写写冲突,因为在分布式事务控制中,可能会出现提交不同步的现象,这个时候就有可能出现“部分已经提交”的事务。此时并发应用访问数据如果没有加以控制,有可能出现“脏读”问题。 - -在分布式领域,要实现强一致性,代价非常高昂。因此,有人基于 CAP 理论以及 BASE 理论,有人就提出了**柔性事务**的概念。柔性事务是指:在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。**并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐**。 - -## CAP 理论 - -> CAP 定理是加州大学计算机科学家埃里克·布鲁尔提出来的猜想,后来被证明成为分布式计算领域公认的定理。 - -**CAP 定理**,指的是:**在一个分布式系统中, 最多只能同时满足其中两项**。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200746619.png) - -CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20211102180526.png) - -- 一致性(**C**onsistency):在任何给定时间,网络中的所有节点都具有完全相同(最近)的值。 -- 可用性(**A**vailability):对网络的每个请求都会收到响应,但不能保证返回的数据是最新的。 -- 分区容错性(**P**artition Tolerance):即使任意数量的节点出现故障,网络仍会继续运行。 - -### 一致性 - -一致性(Consistency)指的是**多个数据副本是否能保持一致**的特性。 - -在一致性的条件下,分布式系统在执行写操作成功后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 - -数据一致性又可以分为以下几点: - -- **强一致性** - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。 -- **最终一致性** - 即物理存储的数据可能是不一致的,终端用户访问到的数据可能也是不一致的,但系统经过一段时间的自我修复和修正,数据最终会达到一致。 - -举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。 - -![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071602.png) - -接下来,用户的读操作就会得到 v1。这就叫一致性。 - -![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071603.png) - -问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。 - -![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071604.png) - -为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。 - -![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071605.png) - -这样的话,用户向 G2 发起读操作,也能得到 v1。 - -![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071606.png) - -### 可用性 - -可用性指**分布式系统在面对各种异常时可以提供正常服务的能力**,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 `99.99%` 的时间是可用的。 - -在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 - -### 分区容错性 - -分区容错性(Partition Tolerance)指 **分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障**。 - -在一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中,这就叫分区。 - -假设,某个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。 - -提高分区容错性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容错性就提高了。 - -然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。 - -总的来说就是,数据存在的节点越多,分区容错性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。 - -大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区(Partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。 - -![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071601.png) - -上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。 - -**一般来说,分区容错无法避免**,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。 - -### AP or CP - -在分布式系统中,分区容错性必不可少,因为需要总是假设网络是不可靠的。因此,**CAP 理论实际在是要在可用性和一致性之间做权衡**。 - -由于分布式数据存储(如区块链)的性质,分区容错性是一个既定的事实;网络中总会有失败/无法访问的节点(尤其是因为互联网的不稳定特性)。 CAP 定理指出,当存在 P(分区)时,必须在 C(一致性)或 A(可用性)之间进行选择。 - -(1)AP 模式 - -> **AP** **模式**:对网络的每个请求都会收到响应,即使网络由于网络分区故障而无法保证它是最新的。 - -选择 **AP** **模式**,实现了服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误;但是,当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。 - - - -(2)CP 模式 - -> **CP** **模式**:如果由于网络分区(故障节点)而无法保证特定信息是最新的,则系统将返回错误或超时。 - -选择 **CP** **模式**,这样能够提供一部分的可用性。采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性。因为为了防止数据不一致,集群将拒绝新数据的写入。 - - - -## BASE 理论 - -### 什么是 BASE 定理 - -> **BASE 定理是对 CAP 中一致性和可用性权衡的结果**。 - -BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State)`** 和 **`最终一致性(Eventually Consistent)`** 三个短语的缩写。 - -BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 - -- **基本可用(Basically Available)**分布式系统在出现故障的时候,**保证核心可用,允许损失部分可用性**。例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。 -- **软状态(Soft State)**指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即**允许系统不同节点的数据副本之间进行同步的过程存在延时**。 -- **最终一致性(Eventually Consistent)**强调的是**系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态**。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/architecture/%E5%88%86%E5%B8%83%E5%BC%8F%E7%90%86%E8%AE%BA-BASE.png) - -### BASE vs. ACID - -BASE 的理论的**核心思想**是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 - -ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过**牺牲强一致性来达到可用性**,通常运用在大型分布式系统中。 - - - -在实际的分布式场景中,不同业务单元和组件对一致性的要求是不同的,因此 ACID 和 BASE 往往会结合在一起使用。 - -## 参考资料 - -- [CAP Theorem](https://cryptographics.info/cryptographics/blockchain/cap-theorem/) -- [CAP twelve years later: How the "rules" have changed](https://www.semanticscholar.org/paper/CAP-twelve-years-later%3A-How-the-%22rules%22-have-Brewer/c9c73f5a1668b8bf12aae2efb6ac5a5a2c34002a) -- [CAP 定理的含义](https://www.ruanyifeng.com/blog/2018/07/cap.html) - by 阮一峰 -- [神一样的 CAP 理论被应用在何方](https://juejin.im/post/5d720e86f265da03cc08de74) -- [BASE: An Acid Alternative](https://queue.acm.org/detail.cfm?id=1394128) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/README.md" deleted file mode 100644 index 1fb5a75b1c..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/README.md" +++ /dev/null @@ -1,73 +0,0 @@ ---- -title: 分布式理论 -date: 2022-06-23 17:17:18 -categories: - - 分布式 - - 分布式理论 -tags: - - 分布式 - - 理论 -permalink: /pages/86cdf2/ -hidden: true -index: false ---- - -# 分布式理论 - -## 📖 内容 - -- [分布式理论](01.分布式基础理论.md) - 关键词:`拜占庭将军`、`错误的分布式假设` -- [分布式一致性](02.分布式一致性.md) - 关键词:`ACID`、`CAP`、`BASE`、`强一致性`、`最终一致性` -- [拜占庭将军问题](10.拜占庭将军问题.md) - 关键词:`共识性` -- [Paxos 算法](11.Paxos算法.md) - 关键词:`共识性` -- [Raft 算法](12.Raft算法.md) - 关键词:`共识性` -- [Gossip 算法](13.Gossip算法.md) - 关键词:`数据传播` -- QuorumNWR 算法 -- ZAB 协议 - -## 📚 资料 - -### 分布式理论 - -- [The Google File System](https://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf):Google 三大经典论文之一 -- [Bigtable: A Distributed Storage System for Structured Data](https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf):Google 三大经典论文之一 -- [MapReduce: Simplifed Data Processing on Large Clusters](https://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf):Google 三大经典论文之一 -- [分布式系统原理与范型](https://book.douban.com/subject/11691266/):书原名 Distributed Systems Principles and Paradigms。经典分布式教程,介绍了分布式系统的七大核心原理,并给出了大量的例子;系统讲述了分布式系统的概念和技术,包括通信、进程、命名、同步化、一致性和复制、容错以及安全等。 -- [The Eight Fallacies of Distributed Computing - Tech Talk](https://web.archive.org/web/20171107014323/http://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/) - 分布式系统新手常犯的 8 个错误,并探讨了其会带来的影响。 -- [Distributed Systems for Fun and Profit](http://book.mixu.net/distsys/) - 一本学习小册,涵盖了分布式系统中的关键问题,包括时间的作用和不同的复制策略。 -- [A Note on Distributed Systems](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.41.7628&rep=rep1&type=pdf) - 这是一篇经典的论文,讲述了为什么在分布式系统中,远程交互不能像本地对象那样进行。 -- [Amazon’s Highly Available Key-value Store](https://www.allthingsdistributed.com/files/amazon-dynamo-sosp2007.pdf) -- [CAP Theorem](https://cryptographics.info/cryptographics/blockchain/cap-theorem/) -- [CAP twelve years later: How the "rules" have changed](https://www.semanticscholar.org/paper/CAP-twelve-years-later%3A-How-the-%22rules%22-have-Brewer/c9c73f5a1668b8bf12aae2efb6ac5a5a2c34002a) -- [CAP 定理的含义](https://www.ruanyifeng.com/blog/2018/07/cap.html) - by 阮一峰 -- [神一样的 CAP 理论被应用在何方](https://juejin.im/post/5d720e86f265da03cc08de74) -- [BASE: An Acid Alternative](https://queue.acm.org/detail.cfm?id=1394128) - -### 分布式算法 - -- **Paxos** - - [Part-time Parliament 论文](https://research.microsoft.com/en-us/um/people/lamport/pubs/lamport-paxos.pdf) - Lamport 的 Paxos 论文。这篇论文很权威,但较为晦涩难懂。 - - [Paxos Made Simple 论文](https://lamport.azurewebsites.net/pubs/paxos-simple.pdf) - - [Paxos 算法详解](https://zhuanlan.zhihu.com/p/31780743) - - Neat Algorithms - Paxos - - [Wiki - Paxos 算法](https://zh.wikipedia.org/w/index.php?title=Paxos%E7%AE%97%E6%B3%95) - - [一致性算法(Paxos、Raft、Zab)](https://www.bilibili.com/video/BV1TW411M7Fx?from=search&seid=11524608198747599965) - - [Raft 作者讲解 Paxos 视频](https://www.bilibili.com/video/av36556594) - - [Paxos 算法讲解视频](https://www.youtube.com/watch?v=d7nAGI_NZPk) -- **Raft** - - [Raft 算法论文原文](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) - - [Raft 算法论文译文](https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md) - - [Raft 作者讲解视频](https://www.youtube.com/watch?v=YbZ3zDzDnrw&feature=youtu.be) - - [Raft 作者讲解视频对应的 PPT](http://www2.cs.uh.edu/~paris/6360/PowerPoint/Raft.ppt) - - [Raft 算法详解](https://zhuanlan.zhihu.com/p/32052223) - - [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) - 一个动画教程 - - [The Raft Consensus Algorithm](https://raft.github.io/) - 一个交互式动画教程 -- **Goosip** - - [Epidemic Algorithms for Replicated Database Maintenance](http://bitsavers.trailing-edge.com/pdf/xerox/parc/techReports/CSL-89-1_Epidemic_Algorithms_for_Replicated_Database_Maintenance.pdf) - - [P2P 网络核心技术:Gossip 协议](https://zhuanlan.zhihu.com/p/41228196) - - [INTRODUCTION TO GOSSIP](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) - - [Goosip 协议仿真动画](https://flopezluis.github.io/gossip-simulator/) - -## 🚪 传送 - -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/02.\345\210\206\345\270\203\345\274\217\345\244\215\345\210\266.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/02.\345\210\206\345\270\203\345\274\217\345\244\215\345\210\266.md" deleted file mode 100644 index 83d3b20dd0..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/02.\345\210\206\345\270\203\345\274\217\345\244\215\345\210\266.md" +++ /dev/null @@ -1,324 +0,0 @@ ---- -title: 分布式复制 -date: 2022-06-11 10:40:10 -order: 02 -categories: - - 分布式 - - 分布式协同 - - 分布式协同综合 -tags: - - 分布式 - - 协同 - - 复制 -permalink: /pages/47f7bd/ ---- - -# 分布式复制 - -分布式复制是指:在多个节点上保存相同数据的副本,每个副本具体的存储位置可能不尽相同。复制方住可以提供冗余:如果某些节点发生不可用,则可以通过其他节点继续提供数据访问服务。复制也可以帮助提高系统性能。 - -## 分布式复制简介 - -数据复制的作用 - -- 使数据在地理位置上更接近用户,从而**降低访问延迟**。 -- 当部分组件出现位障,系统依然可以继续工作,从而**提高可用性**。 -- 扩展至多台机器以同时提供数据访问服务,从而**提高读吞吐量**。 - -分布式系统的复制方式有以下几种: - -- **主从复制** -- **多主节点复制** -- **无主节点复制** - -分布式系统的复制需要考虑以下问题: - -- **同步还是异步** -- **如何处理失败的副本** -- **如何保证数据一致** - -## 主节点与从节点 - -每个保存数据库完整数据集的节点称之为副本。有了多副本,必然会面临一个问题:如何确保所有副本之间的数据是一致的? - -主从复制的工作原理如下: - -1. 指定某一个副本为主副本(或称为主节点) 。当客户写数据库时,必须将写请求首先发送给主副本,主副本首先将新数据写入本地存储。 -2. 其他副本则全部称为从副本(或称为从节点)。主副本把新数据写入本地存储后,然后将数据更改作为复制的日志或更改流发送给所有从副本。每个从副本获得更改日志之后将其应用到本地,且严格保持与主副本相同的写入顺序。 -3. 客户端从数据库中读数据时,可以在主副本或者从副本上执行查询。再次强调,只有主副本才可以接受写请求:从客户端的角度来看,从副本都是只读的。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202101.png) - -典型应用: - -- 数据库:MySql、MongoDB 等 -- 消息队列:Kafka、RabbitMQ 等 - -### 同步复制与异步复制 - -对于关系数据库系统,同步或异步通常是一个可配置的选项:而其他系统则可能是硬性指定或者只能二选一。同步复制与异步复制基本流程是,客户将更新请求发送给主节点,主节点接收到请求,接下来将数据更新转发给从节点。最后,由主节点来通知客户更新完成。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202158.png) - -通常情况下, 复制速度会非常快,例如多数数据库系统可以在一秒之内完成所有从节点的更新。但是,系统其实并没有保证一定会在多段时间内完成复制。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如,由于从节点刚从故障中恢复,或者系统已经接近最大设计上限,或者节点之间的网络出现问题。 - -- **同步复制的优点**: 一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。 -- **同步复制的缺点**:如果同步的从节点无法完成确认(例如由于从节点发生崩愤,或者网络故障,或任何其他原因), 写入就不能视为成功。主节点会阻塞其后所有的写操作,直到同步副本确认完成。 - -因此,把所有从节点都配置为同步复制有些不切实际。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前。实际应用中,很多数据库推荐的模式是:只要有一个从节点或半数以上的从节点同步成功,就视为同步,直接返回结果;剩下的节点都通过异步方式同步。万一同步的从节点变得不可用或性能下降, 则将另一个异步的从节点提升为同步模式。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。这种配置有时也称为半同步 。 - -主从复制还经常会被配置为全异步模式。此时如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了写操作, 却无法保证数据的持久化。但全异步配置的优点则是,不管从节点上数据多么滞后, 主节点总是可以继续响应写请求,系统的吞吐性能更好。 - -- **异步复制的优点**:不管从节点上数据多么滞后,主节点总是可以继续响应写请求,系统的吞吐性能更好。 -- **异步复制的缺点**:如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。 - -### 配置新的从节点 - -当如果出现以下情况时,如需要增加副本数以提高容错能力,或者替换失败的副本,就需要考虑增加新的从节点。但如何确保新的从节点和主节点保持数据一致呢? - -简单地将数据文件从一个节点复制到另一个节点通常是不够的。主要是因为客户端仍在不断向数据库写入新数据,数据始终处于不断变化之中,因此常规的文件拷贝方式将会导致不同节点上呈现出不同时间点的数据。 - -另一种思路是:考虑锁定数据库(使其不可写)来使磁盘上的文件保持一致,但这会违反高可用的设计目标。 - -在不停机、数据服务不中断的前提下,也有一种可行性复制方案,其主要操作步骤如下: - -1. 在某个时间点对主节点的数据副本产生一个一致性快照,这样避免长时间锁定整个数据库。目前大多数数据库都支持此功能,快照也是系统备份所必需的。而在某些情况下,可能需要第三方工具, 如 MySQL 的 innobackupex。 -2. 将此快照拷贝到新的从节点。 -3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定位置相关联,这个位置信息在不同的系统有不同的称呼,如 PostgreSQL 将其称为“ log sequence number” (日志序列号),而 MySQL 将其称为“ binlog coordinates ” 。 -4. 获得日志之后,从节点来应用这些快照点之后所有数据变更,这个过程称之为追赶。接下来,它可以继续处理主节点上新的数据变化。井重复步骤 1 ~步骤 4 。 - -建立新的从副本具体操作步骤可能因数据库系统而异。 - -### 处理节点失效 - -系统中的任何节点都可能因故障或者计划内的维护(例如重启节点以安装内核安全补丁)而导致中断甚至停机。如果能够在不停机的情况下重启某个节点,这会对运维带来巨大的便利。我们的目标是,尽管个别节点会出现中断,但要保持系统总体的持续运行,并尽可能减小节点中断带来的影响。 - -如何通过主从复制技术来实现系统高可用呢? - -#### 从节点失效: 追赶式恢复 - -从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,或者主从节点之间的网络发生暂时中断(闪断),则恢复比较容易,根据副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。 - -#### 主节点失效:节点切换 - -选择某个从节点将其提升为主节点;客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接受来自新的主节点上的变更数据,这一过程称之为切换。 - -故障切换可以手动进行,例如通知管理员主节点发生失效,采取必要的步骤来创建新的主节点;或者以自动方式进行。自动切换的步骤通常如下: - -1. **确认主节点失效**。有很多种出错可能性,很难准确检测出问题的原因,所以大多数系统都采用了基于超时的机制:节点间频繁地互相发生发送心跳悄息,如果发现某一个节点在一段比较长时间内(例如 30s )没有响应,即认为该节点发生失效。 -2. **选举新的主节点**。可以通过选举的方式(超过多数的节点达成共识)来选举新的主节点,或者由之前选定的某控制节点来指定新的主节点。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。让所有节点同意新的主节点是个典型的共识问题。 -3. **重新配置系统使新主节点生效**。客户端现在需要将写请求发送给新的主节点。如果原主节点之后重新上线,可能仍然自认为是主节点,而没有意识到其他节点已经达成共识迫使其下台。这时系统要确保原主节点降级为从节点,并认可新的主节点。 - -上述切换过程依然充满了很多变数: - -- 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线并加入到集群,接下来的写操作会发生什么?新的主节点很可能会收到冲突的写请求,这是因为原主节点未意识的角色变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是,原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。 -- 如果在数据库之外有其他系统依赖于数据库的内容并在一起协同使用,丢弃数据的方案就特别危险。例如,在 GitHub 的一个事故中,某个数据并非完全同步的 MySQL 从节点被提升为主副本,数据库使用了自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点( 即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部 Redis 所引用,结果出现 MySQL 和 Redis 之间的不一致,最后导致了某些私有数据被错误地泄露给了其他用户。 -- 在某些故障情况下,可能会发生两个节点同时-都自认为是主节点。这种情况被称为**脑裂**,它非常危险:两个主节点都可能接受写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏。作为一种安全应急方案,有些系统会采取措施来强制关闭其中一个节点。然而,如果设计或者实现考虑不周,可能会出现两个节点都被关闭的情况。 -- 如何设置合适的超时来检测主节点失效呢? 主节点失效后,超时时间设置得越长也意味着总体恢复时间就越长。但如果超时设置太短,可能会导致很多不必要的切换。例如,突发的负载峰值会导致节点的响应时间变长甚至超肘,或者由于网络故障导致延迟增加。如果系统此时已经处于高负载压力或网络已经出现严重拥塞,不必要的切换操作只会使总体情况变得更糟。 - -### 复制日志的实现 - -#### 基于语句的复制 - -最简单的情况,主节点记录所执行的每个写请求(操作语句)井将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个 INSERT 、UPDATE 或 DELETE 语句都会转发给从节点,并且每个从节点都会分析井执行这些 SQU 吾句,如同它们是来自客户端那样。 - -听起来很合理也不复杂,但这种复制方式有一些不适用的场景: - -- 任何调用非确定性函数的语句,如 `NOW()` 获取当前时间,或 `RAND()` 获取一个随机数等,可能会在不同的副本上产生不同的值。 -- 如果语句中使用了自增列,或者依赖于数据库的现有数据(例如, `UPDATE ... WHERE <某些条件>`),则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。进而,如果有多个同时并发执行的事务时, 会有很大的限制。 -- 有副作用的语句(例如,触发器、存储过程、用户定义的函数等),可能会在每个副本上产生不同的副作用。 - -有可能采取一些特殊措施来解决这些问题,例如,主节点可以在记录操作语句时将非确定性函数替换为执行之后的确定的结果,这样所有节点直接使用相同的结果值。但是,这里面存在太多边界条件需要考虑,因此目前通常首选的是其他复制实现方案。 - -MySQL 5.1 版本之前采用基于操作语句的复制。现在由于逻辑紧凑,依然在用,但是默认情况下,如果语句中存在一些不确定性操作,则 MySQL 会切换到基于行的复制(稍后讨论)。VoltDB 使用基于语句的复制,它通过事务级别的确定性来保证复制的安全。 - -#### 基于预写日志(WAL)传输 - -通常每个写操作都是以追加写的方式写入到日志中: - -- 对于日志结构存储引擎,日志是主要的存储方式。日志段在后台压缩井支持垃圾回收。 -- 对于采用覆写磁盘的 BTree 结构,每次修改会预先写入日志,如系统发生崩溃,通过索引更新的方式迅速恢复到此前一致状态。 - -不管哪种情况,所有对数据库写入的字节序列都被记入日志。因此可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外, 主节点还可以通过网络将其发送给从节点。 - -PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要缺点是日志描述的数据结果非常底层: 一个 WAL 包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无能支持主从节点上运行不同版本的软件。 - -看起来这似乎只是个有关实现方面的小细节,但可能对运营产生巨大的影响。如果复制协议允许从节点的软件版本比主节点更新,则可以实现数据库软件的不停机升级:首先升级从节点,然后执行主节点切换,使升级后的从节点成为新的主节点。相反,复制协议如果要求版本必须严格一致(例如 WALf 专输),那么就势必以停机为代价。 - -#### 基于行的逻辑日志复制 - -另一种方能是复制和存储引擎采用不同的日志格式,这样复制与存储逻辑剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。 - -关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求: - -- 对于行插入,日志包含所有相关列的新值。 -- 对于行删除,日志里有足够的信息来唯一标识已删除的行,通常是靠主键,但如果表上没有定义主键,就需要记录所有列的旧值。 -- 对于行更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少包含所有已更新列的新值)。 - -如果一条事务涉及多行的修改,则会产生多个这样的日志记录,并在后面跟着一条记录,指出该事务已经提交。MySQL 的二进制日志 binlog (当配置为基于行的复制时)使用该方式。 - -由于逻辑日志与存储引擎逻辑解耦,因此可以更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。 - -对于外部应用程序来说,逻辑日志格式也更容易解析。 - -#### 基于触发器的复制 - -在某些情况下,我们可能需要更高的灵活性。例如,只想复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要订制、管理冲突解决逻辑( 参阅本章后面的“处理写冲突”),则需要将复制控制交给应用程序层。 - -有一些工具,可以通过读取数据库日志让应用程序获取数据变更。另一种方法则是借助许多关系数据库都支持的功能:触发器和存储过程。 - -触发器支持注册自己的应用层代码,使得当数据库系统发生数据更改(写事务)时自动执行上述自定义代码。通过触发器技术,可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表,实施必要的自定义应用层逻辑,例如将数据更改复制到另一个系统。Oracle 的 Databus 和 Postgres 的 Bucardo 就是这种技术的典型代表。基于触发器的复制通常比其他复制方式开销更高, 也比数据库内置复制更容易出错,或者暴露一些限制。然而,其高度灵活性仍有用武之地。 - -## 复制滞后问题 - -主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询。对于读操作密集的负载(如 Web ),这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载井允许读取请求就近满足。 - -在这种扩展体系下,只需添加更多的从副本,就可以提高读请求的服务吞吐量。但是,这种方法实际上只能用于异步复制,如果试图同步复制所有的从副本,则单个节点故障或网络中断将使整个系统无法写入。而且节点越多,发生故障的概率越高,所以完全同步的配置现实中反而非常不可靠。 - -不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显的不一致:由于并非所有的写入都反映在从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。经过一段时间之后,从节点最终会赶上并与主节点数据保持一致。这种效应也被称为**最终一致性**。 - -#### 读自己的写 - -许多应用让用户提交一些数据,接下来查看他们自己所提交的内容。例如客户数据库中的记录,亦或者是讨论主题的评论等。提交新数据须发送到主节点,但是当用户读取数据时,数据可能来自从节点。这对于读密集和偶尔写入的负载是个非常合适的方案。 - -然而对于异步复制存在这样一个问题,如图所示,用户在写入不久即查看数据,则新数据可能尚未到达从节点。对用户来讲, 看起来似乎是刚刚提交的数据丢失了,显然用户不会高兴。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302204836.png) - -对于这种情况,我们需要读写一致性。该机制保证如果用户重新加载页面, 他们总能看到自己最近提交的更新。但对其他用户则没有任何保证,这些用户的更新可能会在稍后才能刷新看到。如何实现呢?有以下几种可行性方案: - -- **如果用户访问可能会被修改的内容,从主节点读取; 否则,在从节点读取**。这背后就要求有一些方法在实际执行查询之前,就已经知道内容是否可能会被修改。例如,社交网络上的用户首页信息通常只能由所有者编辑,而其他人无法编辑。因此,这就形成一个简单的规则: 总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件。 -- **如果应用的大部分内容都可能被所有用户修改**,那么上述方法将不太有效,它会导致大部分内容都必须经由主节点,这就丧失了读操作的扩展性。此时需要其他方案来判断是否从主节点读取。例如,跟踪最近更新的时间,如果更新后一分钟之内,则总是在主节点读取;井监控从节点的复制滞后程度,避免从那些滞后时间超过一分钟的从节点读取。 -- 客户端还可以记住最近更新时的时间戳,井附带在读请求中,据此信息,系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理,要么等待直到副本接收到了最近的更新。时间戳可以是逻辑时间戳(例如用来指示写入顺序的日志序列号)或实际系统时钟(在这种情况下,时钟同步又称为一个关键点)。 -- 如果副本分布在多数据中心(例如考虑与用户的地理接近,以及高可用性),情况会更复杂些。必须先把请求路由到主节点所在的数据中心(该数据中心可能离用户很远)。 - -如果同一用户可能会从多个设备访问数据,情况会更加复杂。 - -- 记住用户上次更新时间戳的方法实现起来会比较困难,因为在一台设备上运行的代码完全无法知道在其他设备上发生了什么。此时,元数据必须做到全局共享。 -- 如果副本分布在多数据中心,无法保证来自不同设备的连接经过路由之后都到达同一个数据中心。例如,用户的台式计算机使用了家庭宽带连接,而移动设备则使用蜂窝数据网络,不同设备的网络连接线路可能完全不同。如果方案要求必须从主节点读取,则首先需要想办毡确保将来自不同设备的请求路由到同一个数据中心。 - -#### 单调读 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303093658.png) - -用户看到了最新内容之后又读到了过期的内容,好像时间被回拨, 此时需要单调读一致性。 - -单调读一致性可以确保不会发生这种异常。这是一个比强一致性弱,但比最终一致性强的保证。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回攘现象,即在读取较新值之后又发生读旧值的情况。 - -实现单调读的一种方式是,确保每个用户总是从固定的同一副本执行读取(而不同的用户可以从不同的副本读取)。例如,基于用户 ID 的哈希的方怯而不是随机选择副本。但如果该副本发生失效,则用户的查询必须重新路由到另一个副本。 - -#### 前缀一致读 - -前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序。 - -如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生这种反常。然而,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序。这就导致当用户从数据库中读数据时,可能会看到数据库的某部分旧值和另一部分新值。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220613071613.png) - -一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但该方案真实实现效率会大打折扣。现在有一些新的算法来显式地追踪事件因果关系。 - -### 复制滞后的解决方案 - -使用最终一致性系统时,最好事先就思考这样的问题:如果复制延迟增加到几分钟甚至几小时,那么应用层的行为会是什么样子?如果这种情况不可接受,那么在设计系统肘,就要考虑提供一个更强的一致性保证,比如写后读; 如果系统设计时假定是同步复制,但最终它事实上成为了异步复制,就可能会导致灾难性后果。 - -在应用层可以提供比底层数据库更强有力的保证。例如只在主节点上进行特定类型的读取,而代价则是,应用层代码中处理这些问题通常会非常复杂,且容易出错。 - -如果应用程序开发人员不必担心这么多底层的复制问题,而是假定数据库在“做正确的事情”,情况就变得很简单。而这也是事务存在的原因,事务是数据库提供更强保证的一种方式。 - -单节点上支持事务已经非常成熟。然而,在转向分布式数据库(即支持复制和分区)的过程中,有许多系统却选择放弃支持事务,并声称事务在性能与可用性方面代价过高,所以选择了最终一致性。 - -## 多主节点复制 - -主从复制方法较为常见,但存在一个明显的缺点:系统只有一个主节点,而所有写入都必须经由主节点。如果由于某种原因,例如与主节点之间的网络中断而导致主节点无法连接,主从复制方案就会影响所有的写入操作。 - -对主从复制模型进行自然的扩展,则可以配置多个主节点,每个主节点都可以接受写 s 操作,后面复制的流程类似: 处理写的每个主节点都必须将该数据更改转发到所有其他节点。这就是多主节点( 也称为主-主,或主动/主动)复制。此时,每个主节点还同时扮演其他主节点的从节点。 - -### 适用场景 - -在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。 - -但是,以下场景这种配置则是合理的: - -- 多数据中心 -- 离线客户端操作 -- 协作编辑 - -#### 多数据中心 - -为了容忍整个数据中心级别故障或者更接近用户,可以把数据库的副本横跨多个数据中心。而如果使用常规的基于主从的复制模型,主节点势必只能放在其中的某一个数据中心,而所有写请求都必须经过该数据中心。 - -有了多主节点复制模型,则可以在每个数据中心都配置主节点。在每个数据中心内,采用常规的主从复制方案;而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。 - -部署单主节点的主从复制方案与多主复制方案之间的差异 - -- **性能**:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,井基本偏离了采用多数据中心的初衷(即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。因此,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。 -- **容忍数据中心失效**:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。 -- **容忍网络问题**:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络可靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。 - -#### 离线客户端操作 - -另一种多主复制比较适合的场景是,应用在与网络断开后还需要继续工作。 - -这种情况下,每个设备都有一个充当主节点的本地数据库(用来接受写请求),然后在所有设备之间采用异步方式同步这些多主节点上的副本,同步滞后可能是几小时或者数天,具体时间取决于设备何时可以再次联网。 - -从架构层面来看,上述设置基本上等同于数据中心之间的多主复制,只不过是个极端情况,即一个设备就是数据中心,而且它们之间的网络连接非常不可靠。多个设备同步日历的例子表明,多主节点可以得到想要的结果,但中间过程依然有很多的未知数。 - -有一些工具可以使多主配置更为容易,如 CouchDB 就是为这种操作模式而设计的。 - -#### 协作编辑 - -实时协作编辑应用程序允许多个用户同时编辑文档。 - -我们通常不会将协作编辑完全等价于数据库复制问题,但二者确实有很多相似之处。当一个用户编辑文档时· ,所做的更改会立即应用到本地副本( Web 浏览器或客户端应用程序),然后异步复制到服务器以及编辑同一文档的其他用户。如果要确保不会发生编辑冲突,则应用程序必须先将文档锁定,然后才能对其进行编辑。如果另一个用户想要编辑同一个文档, 首先必须等到第一个用户提交修改并释放锁。这种协作模式相当于主从复制模型下在主节点上执行事务操作。 - -为了加快协作编辑的效率, 可编辑的粒度需要非常小。例如,单个按键甚至是全程无锁。然而另一方面, 也会面临所有多主复制都存在的挑战, 即如何解决冲突。 - -### 处理写冲突 - -多主复制的最大问题是可能发生写冲突。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220613072848.png) - -#### 同步与异步冲突检测 - -如果是主从复制数据库,第二个写请求要么会被阻塞直到第一个写完成, 要么被中止(用户必须重试) 。然而在多主节点的复制模型下,这两个写请求都是成功的,井且只能在稍后的时间点上才能异步检测到冲突,那时再要求用户层来解决冲突为时已晚。 - -理论上, 也可以做到同步冲突检测,即等待写请求完成对所有副本的同步,然后再通知用户写入成功。但是,这样做将会失去多主节点的主要优势:允许每个主节点独立接受写请求。如果确实想要同步方式冲突检测,或许应该考虑采用单主节点的主从复制模型。 - -#### 避免冲突 - -处理冲突最理想的策略是避免发生冲突,即如果应用层可以保证对特定记录的写请求总是通过同一个主节点,这样就不会发生写冲突。现实中,由于不少多主节点复制模型所实现的冲突解决方案存在瑕疵,因此,避免冲突反而成为大家普遍推荐的首选方案。 - -但是,有时可能需要改变事先指定的主节点,例如由于该数据中心发生故障,不得不将流量重新路由到其他数据中心,或者是因为用户已经漫游到另一个位置,因而更靠近新数据中心。此时,冲突避免方式不再有效,必须有措施来处理同时写入冲突的可能性。 - -#### 收敛于一致状态 - -对于主从复制模型,数据更新符合顺序性原则,即如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。 - -对于多主节点复制模型,由于不存在这样的写入顺序,所以最终值也会变得不确定。 - -实现收敛的冲突解决有以下可能的方式: - -- 给每个写入分配唯一的 ID ,例如, 一个时间戳, 一个足够长的随机数, 一个 UUID 或者一个基于键-值的哈希,挑选最高 ID 的写入作为胜利者,并将其他写入丢弃。如果基于时间戳,这种技术被称为最后写入者获胜。虽然这种方法很流行,但是很容易造成数据丢失。 -- 为每个副本分配一个唯一的 ID ,井制定规则,例如序号高的副本写入始终优先于序号低的副本。这种方法也可能会导致数据丢失。 -- 以某种方式将这些值合并在一起。例如,按字母顺序排序,然后拼接在一起。 -- 利用预定义好的格式来记录和保留冲突相关的所有信息,然后依靠应用层的逻辑,事后解决冲突(可能会提示用户) 。 - -#### 自定义冲突解决逻辑 - -解决冲突最合适的方式可能还是依靠应用层,所以大多数多主节点复制模型都有工具来让用户编写应用代码来解决冲突。可以在写入时或在读取时执行这些代码逻辑: - -- **在写入时执行**:只要数据库系统在复制变更日志时检测到冲突,就会调用应用层的冲突处理程序。 -- **在读取时执行**:当检测到冲突时,所有冲突写入值都会暂时保存下来。下一次读取数据时,会将数据的多个版本读返回给应用层。应用层可能会提示用户或自动解决冲突, 井将最后的结果返回到数据库。 - -注意, 冲突解决通常用于单个行或文档, 而不是整个事务。因此,如果有一个原子事务包含多个不同写请求,每个写请求仍然是分开考虑来解决冲突。 - -## 无主节点复制 - -单主节点和多主节点复制,都是基于这样一种核心思路,即客户端先向某个节点(主节点)发送写请求,然后数据库系统负责将写请求复制到其他副本。由主节点决定写操作的顺序, 从节点按照相同的顺序来应用主节点所发送的写日志。 - -一些数据存储系统则采用了不同的设计思路:选择放弃主节点,允许任何副本直接接受来自客户端的写请求。对于某些无主节点系统实现,客户端直接将其写请求发送到多副本,而在其他一些实现中,由一个协调者节点代表客户端进行写人,但与主节点的数据库不同,协调者井不负责写入顺序的维护。 - -## 参考资料 - -- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/03.\345\210\206\345\270\203\345\274\217\345\210\206\345\214\272.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/03.\345\210\206\345\270\203\345\274\217\345\210\206\345\214\272.md" deleted file mode 100644 index 3eb4f21da3..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/03.\345\210\206\345\270\203\345\274\217\345\210\206\345\214\272.md" +++ /dev/null @@ -1,163 +0,0 @@ ---- -title: 分布式分区 -date: 2022-06-14 08:49:21 -order: 03 -categories: - - 分布式 - - 分布式协同 - - 分布式协同综合 -tags: - - 分布式 - - 协同 - - 复制 -permalink: /pages/03714e/ ---- - -# 分布式分区 - -> 在不同系统中,分区有着不同的称呼,例如它对应于 MongoDB, Elasticsearch 和 SolrCloud 中的 shard, HBase 的 region, Bigtable 中的 -> tablet, Cassandra 和 Riak 中的 vnode ,以及 Couch base 中的 vBucket。总体而言,分区是最普遍的术语。 -> -> 分区通常是这样定义的,即每一条数据(或者每条记录,每行或每个文档)只属于某个特定分区。实际上,每个分区都可以视为一个完整的小型数据库,虽然数据库可能存在一些跨分区的操作。 - -**采用数据分区的主要目的是提高可扩展性**。不同的分区可以放在一个无共享集群的不同节点上,这样一个大数据集可以分散在更多的磁盘上,查询负载也随之分布到更多的处理器上。 - -对单个分区进行查询时,每个节点对自己所在分区可以独立执行查询操作,因此添加更多的节点可以提高查询吞吐量。超大而复杂的查询尽管比较困难,但也可能做到跨节点的并行处理。 - -## 数据分区与数据复制 - -分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而同样的内容会保存在不同的节点上以提高系统的容错性。 - -一个节点上可能存储了多个分区。每个分区都有自己的主副本,例如被分配给某节点,而从副本则分配在其他一些节点。一个节点可能既是某些分区的主副本,同时又是其他分区的从副本。 - -## 键-值数据的分区 - -分区的主要目标是将数据和查询负载均匀分布在所有节点上。如果节点平均分担负载,那么理论上 10 个节点应该能够处理 10 倍的数据量和 10 倍于单个节点的读写吞吐量(忽略复制) 。 - -而如果分区不均匀,则会出现某些分区节点比其他分区承担更多的数据量或查询负载,称之为倾斜。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10 个节点 9 个空闲,系统的瓶颈在最繁忙的那个节点上。这种负载严重不成比例的分区即成为系统热点。 - -### 基于关键字区间分区 - -一种分区方式是为每个分区分配一段连续的关键字或者关键宇区间范围(以最小值和最大值来指示)。 - -关键字的区间段不一定非要均匀分布,这主要是因为数据本身可能就不均匀。 - -每个分区内可以按照关键字排序保存(参阅第 3 章的“ SSTables 和 LSM Trees ”)。这样可以轻松支持区间查询,即将关键字作为一个拼接起来的索引项从而一次查询得到多个相关记录。 - -然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围,所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。为了避免上述问题,需要使用时间戳以外的其他内容作为关键字的第一项。 - -### 基于关键字晗希值分区 - -一且找到合适的关键宇 H 合希函数,就可以为每个分区分配一个哈希范围(而不是直接作用于关键宇范围),关键字根据其哈希值的范围划分到不同的分区中。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303105925.png) - -这种方总可以很好地将关键字均匀地分配到多个分区中。分区边界可以是均匀间隔,也可以是伪随机选择( 在这种情况下,该技术有时被称为一致性哈希) 。 - -然而,通过关键宇 II 合希进行分区,我们丧失了良好的区间查询特性。即使关键字相邻,但经过哈希之后会分散在不同的分区中,区间查询就失去了原有的有序相邻的特性。 - -### 负载倾斜与热点 - -基于哈希的分区方能可以减轻热点,但无住做到完全避免。一个极端情况是,所有的读/写操作都是针对同一个关键字,则最终所有请求都将被路由到同一个分区。 - -一个简单的技术就是在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到 100 个不同的关键字上,从而分配到不同的分区上。但是,随之而来的问题是,之后的任何读取都需要些额外的工作,必须从所有 100 个关键字中读取数据然后进行合井。因此通常只对少量的热点关键字附加随机数才有意义。 - -## 分区与二级索引 - -二级索引通常不能唯一标识一条记录,而是用来加速特定值的查询。 - -二级索引带来的主要挑战是它们不能规整的地映射到分区中。有两种主要的方法来支持对二级索引进行分区:基于文档的分区和基于词条的分区。 - -### 基于文档分区的二级索引 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303111528.png) - -在这种索引方法中,每个分区完全独立,各自维护自己的二级索引,且只负责自己分区内的文档而不关心其他分区中数据。每当需要写数据库时,包括添加,删除或更新文档等,只需要处理包含目标文档 ID 的那一个分区。因此文档分区索引也被称为本地索引,而不是全局索引。 - -这种查询分区数据库的方陆有时也称为分散/聚集,显然这种二级索引的查询代价高昂。即使采用了并行查询,也容易导致读延迟显著放大。尽管如此,它还是广泛用于实践: MongoDB 、Riak、Cassandra、Elasticsearch 、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。 - -### 基于词条的二级索引分区 - -可以对所有的数据构建全局索引,而不是每个分区维护自己的本地索引。 - -为避免成为瓶颈,不能将全局索引存储在一个节点上,否则就破坏了设计分区均衡的目标。所以,全局索引也必须进行分区,且可以与数据关键字采用不同的分区策略。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303112708.png) - -可以直接通过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是可以支持高效的区间查询;而采用哈希的方式则可以更均句的划分分区。 - -这种全局的词条分区相比于文档分区索引的主要优点是,它的读取更为高效,即它不需要采用 scatter/gather 对所有的分区都执行一遍查询。 - -然而全局索引的不利之处在于, 写入速度较慢且非常复杂,主要因为单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能完全不同甚至在不同的节点上,由此势必引人显著的写放大。 - -理想情况下,索引应该时刻保持最新,即写入的数据要立即反映在最新的索引上。但是,对于词条分区来讲,这需要一个跨多个相关分区的分布式事务支持,写入速度会受到极大的影响,所以现有的数据库都不支持同步更新二级索引。 - -## 分区再均衡 - -随着时间的推移,数据库可能总会出现某些变化: - -- 查询压力增加,因此需要更多的 C PU 来处理负载。 -- 数据规模增加,因此需要更多的磁盘和内存来存储数据。 -- 节点可能出现故障,因此需要其他机器来接管失效的节点。 - -所有这些变化都要求数据和请求可以从一个节点转移到另一个节点。这样一个迁移负载的过程称为再平衡(或者动态平衡)。无论对于哪种分区方案, 分区再平衡通常至少要满足: - -- 平衡之后,负载、数据存储、读写请求等应该在集群范围更均匀地分布。 -- 再平衡执行过程中,数据库应该可以继续正常提供读写服务。 -- 避免不必要的负载迁移,以加快动态再平衡,井尽量减少网络和磁盘 I/O 影响。 - -### 动态再平衡的策略 - -#### 为什么不用取模? - -最好将哈希值划分为不同的区间范围,然后将每个区间分配给一个分区。 - -为什么不直接使用 mod,对节点数取模方怯的问题是,如果节点数 N 发生了变化,会导致很多关键字需要从现有的节点迁移到另一个节点。 - -#### 固定数量的分区 - -创建远超实际节点数的分区数,然后为每个节点分配多个分区。 - -接下来, 如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。 - -#### 动态分区 - -对于采用关键宇区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。 - -因此, 一些数据库如 HBas e 和 RethinkDB 等采用了动态创建分区。当分区的数据增长超过一个可配的参数阔值( HBase 上默认值是 lOGB ),它就拆分为两个分区,每个承担一半的数据量[26]。相反,如果大量数据被删除,并且分区缩小到某个阑值以下,则将其与相邻分区进行合井。 - -每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。 - -但是,需要注意的是,对于一个空的数据库, 因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理, 而其他节点则处于空闲状态。 - -#### 按节点比例分区 - -采用动态分区策略,拆分和合并操作使每个分区的大小维持在设定的最小值和最大值之间,因此分区的数量与数据集的大小成正比关系。另一方面,对于固定数量的分区方式,其每个分区的大小也与数据集的大小成正比。两种情况,分区的数量都与节点数无关。 - -Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每个节点具有固定数量的分区。此时, 当节点数不变时,每个分区的大小与数据集大小保持正比的增长关系; 当节点数增加时,分区则会调整变得更小。较大的数据量通常需要大量的节点来存储,因此这种方陆也使每个分区大小保持稳定。当一个 - -新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界) - -### 自动与手动再平衡操作 - -全自动式再平衡会更加方便,它在正常维护之外所增加的操作很少。但是,也有可能出现结果难以预测的情况。再平衡总体讲是个比较昂贵的操作,它需要重新路由请求井将大量数据从一个节点迁移到另一个节点。万一执行过程中间出现异常,会使网络或节点的负载过重,井影响其他请求的性能。 - -将自动平衡与自动故障检测相结合也可能存在一些风险。例如,假设某个节点负载过重,对请求的响应暂时受到影响,而其他节点可能会得到结论:该节点已经失效;接下来激活自动平衡来转移其负载。客观上这会加重该节点、其他节点以及网络的负荷,可能会使总体情况变得更槽,甚至导致级联式的失效扩散。 - -## 请求路由 - -处理策略 - -1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,贝 lj 直接处理该请求:否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。 -2. 将所有客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层本身不处理任何请求,它仅充一个分区感知的负载均衡器。 -3. 客户端感知分区和节点分配关系。此时,客户端可以直接连接到目标节点,而不需要任何中介。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220304120137.png) - -许多分布式数据系统依靠独立的协调服务(如 ZooKeeper )跟踪集群范围内的元数据。每个节点都向 ZooKeeper 中注册自己, ZooKeeper 维护了分区到节点的最终映射关系。其他参与者(如路由层或分区感知的客户端)可以向 ZooKeeper 订阅此信息。一旦分区发生了改变,或者添加、删除节点, ZooKeeper 就会主动通知路由层,这样使路由信息保持最新状态。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220304163629.png) - -## 参考资料 - -- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/06.\345\210\206\345\270\203\345\274\217\351\224\201.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/06.\345\210\206\345\270\203\345\274\217\351\224\201.md" deleted file mode 100644 index 89f307dd0f..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/06.\345\210\206\345\270\203\345\274\217\351\224\201.md" +++ /dev/null @@ -1,488 +0,0 @@ ---- -title: 分布式锁基本原理 -date: 2019-06-04 23:42:00 -order: 06 -categories: - - 分布式 - - 分布式协同 - - 分布式协同综合 -tags: - - 分布式 - - 数据调度 - - 锁 -permalink: /pages/40ac64/ ---- - -# 分布式锁基本原理 - -> 在并发场景下,为了保证并发安全,我们常常要通过互斥(加锁)手段来保证数据同步安全。 -> -> JDK 虽然提供了大量锁工具,但是只能作用于单一 Java 进程,无法应用于分布式系统。为了解决这个问题,需要使用分布式锁。 -> -> 分布式锁的解决方案大致有以下几种: -> -> - 基于数据库实现 -> - 基于缓存(redis,memcached 等)实现 -> - 基于 Zookeeper 实现 ✅ -> -> 注:推荐基于 ZooKeeper 实现分布式锁,具体原因看完本文即可明了。 - -## 分布式锁思路 - -分布式锁的总体思路大同小异,仅在实现细节上有所不同。 - -分布式锁的主要思路如下: - -- **互斥、可重入** - 创建锁必须是唯一的,表现形式为向数据存储服务器或容器插入一个唯一的 key,一旦有一个线程插入这个 key,其他线程就不能再插入了。 - - 保证 key 唯一性的最简单的方式是使用 UUID。 - - 存储锁的重入次数,以及分布式环境下唯一的线程标识。举例来说,可以使用 json 存储结构化数据,为了保证唯一,可以考虑将 mac 地址(IP 地址、机器 ID)、Jvm 进程 ID(应用 ID、服务 ID)、线程 ID 拼接起来作为唯一标识。 - ``` - {"count":1,"expireAt":147506817232,"jvmPid":22224,"mac":"28-D2-44-0E-0D-9A","threadId":14} - ``` -- **避免死锁** - 数据库分布式锁和缓存分布式锁(Redis)的思路都是引入超时机制,即成功申请锁后,超过一定时间,锁失效(删除 key),原因在于它们无法感知申请锁的客户端节点状态。而 ZooKeeper 由于其 znode 以目录、文件形式组织,天然就存在物理空间隔离,只要 znode 存在,即表示客户端节点还在工作,所以不存在这种问题。 -- **容错** - 只要大部分 Redis 节点可用,客户端就能正常加锁。 -- **自旋重试** - 获取不到锁时,不要直接返回失败,而是支持一定的周期自旋重试,设置一个总的超时时间,当过了超时时间以后还没有获取到锁则返回失败。 - -## 数据库分布式锁 - -### 数据库分布式锁原理 - -(1)创建表 - -```sql -CREATE TABLE `methodLock` ( - `id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键', - `method_name` varchar(64) NOT NULL DEFAULT '' COMMENT '锁定的方法名', - `desc` varchar(1024) NOT NULL DEFAULT '备注信息', - `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '保存数据时间,自动生成', - PRIMARY KEY (`id`), - UNIQUE KEY `uidx_method_name` (`method_name `) USING BTREE -) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='锁定中的方法'; -``` - -(2)获取锁 - -想要锁住某个方法时,执行以下 SQL: - -```sql -insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) -``` - -因为我们对 `method_name` 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 - -成功插入则获取锁。 - -(3)释放锁 - -当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql: - -```sql -delete from methodLock where method_name ='method_name' -``` - -### 数据库分布式锁问题 - -- 这把锁强依赖数据库的可用性。如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。 -- 这把锁没有失效时间,一旦解锁操作失败,就会导致锁记录一直在数据库中,其他线程无法再获得到锁。 -- 这把锁只能是非阻塞的,因为数据的 insert 操作,一旦插入失败就会直接报错。没有获得锁的线程并不会进入排队队列,要想再次获得锁就要再次触发获得锁操作。 -- 这把锁是非重入的,同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。 - -解决办法: - -- 单点问题可以用多数据库实例,同时塞 N 个表,N/2+1 个成功就任务锁定成功 -- 写一个定时任务,隔一段时间清除一次过期的数据。 -- 写一个 while 循环,不断的重试插入,直到成功。 -- 在数据库表中加个字段,记录当前获得锁的机器的主机信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 - -### 数据库分布式锁小结 - -- 优点: 直接借助数据库,容易理解。 -- 缺点: 会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。操作数据库需要一定的开销,性能问题需要考虑。 - -## Redis 分布式锁 - -相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好。目前有很多成熟的分布式产品,包括 Redis、memcache、Tair 等。这里以 Redis 举例。 - -### Redis 分布式锁原理 - -这个分布式锁有 3 个重要的考量点: - -1. 互斥(只能有一个客户端获取锁) -2. 不能死锁 -3. 容错(只要大部分 redis 节点创建了这把锁就可以) - -对应的 Redis 指令如下: - -- `setnx` - `setnx key val`:当且仅当 key 不存在时,set 一个 key 为 val 的字符串,返回 1;若 key 存在,则什么都不做,返回 0。 -- `expire` - `expire key timeout`:为 key 设置一个超时时间,单位为 second,超过这个时间锁会自动释放,避免死锁。 -- `delete` - `delete key`:删除 key - -> 注意: -> -> 不要将 `setnx` 和 `expire` 作为两个命令组合实现加锁,这样就**无法保证原子性**。如果客户端在 `setnx` 之后崩溃,那么将导致锁无法释放。正确的做法应是在 `setnx` 命令中指定 `expire` 时间。 - -### Redis 分布式锁实现 - -(1)申请锁 - -``` -SET resource_name my_random_value NX PX 30000 -``` - -执行这个命令就 ok。 - -- `NX`:表示只有 `key` 不存在的时候才会设置成功。(如果此时 redis 中存在这个 key,那么设置失败,返回 `nil`) -- `PX 30000`:意思是 30s 后锁自动释放。别人创建的时候如果发现已经有了就不能加锁了。 - -(2)释放锁 - -释放锁就是删除 key ,但是一般可以用 `lua` 脚本删除,判断 value 一样才删除: - -```python --- 删除锁的时候,找到 key 对应的 value,跟自己传过去的 value 做比较,如果是一样的才删除。 -if redis.call("get",KEYS[1]) == ARGV[1] then - return redis.call("del",KEYS[1]) -else - return 0 -end -``` - -### 数据库分布式锁小结 - -为啥要用 `random_value` 随机值呢?因为如果某个客户端获取到了锁,但是阻塞了很长时间才执行完,比如说超过了 30s,此时可能已经自动释放锁了,此时可能别的客户端已经获取到了这个锁,要是你这个时候直接删除 key 的话会有问题,所以得用随机值加上面的 `lua` 脚本来释放锁。 - -但是这样是肯定不行的。因为如果是普通的 redis 单实例,那就是单点故障。或者是 redis 普通主从,那 redis 主从异步复制,如果主节点挂了(key 就没有了),key 还没同步到从节点,此时从节点切换为主节点,别人就可以 set key,从而拿到锁。 - -### RedLock 算法 - -这个场景是假设有一个 redis cluster,有 5 个 redis master 实例。然后执行如下步骤获取一把锁: - -1. 获取当前时间戳,单位是毫秒; -2. 跟上面类似,轮流尝试在每个 master 节点上创建锁,过期时间较短,一般就几十毫秒; -3. 尝试在**大多数节点**上建立一个锁,比如 5 个节点就要求是 3 个节点 `n / 2 + 1`; -4. 客户端计算建立好锁的时间,如果建立锁的时间小于超时时间,就算建立成功了; -5. 要是锁建立失败了,那么就依次之前建立过的锁删除; -6. 只要别人建立了一把分布式锁,你就得**不断轮询去尝试获取锁**。 - -[Redis 官方](https://redis.io/)给出了以上两种基于 Redis 实现分布式锁的方法,详细说明可以查看: 。 - -## ZooKeeper 分布式锁 - -### ZooKeeper 分布式锁原理 - -ZooKeeper 实现分布式锁基于 ZooKeeper 的两个特性: - -- **顺序临时节点**:ZooKeeper 的存储类似于 DNS 那样的具有层级的命名空间。ZooKeeper 节点类型可以分为持久节点(PERSISTENT )、临时节点(EPHEMERAL),每个节点还能被标记为有序性(SEQUENTIAL),一旦节点被标记为有序性,那么整个节点就具有顺序自增的特点。 -- **Watch 机制**:ZooKeeper 允许用户在指定节点上注册一些 Watcher,并且在特定事件触发的时候,ZooKeeper 服务端会将事件通知给用户。 - -这也是 ZooKeeper 客户端 curator 的分布式锁实现。 - -1. 创建一个目录 mylock; -2. 线程 A 想获取锁就在 mylock 目录下创建临时顺序节点; -3. 获取 mylock 目录下所有的子节点,然后获取比自己小的兄弟节点,如果不存在,则说明当前线程顺序号最小,获得锁; -4. 线程 B 获取所有节点,判断自己不是最小节点,设置监听比自己次小的节点; -5. 线程 A 处理完,删除自己的节点,线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 - -### ZooKeeper 分布式锁实现 - -```java -/** - * ZooKeeperSession - * - * @author bingo - * @since 2018/11/29 - * - */ -public class ZooKeeperSession { - - private static CountDownLatch connectedSemaphore = new CountDownLatch(1); - - private ZooKeeper zookeeper; - private CountDownLatch latch; - - public ZooKeeperSession() { - try { - this.zookeeper = new ZooKeeper("192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181", 50000, new ZooKeeperWatcher()); - try { - connectedSemaphore.await(); - } catch (InterruptedException e) { - e.printStackTrace(); - } - - System.out.println("ZooKeeper session established......"); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * 获取分布式锁 - * - * @param productId - */ - public Boolean acquireDistributedLock(Long productId) { - String path = "/product-lock-" + productId; - - try { - zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); - return true; - } catch (Exception e) { - while (true) { - try { - // 相当于是给node注册一个监听器,去看看这个监听器是否存在 - Stat stat = zk.exists(path, true); - - if (stat != null) { - this.latch = new CountDownLatch(1); - this.latch.await(waitTime, TimeUnit.MILLISECONDS); - this.latch = null; - } - zookeeper.create(path, "".getBytes(), Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); - return true; - } catch (Exception ee) { - continue; - } - } - - } - return true; - } - - /** - * 释放掉一个分布式锁 - * - * @param productId - */ - public void releaseDistributedLock(Long productId) { - String path = "/product-lock-" + productId; - try { - zookeeper.delete(path, -1); - System.out.println("release the lock for product[id=" + productId + "]......"); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * 建立zk session的watcher - * - * @author bingo - * @since 2018/11/29 - * - */ - private class ZooKeeperWatcher implements Watcher { - - public void process(WatchedEvent event) { - System.out.println("Receive watched event: " + event.getState()); - - if (KeeperState.SyncConnected == event.getState()) { - connectedSemaphore.countDown(); - } - - if (this.latch != null) { - this.latch.countDown(); - } - } - - } - - /** - * 封装单例的静态内部类 - * - * @author bingo - * @since 2018/11/29 - * - */ - private static class Singleton { - - private static ZooKeeperSession instance; - - static { - instance = new ZooKeeperSession(); - } - - public static ZooKeeperSession getInstance() { - return instance; - } - - } - - /** - * 获取单例 - * - * @return - */ - public static ZooKeeperSession getInstance() { - return Singleton.getInstance(); - } - - /** - * 初始化单例的便捷方法 - */ - public static void init() { - getInstance(); - } - -} -``` - -也可以采用另一种方式,创建临时顺序节点: - -如果有一把锁,被多个人给竞争,此时多个人会排队,第一个拿到锁的人会执行,然后释放锁;后面的每个人都会去监听**排在自己前面**的那个人创建的 node 上,一旦某个人释放了锁,排在自己后面的人就会被 zookeeper 给通知,一旦被通知了之后,就 ok 了,自己就获取到了锁,就可以执行代码了。 - -```java -public class ZooKeeperDistributedLock implements Watcher { - - private ZooKeeper zk; - private String locksRoot = "/locks"; - private String productId; - private String waitNode; - private String lockNode; - private CountDownLatch latch; - private CountDownLatch connectedLatch = new CountDownLatch(1); - private int sessionTimeout = 30000; - - public ZooKeeperDistributedLock(String productId) { - this.productId = productId; - try { - String address = "192.168.31.187:2181,192.168.31.19:2181,192.168.31.227:2181"; - zk = new ZooKeeper(address, sessionTimeout, this); - connectedLatch.await(); - } catch (IOException e) { - throw new LockException(e); - } catch (KeeperException e) { - throw new LockException(e); - } catch (InterruptedException e) { - throw new LockException(e); - } - } - - public void process(WatchedEvent event) { - if (event.getState() == KeeperState.SyncConnected) { - connectedLatch.countDown(); - return; - } - - if (this.latch != null) { - this.latch.countDown(); - } - } - - public void acquireDistributedLock() { - try { - if (this.tryLock()) { - return; - } else { - waitForLock(waitNode, sessionTimeout); - } - } catch (KeeperException e) { - throw new LockException(e); - } catch (InterruptedException e) { - throw new LockException(e); - } - } - - public boolean tryLock() { - try { - // 传入进去的locksRoot + “/” + productId - // 假设productId代表了一个商品id,比如说1 - // locksRoot = locks - // /locks/10000000000,/locks/10000000001,/locks/10000000002 - lockNode = zk.create(locksRoot + "/" + productId, new byte[0], ZooDefs.Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL_SEQUENTIAL); - - // 看看刚创建的节点是不是最小的节点 - // locks:10000000000,10000000001,10000000002 - List locks = zk.getChildren(locksRoot, false); - Collections.sort(locks); - - if(lockNode.equals(locksRoot+"/"+ locks.get(0))){ - //如果是最小的节点,则表示取得锁 - return true; - } - - //如果不是最小的节点,找到比自己小1的节点 - int previousLockIndex = -1; - for(int i = 0; i < locks.size(); i++) { - if(lockNode.equals(locksRoot + “/” + locks.get(i))) { - previousLockIndex = i - 1; - break; - } - } - - this.waitNode = locks.get(previousLockIndex); - } catch (KeeperException e) { - throw new LockException(e); - } catch (InterruptedException e) { - throw new LockException(e); - } - return false; - } - - private boolean waitForLock(String waitNode, long waitTime) throws InterruptedException, KeeperException { - Stat stat = zk.exists(locksRoot + "/" + waitNode, true); - if (stat != null) { - this.latch = new CountDownLatch(1); - this.latch.await(waitTime, TimeUnit.MILLISECONDS); - this.latch = null; - } - return true; - } - - public void unlock() { - try { - // 删除/locks/10000000000节点 - // 删除/locks/10000000001节点 - System.out.println("unlock " + lockNode); - zk.delete(lockNode, -1); - lockNode = null; - zk.close(); - } catch (InterruptedException e) { - e.printStackTrace(); - } catch (KeeperException e) { - e.printStackTrace(); - } - } - - public class LockException extends RuntimeException { - private static final long serialVersionUID = 1L; - - public LockException(String e) { - super(e); - } - - public LockException(Exception e) { - super(e); - } - } -} -``` - -### ZooKeeper 分布式锁小结 - -ZooKeeper 版本的分布式锁问题相对比较来说少。 - -- 锁的占用时间限制:redis 就有占用时间限制,而 ZooKeeper 则没有,最主要的原因是 redis 目前没有办法知道已经获取锁的客户端的状态,是已经挂了呢还是正在执行耗时较长的业务逻辑。而 ZooKeeper 通过临时节点就能清晰知道,如果临时节点存在说明还在执行业务逻辑,如果临时节点不存在说明已经执行完毕释放锁或者是挂了。由此看来 redis 如果能像 ZooKeeper 一样添加一些与客户端绑定的临时键,也是一大好事。 -- 是否单点故障:redis 本身有很多中玩法,如客户端一致性 hash,服务器端 sentinel 方案或者 cluster 方案,很难做到一种分布式锁方式能应对所有这些方案。而 ZooKeeper 只有一种玩法,多台机器的节点数据是一致的,没有 redis 的那么多的麻烦因素要考虑。 - -总体上来说 ZooKeeper 实现分布式锁更加的简单,可靠性更高。但 ZooKeeper 因为需要频繁的创建和删除节点,性能上不如 Redis 方式。 - -## 分布式锁方案对比 - -数据库分布式锁,问题比较多,解决起来比较麻烦,不推荐。 - -性能: - -- Redis 分布式锁,其实**需要自己不断自旋去尝试获取锁**,比较消耗性能。 -- ZooKeeper 分布式锁,获取不到锁,注册个监听器即可,不需要不断主动尝试获取锁,性能开销较小。 - -可靠性: - -- 如果是 redis 获取锁的那个客户端出现 bug 挂了,那么只能等待超时时间之后才能释放锁; -- 而 zk 的话,因为创建的是临时 znode,只要客户端挂了,znode 就没了,此时就自动释放锁。 - -综上分析,**ZooKeeper 实现分布式锁更加的简单,可靠性更高**。✅ - -## 参考资料 - -- [分布式锁实现汇总](https://juejin.im/post/5a20cd8bf265da43163cdd9a) -- [Redis 实现分布式锁,以及可重入锁思路](https://www.jianshu.com/p/1c5c1a592088) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/README.md" index cb3dd9abf2..11c97ec692 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/README.md" @@ -8,7 +8,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/2fe804/ +permalink: /pages/a15fcfce/ hidden: true index: false --- @@ -17,17 +17,15 @@ index: false ## 📖 内容 -- [分布式复制](02.分布式复制.md) -- [分布式分区](03.分布式分区.md) -- 选举 -- 共识 -- [分布式事务](05.分布式事务.md) -- [分布式锁](06.分布式锁.md) - -## 📚 资料 - -- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 +- [分布式复制](分布式复制.md) - 关键词:`主从`、`多主`、`无主` +- [分布式分区](分布式分区.md) - 关键词:`分区再均衡`、`路由` +- [分布式共识](分布式共识.md) - 关键词:`共识`、`广播`、`epoch`、`quorum` +- [分布式事务](分布式事务.md) - 关键词:`2PC`、`3PC`、`TCC`、`本地消息表`、`消息事务`、`SAGA` +- [分布式锁](分布式锁.md) - 关键词:`互斥`、`可重入`、`死锁`、`容错`、`自旋尝试`、`公平性` +- [分布式 ID](分布式ID.md) - 关键词:`UUID`、`自增序列`、`雪花算法`、`Leaf` +- [分布式会话](分布式会话.md) - 关键词:`UUID`、`自增序列`、`雪花算法`、`Leaf` +- [分布式协同面试](分布式协同面试.md) 💯 ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217ID.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217ID.md" new file mode 100644 index 0000000000..f30abc1498 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217ID.md" @@ -0,0 +1,369 @@ +--- +title: 分布式 ID +date: 2019-07-24 11:55:00 +order: 04 +categories: + - 分布式 + - 分布式协同 + - 分布式协同综合 +tags: + - 分布式 + - 协同 + - 分布式 ID + - UUID + - Snowflake + - Leaf +permalink: /pages/058bdd15/ +--- + +# 分布式 ID + +## 分布式 ID 简介 + +### 什么是分布式 ID? + +ID 是 Identity 的缩写,用于唯一的标识一条数据。**分布式 ID**,顾名思义,是**用于在分布式系统中唯一标识数据的 ID**。 + +### 为什么需要分布式 ID? + +传统数据库基本都支持针对单表生成唯一性的自增主键。随着数据的膨胀,单机成为了性能和容量的瓶颈。为了解决这个问题,有了分库分表技术。分库分表所要面临的第一个问题是:数据分布在不同机器上,数据库无法保证多个节点上产生的主键唯一。 这就需要用到分布式 ID 了,它起到了分布式系统中**全局 ID** 的作用。 + +### 分布式 ID 的设计目标 + +首先,分布式 ID 应该具备哪些特性呢? + +1. **全局唯一性** - 不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求。 +2. **单调递增** - 保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求。 +3. **高性能** - 分布式 ID 的生成速度要快,对本地资源消耗要小。 +4. **高可用** - 生成分布式 ID 的服务要保证可用性无限接近于 100%。 +5. **安全性** - ID 中不应包括敏感信息。 + +## UUID + +UUID 是通用唯一识别码(Universally Unique Identifier)的缩写,是一种 128 位的标识符,由32个16进制字符表示。**UUID 会根据运行应用的计算机网卡 MAC 地址、时间戳、命名空间等元素,通过一定的随机算法产生**。 + +UUID 不保证全局唯一性,我们需要小心 ID 冲突(尽管这种可能性很小)。 + +[维基百科 - UUID](https://zh.wikipedia.org/wiki/%E9%80%9A%E7%94%A8%E5%94%AF%E4%B8%80%E8%AF%86%E5%88%AB%E7%A0%81) 中介绍了 5 种 UUID 算法。 + +### 版本 1 + +UUID 版本 1 **根据时间和 MAC 地址生成 UUID**。 + +![img](https://bleid.netlify.app/img/version/version_1_uuid.png) + +组成参数说明: + +- **time_low** - 与日期时间信息的低值有关 +- **time_mid** - 与日期时间信息的 mid 值有关 +- **time_high_and_version** - 与日期时间信息的高值有关 +- **clock_seq_hi_and_reserved** - 与计算机系统的内部时钟序列有关 +- **MAC 地址** - 设备的 MAC 地址 + +### 版本 2 + +UUID 版本 2 **根据时间和 MAC 地址、DCE Security 生成 UUID**。 + +它将版本 1 中的日期时间信息替换为本地域名。它没有被广泛使用,因为它降低了唯一性。 + +### 版本 3 + +UUID 版本 3 **使用命名空间和名称生成 UUID**。**命名空间**本身是一个 UUID,URL 名称用作标识。二者组合后,通过 **MD5** 哈希算法计算生成 UUID。 + +![img](https://bleid.netlify.app/img/version/version_3_uuid.png) + +### 版本 5 + +UUID 版本 5 和 版本 4 近似,都**使用命名空间和名称生成 UUID**。差异在于:**版本 3 采用 MD5 作为哈希算法**;**版本 5 采用 SHA1 作为哈希算法**。 + +![img](https://bleid.netlify.app/img/version/version_5_uuid.png) + +**版本 3 、版本 5** - 基于哈希命名空间标识符和名称生成 UUID,差异在于:版本 3 采用 MD5 作为哈希算法;版本 5 采用 SHA1 作为哈希算法。 + +### 版本 4 + +版本 4 随机生成 UUID,不包含其他 UUID 中使用的任何信息 (命名空间、MAC 地址、时间)。识别它的唯一方法是版本 4 UUID,字符只是 **4** 位于 UUID 第三部分的第一个位置。其他字符是随机生成的。 + +![img](https://bleid.netlify.app/img/version/version_4_uuid.png) + +版本 4 是最常见的 UUID 实现,JDK 中也提供了实现,示例如下: + +```java +String uuid = UUID.randomUUID().toString(); +``` + +### UUID 的优缺点 + +- **优点** + - 简单、生成速度较快(本地生成,不依赖其他服务) +- **缺点** + - **无序** - 不能生成递增有序的数字,这不利于一些特定场景。如:MySQL InnoDB 存储引擎使用 B+ 树存储索引数据,索引数据在 B+ 树中是有序排列的。而 UUID 的无序性可能会引起数据位置频繁变动,严重影响性能。 + - **长度过长** - UUID 需要占用 32 个字节 + - **信息不安全** - 基于 MAC 地址生成 UUID 的算法,可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。 + +## 数据库自增序列 + +大多数数据库都支持自增主键。基于此特性,可以利用事务管理控制生成唯一 ID。 + +以 MySQL 举例,我们通过下面的方式即可。 + +(1)创建一个专用于生成 ID 的表 + +```sql +CREATE TABLE `sequence_id` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, + `stub` char(10) NOT NULL DEFAULT '', + PRIMARY KEY (`id`), + UNIQUE KEY `stub` (`stub`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; +``` + +`stub` 字段无意义,只是为了占位,便于我们插入或者修改数据。并且,给 `stub` 字段创建了唯一索引,保证其唯一性。 + +(2)通过 `replace into` 来插入数据。 + +```sql +BEGIN; +REPLACE INTO sequence_id (stub) VALUES ('stub'); +SELECT LAST_INSERT_ID(); +COMMIT; +``` + +插入数据这里,我们没有使用 `insert into` 而是使用 `replace into` 来插入数据,具体步骤是这样的: + +- 第一步:尝试把数据插入到表中。 +- 第二步:如果主键或唯一索引字段出现重复数据错误而插入失败时,先从表中删除含有重复关键字值的冲突行,然后再次尝试把数据插入到表中。 + +这种方式的优缺点也比较明显: + +- **优点**: + - 方案简单 + - 有序 + - ID 长度小 +- **缺点**: + - 性能差 + - 每次获取 ID 都要访问一次数据库,增加了对数据库的压力 + - 不安全,根据发号数量信息可能推测出业务规模 + - 单点问题,如果数据库宕机会造成服务不可用,可以使用高可用方案来解决,但会增加复杂度 + +## 数据库生成号段 + +数据库自增序列这种模式,每次获取 ID 都要请求一次数据库。当请求并发量高时,会给数据库带来很大的压力,并且生成 ID 的性能也比较差。 + +可以采用**批处理**的思路来优化数据库自增序列方案。申请 ID 改为批量获取,不再一次只申请一个 ID,而是一次批量生成一个 segment(号段),号段的大小由 step(步长)控制。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。各个业务不同的发号需求用 biz_tag 字段来区分,每个 biz_tag 的 ID 获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对 biz_tag 分库分表就行。 + +以 MySQL 举例,我们通过下面的方式即可。 + +```sql +CREATE TABLE `leaf_alloc` ( + `biz_tag` varchar(128) NOT NULL DEFAULT '', + `max_id` bigint(20) NOT NULL DEFAULT '1', + `step` int(11) NOT NULL, + `description` varchar(256) DEFAULT NULL, + `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + PRIMARY KEY (`biz_tag`) +) ENGINE=InnoDB; + +insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id') +``` + +重要字段说明: + +- `biz_tag` 用来区分业务 +- `max_id` 表示该 `biz_tag` 目前所被分配的 ID 号段的最大值 +- `step` 表示每次分配的号段长度。原来获取 ID 每次都需要写数据库,现在只需要把 `step` 设置得足够大,比如 1000。那么只有当 1000 个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从 1 减小到了 1/step。 + +大致架构如下图所示: + +![image](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2017/5e4ff128.png) + +test_tag 在第一台 Leaf 机器上是 `1~1000` 的号段,当这个号段用完时,会去加载另一个长度为 step=1000 的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是 `3001~4000`。同时数据库对应的 biz_tag 这条数据的 max_id 会从 3000 被更新成 4000,更新号段的 SQL 语句如下: + +```sql +Begin +UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx +SELECT tag, max_id, step FROM table WHERE biz_tag=xxx +Commit +``` + +**数据库号段模式的优缺点:** + +- **优点**: + - 有序 + - ID 长度小 + - 效率比数据库自增序列方式高很多 +- **缺点** + - 号段使用完,还是需要向数据库发起事务更新,以获取新号段 + - 不安全,根据发号数量信息可能推测出业务规模 + - 单点问题,如果数据库宕机会造成服务不可用,可以使用高可用方案来解决,但会增加复杂度 + +> 扩展:滴滴的 [tinyid](https://github.com/didi/tinyid) 和美团的 [Leaf](https://github.com/Meituan-Dianping/Leaf) 都是基于数据库生成号段方案实现的,不过都各自做了一些优化。 +> +> 美团技术团队还对分布式 ID 生成做了一篇技术分享:[Leaf——美团点评分布式 ID 生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html),其对于数据库号段模式的优化要点如下: +> +> - Leaf 采用双 Buffer 优化,避免号段耗尽时,阻塞以获取新号段。其本质上是:通过双缓存,提前预热号段缓存。 +> - 此外,基于 Atlas(以改名 DBProxy)保障数据库的高可用。也就是保护了号段数据存储的高可用。 + +## 原子计数器 + +一些 NoSQL 数据库提供了原子性的计数器,可以基于这点,来实现分布式 ID。 + +### Redis 生成自增键 + +Redis 的 String 类型提供 `INCR` 和 `INCRBY` 命令将 key 中储存的数字**原子递增**。 + +为避免单点问题,可以采用 Redis Cluster。 + +**Redis 方案的优缺点:** + +- **优点**:高性能、有序 +- **缺点**:和数据库自增序列方案的缺点类似 + +### ZooKeeper 生成自增键 + +利用 ZooKeeper 中的顺序节点特性,很容易使我们创建的 ID 编码具有有序的特性。并且我们也可以通过客户端传递节点的名称,根据不同的业务编码区分不同的业务系统,从而使编码的扩展能力更强。 + +**每个需要 ID 编码的业务服务器可以看作是 ZooKeeper 的客户端**。ID 编码生成器可以作为 ZooKeeper 的服务端。客户端通过发送请求到 ZooKeeper 服务器,来获取编码信息,服务端接收到请求后,发送 ID 编码给客户端。 + +![Drawing 2.png](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/ZooKeeper%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90%E4%B8%8E%E5%AE%9E%E6%88%98-%E5%AE%8C/assets/CgqCHl8RTBGAB7QNAAAvwu3rspw007.png) + +可以利用 ZooKeeper 数据模型中的顺序节点作为 ID 编码。客户端通过调用 create 函数创建顺序节点。服务器成功创建节点后,会响应客户端请求,把创建好的节点信息发送给客户端。客户端用数据节点名称作为 ID 编码,进行之后的本地业务操作。 + +:::details 要点 + +```java +@Slf4j +public class ZookeeperDistributedId { + + public static void main(String[] args) throws Exception { + + // 获取客户端 + RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); + CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); + + // 开启会话 + client.start(); + + String id1 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id1); + + String id2 = client.create() + .creatingParentsIfNeeded() + .withMode(CreateMode.PERSISTENT_SEQUENTIAL) + .forPath("/zkid/id_"); + log.info("id: {}", id2); + + List children = client.getChildren().forPath("/zkid"); + if (CollectionUtil.isNotEmpty(children)) { + for (String child : children) { + client.delete().forPath("/zkid/" + child); + } + } + client.delete().forPath("/zkid"); + + // 关闭客户端 + client.close(); + } + +} +``` + +::: + +**ZooKeeper 方案的优缺点:** + +- **优点**:简单、可靠性高 +- **缺点**:性能不高 + +## 雪花算法(Snowflake) + +雪花算法(Snowflake)是由 Twitter 公布的分布式主键生成算法,**它会生成一个 `64 bit` 的整数**,可以保证不同进程主键的不重复性,以及相同进程主键的有序性。在同一个进程中,它首先是通过时间位保证不重复,如果时间相同则是通过序列位保证。 同时由于时间位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。 + +### 键的组成 + +使用**雪花算法生成的主键,二进制表示形式包含 4 部分**,从高位到低位分表为:1bit 符号位、41bit 时间戳位、10bit 工作进程位以及 12bit 序列号位。 + +- **符号位 (1bit)** + +预留的符号位,恒为零。 + +- **时间戳位 (41bit)** + +41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是:`365 * 24 * 60 * 60 * 1000`。通过计算可知: + +```java +Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L); +``` + +结果约等于 69.73 年。ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日零点开始,可以使用到 2086 年,相信能满足绝大部分系统的要求。 + +- **工作进程位 (10bit)** + +该标志在 Java 进程内是唯一的,如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为 0,可通过属性设置。 + +- **序列号位 (12bit)** + +该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096(2 的 12 次幂),那么生成器会等待到下个毫秒继续生成。 + +雪花算法主键的详细结构见下图: + +![雪花算法](https://shardingsphere.apache.org/document/current/img/sharding/snowflake_cn_v2.png) + +### 时钟回拨 + +服务器时钟回拨会导致产生重复序列,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。 如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。 最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。 + +雪花算法是强依赖于时间的,而如果机器时间发生回拨,有可能会生成重复的 ID。 + +我们可以针对算法做一些优化,来防止时钟回拨生成重复 ID。 + +用当前时间和上一次的时间进行判断,如果当前时间小于上一次的时间那么肯定是发生了回拨。普通的算法会直接抛出异常,这里我们可以对其进行优化,一般分为两个情况: + +- 如果时间回拨时间较短,比如配置 `5ms` 以内,那么可以直接等待一定的时间,让机器的时间追上来。 +- 如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略: + - 直接拒绝,抛出异常。打日志,通知 RD 时钟回滚。 + - 利用扩展位。上面我们讨论过,不同业务场景位数可能用不到那么多比特位,那么我们可以把扩展位数利用起来。比如:当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加 1。两位的扩展位允许我们有三次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。 + +### 灵活定制 + +上面只是一个将 `64bit` 划分的标准,当然也不一定这么做,可以根据不同业务的具体场景来划分,比如下面给出一个业务场景: + +- 服务目前 QPS10 万,预计几年之内会发展到百万。 +- 当前机器三地部署,上海,北京,深圳都有。 +- 当前机器 10 台左右,预计未来会增加至百台。 + +这个时候我们根据上面的场景可以再次合理的划分 62bit,QPS 几年之内会发展到百万,那么每毫秒就是千级的请求,目前 10 台机器那么每台机器承担百级的请求,为了保证扩展,后面的循环位可以限制到 1024,也就是 2^10,那么循环位 10 位就足够了。 + +机器三地部署我们可以用 3bit 总共 8 来表示机房位置,当前的机器 10 台,为了保证扩展到百台那么可以用 7bit 128 来表示,时间位依然是 41bit,那么还剩下 64-10-3-7-41-1 = 2bit,还剩下 2bit 可以用来进行扩展。 + +![img](https://user-gold-cdn.xitu.io/2018/9/29/16624909d2007c22?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) + +### 雪花算法小结 + +雪花算法的**利弊**: + +- **优点** + - 生成的 ID 都是趋势递增的。 + - 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的。 + - 可以根据自身业务特性分配 bit 位,非常灵活。 +- **缺点** + - 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。 + +雪花算法的**适用场景**: + +当我们需要无序不能被猜测的 ID,并且需要一定高性能,且需要 long 型,那么就可以使用我们雪花算法。比如常见的订单 ID,用雪花算法别人就无法猜测你每天的订单量是多少。 + +## 参考资料 + +- [如果再有人问你分布式 ID,这篇文章丢给他](https://juejin.im/post/5bb0217ef265da0ac2567b42) +- [理解分布式 id 生成算法 SnowFlake](https://segmentfault.com/a/1190000011282426) +- [Leaf——美团点评分布式 ID 生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html) +- [UUID 规范](https://www.ietf.org/rfc/rfc4122.txt) +- [百度分布式 ID](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md) +- [ShardingSphere 分布式主键](https://shardingsphere.apache.org/document/current/cn/features/sharding/other-features/key-generator/) +- [7 Famous Approaches to Generate Distributed ID with Comparison Table](https://medium.com/bytebytego-system-design-alliance/7-famous-approaches-to-generate-distributed-id-with-comparison-table-af89afe4601f) +- [冷饭新炒:理解 JDK 中 UUID 的底层实现](https://www.cnblogs.com/throwable/p/14343086.html) +- [What is UUID?](https://bleid.netlify.app/) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/05.\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" similarity index 54% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/05.\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" index f3b30e22c9..6711ade3f0 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/05.\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\344\272\213\345\212\241.md" @@ -1,41 +1,63 @@ --- -title: 分布式事务基本原理 +title: 分布式事务 date: 2019-06-21 11:30:00 -order: 05 categories: - 分布式 - 分布式协同 - 分布式协同综合 tags: - 分布式 - - 数据调度 + - 协同 - 事务 + - 互斥 + - ACID - 2PC + - 3PC - TCC - 本地消息表 - - MQ事务消息 + - 消息事务 - SAGA -permalink: /pages/e1881c/ + - XA +permalink: /pages/d46468f7/ --- -# 分布式事务基本原理 +# 分布式事务 -> **分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。** +## 事务简介 -## 分布式事务简介 +### 什么是事务 + +在数据存储环境中,可能会出现各种各样的问题: + +- 数据库软件或硬件可能会随时失效(包括正在执行写操作的过程中)。 +- 应用程序可能随时崩愤(包括一系列操作执行到中间某一步)。 +- 应用与数据库节点间的连接可能会随时中断,数据库节点间也存在同样问题。 +- 多个客户端可能同时写入数据库,导致数据覆盖。 +- 客户端可能读到一些无意义的、部分更新的数据。 +- 客户端之间由于边界条件竞争所引入的各种奇怪问题。 + +为了解决以上问题,产生了事务这个概念。 + +**事务(Transaction)指的是满足 ACID 特性的一组操作**。事务内的 SQL 语句,要么全执行成功,要么全执行失败。可以通过 `Commit` 提交一个事务,也可以使用 `Rollback` 进行回滚。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190813952.png) + +通俗的说,**事务将多个读、写操作捆绑在一起成为一个逻辑操作单元**。**事务中的所有读写是一个执行的整体,整个事务要么成功(提交)、要么失败(中止或回滚)**。如果失败,应用程序可以安全地重试。这样,由于不需要担心部分失败的情况(无论出于任何原因),应用层的错误处理就变得简单很多。 ### ACID -ACID 是数据库事务正确执行的四个基本要素。 +那么,什么是 ACID 特性呢?ACID 是数据库事务正确执行的四个基本要素的单词缩写: - **原子性(Atomicity)** - - 事务被视为不可分割的最小单元,事务中的所有操作要么全部提交成功,要么全部失败回滚。 + - 原子是指不可分解为更小粒度的东西。事务的原子性意味着:**事务中的所有操作要么全部成功,要么全部失败**。 - 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 + - ACID 中的原子性并不关乎多个操作的并发性,它并没有描述多个线程试图访问相同的数据会发生什么情况,后者其实是由 ACID 的隔离性所定义。 - **一致性(Consistency)** - 数据库在事务执行前后都保持一致性状态。 - 在一致性状态下,所有事务对一个数据的读取结果都是相同的。 + - 一致性本质上要求应用层来维护状态一致(或者恒等),应用程序有责任正确地定义事务来保持一致性。这不是数据库可以保证的事情。 - **隔离性(Isolation)** - - 一个事务所做的修改在最终提交以前,对其它事务是不可见的。 + - **同时运行的事务互不干扰**。换句话说,一个事务所做的修改在最终提交以前,对其它事务是不可见的。 - **持久性(Durability)** - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 @@ -47,13 +69,9 @@ ACID 是数据库事务正确执行的四个基本要素。 - 在并发的情况下,多个事务并行执行,事务不仅要满足原子性,还需要满足隔离性,才能满足一致性。 - 事务满足持久化是为了能应对系统崩溃的情况。 -### 本地事务和分布式事务 - -学习分布式之前,先了解一下本地事务的概念。 +### 什么是分布式事务 -**事务指的是满足 ACID 特性的一组操作**。事务内的 SQL 语句,要么全执行成功,要么全执行失败。可以通过 `Commit` 提交一个事务,也可以使用 `Rollback` 进行回滚。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310092226555.png) +在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为**本地事务**。几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。 **分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。** @@ -63,31 +81,148 @@ ACID 是数据库事务正确执行的四个基本要素。 举个互联网常用的交易业务为例: -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102324283.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190750592.png) 上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102324139.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190750182.png) 可以看到,如果多个数据库之间的数据更新没有保证事务,将会导致出现子系统数据不一致,业务出现问题。 -分布式事务的难点: +分布式事务相比于本地事务,实现复杂度要高很多,主要是因为其存在以下**难点**: - **事务的原子性**:事务操作跨不同节点,当多个节点某一节点操作失败时,需要保证多节点操作的**都做或都不做(All or Nothing)**的原子性。 - **事务的一致性**:当发生网络传输故障或者节点故障,节点间数据复制通道中断,在进行事务操作时需要保证数据一致性,保证事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。 - **事务的隔离性**:事务隔离性的本质就是如何正确多个并发事务的处理的读写冲突和写写冲突,因为在分布式事务控制中,可能会出现提交不同步的现象,这个时候就有可能出现“部分已经提交”的事务。此时并发应用访问数据如果没有加以控制,有可能出现“脏读”问题。 -### CAP 和 BASE +在分布式领域,要实现强一致性,代价非常高昂。因此,有人基于 CAP 理论以及 BASE 理论,有人就提出了**柔性事务**的概念。柔性事务是指:在不影响系统整体可用性的情况下 (Basically Available 基本可用),允许系统存在数据不一致的中间状态 (Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。**并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐**。 + +### CAP 理论 + +> CAP 定理是加州大学计算机科学家埃里克·布鲁尔提出来的猜想,后来被证明成为分布式计算领域公认的定理。 + +**CAP 定理**,指的是:**在一个分布式系统中,当发生网络分区时,那么强一致性和可用性只能二选一**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310200746619.png) + +CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20211102180526.png) + +- 一致性(**C**onsistency):在任何给定时间,网络中的所有节点都具有完全相同(最近)的值。 +- 可用性(**A**vailability):对网络的每个请求都会收到响应,但不能保证返回的数据是最新的。 +- 分区容错性(**P**artition Tolerance):即使任意数量的节点出现故障,网络仍会继续运行。 + +#### 一致性 + +一致性(Consistency)指的是**多个数据副本是否能保持一致**的特性。 + +在一致性的条件下,分布式系统在执行写操作成功后,如果所有用户都能够读取到最新的值,该系统就被认为具有强一致性。 + +数据一致性又可以分为以下几点: + +- **强一致性** - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。 +- **最终一致性** - 即物理存储的数据可能是不一致的,终端用户访问到的数据可能也是不一致的,但系统经过一段时间的自我修复和修正,数据最终会达到一致。 + +举例来说,某条记录是 v0,用户向 G1 发起一个写操作,将其改为 v1。 + +![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071602.png) + +接下来,用户的读操作就会得到 v1。这就叫一致性。 + +![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071603.png) + +问题是,用户有可能向 G2 发起读操作,由于 G2 的值没有发生变化,因此返回的是 v0。G1 和 G2 读操作的结果不一致,这就不满足一致性了。 + +![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071604.png) + +为了让 G2 也能变为 v1,就要在 G1 写操作的时候,让 G1 向 G2 发送一条消息,要求 G2 也改成 v1。 + +![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071605.png) + +这样的话,用户向 G2 发起读操作,也能得到 v1。 + +![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071606.png) + +#### 可用性 + +可用性指**分布式系统在面对各种异常时可以提供正常服务的能力**,可以用系统可用时间占总时间的比值来衡量,4 个 9 的可用性表示系统 `99.99%` 的时间是可用的。 + +在可用性条件下,系统提供的服务一直处于可用的状态,对于用户的每一个操作请求总是能够在有限的时间内返回结果。 + +#### 分区容错性 + +分区容错性(Partition Tolerance)指 **分布式系统在遇到任何网络分区故障的时候,仍然需要能对外提供一致性和可用性的服务,除非是整个网络环境都发生了故障**。 + +在一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障,使得有些节点之间不连通了,整个网络就分成了几块区域。数据就散布在了这些不连通的区域中,这就叫分区。 -CAP 定理又称为 CAP 原则,指的是:**在一个分布式系统中, `一致性(C:Consistency)`、`可用性(A:Availability)` 和 `分区容忍性(P:Partition Tolerance)`,最多只能同时满足其中两项**。 +假设,某个数据项只在一个节点中保存,那么分区出现后,和这个节点不连通的部分就访问不到这个数据了。这时分区就是无法容忍的。 -BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State)`** 和 **`最终一致性(Eventually Consistent)`** 三个短语的缩写。BASE 理论是对 CAP 中一致性和可用性权衡的结果,它的理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 +提高分区容错性的办法就是一个数据项复制到多个节点上,那么出现分区之后,这一数据项就可能分布到各个区里。容错性就提高了。 + +然而,要把数据复制到多个节点,就会带来一致性的问题,就是多个节点上面的数据可能是不一致的。要保证一致,每次写操作就都要等待全部节点写成功,而这等待又会带来可用性的问题。 + +总的来说就是,数据存在的节点越多,分区容错性越高,但要复制更新的数据就越多,一致性就越难保证。为了保证一致性,更新所有节点数据所需要的时间就越长,可用性就会降低。 + +大多数分布式系统都分布在多个子网络,每个子网络就叫做一个区(Partition)。分区容错的意思是,区间通信可能失败。比如,一台服务器放在中国,另一台服务器放在美国,这就是两个区,它们之间可能无法通信。 + +![img](https://www.wangbase.com/blogimg/asset/201807/bg2018071601.png) + +上图中,G1 和 G2 是两台跨区的服务器。G1 向 G2 发送一条消息,G2 可能无法收到。系统设计的时候,必须考虑到这种情况。 + +**一般来说,分区容错无法避免**,因此可以认为 CAP 的 P 总是成立。CAP 定理告诉我们,剩下的 C 和 A 无法同时做到。 + +#### AP or CP + +在分布式系统中,分区容错性必不可少,因为需要总是假设网络是不可靠的。因此,**CAP 理论实际在是要在可用性和一致性之间做权衡**。 + +由于分布式数据存储(如区块链)的性质,分区容错性是一个既定的事实;网络中总会有失败/无法访问的节点(尤其是因为互联网的不稳定特性)。 CAP 定理指出,当存在 P(分区)时,必须在 C(一致性)或 A(可用性)之间进行选择。 + +(1)AP 模式 + +> **AP** **模式**:对网络的每个请求都会收到响应,即使网络由于网络分区故障而无法保证它是最新的。 + +选择 **AP** **模式**,实现了服务的高可用。用户访问系统的时候,都能得到响应数据,不会出现响应错误;但是,当出现分区故障时,相同的读操作,访问不同的节点,得到响应数据可能不一样。 + + + +(2)CP 模式 + +> **CP** **模式**:如果由于网络分区(故障节点)而无法保证特定信息是最新的,则系统将返回错误或超时。 + +选择 **CP** **模式**,这样能够提供一部分的可用性。采用 CP 模型的分布式系统,一旦因为消息丢失、延迟过高发生了网络分区,就影响用户的体验和业务的可用性。因为为了防止数据不一致,集群将拒绝新数据的写入。 + + + +### BASE 理论 + +#### 什么是 BASE 定理 + +> **BASE 定理是对 CAP 中一致性和可用性权衡的结果**。 + +不符合 ACID 标准的系统有时被冠以 BASE。BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State)`** 和 **`最终一致性(Eventually Consistent)`** 三个短语的缩写。 + +BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +- **基本可用(Basically Available)**分布式系统在出现故障的时候,**保证核心可用,允许损失部分可用性**。例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。 +- **软状态(Soft State)**指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即**允许系统不同节点的数据副本之间进行同步的过程存在延时**。 +- **最终一致性(Eventually Consistent)**强调的是**系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态**。 + +#### BASE vs. ACID + +BASE 的理论的**核心思想**是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过**牺牲强一致性来达到可用性**,通常运用在大型分布式系统中。 + +BASE 唯一可以确定的是“它不是 ACID”,此外它几乎没有承诺任何东西。 + + ### 柔性事务 -在电商等互联网场景下,传统的事务在数据库性能和处理能力上都暴露出了瓶颈。在分布式领域基于 CAP 理论以及 BASE 理论,有人就提出了**柔性事务**的概念。 +在分布式领域,要实现强一致性,代价非常高昂。因此,有人基于 CAP 理论以及 BASE 理论,提出了**柔性事务**的概念。 -柔性事务是指:在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。**并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐**。 +柔性事务是指:在不影响系统整体可用性的情况下 (Basically Available 基本可用),允许系统存在数据不一致的中间状态 (Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。**并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐**。 下面介绍的是实现柔性事务的一些常见特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样。 @@ -102,7 +237,7 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State 在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。 -二阶段提交的思路可以概括为:**参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作**。 +二阶段提交的思路可以概括为:**参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈,决定提交或回滚**。 核心思想就是对每一个事务都采用先尝试后提交的处理方式,处理后所有的读操作都要能获得最新的数据,因此也可以将二阶段提交看作是一个强一致性算法。 @@ -118,20 +253,20 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State #### 阶段 2:提交阶段 -如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源) 接下来分两种情况分别讨论提交阶段的过程。 +如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚 (rollback) 消息;否则,发送提交 (commit) 消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源) 接下来分两种情况分别讨论提交阶段的过程。 **情况 1,当所有参与者均反馈 yes,提交事务**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102325237.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190751196.png) > 1. 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。 > 2. 参与者执行 commit 请求,并释放整个事务期间占用的资源。 -> 3. 各参与者向协调者反馈 ack(应答)完成的消息。 +> 3. 各参与者向协调者反馈 ack(应答)完成的消息。 > 4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。 **情况 2,当任何阶段 1 一个参与者反馈 no,中断事务**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102325376.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190751172.png) > 1. 协调者向所有参与者发出回滚请求(即 rollback 请求)。 > 2. 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。 @@ -158,7 +293,7 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State #### 阶段 1:canCommit -协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应(参与者不执行事务操作),否则返回 no 响应: +协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应(参与者不执行事务操作),否则返回 no 响应: 1. 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。 2. 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。 @@ -169,7 +304,7 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State **情况 1:阶段 1 所有参与者均反馈 yes,参与者预执行事务**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102326223.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190752281.png) > 1. 协调者向所有参与者发出 preCommit 请求,进入准备阶段。 > 2. 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。 @@ -177,7 +312,7 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State **情况 2:阶段 1 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102326360.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190752525.png) > 1. 协调者向所有参与者发出 abort 请求。 > 2. 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。 @@ -188,33 +323,28 @@ BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State **情况 1:阶段 2 所有参与者均反馈 ack 响应,执行真正的事务提交**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102327231.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190753250.png) -> 1. 如果协调者处于工作状态,则向所有参与者发出 do Commit 请求。 -> 2. 参与者收到 do Commit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。 +> 1. 如果协调者处于工作状态,则向所有参与者发出 doCommit 请求。 +> 2. 参与者收到 doCommit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。 > 3. 各参与者向协调者反馈 ack 完成的消息。 > 4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。 **情况 2:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102327163.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190753329.png) > 1. 如果协调者处于工作状态,向所有参与者发出 abort 请求。 > 2. 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。 > 3. 各参与者向协调者反馈 ack 完成的消息。 > 4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。 -注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 do Commit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。 +注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 doCommit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。 ### 方案总结 -优点: - -- 相比二阶段提交,三阶段降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。 - -缺点: - -- 数据不一致问题依然存在,当在参与者收到 preCommit 请求后等待 do commite 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。 +- 优点:**相比二阶段提交,三阶段降低了阻塞范围**,在**等待超时后协调者或参与者会中断事务**。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。 +- 缺点:**数据不一致问题依然存在**,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。 ## 补偿事务(TCC) @@ -238,8 +368,8 @@ TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Com 从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成: -- 完成所有业务检查( 一致性 ) -- 预留必须业务资源( 准隔离性 ) +- 完成所有业务检查(一致性) +- 预留必须业务资源(准隔离性) - Try 尝试执行业务 TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。 假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。 @@ -250,25 +380,25 @@ TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Com **Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作** -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102329798.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190757821.png) 这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。 **Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段** -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102329798.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190757821.png) Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。 ### 方案总结 -TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点: +TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点: - **性能提升** - 具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。 - **数据最终一致性** - 基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。 - **可靠性** - 解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。 -缺点: TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。 +缺点: TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,**业务耦合度较高**,提高了开发成本。 ## 本地消息表 @@ -282,7 +412,7 @@ TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相 ### 处理流程 -下面把分布式事务最先开始处理的事务方成为事务主动方,在事务主动方之后处理的业务内的其他事务成为事务被动方。 +下面把分布式事务最先开始处理的事务方称为事务主动方,在事务主动方之后处理的业务内的其他事务称为事务被动方。 为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建 2 个步骤,库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。 @@ -290,16 +420,16 @@ TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相 整个业务处理流程如下: -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102329044.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190758570.png) -> 1. **步骤 1 事务主动方处理本地事务。** 事务主动发在本地事务中处理业务更新操作和写消息表操作。 上面例子中库存服务阶段再本地事务中完成扣减库存和写消息表(图中 1、2)。 -> 2. **步骤 2 事务主动方通过 MQ 通知事务被动方处理事务**。 消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。 上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。 -> 3. **步骤 3 事务被动方通过 MQ 反会处理结果。** 上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8) +> 1. **步骤 1、事务主动方处理本地事务。** 事务主动发在本地事务中处理业务更新操作和写消息表操作。 上面例子中库存服务阶段再本地事务中完成扣减库存和写消息表(图中 1、2)。 +> 2. **步骤 2、事务主动方通过 MQ 通知事务被动方处理事务**。 消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。 上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。 +> 3. **步骤 3、事务被动方通过 MQ 返回处理结果。** 上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8) 为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下: > - 当步骤 1 处理出错,事务回滚,相当于什么都没发生。 -> - 当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送的消息中间件进行处理。事务被动方消费事务消息重试处理。 +> - 当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询超时 d 的消息数据,再次发送消息到 MQ 进行处理。事务被动方消费事务消息重试处理。 > - 如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。 > - 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。 @@ -307,16 +437,16 @@ TCC 事务机制相对于传统事务机制(X/Open XA),TCC 事务机制相 方案的优点如下: -- 从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对 MQ 中间件特性的依赖。 -- 方案轻量,容易实现。 +- 从应用设计开发的角度实现了消息数据的可靠性,**消息数据的可靠性不依赖于消息中间件**,弱化了对 MQ 中间件特性的依赖。 +- **方案简单**,容易实现。 缺点如下: -- 与具体的业务场景绑定,耦合性强,不可复用。 -- 消息数据与业务数据同库,占用业务系统资源。 +- 与具体的业务场景绑定,**耦合性高,不可复用**。 +- 需要额外维护消息数据的传输,占用业务系统资源。 - 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。 -## MQ 事务 +## 消息事务 MQ 事务方案本质是利用 MQ 功能实现的本地消息表。事务消息需要消息队列提供相应的功能才能实现,Kafka 和 RocketMQ 都提供了事务相关功能。 @@ -331,15 +461,17 @@ MQ 事务方案本质是利用 MQ 功能实现的本地消息表。事务消息 事务消息交互流程如下图所示。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310102330908.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190758755.png) 1. 生产者将消息发送至 Apache RocketMQ 服务端。 2. Apache RocketMQ 服务端将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。 3. 生产者开始执行本地事务逻辑。 4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit 或是 Rollback),服务端收到确认结果后处理逻辑如下: - - 二次确认结果为 Commit:服务端将半事务消息标记为可投递,并投递给消费者。 - - 二次确认结果为 Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。 -5. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为 Unknown 未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 **说明** 服务端回查的间隔时间和最大回查次数,请参见[参数限制](https://rocketmq.apache.org/zh/docs/introduction/03limits)。 + +- 二次确认结果为 Commit:服务端将半事务消息标记为可投递,并投递给消费者。 +- 二次确认结果为 Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。 + +5. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为 Unknown 未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 **说明** 服务端回查的间隔时间和最大回查次数,请参见 [参数限制](https://rocketmq.apache.org/zh/docs/introduction/03limits)。 6. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。 7. 生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行处理。 @@ -349,69 +481,69 @@ MQ 事务方案本质是利用 MQ 功能实现的本地消息表。事务消息 - 事务待提交:半事务消息被发送到服务端,和普通消息不同,并不会直接被服务端持久化,而是会被单独存储到事务存储系统中,等待第二阶段本地事务返回执行结果后再提交。此时消息对下游消费者不可见。 - 消息回滚:第二阶段如果事务执行结果明确为回滚,服务端会将半事务消息回滚,该事务消息流程终止。 - 提交待消费:第二阶段如果事务执行结果明确为提交,服务端会将半事务消息重新存储到普通存储系统中,此时消息对下游消费者可见,等待被消费者获取并消费。 -- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ 会对消息进行重试处理。具体信息,请参见[消费重试](https://rocketmq.apache.org/zh/docs/featureBehavior/10consumerretrypolicy)。 +- 消费中:消息被消费者获取,并按照消费者本地的业务逻辑进行处理的过程。 此时服务端会等待消费者完成消费并提交消费结果,如果一定时间后没有收到消费者的响应,Apache RocketMQ 会对消息进行重试处理。具体信息,请参见 [消费重试](https://rocketmq.apache.org/zh/docs/featureBehavior/10consumerretrypolicy)。 - 消费提交:消费者完成消费处理,并向服务端提交消费结果,服务端标记当前消息已经被处理(包括消费成功和失败)。 Apache RocketMQ 默认支持保留所有消息,此时消息数据并不会立即被删除,只是逻辑标记已消费。消息在保存时间到期或存储空间不足被删除前,消费者仍然可以回溯消息重新消费。 -- 消息删除:Apache RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。更多信息,请参见[消息存储和清理机制](https://rocketmq.apache.org/zh/docs/featureBehavior/11messagestorepolicy)。 +- 消息删除:Apache RocketMQ 按照消息保存机制滚动清理最早的消息数据,将消息从物理文件中删除。更多信息,请参见 [消息存储和清理机制](https://rocketmq.apache.org/zh/docs/featureBehavior/11messagestorepolicy)。 ### MQ 事务方案总结 相比本地消息表方案,MQ 事务方案优点是: -- 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。 -- 吞吐量优于使用本地消息表方案。 +- **业务解耦** - 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。 +- **吞吐量优于本地消息表**方案。 缺点是: -- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) -- 业务处理服务需要实现消息状态回查接口 +- **一次消息发送需要两次网络请求** (half 消息 + commit/rollback 消息) +- **业务处理服务需要实现消息状态回查接口** -## SAGA +## SAGA 事务 ### 方案简介 -Saga 事务源于 1987 年普林斯顿大学的 Hecto 和 Kenneth 发表的如何处理 long lived transaction(长活事务)论文,Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。 +1987 年,Hector Garcia-Molina 和 Kenneth Salem 发表了名为 [SAGAS](https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf) 的论文,讲述了如何处理 long lived transaction(长活事务)。Saga 事务的核心思想是:将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序依次调用补偿操作。 ### 处理流程 **Saga 事务基本协议如下**: -- 每个 Saga 事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。 -- 每个 Ti 都有对应的幂等补偿动作 Ci,补偿动作用于撤销 Ti 造成的结果。 +- **将长事务拆分为多个有序子事务** - 每个 Saga 事务由一系列幂等的有序子事务 (sub-transaction) Ti 组成。 +- **每个子事务 Ti 都有对应的幂等补偿动作 Ci**,补偿动作用于撤销 Ti 造成的结果。 可以看到,和 TCC 相比,Saga 没有“预留”动作,它的 Ti 就是直接提交到库。 下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分 Saga 的执行顺序有两种: -![Saga事务执行顺序](https://user-gold-cdn.xitu.io/2018/12/10/1679817d8ce9b4b7?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190758952.png) -- 事务正常执行完成 T1, T2, T3, ..., Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。 -- 事务回滚 T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。 +- 事务正常执行完成 T1, T2, T3, ..., Tn,例如:扣减库存 (T1),创建订单 (T2),支付 (T3),依次有序完成整个事务。 +- 事务回滚 T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n,例如:扣减库存 (T1),创建订单 (T2),支付 (T3,支付失败),支付回滚 (C3),订单回滚 (C2),恢复库存 (C1)。 #### 恢复策略 Saga 定义了两种恢复策略: -- 向前恢复(forward recovery) +- 向前恢复 (forward recovery) -![Saga事务向前恢复](https://user-gold-cdn.xitu.io/2018/12/10/1679817da631d59c?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190759028.png) -对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中 j 是发生错误的子事务(sub-transaction)。该情况下不需要 Ci。 +对应于上面第一种执行顺序,**适用于必须要成功的场景**,**失败需要进行重试**,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中 j 是发生错误的子事务 (sub-transaction)。该情况下不需要 Ci。 -- 向后恢复(backward recovery) +- 向后恢复 (backward recovery) -![Saga事务向后恢复](https://user-gold-cdn.xitu.io/2018/12/10/1679817da706b3c2?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190800829.png) -对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。 +对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务 (sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。 Saga 事务常见的有两种不同的实现方式:命令协调和事件编排。 #### 命令协调 -- **命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。** +- **命令协调 (Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。** 中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。 -![命令协调模式](https://user-gold-cdn.xitu.io/2018/12/10/1679817daa1798dd?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190800891.png) 以电商订单的例子为例: @@ -421,7 +553,7 @@ Saga 事务常见的有两种不同的实现方式:命令协调和事件编排 > 4. OSO 向支付服务请求支付,支付服务回复处理结果。 > 5. 主业务逻辑接收并处理 OSO 事务处理结果回复。 -中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。 +中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。 #### 事件编排 @@ -433,7 +565,7 @@ Saga 事务常见的有两种不同的实现方式:命令协调和事件编排 以电商订单的例子为例: -![事件编排模式](https://user-gold-cdn.xitu.io/2018/12/10/1679817dba9b2b61?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190808479.png) > 1. 事务发起方的主业务逻辑发布开始订单事件 > 2. 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件 @@ -441,7 +573,7 @@ Saga 事务常见的有两种不同的实现方式:命令协调和事件编排 > 4. 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件 > 5. 主业务逻辑监听订单已支付事件并处理。 -事件/编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。 +事件编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。 ### 方案总结 @@ -450,7 +582,7 @@ Saga 事务常见的有两种不同的实现方式:命令协调和事件编排 优点如下: - 服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器 -- 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。 +- 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。 - 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试 缺点如下: @@ -478,12 +610,32 @@ Saga 事务常见的有两种不同的实现方式:命令协调和事件编排 介绍完分布式事务相关理论和常见解决方案后,最终的目的在实际项目中运用,因此,总结一下各个方案的常见的使用场景。 -![方案比较](https://user-gold-cdn.xitu.io/2018/12/10/1679817dc68ae74d?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +分布式事务的常见方案如下: + +- **两阶段提交(2PC)** - 将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。**参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈信息决定各参与者是否要提交操作还是中止操作**。 +- **三阶段提交(3PC)** - 与二阶段提交不同的是,**引入超时机制**。同时在协调者和参与者中都引入超时机制。将二阶段的准备阶段拆分为 2 个阶段,插入了一个 preCommit 阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。 +- **补偿事务(TCC)** + - **Try** - 操作作为一阶段,负责资源的检查和预留。 + - **Confirm** - 操作作为二阶段提交操作,执行真正的业务。 + - **Cancel** - 是预留资源的取消。 +- **本地消息表** - 在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。 +- **消息事务** - 基于 MQ 的分布式事务方案其实是对本地消息表的封装。 +- **SAGA** - Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。 + +分布式事务方案对比: - 2PC/3PC 依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。 - TCC 适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。 -- 本地消息表/MQ 事务 都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。 -- Saga 事务 由于 Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。 Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。 +- 本地消息表/消息事务都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。 +- Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。 + +| | 2PC | 3PC | TCC | 本地消息表 | MQ 事务 | SAGA | +| ---------- | --- | --- | --- | ---------- | ------- | ---- | +| 数据一致性 | 强 | 强 | 若 | 弱 | 弱 | 弱 | +| 容错性 | 低 | 低 | 高 | 高 | 高 | 高 | +| 复杂性 | 中 | 高 | 高 | 低 | 低 | 高 | +| 性能 | 低 | 低 | 中 | 中 | 高 | 中 | +| 维护成本 | 低 | 中 | 高 | 中 | 中 | 高 | ### 分布式事务方案设计 @@ -499,6 +651,10 @@ Saga 事务常见的有两种不同的实现方式:命令协调和事件编排 ## 参考资料 +- [**Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services**](https://www.comp.nus.edu.sg/~gilbert/pubs/BrewersConjecture-SigAct.pdf),[**解读**](https://juejin.cn/post/6844903936718012430) - 经典的 CAP 理论,即:在一个分布式系统中,当发生网络分区时,那么强一致性和可用性只能二选一。 +- [**CAP Twelve Years Later: How the “Rules” Have Changed**](https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/), [**解读**](https://www.zhihu.com/question/64778723/answer/224266038) - CAP 理论的新解读,并阐述 CAP 理论的一些常见误区。 +- [**BASE: An Acid Alternative**](https://www.semanticscholar.org/paper/BASE%3A-An-Acid-Alternative-Pritchett/2e72e6c022dd33115304ecfcb6dad7ea609534a4),[**译文**](https://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html) - BASE 理论是对 CAP 中一致性和可用性的权衡,提出采用适当的方式来使系统达到最终一致性。 - [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html) - [理解分布式事务](https://juejin.im/post/5c0e5bf8e51d45063322fe50) -- [RocketMQ 官方文档之事务消息](https://rocketmq.apache.org/zh/docs/featureBehavior/04transactionmessage) \ No newline at end of file +- [RocketMQ 官方文档之事务消息](https://rocketmq.apache.org/zh/docs/featureBehavior/04transactionmessage) +- [SAGAS](https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/10.\345\210\206\345\270\203\345\274\217\344\274\232\350\257\235.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\344\274\232\350\257\235.md" similarity index 99% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/10.\345\210\206\345\270\203\345\274\217\344\274\232\350\257\235.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\344\274\232\350\257\235.md" index 0ee5fd1f7d..d7034d0946 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/10.\345\210\206\345\270\203\345\274\217\344\274\232\350\257\235.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\344\274\232\350\257\235.md" @@ -1,15 +1,16 @@ --- title: 分布式会话基本原理 date: 2019-06-04 23:42:00 -order: 10 categories: - 分布式 - - 分布式调度 + - 分布式协同 + - 分布式协同综合 tags: - 分布式 - - 流量调度 - - 会话 -permalink: /pages/95e45f/ + - 协同 + - Cookie + - Session +permalink: /pages/9f390e41/ --- # 分布式会话基本原理 @@ -281,4 +282,4 @@ public class TestController { - [聊一聊 session 和 cookie](https://juejin.im/post/5aede266f265da0ba266e0ef) - [YouTube 视频 - What is a cookie?](https://www.youtube.com/watch?v=I01XMRo2ESg) - [YouTube 视频 - How cookies can track you (Simply Explained)](https://www.youtube.com/watch?v=QWw7Wd2gUJk) -- [MDN HTTP cookies](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies) \ No newline at end of file +- [MDN HTTP cookies](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Cookies) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\205\261\350\257\206.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\205\261\350\257\206.md" new file mode 100644 index 0000000000..53466791fc --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\205\261\350\257\206.md" @@ -0,0 +1,141 @@ +--- +title: 分布式共识 +date: 2024-05-07 06:34:08 +categories: + - 分布式 + - 分布式协同 + - 分布式协同综合 +tags: + - 分布式 + - 协同 + - 共识 + - 广播 + - epoch + - quorum +permalink: /pages/4ce1fe4e/ +--- + +# 分布式共识 + +## 什么是分布式共识 + +分布式系统最重要的抽象之一就是**共识(consensus):所有的节点就某一项提议达成一致**。 + +共识问题通常形式化如下:一个或多个节点可以**提议(propose)** 某些值,而集群中的所有有效节点根据共识算法进行协商,最终**决议(decides)** 采纳某个节点的提议。 + +而共识算法必须满足以下性质: + +1. 达成一致(Uniform agreement) - 没有两个节点的决定不同。 +2. 完整性(Integrity) - 每个节点最多决议一次。 +3. 有效性(Validity) - 如果一个节点决定了值 `v` ,则 `v` 由某个节点所提议。 +4. 终止(Termination) - 由所有未崩溃的节点来最终决议。 + +**达成一致**和**完整性**定义了共识算法的核心思想:所有人同意了相同的结果,且一旦决定了,就不能改变主意。**有效性** 主要是为了排除无效的提案。如果不关心容错,那么满足前三个属性很容易:你可以将一个节点做为 “独裁者”,并让该节点做出所有的决定。但如果该节点失效,那么系统就无法再做出任何决定。事实上,2PC 就存在这种问题:如果协调者失效,那么存疑的参与者就无法决定提交还是中止。 + +**终止** 意味着:即使部分节点出现故障,其他节点也必须达成共识。当然,算法可以容忍的失效节点数是有限的:需要**超过半数以上**的服务器达成一致。假设有 N 台服务器, 大于等于 `N/2 + 1` 台服务器就算是半数以上了 。 + +> 共识(Consensus)与一致性(Consistency)的区别:一致性是指数据不同副本之间的差异;而共识是指达成一致性的方法与过程。很多中文资料把 Consensus 翻译为一致性,但其实是不准确的。 + +## 为什么需要分布式共识 + +对于一个主从复制的数据库,如果主节点发生失效,就需要切换到另一个节点。如果主节点故障了,集群就会天下大乱,就好比一个国家的皇帝驾崩了,国家大乱一样。比如,数据库集群中主节点故障后,可能导致每个节点上的数据会不一致。这,就应了那句话“国不可一日无君”,对应到分布式系统中就是“集群不可一刻无主”。集群中的有效节点可以**采用共识算法来选举新的主节点**。 + +某一时刻必须只有一个主节点,所有的节点必须就此达成一致。如果有两个节点都自认为是主节点,就会发生**脑裂**,导致数据丢失。正确实现共识算怯则可以避免此类问题。 + +## 一致性保证 + + + +## 线性化 + +线性化(一种流行的一致性模型) 其目标是使多副本对外看起来好像是单一副本,然后所有操作以原子方式运行,就像一个单线程程序操作变量一样。线性化的概念简单,容易理解,但它的主要问题在于性能,特别是在网络延迟较大的环境中。 + +## 顺序保证 + +线性化是将所有操作都放在唯一的、全局有序时间线上,而因果性则不同,它为我们提供了一个弱一致性模型: 允许存在某些井发事件,所以版本历史 +是一个包含多个分支与合井的时间线。因果一致性避免了线性化昂贵的协调开销,且对网络延迟的敏感性要低很多。 + +## 分布式共识能否达成 + +Fischer、Lynch 和 Paterson (FLP)在 [**Impossibility of Distributed Consensus with One Faulty Process**](https://groups.csail.mit.edu/tds/papers/Lynch/jacm85.pdf) 论文中论证了:在一个**异步**系统中,即使只有一个进程出现了故障,也没有算法能**保证**达成共识。 + +简单来说,在一个异步系统中,由于进程可以随时发出响应,所以没有办法分辨一个进程是速度很慢还是已经崩溃,这不满足终止性(Termination)。 + +> **共识的不可能性** +> +> FLP 是一种限制性很强的模型,它假定共识性算法不能使用任何时钟或超时。如果允许算法使用 **超时** 或其他方法来识别可疑的崩溃节点(即使怀疑有时是错误的),则共识变为一个可解的问题。因此,虽然 FLP 是关于共识不可能性的重要理论结果,但现实中的分布式系统通常是可以达成共识的。 + +## 分布式共识算法 + +共识意味着就某一项提议,所有节点做出一致的决定,而且决定不可撤销。通过逐一分析,事实证明,多个广泛的问题最终都可以归结为共识,并且彼此等价(这就意味着,如果找到其中一个解决方案,就可以比较容易地将其转换为其他问题的解决方案)。这些等价的问题包括: + +- 可线性化的比较-设置寄存器 - 寄存器需要根据当前值是否等于输入的参数, 来自动决定接下来是否应该设置新值。 +- 原子事务提交 - 数据库需要决定是否提交或中止分布式事务。 +- 全序广播 - 消息系统要决定以何种顺序发送消息。 +- 锁与租约 - 当多个客户端争抢锁或租约时,要决定其中哪一个成功。 +- 成员/协调服务 - 对于失败检测器(例如超时机制),系统要决定节点的存活状态(例如基于会i舌超时)。 +- 唯一性约束 - 当多个事务在相同的主键上试图井发创建冲突资源时,约束条件要决定哪一个被允许,哪些违反约束因而必须失败。 + +如果系统只存在一个节点,或者愿意把所有决策功能都委托给某一个节点,那么事情就变得很简单。这和主从复制数据库的情形是一样的,即由主节点负责所有的决策事宜,正因如此,这样的数据库可以提供线性化操作、唯一性约束、完全有序的复制日志等。 + +然而,如果唯一的主节点发生故障,或者出现网络中断而导致主节点不可达,这样的系统就会陷入停顿状态。有以下三种基本思路来处理这种情况: + +- 系统服务停止,井等待主节点恢复。许多XA I JTA 事务协调者采用了该方式。本质上,这种方怯并没有完全解决共识问题,因为它不满足终止性条件,试想如果主节点没法恢复,则系统就会永远处于停顿状态。 +- 人为介入来选择新的主节点,并重新配置系统使之生效。许多关系数据库都采用这种方怯。本质上它引入了一种“上帝旨意” 的共识, 即在计算机系统之外由人 + 类来决定最终命运。故障切换的速度完全取决于人类的操作,通常比计算机慢。 +- 采用算i法来自动选择新的主节点。这需要一个共识算法,我们建议采用那些经过验证的共识系统来确保正确处理各种网络异常。 + +共识算法选举主节点的过程如同投票选举领导者(Leader),参选者(Candidate)需要说服大多数投票者(Follower)投票给他。一旦选举出领导者,就由领导者发号施令,所有追随者必须服从命令。 + +常见的分布式共识算法有: + +- [Paxos 算法](https://dunwu.github.io/waterdrop/pages/ea903d16/) +- [Raft 算法](https://dunwu.github.io/waterdrop/pages/9386474c/) - 应用代表:Redis、etcd +- [Zab 算法](https://dunwu.github.io/waterdrop/pages/51168337/) - 应用代表:ZooKeeper + +这些算法之间有不少相似之处,但并不相同。下面,将大致介绍一下它们的共同思想。 + +### 全序广播 + +全序广播要求将消息按照相同的顺序,恰好传递一次,准确传送到所有节点。这相当于进行了几轮共识:在每一轮中,节点提议下一条要发送的消息,然后决定在全序中下一条要发送的消息。 + +所以,全序广播相当于重复进行多轮共识(每次共识决定与一次消息传递相对应): + +- 由于 **一致同意** 属性,所有节点决定以相同的顺序传递相同的消息。 +- 由于 **完整性** 属性,消息不会重复。 +- 由于 **有效性** 属性,消息不会被损坏,也不能凭空编造。 +- 由于 **终止** 属性,消息不会丢失。 + +Raft 和 Zab 直接实现了全序广播,因为这样做比重复**一次一值(one value a time)**的共识更高效。在 Paxos 的情况下,这种优化被称为 Multi-Paxos。 + +### 主从复制和共识 + +主从复制将所有的写入操作都交给领导者,并以相同的顺序将状态变化广播同步到追随者,从而保持一致性。这实际上不就是一个全序广播吗?为什么不需要担心共识问题呢? + +因为,这种场景下实际是一种独裁型的共识模型:只有一个节点被允许接收写入(即决定写入复制日志的顺序),如果该节点发生故障,则系统将无法写入,直到选出新的领导者。 + +### 纪元和法定人数 + +为了保证领导者是独一无二的,共识算法通常会定义一个逻辑时钟,用于表示选举领导者的投票轮次(纪元),而共识算法要保证每界选举得出的领导者是惟一的。不同算法中,对代表逻辑时钟的值定义不同,但作用是共通的:在 Paxos 中称其为选票(ballot);在 Raft 中称其为任期(term);在 Zab 中称其为纪元(epoch)。 + +每当现任领导者被认为宕机时,节点间就会发起一场投票,选举出新的领导者。这次选举被赋予一个全序且单调递增的纪元编号。如果出现两个不同时代的领导者,则以更高纪元编号的领导为主。 + +在每轮选举中,参选者如果要赢得选举,当选领导者,必须获得法定人数**(quorum)** 的选票。通常,会约定法定人数为**超过半数以上**,举例来说:假设总共有 N 张投票, 大于等于 `N/2 + 1` 张投票就算是半数以上了 。 + +### 共识的局限性 + +#### 共识对于集群节点数的限制 + +多数派共识算法的核心是少数服从多数,获得投票多的节点胜出。这对于集群节点数有以下限制: + +- **集群中最多可以容忍半数以下的节点出现故障**。因为,一旦故障节点数达到半数,则无法在选举中获得半数以上投票。举例来说:如果集群有 3 个节点,最多允许 1 个节点出现故障;如果集群中有 5 个节点,最多允许 2 个节点出现故障。 +- **集群的节点数一般要求是奇数**。如果集群节点数为偶数,就很有可能在选主时出现某两个节点均获得半数以上投票的情况,这种情况下就必须重新投票选举。 + +#### 选举会影响性能 + +共识系统通常依靠超时来检测失效的节点。在网络延迟高度变化的环境中,特别是在地理上散布的系统中,经常发生一个节点由于暂时的网络问题,错误地认为领导者已经失效。虽然这种错误不会损害安全属性,但频繁的领导者选举会导致糟糕的性能表现,因系统最后可能花在权力倾扎上的时间要比花在建设性工作的多得多。 + +## 参考资料 + +- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 +- [**Impossibility of Distributed Consensus with One Faulty Process**](https://groups.csail.mit.edu/tds/papers/Lynch/jacm85.pdf) - 论证了在一个异步系统中,即使只有一个进程出现了故障,也没有算法能保证达成共识。 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\210\206\345\214\272.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\210\206\345\214\272.md" new file mode 100644 index 0000000000..4bf463101e --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\210\206\345\214\272.md" @@ -0,0 +1,231 @@ +--- +title: 分布式分区 +date: 2022-06-14 08:49:21 +categories: + - 分布式 + - 分布式协同 + - 分布式协同综合 +tags: + - 分布式 + - 协同 + - 分区 + - 分区再均衡 + - 路由 +permalink: /pages/f03f855b/ +--- + +# 分布式分区 + +## 什么是分区 + +分区通常是这样定义的,即每一条数据(或者每条记录,每行或每个文档)只属于某个特定分区。实际上,每个分区都可以视为一个完整的小型数据库,虽然数据库可能存在一些跨分区的操作。 + +在不同系统中,分区有着不同的称呼,例如它对应于 MongoDB, Elasticsearch 和 SolrCloud 中的 shard, HBase 的 region, Bigtable 中的 tablet, Cassandra 和 Riak 中的 vnode ,以及 Couch base 中的 vBucket。总体而言,分区是最普遍的术语。 + +## 为什么需要分区 + +数据量如果太大,单台机器进行存储和处理就会成为瓶颈,因此需要引入数据分区机制。 + +分区的目地是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。 + +## 数据分区与数据复制 + +分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而同样的内容会保存在不同的节点上以提高系统的容错性。 + +一个节点上可能存储了多个分区。每个分区都有自己的主副本,例如被分配给某节点,而从副本则分配在其他一些节点。一个节点可能既是某些分区的主副本,同时又是其他分区的从副本。 + +## 键-值数据的分区 + +分区的主要目标是将数据和查询负载均匀分布在所有节点上。如果节点平均分担负载,那么理论上 10 个节点应该能够处理 10 倍的数据量和 10 倍于单个节点的读写吞吐量(忽略复制) 。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122230559.png) + +而如果分区不均匀,则会出现某些分区节点比其他分区承担更多的数据量或查询负载,称之为**倾斜**。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10 个节点 9 个空闲,系统的瓶颈在最繁忙的那个节点上。这种负载严重不成比例的分区即成为系统**热点**。 + +避免热点最简单的方法是将记录随机分配给所有节点。这种方法可以比较均匀地分布数据,但是有一个很大的缺点:当视图读取特定的数据时,没有办法知道数据保存在哪个节点上,所以不得不并行查询所有节点。 + +可以改进上述方法。现在我们假设数据是简单的键-值数据模型,这意味着总是可以通过关键字来访问记录。 + +### 基于关键字区间分区 + +一种分区方式是为每个分区分配一段连续的关键字或者关键宇区间范围(以最小值和最大值来指示)。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122230081.png) + +关键字的区间段不一定非要均匀分布,这主要是因为数据本身可能就不均匀。 + +分区边界可以由管理员手动确定,或由数据库自动选择。采用这种分区策略的系统包括 Bigtable、HBase、RethinkDB、2.4 版本前的 MongoDB。 + +每个分区内可以按照关键字排序保存(参阅第 3 章的“ SSTables 和 LSM Trees ”)。这样可以轻松支持区间查询,即将关键字作为一个拼接起来的索引项从而一次查询得到多个相关记录。 + +然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围,所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。为了避免上述问题,需要使用时间戳以外的其他内容作为关键字的第一项。 + +### 基于关键字晗希值分区 + +对于上述数据倾斜和热点问题,许多分布式系统采用了基于关键字哈希函数的方式来分区。 + +一个好的哈希函数可以处理数据倾斜并使其均匀分布。用于数据分区目的的哈希函数不需要再加密方面很强:例如 :Cassandra 和 MongoDB 使用 MD5,Voldemort 使用 Fowler-Noll-Vo。许多编程语言也有内置的简单哈希函数,但是要注意这些内置的哈希函数可能并不适合分区,例如,Java 的 Object.hashCode 和 Object#hash,同一个键在不同的进程中可能返回不同的哈希值。 + +一且找到合适的关键宇哈希函数,就可以为每个分区分配一个哈希范围(而不是直接作用于关键宇范围),关键字根据其哈希值的范围划分到不同的分区中。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303105925.png) + +这种方法可以很好地将关键字均匀地分配到多个分区中。分区边界可以是均匀间隔,也可以是伪随机选择( 在这种情况下,该技术有时被称为一致性哈希) 。 + +然而,通过关键宇哈希进行分区,我们丧失了良好的区间查询特性。即使关键字相邻,但经过哈希之后会分散在不同的分区中,区间查询就失去了原有的有序相邻的特性。在 MongoDB 中,如果启用了基于哈希的分片模式,则区间查询会发送到所有的分区上,而 Riak、Couchbase 和 Voldemort 干脆就不支持关键字上的区间查询。 + +Cassandra 则在两种分区策略之间做了一个折中。Cassandra 中的表可以声明为由多个列组成的复合主键。复合主键只有第一部分可用于哈希分区,而其他列则用作组合索引来对 Cassandra SSTable 中的数据进行排序。因此,它不支持在第一列上进行区间查询,但如果为第一列指定好了固定值,可以对其他列执行高效的区间查询。 + +组合索引为一对多的关系提供了一个优雅的数据模型。 + +### 负载倾斜与热点 + +基于哈希的分区方法可以减轻热点,但无住做到完全避免。一个极端情况是,所有的读/写操作都是针对同一个关键字,则最终所有请求都将被路由到同一个分区。 + +一个简单的技术就是在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到 100 个不同的关键字上,从而分配到不同的分区上。但是,随之而来的问题是,之后的任何读取都需要些额外的工作,必须从所有 100 个关键字中读取数据然后进行合井。因此通常只对少量的热点关键字附加随机数才有意义;而对于写入吞吐量低的绝大多数关键字,这些都意味着不必要的开销。此外,还需要额外的元数据来标记哪些关键字进行了特殊处理。 + +## 分区与二级索引 + +二级索引通常不能唯一标识一条记录,而是用来加速特定值的查询。 + +二级索引是关系数据库的必要特性,在文档数据库中应用也非常普遍。但考虑到其复杂性,许多键-值存储(如 HBase 和 Voldemort)并不支持二级索引;但其他一些如 Riak 则开始增加对二级索引的支持。此外,二级索引技术也是 Solr 和 Elasticsearch 等全文索引服务器存在之根本。 + +二级索引带来的主要挑战是它们不能规整的地映射到分区中。有两种主要的方法来支持对二级索引进行分区:基于文档的分区和基于词条的分区。 + +### 基于文档分区的二级索引 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303111528.png) + +在这种索引方法中,每个分区完全独立,各自维护自己的二级索引,且只负责自己分区内的文档而不关心其他分区中数据。每当需要写数据库时,包括添加,删除或更新文档等,只需要处理包含目标文档 ID 的那一个分区。因此文档分区索引也被称为本地索引,而不是全局索引。 + +这种查询分区数据库的方法有时也称为分散/聚集,显然这种二级索引的查询代价高昂。即使采用了并行查询,也容易导致读延迟显著放大。尽管如此,它还是广泛用于实践: MongoDB 、Riak、Cassandra、Elasticsearch 、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。大多数数据库供应商都建议用户自己来构建合适的分区方案,尽量由单个分区满足二级索引查询,但现实往往难以如愿,尤其是当查询中可能引用多个二级索引时。 + +### 基于词条的二级索引分区 + +另一种方法,可以对所有的数据构建全局索引,而不是每个分区维护自己的本地索引。而且,为避免成为瓶颈,不能将全局索引存储在一个节点上,否则就破坏了设计分区均衡的目标。所以,全局索引也必须进行分区,且可以与数据关键字采用不同的分区策略。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303112708.png) + +词条分区以待查找的关键字本身作为索引。名字词条源于全文索引,term 指的是文档中出现的所有单词的集合。 + +可以直接通过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是可以支持高效的区间查询;而采用哈希的方式则可以更均句的划分分区。 + +这种全局的词条分区相比于文档分区索引的主要优点是,它的读取更为高效,即它不需要采用 scatter/gather 对所有的分区都执行一遍查询,客户端只需要想包含词条的那一个分区发出读请求。然而全局索引的不利之处在于, 写入速度较慢且非常复杂,主要因为单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能完全不同甚至在不同的节点上,由此势必引人显著的写放大。 + +理想情况下,索引应该时刻保持最新,即写入的数据要立即反映在最新的索引上。但是,对于词条分区来讲,这需要一个跨多个相关分区的分布式事务支持,写入速度会受到极大的影响,所以现有的数据库都不支持同步更新二级索引。 + +## 分区再均衡 + +集群节点数变化,数据规模增长等情况,都会导致分区的分布不均。要保持分区的均衡,势必要将数据和请求进行迁移,这样一个迁移负载的过程称为**分区再均衡**。 + +无论对于哪种分区方案, 分区再平衡通常至少要满足: + +- 平衡之后,负载、数据存储、读写请求等应该在集群范围更均匀地分布。 +- 再平衡执行过程中,数据库应该可以继续正常提供读写服务。 +- 避免不必要的负载迁移,以加快动态再平衡,井尽量减少网络和磁盘 I/O 影响。 + +### 动态再平衡的策略 + +#### 为什么不用取模? + +最好将哈希值划分为不同的区间范围,然后将每个区间分配给一个分区。 + +为什么不直接使用 mod?对节点数取模方法的问题是,如果节点数 N 发生了变化,会导致很多关键字需要从现有的节点迁移到另一个节点。 + +#### 固定数量的分区 + +创建远超实际节点数的分区数,然后为每个节点分配多个分区。 + +接下来, 如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。 + +选中的整个分区会在节点之间迁移,但分区的总数量仍维持不变,也不会改变关键字到分区的映射关系。这里唯一要调整的是分区与节点的对应关系。考虑到节点间通过网络传输数据总是需要些时间,这样调整可以逐步完成,在此期间,旧分区仍然可以接收读写请求。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122231579.png) + +原则上,也可以将集群中的不同的硬件配置因素考虑进来,即性能更强大的节点将分配更多的分区,从而分担更多的负载。 + +目前,Riak、Elasticsearch、Couchbase 和 Voldemort 都支持这种动态平衡方法。 + +使用该策略时,分区的数量往往在数据库创建时就确定好,之后不会改变。原则上也可以拆分和合并分区(稍后介绍),但固定数量的分区使得相关操作非常简单,因此许多采用固定分区策略的数据库决定不支持分区拆分功能。所以,在初始化时,已经充分考虑将来扩容增长的需求(未来可能拥有的最大节点数),设置一个足够大的分区数。而每个分区也有些额外的管理开销,选择过高的数字可能会有副作用。 + +#### 动态分区 + +对于采用关键宇区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。 + +因此, 一些数据库如 HBase 和 RethinkDB 等采用了动态创建分区。当分区的数据增长超过一个可配的参数阔值(HBase 上默认值是 10GB),它就拆分为两个分区,每个承担一半的数据量。相反,如果大量数据被删除,并且分区缩小到某个阈值以下,则将其与相邻分区进行合井。该过程类似于 B 树的分裂操作。 + +每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。对于 HBase,分区文件的传输需要借助 HDFS。 + +动态分区的一个优点是分区数量可以自动适配数据总量。如果只有少量的数据,少量的分区就足够了,这样系统开销很小;如果有大量的数据,每个分区的大小则被限制在一个可配的最大值。 + +但是,需要注意的是,对于一个空的数据库, 因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理, 而其他节点则处于空闲状态。为了缓解这个问题,HBase 和 MongoDB 允许在一个空的数据库上配置一组初始分区(这被称为预分裂)。对于关键字区间分区,预分裂要求已经知道一些关键字的分布情况。 + +动态分区不仅适用于关键字区间分区,也适用于基于哈希的分区策略。MongoDB 从版本 2.4 开始,同时支持二者,井且都可以动态分裂分区。 + +#### 按节点比例分区 + +采用动态分区策略,拆分和合并操作使每个分区的大小维持在设定的最小值和最大值之间,因此分区的数量与数据集的大小成正比关系。另一方面,对于固定数量的分区方式,其每个分区的大小也与数据集的大小成正比。两种情况,分区的数量都与节点数无关。 + +Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每个节点具有固定数量的分区。此时, 当节点数不变时,每个分区的大小与数据集大小保持正比的增长关系; 当节点数增加时,分区则会调整变得更小。较大的数据量通常需要大量的节点来存储,因此这种方法也使每个分区大小保持稳定。 + +当一个新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择可能会带来不太公平的分区分裂,但是当平均分区数量较大时(Cassandra 默认情况下,每个节点有 256 个分区),新节点最终会从现有节点中拿走相当数量的负载。Cassandra 在 3.0 时推出了改进算洁,可以避免上述不公平的分裂。 + +随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界)。这种方法也最符合本章开头所定义一致性哈希。一些新设计的哈希函数也可以以较低的元数据开销达到类似的效果。 + +### 自动与手动再平衡操作 + +动态平衡另一个重要问题我们还没有考虑:它是自动执行还是手动方式执行? + +全自动式再平衡会更加方便,它在正常维护之外所增加的操作很少。但是,也有可能出现结果难以预测的情况。再平衡总体讲是个比较昂贵的操作,它需要重新路由请求井将大量数据从一个节点迁移到另一个节点。万一执行过程中间出现异常,会使网络或节点的负载过重,井影响其他请求的性能。 + +将自动平衡与自动故障检测相结合也可能存在一些风险。例如,假设某个节点负载过重,对请求的响应暂时受到影响,而其他节点可能会得到结论:该节点已经失效;接下来激活自动平衡来转移其负载。客观上这会加重该节点、其他节点以及网络的负荷,可能会使总体情况变得更槽,甚至导致级联式的失效扩散。 + +## 请求路由 + +当数据集分布到多个节点上,需要解决一个问题:当客户端发起请求时,如何知道应该连接哪个节点?如果发生了分区再平衡,分区与节点的对应关系随之还会变化。为了回答该问题,我们需要一段处理逻辑来感知这些变化,并负责处理客户端的连接。 + +这其实属于一类典型的服务发现问题,服务发现并不限于数据库,任何通过网络访问的系统都有这样的需求,尤其是当服务目标支持高可用时(在多台机器上有冗余配置)。 + +服务发现有以下处理策略: + +1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,则直接处理该请求:否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。 +2. 将所有客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层本身不处理任何请求,它仅充一个分区感知的负载均衡器。 +3. 客户端感知分区和节点分配关系。此时,客户端可以直接连接到目标节点,而不需要任何中介。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220304120137.png) + +许多分布式数据系统依靠独立的协调服务(如 ZooKeeper )跟踪集群范围内的元数据。每个节点都向 ZooKeeper 中注册自己, ZooKeeper 维护了分区到节点的最终映射关系。其他参与者(如路由层或分区感知的客户端)可以向 ZooKeeper 订阅此信息。一旦分区发生了改变,或者添加、删除节点, ZooKeeper 就会主动通知路由层,这样使路由信息保持最新状态。 + +例如,HBase、SolrCloud 和 Kafka 也使用 ZooKeeper 来跟踪分区分配情况。MongoDB 有类似的设计,但它依赖于自己的配置服务器和 mongos 守护进程来充当路由层。 + +Cassandra 和 Riak 则采用了不同的方法,它们在节点之间使用 gossip 协议来同步群集状态的变化。请求可以发送到任何节点,由该节点负责将其转发到目标分区节点。这种方式增加了数据库节点的复杂性,但是避免了对 ZooKeeper 之类的外部协调服务的依赖。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220304163629.png) + +## 并行查询执行 + +到目前为止,我们只关注了读取或写入单个关键字这样简单的查询(对于文档分区的二级索引,里面要求分散/聚集查询)。这基本上也是大多数 NoSQL 分布式数据存储所支持的访问类型。 + +然而对于大规模并行处理(massively parallel processing, MPP)这一类主要用于数据分析的关系数据库,在查询类型方面要复杂得多。典型的数据仓库查询包含多个联合、过滤、分组和聚合操作。MPP 查询优化器会将复杂的查询分解成许多执行阶段和分区,以便在集群的不同节点上井行执行。尤其是涉及全表扫描这样的查询操作,可以通过并行执行获益颇多。 + +## 小结 + +数据量如果太大,单台机器进行存储和处理就会成为瓶颈,因此需要引入数据分区机制。分区的目地是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。 + +两种主要的分区方法: + +- **基于关键字区间的分区**。先对关键字进行排序,每个分区只负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以支持高效的区间查询,但是如果应用程序经常访问与排序一致的某段关键字,就会存在热点的风险。采用这种方怯,当分区太大时,通常将其分裂为两个子区间,从而动态地再平衡分区。 +- **哈希分区**。将哈希函数作用于每个关键字,每个分区负责一定范围的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载。采用哈希分区时,通常事先创建好足够多(但固定数量)的分区,让每个节点承担多个分区,当添加或删除节点时将某些分区从一个节点迁移到另一个节点,也可以支持动态分区。 + +混合上述两种基本方住也是可行的,例如使用复合键:键的一部分来标识分区,而另一部分来记录排序后的顺序。 + +二级索引也需要进行分区,有两种方法: + +- 基于文档来分区二级索引(本地索引)。二级索引存储在与关键字相同的分区中,这意味着写入时我们只需要更新一个分区,但缺点是读取二级索引时需要在所有分区上执行 scatter/gather。 +- 基于词条来分区二级索引(全局索引)。它是基于索引的值而进行的独立分区。二级索引中的条目可能包含来自关键字的多个分区里的记录。在写入时,不得不更新二级索引的多个分区;但读取时,则可以从单个分区直接快速提取数据。 + +最后,讨论了如何将查询请求路由到正确的分区,包括简单的分区感知负载均衡器,以及复杂的并行查询执行引擎。 + +## 参考资料 + +- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..9bcfa4e22f --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\351\235\242\350\257\225.md" @@ -0,0 +1,1269 @@ +--- +title: 分布式协同面试 +date: 2024-12-16 23:57:04 +categories: + - 分布式 + - 分布式协同 + - 分布式协同综合 +tags: + - 分布式 + - 协同 + - 面试 +permalink: /pages/372a9cc0/ +--- + +# 分布式协同面试 + +## 复制 + +### 【基础】什么是复制?复制有什么作用? + +:::details 要点 + +**复制主要指通过网络在多台机器上保存相同数据的副本**。 + +复制数据,可能出于各种各样的原因: + +- **提高可用性** - 当部分组件出现位障,系统依然可以继续工作,系统依然可以继续工作。 +- **降低访问延迟** - 使数据在地理位置上更接近用户。 +- **提高读吞吐量** - 扩展至多台机器以同时提供数据访问服务。 + +::: + +### 【中级】复制有哪些模式? + +:::details 要点 + +复制的模式有以下几种: + +- **主从复制** - **所有的写入操作都发送到主节点**,由主节点负责将数据更改事件发送到从节点。每个从节点都可以接收读请求,但内容可能是过期值。支持主从复制的系统: + - 数据库:Mysql、PostgreSQL、MongoDB 等 + - 消息队列:Kafka、RabbitMQ 等 +- **多主复制** - **系统存在多个主节点,每个都可以接收写请求**,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其他主节点和自己的从节点。 +- **无主复制** - **系统中不存在主节点,每一个节点都能接受客户端的写请求**。此外,**读取时从多个节点上并行读取,以此检测和纠正某些过期数据**。支持无主复制的系统: + - 数据库:Cassandra + +此外,复制还需要考虑以下问题: + +- **同步还是异步** +- **如何处理失败的副本** +- **如何保证数据一致** + +::: + +### 【中级】主从复制是如何工作的? + +:::details 要点 + +最常见的解决方案就是主从复制,其原理如下: + +主从复制模式中只有一个主副本(或称为主节点) ,其余称为从副本(或称为从节点)。 + +1. 所有的写请求只能发送给主副本,主副本首先将新数据写入本地存储。 +2. 然后,主副本将数据更改作为复制的日志或更新流发送给所有从副本。每个从副本获得更新数据之后将其应用到本地,且严格保持与主副本相同的写入顺序。 +3. 读请求既可以在主副本上,也可以在从副本上执行。 + +再次强调,**只有主副本才可以接受写请求**:从客户端的角度来看,从副本都是只读的。如果由于某种原因,例如与主节点之间的网络中断而导致主节点无法连接,主从复制方案就会影响所有的写入操作。 + +![主从复制系统](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202101.png) + +::: + +### 【中级】同步复制、半同步复制、异步复制有什么差异? + +:::details 要点 + +![主从复制——同步和异步](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202158.png) + +一般,复制速度会非常快;但是,系统不能保证复制多久能完成。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如:从节点刚从故障中恢复;或系统已经接近最大设计上限;或节点之间的网络出现问题。 + +**全同步复制**的优缺点: + +- **优点**:只有所有从节点都完成复制,才视为成功,因此是**强一致的**。 +- **缺点**:即使只有一个从节点未完成复制,写入都不能视为成功。所有从节点完成复制过程之前,主节点会**阻塞**后续所有的写操作。 + +因此,**把所有从节点都配置为同步复制有些不切实际**。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前。 + +**全异步复制**的优缺点: + +- **优点**:不管从节点上数据多么滞后,主节点总是可以继续响应写请求,**系统的性能更好**。 +- **缺点**:如果主节点发生故障且不可恢复,则**所有尚未复制到从节点的写请求都会丢失**。 + +还有一种折中的方案——**半同步复制**:**只要有一个从节点或半数以上的从节点同步成功,就视为同步,直接返回结果;剩下的节点都通过异步方式同步**。万一同步的从节点变得不可用或性能下降,则将另一个异步的从节点提升为同步模式。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。 + +::: + +### 【中级】新的从节点如何复制主节点数据? + +:::details 要点 + +两种不可行的方案: + +- 由于主节点会源源不断接受新的写入数据,数据始终处于变化中,因此**一次性从主节点复制数据到从节点是无法保证数据一致的**。 +- 另一种思路是:考虑**锁定数据库**(使其不可写)来使磁盘上的文件保持一致,但这会**违反高可用的设计目标**。 + +可行的方案: + +1. 生成主节点某时刻的快照,避免长时间锁定数据库。 +2. 将快照复制到从节点。 +3. 从节点复制主节点快照过程中,所有的数据变更写入一个日志中(这个数据变更日志在不同数据库中有着不同的称呼,Mysql 称其为 binlog;Redis 称其为 AOF)。 +4. 从节点复制完主节点的快照后,请求数据变更日志中的数据,并基于此补全数据,这个过程称为**追赶**,直至主从数据一致。井重复步骤 1 ~步骤 4 。 + +::: + +### 【高级】如何通过主从复制技术来实现系统高可用呢? + +:::details 要点 + +#### 从节点失效:追赶式恢复 + +从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点从故障中恢复,可以和主节点对比数据变更日志的偏移量,从而确认数据是否滞后。如果数据存在滞后,则向主节点请求数据变更日志,并补全数据。这个过程称为**追赶**。 + +#### 主节点失效:节点切换 + +主节点失效后,需要选举出新主节点。然后,客户端需要更新路由,将所有写请求发送给新的主节点;其他从节点要接受来自新的主节点上的变更数据。这个过程称之为**切换**。 + +主节点切换可以手动或自动进行。自动切换的步骤通常如下: + +1. **确认主节点失效**。有很多种出错可能性,很难准确检测出问题的原因。所以,大多数系统都基于超时机制来确认主节点是否失效:节点间频繁地互相发生发送心跳存活悄息,如果发现某一个节点在一段比较长时间内没有响应,即认为该节点发生失效。 +2. **选举新的主节点**。基于多数派共识选主。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。 +3. **重新配置系统使新主节点生效**。客户端现在需要将写请求发送给新的主节点。原主节点若恢复,需降级处理,避免脑裂。 + +::: + +### 【高级】复制日志如何实现? + +:::details 要点 + +复制日志的视线方式: + +- **基于语句的复制** - 将数据写操作写入日志。主要缺点是**必须完全按照相同顺序执行**,否则可能会产生不同的结果。 +- **基于预写日志(WAL)传输** - 通常每个写操作都是以追加写的方式写入到日志中。主要缺点是**日志描述的数据结果非常底层**,如果数据库不同版本的存储格式存在差异,就可能无法兼容。 + - 对于日志结构存储引擎,日志是主要的存储方式。日志段在后台压缩井支持垃圾回收。 + - 对于采用覆写磁盘的 BTree 结构,每次修改会预先写入日志,如系统发生崩溃,通过索引更新的方式迅速恢复到此前一致状态。 +- **基于行的逻辑日志复制** - 如果复制和存储引擎采用不同的日志格式,这样复制与存储的逻辑就可以剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。 +- **基于触发器的复制** - 这种方式**很灵活**,可以定制化控制复制逻辑。主要缺点是复制**开销更高,也更容易出错**。 + +::: + +### 【高级】多主复制是如何工作的? + +:::details 要点 + +对主从复制模型进行自然的扩展,则可以配置多个主节点,每个主节点都可以接受写操作,后面复制的流程类似:处理写的每个主节点都必须将该数据更改转发到所有其他节点。这就是多主节点( 也称为主-主,或主动/主动)复制。此时,每个主节点还同时扮演其他主节点的从节点。 + +在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。 + +但是,以下场景这种配置则是合理的: + +- 多数据中心 +- 离线客户端操作 +- 协作编辑 + +#### 多数据中心 + +有了多主节点复制模型,则可以在每个数据中心都配置主节点。在每个数据中心内,采用常规的主从复制方案;而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122221705.png) + +部署单主节点的主从复制方案与多主复制方案之间的差异 + +- **性能**:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,井基本偏离了采用多数据中心的初衷(即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。因此,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。 +- **容忍数据中心失效**:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。 +- **容忍网络问题**:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络可靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。 + +::: + +### 【高级】无主复制是如何工作的? + +:::details 要点 + +无主复制模式,**系统中不存在主节点,每一个节点都能接受客户端的写请求**。此外,**读取时从多个节点上并行读取,以此检测和纠正某些过期数据**。 + +#### 读修复和反熵 + +复制模型应确保所有数据最终复制到所有的副本。当一个失效的节点重新上线之后,如何赶上中间错过的那些写请求呢? + +有以下两种机制: + +- **读修复** - 客户端并行读取多个副本,根据版本识别过期返回值并更新最新值到相应副本。这种方法主要适合那些被频繁读取的场景。 +- **反熵** - 利用后台进程不断查找副本间的数据差异,将任何缺少的数据从一个副本复制到另一个副本。与基于主节点复制的复制日志不同,反熵过程并不保证以特定的顺序复制写入,并且会引入明显的同步滞后。 + +#### QuorumNWR 算法 + +无主复制模式中,究竟多少个副本完成才可以认为写成功? + +如果有 n 个副本,写人需要 w 个节点确认,读取必须至少查询 r 个节点, 则只要 `w+r>n` ,读取的节点中一定会包含最新值。 + +#### 并发写冲突 + +无主模式中,并发向多副本写操作,以及读时修复或数据回传都会导致并发写冲突。如何解决冲突呢?有以下几种机制: + +- 最后写入者获胜(丢弃并发写入) - 每个副本总是保存最新值,允许覆盖井丢弃旧值。 +- Happens Before - 利用全序的逻辑时钟来确定事件发生的前后顺序。 +- 向量时钟、版本向量时钟 - 本质上是将全序的逻辑时钟改造为维护所有副本版本号的合集,基于此合集可以进行偏序比较。 + +::: + +## 分区 + +### 【基础】什么是分区?为什么要分区? + +:::details 要点 + +分区通常是这样定义的,即每一条数据(或者每条记录,每行或每个文档)只属于某个特定分区。实际上,每个分区都可以视为一个完整的小型数据库,虽然数据库可能存在一些跨分区的操作。 + +在不同系统中,分区有着不同的称呼,例如它对应于 MongoDB, Elasticsearch 和 SolrCloud 中的 shard, HBase 的 region, Bigtable 中的 tablet, Cassandra 和 Riak 中的 vnode ,以及 Couch base 中的 vBucket。总体而言,分区是最普遍的术语。 + +数据量如果太大,单台机器进行存储和处理就会成为瓶颈,因此需要引入数据分区机制。 + +分区的目地是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。 + +::: + +### 【中级】分区有哪些模式? + +:::details 要点 + +分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而同样的内容会保存在不同的节点上以提高系统的容错性。 + +一个节点上可能存储了多个分区。每个分区都有自己的主副本,例如被分配给某节点,而从副本则分配在其他一些节点。一个节点可能既是某些分区的主副本,同时又是其他分区的从副本。 + +分区主要有两种模式: + +- **基于关键字区间的分区** - 先对关键字进行排序,每个分区只负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以支持高效的区间查询,但是如果应用程序经常访问与排序一致的某段关键字,就会存在热点的风险。采用这种方怯,当分区太大时,通常将其分裂为两个子区间,从而动态地再平衡分区。典型代表:HBase +- **哈希分区** - 将哈希函数作用于每个关键字,每个分区负责一定范围的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载。采用哈希分区时,通常事先创建好足够多(但固定数量)的分区, 让每个节点承担多个分区,当添加或删除节点时将某些分区从一个节点迁移到另一个节点,也可以支持动态分区。典型代表:Elasticsearch、Redis。 + +::: + +### 【高级】二级索引如何分区? + +:::details 要点 + +二级索引是关系数据库的必备特性,在文档数据库中应用也非常普遍。但考虑到其复杂性,许多键值存储(如 HBase 和 Voldemort)并不支持二级索引。此外, 二级索引技术也是 Solr 和 Elasticsearch 等搜索引擎数据库存在之根本。 + +分区不仅仅是针对数据,二级索引也需要分区。通常有两种方法: + +**基于文档来分区二级索引(本地索引)** - 二级索引存储在与关键字相同的分区中,这意味着写入时我们只需要更新一个分区,但缺点是读取二级索引时需要在所有分区上并行执行。它广泛用于实践: MongoDB 、Riak、Cassandra、Elasticsearch 、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303111528.png) + +**基于词条来分区二级索引(全局索引)** - 它是基于索引的值而进行的独立分区。二级索引中的条目可能包含来自关键字的多个分区里的记录。在写入时,不得不更新二级索引的多个分区;但读取时,则可以从单个分区直接快速提取数据。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303112708.png) + +::: + +### 【基础】什么是分区再均衡? + +:::details 要点 + +集群节点数变化,数据规模增长等情况,都会导致分区的分布不均。要保持分区的均衡,势必要将数据和请求进行迁移,这样一个迁移负载的过程称为**分区再均衡**。 + +::: + +### 【高级】分区再均衡有哪些策略? + +:::details 要点 + +#### 固定数量的分区 + +创建远超实际节点数的分区数,然后为每个节点分配多个分区。接下来, 如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。 + +选中的整个分区会在节点之间迁移,但分区的总数量仍维持不变,也不会改变关键字到分区的映射关系。这里唯一要调整的是分区与节点的对应关系。考虑到节点间通过网络传输数据总是需要些时间,这样调整可以逐步完成,在此期间,旧分区仍然可以接收读写请求。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122231579.png) + +原则上,也可以将集群中的不同的硬件配置因素考虑进来,即性能更强大的节点将分配更多的分区,从而分担更多的负载。 + +目前,Riak、Elasticsearch、Couchbase 和 Voldemort 都支持这种动态平衡方法。 + +使用该策略时,分区的数量往往在数据库创建时就确定好,之后不会改变。原则上也可以拆分和合并分区(稍后介绍),但固定数量的分区使得相关操作非常简单,因此许多采用固定分区策略的数据库决定不支持分区拆分功能。所以,在初始化时,已经充分考虑将来扩容增长的需求(未来可能拥有的最大节点数),设置一个足够大的分区数。而每个分区也有些额外的管理开销,选择过高的数字可能会有副作用。 + +#### 动态分区 + +对于采用关键宇区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。 + +因此, 一些数据库如 HBase 和 RethinkDB 等采用了动态创建分区。当分区的数据增长超过一个可配的参数阔值(HBase 上默认值是 10GB),它就拆分为两个分区,每个承担一半的数据量。相反,如果大量数据被删除,并且分区缩小到某个阈值以下,则将其与相邻分区进行合井。该过程类似于 B 树的分裂操作。 + +每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。对于 HBase,分区文件的传输需要借助 HDFS。 + +动态分区的一个优点是分区数量可以自动适配数据总量。如果只有少量的数据,少量的分区就足够了,这样系统开销很小;如果有大量的数据,每个分区的大小则被限制在一个可配的最大值。 + +但是,需要注意的是,对于一个空的数据库, 因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理, 而其他节点则处于空闲状态。为了缓解这个问题,HBase 和 MongoDB 允许在一个空的数据库上配置一组初始分区(这被称为预分裂)。对于关键字区间分区,预分裂要求已经知道一些关键字的分布情况。 + +动态分区不仅适用于关键字区间分区,也适用于基于哈希的分区策略。MongoDB 从版本 2.4 开始,同时支持二者,井且都可以动态分裂分区。 + +#### 按节点比例分区 + +采用动态分区策略,拆分和合并操作使每个分区的大小维持在设定的最小值和最大值之间,因此分区的数量与数据集的大小成正比关系。另一方面,对于固定数量的分区方式,其每个分区的大小也与数据集的大小成正比。两种情况,分区的数量都与节点数无关。 + +Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每个节点具有固定数量的分区。此时, 当节点数不变时,每个分区的大小与数据集大小保持正比的增长关系; 当节点数增加时,分区则会调整变得更小。较大的数据量通常需要大量的节点来存储,因此这种方法也使每个分区大小保持稳定。 + +当一个新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择可能会带来不太公平的分区分裂,但是当平均分区数量较大时(Cassandra 默认情况下,每个节点有 256 个分区),新节点最终会从现有节点中拿走相当数量的负载。Cassandra 在 3.0 时推出了改进算洁,可以避免上述不公平的分裂。 + +随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界)。这种方法也最符合本章开头所定义一致性哈希。一些新设计的哈希函数也可以以较低的元数据开销达到类似的效果。 + +::: + +### 【高级】如何确定读写请求发往哪个节点? + +:::details 要点 + +当数据集分布到多个节点上,需要解决一个问题:当客户端发起请求时,如何知道应该连接哪个节点?如果发生了分区再平衡,分区与节点的对应关系随之还会变化。 + +这其实属于一类典型的服务发现问题,任何通过网络访问的系统都有这样的需求,尤其是当服务目标支持高可用时(在多台机器上有冗余配置)。 + +服务发现有以下处理策略: + +1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,则直接处理该请求:否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。 +2. 将所有客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层本身不处理任何请求,它仅充一个分区感知的负载均衡器。 +3. 客户端感知分区和节点分配关系。此时,客户端可以直接连接到目标节点,而不需要任何中介。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220304120137.png) + +许多分布式数据系统依靠独立的协调服务(如 ZooKeeper )跟踪集群范围内的元数据。每个节点都向 ZooKeeper 中注册自己, ZooKeeper 维护了分区到节点的最终映射关系。其他参与者(如路由层或分区感知的客户端)可以向 ZooKeeper 订阅此信息。一旦分区发生了改变,或者添加、删除节点, ZooKeeper 就会主动通知路由层,这样使路由信息保持最新状态。 + +例如,HBase、SolrCloud 和 Kafka 也使用 ZooKeeper 来跟踪分区分配情况。MongoDB 有类似的设计,但它依赖于自己的配置服务器和 mongos 守护进程来充当路由层。 + +Cassandra 和 Redis 则采用了不同的方法,它们在节点之间使用 gossip 协议来同步群集状态的变化。请求可以发送到任何节点,由该节点负责将其转发到目标分区节点。这种方式增加了数据库节点的复杂性,但是避免了对 ZooKeeper 之类的外部协调服务的依赖。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220304163629.png) + +::: + +## 共识 + +## 分布式事务 + +> 扩展: +> +> - [理解分布式事务](https://juejin.cn/post/6844903734753886216?searchId=2024121710293693FB283CD941B5B19BE2) +> - [分布式事务](https://dunwu.github.io/waterdrop/pages/d46468f7/) + +### 【基础】什么是事务?什么是分布式事务? + +:::details 要点 + +**事务将多个读、写操作捆绑在一起成为一个逻辑操作单元**。**事务中的所有读写是一个执行的整体,整个事务要么成功(提交)、要么失败(中止或回滚)**。 + +在单一数据节点中,事务仅限于对单一数据库资源的访问控制,称之为**本地事务**。几乎所有的成熟的关系型数据库都提供了对本地事务的原生支持。 + +**分布式事务指的是事务操作跨越多个节点,并且要求满足事务的 ACID 特性。** + +::: + +### 【基础】什么是 ACID?什么是 BASE?二者有何区别? + +:::details 要点 + +#### ACID + +ACID 是数据库事务正确执行的四个基本要素的单词缩写: + +- **原子性(Atomicity)** + - 原子是指不可分解为更小粒度的东西。事务的原子性意味着:**事务中的所有操作要么全部成功,要么全部失败**。 + - 回滚可以用日志来实现,日志记录着事务所执行的修改操作,在回滚时反向执行这些修改操作即可。 + - ACID 中的原子性并不关乎多个操作的并发性,它并没有描述多个线程试图访问相同的数据会发生什么情况,后者其实是由 ACID 的隔离性所定义。 +- **一致性(Consistency)** + - 数据库在事务执行前后都保持一致性状态。 + - 在一致性状态下,所有事务对一个数据的读取结果都是相同的。 + - 一致性本质上要求应用层来维护状态一致(或者恒等),应用程序有责任正确地定义事务来保持一致性。这不是数据库可以保证的事情。 +- **隔离性(Isolation)** + - **同时运行的事务互不干扰**。换句话说,一个事务所做的修改在最终提交以前,对其它事务是不可见的。 +- **持久性(Durability)** + - 一旦事务提交,则其所做的修改将会永远保存到数据库中。即使系统发生崩溃,事务执行的结果也不能丢失。 + - 可以通过数据库备份和恢复来实现,在系统发生奔溃时,使用备份的数据库进行数据恢复。 + +#### BASE + +BASE 是 **`基本可用(Basically Available)`**、**`软状态(Soft State)`** 和 **`最终一致性(Eventually Consistent)`** 三个短语的缩写。 + +BASE 理论的核心思想是:即使无法做到强一致性,但每个应用都可以根据自身业务特点,采用适当的方式来使系统达到最终一致性。 + +- **基本可用(Basically Available)**分布式系统在出现故障的时候,**保证核心可用,允许损失部分可用性**。例如,电商在做促销时,为了保证购物系统的稳定性,部分消费者可能会被引导到一个降级的页面。 +- **软状态(Soft State)**指允许系统中的数据存在中间状态,并认为该中间状态不会影响系统整体可用性,即**允许系统不同节点的数据副本之间进行同步的过程存在延时**。 +- **最终一致性(Eventually Consistent)**强调的是**系统中所有的数据副本,在经过一段时间的同步后,最终能达到一致的状态**。 + +#### BASE vs. ACID + +ACID 要求强一致性,通常运用在传统的数据库系统上。而 BASE 要求最终一致性,通过**牺牲强一致性来达到可用性**,通常运用在大型分布式系统中。BASE 唯一可以确定的是“它不是 ACID”,此外它几乎没有承诺任何东西。 + +::: + +### 【基础】什么是一致性?什么是最终一致性? + +:::details 要点 + +一致性(Consistency)指的是**多个数据副本是否能保持一致**的特性。 + +数据一致性又可以分为以下几点: + +- **强一致性** - 数据更新操作结果和操作响应总是一致的,即操作响应通知更新失败,那么数据一定没有被更新,而不是处于不确定状态。 +- **最终一致性** - 即物理存储的数据可能是不一致的,终端用户访问到的数据可能也是不一致的,但系统经过一段时间的自我修复和修正,数据最终会达到一致。 + +在分布式领域,要实现强一致性,代价非常高昂。因此,有人基于 CAP 理论以及 BASE 理论,有人就提出了**柔性事务**的概念。柔性事务是指:在不影响系统整体可用性的情况下 (Basically Available 基本可用),允许系统存在数据不一致的中间状态 (Soft State 软状态),在经过数据同步的延时之后,达到**最终一致性**。**并不是完全放弃了 ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐**。 + +::: + +### 【中级】有哪些分布式事务解决方案?各有什么利弊? + +:::details 要点 + +分布式事务的常见方案如下: + +- **两阶段提交(2PC)** - 将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作。 +- **三阶段提交(3PC)** - 与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。将二阶段的准备阶段拆分为 2 个阶段,插入了一个 preCommit 阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。 +- **补偿事务(TCC)** + - **Try** - 操作作为一阶段,负责资源的检查和预留。 + - **Confirm** - 操作作为二阶段提交操作,执行真正的业务。 + - **Cancel** - 是预留资源的取消。 +- **本地消息表** - 在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。 +- **消息事务** - 基于 MQ 的分布式事务方案其实是对本地消息表的封装。 +- **SAGA** - Saga 事务核心思想是将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。 + +分布式事务方案对比: + +- 2PC/3PC 依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。 +- TCC 适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。 +- 本地消息表/消息事务都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。 +- Saga 事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。Saga 相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga 事务较适用于补偿动作容易处理的场景。 + +| | 2PC | 3PC | TCC | 本地消息表 | MQ 事务 | SAGA | +| ---------- | --- | --- | --- | ---------- | ------- | ---- | +| 数据一致性 | 强 | 强 | 若 | 弱 | 弱 | 弱 | +| 容错性 | 低 | 低 | 高 | 高 | 高 | 高 | +| 复杂性 | 中 | 高 | 高 | 低 | 低 | 高 | +| 性能 | 低 | 低 | 中 | 中 | 高 | 中 | +| 维护成本 | 低 | 中 | 高 | 中 | 中 | 高 | + +::: + +### 【中级】2PC 是如何工作的? + +:::details 要点 + +二阶段提交协议(Two-phase Commit,即 2PC)**将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段**。事务的发起者称协调者,事务的执行者称参与者。二阶段提交的思路可以概括为:**参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈,决定提交或回滚**。 + +**阶段 1:准备阶段** + +1. 协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。 +2. 各参与者执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。 +3. 如参与者执行成功,给协调者反馈 yes,即可以提交;如执行失败,给协调者反馈 no,即不可提交。 + +**阶段 2:提交阶段** + +如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚 (rollback) 消息;否则,发送提交 (commit) 消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源) 接下来分两种情况分别讨论提交阶段的过程。 + +**情况 1,当所有参与者均反馈 yes,提交事务**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140719760.png) + +> 1. 协调者向所有参与者发出正式提交事务的请求(即 commit 请求)。 +> 2. 参与者执行 commit 请求,并释放整个事务期间占用的资源。 +> 3. 各参与者向协调者反馈 ack(应答)完成的消息。 +> 4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。 + +**情况 2,当任何阶段 1 一个参与者反馈 no,中断事务**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140723628.png) + +> 1. 协调者向所有参与者发出回滚请求(即 rollback 请求)。 +> 2. 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。 +> 3. 各参与者向协调者反馈 ack 完成的消息。 +> 4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。 + +方案总结: + +2PC 方案实现起来简单,实际项目中使用比较少,主要因为以下问题: + +- **性能问题** - 所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。 +- **可靠性问题** - 如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。 +- **数据一致性问题** - 在阶段 2 中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。 + +::: + +### 【中级】3PC 是如何工作的? + +:::details 要点 + +三阶段提交协议(Three-phase Commit,3PC),是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。 + +**阶段 1:canCommit** + +协调者向参与者发送 commit 请求,参与者如果可以提交就返回 yes 响应(参与者不执行事务操作),否则返回 no 响应: + +1. 协调者向所有参与者发出包含事务内容的 canCommit 请求,询问是否可以提交事务,并等待所有参与者答复。 +2. 参与者收到 canCommit 请求后,如果认为可以执行事务操作,则反馈 yes 并进入预备状态,否则反馈 no。 + +**阶段 2:preCommit** + +协调者根据阶段 1 canCommit 参与者的反应情况来决定是否可以基于事务的 preCommit 操作。根据响应情况,有以下两种可能。 + +**情况 1:阶段 1 所有参与者均反馈 yes,参与者预执行事务**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140724776.png) + +> 1. 协调者向所有参与者发出 preCommit 请求,进入准备阶段。 +> 2. 参与者收到 preCommit 请求后,执行事务操作,将 undo 和 redo 信息记入事务日志中(但不提交事务)。 +> 3. 各参与者向协调者反馈 ack 响应或 no 响应,并等待最终指令。 + +**情况 2:阶段 1 任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140725035.png) + +> 1. 协调者向所有参与者发出 abort 请求。 +> 2. 无论收到协调者发出的 abort 请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。 + +**阶段 3:doCommit** + +该阶段进行真正的事务提交,也可以分为以下两种情况: + +**情况 1:阶段 2 所有参与者均反馈 ack 响应,执行真正的事务提交**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140725296.png) + +> 1. 如果协调者处于工作状态,则向所有参与者发出 doCommit 请求。 +> 2. 参与者收到 doCommit 请求后,会正式执行事务提交,并释放整个事务期间占用的资源。 +> 3. 各参与者向协调者反馈 ack 完成的消息。 +> 4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务提交。 + +**情况 2:任何一个参与者反馈 no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140726693.png) + +> 1. 如果协调者处于工作状态,向所有参与者发出 abort 请求。 +> 2. 参与者使用阶段 1 中的 undo 信息执行回滚操作,并释放整个事务期间占用的资源。 +> 3. 各参与者向协调者反馈 ack 完成的消息。 +> 4. 协调者收到所有参与者反馈的 ack 消息后,即完成事务中断。 + +注意:进入阶段 3 后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的 doCommit 请求或 abort 请求。此时,参与者都会在等待超时之后,继续执行事务提交。 + +**方案总结**: + +- 优点:**相比二阶段提交,三阶段降低了阻塞范围**,在**等待超时后协调者或参与者会中断事务**。避免了协调者单点问题,阶段 3 中协调者出现问题时,参与者会继续提交事务。 + +- 缺点:**数据不一致问题依然存在**,当在参与者收到 preCommit 请求后等待 doCommit 指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。 + +::: + +### 【中级】TCC 是如何工作的? + +:::details 要点 + +TCC 是服务化的二阶段编程模型,其 Try、Confirm、Cancel 3 个方法均由业务编码实现; + +- **Try** - 操作作为一阶段,负责资源的检查和预留。 +- **Confirm** - 操作作为二阶段提交操作,执行真正的业务。 +- **Cancel** - 是预留资源的取消。 + +TCC 事务的 Try、Confirm、Cancel 可以理解为 SQL 事务中的 Lock、Commit、Rollback。 + +**Try 阶段** + +从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。TCC 机制中的 Try 仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成: + +- 完成所有业务检查(一致性) +- 预留必须业务资源(准隔离性) +- Try 尝试执行业务 TCC 事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try 阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。 + +假设商品库存为 100,购买数量为 2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。 + +**Confirm / Cancel 阶段** + +根据 Try 阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。 Confirm 和 Cancel 操作满足幂等性,如果 Confirm 或 Cancel 操作执行失败,将会不断重试直到执行完成。 + +**Confirm:当 Try 阶段服务全部正常执行, 执行确认业务逻辑操作** + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140726821.png) + +这里使用的资源一定是 Try 阶段预留的业务资源。在 TCC 事务机制中认为,如果在 Try 阶段能正常的预留资源,那 Confirm 一定能完整正确的提交。Confirm 阶段也可以看成是对 Try 阶段的一个补充,Try+Confirm 一起组成了一个完整的业务逻辑。 + +**Cancel:当 Try 阶段存在服务执行失败, 进入 Cancel 阶段** + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140726904.png) + +Cancel 取消执行,释放 Try 阶段预留的业务资源,上面的例子中,Cancel 操作会把冻结的库存释放,并更新订单状态为取消。 + +**方案总结** + +TCC 事务机制相比于上面介绍的 XA 事务机制,有以下优点: + +- **性能提升** - 具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。 +- **数据最终一致性** - 基于 Confirm 和 Cancel 的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。 +- **可靠性** - 解决了 XA 协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。 + +缺点: TCC 的 Try、Confirm 和 Cancel 操作功能要按具体业务来实现,**业务耦合度较高**,提高了开发成本。 + +::: + +### 【高级】本地消息表是如何工作的? + +:::details 要点 + +本地消息表的核心思路是将分布式事务拆分成本地事务进行处理。 + +方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。 + +这样设计可以避免”**业务处理成功 + 事务消息发送失败**",或"**业务处理失败 + 事务消息发送成功**"的棘手情况出现,保证 2 个系统事务的数据一致性。 + +事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。 + +整个业务处理流程如下: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140809614.png) + +> 1. **步骤 1、事务主动方处理本地事务。** 事务主动发在本地事务中处理业务更新操作和写消息表操作。 上面例子中库存服务阶段再本地事务中完成扣减库存和写消息表(图中 1、2)。 +> 2. **步骤 2、事务主动方通过 MQ 通知事务被动方处理事务**。 消息中间件可以基于 Kafka、RocketMQ 消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。 上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中 3 - 5)。 +> 3. **步骤 3、事务被动方通过 MQ 返回处理结果。** 上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中 6 - 8) + +为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下: + +> - 当步骤 1 处理出错,事务回滚,相当于什么都没发生。 +> - 当步骤 2、步骤 3 处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询超时 d 的消息数据,再次发送消息到 MQ 进行处理。事务被动方消费事务消息重试处理。 +> - 如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。 +> - 如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。 + +**方案总结** + +方案的优点如下: + +- 从应用设计开发的角度实现了消息数据的可靠性,**消息数据的可靠性不依赖于消息中间件**,弱化了对 MQ 中间件特性的依赖。 +- **方案简单**,容易实现。 + +缺点如下: + +- 与具体的业务场景绑定,**耦合性高,不可复用**。 +- 需要额外维护消息数据的传输,占用业务系统资源。 +- 业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。 + +::: + +### 【高级】消息事务是如何工作的? + +:::details 要点 + +MQ 事务方案本质是利用 MQ 功能实现的本地消息表。事务消息需要消息队列提供相应的功能才能实现,Kafka 和 RocketMQ 都提供了事务相关功能。 + +- **Kafka** 的解决方案是:直接抛出异常,让用户自行处理。用户可以在业务代码中反复重试提交,直到提交成功,或者删除之前修改的数据记录进行事务补偿。 +- **RocketMQ** 的解决方案是:通过事务反查机制来解决事务消息提交失败的问题。如果 Producer 在提交或者回滚事务消息时发生网络异常,RocketMQ 的 Broker 没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。为了支撑这个事务反查机制,业务代码需要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是成功还是失败。 + +#### RocketMQ 事务消息实现 + +事务消息是 Apache RocketMQ 提供的一种高级消息类型,支持在分布式场景下保障消息生产和本地事务的最终一致性。 + +**事务消息处理流程** + +事务消息交互流程如下图所示。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405140759853.png) + +1. 生产者将消息发送至 Apache RocketMQ 服务端。 +2. Apache RocketMQ 服务端将消息持久化成功之后,向生产者返回 Ack 确认消息已经发送成功,此时消息被标记为"暂不能投递",这种状态下的消息即为半事务消息。 +3. 生产者开始执行本地事务逻辑。 +4. 生产者根据本地事务执行结果向服务端提交二次确认结果(Commit 或是 Rollback),服务端收到确认结果后处理逻辑如下: + - 二次确认结果为 Commit:服务端将半事务消息标记为可投递,并投递给消费者。 + - 二次确认结果为 Rollback:服务端将回滚事务,不会将半事务消息投递给消费者。 +5. 在断网或者是生产者应用重启的特殊情况下,若服务端未收到发送者提交的二次确认结果,或服务端收到的二次确认结果为 Unknown 未知状态,经过固定时间后,服务端将对消息生产者即生产者集群中任一生产者实例发起消息回查。 **说明** 服务端回查的间隔时间和最大回查次数,请参见 [参数限制](https://rocketmq.apache.org/zh/docs/introduction/03limits)。 +6. 生产者收到消息回查后,需要检查对应消息的本地事务执行的最终结果。 +7. 生产者根据检查到的本地事务的最终状态再次提交二次确认,服务端仍按照步骤 4 对半事务消息进行处理。 + +**MQ 事务方案总结** + +相比本地消息表方案,MQ 事务方案优点是: + +- **业务解耦** - 消息数据独立存储 ,降低业务系统与消息系统之间的耦合。 +- **吞吐量优于本地消息表**方案。 + +缺点是: + +- **一次消息发送需要两次网络请求** (half 消息 + commit/rollback 消息) +- **业务处理服务需要实现消息状态回查接口** + +::: + +### 【高级】SAGA 事务是如何工作的? + +:::details 要点 + +Saga 事务的核心思想是:将长事务拆分为多个本地短事务,由 Saga 事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序依次调用补偿操作。 + +**Saga 事务基本协议如下**: + +- **将长事务拆分为多个有序子事务** - 每个 Saga 事务由一系列幂等的有序子事务 (sub-transaction) Ti 组成。 +- **每个子事务 Ti 都有对应的幂等补偿动作 Ci**,补偿动作用于撤销 Ti 造成的结果。 + +可以看到,和 TCC 相比,Saga 没有“预留”动作,它的 Ti 就是直接提交到库。 + +下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分 Saga 的执行顺序有两种: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405150751202.png) + +- 事务正常执行完成 T1, T2, T3, ..., Tn,例如:扣减库存 (T1),创建订单 (T2),支付 (T3),依次有序完成整个事务。 +- 事务回滚 T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n,例如:扣减库存 (T1),创建订单 (T2),支付 (T3,支付失败),支付回滚 (C3),订单回滚 (C2),恢复库存 (C1)。 + +恢复策略 + +Saga 定义了两种恢复策略: + +- 向前恢复 (forward recovery) + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405150752176.png) + +对应于上面第一种执行顺序,**适用于必须要成功的场景**,**失败需要进行重试**,执行顺序是类似于这样的:T1, T2, ..., Tj(失败), Tj(重试),..., Tn,其中 j 是发生错误的子事务 (sub-transaction)。该情况下不需要 Ci。 + +- 向后恢复 (backward recovery) + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405150752372.png) + +对应于上面提到的第二种执行顺序,其中 j 是发生错误的子事务 (sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个 Saga 的执行结果撤销。 + +Saga 事务常见的有两种不同的实现方式:命令协调和事件编排。 + +**命令协调** + +- **命令协调 (Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。** + +中央协调器(Orchestrator,简称 OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405150752580.png) + +以电商订单的例子为例: + +> 1. 事务发起方的主业务逻辑请求 OSO 服务开启订单事务。 +> 2. OSO 向库存服务请求扣减库存,库存服务回复处理结果。 +> 3. OSO 向订单服务请求创建订单,订单服务回复创建结果。 +> 4. OSO 向支付服务请求支付,支付服务回复处理结果。 +> 5. 主业务逻辑接收并处理 OSO 事务处理结果回复。 + +中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。 + +**事件编排** + +- **事件编排 (Event Choreography0:没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动**。 + +在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。 + +当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何 Saga 参与者听到都意味着事务结束。 + +以电商订单的例子为例: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405150753776.png) + +> 1. 事务发起方的主业务逻辑发布开始订单事件 +> 2. 库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件 +> 3. 订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件 +> 4. 支付服务监听订单已创建事件,进行支付,并发布订单已支付事件 +> 5. 主业务逻辑监听订单已支付事件并处理。 + +事件编排是实现 Saga 模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及 2 至 4 个步骤,则可能是非常合适的。 + +**方案总结** + +**命令协调设计的优点和缺点:** + +优点如下: + +- 服务之间关系简单,避免服务之间的循环依赖关系,因为 Saga 协调器会调用 Saga 参与者,但参与者不会调用协调器 +- 程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。 +- 易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试 + +缺点如下: + +- 中央协调器容易处理逻辑容易过于复杂,导致难以维护。 +- 存在协调器单点故障风险。 + +**事件/编排设计的优点和缺点** + +优点如下: + +- 避免中央协调器单点故障风险。 +- 当涉及的步骤较少服务开发简单,容易实现。 + +缺点如下: + +- 服务之间存在循环依赖的风险。 +- 当涉及的步骤较多,服务间关系混乱,难以追踪调测。 + +值得补充的是,由于 Saga 模型中没有 Prepare 阶段,因此事务间不能保证隔离性,当多个 Saga 事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。 + +::: + +## 分布式锁 + +> 扩展: +> +> - [分布式锁实现汇总](https://juejin.im/post/5a20cd8bf265da43163cdd9a) +> - [分布式锁实现原理与最佳实践 - 阿里云开发者](https://mp.weixin.qq.com/s/JzCHpIOiFVmBoAko58ZuGw) +> - [聊聊分布式锁 - 字节跳动技术团队](https://mp.weixin.qq.com/s/-N4x6EkxwAYDGdJhwvmZLw) +> - [Redis、ZooKeeper、Etcd,谁有最好用的分布式锁? - 腾讯云开发者](https://mp.weixin.qq.com/s/yZC6VJGxt1ANZkn0SljZBg) + +### 【初级】什么是分布式锁?为什么需要分布式锁? + +:::details 要点 + +在计算机科学中,**锁是在并发场景下用于强行限制资源访问的一种同步机制**,即用于在并发控制中通过互斥手段来保证数据同步安全。 + +在 Java 进程中,可以使用 Lock、synchronized 等来支持并发锁。如果是同一台机器的不同进程,想要同时操作一个共享资源(例如修改同一个文件),可以使用操作系统提供的「文件锁」或「信号量」来做互斥。这些发生在同一台机器上的互斥操作,可以称为**本地锁**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190814629.png) + +本地锁无法协同不同机器间的互斥操作。为了解决这个问题,需要引入分布式锁。 + +**分布式锁**,顾名思义,应用于分布式场景下,它和单进程中的锁并没有本质上的不同,只是控制对象由一个进程中的多个线程变成了多个进程中的多个线程。此外,临界区的资源也由进程内共享资源变成了分布式系统内部共享资源。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190815373.png) + +::: + +### 【高级】实现分布式锁有哪些要点? + +:::details 要点 + +分布式锁的实现要点如下: + +- **互斥** - **分布式锁必须是独一无二的**,表现形式为:向数据存储插入一个唯一的 key,一旦有一个线程插入这个 key,其他线程就不能再插入了。 + - 保证 key 唯一性的最简单的方式是使用 UUID。 + - 此外,可以参考 Snowflake ID(雪花算法),将机器地址(IP 地址、机器 ID、MAC 地址)、Jvm 进程 ID(应用 ID、服务 ID)、时间戳等关键信息拼接起来作为唯一标识。 +- **避免死锁** - 在分布式锁的场景中,部分失败和异步网络这两个问题是同时存在的。如果一个进程获得了锁,但是这个进程与锁服务之间的网络出现了问题,导致无法通信,那么这个情况下,如果锁服务让它一直持有锁,就会导致死锁的发生。常见的解决思路都是引入**超时机制**,即成功申请锁后,超过一定时间,锁失效(删除 key),原因在于它们无法感知申请锁的客户端节点状态。而 ZooKeeper 由于其 znode 以目录、文件形式组织,天然就存在物理空间隔离,只要 znode 存在,即表示客户端节点还在工作,所以不存在这种问题。 +- **可重入** - **可重入**指的是:**同一个线程在没有释放锁之前,能否再次获得该锁**。其实现方案是:只需在加锁的时候,**记录好当前获取锁的节点 + 线程组合的唯一标识**,然后在后续的加锁请求时,如果当前请求的节点 + 线程的唯一标识和当前持有锁的相同,那么就直接返回加锁成功;如果不相同,则按正常加锁流程处理。 +- **公平性** - 当多个线程请求同一锁时,它们必须按照请求的顺序来获取锁,即先来先得的原则。锁的公平性的实现也非常简单,对于被阻塞的加锁请求,我们只要先记录好它们的顺序,在锁被释放后,按顺序颁发就可以了。 +- **重试** - 有时候,加锁失败可能只是由于网络波动、请求超时等原因,稍候就可以成功获取锁。为了应对这种情况,加锁操作需要支持重试机制。常见的做法是,设置一个加锁超时时间,在该时间范围内,不断自旋重试加锁操作,超时后再判定加锁失败。 +- **容错** - 分布式锁若存储在单一节点,一旦该节点宕机或失联,就会导致锁失效。将分布式锁存储在多数据库实例中,加锁时并发写入 `N` 个节点,只要 `N / 2 + 1` 个节点写入成功即视为加锁成功。 + +::: + +### 【中级】数据库分布式锁如何实现? + +:::details 要点 + +#### 数据库分布式锁原理 + +(1)创建锁表 + +```sql +CREATE TABLE `distributed_lock` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `resource` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '资源', + `count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '锁次数,统计可重入锁', + `desc` TEXT DEFAULT NULL COMMENT '备注', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_resource`(`resource`) +) + ENGINE = InnoDB DEFAULT CHARSET = `utf8mb4`; +``` + +(2)获取锁 + +想要锁住某个方法时,执行以下 SQL: + +```sql +insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) +``` + +因为我们对 `method_name` 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 + +成功插入则获取锁。 + +(3)释放锁 + +当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql: + +```sql +delete from methodLock where method_name ='method_name' +``` + +#### 数据库分布式锁小结 + +数据库分布式锁的**问题**: + +- **死锁**:一旦释放锁操作失败,或持有锁的机器宕机、断连,就会导致锁记录一直存在,其他线程无法再获得锁。解决办法:为锁增加失效时间字段,启动一个定时任务,隔一段时间清除一次过期的数据。 +- **非阻塞**:因为 `insert` 操作一旦失败就会报错,因此未获得锁的线程并不会进入排队队列,要想获得锁就要再次触发加锁操作。解决办法:循环重试,直到插入成功,这么做会产生一定额外开销。 +- **非重入**:同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。解决办法:在数据库表中加个字段,记录当前获得锁的节点信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 +- **单点问题**:如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。解决办法:单点问题可以用多数据库实例,同时写入 `N` 个节点,`N / 2 + 1` 个成功就加锁成功。 + +数据库分布式锁的**利弊**: + +- **优点**:直接借助数据库,简单易懂。 +- **缺点**:会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。此外,数据库性能易成为瓶颈。 + +::: + +### 【高级】ZooKeeper 分布式锁如何实现? + +:::details 要点 + +#### ZooKeeper 分布式锁实现原理 + +ZooKeeper 分布式锁的实现基于 ZooKeeper 的两个重要特性: + +- **顺序临时节点**:ZooKeeper 的存储类似于 DNS 那样的具有层级的命名空间。ZooKeeper 节点类型可以分为持久节点(`PERSISTENT`)、临时节点(`EPHEMERAL`),每个节点还能被标记为有序性(`SEQUENTIAL`),一旦节点被标记为有序性,那么整个节点就具有顺序自增的特点。 +- **Watch 机制**:ZooKeeper 允许用户在指定节点上注册一些 `Watcher`,并且在特定事件触发的时候,ZooKeeper 服务端会将事件通知给用户。 + +下面是 ZooKeeper 分布式锁的工作流程: + +1. 创建一个目录节点,比如叫做 `/locks`; +2. 线程 A 想获取锁,就在 `/locks` 目录下创建临时顺序 zk 节点; +3. 获取 `/locks`目录下所有的子节点,检查是否存在比自己顺序更小的节点:若不存在,则说明当前线程创建的节点顺序最小,获取锁成功; +4. 此时,线程 B 试图获取锁,发现自己的节点顺序不是最小,设置监听锁号在自己前一位的节点; +5. 线程 A 处理完,删除自己的节点。线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 + +#### ZooKeeper 分布式锁小结 + +ZooKeeper 分布式锁的**优点**是较为**可靠**: + +- **避免死锁**:ZooKeeper 通过临时节点 + 监听机制,可以保证:如果持有临时节点的线程主动解锁或断连,Zk 会自动删除临时节点,这意味着锁的释放。所以,不存在锁永久不释放从而导致死锁的问题。 +- **单点问题**:ZooKeeper 采用主从架构,并确保主从同步是强一致的,因此不会出现单点问题。 + +ZooKeeper 分布式锁的**缺点**是:加锁、解锁操作,本质上是对 ZooKeeper 的写操作,全部由 ZooKeeper 主节点负责。如果加锁、解锁的吞吐量很大,容易出现单点写入瓶颈。 + +::: + +### 【高级】Redis 分布式锁如何实现? + +:::details 要点 + +#### Redis 分布式锁实现原理 + +##### 极简版本 + +我们先来看一下,如何实现一个极简版本的 Redis 分布式锁。 + +(1)加锁 + +Redis 中的 `setnx` 命令,表示当且仅当 key 不存在时,才会写入 key。由于其互斥性,所以可以基于此来实现分布式锁。 + +执行 `setnx key val`,若返回 1,表示写入成功,即加锁成功;若返回 0,表示该 key 已存在,写入失败,即加锁失败。 + +(2)解锁 + +Redis 分布式锁如何解锁呢? + +很简单,删除 key 就意味着释放锁,即执行 `del key` 命令。 + +##### 避免死锁 + +极简版本的解决方案有一个很大的问题:**存在死锁的可能**。持有锁的节点如果执行业务过程中出现异常或机器宕机,都可能导致无法释放锁。这种情况下,其他节点永远也无法再获取锁。 + +对于异常,在 Java 中,可以通过 `try...catch...finally` 来保证:最终一定会释放锁,其他编程语言也有相似的语法特性。 + +对于机器宕机这种情况,如何处理呢?通常的对策是:为锁加上**超时机制,过期自动删除**。 + +在 Redis 中,`expire` 命令可以为 key 设置一个超时时间,一旦过期,Redis 会自动删除 key。如此看来,`setnx` + `expire` 组合使用,就能解决死锁问题了。可惜,没那么简单。Redis 只能保证单一命令的原子性,不保证组合命令的原子性。 + +那么,Redis 中有没有一条命令可以实现 setnx + expire 的组合语义呢?还真有,可以通过下面的命令来实现: + +```bash +# 下面两条命令是等价的 +SET key val NX PX 30000 +SET key val NX EX 30 +``` + +参数说明: + +- `NX`:该参数表示当且仅当 key 不存在,才能写入成功 +- `PX`:超时时间,单位毫秒 +- `EX`:超时时间,单位秒 + +##### 超时续期 + +为了避免死锁,我们为锁添加了超时时间。但这里有一个问题,如果应用加锁时,对于操作共享资源的时长估计不足,可能会出现:操作尚未执行完,但是锁没了的尴尬情况。为了解决这个问题,很自然会想到,时间不够,就续期呗。 + +具体来说,如何续期呢?一种方案是:加锁后,启动一个定时任务,周期性检测锁是否快要过期,如果快要过期并且操作尚未结束,就对锁进行自动续期。自行实现这个方案似乎有点繁琐,好在开源 Redis 客户端 [Redisson](https://github.com/redisson/redisson) 中已经为锁的**超时续期**提供了一个成熟的机制——WatchDog(看门狗)。我们可以直接拿来主义即可。 + +##### 安全解锁 + +前文提到了,解锁的操作,实际上就是 `del key`。这里存在一个问题:因为没有任何判断,任何节点都可以随意删除 key,换句话说,锁可能会被其他节点释放。如何避免这个问题呢?解决方法就是:为锁添加**唯一性标识**来进行互斥。唯一性标识可以是 UUID,可以是雪花算法 ID 等。 + +在 Redis 分布式锁中,唯一性标识的具体实现就是在 `set key val` 时,将唯一性标识 id 作为 `val` 写入。**解锁前,先判断 key 的 value,必须和 set 时写入的 id 值保持一致,以此确认锁归属于自己**。解锁的伪代码如下: + +```java +if (redis.get("key") == id) + redis.del("key"); +``` + +这里依然存在一个问题,由于需要在 Redis 中,先 `get`,后 `del` 操作,所以无法保证操作的原子性。为了保证原子性,可以将这段伪代码用 lua 脚本来实现,这么做的理由是 Redis 中支持原子性的执行 lua 脚本。下面是安全解锁的 lua 脚本代码: + +```lua +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + +##### 自旋重试 + +有时候,加锁失败可能只是由于网络波动、请求超时等原因,稍候就可以成功获取锁。为了应对这种情况,加锁操作需要支持重试机制。常见的做法是,设置一个加锁超时时间,在该时间范围内,不断自旋重试加锁操作,超时后再判定加锁失败。 + +下面是一个自旋重试获取锁的伪代码示例: + +```java +try { + long begin = System.currentTimeMillis(); + while (true) { + String result = jedis.set(lockKey, uniqId, "NX", "PX", expireTime); + if ("OK".equals(result)) { + // 加锁成功,执行业务操作 + return true; + } + + long time = System.currentTimeMillis() - begin; + if (time >= timeout) { + return false; + } + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} catch (Exception e) { + // 异常处理 +} finally { + // 释放锁 +} +``` + +#### Redis 分布式锁小结 + +在前文中,为了实现一个靠谱的 Redis 分布式锁,我们讨论了避免死锁、超时续期、安全解锁几个问题以及应对策略。但是,依然存在一些其他问题: + +- **不可重入** - 同一个线程无法多次获取同一把锁。 +- **单点问题** - Redis 主从同步存在延迟,有可能导致锁冲突。举例来说:线程一在主节点加锁,如果主节点尚未同步给从节点就发生宕机;此时,Redis 集群会选举一个从节点作为新的主节点。此时,新的主节点没有锁的数据,若有其他线程试图加锁,就可以成功获取锁,即出现同时有多个线程持有锁的情况。解决这个问题,可以使用 RedLock 算法。 + +[Redisson](https://github.com/redisson/redisson) 是一个流行的 Redis Java 客户端,它基于 Netty 开发,并提供了丰富的扩展功能,如:[分布式计数器](https://redisson.org/docs/data-and-services/counters/)、[分布式集合](https://redisson.org/docs/data-and-services/collections/)、[分布式锁](https://redisson.org/docs/data-and-services/locks-and-synchronizers/) 等。 + +Redisson 支持的分布式锁有多种:Lock, FairLock, MultiLock, RedLock, ReadWriteLock, Semaphore, PermitExpirableSemaphore, CountDownLatch,可以根据场景需要去选择,非常方面。一般而言,使用 Redis 分布式锁,推荐直接使用 Redisson 提供的 API,功能全面且较为可靠。 + +::: + +### 【中级】RedLock 分布式锁如何实现? + +:::details 要点 + +RedLock 分布式锁,是 Redis 的作者 Antirez 提出的一种解决方案。 + +> 扩展:[RedLock 官方文档](https://redis.io/docs/latest/develop/use/patterns/distributed-locks/) + +#### RedLock 分布式锁原理 + +RedLock 分布式锁在普通 Redis 分布式锁的基础上,进行了扩展,其要点在于: + +- (1)加锁操作不是写入单一节点,而是同时写入多个主节点,官方推荐集群中至少有 5 个主节点。 +- (2)只要半数以上的主节点写入成功,即视为加锁成功。 +- (3)大多数节点加锁的总耗时,要小于锁设置的过期时间。 +- (4)解锁时,要向所有节点发起请求。 + +下面来逐一解释以上各要点的用意: + +(1)RedLock 加锁时,为什么要同时写入多个主节点? + +这是为了避免单点问题,即使有部分实例出现异常,依然可以正常提供加锁、解锁能力。 + +(2)为什么要半数以上的主节点写入成功,才视为加锁成功? + +在分布式系统中,为了达成共识,常常采用“多数派”策略来进行决策:大多数节点认可的行为,就视为整体通过。 + +(3)为什么加锁成功后,还要计算加锁的累计耗时? + +因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久。而且,网络情况是复杂的,可能存在延迟、丢包、超时等情况。网络请求越多,异常发生的概率就越大。所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经**超过**了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。 + +(4)解锁时,为什么要向所有节点发起请求? + +因为网络环境的复杂性,可能会存在这种情况:向某主节点写入锁信息,实际写入成功,但是响应超时或丢包。 + +所以,释放锁时,不管之前有没有加锁成功,需要释放**所有节点**的锁,以保证清理节点上**残留**的锁。 + +#### RedLock 分布式锁小结 + +(1)**RedLock 不能完全保证安全性** + +分布式系统会遇到三座大山:**NPC** + +- N:Network Delay,**网络延迟**; +- P:Process Pause,进程暂停(**GC**); +- C:Clock Drift,**时钟漂移**。 + +RedLock 在遇到以上情况时,不能保证安全性。 + +(2)RedLock 加锁、解锁需要处理多个节点,代价太高 + +> 总结来说,**已知的分布式锁,无论采用什么解决方案,在极端情况下,都无法保证百分百的安全。** + +::: + +### 【高级】分布式锁如何进行技术选型? + +:::details 要点 + +下面是主流分布式锁技术方案的对比,可以在技术选型时作为参考: + +| | 数据库分布式锁 | Redis 分布式锁 | ZooKeeper 分布式锁 | +| -------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| 方案要点 | 1. 维护一张锁表,为锁的唯一标识字段添加唯一性约束。
    2. 只要 insert 成功,即视为加锁成功。 | `set lockKey randomValue NX PX/EX time` 当且仅当 key 不存在时才可以写入,并且设定超时时间,以避免死锁。 | 加锁本质上是在 zk 中指定目录创建**顺序临时接节点**,序号最小即加锁成功。节点删除时,有监听通知机制告知申请锁的线程。 | +| 方案难度 | 实现简单、易于理解 | 较为简单,但要使其更可靠,需要有一些完善策略 | 应用简单,但 zk 内部机制并不简单 | +| 性能 | 性能最差,易成为瓶颈 | 性能最高 | 性能弱于 Redis | +| 可靠性 | 有锁表的风险 | 较为可靠(需要一些完善策略) | 可靠性最高 | +| 适用场景 | 一般不采用 | 适用于高并发的场景 | 适用于要求可靠,但并发量不高的场景 | +| 开源实现 | 无 | [Redisson](https://github.com/redisson/redisson) | [Apache Curator](https://curator.apache.org/docs/about/) | + +::: + +## 分布式 ID + +> 扩展: +> +> - [如果再有人问你分布式 ID,这篇文章丢给他](https://juejin.im/post/5bb0217ef265da0ac2567b42) +> - [理解分布式 id 生成算法 SnowFlake](https://segmentfault.com/a/1190000011282426) +> - [Leaf——美团点评分布式 ID 生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html) +> - [UUID 规范](https://www.ietf.org/rfc/rfc4122.txt) +> - [百度分布式 ID](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md) +> - [ShardingSphere 分布式主键](https://shardingsphere.apache.org/document/current/cn/features/sharding/other-features/key-generator/) + +### 【初级】什么是分布式 ID?为什么需要分布式 ID? + +:::details 要点 + +ID是Identity的缩写,用于唯一的标识一条数据。**分布式 ID**,顾名思义,是**用于在分布式系统中唯一标识数据的ID**。 + +传统数据库基本都支持针对单表生成唯一性的自增主键。随着数据的膨胀,单机成为了性能和容量的瓶颈。为了解决这个问题,有了分库分表技术。分库分表所要面临的第一个问题是:数据分布在不同机器上,数据库无法保证多个节点上产生的主键唯一。 这就需要用到分布式 ID 了,它起到了分布式系统中**全局 ID** 的作用。 + +::: + +### 【中级】有哪些生成分布式 ID 的方式? + +:::details 要点 + +生成分布式 ID 主要有以下方式: + +- **UUID** - UUID 是通用唯一识别码(Universally Unique Identifier)的缩写,是一种 128 位的标识符,用 16 进制表示,需要 32 个字符。**UUID 会根据运行应用的计算机网卡 MAC 地址、时间戳、命令空间等元素,通过一定的随机算法产生**。 + - UUID 存在 5 个版本。 + - UUID 不保证全局唯一性,我们需要小心 ID 冲突(尽管这种可能性很小)。 + - **优点**:实现简单、生成速度较快(本地生成,不依赖其他服务)。 + - **缺点**:无序、长度过长、不安全(基于 MAC 地址生成 UUID 的算法,可能会造成 MAC 地址泄露)。 +- **数据库自增主键** - 大多数数据库都支持自增主键。基于此特性,可以利用事务管理控制生成唯一 ID。 + - **优点**:实现简单、有序、长度较小 + - **缺点**:性能差、存在单点问题、不安全(可以通过 ID 递增规律推算出数据量) +- **数据库号段** - 一次批量生成一个 segment(号段),号段的大小由 step(步长)控制。用完之后再去数据库获取新的号段。 +- **原子计数器** - 一些 NoSQL 数据库提供了原子性的计数器原子计数器 - 利用一些 NoSQL 数据库提供的原子性计数器,来实现分布式 ID。 + - **Redis `incr` / `incrby`** - Redis 的 String 类型提供 `INCR` 和 `INCRBY` 命令将 key 中储存的数字**原子递增**。 + - **优点**:高性能、有序 + - **缺点**:和数据库自增序列方案的缺点类似 + - **ZooKeeper 顺序节点** - 利用 ZooKeeper 数据模型中的顺序节点作为分布式 ID。 + - **优点**:简单、可靠性高 + - **缺点**:性能不高 +- **Snowflak(雪花算法)** - Snowflake ID 生成过程包含多个组件:时间戳、机器 ID 和序列号。第一位未使用,以确保 ID 正确。此生成器不需要通过网络与 ID 生成器通信,因此速度快且可扩展。Snowflake 的实现各不相同。例如,可以将数据中心 ID 添加到“MachineID”组件中,以保证全局唯一性。 + +::: + +## 分布式会话 + +### 【初级】Cookie 和 Session 有什么区别? + +:::details 要点 + +由于 Http 是一种无状态的协议,服务器单从网络连接上无从知道客户身份。 + +所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器。而这个状态需要通过 cookie 或者 session 去实现。 + +**Cookie** 实际上是存储在用户浏览器上的文本信息,并保留了各种跟踪的信息。生成 Cookie 后,用户后续每次请求都会携带 Cookie。 + +Cookie 通常有大小限制(4KB)。用户可以选择在浏览器中禁用 Cookie。 + +一个简单的 cookie 设置如下: + +```http +Set-Cookie: = +``` + +```http +HTTP/2.0 200 OK +Content-Type: text/html +Set-Cookie: yummy_cookie=choco +Set-Cookie: tasty_cookie=strawberry + +[page content] +``` + +Session 是在服务器端创建和存储的。服务器上通常会生成一个唯一的会话 ID(sessionId),sessionId 附加到特定的用户会话。sessionId 以 Cookie 的形式返回到客户端。Session 可以容纳大量数据。由于 Session 数据不直接由客户端访问,因此 Session 提供了更高的安全性。 + +Cookie 和 Session 的主要区别可以参考以下表格: + +| | Cookie | Session | +| ------------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | +| **作用范围** | 保存在客户端(浏览器) | 保存在服务器端 | +| **隐私策略** | 存储在客户端,比较容易遭到非法获取 | 存储在服务端,安全性相对 Cookie 要好一些 | +| **存储方式** | 只能保存 ASCII | 可以保存任意数据类型。
    一般情况下我们可以在 Session 中保持一些常用变量信息,比如说 UserId 等。 | +| **存储大小** | 不能超过 4K | 存储大小远高于 Cookie | +| **生命周期** | 可设置为永久保存
    比如我们经常使用的默认登录(记住我)功能 | 一般失效时间较短
    客户端关闭或者 Session 超时都会失效。 | + +::: + +### 【中级】如果禁用了 Cookie 怎么办? + +:::details 要点 + +既然服务端是根据 Cookie 中的信息判断用户是否登录,那么如果浏览器中禁止了 Cookie,如何保障整个机制的正常运转。 + +- 第一种方案,每次请求中都携带一个 SessionID 的参数,也可以 Post 的方式提交,也可以在请求的地址后面拼接 `xxx?SessionID=123456...`。 + +- 第二种方案,Token 机制。Token 机制多用于 App 客户端和服务器交互的模式,也可以用于 Web 端做用户状态管理。 + +Token 的意思是“令牌”,是服务端生成的一串字符串,作为客户端进行请求的一个标识。Token 机制和 Cookie 和 Session 的使用机制比较类似。 + +当用户第一次登录后,服务器根据提交的用户信息生成一个 Token,响应时将 Token 返回给客户端,以后客户端只需带上这个 Token 前来请求数据即可,无需再次登录验证。 + +::: + +### 【中级】分布式 Session 有几种实现方案? + +:::details 要点 + +在分布式场景下,一个用户的 Session 如果只存储在一个服务器上,那么当负载均衡器把用户的下一个请求转发到另一个服务器上,该服务器没有用户的 Session,就可能导致用户需要重新进行登录等操作。 + +分布式 Session 的几种实现策略: + +1. 粘性 session +2. 应用服务器间的 session 复制共享 +3. 基于缓存的 session 共享 ✅ + +> 推荐:基于缓存的 session 共享 + +#### 粘性 Session + +粘性 Session(Sticky Sessions)**需要配置负载均衡器,使得一个用户的所有请求都路由到一个服务器节点上**,这样就可以把用户的 Session 存放在该服务器节点中。 + +缺点:**当服务器节点宕机时,将丢失该服务器节点上的所有 Session**。 + +
    + +
    + +#### Session 复制共享 + +Session 复制共享(Session Replication)**在服务器节点之间进行 Session 同步操作**,这样的话用户可以访问任何一个服务器节点。 + +缺点:**占用过多内存**;**同步过程占用网络带宽以及服务器处理器时间**。 + +
    + +
    + +#### 基于缓存的 session 共享 + +**使用一个单独的存储服务器存储 Session 数据**,可以存在 MySQL 数据库上,也可以存在 Redis 或者 Memcached 这种内存型数据库。 + +缺点:需要去实现存取 Session 的代码。 + +
    + +
    + +::: + +## 参考资料 + +- [**数据密集型应用系统设计**](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\244\215\345\210\266.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\244\215\345\210\266.md" new file mode 100644 index 0000000000..5f0d2da1ab --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\345\244\215\345\210\266.md" @@ -0,0 +1,487 @@ +--- +title: 分布式复制 +date: 2022-06-11 10:40:10 +categories: + - 分布式 + - 分布式协同 + - 分布式协同综合 +tags: + - 分布式 + - 协同 + - 复制 + - 主从 + - 多主 + - 无主 +permalink: /pages/2057482f/ +--- + +# 分布式复制 + +**复制主要指通过网络在多台机器上保存相同数据的副本**。 + +复制数据,可能出于各种各样的原因: + +- **提高可用性** - 当部分组件出现位障,系统依然可以继续工作,系统依然可以继续工作。 +- **降低访问延迟** - 使数据在地理位置上更接近用户。 +- **提高读吞吐量** - 扩展至多台机器以同时提供数据访问服务。 + +复制的模式有以下几种: + +- **主从复制** - **所有的写入操作都发送到主节点**,由主节点负责将数据更改事件发送到从节点。每个从节点都可以接收读请求,但内容可能是过期值。 +- **多主复制** - **系统存在多个主节点,每个都可以接收写请求**,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其他主节点和自己的从节点。 +- **无主复制** - **系统中不存在主节点,每一个节点都能接受客户端的写请求**。接受写请求的副本不会将数据变更同步到其他的副本。此外,**读取时从多个节点上并行读取,以此检测和纠正某些过期数据**。 + +此外,复制还需要考虑以下问题: + +- **同步还是异步** +- **如何处理失败的副本** +- **如何保证数据一致** + +## 主从复制 + +如何确保所有副本之间的数据是一致的? + +对于每一次数据写入,所有副本都需要随之更新;否则,某些副本将出现数据不一致。 + +最常见的解决方案就是**主从复制**,其原理如下: + +主从复制模式中只有一个主副本(或称为主节点) ,其余称为从副本(或称为从节点)。 + +1. 所有的写请求只能发送给主副本,主副本首先将新数据写入本地存储。 + +2. 然后,主副本将数据更改作为复制的日志或更新流发送给所有从副本。每个从副本获得更新数据之后将其应用到本地,且严格保持与主副本相同的写入顺序。 + +3. 读请求既可以在主副本上,也可以在从副本上执行。 + +再次强调,**只有主副本才可以接受写请求**:从客户端的角度来看,从副本都是只读的。如果由于某种原因,例如与主节点之间的网络中断而导致主节点无法连接,主从复制方案就会影响所有的写入操作。 + +![主从复制系统](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202101.png) + +支持主从复制的系统: + +- 数据库:MySql、PostgreSQL(9.0 版本后)、MongoDB 等 +- 消息队列:Kafka、RabbitMQ 等 + +### 同步复制与异步复制 + +![主从复制——同步和异步](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202158.png) + +通常情况下,复制速度会非常快。但是,系统其实并没有保证一定会在多长时间内完成复制。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如,由于从节点刚从故障中恢复,或者系统已经接近最大设计上限,或者节点之间的网络出现问题。 + +- **同步复制的优点**:一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。 +- **同步复制的缺点**:如果同步的从节点无法完成确认(例如由于从节点发生崩溃,或者网络故障,或任何其他原因),写入就不能视为成功。主节点会阻塞其后所有的写操作,直到同步副本确认完成。 + +因此,**把所有从节点都配置为同步复制有些不切实际**。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前。实际应用中,推荐的同步模式(也是很多数据库的选择)是:**只要有一个从节点或半数以上的从节点同步成功,就视为同步,直接返回结果;剩下的节点都通过异步方式同步**。万一同步的从节点变得不可用或性能下降,则将另一个异步的从节点提升为同步模式。这样可以保证至少有两个节点(即主节点和一个同步从节点)拥有最新的数据副本。这种配置有时也称为**半同步**。 + +主从复制还经常会被配置为全异步模式。 + +- **异步复制的优点**:不管从节点上数据多么滞后,主节点总是可以继续响应写请求,系统的吞吐性能更好。 +- **异步复制的缺点**:如果主节点发生故障且不可恢复,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了写操作,却无法保证数据的持久化。 + +### 配置新的从节点 + +当如果出现以下情况时,如需要增加副本数以提高容错能力,或者替换失败的副本,就需要考虑增加新的从节点。但如何确保新的从节点和主节点保持数据一致呢? + +简单地将数据文件从一个节点复制到另一个节点通常是不够的。主要是因为客户端仍在不断向数据库写入新数据,数据始终处于不断变化之中,因此常规的文件拷贝方式将会导致不同节点上呈现出不同时间点的数据。 + +另一种思路是:考虑锁定数据库(使其不可写)来使磁盘上的文件保持一致,但这会违反高可用的设计目标。在不停机、数据服务不中断的前提下,也有一种可行性复制方案,其主要操作步骤如下: + +1. 在某个时间点对主节点的数据副本产生一个一致性快照,这样避免长时间锁定整个数据库。目前大多数数据库都支持此功能,快照也是系统备份所必需的。而在某些情况下,可能需要第三方工具,如 MySQL 的 innobackupex。 +2. 将此快照拷贝到新的从节点。 +3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定位置相关联,这个位置信息在不同的系统有不同的称呼,如 PostgreSQL 将其称为“ log sequence number” (日志序列号),而 MySQL 将其称为“ binlog coordinates ” 。 +4. 获得日志之后,从节点来应用这些快照点之后所有数据变更,这个过程称之为追赶。接下来,它可以继续处理主节点上新的数据变化。井重复步骤 1 ~步骤 4 。 + +在不同系统中,建立新的从副本具体操作步骤可能有所不同。 + +### 处理节点失效 + +系统中的任何节点都可能因故障或者计划内的维护(例如重启节点以安装内核安全补丁)而导致中断甚至停机。如果能够在不停机的情况下重启某个节点,这会对运维带来巨大的便利。我们的目标是,尽管个别节点会出现中断,但要保持系统总体的持续运行,并尽可能减小节点中断带来的影响。 + +如何通过主从复制技术来实现系统高可用呢? + +#### 从节点失效:追赶式恢复 + +从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,或者主从节点之间的网络发生暂时中断(闪断),则恢复比较容易,根据副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。 + +#### 主节点失效:节点切换 + +选择某个从节点将其提升为主节点;客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接受来自新的主节点上的变更数据,这一过程称之为切换。 + +故障切换可以手动进行,例如通知管理员主节点发生失效,采取必要的步骤来创建新的主节点;或者以自动方式进行。自动切换的步骤通常如下: + +1. **确认主节点失效**。有很多种出错可能性,很难准确检测出问题的原因,所以大多数系统都采用了基于超时的机制:节点间频繁地互相发生发送心跳悄息,如果发现某一个节点在一段比较长时间内(例如 30s )没有响应,即认为该节点发生失效。 +2. **选举新的主节点**。可以通过选举的方式(超过多数的节点达成共识)来选举新的主节点,或者由之前选定的某控制节点来指定新的主节点。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。让所有节点同意新的主节点是个典型的共识问题。 +3. **重新配置系统使新主节点生效**。客户端现在需要将写请求发送给新的主节点。如果原主节点之后重新上线,可能仍然自认为是主节点,而没有意识到其他节点已经达成共识迫使其下台。这时系统要确保原主节点降级为从节点,并认可新的主节点。 + +上述切换过程依然充满了很多变数: + +- 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线并加入到集群,接下来的写操作会发生什么?新的主节点很可能会收到冲突的写请求,这是因为原主节点未意识的角色变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是,原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。 +- 如果在数据库之外有其他系统依赖于数据库的内容并在一起协同使用,丢弃数据的方案就特别危险。例如,在 GitHub 的一个事故中,某个数据并非完全同步的 MySQL 从节点被提升为主副本,数据库使用了自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点( 即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部 Redis 所引用,结果出现 MySQL 和 Redis 之间的不一致,最后导致了某些私有数据被错误地泄露给了其他用户。 +- 在某些故障情况下,可能会发生两个节点同时-都自认为是主节点。这种情况被称为**脑裂**,它非常危险:两个主节点都可能接受写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏。作为一种安全应急方案,有些系统会采取措施来强制关闭其中一个节点。然而,如果设计或者实现考虑不周,可能会出现两个节点都被关闭的情况。 +- 如何设置合适的超时来检测主节点失效呢? 主节点失效后,超时时间设置得越长也意味着总体恢复时间就越长。但如果超时设置太短,可能会导致很多不必要的切换。例如,突发的负载峰值会导致节点的响应时间变长甚至超肘,或者由于网络故障导致延迟增加。如果系统此时已经处于高负载压力或网络已经出现严重拥塞,不必要的切换操作只会使总体情况变得更糟。 + +### 复制日志的实现 + +#### 基于语句的复制 + +最简单的情况,主节点记录所执行的每个写请求(操作语句)井将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个 INSERT 、UPDATE 或 DELETE 语句都会转发给从节点,并且每个从节点都会分析井执行这些 SQU 吾句,如同它们是来自客户端那样。 + +听起来很合理也不复杂,但这种复制方式有一些不适用的场景: + +- 任何调用非确定性函数的语句,如 `NOW()` 获取当前时间,或 `RAND()` 获取一个随机数等,可能会在不同的副本上产生不同的值。 +- 如果语句中使用了自增列,或者依赖于数据库的现有数据(例如,`UPDATE ... WHERE <某些条件>`),则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。进而,如果有多个同时并发执行的事务时,会有很大的限制。 +- 有副作用的语句(例如,触发器、存储过程、用户定义的函数等),可能会在每个副本上产生不同的副作用。 + +有可能采取一些特殊措施来解决这些问题,例如,主节点可以在记录操作语句时将非确定性函数替换为执行之后的确定的结果,这样所有节点直接使用相同的结果值。但是,这里面存在太多边界条件需要考虑,因此目前通常首选的是其他复制实现方案。 + +MySQL 5.1 版本之前采用基于操作语句的复制。现在由于逻辑紧凑,依然在用,但是默认情况下,如果语句中存在一些不确定性操作,则 MySQL 会切换到基于行的复制(稍后讨论)。VoltDB 使用基于语句的复制,它通过事务级别的确定性来保证复制的安全。 + +#### 基于预写日志(WAL)传输 + +通常每个写操作都是以追加写的方式写入到日志中: + +- 对于日志结构存储引擎,日志是主要的存储方式。日志段在后台压缩井支持垃圾回收。 +- 对于采用覆写磁盘的 BTree 结构,每次修改会预先写入日志,如系统发生崩溃,通过索引更新的方式迅速恢复到此前一致状态。 + +不管哪种情况,所有对数据库写入的字节序列都被记入日志。因此可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外,主节点还可以通过网络将其发送给从节点。 + +PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要缺点是日志描述的数据结果非常底层:一个 WAL 包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无能支持主从节点上运行不同版本的软件。 + +看起来这似乎只是个有关实现方面的小细节,但可能对运营产生巨大的影响。如果复制协议允许从节点的软件版本比主节点更新,则可以实现数据库软件的不停机升级:首先升级从节点,然后执行主节点切换,使升级后的从节点成为新的主节点。相反,复制协议如果要求版本必须严格一致(例如 WALf 专输),那么就势必以停机为代价。 + +#### 基于行的逻辑日志复制 + +如果复制和存储引擎采用不同的日志格式,这样复制与存储的逻辑就可以剥离。这种复制日志称为逻辑日志,以区分物理存储引擎的数据表示。 + +关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求: + +- 对于行插入,日志包含所有相关列的新值。 +- 对于行删除,日志里有足够的信息来唯一标识已删除的行,通常是靠主键,但如果表上没有定义主键,就需要记录所有列的旧值。 +- 对于行更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少包含所有已更新列的新值)。 + +如果一条事务涉及多行的修改,则会产生多个这样的日志记录,并在后面跟着一条记录,指出该事务已经提交。MySQL 的二进制日志 binlog (当配置为基于行的复制时)使用该方式。 + +由于逻辑日志与存储引擎逻辑解耦,因此可以更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。 + +对于外部应用程序来说,逻辑日志格式也更容易解析。 + +#### 基于触发器的复制 + +在某些情况下,我们可能需要更高的灵活性。例如,只想复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要订制、管理冲突解决逻辑,则需要将复制控制交给应用程序层。 + +有一些工具,可以通过读取数据库日志让应用程序获取数据变更。另一种方法则是借助许多关系数据库都支持的功能:触发器和存储过程。 + +触发器支持注册自己的应用层代码,使得当数据库系统发生数据更改(写事务)时自动执行上述自定义代码。通过触发器技术,可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表,实施必要的自定义应用层逻辑,例如将数据更改复制到另一个系统。Oracle 的 Databus 和 Postgres 的 Bucardo 就是这种技术的典型代表。基于触发器的复制通常比其他复制方式开销更高,也比数据库内置复制更容易出错,或者暴露一些限制。然而,其高度灵活性仍有用武之地。 + +## 复制滞后问题 + +主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询。对于读操作密集的负载(如 Web ),这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载井允许读取请求就近满足。 + +在这种扩展体系下,只需添加更多的从副本,就可以提高读请求的服务吞吐量。但是,这种方法实际上只能用于异步复制,如果试图同步复制所有的从副本,则单个节点故障或网络中断将使整个系统无法写入。而且节点越多,发生故障的概率越高,所以完全同步的配置现实中反而非常不可靠。 + +不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显的不一致:由于并非所有的写入都反映在从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。经过一段时间之后,从节点最终会赶上并与主节点数据保持一致。这种效应也被称为**最终一致性**。 + +### 写后读一致性 + +许多应用让用户提交一些数据,接下来查看他们自己所提交的内容。例如客户数据库中的记录,亦或者是讨论主题的评论等。提交新数据须发送到主节点,但是当用户读取数据时,数据可能来自从节点。这对于读多写少的场景是个非常合适的方案。 + +然而对于异步复制存在这样一个问题,如图所示,用户在写入不久即查看数据,则新数据可能尚未到达从节点。对用户来讲,看起来似乎是刚刚提交的数据丢失了,显然用户不会高兴。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302204836.png) + +对于这种情况,我们需要读写一致性。该机制保证如果用户重新加载页面,他们总能看到自己最近提交的更新。但对其他用户则没有任何保证,这些用户的更新可能会在稍后才能刷新看到。如何实现呢?有以下几种可行性方案: + +- **如果用户访问可能会被修改的内容,从主节点读取; 否则,在从节点读取**。这背后就要求有一些方法在实际执行查询之前,就已经知道内容是否可能会被修改。例如,社交网络上的用户首页信息通常只能由所有者编辑,而其他人无法编辑。因此,这就形成一个简单的规则:总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件。 +- **如果应用的大部分内容都可能被所有用户修改**,那么上述方法将不太有效,它会导致大部分内容都必须经由主节点,这就丧失了读操作的扩展性。此时需要其他方案来判断是否从主节点读取。例如,跟踪最近更新的时间,如果更新后一分钟之内,则总是在主节点读取;井监控从节点的复制滞后程度,避免从那些滞后时间超过一分钟的从节点读取。 +- 客户端还可以记住最近更新时的时间戳,井附带在读请求中,据此信息,系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理,要么等待直到副本接收到了最近的更新。时间戳可以是逻辑时间戳(例如用来指示写入顺序的日志序列号)或实际系统时钟(在这种情况下,时钟同步又称为一个关键点)。 +- 如果副本分布在多数据中心(例如考虑与用户的地理接近,以及高可用性),情况会更复杂些。必须先把请求路由到主节点所在的数据中心(该数据中心可能离用户很远)。 + +如果同一用户可能会从多个设备访问数据,情况会更加复杂。此时,要提供跨设备的写后读一致性,即如果用户在某设备上输入了一些信息然后在另一台设备商查看,也应该看到刚刚所输入的内容。在这种情况下,还有一些需要考虑的问题: + +- 记住用户上次更新时间戳的方法实现起来会比较困难,因为在一台设备上运行的代码完全无法知道在其他设备上发生了什么。此时,元数据必须做到全局共享。 +- 如果副本分布在多数据中心,无法保证来自不同设备的连接经过路由之后都到达同一个数据中心。例如,用户的台式计算机使用了家庭宽带连接,而移动设备则使用蜂窝数据网络,不同设备的网络连接线路可能完全不同。如果方案要求必须从主节点读取,则首先需要想办毡确保将来自不同设备的请求路由到同一个数据中心。 + +### 单调读 + +假定用户从不同副本进行了多次读取,如图所示,用户刷新一个网页,读请求可能被随机路由到某个从节点。用户 2345 先后在两个从节点上执行了两次完全相同的查询(先是少量滞后的节点,然后是滞后很大的从节点),则很有可能出现以下情况。第一个查询返回了最近用户 1234 所添加的评论,但第二个查询因为滞后的原因,还没有收到更新因而返回结果是空。用户看到了最新内容之后又读到了过期的内容,好像时间被回拨,此时需要单调读一致性。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303093658.png) + +单调读一致性可以确保不会发生这种异常。这是一个比强一致性弱,但比最终一致性强的保证。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回攘现象,即在读取较新值之后又发生读旧值的情况。 + +实现单调读的一种方式是,确保每个用户总是从固定的同一副本执行读取(而不同的用户可以从不同的副本读取)。例如,基于用户 ID 的哈希的方怯而不是随机选择副本。但如果该副本发生失效,则用户的查询必须重新路由到另一个副本。 + +### 前缀一致读 + +前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序。 + +如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生这种反常。然而,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序。这就导致当用户从数据库中读数据时,可能会看到数据库的某部分旧值和另一部分新值。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220613071613.png) + +一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但该方案真实实现效率会大打折扣。现在有一些新的算法来显式地追踪事件因果关系。 + +### 复制滞后的解决方案 + +使用最终一致性系统时,最好事先就思考这样的问题:如果复制延迟增加到几分钟甚至几小时,那么应用层的行为会是什么样子?如果这种情况不可接受,那么在设计系统肘,就要考虑提供一个更强的一致性保证,比如写后读; 如果系统设计时假定是同步复制,但最终它事实上成为了异步复制,就可能会导致灾难性后果。 + +在应用层可以提供比底层数据库更强有力的保证。例如只在主节点上进行特定类型的读取,而代价则是,应用层代码中处理这些问题通常会非常复杂,且容易出错。 + +如果应用程序开发人员不必担心这么多底层的复制问题,而是假定数据库在“做正确的事情”,情况就变得很简单。而这也是事务存在的原因,事务是数据库提供更强保证的一种方式。 + +单节点上支持事务已经非常成熟。然而,在转向分布式数据库(即支持复制和分区)的过程中,有许多系统却选择放弃支持事务,并声称事务在性能与可用性方面代价过高,所以选择了最终一致性。 + +## 多主复制 + +主从复制方法较为常见,但存在一个明显的缺点:系统只有一个主节点,而所有写入都必须经由主节点。如果由于某种原因,例如与主节点之间的网络中断而导致主节点无法连接,主从复制方案就会影响所有的写入操作。 + +对主从复制模型进行自然的扩展,则可以配置多个主节点,每个主节点都可以接受写操作,后面复制的流程类似:处理写的每个主节点都必须将该数据更改转发到所有其他节点。这就是多主节点( 也称为主-主,或主动/主动)复制。此时,每个主节点还同时扮演其他主节点的从节点。 + +### 适用场景 + +在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。 + +但是,以下场景这种配置则是合理的: + +- 多数据中心 +- 离线客户端操作 +- 协作编辑 + +#### 多数据中心 + +为了容忍整个数据中心级别故障或者更接近用户,可以把数据库的副本横跨多个数据中心。而如果使用常规的基于主从的复制模型,主节点势必只能放在其中的某一个数据中心,而所有写请求都必须经过该数据中心。 + +有了多主节点复制模型,则可以在每个数据中心都配置主节点。在每个数据中心内,采用常规的主从复制方案;而在数据中心之间,由各个数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122221705.png) + +部署单主节点的主从复制方案与多主复制方案之间的差异 + +- **性能**:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,井基本偏离了采用多数据中心的初衷(即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。因此,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。 +- **容忍数据中心失效**:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。 +- **容忍网络问题**:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络可靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。 + +有些数据库己内嵌支持了多主复制,但有些则借助外部工具来实现,例如 MySQL 的 Tungsten Replicator,PostgreSQL BDR 以及 Oracle GoldenGate。 + +多主复制的缺点:不同的数据中心可能会同时修改相同的数据,因而必须解决潜在的写冲突。 + +#### 离线客户端操作 + +另一种多主复制比较适合的场景是,应用在与网络断开后还需要继续工作。在离线状态下进行的任何更改,会在设备下次上线时,与服务器一级其他设备同步。 + +这种情况下,每个设备都有一个充当主节点的本地数据库(用来接受写请求),然后在所有设备之间采用异步方式同步这些多主节点上的副本,同步滞后可能是几小时或者数天,具体时间取决于设备何时可以再次联网。 + +从架构层面来看,上述设置基本上等同于数据中心之间的多主复制,只不过是个极端情况,即一个设备就是数据中心,而且它们之间的网络连接非常不可靠。多个设备同步日历的例子表明,多主节点可以得到想要的结果,但中间过程依然有很多的未知数。 + +有一些工具可以使多主配置更为容易,如 CouchDB 就是为这种操作模式而设计的。 + +#### 协作编辑 + +实时协作编辑应用程序允许多个用户同时编辑文档。例如,Etherpad 和 Google Docs 允许多人同时编辑文本文档或电子表格。 + +我们通常不会将协作编辑完全等价于数据库复制问题,但二者确实有很多相似之处。当一个用户编辑文档时· ,所做的更改会立即应用到本地副本( Web 浏览器或客户端应用程序),然后异步复制到服务器以及编辑同一文档的其他用户。 + +如果要确保不会发生编辑冲突,则应用程序必须先将文档锁定,然后才能对其进行编辑。如果另一个用户想要编辑同一个文档,首先必须等到第一个用户提交修改并释放锁。这种协作模式相当于主从复制模型下在主节点上执行事务操作。 + +为了加快协作编辑的效率,可编辑的粒度需要非常小。例如,单个按键甚至是全程无锁。然而另一方面,也会面临所有多主复制都存在的挑战,即如何解决冲突。 + +### 处理写冲突 + +多主复制的最大问题是可能发生写冲突。 + +例如,两个用户同时编辑 Wiki 页面,如图所示。用户 1 将页面的标题从 A 更改为 B,与此同时用户 2 将标题从 A 更改为 C。每个用户的更改都成功地提交到本地主节点。但是,当更改被异步复制到对方时,会发现存在冲突。注意:正常情况下的主从复制不会出现这种情况。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220613072848.png) + +#### 同步与异步冲突检测 + +如果是主从复制数据库,第二个写请求要么会被阻塞直到第一个写完成,要么被中止(用户必须重试) 。然而在多主节点的复制模型下,这两个写请求都是成功的,井且只能在稍后的时间点上才能异步检测到冲突,那时再要求用户层来解决冲突为时已晚。 + +理论上,也可以做到同步冲突检测,即等待写请求完成对所有副本的同步,然后再通知用户写入成功。但是,这样做将会失去多主节点的主要优势:允许每个主节点独立接受写请求。如果确实想要同步方式冲突检测,或许应该考虑采用单主节点的主从复制模型。 + +#### 避免冲突 + +处理冲突最理想的策略是避免发生冲突,即如果应用层可以保证对特定记录的写请求总是通过同一个主节点,这样就不会发生写冲突。现实中,由于不少多主节点复制模型所实现的冲突解决方案存在瑕疵,因此,避免冲突反而成为大家普遍推荐的首选方案。 + +但是,有时可能需要改变事先指定的主节点,例如由于该数据中心发生故障,不得不将流量重新路由到其他数据中心,或者是因为用户已经漫游到另一个位置,因而更靠近新数据中心。此时,冲突避免方式不再有效,必须有措施来处理同时写入冲突的可能性。 + +#### 收敛于一致状态 + +对于主从复制模型,数据更新符合顺序性原则,即如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。 + +对于多主节点复制模型,由于不存在这样的写入顺序,所以最终值也会变得不确定。 + +实现收敛的冲突解决有以下可能的方式: + +- 给每个写入分配唯一的 ID ,例如,一个时间戳,一个足够长的随机数,一个 UUID 或者一个基于键-值的哈希,挑选最高 ID 的写入作为胜利者,并将其他写入丢弃。如果基于时间戳,这种技术被称为最后写入者获胜。虽然这种方法很流行,但是很容易造成数据丢失。 +- 为每个副本分配一个唯一的 ID ,井制定规则,例如序号高的副本写入始终优先于序号低的副本。这种方法也可能会导致数据丢失。 +- 以某种方式将这些值合并在一起。例如,按字母顺序排序,然后拼接在一起。 +- 利用预定义好的格式来记录和保留冲突相关的所有信息,然后依靠应用层的逻辑,事后解决冲突(可能会提示用户) 。 + +#### 自定义冲突解决逻辑 + +解决冲突最合适的方式可能还是依靠应用层,所以大多数多主节点复制模型都有工具来让用户编写应用代码来解决冲突。可以在写入时或在读取时执行这些代码逻辑: + +- **在写入时执行**:只要数据库系统在复制变更日志时检测到冲突,就会调用应用层的冲突处理程序。 +- **在读取时执行**:当检测到冲突时,所有冲突写入值都会暂时保存下来。下一次读取数据时,会将数据的多个版本读返回给应用层。应用层可能会提示用户或自动解决冲突,井将最后的结果返回到数据库。 + +注意,冲突解决通常用于单个行或文档,而不是整个事务。因此,如果有一个原子事务包含多个不同写请求,每个写请求仍然是分开考虑来解决冲突。 + +### 拓扑结构 + +如果存在两个以上的主节点,则存在多种可能的复制拓扑结构。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122222814.png) + +最常见的拓扑结构是全部-至-全部,每个主节点将其写入同步到其他所有主节点。而其他一些拓扑结构也有普遍使用,例如,默认情况下 MySQL 只支持环形拓扑结构,其中的每个节点接收来自前序节点的写入,并将这些写入(加上自己的写入)转发给后序节点。另一种流行的拓扑是星形结构:一个指定的根节点将写入转发给所有其他节点。星形拓扑还可以推广到树状结构。 + +在环形和星形拓扑中,写请求需要通过多个节点才能到达所有的副本,即中间节点需要转发从其他节点收到的数据变更。为防止无限循环,**每个节点需要赋予一个唯一的标识符,在复制日志中的每个写请求都标记了已通过的节点标识符。如果某个节点收到了包含自身标识符的数据更改,表明该请求已经被处理过,因此会忽略此变更请求,避免重复转发**。 + +**环形和星形拓扑的问题是,如果某一个节点发生了故障,在修复之前,会影响其他节点之间复制日志的转发。**可以采用重新配置拓扑结构的方法暂时排除掉故障节点。在大多数部署中,这种重新配置必须手动完成。而对于链接更密集的拓扑(如全部到全部),消息可以沿着不同的路径传播,避免了单点故障,因而有更好的容错性。 + +但另一方面,全链接拓扑也存在一些自身的问题。主要是存在某些网络链路比其他链路更快的情况(例如由于不同网络拥塞),从而导致复制日志之间的覆盖。 + +如下图所示,客户端 A 向主节点 1 的表中首先插入一行,然后客户端 B 在主节点 3 上对行记录进行更新。而在主节点 2 上,由于网络原因可能出现意外的写日志复制顺序,例如它先接收到了主节点 3 的更新日志,之后才接收到主节点 1 的插入日志。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122222242.png) + +这里涉及到一个因果关系问题,类似于在前面“前缀一致读”所看到的:更新操作一定是依赖于先前完成的插入,因此我们要确保所有节点上一定先接收插入日志,然后再处理更新。在每笔写日志里简单地添加时间戳还不够,主要因为无法确保时钟完全同步,因而无法在主节点 2 上正确地排序所收到日志。 + +为了使得日志消息正确有序,可以使用一种称为版本向量的技术,稍后将讨论这种技术(参见“检测并发写入”)。需要指出,冲突检测技术在许多多主节点复制系统中的实现还不够完善。 + +## 无主复制 + +单主节点和多主节点复制,都是基于这样一种核心思路,即客户端先向某个节点(主节点)发送写请求,然后数据库系统负责将写请求复制到其他副本。由主节点决定写操作的顺序,从节点按照相同的顺序来应用主节点所发送的写日志。 + +一些数据存储系统则采用了不同的设计思路:选择放弃主节点,允许任何副本直接接受来自客户端的写请求。对于某些无主节点系统实现,客户端直接将其写请求发送到多副本,而在其他一些实现中,由一个协调者节点代表客户端进行写人,但与主节点的数据库不同,协调者井不负责写入顺序的维护。 + +### 节点失效时写入数据库 + +假设一个三副本数据库,其中一个副本当前不可用。在基于主节点复制模型下,如果要继续处理写操作,则需要执行切换操作。 + +对于无主节点配置,则不存在这样的切换操作。用户将写请求并行发送到三个副本,有两个可用副本接受写请求,而不可用的副本无法处理该写请求。如果假定三个副本中有两个成功确认写操作,用户收到两个确认的回复之后,即可认为写入成功。客户完全可以忽略其中一个副本无法写入的情况。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122222783.png) + +失效的节点之后重新上线,而客户端又开始从中读取内容。由于节点失效期间发生的任何写入在该节点上都尚未同步,因此读取可能会得到过期的数据。 + +为了解决这个问题,当一个客户端从数据库中读取数据时,它不是向一个副本发送请求,而是并行地发送到多个副本。客户端可能会得到不同节点的不同响应,包括某些节点的新值和某些节点的旧值。可以采用版本号技术确定哪个值更新(参见后面的“检测并发写入”)。 + +#### 读修复与反熵 + +复制模型应确保所有数据最终复制到所有的副本。当一个失效的节点重新上线之后,如何赶上中间错过的那些写请求呢? + +有以下两种机制: + +- **读修复** - 当客户端井行读取多个副本时,可以检测到过期的返回值。然后将新值写入到过期的副本中。这种方法主要适合那些被频繁读取的场景。 +- **反熵** - 利用后台进程不断查找副本间的数据差异,将任何缺少的数据从一个副本复制到另一个副本。与基于主节点复制的复制日志不同,反熵过程并不保证以特定的顺序复制写入,并且会引入明显的同步滞后。 + +#### 读写 quorum + +我们知道,成功的写操作要求三个副本中至少两个完成,这意味着至多有一个副本可能包含旧值。因此,在读取时需要至少向两个副本发起读请求,通过版本号可以确定一定至少有一个包含新值。如果第三个副本出现停机或响应缓慢,则读取仍可以继续并返回最新值。 + +把上述道理推广到一般情况,**如果有 n 个副本,写入需要 w 个节点确认,读取必须至少查询 r 个节点,则只要 w + r > n,读取的节点中一定会包含最新值**。例如在前面的例子中,n = 3,w = 2,r = 2。满足上述这些 r、w 值的读/写操作称之为法定票数读或法定票数写。也可以认为 r 和 w 是用于判定读、写是否有效的最低票数。 + +参数 n、w 和 r 通常是可配置的,一个常见的选择是设置 n 为某奇数,w = r = (n + 1) / 2(向上舍入)。也可以根据自己的需求灵活调整这些配置。例如,对于读多写少的负载,设置 w = n 和 r = 1 比较合适,这样读取速度更快,但是一个失效的节点就会使得数据库所有写入因无法完成 quorum 而失败。 + +### quorum 一致性的局限性 + +通常,设定 r 和 w 为简单多数(多于 n / 2)节点,即可确保 w + r > n,且同时容忍多达 n / 2 个节点故障。但是,**quorum 不一定非得是多数,读和写的节点集中有一个重叠的节点才是最关键的**。 + +也可以将 w 和 r 设置为较小的数字,从而让 w + r <= n。此时,读取和写入操作仍会被发送到 n 个节点,但只需等待更少的节点回应即可返回。 + +由于 w 和 r 配置的节点数较小,读取请求当中可能恰好没有包含新值的节点,因此最终可能会返回一个过期的旧值。好的一方面是,这种配置可以获得更低的延迟和更高的可用性,例如网络中断,许多副本变得无法访问,相比而言有更高的概率继续处理读取和写入。只有当可用的副本数已经低于 w 或 r 时,数据库才会变得无法读/写,即处于不可用状态。 + +即使在 w + r > n 的情况下,也可能存在返回旧值的边界条件。这主要取决于具体实现,可能的情况包括: + +- 如果采用了 sloppy quorum(参阅后面的“宽松的 quorum 与数据回传”),写操作的 w 节点和读取的 r 节点可能完全不同,因此无法保证读写请求一定存在重叠的节点。 +- 如果两个写操作同时发生,则无法明确先后顺序。这种情况下,唯一安全的解决方案是合并并发写入(参见前面的“处理写冲突”)。如果根据时间戳挑选胜者,则由于时钟偏差问题,某些写入可能会被错误地抛弃。 +- 如果写操作与读操作同时发生,写操作可能仅在一部分副本上完成。此时,读取时返回旧值还是新值存在不确定性。 +- 如果某些副本上已经写入成功,而其他一些副本发生写入失败(例如磁盘已满),且总的成功副本数少于 w,那些已成功的副本上不会做回滚。这意味着尽管这样的写操作被视为失败,后续的读操作仍可能返回新值。 +- 如果具有新值的节点后来发生失效,但恢复数据来自某个旧值,则总的新值副本数会低于 w,这就打破了之前的判定条件。 +- 即使一切工作正常,也会出现一些边界情况,如一致性与共识中所介绍的“可线性化与 quorum”。 + +建议最好不要把参数 w 和 r 视为绝对的保证,而是一种灵活可调的读取新值的概率。 + +这里通常无法得到前面的“复制滞后问题”中所罗列的一致性保证,包括写后读、单调读、前缀一致读等,因此前面讨论种种异常同样会发生在这里。如果确实需要更强的保证,需要考虑事务与共识问题。 + +#### 宽松的 quorum 与数据回传 + +quorum 并不总如期待的那样提供高容错能力。一个网络中断可以很容易切断一个客户端到多数数据库节点的连接。尽管这些集群节点是活着的,而且其他客户端也确实可以正常连接,但是对于断掉连接的客户端来讲,情况无疑等价于集群整体失效。这种情况下,很可能无法满足最低的 w 和 r 所要求的节点数,因此导致客户端无法满足 quorum 要求。 + +在一个大规模集群中(节点数远大于 n 个),客户可能在网络中断期间还能连接到某些数据库节点,但这些节点又不是能够满足数据仲裁的那些节点。此时,我们是否应该接受该写请求,只是将它们暂时写入一些可访问的节点中?(这些节点并不在 n 个节点集合中)。 + +这种方案称之为宽松的仲裁:写入和读取仍然需要 w 和 r 个成功的响应,但包含了那些并不在先前指定的 n 个节点。一旦网络问题得到解决,临时节点需要把接收到的写入全部发送到原始主节点上。这就是所谓的数据回传。 + +可以看出,sloppy quorum 对于提高写入可用性特别有用:要有任何 w 个节点可用,数据库就可以接受新的写入。然而这意味着,即使满足 w + r > n,也不能保证在读取某个键时,一定能读到最新值,因为新值可能被临时写入 n 之外的某些节点且尚未回传过来。 + +### 检测并发写 + +无主复制数据库允许多个客户端对相同的主键同时发起写操作,即使采用严格的 quorum 机制也可能会发生写冲突。这与多主复制类似,此外,由于读时修复或者数据回传也会导致并发写冲突。 + +一个核心问题是,由于网络延迟不稳定或者局部失效,请求在不同的节点上可能会呈现不同的顺序。如图所示,对于包含三个节点的数据系统,客户端 A 和 B 同时向主键 X 发起写请求: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122225446.png) + +- 节点 1 收到来自客户端 A 的写请求,但由于节点失效,没有收到客户端 B 的写请求。 +- 节点 2 首先收到 A 的写请求,然后是 B 的写请求。 +- 节点 3 首先收到 B 的写请求,然后是 A 的写请求。 + +如果节点每当收到新的写请求时就简单地覆盖原有的主键,那么这些节点将永久无法达成一致。我们知道副本应该收敛于相同的内容,这样才能达成最终一致。但如何才能做到呢?如果不想丢失数据,必须了解很多关于数据库内部冲突处理的机制。 + +我们已经在前面的“处理写冲突”简要介绍了一些解决冲突的技巧,现在我们来更详细地探讨这个问题。 + +#### 最后写入者获胜(丢弃并发写入) + +一种实现最终收敛的方法是,每个副本总是保存最新值,允许覆盖并丢弃旧值。那么,假定每个写请求都最终同步到所有副本,只要我们有一个明确的方法来确定哪一个写入是最新的,则副本可以最终收敛到相同的值。 + +这个想法其实有些争议,关键点在于前面所提到关于如何定义“最新”。不过即使无法确定写请求的“自然顺序”,我们可以强制对其排序。例如,为每个写请求附加一个时间戳,然后选择最新即最大的时间戳,丢弃较早时间戳的写入。这个冲突解决算法被称为最后写入者获胜(last write wins,LWW)。 + +LWW 可以实现最终收敛的目标,但是以牺牲数据持久性为代价。如果同一个主键有多个并发写,即使这些并发写都向客户端报告成功,但最后只有一个写入值会存活下来,其他的将被系统默默丢弃。在一些场景如缓存系统,覆盖写是可以接受的。如果覆盖、丢失数据不可接受,则 LWW 并不是解决冲突很好的选择。 + +要确保 LWW 安全无副作用的唯一方法是,只写入一次然后写入值视为不可变,这样就避免了对同一个主键的并发写。例如,Cassandra 的一个推荐使用方法就是采用 UUID 作为主键,这样每个写操作都针对的不同的、系统唯一的主键。 + +#### Happens-before 关系和并发 + +如果 B 知道 A,或者依赖于 A,或者以某种方式在 A 基础上构建,则称操作 A 在操作 B 之前发生。这是定义何为并发的关键。事实上,我们也可以简单地说,如果两个操作都不在另一个之前发生,那么操作是并发的。 + +因此,对于两个操作 A 和 B,一共存在三种可能性,我们需要的是一个算法来判定两个操作是否并发。如果一个操作发生在另一个操作之前,则后面的操作可以覆盖较早的操作。如果属于并发,就需要解决潜在的冲突问题。 + +#### 确定前后关系 + +我们来看一个确定操作并发性的算法,即两个操作究竟属于并发还是一个发生在另一个之前。简单起见,我们先从只有一个副本的数据库开始。 + +下图的例子是两个客户端同时向购物车添加商品。初始时购物车为空。然后两个客户端向数据库共发出五次写入操作: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122224778.png) + +1. 客户端 1 首先将牛奶加入购物车。这是第一次写入该主键的值,服务器保存成功然后分配版本 1,服务器将值与版本号一起返回给该客户端 1。 +2. 客户端 2 将鸡蛋加入购物车,此时它并不知道客户端 1 已添加了牛奶,而是认为鸡蛋是购物车中的唯一物品。服务器为此写入并分配版本 2,然后将鸡蛋和牛奶存储为两个单独的值,最后将这两个值与版本号 2 返回给客户端 2。 +3. 客户端 1 也并不意识上述步骤 2,想要将面粉加入购物车,且以为购物车的内容应该是[牛奶,面粉],将此值与版本号 1 一起发送到服务器。服务器可以从版本号中知道[牛奶,面粉]的新值要取代先前值[牛奶],但值[鸡蛋]则是新的并发操作。因此,服务器将版本 3 分配给[牛奶,面粉]并覆盖版本 1 的[牛奶],同时保留版本 2 的值[鸡蛋],将二者同时返回给客户端 1。 +4. 客户端 2 想要加入火腿,也不知道客户端 1 刚刚加了面粉。其在最后一个响应中从服务器收到的两个值是[牛奶]和[蛋],现在合并这些值,并添加火腿形成一个新的值[鸡蛋,牛奶,火腿]。它将该值与前一个版本号 2 一起发送到服务器。服务器检测到版本 2 会覆盖[鸡蛋],但与[牛奶,面粉]是同时发生,所以设置为版本 4 并将所有这些值发送给客户端 2。 +5. 最后,客户端 1 想要加培根。它以前在版本 3 中从服务器接收[牛奶,面粉]和[鸡蛋],所以合并这些值,添加培根,并将最终值[牛奶,面粉,鸡蛋,培根]连同版本号 3 来覆盖[牛奶,面粉],但与[鸡蛋,牛奶,火腿]并发,所以服务器会保留这些并发值。 + +上面操作之间的数据流可以通过下图展示。箭头表示某个操作发生在另一个操作之前,即后面的操作“知道”或是“依赖”于前面的操作。在这个例子中,因为总有另一个操作同时进行,所以每个客户端都没有时时刻刻和服务器上的数据保持同步。但是,新版本值最终会覆盖旧值,且不会发生已写入值的丢失。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202405122229341.png) + +服务器判断操作是否并发的依据主要依靠对比版本号,而并不需要解释新旧值本身。算法的工作流程如下: + +- 服务器为每个主键维护一个版本号,每当主键新值写入时递增版本号,并将新版本号与写入的值一起保存。 +- 当客户端读取主键时,服务器将返回所有(未被覆盖的)当前值以及最新的版本号。且要求写之前,客户必须先发送读请求。 +- 客户端写主键,写请求必须包含之前读到的版本号、读到的值和新值合并后的集合。写请求的响应可以像读操作一样,会返回所有当前值,这样就可以像购物车例子那样一步步链接起多个写入的值。 +- 当服务器收到带有特定版本号的写入时,覆盖该版本号或更低版本的所有值(因为知道这些值已经被合并到新传入的值集合中),但必须保存更高版本号的所有值(因为这些值与当前的写操作属于并发)。 + +#### 合并同时写入的值 + +一个简单的合并方法是基于版本号或时间戳来选择最后一个值,但这意味着会丢失部分数据。所以,需要在程序中额外做一些工作。在应用代码中合并非常复杂且容易出错,因此可以设计一些专门的数据结构来自动执行合并。例如,Riak 支持成为 CRDT 一系列数据结构,以合理的方式高效自动合并,包括支持删除标记。 + +#### 版本矢量 + +使用单个版本号来捕获操作间的依赖关系,当多个副本同时接受写入时,这是不够的。因此我们需要为每个副本和每个主键均定义一个版本号。每个副本在处理写入时增加自己的版本号,并且跟踪从其他副本看到的版本号。通过这些信息来指示要覆盖哪些值,该保留哪些并发值。 + +所有副本的版本号集合成为版本矢量。 + +## 参考资料 + +- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\351\224\201.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\351\224\201.md" new file mode 100644 index 0000000000..16a7410510 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\345\210\206\345\270\203\345\274\217\351\224\201.md" @@ -0,0 +1,511 @@ +--- +title: 分布式锁 +date: 2019-06-04 23:42:00 +categories: + - 分布式 + - 分布式协同 + - 分布式协同综合 +tags: + - 分布式 + - 协同 + - 锁 +permalink: /pages/0eb5a899/ +--- + +# 分布式锁 + +## 什么是分布式锁 + +在计算机科学中,**锁是在并发场景下用于强行限制资源访问的一种同步机制**,即用于在并发控制中通过互斥手段来保证数据同步安全。 + +在 Java 进程中,可以使用 Lock、synchronized 等来支持并发锁。如果是同一台机器的不同进程,想要同时操作一个共享资源(例如修改同一个文件),可以使用操作系统提供的「文件锁」或「信号量」来做互斥。这些发生在同一台机器上的互斥操作,可以称为**本地锁**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190814629.png) + +本地锁无法协同不同机器间的互斥操作。为了解决这个问题,需要引入分布式锁。 + +**分布式锁**,顾名思义,应用于分布式场景下,它和单进程中的锁并没有本质上的不同,只是控制对象由一个进程中的多个线程变成了多个进程中的多个线程。此外,临界区的资源也由进程内共享资源变成了分布式系统内部共享资源。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190815373.png) + +分布式锁典型应用场景是: + +- **选举 Leader** - 分布式锁可用于确保:在任何指定时间内,只有一个节点成为领导者。 +- **任务调度** - 在分布式任务调度器中,分布式锁确保一个调度任务仅由一个 worker 节点执行,从而防止重复执行。 +- **资源配置** - 在管理共享资源(如文件系统、网络 Socket 或硬件设备)时,分布式锁可确保一次只有一个进程可以访问资源。 +- **微服务协调** - 当多个微服务需要执行协同操作时,例如更新不同数据库中的相关数据,分布式锁可以确保这些操作以可控和有序的方式执行。 +- **库存管理** - 在电商系统中,分布式锁可以管理库存更新,以确保当多个用户尝试同时购买相同商品时,正确增减库存,防止超卖。 +- **会话管理** - 在分布式环境中处理用户会话时,分布式锁可以确保用户会话一次只能由一个服务器修改,从而防止不一致。 + +![](https://substackcdn.com/image/fetch/w_1456,c_limit,f_auto,q_auto:good,fl_lossy/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F9d97726d-333d-434e-b00d-7e1d56c19def_1329x1536.gif) + +图来自:https://blog.bytebytego.com/i/149472287/why-do-we-need-to-use-a-distributed-lock + +## 分布式锁的设计目标 + +分布式锁的解决方案大致有以下几种: + +- 基于数据库实现 +- 基于缓存(Redis,Memcached 等)实现 +- 基于 Zookeeper 实现 + +分布式锁的实现要点大同小异,仅在实现细节上有所不同。 + +### 互斥 + +**分布式锁必须是独一无二的**,表现形式为:向数据存储插入一个唯一的 key,一旦有一个线程插入这个 key,其他线程就不能再插入了。 + +- 保证 key 唯一性的最简单的方式是使用 UUID。 +- 此外,可以参考 Snowflake ID(雪花算法),将机器地址(IP 地址、机器 ID、MAC 地址)、Jvm 进程 ID(应用 ID、服务 ID)、时间戳等关键信息拼接起来作为唯一标识。 +- 应用自行保证 + +### 避免死锁 + +在分布式锁的场景中,部分失败和异步网络这两个问题是同时存在的。如果一个进程获得了锁,但是这个进程与锁服务之间的网络出现了问题,导致无法通信,那么这个情况下,如果锁服务让它一直持有锁,就会导致死锁的发生。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190816412.png) + +常见的解决思路是引入**超时机制**,即成功申请锁后,超过一定时间,锁失效(删除 key)。这样就不会出现锁一直不释放,导致其他线程无法获取锁的情况。Redis 分布式锁就采用了这种思路。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412190816042.png) + +超时机制解锁了死锁问题,但又引入了一个新问题:如果应用加锁时,对于操作共享资源的时长估计不足,可能会出现:操作尚未执行完,但是锁没了的尴尬情况。为了解决这个问题,需要引入**锁续期**机制:当持有锁的线程尚未执行完操作前,不断周期性检测锁的超时时间,一旦发现快要过期,就自动为锁续期。 + +ZooKeeper 分布式锁避免死锁采用了另外一种思路。ZooKeeper 的存储单元叫 znode,它是以文件层级形式组织,天然就存在物理空间隔离。并且 ZooKeeper 支持临时节点 + Watch 机制,可以在客户端断连时主动删除临时节点,所以不存在死锁问题。 + +### 可重入 + +**可重入**指的是:**同一个线程在没有释放锁之前,能否再次获得该锁**。其实现方案是:只需在加锁的时候,**记录好当前获取锁的节点 + 线程组合的唯一标识**,然后在后续的加锁请求时,如果当前请求的节点 + 线程的唯一标识和当前持有锁的相同,那么就直接返回加锁成功;如果不相同,则按正常加锁流程处理。 + +### 公平性 + +当多个线程请求同一锁时,它们必须按照请求的顺序来获取锁,即先来先得的原则。锁的公平性的实现也非常简单,对于被阻塞的加锁请求,我们只要先记录好它们的顺序,在锁被释放后,按顺序颁发就可以了。 + +### 重试 + +有时候,加锁失败可能只是由于网络波动、请求超时等原因,稍候就可以成功获取锁。为了应对这种情况,加锁操作需要支持重试机制。常见的做法是,设置一个加锁超时时间,在该时间范围内,不断自旋重试加锁操作,超时后再判定加锁失败。 + +### 容错 + +分布式锁若存储在单一节点,一旦该节点宕机或失联,就会导致锁失效。将分布式锁存储在多数据库实例中,加锁时并发写入 `N` 个节点,只要 `N / 2 + 1` 个节点写入成功即视为加锁成功。 + +## 数据库分布式锁 + +### 数据库分布式锁原理 + +基于数据库实现分布式锁的思路是:维护一张锁记录表,为用于标识分布式锁的字段增加**唯一性约束**。利用唯一性约束的互斥性,当且仅当成功插入记录,即表示加锁成功。 + +(1)创建锁表 + +```sql +CREATE TABLE `distributed_lock` ( + `id` BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主键', + `resource` VARCHAR(64) NOT NULL DEFAULT '' COMMENT '资源', + `count` INT(10) UNSIGNED NOT NULL DEFAULT '0' COMMENT '锁次数,统计可重入锁', + `desc` TEXT DEFAULT NULL COMMENT '备注', + `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', + `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', + PRIMARY KEY (`id`), + UNIQUE KEY `uniq_resource`(`resource`) +) + ENGINE = InnoDB DEFAULT CHARSET = `utf8mb4`; +``` + +(2)获取锁 + +想要锁住某个方法时,执行以下 SQL: + +```sql +insert into methodLock(method_name,desc) values (‘method_name’,‘desc’) +``` + +因为我们对 `method_name` 做了唯一性约束,这里如果有多个请求同时提交到数据库的话,数据库会保证只有一个操作可以成功,那么我们就可以认为操作成功的那个线程获得了该方法的锁,可以执行方法体内容。 + +成功插入则获取锁。 + +(3)释放锁 + +当方法执行完毕之后,想要释放锁的话,需要执行以下 Sql: + +```sql +delete from methodLock where method_name ='method_name' +``` + +### 数据库分布式锁小结 + +数据库分布式锁的**问题**: + +- **死锁**:一旦释放锁操作失败,或持有锁的机器宕机、断连,就会导致锁记录一直存在,其他线程无法再获得锁。解决办法:为锁增加失效时间字段,启动一个定时任务,隔一段时间清除一次过期的数据。 +- **非阻塞**:因为 `insert` 操作一旦失败就会报错,因此未获得锁的线程并不会进入排队队列,要想获得锁就要再次触发加锁操作。解决办法:循环重试,直到插入成功,这么做会产生一定额外开销。 +- **非重入**:同一个线程在没有释放锁之前无法再次获得该锁。因为数据中数据已经存在了。解决办法:在数据库表中加个字段,记录当前获得锁的节点信息和线程信息,那么下次再获取锁的时候先查询数据库,如果当前机器的主机信息和线程信息在数据库可以查到的话,直接把锁分配给他就可以了。 +- **单点问题**:如果数据库是一个单点,一旦数据库挂掉,会导致业务系统不可用。解决办法:单点问题可以用多数据库实例,同时写入 `N` 个节点,`N / 2 + 1` 个成功就加锁成功。 + +数据库分布式锁的**利弊**: + +- **优点**:直接借助数据库,简单易懂。 +- **缺点**:会有各种各样的问题,在解决问题的过程中会使整个方案变得越来越复杂。此外,数据库性能易成为瓶颈。 + +## ZooKeeper 分布式锁 + +### ZooKeeper 分布式锁原理 + +ZooKeeper 分布式锁的实现基于 ZooKeeper 的两个重要特性: + +- **顺序临时节点**:ZooKeeper 的存储类似于 DNS 那样的具有层级的命名空间。ZooKeeper 节点类型可以分为持久节点(`PERSISTENT`)、临时节点(`EPHEMERAL`),每个节点还能被标记为有序性(`SEQUENTIAL`),一旦节点被标记为有序性,那么整个节点就具有顺序自增的特点。 +- **Watch 机制**:ZooKeeper 允许用户在指定节点上注册一些 `Watcher`,并且在特定事件触发的时候,ZooKeeper 服务端会将事件通知给用户。 + +下面是 ZooKeeper 分布式锁的工作流程: + +1. 创建一个目录节点,比如叫做 `/locks`; +2. 线程 A 想获取锁,就在 `/locks` 目录下创建临时顺序 zk 节点; +3. 获取 `/locks`目录下所有的子节点,检查是否存在比自己顺序更小的节点:若不存在,则说明当前线程创建的节点顺序最小,获取锁成功; +4. 此时,线程 B 试图获取锁,发现自己的节点顺序不是最小,设置监听锁号在自己前一位的节点; +5. 线程 A 处理完,删除自己的节点。线程 B 监听到变更事件,判断自己是不是最小的节点,如果是则获得锁。 + +[Apache Curator](https://curator.apache.org/docs/about/) 提供了基于 ZooKeeper 实现的可重入公平锁 `InterProcessMutex`,它正是采用了上面所述的工作流程。 + +:::details ZooKeeper 分布式锁实现示例 + +下面是一个简单的 `InterProcessMutex` 封装示例: + +```java +import cn.hutool.core.collection.CollectionUtil; +import lombok.extern.slf4j.Slf4j; +import org.apache.curator.framework.CuratorFramework; +import org.apache.curator.framework.recipes.locks.InterProcessMutex; + +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +@Slf4j +public class ZookeeperReentrantDistributedLock { + + /** + * 锁路径,即锁的唯一标识,对应 zk 的一个 PERSISTENT 节点,加锁时会在该节点下新建 EPHEMERAL 节点 + */ + private final String path; + + /** + * zk 的客户端 + */ + private final CuratorFramework client; + + /** + * curator 客户端提供的 zk 可重入公平锁 + */ + private final InterProcessMutex mutex; + + public ZookeeperReentrantDistributedLock(String lockId, CuratorFramework client) { + this.client = client; + this.path = "/locks/" + lockId; + this.mutex = new InterProcessMutex(this.client, this.path); + } + + public void lock() { + try { + mutex.acquire(); + System.out.println("lock success"); + } catch (Exception e) { + log.error("lock exception", e); + } + } + + public boolean tryLock(long timeout, TimeUnit unit) { + try { + boolean isOk = mutex.acquire(timeout, unit); + if (isOk) { + System.out.println("tryLock success"); + } + return isOk; + } catch (Exception e) { + log.error("tryLock exception", e); + return false; + } + } + + public void unlock() { + try { + mutex.release(); + System.out.println("unlock success"); + } catch (Throwable e) { + log.error("unlock exception", e); + } finally { + // 清除根路径 + // 生产环境中应指定线程池 + CompletableFuture.runAsync(() -> { + try { + List list = client.getChildren().forPath(path); + if (CollectionUtil.isEmpty(list)) { + client.delete().forPath(path); + } + } catch (Exception e) { + log.error("final unlock exception", e); + } + }); + } + } + +} +``` + +测试代码: + +```java +RetryPolicy retryPolicy = new ExponentialBackoffRetry(1000, 3); +CuratorFramework client = CuratorFrameworkFactory.newClient("127.0.0.1:2181", retryPolicy); +client.start(); +ZookeeperReentrantDistributedLock lock = new ZookeeperReentrantDistributedLock("订单流水号", client); +lock.lock(); +System.out.println("do something"); +lock.unlock(); +client.close(); +``` + +::: + +### ZooKeeper 分布式锁小结 + +ZooKeeper 分布式锁的**优点**是较为**可靠**: + +- **避免死锁**:ZooKeeper 通过临时节点 + 监听机制,可以保证:如果持有临时节点的线程主动解锁或断连,Zk 会自动删除临时节点,这意味着锁的释放。所以,不存在锁永久不释放从而导致死锁的问题。 +- **单点问题**:ZooKeeper 采用主从架构,并确保主从同步是强一致的,因此不会出现单点问题。 + +ZooKeeper 分布式锁的**缺点**是:加锁、解锁操作,本质上是对 ZooKeeper 的写操作,全部由 ZooKeeper 主节点负责。如果加锁、解锁的吞吐量很大,容易出现单点写入瓶颈。 + +## Redis 分布式锁 + +相比于用数据库来实现分布式锁,基于缓存实现的分布式锁的性能会更好。目前有很多成熟的分布式产品,包括 Redis、memcache、Tair 等。这里以 Redis 举例。 + +### Redis 分布式锁原理 + +#### 极简版本 + +我们先来看一下,如何实现一个极简版本的 Redis 分布式锁。 + +(1)加锁 + +Redis 中的 `setnx` 命令,表示当且仅当 key 不存在时,才会写入 key。由于其互斥性,所以可以基于此来实现分布式锁。 + +执行 `setnx key val`,若返回 1,表示写入成功,即加锁成功;若返回 0,表示该 key 已存在,写入失败,即加锁失败。 + +(2)解锁 + +Redis 分布式锁如何解锁呢? + +很简单,删除 key 就意味着释放锁,即执行 `del key` 命令。 + +#### 避免死锁 + +极简版本的解决方案有一个很大的问题:**存在死锁的可能**。持有锁的节点如果执行业务过程中出现异常或机器宕机,都可能导致无法释放锁。这种情况下,其他节点永远也无法再获取锁。 + +对于异常,在 Java 中,可以通过 `try...catch...finally` 来保证:最终一定会释放锁,其他编程语言也有相似的语法特性。 + +对于机器宕机这种情况,如何处理呢?通常的对策是:为锁加上**超时机制,过期自动删除**。 + +在 Redis 中,`expire` 命令可以为 key 设置一个超时时间,一旦过期,Redis 会自动删除 key。如此看来,`setnx` + `expire` 组合使用,就能解决死锁问题了。可惜,没那么简单。Redis 只能保证单一命令的原子性,不保证组合命令的原子性。 + +那么,Redis 中有没有一条命令可以实现 setnx + expire 的组合语义呢?还真有,可以通过下面的命令来实现: + +```bash +# 下面两条命令是等价的 +SET key val NX PX 30000 +SET key val NX EX 30 +``` + +参数说明: + +- `NX`:该参数表示当且仅当 key 不存在,才能写入成功 +- `PX`:超时时间,单位毫秒 +- `EX`:超时时间,单位秒 + +#### 超时续期 + +为了避免死锁,我们为锁添加了超时时间。但这里有一个问题,如果应用加锁时,对于操作共享资源的时长估计不足,可能会出现:操作尚未执行完,但是锁没了的尴尬情况。为了解决这个问题,很自然会想到,时间不够,就续期呗。 + +具体来说,如何续期呢?一种方案是:加锁后,启动一个定时任务,周期性检测锁是否快要过期,如果快要过期并且操作尚未结束,就对锁进行自动续期。自行实现这个方案似乎有点繁琐,好在开源 Redis 客户端 [Redisson](https://github.com/redisson/redisson) 中已经为锁的**超时续期**提供了一个成熟的机制——WatchDog(看门狗)。我们可以直接拿来主义即可。 + +#### 安全解锁 + +前文提到了,解锁的操作,实际上就是 `del key`。这里存在一个问题:因为没有任何判断,任何节点都可以随意删除 key,换句话说,锁可能会被其他节点释放。如何避免这个问题呢?解决方法就是:为锁添加**唯一性标识**来进行互斥。唯一性标识可以是 UUID,可以是雪花算法 ID 等。 + +在 Redis 分布式锁中,唯一性标识的具体实现就是在 `set key val` 时,将唯一性标识 id 作为 `val` 写入。**解锁前,先判断 key 的 value,必须和 set 时写入的 id 值保持一致,以此确认锁归属于自己**。解锁的伪代码如下: + +```java +if (redis.get("key") == id) + redis.del("key"); +``` + +这里依然存在一个问题,由于需要在 Redis 中,先 `get`,后 `del` 操作,所以无法保证操作的原子性。为了保证原子性,可以将这段伪代码用 lua 脚本来实现,这么做的理由是 Redis 中支持原子性的执行 lua 脚本。下面是安全解锁的 lua 脚本代码: + +```lua +if redis.call("get",KEYS[1]) == ARGV[1] then + return redis.call("del",KEYS[1]) +else + return 0 +end +``` + +#### 自旋重试 + +有时候,加锁失败可能只是由于网络波动、请求超时等原因,稍候就可以成功获取锁。为了应对这种情况,加锁操作需要支持重试机制。常见的做法是,设置一个加锁超时时间,在该时间范围内,不断自旋重试加锁操作,超时后再判定加锁失败。 + +下面是一个自旋重试获取锁的伪代码示例: + +```java +try { + long begin = System.currentTimeMillis(); + while (true) { + String result = jedis.set(lockKey, uniqId, "NX", "PX", expireTime); + if ("OK".equals(result)) { + // 加锁成功,执行业务操作 + return true; + } + + long time = System.currentTimeMillis() - begin; + if (time >= timeout) { + return false; + } + try { + Thread.sleep(50); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } +} catch (Exception e) { + // 异常处理 +} finally { + // 释放锁 +} +``` + +### Redis 分布式锁小结 + +在前文中,为了实现一个靠谱的 Redis 分布式锁,我们讨论了避免死锁、超时续期、安全解锁几个问题以及应对策略。但是,依然存在一些其他问题: + +- **不可重入** - 同一个线程无法多次获取同一把锁。 +- **单点问题** - Redis 主从同步存在延迟,有可能导致锁冲突。举例来说:线程一在主节点加锁,如果主节点尚未同步给从节点就发生宕机;此时,Redis 集群会选举一个从节点作为新的主节点。此时,新的主节点没有锁的数据,若有其他线程试图加锁,就可以成功获取锁,即出现同时有多个线程持有锁的情况。解决这个问题,可以使用 RedLock 算法。 + +## RedLock 分布式锁 + +RedLock 分布式锁,是 Redis 的作者 Antirez 提出的一种解决方案。 + +> 扩展:[RedLock 官方文档](https://redis.io/docs/latest/develop/use/patterns/distributed-locks/) + +### RedLock 分布式锁原理 + +RedLock 分布式锁在普通 Redis 分布式锁的基础上,进行了扩展,其要点在于: + +- (1)加锁操作不是写入单一节点,而是同时写入多个主节点,官方推荐集群中至少有 5 个主节点。 +- (2)只要半数以上的主节点写入成功,即视为加锁成功。 +- (3)大多数节点加锁的总耗时,要小于锁设置的过期时间。 +- (4)解锁时,要向所有节点发起请求。 + +下面来逐一解释以上各要点的用意: + +(1)RedLock 加锁时,为什么要同时写入多个主节点? + +这是为了避免单点问题,即使有部分实例出现异常,依然可以正常提供加锁、解锁能力。 + +(2)为什么要半数以上的主节点写入成功,才视为加锁成功? + +在分布式系统中,为了达成共识,常常采用“多数派”策略来进行决策:大多数节点认可的行为,就视为整体通过。 + +(3)为什么加锁成功后,还要计算加锁的累计耗时? + +因为操作的是多个节点,所以耗时肯定会比操作单个实例耗时更久。而且,网络情况是复杂的,可能存在延迟、丢包、超时等情况。网络请求越多,异常发生的概率就越大。所以,即使大多数节点加锁成功,但如果加锁的累计耗时已经**超过**了锁的过期时间,那此时有些实例上的锁可能已经失效了,这个锁就没有意义了。 + +(4)解锁时,为什么要向所有节点发起请求? + +因为网络环境的复杂性,可能会存在这种情况:向某主节点写入锁信息,实际写入成功,但是响应超时或丢包。 + +所以,释放锁时,不管之前有没有加锁成功,需要释放**所有节点**的锁,以保证清理节点上**残留**的锁。 + +### RedLock 分布式锁小结 + +RedLock 分布式锁的解决方案看上去考虑的面面俱到,似乎已经万无一失了,但真的是如此吗? + +分布式领域典中典著作《数据密集型应用系统设计》的作者 Martin 就曾对 RedLock 提出了质疑,他和 Redis 以及 RedLock 的作者 Antirez 掀起了一场激烈的争论。 + +> 二人的讨论文章如下,有兴趣可以看一下: +> +> - Martin 质疑 RedLock 的文章:[How to do distributed locking](https://martin.kleppmann.com/2016/02/08/how-to-do-distributed-locking.html) +> - Antirez 的辩驳文章:[Is Redlock safe?](https://antirez.com/news/101) + +Martin 的观点: + +(1)**RedLock 不能完全保证安全性** + +分布式系统会遇到三座大山:**NPC** + +- N:Network Delay,**网络延迟**; +- P:Process Pause,进程暂停(**GC**); +- C:Clock Drift,**时钟漂移**。 + +RedLock 在遇到以上情况时,不能保证安全性。 + +(2)RedLock 加锁、解锁需要处理多个节点,代价太高 + +(3)提出 fencing token 的方案,保证正确性 + +这个模型流程如下: + +- 客户端在获取锁时,锁服务可以提供一个**递增**的 token +- 客户端拿着这个 token 去操作共享资源 +- 共享资源可以根据 token 拒绝**后来者**的请求 + +Antirez 的观点: + +- 同意时钟跳跃对 Redlock 的影响,但认为时钟跳跃是可以避免的,取决于基础设施和运维。并且如果误差不大,也是可以接受的。 +- Redlock 在设计时,充分考虑了 NPC 问题,在 Redlock 步骤 3 之前出现 NPC,可以保证锁的正确性,但在步骤 3 之后发生 NPC,不止是 Redlock 有问题,其它分布式锁服务同样也有问题,所以不在讨论范畴内。 + +总结来说,**已知的分布式锁,无论采用什么解决方案,在极端情况下,都无法保证百分百的安全。** + +## Redisson 提供的分布式锁 + +[Redisson](https://github.com/redisson/redisson) 是一个流行的 Redis Java 客户端,它基于 Netty 开发,并提供了丰富的扩展功能,如:[分布式计数器](https://redisson.org/docs/data-and-services/counters/)、[分布式集合](https://redisson.org/docs/data-and-services/collections/)、[分布式锁](https://redisson.org/docs/data-and-services/locks-and-synchronizers/) 等。 + +Redisson 支持的分布式锁有多种:Lock, FairLock, MultiLock, RedLock, ReadWriteLock, Semaphore, PermitExpirableSemaphore, CountDownLatch,可以根据场景需要去选择,非常方面。一般而言,使用 Redis 分布式锁,推荐直接使用 Redisson 提供的 API,功能全面且较为可靠。 + +下面是 Redisson Lock API 的一个简单示例: + +```java +RLock lock = redisson.getLock("myLock"); + +// traditional lock method +lock.lock(); + +// or acquire lock and automatically unlock it after 10 seconds +lock.lock(10, TimeUnit.SECONDS); + +// or wait for lock aquisition up to 100 seconds +// and automatically unlock it after 10 seconds +boolean res = lock.tryLock(100, 10, TimeUnit.SECONDS); +if (res) { + try { + ... + } finally { + lock.unlock(); + } +} +``` + +## 分布式锁技术选型 + +下面是主流分布式锁技术方案的对比,可以在技术选型时作为参考: + +| | 数据库分布式锁 | Redis 分布式锁 | ZooKeeper 分布式锁 | +| -------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | +| 方案要点 | 1. 维护一张锁表,为锁的唯一标识字段添加唯一性约束。
    2. 只要 insert 成功,即视为加锁成功。 | `set lockKey randomValue NX PX/EX time` 当且仅当 key 不存在时才可以写入,并且设定超时时间,以避免死锁。 | 加锁本质上是在 zk 中指定目录创建**顺序临时接节点**,序号最小即加锁成功。节点删除时,有监听通知机制告知申请锁的线程。 | +| 方案难度 | 实现简单、易于理解 | 较为简单,但要使其更可靠,需要有一些完善策略 | 应用简单,但 zk 内部机制并不简单 | +| 性能 | 性能最差,易成为瓶颈 | 性能最高 | 性能弱于 Redis | +| 可靠性 | 有锁表的风险 | 较为可靠(需要一些完善策略) | 可靠性最高 | +| 适用场景 | 一般不采用 | 适用于高并发的场景 | 适用于要求可靠,但并发量不高的场景 | +| 开源实现 | 无 | [Redisson](https://github.com/redisson/redisson) | [Apache Curator](https://curator.apache.org/docs/about/) | + +## 参考资料 + +- [分布式锁实现汇总](https://juejin.im/post/5a20cd8bf265da43163cdd9a) +- [分布式锁实现原理与最佳实践 - 阿里云开发者](https://mp.weixin.qq.com/s/JzCHpIOiFVmBoAko58ZuGw) +- [聊聊分布式锁 - 字节跳动技术团队](https://mp.weixin.qq.com/s/-N4x6EkxwAYDGdJhwvmZLw) +- [Redis、ZooKeeper、Etcd,谁有最好用的分布式锁? - 腾讯云开发者](https://mp.weixin.qq.com/s/yZC6VJGxt1ANZkn0SljZBg) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/13.\345\210\206\345\270\203\345\274\217\351\253\230\345\217\257\347\224\250/02.\346\234\215\345\212\241\345\256\271\351\224\231.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\346\234\215\345\212\241\345\256\271\351\224\231.md" similarity index 98% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/13.\345\210\206\345\270\203\345\274\217\351\253\230\345\217\257\347\224\250/02.\346\234\215\345\212\241\345\256\271\351\224\231.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\346\234\215\345\212\241\345\256\271\351\224\231.md" index 55b1a02c2b..6907d6b78c 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/13.\345\210\206\345\270\203\345\274\217\351\253\230\345\217\257\347\224\250/02.\346\234\215\345\212\241\345\256\271\351\224\231.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/01.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214\347\273\274\345\220\210/\346\234\215\345\212\241\345\256\271\351\224\231.md" @@ -4,14 +4,16 @@ date: 2022-04-20 17:27:42 order: 02 categories: - 分布式 - - 分布式高可用 + - 分布式协同 + - 分布式协同综合 tags: - 分布式 + - 协同 - 服务治理 - 监控 - APM - 链路追踪 -permalink: /pages/e32c7e/ +permalink: /pages/78df4826/ --- # 服务容错 @@ -83,4 +85,4 @@ Hystrix 就采用舱壁隔离模式来实现线程隔离。 ## 参考资料 - [从 0 开始学微服务](https://time.geekbang.org/column/intro/100014401) -- [凤凰架构之服务容错](http://icyfenix.cn/distribution/traffic-management/failure.html) \ No newline at end of file +- [凤凰架构之服务容错](http://icyfenix.cn/distribution/traffic-management/failure.html) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/01.ZooKeeper\345\216\237\347\220\206.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/01.ZooKeeper\345\216\237\347\220\206.md" index 41d8b4a9de..346aa20c82 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/01.ZooKeeper\345\216\237\347\220\206.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/01.ZooKeeper\345\216\237\347\220\206.md" @@ -9,7 +9,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/f9ff40/ +permalink: /pages/885cc3a1/ --- # ZooKeeper 原理 @@ -76,7 +76,7 @@ Zookeeper 服务是一个基于主从复制的高可用集群,集群中每个 树中的节点被称为 **`znode`**,其中根节点为 `/`,每个节点上都会保存自己的数据和节点信息。znode 可以用于存储数据,并且有一个与之相关联的 ACL(详情可见 [ACL](#ACL))。ZooKeeper 的设计目标是实现协调服务,而不是真的作为一个文件存储,因此 znode 存储数据的**大小被限制在 1MB 以内**。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_1.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240730789.png) **ZooKeeper 的数据访问具有原子性**。其读写操作都是要么全部成功,要么全部失败。 @@ -145,7 +145,7 @@ ZooKeeper 定义了如下五种权限: 由于处理读请求不需要服务器之间的交互,**Follower/Observer 越多,整体系统的读请求吞吐量越大**,也即读性能越好。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_3.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240730119.png) ### 写操作 @@ -153,7 +153,7 @@ ZooKeeper 定义了如下五种权限: #### 写 Leader -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_4.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240731595.png) 由上图可见,通过 Leader 进行写操作,主要分为五步: @@ -171,7 +171,7 @@ ZooKeeper 定义了如下五种权限: #### 写 Follower/Observer -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_5.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240731844.png) - Follower/Observer 均可接受写请求,但不能直接处理,而需要将写请求转发给 Leader 处理。 - 除了多了一步请求转发,其它流程与直接写 Leader 无任何区别。 @@ -247,7 +247,7 @@ Zookeeper 中的所有数据其实都是由一个名为 `DataTree` 的数据结 通常来说,会话应该长期存在,而这需要由客户端来保证。客户端可以通过心跳方式(ping)来保持会话不过期。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200602182239.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240732938.png) ZooKeeper 的会话具有四个属性: @@ -341,7 +341,7 @@ ZAB 协议定义了两个可以**无限循环**的流程: 那么,ZooKeeper 是如何实现副本机制的呢?答案是:ZAB 协议的原子广播。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_3.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240735474.png) ZAB 协议的原子广播要求: @@ -359,7 +359,7 @@ ZAB 协议的原子广播要求: 在分布式系统中,通常需要一个全局唯一的名字,如生成全局唯一的订单号等,ZooKeeper 可以通过顺序节点的特性来生成全局唯一 ID,从而可以对分布式系统提供命名服务。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200602182548.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240736223.png) ### 配置管理 @@ -373,7 +373,7 @@ ZAB 协议的原子广播要求: (1)访问 `/lock` (这个目录路径由程序自己决定),创建 **带序列号的临时节点(EPHEMERAL)** 。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200602191358.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240738997.png) (2)每个节点尝试获取锁时,拿到 `/locks`节点下的所有子节点(`id_0000`,`id_0001`,`id_0002`),**判断自己创建的节点是不是序列号最小的** @@ -381,11 +381,11 @@ ZAB 协议的原子广播要求: - 释放锁:执行完操作后,把创建的节点给删掉。 - 如果不是,则监听比自己要小 1 的节点变化。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200602192619.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240738641.png) (3)释放锁,即删除自己创建的节点。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200602192341.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202412240739623.png) 图中,NodeA 删除自己创建的节点 `id_0000`,NodeB 监听到变化,发现自己的节点已经是最小节点,即可获取到锁。 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/02.ZooKeeperJavaApi.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/02.ZooKeeperJavaApi.md" index f2224db842..b20d94346c 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/02.ZooKeeperJavaApi.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/02.ZooKeeperJavaApi.md" @@ -9,7 +9,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/3cb33a/ +permalink: /pages/029ebe41/ --- # ZooKeeper Java Api diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/03.ZooKeeper\345\221\275\344\273\244.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/03.ZooKeeper\345\221\275\344\273\244.md" index 78f25d8a8d..17dfd9d165 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/03.ZooKeeper\345\221\275\344\273\244.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/03.ZooKeeper\345\221\275\344\273\244.md" @@ -9,7 +9,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/13c5e2/ +permalink: /pages/bd3b7203/ --- # ZooKeeper 命令 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/04.ZooKeeper\350\277\220\347\273\264.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/04.ZooKeeper\350\277\220\347\273\264.md" index 6c174b5abe..45c9d26810 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/04.ZooKeeper\350\277\220\347\273\264.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/04.ZooKeeper\350\277\220\347\273\264.md" @@ -9,7 +9,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/bb5e61/ +permalink: /pages/c26d08a8/ --- # ZooKeeper 运维指南 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/05.ZooKeeperAcl.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/05.ZooKeeperAcl.md" index ea7d484192..af6980d611 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/05.ZooKeeperAcl.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/05.ZooKeeperAcl.md" @@ -9,7 +9,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/4046ce/ +permalink: /pages/d5540d63/ --- # ZooKeeper ACL diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/README.md" index d5b156e057..a19ca35bd8 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/02.ZooKeeper/README.md" @@ -8,7 +8,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/1b41b6/ +permalink: /pages/886d8061/ hidden: true index: false --- diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/README.md" index 5fab51a260..9d61c886f2 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/11.\345\210\206\345\270\203\345\274\217\345\215\217\345\220\214/README.md" @@ -7,7 +7,7 @@ categories: tags: - 分布式 - 分布式协同 -permalink: /pages/52c8b1/ +permalink: /pages/d8a785a7/ hidden: true index: false --- @@ -17,12 +17,12 @@ index: false ## 📖 内容 - **分布式协同综合** - - 集群 - - [分布式复制](01.分布式协同综合/02.分布式复制.md) - - 分区 - - 选主 - - [分布式事务](01.分布式协同综合/05.分布式事务.md) - 关键词:`2PC`、`3PC`、`TCC`、`本地消息表`、`MQ 消息`、`SAGA` - - [分布式锁](01.分布式协同综合/06.分布式锁.md) - 关键词:`数据库`、`Redis`、`ZooKeeper`、`互斥`、`可重入`、`死锁`、`容错`、`自旋尝试` + - [分布式复制](01.分布式协同综合/分布式复制.md) - 关键词:`主从`、`多主`、`无主` + - [分布式分区](01.分布式协同综合/分布式分区.md) - 关键词:`分区再均衡`、`路由` + - [分布式共识](01.分布式协同综合/分布式共识.md) - 关键词:`共识`、`广播`、`epoch`、`quorum` + - [分布式事务](01.分布式协同综合/分布式事务.md) - 关键词:`2PC`、`3PC`、`TCC`、`本地消息表`、`消息事务`、`SAGA` + - [分布式锁](01.分布式协同综合/分布式锁.md) - 关键词:`互斥`、`可重入`、`死锁`、`容错`、`自旋尝试`、`公平性` + - [分布式协同面试](01.分布式协同综合/分布式协同面试.md) 💯 - **ZooKeeper** - [ZooKeeper 原理](02.ZooKeeper/01.ZooKeeper原理.md) - [ZooKeeper Java Api](02.ZooKeeper/02.ZooKeeperJavaApi.md) @@ -30,29 +30,6 @@ index: false - [ZooKeeper 运维](02.ZooKeeper/04.ZooKeeper运维.md) - [ZooKeeper Acl](02.ZooKeeper/05.ZooKeeperAcl.md) -## 📚 资料 - -- [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 - -### ZooKeeper 资料 - -- **官方** - - [ZooKeeper 官网](http://zookeeper.apache.org/) - - [ZooKeeper 官方文档](https://cwiki.apache.org/confluence/display/ZOOKEEPER) - - [ZooKeeper Github](https://github.com/apache/zookeeper) - - [Apache Curator 官网](http://curator.apache.org/) -- **书籍** - - [《Hadoop 权威指南(第四版)》](https://item.jd.com/12109713.html) - - [《从 Paxos 到 Zookeeper 分布式一致性原理与实践》](https://item.jd.com/11622772.html) -- **文章** - - [分布式服务框架 ZooKeeper -- 管理分布式环境中的数据](https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/index.html) - - [ZooKeeper 的功能以及工作原理](https://www.cnblogs.com/felixzh/p/5869212.html) - - [ZooKeeper 简介及核心概念](https://github.com/heibaiying/BigData-Notes/blob/master/notes/ZooKeeper%E7%AE%80%E4%BB%8B%E5%8F%8A%E6%A0%B8%E5%BF%83%E6%A6%82%E5%BF%B5.md) - - [详解分布式协调服务 ZooKeeper](https://draveness.me/zookeeper-chubby) - - [深入浅出 Zookeeper(一) Zookeeper 架构及 FastLeaderElection 机制](http://www.jasongj.com/zookeeper/fastleaderelection/) - - [Introduction to Apache ZooKeeper](https://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper) - - [Zookeeper 的优缺点](https://blog.csdn.net/wwwsq/article/details/7644445) - ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/03.\346\265\201\351\207\217\346\216\247\345\210\266.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/03.\346\265\201\351\207\217\346\216\247\345\210\266.md" deleted file mode 100644 index b032c68891..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/03.\346\265\201\351\207\217\346\216\247\345\210\266.md" +++ /dev/null @@ -1,680 +0,0 @@ ---- -title: 流量控制 -date: 2020-01-20 11:06:00 -order: 03 -categories: - - 分布式 - - 分布式调度 -tags: - - 分布式 - - 流量调度 - - 流量控制 - - 限流 - - 熔断 - - 降级 -permalink: /pages/60bb6d/ ---- - -# 流量控制 - -> 在高并发场景下,为了应对瞬时海量请求的压力,保障系统的平稳运行,必须预估系统的流量阈值,通过限流规则阻断处理不过来的请求。 - -## 限流简介 - -限流可以认为是服务降级的一种。限流就是**限制系统的输入和输出流量已达到保护系统的目的**。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。 - -限流规则包含三个部分:时间粒度,接口粒度,最大限流值。限流规则设置是否合理直接影响到限流是否合理有效。 - -## 限流算法 - -### 固定窗口限流算法 - -#### 固定窗口限流算法的原理 - -固定窗口限流算法的**基本策略**是: - -1. 设置一个固定时间窗口,以及这个固定时间窗口内的最大请求数; -2. 为每个固定时间窗口设置一个计数器,用于统计请求数; -3. 一旦请求数超过最大请求数,则请求会被拦截。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748006.png) - -#### 固定窗口限流算法的利弊 - -固定窗口限流算法的**优点**是:实现简单。 - -固定窗口限流算法的**缺点**是:存在**临界问题**。所谓临界问题,是指:流量分别集中在一个固定时间窗口的尾部和一个固定时间窗口的头部。举例来说,假设限流规则为每分钟不超过 100 次请求。在第一个时间窗口中,起初没有任何请求,在最后 1 s,收到 100 次请求,由于没有达到阈值,所有请求都通过;在第二个时间窗口中,第 1 秒就收到 100 次请求,而后续没有任何请求。虽然,这两个时间窗口内的流量都符合限流要求,但是在两个时间窗口临界的这 2s 内,实际上有 200 次请求,显然是超过预期吞吐量的,存在压垮系统的可能。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748769.png) - -#### 固定窗口限流算法的实现 - -【示例】Java 版本的固定窗口限流算法 - -```java -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -public class SlidingWindowRateLimiter implements RateLimiter { - - /** - * 允许的最大请求数 - */ - private final long maxPermits; - - /** - * 窗口期时长 - */ - private long periodMillis; - - /** - * 分片窗口期时长 - */ - private final long shardPeriodMillis; - - /** - * 窗口期截止时间 - */ - private long lastPeriodMillis; - - /** - * 分片窗口数 - */ - private final int shardNum; - - /** - * 请求总计数 - */ - private final AtomicLong totalCount = new AtomicLong(0); - - /** - * 分片窗口计数列表 - */ - private final List countList = new LinkedList<>(); - - public SlidingWindowRateLimiter(long qps, int shardNum) { - this(qps, 1000, TimeUnit.MILLISECONDS, shardNum); - } - - public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) { - this.maxPermits = maxPermits; - this.periodMillis = timeUnit.toMillis(period); - this.lastPeriodMillis = System.currentTimeMillis(); - this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum; - this.shardNum = shardNum; - for (int i = 0; i < shardNum; i++) { - countList.add(new AtomicLong(0)); - } - } - - @Override - public synchronized boolean tryAcquire(int permits) { - long now = System.currentTimeMillis(); - if (now > lastPeriodMillis) { - for (int shardId = 0; shardId < shardNum; shardId++) { - long shardCount = countList.get(shardId).get(); - totalCount.addAndGet(-shardCount); - countList.set(shardId, new AtomicLong(0)); - lastPeriodMillis += shardPeriodMillis; - } - } - int shardId = (int) (now % periodMillis / shardPeriodMillis); - if (totalCount.get() + permits <= maxPermits) { - countList.get(shardId).addAndGet(permits); - totalCount.addAndGet(permits); - return true; - } else { - return false; - } - } - -} -``` - -### 滑动窗口限流算法 - -#### 滑动窗口限流算法的原理 - -滑动窗口限流算法是对固定窗口限流算法的改进,解决了临界问题。 - -滑动窗口限流算法的**基本策略**是: - -- 将固定时间窗口分片为多个子窗口,每个子窗口的访问次数独立统计; -- 当请求时间大于当前子窗口的最大时间时,则将当前子窗口废弃,并将计时窗口向前滑动,并将下一个子窗口置为当前窗口。 -- 要保证所有子窗口的统计数之和不能超过阈值。 - -滑动窗口限流算法就是针对固定窗口限流算法的更细粒度的控制,分片越多,则限流越精准。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748277.png) - -#### 滑动窗口限流算法的利弊 - -滑动窗口限流算法的**优点**是:在滑动窗口限流算法中,临界位置的突发请求都会被算到时间窗口内,因此可以解决计数器算法的临界问题。 - -滑动窗口限流算法的**缺点**是: - -- **额外的内存开销** - 滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,所以存在额外的内存开销。 -- **限流的控制粒度受限于窗口分片粒度** - 滑动窗口限流算法,**只能在选定的时间粒度上限流,对选定时间粒度内的更加细粒度的访问频率不做限制**。但是,由于每个分片窗口都有额外的内存开销,所以也并不是分片数越多越好的。 - -#### 滑动窗口限流算法的实现 - -【示例】Java 版本的滑动窗口限流算法 - -```java -import java.util.LinkedList; -import java.util.List; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicLong; - -public class SlidingWindowRateLimiter implements RateLimiter { - - /** - * 允许的最大请求数 - */ - private final long maxPermits; - - /** - * 窗口期时长 - */ - private final long periodMillis; - - /** - * 分片窗口期时长 - */ - private final long shardPeriodMillis; - - /** - * 窗口期截止时间 - */ - private long lastPeriodMillis; - - /** - * 分片窗口数 - */ - private final int shardNum; - - /** - * 请求总计数 - */ - private final AtomicLong totalCount = new AtomicLong(0); - - /** - * 分片窗口计数列表 - */ - private final List countList = new LinkedList<>(); - - public SlidingWindowRateLimiter(long qps, int shardNum) { - this(qps, 1000, TimeUnit.MILLISECONDS, shardNum); - } - - public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) { - this.maxPermits = maxPermits; - this.periodMillis = timeUnit.toMillis(period); - this.lastPeriodMillis = System.currentTimeMillis(); - this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum; - this.shardNum = shardNum; - for (int i = 0; i < shardNum; i++) { - countList.add(new AtomicLong(0)); - } - } - - @Override - public synchronized boolean tryAcquire(int permits) { - long now = System.currentTimeMillis(); - if (now > lastPeriodMillis) { - for (int shardId = 0; shardId < shardNum; shardId++) { - long shardCount = countList.get(shardId).get(); - totalCount.addAndGet(-shardCount); - countList.set(shardId, new AtomicLong(0)); - lastPeriodMillis += shardPeriodMillis; - } - } - int shardId = (int) (now % periodMillis / shardPeriodMillis); - if (totalCount.get() + permits <= maxPermits) { - countList.get(shardId).addAndGet(permits); - totalCount.addAndGet(permits); - return true; - } else { - return false; - } - } - -} -``` - -### 漏桶限流算法 - -#### 漏桶限流算法的原理 - -漏桶限流算法的**基本策略**是: - -- 水(请求)以任意速率由入口进入到漏桶中; -- 水以固定的速率由出口出水(请求通过); -- 漏桶的容量是固定的,如果水的流入速率大于流出速率,最终会导致漏桶中的水溢出(这意味着请求拒绝)。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230749486.png) - -#### 漏桶限流算法的利弊 - -漏桶限流算法的**优点**是:**消费速率固定**——即无论流量多大,即便是突发的大流量,处理请求的速度始终是固定的。 - -漏桶限流算法的**缺点**是:不能灵活的调整流量。例如:一个集群通过增减节点的方式,弹性伸缩了其吞吐能力,漏桶限流算法无法随之调整。 - -**漏桶策略适用于间隔性突发流量且流量不用即时处理的场景**。 - -#### 漏桶限流算法的实现 - -【示例】Java 版本的漏桶限流算法 - -```java -import java.util.concurrent.atomic.AtomicLong; - -public class LeakyBucketRateLimiter implements RateLimiter { - - /** - * QPS - */ - private final int qps; - - /** - * 桶的容量 - */ - private final long capacity; - - /** - * 计算的起始时间 - */ - private long beginTimeMillis; - - /** - * 桶中当前的水量 - */ - private final AtomicLong waterNum = new AtomicLong(0); - - public LeakyBucketRateLimiter(int qps, int capacity) { - this.qps = qps; - this.capacity = capacity; - } - - @Override - public synchronized boolean tryAcquire(int permits) { - - // 如果桶中没有水,直接通过 - if (waterNum.get() == 0) { - beginTimeMillis = System.currentTimeMillis(); - waterNum.addAndGet(permits); - return true; - } - - // 计算水量 - long leakedWaterNum = ((System.currentTimeMillis() - beginTimeMillis) / 1000) * qps; - long currentWaterNum = waterNum.get() - leakedWaterNum; - waterNum.set(Math.max(0, currentWaterNum)); - - // 重置时间 - beginTimeMillis = System.currentTimeMillis(); - - if (waterNum.get() + permits < capacity) { - waterNum.addAndGet(permits); - return true; - } else { - return false; - } - } - -} -``` - -### 令牌桶限流算法 - -#### 令牌桶限流算法的原理 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230750231.png) - -令牌桶算法的**原理**: - -1. 接口限制 T 秒内最大访问次数为 N,则每隔 T/N 秒会放一个 token 到桶中 -2. 桶内最多存放 M 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 就会被丢弃 -3. 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 则进行限流处理 - -#### 令牌桶限流算法的利弊 - -因为令牌桶存放了很多令牌,那么大量的突发请求会被执行,但是它不会出现临界问题,在令牌用完之后,令牌是以一个恒定的速率添加到令牌桶中的,因此不能再次发送大量突发请求。 - -规定固定容量的桶,token 以固定速度往桶内填充,当桶满时 token 不会被继续放入,每过来一个请求把 token 从桶中移除,如果桶中没有 token 不能请求。 - -**令牌桶算法适用于有突发特性的流量,且流量需要即时处理的场景**。 - -#### 令牌桶限流算法的实现 - -【示例】Java 实现令牌桶算法 - -```java -import java.util.concurrent.atomic.AtomicLong; - -/** - * 令牌桶限流算法 - * - * @author Zhang Peng - * @date 2024-01-18 - */ -public class TokenBucketRateLimiter implements RateLimiter { - - /** - * QPS - */ - private final long qps; - - /** - * 桶的容量 - */ - private final long capacity; - - /** - * 上一次令牌发放时间 - */ - private long endTimeMillis; - - /** - * 桶中当前的令牌数量 - */ - private final AtomicLong tokenNum = new AtomicLong(0); - - public TokenBucketRateLimiter(long qps, long capacity) { - this.qps = qps; - this.capacity = capacity; - this.endTimeMillis = System.currentTimeMillis(); - } - - @Override - public synchronized boolean tryAcquire(int permits) { - - long now = System.currentTimeMillis(); - long gap = now - endTimeMillis; - - // 计算令牌数 - long newTokenNum = (gap * qps / 1000); - long currentTokenNum = tokenNum.get() + newTokenNum; - tokenNum.set(Math.min(capacity, currentTokenNum)); - - if (tokenNum.get() < permits) { - return false; - } else { - tokenNum.addAndGet(-permits); - endTimeMillis = now; - return true; - } - } - -} -``` - -> **扩展** -> -> Guava 的 RateLimiter 工具类就是基于令牌桶算法实现,其源码分析可以参考:[RateLimiter 基于漏桶算法,但它参考了令牌桶算法](https://blog.csdn.net/forezp/article/details/100060686) - -### 限流算法测试 - -```java -import cn.hutool.core.thread.ThreadUtil; -import cn.hutool.core.util.RandomUtil; -import lombok.extern.slf4j.Slf4j; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; - -@Slf4j -public class RateLimiterDemo { - - public static void main(String[] args) { - - // ============================================================================ - - int qps = 20; - - System.out.println("======================= 固定时间窗口限流算法 ======================="); - FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(qps); - testRateLimit(fixedWindowRateLimiter, qps); - - System.out.println("======================= 滑动时间窗口限流算法 ======================="); - SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(qps, 10); - testRateLimit(slidingWindowRateLimiter, qps); - - System.out.println("======================= 漏桶限流算法 ======================="); - LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter(qps, 100); - testRateLimit(leakyBucketRateLimiter, qps); - - System.out.println("======================= 令牌桶限流算法 ======================="); - TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter(qps, 100); - testRateLimit(tokenBucketRateLimiter, qps); - } - - private static void testRateLimit(RateLimiter rateLimiter, int qps) { - - AtomicInteger okNum = new AtomicInteger(0); - AtomicInteger limitNum = new AtomicInteger(0); - ExecutorService executorService = ThreadUtil.newFixedExecutor(10, "限流测试", true); - long beginTime = System.currentTimeMillis(); - - int threadNum = 4; - final CountDownLatch latch = new CountDownLatch(threadNum); - for (int i = 0; i < threadNum; i++) { - executorService.submit(() -> { - try { - batchRequest(rateLimiter, okNum, limitNum, 1000); - } catch (Exception e) { - log.error("发生异常!", e); - } finally { - latch.countDown(); - } - }); - } - - try { - latch.await(10, TimeUnit.SECONDS); - long endTime = System.currentTimeMillis(); - long gap = endTime - beginTime; - log.info("限流 QPS: {} -> 实际结果:耗时 {} ms,{} 次请求成功,{} 次请求被限流,实际 QPS: {}", - qps, gap, okNum.get(), limitNum.get(), okNum.get() * 1000 / gap); - if (okNum.get() == qps) { - log.info("限流符合预期"); - } - } catch (Exception e) { - log.error("发生异常!", e); - } finally { - executorService.shutdown(); - } - } - - private static void batchRequest(RateLimiter rateLimiter, AtomicInteger okNum, AtomicInteger limitNum, int num) - throws InterruptedException { - for (int j = 0; j < num; j++) { - if (rateLimiter.tryAcquire(1)) { - log.info("请求成功"); - okNum.getAndIncrement(); - } else { - log.info("请求限流"); - limitNum.getAndIncrement(); - } - TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(0, 10)); - } - } - -} -``` - -## 分布式限流 - -前文中,基于 Java 实现的限流算法示例只能运行在单节点,无法有效应对集群部署的服务,这中场景下就需要分布式限流。 - -实现分布式限流的一种简单解决方案是使用 Redis + Lua 来实现。使用二者来开发的原因是:1. Redis 的性能极高;2. Redis 支持以原子操作的方式执行 Lua 脚本。 - -### Redis + Lua 实现的固定窗口限流算法 - -Redis + Lua 实现的固定窗口限流算法实现思路: - -- 根据实际需要,将当前时间格式化为天(`yyyyMMdd`)、时(`yyyyMMddHH`)、分(`yyyyMMddHHmm`)、秒(`yyyyMMddHHmmss`),并作为 Redis 的 String 类型 Key。该 Key 可以视为一个固定时间窗口,其中的 value 用于统计访问量; -- 用于代表不同粒度的时间窗口按需设置过期时间; -- 一旦达到窗口的限流阈值时,请求被限流;否则请求通过。 - -【示例】Redis + Lua 实现的固定窗口限流算法 - -下面的代码片段模拟通过一个大小为 1 分钟的固定时间窗口进行限流,阈值为 100,过期时间 60s。 - -```java - private final String key = "rate:limit:202401222100"; - private final int limit = 100; - private final int seconds = 60; - - public boolean tryAcquire(int permits) { - // -- 缓存 Key - // local key = KEYS[1] - // -- 访问请求数 - // local permits = tonumber(ARGV[1]) - // -- 过期时间 - // local seconds = tonumber(ARGV[2]) - // -- 限流阈值 - // local limit = tonumber(ARGV[3]) - // - // -- 获取统计值 - // local count = tonumber(redis.call('GET', key) or "0") - // - // if count + permits > limit then - // -- 触发限流 - // return 0 - // else - // redis.call('INCRBY', key, permits) - // redis.call('EXPIRE', key, seconds) - // return count + permits - // end - String script = - "-- 缓存 Key\n" - + "local key = KEYS[1]\n" - + "-- 访问请求数\n" - + "local permits = tonumber(ARGV[1])\n" - + "-- 过期时间\n" - + "local seconds = tonumber(ARGV[2])\n" - + "-- 限流阈值\n" - + "local limit = tonumber(ARGV[3])\n" - + "\n" - + "-- 获取统计值\n" - + "local count = tonumber(redis.call('GET', key) or \"0\")\n" - + "\n" - + "if count + permits > limit then\n" - + " -- 触发限流\n" - + " return 0\n" - + "else\n" - + " redis.call('INCRBY', key, permits)\n" - + " redis.call('EXPIRE', key, seconds)\n" - + " return count + permits\n" - + "end"; - List keys = Collections.singletonList(key); - List args = Arrays.asList(String.valueOf(permits), String.valueOf(seconds), String.valueOf(limit)); - Object eval = jedis.eval(script, keys, args); - long value = (long) eval; - return value != 0; - } - - @Test - public void test() { - - for (int i = 0; i < 11; i++) { - if (tryAcquire(10)) { - System.out.println("请求成功"); - } else { - System.out.println("请求失败"); - } - } - } - // 请求成功 - // 请求成功 - // 请求成功 - // 请求成功 - // 请求成功 - // 请求成功 - // 请求成功 - // 请求成功 - // 请求成功 - // 请求成功 - // 请求失败 - // rate:limit:202401222100 统计值达到 100 -``` - -### Redis + Lua 实现的令牌桶限流算法 - -【示例】基于 Redis Lua 令牌桶限流算法实现 - -```lua --- 令牌桶限流 - --- 令牌的唯一标识 -local bucketKey = KEYS[1] --- 上次请求的时间 -local last_mill_request_key = KEYS[2] --- 令牌桶的容量 -local limit = tonumber(ARGV[1]) --- 请求令牌的数量 -local permits = tonumber(ARGV[2]) --- 令牌流入的速率 -local rate = tonumber(ARGV[3]) --- 当前时间 -local curr_mill_time = tonumber(ARGV[4]) - --- 添加令牌 - --- 获取当前令牌的数量 -local current_limit = tonumber(redis.call('get', bucketKey) or "0") --- 获取上次请求的时间 -local last_mill_request_time = tonumber(redis.call('get', last_mill_request_key) or "0") --- 计算向桶里添加令牌的数量 -if last_mill_request_time == 0 then - -- 令牌桶初始化 - -- 更新上次请求时间 - redis.call("HSET", last_mill_request_key, curr_mill_time) - return 0 -else - local add_token_num = math.floor((curr_mill_time - last_mill_request_time) * rate) -end - --- 更新令牌的数量 -if current_limit + add_token_num > limit then - current_limit = limit -else - current_limit = current_limit + add_token_num -end - redis.pcall("HSET",bucketKey, current_limit) --- 设置过期时间 -redis.call("EXPIRE", bucketKey, 2) - --- 限流判断 -if current_limit - permits < 1 then - -- 达到限流大小 - return 0 -else - -- 没有达到限流大小 - current_limit = current_limit - permits - redis.pcall("HSET", bucketKey, current_limit) - -- 设置过期时间 - redis.call("EXPIRE", bucketKey, 2) - -- 更新上次请求的时间 - redis.call("HSET", last_mill_request_key, curr_mill_time) -end -``` - -## 限流工具 - -前面介绍了限流算法的基本原理和一些简单的实现。但在生产环境,我们一般应该使用更成熟的限流工具。 - -- > Guava 的 `RateLimiter`:RateLimiter 基于漏桶算法,但它参考了令牌桶算法。具体用法可以参考:[RateLimiter 基于漏桶算法,但它参考了令牌桶算法](https://blog.csdn.net/forezp/article/details/100060686) -- [Hystrix](https://github.com/Netflix/Hystrix):经典的限流、熔断工具,很值得借鉴学习。注:官方已停止发布版本。 -- [Sentinel](https://github.com/alibaba/Sentinel):阿里的限流、熔断工具。 - -## 参考资料 - -- [《大型网站技术架构:核心原理与案例分析》](https://item.jd.com/11322972.html) -- [谈谈限流算法的几种实现](https://www.jianshu.com/p/76cc8ba5ca91) -- [如何限流?在工作中是怎么做的?说一下具体的实现?](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/huifer-how-to-limit-current.md) -- [浅析限流算法](https://gongfukangee.github.io/2019/04/04/Limit/) -- [RateLimiter 基于漏桶算法,但它参考了令牌桶算法](https://blog.csdn.net/forezp/article/details/100060686) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/04.\345\210\206\345\270\203\345\274\217ID.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/04.\345\210\206\345\270\203\345\274\217ID.md" deleted file mode 100644 index bdcf749703..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/04.\345\210\206\345\270\203\345\274\217ID.md" +++ /dev/null @@ -1,247 +0,0 @@ ---- -title: 分布式 ID 基本原理 -date: 2019-07-24 11:55:00 -order: 04 -categories: - - 分布式 - - 分布式调度 -tags: - - 分布式 - - 数据调度 - - 分布式ID -permalink: /pages/3ae455/ ---- - -# 分布式 ID 基本原理 - -> 传统数据库软件开发中,主键自动生成技术是基本需求。而各个数据库对于该需求也提供了相应的支持,比如 MySQL 的自增键,Oracle 的自增序列等。 -> -> 数据分片后,不同数据节点生成全局唯一主键是非常棘手的问题。同一个逻辑表内的不同实际表之间的自增键由于无法互相感知而产生重复主键。 虽然可通过约束自增主键初始值和步长的方式避免碰撞,但需引入额外的运维规则,使解决方案缺乏完整性和可扩展性。 -> -> 为此,需要使用分布式 ID 来解决此问题。本文总结业界常用的分布式 ID 解决方案。 - -## 1. 分布式 ID 简介 - -首先,分布式 ID 应该具备哪些特性呢? - -1. **全局唯一性** - 不能出现重复的 ID 号,既然是唯一标识,这是最基本的要求。 -2. **趋势递增** - 在 MySQL InnoDB 引擎中使用的是聚集索引,由于多数 RDBMS 使用 B-tree 的数据结构来存储索引数据,在主键的选择上面我们应该尽量使用有序的主键保证写入性能。 -3. **单调递增** - 保证下一个 ID 一定大于上一个 ID,例如事务版本号、IM 增量消息、排序等特殊需求。 -4. **信息安全** - 如果 ID 是连续的,恶意用户的扒取工作就非常容易做了,直接按照顺序下载指定 URL 即可;如果是订单号就更危险了,竞对可以直接知道我们一天的单量。所以在一些应用场景下,会需要 ID 无规则、不规则。 - -## 2. UUID - -UUID 是最简单的分布式 ID 方案。 - -UUID 是通用唯一识别码(Universally Unique Identifier)的缩写,开放软件基金会(OSF)规范定义了包括网卡 MAC 地址、时间戳、名字空间(Namespace)、随机或伪随机数、时序等元素。利用这些元素来生成 UUID。 - -UUID 是由 128 位二进制组成,一般转换成十六进制,然后用 String 表示。在 java 中有个 UUID 类,在他的注释中我们看见这里有 4 种不同的 UUID 的生成策略: - -- random - 基于随机数生成 UUID,由于 Java 中的随机数是伪随机数,其重复的概率是可以被计算出来的。这个一般我们用下面的代码获取基于随机数的 UUID: - -```java -String id = UUID.randomUUID().toString(); -``` - -- time-based - 基于时间的 UUID,这个一般是通过当前时间,随机数,和本地 Mac 地址来计算出来,自带的 JDK 包并没有这个算法的我们在一些 UUIDUtil 中,比如我们的 log4j.core.util,会重新定义 UUID 的高位和低位。 - -```java - public static UUID getTimeBasedUuid() { - long time = System.currentTimeMillis() * 10000L + 122192928000000000L + (long)(COUNT.incrementAndGet() % 10000); - long timeLow = (time & 4294967295L) << 32; - long timeMid = (time & 281470681743360L) >> 16; - long timeHi = (time & 1152640029630136320L) >> 48; - long most = timeLow | timeMid | 4096L | timeHi; - return new UUID(most, LEAST); - } -``` - -- DCE security - DCE 安全的 UUID。 - -- name-based - 基于名字的 UUID,通过计算名字和名字空间的 MD5 来计算 UUID。 - -### 2.1. UUID 的优点 - -- 通过本地生成,没有经过网络 I/O,性能较快。 - -### 2.2. UUID 的缺点 - -- **长度过长** - UUID 太长,16 字节 128 位,通常以 36 长度的字符串表示,很多场景不适用。例如:Mysql 官方明确建议主键越短越好,36 个字符长度的 UUID 不符合要求。 -- **信息不安全** - 基于 MAC 地址生成 UUID 的算法可能会造成 MAC 地址泄露,这个漏洞曾被用于寻找梅丽莎病毒的制作者位置。 -- **无序性** - 不能生成递增有序的数字。这对于一些特定场景不利。例如:MySQL InnoDB 存储引擎使用 B+ 树存储索引数据,而主键也是一种索引。索引数据在 B+ 树中是有序排列的。UUID 的无序性可能会引起数据位置频繁变动,严重影响性能。 - -### 2.3. 适用场景 - -UUID 的适用场景可以为不需要担心过多的空间占用,以及不需要生成有递增趋势的数字。在 Log4j 里 `UuidPatternConverter` 中加入了 UUID 来标识每一条日志。 - -## 3. 利用第三方存储生成键 - -提到自增键,最先想到的肯定是直接使用数据库自增键。_各数据库对于该需求也提供了相应的支持,比如 MySQL 的自增键,Oracle 的自增序列等_。 - -当然,也可以考虑是用 Redis 这样的 Nosql,甚至 ZooKeeper 去生成键 - -### 3.1. 优点 - -- 非常简单,利用现有的功能实现,成本小 -- 有序递增 -- 方便排序和分页 - -### 3.2. 缺点 - -- 强依赖第三方存储,如果第三方存储非高可用系统,若出现丢失数据的情况,就可能出现重复生成 ID 的问题。 -- 生成 ID 性能瓶颈依赖于第三方存储的性能。 -- 增加了对第三方存储运维的成本。 - -## 4. 雪花算法(Snowflake) - -雪花算法(Snowflake)是由 Twitter 公布的分布式主键生成算法,**它会生成一个 `64 bit` 的整数**,可以保证不同进程主键的不重复性,以及相同进程主键的有序性。 - -在同一个进程中,它首先是通过时间位保证不重复,如果时间相同则是通过序列位保证。 同时由于时间位是单调递增的,且各个服务器如果大体做了时间同步,那么生成的主键在分布式环境可以认为是总体有序的,这就保证了对索引字段的插入的高效性。 - -### 4.1. 基本原理 - -#### 4.1.1. 键的组成 - -使用**雪花算法生成的主键,二进制表示形式包含 4 部分**,从高位到低位分表为:1bit 符号位、41bit 时间戳位、10bit 工作进程位以及 12bit 序列号位。 - -- **符号位(1bit)** - -预留的符号位,恒为零。 - -- **时间戳位(41bit)** - -41 位的时间戳可以容纳的毫秒数是 2 的 41 次幂,一年所使用的毫秒数是:`365 * 24 * 60 * 60 * 1000`。通过计算可知: - -```java -Math.pow(2, 41) / (365 * 24 * 60 * 60 * 1000L); -``` - -结果约等于 69.73 年。ShardingSphere 的雪花算法的时间纪元从 2016 年 11 月 1 日零点开始,可以使用到 2086 年,相信能满足绝大部分系统的要求。 - -- **工作进程位(10bit)** - -该标志在 Java 进程内是唯一的,如果是分布式应用部署应保证每个工作进程的 id 是不同的。该值默认为 0,可通过属性设置。 - -- **序列号位(12bit)** - -该序列是用来在同一个毫秒内生成不同的 ID。如果在这个毫秒内生成的数量超过 4096(2 的 12 次幂),那么生成器会等待到下个毫秒继续生成。 - -雪花算法主键的详细结构见下图: - -![雪花算法](https://shardingsphere.apache.org/document/current/img/sharding/snowflake_cn_v2.png) - -#### 4.1.2. 时钟回拨 - -服务器时钟回拨会导致产生重复序列,因此默认分布式主键生成器提供了一个最大容忍的时钟回拨毫秒数。 如果时钟回拨的时间超过最大容忍的毫秒数阈值,则程序报错;如果在可容忍的范围内,默认分布式主键生成器会等待时钟同步到最后一次主键生成的时间后再继续工作。 最大容忍的时钟回拨毫秒数的默认值为 0,可通过属性设置。 - -#### 4.1.3. 灵活定制 - -上面只是一个将 `64bit` 划分的标准,当然也不一定这么做,可以根据不同业务的具体场景来划分,比如下面给出一个业务场景: - -- 服务目前 QPS10 万,预计几年之内会发展到百万。 -- 当前机器三地部署,上海,北京,深圳都有。 -- 当前机器 10 台左右,预计未来会增加至百台。 - -这个时候我们根据上面的场景可以再次合理的划分 62bit,QPS 几年之内会发展到百万,那么每毫秒就是千级的请求,目前 10 台机器那么每台机器承担百级的请求,为了保证扩展,后面的循环位可以限制到 1024,也就是 2^10,那么循环位 10 位就足够了。 - -机器三地部署我们可以用 3bit 总共 8 来表示机房位置,当前的机器 10 台,为了保证扩展到百台那么可以用 7bit 128 来表示,时间位依然是 41bit,那么还剩下 64-10-3-7-41-1 = 2bit,还剩下 2bit 可以用来进行扩展。 - -![img](https://user-gold-cdn.xitu.io/2018/9/29/16624909d2007c22?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -### 4.2. 优点 - -- 生成的 ID 都是趋势递增的。 -- 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成 ID 的性能也是非常高的。 -- 可以根据自身业务特性分配 bit 位,非常灵活。 - -### 4.3. 缺点 - -- 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。 - -### 4.4. 适用场景 - -当我们需要无序不能被猜测的 ID,并且需要一定高性能,且需要 long 型,那么就可以使用我们雪花算法。比如常见的订单 ID,用雪花算法别人就无法猜测你每天的订单量是多少。 - -### 4.5. 防止时钟回拨 - -雪花算法是强依赖于时间的,而如果机器时间发生回拨,有可能会生成重复的 ID。 - -我们可以针对算法做一些优化,来防止时钟回拨生成重复 ID。 - -用当前时间和上一次的时间进行判断,如果当前时间小于上一次的时间那么肯定是发生了回拨。普通的算法会直接抛出异常,这里我们可以对其进行优化,一般分为两个情况: - -- 如果时间回拨时间较短,比如配置 `5ms` 以内,那么可以直接等待一定的时间,让机器的时间追上来。 -- 如果时间的回拨时间较长,我们不能接受这么长的阻塞等待,那么又有两个策略: - - 直接拒绝,抛出异常。打日志,通知 RD 时钟回滚。 - - 利用扩展位。上面我们讨论过,不同业务场景位数可能用不到那么多比特位,那么我们可以把扩展位数利用起来。比如:当这个时间回拨比较长的时候,我们可以不需要等待,直接在扩展位加 1。两位的扩展位允许我们有三次大的时钟回拨,一般来说就够了,如果其超过三次我们还是选择抛出异常,打日志。 - -## 5. Leaf - -> 美团提供了一种分布式 ID 解决方案 Leaf,其本质可以视为数据库分段+服务缓存 ID。 -> -> 详情可以参考 [Leaf——美团点评分布式 ID 生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html) - -### 5.1. 基本原理 - -使用数据库生成 ID,但是做了如下改进: - -原方案每次获取 ID 都得读写一次数据库,造成数据库压力大。改为利用 proxy server 批量获取,每次获取一个 segment(step 决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力。 - 各个业务不同的发号需求用 biz_tag 字段来区分,每个 biz-tag 的 ID 获取相互隔离,互不影响。如果以后有性能需求需要对数据库扩容,不需要上述描述的复杂的扩容操作,只需要对 biz_tag 分库分表就行。 - -数据库表设计如下: - -```sql -+-------------+--------------+------+-----+-------------------+-----------------------------+ -| Field | Type | Null | Key | Default | Extra | -+-------------+--------------+------+-----+-------------------+-----------------------------+ -| biz_tag | varchar(128) | NO | PRI | | | -| max_id | bigint(20) | NO | | 1 | | -| step | int(11) | NO | | NULL | | -| desc | varchar(256) | YES | | NULL | | -| update_time | timestamp | NO | | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP | -+-------------+--------------+------+-----+-------------------+-----------------------------+ -``` - -重要字段说明: - -- `biz_tag` 用来区分业务 -- `max_id` 表示该 `biz_tag` 目前所被分配的 ID 号段的最大值 -- `step` 表示每次分配的号段长度。原来获取 ID 每次都需要写数据库,现在只需要把 `step` 设置得足够大,比如 1000。那么只有当 1000 个号被消耗完了之后才会去重新读写一次数据库。读写数据库的频率从 1 减小到了 1/step。 - -大致架构如下图所示: - -![image](https://awps-assets.meituan.net/mit-x/blog-images-bundle-2017/5e4ff128.png) - -test_tag 在第一台 Leaf 机器上是 `1~1000` 的号段,当这个号段用完时,会去加载另一个长度为 step=1000 的号段,假设另外两台号段都没有更新,这个时候第一台机器新加载的号段就应该是 `3001~4000`。同时数据库对应的 biz_tag 这条数据的 max_id 会从 3000 被更新成 4000,更新号段的 SQL 语句如下: - -```sql -Begin -UPDATE table SET max_id=max_id+step WHERE biz_tag=xxx -SELECT tag, max_id, step FROM table WHERE biz_tag=xxx -Commit -``` - -### 5.2. 优点 - -- 比数据库自增键性能高 -- 能保证键趋势递增。 -- 如果数据库宕机,由于 proxServer 有缓存,依然可以坚持一段时间。 - -### 5.3. 缺点 - -- 和主键递增一样,容易被人猜测。 -- 数据库宕机后,虽然能支撑一段时间,但是仍然会造成系统不可用。 - -### 5.4. 适用场景 - -需要趋势递增,并且 ID 大小可控制的,可以使用这套方案。 - -当然这个方案也可以通过一些手段避免被人猜测,把 ID 变成是无序的,比如把我们生成的数据是一个递增的 long 型,把这个 Long 分成几个部分,比如可以分成几组三位数,几组四位数,然后在建立一个映射表,将我们的数据变成无序。 - -## 6. 参考资料 - -- [百度分布式 ID](https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md) -- [如果再有人问你分布式 ID,这篇文章丢给他](https://juejin.im/post/5bb0217ef265da0ac2567b42) -- [理解分布式 id 生成算法 SnowFlake](https://segmentfault.com/a/1190000011282426) -- [Leaf——美团点评分布式 ID 生成系统](https://tech.meituan.com/2017/04/21/mt-leaf.html) -- [UUID 规范](https://www.ietf.org/rfc/rfc4122.txt) -- [ShardingSphere 分布式主键](https://shardingsphere.apache.org/document/current/cn/features/sharding/other-features/key-generator/) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/README.md" index c489319dce..364e3c0392 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/README.md" @@ -7,7 +7,7 @@ categories: tags: - 分布式 - 分布式调度 -permalink: /pages/ba4012/ +permalink: /pages/896ffada/ hidden: true index: false --- @@ -16,12 +16,12 @@ index: false ## 📖 内容 -- [服务路由](01.服务路由.md) - 关键词:`路由`、`条件路由`、`脚本路由`、`标签路由` -- [负载均衡](02.负载均衡.md) - 关键词:`轮询`、`随机`、`最少连接`、`源地址哈希`、`一致性哈希`、`虚拟 hash 槽` -- [流量控制](03.流量控制.md) - 关键词:`限流`、`熔断`、`降级`、`计数器法`、`时间窗口法`、`令牌桶法`、`漏桶法` - -## 📚 资料 +- [服务注册和发现](服务注册和发现.md) - 关键词:`服务注册`、`服务发现`、`元数据` +- [负载均衡](负载均衡.md) - 关键词:`轮询`、`随机`、`最少连接`、`源地址哈希`、`一致性哈希`、`虚拟 hash 槽` +- [流量控制](流量控制.md) - 关键词:`限流`、`熔断`、`降级`、`计数器法`、`时间窗口法`、`令牌桶法`、`漏桶法` +- [路由和网关](网关路由.md) - 关键词:`路由`、`条件路由`、`脚本路由`、`标签路由` +- [分布式调度面试](分布式调度面试.md) 💯 ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..edd7e4ac53 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246\351\235\242\350\257\225.md" @@ -0,0 +1,1226 @@ +--- +title: 分布式调度面试 +date: 2024-12-26 16:51:30 +categories: + - 分布式 + - 分布式调度 +tags: + - 分布式 + - 协同 + - 面试 +permalink: /pages/6af68a7a/ +--- + +# 分布式调度面试 + +## 服务注册和发现 + +### 【基础】什么是服务注册和发现? + +:::details 要点 + +服务定义是服务提供者和服务消费者之间的约定,但是在微服务架构中,如何达成这个约定呢?这就依赖于服务注册和发现机制。 + +在微服务架构下,服务注册和发现机制中主要有三种角色: + +- **服务提供者**(RPC Server / Provider) +- **服务消费者**(RPC Client / Consumer) +- **服务注册中心**(Registry) + +服务发现通常依赖于**注册中心**来协调服务发现的过程,其步骤如下: + +1. 服务提供者将接口信息以注册到注册中心。 +2. 服务消费者从注册中心读取和订阅服务提供者的地址信息。 +3. 如果有可用的服务,注册中心会主动通知服务消费者。 +4. 服务消费者根据可用服务的地址列表,调用服务提供者的接口。 + +这个过程很像是生活中的房屋租赁,房东将租房信息挂到中介公司,房客从中介公司查找租房信息。房客如果想要租房东的房子,通过中介公司牵线搭桥,联系上房东,双方谈妥签订协议,就可以正式建立起租赁关系。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220415171843.png) + +::: + +### 【中级】注册中心有哪些基本功能? + +:::details 要点 + +从服务注册和发现的流程,可以看出,**注册中心是服务发现的核心组件**。常见的注册中心组件有:Nacos、Consul、Zookeeper 等。 + +注册中心的实现主要涉及几个问题:注册中心需要提供哪些接口,该如何部署;如何存储服务信息;如何监控服务提供者节点的存活;如果服务提供者节点有变化如何通知服务消费者,以及如何控制注册中心的访问权限。 + +#### 元数据定义 + +构建微服务的首要问题是:服务提供者和服务消费者通信时,如何达成共识。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 + +常见的定义服务元数据的方式有: + +- **XML 文件** - 如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。 +- **IDL 文件** - 如果企业内部存在多个跨语言服务,建议使用 IDL 文件方式进行描述服务。 +- **REST API** - 如果存在对外开放服务调用的情形的话,使用 REST API 方式则更加通用。 + +#### 元数据存储 + +注册中心本质上是一个用于保存元数据的分布式存储。你如果明白了这一点,就会了解实现一个注册中心的所有要点都是围绕这个目标去构建的。 + +想要构建微服务,首先要解决的问题是,服务提供者如何发布一个服务,服务消费者如何引用这个服务。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 + +服务的**元数据信息**通常有以下信息: + +- 服务节点信息,如 IP、端口等。 +- 接口定义,如接口名、请求参数、响应参数等。 +- 请求失败的重试次数 +- 序列化方式 +- 压缩方式 +- 通信协议 +- 等等 + +在具体存储时,注册中心一般会按照“服务 - 分组 - 节点信息”的**层次化的结构**来存储。 + +#### 注册中心 API + +既然是分布式存储,势必要提供支持读写数据的接口,也就是 API,一般来说,需要支持以下功能: + +- **服务注册接口**:服务提供者通过调用服务注册接口来完成服务注册。 +- **服务反注册接口**:服务提供者通过调用服务反注册接口来完成服务注销。 +- **心跳汇报接口**:服务提供者通过调用心跳汇报接口完成节点存活状态上报。 +- **服务订阅接口**:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。 +- **服务变更查询接口**:服务消费者通过调用服务变更查询接口,获取最新的可用服务节点列表。 + +除此之外,为了便于管理,注册中心还必须提供一些后台管理的 API,例如: + +- **服务查询接口**:查询注册中心当前注册了哪些服务信息。 +- **服务修改接口**:修改注册中心中某一服务的信息。 + +#### 服务健康检测 + +注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。**注册中心通常使用长连接或心跳探测方式检查服务健康状态**。 + +还是以 ZooKeeper 为例,它是基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。在 ZooKeeper 中,客户端和服务端建立连接后,会话也随之建立,并生成一个全局唯一的 Session ID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT 周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT 后服务端都没有收到客户端的心跳消息,则服务端认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。 + +#### 服务状态变更通知 + +一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。注册中心通常基于服务状态订阅来实现服务状态变更通知。 + +继续以 ZooKeeper 为例,基于 ZooKeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 ZooKeeper 的 getData 方法订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData 方法来获取变更后的数据,刷新本地缓存的服务节点信息。 + +#### 集群部署 + +注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。根据 [CAP 理论](https://en.wikipedia.org/wiki/CAP_theorem),三种特性无法同时达成,必须在可用性和一致性之间做取舍。于是,根据不同侧重点,注册中心可以分为 CP 和 AP 两个阵营: + +- **CP 型注册中心** - **牺牲可用性来换取数据强一致性**,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 Raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。 +- **AP 型注册中心** - **牺牲一致性(只保证最终一致性)来换取可用性**,最典型的例子就是 Eureka、Nacos 了。对比下 Zookeeper,Eureka 不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。 + +::: + +### 【高级】注册中心有哪些扩展功能? + +:::details 要点 + +#### 多注册中心 + +对于服务消费者来说,要能够同时从多个注册中心订阅服务; + +对于服务提供者来说,要能够同时向多个注册中心注册服务。 + +#### 并行订阅服务 + +如果只支持串行订阅,如果服务消费者订阅的服务较多,并且某些服务节点的初始化连接过程中出现连接超时的情况,则后续所有的服务节点的初始化连接都需要等待它完成,这就会导致消费者启动非常慢。 + +可以每订阅一个服务就单独用一个线程来处理,这样的话即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时控制在了 30 秒以内。 + +#### 批量注销服务 + +在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。对于注册中心来说,偶发的注册调用失败对服务调用基本没有影响,其结果顶多就是某一个服务少了一个可用的节点。但偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成“僵尸节点”。 + +需要定时去清理注册中心中的“僵尸节点”,如果支持批量注销服务,就可以一次调用就把该节点上提供的所有服务同时注销掉。 + +#### 服务变更信息增量更新 + +为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新的方式,注册中心只返回变化的那部分节点信息。尤其在只有少数节点信息变更时,此举可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度避免产生网络风暴。 + +#### 心跳开关保护机制 + +在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,这时候服务消费者会频繁收到服务提供者节点变更的信息,于是就不断地请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满,尤其是注册中心是百兆网卡的情况下。 + +所以针对这种情况,**需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息**。 + +我曾经就遇到过这种情况,一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的 1/10。 + +当然打开这个开关也是有一定代价的,它会导致服务消费者感知最新的服务节点信息延迟,原先可能在 10s 内就能感知到服务提供者节点信息的变更,现在可能会延迟到几分钟,所以在网络正常的情况下,开关并不适合打开;可以作为一个紧急措施,在网络频繁抖动的时候,才打开这个开关。 + +#### 服务节点摘除保护机制 + +服务提供者在进程启动时,会注册服务到注册中心,并每隔一段时间,汇报心跳给注册中心,以标识自己的存活状态。如果隔了一段固定时间后,服务提供者仍然没有汇报心跳给注册中心,注册中心就会认为该节点已经处于“dead”状态,于是从服务的可用节点信息中移除出去。 + +如果遇到网络问题,大批服务提供者节点汇报给注册中心的心跳信息都可能会传达失败,注册中心就会把它们都从可用节点列表中移除出去,造成剩下的可用节点难以承受所有的调用,引起“雪崩”。但是这种情况下,可能大部分服务提供者节点是可用的,仅仅因为网络原因无法汇报心跳给注册中心就被“无情”的摘除了。 + +**这个时候就需要根据实际业务的情况,设定一个阈值比例,即使遇到刚才说的这种情况,注册中心也不能摘除超过这个阈值比例的节点**。 + +这个阈值比例可以根据实际业务的冗余度来确定,我通常会把这个比例设定在 20%,就是说注册中心不能摘除超过 20% 的节点。因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。 + +#### 白名单机制 + +在实际的微服务测试和部署时,通常包含多套环境,比如生产环境一套、测试环境一套。开发在进行业务自测、测试在进行回归测试时,一般都是用测试环境,部署的 RPC Server 节点注册到测试的注册中心集群。但经常会出现开发或者测试在部署时,错误的把测试环境下的服务节点注册到了线上注册中心集群,这样的话线上流量就会调用到测试环境下的 RPC Server 节点,可能会造成意想不到的后果。 + +为了防止这种情况发生,注册中心需要提供一个保护机制,你可以把注册中心想象成一个带有门禁的房间,只有拥有门禁卡的 RPC Server 才能进入。在实际应用中,注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。 + +#### 静态注册中心 + +因为服务提供者是向服务消费者提供服务的,服务是否可用,服务消费者应该比注册中心更清楚。因此,可以直接在服务消费者端,根据调用服务提供者是否成功来判定服务提供者是否可用。如果服务消费者调用某一个服务提供者节点连续失败超过一定次数,可以在本地内存中将这个节点标记为不可用。并且每隔一段固定时间,服务消费者都要向标记为不可用的节点发起保活探测,如果探测成功了,就将标记为不可用的节点再恢复为可用状态,重新发起调用。 + +::: + +## 负载均衡 + +### 【基础】什么是负载均衡?为什么需要负载均衡? + +:::details 要点 + +**“负载均衡(Load Balance,简称 LB)”是一种技术,用来在多个计算机、网络连接、CPU、磁盘驱动器或其他资源中分配负载,以达到优化资源利用率、最大化吞吐率、最小化响应时间、同时避免过载的目的**。 + +负载均衡的主要作用如下: + +- **高并发**:负载均衡可以优化资源使用率,通过算法调整负载,尽力均匀的分配资源,以此提高资源利用率、从而提升整体吞吐量。 +- **伸缩性**:发生增减资源时,负载均衡可以自动调整分发,使得应用集群具备伸缩性。 +- **高可用**:负载均衡器可以监控候选机器,当某机器不可用时,自动跳过,将请求分发给可用的机器。这使得应用集群具备高可用的特性。 +- **安全防护**:有些负载均衡软件或硬件提供了安全性功能,如:黑白名单、防火墙,防 DDos 攻击等。 + +::: + +### 【中级】负载均衡技术有哪些分类? + +:::details 要点 + +支持负载均衡的技术很多,我们可以通过不同维度去进行分类。 + +#### 载体维度分类 + +从支持负载均衡的载体来看,可以将负载均衡分为两类: + +- 硬件负载均衡 +- 软件负载均衡 + +##### 硬件负载均衡 + +硬件负载均衡,一般是在定制处理器上运行的独立负载均衡服务器,**价格昂贵,土豪专属**。 + +硬件负载均衡的**主流产品**有:[F5](https://f5.com/zh) 和 [A10](https://www.a10networks.com.cn/)。 + +硬件负载均衡的**优点**: + +- **功能强大**:支持全局负载均衡并提供较全面的、复杂的负载均衡算法。 +- **性能强悍**:硬件负载均衡由于是在专用处理器上运行,因此吞吐量大,可支持单机百万以上的并发。 +- **安全性高**:往往具备防火墙,防 DDos 攻击等安全功能。 + +硬件负载均衡的**缺点**: + +- **成本昂贵**:购买和维护硬件负载均衡的成本都很高。 +- **扩展性差**:当访问量突增时,超过限度不能动态扩容。 + +##### 软件负载均衡 + +软件负载均衡,**应用最广泛**,无论大公司还是小公司都会使用。 + +软件负载均衡从软件层面实现负载均衡,一般可以在任何标准物理设备上运行。 + +软件负载均衡的 **主流产品** 有:[Nginx](https://www.nginx.com/)、[HAProxy](http://www.haproxy.org/)、[LVS](https://github.com/alibaba/LVS)。 + +- [LVS](https://github.com/alibaba/LVS) 可以作为四层负载均衡器。其负载均衡的性能要优于 Nginx。 +- [HAProxy](http://www.haproxy.org/) 可以作为 HTTP 和 TCP 负载均衡器。 +- [Nginx](https://www.nginx.com/)、[HAProxy](http://www.haproxy.org/) 可以作为四层或七层负载均衡器。 + +软件负载均衡的 **优点**: + +- **扩展性好**:适应动态变化,可以通过添加软件负载均衡实例,动态扩展到超出初始容量的能力。 +- **成本低廉**:软件负载均衡可以在任何标准物理设备上运行,降低了购买和运维的成本。 + +软件负载均衡的 **缺点**: + +- **性能略差**:相比于硬件负载均衡,软件负载均衡的性能要略低一些。 + +#### 网络通信分类 + +软件负载均衡从通信层面来看,又可以分为四层和七层负载均衡。 + +- 七层负载均衡:就是可以根据访问用户的 HTTP 请求头、URL 信息将请求转发到特定的主机。 + - DNS 重定向 + - HTTP 重定向 + - 反向代理 +- 四层负载均衡:基于 IP 地址和端口进行请求的转发。 + - 修改 IP 地址 + - 修改 MAC 地址 + +##### DNS 负载均衡 + +DNS 负载均衡一般用于互联网公司,复杂的业务系统不适合使用。大型网站一般使用 DNS 负载均衡作为 **第一级负载均衡手段**,然后在内部使用其它方式做第二级负载均衡。DNS 负载均衡属于七层负载均衡。 + +DNS 即 **域名解析服务**,是 OSI 第七层网络协议。DNS 被设计为一个树形结构的分布式应用,自上而下依次为:根域名服务器,一级域名服务器,二级域名服务器,... ,本地域名服务器。显然,如果所有数据都存储在根域名服务器,那么 DNS 查询的负载和开销会非常庞大。 + +因此,DNS 查询相对于 DNS 层级结构,是一个逆向的递归流程,DNS 客户端依次请求本地 DNS 服务器,上一级 DNS 服务器,上上一级 DNS 服务器,... ,根 DNS 服务器(又叫权威 DNS 服务器),一旦命中,立即返回。为了减少查询次数,每一级 DNS 服务器都会设置 DNS 查询缓存。 + +DNS 负载均衡的工作原理就是:**基于 DNS 查询缓存,按照负载情况返回不同服务器的 IP 地址**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643409.png) + +DNS 重定向的 **优点**: + +- **使用简单**:负载均衡工作,交给 DNS 服务器处理,省掉了负载均衡服务器维护的麻烦 +- **提高性能**:可以支持基于地址的域名解析,解析成距离用户最近的服务器地址(类似 CDN 的原理),可以加快访问速度,改善性能; + +DNS 重定向的 **缺点**: + +- **可用性差**:DNS 解析是多级解析,新增/修改 DNS 后,解析时间较长;解析过程中,用户访问网站将失败; +- **扩展性差**:DNS 负载均衡的控制权在域名商那里,无法对其做更多的改善和扩展; +- **维护性差**:也不能反映服务器的当前运行状态;支持的算法少;不能区分服务器的差异(不能根据系统与服务的状态来判断负载) + +##### HTTP 负载均衡 + +**HTTP 负载均衡是基于 HTTP 重定向实现的**。HTTP 负载均衡属于七层负载均衡。 + +HTTP 重定向原理是:**根据用户的 HTTP 请求计算出一个真实的服务器地址,将该服务器地址写入 HTTP 重定向响应中,返回给浏览器,由浏览器重新进行访问**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643410.png) + +HTTP 重定向的 **优点**:**方案简单**。 + +HTTP 重定向的 **缺点**: + +- **额外的转发开销**:每次访问需要两次请求服务器,增加了访问的延迟。 +- **降低搜索排名**:使用重定向后,搜索引擎会视为 SEO 作弊。 +- 如果负载均衡器宕机,就无法访问该站点。 + +由于其缺点比较明显,所以这种负载均衡策略实际应用较少。 + +##### 反向代理负载均衡 + +反向代理(Reverse Proxy)方式是指以 **代理服务器** 来接受网络请求,然后 **将请求转发给内网中的服务器**,并将从内网中的服务器上得到的结果返回给网络请求的客户端。反向代理负载均衡属于七层负载均衡。 + +反向代理服务的主流产品:**Nginx**、**Apache**。 + +正向代理与反向代理有什么区别? + +- 正向代理:发生在 **客户端**,是由用户主动发起的。翻墙软件就是典型的正向代理,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。 +- 反向代理:发生在 **服务端**,用户不知道代理的存在。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643411.png) + +反向代理是如何实现负载均衡的呢?以 Nginx 为例,如下所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/web/nginx/nginx-load-balance.png) + +首先,在代理服务器上设定好负载均衡规则。然后,当收到客户端请求,反向代理服务器拦截指定的域名或 IP 请求,根据负载均衡算法,将请求分发到候选服务器上。其次,如果某台候选服务器宕机,反向代理服务器会有容错处理,比如分发请求失败 3 次以上,将请求分发到其他候选服务器上。 + +反向代理的 **优点**: + +- **多种负载均衡算法**:支持多种负载均衡算法,以应对不同的场景需求。 +- **可以监控服务器**:基于 HTTP 协议,可以监控转发服务器的状态,如:系统负载、响应时间、是否可用、连接数、流量等,从而根据这些数据调整负载均衡的策略。 + +反向代理的 **缺点**: + +- **额外的转发开销**:反向代理的转发操作本身是有性能开销的,可能会包括创建连接,等待连接响应,分析响应结果等操作。 + +- **增加系统复杂度**:反向代理常用于做分布式应用的水平扩展,但反向代理服务存在以下问题,为了解决以下问题会给系统整体增加额外的复杂度和运维成本: +- 反向代理服务如果自身宕机,就无法访问站点,所以需要有 **高可用** 方案,常见的方案有:主备模式(一主一备)、双主模式(互为主备)。 + - 反向代理服务自身也存在性能瓶颈,随着需要转发的请求量不断攀升,需要有 **可扩展** 方案。 + +##### IP 负载均衡 + +IP 负载均衡是在网络层通过修改请求目的地址进行负载均衡。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643413.png) + +如上图所示,IP 均衡处理流程大致为: + +1. 客户端请求 192.168.137.10,由负载均衡服务器接收到报文。 +2. 负载均衡服务器根据算法选出一个服务节点 192.168.0.1,然后将报文请求地址改为该节点的 IP。 +3. 真实服务节点收到请求报文,处理后,返回响应数据到负载均衡服务器。 +4. 负载均衡服务器将响应数据的源地址改负载均衡服务器地址,返回给客户端。 + +IP 负载均衡在内核进程完成数据分发,较反向代理负载均衡有更好的处理性能。但是,由于所有请求响应都要经过负载均衡服务器,集群的吞吐量受制于负载均衡服务器的带宽。 + +##### 数据链路层负载均衡 + +数据链路层负载均衡是指在通信协议的数据链路层修改 mac 地址进行负载均衡。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643412.png) + +在 Linux 平台上最好的链路层负载均衡开源产品是 LVS (Linux Virtual Server)。 + +LVS 是基于 Linux 内核中 netfilter 框架实现的负载均衡系统。netfilter 是内核态的 Linux 防火墙机制,可以在数据包流经过程中,根据规则设置若干个关卡(hook 函数)来执行相关的操作。 + +LVS 的工作流程大致如下: + +- 当用户访问 www.sina.com.cn 时,用户数据通过层层网络,最后通过交换机进入 LVS 服务器网卡,并进入内核网络层。 +- 进入 PREROUTING 后经过路由查找,确定访问的目的 VIP 是本机 IP 地址,所以数据包进入到 INPUT 链上 +- IPVS 是工作在 INPUT 链上,会根据访问的 `vip+port` 判断请求是否 IPVS 服务,如果是则调用注册的 IPVS HOOK 函数,进行 IPVS 相关主流程,强行修改数据包的相关数据,并将数据包发往 POSTROUTING 链上。 +- POSTROUTING 上收到数据包后,根据目标 IP 地址(后端服务器),通过路由选路,将数据包最终发往后端的服务器上。 + +开源 LVS 版本有 3 种工作模式,每种模式工作原理截然不同,说各种模式都有自己的优缺点,分别适合不同的应用场景,不过最终本质的功能都是能实现均衡的流量调度和良好的扩展性。主要包括三种模式:DR 模式、NAT 模式、Tunnel 模式。 + +::: + +### 【高级】负载均衡有哪些算法? + +:::details 要点 + +负载均衡器的实现可以分为两个部分: + +- 根据负载均衡算法在候选机器列表选出一个机器; +- 将请求数据发送到该机器上。 + +负载均衡算法是负载均衡服务核心中的核心。负载均衡产品多种多样,但是各种负载均衡算法原理是共性的。 + +负载均衡算法有很多种,分别适用于不同的应用场景。本章节将由浅入深的,逐一讲解各种负载均衡算法的策略和特性,并根据算法之间的互补关系将它们串联起来。 + +> 注:负载均衡算法的实现,推荐阅读 [Dubbo 官方负载均衡算法说明](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/) ,源码讲解非常详细,非常值得借鉴。 +> +> 下文中的各种算法的可执行示例已归档在 Github 仓库:[java-load-balance](https://github.com/dunwu/java-tutorial/tree/master/codes/java-distributed/java-load-balance),可以通过执行 `io.github.dunwu.javatech.LoadBalanceDemo` 查看各算法执行效果。 + +#### 轮询算法 + +**“轮询算法(Round Robin)”的策略是:将请求“依次”分发到候选机器**。 + +如下图所示,轮询负载均衡器收到来自客户端的 6 个请求,编号为 1、4 的请求会被发送到服务端 0;编号为 2、5 的请求会被发送到服务端 1;编号为 3、6 的请求会被发送到服务端 2。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250648178.png) + +**轮询算法适合的场景需要满足:各机器处理能力相近,且每个请求工作量差异不大**。 + +#### 随机算法 + +**“随机算法(Random)” 将请求“随机”分发到候选机器**。 + +如下图所示,随机负载均衡器收到来自客户端的 6 个请求,会随机分发请求,可能会出现:编号为 1、5 的请求会被发送到服务端 0;编号为 2、4 的请求会被发送到服务端 1;编号为 3、6 的请求会被发送到服务端 2。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250648899.png) + +**随机算法适合的场景需要满足:各机器处理能力相近,且每个请求工作量差异不大**。 + +学习过概率论的都知道,调用量较小的时候,可能负载并不均匀,**调用量越大,负载越均衡**。 + +#### 加权轮询/随机算法 + +轮询/随机算法适合的场景都需要满足:各机器处理能力相近,且每个请求工作量差异不大。 + +在理想状况下,假设每个机器的硬件条件相同,如:CPU、内存、网络 IO 等配置都相同;并且每个请求的耗时一样(请求传输时间、请求访问数据时间、计算时间等),这时轮询算法才能真正做到负载均衡。显然,要满足以上条件都相同是几乎不可能的,更不要说实际的网络通信中还有更多复杂的情况。 + +以上,如果有一点不能满足,都无法做到真正的负载均衡。个体存在较大差异,当请求量较大时,处理较慢的机器可能会逐渐积压请求,从而导致过载甚至宕机。 + +如下图所示,假设存在这样的场景: + +- 服务端 1 的处理能力远低于服务端 0 和服务端 2; +- 轮询/随机算法可以保证将请求尽量均匀的分发给两个机器; +- 编号为 1、4 的请求被发送到服务端 0;编号为 3、6 的请求被发送到服务端 2;二者处理能力强,应对游刃有余; +- 编号为 2、5 的请求被发送到服务端 1,服务端 1 处理能力弱,应对捉襟见肘,导致过载。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250649920.png) + +> 《蜘蛛侠》电影中有一句经典台词:**能力越大,责任越大**。显然,以上情况不符合这句话,处理能力强的机器并没有被分发到更多的请求,它的处理能力被闲置了。那么,如何解决这个问题呢? + +一种比较容易想到的思路是:引入权重属性,可以根据机器的硬件条件为其设置合理的权重值,负载均衡时,优先将请求分发到权重较高的机器。 + +“加权轮询算法(Weighted Round Robbin)” 和“加权随机算法(Weighted Random)” 都采用了加权的思路,在轮询/随机算法的基础上,引入了权重属性,优先将请求分发到权重较高的机器。这样,就可以针对性能高、处理速度快的机器设置较高的权重,让其处理更多的请求;而针对性能低、处理速度慢的机器则与之相反。一言以蔽之,加权策略强调了——能力越大,责任越大。 + +如下图所示,服务端 0 设置权重为 3,服务端 1 设置权重为 1,服务端 2 设置权重为 2。负载均衡器收到来自客户端的 6 个请求,那么编号为 1、2、5 的请求会被发送到服务端 0,编号为 4 的请求会被发送到服务端 1,编号为 3、6 的请求会被发送到机器 2。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250649943.png) + +#### 最少连接数算法 + +加权轮询/随机算法虽然一定程度上解决了机器处理能力不同时的负载均衡场景,但它最大的问题在于不能动态应对网络中负载不均的场景。加权的思路是在负载均衡处理的事前,预设好不同机器的权重,然后分发。然而,每个请求的连接时长不同,负载均衡器也不可能准确预估出请求的连接时长。因此,采用加权轮询/随机算法算法,都无法动态应对连接时长不均的网络场景,可能会出现**某些机器当前连接数过多,而另一些机器的连接过少**的情况,即并非真正的流量负载均衡。 + +如下图所示,假设存在这样的场景: + +- 3 个服务端的处理能力相同; +- 编号为 1、4 的请求被发送到服务端 0,但是 1 很快就断开连接,此时只有 4 请求连接服务端 0; +- 编号为 2、5 的请求被发送到服务端 1,但是 2 始终保持长连接;该系统继续运行时,服务端 1 发生过载; +- 编号为 3、6 的请求被发送到服务端 2,但是 3 很快就断开连接,此时只有 6 请求连接服务端 2; + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650176.png) + +既然,请求的连接时长不同,会导致有的服务端处理慢,积压大量连接数;而有的服务端处理快,保持的连接数少。那么,我们不妨想一下,如果负载均衡器监控一下服务端当前所持有的连接数,优先将请求分发给连接数少的服务端,不就能有效提高分发效率了吗?最少连接数算法正是采用这个思路去设计的。 + +**“最少连接数算法(Least Connections)” 将请求分发到连接数/请求数最少的候选机器**。 + +要根据机器连接数分发,显然要先维护机器的连接数。因此,**最少连接数算法需要实时追踪每个候选机器的活跃连接数;然后,动态选出连接数最少的机器,优先分发请求**。最少连接数算法会记录当前时刻,每个候选节点正在处理的连接数,然后选择连接数最小的节点。该策略能够动态、实时地反应机器的当前状况,较为合理地将负责分配均匀,适用于对当前系统负载较为敏感的场景。 + +由此可见,**最少连接数算法适用于对系统负载较为敏感且请求连接时长相差较大的场景**。 + +如下图所示,假设存在这样的场景: + +- 服务端 0 和服务端 1 的处理能力相同; +- 编号为 1、3 的请求被发送到服务端 0,但是 1、3 很快就断开连接; +- 编号为 2、4 的请求被发送到服务端 1,但是 2、4 保持长连接; +- 由于服务端 0 当前连接数最少,编号为 5、6 的请求被分发到服务端 0。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650852.png) + +“加权最少连接数算法(Weighted Least Connection)”在最少连接数算法的基础上,根据机器的性能为每台机器分配权重,再根据权重计算出每台机器能处理的连接数。 + +#### 最少响应时间算法 + +**“最少响应时间算法(Least Time)” 将请求分发到响应时间最短的候选机器**。最少响应时间算法和最少连接数算法二者的目标其实是殊途同归,都是动态调整,将请求尽量分发到处理能力强的机器上。不同点在于,最少连接数关注的维度是机器持有的连接数,而最少响应时间关注的维度是机器上一次响应时间哪个最短。理论上来说,持有的连接数少,响应时间短,都可以表明机器潜在的处理能力比较强。 + +**最少响应时间算法具有高度的敏感性、自适应性**。但是,由于它需要持续监控候选机器的响应时延,相比于监控候选机器的连接数,会显著增加监控的开销。此外,请求的响应时延并不一定能完全反应机器的处理能力,有可能某机器上一次处理的请求恰好是一个开销非常小的请求。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650334.png) + +#### 哈希算法 + +前面提到的负载均衡算法,都只适用于无状态应用。所谓无状态应用,意味着:请求无论分发到集群中的任意机器上,得到的响应都是相同的:然而,有状态服务则不然:请求分发到不同的机器上,得到的结果是不一样的。典型的无状态应用是普通的 Web 服务器;典型的有状态应用是各种分布式数据库(如:Redis、ElasticSearch 等),这些数据库存储了大量,乃至海量的数据,无法全部存储在一台机器上,为了提高整体容量以及吞吐量,采用了分区(分片)的设计,将数据化整为零的存储在不同机器上。 + +对于有状态应用,不仅仅需要保证负载的均衡,更为重要的是,需要保证针对相同数据的请求始终访问的是相同的机器,否则,就无法获取到正确的数据。 + +那么,如何解决有状态应用的负载均衡呢?有一种方案是哈希算法。 + +**“哈希算法(Hash)” 根据一个 key (可以是唯一 ID、IP、URL 等),通过哈希函数计算得到一个数值,用该数值在候选机器列表的进行取模运算,得到的结果便是选中的机器**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250652913.png) + +这种算法可以保证,同一关键字(IP 或 URL 等)的请求,始终会被转发到同一台机器上。哈希负载均衡算法常被用于实现会话粘滞(Sticky Session)。 + +但是 ,哈希算法的问题是:当增减节点时,由于哈希取模函数的基数发生变化,会影响大部分的映射关系,从而导致之前的数据不可访问。要解决这个问题,就必须根据新的计算公式迁移数据。显然,如果数据量很大的情况下,迁移成本很高;并且,在迁移过程中,要保证业务平滑过渡,需要使用数据双写等较为复杂的技术手段。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653034.png) + +#### 一致性哈希算法 + +哈希算法的缺点是:当集群中出现增减节点时,由于哈希取模函数的基数发生变化,会导致大量集群中的机器不可用;需要通过代价高昂的数据迁移,来解决问题。那么,我们自然会希望有一种更优化的方案,来尽量减少影响的机器数。一致性哈希算法就是为了这个目标而应运而生。 + +一致性哈希算法对哈希算法进行了改良。**“一致性哈希算法(Consistent Hash)”,根据哈希算法将对应的 key 哈希到一个具有 2^32 个桶的空间,并且头尾相连(0 到 2^32-1),即一个闭合的环形,这个圆环被称为“哈希环”**。哈希算法是对节点的数量进行取模运算;而一致性哈希算法则是对 2^32 进行取模运算。 + +**哈希环的空间是按顺时针方向组织的**,需要对指定 key 的数据进行读写时,会执行两步: + +1. 先对节点进行哈希计算,计算的关键字通常是 IP 或其他唯一标识(例:hash(ip)),然后对 2^32 取模,以确定节点在哈希环上的位置。 +2. 先对 key 进行哈希计算(hash(key)),然后对 2^32 取模,以确定 key 在哈希环上的位置。 +3. 然后根据 key 的位置,顺时针找到的第一个节点,就是 key 对应的节点。 + +所以,**一致性哈希是将“存储节点”和“数据”都映射到一个顺时针排序的哈希环上**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653412.png) + +一致性哈希算法会尽可能保证,相同的请求被分发到相同的机器上。**当出现增减节点时,只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响,不会引起剧烈变动**。 + +- **相同的请求**是指:一般在使用一致性哈希时,需要指定一个 key 用于 hash 计算,可能是:用户 ID、请求方 IP、请求服务名称,参数列表构成的串 +- **尽可能**是指:哈希环上出现增减节点时,少数机器的变化不应该影响大多数的请求。 + +(1)增加节点 + +如下图所示,假设,哈希环中新增了一个节点 S4,新增节点经过哈希计算映射到图中位置: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653974.png) + +此时,只有 K1 收到影响;而 K0、K2 均不受影响。 + +(2)减少节点 + +如下图所示,假设,哈希环中减少了一个节点 S0: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653207.png) + +此时,只有 K0 收到影响;而 K1、K2 均不受影响。 + +**一致性哈希算法并不保证节点能够在哈希环上分布均匀**,由此而产生一个问题,哈希环上可能有大量的请求集中在一个节点上。从概率角度来看,**哈希环上的节点越多,分布就越均匀**。正因为如此,一致性哈希算法不适用于节点数过少的场景。 + +如下图所示:极端情况下,可能由于节点在哈希环上分布不均,有大量请求计算得到的 key 会被集中映射到少数节点,甚至某一个节点上。此外,节点分布不均匀的情况下,进行容灾与扩容时,哈希环上的相邻节点容易受到过大影响,从而引发雪崩式的连锁反应。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250654770.png) + +#### 虚拟一致性哈希算法 + +在一致性哈希算法中,如果节点数过少,可能会分布不均,从而导致负载不均衡。在实际生产环境中,一个分布式系统应该具备良好的伸缩性,既能从容的扩展到大规模的集群,也要能支持小规模的集群。为此,又产生了虚拟哈希算法,进一步对一致性哈希算法进行了改良。 + +虚拟哈希算法的解决思路是:虽然实际的集群可能节点数较少,但是在哈希环上引入大量的虚拟哈希节点。具体来说,**“虚拟哈希算法”有二次映射:先将虚拟节点映射到哈希环上,再将虚拟节点映射到实际节点上。** + +如下图所示,假设存在这样的场景: + +- 分布式集群中有 4 个真实节点,分别是:S0、S1、S2、S3; +- 我们不妨先假定分配给哈希环 12 个虚拟节点,并将虚拟节点映射到真实节点上,映射关系如下: + - S0 - S0_0、S0_1、S0_2、S0_3 + - S1 - S1_0、S1_1、S1_2、S1_3 + - S2 - S2_0、S2_1、S2_2、S2_3 + - S3 - S3_0、S3_1、S3_2、S3_3 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250654220.png) + +通过引入虚拟哈希节点,是的哈希环上的节点分布相对均匀了。举例来说,假如此时,某请求的 key 哈希取模后,先映射到哈希环的 [S3_2, S0_0]、[S3_0, S0_1]、[S3_1, S0_2] 这三个区间的任意一点;接下来的二次映射都会匹配到真实节点 S0。 + +在实际应用中,虚拟哈希节点数一般都比较大(例如:Redis 的虚拟哈希槽有 16384 个),较大的数量保证了虚拟哈希环上的节点分布足够均匀。 + +虚拟节点除了会提高节点的均衡度,还会提高系统的稳定性。**当节点变化时,会有不同的节点共同分担系统的变化,因此稳定性更高**。例如,当某个节点被移除时,分配给该节点的多个虚拟节点会被一并移除,而这些虚拟节点按顺时针方向的下一个虚拟节点,可能会对应不同的真实节点,即这些不同的真实节点共同分担了节点变化导致的压力。 + +此外,有了虚拟节点后,可以通过调整分配给真实节点的虚拟节点数,来达到设置权重一样的效果,使得负载均衡更加灵活。 + +综上所述,**虚拟一致性哈希算法不仅适合硬件配置不同的节点的场景,而且适合节点规模会发生变化的场景**。 + +::: + +## 流量控制 + +### 【基础】什么是流量控制?为什么需要流量控制? + +:::details 要点 + +**流量控制(Flow Control)**,根据流量、并发线程数、响应时间等指标,把随机到来的流量调整成合适的形状,即**流量塑形**。避免应用被瞬时的流量高峰冲垮,从而保障应用的高可用性。 + +复杂的分布式系统架构中的应用程序往往具有数十个依赖项,每个依赖项都会不可避免地在某个时刻失败。 如果主机应用程序未与这些外部故障隔离开来,则可能会被波及。 + +例如,对于依赖于 30 个服务的应用程序,假设每个服务的正常运行时间为 99.99%,则可以期望: + +> 99.9930 = 99.7% 的正常运行时间 +> +> 10 亿个请求中的 0.3%= 3,000,000 个失败 +> +> 即使所有依赖项都具有出色的正常运行时间,每月也会有 2 个小时以上的停机时间。 +> +> 然而,现实情况一般比这种估量情况更糟糕。 + +--- + +当一切正常时,整体系统如下所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401280931974.png) + +图片来自 [Hystrix Wiki](https://github.com/Netflix/Hystrix/wiki) + +在分布式系统架构下,这些强依赖的子服务稳定与否对系统的影响非常大。但是,依赖的子服务可能有很多不可控问题:如网络连接、资源繁忙、服务宕机等。例如:下图中有一个 QPS 为 50 的依赖服务 I 出现不可用,但是其他依赖服务是可用的。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401280931939.png) + +图片来自 [Hystrix Wiki](https://github.com/Netflix/Hystrix/wiki) + +当流量很大的情况下,某个依赖的阻塞,会导致上游服务请求被阻塞。当这种级联故障愈演愈烈,就可能造成整个线上服务不可用的雪崩效应,如下图。这种情况若持续恶化,如果上游服务本身还被其他服务所依赖,就可能出现多米洛骨牌效应,导致多个服务都无法正常工作。 + +![img](https://github.com/Netflix/Hystrix/wiki/images/soa-3-640.png) + +图片来自 [Hystrix Wiki](https://github.com/Netflix/Hystrix/wiki) + +::: + +### 【基础】流量控制有哪些衡量指标? + +:::details 要点 + +::: + +### 【中级】流量控制有哪些保护机制? + +:::details 要点 + +流量控制常见的手段就是限流、熔断、降级。 + +#### 什么是降级? + +**降级**是保障服务能够稳定运行的一种保护方式:面对突增的流量,牺牲一些吞吐量以换取系统的稳定。常见的降级实现方式有:开关降级、限流降级、熔断降级。 + +#### 什么是限流? + +限流一般针对下游服务,当上游流量较大时,避免被上游服务的请求撑爆。 + +**限流**就是限制系统的输入和输出流量,以达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。 + +限流规则包含三个部分:时间粒度,接口粒度,最大限流值。限流规则设置是否合理直接影响到限流是否合理有效。 + +#### 什么是熔断? + +熔断一般针对上游服务,当下游服务超时/异常较多时,避免被下游服务拖垮。 + +当调用链路中某个资源出现不稳定,例如,超时异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。 + +熔断尽最大的可能去完成所有的请求,容忍一些失败,熔断也能自动恢复。熔断的常见策略有: + +- 在每秒请求异常数超过多少时触发熔断降级 +- 在每秒请求异常错误率超过多少时触发熔断降级 +- 在每秒请求平均耗时超过多少时触发熔断降级 + +::: + +### 流量控制有哪些衡量指标 + +:::details 要点 + +流量控制有以下几个角度: + +- 流量指标,例如 QPS、并发线程数等。 +- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系,调用来源等。 +- 控制效果,例如排队等待、直接拒绝、Warm Up(预热)等。 + +::: + +### 【中级】流量控制有哪些隔离模式? + +:::details 要点 + +线程池隔离 + +信号量隔离 + +资源隔离 + +::: + +### 【高级】有哪些限流算法? + +:::details 要点 + +常见的限流算法有:固定窗口限流算法、滑动窗口限流算法、漏桶限流算法、令牌桶限流算法。 + +#### 固定窗口限流算法 + +**固定窗口限流算法的原理** + +固定窗口限流算法的**基本策略**是: + +1. 设置一个固定时间窗口,以及这个固定时间窗口内的最大请求数; +2. 为每个固定时间窗口设置一个计数器,用于统计请求数; +3. 一旦请求数超过最大请求数,则请求会被拦截。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748006.png) + +**固定窗口限流算法的利弊** + +固定窗口限流算法的**优点**是:实现简单。 + +固定窗口限流算法的**缺点**是:存在**临界问题**。所谓临界问题,是指:流量分别集中在一个固定时间窗口的尾部和一个固定时间窗口的头部。举例来说,假设限流规则为每分钟不超过 100 次请求。在第一个时间窗口中,起初没有任何请求,在最后 1 s,收到 100 次请求,由于没有达到阈值,所有请求都通过;在第二个时间窗口中,第 1 秒就收到 100 次请求,而后续没有任何请求。虽然,这两个时间窗口内的流量都符合限流要求,但是在两个时间窗口临界的这 2s 内,实际上有 200 次请求,显然是超过预期吞吐量的,存在压垮系统的可能。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748769.png) + +#### 滑动窗口限流算法 + +**滑动窗口限流算法的原理** + +滑动窗口限流算法是对固定窗口限流算法的改进,解决了临界问题。 + +滑动窗口限流算法的**基本策略**是: + +- 将固定时间窗口分片为多个子窗口,每个子窗口的访问次数独立统计; +- 当请求时间大于当前子窗口的最大时间时,则将当前子窗口废弃,并将计时窗口向前滑动,并将下一个子窗口置为当前窗口。 +- 要保证所有子窗口的统计数之和不能超过阈值。 + +滑动窗口限流算法就是针对固定窗口限流算法的更细粒度的控制,分片越多,则限流越精准。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748277.png) + +**滑动窗口限流算法的利弊** + +滑动窗口限流算法的**优点**是:在滑动窗口限流算法中,临界位置的突发请求都会被算到时间窗口内,因此可以解决计数器算法的临界问题。 + +滑动窗口限流算法的**缺点**是: + +- **额外的内存开销** - 滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,所以存在额外的内存开销。 +- **限流的控制粒度受限于窗口分片粒度** - 滑动窗口限流算法,**只能在选定的时间粒度上限流,对选定时间粒度内的更加细粒度的访问频率不做限制**。但是,由于每个分片窗口都有额外的内存开销,所以也并不是分片数越多越好的。 + +#### 漏桶限流算法 + +**漏桶限流算法的原理** + +漏桶限流算法的**基本策略**是: + +- 水(请求)以任意速率由入口进入到漏桶中; +- 水以固定的速率由出口出水(请求通过); +- 漏桶的容量是固定的,如果水的流入速率大于流出速率,最终会导致漏桶中的水溢出(这意味着请求拒绝)。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230749486.png) + +**漏桶限流算法的利弊** + +漏桶限流算法的**优点**是:**流量速率固定**——即无论流量多大,即便是突发的大流量,处理请求的速度始终是固定的。 + +漏桶限流算法的**缺点**是:不能灵活的调整流量。例如:一个集群通过增减节点的方式,弹性伸缩了其吞吐能力,漏桶限流算法无法随之调整。 + +**漏桶策略适用于间隔性突发流量且流量不用即时处理的场景**。 + +#### 令牌桶限流算法 + +**令牌桶限流算法的原理** + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230750231.png) + +令牌桶算法的**原理**: + +1. 接口限制 T 秒内最大访问次数为 N,则每隔 T/N 秒会放一个 token 到桶中 +2. 桶内最多存放 M 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 就会被丢弃 +3. 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 则进行限流处理 + +**令牌桶限流算法的利弊** + +因为令牌桶存放了很多令牌,那么大量的突发请求会被执行,但是它不会出现临界问题,在令牌用完之后,令牌是以一个恒定的速率添加到令牌桶中的,因此不能再次发送大量突发请求。 + +规定固定容量的桶,token 以固定速度往桶内填充,当桶满时 token 不会被继续放入,每过来一个请求把 token 从桶中移除,如果桶中没有 token 不能请求。 + +**令牌桶算法适用于有突发特性的流量,且流量需要即时处理的场景**。 + +> **扩展** +> +> Guava 的 RateLimiter 工具类就是基于令牌桶算法实现,其源码分析可以参考:[RateLimiter 基于漏桶算法,但它参考了令牌桶算法](https://blog.csdn.net/forezp/article/details/100060686) + +::: + +## 网关路由 + +### 【基础】什么是服务路由?路由有什么用? + +:::details 要点 + +**服务路由**是指通过一定的规则从集群中选择合适的节点。 + +负载均衡的作用和服务路由的功能看上去很近似,二者有什么区别呢? + +负载均衡的目标是提供服务分发而不是解决路由问题,常见的**静态、动态负载均衡算法无法实现精细化的路由管理**,但是负载均衡也可以简单看做是路由方案的一种。 + +服务路由通常用于以下场景,目的在于实现流量隔离: + +- **分组调用**:一般来讲,为了保证服务的高可用性,实现异地多活的需求,一个服务往往不止部署在一个数据中心,而且出于节省成本等考虑,有些业务可能不仅在私有机房部署,还会采用公有云部署,甚至采用多家公有云部署。服务节点也会按照不同的数据中心分成不同的分组,这时对于服务消费者来说,选择哪一个分组调用,就必须有相应的路由规则。 +- **蓝绿发布**:蓝绿发布场景中,一共有两套服务群组:一套是提供旧版功能的服务群组,标记为**绿色**;另一套是提供新版功能的服务群组,标记为**蓝色**。两套服务群组都是功能完善的,并且正在运行的系统,只是服务版本和访问流量不同。新版群组(蓝色)通常是为了做内部测试、验收,不对外部用户暴露。 + - 如果新版群组(蓝色)运行稳定,并测试、验收通过后,则通过服务路由、负载均衡等手段逐步将外部用户流量导向新版群组(蓝色)。 + - 如果新版群组(蓝色)运行不稳定,或测试、验收不通过,则排查、解决问题后,再继续测试、验收。 +- **灰度发布**:灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行 A/B 测试,即让一部分用户使用特性 A,一部分用户使用特性 B:如果用户对 B 没有什么反对意见,那么逐步扩大发布范围,直到把所有用户都迁移到 B 上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。要支持灰度发布,就要求服务能够根据一定的规则,将流量隔离。 +- **流量切换**:在业务线上运行过程中,经常会遇到一些不可抗力因素导致业务故障,比如某个机房的光缆被挖断,或者发生着火等事故导致整个机房的服务都不可用。这个时候就需要按照某个指令,能够把原来调用这个机房服务的流量切换到其他正常的机房。 +- **线下测试联调**:线下测试时,可能会缺少相应环境。可以将测试应用注册到线上,然后开启路由规则,在本地进行测试。 +- **读写分离**。对于大多数互联网业务来说都是读多写少,所以在进行服务部署的时候,可以把读写分开部署,所有写接口可以部署在一起,而读接口部署在另外的节点上。 + +::: + +### 服务路由有哪些常见规则? + +:::details 要点 + +#### 条件路由 + +**条件路由是基于条件表达式的路由规则**。各个 RPC 框架的条件路由表达式各不相同。 + +我们不妨参考一下 Dubbo 的条件路由。Dubbo 的条件路由有两种配置粒度,如下: + +- **应用粒度** + + ```yaml + # app1 的消费者只能消费所有端口为 20880 的服务实例 + # app2 的消费者只能消费所有端口为 20881 的服务实例 + --- + scope: application + force: true + runtime: true + enabled: true + key: governance-conditionrouter-consumer + conditions: + - application=app1 => address=*:20880 + - application=app2 => address=*:20881 + ``` + +- **服务粒度** + + ```yaml + # DemoService 的 sayHello 方法只能消费所有端口为 20880 的服务实例 + # DemoService 的 sayHi 方法只能消费所有端口为 20881 的服务实例 + --- + scope: service + force: true + runtime: true + enabled: true + key: org.apache.dubbo.samples.governance.api.DemoService + conditions: + - method=sayHello => address=*:20880 + - method=sayHi => address=*:20881 + ``` + +> 其中,`conditions` 定义具体的路由规则内容。`conditions` 部分是规则的主体,由 1 到任意多条规则组成。详见:[Dubbo 路由规则](https://dubbo.apache.org/zh/docs/v2.7/user/examples/routing-rule/) + +Dubbo 的条件路由规则由两个条件组成,分别用于对服务消费者和提供者进行匹配。条件路由规则的格式如下: + +``` +[服务消费者匹配条件] => [服务提供者匹配条件] +``` + +- 服务消费者匹配条件:所有参数和消费者的 URL 进行对比,当消费者满足匹配条件时,对该消费者执行后面的过滤规则。 +- 服务提供者匹配条件:所有参数和提供者的 URL 进行对比,消费者最终只拿到过滤后的地址列表。 + +`condition://` 代表了这是一段用条件表达式编写的路由规则,下面是一个条件路由规则示例: + +``` +host = 10.20.153.10 => host = 10.20.153.11 +``` + +该条规则表示 IP 为 `10.20.153.10` 的服务消费者**只可**调用 IP 为 `10.20.153.11` 机器上的服务,不可调用其他机器上的服务。 + +下面列举一些 Dubbo 条件路由的典型应用场景: + +- 如果服务消费者的匹配条件为空,就表示**所有的服务消费者都可以访问**,就像下面的表达式一样。 + +``` +=> host != 10.20.153.11 +``` + +- 如果服务提供者的过滤条件为空,就表示**禁止所有的服务消费者访问**,就像下面的表达式一样。 + +``` +host = 10.20.153.10 => +``` + +- **排除某个服务节点** + +``` +=> host != 172.22.3.91 +``` + +- **白名单** + +```bash +register.ip != 10.20.153.10,10.20.153.11 => +``` + +- **黑名单** + +``` +register.ip = 10.20.153.10,10.20.153.11 => +``` + +- **只暴露部分机器节点** + +``` +=> host = 172.22.3.1*,172.22.3.2* +``` + +- **为重要应用提供额外的机器节点** + +``` +application != kylin => host != 172.22.3.95,172.22.3.96 +``` + +- **读写分离** + +``` +method = find*,list*,get*,is* => host = 172.22.3.94,172.22.3.95,172.22.3.96 +method != find*,list*,get*,is* => host = 172.22.3.97,172.22.3.98 +``` + +- **前后台分离** + +``` +application = bops => host = 172.22.3.91,172.22.3.92,172.22.3.93 +application != bops => host = 172.22.3.94,172.22.3.95,172.22.3.96 +``` + +- **隔离不同机房网段** + +``` +host != 172.22.3.* => host != 172.22.3.* +``` + +- 提供者与消费者部署在同集群内,**本机只访问本机的服务** + +``` +=> host = $host +``` + +#### 脚本路由 + +**脚本路由**是基于脚本语言的路由规则,常用的脚本语言比如 JavaScript、Groovy、JRuby 等。 + +```javascript +'script://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=' + + URL.encode('(function route(invokers) { ... } (invokers))') +``` + +这里面 `script://` 就代表了这是一段脚本语言编写的路由规则,具体规则定义在脚本语言的 route 方法实现里,比如下面这段用 JavaScript 编写的 route() 方法表达的意思是,只有 IP 为 `10.20.153.10` 的服务消费者可以发起服务调用。 + +```javascript +function route(invokers){ + var result = new java.util.ArrayList(invokers.size()); + for(i =0; i < invokers.size(); i ++){ + if("10.20.153.10".equals(invokers.get(i).getUrl().getHost())){ + result.add(invokers.get(i)); + } + } + return result; + } (invokers)); +``` + +#### 标签路由 + +**标签路由**通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。 + +标签主要是指对服务提供者的分组,目前有两种方式可以完成实例分组,分别是**动态规则打标**和**静态规则打标**。一般,动态规则优先级比静态规则更高,当两种规则同时存在且出现冲突时,将以动态规则为准。 + +以 Dubbo 的标签路由用法为例 + +(1)**动态规则打标**,可随时在**服务治理控制台**下发标签归组规则 + +```yaml +# governance-tagrouter-provider 应用增加了两个标签分组 tag1 和 tag2 +# tag1 包含一个实例 127.0.0.1:20880 +# tag2 包含一个实例 127.0.0.1:20881 +--- + force: false + runtime: true + enabled: true + key: governance-tagrouter-provider + tags: + - name: tag1 + addresses: ["127.0.0.1:20880"] + - name: tag2 + addresses: ["127.0.0.1:20881"] + ... +``` + +(2)**静态规则打标** + +```xml + +``` + +or + +```xml + +``` + +or + +```bash +java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV} +``` + +(3)**服务消费者指定标签路由** + +```java +RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1"); +``` + +请求标签的作用域为每一次 invocation,使用 `attachment` 来传递请求标签,注意保存在 `attachment` 中的值将会在一次完整的远程调用中持续传递,得益于这样的特性,我们只需要在起始调用时,通过一行代码的设置,达到标签的持续传递。 + +### 路由规则获取方式 + +路由规则的获取方式主要有三种: + +- **本地静态配置**:顾名思义就是路由规则存储在服务消费者本地上。服务消费者发起调用时,从本地固定位置读取路由规则,然后按照路由规则选取一个服务节点发起调用。 +- **配置中心管理**:这种方式下,所有的服务消费者都从配置中心获取路由规则,由配置中心来统一管理。 +- **注册中心动态下发**:这种方式下,一般是运维人员或者开发人员,通过服务治理平台修改路由规则,服务治理平台调用配置中心接口,把修改后的路由规则持久化到配置中心。因为服务消费者订阅了路由规则的变更,于是就会从配置中心获取最新的路由规则,按照最新的路由规则来执行。 + +一般来讲,**服务路由最好是存储在配置中心**,由配置中心来统一管理。这样的话,所有的服务消费者就不需要在本地管理服务路由,因为大部分的服务消费者并不关心服务路由的问题,或者说也不需要去了解其中的细节。通过配置中心,统一给各个服务消费者下发统一的服务路由,节省了沟通和管理成本。 + +但也不排除某些服务消费者有特定的需求,需要定制自己的路由规则,这个时候就适合通过本地配置来定制。 + +而动态下发可以理解为一种高级功能,它能够动态地修改路由规则,在某些业务场景下十分有用。比如某个数据中心存在问题,需要把调用这个数据中心的服务消费者都切换到其他数据中心,这时就可以通过动态下发的方式,向配置中心下发一条路由规则,将所有调用这个数据中心的请求都迁移到别的地方。 + +::: + +## 分布式任务 + +### 【中级】在 Java 中,实现一个进程内定时任务有哪些方案? + +:::details 要点 + +定时器有非常多的使用场景,例如生成年/月/周/日统计报表、财务对账、会员积分结算、邮件推送等,都是定时器的使用场景。定时器一般有三种表现形式:按固定周期定时执行、延迟一定时间后执行、指定某个时刻执行。 + +定时器的本质是设计一种数据结构,能够存储和调度任务集合,而且 deadline 越近的任务拥有更高的优先级。那么定时器如何知道一个任务是否到期了呢?定时器需要通过轮询的方式来实现,每隔一个时间片去检查任务是否到期。 + +所以定时器的内部结构一般需要一个任务队列和一个异步轮询线程,并且能够提供三种基本操作: + +- Schedule 新增任务至任务集合; +- Cancel 取消某个任务; +- Run 执行到期的任务。 + +JDK 原生提供了三种常用的定时器实现方式,分别为 `Timer`、`DelayedQueue` 和 `ScheduledThreadPoolExecutor`。 + +JDK 内置的三种实现定时器的方式,实现思路都非常相似,都离不开**任务**、**任务管理**、**任务调度**三个角色。三种定时器新增和取消任务的时间复杂度都是 `O(logn)`,面对海量任务插入和删除的场景,这三种定时器都会遇到比较严重的性能瓶颈。**对于性能要求较高的场景,一般都会采用时间轮算法来实现定时器**。 + +#### Timer + +Timer 属于 JDK 比较早期版本的实现,它可以实现固定周期的任务,以及延迟任务。`Timer` 会启动一个异步线程去执行到期的任务,任务可以只被调度执行一次,也可以周期性反复执行多次。我们先来看下 `Timer` 是如何使用的,示例代码如下。 + +```java +Timer timer = new Timer(); +timer.scheduleAtFixedRate(new TimerTask() { + @Override + public void run() { + // do something + } +}, 10000, 1000); // 10s 后调度一个周期为 1s 的定时任务 +``` + +可以看出,任务是由 `TimerTask` 类实现,`TimerTask` 是实现了 `Runnable` 接口的抽象类,`Timer` 负责调度和执行 `TimerTask`。接下来我们看下 `Timer` 的内部构造。 + +```java +public class Timer { + + private final TaskQueue queue = new TaskQueue(); + private final TimerThread thread = new TimerThread(queue); + + public Timer(String name) { + thread.setName(name); + thread.start(); + } +} +``` + +`TaskQueue` 是由数组结构实现的小根堆,deadline 最近的任务位于堆顶端,`queue[1]` 始终是最优先被执行的任务。所以使用小根堆的数据结构,`Run` 操作时间复杂度 `O(1)`,新增(`Schedule`)和取消(`Cancel`)操作的时间复杂度都是 `O(logn)`。 + +`Timer` 内部启动了一个 `TimerThread` 异步线程,不论有多少任务被加入数组,始终都是由 `TimerThread` 负责处理。`TimerThread` 会定时轮询 `TaskQueue` 中的任务,如果堆顶的任务的 deadline 已到,那么执行任务;如果是周期性任务,执行完成后重新计算下一次任务的 deadline,并再次放入小根堆;如果是单次执行的任务,执行结束后会从 `TaskQueue` 中删除。 + +`Timer` 只使用一个线程来执行任务意味着同一时间只能有一个任务得到执行,而前一个任务的延迟或者异常会影响到之后的任务。如果有一个定时任务在运行时,产生未处理的异常,那么当前这个线程就会停止,那么所有的定时任务都会停止,受到影响。 + +**不推荐使用 `Timer`** ,因为 Timer 存在以下设计缺陷: + +- Timer 是单线程模式。如果某个 TimerTask 执行时间很久,会影响其他任务的调度。 +- Timer 的任务调度是基于系统绝对时间的,如果系统时间不正确,可能会出现问题。 +- TimerTask 如果执行出现异常,Timer 并不会捕获,会导致线程终止,其他任务永远不会执行。 + +#### ScheduledExecutorService + +为了解决 `Timer` 的设计缺陷,JDK 提供了功能更加丰富的 `ScheduledThreadPoolExecutor`。`ScheduledThreadPoolExecutor` 提供了周期执行任务和延迟执行任务的特性。 + +```java +public class ScheduledExecutorServiceTest { + public static void main(String[] args) { + ScheduledExecutorService executor = Executors.newScheduledThreadPool(5); + // 1s 延迟后开始执行任务,每 2s 重复执行一次 + executor.scheduleAtFixedRate(() -> System.out.println("Hello World"), 1000, 2000, TimeUnit.MILLISECONDS); + } +} +``` + +`ScheduledThreadPoolExecutor` 继承于 `ThreadPoolExecutor`,因此它具备线程池异步处理任务的能力。线程池主要负责管理创建和管理线程,并从自身的阻塞队列中不断获取任务执行。线程池有两个重要的角色,分别是任务和阻塞队列。`ScheduledThreadPoolExecutor` 在 `ThreadPoolExecutor` 的基础上,重新设计了任务 `ScheduledFutureTask` 和阻塞队列 `DelayedWorkQueue`。`ScheduledFutureTask` 继承于 `FutureTask`,并重写了 `run()` 方法,使其具备周期执行任务的能力。`DelayedWorkQueue` 内部是优先级队列,deadline 最近的任务在队列头部。对于周期执行的任务,在执行完会重新设置时间,并再次放入队列中。 + +#### DelayedQueue + +`DelayedQueue` 是 JDK 中一种可以延迟获取对象的阻塞队列,其内部是采用优先级队列 `PriorityQueue` 存储对象。`DelayQueue` 中的每个对象都必须实现 `Delayed` 接口,并重写 `compareTo` 和 `getDelay` 方法。`DelayedQueue` 的使用方法如下: + +```java +public class DelayQueueTest { + + public static void main(String[] args) throws Exception { + + BlockingQueue delayQueue = new DelayQueue<>(); + long now = System.currentTimeMillis(); + delayQueue.put(new SampleTask(now + 1000)); + delayQueue.put(new SampleTask(now + 2000)); + delayQueue.put(new SampleTask(now + 3000)); + for (int i = 0; i < 3; i++) { + System.out.println(new Date(delayQueue.take().getTime())); + } + } + + static class SampleTask implements Delayed { + + long time; + + public SampleTask(long time) { + this.time = time; + } + + public long getTime() { + return time; + } + + @Override + + public int compareTo(Delayed o) { + return Long.compare(this.getDelay(TimeUnit.MILLISECONDS), o.getDelay(TimeUnit.MILLISECONDS)); + } + + @Override + + public long getDelay(TimeUnit unit) { + return unit.convert(time - System.currentTimeMillis(), TimeUnit.MILLISECONDS); + } + } + +} +``` + +`DelayQueue` 提供了 `put()` 和 `take()` 的阻塞方法,可以向队列中添加对象和取出对象。对象被添加到 `DelayQueue` 后,会根据 `compareTo()` 方法进行优先级排序。`getDelay()` 方法用于计算消息延迟的剩余时间,只有 `getDelay <=0` 时,该对象才能从 `DelayQueue` 中取出。 + +`DelayQueue` 在日常开发中最常用的场景就是实现重试机制。例如,接口调用失败或者请求超时后,可以将当前请求对象放入 `DelayQueue`,通过一个异步线程 `take()` 取出对象然后继续进行重试。如果还是请求失败,继续放回 `DelayQueue`。为了限制重试的频率,可以设置重试的最大次数以及采用指数退避算法设置对象的 deadline,如 2s、4s、8s、16s ……以此类推。 + +相比于 `Timer`,`DelayQueue` 只实现了任务管理的功能,需要与异步线程配合使用。`DelayQueue` 使用优先级队列实现任务的优先级排序,新增(`Schedule`)和取消(`Cancel`)操作的时间复杂度也是 `O(logn)`。 + +#### 时间轮 + +JDK 内置的三种实现定时器的方式,实现思路都非常相似,都离不开**任务**、**任务管理**、**任务调度**三个角色。三种定时器新增和取消任务的时间复杂度都是 `O(logn)`,面对海量任务插入和删除的场景,这三种定时器都会遇到比较严重的性能瓶颈。**对于性能要求较高的场景,一般都会采用时间轮算法来实现定时器**。 + +时间轮(Timing Wheel)是 George Varghese 和 Tony Lauck 在 1996 年的论文 [Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility](https://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt) 实现的,它在 Linux 内核中使用广泛,是 Linux 内核定时器的实现方法和基础之一。 + +时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot 槽位。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。 + +![图片 22.png](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/assets/CgpVE1_okKiAGl0gAAMLshtTq-M933.png) + +任务是如何添加到时间轮当中的呢?可以根据任务的到期时间进行取模,然后将任务分布到不同的 slot 中。如上图所示,时间轮被划分为 8 个 slot,每个 slot 代表 1s,当前时针指向 2。假如现在需要调度一个 3s 后执行的任务,应该加入 `2+3=5` 的 slot 中;如果需要调度一个 12s 以后的任务,需要等待时针完整走完一圈 round 零 4 个 slot,需要放入第 `(2+12)%8=6` 个 slot。 + +那么当时针走到第 6 个 slot 时,怎么区分每个任务是否需要立即执行,还是需要等待下一圈 round,甚至更久时间之后执行呢?所以我们需要把 round 信息保存在任务中。例如图中第 6 个 slot 的链表中包含 3 个任务,第一个任务 round=0,需要立即执行;第二个任务 round=1,需要等待 `1*8=8s` 后执行;第三个任务 round=2,需要等待 `2*8=8s` 后执行。所以当时针转动到对应 slot 时,只执行 round=0 的任务,slot 中其余任务的 round 应当减 1,等待下一个 round 之后执行。 + +上面介绍了时间轮算法的基本理论,可以看出时间轮有点类似 HashMap,如果多个任务如果对应同一个 slot,处理冲突的方法采用的是拉链法。在任务数量比较多的场景下,适当增加时间轮的 slot 数量,可以减少时针转动时遍历的任务个数。 + +时间轮定时器最大的优势就是,任务的新增和取消都是 O(1) 时间复杂度,而且只需要一个线程就可以驱动时间轮进行工作。 + +HashedWheelTimer 是 Netty 中时间轮算法的实现类。 + +::: + +### 【中级】分布式定时任务有哪些方案? + +:::details 要点 + +分布式定时任务常见方案有: + +- Quartz +- XXL-Job +- ElasticJob + +#### Quartz + +Quartz 是一个经典的开源定时调度框架。它支持进程内调度和分布式调度。 + +Quartz 提供两种基本作业存储类型: + +- **RAMJobStore** - 在默认情况下 Quartz 将任务调度的运行信息保存在内存中,这种方法提供了最佳的性能,因为内存中数据访问最快。不足之处是缺乏数据的持久性,当程序路途停止或系统崩溃时,所有运行的信息都会丢失。 +- **JobStoreTX** - 所有的任务信息都会保存到数据库中,可以控制事物,还有就是如果应用服务器关闭或者重启,任务信息都不会丢失,并且可以恢复因服务器关闭或者重启而导致执行失败的任务 + +#### XXL-Job + +[xxl-job](https://github.com/xuxueli/xxl-job) 是一个分布式任务调度平台。 + +**设计思想** + +将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“调度中心”负责发起调度请求。 + +将任务抽象成分散的 JobHandler,交由“执行器”统一管理,“执行器”负责接收调度请求并执行对应的 JobHandler 中业务逻辑。 + +因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性; + +**系统组成** + +- **调度模块(调度中心)**: + 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; + 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,GLUE 开发和任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器 Failover。 +- **执行模块(执行器)**: + 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; + 接收“调度中心”的执行请求、终止请求和日志请求等。 + +![输入图片说明](https://www.xuxueli.com/doc/static/xxl-job/images/img_Qohm.png) + +#### ElasticJob + +两个相互独立的子项目 ElasticJob-Lite 和 ElasticJob-Cloud 组成。 它通过弹性调度、资源管控、以及作业治理的功能,打造一个适用于互联网场景的分布式调度解决方案,并通过开放的架构设计,提供多元化的作业生态。 它的各个产品使用统一的作业 API,开发者仅需一次开发,即可随意部署。 + +ElasticJob 采用去中心化架构,没有作业调度中心。它以框架的形式,集成到应用中,提供调度服务。 + +ElasticJob-Lite 定位为轻量级无中心化解决方案,使用 jar 的形式提供分布式任务的协调服务。 + +[![ElasticJob Architecture](https://shardingsphere.apache.org/elasticjob/current/img/architecture/elasticjob_lite.png)](https://shardingsphere.apache.org/elasticjob/current/img/architecture/elasticjob_lite.png) + +ElasticJob-Cloud 采用自研 Mesos Framework 的解决方案,额外提供资源治理、应用分发以及进程隔离等功能。 + +![ElasticJob-Cloud Architecture](https://shardingsphere.apache.org/elasticjob/current/img/architecture/elasticjob_cloud.png) + +ElasticJob-Lite 和 ElasticJob-Cloud 对比: + +| ElasticJob-Lite | ElasticJob-Cloud | | +| :-------------- | :--------------- | ------------------- | +| 无中心化 | 是 | `否` | +| 资源分配 | 不支持 | `支持` | +| 作业模式 | 常驻 | `常驻 + 瞬时` | +| 部署依赖 | ZooKeeper | `ZooKeeper + Mesos` | + +ElasticJob-Cloud 的优势在于对资源细粒度治理,适用于需要削峰填谷的大数据系统。 + +::: diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/11.\346\234\215\345\212\241\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\234\215\345\212\241\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" similarity index 63% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/11.\346\234\215\345\212\241\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\234\215\345\212\241\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" index 8b65f41323..b28173a3b1 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/11.\346\234\215\345\212\241\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\234\215\345\212\241\346\263\250\345\206\214\345\222\214\345\217\221\347\216\260.md" @@ -1,96 +1,91 @@ --- title: 服务注册和发现 -date: 2022-04-18 19:34:47 -order: 11 +date: 2024-05-27 06:57:09 categories: - 分布式 - - 分布式通信 - - RPC - - RPC综合 + - 分布式调度 tags: - 分布式 - 服务治理 + - 调度 - 服务注册 - 服务发现 - - CAP -permalink: /pages/1a90aa/ +permalink: /pages/91edb7c3/ --- # 服务注册和发现 -## 服务元数据 +## 服务注册和发现的基本原理 -构建微服务的首要问题是:服务提供者和服务消费者通信时,如何达成共识。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 +服务定义是服务提供者和服务消费者之间的约定,但是在微服务架构中,如何达成这个约定呢?这就依赖于服务注册和发现机制。 -服务的元数据信息通常有以下信息: +### 注册和发现的角色 -- 服务节点信息,如 IP、端口等。 -- 接口定义,如接口名、请求参数、响应参数等。 -- 请求失败的重试次数 -- 序列化方式 -- 压缩方式 -- 通信协议 -- 等等 +在微服务架构下,服务注册和发现机制中主要有三种角色: -常见的发布服务元数据的方式有: +- **服务提供者**(RPC Server / Provider) +- **服务消费者**(RPC Client / Consumer) +- **服务注册中心**(Registry) -- REST API -- XML 文件 -- IDL 文件 +服务发现通常依赖于**注册中心**来协调服务发现的过程,其步骤如下: -### REST API +1. 服务提供者将接口信息注册到注册中心。 +2. 服务消费者从注册中心读取和订阅服务提供者的地址信息。 +3. 如果有可用的服务,注册中心会主动通知服务消费者。 +4. 服务消费者根据可用服务的地址列表,调用服务提供者的接口。 -以 Eureka 为例 +这个过程很像是生活中的房屋租赁,房东将租房信息挂到中介公司,房客从中介公司查找租房信息。房客如果想要租房东的房子,通过中介公司牵线搭桥,联系上房东,双方谈妥签订协议,就可以正式建立起租赁关系。 -服务提供者定义接口 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220415171843.png) -```java -@RestController -public class ProviderController { +主流的服务注册与发现的解决方案,主要有两种: - private final DiscoveryClient discoveryClient; +- **应用内注册与发现**:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。 +- **应用外注册与发现**:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。 - public ProviderController(DiscoveryClient discoveryClient) { - this.discoveryClient = discoveryClient; - } +### 应用内注册与发现 - @GetMapping("/send") - public String send() { - String services = "Services: " + discoveryClient.getServices(); - System.out.println(services); - return services; - } +**应用内注册与发现**方案是:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。最典型的案例要属 Netflix 开源的 Eureka,官方架构图如下: -} -``` +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220418204148.jfif) -服务消费者消费接口 +Eureka 的架构主要由三个重要的组件组成: -``` -@RestController -public class ConsumerController { +- **Eureka Server**:注册中心的服务端,实现了服务信息注册、存储以及查询等功能。 +- **服务端的 Eureka Client**:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能。 +- **客户端的 Eureka Client**:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能。 - private final LoadBalancerClient loadBalancerClient; - private final RestTemplate restTemplate; +### 应用外注册与发现 - public ConsumerController(LoadBalancerClient loadBalancerClient, - RestTemplate restTemplate) { - this.loadBalancerClient = loadBalancerClient; - this.restTemplate = restTemplate; - } +**应用外注册与发现**方案是:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。最典型的案例是开源注册中心 Consul。 - @GetMapping("/recv") - public String recv() { - ServiceInstance serviceInstance = loadBalancerClient.choose("eureka-provider"); - String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/send"; - System.out.println(url); - return restTemplate.getForObject(url, String.class); - } +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220418204352.png) -} -``` +Consul 实现应用外服务注册和发现主要依靠三个重要的组件: + +- Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务。 +- [Registrator](https://github.com/gliderlabs/registrator):一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。 +- [Consul Template](https://github.com/hashicorp/consul-template):定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息。 + +## 注册中心的基本功能 + +从服务注册和发现的流程,可以看出,**注册中心是服务发现的核心组件**。常见的注册中心组件有:Nacos、Consul、Zookeeper 等。 + +注册中心的实现主要涉及几个问题:注册中心需要提供哪些接口,该如何部署;如何存储服务信息;如何监控服务提供者节点的存活;如果服务提供者节点有变化如何通知服务消费者,以及如何控制注册中心的访问权限。 + +### 元数据定义 + +构建微服务的首要问题是:服务提供者和服务消费者通信时,如何达成共识。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 + +常见的定义服务元数据的方式有: + +- **XML 文件** - 如果只是企业内部之间的服务调用,并且都是 Java 语言的话,选择 XML 配置方式是最简单的。 +- **IDL 文件** - 如果企业内部存在多个跨语言服务,建议使用 IDL 文件方式进行描述服务。 +- **REST API** - 如果存在对外开放服务调用的情形的话,使用 REST API 方式则更加通用。 -### XML 文件 +#### XML 文件 + +**XML 配置方式通过在服务提供者和服务消费者之间维持一份对等的 XML 配置文件,来保证服务消费者按照服务提供者的约定来进行服务调用**。在这种方式下,如果服务提供者变更了接口定义,不仅需要更新服务提供者加载的接口描述文件 server.xml,还需要同时更新服务消费者加载的接口描述文件 client.xml。但这种方式对业务代码侵入性比较高,XML 配置有变更的时候,服务消费者和服务提供者都要更新,所以适合公司内部联系比较紧密的业务之间采用。支持 XML 文件的主流 RPC 有:阿里的 [Dubbo](https://github.com/apache/dubbo)(XML 配置示例:[基于 Spring XML 开发微服务应用](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/quick-start/spring-xml/))、微博的 Motan。 XML 文件这种方式的服务发布和引用主要分三个步骤: @@ -99,7 +94,7 @@ XML 文件这种方式的服务发布和引用主要分三个步骤: ```java // The demo service definition. service DemoService { - rpc SayHello (HelloRequest) returns (HelloReply) {} + rpc sayHello (HelloRequest) returns (HelloReply) {} } // The request message containing the user's name. @@ -131,8 +126,6 @@ message HelloReply { (3)服务消费者进程启动时,通过加载 xml 配置文件来引入要调用的接口。 -consumer.xml 示例 - ```xml ``` -### IDL 文件 +#### IDL 文件 -IDL 就是接口描述语言(interface description language)的缩写,通过一种中立的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。 +IDL 就是接口描述语言(interface description language)的缩写,通过一种中立、通用的方式来描述接口,使得在不同的平台上运行的对象和不同语言编写的程序可以相互通信交流。也就是说,**IDL 主要用于跨语言的服务之间的调用**。支持 IDL 文件的主流 RPC 有:阿里的 [Dubbo](https://github.com/apache/dubbo)(XML 配置示例:[IDL 定义跨语言服务](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/quick-start/idl/)),Facebook 的 [Thrift](https://github.com/apache/thrift),Google 的 [gRPC](https://github.com/grpc/grpc) 。 -也就是说 IDL 主要是**用作跨语言平台的服务之间的调用**,有两种最常用的 IDL:一个是 Facebook 开源的**Thrift 协议**,另一个是 Google 开源的**gRPC 协议**。 - -以 gRPC 协议为例,gRPC 协议使用 Protobuf 简称 proto 文件来定义接口名、调用参数以及返回值类型。 - -比如文件 helloword.proto 定义了一个接口 SayHello 方法,它的请求参数是 HelloRequest,它的返回值是 HelloReply。 +以 gRPC 协议为例,gRPC 协议使用 Protobuf 简称 proto 文件来定义接口名、调用参数以及返回值类型。比如文件 helloword.proto 定义了一个接口 SayHello 方法,它的请求参数是 HelloRequest,它的返回值是 HelloReply。 ```java // The greeter service definition. @@ -198,7 +187,7 @@ private class GreeterImpl extends GreeterGrpc.GreeterImplBase { 假如服务消费者使用的也是 Java 语言,那么利用 protoc 插件即可自动生成 Client 端的 Java 代码。 -``` +```java public void greet(String name) { logger.info("Will try to greet " + name + " ..."); HelloRequest request = HelloRequest.newBuilder().setName(name).build(); @@ -226,53 +215,65 @@ public void greet(String name) { 有一点特别需要注意的是,在描述接口定义时,IDL 文件需要对接口返回值进行详细定义。如果接口返回值的字段比较多,并且经常变化时,采用 IDL 文件方式的接口定义就不太合适了。一方面可能会造成 IDL 文件过大难以维护,另一方面只要 IDL 文件中定义的接口返回值有变更,都需要同步所有的服务消费者都更新,管理成本就太高了。 -## 服务注册和发现基本原理 - -服务发现通常依赖于**注册中心**来协调服务发现的过程,其步骤如下: - -1. 服务提供者将接口信息以注册到注册中心。 -2. 服务消费者从注册中心读取和订阅服务提供者的地址信息。 -3. 如果有可用的服务,注册中心会主动通知服务消费者。 -4. 服务消费者根据可用服务的地址列表,调用服务提供者的接口。 +#### REST API -这个过程很像是生活中的房屋租赁,房东将租房信息挂到中介公司,房客从中介公司查找租房信息。房客如果想要租房东的房子,通过中介公司牵线搭桥,联系上房东,双方谈妥签订协议,就可以正式建立起租赁关系。 +REST API 方式主要被用作 HTTP 或者 HTTPS 协议的接口定义,即使在非微服务架构体系下,也被广泛采用。由于 HTTP 本身就是公开标准网络协议,所以几乎没有什么额外学习成本。支持 REST API 的主流 RPC 有:Eureka,下面以 Eureka 为例。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220415171843.png) +服务提供者定义接口 -主流的服务注册与发现的解决方案,主要有两种: +```java +@RestController +public class ProviderController { -- **应用内注册与发现**:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。 -- **应用外注册与发现**:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。 + private final DiscoveryClient discoveryClient; -### 应用内注册与发现 + public ProviderController(DiscoveryClient discoveryClient) { + this.discoveryClient = discoveryClient; + } -**应用内注册与发现**方案是:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。最典型的案例要属 Netflix 开源的 Eureka,官方架构图如下: + @GetMapping("/send") + public String send() { + String services = "Services: " + discoveryClient.getServices(); + System.out.println(services); + return services; + } -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220418204148.jfif) +} +``` -Eureka 的架构主要由三个重要的组件组成: +服务消费者消费接口 -- **Eureka Server**:注册中心的服务端,实现了服务信息注册、存储以及查询等功能。 -- **服务端的 Eureka Client**:集成在服务端的注册中心 SDK,服务提供者通过调用 SDK,实现服务注册、反注册等功能。 -- **客户端的 Eureka Client**:集成在客户端的注册中心 SDK,服务消费者通过调用 SDK,实现服务订阅、服务更新等功能。 +```java +@RestController +public class ConsumerController { -### 应用外注册与发现 + private final LoadBalancerClient loadBalancerClient; + private final RestTemplate restTemplate; -**应用外注册与发现**方案是:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。最典型的案例是开源注册中心 Consul。 + public ConsumerController(LoadBalancerClient loadBalancerClient, + RestTemplate restTemplate) { + this.loadBalancerClient = loadBalancerClient; + this.restTemplate = restTemplate; + } -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220418204352.png) + @GetMapping("/recv") + public String recv() { + ServiceInstance serviceInstance = loadBalancerClient.choose("eureka-provider"); + String url = "http://" + serviceInstance.getHost() + ":" + serviceInstance.getPort() + "/send"; + System.out.println(url); + return restTemplate.getForObject(url, String.class); + } -Consul 实现应用外服务注册和发现主要依靠三个重要的组件: +} +``` -- Consul:注册中心的服务端,实现服务注册信息的存储,并提供注册和发现服务。 -- [Registrator](https://github.com/gliderlabs/registrator):一个开源的第三方服务管理器项目,它通过监听服务部署的 Docker 实例是否存活,来负责服务提供者的注册和销毁。 -- [Consul Template](https://github.com/hashicorp/consul-template):定时从注册中心服务端获取最新的服务提供者节点列表并刷新 LB 配置(比如 Nginx 的 upstream),这样服务消费者就通过访问 Nginx 就可以获取最新的服务提供者信息。 +### 元数据存储 -## 注册中心基本功能 +注册中心本质上是一个用于保存元数据的分布式存储。你如果明白了这一点,就会了解实现一个注册中心的所有要点都是围绕这个目标去构建的。 -从服务注册和发现的流程,可以看出,**注册中心是服务发现的核心组件**。常见的注册中心组件有:Nacos、Consul、Zookeeper 等。 +想要构建微服务,首先要解决的问题是,服务提供者如何发布一个服务,服务消费者如何引用这个服务。具体来说,就是这个服务的接口名是什么?调用这个服务需要传递哪些参数?接口的返回值是什么类型?以及一些其他接口描述信息。 -注册中心是用来存储服务的元数据信息。服务的元数据信息通常有以下信息: +服务的**元数据信息**通常有以下信息: - 服务节点信息,如 IP、端口等。 - 接口定义,如接口名、请求参数、响应参数等。 @@ -282,13 +283,17 @@ Consul 实现应用外服务注册和发现主要依靠三个重要的组件: - 通信协议 - 等等 -在具体存储时,一般会按照“服务 - 分组 - 节点信息”三层结构来存储。 +在具体存储时,注册中心一般会按照“服务 - 分组 - 节点信息”的**层次化的结构**来存储。以 ZooKeeper 为例: -注册中心的实现主要涉及几个问题:注册中心需要提供哪些接口,该如何部署;如何存储服务信息;如何监控服务提供者节点的存活;如果服务提供者节点有变化如何通知服务消费者,以及如何控制注册中心的访问权限。 +- 在 ZooKeeper 中,数据按目录层级存储,每个目录叫作 znode,并且其有一个唯一的路径标识。 +- znode 可以包含数据和子 znode。 +- znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_1.png) ### 注册中心 API -根据注册中心原理的描述,注册中心必须提供以下最基本的 API,例如: +既然是分布式存储,势必要提供支持读写数据的接口,也就是 API,一般来说,需要支持以下功能: - **服务注册接口**:服务提供者通过调用服务注册接口来完成服务注册。 - **服务反注册接口**:服务提供者通过调用服务反注册接口来完成服务注销。 @@ -301,9 +306,24 @@ Consul 实现应用外服务注册和发现主要依靠三个重要的组件: - **服务查询接口**:查询注册中心当前注册了哪些服务信息。 - **服务修改接口**:修改注册中心中某一服务的信息。 +### 服务健康检测 + +注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。**注册中心通常使用长连接或心跳探测方式检查服务健康状态**。 + +还是以 ZooKeeper 为例,它是基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。在 ZooKeeper 中,客户端和服务端建立连接后,会话也随之建立,并生成一个全局唯一的 Session ID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT 周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT 后服务端都没有收到客户端的心跳消息,则服务端认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。 + +### 服务状态变更通知 + +一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。注册中心通常基于服务状态订阅来实现服务状态变更通知。 + +继续以 ZooKeeper 为例,基于 ZooKeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 ZooKeeper 的 getData 方法订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData 方法来获取变更后的数据,刷新本地缓存的服务节点信息。 + ### 集群部署 -注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。 +注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。根据 [CAP 理论](https://en.wikipedia.org/wiki/CAP_theorem),三种特性无法同时达成,必须在可用性和一致性之间做取舍。于是,根据不同侧重点,注册中心可以分为 CP 和 AP 两个阵营: + +- **CP 型注册中心** - **牺牲可用性来换取数据强一致性**,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 Raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。 +- **AP 型注册中心** - **牺牲一致性(只保证最终一致性)来换取可用性**,最典型的例子就是 Eureka 了。对比下 Zookeeper,Eureka 不用选举一个 Leader,每个 Eureka 服务器单独保存服务注册地址,因此有可能出现数据信息不一致的情况。但是当网络出现问题的时候,每台服务器都可以完成独立的服务。 以开源注册中心 ZooKeeper 为例,ZooKeeper 集群中包含多个节点,服务提供者和服务消费者可以同任意一个节点通信,因为它们的数据一定是相同的,这是为什么呢?这就要从 ZooKeeper 的工作原理说起: @@ -316,45 +336,39 @@ Consul 实现应用外服务注册和发现主要依靠三个重要的组件: ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_3.png) -### 元数据存储 - -注册中心存储服务信息一般采用层次化的目录结构,以 ZooKeeper 为例: - -- 每个目录在 ZooKeeper 中叫作 znode,并且其有一个唯一的路径标识。 -- znode 可以包含数据和子 znode。 -- znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。 +## 注册中心的扩展功能 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/zookeeper/zookeeper_1.png) +### 多注册中心 -### 白名单机制 +对于服务消费者来说,要能够同时从多个注册中心订阅服务; -在实际的微服务测试和部署时,通常包含多套环境,比如生产环境一套、测试环境一套。开发在进行业务自测、测试在进行回归测试时,一般都是用测试环境,部署的 RPC Server 节点注册到测试的注册中心集群。但经常会出现开发或者测试在部署时,错误的把测试环境下的服务节点注册到了线上注册中心集群,这样的话线上流量就会调用到测试环境下的 RPC Server 节点,可能会造成意想不到的后果。 +对于服务提供者来说,要能够同时向多个注册中心注册服务。 -为了防止这种情况发生,注册中心需要提供一个保护机制,你可以把注册中心想象成一个带有门禁的房间,只有拥有门禁卡的 RPC Server 才能进入。在实际应用中,注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。 +### 并行订阅服务 -## 服务健康检测 +如果只支持串行订阅,如果服务消费者订阅的服务较多,并且某些服务节点的初始化连接过程中出现连接超时的情况,则后续所有的服务节点的初始化连接都需要等待它完成,这就会导致消费者启动非常慢。 -注册中心除了要支持最基本的服务注册和服务订阅功能以外,还必须具备对服务提供者节点的健康状态检测功能,这样才能保证注册中心里保存的服务节点都是可用的。 +可以每订阅一个服务就单独用一个线程来处理,这样的话即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时控制在了 30 秒以内。 -还是以 ZooKeeper 为例,它是基于 ZooKeeper 客户端和服务端的长连接和会话超时控制机制,来实现服务健康状态检测的。 +### 批量注销服务 -在 ZooKeeper 中,客户端和服务端建立连接后,会话也随之建立,并生成一个全局唯一的 Session ID。服务端和客户端维持的是一个长连接,在 SESSION_TIMEOUT 周期内,服务端会检测与客户端的链路是否正常,具体方式是通过客户端定时向服务端发送心跳消息(ping 消息),服务器重置下次 SESSION_TIMEOUT 时间。如果超过 SESSION_TIMEOUT 后服务端都没有收到客户端的心跳消息,则服务端认为这个 Session 就已经结束了,ZooKeeper 就会认为这个服务节点已经不可用,将会从注册中心中删除其信息。 +在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。对于注册中心来说,偶发的注册调用失败对服务调用基本没有影响,其结果顶多就是某一个服务少了一个可用的节点。但偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成“僵尸节点”。 -### 服务状态变更通知 +需要定时去清理注册中心中的“僵尸节点”,如果支持批量注销服务,就可以一次调用就把该节点上提供的所有服务同时注销掉。 -一旦注册中心探测到有服务提供者节点新加入或者被剔除,就必须立刻通知所有订阅该服务的服务消费者,刷新本地缓存的服务节点信息,确保服务调用不会请求不可用的服务提供者节点。 +### 服务变更信息增量更新 -继续以 ZooKeeper 为例,基于 ZooKeeper 的 Watcher 机制,来实现服务状态变更通知给服务消费者的。服务消费者在调用 ZooKeeper 的 getData 方法订阅服务时,还可以通过监听器 Watcher 的 process 方法获取服务的变更,然后调用 getData 方法来获取变更后的数据,刷新本地缓存的服务节点信息。 +为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新的方式,注册中心只返回变化的那部分节点信息。尤其在只有少数节点信息变更时,此举可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度**避免产生网络风暴**。 ### 心跳开关保护机制 在网络频繁抖动的情况下,注册中心中可用的节点会不断变化,这时候服务消费者会频繁收到服务提供者节点变更的信息,于是就不断地请求注册中心来拉取最新的可用服务节点信息。当有成百上千个服务消费者,同时请求注册中心获取最新的服务提供者的节点信息时,可能会把注册中心的带宽给占满,尤其是注册中心是百兆网卡的情况下。 -针对这种情况,**需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息**。 +所以针对这种情况,**需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息**。 -一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的 1/10。当然打开这个开关也是有一定代价的,它会导致服务消费者感知最新的服务节点信息延迟,原先可能在 10s 内就能感知到服务提供者节点信息的变更,现在可能会延迟到几分钟,所以在网络正常的情况下,开关并不适合打开;可以作为一个紧急措施,在网络频繁抖动的时候,才打开这个开关。 +我曾经就遇到过这种情况,一个可行的解决方案就是给注册中心设置一个开关,当开关打开时,即使网络频繁抖动,注册中心也不会通知所有的服务消费者有服务节点信息变更,比如只给 10% 的服务消费者返回变更,这样的话就能将注册中心的请求量减少到原来的 1/10。 -心跳开关保护机制,是为了防止服务提供者节点频繁变更导致的服务消费者同时去注册中心获取最新服务节点信息。 +当然打开这个开关也是有一定代价的,它会导致服务消费者感知最新的服务节点信息延迟,原先可能在 10s 内就能感知到服务提供者节点信息的变更,现在可能会延迟到几分钟,所以在网络正常的情况下,开关并不适合打开;可以作为一个紧急措施,在网络频繁抖动的时候,才打开这个开关。 ### 服务节点摘除保护机制 @@ -366,60 +380,17 @@ Consul 实现应用外服务注册和发现主要依靠三个重要的组件: 这个阈值比例可以根据实际业务的冗余度来确定,我通常会把这个比例设定在 20%,就是说注册中心不能摘除超过 20% 的节点。因为大部分情况下,节点的变化不会这么频繁,只有在网络抖动或者业务明确要下线大批量节点的情况下才有可能发生。而业务明确要下线大批量节点的情况是可以预知的,这种情况下可以关闭阈值保护;而正常情况下,应该打开阈值保护,以防止网络抖动时,大批量可用的服务节点被摘除。 -服务节点摘除保护机制,是为了防止服务提供者节点被大量摘除引起服务消费者可以调用的节点不足。 - -### 静态注册中心 - -因为服务提供者是向服务消费者提供服务的,是否可用服务消费者应该比注册中心更清楚,因此可以直接在服务消费者端根据调用服务提供者是否成功来判定服务提供者是否可用。如果服务消费者调用某一个服务提供者节点连续失败超过一定次数,可以在本地内存中将这个节点标记为不可用。并且每隔一段固定时间,服务消费者都要向标记为不可用的节点发起保活探测,如果探测成功了,就将标记为不可用的节点再恢复为可用状态,重新发起调用。 - -## 注册中心选型 - -注册中心选型时最需要关注两个问题:**高可用性**和**数据一致性**。 - -### 高可用性 - -注册中心作为服务提供者和服务消费者之间沟通的纽带,它的高可用性十分重要。如果注册中心不可用了,那么服务提供者就无法对外暴露自己的服务,而服务消费者也无法知道自己想要调用的服务的具体地址。 - -实现高可用性的手段主要有两种 - -- **集群部署**,即使有部分机器宕机,将请求分发到正常的机器上就可以保证服务的正常访问。 -- **多机房部署**,避免一个机房因为断电或者光缆被挖断等不可抗力因素不可用时,仍然可以通过把请求迁移到其他机房来保证服务的正常访问。 - -这两种手段本质上都是服务冗余。 - -### 数据一致性 - -## 服务发现的问题 - -### 多注册中心 - -理想情况下,如果始终只有一个注册中心,那么整个交互非常简单。但在实际工作中,往往需要对接多个注册中心,常见场景如下: - -- **服务消费者订阅多个注册中心**:服务消费者可能订阅了多个服务,多个服务可能是由多个业务部门提供的,而且每个业务部门都有自己的注册中心,提供的服务只在自己的注册中心里有记录。这就要求服务消费者要具备在启动时,能够从多个注册中心订阅服务的能力。 -- **服务提供者注册多个注册中心**:一个服务提供者提供了某个服务,可能作为静态服务对外提供,也可能作为动态服务对外提供,这两个服务部署在不同的注册中心,所以要求服务提供者在启动的时候,要能够同时向多个注册中心注册服务。 - -### 并行订阅服务 - -通常一个服务消费者订阅了不止一个服务。如果采用串行订阅方式,即每订阅一个服务,服务消费者就调用一次注册中心的订阅接口,获取这个服务的节点列表并初始化连接,就可能要执行很多次这样的过程。在某些服务节点的初始化连接过程中,出现连接超时的情况,后续所有的服务节点的初始化连接都需要等待它完成,导致服务消费者启动变慢,最后耗费了将近五分钟时间来完成所有服务节点的初始化连接过程。 - -由于以上问题,所以服务发现应该支持并行订阅,每订阅一个服务就单独用一个线程来处理,这样的话即使遇到个别服务节点连接超时,其他服务节点的初始化连接也不受影响,最慢也就是这个服务节点的初始化连接耗费的时间,最终所有服务节点的初始化连接耗时控制在一个可以接受的时间范围内。 - -### 批量注销服务 - -通常一个服务提供者节点提供不止一个服务,所以注册和反注册都需要多次调用注册中心。在与注册中心的多次交互中,可能由于网络抖动、注册中心集群异常等原因,导致个别调用失败。对于注册中心来说,偶发的注册调用失败对服务调用基本没有影响,其结果顶多就是某一个服务少了一个可用的节点。但偶发的反注册调用失败会导致不可用的节点残留在注册中心中,变成“僵尸节点”,但服务消费者端还会把它当成“活节点”,继续发起调用,最终导致调用失败。 - -可以通过调用注册中心提供的批量反注册接口,一次调用就可以把该节点上提供的所有服务同时注销掉,从而避免了“僵尸节点”的出现。 +### 白名单机制 -### 服务变更信息的增量更新 +在实际的微服务测试和部署时,通常包含多套环境,比如生产环境一套、测试环境一套。开发在进行业务自测、测试在进行回归测试时,一般都是用测试环境,部署的 RPC Server 节点注册到测试的注册中心集群。但经常会出现开发或者测试在部署时,错误的把测试环境下的服务节点注册到了线上注册中心集群,这样的话线上流量就会调用到测试环境下的 RPC Server 节点,可能会造成意想不到的后果。 -服务消费者端启动时,除了会查询订阅服务的可用节点列表做初始化连接,还会订阅服务的变更,每隔一段时间从注册中心获取最新的服务节点信息标记 sign,并与本地保存的 sign 值作比对,如果不一样,就会调用注册中心获取最新的服务节点信息。 +为了防止这种情况发生,注册中心需要提供一个保护机制,你可以把注册中心想象成一个带有门禁的房间,只有拥有门禁卡的 RPC Server 才能进入。在实际应用中,注册中心可以提供一个白名单机制,只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。 -一般情况下,按照这个过程是没问题的,但是在网络频繁抖动时,服务提供者上报给注册中心的心跳可能会一会儿失败一会儿成功,这时候注册中心就会频繁更新服务的可用节点信息,导致服务消费者频繁从注册中心拉取最新的服务可用节点信息,严重时可能产生网络风暴,导致注册中心带宽被打满。 +### 静态注册中心 -为了减少服务消费者从注册中心中拉取的服务可用节点信息的数据量,这个时候可以通过增量更新的方式,注册中心只返回变化的那部分节点信息,尤其在只有少数节点信息变更时,此举可以大大减少服务消费者从注册中心拉取的数据量,从而最大程度避免产生网络风暴。 +因为服务提供者是向服务消费者提供服务的,服务是否可用,服务消费者应该比注册中心更清楚。因此,可以直接在服务消费者端,根据调用服务提供者是否成功来判定服务提供者是否可用。如果服务消费者调用某一个服务提供者节点连续失败超过一定次数,可以在本地内存中将这个节点标记为不可用。并且每隔一段固定时间,服务消费者都要向标记为不可用的节点发起保活探测,如果探测成功了,就将标记为不可用的节点再恢复为可用状态,重新发起调用。 ## 参考资料 - [从 0 开始学微服务](https://time.geekbang.org/column/intro/100014401) - [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) -- [微服务架构核心 20 讲](https://time.geekbang.org/course/intro/100003901) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\265\201\351\207\217\346\216\247\345\210\266.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\265\201\351\207\217\346\216\247\345\210\266.md" new file mode 100644 index 0000000000..cd496d1365 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\265\201\351\207\217\346\216\247\345\210\266.md" @@ -0,0 +1,1113 @@ +--- +title: 流量控制 +date: 2020-01-20 11:06:00 +categories: + - 分布式 + - 分布式调度 +tags: + - 分布式 + - 服务治理 + - 调度 + - 流量控制 + - 限流 + - 熔断 + - 降级 +permalink: /pages/9f57a068/ +--- + +# 流量控制 + +> 在高并发场景下,为了应对瞬时海量请求的压力,保障系统的平稳运行,必须预估系统的流量阈值,通过限流规则阻断处理不过来的请求。 + +## 流量控制简介 + +### 什么是流量控制 + +流量控制(Flow Control),根据流量、并发线程数、响应时间等指标,把随机到来的流量调整成合适的形状,即流量塑形。避免应用被瞬时的流量高峰冲垮,从而保障应用的高可用性。 + +### 为什么需要流量控制 + +复杂的分布式系统架构中的应用程序往往具有数十个依赖项,每个依赖项都会不可避免地在某个时刻失败。 如果主机应用程序未与这些外部故障隔离开来,则可能会被波及。 + +例如,对于依赖于 30 个服务的应用程序,假设每个服务的正常运行时间为 99.99%,则可以期望: + +> 99.9930 = 99.7% 的正常运行时间 +> +> 10 亿个请求中的 0.3%= 3,000,000 个失败 +> +> 即使所有依赖项都具有出色的正常运行时间,每月也会有 2 个小时以上的停机时间。 +> +> 然而,现实情况一般比这种估量情况更糟糕。 + +--- + +当一切正常时,整体系统如下所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401280931974.png) + +图片来自 [Hystrix Wiki](https://github.com/Netflix/Hystrix/wiki) + +在分布式系统架构下,这些强依赖的子服务稳定与否对系统的影响非常大。但是,依赖的子服务可能有很多不可控问题:如网络连接、资源繁忙、服务宕机等。例如:下图中有一个 QPS 为 50 的依赖服务 I 出现不可用,但是其他依赖服务是可用的。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401280931939.png) + +图片来自 [Hystrix Wiki](https://github.com/Netflix/Hystrix/wiki) + +当流量很大的情况下,某个依赖的阻塞,会导致上游服务请求被阻塞。当这种级联故障愈演愈烈,就可能造成整个线上服务不可用的雪崩效应,如下图。这种情况若持续恶化,如果上游服务本身还被其他服务所依赖,就可能出现多米洛骨牌效应,导致多个服务都无法正常工作。 + +![img](https://github.com/Netflix/Hystrix/wiki/images/soa-3-640.png) + +图片来自 [Hystrix Wiki](https://github.com/Netflix/Hystrix/wiki) + +### 流量控制有哪些保护机制 + +流量控制常见的手段就是限流、熔断、降级。 + +#### 什么是降级? + +**降级**是保障服务能够稳定运行的一种保护方式:面对突增的流量,牺牲一些吞吐量以换取系统的稳定。常见的降级实现方式有:开关降级、限流降级、熔断降级。 + +#### 什么是限流? + +限流一般针对下游服务,当上游流量较大时,避免被上游服务的请求撑爆。 + +**限流**就是限制系统的输入和输出流量,以达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。 + +限流规则包含三个部分:时间粒度,接口粒度,最大限流值。限流规则设置是否合理直接影响到限流是否合理有效。 + +#### 什么是熔断? + +熔断一般针对上游服务,当下游服务超时/异常较多时,避免被下游服务拖垮。 + +当调用链路中某个资源出现不稳定,例如,超时异常比例升高的时候,则对这个资源的调用进行限制,并让请求快速失败,避免影响到其它的资源,最终产生雪崩的效果。 + +熔断尽最大的可能去完成所有的请求,容忍一些失败,熔断也能自动恢复。熔断的常见策略有: + +- 在每秒请求异常数超过多少时触发熔断降级 +- 在每秒请求异常错误率超过多少时触发熔断降级 +- 在每秒请求平均耗时超过多少时触发熔断降级 + +### 流量控制有哪些衡量指标 + +流量控制有以下几个角度: + +- 流量指标,例如 QPS、并发线程数等。 +- 资源的调用关系,例如资源的调用链路,资源和资源之间的关系,调用来源等。 +- 控制效果,例如排队等待、直接拒绝、Warm Up(预热)等。 + +## 限流算法 + +限流的本质是:在一定的时间范围内,限制某一个资源被访问的频率。如何去限制流量,就需要采用一定的策略,即限流算法。常见的限流算法有:固定窗口限流算法、滑动窗口限流算法、漏桶限流算法、令牌桶限流算法。 + +下面,将对这几种算法进行一一介绍。 + +### 固定窗口限流算法 + +#### 固定窗口限流算法的原理 + +固定窗口限流算法的**基本策略**是: + +1. 设置一个固定时间窗口,以及这个固定时间窗口内的最大请求数; +2. 为每个固定时间窗口设置一个计数器,用于统计请求数; +3. 一旦请求数超过最大请求数,则请求会被拦截。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748006.png) + +#### 固定窗口限流算法的利弊 + +固定窗口限流算法的**优点**是:实现简单。 + +固定窗口限流算法的**缺点**是:存在**临界问题**。所谓临界问题,是指:流量分别集中在一个固定时间窗口的尾部和一个固定时间窗口的头部。举例来说,假设限流规则为每分钟不超过 100 次请求。在第一个时间窗口中,起初没有任何请求,在最后 1 s,收到 100 次请求,由于没有达到阈值,所有请求都通过;在第二个时间窗口中,第 1 秒就收到 100 次请求,而后续没有任何请求。虽然,这两个时间窗口内的流量都符合限流要求,但是在两个时间窗口临界的这 2s 内,实际上有 200 次请求,显然是超过预期吞吐量的,存在压垮系统的可能。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748769.png) + +#### 固定窗口限流算法的实现 + +:::details Java 版本的固定窗口限流算法 + +```java +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class SlidingWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private long periodMillis; + + /** + * 分片窗口期时长 + */ + private final long shardPeriodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 分片窗口数 + */ + private final int shardNum; + + /** + * 请求总计数 + */ + private final AtomicLong totalCount = new AtomicLong(0); + + /** + * 分片窗口计数列表 + */ + private final List countList = new LinkedList<>(); + + public SlidingWindowRateLimiter(long qps, int shardNum) { + this(qps, 1000, TimeUnit.MILLISECONDS, shardNum); + } + + public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis(); + this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum; + this.shardNum = shardNum; + for (int i = 0; i < shardNum; i++) { + countList.add(new AtomicLong(0)); + } + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (now > lastPeriodMillis) { + for (int shardId = 0; shardId < shardNum; shardId++) { + long shardCount = countList.get(shardId).get(); + totalCount.addAndGet(-shardCount); + countList.set(shardId, new AtomicLong(0)); + lastPeriodMillis += shardPeriodMillis; + } + } + int shardId = (int) (now % periodMillis / shardPeriodMillis); + if (totalCount.get() + permits <= maxPermits) { + countList.get(shardId).addAndGet(permits); + totalCount.addAndGet(permits); + return true; + } else { + return false; + } + } + +} +``` + +::: + +### 滑动窗口限流算法 + +#### 滑动窗口限流算法的原理 + +滑动窗口限流算法是对固定窗口限流算法的改进,解决了临界问题。 + +滑动窗口限流算法的**基本策略**是: + +- 将固定时间窗口分片为多个子窗口,每个子窗口的访问次数独立统计; +- 当请求时间大于当前子窗口的最大时间时,则将当前子窗口废弃,并将计时窗口向前滑动,并将下一个子窗口置为当前窗口。 +- 要保证所有子窗口的统计数之和不能超过阈值。 + +滑动窗口限流算法就是针对固定窗口限流算法的更细粒度的控制,分片越多,则限流越精准。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230748277.png) + +#### 滑动窗口限流算法的利弊 + +滑动窗口限流算法的**优点**是:在滑动窗口限流算法中,临界位置的突发请求都会被算到时间窗口内,因此可以解决计数器算法的临界问题。 + +滑动窗口限流算法的**缺点**是: + +- **额外的内存开销** - 滑动时间窗口限流算法的时间窗口是持续滑动的,并且除了需要一个计数器来记录时间窗口内接口请求次数之外,还需要记录在时间窗口内每个接口请求到达的时间点,所以存在额外的内存开销。 +- **限流的控制粒度受限于窗口分片粒度** - 滑动窗口限流算法,**只能在选定的时间粒度上限流,对选定时间粒度内的更加细粒度的访问频率不做限制**。但是,由于每个分片窗口都有额外的内存开销,所以也并不是分片数越多越好的。 + +#### 滑动窗口限流算法的实现 + +:::details Java 版本的滑动窗口限流算法 + +```java +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +public class SlidingWindowRateLimiter implements RateLimiter { + + /** + * 允许的最大请求数 + */ + private final long maxPermits; + + /** + * 窗口期时长 + */ + private final long periodMillis; + + /** + * 分片窗口期时长 + */ + private final long shardPeriodMillis; + + /** + * 窗口期截止时间 + */ + private long lastPeriodMillis; + + /** + * 分片窗口数 + */ + private final int shardNum; + + /** + * 请求总计数 + */ + private final AtomicLong totalCount = new AtomicLong(0); + + /** + * 分片窗口计数列表 + */ + private final List countList = new LinkedList<>(); + + public SlidingWindowRateLimiter(long qps, int shardNum) { + this(qps, 1000, TimeUnit.MILLISECONDS, shardNum); + } + + public SlidingWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, int shardNum) { + this.maxPermits = maxPermits; + this.periodMillis = timeUnit.toMillis(period); + this.lastPeriodMillis = System.currentTimeMillis(); + this.shardPeriodMillis = timeUnit.toMillis(period) / shardNum; + this.shardNum = shardNum; + for (int i = 0; i < shardNum; i++) { + countList.add(new AtomicLong(0)); + } + } + + @Override + public synchronized boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + if (now > lastPeriodMillis) { + for (int shardId = 0; shardId < shardNum; shardId++) { + long shardCount = countList.get(shardId).get(); + totalCount.addAndGet(-shardCount); + countList.set(shardId, new AtomicLong(0)); + lastPeriodMillis += shardPeriodMillis; + } + } + int shardId = (int) (now % periodMillis / shardPeriodMillis); + if (totalCount.get() + permits <= maxPermits) { + countList.get(shardId).addAndGet(permits); + totalCount.addAndGet(permits); + return true; + } else { + return false; + } + } + +} +``` + +::: + +### 漏桶限流算法 + +#### 漏桶限流算法的原理 + +漏桶限流算法的**基本策略**是: + +- 水(请求)以任意速率由入口进入到漏桶中; +- 水以固定的速率由出口出水(请求通过); +- 漏桶的容量是固定的,如果水的流入速率大于流出速率,最终会导致漏桶中的水溢出(这意味着请求拒绝)。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230749486.png) + +#### 漏桶限流算法的利弊 + +漏桶限流算法的**优点**是:**流量速率固定**——即无论流量多大,即便是突发的大流量,处理请求的速度始终是固定的。 + +漏桶限流算法的**缺点**是:不能灵活的调整流量。例如:一个集群通过增减节点的方式,弹性伸缩了其吞吐能力,漏桶限流算法无法随之调整。 + +**漏桶策略适用于间隔性突发流量且流量不用即时处理的场景**。 + +#### 漏桶限流算法的实现 + +:::details Java 版本的漏桶限流算法 + +```java +import java.util.concurrent.atomic.AtomicLong; + +public class LeakyBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final int qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 计算的起始时间 + */ + private long beginTimeMillis; + + /** + * 桶中当前的水量 + */ + private final AtomicLong waterNum = new AtomicLong(0); + + public LeakyBucketRateLimiter(int qps, int capacity) { + this.qps = qps; + this.capacity = capacity; + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + // 如果桶中没有水,直接通过 + if (waterNum.get() == 0) { + beginTimeMillis = System.currentTimeMillis(); + waterNum.addAndGet(permits); + return true; + } + + // 计算水量 + long leakedWaterNum = ((System.currentTimeMillis() - beginTimeMillis) / 1000) * qps; + long currentWaterNum = waterNum.get() - leakedWaterNum; + waterNum.set(Math.max(0, currentWaterNum)); + + // 重置时间 + beginTimeMillis = System.currentTimeMillis(); + + if (waterNum.get() + permits < capacity) { + waterNum.addAndGet(permits); + return true; + } else { + return false; + } + } + +} +``` + +::: + +### 令牌桶限流算法 + +#### 令牌桶限流算法的原理 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202401230750231.png) + +令牌桶算法的**原理**: + +1. 接口限制 T 秒内最大访问次数为 N,则每隔 T/N 秒会放一个 token 到桶中 +2. 桶内最多存放 M 个 token,如果 token 到达时令牌桶已经满了,那么这个 token 就会被丢弃 +3. 接口请求会先从令牌桶中取 token,拿到 token 则处理接口请求,拿不到 token 则进行限流处理 + +#### 令牌桶限流算法的利弊 + +因为令牌桶存放了很多令牌,那么大量的突发请求会被执行,但是它不会出现临界问题,在令牌用完之后,令牌是以一个恒定的速率添加到令牌桶中的,因此不能再次发送大量突发请求。 + +规定固定容量的桶,token 以固定速度往桶内填充,当桶满时 token 不会被继续放入,每过来一个请求把 token 从桶中移除,如果桶中没有 token 不能请求。 + +**令牌桶算法适用于有突发特性的流量,且流量需要即时处理的场景**。 + +#### 令牌桶限流算法的实现 + +:::details Java 实现令牌桶算法 + +```java +import java.util.concurrent.atomic.AtomicLong; + +/** + * 令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-18 + */ +public class TokenBucketRateLimiter implements RateLimiter { + + /** + * QPS + */ + private final long qps; + + /** + * 桶的容量 + */ + private final long capacity; + + /** + * 上一次令牌发放时间 + */ + private long endTimeMillis; + + /** + * 桶中当前的令牌数量 + */ + private final AtomicLong tokenNum = new AtomicLong(0); + + public TokenBucketRateLimiter(long qps, long capacity) { + this.qps = qps; + this.capacity = capacity; + this.endTimeMillis = System.currentTimeMillis(); + } + + @Override + public synchronized boolean tryAcquire(int permits) { + + long now = System.currentTimeMillis(); + long gap = now - endTimeMillis; + + // 计算令牌数 + long newTokenNum = (gap * qps / 1000); + long currentTokenNum = tokenNum.get() + newTokenNum; + tokenNum.set(Math.min(capacity, currentTokenNum)); + + if (tokenNum.get() < permits) { + return false; + } else { + tokenNum.addAndGet(-permits); + endTimeMillis = now; + return true; + } + } + +} +``` + +::: + +> **扩展** +> +> Guava 的 RateLimiter 工具类就是基于令牌桶算法实现,其源码分析可以参考:[RateLimiter 基于漏桶算法,但它参考了令牌桶算法](https://blog.csdn.net/forezp/article/details/100060686) + +### 限流算法测试 + +:::details 限流算法测试 + +```java +import cn.hutool.core.thread.ThreadUtil; +import cn.hutool.core.util.RandomUtil; +import lombok.extern.slf4j.Slf4j; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +@Slf4j +public class RateLimiterDemo { + + public static void main(String[] args) { + + // ============================================================================ + + int qps = 20; + + System.out.println("======================= 固定时间窗口限流算法 ======================="); + FixedWindowRateLimiter fixedWindowRateLimiter = new FixedWindowRateLimiter(qps); + testRateLimit(fixedWindowRateLimiter, qps); + + System.out.println("======================= 滑动时间窗口限流算法 ======================="); + SlidingWindowRateLimiter slidingWindowRateLimiter = new SlidingWindowRateLimiter(qps, 10); + testRateLimit(slidingWindowRateLimiter, qps); + + System.out.println("======================= 漏桶限流算法 ======================="); + LeakyBucketRateLimiter leakyBucketRateLimiter = new LeakyBucketRateLimiter(qps, 100); + testRateLimit(leakyBucketRateLimiter, qps); + + System.out.println("======================= 令牌桶限流算法 ======================="); + TokenBucketRateLimiter tokenBucketRateLimiter = new TokenBucketRateLimiter(qps, 100); + testRateLimit(tokenBucketRateLimiter, qps); + } + + private static void testRateLimit(RateLimiter rateLimiter, int qps) { + + AtomicInteger okNum = new AtomicInteger(0); + AtomicInteger limitNum = new AtomicInteger(0); + ExecutorService executorService = ThreadUtil.newFixedExecutor(10, "限流测试", true); + long beginTime = System.currentTimeMillis(); + + int threadNum = 4; + final CountDownLatch latch = new CountDownLatch(threadNum); + for (int i = 0; i < threadNum; i++) { + executorService.submit(() -> { + try { + batchRequest(rateLimiter, okNum, limitNum, 1000); + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + latch.countDown(); + } + }); + } + + try { + latch.await(10, TimeUnit.SECONDS); + long endTime = System.currentTimeMillis(); + long gap = endTime - beginTime; + log.info("限流 QPS: {} -> 实际结果:耗时 {} ms,{} 次请求成功,{} 次请求被限流,实际 QPS: {}", + qps, gap, okNum.get(), limitNum.get(), okNum.get() * 1000 / gap); + if (okNum.get() == qps) { + log.info("限流符合预期"); + } + } catch (Exception e) { + log.error("发生异常!", e); + } finally { + executorService.shutdown(); + } + } + + private static void batchRequest(RateLimiter rateLimiter, AtomicInteger okNum, AtomicInteger limitNum, int num) + throws InterruptedException { + for (int j = 0; j < num; j++) { + if (rateLimiter.tryAcquire(1)) { + log.info("请求成功"); + okNum.getAndIncrement(); + } else { + log.info("请求限流"); + limitNum.getAndIncrement(); + } + TimeUnit.MILLISECONDS.sleep(RandomUtil.randomInt(0, 10)); + } + } + +} +``` + +::: + +## 限流框架 - Hystrix + +Hystrix 是由 Netflix 开源,用于处理分布式系统的延迟和容错的一个开源组件。在分布式系统里,许多依赖不可避免的会调用失败,比如超时、异常等。Hystrix 采用**断路器模式**来实现服务间的彼此隔离,从而避免级联故障,以提高分布式系统整体的弹性。 + +“断路器”本身是一种开关装置,当某个服务单元发生故障之后,通过断路器的故障监控(类似熔断保险丝),**向调用方返回一个符合预期的、可处理的备选响应(FallBack),而不是长时间的等待或者抛出调用方无法处理的异常**,这样就保证了服务调用方的线程不会被长时间、不必要地占用,从而避免了故障在分布式系统中的蔓延,乃至雪崩。 + +Hystrix 官方已宣布**不再发布新版本**。但是,Hystrix 的断路器设计理念,有非常高的学习价值。 + +如果使用 Hystrix 对每个基础依赖服务进行过载保护,则整个系统架构将会类似下图所示,每个依赖项彼此隔离,受到延迟时发生饱和的资源的被限制访问,并包含 fallback 逻辑(用于降级处理),该逻辑决定了在依赖项中发生任何类型的故障时做出对应的处理。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717142842.png) + +### Hystrix 原理 + +如下图所示,Hystrix 的工作流程大致可以分为 9 个步骤。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717143247.png) + +**(一)构建一个 HystrixCommand 或 HystrixObservableCommand 对象** + +Hystrix 进行资源隔离,其实是提供了一个抽象,叫做**命令模式**。这也是 Hystrix 最基本的**资源隔离技术**。 + +在使用 Hystrix 的过程中,会对依赖服务的调用请求封装成命令对象,Hystrix 对 命令对象抽象了两个抽象类:`HystrixCommand` 和 `HystrixObservableCommand` 。 + +- `HystrixCommand` 表示的命令对象会返回一个唯一返回值。 +- `HystrixObservableCommand` 表示的命令对象会返回多个返回值。 + +```java +HystrixCommand command = new HystrixCommand(arg1, arg2); +HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2); +``` + +**(二)执行命令** + +Hystrix 中共有 4 种方式执行命令,如下所示: + +| 执行方式 | 说明 | 可用对象 | +| :------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------- | :------------------------- | +| [`execute()`]() | 阻塞式同步执行,返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`queue()`]() | 异步执行,通过 `Future` 返回依赖服务的单一返回结果(或者抛出异常) | `HystrixCommand` | +| [`observe()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。代调用代码先执行 (Hot Obserable) | `HystrixObservableCommand` | +| [`toObservable()`]() | 基于 Rxjava 的 Observable 方式,返回通过 Observable 表示的依赖服务返回结果。执行代码等到真正订阅的时候才会执行 (cold observable) | `HystrixObservableCommand` | + +这四种命令中,`exeucte()`、`queue()`、`observe()` 的表示其实是通过 `toObservable()` 实现的,其转换关系如下图所示: + +![img](https:////upload-images.jianshu.io/upload_images/14126519-60964d9fa41614c1.png?imageMogr2/auto-orient/strip|imageView2/2/w/563/format/webp) + +`HystrixCommand` 执行方式 + +```java +K value = command.execute(); +// 等价语句: +K value = command.execute().queue().get(); + +Future fValue = command.queue(); +//等价语句: +Future fValue = command.toObservable().toBlocking().toFuture(); + +Observable ohValue = command.observe(); //hot observable,立刻订阅,命令立刻执行 +//等价语句: +Observable ohValue = command.toObservable().subscribe(subject); + +// 上述执行最终实现还是基于 toObservable() +Observable ocValue = command.toObservable(); //cold observable,延后订阅,订阅发生后,执行才真正执行 +``` + +**(三)是否缓存** + +如果当前命令对象启用了请求缓存,并且请求的响应存在于缓存中,则缓存的响应会立刻以 `Observable` 的形式返回。 + +**(四)是否开启断路器** + +如果第三步没有缓存没有命中,则判断一下当前断路器的断路状态是否打开。如果断路器状态为打开状态,则 Hystrix 将不会执行此 Command 命令,直接执行步骤 8 调用 Fallback; + +如果断路器状态是关闭,则执行步骤 5 检查是否有足够的资源运行 Command 命令 + +**(五)信号量、线程池是否拒绝** + +当您执行该命令时,Hystrix 会检查断路器以查看电路是否打开。 + +如果电路开路(或“跳闸”),则 Hystrix 将不会执行该命令,而是将流程路由到 (8) 获取回退。 + +如果电路闭合,则流程前进至 (5) 以检查是否有可用容量来运行命令。 + +如果当前要执行的 Command 命令 先关连的线程池 和队列(或者信号量)资源已经满了,Hystrix 将不会运行 Command 命令,直接执行 **步骤 8 **的 Fallback 降级处理;如果未满,表示有剩余的资源执行 Command 命令,则执行**步骤 6** + +**(六)`construct()` 或 `run()`** + +当经过**步骤 5** 判断,有足够的资源执行 Command 命令时,本步骤将调用 Command 命令运行方法,基于不同类型的 Command,有如下两种两种运行方式: + +| 运行方式 | 说明 | +| :------------------------------------- | :----------------------------------------------------------------------- | +| `HystrixCommand.run()` | 返回一个处理结果或者抛出一个异常 | +| `HystrixObservableCommand.construct()` | 返回一个 Observable 表示的结果(可能多个),或者 基于`onError`的错误通知 | + +如果`run()` 或者`construct()`方法 的`真实执行时间`超过了 Command 设置的`超时时间阈值`, 则**当前则执行线程**(或者是独立的定时器线程)将会抛出`TimeoutException`。抛出超时异常 TimeoutException,后,将执行**步骤 8 **的 Fallback 降级处理。即使`run()`或者`construct()`执行没有被取消或中断,最终能够处理返回结果,但在降级处理逻辑中,将会抛弃`run()`或`construct()`方法的返回结果,而返回 Fallback 降级处理结果。 + +> **注意事项** +> 需要注意的是,Hystrix 无法强制 将正在运行的线程停止掉--Hystrix 能够做的最好的方式就是在 JVM 中抛出一个`InterruptedException`。如果 Hystrix 包装的工作不抛出中断异常`InterruptedException`, 则在 Hystrix 线程池中的线程将会继续执行,尽管`调用的客户端`已经接收到了`TimeoutException`。这种方式会使 Hystrix 的线程池处于饱和状态。大部分的 Java Http Client 开源库并不会解析 `InterruptedException`。所以确认 HTTP client 相关的连接和读/写相关的超时时间设置。 +> 如果 Command 命令没有抛出任何异常,并且有返回结果,则 Hystrix 将会在做完日志记录和统计之后会将结果返回。 如果是通过`run()`方式运行,则返回一个`Obserable`对象,包含一个唯一值,并且发送一个`onCompleted`通知;如果是通过`consturct()`方式运行 ,则返回一个`Observable 对象`。 + +**(七)健康检查** + +Hystrix 会统计 Command 命令执行执行过程中的**成功数**、**失败数**、**拒绝数**和**超时数**, 将这些信息记录到**断路器 (Circuit Breaker) **中。断路器将上述统计按照**时间窗**的形式记录到一个定长数组中。断路器根据时间窗内的统计数据去判定请求什么时候可以被熔断,熔断后,在接下来一段恢复周期内,相同的请求过来后会直接被熔断。当再次校验,如果健康监测通过后,熔断开关将会被关闭。 + +**(八)获取 Fallback** + +当以下场景出现后,Hystrix 将会尝试触发 `Fallback`: + +> - 步骤 6 Command 执行时抛出了任何异常; +> - 步骤 4 断路器已经被打开 +> - 步骤 5 执行命令的线程池、队列或者信号量资源已满 +> - 命令执行的时间超过阈值 + +**(九)返回结果** + +如果 Hystrix 命令对象执行成功,将会返回结果,或者以`Observable`形式包装的结果。根据**步骤 2 **的 command 调用方式,返回的`Observable` 会按照如下图说是的转换关系进行返回: + +![img](https:////upload-images.jianshu.io/upload_images/14126519-8790f97df332d9a2.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +- `execute()` — 用和 `.queue()` 相同的方式获取 `Future`,然后调用 `Future` 的 `get()` 以获取 `Observable` 的单个值。 +- `queue()` —将 `Observable` 转换为 `BlockingObservable`,以便可以将其转换为 `Future` 并返回。 +- `watch()` —订阅 `Observable` 并开始执行命令的流程; 返回一个 `Observable`,当订阅该 `Observable` 时,它会重新通知。 +- `toObservable()` —返回不变的 `Observable`; 必须订阅它才能真正开始执行命令的流程。 + +### 断路器工作原理 + +![img](https:////upload-images.jianshu.io/upload_images/14126519-dce007513bf90794.png?imageMogr2/auto-orient/strip|imageView2/2/w/640/format/webp) + +1. 断路器时间窗内的请求数是否超过了**请求数断路器生效阈值**`circuitBreaker.requestVolumeThreshold`,如果超过了阈值,则将会触发断路,断路状态为**开启** + 例如,如果当前阈值设置的是`20`,则当时间窗内统计的请求数共计 19 个,即使 19 个全部失败了,都不会触发断路器。 +2. 并且请求错误率超过了**请求错误率阈值**`errorThresholdPercentage` +3. 如果两个都满足,则将断路器由**关闭**迁移到**开启** +4. 如果断路器开启,则后续的所有相同请求将会被断路掉; +5. 直到过了**沉睡时间窗**`sleepWindowInMilliseconds`后,再发起请求时,允许其通过(此时的状态为**半开起状态**)。如果请求失败了,则保持断路器状态为**开启**状态,并更新**沉睡时间窗**。如果请求成功了,则将断路器状态改为**关闭**状态; + +核心的逻辑如下: + +```java +@Override +public void onNext(HealthCounts hc) { + // check if we are past the statisticalWindowVolumeThreshold + if (hc.getTotalRequests() < properties.circuitBreakerRequestVolumeThreshold().get()) { + // we are not past the minimum volume threshold for the stat window, + // so no change to circuit status. + // if it was CLOSED, it stays CLOSED + // if it was half-open, we need to wait for a successful command execution + // if it was open, we need to wait for sleep window to elapse + } else { + if (hc.getErrorPercentage() < properties.circuitBreakerErrorThresholdPercentage().get()) { + //we are not past the minimum error threshold for the stat window, + // so no change to circuit status. + // if it was CLOSED, it stays CLOSED + // if it was half-open, we need to wait for a successful command execution + // if it was open, we need to wait for sleep window to elapse + } else { + // our failure rate is too high, we need to set the state to OPEN + if (status.compareAndSet(Status.CLOSED, Status.OPEN)) { + circuitOpened.set(System.currentTimeMillis()); + } + } + } +} +``` + +## 限流框架 - Sentinel + +## 其他限流解决方案 + +### Guava RateLimiter + +Guava 是 Google 开源的 Java 类库,提供了一个工具类 `RateLimiter`,它基于令牌桶算法实现了本地限流器。 + +:::details RateLimiter 限流示例 + +```java +// 限流器流速:2 个请求/秒 +RateLimiter limiter = RateLimiter.create(2.0); +// 执行任务的线程池 +ExecutorService es = Executors.newFixedThreadPool(1); +// 记录上一次执行时间 +prev = System.nanoTime(); +// 测试执行 20 次 +for (int i = 0; i < 20; i++) { + // 限流器限流 + limiter.acquire(); + // 提交任务异步执行 + es.execute(() -> { + long cur = System.nanoTime(); + // 打印时间间隔:毫秒 + System.out.println((cur - prev) / 1000000); + prev = cur; + }); +} + +// 输出结果: +// ... +// 500 +// 499 +// 500 +// 499 +``` + +::: + +### Redis + Lua + +如果想要针对分布式系统资源进行限流,则必须具备两个要素: + +1. 对于资源的访问统计,必须是所有分布式节点都可以共享访问的数据存储;并且,由于在高并发场景下,读写访问统计数据会很频繁,该数据存储必须有很高的读写性能。 +2. 访问统计、限流计算都以原子操作方式进行。 + +满足以上要素的一种简单解决方案是,采用 Redis + Lua 来实现,原因在于: + +- Redis 数据库的读写性能极高; +- Redis 支持以原子操作的方式执行 Lua 脚本。 + +#### Redis + Lua 实现的固定窗口限流算法 + +Redis + Lua 实现的固定窗口限流算法实现思路: + +- 根据实际需要,将当前时间格式化为天(`yyyyMMdd`)、时(`yyyyMMddHH`)、分(`yyyyMMddHHmm`)、秒(`yyyyMMddHHmmss`),并作为 Redis 的 String 类型 Key。该 Key 可以视为一个固定时间窗口,其中的 value 用于统计访问量; +- 用于代表不同粒度的时间窗口按需设置过期时间; +- 一旦达到窗口的限流阈值时,请求被限流;否则请求通过。 + +:::details Redis + Lua 实现的固定窗口限流算法 + +下面的代码片段模拟通过一个大小为 1 分钟的固定时间窗口进行限流,阈值为 100,过期时间 60s。 + +限流脚本 `fixed_window_rate_limit.lua` 代码: + +```lua +-- 缓存 Key +local key = KEYS[1] +-- 访问请求数 +local permits = tonumber(ARGV[1]) +-- 过期时间 +local seconds = tonumber(ARGV[2]) +-- 限流阈值 +local limit = tonumber(ARGV[3]) + +-- 获取统计值 +local count = tonumber(redis.call('GET', key) or "0") + +if count + permits > limit then + -- 触发限流 + return 0 +else + redis.call('INCRBY', key, permits) + redis.call('EXPIRE', key, seconds) + return count + permits +end +``` + +调用 lua 的实际限流代码: + +```java +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class RedisFixedWindowRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/fixed_window_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long maxPermits; + private final long periodSeconds; + private final String key; + + public RedisFixedWindowRateLimiter(long qps, String key) { + this(qps * 60, 60, TimeUnit.SECONDS, key); + } + + public RedisFixedWindowRateLimiter(long maxPermits, long period, TimeUnit timeUnit, String key) { + this.maxPermits = maxPermits; + this.periodSeconds = timeUnit.toSeconds(period); + this.key = key; + } + + @Override + public boolean tryAcquire(int permits) { + List keys = Collections.singletonList(key); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(periodSeconds), + String.valueOf(maxPermits)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + RateLimiter jedisFixedWindowRateLimiter = new RedisFixedWindowRateLimiter(qps, "rate:limit:20240122210000"); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + int num = RandomUtil.randomInt(qps, 100); + for (int second = 0; second < seconds; second++) { + for (int i = 0; i < num; i++) { + total++; + if (jedisFixedWindowRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} +``` + +::: + +#### Redis + Lua 实现的令牌桶限流算法 + +:::details Redis + Lua 实现的令牌桶限流算法 + +限流脚本 `token_bucket_rate_limit.lua` 代码: + +```lua +local tokenKey = KEYS[1] +local timeKey = KEYS[2] + +-- 申请令牌数 +local permits = tonumber(ARGV[1]) +-- QPS +local qps = tonumber(ARGV[2]) +-- 桶的容量 +local capacity = tonumber(ARGV[3]) +-- 当前时间(单位:毫秒) +local nowMillis = tonumber(ARGV[4]) +-- 填满令牌桶所需要的时间 +local fillTime = capacity / qps +local ttl = math.min(capacity, math.floor(fillTime * 2)) + +local currentTokenNum = tonumber(redis.call("GET", tokenKey)) +if currentTokenNum == nil then + currentTokenNum = capacity +end + +local endTimeMillis = tonumber(redis.call("GET", timeKey)) +if endTimeMillis == nil then + endTimeMillis = 0 +end + +local gap = nowMillis - endTimeMillis +local newTokenNum = math.max(0, gap * qps / 1000) +local currentTokenNum = math.min(capacity, currentTokenNum + newTokenNum) + +if currentTokenNum < permits then + -- 请求拒绝 + return -1 +else + -- 请求通过 + local finalTokenNum = currentTokenNum - permits + redis.call("SETEX", tokenKey, ttl, finalTokenNum) + redis.call("SETEX", timeKey, ttl, nowMillis) + return finalTokenNum +end +``` + +调用 lua 的实际限流代码: + +```java +package io.github.dunwu.distributed.ratelimit; + +import cn.hutool.core.collection.CollectionUtil; +import cn.hutool.core.io.FileUtil; +import cn.hutool.core.io.resource.ResourceUtil; +import cn.hutool.core.util.RandomUtil; +import cn.hutool.core.util.StrUtil; +import redis.clients.jedis.Jedis; +import redis.clients.jedis.exceptions.JedisConnectionException; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.concurrent.TimeUnit; + +/** + * 基于 Redis + Lua 实现的令牌桶限流算法 + * + * @author Zhang Peng + * @date 2024-01-23 + */ +public class RedisTokenBucketRateLimiter implements RateLimiter { + + private static final String REDIS_HOST = "localhost"; + + private static final int REDIS_PORT = 6379; + + private static final Jedis JEDIS; + + public static final String SCRIPT; + + static { + // Jedis 有多种构造方法,这里选用最简单的一种情况 + JEDIS = new Jedis(REDIS_HOST, REDIS_PORT); + + // 触发 ping 命令 + try { + JEDIS.ping(); + System.out.println("jedis 连接成功"); + } catch (JedisConnectionException e) { + e.printStackTrace(); + } + + SCRIPT = FileUtil.readString(ResourceUtil.getResource("scripts/token_bucket_rate_limit.lua"), + StandardCharsets.UTF_8); + } + + private final long qps; + private final long capacity; + private final String tokenKey; + private final String timeKey; + + public RedisTokenBucketRateLimiter(long qps, long capacity, String tokenKey, String timeKey) { + this.qps = qps; + this.capacity = capacity; + this.tokenKey = tokenKey; + this.timeKey = timeKey; + } + + @Override + public boolean tryAcquire(int permits) { + long now = System.currentTimeMillis(); + List keys = CollectionUtil.newLinkedList(tokenKey, timeKey); + List args = CollectionUtil.newLinkedList(String.valueOf(permits), String.valueOf(qps), + String.valueOf(capacity), String.valueOf(now)); + Object eval = JEDIS.eval(SCRIPT, keys, args); + long value = (long) eval; + return value != -1; + } + + public static void main(String[] args) throws InterruptedException { + + int qps = 20; + int bucket = 100; + RedisTokenBucketRateLimiter redisTokenBucketRateLimiter = + new RedisTokenBucketRateLimiter(qps, bucket, "token:rate:limit", "token:rate:limit:time"); + + // 先将令牌桶预热令牌申请完,后续才能真实反映限流 QPS + redisTokenBucketRateLimiter.tryAcquire(bucket); + TimeUnit.SECONDS.sleep(1); + + // 模拟在一分钟内,不断收到请求,限流是否有效 + int seconds = 60; + long okNum = 0L; + long total = 0L; + long beginTime = System.currentTimeMillis(); + for (int second = 0; second < seconds; second++) { + int num = RandomUtil.randomInt(qps, 100); + for (int i = 0; i < num; i++) { + total++; + if (redisTokenBucketRateLimiter.tryAcquire(1)) { + okNum++; + System.out.println("请求成功"); + } else { + System.out.println("请求限流"); + } + } + TimeUnit.SECONDS.sleep(1); + } + long endTime = System.currentTimeMillis(); + long time = (endTime - beginTime) / 1000; + System.out.println(StrUtil.format("请求通过数:{},总请求数:{},实际 QPS:{}", okNum, total, okNum / time)); + } + +} +``` + +::: + +## 参考资料 + +- [Hystrix Wiki](https://github.com/Netflix/Hystrix/wiki) +- [《大型网站技术架构:核心原理与案例分析》](https://item.jd.com/11322972.html) +- [谈谈限流算法的几种实现](https://www.jianshu.com/p/76cc8ba5ca91) +- [如何限流?在工作中是怎么做的?说一下具体的实现?](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/huifer-how-to-limit-current.md) +- [浅析限流算法](https://gongfukangee.github.io/2019/04/04/Limit/) +- [RateLimiter 基于漏桶算法,但它参考了令牌桶算法](https://blog.csdn.net/forezp/article/details/100060686) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/01.\346\234\215\345\212\241\350\267\257\347\224\261.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\347\275\221\345\205\263\350\267\257\347\224\261.md" similarity index 86% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/01.\346\234\215\345\212\241\350\267\257\347\224\261.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\347\275\221\345\205\263\350\267\257\347\224\261.md" index 1c44b7c06b..67bc89e22b 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/01.\346\234\215\345\212\241\350\267\257\347\224\261.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\347\275\221\345\205\263\350\267\257\347\224\261.md" @@ -1,27 +1,41 @@ --- -title: 服务路由 +title: 网关路由 date: 2022-04-19 15:54:25 -order: 01 categories: - 分布式 - 分布式调度 tags: - 分布式 - 服务治理 - - 流量调度 + - 调度 - 路由 -permalink: /pages/3915e8/ + - 网关 +permalink: /pages/e45692ee/ --- -# 服务路由 +# 网关路由 -## 服务路由简介 +## 什么是网关 -### 什么是服务路由 +网关的首要职责就是:作为统一的出口,对外提供服务;将外部访问网关地址的流量,根据适当的规则路由到内部集群中正确的服务节点之上。因此,微服务中的网关,也常被称为“服务网关”或“API 网关”。 -**服务路由**是指通过一定的规则从集群中选择合适的节点。 +网关首先应该是个路由器,在满足此前提的基础上,网关还可以根据需要作为流量过滤器来使用,提供某些额外的可选的功能。网关常见的能力如下: + +- **动态路由**:根据请求路由到对应的服务上去,如果服务不可用还会有重试机制 +- **负载均衡**:多服务器提供同一种服务,网关会从配置中心拉取各服务注册信息,然后将请求负载均衡风阀到这些服务器进行处理 +- **流量控制**:限制并发请求的流量,避免内部系统受到冲击 +- **安全认证**:网关对相关权限验证、脱敏和流量清洗、签名和黑名单功能 +- **熔断降级**:当服务不可用或者访问量过大,网关可以将请求做降级,将流量打到其他服务器或者做其他处理,提示用户暂时不可用 +- **灰度发布**:先进行小部分服务器升级,通过网关将少量的服务路由到已升级的服务器用来测试服务是否正常,大部分请求依旧在老版本服务器上处理 +- **日志服务**:服务访问情况监控和统计报表,请求的吞吐量、并发数、流量监控、性能监控和日常告警等 + +简单来说: -### 为什么需要服务路由 +> 网关 = 路由器(基础职能) + 过滤器(可选职能) + +## 什么是服务路由 + +**服务路由**是指通过一定的规则从集群中选择合适的节点。 负载均衡的作用和服务路由的功能看上去很近似,二者有什么区别呢? @@ -119,7 +133,7 @@ host = 10.20.153.10 => - **白名单** -```fallback +```bash register.ip != 10.20.153.10,10.20.153.11 => ``` @@ -230,7 +244,7 @@ or or -```fallback +```bash java -jar xxx-provider.jar -Ddubbo.provider.tag={the tag you want, may come from OS ENV} ``` @@ -260,4 +274,4 @@ RpcContext.getContext().setAttachment(Constants.REQUEST_TAG_KEY,"tag1"); - [从 0 开始学微服务](https://time.geekbang.org/column/intro/100014401) - [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) -- [微服务架构核心 20 讲](https://time.geekbang.org/course/intro/100003901) \ No newline at end of file +- [微服务架构核心 20 讲](https://time.geekbang.org/course/intro/100003901) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/02.\350\264\237\350\275\275\345\235\207\350\241\241.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\350\264\237\350\275\275\345\235\207\350\241\241.md" similarity index 93% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/02.\350\264\237\350\275\275\345\235\207\350\241\241.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\350\264\237\350\275\275\345\235\207\350\241\241.md" index fe423b103c..ba0cba851e 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/02.\350\264\237\350\275\275\345\235\207\350\241\241.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\350\264\237\350\275\275\345\235\207\350\241\241.md" @@ -1,19 +1,19 @@ --- -title: 深入浅出负载均衡 -date: 2018-07-05 15:50:00 +title: 负载均衡 cover: https://raw.githubusercontent.com/dunwu/images/master/snap/202310250658719.png -order: 02 +date: 2018-07-05 15:50:00 categories: - 分布式 - 分布式调度 tags: - 分布式 - - 流量调度 + - 服务治理 + - 调度 - 负载均衡 -permalink: /pages/98a1c1/ +permalink: /pages/bcf0fb8c/ --- -# 深入浅出负载均衡 +# 负载均衡 ## 负载均衡简介 @@ -110,7 +110,7 @@ DNS 即 **域名解析服务**,是 OSI 第七层网络协议。DNS 被设计 DNS 负载均衡的工作原理就是:**基于 DNS 查询缓存,按照负载情况返回不同服务器的 IP 地址**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643409.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643409.png) DNS 重定向的 **优点**: @@ -129,7 +129,7 @@ DNS 重定向的 **缺点**: HTTP 重定向原理是:**根据用户的 HTTP 请求计算出一个真实的服务器地址,将该服务器地址写入 HTTP 重定向响应中,返回给浏览器,由浏览器重新进行访问**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643410.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643410.png) HTTP 重定向的 **优点**:**方案简单**。 @@ -152,7 +152,7 @@ HTTP 重定向的 **缺点**: - 正向代理:发生在 **客户端**,是由用户主动发起的。翻墙软件就是典型的正向代理,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。 - 反向代理:发生在 **服务端**,用户不知道代理的存在。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643411.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643411.png) 反向代理是如何实现负载均衡的呢?以 Nginx 为例,如下所示: @@ -177,7 +177,7 @@ HTTP 重定向的 **缺点**: IP 负载均衡是在网络层通过修改请求目的地址进行负载均衡。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643413.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643413.png) 如上图所示,IP 均衡处理流程大致为: @@ -192,7 +192,7 @@ IP 负载均衡在内核进程完成数据分发,较反向代理负载均衡 数据链路层负载均衡是指在通信协议的数据链路层修改 mac 地址进行负载均衡。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643412.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250643412.png) 在 Linux 平台上最好的链路层负载均衡开源产品是 LVS (Linux Virtual Server)。 @@ -220,7 +220,7 @@ LVS 的工作流程大致如下: > 注:负载均衡算法的实现,推荐阅读 [Dubbo 官方负载均衡算法说明](https://dubbo.apache.org/zh/docs/v2.7/dev/source/loadbalance/) ,源码讲解非常详细,非常值得借鉴。 > -> 下文中的各种算法的可执行示例已归档在 Github 仓库:https://github.com/dunwu/java-tutorial/tree/master/codes/java-distributed/java-load-balance,可以通过执行 `io.github.dunwu.javatech.LoadBalanceDemo` 查看各算法执行效果。 +> 下文中的各种算法的可执行示例已归档在 Github 仓库:[java-load-balance](https://github.com/dunwu/java-tutorial/tree/master/codes/java-distributed/java-load-balance),可以通过执行 `io.github.dunwu.javatech.LoadBalanceDemo` 查看各算法执行效果。 ### 轮询算法 @@ -228,7 +228,7 @@ LVS 的工作流程大致如下: 如下图所示,轮询负载均衡器收到来自客户端的 6 个请求,编号为 1、4 的请求会被发送到服务端 0;编号为 2、5 的请求会被发送到服务端 1;编号为 3、6 的请求会被发送到服务端 2。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250648178.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250648178.png) **轮询算法适合的场景需要满足:各机器处理能力相近,且每个请求工作量差异不大**。 @@ -258,7 +258,7 @@ public class RoundRobinLoadBalance extends BaseLoadBalance im 如下图所示,随机负载均衡器收到来自客户端的 6 个请求,会随机分发请求,可能会出现:编号为 1、5 的请求会被发送到服务端 0;编号为 2、4 的请求会被发送到服务端 1;编号为 3、6 的请求会被发送到服务端 2。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250648899.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250648899.png) **随机算法适合的场景需要满足:各机器处理能力相近,且每个请求工作量差异不大**。 @@ -347,9 +347,9 @@ public class RandomLoadBalance extends BaseLoadBalance implem - 编号为 1、4 的请求被发送到服务端 0;编号为 3、6 的请求被发送到服务端 2;二者处理能力强,应对游刃有余; - 编号为 2、5 的请求被发送到服务端 1,服务端 1 处理能力弱,应对捉襟见肘,导致过载。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250649920.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250649920.png) -《蜘蛛侠》电影中有一句经典台词:**能力越大,责任越大**。显然,以上情况不符合这句话,处理能力强的机器并没有被分发到更多的请求,它的处理能力被闲置了。那么,如何解决这个问题呢? +> 《蜘蛛侠》电影中有一句经典台词:**能力越大,责任越大**。显然,以上情况不符合这句话,处理能力强的机器并没有被分发到更多的请求,它的处理能力被闲置了。那么,如何解决这个问题呢? 一种比较容易想到的思路是:引入权重属性,可以根据机器的硬件条件为其设置合理的权重值,负载均衡时,优先将请求分发到权重较高的机器。 @@ -357,7 +357,7 @@ public class RandomLoadBalance extends BaseLoadBalance implem 如下图所示,服务端 0 设置权重为 3,服务端 1 设置权重为 1,服务端 2 设置权重为 2。负载均衡器收到来自客户端的 6 个请求,那么编号为 1、2、5 的请求会被发送到服务端 0,编号为 4 的请求会被发送到服务端 1,编号为 3、6 的请求会被发送到机器 2。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250649943.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250649943.png) 【示例】加权随机负载均衡算法实现示例 @@ -403,7 +403,7 @@ public class WeightRandomLoadBalance extends BaseLoadBalance public class WeightRoundRobinLoadBalance extends BaseLoadBalance implements LoadBalance { /** - * 60秒 + * 60 秒 */ private static final int RECYCLE_PERIOD = 60000; @@ -477,7 +477,7 @@ public class WeightRoundRobinLoadBalance extends BaseLoadBalance // 对 weightMap 进行检查,过滤掉长时间未被更新的节点。 // 该节点可能挂了,nodes 中不包含该节点,所以该节点的 lastUpdate 长时间无法被更新。 - // 若未更新时长超过阈值后,就会被移除掉,默认阈值为60秒。 + // 若未更新时长超过阈值后,就会被移除掉,默认阈值为 60 秒。 if (!updateLock.get() && nodes.size() != weightMap.size()) { if (updateLock.compareAndSet(false, true)) { try { @@ -561,7 +561,7 @@ public class WeightRoundRobinLoadBalance extends BaseLoadBalance - 编号为 2、5 的请求被发送到服务端 1,但是 2 始终保持长连接;该系统继续运行时,服务端 1 发生过载; - 编号为 3、6 的请求被发送到服务端 2,但是 3 很快就断开连接,此时只有 6 请求连接服务端 2; -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650176.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650176.png) 既然,请求的连接时长不同,会导致有的服务端处理慢,积压大量连接数;而有的服务端处理快,保持的连接数少。那么,我们不妨想一下,如果负载均衡器监控一下服务端当前所持有的连接数,优先将请求分发给连接数少的服务端,不就能有效提高分发效率了吗?最少连接数算法正是采用这个思路去设计的。 @@ -578,7 +578,7 @@ public class WeightRoundRobinLoadBalance extends BaseLoadBalance - 编号为 2、4 的请求被发送到服务端 1,但是 2、4 保持长连接; - 由于服务端 0 当前连接数最少,编号为 5、6 的请求被分发到服务端 0。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650852.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650852.png) “加权最少连接数算法(Weighted Least Connection)”在最少连接数算法的基础上,根据机器的性能为每台机器分配权重,再根据权重计算出每台机器能处理的连接数。 @@ -648,7 +648,7 @@ public class LeastActiveLoadBalance extends BaseLoadBalance i // 随机生成一个 [0, totalWeight) 之间的数字 int offsetWeight = random.nextInt(totalWeight); // 循环让随机数减去具有最少连接数的 Node 的权重值, - // 当 offset 小于等于0时,返回相应的 Node + // 当 offset 小于等于 0 时,返回相应的 Node for (int i = 0; i < leastCount; i++) { int leastIndex = leastIndexs[i]; // 获取权重值,并让随机数减去权重值 @@ -658,7 +658,7 @@ public class LeastActiveLoadBalance extends BaseLoadBalance i } } } - // 如果权重相同或权重为0时,随机返回一个 Node + // 如果权重相同或权重为 0 时,随机返回一个 Node return nodes.get(leastIndexs[random.nextInt(leastCount)]); } @@ -671,7 +671,7 @@ public class LeastActiveLoadBalance extends BaseLoadBalance i **最少响应时间算法具有高度的敏感性、自适应性**。但是,由于它需要持续监控候选机器的响应时延,相比于监控候选机器的连接数,会显著增加监控的开销。此外,请求的响应时延并不一定能完全反应机器的处理能力,有可能某机器上一次处理的请求恰好是一个开销非常小的请求。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650334.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250650334.png) ### 哈希算法 @@ -683,13 +683,13 @@ public class LeastActiveLoadBalance extends BaseLoadBalance i **“哈希算法(Hash)” 根据一个 key (可以是唯一 ID、IP、URL 等),通过哈希函数计算得到一个数值,用该数值在候选机器列表的进行取模运算,得到的结果便是选中的机器**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250652913.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250652913.png) 这种算法可以保证,同一关键字(IP 或 URL 等)的请求,始终会被转发到同一台机器上。哈希负载均衡算法常被用于实现会话粘滞(Sticky Session)。 但是 ,哈希算法的问题是:当增减节点时,由于哈希取模函数的基数发生变化,会影响大部分的映射关系,从而导致之前的数据不可访问。要解决这个问题,就必须根据新的计算公式迁移数据。显然,如果数据量很大的情况下,迁移成本很高;并且,在迁移过程中,要保证业务平滑过渡,需要使用数据双写等较为复杂的技术手段。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653034.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653034.png) 【示例】源地址哈希算法实现示例 @@ -722,12 +722,13 @@ public class IpHashLoadBalance extends BaseLoadBalance implem **哈希环的空间是按顺时针方向组织的**,需要对指定 key 的数据进行读写时,会执行两步: -1. 先对 key 进行哈希计算,以确定 key 在环上的位置; -2. 然后根据这个位置,顺时针找到的第一个节点,就是 key 对应的节点。 +1. 先对节点进行哈希计算,计算的关键字通常是 IP 或其他唯一标识(例:hash(ip)),然后对 2^32 取模,以确定节点在哈希环上的位置。 +2. 先对 key 进行哈希计算(hash(key)),然后对 2^32 取模,以确定 key 在哈希环上的位置。 +3. 然后根据 key 的位置,顺时针找到的第一个节点,就是 key 对应的节点。 所以,**一致性哈希是将“存储节点”和“数据”都映射到一个顺时针排序的哈希环上**。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653412.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653412.png) 一致性哈希算法会尽可能保证,相同的请求被分发到相同的机器上。**当出现增减节点时,只影响哈希环中顺时针方向的相邻的节点,对其他节点无影响,不会引起剧烈变动**。 @@ -738,7 +739,7 @@ public class IpHashLoadBalance extends BaseLoadBalance implem 如下图所示,假设,哈希环中新增了一个节点 S4,新增节点经过哈希计算映射到图中位置: -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653974.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653974.png) 此时,只有 K1 收到影响;而 K0、K2 均不受影响。 @@ -746,7 +747,7 @@ public class IpHashLoadBalance extends BaseLoadBalance implem 如下图所示,假设,哈希环中减少了一个节点 S0: -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653207.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250653207.png) 此时,只有 K0 收到影响;而 K1、K2 均不受影响。 @@ -754,7 +755,7 @@ public class IpHashLoadBalance extends BaseLoadBalance implem 如下图所示:极端情况下,可能由于节点在哈希环上分布不均,有大量请求计算得到的 key 会被集中映射到少数节点,甚至某一个节点上。此外,节点分布不均匀的情况下,进行容灾与扩容时,哈希环上的相邻节点容易受到过大影响,从而引发雪崩式的连锁反应。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250654770.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202310250654770.png) 【示例】一致性哈希算法示例 @@ -770,7 +771,7 @@ public class ConsistentHashLoadBalance extends BaseLoadBalance nodes, String ip) { // 分片数,这里设为节点数的 4 倍 Integer replicaNum = nodes.size() * 4; - // 获取 nodes 原始的 hashcode[11.分布式协同](..%2F11.%B7%D6%B2%BC%CA%BD%D0%AD%CD%AC) + // 获取 nodes 原始的 hashcode[11. 分布式协同](..%2F11.%B7%D6%B2%BC%CA%BD%D0%AD%CD%AC) int identityHashCode = System.identityHashCode(nodes); // 如果 nodes 是一个新的 List 对象,意味着节点数量发生了变化 @@ -813,12 +814,12 @@ public class ConsistentHashLoadBalance extends BaseLoadBalance extends BaseLoadBalance extends BaseLoadBalance - -## RPC 协议 - -### 协议的作用 - -只有二进制才能在网络中传输,所以 RPC 请求在发送到网络中之前,他需要把方法调用的请求参数转成二进制;转成二进制后,写入本地 Socket 中,然后被网卡发送到网络设备中。 - -在传输过程中,RPC 并不会把请求参数的所有二进制数据整体一下子发送到对端机器上,中间可能会拆分成好几个数据包,也可能会合并其他请求的数据包(合并的前提是同一个 TCP 连接上的数据),至于怎么拆分合并,这其中的细节会涉及到系统参数配置和 TCP 窗口大小。对于服务提供方应用来说,他会从 TCP 通道里面收到很多的二进制数据,那这时候怎么识别出哪些二进制是第一个请求的呢? - -这就好比让你读一篇没有标点符号的文章,你要怎么识别出每一句话到哪里结束呢?很简单啊,我们加上标点,完成断句就好了。 - -为了避免语义不一致的事情发生,我们就需要在发送请求的时候设定一个边界,然后在收到请求的时候按照这个设定的边界进行数据分割。这个边界语义的表达,就是我们所说的协议。 - -### 为何需要设计 RPC 协议 - -既然有了现成的 HTTP 协议,还有必要设计 RPC 协议吗? - -有必要。因为 HTTP 这些通信标准协议,数据包中的实际请求数据相对于数据包本身要小很多,有很多无用的内容;并且 HTTP 属于无状态协议,无法将请求和响应关联,每次请求要重新建立连接。这对于高性能的 RPC 来说,HTTP 协议难以满足需求,所以有必要设计一个**紧凑的私有协议**。 - -### 如何? - -首先,必须先明确消息的边界,即确定消息的长度。因此,至少要分为:消息长度+消息内容两部分。 - -接下来,我们会发现,在使用过程中,仅消息长度,不足以明确通信中的很多细节:如序列化方式是怎样的?是否消息压缩?压缩格式是怎样的?如果协议发生变化,需要明确协议版本等等。 - -综上,一个 RPC 协议大概会由下图中的这些参数组成: - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220619102052.png) - -### 可扩展的协议 - -前面所述的协议属于定长协议头,那也就是说往后就不能再往协议头里加新参数了,如果加参 -数就会导致线上兼容问题。 - -为了保证能平滑地升级改造前后的协议,我们有必要设计一种支持可扩展的协议。其关键在于让协议头支持可扩展,扩展后协议头的长度就不能定长了。那要实现读取不定长的协议头里面的内容,在这之前肯定需要一个固定的地方读取长度,所以我们需要一个固定的写入协议头的长度。整体协议就变成了三部分内容:固定部分、协议头内容、协议体内容。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220619102833.png) - -## RPC 序列化 - -> 有兴趣深入了解 JDK 序列化方式,可以参考:[Java 序列化](https://dunwu.github.io/waterdrop/pages/2b2f0f/) - -由于,网络传输的数据必须是二进制数据,而调用方请求的出参、入参都是对象。因此,必须将对象转换可传输的二进制,并且要求转换算法是可逆的。 - -- **序列化(serialize)**:序列化是将对象转换为二进制数据。 -- **反序列化(deserialize)**:反序列化是将二进制数据转换为对象。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220619110947.png) - -序列化就是将对象转换成二进制数据的过程,而反序列就是反过来将二进制转换为对象的过程。 - -### 序列化技术 - -Java 领域,常见的序列化技术如下 - -- JDK 序列化:JDK 内置的二进制序列化方式 -- 其他二进制序列化 - - [Thrift](https://github.com/apache/thrift) - - [Protobuf](https://github.com/protocolbuffers/protobuf) - - [Hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) -- JSON 序列化 - - [Jackson](https://github.com/FasterXML/jackson) - - [Gson](https://github.com/google/gson) - - [Fastjson](https://github.com/alibaba/fastjson) - -### 序列化技术选型 - -市面上有如此多的序列化技术,那么我们在应用时如何选择呢? - -序列化技术选型,需要考量的维度,根据重要性从高到低,依次有: - -- **安全性**:是否存在漏洞。如果存在漏洞,就有被攻击的可能性。 -- **兼容性**:版本升级后的兼容性是否很好,是否支持更多的对象类型,是否是跨平台、跨语言的。服务调用的稳定性与可靠性,要比服务的性能更加重要。 -- **性能** - - **时间开销**:序列化、反序列化的耗时性能自然越小越好。 - - **空间开销**:序列化后的数据越小越好,这样网络传输效率就高。 -- **易用性**:类库是否轻量化,API 是否简单易懂。 - -鉴于以上的考量,序列化技术的选型建议如下: - -- JDK 序列化:性能较差,且有很多使用限制,不建议使用。 -- [Thrift](https://github.com/apache/thrift)、[Protobuf](https://github.com/protocolbuffers/protobuf):适用于**对性能敏感,对开发体验要求不高**。 -- [Hessian](http://hessian.caucho.com/doc/hessian-overview.xtp):适用于**对开发体验敏感,性能有要求**。 -- [Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson)、[Fastjson](https://github.com/alibaba/fastjson):适用于对序列化后的数据要求有**良好的可读性**(转为 json 、xml 形式)。 - -### 序列化问题 - -由于 RPC 每次通信,都要经过序列化、反序列化的过程,所以序列化方式,会直接影响 RPC 通信的性能。除了选择合适的序列化技术,如何合理使用序列化也非常重要。 - -RPC 序列化常见的使用不当的情况如下: - -- **对象过于复杂、庞大** - 对象过于复杂、庞大,会降低序列化、反序列化的效率,并增加传输开销,从而导致响应时延增大。 - - - 过于复杂:存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象 - - 过于庞大:比如一个大 List 或者大 Map - -- **对象有复杂的继承关系** - 对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。大多数序列化框架在进行序列化时,如果发现类有继承关系,会不停地寻找父类,遍历属性。 -- **使用序列化框架不支持的类作为入参类** - 比如 Hessian 框架,他天然是不支持 LinkHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,如 Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList。 - -### 序列化要点 - -前面已经列举了常见的序列化问题,既然明确了问题,就要针对性预防。RPC 序列化时要注意以下几点: - -1. 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚; -2. 入参对象与返回值对象体积不要太大,更不要传太大的集合; -3. 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类; -4. 对象不要有复杂的继承关系,最好不要有父子类的情况。 - -## RPC 通信 - -一次 RPC 调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。可见,通信时 RPC 实现的核心。 - -常见的网络 IO 模型有:同步阻塞(BIO)、同步非阻塞(NIO)、异步非阻塞(AIO)。 - -### IO 多路复用 - -IO 多路复用(Reactor 模式)在高并发场景下使用最为广泛,很多知名软件都应用了这一技术,如:Netty、Redis、Nginx 等。 - -IO 多路复用分为 select,poll 和 epoll。 - -什么是 IO 多路复用?字面上的理解,多路就是指多个通道,也就是多个网络连接的 IO,而复用就是指多个通道复用在一个复用器上。 - -### 零拷贝 - -系统内核处理 IO 操作分为两个阶段——等待数据和拷贝数据。等待数据,就是系统内核在等待网卡接收到数据后,把数据写到内核中;而拷贝数据,就是系统内核在获取到数据后,将数据拷贝到用户进程的空间中。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717154300) - -应用进程的每一次写操作,都会把数据写到用户空间的缓冲区中,再由 CPU 将数据拷贝到系统内核的缓冲区中,之后再由 DMA 将这份数据拷贝到网卡中,最后由网卡发送出去。这里我们可以看到,一次写操作数据要拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到数据。 - -应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要 CPU 进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程),这样很浪费 CPU 和性能。 - -所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,可以通过一种方式,直接将数据写入内核或从内核中读取数据,再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717154716.jfif) - -Netty 的零拷贝偏向于用户空间中对数据操作的优化,这对处理 TCP 传输中的拆包粘包问题有着重要的意义,对应用程序处理请求数据与返回数据也有重要的意义。 - -Netty 框架中很多内部的 ChannelHandler 实现类,都是通过 CompositeByteBuf、slice、wrap 操作来处理 TCP 传输中的拆包与粘包问题的。 - -Netty 的 ByteBuffer 可以采用 Direct Buffers,使用堆外直接内存进行 Socketd 的读写 -操作,最终的效果与我刚才讲解的虚拟内存所实现的效果是一样的。 - -Netty 还提供 FileRegion 中包装 NIO 的 FileChannel.transferTo() 方法实现了零拷 -贝,这与 Linux 中的 sendfile 方式在原理上也是一样的。 - -## RPC 动态代理 - -RPC 的远程过程调用是通过反射+动态代理实现的。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1553614585028.png) - -RPC 框架会自动为要调用的接口生成一个代理类。当在项目中注入接口的时候,运行过程中实际绑定的就是这个接口生成的代理类。在接口方法被调用时,会被代理类拦截,这样,就可以在生成的代理类中,加入远程调用逻辑。 - -除了 JDK 默认的 `InvocationHandler` 能完成代理功能,还有很多其他的第三方框架也可以,比如像 Javassist、Byte Buddy 这样的框架。 - -> 反射+动态代理更多详情可以参考:[深入理解 Java 反射和动态代理](https://dunwu.github.io/waterdrop/pages/0d066a/) - -## 参考资料 - -- [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/02.RPC\350\277\233\351\230\266.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/02.RPC\350\277\233\351\230\266.md" deleted file mode 100644 index 44481f44cf..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/02.RPC\350\277\233\351\230\266.md" +++ /dev/null @@ -1,222 +0,0 @@ ---- -title: RPC 进阶篇 -date: 2022-06-19 22:04:59 -order: 02 -categories: - - 分布式 - - 分布式通信 - - RPC - - RPC综合 -tags: - - 分布式 - - 分布式应用 - - 微服务 - - RPC -permalink: /pages/19f809/ ---- - -# RPC 进阶篇 - -## RPC 架构模型 - -了解前面的知识点(序列化、动态代理、通信),其实已经可以实现一个点对点的 RPC 架构了。 - -采用微内核架构的 RPC 架构模型: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200610164920.png) - -在 RPC 框架里面,怎么支持插件化架构的呢?我们可以将每个功能点抽象成一个接 -口,将这个接口作为插件的契约,然后把这个功能的接口与功能的实现分离,并提供接口的默认实现。在 Java 里面,JDK 有自带的 SPI(Service Provider Interface)服务发现机 -制,它可以动态地为某个接口寻找服务实现。使用 SPI 机制需要在 Classpath 下的 `META-INF/services` 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体实现类。 - -但在实际项目中,我们其实很少使用到 JDK 自带的 SPI 机制,首先它不能按需加载, -`ServiceLoader` 加载某个接口实现类的时候,会遍历全部获取,也就是接口的实现类得全部载入并实例化一遍,会造成不必要的浪费。另外就是扩展如果依赖其它的扩展,那就做不到自动注入和装配,这就很难和其他框架集成,比如扩展里面依赖了一个 Spring Bean,原生的 Java SPI 就不支持。 - -## 服务注册和发现 - -RPC 框架必须要有服务注册和发现机制,这样,集群中的节点才能知道通信方的请求地址。 - -- **服务注册**:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。 -- **服务订阅**:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。 - -### 基于 ZooKeeper 的服务发现 - -使用 ZooKeeper 作为服务注册中心,是 Java 分布式系统的经典方案。 - -搭建一个 ZooKeeper 集群作为注册中心集群,服务注册的时候只需要服务节点向 ZooKeeper 节点写入注册信息即可,利用 ZooKeeper 的 Watcher 机制完成服务订阅与服务下发功能 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200610180056.png) - -通常我们可以使用 ZooKeeper、etcd 或者分布式缓存(如 Hazelcast)来解决事件通知问题,但当集群达到一定规模之后,依赖的 ZooKeeper 集群、etcd 集群可能就不稳定了,无法满足我们的需求。 - -在超大规模的服务集群下,注册中心所面临的挑战就是超大批量服务节点同时上下线,注册中心集群接受到大量服务变更请求,集群间各节点间需要同步大量服务节点数据,最终导致如下问题: - -- 注册中心负载过高; -- 各节点数据不一致; -- 服务下发不及时或下发错误的服务节点列表。 - -RPC 框架依赖的注册中心的服务数据的一致性其实并不需要满足 CP,只要满足 AP 即可。 - -### 基于消息总线的最终一致性的注册中心 - -ZooKeeper 的一大特点就是强一致性,ZooKeeper 集群的每个节点的数据每次发生更新操作,都会通知其它 ZooKeeper 节点同时执行更新。它要求保证每个节点的数据能够实时的完全一致,这也就直接导致了 ZooKeeper 集群性能上的下降。 - -而 RPC 框架的服务发现,在服务节点刚上线时,服务调用方是可以容忍在一段时间之后 -(比如几秒钟之后)发现这个新上线的节点的。毕竟服务节点刚上线之后的几秒内,甚至更长的一段时间内没有接收到请求流量,对整个服务集群是没有什么影响的,所以我们可以牺牲掉 CP(强制一致性),而选择 AP(最终一致),来换取整个注册中心集群的性能和稳定性。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717162006.png) - -## 健康检查 - -**使用频率适中的心跳去检测目标机器的健康状态**。 - -- 健康状态:建立连接成功,并且心跳探活也一直成功; -- 亚健康状态:建立连接成功,但是心跳请求连续失败; -- 死亡状态:建立连接失败。 - -可以**使用可用率来作为健康状态的量化标准**: - -``` -可用率 = 一个时间窗口内接口调用成功次数 / 总调用次数 -``` - -当可用率低于某个比例,就认为这个节点存在问题,把它挪到亚健康列表,这样既考虑了高低频的调用接口,也兼顾了接口响应时间不同的问题。 - -## 路由和负载均衡 - -对于服务调用方来说,一个接口会有多个服务提供方同时提供服务,所以我们的 RPC 在每次发起请求的时候,都需要从多个服务提供方节点里面选择一个用于发请求的节点。这被称为路由策略。 - -- IP 路由:最简单的当然是 IP 路由,因为服务上线后,会暴露服务到注册中心,将自身 IP、端口等元信息告知注册中心。这样消费方就可以在向注册中心请求服务地址时,感知其存在。 -- 参数路由:但有时,会有一些复杂的场景,如:灰度发布、定点调用,我们并不希望上线的服务被所有消费者感知,为了更加细粒度的控制,可以使用参数路由。通过参数控制通信的路由策略。 - -除了特殊场景的路由策略以外,对于机器中多个服务方,如何选择调用哪个服务节点,可以应用负载均衡策略。RPC 负载均衡策略一般包括随机、轮询、一致性 Hash、最近最少连接等。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717163401.png) - -> 负载均衡详情可以参考:[负载均衡基本原理](https://dunwu.github.io/waterdrop/pages/98a1c1/) - -### 超时重试 - -超时重试机制是指:当调用端发起的请求失败或超时未收到响应时,RPC 框架自身可以进行重试,再重新发送请求,用户可以自行设置是否开启重试以及重试的次数。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200610193748.png) - -### 限流、降级、熔断 - -限流方案:Redis + lua、Sentinel - -熔断方案:Hystrix - -### 优雅启动关闭 - -如何避免服务停机带来的业务损失: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200610193806.png) - -如何避免流量打到没有启动完成的节点: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200610193829.png) - -## 容错处理 - -### 异常重试 - -就是当调用端发起的请求失败时,RPC 框架自身可以进行重试,再重新发送请求,用户可以自行设置是否开启重试以及重试的次数。 - -当然,不是所有的异常都要触发重试,只有符合重试条件的异常才能触发重试,比如网络超时异常、网络连接异常等等(这个需要 RPC 去判定)。 - -> 注意:有时网络可能发生抖动,导致请求超时,这时如果 RPC 触发超时重试,会触发业务逻辑重复执行,如果接口没有幂等性设计,就可能引发问题。如:重发写表。 - -### 重试超时时间 - -连续的异常重试可能会出现一种不可靠的情况,那就是连续的异常重试并且每次处理的请求时间比较长,最终会导致请求处理的时间过长,超出用户设置的超时时间。 - -解决这个问题最直接的方式就是,在每次重试后都重置一下请求的超时时间。 - -当调用端发起 RPC 请求时,如果发送请求发生异常并触发了异常重试,我们可以先判定下这个请求是否已经超时,如果已经超时了就直接返回超时异常,否则就先重置下这个请求的超时时间,之后再发起重试。 - -在所有发起重试、负载均衡选择节点的时候,去掉重试之前出现过问题的那个节点,以保证重试的成功率。 - -### 业务异常 - -RPC 框架是不会知道哪些业务异常能够去进行异常重试的,我们可以加个重试异常的白名 -单,用户可以将允许重试的异常加入到这个白名单中。当调用端发起调用,并且配置了异常重试策略,捕获到异常之后,我们就可以采用这样的异常处理策略。如果这个异常是 RPC 框架允许重试的异常,或者这个异常类型存在于可重试异常的白名单中,我们就允许对这个请求进行重试。 - ---- - -综上,一个可靠的 RPC 容错处理机制如下: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717163921.png) - -## 优雅上线下线 - -如何避免服务停机带来的业务损失? - -### 优雅下线 - -当服务提供方正在关闭,如果这之后还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方(比如 ShutdownException)。这个异常就是告诉调用方“我已经收到这个请求了,但是我正在关闭,并没有处理这个请求”,然后调用方收到这个异常响应后,RPC 框架把这个节点从健康列表挪出,并把请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试到其他节点,这样就可以实现对业务无损。 - -在 Java 语言里面,对应的是 Runtime.addShutdownHook 方法,可以注册关闭的钩子。在 RPC 启动的时候,我们提前注册关闭钩子,并在里面添加了两个处理程序,一个负责开启关闭标识,一个负责安全关闭服务对象,服务对象在关闭的时候会通知调用方下线节点。同时需要在我们调用链里面加上挡板处理器,当新的请求来的时候,会判断关闭标识,如果正在关闭,则抛出特定异常。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220630194749.png) - -### 优雅上线 - -#### 启动预热 - -启动预热,就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。 - -首先,在真实环境中机器都会默认开启 NTP 时间同步功能,来保证所有机器时间的一致性。 - -调用方通过服务发现,除了可以拿到 IP 列表,还可以拿到对应的启动时间。我们需要把这个时间作用在负载均衡上。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220630194822.png) - -通过这个小逻辑的改动,我们就可以保证当服务提供方运行时长小于预热时间时,对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。 - -#### 延迟暴露 - -服务提供方应用在没有启动完成的时候,调用方的请求就过来了,而调用方请求过来的原因是,服务提供方应用在启动过程中把解析到的 RPC 服务注册到了注册中心,这就导致在后续加载没有完成的情况下服务提供方的地址就被服务调用方感知到了。 - -为了解决这个问题,需要在应用启动加载、解析 Bean 的时候,如果遇到了 RPC 服务的 Bean,只先把这个 -Bean 注册到 Spring-BeanFactory 里面去,而并不把这个 Bean 对应的接口注册到注册中心,只有等应用启动完成后,才把接口注册到注册中心用于服务发现,从而实现让服务调用方延迟获取到服务提供方地址。 - -具体如何实现呢? - -我们可以在服务提供方应用启动后,接口注册到注册中心前,预留一个 Hook 过程,让用户可以实现可扩展的 -Hook 逻辑。用户可以在 Hook 里面模拟调用逻辑,从而使 JVM 指令能够预热起来,并且用户也可以在 Hook 里面事先预加载一些资源,只有等所有的资源都加载完成后,最后才把接口注册到注册中心。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220630194919.png) - -## 限流熔断 - -限流算法有很多,比如最简单的计数器,还有可以做到平滑限流的滑动窗口、漏斗算法以及令牌桶算法等等。其中令牌桶算法最为常用。 - -服务端主要是通过限流来进行自我保护,我们在实现限流时要考虑到应用和 IP 级别,方便我们在服务治理的时候,对部分访问量特别大的应用进行合理的限流。 - -服务端的限流阈值配置都是作用于单机的,而在有些场景下,例如对整个服务设置限流阈值,服务进行扩容时, -限流的配置并不方便,我们可以在注册中心或配置中心下发限流阈值配置的时候,将总服务节点数也下发给服务节点,让 RPC 框架自己去计算限流阈值。 - -我们还可以让 RPC 框架的限流模块依赖一个专门的限流服务,对服务设置限流阈值进行精准地控制,但是这种方式依赖了限流服务,相比单机的限流方式,在性能和耗时上有劣势。 - -调用端可以通过熔断机制进行自我保护,防止调用下游服务出现异常,或者耗时过长影响调 -用端的业务逻辑,RPC 框架可以在动态代理的逻辑中去整合熔断器,实现 RPC 框架的熔断 -功能。 - -## 业务分组 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200718204407.png) - -在 RPC 里面我们可以通过分组的方式人为地给不同的调用方划分出不同的小集群,从而实现调用方流量隔离的效果,保障我们的核心业务不受非核心业务的干扰。但我们在考虑问题的时候,不能顾此失彼,不能因为新加一个的功能而影响到原有系统的稳定性。 - -其实我们不仅可以通过分组把服务提供方划分成不同规模的小集群,我们还可以利用分组完成一个接口多种实现的功能。正常情况下,为了方便我们自己管理服务,我一般都会建议每个接口完成的功能尽量保证唯一。但在有些特殊场景下,两个接口也会完全一样,只是具体实现上有那么一点不同,那么我们就可以在服务提供方应用里面同时暴露两个相同接口,但只是接口分组不一样罢了。 - -### 动态分组 - -分组可以帮助服务提供方实现调用方的隔离。但是因为调用方流量并不是一成不变的,而且还可能会因为突发事件导致某个分组的流量溢出,而在整个大集群还有富余能力的时候,又因为分组隔离不能为出问题的集群提供帮助。 - -为了解决这种突发流量的问题,我们提供了一种更高效的方案,可以实现分组的快速伸缩。事实上我们还可以利用动态分组解决分组后给每个分组预留机器冗余的问题,我们没有必要把所有冗余的机器都分配到分组里面,我们可以把这些预留的机器做成一个共享的池子,从而减少整体预留的实例数量。 - -## 参考资料 - -- [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/03.RPC\351\253\230\347\272\247.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/03.RPC\351\253\230\347\272\247.md" deleted file mode 100644 index d55395cc8f..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/00.RPC\347\273\274\345\220\210/03.RPC\351\253\230\347\272\247.md" +++ /dev/null @@ -1,111 +0,0 @@ ---- -title: RPC 高级篇 -date: 2022-06-23 17:04:13 -order: 03 -categories: - - 分布式 - - 分布式通信 - - RPC - - RPC综合 -tags: - - 分布式 - - 分布式应用 - - 微服务 - - RPC -permalink: /pages/3698ef/ ---- - -# RPC 高级篇 - -## 异步 RPC - -## 链路跟踪 - -分布式链路跟踪就是将一次分布式请求还原为一个完整的调用链路,我们可以在整个调用链路中跟踪到这一次分布式请求的每一个环节的调用情况,比如调用是否成功,返回什么异常,调用的哪个服务节点以及请求耗时等等。 - -Trace 就是代表整个链路,每次分布式都会产生一个 Trace,每个 Trace 都有它的唯一标识即 TraceId,在分布式链路跟踪系统中,就是通过 TraceId 来区分每个 Trace 的。 -Span 就是代表了整个链路中的一段链路,也就是说 Trace 是由多个 Span 组成的。在一个 Trace 下,每个 Span 也都有它的唯一标识 SpanId,而 Span 是存在父子关系的。还是以讲过的例子为例子,在 A->B->C->D 的情况下,在整个调用链中,正常情况下会产生 3 个 Span,分别是 Span1(A->B)、Span2(B->C)、Span3(C->D),这时 Span3 的父 Span 就是 Span2,而 Span2 的父 Span 就是 Span1。 - -RPC 在整合分布式链路跟踪需要做的最核心的两件事就是“埋点”和“传递”。 - -我们前面说是因为各子应用、子服务间复杂的依赖关系,所以通过日志难定位问题。那我们就想办法通过日志定位到是哪个子应用的子服务出现问题就行了。 - -其实,在 RPC 框架打印的异常信息中,是包括定位异常所需要的异常信息的,比如是哪类异常引起的问题(如序列化问题或网络超时问题),是调用端还是服务端出现的异常,调用端与服务端的 IP 是什么,以及服务接口与服务分组都是什么等等。具体如下图所示: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200719082205.png) - -## 泛化调用 - -在一些特定场景下,需要在没有接口的情况下进行 RPC 调用。例如: - -场景一:搭建一个统一的测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的 RPC 服务。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200719095518.png) - -场景二:搭建一个轻量级的服务网关,可以让各个业务方用 HTTP 的方式,通过服务网关调用其它服务。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200719095704.png) - -为了解决这些场景的问题,可以使用泛化调用。 - -就是 RPC 框架提供统一的泛化调用接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,通过调用 GenericService 代理的 \$invoke 方法将服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等封装成请求消息,发送给服务端,实现在没有接口的情况下进行 -RPC 调用的功能。 - -```java -class GenericService { -Object $invoke(String methodName, String[] paramTypes, Object[] params); -CompletableFuture $asyncInvoke(String methodName, String[] paramTypes -} -``` - -而通过泛化调用的方式发起调用,由于调用端没有服务端提供方提供的接口 API,不能正常地进行序列化与反序列化,我们可以为泛化调用提供专属的序列化插件,来解决实际问题。 - -## 时钟轮 - -时钟轮这个机制很好地解决了定时任务中,因每个任务都创建一个线程,导致的创建过多线程的问题,以及一个线程扫描所有的定时任务,让 CPU 做了很多额外的轮询遍历操作而浪费 CPU 的问题。 - -时钟轮的实现机制就是模拟现实生活中的时钟,将每个定时任务放到对应的时间槽位上,这样可以减少扫描任务时对其它时间槽位定时任务的额外遍历操作。 - -在时间轮的使用中,有些问题需要你额外注意: - -时间槽位的单位时间越短,时间轮触发任务的时间就越精确。例如时间槽位的单位时间是 10 毫秒,那么执行定时任务的时间误差就在 10 毫秒内,如果是 100 毫秒,那么误差就在 100 毫秒内。 - -时间轮的槽位越多,那么一个任务被重复扫描的概率就越小,因为只有在多层时钟轮中的任务才会被重复扫描。比如一个时间轮的槽位有 1000 个,一个槽位的单位时间是 10 毫秒,那么下一层时间轮的一个槽位的单位时间就是 10 秒,超过 10 秒的定时任务会被放到下一层时间轮中,也就是只有超过 10 秒的定时任务会被扫描遍历两次,但如果槽位是 10 个,那么超过 100 毫秒的任务,就会被扫描遍历两次。 - -结合这些特点,我们就可以视具体的业务场景而定,对时钟轮的周期和时间槽数进行设置。 - -在 RPC 框架中,只要涉及到定时任务,我们都可以应用时钟轮,比较典型的就是调用端的超时处理、调用端与服务端的启动超时以及定时心跳等等。 - -## 流量回放 - -所谓的流量就是某个时间段内的所有请求,我们通过某种手段把发送到 A 应用的所有请求录制下来,然后把这些请求统一转发到 B 应用,让 B 应用接收到的请求参数跟 A 应用保持一致,从而实现 A 接收到的请求在 B 应用里面重新请求了一遍。整个过程称之为“**流量回放**”。 - -流量回放可以做什么? - -为了保障应用升级后,我们的业务行为还能保持和升级前一样,我们在大多数情况下都是依靠已有的 TestCase 去验证,但这种方式在一定程度上并不是完全可靠的。最可靠的方式就是引入线上 Case 去验证改造后的应用,把线上的真实流量在改造后的应用里面进行回放,这样不仅节省整个上线时间,还能弥补手动维护 Case 存在的缺陷。 - -应用引入了 RPC 后,所有的请求流量都会被 RPC 接管,所以我们可以很自然地在 RPC 里面支持流量回放功能。虽然这个功能本身并不是 RPC 的核心功能,但对于使用 RPC 的人来说,他们有了这个功能之后,就可以更放心地升级自己的应用了。 - -## RPC 高级 - -### RPC 性能 - -如何提升单机吞吐量? - -大多数情况下,影响到 RPC 调用的吞吐量的原因也就是业务逻辑处理慢了,CPU 大部分时间都在等待资源。 - -为了解决等待的耗时,可以使用**异步**。异步可以使用 Future 或 Callback 方式,Future 最为简单。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220630195115.png) - -另外,我们可以通过对 CompletableFuture 的支持,实现 RPC 调用在调用端与服务端之间的完全异步,同时提升两端的单机吞吐量。 - -### RPC 安全 - -虽然 RPC 经常用于解决内网应用之间的调用,内网环境相对公网也没有那么恶劣,但我们也有必要去建立一套可控的安全体系,去防止一些错误行为。对于 RPC 来说,我们所关心的安全问题不会有公网应用那么复杂,我们只要保证让服务调用方能拿到真实的服务提供方 IP 地址集合,且服务提供方可以管控调用自己的应用就够了。 - -服务提供方应用里面放一个用于 HMAC 签名的私钥,在授权平台上用这个私钥为申请调用的调用方应用进行签名,这个签名生成的串就变成了调用方唯一的身份。服务提供方在收到调用方的授权请求之后,我们只要需要验证下这个签名跟调用方应用信息是否对应得上就行了,这样集中式授权的瓶颈也就不存在了。 - -## 参考资料 - -- [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/01.Dubbo/01.Dubbo.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/01.Dubbo/01.Dubbo.md" deleted file mode 100644 index c6014e444a..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/01.Dubbo/01.Dubbo.md" +++ /dev/null @@ -1,891 +0,0 @@ ---- -title: Dubbo 快速入门 -date: 2022-02-17 22:34:30 -order: 01 -categories: - - 分布式 - - 分布式通信 - - RPC - - Dubbo -tags: - - 分布式 - - 分布式应用 - - 微服务 - - Dubbo -permalink: /pages/3a499f/ ---- - -# Dubbo 快速入门 - -> [Apache Dubbo](https://dubbo.apache.org/zh-cn/) 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。 - -## 一、Dubbo 简介 - -[Apache Dubbo](https://dubbo.apache.org/zh-cn/) 是一款高性能、轻量级的开源 Java RPC 框架。 - -Dubbo 提供了三大核心能力: - -- 面向接口的远程方法调用 -- 智能容错和负载均衡 -- 服务自动注册和发现 - -### RPC 原理简介 - -#### 什么是 RPC - -RPC(Remote Procedure Call),即远程过程调用,它是一种通过网络从远程计算机程序上请求服务,而不需要了解底层网络技术的协议。比如两个不同的服务 A、B 部署在两台不同的机器上,那么服务 A 如果想要调用服务 B 中的某个方法该怎么办呢?使用 HTTP 请求 当然可以,但是可能会比较慢而且一些优化做的并不好。 RPC 的出现就是为了解决这个问题。 - -#### RPC 工作流程 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200305121252.jpg) - -1. 服务消费方(client)调用以本地调用方式调用服务; -2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; -3. client stub 找到服务地址,并将消息发送到服务端; -4. server stub 收到消息后进行解码; -5. server stub 根据解码结果调用本地的服务; -6. 本地服务执行并将结果返回给 server stub; -7. server stub 将返回结果打包成消息并发送至消费方; -8. client stub 接收到消息,并进行解码; -9. 服务消费方得到最终结果。 - -### 为什么需要 Dubbo - -**如果你要开发分布式程序,你也可以直接基于 HTTP 接口进行通信,但是为什么要用 Dubbo 呢?** - -我觉得主要可以从 Dubbo 提供的下面四点特性来说为什么要用 Dubbo: - -1. **负载均衡**——同一个服务部署在不同的机器时该调用那一台机器上的服务。 -2. **服务调用链路**——随着系统的发展,服务越来越多,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。Dubbo 可以为我们解决服务之间互相是如何调用的。 -3. **服务访问压力以及时长统计、资源调度和治理**——基于访问压力实时管理集群容量,提高集群利用率。 -4. **服务治理**——某个服务挂掉之后调用备用服务。 - -另外,Dubbo 除了能够应用在分布式系统中,也可以应用在现在比较火的微服务系统中。不过,由于 Spring Cloud 在微服务中应用更加广泛,所以,我觉得一般我们提 Dubbo 的话,大部分是分布式系统的情况。 - -## 二、QuickStart - -(1)添加 maven 依赖 - -```xml - - com.alibaba - dubbo - ${dubbo.version} - -``` - -(2)定义 Provider - -```java -package com.alibaba.dubbo.demo; - -public interface DemoService { - String sayHello(String name); -} -``` - -(3)实现 Provider - -```java -package com.alibaba.dubbo.demo.provider; -import com.alibaba.dubbo.demo.DemoService; - -public class DemoServiceImpl implements DemoService { - public String sayHello(String name) { - return "Hello " + name; - } -} -``` - -(4)配置 Provider - -```xml - - - - - - - - -``` - -(5)启动 Provider - -```java -import org.springframework.context.support.ClassPathXmlApplicationContext; - -public class Provider { - public static void main(String[] args) throws Exception { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - new String[] {"META-INF/spring/dubbo-demo-provider.xml"}); - context.start(); - // press any key to exit - System.in.read(); - } -} -``` - -(6)配置 Consumer - -```xml - - - - - - -``` - -(7)启动 Consumer - -```java -import com.alibaba.dubbo.demo.DemoService; -import org.springframework.context.support.ClassPathXmlApplicationContext; - -public class Consumer { - public static void main(String[] args) throws Exception { - ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext( - new String[]{"META-INF/spring/dubbo-demo-consumer.xml"}); - context.start(); - // obtain proxy object for remote invocation - DemoService demoService = (DemoService) context.getBean("demoService"); - // execute remote invocation - String hello = demoService.sayHello("world"); - // show the result - System.out.println(hello); - } -} -``` - -## 三、Dubbo 配置 - -Dubbo 所有配置最终都将转换为 URL 表示,并由服务提供方生成,经注册中心传递给消费方,各属性对应 URL 的参数,参见配置项一览表中的 "对应 URL 参数" 列。 - -只有 group,interface,version 是服务的匹配条件,三者决定是不是同一个服务,其它配置项均为调优和治理参数。 - -URL 格式:`protocol://username:password@host:port/path?key=value&key=value` - -### 配置方式 - -Dubbo 支持多种配置方式: - -- xml 配置 -- properties 配置 -- API 配置 -- 注解配置 - -如果同时存在多种配置方式,遵循以下覆盖策略: - -- JVM 启动 -D 参数优先,这样可以使用户在部署和启动时进行参数重写,比如在启动时需改变协议的端口。 -- XML 次之,如果在 XML 中有配置,则 dubbo.properties 中的相应配置项无效。 -- Properties 最后,相当于缺省值,只有 XML 没有配置时,dubbo.properties 的相应配置项才会生效,通常用于共享公共配置,比如应用名。 - -
    - -
    - -#### xml 配置 - -示例: - -```xml - - - - - - - - - - - - - - - - -``` - -Dubbo 会把以上配置项解析成下面的 URL 格式: - -``` -dubbo://host-ip:20880/com.alibaba.dubbo.demo.DemoService -``` - -然后基于[扩展点自适应机制](http://dubbo.incubator.apache.org/zh-cn/docs/dev/SPI.html),通过 URL 的 `dubbo://` 协议头识别,就会调用 DubboProtocol 的 export() 方法,打开服务端口 20880,就可以把服务 demoService 暴露到 20880 端口了。 - -#### properties 配置 - -示例: - -```properties -dubbo.application.name=foo -dubbo.application.owner=bar -dubbo.registry.address=10.20.153.10:9090 -``` - -### 配置项 - -所有配置项分为三大类: - -- 服务发现:表示该配置项用于服务的注册与发现,目的是让消费方找到提供方。 -- 服务治理:表示该配置项用于治理服务间的关系,或为开发测试提供便利条件。 -- 性能调优:表示该配置项用于调优性能,不同的选项对性能会产生影响。 - -配置项清单: - -| 标签 | 用途 | 解释 | -| ------------------- | ------------ | ------------------------------------------------------------------------------------------------ | -| `dubbo:service` | 服务配置 | 用于暴露一个服务,定义服务的元信息,一个服务可以用多个协议暴露,一个服务也可以注册到多个注册中心 | -| `dubbo:reference` | 引用配置 | 用于创建一个远程服务代理,一个引用可以指向多个注册中心 | -| `dubbo:protocol` | 协议配置 | 用于配置提供服务的协议信息,协议由提供方指定,消费方被动接受 | -| `dubbo:application` | 应用配置 | 用于配置当前应用信息,不管该应用是提供者还是消费者 | -| `dubbo:module` | 模块配置 | 用于配置当前模块信息,可选 | -| `dubbo:registry` | 注册中心配置 | 用于配置连接注册中心相关信息 | -| `dubbo:monitor` | 监控中心配置 | 用于配置连接监控中心相关信息,可选 | -| `dubbo:provider` | 提供方配置 | 当 ProtocolConfig 和 ServiceConfig 某属性没有配置时,采用此缺省值,可选 | -| `dubbo:consumer` | `消费方配置` | `当 ReferenceConfig 某属性没有配置时,采用此缺省值,可选` | -| `dubbo:method` | 方法配置 | 用于 ServiceConfig 和 ReferenceConfig 指定方法级的配置信息 | -| `dubbo:argument` | 参数配置 | 用于指定方法参数配置 | - -> 详细配置说明请参考:[官方配置](http://dubbo.apache.org/books/dubbo-user-book/references/xml/introduction.html) - -#### 配置之间的关系 - -
    - -
    - -#### 配置覆盖关系 - -以 timeout 为例,显示了配置的查找顺序,其它 retries, loadbalance, actives 等类似: - -- **方法级优先,接口级次之,全局配置再次之**。 -- **如果级别一样,则消费方优先,提供方次之**。 - -其中,服务提供方配置,通过 URL 经由注册中心传递给消费方。 - -
    - -
    -### 动态配置中心 - -配置中心(v2.7.0)在 Dubbo 中承担两个职责: - -1. 外部化配置。启动配置的集中式存储 (简单理解为 dubbo.properties 的外部化存储)。 -2. 服务治理。服务治理规则的存储与通知。 - -启用动态配置: - -```xml - -``` - -或者 - -```properties -dubbo.config-center.address=zookeeper://127.0.0.1:2181 -``` - -或者 - -```java -ConfigCenterConfig configCenter = new ConfigCenterConfig(); -configCenter.setAddress("zookeeper://127.0.0.1:2181"); -``` - -## 四、Dubbo 架构 - -### Dubbo 核心组件 - -
    - -
    - -节点角色: - -| 节点 | 角色说明 | -| --------- | -------------------------------------- | -| Provider | 暴露服务的服务提供方 | -| Consumer | 调用远程服务的服务消费方 | -| Registry | 服务注册与发现的注册中心 | -| Monitor | 统计服务的调用次数和调用时间的监控中心 | -| Container | 服务运行容器 | - -调用关系: - -1. 服务容器负责启动,加载,运行服务提供者。 -2. 服务提供者在启动时,向注册中心注册自己提供的服务。 -3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 -4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 -5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 -6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 - -**重要知识点总结:** - -- **注册中心负责服务地址的注册与查找,相当于目录服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小** -- **监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示** -- **注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外** -- **注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者** -- **注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表** -- **注册中心和监控中心都是可选的,服务消费者可以直连服务提供者** -- **服务提供者无状态,任意一台宕掉后,不影响使用** -- **服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复** - -> 问:注册中心挂了可以继续通信吗? -> -> 答:可以,因为刚开始初始化的时候,消费者会将提供者的地址等信息**拉取到本地缓存**,所以注册中心挂了可以继续通信。 - -### Dubbo 架构层次 - -
    - -
    - -图例说明: - -- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。 -- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。 -- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。 -- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。 - -#### 各层说明 - -- **config 配置层**:对外配置接口,以 ServiceConfig, ReferenceConfig 为中心,可以直接初始化配置类,也可以通过 spring 解析配置生成配置类 -- **proxy 服务代理层**:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton, 以 ServiceProxy 为中心,扩展接口为 ProxyFactory -- **registry 注册中心层**:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 RegistryFactory, Registry, RegistryService -- **cluster 路由层**:封装多个提供者的路由及负载均衡,并桥接注册中心,以 Invoker 为中心,扩展接口为 Cluster, Directory, Router, LoadBalance -- **monitor 监控层**:RPC 调用次数和调用时间监控,以 Statistics 为中心,扩展接口为 MonitorFactory, Monitor, MonitorService -- **protocol 远程调用层**:封装 RPC 调用,以 Invocation, Result 为中心,扩展接口为 Protocol, Invoker, Exporter -- **exchange 信息交换层**:封装请求响应模式,同步转异步,以 Request, Response 为中心,扩展接口为 Exchanger, ExchangeChannel, ExchangeClient, ExchangeServer -- **transport 网络传输层**:抽象 mina 和 netty 为统一接口,以 Message 为中心,扩展接口为 Channel, Transporter, Client, Server, Codec -- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool -- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 Serialization, ObjectInput, ObjectOutput, ThreadPool - -#### 各层关系说明 - -- 在 RPC 中,Protocol 是核心层,也就是只要有 Protocol + Invoker + Exporter 就可以完成非透明的 RPC 调用,然后在 Invoker 的主过程上 Filter 拦截点。 -- 图中的 Consumer 和 Provider 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 Provider, Consumer, Registry, Monitor 划分逻辑拓普节点,保持统一概念。 -- 而 Cluster 是外围概念,所以 Cluster 的目的是将多个 Invoker 伪装成一个 Invoker,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。 -- Proxy 层封装了所有接口的透明化代理,而在其它层都以 Invoker 为中心,只有到了暴露给用户使用时,才用 Proxy 将 Invoker 转成接口,或将接口实现转成 Invoker,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。 -- 而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,Transport 层只负责单向消息传输,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 Exchange 层是在传输层之上封装了 Request-Response 语义。 -- Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。 - -## 五、服务发现 - -服务提供者注册服务的过程: - -Dubbo 配置项 `dubbo://registry` 声明了注册中心的地址,Dubbo 会把以上配置项解析成类似下面的 URL 格式: - -``` -registry://multicast://224.5.6.7:1234/com.alibaba.dubbo.registry.RegistryService?export=URL.encode("dubbo://host-ip:20880/com.alibaba.dubbo.demo.DemoService") -``` - -然后基于扩展点自适应机制,通过 URL 的“registry://”协议头识别,就会调用 RegistryProtocol 的 export() 方法,将 export 参数中的提供者 URL,注册到注册中心。 - -服务消费者发现服务的过程: - -Dubbo 配置项 `dubbo://registry` 声明了注册中心的地址,跟服务注册的原理类似,Dubbo 也会把以上配置项解析成下面的 URL 格式: - -``` -registry://multicast://224.5.6.7:1234/com.alibaba.dubbo.registry.RegistryService?refer=URL.encode("consummer://host-ip/com.alibaba.dubbo.demo.DemoService") -``` - -然后基于扩展点自适应机制,通过 URL 的“registry://”协议头识别,就会调用 RegistryProtocol 的 refer() 方法,基于 refer 参数中的条件,查询服务 demoService 的地址。 - -### 启动时检查 - -Dubbo 缺省会在启动时检查依赖的服务是否可用,不可用时会抛出异常,阻止 Spring 初始化完成,以便上线时,能及早发现问题,默认 `check="true"`。 - -可以通过 xml、properties、-D 参数三种方式设置。启动时检查 - -## 六、Dubbo 协议 - -Dubbo 支持多种通信协议,不同的协议针对不同的序列化方式。 - -### dubbo 协议 - -[dubbo](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/dubbo.html) 协议是 Dubbo 的默认通信协议,采用单一长连接和 NIO 异步通信,基于 hessian 作为序列化协议。 - -[dubbo](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/dubbo.html) 协议适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。 - -为了要支持高并发场景,一般是服务提供者就几台机器,但是服务消费者有上百台,可能每天调用量达到上亿次!此时用长连接是最合适的,就是跟每个服务消费者维持一个长连接就可以,可能总共就 100 个连接。然后后面直接基于长连接 NIO 异步通信,可以支撑高并发请求。 - -### rmi 协议 - -[rmi](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/rmi.html) - 采用 JDK 标准的 `java.rmi.*` 实现,采用阻塞式短连接和 JDK 标准序列化方式。 - -注意:如果正在使用 RMI 提供服务给外部访问,同时应用里依赖了老的 `common-collections` 包的情况下,存在反序列化安全风险。 - -### hessian 协议 - -[hessian](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/hessian.html) 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。 - -Dubbo 的 Hessian 协议可以和原生 Hessian 服务互操作,即: - -- 提供者用 Dubbo 的 Hessian 协议暴露服务,消费者直接用标准 Hessian 接口调用 -- 或者提供方用标准 Hessian 暴露服务,消费方用 Dubbo 的 Hessian 协议调用。 - -### thrift 协议 - -当前 dubbo 支持的 [thrift](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/thrift.html) 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。 - -使用 dubbo thrift 协议同样需要使用 thrift 的 idl compiler 编译生成相应的 java 代码,后续版本中会在这方面做一些增强。 - -### http 协议 - -[http](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/http.html) 协议基于 HTTP 表单的远程调用协议,采用 Spring 的 HttpInvoker 实现。 - -使用 JSON 序列化方式。 - -### webservice 协议 - -基于 WebService 的远程调用协议,基于 [Apache CXF](http://cxf.apache.org/) 的 `frontend-simple` 和 `transports-http` 实现。 - -使用 SOAP 序列化方式。 - -可以和原生 WebService 服务互操作,即: - -- 提供者用 Dubbo 的 WebService 协议暴露服务,消费者直接用标准 WebService 接口调用, -- 或者提供方用标准 WebService 暴露服务,消费方用 Dubbo 的 WebService 协议调用。 - -### rest 协议 - -基于标准的 Java REST API——JAX-RS 2.0(Java API for RESTful Web Services 的简写)实现的 REST 调用支持 - -### memcached 协议 - -基于 memcached 实现的 RPC 协议。 - -### redis 协议 - -基于 redis 实现的 RPC 协议。 - -> 在现实世界中,序列化有多种方式。 -> -> JDK 自身提供的序列化方式,效率不高,但是 Java 程序使用最多。 -> -> 如果想要较好的可读性,可以使用 JSON (常见库有:[jackson](https://github.com/FasterXML/jackson)、[gson](https://github.com/google/gson)、[fastjson](https://github.com/alibaba/fastjson))或 SOAP (即 xml 形式) -> -> 如果想要更好的性能,可以使用 [thrift](https://github.com/apache/thrift)、[protobuf](https://github.com/protocolbuffers/protobuf)、[hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) -> -> 想深入了解可以参考:[序列化](https://github.com/dunwu/java-tutorial/blob/master/docs/lib/serialized) - -## 七、集群容错 - -在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。 - -
    - -
    - -- **Failover** - **失败自动切换**,当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 retries="2" 来设置重试次数(不含第一次)。 -- **Failfast** - **快速失败**,只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 -- **Failsafe** - **失败安全**,出现异常时,直接忽略。通常用于写入审计日志等操作。 -- **Failback** - **失败自动恢复**,后台记录失败请求,定时重发。通常用于消息通知操作。 -- **Forking** - **并行调用多个服务器**,只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 forks="2" 来设置最大并行数。 -- **Broadcast** - **广播调用所有提供者**,逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 - -集群容错配置示例: - -```xml - - -``` - -## 八、负载均衡 - -Dubbo 提供了多种负载均衡(LoadBalance)策略,缺省为 `Random` 随机调用。 - -Dubbo 的负载均衡配置可以细粒度到服务、方法级别,且 `dubbo:service` 和 `dubbo:reference` 均可配置。 - -```xml - - - - - - - - - - - - -``` - -#### Random - -- **随机**,按权重设置随机概率。 -- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 - -#### RoundRobin - -- **轮询**,按公约后的权重设置轮询比率。 -- 存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 - -#### LeastActive - -- **最少活跃调用数**,相同活跃数的随机,活跃数指调用前后计数差。 -- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大。 - -#### ConsistentHash - -- **一致性 Hash**,相同参数的请求总是发到同一提供者。 -- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 -- 算法参见:[http://en.wikipedia.org/wiki/Consistent_hashing](http://en.wikipedia.org/wiki/Consistent_hashing) -- 缺省只对第一个参数 Hash,如果要修改,请配置 `` -- 缺省用 160 份虚拟节点,如果要修改,请配置 `` - -## 九、Dubbo 服务治理 - -### 服务治理简介 - -- 当服务越来越多时,服务 URL 配置管理变得非常困难,F5 硬件负载均衡器的单点压力也越来越大。 -- 当进一步发展,服务间依赖关系变得错踪复杂,甚至分不清哪个应用要在哪个应用之前启动,架构师都不能完整的描述应用的架构关系。 -- 接着,服务的调用量越来越大,服务的容量问题就暴露出来,这个服务需要多少机器支撑?什么时候该加机器? - -以上问题可以归纳为服务治理问题,这也是 Dubbo 的核心功能。 - -#### 调用链路 - -一个微服务架构,往往由大量分布式服务组成。那么这些服务之间互相是如何调用的?调用链路是啥?说实话,几乎到后面没人搞的清楚了,因为服务实在太多了,可能几百个甚至几千个服务。 - -那就需要基于 dubbo 做的分布式系统中,对各个服务之间的调用自动记录下来,然后自动将**各个服务之间的依赖关系和调用链路生成出来**,做成一张图,显示出来,大家才可以看到对吧。 - -#### 服务访问压力以及时长统计 - -需要自动统计**各个接口和服务之间的调用次数以及访问延时**,而且要分成两个级别。 - -- 一个级别是接口粒度,就是每个服务的每个接口每天被调用多少次,TP50/TP90/TP99,三个档次的请求延时分别是多少; -- 第二个级别是从源头入口开始,一个完整的请求链路经过几十个服务之后,完成一次请求,每天全链路走多少次,全链路请求延时的 TP50/TP90/TP99,分别是多少。 - -#### 其他 - -- 服务分层(避免循环依赖) -- 调用链路失败监控和报警 -- 服务鉴权 -- 每个服务的可用性的监控(接口调用成功率?几个 9?99.99%,99.9%,99%) - -所谓失败重试,就是 consumer 调用 provider 要是失败了,比如抛异常了,此时应该是可以重试的,或者调用超时了也可以重试。配置如下: - -``` - -``` - -举个栗子。 - -某个服务的接口,要耗费 5s,你这边不能干等着,你这边配置了 timeout 之后,我等待 2s,还没返回,我直接就撤了,不能干等你。 - -可以结合你们公司具体的场景来说说你是怎么设置这些参数的: - -- `timeout`:一般设置为 `200ms`,我们认为不能超过 `200ms` 还没返回。 -- `retries`:设置 retries,一般是在读请求的时候,比如你要查询个数据,你可以设置个 retries,如果第一次没读到,报错,重试指定的次数,尝试再次读取。 - -### 路由规则 - -路由规则决定一次 dubbo 服务调用的目标服务器,分为条件路由规则和脚本路由规则,并且支持可扩展。 - -向注册中心写入路由规则的操作通常由监控中心或治理中心的页面完成。 - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("condition://0.0.0.0/com.foo.BarService?category=routers&dynamic=false&rule=" + URL.encode("host = 10.20.153.10 => host = 10.20.153.11") + ")); -``` - -- **condition://** - 表示路由规则的类型,支持条件路由规则和脚本路由规则,可扩展,必填。 -- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想对某个 IP 的生效,请填入具体 IP,必填。 -- **com.foo.BarService** - 表示只对指定服务生效,必填。 -- **category=routers** - 表示该数据为动态配置类型,必填。 -- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 -- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 -- **force=false** - 当路由结果为空时,是否强制执行,如果不强制执行,路由结果为空的路由规则将自动失效,可不填,缺省为 flase。 -- **runtime=false** - 是否在每次调用时执行路由规则,否则只在提供者地址列表变更时预先执行并缓存结果,调用时直接从缓存中获取路由结果。如果用了参数路由,必须设为 true,需要注意设置会影响调用的性能,可不填,缺省为 flase。 -- **priority=1** - 路由规则的优先级,用于排序,优先级越大越靠前执行,可不填,缺省为 0。 -- **rule=URL.encode("host = 10.20.153.10 => host = 10.20.153.11")** - 表示路由规则的内容,必填。 - -### 服务降级 - -可以通过服务降级功能临时屏蔽某个出错的非关键服务,并定义降级后的返回策略。 - -向注册中心写入动态配置覆盖规则: - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null")); -``` - -其中: - -**`mock=force:return+null`** 表示消费方对该服务的方法调用都直接返回 null 值,不发起远程调用。用来屏蔽不重要服务不可用时对调用方的影响。 -还可以改为 **`mock=fail:return+null`** 表示消费方对该服务的方法调用在失败后,再返回 null 值,不抛异常。用来容忍不重要服务不稳定时对调用方的影响。 - -比如说服务 A 调用服务 B,结果服务 B 挂掉了,服务 A 重试几次调用服务 B,还是不行,那么直接降级,走一个备用的逻辑,给用户返回响应。 - -举个例子,我们有接口 `HelloService`。`HelloServiceImpl` 有该接口的具体实现。 - -```java -public interface HelloService { - void sayHello(); -} - -public class HelloServiceImpl implements HelloService { - public void sayHello() { - System.out.println("hello world......"); - } -} -``` - -Dubbo 配置: - -```xml - - - - - - - - - - - - - - - - - - - - - - - - -``` - -我们调用接口失败的时候,可以通过 `mock` 统一返回 null。 - -mock 的值也可以修改为 true,然后再跟接口同一个路径下实现一个 Mock 类,命名规则是 “接口名称+`Mock`” 后缀。然后在 Mock 类里实现自己的降级逻辑。 - -```java -public class HelloServiceMock implements HelloService { - public void sayHello() { - // 降级逻辑 - } -} -``` - -### 访问控制 - -#### 直连 - -在开发及测试环境下,经常需要绕过注册中心,只测试指定服务提供者,这时候可能需要点对点直连,点对点直联方式,将以服务接口为单位,忽略注册中心的提供者列表,A 接口配置点对点,不影响 B 接口从注册中心获取列表。 - -
    - -
    - -配置方式: - -(1)通过 XML 配置 - -如果是线上需求需要点对点,可在 中配置 url 指向提供者,将绕过注册中心,多个地址用分号隔开,配置如下: - -```xml - -``` - -(2)通过 -D 参数指定 - -在 JVM 启动参数中加入-D 参数映射服务地址: - -``` -java -Dcom.alibaba.xxx.XxxService=dubbo://localhost:20890 -``` - -(3)通过文件映射 -如果服务比较多,也可以用文件映射,用 -Ddubbo.resolve.file 指定映射文件路径,此配置优先级高于 中的配置: - -``` -java -Ddubbo.resolve.file=xxx.properties -``` - -然后在映射文件 xxx.properties 中加入配置,其中 key 为服务名,value 为服务提供者 URL: - -```properties -com.alibaba.xxx.XxxService=dubbo://localhost:20890 -``` - -#### 只订阅 - -为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。 - -可以让服务提供者开发方,只订阅服务(开发的服务可能依赖其它服务),而不注册正在开发的服务,通过直连测试正在开发的服务。 - -禁用注册配置: - -```xml - -``` - -或者 - -```xml - -``` - -#### 只注册 - -如果有两个镜像环境,两个注册中心,有一个服务只在其中一个注册中心有部署,另一个注册中心还没来得及部署,而两个注册中心的其它应用都需要依赖此服务。这个时候,可以让服务提供者方只注册服务到另一注册中心,而不从另一注册中心订阅服务。 - -禁用订阅配置 - -```xml - - -``` - -或者 - -```xml - - -``` - -#### 静态服务 - -有时候希望人工管理服务提供者的上线和下线,此时需将注册中心标识为非动态管理模式。 - -``` - -``` - -或者 - -``` - -``` - -服务提供者初次注册时为禁用状态,需人工启用。断线时,将不会被自动删除,需人工禁用。 - -### 动态配置 - -向注册中心写入动态配置覆盖规则。该功能通常由监控中心或治理中心的页面完成。 - -```java -RegistryFactory registryFactory = ExtensionLoader.getExtensionLoader(RegistryFactory.class).getAdaptiveExtension(); -Registry registry = registryFactory.getRegistry(URL.valueOf("zookeeper://10.20.153.10:2181")); -registry.register(URL.valueOf("override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&timeout=1000")); -``` - -其中: - -- **override://** - 表示数据采用覆盖方式,支持 override 和 absent,可扩展,必填。 -- **0.0.0.0** - 表示对所有 IP 地址生效,如果只想覆盖某个 IP 的数据,请填入具体 IP,必填。 -- **com.foo.BarService** - 表示只对指定服务生效,必填。 -- **category=configurators** - 表示该数据为动态配置类型,必填。 -- **dynamic=false** - 表示该数据为持久数据,当注册方退出时,数据依然保存在注册中心,必填。 -- **enabled=true** - 覆盖规则是否生效,可不填,缺省生效。 -- **application=foo** - 表示只对指定应用生效,可不填,表示对所有应用生效。 -- **timeout=1000** - 表示将满足以上条件的 timeout 参数的值覆盖为 1000。如果想覆盖其它参数,直接加在 override 的 URL 参数上。 - -示例: - -- 禁用提供者:(通常用于临时踢除某台提供者机器,相似的,禁止消费者访问请使用路由规则) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&disbaled=true -``` - -- 调整权重:(通常用于容量评估,缺省权重为 100) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&weight=200 -``` - -- 调整负载均衡策略:(缺省负载均衡策略为 random) - -``` -override://10.20.153.10/com.foo.BarService?category=configurators&dynamic=false&loadbalance=leastactive -``` - -- 服务降级:(通常用于临时屏蔽某个出错的非关键服务) - -``` -override://0.0.0.0/com.foo.BarService?category=configurators&dynamic=false&application=foo&mock=force:return+null -``` - -## 十、多版本 - -当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。 - -可以按照以下的步骤进行版本迁移: - -1. 在低压力时间段,先升级一半提供者为新版本 -2. 再将所有消费者升级为新版本 -3. 然后将剩下的一半提供者升级为新版本 - -老版本服务提供者配置: - -```xml - -``` - -新版本服务提供者配置: - -```xml - -``` - -老版本服务消费者配置: - -```xml - -``` - -新版本服务消费者配置: - -```xml - -``` - -如果不需要区分版本,可以按照以下的方式配置 [[1\]](http://dubbo.apache.org/zh-cn/docs/user/demos/multi-versions.html#fn1): - -```xml - -``` - -## 十一、Dubbo SPI - -SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是**将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类**。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。 - -Dubbo SPI 的相关逻辑被封装在了 `ExtensionLoader` 类中,通过 `ExtensionLoader`,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 `META-INF/dubbo` 路径下。 - -## 参考资料 - -- **官方** - - [Dubbo Github](https://github.com/apache/dubbo) - - [Dubbo 官方文档](https://dubbo.apache.org/zh-cn/) - - [管理员手册](https://dubbo.gitbooks.io/dubbo-admin-book/content/) -- **文章** - - [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/dubbo-service-management.md) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/01.Dubbo/01.Dubbo\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/01.Dubbo/01.Dubbo\345\277\253\351\200\237\345\205\245\351\227\250.md" deleted file mode 100644 index 2111542d56..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/01.Dubbo/01.Dubbo\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ /dev/null @@ -1,106 +0,0 @@ ---- -title: Dubbo 快速入门 -date: 2022-04-25 19:12:19 -order: 01 -categories: - - 分布式 - - 分布式通信 - - RPC - - Dubbo -tags: - - 分布式 - - 分布式应用 - - 微服务 - - Dubbo -permalink: /pages/5bdbcd/ ---- - -# Dubbo 快速入门 - -## 简介 - -Apache Dubbo 是一款高性能、轻量级的开源微服务框架。它提供了 **RPC 通信** 与 **微服务治理** 两大关键能力。 - -### Dubbo 的核心功能 - -Dubbo 提供了六大核心能力: - -- **面向接口代理的高性能 RPC 调用**:提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节。 -- **智能容错和负载均衡**:内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量。 -- **服务自动注册和发现**:支持多种注册中心服务,服务实例上下线实时感知。 -- **高度可扩展能力**:遵循微内核+插件的设计原则,所有核心能力如 Protocol、Transport、Serialization 被设计为扩展点,平等对待内置实现和第三方实现。 -- **运行期流量调度**:内置条件、脚本等路由策略,通过配置不同的路由规则,轻松实现灰度发布,同机房优先等功能。 -- **可视化的服务治理与运维**:提供丰富服务治理、运维工具:随时查询服务元数据、服务健康状态及调用统计,实时下发路由策略、调整配置参数。 - -### Dubbo 的优势 - -Dubbo 提供了一站式微服务解决方案,包括服务定义、服务发现、服务通信到流量管控等几乎所有的服务治理能力,并且尝试从使用上对用户屏蔽底层细节,以提供更好的易用性。 - -**定义服务**在 Dubbo 中非常简单与直观: - -- 使用与某种语言绑定的方式(如 Java 中可直接定义 Interface) -- 使用 Protobuf IDL 语言的方式。 - -Dubbo 提供了丰富的**通信模型**: - -- 消费端异步请求(Client Side Asynchronous Request-Response) -- 提供端异步执行(Server Side Asynchronous Request-Response) -- 消费端请求流(Request Streaming) -- 提供端响应流(Response Streaming) -- 双向流式通信(Bidirectional Streaming) - -Dubbo 提供基于客户端的**服务发现**机制,可以采用多种方式启用服务发现: - -- 使用第三方的注册中心组件,如 Nacos、Zookeeper、Consul、Etcd 等。 -- 将服务的组织与注册交给底层容器平台,如 Kubernetes,这被理解是一种更云原生的方式 - -Dubbo 提供了多种**流量控制**手段,包括负载均衡、流量路由、请求超时、流量降级、重试等策略。基于这些基础能力可以轻松的实现更多场景化的路由方案,包括金丝雀发布、A/B 测试、权重路由、同区域优先等。 - -Dubbo 的**扩展性**良好:通过 Filter、Router、Protocol 等几乎存在于每一个关键流程上的扩展点定义,我们可以丰富 Dubbo 的功能或实现与其他微服务配套系统的对接,包括 Transaction、Tracing 目前都有通过 SPI 扩展的实现方案,具体可以参见 Dubbo 扩展性的详情,也可以在 [apache/dubbo-spi-extensions](https://github.com/apache/dubbo-spi-extensions) 项目中发现与更多的扩展实现。 - -Dubbo 在支持微服务集群方面有着非常大的规模与非常久的实践经验积累,是最具有企业规模化微服务实践话语权的框架之一。 - -Dubbo3 的设计是面向**云原生**的: - -- 首先是对云原生基础设施与部署架构的支持,包括 Kubernetes、Service Mesh 等。 -- 另一方面,Dubbo 众多核心组件都已面向云原生升级,包括 Triple 协议、统一路由规则、对多语言支持。 - -## Dubbo3 新特性 - -### 全新服务发现模型 - -相比于 2.x 版本中的基于`接口`粒度的服务发现机制,3.x 引入了全新的[基于应用粒度的服务发现机制](https://dubbo.apache.org/zh/docs/concepts/service-discovery), 新模型带来两方面的巨大优势: - -- **进一步提升了 Dubbo3 在大规模集群实践中的性能与稳定性**。新模型可大幅提高系统资源利用率,降低 Dubbo 地址的单机内存消耗(50%),降低注册中心集群的存储与推送压力(90%), Dubbo 可支持集群规模步入百万实例层次。 -- **打通与其他异构微服务体系的地址互发现障碍**。新模型使得 Dubbo3 能实现与异构微服务体系如 Spring Cloud、Kubernetes Service、gRPC 等,在地址发现层面的互通, 为连通 Dubbo 与其他微服务体系提供可行方案。 - -### 下一代 RPC 通信协议 - -定义了全新的 RPC 通信协议 – Triple,一句话概括 Triple:它是基于 HTTP/2 上构建的 RPC 协议,完全兼容 gRPC,并在此基础上扩展出了更丰富的语义。 使用 Triple 协议,用户将获得以下能力 - -- 更容易到适配网关、Mesh 架构,Triple 协议让 Dubbo 更方便的与各种网关、Sidecar 组件配合工作。 -- 多语言友好,推荐配合 Protobuf 使用 Triple 协议,使用 IDL 定义服务,使用 Protobuf 编码业务数据。 -- 流式通信支持。Triple 协议支持 Request Stream、Response Stream、Bi-direction Stream - -### 云原生 - -Dubbo3 构建的业务应用可直接部署在 VM、Container、Kubernetes 等平台,Dubbo3 很好的解决了 Dubbo 服务与调度平台之间的生命周期对齐,Dubbo 服务发现地址 与容器平台绑定的问题。 - -在服务发现层面,Dubbo3 支持与 [Kubernetes Native Service](https://dubbo.apache.org/zh/docs/new-in-dubbo3/) 的融合,目前限于 Headless Service。 - -Dubbo3 规划了两种形态的 Service Mesh 方案,在不同的业务场景、不同的迁移阶段、不同的基础设施保障情况下,Dubbo 都会有 Mesh 方案可供选择, 而这进一步的都可以通过统一的控制面进行治理。 - -- 经典的基于 Sidecar 的 Service Mesh -- 无 Sidecar 的 Proxyless Mesh - -用户在 Dubbo2 中熟知的路由规则,在 3.x 中将被一套[`统一的流量治理规则`](https://dubbo.apache.org/zh/docs/concepts/traffic-management)取代,这套统一流量规则将覆盖未来 Dubbo3 的 Service Mesh、SDK 等多种部署形态, 实现对整套微服务体系的治理。 - -## 快速开始 - -定义服务 - -## 参考资料 - -- **官方** - - [Dubbo Github](https://github.com/apache/dubbo) - - [Dubbo 官方文档](https://dubbo.apache.org/zh-cn/) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/22.Kong.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/22.Kong.md" deleted file mode 100644 index 2d09c1258c..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/22.Kong.md" +++ /dev/null @@ -1,178 +0,0 @@ ---- -title: kong -date: 2018-10-11 13:08:18 -order: 22 -categories: - - 分布式 - - 分布式通信 - - RPC -tags: - - 分布式 - - 分布式应用 - - RPC -permalink: /pages/8d1bee/ ---- - -# Kong - -> [Kong](https://github.com/Kong/kong) 是一个云原生、快速、可扩展和分布式的微服务抽象层(也称为 API 网关,API 中间件)。 -> -> 关键词:`nginx`,`api网关`,`微服务` - -## 简介 - -### 为什么选择 Kong - -

    - -## Quickstart - -### 安装配置 - -> 本文仅以 Centos7 为例。 - -Kong 支持在多种环境下安装。 - -官方安装说明:https://konghq.com/install/ - -

    - -以下为 Centos7 安装步骤: - -(1)下载 rpm 安装包到本地 - -(2)安装 Kong - -``` -$ sudo yum install epel-release -$ sudo yum install kong-community-edition-0.14.1.*.noarch.rpm --nogpgcheck -``` - -(3)准备数据库 - -Kong 需要存储数据,支持两种数据库:[PostgreSQL 9.5+](http://www.postgresql.org/) 和 [Cassandra 3.x.x](http://cassandra.apache.org/) - -本人选择了 PostgreSQL,安装方法可以参考 —— [PostgreSQL 安装](https://github.com/dunwu/database/blob/master/docs/postgresql.md#安装) - -安装 PostgreSQL 后,配置一个数据库和数据库用户: - -```sql -CREATE USER kong; -CREATE DATABASE kong OWNER kong; -``` - -(4)执行 Kong 迁移 - -执行以下命令: - -``` -$ kong migrations up [-c /path/to/kong.conf] -``` - -注意:永远不应同时运行迁移;一个 Kong 节点应该只执行一次迁移。 - -(5)启动 Kong - -``` -$ kong start [-c /path/to/kong.conf] -``` - -(6)测试启动成功 - -``` -$ curl -i http://localhost:8001/ -``` - -至此,安装配置完成。 - -### 使用 Kong - -- 启动(必须确保执行过 `kong migrations up`) - `kong start [-c /path/to/kong.conf]` - - `-c /path/to/kong.conf` 参数用来指定用户的配置 -- 停止 - `kong stop` -- 重启 - `kong reload` - -### 配置服务 - -(1)添加第一个服务 - -```sh -$ curl -i -X POST \ - --url http://localhost:8001/services/ \ - --data 'name=example-service' \ - --data 'url=http://mockbin.org' -``` - -应答类似下面形式: - -```http -HTTP/1.1 201 Created -Content-Type: application/json -Connection: keep-alive - -{ - "host":"mockbin.org", - "created_at":1519130509, - "connect_timeout":60000, - "id":"92956672-f5ea-4e9a-b096-667bf55bc40c", - "protocol":"http", - "name":"example-service", - "read_timeout":60000, - "port":80, - "path":null, - "updated_at":1519130509, - "retries":5, - "write_timeout":60000 -} -``` - -(2)为服务添加路由 - -```sh -$ curl -i -X POST \ - --url http://localhost:8001/services/example-service/routes \ - --data 'hosts[]=example.com' -``` - -应答类似下面形式: - -```http -HTTP/1.1 201 Created -Content-Type: application/json -Connection: keep-alive - -{ - "created_at":1519131139, - "strip_path":true, - "hosts":[ - "example.com" - ], - "preserve_host":false, - "regex_priority":0, - "updated_at":1519131139, - "paths":null, - "service":{ - "id":"79d7ee6e-9fc7-4b95-aa3b-61d2e17e7516" - }, - "methods":null, - "protocols":[ - "http", - "https" - ], - "id":"f9ce2ed7-c06e-4e16-bd5d-3a82daef3f9d" -} -``` - -此时,Kong 已经关注这个服务,并准备代理请求。 - -(3)通过 Kong 转发请求 - -```sh -$ curl -i -X GET \ - --url http://localhost:8000/ \ - --header 'Host: example.com' -``` - -## 参考资料 - -https://www.itcodemonkey.com/article/5980.html \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/Dubbo\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/Dubbo\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..d825b3ed74 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/Dubbo\351\235\242\350\257\225.md" @@ -0,0 +1,953 @@ +--- +title: Dubbo 面试 +date: 2024-12-12 08:18:57 +categories: + - 分布式 + - 分布式通信 + - RPC +tags: + - 分布式 + - 通信 + - RPC + - 微服务 + - Dubbo + - 面试 +permalink: /pages/02820fbd/ +--- + +# Dubbo 面试 + +## 简介 + +### 【基础】Dubbo 是什么?为什么使用 Dubbo? + +:::details 要点 + +[Dubbo](https://dubbo.apache.org/zh-cn/) 是一款高性能、轻量级的开源 Java RPC 框架。 + +Dubbo 提供了三大核心能力: + +- **面向接口的远程过程调用(RPC)**:提供高性能的基于代理的远程调用能力,服务以接口为粒度,为开发者屏蔽远程调用底层细节。 +- **智能容错和负载均衡**:内置多种负载均衡策略,智能感知下游节点健康状况,显著减少调用延迟,提高系统吞吐量。 +- **服务自动注册和发现**:支持多种注册中心服务,服务实例上下线实时感知。 + +::: + +### 【基础】Dubbo3 有什么新特性? + +:::details 要点 + +Dubbo3 的核心新特性: + +- [新通信协议 - Triple](https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/triple/) - Triple 协议是 Dubbo3 设计的基于 HTTP 的 RPC 通信协议规范。它**完全兼容 gRPC 协议**,支持 Request-Response、Streaming 流式等通信模型,**可同时运行在 HTTP/1 和 HTTP/2 之上**。 +- [应用级服务发现](https://cn.dubbo.apache.org/zh-cn/blog/2023/01/30/dubbo3-%E5%BA%94%E7%94%A8%E7%BA%A7%E6%9C%8D%E5%8A%A1%E5%8F%91%E7%8E%B0%E8%AE%BE%E8%AE%A1/) + - 接口级服务发现,以接口为粒度将信息注册到注册中心。举例来说,如果有 10 个 RPC Provider,部署在 100 台机器实例上,就要注册 `10 * 100` 条数据。 + - 应用级服务发现,,以应用为粒度将信息注册到注册中心。将信息进行了**拆分**:接口元数据信息、接口和应用的映射关系维护在元数据中心;应用信息维护在注册中心。这样的好处是,存储的数据量大大减少,则传输数据的 I/O 开销也随之显著减少。 +- [Dubbo Mesh](https://cn.dubbo.apache.org/zh/docs3-v2/java-sdk/concepts-and-architecture/mesh/) - 让 Dubbo 应用能够无缝接入 Istio 等业界主流服务网格产品。 + +> 扩展:[技术创想66 | Dubbo3.0应用级服务注册原理](https://zhuanlan.zhihu.com/p/581776302) + +::: + +## 架构 + +### 【基础】Dubbo 有哪些核心组件? + +:::details 要点 + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/dubbo/dubbo基本架构.png) + +节点角色: + +| 节点 | 角色说明 | +| --------- | -------------------------------------- | +| Provider | 暴露服务的服务提供方 | +| Consumer | 调用远程服务的服务消费方 | +| Registry | 服务注册与发现的注册中心 | +| Monitor | 统计服务的调用次数和调用时间的监控中心 | +| Container | 服务运行容器 | + +调用关系: + +1. 服务容器负责启动,加载,运行服务提供者。 +2. 服务提供者在启动时,向注册中心注册自己提供的服务。 +3. 服务消费者在启动时,向注册中心订阅自己所需的服务。 +4. 注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。 +5. 服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。 +6. 服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。 + +**重要知识点总结:** + +- 注册中心负责服务地址的注册与查找,相当于元数据管理服务,服务提供者和消费者只在启动时与注册中心交互,注册中心不转发请求,压力较小。 +- 监控中心负责统计各服务调用次数,调用时间等,统计先在内存汇总后每分钟一次发送到监控中心服务器,并以报表展示。 +- 注册中心,服务提供者,服务消费者三者之间均为长连接,监控中心除外. +- 注册中心通过长连接感知服务提供者的存在,服务提供者宕机,注册中心将立即推送事件通知消费者。 +- 注册中心和监控中心全部宕机,不影响已运行的提供者和消费者,消费者在本地缓存了提供者列表。 +- 注册中心和监控中心都是可选的,服务消费者可以直连服务提供者。 +- 服务提供者无状态,任意一台宕掉后,不影响使用。 +- 服务提供者全部宕掉后,服务消费者应用将无法使用,并无限次重连等待服务提供者恢复。 + +::: + +### 【高级】Dubbo 框架整体如何设计的? + +:::details 要点 + +Dubbo 的整体设计原则如下: + +- 采用 Microkernel + Plugin 模式,Microkernel 只负责组装 Plugin,Dubbo 自身的功能也是通过扩展点实现的,也就是 Dubbo 的所有功能点都可被用户自定义扩展所替换。 +- 采用 URL 作为配置信息的统一格式,所有扩展点都通过传递 URL 携带配置信息。 + +#### 整体设计 + +![总设计图](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/dubbo/dubbo整体设计.jpg) + +- 图中左边淡蓝背景的为服务消费方使用的接口,右边淡绿色背景的为服务提供方使用的接口,位于中轴线上的为双方都用到的接口。 +- 图中从下至上分为十层,各层均为单向依赖,右边的黑色箭头代表层之间的依赖关系,每一层都可以剥离上层被复用,其中,Service 和 Config 层为 API,其它各层均为 SPI。 +- 图中绿色小块的为扩展接口,蓝色小块为实现类,图中只显示用于关联各层的实现类。 +- 图中蓝色虚线为初始化过程,即启动时组装链,红色实线为方法调用过程,即运行时调时链,紫色三角箭头为继承,可以把子类看作父类的同一个节点,线上的文字为调用的方法。 + +#### 各层说明 + +- **config 配置层**:对外配置接口,以 `ServiceConfig`、`ReferenceConfig` 为中心,可以直接初始化配置类,也可以通过 Spring 解析配置生成配置类 +- **proxy 服务代理层**:服务接口透明代理,生成服务的客户端 Stub 和服务器端 Skeleton,以 `ServiceProxy` 为中心,扩展接口为 `ProxyFactory`。 +- **registry 注册中心层**:封装服务地址的注册与发现,以服务 URL 为中心,扩展接口为 `RegistryFactory`、`Registry`、`RegistryService`。 +- **cluster 路由层**:封装多个提供者的路由及负载均衡,并桥接注册中心,以 `Invoker` 为中心,扩展接口为 `Cluster`、`Directory`、`Router`、`LoadBalance`。 +- **monitor 监控层**:RPC 调用次数和调用时间监控,以 `Statistics` 为中心,扩展接口为 `MonitorFactory`、`Monitor`、`MonitorService`。 +- **protocol 远程调用层**:封装 RPC 调用,以 `Invocation`、`Result` 为中心,扩展接口为 `Protocol`、`Invoker`、`Exporter`。 +- **exchange 信息交换层**:封装请求响应模式,同步转异步,以 `Request`、`Response` 为中心,扩展接口为 `Exchanger`、`ExchangeChannel`、`ExchangeClient`、`ExchangeServer`。 +- **transport 网络传输层**:抽象 mina 和 netty 为统一接口,以 `Message` 为中心,扩展接口为 `Channel`、`Transporter`、`Client`、`Server`、`Codec`。 +- **serialize 数据序列化层**:可复用的一些工具,扩展接口为 `Serialization`、`ObjectInput`、`ObjectOutput`、`ThreadPool`。 + +#### 关系说明 + +- 在 RPC 中,**`Protocol` 是核心层,也就是只要有 `Protocol` + `Invoker` + `Exporter` 就可以完成非透明的 RPC 调用**,然后在 `Invoker` 的主过程上设置拦截点(Filter)。 +- 图中的 `Consumer` 和 `Provider` 是抽象概念,只是想让看图者更直观的了解哪些类分属于客户端与服务器端,不用 Client 和 Server 的原因是 Dubbo 在很多场景下都使用 `Provider`、`Consumer`、Registry、`Monitor` 划分逻辑拓普节点,保持统一概念。 +- 而 Cluster 是外围概念,所以 **Cluster 的目的是将多个 Invoker 伪装成一个 Invoker**,这样其它人只要关注 Protocol 层 Invoker 即可,加上 Cluster 或者去掉 Cluster 对其它层都不会造成影响,因为只有一个提供者时,是不需要 Cluster 的。 +- **Proxy 层封装了所有接口的透明化代理**。在其它层都以 `Invoker` 为中心,只有到了暴露给用户使用时,才用 `Proxy` 将 `Invoker` 转成接口,或将接口实现转成 `Invoker`,也就是去掉 Proxy 层 RPC 是可以 Run 的,只是不那么透明,不那么看起来像调本地服务一样调远程服务。 +- 而 Remoting 实现是 Dubbo 协议的实现,如果你选择 RMI 协议,整个 Remoting 都不会用上,Remoting 内部再划为 Transport 传输层和 Exchange 信息交换层,**Transport 层只负责单向消息传输**,是对 Mina, Netty, Grizzly 的抽象,它也可以扩展 UDP 传输,而 **Exchange 层是在传输层之上封装了 Request-Response 语义**。 +- Registry 和 Monitor 实际上不算一层,而是一个独立的节点,只是为了全局概览,用层的方式画在一起。 + +#### 依赖关系 + +![依赖关系](https://cn.dubbo.apache.org/imgs/dev/dubbo-relation.jpg) + +- 图中小方块 Protocol, Cluster, Proxy, Service, Container, Registry, Monitor 代表层或模块,蓝色的表示与业务有交互,绿色的表示只对 Dubbo 内部交互。 +- 图中背景方块 Consumer, Provider, Registry, Monitor 代表部署逻辑拓扑节点。 +- 图中蓝色虚线为初始化时调用,红色虚线为运行时异步调用,红色实线为运行时同步调用。 +- 图中只包含 RPC 的层,不包含 Remoting 的层,Remoting 整体都隐含在 Protocol 中。 + +#### 调用链 + +展开总设计图的红色调用链,如下: + +![总设计图的红色调用链](https://cn.dubbo.apache.org/imgs/dev/dubbo-extension.jpg) + +> 扩展阅读:[Dubbo 框架设计](https://cn.dubbo.apache.org/zh-cn/docsv2.7/dev/design/) + +::: + +### 【高级】Dubbo 架构是如何实现高度可扩展的? + +:::details 要点 + +#### 微内核+插件架构 + +Dubbo 的架构设计采用**微内核+插件**架构,高度支持可扩展。 + +基于扩展点,用户完全可以基于自身需求,替换 Dubbo 原生实现,来满足自身业务需求。 + +![Admin 效果图](https://cn.dubbo.apache.org/imgs/v3/advantages/extensibility.png) + +- **协议与编码扩展**。通信协议、序列化编码协议等 +- **流量管控扩展**。集群容错策略、路由规则、负载均衡、限流降级、熔断策略等 +- **服务治理扩展**。注册中心、配置中心、元数据中心、分布式事务、全链路追踪、监控系统等 +- **诊断与调优扩展**。流量统计、线程池策略、日志、QoS 运维命令、健康检查、配置加载等 + +#### 基于扩展的生态 + +Dubbo 调用链路中几乎所有核心节点都被定义为扩展点。 + +![extensibility-echosystem.png](https://cn.dubbo.apache.org/imgs/v3/feature/extensibility/arc.png) + +以上是按架构层次划分的 Dubbo 内的一些核心扩展点定义及实现,可以从三个层次来展开: + +1. 协议通信层 +2. 流量管控层 +3. 服务治理层 + +##### 协议通信层 + +- **Protocol** - Protocol 定义了 RPC 协议,利用这个扩展点可以实现灵活切换通信协议。Dubbo 官方提供了 Triple、gRPC、Dubbo2、REST 等 RPC 协议。 +- **Serialization** - Serialization 定义了序列化协议,利用这个扩展点可以实现灵活切换序列化协议。Dubbo 官方提供了 Fastjson、Protobuf、Hessian2、Kryo、FST 等序列化协议。 + +![协议与编码原理图](https://cn.dubbo.apache.org/imgs/v3/feature/extensibility/protocol.png) + +##### 流量管控层 + +Dubbo 在服务调用链路上预置了大量扩展点,通过这些扩展点用户可以控制运行态的流量走向、改变运行时调用行为等,包括 Dubbo 内置的一些负载均衡策略、流量路由策略、超时等很多流量管控能力都是通过这类扩展点实现的。 + +![协议与编码原理图](https://cn.dubbo.apache.org/imgs/v3/feature/extensibility/traffic.png) + +- **Filter** - Filter 流量拦截器是 Dubbo 服务调用之上的 AOP 设计模式,Filter 用来对每次服务调用做一些预处理、后处理动作,使用 Filter 可以完成访问日志、加解密、流量统计、参数验证等任务,Dubbo 中的很多生态适配如限流降级 Sentinel、全链路追踪 Tracing 等都是通过 Fitler 扩展实现的。Filter 以链式串联工作,彼此独立。 + - 从消费端视角,它在请求发起前基于请求参数等做一些预处理工作,在接收到响应后,对响应结果做一些后置处理; + - 从提供者视角则,在接收到访问请求后,在返回响应结果前做一些预处理, +- **Router** - Router 将符合一定条件的流量转发到特定分组的地址子集,是 Dubbo 中一些关键能力如按比例流量转发、流量隔离等的基础。每次服务调用请求都会流经一组路由器 (路由链),每个路由器根据预先设定好的规则、全量地址列表以及当前请求上下文计算出一个地址子集,再传给下一个路由器,重复这一过程直到最后得出一个有效的地址子集。 +- **Load Balance** - 在 Dubbo 中,Load Balance 负载均衡工作在 Router 之后,对于每次服务调用,负载均衡负责在 Router 链输出的地址子集中选择一台机器实例进行访问,保证一段时间内的调用都均匀的分布在地址子集的所有机器上。Dubbo 官方提供了加权随机、加权轮询、一致性哈希、最小活跃度优先、最短响应时间优先等负载均衡策略,还提供了根据集群负载自适应调度的负载均衡算法。 + +##### 服务治理层 + +Dubbo3 由注册中心 (服务发现)、配置中心和元数据中心构成了整个服务治理的核心。 + +![服务治理架构图](https://cn.dubbo.apache.org/imgs/v3/concepts/threecenters.png) + +Dubbo 很多服务治理的核心能力都是通过上图描述的几个关键组件实现的。用户通过控制面或者 Admin 下发的各种规则与配置、各类微服务集群状态的展示等都是直接与注册中心、配置中心和元数据中心交互。在具体实现或者部署上,注册中心、配置中心和元数据中心可以是同一组件,比如 Zookeeper 可同时作为注册、配置和元数据中心,Nacos 也是如此。因此,三个中心只是从架构职责上的划分,你甚至可以用同一个 Zookeeper 集群来承担所有三个职责,只需要在应用里将他们设置为同一个集群地址就可以了。 + +- **Registry** - **注册中心是 Dubbo 实现服务发现能力的基础**。Dubbo 官方支持 Zookeeper、Nacos、Etcd、Consul、Eureka 等注册中心。通过对 Consul、Eureka 的支持,Dubbo 也实现了与 Spring Cloud 体系在地址和通信层面的互通,让用户同时部署 Dubbo 与 Spring Cloud,或者从 Spring Cloud 迁移到 Dubbo 变得更容易。 +- **Config Center** - **配置中心是用户实现动态控制 Dubbo 行为的关键组件**。Dubbo 所有的路由规则,都是先下发到配置中心保存起来,进而 Dubbo 实例通过监听配置中心的变化,收到路由规则并达到控制流量的行为。Dubbo 官方支持 Zookeeper、Nacos、Etcd、Redis、Apollo 等配置中心实现。 +- **Metadata Center** - 与配置中心相反,从用户视角来看元数据中心是只读的,元数据中心唯一的写入方是 Dubbo 进程实例,Dubbo 实例会在启动之后将一些内部状态(如服务列表、服务配置、服务定义格式等)上报到元数据中心,供一些治理能力作为数据来源,如服务测试、文档管理、服务状态展示等。Dubbo 官方支持 Zookeeper、Nacos、Etcd、Redis 等元数据中心实现。 + +> 扩展阅读:[Dubbo 官方文档之扩展适配](https://cn.dubbo.apache.org/zh-cn/overview/what/core-features/extensibility/) + +::: + +### 【高级】Dubbo 的 SPI 机制是如何设计的? + +:::details 要点 + +#### Java SPI + +**SPI** 全称 Service Provider Interface,**旨在由第三方实现或扩展的 API,它是一种用于动态加载服务的机制**。SPI 的本质是**将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类**。这样可以在运行时,动态为接口替换实现类。 + +Java 中 SPI 机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 **解耦**。 + +Java SPI 有四个要素: + +- **SPI 接口**:为服务提供者实现类约定的的接口或抽象类。 +- **SPI 实现类**:实际提供服务的实现类。 +- **SPI 配置**:Java SPI 机制约定的配置文件,提供查找服务实现类的逻辑。配置文件必须置于 `META-INF/services` 目录中,并且,文件名应与服务提供者接口的完全限定名保持一致。文件中的每一行都有一个实现服务类的详细信息,同样是服务提供者类的完全限定名称。 +- **`ServiceLoader`**:Java SPI 的核心类,用于加载 SPI 实现类。 `ServiceLoader` 中有各种实用方法来获取特定实现、迭代它们或重新加载服务。 + +Java SPI 存在一些不足: + +- **不能按需加载**,需要遍历所有的实现并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。 +- 获取某个实现类的方式不够灵活,**只能通过 `Iterator` 形式获取**,不能根据某个参数来获取对应的实现类。 +- 并发多线程使用 `ServiceLoader` 类的实例是**不安全**的。 + +#### Dubbo SPI + +正是有 Java SPI 存在以上不足点,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。 + +Dubbo SPI 所需的配置文件需放置在 `META-INF/dubbo` 路径下。配置内容形式如下: + +``` +optimusPrime = org.apache.spi.OptimusPrime +bumblebee = org.apache.spi.Bumblebee +``` + +与 Java SPI 实现类配置不同,Dubbo SPI 是**通过键值对的方式进行配置**,这样可以**按需加载**指定的实现类。Dubbo SPI 除了支持按需加载接口实现类,还增加了 IOC 和 AOP 等特性。 + +Dubbo SPI 的相关逻辑被封装在了 `ExtensionLoader` 类中,通过 `ExtensionLoader`,可以加载指定的实现类。`ExtensionLoader` 的 `getExtension` 方法是其入口方法。 + +> 扩展阅读: +> +> - [Dubbo SPI 概述](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/reference-manual/spi/overview/) +> - [源码级深度理解 Java SPI](https://dunwu.github.io/waterdrop/pages/2131c240/) + +::: + +### 【高级】Dubbo 中的时钟轮机制是如何设计的? + +:::details 要点 + +#### JDK 中定时任务的实现 + +在很多开源框架中,都需要定时任务的管理功能,例如 ZooKeeper、Netty、Quartz、Kafka 以及 Linux 操作系统。 + +定时器的本质是设计一种数据结构,能够存储和调度任务集合,而且 deadline 越近的任务拥有更高的优先级。那么定时器如何知道一个任务是否到期了呢?定时器需要通过轮询的方式来实现,每隔一个时间片去检查任务是否到期。 + +所以定时器的内部结构一般需要一个任务队列和一个异步轮询线程,并且能够提供三种基本操作: + +- Schedule 新增任务至任务集合; +- Cancel 取消某个任务; +- Run 执行到期的任务。 + +JDK 原生提供了三种常用的定时器实现方式,分别为 `Timer`、`DelayedQueue` 和 `ScheduledThreadPoolExecutor`。 + +JDK 内置的三种实现定时器的方式,实现思路都非常相似,都离不开**任务**、**任务管理**、**任务调度**三个角色。三种定时器新增和取消任务的时间复杂度都是 `O(logn)`,面对海量任务插入和删除的场景,这三种定时器都会遇到比较严重的性能瓶颈。 + +**对于性能要求较高的场景,一般都会采用时间轮算法来实现定时器**。时间轮(Timing Wheel)是 George Varghese 和 Tony Lauck 在 1996 年的论文 [Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility](https://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt) 实现的,它在 Linux 内核中使用广泛,是 Linux 内核定时器的实现方法和基础之一。 + +#### 时间轮的基本原理 + +**时间轮是一种高效的、批量管理定时任务的调度模型**。时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot 槽位。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。 + +![图片 22.png](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/assets/CgpVE1_okKiAGl0gAAMLshtTq-M933.png) + +任务是如何添加到时间轮当中的呢?可以根据任务的到期时间进行取模,然后将任务分布到不同的 slot 中。如上图所示,时间轮被划分为 8 个 slot,每个 slot 代表 1s,当前时针指向 2。假如现在需要调度一个 3s 后执行的任务,应该加入 `2+3=5` 的 slot 中;如果需要调度一个 12s 以后的任务,需要等待时针完整走完一圈 round 零 4 个 slot,需要放入第 `(2+12)%8=6` 个 slot。 + +那么当时针走到第 6 个 slot 时,怎么区分每个任务是否需要立即执行,还是需要等待下一圈,甚至更久时间之后执行呢?所以我们需要把 round 信息保存在任务中。例如图中第 6 个 slot 的链表中包含 3 个任务,第一个任务 round=0,需要立即执行;第二个任务 round=1,需要等待 `1*8=8s` 后执行;第三个任务 round=2,需要等待 `2*8=8s` 后执行。所以当时针转动到对应 slot 时,只执行 round=0 的任务,slot 中其余任务的 round 应当减 1,等待下一个 round 之后执行。 + +上面介绍了时间轮算法的基本理论,可以看出时间轮有点类似 HashMap,如果多个任务如果对应同一个 slot,处理冲突的方法采用的是拉链法。在任务数量比较多的场景下,适当增加时间轮的 slot 数量,可以减少时针转动时遍历的任务个数。 + +时间轮定时器最大的优势就是,任务的新增和取消都是 `O(1)` 时间复杂度,而且只需要一个线程就可以驱动时间轮进行工作。 + +#### Dubbo 中的时间轮 + +`org.apache.dubbo.common.timer.HashedWheelTimer` 是 Dubbo 中时间轮的算法实现。它主要应用于以下方面: + +- **失败重试,** 例如,Provider 向注册中心进行注册失败时的重试操作,或是 Consumer 向注册中心订阅时的失败重试等。 +- **周期性定时任务,** 例如,定期发送心跳请求,请求超时的处理,或是网络连接断开后的重连机制。 + +::: + +### 【高级】Dubbo 中的线程模型是如何设计的? + +:::details 要点 + +#### Consumer 线程模型 + +对 2.7.5 版本之前的 Dubbo 应用,尤其是一些消费端应用,当面临需要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),经常会出现消费端线程数分配过多的问题,具体问题讨论可参见 [Need a limited Threadpool in consumer side #2013](https://github.com/apache/dubbo/issues/2013) + +改进后的消费端线程池模型,通过复用业务端被阻塞的线程,很好的解决了这个问题。 + +**老的线程池模型** + +![消费端线程池.png](https://cn.dubbo.apache.org/imgs/user/consumer-threadpool0.png) + +我们重点关注 Consumer 部分: + +1. 业务线程发出请求,拿到一个 `Future` 实例。 +2. 业务线程紧接着调用 `future.get` 阻塞等待业务结果返回。 +3. 当业务数据返回后,交由独立的 `Consumer` 端线程池进行反序列化等处理,并调用 `future.set` 将反序列化后的业务结果置回。 +4. 业务线程拿到结果直接返回 + +**当前线程池模型** + +![消费端线程池新.png](https://cn.dubbo.apache.org/imgs/user/consumer-threadpool1.png) + +1. 业务线程发出请求,拿到一个 `Future` 实例。 +2. 在调用 `future.get()` 之前,先调用 `ThreadlessExecutor.wait()`,`wait` 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。 +3. 当业务数据返回后,生成一个 `Runnable Task` 并放入 `ThreadlessExecutor` 队列 +4. 业务线程将 `Task` 取出并在本线程中执行:反序列化业务数据并 `set` 到 `Future`。 +5. 业务线程拿到结果直接返回 + +这样,相比于老的线程池模型,由业务线程自己负责监测并解析返回结果,免去了额外的消费端线程池开销。 + +#### Provider 线程模型 + +Dubbo 协议的和 Triple 协议目前的线程模型还并没有对齐。 + +Dubbo 对 channel 上的操作抽象成了五种行为: + +- **建立连接(connected)** - 主要是的职责是在 channel 记录 read、write 的时间,以及处理建立连接后的回调逻辑,比如 dubbo 支持在断开后自定义回调的 hook(onconnect),即在该操作中执行。 +- **断开连接(disconnected)** - 主要是的职责是在 channel 移除 read、write 的时间,以及处理端开连接后的回调逻辑,比如 dubbo 支持在断开后自定义回调的 hook(ondisconnect),即在该操作中执行。 +- **发送消息(sent)** - 包括发送请求和发送响应。记录 write 的时间。 +- **接收消息(received)** - 包括接收请求和接收响应。记录 read 的时间。 +- **异常捕获(caught)** - 用于处理在 channel 上发生的各类异常。 + +Dubbo 框架的线程模型与以上这五种行为息息相关,Dubbo 协议 Provider 线程模型可以分为五类,也就是 AllDispatcher、DirectDispatcher、MessageOnlyDispatcher、ExecutionDispatcher、ConnectionOrderedDispatcher。 + +##### All Dispatcher + +所有消息都派发到 Dubbo 线程池。 + +![dubbo-provider-alldispatcher](https://cn.dubbo.apache.org/imgs/v3/feature/performance/threading-model/dubbo-provider-alldispatcher.png) + +在 IO 线程中执行的操作有: + +1. `sent` 操作在 IO 线程上执行。 +2. 序列化响应在 IO 线程上执行。 + +在 Dubbo 线程中执行的操作有: + +1. `received`、`connected`、`disconnected`、`caught` 都是在 Dubbo 线程上执行的。 +2. 反序列化请求的行为在 Dubbo 中做的。 + +##### Direct Dispatcher + +所有消息都不派发到 Dubbo 线程池,全部在 IO 线程上直接执行。 + +![dubbo-provider-directDispatcher](https://cn.dubbo.apache.org/imgs/v3/feature/performance/threading-model/dubbo-provider-directDispatcher.png) + +在 IO 线程中执行的操作有: + +1. `received`、`connected`、`disconnected`、`caught`、`sent` 操作在 IO 线程上执行。 +2. 反序列化请求和序列化响应在 IO 线程上执行。 + +并没有在 Dubbo 线程操作的行为。 + +##### Execution Dispatcher + +只有请求消息派发到 Dubbo 线程池,不含响应,响应和其它连接断开事件,心跳等消息,直接在 IO 线程上执行。 + +![dubbo-provider-ExecutionDispatcher](https://cn.dubbo.apache.org/imgs/v3/feature/performance/threading-model/dubbo-provider-executionDispatcher.png) + +在 IO 线程中执行的操作有: + +1. `sent`、`connected`、`disconnected`、`caught` 操作在 IO 线程上执行。 +2. 序列化响应在 IO 线程上执行。 + +在 Dubbo 线程中执行的操作有: + +1. `received` 都是在 Dubbo 线程上执行的。 +2. 反序列化请求的行为在 Dubbo 中做的。 + +##### Message Only Dispatcher + +在 Provider 端,Message Only Dispatcher 和 Execution Dispatcher 的线程模型是一致的,所以下图和 Execution Dispatcher 的图一致,区别在 Consumer 端。见下方 Consumer 端的线程模型。 + +![dubbo-provider-ExecutionDispatcher](https://cn.dubbo.apache.org/imgs/v3/feature/performance/threading-model/dubbo-provider-executionDispatcher.png) + +在 IO 线程中执行的操作有: + +1. `sent`、`connected`、`disconnected`、`caught` 操作在 IO 线程上执行。 +2. 序列化响应在 IO 线程上执行。 + +在 Dubbo 线程中执行的操作有: + +1. `received` 都是在 Dubbo 线程上执行的。 +2. 反序列化请求的行为在 Dubbo 中做的。 + +##### Connection Ordered Dispatcher + +![dubbbo-provider-connectionOrderedDispatcher](https://cn.dubbo.apache.org/imgs/v3/feature/performance/threading-model/dubbbo-provider-connectionOrderedDispatcher.png) + +在 IO 线程中执行的操作有: + +1. `sent` 操作在 IO 线程上执行。 +2. 序列化响应在 IO 线程上执行。 + +在 Dubbo 线程中执行的操作有: + +1. `received`、`connected`、`disconnected`、`caught` 都是在 Dubbo 线程上执行的。但是 `connected` 和 `disconnected` 两个行为是与其他两个行为通过线程池隔离开的。并且在 Dubbo connected thread pool 中提供了链接限制、告警灯能力。 +2. 反序列化请求的行为在 Dubbo 中做的。 + +::: + +### 【中级】Dubbo 中用到哪些设计模式? + +:::details 要点 + +**单例模式** + +Dubbo 中大量使用单例模式来确保一些特定类在整个应用中只有一个实例。举例来说,`ExtensionLoader` 是 Dubbo SPI 加载器,负责管理 Dubbo 中的扩展点。`ExtensionLoader` 使用了单例模式来确保 `ExtensionLoader` 在整个应用中只有一个实例。 + +```java +public class ExtensionLoader { + private static final ConcurrentMap, ExtensionLoader> EXTENSION_LOADERS = new ConcurrentHashMap<>(); + + public static ExtensionLoader getExtensionLoader(Class type) { + ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + if (loader == null) { + EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader(type)); + loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + } + return loader; + } +} +``` + +**责任链模式** + +Dubbo 的调用链是基于责任链模式组织起来的。责任链中的每个节点实现 `Filter` 接口,然后由 `ProtocolFilterWrapper` 将所有 `Filter` 串连起来。Dubbo 的许多功能都是通过 `Filter` 扩展实现的,比如监控、日志、缓存、安全等。 + +**装饰器模式** + +Dubbo 中大量用到了修饰器模式。比如 `ProtocolFilterWrapper` 类是对 `Protocol` 类的修饰。在 `export` 和 `refer` 方法中,配合责任链模式,把 `Filter` 组装成责任链,实现对 `Protocol` 功能的修饰。其他还有 `ProtocolListenerWrapper`、 `ListenerInvokerWrapper`、`InvokerWrapper` 等。 + +**策略模式** + +Dubbo 中的负载均衡器采用了策略模式,以便灵活的替换算法。在 Dubbo 中,`LoadBalance` 接口定义了负载均衡的策略接口,它有以下具体实现:`AdaptiveLoadBalance`、`ConsistentHashLoadBalance`、`LeastActiveLoadBalance`、`RandomLoadBalance`、`RoundRobinLoadBalance`、`ServerCpuLoadBalance2`、`ShortestResponseLoadBalance`。 + +```java +public interface LoadBalance { + Invoker select(List> invokers, URL url, Invocation invocation) throws RpcException; +} +``` + +**抽象工厂模式** + +Dubbo 中的 `ProxyFactory` 采用了**抽象工厂模式**。`AbstractProxyFactory` 实现了 `ProxyFactory` 接口,并且有 `JdkProxyFactory` 和 `JavassistProxyFactory` 两个子类,可以分别生产不同序列化方式的 `Proxy` 和 `Invoke`。 + +**代理模式** + +Dubbo 使用代理模式隐藏远程调用的细节。`ProxyFactory` 接口及其实现类负责为服务创建代理对象,使得调用者无需关心实际的服务调用过程。 + +**适配器模式** + +Dubbo 中 `RegistryProtocol` 类负责将不同的注册中心协议适配到统一的接口 `Protocol` 中,以便在不同的注册中心下工作。`RegistryProtocol` 通过适配不同的注册中心实现,使得 Dubbo 能够在多种注册中心协议下工作,而不必修改客户端代码。 + +> 扩展:[长文详解:DUBBO源码使用了哪些设计模式](https://juejin.cn/post/7126675470107541534#heading-24) + +::: + +## 服务注册和发现 + +### 【基础】服务注册和发现的流程是怎样的? + +:::details 要点 + +服务提供者注册服务的过程: + +Dubbo 配置项 `dubbo://registry` 声明了注册中心的地址,Dubbo 会把以上配置项解析成类似下面的 URL 格式: + +``` +registry://multicast://224.5.6.7:1234/com.alibaba.dubbo.registry.RegistryService?export=URL.encode("dubbo://host-ip:20880/com.alibaba.dubbo.demo.DemoService") +``` + +然后基于扩展点自适应机制,通过 URL 的 `registry://` 协议头识别,就会调用 `RegistryProtocol` 的 `export` 方法,将 `export` 参数中的提供者 URL,注册到注册中心。 + +服务消费者发现服务的过程: + +Dubbo 配置项 `dubbo://registry` 声明了注册中心的地址,跟服务注册的原理类似,Dubbo 也会把以上配置项解析成下面的 URL 格式: + +``` +registry://multicast://224.5.6.7:1234/com.alibaba.dubbo.registry.RegistryService?refer=URL.encode("consummer://host-ip/com.alibaba.dubbo.demo.DemoService") +``` + +然后基于扩展点自适应机制,通过 URL 的 `registry://` 协议头识别,就会调用 `RegistryProtocol` 的 `refer` 方法,基于 `refer` 参数中的条件,查询服务 `demoService` 的地址。 + +::: + +### 【基础】Dubbo 支持哪些注册中心? + +:::details 要点 + +不同于传统的 Dubbo2,Dubbo3 中定义了三种中心:注册中心、配置中心、元数据中心。配置中心、元数据中心是实现 Dubbo 高阶服务治理能力会依赖的组件,如流量管控规则等,相比于注册中心通常这两个组件的配置是可选的。 + +配置方式如下: + +```yaml +dubbo + registry + address: nacos://localhost:8848 + config-center + address: nacos://localhost:8848 + metadata-report + address: nacos://localhost:8848 +``` + +需要注意的是,**对于部分注册中心类型(如 Zookeeper、Nacos 等),Dubbo 会默认同时将其用作元数据中心和配置中心(建议保持默认开启状态)。** + +Dubbo 目前支持的主流注册中心实现包括: + +- Zookeeper +- Nacos +- Redis +- Consul +- Etcd +- 更多实现 + +同时也支持 Kubernetes、Mesh 体系的服务发现,具体请参考 [使用教程 - kubernetes部署](http://localhost:1313/zh-cn/overview/mannual/java-sdk/tasks/deploy/) + +::: + +### 【中级】注册中心是选择 CP 还是 AP? + +:::details 要点 + +#### 什么是 CAP + +在分布式系统领域,有一个著名的 [CAP 理论](https://en.wikipedia.org/wiki/CAP_theorem)。CAP 定理提出:分布式系统有三个指标,这三个指标不能同时做到: + +- **一致性(Consistency)** - 在任何给定时间,网络中的所有节点都具有完全相同(最近)的值。 +- **可用性(Availability)** - 对网络的每个请求都会返回响应,但不能保证返回的数据是最新的。 +- **分区容错性(Partition Tolerance)** - 即使任意数量的节点出现故障,网络仍会继续运行。 + +CAP 就是取 Consistency、Availability、Partition Tolerance 的首字母而命名。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202405160639643.png) + +在分布式系统中,分区容错性是一个既定的事实:因为分布式系统总会出现各种各样的问题,如由于网络原因而导致节点失联;发生机器故障;机器重启或升级等等。因此,**CAP 定理实际上是要在可用性(A)和一致性(C)之间做权衡**。 + +#### 注册中心选 AP 还是 CP + +注册中心作为服务提供者和服务消费者之间沟通的桥梁,它的重要性不言而喻。所以注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。 + +根据 [CAP 理论](https://en.wikipedia.org/wiki/CAP_theorem),三种特性无法同时达成,必须在可用性和一致性之间做取舍。于是,根据不同侧重点,注册中心可以分为 CP 和 AP 两个阵营: + +- **CP 型注册中心** - **牺牲可用性来换取数据强一致性**,最典型的例子就是 ZooKeeper,etcd,Consul 了。ZooKeeper 集群内只有一个 Leader,而且在 Leader 无法使用的时候通过算法选举出一个新的 Leader。这个 Leader 的目的就是保证写信息的时候只向这个 Leader 写入,Leader 会同步信息到 Followers,这个过程就可以保证数据的强一致性。但如果多个 ZooKeeper 之间网络出现问题,造成出现多个 Leader,发生脑裂的话,注册中心就不可用了。而 etcd 和 Consul 集群内都是通过 Raft 协议来保证强一致性,如果出现脑裂的话, 注册中心也不可用。 +- **AP 型注册中心** - **牺牲一致性(只保证最终一致性)来换取可用性**,最典型的例子就是 Eureka 了。Eureka 在设计的时候就是优先保证 A (可用性)。在 Eureka 中不存在什么 Leader 节点,每个节点都是一样的、平等的。因此 Eureka 不会像 ZooKeeper 那样出现选举过程中或者半数以上的机器不可用的时候服务就是不可用的情况。 Eureka 保证即使大部分节点挂掉也不会影响正常提供服务,只要有一个节点是可用的就行了。只不过这个节点上的数据可能并不是最新的。 +- **CP & AP 都支持型注册中心** - Nacos的内在设计偏向于 CP,即在发生网络分区的情况下优先保证数据的一致性和分区容错性,牺牲一定的可用性。虽然 Nacos 的内在设计偏向于CP,但通过合理的配置与实践,可以在一定程度上优化其可用性。例如:调整副本数、配置同步策略。更多详情可以参考:[Nacos CAP](https://nacos.io/en/blog/faq/nacos-user-question-history10508/?spm=5238cd80.e9131ff.0.0.69845e2958zjvo&source=wuyi) + +选择 CP 还是 AP,根据实际需要来定:如果业务场景要求强一致,优先选择 CP 型注册中心;如果业务场景强调可用性,优先选择 AP 型注册中心。 + +::: + +### 【基础】注册中心挂了可以继续通信吗? + +可以。Dubbo 消费者在应用启动时会从注册中心拉取已注册的生产者的地址接口,并缓存在本地。每次调用时,按照本地存储的地址进行调用。 + +## 通信协议和序列化 + +### 【基础】Dubbo 支持哪些通信协议,各有什么利弊? + +:::details 要点 + +Dubbo 框架提供了自定义的高性能 RPC 通信协议:基于 HTTP/2 的 Triple 协议 和 基于 TCP 的 Dubbo2 协议。除此之外,Dubbo 框架支持任意第三方通信协议,如官方支持的 gRPC、Thrift、REST、JsonRPC、Hessian2 等,更多协议可以通过自定义扩展实现。这对于微服务实践中经常要处理的多协议通信场景非常有用。 + +**Dubbo 框架不绑定任何通信协议,在实现上 Dubbo 对多协议的支持也非常灵活,它可以让你在一个应用内发布多个使用不同协议的服务,并且支持用同一个 port 端口对外发布所有协议。** + +![protocols](https://cn.dubbo.apache.org/imgs/v3/feature/protocols/protocol1.png) + +Dubbo 官方支持的协议如下: + +- **HTTP/2 (Triple)** - Dubbo3 新增,基于 HTTP/2 并且完全兼容 gRPC 协议,原生支持 Streaming 通信语义,Triple 可同时运行在 HTTP/1 和 HTTP/2 传输协议之上,让你可以直接使用 curl、浏览器访问后端 Dubbo 服务。自 Triple 协议开始,Dubbo 还支持基于 Protocol Buffers 的服务定义与数据传输,但 Triple 实现并不绑定 IDL。Triple 具备更好的网关、代理穿透性,因此非常适合于跨网关、代理通信的部署架构,如服务网格等。更多详情见:Triple 协议详情见 [Triple 协议开发任务](https://cn.dubbo.apache.org/zh-cn/overview/what/tasks/protocols/triple/)、[Triple 设计思路与协议规范](https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/triple/)。 +- **Dubbo2** - Dubbo2 协议是基于 TCP 传输层协议之上构建的一套 RPC 通信协议,具有紧凑、灵活、高性能等特点。它是 Dubbo 的默认通信协议,采用单一长连接和 NIO 异步通信,基于 hessian 作为序列化协议。Dubbo2 协议适合于小数据量大并发的服务调用,以及服务消费者机器数远大于服务提供者机器数的情况。反之,Dubbo 缺省协议不适合传送大数据量的服务,比如传文件,传视频等,除非请求量很低。Dubbo 协议详情见 [Dubbo2 协议开发任务](https://cn.dubbo.apache.org/zh-cn/overview/what/tasks/protocols/dubbo/)、[Dubbo2 设计思路与协议规范](https://cn.dubbo.apache.org/zh-cn/overview/reference/protocols/tcp/)。 +- **gRPC** - gRPC 是谷歌开源的基于 HTTP/2 的通信协议。gRPC 的定位是通信协议与实现,是一款纯粹的 RPC 框架,而 Dubbo 定位是一款微服务框架,为微服务实践提供解决方案。在 Dubbo 体系下使用 gRPC 协议是一个非常高效和轻量的选择,它让你既能使用原生的 gRPC 协议通信,又避免了基于 gRPC 进行二次定制与开发的复杂度。gRPC 协议详情见 [gRPC over Dubbo 示例](https://cn.dubbo.apache.org/zh-cn/overview/what/tasks/protocols/grpc/)。 +- **REST** - 微服务领域常用的一种通信模式是 HTTP + JSON,包括 Spring Cloud、Microprofile 等一些主流的微服务框架都默认使用的这种通信模式,Dubbo 同样提供了对基于 HTTP 的编程、通信模式的支持。REST 协议详情见 [HTTP over Dubbo 示例](https://cn.dubbo.apache.org/zh-cn/overview/what/tasks/protocols/web/)、[Dubbo 与 Spring Cloud 体系互通](https://cn.dubbo.apache.org/zh-cn/overview/what/tasks/protocols/springcloud/)。 +- **Hessian** - [hessian](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/hessian.html) 协议用于集成 Hessian 的服务,Hessian 底层采用 Http 通讯,采用 Servlet 暴露服务,Dubbo 缺省内嵌 Jetty 作为服务器实现。Dubbo 的 Hessian 协议可以和原生 Hessian 服务互操作,即: + - 提供者用 Dubbo 的 Hessian 协议暴露服务,消费者直接用标准 Hessian 接口调用 + - 或者提供方用标准 Hessian 暴露服务,消费方用 Dubbo 的 Hessian 协议调用。 +- **Thrift** - dubbo 支持的 [thrift](http://dubbo.apache.org/zh-cn/docs/user/references/protocol/thrift.html) 协议是对 thrift 原生协议的扩展,在原生协议的基础上添加了一些额外的头信息,比如 service name,magic number 等。使用 dubbo thrift 协议同样需要使用 thrift 的 idl compiler 编译生成相应的 java 代码。 + +扩展:[Dubbo 官方文档之通信协议](https://cn.dubbo.apache.org/zh-cn/overview/what/core-features/protocols/) + +::: + +## 负载均衡 + +### 【中级】Dubbo 支持哪些负载均衡方式?各有什么利弊? + +:::details 要点 + +Dubbo 提供了多种均衡策略,缺省为 `weighted random` 基于权重的随机负载均衡策略。 + +具体实现上,Dubbo 提供的是客户端负载均衡,即由 Consumer 通过负载均衡算法得出需要将请求提交到哪个 Provider 实例。 + +目前 Dubbo 内置了如下负载均衡算法,可通过调整配置项启用。 + +| 算法 | 特性 | 备注 | +| :---------------------------- | :---------------------- | :--------------------------------------------------- | +| Weighted Random LoadBalance | 加权随机 | 默认算法,默认权重相同 | +| RoundRobin LoadBalance | 加权轮询 | 借鉴于 Nginx 的平滑加权轮询算法,默认权重相同, | +| LeastActive LoadBalance | 最少活跃优先 + 加权随机 | 背后是能者多劳的思想 | +| Shortest-Response LoadBalance | 最短响应优先 + 加权随机 | 更加关注响应速度 | +| ConsistentHash LoadBalance | 一致性哈希 | 确定的入参,确定的提供者,适用于有状态请求 | +| P2C LoadBalance | Power of Two Choice | 随机选择两个节点后,继续选择“连接数”较小的那个节点。 | +| Adaptive LoadBalance | 自适应负载均衡 | 在 P2C 算法基础上,选择二者中 load 最小的那个节点 | + +Dubbo 的负载均衡配置可以细粒度到服务、方法级别,且 `dubbo:service` 和 `dubbo:reference` 均可配置。 + +```xml + + + + + + + + + + + + +``` + +#### Weighted Random + +- **加权随机**,按权重设置随机概率。 +- 在一个截面上碰撞的概率高,但调用量越大分布越均匀,而且按概率使用权重后也比较均匀,有利于动态调整提供者权重。 +- 缺点:存在慢的提供者累积请求的问题,比如:第二台机器很慢,但没挂,当请求调到第二台时就卡在那,久而久之,所有请求都卡在调到第二台上。 + +#### RoundRobin + +- **加权轮询**,按公约后的权重设置轮询比率,循环调用节点 +- 缺点:同样存在慢的提供者累积请求的问题。 + +#### LeastActive + +- **加权最少活跃调用优先**,活跃数越低,越优先调用,相同活跃数的进行加权随机。活跃数指调用前后计数差(针对特定提供者:请求发送数 - 响应返回数),表示特定提供者的任务堆积量,活跃数越低,代表该提供者处理能力越强。 +- 使慢的提供者收到更少请求,因为越慢的提供者的调用前后计数差会越大;相对的,处理能力越强的节点,处理更多的请求。 + +#### ShortestResponse + +- **加权最短响应优先**,在最近一个滑动窗口中,响应时间越短,越优先调用。相同响应时间的进行加权随机。 +- 使得响应时间越快的提供者,处理更多的请求。 +- 缺点:可能会造成流量过于集中于高性能节点的问题。 + +这里的响应时间 = 某个提供者在窗口时间内的平均响应时间,窗口时间默认是 30s。 + +#### ConsistentHash + +- **一致性 Hash**,相同参数的请求总是发到同一提供者。 +- 当某一台提供者挂时,原本发往该提供者的请求,基于虚拟节点,平摊到其它提供者,不会引起剧烈变动。 +- 算法参见:[Consistent Hashing | WIKIPEDIA](http://en.wikipedia.org/wiki/Consistent_hashing) +- 缺省只对第一个参数 Hash,如果要修改,请配置 `` +- 缺省用 160 份虚拟节点,如果要修改,请配置 `` + +#### P2C Load Balance + +Power of Two Choice 算法简单但是经典,主要思路如下: + +1. 对于每次调用,从可用的 provider 列表中做两次随机选择,选出两个节点 providerA 和 providerB。 +2. 比较 providerA 和 providerB 两个节点,选择其“当前正在处理的连接数”较小的那个节点。 + +以下是 [Dubbo P2C 算法实现提案](https://cn.dubbo.apache.org/zh-cn/overview/reference/proposals/heuristic-flow-control/#p2c算法) + +#### Adaptive Load Balance + +Adaptive 即自适应负载均衡,是一种能根据后端实例负载自动调整流量分布的算法实现,它总是尝试将请求转发到负载最小的节点。 + +以下是 [Dubbo Adaptive 算法实现提案](https://cn.dubbo.apache.org/zh-cn/overview/reference/proposals/heuristic-flow-control/#adaptive算法) + +> 扩展: +> +> - [Dubbo 官方文档之负载均衡](https://cn.dubbo.apache.org/zh-cn/overview/what/core-features/load-balance/) +> - [负载均衡](https://dunwu.github.io/waterdrop/pages/bcf0fb8c/) + +::: + +## 路由 + +### 【中级】Dubbo 路由是怎样工作的? + +:::details 要点 + +以下是 Dubbo 单个路由器的工作过程,路由器接收一个服务的实例地址集合作为输入,基于请求上下文 (Request Context) 和 (Router Rule) 实际的路由规则定义对输入地址进行匹配,所有匹配成功的实例组成一个地址子集,最终地址子集作为输出结果继续交给下一个路由器或者负载均衡组件处理。 + +![Router](https://cn.dubbo.apache.org/imgs/v3/feature/traffic/router1.png) + +通常,在 Dubbo 中,多个路由器组成一条路由链共同协作,前一个路由器的输出作为另一个路由器的输入,经过层层路由规则筛选后,最终生成有效的地址集合。 + +- Dubbo 中的每个服务都有一条完全独立的路由链,每个服务的路由链组成可能不通,处理的规则各异,各个服务间互不影响。 +- 对单条路由链而言,即使每次输入的地址集合相同,根据每次请求上下文的不同,生成的地址子集结果也可能不同。 + +![Router](https://cn.dubbo.apache.org/imgs/v3/feature/traffic/router2.png) + +::: + +### 【中级】Dubbo 支持哪些路由方式?分别适用于什么场景? + +:::details 要点 + +Dubbo 的路由规则可以基于应用、服务、方法、参数等粒度精准的控制请求分发,根据请求的目标服务、方法以及请求体中的其他附加参数进行匹配,符合匹配条件的请求会进一步的按照特定规则转发到一个地址子集。 + +Dubbo 支持以下路由规则: + +- 标签路由规则 +- 条件路由规则 +- 脚本路由规则 +- 动态配置规则 + +#### 标签路由规则 + +**标签路由**通过将某一个服务的实例划分到不同的**分组**,**约束具有特定标签的流量只能在指定分组中流转**,不同分组为不同的流量场景服务,从而实现流量隔离的目的。**标签路由可以作为蓝绿发布、灰度发布等场景能力的基础**。 + +标签路由规则是一个非此即彼的流量隔离方案,也就是匹配标签的请求会 100% 转发到有相同标签的实例,没有匹配标签的请求会 100% 转发到其余未匹配的实例。如果您需要按比例的流量调度方案,请参考示例 [基于权重的按比例流量路由](https://cn.dubbo.apache.org/zh-cn/overview/what/core-features/tasks/traffic-management/weight/)。 + +**标签主要是指对 Provider 端应用实例的分组**,目前有两种方式可以完成实例分组,分别是动态规则打标和静态规则打标。**动态规则打标**可以在运行时动态的圈住一组机器实例,而**静态规则打标**则需要实例重启后才能生效,其中,动态规则相较于静态规则优先级更高,而当两种规则同时存在且出现冲突时,将以动态规则为准。 + +#### 条件路由规则 + +条件路由与标签路由的工作模式非常相似,也是首先对请求中的参数进行匹配,**符合匹配条件的请求将被转发到包含特定实例地址列表的子集**。相比于标签路由,条件路由的匹配方式更灵活: + +- 在标签路由中,一旦给某一台或几台机器实例打了标签,则这部分实例就会被立马从通用流量集合中移除,不同标签之间不会再有交集。有点类似下图,地址集合在输入阶段就已经划分明确。 + +![tag-condition-compare](https://cn.dubbo.apache.org/imgs/v3/feature/traffic/tag-condition-compare1.png) + +- 而从条件路由的视角,所有的实例都是一致的,路由过程中不存在分组隔离的问题,每次路由过滤都是基于全量地址中执行 + +![tag-condition-compare](https://cn.dubbo.apache.org/imgs/v3/feature/traffic/tag-condition-compare2.png) + +条件路由规则的主体 `conditions` 主要包含两部分内容: + +- => 之前的为请求参数匹配条件,指定的**匹配条件指定的参数**将与**消费者的请求上下文 (URL)**、甚至**方法参数**进行对比,当消费者满足匹配条件时,对该消费者执行后面的地址子集过滤规则。 +- => 之后的为地址子集过滤条件,指定的**过滤条件指定的参数**将与**提供者实例地址 (URL)**进行对比,消费者最终只能拿到符合过滤条件的实例列表,从而确保流量只会发送到符合条件的地址子集。 + - 如果匹配条件为空,表示对所有请求生效,如:`=> status != staging` + - 如果过滤条件为空,表示禁止来自相应请求的访问,如:`application = product =>` + +#### 动态配置规则 + +通过 Dubbo 提供的动态配置规则,可以动态的修改 Dubbo 服务进程的运行时行为,整个过程不需要重启,配置参数实时生效。基于这个强大的功能,基本上所有运行期参数都可以动态调整,比如超时时间、临时开启 Access Log、修改 Tracing 采样率、调整限流降级参数、负载均衡、线程池配置、日志等级、给机器实例动态打标签等。与上文讲到的流量管控规则类似,动态配置规则支持应用、服务两个粒度,也就是说一次可以选择只调整应用中的某一个或几个服务的参数配置。 + +当然,出于系统稳定性、安全性的考量,有些特定的参数是不允许动态修改的,但除此之外,基本上所有参数都允许动态修改,很多强大的运行态能力都可以通过这个规则实现。通常 URL 地址中的参数均可以修改,这在每个语言实现的参考手册里也记录了一些更详细的说明。 + +#### 脚本路由规则 + +脚本路由是最直观的路由方式,同时它也是当前最灵活的路由规则,因为你可以在脚本中定义任意的地址筛选规则。如果我们为某个服务定义一条脚本规则,则后续所有请求都会先执行一遍这个脚本,脚本过滤出来的地址即为请求允许发送到的、有效的地址集合。 + +```yaml +configVersion: v3.0 +key: demo-provider +type: javascript +enabled: true +script: | + (function route(invokers,invocation,context) { + var result = new java.util.ArrayList(invokers.size()); + for (i = 0; i < invokers.size(); i ++) { + if ("10.20.3.3".equals(invokers.get(i).getUrl().getHost())) { + result.add(invokers.get(i)); + } + } + return result; + } (invokers, invocation, context)); // 表示立即执行方法 +``` + +::: + +## 服务治理 + +### 【中级】Dubbo 有哪些集群容错策略? + +:::details 要点 + +在集群调用失败时,Dubbo 提供了多种容错方案,缺省为 failover 重试。 + +![Dubbo 容错](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/rpc/dubbo/dubbo集群容错.jpg) + +图中节点关系说明: + +- 这里的 `Invoker` 是 `Provider` 的一个可调用 `Service` 的抽象,`Invoker` 封装了 `Provider` 地址及 `Service` 接口信息 +- `Directory` 代表多个 `Invoker`,可以把它看成 `List` ,但与 `List` 不同的是,它的值可能是动态变化的,比如注册中心推送变更 +- `Cluster` 将 `Directory` 中的多个 `Invoker` 伪装成一个 `Invoker`,对上层透明,伪装过程包含了容错逻辑,调用失败后,重试另一个 +- `Router` 负责从多个 `Invoker` 中按路由规则选出子集,比如读写分离,应用隔离等 +- `LoadBalance` 负责从多个 `Invoker` 中选出具体的一个用于本次调用,选的过程包含了负载均衡算法,调用失败后,需要重选 + +Dubbo 支持的容错策略: + +- **Failover** - **失败自动切换**。当出现失败,重试其它服务器。通常用于读操作,但重试会带来更长延迟。可通过 `retries="2"` 来设置重试次数(不含第一次)。 +- **Failfast** - **快速失败**。只发起一次调用,失败立即报错。通常用于非幂等性的写操作,比如新增记录。 +- **Failsafe** - **失败安全**。出现异常时,直接忽略。通常用于写入审计日志等操作。 +- **Failback** - **失败自动恢复**。后台记录失败请求,定时重发。通常用于消息通知操作。 +- **Forking** - **并行调用多个服务器**。只要一个成功即返回。通常用于实时性要求较高的读操作,但需要浪费更多服务资源。可通过 `forks="2"` 来设置最大并行数。 +- **Broadcast** - **广播调用所有提供者**。逐个调用,任意一台报错则报错。通常用于通知所有提供者更新缓存或日志等本地资源信息。 + +集群容错配置示例: + +```xml + + +``` + +::: + +### 【中级】Dubbo 提供了哪些监控能力? + +:::details 要点 + +Dubbo 内部维护了多个纬度的可观测指标,并且支持多种方式的可视化监测。可观测性指标从总体上来说分为三个度量纬度: + +- **Admin** - Admin 控制台可视化展示了集群中的应用、服务、实例及依赖关系,支持流量治理规则下发,同时还提供如服务测试、mock、文档管理等提升研发测试效率的工具。 +- **Metrics** - Dubbo 统计了一系列的流量指标如 QPS、RT、成功请求数、失败请求数等,还包括一系列的内部组件状态如线程池数、服务健康状态等。 +- **Tracing** - Dubbo 与业界主流的链路追踪工作做了适配,包括 Skywalking、Zipkin、Jaeger 都支持 Dubbo 服务的链路追踪。 +- **Logging** - Dubbo 支持多种日志框架适配。以 Java 体系为例,支持包括 Slf4j、Log4j2、Log4j、Logback、Jcl 等,用户可以基于业务需要选择合适的框架;同时 Dubbo 还支持 Access Log 记录请求踪迹。 + +::: + +## 应用 + +### 【基础】接口不同版本如何兼容? + +:::details 要点 + +#### 版本和分组 + +Dubbo服务中,接口并不能唯一确定一个服务,只有 `接口+分组+版本号` 的三元组才能唯一确定一个服务。 + +- 当同一个接口针对不同的业务场景、不同的使用需求或者不同的功能模块等场景,可使用服务分组来区分不同的实现方式。同时,这些不同实现所提供的服务是可并存的,也支持互相调用。 +- 当接口实现需要升级又要保留原有实现的情况下,即出现不兼容升级时,我们可以使用不同版本号进行区分。 + +下面以官方示例来解释一下如何指定版本。 + +假设,接口定义如下: + +```java +public interface DevelopService { + String invoke(String param); +} +``` + +版本 1 实现: + +```java +@DubboService(group = "group1", version = "1.0") +public class DevelopProviderServiceV1 implements DevelopService{ + @Override + public String invoke(String param) { + StringBuilder s = new StringBuilder(); + s.append("ServiceV1 param:").append(param); + return s.toString(); + } +} +``` + +版本 2 实现: + +```java +@DubboService(group = "group2", version = "2.0") +public class DevelopProviderServiceV2 implements DevelopService{ + @Override + public String invoke(String param) { + StringBuilder s = new StringBuilder(); + s.append("ServiceV2 param:").append(param); + return s.toString(); + } +} +``` + +#### 跨版本升级 + +可以按照以下的步骤进行版本迁移: + +1. 在低压力时间段,先升级一半提供者为新版本 +2. 再将所有消费者升级为新版本 +3. 然后将剩下的一半提供者升级为新版本 + +当一个接口实现,出现不兼容升级时,可以用版本号过渡,版本号不同的服务相互间不引用。 + +> 参考用例 [https://github.com/apache/dubbo-samples/tree/master/dubbo-samples-version](https://github.com/apache/dubbo-samples/tree/master/2-advanced/dubbo-samples-version) + +**服务提供者** + +老版本服务提供者配置: + +```xml + +``` + +新版本服务提供者配置: + +```xml + +``` + +**服务消费者** + +老版本服务消费者配置: + +```xml + +``` + +新版本服务消费者配置: + +```xml + +``` + +**不区分版本** + +如果不需要区分版本,可以按照以下的方式配置: + +```xml + +``` + +通过以上描述,可以看到,通过版本号来进行 Dubbo 接口升级实际上较为麻烦。如果接口提供方和消费方分属不同的业务团队,同步发版就更加麻烦了。因此,在实际应用中,更常见的操作是应该尽量充分考虑接口的后向兼容性,确保不会影响旧版本的调用。需要考虑的点如下: + +- 如果方法签名无任何变化,不会影响旧版本的调用。服务提供方可以直接先全量上线。 +- 如果入参、出参上新增属性,不会影响旧版本的调用(当然,对于新增属性的逻辑处理要充分考虑兼容性)。服务提供方可以直接先全量上线,消费方根据需要选择是否后续安排对接。 +- 如果入参、出参上删除或修改属性,老接口无法正常调用,会出现序列化问题。这种情况,可以添加新的方法来实现。 + +> 扩展阅读:[Dubbo 官方文档之版本与分组](https://cn.dubbo.apache.org/zh-cn/overview/mannual/java-sdk/tasks/framework/version_group/) + +::: + +## 参考资料 + +- [Dubbo Github](https://github.com/apache/dubbo) +- [Dubbo 官方文档](https://dubbo.apache.org/zh-cn/) +- [Dubbo 框架设计](https://cn.dubbo.apache.org/zh-cn/docsv2.7/dev/design/) +- [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/dubbo-service-management.md) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/README.md" index 755c50e347..e3549a5890 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/README.md" @@ -7,9 +7,10 @@ categories: - RPC tags: - 分布式 - - 分布式通信 + - 通信 - RPC -permalink: /pages/a03b7b/ + - 微服务 +permalink: /pages/2af2f5a6/ hidden: true index: false --- @@ -18,14 +19,11 @@ index: false ## 📖 内容 -### RPC 综合 - -- [RPC 基础](00.RPC综合/01.RPC基础.md) -- [RPC 进阶](00.RPC综合/02.RPC进阶.md) -- [RPC 高级](00.RPC综合/03.RPC高级.md) +- [Dubbo 面试](Dubbo面试.md) +- [RPC 面试](RPC面试.md) ## 📚 资料 ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/RPC\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/RPC\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..f7154649e7 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC/RPC\351\235\242\350\257\225.md" @@ -0,0 +1,556 @@ +--- +title: RPC 面试 +date: 2025-01-22 08:08:21 +categories: + - 分布式 + - 分布式通信 + - RPC +tags: + - 分布式 + - 通信 + - 微服务 + - RPC +permalink: /pages/9b74993b/ +--- + +# RPC 面试 + +## RPC 简介 + +### 【基础】什么是 RPC?RPC 有什么用? + +:::details 要点 + +RPC 的全称是 **Remote Procedure Call**,即**远程过程调用**。 + +RPC 的主要作用是: + +- **屏蔽远程调用跟本地调用的差异**,让用户像调用本地一样去调用远程方法。 +- **隐藏底层网络通信的复杂性**,让用户更聚焦于业务逻辑。 + +RPC 是微服务架构的基石,它提供了一种应用间通信的方式。 + +::: + +### 【中级】RPC 是怎样工作的? + +:::details 要点 + +RPC 是一种应用间通信的方式,它的通信流程中需要注意以下环节: + +- **传输方式**:RPC 是一个远程调用,因此必然需要通过网络传输数据,且 RPC 常用于业务系统之间的数据交互,需要保证其可靠性,所以 RPC 一般默认采用 TCP 来传输。 +- **序列化**:在网络中传输的数据只能是二进制数据,而 RPC 请求时,发送的都是对象。因此,请求方需要将请求参数转为二进制数据,即序列化。 +- **反序列化**:RPC 响应方接受到请求,要将二进制数据转换为请求参数,需要**反序列化**。 +- **协议**:请求方和响应方要互相识别彼此的信息,需要约定好彼此数据的格式,即协议。大多数的协议至少分成两部分,分别是数据头和消息体。数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。 +- **动态代理**:为了屏蔽底层通信细节,使用户聚焦自身业务,因此 RPC 框架一般引入了动态代理,通过依赖注入等技术,拦截方法调用,完成远程调用的通信逻辑。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220625094814.png) + +1. 服务消费方(client)调用以本地调用方式调用服务; +2. client stub 接收到调用后负责将方法、参数等组装成能够进行网络传输的消息体; +3. client stub 找到服务地址,并将消息发送到服务端; +4. server stub 收到消息后进行解码; +5. server stub 根据解码结果调用本地的服务; +6. 本地服务执行并将结果返回给 server stub; +7. server stub 将返回结果打包成消息并发送至消费方; +8. client stub 接收到消息,并进行解码; +9. 服务消费方得到最终结果。 + +::: + +## 协议 + +### 【中级】为何需要 RPC 协议? + +:::details 要点 + +只有二进制才能在网络中传输,所以 RPC 请求需要把方法调用的请求参数先转成二进制,然后再通过网络传输。 + +传输的数据可能很大,RPC 请求需要将数据分解为多个数据包;传输的数据也可能较小,需要和其他请求的数据包进行合并。当接收方收到请求时,需要从二进制数据中识别出不同的请求。问题是,如何从二进制数据中识别出其所属的请求呢? + +这就需要发送方、接收方在通信过程中达成共识,严格按照协议处理二进制数据。这就好比让你读一篇没有标点符号的文章,你要怎么识别出每一句话到哪里结束呢?很简单啊,我们加上标点,完成断句就好了。这里有个潜在的含义,写文章和读文章的人,都遵循标点符号的用法。 + +再进一步探讨,既然已经有很多成熟的网络协议,为何还要设计 RPC 协议? + +有必要。因为 HTTP 这些通信标准协议,数据包中的实际请求数据相对于数据包本身要小很多,有很多无用的内容;并且 HTTP 属于无状态协议,无法将请求和响应关联,每次请求要重新建立连接。这对于高性能的 RPC 来说,HTTP 协议难以满足需求,所以有必要设计一个**紧凑的私有协议**。 + +::: + +### 【中级】设计一个 RPC 协议的要点? + +:::details 要点 + +首先,必须先明确消息的边界,即确定消息的长度。因此,至少要分为:消息长度+消息内容两部分。 + +接下来,我们会发现,在使用过程中,仅消息长度,不足以明确通信中的很多细节:如序列化方式是怎样的?是否消息压缩?压缩格式是怎样的?如果协议发生变化,需要明确协议版本等等。 + +大多数的协议会分成两部分,分别是数据头和消息体。数据头一般用于身份识别,包括协议标识、数据大小、请求类型、序列化类型等信息;消息体主要是请求的业务参数信息和扩展属性等。 + +综上,一个 RPC 协议大概会由下图中的这些参数组成: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220619102052.png) + +前面所述的协议属于定长协议头,那也就是说往后就不能再往协议头里加新参数了,如果加参数就会导致线上兼容问题。 + +为了保证能平滑地升级改造前后的协议,我们有必要设计一种支持可扩展的协议。其关键在于让协议头支持可扩展,扩展后协议头的长度就不能定长了。那要实现读取不定长的协议头里面的内容,在这之前肯定需要一个固定的地方读取长度,所以我们需要一个固定的写入协议头的长度。整体协议就变成了三部分内容:固定部分、协议头内容、协议体内容。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220619102833.png) + +::: + +## 序列化 + +### 【基础】什么是序列化?有哪些常见的序列化方式? + +:::details 要点 + +由于,网络传输的数据必须是二进制数据,而调用方请求的出参、入参都是对象。因此,必须将对象转换可传输的二进制,并且要求转换算法是可逆的。 + +- **序列化(serialize)**:序列化是将对象转换为二进制数据。 +- **反序列化(deserialize)**:反序列化是将二进制数据转换为对象。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220619110947.png) + +Java 领域,常见的序列化技术如下 + +- JDK 序列化:JDK 内置的二进制序列化方式 +- 其他二进制序列化 + - [Thrift](https://github.com/apache/thrift) + - [Protobuf](https://github.com/protocolbuffers/protobuf) + - [Hessian](http://hessian.caucho.com/doc/hessian-overview.xtp) +- JSON 序列化 + - [Jackson](https://github.com/FasterXML/jackson) + - [Gson](https://github.com/google/gson) + - [Fastjson](https://github.com/alibaba/fastjson) + +市面上有如此多的序列化技术,那么我们在应用时如何选择呢? + +一般而言,序列化技术选型需要考量的维度,根据重要性从高到低,依次有: + +- **安全性**:是否存在漏洞。如果存在漏洞,就有被攻击的可能性。 +- **兼容性**:版本升级后的兼容性是否很好,是否支持更多的对象类型,是否是跨平台、跨语言的。服务调用的稳定性与可靠性,要比服务的性能更加重要。 +- **性能** + - **时间开销**:序列化、反序列化的耗时性能自然越小越好。 + - **空间开销**:序列化后的数据越小越好,这样网络传输效率就高。 +- **易用性**:类库是否轻量化,API 是否简单易懂。 + +鉴于以上的考量,序列化技术的选型建议如下: + +- JDK 序列化:性能较差,且有很多使用限制,不建议使用。 +- [Thrift](https://github.com/apache/thrift)、[Protobuf](https://github.com/protocolbuffers/protobuf):适用于**对性能敏感,对开发体验要求不高**。 +- [Hessian](http://hessian.caucho.com/doc/hessian-overview.xtp):适用于**对开发体验敏感,性能有要求**。 +- [Jackson](https://github.com/FasterXML/jackson)、[Gson](https://github.com/google/gson)、[Fastjson](https://github.com/alibaba/fastjson):适用于对序列化后的数据要求有**良好的可读性**(转为 json 、xml 形式)。 + +> 扩展阅读:[深入理解 Java 序列化](https://dunwu.github.io/waterdrop/pages/dc9f1331/) + +::: + +### 【基础】序列化的使用中需要注意哪些问题? + +:::details 要点 + +由于 RPC 每次通信,都要经过序列化、反序列化的过程,所以序列化方式,会直接影响 RPC 通信的性能。除了选择合适的序列化技术,如何合理使用序列化也非常重要。 + +RPC 序列化常见的使用不当的情况如下: + +- **对象过于复杂、庞大** - 对象过于复杂、庞大,会降低序列化、反序列化的效率,并增加传输开销,从而导致响应时延增大。 + + - 过于复杂:存在多层的嵌套,比如 A 对象关联 B 对象,B 对象又聚合 C 对象,C 对象又关联聚合很多其他对象 + - 过于庞大:比如一个大 List 或者大 Map + +- **对象有复杂的继承关系** - 对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。大多数序列化框架在进行序列化时,如果发现类有继承关系,会不停地寻找父类,遍历属性。 +- **使用序列化框架不支持的类作为入参类** - 比如 Hessian 框架,他天然是不支持 LinkHashMap、LinkedHashSet 等,而且大多数情况下最好不要使用第三方集合类,如 Guava 中的集合类,很多开源的序列化框架都是优先支持编程语言原生的对象。因此如果入参是集合类,应尽量选用原生的、最为常用的集合类,如 HashMap、ArrayList。 + +前面已经列举了常见的序列化问题,既然明确了问题,就要针对性预防。RPC 序列化时要注意以下几点: + +1. 对象要尽量简单,没有太多的依赖关系,属性不要太多,尽量高内聚; +2. 入参对象与返回值对象体积不要太大,更不要传太大的集合; +3. 尽量使用简单的、常用的、开发语言原生的对象,尤其是集合类; +4. 对象不要有复杂的继承关系,最好不要有父子类的情况。 + +::: + +## 通信 + +### 【中级】RPC 在网络通信上倾向选择哪种网络 IO 模型? + +:::details 要点 + +一次 RPC 调用,本质就是服务消费者与服务提供者间的一次网络信息交换的过程。可见,通信是 RPC 实现的核心。 + +常见的网络 IO 模型分为四种:同步阻塞 IO(BIO)、同步非阻塞 IO(NIO)、IO 多路复用和异步非阻塞 IO(AIO)。在这四种 IO 模型中,只有 AIO 为异步 IO,其他都是同步 IO。 + +什么是 IO 多路复用?字面上的理解,多路就是指多个通道,也就是多个网络连接的 IO,而复用就是指多个通道复用在一个复用器上。IO 多路复用(Reactor 模式)在高并发场景下使用最为广泛,很多知名软件都应用了这一技术,如:Netty、Redis、Nginx 等。 + +RPC 调用在大多数的情况下,是一个高并发调用的场景,考虑到系统内核的支持、编程语言的支持以及 IO 模型本身的特点,在 RPC 框架的实现中,在网络通信的处理上,通常会选择 IO 多路复用的方式。开发语言的网络通信框架的选型上,最优的选择是基于 Reactor 模式实现的框架,如 Java 语言,首选的框架便是 Netty 框架(Java 还有很多其他 NIO 框架,但目前 Netty 应用得最为广泛),并且在 Linux 环境下,也要开启 epoll 来提升系统性能(Windows 环境下是无法开启 epoll 的,因为系统内核不支持)。 + +::: + +### 【高级】什么是零拷贝? + +:::details 要点 + +系统内核处理 IO 操作分为两个阶段——等待数据和拷贝数据。等待数据,就是系统内核在等待网卡接收到数据后,把数据写到内核中;而拷贝数据,就是系统内核在获取到数据后,将数据拷贝到用户进程的空间中。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717154300) + +应用进程的每一次写操作,都会把数据写到用户空间的缓冲区中,再由 CPU 将数据拷贝到系统内核的缓冲区中,之后再由 DMA 将这份数据拷贝到网卡中,最后由网卡发送出去。这里我们可以看到,一次写操作数据要拷贝两次才能通过网卡发送出去,而用户进程的读操作则是将整个流程反过来,数据同样会拷贝两次才能让应用程序读取到数据。 + +应用进程的一次完整的读写操作,都需要在用户空间与内核空间中来回拷贝,并且每一次拷贝,都需要 CPU 进行一次上下文切换(由用户进程切换到系统内核,或由系统内核切换到用户进程),这样很浪费 CPU 和性能。 + +所谓的零拷贝,就是取消用户空间与内核空间之间的数据拷贝操作,应用进程每一次的读写操作,可以通过一种方式,直接将数据写入内核或从内核中读取数据,再通过 DMA 将内核中的数据拷贝到网卡,或将网卡中的数据 copy 到内核。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200717154716.jfif) + +Netty 的零拷贝偏向于用户空间中对数据操作的优化,这对处理 TCP 传输中的拆包粘包问题有着重要的意义,对应用程序处理请求数据与返回数据也有重要的意义。 + +Netty 框架中很多内部的 ChannelHandler 实现类,都是通过 CompositeByteBuf、slice、wrap 操作来处理 TCP 传输中的拆包与粘包问题的。 + +Netty 的 ByteBuffer 可以采用 Direct Buffers,使用堆外直接内存进行 Socketd 的读写操作,最终的效果与我刚才讲解的虚拟内存所实现的效果是一样的。 + +Netty 还提供 FileRegion 中包装 NIO 的 FileChannel.transferTo() 方法实现了零拷贝,这与 Linux 中的 sendfile 方式在原理上也是一样的。 + +> 扩展阅读:[深入剖析Linux IO原理和几种零拷贝机制的实现](https://zhuanlan.zhihu.com/p/83398714) + +::: + +## 动态代理 + +### 【中级】RPC 如何将远程调用转为本地调用的? + +:::details 要点 + +**RPC 的远程过程调用是通过动态代理实现的**。 + +RPC 框架会自动为要调用的接口生成一个代理类。当在项目中注入接口的时候,运行过程中实际绑定的就是这个接口生成的代理类。在接口方法被调用时,会被代理类拦截,这样,就可以在生成的代理类中,加入远程调用逻辑。 + +![img](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/RPC%E5%AE%9E%E6%88%98%E4%B8%8E%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/assets/aaf85f07dd40412aa1a6e53224b1e2ab.jpg) + +除了 JDK 默认的 `InvocationHandler` 能完成代理功能,还有很多其他的第三方框架也可以,比如像 Javassist、Byte Buddy 这样的框架。 + +单纯从代理功能上来看,JDK 默认的代理功能是有一定的局限性的,它要求被代理的类只能是接口。原因是因为生成的代理类会继承 Proxy 类,但 Java 是不支持多重继承的。此外,由于它生成后的代理类是使用反射来完成方法调用的,而这种方式相对直接用编码调用来说,性能会降低。 + +> 反射+动态代理更多详情可以参考:[深入理解 Java 反射和动态代理](https://dunwu.github.io/waterdrop/pages/31248a53/) + +::: + +## 服务发现 + +### 【中级】如何实现服务发现? + +:::details 要点 + +RPC 框架必须要有服务注册和发现机制,这样,集群中的节点才能知道通信方的请求地址。 + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/RPC%e5%ae%9e%e6%88%98%e4%b8%8e%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86/assets/2d4469c7ceb8496b844e8652f634673c.jpg) + +- **服务注册**:在服务提供方启动的时候,将对外暴露的接口注册到注册中心之中,注册中心将这个服务节点的 IP 和接口保存下来。 +- **服务订阅**:在服务调用方启动的时候,去注册中心查找并订阅服务提供方的 IP,然后缓存到本地,并用于后续的远程调用。 + +#### 基于 ZooKeeper 的服务发现 + +使用 ZooKeeper 作为服务注册中心,是 Java 分布式系统的经典方案。 + +搭建一个 ZooKeeper 集群作为注册中心集群,服务注册的时候只需要服务节点向 ZooKeeper 节点写入注册信息即可,利用 ZooKeeper 的 Watcher 机制完成服务订阅与服务下发功能。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200610180056.png) + +通常我们可以使用 ZooKeeper、etcd 或者分布式缓存(如 Hazelcast)来解决事件通知问题,但当集群达到一定规模之后,依赖的 ZooKeeper 集群、etcd 集群可能就不稳定了,无法满足我们的需求。 + +在超大规模的服务集群下,注册中心所面临的挑战就是超大批量服务节点同时上下线,注册中心集群接受到大量服务变更请求,集群间各节点间需要同步大量服务节点数据,最终导致如下问题: + +- 注册中心负载过高; +- 各节点数据不一致; +- 服务下发不及时或下发错误的服务节点列表。 + +RPC 框架依赖的注册中心的服务数据的一致性其实并不需要满足 CP,只要满足 AP 即可。 + +::: + +## 负载均衡 + +### 【中级】负载均衡有哪些策略? + +> 负载均衡详情可以参考:[负载均衡基本原理](https://dunwu.github.io/waterdrop/pages/bcf0fb8c/) + +### 【中级】如何设计自适应的负载均衡? + +:::details 要点 + +可以采用一种打分的策略,服务调用者收集与之建立长连接的每个服务节点的指标数据,如服务节点的负载指标、CPU 核数、内存大小、请求处理的耗时指标(如请求平均耗时、TP99、TP999)、服务节点的状态指标(如正常、亚健康)。通过这些指标,计算出一个分数,比如总分 10 分,如果 CPU 负载达到 70%,就减它 3 分,当然了,减 3 分只是个类比,需要减多少分是需要一个计算策略的。可以为每个指标都设置一个指标权重占比,然后再根据这些指标数据,计算分数。 + +可以配合随机权重的负载均衡策略去控制,通过最终的指标分数修改服务节点最终的权重。例如给一个服务节点综合打分是 8 分(满分 10 分),服务节点的权重是 100,那么计算后最终权重就是 80(100\*80%)。服务调用者发送请求时,会通过随机权重的策略来选择服务节点,那么这个节点接收到的流量就是其他正常节点的 80%(这里假设其他节点默认权重都是 100,且指标正常,打分为 10 分的情况)。 + +到这儿,一个自适应的负载均衡我们就完成了,整体的设计方案如下图所示: + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/RPC%e5%ae%9e%e6%88%98%e4%b8%8e%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86/assets/e8aa1dfaeffd4a6b8745a36a2c15be5a.jpg) + +关键步骤我来解释下: + +1. 添加服务指标收集器,并将其作为插件,默认有运行时状态指标收集器、请求耗时指标收集器。 +2. 运行时状态指标收集器收集服务节点 CPU 核数、CPU 负载以及内存等指标,在服务调用者与服务提供者的心跳数据中获取。 +3. 请求耗时指标收集器收集请求耗时数据,如平均耗时、TP99、TP999 等。 +4. 可以配置开启哪些指标收集器,并设置这些参考指标的指标权重,再根据指标数据和指标权重来综合打分。 +5. 通过服务节点的综合打分与节点的权重,最终计算出节点的最终权重,之后服务调用者会根据随机权重的策略,来选择服务节点。 + +::: + +## 路由 + +### 【中级】什么是服务路由?有哪些常见的路由规则? + +:::details 要点 + +**服务路由**是指通过一定的规则从集群中选择合适的节点。 + +负载均衡的作用和服务路由的功能看上去很近似,二者有什么区别呢? + +**负载均衡的目标是提供服务分发而**不是解决路由问题,常见的静态、动态负载均衡算法也无法实现精细化的路由管理,但是负载均衡也可以简单看做是路由方案的一种。 + +服务路由通常用于以下场景,**目的在于实现流量隔离**: + +- **分组调用**:一般来讲,为了保证服务的高可用性,实现异地多活的需求,一个服务往往不止部署在一个数据中心,而且出于节省成本等考虑,有些业务可能不仅在私有机房部署,还会采用公有云部署,甚至采用多家公有云部署。服务节点也会按照不同的数据中心分成不同的分组,这时对于服务消费者来说,选择哪一个分组调用,就必须有相应的路由规则。 +- **蓝绿发布**:蓝绿发布场景中,一共有两套服务群组:一套是提供旧版功能的服务群组,标记为**绿色**;另一套是提供新版功能的服务群组,标记为**蓝色**。两套服务群组都是功能完善的,并且正在运行的系统,只是服务版本和访问流量不同。新版群组(蓝色)通常是为了做内部测试、验收,不对外部用户暴露。 + - 如果新版群组(蓝色)运行稳定,并测试、验收通过后,则通过服务路由、负载均衡等手段逐步将外部用户流量导向新版群组(蓝色)。 + - 如果新版群组(蓝色)运行不稳定,或测试、验收不通过,则排查、解决问题后,再继续测试、验收。 +- **灰度发布**:灰度发布(又名金丝雀发布)是指在黑与白之间,能够平滑过渡的一种发布方式。在其上可以进行 A/B 测试,即让一部分用户使用特性 A,一部分用户使用特性 B:如果用户对 B 没有什么反对意见,那么逐步扩大发布范围,直到把所有用户都迁移到 B 上面来。灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。要支持灰度发布,就要求服务能够根据一定的规则,将流量隔离。 +- **流量切换**:在业务线上运行过程中,经常会遇到一些不可抗力因素导致业务故障,比如某个机房的光缆被挖断,或者发生着火等事故导致整个机房的服务都不可用。这个时候就需要按照某个指令,能够把原来调用这个机房服务的流量切换到其他正常的机房。 +- **线下测试联调**:线下测试时,可能会缺少相应环境。可以将测试应用注册到线上,然后开启路由规则,在本地进行测试。 +- **读写分离**。对于大多数互联网业务来说都是读多写少,所以在进行服务部署的时候,可以把读写分开部署,所有写接口可以部署在一起,而读接口部署在另外的节点上。 + +常见的路由规则有: + +- **条件路由规则** - **条件路由是基于条件表达式的路由规则**。各个 RPC 框架的条件路由表达式各不相同。 +- **标签路由规则** - **标签路由**通过将某一个或多个服务的提供者划分到同一个分组,约束流量只在指定分组中流转,从而实现流量隔离的目的,可以作为蓝绿发布、灰度发布等场景的能力基础。标签主要是指对服务提供者的分组,目前有两种方式可以完成实例分组,分别是**动态规则打标**和**静态规则打标**。一般,动态规则优先级比静态规则更高,当两种规则同时存在且出现冲突时,将以动态规则为准。 +- **脚本路由规则** - **脚本路由**是基于脚本语言的路由规则,具有最高的灵活性,常用的脚本语言比如 JavaScript、Groovy、JRuby 等。 + +::: + +## 监控 + +### 【中级】如何实现 RPC 的健康检查? + +:::details 要点 + +**使用频率适中的心跳去检测目标机器的健康状态**。 + +- 健康状态:建立连接成功,并且心跳探活也一直成功; +- 亚健康状态:建立连接成功,但是心跳请求连续失败; +- 死亡状态:建立连接失败。 + +可以**使用可用率来作为健康状态的量化标准**: + +``` +可用率 = 一个时间窗口内接口调用成功次数 / 总调用次数 +``` + +当可用率低于某个比例,就认为这个节点存在问题,把它挪到亚健康列表,这样既考虑了高低频的调用接口,也兼顾了接口响应时间不同的问题。 + +::: + +### 链路跟踪 + +分布式链路跟踪就是将一次分布式请求还原为一个完整的调用链路,我们可以在整个调用链路中跟踪到这一次分布式请求的每一个环节的调用情况,比如调用是否成功,返回什么异常,调用的哪个服务节点以及请求耗时等等。 + +Trace 就是代表整个链路,每次分布式都会产生一个 Trace,每个 Trace 都有它的唯一标识即 TraceId,在分布式链路跟踪系统中,就是通过 TraceId 来区分每个 Trace 的。 +Span 就是代表了整个链路中的一段链路,也就是说 Trace 是由多个 Span 组成的。在一个 Trace 下,每个 Span 也都有它的唯一标识 SpanId,而 Span 是存在父子关系的。还是以讲过的例子为例子,在 A->B->C->D 的情况下,在整个调用链中,正常情况下会产生 3 个 Span,分别是 Span1(A->B)、Span2(B->C)、Span3(C->D),这时 Span3 的父 Span 就是 Span2,而 Span2 的父 Span 就是 Span1。 + +RPC 在整合分布式链路跟踪需要做的最核心的两件事就是“埋点”和“传递”。 + +我们前面说是因为各子应用、子服务间复杂的依赖关系,所以通过日志难定位问题。那我们就想办法通过日志定位到是哪个子应用的子服务出现问题就行了。 + +其实,在 RPC 框架打印的异常信息中,是包括定位异常所需要的异常信息的,比如是哪类异常引起的问题(如序列化问题或网络超时问题),是调用端还是服务端出现的异常,调用端与服务端的 IP 是什么,以及服务接口与服务分组都是什么等等。具体如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200719082205.png) + +## 优雅启停 + +### 【中级】如何实现 RPC 优雅关闭? + +:::details 要点 + +当服务提供方要上线的时候,一般是通过部署系统完成实例重启。在这个过程中,服务提供方的团队并不会事先告诉调用方他们需要操作哪些机器,从而让调用方去事先切走流量。而对调用方来说,它也无法预测到服务提供方要对哪些机器重启上线,因此负载均衡就有可能把要正在重启的机器选出来,这样就会导致把请求发送到正在重启中的机器里面,从而导致调用方不能拿到正确的响应结果。 + +![img](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/RPC%E5%AE%9E%E6%88%98%E4%B8%8E%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/assets/a745ea3875594e399b297b41092bd326.jpg) + +在服务重启的时候,对于调用方来说,这时候可能会存在以下几种情况: + +- 调用方发请求前,目标服务已经下线。对于调用方来说,跟目标节点的连接会断开,这时候调用方可以立马感知到,并且在其健康列表里面会把这个节点挪掉,自然也就不会被负载均衡选中。 +- 调用方发请求的时候,目标服务正在关闭,但调用方并不知道它正在关闭,而且两者之间的连接也没断开,所以这个节点还会存在健康列表里面,因此该节点就有一定概率会被负载均衡选中。 + +当出现第二种情况的时候,调用方业务会受损,如何避免这种问题呢。当服务提供方关闭前,是不是可以先通知注册中心进行下线,然后通过注册中心告诉调用方进行节点摘除? + +![img](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/RPC%E5%AE%9E%E6%88%98%E4%B8%8E%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/assets/80de863eb7d142caaadfa7cbbbb1d55f.jpg) + +如上图所示,整个关闭过程中依赖了两次 RPC 调用,一次是服务提供方通知注册中心下线操作,一次是注册中心通知服务调用方下线节点操作。注册中心通知服务调用方都是异步的。服务发现只保证最终一致性,并不保证实时性,所以注册中心在收到服务提供方下线的时候,并不能成功保证把这次要下线的节点推送到所有的调用方。所以这么来看,通过服务发现并不能做到应用无损关闭。 + +可以这么处理:当服务提供方正在关闭,如果这之后还收到了新的业务请求,服务提供方直接返回一个特定的异常给调用方(比如 ShutdownException)。这个异常就是告诉调用方“我已经收到这个请求了,但是我正在关闭,并没有处理这个请求”,然后调用方收到这个异常响应后,RPC 框架把这个节点从健康列表挪出,并把请求自动重试到其他节点,因为这个请求是没有被服务提供方处理过,所以可以安全地重试到其他节点,这样就可以实现对业务无损。 + +如何捕获到关闭事件呢?可以通过捕获操作系统的进程信号来获取,在 Java 语言里面,对应的是 Runtime.addShutdownHook 方法,可以注册关闭的钩子。在 RPC 启动的时候,我们提前注册关闭钩子,并在里面添加了两个处理程序,一个负责开启关闭标识,一个负责安全关闭服务对象,服务对象在关闭的时候会通知调用方下线节点。同时需要在我们调用链里面加上挡板处理器,当新的请求来的时候,会判断关闭标识,如果正在关闭,则抛出特定异常。 + +关闭过程中已经在处理的请求会不会受到影响呢?如果进程结束过快会造成这些请求还没有来得及应答,同时调用方会也会抛出异常。为了尽可能地完成正在处理的请求,首先我们要把这些请求识别出来。可以在服务对象加上引用计数器,每开始处理请求之前加一,完成请求处理减一,通过该计数器我们就可以快速判断是否有正在处理的请求。服务对象在关闭过程中,会拒绝新的请求,同时根据引用计数器等待正在处理的请求全部结束之后才会真正关闭。但考虑到有些业务请求可能处理时间长,或者存在被挂住的情况,为了避免一直等待造成应用无法正常退出,我们可以在整个 ShutdownHook 里面,加上超时时间控制,当超过了指定时间没有结束,则强制退出应用。超时时间我建议可以设定成 10s,基本可以确保请求都处理完了。 + +![img](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/RPC%E5%AE%9E%E6%88%98%E4%B8%8E%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86/assets/6f6233c36f5c4e5a89e2206696f21832.jpg) + +::: + +### 【中级】如何实现 RPC 优雅启动? + +运行了一段时间后的应用,执行速度会比刚启动的应用更快。这是因为在Java里面,在运行过程中,JVM虚拟机会把高频的代码编译成机器码,被加载过的类也会被缓存到JVM缓存中,再次使用的时候不会触发临时加载,这样就使得“热点”代码的执行不用每次都通过解释,从而提升执行速度。 + +但是这些“临时数据”,都在应用重启后就消失了。重启后的这些“红利”没有了之后,如果让刚启动的应用就承担像停机前一样的流量,这会使应用在启动之初就处于高负载状态,从而导致调用方过来的请求可能出现大面积超时,进而对线上业务产生损害行为。 + +#### 启动预热 + +启动预热,就是让刚启动的服务提供方应用不承担全部的流量,而是让它被调用的次数随着时间的移动慢慢增加,最终让流量缓和地增加到跟已经运行一段时间后的水平一样。如何做到这点呢? + +首先,对于调用方来说,我们要知道服务提供方启动的时间。一种是服务提供方在启动的时候,把自己启动的时间告诉注册中心;另外一种就是注册中心收到的服务提供方的请求注册时间。因为整个预热过程的时间是一个粗略值,即使机器之间的日期时间存在1分钟的误差也不影响,并且在真实环境中机器都会默认开启NTP时间同步功能,来保证所有机器时间的一致性。 + +不管你是选择哪个时间,最终的结果就是,调用方通过服务发现,除了可以拿到IP列表,还可以拿到对应的启动时间。接着,可以利用加权负载均衡算法来分发流量。现在,需要让这个权重变为动态的,并且是随着时间的推移慢慢增加到服务提供方设定的固定值。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220630194822.png) + +通过这个小逻辑的改动,我们就可以保证当服务提供方运行时长小于预热时间时,对服务提供方进行降权,减少被负载均衡选择的概率,避免让应用在启动之初就处于高负载状态,从而实现服务提供方在启动后有一个预热的过程。 + +#### 延迟暴露 + +服务提供方应用在没有启动完成的时候,调用方的请求就过来了,而调用方请求过来的原因是,服务提供方应用在启动过程中把解析到的 RPC 服务注册到了注册中心,这就导致在后续加载没有完成的情况下服务提供方的地址就被服务调用方感知到了。 + +为了解决这个问题,需要在应用启动加载、解析 Bean 的时候,如果遇到了 RPC 服务的 Bean,只先把这个 Bean 注册到 Spring-BeanFactory 里面去,而并不把这个 Bean 对应的接口注册到注册中心,只有等应用启动完成后,才把接口注册到注册中心用于服务发现,从而实现让服务调用方延迟获取到服务提供方地址。 + +具体如何实现呢? + +我们可以在服务提供方应用启动后,接口注册到注册中心前,预留一个 Hook 过程,让用户可以实现可扩展的 Hook 逻辑。用户可以在 Hook 里面模拟调用逻辑,从而使 JVM 指令能够预热起来,并且用户也可以在 Hook 里面事先预加载一些资源,只有等所有的资源都加载完成后,最后才把接口注册到注册中心。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220630194919.png) + +::: + +## 流量回放 + +## 架构 + +### 【高级】如何设计一个 RPC 框架? + +:::details 要点 + +设计一个 RPC 框架,可以自下而上梳理一下所需要的能力: + +- 通信传输模块:RPC 本质上就是一个远程调用,那肯定就需要通过网络来传输数据。 +- 协议模块:传输的数据如何定义,就需要通过协议和序列化方式来确定。此外,为了减少传输数据的大小,可以加入压缩功能。 +- 代理模块:为了屏蔽用户的感知,让用户更聚焦于自身业务,需要引入动态代理来托管远程调用。 + +以上,是一个 RPC 框架的基础能力,使用于 P2P 场景。 + +但是,如果面对集群模式,以上能力就不够了。同一个服务可能有多个提供者。消费者选择调用哪个提供者?消费者怎么找到提供者的访问地址?请求提供者失败了如何处理?这些都依赖于服务治理的能力。 + +服务治理,需要很多个模块的能力:服务发现、负载均衡、路由、容错、配置挂历等。 + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/RPC%e5%ae%9e%e6%88%98%e4%b8%8e%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86/assets/db030426b9b84d3e9794aea11c751469.jpg) + +具备了这些能力就万事大吉了吗?RPC 框架很难一开始就面面俱到,但作为基础能力,在实际应用中,难免会有定制化的要求。这就要求 RPC 框架具备良好的扩展性。 + +通常来说,**框架软件可以通过 SPI 技术来实现微内核+插件架构**。根据依赖倒置原则,框架应该先将每个功能点都抽象成接口,并提供默认实现。然后,利用 SPI 机制,可以动态地为某个接口寻找服务实现。 + +加上了插件功能之后,我们的RPC框架就包含了两大核心体系——核心功能体系与插件体系,如下图所示: + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/RPC%e5%ae%9e%e6%88%98%e4%b8%8e%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86/assets/8e566b59c5bf4e08bbed5e11c319e674.jpg) + +::: + +### 【高级】如何实现 RPC 异步调用? + +:::details 要点 + +一次 RPC 调用的本质就是调用端向服务端发送一条请求消息,服务端收到消息后进行处理,处理之后响应给调用端一条响应消息,调用端收到响应消息之后再进行处理,最后将最终的返回值返回给动态代理。 + +**对于 RPC 框架,无论是同步调用还是异步调用,调用端的内部实现都是异步的**。 + +调用端发送的每条消息都一个唯一的消息标识,实际上调用端向服务端发送请求消息之前会先创建一个 Future,并会存储这个消息标识与这个 Future 的映射,动态代理所获得的返回值最终就是从这个 Future 中获取的;当收到服务端响应的消息时,调用端会根据响应消息的唯一标识,通过之前存储的映射找到对应的 Future,将结果注入给那个 Future,再进行一系列的处理逻辑,最后动态代理从 Future 中获得到正确的返回值。 + +所谓的同步调用,不过是 RPC 框架在调用端的处理逻辑中主动执行了这个 Future 的 get 方法,让动态代理等待返回值;而异步调用则是 RPC 框架没有主动执行这个 Future 的 get 方法,用户可以从请求上下文中得到这个 Future,自己决定什么时候执行这个 Future 的 get 方法。 + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/RPC%e5%ae%9e%e6%88%98%e4%b8%8e%e6%a0%b8%e5%bf%83%e5%8e%9f%e7%90%86/assets/000a51dca2194170bcb3210cfe4b507d.jpg) + +如何做到 RPC 调用全异步? + +实现 RPC 调用全异步的方法是让 RPC 框架支持 `CompletableFuture`。`CompletableFuture` 是 Java8 原生支持的。如果 RPC 框架能够支持 `CompletableFuture`,现在发布一个 RPC 服务,服务接口定义的返回值是 `CompletableFuture` 对象,整个调用过程会分为这样几步: + +- 服务调用方发起 RPC 调用,直接拿到返回值 `CompletableFuture` 对象,之后就不需要任何额外的与 RPC 框架相关的操作了,直接就可以进行异步处理; +- 在服务端的业务逻辑中创建一个返回值 `CompletableFuture` 对象,之后服务端真正的业务逻辑完全可以在一个线程池中异步处理,业务逻辑完成之后再调用这个 `CompletableFuture` 对象的 `complete` 方法,完成异步通知; +- 调用端在收到服务端发送过来的响应之后,RPC 框架再自动地调用调用端拿到的那个返回值 `CompletableFuture` 对象的 `complete` 方法,这样一次异步调用就完成了。 + +通过对 `CompletableFuture` 的支持,RPC 框架可以真正地做到在调用端与服务端之间完全异步,同时提升了调用端与服务端的两端的单机吞吐量,并且 `CompletableFuture` 是 Java8 原生支持,业务逻辑中没有任何代码入侵性。 + +::: + +### 【高级】Dubbo 中的时间轮机制是如何设计的? + +:::details 要点 + +#### JDK 中定时任务的实现 + +在很多开源框架中,都需要定时任务的管理功能,例如 ZooKeeper、Netty、Quartz、Kafka 以及 Linux 操作系统。 + +定时器的本质是设计一种数据结构,能够存储和调度任务集合,而且 deadline 越近的任务拥有更高的优先级。那么定时器如何知道一个任务是否到期了呢?定时器需要通过轮询的方式来实现,每隔一个时间片去检查任务是否到期。 + +所以定时器的内部结构一般需要一个任务队列和一个异步轮询线程,并且能够提供三种基本操作: + +- Schedule 新增任务至任务集合; +- Cancel 取消某个任务; +- Run 执行到期的任务。 + +JDK 原生提供了三种常用的定时器实现方式,分别为 `Timer`、`DelayedQueue` 和 `ScheduledThreadPoolExecutor`。 + +JDK 内置的三种实现定时器的方式,实现思路都非常相似,都离不开**任务**、**任务管理**、**任务调度**三个角色。三种定时器新增和取消任务的时间复杂度都是 `O(logn)`,面对海量任务插入和删除的场景,这三种定时器都会遇到比较严重的性能瓶颈。 + +**对于性能要求较高的场景,一般都会采用时间轮算法来实现定时器**。时间轮(Timing Wheel)是 George Varghese 和 Tony Lauck 在 1996 年的论文 [Hashed and Hierarchical Timing Wheels: data structures to efficiently implement a timer facility](https://www.cse.wustl.edu/~cdgill/courses/cs6874/TimingWheels.ppt) 实现的,它在 Linux 内核中使用广泛,是 Linux 内核定时器的实现方法和基础之一。 + +#### 时间轮的基本原理 + +**时间轮是一种高效的、批量管理定时任务的调度模型**。时间轮可以理解为一种环形结构,像钟表一样被分为多个 slot 槽位。每个 slot 代表一个时间段,每个 slot 中可以存放多个任务,使用的是链表结构保存该时间段到期的所有任务。时间轮通过一个时针随着时间一个个 slot 转动,并执行 slot 中的所有到期任务。 + +![图片 22.png](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/Netty%20%E6%A0%B8%E5%BF%83%E5%8E%9F%E7%90%86%E5%89%96%E6%9E%90%E4%B8%8E%20RPC%20%E5%AE%9E%E8%B7%B5-%E5%AE%8C/assets/CgpVE1_okKiAGl0gAAMLshtTq-M933.png) + +任务是如何添加到时间轮当中的呢?可以根据任务的到期时间进行取模,然后将任务分布到不同的 slot 中。如上图所示,时间轮被划分为 8 个 slot,每个 slot 代表 1s,当前时针指向 2。假如现在需要调度一个 3s 后执行的任务,应该加入 `2+3=5` 的 slot 中;如果需要调度一个 12s 以后的任务,需要等待时针完整走完一圈 round 零 4 个 slot,需要放入第 `(2+12)%8=6` 个 slot。 + +那么当时针走到第 6 个 slot 时,怎么区分每个任务是否需要立即执行,还是需要等待下一圈,甚至更久时间之后执行呢?所以我们需要把 round 信息保存在任务中。例如图中第 6 个 slot 的链表中包含 3 个任务,第一个任务 round=0,需要立即执行;第二个任务 round=1,需要等待 `1*8=8s` 后执行;第三个任务 round=2,需要等待 `2*8=8s` 后执行。所以当时针转动到对应 slot 时,只执行 round=0 的任务,slot 中其余任务的 round 应当减 1,等待下一个 round 之后执行。 + +上面介绍了时间轮算法的基本理论,可以看出时间轮有点类似 HashMap,如果多个任务如果对应同一个 slot,处理冲突的方法采用的是拉链法。在任务数量比较多的场景下,适当增加时间轮的 slot 数量,可以减少时针转动时遍历的任务个数。 + +时间轮定时器最大的优势就是,任务的新增和取消都是 `O(1)` 时间复杂度,而且只需要一个线程就可以驱动时间轮进行工作。 + +::: + +### 【中级】RPC 如何实现那泛化调用? + +:::details 要点 + +在一些特定场景下,需要在没有接口的情况下进行 RPC 调用。例如: + +场景一:搭建一个统一的测试平台,可以让各个业务方在测试平台中通过输入接口、分组名、方法名以及参数值,在线测试自己发布的 RPC 服务。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200719095518.png) + +场景二:搭建一个轻量级的服务网关,可以让各个业务方用 HTTP 的方式,通过服务网关调用其它服务。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200719095704.png) + +为了解决这些场景的问题,可以使用泛化调用。 + +就是 RPC 框架提供统一的泛化调用接口(GenericService),调用端在创建 GenericService 代理时指定真正需要调用的接口的接口名以及分组名,通过调用 GenericService 代理的 \$invoke 方法将服务端所需要的所有信息,包括接口名、业务分组名、方法名以及参数信息等封装成请求消息,发送给服务端,实现在没有接口的情况下进行 RPC 调用的功能。 + +```java +class GenericService { +Object $invoke(String methodName, String[] paramTypes, Object[] params); +CompletableFuture $asyncInvoke(String methodName, String[] paramTypes +} +``` + +而通过泛化调用的方式发起调用,由于调用端没有服务端提供方提供的接口 API,不能正常地进行序列化与反序列化,我们可以为泛化调用提供专属的序列化插件,来解决实际问题。 + +::: + +## 参考资料 + +- [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/01.\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/01.\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" deleted file mode 100644 index fbb307c1b5..0000000000 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/01.\346\266\210\346\201\257\351\230\237\345\210\227\351\235\242\350\257\225.md" +++ /dev/null @@ -1,378 +0,0 @@ ---- -title: 消息队列面试 -date: 2022-02-17 22:34:30 -order: 01 -categories: - - 分布式 - - 分布式通信 - - MQ - - MQ综合 -tags: - - Java - - 中间件 - - MQ - - 面试 -permalink: /pages/baf673/ ---- - -# 消息队列面试夺命连环问 - -## 为什么使用消息队列? - -### 解耦 - -看这么个场景。A 系统发送数据到 BCD 三个系统,通过接口调用发送。如果 E 系统也要这个数据呢?那如果 C 系统现在不需要了呢?A 系统负责人几乎崩溃...... - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-1.png) - -在这个场景中,A 系统跟其它各种乱七八糟的系统严重耦合,A 系统产生一条比较关键的数据,很多系统都需要 A 系统将这个数据发送过来。A 系统要时时刻刻考虑 BCDE 四个系统如果挂了该咋办?要不要重发,要不要把消息存起来?头发都白了啊! - -如果使用 MQ,A 系统产生一条数据,发送到 MQ 里面去,哪个系统需要数据自己去 MQ 里面消费。如果新系统需要数据,直接从 MQ 里消费即可;如果某个系统不需要这条数据了,就取消对 MQ 消息的消费即可。这样下来,A 系统压根儿不需要去考虑要给谁发送数据,不需要维护这个代码,也不需要考虑人家是否调用成功、失败超时等情况。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-2.png) - -**总结**:通过一个 MQ,Pub/Sub 发布订阅消息这么一个模型,A 系统就跟其它系统彻底解耦了。 - -**面试技巧**:你需要去考虑一下你负责的系统中是否有类似的场景,就是一个系统或者一个模块,调用了多个系统或者模块,互相之间的调用很复杂,维护起来很麻烦。但是其实这个调用是不需要直接同步调用接口的,如果用 MQ 给它异步化解耦,也是可以的,你就需要去考虑在你的项目里,是不是可以运用这个 MQ 去进行系统的解耦。在简历中体现出来这块东西,用 MQ 作解耦。 - -### 异步 - -再来看一个场景,A 系统接收一个请求,需要在自己本地写库,还需要在 BCD 三个系统写库,自己本地写库要 3ms,BCD 三个系统分别写库要 300ms、450ms、200ms。最终请求总延时是 3 + 300 + 450 + 200 = 953ms,接近 1s,用户感觉搞个什么东西,慢死了慢死了。用户通过浏览器发起请求,等待个 1s,这几乎是不可接受的。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-3.png) - -一般互联网类的企业,对于用户直接的操作,一般要求是每个请求都必须在 200 ms 以内完成,对用户几乎是无感知的。 - -如果**使用 MQ**,那么 A 系统连续发送 3 条消息到 MQ 队列中,假如耗时 5ms,A 系统从接受一个请求到返回响应给用户,总时长是 3 + 5 = 8ms,对于用户而言,其实感觉上就是点个按钮,8ms 以后就直接返回了,爽!网站做得真好,真快! - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-4.png) - -### 削峰 - -每天 0:00 到 12:00,A 系统风平浪静,每秒并发请求数量就 50 个。结果每次一到 12:00 \~ 13:00 ,每秒并发请求数量突然会暴增到 5k+ 条。但是系统是直接基于 MySQL 的,大量的请求涌入 MySQL,每秒钟对 MySQL 执行约 5k 条 SQL。 - -一般的 MySQL,扛到每秒 2k 个请求就差不多了,如果每秒请求到 5k 的话,可能就直接把 MySQL 给打死了,导致系统崩溃,用户也就没法再使用系统了。 - -但是高峰期一过,到了下午的时候,就成了低峰期,可能也就 1w 的用户同时在网站上操作,每秒中的请求数量可能也就 50 个请求,对整个系统几乎没有任何的压力。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-5.png) - -如果使用 MQ,每秒 5k 个请求写入 MQ,A 系统每秒钟最多处理 2k 个请求,因为 MySQL 每秒钟最多处理 2k 个。A 系统从 MQ 中慢慢拉取请求,每秒钟就拉取 2k 个请求,不要超过自己每秒能处理的最大请求数量就 ok,这样下来,哪怕是高峰期的时候,A 系统也绝对不会挂掉。而 MQ 每秒钟 5k 个请求进来,就 2k 个请求出去,结果就导致在中午高峰期(1 个小时),可能有几十万甚至几百万的请求积压在 MQ 中。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-6.png) - -这个短暂的高峰期积压是 ok 的,因为高峰期过了之后,每秒钟就 50 个请求进 MQ,但是 A 系统依然会按照每秒 2k 个请求的速度在处理。所以说,只要高峰期一过,A 系统就会快速将积压的消息给解决掉。 - -## 消息队列有什么优缺点 - -优点上面已经说了,就是**在特殊场景下有其对应的好处**,**解耦**、**异步**、**削峰**。 - -缺点有以下几个: - -- 系统可用性降低 - 系统引入的外部依赖越多,越容易挂掉。本来你就是 A 系统调用 BCD 三个系统的接口就好了,人 ABCD 四个系统好好的,没啥问题,你偏加个 MQ 进来,万一 MQ 挂了咋整,MQ 一挂,整套系统崩溃的,你不就完了?如何保证消息队列的高可用,可以[点击这里查看](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md)。 -- 系统复杂度提高 - 硬生生加个 MQ 进来,你怎么[保证消息没有重复消费](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-that-messages-are-not-repeatedly-consumed.md)?怎么[处理消息丢失的情况](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/how-to-ensure-the-reliable-transmission-of-messages.md)?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已。 -- 一致性问题 - A 系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是 BCD 三个系统那里,BD 两个系统写库成功了,结果 C 系统写库失败了,咋整?你这数据就不一致了。 - -所以消息队列实际是一种非常复杂的架构,你引入它有很多好处,但是也得针对它带来的坏处做各种额外的技术方案和架构来规避掉,做好之后,你会发现,妈呀,系统复杂度提升了一个数量级,也许是复杂了 10 倍。但是关键时刻,用,还是得用的。 - -## Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点? - -| 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | -| ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | -| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | -| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | -| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | -| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ | -| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 | - -综上,各种对比之后,有如下建议: - -- 一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了; -- 后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高; -- 不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 [Apache](https://github.com/apache/rocketmq),但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。 -- 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;**大型公司**,基础架构研发实力较强,用 RocketMQ 是很好的选择。 -- 如果是**大数据领域**的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 - -## 如何保证消息队列的高可用? - -### RabbitMQ 的高可用性 - -RabbitMQ 是比较有代表性的,因为是**基于主从**(非分布式)做高可用性的,我们就以 RabbitMQ 为例子讲解第一种 MQ 的高可用性怎么实现。 - -RabbitMQ 有三种模式:单机模式、普通集群模式、镜像集群模式。 - -#### 单机模式 - -单机模式,就是 Demo 级别的,一般就是你本地启动了玩玩儿的 😄,没人生产用单机模式。 - -#### 普通集群模式(无高可用性) - -普通集群模式,意思就是在多台机器上启动多个 RabbitMQ 实例,每个机器启动一个。你**创建的 queue,只会放在一个 RabbitMQ 实例上**,但是每个实例都同步 queue 的元数据(元数据可以认为是 queue 的一些配置信息,通过元数据,可以找到 queue 所在实例)。你消费的时候,实际上如果连接到了另外一个实例,那么那个实例会从 queue 所在实例上拉取数据过来。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-7.png) - -这种方式确实很麻烦,也不怎么好,**没做到所谓的分布式**,就是个普通集群。因为这导致你要么消费者每次随机连接一个实例然后拉取数据,要么固定连接那个 queue 所在实例消费数据,前者有**数据拉取的开销**,后者导致**单实例性能瓶颈**。 - -而且如果那个放 queue 的实例宕机了,会导致接下来其他实例就无法从那个实例拉取,如果你**开启了消息持久化**,让 RabbitMQ 落地存储消息的话,**消息不一定会丢**,得等这个实例恢复了,然后才可以继续从这个 queue 拉取数据。 - -所以这个事儿就比较尴尬了,这就**没有什么所谓的高可用性**,**这方案主要是提高吞吐量的**,就是说让集群中多个节点来服务某个 queue 的读写操作。 - -#### 镜像集群模式(高可用性) - -这种模式,才是所谓的 RabbitMQ 的高可用模式。跟普通集群模式不一样的是,在镜像集群模式下,你创建的 queue,无论元数据还是 queue 里的消息都会**存在于多个实例上**,就是说,每个 RabbitMQ 节点都有这个 queue 的一个**完整镜像**,包含 queue 的全部数据的意思。然后每次你写消息到 queue 的时候,都会自动把**消息同步**到多个实例的 queue 上。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-8.png) - -那么**如何开启这个镜像集群模式**呢?其实很简单,RabbitMQ 有很好的管理控制台,就是在后台新增一个策略,这个策略是**镜像集群模式的策略**,指定的时候是可以要求数据同步到所有节点的,也可以要求同步到指定数量的节点,再次创建 queue 的时候,应用这个策略,就会自动将数据同步到其他的节点上去了。 - -这样的话,好处在于,你任何一个机器宕机了,没事儿,其它机器(节点)还包含了这个 queue 的完整数据,别的 consumer 都可以到其它节点上去消费数据。坏处在于,第一,这个性能开销也太大了吧,消息需要同步到所有机器上,导致网络带宽压力和消耗很重!第二,这么玩儿,不是分布式的,就**没有扩展性可言**了,如果某个 queue 负载很重,你加机器,新增的机器也包含了这个 queue 的所有数据,并**没有办法线性扩展**你的 queue。你想,如果这个 queue 的数据量很大,大到这个机器上的容量无法容纳了,此时该怎么办呢? - -### Kafka 的高可用性 - -Kafka 一个最基本的架构认识:由多个 broker 组成,每个 broker 是一个节点;你创建一个 topic,这个 topic 可以划分为多个 partition,每个 partition 可以存在于不同的 broker 上,每个 partition 就放一部分数据。 - -这就是**天然的分布式消息队列**,就是说一个 topic 的数据,是**分散放在多个机器上的,每个机器就放一部分数据**。 - -实际上 RabbmitMQ 之类的,并不是分布式消息队列,它就是传统的消息队列,只不过提供了一些集群、HA(High Availability, 高可用性) 的机制而已,因为无论怎么玩儿,RabbitMQ 一个 queue 的数据都是放在一个节点里的,镜像集群下,也是每个节点都放这个 queue 的完整数据。 - -Kafka 0.8 以前,是没有 HA 机制的,就是任何一个 broker 宕机了,那个 broker 上的 partition 就废了,没法写也没法读,没有什么高可用性可言。 - -比如说,我们假设创建了一个 topic,指定其 partition 数量是 3 个,分别在三台机器上。但是,如果第二台机器宕机了,会导致这个 topic 的 1/3 的数据就丢了,因此这个是做不到高可用的。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-before.png) - -Kafka 0.8 以后,提供了 HA 机制,就是 replica(复制品) 副本机制。每个 partition 的数据都会同步到其它机器上,形成自己的多个 replica 副本。所有 replica 会选举一个 leader 出来,那么生产和消费都跟这个 leader 打交道,然后其他 replica 就是 follower。写的时候,leader 会负责把数据同步到所有 follower 上去,读的时候就直接读 leader 上的数据即可。只能读写 leader?很简单,**要是你可以随意读写每个 follower,那么就要 care 数据一致性的问题**,系统复杂度太高,很容易出问题。Kafka 会均匀地将一个 partition 的所有 replica 分布在不同的机器上,这样才可以提高容错性。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-after.png) - -这么搞,就有所谓的**高可用性**了,因为如果某个 broker 宕机了,没事儿,那个 broker 上面的 partition 在其他机器上都有副本的。如果这个宕机的 broker 上面有某个 partition 的 leader,那么此时会从 follower 中**重新选举**一个新的 leader 出来,大家继续读写那个新的 leader 即可。这就有所谓的高可用性了。 - -**写数据**的时候,生产者就写 leader,然后 leader 将数据落地写本地磁盘,接着其他 follower 自己主动从 leader 来 pull 数据。一旦所有 follower 同步好数据了,就会发送 ack 给 leader,leader 收到所有 follower 的 ack 之后,就会返回写成功的消息给生产者。(当然,这只是其中一种模式,还可以适当调整这个行为) - -**消费**的时候,只会从 leader 去读,但是只有当一个消息已经被所有 follower 都同步成功返回 ack 的时候,这个消息才会被消费者读到。 - -看到这里,相信你大致明白了 Kafka 是如何保证高可用机制的了,对吧?不至于一无所知,现场还能给面试官画画图。要是遇上面试官确实是 Kafka 高手,深挖了问,那你只能说不好意思,太深入的你没研究过。 - -## 如何保证消息不被重复消费?(如何保证消息消费的幂等性) - -首先,比如 RabbitMQ、RocketMQ、Kafka,都有可能会出现消息重复消费的问题,正常。因为这问题通常不是 MQ 自己保证的,是由我们开发来保证的。挑一个 Kafka 来举个例子,说说怎么重复消费吧。 - -Kafka 实际上有个 offset 的概念,就是每个消息写进去,都有一个 offset,代表消息的序号,然后 consumer 消费了数据之后,**每隔一段时间**(定时定期),会把自己消费过的消息的 offset 提交一下,表示“我已经消费过了,下次我要是重启啥的,你就让我继续从上次消费到的 offset 来继续消费吧”。 - -但是凡事总有意外,比如我们之前生产经常遇到的,就是你有时候重启系统,看你怎么重启了,如果碰到点着急的,直接 kill 进程了,再重启。这会导致 consumer 有些消息处理了,但是没来得及提交 offset,尴尬了。重启之后,少数消息会再次消费一次。 - -举个栗子。 - -有这么个场景。数据 1/2/3 依次进入 kafka,kafka 会给这三条数据每条分配一个 offset,代表这条数据的序号,我们就假设分配的 offset 依次是 152/153/154。消费者从 kafka 去消费的时候,也是按照这个顺序去消费。假如当消费者消费了 `offset=153` 的这条数据,刚准备去提交 offset 到 zookeeper,此时消费者进程被重启了。那么此时消费过的数据 1/2 的 offset 并没有提交,kafka 也就不知道你已经消费了 `offset=153` 这条数据。那么重启之后,消费者会找 kafka 说,嘿,哥儿们,你给我接着把上次我消费到的那个地方后面的数据继续给我传递过来。由于之前的 offset 没有提交成功,那么数据 1/2 会再次传过来,如果此时消费者没有去重的话,那么就会导致重复消费。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-10.png) - -如果消费者干的事儿是拿一条数据就往数据库里写一条,会导致说,你可能就把数据 1/2 在数据库里插入了 2 次,那么数据就错啦。 - -其实重复消费不可怕,可怕的是你没考虑到重复消费之后,**怎么保证幂等性**。 - -举个例子吧。假设你有个系统,消费一条消息就往数据库里插入一条数据,要是你一个消息重复两次,你不就插入了两条,这数据不就错了?但是你要是消费到第二次的时候,自己判断一下是否已经消费过了,若是就直接扔了,这样不就保留了一条数据,从而保证了数据的正确性。 - -一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。 - -幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,**不能出错**。 - -所以第二个问题来了,怎么保证消息队列消费的幂等性? - -其实还是得结合业务来思考,我这里给几个思路: - -- 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。 -- 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。 -- 比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写 Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。 -- 比如基于数据库的唯一键来保证重复数据不会重复插入多条。因为有唯一键约束了,重复数据插入只会报错,不会导致数据库中出现脏数据。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/mq-11.png) - -当然,如何保证 MQ 的消费是幂等性的,需要结合具体的业务来看。 - -## 如何保证消息的可靠性传输?(如何处理消息丢失的问题) - -数据的丢失问题,可能出现在生产者、MQ、消费者中,咱们从 RabbitMQ 和 Kafka 分别来分析一下吧。 - -### RabbitMQ - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-message-lose.png) - -#### 生产者弄丢了数据 - -生产者将数据发送到 RabbitMQ 的时候,可能数据就在半路给搞丢了,因为网络问题啥的,都有可能。 - -此时可以选择用 RabbitMQ 提供的事务功能,就是生产者**发送数据之前**开启 RabbitMQ 事务`channel.txSelect`,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务`channel.txRollback`,然后重试发送消息;如果收到了消息,那么可以提交事务`channel.txCommit`。 - -```java -// 开启事务 -channel.txSelect -try { - // 这里发送消息 -} catch (Exception e) { - channel.txRollback - - // 这里再次重发这条消息 -} - -// 提交事务 -channel.txCommit -``` - -但是问题是,RabbitMQ 事务机制(同步)一搞,基本上**吞吐量会下来,因为太耗性能**。 - -所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 `confirm` 模式,在生产者那里设置开启 `confirm`模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 `ack` 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 `nack` 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。 - -事务机制和 `confirm` 机制最大的不同在于,**事务机制是同步的**,你提交一个事务之后会**阻塞**在那儿,但是 `confirm` 机制是**异步**的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。 - -所以一般在生产者这块**避免数据丢失**,都是用 `confirm` 机制的。 - -#### RabbitMQ 弄丢了数据 - -就是 RabbitMQ 自己弄丢了数据,这个你必须**开启 RabbitMQ 的持久化**,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,**恢复之后会自动读取之前存储的数据**,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,**可能导致少量数据丢失**,但是这个概率较小。 - -设置持久化有**两个步骤**: - -- 创建 queue 的时候将其设置为持久化 - 这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。 -- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2 - 就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。 - -必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。 - -注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。 - -所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack`,你也是可以自己重发的。 - -#### 消费端弄丢了数据 - -RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费到,还没处理,结果进程挂了**,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。 - -这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-message-lose-solution.png) - -### Kafka - -#### 消费端弄丢了数据 - -唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边**自动提交了 offset**,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。 - -这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要**关闭自动提交** offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是**可能会有重复消费**,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。 - -生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动提交 offset。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。 - -#### Kafka 弄丢了数据 - -这块比较常见的一个场景,就是 Kafka 某个 broker 宕机,然后重新选举 partition 的 leader。大家想想,要是此时其他的 follower 刚好还有些数据没有同步,结果此时 leader 挂了,然后选举某个 follower 成 leader 之后,不就少了一些数据?这就丢了一些数据啊。 - -生产环境也遇到过,我们也是,之前 Kafka 的 leader 机器宕机了,将 follower 切换为 leader 之后,就会发现说这个数据就丢了。 - -所以此时一般是要求起码设置如下 4 个参数: - -- 给 topic 设置 `replication.factor` 参数:这个值必须大于 1,要求每个 partition 必须有至少 2 个副本。 -- 在 Kafka 服务端设置 `min.insync.replicas` 参数:这个值必须大于 1,这个是要求一个 leader 至少感知到有至少一个 follower 还跟自己保持联系,没掉队,这样才能确保 leader 挂了还有一个 follower 吧。 -- 在 producer 端设置 `acks=all`:这个是要求每条数据,必须是**写入所有 replica 之后,才能认为是写成功了**。 -- 在 producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思):这个是**要求一旦写入失败,就无限重试**,卡在这里了。 - -我们生产环境就是按照上述要求配置的,这样配置之后,至少在 Kafka broker 端就可以保证在 leader 所在 broker 发生故障,进行 leader 切换时,数据不会丢失。 - -#### 生产者会不会弄丢数据? - -如果按照上述的思路设置了 `acks=all`,一定不会丢,要求是,你的 leader 接收到消息,所有的 follower 都同步到了消息之后,才认为本次写成功了。如果没满足这个条件,生产者会自动不断的重试,重试无限次。 - -## 如何保证消息的顺序性? - -我举个例子,我们以前做过一个 mysql `binlog` 同步的系统,压力还是非常大的,日同步数据要达到上亿,就是说数据从一个 mysql 库原封不动地同步到另一个 mysql 库里面去(mysql -> mysql)。常见的一点在于说比如大数据 team,就需要同步一个 mysql 库过来,对公司的业务系统的数据做各种复杂的操作。 - -你在 mysql 里增删改一条数据,对应出来了增删改 3 条 `binlog` 日志,接着这三条 `binlog` 发送到 MQ 里面,再消费出来依次执行,起码得保证人家是按照顺序来的吧?不然本来是:增加、修改、删除;你楞是换了顺序给执行成删除、修改、增加,不全错了么。 - -本来这个数据同步过来,应该最后这个数据被删除了;结果你搞错了这个顺序,最后这个数据保留下来了,数据同步就出错了。 - -先看看顺序会错乱的俩场景: - -- **RabbitMQ**:一个 queue,多个 consumer。比如,生产者向 RabbitMQ 里发送了三条数据,顺序依次是 data1/data2/data3,压入的是 RabbitMQ 的一个内存队列。有三个消费者分别从 MQ 中消费这三条数据中的一条,结果消费者 2 先执行完操作,把 data2 存入数据库,然后是 data1/data3。这不明显乱了。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-order-01.png) - -- **Kafka**:比如说我们建了一个 topic,有三个 partition。生产者在写的时候,其实可以指定一个 key,比如说我们指定了某个订单 id 作为 key,那么这个订单相关的数据,一定会被分发到同一个 partition 中去,而且这个 partition 中的数据一定是有顺序的。 - 消费者从 partition 中取出来数据的时候,也一定是有顺序的。到这里,顺序还是 ok 的,没有错乱。接着,我们在消费者里可能会搞**多个线程来并发处理消息**。因为如果消费者是单线程消费处理,而处理比较耗时的话,比如处理一条消息耗时几十 ms,那么 1 秒钟只能处理几十条消息,这吞吐量太低了。而多个线程并发跑的话,顺序可能就乱掉了。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-order-01.png) - -### 解决方案 - -#### RabbitMQ - -![img](https://github.com/doocs/advanced-java/blob/master/images/rabbitmq-order-02.png) - -#### Kafka - -- 一个 topic,一个 partition,一个 consumer,内部单线程消费,单线程吞吐量太低,一般不会用这个。 -- 写 N 个内存 queue,具有相同 key 的数据都到同一个内存 queue;然后对于 N 个线程,每个线程分别消费一个内存 queue 即可,这样就能保证顺序性。 - -![img](https://github.com/doocs/advanced-java/blob/master/images/kafka-order-02.png) - -## 如何解决消息队列的延时以及过期失效问题?消息队列满了以后该怎么处理?有几百万消息持续积压几小时,说说怎么解决? - -你看这问法,其实本质针对的场景,都是说,可能你的消费端出了问题,不消费了;或者消费的速度极其慢。接着就坑爹了,可能你的消息队列集群的磁盘都快写满了,都没人消费,这个时候怎么办?或者是这整个就积压了几个小时,你这个时候怎么办?或者是你积压的时间太长了,导致比如 RabbitMQ 设置了消息过期时间后就没了怎么办? - -所以就这事儿,其实线上挺常见的,一般不出,一出就是大 case。一般常见于,举个例子,消费端每次消费之后要写 mysql,结果 mysql 挂了,消费端 hang 那儿了,不动了;或者是消费端出了个什么岔子,导致消费速度极其慢。 - -### 面试题剖析 - -关于这个事儿,我们一个一个来梳理吧,先假设一个场景,我们现在消费端出故障了,然后大量消息在 mq 里积压,现在出事故了,慌了。 - -### 大量消息在 mq 里积压了几个小时了还没解决 - -几千万条数据在 MQ 里积压了七八个小时,从下午 4 点多,积压到了晚上 11 点多。这个是我们真实遇到过的一个场景,确实是线上故障了,这个时候要不然就是修复 consumer 的问题,让它恢复消费速度,然后傻傻的等待几个小时消费完毕。这个肯定不能在面试的时候说吧。 - -一个消费者一秒是 1000 条,一秒 3 个消费者是 3000 条,一分钟就是 18 万条。所以如果你积压了几百万到上千万的数据,即使消费者恢复了,也需要大概 1 小时的时间才能恢复过来。 - -一般这个时候,只能临时紧急扩容了,具体操作步骤和思路如下: - -- 先修复 consumer 的问题,确保其恢复消费速度,然后将现有 consumer 都停掉。 -- 新建一个 topic,partition 是原来的 10 倍,临时建立好原先 10 倍的 queue 数量。 -- 然后写一个临时的分发数据的 consumer 程序,这个程序部署上去消费积压的数据,**消费之后不做耗时的处理**,直接均匀轮询写入临时建立好的 10 倍数量的 queue。 -- 接着临时征用 10 倍的机器来部署 consumer,每一批 consumer 消费一个临时 queue 的数据。这种做法相当于是临时将 queue 资源和 consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 -- 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。 - -### mq 中的消息过期失效了 - -假设你用的是 RabbitMQ,RabbtiMQ 是可以设置过期时间的,也就是 TTL。如果消息在 queue 中积压超过一定的时间就会被 RabbitMQ 给清理掉,这个数据就没了。那这就是第二个坑了。这就不是说数据会大量积压在 mq 里,而是**大量的数据会直接搞丢**。 - -这个情况下,就不是说要增加 consumer 消费积压的消息,因为实际上没啥积压,而是丢了大量的消息。我们可以采取一个方案,就是**批量重导**,这个我们之前线上也有类似的场景干过。就是大量积压的时候,我们当时就直接丢弃数据了,然后等过了高峰期以后,比如大家一起喝咖啡熬夜到晚上 12 点以后,用户都睡觉了。这个时候我们就开始写程序,将丢失的那批数据,写个临时程序,一点一点的查出来,然后重新灌入 mq 里面去,把白天丢的数据给他补回来。也只能是这样了。 - -假设 1 万个订单积压在 mq 里面,没有处理,其中 1000 个订单都丢了,你只能手动写程序把那 1000 个订单给查出来,手动发到 mq 里去再补一次。 - -### mq 都快写满了 - -如果消息积压在 mq 里,你很长时间都没有处理掉,此时导致 mq 都快写满了,咋办?这个还有别的办法吗?没有,谁让你第一个方案执行的太慢了,你临时写程序,接入数据来消费,**消费一个丢弃一个,都不要了**,快速消费掉所有的消息。然后走第二个方案,到了晚上再补数据吧。 - -## 如果让你写一个消息队列,该如何进行架构设计啊?说一下你的思路。 - -### 面试官心理分析 - -其实聊到这个问题,一般面试官要考察两块: - -- 你有没有对某一个消息队列做过较为深入的原理的了解,或者从整体了解把握住一个消息队列的架构原理。 -- 看看你的设计能力,给你一个常见的系统,就是消息队列系统,看看你能不能从全局把握一下整体架构设计,给出一些关键点出来。 - -说实话,问类似问题的时候,大部分人基本都会蒙,因为平时从来没有思考过类似的问题,大多数人就是平时埋头用,从来不去思考背后的一些东西。类似的问题,比如,如果让你来设计一个 Spring 框架你会怎么做?如果让你来设计一个 Dubbo 框架你会怎么做?如果让你来设计一个 MyBatis 框架你会怎么做? - -### 面试题剖析 - -其实回答这类问题,说白了,不求你看过那技术的源码,起码你要大概知道那个技术的基本原理、核心组成部分、基本架构构成,然后参照一些开源的技术把一个系统设计出来的思路说一下就好。 - -比如说这个消息队列系统,我们从以下几个角度来考虑一下: - -- 首先这个 mq 得支持可伸缩性吧,就是需要的时候快速扩容,就可以增加吞吐量和容量,那怎么搞?设计个分布式的系统呗,参照一下 kafka 的设计理念,broker -> topic -> partition,每个 partition 放一个机器,就存一部分数据。如果现在资源不够了,简单啊,给 topic 增加 partition,然后做数据迁移,增加机器,不就可以存放更多数据,提供更高的吞吐量了? -- 其次你得考虑一下这个 mq 的数据要不要落地磁盘吧?那肯定要了,落磁盘才能保证别进程挂了数据就丢了。那落磁盘的时候怎么落啊?顺序写,这样就没有磁盘随机读写的寻址开销,磁盘顺序读写的性能是很高的,这就是 kafka 的思路。 -- 其次你考虑一下你的 mq 的可用性啊?这个事儿,具体参考之前可用性那个环节讲解的 kafka 的高可用保障机制。多副本 -> leader & follower -> broker 挂了重新选举 leader 即可对外服务。 -- 能不能支持数据 0 丢失啊?可以的,参考我们之前说的那个 kafka 数据零丢失方案。 - -mq 肯定是很复杂的,面试官问你这个问题,其实是个开放题,他就是看看你有没有从架构角度整体构思和设计的思维以及能力。确实这个问题可以刷掉一大批人,因为大部分人平时不思考这些东西。 \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/02.\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/MQ\351\235\242\350\257\225.md" similarity index 78% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/02.\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/MQ\351\235\242\350\257\225.md" index 7333f6e57b..8bfa56818e 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/02.\346\266\210\346\201\257\351\230\237\345\210\227\345\237\272\346\234\254\345\216\237\347\220\206.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/00.MQ\347\273\274\345\220\210/MQ\351\235\242\350\257\225.md" @@ -1,7 +1,6 @@ --- -title: 消息队列基本原理 -date: 2019-07-05 15:11:00 -order: 02 +title: MQ 面试 +date: 2022-02-17 22:34:30 categories: - 分布式 - 分布式通信 @@ -11,10 +10,11 @@ tags: - Java - 中间件 - MQ -permalink: /pages/1fd240/ + - 面试 +permalink: /pages/296e650c/ --- -# 消息队列基本原理 +# MQ 面试 > 消息队列(Message Queue,简称 MQ)技术是**应用间交换信息**的一种技术。 > @@ -24,9 +24,11 @@ permalink: /pages/1fd240/ > > 注意:_为了简便,下文中除了文章标题,一律使用 MQ 简称_。 -## MQ 的简介 +## MQ 简介 -### 什么是 MQ +### 【基础】什么是 MQ? + +:::details 要点 消息队列(Message Queue,简称 MQ)技术是应用间交换信息的一种技术。 @@ -38,67 +40,75 @@ MQ 的数据可驻留在内存或磁盘上,直到它们被应用程序读取 目前主流的 MQ 有:Kafka、RabbitMQ、RocketMQ、ActiveMQ。 -### MQ 通信模型 +::: -MQ 通信模型大致有以下类型: +### 【基础】为什么需要 MQ? -- **点对点** - 点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 -- **多点广播** - MQ 适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点 (Destination List)。可以使用一条 MQ 指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ 不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ 将消息的一个复制版本和该系统上接收者的名单发送到目标 MQ 系统。目标 MQ 系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 -- **发布/订阅 (Publish/Subscribe)** - 发布/订阅模式使消息的分发可以突破目的队列地理位置的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅模式使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。 -- **集群 (Cluster)** - 为了简化点对点通讯模式中的系统配置,MQ 提供 Cluster(集群) 的解决方案。集群类似于一个域 (Domain),集群内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用集群 (Cluster) 通道与其它成员通讯,从而大大简化了系统配置。此外,集群中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。 +:::details 要点 -## MQ 的应用 +MQ 的常见应用场景有: -### 异步处理 +- **异步处理** +- **系统解耦** +- **流量削峰** +- **系统间通信** +- **传输缓冲** +- **最终一致性** -> MQ 可以将系统间的处理流程异步化,减少等待响应的时间,从而提高整体并发吞吐量。 -> -> 一般,MQ 异步处理应用于非核心流程,例如:短信/邮件通知、数据推送、上报数据到监控中心/日志中心等。 +#### 异步处理 -假设这样一个场景,用户向系统 A 发起请求,系统 A 处理计算只需要 10 ms,然后通知系统 BCD 写库,系统 BCD 写库耗时分别为:100ms、200ms、300ms。最终总耗时为: 10+100ms+200ms+300ms=610ms。此外,加上请求和响应的网络传输时间,从用户角度看,可能要等待将近 1s 才能得到结果。 +MQ 可以将系统间的处理流程异步化,减少等待响应的时间,从而提高整体并发吞吐量。一般,MQ 异步处理应用于非核心流程,例如:短信/邮件通知、数据推送、上报数据到监控中心、日志中心等。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/theory/mq/mq_3.png) +假设这样一个场景,用户向系统 A 发起请求,系统 A 处理计算只需要 `10ms`,然后通知系统 BCD 写库,系统 BCD 写库耗时分别为:`100ms`、`200ms`、`300ms`。最终总耗时为: `10ms+100ms+200ms+300ms=610ms`。此外,加上请求和响应的网络传输时间,从用户角度看,可能要等待将近 `1s` 才能得到结果。 -如果使用 MQ,系统 A 接到请求后,耗时 10ms 处理计算,然后向系统 BCD 连续发送消息,假设耗时 5ms。那么 这一过程的总耗时为 3ms + 5ms = 8ms,这相比于 610 ms,大大缩短了响应时间。至于系统 BCD 的写库操作,只要自行消费 MQ 后处理即可,用户无需关注。 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502021707928.png) -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/theory/mq/mq_4.png) +如果使用 MQ,系统 A 接到请求后,耗时 `10ms` 处理计算,然后向系统 BCD 连续发送消息,假设耗时 `5ms`。那么 这一过程的总耗时为 `3ms + 5ms = 8ms`,这相比于 `610 ms`,大大缩短了响应时间。至于系统 BCD 的写库操作,只要自行消费 MQ 后处理即可,用户无需关注。 -### 系统解耦 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502021707517.png) -> 通过 MQ,可以消除系统间的强耦合。它的好处在于: -> -> - 消息的消费者系统可以随意增加,无需修改生产者系统的代码。 -> - 生产者系统、消费者系统彼此不会影响对方的流程。 -> - 如果生产者系统宕机,消费者系统收不到消息,就不会有下一步的动作。 -> - 如果消费者系统宕机,生产者系统让然可以正常发送消息,不影响流程。 +#### 系统解耦 + +通过 MQ,可以消除系统间的强耦合。它的好处在于: + +- 消息的消费者系统可以随意增加,无需修改生产者系统的代码。 +- 生产者系统、消费者系统彼此不会影响对方的流程。 + - 如果生产者系统宕机,消费者系统收不到消息,就不会有下一步的动作。 + - 如果消费者系统宕机,生产者系统让然可以正常发送消息,不影响流程。 不同系统如果要建立通信,传统的做法是:调用接口。 如果需要和新的系统建立通信或删除已建立的通信,都需要修改代码,这种方案显然耦合度很高。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/theory/mq/mq_1.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502021719775.png) 如果使用 MQ,系统间的通信只需要通过发布/订阅(Pub/Sub)模型即可,彼此没有直接联系,也就不需要相互感知,从而达到 **解耦**。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/theory/mq/mq_2.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502021719470.png) -### 流量削峰 +#### 流量削峰 -> 当 **上下游系统** 处理能力存在差距的时候,利用 MQ 做一个 “**漏斗**” 模型,进行 **流控**。把 MQ 当成可靠的 **消息暂存地**,进行一定程度的 **消息堆积**;在下游有能力处理的时候,再发送消息。 -> -> MQ 的流量削峰常用于高并发场景(例如:秒杀、团抢等业务场景),它是缓解瞬时暴增流量的核心手段之一。 -> -> 如果没有 MQ,两个系统之间通过 **协商**、**滑动窗口**、**限流**/**降级**/**熔断** 等复杂的方案也能实现 **流控**。但 **系统复杂性** 指数级增长,势必在上游或者下游做存储,并且要处理 **定时**、**拥塞** 等一系列问题。而且每当有 **处理能力有差距** 的时候,都需要 **单独** 开发一套逻辑来维护这套逻辑。 +当 **上下游系统** 处理能力存在差距的时候,利用 MQ 做一个 “**漏斗**” 模型,进行 **流控**。把 MQ 当成可靠的 **消息缓冲池**,进行一定程度的 **消息堆积**;在下游有能力处理的时候,再发送消息。 + +MQ 的流量削峰常用于高并发场景(例如:秒杀、团抢等业务场景),它是缓解瞬时暴增流量的核心手段之一。 + +如果没有 MQ,两个系统之间通过 **协商**、**滑动窗口**、**限流**/**降级**/**熔断** 等复杂的方案也能实现 **流控**。但 **系统复杂性** 指数级增长,势必在上游或者下游做存储,并且要处理 **定时**、**拥塞** 等一系列问题。而且每当有 **处理能力有差距** 的时候,都需要 **单独** 开发一套逻辑来维护这套逻辑。 假设某个系统读写数据库的稳定性能为每秒处理 1000 条数据。平常情况下,远远达不到这么大的处理量。假设,因为因为做活动,系统的瞬时请求量剧增,达到每秒 10000 个并发请求,数据库根本承受不了,可能直接就把数据库给整崩溃了,这样系统服务就不可用了。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/theory/mq/mq_5.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502021738906.png) 如果使用 MQ,每秒写入 10000 条请求,但是系统 A 每秒只从 MQ 中消费 1000 条请求,然后写入数据库。这样,就不会超过数据库的承受能力,而是把请求积压在 MQ 中。只要高峰期一过,系统 A 就会很快把积压的消息给处理掉。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/design/theory/mq/mq_6.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502021739806.png) -### 传输缓冲 +#### 系统间通信 + +消息队列一般都内置了 **高效的通信机制**,因此也可以用于单纯的 **消息通讯**,比如实现 **点对点消息队列** 或者 **聊天室** 等。 + +**生产者/消费者** 模式,只需要关心消息是否 **送达队列**,至于谁希望订阅和需要消费,是 **下游** 的事情,无疑极大地减少了开发和联调的工作量。 + +#### 传输缓冲 (1)MQ 常被用于做海量数据的传输缓冲。 @@ -110,7 +120,7 @@ MQ 通信模型大致有以下类型: 例如,Kafka 几乎已经是流计算的数据采集端的标准组件。而流计算通过实时数据处理能力,提供了更为快捷的聚合计算能力,被大量应用于链路监控、实时监控、实时数仓、实时大屏、风控、推荐等应用领域。 -### 最终一致性 +#### 最终一致性 **最终一致性** 不是 **消息队列** 的必备特性,但确实可以依靠 **消息队列** 来做 **最终一致性** 的事情。 @@ -119,19 +129,54 @@ MQ 通信模型大致有以下类型: > 像 `Kafka` 一类的设计,在设计层面上就有 **丢消息** 的可能(比如 **定时刷盘**,如果掉电就会丢消息)。哪怕只丢千分之一的消息,业务也必须用其他的手段来保证结果正确。 -### 系统间通信 +::: -消息队列一般都内置了 **高效的通信机制**,因此也可以用于单纯的 **消息通讯**,比如实现 **点对点消息队列** 或者 **聊天室** 等。 +### 【基础】MQ 有哪些通信模型? -**生产者/消费者** 模式,只需要关心消息是否 **送达队列**,至于谁希望订阅和需要消费,是 **下游** 的事情,无疑极大地减少了开发和联调的工作量。 +:::details 要点 + +MQ 通信模型大致有以下类型: + +- **点对点** - 点对点方式是最为传统和常见的通讯方式,它支持一对一、一对多、多对多、多对一等多种配置方式,支持树状、网状等多种拓扑结构。 +- **多点广播** - MQ 适用于不同类型的应用。其中重要的,也是正在发展中的是"多点广播"应用,即能够将消息发送到多个目标站点 (Destination List)。可以使用一条 MQ 指令将单一消息发送到多个目标站点,并确保为每一站点可靠地提供信息。MQ 不仅提供了多点广播的功能,而且还拥有智能消息分发功能,在将一条消息发送到同一系统上的多个用户时,MQ 将消息的一个复制版本和该系统上接收者的名单发送到目标 MQ 系统。目标 MQ 系统在本地复制这些消息,并将它们发送到名单上的队列,从而尽可能减少网络的传输量。 +- **发布/订阅 (Publish/Subscribe)** - 发布/订阅模式使消息的分发可以突破目的队列地理位置的限制,使消息按照特定的主题甚至内容进行分发,用户或应用程序可以根据主题或内容接收到所需要的消息。发布/订阅模式使得发送者和接收者之间的耦合关系变得更为松散,发送者不必关心接收者的目的地址,而接收者也不必关心消息的发送地址,而只是根据消息的主题进行消息的收发。 +- **集群 (Cluster)** - 为了简化点对点通讯模式中的系统配置,MQ 提供 Cluster(集群) 的解决方案。集群类似于一个域 (Domain),集群内部的队列管理器之间通讯时,不需要两两之间建立消息通道,而是采用集群 (Cluster) 通道与其它成员通讯,从而大大简化了系统配置。此外,集群中的队列管理器之间能够自动进行负载均衡,当某一队列管理器出现故障时,其它队列管理器可以接管它的工作,从而大大提高系统的高可靠性。 + +::: + +### 【基础】获取 MQ 消息有哪些模式? + +:::details 要点 + +消息引擎获取消息有两种模式: + +- **push 模式** - MQ 推送数据给消费者 +- **pull 模式** - 消费者主动向 MQ 请求数据 -## MQ 的问题 +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502031317162.png) + +Kafka 消费者(Consumer)以 pull 方式从 Broker 拉取消息。相比于 push 方式,pull 方式灵活度和扩展性更好,因为消费的主动性由消费者自身控制。 + +push 模式的优缺点: + +- 缺点:由 broker 决定消息推送的速率,对于不同消费速率的 consumer 就不太好处理了。push 模式下,当 broker 推送的速率远大于 consumer 消费的速率时,consumer 恐怕就要崩溃了。 + +push 模式的优缺点: + +- 优点:consumer 可以根据自己的消费能力自主的决定消费策略 +- 缺点:如果 broker 没有可供消费的消息,将导致 consumer 不断在循环中轮询,直到新消息到达。为了避免这点,Kafka 有个参数可以让 consumer 阻塞直到新消息到达 + +::: + +### 【中级】引入 MQ 带来哪些问题? + +:::details 要点 任何技术都会有利有弊,MQ 给整体系统架构带来很多好处,但也会付出一定的代价。 MQ 主要引入了以下问题: -- **系统可用性降低**:引入了 MQ 后,通信需要基于 MQ 完成,如果 MQ 宕机,则服务不可用。因此,MQ 要保证是高可用的,详情参考:[MQ 的高可用](#MQ-的高可用) +- **系统可用性降低**:引入了 MQ 后,通信需要基于 MQ 完成,如果 MQ 宕机,则服务不可用。因此,MQ 要保证是高可用的。 - **系统复杂度提高**:使用 MQ,需要关注一些新的问题: - 如何保证消息没有 **重复消费**? - 如何处理 **消息丢失** 的问题? @@ -139,15 +184,13 @@ MQ 主要引入了以下问题: - 如何处理大量 **消息积压** 的问题? - **一致性问题**:假设系统 A 处理完直接返回成功的结果给用户,用户认为请求成功。但如果此时,系统 BCD 中只要有任意一个写库失败,那么数据就不一致了。这种情况如何处理? -下面,我们针对以上问题来一一分析。 - -### 重复消费 +::: -**如何保证消息不被重复消费** 和 **如何保证消息消费的幂等性** 是同一个问题。 +## 重复消费 -必须先明确产生重复消费的原因,才能对症下药。 +### 【中级】MQ 为什么会存在重复消费问题? -#### 重复消费问题原因 +:::details 要点 重复消费问题通常不是 MQ 来处理,而是由开发来处理的。 @@ -155,14 +198,20 @@ MQ 主要引入了以下问题: Kafka 的客户端和 Broker 都会保存 Offset。客户端消费消息后,每隔一段时间,就把已消费的 Offset 提交给 Kafka Broker,表示已消费。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210427194009.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502022054834.png) 在这个过程中,如果客户端应用消费消息后,因为宕机、重启等情况而没有提交已消费的 Offset 。当系统恢复后,会继续消费消息,由于 Offset 未提交,就会出现重复消费的问题。 -#### 重复消费解决方案 +::: + +### 【中级】如何保证消息不被重复消费? + +:::details 要点 应对重复消费问题,需要在业务层面,通过 **幂等性设计** 来解决。 +**幂等**(idempotent、idempotence)是一个数学与计算机学概念,指的是:**一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。** + MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思路有: - 如果是写关系型数据库,可以先根据主键查询,判断数据是否已存在,存在则更新,不存在则插入; @@ -171,13 +220,23 @@ MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思 在实际开发中,可以参考上面的例子,结合现实场景,设计合理的幂等性方案。 -### 消息丢失 +::: + +## 消息丢失 + +### 【高级】如何保证消息不丢失? -**如何处理消息丢失的问题** 和 **如何保证消息不被重复消费** 是同一个问题。关注点有: +:::details 要点 +要保证消息不丢失,首先要弄清楚 MQ 消息在哪些环节可能出现丢失的情况,才能对症下药。 + +实际上,MQ 消息在以下场景都可能会出现丢失: + +- 生产方丢失数据 - MQ Server 丢失数据 - 消费方丢失数据 -- 生产方丢失数据 + +下面以 Kafka 为例,讲解在传输的不同场景下如何保证消息不丢失。 #### 消费方丢失数据 @@ -185,16 +244,17 @@ MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思 解决方法就是:消费方关闭自动提交 Offset,处理完消息后**手动提交 Offset**。但这种情况下可能会出现重复消费的情形,需要自行保证幂等性。 -#### Kafka Server 丢失数据 +#### MQ Server 丢失数据 当 Kafka 某个 Broker 宕机,需要重新选举 Partition 的 Leader。若此时其他的 Follower 尚未同步 Leader 的数据,那么新选某个 Follower 为 Leader 后,就丢失了部分数据。 为此,一般要求至少设置 4 个参数: -- 给 Topic 设置 `replication.factor` 参数 - 这个值必须大于 1,要求每个 Partition 必须有至少 2 个副本。 -- 在 Kafka 服务端设置 `min.insync.replicas` 参数 - 这个值必须大于 1,这是要求一个 Leader 需要和至少一个 Follower 保持通信,这样才能确保 Leader 挂了还有替补。 -- 在 Producer 端设置 `acks=all` - 这意味着:要求每条数据,必须是**写入所有 replica 之后,才能认为写入成功了**。 -- 在 Producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思) - 这意味着**要求一旦写入失败,就无限重试**,卡在这里了。 +- **冗余** - 通过副本机制保证冗余。 + - 给 Topic 设置 `replication.factor` 参数 - 这个值必须大于 1,要求每个 Partition 必须有至少 2 个副本。 + - 在 Kafka 服务端设置 `min.insync.replicas` 参数 - 这个值必须大于 1,这是要求一个 Leader 需要和至少一个 Follower 保持通信,这样才能确保 Leader 挂了还有替补。 +- **强一致性** - 在 Producer 端设置 `acks=all`,这意味着:要求每条数据,必须是**写入所有 replica 之后,才能认为写入成功了**。保证强一致性需要付出一定的代价,通常只有业务场景真的需要保证万无一失才会这么设置。 +- **失败重试** - 在 Producer 端设置 `retries=MAX`(很大很大很大的一个值,无限次重试的意思),这意味着**要求一旦写入失败,就无限重试**,卡在这里了。 #### 生产方丢失数据 @@ -202,8 +262,14 @@ MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思 要求是,你的 Leader 接收到消息,所有的 Follower 都同步到了消息之后,才认为本生产消息成功了。如果未满足这个条件,生产者会自动不断的重试,重试无限次。 +::: + ### 消息的顺序性 +### 【高级】如何保证消息的顺序性? + +:::details 要点 + 要保证 MQ 的顺序性,势必要付出一定的代价,所以实施方案前,要先明确业务场景是不是有必要保证消息的顺序性。只有那些明确对消息处理顺序有要求的业务场景才值得去保证消息顺序性。 方案一 @@ -217,9 +283,15 @@ MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思 - 消费方维护 N 个缓存队列,具有相同 ID 的数据都写入同一个队列中; - 创建 N 个线程,每个线程只负责从指定的一个队列中取数据。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210427194215.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502022152450.png) -### 消息积压 +::: + +## 消息积压 + +### 【高级】如何解决消息积压? + +:::details 要点 假设一个 MQ 消费者可以一秒处理 1000 条消息,三个 MQ 消费者可以一秒处理 3000 条消息,那么一分钟的处理量是 18 万条。如果 MQ 中积压了几百万到上千万的数据,即使消费者恢复了,也需要大概很长的时间才能恢复过来。 @@ -231,11 +303,15 @@ MQ 重复消费不可怕,可怕的是没有应对机制,可以借鉴的思 - 接着临时征用 10 倍的机器来部署 Consumer ,每一批 Consumer 消费一个临时 Queue 的数据。这种做法相当于是临时将 Queue 资源和 Consumer 资源扩大 10 倍,以正常的 10 倍速度来消费数据。 - 等快速消费完积压数据之后,**得恢复原先部署的架构**,**重新**用原先的 consumer 机器来消费消息。 -## MQ 的高可用 +::: -不同 MQ 实现高可用的原理各不相同。因为 Kafka 比较具有代表性,所以这里以 Kafka 为例。 +## MQ 高可用 + +### 【高级】如何保证 MQ 的高可用? -### Kafka 的高可用 +:::details 要点 + +不同 MQ 实现高可用的原理各不相同。因为 Kafka 比较具有代表性,所以这里以 Kafka 为例。 #### Kafka 的核心概念 @@ -257,9 +333,7 @@ Kafka 是如何实现高可用的呢? Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 Partition 都不能用了,这自然不是高可用的。 -为了实现高可用,Kafka 引入了复制功能。 - -简单来说,就是副本机制( Replicate )。 +为了实现高可用,Kafka 引入了复制功能,简单来说,就是副本机制( Replicate ): **每个 Partition 都有一个 Leader,零个或多个 Follower**。Leader 和 Follower 都是 Broker,每个 Broker 都会成为某些分区的 Leader 和某些分区的 Follower,因此集群的负载是平衡的。 @@ -284,15 +358,19 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 如果 Leader 宕机了,会从 Follower 中**重新选举**一个新的 Leader。 -## 主流 MQ +::: -### ActiveMQ +## MQ 架构 -`ActiveMQ` 是由 `Apache` 出品,`ActiveMQ` 是一个完全支持`JMS1.1` 和 `J2EE 1.4` 规范的 `JMS Provider` 实现。它非常快速,支持 **多种语言的客户端** 和 **协议**,而且可以非常容易的嵌入到企业的应用环境中,并有许多高级功能。 +### 【高级】Kafka、ActiveMQ、RabbitMQ、RocketMQ 有什么优缺点? -![img](https://user-gold-cdn.xitu.io/2018/7/8/16479c8ea7cdc2c0?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) +:::details 要点 -#### (a) 主要特性 +#### ActiveMQ + +`ActiveMQ` 是由 `Apache` 出品,`ActiveMQ` 是一个完全支持`JMS1.1` 和 `J2EE 1.4` 规范的 `JMS Provider` 实现。它非常快速,支持 **多种语言的客户端** 和 **协议**,而且可以非常容易的嵌入到企业的应用环境中,并有许多高级功能。 + +**(a) 主要特性** 1. **服从 JMS 规范**:`JMS` 规范提供了良好的标准和保证,包括:**同步** 或 **异步** 的消息分发,一次和仅一次的消息分发,**消息接收** 和 **订阅** 等等。遵从 `JMS` 规范的好处在于,不论使用什么 `JMS` 实现提供者,这些基础特性都是可用的; 2. **连接灵活性**:`ActiveMQ` 提供了广泛的 **连接协议**,支持的协议有:`HTTP/S`,`IP` **多播**,`SSL`,`TCP`,`UDP` 等等。对众多协议的支持让 `ActiveMQ` 拥有了很好的灵活性; @@ -302,14 +380,14 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 6. **代理集群**:多个 `ActiveMQ` **代理** 可以组成一个 **集群** 来提供服务; 7. **异常简单的管理**:`ActiveMQ` 是以开发者思维被设计的。所以,它并不需要专门的管理员,因为它提供了简单又使用的管理特性。有很多中方法可以 **监控** `ActiveMQ` 不同层面的数据,包括使用在 `JConsole` 或者在 `ActiveMQ` 的 `Web Console` 中使用 `JMX`。通过处理 `JMX` 的告警消息,通过使用 **命令行脚本**,甚至可以通过监控各种类型的 **日志**。 -#### (b) 部署环境 +**(b) 部署环境** `ActiveMQ` 可以运行在 `Java` 语言所支持的平台之上。使用 `ActiveMQ` 需要: - `Java JDK` - `ActiveMQ` 安装包 -#### (c) 优点 +**(c) 优点** 1. **跨平台** (`JAVA` 编写与平台无关,`ActiveMQ` 几乎可以运行在任何的 `JVM` 上); 2. 可以用 `JDBC`:可以将 **数据持久化** 到数据库。虽然使用 `JDBC` 会降低 `ActiveMQ` 的性能,但是数据库一直都是开发人员最熟悉的存储介质; @@ -319,20 +397,18 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 6. 监控完善:拥有完善的 **监控**,包括 `Web Console`,`JMX`,`Shell` 命令行,`Jolokia` 的 `RESTful API`; 7. 界面友善:提供的 `Web Console` 可以满足大部分情况,还有很多 **第三方的组件** 可以使用,比如 `hawtio`; -#### (d) 缺点 +**(d) 缺点** 1. 社区活跃度不及 `RabbitMQ` 高; 2. 根据其他用户反馈,会出莫名其妙的问题,会 **丢失消息**; 3. 目前重心放到 `activemq 6.0` 产品 `Apollo`,对 `5.x` 的维护较少; 4. 不适合用于 **上千个队列** 的应用场景; -### RabbitMQ +#### RabbitMQ `RabbitMQ` 于 `2007` 年发布,是一个在 `AMQP` (**高级消息队列协议**)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。 -![img](https://user-gold-cdn.xitu.io/2018/7/8/16479c8ece3b5d7a?imageView2/0/w/1280/h/960/format/webp/ignore-error/1) - -#### (a) 主要特性 +**(a) 主要特性** 1. **可靠性**:提供了多种技术可以让你在 **性能** 和 **可靠性** 之间进行 **权衡**。这些技术包括 **持久性机制**、**投递确认**、**发布者证实** 和 **高可用性机制**; 2. **灵活的路由**:消息在到达队列前是通过 **交换机** 进行 **路由** 的。`RabbitMQ` 为典型的路由逻辑提供了 **多种内置交换机** 类型。如果你有更复杂的路由需求,可以将这些交换机组合起来使用,你甚至可以实现自己的交换机类型,并且当做 `RabbitMQ` 的 **插件** 来使用; @@ -344,14 +420,14 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 8. **跟踪机制**:如果 **消息异常**,`RabbitMQ` 提供消息跟踪机制,使用者可以找出发生了什么; 9. **插件机制**:提供了许多 **插件**,来从多方面进行扩展,也可以编写自己的插件。 -#### (b) 部署环境 +**(b) 部署环境** `RabbitMQ` 可以运行在 `Erlang` 语言所支持的平台之上,包括 `Solaris`,`BSD`,`Linux`,`MacOSX`,`TRU64`,`Windows` 等。使用 `RabbitMQ` 需要: - `ErLang` 语言包 - `RabbitMQ` 安装包 -#### (c) 优点 +**(c) 优点** 1. 由于 `Erlang` 语言的特性,消息队列性能较好,支持 **高并发**; 2. 健壮、稳定、易用、**跨平台**、支持 **多种语言**、文档齐全; @@ -359,17 +435,17 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 4. 高度可定制的 **路由**; 5. **管理界面** 较丰富,在互联网公司也有较大规模的应用,社区活跃度高。 -#### (d) 缺点 +**(d) 缺点** 1. 尽管结合 `Erlang` 语言本身的并发优势,性能较好,但是不利于做 **二次开发和维护**; 2. 实现了 **代理架构**,意味着消息在发送到客户端之前可以在 **中央节点** 上排队。此特性使得 `RabbitMQ` 易于使用和部署,但是使得其 **运行速度较慢**,因为中央节点 **增加了延迟**,**消息封装后** 也比较大; 3. 需要学习 **比较复杂** 的 **接口和协议**,学习和维护成本较高。 -### RocketMQ +#### RocketMQ `RocketMQ` 出自 **阿里** 的开源产品,用 `Java` 语言实现,在设计时参考了 `Kafka`,并做出了自己的一些改进,**消息可靠性上** 比 `Kafka` 更好。`RocketMQ` 在阿里内部  被广泛应用在 **订单**,**交易**,**充值**,**流计算**,**消息推送**,**日志流式处理**,`binglog` **分发** 等场景。 -#### (a) 主要特性 +**(a) 主要特性** 1. 基于 **队列模型**:具有 **高性能**、**高可靠**、**高实时**、**分布式** 等特点; 2. `Producer`、`Consumer`、**队列** 都支持 **分布式**; @@ -381,7 +457,7 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 8. 亿级 **消息堆积** 能力; 9. 较少的外部依赖。 -#### (b) 部署环境 +**(b) 部署环境** `RocketMQ` 可以运行在 `Java` 语言所支持的平台之上。使用 `RocketMQ` 需要: @@ -389,7 +465,7 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 - 安装 `git`、`Maven` - `RocketMQ` 安装包 -#### (c) 优点 +**(c) 优点** 1. **单机** 支持 `1` 万以上 **持久化队列**; 2. `RocketMQ` 的所有消息都是 **持久化的**,先写入系统 `PAGECACHE`,然后 **刷盘**,可以保证 **内存** 与 **磁盘** 都有一份数据,而 **访问** 时,直接 **从内存读取**。 @@ -399,18 +475,18 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 6. 各个环节 **分布式扩展设计**,支持 **主从** 和 **高可用**; 7. 开发度较活跃,版本更新很快。 -#### (d) 缺点 +**(d) 缺点** 1. 支持的 **客户端语言** 不多,目前是 `Java` 及 `C++`,其中 `C++` 还不成熟; 2. `RocketMQ` 社区关注度及成熟度也不及前两者; 3. 没有 `Web` 管理界面,提供了一个 `CLI` (命令行界面) 管理工具带来 **查询**、**管理** 和 **诊断各种问题**; 4. 没有在 `MQ` 核心里实现 `JMS` 等接口; -### Kafka +#### Kafka `Apache Kafka` 是一个 **分布式消息发布订阅** 系统。它最初由 `LinkedIn` 公司基于独特的设计实现为一个 **分布式的日志提交系统** (`a distributed commit log`),之后成为 `Apache` 项目的一部分。`Kafka` **性能高效**、**可扩展良好** 并且 **可持久化**。它的 **分区特性**,**可复制** 和 **可容错** 都是其不错的特性。 -#### (a) 主要特性 +**(a) 主要特性** 1. **快速持久化**:可以在 `O(1)` 的系统开销下进行 **消息持久化**; 2. **高吞吐**:在一台普通的服务器上既可以达到 `10W/s` 的 **吞吐速率**; @@ -422,14 +498,14 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 8. **无需停机** 即可扩展机器; 9. **其他特性**:丰富的 **消息拉取模型**、高效 **订阅者水平扩展**、实时的 **消息订阅**、亿级的 **消息堆积能力**、定期删除机制; -#### (b) 部署环境 +**(b) 部署环境** 使用 `Kafka` 需要: - `Java JDK` - `Kafka` 安装包 -#### (c) 优点 +**(c) 优点** 1. **客户端语言丰富**:支持 `Java`、`.Net`、`PHP`、`Ruby`、`Python`、`Go` 等多种语言; 2. **高性能**:单机写入 `TPS` 约在 `100` 万条/秒,消息大小 `10` 个字节; @@ -439,7 +515,7 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 6. 有优秀的第三方 `Kafka Web` 管理界面 `Kafka-Manager`; 7. 在 **日志领域** 比较成熟,被多家公司和多个开源项目使用。 -#### (d) 缺点 +**(d) 缺点** 1. `Kafka` 单机超过 `64` 个 **队列/分区** 时,`Load` 时会发生明显的飙高现象。**队列** 越多,**负载** 越高,发送消息 **响应时间变长**; 2. 使用 **短轮询方式**,**实时性** 取决于 **轮询间隔时间**; @@ -447,19 +523,11 @@ Kafka 在 0.8 以前的版本中,如果一个 Broker 宕机了,其上面的 4. 支持 **消息顺序**,但是 **一台代理宕机** 后,就会产生 **消息乱序**; 5. 社区更新较慢。 -### MQ 的技术选型 - -MQ 的技术选型一般要考虑以下几点: - -- **是否开源**:这决定了能否商用,所以最为重要。 -- **社区活跃度越高越好**:高社区活跃度,一般保证了低 Bug 率,因为大部分 Bug,已经有人遇到并解决了。 -- **技术生态适配性**:客户端对各种编程语言的支持。比如:如果使用 MQ 的都是 Java 应用,那么 ActiveMQ、RabbitMQ、RocketMQ、Kafka 都可以。如果需要支持其他语言,那么 RMQ 比较合适,因为它支持的编程语言比较丰富。如果 MQ 是应用于大数据或流式计算,那么 Kafka 几乎是标配。如果是应用于在线业务系统,那么 Kafka 就不合适了,可以考虑 RabbitMQ、 RocketMQ 很合适。 -- **高可用性**:应用于线上的准入标准。 -- **性能**:具备足够好的性能,能满足绝大多数场景的性能要求。 +#### 技术选型 | 特性 | ActiveMQ | RabbitMQ | RocketMQ | Kafka | | ------------------------ | ------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------- | -| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行流式计算、日志采集等场景 | +| 单机吞吐量 | 万级,比 RocketMQ、Kafka 低一个数量级 | 同 ActiveMQ | 10 万级,支撑高吞吐 | 10 万级,高吞吐,一般配合大数据类的系统来进行实时数据计算、日志采集等场景 | | topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 | | 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 | | 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 | @@ -468,10 +536,17 @@ MQ 的技术选型一般要考虑以下几点: 综上,各种对比之后,有如下建议: -- 业务系统场景,建议使用 RocketMQ、RabbitMQ。如果所有应用都是 Java,优选 RocketMQ,因为 RocketMQ 本身就是 Java 开发的,所以最适配。如果业务中有多种编程语言的应用,建议选择 RabbitMQ。 -- 大数据和流式计算领域,或是作为日志缓冲,强烈建议选择 Kafka,业界标准,久经考验。 +- 一般的业务系统要引入 MQ,最早大家都用 ActiveMQ,但是现在确实大家用的不多了,没经过大规模吞吐量场景的验证,社区也不是很活跃,所以大家还是算了吧,我个人不推荐用这个了; +- 后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高; +- 不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 [Apache](https://github.com/apache/rocketmq),但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。 +- 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;**大型公司**,基础架构研发实力较强,用 RocketMQ 是很好的选择。 +- 如果是**大数据领域**的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 + +::: + +### 【高级】什么是 JMS? -## JMS +:::details 要点 提到 MQ,就顺便提一下 JMS 。 @@ -479,16 +554,16 @@ MQ 的技术选型一般要考虑以下几点: 在 EJB 架构中,有消息 bean 可以无缝的与 JMS 消息服务集成。在 J2EE 架构模式中,有消息服务者模式,用于实现消息与应用直接的解耦。 -### 消息模型 +#### JMS 消息模型 在 JMS 标准中,有两种消息模型: - P2P(Point to Point) - Pub/Sub(Publish/Subscribe) -#### P2P 模式 +##### P2P 模式 -
    +![](http://upload-images.jianshu.io/upload_images/3101171-2adc66e2367cd2c2.png) P2P 模式包含三个角色:MQ(Queue),发送者(Sender),接收者(Receiver)。每个消息都被发送到一个特定的队列,接收者从队列中获取消息。队列保留着消息,直到他们被消费或超时。 @@ -500,9 +575,9 @@ P2P 的特点 如果希望发送的每个消息都会被成功处理的话,那么需要 P2P 模式。 -#### Pub/sub 模式 +##### Pub/sub 模式 -
    +![](http://upload-images.jianshu.io/upload_images/3101171-12afe9581da889ea.png) 包含三个角色主题(Topic),发布者(Publisher),订阅者(Subscriber) 。多个发布者将消息发送到 Topic,系统将这些消息传递给多个订阅者。 @@ -516,7 +591,7 @@ Pub/Sub 的特点 如果希望发送的消息可以不被做任何处理、或者只被一个消息者处理、或者可以被多个消费者处理的话,那么可以采用 Pub/Sub 模型。 -### 消息消费 +#### JMS 消息消费 在 JMS 中,消息的产生和消费都是异步的。对于消费来说,JMS 的消息者可以通过两种方式来消费消息。 @@ -527,37 +602,7 @@ Pub/Sub 的特点 JNDI 在 JMS 中起到查找和访问发送目标或消息来源的作用。 -### JMS 编程模型 - -#### ConnectionFactory - -创建 Connection 对象的工厂,针对两种不同的 jms 消息模型,分别有 QueueConnectionFactory 和 TopicConnectionFactory 两种。可以通过 JNDI 来查找 ConnectionFactory 对象。 - -#### Destination - -Destination 的意思是消息生产者的消息发送目标或者说消息消费者的消息来源。对于消息生产者来说,它的 Destination 是某个队列(Queue)或某个主题(Topic);对于消息消费者来说,它的 Destination 也是某个队列或主题(即消息来源)。 - -所以,Destination 实际上就是两种类型的对象:Queue、Topic。可以通过 JNDI 来查找 Destination。 - -#### Connection - -Connection 表示在客户端和 JMS 系统之间建立的链接(对 TCP/IP socket 的包装)。Connection 可以产生一个或多个 Session。跟 ConnectionFactory 一样,Connection 也有两种类型:QueueConnection 和 TopicConnection。 - -#### Session - -Session 是操作消息的接口。可以通过 session 创建生产者、消费者、消息等。Session 提供了事务的功能。当需要使用 session 发送/接收多个消息时,可以将这些发送/接收动作放到一个事务中。同样,也分 QueueSession 和 TopicSession。 - -#### 消息的生产者 - -消息生产者由 Session 创建,并用于将消息发送到 Destination。同样,消息生产者分两种类型:QueueSender 和 TopicPublisher。可以调用消息生产者的方法(send 或 publish 方法)发送消息。 - -#### 消息消费者 - -消息消费者由 Session 创建,用于接收被发送到 Destination 的消息。两种类型:QueueReceiver 和 TopicSubscriber。可分别通过 session 的 createReceiver(Queue)或 createSubscriber(Topic)来创建。当然,也可以 session 的 creatDurableSubscriber 方法来创建持久化的订阅者。 - -#### MessageListener - -消息监听器。如果注册了消息监听器,一旦消息到达,将自动调用监听器的 onMessage 方法。EJB 中的 MDB(Message-Driven Bean)就是一种 MessageListener。 +::: ## 参考资料 @@ -566,4 +611,4 @@ Session 是操作消息的接口。可以通过 session 创建生产者、消费 - [分布式开放 MQ(RocketMQ)的原理与实践](https://www.jianshu.com/p/453c6e7ff81c) - [阿里 RocketMQ 优势对比](https://juejin.im/entry/5a0abfb5f265da43062a4a91) - [advanced-java 之 MQ](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/mq-interview.md) -- [浅谈消息队列及常见的消息中间件](https://juejin.im/post/6844903635046924296) \ No newline at end of file +- [浅谈消息队列及常见的消息中间件](https://juejin.im/post/6844903635046924296) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/05.Kafka\345\217\257\351\235\240\344\274\240\350\276\223.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\217\257\351\235\240\344\274\240\350\276\223.md" similarity index 97% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/05.Kafka\345\217\257\351\235\240\344\274\240\350\276\223.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\217\257\351\235\240\344\274\240\350\276\223.md" index 89ed302433..1fd1c5e596 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/05.Kafka\345\217\257\351\235\240\344\274\240\350\276\223.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\217\257\351\235\240\344\274\240\350\276\223.md" @@ -1,7 +1,6 @@ --- title: Kafka 可靠传输 date: 2021-04-14 15:05:34 -order: 05 categories: - 分布式 - 分布式通信 @@ -10,7 +9,7 @@ categories: tags: - MQ - Kafka -permalink: /pages/481bdd/ +permalink: /pages/4c187841/ --- # Kafka 可靠传输 @@ -21,7 +20,7 @@ permalink: /pages/481bdd/ 一条消息从生产到消费,可以划分三个阶段: -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210422042613.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070727544.png) - **生产阶段**:Producer 创建消息,并通过网络发送给 Broker。 - **存储阶段**:Broker 收到消息并存储,如果是集群,还要同步副本给其他 Broker。 @@ -77,7 +76,7 @@ Broker 有 3 个配置参数会影响 Kafka 消息存储的可靠性。 在生产消息阶段,消息队列一般通过请求确认机制,来保证消息的可靠传递,Kafka 也不例外。 -[Kafka 生产者](02.Kafka生产者.md) 中提到了,Kafka 有三种发送方式:同步、异步、异步回调。 +[Kafka 生产](Kafka生产) 中提到了,Kafka 有三种发送方式:同步、异步、异步回调。 同步方式能保证消息不丢失,但性能太差;异步方式发送消息,通常会立即返回,但消息可能丢失。 @@ -258,8 +257,8 @@ Kafka 每一个 Partition 只能隶属于消费者群组中的一个 Consumer, - [Kafka Github](https://github.com/apache/kafka) - [Kafka 官方文档](https://kafka.apache.org/documentation/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) - **教程** - [消息队列高手课](https://time.geekbang.org/column/intro/100032301) - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) \ No newline at end of file + - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/06.Kafka\345\255\230\345\202\250.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\255\230\345\202\250.md" similarity index 96% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/06.Kafka\345\255\230\345\202\250.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\255\230\345\202\250.md" index c73ca85bae..8132e92d18 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/06.Kafka\345\255\230\345\202\250.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\255\230\345\202\250.md" @@ -1,7 +1,6 @@ --- title: Kafka 存储 date: 2021-04-29 08:17:17 -order: 06 categories: - 分布式 - 分布式通信 @@ -10,7 +9,7 @@ categories: tags: - MQ - Kafka -permalink: /pages/8de948/ +permalink: /pages/4d7aaaa2/ --- # Kafka 存储 @@ -21,7 +20,7 @@ permalink: /pages/8de948/ ## 逻辑存储 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210427195053.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070720162.png) ## 持久化 @@ -56,7 +55,7 @@ Partiton 命名规则为 Topic 名称 + 有序序号,第一个 Partiton 序号 ### Log Segment -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210615200304.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070721654.png) 因为在一个大文件中查找和删除消息是非常耗时且容易出错的。所以,Kafka 将每个 Partition 切割成若干个片段,即日志段(Log Segment)。**默认每个 Segment 大小不超过 1G,且只包含 7 天的数据**。如果 Segment 的消息量达到 1G,那么该 Segment 会关闭,同时打开一个新的 Segment 进行写入。 @@ -96,7 +95,7 @@ Kafka 允许消费者从任意有效的偏移量位置开始读取消息。Kafka 有了偏移量索引文件,通过它,Kafka 就能够根据指定的偏移量快速定位到消息的实际物理位置。具体的做法是,根据指定的偏移量,使用二分法查询定位出该偏移量对应的消息所在的分段索引文件和日志数据文件。然后通过二分查找法,继续查找出小于等于指定偏移量的最大偏移量,同时也得出了对应的 position(实际物理位置),根据该物理位置在分段的日志数据文件中顺序扫描查找偏移量与指定偏移量相等的消息。下面是 Kafka 中分段的日志数据文件和偏移量索引文件的对应映射关系图(其中也说明了如何按照起始偏移量来定位到日志数据文件中的具体消息)。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210615222550.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070722556.png) ## 清理 @@ -130,9 +129,9 @@ Kafka 允许消费者从任意有效的偏移量位置开始读取消息。Kafka - [Kafka Github](https://github.com/apache/kafka) - [Kafka 官方文档](https://kafka.apache.org/documentation/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) - **教程** - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - **文章** - - [Kafka 剖析(一):Kafka 背景及架构介绍](http://www.infoq.com/cn/articles/kafka-analysis-part-1) \ No newline at end of file + - [Kafka 剖析(一):Kafka 背景及架构介绍](http://www.infoq.com/cn/articles/kafka-analysis-part-1) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/01.Kafka\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\277\253\351\200\237\345\205\245\351\227\250.md" similarity index 95% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/01.Kafka\345\277\253\351\200\237\345\205\245\351\227\250.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\277\253\351\200\237\345\205\245\351\227\250.md" index 082ed56c7a..b039ece69c 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/01.Kafka\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -1,7 +1,6 @@ --- title: Kafka 快速入门 date: 2020-06-03 09:55:35 -order: 01 categories: - 分布式 - 分布式通信 @@ -10,16 +9,14 @@ categories: tags: - MQ - Kafka -permalink: /pages/a697a6/ +permalink: /pages/838a5f6a/ --- # Kafka 快速入门 -> **Apache Kafka 是一款开源的消息引擎系统,也是一个分布式流计算平台,此外,还可以作为数据存储**。 - ## Kafka 简介 -> **Apache Kafka 是一款开源的消息引擎系统,也是一个分布式流计算平台,此外,还可以作为数据存储**。 +**Apache Kafka 是一款开源的消息引擎系统,也是一个分布式流计算平台,此外,还可以作为数据存储**。 ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/mq/kafka/kafka-event-system.png) @@ -64,9 +61,9 @@ Kafka 的设计目标: - **消费者(Consumer)**:消费者是从主题订阅新消息的 Kafka 客户端。消费者通过检查消息的偏移量来区分消息是否已读。 - **消费者群组(Consumer Group)**:多个消费者共同构成的一个群组,同时消费多个分区以实现高并发。 - 每个消费者属于一个特定的消费者群组(可以为每个消费者指定消费者群组,若不指定,则属于默认的群组)。 - - 群组中,一个消费者可以消费多个分区 - - 群组中,每个分区只能被指定给一个消费 -- **再均衡(Rebalance)**:消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。分区再均衡是 Kafka 消费者端实现高可用的重要手段。 + - 群组中,一个消费者可以消费多个分区。 + - 群组中,每个分区只能被指定给一个消费者。 +- **再均衡(Rebalance)**:消费者群组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。分区再均衡是 Kafka 消费者端实现高可用的重要手段。 - **Broker** - 一个独立的 Kafka 服务器被称为 Broker。Broker 接受来自生产者的消息,为消息设置偏移量,并提交消息到磁盘保存;消费者向 Broker 请求消息,Broker 负责返回已提交的消息。 - **副本(Replica)**:Kafka 中同一条消息能够被拷贝到多个地方以提供数据冗余,这些地方就是所谓的副本。副本还分为领导者副本和追随者副本,各自有不同的角色划分。副本是在分区层级下的,即每个分区可配置多个副本实现高可用。 @@ -82,23 +79,24 @@ Kafka 主要有以下发行版本: Kafka 有以下重大版本: +- 0.7 - 只提供了最基础的消息队列功能 - 0.8 - 正式引入了副本机制 - 至少升级到 0.8.2.2 - 0.9 - 增加了基础的安全认证 / 权限功能 + - 用 Java 重写了新版本消费者 API + - 引入了 Kafka Connect 组件 - 新版本 Producer API 在这个版本中算比较稳定 - 0.10 - - 引入了 Kafka Streams + - 引入了 Kafka Streams,正式升级成分布式流处理平台 - 至少升级到 0.10.2.2 - 修复了一个可能导致 Producer 性能降低的 Bug - - 使用新版本 Consumer API - 0.11 - 提供幂等性 Producer API 以及事务 - 对 Kafka 消息格式做了重构 - 至少升级到 0.11.0.3 -- 1.0 和 2.0 - - Kafka Streams 的改进 +- 1.0 和 2.0 - Kafka Streams 的改进 ## Kafka 服务端使用入门 @@ -473,7 +471,7 @@ public void consumeMessageForIndependentConsumer(String topic){ - [Kafka Github](https://github.com/apache/kafka) - [Kafka 官方文档](https://kafka.apache.org/documentation/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) - **教程** - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/07.Kafka\346\265\201\345\274\217\345\244\204\347\220\206.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\346\265\201\345\274\217\345\244\204\347\220\206.md" similarity index 98% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/07.Kafka\346\265\201\345\274\217\345\244\204\347\220\206.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\346\265\201\345\274\217\345\244\204\347\220\206.md" index 58dc648b63..51efafdaf3 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/07.Kafka\346\265\201\345\274\217\345\244\204\347\220\206.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\346\265\201\345\274\217\345\244\204\347\220\206.md" @@ -1,7 +1,6 @@ --- title: Kafka 流式处理 date: 2020-07-24 06:52:07 -order: 07 categories: - 分布式 - 分布式通信 @@ -10,7 +9,7 @@ categories: tags: - MQ - Kafka -permalink: /pages/55f66f/ +permalink: /pages/640d44c6/ --- # Kafka 流式处理 @@ -146,9 +145,9 @@ Kafka 的消息传递层对数据进行分区以进行存储和传输。 Kafka S - [Kafka Github](https://github.com/apache/kafka) - [Kafka 官方文档](https://kafka.apache.org/documentation/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) - **教程** - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - **文章** - - [Kafka 设计解析(七):流式计算的新贵 Kafka Stream](https://www.infoq.cn/article/kafka-analysis-part-7) \ No newline at end of file + - [Kafka 设计解析(七):流式计算的新贵 Kafka Stream](https://www.infoq.cn/article/kafka-analysis-part-7) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/03.Kafka\346\266\210\350\264\271\350\200\205.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\346\266\210\350\264\271.md" similarity index 94% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/03.Kafka\346\266\210\350\264\271\350\200\205.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\346\266\210\350\264\271.md" index d9708c5ccd..5e506a4896 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/03.Kafka\346\266\210\350\264\271\350\200\205.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\346\266\210\350\264\271.md" @@ -1,7 +1,6 @@ --- -title: Kafka 消费者 +title: Kafka 消费 date: 2021-04-14 15:05:34 -order: 03 categories: - 分布式 - 分布式通信 @@ -10,21 +9,21 @@ categories: tags: - MQ - Kafka -permalink: /pages/41a171/ +permalink: /pages/4952bbd2/ --- -# Kafka 消费者 +# Kafka 消费 ## 消费者简介 -### pull 模式 +### 获取消息模式 消息引擎获取消息有两种模式: -- push 模式:MQ 推送数据给消费者 -- pull 模式:消费者主动向 MQ 请求数据 +- **push 模式** - MQ 推送数据给消费者 +- **pull 模式** - 消费者主动向 MQ 请求数据 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210425190248.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502031317162.png) Kafka 消费者(Consumer)以 pull 方式从 Broker 拉取消息。相比于 push 方式,pull 方式灵活度和扩展性更好,因为消费的主动性由消费者自身控制。 @@ -57,20 +56,20 @@ Kafka 消费者从属于消费者群组,**一个群组里的 Consumer 订阅 同一时刻,**一条消息只能被同一消费者组中的一个消费者实例消费**。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210408194235.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070722981.png) **不同消费者群组之间互不影响**。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210408194839.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070723165.png) ### 消费流程 -Kafka 消费者通过 `poll` 来获取消息,但是获取消息时并不是立刻返回结果,需要考虑两个因素: +Kafka 消费者通过 `poll` 模式来获取消息,但是获取消息时并不是立刻返回结果,需要考虑两个因素: - 消费者通过 `customer.poll(time)` 中设置等待时间 - Broker 会等待累计一定量数据,然后发送给消费者。这样可以减少网络开销。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210425194822.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070724283.png) poll 除了获取消息外,还有其他作用: @@ -329,7 +328,7 @@ try { **Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区**。比如某个 Group 下有 20 个 Consumer 实例,它订阅了一个具有 100 个分区的 Topic。正常情况下,Kafka 平均会为每个 Consumer 分配 5 个分区。这个分配的过程就叫 Rebalance。 -当在群组里面 新增/移除消费者 或者 新增/移除 kafka 集群 broker 节点 时,群组协调器 Broker 会触发再均衡,重新为每一个 Partition 分配消费者。**Rebalance 期间,消费者无法读取消息,造成整个消费者群组一小段时间的不可用。** +当在群组里面新增/移除消费者或者新增/移除 kafka 集群 broker 节点时,群组协调器 Broker 会触发再均衡,重新为每一个 Partition 分配消费者。**Rebalance 期间,消费者无法读取消息,造成整个消费者群组一小段时间的不可用。** ### 何时生分区再均衡 @@ -355,7 +354,7 @@ try { (2)消费者通过向被指派为群组协调器(Coordinator)的 Broker 定期发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210415160730.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070723810.png) (3)群主从群组协调器获取群组成员列表,然后给每一个消费者进行分配分区 Partition。有两种分配策略:Range 和 RoundRobin。 @@ -384,7 +383,7 @@ try { ### 分区再均衡的问题 -- 首先,Rebalance 过程对 Consumer Group 消费过程有极大的影响。在 Rebalance 过程中,所有 Consumer 实例都会停止消费,等待 Rebalance 完成。 +- 首先,Rebalance 过程对 Consumer Group 消费过程有极大的影响。**在 Rebalance 过程中,所有 Consumer 实例都会停止消费,等待 Rebalance 完成**。 - 其次,目前 Rebalance 的设计是所有 Consumer 实例共同参与,全部重新分配所有分区。其实更高效的做法是尽量减少分配方案的变动。 - 最后,Rebalance 实在是太慢了。 @@ -407,7 +406,7 @@ try { #### 未及时发送心跳 -**第一类非必要 Rebalance 是因为未能及时发送心跳,导致 Consumer 被“踢出”Group 而引发的**。因此,你需要仔细地设置**session.timeout.ms 和 heartbeat.interval.ms**的值。我在这里给出一些推荐数值,你可以“无脑”地应用在你的生产环境中。 +**第一类非必要 Rebalance 是因为未能及时发送心跳**,导致 Consumer 被“踢出”Group 而引发的。因此,**需要合理设置会话超时时间**。这里给出一些推荐数值,你可以“无脑”地应用在你的生产环境中。 - 设置 `session.timeout.ms` = 6s。 - 设置 `heartbeat.interval.ms` = 2s。 @@ -417,7 +416,7 @@ try { #### Consumer 消费时间过长 -**第二类非必要 Rebalance 是 Consumer 消费时间过长导致的**。我之前有一个客户,在他们的场景中,Consumer 消费数据时需要将消息处理之后写入到 MongoDB。显然,这是一个很重的消费逻辑。MongoDB 的一丁点不稳定都会导致 Consumer 程序消费时长的增加。此时,**`max.poll.interval.ms`** 参数值的设置显得尤为关键。如果要避免非预期的 Rebalance,你最好将该参数值设置得大一点,比你的下游最大处理时间稍长一点。就拿 MongoDB 这个例子来说,如果写 MongoDB 的最长时间是 7 分钟,那么你可以将该参数设置为 8 分钟左右。 +**第二类非必要 Rebalance 是 Consumer 消费时间过长导致的**。此时,**`max.poll.interval.ms`** 参数值的设置显得尤为关键。如果要避免非预期的 Rebalance,你最好将该参数值设置得大一点,比你的下游最大处理时间稍长一点。 #### GC 参数 @@ -599,7 +598,7 @@ if (partitionInfos != null) { - [Kafka Github](https://github.com/apache/kafka) - [Kafka 官方文档](https://kafka.apache.org/documentation/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) - **教程** - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) \ No newline at end of file + - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/02.Kafka\347\224\237\344\272\247\350\200\205.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\347\224\237\344\272\247.md" similarity index 99% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/02.Kafka\347\224\237\344\272\247\350\200\205.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\347\224\237\344\272\247.md" index f83206860a..af3c36f669 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/02.Kafka\347\224\237\344\272\247\350\200\205.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\347\224\237\344\272\247.md" @@ -1,7 +1,6 @@ --- -title: Kafka 生产者 +title: Kafka 生产 date: 2021-04-14 15:05:34 -order: 02 categories: - 分布式 - 分布式通信 @@ -10,10 +9,10 @@ categories: tags: - MQ - Kafka -permalink: /pages/141b2e/ +permalink: /pages/f49f3bd2/ --- -# Kafka 生产者 +# Kafka 生产 ## 生产者简介 @@ -36,7 +35,7 @@ Kafka Producer 发送的数据对象叫做 `ProducerRecord` ,它有 4 个关 Kafka 生产者发送消息流程: -(1)**序列化** - 发送前,生产者要先把键和值序列化。 +(1)**序列化** - 发送前,生产者要先把键和值序列化成字节数组,这样它们才能够在网络中传输。 (2)**分区** - 数据被传给分区器。如果在 `ProducerRecord` 中已经指定了分区,那么分区器什么也不会做;否则,分区器会根据 `ProducerRecord` 的键来选择一个分区。选定分区后,生产者就知道该把消息发送给哪个主题的哪个分区。 @@ -358,6 +357,7 @@ Producer producer = new KafkaProducer<>(props); Broker 端在缓存中保存了这 seq number,对于接收的每条消息,如果其序号比 Broker 缓存中序号大于 1 则接受它,否则将其丢弃。这样就可以实现了消息重复提交了。但是,只能保证单个 Producer 对于同一个 `` 的 Exactly Once 语义。不能保证同一个 Producer 一个 topic 不同的 partion 幂等。 ![img](http://www.heartthinkdo.com/wp-content/uploads/2018/05/1-1.png) + 实现幂等之后: ![img](http://www.heartthinkdo.com/wp-content/uploads/2018/05/2.png) @@ -802,11 +802,11 @@ public void onlyConsumeInTransaction() { - [Kafka Confluent 官网](http://kafka.apache.org/) - [Kafka Jira](https://issues.apache.org/jira/projects/KAFKA?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) - - [《深入理解 Kafka:核心设计与实践原理》](https://item.jd.com/12489649.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) + - [《深入理解 Kafka:核心设计与实践原理》](https://book.douban.com/subject/30437872/) - [《Kafka 技术内幕》](https://item.jd.com/12234113.html) - **教程** - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - **文章** - - [Kafak(04) Kafka 生产者事务和幂等](http://www.heartthinkdo.com/?p=2040#43) \ No newline at end of file + - [Kafak(04) Kafka 生产者事务和幂等](http://www.heartthinkdo.com/?p=2040#43) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/08.Kafka\350\277\220\347\273\264.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\350\277\220\347\273\264.md" similarity index 99% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/08.Kafka\350\277\220\347\273\264.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\350\277\220\347\273\264.md" index e885b43f2d..24c7a5ed4b 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/08.Kafka\350\277\220\347\273\264.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\350\277\220\347\273\264.md" @@ -1,7 +1,6 @@ --- title: Kafka 运维 date: 2020-06-03 09:55:35 -order: 08 categories: - 分布式 - 分布式通信 @@ -10,7 +9,7 @@ categories: tags: - MQ - Kafka -permalink: /pages/21011e/ +permalink: /pages/91694ba0/ --- # Kafka 运维 @@ -410,9 +409,9 @@ Kafka 机器数 = 单位时间需要处理的总数据量 / 单机所占用带 - [Kafka Github](https://github.com/apache/kafka) - [Kafka 官方文档](https://kafka.apache.org/documentation/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) - **教程** - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - **文章** - - [kafka-cheat-sheet](https://github.com/lensesio/kafka-cheat-sheet) \ No newline at end of file + - [kafka-cheat-sheet](https://github.com/lensesio/kafka-cheat-sheet) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/04.Kafka\351\233\206\347\276\244.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\351\233\206\347\276\244.md" similarity index 96% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/04.Kafka\351\233\206\347\276\244.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\351\233\206\347\276\244.md" index 4f0b635fca..1a02f56263 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/04.Kafka\351\233\206\347\276\244.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\351\233\206\347\276\244.md" @@ -1,7 +1,6 @@ --- title: Kafka 集群 date: 2021-04-29 08:17:17 -order: 04 categories: - 分布式 - 分布式通信 @@ -10,7 +9,7 @@ categories: tags: - MQ - Kafka -permalink: /pages/fc8f54/ +permalink: /pages/32977605/ --- # Kafka 集群 @@ -25,7 +24,7 @@ permalink: /pages/fc8f54/ 在 Broker 停机、出现网络分区或长时间垃圾回收停顿时,Broker 会与 ZooKeeper 断开连接,此时 Broker 在启动时创建的临时节点会自动被 ZooKeeper 移除。监听 Broker 列表的 Kafka 组件会被告知 Broker 已移除。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210423171607.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070741387.png) Kafka 在 ZooKeeper 的关键存储信息: @@ -48,7 +47,7 @@ Kafka 在 ZooKeeper 的关键存储信息: 控制器(Controller),是 Apache Kafka 的核心组件。它的主要作用是在 ZooKeeper 的帮助下管理和协调整个 Kafka 集群。控制器其实就是一个 Broker,只不过它除了具有一般 Broker 的功能以外,还负责 Leader 的选举。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210429071042.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070741426.png) ### 如何选举控制器 @@ -56,7 +55,7 @@ Kafka 在 ZooKeeper 的关键存储信息: 选举控制器的详细流程: -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210502213820.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070742505.png) 1. 第一个在 ZooKeeper 中成功创建 `/controller` 临时节点的 Broker 会被指定为控制器。 @@ -122,7 +121,7 @@ Preferred 领导者选举主要是 Kafka 为了避免部分 Broker 负载过重 Kafka 使用 Topic 来组织数据,每个 Topic 被分为若干个 Partition,每个 Partition 有多个副本。每个 Broker 可以保存成百上千个属于不同 Topic 和 Partition 的副本。**Kafka 副本的本质是一个只能追加写入的提交日志**。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210407180101.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070743894.png) Kafka 副本有两种角色: @@ -130,7 +129,7 @@ Kafka 副本有两种角色: - **Follower 副本(从)**:Leader 副本以外的副本都是 Follower 副本。**Follower 唯一的任务就是从 Leader 那里复制消息,保持与 Leader 一致的状态**。 - 如果 Leader 宕机,其中一个 Follower 会被选举为新的 Leader。 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20210407191337.png) +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070743006.png) 为了与 Leader 保持同步,Follower 向 Leader 发起获取数据的请求,这种请求与消费者为了读取消息而发送的请求是一样的。请求消息里包含了 Follower 想要获取消息的偏移量,而这些偏移量总是有序的。 @@ -148,8 +147,6 @@ Kafka Broker 端参数 `replica.lag.time.max.ms` 参数,指定了 Follower 副 ISR 是一个动态调整的集合,会不断将同步副本加入集合,将不同步副本移除集合。Leader 副本天然就在 ISR 中。 -## 选举 Leader - ### Unclean 领导者选举 因为 Leader 副本天然就在 ISR 中,如果 ISR 为空了,就说明 Leader 副本也“挂掉”了,Kafka 需要重新选举一个新的 Leader。 @@ -243,9 +240,9 @@ Follower 宕机,啥事儿没有;Leader 宕机了,会从 Follower 中重新 - [Kafka Github](https://github.com/apache/kafka) - [Kafka 官方文档](https://kafka.apache.org/documentation/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) - **教程** - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - **文章** - - [Thorough Introduction to Apache Kafka](https://hackernoon.com/thorough-introduction-to-apache-kafka-6fbf2989bbc1) \ No newline at end of file + - [Thorough Introduction to Apache Kafka](https://hackernoon.com/thorough-introduction-to-apache-kafka-6fbf2989bbc1) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..3231528212 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/Kafka\351\235\242\350\257\225.md" @@ -0,0 +1,758 @@ +--- +title: Kafka 面试 +cover: https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/mq/kafka/kafka-event-system.png +date: 2025-02-03 11:15:43 +categories: + - 分布式 + - 分布式通信 + - MQ + - Kafka +tags: + - Java + - 中间件 + - MQ + - Kafka + - 面试 +permalink: /pages/404a13d7/ +--- + +# Kafka 面试 + +## Kafka 简介 + +### 【基础】什么是 Kafka? + +:::details 要点 + +**Apache Kafka 是一款开源的消息引擎系统,也是一个分布式流计算平台,此外,还可以作为数据存储**。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070719480.gif) + +Kafka 的核心功能如下: + +- **消息引擎** - Kafka 可以作为一个消息引擎系统。 +- **流处理** - Kafka 可以作为一个分布式流处理平台。 +- **存储** - Kafka 可以作为一个安全的分布式存储。 + +Kafka 的设计目标: + +- **高性能** + - **分区、分段、索引**:基于分区机制提供并发处理能力。分段、索引提升了数据读写的查询效率。 + - **顺序读写**:使用顺序读写提升磁盘 IO 性能。 + - **零拷贝**:利用零拷贝技术,提升网络 I/O 效率。 + - **页缓存**:利用操作系统的 PageCache 来缓存数据(典型的利用空间换时间) + - **批量读写**:批量读写可以有效提升网络 I/O 效率。 + - **数据压缩**:Kafka 支持数据压缩,可以有效提升网络 I/O 效率。 + - **pull 模式**:Kafka 架构基于 pull 模式,可以自主控制消费策略,提升传输效率。 +- **高可用** + - **持久化**:Kafka 所有的消息都存储在磁盘,天然支持持久化。 + - **副本机制**:Kafka 的 Broker 集群支持副本机制,可以通过冗余,来保证其整体的可用性。 + - **选举 Leader**:Kafka 基于 ZooKeeper 支持选举 Leader,实现了故障转移能力。 +- **伸缩性** + - **分区**:Kafka 的分区机制使得其具有良好的伸缩性。 + +::: + +### 【基础】Kafka 有哪些核心术语? + +:::details 要点 + +Kafka 的核心术语如下: + +- **消息** - Record。Kafka 是消息引擎嘛,这里的消息就是指 Kafka 处理的主要对象。 +- **主题** - Topic。主题是承载消息的逻辑容器,在实际使用中多用来区分具体的业务。 +- **分区** - Partition。一个有序不变的消息序列。每个主题下可以有多个分区。 +- **消息位移** - Offset。表示分区中每条消息的位置信息,是一个单调递增且不变的值。 +- **副本** - Replica。Kafka 中同一条消息能够被拷贝到多个地方以提供数据冗余,这些地方就是所谓的副本。副本还分为领导者副本和追随者副本,各自有不同的角色划分。副本是在分区层级下的,即每个分区可配置多个副本实现高可用。 +- **生产者** - Producer。向主题发布新消息的应用程序。 +- **消费者** - Consumer。从主题订阅新消息的应用程序。 +- **消费者位移** - Consumer Offset。表征消费者消费进度,每个消费者都有自己的消费者位移。 +- **消费者组** - Consumer Group。多个消费者实例共同组成的一个组,同时消费多个分区以实现高吞吐。 +- **分区再均衡** - Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070720162.png) + +Kafka 的三层消息架构: + +- 第一层是主题层,每个主题可以配置 M 个分区,而每个分区又可以配置 N 个副本。 +- 第二层是分区层,每个分区的 N 个副本中只能有一个充当领导者角色,对外提供服务;其他 N-1 个副本是追随者副本,只是提供数据冗余之用。 +- 第三层是消息层,分区中包含若干条消息,每条消息的位移从 0 开始,依次递增。 +- 最后,客户端程序只能与分区的领导者副本进行交互。 + +::: + +## Kafka 存储 + +### 【中级】Kafka 是如何存储数据的? + +:::details 要点 + +#### Kafka 逻辑存储 + +Kafka 的数据结构采用三级结构,即:主题(Topic)、分区(Partition)、消息(Record)。 + +在 Kafka 中,任意一个 Topic 维护了一组 Partition 日志,如下所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070720162.png) + +请注意:这里的主题只是一个逻辑上的抽象概念,实际上,**Kafka 的基本存储单元是 Partition**。Partition 无法在多个 Broker 间进行再细分,也无法在同一个 Broker 的多个磁盘上进行再细分。所以,分区的大小受到单个挂载点可用空间的限制。 + +Partiton 命名规则为 Topic 名称 + 有序序号,第一个 Partiton 序号从 0 开始,序号最大值为 Partition 数量减 1。 + +#### Kafka 物理存储 + +`Log` 是 Kafka 用于表示日志文件的组件。每个 Partiton 对应一个 `Log` 对象,在物理磁盘上则对应一个目录。如:创建一个双分区的主题 `test`,那么,Kafka 会在磁盘上创建两个子目录:`test-0` 和 `test-1`;而在服务器端,这就对应两个 `Log` 对象。 + +因为在一个大文件中查找和删除消息是非常耗时且容易出错的。所以,Kafka 将每个 Partition 切割成若干个片段,即日志段(Log Segment)。**默认每个 Segment 大小不超过 1G,且只包含 7 天的数据**。如果 Segment 的消息量达到 1G,那么该 Segment 会关闭,同时打开一个新的 Segment 进行写入。 + +Broker 会为 Partition 里的每个 Segment 打开一个文件句柄(包括不活跃的 Segment),因此打开的文件句柄数通常会比较多,这个需要适度调整系统的进程文件句柄参数。**正在写入的分片称为活跃片段(active segment),活跃片段永远不会被删除**。 + +Segment 文件命名规则:Partition 全局的第一个 segment 从 0 开始,后续每个 segment 文件名为上一个 segment 文件最后一条消息的 offset 值。数值最大为 64 位 long 大小,19 位数字字符长度,没有数字用 0 填充。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070721654.png) + +Segment 文件可以分为两类: + +- 索引文件 + - 偏移量索引文件( `.index` ) + - 时间戳索引文件( `.timeindex` ) + - 已终止事务的索引文件(`.txnindex`):如果没有使用 Kafka 事务,则不会创建该文件 +- 日志数据文件(`.log`) + +::: + +### 【高级】Kafka 文件格式是怎样的? + +:::details 要点 + +Kafka 的消息和偏移量保存在文件里。保存在磁盘上的数据格式和从生产者发送过来或消费者读取的数据格式是一样的。因为使用了相同的数据格式,使得 Kafka 可以进行零拷贝技术给消费者发送消息,同时避免了压缩和解压。 + +除了键、值和偏移量外,消息里还包含了消息大小、校验和(检测数据损坏)、魔数(标识消息格式版本)、压缩算法(Snappy、GZip 或者 LZ4)和时间戳(0.10.0 新增)。时间戳可以是生产者发送消息的时间,也可以是消息到达 Broker 的时间,这个是可配的。 + +如果生产者发送的是压缩的消息,那么批量发送的消息会压缩在一起,以“包装消息”(wrapper message)来发送,如下所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200621134404.png) + +如果生产者使用了压缩功能,发送的批次越大,就意味着能获得更好的网络传输效率,并且节省磁盘存储空间。 + +Kafka 附带了一个叫 DumpLogSegment 的工具,可以用它查看片段的内容。它可以显示每个消息的偏移量、校验和、魔术数字节、消息大小和压缩算法。 + +::: + +### 【高级】Kafka 如何检索数据? + +:::details 要点 + +Kafka 允许消费者从任意有效的偏移量位置开始读取消息。Kafka 为每个 Partition 都维护了一个索引(即 `.index` 文件),该索引将偏移量映射到片段文件以及偏移量在文件里的位置。 + +索引也被分成片段,所以在删除消息时,也可以删除相应的索引。Kafka 不维护索引的校验和。如果索引出现损坏,Kafka 会通过重读消息并录制偏移量和位置来重新生成索引。如果有必要,管理员可以删除索引,这样做是绝对安全的,Kafka 会自动重新生成这些索引。 + +索引文件用于将偏移量映射成为消息在日志数据文件中的实际物理位置,每个索引条目由 offset 和 position 组成,每个索引条目可以唯一确定在各个分区数据文件的一条消息。其中,Kafka 采用稀疏索引存储的方式,每隔一定的字节数建立了一条索引,可以通过**“index.interval.bytes”**设置索引的跨度; + +有了偏移量索引文件,通过它,Kafka 就能够根据指定的偏移量快速定位到消息的实际物理位置。具体的做法是,根据指定的偏移量,使用二分法查询定位出该偏移量对应的消息所在的分段索引文件和日志数据文件。然后通过二分查找法,继续查找出小于等于指定偏移量的最大偏移量,同时也得出了对应的 position(实际物理位置),根据该物理位置在分段的日志数据文件中顺序扫描查找偏移量与指定偏移量相等的消息。下面是 Kafka 中分段的日志数据文件和偏移量索引文件的对应映射关系图(其中也说明了如何按照起始偏移量来定位到日志数据文件中的具体消息)。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070722556.png) + +## + +::: + +### 【高级】Kafka 如何清理数据? + +:::details 要点 + +每个日志片段可以分为以下两个部分: + +- **干净的部分**:这部分消息之前已经被清理过,每个键只存在一个值。 +- **污浊的部分**:在上一次清理后写入的新消息。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200621135557.png) + +如果在 Kafka 启动时启用了清理功能(通过 `log.cleaner.enabled` 配置),每个 Broker 会启动一个清理管理器线程和若干个清理线程,每个线程负责一个 Partition。 + +清理线程会读取污浊的部分,并在内存里创建一个 map。map 的 key 是消息键的哈希值,value 是消息的偏移量。对于相同的键,只保留最新的位移。其中 key 的哈希大小为 16 字节,位移大小为 8 个字节。也就是说,一个映射只有 24 字节,假设消息大小为 1KB,那么 1GB 的段有 1 百万条消息,建立这个段的映射只需要 24MB 的内存,映射的内存效率是非常高效的。 + +在配置 Kafka 时,管理员需要设置这些清理线程可以使用的总内存。如果设置 1GB 的总内存同时有 5 个清理线程,那么每个线程只有 200MB 的内存可用。在清理线程工作时,它不需要把所有脏的段文件都一起在内存中建立上述映射,但需要保证至少能够建立一个段的映射。如果不能同时处理所有脏的段,Kafka 会一次清理最老的几个脏段,然后在下一次再处理其他的脏段。 + +一旦建立完脏段的键与位移的映射后,清理线程会从最老的干净的段开始处理。如果发现段中的消息的键没有在映射中出现,那么可以知道这个消息是最新的,然后简单的复制到一个新的干净的段中;否则如果消息的键在映射中出现,这条消息需要抛弃,因为对于这个键,已经有新的消息写入。处理完会将产生的新段替代原始段,并处理下一个段。 + +对于一个段,清理前后的效果如下: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200621140117.png) + +对于只保留最新消息的清理策略来说,Kafka 还支持删除相应键的消息操作(而不仅仅是保留最新的消息内容)。这是通过生产者发送一条特殊的消息来实现的,该消息包含一个键以及一个 null 的消息内容。当清理线程发现这条消息时,它首先仍然进行一个正常的清理并且保留这个包含 null 的特殊消息一段时间,在这段时间内消费者消费者可以获取到这条消息并且知道消息内容已经被删除。过了这段时间,清理线程会删除这条消息,这个键会从 Partition 中消失。这段时间是必须的,因为它可以使得消费者有一定的时间余地来收到这条消息。 + +::: + +## 生产者 + +### 【中级】Kafka 发送消息的工作流程是怎样的? + +:::details 要点 + +Kafka 生产者用一个 `ProducerRecord` 对象来抽象一条要发送的消息, `ProducerRecord` 对象需要包含目标主题和要发送的内容,还可以指定键或分区。其发送消息流程如下: + +(1)**序列化** - 生产者要先把键和值序列化成字节数组,这样它们才能够在网络中传输。 + +(2)**分区** - 数据被传给分区器。如果在 `ProducerRecord` 中已经指定了分区,那么分区器什么也不会做;否则,分区器会根据 `ProducerRecord` 的键来选择一个分区。选定分区后,生产者就知道该把消息发送给哪个主题的哪个分区。 + +(3)**批次传输** - 接着,这条记录会被添加到一个记录批次中。这个批次中的所有消息都会被发送到相同的主题和分区上。有一个独立的线程负责将这些记录批次发送到相应 Broker 上。 + +- **批次,就是一组消息,这些消息属于同一个主题和分区**。 +- 发送时,会把消息分成批次传输,如果每次只发送一个消息,会占用大量的网路开销。 + +(4)**响应** - 服务器收到消息会返回一个响应。 + +- 如果**成功**,则返回一个 `RecordMetaData` 对象,它包含了主题、分区、偏移量; +- 如果**失败**,则返回一个错误。生产者在收到错误后,可以进行重试,重试次数可以在配置中指定。失败一定次数后,就返回错误消息。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200528224323.png) + +生产者向 Broker 发送消息时是怎么确定向哪一个 Broker 发送消息? + +- 生产者会向任意 broker 发送一个元数据请求(`MetadataRequest`),获取到每一个分区对应的 Leader 信息,并缓存到本地。 +- 生产者在发送消息时,会指定 Partition 或者通过 key 得到到一个 Partition,然后根据 Partition 从缓存中获取相应的 Leader 信息。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200621113043.png) + +::: + +## 消费者 + +### 【基础】Kafka 为什么要支持消费者群组? + +:::details 要点 + +#### 消费者 + +每个 Consumer 的唯一元数据是该 Consumer 在日志中消费的位置。这个偏移量是由 Consumer 控制的:Consumer 通常会在读取记录时线性的增加其偏移量。但实际上,由于位置由 Consumer 控制,所以 Consumer 可以采用任何顺序来消费记录。 + +**一条消息只有被提交,才会被消费者获取到**。如下图,只能消费 Message0、Message1、Message2: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200621113917.png) + +#### 消费者群组 + +**Consumer Group 是 Kafka 提供的可扩展且具有容错性的消费者机制**。 + +Kafka 的写入数据量很庞大,如果只有一个消费者,消费消息速度很慢,时间长了,就会造成数据积压。为了减少数据积压,Kafka 支持消费者群组,可以让多个消费者并发消费消息,对数据进行分流。 + +Kafka 消费者从属于消费者群组,**一个群组里的 Consumer 订阅同一个 Topic,一个主题有多个 Partition,每一个 Partition 只能隶属于消费者群组中的一个 Consumer**。 + +如果超过主题的分区数量,那么有一部分消费者就会被闲置,不会接收到任何消息。 + +同一时刻,**一条消息只能被同一消费者组中的一个消费者实例消费**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070722981.png) + +**不同消费者群组之间互不影响**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070723165.png) + +::: + +### 【中级】如何消费 Kafka 消息? + +:::details 要点 + +Kafka 消费者通过 `pull` 模式来获取消息,但是获取消息时并不是立刻返回结果,需要考虑两个因素: + +- 消费者通过 `customer.poll(time)` 中设置等待时间 +- Broker 会等待累计一定量数据,然后发送给消费者。这样可以减少网络开销。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070724283.png) + +`pull` 除了获取消息外,还有其他作用: + +- **发送心跳信息**。消费者通过向被指派为群组协调器的 Broker 发送心跳来维护他和群组的从属关系,当机器宕掉后,群组协调器触发再均衡。 + +::: + +## 分区 + +### 【中级】什么是分区?为什么要分区? + +:::details 要点 + +Kafka 的数据结构采用三级结构,即:主题(Topic)、分区(Partition)、消息(Record)。 + +在 Kafka 中,任意一个 Topic 维护了一组 Partition 日志,如下所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/distributed/mq/kafka/kafka-log-anatomy.png) + +每个 Partition 都是一个单调递增的、不可变的日志记录,以不断追加的方式写入数据。Partition 中的每条记录会被分配一个单调递增的 id 号,称为偏移量(Offset),用于唯一标识 Partition 内的每条记录。 + +为什么 Kafka 的数据结构采用三级结构? + +**分区的作用就是提供负载均衡的能力**,以实现系统的高伸缩性(Scalability)。 + +不同的分区能够被放置到不同节点的机器上,而数据的读写操作也都是针对分区这个粒度而进行的,这样每个节点的机器都能独立地执行各自分区的读写请求处理。并且,我们还可以通过添加新的机器节点来增加整体系统的吞吐量。 + +::: + +### 【中级】Kafka 的分区策略是怎样的? + +:::details 要点 + +所谓分区策略是决定生产者将消息发送到哪个分区的算法,也就是负载均衡算法。 + +Kafka 生产者发送消息使用的对象 `ProducerRecord` ,可以选填 Partition 和 Key。不过,大多数应用会用到 key。key 有两个作用:作为消息的附加信息;也可以用来决定消息该被写到 Topic 的哪个 Partition,拥有相同 key 的消息将被写入同一个 Partition。 + +**如果 `ProducerRecord` 指定了 Partition,则分区器什么也不做**,否则分区器会根据 key 选择一个 Partition 。 + +- 没有 key 时的分发逻辑:每隔 `topic.metadata.refresh.interval.ms` 的时间,随机选择一个 partition。这个时间窗口内的所有记录发送到这个 partition。发送数据出错后会重新选择一个 partition。 +- 根据 key 分发:Kafka 的选择分区策略是:根据 key 求 hash 值,然后将 hash 值对 partition 数量求模。这里的关键点在于,**同一个 key 总是被映射到同一个 Partition 上**。所以,在选择分区时,Kafka 会使用 Topic 的所有 Partition ,而不仅仅是可用的 Partition。这意味着,**如果写入数据的 Partition 是不可用的,那么就会出错**。 + +::: + +### 【中级】如何自定义分区策略? + +:::details 要点 + +如果 Kafka 的默认分区策略无法满足实际需要,可以自定义分区策略。需要显式地配置生产者端的参数 `partitioner.class`。这个参数该怎么设定呢? + +首先,要实现 `org.apache.kafka.clients.producer.Partitioner` 接口。这个接口定义了两个方法:`partition` 和 `close`,通常只需要实现最重要的 `partition` 方法。我们来看看这个方法的方法签名: + +```java +int partition(String topic, Object key, byte[] keyBytes, Object value, byte[] valueBytes, Cluster cluster); +``` + +这里的 `topic`、`key`、`keyBytes`、`value`和 `valueBytes` 都属于消息数据,`cluster` 则是集群信息(比如当前 Kafka 集群共有多少主题、多少 Broker 等)。Kafka 给你这么多信息,就是希望让你能够充分地利用这些信息对消息进行分区,计算出它要被发送到哪个分区中。 + +接着,设置 `partitioner.class` 参数为自定义类的全限定名,那么生产者程序就会按照你的代码逻辑对消息进行分区。 + +负载均衡算法常见的有: + +- 随机算法 +- 轮询算法 +- 最小活跃数算法 +- 源地址哈希算法 + +可以根据实际需要去实现。 + +::: + +### 【高级】Kafka 如何实现分区再均衡? + +:::details 要点 + +#### 什么是分区再均衡 + +分区的所有权从一个消费者转移到另一个消费者,这样的行为被称为**分区再均衡(Rebalance)**。**Rebalance 实现了消费者群组的高可用性和伸缩性**。 + +**Rebalance 本质上是一种协议,规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区**。比如某个 Group 下有 20 个 Consumer 实例,它订阅了一个具有 100 个分区的 Topic。正常情况下,Kafka 平均会为每个 Consumer 分配 5 个分区。这个分配的过程就叫 Rebalance。 + +当在群组里面新增/移除消费者或者新增/移除 kafka 集群 broker 节点时,群组协调器 Broker 会触发再均衡,重新为每一个 Partition 分配消费者。**Rebalance 期间,消费者无法读取消息,造成整个消费者群组一小段时间的不可用。** + +#### 何时生分区再均衡 + +分区再均衡的触发时机有三种: + +- **消费者群组成员数发生变更**。比如有新的 Consumer 加入群组或者离开群组,或者是有 Consumer 实例崩溃被“踢出”群组。 + - 新增消费者。consumer 订阅主题之后,第一次执行 poll 方法 + - 移除消费者。执行 `consumer.close()` 操作或者消费客户端宕机,就不再通过 poll 向群组协调器发送心跳了,当群组协调器检测次消费者没有心跳,就会触发再均衡。 +- **订阅主题数发生变更**。Consumer Group 可以使用正则表达式的方式订阅主题,比如 `consumer.subscribe(Pattern.compile(“t.*c”))` 就表明该 Group 订阅所有以字母 t 开头、字母 c 结尾的主题。在 Consumer Group 的运行过程中,你新创建了一个满足这样条件的主题,那么该 Group 就会发生 Rebalance。 +- **订阅主题的分区数发生变更**。Kafka 当前只能允许增加一个主题的分区数。当分区数增加时,就会触发订阅该主题的所有 Group 开启 Rebalance。 + - 新增 broker。如重启 broker 节点 + - 移除 broker。如 kill 掉 broker 节点。 + +#### 分区再均衡的过程 + +**Rebalance 是通过消费者群组中的称为“群主”消费者客户端进行的**。 + +(1)选择群主 + +当消费者要加入群组时,会向群组协调器发送一个 JoinGroup 请求。第一个加入群组的消费者将成为“群主”。**群主从协调器那里获取群组的活跃成员列表,并负责给每一个消费者分配分区**。 + +> 所谓协调者,在 Kafka 中对应的术语是 Coordinator,它专门为 Consumer Group 服务,负责为 Group 执行 Rebalance 以及提供位移管理和组成员管理等。具体来讲,Consumer 端应用程序在提交位移时,其实是向 Coordinator 所在的 Broker 提交位移。同样地,当 Consumer 应用启动时,也是向 Coordinator 所在的 Broker 发送各种请求,然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作。 + +(2)消费者通过向被指派为群组协调器(Coordinator)的 Broker 定期发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070723810.png) + +(3)群主从群组协调器获取群组成员列表,然后给每一个消费者进行分配分区 Partition。有两种分配策略:Range 和 RoundRobin。 + +- **Range 策略**,就是把若干个连续的分区分配给消费者,如存在分区 1-5,假设有 3 个消费者,则消费者 1 负责分区 1-2,消费者 2 负责分区 3-4,消费者 3 负责分区 5。 +- **RoundRoin 策略**,就是把所有分区逐个分给消费者,如存在分区 1-5,假设有 3 个消费者,则分区 1->消费 1,分区 2->消费者 2,分区 3>消费者 3,分区 4>消费者 1,分区 5->消费者 2。 + +(4)群主分配完成之后,把分配情况发送给群组协调器。 + +(5)群组协调器再把这些信息发送给消费者。**每个消费者只能看到自己的分配信息,只有群主知道所有消费者的分配信息**。 + +#### 如何判定消费者已经死亡 + +消费者通过向被指定为群组协调器的 Broker 发送心跳来维持它们和群组的从属关系以及它们对分区的所有权关系。只要消费者以正常的时间间隔发送心跳,就被认为是活跃的。消费者会在轮询消息或提交偏移量时发送心跳。如果消费者超时未发送心跳,会话就会过期,群组协调器认定它已经死亡,就会触发一次再均衡。 + +当一个消费者要离开群组时,会通知协调器,协调器会立即触发一次再均衡,尽量降低处理停顿。 + +#### 查找协调者 + +所有 Broker 在启动时,都会创建和开启相应的 Coordinator 组件。也就是说,**所有 Broker 都有各自的 Coordinator 组件**。那么,Consumer Group 如何确定为它服务的 Coordinator 在哪台 Broker 上呢?答案就在我们之前说过的 Kafka 内部位移主题 `__consumer_offsets` 身上。 + +目前,Kafka 为某个 Consumer Group 确定 Coordinator 所在的 Broker 的算法有 2 个步骤。 + +1. 第 1 步:确定由位移主题的哪个分区来保存该 Group 数据:`partitionId=Math.abs(groupId.hashCode() % offsetsTopicPartitionCount)`。 + +2. 第 2 步:找出该分区 Leader 副本所在的 Broker,该 Broker 即为对应的 Coordinator。 + +::: + +### 【高级】分区再均衡存在什么问题?如何避免分区再均衡? + +:::details 要点 + +#### 分区再均衡的问题 + +- 首先,Rebalance 过程对 Consumer Group 消费过程有极大的影响。在 Rebalance 过程中,所有 Consumer 实例都会停止消费,等待 Rebalance 完成。 +- 其次,目前 Rebalance 的设计是所有 Consumer 实例共同参与,全部重新分配所有分区。其实更高效的做法是尽量减少分配方案的变动。 +- 最后,Rebalance 实在是太慢了。 + +#### 避免分区再均衡 + +通过前文,我们已经知道了:分区再均衡的代价很高,应该尽量避免不必要的分区再均衡,以整体提高 Consumer 的吞吐量。 + +分区再均衡发生的时机有三个: + +- **消费群组成员数量发生变化** +- **订阅主题数量发生变化** +- **订阅主题的分区数发生变化** + +后面两个通常都是运维的主动操作,所以它们引发的 Rebalance 大都是不可避免的。实际上,大部分情况下,导致分区再均衡的原因是:消费群组成员数量发生变化。 + +有两种情况,消费者并没有宕机,但也被视为消亡: + +- 未及时发送心跳 +- Consumer 消费时间过长 + +##### 未及时发送心跳 + +**第一类非必要 Rebalance 是因为未能及时发送心跳**,导致 Consumer 被“踢出”Group 而引发的。因此,**需要合理设置会话超时时间**。这里给出一些推荐数值,你可以“无脑”地应用在你的生产环境中。 + +- 设置 `session.timeout.ms` = 6s。 +- 设置 `session.timeout.ms` = 6s。 +- 设置 `heartbeat.interval.ms` = 2s。 +- 要保证 Consumer 实例在被判定为“dead”之前,能够发送至少 3 轮的心跳请求,即 `session.timeout.ms` >= 3 \* `heartbeat.interval.ms`。 + +将 `session.timeout.ms` 设置成 6s 主要是为了让 Coordinator 能够更快地定位已经挂掉的 Consumer。毕竟,我们还是希望能尽快揪出那些“尸位素餐”的 Consumer,早日把它们踢出 Group。希望这份配置能够较好地帮助你规避第一类“不必要”的 Rebalance。 + +##### Consumer 消费时间过长 + +**第二类非必要 Rebalance 是 Consumer 消费时间过长导致的**。此时,**`max.poll.interval.ms`** 参数值的设置显得尤为关键。如果要避免非预期的 Rebalance,你最好将该参数值设置得大一点,比你的下游最大处理时间稍长一点。 + +##### GC 参数 + +如果你按照上面的推荐数值恰当地设置了这几个参数,却发现还是出现了 Rebalance,那么我建议你去排查一下 **Consumer 端的 GC 表现**,比如是否出现了频繁的 Full GC 导致的长时间停顿,从而引发了 Rebalance。为什么特意说 GC?那是因为在实际场景中,我见过太多因为 GC 设置不合理导致程序频发 Full GC 而引发的非预期 Rebalance 了。 + +::: + +## 复制 + +### 【中级】Kafka 如何管理副本? + +:::details 要点 + +副本机制是分布式系统实现高可用的不二法门,Kafka 也不例外。 + +副本机制有哪些好处? + +1. **提供可用性**:有句俗语叫:鸡蛋不要放在一个篮子里。副本机制也是一个道理——当部分节点宕机时,系统仍然可以依靠其他正常运转的节点,从整体上对外继续提供服务。 +2. **提供伸缩性**:通过增加、减少机器可以控制系统整体的吞吐量。 +3. **改善数据局部性**:允许将数据放入与用户地理位置相近的地方,从而降低系统延时。 + +但是,Kafka 只实现了第一个好处,原因后面会阐述。 + +- 每个 Partition 都有一个 Leader,零个或多个 Follower。 +- Leader 处理一切对 Partition (分区)的读写请求;而 Follower 只需被动的同步 Leader 上的数据。 +- 同一个 Topic 的不同 Partition 会分布在多个 Broker 上,而且一个 Partition 还会在其他的 Broker 上面进行备份。 + +#### Kafka 副本角色 + +Kafka 使用 Topic 来组织数据,每个 Topic 被分为若干个 Partition,每个 Partition 有多个副本。每个 Broker 可以保存成百上千个属于不同 Topic 和 Partition 的副本。**Kafka 副本的本质是一个只能追加写入的提交日志**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070726284.png) + +Kafka 副本有两种角色: + +- **Leader 副本(主)**:每个 Partition 都有且仅有一个 Leader 副本。为了保证数据一致性,**Leader 处理一切对 Partition (分区)的读写请求**; +- **Follower 副本(从)**:Leader 副本以外的副本都是 Follower 副本。**Follower 唯一的任务就是从 Leader 那里复制消息,保持与 Leader 一致的状态**。 +- 如果 Leader 宕机,其中一个 Follower 会被选举为新的 Leader。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070726185.png) + +为了与 Leader 保持同步,Follower 向 Leader 发起获取数据的请求,这种请求与消费者为了读取消息而发送的请求是一样的。请求消息里包含了 Follower 想要获取消息的偏移量,而这些偏移量总是有序的。 + +Leader 另一个任务是搞清楚哪个 Follower 的状态与自己是一致的。通过查看每个 Follower 请求的最新偏移量,Leader 就会知道每个 Follower 复制的进度。如果跟随者在 10s 内没有请求任何消息,或者虽然在请求消息,但是在 10s 内没有请求最新的数据,那么它就会被认为是**不同步**的。**如果一个副本是不同步的,在 Leader 失效时,就不可能成为新的 Leader**——毕竟它没有包含全部的消息。 + +除了当前首领之外,每个分区都有一个首选首领——创建 Topic 时选定的首领就是分区的首选首领。之所以叫首选 Leader,是因为在创建分区时,需要在 Broker 之间均衡 Leader。 + +#### ISR + +ISR 即 In-sync Replicas,表示同步副本。Follower 副本不提供服务,只是定期地异步拉取领导者副本中的数据而已。既然是异步的,说明和 Leader 并非数据强一致性的。 + +**判断 Follower 是否与 Leader 同步的标准**: + +Kafka Broker 端参数 `replica.lag.time.max.ms` 参数,指定了 Follower 副本能够落后 Leader 副本的最长时间间隔,默认为 10s。这意味着:只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 Leader 是**同步**的,即使此时 Follower 副本中保存的消息明显少于 Leader 副本中的消息。 + +ISR 是一个动态调整的集合,会不断将同步副本加入集合,将不同步副本移除集合。Leader 副本天然就在 ISR 中。 + +#### Unclean 领导者选举 + +因为 Leader 副本天然就在 ISR 中,如果 ISR 为空了,就说明 Leader 副本也“挂掉”了,Kafka 需要重新选举一个新的 Leader。 + +**Kafka 把所有不在 ISR 中的存活副本都称为非同步副本**。通常来说,非同步副本落后 Leader 太多,因此,如果选择这些副本作为新 Leader,就可能出现数据的丢失。毕竟,这些副本中保存的消息远远落后于老 Leader 中的消息。在 Kafka 中,选举这种副本的过程称为 Unclean 领导者选举。**Broker 端参数 `unclean.leader.election.enable` 控制是否允许 Unclean 领导者选举**。 + +**开启 Unclean 领导者选举可能会造成数据丢失**,但好处是:它使得 Partition Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止 Unclean 领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。 + +::: + +## 可靠传输 + +### 【高级】如何保证 Kafka 消息不丢失? + +:::details 要点 + +如何保证消息的可靠性传输,或者说,如何保证消息不丢失?这对于任何 MQ 都是核心问题。 + +一条消息从生产到消费,可以划分三个阶段: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070727544.png) + +- **生产阶段**:Producer 创建消息,并通过网络发送给 Broker。 +- **存储阶段**:Broker 收到消息并存储,如果是集群,还要同步副本给其他 Broker。 +- **消费阶段**:Consumer 向 Broker 请求消息,Broker 通过网络传输给 Consumer。 + +这三个阶段都可能丢失数据,所以要保证消息丢失,就需要任意一环都保证可靠。 + +#### 存储阶段不丢消息 + +存储阶段指的是 Kafka Server,也就是 Broker 如何保证消息不丢失。 + +一句话概括,**Kafka 只对“已提交”的消息(committed message)做有限度的持久化保证**。 + +上面的话可以解读为: + +- **已提交**:**只有当消息被写入分区的若干同步副本时,才被认为是已提交的**。为什么是若干个 Broker 呢?这取决于你对“已提交”的定义。你可以选择只要 Leader 成功保存该消息就算是已提交,也可以是令所有 Broker 都成功保存该消息才算是已提交。 +- **持久化**:Kafka 的数据存储在磁盘上,所以只要写入成功,天然就是持久化的。 +- **只要还有一个副本是存活的,那么已提交的消息就不会丢失**。 +- **消费者只能读取已提交的消息**。 + +**Kafka 的副本机制是 kafka 可靠性保证的核心**。 + +Kafka 的主题被分为多个分区,分区是基本的数据块。每个分区可以有多个副本,有一个是 Leader(主副本),其他是 Follower(从副本)。所有数据都直接发送给 Leader,或者直接从 Leader 读取事件。Follower 只需要与 Leader 保持同步,并及时复制最新的数据。当 Leader 宕机时,从 Follower 中选举一个成为新的 Leader。 + +Broker 有 3 个配置参数会影响 Kafka 消息存储的可靠性。 + +- **副本数** - **`replication.factor` 的作用是设置每个分区的副本数**。`replication.factor` 是主题级别配置; `default.replication.factor` 是 broker 级别配置。副本数越多,数据可靠性越高;但由于副本数增多,也会增加同步副本的开销,可能会降低集群的可用性。一般,建议设为 3,这也是 Kafka 的默认值。 +- **不完全的选主** - `unclean.leader.election.enable` 用于控制是否支持不同步的副本参与选举 Leader。`unclean.leader.election.enable` 是 broker 级别(实际上是集群范围内)配置,默认值为 true。 + - 如果设为 true,代表着**允许不同步的副本成为主副本**(即不完全的选举),那么将**面临丢失消息的风险**; + - 如果设为 false,就要**等待原先的主副本重新上线**,从而降低了可用性。 +- **最少同步副本** - **`min.insync.replicas` 控制的是消息至少要被写入到多少个副本才算是“已提交”**。`min.insync.replicas` 是主题级别和 broker 级别配置。尽管可以为一个主题配置 3 个副本,但还是可能会出现只有一个同步副本的情况。如果这个同步副本变为不可用,则必须在可用性和数据一致性之间做出选择。Kafka 中,消息只有被写入到所有的同步副本之后才被认为是已提交的。但如果只有一个同步副本,那么在这个副本不可用时,则数据就会丢失。 + - 如果要确保已经提交的数据被已写入不止一个副本,就需要把最小同步副本的设置为大一点的值。 + - 注意:要确保 `replication.factor` > `min.insync.replicas`。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 `replication.factor = min.insync.replicas + 1`。 + +#### 生产阶段不丢消息 + +在生产消息阶段,消息队列一般通过请求确认机制,来保证消息的可靠传递,Kafka 也不例外。 + +Kafka 有三种发送方式:同步、异步、异步回调。同步方式能保证消息不丢失,但性能太差;异步方式发送消息,通常会立即返回,但消息可能丢失。 + +解决生产者丢失消息的方案: + +生产者使用异步回调方式 `producer.send(msg, callback)` 发送消息。callback(回调)能准确地告诉你消息是否真的提交成功了。一旦出现消息提交失败的情况,你就可以有针对性地进行处理。 + +- 如果是因为那些瞬时错误,那么仅仅让 Producer 重试就可以了; +- 如果是消息不合格造成的,那么可以调整消息格式后再次发送。 + +然后,需要基于以下几点来保证 Kafka 生产者的可靠性: + +- **ACK** - 生产者可选的确认模式有三种:`acks=0`、`acks=1`、`acks=all`。 + - `acks=0`、`acks=1` 都有丢失数据的风险。 + - `acks=all` 意味着会等待所有同步副本都收到消息。再结合 `min.insync.replicas` ,就可以决定在得到确认响应前,至少有多少副本能够收到消息。这是最保险的做法,但也会降低吞吐量。 +- **重试** - 如果 broker 返回的错误可以通过**重试**来解决,生产者会自动处理这些错误。需要注意的是:有时可能因为网络问题导致没有收到确认,但实际上消息已经写入成功。生产者会认为出现临时故障,重试发送消息,这样就会出现重复记录。所以,尽可能在业务上保证幂等性。设置 `retries` 为一个较大的值。这里的 `retries` 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 retries > 0 的 Producer 能够自动重试消息发送,避免消息丢失。 + - **可重试错误**,如:`LEADER_NOT_AVAILABLE`,主副本不可用,可能过一段时间,集群就会选举出新的主副本,重试可以解决问题。 + - **不可重试错误**,如:`INVALID_CONFIG`,即使重试,也无法改变配置选项,重试没有意义。 +- **错误处理** - 开发者需要自行处理的错误: + - 不可重试的 broker 错误,如消息大小错误、认证错误等; + - 消息发送前发生的错误,如序列化错误; + - 生产者达到重试次数上限或消息占用的内存达到上限时发生的错误。 + +#### 消费阶段不丢消息 + +前文已经提到,**消费者只能读取已提交的消息**。这就保证了消费者接收到消息时已经具备了数据一致性。 + +消费者唯一要做的是确保哪些消息是已经读取过的,哪些是没有读取过的(通过提交偏移量给 Broker 来确认)。如果消费者提交了偏移量却未能处理完消息,那么就有可能造成消息丢失,这也是消费者丢失消息的主要原因。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200727140159.png) + +消费者的可靠性配置: + +- `group.id` - 如果希望消费者可以看到主题的所有消息,那么需要为它们设置唯一的 `group.id`。 +- `auto.offset.reset` - 有两个选项: + - `earliest` - 消费者会从分区的开始位置读取数据 + - `latest` - 消费者会从分区末尾位置读取数据 +- `enable.auto.commit` - 消费者自动提交偏移量。如果设为 true,处理流程更简单,但无法保证重复处理消息。 +- `auto.commit.interval.ms` - 自动提交的频率,默认为每 5 秒提交一次。 + +如果 `enable.auto.commit` 设为 true,即自动提交,就无需考虑提交偏移量的问题。 + +如果选择显示提交偏移量,需要考虑以下问题: + +- 必须在处理完消息后再发送确认(提交偏移量),不要收到消息立即确认。 +- 提交频率是性能和重复消息数之间的权衡 +- 分区再均衡 +- 消费可能需要重试机制 +- 超时处理 +- 消费者可能需要维护消费状态,如:处理完消息后,记录在数据库中。 +- 幂等性设计 + - 写数据库:根据主键判断记录是否存在 + - 写 Redis:set 操作天然具有幂等性 + - 复杂的逻辑处理,则可以在消息中加入全局 ID + +::: + +### 【高级】如何保证 Kafka 消息不重复? + +:::details 要点 + +在 MQTT 协议中,给出了三种传递消息时能够提供的服务质量标准,这三种服务质量从低到高依次是: + +- **At most once**:至多一次。消息在传递时,最多会被送达一次。换一个说法就是,没什么消息可靠性保证,允许丢消息。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。 +- **At least once**: 至少一次。消息在传递时,至少会被送达一次。也就是说,不允许丢消息,但是允许有少量重复消息出现。 +- **Exactly once**:恰好一次。消息在传递时,只会被送达一次,不允许丢失也不允许重复,这个是最高的等级。 + +绝大部分消息队列提供的服务质量都是 At least once,包括 RocketMQ、RabbitMQ 和 Kafka 都是这样。也就是说,消息队列很难保证消息不重复。 + +一般解决重复消息的办法是,在消费端,**保证消费消息的操作具备幂等性**。 + +**幂等**(idempotent、idempotence)是一个数学与计算机学概念,指的是:**一个幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。** + +常用的实现幂等操作的方法: + +- **利用数据库的唯一约束实现幂等** - 关系型数据库可以使用 `INSERT IF NOT EXIST` 语句防止重复;Redis 可以使用 `SETNX` 命令来防止重复;其他数据库只要支持类似语义,也是一个道理。 +- **为更新的数据设置前置条件** - 如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。这样,重复执行这个操作时,由于第一次更新数据的时候已经变更了前置条件中需要判断的数据,不满足前置条件,则不会重复执行更新数据操作。但是,如果我们要更新的数据不是数值,或者我们要做一个比较复杂的更新操作怎么办?用什么作为前置判断条件呢?更加通用的方法是,给数据增加一个版本号属性,每次更数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 +1,一样可以实现幂等更新。 +- **记录并检查操作**- 也称为“Token 机制或者 GUID(全局唯一 ID)机制”,通用性最强,适用范围最广。实现的思路特别简单,在执行数据更新操作之前,先检查一下是否执行过这个更新操作。 + - 具体的实现方法是,在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。 + - 需要注意的是,“检查消费状态,然后更新数据并且设置消费状态”中,三个操作必须作为一组操作保证原子性,才能真正实现幂等,否则就会出现 Bug。这一组操作可以通过分布式事务或分布式锁来保证其原子性。 + +::: + +### 【高级】如何保证 Kafka 消息有序? + +:::details 要点 + +某些场景下,可能会要求按序发送消息。 + +方案一、单 Partition + +Kafka 每一个 Partition 只能隶属于消费者群组中的一个 Consumer,换句话说,每个 Partition 只能被一个 Consumer 消费。所以,如果 Topic 是单 Partition,自然是有序的。 + +方案分析 + +优点:简单粗暴。开发者什么也不用做。 + +缺点:**Kafka 基于 Partition 实现其高并发**能力,如果使用单 Partition,会严重限制 Kafka 的吞吐量。 + +结论:作为分布式消息引擎,限制并发能力,显然等同于自废武功,所以,这个方案几乎是不可接受的。 + +方案二、同一个 key 的消息发送给指定 Partition + +(1)生产者端显示指定 key 发往一个指定的 Partition,就可以保证同一个 key 在这个 Partition 中是有序的。 + +(2)接下来,消费者端为每个 key 设定一个缓存队列,然后让一个独立线程负责消费指定 key 的队列,这就保证了消费消息也是有序的。 + +::: + +### 【高级】如何应对 Kafka 消息积压? + +:::details 要点 + +先修复消费者,然后停掉当前所有消费者。 + +新建 Topic,扩大分区,以提高并发处理能力。 + +创建临时消费者程序,并部署在多节点上,扩大消费处理能力。 + +最后处理完积压消息后,恢复原先部署架构。 + +::: + +## 事务 + +### 【中级】Kafka 是否支持事务?如何支持事务? + +:::details 要点 + +**Kafka 的事务概念是指一系列的生产者生产消息和消费者提交偏移量的操作在一个事务,或者说是是一个原子操作),同时成功或者失败**。 + +消息可靠性保障,由低到高为: + +- 最多一次(at most once):消息可能会丢失,但绝不会被重复发送。 +- 至少一次(at least once):消息不会丢失,但有可能被重复发送。 +- 精确一次(exactly once):消息不会丢失,也不会被重复发送。 + +Kafka 支持事务功能主要是为了实现精确一次处理语义的,而精确一次处理是实现流处理的基石。 + +Kafka 自 0.11 版本开始提供了对事务的支持,目前主要是在 read committed 隔离级别上做事情。它能**保证多条消息原子性地写入到目标分区,同时也能保证 Consumer 只能看到事务成功提交的消息**。 + +#### 事务型 Producer + +事务型 Producer 能够保证将消息原子性地写入到多个分区中。这批消息要么全部写入成功,要么全部失败。另外,事务型 Producer 也不惧进程的重启。Producer 重启回来后,Kafka 依然保证它们发送消息的精确一次处理。 + +**事务属性实现前提是幂等性**,即在配置事务属性 `transaction.id` 时,必须还得配置幂等性;但是幂等性是可以独立使用的,不需要依赖事务属性。 + +在事务属性之前先引入了生产者幂等性,它的作用为: + +- **生产者多次发送消息可以封装成一个原子操作**,要么都成功,要么失败。 +- consumer-transform-producer 模式下,因为消费者提交偏移量出现问题,导致**重复消费**。需要将这个模式下消费者提交偏移量操作和生产者一系列生成消息的操作封装成一个原子操作。 + +**消费者提交偏移量导致重复消费消息的场景**:消费者在消费消息完成提交便宜量 o2 之前挂掉了(假设它最近提交的偏移量是 o1),此时执行再均衡时,其它消费者会重复消费消息(o1 到 o2 之间的消息)。 + +#### Kafka 事务相关配置 + +使用 kafka 的事务 api 时的一些注意事项: + +- 需要消费者的自动模式设置为 false,并且不能子再手动的进行执行 `consumer#commitSync` 或者 `consumer#commitAsyc` +- 设置 Producer 端参数 `transctional.id`。最好为其设置一个有意义的名字。 +- 和幂等性 Producer 一样,开启 `enable.idempotence = true`。如果配置了 `transaction.id`,则此时 `enable.idempotence` 会被设置为 true +- 消费者需要配置事务隔离级别 `isolation.level`。在 `consume-trnasform-produce` 模式下使用事务时,必须设置为 `READ_COMMITTED`。 + - `read_uncommitted`:这是默认值,表明 Consumer 能够读取到 Kafka 写入的任何消息,不论事务型 Producer 提交事务还是终止事务,其写入的消息都可以读取。很显然,如果你用了事务型 Producer,那么对应的 Consumer 就不要使用这个值。 + - `read_committed`:表明 Consumer 只会读取事务型 Producer 成功提交事务写入的消息。当然了,它也能看到非事务型 Producer 写入的所有消息。 + +::: + +## 架构 + +### 【高级】Kafka 的数据存储在磁盘上,为什么还能这么快? + +:::details 要点 + +说 Kafka 很快时,他们通常指的是 Kafka 高效移动大量数据的能力。 + +Kafka 为了提高传输效率,做了很多精妙的设计。 + +核心设计: + +- **顺序 I/O** - 磁盘读写有两种方式:顺序读写或者随机读写。在顺序读写的情况下,磁盘的顺序读写速度和内存接近。因为磁盘是机械结构,每次读写都会寻址写入,其中寻址是一个“机械动作”。Kafka 利用了一种分段式的、只追加 (Append-Only) 的日志,基本上把自身的读写操作限制为**顺序 I/O**,也就使得它在各种存储介质上能有很快的速度。 +- **零拷贝** - Kafka 数据传输是一个从网络到磁盘,再由磁盘到网络的过程。在网络和磁盘之间传输数据时,消除多余的复制是提高效率的关键。**Kafka 利用零拷贝技术来消除传输过程中的多余复制**。 + - 如果不采用零拷贝,Kafka 将数据同步给消费者的大致流程是: + 1. 从磁盘加载数据到 os buffer + 2. 拷贝数据到 app buffer + 3. 再拷贝数据到 socket buffer + 4. 接下来,将数据拷贝到网卡 buffer + 5. 最后,通过网络传输,将数据发送到消费者 + - 采用零拷贝技术,Kafka 使用 `sendfile()` 系统方法,将数据从 os buffer 直接复制到网卡 buffer。这个过程中,唯一一次复制数据是从 os buffer 到网卡 buffer。这个复制过程是通过 DMA(Direct Memory Access,直接内存访问) 完成的。使用 DMA 时,CPU 不参与,这使得它非常高效。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070727055.webp) + +其他设计: + +- **页缓存** - Kafka 的数据并不是实时的写入磁盘,它充分利用了现代操作系统分页存储来利用内存提高 I/O 效率。具体来说,就是把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问。Kafka 接收来自 socket buffer 的网络数据,应用进程不需要中间处理、直接进行持久化时。可以使用 mmap 内存文件映射。 +- **压缩** - Kafka 内置了几种压缩算法,并允许定制化压缩算法。通过压缩算法,可以有效减少传输数据的大小,从而提升传输效率。 +- **批处理** - Kafka 的 Clients 和 Brokers 会把多条读写的日志记录合并成一个批次,然后才通过网络发送出去。日志记录的批处理通过使用更大的包以及提高带宽效率来摊薄网络往返的开销。 +- **分区** - Kafka 将 Topic 分区,每个分区对应一个名为的 Log 的磁盘目录,而 Log 又根据大小,可以分为多个 Log Segment 文件。这种分而治之的策略,使得 Kafka 可以**并发**读,以支撑非常高的吞吐量。此外,Kafka 支持负载均衡机制,将数据分区近似均匀地分配给消费者群组的各个消费者。 + +::: + +## 参考资料 + +- [聊聊 Kafka: Kafka 为啥这么快?](https://xie.infoq.cn/article/49bc80d683c373db93d017a99) \ No newline at end of file diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/README.md" index 389f56fe17..5f9db8e3db 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/01.Kafka/README.md" @@ -9,7 +9,7 @@ categories: tags: - MQ - Kafka -permalink: /pages/328f1c/ +permalink: /pages/260fb327/ hidden: true index: false --- @@ -22,43 +22,16 @@ index: false ## 📖 内容 -### [Kafka 快速入门](01.Kafka快速入门.md) - -### [Kafka 生产者](02.Kafka生产者.md) - -### [Kafka 消费者](03.Kafka消费者.md) - -### [Kafka 集群](04.Kafka集群.md) - -### [Kafka 可靠传输](05.Kafka可靠传输.md) - -### [Kafka 存储](06.Kafka存储.md) - -### [Kafka 流式处理](07.Kafka流式处理.md) - -### [Kafka 运维](08.Kafka运维.md) - -## 📚 资料 - -- **官方** - - [Kafka 官网](http://kafka.apache.org/) - - [Kafka Github](https://github.com/apache/kafka) - - [Kafka 官方文档](https://kafka.apache.org/documentation/) - - [Kafka Confluent 官网](http://kafka.apache.org/) - - [Kafka Jira](https://issues.apache.org/jira/projects/KAFKA?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page) -- **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) - - [《深入理解 Kafka:核心设计与实践原理》](https://item.jd.com/12489649.html) - - [《Kafka 技术内幕》](https://item.jd.com/12234113.html) -- **教程** - - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - - [消息队列高手课](https://time.geekbang.org/column/intro/100032301) -- **文章** - - [Introduction and Overview of Apache Kafka](https://www.slideshare.net/mumrah/kafka-talk-tri-hug) - - [The Log: What every software engineer should know about real-time data’s unifying abstraction](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying) - - [《日志:每个软件工程师都应该知道的有关实时数据的统一抽象》](https://github.com/oldratlee/translations/blob/master/log-what-every-software-engineer-should-know-about-real-time-datas-unifying/README.md) - 上面文章的译文 +- [Kafka 快速入门](Kafka快速入门.md) +- [Kafka 生产](Kafka生产.md) +- [Kafka 消费](Kafka消费.md) +- [Kafka 集群](Kafka集群.md) +- [Kafka 可靠传输](Kafka可靠传输.md) +- [Kafka 存储](Kafka存储.md) +- [Kafka 流式处理](Kafka流式处理.md) +- [Kafka 运维](Kafka运维.md) +- [Kafka 面试](Kafka面试.md) 💯 ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/README.md" index df28820712..15a6f2bac1 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/README.md" @@ -8,7 +8,7 @@ categories: - RocketMQ tags: - MQ -permalink: /pages/13dc3a/ +permalink: /pages/4b74671e/ hidden: true index: false --- @@ -17,8 +17,8 @@ index: false ## 📖 内容 -- [RocketMQ 快速入门](01.RocketMQ快速入门.md) -- [RocketMQ 基本原理](02.RocketMQ基本原理.md) +- [RocketMQ 快速入门](RocketMQ快速入门) +- [RocketMQ 基本原理](RocketMQ基本原理) ## 📚 资料 @@ -29,4 +29,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/99.RocketMQFaq.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQFaq.md" similarity index 98% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/99.RocketMQFaq.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQFaq.md" index 0019fa0e9f..72087d8bca 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/99.RocketMQFaq.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQFaq.md" @@ -12,7 +12,7 @@ tags: - 中间件 - MQ - RocketMQ -permalink: /pages/518800/ +permalink: /pages/5a9aee9a/ --- # RocketMQ FAQ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/02.RocketMQ\345\237\272\346\234\254\345\216\237\347\220\206.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQ\345\237\272\346\234\254\345\216\237\347\220\206.md" similarity index 99% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/02.RocketMQ\345\237\272\346\234\254\345\216\237\347\220\206.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQ\345\237\272\346\234\254\345\216\237\347\220\206.md" index 0c8923633f..5523cf4dde 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/02.RocketMQ\345\237\272\346\234\254\345\216\237\347\220\206.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQ\345\237\272\346\234\254\345\216\237\347\220\206.md" @@ -12,7 +12,7 @@ tags: - 中间件 - MQ - RocketMQ -permalink: /pages/36eab6/ +permalink: /pages/546c5f8a/ --- # RocketMQ 基本原理 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/01.RocketMQ\345\277\253\351\200\237\345\205\245\351\227\250.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQ\345\277\253\351\200\237\345\205\245\351\227\250.md" similarity index 99% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/01.RocketMQ\345\277\253\351\200\237\345\205\245\351\227\250.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQ\345\277\253\351\200\237\345\205\245\351\227\250.md" index 15a81e8a02..d651e6ae7a 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/01.RocketMQ\345\277\253\351\200\237\345\205\245\351\227\250.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/02.RocketMQ/RocketMQ\345\277\253\351\200\237\345\205\245\351\227\250.md" @@ -12,7 +12,7 @@ tags: - 中间件 - MQ - RocketMQ -permalink: /pages/d404be/ +permalink: /pages/e3a757f7/ --- # RocketMQ 快速入门 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/99.\345\205\266\344\273\226MQ/01.ActiveMQ.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/99.\345\205\266\344\273\226MQ/ActiveMQ.md" similarity index 99% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/99.\345\205\266\344\273\226MQ/01.ActiveMQ.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/99.\345\205\266\344\273\226MQ/ActiveMQ.md" index ff676edf64..03a09d5b9c 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/99.\345\205\266\344\273\226MQ/01.ActiveMQ.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/99.\345\205\266\344\273\226MQ/ActiveMQ.md" @@ -12,7 +12,7 @@ tags: - 中间件 - MQ - ActiveMQ -permalink: /pages/5aee88/ +permalink: /pages/d756a310/ --- # ActiveMQ 快速入门 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/README.md" index c26c28238e..2eb4ffab72 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.MQ/README.md" @@ -9,7 +9,7 @@ tags: - Java - 中间件 - MQ -permalink: /pages/dfe847/ +permalink: /pages/001529b0/ hidden: true index: false --- @@ -20,33 +20,34 @@ index: false > > 消息队列主要解决应用耦合,异步消息,流量削锋等问题,实现高性能,高可用,可伸缩和最终一致性架构。是大型分布式系统不可缺少的中间件。 > -> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://dunwu.github.io/waterdrop/pages/1fd240/) ,有助于理解消息队列特性的实现和设计思路。 +> 如果想深入学习各种消息队列产品,建议先了解一下 [消息队列基本原理](https://dunwu.github.io/waterdrop/pages/214c1fa6/) ,有助于理解消息队列特性的实现和设计思路。 ## 内容 -### MQ 综合 +### [MQ 综合](00.MQ综合) -- [消息队列面试](00.MQ综合/01.消息队列面试.md) -- [消息队列基本原理](00.MQ综合/02.消息队列基本原理.md) +- [MQ 面试](00.MQ综合/MQ面试.md) -### Kafka +### [Kafka](01.Kafka) -- [Kafka 快速入门](01.Kafka/01.Kafka快速入门.md) -- [Kafka 生产者](01.Kafka/02.Kafka生产者.md) -- [Kafka 消费者](01.Kafka/03.Kafka消费者.md) -- [Kafka 集群](01.Kafka/04.Kafka集群.md) -- [Kafka 可靠传输](01.Kafka/05.Kafka可靠传输.md) -- [Kafka 存储](01.Kafka/06.Kafka存储.md) -- [Kafka 流式处理](01.Kafka/07.Kafka流式处理.md) -- [Kafka 运维](01.Kafka/08.Kafka运维.md) +- [Kafka 快速入门](01.Kafka/Kafka快速入门.md) +- [Kafka 生产](01.Kafka/Kafka生产.md) +- [Kafka 消费](01.Kafka/Kafka消费.md) +- [Kafka 集群](01.Kafka/Kafka集群.md) +- [Kafka 可靠传输](01.Kafka/Kafka可靠传输.md) +- [Kafka 存储](01.Kafka/Kafka存储.md) +- [Kafka 流式处理](01.Kafka/Kafka流式处理.md) +- [Kafka 运维](01.Kafka/Kafka运维.md) -### RocketMQ +### [RocketMQ](02.RocketMQ) -- [RocketMQ](02.RocketMQ/01.RocketMQ快速入门.md) +- [RocketMQ 快速入门](02.RocketMQ/RocketMQ快速入门.md) +- [RocketMQ 基本原理](02.RocketMQ/RocketMQ基本原理.md) +- [RocketMQ Faq](02.RocketMQ/RocketMQFaq.md) ### 其他 MQ -- [ActiveMQ](99.其他MQ/01.ActiveMQ.md) +- [ActiveMQ](99.其他MQ/ActiveMQ.md) ## 技术对比 @@ -65,18 +66,7 @@ index: false - 后来大家开始用 RabbitMQ,但是确实 erlang 语言阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是确实人家是开源的,比较稳定的支持,活跃度也高; - 不过现在确实越来越多的公司会去用 RocketMQ,确实很不错,毕竟是阿里出品,但社区可能有突然黄掉的风险(目前 RocketMQ 已捐给 [Apache](https://github.com/apache/rocketmq),但 GitHub 上的活跃度其实不算高)对自己公司技术实力有绝对自信的,推荐用 RocketMQ,否则回去老老实实用 RabbitMQ 吧,人家有活跃的开源社区,绝对不会黄。 - 所以**中小型公司**,技术实力较为一般,技术挑战不是特别高,用 RabbitMQ 是不错的选择;**大型公司**,基础架构研发实力较强,用 RocketMQ 是很好的选择。 -- 如果是**大数据领域**的实时计算、日志采集等场景,用 Kafka 是业内标准的,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。 - -## 📚 资料 - -- **Kafka** - - [Kafka Github](https://github.com/apache/kafka) - - [Kafka 官网](http://kafka.apache.org/) - - [Kafka 官方文档](https://kafka.apache.org/documentation/) - - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) -- **ActiveMQ** - - [ActiveMQ 官网](http://activemq.apache.org/) ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/README.md" index 6e2e7019da..55c8a1fb01 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/README.md" @@ -7,7 +7,7 @@ categories: tags: - 分布式 - 分布式通信 -permalink: /pages/3a28d0/ +permalink: /pages/dec12b24/ hidden: true index: false --- @@ -18,43 +18,41 @@ index: false ### RPC -#### RPC 综合 +- [Dubbo 面试](01.RPC/Dubbo面试.md) +- [RPC 面试](01.RPC/RPC面试.md) -- [RPC 基础](01.RPC/00.RPC综合/01.RPC基础.md) -- [RPC 进阶](01.RPC/00.RPC综合/02.RPC进阶.md) -- [RPC 高级](01.RPC/00.RPC综合/03.RPC高级.md) -- [服务注册和发现](01.RPC/00.RPC综合/11.服务注册和发现.md) +### [MQ](02.MQ) -### MQ +#### [MQ 综合](02.MQ/00.MQ综合) -#### MQ 综合 +- [MQ 面试](02.MQ/00.MQ综合/MQ面试.md) -- [消息队列面试](02.MQ/00.MQ综合/01.消息队列面试.md) -- [消息队列基本原理](02.MQ/00.MQ综合/02.消息队列基本原理.md) +#### [Kafka](02.MQ/01.Kafka) -#### Kafka +- [Kafka 快速入门](02.MQ/01.Kafka/Kafka快速入门.md) +- [Kafka 生产](02.MQ/01.Kafka/Kafka生产.md) +- [Kafka 消费](02.MQ/01.Kafka/Kafka消费.md) +- [Kafka 集群](02.MQ/01.Kafka/Kafka集群.md) +- [Kafka 可靠传输](02.MQ/01.Kafka/Kafka可靠传输.md) +- [Kafka 存储](02.MQ/01.Kafka/Kafka存储.md) +- [Kafka 流式处理](02.MQ/01.Kafka/Kafka流式处理.md) +- [Kafka 运维](02.MQ/01.Kafka/Kafka运维.md) -- [Kafka 快速入门](02.MQ/01.Kafka/01.Kafka快速入门.md) -- [Kafka 生产者](02.MQ/01.Kafka/02.Kafka生产者.md) -- [Kafka 消费者](02.MQ/01.Kafka/03.Kafka消费者.md) -- [Kafka 集群](02.MQ/01.Kafka/04.Kafka集群.md) -- [Kafka 可靠传输](02.MQ/01.Kafka/05.Kafka可靠传输.md) -- [Kafka 存储](02.MQ/01.Kafka/06.Kafka存储.md) -- [Kafka 流式处理](02.MQ/01.Kafka/07.Kafka流式处理.md) -- [Kafka 运维](02.MQ/01.Kafka/08.Kafka运维.md) +#### [RocketMQ](02.MQ/02.RocketMQ) + +- [RocketMQ 快速入门](02.MQ/02.RocketMQ/RocketMQ快速入门.md) +- [RocketMQ 基本原理](02.MQ/02.RocketMQ/RocketMQ基本原理.md) +- [RocketMQ Faq](02.MQ/02.RocketMQ/RocketMQFaq.md) #### 其他 MQ -- [ActiveMQ](02.MQ/99.其他MQ/01.ActiveMQ.md) -- [RocketMQ](02.MQ/02.RocketMQ/README.md) +- [ActiveMQ](02.MQ/99.其他MQ/ActiveMQ.md) ### 分布式存储 -- [分布式缓存](../22.分布式存储/01.分布式缓存.md) - 关键词:`进程内缓存`、`分布式缓存`、`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` -- [读写分离](../22.分布式存储/02.读写分离.md) -- [分库分表](../22.分布式存储/03.分库分表.md) - 关键词:`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` - -## 📚 资料 +- [分布式缓存](../22.分布式存储/分布式缓存.md) - 关键词:`进程内缓存`、`分布式缓存`、`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` +- [读写分离](../22.分布式存储/读写分离.md) +- [分库分表](../22.分布式存储/分库分表.md) - 关键词:`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` ## 🚪 传送 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/README.md" index ec0da793f6..20e438e228 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/README.md" @@ -6,8 +6,8 @@ categories: - 分布式存储 tags: - 分布式 - - 分布式通信 -permalink: /pages/42beb6/ + - 分布式存储 +permalink: /pages/04e10fe3/ hidden: true index: false --- @@ -16,11 +16,10 @@ index: false ## 📖 内容 -- [分布式缓存](01.分布式缓存.md) - 关键词:`进程内缓存`、`分布式缓存`、`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` -- [读写分离](02.读写分离.md) -- [分库分表](03.分库分表.md) - 关键词:`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` - -## 📚 资料 +- [分布式缓存](分布式缓存.md) - 关键词:`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` +- [读写分离](读写分离.md) - 关键词:`读写分离` +- [分库分表](分库分表.md) - 关键词:`分库分表`、`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` +- [分布式存储面试](分布式存储面试.md) 💯 ## 🚪 传送 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250\351\235\242\350\257\225.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..ac401dcf87 --- /dev/null +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250\351\235\242\350\257\225.md" @@ -0,0 +1,296 @@ +--- +title: 分布式存储面试 +categories: + - 分布式 + - 分布式存储 +tags: + - 分布式 + - 分布式存储 + - 面试 +--- + +# 分布式存储面试 + +## 缓存 + +> 扩展: +> +> - [《大型网站技术架构:核心原理与案例分析》](https://item.jd.com/11322972.html) +> - [你应该知道的缓存进化史](https://link.juejin.im/?target=https%3A%2F%2Fjuejin.im%2Fpost%2F5b7593496fb9a009b62904fa) +> - [如何优雅的设计和使用缓存?](https://link.juejin.im/?target=https%3A%2F%2Fjuejin.im%2Fpost%2F5b849878e51d4538c77a974a) +> - [理解分布式系统中的缓存架构(上)](https://www.jianshu.com/p/73ce0ef820f9) +> - [缓存那些事](https://tech.meituan.com/2017/03/17/cache-about.html) +> - [分布式之数据库和缓存双写一致性方案解析 ](https://www.cnblogs.com/rjzheng/p/9041659.html) +> - [Cache 的基本原理](https://zhuanlan.zhihu.com/p/102293437) +> - [5 分钟看懂系列:HTTP 缓存机制详解](https://segmentfault.com/a/1190000021716418) +> - [浏览器缓存看这一篇就够了](https://zhuanlan.zhihu.com/p/60950750) + +### 【基础】什么是缓存?为什么需要缓存? + +:::details 要点 + +**缓存就是数据交换的缓冲区,用于将频繁访问的数据暂存在访问速度快的存储介质**。 + +缓存的本质是一种利用**空间换时间**的设计:牺牲一定的数据实时性,使得访问**更快**、**更近**: + +- 将数据存储到读取速度**更快**的存储(设备); +- 将数据存储到**离应用最近**的位置; +- 将数据存储到**离用户最近**的位置。 + +缓存是用于存储数据的硬件或软件的组成部分,以使得后续更快访问相应的数据。缓存中的数据可能是提前计算好的结果、数据的副本等。典型的应用场景:有 cpu cache, 磁盘 cache 等。本文中提及到缓存主要是指互联网应用中所使用的缓存组件。 + +**缓存命中率**是缓存的重要度量指标,命中率越高越好。 + +``` +缓存命中率 = 从缓存中读取次数 / 总读取次数 +``` + +::: + +### 【基础】何时需要缓存? + +:::details 要点 + +引入缓存,会增加系统的复杂度,并牺牲一定的数据实时性。所以,引入缓存前,需要先权衡是否值得,考量点如下: + +- **CPU 开销** - 如果应用某个计算需要消耗大量 CPU,可以考虑缓存其计算结果。典型场景:复杂的、频繁调用的正则计算;分布式计算中间状态等。 +- **IO 开销** - 如果数据库连接池比较繁忙,可以考虑缓存其查询结果。 + +在数据层引入缓存,有以下几个好处: + +- 提升数据读取速度。 +- 提升系统扩展能力,通过扩展缓存,提升系统承载能力。 +- 降低存储成本,Cache+DB 的方式可以承担原有需要多台 DB 才能承担的请求量,节省机器成本。 + +::: + +### 【中级】缓存有哪些分类? + +:::details 要点 + +缓存从部署角度,可以分为客户端缓存和服务端缓存。 + +**客户端缓存** + +- **Http 缓存**:HTTP/1.1 中的 `Cache-Control`、HTTP/1 中的 `Expires` +- **浏览器缓存**:HTML5 提供的 SessionStorage 和 LocalStorage、Cookie +- **APP 缓存** + - Android + - IOS + +**服务端缓存** + +- **CDN 缓存** - CDN 将数据缓存到离用户物理距离最近的服务器,使得用户可以就近获取请求内容。CDN 一般缓存静态资源文件(页面,脚本,图片,视频,文件等)。 +- **反向代理缓存** - 反向代理(Reverse Proxy)方式是指以代理服务器来接受网络连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个反向代理服务器。反向代理缓存一般针对的是静态资源,而将动态资源请求转发到应用服务器处理。 +- **数据库缓存** - 数据库(如 Mysql)自身一般也有缓存,但因为命中率和更新频率问题,不推荐使用。 +- **进程内缓存** - 缓存应用字典等常用数据。 +- **分布式缓存** - 缓存数据库中的热点数据。 + +> 其中,CDN 缓存、反向代理缓存、数据库缓存一般由专职人员维护(运维、DBA)。 +> +> 后端开发一般聚焦于进程内缓存、分布式缓存。 + +::: + +### 【中级】CDN 缓存是如何工作的? + +:::details 要点 + +**CDN 将数据缓存到离用户物理距离最近的服务器,使得用户可以就近获取请求内容。CDN 一般缓存静态资源文件(页面,脚本,图片,视频,文件等)**。 + +国内网络异常复杂,跨运营商的网络访问会很慢。为了解决跨运营商或各地用户访问问题,可以在重要的城市,部署 CDN 应用。使用户就近获取所需内容,降低网络拥塞,提高用户访问响应速度和命中率。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/1559138689425.png) + +#### CDN 缓存原理 + +CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求。 + +(1)未部署 CDN 应用前的网络路径: + +- 请求:本机网络(局域网)=> 运营商网络 => 应用服务器机房 +- 响应:应用服务器机房 => 运营商网络 => 本机网络(局域网) + +在不考虑复杂网络的情况下,从请求到响应需要经过 3 个节点,6 个步骤完成一次用户访问操作。 + +(2)部署 CDN 应用后网络路径: + +- 请求:本机网络(局域网) => 运营商网络 +- 响应:运营商网络 => 本机网络(局域网) + +在不考虑复杂网络的情况下,从请求到响应需要经过 2 个节点,2 个步骤完成一次用户访问操作。 + +与不部署 CDN 服务相比,减少了 1 个节点,4 个步骤的访问。极大的提高了系统的响应速度。 + +#### CDN 特点 + +**优点** + +- **本地 Cache 加速** - 提升访问速度,尤其含有大量图片和静态页面站点; +- **实现跨运营商的网络加速** - 消除了不同运营商之间互联的瓶颈造成的影响,实现了跨运营商的网络加速,保证不同网络中的用户都能得到良好的访问质量; +- **远程加速** - 远程访问用户根据 DNS 负载均衡技术智能自动选择 Cache 服务器,选择最快的 Cache 服务器,加快远程访问的速度; +- **带宽优化** - 自动生成服务器的远程 Mirror(镜像)cache 服务器,远程用户访问时从 cache 服务器上读取数据,减少远程访问的带宽、分担网络流量、减轻原站点 WEB 服务器负载等功能。 +- **集群抗攻击** - 广泛分布的 CDN 节点加上节点之间的智能冗余机制,可以有效地预防黑客入侵以及降低各种 D.D.o.S 攻击对网站的影响,同时保证较好的服务质量。 + +**缺点** + +- **不适宜缓存动态资源** + - 解决方案:主要缓存静态资源,动态资源建立多级缓存或准实时同步; +- **存在数据的一致性问题** + - 解决方案(主要是在性能和数据一致性二者间寻找一个平衡) + - 设置缓存失效时间(1 个小时,过期后同步数据)。 + - 针对资源设置版本号。 + +::: + +### 【中级】反向代理缓存是如何工作的? + +:::details 要点 + +**反向代理(Reverse Proxy)方式是指以代理服务器来接受网络连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个反向代理服务器。** + +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/web/nginx/reverse-proxy.png) + +反向代理位于应用服务器同一网络,处理所有对 WEB 服务器的请求。 + +反向代理缓存的原理: + +- 如果用户请求的页面在代理服务器上有缓存的话,代理服务器直接将缓存内容发送给用户。 +- 如果没有缓存则先向 WEB 服务器发出请求,取回数据,本地缓存后再发送给用户。 + +这种方式通过降低向 WEB 服务器的请求数,从而降低了 WEB 服务器的负载。 + +**反向代理缓存一般针对的是静态资源,而将动态资源请求转发到应用服务器处理**。常用的缓存应用服务器有 Varnish,Ngnix,Squid。 + +::: + +### 【中级】缓存有哪些淘汰算法? + +> 扩展: +> +> [Cache Replacement Policies - RR, FIFO, LIFO, & Optimal](https://www.youtube.com/watch?v=7lxAfszjy68&list=PLBlnK6fEyqRjdT1xkkBZSXKwFKqQoYhwy&index=23) +> +> [Cache Replacement Policies - MRU, LRU, Pseudo-LRU, & LFU](https://www.youtube.com/watch?v=_Hh-NcdbHCY&list=PLBlnK6fEyqRjdT1xkkBZSXKwFKqQoYhwy&index=25) + +:::details 要点 + +缓存一般存于访问速度较快的存储介质,快也就意味着资源昂贵并且有限。正所谓,好钢要用在刀刃上。因此,缓存要合理利用,需要设定一些机制,将一些访问频率偏低或过期的数据淘汰。 + +淘汰缓存首先要做的是,确定什么时候触发淘汰缓存,一般有以下几个思路: + +- **基于空间** - 设置缓存空间大小。 +- **基于容量** - 设置缓存存储记录数。 +- **基于时间** + - **TTL(Time To Live,即存活期)** - 缓存数据从创建到过期的时间。 + - **TTI(Time To Idle,即空闲期)** - 缓存数据多久没被访问的时间。 + +接下来,就要确定如何淘汰缓存,常见的缓存淘汰算法有以下几个: + +- **FIFO(First In First Out,先进先出)** - 淘汰最先进入的缓存数据。缓存的行为就像一个队列。 + - 优点:这种方案非常简单 + - 缺点:可能会导致**缓存命中率低**。因为,进入缓存的先后顺序和访问频率无关,这种算法可能会将访问频率高的数据给淘汰。 +- **LIFO(Last In First Out,后进先出)** - 淘汰最后进入的缓存数据。缓存的行为就像一个栈。 + - 优点:这种方案非常简单 + - 缺点:和 FIFO 一样,也可能会导致**缓存命中率低**。因为,进入缓存的先后顺序和访问频率无关,这种算法可能会将访问频率高的数据给淘汰。 +- **MRU(Most Recently Used,最近最多使用)** - 淘汰最近最多使用缓存。 + - 优点:适用于一些特殊场景,例如数据访问具有较强的局部性。举个例子,用户访问一个信息流页面,已经看过的内容,他肯定不想再看到,此时就可以使用 MRU。 + - 缺点:某些情况下,可能会导致频繁的淘汰缓存,从而降低缓存命中率 +- **LRU(Least Recently Used,最近最少使用)** - 淘汰最近最少使用缓存。 + - 优点:避免了 FIFO **缓存命中率低**的问题。 + - 缺点:存在**临界区**问题。假设,缓存只保留 1 分钟以内的热点数据。如果有个数据在 1 个小时的前 59 分钟访问了 1 万次(可见这是个热点数据),最后一分钟没有任何访问;而其他数据有被访问,就会导致这个热点数据被淘汰。 +- **LFU(Less Frequently Used,最近最少频率使用)** - 该算法对 LRU 做了进一步优化:利用额外的空间记录每个数据的使用频率,然后淘汰使用频率最低的数据,如果所有数据使用频率相同,可以用 FIFO 淘汰最早的缓存数据。 + - 优点:解决了 LRU 的**临界区**问题。 + - 缺点:记录使用频率,会产生额外的空间开销 + +::: + +### 【高级】缓存更新有哪些策略? + +:::details 要点 + + + +[![top 5 caching strategies for System design interviews](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3smq5msfo852zeoej5iz.jpg)](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F3smq5msfo852zeoej5iz.jpg) + +一般来说,系统如果不是严格要求缓存和数据库保持一致性的话,尽量不要将**读请求和写请求串行化**。串行化可以保证一定不会出现数据不一致的情况,但是它会导致系统的吞吐量大幅度下降。缓存更新的常见策略有以下几种: + +- Cache Aside +- Wirte Through +- Read Though +- Wirte Behind + +需要注意的是:以上几种缓存更新策略,都无法保证数据强一致。如果一定要保证强一致性,可以通过两阶段提交(2PC)或 Paxos 协议来实现。但是 2PC 太慢,而 Paxos 太复杂,所以如果不是非常重要的数据,不建议使用强一致性方案。 + +#### Cache Aside + +#### Wirte Through + +#### Read Though + +#### Wirte Behind + +::: + +### 【高级】多级缓存架构如何设计? + +:::details 要点 + +::: + +### 【中级】什么是缓存穿透?如何应对? + +:::details 要点 + +::: + +### 【中级】什么是缓存击穿?如何应对? + +:::details 要点 + +::: + +### 【中级】什么是缓存雪崩?如何应对? + +:::details 要点 + +::: + +### 【中级】什么是缓存预热?如何预热? + +:::details 要点 + +::: + +## 读写分离 + +### 【基础】什么是读写分离?为什么需要读写分离? + +:::details 要点 + +::: + +### 【中级】如何实现读写分离? + +:::details 要点 + +::: + +## 分库分表 + +### 【基础】什么是分库分表?为什么需要分库分表? + +:::details 要点 + +::: + +### 【高级】如何实现分库分表? + +:::details 要点 + +::: + +### 【高级】分库分表后,如何应对扩容和迁移? + +:::details 要点 + +::: diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" similarity index 88% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" index 318e5a3bc1..d131f5c1cb 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\270\203\345\274\217\347\274\223\345\255\230.md" @@ -7,9 +7,9 @@ categories: - 分布式存储 tags: - 分布式 - - 数据调度 + - 分布式存储 - 缓存 -permalink: /pages/fd0aaa/ +permalink: /pages/259682d3/ --- # 缓存基本原理 @@ -36,9 +36,9 @@ permalink: /pages/fd0aaa/ ### 什么是缓存 -**缓存就是数据交换的缓冲区,用于将频繁访问的数据暂存**。**缓存的本质是一个内存 Hash**。 +**缓存就是数据交换的缓冲区,用于将频繁访问的数据暂存在访问速度快的存储介质**。 -缓存的本质是一种利用空间换时间的设计:牺牲一定的数据实时性,使得访问**更快**、**更近**: +缓存的本质是一种利用**空间换时间**的设计:牺牲一定的数据实时性,使得访问**更快**、**更近**: - 将数据存储到读取速度**更快**的存储(设备); - 将数据存储到**离应用最近**的位置; @@ -69,31 +69,15 @@ permalink: /pages/fd0aaa/ 根据业务场景,通常缓存有以下几种使用方式: -- 懒汉式(读时触发):先查询 DB 里的数据, 然后把相关的数据写入 Cache。 -- 饥饿式(写时触发):写入 DB 后, 然后把相关的数据也写入 Cache。 +- 懒汉式(读时触发):先查询 DB 里的数据,然后把相关的数据写入 Cache。 +- 饥饿式(写时触发):写入 DB 后,然后把相关的数据也写入 Cache。 - 定期刷新:适合周期性的跑数据的任务,或者列表型的数据,而且不要求绝对实时性。 -### 缓存淘汰策略 - -缓存淘汰的类型: - -- **基于空间** - 设置缓存空间大小。 -- **基于容量** - 设置缓存存储记录数。 -- **基于时间** - - TTL(Time To Live,即存活期)缓存数据从创建到过期的时间。 - - TTI(Time To Idle,即空闲期)缓存数据多久没被访问的时间。 - -缓存淘汰算法: - -- **FIFO (first in first out)** - **先进先出**。在这种淘汰算法中,先进入缓存的会先被淘汰。这种可谓是最简单的了,但是会导致我们命中率很低。试想一下我们如果有个访问频率很高的数据是所有数据第一个访问的,而那些不是很高的是后面再访问的,那这样就会把我们的首个数据但是他的访问频率很高给挤出。 -- **LRU (least recently used)** - **最近最少使用算法**。这种算法避免了 **FIFO** 命中率不高的问题:每次访问数据都会将其放在我们的队尾,如果需要淘汰数据,就只需要淘汰队首即可。但是这个算法依然有缺点:假设,缓存只保留 1 分钟以内的热点数据。如果有个数据在 1 个小时的前 59 分钟访问了 1 万次(可见这是个热点数据),最后一分钟没有任何访问;但是,其他的数据有被访问,就会导致这个热点数据被淘汰。 -- **LFU (less frequently used)** - **最近最少频率使用**。在这种算法中又对上面进行了优化,利用额外的空间记录每个数据的使用频率,然后选出频率最低进行淘汰。这样就避免了 LRU 不能处理时间段的问题。 - -这三种缓存淘汰算法,实现复杂度一个比一个高,同样的命中率也是一个比一个好。而我们一般来说选择的方案居中即可,即实现成本不是太高,而命中率也还行的 **LRU**。 - ## 缓存的分类 -缓存从部署角度,可以分为客户端缓存和服务端缓存。 +缓存从架构维度来看,可以分为客户端缓存和服务端缓存。 + +缓存从集群维度来看,可以分为进程内缓存和分布式缓存。 **客户端缓存** @@ -173,7 +157,7 @@ CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务 ### 反向代理缓存 -> **反向代理(Reverse Proxy)方式是指以代理服务器来接受 internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 internet 上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。** +> **反向代理(Reverse Proxy)方式是指以代理服务器来接受网络连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个反向代理服务器。** ![img](https://raw.githubusercontent.com/dunwu/images/master/cs/web/nginx/reverse-proxy.png) @@ -233,15 +217,15 @@ CDN 的基本原理是广泛采用各种缓存服务器,将这些缓存服务 Guava Cache 采用了类似 `ConcurrentHashMap` 的思想,分段加锁,减少锁竞争。 -Guava Cache 对于过期的 Entry 并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。 +Guava Cache 对于过期的 Entry 并没有马上过期(也就是并没有后台线程一直在扫),而是通过进行读写操作的时候进行过期处理,这样做的好处是避免后台线程扫描的时候进行全局加锁。 直接通过查询,判断其是否满足刷新条件,进行刷新。 ### Caffeine -Caffeine 实现了 W-TinyLFU(**LFU** + **LRU** 算法的变种),其**命中率和读写吞吐量大大优于 Guava Cache**。 +Caffeine 实现了 W-TinyLFU(**LFU** + **LRU** 算法的变种),其**命中率和读写吞吐量大大优于 Guava Cache**。 -其实现原理较复杂,可以参考[你应该知道的缓存进化史](https://juejin.im/post/5b7593496fb9a009b62904fa#comment)。 +其实现原理较复杂,可以参考 [你应该知道的缓存进化史](https://juejin.im/post/5b7593496fb9a009b62904fa#comment)。 ### Ehcache @@ -271,7 +255,7 @@ EhCache 是一个纯 Java 的进程内缓存框架,具有快速、精干等特 | 比较项 | ConcurrentHashMap | LRUMap | Ehcache | Guava Cache | Caffeine | | ------------ | ----------------- | ------------------------ | ----------------------------- | ----------------------------------- | ----------------------- | | 读写性能 | 很好,分段锁 | 一般,全局加锁 | 好 | 好,需要做淘汰操作 | 很好 | -| 淘汰算法 | 无 | LRU,一般 | 支持多种淘汰算法,LRU,LFU,FIFO | LRU,一般 | W-TinyLFU, 很好 | +| 淘汰算法 | 无 | LRU,一般 | 支持多种淘汰算法,LRU,LFU,FIFO | LRU,一般 | W-TinyLFU, 很好 | | 功能丰富程度 | 功能比较简单 | 功能比较单一 | 功能很丰富 | 功能很丰富,支持刷新和虚引用等 | 功能和 Guava Cache 类似 | | 工具大小 | jdk 自带类,很小 | 基于 LinkedHashMap,较小 | 很大,最新版本 1.4MB | 是 Guava 工具类中的一个小部分,较小 | 一般,最新版本 644KB | | 是否持久化 | 否 | 否 | 是 | 否 | 否 | @@ -394,7 +378,7 @@ Memcached 服务器之间彼此不通信,它的分布式能力是依赖客户 | 数据结构 | 只支持简单的 Key-Value 结构 | String,Hash, List, Set, Sorted Set | String,HashMap, List,Set | | 持久化 | 不支持 | 支持 | 支持 | | 容量大小 | 数据纯内存,数据存储不宜过多 | 数据全内存,资源成本考量不宜超过 100GB | 可以配置全内存或内存+磁盘引擎,数据容量可无限扩充 | -| 读写性能 | 很高 | 很高(RT0.5ms 左右) | String 类型比较高(RT1ms 左右),复杂类型比较慢(RT5ms 左右) | +| 读写性能 | 很高 | 很高 (RT0.5ms 左右) | String 类型比较高 (RT1ms 左右),复杂类型比较慢 (RT5ms 左右) | | 过期策略 | 过期后,不删除缓存 | 有六种策略来处理过期数据 | 支持 | - `MemCache` - 只适合基于内存的缓存框架;且不支持数据持久化和容灾。 @@ -409,7 +393,7 @@ Memcached 服务器之间彼此不通信,它的分布式能力是依赖客户 通常,一个大型软件系统的缓存采用多级缓存方案: -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/缓存整体架构.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/缓存整体架构。png) 请求过程: @@ -425,7 +409,7 @@ Memcached 服务器之间彼此不通信,它的分布式能力是依赖客户 **如果应用服务是单点应用,那么进程内缓存当然是缓存的首选方案**。 -对于进程内缓存,其本来受限于内存的大小的限制,以及进程缓存更新后其他缓存无法得知,所以一般来说进程缓存适用于: +对于进程内缓存,其本来受限于内存的大小的限制,以及进程缓存更新后其他缓存无法得知,所以一般来说进程缓存适用于: - 数据量不是很大且更新频率较低的数据。 - 如果更新频繁的数据,也想使用进程内缓存,那么可以将其过期时间设置为较短的时间,或者设置较短的自动刷新时间。 @@ -462,7 +446,7 @@ Redis 用来存储热点数据,如果缓存不命中,则去查询数据库 #### 多级缓存查询 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/多级缓存2.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/多级缓存 2.png) 多级缓存查询流程如下: @@ -472,19 +456,121 @@ Redis 用来存储热点数据,如果缓存不命中,则去查询数据库 #### 多级缓存更新 -对于 L1 缓存,如果有数据更新,只能删除并更新所在机器上的缓存,其他机器只能通过超时机制来刷新缓存。超时设定可以有两种策略: +对于 L1 缓存,如果有数据更新,只能删除并更新所在机器上的缓存,其他机器只能通过超时机制来刷新缓存。超时设定可以有两种策略: - 设置成写入后多少时间后过期 - 设置成写入后多少时间刷新 对于 L2 缓存,如果有数据更新,其他机器立马可见。但是,也必须要设置超时时间,其时间应该比 L1 缓存的有效时间长。 -为了解决进程内缓存不一致的问题,设计可以进一步优化: +为了解决进程内缓存不一致的问题,设计可以进一步优化: -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/多级缓存3.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/多级缓存 3.png) 通过消息队列的发布、订阅机制,可以通知其他应用节点对进程内缓存进行更新。使用这种方案,即使消息队列服务挂了或不可靠,由于先执行了数据库更新,但进程内缓存过期,刷新缓存时,也能保证数据的最终一致性。 +## 缓存淘汰算法 + +缓存一般存于访问速度较快的存储介质,快也就意味着资源昂贵并且有限。正所谓,好钢要用在刀刃上。因此,缓存要合理利用,需要设定一些机制,将一些访问频率偏低或过期的数据淘汰。 + +淘汰缓存首先要做的是,确定什么时候触发淘汰缓存,一般有以下几个思路: + +- **基于空间** - 设置缓存空间大小。 +- **基于容量** - 设置缓存存储记录数。 +- **基于时间** + - **TTL(Time To Live,即存活期)** - 缓存数据从创建到过期的时间。 + - **TTI(Time To Idle,即空闲期)** - 缓存数据多久没被访问的时间。 + +接下来,就要确定如何淘汰缓存,常见的缓存淘汰算法有以下几个: + +- **FIFO(First In First Out,先进先出)** - 淘汰最先进入的缓存数据。缓存的行为就像一个队列。 + - 优点:这种方案非常简单 + - 缺点:可能会导致**缓存命中率低**。因为,进入缓存的先后顺序和访问频率无关,这种算法可能会将访问频率高的数据给淘汰。 +- **LIFO(Last In First Out,后进先出)** - 淘汰最后进入的缓存数据。缓存的行为就像一个栈。 + - 优点:这种方案非常简单 + - 缺点:和 FIFO 一样,也可能会导致**缓存命中率低**。因为,进入缓存的先后顺序和访问频率无关,这种算法可能会将访问频率高的数据给淘汰。 +- **MRU(Most Recently Used,最近最多使用)** - 淘汰最近最多使用缓存。 + - 优点:适用于一些特殊场景,例如数据访问具有较强的局部性。举个例子,用户访问一个信息流页面,已经看过的内容,他肯定不想再看到,此时就可以使用 MRU。 + - 缺点:某些情况下,可能会导致频繁的淘汰缓存,从而降低缓存命中率 +- **LRU(Least Recently Used,最近最少使用)** - 淘汰最近最少使用缓存。 + - 优点:避免了 FIFO **缓存命中率低**的问题。 + - 缺点:存在**临界区**问题。假设,缓存只保留 1 分钟以内的热点数据。如果有个数据在 1 个小时的前 59 分钟访问了 1 万次(可见这是个热点数据),最后一分钟没有任何访问;而其他数据有被访问,就会导致这个热点数据被淘汰。 +- **LFU(Less Frequently Used,最近最少频率使用)** - 该算法对 LRU 做了进一步优化:利用额外的空间记录每个数据的使用频率,然后淘汰使用频率最低的数据,如果所有数据使用频率相同,可以用 FIFO 淘汰最早的缓存数据。 + - 优点:解决了 LRU 的**临界区**问题。 + - 缺点:记录使用频率,会产生额外的空间开销 + +### 缓存更新 + +一般来说,系统如果不是严格要求缓存和数据库保持一致性的话,尽量不要将**读请求和写请求串行化**。串行化可以保证一定不会出现数据不一致的情况,但是它会导致系统的吞吐量大幅度下降。 + +缓存更新的策略有几种模式: + +- Cache Aside +- Read/Write Through + +需要注意的是:以上几种缓存更新策略,都无法保证数据强一致。如果一定要保证强一致性,可以通过两阶段提交(2PC)或 Paxos 协议来实现。但是 2PC 太慢,而 Paxos 太复杂,所以如果不是非常重要的数据,不建议使用强一致性方案。 + +#### Cache Aside + +Cache Aside 应该是最常见的缓存更新策略了。 + +Cache Aside 的思路是:**先更新数据库,再删除缓存**。具体来说: + +- **失效**:尝试读缓存,如果不命中,则读数据库,然后更新缓存。 + +- **命中**:尝试读缓存,命中则直接返回数据。 + +- **更新**:先更新数据库,再删除缓存。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413101039.png) + +##### 为什么不能先更新数据库,再更新缓存? + +**多个并发的写操作可能导致脏数据**:当有多个并发的写请求时,无法保证更新数据库的顺序和更新缓存的顺序一致,从而导致数据库和缓存数据不一致的问题。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413113825.png) + +> 说明:如上图的场景中,两个写线程由于执行顺序,导致数据库中 val = 2,而缓存中 val = 1,数据不一致。 + +##### 为什么不能先删缓存,再更新数据库? + +**存在并发读请求和写请求时,可能导致脏数据**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413113940.png) + +> 说明:如上图的场景中,读线程和写线程并行执行,导致数据库中 val = 2,而缓存中 val = 1,数据不一致。 + +##### 先更新数据库,再删除缓存就没问题了吗 + +**存在并发读请求和写请求时,可能导致脏数据**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413115140.png) + +> 上图中问题发生的概率非常低:因为通常数据库更新操作比内存操作耗时多出几个数量级,最后一步回写缓存速度非常快,通常会在更新数据库之前完成。所以 Cache Aside 模式选择先更新数据库,再删除缓存,而不是先删缓存,再更新数据库。 +> +> 不过,如果真的出现了这种场景,为了避免缓存中一直保留着脏数据,可以为缓存设置过期时间,过期后缓存自动失效。通常,业务系统中允许少量数据短时间出现不一致的情况。 + +#### Read/Write Through + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413101029.png) + +Read Through 的思路是:**查询时更新缓存**。当缓存失效时,缓存服务自己进行加载。 + +Write Through 的思路是:当数据更新时,缓存服务负责更新缓存。 + +Through vs. Cache Aside + +Read Through vs. Cache Aside + +- Cache Aside 模式中,应用需要维护两个数据源头:一个是缓存,一个是数据库。 +- Read-Through 模式中,应用无需管理缓存和数据库,只需要将数据库的同步委托给缓存服务即可。 + +#### Write behind + +Write Behind 又叫 Write Back。Write Behind 的思路是:应用更新数据时,只更新缓存, 缓存服务每隔一段时间将缓存数据批量更新到数据库中,即延迟写入。这个设计的好处就是让提高 I/O 效率,因为异步,Write Behind 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。 + +> 更详细的分析可以参考:[分布式之数据库和缓存双写一致性方案解析 ](https://www.cnblogs.com/rjzheng/p/9041659.html) + ## 缓存问题 ### 缓存雪崩 @@ -517,13 +603,13 @@ Redis 用来存储热点数据,如果缓存不命中,则去查询数据库 **对于返回为 NULL 的依然缓存,对于抛出异常的返回不进行缓存**。 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/缓存穿透1.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/缓存穿透 1.png) 采用这种手段的会增加我们缓存的维护成本,需要在插入缓存的时候删除这个空缓存,当然我们可以通过设置较短的超时时间来解决这个问题。 (二)过滤不可能存在的数据 -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/缓存穿透2.png) +![img](https://raw.githubusercontent.com/dunwu/images/master/cs/java/javaweb/technology/cache/缓存穿透 2.png) **制定一些规则过滤一些不可能存在的数据**。可以使用布隆过滤器(针对二进制操作的数据结构,所以性能高),比如你的订单 ID 明显是在一个范围 1-1000,如果不是 1-1000 之内的数据那其实可以直接给过滤掉。 @@ -539,7 +625,7 @@ Redis 用来存储热点数据,如果缓存不命中,则去查询数据库 **“缓存击穿”是指,热点缓存数据失效瞬间,大量请求直接访问数据库**。例如,某些 key 是热点数据,访问非常频繁。如果某个 key 失效的瞬间,大量的请求过来,缓存未命中,然后去数据库访问,此时数据库访问量会急剧增加。 -为了避免这个问题,我们可以采取下面的两个手段: +为了避免这个问题,我们可以采取下面的两个手段: - **分布式锁** - 锁住热点数据的 key,避免大量线程同时访问同一个 key。 - **定时异步刷新** - 可以对部分数据采取失效前自动刷新的策略,而不是到期自动淘汰。淘汰其实也是为了数据的时效性,所以采用自动刷新也可以。 @@ -585,78 +671,6 @@ Redis 用来存储热点数据,如果缓存不命中,则去查询数据库 采用**懒加载**。对于热点数据,可以设置较短的缓存时间,并定期异步加载。 -### 缓存更新 - -一般来说,系统如果不是严格要求缓存和数据库保持一致性的话,尽量不要将**读请求和写请求串行化**。串行化可以保证一定不会出现数据不一致的情况,但是它会导致系统的吞吐量大幅度下降。 - -缓存更新的策略有几种模式: - -- Cache Aside -- Read/Write Through - -需要注意的是:以上几种缓存更新策略,都无法保证数据强一致。如果一定要保证强一致性,可以通过两阶段提交(2PC)或 Paxos 协议来实现。但是 2PC 太慢,而 Paxos 太复杂,所以如果不是非常重要的数据,不建议使用强一致性方案。 - -#### Cache Aside - -Cache Aside 应该是最常见的缓存更新策略了。 - -Cache Aside 的思路是:**先更新数据库,再删除缓存**。具体来说: - -- **失效**:尝试读缓存,如果不命中,则读数据库,然后更新缓存。 - -- **命中**:尝试读缓存,命中则直接返回数据。 - -- **更新**:先更新数据库,再删除缓存。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413101039.png) - -##### 为什么不能先更新数据库,再更新缓存? - -**多个并发的写操作可能导致脏数据**:当有多个并发的写请求时,无法保证更新数据库的顺序和更新缓存的顺序一致,从而导致数据库和缓存数据不一致的问题。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413113825.png) - -> 说明:如上图的场景中,两个写线程由于执行顺序,导致数据库中 val = 2,而缓存中 val = 1,数据不一致。 - -##### 为什么不能先删缓存,再更新数据库? - -**存在并发读请求和写请求时,可能导致脏数据**。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413113940.png) - -> 说明:如上图的场景中,读线程和写线程并行执行,导致数据库中 val = 2,而缓存中 val = 1,数据不一致。 - -##### 先更新数据库,再删除缓存就没问题了吗 - -**存在并发读请求和写请求时,可能导致脏数据**。 - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413115140.png) - -> 上图中问题发生的概率非常低:因为通常数据库更新操作比内存操作耗时多出几个数量级,最后一步回写缓存速度非常快,通常会在更新数据库之前完成。所以 Cache Aside 模式选择先更新数据库,再删除缓存,而不是先删缓存,再更新数据库。 -> -> 不过,如果真的出现了这种场景,为了避免缓存中一直保留着脏数据,可以为缓存设置过期时间,过期后缓存自动失效。通常,业务系统中允许少量数据短时间出现不一致的情况。 - -#### Read/Write Through - -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220413101029.png) - -Read Through 的思路是:**查询时更新缓存**。当缓存失效时,缓存服务自己进行加载。 - -Write Through 的思路是:当数据更新时,缓存服务负责更新缓存。 - -Through vs. Cache Aside - -Read Through vs. Cache Aside - -- Cache Aside 模式中,应用需要维护两个数据源头:一个是缓存,一个是数据库。 -- Read-Through 模式中,应用无需管理缓存和数据库,只需要将数据库的同步委托给缓存服务即可。 - -#### Write behind - -Write Behind 又叫 Write Back。Write Behind 的思路是:应用更新数据时,只更新缓存, 缓存服务每隔一段时间将缓存数据批量更新到数据库中,即延迟写入。这个设计的好处就是让提高 I/O 效率,因为异步,Write Behind 还可以合并对同一个数据的多次操作,所以性能的提高是相当可观的。 - -> 更详细的分析可以参考:[分布式之数据库和缓存双写一致性方案解析 ](https://www.cnblogs.com/rjzheng/p/9041659.html) - ## 总结 最后,通过一张思维导图来总结一下本文所述的知识点,帮助大家对缓存有一个系统性的认识。 @@ -668,9 +682,11 @@ Write Behind 又叫 Write Back。Write Behind 的思路是:应用更新数据 - [《大型网站技术架构:核心原理与案例分析》](https://item.jd.com/11322972.html) - [你应该知道的缓存进化史](https://link.juejin.im/?target=https%3A%2F%2Fjuejin.im%2Fpost%2F5b7593496fb9a009b62904fa) - [如何优雅的设计和使用缓存?](https://link.juejin.im/?target=https%3A%2F%2Fjuejin.im%2Fpost%2F5b849878e51d4538c77a974a) -- [理解分布式系统中的缓存架构(上)](https://www.jianshu.com/p/73ce0ef820f9) +- [理解分布式系统中的缓存架构(上)](https://www.jianshu.com/p/73ce0ef820f9) - [缓存那些事](https://tech.meituan.com/2017/03/17/cache-about.html) - [分布式之数据库和缓存双写一致性方案解析 ](https://www.cnblogs.com/rjzheng/p/9041659.html) - [Cache 的基本原理](https://zhuanlan.zhihu.com/p/102293437) - [5 分钟看懂系列:HTTP 缓存机制详解](https://segmentfault.com/a/1190000021716418) - [浏览器缓存看这一篇就够了](https://zhuanlan.zhihu.com/p/60950750) +- [Cache Replacement Policies - RR, FIFO, LIFO, & Optimal](https://www.youtube.com/watch?v=7lxAfszjy68&list=PLBlnK6fEyqRjdT1xkkBZSXKwFKqQoYhwy&index=23) - YouTube PPT 讲解视频,生动演示缓存淘汰算法 +- [Cache Replacement Policies - MRU, LRU, Pseudo-LRU, & LFU](https://www.youtube.com/watch?v=_Hh-NcdbHCY&list=PLBlnK6fEyqRjdT1xkkBZSXKwFKqQoYhwy&index=25) - YouTube PPT 讲解视频,生动演示缓存淘汰算法 diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/03.\345\210\206\345\272\223\345\210\206\350\241\250.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\272\223\345\210\206\350\241\250.md" similarity index 96% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/03.\345\210\206\345\272\223\345\210\206\350\241\250.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\272\223\345\210\206\350\241\250.md" index 30cfa72a84..aed67c1c1a 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/03.\345\210\206\345\272\223\345\210\206\350\241\250.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\345\210\206\345\272\223\345\210\206\350\241\250.md" @@ -7,14 +7,14 @@ categories: - 分布式存储 tags: - 分布式 - - 数据调度 + - 分布式存储 - 分库分表 -permalink: /pages/e1046e/ +permalink: /pages/6634a2b3/ --- # 分库分表基本原理 -## 1. 为何要分库分表 +## 为何要分库分表 分库分表主要基于以下理由: @@ -28,13 +28,13 @@ permalink: /pages/e1046e/ | 磁盘使用情况 | MySQL 单机磁盘容量几乎撑满 | 拆分为多个库,数据库服务器磁盘使用率大大降低 | | SQL 执行性能 | 单表数据量太大,SQL 越跑越慢 | 单表数据量减少,SQL 执行效率明显提升 | -## 2. 分库分表原理 +## 分库分表原理 **数据分片指按照某个维度将存放在单一数据库中的数据分散地存放至多个数据库或表中以达到提升性能瓶颈以及可用性的效果**。 数据分片的有效手段是对关系型数据库进行分库和分表。分库和分表均可以有效的避免由数据量超过可承受阈值而产生的查询瓶颈。 除此之外,分库还能够用于有效的分散对数据库单点的访问量;分表虽然无法缓解数据库压力,但却能够提供尽量将分布式事务转化为本地事务的可能,一旦涉及到跨库的更新操作,分布式事务往往会使问题变得复杂。 使用多主多从的分片方式,可以有效的避免数据单点,从而提升数据架构的可用性。 通过分库和分表进行数据的拆分来使得各个表的数据量保持在阈值以下,以及对流量进行疏导应对高访问量,是应对高并发和海量数据系统的有效手段。 数据分片的拆分方式又分为垂直分片和水平分片。 -### 2.1. 垂直分片 +### 垂直分片 垂直分片有两种拆分考量:业务拆分和访问频率拆分 @@ -59,7 +59,7 @@ permalink: /pages/e1046e/ 在数据库的层面使用垂直切分将按数据库中表的密集程度部署到不同的库中,例如将原来的电商数据库垂直切分成商品数据库、用户数据库等。 -### 2.2. 水平分片 +### 水平分片 > **水平拆分** 又称为 **Sharding**,它是将同一个表中的记录拆分到多个结构相同的表中。当 **单表数据量太大** 时,会极大影响 **SQL 执行的性能** 。分表是将原来一张表的数据分布到数据库集群的不同节点上,从而缓解单点的压力。 @@ -73,7 +73,7 @@ permalink: /pages/e1046e/ 读写分离的数据节点中的数据内容是一致的,而水平分片的每个数据节点的数据内容却并不相同。将水平分片和读写分离联合使用,能够更加有效的提升系统性能。 -### 2.3. 分库分表策略 +### 分库分表策略 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200608091832.png) @@ -82,7 +82,7 @@ permalink: /pages/e1046e/ - 根据数值范围划分 - 根据 Hash 划分 -#### 2.3.1. 数值范围路由 +#### 数值范围路由 数值范围路由,就是根据 ID、时间范围 这类具有排序性的字段来进行划分。例如:用户 Id 为 1-9999 的记录分到第一个库,10000-20000 的分到第二个库,以此类推。 @@ -92,7 +92,7 @@ permalink: /pages/e1046e/ 缺点:容易产生热点问题,大量的流量都打在最新的数据上了。 -#### 2.3.2. Hash 路由 +#### Hash 路由 典型的 Hash 路由,如根据数值取模,当需要扩容时,一般以 2 的幂次方进行扩容(这样,扩容时迁移的数据量会小一些)。例如:用户 Id mod n,余数为 0 的记录放到第一个库,余数为 1 的放到第二个库,以此类推。 @@ -102,7 +102,7 @@ permalink: /pages/e1046e/ 缺点:数据迁移、扩容麻烦(之前的数据需要重新计算 hash 值重新分配到不同的库或表)。当 **节点数量** 变化时,如 **扩容** 或 **收缩** 节点,数据节点 **映射关系** 需要重新计算,会导致数据的 **重新迁移**。 -#### 2.3.3. 路由表 +#### 路由表 这种策略,就是用一张独立的表记录路由信息。 @@ -110,15 +110,15 @@ permalink: /pages/e1046e/ 缺点:每次查询,必须先查路由表,增加了 IO 开销。并且,如果路由表本身太大,也会面临性能瓶颈,如果想对路由表再做分库分表,将出现死循环式的路由算法选择问题。 -## 3. 迁移和扩容 +## 迁移和扩容 -### 3.1. 停机迁移/扩容(不推荐) +### 停机迁移/扩容(不推荐) 停机迁移/扩容是最暴力、最简单的迁移、扩容方案。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601114836.png) -#### 3.1.1. 停机迁移/扩容流程 +#### 停机迁移/扩容流程 (0)预估停服时间,发布停服公告。 @@ -132,7 +132,7 @@ permalink: /pages/e1046e/ (5)应用程序修改配置,重启。 -#### 3.1.2. 停机迁移/扩容方案分析 +#### 停机迁移/扩容方案分析 优点:简单、没有数据一致性问题。 @@ -140,7 +140,7 @@ permalink: /pages/e1046e/ 点评:综上,这种方案代价太高,不可取。 -### 3.2. 双写迁移 +### 双写迁移 双写迁移的改造方案如下: @@ -159,7 +159,7 @@ permalink: /pages/e1046e/ ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601135751.png) -#### 3.2.1. 双写迁移流程 +#### 双写迁移流程 1. 修改应用程序配置,将数据同时写入老数据库和中间件。这就是所谓的**双写**,同时写俩库,老库和新库。 @@ -173,7 +173,7 @@ permalink: /pages/e1046e/ 6. 中间件根据分片规则,将数据分发到分库(分表)中。 -### 3.3. 主从升级 +### 主从升级 生产环境的数据库,为了保证高可用,一般会采用主备架构。主库支持读写操作,从库支持读操作。 @@ -183,7 +183,7 @@ permalink: /pages/e1046e/ ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601121400.png) -#### 3.3.1. 升级从库的流程 +#### 升级从库的流程 (1)解除主从关系,从库升级为主库。 @@ -195,25 +195,25 @@ permalink: /pages/e1046e/ (5)为每个分库添加新的从库,保证高可用。 -#### 3.3.2. 升级从库方案分析 +#### 升级从库方案分析 优点:不需要停机,无需数据迁移。 -## 4. 分库分表的问题 +## 分库分表的问题 -### 4.1. 分布式 ID 问题 +### 分布式 ID 问题 一旦数据库被切分到多个物理结点上,我们将不能再依赖数据库自身的主键生成机制。一方面,某个分区数据库自生成的 ID 无法保证在全局上是唯一的;另一方面,应用程序在插入数据之前需要先获得 ID,以便进行 SQL 路由。 -> 分布式 ID 的解决方案详见:[分布式 ID](https://dunwu.github.io/waterdrop/pages/3ae455/) +> 分布式 ID 的解决方案详见:[分布式 ID](https://dunwu.github.io/waterdrop/pages/26843a0b/) -### 4.2. 分布式事务问题 +### 分布式事务问题 跨库事务也是分布式的数据库集群要面对的棘手事情。 合理采用分表,可以在降低单表数据量的情况下,尽量使用本地事务,善于使用同库不同表可有效避免分布式事务带来的麻烦。在不能避免跨库事务的场景,有些业务仍然需要保持事务的一致性。 而基于 XA 的分布式事务由于在并发度高的场景中性能无法满足需要,并未被互联网巨头大规模使用,他们大多采用最终一致性的柔性事务代替强一致事务。 -> 分布式事务的解决方案详见:[分布式事务](https://dunwu.github.io/waterdrop/pages/e1881c/) +> 分布式事务的解决方案详见:[分布式事务](https://dunwu.github.io/waterdrop/pages/d46468f7/) -### 4.3. 跨节点 Join 和聚合 +### 跨节点 Join 和聚合 分库分表后,无法直接跨节点 `join` 、`count`、`order by`、`group by` 以及聚合。 @@ -223,7 +223,7 @@ permalink: /pages/e1046e/ 在程序中将这些结果进行合并、筛选。 -### 4.4. 跨分片的排序分页 +### 跨分片的排序分页 一般来讲,分页时需要按照指定字段进行排序。当排序字段就是分片字段的时候,我们通过分片规则可以比较容易定位到指定的分片,而当排序字段非分片字段的时候,情况就会变得比较复杂了。为了最终结果的准确性,我们需要在不同的分片节点中将数据进行排序并返回,并将不同分片返回的结果集进行汇总和再次排序,最后再返回给用户。如下图所示: @@ -243,7 +243,7 @@ permalink: /pages/e1046e/ 分库设计时,一般还有配套大数据平台汇总所有分库的记录,有些分页查询可以考虑走大数据平台。 -## 5. 中间件 +## 中间件 国内常见分库分表中间件: @@ -262,11 +262,11 @@ permalink: /pages/e1046e/ 通常来说,这两个方案其实都可以选用,但是我个人建议中小型公司选用 sharding-jdbc,client 层方案轻便,而且维护成本低,不需要额外增派人手,而且中小型公司系统复杂度会低一些,项目也没那么多;但是中大型公司最好还是选用 mycat 这类 proxy 层方案,因为可能大公司系统和项目非常多,团队很大,人员充足,那么最好是专门弄个人来研究和维护 mycat,然后大量项目直接透明使用即可。 -## 6. 参考资料 +## 参考资料 - [后端存储实战课](https://time.geekbang.org/column/intro/100046801) - [ShardingSphere 官方文档](https://shardingsphere.apache.org/document/current/cn/overview/) - [“分库分表" ?选型和流程要慎重,否则会失控](https://juejin.im/post/5bf778ef5188251b8a26ed8b) - [分库分表需要考虑的问题及方案](https://www.jianshu.com/p/32b3e91aa22c) - [一次难得的分库分表实践](https://juejin.im/post/5d4b6dc1f265da03c1288332) -- [分库分表平滑扩容](https://www.cnblogs.com/barrywxx/p/11532122.html) \ No newline at end of file +- [分库分表平滑扩容](https://www.cnblogs.com/barrywxx/p/11532122.html) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/02.\350\257\273\345\206\231\345\210\206\347\246\273.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\350\257\273\345\206\231\345\210\206\347\246\273.md" similarity index 93% rename from "source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/02.\350\257\273\345\206\231\345\210\206\347\246\273.md" rename to "source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\350\257\273\345\206\231\345\210\206\347\246\273.md" index 07b69d0ce5..6c50be90d6 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/02.\350\257\273\345\206\231\345\210\206\347\246\273.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/\350\257\273\345\206\231\345\210\206\347\246\273.md" @@ -7,22 +7,22 @@ categories: - 分布式存储 tags: - 分布式 - - 数据调度 + - 分布式存储 - 读写分离 -permalink: /pages/3faf18/ +permalink: /pages/b2caa509/ --- # 读写分离基本原理 **读写分离的基本原理是:主服务器用来处理写操作以及实时性要求比较高的读操作,而从服务器用来处理读操作**。 -## 1. 为何要读写分离 +## 为何要读写分离 - **有效减少锁竞争** - 主服务器只负责写,从服务器只负责读,能够有效的避免由数据更新导致的行锁竞争,使得整个系统的查询性能得到极大的改善。 - **提高查询吞吐量** - 通过一主多从的配置方式,可以将查询请求均匀的分散到多个数据副本,能够进一步的提升系统的处理能力。 - **提升数据库可用性** - 使用多主多从的方式,不但能够提升系统的吞吐量,还能够提升数据库的可用性,可以达到在任何一个数据库宕机,甚至磁盘物理损坏的情况下仍然不影响系统的正常运行。 -## 2. 读写分离的原理 +## 读写分离的原理 读写分离的实现是根据 SQL 语义分析,将读操作和写操作分别路由至主库与从库。 @@ -38,17 +38,17 @@ permalink: /pages/3faf18/ - 业务服务器将写操作发给数据库主机,将读操作发给数据库从机。 - 主机会记录请求的二进制日志,然后推送给从库,从库解析并执行日志中的请求,完成主从复制。这意味着:复制过程存在时延,这段时间内,主从数据可能不一致。 -## 3. 读写分离的问题 +## 读写分离的问题 读写分离存在两个问题:**数据一致性**和**分发机制**。 -### 3.1. 数据一致性 +### 数据一致性 读写分离产生了主库与从库之间的数据一致性的问题。 ![数据分片 + 读写分离](https://shardingsphere.apache.org/document/current/img/read-write-split/sharding-read-write-split.png) -### 3.2. 分发机制 +### 分发机制 数据库读写分离后,一个 SQL 请求具体分发到哪个数据库节点?一般有两种分发方式:客户端分发和中间件代理分发。 @@ -56,7 +56,7 @@ permalink: /pages/3faf18/ 中间件代理分发,指的是独立一套系统出来,实现读写操作分离和数据库服务器连接的管理。中间件对业务服务器提供 SQL 兼容的协议,业务服务器无须自己进行读写分离。对于业务服务器来说,访问中间件和访问数据库没有区别,事实上在业务服务器看来,中间件就是一个数据库服务器。代表有:Mycat。 -## 4. 参考资料 +## 参考资料 - [后端存储实战课](https://time.geekbang.org/column/intro/100046801) -- [ShardingSphere 官方文档](https://shardingsphere.apache.org/document/current/cn/overview/) \ No newline at end of file +- [ShardingSphere 官方文档](https://shardingsphere.apache.org/document/current/cn/overview/) diff --git "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/README.md" "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/README.md" index 5bfec68da1..70ec67e61a 100644 --- "a/source/_posts/15.\345\210\206\345\270\203\345\274\217/README.md" +++ "b/source/_posts/15.\345\210\206\345\270\203\345\274\217/README.md" @@ -5,7 +5,7 @@ categories: - 分布式 tags: - 分布式 -permalink: /pages/f21e8c/ +permalink: /pages/5bfcdcbe/ hidden: true index: false --- @@ -18,28 +18,26 @@ index: false ## 📖 内容 -### 分布式综合 +### [分布式综合](00.分布式综合) -- [分布式面试总结](00.分布式综合/99.分布式面试.md) +- [逻辑时钟](00.分布式综合/逻辑时钟.md) - 关键词:`逻辑时钟`、`向量时钟`、`版本时钟`、`全序`、`偏序` +- [CAP 和 BASE](00.分布式综合/CAP&BASE.md) - 关键词:`ACID`、`CAP`、`BASE`、`一致性` +- [拜占庭将军问题](00.分布式综合/拜占庭将军问题.md) - 关键词:`共识` +- [分布式算法 Paxos](00.分布式综合/Paxos.md) - 关键词:`共识`、`Paxos` +- [分布式算法 Raft](00.分布式综合/Raft.md) - 关键词:`共识`、`Raft` +- [分布式算法 Gossip](00.分布式综合/Gossip.md) - 关键词:`Gossip` +- [ZAB 协议](00.分布式综合/Zab.md) - 关键词:`共识`、`ZAB`、`ZooKeeper` +- [分布式综合面试](00.分布式综合/分布式综合面试.md) -### 分布式理论 - -- **理论** - - [分布式基础理论](01.分布式理论/01.分布式基础理论.md) - 关键词:`拜占庭将军`、`CAP`、`BASE`、`错误的分布式假设` -- **算法** - - [分布式算法 Paxos](01.分布式理论/11.Paxos算法.md) - 关键词:`共识性算法` - - [分布式算法 Raft](01.分布式理论/12.Raft算法.md) - 关键词:`共识性算法` - - [分布式算法 Gossip](01.分布式理论/13.Gossip算法.md) - 关键词:`数据传播` - -### 分布式协同 +### [分布式协同](11.分布式协同) - **分布式协同综合** - - 集群 - - [分布式复制](11.分布式协同/01.分布式协同综合/02.分布式复制.md) - - 分区 - - 选主 - - [分布式事务](11.分布式协同/01.分布式协同综合/05.分布式事务.md) - 关键词:`2PC`、`3PC`、`TCC`、`本地消息表`、`MQ 消息`、`SAGA` - - [分布式锁](11.分布式协同/01.分布式协同综合/06.分布式锁.md) - 关键词:`数据库`、`Redis`、`ZooKeeper`、`互斥`、`可重入`、`死锁`、`容错`、`自旋尝试` + - [分布式复制](11.分布式协同/01.分布式协同综合/分布式复制.md) - 关键词:`主从`、`多主`、`无主` + - [分布式分区](11.分布式协同/01.分布式协同综合/分布式分区.md) - 关键词:`分区再均衡`、`路由` + - [分布式共识](11.分布式协同/01.分布式协同综合/分布式共识.md) - 关键词:`共识`、`广播`、`epoch`、`quorum` + - [分布式事务](11.分布式协同/01.分布式协同综合/分布式事务.md) - 关键词:`2PC`、`3PC`、`TCC`、`本地消息表`、`消息事务`、`SAGA` + - [分布式锁](11.分布式协同/01.分布式协同综合/分布式锁.md) - 关键词:`互斥`、`可重入`、`死锁`、`容错`、`自旋尝试`、`公平性` + - [分布式 ID](11.分布式协同/01.分布式协同综合/分布式ID.md) - 关键词:`UUID`、`自增序列`、`雪花算法`、`Leaf` - **ZooKeeper** - [ZooKeeper 原理](11.分布式协同/02.ZooKeeper/01.ZooKeeper原理.md) - [ZooKeeper Java Api](11.分布式协同/02.ZooKeeper/02.ZooKeeperJavaApi.md) @@ -47,62 +45,56 @@ index: false - [ZooKeeper 运维](11.分布式协同/02.ZooKeeper/04.ZooKeeper运维.md) - [ZooKeeper Acl](11.分布式协同/02.ZooKeeper/05.ZooKeeperAcl.md) -### 分布式调度 +### [分布式调度](12.分布式调度) -- [服务路由](12.分布式调度/01.服务路由.md) - 关键词:`路由`、`条件路由`、`脚本路由`、`标签路由` -- [负载均衡](12.分布式调度/02.负载均衡.md) - 关键词:`轮询`、`随机`、`最少连接`、`源地址哈希`、`一致性哈希`、`虚拟 hash 槽` -- [流量控制](12.分布式调度/03.流量控制.md) - 关键词:`限流`、`熔断`、`降级`、`计数器法`、`时间窗口法`、`令牌桶法`、`漏桶法` -- [分布式会话](12.分布式调度/10.分布式会话.md) - 关键词:`粘性 Session`、`Session 复制共享`、`基于缓存的 session 共享` -- [分布式 ID](12.分布式调度/04.分布式ID.md) - 关键词:`UUID`、`自增序列`、`雪花算法`、`Leaf` +- [服务注册和发现](12.分布式调度/服务注册和发现.md) - 关键词:`服务注册`、`服务发现`、`元数据` +- [负载均衡](12.分布式调度/负载均衡.md) - 关键词:`轮询`、`随机`、`最少连接`、`源地址哈希`、`一致性哈希`、`虚拟 hash 槽` +- [流量控制](12.分布式调度/流量控制.md) - 关键词:`限流`、`熔断`、`降级`、`计数器法`、`时间窗口法`、`令牌桶法`、`漏桶法` +- [路由和网关](12.分布式调度/网关路由.md) - 关键词:`路由`、`条件路由`、`脚本路由`、`标签路由` ### 分布式高可用 -- [服务容错](13.分布式高可用/02.服务容错.md) - -### 分布式通信 +- [服务容错](11.分布式协同/01.分布式协同综合/服务容错.md) -### RPC +### [分布式通信](21.分布式通信) -#### RPC 综合 +#### [RPC](21.分布式通信/01.RPC) -- [RPC 基础](21.分布式通信/01.RPC/00.RPC综合/01.RPC基础.md) -- [RPC 进阶](21.分布式通信/01.RPC/00.RPC综合/02.RPC进阶.md) -- [RPC 高级](21.分布式通信/01.RPC/00.RPC综合/03.RPC高级.md) -- [服务注册和发现](21.分布式通信/01.RPC/00.RPC综合/11.服务注册和发现.md) +- [Dubbo 面试](21.分布式通信/01.RPC/Dubbo面试.md) +- [RPC 面试](21.分布式通信/01.RPC/RPC面试.md) -### MQ +#### [MQ](21.分布式通信/02.MQ) -#### MQ 综合 +##### [MQ 综合](21.分布式通信/02.MQ/00.MQ综合) -- [消息队列面试](21.分布式通信/02.MQ/00.MQ综合/01.消息队列面试.md) -- [消息队列基本原理](21.分布式通信/02.MQ/00.MQ综合/02.消息队列基本原理.md) +- [MQ 面试](21.分布式通信/02.MQ/00.MQ综合/MQ面试.md) -#### Kafka +##### [Kafka](21.分布式通信/02.MQ/01.Kafka) -- [Kafka 快速入门](21.分布式通信/02.MQ/01.Kafka/01.Kafka快速入门.md) -- [Kafka 生产者](21.分布式通信/02.MQ/01.Kafka/02.Kafka生产者.md) -- [Kafka 消费者](21.分布式通信/02.MQ/01.Kafka/03.Kafka消费者.md) -- [Kafka 集群](21.分布式通信/02.MQ/01.Kafka/04.Kafka集群.md) -- [Kafka 可靠传输](21.分布式通信/02.MQ/01.Kafka/05.Kafka可靠传输.md) -- [Kafka 存储](21.分布式通信/02.MQ/01.Kafka/06.Kafka存储.md) -- [Kafka 流式处理](21.分布式通信/02.MQ/01.Kafka/07.Kafka流式处理.md) -- [Kafka 运维](21.分布式通信/02.MQ/01.Kafka/08.Kafka运维.md) +- [Kafka 快速入门](21.分布式通信/02.MQ/01.Kafka/Kafka快速入门.md) +- [Kafka 生产](21.分布式通信/02.MQ/01.Kafka/Kafka生产.md) +- [Kafka 消费](21.分布式通信/02.MQ/01.Kafka/Kafka消费.md) +- [Kafka 集群](21.分布式通信/02.MQ/01.Kafka/Kafka集群.md) +- [Kafka 可靠传输](21.分布式通信/02.MQ/01.Kafka/Kafka可靠传输.md) +- [Kafka 存储](21.分布式通信/02.MQ/01.Kafka/Kafka存储.md) +- [Kafka 流式处理](21.分布式通信/02.MQ/01.Kafka/Kafka流式处理.md) +- [Kafka 运维](21.分布式通信/02.MQ/01.Kafka/Kafka运维.md) -#### RocketMQ +##### [RocketMQ](21.分布式通信/02.MQ/02.RocketMQ) -- [RocketMQ 快速入门](21.分布式通信/02.MQ/02.RocketMQ/01.RocketMQ快速入门.md) -- [RocketMQ 基本原理](21.分布式通信/02.MQ/02.RocketMQ/02.RocketMQ基本原理.md) -- [RocketMQ Faq](21.分布式通信/02.MQ/02.RocketMQ/99.RocketMQFaq.md) +- [RocketMQ 快速入门](21.分布式通信/02.MQ/02.RocketMQ/RocketMQ快速入门.md) +- [RocketMQ 基本原理](21.分布式通信/02.MQ/02.RocketMQ/RocketMQ基本原理.md) +- [RocketMQ Faq](21.分布式通信/02.MQ/02.RocketMQ/RocketMQFaq.md) -#### 其他 MQ +##### 其他 MQ -- [ActiveMQ](21.分布式通信/02.MQ/99.其他MQ/01.ActiveMQ.md) +- [ActiveMQ](21.分布式通信/02.MQ/99.其他MQ/ActiveMQ.md) -### 分布式存储 +### [分布式存储](22.分布式存储) -- [分布式缓存](22.分布式存储/01.分布式缓存.md) - 关键词:`进程内缓存`、`分布式缓存`、`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` -- [读写分离](22.分布式存储/02.读写分离.md) -- [分库分表](22.分布式存储/03.分库分表.md) - 关键词:`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` +- [分布式缓存](22.分布式存储/分布式缓存.md) - 关键词:`进程内缓存`、`分布式缓存`、`缓存雪崩`、`缓存穿透`、`缓存击穿`、`缓存更新`、`缓存预热`、`缓存降级` +- [读写分离](22.分布式存储/读写分离.md) +- [分库分表](22.分布式存储/分库分表.md) - 关键词:`分片`、`路由`、`迁移`、`扩容`、`双写`、`聚合` ## 📚 资料 @@ -111,85 +103,70 @@ index: false #### 分布式理论综合资料 - **教程** - - [分布式技术原理与算法解析](https://time.geekbang.org/column/intro/100036401) - 极客时间教程 - - [分布式协议与算法实战](https://time.geekbang.org/column/intro/100046101) - 极客时间教程 - - [Distributed Systems for fun and profit](http://book.mixu.net/distsys/single-page.html):分为五章,讲述了扩展性、可用性、性能和容错等基础知识,FLP 不可能性和 CAP 定理,探讨了大量的一致性模型;讨论了时间和顺序,及时钟的各种用法。随后,探讨了复制问题,如何防止差异,以及如何接受差异。此外,每章末尾都给出了针对本章内容的扩展阅读资源列表,这些资料是对本书内容的很好补充。 + - [**MIT-6.824**](https://pdos.csail.mit.edu/6.824/index.html) - 麻省理工分布式系统课程 + - [**CMU-15-440**](http://www.cs.cmu.edu/~dga/15-440/S14/) - 卡内基梅隆分布式系统课程 + - [**Standford-CS244b**](https://www.scs.stanford.edu/14au-cs244b/) - 斯坦福分布式系统课程 + - [**UC Berkley-CS294-91**](https://people.eecs.berkeley.edu/~alig/cs294-91/)- 伯克利分布式计算课程 + - [**分布式技术原理与算法解析**](https://time.geekbang.org/column/intro/100036401) - 极客时间教程 + - [**分布式协议与算法实战**](https://time.geekbang.org/column/intro/100046101) - 极客时间教程 - **书籍** - - [分布式系统原理与范型](https://book.douban.com/subject/11691266/):书原名 Distributed Systems Principles and Paradigms。经典分布式教程,介绍了分布式系统的七大核心原理,并给出了大量的例子;系统讲述了分布式系统的概念和技术,包括通信、进程、命名、同步化、一致性和复制、容错以及安全等。 + - [**分布式系统原理与范型**](https://book.douban.com/subject/11691266/):书原名 Distributed Systems Principles and Paradigms。经典分布式教程,介绍了分布式系统的七大核心原理,并给出了大量的例子;系统讲述了分布式系统的概念和技术,包括通信、进程、命名、同步化、一致性和复制、容错以及安全等。 - **文章** - - [The Google File System](https://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf):Google 三大经典论文之一 - - [Bigtable: A Distributed Storage System for Structured Data](https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf):Google 三大经典论文之一 - - [MapReduce: Simplifed Data Processing on Large Clusters](https://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf):Google 三大经典论文之一 - - [Time, Clocks, and the Ordering of Events in a Distributed System](https://lamport.azurewebsites.net/pubs/time-clocks.pdf) - - [The Byzantine Generals Problem](https://lamport.azurewebsites.net/pubs/byz.pdf) - - [Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services](https://www.comp.nus.edu.sg/~gilbert/pubs/BrewersConjecture-SigAct.pdf) - CAP 论文 - - CAP Twelve Years Later: How the “Rules” Have Changed - - BASE: An Acid Alternative - - A Simple Totally Ordered Broadcast Protocol - - Virtual Time and Global States of Distributed Systems - - [The fallacies of distributed computing](https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing) - -#### 分布式一致性算法 + - [**Solution of a Problem in Concurrent Programming Control**](https://dl.acm.org/doi/pdf/10.1145/365559.365617),[**译文**](http://duanple.com/?p=1022) - Dijkstra 首次提出并解决了互斥执行问题 + - [**Time, Clocks, and the Ordering of Events in a Distributed System**](https://lamport.azurewebsites.net/pubs/time-clocks.pdf),[**译文**](https://cloud.tencent.com/developer/article/1163428),[**解读**](https://zhuanlan.zhihu.com/p/56146800) - Lamport 介绍 happened before、偏序关系(partial ordering)、逻辑时钟(Logical Clocks)概念,提出解决分布式系统中区分事件发生的时序问题的方法。 + - [**Virtual Time and Global States of Distributed Systems**](http://courses.csail.mit.edu/6.852/01/papers/VirtTime_GlobState.pdf),[**解读**](https://zhuanlan.zhihu.com/p/56886156) - 逻辑时钟无法描述事件的因果关系。本文提出了向量时钟,这种算法利用了向量这种数据结构将全局各个进程的逻辑时间戳广播给各个进程,通过向量时间戳就能够比较任意两个事件的因果关系。 + - [**Brewer’s Conjecture and the Feasibility of Consistent, Available, Partition-Tolerant Web Services**](https://www.comp.nus.edu.sg/~gilbert/pubs/BrewersConjecture-SigAct.pdf),[**解读**](https://juejin.cn/post/6844903936718012430) - 经典的 CAP 理论,即:在一个分布式系统中,当发生网络分区时,那么强一致性和可用性只能二选一。 + - [**CAP Twelve Years Later: How the “Rules” Have Changed**](https://www.infoq.com/articles/cap-twelve-years-later-how-the-rules-have-changed/) - CAP 理论的新解读,并阐述 CAP 理论的一些常见误区。 + - [**BASE: An Acid Alternative**](https://www.semanticscholar.org/paper/BASE%3A-An-Acid-Alternative-Pritchett/2e72e6c022dd33115304ecfcb6dad7ea609534a4),[**译文**](https://www.cnblogs.com/savorboard/p/base-an-acid-alternative.html) - BASE 理论是对 CAP 中一致性和可用性的权衡,提出采用适当的方式来使系统达到最终一致性。 + - [**A Simple Totally Ordered Broadcast Protocol**](https://diyhpl.us/~bryan/papers2/distributed/distributed-systems/zab.totally-ordered-broadcast-protocol.2008.pdf) - 概述 ZooKeeper 的全序广播协议(Zab) + - [**The Eight Fallacies of Distributed Computing - Tech Talk**](https://web.archive.org/web/20171107014323/http://blog.fogcreek.com/eight-fallacies-of-distributed-computing-tech-talk/) - 分布式系统新手常犯的 8 个错误,并探讨了其会带来的影响。 + - [**Distributed Systems for Fun and Profit**](http://book.mixu.net/distsys/) - 一本学习小册,涵盖了分布式系统中的关键问题,包括时间的作用和不同的复制策略。 + - [**A Note on Distributed System**s](https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf) - 这是一篇经典的论文,讲述了为什么在分布式系统中,远程交互不能像本地对象那样进行。 + - [**深度探索分布式理论经典论文**](https://zhuanlan.zhihu.com/p/338161857) - 分布式理论论文的导读文章 + - [**Distributed Systems for fun and profit**](http://book.mixu.net/distsys/single-page.html):分为五章,讲述了扩展性、可用性、性能和容错等基础知识,FLP 不可能性和 CAP 定理,探讨了大量的一致性模型;讨论了时间和顺序,及时钟的各种用法。随后,探讨了复制问题,如何防止差异,以及如何接受差异。此外,每章末尾都给出了针对本章内容的扩展阅读资源列表,这些资料是对本书内容的很好补充。 + - [**An introduction to distributed systems**](https://github.com/aphyr/distsys-class) - 这是一份分布式系统的提纲挈领的介绍,几乎涵盖了所有知识点,并辅以简洁并切中要害的说明文字,适合初学者了解知识全貌,快速与现有知识结合,形成知识体系。 + +#### 分布式算法资料 -- **教程** - - [Raft: Understandable Distributed Consensus](http://thesecretlivesofdata.com/raft) - 一个动画教程 - - [The Raft Consensus Algorithm](https://raft.github.io/) - 一个交互式动画教程 - **视频** - - [Raft 作者讲解 Paxos 视频](https://www.bilibili.com/video/av36556594) - - [Paxos 算法讲解视频](https://www.youtube.com/watch?v=d7nAGI_NZPk) - - [Raft 作者讲解视频](https://www.youtube.com/watch?v=YbZ3zDzDnrw&feature=youtu.be) - - [Raft 作者讲解视频对应的 PPT](http://www2.cs.uh.edu/~paris/6360/PowerPoint/Raft.ppt) -- 文章 - - - [Part-time Parliament 论文](https://research.microsoft.com/en-us/um/people/lamport/pubs/lamport-paxos.pdf) - - [Paxos Made Simple 论文](https://lamport.azurewebsites.net/pubs/paxos-simple.pdf) - - Paxos Made Practical - - Paxos Made Live: An Engineering Perspective - - Using Paxos to Build a Scalable, Consistent, and Highly Available Datastore - Impossibility of Distributed Consensus With One Faulty Process - - [Paxos 算法详解](https://zhuanlan.zhihu.com/p/31780743) - - [一致性算法(Paxos、Raft、Zab)](https://www.bilibili.com/video/BV1TW411M7Fx?from=search&seid=11524608198747599965) - - [分布式协议与算法实战](https://time.geekbang.org/column/intro/100046101) - - - [Raft: In Search of an Understandable Consensus Algorithm](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf) - - [Raft 算法论文译文](https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md) - - [分布式系统的 Raft 算法](https://www.jdon.com/artichect/raft.html) - - [Raft 算法详解](https://zhuanlan.zhihu.com/p/32052223) - - A Brief History of Consensus, 2PC and Transaction Commit - - Consensus in the Presence of Partial Synchrony - -- 工具 + - [**拜占庭将军问题视频讲解**](https://www.bilibili.com/video/av78588312/) - 李永乐老师通俗讲解拜占庭问题 + - [**Paxos 算法讲解视频**](https://www.youtube.com/watch?v=d7nAGI_NZPk) + - [**Raft 作者讲解 Paxos 视频**](https://www.bilibili.com/video/av36556594) + - [**Raft 作者讲解视频**](https://www.youtube.com/watch?v=YbZ3zDzDnrw&feature=youtu.be) + - [**Raft 作者讲解视频对应的 PPT**](http://www2.cs.uh.edu/~paris/6360/PowerPoint/Raft.ppt) + - [**一致性算法(Paxos、Raft、Zab)**](https://www.bilibili.com/video/BV1TW411M7Fx?from=search&seid=11524608198747599965) +- **动画** + - [**Raft: Understandable Distributed Consensus**](http://thesecretlivesofdata.com/raft) - 分布式一致性算法 Raft 的动画教程 + - [**The Raft Consensus Algorithm**](https://raft.github.io/) - 分布式一致性算法 Raft 的交互式动画教程 + - [**Goosip 协议仿真动画**](https://flopezluis.github.io/gossip-simulator/) +- **文章** + - [**The Byzantine Generals Problem**](https://lamport.azurewebsites.net/pubs/byz.pdf) - Lamport 提出拜占庭将军问题——一种解决分布式系统一致性问题的理论。 + - [**The Part-Time Parliament**](https://lamport.azurewebsites.net/pubs/lamport-paxos.pdf) - Lamport 提出分布式一致性算法 Paxos。 + - [**Paxos Made Simple**](https://lamport.azurewebsites.net/pubs/paxos-simple.pdf),[**译文**](http://duanple.com/?p=166),[**解读**](https://zhuanlan.zhihu.com/p/31780743) - Lamport 重新阐述 Paxos。 + - [**Paxos Made Live: An Engineering Perspective**](https://www.cs.utexas.edu/users/lorenzo/corsi/cs380d/papers/paper2-1.pdf),[**译文**](https://blog.mrcroxx.com/posts/paper-reading/paxos-made-live/) - 讲述了 Google 在最初实现 Paxos 碰到的一系列问题及解决方案。 + - [**How to Build a Highly Availability System using Consensus**](http://bwl-website.s3-website.us-east-2.amazonaws.com/58-Consensus/Acrobat.pdf),[**译文**](http://duanple.com/?p=63) - 以 Paxos 为实例,讲述了如何描述、解决、理解、证明分布式算法。 + - [**Using Paxos to Build a Scalable, Consistent, and Highly Available Datastore**](https://arxiv.org/pdf/1103.2408) - 讲述了 LinkedIn 是如何利用 Paxos 和 ZooKeeper 构建一个名为 Spinnaker 的 KV 数据库。 + - [**Raft: In Search of an Understandable Consensus Algorithm**](https://ramcloud.atlassian.net/wiki/download/attachments/6586375/raft.pdf),[**译文**](https://github.com/maemual/raft-zh_cn/blob/master/raft-zh_cn.md),[**解读**](https://zhuanlan.zhihu.com/p/32052223) - 分布式一致性算法 Raft + - [**A Brief History of Consensus, 2PC and Transaction Commit**](https://betathoughts.blogspot.com/2007/06/brief-history-of-consensus-2pc-and.html) - 一致性, 两阶段提交和事务提交的发展史 + - [**Consensus in the Presence of Partial Synchrony**](https://dl.acm.org/doi/pdf/10.1145/42282.42283) - 部分同步的一致性 + - [**Epidemic Algorithms for Replicated Database Maintenance**](http://bitsavers.trailing-edge.com/pdf/xerox/parc/techReports/CSL-89-1_Epidemic_Algorithms_for_Replicated_Database_Maintenance.pdf),[**解读**](https://zhuanlan.zhihu.com/p/41228196) - 论文提出 Gossip 协议及应用 + - [**INTRODUCTION TO GOSSIP**](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) - Gossip 协议介绍 +- **工具** - [sofa-jraft](https://github.com/sofastack/sofa-jraft) - 蚂蚁金服的 Raft 算法实现库(Java 版) -#### Goosip 资料 - -- [Epidemic Algorithms for Replicated Database Maintenance](http://bitsavers.trailing-edge.com/pdf/xerox/parc/techReports/CSL-89-1_Epidemic_Algorithms_for_Replicated_Database_Maintenance.pdf) -- [P2P 网络核心技术:Gossip 协议](https://zhuanlan.zhihu.com/p/41228196) -- [INTRODUCTION TO GOSSIP](https://managementfromscratch.wordpress.com/2016/04/01/introduction-to-gossip/) -- [Goosip 协议仿真动画](https://flopezluis.github.io/gossip-simulator/) - -### 分布式架构资料 - -- [An introduction to distributed systems](https://github.com/aphyr/distsys-class) - 这是一份分布式系统的提纲挈领的介绍,几乎涵盖了所有知识点,并辅以简洁并切中要害的说明文字,适合初学者了解知识全貌,快速与现有知识结合,形成知识体系。 - ### 分布式通信资料 #### RPC 资料 -- [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) - 极客时间教程 - -#### MQ 资料 - - **教程** - - [消息队列高手课](https://time.geekbang.org/column/intro/100032301) - - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - - [Kafka 核心源码解读](https://time.geekbang.org/column/intro/304) + - [**极客时间教程 - RPC 实战与核心原理**](https://time.geekbang.org/column/intro/100046201) +- **官方** + - [Dubbo Github](https://github.com/apache/dubbo) + - [Dubbo 官方文档](https://dubbo.apache.org/zh-cn/) - **文章** + - [如何基于 Dubbo 进行服务治理、服务降级、失败重试以及超时重试?](https://github.com/doocs/advanced-java/blob/master/docs/distributed-system/dubbo-service-management.md) - - [The Log: What every software engineer should know about real-time data’s unifying abstraction](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying) - - [《日志:每个软件工程师都应该知道的有关实时数据的统一抽象》](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying) - 上面文章的译文 - - [Introduction and Overview of Apache Kafka](https://www.slideshare.net/mumrah/kafka-talk-tri-hug) +#### MQ 资料 - **官方** - [Kafka 官网](http://kafka.apache.org/) @@ -197,29 +174,53 @@ index: false - [Kafka 官方文档](https://kafka.apache.org/documentation/) - [Kafka Confluent 官网](http://kafka.apache.org/) - [Kafka Jira](https://issues.apache.org/jira/projects/KAFKA?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page) + - [RocketMQ Github](https://github.com/apache/rocketmq) + - [RocketMQ 官方文档](http://rocketmq.apache.org/docs/quick-start/) + - [ActiveMQ 官网](http://activemq.apache.org/) - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) - - [《深入理解 Kafka:核心设计与实践原理》](https://item.jd.com/12489649.html) - - [《Kafka 技术内幕》](https://item.jd.com/12234113.html) + - [《Kafka 权威指南》](https://book.douban.com/subject/27665114/) + - [《深入理解 Kafka:核心设计与实践原理》](https://book.douban.com/subject/30437872/) + - [《RocketMQ 技术内幕》](https://book.douban.com/subject/30417623/) +- **教程** + - [**极客时间教程 - 消息队列高手课**](https://time.geekbang.org/column/intro/100032301) + - [**Kafka 中文文档**](https://github.com/apachecn/kafka-doc-zh) + - [**极客时间教程 - Kafka 核心技术与实战**](https://time.geekbang.org/column/intro/100029201) + - [**极客时间教程 - Kafka 核心源码解读**](https://time.geekbang.org/column/intro/304) +- **视频** + - [Apache Kafka Fundamentals You Should Know](https://www.youtube.com/watch?v=-RDyEFvnTXI) + - [Top Kafka Use Cases You Should Know](https://www.youtube.com/watch?v=Ajz6dBp_EB4) + - [System Design: Why is Kafka fast?](https://www.youtube.com/watch?v=UNUz1-msbOM) + - [System Design: Why is Kafka so Popular?](https://www.youtube.com/watch?v=yIAcHMJzqJc) +- **文章** + - [**The Log: What every software engineer should know about real-time data’s unifying abstraction**](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying),[**译文**](https://engineering.linkedin.com/distributed-systems/log-what-every-software-engineer-should-know-about-real-time-datas-unifying) + - [**Introduction and Overview of Apache Kafka**](https://www.slideshare.net/mumrah/kafka-talk-tri-hug) - Kafka 简介 PPT + - [Why is Kafka so fast? How does it work?](https://blog.bytebytego.com/p/why-is-kafka-so-fast-how-does-it) + - [大型网站架构系列:分布式 MQ(一)](https://www.cnblogs.com/itfly8/p/5155983.html) + - [大型网站架构系列:MQ(二)](https://www.cnblogs.com/itfly8/p/5156155.html) + - [阿里 RocketMQ 优势对比](https://juejin.im/entry/5a0abfb5f265da43062a4a91) + - [advanced-java 之 MQ](https://github.com/doocs/advanced-java/blob/master/docs/high-concurrency/mq-interview.md) + - [浅谈消息队列及常见的消息中间件](https://juejin.im/post/6844903635046924296) + - [聊聊 Kafka: Kafka 为啥这么快?](https://xie.infoq.cn/article/49bc80d683c373db93d017a99) ### 分布式存储资料 - **书籍** - - [《数据密集型应用系统设计》](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 + - [**数据密集型应用系统设计**](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 - **文章** - Chord: A Scalable Peer-to-Peer Lookup Service for Internet Applications - Pastry: Scalable, Distributed Object Location, and Routing for Large-Scale Peerto-Peer Systems - Kademlia: A Peer-to-Peer Information System Based on the XOR Metric - A Scalable Content-Addressable Network - Ceph: A Scalable, High-Performance Distributed File System - - [The Log-Structured-Merge-Tree](chrome-extension://efaidnbmnnnibpcajpcglclefindmkaj/https://www.cs.umb.edu/~poneil/lsmtree.pdf) - - [HBase: A NoSQL Database](https://www.researchgate.net/publication/317399857_HBase_A_NoSQL_Database) + - [**The Log-Structured-Merge-Tree**](https://www.cs.umb.edu/~poneil/lsmtree.pdf),[**译文**](https://cloud.tencent.com/developer/article/2057367) - LSM 树被广泛应用于 HBase、RocksDB 等 Nosql 数据库。这篇论文详细介绍了 LSM 树的特性和原理。 + - [**HBase: A NoSQL Database**](https://www.researchgate.net/publication/317399857_HBase_A_NoSQL_Database) - Tango: Distributed Data Structure over a Shared Log -### 分布式系统实战 +### 分布式系统架构资料 -- The Google File System -- BigTable: A Distributed Storage System for Structured Data +- [**The Google File System**](https://static.googleusercontent.com/media/research.google.com/en//archive/gfs-sosp2003.pdf) - Google 三驾马车之 GFS +- [**Bigtable: A Distributed Storage System for Structured Data**](https://static.googleusercontent.com/media/research.google.com/en//archive/bigtable-osdi06.pdf) - Google 三驾马车之 BigTable +- [**MapReduce: Simplifed Data Processing on Large Clusters**](https://static.googleusercontent.com/media/research.google.com/en//archive/mapreduce-osdi04.pdf) - Google 三驾马车之 MapReduce - The Chubby Lock Service for Loosely-Coupled Distributed Systems - Finding a Needle in Haystack: Facebook’s Photo Storage - Windows Azure Storage: A Highly Available Cloud Storage Service with Strong Consistency diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/01.HDFS\345\205\245\351\227\250.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/01.HDFS\345\205\245\351\227\250.md" deleted file mode 100644 index 222fc4b58d..0000000000 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/01.HDFS\345\205\245\351\227\250.md" +++ /dev/null @@ -1,237 +0,0 @@ ---- -title: HDFS 入门 -date: 2020-02-24 21:14:47 -order: 01 -categories: - - 大数据 - - hadoop - - hdfs -tags: - - 大数据 - - Hadoop - - HDFS -permalink: /pages/3cd48f/ ---- - -# HDFS 入门 - -> **HDFS 是 Hadoop 分布式文件系统。** -> -> 关键词:分布式、文件系统 - -## HDFS 简介 - -**HDFS** 是 **Hadoop Distributed File System** 的缩写,即 Hadoop 的分布式文件系统。 - -HDFS 是一种用于存储具有流数据访问模式的超大文件的文件系统,它运行在廉价的机器集群上。 - -HDFS 的设计目标是管理数以千计的服务器、数以万计的磁盘,将这么大规模的服务器计算资源当作一个单一的存储系统进行管理,对应用程序提供数以 PB 计的存储容量,让应用程序像使用普通文件系统一样存储大规模的文件数据。 - -HDFS 是在一个大规模分布式服务器集群上,对数据分片后进行并行读写及冗余存储。因为 HDFS 可以部署在一个比较大的服务器集群上,集群中所有服务器的磁盘都可供 HDFS 使用,所以整个 HDFS 的存储空间可以达到 PB 级容量。 - -### HDFS 的优点 - -- **高容错** - 数据冗余多副本,副本丢失后自动恢复 -- **高可用** - NameNode HA、安全模式 -- **高扩展** - 能够处理 10K 节点的规模;处理数据达到 GB、TB、甚至 PB 级别的数据;能够处理百万规模以上的文件数量,数量相当之大。 -- **批处理** - 流式数据访问;数据位置暴露给计算框架 -- **构建在廉价商用机器上** - 提供了容错和恢复机制 - -### HDFS 的缺点 - -- **不适合低延迟数据访问** - 适合高吞吐率的场景,就是在某一时间内写入大量的数据。但是它在低延时的情况下是不行的,比如毫秒级以内读取数据,它是很难做到的。 -- **不适合大量小文件存储** - - 存储大量小文件(这里的小文件是指小于 HDFS 系统的 Block 大小的文件(默认 64M))的话,它会占用 NameNode 大量的内存来存储文件、目录和块信息。这样是不可取的,因为 NameNode 的内存总是有限的。 - - 磁盘寻道时间超过读取时间 -- **不支持并发写入** - 一个文件同时只能有一个写入者 -- **不支持文件随机修改** - 仅支持追加写入 - -## HDFS 架构 - -**HDFS 采用主从架构**,由单个 NameNode(NN) 和多个 DataNode(DN) 组成。 - -集群中的 Datanode 一般是一个节点一个,负责管理它所在节点上的存储。HDFS 暴露了文件系统的名字空间,用户能够以文件的形式在上面存储数据。从内部看,一个文件其实被分成一个或多个数据块,这些块存储在一组 Datanode 上。Namenode 执行文件系统的名字空间操作,比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体 Datanode 节点的映射。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-architecture.png) - -### NameNode - -**NameNode 负责管理文件系统的命名空间以及客户端对文件的访问**。NameNode 的职责: - -- 管理命名空间 -- 管理元数据:文件的位置、所有者、权限、数据块等 -- 管理 Block 副本策略:默认 3 个副本 -- 处理客户端读写请求,为 DataNode 分配任务 - -### DataNode - -**DataNode 负责文件数据的存储和读写操作,HDFS 将文件数据分割成若干数据块(Block),每个 DataNode 存储一部分数据块,这样文件就分布存储在整个 HDFS 服务器集群中**。 - -- 存储 Block 和数据校验和 -- 执行客户端发送的读写操作 -- 通过心跳机制定期(默认 3 秒)向 NameNode 汇报运行状态和 Block 列表信息 -- 集群启动时,DataNode 向 NameNode 提供 Block 列表信息 - -### 命名空间 - -HDFS 的 `文件系统命名空间` 的层次结构与大多数文件系统类似 (如 Linux), 支持目录和文件的创建、移动、删除和重命名等操作,支持配置用户和访问权限,但不支持硬链接和软连接。`NameNode` 负责维护文件系统名称空间,记录对名称空间或其属性的任何更改。 - -### Block 数据块 - -- HDFS 最小存储单元 -- 文件写入 HDFS 会被切分成若干个 Block -- Block 大小固定,默认为 128MB,可自定义 -- 若一个 Block 的大小小于设定值,不会占用整个块空间 -- 默认情况下每个 Block 有 3 个副本 - -### Client - -- 将文件切分为 Block 数据块 -- 与 NameNode 交互,获取文件元数据 -- 与 DataNode 交互,读取或写入数据 -- 管理 HDFS - -## HDFS 数据流 - -### HDFS 读文件 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-read.png) - -1. 客户端调用 FileSyste 对象的 open() 方法在分布式文件系统中**打开要读取的文件**。 -2. 分布式文件系统通过使用 RPC(远程过程调用)来调用 namenode,**确定文件起始块的位置**。 -3. 分布式文件系统的 DistributedFileSystem 类返回一个支持文件定位的输入流 FSDataInputStream 对象,FSDataInputStream 对象接着封装 DFSInputStream 对象(**存储着文件起始几个块的 datanode 地址**),客户端对这个输入流调用 read()方法。 -4. DFSInputStream 连接距离最近的 datanode,通过反复调用 read 方法,**将数据从 datanode 传输到客户端**。 -5. 到达块的末端时,DFSInputStream 关闭与该 datanode 的连接,**寻找下一个块的最佳 datanode**。 -6. 客户端完成读取,对 FSDataInputStream 调用 close()方法**关闭连接**。 - -### HDFS 写文件 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-write.png) - -1. 客户端通过对 DistributedFileSystem 对象调用 create() 函数来**新建文件**。 -2. 分布式文件系统对 namenod 创建一个 RPC 调用,在文件系统的**命名空间中新建一个文件**。 -3. Namenode 对新建文件进行检查无误后,分布式文件系统返回给客户端一个 FSDataOutputStream 对象,FSDataOutputStream 对象封装一个 DFSoutPutstream 对象,负责处理 namenode 和 datanode 之间的通信,**客户端开始写入数据**。 -4. FSDataOutputStream 将**数据分成一个一个的数据包,写入内部队列“数据队列”**,DataStreamer 负责将数据包依次流式传输到由一组 namenode 构成的管线中。 -5. DFSOutputStream 维护着确认队列来等待 datanode 收到确认回执,**收到管道中所有 datanode 确认后,数据包从确认队列删**除。 -6. **客户端完成数据的写入**,对数据流调用 close() 方法。 -7. namenode **确认完成**。 - -## HDFS 数据复制 - -由于 Hadoop 被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS 提供了数据复制机制。HDFS 将每一个文件存储为一系列**块**,每个块由多个副本来保证容错,块的大小和复制因子可以自行配置(默认情况下,块大小是 128M,默认复制因子是 3)。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200224203958.png) - -**Namenode 全权管理数据块的复制**,它周期性地从集群中的每个 Datanode 接收心跳信号和块状态报告(Blockreport)。接收到心跳信号意味着该 Datanode 节点工作正常。块状态报告包含了一个该 Datanode 上所有数据块的列表。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-replica.png) - -大型的 HDFS 实例在通常分布在多个机架的多台服务器上,不同机架上的两台服务器之间通过交换机进行通讯。在大多数情况下,同一机架中的服务器间的网络带宽大于不同机架中的服务器之间的带宽。因此 HDFS 采用机架感知副本放置策略,对于常见情况,当复制因子为 3 时,HDFS 的放置策略是: - -- 副本 1:放在 Client 所在节点 - - 对于远程 Client,系统会随机选择节点 -- 副本 2:放在不同的机架节点上 -- 副本 3:放在与第二个副本同一机架的不同节点上 -- 副本 N:随机选择 -- 节点选择:同等条件下优先选择空闲节点 - -为了最大限度地减少带宽消耗和读取延迟,HDFS 在执行读取请求时,优先读取距离读取器最近的副本。如果在与读取器节点相同的机架上存在副本,则优先选择该副本。如果 HDFS 群集跨越多个数据中心,则优先选择本地数据中心上的副本。 - -## HDFS 高可用 - -数据存储故障容错 - -磁盘介质在存储过程中受环境或者老化影响,其存储的数据可能会出现错乱。HDFS 的应对措施是,对于存储在 DataNode 上的数据块,计算并存储校验和(CheckSum)。在读取数据的时候,重新计算读取出来的数据的校验和,如果校验不正确就抛出异常,应用程序捕获异常后就到其他 DataNode 上读取备份数据。 - -磁盘故障容错 - -如果 DataNode 监测到本机的某块磁盘损坏,就将该块磁盘上存储的所有 BlockID 报告给 NameNode,NameNode 检查这些数据块还在哪些 DataNode 上有备份,通知相应的 DataNode 服务器将对应的数据块复制到其他服务器上,以保证数据块的备份数满足要求。 - -DataNode 故障容错 - -DataNode 会通过心跳和 NameNode 保持通信,如果 DataNode 超时未发送心跳,NameNode 就会认为这个 DataNode 已经宕机失效,立即查找这个 DataNode 上存储的数据块有哪些,以及这些数据块还存储在哪些服务器上,随后通知这些服务器再复制一份数据块到其他服务器上,保证 HDFS 存储的数据块备份数符合用户设置的数目,即使再出现服务器宕机,也不会丢失数据。 - -NameNode 故障容错 - -NameNode 是整个 HDFS 的核心,记录着 HDFS 文件分配表信息,所有的文件路径和数据块存储信息都保存在 NameNode,如果 NameNode 故障,整个 HDFS 系统集群都无法使用;如果 NameNode 上记录的数据丢失,整个集群所有 DataNode 存储的数据也就没用了。 - -### NameNode 的 HA 机制 - -NameNode 通过 Active NameNode 和 Standby NameNode 实现主备。 - -- **Active NameNode** - 是正在工作的 NameNode; -- **Standby NameNode** - 是备份的 NameNode。 - -Active NameNode 宕机后,Standby NameNode 快速升级为新的 Active NameNode。 - -Standby NameNode 周期性同步 edits 编辑日志,定期合并 fsimage 与 edits 到本地磁盘。 - -Hadoop 3.0 允许配置多个 Standby NameNode。 - -### 元数据文件 - -- **edits(编辑日志文件)** - 保存了自最新检查点(Checkpoint)之后的所有文件更新操作。 -- **fsimage(元数据检查点镜像文件)** - 保存了文件系统中所有的目录和文件信息,如:某个目录下有哪些子目录和文件,以及文件名、文件副本数、文件由哪些 Block 组成等。 - -Active NameNode 内存中有一份最新的元数据(= fsimage + edits)。 - -Standby NameNode 在检查点定期将内存中的元数据保存到 fsimage 文件中。 - -### 利用 QJM 实现元数据高可用 - -> 基于 Paxos 算法 - -QJM 机制(Quorum Journal Manager) - -只要保证 Quorum(法定人数)数量的操作成功,就认为这是一次最终成功的操作 - -QJM 共享存储系统 - -- 部署奇数(2N+1)个 JournalNode -- JournalNode 负责存储 edits 编辑日志 -- 写 edits 的时候,只要超过半数(N+1)的 JournalNode 返回成功,就代表本次写入成功 -- 最多可容忍 N 个 JournalNode 宕机 - -利用 ZooKeeper 实现 Active 节点选举。 - -## 附:图解 HDFS 存储原理 - -> 说明:以下图片引用自博客:[翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) - -### HDFS 写数据原理 - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-write-1.jpg) - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-write-2.jpg) - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-write-3.jpg) - -### HDFS 读数据原理 - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-read-1.jpg) - -### HDFS 故障类型和其检测方法 - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-tolerance-1.jpg) - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-tolerance-2.jpg) - -**第二部分:读写故障的处理** - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-tolerance-3.jpg) - -**第三部分:DataNode 故障处理** - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-tolerance-4.jpg) - -**副本布局策略**: - -![img](https://github.com/heibaiying/BigData-Notes/raw/master/pictures/hdfs-tolerance-5.jpg) - -## 参考资料 - -- [HDFS 官方文档](http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html) -- [HDFS 知识点总结](https://www.cnblogs.com/caiyisen/p/7395843.html) -- [《Hadoop: The Definitive Guide, Fourth Edition》](http://shop.oreilly.com/product/0636920033448.do) by Tom White -- [http://hadoop.apache.org/docs/r1.0.4/cn/hdfs_design.html](http://hadoop.apache.org/docs/r1.0.4/cn/hdfs_design.html) -- [翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) \ No newline at end of file diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/02.HDFS\350\277\220\347\273\264.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/02.HDFS\350\277\220\347\273\264.md" deleted file mode 100644 index 5f4e16d126..0000000000 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/02.HDFS\350\277\220\347\273\264.md" +++ /dev/null @@ -1,199 +0,0 @@ ---- -title: HDFS 运维 -date: 2020-02-24 21:14:47 -order: 02 -categories: - - 大数据 - - hadoop - - hdfs -tags: - - 大数据 - - Hadoop - - HDFS -permalink: /pages/90aeb6/ ---- - -# HDFS 运维 - -## HDFS 命令 - -### 显示当前目录结构 - -```shell -# 显示当前目录结构 -hdfs dfs -ls -# 递归显示当前目录结构 -hdfs dfs -ls -R -# 显示根目录下内容 -hdfs dfs -ls / -``` - -### 创建目录 - -```shell -# 创建目录 -hdfs dfs -mkdir -# 递归创建目录 -hdfs dfs -mkdir -p -``` - -### 删除操作 - -```shell -# 删除文件 -hdfs dfs -rm -# 递归删除目录和文件 -hdfs dfs -rm -R -``` - -### 导入文件到 HDFS - -```shell -# 二选一执行即可 -hdfs dfs -put [localsrc] [dst] -hdfs dfs -copyFromLocal [localsrc] [dst] -``` - -### 从 HDFS 导出文件 - -```shell -# 二选一执行即可 -hdfs dfs -get [dst] [localsrc] -hdfs dfs -copyToLocal [dst] [localsrc] -``` - -### 查看文件内容 - -```shell -# 二选一执行即可 -hdfs dfs -text -hdfs dfs -cat -``` - -### 显示文件的最后一千字节 - -```shell -hdfs dfs -tail -# 和Linux下一样,会持续监听文件内容变化 并显示文件的最后一千字节 -hdfs dfs -tail -f -``` - -### 拷贝文件 - -```shell -hdfs dfs -cp [src] [dst] -``` - -### 移动文件 - -```shell -hdfs dfs -mv [src] [dst] -``` - -### 统计当前目录下各文件大小 - -- 默认单位字节 -- -s : 显示所有文件大小总和, -- -h : 将以更友好的方式显示文件大小(例如 64.0m 而不是 67108864) - -``` -hdfs dfs -du -``` - -### 合并下载多个文件 - -- -nl 在每个文件的末尾添加换行符(LF) -- -skip-empty-file 跳过空文件 - -``` -hdfs dfs -getmerge -# 示例 将HDFS上的hbase-policy.xml和hbase-site.xml文件合并后下载到本地的/usr/test.xml -hdfs dfs -getmerge -nl /test/hbase-policy.xml /test/hbase-site.xml /usr/test.xml -``` - -### 统计文件系统的可用空间信息 - -``` -hdfs dfs -df -h / -``` - -### 更改文件复制因子 - -``` -hdfs dfs -setrep [-R] [-w] -``` - -- 更改文件的复制因子。如果 path 是目录,则更改其下所有文件的复制因子 -- -w : 请求命令是否等待复制完成 - -``` -# 示例 -hdfs dfs -setrep -w 3 /user/hadoop/dir1 -``` - -### 权限控制 - -``` -# 权限控制和Linux上使用方式一致 -# 变更文件或目录的所属群组。 用户必须是文件的所有者或超级用户。 -hdfs dfs -chgrp [-R] GROUP URI [URI ...] -# 修改文件或目录的访问权限 用户必须是文件的所有者或超级用户。 -hdfs dfs -chmod [-R] URI [URI ...] -# 修改文件的拥有者 用户必须是超级用户。 -hdfs dfs -chown [-R] [OWNER][:[GROUP]] URI [URI ] -``` - -### 文件检测 - -``` -hdfs dfs -test - [defsz] URI -``` - -可选选项: - -- -d:如果路径是目录,返回 0。 -- -e:如果路径存在,则返回 0。 -- -f:如果路径是文件,则返回 0。 -- -s:如果路径不为空,则返回 0。 -- -r:如果路径存在且授予读权限,则返回 0。 -- -w:如果路径存在且授予写入权限,则返回 0。 -- -z:如果文件长度为零,则返回 0。 - -``` -# 示例 -hdfs dfs -test -e filename -``` - -## HDFS 安全模式 - -### 什么是安全模式? - -- 安全模式是 HDFS 的一种特殊状态,在这种状态下,HDFS 只接收读数据请求,而不接收写入、删除、修改等变更请求。 -- 安全模式是 HDFS 确保 Block 数据安全的一种保护机制。 -- Active NameNode 启动时,HDFS 会进入安全模式,DataNode 主动向 NameNode 汇报可用 Block 列表等信息,在系统达到安全标准前,HDFS 一直处于“只读”状态。 - -### 何时正常离开安全模式 - -- Block 上报率:DataNode 上报的可用 Block 个数 / NameNode 元数据记录的 Block 个数 -- 当 Block 上报率 >= 阈值时,HDFS 才能离开安全模式,默认阈值为 0.999 -- 不建议手动强制退出安全模式 - -### 触发安全模式的原因 - -- NameNode 重启 -- NameNode 磁盘空间不足 -- Block 上报率低于阈值 -- DataNode 无法正常启动 -- 日志中出现严重异常 -- 用户操作不当,如:**强制关机(特别注意!)** - -### 故障排查 - -- 找到 DataNode 不能正常启动的原因,重启 DataNode -- 清理 NameNode 磁盘 -- 谨慎操作,有问题找星环,以免丢失数据 - -## 参考资料 - -- [HDFS 官方文档](http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html) -- [HDFS 知识点总结](https://www.cnblogs.com/caiyisen/p/7395843.html) \ No newline at end of file diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/README.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/README.md" deleted file mode 100644 index a9dd26426c..0000000000 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/README.md" +++ /dev/null @@ -1,45 +0,0 @@ ---- -title: HDFS 教程 -date: 2022-02-21 20:26:47 -categories: - - 大数据 - - hadoop - - hdfs -tags: - - 大数据 - - Hadoop - - HDFS -permalink: /pages/8d798e/ -hidden: true -index: false ---- - -# HDFS 教程 - -> **HDFS** 是 **Hadoop Distributed File System** 的缩写,即 Hadoop 的分布式文件系统。 -> -> HDFS 是一种用于存储具有流数据访问模式的超大文件的文件系统,它运行在廉价的机器集群上。 -> -> HDFS 的设计目标是管理数以千计的服务器、数以万计的磁盘,将这么大规模的服务器计算资源当作一个单一的存储系统进行管理,对应用程序提供数以 PB 计的存储容量,让应用程序像使用普通文件系统一样存储大规模的文件数据。 -> -> HDFS 是在一个大规模分布式服务器集群上,对数据分片后进行并行读写及冗余存储。因为 HDFS 可以部署在一个比较大的服务器集群上,集群中所有服务器的磁盘都可供 HDFS 使用,所以整个 HDFS 的存储空间可以达到 PB 级容量。 - -## 📖 内容 - -- [HDFS 入门](01.HDFS入门.md) -- [HDFS 运维](02.HDFS运维.md) -- [HDFS Java API](03.HDFSJavaApi.md) - -## 📚 资料 - -- **官方** - - [HDFS 官方文档](http://hadoop.apache.org/docs/current/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html) -- **书籍** - - [《Hadoop 权威指南(第四版)》](https://item.jd.com/12109713.html) -- **文章** - - [翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) - - [HDFS 知识点总结](https://www.cnblogs.com/caiyisen/p/7395843.html) - -## 🚪 传送 - -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/02.yarn.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/02.yarn.md" deleted file mode 100644 index 88d4157ce5..0000000000 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/02.yarn.md" +++ /dev/null @@ -1,162 +0,0 @@ ---- -title: YARN -date: 2019-05-07 20:19:25 -order: 02 -categories: - - 大数据 - - hadoop -tags: - - 大数据 - - Hadoop - - YARN -permalink: /pages/406588/ ---- - -# YARN - -> YARN 的目标是解决 MapReduce 的缺陷。 - -## MapReduce 的缺陷(Hadoop 1.x) - -- 身兼两职:计算框架 + 资源管理框架 -- JobTracker - - 既做资源管理,又做任务调度 - - 任务太重,开销过大 - - 存在单点故障 -- 资源描述模型过于简单,资源利用率较低 - - 仅把 Task 数量看作资源,没有考虑 CPU 和内存 - - 强制把资源分成 Map Task Slot 和 Reduce Task Slot -- 扩展性较差,集群规模上限 4K -- 源码难于理解,升级维护困难 - -## YARN 简介 - -YARN(Yet Another Resource Negotiator,另一种资源管理器)是一个**分布式通用资源管理系统**。 - -设计目标:聚焦资源管理、通用(适用各种计算框架)、高可用、高扩展。 - -## YARN 系统架构 - -- 主从结构(master/slave) -- 将 JobTracker 的资源管理、任务调度功能分离 -- 三种角色: - - ResourceManager(Master) - 集群资源的统一管理和分配 - - NodeManager(Slave) - 管理节点资源,以及容器的生命周期 - - ApplicationMaster(新角色) - 管理应用程序实例,包括任务调度和资源申请 - -### ResourceManager(RM) - -**主要功能** - -- 统一管理集群的所有资源 -- 将资源按照一定策略分配给各个应用(ApplicationMaster) -- 接收 NodeManager 的资源上报信息 - -**核心组件** - -- 用户交互服务(User Service) -- NodeManager 管理 -- ApplicationMaster 管理 -- Application 管理 -- 安全管理 -- 资源管理 - -### NodeManager(NM) - -**主要功能** - -- 管理单个节点的资源 -- 向 ResourceManager 汇报节点资源使用情况 -- 管理 Container 的生命周期 - -**核心组件** - -- NodeStatusUpdater -- ContainerManager -- ContainerExecutor -- NodeHealthCheckerService -- Security -- WebServer - -### ApplicationMaster(AM) - -**主要功能** - -- 管理应用程序实例 -- 向 ResourceManager 申请任务执行所需的资源 -- 任务调度和监管 - -**实现方式** - -- 需要为每个应用开发一个 AM 组件 -- YARN 提供 MapReduce 的 ApplicationMaster 实现 -- 采用基于事件驱动的异步编程模型,由中央事件调度器统一管理所有事件 -- 每种组件都是一种事件处理器,在中央事件调度器中注册 - -### Container - -- 概念:Container 封装了节点上进程的相关资源,是 YARN 中资源的抽象 -- 分类:运行 ApplicationMaster 的 Container 、运行应用任务的 Container - -## YARN 高可用 - -ResourceManager 高可用 - -- 1 个 Active RM、多个 Standby RM -- 宕机后自动实现主备切换 -- ZooKeeper 的核心作用 - - Active 节点选举 - - 恢复 Active RM 的原有状态信息 -- 重启 AM,杀死所有运行中的 Container -- 切换方式:手动、自动 - -## YARN 资源调度策略 - -### FIFO Scheduler(先进先出调度器) - -**调度策略** - -将所有任务放入一个队列,先进队列的先获得资源,排在后面的任务只有等待 - -**缺点** - -- 资源利用率低,无法交叉运行任务 -- 灵活性差,如:紧急任务无法插队,耗时长的任务拖慢耗时短的任务 - -### Capacity Scheduler(容量调度器) - -**核心思想** - 提前**做预算**,在预算指导下分享集群资源。 - -**调度策略** - -- 集群资源由多个队列分享 -- 每个队列都要预设资源分配的比例(提前做预算) -- 空闲资源优先分配给“实际资源/预算资源”比值最低的队列 -- 队列内部采用 FIFO 调度策略 - -**特点** - -- 层次化的队列设计:子队列可使用父队列资源 -- 容量保证:每个队列都要预设资源占比,防止资源独占 -- 弹性分配:空闲资源可以分配给任何队列,当多个队列争用时,会按比例进行平衡 -- 支持动态管理:可以动态调整队列的容量、权限等参数,也可动态增加、暂停队列 -- 访问控制:用户只能向自己的队列中提交任务,不能访问其他队列 -- 多租户:多用户共享集群资源 - -### Fair Scheduler(公平调度器) - -**调度策略** - -- 多队列公平共享集群资源 -- 通过平分的方式,动态分配资源,无需预先设定资源分配比例 -- 队列内部可配置调度策略:FIFO、Fair(默认) - -**资源抢占** - -- 终止其他队列的任务,使其让出所占资源,然后将资源分配给占用资源量少于最小资源量限制的队列 - -**队列权重** - -- 当队列中有任务等待,并且集群中有空闲资源时,每个队列可以根据权重获得不同比例的空闲资源 - -## 资源 \ No newline at end of file diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/03.mapreduce.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/03.mapreduce.md" deleted file mode 100644 index a9ae833f6e..0000000000 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/03.mapreduce.md" +++ /dev/null @@ -1,374 +0,0 @@ ---- -title: MapReduce -date: 2020-06-22 00:22:25 -order: 03 -categories: - - 大数据 - - hadoop -tags: - - 大数据 - - Hadoop - - MapReduce -permalink: /pages/7644aa/ ---- - -# MapReduce - -## MapReduce 简介 - -> Hadoop MapReduce 是一个分布式计算框架,用于编写批处理应用程序。编写好的程序可以提交到 Hadoop 集群上用于并行处理大规模的数据集。 - -MapReduce 的设计思路是: - -- 分而治之,并行计算 -- 移动计算,而非移动数据 - -MapReduce 作业通过将输入的数据集拆分为独立的块,这些块由 `map` 以并行的方式处理,框架对 `map` 的输出进行排序,然后输入到 `reduce` 中。MapReduce 框架专门用于 `` 键值对处理,它将作业的输入视为一组 `` 对,并生成一组 `` 对作为输出。输出和输出的 `key` 和 `value` 都必须实现[Writable](http://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/Writable.html) 接口。 - -``` -(input) -> map -> -> combine -> -> reduce -> (output) -``` - -### 特点 - -- 计算跟着数据走 -- 良好的扩展性:计算能力随着节点数增加,近似线性递增 -- 高容错 -- 状态监控 -- 适合海量数据的离线批处理 -- 降低了分布式编程的门槛 - -### 应用场景 - -适用场景: - -- 数据统计,如:网站的 PV、UV 统计 -- 搜索引擎构建索引 -- 海量数据查询 - -不适用场景: - -- OLAP - - 要求毫秒或秒级返回结果 -- 流计算 - - 流计算的输入数据集是动态的,而 MapReduce 是静态的 -- DAG 计算 - - 多个作业存在依赖关系,后一个的输入是前一个的输出,构成有向无环图 DAG - - 每个 MapReduce 作业的输出结果都会落盘,造成大量磁盘 IO,导致性能非常低下 - -## MapReduce 编程模型 - -MapReduce 编程模型:MapReduce 程序被分为 Map(映射)阶段和 Reduce(化简)阶段。 - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601162305.png) - -1. **input** : 读取文本文件; -2. **splitting** : 将文件按照行进行拆分,此时得到的 `K1` 行数,`V1` 表示对应行的文本内容; -3. **mapping** : 并行将每一行按照空格进行拆分,拆分得到的 `List(K2,V2)`,其中 `K2` 代表每一个单词,由于是做词频统计,所以 `V2` 的值为 1,代表出现 1 次; -4. **shuffling**:由于 `Mapping` 操作可能是在不同的机器上并行处理的,所以需要通过 `shuffling` 将相同 `key` 值的数据分发到同一个节点上去合并,这样才能统计出最终的结果,此时得到 `K2` 为每一个单词,`List(V2)` 为可迭代集合,`V2` 就是 Mapping 中的 V2; -5. **Reducing** : 这里的案例是统计单词出现的总次数,所以 `Reducing` 对 `List(V2)` 进行归约求和操作,最终输出。 - -MapReduce 编程模型中 `splitting` 和 `shuffing` 操作都是由框架实现的,需要我们自己编程实现的只有 `mapping` 和 `reducing`,这也就是 MapReduce 这个称呼的来源。 - -## combiner & partitioner - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601163846.png) - -### InputFormat & RecordReaders - -`InputFormat` 将输出文件拆分为多个 `InputSplit`,并由 `RecordReaders` 将 `InputSplit` 转换为标准的键值对,作为 map 的输出。这一步的意义在于只有先进行逻辑拆分并转为标准的键值对格式后,才能为多个 `map` 提供输入,以便进行并行处理。 - -### Combiner - -`combiner` 是 `map` 运算后的可选操作,它实际上是一个本地化的 `reduce` 操作,它主要是在 `map` 计算出中间文件后做一个简单的合并重复 `key` 值的操作。这里以词频统计为例: - -`map` 在遇到一个 hadoop 的单词时就会记录为 1,但是这篇文章里 hadoop 可能会出现 n 多次,那么 `map` 输出文件冗余就会很多,因此在 `reduce` 计算前对相同的 key 做一个合并操作,那么需要传输的数据量就会减少,传输效率就可以得到提升。 - -但并非所有场景都适合使用 `combiner`,使用它的原则是 `combiner` 的输出不会影响到 `reduce` 计算的最终输入,例如:求总数,最大值,最小值时都可以使用 `combiner`,但是做平均值计算则不能使用 `combiner`。 - -不使用 combiner 的情况: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601164709.png) - -使用 combiner 的情况: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601164804.png) - -可以看到使用 combiner 的时候,需要传输到 reducer 中的数据由 12keys,降低到 10keys。降低的幅度取决于你 keys 的重复率,下文词频统计案例会演示用 combiner 降低数百倍的传输量。 - -## MapReduce 词频统计案例 - -### 项目简介 - -这里给出一个经典的词频统计的案例:统计如下样本数据中每个单词出现的次数。 - -``` -Spark HBase -Hive Flink Storm Hadoop HBase Spark -Flink -HBase Storm -HBase Hadoop Hive Flink -HBase Flink Hive Storm -Hive Flink Hadoop -HBase Hive -Hadoop Spark HBase Storm -HBase Hadoop Hive Flink -HBase Flink Hive Storm -Hive Flink Hadoop -HBase Hive -``` - -为方便大家开发,我在项目源码中放置了一个工具类 `WordCountDataUtils`,用于模拟产生词频统计的样本,生成的文件支持输出到本地或者直接写到 HDFS 上。 - -> 项目完整源码下载地址:[hadoop-word-count](https://github.com/heibaiying/BigData-Notes/tree/master/code/Hadoop/hadoop-word-count) - -### 项目依赖 - -想要进行 MapReduce 编程,需要导入 `hadoop-client` 依赖: - -```xml - - org.apache.hadoop - hadoop-client - ${hadoop.version} - -``` - -### WordCountMapper - -将每行数据按照指定分隔符进行拆分。这里需要注意在 MapReduce 中必须使用 Hadoop 定义的类型,因为 Hadoop 预定义的类型都是可序列化,可比较的,所有类型均实现了 `WritableComparable` 接口。 - -```java -public class WordCountMapper extends Mapper { - - @Override - protected void map(LongWritable key, Text value, Context context) throws IOException, - InterruptedException { - String[] words = value.toString().split("\t"); - for (String word : words) { - context.write(new Text(word), new IntWritable(1)); - } - } - -} -``` - -`WordCountMapper` 对应下图的 Mapping 操作: - -[![img](https://camo.githubusercontent.com/fcf3ac016579c1fbb050d97309660aaa3b9550cc/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d636f64652d6d617070696e672e706e67)](https://camo.githubusercontent.com/fcf3ac016579c1fbb050d97309660aaa3b9550cc/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d636f64652d6d617070696e672e706e67) - -`WordCountMapper` 继承自 `Mappe` 类,这是一个泛型类,定义如下: - -```java -WordCountMapper extends Mapper - -public class Mapper { - ...... -} -``` - -- **KEYIN** : `mapping` 输入 key 的类型,即每行的偏移量 (每行第一个字符在整个文本中的位置),`Long` 类型,对应 Hadoop 中的 `LongWritable` 类型; -- **VALUEIN** : `mapping` 输入 value 的类型,即每行数据;`String` 类型,对应 Hadoop 中 `Text` 类型; -- **KEYOUT** :`mapping` 输出的 key 的类型,即每个单词;`String` 类型,对应 Hadoop 中 `Text` 类型; -- **VALUEOUT**:`mapping` 输出 value 的类型,即每个单词出现的次数;这里用 `int` 类型,对应 `IntWritable` 类型。 - -### WordCountReducer - -在 Reduce 中进行单词出现次数的统计: - -```java -public class WordCountReducer extends Reducer { - - @Override - protected void reduce(Text key, Iterable values, Context context) throws IOException, - InterruptedException { - int count = 0; - for (IntWritable value : values) { - count += value.get(); - } - context.write(key, new IntWritable(count)); - } -} -``` - -如下图,`shuffling` 的输出是 reduce 的输入。这里的 key 是每个单词,values 是一个可迭代的数据类型,类似 `(1,1,1,...)`。 - -[![img](https://camo.githubusercontent.com/bf6ad9c970812b1db6f6f86d12d783db75eaa9fb/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d636f64652d726564756365722e706e67)](https://camo.githubusercontent.com/bf6ad9c970812b1db6f6f86d12d783db75eaa9fb/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d636f64652d726564756365722e706e67) - -### WordCountApp - -组装 MapReduce 作业,并提交到服务器运行,代码如下: - -```java -/** - * 组装作业 并提交到集群运行 - */ -public class WordCountApp { - - - // 这里为了直观显示参数 使用了硬编码,实际开发中可以通过外部传参 - private static final String HDFS_URL = "hdfs://192.168.0.107:8020"; - private static final String HADOOP_USER_NAME = "root"; - - public static void main(String[] args) throws Exception { - - // 文件输入路径和输出路径由外部传参指定 - if (args.length < 2) { - System.out.println("Input and output paths are necessary!"); - return; - } - - // 需要指明 hadoop 用户名,否则在 HDFS 上创建目录时可能会抛出权限不足的异常 - System.setProperty("HADOOP_USER_NAME", HADOOP_USER_NAME); - - Configuration configuration = new Configuration(); - // 指明 HDFS 的地址 - configuration.set("fs.defaultFS", HDFS_URL); - - // 创建一个 Job - Job job = Job.getInstance(configuration); - - // 设置运行的主类 - job.setJarByClass(WordCountApp.class); - - // 设置 Mapper 和 Reducer - job.setMapperClass(WordCountMapper.class); - job.setReducerClass(WordCountReducer.class); - - // 设置 Mapper 输出 key 和 value 的类型 - job.setMapOutputKeyClass(Text.class); - job.setMapOutputValueClass(IntWritable.class); - - // 设置 Reducer 输出 key 和 value 的类型 - job.setOutputKeyClass(Text.class); - job.setOutputValueClass(IntWritable.class); - - // 如果输出目录已经存在,则必须先删除,否则重复运行程序时会抛出异常 - FileSystem fileSystem = FileSystem.get(new URI(HDFS_URL), configuration, HADOOP_USER_NAME); - Path outputPath = new Path(args[1]); - if (fileSystem.exists(outputPath)) { - fileSystem.delete(outputPath, true); - } - - // 设置作业输入文件和输出文件的路径 - FileInputFormat.setInputPaths(job, new Path(args[0])); - FileOutputFormat.setOutputPath(job, outputPath); - - // 将作业提交到群集并等待它完成,参数设置为 true 代表打印显示对应的进度 - boolean result = job.waitForCompletion(true); - - // 关闭之前创建的 fileSystem - fileSystem.close(); - - // 根据作业结果,终止当前运行的 Java 虚拟机,退出程序 - System.exit(result ? 0 : -1); - - } -} -``` - -需要注意的是:如果不设置 `Mapper` 操作的输出类型,则程序默认它和 `Reducer` 操作输出的类型相同。 - -### 提交到服务器运行 - -在实际开发中,可以在本机配置 hadoop 开发环境,直接在 IDE 中启动进行测试。这里主要介绍一下打包提交到服务器运行。由于本项目没有使用除 Hadoop 外的第三方依赖,直接打包即可: - -``` -# mvn clean package -``` - -使用以下命令提交作业: - -``` -hadoop jar /usr/appjar/hadoop-word-count-1.0.jar \ -com.heibaiying.WordCountApp \ -/wordcount/input.txt /wordcount/output/WordCountApp -``` - -作业完成后查看 HDFS 上生成目录: - -``` -# 查看目录 -hadoop fs -ls /wordcount/output/WordCountApp - -# 查看统计结果 -hadoop fs -cat /wordcount/output/WordCountApp/part-r-00000 -``` - -[![img](https://camo.githubusercontent.com/cecb00eef3b951794fbf92b8308d8b6601faf5a0/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d776f7264636f756e746170702e706e67)](https://camo.githubusercontent.com/cecb00eef3b951794fbf92b8308d8b6601faf5a0/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d776f7264636f756e746170702e706e67) - -## 词频统计案例进阶之 Combiner - -### 代码实现 - -想要使用 `combiner` 功能只要在组装作业时,添加下面一行代码即可: - -``` -// 设置 Combiner -job.setCombinerClass(WordCountReducer.class); -``` - -### 执行结果 - -加入 `combiner` 后统计结果是不会有变化的,但是可以从打印的日志看出 `combiner` 的效果: - -没有加入 `combiner` 的打印日志: - -[![img](https://camo.githubusercontent.com/e4849556db34d3a02b82b546af7296154920dfff/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d6e6f2d636f6d62696e65722e706e67)](https://camo.githubusercontent.com/e4849556db34d3a02b82b546af7296154920dfff/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d6e6f2d636f6d62696e65722e706e67) - -加入 `combiner` 后的打印日志如下: - -[![img](https://camo.githubusercontent.com/17f20f481bc4bd01252bc3ccb3b2aceb7d0eca63/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d636f6d62696e65722e706e67)](https://camo.githubusercontent.com/17f20f481bc4bd01252bc3ccb3b2aceb7d0eca63/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d636f6d62696e65722e706e67) - -这里我们只有一个输入文件并且小于 128M,所以只有一个 Map 进行处理。可以看到经过 combiner 后,records 由 `3519` 降低为 `6`(样本中单词种类就只有 6 种),在这个用例中 combiner 就能极大地降低需要传输的数据量。 - -## 词频统计案例进阶之 Partitioner - -### 默认的 Partitioner - -这里假设有个需求:将不同单词的统计结果输出到不同文件。这种需求实际上比较常见,比如统计产品的销量时,需要将结果按照产品种类进行拆分。要实现这个功能,就需要用到自定义 `Partitioner`。 - -这里先介绍下 MapReduce 默认的分类规则:在构建 job 时候,如果不指定,默认的使用的是 `HashPartitioner`:对 key 值进行哈希散列并对 `numReduceTasks` 取余。其实现如下: - -``` -public class HashPartitioner extends Partitioner { - - public int getPartition(K key, V value, - int numReduceTasks) { - return (key.hashCode() & Integer.MAX_VALUE) % numReduceTasks; - } - -} -``` - -### 自定义 Partitioner - -这里我们继承 `Partitioner` 自定义分类规则,这里按照单词进行分类: - -``` -public class CustomPartitioner extends Partitioner { - - public int getPartition(Text text, IntWritable intWritable, int numPartitions) { - return WordCountDataUtils.WORD_LIST.indexOf(text.toString()); - } -} -``` - -在构建 `job` 时候指定使用我们自己的分类规则,并设置 `reduce` 的个数: - -``` -// 设置自定义分区规则 -job.setPartitionerClass(CustomPartitioner.class); -// 设置 reduce 个数 -job.setNumReduceTasks(WordCountDataUtils.WORD_LIST.size()); -``` - -### 执行结果 - -执行结果如下,分别生成 6 个文件,每个文件中为对应单词的统计结果: - -[![img](https://camo.githubusercontent.com/202b1eb7065e18a513db5b2a50b22ab62a7d6692/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d776f7264636f756e74636f6d62696e6572706172746974696f6e2e706e67)](https://camo.githubusercontent.com/202b1eb7065e18a513db5b2a50b22ab62a7d6692/68747470733a2f2f67697465652e636f6d2f68656962616979696e672f426967446174612d4e6f7465732f7261772f6d61737465722f70696374757265732f6861646f6f702d776f7264636f756e74636f6d62696e6572706172746974696f6e2e706e67) - -## 参考资料 - -- [分布式计算框架——MapReduce](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Hadoop-MapReduce.md) \ No newline at end of file diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/README.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/README.md" deleted file mode 100644 index 49f710f600..0000000000 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/README.md" +++ /dev/null @@ -1,31 +0,0 @@ ---- -title: Hive 教程 -date: 2020-09-09 17:53:08 -categories: - - 大数据 - - hive -tags: - - 大数据 - - Hive -permalink: /pages/a958fe/ -hidden: true -index: false ---- - -# Hive 教程 - -## 📖 内容 - -- [Hive 入门](01.Hive入门.md) -- [Hive 表](02.Hive表.md) -- [Hive 视图和索引](03.Hive视图和索引.md) -- [Hive 查询](04.Hive查询.md) -- [Hive DDL](05.HiveDDL.md) -- [Hive DML](06.HiveDML.md) -- [Hive 运维](07.Hive运维.md) - -## 📚 资料 - -## 🚪 传送 - -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/99.\345\205\266\344\273\226/01.flume.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/Flume.md" similarity index 98% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/99.\345\205\266\344\273\226/01.flume.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/Flume.md" index 6014cdd396..b1eb1fb114 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/99.\345\205\266\344\273\226/01.flume.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/Flume.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Flume -permalink: /pages/ac5a41/ +permalink: /pages/c9b51a18/ --- # Flume diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/README.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/README.md" index 4b741b62e2..b346f9e377 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/README.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/README.md" @@ -1,11 +1,11 @@ --- -title: 大数据教程 +title: 大数据 date: 2023-02-10 14:52:25 categories: - 大数据 tags: - 大数据 -permalink: /pages/fc832f/ +permalink: /pages/0789a777/ hidden: true index: false --- @@ -37,59 +37,93 @@ index: false ## 📖 内容 -### [综合](00.综合) +### [综合](综合) -- [大数据简介](00.综合/01.大数据简介.md) -- [大数据学习](00.综合/02.大数据学习.md) +- [大数据简介](综合/01.大数据简介.md) +- [大数据学习](综合/02.大数据学习.md) -### [Hadoop](01.hadoop) +### [Hadoop](hadoop) -#### [HDFS](01.hadoop/01.hdfs) +- [HDFS](hadoop/HDFS.md) +- [YARN](hadoop/YARN.md) +- [MapReduce](hadoop/MapReduce.md) +- [Hadoop 面试](hadoop/Hadoop面试.md) 💯 -- [HDFS 入门](01.hadoop/01.hdfs/01.HDFS入门.md) -- [HDFS 运维](01.hadoop/01.hdfs/02.HDFS运维.md) -- [HDFS Java API](01.hadoop/01.hdfs/03.HDFSJavaApi.md) +### [HIVE](hive) -### [HIVE](02.hive) - -- [Hive 入门](02.hive/01.Hive入门.md) -- [Hive 表](02.hive/02.Hive表.md) -- [Hive 视图和索引](02.hive/03.Hive视图和索引.md) -- [Hive 查询](02.hive/04.Hive查询.md) -- [Hive DDL](02.hive/05.HiveDDL.md) -- [Hive DML](02.hive/06.HiveDML.md) -- [Hive 运维](02.hive/07.Hive运维.md) +- [Hive 入门](hive/Hive入门.md) +- [Hive 表](hive/Hive表.md) +- [Hive 视图和索引](hive/Hive视图和索引.md) +- [Hive 查询](hive/Hive查询.md) +- [Hive DDL](hive/HiveDDL.md) +- [Hive DML](hive/HiveDML.md) +- [Hive 运维](hive/Hive运维.md) ### Kafka -> **[Kafka](https://dunwu.github.io/waterdrop/pages/328f1c/) 是一个分布式流处理平台,此外,它也被广泛应用于消息队列**。 +> **[Kafka](https://dunwu.github.io/waterdrop/pages/260fb327/) 是一个分布式流处理平台,此外,它也被广泛应用于消息队列**。 + +- [Kafka 快速入门](https://dunwu.github.io/waterdrop/pages/838a5f6a/) +- [Kafka 生产](https://dunwu.github.io/waterdrop/pages/f49f3bd2/) +- [Kafka 消费](https://dunwu.github.io/waterdrop/pages/4952bbd2/) +- [Kafka 集群](https://dunwu.github.io/waterdrop/pages/32977605/) +- [Kafka 可靠传输](https://dunwu.github.io/waterdrop/pages/4c187841/) +- [Kafka 存储](https://dunwu.github.io/waterdrop/pages/4d7aaaa2/) +- [Kafka 流式处理](https://dunwu.github.io/waterdrop/pages/640d44c6/) +- [Kafka 运维](https://dunwu.github.io/waterdrop/pages/91694ba0/) + +### 其他 -- [Kafka 快速入门](https://dunwu.github.io/waterdrop/pages/a697a6/) -- [Kafka 生产者](https://dunwu.github.io/waterdrop/pages/141b2e/) -- [Kafka 消费者](https://dunwu.github.io/waterdrop/pages/41a171/) -- [Kafka 集群](https://dunwu.github.io/waterdrop/pages/fc8f54/) -- [Kafka 可靠传输](https://dunwu.github.io/waterdrop/pages/481bdd/) -- [Kafka 存储](https://dunwu.github.io/waterdrop/pages/8de948/) -- [Kafka 流式处理](https://dunwu.github.io/waterdrop/pages/55f66f/) -- [Kafka 运维](https://dunwu.github.io/waterdrop/pages/21011e/) +- [Spark](Spark.md) +- [Flume](Flume.md) +- [Sqoop](Sqoop.md) ## 📚 资料 - **综合** - **教程** - - [从 0 开始学大数据](https://time.geekbang.org/column/intro/100020201) + - [极客时间教程 - 从 0 开始学大数据](https://time.geekbang.org/column/intro/100020201) - [BigData-Notes](https://github.com/heibaiying/BigData-Notes) - **论文** - [The Google File System](https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/gfs-sosp2003.pdf) - Google 大数据三驾马车之一 - [Bigtable: A Distributed Storage System for Structured Data](https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/bigtable-osdi06.pdf) - Google 大数据三驾马车之一 - [MapReduce: Simplified Data Processing on Large Clusters](https://static.googleusercontent.com/media/research.google.com/zh-CN//archive/mapreduce-osdi04.pdf) - Google 大数据三驾马车之一 - **Hadoop** - - [《Hadoop 权威指南(第四版)》](https://item.jd.com/12109713.html) - - [《HBase 权威指南》](https://book.douban.com/subject/10748460/) - - [《Hive 编程指南》](https://book.douban.com/subject/25791255/) + - **官方** + - [Hadoop 官网](https://hadoop.apache.org/) + - [Hadoop 官方文档](https://hadoop.apache.org/docs/stable/index.html) + - [HDFS 官方文档](https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html) + - [MapReduce 官方文档](https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html) + - [YARN 官方文档](https://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html) + - **书籍** + - [《Hadoop 权威指南(第四版)》](https://book.douban.com/subject/27115351/) + - [《Hive 编程指南》](https://book.douban.com/subject/25791255/) + - **视频** + - [Hadoop In 5 Minutes | What Is Hadoop? | Introduction To Hadoop | Hadoop Explained |Simplilearn](https://www.youtube.com/watch?v=aReuLtY0YMI) + - **文章** + - [翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) + - [http://hadoop.apache.org/docs/r1.0.4/cn/hdfs_design.html](http://hadoop.apache.org/docs/r1.0.4/cn/hdfs_design.html) + - [HDFS面试题阅读指南(必看)](https://www.iamshuaidi.com/26263.html) - **Spark** - [《Spark 技术内幕 深入解析 Spark 内核架构设计与实现原理》](https://book.douban.com/subject/26649141/) - [《Spark.The.Definitive.Guide》](https://book.douban.com/subject/27035127/) +- **Kafka** + - **官方** + - [Kafka 官网](http://kafka.apache.org/) + - [Kafka Github](https://github.com/apache/kafka) + - [Kafka 官方文档](https://kafka.apache.org/documentation/) + - [Kafka Confluent 官网](http://kafka.apache.org/) + - [Kafka Jira](https://issues.apache.org/jira/projects/KAFKA?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page) + - **书籍** + - [《Kafka 权威指南》](https://item.jd.com/12270295.html) + - [《深入理解 Kafka:核心设计与实践原理》](https://item.jd.com/12489649.html) + - [《Kafka 技术内幕》](https://item.jd.com/12234113.html) + - **教程** + - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) + - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) + - [消息队列高手课](https://time.geekbang.org/column/intro/100032301) + - [Introduction and Overview of Apache Kafka](https://www.slideshare.net/mumrah/kafka-talk-tri-hug) + - **文章** - **ZooKeeper** - **官方** - [ZooKeeper 官网](http://zookeeper.apache.org/) @@ -97,7 +131,7 @@ index: false - [ZooKeeper Github](https://github.com/apache/zookeeper) - [Apache Curator 官网](http://curator.apache.org/) - **书籍** - - [《Hadoop 权威指南(第四版)》](https://item.jd.com/12109713.html) + - [《Hadoop 权威指南(第四版)》](https://book.douban.com/subject/27115351/) - [《从 Paxos 到 Zookeeper 分布式一致性原理与实践》](https://item.jd.com/11622772.html) - **文章** - [分布式服务框架 ZooKeeper -- 管理分布式环境中的数据](https://www.ibm.com/developerworks/cn/opensource/os-cn-zookeeper/index.html) @@ -107,24 +141,10 @@ index: false - [深入浅出 Zookeeper(一) Zookeeper 架构及 FastLeaderElection 机制](http://www.jasongj.com/zookeeper/fastleaderelection/) - [Introduction to Apache ZooKeeper](https://www.slideshare.net/sauravhaloi/introduction-to-apache-zookeeper) - [Zookeeper 的优缺点](https://blog.csdn.net/wwwsq/article/details/7644445) -- **Kafka** - - **官方** - - [Kafka 官网](http://kafka.apache.org/) - - [Kafka Github](https://github.com/apache/kafka) - - [Kafka 官方文档](https://kafka.apache.org/documentation/) - - [Kafka Confluent 官网](http://kafka.apache.org/) - - [Kafka Jira](https://issues.apache.org/jira/projects/KAFKA?selectedItem=com.atlassian.jira.jira-projects-plugin:components-page) +- **HBase** - **书籍** - - [《Kafka 权威指南》](https://item.jd.com/12270295.html) - - [《深入理解 Kafka:核心设计与实践原理》](https://item.jd.com/12489649.html) - - [《Kafka 技术内幕》](https://item.jd.com/12234113.html) - - **教程** - - [Kafka 中文文档](https://github.com/apachecn/kafka-doc-zh) - - [Kafka 核心技术与实战](https://time.geekbang.org/column/intro/100029201) - - [消息队列高手课](https://time.geekbang.org/column/intro/100032301) - - **文章** - - [Introduction and Overview of Apache Kafka](https://www.slideshare.net/mumrah/kafka-talk-tri-hug) + - [《HBase 权威指南》](https://book.douban.com/subject/10748460/) ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/11.spark/01.Spark\347\256\200\344\273\213.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/Spark.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/11.spark/01.Spark\347\256\200\344\273\213.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/Spark.md" index ed843b09b7..3b3a662160 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/11.spark/01.Spark\347\256\200\344\273\213.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/Spark.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Spark -permalink: /pages/80d4a7/ +permalink: /pages/196ff6f4/ --- # Spark diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/99.\345\205\266\344\273\226/02.sqoop.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/Sqoop.md" similarity index 98% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/99.\345\205\266\344\273\226/02.sqoop.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/Sqoop.md" index 44c52e4a0d..adbbc239db 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/99.\345\205\266\344\273\226/02.sqoop.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/Sqoop.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Sqoop -permalink: /pages/773408/ +permalink: /pages/42e9e354/ --- # Sqoop diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/05.FlinkApi.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkApi.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/05.FlinkApi.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkApi.md" index a465672adb..05930ccab5 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/05.FlinkApi.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkApi.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Flink -permalink: /pages/e7c9a9/ +permalink: /pages/dc12c339/ --- # Flink API diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/03.FlinkETL.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkETL.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/03.FlinkETL.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkETL.md" index 805b596b45..bf24170d99 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/03.FlinkETL.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkETL.md" @@ -9,7 +9,7 @@ tags: - 大数据 - Flink - ETL -permalink: /pages/6c8f32/ +permalink: /pages/b1c7186e/ --- # Flink ETL diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/08.FlinkTableApi.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkTableApi.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/08.FlinkTableApi.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkTableApi.md" index 516d755732..d92d7de7bf 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/08.FlinkTableApi.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/FlinkTableApi.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Flink -permalink: /pages/2288d8/ +permalink: /pages/6a507723/ --- # Flink Table API & SQL diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/04.Flink\344\272\213\344\273\266\351\251\261\345\212\250.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\344\272\213\344\273\266\351\251\261\345\212\250.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/04.Flink\344\272\213\344\273\266\351\251\261\345\212\250.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\344\272\213\344\273\266\351\251\261\345\212\250.md" index 60c2fa6df7..044931fae4 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/04.Flink\344\272\213\344\273\266\351\251\261\345\212\250.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\344\272\213\344\273\266\351\251\261\345\212\250.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Flink -permalink: /pages/692bd7/ +permalink: /pages/3f657cd8/ --- # Flink 事件驱动 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/01.Flink\345\205\245\351\227\250.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\345\205\245\351\227\250.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/01.Flink\345\205\245\351\227\250.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\345\205\245\351\227\250.md" index 663b5f464c..a1e43b3ed4 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/01.Flink\345\205\245\351\227\250.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\345\205\245\351\227\250.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Flink -permalink: /pages/cf625d/ +permalink: /pages/e59a148d/ --- # Flink 入门 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/06.Flink\346\236\266\346\236\204.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\346\236\266\346\236\204.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/06.Flink\346\236\266\346\236\204.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\346\236\266\346\236\204.md" index e547c79566..65e549e2bb 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/06.Flink\346\236\266\346\236\204.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\346\236\266\346\236\204.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Flink -permalink: /pages/373cfb/ +permalink: /pages/981cbe8e/ --- # Flink 架构 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/02.Flink\347\256\200\344\273\213.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\347\256\200\344\273\213.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/02.Flink\347\256\200\344\273\213.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\347\256\200\344\273\213.md" index 194fa907fe..42a709852e 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/02.Flink\347\256\200\344\273\213.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\347\256\200\344\273\213.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Flink -permalink: /pages/d848b7/ +permalink: /pages/c621d72e/ --- # Flink 简介 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/07.Flink\350\277\220\347\273\264.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\350\277\220\347\273\264.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/07.Flink\350\277\220\347\273\264.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\350\277\220\347\273\264.md" index 4e4146e9b8..19bc87d264 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/07.Flink\350\277\220\347\273\264.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/Flink\350\277\220\347\273\264.md" @@ -9,7 +9,7 @@ tags: - 大数据 - Flink - 运维 -permalink: /pages/38ec73/ +permalink: /pages/ee696c77/ --- # Flink 运维 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/README.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/README.md" similarity index 71% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/README.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/README.md" index 5b00af6a28..2063d7e8d0 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/13.flink/README.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/flink/README.md" @@ -7,7 +7,7 @@ categories: tags: - 大数据 - Flink -permalink: /pages/5c85bd/ +permalink: /pages/dbc3a92c/ hidden: true index: false --- @@ -18,21 +18,21 @@ index: false ## 📖 内容 -### [Flink 入门](01.Flink入门.md) +### [Flink 入门](Flink入门.md) -### [Flink 简介](02.Flink简介.md) +### [Flink 简介](Flink简介.md) -### [Flink ETL](03.FlinkETL.md) +### [Flink ETL](FlinkETL.md) -### [Flink 事件驱动](04.Flink事件驱动.md) +### [Flink 事件驱动](Flink事件驱动.md) -### [Flink API](05.FlinkApi.md) +### [Flink API](FlinkApi.md) -### [Flink 架构](06.Flink架构.md) +### [Flink 架构](Flink架构.md) -### [Flink 运维](07.Flink运维.md) +### [Flink 运维](Flink运维.md) -### [Flink Table API & SQL](08.FlinkTableApi.md) +### [Flink Table API & SQL](FlinkTableApi.md) ## 📚 资料 @@ -45,4 +45,4 @@ index: false ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/03.HDFSJavaApi.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/HDFS.md" similarity index 60% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/03.HDFSJavaApi.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/HDFS.md" index 7cbc81b287..32a5917d78 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/01.hdfs/03.HDFSJavaApi.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/HDFS.md" @@ -1,19 +1,181 @@ --- -title: HDFS Java API +icon: devicon:hadoop +title: HDFS 应用 date: 2020-02-24 21:14:47 -order: 03 categories: - 大数据 - hadoop - - hdfs tags: - 大数据 - - Hadoop - - HDFS -permalink: /pages/49a8dc/ + - hadoop + - hdfs +permalink: /pages/301a6069/ --- -# HDFS Java API +# HDFS 应用 + +**HDFS** 是 **Hadoop Distributed File System** 的缩写,即 Hadoop 的分布式文件系统。 + +HDFS 是一种用于**存储具有流数据访问模式的超大文件的文件系统**,它**运行在廉价的机器集群**上。 + +HDFS 的设计目标是管理数以千计的服务器、数以万计的磁盘,将这么大规模的服务器计算资源当作一个单一的存储系统进行管理,对应用程序提供 **PB 级**的存储容量,让应用程序像使用普通文件系统一样存储大规模的文件数据。 + +HDFS 是在一个大规模分布式服务器集群上,对数据分片后进行并行读写及冗余存储。因为 HDFS 可以部署在一个比较大的服务器集群上,集群中所有服务器的磁盘都可供 HDFS 使用,所以整个 HDFS 的存储空间可以达到 PB 级容量。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192251433.png) + +## HDFS 命令 + +### 显示当前目录结构 + +```shell +# 显示当前目录结构 +hdfs dfs -ls +# 递归显示当前目录结构 +hdfs dfs -ls -R +# 显示根目录下内容 +hdfs dfs -ls / +``` + +### 创建目录 + +```shell +# 创建目录 +hdfs dfs -mkdir +# 递归创建目录 +hdfs dfs -mkdir -p +``` + +### 删除操作 + +```shell +# 删除文件 +hdfs dfs -rm +# 递归删除目录和文件 +hdfs dfs -rm -R +``` + +### 导入文件到 HDFS + +```shell +# 二选一执行即可 +hdfs dfs -put [localsrc] [dst] +hdfs dfs -copyFromLocal [localsrc] [dst] +``` + +### 从 HDFS 导出文件 + +```shell +# 二选一执行即可 +hdfs dfs -get [dst] [localsrc] +hdfs dfs -copyToLocal [dst] [localsrc] +``` + +### 查看文件内容 + +```shell +# 二选一执行即可 +hdfs dfs -text +hdfs dfs -cat +``` + +### 显示文件的最后一千字节 + +```shell +hdfs dfs -tail +# 和 Linux 下一样,会持续监听文件内容变化 并显示文件的最后一千字节 +hdfs dfs -tail -f +``` + +### 拷贝文件 + +```shell +hdfs dfs -cp [src] [dst] +``` + +### 移动文件 + +```shell +hdfs dfs -mv [src] [dst] +``` + +### 统计当前目录下各文件大小 + +- 默认单位字节 +- -s : 显示所有文件大小总和, +- -h : 将以更友好的方式显示文件大小(例如 64.0m 而不是 67108864) + +```shell +hdfs dfs -du +``` + +### 合并下载多个文件 + +- -nl 在每个文件的末尾添加换行符(LF) +- -skip-empty-file 跳过空文件 + +```shell +hdfs dfs -getmerge +# 示例 将 HDFS 上的 hbase-policy.xml 和 hbase-site.xml 文件合并后下载到本地的/usr/test.xml +hdfs dfs -getmerge -nl /test/hbase-policy.xml /test/hbase-site.xml /usr/test.xml +``` + +### 统计文件系统的可用空间信息 + +```shell +hdfs dfs -df -h / +``` + +### 更改文件复制因子 + +```shell +hdfs dfs -setrep [-R] [-w] +``` + +- 更改文件的复制因子。如果 path 是目录,则更改其下所有文件的复制因子 +- -w : 请求命令是否等待复制完成 + +```shell +# 示例 +hdfs dfs -setrep -w 3 /user/hadoop/dir1 +``` + +### 权限控制 + +```shell +# 权限控制和 Linux 上使用方式一致 +# 变更文件或目录的所属群组。 用户必须是文件的所有者或超级用户。 +hdfs dfs -chgrp [-R] GROUP URI [URI ...] +# 修改文件或目录的访问权限 用户必须是文件的所有者或超级用户。 +hdfs dfs -chmod [-R] URI [URI ...] +# 修改文件的拥有者 用户必须是超级用户。 +hdfs dfs -chown [-R] [OWNER][:[GROUP]] URI [URI ] +``` + +### 文件检测 + +```shell +hdfs dfs -test - [defsz] URI +``` + +可选选项: + +- -d:如果路径是目录,返回 0。 +- -e:如果路径存在,则返回 0。 +- -f:如果路径是文件,则返回 0。 +- -s:如果路径不为空,则返回 0。 +- -r:如果路径存在且授予读权限,则返回 0。 +- -w:如果路径存在且授予写入权限,则返回 0。 +- -z:如果文件长度为零,则返回 0。 + +``` +# 示例 +hdfs dfs -test -e filename +``` + +## HDFS API + +### 简介 想要使用 HDFS API,需要导入依赖 `hadoop-client`。如果是 CDH 版本的 Hadoop,还需要额外指明其仓库地址: @@ -29,13 +191,11 @@ permalink: /pages/49a8dc/ hdfs-java-api 1.0 - UTF-8 2.6.0-cdh5.15.2 - @@ -44,7 +204,6 @@ permalink: /pages/49a8dc/ - @@ -63,9 +222,9 @@ permalink: /pages/49a8dc/ ``` -## FileSystem +### FileSystem -FileSystem 是所有 HDFS 操作的主入口。由于之后的每个单元测试都需要用到它,这里使用 `@Before` 注解进行标注。 +`FileSystem` 是所有 HDFS 操作的主入口。由于之后的每个单元测试都需要用到它,这里使用 `@Before` 注解进行标注。 ```java private static final String HDFS_PATH = "hdfs://192.168.0.106:8020"; @@ -76,7 +235,7 @@ private static FileSystem fileSystem; public void prepare() { try { Configuration configuration = new Configuration(); - // 这里我启动的是单节点的 Hadoop,所以副本系数设置为 1,默认值为 3 + // 这里我启动的是单节点的 Hadoop, 所以副本系数设置为 1, 默认值为 3 configuration.set("dfs.replication", "1"); fileSystem = FileSystem.get(new URI(HDFS_PATH), configuration, HDFS_USER); } catch (IOException e) { @@ -88,14 +247,15 @@ public void prepare() { } } - @After public void destroy() { fileSystem = null; } ``` -## 创建目录 +> [FileSystem 官方 Java API 文档](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/fs/FileSystem.html) + +### 创建目录 支持递归创建目录: @@ -106,7 +266,7 @@ public void mkDir() throws Exception { } ``` -## 创建指定权限的目录 +### 创建指定权限的目录 `FsPermission(FsAction u, FsAction g, FsAction o)` 的三个参数分别对应:创建者权限,同组其他用户权限,其他用户权限,权限值定义在 `FsAction` 枚举类中。 @@ -118,12 +278,12 @@ public void mkDirWithPermission() throws Exception { } ``` -## 创建文件,并写入内容 +### 创建文件,并写入内容 ```java @Test public void create() throws Exception { - // 如果文件存在,默认会覆盖, 可以通过第二个参数进行控制。第三个参数可以控制使用缓冲区的大小 + // 如果文件存在,默认会覆盖,可以通过第二个参数进行控制。第三个参数可以控制使用缓冲区的大小 FSDataOutputStream out = fileSystem.create(new Path("/hdfs-api/test/a.txt"), true, 4096); out.write("hello hadoop!".getBytes()); @@ -135,7 +295,7 @@ public void create() throws Exception { } ``` -## 判断文件是否存在 +### 判断文件是否存在 ```java @Test @@ -145,7 +305,7 @@ public void exist() throws Exception { } ``` -## 查看文件内容 +### 查看文件内容 查看小文本文件的内容,直接转换成字符串后输出: @@ -186,7 +346,7 @@ private static String inputStreamToString(InputStream inputStream, String encode } ``` -## 文件重命名 +### 文件重命名 ```java @Test @@ -198,21 +358,21 @@ public void rename() throws Exception { } ``` -## 删除目录或文件 +### 删除目录或文件 ```java public void delete() throws Exception { /* * 第二个参数代表是否递归删除 - * + 如果 path 是一个目录且递归删除为 true, 则删除该目录及其中所有文件; - * + 如果 path 是一个目录但递归删除为 false,则会则抛出异常。 + * + 如果 path 是一个目录且递归删除为 true, 则删除该目录及其中所有文件; + * + 如果 path 是一个目录但递归删除为 false, 则会则抛出异常。 */ boolean result = fileSystem.delete(new Path("/hdfs-api/test/b.txt"), true); System.out.println(result); } ``` -## 上传文件到 HDFS +### 上传文件到 HDFS ```java @Test @@ -224,7 +384,7 @@ public void copyFromLocalFile() throws Exception { } ``` -## 上传大文件并显示上传进度 +### 上传大文件并显示上传进度 ```java @Test @@ -250,7 +410,7 @@ public void copyFromLocalFile() throws Exception { } ``` -## 从 HDFS 上下载文件 +### 从 HDFS 上下载文件 ```java @Test @@ -258,17 +418,17 @@ public void copyToLocalFile() throws Exception { Path src = new Path("/hdfs-api/test/kafka.tgz"); Path dst = new Path("D:\\app\\"); /* - * 第一个参数控制下载完成后是否删除源文件,默认是 true,即删除; - * 最后一个参数表示是否将 RawLocalFileSystem 用作本地文件系统; - * RawLocalFileSystem 默认为 false,通常情况下可以不设置, - * 但如果你在执行时候抛出 NullPointerException 异常,则代表你的文件系统与程序可能存在不兼容的情况 (window 下常见), + * 第一个参数控制下载完成后是否删除源文件,默认是 true, 即删除; + * 最后一个参数表示是否将 RawLocalFileSystem 用作本地文件系统; + * RawLocalFileSystem 默认为 false, 通常情况下可以不设置, + * 但如果你在执行时候抛出 NullPointerException 异常,则代表你的文件系统与程序可能存在不兼容的情况 (window 下常见), * 此时可以将 RawLocalFileSystem 设置为 true */ fileSystem.copyToLocalFile(false, src, dst, true); } ``` -## 查看指定目录下所有文件的信息 +### 查看指定目录下所有文件的信息 ```java public void listFiles() throws Exception { @@ -295,7 +455,7 @@ isSymlink=false } ``` -## 递归查看指定目录下所有文件的信息 +### 递归查看指定目录下所有文件的信息 ```java @Test @@ -323,7 +483,7 @@ permission=rw-r--r--; isSymlink=false} ``` -## 查看文件的块信息 +### 查看文件的块信息 ```java @Test @@ -343,4 +503,9 @@ public void getFileBlockLocations() throws Exception { 0,57028557,hadoop001 ``` -这里我上传的文件只有 57M(小于 128M),且程序中设置了副本系数为 1,所有只有一个块信息。 \ No newline at end of file +这里我上传的文件只有 57M(小于 128M),且程序中设置了副本系数为 1,所有只有一个块信息。 + +## 参考资料 + +- https://github.com/heibaiying/BigData-Notes/blob/master/notes/HDFS%E5%B8%B8%E7%94%A8Shell%E5%91%BD%E4%BB%A4.md +- https://github.com/heibaiying/BigData-Notes/blob/master/notes/HDFS-Java-API.md diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/Hadoop\351\235\242\350\257\225.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/Hadoop\351\235\242\350\257\225.md" new file mode 100644 index 0000000000..2bf4bf6174 --- /dev/null +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/Hadoop\351\235\242\350\257\225.md" @@ -0,0 +1,718 @@ +--- +icon: openmoji:military-medal +title: Hadoop 面试 +date: 2020-02-24 21:14:47 +categories: + - 大数据 + - hadoop +tags: + - 大数据 + - hadoop + - hdfs + - yarn + - mapreduce +permalink: /pages/bbe8ffec/ +--- + +# Hadoop 面试 + +## 简介 + +### 【初级】简介一下大数据技术生态? + +:::details 要点 + +- **数据采集**:Flume、Sqoop、Logstash、Filebeat +- **分布式文件存储**:Hadoop HDFS +- **NoSql** + - **文档数据库**:Mongodb + - **列式数据库**:HBase + - **搜索引擎**:Solr、Elasticsearch +- **分布式计算** + - **批处理**:Hadoop MapReduce + - **流处理**:Storm、Kafka + - **混合处理**:Spark、Flink +- **查询分析**:Hive、Spark SQL、Flink SQL、Pig、Phoenix +- **集群资源管理**:Hadoop YARN +- **分布式协调**:Zookeeper +- **任务调度**:Azkaban、Oozie +- **集群部署和监控**:Ambari、Cloudera Manager + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192251433.png) + +::: + +### 【初级】什么是 HDFS? + +:::details 要点 + +**HDFS** 是 **Hadoop Distributed File System** 的缩写,即 Hadoop 的分布式文件系统。 + +HDFS 是一种用于**存储具有流数据访问模式的超大文件的文件系统**,它**运行在廉价的机器集群**上。 + +HDFS 的设计目标是管理数以千计的服务器、数以万计的磁盘,将这么大规模的服务器计算资源当作一个单一的存储系统进行管理,对应用程序提供 **PB 级**的存储容量,让应用程序像使用普通文件系统一样存储大规模的文件数据。 + +HDFS 是在一个大规模分布式服务器集群上,对数据分片后进行并行读写及冗余存储。因为 HDFS 可以部署在一个比较大的服务器集群上,集群中所有服务器的磁盘都可供 HDFS 使用,所以整个 HDFS 的存储空间可以达到 PB 级容量。 + +HDFS 的常见使用场景: + +- **大数据存储** - HDFS 能够存储 PB 级甚至 EB 级的数据,适合存储日志数据、传感器数据、社交媒体数据等。 +- **批处理与分析** - HDFS 是 Hadoop MapReduce 的默认存储系统,MapReduce 作业直接从 HDFS 读取数据并进行分布式计算。 +- **数据仓库** - HDFS 可以作为数据仓库的底层存储,支持大规模数据的离线分析。 +- **数据冷备** - 由于 HDFS 的高可靠和低成本,适用于存储访问频率较低的冷数据(如历史数据、备份数据)。 +- **多媒体数据存储**:HDFS 适合存储大规模的多媒体数据(如图像、视频、音频)。 + +::: + +### 【初级】HDFS 有什么特性(优缺点)? + +:::details 要点 + +**HDFS 的优点**: + +- **高可用** - 冗余数据副本,支持自动故障恢复;支持 NameNode HA、安全模式 +- **易扩展** - 能够处理 10K 节点的规模;处理数据达到 GB、TB、甚至 PB 级别的数据;能够处理百万规模以上的文件数量,数量相当之大。 +- **批处理** - 流式数据访问;数据位置暴露给计算框架 +- **低成本** - HDFS 构建在廉价的商用机器上。 + +**HDFS 的缺点**: + +- **不适合低延迟数据访问** - 适合高吞吐率的场景,就是在某一时间内写入大量的数据。但是它在低延时的情况下是不行的,比如毫秒级以内读取数据,它是很难做到的。 +- **不适合大量小文件存储** + - 存储大量小文件(这里的小文件是指小于 HDFS 系统的 Block 大小的文件(默认 64M)) 的话,它会占用 NameNode 大量的内存来存储文件、目录和块信息。这样是不可取的,因为 NameNode 的内存总是有限的。 + - 磁盘寻道时间超过读取时间 +- **不支持并发写入** - 一个文件同时只能有一个写入者 +- **不支持文件随机修改** - 仅支持追加写入 + +::: + +### 【初级】什么是 YARN? + +:::details 要点 + +**YARN**(Yet Another Resource Negotiator,即另一种资源调度器) 是 Hadoop 的**集群资源管理系统**。YARN 负责资源管理和调度。用户可以将各种服务框架部署在 YARN 上,由 YARN 进行统一地管理和资源分配。 + +在 Hadoop 1.x 版本,MapReduce 中的 jobTracker 担负了太多的责任,接收任务是它,资源调度是它,监控 TaskTracker 运行情况还是它。这样实现的好处是比较简单,但相对的,就容易出现一些问题,比如常见的单点故障问题。要解决这些问题,只能将 jobTracker 进行拆分,将其中部分功能拆解出来。沿着这个思路,于是有了 YARN。 + +::: + +### 【初级】什么是 MapReduce? + +:::details 要点 + +MapReduce 是 Hadoop 项目中的分布式计算框架。它降低了分布式计算的门槛,可以让用户轻松编写程序,让其以可靠、容错的方式运行在大型集群上并行处理海量数据(TB 级)。 + +MapReduce 的设计思路是: + +- 分而治之,并行计算 +- 移动计算,而非移动数据 + +MapReduce 作业通过将输入的数据集拆分为独立的块,这些块由 `map` 任务以并行的方式处理。框架对 `map` 的输出进行排序,然后将其输入到 `reduce` 任务中。作业的输入和输出都存储在文件系统中。该框架负责调度任务、监控任务并重新执行失败的任务。 + +通常,计算节点和存储节点是相同的,即 MapReduce 框架和 HDFS 在同一组节点上运行。此配置允许框架在已存在数据的节点上有效地调度任务,从而在整个集群中实现非常高的聚合带宽。 + +MapReduce 框架由一个主 `ResourceManager`、每个集群节点一个工作程序 `NodeManager` 和每个应用程序的 `MRAppMaster` (YARN 组件) 组成。 + +MapReduce 框架仅对 `` 对进行作,也就是说,框架将作业的输入视为一组 `` 对,并生成一组 `` 对作为作业的输出,可以想象是不同的类型。`键`和`值`类必须可由框架序列化,因此需要实现 [Writable](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/Writable.html) 接口。此外,关键类必须实现 [WritableComparable](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/WritableComparable.html) 接口,以便于按框架进行排序。 + +MapReduce 作业的 Input 和 Output 类型: + +``` +(input) -> map -> -> combine -> -> reduce -> (output) +``` + +MapReduce 适用场景: + +- 数据统计,如:网站的 PV、UV 统计 +- 搜索引擎构建索引 +- 海量数据查询 + +MapReduce 不适用场景: + +- OLAP - 要求毫秒或秒级返回结果 +- 流计算 - 流计算的输入数据集是动态的,而 MapReduce 是静态的 +- DAG 计算 + - 多个作业存在依赖关系,后一个的输入是前一个的输出,构成有向无环图 DAG + - 每个 MapReduce 作业的输出结果都会落盘,造成大量磁盘 IO,导致性能非常低下 + +::: + +### 【初级】MapReduce 有什么特性(优缺点)? + +:::details 要点 + +MapReduce 有以下特性: + +- 移动计算,而非移动数据 +- 良好的扩展性:计算能力随着节点数增加,近似线性递增 +- 高可用 +- 适合海量数据的离线批处理 +- 降低了分布式编程的门槛 + +::: + +## 架构 + +### 【高级】HDFS 的架构是怎样设计的? + +:::details 要点 + +HDFS 架构有以下几个核心要点: + +- **主从架构** +- **按块分区** +- **数据副本** +- **命名空间** + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-architecture.png) + +**(1)HDFS 主从架构** + +HDFS 采用 master/slave 架构。一个 HDFS 集群是由一个 NameNode 和一定数目的 DataNode 组成。NameNode 是一个中心服务器,负责管理文件系统的命名空间 (namespace) 以及客户端对文件的访问。集群中的 DataNode 一般是一个节点一个,负责管理它所在节点上的存储。HDFS 暴露了文件系统的命名空间,用户能够以文件的形式在上面存储数据。从内部看,一个文件其实被分成一个或多个数据块,这些块存储在一组 DataNode 上。NameNode 执行文件系统的命名空间操作,比如打开、关闭、重命名文件或目录。它也负责确定数据块到具体 DataNode 节点的映射。DataNode 负责处理文件系统客户端的读写请求。在 NameNode 的统一调度下进行数据块的创建、删除和复制。 + +- **NameNode** - 负责 HDFS 集群的管理、协调。具体来说,主要有以下职责: + - **管理命名空间** - 执行有关命名空间的操作,例如打开,关闭、重命名文件和目录等。 + - **管理元数据** - 维护文件的位置、所有者、权限、数据块等。 + - **管理 Block 副本策略** - 默认 3 个副本 + - **客户端读写请求寻址** +- **DataNode**:负责提供来自文件系统客户端的读写请求,执行块的创建,删除等操作。具体来说,主要有以下职责: + - 执行客户端发送的读写操作 + - 存储 Block 和数据校验和 + - 定期向 NameNode 发送心跳以续活 + - 定期向 NameNode 上报 Block 信息 + +**(2)按块分区** + +**HDFS 将文件数据分割成若干数据块(Block),每个 DataNode 存储一部分数据块**,这样文件就分布存储在整个 HDFS 服务器集群中。 + +将大文件分割成 Block 的主要目的是为了优化网络传输和数据处理的效率。这种分割机制使得文件的不同部分可以并行处理,大大提高了数据处理的速度。 + +HDFS Block 有以下要点: + +- Block 是 HDFS 最小存储单元 +- 文件写入 HDFS 会被切分成若干个 Block +- Block 大小固定,默认为 128MB,可通过 `dfs.blocksize` 参数修改 +- 若一个 Block 的大小小于设定值,不会占用整个块空间 +- 默认情况下每个 Block 有 3 个副本 + +这实际上是典型的分布式分区思想,使得 HDFS 具备了扩展能力。 + +**(3)数据复制** + +HDFS 被设计成能够在一个大集群中跨机器可靠地存储超大文件。它将每个文件存储成一系列的数据块,除了最后一个,所有的数据块都是同样大小的。为了容错,文件的所有数据块都会有副本。每个文件的数据块大小和副本系数都是可配置的。应用程序可以指定某个文件的副本数目。副本系数可以在文件创建的时候指定,也可以在之后改变。HDFS 中的文件都是一次性写入的,并且严格要求在任何时候只能有一个写入者。 + +NameNode 全权管理数据块的复制,它周期性地从集群中的每个 DataNode 接收心跳信号和块状态报告 (Blockreport)。接收到心跳信号意味着该 DataNode 节点工作正常。块状态报告包含了一个该 DataNode 上所有数据块的列表。 + +**(4)命名空间** + +HDFS 支持传统的层次型文件组织结构。用户或者应用程序可以创建目录,然后将文件保存在这些目录里。文件系统命名空间的层次结构和大多数现有的文件系统类似:用户可以创建、删除、移动或重命名文件。HDFS 不支持用户磁盘配额和访问权限控制,也不支持硬链接和软链接。但是 HDFS 架构并不妨碍实现这些特性。 + +NameNode 负责维护文件系统的命名空间,任何对文件系统命名空间或属性的修改都将被 NameNode 记录下来。应用程序可以设置 HDFS 保存的文件的副本数目。文件副本的数目称为文件的副本系数,这个信息也是由 NameNode 保存的。 + +::: + +### 【中级】HDFS 使用 NameNode 的好处 ? + +:::details 要点 + +HDFS 使用 NameNode 的好处主要体现在以下几个方面: + +- **中心化的元数据管理** - NameNode 在 HDFS 中负责存储整个文件系统的元数据,包括文件和目录的结构、每个文件的数据块信息及其在 DataNode 上的位置等。这种中心化的管理,使得文件系统的组织和管理变得更加简洁高效,并且可以确保整个文件系统的一致性。 +- **易扩展** - 由于实际的数据存储在 DataNode 上,而 NameNode 只存储元数据,这样的架构设计使得 HDFS 可以轻松扩展到处理 PB 级别甚至更大规模的数据集。 +- **快速的文件访问**:用户或应用程序在访问文件时,首先与 NameNode 交互以获得数据块的位置信息,然后直接从 DataNode 读取数据。这种方式可以快速定位数据,提高文件访问的效率。 +- **容错和恢复机制**:NameNode 可以监控 DataNode 的状态,实现系统的容错。在 DataNode 发生故障时,NameNode 可以指导其它 DataNode 复制丢失的数据块,保证数据的可靠性。 +- **简化数据管理**:NameNode 的存在简化了数据的管理和维护。例如,在进行数据备份、系统升级或扩展时,管理员只需要关注 NameNode 上的元数据,而不是每个节点上存储的实际数据。 + +然而,由于 NameNode 是中心节点,它也成为了系统的一个潜在瓶颈和单点故障。因此,HDFS 后来引入了主备 NameNode 机制来保证 NameNode 自身的可用性。 + +::: + +### 【中级】HDFS 使用 Block 的好处 ? + +:::details 要点 + +HDFS 采用文件分块(Block)进行存储管理,主要是基于以下几个原因: + +- **提高可靠性和容错性** - 通过将文件分成多个块,并在不同的 DataNode 上存储这些块的副本,HDFS 可以提高数据的可靠性。即使某些 DataNode 出现故障,其他节点上的副本仍然可以用于数据恢复。 +- **提高数据处理效率**:在处理大规模数据集时,将大文件分割成小块可以提高数据处理的效率。这样,可以并行地在多个节点上处理不同的块,从而加速数据处理和分析。 +- **提高网络传输效率**:分块存储还有利于网络传输。当处理或传输一个大文件的部分数据时,只需处理或传输相关的几个块,而不是整个文件,这减少了网络传输负担。 +- **易于扩展**:分块机制使得 HDFS 易于扩展。可以简单地通过增加更多的 DataNode 来扩大存储容量和处理能力,而不需要对现有的数据块进行任何修改。 +- **负载均衡**:分块存储还有助于在集群中实现负载均衡。不同的数据块可以分布在不同的节点上,从而均衡各个节点的存储和处理负载。 + +::: + +### 【中级】NameNode 与 SecondaryNameNode 的区别与联系 ? + +:::details 要点 + +NameNode 和 SecondaryNameNode 的**区别**: + +- NameNode 是 HDFS 的主要节点,负责管理文件系统的命名空间。它维护着整个文件系统的目录和文件结构,以及所有文件的元数据,包括文件的数据块(block)信息、数据块的位置等。 +- SecondaryNameNode 是 NameNode 的辅助节点。 + - SecondaryNameNode 不是 NameNode 的备份,不能在 NameNode 故障时接管其功能。 + - HDFS 在运行过程中,所有的事务(如文件创建、删除等)都会首先记录在 NameNode 的内存和 EditLog 中。SecondaryNameNode 定期从 NameNode 获取这些日志文件,与文件系统的命名空间镜像(FsImage)合并,然后把新的 FsImage 送回给 NameNode,以帮助减少 NameNode 的内存压力。 + +NameNode 和 SecondaryNameNode 的**联系**: + +- **共同目标**:二者共同目的是维护 HDFS 的稳定和高效运作。NameNode 作为核心,负责实时的元数据管理;而 SecondaryNameNode 辅助 NameNode,通过定期处理 FsImage 和 EditLog,减轻 NameNode 的负担。 +- **数据交互**:SecondaryNameNode 的工作依赖于与 NameNode 的交互,从 NameNode 获取元数据的状态和编辑日志。 + +::: + +### 【中级】什么是 FsImage 和 EditLog? + +:::details 要点 + +HDFS 中,`FsImage`和`EditLog`是两个关键的文件,用于存储和管理文件系统的元数据。它们的主要区别如下: + +**FsImage(文件系统镜像)** + +- **内容**:`FsImage`包含 HDFS 元数据的完整快照,例如文件系统的目录树、文件和目录的属性等。 +- **静态性**:它是在特定时间点上的静态快照。一旦创建,除非进行新的快照操作,否则内容不会改变。 +- **使用场景**:在 NameNode 启动时使用,用于加载文件系统的最初状态。此外,在进行系统备份时也会生成新的`FsImage`。 +- **更新频率**:不是实时更新的。通常在系统进行 checkpoint 操作时才会更新。 + +**EditLog(编辑日志)** + +- **内容**:`EditLog`记录了自上一个`FsImage`快照以来所有对文件系统所做的增量更改。这些更改包括文件和目录的创建、删除、重命名等操作。 +- **动态性**:它是一个动态更新的日志文件。每次对文件系统进行更改时,这个更改就会记录在`EditLog`中。 +- **使用场景**:用于记录所有的文件系统更改操作。在 NameNode 重启时,`FsImage`将与`EditLog`结合使用,以重建文件系统的最新状态。 +- **更新频率**:实时更新。每次对文件系统的更改都会迅速反映在`EditLog`中。 + +**结合使用** + +在 HDFS 中,`FsImage`和`EditLog`一起工作,以确保文件系统的元数据既能够被可靠地存储,又能够反映最新的更改。定期进行 checkpoint 操作(由 Secondary NameNode 或 Standby NameNode 执行)会将`EditLog`中的更改应用到`FsImage`中,创建一个新的、更新的快照。这样可以保证在系统重启或恢复时,可以快速加载最新的文件系统状态。 + +::: + +### 【中级】YARN 有哪些核心组件? + +:::details 要点 + +![YARN Architecture](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192257406.gif) + +YARN 有以下核心组件: + +- **ResourceManager** - ResourceManager 是**管理资源**和安排在 YARN 上运行的**中央调度器**。整个系统有且只有一个 ResourceManager,因为号令发布都来自一处,因此不存在调度不一致的情况(很多分布式系统都是通过经典的一主多从模式来解决一致性问题的)。它也包含了两个主要的子组件: + - **定时调度器(Scheduler)** - 从本质上来说,定时调度器就是一种策略,或者说一种算法。当 Client 提交一个任务的时候,它会根据所需要的资源以及当前集群的资源状况进行分配。注意,它只负责向应用程序分配资源,并不做监控以及应用程序的状态跟踪。 + - **应用管理器(ApplicationManager)** - 应用管理器就是负责管理 Client 提交的应用。上面不是说到定时调度器(Scheduler)不对用户提交的程序监控嘛,其实啊,监控应用的工作正是由应用管理器(ApplicationManager)完成的。 +- **NodeManager** - NodeManager 是 ResourceManager 在每台机器的上代理,负责容器的管理,并监控他们的资源使用情况(cpu、内存、磁盘及网络等),以及向 ResourceManager/Scheduler 提供这些资源使用报告。 +- **ApplicationMaster** - 每当 Client 提交一个 Application 时候,就会新建一个 ApplicationMaster 。由这个 ApplicationMaster 去与 ResourceManager 申请容器资源,获得资源后会将要运行的程序发送到容器上启动,然后进行分布式计算。这么设计的原因在于,数据量大的时候,移动数据成本太高,耗时太久,改为移动计算代价较小。 +- **Container** - `Container` 是 YARN 对资源的抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时,RM 为 AM 返回的资源是用 `Container` 表示的。 + - YARN 会为每个任务分配一个 `Container`,该任务只能使用该 `Container` 中描述的资源。 + - `ApplicationMaster` 可在 `Container` 内运行任何类型的任务。例如,`MapReduce ApplicationMaster` 请求一个容器来启动 map 或 reduce 任务,而 `Giraph ApplicationMaster` 请求一个容器来运行 Giraph 任务。 + - 容器由 NodeManager 启动和管理,并被它所监控。 + - 容器被 ResourceManager 所调度。 + +::: + +### 【中级】MapReduce 有哪些核心组件? + +:::details 要点 + +MapReduce 有以下核心组件: + +- **Job** - [Job](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/Job.html) 表示 MapReduce 作业配置。`Job` 通常用于指定 `Mapper`、combiner(如果有)、`Partitioner`、`Reducer`、`InputFormat`、`OutputFormat` 实现。 +- **Mapper** - [Mapper](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/Mapper.html) 负责将输入键值对**映射**到一组中间键值对。转换的中间记录不需要与输入记录具有相同的类型。一个给定的输入键值对可能映射到零个或多个输出键值对。 +- **Combiner** - `combiner` 是 `map` 运算后的可选操作,它实际上是一个本地化的 `reduce` 操作。它执行中间输出的本地聚合,这有助于减少从 `Mapper` 传输到 `Reducer` 的数据量。 +- **Reducer** - [Reducer](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/Reducer.html) 将共享一个 key 的一组中间值归并为一个小的数值集。Reducer 有 3 个主要子阶段:shuffle,sort 和 reduce。 + - **shuffle** - Reducer 的输入就是 mapper 的排序输出。在这个阶段,框架通过 HTTP 获取所有 mapper 输出的相关分区。 + - **sort** - 在这个阶段中,框架将按照 key (因为不同 mapper 的输出中可能会有相同的 key) 对 Reducer 的输入进行分组。shuffle 和 sort 两个阶段是同时发生的。 + - **reduce** - 对按键分组的数据进行聚合统计。 +- **Partitioner** - [Partitioner](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/Partitioner.html) 负责控制 map 中间输出结果的键的分区。 + - 键(或者键的子集)用于产生分区,通常通过一个散列函数。 + - 分区总数与作业的 reduce 任务数是一样的。因此,它控制中间输出结果(也就是这条记录)的键发送给 m 个 reduce 任务中的哪一个来进行 reduce 操作。 +- **InputFormat** - [InputFormat](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/InputFormat.html) 描述 MapReduce 作业的输入规范。MapReduce 框架依赖作业的 InputFormat 来完成以下工作: + - 确认作业的输入规范。 + - 把输入文件分割成多个逻辑的 InputSplit 实例,然后将每个实例分配给一个单独的 Mapper。[InputSplit](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/InputSplit.html) 表示要由单个 `Mapper` 处理的数据。 + - 提供 RecordReader 的实现。[RecordReader](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/RecordReader.html) 从 `InputSplit` 中读取 `` 对,并提供给 `Mapper` 实现进行处理。 +- **OutputFormat** - [OutputFormat](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/OutputFormat.html) 描述 MapReduce 作业的输出规范。MapReduce 框架依赖作业的 OutputFormat 来完成以下工作: + - 确认作业的输出规范,例如检查输出路径是否已经存在。 + - 提供 RecordWriter 实现。[RecordWriter](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/RecordWriter.html) 将输出 `` 对到文件系统。 + +::: + +## 工作流 + +### 【中级】HDFS 的写数据流程是怎样的? + +:::details 要点 + +HDFS 写数据流程大致为: + +1. **按 Block 大小分割数据** +2. **通过 NameNode 寻址 DataNode** +3. **向 DataNode 写数据** +4. **完成后通知 NameNode** + +> 扩展:下面的漫画生动的展示了 HDFS 的写入流程,图片引用自博客:[翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) +> +> ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192300702.jpg) +> +> ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192301943.jpg) +> +> ![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192302127.jpg) + +HDFS 写数据的源码流程: + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-write.png) + +1. 客户端通过对 `DistributedFileSystem` 对象调用 `create()` 函数来**新建文件**。 +2. 分布式文件系统对 NameNode 创建一个 RPC 调用,**在文件系统的命名空间中新建一个文件**。 +3. NameNode 对新建文件进行检查无误后,分布式文件系统返回给客户端一个 `FSDataOutputStream` 对象,`FSDataOutputStream` 对象封装一个 `DFSoutPutstream` 对象,负责处理 NameNode 和 DataNode 之间的通信,**客户端开始写入数据**。 +4. `FSDataOutputStream` 将**数据分成一个一个的数据包,写入内部数据队列**,DataStreamer 负责将数据包依次流式传输到由一组 NameNode 构成的管道中。 +5. `DFSOutputStream` 维护着确认队列来等待 DataNode 收到确认回执,**收到管道中所有 DataNode 确认后,数据包从确认队列删除**。 +6. **客户端完成数据的写入**,调用 `close()` 方法关闭传输通道。 +7. NameNode **确认完成**。 + +::: + +### 【中级】HDFS 的读数据流程是怎样的? + +:::details 要点 + +HDFS 读数据流程大致为: + +1. 客户端向 NameNode 查询文件信息 +2. NameNode 返回相关信息 + - 该文件的所有数据块 + - 每个数据块对应的 DataNode(按距离客户端的远近排序) +3. 客户端向 DataNode 读数据 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192303732.jpg) + +HDFS 读数据的源码流程: + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-read.png) + +1. 客户端调用 `FileSystem` 对象的 `open()` 方法在 HDFS 中**打开要读取的文件**。 +2. HDFS 通过使用 RPC(远程过程调用)来调用 NameNode,**确定文件起始块(Block)的位置**。 +3. `DistributedFileSystem` 类返回一个支持文件定位的输入流 `FSDataInputStream` 对象,`FSDataInputStream` 对象接着封装 `DFSInputStream` 对象(**存储着文件起始几个块的 DataNode 地址**),客户端对这个输入流调用 `read()` 方法。 +4. `DFSInputStream` 连接距离最近的 DataNode,通过反复调用 `read` 方法,**将数据从 DataNode 传输到客户端**。 +5. 到达块的末端时,`DFSInputStream` 关闭与该 DataNode 的连接,**寻找下一个块的最佳 DataNode**。 +6. 客户端完成读取,对 `FSDataInputStream` 调用 `close()` 方法**关闭连接**。 + +::: + +### 【中级】MapReduce 是如何工作的? + +:::details 要点 + +MapReduce 任务过程分为两个处理阶段:map 极端和 reduce 阶段。每阶段都以键值对作为输入和输出,其类型由程序员来选择。程序员还需要写两个函数:map 函数和 reduce 函数。 + +以词频统计为例,其工作流再细分一下,可以划分为以下阶段: + +1. **input** - 读取文本文件; +2. **splitting** - 将文件按照行进行拆分,此时得到的 `K1` 行数,`V1` 表示对应行的文本内容; +3. **mapping** - 并行将每一行按照空格进行拆分,拆分得到的 `List(K2,V2)`,其中 `K2` 代表每一个单词,由于是做词频统计,所以 `V2` 的值为 1,代表出现 1 次; +4. **shuffling** - 由于 `Mapping` 操作可能是在不同的机器上并行处理的,所以需要通过 `shuffling` 将相同 `key` 值的数据分发到同一个节点上去合并,这样才能统计出最终的结果,此时得到 `K2` 为每一个单词,`List(V2)` 为可迭代集合,`V2` 就是 Mapping 中的 V2; +5. **reducing** - 这里的案例是统计单词出现的总次数,所以 `Reducing` 对 `List(V2)` 进行归约求和操作,最终输出。 + +MapReduce 编程模型中 `splitting` 和 `shuffing` 操作都是由框架实现的,需要我们自己编程实现的只有 `mapping` 和 `reducing`,这也就是 MapReduce 这个称呼的来源。 + +![MapReduce 工作流](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601162305.png) + +::: + +### 【中级】YARN 是如何工作的? + +:::details 要点 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192303306.jpeg) + +这张图简单地标明了提交一个程序所经历的流程,接下来我们来具体说说每一步的过程。 + +1. Client 向 ResourceManager 申请运行一个 Application 进程,这里我们假设是一个 MapReduce 作业。 +2. ResourceManager 向 NodeManager 通信,为该 Application 进程分配第一个容器。并在这个容器中运行这个应用程序对应的 ApplicationMaster。 +3. ApplicationMaster 启动以后,对作业(也就是 Application) 进行拆分,拆分 task 出来,这些 task 可以运行在一个或多个容器中。然后向 ResourceManager 申请要运行程序的容器,并定时向 ResourceManager 发送心跳。 +4. 申请到容器后,ApplicationMaster 会去和容器对应的 NodeManager 通信,而后将作业分发到对应的 NodeManager 中的容器去运行,这里会将拆分后的 MapReduce 进行分发,对应容器中运行的可能是 Map 任务,也可能是 Reduce 任务。 +5. 容器中运行的任务会向 ApplicationMaster 发送心跳,汇报自身情况。当程序运行完成后, ApplicationMaster 再向 ResourceManager 注销并释放容器资源。 + +::: + +## 复制 + +**复制主要指通过网络在多台机器上保存相同数据的副本**。 + +复制数据,可能出于各种各样的原因: + +- **提高可用性** - 当部分组件出现位障,系统依然可以继续工作,系统依然可以继续工作。 +- **降低访问延迟** - 使数据在地理位置上更接近用户。 +- **提高读吞吐量** - 扩展至多台机器以同时提供数据访问服务。 + +所有分布式系统都需要支持复制。 + +### 【中级】HDFS 的副本机制是怎样的? + +:::details 要点 + +#### 基于块的副本 + +由于 Hadoop 被设计运行在廉价的机器上,这意味着硬件是不可靠的,为了保证容错性,HDFS 提供了副本机制。HDFS 将文件分解为若干 Block,Block 是 HDFS 最小存储单元,每个 Block 有多个副本。 + +HDFS 的默认副本数为 3,更多的副本意味着更高的数据安全性,但同时也会带来更高的额外开销(存储成本和带宽成本)。3 个副本是在保障数据可靠性和系统成本之间的一个较好的平衡点。 + +副本数可以通过以下方式修改: + +- 在 HDFS 的配置文件 hdfs-site.xml 中,有一个名为 `dfs.replication` 的属性,可以设置**全局的默认副本数**。修改这个值后,需要重启 HDFS 使配置生效。 +- 针对单个文件或目录修改副本数:如果只想改变某个特定文件或目录的副本数,而不影响整个系统的默认设置,可以使用 HDFS 的命令行工具。例如,使用命令`hdfs dfs -setrep -w <副本数> <文件/目录路径>` 来修改特定文件或目录的副本数。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20200224203958.png) + +**NameNode 全权管理数据块的复制**,它周期性地从集群中的每个 DataNode 接收心跳信号和块状态报告 (BlockReport)。接收到心跳信号意味着该 DataNode 节点工作正常。块状态报告包含了一个该 DataNode 上所有数据块的列表。 + +![](https://raw.githubusercontent.com/dunwu/images/master/cs/bigdata/hdfs/hdfs-replica.png) + +#### 副本分布策略 + +副本分布策略是 HDFS 可靠性和性能的关键。优化的副本存放策略是 HDFS 区分于其他大部分分布式文件系统的重要特性。HDFS 采用一种称为机架感知 (rack-aware) 的策略来改进数据的可靠性、可用性和网络带宽的利用率。大型 HDFS 实例一般运行在跨越多个机架的计算机组成的集群上,不同机架上的两台机器之间的通信需要经过交换机。在大多数情况下,同一个机架内的两台机器间的带宽会比不同机架的两台机器间的带宽大。 + +通过一个机架感知的过程,NameNode 可以确定每个 DataNode 所属的机架 id。一个简单但没有优化的策略就是将副本存放在不同的机架上。这样可以有效防止当整个机架失效时数据的丢失,并且允许读数据的时候充分利用多个机架的带宽。这种策略设置可以将副本均匀分布在集群中,有利于当组件失效情况下的负载均衡。但是,因为这种策略的一个写操作需要传输数据块到多个机架,这增加了写的代价。 + +HDFS 默认的副本数为 3,此时 HDFS 的副本分布策略是: + +- **副本 1** - 放在 Client 所在节点;对于远程 Client,系统会随机选择节点 +- **副本 2** - 放在不同机架的节点上 +- **副本 3** - 放在与第二个副本同一机架的不同节点上 +- **副本 N** - 在满足以下条件的节点中随机选择 + - 每个节点只存储一份副本 + - 每个机架最多存储两份副本 +- **优选** - 同等条件下优先选择空闲节点。 + - 如果某个 DataNode 节点上的空闲空间低于特定的临界点,按照均衡策略系统就会自动地将数据从这个 DataNode 移动到其他空闲的 DataNode。 + +#### 副本选择 + +为了降低整体的带宽消耗和读取延时,HDFS 会尽量让客户端程序读取离它最近的副本。如果在客户端程序的同一个机架上有一个副本,那么就读取该副本。如果一个 HDFS 集群跨越多个数据中心,那么客户端也将首先读本地数据中心的副本。 + +为了最大限度地减少带宽消耗和读取延迟,HDFS 在执行读取请求时,优先读取距离读取器最近的副本。如果在与读取器节点相同的机架上存在副本,则优先选择该副本。如果 HDFS 群集跨越多个数据中心,则优先选择本地数据中心上的副本。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192304008.jpg) + +::: + +### 【中级】HDFS 如何保证数据一致性? + +:::details 要点 + +HDFS 的数据一致性主要依赖以下机制来保证: + +- **NameNode 的中心化管理** - NameNode 在 HDFS 中负责存储整个文件系统的元数据,包括文件和目录的结构、每个文件的数据块信息及其在 DataNode 上的位置等。这种中心化的管理,使得文件系统的组织和管理变得更加简洁高效,并且可以确保整个文件系统的一致性。 +- **数据块的复制(Replication)** - HDFS 采用副本来保证数据的可靠性。一旦数据写入完成,副本就会分散存储在不同的 DataNodes 上。尽管这种方法不是强一致性模型,但通过足够数量的副本和及时的副本替换策略,HDFS 能够提供较高水平的数据一致性和可靠性。 +- **写入和复制的原子性保证** - 在 HDFS 中,文件一旦创建,其内容就不能被更新,只能被追加或重写。这种方式简化了并发控制,因为写操作在文件级别上是原子的。在复制数据块时,HDFS 保证原子性复制,即一个数据块的所有副本在任何时间点上都是相同的。如果复制过程中出现错误,那么不完整的副本会被删除,系统会重新尝试复制直到成功。 +- **客户端的一致性协议** - 客户端在与 HDFS 交互时,遵循特定的协议。例如,客户端在完成文件写入之后,需要向 NameNode 通知,以确保 NameNode 更新文件的元数据。这样可以保证 NameNode 的元数据与实际存储的数据保持一致。 +- **定期检查和错误恢复** + - **心跳和健康检查** - DataNodes 定期向 NameNode 发送心跳和 Block 健康状况报告。NameNode 利用这些信息来检查和维护系统的整体一致性。例如,如果某个 DataNode 失败,NameNode 会重新组织数据块的副本。 + - **校验** - HDFS 在存储和传输数据时,会计算数据的校验和。在读取数据时,会验证这些校验和,确保数据的完整性。 + +通过这些机制,HDFS 确保了系统中的数据在正常操作和故障情况下的一致性和可靠性。虽然 HDFS 不提供像传统数据库那样的强一致性保证,但它的设计和实现确保了在大规模数据处理场景中的有效性和健壮性。 + +::: + +## 容错 + +### 【中级】HDFS 有哪些故障类型?如何检测故障? + +:::details 要点 + +HDFS 常见故障及检测方法: + +- **节点故障** + - DataNode 每 3 秒向 NameNode 发送心跳 + - 超时未收到心跳,NameNode 判定 DataNode 宕机 +- **通信故障** + - 客户端请求 DataNode 会收到 ACK +- **数据损坏** + - 磁盘介质在存储过程中受环境或者老化影响,其存储的数据可能会出现错乱。HDFS 的应对措施是,对于存储在 DataNode 上的数据块,计算并存储校验和(CheckSum)。在读取数据的时候,重新计算读取出来的数据的校验和,如果校验不正确就抛出异常,应用程序捕获异常后就到其他 DataNode 上读取备份数据。 + - 如果 DataNode 监测到本机的某块磁盘损坏,就将该块磁盘上存储的所有 BlockID 报告给 NameNode,NameNode 检查这些数据块还在哪些 DataNode 上有备份,通知相应的 DataNode 服务器将对应的数据块复制到其他服务器上,以保证数据块的备份数满足要求。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192304755.jpg) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192305651.jpg) + +::: + +### 【中级】HDFS 读写故障如何处理? + +:::details 要点 + +#### 写入故障处理 + +- 写入数据通过数据包传输 +- DataNode 接收数据后,返回 ACK +- 如果客户端没有收到 ACK,就判定 DataNode 宕机,跳过节点 +- 没有充分备份的数据块信息通知到 NameNode + +#### 读取故障处理 + +- 读数据先要通过 NameNode 寻址该数据块的所有 DataNode +- 如果某 DataNode 宕机,则读取其他节点 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192305332.jpg) + +::: + +### 【中级】DataNode 故障如何处理? + +:::details 要点 + +DataNode 每 3 秒会向 NameNode 发送心跳消息,以证明自身正常工作。如果 DataNode 超时未发送心跳,NameNode 就会认为该 DataNode 已经宕机。 + +NameNode 会立即查找该 DataNode 上存储的数据块有哪些,以及这些数据块还存储在哪些其他 DataNode 上。 + +随后,NameNode 通知这些 DataNode 再复制一份数据块到其他 DataNode 上,保证 HDFS 存储的数据块副本数符合配置数。即使再出现服务器宕机,也不会丢失数据。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192306957.jpg) + +::: + +### 【中级】NameNode 故障如何处理? + +:::details 要点 + +NameNode 是整个 HDFS 的核心,记录着 HDFS 文件分配表信息,所有的文件路径和数据块存储信息都保存在 NameNode,如果 NameNode 故障,整个 HDFS 系统集群都无法使用。如果 NameNode 上记录的数据丢失,整个集群所有 DataNode 存储的数据也就没用了。 + +NameNode 通过主备架构实现故障转移。 + +- **Active NameNode** - 是正在工作的 NameNode; +- **Standby NameNode** - 是备份的 NameNode。 + +Active NameNode 宕机后,Standby NameNode 快速升级为新的 Active NameNode。Standby NameNode 周期性同步 `edits` 编辑日志,定期合并 `FsImage` 与 `edits` 到本地磁盘。 + +> 注:Hadoop 3.0 允许配置多个 Standby NameNode。 + +#### 元数据文件 + +- **edits(编辑日志文件)** - 保存了自最新检查点(Checkpoint)之后的所有文件更新操作。 +- **FsImage(元数据检查点镜像文件)** - 保存了文件系统中所有的目录和文件信息,如:某个目录下有哪些子目录和文件,以及文件名、文件副本数、文件由哪些 Block 组成等。 + +Active NameNode 内存中有一份最新的元数据(= FsImage + edits)。 + +Standby NameNode 在检查点定期将内存中的元数据保存到 FsImage 文件中。 + +#### 利用 QJM 实现元数据高可用 + +> 基于 Paxos 算法 + +QJM 机制(Quorum Journal Manager) + +只要保证 Quorum(法定人数)数量的操作成功,就认为这是一次最终成功的操作 + +QJM 共享存储系统 + +- 部署奇数(2N+1)个 JournalNode +- JournalNode 负责存储 edits 编辑日志 +- 写 edits 的时候,只要超过半数(N+1)的 JournalNode 返回成功,就代表本次写入成功 +- 最多可容忍 N 个 JournalNode 宕机 + +利用 ZooKeeper 实现 Active 节点选举。 + +::: + +### 【中级】HDFS 安全模式有什么作用? + +:::details 要点 + +在启动过程中,NameNode 进入安全模式。在这个模式下,它会检查数据块的健康状况和副本数量。只有在足够数量的数据块可用时,NameNode 才会退出安全模式,开始正常的操作。 + +::: + +## HA + +### 【高级】HDFS 如何实现高可用? + +:::details 要点 + +HDFS 高可用架构如下: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192307157.png) + +HDFS 高可用架构主要由以下组件所构成: + +- **Active NameNode 和 Standby NameNode**:两台 NameNode 形成互备,一台处于 Active 状态,为主 NameNode,另外一台处于 Standby 状态,为备 NameNode,只有主 NameNode 才能对外提供读写服务。 +- **主备切换控制器 ZKFailoverController**:ZKFailoverController 作为独立的进程运行,对 NameNode 的主备切换进行总体控制。ZKFailoverController 能及时检测到 NameNode 的健康状况,在主 NameNode 故障时借助 Zookeeper 实现自动的主备选举和切换,当然 NameNode 目前也支持不依赖于 Zookeeper 的手动主备切换。 +- **Zookeeper 集群**:为主备切换控制器提供主备选举支持。 +- **共享存储系统**:共享存储系统是实现 NameNode 的高可用最为关键的部分,共享存储系统保存了 NameNode 在运行过程中所产生的 HDFS 的元数据。主 NameNode 和 NameNode 通过共享存储系统实现元数据同步。在进行主备切换的时候,新的主 NameNode 在确认元数据完全同步之后才能继续对外提供服务。 +- **DataNode 节点**:除了通过共享存储系统共享 HDFS 的元数据信息之外,主 NameNode 和备 NameNode 还需要共享 HDFS 的数据块和 DataNode 之间的映射关系。DataNode 会同时向主 NameNode 和备 NameNode 上报数据块的位置信息。 + +目前 Hadoop 支持使用 Quorum Journal Manager (QJM) 或 Network File System (NFS) 作为共享的存储系统,这里以 QJM 集群为例进行说明:Active NameNode 首先把 EditLog 提交到 JournalNode 集群,然后 Standby NameNode 再从 JournalNode 集群定时同步 EditLog,当 Active NameNode 宕机后, Standby NameNode 在确认元数据完全同步之后就可以对外提供服务。 + +需要说明的是向 JournalNode 集群写入 EditLog 是遵循 “过半写入则成功” 的策略,所以你至少要有 3 个 JournalNode 节点,当然你也可以继续增加节点数量,但是应该保证节点总数是奇数。同时如果有 2N+1 台 JournalNode,那么根据过半写的原则,最多可以容忍有 N 台 JournalNode 节点挂掉。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192308642.png) + +::: + +### 【高级】NameNode 如何实现主备切换? + +:::details 要点 + +NameNode 实现主备切换的流程下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192308888.png) + +工作流程说明: + +1. HealthMonitor 初始化完成之后会启动内部的线程来定时调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法,对 NameNode 的健康状态进行检测。 +2. HealthMonitor 如果检测到 NameNode 的健康状态发生变化,会回调 ZKFailoverController 注册的相应方法进行处理。 +3. 如果 ZKFailoverController 判断需要进行主备切换,会首先使用 ActiveStandbyElector 来进行自动的主备选举。 +4. ActiveStandbyElector 与 Zookeeper 进行交互完成自动的主备选举。 +5. ActiveStandbyElector 在主备选举完成后,会回调 ZKFailoverController 的相应方法来通知当前的 NameNode 成为主 NameNode 或备 NameNode。 +6. ZKFailoverController 调用对应 NameNode 的 HAServiceProtocol RPC 接口的方法将 NameNode 转换为 Active 状态或 Standby 状态。 + +主备选举过程: + +NameNode 在选举成功后,会在 zk 上创建了一个 `/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock` 节点,而没有选举成功的备 NameNode 会监控这个节点,通过 Watcher 来监听这个节点的状态变化事件,ZKFC 的 ActiveStandbyElector 主要关注这个节点的 NodeDeleted 事件(这部分实现跟 Kafka 中 Controller 的选举一样)。 + +如果 Active NameNode 对应的 HealthMonitor 检测到 NameNode 的状态异常时, ZKFailoverController 会主动删除当前在 Zookeeper 上建立的临时节点 `/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock`,这样处于 Standby 状态的 NameNode 的 ActiveStandbyElector 注册的监听器就会收到这个节点的 NodeDeleted 事件。收到这个事件之后,会马上再次进入到创建 `/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock` 节点的流程,如果创建成功,这个本来处于 Standby 状态的 NameNode 就选举为主 NameNode 并随后开始切换为 Active 状态。 + +当然,如果是 Active 状态的 NameNode 所在的机器整个宕掉的话,那么根据 Zookeeper 的临时节点特性,`/hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock` 节点会自动被删除,从而也会自动进行一次主备切换。 + +::: + +### 【高级】如何应对 HDFS 脑裂问题? + +:::details 要点 + +在实际中,NameNode 可能会出现这种情况,NameNode 在垃圾回收(GC)时,可能会在长时间内整个系统无响应,因此,也就无法向 zk 写入心跳信息,这样的话可能会导致临时节点掉线,备 NameNode 会切换到 Active 状态,这种情况,可能会导致整个集群会有同时有两个 NameNode,这就是脑裂问题。 + +脑裂问题的解决方案是隔离(Fencing),主要是在以下三处采用隔离措施: + +- 第三方共享存储:任一时刻,只有一个 NN 可以写入; +- DataNode:需要保证只有一个 NN 发出与管理数据副本有关的删除命令; +- Client:需要保证同一时刻只有一个 NN 能够对 Client 的请求发出正确的响应。 + +关于这个问题目前解决方案的实现如下: + +- ActiveStandbyElector 为了实现隔离,会在成功创建 Zookeeper 节点 `hadoop-ha/${dfs.nameservices}/ActiveStandbyElectorLock` 从而成为 Active NameNode 之后,创建另外一个路径为 `/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb` 的持久节点,这个节点里面保存了这个 Active NameNode 的地址信息; +- Active NameNode 的 ActiveStandbyElector 在正常的状态下关闭 Zookeeper Session 的时候,会一起删除这个持久节点; +- 但如果 ActiveStandbyElector 在异常的状态下 Zookeeper Session 关闭 (比如前述的 Zookeeper 假死),那么由于 `/hadoop-ha/${dfs.nameservices}/ActiveBreadCrumb` 是持久节点,会一直保留下来,后面当另一个 NameNode 选主成功之后,会注意到上一个 Active NameNode 遗留下来的这个节点,从而会回调 ZKFailoverController 的方法对旧的 Active NameNode 进行 fencing。 + +在进行隔离的时候,会执行以下的操作: + +首先尝试调用这个旧 Active NameNode 的 HAServiceProtocol RPC 接口的 transitionToStandby 方法,看能不能把它转换为 Standby 状态; 如果 transitionToStandby 方法调用失败,那么就执行 Hadoop 配置文件之中预定义的隔离措施。 + +Hadoop 目前主要提供两种隔离措施,通常会选择第一种:sshfence:通过 SSH 登录到目标机器上,执行命令 fuser 将对应的进程杀死; shellfence:执行一个用户自定义的 shell 脚本来将对应的进程隔离。 只有在成功地执行完成 fencing 之后,选主成功的 ActiveStandbyElector 才会回调 ZKFailoverController 的 becomeActive 方法将对应的 NameNode 转换为 Active 状态,开始对外提供服务。 + +NameNode 选举的实现机制与 Kafka 的 Controller 类似,那么 Kafka 是如何避免脑裂问题的呢? + +Controller 给 Broker 发送的请求中,都会携带 controller epoch 信息,如果 broker 发现当前请求的 epoch 小于缓存中的值,那么就证明这是来自旧 Controller 的请求,就会决绝这个请求,正常情况下是没什么问题的; 但是异常情况下呢?如果 Broker 先收到异常 Controller 的请求进行处理呢?现在看 Kafka 在这一部分并没有适合的方案; 正常情况下,Kafka 新的 Controller 选举出来之后,Controller 会向全局所有 broker 发送一个 metadata 请求,这样全局所有 Broker 都可以知道当前最新的 controller epoch,但是并不能保证可以完全避免上面这个问题,还是有出现这个问题的几率的,只不过非常小,而且即使出现了由于 Kafka 的高可靠架构,影响也非常有限,至少从目前看,这个问题并不是严重的问题。 + +通过标识每次选举的版本号,并以最新版本选举结果为准,是分布式选举避免脑裂的常见做法。在其他分布式系统中,epoch 可能会被称为 term、version 等。 + +::: + +### 【高级】YARN 如何实现高可用? + +:::details 要点 + +YARN ResourceManager 的高可用与 HDFS NameNode 的高可用类似,但是 ResourceManager 不像 NameNode ,没有那么多的元数据信息需要维护,所以它的状态信息可以直接写到 Zookeeper 上,并依赖 Zookeeper 来进行主备选举。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192309573.png) + +::: + +## 参考资料 + +- https://hadoop.apache.org/docs/stable/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html +- https://github.com/heibaiying/BigData-Notes/blob/master/notes/Hadoop-HDFS.md +- [翻译经典 HDFS 原理讲解漫画](https://blog.csdn.net/hudiefenmu/article/details/37655491) diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/MapReduce.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/MapReduce.md" new file mode 100644 index 0000000000..073eb511fd --- /dev/null +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/MapReduce.md" @@ -0,0 +1,107 @@ +--- +icon: devicon:hadoop +title: MapReduce +date: 2020-06-22 00:22:25 +order: 03 +categories: + - 大数据 + - hadoop +tags: + - 大数据 + - hadoop + - mapreduce +permalink: /pages/6ef773ed/ +--- + +# MapReduce + +## MapReduce 简介 + +MapReduce 是 Hadoop 项目中的分布式计算框架。它降低了分布式计算的门槛,可以让用户轻松编写程序,让其以可靠、容错的方式运行在大型集群上并行处理海量数据(TB 级)。 + +MapReduce 的设计思路是: + +- 分而治之,并行计算 +- 移动计算,而非移动数据 + +MapReduce 作业通过将输入的数据集拆分为独立的块,这些块由 `map` 任务以并行的方式处理。框架对 `map` 的输出进行排序,然后将其输入到 `reduce` 任务中。作业的输入和输出都存储在文件系统中。该框架负责调度任务、监控任务并重新执行失败的任务。 + +通常,计算节点和存储节点是相同的,即 MapReduce 框架和 HDFS 在同一组节点上运行。此配置允许框架在已存在数据的节点上有效地调度任务,从而在整个集群中实现非常高的聚合带宽。 + +MapReduce 框架由一个主 `ResourceManager`、每个集群节点一个工作程序 `NodeManager` 和每个应用程序的 `MRAppMaster` (YARN 组件) 组成。 + +MapReduce 框架仅对 `` 对进行作,也就是说,框架将作业的输入视为一组 `` 对,并生成一组 `` 对作为作业的输出,可以想象是不同的类型。`键`和`值`类必须可由框架序列化,因此需要实现 [Writable](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/Writable.html) 接口。此外,关键类必须实现 [WritableComparable](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/io/WritableComparable.html) 接口,以便于按框架进行排序。 + +MapReduce 作业的 Input 和 Output 类型: + +``` +(input) -> map -> -> combine -> -> reduce -> (output) +``` + +MapReduce 的特点 + +- 计算跟着数据走 +- 良好的扩展性:计算能力随着节点数增加,近似线性递增 +- 高容错 +- 状态监控 +- 适合海量数据的离线批处理 +- 降低了分布式编程的门槛 + +## MapReduce 应用场景 + +适用场景: + +- 数据统计,如:网站的 PV、UV 统计 +- 搜索引擎构建索引 +- 海量数据查询 + +不适用场景: + +- OLAP - 要求毫秒或秒级返回结果 +- 流计算 - 流计算的输入数据集是动态的,而 MapReduce 是静态的 +- DAG 计算 + - 多个作业存在依赖关系,后一个的输入是前一个的输出,构成有向无环图 DAG + - 每个 MapReduce 作业的输出结果都会落盘,造成大量磁盘 IO,导致性能非常低下 + +## MapReduce 工作流 + +MapReduce 编程模型:MapReduce 程序被分为 Map(映射)阶段和 Reduce(化简)阶段。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601162305.png) + +1. **input** : 读取文本文件; +2. **splitting** : 将文件按照行进行拆分,此时得到的 `K1` 行数,`V1` 表示对应行的文本内容; +3. **mapping** : 并行将每一行按照空格进行拆分,拆分得到的 `List(K2,V2)`,其中 `K2` 代表每一个单词,由于是做词频统计,所以 `V2` 的值为 1,代表出现 1 次; +4. **shuffling**:由于 `Mapping` 操作可能是在不同的机器上并行处理的,所以需要通过 `shuffling` 将相同 `key` 值的数据分发到同一个节点上去合并,这样才能统计出最终的结果,此时得到 `K2` 为每一个单词,`List(V2)` 为可迭代集合,`V2` 就是 Mapping 中的 V2; +5. **Reducing** : 这里的案例是统计单词出现的总次数,所以 `Reducing` 对 `List(V2)` 进行归约求和操作,最终输出。 + +MapReduce 编程模型中 `splitting` 和 `shuffing` 操作都是由框架实现的,需要我们自己编程实现的只有 `mapping` 和 `reducing`,这也就是 MapReduce 这个称呼的来源。 + +## MapReduce 组件 + +MapReduce 有以下核心组件: + +- **Job** - [Job](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/Job.html) 表示 MapReduce 作业配置。`Job` 通常用于指定 `Mapper`、combiner(如果有)、`Partitioner`、`Reducer`、`InputFormat`、`OutputFormat` 实现。 +- **Mapper** - [Mapper](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/Mapper.html) 负责将输入键值对**映射**到一组中间键值对。转换的中间记录不需要与输入记录具有相同的类型。一个给定的输入键值对可能映射到零个或多个输出键值对。 +- **Combiner** - `combiner` 是 `map` 运算后的可选操作,它实际上是一个本地化的 `reduce` 操作。它执行中间输出的本地聚合,这有助于减少从 `Mapper` 传输到 `Reducer` 的数据量。 +- **Reducer** - [Reducer](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/Reducer.html) 将共享一个 key 的一组中间值归并为一个小的数值集。Reducer 有 3 个主要子阶段:shuffle,sort 和 reduce。 + - **shuffle** - Reducer 的输入就是 mapper 的排序输出。在这个阶段,框架通过 HTTP 获取所有 mapper 输出的相关分区。 + - **sort** - 在这个阶段中,框架将按照 key (因为不同 mapper 的输出中可能会有相同的 key) 对 Reducer 的输入进行分组。shuffle 和 sort 两个阶段是同时发生的。 + - **reduce** - 对按键分组的数据进行聚合统计。 +- **Partitioner** - [Partitioner](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/Partitioner.html) 负责控制 map 中间输出结果的键的分区。 + - 键(或者键的子集)用于产生分区,通常通过一个散列函数。 + - 分区总数与作业的 reduce 任务数是一样的。因此,它控制中间输出结果(也就是这条记录)的键发送给 m 个 reduce 任务中的哪一个来进行 reduce 操作。 +- **InputFormat** - [InputFormat](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/InputFormat.html) 描述 MapReduce 作业的输入规范。MapReduce 框架依赖作业的 InputFormat 来完成以下工作: + - 确认作业的输入规范。 + - 把输入文件分割成多个逻辑的 InputSplit 实例,然后将每个实例分配给一个单独的 Mapper。[InputSplit](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/InputSplit.html) 表示要由单个 `Mapper` 处理的数据。 + - 提供 RecordReader 的实现。[RecordReader](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/RecordReader.html) 从 `InputSplit` 中读取 `` 对,并提供给 `Mapper` 实现进行处理。 +- **OutputFormat** - [OutputFormat](http://hadoop.apache.org/docs/current/api/org/apache/hadoop/mapreduce/OutputFormat.html) 描述 MapReduce 作业的输出规范。MapReduce 框架依赖作业的 OutputFormat 来完成以下工作: + - 确认作业的输出规范,例如检查输出路径是否已经存在。 + - 提供 RecordWriter 实现。[RecordWriter](https://hadoop.apache.org/docs/stable/api/org/apache/hadoop/mapreduce/RecordWriter.html) 将输出 `` 对到文件系统。 + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200601163846.png) + +## 参考资料 + +- [分布式计算框架——MapReduce](https://github.com/heibaiying/BigData-Notes/blob/master/notes/Hadoop-MapReduce.md) +- [MapReduce 官方文档](https://hadoop.apache.org/docs/stable/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html) diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/README.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/README.md" similarity index 52% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/README.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/README.md" index 881e53ecca..0271a6ee72 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/01.hadoop/README.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/README.md" @@ -1,4 +1,5 @@ --- +icon: devicon:hadoop title: Hadoop 教程 date: 2020-09-09 17:53:08 categories: @@ -6,8 +7,8 @@ categories: - hadoop tags: - 大数据 - - Hadoop -permalink: /pages/680e30/ + - hadoop +permalink: /pages/88aa0f3b/ hidden: true index: false --- @@ -16,18 +17,11 @@ index: false ## 📖 内容 -### HDFS - -- [HDFS 入门](01.hdfs/01.HDFS入门.md) -- [HDFS 运维](01.hdfs/02.HDFS运维.md) -- [HDFS Java API](01.hdfs/03.HDFSJavaApi.md) - -### YARN - -### MapReduce - -## 📚 资料 +- [HDFS](HDFS.md) +- [YARN](YARN.md) +- [MapReduce](MapReduce.md) +- [Hadoop 面试](Hadoop面试.md) 💯 ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/YARN.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/YARN.md" new file mode 100644 index 0000000000..c3ae80d941 --- /dev/null +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hadoop/YARN.md" @@ -0,0 +1,110 @@ +--- +icon: devicon:hadoop +title: YARN +date: 2019-05-07 20:19:25 +order: 02 +categories: + - 大数据 + - hadoop +tags: + - 大数据 + - hadoop + - yarn +permalink: /pages/d2171a8a/ +--- + +# YARN + +## YARN 简介 + +**Apache YARN** (Yet Another Resource Negotiator) 是 hadoop 2.0 引入的集群资源管理系统。用户可以将各种服务框架部署在 YARN 上,由 YARN 进行统一地管理和资源分配。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192251433.png) + +## YARN 架构 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192252145.png) + +### ResourceManager + +`ResourceManager` 通常在独立的机器上以后台进程的形式运行,它是整个集群资源的主要协调者和管理者。`ResourceManager` 负责给用户提交的所有应用程序分配资源,它根据应用程序优先级、队列容量、ACLs、数据位置等信息,做出决策,然后以共享的、安全的、多租户的方式制定分配策略,调度集群资源。 + +### NodeManager + +`NodeManager` 是 YARN 集群中的每个具体节点的管理者。主要负责该节点内所有容器的生命周期的管理,监视资源和跟踪节点健康。具体如下: + +- 启动时向 `ResourceManager` 注册并定时发送心跳消息,等待 `ResourceManager` 的指令; +- 维护 `Container` 的生命周期,监控 `Container` 的资源使用情况; +- 管理任务运行时的相关依赖,根据 `ApplicationMaster` 的需要,在启动 `Container` 之前将需要的程序及其依赖拷贝到本地。 + +### ApplicationMaster + +在用户提交一个应用程序时,YARN 会启动一个轻量级的进程 `ApplicationMaster`。`ApplicationMaster` 负责协调来自 `ResourceManager` 的资源,并通过 `NodeManager` 监视容器内资源的使用情况,同时还负责任务的监控与容错。具体如下: + +- 根据应用的运行状态来决定动态计算资源需求; +- 向 `ResourceManager` 申请资源,监控申请的资源的使用情况; +- 跟踪任务状态和进度,报告资源的使用情况和应用的进度信息; +- 负责任务的容错。 + +### Container + +`Container` 是 YARN 中的资源抽象,它封装了某个节点上的多维度资源,如内存、CPU、磁盘、网络等。当 AM 向 RM 申请资源时,RM 为 AM 返回的资源是用 `Container` 表示的。YARN 会为每个任务分配一个 `Container`,该任务只能使用该 `Container` 中描述的资源。`ApplicationMaster` 可在 `Container` 内运行任何类型的任务。例如,`MapReduce ApplicationMaster` 请求一个容器来启动 map 或 reduce 任务,而 `Giraph ApplicationMaster` 请求一个容器来运行 Giraph 任务。 + +## YARN 工作原理 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192253437.png) + +1. `Client` 提交作业到 YARN 上; + +2. `Resource Manager` 选择一个 `Node Manager`,启动一个 `Container` 并运行 `Application Master` 实例; + +3. `Application Master` 根据实际需要向 `Resource Manager` 请求更多的 `Container` 资源(如果作业很小,应用管理器会选择在其自己的 JVM 中运行任务); + +4. `Application Master` 通过获取到的 `Container` 资源执行分布式计算。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502192255544.png) + +#### 作业提交 + +client 调用 job.waitForCompletion 方法,向整个集群提交 MapReduce 作业 (第 1 步) 。新的作业 ID(应用 ID) 由资源管理器分配 (第 2 步)。作业的 client 核实作业的输出,计算输入的 split, 将作业的资源 (包括 Jar 包,配置文件,split 信息) 拷贝给 HDFS(第 3 步)。 最后,通过调用资源管理器的 submitApplication() 来提交作业 (第 4 步)。 + +#### 作业初始化 + +当资源管理器收到 submitApplciation() 的请求时,就将该请求发给调度器 (scheduler), 调度器分配 container, 然后资源管理器在该 container 内启动应用管理器进程,由节点管理器监控 (第 5 步)。 + +MapReduce 作业的应用管理器是一个主类为 MRAppMaster 的 Java 应用,其通过创造一些 bookkeeping 对象来监控作业的进度,得到任务的进度和完成报告 (第 6 步)。然后其通过分布式文件系统得到由客户端计算好的输入 split(第 7 步),然后为每个输入 split 创建一个 map 任务,根据 mapreduce.job.reduces 创建 reduce 任务对象。 + +#### 任务分配 + +如果作业很小,应用管理器会选择在其自己的 JVM 中运行任务。 + +如果不是小作业,那么应用管理器向资源管理器请求 container 来运行所有的 map 和 reduce 任务 (第 8 步)。这些请求是通过心跳来传输的,包括每个 map 任务的数据位置,比如存放输入 split 的主机名和机架 (rack),调度器利用这些信息来调度任务,尽量将任务分配给存储数据的节点,或者分配给和存放输入 split 的节点相同机架的节点。 + +#### 任务运行 + +当一个任务由资源管理器的调度器分配给一个 container 后,应用管理器通过联系节点管理器来启动 container(第 9 步)。任务由一个主类为 YarnChild 的 Java 应用执行, 在运行任务之前首先本地化任务需要的资源,比如作业配置,JAR 文件,以及分布式缓存的所有文件 (第 10 步。 最后,运行 map 或 reduce 任务 (第 11 步)。 + +YarnChild 运行在一个专用的 JVM 中,但是 YARN 不支持 JVM 重用。 + +#### 进度和状态更新 + +YARN 中的任务将其进度和状态 (包括 counter) 返回给应用管理器,客户端每秒 (通 mapreduce.client.progressmonitor.pollinterval 设置) 向应用管理器请求进度更新,展示给用户。 + +#### 作业完成 + +除了向应用管理器请求作业进度外,客户端每 5 分钟都会通过调用 waitForCompletion() 来检查作业是否完成,时间间隔可以通过 mapreduce.client.completion.pollinterval 来设置。作业完成之后,应用管理器和 container 会清理工作状态, OutputCommiter 的作业清理方法也会被调用。作业的信息会被作业历史服务器存储以备之后用户核查。 + +## 提交作业到 YARN 上运行 + +这里以提交 Hadoop Examples 中计算 Pi 的 MApReduce 程序为例,相关 Jar 包在 Hadoop 安装目录的 `share/hadoop/mapreduce` 目录下: + +```shell +# 提交格式:hadoop jar jar 包路径 主类名称 主类参数 +# hadoop jar hadoop-mapreduce-examples-2.6.0-cdh5.15.2.jar pi 3 3 +``` + +## 参考资料 + +- [初步掌握 Yarn 的架构及原理](https://www.cnblogs.com/codeOfLife/p/5492740.html) +- [Apache Hadoop 2.9.2 > Apache Hadoop YARN](http://hadoop.apache.org/docs/stable/hadoop-yarn/hadoop-yarn-site/YARN.html) +- [深入浅出 Hadoop YARN](https://zhuanlan.zhihu.com/p/54192454) diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/05.HiveDDL.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/HiveDDL.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/05.HiveDDL.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/HiveDDL.md" index fe86b0a470..c5958a8579 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/05.HiveDDL.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/HiveDDL.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Hive -permalink: /pages/229daa/ +permalink: /pages/32f9d1d4/ --- # Hive 常用 DDL 操作 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/06.HiveDML.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/HiveDML.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/06.HiveDML.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/HiveDML.md" index 055590be06..bf9f54f2ab 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/06.HiveDML.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/HiveDML.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Hive -permalink: /pages/45f4c1/ +permalink: /pages/c6465f21/ --- # Hive 常用 DML 操作 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/01.Hive\345\205\245\351\227\250.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\345\205\245\351\227\250.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/01.Hive\345\205\245\351\227\250.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\345\205\245\351\227\250.md" index 1831cb414b..cbd2bd3c7f 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/01.Hive\345\205\245\351\227\250.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\345\205\245\351\227\250.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Hive -permalink: /pages/e1b37c/ +permalink: /pages/7f8f9ef8/ --- # Hive 入门 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/04.Hive\346\237\245\350\257\242.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\346\237\245\350\257\242.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/04.Hive\346\237\245\350\257\242.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\346\237\245\350\257\242.md" index 86eb8c747d..9ecebd9f51 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/04.Hive\346\237\245\350\257\242.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\346\237\245\350\257\242.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Hive -permalink: /pages/b7b857/ +permalink: /pages/20ca0683/ --- # Hive 数据查询详解 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/02.Hive\350\241\250.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\241\250.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/02.Hive\350\241\250.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\241\250.md" index 3d84d18dd9..4e857c1fae 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/02.Hive\350\241\250.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\241\250.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Hive -permalink: /pages/18eb58/ +permalink: /pages/43113d65/ --- # Hive 分区表和分桶表 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/03.Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/03.Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" index c077afc603..0b107b9c0e 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/03.Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\247\206\345\233\276\345\222\214\347\264\242\345\274\225.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - Hive -permalink: /pages/5e2d71/ +permalink: /pages/c8079447/ --- # Hive 视图和索引 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/07.Hive\350\277\220\347\273\264.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\277\220\347\273\264.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/07.Hive\350\277\220\347\273\264.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\277\220\347\273\264.md" index edcca3efb5..d6715cd49f 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/02.hive/07.Hive\350\277\220\347\273\264.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/Hive\350\277\220\347\273\264.md" @@ -9,7 +9,7 @@ tags: - 大数据 - Hive - 运维 -permalink: /pages/94f791/ +permalink: /pages/ffa7d14a/ --- # Hive 运维 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/README.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/README.md" new file mode 100644 index 0000000000..1a9111d691 --- /dev/null +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/hive/README.md" @@ -0,0 +1,31 @@ +--- +title: Hive 教程 +date: 2020-09-09 17:53:08 +categories: + - 大数据 + - hive +tags: + - 大数据 + - Hive +permalink: /pages/3bd4c456/ +hidden: true +index: false +--- + +# Hive 教程 + +## 📖 内容 + +- [Hive 入门](Hive入门.md) +- [Hive 表](Hive表.md) +- [Hive 视图和索引](Hive视图和索引.md) +- [Hive 查询](Hive查询.md) +- [Hive DDL](HiveDDL.md) +- [Hive DML](HiveDML.md) +- [Hive 运维](Hive运维.md) + +## 📚 资料 + +## 🚪 传送 + +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/01.\345\244\247\346\225\260\346\215\256\347\256\200\344\273\213.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/01.\345\244\247\346\225\260\346\215\256\347\256\200\344\273\213.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/01.\345\244\247\346\225\260\346\215\256\347\256\200\344\273\213.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/01.\345\244\247\346\225\260\346\215\256\347\256\200\344\273\213.md" index 6000806806..df1e2c2339 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/01.\345\244\247\346\225\260\346\215\256\347\256\200\344\273\213.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/01.\345\244\247\346\225\260\346\215\256\347\256\200\344\273\213.md" @@ -8,7 +8,7 @@ categories: tags: - 大数据 - 综合 -permalink: /pages/9ab9da/ +permalink: /pages/b593a96e/ --- # 大数据简介 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/02.\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/02.\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240.md" similarity index 99% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/02.\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/02.\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240.md" index d04d0a3919..bbd674dcfa 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/02.\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/02.\345\244\247\346\225\260\346\215\256\345\255\246\344\271\240.md" @@ -9,7 +9,7 @@ tags: - 大数据 - 综合 - 学习 -permalink: /pages/e0d035/ +permalink: /pages/9f9208d5/ --- # 大数据学习路线 diff --git "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/README.md" "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/README.md" similarity index 93% rename from "source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/README.md" rename to "source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/README.md" index 86fb6832c4..c54f827aea 100644 --- "a/source/_posts/16.\345\244\247\346\225\260\346\215\256/00.\347\273\274\345\220\210/README.md" +++ "b/source/_posts/16.\345\244\247\346\225\260\346\215\256/\347\273\274\345\220\210/README.md" @@ -7,7 +7,7 @@ categories: tags: - 大数据 - 综合 -permalink: /pages/ad9b6a/ +permalink: /pages/f3e9aa06/ hidden: true index: false --- diff --git "a/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/01.\350\275\257\344\273\266\345\267\245\347\250\213\345\205\245\351\227\250.md" "b/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/01.\350\275\257\344\273\266\345\267\245\347\250\213\345\205\245\351\227\250.md" index 362c6e2c77..dc740b57f4 100644 --- "a/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/01.\350\275\257\344\273\266\345\267\245\347\250\213\345\205\245\351\227\250.md" +++ "b/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/01.\350\275\257\344\273\266\345\267\245\347\250\213\345\205\245\351\227\250.md" @@ -7,7 +7,7 @@ categories: tags: - 软件工程 - 管理 -permalink: /pages/c6742e/ +permalink: /pages/d3a4f33b/ --- # 软件工程入门指南 diff --git "a/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/README.md" "b/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/README.md" index a98dfaa620..8a63b9ddd6 100644 --- "a/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/README.md" +++ "b/source/_posts/21.\350\275\257\344\273\266\345\267\245\347\250\213/README.md" @@ -5,7 +5,7 @@ categories: - 软件工程 tags: - 软件工程 -permalink: /pages/40d1d0/ +permalink: /pages/b74c7c77/ hidden: true index: false --- @@ -19,8 +19,8 @@ index: false - [人月神话](https://book.douban.com/subject/26358448/) - [人件](https://book.douban.com/subject/25956450/) - [构建之法](https://book.douban.com/subject/25965995/) -- [软件工程之美](https://time.geekbang.org/column/intro/100023701) +- [极客时间教程 - 软件工程之美](https://time.geekbang.org/column/intro/100023701) ## 🚪 传送 -◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ \ No newline at end of file +◾ 💧 [钝悟的 IT 知识图谱](https://dunwu.github.io/waterdrop/) ◾ diff --git "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/01.\346\225\210\347\216\207\346\217\220\345\215\207\346\226\271\346\263\225\350\256\272.md" "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/01.\346\225\210\347\216\207\346\217\220\345\215\207\346\226\271\346\263\225\350\256\272.md" index aa491fd73e..70406fda6b 100644 --- "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/01.\346\225\210\347\216\207\346\217\220\345\215\207\346\226\271\346\263\225\350\256\272.md" +++ "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/01.\346\225\210\347\216\207\346\217\220\345\215\207\346\226\271\346\263\225\350\256\272.md" @@ -10,7 +10,7 @@ tags: - 效率提升 - 方法论 - 5W2H -permalink: /pages/c33173/ +permalink: /pages/bbab00f1/ --- # 效率提升方法论 diff --git "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/03.\350\257\235\346\234\257.md" "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/03.\350\257\235\346\234\257.md" index 897fddd815..1e9ad96e3a 100644 --- "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/03.\350\257\235\346\234\257.md" +++ "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/01.\346\226\271\346\263\225\350\256\272/03.\350\257\235\346\234\257.md" @@ -9,7 +9,7 @@ categories: tags: - 沟通 - 话术 -permalink: /pages/da4df8/ +permalink: /pages/6344ebff/ --- # 话术 diff --git "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/01.\346\212\200\346\234\257\346\226\207\346\241\243\350\247\204\350\214\203.md" "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/01.\346\212\200\346\234\257\346\226\207\346\241\243\350\247\204\350\214\203.md" index 9c4579d987..8d9dae510a 100644 --- "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/01.\346\212\200\346\234\257\346\226\207\346\241\243\350\247\204\350\214\203.md" +++ "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/01.\346\212\200\346\234\257\346\226\207\346\241\243\350\247\204\350\214\203.md" @@ -9,7 +9,7 @@ categories: tags: - 效率提升 - 规范 -permalink: /pages/bebc05/ +permalink: /pages/b7252809/ --- # 技术文档规范 @@ -542,4 +542,4 @@ $1,000 - [豌豆荚文案风格指南](https://docs.google.com/document/d/1R8lMCPf6zCD5KEA8ekZ5knK77iw9J-vJ6vEopPemqZM/edit), by 豌豆荚 - [中文文案排版指北](https://github.com/sparanoid/chinese-copywriting-guidelines), by sparanoid - [中文排版需求](http://w3c.github.io/clreq/), by W3C -- [为什么文件名要小写?](http://www.ruanyifeng.com/blog/2017/02/filename-should-be-lowercase.html), by 阮一峰 +- [为什么文件名要小写?](http://www.ruanyifeng.com/blog/2017/02/filename-should-be-lowercase.html), by 阮一峰 \ No newline at end of file diff --git "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/02.\347\233\256\345\275\225\347\256\241\347\220\206\350\247\204\350\214\203.md" "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/02.\347\233\256\345\275\225\347\256\241\347\220\206\350\247\204\350\214\203.md" index a98dd9b6ba..e9df443dff 100644 --- "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/02.\347\233\256\345\275\225\347\256\241\347\220\206\350\247\204\350\214\203.md" +++ "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/02.\347\233\256\345\275\225\347\256\241\347\220\206\350\247\204\350\214\203.md" @@ -9,7 +9,7 @@ categories: tags: - 效率提升 - 规范 -permalink: /pages/a5f6ca/ +permalink: /pages/d7f2c4f6/ --- # 个人目录管理规范 diff --git "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/03.\344\273\243\347\240\201\345\267\245\347\250\213\350\247\204\350\214\203.md" "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/03.\344\273\243\347\240\201\345\267\245\347\250\213\350\247\204\350\214\203.md" index ada685441e..cac8bfb578 100644 --- "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/03.\344\273\243\347\240\201\345\267\245\347\250\213\350\247\204\350\214\203.md" +++ "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/02.\350\247\204\350\214\203/03.\344\273\243\347\240\201\345\267\245\347\250\213\350\247\204\350\214\203.md" @@ -9,7 +9,7 @@ categories: tags: - 效率提升 - 规范 -permalink: /pages/c23cae/ +permalink: /pages/5f8d7c84/ --- # 代码工程规范 diff --git "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/99.\345\267\245\345\205\267/01.Markdown.md" "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/99.\345\267\245\345\205\267/01.Markdown.md" index 852ae809d6..d5a435730b 100644 --- "a/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/99.\345\267\245\345\205\267/01.Markdown.md" +++ "b/source/_posts/96.\345\267\245\344\275\234/01.\346\225\210\350\203\275/99.\345\267\245\345\205\267/01.Markdown.md" @@ -8,7 +8,7 @@ categories: - 工具 tags: - Markdown -permalink: /pages/f668c0/ +permalink: /pages/832412cc/ --- # Markdown 极简教程 diff --git "a/source/_posts/96.\345\267\245\344\275\234/README.md" "b/source/_posts/96.\345\267\245\344\275\234/README.md" index cb2bca4d42..851dbb27b7 100644 --- "a/source/_posts/96.\345\267\245\344\275\234/README.md" +++ "b/source/_posts/96.\345\267\245\344\275\234/README.md" @@ -5,7 +5,7 @@ categories: - 工作 tags: - 工作 -permalink: /pages/1cd051/ +permalink: /pages/3b9cb1ca/ hidden: true index: false --- diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\270\200.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\270\200.md" new file mode 100644 index 0000000000..bcb94121f3 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\270\200.md" @@ -0,0 +1,770 @@ +--- +title: 《极客时间教程 - Java 并发编程实战》笔记一 +date: 2024-08-26 14:36:05 +categories: + - 笔记 + - Java +tags: + - Java + - 并发 +permalink: /pages/25e6ca5a/ +--- + +# 《极客时间教程 - Java 并发编程实战》笔记一 + +## 学习攻略 如何才能学好并发编程? + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261435639.png) + +## 开篇词 你为什么需要学习并发编程? + +**并发编程可以总结为三个核心问题:分工、同步、互斥。** + +- **分工**指的是如何高效地拆解任务并分配给线程。 +- **同步**指的是线程之间如何协作。 +- **互斥**则是保证同一时刻只允许一个线程访问共享资源。 + +## 可见性、原子性和有序性问题:并发编程 Bug 的源头 + +CPU、内存、I/O 设备三者的速度存在很大差异。为了合理利用 CPU 的高性能,平衡这三者的速度差异,计算机体系结构、操作系统、编译程序都做出了贡献,主要体现为: + +1. CPU 增加了缓存,以均衡与内存的速度差异; +2. 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异; +3. 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。 + +**缓存**导致的可见性问题,**线程切换**带来的原子性问题,**编译优化**带来的有序性问题。 + +### 缓存导致的可见性问题 + +一个线程对共享变量的修改,另外一个线程能够立刻看到,我们称为**可见性**。 + +对于**单核**,所有的线程都是在一个 CPU 上执行,操作同一个 CPU 的缓存;一个线程对缓存的写,对另外一个线程来说一定是可见的。 + +例如在下面的图中,线程 A 和线程 B 都是操作同一个 CPU 里面的缓存,所以线程 A 更新了变量 V 的值,那么线程 B 之后再访问变量 V,得到的一定是 V 的最新值(线程 A 写过的值)。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261442765.png) + +对于**多核**,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。这时两个线程对于变量的操作就不具备可见性了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261444744.png) + +【示例】计数器的并发安全问题示例 + +```java +public class Test { + private long count = 0; + private void add10K() { + int idx = 0;1. + while(idx++ < 10000) { + count += 1; + } + } + public static long calc() { + final Test test = new Test(); + // 创建两个线程,执行 add() 操作 + Thread th1 = new Thread(()->{ + test.add10K(); + }); + Thread th2 = new Thread(()->{ + test.add10K(); + }); + // 启动两个线程 + th1.start(); + th2.start(); + // 等待两个线程执行结束 + th1.join(); + th2.join(); + return count; + } +} +``` + +这段程序的目的是将 count 变量累加导 10000,两个线程执行,则应该累加到 20000,但实际结果总是会小于 20000。 + +### 线程切换带来的原子性问题 + +操作系统允许某个进程执行一小段时间,例如 50 毫秒,过了 50 毫秒操作系统就会重新选择一个进程来执行(我们称为“任务切换”),这个 50 毫秒称为“**时间片**”。现代的操作系统都基于更轻量的线程来调度,现在我们提到的“任务切换”都是指“线程切换”。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261450096.png) + +Java 的并发也是基于任务切换。Java 中,即使是一条语句,也可能需要执行多条 CPU 指令。**一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292035170.png) + +### 编译优化带来的有序性问题 + +有序性指的是程序按照代码的先后顺序执行。编译器为了优化性能,有时候会改变程序中语句的先后顺序。 + +【示例】双重检查创建单例对象 + +```java +public class Singleton { + static Singleton instance; + static Singleton getInstance(){ + if (instance == null) { + synchronized(Singleton.class) { + if (instance == null) + instance = new Singleton(); + } + } + return instance; + } +} +``` + +我们以为的 new 操作应该是: + +1. 分配一块内存 M; +2. 在内存 M 上初始化 Singleton 对象; +3. 然后 M 的地址赋值给 instance 变量。 + +但是实际上优化后的执行路径却是这样的: + +1. 分配一块内存 M; +2. 将 M 的地址赋值给 instance 变量; +3. 最后在内存 M 上初始化 Singleton 对象。 + +优化后会导致什么问题呢?我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 `instance != null` ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261457434.png) + +## Java 内存模型:看 Java 如何解决可见性和有序性问题 + +导致可见性的原因是缓存,导致有序性的原因是编译优化,那解决可见性、有序性最直接的办法就是**禁用缓存和编译优化**,但这种方案性能堪忧。 + +合理的方案应该是**按需禁用缓存以及编译优化**。Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括 **volatile**、**synchronized** 和 **final** 三个关键字,以及六项 **Happens-Before 规则**。 + +### Happens-Before 规则 + +- **程序次序规则** - 在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。 +- **锁定规则** - 一个 `unLock` 操作 Happens-Before 于后面对同一个锁的 `lock` 操作。 +- **volatile 变量规则** - 对一个 `volatile` 变量的写操作 Happens-Before 于后面对这个变量的读操作。 +- **线程启动规则** - `Thread` 对象的 `start()` 方法 Happens-Before 于此线程的每个一个动作。 +- **线程终止规则** - 线程中所有的操作都 Happens-Before 于线程的终止检测,我们可以通过 `Thread.join()` 方法是否结束、`Thread.isAlive()` 的返回值手段检测到线程已经终止执行。 +- **线程中断规则** - 对线程 `interrupt()` 方法的调用 Happens-Before 于被中断线程的代码检测到中断事件的发生,可以通过 `Thread.interrupted()` 方法检测到是否有中断发生。 +- **对象终结规则** - 一个对象的初始化完成 Happens-Before 于它的 `finalize()` 方法的开始。 +- **传递性** - 如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。 + +## 互斥锁(上):解决原子性问题 + +并发原子性问题的源头是**线程切换**。 + +解决这个问题的直接方法就是禁止线程切换。操作系统做线程切换是依赖 CPU 中断的,所以禁止 CPU 发生中断就能够禁止线程切换。这个方案对于单核场景是可行的,但不适用于多核场景。 + +举例来说,long 型变量是 64 位,在 32 位 CPU 上执行写操作会被拆分成两次写操作(写高 32 位和写低 32 位,如下图所示)。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261524478.png) + +在多核场景下,同一时刻,有可能有两个线程同时在执行,一个线程执行在 CPU-1 上,一个线程执行在 CPU-2 上,此时禁止 CPU 中断,只能保证 CPU 上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写 long 型变量高 32 位的话,那就有可能出现我们开头提及的诡异 Bug 了。 + +“**同一时刻只有一个线程执行**”称之为**互斥**。如果能够保证对共享变量的修改是互斥的,那么,无论是单核 CPU 还是多核 CPU,就都能保证原子性了。 + +### 简易锁模型 + +一段需要互斥执行的代码称为**临界区**。线程在进入临界区之前,首先尝试加锁 lock(),如果成功,则进入临界区,此时称这个线程持有锁;否则就等待,直到持有锁的线程解锁;持有锁的线程执行完临界区的代码后,执行解锁 unlock()。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292036259.png) + +### 改进后的锁模型 + +首先,我们要把临界区要保护的资源标注出来,如图中临界区里增加了一个元素:受保护的资源 R;其次,我们要保护资源 R 就得为它创建一把锁 LR;最后,针对这把锁 LR,我们还需在进出临界区时添上加锁操作和解锁操作。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292036343.png) + +### Java 语言提供的锁技术:synchronized + +Java 中,synchronized 是一种锁的实现方式。 + +【示例】synchronized 使用示例 + +```java +class X { + // 修饰非静态方法 + synchronized void foo() { + // 临界区 + } + // 修饰静态方法 + synchronized static void bar() { + // 临界区 + } + // 修饰代码块 + Object obj = new Object(); + void baz() { + synchronized(obj) { + // 临界区 + } + } +} +``` + +**可以用一把锁来保护多个资源,但是不能用多把锁来保护一个资源**。 + +### 用 synchronized 解决 count+=1 问题 + +【示例】synchronized 实现并发安全的计数器 + +```java +class SafeCalc { + long value = 0L; + synchronized long get() { + return value; + } + synchronized void addOne() { + value += 1; + } +} +``` + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261541380.png) + +### 锁和受保护资源的关系 + +**受保护资源和锁之间的关联关系是 N:1 的关系**。 + +【示例】synchronized 实现并发安全的计数器错误示例 + +```java +class SafeCalc { + static long value = 0L; + synchronized long get() { + return value; + } + synchronized static void addOne() { + value += 1; + } +} +``` + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261545832.png) + +【示例】synchronized 实现并发安全的计数器错误示例 + +```java +class SafeCalc { + long value = 0L; + long get() { + synchronized (new Object()) { + return value; + } + } + void addOne() { + synchronized (new Object()) { + value += 1; + } + } +} +``` + +上面的例子中,加锁本质就是在锁对象的对象头中写入当前线程 id,但是 new object 每次在内存中都是新对象,所以加锁无效。 + +## 互斥锁(下):如何用一把锁保护多个资源? + +### 保护没有关联关系的多个资源 + +**用不同的锁对受保护资源进行精细化管理,能够提升性能**。这种锁还有个名字,叫**细粒度锁**。 + +【示例】账户类 Account 有两个成员变量,分别是账户余额 balance 和账户密码 password。取款 withdraw() 和查看余额 getBalance() 操作会访问账户余额 balance,创建一个 final 对象 balLock 作为锁(类比球赛门票);而更改密码 updatePassword() 和查看密码 getPassword() 操作会修改账户密码 password,创建一个 final 对象 pwLock 作为锁(类比电影票)。不同的资源用不同的锁保护,各自管各自的。 + +```java +class Account { + // 锁:保护账户余额 + private final Object balLock + = new Object(); + // 账户余额 + private Integer balance; + // 锁:保护账户密码 + private final Object pwLock + = new Object(); + // 账户密码 + private String password; + + // 取款 + void withdraw(Integer amt) { + synchronized(balLock) { + if (this.balance > amt){ + this.balance -= amt; + } + } + } + // 查看余额 + Integer getBalance() { + synchronized(balLock) { + return balance; + } + } + + // 更改密码 + void updatePassword(String pw){ + synchronized(pwLock) { + this.password = pw; + } + } + // 查看密码 + String getPassword() { + synchronized(pwLock) { + return password; + } + } +} +``` + +> 思考:如果账户余额用 this.balance 作为互斥锁,账户密码用 this.password 作为互斥锁,你觉得是否可以呢? +> +> 答:不能用可变对象做锁。 + +### 保护有关联关系的多个资源 + +【示例】保护临界区多个资源的错误示例 + +```java +class Account { + private int balance; + // 转账 + synchronized void transfer( + Account target, int amt){ + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } +} +``` + +synchronized 可以保护 this 对象持有的资源,但不能保护 target 对象持有的资源。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261603806.png) + +### 使用锁的正确姿势 + +```java +class Account { + private Object lock; + private int balance; + private Account(); + // 创建 Account 时传入同一个 lock 对象 + public Account(Object lock) { + this.lock = lock; + } + // 转账 + void transfer(Account target, int amt){ + // 此处检查所有对象共享的锁 + synchronized(lock) { + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } + } +} +``` + +上面代码思路正确,但存在一个问题:如果创建 Account 对象时,传入的 lock 不是同一个对象,就会出现锁自家门来保护他家资产的荒唐事。 + +因此,可以优化为使用 Class 对象(Account.class)作为共享的锁。Account.class 是所有 Account 对象共享的,而且这个对象是 Java 虚拟机在加载 Account 类的时候创建的,所以不用担心它的唯一性。 + +```java +class Account { + private int balance; + // 转账 + void transfer(Account target, int amt){ + synchronized(Account.class) { + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } + } +} +``` + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261610209.png) + +## 一不小心就死锁了,怎么办? + +**死锁**:**一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象**。 + +【示例】存在死锁的示例 + +```java +class Account { + private int balance; + // 转账 + void transfer(Account target, int amt){ + // 锁定转出账户 + synchronized(this) { + // 锁定转入账户 + synchronized(target) { + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } + } + } +} +``` + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261612406.png) + +### 如何预防死锁 + +只有以下这四个条件都发生时才会出现死锁: + +- **互斥**,共享资源 X 和 Y 只能被一个线程占用; +- **占有且等待**,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; +- **不可抢占**,其他线程不能强行抢占线程 T1 占有的资源; +- **循环等待**,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 + +**也就是说只要我们破坏其中一个,就可以成功避免死锁的发生**。 + +其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢? + +1. 对于“**占用且等待**”,可以一次性申请所有的资源,这样就不存在等待了。 +2. 对于“**不可抢占**”,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。 +3. 对于“**循环等待**”,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。 + +#### 破坏占用且等待条件 + +通过一个 Allocator 来管理临界区。当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后再锁定这两个资源;当转账操作执行完,释放锁之后,需通知 Allocator 同时释放转出账户和转入账户这两个资源。 + +```java +class Allocator { + private List als = + new ArrayList<>(); + // 一次性申请所有资源 + synchronized boolean apply( + Object from, Object to){ + if(als.contains(from) || + als.contains(to)){ + return false; + } else { + als.add(from); + als.add(to); + } + return true; + } + // 归还资源 + synchronized void free( + Object from, Object to){ + als.remove(from); + als.remove(to); + } +} + +class Account { + // actr 应该为单例 + private Allocator actr; + private int balance; + // 转账 + void transfer(Account target, int amt){ + // 一次性申请转出账户和转入账户,直到成功 + while(!actr.apply(this, target)) + ; + try{ + // 锁定转出账户 + synchronized(this){ + // 锁定转入账户 + synchronized(target){ + if (this.balance > amt){ + this.balance -= amt; + target.balance += amt; + } + } + } + } finally { + actr.free(this, target) + } + } +} +``` + +上面的核心代码如下 + +```java +// 一次性申请转出账户和转入账户,直到成功 +while(!actr.apply(this, target)) + ; +``` + +如果 apply() 操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的。但如果 apply() 操作耗时长,或者并发冲突量大的时候,可能遥循环大量次数才能获得锁,太消耗 CPU 了。 + +在这种场景下,更好的方案应该是:如果线程要求的条件(转出账本和转入账本同在文件架上)不满足,则线程阻塞自己,进入**等待**状态;当线程要求的条件(转出账本和转入账本同在文件架上)满足后,**通知**等待的线程重新执行。其中,使用线程阻塞的方式就能避免循环等待消耗 CPU 的问题。 + +#### 破坏不可抢占条件 + +核心是要能够主动释放它占有的资源。 + +synchronized 做不到这点,但是可以通过 Lock 来解决此类问题。 + +#### 破坏循环等待条件 + +破坏这个条件,需要对资源进行排序,然后按序申请资源。 + +假设每个账户都有不同的属性 id,这个 id 可以作为排序字段,申请的时候,我们可以按照从小到大的顺序来申请。比如下面代码中,①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。 + +```java +class Account { + private int id; + private int balance; + // 转账 + void transfer(Account target, int amt){ + Account left = this ① + Account right = target; ② + if (this.id > target.id) { ③ + left = target; ④ + right = this; ⑤ + } ⑥ + // 锁定序号小的账户 + synchronized(left){ + // 锁定序号大的账户 + synchronized(right){ + if (this.balance > amt){ + this.balance -= amt; + target.balance += amt; + } + } + } + } +} +``` + +## 用“等待-通知”机制优化循环等待 + +### 用 synchronized 实现等待-通知机制 + +在 Java 中,等待-通知机制有多种实现方式,比如 Java 语言内置的 synchronized 配合 wait()、notify()、notifyAll() 这三个方法就能轻松实现。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292037649.png) + +wait()、notify()、notifyAll() 方法操作的等待队列是互斥锁的等待队列,所以如果 synchronized 锁定的是 this,那么对应的一定是 this.wait()、this.notify()、this.notifyAll();如果 synchronized 锁定的是 target,那么对应的一定是 target.wait()、target.notify()、target.notifyAll() 。而且 wait()、notify()、notifyAll() 这三个方法能够被调用的前提是已经获取了相应的互斥锁,所以我们会发现 wait()、notify()、notifyAll() 都是在 synchronized{}内部被调用的。如果在 synchronized{}外部调用,或者锁定的 this,而用 target.wait() 调用的话,JVM 会抛出一个运行时异常:`java.lang.IllegalMonitorStateException`。 + +### 小试牛刀:一个更好地资源分配器 + +等待-通知机制中,需要考虑以下四个要素。 + +1. 互斥锁:可以用 this 作为互斥锁。 +2. 线程要求的条件:转出账户和转入账户都没有被分配过。 +3. 何时等待:线程要求的条件不满足就等待。 +4. 何时通知:当有线程释放账户时就通知。 + +```java +class Allocator { + private List als; + // 一次性申请所有资源 + synchronized void apply(Object from, Object to){ + // 经典写法 + while(als.contains(from) || + als.contains(to)){ + try{ + wait(); + }catch(Exception e){ + } + } + als.add(from); + als.add(to); + } + // 归还资源 + synchronized void free(Object from, Object to){ + als.remove(from); + als.remove(to); + notifyAll(); + } +} +``` + +### 尽量使用 notifyAll() + +**notify() 是会随机地通知等待队列中的一个线程,而 notifyAll() 会通知等待队列中的所有线程**。从感觉上来讲,应该是 notify() 更好一些,因为即便通知所有线程,也只有一个线程能够进入临界区。但那所谓的感觉往往都蕴藏着风险,实际上使用 notify() 也很有风险,它的风险在于可能导致某些线程永远不会被通知到。 + +## 安全性、活跃性以及性能问题 + +并发编程中,需要注意三类问题:**安全性问题、活跃性问题和性能问题**。 + +### 安全性问题 + +并发安全/线程安全的本质就是正确性,即程序按照预期执行。 + +并发安全问题的三个主要源头是:原子性、可见性、有序性。通俗的说,多线程同时读写共享变量。 + +对于非共享变量(ThreadLocal)或常量(final),不存在并发安全问题。 + +对于共享变量,在并发环境下,存在竞态条件。 + +- **竞态条件(Race Condition)**:程序的执行结果依赖多线程执行的顺序。 +- **临界区(Critical Sections)**:导致竞态条件发生的代码区称作临界区。 + +对于这种情况,解决方案就是互斥(锁)。 + +### 活跃性问题 + +活跃性问题主要分为: + +- **死锁** +- **活锁** - **有时线程虽然没有发生阻塞,但仍然会存在执行不下去的情况**。解决方案:尝试等待一个随机的时间。 +- **饥饿** - **线程因无法访问所需资源而无法执行下去的情况**。解决方案: + 1. 保证资源充足; + 2. 公平地分配资源; + 3. 避免持有锁的线程长时间执行。 + +### 性能问题 + +三个核心性能指标: + +1. **吞吐量**:指的是单位时间内能处理的请求数量。吞吐量越高,说明性能越好。 +2. **延迟**:指的是从发出请求到收到响应的时间。延迟越小,说明性能越好。 +3. **并发量**:指的是能同时处理的请求数量,一般来说随着并发量的增加、延迟也会增加。所以延迟这个指标,一般都会是基于并发量来说的。例如并发量是 1000 的时候,延迟是 50 毫秒。 + +由互斥而产生的阻塞会影响性能。要提升性能有以下思路: + +- **无锁化** - 相关的技术有:ThreadLocal、写入时复制 (Copy-on-write)、乐观锁、原子类、Disruptor +- **减少锁持有的时间** - 互斥锁本质上是将并行的程序串行化,所以要增加并行度,一定要减少持有锁的时间。相关的技术有:细粒度锁(ConcurrentHashMap 中的分段锁技术);读写锁。 + +## 管程:并发编程的万能钥匙 + +### 什么是管程 + +synchronized 关键字及 wait()、notify()、notifyAll() 这三个方法都是管程的组成部分。而**管程和信号量是等价的,所谓等价指的是用管程能够实现信号量,也能用信号量实现管程**。但是管程更容易使用,所以 Java 选择了管程。 + +管程,对应的英文是 Monitor,很多 Java 领域的同学都喜欢将其翻译成“监视器”,这是直译。操作系统领域一般都翻译成“管程”。 + +所谓**管程,指的是管理共享变量以及对共享变量的操作过程,让他们支持并发**。翻译为 Java 领域的语言,就是管理类的成员变量和成员方法,让这个类是线程安全的。 + +### MESA 模型 + +Java 参考了 MESA 模型,语言内置的管程(synchronized)对 MESA 模型进行了精简。MESA 模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。 + +并发领域两大核心问题,管程都是能够解决的。 + +一个是**互斥**,即同一时刻只允许一个线程访问共享资源; + +一个是**同步**,即线程之间如何通信、协作。 + +管程是如何解决**互斥**问题的: + +将共享变量及其对共享变量的操作统一封装起来。在下图中,管程 X 将共享变量 queue 这个队列和相关的操作入队 enq()、出队 deq() 都封装起来了;线程 A 和线程 B 如果想访问共享变量 queue,只能通过调用管程提供的 enq()、deq() 方法来实现;enq()、deq() 保证互斥性,只允许一个线程进入管程。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408261940150.png) + +管程是如何解决线程间的**同步**问题的: + +在管程模型里,共享变量和对共享变量的操作是被封装起来的,图中最外层的框就代表封装的意思。框的上面只有一个入口,并且在入口旁边还有一个入口等待队列。当多个线程同时试图进入管程内部时,只允许一个线程进入,其他线程则在入口等待队列中等待。管程里还引入了条件变量的概念,而且**每个条件变量都对应有一个等待队列**,如下图,条件变量 A 和条件变量 B 分别都有自己的等待队列。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270725745.png) + +## Java 线程(上):Java 线程的生命周期 + +### 通用的线程生命周期 + +通用的线程生命周期:**初始状态、可运行状态、运行状态、休眠状态**和**终止状态**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270729535.png) + +### Java 中线程的生命周期 + +Java 中线程共有六种状态: + +1. NEW(初始化状态) +2. RUNNABLE(可运行 / 运行状态) +3. BLOCKED(阻塞状态) +4. WAITING(无时限等待) +5. TIMED_WAITING(有时限等待) +6. TERMINATED(终止状态) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270731084.png) + +## Java 线程(中):创建多少线程才是合适的? + +### 为什么要使用多线程? + +度量性能的核心指标: + +- **延迟** - 延迟指的是发出请求到收到响应这个过程的时间;延迟越短,意味着程序执行得越快,性能也就越好。 +- **吞吐量** - 吞吐量指的是在单位时间内能处理请求的数量;吞吐量越大,意味着程序能处理的请求越多,性能也就越好。 + +### 多线程的应用场景有哪些? + +**降低延迟,提高吞吐量**,有两个方向:一个方向是**优化算法**,另一个方向是**将硬件的性能发挥到极致**。计算机主要有哪些硬件呢?主要是两类:一个是 I/O,一个是 CPU。简言之,**在并发编程领域,提升性能本质上就是提升硬件的利用率,再具体点来说,就是提升 I/O 的利用率和 CPU 的利用率**。 + +### 创建多少线程合适? + +创建多少线程合适,要看多线程具体的应用场景。 + +程序一般都是 CPU 计算和 I/O 操作交叉执行的。I/O 操作执行时间长的,称为 I/O 密集型计算;CPU 操作执行时间长的,称为 CPU 密集型计算。 + +**对于 CPU 密集型的计算场景,理论上“线程的数量 =CPU 核数”就是最合适的**。不过在工程上,**线程的数量一般会设置为“CPU 核数 +1”**,这样的话,当线程因为偶尔的内存页失效或其他原因导致阻塞时,这个额外的线程可以顶上,从而保证 CPU 的利用率。 + +对于 I/O 密集型计算场景,最佳的线程数是与程序中 CPU 计算和 I/O 操作的耗时比相关的,我们可以总结出这样一个公式: + +> 最佳线程数 =CPU 核数 \* [ 1 +(I/O 耗时 / CPU 耗时)] + +## Java 线程(下):为什么局部变量是线程安全的? + +### 方法是如何被执行的 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270751420.png)“CPU 去哪里找到调用方法的参数和返回地址? + +**通过 CPU 的堆栈寄存器**。CPU 支持一种栈结构,先入后出。因为这个栈是和方法调用相关的,因此经常被称为**调用栈**。 + +例如,有三个方法 A、B、C,他们的调用关系是 A->B->C(A 调用 B,B 调用 C),在运行时,会构建出下面这样的调用栈。每个方法在调用栈里都有自己的独立空间,称为**栈帧**,每个栈帧里都有对应方法需要的参数和返回地址。当调用方法时,会创建新的栈帧,并压入调用栈;当方法返回时,对应的栈帧就会被自动弹出。也就是说,**栈帧和方法是同生共死的**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270753265.png) + +### 局部变量存哪里? + +局部变量的作用域是方法内部,也就是说当方法执行完,局部变量就没用了,局部变量应该和方法同生共死。此时你应该会想到调用栈的栈帧,调用栈的栈帧就是和方法同生共死的,所以局部变量放到调用栈里那儿是相当的合理。事实上,的确是这样的,**局部变量就是放到了调用栈里**。于是调用栈的结构就变成了下图这样。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270755942.png) + +### 调用栈与线程 + +那调用栈和线程之间是什么关系呢? + +答案是:**每个线程都有自己独立的调用栈**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270756092.png) + +因为每个线程都有自己的调用栈,局部变量保存在线程各自的调用栈里面,不会共享,所以自然也就没有并发问题。再次重申一遍:没有共享,就没有伤害。 + +### 线程封闭 + +方法里的局部变量,因为不会和其他线程共享,所以没有并发问题,这个思路很好,已经成为解决并发问题的一个重要技术,同时还有个响当当的名字叫做**线程封闭**,比较官方的解释是:**仅在单线程内访问数据**。由于不存在共享,所以即便不同步也不会有并发问题,性能杠杠的。 + +采用线程封闭技术的案例非常多,例如从数据库连接池里获取的连接 Connection + +## 如何用面向对象思想写好并发程序? + +### 一、封装共享变量 + +**将共享变量作为对象属性封装在内部,对所有公共方法制定并发访问策略**。 + +**对于这些不会发生变化的共享变量,建议你用 final 关键字来修饰**。 + +### 二、识别共享变量间的约束条件 + +识别共享变量间的约束条件非常重要。因为**这些约束条件,决定了并发访问策略**。 + +共享变量之间的约束条件,反映在代码里,基本上都会有 if 语句,所以,一定要特别注意竞态条件。 + +### 三、制定并发访问策略 + +并发访问策略方案: + +1. 避免共享:避免共享的技术主要是利于线程本地存储以及为每个任务分配独立的线程。 +2. 不变模式:这个在 Java 领域应用的很少,但在其他领域却有着广泛的应用,例如 Actor 模式、CSP 模式以及函数式编程的基础都是不变模式。 +3. 管程及其他同步工具:Java 领域万能的解决方案是管程,但是对于很多特定场景,使用 Java 并发包提供的读写锁、并发容器等同步工具会更好。 + +## 理论基础模块热点问题答疑 + +起源是一个硬件的核心矛盾:CPU 与内存、I/O 的速度差异,系统软件(操作系统、编译器)在解决这个核心矛盾的同时,引入了可见性、原子性和有序性问题,这三个问题就是很多并发程序的 Bug 之源。 + +如何解决这三个问题呢?Java 中提供了 Java 内存模型,以应对可见性和有序性问题;提供了互斥锁,以应对原子性问题。 + +互斥锁是解决并发问题的核心工具,但它也可能会带来死锁问题。 + +管程,是 Java 并发编程技术的基础,是解决并发问题的万能钥匙。并发编程里两大核心问题——互斥和同步,都是可以由管程来解决的。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408270805546.png) + +## 参考资料 + +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\270\211.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\270\211.md" new file mode 100644 index 0000000000..30b371bd8a --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\270\211.md" @@ -0,0 +1,1327 @@ +--- +title: 《极客时间教程 - Java 并发编程实战》笔记三 +date: 2024-08-30 08:02:52 +categories: + - 笔记 + - Java +tags: + - Java + - 并发 +permalink: /pages/425a615a/ +--- + +# 《极客时间教程 - Java 并发编程实战》笔记三 + +## Immutability 模式:如何利用不变性解决并发问题? + +解决并发问题,其实最简单的办法就是让共享变量只有读操作,而没有写操作。这个办法如此重要,以至于被上升到了一种解决并发问题的设计模式:**不变性(Immutability)模式**。所谓**不变性,简单来讲,就是对象一旦被创建之后,状态就不再发生变化**。换句话说,就是变量一旦被赋值,就不允许修改了(没有写操作);没有修改操作,也就是保持了不变性。 + +### 快速实现具备不可变性的类 + +**将一个类所有的属性都设置成 final 的,并且只允许存在只读方法,那么这个类基本上就具备不可变性了**。更严格的做法是**这个类本身也是 final 的**,也就是不允许继承。因为子类可以覆盖父类的方法,有可能改变不可变性。 + +经常用到的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。它们都严格遵守不可变类的三点要求:**类和属性都是 final 的,所有方法均是只读的**。 + +Java 的 String 方法也有类似字符替换操作,怎么能说所有方法都是只读的呢?下面的示例代码源自 Java 1.8 SDK,仅保留了关键属性 value[] 和 replace() 方法,你会发现:String 这个类以及它的属性 value[] 都是 final 的;而 replace() 方法的实现,就的确没有修改 value[],而是将替换后的字符串作为返回值返回了。 + +```java +public final class String { + private final char value[]; + // 字符替换 + String replace(char oldChar, + char newChar) { + //无需替换,直接返回 this + if (oldChar == newChar){ + return this; + } + + int len = value.length; + int i = -1; + /* avoid getfield opcode */ + char[] val = value; + //定位到需要替换的字符位置 + while (++i < len) { + if (val[i] == oldChar) { + break; + } + } + //未找到 oldChar,无需替换 + if (i >= len) { + return this; + } + //创建一个 buf[],这是关键 + //用来保存替换后的字符串 + char buf[] = new char[len]; + for (int j = 0; j < i; j++) { + buf[j] = val[j]; + } + while (i < len) { + char c = val[i]; + buf[i] = (c == oldChar) ? + newChar : c; + i++; + } + //创建一个新的字符串返回 + //原字符串不会发生任何变化 + return new String(buf, true); + } +} +``` + +### 利用享元模式避免创建重复对象 + +**享元模式(Flyweight Pattern)可以减少创建对象的数量,从而减少内存占用。**Java 语言里面 Long、Integer、Short、Byte 等这些基本数据类型的包装类都用到了享元模式。 + +享元模式本质上其实就是一个**对象池**,利用享元模式创建对象的逻辑也很简单:创建之前,首先去对象池里看看是不是存在;如果已经存在,就利用对象池里的对象;如果不存在,就会新创建一个对象,并且把这个新创建出来的对象放进对象池里。 + +Long 这个类并没有照搬享元模式,Long 内部维护了一个静态的对象池,仅缓存了 [-128,127] 之间的数字,这个对象池在 JVM 启动的时候就创建好了,而且这个对象池一直都不会变化,也就是说它是静态的。之所以采用这样的设计,是因为 Long 这个对象的状态共有 2^64 种,实在太多,不宜全部缓存,而 [-128,127] 之间的数字利用率最高。 + +```java +Long valueOf(long l) { + final int offset = 128; + // [-128,127] 直接的数字做了缓存 + if (l >= -128 && l <= 127) { + return LongCache + .cache[(int)l + offset]; + } + return new Long(l); +} +//缓存,等价于对象池 +//仅缓存 [-128,127] 直接的数字 +static class LongCache { + static final Long cache[] + = new Long[-(-128) + 127 + 1]; + + static { + for(int i=0; i + rf = new AtomicReference<>( + new WMRange(0,0) + ); + // 设置库存上限 + void setUpper(int v){ + while(true){ + WMRange or = rf.get(); + // 检查参数合法性 + if(v < or.lower){ + throw new IllegalArgumentException(); + } + WMRange nr = new + WMRange(v, or.lower); + if(rf.compareAndSet(or, nr)){ + return; + } + } + } +} +``` + +### 总结 + +利用 Immutability 模式解决并发问题,也许你觉得有点陌生,其实你天天都在享受它的战果。Java 语言里面的 String 和 Long、Integer、Double 等基础类型的包装类都具备不可变性,这些对象的线程安全性都是靠不可变性来保证的。Immutability 模式是最简单的解决并发问题的方法,建议当你试图解决一个并发问题时,可以首先尝试一下 Immutability 模式,看是否能够快速解决。 + +具备不变性的对象,只有一种状态,这个状态由对象内部所有的不变属性共同决定。其实还有一种更简单的不变性对象,那就是**无状态**。无状态对象内部没有属性,只有方法。除了无状态的对象,你可能还听说过无状态的服务、无状态的协议等等。无状态有很多好处,最核心的一点就是性能。在多线程领域,无状态对象没有线程安全问题,无需同步处理,自然性能很好;在分布式领域,无状态意味着可以无限地水平扩展,所以分布式领域里面性能的瓶颈一定不是出在无状态的服务节点上。 + +## Copy-on-Write 模式:不是延时策略的 COW + +Copy-on-Write,经常被缩写为 COW 或者 CoW,顾名思义就是**写时复制**。 + +### Copy-on-Write 模式的应用领域 + +CopyOnWriteArrayList 和 CopyOnWriteArraySet 这两个 Copy-on-Write 容器,它们背后的设计思想就是 Copy-on-Write;通过 Copy-on-Write 这两个容器实现的读操作是无锁的,由于无锁,所以将读操作的性能发挥到了极致。 + +**Copy-on-Write 最大的应用领域还是在函数式编程领域**。函数式编程的基础是不可变性(Immutability),所以函数式编程里面所有的修改操作都需要 Copy-on-Write 来解决。 + +### 一个真实案例 + +Router 的实现代码如下所示,是一种典型 Immutability 模式的实现,需要你注意的是我们重写了 equals 方法,这样 CopyOnWriteArraySet 的 add() 和 remove() 方法才能正常工作。 + +```java +//路由信息 +public final class Router{ + private final String ip; + private final Integer port; + private final String iface; + //构造函数 + public Router(String ip, + Integer port, String iface){ + this.ip = ip; + this.port = port; + this.iface = iface; + } + //重写 equals 方法 + public boolean equals(Object obj){ + if (obj instanceof Router) { + Router r = (Router)obj; + return iface.equals(r.iface) && + ip.equals(r.ip) && + port.equals(r.port); + } + return false; + } + public int hashCode() { + //省略 hashCode 相关代码 + } +} +//路由表信息 +public class RouterTable { + //Key: 接口名 + //Value: 路由集合 + ConcurrentHashMap> + rt = new ConcurrentHashMap<>(); + //根据接口名获取路由表 + public Set get(String iface){ + return rt.get(iface); + } + //删除路由 + public void remove(Router router) { + Set set=rt.get(router.iface); + if (set != null) { + set.remove(router); + } + } + //增加路由 + public void add(Router router) { + Set set = rt.computeIfAbsent( + route.iface, r -> + new CopyOnWriteArraySet<>()); + set.add(router); + } +} +``` + +## 线程本地存储模式:没有共享,就没有伤害 + +**线程封闭**,其本质上就是避免共享。没有共享,自然也就没有并发安全问题。 + +Java 中,ThreadLocal 就可以做到线程封闭。 + +### ThreadLocal 的使用方法 + +SimpleDateFormat 不是线程安全的,如果要保证并发安全,可以使用 ThreadLocal 来解决。 + +```java +static class SafeDateFormat { + //定义 ThreadLocal 变量 + static final ThreadLocal + tl=ThreadLocal.withInitial( + ()-> new SimpleDateFormat( + "yyyy-MM-dd HH:mm:ss")); + + static DateFormat get(){ + return tl.get(); + } +} +//不同线程执行下面代码 +//返回的 df 是不同的 +DateFormat df = SafeDateFormat.get(); +``` + +### ThreadLocal 的工作原理 + +ThreadLocal 的目标是让不同的线程有不同的变量 V,那最直接的方法就是创建一个 Map,它的 Key 是线程,Value 是每个线程拥有的变量 V,ThreadLocal 内部持有这样的一个 Map 就可以了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010704287.png) + +```java +class MyThreadLocal { + Map locals = + new ConcurrentHashMap<>(); + //获取线程变量 + T get() { + return locals.get( + Thread.currentThread()); + } + //设置线程变量 + void set(T t) { + locals.put( + Thread.currentThread(), t); + } +} +``` + +Java 的实现里面也有一个 Map,叫做 ThreadLocalMap,不过持有 ThreadLocalMap 的不是 ThreadLocal,而是 Thread。Thread 这个类内部有一个私有属性 threadLocals,其类型就是 ThreadLocalMap,ThreadLocalMap 的 Key 是 ThreadLocal。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010705524.png) + +Thread 持有 ThreadLocalMap 的示意图 + +```java +class Thread { + //内部持有 ThreadLocalMap + ThreadLocal.ThreadLocalMap + threadLocals; +} +class ThreadLocal{ + public T get() { + //首先获取线程持有的 + //ThreadLocalMap + ThreadLocalMap map = + Thread.currentThread() + .threadLocals; + //在 ThreadLocalMap 中 + //查找变量 + Entry e = + map.getEntry(this); + return e.value; + } + static class ThreadLocalMap{ + //内部是数组而不是 Map + Entry[] table; + //根据 ThreadLocal 查找 Entry + Entry getEntry(ThreadLocal key){ + //省略查找逻辑 + } + //Entry 定义 + static class Entry extends + WeakReference{ + Object value; + } + } +} +``` + +在 Java 的实现方案里面,ThreadLocal 仅仅是一个代理工具类,内部并不持有任何与线程相关的数据,所有和线程相关的数据都存储在 Thread 里面,这样的设计容易理解。 + +当然还有一个更加深层次的原因,那就是**不容易产生内存泄露**。在我们的设计方案中,ThreadLocal 持有的 Map 会持有 Thread 对象的引用,这就意味着,只要 ThreadLocal 对象存在,那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长,所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有 ThreadLocalMap,而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用(WeakReference),所以只要 Thread 对象可以被回收,那么 ThreadLocalMap 就能被回收。Java 的这种实现方案虽然看上去复杂一些,但是更加安全。 + +### ThreadLocal 与内存泄露 + +在线程池中使用 ThreadLocal 为什么可能导致内存泄露呢?原因就出在线程池中线程的存活时间太长,往往都是和程序同生共死的,这就意味着 Thread 持有的 ThreadLocalMap 一直都不会被回收,再加上 ThreadLocalMap 中的 Entry 对 ThreadLocal 是弱引用(WeakReference),所以只要 ThreadLocal 结束了自己的生命周期是可以被回收掉的。但是 Entry 中的 Value 却是被 Entry 强引用的,所以即便 Value 的生命周期结束了,Value 也是无法被回收的,从而导致内存泄露。 + +那在线程池中,我们该如何正确使用 ThreadLocal 呢?其实很简单,既然 JVM 不能做到自动释放对 Value 的强引用,那我们手动释放就可以了。如何能做到手动释放呢?估计你马上想到** try{}finally{}方案**了,这个简直就是**手动释放资源的利器**。 + +```java +ExecutorService es; +ThreadLocal tl; +es.execute(()->{ + //ThreadLocal 增加变量 + tl.set(obj); + try { + // 省略业务逻辑代码 + }finally { + //手动清理 ThreadLocal + tl.remove(); + } +}); +``` + +## InheritableThreadLocal 与继承性 + +通过 ThreadLocal 创建的线程变量,其子线程是无法继承的。也就是说你在线程中通过 ThreadLocal 创建了线程变量 V,而后该线程创建了子线程,你在子线程中是无法通过 ThreadLocal 来访问父线程的线程变量 V 的。 + +如果你需要子线程继承父线程的线程变量,那该怎么办呢?其实很简单,Java 提供了 InheritableThreadLocal 来支持这种特性,InheritableThreadLocal 是 ThreadLocal 子类,所以用法和 ThreadLocal 相同。 + +不过,完全不建议你在线程池中使用 InheritableThreadLocal,不仅仅是因为它具有 ThreadLocal 相同的缺点——可能导致内存泄露,更重要的原因是:线程池中线程的创建是动态的,很容易导致继承关系错乱,如果你的业务逻辑依赖 InheritableThreadLocal,那么很可能导致业务逻辑计算错误,而这个错误往往比内存泄露更要命。 + +## Guarded Suspension 模式:等待唤醒机制的规范实现 + +消息队列在互联网大厂中用的非常多,主要用作流量削峰和系统解耦。在这种接入方式中,发送消息和消费结果这两个操作之间是异步的。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010706341.png) + +```java +class Message{ + String id; + String content; +} +//该方法可以发送消息 +void send(Message msg){ + //省略相关代码 +} +//MQ 消息返回后会调用该方法 +//该方法的执行线程不同于 +//发送消息的线程 +void onMessage(Message msg){ + //省略相关代码 +} +//处理浏览器发来的请求 +Respond handleWebReq(){ + //创建一消息 + Message msg1 = new + Message("1","{...}"); + //发送消息 + send(msg1); + //如何等待 MQ 返回的消息呢? + String result = ...; +} +``` + +### Guarded Suspension 模式 + +**Guarded Suspension** 模式就是“保护性地暂停”。 + +一个对象 GuardedObject,内部有一个成员变量——受保护的对象,以及两个成员方法——`get(Predicate p)`和`onChanged(T obj)`方法。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010706780.png) + +GuardedObject 的内部实现非常简单,是管程的一个经典用法,核心是:get() 方法通过条件变量的 await() 方法实现等待,onChanged() 方法通过条件变量的 signalAll() 方法实现唤醒功能。逻辑还是很简单的,所以这里就不再详细介绍了。 + +```java +class GuardedObject{ + //受保护的对象 + T obj; + final Lock lock = + new ReentrantLock(); + final Condition done = + lock.newCondition(); + final int timeout=1; + //获取受保护对象 + T get(Predicate p) { + lock.lock(); + try { + //MESA 管程推荐写法 + while(!p.test(obj)){ + done.await(timeout, + TimeUnit.SECONDS); + } + }catch(InterruptedException e){ + throw new RuntimeException(e); + }finally{ + lock.unlock(); + } + //返回非空的受保护对象 + return obj; + } + //事件通知方法 + void onChanged(T obj) { + lock.lock(); + try { + this.obj = obj; + done.signalAll(); + } finally { + lock.unlock(); + } + } +} +``` + +### 扩展 Guarded Suspension 模式 + +Guarded Suspension 模式里 GuardedObject 有两个核心方法,一个是 get() 方法,一个是 onChanged() 方法。很显然,在处理 Web 请求的方法 handleWebReq() 中,可以调用 GuardedObject 的 get() 方法来实现等待;在 MQ 消息的消费方法 onMessage() 中,可以调用 GuardedObject 的 onChanged() 方法来实现唤醒。 + +```java +//处理浏览器发来的请求 +Respond handleWebReq(){ + //创建一消息 + Message msg1 = new + Message("1","{...}"); + //发送消息 + send(msg1); + //利用 GuardedObject 实现等待 + GuardedObject go + =new GuardObjec<>(); + Message r = go.get( + t->t != null); +} +void onMessage(Message msg){ + //如何找到匹配的 go? + GuardedObject go=??? + go.onChanged(msg); +} +``` + +handleWebReq() 里面创建了 GuardedObject 对象的实例 go,并调用其 get() 方等待结果,那在 onMessage() 方法中,如何才能够找到匹配的 GuardedObject 对象呢? + +可以扩展一下 Guarded Suspension 模式,从而使它能够很方便地解决小灰同学的问题。在小灰的程序中,每个发送到 MQ 的消息,都有一个唯一性的属性 id,所以我们可以维护一个 MQ 消息 id 和 GuardedObject 对象实例的关系。 + +```java +class GuardedObject{ + //受保护的对象 + T obj; + final Lock lock = + new ReentrantLock(); + final Condition done = + lock.newCondition(); + final int timeout=2; + //保存所有 GuardedObject + final static Map + gos=new ConcurrentHashMap<>(); + //静态方法创建 GuardedObject + static GuardedObject + create(K key){ + GuardedObject go=new GuardedObject(); + gos.put(key, go); + return go; + } + static void + fireEvent(K key, T obj){ + GuardedObject go=gos.remove(key); + if (go != null){ + go.onChanged(obj); + } + } + //获取受保护对象 + T get(Predicate p) { + lock.lock(); + try { + //MESA 管程推荐写法 + while(!p.test(obj)){ + done.await(timeout, + TimeUnit.SECONDS); + } + }catch(InterruptedException e){ + throw new RuntimeException(e); + }finally{ + lock.unlock(); + } + //返回非空的受保护对象 + return obj; + } + //事件通知方法 + void onChanged(T obj) { + lock.lock(); + try { + this.obj = obj; + done.signalAll(); + } finally { + lock.unlock(); + } + } +} +``` + +客户端代码 + +```java +//处理浏览器发来的请求 +Respond handleWebReq(){ + int id=序号生成器。get(); + //创建一消息 + Message msg1 = new + Message(id,"{...}"); + //创建 GuardedObject 实例 + GuardedObject go= + GuardedObject.create(id); + //发送消息 + send(msg1); + //等待 MQ 消息 + Message r = go.get( + t->t != null); +} +void onMessage(Message msg){ + //唤醒等待的线程 + GuardedObject.fireEvent( + msg.id, msg); +} +``` + +### 总结 + +Guarded Suspension 模式本质上是一种等待唤醒机制的实现,只不过 Guarded Suspension 模式将其规范化了。规范化的好处是你无需重头思考如何实现,也无需担心实现程序的可理解性问题,同时也能避免一不小心写出个 Bug 来。但 Guarded Suspension 模式在解决实际问题的时候,往往还是需要扩展的,扩展的方式有很多,本篇文章就直接对 GuardedObject 的功能进行了增强,Dubbo 中 DefaultFuture 这个类也是采用的这种方式,你可以对比着来看,相信对 DefaultFuture 的实现原理会理解得更透彻。当然,你也可以创建新的类来实现对 Guarded Suspension 模式的扩展。 + +Guarded Suspension 模式也常被称作 Guarded Wait 模式、Spin Lock 模式(因为使用了 while 循环去等待),这些名字都很形象,不过它还有一个更形象的非官方名字:多线程版本的 if。单线程场景中,if 语句是不需要等待的,因为在只有一个线程的条件下,如果这个线程被阻塞,那就没有其他活动线程了,这意味着 if 判断条件的结果也不会发生变化了。但是多线程场景中,等待就变得有意义了,这种场景下,if 判断条件的结果是可能发生变化的。所以,用“多线程版本的 if”来理解这个模式会更简单。 + +## Balking 模式:再谈线程安全的单例模式 + +需要快速放弃的一个最常见的例子是各种编辑器提供的自动保存功能。自动保存功能的实现逻辑一般都是隔一定时间自动执行存盘操作,存盘操作的前提是文件做过修改,如果文件没有执行过修改操作,就需要快速放弃存盘操作。下面的示例代码将自动保存功能代码化了,很显然 AutoSaveEditor 这个类不是线程安全的,因为对共享变量 changed 的读写没有使用同步,那如何保证 AutoSaveEditor 的线程安全性呢? + +```java +class AutoSaveEditor { + + //文件是否被修改过 + boolean changed = false; + //定时任务线程池 + ScheduledExecutorService ses = Executors.newSingleThreadScheduledExecutor(); + + //定时执行自动保存 + void startAutoSave() { + ses.scheduleWithFixedDelay(() -> { autoSave(); }, 5, 5, TimeUnit.SECONDS); + } + + //自动存盘操作 + void autoSave() { + if (!changed) { + return; + } + changed = false; + //执行存盘操作 + //省略且实现 + this.execSave(); + } + + //编辑操作 + void edit() { + //省略编辑逻辑 + changed = true; + } + +} +``` + +解决这个问题相信你一定手到擒来了:读写共享变量 changed 的方法 autoSave() 和 edit() 都加互斥锁就可以了。这样做虽然简单,但是性能很差,原因是锁的范围太大了。那我们可以将锁的范围缩小,只在读写共享变量 changed 的地方加锁,实现代码如下所示。 + +```java +//自动存盘操作 +void autoSave() { + synchronized (this) { + if (!changed) { + return; + } + changed = false; + } + //执行存盘操作 + //省略且实现 + this.execSave(); +} + +//编辑操作 +void edit() { + //省略编辑逻辑 + synchronized (this) { + changed = true; + } +} +``` + +### Balking 模式的经典实现 + +Balking 模式本质上是一种规范化地解决“多线程版本的 if”的方案,对于上面自动保存的例子,使用 Balking 模式规范化之后的写法如下所示,你会发现仅仅是将 edit() 方法中对共享变量 changed 的赋值操作抽取到了 change() 中,这样的好处是将并发处理逻辑和业务逻辑分开。 + +```java +boolean changed=false; +//自动存盘操作 +void autoSave(){ + synchronized(this){ + if (!changed) { + return; + } + changed = false; + } + //执行存盘操作 + //省略且实现 + this.execSave(); +} +//编辑操作 +void edit(){ + //省略编辑逻辑 + ...... + change(); +} +//改变状态 +void change(){ + synchronized(this){ + changed = true; + } +} +``` + +### 用 volatile 实现 Balking 模式 + +前面我们用 synchronized 实现了 Balking 模式,这种实现方式最为稳妥,建议你实际工作中也使用这个方案。不过在某些特定场景下,也可以使用 volatile 来实现,但**使用 volatile 的前提是对原子性没有要求**。 + +在 RPC 框架中,本地路由表是要和注册中心进行信息同步的,应用启动的时候,会将应用依赖服务的路由表从注册中心同步到本地路由表中,如果应用重启的时候注册中心宕机,那么会导致该应用依赖的服务均不可用,因为找不到依赖服务的路由表。为了防止这种极端情况出现,RPC 框架可以将本地路由表自动保存到本地文件中,如果重启的时候注册中心宕机,那么就从本地文件中恢复重启前的路由表。这其实也是一种降级的方案。 + +自动保存路由表和前面介绍的编辑器自动保存原理是一样的,也可以用 Balking 模式实现,不过我们这里采用 volatile 来实现,实现的代码如下所示。之所以可以采用 volatile 来实现,是因为对共享变量 changed 和 rt 的写操作不存在原子性的要求,而且采用 scheduleWithFixedDelay() 这种调度方式能保证同一时刻只有一个线程执行 autoSave() 方法。 + +```java +//路由表信息 +public class RouterTable { + //Key: 接口名 + //Value: 路由集合 + ConcurrentHashMap> + rt = new ConcurrentHashMap<>(); + //路由表是否发生变化 + volatile boolean changed; + //将路由表写入本地文件的线程池 + ScheduledExecutorService ses= + Executors.newSingleThreadScheduledExecutor(); + //启动定时任务 + //将变更后的路由表写入本地文件 + public void startLocalSaver(){ + ses.scheduleWithFixedDelay(()->{ + autoSave(); + }, 1, 1, MINUTES); + } + //保存路由表到本地文件 + void autoSave() { + if (!changed) { + return; + } + changed = false; + //将路由表写入本地文件 + //省略其方法实现 + this.save2Local(); + } + //删除路由 + public void remove(Router router) { + Set set=rt.get(router.iface); + if (set != null) { + set.remove(router); + //路由表已发生变化 + changed = true; + } + } + //增加路由 + public void add(Router router) { + Set set = rt.computeIfAbsent( + route.iface, r -> + new CopyOnWriteArraySet<>()); + set.add(router); + //路由表已发生变化 + changed = true; + } +} +``` + +Balking 模式有一个非常典型的应用场景就是单次初始化,下面的示例代码是它的实现。这个实现方案中,我们将 init() 声明为一个同步方法,这样同一个时刻就只有一个线程能够执行 init() 方法;init() 方法在第一次执行完时会将 inited 设置为 true,这样后续执行 init() 方法的线程就不会再执行 doInit() 了。 + +```java +class InitTest{ + boolean inited = false; + synchronized void init(){ + if(inited){ + return; + } + //省略 doInit 的实现 + doInit(); + inited=true; + } +} +``` + +线程安全的单例模式本质上其实也是单次初始化,所以可以用 Balking 模式来实现线程安全的单例模式,下面的示例代码是其实现。这个实现虽然功能上没有问题,但是性能却很差,因为互斥锁 synchronized 将 getInstance() 方法串行化了,那有没有办法可以优化一下它的性能呢? + +```java +class Singleton{ + private static + Singleton singleton; + //构造方法私有化 + private Singleton(){} + //获取实例(单例) + public synchronized static + Singleton getInstance(){ + if(singleton == null){ + singleton=new Singleton(); + } + return singleton; + } +} +``` + +办法当然是有的,那就是经典的**双重检查**(Double Check)方案,下面的示例代码是其详细实现。在双重检查方案中,一旦 Singleton 对象被成功创建之后,就不会执行 synchronized(Singleton.class){}相关的代码,也就是说,此时 getInstance() 方法的执行路径是无锁的,从而解决了性能问题。不过需要你注意的是,这个方案中使用了 volatile 来禁止编译优化。至于获取锁后的二次检查,则是出于对安全性负责。 + +```java +class Singleton{ + private static volatile + Singleton singleton; + //构造方法私有化 + private Singleton() {} + //获取实例(单例) + public static Singleton + getInstance() { + //第一次检查 + if(singleton==null){ + synchronize(Singleton.class){ + //获取锁后二次检查 + if(singleton==null){ + singleton=new Singleton(); + } + } + } + return singleton; + } +} +``` + +### 总结 + +Balking 模式和 Guarded Suspension 模式从实现上看似乎没有多大的关系,Balking 模式只需要用互斥锁就能解决,而 Guarded Suspension 模式则要用到管程这种高级的并发原语;但是从应用的角度来看,它们解决的都是“线程安全的 if”语义,不同之处在于,Guarded Suspension 模式会等待 if 条件为真,而 Balking 模式不会等待。 + +Balking 模式的经典实现是使用互斥锁,你可以使用 Java 语言内置 synchronized,也可以使用 SDK 提供 Lock;如果你对互斥锁的性能不满意,可以尝试采用 volatile 方案,不过使用 volatile 方案需要你更加谨慎。 + +## Thread-Per-Message 模式:最简单实用的分工方法 + +并发编程领域里,解决分工问题也有一系列的设计模式,比较常用的主要有 Thread-Per-Message 模式、Worker Thread 模式、生产者-消费者模式等等。 + +### 如何理解 Thread-Per-Message 模式 + +现实世界里,很多事情我们都需要委托他人办理,委托他人代办有一个非常大的好处,那就是可以专心做自己的事了。 + +在编程领域也有很多类似的需求,比如写一个 HTTP Server,创建一个子线程,委托子线程去处理 HTTP 请求。 + +这种委托他人办理的方式,在并发编程领域被总结为一种设计模式,叫做** Thread-Per-Message 模式**,简言之就是为每个任务分配一个独立的线程。 + +### 用 Thread 实现 Thread-Per-Message 模式 + +Thread-Per-Message 模式的一个最经典的应用场景是**网络编程里服务端的实现**,服务端为每个客户端请求创建一个独立的线程,当线程处理完请求后,自动销毁,这是一种最简单的并发处理网络请求的方法。 + +网络编程里最简单的程序当数 echo 程序了,echo 程序的服务端会原封不动地将客户端的请求发送回客户端。例如,客户端发送 TCP 请求”Hello World”,那么服务端也会返回”Hello World”。 + +```java +final ServerSocketChannel ssc = + ServerSocketChannel.open().bind(new InetSocketAddress(8080)); +//处理请求 +try { + while (true) { + // 接收请求 + SocketChannel sc = ssc.accept(); + // 每个请求都创建一个线程 + new Thread(() -> { + try { + // 读 Socket + ByteBuffer rb = ByteBuffer.allocateDirect(1024); + sc.read(rb); + //模拟处理请求 + Thread.sleep(2000); + // 写 Socket + ByteBuffer wb = (ByteBuffer) rb.flip(); + sc.write(wb); + // 关闭 Socket + sc.close(); + } catch (Exception e) { + throw new UncheckedIOException(e); + } + }).start(); + } +} finally { + ssc.close(); +} +``` + +上面这个 echo 服务的实现方案是不具备可行性的。原因在于 Java 中的线程是一个重量级的对象,创建成本很高,一方面创建线程比较耗时,另一方面线程占用的内存也比较大。所以,为每个请求创建一个新的线程并不适合高并发场景。 + +Java 语言里,Java 线程是和操作系统线程一一对应的,这种做法本质上是将 Java 线程的调度权完全委托给操作系统,而操作系统在这方面非常成熟,所以这种做法的好处是稳定、可靠,但是也继承了操作系统线程的缺点:创建成本高。为了解决这个缺点,Java 并发包里提供了线程池等工具类。这个思路在很长一段时间里都是很稳妥的方案,但是这个方案并不是唯一的方案。 + +业界还有另外一种方案,叫做**轻量级线程**。这个方案在 Java 领域知名度并不高,但是在其他编程语言里却叫得很响,例如 Go 语言、Lua 语言里的协程,本质上就是一种轻量级的线程。轻量级的线程,创建的成本很低,基本上和创建一个普通对象的成本相似;并且创建的速度和内存占用相比操作系统线程至少有一个数量级的提升,所以基于轻量级线程实现 Thread-Per-Message 模式就完全没有问题了。 + +Java 语言目前也已经意识到轻量级线程的重要性了,OpenJDK 有个 Loom 项目,就是要解决 Java 语言的轻量级线程问题,在这个项目中,轻量级线程被叫做** Fiber**。 + +### 用 Fiber 实现 Thread-Per-Message 模式 + +Loom 项目在设计轻量级线程时,充分考量了当前 Java 线程的使用方式,采取的是尽量兼容的态度,所以使用上还是挺简单的。用 Fiber 实现 echo 服务的示例代码如下所示,对比 Thread 的实现,你会发现改动量非常小,只需要把 `new Thread(()->{…}).start()` 换成 `Fiber.schedule(()->{})` 就可以了。 + +```java +final ServerSocketChannel ssc = + ServerSocketChannel.open().bind(new InetSocketAddress(8080)); +//处理请求 +try { + while (true) { + // 接收请求 + final SocketChannel sc = ssc.accept(); + Fiber.schedule(() -> { + try { + // 读 Socket + ByteBuffer rb = ByteBuffer.allocateDirect(1024); + sc.read(rb); + //模拟处理请求 + LockSupport.parkNanos(2000 * 1000000); + // 写 Socket + ByteBuffer wb = + (ByteBuffer) rb.flip() + sc.write(wb); + // 关闭 Socket + sc.close(); + } catch (Exception e) { + throw new UncheckedIOException(e); + } + }); + }//while +} finally { + ssc.close(); +} +``` + +通过压测,可以发现协程方式相比与线程方式,会大大减少线程数。 + +## Worker Thread 模式:如何避免重复创建线程? + +### Worker Thread 模式及其实现 + +Worker Thread 模式可以类比现实世界里车间的工作模式:车间里的工人,有活儿了,大家一起干,没活儿了就聊聊天等着。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010734563.png) + +这个模式,在 Java 中的方案就是线程池。 + +下面的示例代码是用线程池实现的 echo 服务端。 + +```java +ExecutorService es = Executors.newFixedThreadPool(500); +final ServerSocketChannel ssc = + ServerSocketChannel.open().bind(new InetSocketAddress(8080)); +//处理请求 +try { + while (true) { + // 接收请求 + SocketChannel sc = ssc.accept(); + // 将请求处理任务提交给线程池 + es.execute(() -> { + try { + // 读 Socket + ByteBuffer rb = ByteBuffer.allocateDirect(1024); + sc.read(rb); + //模拟处理请求 + Thread.sleep(2000); + // 写 Socket + ByteBuffer wb = (ByteBuffer) rb.flip(); + sc.write(wb); + // 关闭 Socket + sc.close(); + } catch (Exception e) { + throw new UncheckedIOException(e); + } + }); + } +} finally { + ssc.close(); + es.shutdown(); +} +``` + +### 正确地创建线程池 + +Java 的线程池既能够避免无限制地**创建线程**导致 OOM,也能避免无限制地**接收任务**导致 OOM。只不过后者经常容易被我们忽略,例如在上面的实现中,就被我们忽略了。所以强烈建议你**用创建有界的队列来接收任务**。 + +当请求量大于有界队列的容量时,就需要合理地拒绝请求。如何合理地拒绝呢?这需要你结合具体的业务场景来制定,即便线程池默认的拒绝策略能够满足你的需求,也同样建议你**在创建线程池时,清晰地指明拒绝策略**。 + +同时,为了便于调试和诊断问题,我也强烈建议你**在实际工作中给线程赋予一个业务相关的名字**。 + +综合以上,创建线程池的示例: + +```java +ExecutorService es = new ThreadPoolExecutor(50, 500, 60L, TimeUnit.SECONDS, + //注意要创建有界队列 + new LinkedBlockingQueue(2000), + //建议根据业务需求实现 ThreadFactory + r -> { + return new Thread(r, "echo-" + r.hashCode()); + }, + //建议根据业务需求实现 RejectedExecutionHandler + new ThreadPoolExecutor.CallerRunsPolicy()); +``` + +### 避免线程死锁 + +使用线程池过程中,还要注意一种**线程死锁**的场景。如果提交到相同线程池的任务不是相互独立的,而是有依赖关系的,那么就有可能导致线程死锁。具体现象是**应用每运行一段时间偶尔就会处于无响应的状态,监控数据看上去一切都正常,但是实际上已经不能正常工作了**。 + +这个出问题的应用,相关的逻辑精简之后,如下图所示,该应用将一个大型的计算任务分成两个阶段,第一个阶段的任务会等待第二阶段的子任务完成。在这个应用里,每一个阶段都使用了线程池,而且两个阶段使用的还是同一个线程池。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010741505.png) + +我们可以用下面的示例代码来模拟该应用,如果你执行下面的这段代码,会发现它永远执行不到最后一行。执行过程中没有任何异常,但是应用已经停止响应了。 + +```java +//L1、L2 阶段共用的线程池 +ExecutorService es = Executors.newFixedThreadPool(2); +//L1 阶段的闭锁 +CountDownLatch l1 = new CountDownLatch(2); +for (int i = 0; i < 2; i++) { + System.out.println("L1"); + //执行 L1 阶段任务 + es.execute(() -> { + //L2 阶段的闭锁 + CountDownLatch l2 = new CountDownLatch(2); + //执行 L2 阶段子任务 + for (int j = 0; j < 2; j++) { + es.execute(() -> { + System.out.println("L2"); + l2.countDown(); + }); + } + //等待 L2 阶段任务执行完 + l2.await(); + l1.countDown(); + }); +} +//等着 L1 阶段任务执行完 +l1.await(); +System.out.println("end"); +``` + +当应用出现类似问题时,首选的诊断方法是查看线程栈。下图是上面示例代码停止响应后的线程栈,你会发现线程池中的两个线程全部都阻塞在 `l2.await();` 这行代码上了,也就是说,线程池里所有的线程都在等待 L2 阶段的任务执行完,那 L2 阶段的子任务什么时候能够执行完呢?永远都没那一天了,为什么呢?因为线程池里的线程都阻塞了,没有空闲的线程执行 L2 阶段的任务了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010743782.png) + +原因找到了,那如何解决就简单了,最简单粗暴的办法就是将线程池的最大线程数调大,如果能够确定任务的数量不是非常多的话,这个办法也是可行的,否则这个办法就行不通了。其实**这种问题通用的解决方案是为不同的任务创建不同的线程池**。对于上面的这个应用,L1 阶段的任务和 L2 阶段的任务如果各自都有自己的线程池,就不会出现这种问题了。 + +最后再次强调一下:**提交到相同线程池中的任务一定是相互独立的,否则就一定要慎重**。 + +## 两阶段终止模式:如何优雅地终止线程? + +### 如何理解两阶段终止模式 + +**两阶段终止模式**,顾名思义,就是将终止过程分成两个阶段:第一个阶段主要是线程 T1 向线程 T2 **发送终止指令**,而第二阶段则是线程 T2 **响应终止指令**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010920384.png) + +终止指令,其实包括两方面内容:**interrupt() 方法**和**线程终止的标志位**。 + +### 用两阶段终止模式终止监控操作 + +实际工作中,有些监控系统需要动态地采集一些数据,一般都是监控系统发送采集指令给被监控系统的监控代理,监控代理接收到指令之后,从监控目标收集数据,然后回传给监控系统,详细过程如下图所示。出于对性能的考虑(有些监控项对系统性能影响很大,所以不能一直持续监控),动态采集功能一般都会有终止操作。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010923997.png) + +下面的示例代码是**监控代理**简化之后的实现,start() 方法会启动一个新的线程 rptThread 来执行监控数据采集和回传的功能,stop() 方法需要优雅地终止线程 rptThread,那 stop() 相关功能该如何实现呢? + +```java +class Proxy { + boolean started = false; + //采集线程 + Thread rptThread; + //启动采集功能 + synchronized void start(){ + //不允许同时启动多个采集线程 + if (started) { + return; + } + started = true; + rptThread = new Thread(()->{ + while (true) { + //省略采集、回传实现 + report(); + //每隔两秒钟采集、回传一次数据 + try { + Thread.sleep(2000); + } catch (InterruptedException e) { + } + } + //执行到此处说明线程马上终止 + started = false; + }); + rptThread.start(); + } + //终止采集功能 + synchronized void stop(){ + //如何实现? + } +} +``` + +按照两阶段终止模式,我们首先需要做的就是将线程 rptThread 状态转换到 RUNNABLE,做法很简单,只需要在调用 `rptThread.interrupt()` 就可以了。线程 rptThread 的状态转换到 RUNNABLE 之后,如何优雅地终止呢?下面的示例代码中,我们选择的标志位是线程的中断状态:`Thread.currentThread().isInterrupted()` ,需要注意的是,我们在捕获 Thread.sleep() 的中断异常之后,通过 `Thread.currentThread().interrupt()` 重新设置了线程的中断状态,因为 JVM 的异常处理会清除线程的中断状态。 + +```java +class Proxy { + boolean started = false; + //采集线程 + Thread rptThread; + //启动采集功能 + synchronized void start(){ + //不允许同时启动多个采集线程 + if (started) { + return; + } + started = true; + rptThread = new Thread(()->{ + while (!Thread.currentThread().isInterrupted()){ + //省略采集、回传实现 + report(); + //每隔两秒钟采集、回传一次数据 + try { + Thread.sleep(2000); + } catch (InterruptedException e){ + //重新设置线程中断状态 + Thread.currentThread().interrupt(); + } + } + //执行到此处说明线程马上终止 + started = false; + }); + rptThread.start(); + } + //终止采集功能 + synchronized void stop(){ + rptThread.interrupt(); + } +} +``` + +上面的示例代码的确能够解决当前的问题,但是建议你在实际工作中谨慎使用。原因在于我们很可能在线程的 run() 方法中调用第三方类库提供的方法,而我们没有办法保证第三方类库正确处理了线程的中断异常,例如第三方类库在捕获到 Thread.sleep() 方法抛出的中断异常后,没有重新设置线程的中断状态,那么就会导致线程不能够正常终止。所以强烈建议你**设置自己的线程终止标志位**,例如在下面的代码中,使用 isTerminated 作为线程终止标志位,此时无论是否正确处理了线程的中断异常,都不会影响线程优雅地终止。 + +```java +class Proxy { + //线程终止标志位 + volatile boolean terminated = false; + boolean started = false; + //采集线程 + Thread rptThread; + //启动采集功能 + synchronized void start(){ + //不允许同时启动多个采集线程 + if (started) { + return; + } + started = true; + terminated = false; + rptThread = new Thread(()->{ + while (!terminated){ + //省略采集、回传实现 + report(); + //每隔两秒钟采集、回传一次数据 + try { + Thread.sleep(2000); + } catch (InterruptedException e){ + //重新设置线程中断状态 + Thread.currentThread().interrupt(); + } + } + //执行到此处说明线程马上终止 + started = false; + }); + rptThread.start(); + } + //终止采集功能 + synchronized void stop(){ + //设置中断标志位 + terminated = true; + //中断线程 rptThread + rptThread.interrupt(); + } +} +``` + +### 如何优雅地终止线程池 + +Java 领域用的最多的还是线程池,而不是手动地创建线程。那我们该如何优雅地终止线程池呢? + +线程池提供了两个方法:**shutdown() **和** shutdownNow()**。这两个方法有什么区别呢?要了解它们的区别,就先需要了解线程池的实现原理。 + +我们曾经讲过,Java 线程池是生产者-消费者模式的一种实现,提交给线程池的任务,首先是进入一个阻塞队列中,之后线程池中的线程从阻塞队列中取出任务执行。 + +shutdown() 方法是一种很保守的关闭线程池的方法。线程池执行 shutdown() 后,就会拒绝接收新的任务,但是会等待线程池中正在执行的任务和已经进入阻塞队列的任务都执行完之后才最终关闭线程池。 + +而 shutdownNow() 方法,相对就激进一些了,线程池执行 shutdownNow() 后,会拒绝接收新的任务,同时还会中断线程池中正在执行的任务,已经进入阻塞队列的任务也被剥夺了执行的机会,不过这些被剥夺执行机会的任务会作为 shutdownNow() 方法的返回值返回。因为 shutdownNow() 方法会中断正在执行的线程,所以提交到线程池的任务,如果需要优雅地结束,就需要正确地处理线程中断。 + +如果提交到线程池的任务不允许取消,那就不能使用 shutdownNow() 方法终止线程池。不过,如果提交到线程池的任务允许后续以补偿的方式重新执行,也是可以使用 shutdownNow() 方法终止线程池的。[《极客时间教程 - Java 并发编程实战》](time://mall?url=https%3A%2F%2Fh5.youzan.com%2Fv2%2Fgoods%2F2758xqdzr6uuw) 这本书第 7 章《取消与关闭》的“shutdownNow 的局限性”一节中,提到一种将已提交但尚未开始执行的任务以及已经取消的正在执行的任务保存起来,以便后续重新执行的方案。 + +其实分析完 shutdown() 和 shutdownNow() 方法你会发现,它们实质上使用的也是两阶段终止模式,只是终止指令的范围不同而已,前者只影响阻塞队列接收任务,后者范围扩大到线程池中所有的任务。 + +## 生产者-消费者模式:用流水线思想提高效率 + +### 生产者-消费者模式的优点 + +生产者-消费者模式的核心是一个**任务队列**,生产者线程生产任务,并将任务添加到任务队列中,而消费者线程从任务队列中获取任务并执行。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010930317.png) + +生产者和消费者没有任何依赖关系,它们彼此之间的通信只能通过任务队列,所以**生产者-消费者模式是一个不错的解耦方案**。 + +生产者-消费者模式**支持异步,并且能够平衡生产者和消费者的速度差异**。 + +### 支持批量执行以提升性能 + +监控系统动态采集的案例,其实最终回传的监控数据还是要存入数据库的(如下图)。但被监控系统往往有很多,如果每一条回传数据都直接 INSERT 到数据库,那么这个方案就是上面提到的第一种方案:每个线程 INSERT 一条数据。很显然,更好的方案是批量执行 SQL,那如何实现呢?这就要用到生产者-消费者模式了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010933833.png) + +利用生产者-消费者模式实现批量执行 SQL 非常简单:将原来直接 INSERT 数据到数据库的线程作为生产者线程,生产者线程只需将数据添加到任务队列,然后消费者线程负责将任务从任务队列中批量取出并批量执行。 + +在下面的示例代码中,我们创建了 5 个消费者线程负责批量执行 SQL,这 5 个消费者线程以 `while(true){}` 循环方式批量地获取任务并批量地执行。需要注意的是,从任务队列中获取批量任务的方法 pollTasks() 中,首先是以阻塞方式获取任务队列中的一条任务,而后则是以非阻塞的方式获取任务;之所以首先采用阻塞方式,是因为如果任务队列中没有任务,这样的方式能够避免无谓的循环。 + +```java +//任务队列 +BlockingQueue bq=new + LinkedBlockingQueue<>(2000); +//启动 5 个消费者线程 +//执行批量任务 +void start() { + ExecutorService es=executors + .newFixedThreadPool(5); + for (int i=0; i<5; i++) { + es.execute(()->{ + try { + while (true) { + //获取批量任务 + List ts=pollTasks(); + //执行批量任务 + execTasks(ts); + } + } catch (Exception e) { + e.printStackTrace(); + } + }); + } +} +//从任务队列中获取批量任务 +List pollTasks() + throws InterruptedException{ + List ts=new LinkedList<>(); + //阻塞式获取一条任务 + Task t = bq.take(); + while (t != null) { + ts.add(t); + //非阻塞式获取一条任务 + t = bq.poll(); + } + return ts; +} +//批量执行任务 +execTasks(List ts) { + //省略具体代码无数 +} +``` + +### 支持分阶段提交以提升性能 + +利用生产者-消费者模式还可以轻松地支持一种分阶段提交的应用场景。我们知道写文件如果同步刷盘性能会很慢,所以对于不是很重要的数据,我们往往采用异步刷盘的方式。 + +这个日志组件的异步刷盘操作本质上其实就是一种**分阶段提交**。下面我们具体看看用生产者-消费者模式如何实现。在下面的示例代码中,可以通过调用 `info()`和`error()` 方法写入日志,这两个方法都是创建了一个日志任务 LogMsg,并添加到阻塞队列中,调用 `info()`和`error()` 方法的线程是生产者;而真正将日志写入文件的是消费者线程,在 Logger 这个类中,我们只创建了 1 个消费者线程,在这个消费者线程中,会根据刷盘规则执行刷盘操作,逻辑很简单,这里就不赘述了。 + +```java +class Logger { + + //任务队列 + final BlockingQueue bq = new BlockingQueue<>(); + //flush 批量 + static final int batchSize = 500; + //只需要一个线程写日志 + ExecutorService es = Executors.newFixedThreadPool(1); + + //启动写日志线程 + void start() { + File file = File.createTempFile("foo", ".log"); + final FileWriter writer = new FileWriter(file); + this.es.execute(() -> { + try { + //未刷盘日志数量 + int curIdx = 0; + long preFT = System.currentTimeMillis(); + while (true) { + LogMsg log = bq.poll(5, TimeUnit.SECONDS); + //写日志 + if (log != null) { + writer.write(log.toString()); + ++curIdx; + } + //如果不存在未刷盘数据,则无需刷盘 + if (curIdx <= 0) { + continue; + } + //根据规则刷盘 + if (log != null && log.level == LEVEL.ERROR + || curIdx == batchSize + || System.currentTimeMillis() - preFT > 5000) { + writer.flush(); + curIdx = 0; + preFT = System.currentTimeMillis(); + } + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + try { + writer.flush(); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + }); + } + + //写 INFO 级别日志 + void info(String msg) { + bq.put(new LogMsg( + LEVEL.INFO, msg)); + } + + //写 ERROR 级别日志 + void error(String msg) { + bq.put(new LogMsg( + LEVEL.ERROR, msg)); + } + +} + +//日志级别 +enum LEVEL { + INFO, + ERROR +} + +class LogMsg { + LEVEL level; + String msg; + + //省略构造函数实现 + LogMsg(LEVEL lvl, String msg) { } + + //省略 toString() 实现 + String toString() { } +} +``` + +## 设计模式模块热点问题答疑 + +略 + +## 参考资料 + +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\272\214.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\272\214.md" new file mode 100644 index 0000000000..8223833140 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\344\272\214.md" @@ -0,0 +1,1540 @@ +--- +title: 《极客时间教程 - Java 并发编程实战》笔记二 +date: 2024-08-26 14:36:05 +categories: + - 笔记 + - Java +tags: + - Java + - 并发 +permalink: /pages/24d815a8/ +--- + +# 《极客时间教程 - Java 并发编程实战》笔记二 + +## Lock 和 Condition(上):隐藏在并发包中的管程 + +### 再造管程的理由 + +已有 synchronized,还支持 Lock 的原因是,需要一把锁支持: + +1. **能够响应中断**。synchronized 的问题是,持有锁 A 后,如果尝试获取锁 B 失败,那么线程就进入阻塞状态,一旦发生死锁,就没有任何机会来唤醒阻塞的线程。但如果阻塞状态的线程能够响应中断信号,也就是说当我们给阻塞的线程发送中断信号的时候,能够唤醒它,那它就有机会释放曾经持有的锁 A。这样就破坏了不可抢占条件了。 +2. **支持超时**。如果线程在一段时间之内没有获取到锁,不是进入阻塞状态,而是返回一个错误,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。 +3. **非阻塞地获取锁**。如果尝试获取锁失败,并不进入阻塞状态,而是直接返回,那这个线程也有机会释放曾经持有的锁。这样也能破坏不可抢占条件。 + +```java +// 支持中断的 API +void lockInterruptibly() + throws InterruptedException; +// 支持超时的 API +boolean tryLock(long time, TimeUnit unit) + throws InterruptedException; +// 支持非阻塞获取锁的 API +boolean tryLock(); +``` + +### 如何保证可见性 + +以 ReentrantLock 为例,内部持有一个 volatile 的成员变量 state,获取锁的时候,会读写 state 的值;解锁的时候,也会读写 state 的值。由 volatile 保证变量的可见性。 + +### 什么是可重入锁 + +**所谓可重入锁,指的是线程可以重复获取同一把锁**。 + +### 公平锁与非公平锁 + +ReentrantLock 中实现了公平锁和非公平锁。 + +```java +//无参构造函数:默认非公平锁 +public ReentrantLock() { + sync = new NonfairSync(); +} +//根据公平策略参数创建锁 +public ReentrantLock(boolean fair){F + sync = fair ? new FairSync() + : new NonfairSync(); +} +``` + +锁都对应着一个等待队列,如果一个线程没有获得锁,就会进入等待队列,当有线程释放锁的时候,就需要从等待队列中唤醒一个等待的线程。如果是公平锁,唤醒的策略就是谁等待的时间长,就唤醒谁,很公平;如果是非公平锁,则不提供这个公平保证,有可能等待时间短的线程反而先被唤醒。 + +### 用锁的最佳实践 + +1. 永远只在更新对象的成员变量时加锁 +2. 永远只在访问可变的成员变量时加锁 +3. 永远不在调用其他对象的方法时加锁 + +## Lock 和 Condition(下):Dubbo 如何用管程实现异步转同步? + +**Condition 实现了管程模型里面的条件变量**。 + +**如何利用两个条件变量快速实现阻塞队列呢?** + +一个阻塞队列,需要两个条件变量,一个是队列不空(空队列不允许出队),另一个是队列不满(队列已满不允许入队) + +```java +public class BlockedQueue{ + final Lock lock = new ReentrantLock(); + // 条件变量:队列不满 + final Condition notFull = lock.newCondition(); + // 条件变量:队列不空 + final Condition notEmpty = lock.newCondition(); + + // 入队 + void enq(T x) { + lock.lock(); + try { + while (队列已满){ + // 等待队列不满 + notFull.await(); + } + // 省略入队操作。.. + // 入队后,通知可出队 + notEmpty.signal(); + }finally { + lock.unlock(); + } + } + // 出队 + void deq(){ + lock.lock(); + try { + while (队列已空){ + // 等待队列不空 + notEmpty.await(); + } + // 省略出队操作。.. + // 出队后,通知可入队 + notFull.signal(); + }finally { + lock.unlock(); + } + } +} +``` + +Lock 和 Condition 实现的管程,**线程等待和通知需要调用 await()、signal()、signalAll()**,它们的语义和 wait()、notify()、notifyAll() 是相同的。 + +### 同步与异步 + +同步和异步的区别:**调用方是否需要等待结果,如果需要等待结果,就是同步;如果不需要等待结果,就是异步**。 + +### Dubbo 源码分析 + +RPC 调用,**在 TCP 协议层面,发送完 RPC 请求后,线程是不会等待 RPC 的响应结果的**。 + +Dubbo 调用关键代码: + +```java +public class DubboInvoker{ + Result doInvoke(Invocation inv){ + // 下面这行就是源码中 108 行 + // 为了便于展示,做了修改 + return currentClient + .request(inv, timeout) + .get(); + } +} +``` + +当 RPC 返回结果之前,阻塞调用线程,让调用线程等待;当 RPC 返回结果后,唤醒调用线程,让调用线程重新执行。 + +```java +// 创建锁与条件变量 +private final Lock lock = new ReentrantLock(); +private final Condition done = lock.newCondition(); + +// 调用方通过该方法等待结果 +Object get(int timeout){ + long start = System.nanoTime(); + lock.lock(); + try { + while (!isDone()) { + done.await(timeout); + long cur=System.nanoTime(); + if (isDone() || + cur-start > timeout){ + break; + } + } + } finally { + lock.unlock(); + } + if (!isDone()) { + throw new TimeoutException(); + } + return returnFromResponse(); +} +// RPC 结果是否已经返回 +boolean isDone() { + return response != null; +} +// RPC 结果返回时调用该方法 +private void doReceived(Response res) { + lock.lock(); + try { + response = res; + if (done != null) { + done.signal(); + } + } finally { + lock.unlock(); + } +} +``` + +## Semaphore:如何快速实现一个限流器? + +### 信号量模型 + +信号量模型还是很简单的,可以简单概括为:**一个计数器,一个等待队列,三个方法**。在信号量模型里,计数器和等待队列对外是透明的,所以只能通过信号量模型提供的三个方法来访问它们,这三个方法分别是:init()、down() 和 up()。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408280813940.png) + +```java +class Semaphore{ + // 计数器 + int count; + // 等待队列 + Queue queue; + // 初始化操作 + Semaphore(int c){ + this.count=c; + } + // + void down(){ + this.count--; + if(this.count<0){ + // 将当前线程插入等待队列 + // 阻塞当前线程 + } + } + void up(){ + this.count++; + if(this.count<=0) { + // 移除等待队列中的某个线程 T + // 唤醒线程 T + } + } +} +``` + +号量模型里面,down()、up() 这两个操作历史上最早称为 P 操作和 V 操作,所以信号量模型也被称为 PV 原语。在 Java SDK 并发包里,down() 和 up() 对应的则是 acquire() 和 release()。 + +### 如何使用信号量 + +就像我们用互斥锁一样,只需要在进入临界区之前执行一下 down() 操作,退出临界区之前执行一下 up() 操作就可以了。下面是 Java 代码的示例,acquire() 就是信号量里的 down() 操作,release() 就是信号量里的 up() 操作。 + +```java +static int count; +// 初始化信号量 +static final Semaphore s + = new Semaphore(1); +// 用信号量保证互斥 +static void addOne() { + s.acquire(); + try { + count+=1; + } finally { + s.release(); + } +} +``` + +### 快速实现一个限流器 + +**Semaphore 可以允许多个线程访问一个临界区**。 + +```java +class ObjPool { + final List pool; + // 用信号量实现限流器 + final Semaphore sem; + // 构造函数 + ObjPool(int size, T t){ + pool = new Vector(){}; + for(int i=0; i func) { + T t = null; + sem.acquire(); + try { + t = pool.remove(0); + return func.apply(t); + } finally { + pool.add(t); + sem.release(); + } + } +} +// 创建对象池 +ObjPool pool = + new ObjPool(10, 2); +// 通过对象池获取 t,之后执行 +pool.exec(t -> { + System.out.println(t); + return t.toString(); +}); +``` + +读写锁与互斥锁的一个重要区别就是**读写锁允许多个线程同时读共享变量**,而互斥锁是不允许的,这是读写锁在读多写少场景下性能优于互斥锁的关键。但**读写锁的写操作是互斥的**,当一个线程在写共享变量的时候,是不允许其他线程执行写操作和读操作。 + +### 快速实现一个缓存 + +Cache 这个工具类,我们提供了两个方法,一个是读缓存方法 get(),另一个是写缓存方法 put()。读缓存需要用到读锁,读锁的使用和前面我们介绍的 Lock 的使用是相同的,都是 try{}finally{}这个编程范式。写缓存则需要用到写锁,写锁的使用和读锁是类似的。这样看来,读写锁的使用还是非常简单的。 + +```java +class Cache { + final Map m = + new HashMap<>(); + final ReadWriteLock rwl = + new ReentrantReadWriteLock(); + // 读锁 + final Lock r = rwl.readLock(); + // 写锁 + final Lock w = rwl.writeLock(); + // 读缓存 + V get(K key) { + r.lock(); + try { return m.get(key); } + finally { r.unlock(); } + } + // 写缓存 + V put(K key, V value) { + w.lock(); + try { return m.put(key, v); } + finally { w.unlock(); } + } +} +``` + +### 实现缓存的按需加载 + +```java +class Cache { + final Map m = + new HashMap<>(); + final ReadWriteLock rwl = + new ReentrantReadWriteLock(); + final Lock r = rwl.readLock(); + final Lock w = rwl.writeLock(); + + V get(K key) { + V v = null; + //读缓存 + r.lock(); ① + try { + v = m.get(key); ② + } finally{ + r.unlock(); ③ + } + //缓存中存在,返回 + if(v != null) { ④ + return v; + } + //缓存中不存在,查询数据库 + w.lock(); ⑤ + try { + //再次验证 + //其他线程可能已经查询过数据库 + v = m.get(key); ⑥ + if(v == null){ ⑦ + //查询数据库 + v=省略代码无数 + m.put(key, v); + } + } finally{ + w.unlock(); + } + return v; + } +} +``` + +## ReadWriteLock:如何快速实现一个完备的缓存? + +### 读写锁的升级与降级 + +```java +//读缓存 +r.lock(); ① +try { + v = m.get(key); ② + if (v == null) { + w.lock(); + try { + //再次验证并更新缓存 + //省略详细代码 + } finally{ + w.unlock(); + } + } +} finally{ + r.unlock(); ③ +} +``` + +上面的代码,先是获取读锁,然后再升级为写锁,对此还有个专业的名字,叫**锁的升级**。可惜 ReadWriteLock 并不支持这种升级。 + +不过,虽然锁的升级是不允许的,但是锁的降级却是允许的。 + +```java +class CachedData { + Object data; + volatile boolean cacheValid; + final ReadWriteLock rwl = + new ReentrantReadWriteLock(); + // 读锁 + final Lock r = rwl.readLock(); + //写锁 + final Lock w = rwl.writeLock(); + + void processCachedData() { + // 获取读锁 + r.lock(); + if (!cacheValid) { + // 释放读锁,因为不允许读锁的升级 + r.unlock(); + // 获取写锁 + w.lock(); + try { + // 再次检查状态 + if (!cacheValid) { + data = ... + cacheValid = true; + } + // 释放写锁前,降级为读锁 + // 降级是可以的 + r.lock(); ① + } finally { + // 释放写锁 + w.unlock(); + } + } + // 此处仍然持有读锁 + try {use(data);} + finally {r.unlock();} + } +} +``` + +## StampedLock:有没有比读写锁更快的锁? + +### StampedLock 支持的三种锁模式 + +ReadWriteLock 支持两种模式:一种是读锁,一种是写锁。而 StampedLock 支持三种模式,分别是:**写锁**、**悲观读锁**和**乐观读**。其中,写锁、悲观读锁的语义和 ReadWriteLock 的写锁、读锁的语义非常类似,允许多个线程同时获取悲观读锁,但是只允许一个线程获取写锁,写锁和悲观读锁是互斥的。不同的是:StampedLock 里的写锁和悲观读锁加锁成功之后,都会返回一个 stamp;然后解锁的时候,需要传入这个 stamp。 + +```java +final StampedLock sl = + new StampedLock(); + +// 获取/释放悲观读锁示意代码 +long stamp = sl.readLock(); +try { + //省略业务相关代码 +} finally { + sl.unlockRead(stamp); +} + +// 获取/释放写锁示意代码 +long stamp = sl.writeLock(); +try { + //省略业务相关代码 +} finally { + sl.unlockWrite(stamp); +} +``` + +StampedLock 的性能之所以比 ReadWriteLock 还要好,其关键是 StampedLock 支持乐观读的方式。ReadWriteLock 支持多个线程同时读,但是当多个线程同时读的时候,所有的写操作会被阻塞;而 StampedLock 提供的乐观读,是允许一个线程获取写锁的,也就是说不是所有的写操作都被阻塞。 + +在 distanceFromOrigin() 这个方法中,首先通过调用 tryOptimisticRead() 获取了一个 stamp,这里的 tryOptimisticRead() 就是我们前面提到的乐观读。之后将共享变量 x 和 y 读入方法的局部变量中,不过需要注意的是,由于 tryOptimisticRead() 是无锁的,所以共享变量 x 和 y 读入方法局部变量时,x 和 y 有可能被其他线程修改了。因此最后读完之后,还需要再次验证一下是否存在写操作,这个验证操作是通过调用 validate(stamp) 来实现的。 + +```java +class Point { + private int x, y; + final StampedLock sl = + new StampedLock(); + //计算到原点的距离 + int distanceFromOrigin() { + // 乐观读 + long stamp = + sl.tryOptimisticRead(); + // 读入局部变量, + // 读的过程数据可能被修改 + int curX = x, curY = y; + //判断执行读操作期间, + //是否存在写操作,如果存在, + //则 sl.validate 返回 false + if (!sl.validate(stamp)){ + // 升级为悲观读锁 + stamp = sl.readLock(); + try { + curX = x; + curY = y; + } finally { + //释放悲观读锁 + sl.unlockRead(stamp); + } + } + return Math.sqrt( + curX * curX + curY * curY); + } +} +``` + +### 进一步理解乐观读 + +StampedLock 的乐观读和数据库的乐观锁有异曲同工之妙。 + +### StampedLock 使用注意事项 + +对于读多写少的场景 StampedLock 性能很好,简单的应用场景基本上可以替代 ReadWriteLock,但是** StampedLock 的功能仅仅是 ReadWriteLock 的子集**,在使用的时候,还是有几个地方需要注意一下。 + +StampedLock 在命名上并没有增加 Reentrant,想必你已经猜测到 StampedLock 应该是不可重入的。事实上,的确是这样的,**StampedLock 不支持重入**。这个是在使用中必须要特别注意的。 + +另外,StampedLock 的悲观读锁、写锁都不支持条件变量,这个也需要你注意。 + +还有一点需要特别注意,那就是:如果线程阻塞在 StampedLock 的 readLock() 或者 writeLock() 上时,此时调用该阻塞线程的 interrupt() 方法,会导致 CPU 飙升。 + +```java +final StampedLock lock + = new StampedLock(); +Thread T1 = new Thread(()->{ + // 获取写锁 + lock.writeLock(); + // 永远阻塞在此处,不释放写锁 + LockSupport.park(); +}); +T1.start(); +// 保证 T1 获取写锁 +Thread.sleep(100); +Thread T2 = new Thread(()-> + //阻塞在悲观读锁 + lock.readLock() +); +T2.start(); +// 保证 T2 阻塞在读锁 +Thread.sleep(100); +//中断线程 T2 +//会导致线程 T2 所在 CPU 飙升 +T2.interrupt(); +T2.join(); +``` + +所以,**使用 StampedLock 一定不要调用中断操作,如果需要支持中断功能,一定使用可中断的悲观读锁 readLockInterruptibly() 和写锁 writeLockInterruptibly()**。这个规则一定要记清楚。 + +## 总结 + +StampedLock 读模板: + +```java +final StampedLock sl = + new StampedLock(); + +// 乐观读 +long stamp = + sl.tryOptimisticRead(); +// 读入方法局部变量 +...... +// 校验 stamp +if (!sl.validate(stamp)){ + // 升级为悲观读锁 + stamp = sl.readLock(); + try { + // 读入方法局部变量 + ..... + } finally { + //释放悲观读锁 + sl.unlockRead(stamp); + } +} +//使用方法局部变量执行业务操作 +...... +``` + +StampedLock 写模板: + +```java +long stamp = sl.writeLock(); +try { + // 写共享变量 + ...... +} finally { + sl.unlockWrite(stamp); +} +``` + +## CountDownLatch 和 CyclicBarrier:如何让多线程步调一致? + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292030531.png) + +对账系统串行处理流程: + +```java +while(存在未对账订单){ + // 查询未对账订单 + pos = getPOrders(); + // 查询派送单 + dos = getDOrders(); + // 执行对账操作 + diff = check(pos, dos); + // 差异写入差异库 + save(diff); +} +``` + +### 利用并行优化对账系统 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292030390.png) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292030333.png) + +### 用 CountDownLatch 实现线程等待 + +```java +// 创建 2 个线程的线程池 +Executor executor = + Executors.newFixedThreadPool(2); +while(存在未对账订单){ + // 计数器初始化为 2 + CountDownLatch latch = + new CountDownLatch(2); + // 查询未对账订单 + executor.execute(()-> { + pos = getPOrders(); + latch.countDown(); + }); + // 查询派送单 + executor.execute(()-> { + dos = getDOrders(); + latch.countDown(); + }); + + // 等待两个查询操作结束 + latch.await(); + + // 执行对账操作 + diff = check(pos, dos); + // 差异写入差异库 + save(diff); +} +``` + +### 进一步优化性能 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292031238.png) + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292031656.png) + +### 用 CyclicBarrier 实现线程同步 + +CyclicBarrier 的计数器有自动重置的功能,当减到 0 的时候,会自动重置你设置的初始值。 + +```java +// 订单队列 +Vector

    pos; +// 派送单队列 +Vector dos; +// 执行回调的线程池 +Executor executor = + Executors.newFixedThreadPool(1); +final CyclicBarrier barrier = + new CyclicBarrier(2, ()->{ + executor.execute(()->check()); + }); + +void check(){ + P p = pos.remove(0); + D d = dos.remove(0); + // 执行对账操作 + diff = check(p, d); + // 差异写入差异库 + save(diff); +} + +void checkAll(){ + // 循环查询订单库 + Thread T1 = new Thread(()->{ + while(存在未对账订单){ + // 查询订单库 + pos.add(getPOrders()); + // 等待 + barrier.await(); + } + }); + T1.start(); + // 循环查询运单库 + Thread T2 = new Thread(()->{ + while(存在未对账订单){ + // 查询运单库 + dos.add(getDOrders()); + // 等待 + barrier.await(); + } + }); + T2.start(); +} +``` + +### 总结 + +**CountDownLatch 主要用来解决一个线程等待多个线程的场景**。 + +**CyclicBarrier 是一组线程之间互相等待**。 + +CountDownLatch 的计数器是不能循环利用的,也就是说一旦计数器减到 0,再有线程调用 await(),该线程会直接通过。但 **CyclicBarrier 的计数器是可以循环利用的**,而且具备自动重置的功能,一旦计数器减到 0 会自动重置到你设置的初始值。除此之外,CyclicBarrier 还可以设置回调函数,可以说是功能丰富。 + +## 并发容器:都有哪些“坑”需要我们填? + +### 同步容器及其注意事项 + +**组合操作需要注意竞态条件问题**,组合操作往往隐藏着竞态条件问题,即便每个操作都能保证原子性,也并不能保证组合操作的原子性。 + +在容器领域**一个容易被忽视的“坑”是用迭代器遍历容器**。因为遍历元素,进行操作,不能保证原子性。 + +基于 synchronized 这个同步关键字实现的容器被称为**同步容器**。Java 提供的同步容器还有 Vector、Stack 和 Hashtable。对这三个容器的遍历,同样要加锁保证互斥。 + +### 并发容器及其注意事项 + +Java 在 1.5 版本之前所谓的线程安全的容器,主要指的就是**同步容器**。不过同步容器有个最大的问题,那就是性能差,所有方法都用 synchronized 来保证互斥,串行度太高了。因此 Java 在 1.5 及之后版本提供了性能更高的容器,我们一般称为**并发容器**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292032669.png) + +#### (一)List + +List 里面只有一个实现类就是** CopyOnWriteArrayList**。CopyOnWrite,顾名思义就是写的时候会将共享变量新复制一份出来,这样做的好处是读操作完全无锁。 + +CopyOnWriteArrayList 内部维护了一个数组,成员变量 array 就指向这个内部数组,所有的读操作都是基于 array 进行的,如下图所示,迭代器 Iterator 遍历的就是 array 数组。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292032762.png) + +如果在遍历 array 的同时,还有一个写操作,例如增加元素,CopyOnWriteArrayList 是如何处理的呢? + +CopyOnWriteArrayList 会将 array 复制一份,然后在新复制处理的数组上执行增加元素的操作,执行完之后再将 array 指向这个新的数组。通过下图你可以看到,读写是可以并行的,遍历操作一直都是基于原 array 执行,而写操作则是基于新 array 进行。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292032731.png) + +使用 CopyOnWriteArrayList 需要注意的“坑”主要有两个方面。一个是应用场景,CopyOnWriteArrayList 仅适用于写操作非常少的场景,而且能够容忍读写的短暂不一致。例如上面的例子中,写入的新元素并不能立刻被遍历到。另一个需要注意的是,CopyOnWriteArrayList 迭代器是只读的,不支持增删改。因为迭代器遍历的仅仅是一个快照,而对快照进行增删改是没有意义的。 + +#### (二)Map + +Map 接口的两个实现是 ConcurrentHashMap 和 ConcurrentSkipListMap,它们从应用的角度来看,主要区别在于** ConcurrentHashMap 的 key 是无序的,而 ConcurrentSkipListMap 的 key 是有序的**。 + +使用 ConcurrentHashMap 和 ConcurrentSkipListMap 需要注意的地方是,它们的 key 和 value 都不能为空,否则会抛出`NullPointerException`这个运行时异常。下面这个表格总结了 Map 相关的实现类对于 key 和 value 的要求,你可以对比学习。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292033594.png) + +ConcurrentSkipListMap 里面的 SkipList 本身就是一种数据结构,中文一般都翻译为“跳表”。跳表插入、删除、查询操作平均的时间复杂度是 O(log n),理论上和并发线程数没有关系,所以在并发度非常高的情况下,若你对 ConcurrentHashMap 的性能还不满意,可以尝试一下 ConcurrentSkipListMap。 + +#### (三)Set + +Set 接口的两个实现是 CopyOnWriteArraySet 和 ConcurrentSkipListSet,使用场景可以参考前面讲述的 CopyOnWriteArrayList 和 ConcurrentSkipListMap,它们的原理都是一样的,这里就不再赘述了。 + +#### (四)Queue + +Java 并发包里面 Queue 这类并发容器是最复杂的,你可以从以下两个维度来分类。一个维度是**阻塞与非阻塞**,所谓阻塞指的是当队列已满时,入队操作阻塞;当队列已空时,出队操作阻塞。另一个维度是**单端与双端**,单端指的是只能队尾入队,队首出队;而双端指的是队首队尾皆可入队出队。Java 并发包里**阻塞队列都用 Blocking 关键字标识,单端队列使用 Queue 标识,双端队列使用 Deque 标识**。 + +## 原子类:无锁工具类的典范 + +无锁方案相对互斥锁方案,最大的好处就是**性能**。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。 相比之下,无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,既解决了问题,又没有带来新的问题,可谓绝佳方案。 + +### 无锁方案的实现原理 + +CPU 为了解决并发问题,提供了 CAS 指令(CAS,全称是 Compare And Swap,即“比较并交换”)。CAS 指令包含 3 个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C;并且只有当内存中地址 A 处的值等于 B 时,才能将内存中地址 A 处的值更新为新值 C。**作为一条 CPU 指令,CAS 指令本身是能够保证原子性的**。 + +使用 CAS 来解决并发问题,一般都会伴随着自旋,而所谓自旋,其实就是循环尝试。 + +CAS 存在 ABA 问题。 + +### 看 Java 如何实现原子化的 count += 1 + +AtomicLong 的 getAndIncrement() 方法会转调 unsafe.getAndAddLong() 方法。这里 this 和 valueOffset 两个参数可以唯一确定共享变量的内存地址。 + +```java +final long getAndIncrement() { + return unsafe.getAndAddLong( + this, valueOffset, 1L); +} +``` + +unsafe.getAndAddLong() 方法的源码如下: + +```java +public final long getAndAddLong(Object o, long offset, long delta){ + long v; + do { + // 读取内存中的值 + v = getLongVolatile(o, offset); + } while (!compareAndSwapLong(o, offset, v, v + delta)); + return v; +} +//原子性地将变量更新为 x +//条件是内存中的值等于 expected +//更新成功则返回 true +native boolean compareAndSwapLong( + Object o, long offset, + long expected, + long x); +``` + +### 原子类概览 + +Java SDK 并发包里提供的原子类内容很丰富,我们可以将它们分为五个类别:**原子化的基本数据类型、原子化的对象引用类型、原子化数组、原子化对象属性更新器**和**原子化的累加器**。这五个类别提供的方法基本上是相似的,并且每个类别都有若干原子类。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292033394.png) + +#### 1. 原子化的基本数据类型 + +相关实现有 AtomicBoolean、AtomicInteger 和 AtomicLong,提供的方法主要有以下这些: + +```java +getAndIncrement() //原子化 i++ +getAndDecrement() //原子化的 i-- +incrementAndGet() //原子化的++i +decrementAndGet() //原子化的--i +//当前值+=delta,返回+=前的值 +getAndAdd(delta) +//当前值+=delta,返回+=后的值 +addAndGet(delta) +//CAS 操作,返回是否成功 +compareAndSet(expect, update) +//以下四个方法 +//新值可以通过传入 func 函数来计算 +getAndUpdate(func) +updateAndGet(func) +getAndAccumulate(x,func) +accumulateAndGet(x,func) +``` + +#### 2. 原子化的对象引用类型 + +相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。AtomicReference 提供的方法和原子化的基本数据类型差不多。 + +AtomicStampedReference 和 AtomicMarkableReference 这两个原子类可以解决 ABA 问题。解决思路就是增加一个版本号,类似于乐观锁机制。 + +AtomicStampedReference 实现的 CAS 方法就增加了版本号参数,方法签名如下: + +```java +boolean compareAndSet( + V expectedReference, + V newReference, + int expectedStamp, + int newStamp) +``` + +AtomicMarkableReference 的实现机制则更简单,将版本号简化成了一个 Boolean 值,方法签名如下: + +```java +boolean compareAndSet( + V expectedReference, + V newReference, + boolean expectedMark, + boolean newMark) +``` + +#### 3. 原子化数组 + +相关实现有 AtomicIntegerArray、AtomicLongArray 和 AtomicReferenceArray,利用这些原子类,我们可以原子化地更新数组里面的每一个元素。这些类提供的方法和原子化的基本数据类型的区别仅仅是:每个方法多了一个数组的索引参数。 + +#### 4. 原子化对象属性更新器 + +相关实现有 AtomicIntegerFieldUpdater、AtomicLongFieldUpdater 和 AtomicReferenceFieldUpdater,利用它们可以原子化地更新对象的属性,这三个方法都是利用反射机制实现的,创建更新器的方法如下: + +```swift +public static +AtomicXXXFieldUpdater +newUpdater(Class tclass, + String fieldName) +``` + +需要注意的是,**对象属性必须是 volatile 类型的,只有这样才能保证可见性**;如果对象属性不是 volatile 类型的,newUpdater() 方法会抛出 IllegalArgumentException 这个运行时异常。 + +newUpdater() 的方法参数只有类的信息,没有对象的引用,而更新**对象**的属性,一定需要对象的引用,那这个参数是在哪里传入的呢?是在原子操作的方法参数中传入的。例如 compareAndSet() 这个原子操作,相比原子化的基本数据类型多了一个对象引用 obj。原子化对象属性更新器相关的方法,相比原子化的基本数据类型仅仅是多了对象引用参数。 + +```java +boolean compareAndSet( + T obj, + int expect, + int update) +``` + +#### 5. 原子化的累加器 + +DoubleAccumulator、DoubleAdder、LongAccumulator 和 LongAdder,这四个类仅仅用来执行累加操作,相比原子化的基本数据类型,速度更快,但是不支持 compareAndSet() 方法。如果你仅仅需要累加操作,使用原子化的累加器性能会更好。 + +### 总结 + +无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)。Java 提供的原子类大部分都实现了 compareAndSet() 方法。 + +Java 提供的原子类能够解决一些简单的原子性问题,但是所有原子类的方法都是针对一个共享变量的,如果需要解决多个变量的原子性问题,建议还是使用互斥锁方案。 + +## Executor 与线程池:如何创建正确的线程池? + +**线程是一个重量级的对象,应该避免频繁创建和销毁**。 + +### 线程池是一种生产者-消费者模式 + +业界线程池的设计,普遍采用的都是**生产者-消费者模式**。线程池的使用方是生产者,线程池本身是消费者。 + +### 如何使用 Java 中的线程池 + +ThreadPoolExecutor 的构造函数: + +```cpp +ThreadPoolExecutor( + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler handler) +``` + +参数说明: + +- **corePoolSize**:表示线程池保有的最小线程数。有些项目很闲,但是也不能把人都撤了,至少要留 corePoolSize 个人坚守阵地。 +- **maximumPoolSize**:表示线程池创建的最大线程数。当项目很忙时,就需要加人,但是也不能无限制地加,最多就加到 maximumPoolSize 个人。当项目闲下来时,就要撤人了,最多能撤到 corePoolSize 个人。 +- **keepAliveTime & unit**:上面提到项目根据忙闲来增减人员,那在编程世界里,如何定义忙和闲呢?很简单,一个线程如果在一段时间内,都没有执行任务,说明很闲,keepAliveTime 和 unit 就是用来定义这个“一段时间”的参数。也就是说,如果一个线程空闲了`keepAliveTime & unit`这么久,而且线程池的线程数大于 corePoolSize ,那么这个空闲的线程就要被回收了。 +- **workQueue**:工作队列,和上面示例代码的工作队列同义。 +- **threadFactory**:通过这个参数你可以自定义如何创建线程,例如你可以给线程指定一个有意义的名字。 +- handler:通过这个参数你可以自定义任务的拒绝策略。如果线程池中所有的线程都在忙碌,并且工作队列也满了(前提是工作队列是有界队列),那么此时提交任务,线程池就会拒绝接收。至于拒绝的策略,你可以通过 handler 这个参数来指定。ThreadPoolExecutor 已经提供了以下 4 种策略。 + - CallerRunsPolicy:提交任务的线程自己去执行该任务。 + - AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。 + - DiscardPolicy:直接丢弃任务,没有任何异常抛出。 + - DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。 + +Java 在 1.6 版本还增加了 allowCoreThreadTimeOut(boolean value) 方法,它可以让所有线程都支持超时,这意味着如果项目很闲,就会将项目组的成员都撤走。 + +### 使用线程池要注意些什么 + +不建议使用 Executors 的最重要的原因是:Executors 提供的很多方法默认使用的都是无界的 LinkedBlockingQueue,高负载情境下,无界队列很容易导致 OOM,而 OOM 会导致所有请求都无法处理,这是致命问题。所以**强烈建议使用有界队列**。 + +使用有界队列,当任务过多时,线程池会触发执行拒绝策略,线程池默认的拒绝策略会 throw RejectedExecutionException 这是个运行时异常,对于运行时异常编译器并不强制 catch 它,所以开发人员很容易忽略。因此**默认拒绝策略要慎重使用**。如果线程池处理的任务非常重要,建议自定义自己的拒绝策略;并且在实际工作中,自定义的拒绝策略往往和降级策略配合使用。 + +使用线程池,还要注意异常处理的问题,例如通过 ThreadPoolExecutor 对象的 execute() 方法提交任务时,如果任务在执行的过程中出现运行时异常,会导致执行任务的线程终止;不过,最致命的是任务虽然异常了,但是你却获取不到任何通知,这会让你误以为任务都执行得很正常。虽然线程池提供了很多用于异常处理的方法,但是最稳妥和简单的方案还是捕获所有异常并按需处理,你可以参考下面的示例代码。 + +```php +try { + //业务逻辑 +} catch (RuntimeException x) { + //按需处理 +} catch (Throwable x) { + //按需处理 +} +``` + +## Future:如何用多线程实现最优的“烧水泡茶”程序? + +### 如何获取任务执行结果 + +Java 通过 ThreadPoolExecutor 提供的 3 个 submit() 方法和 1 个 FutureTask 工具类来支持获得任务执行结果的需求。 + +```java +// 提交 Runnable 任务 +Future + submit(Runnable task); +// 提交 Callable 任务 + Future + submit(Callable task); +// 提交 Runnable 任务及结果引用 + Future + submit(Runnable task, T result); +``` + +Future 接口有 5 个方法,我都列在下面了,它们分别是**取消任务的方法 cancel()、判断任务是否已取消的方法 isCancelled()、判断任务是否已结束的方法 isDone() **以及** 2 个获得任务执行结果的 get() 和 get(timeout, unit)**,其中最后一个 get(timeout, unit) 支持超时机制。 + +```java +// 取消任务 +boolean cancel( + boolean mayInterruptIfRunning); +// 判断任务是否已取消 +boolean isCancelled(); +// 判断任务是否已结束 +boolean isDone(); +// 获得任务执行结果 +get(); +// 获得任务执行结果,支持超时 +get(long timeout, TimeUnit unit); +``` + +FutureTask 实现了 Runnable 和 Future 接口,由于实现了 Runnable 接口,所以可以将 FutureTask 对象作为任务提交给 ThreadPoolExecutor 去执行,也可以直接被 Thread 执行;又因为实现了 Future 接口,所以也能用来获得任务的执行结果。 + +```java +// 创建 FutureTask +FutureTask futureTask + = new FutureTask<>(()-> 1+2); +// 创建线程池 +ExecutorService es = + Executors.newCachedThreadPool(); +// 提交 FutureTask +es.submit(futureTask); +// 获取计算结果 +Integer result = futureTask.get(); +``` + +FutureTask 对象直接被 Thread 执行的示例代码如下所示。 + +```java +// 创建 FutureTask +FutureTask futureTask + = new FutureTask<>(()-> 1+2); +// 创建并启动线程 +Thread T1 = new Thread(futureTask); +T1.start(); +// 获取计算结果 +Integer result = futureTask.get(); +``` + +### 实现最优的“烧水泡茶”程序 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292034424.png) + +烧水泡茶最优分工方案 + +首先,我们创建了两个 FutureTask——ft1 和 ft2,ft1 完成洗水壶、烧开水、泡茶的任务,ft2 完成洗茶壶、洗茶杯、拿茶叶的任务;这里需要注意的是 ft1 这个任务在执行泡茶任务前,需要等待 ft2 把茶叶拿来,所以 ft1 内部需要引用 ft2,并在执行泡茶之前,调用 ft2 的 get() 方法实现等待。 + +```java +// 创建任务 T2 的 FutureTask +FutureTask ft2 + = new FutureTask<>(new T2Task()); +// 创建任务 T1 的 FutureTask +FutureTask ft1 + = new FutureTask<>(new T1Task(ft2)); +// 线程 T1 执行任务 ft1 +Thread T1 = new Thread(ft1); +T1.start(); +// 线程 T2 执行任务 ft2 +Thread T2 = new Thread(ft2); +T2.start(); +// 等待线程 T1 执行结果 +System.out.println(ft1.get()); + +// T1Task 需要执行的任务: +// 洗水壶、烧开水、泡茶 +class T1Task implements Callable{ + FutureTask ft2; + // T1 任务需要 T2 任务的 FutureTask + T1Task(FutureTask ft2){ + this.ft2 = ft2; + } + @Override + String call() throws Exception { + System.out.println("T1: 洗水壶。.."); + TimeUnit.SECONDS.sleep(1); + + System.out.println("T1: 烧开水。.."); + TimeUnit.SECONDS.sleep(15); + // 获取 T2 线程的茶叶 + String tf = ft2.get(); + System.out.println("T1: 拿到茶叶:"+tf); + + System.out.println("T1: 泡茶。.."); + return "上茶:" + tf; + } +} +// T2Task 需要执行的任务: +// 洗茶壶、洗茶杯、拿茶叶 +class T2Task implements Callable { + @Override + String call() throws Exception { + System.out.println("T2: 洗茶壶。.."); + TimeUnit.SECONDS.sleep(1); + + System.out.println("T2: 洗茶杯。.."); + TimeUnit.SECONDS.sleep(2); + + System.out.println("T2: 拿茶叶。.."); + TimeUnit.SECONDS.sleep(1); + return "龙井"; + } +} +// 一次执行结果: +T1: 洗水壶。.. +T2: 洗茶壶。.. +T1: 烧开水。.. +T2: 洗茶杯。.. +T2: 拿茶叶。.. +T1: 拿到茶叶:龙井 +T1: 泡茶。.. +上茶:龙井 +``` + +## CompletableFuture:异步编程没那么难 + +**异步化**,是并行方案得以实施的基础,更深入地讲其实就是:**利用多线程优化性能这个核心方案得以实施的基础**。 + +### CompletableFuture 的核心优势 + +```java +// 任务 1:洗水壶 -> 烧开水 +CompletableFuture f1 = + CompletableFuture.runAsync(()->{ + System.out.println("T1: 洗水壶。.."); + sleep(1, TimeUnit.SECONDS); + + System.out.println("T1: 烧开水。.."); + sleep(15, TimeUnit.SECONDS); +}); +// 任务 2:洗茶壶 -> 洗茶杯 -> 拿茶叶 +CompletableFuture f2 = + CompletableFuture.supplyAsync(()->{ + System.out.println("T2: 洗茶壶。.."); + sleep(1, TimeUnit.SECONDS); + + System.out.println("T2: 洗茶杯。.."); + sleep(2, TimeUnit.SECONDS); + + System.out.println("T2: 拿茶叶。.."); + sleep(1, TimeUnit.SECONDS); + return " 龙井 "; +}); +// 任务 3:任务 1 和任务 2 完成后执行:泡茶 +CompletableFuture f3 = + f1.thenCombine(f2, (__, tf)->{ + System.out.println("T1: 拿到茶叶:" + tf); + System.out.println("T1: 泡茶。.."); + return " 上茶:" + tf; + }); +// 等待任务 3 执行结果 +System.out.println(f3.join()); + +void sleep(int t, TimeUnit u) { + try { + u.sleep(t); + }catch(InterruptedException e){} +} +// 一次执行结果: +T1: 洗水壶。.. +T2: 洗茶壶。.. +T1: 烧开水。.. +T2: 洗茶杯。.. +T2: 拿茶叶。.. +T1: 拿到茶叶:龙井 +T1: 泡茶。.. +上茶:龙井 +``` + +### 创建 CompletableFuture 对象 + +默认情况下 CompletableFuture 会使用公共的 ForkJoinPool 线程池,这个线程池默认创建的线程数是 CPU 的核数(也可以通过 JVM option:-Djava.util.concurrent.ForkJoinPool.common.parallelism 来设置 ForkJoinPool 线程池的线程数)。如果所有 CompletableFuture 共享一个线程池,那么一旦有任务执行一些很慢的 I/O 操作,就会导致线程池中所有线程都阻塞在 I/O 操作上,从而造成线程饥饿,进而影响整个系统的性能。所以,强烈建议你要**根据不同的业务类型创建不同的线程池,以避免互相干扰**。 + +```java +//使用默认线程池 +static CompletableFuture + runAsync(Runnable runnable) +static CompletableFuture + supplyAsync(Supplier supplier) +//可以指定线程池 +static CompletableFuture + runAsync(Runnable runnable, Executor executor) +static CompletableFuture + supplyAsync(Supplier supplier, Executor executor) +``` + +创建完 CompletableFuture 对象之后,会自动地异步执行 runnable.run() 方法或者 supplier.get() 方法。 + +### 如何理解 CompletionStage 接口 + +CompletionStage 接口可以清晰地描述任务之间的这种时序关系,例如前面提到的 `f3 = f1.thenCombine(f2, ()->{})` 描述的就是一种汇聚关系。 + +#### 1. 描述串行关系 + +CompletionStage 接口里面描述串行关系,主要是 thenApply、thenAccept、thenRun 和 thenCompose 这四个系列的接口。 + +thenApply 系列函数里参数 fn 的类型是接口 Function,这个接口里与 CompletionStage 相关的方法是 `R apply(T t)`,这个方法既能接收参数也支持返回值,所以 thenApply 系列方法返回的是`CompletionStage`。 + +而 thenAccept 系列方法里参数 consumer 的类型是接口`Consumer`,这个接口里与 CompletionStage 相关的方法是 `void accept(T t)`,这个方法虽然支持参数,但却不支持回值,所以 thenAccept 系列方法返回的是`CompletionStage`。 + +thenRun 系列方法里 action 的参数是 Runnable,所以 action 既不能接收参数也不支持返回值,所以 thenRun 系列方法返回的也是`CompletionStage`。 + +这些方法里面 Async 代表的是异步执行 fn、consumer 或者 action。其中,需要你注意的是 thenCompose 系列方法,这个系列的方法会新创建出一个子流程,最终结果和 thenApply 系列是相同的。 + +```java +CompletionStage thenApply(fn); +CompletionStage thenApplyAsync(fn); +CompletionStage thenAccept(consumer); +CompletionStage thenAcceptAsync(consumer); +CompletionStage thenRun(action); +CompletionStage thenRunAsync(action); +CompletionStage thenCompose(fn); +CompletionStage thenComposeAsync(fn); +``` + +#### 2. 描述 AND 汇聚关系 + +CompletionStage 接口里面描述 AND 汇聚关系,主要是 thenCombine、thenAcceptBoth 和 runAfterBoth 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。它们的使用你可以参考上面烧水泡茶的实现程序,这里就不赘述了。 + +```java +CompletionStage thenCombine(other, fn); +CompletionStage thenCombineAsync(other, fn); +CompletionStage thenAcceptBoth(other, consumer); +CompletionStage thenAcceptBothAsync(other, consumer); +CompletionStage runAfterBoth(other, action); +CompletionStage runAfterBothAsync(other, action); +``` + +#### 3. 描述 OR 汇聚关系 + +CompletionStage 接口里面描述 OR 汇聚关系,主要是 applyToEither、acceptEither 和 runAfterEither 系列的接口,这些接口的区别也是源自 fn、consumer、action 这三个核心参数不同。 + +```java +CompletionStage applyToEither(other, fn); +CompletionStage applyToEitherAsync(other, fn); +CompletionStage acceptEither(other, consumer); +CompletionStage acceptEitherAsync(other, consumer); +CompletionStage runAfterEither(other, action); +CompletionStage runAfterEitherAsync(other, action); +``` + +## CompletionService:如何批量执行异步任务? + +用三个线程异步执行询价,通过三次调用 Future 的 get() 方法获取询价结果,之后将询价结果保存在数据库中。 + +```java +// 创建线程池 +ExecutorService executor = + Executors.newFixedThreadPool(3); +// 异步向电商 S1 询价 +Future f1 = + executor.submit( + ()->getPriceByS1()); +// 异步向电商 S2 询价 +Future f2 = + executor.submit( + ()->getPriceByS2()); +// 异步向电商 S3 询价 +Future f3 = + executor.submit( + ()->getPriceByS3()); + +// 获取电商 S1 报价并保存 +r=f1.get(); +executor.execute(()->save(r)); + +// 获取电商 S2 报价并保存 +r=f2.get(); +executor.execute(()->save(r)); + +// 获取电商 S3 报价并保存 +r=f3.get(); +executor.execute(()->save(r)); +``` + +如果获取电商 S1 报价的耗时很长,那么即便获取电商 S2 报价的耗时很短,也无法让保存 S2 报价的操作先执行,因为这个主线程都阻塞在了 `f1.get()` 操作上。这点小瑕疵你该如何解决呢? + +估计你已经想到了,增加一个阻塞队列,获取到 S1、S2、S3 的报价都进入阻塞队列,然后在主线程中消费阻塞队列,这样就能保证先获取到的报价先保存到数据库了。下面的示例代码展示了如何利用阻塞队列实现先获取到的报价先保存到数据库。 + +```java +// 创建阻塞队列 +BlockingQueue bq = + new LinkedBlockingQueue<>(); +//电商 S1 报价异步进入阻塞队列 +executor.execute(()-> + bq.put(f1.get())); +//电商 S2 报价异步进入阻塞队列 +executor.execute(()-> + bq.put(f2.get())); +//电商 S3 报价异步进入阻塞队列 +executor.execute(()-> + bq.put(f3.get())); +//异步保存所有报价 +for (int i=0; i<3; i++) { + Integer r = bq.take(); + executor.execute(()->save(r)); +} +``` + +### 利用 CompletionService 实现询价系统 + +**如何创建 CompletionService 呢?** + +CompletionService 接口的实现类是 ExecutorCompletionService,这个实现类的构造方法有两个,分别是: + +1. `ExecutorCompletionService(Executor executor)`; +2. `ExecutorCompletionService(Executor executor, BlockingQueue> completionQueue)`。 + +这两个构造方法都需要传入一个线程池,如果不指定 completionQueue,那么默认会使用无界的 LinkedBlockingQueue。任务执行结果的 Future 对象就是加入到 completionQueue 中。 + +下面的示例代码完整地展示了如何利用 CompletionService 来实现高性能的询价系统。 + +```java +// 创建线程池 +ExecutorService executor = + Executors.newFixedThreadPool(3); +// 创建 CompletionService +CompletionService cs = new + ExecutorCompletionService<>(executor); +// 异步向电商 S1 询价 +cs.submit(()->getPriceByS1()); +// 异步向电商 S2 询价 +cs.submit(()->getPriceByS2()); +// 异步向电商 S3 询价 +cs.submit(()->getPriceByS3()); +// 将询价结果异步保存到数据库 +for (int i=0; i<3; i++) { + Integer r = cs.take().get(); + executor.execute(()->save(r)); +} +``` + +### CompletionService 接口说明 + +CompletionService 接口提供的方法有 5 个,这 5 个方法的方法签名如下所示。 + +```java +Future submit(Callable task); +Future submit(Runnable task, V result); +Future take() + throws InterruptedException; +Future poll(); +Future poll(long timeout, TimeUnit unit) + throws InterruptedException; +``` + +### 利用 CompletionService 实现 Dubbo 中的 Forking Cluster + +Dubbo 中有一种叫做** Forking 的集群模式**,这种集群模式下,支持**并行地调用多个查询服务,只要有一个成功返回结果,整个服务就可以返回了**。例如你需要提供一个地址转坐标的服务,为了保证该服务的高可用和性能,你可以并行地调用 3 个地图服务商的 API,然后只要有 1 个正确返回了结果 r,那么地址转坐标这个服务就可以直接返回 r 了。这种集群模式可以容忍 2 个地图服务商服务异常,但缺点是消耗的资源偏多。 + +```java +geocoder(addr) { + //并行执行以下 3 个查询服务, + r1=geocoderByS1(addr); + r2=geocoderByS2(addr); + r3=geocoderByS3(addr); + //只要 r1,r2,r3 有一个返回 + //则返回 + return r1|r2|r3; +} +``` + +利用 CompletionService 可以快速实现 Forking 这种集群模式,比如下面的示例代码就展示了具体是如何实现的。首先我们创建了一个线程池 executor 、一个 CompletionService 对象 cs 和一个`Future`类型的列表 futures,每次通过调用 CompletionService 的 submit() 方法提交一个异步任务,会返回一个 Future 对象,我们把这些 Future 对象保存在列表 futures 中。通过调用 `cs.take().get()`,我们能够拿到最快返回的任务执行结果,只要我们拿到一个正确返回的结果,就可以取消所有任务并且返回最终结果了。 + +```java +// 创建线程池 +ExecutorService executor = + Executors.newFixedThreadPool(3); +// 创建 CompletionService +CompletionService cs = + new ExecutorCompletionService<>(executor); +// 用于保存 Future 对象 +List> futures = + new ArrayList<>(3); +//提交异步任务,并保存 future 到 futures +futures.add( + cs.submit(()->geocoderByS1())); +futures.add( + cs.submit(()->geocoderByS2())); +futures.add( + cs.submit(()->geocoderByS3())); +// 获取最快返回的任务执行结果 +Integer r = 0; +try { + // 只要有一个成功返回,则 break + for (int i = 0; i < 3; ++i) { + r = cs.take().get(); + //简单地通过判空来检查是否成功返回 + if (r != null) { + break; + } + } +} finally { + //取消所有任务 + for(Future f : futures) + f.cancel(true); +} +// 返回结果 +return r; +``` + +### 总结 + +当需要批量提交异步任务的时候建议你使用 CompletionService。CompletionService 将线程池 Executor 和阻塞队列 BlockingQueue 的功能融合在了一起,能够让批量异步任务的管理更简单。除此之外,CompletionService 能够让异步任务的执行结果有序化,先执行完的先进入阻塞队列,利用这个特性,你可以轻松实现后续处理的有序性,避免无谓的等待,同时还可以快速实现诸如 Forking Cluster 这样的需求。 + +CompletionService 的实现类 ExecutorCompletionService,需要你自己创建线程池,虽看上去有些啰嗦,但好处是你可以让多个 ExecutorCompletionService 的线程池隔离,这种隔离性能避免几个特别耗时的任务拖垮整个应用的风险。 + +## Fork_Join:单机版的 MapReduce + +**对于简单的并行任务,你可以通过“线程池 +Future”的方案来解决;如果任务之间有聚合关系,无论是 AND 聚合还是 OR 聚合,都可以通过 CompletableFuture 来解决;而批量的并行任务,则可以通过 CompletionService 来解决。** + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292001191.png) + +除了简单并行、聚合、批量并行这三种任务模型,还有一种“分治”的任务模型。 + +**分治**,顾名思义,即分而治之,是一种解决复杂问题的思维方法和模式;具体来讲,指的是**把一个复杂的问题分解成多个相似的子问题,然后再把子问题分解成更小的子问题,直到子问题简单到可以直接求解**。理论上来讲,解决每一个问题都对应着一个任务,所以对于问题的分治,实际上就是对于任务的分治。 + +### 分治任务模型 + +这里你需要先深入了解一下分治任务模型,分治任务模型可分为两个阶段:一个阶段是**任务分解**,也就是将任务迭代地分解为子任务,直至子任务可以直接计算出结果;另一个阶段是**结果合并**,即逐层合并子任务的执行结果,直至获得最终结果。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292012846.png) + +简版分治任务模型图 + +在这个分治任务模型里,任务和分解后的子任务具有相似性,这种相似性往往体现在任务和子任务的算法是相同的,但是计算的数据规模是不同的。具备这种相似性的问题,我们往往都采用递归算法。 + +### Fork/Join 的使用 + +Fork/Join 是一个并行计算的框架,主要就是用来支持分治任务模型的,这个计算框架里的** Fork 对应的是分治任务模型里的任务分解,Join 对应的是结果合并**。Fork/Join 计算框架主要包含两部分,一部分是**分治任务的线程池 ForkJoinPool**,另一部分是**分治任务 ForkJoinTask**。 + +ForkJoinTask 是一个抽象类,它的方法有很多,最核心的是 fork() 方法和 join() 方法,其中 fork() 方法会异步地执行一个子任务,而 join() 方法则会阻塞当前线程来等待子任务的执行结果。ForkJoinTask 有两个子类——RecursiveAction 和 RecursiveTask,通过名字你就应该能知道,它们都是用递归的方式来处理分治任务的。这两个子类都定义了抽象方法 compute(),不过区别是 RecursiveAction 定义的 compute() 没有返回值,而 RecursiveTask 定义的 compute() 方法是有返回值的。这两个子类也是抽象类,在使用的时候,需要你定义子类去扩展。 + +接下来我们就来实现一下,看看如何用 Fork/Join 这个并行计算框架计算斐波那契数列(下面的代码源自 Java 官方示例)。首先我们需要创建一个分治任务线程池以及计算斐波那契数列的分治任务,之后通过调用分治任务线程池的 invoke() 方法来启动分治任务。由于计算斐波那契数列需要有返回值,所以 Fibonacci 继承自 RecursiveTask。分治任务 Fibonacci 需要实现 compute() 方法,这个方法里面的逻辑和普通计算斐波那契数列非常类似,区别之处在于计算 `Fibonacci(n - 1)` 使用了异步子任务,这是通过 `f1.fork()` 这条语句实现的。 + +```java +static void main(String[] args){ + //创建分治任务线程池 + ForkJoinPool fjp = + new ForkJoinPool(4); + //创建分治任务 + Fibonacci fib = + new Fibonacci(30); + //启动分治任务 + Integer result = + fjp.invoke(fib); + //输出结果 + System.out.println(result); +} +//递归任务 +static class Fibonacci extends + RecursiveTask{ + final int n; + Fibonacci(int n){this.n = n;} + protected Integer compute(){ + if (n <= 1) + return n; + Fibonacci f1 = + new Fibonacci(n - 1); + //创建子任务 + f1.fork(); + Fibonacci f2 = + new Fibonacci(n - 2); + //等待子任务结果,并合并结果 + return f2.compute() + f1.join(); + } +} +``` + +## ForkJoinPool 工作原理 + +ForkJoinPool 本质上也是一个生产者-消费者的实现,但是更加智能,ThreadPoolExecutor 内部只有一个任务队列,而 ForkJoinPool 内部有多个任务队列,当我们通过 ForkJoinPool 的 invoke() 或者 submit() 方法提交任务时,ForkJoinPool 根据一定的路由规则把任务提交到一个任务队列中,如果任务在执行过程中会创建出子任务,那么子任务会提交到工作线程对应的任务队列中。 + +如果工作线程对应的任务队列空了,是不是就没活儿干了呢?不是的,ForkJoinPool 支持一种叫做“**任务窃取**”的机制,如果工作线程空闲了,那它可以“窃取”其他工作任务队列里的任务,例如下图中,线程 T2 对应的任务队列已经空了,它可以“窃取”线程 T1 对应的任务队列的任务。如此一来,所有的工作线程都不会闲下来了。 + +ForkJoinPool 中的任务队列采用的是双端队列,工作线程正常获取任务和“窃取任务”分别是从任务队列不同的端消费,这样能避免很多不必要的数据竞争。我们这里介绍的仅仅是简化后的原理,ForkJoinPool 的实现远比我们这里介绍的复杂。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408292016364.png) + +### 模拟 MapReduce 统计单词数量 + +学习 MapReduce 有一个入门程序,统计一个文件里面每个单词的数量,下面我们来看看如何用 Fork/Join 并行计算框架来实现。 + +我们可以先用二分法递归地将一个文件拆分成更小的文件,直到文件里只有一行数据,然后统计这一行数据里单词的数量,最后再逐级汇总结果,你可以对照前面的简版分治任务模型图来理解这个过程。 + +思路有了,我们马上来实现。下面的示例程序用一个字符串数组 `String[] fc` 来模拟文件内容,fc 里面的元素与文件里面的行数据一一对应。关键的代码在 `compute()` 这个方法里面,这是一个递归方法,前半部分数据 fork 一个递归任务去处理(关键代码 mr1.fork()),后半部分数据则在当前任务中递归处理(mr2.compute())。 + +```java +static void main(String[] args){ + String[] fc = {"hello world", + "hello me", + "hello fork", + "hello join", + "fork join in world"}; + //创建 ForkJoin 线程池 + ForkJoinPool fjp = + new ForkJoinPool(3); + //创建任务 + MR mr = new MR( + fc, 0, fc.length); + //启动任务 + Map result = + fjp.invoke(mr); + //输出结果 + result.forEach((k, v)-> + System.out.println(k+":"+v)); +} +//MR 模拟类 +static class MR extends + RecursiveTask> { + private String[] fc; + private int start, end; + //构造函数 + MR(String[] fc, int fr, int to){ + this.fc = fc; + this.start = fr; + this.end = to; + } + @Override protected + Map compute(){ + if (end - start == 1) { + return calc(fc[start]); + } else { + int mid = (start+end)/2; + MR mr1 = new MR( + fc, start, mid); + mr1.fork(); + MR mr2 = new MR( + fc, mid, end); + //计算子任务,并返回合并的结果 + return merge(mr2.compute(), + mr1.join()); + } + } + //合并结果 + private Map merge( + Map r1, + Map r2) { + Map result = + new HashMap<>(); + result.putAll(r1); + //合并结果 + r2.forEach((k, v) -> { + Long c = result.get(k); + if (c != null) + result.put(k, c+v); + else + result.put(k, v); + }); + return result; + } + //统计单词数量 + private Map + calc(String line) { + Map result = + new HashMap<>(); + //分割单词 + String [] words = + line.split("\\s+"); + //统计单词数量 + for (String w : words) { + Long v = result.get(w); + if (v != null) + result.put(w, v+1); + else + result.put(w, 1L); + } + return result; + } +} +``` + +### 总结 + +Fork/Join 并行计算框架主要解决的是分治任务。分治的核心思想是“分而治之”:将一个大的任务拆分成小的子任务去解决,然后再把子任务的结果聚合起来从而得到最终结果。这个过程非常类似于大数据处理中的 MapReduce,所以你可以把 Fork/Join 看作单机版的 MapReduce。 + +Fork/Join 并行计算框架的核心组件是 ForkJoinPool。ForkJoinPool 支持任务窃取机制,能够让所有线程的工作量基本均衡,不会出现有的线程很忙,而有的线程很闲的状况,所以性能很好。Java 1.8 提供的 Stream API 里面并行流也是以 ForkJoinPool 为基础的。不过需要你注意的是,默认情况下所有的并行流计算都共享一个 ForkJoinPool,这个共享的 ForkJoinPool 默认的线程数是 CPU 的核数;如果所有的并行流计算都是 CPU 密集型计算的话,完全没有问题,但是如果存在 I/O 密集型的并行流计算,那么很可能会因为一个很慢的 I/O 计算而拖慢整个系统的性能。所以**建议用不同的 ForkJoinPool 执行不同类型的计算任务**。 + +如果你对 ForkJoinPool 详细的实现细节感兴趣,也可以参考 [Doug Lea 的论文](http://gee.cs.oswego.edu/dl/papers/fj.pdf)。 + +## 并发工具类模块热点问题答疑 + +略 + +## 参考资料 + +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\345\233\233.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\345\233\233.md" new file mode 100644 index 0000000000..dc996da25e --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\345\271\266\345\217\221\347\274\226\347\250\213\345\256\236\346\210\230\347\254\224\350\256\260\345\233\233.md" @@ -0,0 +1,1023 @@ +--- +title: 《极客时间教程 - Java 并发编程实战》笔记四 +date: 2024-08-30 08:02:52 +categories: + - 笔记 + - Java +tags: + - Java + - 并发 +permalink: /pages/37ccdcd1/ +--- + +# 《极客时间教程 - Java 并发编程实战》笔记四 + +## 案例分析(一):高性能限流器 Guava RateLimiter + +Guava 是 Google 开源的 Java 类库,提供了一个工具类 RateLimiter。 + +【示例】使用 RateLimiter 限流 + +```java +//限流器流速:2 个请求/秒 +RateLimiter limiter = RateLimiter.create(2.0); +//执行任务的线程池 +ExecutorService es = Executors.newFixedThreadPool(1); +//记录上一次执行时间 +prev = System.nanoTime(); +//测试执行 20 次 +for (int i = 0; i < 20; i++) { + //限流器限流 + limiter.acquire(); + //提交任务异步执行 + es.execute(() -> { + long cur = System.nanoTime(); + //打印时间间隔:毫秒 + System.out.println((cur - prev) / 1000_000); + prev = cur; + }); +} + +// 输出结果: +// ... +// 500 +// 499 +// 500 +// 499 +``` + +### 经典限流算法:令牌桶算法 + +Guava 限流算法采用**令牌桶算法**,其**核心是要想通过限流器,必须拿到令牌**。也就是说,只要我们能够限制发放令牌的速率,那么就能控制流速了。令牌桶算法的详细描述如下: + +1. 令牌以固定的速率添加到令牌桶中,假设限流的速率是 r/秒,则令牌每 1/r 秒会添加一个; +2. 假设令牌桶的容量是 b ,如果令牌桶已满,则新的令牌会被丢弃; +3. 请求能够通过限流器的前提是令牌桶中有令牌。 + +这个算法中,限流的速率 r 还是比较容易理解的,但令牌桶的容量 b 该怎么理解呢?b 其实是 burst 的简写,意义是**限流器允许的最大突发流量**。比如 b=10,而且令牌桶中的令牌已满,此时限流器允许 10 个请求同时通过限流器,当然只是突发流量而已,这 10 个请求会带走 10 个令牌,所以后续的流量只能按照速率 r 通过限流器。 + +### Guava 如何实现令牌桶算法 + +Guava 实现令牌桶算法,其关键是**记录并动态计算下一令牌发放的时间**。 + +假设令牌桶的容量为 b=1,限流速率 r = 1 个请求/秒,如下图所示,如果当前令牌桶中没有令牌,下一个令牌的发放时间是在第 3 秒,而在第 2 秒的时候有一个线程 T1 请求令牌,此时该如何处理呢? + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010943737.png) + +对于这个请求令牌的线程而言,很显然需要等待 1 秒,因为 1 秒以后(第 3 秒)它就能拿到令牌了。此时需要注意的是,下一个令牌发放的时间也要增加 1 秒,为什么呢?因为第 3 秒发放的令牌已经被线程 T1 预占了。处理之后如下图所示。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010944198.png) + +假设 T1 在预占了第 3 秒的令牌之后,马上又有一个线程 T2 请求令牌,如下图所示。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010945560.png) + +很显然,由于下一个令牌产生的时间是第 4 秒,所以线程 T2 要等待两秒的时间,才能获取到令牌,同时由于 T2 预占了第 4 秒的令牌,所以下一令牌产生时间还要增加 1 秒,完全处理之后,如下图所示。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010946590.png) + +上面线程 T1、T2 都是在**下一令牌产生时间之前**请求令牌,如果线程在**下一令牌产生时间之后**请求令牌会如何呢?假设在线程 T1 请求令牌之后的 5 秒,也就是第 7 秒,线程 T3 请求令牌,如下图所示。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010947529.png) + +由于在第 5 秒已经产生了一个令牌,所以此时线程 T3 可以直接拿到令牌,而无需等待。在第 7 秒,实际上限流器能够产生 3 个令牌,第 5、6、7 秒各产生一个令牌。由于我们假设令牌桶的容量是 1,所以第 6、7 秒产生的令牌就丢弃了,其实等价地你也可以认为是保留的第 7 秒的令牌,丢弃的第 5、6 秒的令牌,也就是说第 7 秒的令牌被线程 T3 占有了,于是下一令牌的的产生时间应该是第 8 秒,如下图所示。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010947885.png) + +通过上面简要地分析们**只需要记录一个下一令牌产生的时间,并动态更新它,就能够轻松完成限流功能**。我们可以将上面的这个算法代码化,示例代码如下所示,依然假设令牌桶的容量是 1。关键是** reserve() 方法**,这个方法会为请求令牌的线程预分配令牌,同时返回该线程能够获取令牌的时间。其实现逻辑就是上面提到的:如果线程请求令牌的时间在下一令牌产生时间之后,那么该线程立刻就能够获取令牌;反之,如果请求时间在下一令牌产生时间之前,那么该线程是在下一令牌产生的时间获取令牌。由于此时下一令牌已经被该线程预占,所以下一令牌产生的时间需要加上 1 秒。 + +```java +class SimpleLimiter { + + //下一令牌产生时间 + long next = System.nanoTime(); + //发放令牌间隔:纳秒 + long interval = 1000_000_000; + + //预占令牌,返回能够获取令牌的时间 + synchronized long reserve(long now) { + //请求时间在下一令牌产生时间之后 + //重新计算下一令牌产生时间 + if (now > next) { + //将下一令牌产生时间重置为当前时间 + next = now; + } + //能够获取令牌的时间 + long at = next; + //设置下一令牌产生时间 + next += interval; + //返回线程需要等待的时间 + return Math.max(at, 0L); + } + + //申请令牌 + void acquire() { + //申请令牌时的时间 + long now = System.nanoTime(); + //预占令牌 + long at = reserve(now); + long waitTime = max(at - now, 0); + //按照条件等待 + if (waitTime > 0) { + try { + TimeUnit.NANOSECONDS.sleep(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} +``` + +如果令牌桶的容量大于 1,又该如何处理呢?按照令牌桶算法,令牌要首先从令牌桶中出,所以我们需要按需计算令牌桶中的数量,当有线程请求令牌时,先从令牌桶中出。具体的代码实现如下所示。我们增加了一个** resync() 方法**,在这个方法中,如果线程请求令牌的时间在下一令牌产生时间之后,会重新计算令牌桶中的令牌数,**新产生的令牌的计算公式是:(now-next)/interval**,你可对照上面的示意图来理解。reserve() 方法中,则增加了先从令牌桶中出令牌的逻辑,不过需要注意的是,如果令牌是从令牌桶中出的,那么 next 就无需增加一个 interval 了。 + +```java +class SimpleLimiter { + + //当前令牌桶中的令牌数量 + long storedPermits = 0; + //令牌桶的容量 + long maxPermits = 3; + //下一令牌产生时间 + long next = System.nanoTime(); + //发放令牌间隔:纳秒 + long interval = 1000_000_000; + + //请求时间在下一令牌产生时间之后,则 + // 1. 重新计算令牌桶中的令牌数 + // 2. 将下一个令牌发放时间重置为当前时间 + void resync(long now) { + if (now > next) { + //新产生的令牌数 + long newPermits = (now - next) / interval; + //新令牌增加到令牌桶 + storedPermits = min(maxPermits, storedPermits + newPermits); + //将下一个令牌发放时间重置为当前时间 + next = now; + } + } + + //预占令牌,返回能够获取令牌的时间 + synchronized long reserve(long now) { + resync(now); + //能够获取令牌的时间 + long at = next; + //令牌桶中能提供的令牌 + long fb = min(1, storedPermits); + //令牌净需求:首先减掉令牌桶中的令牌 + long nr = 1 - fb; + //重新计算下一令牌产生时间 + next = next + nr * interval; + //重新计算令牌桶中的令牌 + this.storedPermits -= fb; + return at; + } + + //申请令牌 + void acquire() { + //申请令牌时的时间 + long now = System.nanoTime(); + //预占令牌 + long at = reserve(now); + long waitTime = max(at - now, 0); + //按照条件等待 + if (waitTime > 0) { + try { + TimeUnit.NANOSECONDS.sleep(waitTime); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + } +} +``` + +### 总结 + +经典的限流算法有两个,一个是**令牌桶算法(Token Bucket)**,另一个是**漏桶算法(Leaky Bucket)**。令牌桶算法是定时向令牌桶发送令牌,请求能够从令牌桶中拿到令牌,然后才能通过限流器;而漏桶算法里,请求就像水一样注入漏桶,漏桶会按照一定的速率自动将水漏掉,只有漏桶里还能注入水的时候,请求才能通过限流器。令牌桶算法和漏桶算法很像一个硬币的正反面,所以你可以参考令牌桶算法的实现来实现漏桶算法。 + +## 案例分析(二):高性能网络应用框架 Netty + +### 网络编程性能的瓶颈 + +BIO 模型里,所有 read() 操作和 write() 操作都会阻塞当前线程的,如果客户端已经和服务端建立了一个连接,而迟迟不发送数据,那么服务端的 read() 操作会一直阻塞,所以**使用 BIO 模型,一般都会为每个 socket 分配一个独立的线程**,这样就不会因为线程阻塞在一个 socket 上而影响对其他 socket 的读写。BIO 的线程模型如下图所示,每一个 socket 都对应一个独立的线程;为了避免频繁创建、消耗线程,可以采用线程池,但是 socket 和线程之间的对应关系并不会变化。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010957084.png) + +BIO 这种线程模型适用于 socket 连接不是很多的场景;但是现在的互联网场景,往往需要服务器能够支撑十万甚至百万连接,而创建十万甚至上百万个线程显然并不现实,所以 BIO 线程模型无法解决百万连接的问题。如果仔细观察,你会发现互联网场景中,虽然连接多,但是每个连接上的请求并不频繁,所以线程大部分时间都在等待 I/O 就绪。也就是说线程大部分时间都阻塞在那里,这完全是浪费,如果我们能够解决这个问题,那就不需要这么多线程了。 + +可以用一个线程来处理多个连接,这样线程的利用率就上来了,同时所需的线程数量也跟着降下来了。这个思路很好,可是使用 BIO 相关的 API 是无法实现的,这是为什么呢?因为 BIO 相关的 socket 读写操作都是阻塞式的,而一旦调用了阻塞式 API,在 I/O 就绪前,调用线程会一直阻塞,也就无法处理其他的 socket 连接了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409010959294.png) + +### Reactor 模式 + +下面是 Reactor 模式的类结构图,其中 Handle 指的是 I/O 句柄,在 Java 网络编程里,它本质上就是一个网络连接。Event Handler 很容易理解,就是一个事件处理器,其中 handle_event() 方法处理 I/O 事件,也就是每个 Event Handler 处理一个 I/O Handle;get_handle() 方法可以返回这个 I/O 的 Handle。Synchronous Event Demultiplexer 可以理解为操作系统提供的 I/O 多路复用 API,例如 POSIX 标准里的 select() 以及 Linux 里面的 epoll()。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409011000910.png) + +Reactor 模式的核心自然是 **Reactor 这个类**,其中 register_handler() 和 remove_handler() 这两个方法可以注册和删除一个事件处理器;**handle_events() 方式是核心**,也是 Reactor 模式的发动机,这个方法的核心逻辑如下:首先通过同步事件多路选择器提供的 select() 方法监听网络事件,当有网络事件就绪后,就遍历事件处理器来处理该网络事件。由于网络事件是源源不断的,所以在主程序中启动 Reactor 模式,需要以 `while(true){}` 的方式调用 handle_events() 方法。 + +```java +void Reactor::handle_events(){ + //通过同步事件多路选择器提供的 + //select() 方法监听网络事件 + select(handlers); + //处理网络事件 + for(h in handlers){ + h.handle_event(); + } +} +// 在主程序中启动事件循环 +while (true) { + handle_events(); +``` + +### Netty 中的线程模型 + +**Netty 中最核心的概念是事件循环(EventLoop)**,其实也就是 Reactor 模式中的 Reactor,**负责监听网络事件并调用事件处理器进行处理**。在 4.x 版本的 Netty 中,网络连接和 EventLoop 是稳定的多对 1 关系,而 EventLoop 和 Java 线程是 1 对 1 关系,这里的稳定指的是关系一旦确定就不再发生变化。也就是说一个网络连接只会对应唯一的一个 EventLoop,而一个 EventLoop 也只会对应到一个 Java 线程,所以**一个网络连接只会对应到一个 Java 线程**。 + +一个网络连接对应到一个 Java 线程上,最大的好处就是对于一个网络连接的事件处理是单线程的,这样就**避免了各种并发问题**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409011004870.png) + +Netty 中还有一个核心概念是** EventLoopGroup**,顾名思义,一个 EventLoopGroup 由一组 EventLoop 组成。实际使用中,一般都会创建两个 EventLoopGroup,一个称为 bossGroup,一个称为 workerGroup。 + +这个和 socket 处理网络请求的机制有关,socket 处理 TCP 网络连接请求,是在一个独立的 socket 中,每当有一个 TCP 连接成功建立,都会创建一个新的 socket,之后对 TCP 连接的读写都是由新创建处理的 socket 完成的。也就是说**处理 TCP 连接请求和读写请求是通过两个不同的 socket 完成的**。 + +**在 Netty 中,bossGroup 就用来处理连接请求的,而 workerGroup 是用来处理读写请求的**。bossGroup 处理完连接请求后,会将这个连接提交给 workerGroup 来处理, workerGroup 里面有多个 EventLoop,那新的连接会交给哪个 EventLoop 来处理呢?这就需要一个负载均衡算法,Netty 中目前使用的是**轮询算法**。 + +### 用 Netty 实现 Echo 程序服务端 + +第一个,如果 NettybossGroup 只监听一个端口,那 bossGroup 只需要 1 个 EventLoop 就可以了,多了纯属浪费。 + +第二个,默认情况下,Netty 会创建“2\*CPU 核数”个 EventLoop,由于网络连接与 EventLoop 有稳定的关系,所以事件处理器在处理网络事件的时候是不能有阻塞操作的,否则很容易导致请求大面积超时。如果实在无法避免使用阻塞操作,那可以通过线程池来异步处理。 + +```java +//事件处理器 +final EchoServerHandler serverHandler = new EchoServerHandler(); +//boss 线程组 +EventLoopGroup bossGroup = new NioEventLoopGroup(1); +//worker 线程组 +EventLoopGroup workerGroup = new NioEventLoopGroup(); +try { + ServerBootstrap b = new ServerBootstrap(); + b.group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + public void initChannel(SocketChannel ch) { + ch.pipeline().addLast(serverHandler); + } + }); + //bind 服务端端口 + ChannelFuture f = b.bind(9090).sync(); + f.channel().closeFuture().sync(); +} finally { + //终止工作线程组 + workerGroup.shutdownGracefully(); + //终止 boss 线程组 + bossGroup.shutdownGracefully(); +} + +//socket 连接处理器 +class EchoServerHandler extends ChannelInboundHandlerAdapter { + + //处理读事件 + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + ctx.write(msg); + } + + //处理读完成事件 + @Override + public void channelReadComplete(ChannelHandlerContext ctx) { + ctx.flush(); + } + + //处理异常事件 + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + cause.printStackTrace(); + ctx.close(); + } +} +``` + +### 总结 + +Netty 是一个款优秀的网络编程框架,性能非常好,为了实现高性能的目标,Netty 做了很多优化,例如优化了 ByteBuffer、支持零拷贝等等,和并发编程相关的就是它的线程模型了。Netty 的线程模型设计得很精巧,每个网络连接都关联到了一个线程上,这样做的好处是:对于一个网络连接,读写操作都是单线程执行的,从而避免了并发程序的各种问题。 + +## 案例分析(三):高性能队列 Disruptor + +**Disruptor 是一款高性能的有界内存队列**,目前应用非常广泛,Log4j2、Spring Messaging、HBase、Storm 都用到了 Disruptor,那 Disruptor 的性能为什么这么高呢?Disruptor 项目团队曾经写过一篇论文,详细解释了其原因,可以总结为如下: + +1. 内存分配更加合理,使用 RingBuffer 数据结构,数组元素在初始化时一次性全部创建,提升缓存命中率;对象循环利用,避免频繁 GC。 +2. 能够避免伪共享,提升缓存利用率。 +3. 采用无锁算法,避免频繁加锁、解锁的性能消耗。 +4. 支持批量消费,消费者可以无锁方式消费多个消息。 + +Disruptor 的使用比 Java SDK 提供 BlockingQueue 要复杂一些,但是总体思路还是一致的,其大致情况如下: + +- 在 Disruptor 中,生产者生产的对象(也就是消费者消费的对象)称为 Event,使用 Disruptor 必须自定义 Event,例如示例代码的自定义 Event 是 LongEvent; + +- 构建 Disruptor 对象除了要指定队列大小外,还需要传入一个 EventFactory,示例代码中传入的是`LongEvent::new`; + +- 消费 Disruptor 中的 Event 需要通过 handleEventsWith() 方法注册一个事件处理器,发布 Event 则需要通过 publishEvent() 方法。 + + ```java + // 自定义 Event + class LongEvent { + private long value; + public void set(long value) { + this.value = value; + } + } + // 指定 RingBuffer 大小, + // 必须是 2 的 N 次方 + int bufferSize = 1024; + + // 构建 Disruptor + Disruptor disruptor + = new Disruptor<>( + LongEvent::new, + bufferSize, + DaemonThreadFactory.INSTANCE); + + // 注册事件处理器 + disruptor.handleEventsWith( + (event, sequence, endOfBatch) -> + System.out.println("E: "+event)); + + // 启动 Disruptor + disruptor.start(); + + // 获取 RingBuffer + RingBuffer ringBuffer + = disruptor.getRingBuffer(); + // 生产 Event + ByteBuffer bb = ByteBuffer.allocate(8); + for (long l = 0; true; l++){ + bb.putLong(0, l); + // 生产者生产消息 + ringBuffer.publishEvent( + (event, sequence, buffer) -> + event.set(buffer.getLong(0)), bb); + Thread.sleep(1000); + } + ``` + +### RingBuffer 如何提升性能 + +Java SDK 中 ArrayBlockingQueue 使用**数组**作为底层的数据存储,而 Disruptor 是使用** RingBuffer **作为数据存储。RingBuffer 本质上也是数组。 + +生产者线程向 ArrayBlockingQueue 增加一个元素,每次增加元素 E 之前,都需要创建一个对象 E,如下图所示,ArrayBlockingQueue 内部有 6 个元素,这 6 个元素都是由生产者线程创建的,由于创建这些元素的时间基本上是离散的,所以这些元素的内存地址大概率也不是连续的。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409020709300.png) + +Disruptor 内部的 RingBuffer 也是用数组实现的,但是这个数组中的所有元素在初始化时是一次性全部创建的,所以这些元素的内存地址大概率是连续的,相关的代码如下所示。 + +```java +for (int i=0; icachedGatingSequence || cachedGatingSequence>current){ + //重新计算所有消费者里面的最小值位置 + long gatingSequence = Util.getMinimumSequence( + gatingSequences, current); + //仍然没有足够的空余位置,出让 CPU 使用权,重新执行下一循环 + if (wrapPoint > gatingSequence){ + LockSupport.parkNanos(1); + continue; + } + //从新设置上一次的最小消费位置 + gatingSequenceCache.set(gatingSequence); + } else if (cursor.compareAndSet(current, next)){ + //获取写入位置成功,跳出循环 + break; + } +} while (true); +``` + +## 案例分析(四):高性能数据库连接池 HiKariCP + +业界知名的数据库连接池有不少,例如 c3p0、DBCP、Tomcat JDBC Connection Pool、Druid 等,不过最近最火的是 HiKariCP。 + +**HiKariCP 号称是业界跑得最快的数据库连接池**,这两年发展得顺风顺水,尤其是 Springboot 2.0 将其作为**默认数据库连接池**。 + +### 什么是数据库连接池 + +数据库连接池和线程池一样,都属于池化资源,作用都是避免重量级资源的频繁创建和销毁,对于数据库连接池来说,也就是避免数据库连接频繁创建和销毁。如下图所示,服务端会在运行期持有一定数量的数据库连接,当需要执行 SQL 时,并不是直接创建一个数据库连接,而是从连接池中获取一个;当 SQL 执行完,也并不是将数据库连接真的关掉,而是将其归还到连接池中。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409020725868.png) + +执行数据库操作基本上是一系列规范化的步骤: + +1. 通过数据源获取一个数据库连接; +2. 创建 Statement; +3. 执行 SQL; +4. 通过 ResultSet 获取 SQL 执行结果; +5. 释放 ResultSet; +6. 释放 Statement; +7. 释放数据库连接。 + +下面的示例代码,通过 `ds.getConnection()` 获取一个数据库连接时,其实是向数据库连接池申请一个数据库连接,而不是创建一个新的数据库连接。同样,通过 `conn.close()` 释放一个数据库连接时,也不是直接将连接关闭,而是将连接归还给数据库连接池。 + +```java +//数据库连接池配置 +HikariConfig config = new HikariConfig(); +config.setMinimumIdle(1); +config.setMaximumPoolSize(2); +config.setConnectionTestQuery("SELECT 1"); +config.setDataSourceClassName("org.h2.jdbcx.JdbcDataSource"); +config.addDataSourceProperty("url", "jdbc:h2:mem:test"); +// 创建数据源 +DataSource ds = new HikariDataSource(config); +Connection conn = null; +Statement stmt = null; +ResultSet rs = null; +try { + // 获取数据库连接 + conn = ds.getConnection(); + // 创建 Statement + stmt = conn.createStatement(); + // 执行 SQL + rs = stmt.executeQuery("select * from abc"); + // 获取结果 + while (rs.next()) { + int id = rs.getInt(1); + ...... + } +} catch(Exception e) { + e.printStackTrace(); +} finally { + //关闭 ResultSet + close(rs); + //关闭 Statement + close(stmt); + //关闭 Connection + close(conn); +} +//关闭资源 +void close(AutoCloseable rs) { + if (rs != null) { + try { + rs.close(); + } catch (SQLException e) { + e.printStackTrace(); + } + } +} +``` + +[HiKariCP 官方网站](https://github.com/brettwooldridge/HikariCP/wiki/Down-the-Rabbit-Hole) 解释了其性能之所以如此之高的秘密。微观上 HiKariCP 程序编译出的字节码执行效率更高,站在字节码的角度去优化 Java 代码,HiKariCP 的作者对性能的执着可见一斑,不过遗憾的是他并没有详细解释都做了哪些优化。而宏观上主要是和两个数据结构有关,一个是 FastList,另一个是 ConcurrentBag。下面我们来看看它们是如何提升 HiKariCP 的性能的。 + +### FastList 解决了哪些性能问题 + +按照规范步骤,执行完数据库操作之后,需要依次关闭 ResultSet、Statement、Connection,但是总有粗心的同学只是关闭了 Connection,而忘了关闭 ResultSet 和 Statement。为了解决这种问题,最好的办法是当关闭 Connection 时,能够自动关闭 Statement。为了达到这个目标,Connection 就需要跟踪创建的 Statement,最简单的办法就是将创建的 Statement 保存在数组 ArrayList 里,这样当关闭 Connection 的时候,就可以依次将数组中的所有 Statement 关闭。 + +HiKariCP 觉得用 ArrayList 还是太慢,当通过 `conn.createStatement()` 创建一个 Statement 时,需要调用 ArrayList 的 add() 方法加入到 ArrayList 中,这个是没有问题的;但是当通过 `stmt.close()` 关闭 Statement 的时候,需要调用 ArrayList 的 remove() 方法来将其从 ArrayList 中删除,这里是有优化余地的。 + +假设一个 Connection 依次创建 6 个 Statement,分别是 S1、S2、S3、S4、S5、S6,按照正常的编码习惯,关闭 Statement 的顺序一般是逆序的,关闭的顺序是:S6、S5、S4、S3、S2、S1,而 ArrayList 的 remove(Object o) 方法是顺序遍历查找,逆序删除而顺序查找,这样的查找效率就太慢了。如何优化呢?很简单,优化成逆序查找就可以了。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409020729492.png) + +HiKariCP 中的 FastList 相对于 ArrayList 的一个优化点就是将 `remove(Object element)` 方法的**查找顺序变成了逆序查找**。除此之外,FastList 还有另一个优化点,是 `get(int index)` 方法没有对 index 参数进行越界检查,HiKariCP 能保证不会越界,所以不用每次都进行越界检查。 + +### ConcurrentBag 解决了哪些性能问题 + +如果让我们自己来实现一个数据库连接池,最简单的办法就是用两个阻塞队列来实现,一个用于保存空闲数据库连接的队列 idle,另一个用于保存忙碌数据库连接的队列 busy;获取连接时将空闲的数据库连接从 idle 队列移动到 busy 队列,而关闭连接时将数据库连接从 busy 移动到 idle。这种方案将并发问题委托给了阻塞队列,实现简单,但是性能并不是很理想。因为 Java SDK 中的阻塞队列是用锁实现的,而高并发场景下锁的争用对性能影响很大。 + +```java +//忙碌队列 +BlockingQueue busy; +//空闲队列 +BlockingQueue idle; +``` + +HiKariCP 并没有使用 Java SDK 中的阻塞队列,而是自己实现了一个叫做 ConcurrentBag 的并发容器。ConcurrentBag 的设计最初源自 C#,它的一个核心设计是使用 ThreadLocal 避免部分并发问题,不过 HiKariCP 中的 ConcurrentBag 并没有完全参考 C#的实现,下面我们来看看它是如何实现的。 + +ConcurrentBag 中最关键的属性有 4 个,分别是:用于存储所有的数据库连接的共享队列 sharedList、线程本地存储 threadList、等待数据库连接的线程数 waiters 以及分配数据库连接的工具 handoffQueue。其中,handoffQueue 用的是 Java SDK 提供的 SynchronousQueue,SynchronousQueue 主要用于线程之间传递数据。 + +```java +//用于存储所有的数据库连接 +CopyOnWriteArrayList sharedList; +//线程本地存储中的数据库连接 +ThreadLocal> threadList; +//等待数据库连接的线程数 +AtomicInteger waiters; +//分配数据库连接的工具 +SynchronousQueue handoffQueue; +``` + +当线程池创建了一个数据库连接时,通过调用 ConcurrentBag 的 add() 方法加入到 ConcurrentBag 中,下面是 add() 方法的具体实现,逻辑很简单,就是将这个连接加入到共享队列 sharedList 中,如果此时有线程在等待数据库连接,那么就通过 handoffQueue 将这个连接分配给等待的线程。 + +```java +//将空闲连接添加到队列 +void add(final T bagEntry){ + //加入共享队列 + sharedList.add(bagEntry); + //如果有等待连接的线程, + //则通过 handoffQueue 直接分配给等待的线程 + while (waiters.get() > 0 + && bagEntry.getState() == STATE_NOT_IN_USE + && !handoffQueue.offer(bagEntry)) { + yield(); + } +} +``` + +通过 ConcurrentBag 提供的 borrow() 方法,可以获取一个空闲的数据库连接,borrow() 的主要逻辑是: + +1. 首先查看线程本地存储是否有空闲连接,如果有,则返回一个空闲的连接; +2. 如果线程本地存储中无空闲连接,则从共享队列中获取。 +3. 如果共享队列中也没有空闲的连接,则请求线程需要等待。 + +需要注意的是,线程本地存储中的连接是可以被其他线程窃取的,所以需要用 CAS 方法防止重复分配。在共享队列中获取空闲连接,也采用了 CAS 方法防止重复分配。 + +```java +T borrow(long timeout, final TimeUnit timeUnit){ + // 先查看线程本地存储是否有空闲连接 + final List list = threadList.get(); + for (int i = list.size() - 1; i >= 0; i--) { + final Object entry = list.remove(i); + final T bagEntry = weakThreadLocals + ? ((WeakReference) entry).get() + : (T) entry; + //线程本地存储中的连接也可以被窃取, + //所以需要用 CAS 方法防止重复分配 + if (bagEntry != null + && bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { + return bagEntry; + } + } + + // 线程本地存储中无空闲连接,则从共享队列中获取 + final int waiting = waiters.incrementAndGet(); + try { + for (T bagEntry : sharedList) { + //如果共享队列中有空闲连接,则返回 + if (bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { + return bagEntry; + } + } + //共享队列中没有连接,则需要等待 + timeout = timeUnit.toNanos(timeout); + do { + final long start = currentTime(); + final T bagEntry = handoffQueue.poll(timeout, NANOSECONDS); + if (bagEntry == null + || bagEntry.compareAndSet(STATE_NOT_IN_USE, STATE_IN_USE)) { + return bagEntry; + } + //重新计算等待时间 + timeout -= elapsedNanos(start); + } while (timeout > 10_000); + //超时没有获取到连接,返回 null + return null; + } finally { + waiters.decrementAndGet(); + } +} +``` + +释放连接需要调用 ConcurrentBag 提供的 requite() 方法,该方法的逻辑很简单,首先将数据库连接状态更改为 STATE_NOT_IN_USE,之后查看是否存在等待线程,如果有,则分配给等待线程;如果没有,则将该数据库连接保存到线程本地存储里。 + +```java +//释放连接 +void requite(final T bagEntry){ + //更新连接状态 + bagEntry.setState(STATE_NOT_IN_USE); + //如果有等待的线程,则直接分配给线程,无需进入任何队列 + for (int i = 0; waiters.get() > 0; i++) { + if (bagEntry.getState() != STATE_NOT_IN_USE + || handoffQueue.offer(bagEntry)) { + return; + } else if ((i & 0xff) == 0xff) { + parkNanos(MICROSECONDS.toNanos(10)); + } else { + yield(); + } + } + //如果没有等待的线程,则进入线程本地存储 + final List threadLocalList = threadList.get(); + if (threadLocalList.size() < 50) { + threadLocalList.add(weakThreadLocals + ? new WeakReference<>(bagEntry) + : bagEntry); + } +} +``` + +## Actor 模型:面向对象原生的并发模型 + +### Hello Actor 模型 + +Actor 模型本质上是一种计算模型,基本的计算单元称为 Actor,换言之,**在 Actor 模型中,所有的计算都是在 Actor 中执行的**。在面向对象编程里面,一切都是对象;在 Actor 模型里,一切都是 Actor,并且 Actor 之间是完全隔离的,不会共享任何变量。 + +并发问题的根源就在于共享变量,而 Actor 模型中 Actor 之间不共享变量,所以很多人就把 Actor 模型定义为一种**并发计算模型**。 + +Java 语言本身并不支持 Actor 模型,所以如果你想在 Java 语言里使用 Actor 模型,就需要借助第三方类库,目前能完备地支持 Actor 模型而且比较成熟的类库就是** Akka **了。 + +在下面的示例代码中,我们首先创建了一个 ActorSystem(Actor 不能脱离 ActorSystem 存在);之后创建了一个 HelloActor,Akka 中创建 Actor 并不是 new 一个对象出来,而是通过调用 system.actorOf() 方法创建的,该方法返回的是 ActorRef,而不是 HelloActor;最后通过调用 ActorRef 的 tell() 方法给 HelloActor 发送了一条消息 “Actor” 。 + +```java +//该 Actor 当收到消息 message 后, +//会打印 Hello message +static class HelloActor + extends UntypedActor { + @Override + public void onReceive(Object message) { + System.out.println("Hello " + message); + } +} + +public static void main(String[] args) { + //创建 Actor 系统 + ActorSystem system = ActorSystem.create("HelloSystem"); + //创建 HelloActor + ActorRef helloActor = + system.actorOf(Props.create(HelloActor.class)); + //发送消息给 HelloActor + helloActor.tell("Actor", ActorRef.noSender()); +} +``` + +### 消息和对象方法的区别 + +Actor 中的消息机制,就可以类比这现实世界里的写信。Actor 内部有一个邮箱(Mailbox),接收到的消息都是先放到邮箱里,如果邮箱里有积压的消息,那么新收到的消息就不会马上得到处理,也正是因为 Actor 使用单线程处理消息,所以不会出现并发问题。你可以把 Actor 内部的工作模式想象成只有一个消费者线程的生产者-消费者模式。 + +在 Actor 模型里,发送消息仅仅是把消息发出去而已,接收消息的 Actor 在接收到消息后,也不一定会立即处理,也就是说** Actor 中的消息机制完全是异步的**。而**调用对象方法**,实际上是**同步**的,对象方法 return 之前,调用方会一直等待。 + +除此之外,**调用对象方法**,需要持有对象的引用,**所有的对象必须在同一个进程中**。而在 Actor 中发送消息,类似于现实中的写信,只需要知道对方的地址就可以,**发送消息和接收消息的 Actor 可以不在一个进程中,也可以不在同一台机器上**。因此,Actor 模型不但适用于并发计算,还适用于分布式计算。 + +### Actor 的规范化定义 + +Actor 是一种基础的计算单元,具体来讲包括三部分能力,分别是: + +1. 处理能力,处理接收到的消息。 +2. 存储能力,Actor 可以存储自己的内部状态,并且内部状态在不同 Actor 之间是绝对隔离的。 +3. 通信能力,Actor 可以和其他 Actor 之间通信。 + +当一个 Actor 接收的一条消息之后,这个 Actor 可以做以下三件事: + +1. 创建更多的 Actor; +2. 发消息给其他 Actor; +3. 确定如何处理下一条消息。 + +### 用 Actor 实现累加器 + +在下面的示例代码中,CounterActor 内部持有累计值 counter,当 CounterActor 接收到一个数值型的消息 message 时,就将累计值 counter += message;但如果是其他类型的消息,则打印当前累计值 counter。在 main() 方法中,我们启动了 4 个线程来执行累加操作。整个程序没有锁,也没有 CAS,但是程序是线程安全的。 + +```java +//累加器 +static class CounterActor extends UntypedActor { + private int counter = 0; + @Override + public void onReceive(Object message){ + //如果接收到的消息是数字类型,执行累加操作, + //否则打印 counter 的值 + if (message instanceof Number) { + counter += ((Number) message).intValue(); + } else { + System.out.println(counter); + } + } +} +public static void main(String[] args) throws InterruptedException { + //创建 Actor 系统 + ActorSystem system = ActorSystem.create("HelloSystem"); + //4 个线程生产消息 + ExecutorService es = Executors.newFixedThreadPool(4); + //创建 CounterActor + ActorRef counterActor = + system.actorOf(Props.create(CounterActor.class)); + //生产 4*100000 个消息 + for (int i=0; i<4; i++) { + es.execute(()->{ + for (int j=0; j<100000; j++) { + counterActor.tell(1, ActorRef.noSender()); + } + }); + } + //关闭线程池 + es.shutdown(); + //等待 CounterActor 处理完所有消息 + Thread.sleep(1000); + //打印结果 + counterActor.tell("", ActorRef.noSender()); + //关闭 Actor 系统 + system.shutdown(); +} +``` + +## 软件事务内存:借鉴数据库的并发经验 + +**软件事务内存(Software Transactional Memory,简称 STM)**。传统的数据库事务,支持 4 个特性:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)和持久性(Durability),也就是大家常说的 ACID,STM 由于不涉及到持久化,所以只支持 ACI。 + +### 用 STM 实现转账 + +并发转账可以简单地使用 synchronized 将 transfer() 方法变成同步方法,但并不能解决并发问题,因为还存在死锁问题。 + +```cpp +class UnsafeAccount { + //余额 + private long balance; + //构造函数 + public UnsafeAccount(long balance) { + this.balance = balance; + } + //转账 + void transfer(UnsafeAccount target, long amt){ + if (this.balance > amt) { + this.balance -= amt; + target.balance += amt; + } + } +} +``` + +该转账操作若使用数据库事务就会非常简单,如下面的示例代码所示。如果所有 SQL 都正常执行,则通过 commit() 方法提交事务;如果 SQL 在执行过程中有异常,则通过 rollback() 方法回滚事务。数据库保证在并发情况下不会有死锁,而且还能保证前面我们说的原子性、一致性、隔离性和持久性,也就是 ACID。 + +```java +Connection conn = null; +try{ + //获取数据库连接 + conn = DriverManager.getConnection(); + //设置手动提交事务 + conn.setAutoCommit(false); + //执行转账 SQL + ...... + //提交事务 + conn.commit(); +} catch (Exception e) { + //出现异常回滚事务 + conn.rollback(); +} +``` + +那如果用 STM 又该如何实现呢?Java 语言并不支持 STM,不过可以借助第三方的类库来支持,[Multiverse](https://github.com/pveentjer/Multiverse) 就是个不错的选择。下面的示例代码就是借助 Multiverse 实现了线程安全的转账操作,相比较上面线程不安全的 UnsafeAccount,其改动并不大,仅仅是将余额的类型从 long 变成了 TxnLong ,将转账的操作放到了 atomic(()->{}) 中。 + +```java +class Account{ + //余额 + private TxnLong balance; + //构造函数 + public Account(long balance){ + this.balance = StmUtils.newTxnLong(balance); + } + //转账 + public void transfer(Account to, int amt){ + //原子化操作 + atomic(()->{ + if (this.balance.get() > amt) { + this.balance.decrement(amt); + to.balance.increment(amt); + } + }); + } +} +``` + +一个关键的 atomic() 方法就把并发问题解决了,这个方案看上去比传统的方案的确简单了很多,那它是如何实现的呢?数据库事务发展了几十年了,目前被广泛使用的是** MVCC**(全称是 Multi-Version Concurrency Control),也就是多版本并发控制。 + +MVCC 可以简单地理解为数据库事务在开启的时候,会给数据库打一个快照,以后所有的读写都是基于这个快照的。当提交事务的时候,如果所有读写过的数据在该事务执行期间没有发生过变化,那么就可以提交;如果发生了变化,说明该事务和有其他事务读写的数据冲突了,这个时候是不可以提交的。 + +为了记录数据是否发生了变化,可以给每条数据增加一个版本号,这样每次成功修改数据都会增加版本号的值。MVCC 的工作原理和乐观锁非常相似。有不少 STM 的实现方案都是基于 MVCC 的,例如知名的 Clojure STM。 + +下面我们就用最简单的代码基于 MVCC 实现一个简版的 STM,这样你会对 STM 以及 MVCC 的工作原理有更深入的认识。 + +## 自己实现 STM + +我们首先要做的,就是让 Java 中的对象有版本号,在下面的示例代码中,VersionedRef 这个类的作用就是将对象 value 包装成带版本号的对象。按照 MVCC 理论,数据的每一次修改都对应着一个唯一的版本号,所以不存在仅仅改变 value 或者 version 的情况,用不变性模式就可以很好地解决这个问题,所以 VersionedRef 这个类被我们设计成了不可变的。 + +所有对数据的读写操作,一定是在一个事务里面,TxnRef 这个类负责完成事务内的读写操作,读写操作委托给了接口 Txn,Txn 代表的是读写操作所在的当前事务, 内部持有的 curRef 代表的是系统中的最新值。 + +```java +//带版本号的对象引用 +public final class VersionedRef { + final T value; + final long version; + //构造方法 + public VersionedRef(T value, long version) { + this.value = value; + this.version = version; + } +} +//支持事务的引用 +public class TxnRef { + //当前数据,带版本号 + volatile VersionedRef curRef; + //构造方法 + public TxnRef(T value) { + this.curRef = new VersionedRef(value, 0L); + } + //获取当前事务中的数据 + public T getValue(Txn txn) { + return txn.get(this); + } + //在当前事务中设置数据 + public void setValue(T value, Txn txn) { + txn.set(this, value); + } +} +``` + +STMTxn 是 Txn 最关键的一个实现类,事务内对于数据的读写,都是通过它来完成的。STMTxn 内部有两个 Map:inTxnMap,用于保存当前事务中所有读写的数据的快照;writeMap,用于保存当前事务需要写入的数据。每个事务都有一个唯一的事务 ID txnId,这个 txnId 是全局递增的。 + +STMTxn 有三个核心方法,分别是读数据的 get() 方法、写数据的 set() 方法和提交事务的 commit() 方法。其中,get() 方法将要读取数据作为快照放入 inTxnMap,同时保证每次读取的数据都是一个版本。set() 方法会将要写入的数据放入 writeMap,但如果写入的数据没被读取过,也会将其放入 inTxnMap。 + +至于 commit() 方法,我们为了简化实现,使用了互斥锁,所以事务的提交是串行的。commit() 方法的实现很简单,首先检查 inTxnMap 中的数据是否发生过变化,如果没有发生变化,那么就将 writeMap 中的数据写入(这里的写入其实就是 TxnRef 内部持有的 curRef);如果发生过变化,那么就不能将 writeMap 中的数据写入了。 + +```java +//事务接口 +public interface Txn { + T get(TxnRef ref); + void set(TxnRef ref, T value); +} +//STM 事务实现类 +public final class STMTxn implements Txn { + //事务 ID 生成器 + private static AtomicLong txnSeq = new AtomicLong(0); + + //当前事务所有的相关数据 + private Map inTxnMap = new HashMap<>(); + //当前事务所有需要修改的数据 + private Map writeMap = new HashMap<>(); + //当前事务 ID + private long txnId; + //构造函数,自动生成当前事务 ID + STMTxn() { + txnId = txnSeq.incrementAndGet(); + } + + //获取当前事务中的数据 + @Override + public T get(TxnRef ref) { + //将需要读取的数据,加入 inTxnMap + if (!inTxnMap.containsKey(ref)) { + inTxnMap.put(ref, ref.curRef); + } + return (T) inTxnMap.get(ref).value; + } + //在当前事务中修改数据 + @Override + public void set(TxnRef ref, T value) { + //将需要修改的数据,加入 inTxnMap + if (!inTxnMap.containsKey(ref)) { + inTxnMap.put(ref, ref.curRef); + } + writeMap.put(ref, value); + } + //提交事务 + boolean commit() { + synchronized (STM.commitLock) { + //是否校验通过 + boolean isValid = true; + //校验所有读过的数据是否发生过变化 + for(Map.Entry entry : inTxnMap.entrySet()){ + VersionedRef curRef = entry.getKey().curRef; + VersionedRef readRef = entry.getValue(); + //通过版本号来验证数据是否发生过变化 + if (curRef.version != readRef.version) { + isValid = false; + break; + } + } + //如果校验通过,则所有更改生效 + if (isValid) { + writeMap.forEach((k, v) -> { + k.curRef = new VersionedRef(v, txnId); + }); + } + return isValid; + } +} +``` + +下面我们来模拟实现 Multiverse 中的原子化操作 atomic()。atomic() 方法中使用了类似于 CAS 的操作,如果事务提交失败,那么就重新创建一个新的事务,重新执行。 + +```java +@FunctionalInterface +public interface TxnRunnable { + void run(Txn txn); +} +//STM +public final class STM { + //私有化构造方法 + private STM() { + //提交数据需要用到的全局锁 + static final Object commitLock = new Object(); + //原子化提交方法 + public static void atomic(TxnRunnable action) { + boolean committed = false; + //如果没有提交成功,则一直重试 + while (!committed) { + //创建新的事务 + STMTxn txn = new STMTxn(); + //执行业务逻辑 + action.run(txn); + //提交事务 + committed = txn.commit(); + } + } +}} +``` + +就这样,我们自己实现了 STM,并完成了线程安全的转账操作,使用方法和 Multiverse 差不多,这里就不赘述了,具体代码如下面所示。 + +```java +class Account { + //余额 + private TxnRef balance; + //构造方法 + public Account(int balance) { + this.balance = new TxnRef(balance); + } + //转账操作 + public void transfer(Account target, int amt){ + STM.atomic((txn)->{ + Integer from = balance.getValue(txn); + balance.setValue(from-amt, txn); + Integer to = target.balance.getValue(txn); + target.balance.setValue(to+amt, txn); + }); + } +} +``` + +## 协程:更轻量级的线程 + +**协程**可以理解**为一种轻量级的线程**。从操作系统的角度来看,线程是在内核态中调度的,而协程是在用户态调度的,所以相对于线程来说,协程切换的成本更低。协程虽然也有自己的栈,但是相比线程栈要小得多,典型的线程栈大小差不多有 1M,而协程栈的大小往往只有几 K 或者几十 K。所以,无论是从时间维度还是空间维度来看,协程都比线程轻量得多。 + +支持协程的语言还是挺多的,例如 Golang、Python、Lua、Kotlin 等都支持协程。 + +## CSP 模型:Golang 的主力队员 + +Golang 是一门号称从语言层面支持并发的编程语言,支持并发是 Golang 一个非常重要的特性。 + +Golang 支持协程,协程可以类比 Java 中的线程,解决并发问题的难点就在于线程(协程)之间的协作。那 Golang 是如何解决协作问题的呢? + +总的来说,Golang 提供了两种不同的方案:一种方案支持协程之间以共享内存的方式通信,Golang 提供了管程和原子类来对协程进行同步控制,这个方案与 Java 语言类似;另一种方案支持协程之间以消息传递(Message-Passing)的方式通信,本质上是要避免共享,Golang 的这个方案是基于** CSP**(Communicating Sequential Processes)模型实现的。Golang 比较推荐的方案是后者。 + +## 参考资料 + +- [极客时间教程 - Java 并发编程实战](https://time.geekbang.org/column/intro/100023901) \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\346\240\270\345\277\203\346\212\200\346\234\257\351\235\242\350\257\225\347\262\276\350\256\262.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\346\240\270\345\277\203\346\212\200\346\234\257\351\235\242\350\257\225\347\262\276\350\256\262.md" new file mode 100644 index 0000000000..048801edd7 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Java\346\240\270\345\277\203\346\212\200\346\234\257\351\235\242\350\257\225\347\262\276\350\256\262.md" @@ -0,0 +1,1435 @@ +--- +title: 《极客时间教程 - Java 核心技术面试精讲》笔记 +date: 2024-09-22 18:33:35 +categories: + - 笔记 + - Java +tags: + - Java + - 面试 +permalink: /pages/b57a2141/ +--- + +# 《极客时间教程 - Java 核心技术面试精讲》笔记 + +## 开篇词 以面试题为切入点,有效提升你的 Java 内功 + +略 + +## 谈谈你对 Java 平台的理解? + +【典型回答】 + +Java 最显著的特性: + +- “**书写一次,到处运行**”(Write once, run anywhere)——跨平台 +- **垃圾收集**(GC, Garbage Collection)——回收、分配内存 + +Java 既是解释型语言,又是编译型语言 + +【考点分析】 + +可以由浅入深的梳理 Java 的知识网络 + +【知识扩展】 + +## Exception 和 Error 有什么区别? + +【典型回答】 + +Exception 和 Error 都是继承了 Throwable 类,在 Java 中只有 Throwable 类型的实例才可以被抛出(throw)或者捕获(catch)。 + +Exception 是程序正常运行中,可以预料的意外情况,可能并且应该被捕获,进行相应处理。 + +Error 是指在正常情况下,不大可能出现的情况,绝大部分的 Error 都会导致程序(比如 JVM 自身)处于非正常的、不可恢复状态。既然是非正常情况,所以不便于也不需要捕获,常见的比如 OutOfMemoryError。 + +Exception 又分为**可检查**(checked)异常和**不检查**(unchecked)异常,可检查异常在源代码里必须显式地进行捕获处理,这是编译期检查的一部分。不检查异常就是所谓的运行时异常,类似 NullPointerException、ArrayIndexOutOfBoundsException。 + +【考点分析】 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240656683.png) + +**理解 Throwable、Exception、Error 的设计和分类**。 + +**理解 Java 语言中操作 Throwable 的元素和实践**。了解 `try {} catch {} finally`、try-with-resource、multiple catch、throw/throws 等关键机制。 + +【知识扩展】 + +尽量不要捕获 Exception、Throwable、Error——使得程序的容错处理不直观 + +不要生吞异常——会导致无法诊断问题 + +不要使用 e.printStackTrace()——这个方法会将堆栈输出到标准错误流,难以判断输出到哪里去了 + +## 谈谈 final、finally、finalize 有什么不同? + +【典型回答】+【考点分析】 + +final 可以用来修饰类、方法、变量,分别有不同的意义,final 修饰的 class 代表不可以继承扩展,final 的变量是不可以修改的,而 final 的方法也是不可以重写的。 + +finally 则是 Java 保证重点代码一定要被执行的一种机制。我们可以使用 try-finally 或者 try-catch-finally 来进行类似关闭 JDBC 连接、保证 unlock 锁等动作。 + +finalize 是基础类 java.lang.Object 的一个方法,它的设计目的是保证对象在被垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9 开始被标记为 deprecated。finalize 被设计成在对象被**垃圾收集前**调用,这就意味着实现了 finalize 方法的对象是个“特殊公民”,JVM 要对它进行额外处理。finalize 本质上成为了快速回收的阻碍者,可能导致你的对象经过多个垃圾收集周期才能被回收。使用不当会影响性能,导致程序死锁、挂起等。 + +【知识扩展】 + +**final 不等于 Immutable!** + +```java + final List strList = new ArrayList<>(); + strList.add("Hello"); + strList.add("world"); + List unmodifiableStrList = List.of("hello", "world"); + unmodifiableStrList.add("again"); +``` + +final 只能约束 strList 这个引用不可以被赋值,但是 strList 对象行为不被 final 影响,添加元素等操作是完全正常的。 + +要实现 Immutable,需要将类和类中的所有成员变量都定义为 final,并且只允许存在只读方法。 + +## 强引用、软引用、弱引用、幻象引用有什么区别? + +【典型回答】+【考点分析】+【知识扩展】 + +不同的引用类型,主要体现的是**对象不同的可达性(reachable)状态和对垃圾收集的影响**。 + +- **强引用(Strong Reference)** - 被强引用关联的对象不会被垃圾收集器回收。 +- **软引用(Soft Reference)** - 被软引用关联的对象,只有在内存不够的情况下才会被回收。 +- **弱引用(Weak Reference)** - 被弱引用关联的对象一定会被垃圾收集器回收,也就是说它只能存活到下一次垃圾收集发生之前。 +- **虚引用(Phantom Reference)** - 又称为幻象引用或幽灵引用。为一个对象设置虚引用关联的唯一目的就是能在这个对象被收集器回收时收到一个系统通知。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象实例。 + +## String、StringBuffer、StringBuilder 有什么区别? + +【典型回答】+【考点分析】+【知识扩展】 + +String 是典型的 Immutable 类,被声明成为 final class,所有属性也都是 final 的。也由于它的不可变性,类似拼接、裁剪字符串等动作,都会产生新的 String 对象。 + +StringBuffer 是线程安全的 String 工具类。 + +StringBuilder 和 StringBuffer 功能近似,只是去掉了保证线程安全的 synchronized 锁,减少了开销。 + +**字符串拼接都应该用 StringBuilder 吗?** + +每次对 `String` 类型进行改变的时候,都会生成一个新的 `String` 对象,然后将指针指向新的 `String` 对象。 + +下面一段代码,利用不同版本的 JDK 编译,然后再反编译,例如: + +```java +public class StringConcat { + public static String concat(String str) { + return str + “aa” + “bb”; + } +} +``` + +先编译再反编译,比如使用不同版本的 JDK: + +```java +${JAVA_HOME}/bin/javac StringConcat.java +${JAVA_HOME}/bin/javap -v StringConcat.class +``` + +JDK 8 的输出片段是: + +```java + 0: new #2 // class java/lang/StringBuilder + 3: dup + 4: invokespecial #3 // Method java/lang/StringBuilder."":()V + 7: aload_0 + 8: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; + 11: ldc #5 // String aa + 13: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; + 16: ldc #6 // String bb + 18: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder; + 21: invokevirtual #7 // Method java/lang/StringBuilder.toString:()Ljava/lang/String; +``` + +而在 JDK 9 中,反编译的结果就会有点特别了,片段是: + +```java + // concat method + 1: invokedynamic #2, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/String;)Ljava/lang/String; + + // ... + // 实际是利用了 MethodHandle, 统一了入口 + 0: #15 REF_invokeStatic java/lang/invoke/StringConcatFactory.makeConcatWithConstants:(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/invoke/MethodType;Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/invoke/CallSite; +``` + +字符串对象通过“+”的字符串拼接方式,实际上是通过 `StringBuilder` 调用 `append()` 方法实现的,拼接完成之后调用 `toString()` 得到一个 `String` 对象 。 + +不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:**编译器不会创建单个 `StringBuilder` 以复用,会导致创建过多的 `StringBuilder` 对象**。 + +在 JDK 9 中,字符串相加“+”改为用动态方法 `makeConcatWithConstants()` 来实现,通过提前分配空间从而减少了部分临时对象的创建。然而这种优化主要针对简单的字符串拼接,如: `a+b+c` 。对于循环中的大量拼接操作,仍然会逐个动态分配内存(类似于两个两个 append 的概念),并不如手动使用 StringBuilder 来进行拼接效率高。 + +**String 内部存储结构为何从 char 数组转为 byte 数组?** + +Java 中的 char 是两个 bytes 大小,拉丁语系语言的字符,根本就不需要太宽的 char,这样无区别的实现就造成了一定的浪费。 + +在 Java 9 中,引入了 Compact Strings 的设计,将数据存储方式从 char 数组,改变为一个 byte 数组加上一个标识编码的所谓 coder,并且将相关字符串操作类都进行了修改。紧凑字符串带来的优势,**即更小的内存占用、更快的操作速度**。 + +## 动态代理是基于什么原理? + +【典型回答】+【考点分析】+【知识扩展】 + +通过反射可以直接操作类或者对象,比如获取某个对象的类定义,获取类声明的属性和方法,调用方法或者构造对象,甚至可以运行时修改类定义。 + +反射工具类在 java.lang.reflect 包下,主要有:Class、Field、Method、Constructor 等。官方提供的参考文档:https://docs.oracle.com/javase/tutorial/reflect/index.html 。 + +反射提供的 AccessibleObject.setAccessible(boolean flag)。它的子类也大都重写了这个方法,这里的所谓 accessible 可以理解成修饰成员的 public、protected、private,这意味着我们可以在运行时修改成员访问限制! + +动态代理是一种方便运行时动态构建代理、动态处理代理方法调用的机制,很多场景都是利用类似机制做到的,比如用来包装 RPC 调用、面向切面的编程(AOP)。 + +实现动态代理的方式很多:JDK 动态代理、ASM、cglib、Javassist + +动态代理应用了设计模式中的代理模式。 + +## int 和 Integer 有什么区别? + +【典型回答】+【考点分析】+【知识扩展】 + +自动装箱、拆箱实际上可以视为一种语法糖。什么是语法糖?可以简单理解为 Java 平台为我们自动进行了一些转换,保证不同的写法在运行时等价,它们发生在编译阶段,也就是生成的字节码是一致的。 + +javac 替我们自动把装箱转换为 Integer.valueOf(),把拆箱替换为 Integer.intValue()。 + +```java +Integer integer = 1; +int unboxing = integer++; +``` + +反编译输出: + +```java +1: invokestatic #2 // Method +java/lang/Integer.valueOf:(I)Ljava/lang/Integer; +8: invokevirtual #3 // Method +java/lang/Integer.intValue:()I +``` + +应尽量避免自动装箱、拆箱行为,尤其是性能敏感的场景。 + +Java 基本数据类型的包装类型的大部分都用到了缓存机制来提升性能。 + +`Byte`,`Short`,`Integer`,`Long` 这 4 种包装类默认创建了数值 **[-128,127]** 的相应类型的缓存数据,`Character` 创建了数值在 **[0,127]** 范围的缓存数据,`Boolean` 直接返回 `True` or `False`。 + +Long 缓存源码: + +```java +public static Long valueOf(long l) { + final int offset = 128; + if (l >= -128 && l <= 127) { // will cache + return LongCache.cache[(int)l + offset]; + } + return new Long(l); +} + +private static class LongCache { + private LongCache(){} + + static final Long cache[] = new Long[-(-128) + 127 + 1]; + + static { + for(int i = 0; i < cache.length; i++) + cache[i] = new Long(i - 128); + } +} +``` + +Java 对于自动装箱和拆箱的设计,依赖于一种叫做享元模式的设计模式。 + +## 对比 Vector、ArrayList、LinkedList 有何区别? + +【典型回答】+【考点分析】+【知识扩展】 + +三者都是实现集合框架中的 List,具体功能也比较近似。 + +Vector 是 Java 早期提供的线程**安全的动态数组**,如果不需要线程安全,并不建议选择,毕竟同步是有额外开销的。Vector 内部是使用对象数组来保存数据,可以根据需要自动的增加容量,当数组已满时,会创建新的数组,并拷贝原有数组数据。 + +ArrayList 是应用更加广泛的**动态数组**实现,它本身不是线程安全的,所以性能要好很多。与 Vector 近似,ArrayList 也是可以根据需要调整容量,不过两者的调整逻辑有所区别,Vector 在扩容时会提高 1 倍,而 ArrayList 则是增加 50%。 + +LinkedList 顾名思义是 Java 提供的**双向链表**,所以它不需要像上面两种那样调整容量,它也不是线程安全的。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240657167.png) + +## 对比 Hashtable、HashMap、TreeMap 有什么不同? + +【典型回答】+【考点分析】+【知识扩展】 + +Hashtable 是早期 Java 类库提供的一个 [哈希表](https://zh.wikipedia.org/wiki/哈希表)实现,本身是同步的,不支持 null 键和值,由于同步导致的性能开销,所以已经很少被推荐使用。 + +HashMap 是应用更加广泛的哈希表实现,行为上大致上与 HashTable 一致,主要区别在于 HashMap 不是同步的,支持 null 键和值等。通常情况下,HashMap 进行 put 或者 get 操作,可以达到常数时间的性能,所以**它是绝大部分利用键值对存取场景的首选**,比如,实现一个用户 ID 和用户信息对应的运行时存储结构。 + +TreeMap 则是基于红黑树的一种提供顺序访问的 Map,和 HashMap 不同,它的 get、put、remove 之类操作都是 O(logN) 的时间复杂度,具体顺序可以由指定的 Comparator 或 Comparable 来决定,或者根据键的自然顺序来判断。 + +LinkedHashMap 通常提供的是遍历顺序符合插入顺序,它的实现是通过为条目(键值对)维护一个双向链表。 + +**HashMap 的性能表现非常依赖于哈希码的有效性,请务必掌握 hashCode 和 equals 的一些基本约定,**比如: + +- equals 相等,hashCode 一定要相等。 +- 重写了 hashCode 也要重写 equals。 +- hashCode 需要保持一致性,状态改变返回的哈希值仍然要一致。 +- equals 的对称、反射、传递等特性。 + +HashMap 源码实现: + +- 容量(capacity)和负载系数(load factor)。 +- 树化 。 + +## 如何保证集合是线程安全的 ConcurrentHashMap 如何实现高效地线程安全? + +【典型回答】+【考点分析】+【知识扩展】 + +### ConcurrentHashMap JDK7 实现 + +早期 ConcurrentHashMap,其实现是基于: + +- 分离锁,也就是将内部进行分段(Segment),里面则是 HashEntry 的数组,和 HashMap 类似,哈希相同的条目也是以链表形式存放。 +- HashEntry 内部使用 volatile 的 value 字段来保证可见性,也利用了不可变对象的机制以改进利用 Unsafe 提供的底层能力,比如 volatile access,去直接完成部分操作,以最优化性能,毕竟 Unsafe 中的很多操作都是 JVM intrinsic 优化过的。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240701236.png) + +```java +public V get(Object key) { + Segment s; // manually integrate access methods to reduce overhead + HashEntry[] tab; + int h = hash(key.hashCode()); + //利用位操作替换普通数学运算 + long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE; + // 以 Segment 为单位,进行定位 + // 利用 Unsafe 直接进行 volatile access + if ((s = (Segment)UNSAFE.getObjectVolatile(segments, u)) != null && + (tab = s.table) != null) { + //省略 + } + return null; + } +``` + +而对于 put 操作,首先是通过二次哈希避免哈希冲突,然后以 Unsafe 调用方式,直接获取相应的 Segment,然后进行线程安全的 put 操作: + +```java + public V put(K key, V value) { + Segment s; + if (value == null) + throw new NullPointerException(); + // 二次哈希,以保证数据的分散性,避免哈希冲突 + int hash = hash(key.hashCode()); + int j = (hash >>> segmentShift) & segmentMask; + if ((s = (Segment)UNSAFE.getObject // nonvolatile; recheck + (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment + s = ensureSegment(j); + return s.put(key, hash, value, false); + } +``` + +其核心逻辑实现在下面的内部方法中: + +```java +final V put(K key, int hash, V value, boolean onlyIfAbsent) { + // scanAndLockForPut 会去查找是否有 key 相同 Node + // 无论如何,确保获取锁 + HashEntry node = tryLock() ? null : + scanAndLockForPut(key, hash, value); + V oldValue; + try { + HashEntry[] tab = table; + int index = (tab.length - 1) & hash; + HashEntry first = entryAt(tab, index); + for (HashEntry e = first;;) { + if (e != null) { + K k; + // 更新已有 value... + } + else { + // 放置 HashEntry 到特定位置,如果超过阈值,进行 rehash + // ... + } + } + } finally { + unlock(); + } + return oldValue; + } +``` + +从上面的源码清晰的看出,在进行并发写操作时: + +- ConcurrentHashMap 会获取再入锁,以保证数据一致性,Segment 本身就是基于 ReentrantLock 的扩展实现,所以,在并发修改期间,相应 Segment 是被锁定的。 +- 在最初阶段,进行重复性的扫描,以确定相应 key 值是否已经在数组里面,进而决定是更新还是放置操作,你可以在代码里看到相应的注释。重复扫描、检测冲突是 ConcurrentHashMap 的常见技巧。 +- ConcurrentHashMap 也存在扩容行为。不过有一个明显区别,就是它进行的不是整体的扩容,而是单独对 Segment 进行扩容,细节就不介绍了。 + +### ConcurrentHashMap JDK8 实现 + +- 其内部仍然有 Segment 定义,但仅仅是为了保证序列化时的兼容性而已,不再有任何结构上的用处。 +- 因为不再使用 Segment,初始化操作大大简化,修改为 lazy-load 形式,这样可以有效避免初始开销,解决了老版本很多人抱怨的这一点。 +- 数据存储利用 volatile 来保证可见性。 +- 使用 CAS 等操作,在特定场景进行无锁并发操作。 +- 使用 Unsafe、LongAdder 之类底层手段,进行极端情况的优化。 + +先看看现在的数据存储内部实现,我们可以发现 Key 是 final 的,因为在生命周期中,一个条目的 Key 发生变化是不可能的;与此同时 val,则声明为 volatile,以保证可见性。 + +```java + static class Node implements Map.Entry { + final int hash; + final K key; + volatile V val; + volatile Node next; + // … + } +``` + +并发的 put 是如何实现的: + +```java +final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException(); + int hash = spread(key.hashCode()); + int binCount = 0; + for (Node[] tab = table;;) { + Node f; int n, i, fh; K fk; V fv; + if (tab == null || (n = tab.length) == 0) + tab = initTable(); + else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { + // 利用 CAS 去进行无锁线程安全操作,如果 bin 是空的 + if (casTabAt(tab, i, null, new Node(hash, key, value))) + break; + } + else if ((fh = f.hash) == MOVED) + tab = helpTransfer(tab, f); + else if (onlyIfAbsent // 不加锁,进行检查 + && fh == hash + && ((fk = f.key) == key || (fk != null && key.equals(fk))) + && (fv = f.val) != null) + return fv; + else { + V oldVal = null; + synchronized (f) { + // 细粒度的同步修改操作。.. + } + } + // Bin 超过阈值,进行树化 + if (binCount != 0) { + if (binCount >= TREEIFY_THRESHOLD) + treeifyBin(tab, i); + if (oldVal != null) + return oldVal; + break; + } + } + } + addCount(1L, binCount); + return null; +} +``` + +初始化操作实现在 initTable 里面,这是一个典型的 CAS 使用场景,利用 volatile 的 sizeCtl 作为互斥手段:如果发现竞争性的初始化,就 spin 在那里,等待条件恢复;否则利用 CAS 设置排他标志。如果成功则进行初始化;否则重试。 + +请参考下面代码: + +```java +private final Node[] initTable() { + Node[] tab; int sc; + while ((tab = table) == null || tab.length == 0) { + // 如果发现冲突,进行 spin 等待 + if ((sc = sizeCtl) < 0) + Thread.yield(); + // CAS 成功返回 true,则进入真正的初始化逻辑 + else if (U.compareAndSetInt(this, SIZECTL, sc, -1)) { + try { + if ((tab = table) == null || tab.length == 0) { + int n = (sc > 0) ? sc : DEFAULT_CAPACITY; + @SuppressWarnings("unchecked") + Node[] nt = (Node[])new Node[n]; + table = tab = nt; + sc = n - (n >>> 2); + } + } finally { + sizeCtl = sc; + } + break; + } + } + return tab; +} +``` + +## Java 提供了哪些 IO 方式? NIO 如何实现多路复用? + +【典型回答】+【考点分析】+【知识扩展】 + +- BIO - 传统的 java.io 包,它基于流模型实现。很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。 + - InputStream/OutputStream 和 Reader/Writer 的关系和区别。 +- NO - 在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。 + - BIO 和 NIO 的设计、原理差异 + - NIO 为什么高性能 + - NIO 组成 + - Buffer,高效的数据容器,除了布尔类型,所有原始数据类型都有相应的 Buffer 实现。 + - Channel,类似在 Linux 之类操作系统上看到的文件描述符,是 NIO 中被用来支持批量式 IO 操作的一种抽象。 + - Selector,是 NIO 实现多路复用的基础,它提供了一种高效的机制,可以检测到注册在 Selector 上的多个 Channel 中,是否有 Channel 处于就绪状态,进而实现了单线程对多 Channel 的高效管理。Selector 同样是基于底层操作系统机制,不同模式、不同版本都存在区别:Linux 上依赖于 [epoll](http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/linux/classes/sun/nio/ch/EPollSelectorImpl.java),Windows 上 NIO2(AIO)模式则是依赖于 [iocp](http://hg.openjdk.java.net/jdk/jdk/file/d8327f838b88/src/java.base/windows/classes/sun/nio/ch/Iocp.java)。 +- NIO2 - 在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。 + +## Java 有几种文件拷贝方式?哪一种最高效? + +【典型回答】 + +字节流方式: + +```java +public static void copyFileByStream(File source, File dest) throws + IOException { + try (InputStream is = new FileInputStream(source); + OutputStream os = new FileOutputStream(dest);){ + byte[] buffer = new byte[1024]; + int length; + while ((length = is.read(buffer)) > 0) { + os.write(buffer, 0, length); + } + } + } +``` + +NIO 方式: + +```java +public static void copyFileByChannel(File source, File dest) throws + IOException { + try (FileChannel sourceChannel = new FileInputStream(source) + .getChannel(); + FileChannel targetChannel = new FileOutputStream(dest).getChannel + ();){ + for (long count = sourceChannel.size() ;count>0 ;) { + long transferred = sourceChannel.transferTo( + sourceChannel.position(), count, targetChannel); sourceChannel.position(sourceChannel.position() + transferred); + count -= transferred; + } + } + } +``` + +【考点分析】 + +- 不同的 copy 方式,底层机制有什么区别? +- 为什么零拷贝(zero-copy)可能有性能优势? +- Buffer 分类与使用。 +- Direct Buffer 对垃圾收集等方面的影响与实践选择。 + +【知识扩展】 + +首先,你需要理解用户态空间(User Space)和内核态空间(Kernel Space),这是操作系统层面的基本概念,操作系统内核、硬件驱动等运行在内核态空间,具有相对高的特权;而用户态空间,则是给普通应用和服务使用。你可以参考:https://en.wikipedia.org/wiki/User_space。 + +当我们使用输入输出流进行读写时,实际上是进行了多次上下文切换,比如应用读取数据时,先在内核态将数据从磁盘读取到内核缓存,再切换到用户态将数据从内核缓存读取到用户缓存。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240702998.png) + +基于 NIO transferTo 的实现方式,在 Linux 和 Unix 上,则会使用到零拷贝技术,数据传输并不需要用户态参与,省去了上下文切换的开销和不必要的内存拷贝,进而可能提高应用拷贝性能。注意,transferTo 不仅仅是可以用在文件拷贝中,与其类似的,例如读取磁盘文件,然后进行 Socket 发送,同样可以享受这种机制带来的性能和扩展性提高。 + +transferTo 的传输过程是: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240702146.png) + +## 谈谈接口和抽象类有什么区别? + +【典型回答】+【考点分析】 + +接口是对行为的抽象,它是抽象方法的集合,利用接口可以达到 API 定义和实现分离的目的。接口,不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;同时,没有非静态方法实现,也就是说要么是抽象方法,要么是静态方法。 + +抽象类是不能实例化的类,用 abstract 关键字修饰 class,其目的主要是代码重用。除了不能实例化,形式上和一般的 Java 类并没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。 + +【知识扩展】 + +Java 相比于其他面向对象语言,**Java 不支持多继承**。 + +Java 8 增加了函数式编程的支持,所以又增加了一类定义,即所谓 functional interface,简单说就是只有一个抽象方法的接口,通常建议使用 @FunctionalInterface Annotation 来标记。Lambda 表达式本身可以看作是一类 functional interface,某种程度上这和面向对象可以算是两码事。我们熟知的 Runnable、Callable 之类,都是 functional interface。 + +从 Java 8 开始,interface 增加了对 default method 的支持。Java 9 以后,甚至可以定义 private default method。Default method 提供了一种二进制兼容的扩展已有接口的办法。 + +面向对象设计: + +- **封装**的目的是隐藏事务内部的实现细节,以便提高安全性和简化编程。封装提供了合理的边界,避免外部调用者接触到内部的细节。 +- **继承**是代码复用的基础机制,但要注意,继承可以看作是非常紧耦合的一种关系,父类代码修改,子类行为也会变动。在实践中,过度滥用继承,可能会起到反效果。 +- **多态**,你可能立即会想到重写(override)和重载(overload)、向上转型。简单说,重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数。 + +面向对象设计原则(S.O.L.I.D) + +- 单一职责(Single Responsibility),类或者对象最好是只有单一职责,在程序设计中如果发现某个类承担着多种义务,可以考虑进行拆分。 +- 开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。换句话说,程序设计应保证平滑的扩展性,尽量避免因为新增同类功能而修改已有实现,这样可以少产出些回归(regression)问题。 +- 里氏替换(Liskov Substitution),这是面向对象的基本要素之一,进行继承关系抽象时,凡是可以用父类或者基类的地方,都可以用子类替换。 +- 接口分离(Interface Segregation),我们在进行类和接口设计时,如果在一个接口里定义了太多方法,其子类很可能面临两难,就是只有部分方法对它是有意义的,这就破坏了程序的内聚性。 +- 依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。也就是说高层次模块,不应该依赖于低层次模块,而是应该基于抽象。实践这一原则是保证产品代码之间适当耦合度的法宝。 + +## 谈谈你知道的设计模式? + +【典型回答】+【考点分析】+【知识扩展】 + +设计模式可以分为创建型模式、结构型模式和行为型模式。 + +- 创建型模式,是对对象创建过程的各种问题和解决方案的总结,包括:各种工厂模式(Factory、Abstract Factory)、单例模式(Singleton)、构建器模式(Builder)、原型模式(ProtoType)。 +- 结构型模式,是针对软件设计结构的总结,关注于类、对象继承、组合方式的实践经验。常见的结构型模式,包括:桥接模式(Bridge)、适配器模式(Adapter)、装饰者模式(Decorator)、代理模式(Proxy)、组合模式(Composite)、外观模式(Facade)、享元模式(Flyweight)等。 +- 行为型模式,是从类或对象之间交互、职责划分等角度总结的模式。比较常见的行为型模式有策略模式(Strategy)、解释器模式(Interpreter)、命令模式(Command)、观察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、访问者模式(Visitor)。 + +## synchronized 和 ReentrantLock 有什么区别呢? + +【典型回答】+【考点分析】+【知识扩展】 + +synchronized 和 ReentrantLock 的语义基本相同。 + +synchronized 是内置锁,ReentrantLock 是显式锁,二者的差异有: + +- **主动获取锁和释放锁** + - `synchronized` 不能主动获取锁和释放锁。获取锁和释放锁都是 JVM 控制的。 + - `ReentrantLock` 可以主动获取锁和释放锁。(如果忘记释放锁,就可能产生死锁)。 +- **响应中断** + - `synchronized` 不能响应中断。 + - `ReentrantLock` 可以响应中断。 +- **超时机制** + - `synchronized` 没有超时机制。 + - `ReentrantLock` 有超时机制。`ReentrantLock` 可以设置超时时间,超时后自动释放锁,避免一直等待。 +- **支持公平锁** + - `synchronized` 只支持非公平锁。 + - `ReentrantLock` 支持非公平锁和公平锁。 +- **是否支持共享** + - 被 `synchronized` 修饰的方法或代码块,只能被一个线程访问(独享)。如果这个线程被阻塞,其他线程也只能等待 + - `ReentrantLock` 可以基于 `Condition` 灵活的控制同步条件。 + +## synchronized 底层如何实现?什么是锁的升级、降级? + +【典型回答】+【考点分析】+【知识扩展】 + +synchronized 代码块是由一对儿 monitorenter/monitorexit 指令实现的,Monitor 对象是同步的基本实现 [单元](https://docs.oracle.com/javase/specs/jls/se10/html/jls-8.html#d5e13622)。 + +JDK6 以前,由于 synchronized 阻塞度高,导致性能不佳。JDK6 对此,进行了大量优化,其性能与 `ReentrantLock` 已基本持平。 + +在 JDK1.6 JVM 中,对象实例在堆内存中被分为了三个部分:对象头、实例数据和对齐填充。其中 Java 对象头由 Mark Word、指向类的指针以及数组长度三部分组成。 + +Mark Word 记录了对象和锁有关的信息。Mark Word 在 64 位 JVM 中的长度是 64bit,我们可以一起看下 64 位 JVM 的存储结构是怎么样的。如下图所示: + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20200629191250.png) + +锁升级功能主要依赖于 Mark Word 中的锁标志位和释放偏向锁标志位,`synchronized` 同步锁就是从偏向锁开始的,随着竞争越来越激烈,偏向锁升级到轻量级锁,最终升级到重量级锁。 + +Java 1.6 引入了偏向锁和轻量级锁,从而让 `synchronized` 拥有了四个状态: + +- **无锁状态(unlocked)** +- **偏向锁状态(biasble)** +- **轻量级锁状态(lightweight locked)** +- **重量级锁状态(inflated)** + +当 JVM 检测到不同的竞争状况时,会自动切换到适合的锁实现。 + +当没有竞争出现时,默认会使用偏向锁。JVM 会利用 CAS 操作(compare and swap),在对象头上的 Mark Word 部分设置线程 ID,以表示这个对象偏向于当前线程,所以并不涉及真正的互斥锁。这样做的假设是基于在很多应用场景中,大部分对象生命周期中最多会被一个线程锁定,使用偏斜锁可以降低无竞争开销。 + +如果有另外的线程试图锁定某个已经被偏斜过的对象,JVM 就需要撤销(revoke)偏向锁,并切换到轻量级锁实现。轻量级锁依赖 CAS 操作 Mark Word 来试图获取锁,如果重试成功,就使用普通的轻量级锁;否则,进一步升级为重量级锁。 + +## 一个线程两次调用 start() 方法会出现什么情况? + +【典型回答】+【考点分析】+【知识扩展】 + +Java 的线程是不允许调用 start() 两次的,第二次调用必然会抛出 IllegalThreadStateException。 + +线程是系统调度的最小单元,一个进程可以包含多个线程,作为任务的真正运作者,有自己的栈(Stack)、寄存器(Register)、本地存储(Thread Local)等,但是会和进程内其他线程共享文件描述符、虚拟地址空间等。 + +线程还分为内核线程、用户线程,Java 的线程实现其实是与虚拟机相关的。对于我们最熟悉的 Sun/Oracle JDK,其线程也经历了一个演进过程,基本上在 Java 1.2 之后,JDK 已经抛弃了所谓的 [Green Thread](https://en.wikipedia.org/wiki/Green_threads),也就是用户调度的线程,现在的模型是一对一映射到操作系统内核线程。 + +如果我们来看 Thread 的源码,你会发现其基本操作逻辑大都是以 JNI 形式调用的本地代码。 + +```java +private native void start0(); +private native void setPriority0(int newPriority); +private native void interrupt0(); +``` + +近几年的 Go 语言等提供了协程([coroutine](https://en.wikipedia.org/wiki/Coroutine)),大大提高了构建并发应用的效率。于此同时,Java 也在 [Loom](http://openjdk.java.net/projects/loom/) 项目中,孕育新的类似轻量级用户线程(Fiber)等机制,也许在不久的将来就可以在新版 JDK 中使用到它。 + +### 线程生命周期 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202408290809602.png) + +`java.lang.Thread.State` 中定义了 **6** 种不同的线程状态,在给定的一个时刻,线程只能处于其中的一个状态。 + +以下是各状态的说明,以及状态间的联系: + +- **开始(NEW)** - 尚未调用 `start` 方法的线程处于此状态。此状态意味着:**创建的线程尚未启动**。 +- **可运行(RUNNABLE)** - 已经调用了 `start` 方法的线程处于此状态。此状态意味着,**线程已经准备好了**,一旦被线程调度器分配了 CPU 时间片,就可以运行线程。 + - 在操作系统层面,线程有 READY 和 RUNNING 状态;而在 JVM 层面,只能看到 RUNNABLE 状态,所以 Java 系统一般将这两个状态统称为 RUNNABLE(运行中) 状态 。 +- **阻塞(BLOCKED)** - 此状态意味着:**线程处于被阻塞状态**。表示线程在等待 `synchronized` 的隐式锁(Monitor lock)。`synchronized` 修饰的方法、代码块同一时刻只允许一个线程执行,其他线程只能等待,即处于阻塞状态。当占用 `synchronized` 隐式锁的线程释放锁,并且等待的线程获得 `synchronized` 隐式锁时,就又会从 `BLOCKED` 转换到 `RUNNABLE` 状态。 +- **等待(WAITING)** - 此状态意味着:**线程无限期等待,直到被其他线程显式地唤醒**。 阻塞和等待的区别在于,阻塞是被动的,它是在等待获取 `synchronized` 的隐式锁。而等待是主动的,通过调用 `Object.wait` 等方法进入。 + - 进入:`Object.wait()`;退出:`Object.notify` / `Object.notifyAll` + - 进入:`Thread.join()`;退出:被调用的线程执行完毕 + - 进入:`LockSupport.park()`;退出:`LockSupport.unpark` +- **定时等待(TIMED_WAITING)** - 等待指定时间的状态。一个线程处于定时等待状态,是由于执行了以下方法中的任意方法: + - 进入:`Thread.sleep(long)`;退出:时间结束 + - 进入:`Object.wait(long)`;退出:时间结束 / `Object.notify` / `Object.notifyAll` + - 进入:`Thread.join(long)`;退出:时间结束 / 被调用的线程执行完毕 + - 进入:`LockSupport.parkNanos(long)`;退出:`LockSupport.unpark` + - 进入:`LockSupport.parkUntil(long)`;退出:`LockSupport.unpark` +- **终止 (TERMINATED)** - 线程 `run()` 方法执行结束,或者因异常退出了 `run()` 方法,则该线程结束生命周期。死亡的线程不可再次复生。 + +## 什么情况下 Java 程序会产生死锁?如何定位、修复? + +【典型回答】+【考点分析】+【知识扩展】 + +### 什么是死锁 + +**死锁**:**一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象**。 + +死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的锁,而永久处于阻塞的状态。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409050712813.png) + +### 如何检测死锁 + +首先,可以使用 jps 或者系统的 ps 命令、任务管理器等工具,确定进程 ID。 + +其次,调用 jstack 获取线程栈: + +```java +${JAVA_HOME}\bin\jstack your_pid +``` + +然后,分析得到的输出,具体片段如下: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240702087.png) + +最后,结合代码分析线程栈信息。上面这个输出非常明显,找到处于 BLOCKED 状态的线程,按照试图获取(waiting)的锁 ID(请看我标记为相同颜色的数字)查找,很快就定位问题。 jstack 本身也会把类似的简单死锁抽取出来,直接打印出来。 + +识别死锁总体上可以理解为:**区分线程状态 -> 查看等待目标 -> 对比 Monitor 等持有状态** + +### 如何避免死锁 + +只有以下这四个条件都发生时才会出现死锁: + +- **互斥**,共享资源 X 和 Y 只能被一个线程占用; +- **占有且等待**,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X; +- **不可抢占**,其他线程不能强行抢占线程 T1 占有的资源; +- **循环等待**,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。 + +**也就是说只要破坏任意一个,就可以避免死锁的发生**。 + +其中,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥。不过其他三个条件都是有办法破坏掉的,到底如何做呢? + +1. 对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。 +2. 对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。超时释放锁 +3. 对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。 + +## Java 并发包提供了哪些并发工具类? + +【典型回答】+【考点分析】+【知识扩展】 + +J.U.C 提供了以下方面的工具: + +- 提供了比 synchronized 更加高级的各种同步结构,包括 CountDownLatch、CyclicBarrier、Semaphore 等,可以实现更加丰富的多线程操作,比如利用 Semaphore 作为资源控制器,限制同时进行工作的线程数量。 +- 各种线程安全的容器,比如最常见的 ConcurrentHashMap、有序的 ConcurrentSkipListMap,或者通过类似快照机制,实现线程安全的动态数组 CopyOnWriteArrayList 等。 +- 各种并发队列实现,如各种 BlockingQueue 实现,比较典型的 ArrayBlockingQueue、 SynchronousQueue 或针对特定场景的 PriorityBlockingQueue 等。 +- 强大的 Executor 框架,可以创建各种不同类型的线程池,调度任务运行等,绝大部分情况下,不再需要自己从头实现线程池和任务调度器。 + +同步工具: + +- [CountDownLatch](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/CountDownLatch.html),允许一个或多个线程等待某些操作完成。 +- [CyclicBarrier](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/CyclicBarrier.html),一种辅助性的同步结构,允许多个线程等待到达某个屏障。 +- [Semaphore](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Semaphore.html),Java 版本的信号量实现。 + +## 并发包中的 ConcurrentLinkedQueue 和 LinkedBlockingQueue 有什么区别? + +【典型回答】+【考点分析】+【知识扩展】 + +关于问题中它们的区别: + +- Concurrent 类型基于 lock-free,在常见的多线程访问场景,一般可以提供较高吞吐量。 +- 而 LinkedBlockingQueue 内部则是基于锁,并提供了 BlockingQueue 的等待性方法。 + +J.U.C 包提供的容器(Queue、List、Set)、Map,从命名上可以大概区分为 Concurrent\*、CopyOnWrite 和 Blocking 等三类,同样是线程安全容器,可以简单认为: + +- Concurrent 类型没有类似 CopyOnWrite 之类容器相对较重的修改开销。 +- 但是,凡事都是有代价的,Concurrent 往往提供了较低的遍历一致性。你可以这样理解所谓的弱一致性,例如,当利用迭代器遍历时,如果容器发生修改,迭代器仍然可以继续进行遍历。 +- 与弱一致性对应的,就是我介绍过的同步容器常见的行为“fail-fast”,也就是检测到容器在遍历过程中发生了修改,则抛出 ConcurrentModificationException,不再继续遍历。 +- 弱一致性的另外一个体现是,size 等操作准确性是有限的,未必是 100% 准确。 +- 与此同时,读取的性能具有一定的不确定性。 + +下面这张图是 Java 并发类库提供的各种各样的**线程安全**队列实现,注意,图中并未将非线程安全部分包含进来。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240702121.png) + +我们可以从不同的角度进行分类,从基本的数据结构的角度分析,有两个特别的 [Deque](https://docs.oracle.com/javase/9/docs/api/java/util/Deque.html) 实现,ConcurrentLinkedDeque 和 LinkedBlockingDeque。Deque 的侧重点是支持对队列头尾都进行插入和删除,所以提供了特定的方法,如: + +- 尾部插入时需要的 [addLast(e)](https://docs.oracle.com/javase/9/docs/api/java/util/Deque.html#addLast-E-)、[offerLast(e)](https://docs.oracle.com/javase/9/docs/api/java/util/Deque.html#offerLast-E-)。 +- 尾部删除所需要的 [removeLast()](https://docs.oracle.com/javase/9/docs/api/java/util/Deque.html#removeLast--)、[pollLast()](https://docs.oracle.com/javase/9/docs/api/java/util/Deque.html#pollLast--)。 + +队列是否有界、无界: + +- ArrayBlockingQueue 是最典型的的有界队列,其内部以 final 的数组保存数据,数组的大小就决定了队列的边界,所以我们在创建 ArrayBlockingQueue 时,都要指定容量,如 + +```java +public ArrayBlockingQueue(int capacity, boolean fair) +``` + +- LinkedBlockingQueue,容易被误解为无边界,但其实其行为和内部代码都是基于有界的逻辑实现的,只不过如果我们没有在创建队列时就指定容量,那么其容量限制就自动被设置为 Integer.MAX_VALUE,成为了无界队列。 +- SynchronousQueue,这是一个非常奇葩的队列实现,每个删除操作都要等待插入操作,反之每个插入操作也都要等待删除动作。那么这个队列的容量是多少呢?是 1 吗?其实不是的,其内部容量是 0。 +- PriorityBlockingQueue 是无边界的优先队列,虽然严格意义上来讲,其大小总归是要受系统资源影响。 +- DelayedQueue 和 LinkedTransferQueue 同样是无边界的队列。对于无边界的队列,有一个自然的结果,就是 put 操作永远也不会发生其他 BlockingQueue 的那种等待情况。 + +如果我们分析不同队列的底层实现,BlockingQueue 基本都是基于锁实现。 + +## Java 并发类库提供的线程池有哪几种? 分别有什么特点? + +【典型回答】+【考点分析】+【知识扩展】 + +Executors 目前提供了 5 种不同的线程池创建配置: + +- `CachedThreadPool`,它是一种用来处理大量短时间工作任务的线程池,具有几个鲜明特点:它会试图缓存线程并重用,当无缓存线程可用时,就会创建新的工作线程;如果线程闲置的时间超过 60 秒,则被终止并移出缓存;长时间闲置时,这种线程池,不会消耗什么资源。其内部使用 `SynchronousQueue` 作为工作队列。 +- `FixedThreadPool`,重用指定数目(nThreads)的线程,其背后使用的是无界的工作队列,任何时候最多有 nThreads 个工作线程是活动的。这意味着,如果任务数量超过了活动队列数目,将在工作队列中等待空闲线程出现;如果有工作线程退出,将会有新的工作线程被创建,以补足指定的数目 nThreads。 +- `SingleThreadExecutor`,它的特点在于工作线程数目被限制为 1,操作一个无界的工作队列,所以它保证了所有任务的都是被顺序执行,最多会有一个任务处于活动状态,并且不允许使用者改动线程池实例,因此可以避免其改变线程数目。 +- `SingleThreadScheduledExecutor` 和 `ScheduledThreadPool`,创建的是个 `ScheduledExecutorService`,可以进行定时或周期性的工作调度,区别在于单一工作线程还是多个工作线程。 +- `WorkStealingPool`,这是一个经常被人忽略的线程池,Java 8 才加入这个创建方法,其内部会构建 [ForkJoinPool](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/ForkJoinPool.html),利用 [Work-Stealing](https://en.wikipedia.org/wiki/Work_stealing) 算法,并行地处理任务,不保证处理顺序。 + +Executor 框架的基本组成,请参考下面的类图。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240703740.png) + +- Executor 是一个基础的接口,其初衷是将任务提交和任务执行细节解耦,这一点可以体会其定义的唯一方法。 + +```java +void execute(Runnable command); +``` + +Executor 的设计是源于 Java 早期线程 API 使用的教训,开发者在实现应用逻辑时,被太多线程创建、调度等不相关细节所打扰。就像我们进行 HTTP 通信,如果还需要自己操作 TCP 握手,开发效率低下,质量也难以保证。 + +- ExecutorService 则更加完善,不仅提供 service 的管理功能,比如 shutdown 等方法,也提供了更加全面的提交任务机制,如返回 [Future](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/Future.html) 而不是 void 的 submit 方法。 + +```java + Future submit(Callable task); +``` + +线程池设计: + +- 工作队列负责存储用户提交的各个任务,这个工作队列,可以是容量为 0 的 SynchronousQueue(使用 newCachedThreadPool),也可以是像固定大小线程池(newFixedThreadPool)那样使用 LinkedBlockingQueue。 + +```java +private final BlockingQueue workQueue; +``` + +- 内部的“线程池”,这是指保持工作线程的集合,线程池需要在运行过程中管理线程创建、销毁。例如,对于带缓存的线程池,当任务压力较大时,线程池会创建新的工作线程;当业务压力退去,线程池会在闲置一段时间(默认 60 秒)后结束线程。 + +```java +private final HashSet workers = new HashSet<>(); +``` + +线程池的工作线程被抽象为静态内部类 Worker,基于 [AQS](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/locks/AbstractQueuedSynchronizer.html) 实现。 + +- ThreadFactory 提供上面所需要的创建线程逻辑。 +- 如果任务提交时被拒绝,比如线程池已经处于 SHUTDOWN 状态,需要为其提供处理逻辑,Java 标准库提供了类似 [ThreadPoolExecutor.AbortPolicy](https://docs.oracle.com/javase/9/docs/api/java/util/concurrent/ThreadPoolExecutor.AbortPolicy.html) 等默认实现,也可以按照实际需求自定义。 + +## AtomicInteger 底层实现原理是什么?如何在自己的产品代码中应用 CAS 操作? + +【典型回答】+【考点分析】+【知识扩展】 + +原子类基于 CAS([compare-and-swap](https://en.wikipedia.org/wiki/Compare-and-swap))技术。从其代码来看,它依赖于 Unsafe 提供的一些底层能力。 + +```java +private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe(); +private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value"); +private volatile int value; +``` + +CAS 底层实现依赖于 CPU 提供的特定原子指令,具体根据体系结构的不同还存在着明显区别。 + +CAS 的问题: + +- 如果并发冲突频繁,CAS 反复自旋重试,会大量消耗 CPU +- ABA 问题——可以通过 `AtomicStampedReference` 解决(增加时间戳、版本号来识别)。 + +AQS 内部数据和方法,可以简单拆分为: + +- 一个 volatile 的整数成员表征状态,同时提供了 setState 和 getState 方法 + +```java +private volatile int state; +``` + +- 一个先入先出(FIFO)的等待线程队列,以实现多线程间竞争和等待,这是 AQS 机制的核心之一。 +- 各种基于 CAS 的基础操作方法,以及各种期望具体同步结构去实现的 acquire/release 方法。 + +利用 AQS 实现一个同步结构,至少要实现两个基本类型的方法,分别是 acquire 操作,获取资源的独占权;还有就是 release 操作,释放对某个资源的独占。 + +以 ReentrantLock 为例,它内部通过扩展 AQS 实现了 Sync 类型,以 AQS 的 state 来反映锁的持有情况。 + +```java +private final Sync sync; +abstract static class Sync extends AbstractQueuedSynchronizer { …} +``` + +下面是 ReentrantLock 对应 acquire 和 release 操作,如果是 CountDownLatch 则可以看作是 await()/countDown(),具体实现也有区别。 + +```java +public void lock() { + sync.acquire(1); +} +public void unlock() { + sync.release(1); +} +``` + +排除掉一些细节,整体地分析 acquire 方法逻辑,其直接实现是在 AQS 内部,调用了 tryAcquire 和 acquireQueued,这是两个需要搞清楚的基本部分。 + +```java +public final void acquire(int arg) { + if (!tryAcquire(arg) && + acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) + selfInterrupt(); +} +``` + +首先,我们来看看 tryAcquire。在 ReentrantLock 中,tryAcquire 逻辑实现在 NonfairSync 和 FairSync 中,分别提供了进一步的非公平或公平性方法,而 AQS 内部 tryAcquire 仅仅是个接近未实现的方法(直接抛异常),这是留个实现者自己定义的操作。 + +我们可以看到公平性在 ReentrantLock 构建时如何指定的,具体如下: + +```java +public ReentrantLock() { + sync = new NonfairSync(); // 默认是非公平的 +} +public ReentrantLock(boolean fair) { + sync = fair ? new FairSync() : new NonfairSync(); +} +``` + +以非公平的 tryAcquire 为例,其内部实现了如何配合状态与 CAS 获取锁,注意,对比公平版本的 tryAcquire,它在锁无人占有时,并不检查是否有其他等待者,这里体现了非公平的语义。 + +```java +final boolean nonfairTryAcquire(int acquires) { + final Thread current = Thread.currentThread(); + int c = getState();// 获取当前 AQS 内部状态量 + if (c == 0) { // 0 表示无人占有,则直接用 CAS 修改状态位, + if (compareAndSetState(0, acquires)) {// 不检查排队情况,直接争抢 + setExclusiveOwnerThread(current); //并设置当前线程独占锁 + return true; + } + } else if (current == getExclusiveOwnerThread()) { //即使状态不是 0,也可能当前线程是锁持有者,因为这是再入锁 + int nextc = c + acquires; + if (nextc < 0) // overflow + throw new Error("Maximum lock count exceeded"); + setState(nextc); + return true; + } + return false; +} +``` + +接下来我再来分析 acquireQueued,如果前面的 tryAcquire 失败,代表着锁争抢失败,进入排队竞争阶段。这里就是我们所说的,利用 FIFO 队列,实现线程间对锁的竞争的部分,算是是 AQS 的核心逻辑。 + +当前线程会被包装成为一个排他模式的节点(EXCLUSIVE),通过 addWaiter 方法添加到队列中。acquireQueued 的逻辑,简要来说,就是如果当前节点的前面是头节点,则试图获取锁,一切顺利则成为新的头节点;否则,有必要则等待,具体处理逻辑请参考我添加的注释。 + +```java +final boolean acquireQueued(final Node node, int arg) { + boolean interrupted = false; + try { + for (;;) {// 循环 + final Node p = node.predecessor();// 获取前一个节点 + if (p == head && tryAcquire(arg)) { // 如果前一个节点是头结点,表示当前节点合适去 tryAcquire + setHead(node); // acquire 成功,则设置新的头节点 + p.next = null; // 将前面节点对当前节点的引用清空 + return interrupted; + } + if (shouldParkAfterFailedAcquire(p, node)) // 检查是否失败后需要 park + interrupted |= parkAndCheckInterrupt(); + } + } catch (Throwable t) { + cancelAcquire(node);// 出现异常,取消 + if (interrupted) + selfInterrupt(); + throw t; + } +} +``` + +到这里线程试图获取锁的过程基本展现出来了,tryAcquire 是按照特定场景需要开发者去实现的部分,而线程间竞争则是 AQS 通过 Waiter 队列与 acquireQueued 提供的,在 release 方法中,同样会对队列进行对应操作。 + +## 请介绍类加载过程,什么是双亲委派模型? + +【典型回答】+【考点分析】+【知识扩展】 + +类加载过程: + +- 加载 - 将字节码数据从不同的数据源读取到 JVM 中,并映射为 JVM 认可的数据结构(Class 对象)。 +- 链接 + - 验证 - 核验字节信息是符合 Java 虚拟机规范。 + - 准备 - 创建类或接口中的静态变量,并初始化静态变量的初始值。 + - 解析 - 将常量池中的符号引用(symbolic reference)替换为直接引用。 +- 初始化 - 真正去执行类初始化的代码逻辑,包括静态字段赋值的动作,以及执行类定义中的静态初始化块内的逻辑,编译器在编译阶段就会把这部分逻辑整理好,父类型的初始化逻辑优先于当前类型的逻辑。 + +双亲委派 + +- Bootstrap ClassLoader - 负责加载 /jre/lib 路径下的 jar。可以通过 `java -Xbootclasspath` 参数修改扫描路径。 +- Ext ClassLoader - 负责加载 `/jre/lib/ext` 路径下的 jar。可以通过 `-Djava.ext.dirs` 参数修改扫描路径。 +- App ClassLoaer- 负责加载 classpath 路径下的内容。可以通过 -Djava.system.class.loader 参数修改扫描路径。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240704663.png) + +通常类加载机制有三个基本特征: + +- 双亲委派模型。但不是所有类加载都遵守这个模型,有的时候,启动类加载器所加载的类型,是可能要加载用户代码的,比如 JDK 内部的 ServiceProvider/[ServiceLoader](https://docs.oracle.com/javase/9/docs/api/java/util/ServiceLoader.html) 机制,用户可以在标准 API 框架上,提供自己的实现,JDK 也需要提供些默认的参考实现。 例如,Java 中 JNDI、JDBC、文件系统、Cipher 等很多方面,都是利用的这种机制,这种情况就不会用双亲委派模型去加载,而是利用所谓的上下文加载器。 +- 可见性,子类加载器可以访问父加载器加载的类型,但是反过来是不允许的,不然,因为缺少必要的隔离,我们就没有办法利用类加载器去实现容器的逻辑。 +- 单一性,由于父加载器的类型对于子加载器是可见的,所以父加载器中加载过的类型,就不会在子加载器中重复加载。但是注意,类加载器“邻居”间,同一类型仍然可以被加载多次,因为互相并不可见。 + +在 JDK 9 中,由于 Jigsaw 项目引入了 Java 平台模块化系统(JPMS),Java SE 的源代码被划分为一系列模块。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240704856.png) + +类加载器,类文件容器等都发生了非常大的变化: + +- -Xbootclasspath 参数不可用了。API 已经被划分到具体的模块,所以上文中,利用“-Xbootclasspath/p”替换某个 Java 核心类型代码,实际上变成了对相应的模块进行的修补,可以采用下面的解决方案: + +首先,确认要修改的类文件已经编译好,并按照对应模块(假设是 java.base)结构存放, 然后,给模块打补丁: + +```bash +java --patch-module java.base=your_patch yourApp +``` + +- 扩展类加载器被重命名为平台类加载器(Platform Class-Loader),而且 extension 机制则被移除。也就意味着,如果我们指定 java.ext.dirs 环境变量,或者 lib/ext 目录存在,JVM 将直接返回**错误**!建议解决办法就是将其放入 classpath 里。 +- 部分不需要 AllPermission 的 Java 基础模块,被降级到平台类加载器中,相应的权限也被更精细粒度地限制起来。 +- rt.jar 和 tools.jar 同样是被移除了!JDK 的核心类库以及相关资源,被存储在 jimage 文件中,并通过新的 JRT 文件系统访问,而不是原有的 JAR 文件系统。虽然看起来很惊人,但幸好对于大部分软件的兼容性影响,其实是有限的,更直接地影响是 IDE 等软件,通常只要升级到新版本就可以了。 +- 增加了 Layer 的抽象, JVM 启动默认创建 BootLayer,开发者也可以自己去定义和实例化 Layer,可以更加方便的实现类似容器一般的逻辑抽象。 + +结合了 Layer,目前的 JVM 内部结构就变成了下面的层次,内建类加载器都在 BootLayer 中,其他 Layer 内部有自定义的类加载器,不同版本模块可以同时工作在不同的 Layer。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240704234.png) + +## 有哪些方法可以在运行时动态生成一个 Java 类? + +【典型回答】+【考点分析】+【知识扩展】 + +可以利用 Java 字节码操纵工具和类库来实现,如:[ASM](https://asm.ow2.io/)、[Javassist](http://www.javassist.org/)、cglib 等 + +类从字节码到 Class 对象的转换,在类加载过程中,这一步是通过下面的方法提供的功能,或者 defineClass 的其他本地对等实现。 + +```java +protected final Class defineClass(String name, byte[] b, int off, int len, + ProtectionDomain protectionDomain) +protected final Class defineClass(String name, java.nio.ByteBuffer b, + ProtectionDomain protectionDomain) +``` + +JDK 提供的 defineClass 方法,最终都是本地代码实现的。 + +```java +static native Class defineClass1(ClassLoader loader, String name, byte[] b, int off, int len, + ProtectionDomain pd, String source); + +static native Class defineClass2(ClassLoader loader, String name, java.nio.ByteBuffer b, + int off, int len, ProtectionDomain pd, + String source); +``` + +## 谈谈 JVM 内存区域的划分,哪些区域可能发生 OutOfMemoryError + +【典型回答】+【考点分析】+【知识扩展】 + +- 首先,**程序计数器**(PC,Program Counter Register)。在 JVM 规范中,每个线程都有它自己的程序计数器,并且任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的 Java 方法的 JVM 指令地址;或者,如果是在执行本地方法,则是未指定值(undefined)。 +- 第二,**Java 虚拟机栈**(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用。 + - 前面谈程序计数器时,提到了当前方法;同理,在一个时间点,对应的只会有一个活动的栈帧,通常叫作当前帧,方法所在的类叫作当前类。如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,成为新的当前帧,一直到它返回结果或者执行结束。JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈。 + - 栈帧中存储着局部变量表、操作数(operand)栈、动态链接、方法正常退出或者异常退出的定义等。 +- 第三,**堆**(Heap),它是 Java 内存管理的核心区域,用来放置 Java 对象实例,几乎所有创建的 Java 对象实例都是被直接分配在堆上。堆被所有的线程共享,在虚拟机启动时,我们指定的“Xmx”之类参数就是用来指定最大堆空间等指标。 + - 理所当然,堆也是垃圾收集器重点照顾的区域,所以堆内空间还会被不同的垃圾收集器进行进一步的细分,最有名的就是新生代、老年代的划分。 +- 第四,**方法区**(Method Area)。这也是所有线程共享的一块内存区域,用于存储所谓的元(Meta)数据,例如类结构信息,以及对应的运行时常量池、字段、方法代码等。 + - 由于早期的 Hotspot JVM 实现,很多人习惯于将方法区称为永久代(Permanent Generation)。Oracle JDK 8 中将永久代移除,同时增加了元数据区(Metaspace)。 +- 第五,**运行时常量池**(Run-Time Constant Pool),这是方法区的一部分。如果仔细分析过反编译的类文件结构,你能看到版本号、字段、方法、超类、接口等各种信息,还有一项信息就是常量池。Java 的常量池可以存放各种常量信息,不管是编译期生成的各种字面量,还是需要在运行时决定的符号引用,所以它比一般语言的符号表存储的信息更加宽泛。 +- 第六,**本地方法栈**(Native Method Stack)。它和 Java 虚拟机栈是非常相似的,支持对本地方法的调用,也是每个线程都会创建一个。在 Oracle Hotspot JVM 中,本地方法栈和 Java 虚拟机栈是在同一块儿区域,这完全取决于技术实现的决定,并未在规范中强制。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240705853.png) + +OOM 场景: + +- Java heap space - 堆空间溢出 +- GC overhead limit exceeded - GC 开销超过限制。官方给出的定义是:**超过 `98%` 的时间用来做 GC 并且回收了不到 `2%` 的堆内存时会抛出此异常**。这意味着,发生在 GC 占用大量时间为释放很小空间的时候发生的,是一种保护机制。导致异常的原因:一般是因为堆太小,没有足够的内存。 +- PermGen space - Perm (永久代)空间主要用于存放 `Class` 和 Meta 信息,包括类的名称和字段,带有方法字节码的方法,常量池信息,与类关联的对象数组和类型数组以及即时编译器优化。GC 在主程序运行期间不会对永久代空间进行清理,默认是 64M 大小。根据上面的定义,可以得出 **PermGen 大小要求取决于加载的类的数量以及此类声明的大小**。因此,可以说造成该错误的主要原因是永久代中装入了太多的类或太大的类。在 JDK8 之前的版本中,可以通过 `-XX:PermSize` 和 `-XX:MaxPermSize` 设置永久代空间大小,从而限制方法区大小,并间接限制其中常量池的容量。 +- Metaspace - Java8 以后,JVM 内存空间发生了很大的变化。取消了永久代,转而变为元数据区。 +- Unable to create new native thread - 无法新建本地线程。这个错误意味着:**Java 应用程序已达到其可以启动线程数的限制**。 +- 直接内存溢出 - 由直接内存导致的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见有什么明显的异常情况,如果读者发现内存溢出之后产生的 Dump 文件很小,而程序中又直接或间接使用了 DirectMemory(典型的间接使用就是 NIO),那就可以考虑重点检查一下直接内存方面的原因了。 + +## 如何监控和诊断 JVM 堆内和堆外内存使用? + +【典型回答】+【考点分析】+【知识扩展】 + +- `jps` - 显示指定系统内所有的 JVM 进程 +- `jstat` - 查看 JVM 统计信息(类装载、内存、垃圾收集、JIT 编译等运行数据) +- `jmap` - 生成堆内存快照(称为 heapdump 或 dump 文件) +- `jhat` - 用来分析 jmap 生成的 dump 文件 +- `jstack` - 生成线程快照(称为 threaddump 或 coredump 文件) +- jinfo - 用于实时查看和调整虚拟机运行参数 +- `JConsole` - 基于 JMX 的可视化监视与管理工具 +- `VisualVM` - 多合一故障处理工具 +- `MAT` - Eclipse 提供的内存分析工具 +- JMC - [Java Mission Control](http://www.oracle.com/technetwork/java/javaseproducts/mission-control/java-mission-control-1998576.html) 不仅仅能够使用 [JMX](https://en.wikipedia.org/wiki/Java_Management_Extensions) 进行普通的管理、监控任务,还可以配合 [Java Flight Recorder](https://docs.oracle.com/javacomponents/jmc-5-4/jfr-runtime-guide/about.htm#JFRUH171)(JFR)技术,以非常低的开销,收集和分析 JVM 底层的 Profiling 和事件等信息。 +- `JProfile` + +堆结构示意图。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240705463.png) + +### 年轻代 + +新生代是大部分对象创建和销毁的区域,在通常的 Java 应用中,绝大部分对象生命周期都是很短暂的。其内部又分为 Eden 区域,作为对象初始分配的区域;两个 Survivor,有时候也叫 from、to 区域,被用来放置从 Minor GC 中保留下来的对象。 + +JVM 会随意选取一个 Survivor 区域作为“to”,然后会在 GC 过程中进行区域间拷贝,也就是将 Eden 中存活下来的对象和 from 区域的对象,拷贝到这个“to”区域。这种设计主要是为了防止内存的碎片化,并进一步清理无用对象。 + +从内存模型而不是垃圾收集的角度,对 Eden 区域继续进行划分,Hotspot JVM 还有一个概念叫做 Thread Local Allocation Buffer(TLAB)。这是 JVM 为每个线程分配的一个私有缓存区域,否则,多线程同时分配内存时,为避免操作同一地址,可能需要使用加锁等机制,进而影响分配速度。TLAB 仍然在堆上,它是分配在 Eden 区域内的。其内部结构比较直观易懂,start、end 就是起始地址,top(指针)则表示已经分配到哪里了。所以我们分配新对象,JVM 就会移动 top,当 top 和 end 相遇时,即表示该缓存已满,JVM 会试图再从 Eden 里分配一块儿。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240705117.png) + +### 老年代 + +放置长生命周期的对象,通常都是从 Survivor 区域拷贝过来的对象。当然,也有特殊情况,我们知道普通的对象会被分配在 TLAB 上;如果对象较大,JVM 会试图直接分配在 Eden 其他位置上;如果对象太大,完全无法在新生代找到足够长的连续空闲空间,JVM 就会直接分配到老年代。 + +### 永久代 + +这部分就是早期 Hotspot JVM 的方法区实现方式了,储存 Java 类元数据、常量池、Intern 字符串缓存,在 JDK 8 之后,这些数据被存储在元数据空间。 + +### JVM 参数 + +- 最大堆体积 + +```java +-Xmx value +``` + +- 初始的最小堆体积 + +```java +-Xms value +``` + +- 老年代和新生代的比例 + +```java +-XX:NewRatio=value +``` + +默认情况下,这个数值是 2,意味着老年代是新生代的 2 倍大;换句话说,新生代是堆大小的 1/3。 + +- 当然,也可以不用比例的方式调整新生代的大小,直接指定下面的参数,设定具体的内存大小数值。 + +```java +-XX:NewSize=value +``` + +- Eden 和 Survivor 的大小是按照比例设置的,如果 SurvivorRatio 是 8,那么 Survivor 区域就是 Eden 的 1⁄8 大小,也就是新生代的 1/10,因为 YoungGen=Eden + 2\*Survivor,JVM 参数格式是 + +```java +-XX:SurvivorRatio=value +``` + +- TLAB 当然也可以调整,JVM 实现了复杂的适应策略,如果你有兴趣可以参考这篇 [说明](https://blogs.oracle.com/jonthecollector/the-real-thing)。 + +## Java 常见的垃圾收集器有哪些? + +【典型回答】+【考点分析】+【知识扩展】 + +### 垃圾收集器 + +- Serial GC,它是最古老的垃圾收集器,“Serial”体现在其收集工作是单线程的,并且在进行垃圾收集过程中,会进入臭名昭著的“Stop-The-World”状态。当然,其单线程设计也意味着精简的 GC 实现,无需维护复杂的数据结构,初始化也简单,所以一直是 Client 模式下 JVM 的默认选项。从年代的角度,通常将其老年代实现单独称作 Serial Old,它采用了标记 - 整理(Mark-Compact)算法,区别于新生代的复制算法。 + +- ParNew GC,很明显是个新生代 GC 实现,它实际是 Serial GC 的多线程版本,最常见的应用场景是配合老年代的 CMS GC 工作 + +- CMS(Concurrent Mark Sweep) GC,基于标记 - 清除(Mark-Sweep)算法,设计目标是尽量减少停顿时间,这一点对于 Web 等反应时间敏感的应用非常重要,一直到今天,仍然有很多系统使用 CMS GC。但是,CMS 采用的标记 - 清除算法,存在着内存碎片化问题,所以难以避免在长时间运行等情况下发生 full GC,导致恶劣的停顿。另外,既然强调了并发(Concurrent),CMS 会占用更多 CPU 资源,并和用户线程争抢。 +- Parallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默认 GC 选择,也被称作是吞吐量优先的 GC。它的算法和 Serial GC 比较相似,尽管实现要复杂的多,其特点是新生代和老年代 GC 都是并行进行的,在常见的服务器环境中更加高效。 + +- G1 GC 这是一种兼顾吞吐量和停顿时间的 GC 实现,是 Oracle JDK 9 以后的默认 GC 选项。G1 可以直观的设定停顿时间的目标,相比于 CMS GC,G1 未必能做到 CMS 在最好情况下的延时停顿,但是最差情况要好很多。 + +### 对象是否回收算法 + +**引用计数法** - 就是为对象添加一个引用计数,用于记录对象被引用的情况,如果计数为 0,即表示对象可回收。引用计数法最大的问题是难以处理循环引用。 + +**可达性分析法** - 就是将对象及其引用关系看作一个图,选定活动的对象作为 GC Roots,然后跟踪引用链条,如果一个对象和 GC Roots 之间不可达,也就是不存在引用链条,那么即可认为是可回收对象。JVM 会把虚拟机栈和本地方法栈中正在引用的对象、静态属性引用的对象和常量,作为 GC Roots。 + +### 垃圾收集算法 + +**标记 - 复制(Copying)** - 将内存划分为大小相等的两块,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。这实际上也是利用了 CoW 机制。 + +**标记 - 清除(Mark-Sweep)** - 将需要回收的对象进行标记,然后清除。标记和清除过程效率都不高,会产生大量碎片,导致无法给大对象分配内存。 + +**标记 - 整理(Mark-Compact)** - 让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。 + +### 垃圾收集过程 + +第一,Java 应用不断创建对象,通常都是分配在 Eden 区域,当其空间占用达到一定阈值时,触发 minor GC。仍然被引用的对象(绿色方块)存活下来,被复制到 JVM 选择的 Survivor 区域,而没有被引用的对象(黄色方块)则被回收。注意,我给存活对象标记了“数字 1”,这是为了表明对象的存活时间。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240706966.png) + +第二, 经过一次 Minor GC,Eden 就会空闲下来,直到再次达到 Minor GC 触发条件,这时候,另外一个 Survivor 区域则会成为 to 区域,Eden 区域的存活对象和 From 区域对象,都会被复制到 to 区域,并且存活的年龄计数会被加 1。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240706215.png) + +第三, 类似第二步的过程会发生很多次,直到有对象年龄计数达到阈值,这时候就会发生所谓的晋升(Promotion)过程,如下图所示,超过阈值的对象会被晋升到老年代。这个阈值是可以通过参数指定: + +`-XX:MaxTenuringThreshold=` + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240706521.png) + +后面就是老年代 GC,具体取决于选择的 GC 选项,对应不同的算法。下面是一个简单标记 - 整理算法过程示意图,老年代中的无用对象被清除后, GC 会将对象进行整理,以防止内存碎片化。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240707750.png) + +通常我们把老年代 GC 叫作 Major GC,将对整个堆进行的清理叫作 Full GC,但是这个也没有那么绝对,因为不同的老年代 GC 算法其实表现差异很大,例如 CMS,“concurrent”就体现在清理工作是与工作线程一起并发运行的。 + +## 谈谈你的 GC 调优思路 + +【典型回答】+【考点分析】+【知识扩展】 + +GC 调优,从性能角度来看,通常关注三个方面,内存占用(footprint)、延时(latency)和吞吐量(throughput),大多数情况下调优会侧重于其中一个或者两个方面的目标,很少有情况可以兼顾三个不同的角度。 + +调优思路: + +- 理解应用需求和问题,确定调优目标。假设,我们开发了一个应用服务,但发现偶尔会出现性能抖动,出现较长的服务停顿。评估用户可接受的响应时间和业务量,将目标简化为,希望 GC 暂停尽量控制在 200ms 以内,并且保证一定标准的吞吐量。 +- 掌握 JVM 和 GC 的状态,定位具体的问题,确定真的有 GC 调优的必要。具体有很多方法,比如,通过 jstat 等工具查看 GC 等相关状态,可以开启 GC 日志,或者是利用操作系统提供的诊断工具等。例如,通过追踪 GC 日志,就可以查找是不是 GC 在特定时间发生了长时间的暂停,进而导致了应用响应不及时。 +- 这里需要思考,选择的 GC 类型是否符合我们的应用特征,如果是,具体问题表现在哪里,是 Minor GC 过长,还是 Mixed GC 等出现异常停顿情况;如果不是,考虑切换到什么类型,如 CMS 和 G1 都是更侧重于低延迟的 GC 选项。 +- 通过分析确定具体调整的参数或者软硬件配置。 +- 验证是否达到调优目标,如果达到目标,即可以考虑结束调优;否则,重复完成分析、调整、验证这个过程。 + +### G1 GC 机制 + +G1 内存区域如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240707671.png) + +region 的大小是一致的,数值是在 1M 到 32M 字节之间的一个 2 的幂值数,JVM 会尽量划分 2048 个左右、同等大小的 region。这个数字既可以手动调整,G1 也会根据堆大小自动进行调整。 + +在 G1 实现中,年代是个逻辑概念,具体体现在,一部分 region 是作为 Eden,一部分作为 Survivor,除了意料之中的 Old region,G1 会将超过 region 50% 大小的对象(在应用中,通常是 byte 或 char 数组)归类为 Humongous 对象,并放置在相应的 region 中。逻辑上,Humongous region 算是老年代的一部分,因为复制这样的大对象是很昂贵的操作,并不适合新生代 GC 的复制算法。 + +从 GC 算法的角度,G1 选择的是复合算法,可以简化理解为: + +- 在新生代,G1 采用的仍然是并行的复制算法,所以同样会发生 Stop-The-World 的暂停。 +- 在老年代,大部分情况下都是并发标记,而整理(Compact)则是和新生代 GC 时捎带进行,并且不是整体性的整理,而是增量进行的。 + +## Java 内存模型中的 happen-before 是什么? + +【典型回答】+【考点分析】+【知识扩展】 + +JMM 为程序中所有的操作定义了一个偏序关系,称之为 **`先行发生原则(Happens-Before)`**。**Happens-Before** 是指 **前面一个操作的结果对后续操作是可见的**。 + +**Happens-Before** 非常重要,它是判断数据是否存在竞争、线程是否安全的主要依据,依靠这个原则,我们可以通过几条规则一揽子地解决并发环境下两个操作间是否可能存在冲突的所有问题。 + +- **程序顺序规则** - 在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。 +- **锁定规则** - 一个 `unLock` 操作 Happens-Before 于后面对同一个锁的 `lock` 操作。 +- **volatile 变量规则** - 对一个 `volatile` 变量的写操作 Happens-Before 于后面对这个变量的读操作。 +- **线程启动规则** - `Thread` 对象的 `start()` 方法 Happens-Before 于此线程的每个一个动作。 +- **线程终止规则** - 线程中所有的操作都 Happens-Before 于线程的终止检测,我们可以通过 `Thread.join()` 方法是否结束、`Thread.isAlive()` 的返回值手段检测到线程已经终止执行。 +- **线程中断规则** - 对线程 `interrupt()` 方法的调用 Happens-Before 于被中断线程的代码检测到中断事件的发生,可以通过 `Thread.interrupted()` 方法检测到是否有中断发生。 +- **对象终结规则** - 一个对象的初始化完成 Happens-Before 于它的 `finalize()` 方法的开始。 +- **传递性** - 如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。 + +## Java 程序运行在 Docker 等容器环境有哪些新问题? + +【典型回答】+【考点分析】+【知识扩展】 + +虽然看起来 Docker 之类容器和虚拟机非常相似,例如,它也有自己的 shell,能独立安装软件包,运行时与其他容器互不干扰。但是,如果深入分析你会发现,Docker 并不是一种完全的**虚拟化**技术,而更是一种轻量级的**隔离**技术。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240707589.png) + +基于 namespace,Docker 为每个容器提供了单独的命名空间,对网络、PID、用户、IPC 通信、文件系统挂载点等实现了隔离。对于 CPU、内存、磁盘 IO 等计算资源,则是通过 CGroup 进行管理。 + +Docker 仅在类似 Linux 内核之上实现了有限的隔离和虚拟化,并不是像传统虚拟化软件那样,独立运行一个新的操作系统。对于 Java 来说,Docker 未完全隐藏底层信息,会产生以下问题: + +第一,容器环境对于计算资源的管理方式是全新的,CGroup 作为相对比较新的技术,历史版本的 Java 显然并不能自然地理解相应的资源限制。 + +第二,namespace 对于容器内的应用细节增加了一些微妙的差异,比如 jcmd、jstack 等工具会依赖于“/proc//”下面提供的部分信息,但是 Docker 的设计改变了这部分信息的原有结构,我们需要对原有工具进行修改以适应这种变化。 + +**从 JVM 运行机制的角度,为什么这些“沟通障碍”会导致 OOM 等问题呢?** + +- JVM 会大概根据检测到的内存大小,设置最初启动时的堆大小为系统内存的 1/64;并将堆最大值,设置为系统内存的 1/4。 +- 而 JVM 检测到系统的 CPU 核数,则直接影响到了 Parallel GC 的并行线程数目和 JIT complier 线程数目,甚至是我们应用中 ForkJoinPool 等机制的并行等级。 + +这些默认参数,是根据通用场景选择的初始值。但是由于容器环境的差异,Java 的判断很可能是基于错误信息而做出的。更加严重的是,JVM 的一些原有诊断或备用机制也会受到影响。为保证服务的可用性,一种常见的选择是依赖“-XX:OnOutOfMemoryError”功能,通过调用处理脚本的形式来做一些补救措施,比如自动重启服务等。但是,这种机制是基于 fork 实现的,当 Java 进程已经过度提交内存时,fork 新的进程往往已经不可能正常运行了。 + +**如何解决这些问题呢?** + +首先,如果你能够**升级到最新的 JDK 版本**,这个问题就迎刃而解了。针对这种情况,JDK 9 中引入了一些实验性的参数,以方便 Docker 和 Java“沟通”。 + +如果你可以切换到 JDK 10 或者更新的版本,问题就更加简单了。Java 对容器(Docker)的支持已经比较完善,默认就会自适应各种资源限制和实现差异。 + +JDK 9 中的实验性改进已经被移植到 Oracle JDK 8u131 之中。 + +## 你了解 Java 应用开发中的注入攻击吗? + +【典型回答】+【考点分析】+【知识扩展】 + +注入攻击其基本特征是程序允许攻击者将不可信的动态内容注入到程序中,并将其执行,这就可能完全改变最初预计的执行过程,产生恶意效果。 + +- SQL 注入攻击 +- 系统命令注入 +- XML 注入攻击 + +## 如何写出安全的 Java 代码? + +【典型回答】+【考点分析】+【知识扩展】 + +略 + +## 后台服务出现明显“变慢”,谈谈你的诊断思路? + +【典型回答】+【考点分析】+【知识扩展】 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240708746.png) + +## 有人说“Lambda 能让 Java 程序慢 30 倍”,你怎么看? + +【典型回答】+【考点分析】+【知识扩展】 + +在实际运行中,基于 Lambda/Stream 的版本(lambdaMaxInteger),比传统的 for-each 版本(forEachLoopMaxInteger)慢很多。 + +```java +// 一个大的 ArrayList,内部是随机的整形数据 +volatile List integers = … + +// 基准测试 1 +public int forEachLoopMaxInteger() { + int max = Integer.MIN_VALUE; + for (Integer n : integers) { + max = Integer.max(max, n); + } + return max; +} + +// 基准测试 2 +public int lambdaMaxInteger() { + return integers.stream().reduce(Integer.MIN_VALUE, (a, b) -> Integer.max(a, b)); +} +``` + +以上代码片段更多的开销是源于自动装箱、拆箱(auto-boxing/unboxing),而不是源自 Lambda 和 Stream。 + +一般来说,可以认为 Lambda/Stream 提供了与传统方式接近对等的性能,但是如果对于性能非常敏感,就不能完全忽视它在特定场景的性能差异了,例如:**初始化的开销**。 Lambda 并不算是语法糖,而是一种新的工作机制,在首次调用时,JVM 需要为其构建 [CallSite](https://docs.oracle.com/javase/8/docs/api/java/lang/invoke/CallSite.html) 实例。这意味着,如果 Java 应用启动过程引入了很多 Lambda 语句,会导致启动过程变慢。其实现特点决定了 JVM 对它的优化可能与传统方式存在差异。 + +## JVM 优化 Java 代码时都做了什么? + +【典型回答】+【考点分析】+【知识扩展】 + +略 + +## 谈谈 MySQL 支持的事务隔离级别,以及悲观锁和乐观锁的原理和应用场景? + +【典型回答】+【考点分析】+【知识扩展】 + +以最常见的 MySQL InnoDB 引擎为例,它是基于 MVCC(Multi-Versioning Concurrency Control)和锁的复合实现,按照隔离程度从低到高,MySQL 事务隔离级别分为四个不同层次: + +- 读未提交(Read uncommitted),就是一个事务能够看到其他事务尚未提交的修改,这是最低的隔离水平,允许脏读出现。 +- 读已提交(Read committed),事务能够看到的数据都是其他事务已经提交的修改,也就是保证不会看到任何中间性状态,当然脏读也不会出现。读已提交仍然是比较低级别的隔离,并不保证再次读取时能够获取同样的数据,也就是允许其他事务并发修改数据,允许不可重复读和幻象读(Phantom Read)出现。 +- 可重复读(Repeatable reads),保证同一个事务中多次读取的数据是一致的,这是 MySQL InnoDB 引擎的默认隔离级别,但是和一些其他数据库实现不同的是,可以简单认为 MySQL 在可重复读级别不会出现幻象读。 +- 串行化(Serializable),并发事务之间是串行化的,通常意味着读取需要获取共享读锁,更新需要获取排他写锁,如果 SQL 使用 WHERE 语句,还会获取区间锁(MySQL 以 GAP 锁形式实现,可重复读级别中默认也会使用),这是最高的隔离级别。 + +悲观锁和乐观锁: + +悲观锁 - 悲观锁一般就是利用类似 `SELECT … FOR UPDATE` 这样的语句,对数据加锁,避免其他事务意外修改数据。 + +乐观锁 - 乐观锁则与 Java 并发包中的 AtomicFieldUpdater 类似,也是利用 CAS 机制,并不会对数据加锁,而是通过对比数据的时间戳或者版本号,来实现乐观锁需要的版本判断。 + +## 谈谈 Spring Bean 的生命周期和作用域? + +【典型回答】+【考点分析】+【知识扩展】 + +### Spring 创建 Bean + +- 实例化 Bean 对象。 +- 设置 Bean 属性。 +- 如果我们通过各种 Aware 接口声明了依赖关系,则会注入 Bean 对容器基础设施层面的依赖。具体包括 BeanNameAware、BeanFactoryAware 和 ApplicationContextAware,分别会注入 Bean ID、Bean Factory 或者 ApplicationContext。 +- 调用 BeanPostProcessor 的前置初始化方法 postProcessBeforeInitialization。 +- 如果实现了 InitializingBean 接口,则会调用 afterPropertiesSet 方法。 +- 调用 Bean 自身定义的 init 方法。 +- 调用 BeanPostProcessor 的后置初始化方法 postProcessAfterInitialization。 +- 创建过程完毕。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240708158.png) + +### Spring 销毁 Bean + +Spring Bean 的销毁过程会依次调用 DisposableBean 的 destroy 方法和 Bean 自身定制的 destroy 方法。 + +Spring Bean 有五个作用域,其中最基础的有下面两种: + +- Singleton,这是 Spring 的默认作用域,也就是为每个 IOC 容器创建唯一的一个 Bean 实例。 +- Prototype,针对每个 getBean 请求,容器都会单独创建一个 Bean 实例。 + +从 Bean 的特点来看,Prototype 适合有状态的 Bean,而 Singleton 则更适合无状态的情况。另外,使用 Prototype 作用域需要经过仔细思考,毕竟频繁创建和销毁 Bean 是有明显开销的。 + +如果是 Web 容器,则支持另外三种作用域: + +- Request,为每个 HTTP 请求创建单独的 Bean 实例。 +- Session,很显然 Bean 实例的作用域是 Session 范围。 + - GlobalSession,用于 Portlet 容器,因为每个 Portlet 有单独的 Session,GlobalSession 提供一个全局性的 HTTP Session。· + +## 对比 Java 标准 NIO 类库,你知道 Netty 是如何实现更高性能的吗? + +【典型回答】+【考点分析】+【知识扩展】 + +多路复用 + +零拷贝 + +从 API 能力范围来看,Netty 完全是 Java NIO 框架的一个大大的超集 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240708363.png) + +Netty 官方提供的 Server 部分,完整用例请点击 [链接](http://netty.io/4.1/xref/io/netty/example/echo/package-summary.html)。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240708879.png) + +- [ServerBootstrap](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/bootstrap/ServerBootstrap.java),服务器端程序的入口,这是 Netty 为简化网络程序配置和关闭等生命周期管理,所引入的 Bootstrapping 机制。我们通常要做的创建 Channel、绑定端口、注册 Handler 等,都可以通过这个统一的入口,以 **Fluent** API 等形式完成,相对简化了 API 使用。与之相对应, [Bootstrap](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/bootstrap/Bootstrap.java) 则是 Client 端的通常入口。 +- [Channel](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/Channel.java),作为一个基于 NIO 的扩展框架,Channel 和 Selector 等概念仍然是 Netty 的基础组件,但是针对应用开发具体需求,提供了相对易用的抽象。 +- [EventLoop](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/EventLoop.java),这是 Netty 处理事件的核心机制。例子中使用了 EventLoopGroup。我们在 NIO 中通常要做的几件事情,如注册感兴趣的事件、调度相应的 Handler 等,都是 EventLoop 负责。 +- [ChannelFuture](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/ChannelFuture.java),这是 Netty 实现异步 IO 的基础之一,保证了同一个 Channel 操作的调用顺序。Netty 扩展了 Java 标准的 Future,提供了针对自己场景的特有 [Future](https://github.com/netty/netty/blob/eb7f751ba519cbcab47d640cd18757f09d077b55/common/src/main/java/io/netty/util/concurrent/Future.java) 定义。 +- ChannelHandler,这是应用开发者**放置业务逻辑的主要地方**,也是我上面提到的“Separation Of Concerns”原则的体现。 +- [ChannelPipeline](https://github.com/netty/netty/blob/2c13f71c733c5778cd359c9148f50e63d1878f7f/transport/src/main/java/io/netty/channel/ChannelPipeline.java),它是 ChannelHandler 链条的容器,每个 Channel 在创建后,自动被分配一个 ChannelPipeline。在上面的示例中,我们通过 ServerBootstrap 注册了 ChannelInitializer,并且实现了 initChannel 方法,而在该方法中则承担了向 ChannelPipleline 安装其他 Handler 的任务。 + +参考下面的简化示意图,忽略 Inbound/OutBound Handler 的细节,理解这几个基本单元之间的操作流程和对应关系。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240709828.png) + +对比 Java 标准 NIO 的代码,Netty 提供的相对高层次的封装,减少了对 Selector 等细节的操纵,而 EventLoop、Pipeline 等机制则简化了编程模型,开发者不用担心并发等问题,在一定程度上简化了应用代码的开发。 + +## 谈谈常用的分布式 ID 的设计方案?Snowflake 是否受冬令时切换影响? + +【典型回答】+【考点分析】+【知识扩展】 + +分布式 ID 基本要求: + +- 全局唯一,区别于单点系统的唯一,全局是要求分布式系统内唯一。 +- 有序性,通常都需要保证生成的 ID 是有序递增的。例如,在数据库存储等场景中,有序 ID 便于确定数据位置,往往更加高效。 + +业界方案: + +UUID + +各种数据库自增序列 + +雪花算法 - 如 Twitter 早期开源的 [Snowflake](https://github.com/twitter/snowflake) 的实现,其结构定义可以参考下图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202409240710538.png) + +## 周末福利 一份 Java 工程师必读书单 + +- 《Java 编程思想》 + +- 《Java 核心技术》 + +- 《Effective Java》 + +- 《Head First 设计模式》 + +- 《Java 并发编程实战》 + +- 《深入理解 Java 虚拟机》 + +- 《Java 性能优化权威指南》 + +- 《Spring 实战》 + +- 《Netty 实战》 + +- 《大型分布式网站架构设计与实践》 + +- 《深入分布式缓存:从原理到实践》 + +## 周末福利 谈谈我对 Java 学习和面试的看法 + +略 + +## 结束语 技术没有终点 + +## 参考资料 + +- [极客时间教程 - Java 核心技术面试精讲](https://time.geekbang.org/column/intro/82) - 极客时间教程——从面试官视角梳理如何解答常见 Java 面试问题 diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\213\206\350\247\243Java\350\231\232\346\213\237\346\234\272\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\213\206\350\247\243Java\350\231\232\346\213\237\346\234\272\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..6f9656b679 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\213\206\350\247\243Java\350\231\232\346\213\237\346\234\272\347\254\224\350\256\260.md" @@ -0,0 +1,351 @@ +--- +title: 《极客时间教程 - 深入拆解 Java 虚拟机》笔记 +date: 2024-08-07 07:22:58 +order: 03 +categories: + - 笔记 + - Java +tags: + - Java + - JVM +permalink: /pages/e416f8f1/ +--- + +# 《极客时间教程 - 深入拆解 Java 虚拟机》笔记 + +## 开篇词 为什么我们要学习 Java 虚拟机? + +![](https://learn.lianglianglee.com/%E4%B8%93%E6%A0%8F/%E6%B7%B1%E5%85%A5%E6%8B%86%E8%A7%A3Java%E8%99%9A%E6%8B%9F%E6%9C%BA/assets/414248014bf825dd610c3095eed75377.jpg) + +## Java 代码是怎么运行的? + +从虚拟机视角来看,执行 Java 代码首先需要将它编译而成的 class 文件加载到 Java 虚拟机中。加载后的 Java 类会被存放于方法区(Method Area)中。实际运行时,虚拟机会执行方法区内的代码。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/ab5c3523af08e0bf2f689c1d6033ef77.png) + +在运行过程中,每当调用进入一个 Java 方法,Java 虚拟机会在当前线程的 Java 方法栈中生成一个栈帧,用以存放局部变量以及字节码的操作数。这个栈帧的大小是提前计算好的,而且 Java 虚拟机不要求栈帧在内存空间里连续分布。 + +当退出当前执行的方法时,不管是正常返回还是异常返回,Java 虚拟机均会弹出当前线程的当前栈帧,并将之舍弃。 + +从硬件视角来看,Java 字节码无法直接执行。因此,Java 虚拟机需要将字节码翻译成机器码。 + +在 HotSpot 里面,上述翻译过程有两种形式:第一种是解释执行,即逐条将字节码翻译成机器码并执行;第二种是即时编译(Just-In-Time compilation,JIT),即将一个方法中包含的所有字节码编译成机器码后再执行。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/5ee351091464de78eed75438b6f9183b.png) + +## Java 的基本类型 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/77dfb788a8ad5877e77fc28ed2d51745.png) + +## Java 虚拟机是如何加载 Java 类的 + +- 加载 - 是指查找字节流,并且据此创建类的过程。 +- 链接 - 是指将创建成的类合并至 Java 虚拟机中,使之能够执行的过程。 + - 验证 - 确保被加载类能够满足 Java 虚拟机的约束条件。 + - 准备 - 为被加载类的静态字段分配内存。 + - 解析 - 将符号引用解析为直接引用。 +- 初始化 - 为标记为常量值的字段赋值,以及执行 `` 方法的过程。Java 虚拟机会通过加锁来确保类的 `` 方法仅被执行一次。 + +## JVM 是如何执行方法调用的?(上) + +重载指的是方法名相同而参数类型不相同的方法之间的关系。 + +重写指的是子类中定义和父类方法名相同且参数类型相同的方法。 + +重载的方法在编译过程中即可完成识别。具体到每一个方法调用,Java 编译器会根据所传入参数的声明类型(注意与实际类型区分)来选取重载方法。选取的过程共分为三个阶段: + +1. 在不考虑对基本类型自动装拆箱(auto-boxing,auto-unboxing),以及可变长参数的情况下选取重载方法; +2. 如果在第 1 个阶段中没有找到适配的方法,那么在允许自动装拆箱,但不允许可变长参数的情况下选取重载方法; +3. 如果在第 2 个阶段中没有找到适配的方法,那么在允许自动装拆箱以及可变长参数的情况下选取重载方法。 + +## JVM 是如何执行方法调用的?(下) + +虚方法调用包括 invokevirtual 指令和 invokeinterface 指令。如果这两种指令所声明的目标方法被标记为 final,那么 Java 虚拟机会采用静态绑定。 + +否则,Java 虚拟机将采用动态绑定,在运行过程中根据调用者的动态类型,来决定具体的目标方法。 + +Java 虚拟机的动态绑定是通过方法表这一数据结构来实现的。方法表中每一个重写方法的索引值,与父类方法表中被重写的方法的索引值一致。 + +在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。 + +Java 虚拟机中的即时编译器会使用内联缓存来加速动态绑定。Java 虚拟机所采用的单态内联缓存将纪录调用者的动态类型,以及它所对应的目标方法。 + +当碰到新的调用者时,如果其动态类型与缓存中的类型匹配,则直接调用缓存的目标方法。 + +否则,Java 虚拟机将该内联缓存劣化为超多态内联缓存,在今后的执行过程中直接使用方法表进行动态绑定。 + +## JVM 是如何处理异常的? + +Java 字节码中,每个方法对应一个异常表。当程序触发异常时,Java 虚拟机将查找异常表,并依此决定需要将控制流转移至哪个异常处理器之中。Java 代码中的 catch 代码块和 finally 代码块都会生成异常表条目。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/17e2a3053b06b0a4383884f106e31c06.png) + +## JVM 是如何实现反射的? + +在默认情况下,方法的反射调用为委派实现,委派给本地实现来进行方法调用。在调用超过 15 次之后,委派实现便会将委派对象切换至动态实现。这个动态实现的字节码是自动生成的,它将直接使用 invoke 指令来调用目标方法。 + +方法的反射调用会带来不少性能开销,原因主要有三个:变长参数方法导致的 Object 数组,基本类型的自动装箱、拆箱,还有最重要的方法内联。 + +## JVM 是怎么实现 invokedynamic 的?(上) + +invokedynamic 底层机制的基石:方法句柄。 + +方法句柄是一个强类型的、能够被直接执行的引用。它仅关心所指向方法的参数类型以及返回类型,而不关心方法所在的类以及方法名。方法句柄的权限检查发生在创建过程中,相较于反射调用节省了调用时反复权限检查的开销。 + +方法句柄可以通过 invokeExact 以及 invoke 来调用。其中,invokeExact 要求传入的参数和所指向方法的描述符严格匹配。方法句柄还支持增删改参数的操作,这些操作是通过生成另一个充当适配器的方法句柄来实现的。 + +方法句柄的调用和反射调用一样,都是间接调用,同样会面临无法内联的问题。 + +## JVM 是怎么实现 invokedynamic 的?(下) + +invokedymaic 指令抽象出调用点的概念,并且将调用该调用点所链接的方法句柄。在第一次执行 invokedynamic 指令时,Java 虚拟机将执行它所对应的启动方法,生成并且绑定一个调用点。之后如果再次执行该指令,Java 虚拟机则直接调用已经绑定了的调用点所链接的方法。 + +Lambda 表达式到函数式接口的转换是通过 invokedynamic 指令来实现的。该 invokedynamic 指令对应的启动方法将通过 ASM 生成一个适配器类。 + +对于没有捕获其他变量的 Lambda 表达式,该 invokedynamic 指令始终返回同一个适配器类的实例。对于捕获了其他变量的 Lambda 表达式,每次执行 invokedynamic 指令将新建一个适配器类实例。 + +不管是捕获型的还是未捕获型的 Lambda 表达式,它们的性能上限皆可以达到直接调用的性能。其中,捕获型 Lambda 表达式借助了即时编译器中的逃逸分析,来避免实际的新建适配器类实例的操作。 + +## Java 对象的内存布局 + +### 压缩指针 + +在 Java 虚拟机中,每个 Java 对象都有一个对象头(object header),这个由标记字段和类型指针所构成。其中,标记字段用以存储 Java 虚拟机有关该对象的运行数据,如哈希码、GC 信息以及锁信息,而类型指针则指向该对象的类。 + +在 64 位的 Java 虚拟机中,对象头的标记字段占 64 位,而类型指针又占了 64 位。也就是说,每一个 Java 对象在内存中的额外开销就是 16 个字节。 + +Java 虚拟机引入了压缩指针的概念,将原本的 64 位指针压缩成 32 位。压缩指针要求 Java 虚拟机堆中对象的起始地址要对齐至 8 的倍数。Java 虚拟机还会对每个类的字段进行重排列,使得字段也能够内存对齐。 + +### 字段重排序 + +## 垃圾回收(上) + +### 引用计数法 + +引用计数法(reference counting)。它的做法是为每个对象添加一个引用计数器,用来统计指向该对象的引用个数。一旦某个对象的引用计数器为 0,则说明该对象已经死亡,便可以被回收了。 + +如果有一个引用,被赋值为某一对象,那么将该对象的引用计数器 +1。如果一个指向某一对象的引用,被赋值为其他值,那么将该对象的引用计数器 -1。也就是说,我们需要截获所有的引用更新操作,并且相应地增减目标对象的引用计数器。除了需要额外的空间来存储计数器,以及繁琐的更新操作,引用计数法还有一个重大的漏洞,那便是无法处理循环引用对象。 + +### 可达性分析法 + +可达性分析法的实质在于将一系列 GC Roots 作为初始的存活对象合集(live set),然后从该合集出发,探索所有能够被该集合引用到的对象,并将其加入到该集合中,这个过程我们也称之为标记(mark)。最终,未被探索到的对象便是死亡的,是可以回收的。 + +GC Roots 包括(但不限于)如下几种: + +1. Java 方法栈桢中的局部变量; +2. 已加载类的静态变量; +3. JNI handles; +4. 已启动且未停止的 Java 线程。 + +### Stop-the-world + +传统的垃圾回收算法采用的是一种简单粗暴的方式,那便是 Stop-the-world,停止其他非垃圾回收线程的工作,直到完成垃圾回收。这也就造成了垃圾回收所谓的暂停时间(GC pause)。 + +### 垃圾回收方式 + +#### 标记-复制 + +标记-复制把内存区域分为两等分,分别用两个指针 from 和 to 来维护,并且只是用 from 指针指向的内存区域来分配内存。当发生垃圾回收时,便把存活的对象复制到 to 指针指向的内存区域中,并且交换 from 指针和 to 指针的内容。复制这种回收方式同样能够解决内存碎片化的问题,但是它的缺点也极其明显,即堆空间的使用效率极其低下。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/4749cad235deb1542d4ca3b232ebf261.png) + +#### 标记-清除 + +标记-清除即把死亡对象所占据的内存标记为空闲内存,并记录在一个空闲列表(free list)之中。当需要新建对象时,内存管理模块便会从该空闲列表中寻找空闲内存,并划分给新建的对象。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/f225126be24826658ca5a899fcff5003.png) + +#### 标记-整理 + +标记-整理把存活的对象聚集到内存区域的起始位置,从而留下一段连续的内存空间。这种做法能够解决内存碎片化的问题,但代价是压缩算法的性能开销。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/415ee8e4aef12ff076b42e41660dad39.png) + +## 垃圾回收(下) + +Java 虚拟机将堆划分为新生代和老年代。其中,新生代又被划分为 Eden 区,以及两个大小相同的 Survivor 区。 + +可以通过参数 -XX:SurvivorRatio 来调整 Eden 区和 Survivor 区的比例。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%8b%86%e8%a7%a3Java%e8%99%9a%e6%8b%9f%e6%9c%ba/assets/2cc29b8de676d3747416416a3523e4e5.png) + +当发生 Minor GC 时,Eden 区和 from 指向的 Survivor 区中的存活对象会被复制到 to 指向的 Survivor 区中,然后交换 from 和 to 指针,以保证下一次 Minor GC 时,to 指向的 Survivor 区还是空的。 + +Java 虚拟机会记录 Survivor 区中的对象一共被来回复制了几次。如果一个对象被复制的次数为 15(对应虚拟机参数 -XX:+MaxTenuringThreshold),那么该对象将被晋升(promote)至老年代。另外,如果单个 Survivor 区已经被占用了 50%(对应虚拟机参数 -XX:TargetSurvivorRatio),那么较高复制次数的对象也会被晋升至老年代。 + +总而言之,当发生 Minor GC 时,我们应用了标记 - 复制算法,将 Survivor 区中的老存活对象晋升到老年代,然后将剩下的存活对象和 Eden 区的存活对象复制到另一个 Survivor 区中。理想情况下,Eden 区中的对象基本都死亡了,那么需要复制的数据将非常少,因此采用这种标记 - 复制算法的效果极好。 + +## Java 内存模型 + +Java 内存模型还定义了下述线程间的 happens-before 关系。 + +1. 解锁操作 happens-before 之后(这里指时钟顺序先后)对同一把锁的加锁操作。 +2. volatile 字段的写操作 happens-before 之后(这里指时钟顺序先后)对同一字段的读操作。 +3. 线程的启动操作(即 Thread.starts()) happens-before 该线程的第一个操作。 +4. 线程的最后一个操作 happens-before 它的终止事件(即其他线程通过 Thread.isAlive() 或 Thread.join() 判断该线程是否中止)。 +5. 线程对其他线程的中断操作 happens-before 被中断线程所收到的中断事件(即被中断线程的 InterruptedException 异常,或者第三个线程针对被中断线程的 Thread.interrupted 或者 Thread.isInterrupted 调用)。 +6. 构造器中的最后一个操作 happens-before 析构器的第一个操作。 + +happens-before 关系还具备传递性。如果操作 X happens-before 操作 Y,而操作 Y happens-before 操作 Z,那么操作 X happens-before 操作 Z。 + +## Java 虚拟机是怎么实现 synchronized 的? + +当声明 synchronized 代码块时,编译而成的字节码将包含 monitorenter 和 monitorexit 指令。 + +当用 synchronized 标记方法时,你会看到字节码中方法的访问标记包括 ACC_SYNCHRONIZED。该标记表示在进入该方法时,Java 虚拟机需要进行 monitorenter 操作。而在退出该方法时,不管是正常返回,还是向调用者抛异常,Java 虚拟机均需要进行 monitorexit 操作。 + +## Java 语法糖与 Java 编译器 + +Java 程序中的泛型信息会被擦除。具体来说,Java 编译器将选取该泛型所能指代的所有类中层次最高的那个,作为替换泛型的具体类。 + +## 即时编译(上) + +从 Java 8 开始,Java 虚拟机默认采用分层编译的方式。它将执行分为五个层次,分为为 0 层解释执行,1 层执行没有 profiling 的 C1 代码,2 层执行部分 profiling 的 C1 代码,3 层执行全部 profiling 的 C1 代码,和 4 层执行 C2 代码。 + +通常情况下,方法会首先被解释执行,然后被 3 层的 C1 编译,最后被 4 层的 C2 编译。 + +即时编译是由方法调用计数器和循环回边计数器触发的。在使用分层编译的情况下,触发编译的阈值是根据当前待编译的方法数目动态调整的。 + +OSR 是一种能够在非方法入口处进行解释执行和编译后代码之间切换的技术。OSR 编译可以用来解决单次调用方法包含热循环的性能优化问题。 + +## 即时编译(下) + +通常情况下,解释执行过程中仅收集方法的调用次数以及循环回边的执行次数。 + +当方法被 3 层 C1 所编译时,生成的 C1 代码将收集条件跳转指令的分支 profile,以及类型相关指令的类型 profile。在部分极端情况下,Java 虚拟机也会在解释执行过程中收集这些 profile。 + +基于分支 profile 的优化以及基于类型 profile 的优化都将对程序今后的执行作出假设。这些假设将精简所要编译的代码的控制流以及数据流。在假设失败的情况下,Java 虚拟机将采取去优化,退回至解释执行并重新收集相关的 profile。 + +## 即时编译器的中间表达形式 + +即时编译器将所输入的 Java 字节码转换成 SSA IR,以便更好地进行优化。 + +## Java 字节码(基础篇) + +Java 方法的栈桢分为操作数栈和局部变量区。通常来说,程序需要将变量从局部变量区加载至操作数栈中,进行一番运算之后再存储回局部变量区中。 + +Java 字节码可以划分为很多种类型,如加载常量指令,操作数栈专用指令,局部变量区访问指令,Java 相关指令,方法调用指令,数组相关指令,控制流指令,以及计算相关指令。 + +## 方法内联(上) + +方法内联是指,在编译过程中,当遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。 + +即时编译器既可以在解析过程中替换方法调用字节码,也可以在 IR 图中替换方法调用 IR 节点。这两者都需要将目标方法的参数以及返回值映射到当前方法来。 + +方法内联有许多规则。除了一些强制内联以及强制不内联的规则外,即时编译器会根据方法调用的层数、方法调用指令所在的程序路径的热度、目标方法的调用次数及大小,以及当前 IR 图的大小来决定方法调用能否被内联。 + +## 方法内联(下) + +完全去虚化通过类型推导或者类层次分析,将虚方法调用转换为直接调用。它的关键在于证明虚方法调用的目标方法是唯一的。 + +条件去虚化通过向代码中增添类型比较,将虚方法调用转换为一个个的类型测试以及对应该类型的直接调用。它将借助 Java 虚拟机所收集的类型 Profile。 + +## HotSpot 虚拟机的 intrinsic + +HotSpot 虚拟机将对标注了`@HotSpotIntrinsicCandidate`注解的方法的调用,替换为直接使用基于特定 CPU 指令的高效实现。这些方法我们便称之为 intrinsic。 + +具体来说,intrinsic 的实现有两种。一是不大常见的桩程序,可以在解释执行或者即时编译生成的代码中使用。二是特殊的 IR 节点。即时编译器将在方法内联过程中,将对 intrinsic 的调用替换为这些特殊的 IR 节点,并最终生成指定的 CPU 指令。 + +HotSpot 虚拟机定义了三百多个 intrinsic。其中比较特殊的有`Unsafe`类的方法,基本上使用 java.util.concurrent 包便会间接使用到`Unsafe`类的 intrinsic。除此之外,`String`类和`Arrays`类中的 intrinsic 也比较特殊。即时编译器将为之生成非常高效的 SIMD 指令。 + +## 逃逸分析 + +Java 中`Iterable`对象的 foreach 循环遍历是一个语法糖,Java 编译器会将该语法糖编译为调用`Iterable`对象的`iterator`方法,并用所返回的`Iterator`对象的`hasNext`以及`next`方法,来完成遍历。 + +逃逸分析是“一种确定指针动态范围的静态分析,它可以分析在程序的哪些地方可以访问到指针”。 + +在 Java 虚拟机的即时编译语境下,逃逸分析将判断新建的对象是否会逃逸。即时编译器判断对象逃逸的依据有两个:一是看对象是否被存入堆中,二是看对象是否作为方法调用的调用者或者参数。 + +即时编译器会根据逃逸分析的结果进行优化,如锁消除以及标量替换。后者指的是将原本连续分配的对象拆散为一个个单独的字段,分布在栈上或者寄存器中。 + +部分逃逸分析是一种附带了控制流信息的逃逸分析。它将判断新建对象真正逃逸的分支,并且支持将新建操作推延至逃逸分支。 + +## 字段访问相关优化 + +即时编译器将沿着控制流缓存字段存储、读取的值,并在接下来的字段读取操作时直接使用该缓存值。 + +这要求生成缓存值的访问以及使用缓存值的读取之间没有方法调用、内存屏障,或者其他可能存储该字段的节点。 + +即时编译器还会优化冗余的字段存储操作。如果一个字段的两次存储之间没有对该字段的读取操作、方法调用以及内存屏障,那么即时编译器可以将第一个冗余的存储操作给消除掉。 + +## 循环优化 + +循环无关代码外提将循环中值不变的表达式,或者循环无关检测外提至循环之前,以避免在循环中重复进行冗余计算。前者是通过 Sea-of-Nodes IR 以及节点调度来共同完成的,而后者则是通过一个独立优化 —— 循环预测来完成的。循环预测还可以外提循环有关的数组下标范围检测。 + +循环展开是一种在循环中重复多次迭代,并且相应地减少循环次数的优化方式。它是一种以空间换时间的优化方式,通过增大循环体来获取更多的优化机会。循环展开的特殊形式是完全展开,将原本的循环转换成若干个循环体的顺序执行。 + +## 向量化 + +向量化优化借助的是 CPU 的 SIMD 指令,即通过单条指令控制多组数据的运算。它被称为 CPU 指令级别的并行。 + +HotSpot 虚拟机运用向量化优化的方式有两种。第一种是使用 HotSpot intrinsic,在调用特定方法的时候替换为使用了 SIMD 指令的高效实现。Intrinsic 属于点覆盖,只有当应用程序明确需要这些 intrinsic 的语义,才能够获得由它带来的性能提升。 + +## 注解处理器 + +## 基准测试框架 JMH(上) + +性能基准测试框架 JMH 是 OpenJDK 中的其中一个开源项目。它内置了许多功能,来规避由 Java 虚拟机中的即时编译器或者其他优化对性能测试造成的影响。此外,它还提供了不少策略来降低来自操作系统以及硬件系统的影响。 + +开发人员仅需将所要测试的业务逻辑通过`@Benchmark`注解,便可以让 JMH 的注解处理器自动生成真正的性能测试代码,以及相应的性能测试配置文件。 + +## 基准测试框架 JMH(下) + +- `@Fork`允许开发人员指定所要 Fork 出的 Java 虚拟机的数目。 +- `@BenchmarkMode`允许指定性能数据的格式。 +- `@Warmup`和`@Measurement`允许配置预热迭代或者测试迭代的数目,每个迭代的时间以及每个操作包含多少次对测试方法的调用。 +- `@State`允许配置测试程序的状态。测试前对程序状态的初始化以及测试后对程序状态的恢复或者校验可分别通过`@Setup`和`@TearDown`来实现。 + +## Java 虚拟机的监控及诊断工具(命令行篇) + +- `jps`将打印所有正在运行的 Java 进程。 +- `jstat`允许用户查看目标 Java 进程的类加载、即时编译以及垃圾回收相关的信息。它常用于检测垃圾回收问题以及内存泄漏问题。 +- `jmap`允许用户统计目标 Java 进程的堆中存放的 Java 对象,并将它们导出成二进制文件。 +- `jinfo`将打印目标 Java 进程的配置参数,并能够改动其中 manageabe 的参数。 +- `jstack`将打印目标 Java 进程中各个线程的栈轨迹、线程状态、锁状况等信息。它还将自动检测死锁。 +- `jcmd`则是一把瑞士军刀,可以用来实现前面除了`jstat`之外所有命令的功能。 + +## Java 虚拟机的监控及诊断工具(GUI 篇) + +Eclipse MAT 可用于分析由`jmap`命令导出的 Java 堆快照。它包括两个相对比较重要的视图,分别为直方图和支配树。直方图展示了各个类的实例数目以及这些实例的 Shallow heap 或 Retained heap 的总和。支配树则展示了快照中每个对象所直接支配的对象。 + +Java Mission Control 是 Java 虚拟机平台上的性能监控工具。Java Flight Recorder 是 JMC 的其中一个组件,能够以极低的性能开销收集 Java 虚拟机的性能数据。 + +## JNI 的运行机制 + +Java 中的 native 方法的链接方式主要有两种。一是按照 JNI 的默认规范命名所要链接的 C 函数,并依赖于 Java 虚拟机自动链接。另一种则是在 C 代码中主动链接。 + +JNI 提供了一系列 API 来允许 C 代码使用 Java 语言特性。这些 API 不仅使用了特殊的数据结构来表示 Java 类,还拥有特殊的异常处理模式。 + +JNI 中的引用可分为局部引用和全局引用。这两者都可以阻止垃圾回收器回收被引用的 Java 对象。不同的是,局部引用在 native 方法调用返回之后便会失效。传入参数以及大部分 JNI API 函数的返回值都属于局部引用。 + +## JavaAgent 与字节码注入 + +## Graal:用 Java 编译 Java + +Graal 是一个用 Java 写就的、并能够将 Java 字节码转换成二进制码的即时编译器。它通过 JVMCI 与 Java 虚拟机交互,响应由后者发出的编译请求、完成编译并部署编译结果。 + +对 Java 程序而言,Graal 编译结果的性能略优于 OpenJDK 中的 C2;对 Scala 程序而言,它的性能优势可达到 10%(企业版甚至可以达到 20%!)。这背后离不开 Graal 所采用的激进优化方式。 + +## Truffle:语言实现框架 + +Truffle 是一个语言实现框架,允许语言开发者在仅实现词法解析、语法解析以及 AST 解释器的情况下,达到极佳的性能。目前 Oracle Labs 已经实现并维护了 JavaScript、Ruby、R、Python 以及可用于解析 LLVM bitcode 的 Sulong。后者将支持在 GraalVM 上运行 C/C++ 代码。 + +Truffle 背后所依赖的技术是 Partial Evaluation 以及节点重写。Partial Evaluation 指的是将所要编译的目标程序解析生成的抽象语法树当做编译时常量,特化该 Truffle 语言的解释器,从而得到指代这段程序解释执行过程的 Java 代码。然后,我们可以借助 Graal 编译器将这段 Java 代码即时编译为机器码。 + +节点重写则是收集 AST 节点的类型,根据所收集的类型 profile 进行的特化,并在节点类型不匹配时进行去优化并重新收集、编译的一项技术。 + +Truffle 的 Polyglot 特性支持在一段代码中混用多种不同的语言。与其他 Polyglot 框架相比,它支持在不同的 Truffle 语言中复用内存中存储的同一个对象。 + +## SubstrateVM:AOT 编译框架 + +SubstrateVM 的设计初衷是提供一个高启动性能、低内存开销,和能够无缝衔接 C 代码的 Java 运行时。它是一个独立的运行时,拥有自己的内存管理等组件。 + +SubstrateVM 要求所要 AOT 编译的目标程序是封闭的,即不能动态加载其他类库等。在进行 AOT 编译时,它会探索所有可能运行到的方法,并全部纳入编译范围之内。 + +SubstrateVM 的启动时间和内存开销都非常少,这主要得益于在 AOT 编译时便已保存了已初始化好的堆快照,并支持从程序入口直接开始运行。作为对比,HotSpot 虚拟机在执行 main 方法前需要执行一系列的初始化操作,因此启动时间和内存开销都要远大于运行在 SubstrateVM 上的程序。 + +Metropolis 项目将运用 SubstrateVM 项目,逐步地将 HotSpot 虚拟机中的 C++ 代码替换成 Java 代码,从而提升 HotSpot 虚拟机的可维护性,也加快新 Java 功能的开发效率。 + +## 尾声丨道阻且长,努力加餐。html + +## 工具篇 常用工具介绍 diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\265\205\345\207\272Java\350\231\232\346\213\237\346\234\272\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\265\205\345\207\272Java\350\231\232\346\213\237\346\234\272\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..46a404423f --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\265\205\345\207\272Java\350\231\232\346\213\237\346\234\272\347\254\224\350\256\260.md" @@ -0,0 +1,204 @@ +--- +title: 《极客时间教程 - 深入浅出 Java 虚拟机》笔记 +date: 2024-08-06 08:00:04 +order: 02 +categories: + - 笔记 + - Java +tags: + - Java + - JVM +permalink: /pages/0685fb75/ +--- + +# 《极客时间教程 - 深入浅出 Java 虚拟机》笔记 + +## 开篇词:JVM,一块难啃的骨头 + +略 + +## 一探究竟:为什么需要 JVM?它处在什么位置? + +**JVM** - Java Virtual Machine 的缩写,即 Java 虚拟机。JVM 是运行 Java 字节码的虚拟机。JVM 不理解 Java 源代码,这就是为什么要将 `*.java` 文件编译为 JVM 可理解的 `*.class` 文件(字节码)。Java 有一句著名的口号:“Write Once, Run Anywhere(一次编写,随处运行)”,JVM 正是其核心所在。实际上,JVM 针对不同的系统(Windows、Linux、MacOS)有不同的实现,目的在于用相同的字节码执行同样的结果。 + +**JRE** - Java Runtime Environment 的缩写,即 Java 运行时环境。它是运行已编译 Java 程序所需的一切的软件包,主要包括 JVM、Java 类库(Class Library)、Java 命令和其他基础结构。但是,它不能用于创建新程序。 + +**JDK** - Java Development Kit 的缩写,即 Java SDK。它不仅包含 JRE 的所有功能,还包含编译器 (javac) 和工具(如 javadoc 和 jdb)。它能够创建和编译程序。 + +> 总结来说,JDK、JRE、JVM 三者的关系是:JDK > JRE > JVM +> +> **JDK = JRE + 开发/调试工具** +> +> **JRE = JVM + Java 类库 + Java 运行库** +> +> **JVM = 类加载系统 + 运行时内存区域 + 执行引擎** + +![enter image description here](https://i.sstatic.net/CBNux.png) + +> 摘自 [stackoverflow 高票问题 - What is the difference between JDK and JRE?](https://stackoverflow.com/questions/1906445/what-is-the-difference-between-jdk-and-jre) + +## 大厂面试题:你不得不掌握的 JVM 内存管理 + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/Cgq2xl4VrjWAPqAuAARqnz6cigo666.png) + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/Cgq2xl4VrjaANruFAAQKxZvgfSs652.png) + +![img](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/Cgq2xl4VrjaAIlgaAAJKReuKXII670.png) + +## 大厂面试题:从覆盖 JDK 的类开始掌握类的加载机制 + +Java 类的完整生命周期包括以下几个阶段: + +- **加载(Loading)** - 将 _.java 文件转为 _.class +- **链接(Linking)** + - **验证(Verification)** - 确保 Class 文件的字节流中包含的信息符合当前虚拟机的要求 + - **准备(Preparation)** - 为 static 变量在方法区分配内存并初始化为默认值 + - **解析(Resolution)** - 将常量池的符号引用替换为直接引用的过程 +- **初始化(Initialization)** - 为类的静态变量赋予正确的初始值,JVM 负责对类进行初始化,主要对类变量进行初始化 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/CgqCHl9ZjveAemjoAAB4J1dCVDo17.jpeg) + +类加载器 + +- Bootstrap ClassLoader - 负责加载 `\lib` 或被 `-Xbootclasspath` 指定的路径 + +- ExtClassLoader - 负责加载 `\lib\ext` 或被`java.ext.dir` 指定的路径 + +- AppClassLoader - 负载加载 `classpath` 路径 +- 自定义类加载器 - 继承自 `java.lang.ClassLoader` + +**双亲委派机制** - 除了顶层的启动类加载器以外,其余的类加载器,在加载之前,都会委派给它的父加载器进行加载。 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/Cgq2xl4cQNeAZ4FuAABzsqSozok762.png) + +## 动手实践:从栈帧看字节码是如何在 JVM 中进行流转的 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/CgpOIF4ezuOAK_6bAACFY5oeX-Y174.jpg) + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/CgpOIF4ezeKAHVCXAABv7rzSgXE896.jpg) + +- javap - javap 是 JDK 自带的反解析工具。它的作用是将 .class 字节码文件解析成可读的文件格式。 + +- jclasslib - jclasslib 是一个图形化的工具,能够更加直观的查看字节码中的内容。 + +## 大厂面试题:得心应手应对 OOM 的疑难杂症 + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/Cgq2xl4hefWAWKFZAAMwndGjScg437.png) + +对象生命周期判断 + +- 引用计数法 +- 可达性分析法 - GC Roots + +引用类型: + +- 强引用 +- 软引用 +- 弱引用 +- 虚引用 + +## 深入剖析:垃圾回收你真的了解吗?(上) + +垃圾回收算法 + +- 标记-复制 - 效率最高,但会浪费大量内存空间 +- 标记-清除 - 效率一般,会产生大量内存碎片 +- 标记-整理 - 效率最差,但是不会浪费空间,也消除了内存碎片 + +GC 分代收集:年轻代 GC 使用标记-复制算法;老年代 GC 使用标记-清除算法、标记-整理算法。 + +常见 GC 收集器: + +- 年轻代:Serial、ParNew、Parallel +- 老年代:Serial Old、Parallel Old、CMS +- 元空间:G1、ZGC + +![](https://learn.lianglianglee.com/%e4%b8%93%e6%a0%8f/%e6%b7%b1%e5%85%a5%e6%b5%85%e5%87%ba%20Java%20%e8%99%9a%e6%8b%9f%e6%9c%ba-%e5%ae%8c/assets/Cgq2xl4lQuiAHmINAACWihcFScA929.jpg) + +GC 收集器配置参数: + +- **-XX:+UseSerialGC** 年轻代和老年代都用串行收集器 +- **-XX:+UseParNewGC** 年轻代使用 ParNew,老年代使用 Serial Old +- **-XX:+UseParallelGC** 年轻代使用 ParallerGC,老年代使用 Serial Old +- **-XX:+UseParallelOldGC** 新生代和老年代都使用并行收集器 +- **-XX:+UseConcMarkSweepGC**,表示年轻代使用 ParNew,老年代的用 CMS +- **-XX:+UseG1GC** 使用 G1 垃圾回收器 +- **-XX:+UseZGC** 使用 ZGC 垃圾回收器 + +## 深入剖析:垃圾回收你真的了解吗?(下) + +- Minor GC:发生在年轻代的 GC。 +- Major GC:发生在老年代的 GC。 +- Full GC:全堆垃圾回收。比如 Metaspace 区引起年轻代和老年代的回收。 + +CMS 垃圾回收器分为四个阶段: + +1. 初始标记 +2. 并发标记 +3. 重新标记 +4. 并发清理 + +CMS 中都会有哪些停顿(STW): + +1. 初始标记,这部分的停顿时间较短; +2. Minor GC(可选),在预处理阶段对年轻代的回收,停顿由年轻代决定; +3. 重新标记,由于 preclaen 阶段的介入,这部分停顿也较短; +4. Serial-Old 收集老年代的停顿,主要发生在预留空间不足的情况下,时间会持续很长; +5. Full GC,永久代空间耗尽时的操作,由于会有整理阶段,持续时间较长。 + +## 大厂面试题:有了 G1 还需要其他垃圾回收器吗? + +G1 最重要的概念,其实就是 Region。它采用分而治之,部分收集的思想,尽力达到我们给它设定的停顿目标。 + +## 案例实战:亿级流量高并发下如何进行估算和调优 + +GC 指标: + +- 系统容量(Capacity) +- 延迟(Latency) +- 吞吐量(Throughput) + +**选择垃圾回收器** + +- 如果你的堆大小不是很大(比如 100MB),选择串行收集器一般是效率最高的。参数:-XX:+UseSerialGC。 +- 如果你的应用运行在单核的机器上,或者你的虚拟机核数只有 1C,选择串行收集器依然是合适的,这时候启用一些并行收集器没有任何收益。参数:-XX:+UseSerialGC。 +- 如果你的应用是“吞吐量”优先的,并且对较长时间的停顿没有什么特别的要求。选择并行收集器是比较好的。参数:-XX:+UseParallelGC。 +- 如果你的应用对响应时间要求较高,想要较少的停顿。甚至 1 秒的停顿都会引起大量的请求失败,那么选择 G1、ZGC、CMS 都是合理的。虽然这些收集器的 GC 停顿通常都比较短,但它需要一些额外的资源去处理这些工作,通常吞吐量会低一些。参数:-XX:+UseConcMarkSweepGC、-XX:+UseG1GC、-XX:+UseZGC 等。 + +## 第 09 讲:案例实战:面对突如其来的 GC 问题如何下手解决 + +## 第 10 讲:动手实践:自己模拟 JVM 内存溢出场景 + +## 第 11 讲:动手实践:遇到问题不要慌,轻松搞定内存泄漏 + +jinfo、jstat、jstack、jhsdb(jmap)等是经常被使用的一些工具,尤其是 jmap,在分析处理内存泄漏问题的时候,是必须的。 + +## 工具进阶:如何利用 MAT 找到问题发生的根本原因 + +MAT 是用来分析内存快照的。 + +## 动手实践:让面试官刮目相看的堆外内存排查 + +## 预警与解决:深入浅出 GC 监控与调优 + +## 案例分析:一个高死亡率的报表系统的优化之路 + +## 案例分析:分库分表后,我的应用崩溃了 + +## 动手实践:从字节码看方法调用的底层实现 + +## 大厂面试题:不要搞混 JMM 与 JVM + +## 动手实践:从字节码看并发编程的底层实现 + +## 动手实践:不为人熟知的字节码指令 + +## 深入剖析:如何使用 Java Agent 技术对字节码进行修改 + +## 23 动手实践:JIT 参数配置如何影响程序运行? + +## 案例分析:大型项目如何进行性能瓶颈调优? + +## 未来:JVM 的历史与展望 + +## 福利:常见 JVM 面试题补充 diff --git "a/source/_posts/99.\347\254\224\350\256\260/01.Java/01.\347\216\251\350\275\254Spring\345\205\250\345\256\266\346\241\266\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\347\216\251\350\275\254Spring\345\205\250\345\256\266\346\241\266\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/01.Java/01.\347\216\251\350\275\254Spring\345\205\250\345\256\266\346\241\266\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\347\216\251\350\275\254Spring\345\205\250\345\256\266\346\241\266\347\254\224\350\256\260.md" index 8a0d8480fd..e4c52f7496 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/01.Java/01.\347\216\251\350\275\254Spring\345\205\250\345\256\266\346\241\266\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/01.Java/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\347\216\251\350\275\254Spring\345\205\250\345\256\266\346\241\266\347\254\224\350\256\260.md" @@ -1,5 +1,5 @@ --- -title: 《玩转 Spring 全家桶》笔记 +title: 《极客时间教程 - 玩转 Spring 全家桶》笔记 date: 2023-07-29 15:25:09 order: 01 categories: @@ -11,10 +11,10 @@ tags: - Spring - SpringBoot - SpringCloud -permalink: /pages/eb5c76/ +permalink: /pages/6377841a/ --- -# 《玩转 Spring 全家桶》笔记 +# 《极客时间教程 - 玩转 Spring 全家桶》笔记 ## 第一章:初识 Spring (4 讲) @@ -255,9 +255,9 @@ TransactionDefinition | 隔离性 | 值 | 脏读 | 不可重复读 | 幻读 | | -------------------------- | --- | ---- | ---------- | ---- | -| ISOLATION_READ_UNCOMMITTED | 1 | ✔️️️ | ✔️️️ | ✔️️️ | -| ISOLATION_READ_COMMITTED | 2 | ❌ | ✔️️️ | ✔️️️ | -| ISOLATION_REPEATABLE_READ | 3 | ❌ | ❌ | ✔️️️ | +| ISOLATION_READ_UNCOMMITTED | 1 | ✔️️️ | ✔️️️ | ✔️️️ | +| ISOLATION_READ_COMMITTED | 2 | ❌ | ✔️️️ | ✔️️️ | +| ISOLATION_REPEATABLE_READ | 3 | ❌ | ❌ | ✔️️️ | | ISOLATION_SERIALIZABLE | 4 | ❌ | ❌ | ❌ | #### 编程式事务 @@ -2558,4 +2558,4 @@ Spring 事件机制 ## 参考资料 -- [玩转 Spring 全家桶](https://time.geekbang.org/course/intro/156) \ No newline at end of file +- [玩转 Spring 全家桶](https://time.geekbang.org/course/intro/156) diff --git "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/10.\344\273\2160\345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241.md" "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/10.\344\273\2160\345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241.md" rename to "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241.md" index 27295a3964..9a3b966e58 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/10.\344\273\2160\345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\345\276\256\346\234\215\345\212\241.md" @@ -1,7 +1,6 @@ --- -title: 《从 0 开始学微服务》笔记 +title: 《极客时间教程 - 从 0 开始学微服务》笔记 date: 2021-08-15 15:27:00 -order: 10 categories: - 笔记 - 设计 @@ -9,10 +8,10 @@ tags: - 设计 - 架构 - 微服务 -permalink: /pages/7bb3ee/ +permalink: /pages/0f546f2f/ --- -# 《从 0 开始学微服务》笔记 +# 《极客时间教程 - 从 0 开始学微服务》笔记 ## 到底什么是微服务? @@ -676,4 +675,4 @@ Service Mesh 实现的关键点: Istio 的架构可以说由两部分组成,分别是 Proxy 和 Control Plane。 - Proxy,就是前面提到的 SideCar,与应用程序部署在同一个主机上,应用程序之间的调用都通过 Proxy 来转发,目前支持 HTTP/1.1、HTTP/2、gRPC 以及 TCP 请求。 -- Control Plane,与 Proxy 通信,来实现各种服务治理功能,包括三个基本组件:Pilot、Mixer 以及 Citadel。 \ No newline at end of file +- Control Plane,与 Proxy 通信,来实现各种服务治理功能,包括三个基本组件:Pilot、Mixer 以及 Citadel。 diff --git "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/01.\344\273\2160\345\274\200\345\247\213\345\255\246\346\236\266\346\236\204\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\346\236\266\346\236\204\347\254\224\350\256\260.md" similarity index 83% rename from "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/01.\344\273\2160\345\274\200\345\247\213\345\255\246\346\236\266\346\236\204\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\346\236\266\346\236\204\347\254\224\350\256\260.md" index 3af3aba3cf..128c982f85 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/01.\344\273\2160\345\274\200\345\247\213\345\255\246\346\236\266\346\236\204\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\346\236\266\346\236\204\347\254\224\350\256\260.md" @@ -1,17 +1,16 @@ --- -title: 《从0开始学架构》笔记 +title: 《极客时间教程 - 从0开始学架构》笔记 date: 2022-04-15 09:38:33 -order: 01 categories: - 笔记 - 设计 tags: - 设计 - 架构 -permalink: /pages/531ef7/ +permalink: /pages/5d649f09/ --- -# 《从 0 开始学架构》笔记 +# 《极客时间教程 - 从 0 开始学架构》笔记 ## 架构到底是指什么? @@ -45,4 +44,4 @@ permalink: /pages/531ef7/ ## 参考资料 -- [从 0 开始学架构](https://time.geekbang.org/column/intro/100006601) \ No newline at end of file +- [从 0 开始学架构](https://time.geekbang.org/column/intro/100006601) diff --git "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/03.\345\267\246\350\200\263\345\220\254\351\243\216\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\267\246\350\200\263\345\220\254\351\243\216\347\254\224\350\256\260.md" similarity index 90% rename from "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/03.\345\267\246\350\200\263\345\220\254\351\243\216\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\267\246\350\200\263\345\220\254\351\243\216\347\254\224\350\256\260.md" index 14e1c40dbd..73a822c379 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/03.\345\267\246\350\200\263\345\220\254\351\243\216\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\267\246\350\200\263\345\220\254\351\243\216\347\254\224\350\256\260.md" @@ -1,17 +1,16 @@ --- -title: 《左耳听风》笔记 +title: 《极客时间教程 - 左耳听风》笔记 date: 2021-08-15 15:27:00 -order: 03 categories: - 笔记 - 设计 tags: - 设计 - 架构 -permalink: /pages/74d040/ +permalink: /pages/4f309ecf/ --- -# 《左耳听风》笔记 +# 《极客时间教程 - 左耳听风》笔记 ## 洞悉技术的本质 @@ -79,4 +78,4 @@ permalink: /pages/74d040/ 5. **技术的底层原理和关键实现**。 6. **已有的实现和它之间的对比**。 -## 高效沟通 \ No newline at end of file +## 高效沟通 diff --git "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/11.\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" similarity index 98% rename from "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/11.\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" index 75b30f90f7..194b75befd 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/11.\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\276\256\346\234\215\345\212\241\346\236\266\346\236\204\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" @@ -1,7 +1,6 @@ --- -title: 《微服务架构核心 20 讲》笔记 +title: 《极客时间教程 - 微服务架构核心 20 讲》笔记 date: 2022-06-26 18:09:46 -order: 11 categories: - 笔记 - 设计 @@ -9,10 +8,10 @@ tags: - 设计 - 架构 - 微服务 -permalink: /pages/b4661f/ +permalink: /pages/dff7afff/ --- -# 《微服务架构核心 20 讲》笔记 +# 《极客时间教程 - 微服务架构核心 20 讲》笔记 ## 什么是微服务架构 @@ -312,4 +311,4 @@ UAT 环境: User Acceptance Test (用户验收测试) 基于容器的云发布体系 -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220628071152.png) \ No newline at end of file +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220628071152.png) diff --git "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/02.\346\236\266\346\236\204\345\256\236\346\210\230\346\241\210\344\276\213\350\247\243\346\236\220\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\236\266\346\236\204\345\256\236\346\210\230\346\241\210\344\276\213\350\247\243\346\236\220\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/02.\346\236\266\346\236\204\345\256\236\346\210\230\346\241\210\344\276\213\350\247\243\346\236\220\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\236\266\346\236\204\345\256\236\346\210\230\346\241\210\344\276\213\350\247\243\346\236\220\347\254\224\350\256\260.md" index 659613ed10..6be8562d9b 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/02.\346\236\266\346\236\204\345\256\236\346\210\230\346\241\210\344\276\213\350\247\243\346\236\220\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\236\266\346\236\204\345\256\236\346\210\230\346\241\210\344\276\213\350\247\243\346\236\220\347\254\224\350\256\260.md" @@ -1,17 +1,16 @@ --- -title: 《架构实战案例解析》笔记 +title: 《极客时间教程 - 架构实战案例解析》笔记 date: 2021-08-26 23:32:00 -order: 02 categories: - 笔记 - 设计 tags: - 设计 - 架构 -permalink: /pages/c75707/ +permalink: /pages/7d0c063c/ --- -# 《架构实战案例解析》笔记 +# 《极客时间教程 - 架构实战案例解析》笔记 ## 架构的本质:如何打造一个有序的系统? @@ -395,4 +394,4 @@ SOA 架构 ## 参考资料 -[架构实战案例解析](https://time.geekbang.org/column/intro/100046301) \ No newline at end of file +[架构实战案例解析](https://time.geekbang.org/column/intro/100046301) diff --git "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\347\247\222\346\235\200\347\263\273\347\273\237\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\347\247\222\346\235\200\347\263\273\347\273\237\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..3c98e8c2bc --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\347\247\222\346\235\200\347\263\273\347\273\237\347\254\224\350\256\260.md" @@ -0,0 +1,266 @@ +--- +title: 《极客时间教程 - 秒杀系统》笔记 +date: 2025-01-07 07:49:18 +categories: + - 笔记 + - 设计 +tags: + - 设计 + - 架构 + - 秒杀系统 + - 超卖 +permalink: /pages/c10a0f37/ +--- + +# 《极客时间教程 - 秒杀系统》笔记 + +## 开篇词丨秒杀系统架构设计都有哪些关键点? + +秒杀的整体架构可以概括为“稳、准、快”几个关键字 + +- **稳-高可用** - 服务需要考虑各种容错场景,保证服务可用 +- **准-一致性** - 高并发下的库存数量增减不能出错,避免超卖 +- **快-高性能** - 支持高并发的读写 + +## 设计秒杀系统时应该注意的 5 个架构原则 + +秒杀系统本质上就是一个满足大并发、高性能和高可用的分布式系统。 + +### 架构原则:“4 要 1 不要” + +- **数据尽量少** + - 请求及响应的数据量越小,则传输数据量越小,可以显著减少 CPU 和带宽; + - 依赖数据库的数据越少,数据库压力越小,I/O 耗时越少 +- **请求数尽量少** - 合并 css+js,减少静态资源的请求数 +- **路径尽量短** + - 路径,是指用户发出请求、收到响应的整个过程中,数据经过的节点数。 + - 路径越短,则 I/O 传输耗时越少,也更加可靠。 +- **依赖尽量少** - 依赖,是指要完成一次用户请求必须依赖的系统或者服务。 +- **避免单点** +- 对于应用服务,应设计为无状态,然后以集群模式提供整体服务,以此提高可用性 +- 对于数据库,应通过副本机制+故障转移,来保证可用性。 + +### 不同场景下的不同架构案例 + +(1)请求量级 10w QPS 的架构 + +架构要点: + +1. 把秒杀系统独立出来单独打造一个系统,这样可以有针对性地做优化 +2. 在系统部署上也独立做一个机器集群,这样秒杀的大流量就不会影响到正常的商品购买集群的机器负载; +3. 将热点数据(如库存数据)单独放到一个缓存系统中,以提高“读性能”; +4. 增加秒杀答题,防止有秒杀器抢单。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070731631.png) + +(1)请求量级 100w QPS 的架构 + +1. 对页面进行彻底的动静分离,使得用户秒杀时不需要刷新整个页面,而只需要点击抢宝按钮,借此把页面刷新的数据降到最少; +2. 在服务端对秒杀商品进行本地缓存,不需要再调用依赖系统的后台服务获取数据,甚至不需要去公共的缓存集群中查询数据,这样不仅可以减少系统调用,而且能够避免压垮公共缓存集群。 +3. 增加系统限流保护,防止最坏情况发生。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070731927.png) + +小结:架构之道,在于权衡取舍。要取得极致的性能,往往要在通用性、易用性、成本等方面有所牺牲,反之亦然。 + +## 如何才能做好动静分离?有哪些方案可选? + +### 何为动静数据 + +**“动态数据”和“静态数据”的主要区别就是看页面中输出的数据是否和 URL、浏览者、时间、地域相关,以及是否含有 Cookie 等私密数据**。 + +所谓“动态”还是“静态”,并不是说数据本身是否动静,而是数据中是否含有和访问者相关的个性化数据。更通俗的来说,是不是每个人看到的页面是相同的。 + +怎样对静态数据做缓存呢? + +- **第一,你应该把静态数据缓存到离用户最近的地方**。常见技术:CDN、Cookie、服务器缓存 +- **第二,静态化改造就是要直接缓存 HTTP 连接**。例如:Nginx 静态缓存 +- 第三,让谁来缓存静态数据也很重要。Web 服务器(如 Nginx、Apache、Varnish)更擅长处理大并发的静态文件请求。 + +### 如何做动静分离的改造 + +1. **URL 唯一化**。商品详情系统天然地就可以做到 URL 唯一化,比如每个商品都由 ID 来标识,那么 `http://item.xxx.com/item.htm?id=xxxx` 就可以作为唯一的 URL 标识。为啥要 URL 唯一呢?前面说了我们是要缓存整个 HTTP 连接,那么以什么作为 Key 呢?就以 URL 作为缓存的 Key,例如以 id=xxx 这个格式进行区分。 +2. **分离浏览者相关的因素**。浏览者相关的因素包括身份、认证信息等。这部分少量数据可以通过动态请求来获取。 +3. **分离时间因素**。服务端输出的时间也通过动态请求获取。 +4. **异步化地域因素**。详情页面上与地域相关的因素做成异步方式获取,当然你也可以通过动态请求方式获取,只是这里通过异步获取更合适。 +5. **去掉 Cookie**。服务端输出的页面包含的 Cookie 可以通过代码软件来删除,如 Web 服务器 Varnish 可以通过 unset req.http.cookie 命令去掉 Cookie。注意,这里说的去掉 Cookie 并不是用户端收到的页面就不含 Cookie 了,而是说,在缓存的静态数据中不含有 Cookie。 + +分离出动态内容之后,如何组织这些内容页就变得非常关键了。动态内容的处理通常有两种方案: + +1. **ESI 方案(或者 SSI)**:即在 Web 代理服务器上做动态内容请求,并将请求插入到静态页面中,当用户拿到页面时已经是一个完整的页面了。这种方式对服务端性能有些影响,但是用户体验较好。 +2. **CSI 方案**。即单独发起一个异步 JavaScript 请求,以向服务端获取动态内容。这种方式服务端性能更佳,但是用户端页面可能会延时,体验稍差。 + +### 动静分离的几种架构方案 + +#### 方案 1:实体机单机部署 + +这种方案是将虚拟机改为实体机,以增大 Cache 的容量,并且采用了一致性 Hash 分组的方式来提升命中率。这里将 Cache 分成若干组,是希望能达到命中率和访问热点的平衡。Hash 分组越少,缓存的命中率肯定就会越高,但短板是也会使单个商品集中在一个分组中,容易导致 Cache 被击穿,所以我们应该适当增加多个相同的分组,来平衡访问热点和命中率的问题。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070732212.png) + +实体机单机部署有以下几个优点: + +1. 没有网络瓶颈,而且能使用大内存; +2. 既能提升命中率,又能减少 Gzip 压缩; +3. 减少 Cache 失效压力,因为采用定时失效方式,例如只缓存 3 秒钟,过期即自动失效。 + +缺点: + +- 一定程度上也造成了 CPU 的浪费,因为单个的 Java 进程很难用完整个实体机的 CPU。 +- 一个实体机上部署了 Java 应用又作为 Cache 来使用,这造成了运维上的高复杂度。 + +#### 方案 2:统一 Cache 层 + +所谓统一 Cache 层,就是将单机的 Cache 统一分离出来,形成一个单独的 Cache 集群。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070732629.png) + +优点: + +1. 应用无需单独维护 Cache +2. 运维简单 +3. 可以共享内存,最大化利用内存 + +缺点: + +1. Cache 层内部交换网络成为瓶颈; +2. 缓存服务器的网卡也会是瓶颈; +3. 机器少风险较大,挂掉一台就会影响很大一部分缓存数据。 + +#### 方案 3:CDN + +动静分离后,缓存如果前置到 CDN,由于离用户更近,因此访问更快。 + +CDN 方案有以下问题: + +1. **失效问题**。需要考虑如果让 CDN 分布在全国各地的 Cache 在秒级时间内失效。 +2. **命中率问题**。如果将数据全部放到全国的 CDN 上,必然导致 Cache 分散,而 Cache 分散又会导致访问请求命中同一个 Cache 的可能性降低,那么命中率就成为一个问题。 +3. **发布更新问题**。若业务迭代快速,则发布系统必须足够简洁高效 + +将商品详情系统放到全国的所有 CDN 节点上是不太现实的,因为存在失效问题、命中率问题以及系统的发布更新问题。那么是否可以选择若干个节点来尝试实施呢?答案是“可以”,但是这样的节点需要满足几个条件: + +1. 靠近访问量比较集中的地区; +2. 离主站相对较远; +3. 节点到主站间的网络比较好,而且稳定; +4. 节点容量比较大,不会占用其他 CDN 太多的资源。 + +最后,还有一点也很重要,那就是:节点不要太多。 + +基于上面几个因素,选择 CDN 的二级 Cache 比较合适,因为二级 Cache 数量偏少,容量也更大,让用户的请求先回源的 CDN 的二级 Cache 中,如果没命中再回源站获取数据,部署方式如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070732753.png) + +## 二八原则:有针对性地处理好系统的“热点数据” + +所谓“静态热点数据”,就是能够提前预测的热点数据。例如,我们可以通过卖家报名的方式提前筛选出来,通过报名系统对这些热点商品进行打标。另外,我们还可以通过大数据分析来提前发现热点商品,比如我们分析历史成交记录、用户的购物车记录,来发现哪些商品可能更热门、更好卖,这些都是可以提前分析出来的热点。 + +所谓“动态热点数据”,就是不能被提前预测到的,系统在运行过程中临时产生的热点。例如,卖家在抖音上做了广告,然后商品一下就火了,导致它在短时间内被大量购买。 + +### 发现热点数据 + +动态热点发现系统的具体实现。 + +1. 构建一个异步的系统,它可以收集交易链路上各个环节中的中间件产品的热点 Key,如 Nginx、缓存、RPC 服务框架等这些中间件(一些中间件产品本身已经有热点统计模块)。 +2. 建立一个热点上报和可以按照需求订阅的热点服务的下发规范,主要目的是通过交易链路上各个系统(包括详情、购物车、交易、优惠、库存、物流等)访问的时间差,把上游已经发现的热点透传给下游系统,提前做好保护。比如,对于大促高峰期,详情系统是最早知道的,在统一接入层上 Nginx 模块统计的热点 URL。 +3. 将上游系统收集的热点数据发送到热点服务台,然后下游系统(如交易系统)就会知道哪些商品会被频繁调用,然后做热点保护。 + +这里我给出了一个图,其中用户访问商品时经过的路径有很多,我们主要是依赖前面的导购页面(包括首页、搜索页面、商品详情、购物车等)提前识别哪些商品的访问量高,通过这些系统中的中间件来收集热点数据,并记录到日志中。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070733476.png) + +### 处理热点数据 + +**处理热点数据通常有几种思路:一是优化,二是限制,三是隔离**。 + +具体到“秒杀”业务,我们可以在以下几个层次实现隔离。 + +1. **业务隔离**。把秒杀做成一种营销活动,卖家要参加秒杀这种营销活动需要单独报名,从技术上来说,卖家报名后对我们来说就有了已知热点,因此可以提前做好预热。 +2. **系统隔离**。系统隔离更多的是运行时的隔离,可以通过分组部署的方式和另外 99%分开。秒杀可以申请单独的域名,目的也是让请求落到不同的集群中。 +3. **数据隔离**。秒杀所调用的数据大部分都是热点数据,比如会启用单独的 Cache 集群或者 MySQL 数据库来放热点数据,目的也是不想 0.01%的数据有机会影响 99.99%数据。 + +## 流量削峰这事应该怎么做? + +流量削峰的思路:排队、答题、分层过滤 + +**排队** - 使用 MQ 削峰、解耦 + +适用于内部上下游系统之间调用请求不平缓的场景,由于内部系统的服务质量要求不能随意丢弃请求,所以使用消息队列能起到很好的削峰和缓冲作用。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070739467.png) + +**答题** - 延缓请求、限制秒杀器 + +适用于秒杀或者营销活动等应用场景,在请求发起端就控制发起请求的速度,因为越到后面无效请求也会越多,所以配合后面介绍的分层拦截的方式,可以更进一步减少无效请求对系统资源的消耗。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070739219.png) + +**分层过滤** - 请求分别经过 CDN、前台读系统(如商品详情系统)、后台系统(如交易系统)和数据库这几层分层过滤。 + +分层过滤非常适合交易性的写请求,比如减库存或者拼车这种场景,在读的时候需要知道还有没有库存或者是否还有剩余空座位。但是由于库存和座位又是不停变化的,所以读的数据是否一定要非常准确呢?其实不一定,你可以放一些请求过去,然后在真正减的时候再做强一致性保证,这样既过滤一些请求又解决了强一致性读的瓶颈。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070740876.png) + +分层校验的基本原则是: + +1. 将动态请求的读数据缓存(Cache)在 Web 端,过滤掉无效的数据读; +2. 对读数据不做强一致性校验,减少因为一致性校验产生瓶颈的问题; +3. 对写数据进行基于时间的合理分片,过滤掉过期的失效请求; +4. 对写请求做限流保护,将超出系统承载能力的请求过滤掉; +5. 对写数据进行强一致性校验,只保留最后有效的数据。 + +## 影响性能的因素有哪些?又该如何提高系统的性能? + +- **影响性能的因素**:响应时间、线程数 +- **如何发现瓶颈**: + - 瓶颈点:CPU、内存、磁盘、带宽 + - 针对 CPU 而言,可以使用 CPU 相关工具:JProfile、Yourkit、jstack,此外,还可以使用链路追踪进行链路分析 +- **如何优化系统**:编码、序列化、压缩、传输方式(NIO)、并发 + +## 秒杀系统“减库存”设计的核心逻辑 + +减库存的一般方式: + +- 下单减库存:不会出现超卖;不能应对下单不付款的情况 +- 付款减库存:高并发下,可能出现超卖——下单后无法付款的情况(库存已经清空) +- 预扣库存:买家下单后,库存为其保留一定的时间(如 10 分钟),超过这个时间,库存将会自动释放,释放后其他买家就可以继续购买。在买家付款前,系统会校验该订单的库存是否还有保留:如果没有保留,则再次尝试预扣;如果库存不足(也就是预扣失败)则不允许继续付款;如果预扣成功,则完成付款并实际地减去库存。 + +针对秒杀场景,一般“抢到就是赚到”,所以成功下单后却不付款的情况比较少,再加上卖家对秒杀商品的库存有严格限制,所以秒杀商品采用“下单减库存”更加合理。另外,理论上,“下单减库存”比“预扣库存”以及涉及第三方支付的“付款减库存”在逻辑上更为简单,所以性能上更占优势。 + +“下单减库存”在数据一致性上,主要就是保证大并发请求时库存数据不能为负数,也就是要保证数据库中的库存字段值不能为负数,一般我们有多种解决方案:一种是在应用程序中通过事务来判断,即保证减后库存不能为负数,否则就回滚;另一种办法是直接设置数据库的字段数据为无符号整数,这样减后库存字段值小于零时会直接执行 SQL 语句来报错;再有一种就是使用 CASE WHEN 判断语句,例如这样的 SQL 语句: + +``` +UPDATE item SET inventory = CASE WHEN inventory >= xxx THEN inventory-xxx ELSE inventory END +``` + +## 准备 Plan B:如何设计兜底方案 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070741134.png) + +高可用系统建设: + +1. **设计阶段**:考虑系统的可扩展性和容错性。避免单点问题,采用多活方案(多机房部署)。 +2. **编码阶段**:保证代码的健壮性。识别边界,捕获、处理异常;设置合适的超时机制。 +3. **测试阶段**:测试用例覆盖度尽量全面。 +4. **发布阶段**:自动化发布,支持灰度发布、回滚。 +5. **运行阶段**:健全监控机制:日志、指标、链路监控 +6. **故障发生**:容错处理、故障恢复、故障演练 + +### 降级 + +“降级”,就是当系统的容量达到一定程度时,限制或者关闭系统的某些非核心功能,从而把有限的资源保留给更核心的业务。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070741174.png) + +### 限流 + +限流就是当系统容量达到瓶颈时,我们需要通过限制一部分流量来保护系统,并做到既可以人工执行开关,也支持自动化保护的措施。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202501070745436.png) + +### 拒绝服务 + +**过载保护** - 当系统负载达到一定阈值时,例如 CPU 使用率达到 90%或者系统 load 值达到 2\*CPU 核数时,系统直接拒绝所有请求,这种方式是最暴力但也最有效的系统保护方式。 + +拒绝服务可以说是一种不得已的兜底方案,用以防止最坏情况发生,防止因把服务器压跨而长时间彻底无法提供服务。像这种系统过载保护虽然在过载时无法提供服务,但是系统仍然可以运作,当负载下降时又很容易恢复,所以每个系统和每个环节都应该设置这个兜底方案,对系统做最坏情况下的保护。 + +高可用建设需要长期规划并进行体系化建设,要在预防(建立常态的压力体系,例如上线前的单机压测到上线后的全链路压测)、管控(做好线上运行时的降级、限流和兜底保护)、监控(建立性能基线来记录性能的变化趋势以及线上机器的负载报警体系,发现问题及时预警)和恢复体系(遇到故障要及时止损,并提供快速的数据订正工具等)等这些地方加强建设。 \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/21.\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\24140\351\227\256\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\24140\351\227\256\347\254\224\350\256\260.md" similarity index 97% rename from "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/21.\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\24140\351\227\256\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\24140\351\227\256\347\254\224\350\256\260.md" index 37da418516..cc7cd356e9 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/21.\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\24140\351\227\256\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/03.\350\256\276\350\256\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\351\253\230\345\271\266\345\217\221\347\263\273\347\273\237\350\256\276\350\256\24140\351\227\256\347\254\224\350\256\260.md" @@ -1,7 +1,6 @@ --- -title: 《高并发系统设计 40 问》笔记 +title: 《极客时间教程 - 高并发系统设计 40 问》笔记 date: 2021-08-05 23:42:00 -order: 21 categories: - 笔记 - 设计 @@ -9,10 +8,10 @@ tags: - 设计 - 架构 - 高并发 -permalink: /pages/daf740/ +permalink: /pages/98f4abb5/ --- -# 《高并发系统设计 40 问》笔记 +# 《极客时间教程 - 高并发系统设计 40 问》笔记 ## 基础篇 @@ -229,4 +228,4 @@ CPU、内存、磁盘、网络 - 配置中心可以提供配置变更通知的功能,可以实现配置的热更新; - 配置中心关注的性能指标中,可用性的优先级是高于性能的,一般我们会要求配置中心的可用性达到 99.999%,甚至会是 99.9999%。 -## 实战篇 \ No newline at end of file +## 实战篇 diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/Elasticsearch\345\256\236\346\210\230\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/Elasticsearch\345\256\236\346\210\230\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..05e028076b --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/Elasticsearch\345\256\236\346\210\230\347\254\224\350\256\260.md" @@ -0,0 +1,145 @@ +--- +title: 《Elasticsearch 实战》笔记 +date: 2024-11-05 07:02:10 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +permalink: /pages/0f97fbb0/ +--- + +# 《Elasticsearch 实战》笔记 + +## 第 1 章 Elasticsearch 介绍 + +- Elasticsearch 是构建在 Apache Lucene 基础之上的开源分布式搜索引擎。 +- Elasticsearch 常见的用法是索引大规模的数据,这样可以运行全文搜索和实时数据统计。 +- Elasticsearch 提供的特性远远超越了全文搜索。例如,可以调优搜索相关性并提供搜索建议。 +- 对于数据的索引和搜索,以及集群配置的管理,都可以使用 HTTP API 的 JSON,并获得 JSON 应答。 +- 可以将 Elasticsearch 当作一个 NoSQL 的数据存储,包括了实时性搜索和分析能力。它是面向文档的,默认情况下就是可扩展的。 +- Elasticsearch 自动将数据划分为分片,在集群中的服务器上做负载均衡。这使得动态添加和移除服务器变得很容易。分片也可以复制,使得集群具有容错性。 + +## 第 2 章 深入功能 + +- Elasticsearch 默认是面向文档的、可扩展的且没有固定模式(schema)的。 +- 尽管可以使用默认设置来组建集群,但在继续前行之前至少应该调整一些配置。例如, 集群的名称和堆的大小。 +- 写请求在主分片中分发,然后复制到这些主分片的副本分片。 +- 搜索在多组完整数据上轮询执行,每组数据由主分片或副本分片组成。接收搜索请求的节点将来自多份分片的部分结果进行聚合,然后将综合的结果返 回给应用程序。 +- 可以通过 HTTP 请求的 JSON 有效载荷,发送新的文档和搜索参数,然后获取 JSON 应答。 + +## 第 3 章 索引、更新和删除数据 + +- 映射定义了文档中的字段,以及这些字段是如何被索引的。Elasticsearch 是无模式的,是因为映射是自动扩展的。不过在实际生产中,需要经常控制哪些被 索引,哪些被存储,以及如何存储。 +- 文档中的多数字段是核心类型,如字符串和数值。这些字段的索引方式对于 Elasticsearch 的表现以及搜索结果的相关性有着很大的影响。 +- 单一字段也可以包含多个字段或取值。我们了解了数组和多字段,它们让你在单一字段中拥有同一核心类型的多个实例。 +- 除了用于文档的字段,Elasticsearch 还提供了预定义的字段,如 `_source` 和 `_all`。配置这些字段将修改某些你并没有显式提供给文档的数据,但是对于性能和功能都有很大影响。例如,可以决定哪些字段需要在 `_all`里索引。 +- 由于 Elasticsearch 在 Lucene 分段里存储数据,而分段一旦创建就不会修改,因此更新文档意味着检索现存的文档,将修改放入即将索引的新文档中,然后删除旧的索引。 +- 当 Lucene 分段异步合并时,就会移除待删的文档。这也是为什么删除整个索引要比删除单个或多个文档要快——索引删除只是意味着移除磁盘上的文件,而且无须合并。 +- 在索引、更新和删除过程中,可以使用文档版本来管理并发问题。对于更新而言,如果因为并发问题而导致更新失败了,可以告诉 Elasticsearch 自动重试。 + +## 第 4 章 搜索数据 + +- 人类语言类型的查询,如 match 和 query_string 查询,对于搜索框而言是非常合适的。 +- match 查询对于全文搜索而言是核心类型,但是 query_string 查询更为灵活,也更为复杂,因为它暴露了全部的 Lucene 查询语法。 +- match 查询有多个子类型:boolean、phrase 和 phrase_prefix。主要的区别在于 boolean 匹配单独的关键词,而 phrase 考虑了多个单词在词组里的顺序。 +- 像 prefix 和 wildcard 这样的特殊查询,Elasticsearch 也是支持的。 +- 要过滤某个字段不存在的文档,请使用 missing 过滤器。 +- exists 过滤器恰恰相反,它只返回拥有指定字段值的文档。 + +## 第 5 章 分析数据 + +- 分析是通过文档字段的文本,生成分词的过程。在 match 查询这样的查询中,搜索字符串会经过同样的过程,如果一篇文档的分词和搜索字符串的分词相匹配,那么它就会和搜索匹配。 +- 通过映射,每个字段都会分配一个分析器。分析器既可以在 Elasticsearch 配置或索引设置中定义,也可以是一个默认的分析器。 +- 分析器是处理的链条,由一个分词器以及若干在此分析器之前的字符过滤器、在此分词器之后的分词过滤器组成。 +- 在字符串传送到分词器之前,字符过滤器用于处理这些字符串。例如,可以使用映射字符过滤器将字符“&”转化为“and”。 +- 分词器用于将字符串切分为多个分词。例如,空白分词器将使用空格来划分单词。 +- 分词过滤器用于处理分词器所产生的分词。例如,可以使用词干提取来将单词缩减为其词根,并让搜索在该词的复数和单数形式上都可以正常运作。 +- N 元语法分词过滤器使用单词的部分来产生分词。例如,可以让每两个连续的字符生成一个分词。如果希望即使搜索字符串包含错误拼写,搜索还能奏效,那么这个就很有帮助了。 +- 侧边 N 元语法就像 N 元语法一样,但是它们只从单词的头部或结尾开始。例如,对于 “event”可以获得 e、ev 和 eve 分词。 +- 在词组级别,滑动窗口分词过滤器和 N 元语法分词过滤器相似。例如,可以使用词组里每两个连续的单词来生成分词。当用户希望提升多词匹配的相关性时,例如,在产品的简短描述中,这一点就很有帮助。 + +## 第 6 章 使用相关性进行搜索 + +- 词条的频率和词条出现在文档中的次数被用于计算查询词条的得分。 +- Elasticsearch 有很多工具来定制和修改得分。 +- 重新计算部分文档的得分将会减小评分机制的影响。 +- 使用解释 API 接口来理解文档是如何被评分的。 +- Function_score 查询使用户拥有了对文档得分最终极的控制权。 +- 理解字段数据缓存,将有助于用户理解 Elasticsearch 集群是如何使用内存的。 +- 如果字段数据缓存消耗了过多的内存,可以使用像 doc_values 这样的替换方案。 + +## 第 7 章 使用聚集来探索数据 + +- 通过结果文档的词条计数和统计值计算,聚集帮助用户获得查询结果的概览。 +- 聚集是 Elasticsearch 中一种新形式的切面,拥有更多类型,还可以将它们组合以获取对数据更深入的理解。 +- 主要有两种类型的聚集:桶型和度量型。 +- 度量型聚集计算一组文档上的统计值,如某个数值型字段的最小值、最大值或者平均值。 +- 某些度量型聚集通过近似算法来计算,这使得它们具有比精确聚集更好的扩展性。百分位 percentiles 和 cardinality 聚集就是如此。 +- 桶型聚集将文档放入 1 个或多个桶中,并为这些桶返回计数器。例如,某个论坛中最流行的帖子。用户可以在桶型聚集中嵌入子聚集,对于父聚集所产生的每个桶一次性地运行子聚集。比如,对于匹配每个标签的博客帖,可以使用嵌套来获得该结果集的平均评论数。 +- top_hits 聚集可以用作一种子聚集,来实现结果的分组。 +- terms 聚集通常用于发现活跃的用户、常见地址、热门的物品等场景。其他的多桶型聚集是 terms 聚集的变体,如 signif icant_terms 聚集,返回了相对于整体索引而言,查询结果集中经常出现的词。 +- range 和 date_range 聚集用于对数值和日期数据的分类。而 histogram 和 date_histogram 聚集是类似的,不过它们使用固定的间距而不是人工定义的范围。 +- 单桶型聚集,如 global, filter, filters 和 missing 聚集,可以修改用于其他聚集运行的文档集合,因为默认情况下文档集合是由查询所确定的。 + +## 第 8 章 文档间的关系 + +- 对象映射,对于一对一关系最有用。 +- 嵌套文档和父子结构,处理了一对多的关系。 +- 反规范化和应用端的连接,对于多对多的关系而言最有帮助。 + +即使是在本地进行,连接操作仍然损害了性能。所以,通常情况下将尽量多的属性放入单个 文档是个好主意。对象映射能起到作用是因为它允许文档中存在层级结构。这里搜索和聚集就像在扁平结构的文档上一样运作。需要使用全路径来指向字段,就像 location.name。 + +- 嵌套文档通常是在索引的时候进行连接,将多个 Lucene 文档放入单个分块。对于应用,分块看上去就像一篇单独的 Elasticsearch 文档。 +- \_parent 字段允许你在同一索引中将一篇文档指向其父辈,也就是另一篇不同类型的文档。Elasticsearch 将使用路由来确保父辈和子辈存储在同一分片上,这样查询的时候只需 要进行本地连接。 + +可以使用下面的查询和过滤器来搜索嵌套和父子文档。 + +- nested 查询和过滤器。 +- has_child 查询和过滤器。 +- has_parent 查询和过滤器。 + +## 第 9 章 向外扩展 + +- Elasticsearch 集群是如何组建的、是如何由多个节点构成的,每个节点包含了多个索引,而每个索引又是由多个分片组成。 +- 当节点加入 Elasticsearch 集群的时候会发生什么。 +- 主节点是如何选举的。 +- 删除和停用节点。 +- 使用 `__cat` API 接口来了解你的集群。 +- 什么是过度分片,以及如何使用它来规划集群的未来成长。 +- 如何使用别名和路由来提升集群的灵活性和可扩展性。 + +## 第 10 章 提升性能 + +- 使用 bulk 批量 API 接口将多个 index、create、update 或者 delete 操作合并到同一个请求。 +- 为了组合多个 get 或多个 search 请求,你分别可以使用多条获取或多条搜索的 API。 +- 当索引缓冲区已满、事务日志过大或上次冲刷过去太久的时候,冲刷的操作将内存中的 Lucene 分段提交到磁盘。 +- 刷新使得新的分段,无论是否冲刷,都可以用于搜索。在索引操作密集的时候,最好降低刷新的频率或者干脆关闭刷新。 +- 合并的策略可以根据分段的多少来调优。较少的分段使得搜索更快,不过合并需要花费更多的 CPU 时间。较多的分段使得合并时间更少、索引更快,但是会导致搜索变慢。 +- 优化的操作会强制合并,对于处理很多搜索请求的静态索引,这可以很好的运作。 +- 存储限流可能会使合并落后于更新,限制了索引的性能。如果你的高速的 I/O, 放宽或者取消这个限制。 +- 组合使用在 bool 过滤中的位集合过滤器和 and/or/not 过滤中的非位集合过滤器。 +- 如果你的索引是静态不变的,在分片查询缓存中缓存数量和聚集。 +- 监控 JVM 堆的使用情况,预留足够的内存空间,这样就不会遇到频繁的垃圾回收或者 OOM 错误,但是同时也要给操作系统的缓存留出一些内存。 +- 如果首次查询太慢,而且你也不介意较慢的索引过程,请使用索引预热器。 +- 如果有足够的空间存储较大的索引,请使用 N 元语法和滑动窗口而不是模糊、通配或词组查询,这会使你的搜索运行得更快。 +- 在索引之前,使用所需的数据在文档中创建新的字段,这样通常可以避免使用脚本。 +- 合适的时候,在脚本中尝试使用 Lucene 表达式、词条统计和字段数据。 +- 如果脚本不会经常变化,请参考附录 B, 学习如何在 Elasticsearch 插件中书写一个本地脚本。 +- 如果在多个分片之中没有均衡的文档频率,使用 dfs_query_then_fetcho +- 如果不需要任何命中的文档,请使用 count 搜索类型。如果需要很多命中的文档,请使用 scan 搜索类型。 + +## 第 11 章 管理集群 + +- 默认映射为索引中重复的相似映射创建提供了便利。 +- 别名允许使用单个名字查询多个索引,因此让你可以按需分割数据。 +- 集群健康的 API 提供了一个简单的方式来测量集群、节点和分片的整体健康状态。 +- 使用慢索引和慢查询的日志,有助于诊断可能影响集群性能的索引和查询操作。 +- 充分理解 JVM 虚拟机、Lucene 和 Elasticsearch 是如何分配和使用内存的,可以预防操作 系统将进程交换到磁盘。 +- 快照 API 为使用网络存储的集群提供了便捷的备份和恢复方法,资料库插件将这个功能 扩展到了共用的云服务之上。 + +## 参考资料 + +- [《Elasticsearch 实战》](https://book.douban.com/subject/30380439/) diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/MongoDB\346\235\203\345\250\201\346\214\207\345\215\227\347\254\224\350\256\260\344\270\200.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/MongoDB\346\235\203\345\250\201\346\214\207\345\215\227\347\254\224\350\256\260\344\270\200.md" new file mode 100644 index 0000000000..bf870509c4 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/MongoDB\346\235\203\345\250\201\346\214\207\345\215\227\347\254\224\350\256\260\344\270\200.md" @@ -0,0 +1,726 @@ +--- +title: 《MongoDB 权威指南》笔记一 +date: 2024-09-29 07:45:34 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 文档数据库 + - MongoDB +permalink: /pages/ee6834b2/ +--- + +# 《MongoDB 权威指南》笔记一 + +## 第 1 章 MongoDB 简介 + +### MongoDB 简介 + +MongoDB 是一个分布式文档数据库,由 C++ 语言编写。 + +#### 面向文档 + +面向文档的数据库使用更灵活的“文档”模型取代了“行”的概念。通过嵌入文档和数组,面向文档的方式可以仅用一条记录来表示复杂的层次关系。 + +MongoDB 中也没有预定义模式(predefined schema):文档键值的类型和大小不是固定的。由于没有固定的模式,因此按需添加或删除字段变得更容易。 + +综上,**MongoDB 支持结构化、半结构化数据模型,可以动态响应结构变化**。 + +#### 功能丰富 + +MongoDB 提供了丰富的功能: + +- **索引** - MongoDB 支持通用的二级索引,并提供唯一索引、复合索引、地理空间索引及全文索引功能。此外,它还支持在不同层次结构(如嵌套文档和数组)上建立二级索引。 +- **聚合** - MongoDB 提供了一种基于数据处理管道的聚合框架。 +- **特殊的集合和索引类型** - MongoDB 支持有限生命周期(TTL)集合,适用于保存将在特定时间过期的数据,比如会话和固定大小的集合,以及用于保存最近的数据(日志)。MongoDB 还支持部分索引,可以仅对符合某个条件的文档创建索引,以提高效率并减少所需的存储空间。 +- **文件存储** - 针对大文件及文件元数据的存储,MongoDB 使用了一种非常易用的协议。 +- ... + +#### 分布式 + +MongoDB 作为分布式存储,自然也具备了分布式的一般特性: + +- 通过副本机制提供高可用 +- 通过分片提供扩容能力 + +## 第 2 章 入门指南 + +文档是 MongoDB 中的基本数据单元,可以粗略地认为其相当于关系数据库管理系统中的行(但表达力要强得多)。 + +类似地,集合可以被看作具有动态模式的表。 + +一个 MongoDB 实例可以拥有多个独立的数据库,每个数据库都拥有自己的集合。 + +每个文档都有一个特殊的键 "\_id",其在所属的集合中是唯一的。 + +MongoDB 自带了一个简单但功能强大的工具:mongo shell。mongo shell 对管理 MongoDB 实例和使用 MongoDB 的查询语言操作数据提供了内置的支持。它也是一个功能齐全的 JavaScript 解释器,用户可以根据需求创建或加载自己的脚本。 + +### 文档 + +文档是一组有序键值的集合。 + +文档中的值不仅仅是“二进制大对象”,它们可以是几种不同的数据类型之一(甚至可以是一个完整的嵌入文档)。 + +文档中的键是字符串类型。除了少数例外的情况,可以使用任意 UTF-8 字符作为键。 + +键中不能含有 `\0`(空字符)。这个字符用于表示一个键的结束。 + +`.` 和 `$` 是特殊字符,只能在某些特定情况下使用。 + +MongoDB 会区分类型和大小写。 + +下面这两个文档是不同的: + +```json +{"count" : 5} +{"count" : "5"} +``` + +下面这两个文档也不同: + +```json +{"count" : 5} +{"Count" : 5} +``` + +需要注意,MongoDB 中的文档不能包含重复的键。例如,下面这个文档是不合法的。 + +```json +{ "greeting": "Hello, world!", "greeting": "Hello, MongoDB!" } +``` + +### 集合 + +集合就是一组文档。如果将文档比作关系数据库中的行,那么一个集合就相当于一张表。 + +集合具有**动态模式**的特性。这意味着一个集合中的文档可以具有任意数量的不同“形状”。例如,以下两个文档可以存储在同一个集合中: + +```json +{"greeting" : "Hello, world!", "views": 3} +{"signoff": "Good night, and good luck"} +``` + +集合由其名称进行标识。集合名称可以是任意 UTF-8 字符串,但有以下限制。 + +- 集合名称不能是空字符串("")。 +- 集合名称不能含有 `\0`(空字符),因为这个字符用于表示一个集合名称的结束。 +- 集合名称不能以 `system.` 开头,该前缀是为内部集合保留的。例如,`system.users` 集合中保存着数据库的用户,`system.namespaces` 集合中保存着有关数据库所有集合的信息。 +- 用户创建的集合名称中不应包含保留字符 `$`。许多驱动程序确实支持在集合名称中使用 `$`,这是因为某些由系统生成的集合会包含它,但除非你要访问的是这些集合之一,否则不应在名称中使用 `$` 字符。 + +使用 `.` 字符分隔不同命名空间的子集合是一种组织集合的惯例。例如,有一个具有博客功能的应用程序,可能包含名为 `blog.posts` 和名为 `blog.authors` 的集合。这只是一种组织管理的方式,blog 集合(它甚至不必存在)与其“子集合”之间没有任何关系。 + +### 数据库 + +MongoDB 使用集合对文档进行分组,使用数据库对集合进行分组。一个 MongoDB 实例可以承载多个数据库,每个数据库有零个或多个集合。 + +数据库按照名称进行标识的。数据库名称可以是任意 UTF-8 字符串,但有以下限制: + +- 数据库名称不能是空字符串("")。 +- 数据库名称不能包含 `/`、`\`、`.`、`"`、`*`、`<`、`>`、`:`、`|`、`?`、`$`、单一的空格以及 `\0`(空字符),基本上只能使用 ASCII 字母和数字。 +- 数据库名称区分大小写。 +- 数据库名称的长度限制为 64 字节。 + +MongoDB 使用 WiredTiger 存储引擎之前,数据库名称会对应文件系统中的文件名。尽管现在已经不这样处理了,但之前的许多限制遗留了下来。 + +此外,还有一些数据库名称是保留的。这些数据库可以被访问,但它们具有特殊的语义。具体如下。 + +- **admin**:`admin` 数据库会在身份验证和授权时被使用。此外,某些管理操作需要访问此数据库。 +- **local**:在副本集中,`local` 用于存储复制过程中所使用的数据,而 `local` 数据库本身不会被复制。 +- **config**:MongoDB 的分片集群会使用 config 数据库存储关于每个分片的信息。通过将数据库名称与该库中的集合名称连接起来,可以获得一个完全限定的集合名称,称为命名空间 + +### 启动 MongoDB + +启动 MongoDB 的方式: + +- Unix 系统 - 执行 mongod +- Windows 系统 - 执行 mongod.exe + +如果没有指定参数,则 mongod 会使用默认的数据目录 `/data/db/`。如果数据目录不存在或不可写,那么服务器端将无法启动。因此在启动 MongoDB 之前,创建数据目录(如 mkdir -p /data/db/)并确保对该目录有写权限非常重要。 + +默认情况下,MongoDB 会监听 27017 端口上的套接字连接。如果端口不可用,那么服务器将无法启动。 + +### MongoDB Shell + +MongoDB 内置了 MongoDB Shell 工具来提供命令行交互工具。 + +要启动 shell,可以执行 mongo 文件。 + +【示例】MongoDB Shell 基本操作 + +```shell +# 查看有哪些数据库 +> show dbs +admin 0.000GB +config 0.000GB +fc_open_core 0.000GB +local 0.000GB +spring-tutorial 0.000GB +test 0.919GB + +# 切换到 test 数据库 +> use test +switched to db test + +# 插入文档 +> db.user.insertOne({ name: "dunwu", sex: 'man' }) +{ + "acknowledged" : true, + "insertedId" : ObjectId("670a281a2647017bf5f42962") +} + +# 查询文档 +} +> db.user.find() +{ "_id" : ObjectId("670a281a2647017bf5f42962"), "name" : "dunwu", "sex" : "man" } + +# 更新文档 +> db.user.updateOne({ name: "dunwu" }, { $set: { age: 30 } }) +{ "acknowledged" : true, "matchedCount" : 1, "modifiedCount" : 1 } + +# 删除文档 +> db.user.deleteOne({ name: "dunwu" }) +{ "acknowledged" : true, "deletedCount" : 1 } + +# 退出 MongoDB Shell +> quit() +``` + +### 数据类型 + +MongoDB 中的文档可以被认为是“类似于 JSON”的形式。 + +MongoDB 基本数据类型如下: + +**`null`** - `null` 类型用于表示空值或不存在的字段。 + +```json +{ "x": null } +``` + +**布尔类型** - 布尔类型的值可以为 true 或者 false。 + +```json +{ "x": true } +``` + +**数值类型** - shell 默认使用 64 位的浮点数来表示数值类型。因此,下面的数值在 shell 中看起来是“正常”的: + +```json +{"x" : 3.14} +{"x" : 3} +``` + +对于整数,可以使用 NumberInt 或 NumberLong 类,它们分别表示 4 字节和 8 字节的有符号整数。 + +```json +{"x" : NumberInt("3")} +{"x" : NumberLong("3")} +``` + +**字符串类型** - 任何 UTF-8 字符串都可以使用字符串类型来表示。 + +```json +{ "x": "foobar" } +``` + +**日期类型** - MongoDB 会将日期存储为 64 位整数,表示自 Unix 纪元(1970 年 1 月 1 日)以来的毫秒数,不包含时区信息。 + +```json +{"x" : new Date()} +``` + +**正则表达式** - 查询时可以使用正则表达式,语法与 JavaScript 的正则表达式语法相同。 + +```json +{"x" : /foobar/i} +``` + +**数组类型** - 集合或者列表可以表示为数组。 + +```json +{ "x": ["a", "b", "c"] } +``` + +**内嵌文档** - 文档可以嵌套其他文档,此时被嵌套的文档就成了父文档的值。 + +```json +{ "x": { "foo": "bar" } } +``` + +**Object ID** - Object ID 是一个 12 字节的 ID,是文档的唯一标识。MongoDB 中存储的每个文档都必须有一个 "\_id" 键。"\_id" 的值可以是任何类型,但其默认为 ObjectId。在单个集合中,每个文档的 "\_id" 值都必须是唯一的,以确保集合中的每个文档都可以被唯一标识。 + +```json +{"x" : ObjectId()} +``` + +ObjectId 占用了 12 字节的存储空间,可以用 24 个十六进制数字组成的字符串来表示。 + +``` +0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 + 时间戳 | 随机值 | 计数器(起始值随机) +``` + +ObjectId 的前 4 字节是从 Unix 纪元开始以秒为单位的时间戳。这提供了一些有用的属性。时间戳与接下来的 5 字节(稍后会介绍)组合在一起,在秒级别的粒度上提供了唯一性。 + +**二进制数据** - 二进制数据是任意字节的字符串,不能通过 shell 操作。如果要将非 UTF-8 字符串存入数据库,那么使用二进制数据是唯一的方法。 + +代码 - MongoDB 还可以在查询和文档中存储任意的 JavaScript 代码: + +```json +{"x" : function() { /* ... */ }} +``` + +最后,还有一些类型主要在内部使用(或被其他类型取代)。这些将根据需要在文中特别说明 + +### 使用 MongoDB shell(略) + +## 第 3 章 创建、更新和删除文档 + +### 插入文档 + +#### insertOne + +`insertOne` 方法用于**插入单条文档**。 + +insertOne 方法语法如下: + +``` +db.collection.insertOne(document, options) +``` + +- document - 要插入的单个文档。 +- options(可选) - 一个可选参数对象,可以包含 `writeConcern` 和 `bypassDocumentValidation` 等。 + +【示例】向 `movies` 集合中插入一条文档 + +```json +> db.movies.insertOne({"title" : "Stand by Me"}) +``` + +#### insertMany + +`insertMany` 方法用于**批量插入文档**。 + +`insertMany` 方法语法如下: + +``` +db.collection.insertMany(documents, options) +``` + +- documents - 要插入的文档数组。 +- options(可选) - 一个可选参数对象,可以包含 `ordered`、`writeConcern` 和 `bypassDocumentValidation` 等。 + +```json +> db.movies.insertMany([{"title" : "Ghostbusters"},{"title" : "E.T."},{"title" : "Blade Runner"}]); +``` + +在当前版本中,MongoDB 能够接受的最大消息长度是 48MB,因此在单次批量插入中能够插入的文档是有限制的。如果尝试插入超过 48MB 的数据,则多数驱动程序会将这个批量插入请求拆分为多个 48MB 的批量插入请求。 + +MongoDB 会对要插入的数据进行最基本的检查:检查文档的基本结构,如果不存在 "\_id" 字段,则自动添加一个。 + +### 删除文档 + +#### deleteOne + +`deleteOne` 方法用于**删除单条文档** + +```json +> db.movies.find() +{ "_id" : ObjectId("670a31a206fe06538fb4d138"), "title" : "Stand by Me" } +{ "_id" : ObjectId("670a31ab06fe06538fb4d139"), "title" : "Ghostbusters" } +{ "_id" : ObjectId("670a31ab06fe06538fb4d13a"), "title" : "E.T." } +{ "_id" : ObjectId("670a31ab06fe06538fb4d13b"), "title" : "Blade Runner" } + +> db.movies.deleteOne({"_id" : ObjectId("670a31a206fe06538fb4d138")}) +{ "acknowledged" : true, "deletedCount" : 1 } + +> db.movies.find() +{ "_id" : ObjectId("670a31ab06fe06538fb4d139"), "title" : "Ghostbusters" } +{ "_id" : ObjectId("670a31ab06fe06538fb4d13a"), "title" : "E.T." } +{ "_id" : ObjectId("670a31ab06fe06538fb4d13b"), "title" : "Blade Runner" } +``` + +#### deleteMany + +`deleteMany` 方法用于**删除满足筛选条件的所有文档** + +```json +> db.movies.find() +{ "_id" : 0, "title" : "Top Gun", "year" : 1986 } +{ "_id" : 1, "title" : "Back to the Future", "year" : 1985 } +{ "_id" : 3, "title" : "Sixteen Candles", "year" : 1984 } +{ "_id" : 4, "title" : "The Terminator", "year" : 1984 } +{ "_id" : 5, "title" : "Scarface", "year" : 1983 } + +> db.movies.deleteMany({"year" : 1984}){ "acknowledged" : true, "deletedCount" : 2 } + +> db.movies.find() +{ "_id" : 0, "title" : "Top Gun", "year" : 1986 } +{ "_id" : 1, "title" : "Back to the Future", "year" : 1985 } +{ "_id" : 5, "title" : "Scarface", "year" : 1983 } +``` + +可以使用 `deleteMany` 来**删除集合中的所有文档** + +```json +> db.movies.find() +{ "_id" : 0, "titl +e" : "Top Gun", "year" : 1986 }{ "_id" : 1, "titl +e" : "Back to the Future", "year" : 1985 }{ "_id" : 3, "titl +e" : "Sixteen Candles", "year" : 1984 }{ "_id" : 4, "titl +e" : "The Terminator", "year" : 1984 }{ "_id" : 5, "titl +e" : "Scarface", "year" : 1983 } + +> db.movies.deleteMany({}) +{ "acknowledged" :true, "deletedCount" : 5 } + +> db.movies.find() +``` + +### 更新文档 + +#### replaceOne + +`replaceOne` 方法用于**将新文档完全替换匹配的文档**。这对于进行大规模模式迁移的场景非常有用。 + +``` +db.collection.replaceOne(filter, replacement, options) +``` + +- **filter** - 用于查找文档的查询条件。 +- **replacement** - 新的文档,将替换旧的文档。 +- **options** - 可选参数对象,如 `upsert` 等。 + +【示例】replaceOne 示例 + +```json +db.collection.replaceOne( + { name: "Tom" }, // 过滤条件 + { name: "Tom", age: 18 } // 新文档 +); +``` + +#### updateOne + +`updateOne` 方法用于**更新单条文档**。 + +`updateOne` 方法语法如下: + +``` +db.collection.updateOne(filter, update, options) +``` + +- **filter** - 用于查找文档的查询条件。 +- **update** - 指定更新操作的文档或更新操作符。 +- **options** - 可选参数对象,如 `upsert`、`arrayFilters` 等。 + +【示例】updateOne 示例 + +```json +db.collection.updateOne( + { name: "Tom" }, // 过滤条件 + { $set: { age: 19 } }, // 更新操作 + { upsert: false } // 可选参数 +); +``` + +#### updateMany + +`updateMany` 方法用于**批量更新文档**。 + +`updateMany` 方法语法如下: + +``` +db.collection.updateMany(filter, update, options) +``` + +- **filter** - 用于查找文档的查询条件。 +- **update** - 指定更新操作的文档或更新操作符。 +- **options** - 可选参数对象,如 `upsert`、`arrayFilters` 等。 + +【示例】updateMany 示例 + +```json +db.collection.updateMany( + { age: { $lt: 30 } }, // 过滤条件 + { $set: { status: "active" } }, // 更新操作 + { upsert: false } // 可选参数 +); +``` + +#### 更新运算符 + +**"$set" 用来设置一个字段的值**。如果这个字段不存在,则创建该字段。 + +```json +// 使用 "$set" 来修改值 +> db.users.updateOne({"name" : "joe"},{"$set" : {"favorite book" : "Green Eggs and Ham"}}) + +// 使用 "$set" 来修改键的类型 +// 将 "favorite book" 键的值更改为一个数组 +> db.users.updateOne({"name" : "joe"},{"$set" : {"favorite book" : "Green Eggs and Ham"}}) + +// 如果用户发现自己其实不爱读书,则可以用 "$unset" 将这个键完全删除 +> db.users.updateOne({"name" : "joe"},{"$unset" : {"favorite book" : 1}}) +``` + +**"$inc" 运算符可以用来修改已存在的键值或者在该键不存在时创建它**。对于更新分析数据、因果关系、投票或者其他有数值变化的地方,使用这个会非常方便。 + +```json +> db.games.updateOne({"game":"pinball","user":"joe"},{"$inc":{"score":50}}) +``` + +**如果数组已存在,"$push" 就会将元素添加到数组末尾;如果数组不存在,则会创建一个新的数组**。 + +```json +> db.blog.posts.updateOne({"title":"A blog post"},{"$push":{"comments":{"name":"joe","email":"joe@example.com","content":"nice post."}}}) +``` + +**如果将数组视为队列或者栈,那么可以使用 "$pop"从任意一端删除元素**。`{"$pop":{"key":1}}` 会从数组末尾删除一个元素,`{"$pop":{"key":-1}}` 则会从头部删除它。 + +**"$pull" 用于删除与给定条件匹配的数组元素**。 + +```json +> db.lists.updateOne({}, {"$pull" : {"todo" : "laundry"}}) +``` + +#### upsert + +upsert 是一种特殊类型的更新。如果找不到与筛选条件相匹配的文档,则会以这个条件和更新文档为基础来创建一个新文档;如果找到了匹配的文档,则进行正常的更新。 + +```json +> db.users.updateOne({"rep" : 25}, {"$inc" : {"rep" : 3}}, {"upsert" : true}) +``` + +## 第 4 章 查询 + +### find 简介 + +**MongoDB 中使用 find 方法来进行查询**。查询就是返回集合中文档的一个子集,子集的范围从 0 个文档到整个集合。要返回哪些文档由 find 的第一个参数决定,该参数是一个用于指定查询条件的文档。 + +find 的语法格式如下: + +```json +db.collection.find(query, projection) +``` + +- **query** - 用于查找文档的查询条件。默认为 `{}`,即匹配所有文档。 +- **projection**(可选) - 指定返回结果中包含或排除的字段。 + +【示例】查找示例 + +```json +// 查找所有文档 +> db.collection.find() + +// 按条件查找文档 +> db.collection.find({ age: { $gt: 25 } }) + +// 按条件查找文档,并只返回指定字段 +> db.collection.find( + { age: { $gt: 25 } }, + { name: 1, age: 1, _id: 0 } +) + +// 以格式化的方式来显示所有文档 +> db.collection.find().pretty() +``` + +### 查询条件 + +#### 比较操作符 + +| 操作符 | 描述 | 示例 | +| :----- | :--------------- | :-------------------------------- | +| `$eq` | 等于 | `{ age: { $eq: 25 } }` | +| `$ne` | 不等于 | `{ age: { $ne: 25 } }` | +| `$gt` | 大于 | `{ age: { $gt: 25 } }` | +| `$gte` | 大于等于 | `{ age: { $gte: 25 } }` | +| `$lt` | 小于 | `{ age: { $lt: 25 } }` | +| `$lte` | 小于等于 | `{ age: { $lte: 25 } }` | +| `$in` | 在指定的数组中 | `{ age: { $in: [25, 30, 35] } }` | +| `$nin` | 不在指定的数组中 | `{ age: { $nin: [25, 30, 35] } }` | + +【示例】查找年龄大于 25 且城市为 "New York" 的文档: + +```json +db.collection.find({ age: { $gt: 25 }, city: "New York" }); +``` + +#### 逻辑操作符 + +| 操作符 | 描述 | 示例 | +| :----- | :--------------------- | :--------------------------------------------------------- | +| `$and` | 逻辑与,符合所有条件 | `{ $and: [ { age: { $gt: 25 } }, { city: "New York" } ] }` | +| `$or` | 逻辑或,符合任意条件 | `{ $or: [ { age: { $lt: 25 } }, { city: "New York" } ] }` | +| `$not` | 取反,不符合条件 | `{ age: { $not: { $gt: 25 } } }` | +| `$nor` | 逻辑与非,均不符合条件 | `{ $nor: [ { age: { $gt: 25 } }, { city: "New York" } ] }` | + +【示例】查找年龄大于 25 或城市为 "New York" 的文档: + +```json +db.collection.find({ $or: [ { age: { $gt: 25 } }, { city: "New York" } ] }); +``` + +#### 元素操作符 + +| 操作符 | 描述 | 示例 | +| :-------- | :--------------- | :--------------------------- | +| `$exists` | 字段是否存在 | `{ age: { $exists: true } }` | +| `$type` | 字段的 BSON 类型 | `{ age: { $type: "int" } }` | + +【示例】查找包含 age 字段的文档: + +```json +db.myCollection.find({ age: { $exists: true } }); +``` + +#### 数组操作符 + +| 操作符 | 描述 | 示例 | +| :----------- | :------------------------- | :------------------------------------------------------------- | +| `$all` | 数组包含所有指定的元素 | `{ tags: { $all: ["red", "blue"] } }` | +| `$elemMatch` | 数组中的元素匹配指定条件 | `{ results: { $elemMatch: { score: { $gt: 80, $lt: 85 } } } }` | +| `$size` | 数组的长度等于指定值 | `{ tags: { $size: 3 } }` | +| `slice` | 返回一个数组键中元素的子集 | | + +查询数组元素的方式与查询标量值相同。 + +```json +// 插入数组列表 +db.food.insertOne({"fruit" : ["apple", "banana", "peach"]} +// 查找数组中的 banana 元素 +db.food.find({"fruit" : "banana"}) +``` + +如果需要通过多个元素来匹配数组,那么可以使用 "$all"。 + +```json +db.food.insertOne({"_id" : 1, "fruit" : ["apple", "banana", "peach"]}) +db.food.insertOne({"_id" : 2, "fruit" : ["apple", "kumquat", "orange"]}) +db.food.insertOne({"_id" : 3, "fruit" : ["cherry", "banana", "apple"]}) + +// 查询同时包含元素 "apple" 和 "banana" 的文档 +db.food.find({fruit : {$all : ["apple", "banana"]}}) +``` + +查询特定长度的数组 + +```json +db.food.find({"fruit" : {"$size" : 3}}) +``` + +#### 其他操作符 + +还有一些其他操作符如下: + +| 操作符 | 描述 | 示例 | +| :------- | :--------------------------------- | :--------------------------------- | +| `$regex` | 匹配正则表达式 | `{ name: { $regex: /^A/ } }` | +| `$text` | 进行文本搜索 | `{ $text: { $search: "coffee" } }` | +| `$where` | 使用 JavaScript 表达式进行条件过滤 | `{ $where: "this.age > 25" }` | + +查找名字以 "A" 开头的文档: + +```json +db.myCollection.find({ name: { $regex: /^A/ } }) +``` + +### 特定类型查询 + +#### null + +null 会匹配值为 null 的文档以及缺少这个键的所有文档 + +```json +> db.c.find({"z" : null}) +``` + +如果仅想匹配键值为 null 的文档,则需要检查该键的值是否为 null,并且通过 "$exists" 条件确认该键已存在。 + +```json +db.c.find({"z" : {"$eq" : null, "$exists" : true}}) +``` + +#### 正则表达式 + +$regex" 可以在查询中为字符串的模式匹配提供正则表达式功能。正则表达式对于灵活的字符串匹配非常有用。 + +```json +> db.users.find( {"name" : {"$regex" : /joe/i } }) +``` + +MongoDB 会使用 Perl 兼容的正则表达式(PCRE)库来对正则表达式进行匹配。任何 PCRE 支持的正则表达式语法都能被 MongoDB 接受。在查询中使用正则表达式之前,最好先在 JavaScript shell 中检查一下语法,这样可以确保匹配与预想的一致。 + +#### 查询内嵌文档 + +假设文档形式如下: + +```json +{ + "name": { + "first": "Joe", + "last": "Schmoe" + }, + "age": 45 +} +``` + +查询 first 属性为 Joe,last 属性为 Schmoe 的文档: + +``` +db.people.find({"name.first" : "Joe", "name.last" : "Schmoe"}) +``` + +#### `$where` 查询 + +`$where` 允许你在查询中执行任意的 JavaScript 代码。这样就能在查询中做大部分事情了。除非绝对必要,否则不应该使用 "$where" 查询:它们比常规查询慢得多。 + +```json +db.foo.find({"$where" : function () { + for (var current in this) { + for (var other in this) { + if (current != other && this[current] == this[other]) { + return true; + } + } + } + return false; +}}); +``` + +#### 游标 + +数据库会使用游标返回 find 的执行结果。 + +```json +var cursor = db.people.find(); +cursor.forEach(function(x) { + print(x.name); +}); +``` + +`limit()` 方法用于限制查询结果返回的文档数量。 + +```json +db.collection.find().limit(10); +``` + +`skip()` 方法用于跳过指定数量的文档,从而实现分页或分批查询。 + +```json +// 跳过前 10 个文档,返回接下来的 10 个文档 +db.collection.find().skip(10).limit(10); +``` + +`sort()` 方法可以通过参数指定排序的字段。 + +```json +// 先按 age 字段升序排序,再按 createdAt 字段降序排序 +db.myCollection.find().sort({ age: 1, createdAt: -1 }); +``` + +## 参考资料 + +- [《MongoDB 权威指南》](https://book.douban.com/subject/35688800/) \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/MongoDB\346\235\203\345\250\201\346\214\207\345\215\227\347\254\224\350\256\260\344\272\214.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/MongoDB\346\235\203\345\250\201\346\214\207\345\215\227\347\254\224\350\256\260\344\272\214.md" new file mode 100644 index 0000000000..43ce0d880c --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/MongoDB\346\235\203\345\250\201\346\214\207\345\215\227\347\254\224\350\256\260\344\272\214.md" @@ -0,0 +1,179 @@ +--- +title: 《MongoDB 权威指南》笔记二 +date: 2024-09-29 07:45:34 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 文档数据库 + - MongoDB +permalink: /pages/8bcf7f2b/ +--- + +# 《MongoDB 权威指南》笔记二 + +## 第 5 章 索引 + +索引是用于提升查询效率的一种存储结构。 + +### 索引简介 + +在 MongoDB 中,不使用索引的查询称为**集合扫描**,这意味要扫描所有数据。 + +创建索引 + +```json +db.users.createIndex({"username" : 1}) +``` + +创建复合索引 + +如果查询中有多个排序方向或者查询条件中有多个键,那么复合索引会非常有用。 + +```json +db.users.createIndex({"age" : 1, "username" : 1}) +``` + +### explain + +### 何时不使用索引 + +### 索引类型 + +### 索引管理 + +## 第 6 章 特殊的索引和集合类型 + +### 地理空间索引 + +### 全文搜索索引 + +### 固定集合 + +### TTL 索引 + +### 使用 GridFS 存储文件 + +## 第 7 章 聚合框架 + +### 管道、阶段和可调参数 + +### 阶段入门:常见操作 + +### 表达式 + +### $project + +### $unwind + +### 数组表达式 + +### 累加器 + +### 分组简介 + +### 将聚合管道结果写入集合中 + +## 第 8 章 事务 + +## 第 9 章 应用程序设计 + +## 第 10 章 创建副本集 + +### 如何设计副本集 + +在 MongoDB 中,副本集``是一组服务器,其中一个是用于处理写操作的主节点(primary),还有多个用于保存主节点的数据副本的从节点(secondary)。如果主节点崩溃了,则从节点会从其中选取出一个新的主节点。 + +副本集中的停止运行或不可用的节点数只要不到一半,副本集整体都是可以对外提供服务的。——这是为什么呢? + +MongoDB 的故障转移能力基于 Raft 共识协议实现。该协议的核心在于,当主节点出现故障时,集群中半数以上的节点所认可的节点才能当选为新的主节点。假设一个集群中有 5 个节点,其中 3 个节点不可用,2 个节点可以正常工作。这 2 个节点不能达到副本集大多数的要求(至少需要 3 个节点),因此它们无法选举出一个主节点。如果这 2 个节点中有一个是主节点,那么当它注意到无法得到大多数节点支持时,就会从主节点上退位。几秒后,副本集中会包含 2 个从节点和 3 个无法访问的节点。 + +为什么剩下的 2 个节点不能选举出主节点?问题在于:其他 3 个节点实际上可能没有崩溃,而是因为网络故障而不可达。在这种情况下,左侧的 3 个节点将选举出一个主节点,因为它们可以达到副本集节点中的大多数(5 个节点中的 3 个)。在网络分区的情况下,我们不希望两边的网络各自选举出一个主节点,因为那样的话副本集就拥有 2 个主节点了。如果 2 个主节点都可以写入数据,那么整个副本集的数据就会发生混乱,这就是所谓的“脑裂”。 + +有两种推荐的集群模式: + +- 将“大多数”成员放在一个数据中心。如果有一个主数据中心,而且你希望副本集的主节点总是位于主数据中心,那么这是一个比较好的配置。只要主数据中心正常运转,就会有一个主节点。不过,如果主数据中心不可用了,那么备份数据中心的成员将无法选举出主节点。 +- 在两个数据中心各自放置数量相等的成员,在第三个地方放置一个用于打破僵局的副本集成员。如果两个数据中心“同等”重要,那么这种配置会比较好,因为任意一个数据中心的服务器都可以找到另一台服务器以达到“大多数”。不过,这样就需要将服务器分散到 3 个地方。 + +### 如何进行选举 + +当一个从节点无法与主节点连通时,它就会联系并请求其他的副本集成员将自己选举为主节点。其他成员会做几项健全性检查:它们能否连接到一个主节点,而这个主节点是发起选举的节点无法连接到的?这个发起选举的从节点是否有最新数据?有没有其他更高优先级的成员可以被选举为主节点? + +MongoDB 在其 3.2 版本中引入了 Raft 共识协议。 + +副本集成员相互间每隔两秒发送一次心跳(heartbeat,也就是 ping)。如果某个成员在 10秒内没有反馈心跳,则其他成员会将该不良成员标记为无法访问。选举算法将尽“最大努力”尝试让具有最高优先权的从节点发起选举。成员优先权会影响选举的时机和结果。优先级高的从节点要比优先级低的从节点更快地发起选举,而且也更有可能成为主节点。然而,低优先级的从节点也可能短暂地被选为主节点,即使还存在一个可用的高优先级从节点。副本集成员会继续发起选举直到可用的最高优先级成员被选为主节点。 + +就所有能连接到的成员,被选为主节点的成员必须拥有最新的复制数据。严格地说,所有的操作都必须比任何一个成员的操作都要高,因此所有的操作都必须比任何一个成员的操作都要晚。 + +## 第 11 章 副本集的组成 + +### 同步 + +复制是指在多台服务器上保持相同的数据副本。MongoDB 实现此功能的方式是保存操作日志(oplog),其中包含了主节点执行的每一次写操作。oplog 是存在于主节点 local 数据库中的一个固定集合。从节点通过查询此集合以获取需要复制的操作。 + +每个从节点都维护着自己的 oplog,用来记录它从主节点复制的每个操作。 + +如果一个从节点由于某种原因而停止运行,那么当它重新启动后,就会从 oplog 中的最后一个操作开始同步。由于这些操作是先应用到数据上然后再写入 oplog,因此从节点可能会重复已经应用到其数据上的操作。MongoDB 在设计时就考虑到了这种情况:将 oplog 中的同一个操作执行多次与只执行一次效果是一样的。oplog 中的每个操作都是幂等的。也就是说,无论对目标数据集应用一次还是多次,oplog 操作都会产生相同的结果。 + +### 心跳 + +每个成员需要知道其他成员的状态:谁是主节点?谁可以作为同步源?谁停止运行了?为了维护副本集的最新视图,所有成员每隔两秒会向副本集的其他成员发送一个心跳请求。 + +心跳请求用于检查每个成员的状态。 + +心跳的一个最重要的功能是让主节点知道自己是否满足副本集“大多数”的条件。如果主节点不再得到“大多数”节点的支持,它就会降级,成为一个从节点。 + +### 选举 + +当一个成员无法访问到主节点时,便会申请选举。申请选举的成员会向其所能访问到的所有成员发出通知。如果这个成员不适合作为主节点,那么其他成员会知道原因:可能这个成员的数据落后于副本集,或者已经有一个主节点在申请选举,而那个失败的成员无法访问到此节点。在这些情况下,其他成员将投票反对该成员的申请。 + +如果申请选举的成员从副本集中获得了大多数选票,该成员将转换到 PRIMARY 状态。 + +### 回滚 + +如果主节点执行一个写操作之后停止了运行,而从节点还没来得及复制此操作,那么新选举出的主节点可能会丢失这个写操作。 + +## 第 12 章 从应用程序连接副本集 + +## 第 13 章 管理 + +## 第 14 章 分片简介 + +MongoDB 支持自动分片。 + +MongoDB 通过 mongos 进程来进行分片的路由寻址。mongos 维护着一个“目录”,指明了哪个分片包含哪些数据。mongos 知道哪些数据在哪个分片上,可以将请求转发到适当的分片。如果有对请求的响应,mongos 会收集它们,并在必要时进行合并,然后再发送回应用程序。 + +在对集合进行分片时,需要选择一个片键(shard key)。片键是 MongoDB 用来拆分数据的一个或几个字段。 + +包含片键并可以发送到单个分片或分片子集的查询称为定向查询(targeted query)。必须发送到所有分片的查询称为分散–收集查询(scatter-gather query),也称为广播查询:mongos会将查询分散到所有分片,然后再从各个分片收集结果。 + +## 第 15 章 配置分片 + +### 均衡器 + +均衡器负责数据的迁移。它会定期检查分片之间是否存在不均衡(一个分片的块数量达到阈值),如果存在,就会对块进行迁移。 + +MongoDB 3.4 以后,均衡器位于配置服务器副本集的主节点成员上;MongoDB 3.4 及以前,每个 mongos 会偶尔扮演均衡器角色。 + +## 第 16 章 选择片键 + +## 第 17 章 分片管理 + +## 第 18 章 了解应用程序的动态 + +## 第 19 章 MongoDB 安全介绍 + +## 第 20 章 持久性 + +## 第 21 章 在生产环境中设置 MongoDB + +## 第 22 章 监控 MongoDB + +## 第 23 章 备份 + +## 第 24 章 部署 MongoDB + +## 参考资料 + +- [《MongoDB 权威指南》](https://book.douban.com/subject/35688800/) diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/SQL\345\277\205\347\237\245\345\277\205\344\274\232\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/SQL\345\277\205\347\237\245\345\277\205\344\274\232\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..03012624cd --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/SQL\345\277\205\347\237\245\345\277\205\344\274\232\347\254\224\350\256\260.md" @@ -0,0 +1,1284 @@ +--- +title: 《SQL 必知必会》笔记 +date: 2024-09-29 07:45:34 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 关系型数据库 +permalink: /pages/abe5a720/ +--- + +# 《SQL 必知必会》笔记 + +## 第 1 课 了解 SQL + +### 数据库基础 + +- 数据库(database) - 保存有组织的数据的容器(通常是一个文件或一组文件)。 +- 表(table) - 某种特定类型数据的结构化清单。 +- 模式 - 关于数据库和表的布局及特性的信息。 +- 列(column) - 表中的一个字段。所有表都是由一个或多个列组成的。 +- 数据类型 - 所允许的数据的类型。每个表列都有相应的数据类型,它限制(或允许)该列中存储的数据。 +- 行(row) - 表中的一个记录。 +- 主键(primary key) - 一列(或一组列),其值能够唯一标识表中每一行。表中的任何列都可以作为主键,只要它满足以下条件: + - 任意两行都不具有相同的主键值; + - 每一行都必须具有一个主键值(主键列不允许 NULL 值); + - 主键列中的值不允许修改或更新; + - 主键值不能重用(如果某行从表中删除,它的主键不能赋给以后的新行)。 + +### 什么是 SQL + +SQL 是 Structured Query Language(结构化查询语言)的缩写。SQL 是一种专门用来与数据库沟通的语言。 + +## 第 2 课 检索数据 + +作为 SQL 组成部分的保留字。关键字不能用作表或列的名字。 + +检索单列 + +```sql +SELECT prod_name +FROM Products; +``` + +检索多列 + +```sql +SELECT prod_id, prod_name, prod_price +FROM Products; +``` + +检索所有列 + +```sql +SELECT * +FROM Products; +``` + +检索去重 + +```sql +SELECT DISTINCT vend_id +FROM Products; +``` + +限制数量 + +检索 TOP5 数据: + +```sql +-- SQL Server 和 Access +SELECT TOP 5 prod_name +FROM Products; + +-- DB2 +SELECT prod_name +FROM Products +FETCH FIRST 5 ROWS ONLY; + +-- Oracle +SELECT prod_name +FROM Products +WHERE ROWNUM <=5; + +-- MySQL、MariaDB、PostgreSQL 或者 SQLite +SELECT prod_name +FROM Products +LIMIT 5; +-- 检索从第 5 行起的 5 行数据 +SELECT prod_name +FROM Products +LIMIT 5 OFFSET 5; +-- MySQL 和 MariaDB 中,上面的示例可以简化如下 +SELECT prod_name +FROM Products +LIMIT 5, 5; +``` + +使用注释 + +```sql +SELECT prod_name -- 这是一条注释 +FROM Products; + +# 这是一条注释 +SELECT prod_name +FROM Products; + +/* SELECT prod_name, vend_id +FROM Products; */ +SELECT prod_name +FROM Products; +``` + +## 第 3 课 排序检索数据 + +SQL 语句由子句构成,有些子句是必需的,有些则是可选的。一个子句通常由一个关键字加上所提供的数据组成。例如,SELECT 语句中的 FROM 子句。 + +ORDER BY 子句取一个或多个列的名字,据此对输出进行排序。ORDER BY 支持两种排序方式:ASC(升序) 和 DESC(降序)。 + +按单列排序: + +```sql +SELECT prod_name +FROM Products +ORDER BY prod_name; +``` + +按多列排序: + +```sql +SELECT prod_id, prod_price, prod_name +FROM Products +ORDER BY prod_price DESC, prod_name; +``` + +按列位置排序(不推荐): + +```sql +SELECT prod_id, prod_price, prod_name +FROM Products +ORDER BY 2, 3; +``` + +指定排序方向 + +```sql +SELECT prod_id, prod_price, prod_name +FROM Products +ORDER BY prod_price DESC; +``` + +## 第 4 课 过滤数据 + +只检索所需数据需要指定搜索条件(search criteria),搜索条件也称为过滤条件(filter condition)。 + +在 SELECT 语句中,数据根据 WHERE 子句中指定的搜索条件进行过滤。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price = 3.49; +``` + +检索所有价格小于 10 美元的产品。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price < 10; +``` + +检索所有不是供应商 DLL01 制造的产品 + +```sql +-- 下面两条查询语句作用相同 + +SELECT vend_id, prod_name +FROM Products +WHERE vend_id <> 'DLL01'; + +SELECT vend_id, prod_name +FROM Products +WHERE vend_id != 'DLL01'; +``` + +检索价格在 5 美元和 10 美元之间的所有产品 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE prod_price BETWEEN 5 AND 10; +``` + +检索所有没有邮件地址的顾客 + +```sql +SELECT cust_name +FROM CUSTOMERS +WHERE cust_email IS NULL; +``` + +## 第 5 课 高级数据过滤 + +- **AND** - AND 用来表示检索满足所有给定条件的行。 +- **OR** - OR 用来表示检索匹配任一给定条件的行。 + +### 组合 WHERE 子句 + +检索由供应商 DLL01 制造且价格小于等于 4 美元的所有产品的名称和价格 + +```sql +SELECT prod_id, prod_price, prod_name +FROM Products +WHERE vend_id = 'DLL01' AND prod_price <= 4; +``` + +检索由供应商 DLL01 或供应商 BRS01 制造的所有产品的名称和价格 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE vend_id = 'DLL01' OR vend_id = 'BRS01'; +``` + +WHERE 子句可以包含任意数目的 AND 和 OR 操作符。允许两者结合以进行复杂、高级的过滤。 + +SQL 在处理 OR 操作符前,优先处理 AND 操作符。 + +下面的示例中,SQL 会理解为由供应商 BRS01 制造的价格为 10 美元以上的所有产品,以及由供应商 DLL01 制造的所有产品,而不管其价格如何。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE vend_id = 'DLL01' OR vend_id = 'BRS01' +AND prod_price >= 10; +``` + +任何时候使用具有 AND 和 OR 操作符的 WHERE 子句,都应该使用圆括号明确地分组操作符。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE (vend_id = 'DLL01' OR vend_id = 'BRS01') +AND prod_price >= 10; +``` + +### IN 操作符 + +IN 操作符用来指定条件范围,范围中的每个条件都可以进行匹配。IN 取一组由逗号分隔、括在圆括号中的合法值。 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE vend_id IN ( 'DLL01', 'BRS01' ) +ORDER BY prod_name; +``` + +和下面的示例作用相同 + +```sql +SELECT prod_name, prod_price +FROM Products +WHERE vend_id = 'DLL01' OR vend_id = 'BRS01' +ORDER BY prod_name; +``` + +为什么要使用 IN 操作符?其优点如下。 + +- 在有很多合法选项时,IN 操作符的语法更清楚,更直观。 +- 在与其他 AND 和 OR 操作符组合使用 IN 时,求值顺序更容易管理。 +- IN 操作符一般比一组 OR 操作符执行得更快。 +- IN 的最大优点是可以包含其他 SELECT 语句,能够更动态地建立 HERE 子句。 + +### NOT 操作符 + +NOT 用来否定其后条件的关键字。 + +检索除 DLL01 之外的所有供应商制造的产品 + +```sql +SELECT prod_name +FROM Products +WHERE NOT vend_id = 'DLL01' +ORDER BY prod_name; +``` + +和下面的示例作用相同 + +```sql +SELECT prod_name +FROM Products +WHERE vend_id <> 'DLL01' +ORDER BY prod_name; +``` + +## 第 6 课 用通配符进行过滤 + +通配符(wildcard)用来匹配值的一部分的特殊字符。 + +搜索模式(search pattern)由字面值、通配符或两者组合构成的搜索条件。 + +在搜索子句中使用通配符,必须使用 LIKE 操作符。LIKE 指示 DBMS,后跟的搜索模式利用通配符匹配而不是简单的相等匹配进行比较。 + +### 百分号(%)通配符 + +%表示任何字符出现任意次数。 + +检索所有产品名以 Fish 开头的产品 + +```sql +SELECT prod_id, prod_name +FROM Products +WHERE prod_name LIKE 'Fish%'; +``` + +匹配任何位置上包含文本 bean bag 的值, +不论它之前或之后出现什么字符。 + +检索产品名中包含 bean bag 的产品 + +```sql +SELECT prod_id, prod_name +FROM Products +WHERE prod_name LIKE '%bean bag%'; +``` + +检索产品名中以 F 开头,y 结尾的产品 + +```sql +SELECT prod_name +FROM Products +WHERE prod_name LIKE 'F%y'; +``` + +### 下划线(\_)通配符 + +下划线(\_)的用途与%一样,但它只匹配单个字符。 + +```sql +SELECT prod_id, prod_name +FROM Products +WHERE prod_name LIKE '__ inch teddy bear'; +``` + +### 方括号([ ])通配符 + +方括号([])通配符用来指定一个字符集,它必须匹配指定位置(通配符的位置)的一个字符。 + +> 说明:并不是所有 DBMS 都支持用来创建集合的 []。只有微软的 Access 和 SQL Server 支持集合。 + +找出所有名字以 J 或 M 开头的联系人: + +```sql +SELECT cust_contact +FROM Customers +WHERE cust_contact LIKE '[JM]%' +ORDER BY cust_contact; +``` + +## 第 7 课 创建计算字段 + +### 拼接字段 + +拼接字符串值: + +```sql +-- Access 和 SQL Server +SELECT vend_name + ' (' + vend_country + ')' +FROM Vendors +ORDER BY vend_name; + +-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base +SELECT vend_name || ' (' || vend_country || ')' +FROM Vendors +ORDER BY vend_name; + +-- MySQL 或 MariaDB +SELECT Concat(vend_name, ' (', vend_country, ')') +FROM Vendors +ORDER BY vend_name; +``` + +去除字符串中的空格 + +```sql +-- Access 和 SQL Server +SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')' +FROM Vendors +ORDER BY vend_name; + +-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base +SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')' +FROM Vendors +ORDER BY vend_name; +``` + +### 别名 + +使用别名 + +```sql +-- Access 和 SQL Server +SELECT RTRIM(vend_name) + ' (' + RTRIM(vend_country) + ')' +AS vend_title +FROM Vendors +ORDER BY vend_name; + +-- DB2、Oracle、PostgreSQL、SQLite 和 Open Office Base +SELECT RTRIM(vend_name) || ' (' || RTRIM(vend_country) || ')' +AS vend_title +FROM Vendors +ORDER BY vend_name; + +-- MySQL 和 MariaDB +SELECT Concat(vend_name, ' (', vend_country, ')') +AS vend_title +FROM Vendors +ORDER BY vend_name; +``` + +### 执行算术计算 + +汇总物品的价格(单价乘以订购数量): + +```sql +SELECT prod_id, +quantity, +item_price, +quantity*item_price AS expanded_price +FROM OrderItems +WHERE order_num = 20008; +``` + +## 第 8 课 使用函数处理数据 + +大多数 SQL 实现支持以下类型的函数: + +- 算术函数 +- 文本处理函数 +- 时间处理函数 +- 聚合函数 +- 返回 DBMS 正使用的特殊信息(如返回用户登录信息)的系统函数 + +### 文本处理函数 + +| 函数 | 说明 | +| ---------------------------------------- | ----------------------- | +| LEFT()(或使用子字符串函数) | 返回字符串左边的字符 | +| LENGTH()(也使用 DATALENGTH() 或 LEN()) | 返回字符串的长度 | +| LOWER()(Access 使用 LCASE()) | 将字符串转换为小写 | +| LTRIM() | 去掉字符串左边的空格 | +| RIGHT()(或使用子字符串函数) | 返回字符串右边的字符 | +| RTRIM() | 去掉字符串右边的空格 | +| SOUNDEX() | 返回字符串的 SOUNDEX 值 | +| UPPER()(Access 使用 UCASE()) | 将字符串转换为大写 | + +UPPER() 将文本转换为大写 + +```sql +SELECT vend_name, UPPER(vend_name) AS vend_name_upcase +FROM Vendors +ORDER BY vend_name; +``` + +### 日期和时间处理函数 + +```sql +-- SQL Server +SELECT order_num +FROM Orders +WHERE DATEPART(yy, order_date) = 2012; + +-- Access +SELECT order_num +FROM Orders +WHERE DATEPART('yyyy', order_date) = 2012; + +-- PostgreSQL +SELECT order_num +FROM Orders +WHERE DATE_PART('year', order_date) = 2012; + +-- Oracle +SELECT order_num +FROM Orders +WHERE to_number(to_char(order_date, 'YYYY')) = 2012; + +-- MySQL 和 MariaDB +SELECT order_num +FROM Orders +WHERE YEAR(order_date) = 2012; +``` + +### 数值处理函数 + +| 函数 | 说明 | +| ------ | ------------------ | +| ABS() | 返回一个数的绝对值 | +| COS() | 返回一个角度的余弦 | +| EXP() | 返回一个数的指数值 | +| PI() | 返回圆周率 | +| SIN() | 返回一个角度的正弦 | +| SQRT() | 返回一个数的平方根 | +| TAN() | 返回一个角度的正切 | + +## 第 9 课 汇总数据 + +聚集函数(aggregate function)对某些行运行的函数,计算并返回一个值。 + +| 函数 | 说明 | +| ------- | ---------------- | +| AVG() | 返回某列的平均值 | +| COUNT() | 返回某列的行数 | +| MAX() | 返回某列的最大值 | +| MIN() | 返回某列的最小值 | +| SUM() | 返回某列值之和 | + +AVG() 通过对表中行数计数并计算其列值之和,求得该列的平均值。 + +使用 AVG() 返回 Products 表中所有产品的平均价格: + +```sql +SELECT AVG(prod_price) AS avg_price +FROM Products; +``` + +COUNT() 函数进行计数。可利用 COUNT() 确定表中行的数目或符合特定条件的行的数目。 + +返回 Customers 表中顾客的总数: + +```sql +SELECT COUNT(*) AS num_cust +FROM Customers; +``` + +只对具有电子邮件地址的客户计数: + +```sql +SELECT COUNT(cust_email) AS num_cust +FROM Customers; +``` + +MAX() 返回指定列中的最大值。 + +返回 Products 表中最贵物品的价格: + +```sql +SELECT MAX(prod_price) AS max_price +FROM Products; +``` + +MIN() 返回指定列的最小值。 + +返回 Products 表中最便宜物品的价格 + +```sql +SELECT MIN(prod_price) AS min_price +FROM Products; +``` + +SUM() 用来返回指定列值的和(总计)。 + +返回订单中所有物品数量之和 + +```sql +SELECT SUM(quantity) AS items_ordered +FROM OrderItems +WHERE order_num = 20005; +``` + +### 组合聚集函数 + +```sql +SELECT COUNT(*) AS num_items, +MIN(prod_price) AS price_min, +MAX(prod_price) AS price_max, +AVG(prod_price) AS price_avg +FROM Products; +``` + +## 第 10 课 分组数据 + +分组是使用 SELECT 语句的 GROUP BY 子句建立的。 + +```sql +SELECT vend_id, COUNT(*) AS num_prods +FROM Products +GROUP BY vend_id; +``` + +GROUP BY 要点: + +- GROUP BY 子句可以包含任意数目的列,因而可以对分组进行嵌套,更细致地进行数据分组。 +- 如果在 GROUP BY 子句中嵌套了分组,数据将在最后指定的分组上进行汇总。换句话说,在建立分组时,指定的所有列都一起计算(所以不能从个别的列取回数据)。 +- GROUP BY 子句中列出的每一列都必须是检索列或有效的表达式(但不能是聚集函数)。如果在 SELECT 中使用表达式,则必须在 GROUP BY 子句中指定相同的表达式。不能使用别名。 +- 大多数 SQL 实现不允许 GROUP BY 列带有长度可变的数据类型(如文本或备注型字段)。 +- 除聚集计算语句外,SELECT 语句中的每一列都必须在 GROUP BY 子句中给出。 +- 如果分组列中包含具有 NULL 值的行,则 NULL 将作为一个分组返回。如果列中有多行 NULL 值,它们将分为一组。 +- GROUP BY 子句必须出现在 WHERE 子句之后,ORDER BY 子句之前。 + +HAVING 要点: + +HAVING 非常类似于 WHERE。唯一的差别是,WHERE 过滤行,而 HAVING 过滤分组。 + +过滤两个以上订单的分组 + +```sql +SELECT cust_id, COUNT(*) AS orders +FROM Orders +GROUP BY cust_id +HAVING COUNT(*) >= 2; +``` + +列出具有两个以上产品且其价格大于等于 4 的供应商: + +```sql +SELECT vend_id, COUNT(*) AS num_prods +FROM Products +WHERE prod_price >= 4 +GROUP BY vend_id +HAVING COUNT(*) >= 2; +``` + +检索包含三个或更多物品的订单号和订购物品的数目: + +```sql +SELECT order_num, COUNT(*) AS items +FROM orderitems +GROUP BY order_num +HAVING COUNT(*) >= 3; +``` + +要按订购物品的数目排序输出,需要添加 ORDER BY 子句 + +```sql +SELECT order_num, COUNT(*) AS items +FROM orderitems +GROUP BY order_num +HAVING COUNT(*) >= 3 +ORDER BY items, order_num; +``` + +在 SELECT 语句中使用时必须遵循的次序: + +```sql +SELECT -> FROM -> WHERE -> GROUP BY -> HAVING -> ORDER BY +``` + +## 第 11 课 使用子查询 + +子查询(subquery),即嵌套在其他查询中的查询。 + +假如需要列出订购物品 RGAN01 的所有顾客,应该怎样检索?下面列出具体的步骤。 + +(1) 检索包含物品 RGAN01 的所有订单的编号。 + +```sql +SELECT order_num +FROM OrderItems +WHERE prod_id = 'RGAN01'; +``` + +输出 + +``` +order_num +----------- +20007 +20008 +``` + +(2) 检索具有前一步骤列出的订单编号的所有顾客的 ID。 + +```sql +SELECT cust_id +FROM Orders +WHERE order_num IN (20007,20008); +``` + +输出 + +``` +cust_id +---------- +1000000004 +1000000005 +``` + +(3) 检索前一步骤返回的所有顾客 ID 的顾客信息。 + +```sql +SELECT cust_name, cust_contact +FROM Customers +WHERE cust_id IN ('1000000004','1000000005'); +``` + +现在,结合这两个查询,把第一个查询(返回订单号的那一个)变为子查询。 + +```sql +SELECT cust_id +FROM orders +WHERE order_num IN (SELECT order_num + FROM orderitems + WHERE prod_id = 'RGAN01'); +``` + +再进一步结合第三个查询 + +```sql +SELECT cust_name, cust_contact +FROM customers +WHERE cust_id IN (SELECT cust_id + FROM orders + WHERE order_num IN (SELECT order_num + FROM orderitems + WHERE prod_id = 'RGAN01')); +``` + +## 第 12 课 联结表 + +笛卡尔积 - 由没有联结条件的表关系返回的结果为笛卡儿积。检索出的行的数目将是第一个表中的行数乘以第二个表中的行数。 + +内联结 + +```sql +SELECT vend_name, prod_name, prod_price +FROM vendors INNER JOIN products +ON vendors.vend_id = products.vend_id; +``` + +联结多个表 + +下面两个 SQL 等价: + +```sql +SELECT cust_name, cust_contact +FROM customers, orders, orderitems +WHERE customers.cust_id = orders.cust_id AND orderitems.order_num = orders.order_num AND prod_id = 'RGAN01'; + +SELECT cust_name, cust_contact +FROM customers +WHERE cust_id IN (SELECT cust_id + FROM orders + WHERE order_num IN (SELECT order_num + FROM orderitems + WHERE prod_id = 'RGAN01')); +``` + +## 第 13 课 创建高级联结 + +自联结 + +给与 Jim Jones 同一公司的所有顾客发送一封信件: + +```sql +-- 子查询方式 +SELECT cust_id, cust_name, cust_contact +FROM customers +WHERE cust_name = (SELECT cust_name + FROM customers + WHERE cust_contact = 'Jim Jones'); + +-- 自联结方式 +SELECT c1.cust_id, c1.cust_name, c1.cust_contact +FROM customers AS c1, customers AS c2 +WHERE c1.cust_name = c2.cust_name AND c2.cust_contact = 'Jim Jones'; +``` + +自然联结 + +```sql +SELECT c.*, o.order_num, o.order_date, oi.prod_id, oi.quantity, oi.item_price +FROM customers AS c, orders AS o, orderitems AS oi +WHERE c.cust_id = o.cust_id AND oi.order_num = o.order_num AND prod_id = 'RGAN01'; +``` + +左外联结 + +```sqL +SELECT customers.cust_id, orders.order_num +FROM customers + INNER JOIN orders +ON customers.cust_id = orders.cust_id; + +SELECT customers.cust_id, orders.order_num +FROM customers + LEFT OUTER JOIN orders +ON customers.cust_id = orders.cust_id; +``` + +右外联结 + +```sql +SELECT customers.cust_id, orders.order_num +FROM customers + RIGHT OUTER JOIN orders +ON orders.cust_id = customers.cust_id; +``` + +全外联结 + +```sql +SELECT customers.cust_id, orders.order_num +FROM orders + FULL OUTER JOIN customers +ON orders.cust_id = customers.cust_id; +``` + +> 注意:Access、MariaDB、MySQL、Open Office Base 和 SQLite 不支持 FULLOUTER JOIN 语法。 + +使用带聚集函数的联结 + +```sql +SELECT customers.cust_id, + COUNT(orders.order_num) AS num_ord +FROM customers + INNER JOIN orders +ON customers.cust_id = orders.cust_id +GROUP BY customers.cust_id; +``` + +## 第 14 课 组合查询 + +主要有两种情况需要使用组合查询: + +- 在一个查询中从不同的表返回结构数据; +- 对一个表执行多个查询,按一个查询返回数据。 + +把 Illinois、Indiana、Michigan 等州的缩写传递给 IN 子句,检索出这些州的所有行 + +```sql +SELECT cust_name, cust_contact, cust_email +FROM Customers +WHERE cust_state IN ('IL','IN','MI'); +``` + +找出所有 Fun4All + +```sql +SELECT cust_name, cust_contact, cust_email +FROM Customers +WHERE cust_name = 'Fun4All'; +``` + +组合这两条语句 + +```sql +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_state IN ('IL', 'IN', 'MI') +UNION +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_name = 'Fun4All'; +``` + +UNION 默认从查询结果集中自动去除了重复的行;如果想返回所有的匹配行,可使用 UNION ALL + +```sql +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_state IN ('IL', 'IN', 'MI') +UNION ALL +SELECT cust_name, cust_contact, cust_email +FROM customers +WHERE cust_name = 'Fun4All'; +``` + +## 第 15 课 插入数据 + +插入完整的行 + +```sql +-- 下面两条 SQL 等价 +INSERT INTO Customers +VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA', NULL, NULL); + +INSERT INTO Customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country, cust_contact, cust_email) +VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY','11111', 'USA', NULL, NULL); +``` + +插入行的一部分 + +```sql +INSERT INTO customers(cust_id, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country) +VALUES ('1000000006', 'Toy Land', '123 Any Street', 'New York', 'NY', '11111', 'USA'); +``` + +插入某些查询的结果 + +```sql +INSERT INTO Customers(cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country) +SELECT cust_id, cust_contact, cust_email, cust_name, cust_address, cust_city, cust_state, cust_zip, cust_country +FROM CustNew; +``` + +从一个表复制到另一个表 + +```sql +SELECT * +INTO CustCopy +FROM Customers; + +-- MariaDB、MySQL、Oracle、PostgreSQL 和 SQLite +CREATE TABLE CustCopy AS +SELECT * FROM Customers; +``` + +## 第 16 课 更新和删除数据 + +更新单列 + +更新客户 1000000005 的电子邮件地址 + +```sql +UPDATE Customers +SET cust_email = 'kim@thetoystore.com' +WHERE cust_id = '1000000005'; +``` + +更新多列 + +```sql +UPDATE customers +SET cust_contact = 'Sam Roberts', cust_email = 'sam@toyland.com' +WHERE cust_id = '1000000006'; +``` + +从表中删除特定的行 + +```sql +DELETE FROM Customers +WHERE cust_id = '1000000006'; +``` + +更新和删除的指导原则 + +- 除非确实打算更新和删除每一行,否则绝对不要使用不带 WHERE 子句的 UPDATE 或 DELETE 语句。 +- 保证每个表都有主键,尽可能像 WHERE 子句那样使用它(可以指定各主键、多个值或值的范围)。 +- 在 UPDATE 或 DELETE 语句使用 WHERE 子句前,应该先用 SELECT 进行测试,保证它过滤的是正确的记录,以防编写的 WHERE 子句不正确。 +- 使用强制实施引用完整性的数据库,这样 DBMS 将不允许删除其数据与其他表相关联的行。 +- 有的 DBMS 允许数据库管理员施加约束,防止执行不带 WHERE 子句的 UPDATE 或 DELETE 语句。如果所采用的 DBMS 支持这个特性,应该使用它。 + +## 第 17 课 创建和操纵表 + +创建表 + +利用 CREATE TABLE 创建表,必须给出下列信息: + +- 新表的名字,在关键字 CREATE TABLE 之后给出; +- 表列的名字和定义,用逗号分隔; +- 有的 DBMS 还要求指定表的位置。 + +```sql +CREATE TABLE products ( + prod_id CHAR(10) NOT NULL, + vend_id CHAR(10) NOT NULL, + prod_name CHAR(254) NOT NULL, + prod_price DECIMAL(8, 2) NOT NULL, + prod_desc VARCHAR(1000) NULL +); +``` + +### 更新表 + +添加列: + +```sql +ALTER TABLE Vendors +ADD vend_phone CHAR(20); +``` + +删除列: + +```sql +ALTER TABLE Vendors +DROP COLUMN vend_phone; +``` + +### 删除表 + +```sql +DROP TABLE CustCopy; +``` + +## 第 18 课 使用视图 + +视图是虚拟的表。与包含数据的表不一样,视图只包含使用时动态检索数据的查询。 + +视图的一些常见应用 + +重用 SQL 语句 + +- 简化复杂的 SQL 操作。在编写查询后,可以方便地重用它而不必知道其基本查询细节。 +- 使用表的一部分而不是整个表。 +- 保护数据。可以授予用户访问表的特定部分的权限,而不是整个表的访问权限。 +- 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。 + +创建视图 + +创建一个名为 ProductCustomers 的视图,它联结三个表,返回已订购了任意产品的所有顾客的列表。 + +```sql +CREATE VIEW ProductCustomers AS +SELECT cust_name, cust_contact, prod_id +FROM Customers, Orders, OrderItems +WHERE Customers.cust_id = Orders.cust_id +AND OrderItems.order_num = Orders.order_num; +``` + +检索订购了产品 RGAN01 的顾客 + +```sql +SELECT cust_name, cust_contact +FROM ProductCustomers +WHERE prod_id = 'RGAN01'; +``` + +## 第 19 课 使用存储过程 + +创建存储过程 + +对邮件发送清单中具有邮件地址的顾客进行计数 + +```sql +CREATE PROCEDURE MailingListCount ( + ListCount OUT INTEGER +) IS +v_rows INTEGER; + +BEGIN +SELECT COUNT(*) +INTO v_rows +FROM customers +WHERE NOT cust_email IS NULL; +ListCount := v_rows; +END; +``` + +## 第 20 课 管理事务处理 + +使用事务处理(transaction processing),通过确保成批的 SQL 操作要么完全执行,要么完全不执行,来维护数据库的完整性。 + +- 事务(transaction)指一组 SQL 语句; +- 回退(rollback)指撤销指定 SQL 语句的过程; +- 提交(commit)指将未存储的 SQL 语句结果写入数据库表; +- 保留点(savepoint)指事务处理中设置的临时占位符(placeholder),可以对它发布回退(与回退整个事务处理不同)。 + +事务开始结束标记 + +```sql +-- SQL Server +BEGIN TRANSACTION +... +COMMIT TRANSACTION + +-- MariaDB 和 MySQL +SET TRANSACTION +... + +-- Oracle +SET TRANSACTION +... + +-- PostgreSQL +BEGIN +... +``` + +SQL 的 ROLLBACK 命令用来回退(撤销)SQL 语句 + +```sql +DELETE FROM Orders; +ROLLBACK; +``` + +一般的 SQL 语句都是针对数据库表直接执行和编写的。这就是所谓的隐式提交(implicit commit),即提交(写或保存)操作是自动进行的。 + +在事务处理块中,提交不会隐式进行。进行明确的提交,使用 COMMIT 语句。 + +```sql +BEGIN TRANSACTION +DELETE OrderItems WHERE order_num = 12345 +DELETE Orders WHERE order_num = 12345 +COMMIT TRANSACTION +``` + +要支持回退部分事务,必须在事务处理块中的合适位置放置占位符。这样,如果需要回退,可以回退到某个占位符。在 SQL 中,这些占位符称为保留点。 + +```sql +-- MariaDB、MySQL 和 Oracle +SAVEPOINT delete1; +... +ROLLBACK TO delete1; + +-- SQL Server +SAVE TRANSACTION delete1; +ROLLBACK TRANSACTION delete1; +``` + +## 第 21 课 使用游标 + +SQL 检索操作返回一组称为结果集的行,这组返回的行都是与 SQL 语句相匹配的行(零行或多行)。简单地使用 SELECT 语句,没有办法得到第一行、下一行或前 10 行。 + +有时,需要在检索出来的行中前进或后退一行或多行,这就是游标的用途所在。游标(cursor)是一个存储在 DBMS 服务器上的数据库查询,它不是一条 SELECT 语句,而是被该语句检索出来的结果集。 + +游标要点 + +- 能够标记游标为只读,使数据能读取,但不能更新和删除。 +- 能控制可以执行的定向操作(向前、向后、第一、最后、绝对位置、相对位置等)。 +- 能标记某些列为可编辑的,某些列为不可编辑的。 +- 规定范围,使游标对创建它的特定请求(如存储过程)或对所有请求可访问。 +- 指示 DBMS 对检索出的数据(而不是指出表中活动数据)进行复制,使数据在游标打开和访问期间不变化。 + +使用 DECLARE 语句创建游标,这条语句在不同的 DBMS 中有所不同。DECLARE 命名游标,并定义相应的 SELECT 语句,根据需要带 WHERE 和其他子句。 + +```sql +-- DB2、MariaDB、MySQL 和 SQL Server +DECLARE CustCursor CURSOR +FOR +SELECT * FROM Customers +WHERE cust_email IS NULL + +-- Oracle 和 PostgreSQL +DECLARE CURSOR CustCursor +IS +SELECT * FROM Customers +WHERE cust_email IS NULL +``` + +使用 OPEN CURSOR 语句打开游标,在大多数 DBMS 中的语法相同: + +```sql +OPEN CURSOR CustCursor +``` + +关闭游标 + +```sql +CLOSE CustCursor +``` + +## 第 22 课 高级 SQL 特性 + +### 约束 + +约束(constraint)管理如何插入或处理数据库数据的规则。 + +DBMS 通过在数据库表上施加约束来实施引用完整性。大多数约束是在表定义中定义的,用 CREATE TABLE 或 ALTER TABLE 语句。 + +主键是一种特殊的约束,用来保证一列(或一组列)中的值是唯一的,而且永不改动。换句话说,表中的一列(或多个列)的值唯一标识表中的每一行。 + +表中任意列只要满足以下条件,都可以用于主键 + +- 任意两行的主键值都不相同。 +- 每行都具有一个主键值(即列中不允许 NULL 值)。 +- 包含主键值的列从不修改或更新。 +- 主键值不能重用。如果从表中删除某一行,其主键值不分配给新行。 + +创建表时指定主键 + +```sql +CREATE TABLE vendors ( + vend_id CHAR(10) NOT NULL PRIMARY KEY, + vend_name CHAR(50) NOT NULL, + vend_address CHAR(50) NULL, + vend_city CHAR(50) NULL, + vend_state CHAR(5) NULL, + vend_zip CHAR(10) NULL, + vend_country CHAR(50) NULL +); +``` + +更新表时指定主键 + +```sql +ALTER TABLE Vendors +ADD CONSTRAINT PRIMARY KEY (vend_id); +``` + +外键是表中的一列,其值必须列在另一表的主键中。 + +创建表时指定外键 + +cust_id 中的任何值都必须是 Customers 表的 cust_id 中的值 + +```sql +CREATE TABLE Orders ( + order_num INTEGER NOT NULL PRIMARY KEY, + order_date DATETIME NOT NULL, + cust_id CHAR(10) NOT NULL REFERENCES customers(cust_id) +); +``` + +更新表时指定外键 + +```sql +ALTER TABLE Orders +ADD CONSTRAINT +FOREIGN KEY (cust_id) REFERENCES Customers (cust_id) +``` + +唯一约束用来保证一列(或一组列)中的数据是唯一的。它们类似于主键,但存在以下重要区别。 + +- 表可包含多个唯一约束,但每个表只允许一个主键。 +- 唯一约束列可包含 NULL 值。 +- 唯一约束列可修改或更新。 +- 唯一约束列的值可重复使用。 +- 与主键不一样,唯一约束不能用来定义外键。 + +检查约束用来保证一列(或一组列)中的数据满足一组指定的条件。检查约束的常见用途有以下几点。 + +- 检查最小或最大值。例如,防止 0 个物品的订单(即使 0 是合法的数)。 +- 指定范围。例如,保证发货日期大于等于今天的日期,但不超过今天起一年后的日期。 +- 只允许特定的值。例如,在性别字段中只允许 M 或 F。 + +利用这个约束,任何插入(或更新)的行都会被检查,保证 quantity 大于 0。 + +```sql +CREATE TABLE OrderItems ( + order_num INTEGER NOT NULL, + order_item INTEGER NOT NULL, + prod_id CHAR(10) NOT NULL, + quantity INTEGER NOT NULL CHECK (quantity > 0), + item_price MONEY NOT NULL +); +``` + +检查名为 gender 的列只包含 M 或 F,可编写如下的 ALTER TABLE 语句: + +```sql +ADD CONSTRAINT CHECK (gender LIKE '[MF]') +``` + +### 索引 + +索引用来排序数据以加快搜索和排序操作的速度。 + +```sql +CREATE INDEX prod_name_ind +ON Products (prod_name); +``` + +### 触发器 + +触发器是特殊的存储过程,它在特定的数据库活动发生时自动执行。触发器可以与特定表上的 INSERT、UPDATE 和 DELETE 操作(或组合)相关联。 + +触发器的一些常见用途 + +- 保证数据一致。例如,在 INSERT 或 UPDATE 操作中将所有州名转换为大写。 +- 基于某个表的变动在其他表上执行活动。例如,每当更新或删除一行时将审计跟踪记录写入某个日志表。 +- 进行额外的验证并根据需要回退数据。例如,保证某个顾客的可用资金不超限定,如果已经超出,则阻塞插入。 +- 计算计算列的值或更新时间戳。 + +```sql +-- SQL Server +CREATE TRIGGER customer_state +ON Customers +FOR INSERT, UPDATE +AS +UPDATE Customers +SET cust_state = Upper(cust_state) +WHERE Customers.cust_id = inserted.cust_id; + +-- Oracle 和 PostgreSQL +CREATE TRIGGER customer_state +AFTER INSERT OR UPDATE +FOR EACH ROW +BEGIN +UPDATE Customers +SET cust_state = Upper(cust_state) +WHERE Customers.cust_id = :OLD.cust_id +END; +``` + +### 数据库安全 + +安全性使用 SQL 的 GRANT 和 REVOKE 语句来管理。 + +## 参考资料 + +- [《SQL 必知必会》](https://book.douban.com/subject/35167240/) diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Elasticsearch\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260\344\270\200.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Elasticsearch\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260\344\270\200.md" new file mode 100644 index 0000000000..590a768a05 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Elasticsearch\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260\344\270\200.md" @@ -0,0 +1,1618 @@ +--- +title: 《极客时间教程 - Elasticsearch 核心技术与实战》笔记一 +date: 2024-11-07 07:36:23 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +permalink: /pages/bc8326a0/ +--- + +# 《极客时间教程 - Elasticsearch 核心技术与实战》笔记一 + +## 第一章:概述 + +### 课程介绍(略) + +### 课程综述及学习建议(略) + +### Elasticsearch 概述及其发展历史 + +Elasticsearch 是一款基于 Lucene 的开源分布式搜索引擎。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202411060749487.png) + +- 1.0(2014 年 1 月) +- 5.0(2016 年 10 月) + - Lucene 6.x + - 默认打分机制从 TD-IDF 改为 BM 25 + - 支持 Keyword 类型 +- 6.0(2017 年 10 月) + - Lucene 7.x + - 跨集群复制 + - 索引生命周期管理 + - SQL 的支持 +- 7.0(2019 年 4 月) + - Lucene 7.x + - 移除 Type + - ECK (用于支持 K8S) + - 集群协调 + - High Level Rest Client + - Script Score 查询 + +### Elastic Stack 家族成员及其应用场景 + +Elasticsearch、Logstash、Kibana + +Beats - 各种采集器 + +X-Pack - 商业化套件 + +## 第二章:安装上手 + +### Elasticsearch 的安装与简单配置 + +【示例】 + +```shell +#启动单节点 +bin/elasticsearch -E node.name=node0 -E cluster.name=geektime -E path.data=node0_data + +#安装插件 +bin/elasticsearch-plugin install analysis-icu + +#查看插件 +bin/elasticsearch-plugin list +#查看安装的插件 +GET http://localhost:9200/_cat/plugins?v + +#start multi-nodes Cluster +bin/elasticsearch -E node.name=node0 -E cluster.name=geektime -E path.data=node0_data +bin/elasticsearch -E node.name=node1 -E cluster.name=geektime -E path.data=node1_data +bin/elasticsearch -E node.name=node2 -E cluster.name=geektime -E path.data=node2_data +bin/elasticsearch -E node.name=node3 -E cluster.name=geektime -E path.data=node3_data + +#查看集群 +GET http://localhost:9200 +#查看 nodes +GET _cat/nodes +GET _cluster/health +``` + +- [ES 安装指南](https://www.elastic.co/guide/en/elasticsearch/reference/current/install-elasticsearch.html) + +### Kibana 的安装与界面快速浏览 + +```shell +#启动 kibana +bin/kibana + +#查看插件 +bin/kibana-plugin list +``` + +资料: + +- [Kibana 安装](https://www.elastic.co/guide/en/kibana/current/setup.html) +- [Kibana 相关插件](https://www.elastic.co/guide/en/kibana/current/known-plugins.html) + +### 在 Docker 容器中运行 Elasticsearch,Kibana 和 Cerebro + +### Logstash 安装与导入数据 + +- [Logstash 下载](https://www.elastic.co/cn/downloads/logstash) +- [Logstash 参考文档](https://www.elastic.co/guide/en/logstash/current/index.html) + +## Elasticsearch 入门 + +### 基本概念 1 索引文档和 RESTAPI + +基本概念: + +- **Document** + - Elasticsearch 是面向文档的,文档是所有可搜索数据的最小单位。 + - Elasticsearch 中,文档会被序列化成 JSON 格式保存。无模式。 + - 每个文档都有一个唯一性 ID,如果没有指定,ES 会自动生成。 +- **Field** - 文档包含一组字段。每个字段有对应类型(字符串、数值、布尔、日期、二进制、范围) + - 元数据(内置字段) - 以 `_` 开头 + - `_index` - 文档所属索引 + - `_type` - 文档所属类型 + - `_id` - 文档的唯一 ID + - `_source` - 文档的原始数据(JSON) + - `_all` - 整合所有字段内容到该字段,已废弃 + - `_version` - 文档版本 + - `_score` - 相关性打分 +- **Index** - Document 的容器。 + - **Mapping** - 定义文档字段类型 + - **Setting** - 定义不同数据分布 +- **Type** - 7.0 移除 Type,每个 Index 只有一个名为 `_doc` 的 Type。 +- Node +- Shard +- Cluster + +【示例】 + +```shell +#查看索引相关信息 +GET kibana_sample_data_ecommerce + +#查看索引的文档总数 +GET kibana_sample_data_ecommerce/_count + +#查看前 10 条文档,了解文档格式 +POST kibana_sample_data_ecommerce/_search +{ +} + +#_cat indices API +#查看 indices +GET /_cat/indices/kibana*?v&s=index + +#查看状态为绿的索引 +GET /_cat/indices?v&health=green + +#按照文档个数排序 +GET /_cat/indices?v&s=docs.count:desc + +#查看具体的字段 +GET /_cat/indices/kibana*?pri&v&h=health,index,pri,rep,docs.count,mt + +#How much memory is used per index? +GET /_cat/indices?v&h=i,tm&s=tm:desc +``` + +### 基本概念 2 - 集群、节点、分片、副本 + +集群的作用:高可用、可扩展 + +ES 集群通过集群名来区分。集群名通过配置文件或 `-E cluster.name=xxx` 来指定。 + +ES 节点通过配置文件或 `-E node.name=xxx` 指定。 + +每个 ES 节点启动后,会分配一个 UID,保存在 `data` 目录下 + +#### master 候选节点和 master 节点 + +每个节点启动后,默认就是一个 master 候选节点。候选节点可以通过选举,成为 master 节点。 + +集群中第一个节点启动时,会将自己选举为 master 节点。 + +每个节点上都保存了集群的状态,只有 master 节点才能修改集群的状态信息(通过集中式管理,保证数据一致性)。 + +集群状态信息: + +- 所有的节点信息 +- 所有的索引和相关 mapping、setting 信息 +- 分片的路由信息 + +#### data node 和 coordinating node + +- data node - 保存数据的节点,叫做 data node。负责保存分片数据。 +- coordinating node - 负责接受 client 请求,将请求分发到合适节点,最终把结果汇聚到一起。每个节点默认都有 coordinating node 的职责。 + +#### 其他节点类型 + +hot & warm 节点 - 不同硬件配置的 data node,用来实现 hot & warm 架构,降低集群部署成本。 + +机器学习节点 - 负责跑机器学习的 Job,用来做异常检测 + +tribe 节点 - 连接到不同的 ES 集群 + +#### 分片 + +主分片 - 用于水平扩展,以提升系统可承载的总数据量以及吞吐量。 + +- 一个分片是一个运行 Lucene 实例 +- 主分片数在索引创建时指定,后续不允许修改,除非 reindex + +副分片(副本) - 用于冗余,解决高可用的问题。 + +- 副本数,可以动态调整 +- 增加副本数,可以在一定程度上提高服务的可用性,以及查询的吞吐量。 + +生产环境的分片数,需要提前规划: + +分片数过小: + +- 无法通过增加节点实现水平扩展 +- 单个分片的数据量太大,导致数据重新分配耗时 + +分片数过大: + +- 影响搜索结果的相关性打分,影响统计结果的准确性 +- 单个节点上过多的分片,会导致资源浪费,同时也会影响性能 +- 7.0 开始,默认主分片数设置为 1, 解决了 over-sharding 的问题 + +#### 查看集群健康状态 + +`GET _cluster/health` 有三种结果: + +- Green - 主分片和副本都正常分配 +- Yellow - 主分片全部正常分配,有副本分片未能正常分配 +- Red - 有主分片未能分配 + +【示例】 + +```shell +get _cat/nodes?v +GET /_nodes/es7_01,es7_02 +GET /_cat/nodes?v +GET /_cat/nodes?v&h=id,ip,port,v,m + +GET _cluster/health +GET _cluster/health?level=shards +GET /_cluster/health/kibana_sample_data_ecommerce,kibana_sample_data_flights +GET /_cluster/health/kibana_sample_data_flights?level=shards + +#### cluster state +The cluster state API allows access to metadata representing the state of the whole cluster. This includes information such as +GET /_cluster/state + +#cluster get settings +GET /_cluster/settings +GET /_cluster/settings?include_defaults=true + +GET _cat/shards +GET _cat/shards?h=index,shard,prirep,state,unassigned.reason +``` + +### 文档的基本 CRUD 和批量操作 + +#### 文档的 CRUD + +- create - 创建文档,如果 ID 已存在,会失败 +- update - 增量更新文档,且文档必须已存在 +- index - 若文档不存在,则创建新文档;若文档存在,则删除现有文档,再创建新文档,同时 version+1 +- delete - DELETE `/_doc/1` +- read + +【示例】 + +```shell +# create document. 自动生成 _id +POST users/_doc +{ + "user" : "Mike", + "post_date" : "2019-04-15T14:12:12", + "message" : "trying out Kibana" +} + +#create document. 指定 Id。如果 id 已经存在,报错 +PUT users/_doc/1?op_type=create +{ + "user" : "Jack", + "post_date" : "2019-05-15T14:12:12", + "message" : "trying out Elasticsearch" +} + +#create document. 指定 ID 如果已经存在,就报错 +PUT users/_create/1 +{ + "user" : "Jack", + "post_date" : "2019-05-15T14:12:12", + "message" : "trying out Elasticsearch" +} + +### Get Document by ID +#Get the document by ID +GET users/_doc/1 + +### Index & Update +#Update 指定 ID (先删除,在写入) +GET users/_doc/1 + +PUT users/_doc/1 +{ + "user" : "Mike" +} + +#GET users/_doc/1 +#在原文档上增加字段 +POST users/_update/1/ +{ + "doc": { + "post_date": "2019-05-15T14:12:12", + "message": "trying out Elasticsearch" + } +} + +### Delete by Id +# 删除文档 +DELETE users/_doc/1 +``` + +#### 批量写 + +bulk API 支持四种类型: + +- index +- create +- update +- delete + +```shell +### Bulk 操作 +#执行两次,查看每次的结果 + +#执行第 1 次 +POST _bulk +{ "index" : { "_index" : "test", "_id" : "1" } } +{ "field1" : "value1" } +{ "delete" : { "_index" : "test", "_id" : "2" } } +{ "create" : { "_index" : "test2", "_id" : "3" } } +{ "field1" : "value3" } +{ "update" : {"_id" : "1", "_index" : "test"} } +{ "doc" : {"field2" : "value2"} } + +#执行第 2 次 +POST _bulk +{ "index" : { "_index" : "test", "_id" : "1" } } +{ "field1" : "value1" } +{ "delete" : { "_index" : "test", "_id" : "2" } } +{ "create" : { "_index" : "test2", "_id" : "3" } } +{ "field1" : "value3" } +{ "update" : {"_id" : "1", "_index" : "test"} } +{ "doc" : {"field2" : "value2"} } +``` + +#### 批量读 + +- mget + +- msearch + +```shell +### mget 操作 +GET /_mget +{ + "docs": [ + { + "_index": "test", + "_id": "1" + }, + { + "_index": "test", + "_id": "2" + } + ] +} + +#URI 中指定 index +GET /test/_mget +{ + "docs": [ + { + "_id": "1" + }, + { + "_id": "2" + } + ] +} + +GET /_mget +{ + "docs": [ + { + "_index": "test", + "_id": "1", + "_source": false + }, + { + "_index": "test", + "_id": "2", + "_source": [ + "field3", + "field4" + ] + }, + { + "_index": "test", + "_id": "3", + "_source": { + "include": [ + "user" + ], + "exclude": [ + "user.location" + ] + } + } + ] +} + +### msearch 操作 +POST kibana_sample_data_ecommerce/_msearch +{} +{"query":{"match_all":{}},"size":1} +{"index":"kibana_sample_data_flights"} +{"query":{"match_all":{}},"size":2} + +### 清除测试数据 +#清除数据 +DELETE users +DELETE test +DELETE test2 +``` + +### 倒排索引入门 + +什么是正排,什么是倒排? + +- ** 正排 **:文档 ID 到文档内容和单词的关联 +- ** 倒排 **:单词到文档 ID 的关系 + +倒排索引含两个部分 + +- ** 单词词典 ** - 记录所有文档的单词,记录单词到倒排列表的关联关系 +- ** 倒排列表 ** - 记录了单词对应的文档结合,由倒排索引项组成。 + +倒排索引项: + +- 文档 ID +- 词频 TF - 单词在文档中出现的次数,用于相关性评分 +- 位置 - 单词文档中分词的位置。用于语句搜索 +- 偏移 - 记录单词的开始结束位置,实现高亮显示 + +要点: + +- 文档中每个字段都有自己的倒排索引 +- 可以指定某些字段不做索引 + +【示例】 + +```shell +POST _analyze +{ + "analyzer": "standard", + "text": "Mastering Elasticsearch" +} + +POST _analyze +{ + "analyzer": "standard", + "text": "Elasticsearch Server" +} + +POST _analyze +{ + "analyzer": "standard", + "text": "Elasticsearch Essentials" +} +``` + +### 通过分析器进行分词 + +** 分词 **:文本分析是把全文本转换一系列单词(term / token)的过程。 + +分析组件由如下三部分组成,它的执行顺序如下: + +``` +Character Filters -> Tokenizer -> Token Filters +``` + +说明: + +- Character Filters(字符过滤器) - 针对原始文本处理, 例如去除特殊字符、过了 html 标签 +- Tokenizer(分词器) - 按照策略将文本切分为单词 +- Token Filters(分词过滤器) - 对切分的单词进行加工,如:转为小写、删除 stop word、增加同义词等 + +ES 内置分析器: + +- **[Standard Analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-standard-analyzer.html)** - 默认分词器,按词切分,小写处理。 +- **[Simple Analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-simple-analyzer.html)** - 按非字母切分(过滤符号),小写处理。 +- **[Whitespace Analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-whitespace-analyzer.html)** - 按空格切分,不转小写。 +- **[Stop Analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-stop-analyzer.html)** - 小写处理,停用词过滤。 +- **[Keyword Analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-keyword-analyzer.html)** - 不分词,直接将输入当做输出。 +- **[Pattern Analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-pattern-analyzer.html)** - 按正则分词,默认正则为 `\W+`。 +- **[Language Analyzers](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-lang-analyzer.html)** - 提供 30 多种常见语言的分词器。 +- **[Fingerprint Analyzer](https://www.elastic.co/guide/en/elasticsearch/reference/current/analysis-fingerprint-analyzer.html)** - 可用于重复检测的指纹。 + +中文分词 + +elasticsearch-analysis-ik + +elasticsearch-thulac-plugin + +【示例】 + +```shell +#查看不同的 analyzer 的效果 +#standard +GET _analyze +{ + "analyzer": "standard", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +#simpe +GET _analyze +{ + "analyzer": "simple", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +GET _analyze +{ + "analyzer": "stop", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +#stop +GET _analyze +{ + "analyzer": "whitespace", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +#keyword +GET _analyze +{ + "analyzer": "keyword", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +GET _analyze +{ + "analyzer": "pattern", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +#english +GET _analyze +{ + "analyzer": "english", + "text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening." +} + +POST _analyze +{ + "analyzer": "icu_analyzer", + "text": "他说的确实在理”" +} + +POST _analyze +{ + "analyzer": "standard", + "text": "他说的确实在理”" +} + +POST _analyze +{ + "analyzer": "icu_analyzer", + "text": "这个苹果不大好吃" +} +``` + +### SearchAPI 概览 + +ES Search 有两种类型: + +- URI 查询 - 在 URL 中使用查询 +- Request Body 查询 - 基于 JSON 格式的 DSL + +| 语法 | 范围 | +| ------------------------ | ------------------- | +| `/_search` | 集群上的所有索引 | +| `/index1/_search` | index1 | +| `/index1,index2/_search` | index1 和 index2 | +| `/index*/_search` | 以 index 开头的索引 | + +【示例】 + +```shell +#URI Query +GET kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie +GET kibana*/_search?q=customer_first_name:Eddie +GET /_all/_search?q=customer_first_name:Eddie + +#REQUEST Body +POST kibana_sample_data_ecommerce/_search +{ + "profile": true, + "query": { + "match_all": {} + } +} +``` + +### URISearch 详解 + +使用 `q` 指定查询字符串(query string) + +- `q` - 指定查询语句,使用 Query String 语义 +- `df` - 默认字段 +- `sort` - 排序 +- `from/size` - 分页 +- `profile` - 显示查询是如何被执行的 + +指定字段 vs. 泛查询 + +- q=title:2012 / q=2012 + +Term vs. Phrase + +- Beautiful Mind,等效于 Beautiful Or Mind +- "Beautiful Mind",等效于 Beautiful And Mind + +分组与引号 + +- title:(Beautiful And Mind) +- title="Beautiful Mind" + +布尔操作 + +- AND / OR / NOT 或 `&&` / `||` / `!` +- 必须大写 +- `title:(matrix NOT reloaded)` + +分组 + +- `+` 表示 must +- `-` 表示 must_not +- `title:(+matrix -reloaded)` + +范围查询 + +区间表示:[] 闭区间,{} 开区间 + +- `year:{2019 TO 2018}` +- `year:{* TO 2018}` + +算数符号 + +- `year:>2010` +- `year:(>2010 && <=2018)` +- `year:(+>2010 +<=2018)` + +通配符查询(通配符查询效率低,占用内存大,不建议使用。特别是放在最前面) + +`?` 表示 1 个字符;`*` 表示任意个字符 + +- `title:mi?d` +- `title:be*` + +正则表达式 + +- `title:[bt]oy` + +模糊匹配与近似查询 + +- `title:befutifl~1` +- `title:"lord rings"~2` + +```shell + +#基本查询 +GET /movies/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s + +#带 profile +GET /movies/_search?q=2012&df=title +{ + "profile":"true" +} + +#泛查询,正对 _all, 所有字段 +GET /movies/_search?q=2012 +{ + "profile":"true" +} + +#指定字段 +GET /movies/_search?q=title:2012&sort=year:desc&from=0&size=10&timeout=1s +{ + "profile":"true" +} + +# 查找美丽心灵,Mind 为泛查询 +GET /movies/_search?q=title:Beautiful Mind +{ + "profile":"true" +} + +# 泛查询 +GET /movies/_search?q=title:2012 +{ + "profile":"true" +} + +#使用引号,Phrase 查询 +GET /movies/_search?q=title:"Beautiful Mind" +{ + "profile":"true" +} + +#分组,Bool 查询 +GET /movies/_search?q=title:(Beautiful Mind) +{ + "profile":"true" +} + +#布尔操作符 +# 查找美丽心灵 +GET /movies/_search?q=title:(Beautiful AND Mind) +{ + "profile":"true" +} + +# 查找美丽心灵 +GET /movies/_search?q=title:(Beautiful NOT Mind) +{ + "profile":"true" +} + +# 查找美丽心灵 +GET /movies/_search?q=title:(Beautiful %2BMind) +{ + "profile":"true" +} + +#范围查询 , 区间写法 +GET /movies/_search?q=title:beautiful AND year:[2002 TO 2018%7D +{ + "profile":"true" +} + +#通配符查询 +GET /movies/_search?q=title:b* +{ + "profile":"true" +} + +// 模糊匹配 & 近似度匹配 +GET /movies/_search?q=title:beautifl~1 +{ + "profile":"true" +} + +GET /movies/_search?q=title:"Lord Rings"~2 +{ + "profile":"true" +} +``` + +### RequestBody 与 QueryDSL 简介 + +- DSL +- from / size(分页) +- sort(排序) +- \_source(原文本查询) +- script_fields(脚本) +- match +- match_phrase +- simple_query_string + +```shell +curl -XGET "http://localhost:9200/kibana_sample_data_ecommerce/_search" -H 'Content-Type: application/json' -d' +{ + "query": { + "match_all": {} + } +}' + +#ignore_unavailable=true,可以忽略尝试访问不存在的索引“404_idx”导致的报错 +#查询 movies 分页 +POST /movies,404_idx/_search?ignore_unavailable=true +{ + "profile": true, + "query": { + "match_all": {} + } +} + +POST /kibana_sample_data_ecommerce/_search +{ + "from":10, + "size":20, + "query":{ + "match_all": {} + } +} + +#对日期排序 +POST kibana_sample_data_ecommerce/_search +{ + "sort":[{"order_date":"desc"}], + "query":{ + "match_all": {} + } + +} + +#source filtering +POST kibana_sample_data_ecommerce/_search +{ + "_source":["order_date"], + "query":{ + "match_all": {} + } +} + +#脚本字段 +GET kibana_sample_data_ecommerce/_search +{ + "script_fields": { + "new_field": { + "script": { + "lang": "painless", + "source": "doc['order_date'].value+'hello'" + } + } + }, + "query": { + "match_all": {} + } +} + +POST movies/_search +{ + "query": { + "match": { + "title": "last christmas" + } + } +} + +POST movies/_search +{ + "query": { + "match": { + "title": { + "query": "last christmas", + "operator": "and" + } + } + } +} + +POST movies/_search +{ + "query": { + "match_phrase": { + "title":{ + "query": "one love" + + } + } + } +} + +POST movies/_search +{ + "query": { + "match_phrase": { + "title":{ + "query": "one love", + "slop": 1 + + } + } + } +} +``` + +### QueryString&SimpleQueryString 查询 + +```shell +PUT /users/_doc/1 +{ + "name":"Ruan Yiming", + "about":"java, golang, node, swift, elasticsearch" +} + +PUT /users/_doc/2 +{ + "name":"Li Yiming", + "about":"Hadoop" +} + +POST users/_search +{ + "query": { + "query_string": { + "default_field": "name", + "query": "Ruan AND Yiming" + } + } +} + +POST users/_search +{ + "query": { + "query_string": { + "fields":["name","about"], + "query": "(Ruan AND Yiming) OR (Java AND Elasticsearch)" + } + } +} + +#Simple Query 默认的 operator 是 Or +POST users/_search +{ + "query": { + "simple_query_string": { + "query": "Ruan AND Yiming", + "fields": ["name"] + } + } +} + +POST users/_search +{ + "query": { + "simple_query_string": { + "query": "Ruan Yiming", + "fields": ["name"], + "default_operator": "AND" + } + } +} + +GET /movies/_search +{ + "profile": true, + "query":{ + "query_string":{ + "default_field": "title", + "query": "Beafiful AND Mind" + } + } +} + +# 多 fields +GET /movies/_search +{ + "profile": true, + "query":{ + "query_string":{ + "fields":[ + "title", + "year" + ], + "query": "2012" + } + } +} + +GET /movies/_search +{ + "profile":true, + "query":{ + "simple_query_string":{ + "query":"Beautiful +mind", + "fields":["title"] + } + } +} +``` + +### DynamicMapping 和常见字段类型 + +#### 什么是 Mapping + +Mapping 类似数据库中 schema 的定义 + +Mapping 会将 JSON 文档映射成 Lucene 所需要的数据格式 + +一个 Mapping 属于一个索引的 Type + +#### 字段数据类型 + +- 简单类型 +- Text / Keyword +- Date +- Integer / Floating +- Boolean +- Ipv4 / Ipv6 +- 复杂类型 +- 对象类型 / 嵌套类型 +- 特殊类型 +- get_point & geo_shape / percolator + +#### 什么是 Dynamic Mapping + +在写入文档时,如果索引不存在,会自动创建索引 + +ES 会根据文档信息,自动推算出字段的类型 + +有时候,推算可能会不准确,当类型设置错误时,可能会导致一些功能无法正常运行。例如范围查询 + +#### 能否更改 Mapping 的字段类型 + +Dynamic 设为 true 时,一旦有新增字段的文档写入,Mapping 也同时被更新 + +Dynamic 设为 false 时,Mapping 不会被更新,新增字段的数据无法被索引,但是信息会出现在 \_source 中。 + +Dynamic 设为 stric 时,文档写入失败 + +对已有字段,一旦有数据写入,就不再支持修改字段的定义 + +如果希望改变字段类型,必须 reindex API,重建索引 + +【示例】 + +```shell +#写入文档,查看 Mapping +PUT mapping_test/_doc/1 +{ + "firstName":"Chan", + "lastName": "Jackie", + "loginDate":"2018-07-24T10:29:48.103Z" +} + +#查看 Mapping 文件 +GET mapping_test/_mapping + +#Delete index +DELETE mapping_test + +#dynamic mapping,推断字段的类型 +PUT mapping_test/_doc/1 +{ + "uid" : "123", + "isVip" : false, + "isAdmin": "true", + "age":19, + "heigh":180 +} + +#查看 Dynamic +GET mapping_test/_mapping + +#默认 Mapping 支持 dynamic,写入的文档中加入新的字段 +PUT dynamic_mapping_test/_doc/1 +{ + "newField":"someValue" +} + +#该字段可以被搜索,数据也在 _source 中出现 +POST dynamic_mapping_test/_search +{ + "query":{ + "match":{ + "newField":"someValue" + } + } +} + +#修改为 dynamic false +PUT dynamic_mapping_test/_mapping +{ + "dynamic": false +} + +#新增 anotherField +PUT dynamic_mapping_test/_doc/10 +{ + "anotherField":"someValue" +} + +#该字段不可以被搜索,因为 dynamic 已经被设置为 false +POST dynamic_mapping_test/_search +{ + "query":{ + "match":{ + "anotherField":"someValue" + } + } +} + +get dynamic_mapping_test/_doc/10 + +#修改为 strict +PUT dynamic_mapping_test/_mapping +{ + "dynamic": "strict" +} + +#写入数据出错,HTTP Code 400 +PUT dynamic_mapping_test/_doc/12 +{ + "lastField":"value" +} + +DELETE dynamic_mapping_test +``` + +### 显式 Mapping 设置与常见参数介绍 + +- index - 控制当前字段是否被索引 +- index_options - 控制倒排索引记录的内容 + - docs - 记录 doc id + - freqs - 记录 doc id 和 term freqencies + - positions - 记录 doc id 和 term freqencies、term position + - offsets - 记录 doc id 和 term freqencies、term position、char offsets +- null_value - 对 null 值实现搜索,只有 keyword 类型支持 +- copy_to - \_all 在 ES 7.X 被 copy_to 替代 + +ES 不提供专门的数组类型。但是任何字段 ,都可以包含多个相同类型的数值。 + +```shell +#设置 index 为 false +DELETE users +PUT users +{ + "mappings": { + "properties": { + "firstName": { + "type": "text" + }, + "lastName": { + "type": "text" + }, + "mobile": { + "type": "text", + "index": false + } + } + } +} + +PUT users/_doc/1 +{ + "firstName":"Ruan", + "lastName": "Yiming", + "mobile": "12345678" +} + +POST /users/_search +{ + "query": { + "match": { + "mobile":"12345678" + } + } +} + +#设定 Null_value +DELETE users +PUT users +{ + "mappings": { + "properties": { + "firstName": { + "type": "text" + }, + "lastName": { + "type": "text" + }, + "mobile": { + "type": "keyword", + "null_value": "NULL" + } + } + } +} + +PUT users/_doc/1 +{ + "firstName":"Ruan", + "lastName": "Yiming", + "mobile": null +} + +PUT users/_doc/2 +{ + "firstName": "Ruan2", + "lastName": "Yiming2" +} + +GET users/_search +{ + "query": { + "match": { + "mobile": "NULL" + } + } +} + +#设置 Copy to +DELETE users +PUT users +{ + "mappings": { + "properties": { + "firstName": { + "type": "text", + "copy_to": "fullName" + }, + "lastName": { + "type": "text", + "copy_to": "fullName" + } + } + } +} + +PUT users/_doc/1 +{ + "firstName": "Zhang", + "lastName": "Peng" +} + +GET users/_search?q=fullName:(Zhang Peng) + +POST users/_search +{ + "query": { + "match": { + "fullName": { + "query": "Zhang Peng", + "operator": "and" + } + } + } +} + +#数组类型 +PUT users/_doc/1 +{ + "name":"onebird", + "interests":"reading" +} + +PUT users/_doc/1 +{ + "name":"twobirds", + "interests":["reading","music"] +} + +POST users/_search +{ + "query": { + "match_all": {} + } +} + +GET users/_mapping +``` + +### 多字段特性及 Mapping 中配置自定义 Analyzer + +ES 内置的分析器无法满足需求时,可以自定义分析器,通过组合不同组件来进行定制: + +- Character Filter - html strip、mapping、pattern replace +- Tokenizer - whitespace、standard、uax_url_email、pattern、keyword、path hierarchy +- Token Filter - lowercase、stop、synonym + +【示例】 + +```shell +PUT logs/_doc/1 +{ + "level": "DEBUG" +} + +GET /logs/_mapping + +POST _analyze +{ + "tokenizer":"keyword", + "char_filter":["html_strip"], + "text": "hello world" +} + +POST _analyze +{ + "tokenizer":"path_hierarchy", + "text":"/user/ymruan/a/b/c/d/e" +} + +#使用 char filter 进行替换 +POST _analyze +{ + "tokenizer": "standard", + "char_filter": [ + { + "type" : "mapping", + "mappings" : [ "- => _"] + } + ], + "text": "123-456, I-test! test-990 650-555-1234" +} + +# char filter 替换表情符号 +POST _analyze +{ + "tokenizer": "standard", + "char_filter": [ + { + "type" : "mapping", + "mappings" : [ ":) => happy", ":( => sad"] + } + ], + "text": ["I am felling :)", "Feeling :( today"] +} + +# white space and snowball +GET _analyze +{ + "tokenizer": "whitespace", + "filter": ["stop","snowball"], + "text": ["The gilrs in China are playing this game!"] +} + +# whitespace 与 stop +GET _analyze +{ + "tokenizer": "whitespace", + "filter": ["stop","snowball"], + "text": ["The rain in Spain falls mainly on the plain."] +} + +# remove 加入 lowercase 后,The 被当成 stopword 删除 +GET _analyze +{ + "tokenizer": "whitespace", + "filter": ["lowercase","stop","snowball"], + "text": ["The gilrs in China are playing this game!"] +} + +# 正则表达式 +GET _analyze +{ + "tokenizer": "standard", + "char_filter": [ + { + "type": "pattern_replace", + "pattern": "http://(.*)", + "replacement": "$1" + } + ], + "text": "http://www.elastic.co" +} +``` + +### IndexTemplate 和 DynamicTemplate + +集群上的索引会越来越多,可以根据时间周期性创建索引,例如:log-yyyyMMdd + +index template - 帮助设定 mapping 和 setting,并按照一定规则,自动匹配到新创建的索引上。 + +- 模板仅在一个索引被新建时,才会起作用。修改模板不会影响已创建的索引。 +- 可以设定多个索引模板,这些设置会被 merge 在一起 +- 可以指定 order,以控制模板合并过程 + +什么是 Dynamic Template + +根据 ES 识别的数据类型,结合字段名称,来动态设定字段类型 + +- 所有的字符串类型都设定成 keyword,或关闭 keyword 字段 +- is 开头的字段都设置成 boolean +- long\_ 开头的都设置成 long 类型 + +Dynamic Template 要点 + +- Dynamic Template 是定义在某索引的 mapping 中 +- Template 有一个名称 +- 匹配规则是一个数组 +- 为匹配到字段设置 mapping +- match_mapping_type - 匹配自动识别的字段类型,如 string、boolean 等 +- match、unmatch - 匹配字段名 +- path_match、path_unmatch + +【示例】 + +```shell +#数字字符串被映射成 text,日期字符串被映射成日期 +PUT ttemplate/_doc/1 +{ + "someNumber":"1", + "someDate":"2019/01/01" +} +GET ttemplate/_mapping + +#Create a default template +PUT _template/template_default +{ + "index_patterns": ["*"], + "order" : 0, + "version": 1, + "settings": { + "number_of_shards": 1, + "number_of_replicas":1 + } +} + +PUT /_template/template_test +{ + "index_patterns" : ["test*"], + "order" : 1, + "settings" : { + "number_of_shards": 1, + "number_of_replicas" : 2 + }, + "mappings" : { + "date_detection": false, + "numeric_detection": true + } +} + +#查看 template 信息 +GET /_template/template_default +GET /_template/temp* + +#写入新的数据,index 以 test 开头 +PUT testtemplate/_doc/1 +{ + "someNumber":"1", + "someDate":"2019/01/01" +} +GET testtemplate/_mapping +get testtemplate/_settings + +PUT testmy +{ + "settings":{ + "number_of_replicas":5 + } +} + +put testmy/_doc/1 +{ + "key":"value" +} + +get testmy/_settings +DELETE testmy +DELETE /_template/template_default +DELETE /_template/template_test + +#Dynaminc Mapping 根据类型和字段名 +DELETE my_index + +PUT my_index/_doc/1 +{ + "firstName":"Ruan", + "isVIP":"true" +} + +GET my_index/_mapping +DELETE my_index +PUT my_index +{ + "mappings": { + "dynamic_templates": [ + { + "strings_as_boolean": { + "match_mapping_type": "string", + "match":"is*", + "mapping": { + "type": "boolean" + } + } + }, + { + "strings_as_keywords": { + "match_mapping_type": "string", + "mapping": { + "type": "keyword" + } + } + } + ] + } +} + +DELETE my_index +#结合路径 +PUT my_index +{ + "mappings": { + "dynamic_templates": [ + { + "full_name": { + "path_match": "name.*", + "path_unmatch": "*.middle", + "mapping": { + "type": "text", + "copy_to": "full_name" + } + } + } + ] + } +} + +PUT my_index/_doc/1 +{ + "name": { + "first": "John", + "middle": "Winston", + "last": "Lennon" + } +} + +GET my_index/_search?q=full_name:John +``` + +### Elasticsearch 聚合分析简介 + +聚合分类: + +- **Bucket** - 一些字段满足特定条件的文档的集合(分组) +- **Metric** - 一些数学运算,可以对文档字段进行统计分析 +- **Pipeline** - 对其他的聚合结果进行二次聚合 +- **Matrix** - 支持对多个字段的操作并提供一个结果矩阵 + +聚合支持嵌套 + +【示例】 + +```shell +# 按照目的地进行分桶统计 +GET kibana_sample_data_flights/_search +{ + "size": 0, + "aggs":{ + "flight_dest":{ + "terms":{ + "field":"DestCountry" + } + } + } +} + +#查看航班目的地的统计信息,增加平均,最高最低价格 +GET kibana_sample_data_flights/_search +{ + "size": 0, + "aggs":{ + "flight_dest":{ + "terms":{ + "field":"DestCountry" + }, + "aggs":{ + "avg_price":{ + "avg":{ + "field":"AvgTicketPrice" + } + }, + "max_price":{ + "max":{ + "field":"AvgTicketPrice" + } + }, + "min_price":{ + "min":{ + "field":"AvgTicketPrice" + } + } + } + } + } +} + +#价格统计信息 + 天气信息 +GET kibana_sample_data_flights/_search +{ + "size": 0, + "aggs": { + "flight_dest": { + "terms": { + "field": "DestCountry" + }, + "aggs": { + "stats_price": { + "stats": { + "field": "AvgTicketPrice" + } + }, + "weather": { + "terms": { + "field": "DestWeather", + "size": 5 + } + } + } + } + } +} +``` + +## 参考资料 + +- [极客时间教程 - Elasticsearch 核心技术与实战](https://time.geekbang.org/course/detail/100030501-102659) \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Elasticsearch\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260\344\272\214.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Elasticsearch\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260\344\272\214.md" new file mode 100644 index 0000000000..94b7c1d8fa --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Elasticsearch\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260\344\272\214.md" @@ -0,0 +1,1787 @@ +--- +title: 《极客时间教程 - Elasticsearch 核心技术与实战》笔记二 +date: 2024-11-12 07:58:46 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 搜索引擎数据库 + - Elasticsearch +permalink: /pages/87ebfd72/ +--- + +# 《极客时间教程 - Elasticsearch 核心技术与实战》笔记二 + +## 第四章:深入搜索 + +### 基于词项和基于全文的搜索 + +#### 基于词项的查询 + +Term 是表达语意的最小单位。搜索和利用统计语言模型进行自然语言处理都需要处理 Term + +Term 级别查询:Term / Range / Exists / Prefix / Wildcard + +在 ES 中,Term 查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度计算公式为每个包含该词项的文档进行相关度计算。 + +可以通过 Constant Score 将查询转换成一个 Filtering,避免算法,并利用缓存,提高性能。 + +#### 基于文本的查询 + +文本查询:match、match_phrase、query_string + +索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的词项列表。 + +查询时,会先对输入的查询进行分词,然后每个词项柱哥进行底层的查询,最终将结果进行合并,并为每个文档计算一个相关度分值。 + +【示例】 + +```shell +DELETE products +PUT products +{ + "settings": { + "number_of_shards": 1 + } +} + +POST /products/_bulk +{ "index": { "_id": 1 }} +{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" } +{ "index": { "_id": 2 }} +{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" } +{ "index": { "_id": 3 }} +{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" } + +GET /products + +POST /products/_search +{ + "query": { + "term": { + "desc": { + //"value": "iPhone" + "value":"iphone" + } + } + } +} + +POST /products/_search +{ + "query": { + "term": { + "desc.keyword": { + //"value": "iPhone" + //"value":"iphone" + } + } + } +} + +POST /products/_search +{ + "query": { + "term": { + "productID": { + "value": "XHDK-A-1293-#fJ3" + } + } + } +} + +POST /products/_search +{ + //"explain": true, + "query": { + "term": { + "productID.keyword": { + "value": "XHDK-A-1293-#fJ3" + } + } + } +} + +POST /products/_search +{ + "explain": true, + "query": { + "constant_score": { + "filter": { + "term": { + "productID.keyword": "XHDK-A-1293-#fJ3" + } + } + + } + } +} + +#设置 position_increment_gap +DELETE groups +PUT groups +{ + "mappings": { + "properties": { + "names":{ + "type": "text", + "position_increment_gap": 0 + } + } + } +} + +GET groups/_mapping + +POST groups/_doc +{ + "names": [ "John Water", "Water Smith"] +} + +POST groups/_search +{ + "query": { + "match_phrase": { + "names": { + "query": "Water Water", + "slop": 100 + } + } + } +} + +POST groups/_search +{ + "query": { + "match_phrase": { + "names": "Water Smith" + } + } +} +``` + +### 结构化搜索 + +结构化搜索是指对结构化数据的搜索。 + +日期、布尔、和数字类型都是结构化的。它们都有精确的格式,可以根据这些格式进行逻辑操作,如:比较范围、判定大小。 + +文本也可以是结构化的。结构化的文本可以精确匹配(term)或部分匹配(prefix) + +结构化结果只有是或否两个选项。 + +【示例】 + +```shell +#结构化搜索,精确匹配 +DELETE products +POST /products/_bulk +{ "index": { "_id": 1 }} +{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" } +{ "index": { "_id": 2 }} +{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" } +{ "index": { "_id": 3 }} +{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" } +{ "index": { "_id": 4 }} +{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" } + +GET products/_mapping + +#对布尔值 match 查询,有算分 +POST products/_search +{ + "profile": "true", + "query": { + "term": { + "avaliable": true + } + } +} + +#对布尔值,通过 constant score 转成 filtering,没有算分 +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "constant_score": { + "filter": { + "term": { + "avaliable": true + } + } + } + } +} + +#数字类型 Term +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "term": { + "price": 30 + } + } +} + +#数字类型 terms +POST products/_search +{ + "query": { + "constant_score": { + "filter": { + "terms": { + "price": [ + "20", + "30" + ] + } + } + } + } +} + +#数字 Range 查询 +GET products/_search +{ + "query" : { + "constant_score" : { + "filter" : { + "range" : { + "price" : { + "gte" : 20, + "lte" : 30 + } + } + } + } + } +} + +# 日期 range +POST products/_search +{ + "query" : { + "constant_score" : { + "filter" : { + "range" : { + "date" : { + "gte" : "now-1y" + } + } + } + } + } +} + +#exists 查询 +POST products/_search +{ + "query": { + "constant_score": { + "filter": { + "exists": { + "field": "date" + } + } + } + } +} + +#处理多值字段 +POST /movies/_bulk +{ "index": { "_id": 1 }} +{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy"} +{ "index": { "_id": 2 }} +{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"] } + +#处理多值字段,term 查询是包含,而不是等于 +POST movies/_search +{ + "query": { + "constant_score": { + "filter": { + "term": { + "genre.keyword": "Comedy" + } + } + } + } +} + +#字符类型 terms +POST products/_search +{ + "query": { + "constant_score": { + "filter": { + "terms": { + "productID.keyword": [ + "QQPX-R-3956-#aD8", + "JODL-X-1937-#pV7" + ] + } + } + } + } +} + +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "match": { + "price": 30 + } + } +} + +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "term": { + "date": "2019-01-01" + } + } +} + +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "match": { + "date": "2019-01-01" + } + } +} + +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "constant_score": { + "filter": { + "term": { + "productID.keyword": "XHDK-A-1293-#fJ3" + } + } + } + } +} + +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "term": { + "productID.keyword": "XHDK-A-1293-#fJ3" + } + } +} + +#对布尔数值 +POST products/_search +{ + "query": { + "constant_score": { + "filter": { + "term": { + "avaliable": "false" + } + } + } + } +} + +POST products/_search +{ + "query": { + "term": { + "avaliable": { + "value": "false" + } + } + } +} + +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "term": { + "price": { + "value": "20" + } + } + } +} + +POST products/_search +{ + "profile": "true", + "explain": true, + "query": { + "match": { + "price": "20" + } +} + +POST products/_search +{ + "query": { + "constant_score": { + "filter": { + "bool": { + "must_not": { + "exists": { + "field": "date" + } + } + } + } + } + } +} +``` + +### 搜索的相关性算分 + +搜索的相关性打分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分(\_score)。 + +ES5 之前,默认的相关性算法是 TD-IDF;ES5 后,采用 BM25。 + +词频(Term Frequency,TF) - 检索词在一篇文档中出现的频率 + +逆文档频率(Inverse Document Frequency,IDF) - log(全部文档数/检索词出现过的文档总数),用以表示检索词在所有文档中出现的频率。 + +Stop Word - 词项出现频率岁高,但对相关度几乎没有用户,例如:的、the、a 之类的词。 + +TF-IDF 本质上就是将 TF 求和变成了加权求和。 + +和 TF-IDF 相比,当 TF 无限增加时, BM 25 分支会趋于一个平稳值。 + +Boosting 是控制相关度的一种手段。 + +- boost > 1,打分的权重提升; +- 0 < boost < 1,打分的权重降低 +- boost < 0,贡献负分 + +【示例】 + +```shell +PUT testscore +{ + "settings": { + "number_of_shards": 1 + }, + "mappings": { + "properties": { + "content": { + "type": "text" + } + } + } +} + +PUT testscore/_bulk +{ "index": { "_id": 1 }} +{ "content":"we use Elasticsearch to power the search" } +{ "index": { "_id": 2 }} +{ "content":"we like elasticsearch" } +{ "index": { "_id": 3 }} +{ "content":"The scoring of documents is caculated by the scoring formula" } +{ "index": { "_id": 4 }} +{ "content":"you know, for search" } + +POST /testscore/_search +{ + //"explain": true, + "query": { + "match": { + "content":"you" + //"content": "elasticsearch" + //"content":"the" + //"content": "the elasticsearch" + } + } +} + +POST testscore/_search +{ + "query": { + "boosting" : { + "positive" : { + "term" : { + "content" : "elasticsearch" + } + }, + "negative" : { + "term" : { + "content" : "like" + } + }, + "negative_boost" : 0.2 + } + } +} + +POST tmdb/_search +{ + "_source": [ + "title", + "overview" + ], + "query": { + "more_like_this": { + "fields": [ + "title^10", + "overview" + ], + "like": [ + { + "_id": "14191" + } + ], + "min_term_freq": 1, + "max_query_terms": 12 + } + } +} +``` + +### Query & Filtering 实现多字符串多字段查询 + +ES 中,有 Query 和 Filter 两种不同的 Context + +- Query - 有相关性计算 +- Filter - 没有相关性计算,可以利用缓存,性能更好 + +【示例】 + +```shell +POST /products/_bulk +{ "index": { "_id": 1 }} +{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" } +{ "index": { "_id": 2 }} +{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" } +{ "index": { "_id": 3 }} +{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" } +{ "index": { "_id": 4 }} +{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" } + +#基本语法 +POST /products/_search +{ + "query": { + "bool" : { + "must" : { + "term" : { "price" : "30" } + }, + "filter": { + "term" : { "avaliable" : "true" } + }, + "must_not" : { + "range" : { + "price" : { "lte" : 10 } + } + }, + "should" : [ + { "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } }, + { "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } } + ], + "minimum_should_match" :1 + } + } +} + +#改变数据模型,增加字段。解决数组包含而不是精确匹配的问题 +POST /newmovies/_bulk +{ "index": { "_id": 1 }} +{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy","genre_count":1 } +{ "index": { "_id": 2 }} +{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"],"genre_count":2 } + +#must,有算分 +POST /newmovies/_search +{ + "query": { + "bool": { + "must": [ + {"term": {"genre.keyword": {"value": "Comedy"}}}, + {"term": {"genre_count": {"value": 1}}} + + ] + } + } +} + +#Filter。不参与算分,结果的 score 是 0 +POST /newmovies/_search +{ + "query": { + "bool": { + "filter": [ + {"term": {"genre.keyword": {"value": "Comedy"}}}, + {"term": {"genre_count": {"value": 1}}} + ] + + } + } +} + +#Filtering Context +POST _search +{ + "query": { + "bool" : { + + "filter": { + "term" : { "avaliable" : "true" } + }, + "must_not" : { + "range" : { + "price" : { "lte" : 10 } + } + } + } + } +} + +#Query Context +POST /products/_bulk +{ "index": { "_id": 1 }} +{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" } +{ "index": { "_id": 2 }} +{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" } +{ "index": { "_id": 3 }} +{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" } +{ "index": { "_id": 4 }} +{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" } + +POST /products/_search +{ + "query": { + "bool": { + "should": [ + { + "term": { + "productID.keyword": { + "value": "JODL-X-1937-#pV7"}} + }, + {"term": {"avaliable": {"value": true}} + } + ] + } + } +} + +#嵌套,实现了 should not 逻辑 +POST /products/_search +{ + "query": { + "bool": { + "must": { + "term": { + "price": "30" + } + }, + "should": [ + { + "bool": { + "must_not": { + "term": { + "avaliable": "false" + } + } + } + } + ], + "minimum_should_match": 1 + } + } +} + +#Controll the Precision +POST _search +{ + "query": { + "bool" : { + "must" : { + "term" : { "price" : "30" } + }, + "filter": { + "term" : { "avaliable" : "true" } + }, + "must_not" : { + "range" : { + "price" : { "lte" : 10 } + } + }, + "should" : [ + { "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } }, + { "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } } + ], + "minimum_should_match" :2 + } + } +} + +POST /animals/_search +{ + "query": { + "bool": { + "should": [ + { "term": { "text": "brown" }}, + { "term": { "text": "red" }}, + { "term": { "text": "quick" }}, + { "term": { "text": "dog" }} + ] + } + } +} + +POST /animals/_search +{ + "query": { + "bool": { + "should": [ + { "term": { "text": "quick" }}, + { "term": { "text": "dog" }}, + { + "bool":{ + "should":[ + { "term": { "text": "brown" }}, + { "term": { "text": "brown" }}, + ] + } + + } + ] + } + } +} + +DELETE blogs +POST /blogs/_bulk +{ "index": { "_id": 1 }} +{"title":"Apple iPad", "content":"Apple iPad,Apple iPad" } +{ "index": { "_id": 2 }} +{"title":"Apple iPad,Apple iPad", "content":"Apple iPad" } + +POST blogs/_search +{ + "query": { + "bool": { + "should": [ + {"match": { + "title": { + "query": "apple,ipad", + "boost": 1.1 + } + }}, + + {"match": { + "content": { + "query": "apple,ipad", + "boost": + } + }} + ] + } + } +} + +DELETE news +POST /news/_bulk +{ "index": { "_id": 1 }} +{ "content":"Apple Mac" } +{ "index": { "_id": 2 }} +{ "content":"Apple iPad" } +{ "index": { "_id": 3 }} +{ "content":"Apple employee like Apple Pie and Apple Juice" } + +POST news/_search +{ + "query": { + "bool": { + "must": { + "match":{"content":"apple"} + } + } + } +} + +POST news/_search +{ + "query": { + "bool": { + "must": { + "match":{"content":"apple"} + }, + "must_not": { + "match":{"content":"pie"} + } + } + } +} + +POST news/_search +{ + "query": { + "boosting": { + "positive": { + "match": { + "content": "apple" + } + }, + "negative": { + "match": { + "content": "pie" + } + }, + "negative_boost": 0.5 + } + } +} +``` + +### 单字符串多字段查询 - DisMaxQuery + +Disjunction Max Query - 将评分最高的字符评分作为结果返回,满足多个字段是竞争关系的场景 + +对最佳字段查询进行调优:通过控制 tie_breaker 参数,引入其他字段对计算的一些影响 + +【示例】 + +```shell +PUT /blogs/_doc/1 +{ + "title": "Quick brown rabbits", + "body": "Brown rabbits are commonly seen." +} + +PUT /blogs/_doc/2 +{ + "title": "Keeping pets healthy", + "body": "My quick brown fox eats rabbits on a regular basis." +} + +POST /blogs/_search +{ + "query": { + "bool": { + "should": [ + { "match": { "title": "Brown fox" }}, + { "match": { "body": "Brown fox" }} + ] + } + } +} + +POST blogs/_search +{ + "query": { + "dis_max": { + "queries": [ + { "match": { "title": "Quick pets" }}, + { "match": { "body": "Quick pets" }} + ] + } + } +} + +POST blogs/_search +{ + "query": { + "dis_max": { + "queries": [ + { + "match": { + "title": "Quick pets" + } + }, + { + "match": { + "body": "Quick pets" + } + } + ], + "tie_breaker": 0.2 + } + } +} +``` + +### 单字符串多字段查询 - Multi Match + +场景:最佳字段、多数字段、混合字段 + +multi_match + +best_fields 是默认类型,可以不指定 + +minimum_should_match 等参数可以传递到生成的 query 中 + +【示例】 + +```shell +POST blogs/_search +{ + "query": { + "dis_max": { + "queries": [ + { "match": { "title": "Quick pets" }}, + { "match": { "body": "Quick pets" }} + ], + "tie_breaker": 0.2 + } + } +} + +POST blogs/_search +{ + "query": { + "multi_match": { + "type": "best_fields", + "query": "Quick pets", + "fields": ["title","body"], + "tie_breaker": 0.2, + "minimum_should_match": "20%" + } + } +} + +POST books/_search +{ + "multi_match": { + "query": "Quick brown fox", + "fields": "*_title" + } +} + +POST books/_search +{ + "multi_match": { + "query": "Quick brown fox", + "fields": [ "*_title", "chapter_title^2" ] + } +} + +DELETE /titles +PUT /titles +{ + "settings": { "number_of_shards": 1 }, + "mappings": { + "my_type": { + "properties": { + "title": { + "type": "string", + "analyzer": "english", + "fields": { + "std": { + "type": "string", + "analyzer": "standard" + } + } + } + } + } + } +} + +PUT /titles +{ + "mappings": { + "properties": { + "title": { + "type": "text", + "analyzer": "english" + } + } + } +} + +POST titles/_bulk +{ "index": { "_id": 1 }} +{ "title": "My dog barks" } +{ "index": { "_id": 2 }} +{ "title": "I see a lot of barking dogs on the road " } + +GET titles/_search +{ + "query": { + "match": { + "title": "barking dogs" + } + } +} + +DELETE /titles +PUT /titles +{ + "mappings": { + "properties": { + "title": { + "type": "text", + "analyzer": "english", + "fields": {"std": {"type": "text","analyzer": "standard"}} + } + } + } +} + +POST titles/_bulk +{ "index": { "_id": 1 }} +{ "title": "My dog barks" } +{ "index": { "_id": 2 }} +{ "title": "I see a lot of barking dogs on the road " } + +GET /titles/_search +{ + "query": { + "multi_match": { + "query": "barking dogs", + "type": "most_fields", + "fields": [ "title", "title.std" ] + } + } +} + +GET /titles/_search +{ + "query": { + "multi_match": { + "query": "barking dogs", + "type": "most_fields", + "fields": [ + "title^10", + "title.std" + ] + } + } +} +``` + +### 多语言及中文分词与检索 + +自然语言与查询 recall + +处理人类自然语言时,有些情况下,尽管搜索和原文不完全匹配,但希望搜到一些内容。 + +可采取的优化: + +- 归一化词元 +- 抽取词根 +- 包含同义词 +- 拼写错误 + +混合语言、中文的分词都存在一些挑战 + +- 词干提取 +- 不正确的文档频率 +- 语言识别 +- 歧义 + +中文分析器 + +- elasticsearch-analysis-hanlp +- elasticsearch-analysis-ik +- elasticsearch-analysis-pinyin + +### SpaceJam 一个全文搜索的实例 + +### 使用 SearchTemplate 和 IndexAlias 进行查询 + +### 综合排序:Function Score Query 优化算分 + +ES 默认会以文档的相关度算分进行排序 + +可以指定一个或多个字段进行排序 + +使用相关度算分排序,不能满足某些特定条件 + +function_score 可以在查询结束后,对每一个匹配的文档进行一系列的重新算分,根据新生成的分数进行排序。提供了几种默认的计算分值的函数: + +- weight - 为每一个文档设置一个简单而不被规范化的权重 +- field_value_factor - 使用该数值来修改 `_score` +- random_score - 为每一个用户使用一个不同的,随机算分结果 +- 衰减函数 - 以某个字段的值为标准,距离某个值越近,得分越高 +- script_score - 自定义脚本完全控制所需逻辑 + +Boost Mode + +- multiply +- sum +- min / max +- replace + +Max Boost 可以将算分控制在一个最大值 + +【示例】 + +```shell +DELETE blogs +PUT /blogs/_doc/1 +{ + "title": "About popularity", + "content": "In this post we will talk about...", + "votes": 0 +} + +PUT /blogs/_doc/2 +{ + "title": "About popularity", + "content": "In this post we will talk about...", + "votes": 100 +} + +PUT /blogs/_doc/3 +{ + "title": "About popularity", + "content": "In this post we will talk about...", + "votes": 1000000 +} + +POST /blogs/_search +{ + "query": { + "function_score": { + "query": { + "multi_match": { + "query": "popularity", + "fields": [ "title", "content" ] + } + }, + "field_value_factor": { + "field": "votes" + } + } + } +} + +POST /blogs/_search +{ + "query": { + "function_score": { + "query": { + "multi_match": { + "query": "popularity", + "fields": [ "title", "content" ] + } + }, + "field_value_factor": { + "field": "votes", + "modifier": "log1p" + } + } + } +} + +POST /blogs/_search +{ + "query": { + "function_score": { + "query": { + "multi_match": { + "query": "popularity", + "fields": [ "title", "content" ] + } + }, + "field_value_factor": { + "field": "votes", + "modifier": "log1p" , + "factor": 0.1 + } + } + } +} + +POST /blogs/_search +{ + "query": { + "function_score": { + "query": { + "multi_match": { + "query": "popularity", + "fields": [ "title", "content" ] + } + }, + "field_value_factor": { + "field": "votes", + "modifier": "log1p" , + "factor": 0.1 + }, + "boost_mode": "sum", + "max_boost": 3 + } + } +} + +POST /blogs/_search +{ + "query": { + "function_score": { + "random_score": { + "seed": 911119 + } + } + } +} +``` + +### Term&PhraseSuggester + +### 自动补全与基于上下文的提示 + +Completion Suggester,对性能要求比较苛刻。采用了不同的数据结构,并非通过倒排索引来完成。而是将 Analyze 的数据编码成 FST 和索引一起存放。FST 会被 ES 整个加载进内存,速度很快。 + +精准度:completion > Phrase > Term + +召回率:Term > Phrase > Completion + +性能:Completion > Phrase > Term + +### 跨集群搜索 + +早期版本,通过 Tribe Node 可以实现多集群访问的需求,但是还存在一定的问题,现已废弃。 + +ES 5.3 引入了跨集群搜索的功能。 + +- 允许任何节点扮演 federated 节点,以轻量的方式,将搜索请求进行代理 +- 不需要以 Client Node 形式加入其他集群 + +## 第五章:分布式特性及分布式搜索的机制 + +### 集群分布式模型及选主与脑裂问题 + +分布式特性:高可用、易扩展(水平扩展,支持 PB 级数据) + +ES 集群名称可以通过配置或 -E cluster.name=xxx 来设定。 + +ES 节点本质上就是一个 JAVA 进程。一台机器上可以运行多个 ES 进程。 + +每个 ES 节点都有名字,可以通过配置文件或 -E node.name=xxx 来设定。 + +每个 ES 节点在启动后,会分片一个 UID,保存在 data 目录下。 + +- Coordinating Node - 处理请求的节点,叫 Coordinating Node(协调节点),每个节点默认都是协调节点。 +- Data Node - 保存分片数据的节点。默认就是 data node,可以设置 `node.data: false` 禁止成为数据节点。 +- Master Node - 负责处理创建、删除索引等请求;决定分片被分配到哪个节点;负责索引的创建与删除;维护并更新集群状态。 + - 节点启动后,默认为主节点的候选节点,可以在必要时参与选主,成为 master node。可以设置 node.master: false 禁止成为主节点候选节点。 +- 集群状态 + - 所有的节点信息 + - 所有的索引和其相关的 mapping、setting + - 分片的路由信息 + - 每个节点上都保存了集群的状态信息 + - 只有 master 节点才能修改集群的状态信息,并负责同步给其他节点 + +#### 选主过程 + +集群中的节点互 ping,node id 最小的会成为被选举的节点。 + +其他节点会加入集群,但是不承担 master 节点的角色,一旦发现被选中的主节点丢失,就会选举出新的 master。 + +#### 避免脑裂 + +7.0 之前,minimum_master_nodes 设为 N / 2 + 1 + +7.0 开始,ES 自动选择以形成仲裁的节点。 + +### 分片与集群的故障转移 + +主分片 - 水平扩展 + +副本分片 - 高可用:冗余、故障转移 + +分片数过小:无法通过增加节点实现扩展;分片数过大:使得单个分片容量很小,导致一个节点上有过多分片,影响性能。 + +### 文档分布式存储 + +文档到分配的路由 + +``` +shard = hash(_routing) % number_of_primary_shards +``` + +hash 算法确保离散 + +默认的 _routing 值是文档 id,可以定制 routing 数值 + +这也是设置 setting 中主分片数后,不能随意修改的根本原因。 + +### 分片及其生命周期 + +分片是 ES 中的最小工作单元。分片是一个 Lucene 的索引。 + +#### 倒排索引不可变性 + +无需考虑并发写文件的问题,避免了锁机制带来的性能问题 + +一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能 + +如果需要让一个新的文档可以被搜索,需要重建整个索引。 + +#### Lucene Index + +在 Lucene 中,单个倒排索引文件被称为 Segment。Segment 是自包闭的,不可变更的。多个 Segment 汇总在一起,称为 Lucene 的 Index,其对应的就是 ES 中的 shard + +当有新文档写入时,会生成新 Segment,查询时会同时查询所有 Segment,并且对结果汇总。Lucene 中有一个文件,用来记录所有 Segment 信息,叫做 Commit Point。 + +删除的文档信息,保存在 .del 文件中。 + +#### 什么是 Refresh + +将 Index buffer 写入 Segment 的过程叫 refresh。refresh 不执行 fsync 操作。 + +refresh 默认 1 秒发生一次,refresh 后,数据就可以被搜索到了。 + +如果系统有大量的数据写入,就会产生很多的 Segment + +index buffer 被占满时,会触发 refresh,默认是 JVM 的 10% + +#### 什么是事务日志 + +segment 写入磁盘的过程相对耗时,借助文件系统缓存,refresh 时,现将 segment 写入缓存以开放查询 + +为了保证数据不丢失,所以在 index 文档时,同时写事务日志,高版本开始,事务日志默认落盘。每个分片有一个事务日志。 + +在 ES refresh 时,index buffer 被清空,事务日志不会清空 + +#### 什么是 flush + +调用 refresh,index buffer 清空并 refresh + +调用 fsync,将缓存中的 segments 写入磁盘 + +清空事务日志 + +默认 30 分钟调用一次 + +事务日志满(512MB) + +#### Merge + +Segment 很多,需要被定期合并 + +ES 和 Lucene 会自动进行 Merge 操作 + +### 剖析分布式查询及相关性评分 + +ES 搜索分为两阶段: + +1. Query +2. Fetch + +#### Query 阶段 + +用户发出搜索请求到 ES 节点。节点收到请求后,会以协调节点的身份,在所有主副本分片中随机选择主分片数个分片,发送查询请求。 + +被选中的分片执行查询,进行排序。然后,每个分片都会返回 from +size 个排序后的文档 id 和排序值给协调节点。 + +#### Fetch 阶段 + +协调节点会将 Query 阶段从每个分片获取的排序后的文档 id 列表,进行重排序,选取 from 到 from +size 个文档的 id + +以 multi get 请求的方式,到相应的分片获取详细的文档数据 + +#### 潜在问题 + +性能问题 + +- 每个分片上需要查的文档数 = from + size +- 最终协调节点需要处理 = 主分片数 * ( from + size) +- 深度分页 + +相关性算分 + +每个分片都要基于自己分片上的数据进行相关度计算。这会导致打分偏离的情况,尤其是数据量很少时。当文档总数很少的情况下,主分片数越多,相关性计算会越不准。 + +解决算分不准的方法 + +将主分片数设为 1; + +使用 `_search?search_type=dfs_query_then_fetch`,消耗更多 CPU 和内存,执行性能低下 + +```shell +DELETE message +PUT message +{ + "settings": { + "number_of_shards": 20 + } +} + +GET message + +POST message/_doc?routing=1 +{ + "content":"good" +} + +POST message/_doc?routing=2 +{ + "content":"good morning" +} + +POST message/_doc?routing=3 +{ + "content":"good morning everyone" +} + +POST message/_search +{ + "explain": true, + "query": { + "match_all": {} + } +} + +POST message/_search +{ + "explain": true, + "query": { + "term": { + "content": { + "value": "good" + } + } + } +} + +POST message/_search?search_type=dfs_query_then_fetch +{ + + "query": { + "term": { + "content": { + "value": "good" + } + } + } +} +``` + +### 排序及 DocValues&Fielddata + +默认采用相关性算分对结果进行降序排序 + +可以通过设定 sorting 参数,自行设定排序 + +如果不指定 _score,算分为 null + +```shell +#单字段排序 +POST /kibana_sample_data_ecommerce/_search +{ + "size": 5, + "query": { + "match_all": { + + } + }, + "sort": [ + {"order_date": {"order": "desc"}} + ] +} + +#多字段排序 +POST /kibana_sample_data_ecommerce/_search +{ + "size": 5, + "query": { + "match_all": { + + } + }, + "sort": [ + {"order_date": {"order": "desc"}}, + {"_doc":{"order": "asc"}}, + {"_score":{ "order": "desc"}} + ] +} + +GET kibana_sample_data_ecommerce/_mapping + +#对 text 字段进行排序。默认会报错,需打开 fielddata +POST /kibana_sample_data_ecommerce/_search +{ + "size": 5, + "query": { + "match_all": { + + } + }, + "sort": [ + {"customer_full_name": {"order": "desc"}} + ] +} + +#打开 text 的 fielddata +PUT kibana_sample_data_ecommerce/_mapping +{ + "properties": { + "customer_full_name" : { + "type" : "text", + "fielddata": true, + "fields" : { + "keyword" : { + "type" : "keyword", + "ignore_above" : 256 + } + } + } + } +} + +#关闭 keyword 的 doc values +PUT test_keyword +PUT test_keyword/_mapping +{ + "properties": { + "user_name":{ + "type": "keyword", + "doc_values":false + } + } +} + +DELETE test_keyword + +PUT test_text +PUT test_text/_mapping +{ + "properties": { + "intro":{ + "type": "text", + "doc_values":true + } + } +} + +DELETE test_text + +DELETE temp_users +PUT temp_users +PUT temp_users/_mapping +{ + "properties": { + "name":{"type": "text","fielddata": true}, + "desc":{"type": "text","fielddata": true} + } +} + +Post temp_users/_doc +{"name":"Jack","desc":"Jack is a good boy!","age":10} + +#打开 fielddata 后,查看 docvalue_fields 数据 +POST temp_users/_search +{ + "docvalue_fields": [ + "name","desc" + ] +} + +#查看整型字段的 docvalues +POST temp_users/_search +{ + "docvalue_fields": [ + "age" + ] +} +``` + +### 分页与遍历-FromSize&SearchAfter&ScrollAPI + +#### from + size + +当一个查询:from = 990, size = 10,会在每个分片上先获取 1000 个文档。然后,通过协调节点聚合所有结果。最后,再通过排序选取前 1000 个文档。 + +页数越深,占用内存越多。为了避免深分页问题,ES 默认限定到 10000 个文档。 + +#### search after + +实时获取下一页文档信息,不支持指定页数,只能向下翻页。 + +需要指定 sort,并保证值是唯一的 + +然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询 + +#### scroll + +创建一个快照,有新的数据写入以后,无法被查到。 + +每次持续后,输入上一次的 scroll id + +```shell +POST tmdb/_search +{ + "from": 10000, + "size": 1, + "query": { + "match_all": { + + } + } +} + +#Scroll API +DELETE users + +POST users/_doc +{"name":"user1","age":10} + +POST users/_doc +{"name":"user2","age":11} + +POST users/_doc +{"name":"user2","age":12} + +POST users/_doc +{"name":"user2","age":13} + +POST users/_count + +POST users/_search +{ + "size": 1, + "query": { + "match_all": {} + }, + "sort": [ + {"age": "desc"} , + {"_id": "asc"} + ] +} + +POST users/_search +{ + "size": 1, + "query": { + "match_all": {} + }, + "search_after": + [ + 10, + "ZQ0vYGsBrR8X3IP75QqX"], + "sort": [ + {"age": "desc"} , + {"_id": "asc"} + ] +} + +#Scroll API +DELETE users +POST users/_doc +{"name":"user1","age":10} + +POST users/_doc +{"name":"user2","age":20} + +POST users/_doc +{"name":"user3","age":30} + +POST users/_doc +{"name":"user4","age":40} + +POST /users/_search?scroll=5m +{ + "size": 1, + "query": { + "match_all" : { + } + } +} + +POST users/_doc +{"name":"user5","age":50} +POST /_search/scroll +{ + "scroll" : "1m", + "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ==" +} +``` + +### 处理并发读写 + +采用乐观锁机制 + +内部版本控制:`_seq_no` + `primary_term` + +外部版本控制:`version` + `version_type=external` + +## 第六章:深入聚合分析 + +### Bucket&Metric聚合分析及嵌套聚合 + +Metric(统计) - 统计计算 + +Bucket(分组) - 按一定规则,将文档分配到不同的桶中。 + +Metric 聚合 + +- **单值聚合** - 只输出一个分析结果 + - min、max、avg、sum、cardinality +- **多值聚合** - 输出多个分析结果 + - stats、extended_stats、percentile、percentile_rank、top_hits + +### Pipeline聚合分析 + +Pipeline聚合支持对聚合分析的结果,进行再次聚合分析。 + +Pipeline 聚合的分析结果会输出到原结果中,根据位置的不同,分为两类: + +- **sibling** - 结果和现有分析结果同级。例如:[max_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-max-bucket-aggregation.html)、[min_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-min-bucket-aggregation.html)、[avg_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-avg-bucket-aggregation.html)、[sum_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-sum-bucket-aggregation.html)、[stats_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-stats-bucket-aggregation.html)、[extended_stats_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-extended-stats-bucket-aggregation.html)、[percentiles_bucket](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-percentiles-bucket-aggregation.html)。 +- **parent** - 结果内嵌到现有的聚合分析结果中。例如:[derivative](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-derivative-aggregation.html)、[cumulative_sum](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-cumulative-sum-aggregation.html)、[moving_function](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-pipeline-movfn-aggregation.html)。 + +### 聚合的作用范围及排序 + +ES 聚合分析的默认作用范围是 query 的查询结果集。 + +同时 ES 还支持以下方式改变聚合的作用范围: + +- filter +- post_filter +- global + +指定 order,按照 `_count` 和 `_key` 进行排序。 + +### 聚合分析的原理及精准度问题 + +ES 在进行聚合分析时,协调节点会在每个分片的主分片、副分片中选一个,然后在不同分片上分别进行聚合计算,然后将每个分片的聚合结果进行汇总,返回最终结果。 + +由于,并非基于全量数据进行计算,所以聚合结果并非完全准确。 + +要解决聚合准确性问题,有两个解决方案: + +- 解决方案 1:当数据量不大时,设置 Primary Shard 为 1,这意味着在数据全集上进行聚合。 +- 解决方案 2:设置 [`shard_size`](https://www.elastic.co/guide/en/elasticsearch/reference/current/search-aggregations-bucket-terms-aggregation.html#search-aggregations-bucket-terms-aggregation-shard-size) 参数,将计算数据范围变大,进而使得 ES 的**整体性能变低,精准度变高**。shard_size 值的默认值是 `size * 1.5 + 10`。 + +## 第七章:数据建模(略) + +## 第八章:保护你的数据(略) + +## 第九章:水平扩展 Elasticsearch 集群 + +## 第十章:生产环境中的集群运维(略) + +## 第十一章:索引生命周期管理(略) + +## 第十二章:用Logstash和Beats构建数据管道(略) + +## 第十三章:用Kibana进行数据可视化分析(略) + +## 第十四章:探索X-Pack套件(略) + +## 参考资料 + +- [极客时间教程 - Elasticsearch 核心技术与实战](https://time.geekbang.org/course/detail/100030501-102659) \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MongoDB\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260\344\270\200.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MongoDB\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260\344\270\200.md" new file mode 100644 index 0000000000..daf583033e --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MongoDB\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260\344\270\200.md" @@ -0,0 +1,1043 @@ +--- +title: 《极客时间教程 - MongoDB 高手课》笔记一 +date: 2024-10-17 07:19:53 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 文档数据库 + - MongoDB +permalink: /pages/bbbcae56/ +--- + +# 《极客时间教程 - MongoDB 高手课》笔记一 + +## 第一章:MongoDB 再入门 + +### MongoDB 简介 + +什么是 MongoDB? + +一个以 JSON 为数据模型的文档数据库。 + +为什么叫文档数据库? + +文档来自于“JSON Document”,并非我们一般理解的 PDF,WORD 文档。 + +谁开发 MongDB? + +上市公司 MongoDB Inc. ,总部位于美国纽约。 + +主要用途 + +- 应用数据库,类似于 Oracle, MySQL +- 海量数据处理,数据平台。 + +主要特点 + +- 建模为可选 + +- JSON 数据模型比较适合开发者 + +- 横向扩展可以支撑很大数据量和并发 + +MongoDB 是免费的吗? + +MongoDB 有两个发布版本:社区版和企业版。 + +- 社区版是基于 SSPL,一种和 AGPL 基本类似的开源协议 。 +- 企业版是基于商业协议,需付费使用。 + +### MongoDB vs. RDBMS + +| | MongoDB | RDBMS | +| ------------ | ------------------------------------------------------------ | ---------------------- | +| 数据模型 | 文档模型 | 关系模型 | +| 数据库类型 | OLTP | OLTP | +| CRUD 操作 | MQL/SQL | SQL | +| 高可用 | 复制集 | 集群模式 | +| 横向扩展能力 | 通过原生分片完善支持 | 数据分区或者应用侵入式 | +| 索引支持 | B-树、全文索引、地理位置索引、多键 (multikey) 索引、TTL 索引 | B 树 | +| 开发难度 | 容易 | 困难 | +| 数据容量 | 没有理论上限 | 千万、亿 | +| 扩展方式 | 垂直扩展+水平扩展 | 垂直扩展 | + +### MongoDB 特色及优势 + +文档模型的面向对象特点 + +灵活:快速响应业务变化 + +- 多形性:同一个集合中可以包含 不同字段(类型)的文档对象 +- 动态性:线上修改数据模式,修 改是应用与数据库均无须下线 +- 数据治理:支持使用 JSON Schema 来规范数据模式。在保证模式灵活动态的前提下,提供数据治理能力文档模型的快速开发特点 + +快速:最简单快速的开发方式 + +- 数据库引擎只需要在一个存储区读写 +- 反范式、无关联的组织极大优化查询速度 +- 程序 API 自然,开发快速 + +MongoDB 优势 + +- 原生的高可用和横向扩展能力 + - Replica Set – 2 to 50 个成员 + - 自恢复 + - 多中心容灾能力 + - 滚动服务 – 最小化服务终端 +- 横向扩展能力 + - 需要的时候无缝扩展 + - 应用全透明 + - 多种数据分布策略 + - 轻松支持 TB – PB 数量级 + +MongoDB 技术优势总结 + +- JSON 结构和对象模型接近,开发代码量低 +- JSON 的动态模型意味着更容易响应新的业务需求 +- 复制集提供 99.999% 高可用 +- 分片架构支持海量数据和无缝扩容 + +### MongoDB 基本操作 + +#### 使用 insert 完成插入操作 + +操作格式: + +```json +db.<集合>.insertOne() +db.<集合>.insertMany([, , …]) +``` + +示例: + +```json +db.fruit.insertOne({name: "apple"}) +db.fruit.insertMany([ + {name: "apple"}, + {name: "pear"}, + {name: "orange"} +]) +``` + +#### 使用 find 查询文档 + +find 是 MongoDB 中查询数据的基本指令,相当于 SQL 中的 SELECT 。 + +find 返回的是游标。 + +示例: + +```json +db.movies.find( { "year" : 1975 } ) //单条件查询 + +db.movies.find( { "year" : 1989, "title" : "Batman" } ) //多条件 and 查询 + +db.movies.find( { $and : [ {"title" : "Batman"}, { "category" : "action" }] } ) // and 的另一种形式 + +db.movies.find( { $or: [{"year" : 1989}, {"title" : "Batman"}] } ) //多条件 or 查询 + +db.movies.find( { "title" : /^B/} ) //按正则表达式查找 +``` + +##### 查询条件对照表 + +| SQL | MQL | +| -------- | ---------------- | +| `a = 1` | `{a: 1}` | +| `a <> 1` | `{a: {$ne: 1}}` | +| `a > 1` | `{a: {$gt: 1}}` | +| `a >= 1` | `{a: {$gte: 1}}` | +| `a < 1` | `{a: {$lt: 1}}` | +| `a <= 1` | `{a: {$lte: 1}}` | + +##### 查询逻辑对照表 + +| SQL | MQL | +| ----------------- | -------------------------------------------- | +| `a = 1 AND b = 1` | `{a: 1, b: 1}` 或 `{$and: [{a: 1}, {b: 1}]}` | +| `a = 1 OR b = 1` | `{$or: [{a: 1}, {b: 1}]}` | +| `a IS NULL` | `{a: {$exists: false}}` | +| `a IN (1, 2, 3)` | `{a: {$in: [1, 2, 3]}}` | + +##### 查询逻辑运算符 + +- `$lt` - 存在并小于 +- `$lte` - 存在并小于等于 +- `$gt` - 存在并大于 +- `$gte` - 存在并大于等于 +- `$ne` - 不存在或存在但不等于 +- `$in` - 存在并在指定数组中 +- `$nin` - 不存在或不在指定数组中 +- `$or` - 匹配两个或多个条件中的一个 +- `$and` - 匹配全部条件 + +#### 使用 find 搜索子文档 + +find 支持使用“field.sub_field”的形式查询子文档。假设有一个文档: + +```json +db.fruit.insertOne({ + name: "apple", + from: { + country: "China", + province: "Guangdon" + } +}) +``` + +```json +db.fruit.find( { "from.country" : "China" } ) +db.fruit.find( { "from" : {country: "China"} } ) +``` + +#### 使用 find 搜索数组 + +find 支持对数组中的元素进行搜索。 + +```json +db.fruit.insert([ + { "name" : "Apple", color: ["red", "green" ] }, + { "name" : "Mango", color: ["yellow", "green"] } +]) + +db.fruit.find({color: "red"}) +db.fruit.find({$or: [{color: "red"}, {color: "yellow"}]} ) +``` + +#### 使用 find 搜索数组中的对象 + +```json +db.movies.insertOne( { + "title" : "Raiders of the Lost Ark", + "filming_locations" : [ + { "city" : "Los Angeles", "state" : "CA", "country" : "USA" }, + { "city" : "Rome", "state" : "Lazio", "country" : "Italy" }, + { "city" : "Florence", "state" : "SC", "country" : "USA" } + ] +}) +// 查找城市是 Rome 的记录 +db.movies.find({"filming_locations.city": "Rome"}) +``` + +在数组中搜索子对象的多个字段时,如果使用 $elemMatch,它表示必须是同一个 子对象满足多个条件。考虑以下两个查询: + +```json +db.getCollection('movies').find({ + "filming_locations.city": "Rome", + "filming_locations.country": "USA" +}) + +db.getCollection('movies').find({ + "filming_locations": { + $elemMatch:{"city":"Rome", "country": "USA"} + } +}) +``` + +#### 控制 find 返回的字段 + +- find 可以指定只返回指定的字段; +- `_id`字段必须明确指明不返回,否则默认返回; +- 在 MongoDB 中我们称这为投影(projection); +- `db.movies.find({"category": "action"},{"_id":0, title:1})` + +#### 使用 remove 删除文档 + +remove 命令需要配合查询条件使用; + +匹配查询条件的的文档会被删除; + +指定一个空文档条件会删除所有文档; + +示例: + +```json +db.testcol.remove( { a : 1 } ) // 删除 a 等于 1 的记录 +db.testcol.remove( { a : { $lt : 5 } } ) // 删除 a 小于 5 的记录 +db.testcol.remove( { } ) // 删除所有记录 +db.testcol.remove() //报错 +``` + +#### 使用 update 更新文档 + +Update 操作执行格式:`db.<集合>.update(<查询条件>, <更新字段>)` + +示例: + +```json +db.fruit.insertMany([ + {name: "apple"}, + {name: "pear"}, + {name: "orange"} +]) + +db.fruit.updateOne({name: "apple"}, {$set: {from: "China"}}) +``` + +使用 updateOne 表示无论条件匹配多少条记录,始终只更新第一条; + +使用 updateMany 表示条件匹配多少条就更新多少条; + +updateOne/updateMany 方法要求更新条件部分必须具有以下之一,否则将报错: + +- `$set/$unset` + +- `$push/$pushAll/$pop` + +- `$pull/$pullAll` + +- `$addToSet` + +#### 使用 update 更新数组 + +- `$push`: 增加一个对象到数组底部 +- `$pushAll`: 增加多个对象到数组底部 +- `$pop`: 从数组底部删除一个对象 +- `$pull`: 如果匹配指定的值,从数组中删除相应的对象 +- `$pullAll`: 如果匹配任意的值,从数据中删除相应的对象 +- `$addToSet`: 如果不存在则增加一个值到数组 + +#### 使用 drop 删除集合 + +使用 db.<集合>.drop() 来删除一个集合 + +集合中的全部文档都会被删除 + +集合相关的索引也会被删除 + +```json +db.collection.drop() +``` + +#### 使用 dropDatabase 删除数据库 + +使用 db.dropDatabase() 来删除数据库 + +数据库相应文件也会被删除,磁盘空间将被释放 + +```json +use tempDB +db.dropDatabase() +show collections // No collections +show dbs // The db is gone +``` + +### 聚合查詢 + +#### 什么是 MongoDB 聚合框架 + +MongoDB 聚合框架(Aggregation Framework)是一个计算框架,它可以: + +- 作用在一个或几个集合上; + +- 对集合中的数据进行的一系列运算; + +- 将这些数据转化为期望的形式; + +从效果而言,聚合框架相当于 SQL 查询中的: + +- GROUP BY + +- LEFT OUTER JOIN + +- AS 等 + +#### 管道(Pipeline)和步骤(Stage) + +整个聚合运算过程称为管道(Pipeline),它是由多个步骤(Stage)组成的, 每个管道: + +- 接受一系列文档(原始数据); +- 每个步骤对这些文档进行一系列运算; +- 结果文档输出给下一个步骤; + +聚合计算的基本格式: + +``` +pipeline = [$stage1, $stage2, ...$stageN]; + +db..aggregate( + pipeline, + { options } +); +``` + +常见步骤: + +| 步骤 | 作用 | SQL 等价运算符 | +| -------------------- | -------- | ----------------- | +| `$match` | 过滤 | `WHERE` | +| `$project` | 投影 | `AS` | +| `$sort` | 排序 | `ORDER BY` | +| `$group` | 分组 | `GROUP BY` | +| `$skip` / `$limit` | 结果限制 | `SKIP` / `LIMIT` | +| `$lookup` | 左外连接 | `LEFT OUTER JOIN` | +| `$unwind` | 展开数组 | N/A | +| `$graphLookup` | 图搜索 | N/A | +| `$facet` / `$bucket` | 分面搜索 | N/A | + +常见步骤中的运算符 + +| `$match` | `$project` | `$group` | +| ------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | +| `$eq`/`$gt`/`$gte`/`$lt`/`$lte`
    `$and`/`$or`/`$not`/`$in`
    `$geoWithin`/`$intersect` | 选择需要的或排除不需要的字段
    `$map`/`$reduce`/`$filter`
    `$range`
    `$multiply`/`$divide`/`$substract`/`$add`
    `$year`/`$month`/`$dayOfMonth`/`$hour`/`$minute`/`$second`
    …… | `$sum`/`$avg`
    `$push`/`$addToSet`
    `$first`/`$last`/`$max`/`$min`
    …… | + +#### 聚合运算的使用场景 + +聚合查询可以用于 OLAP 和 OLTP 场景。例如: + +- OTLP - 计算 +- OLAP + - 分析一段时间内的销售总额、均值 + - 计算一段时间内的净利润 + - 分析购买人的年龄分布 + - 分析学生成绩分布 + - 统计员工绩效 + +MQL 常用步骤与 SQL 对比 + +【示例一】 + +```sql +SELECT +FIRST_NAME AS `名`, +LAST_NAME AS `姓` +FROM Users +WHERE GENDER = '男' +SKIP 100 +LIMIT 20 +``` + +等价于 + +```json +db.users.aggregate([ + {$match: {gender: ’’男”}}, + {$skip: 100}, + {$limit: 20}, + {$project: { + '名': '$first_name', + '姓': '$last_name' + }} +]); +``` + +【示例二】 + +```sql +SELECT DEPARTMENT, +COUNT(NULL) AS EMP_QTY +FROM Users +WHERE GENDER = '女' +GROUP BY DEPARTMENT HAVING +COUNT(*) < 10 +``` + +等价于 + +```json +db.users.aggregate([ + {$match: {gender: '女'}}, + {$group: { + _id: '$DEPARTMENT’, + emp_qty: {$sum: 1} + }}, + {$match: {emp_qty: {$lt: 10}}} +]); +``` + +【示例三】 + +```json +> db.students.findOne() +{ + name:'张三', + score:[ + {subject:'语文',score:84}, + {subject:'数学',score:90}, + {subject:'外语',score:69} + ] +} + +> db.students.aggregate([{$unwind: '$score'}]) +{name: '张三', score: {subject: '语文', score: 84}} +{name: '张三', score: {subject: '数学', score: 90}} +{name: '张三', score: {subject: '外语', score: 69}} +``` + +#### MQL 特有步骤 `$bucket` + +```json +db.products.aggregate([{ + $bucket:{ + groupBy: "$price", + boundaries: [0,10,20,30,40], + default: "Other", + output:{"count":{$sum:1}} + } +}]) +``` + +#### MQL 特有步骤 $facet + +```json +db.products.aggregate([{ + $facet:{ + price:{ + $bucket:{…} + }, + year:{ + $bucket:{…} + } + } +}]) +``` + +#### 聚合查询实验 + +计算到目前为止的所有订单的总销售额 + +```json +db.orders.aggregate([ + { $group: + { + _id: null, + total: { $sum: "$total" } + } + } +]) + +// 结果: // { "_id" : null, "total" : NumberDecimal("44019609") } +``` + +查询 2019 年第一季度(1 月 1 日~3 月 31 日)已完成订单(completed)的订单总金额和订单总数 + +```json +db.orders.aggregate([ + + // 步骤 1:匹配条件 + { $match: { status: "completed", orderDate: { + $gte: ISODate("2019-01-01"), + $lt: ISODate("2019-04-01") } } }, + + // 步骤二:聚合订单总金额、总运费、总数量 + { $group: { + _id: null, + total: { $sum: "$total" }, + shippingFee: { $sum: "$shippingFee" }, + count: { $sum: 1 } } }, + { $project: { + // 计算总金额 + grandTotal: { $add: ["$total", "$shippingFee"] }, + count: 1, + _id: 0 } } +]) + +// 结果: +// { "count" : 5875, "grandTotal" : NumberDecimal("2636376.00") } +``` + +### 复制集机制及原理 + +#### 复制集的作用 + +MongoDB 复制集的主要意义在于实现服务高可用 + +它的现实依赖于两个方面的功能: + +- 数据写入时将数据迅速复制到另一个独立节点上 +- 在接受写入的节点发生故障时自动选举出一个新的替代节点 + +在实现高可用的同时,复制集实现了其他几个附加作用: + +- 数据分发:将数据从一个区域复制到另一个区域,减少另一个区域的读延迟 + +- 读写分离:不同类型的压力分别在不同的节点上执行 + +- 异地容灾:在数据中心故障时候快速切换到异地 + +#### 典型复制集结构 + +一个典型的复制集由 3 个以上具有投票权的节点组成,包括: + +- 一个主节点(PRIMARY):接受写入操作和选举时投票 +- 两个(或多个)从节点(SECONDARY):复制主节点上的新数据和选举时投票 +- 不推荐使用 Arbiter(投票节点) + +#### 数据是如何复制的? + +当一个修改操作,无论是插入、更新或删除,到达主节点时,它对数据的操作将被 记录下来(经过一些必要的转换),这些记录称为 oplog。 + +从节点通过在主节点上打开一个 tailable 游标不断获取新进入主节点的 oplog,并 在自己的数据上回放,以此保持跟主节点的数据一致。 + +#### 通过选举完成故障恢复 + +- 具有投票权的节点之间两两互相发送心跳; + +- 当 5 次心跳未收到时判断为节点失联; + +- 如果失联的是主节点,从节点会发起选举,选出新的主节点; + +- 如果失联的是从节点则不会产生新的选举; + +- 选举基于 RAFT 一致性算法 实现,选举成功的必要条件是大多数投票节点存活; + +- 复制集中最多可以有 50 个节点,但具有投票权的节点最多 7 个。 + +#### 影响选举的因素 + +整个集群必须有大多数节点(N / 2 + 1)存活; + +被选举为主节点的节点必须: + +- 能够与多数节点建立连接 +- 具有较新的 oplog +- 具有较高的优先级(如果有配置) + +#### 常见选项 + +复制集节点有以下常见的选配项: + +- 是否具有投票权(v 参数):有则参与投票; +- 优先级(priority 参数):优先级越高的节点越优先成为主节点。优先级为 0 的节点无法成 为主节点; +- 隐藏(hidden 参数):复制数据,但对应用不可见。隐藏节点可以具有投票仅,但优先 级必须为 0; +- 延迟(slaveDelay 参数):复制 n 秒之前的数据,保持与主节点的时间差。 + +#### 复制集注意事项 + +关于硬件: + +- 因为正常的复制集节点都有可能成为主节点,它们的地位是一样的,因此硬件配置上必须一致; +- 为了保证节点不会同时宕机,各节点使用的硬件必须具有独立性。 + +关于软件: + +- 复制集各节点软件版本必须一致,以避免出现不可预知的问题。 + +增加节点不会增加系统写性能! + +### MongoDB 全家桶 + +| 软件模块 | 描述 | +| ------------------------- | ----------------------------------------------- | +| mongod | MongoDB 数据库软件 | +| mongo | MongoDB 命令行工具,管理 MongoDB 数据库 | +| mongos | MongoDB 路由进程,分片环境下使用 | +| mongodump / mongorestore | 命令行数据库备份与恢复工具 | +| mongoexport / mongoimport | CSV/JSON 导入与导出,主要用于不同系统间数据迁移 | +| Compass | MongoDB GUI 管理工具 | +| Ops Manager(企业版) | MongoDB 集群管理软件 | +| BI Connector(企业版) | SQL 解释器 / BI 套接件 | +| MongoDB Charts(企业版) | MongoDB 可视化软件 | +| Atlas(付费及免费) | MongoDB 云托管服务,包括永久免费云数据库 | + +## 第二章:从熟练到精通的开发之路 + +### 模型设计基础 + +#### 数据模型 + +什么是数据模型? + +数据模型是一组由符号、文本组成的集合,用以准确表达信息,达到有效交流、沟通 的目的。 + +#### 数据模型设计的元素 + +**实体 Entity** + +- 描述业务的主要数据集合 + +- 谁,什么,何时,何地,为何,如何 + +**属性 Attribute** + +- 描述实体里面的单个信息 + +**关系 Relationship** + +- 描述实体与实体之间的数据规则 + +- 结构规则:1-N, N-1, N-N + +- 引用规则:电话号码不能单独存在 + +数据模型的三层深度: + +- 概念模型,逻辑模型,物理模型 +- 一个模型逐步细化的过程 + +#### MongoDB 文档模型设计的三个误区 + +1. 不需要模型设计 +2. MongoDB 应该用一个超级大文档来组织所有数据 +3. MongoDB 不支持关联或者事务 + +#### 关于 JSON 文档模型设计 + +文档模型设计处于是物理模型设计阶段 (PDM) + +JSON 文档模型通过内嵌数组或引用字段来表示关系 + +文档模型设计不遵从第三范式,允许冗余。 + +#### 为什么人们都说 MongoDB 是无模式? + +MongoDB 同样需要概念/逻辑建模 + +文档模型设计的物理层结构可以和逻辑层类似 + +MongoDB 无模式由来: 可以省略物理建模的具体过程 + +#### 关系模型 vs 文档模型 + +| | 关系数据库 | JSON 文档模型 | +| ------------ | ---------------------------------- | --------------------- | +| 模型设计层次 | 概念模型
    逻辑模型
    物理模型 | 概念模型
    逻辑模型 | +| 模型实体 | 表 | 集合 | +| 模型属性 | 列 | 字段 | +| 模型关系 | 关联关系,主外键 | 内嵌数组,引用字段 | + +### 文档模型设计之一:基础设计 + +建立基础文档模型 + +- 根据概念模型或者业务需求推导出逻辑模型 – 找到对象 +- 列出实体之间的关系(及基数) - 明确关系 +- 套用逻辑设计原则来决定内嵌方式 – 进行建模 +- 完成基础模型构建 + +基础建模小结 + +- 90:10 规则: 大部分时候你会使用内嵌来表示 1-1,1-N,N-N +- 内嵌类似于预先聚合(关联) +- 内嵌后对读操作通常有优势(减少关联) + +### 文档模型设计之二:工况细化 + +场景梳理: + +- 最频繁的数据查询模式 + +- 最常用的查询参数 + +- 最频繁的数据写入模式 + +- 读写操作的比例 + +- 数据量的大小 + +基于内嵌的文档模型 + +根据业务需求 + +- 使用引用来避免性能瓶颈 +- 使用冗余来优化访问性能 + +什么时候该使用引用方式? + +- 内嵌文档太大,数 MB 或者超过 16MB +- 内嵌文档或数组元素会频繁修改 +- 内嵌数组元素会持续增长并且没有封顶 + +MongoDB 引用设计的限制 + +- MongoDB 对使用引用的集合之间并无主外键检查 +- MongoDB 使用聚合框架的 `$lookup` 来模仿关联查询 +- `$lookup` 只支持 left outer join +- `$lookup` 的关联目标(from)不能是分片表 + +### 文档模型设计之三:模式套用 + +- 利用文档内嵌数组,将一个时间段的数据聚合到一个文档里。 + +- 大量减少文档数量 + +- 大量减少索引占用空间 + +### 设计模式集锦 + +大文档,很多字段,很多索引 + +列转行 + +模型灵活了,如何管理文档不同版本? + +- 增加一个版本号字段 +- 快速过滤掉不需要升级的文档 +- 升级时候对不同版本的文档做 不同的处理 + +统计网页点击流量 + +- 用近似计算 + +业绩排名,游戏排名,商品统计等精确统计 + +- 用预聚合字段 +- 模型中直接增加统计字段 +- 每次更新数据时候同时更新统计值 + +### 事务开发:写操作事务 + +writeConcern 决定一个写操作落到多少个节点上才算成功。writeConcern 的取值包括: + +- 0:发起写操作,不关心是否成功; + +- 1~集群最大数据节点数:写操作需要被复制到指定节点数才算成功; + +- majority:写操作需要被复制到大多数节点上才算成功。 + +发起写操作的程序将阻塞到写操作到达指定的节点数为止。 + +writeConcern 可以决定写操作到达多少个节点才算成功,journal 则定义如何才算成 功。取值包括: + +- true: 写操作落到 journal 文件中才算成功; +- false: 写操作到达内存即算作成功。 + +### 事务开发:读操作事务之一 + +在读取数据的过程中我们需要关注以下两个问题: + +从哪里读?——由 readPreference 来解决 + +什么样的数据可以读? ——由 readConcern 来解决 + +#### 什么是 readPreference? + +readPreference 决定使用哪一个节点来满足 正在发起的读请求。可选值包括: + +- primary: 只选择主节点; +- primaryPreferred:优先选择主节点,如 果不可用则选择从节点; +- secondary:只选择从节点; +- secondaryPreferred:优先选择从节点, 如果从节点不可用则选择主节点; +- nearest:选择最近的节点; + +#### readPreference 场景举例 + +用户下订单后马上将用户转到订单详情页——primary/primaryPreferred。因为此时从节点可能还没复制到新订单; + +用户查询自己下过的订单——secondary/secondaryPreferred。查询历史订单对时效性通常没有太高要求; + +生成报表——secondary。报表对时效性要求不高,但资源需求大,可以在从节点单独处理,避免对线上用户造成影响; + +将用户上传的图片分发到全世界,让各地用户能够就近读取——nearest。每个地区的应用选择最近的节点读取数据。 + +#### readPreference 与 Tag + +readPreference 只能控制使用一类节点。Tag 则可以将节点选择控制 +到一个或几个节点。考虑以下场景: + +- 一个 5 个节点的复制集; + +- 3 个节点硬件较好,专用于服务线上客户; + +- 2 个节点硬件较差,专用于生成报表; + +可以使用 Tag 来达到这样的控制目的: + +- 为 3 个较好的节点打上 {purpose: "online"}; + +- 为 2 个较差的节点打上 {purpose: "analyse"}; + +- 在线应用读取时指定 online,报表读取时指定 reporting。 + +#### 注意事项 + +- 指定 readPreference 时也应注意高可用问题。例如将 readPreference 指定 primary,则发生故障转移不存在 primary 期间将没有节点可读。如果业务允许,则应选择 primaryPreferred; + +- 使用 Tag 时也会遇到同样的问题,如果只有一个节点拥有一个特定 Tag,则在这个节点失效时将无节点可读。这在有时候是期望的结果,有时候不是。例如: + - 如果报表使用的节点失效,即使不生成报表,通常也不希望将报表负载转移到其他节点上,此时只有一个节点有报表 Tag 是合理的选择; + - 如果线上节点失效,通常希望有替代节点,所以应该保持多个节点有同样的 Tag; +- Tag 有时需要与优先级、选举权综合考虑。例如做报表的节点通常不会希望它成为主节点,则优先级应为 0。 + +### 事务开发:读操作事务之二 + +#### 什么是 readConcern? + +在 readPreference 选择了指定的节点后,readConcern 决定这个节点上的数据哪些 是可读的,类似于关系数据库的隔离级别。可选值包括: + +- available:读取所有可用的数据; +- local:读取所有可用且属于当前分片的数据,默认设置; +- majority:读取在大多数节点上提交完成的数据; +- linearizable:可线性化读取文档;增强处理 majority 情况下主节点失联时候的例外情况 +- snapshot:读取最近快照中的数据;最高隔离级别,接近于 Seriazable + +#### readConcern: local 和 available + +在复制集中 local 和 available 是没有区别的。两者的区别主要体现在分片集上。考虑以下场景: + +- 一个 chunk x 正在从 shard1 向 shard2 迁移; + +- 整个迁移过程中 chunk x 中的部分数据会在 shard1 和 shard2 中同时存在,但源分片 shard1 仍然是 chunk x 的负责方: + - 所有对 chunk x 的读写操作仍然进入 shard1; + - config 中记录的信息 chunk x 仍然属于 shard1; +- 此时如果读 shard2,则会体现出 local 和 available 的区别: + - local:只取应该由 shard2 负责的数据(不包括 x); + - available:shard2 上有什么就读什么(包括 x); + +注意事项: + +- 虽然看上去总是应该选择 local,但毕竟对结果集进行过滤会造成额外消耗。在一些 无关紧要的场景(例如统计)下,也可以考虑 available; +- `MongoDB <=3.6` 不支持对从节点使用 {readConcern: "local"}; +- 从主节点读取数据时默认 readConcern 是 local,从从节点读取数据时默认 readConcern 是 available(向前兼容原因)。 + +#### readConcern: majority + +只读取大多数据节点上都提交了的数据。考虑如 下场景: + +- 集合中原有文档 {x: 0}; +- 将 x 值更新为 1; + +如果在各节点上应用 {readConcern: “majority”} 来读取数据: + +如何实现? + +节点上维护多个 x 版本,MVCC 机制 MongoDB 通过维护多个快照来链接不同的版本: + +- 每个被大多数节点确认过的版本都将是一个快照; +- 快照持续到没有人使用为止才被删除; + +#### readConcern: majority 与脏读 + +MongoDB 中的回滚: + +- 写操作到达大多数节点之前都是不安全的,一旦主节点崩溃,而从节还没复制到该次操作,刚才的写操作就丢失了; + +- 把一次写操作视为一个事务,从事务的角度,可以认为事务被回滚了。 + +所以从分布式系统的角度来看,事务的提交被提升到了分布式集群的多个节点级别的“提交”,而不再是单个节点上的“提交”。 + +在可能发生回滚的前提下考虑脏读问题: + +- 如果在一次写操作到达大多数节点前读取了这个写操作,然后因为系统故障该操作回滚了,则发生了脏读问题; + +使用 {readConcern: “majority”} 可以有效避免脏读 + +#### readConcern: 如何实现安全的读写分离 + +考虑如下场景: + +向主节点写入一条数据; + +立即从从节点读取这条数据。 + +如何保证自己能够读到刚刚写入的数据? + +下述方式有可能读不到刚写入的订单 + +```json +db.orders.insert({ oid: 101, sku: ”kite", q: 1}) +db.orders.find({oid:101}).readPref("secondary") +``` + +使用 writeConcern + readConcern majority 来解决 + +```json +db.orders.insert({ oid: 101, sku: "kiteboar", q: 1}, {writeConcern:{w: "majority”}}) +db.orders.find({oid:101}).readPref(“secondary”).readConcern("majority") +``` + +#### readConcern: linearizable + +只读取大多数节点确认过的数据。和 majority 最大差别是保证绝对的操作线性顺序 –在写操作自然时间后面的发生的读,一定可以读到之前的写 + +- 只对读取单个文档时有效; +- 可能导致非常慢的读,因此总是建议配合使用 maxTimeMS; + +#### readConcern: snapshot + +{readConcern: “snapshot”} 只在多文档事务中生效。将一个事务的 readConcern 设置为 snapshot,将保证在事务中的读: + +- 不出现脏读; + +- 不出现不可重复读; + +- 不出现幻读。 + +因为所有的读都将使用同一个快照,直到事务提交为止该快照才被释放。 + +### 事务开发:多文档事务 + +MongoDB 虽然已经在 4.2 开始全面支持了多文档事务,但并不代表大家应该毫无节制 地使用它。相反,对事务的使用原则应该是:能不用尽量不用。 + +通过合理地设计文档模型,可以规避绝大部分使用事务的必要性 + +为什么?事务 = 锁,节点协调,额外开销,性能影响 + +MongoDB ACID 多文档事务支持 + +- Atomocity 原子性 + - 单表单文档 : 1.x 就支持 + - 复制集多表多行:4.0 复制集 + - 分片集群多表多行 4.2 +- Consistency 一致性 - writeConcern, readConcern (3.2) +- Isolation 隔离性 - readConcern (3.2) +- Durability 持久性 - Journal and Replication + +#### 事务的隔离级别 + +事务完成前,事务外的操作对该事务所做的修改不可访问 + +如果事务内使用 {readConcern: “snapshot”},则可以达到可重复读 Repeatable Read + +#### 事务写机制 + +MongoDB 的事务错误处理机制不同于关系数据库: + +- 当一个事务开始后,如果事务要修改的文档在事务外部被修改过,则事务修改这个文档时会触发 Abort 错误,因为此时的修改冲突了; + +- 这种情况下,只需要简单地重做事务就可以了; + +- 如果一个事务已经开始修改一个文档,在事务以外尝试修改同一个文档,则事务以外的修改会等待事务完成才能继续进行(write-wait.md 实验)。 + +### Change Stream + +Change Stream 是 MongoDB 用于实现变更追踪的解决方案,类似于关系数据库的触 发器,但原理不完全相同: + +| | Change Stream | 触发器 | +| -------- | -------------------- | ---------------- | +| 触发方式 | 异步 | 同步(事务保证) | +| 触发位置 | 应用回调事件 | 数据库触发器 | +| 触发次数 | 每个订阅事件的客户端 | 1 次(触发器) | +| 故障恢复 | 从上次断点重新触发 | 事务回滚 | + +#### Change Stream 的实现原理 + +Change Stream 是基于 oplog 实现的。它在 oplog 上开启一个 tailable cursor 来追踪所有复制集上的变更操作,最终调用应用中定义的回调函数。 + +被追踪的变更事件主要包括: + +- insert/update/delete:插入、更新、删除; + +- drop:集合被删除; + +- rename:集合被重命名; + +- dropDatabase:数据库被删除; + +- invalidate:drop/rename/dropDatabase 将导致 invalidate 被触发,并关闭 change stream; + +Change Stream 只推送已经在大多数节点上提交的变更操作。即“可重复读”的变更。 这个验证是通过 {readConcern: “majority”} 实现的。因此: + +- 未开启 majority readConcern 的集群无法使用 Change Stream; +- 当集群无法满足 {w: “majority”} 时,不会触发 Change Stream(例如 PSA 架构 中的 S 因故障宕机)。 + +#### Change Stream 使用场景 + +- 跨集群的变更复制——在源集群中订阅 Change Stream,一旦得到任何变更立即写入目标集群。 + +- 微服务联动——当一个微服务变更数据库时,其他微服务得到通知并做出相应的变更。 + +- 其他任何需要系统联动的场景。 + +#### 注意事项 + +- Change Stream 依赖于 oplog,因此中断时间不可超过 oplog 回收的最大时间窗; + +- 在执行 update 操作时,如果只更新了部分数据,那么 Change Stream 通知的也 + 是增量部分; +- 同理,删除数据时通知的仅是删除数据的 `_id`。 + +## 参考资料 + +- [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MongoDB\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260\344\272\214.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MongoDB\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260\344\272\214.md" new file mode 100644 index 0000000000..0b456bafca --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MongoDB\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260\344\272\214.md" @@ -0,0 +1,312 @@ +--- +title: 《极客时间教程 - MongoDB 高手课》笔记二 +date: 2024-10-17 07:19:53 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 文档数据库 + - MongoDB +permalink: /pages/463dcaa5/ +--- + +# 《极客时间教程 - MongoDB 高手课》笔记二 + +## 第三章:分片集群与高级运维之道 + +### 分片集群机制及原理 + +#### MongoDB 常见部署架构 + +TODO: 补图 + +为什么要使用分片集群? + +——分而治之 + +分片如何解决? + +TODO:补图 + +分片组件: + +- 路由节点(mongos) - 提供集群单一入口转发应用端请求选择合适数据节点进行读写合并多个数据节点的返回。无状态,建议至少 2 个。 +- 配置节点(config) - 提供集群元数据存储分片数据分布的映射。 +- 数据节点(shard) - 以复制集为单位水平扩展,最大 1024 分片。分片之间数据不重复所有分片在一起才可完整工作 + +分片特点: + +- 应用全透明,无特殊处理 +- 数据自动均衡 +- 动态扩容,无须下线 +- 提供三种分片方式 + - 基于范围 + - 优点:范围查询性能好 + - 缺点:数据分布不均;容易出现热点问题 + - 基于 Hash + - 优点:数据分布均匀 + - 缺点:范围查询效率低 + - 基于 zone / tag + +### 分片集群设计 + +分片的基本标准: + +- 关于数据:数据量不超过 3TB,尽可能保持在 2TB 一个片; +- 关于索引:常用索引必须容纳进内存; + +按照以上标准初步确定分片后,还需要考虑业务压力,随着压力增大,CPU、RAM、磁盘中的任何一项出现瓶颈时,都可以通过添加更多分片来解决。 + +合理的架构–需要多少个分片 + +- A = 所需存储总量 / 单服务器可挂载容量。如:8TB / 2TB = 4 +- B = 工作集大小 / 单服务器内存容量。如:400GB / (256G \* 0.6)= 3 +- C = 并发量总数 / (单服务器并发量 * 0.7)。如:30000 / (9000*0.7) = 6 +- 分片数量= max(A, B, C) = 6 + +关键概念 + +- 片键 shard key:文档中的一个字段 +- 文档 doc :包含 shard key 的一行数据 +- 块 Chunk :包含 n 个文档 +- 分片 Shard:包含 n 个 chunk +- 集群 Cluster:包含 n 个分片 + +选择合适片键 + +- 取值基数(Cardinality) - 取值基数要大,因为备选值有限,不利于水平扩展 +- 取值分布 - 应尽可能均匀,以避免热点问题 +- 分散写,集中读 +- 被尽可能多的业务场景用到 +- 避免单调递增或递减的片键 + +足够的资源 + +mongos 与 config 通常消耗很少的资源,可以选择低规格虚拟机; + +资源的重点在于 shard 服务器: + +- 需要足以容纳热数据索引的内存; +- 正确创建索引后 CPU 通常不会成为瓶颈,除非涉及非常多的计算; +- 磁盘尽量选用 SSD; + +### 实验:分片集群搭建及扩容(略) + +### MongoDB 监控最佳实践 + +常用的监控工具及手段 + +- MongoDB Ops Manager +- Percona +- 通用监控平台 +- 程序脚本 + +监控信息的来源: + +- db.serverStatus()(主要) +- db.isMaster()(次要) +- mongostats 命令行工具(只有部分信息) + +注意:db.serverStatus() 包含的监控信息是从上次开机到现在为止的累计数据,因此不能简单使用。 + +serverStatus() 主要信息 + +- connections: 关于连接数的信息; +- locks: 关于 MongoDB 使用的锁情况; +- network: 网络使用情况统计; +- opcounters: CRUD 的执行次数统计; +- repl: 复制集配置信息; +- wiredTiger: 包含大量 WirdTiger 执行情况的信息: + - block-manager: WT 数据块的读写情况; + - session: session 使用数量; + - concurrentTransactions: Ticket 使用情况; +- mem: 内存使用情况; +- metrics: 一系列性能指标统计信息; + +监控报警的考量 + +- 具备一定的容错机制以减少误报的发生; +- 总结应用各指标峰值; +- 适时调整报警阈值; +- 留出足够的处理时间; + +建议监控指标 + +| 指标 | 意义 | 获取 | +| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| opcounters(操作计数器) | 查询、更新、插入、删除、getmore 和其他命令的的数量。 | `db.serverStatus().opcounters` | +| tickets(令牌) | 对 WiredTiger 存储引擎的读/写令牌数量。令牌数量表示了可以进入存储引擎的并发操作数量。 | `db.serverStatus().wiredTiger.concurrentTransactions` | +| replication lag(复制延迟) | 这个指标代表了写操作到达从结点所需要的最小时间。过高的 replication lag 会减小从结点的价值并且不利于配置了写关 | `db.adminCommand({'replSetGetStatus': 1})` | +| oplog window(复制时间窗) | 这个指标代表 oplog 可以容纳多长时间的写操作。它表示了一个从结点可以离线多长时间仍能够追上主节点。通常建议该值应大于 24 小时为佳。 | `db.oplog.rs.find().sort({$natural: -1}).limit(1).next().ts -db.oplog.rs.find().sort({$natural: 1}).limit(1).next().ts` | +| connections(连接数) | 连接数应作为监控指标的一部分,因为每个连接都将消耗资源。应该计算低峰/正常/高峰时间的连接数,并制定合理的报警阈值范围。 | `db.serverStatus().connections` | +| Query targeting(查询专注度) | 索引键/文档扫描数量比返回的文档数量,按秒平均。如果该值比较高表示查询系需要进行很多低效的扫描来满足查询。这个情况通常代表了索引不当或缺少索引来支持查询。 | `var status = db.serverStatus()status.metrics.queryExecutor.scanned / status.metrics.document.returnedstatus.metrics.queryExecutor.scannedObjects / status.metrics.document.returned` | +| Scan and Order(扫描和排序) | 每秒内内存排序操作所占的平均比例。内存排序可能会十分昂贵,因为它们通常要求缓冲大量数据。如果有适当索引的情况下,内存排序是可以避免的。 | `var status = db.serverStatus()status.metrics.operation.scanAndOrder / status.opcounters.query` | +| 节点状态 | 每个节点的运行状态。如果节点状态不是 PRIMARY、SECONDARY、ARBITER 中的一个,或无法执行上述命令则报警 | `db.runCommand("isMaster")` | +| dataSize(数据大小) | 整个实例数据总量(压缩前) | 每个 DB 执行 db.stats(); | +| StorageSize(磁盘空间大小) | 已使用的磁盘空间占总空间的百分比。 | | + +### MongoDB 备份与恢复 + +MongoDB 的备份机制分为: + +- 延迟节点备份 +- 全量备份 + Oplog 增量 + +最常见的全量备份方式包括: + +- mongodump; +- 复制数据文件; +- 文件系统快照; + +#### 方案一:延迟节点备份 + +安全范围内的任意时间点状态 = 延迟从节点当前状态 + 定量重放 oplog + +主节点的 oplog 时间窗 t 应满足:t >= 延迟时间 + 48 小时 + +#### 方案二:全量备份加 oplog + +- 最近的 oplog 已经在 oplog.rs 集合中,因此可以在定期从集合中导出便得到了 oplog; +- 如果主节点上的 oplog.rs 集合足够大,全量备份足够密集,自然也可以不用备份 oplog; +- 只要有覆盖整个时间段的 oplog,就可以结合全量备份得到任意时间点的备份。 + +全量备份(mongodump、复制数据文件、文件系统快照) + oplog = 任意时间点备份恢复 (PIT) + +复制文件全量备份注意事项 + +- 必须先关闭节点才能复制,否则复制到的文件无效; +- 也可以选择 db.fsyncLock() 锁定节点,但完成后不要忘记 db.fsyncUnlock() 解锁; +- 可以且应该在从节点上完成; +- 该方法实际上会暂时宕机一个从节点,所以整个过程中应注意投票节点总数。 + +全量备份加 oplog 注意事项–文件系统快照 + +- MongoDB 支持使用文件系统快照直接获取数据文件在某一时刻的镜像; +- 快照过程中可以不用停机; +- 数据文件和 Journal 必须在同一个卷上; +- 快照完成后请尽快复制文件并删除快照; + +Mongodump 全量备份注意事项 + +- 使用 mongodump 备份最灵活,但速度上也是最慢的; +- mongodump 出来的数据不能表示某个个时间点,只是某个时间段 + +### 备份与恢复操作(略) + +### MongoDB 安全架构(略) + +### MongoDB 安全加固实践(略) + +### MongoDB 索引机制(一) + +MongoDB 索引数据结构为 B-树。 + +B-树:基于 B 树,但是子节点数量可以超过 2 个。 + +MongoDB 索引类型 + +- 单键索引 +- 组合索引 +- 多值索引 +- 地理位置索引 +- 全文索引 +- TTL 索引 +- 部分索引 +- 哈希索引 + +组合索引的最佳方式:ESR 原则 + +- 精确(Equal)匹配的字段放最前面 +- 排序(Sort)条件放中间 +- 范围(Range)匹配的字段放最后面 + +### MongoDB 索引机制(二) + +### MongoDB 读写性能机制 + +客户端请求流程图 + +TODO:补图 + +#### 应用端-选择节点 + +对于复制集读操作,选择哪个节点是由 readPreference 决定的: + +- primary/primaryPreferred +- secondary/secondaryPreferred +- nearest + +如果不希望一个远距离节点被选择,应做到以下之一 + +- 将它设置为隐藏节点; +- 通过标签(Tag)控制可选的节点; +- 使用 nearest 方式; + +#### 数据库端-执行请求(读) + +不能命中索引的搜索和内存排序是导致性能问题的最主要原因 + +#### 数据库端-执行请求(写) + +#### 数据库端-合并结果 + +- 如果顺序不重要则不要排序 +- 尽可能使用带片键的查询条件以减少参与查询的分片数 + +### 性能诊断工具 + +mongostat: 用于了解 MongoDB 运行状态的工具 + +mongotop: 用于了解集合压力状态的工具 + +mongod 日志:日志中会记录执行超过 100ms 的查询及其执行计划 + +### 高级集群设计:两地三中心 + +#### 容灾级别 + +- **无备源中心** - 没有灾难恢复能力,只在本地进行数据备份。 +- **本地备份+异地保存** - 本地将关键数据备份,然后送到异地保存。灾难发生后,按预定数据恢复程序恢复系统和数据。 +- **双中心主备模式** - 在异地建立一个热备份点,通过网络进行数据备份。当出现灾难时,备份站点接替主站点的业务,维护业务连续性。 +- **双中心双活** - 在相隔较远的地方分别建立两个数据中心,进行相互数据备份。当某个数据中心发生灾难时,另一个数据中心接替其工作任务。 +- **双中心双活 +异地热备 =两地三中心** - 在同城分别建立两个数据中心,进行相互数据备份。当该城市的 2 个中心同时不可用(地震/大面积停电/网络等),快速切换到异地 + +网络层解决方案 + +TODO:补图 + +应用层解决方案 + +- 负载均衡、虚拟 IP +- 分布式 Session +- 使用同一套数据 + +数据跨中心同步 + +- DBMS 跨机房基于日志同步 +- 文件系统跨机房基于存储镜像同步 + +多数据中心要点: + +- 正常运行状态 - 集群内一个主节点接受写,其他节点只读。 +- 主节点故障 - 主数据中心内自动切主切换时间 5-10 秒 +- 主数据中心对外网络故障或者整个数据中心不可用,主数据中心主节点自动降级。从节点升级为主节点选举时间 5-30 秒。 +- 双中心双活,分流模式 -需要跨中心写数据,同城双中心需要低延迟专线。 +- 节点数量建议要 5 个,2+2+1 模式 +- 主数据中心的两个节点要设置高一点的优先级,减少跨中心换主节点 +- 同城双中心之间的网络要保证低延迟和频宽,满足 writeConcern: Majority 的双中心写需求 +- 使用 Retryable Writes and Retryable Reads 来保证零下线时间 +- 用户需要自行处理好业务层的双中心切换 + +### 实验:搭建两地三中心集群(略) + +### 高级集群设计:全球多写(略) + +### MongoDB 上线及升级(略) + +## 第四章:企业架构师进阶之法(略) + +## 参考资料 + +- [MongoDB 高手课](https://time.geekbang.org/course/intro/100040001) diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/03.MySQL\345\256\236\346\210\23045\350\256\262.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MySQL\345\256\236\346\210\23045\350\256\262.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/03.MySQL\345\256\236\346\210\23045\350\256\262.md" rename to "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MySQL\345\256\236\346\210\23045\350\256\262.md" index a7be6cd34b..05ec58e745 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/03.MySQL\345\256\236\346\210\23045\350\256\262.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-MySQL\345\256\236\346\210\23045\350\256\262.md" @@ -1,14 +1,13 @@ --- title: 《MySQL 实战 45 讲》笔记 date: 2022-07-20 19:20:08 -order: 03 categories: - 笔记 - 数据库 tags: - 数据库 - Mysql -permalink: /pages/1ee347/ +permalink: /pages/b58ace19/ --- # 《MySQL 实战 45 讲》笔记 @@ -1404,4 +1403,4 @@ flush privileges 语句本身会用数据表的数据重建一份内存权限数 ## 参考资料 -- [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139) \ No newline at end of file +- [MySQL 实战 45 讲](https://time.geekbang.org/column/intro/139) diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/02.SQL\345\277\205\347\237\245\345\277\205\344\274\232.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-SQL\345\277\205\347\237\245\345\277\205\344\274\232.md" similarity index 81% rename from "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/02.SQL\345\277\205\347\237\245\345\277\205\344\274\232.md" rename to "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-SQL\345\277\205\347\237\245\345\277\205\344\274\232.md" index 78db02ba4d..2375c90b23 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/02.SQL\345\277\205\347\237\245\345\277\205\344\274\232.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-SQL\345\277\205\347\237\245\345\277\205\344\274\232.md" @@ -1,30 +1,27 @@ --- -title: 《SQL 必知必会》笔记 +title: 《极客时间教程 - SQL 必知必会》笔记 date: 2022-07-16 10:46:05 -order: 02 categories: - 笔记 - 数据库 tags: - 数据库 - 关系型数据库 -permalink: /pages/34699b/ +permalink: /pages/fbff611f/ --- -# 《SQL 必知必会》笔记 +# 《极客时间教程 - SQL 必知必会》笔记 -## 第一章:SQL 语法基础篇 - -### 01 丨了解 SQL:一门半衰期很长的语言 +## 01 丨了解 SQL:一门半衰期很长的语言 SQL 语言按照功能划分成以下的 4 个部分: -- **DDL**,英文叫做 Data Definition Language,也就是数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用 DDL,我们可以创建,删除和修改数据库和表结构。 -- **DML**,英文叫做 Data Manipulation Language,数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。 -- **DCL**,英文叫做 Data Control Language,数据控制语言,我们用它来定义访问权限和安全级别。 -- **DQL**,英文叫做 Data Query Language,数据查询语言,我们用它查询想要的记录,它是 SQL 语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。 +- **DDL** 是 Data Definition Language 的缩写,即数据定义语言,它用来定义我们的数据库对象,包括数据库、数据表和列。通过使用 DDL,我们可以创建,删除和修改数据库和表结构。 +- **DML** 是 Data Manipulation Language 的缩写,即数据操作语言,我们用它操作和数据库相关的记录,比如增加、删除、修改数据表中的记录。 +- **DCL** 是 Data Control Language 的缩写,即数据控制语言,我们用它来定义访问权限和安全级别。 +- **DQL** 是 Data Query Language 的缩写,即数据查询语言,我们用它查询想要的记录,它是 SQL 语言的重中之重。在实际的业务中,我们绝大多数情况下都是在和查询打交道,因此学会编写正确且高效的查询语句,是学习的重点。 -### 02 丨 DBMS 的前世今生 +## 02 丨 DBMS 的前世今生 DB、DBS 和 DBMS 的区别: @@ -40,9 +37,9 @@ NoSql 不同时期的释义 - 2005:NoSQL = Not only SQL - 2013:NoSQL = No, SQL! -### 03 丨学会用数据库的方式思考 SQL 是如何执行的 +## 03 丨学会用数据库的方式思考 SQL 是如何执行的 -#### Oracle 中的 SQL 是如何执行的 +### Oracle 中的 SQL 是如何执行的 ![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220716105947.png) @@ -55,11 +52,9 @@ NoSql 不同时期的释义 5. **优化器**:优化器中就是要进行硬解析,也就是决定怎么做,比如创建解析树,生成执行计划。 6. **执行器**:当有了解析树和执行计划之后,就知道了 SQL 该怎么被执行,这样就可以在执行器中执行语句了。 -共享池是 Oracle 中的术语,包括了库缓存,数据字典缓冲区等。它主要缓存 SQL 语句和执行计划。 - -而数据字典缓冲区存储的是 Oracle 中的对象定义,比如表、视图、索引等对象。当对 SQL 语句进行解析的时候,如果需要相关的数据,会从数据字典缓冲区中提取。 +共享池是 Oracle 中的术语,包括了库缓存,数据字典缓冲区等。它主要缓存 SQL 语句和执行计划。而数据字典缓冲区存储的是 Oracle 中的对象定义,比如表、视图、索引等对象。当对 SQL 语句进行解析的时候,如果需要相关的数据,会从数据字典缓冲区中提取。 -#### MySQL 中的 SQL 是如何执行的 +### MySQL 中的 SQL 是如何执行的 MySQL 是典型的 C/S 架构,即 Client/Server 架构,服务器端程序使用的 mysqld。 @@ -88,18 +83,20 @@ SQL 层的结构 4. NDB 存储引擎:也叫做 NDB Cluster 存储引擎,主要用于 MySQL Cluster 分布式集群环境,类似于 Oracle 的 RAC 集群。 5. Archive 存储引擎:它有很好的压缩机制,用于文件归档,在请求写入时会进行压缩,所以也经常用来做仓库。 -### 04 丨使用 DDL 创建数据库&数据表时需要注意什么? +## 04 丨使用 DDL 创建数据库&数据表时需要注意什么? DDL 的核心指令是 `CREATE`、`ALTER`、`DROP`。 +执行 DDL 的时候,不需要 COMMIT,就可以完成执行任务。 + 设计数据表的原则 - **数据表的个数越少越好** - RDBMS 的核心在于对实体和联系的定义,也就是 E-R 图(Entity Relationship Diagram),数据表越少,证明实体和联系设计得越简洁,既方便理解又方便操作。 - **数据表中的字段个数越少越好** - 字段个数越多,数据冗余的可能性越大。设置字段个数少的前提是各个字段相互独立,而不是某个字段的取值可以由其他字段计算出来。当然字段个数少是相对的,我们通常会在数据冗余和检索效率中进行平衡。 - **数据表中联合主键的字段个数越少越好** - 设置主键是为了确定唯一性,当一个字段无法确定唯一性的时候,就需要采用联合主键的方式(也就是用多个字段来定义一个主键)。联合主键中的字段越多,占用的索引空间越大,不仅会加大理解难度,还会增加运行时间和索引空间,因此联合主键的字段个数越少越好。 -- **使用主键和外键越多越好** - 数据库的设计实际上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高。这样做的好处在于不仅保证了数据表之间的独立性,还能提升相互之间的关联使用率。——不同意 +- ~~**使用主键和外键越多越好** - 数据库的设计实际上就是定义各种表,以及各种字段之间的关系。这些关系越多,证明这些实体之间的冗余度越低,利用度越高。这样做的好处在于不仅保证了数据表之间的独立性,还能提升相互之间的关联使用率~~。——不同意 -### 05 丨检索数据:你还在 SELECT 么? +## 05 丨检索数据:你还在 SELECT 么? SELECT 的作用是从一个表或多个表中检索出想要的数据行。 @@ -109,6 +106,8 @@ SELECT 的作用是从一个表或多个表中检索出想要的数据行。 - `ASC` :升序(默认) - `DESC` :降序 +### SELECT 查询的基础语法 + 查询单列 ```sql @@ -127,13 +126,13 @@ SELECT name, continent, region FROM world.country; SELECT * FROM world.country; ``` -查询不同的值 +查询过滤重复值 ```sql SELECT distinct(continent) FROM world.country; ``` -限制查询结果 +限制查询数量 ```sql -- 返回前 5 行 @@ -143,9 +142,35 @@ SELECT * FROM world.country LIMIT 0, 5; SELECT * FROM world.country LIMIT 2, 3; ``` -### 06 丨数据过滤:SQL 数据过滤都有哪些方法? +### SELECT 的执行顺序 + +关键字的顺序是不能颠倒的: + +```sql +SELECT ... FROM ... WHERE ... GROUP BY ... HAVING ... ORDER BY ... +``` + +SELECT 语句的执行顺序(在 MySQL 和 Oracle 中,SELECT 执行顺序基本相同): -#### 比较操作符 +```sql +FROM > WHERE > GROUP BY > HAVING > SELECT 的字段 > DISTINCT > ORDER BY > LIMIT +``` + +比如你写了一个 SQL 语句,那么它的关键字顺序和执行顺序是下面这样的: + +```sql +SELECT DISTINCT player_id, player_name, count(*) as num -- 顺序 5 +FROM player JOIN team ON player.team_id = team.team_id -- 顺序 1 +WHERE height > 1.80 -- 顺序 2 +GROUP BY player.team_id -- 顺序 3 +HAVING num > 2 -- 顺序 4 +ORDER BY num DESC -- 顺序 6 +LIMIT 2 -- 顺序 7 +``` + +## 06 丨数据过滤:SQL 数据过滤都有哪些方法? + +### 比较操作符 | 运算符 | 描述 | | ------ | ------------------------------------------------------ | @@ -156,14 +181,14 @@ SELECT * FROM world.country LIMIT 2, 3; | `>=` | 大于等于 | | `<=` | 小于等于 | -#### 范围操作符 +### 范围操作符 | 运算符 | 描述 | | --------- | -------------------------- | | `BETWEEN` | 在某个范围内 | | `IN` | 指定针对某个列的多个可能值 | -#### 逻辑操作符 +### 逻辑操作符 | 运算符 | 描述 | | ------ | ---------- | @@ -171,7 +196,7 @@ SELECT * FROM world.country LIMIT 2, 3; | `OR` | 或者(或) | | `NOT` | 否定(非) | -#### 通配符 +### 通配符 | 运算符 | 描述 | | ------ | -------------------------- | @@ -180,7 +205,7 @@ SELECT * FROM world.country LIMIT 2, 3; | `_` | 表示任意字符出现一次 | | `[]` | 必须匹配指定位置的一个字符 | -### 07 丨什么是 SQL 函数?为什么使用 SQL 函数可能会带来问题? +## 07 丨什么是 SQL 函数?为什么使用 SQL 函数可能会带来问题? - 数学函数 - 字符串函数 @@ -188,7 +213,7 @@ SELECT * FROM world.country LIMIT 2, 3; - 转换函数 - 聚合函数 -### 08 丨什么是 SQL 的聚集函数,如何利用它们汇总表的数据? +## 08 丨什么是 SQL 的聚集函数,如何利用它们汇总表的数据? 聚合函数 @@ -200,11 +225,23 @@ SELECT * FROM world.country LIMIT 2, 3; | `MIN()` | 返回某列的最小值 | | `SUM()` | 返回某列值之和 | -### 09 丨子查询:子查询的种类都有哪些,如何提高子查询的性能? +## 09 丨子查询:子查询的种类都有哪些,如何提高子查询的性能? + +子查询可以分为关联子查询和非关联子查询。 + +子查询从数据表中查询了数据结果,如果这个数据结果只执行一次,然后这个数据结果作为主查询的条件进行执行,那么这样的子查询叫做非关联子查询。 + +如果子查询需要执行多次,即采用循环的方式,先从外部查询开始,每次都传入子查询进行查询,然后再将结果反馈给外部,这种嵌套的执行方式就称为关联子查询。 + +子查询关键词:EXISTS、IN、ANY、ALL、SOME -EXISTS、IN、ANY、ALL、SOME +如果表 A 比表 B 大,那么 IN 子查询的效率要比 EXIST 子查询效率高,因为这时 B 表中如果对 cc 列进行了索引,那么 IN 子查询的效率就会比较高。 -### 10 丨常用的 SQL 标准有哪些,在 SQL92 中是如何使用连接的? +ANY 和 ALL 都需要使用比较符,比较符包括了(>)(=)(<)(>=)(<=)和(<>)等。 + +子查询可以作为主查询的列 + +## 10 丨常用的 SQL 标准有哪些,在 SQL92 中是如何使用连接的? 内连接(INNER JOIN) @@ -218,11 +255,11 @@ EXISTS、IN、ANY、ALL、SOME 右连接(RIGHT JOIN) -### 11 丨 SQL99 是如何使用连接的,与 SQL92 的区别是什么? +## 11 丨 SQL99 是如何使用连接的,与 SQL92 的区别是什么? -### 12 丨视图在 SQL 中的作用是什么,它是怎样工作的? +## 12 丨视图在 SQL 中的作用是什么,它是怎样工作的? -> 视图是基于 SQL 语句的结果集的可视化的表。**视图是虚拟的表,本身不存储数据,也就不能对其进行索引操作**。对视图的操作和对普通表的操作一样。 +视图是基于 SQL 语句的结果集的可视化的表。**视图是虚拟的表,本身不存储数据,也就不能对其进行索引操作**。对视图的操作和对普通表的操作一样。 视图的作用: @@ -231,7 +268,7 @@ EXISTS、IN、ANY、ALL、SOME - 通过只给用户访问视图的权限,保证数据的安全性。 - 更改数据格式和表示。 -### 13 丨什么是存储过程,在实际项目中用得多么? +## 13 丨什么是存储过程,在实际项目中用得多么? 存储过程的英文是 Stored Procedure。它可以视为一组 SQL 语句的批处理。一旦存储过程被创建出来,使用它就像使用函数一样简单,我们直接通过调用存储过程名即可。 @@ -253,7 +290,7 @@ EXISTS、IN、ANY、ALL、SOME > _综上,存储过程的优缺点都非常突出,是否使用一定要慎重,需要根据具体应用场景来权衡_。 -### 14 丨什么是事务处理,如何使用 COMMIT 和 ROLLBACK 进行操作? +## 14 丨什么是事务处理,如何使用 COMMIT 和 ROLLBACK 进行操作? ACID: @@ -271,25 +308,23 @@ ACID: 5. RELEASE SAVEPOINT:删除某个保存点。 6. SET TRANSACTION,设置事务的隔离级别。 -### 15 丨初识事务隔离:隔离的级别有哪些,它们都解决了哪些异常问题? +## 15 丨初识事务隔离:隔离的级别有哪些,它们都解决了哪些异常问题? 事务隔离级别从低到高分别是:读未提交(READ UNCOMMITTED )、读已提交(READ COMMITTED)、可重复读(REPEATABLE READ)和可串行化(SERIALIZABLE)。 -### 16 丨游标:当我们需要逐条处理数据时,该怎么做? +## 16 丨游标:当我们需要逐条处理数据时,该怎么做? -### 17 丨如何使用 Python 操作 MySQL? +## 17 丨如何使用 Python 操作 MySQL? 略 -### 18 丨 SQLAlchemy:如何使用 PythonORM 框架来操作 MySQL? +## 18 丨 SQLAlchemy:如何使用 PythonORM 框架来操作 MySQL? 略 -### 19 丨基础篇总结:如何理解查询优化、通配符以及存储过程? - -## 第二章:SQL 性能优化篇 +## 19 丨基础篇总结:如何理解查询优化、通配符以及存储过程? -### 20 丨当我们思考数据库调优的时候,都有哪些维度可以选择? +## 20 丨当我们思考数据库调优的时候,都有哪些维度可以选择? 我的理解: @@ -301,7 +336,7 @@ ACID: - 使用缓存 - 读写分离+分库分表 -### 21 丨范式设计:数据表的范式有哪些,3NF 指的是什么? +## 21 丨范式设计:数据表的范式有哪些,3NF 指的是什么? 范式定义: @@ -312,7 +347,7 @@ ACID: **范式化的目标是尽力减少冗余列,节省空间**。 -### 22 丨反范式设计:3NF 有什么不足,为什么有时候需要反范式设计? +## 22 丨反范式设计:3NF 有什么不足,为什么有时候需要反范式设计? **反范式化的目标是适当增加冗余列,以避免关联查询**。 @@ -326,7 +361,7 @@ ACID: - 增加了关联查询,而关联查询代价很高 -### 23 丨索引的概览:用还是不用索引,这是一个问题 +## 23 丨索引的概览:用还是不用索引,这是一个问题 > 索引的优缺点 @@ -358,13 +393,13 @@ ACID: - 非常小的表(比如不到 1000 行):简单的全表扫描更高效 - 特大型的表:索引的代价很高昂,可以用分区或 Nosql -### 24 丨索引的原理:我们为什么用 B+树来做索引? +## 24 丨索引的原理:我们为什么用 B+树来做索引? 磁盘的 I/O 操作次数对索引的使用效率至关重要。虽然传统的二叉树数据结构查找数据的效率高,但很容易增加磁盘 I/O 操作的次数,影响索引使用的效率。因此在构造索引的时候,我们更倾向于采用“矮胖”的数据结构。 B 树和 B+ 树都可以作为索引的数据结构,在 MySQL 中采用的是 B+ 树,B+ 树在查询性能上更稳定,在磁盘页大小相同的情况下,树的构造更加矮胖,所需要进行的磁盘 I/O 次数更少,更适合进行关键字的范围查询。 -### 25 丨 Hash 索引的底层原理是什么? +## 25 丨 Hash 索引的底层原理是什么? Mysql 中,只有 Memory 存储引擎显示支持哈希索引。 @@ -386,7 +421,7 @@ Mysql 中,只有 Memory 存储引擎显示支持哈希索引。 > 提示:因为种种限制,所以哈希索引只适用于特定的场合。而一旦使用哈希索引,则它带来的性能提升会非常显著。 -### 26 丨索引的使用原则:如何通过索引让 SQL 查询效率最大化? +## 26 丨索引的使用原则:如何通过索引让 SQL 查询效率最大化? ✔️️️️ 什么情况**适用**索引? @@ -411,7 +446,7 @@ Mysql 中,只有 Memory 存储引擎显示支持哈希索引。 - 索引列判空 - WHERE 子句中的 OR 前后条件存在非索引列 -### 27 丨从数据页的角度理解 B+树查询 +## 27 丨从数据页的角度理解 B+树查询 **在数据库中,不论读一行,还是读多行,都是将这些行所在的页进行加载。也就是说,数据库管理存储空间的基本单位是页(Page)。** @@ -426,7 +461,7 @@ Mysql 中,只有 Memory 存储引擎显示支持哈希索引。 - 表空间(Tablespace)是一个逻辑容器,表空间存储的对象是段,在一个表空间中可以有一个或多个段,但是一个段只能属于一个表空间。数据库由一个或多个表空间组成,表空间从管理上可以划分为系统表空间、用户表空间、撤销表空间、临时表空间等。 -### 28 丨从磁盘 I/O 的角度理解 SQL 查询的成本 +## 28 丨从磁盘 I/O 的角度理解 SQL 查询的成本 磁盘 I/O 耗时远大于内存,因此数据库会采用缓冲池的方式提升页的查找效率。 @@ -435,11 +470,11 @@ SQL 查询是一个动态的过程,从页加载的角度来看: 1. 位置决定效率。如果页就在数据库缓冲池中,那么效率是最高的,否则还需要从内存或者磁盘中进行读取,当然针对单个页的读取来说,如果页存在于内存中,会比在磁盘中读取效率高很多。 2. 批量决定效率。如果我们从磁盘中对单一页进行随机读,那么效率是很低的(差不多 10ms),而采用顺序读取的方式,批量对页进行读取,平均一页的读取效率就会提升很多,甚至要快于单个页面在内存中的随机读取。 -### 29 丨为什么没有理想的索引? +## 29 丨为什么没有理想的索引? 略 -### 30 丨锁:悲观锁和乐观锁是什么? +## 30 丨锁:悲观锁和乐观锁是什么? 基于加锁方式分类,Mysql 可以分为悲观锁和乐观锁。 @@ -449,7 +484,7 @@ SQL 查询是一个动态的过程,从页加载的角度来看: - **乐观锁** - 假设最好的情况——每次访问数据时,都假设数据不会被其他线程修改,不必加锁。只在更新的时候,判断一下在此期间是否有其他线程更新该数据。 - 实现方式:**更新数据时,先使用版本号机制或 CAS 算法检查数据是否被修改**。 -### 31 丨为什么大部分 RDBMS 都会支持 MVCC? +## 31 丨为什么大部分 RDBMS 都会支持 MVCC? MVCC 的核心就是 Undo Log+ Read View @@ -457,7 +492,7 @@ MVCC 的核心就是 Undo Log+ Read View - 通过 Read View 原则来决定数据是否显示; - 时针对不同的隔离级别,Read View 的生成策略不同,也就实现了不同的隔离级别 -### 32 丨查询优化器是如何工作的? +## 32 丨查询优化器是如何工作的? MySQL 整个查询执行过程,总的来说分为 6 个步骤,分别对应 6 个组件: @@ -468,13 +503,13 @@ MySQL 整个查询执行过程,总的来说分为 6 个步骤,分别对应 6 5. **执行器** - MySQL 服务器根据执行计划,调用存储引擎的 API 来执行查询。 6. **返回结果** - MySQL 服务器将结果返回给客户端,同时缓存查询结果。 -### 33 丨如何使用性能分析工具定位 SQL 执行慢的原因? +## 33 丨如何使用性能分析工具定位 SQL 执行慢的原因? ![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220720093823.png) -### 34 丨答疑篇:关于索引以及缓冲池的一些解惑 +## 34 丨答疑篇:关于索引以及缓冲池的一些解惑 -### 35 丨数据库主从同步的作用是什么,如何解决数据不一致问题? +## 35 丨数据库主从同步的作用是什么,如何解决数据不一致问题? Mysql 支持两种复制:基于行的复制和基于语句的复制。 @@ -508,9 +543,9 @@ Mysql 支持两种复制:基于行的复制和基于语句的复制。 在一个复制组内有多个节点组成,它们各自维护了自己的数据副本,并且在一致性协议层实现了原子消息和全局有序消息,从而保证组内数据的一致性。 -### 36 丨数据库没有备份,没有使用 Binlog 的情况下,如何恢复数据? +## 36 丨数据库没有备份,没有使用 Binlog 的情况下,如何恢复数据? -### 37 丨 SQL 注入:你的 SQL 是如何被注入的? +## 37 丨 SQL 注入:你的 SQL 是如何被注入的? **SQL 注入攻击(SQL injection)**,是发生于应用程序之数据层的安全漏洞。简而言之,是在输入的字符串之中注入 SQL 指令,在设计不良的程序当中忽略了检查,那么这些注入进去的指令就会被数据库服务器误认为是正常的 SQL 指令而运行,因此遭到破坏或是入侵。 @@ -579,34 +614,28 @@ MSSQL 服务器会执行这条 SQL 语句,包括它后面那个用于向系统 - **使用参数化查询** - 建议使用数据库提供的参数化查询接口,参数化的语句使用参数而不是将用户输入变量嵌入到 SQL 语句中,即不要直接拼接 SQL 语句。例如使用 database/sql 里面的查询函数 `Prepare` 和 `Query` ,或者 `Exec(query string, args ...interface{})`。 - **单引号转换** - 在组合 SQL 字符串时,先针对所传入的参数进行字符替换(将单引号字符替换为连续 2 个单引号字符)。 -## 第三章:认识 DBMS - -> 内容对我意义不大,略 - -### 38 丨如何在 Excel 中使用 SQL 语言? - -### 39 丨 WebSQL:如何在 H5 中存储一个本地数据库? +## 38 丨如何在 Excel 中使用 SQL 语言? -### 40 丨 SQLite:为什么微信用 SQLite 存储聊天记录? +## 39 丨 WebSQL:如何在 H5 中存储一个本地数据库? -### 41 丨初识 Redis:Redis 为什么会这么快? +## 40 丨 SQLite:为什么微信用 SQLite 存储聊天记录? -### 42 丨如何使用 Redis 来实现多用户抢票问题 +## 41 丨初识 Redis:Redis 为什么会这么快? -### 43 丨如何使用 Redis 搭建玩家排行榜? +## 42 丨如何使用 Redis 来实现多用户抢票问题 -### 44 丨 DBMS 篇总结和答疑:用 SQLite 做词云 +## 43 丨如何使用 Redis 搭建玩家排行榜? -## 第四章:SQL 项目实战 +## 44 丨 DBMS 篇总结和答疑:用 SQLite 做词云 -### 45 丨数据清洗:如何使用 SQL 对数据进行清洗? +## 45 丨数据清洗:如何使用 SQL 对数据进行清洗? SQL 可以帮我们进行数据处理,总的来说可以分成 OLTP 和 OLAP 两种方式。 - **OLTP**:称之为**联机事务处理**。对数据进行增删改查,SQL 查询优化,事务处理等就属于 OLTP 的范畴。它对实时性要求高,需要将用户的数据有效地存储到数据库中,同时有时候针对互联网应用的需求,我们还需要设置数据库的主从架构保证数据库的高并发和高可用性。 - **OLAP**:称之为**联机分析处理**。它是对已经存储在数据库中的数据进行分析,帮我们得出报表,指导业务。它对数据的实时性要求不高,但数据量往往很大,存储在数据库(数据仓库)中的数据可能还存在数据质量的问题,比如数据重复、数据中有缺失值,或者单位不统一等,因此在进行数据分析之前,首要任务就是对收集的数据进行清洗,从而保证数据质量。 -### 46 丨数据集成:如何对各种数据库进行集成和转换? +## 46 丨数据集成:如何对各种数据库进行集成和转换? ![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220720142031.png) @@ -616,10 +645,10 @@ ETL 是英文 Extract、Transform 和 Load 的缩写,也就是将数据从不 - 在 Transform 数据转换的过程中,我们可以使用一些数据转换的组件,比如说数据字段的映射、数据清洗、数据验证和数据过滤等,这些模块可以像是在流水线上进行作业一样,帮我们完成各种数据转换的需求,从而将不同质量,不同规范的数据进行统一。 - 在 Load 数据加载的过程中,我们可以将转换之后的数据加载到目的地,如果目标是 RDBMS,我们可以直接通过 SQL 进行加载,或者使用批量加载的方式进行加载。 -### 47 丨如何利用 SQL 对零售数据进行分析? +## 47 丨如何利用 SQL 对零售数据进行分析? 略 -# 参考资料 +## 参考资料 - [SQL 必知必会](https://time.geekbang.org/column/intro/192) diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/01.\345\220\216\347\253\257\345\255\230\345\202\250\345\256\236\346\210\230\350\257\276\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\220\216\347\253\257\345\255\230\345\202\250\345\256\236\346\210\230\350\257\276\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/01.\345\220\216\347\253\257\345\255\230\345\202\250\345\256\236\346\210\230\350\257\276\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\220\216\347\253\257\345\255\230\345\202\250\345\256\236\346\210\230\350\257\276\347\254\224\350\256\260.md" index 11c9b1f12e..91857156a5 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/01.\345\220\216\347\253\257\345\255\230\345\202\250\345\256\236\346\210\230\350\257\276\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\220\216\347\253\257\345\255\230\345\202\250\345\256\236\346\210\230\350\257\276\347\254\224\350\256\260.md" @@ -1,14 +1,13 @@ --- title: 《后端存储实战课》笔记 date: 2022-04-08 17:00:00 -order: 01 categories: - 笔记 - 数据库 tags: - 架构 - 数据库 -permalink: /pages/a16273/ +permalink: /pages/61d5da7a/ --- # 《后端存储实战课》笔记 @@ -191,4 +190,4 @@ LSM-Tree 的全称是:The Log-Structured Merge-Tree,是一种非常复杂的 ## 参考资料 -- [后端存储实战课](https://time.geekbang.org/column/intro/100046801) - 极客教程【入门】:讲解存储在电商领域的种种应用和一些基本特性 \ No newline at end of file +- [后端存储实战课](https://time.geekbang.org/column/intro/100046801) - 极客教程【入门】:讲解存储在电商领域的种种应用和一些基本特性 diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/11.\346\243\200\347\264\242\346\212\200\346\234\257\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\243\200\347\264\242\346\212\200\346\234\257\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/11.\346\243\200\347\264\242\346\212\200\346\234\257\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\243\200\347\264\242\346\212\200\346\234\257\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" index b3051674f7..1f0d44ff30 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/11.\346\243\200\347\264\242\346\212\200\346\234\257\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\243\200\347\264\242\346\212\200\346\234\257\346\240\270\345\277\20320\350\256\262\347\254\224\350\256\260.md" @@ -1,13 +1,12 @@ --- title: 《检索技术核心 20 讲》笔记 date: 2022-03-04 20:03:00 -order: 11 categories: - 笔记 - 数据库 tags: - 架构 -permalink: /pages/346350/ +permalink: /pages/c10355d2/ --- # 《检索技术核心 20 讲》笔记 @@ -266,4 +265,4 @@ LevelDB 采用了延迟合并的设计来优化。具体来说就是,先将 Im ## 参考资料 -- [检索技术核心 20 讲](https://time.geekbang.org/column/intro/100048401) \ No newline at end of file +- [检索技术核心 20 讲](https://time.geekbang.org/column/intro/100048401) diff --git "a/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\351\253\230\346\200\247\350\203\275MySQL\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\351\253\230\346\200\247\350\203\275MySQL\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..84273a3a62 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/12.\346\225\260\346\215\256\345\272\223/\351\253\230\346\200\247\350\203\275MySQL\347\254\224\350\256\260.md" @@ -0,0 +1,572 @@ +--- +title: 《高性能 MySQL》笔记 +date: 2024-10-02 20:50:31 +categories: + - 笔记 + - 数据库 +tags: + - 数据库 + - 关系型数据库 + - MySQL +permalink: /pages/a8664b1a/ +--- + +# 《高性能 MySQL》笔记 + +> 部分章节内容更偏向于 DBA 的工作,在实际的开发工作中相关性较少,直接略过。 + +## 第一章 MySQL 架构与历史 + +### MySQL 逻辑架构 + +MySQL 逻辑架构分为三层: + +- 连接层 - 连接管理、认证管理 +- 核心服务层 - 缓存、解析、优化、执行 +- 存储引擎层 - 数据实际读写 + +### 并发控制 + +解决并发问题的最常见方式是加锁。 + +- 排它锁(exclusive lock) - 也叫写锁(write lock)。**锁一次只能被一个线程所持有**。 + +- 共享锁(shared lock) - 也叫读锁(read lock)。**锁可被多个线程所持有**。 + +加锁、解锁,检查锁是否已释放,都需要消耗资源,因此锁定的粒度越小,并发度越高。 + +MySQL 中支持多种锁粒度: + +- 表级锁(table lock) - 锁定整张表,会阻塞其他用户对该表的读写操作。 +- 行级锁(row lock) - 可以最大程度的支持并发处理。 + +### 事务 + +事务就是一组原子性的 SQL 查询。事务内的语句,要么全部执行成功,要么全部执行失败。 + +#### ACID + +ACID 是数据库事务正确执行的四个基本要素。 + +- **原子性 (Atomicity)**:一个事务被视为不可分割的最小工作单元,一个事务的所有操作要么全部提交成功,要么全部失败回滚。 +- **一致性 (Consistency)**:数据库总是从一个一致的状态到另一个一致的状态。事务没有提交,事务的修改就不会保存到数据库中。 +- **隔离性 (isolation)**:通常来说,一个事务所作的操作在最终提交之前,对其他事务来说是不可见的。 +- **持久性 (durability)**:一旦事务提交,则其所作的修改就会永久的保存到数据库中。 + +#### 事务隔离级别 + +SQL 标准提出了四种“事务隔离级别”。事务隔离级别等级越高,越能保证数据的一致性和完整性,但是执行效率也越低。因此,设置数据库的事务隔离级别时需要做一下权衡。 + +事务隔离级别从低到高分别是: + +- **“读未提交(read uncommitted)”** - 是指,**事务中的修改,即使没有提交,对其它事务也是可见的**。 + - **读未提交存在脏读问题**。“脏读(dirty read)”是指当前事务可以读取其他事务未提交的数据。 +- **“读已提交(read committed)” ** - 是指,**事务提交后,其他事务才能看到它的修改**。换句话说,一个事务所做的修改在提交之前对其它事务是不可见的。 + - **读已提交解决了脏读的问题**。 + - **读已提交存在不可重复读问题**。“不可重复读(non-repeatable read)”是指一个事务内多次读取同一数据,过程中,该数据被其他事务所修改,导致当前事务多次读取的数据可能不一致。 + - **读已提交是大多数数据库的默认事务隔离级别**,如 Oracle。 +- **“可重复读(repeatable read)”** - 是指:**保证在同一个事务中多次读取同样数据的结果是一样的**。 + - **可重复读解决了不可重复读问题**。 + - **可重复读存在幻读问题**。“幻读(phantom read)”是指一个事务内多次读取同一范围的数据过程中,其他事务在该数据范围新增了数据,导致当前事务未发现新增数据。 + - **可重复读是 InnoDB 存储引擎的默认事务隔离级别**。 +- **串行化(serializable )** - 是指,**强制事务串行执行**,对读取的每一行数据都加锁,一旦出现锁冲突,必须等前面的事务释放锁。 + - **串行化解决了幻读问题**。由于强制事务串行执行,自然避免了所有的并发问题。 + - **串行化策略会在读取的每一行数据上都加锁**,这可能导致大量的超时和锁竞争。这对于高并发应用基本上是不可接受的,所以一般不会采用这个级别。 + +事务隔离级别对并发一致性问题的解决情况: + +| 隔离级别 | 丢失修改 | 脏读 | 不可重复读 | 幻读 | +| :------: | :------: | :--: | :--------: | :--: | +| 读未提交 | ✔️️️ | ❌ | ❌ | ❌ | +| 读已提交 | ✔️️️ | ✔️️️ | ❌ | ❌ | +| 可重复读 | ✔️️️ | ✔️️️ | ✔️️️ | ❌ | +| 可串行化 | ✔️️️ | ✔️️️ | ✔️️️ | ✔️️️ | + +#### 死锁 + +死锁是指两个或多个事务竞争同一资源,从而导致恶性循环的现象。多个事务视图以不同顺序锁定资源时,就可能会产生死锁;多个事务同时锁定同一资源时,也会产生死锁。 + +InnoDB 目前处理死锁的方法是**将持有最少行级锁的事务进行回滚**。 + +#### 事务日志 + +InnoDB 通过事务日志记录修改操作。事务日志的写入采用追加方式,因此是顺序 I/O,比随机 I/O 快很多。 + +事务日志持久化后,内存中被修改的数据由后台程序慢慢刷回磁盘,这称为预写日志(Write Ahead Logging,WAL) + +如果数据修改以及记录到事务日志并持久化,此时系统崩溃,存储引擎可以在系统重启之后自动恢复数据。 + +#### MySQL 中的事务 + +MySQL 提供了两种事务存储引擎:InnoDB 和 NDB CLuster。 + +MySQL 默认采用自动提交模式(AUTOCOMMIT)。即如果不显式的声明一个事务,MySQL 会把每一个查询都当作一个事务来操作。 + +可以通过设置 AUTOCOMMIT 来启用或禁用自动提交模式。 + +可以通过执行 SET TRANSACTION ISOLATION LEVEL 来设置事务隔离级别。 + +InnoDB 采用两阶段锁定协议,在事务执行过程中,随时都可以执行锁定,锁只有在执行 COMMIT 或者 ROLLBACK 时才会释放,并且所有的锁都在一瞬间释放。 + +InnoDB 也支持通过特定语句显示加锁: + +```sql +// 先在表上加上 IS 锁,然后对读取的记录加 S 锁 +select ... lock in share mode; + +// 当前读:先在表上加上 IX 锁,然后对读取的记录加 X 锁 +select ... for update; +``` + +### 多版本并发控制 + +可以将 MVCC 视为行级锁的一个变种,它在很多情况下避免了加锁,因此开销更低。 + +MVCC 是通过保存数据在某个时刻的快照来实现的。也就是说,不管执行多久,每个事务看到的数据是一致的。根据事务开始时间不同, 每个事务对同一张表,同一时刻看到的数据可能是不一样的。 + +不同存储引擎实现 MVCC 的方式有所不同,典型的有乐观并发控制和悲观并发控制。 + +InnoDB 的 MVCC 是通过在每行记录后面保存两个隐藏列来实现。一个列保存了行的创建时间,一个是保存了过期时间。当然存储的不是实际的时间,而是系统版本号(system version number),每开始一个新事务,系统版本号都会自动递增。事务开始时刻的系统版本号作为事务的版本号,用来和查询到的每行记录的版本号作比较。 + +- **Select** - InnoDB 会根据这两个条件来查询: + - 只查找版本号小于或者等于当前事务的数据行,这样可以保证事务读取到的数据要么是在事务开始前就存在的,要么是自己插入或者修改的。 + - 行的删除版本要么未定义,要么大于当前事务的版本号,这样可以保证读取到的数据在事务开始之前没有被删除。 +- **Insert** - InnoDB 为新插入的每一行数据保存当前的系统版本号为行版本号。 +- **Delete** - InnoDB 为删除的每一行保存当前的版本号为行删除标识。 +- **Update** - InnoDB 为插入一条新纪录,保存当前系统版本号为行版本号,同时保存当前系统的版本号到原来的行为行删除标识。 + +MVCC 只在可重复读和读已提交两个隔离级别下工作。 + +### MySQL 的存储引擎 + +Mysql 将每个数据库保存为数据目录下的一个子目录。建表时,MySQL 会在数据库子目录下创建一个和表同名的 .frm 文件保存表的定义。因为 MySQL 使用文件系统的目录和文件来保存数据库和表的定义,大小写敏感性和具体的平台密切相关:在 Windows 中,大小写不敏感;在 Linux 中,大小写敏感。 + +Mysql 常见存储引擎 + +- InnoDB - 默认事务引擎。 +- MyISAM - Mysql 5.1 及之前的默认引擎。 +- Archive +- Memory +- NDB + +## ~~第二章 MySQL 基准测试(略)~~ + +## ~~第三章 服务器性能剖析(略)~~ + +## 第四章 Schema 与数据类型优化 + +### 数据类型 + +#### 整数类型 + +整数类型有可选的 `UNSIGNED` 属性,标识不允许负值,大致可以使正数的上限提高一倍。 + +| 类型 | 大小 | 作用 | +| :---------- | :----- | :--------- | +| `TINYINT` | 1 字节 | 小整数值 | +| `SMALLINT` | 2 字节 | 大整数值 | +| `MEDIUMINT` | 3 字节 | 大整数值 | +| `INT` | 4 字节 | 大整数值 | +| `BIGINT` | 8 字节 | 极大整数值 | + +#### 浮点数类型 + +`FLOAT` 和 `DOUBLE` 分别使用 4 个字节、8 个字节存储空间,它们支持使用标准的浮点运算进行近似计算,存在丢失精度的可能。 + +`DECIMAL` 类型用于存储精确的小数,支持精确计算,但是计算代价高。只有在需要对小数进行精确计算时,才应该使用 `DECIMAL`,例如财务数据。此外,当数据量较大时,可以考虑使用 BIGINT 代替 DECIMAL,将需要存储的货币单位乘以需要精确的倍数即可。 + +| 类型 | 大小 | 用途 | +| :-------- | :----- | :------------- | +| `FLOAT` | 4 字节 | 单精度浮点数值 | +| `DOUBLE` | 8 字节 | 双精度浮点数值 | +| `DECIMAL` | | 精确的小数值 | + +#### 字符串类型 + +VARCHAR 类型用于存储可变长字符串。 + +CHAR 类型是定长字符串。 + +与 CHAR 和 VARCHAR 类似的类型还有 BINARY 和 VARBINARY,它们存储的是二进制字符串。 + +| 类型 | 大小 | 用途 | +| :-------- | :----------- | :--------- | +| `CHAR` | 0-255 字节 | 定长字符串 | +| `VARCHAR` | 0-65535 字节 | 变长字符串 | + +#### BLOB 和 TEXT + +`BLOB` 和 `TEXT` 都用于存储很大的数据,分别采用二进制和字符串方式存储。 + +| 类型 | 大小 | 用途 | +| :----------- | :------------------- | :------------------------------ | +| `TINYBLOB` | 0-255 字节 | 不超过 255 个字符的二进制字符串 | +| `TINYTEXT` | 0-255 字节 | 短文本字符串 | +| `BLOB` | 0-65 535 字节 | 二进制形式的长文本数据 | +| `TEXT` | 0-65 535 字节 | 长文本数据 | +| `MEDIUMBLOB` | 0-16 777 215 字节 | 二进制形式的中等长度文本数据 | +| `MEDIUMTEXT` | 0-16 777 215 字节 | 中等长度文本数据 | +| `LONGBLOB` | 0-4 294 967 295 字节 | 二进制形式的极大文本数据 | +| `LONGTEXT` | 0-4 294 967 295 字节 | 极大文本数据 | + +#### 日期和时间类型 + +| 类型 | 大小 | 格式 | 作用 | 备注 | +| :-------- | :----- | :------------------ | :----------------------- | --------------------------------------------------------- | +| DATE | 3 字节 | YYYY-MM-DD | 日期值 | | +| TIME | 3 字节 | HH:MM:SS | 时间值或持续时间 | | +| YEAR | 1 字节 | YYYY | 年份值 | | +| DATETIME | 8 字节 | YYYY-MM-DD hh:mm:ss | 混合日期和时间值 | 有效时间范围为 1000-01-01 00:00:00 到 9999-12-31 23:59:59 | +| TIMESTAMP | 4 字节 | YYYY-MM-DD hh:mm:ss | 混合日期和时间值,时间戳 | 有效时间范围为 1970-01-01 00:00:01 到 2038-01-19 03:14:07 | + +#### 特殊类型 + +- **ENUM** - 枚举类型,用于存储单一值,可以选择一个预定义的集合。 +- **SET** - 集合类型,用于存储多个值,可以选择多个预定义的集合。 + +### Schema 设计简单规则 + +- 尽量避免过度设计,例如会导致极其复杂查询的 schema 设计,或者有很多列的表设计。 +- 使用小而简单的合适数据类型,除非真实数据模型中有确切的需要,否则应该尽可能地避免使用 NULL 值。 +- 尽量使用相同的数据类型存储相似或相关的值,尤其是要在关联条件中使用的列。 +- 注意可变长字符串,其在临时表和排序时可能导致悲观的按最大长度分配内存。 +- 尽量使用整型定义标识列。 +- 避免使用 MySQL 已经遗弃的特性,例如制定浮点数的精度,或者整数的显示宽度。 +- 小心使用 ENUM 和 SET,虽然它们用起来很方便,但是不要滥用,否则有时候会变成陷阱,最好避免使用 BIT。 + +范式意味着不存储冗余数据,但往往需要多关联查询,增加了查询的复杂度;反范式意味着存储冗余数据,但是减少了关联查询。在实际应用中,范式和反范式应当混合使用。 + +ALTER TABLE 如果操作的是大表,需要耗费大量时间。一般的操作是:用新结构创建一张空表,从旧表查出所有数据插入新表,然后删除旧表。 + +有两种替代方案: + +- 在一台不提供服务的机器上执行 ALTER TABLE 操作,然后和提供服务的主库进行切换。 +- 影子拷贝:创建一张新表,然后通过重命名和删表操作交换两张表。 + +## 第五章 创建高性能的索引 + +索引是存储引擎用于快速找到记录的一种数据结构。 + +索引优化应该是对查询性能优化最有效的手段了。 + +### 索引基础 + +索引可以包含一个或多个列的值。如果索引包含多个列,那么列的顺序也十分重要,因为 MySQL 只能高效地使用索引的最左前缀列。 + +#### B-Tree 索引 + +大多数 MySQL 引擎都支持 B-Tree 索引。存储引擎以不同的方式使用 B-Tree 索引,性能也各有不同,各有优劣。例如,MyISAM 使用前缀压缩技术使得索引更小,但 InnoDB 则按照原数据格式进行存储。再如 MyISAM 索引通过数据的物理位置引用被索引的行,而 InnoDB 则根据主键引用被索引的行。 + +B-Tree 通常意味着所有的值都是按顺序存储的,并且每一个叶子页到根的距离相同。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041118763.png) + +B-Tree 索引从索引的根节点开始进行搜索。根节点的槽中存放了指向子节点的指针,存储引擎根据这些指针向下层查找。通过比较节点页的值和要查找的值可以找到合适的指针进入下层子节点,这些指针实际上定义了子节点页中值的上限和下限。最终存储引擎要么是找到对应的值,要么该记录不存在。 + +叶子节点比较特别,它们的指针指向的是被索引的数据,而不是其他的节点页。在根节点和叶子节点之间可能有很多层节点页。树的深度和表的大小直接相关。 + +B-Tree 对索引列是顺序组织存储的,所以很适合查找范围数据。 + +假设有如下数据表: + +```sql +CREATE TABLE People( + last_name varchar(50) not null, + first_name varchar(50) not null, + dob date not null, + gender enum('m','f')not null, + key(last_name, first_name, dob) +); +``` + +对于表中的每一行数据,索引中包含了 last_name、 first_name 和 dob 列的值。 + +请注意,索引对多个值进行排序的依据是 CREATE TABLE 语句中定义索引时列的顺序。看一下最后两个条目,两个人的姓和名都-样,则根据他们的出生日期来排列顺序。 + +可以使用 B-Tree 索引的查询类型。B-Tree 索引适用于全键值、键值范围或键前缀查找。 + +其中键前缀查找只适用于根据最左前缀的查找生。前面所述的索引对如下类型的查询有效。 + +- _全值匹配_ - 全值匹配指的是和索引中的所有列进行匹配,例如前面提到的索引可用于查找姓名为 Cuba Allen、出生于 1960-01-01 的人。 +- _匹配最左前缀_ - 前面提到的索引可用于查找所有姓为 Allen 的人,即只使用索引的第一列。 +- _匹配列前缀_ - 也可以只匹配某–列的值的开头部分。例如前面提到的索引可用于查找所有以 J 开头的姓的人。这里也只使用了索引的第一列。 +- _匹配范围值_ - 例如前面提到的索引可用于查找姓在 Allen 和 Barrymore 之间的人。这里也只使用了索引的第一列。 +- _精确匹配某一列并范围匹配另外一列_ - 前面提到的索引也可用于查找所有姓为 Allen, 并且名字是字母 K 开头的人。即第一列 last\_ name 全匹配,第二列 first_name 范围匹配。 +- _只访问索引的查询_ - B-Tree 通常可以支持“只访问索引的查询”,即查询只需要访问索引,而无须访问数据行。也叫做覆盖索引。 + +因为索引树中的节点是有序的,所以除了按值查找外,索引还可以用于查询中的排序操作。 + +B-Tree 索引的限制: + +- **如果不是按照索引的最左列开始查找,则无法使用索引**。例如上面例子中的索引无法用于查找名字为 Bill 的人,也无法查找某个特定生日的人,因为这两列都不是最左数据列。类似地,也无法查找姓氏以某个字母结尾的人。 +- **不能跳过索引中的列**。也就是说,前面所述的索引无法用于查找姓为 Smith 并且在某个特定日期出生的人。如果不指定名 (first_name),则 MySQL 只能使用索引的第一列。 +- **如果查询中有某个列的范围查询,则其右边所有列都无法使用索引优化查找**。例如有查询 `WHERE last_name=' Smith' AND first_name LIKE 'J%' AND dob = '1976-12-23'` ,这个查询只能使用索引的前两列,因为这里 LIKE 是一个范围条件(但是服务器可以把其余列用于其他目的)。如果范围查询列值的数量有限,那么可以通过使用多个等于条件来代替范围条件。 + +#### 哈希索引 + +哈希索引 (hashindex) 基于哈希表实现,只有精确匹配索引所有列的查询才有效。 + +对于每一行数据,存储引擎都会对所有的索引列计算-一个哈希码 (hash code), 哈希码是一个较小的值,并且不同键值的行计算出来的哈希码也不一样。哈希索引将所有的哈希码存储在索引中,同时在哈希表中保存指向每个数据行的指针。 + +如果多个列的哈希值相同,索引会以链表的方式存放多个记录指针到同一个哈希条目中。 + +哈希索引的限制: + +- 哈希索引只包含哈希值和行指针,而不存储字段值,所以**不能使用索引中的值来避免读取行**。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。 +- 哈希索引数据并不是按照索引值顺序存储的,所以也就**无法用于排序**。 + +- 哈希索引也**不支持部分索引列匹配查找**,因为哈希索引始终是使用索引列的全部内容来计算哈希值的。例如,在数据列 (A,B) 上建立哈希索引,如果查询只有数据列 A, 则无法使用该索引。 + +- 哈希索引**只支持等值比较查询**,包括 `=`、`IN()`、 `<=>` (注意 `<>` 和 `<=>` 是不同的操作)。也不支持任何范围查询,例如 `WHERE price > 100`。 + +- **访问哈希索引的数据非常快**,除非有很多哈希冲突(不同的索引列值却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中所有的行指针,逐行进行比较,直到找到所有符合条件的行。 + +- **如果哈希冲突很多的话,一些索引维护操作的代价也会很高**。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。 + +#### 空间数据索引 (R-Tree) + +MyISAM 表支持空间索引,可以用作地理数据存储。和 B-Tree 索引不同,这类索引无须前缀查询。空间索引会从所有维度来索引数据。 + +查询时,可以有效地使用任意维度来组合查询。必须使用 MySQL 的 GIS 相关函数如 MBRCONTAINS() 等来维护数据。MySQL 的 GIS 支持并不完善,所以大部分人都不会使用这个特性。开源关系数据库系统中对 GIS 的解决方案做得比较好的是 PostgreSQL 的 PostGIS + +#### 全文索引 + +全文索引是一种特殊类型的索引,它查找的是文本中的关键词,而不是直接比较索引中的值。 + +全文搜索和其他几类索引的匹配方式完全不一样。它有许多需要注意的细节,如停用词、词干和复数、布尔搜索等。 + +全文索引更类似于搜索引擎做的事情,而不是简单的 WHERE 条件匹配。 + +在相同的列上同时创建全文索引和基于值的 B-Tree 索引不会有冲突,全文索引适用于 MATCH AGAINST 操作,而不是普通的 WHERE 条件操作。 + +### 索引的优点 + +索引有以下优点: + +1. 索引大大减少了服务器需要扫描的数据量。 +2. 索引可以帮助服务器避免排序和临时表。 +3. 索引可以将随机 I/O 变为顺序 I/O。 + +索引是最好的解决方案吗? + +- 对于非常小的表,大部分情况下简单的全表扫描更高效。 + +- 对于中到大型的表,索引就非常有效。 + +- 但对于特大型的表,建立和使用索引的代价将随之增长。这种情况下,则需要一种技术可以直接区分出查询需要的一组数据,而不是一条记录一条记录地匹配。例如可以使用分区技术。 + +- 如果表的数量特别多,可以建立一个元数据信息表,用来查询需要用到的某些特性。例如执行那些需要聚合多个应用分布在多个表的数据的查询,则需要记录。哪个用户的信息存储在哪个表中”的元数据,这样在查询时就可以直接忽略那些不包含指定用户信息的表。对于大型系统,这是一个常用的技巧。 + +### 高性能的索引策略 + +正确地创建和使用索引是实现高性能查询的基础。 + +#### 独立的列 + +**独立的列**是指索引列不能是表达式的一部分,也不能是函数的参数。 + +下面两个例子都无法使用索引: + +```sql +SELECT actor_ id FROM sakila.actor WHERE actor_id + 1 = 5; +SELECT ... WHERE TO_DAYS(CURRENT_DATE) - TO_ DAYS(date_col) <= 10; +``` + +#### 前缀索引和索引选择性 + +有时候需要索引很长的字符列,这会让索引变得大且慢。一种策略是,可以索引开始的部分字符,这样可以大大节约索引空间,从而提高索引效率。但这样也会降低索引的选择性。索引的选择性是指,不重复的索引值和总记录数的比值。索引的选择性越高则查询效率越高。 + +对于 BLOB、TEXT 或者很长的 VARCHAR 类型的列,必须使用前缀索引,因为 MySQL **不允许索引这些列的完整长度**。 + +前缀应该足够长,以使得前缀索引的选择性接近于索引整个列。通常来说,选择性能够接近 0.03,基本上就可用了。 + +计算前缀索引选择性的示例 + +```sql +SELECT COUNT(DISTINCT LEFT (city, 3)) / COUNT(*) AS sel3, + COUNT(DISTINCT LEFT (city, 4)) / COUNT(*) AS sel4, + COUNT(DISTINCT LEFT (city, 5)) / COUNT(*) AS sel5, + COUNT(DISTINCT LEFT (city, 6)) / COUNT(*) AS se16, + COUNT(DISTINCT LEFT (city, 7)) / COUNT(*) AS sel7, +FROM sakila.city demo; +``` + +#### 多列索引 + +**在多个列上建立独立的单列索引大部分情况下并不能提高 MySQL 的查询性能。** + +例如,表 film_actor 在字段 film_id 和 actor_id 上各有一个单列索引。但对于下面这个查询 WHERE 条件,这两个单列索引都不是好的选择: + +```sql +SELECT film_id, actor_id FROM sakila.film_actor +WHERE actor_id = 10 or film_id = 1; +``` + +#### 选择合适的索引列顺序 + +正确的顺序依赖于使用该索引的查询,并且同时需要考虑如何更好地满足排序和分组的需要。 + +如何选择索引的列顺序: + +- 将选择性最高的列放到索引最前列。 +- 可能需要根据那些运行频率最高的查询来调整索引列的顺序,让这种情况下索引的选择性最高。 + +```sql +SELECT * FROM payment WHERE staff.id = 2 AND customer._id = 584; +``` + +是应该创建一个 (staff*id, customer* id) 索引还是应该颠倒一下顺序? + +可以跑一些查询来确定在这个表中值的分布情况,并确定哪个列的选择性更高。 + +#### 聚簇索引 + +聚簇索引并不是一种单独的索引类型,而是一种数据存储方式。**聚簇**表示数据行和相邻的键值紧凑地存储在一起。因为无法同时把数据行存放在两个不同的地方,所以一个表**只能有一个聚簇索引**。 + +具体的细节依赖于其实现方式,在 InnoDB 中,数据行实际上存放在索引的**叶子页 (leaf page)** 中。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041119862.png) + +聚簇索引的优点: + +- **可以把相关数据保存在一起**,访问数据时,可以减少磁盘 I/O。 +- **数据访问更快**。聚簇索引将索引和数据保存在同一个 B-Tree 中,因此从聚簇索引中获取数据通常比在非聚簇索引中查找要快。 +- **使用覆盖索引扫描的查询可以直接使用页节点中的主键值**。 + +聚簇索引的缺点: + +- **聚簇数据最大限度地提高了 I/O 密集型应用的性能**,但如果数据全部都放在内存中,则访问的顺序就没那么重要了,聚簇索引也就没什么优势了。 +- **插入速度严重依赖于插入顺序**。按照主键的顺序插入是加载数据到 InnoDB 表中速度最快的方式。但如果不是按照主键顺序加载数据,那么在加载完成后最好使用 OPTIMIZE TABLE 命令重新组织一下表。 +- **更新聚簇索引列的代价很高**,因为会强制 InnoDB 将每个被更新的行移动到新的位置。 +- 基于聚簇索引的表在插入新行,或者主键被更新导致需要移动行的时候,**可能面临页分裂 (page split) 的问题**。当行的主键值要求必须将这一行插人到某个已满的页中时,存储引擎会将该页分裂成两个页面来容纳该行,这就是一次页分裂操作。页分裂会导致表占用更多的磁盘空间。 +- **聚簇索引可能导致全表扫描变慢**,尤其是行比较稀疏,或者由于页分裂导致数据存储不连续的时候。 +- **二级索引 (非聚簇索引)可能比想象的要更大**,因为在二级索引的叶子节点包含了引用行的主键列。 +- **二级索引访问需要两次索引查找**,而不是一次。(回表) + +#### InnoDB 和 MyISAM 的数据分布对比 + +MyISAM 存储引擎采用非聚簇索引存储数据,而 InnoDB 存储引擎采用聚簇索引存储数据。 + +来看下 MyISAM 和 InnoDB 是如何存储下面的表: + +``` +CREATE TABLE layout_test ( + col1 int NOT NULL, + col2 int NOT NULL, + PRIMARY KEY(col1), + KEY(col2), +); +``` + +对于 MyISAM,其数据分布比较简单,按照数据插入的顺序存储在磁盘上。对于每一行数据,都是一个行号,从 0 开始递增。由于行是定长的,所以 MyISAM 可以从表的开头跳过所需的字节找到需要的行(有点类似于数组)。如下图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041138948.png) + +MyISAM 使用主键索引查找数据时,在 B+Tree 的叶子节点除了存储索引键之外,还保存了每个键所处的行指针(可以理解为行号)。当找到某个索引键对应的行指针后,就能定位到它对应的数据。如下图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041138993.png) + +对于 MyISAM 的二级索引,它的存储方式跟主键索引没有什么区别,如下图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041216856.png) + +**所以对于 MyISAM 来讲,主键索引和其它索引在存储结构上并没有什么区别。主键索引就是一个名为 PRIMARY 的惟一非空索引**。 + +对于 InnoDB 来讲,主键索引是聚簇的,也就是主键索引就是表,所以不像 MyISAM 那样需要独立的行存储。 聚簇索引的每个叶子节点都包含了主键值、事务 ID、用于事务和 MVCC 的回滚指针以及所有剩余列(这个例子中是 col2)。对于 InnoDB 的主键索引,数据分布如下图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041216040.png) + +InnoDB 的二级索引和聚簇索引区别比较大,它的二级索引的叶子节点存储的不是”行指针”,而是主键值。存储主键值带来的好处是,InnoDB 在移动行时无须更新二级索引的这个指针。如下图: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041217784.png) + +**由于 InnoDB 是通过主键聚集数据,所以使用 InnoDB 时,一定要指定主键,如果没有定义主键,InnoDB 会选择一个惟一的非空索引代替,如果没有这样的索引,InnoDB 会隐式定义一个主键来作为聚簇索引。** + +**由于聚簇索引插入速度严重依赖于插入顺序。按照主键的顺序插入是加载数据到 InnoDB 表中速度最快的方式,所以通常我们都使用一个递增 ID 作为主键。** + +最后,我们使用一个比较抽象的图,对比一下聚簇和非聚簇的数据分布: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202410041217744.png) + +#### 覆盖索引 + +如果一个索引包含所有需要查询的字段的值,我们就称之为“ 覆盖索引"。覆盖索引能极大地提高性能。 + +- 索引条目通常远小于数据行大小,所以如果只需要读取索引,那 MySQL 就会极大地减少数据访问量。 +- 因为索引是按照列值顺序存储的(至少在单个页内是如此),所以对于 I/O 密集型的范围查询会比随机从磁盘读取每一行数据的 I/O 要少得多。 +- 一些存储引擎如 MyISAM 在内存中只缓存索引,数据则依赖于操作系统来缓存,因此要访问数据需要一次系统调用。 +- InnoDB 的二级索引在叶子节点中保存了行的主键值,所以如果二级主键能够覆盖查询,则可以避免对主键索引的二次查询。 + +覆盖索引必须要存储索引列的值,而哈希索引、空间索引和全文索引等都不存储索引列的值,所以 MySQL 只能使用 B-Tree 索引做覆盖索引。 + +#### 使用索引扫描来做排序 + +如果 `EXPLAIN` 出来的 `type` 列的值为 `index`, 则说明 MySQL 使用了索引扫描来做排序(不要和 `Extra` 列的 `Using index` 搞混淆了)。 + +MySQL 可以使用同一个索引既满足排序,又用于查找行。只有当索引的列顺序和 ORDER BY 子句的顺序完全一致,并且所有列的排序方向都一样时,MySQL 才能够使用索引来对结果做排序。 + +#### 索引和锁 + +InnoDB 只有在访问行的时候才会对其加锁,而索引能够减少 InnoDB 访问的行数,从而减少锁的数量。 + +## 第六章 查询性能优化 + +### 为什么查询速度会慢 + +### 慢查询基础:优化数据访问 + +### 重构查询的方式 + +### 查询执行的基础 + +### MySQL 查询优化器的局限性 + +### 查询优化器的提示(hint) + +### 优化特定类型的查询 + +### 案例学习 + +## ~~第七章 MySQL 高级特性(略)~~ + +## ~~第八章 优化服务器设置(略)~~ + +## ~~第九章 操作系统和硬件优化(略)~~ + +## 第十章 复制 + +### 复杂概述 + +### 配置复制 + +### 复制的原理 + +### 复制拓扑 + +### 复制和容量规划 + +### 复制管理和维护 + +### 复制的问题和解决方案 + +### 复制有多快 + +### MySQL 复制的高级特性 + +### 其他复制技术 + +## ~~第十一章 可扩展的 MySQL(略)~~ + +## ~~第十二章 高可用性(略)~~ + +## ~~第十三章 云端的 MySQL(略)~~ + +## ~~第十四章 应用层优化(略)~~ + +## ~~第十五章 备份与恢复(略)~~ + +## ~~第十六章 MySQL 用户工具(略)~~ + +## 参考资料 + +- [《高性能 MySQL》](https://book.douban.com/subject/23008813/) diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/02.\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\272\214.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/02.\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\272\214.md" deleted file mode 100644 index 28702e5ce2..0000000000 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/02.\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\272\214.md" +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: 《数据密集型应用系统设计》笔记二之数据系统基础 -date: 2021-08-26 23:32:00 -order: 02 -categories: - - 笔记 - - 分布式 - - 分布式综合 -tags: - - 数据库 - - 原理 -permalink: /pages/72a4bd/ ---- - -# 《数据密集型应用系统设计》笔记二之数据系统基础 - -## 第 1 章 可靠、可扩展与可维护的应用系统 - -### 认识数据系统 - -很多应用系统都包含以下数据处理系统: - -- 数据库:用以存储数据,这样之后应用可以再次面问。 -- 高速缓存: 缓存那些复杂或操作代价昂贵的结果,以加快下一次访问。 -- 索引: 用户可以按关键字搜索数据井支持各种过掳。 -- 流式处理:持续发送消息至另一个进程,处理采用异步方式。 -- 批处理: 定期处理大量的累积数据。 - -设计数据系统或数据服务时,需要考虑很多因素,其中最重要的三个问题: - -- **可靠性(Reliability)**:当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转:虽然性能可能有所降低,但确保功能正确。 -- **可扩展性(Scalability)**:随着规模的增长,例如数据量、流量或复杂性,系统应以合理的方式来匹配这种增长。 -- **可维护性(Maintainability)**:随着时间的推移,许多新的人员参与到系统开发和运维, 以维护现有功能或适配新场景等,系统都应高效运转。 - -### 可靠性 - -可靠性意味着:即时发生了某些错误,系统仍然可以继续正常工作。 - -系统可应对错误则称为容错(fault tolerant)或者弹性(resilient)。 - -常见的故障类型: - -- 硬件故障:通常是随机的,如:硬盘崩溃、内存故障、电网停电、断网等。常见应对策略:使用集群去冗余。 -- 软件故障:各种难以预料的 bug。 -- 人为故障:如操作不当。 - -### 可扩展性 - -可扩展性是指负载增加时, 有效保持系统性能的相关技术策略。 - -吞吐量:每秒可处理的记录数 - -响应时间:中位数指标比平均响应时间更适合描述等待时间。 - -如何应对负载:垂直扩展(升级硬件)和水平扩展(集群、分布式) - -### 可维护性 - -- 可运维性:方便运营团队来保持系统平稳运行。 -- 简单性:简化系统复杂性,使新工程师能够轻松理解系统。 -- 可演化性:后续工程师能够轻松地对系统进行改进,井根据需求变化将其适配到非典型场景,也称为可延伸性、易修改性或可塑性。 - -主要措施: - -- 良好的抽象可以帮助降低复杂性, 井使系统更易于修改和适配新场景。 -- 良好的可操作性意味着对系统健康状况有良好的可观测性和有效的管理方战。 - -## 第 2 章 数据模型与查询语言 - -复杂的应用程序可能会有更多的中间层,每层都通过提供一个简洁的数据模型来隐藏下层的复杂性。 - -如果数据大多是一对多关系(树结构数据)或者记录之间没有关系,那么文档模型是最合适的。 - -关系模型能够处理简单的多对多关系,但是随着数据之间的关联越来越复杂,将数据建模转化为图模型会更加自然。 - -## 第 3 章 数据存储与检索 - -从最基本的层面看,数据库只需做两件事情:存储和检索。 - -### 数据库核心:数据结构 - -为了高效地查找数据库中特定键的值, 需要新的数据结构: 索引。 - -存储系统的设计权衡:适当的索引可以加速读取查询,但每个索引都会减慢写速度。数据库通常不会对所有内容进行索引。 - -索引类型: - -- 哈希索引 -- B+ 树 -- LSM 树 -- 等等 - -> 扩展阅读:[检索技术核心 20 讲](https://time.geekbang.org/column/intro/100048401) - -### 事务处理与分析处理 - -### 列式存储 - -如果表中有数以万亿行、PB 大小的数据,则适合用于存储在列式存储中。 - -## 第 4 章 数据编码与演化 - -本章节主要介绍各种序列化、反序列化方式。略 \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\270\200.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\270\200.md" new file mode 100644 index 0000000000..90983eb242 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\270\200.md" @@ -0,0 +1,383 @@ +--- +title: 《数据密集型应用系统设计》笔记一——数据系统基础 +date: 2021-08-26 23:32:00 +order: 02 +categories: + - 笔记 + - 分布式 + - 分布式综合 +tags: + - 分布式 + - 数据库 + - 原理 +permalink: /pages/b3b63c5f/ +--- + +# 《数据密集型应用系统设计》笔记一——数据系统基础 + +## 第一章:可靠、可扩展与可维护的应用系统 + +### 认识数据系统 + +单一工具难以满足复杂应用系统的需求,因此整体工作被拆解为一系列能被单个工具高效完成的任务,并通过**应用代码**将它们缝合起来。比如一个缓存、索引、数据库协作的例子: ![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630635449781-eccd8717-84aa-4d52-b8d7-98790e2c92c7.png) 一个应用被称为数据密集型的,如果数据是其主要挑战(数据量,数据复杂度、数据变化速度)——与之相对的是计算密集型,即处理器速度是其瓶颈。 软件系统中很重要的三个问题: + +1. **可靠性**(Reliability):系统面临各种错误(硬件故障、软件故障、人为错误),仍可正常工作。 +2. **可扩展性**(Scalability):有合理的办法应对系统的增长(数据量、流量、复杂性)。 +3. **可维护性**(Maintainability):许多不同的人在不同的生命周期,都能高效地在系统上工作。 + +### 可靠性 + +可靠性意味着:即时发生了某些错误,系统仍然可以继续正常工作。 + +可能出错的事情称为错误(fault)或故障,系统可应对错误则称为容错(fault tolerant)或者弹性(resilient)。 + +故障与失效(failure)不完全一致。故障通常被定义为组件偏离其正常规格,而失效意味着系统作为一个整体,停止对外提供服务。 + +常见的故障分类: + +- **硬件故障** + - 故障场景:硬盘崩溃、内存故障、停电、断网等。 + - 应对策略:添加冗余硬件以备用;软件容错(如:负载均衡)。 +- **软件故障** + - 故障场景:各种难以预料的 Bug。 + - 应对策略:仔细考虑细节;全面测试;监控、告警;系统/数据隔离机制;自动化部署、回滚机制等。 +- **人为失误** + - 故障场景:操作不当、配置错误等。 + - 应对策略:快速恢复机制;监控、告警等。 + +### 可扩展性 + +可扩展性(Scalability)是用来描述系统应对负载增长能力的术语。 + +#### 描述负载 + +负载可以用称为负载参数的若干数字来描述。参数的最佳选择取决于系统的体系结构。它可能是 QPS、数据库中写入的比例、日活用户量、缓存命中率等。 + +推特发送推文的设计变迁: + +推文放在全局推文集合中,查询的时候做 join + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630635645347-1e1e5660-4229-42a2-9bf9-da9850ff944b.png) + +推文插入到每个关注者的时间线中,「扇出」比较大,当有千万粉丝的大 V 发推压力大 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630635669997-5d4951ae-5ec3-426d-9fc4-35a3cf579088.png) + +推特从方案一变成了方案二,然后变成了两者结合的方式 + +#### 描述性能 + +负责增加将会发生什么: + +1. 负载增加,但系统资源保持不变时,系统性能将受到什么影响? +2. 负载增加,如果希望性能保持不变时,需要增加多少系统资源? + +批处理系统,通常关心吞吐量(throughput);在线系统,通常更关心响应时间(response time)。 + +度量场景的响应时间,平均响应时间并不是一个合适的指标,因为它无法告诉有多少用户实际经历了多少延迟。最好使用百分位数,比如中位数(P50)、P95、P99、P999 等标识。 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630635717226-c218a4b8-b6f9-4e35-8f10-549d65cf3e23.png) + +测量客户端的响应时间非常重要(而不是服务端),比如会出现头部阻塞、网络延迟等。 + +实践中的百分位点,可以用一个滑动的时间窗口(比如 10 分钟)进行统计。可以对列表进行排序,效率低的话,考虑一下正向衰减,t-digest 等近似计算方法。 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630635787568-a7885c39-997f-4edb-8fb9-79eff18467a2.png) + +响应时间:中位数指标比平均响应时间更适合描述等待时间。 + +如何应对负载:垂直扩展(升级硬件)和水平扩展(集群、分布式) + +#### 应对负载的方法 + +- 垂直扩展:升级硬件 +- 水平扩展:将负载分布到多台小机器上 +- 弹性设计:自动检测负载增加,然后自动添加计算资源 +- 无状态服务可以组成集群进行扩展;有状态服务从单点到分布式,复杂性会大大增加,因此,应该尽量将数据库放在单节点上。 + +### 可维护性 + +三个设计原则: + +- **可运维性**:运维更轻松。应对:监控、链路追踪、CI/CD、规范流程等。 +- **简单性**:简化复杂度。应对:良好的抽象。 +- **可演化性**:易于改变。应对:DDD、TDD、重构、敏捷。 + +## 第二章:数据模型与查询语言 + +### 关系模型与文档模型 + +关系模型 - 数据被组织成**关系**(SQL 中称作**表**),其中每个关系是**元组**(SQL 中称作**行**) 的无序集合。 + +NoSql - 不仅是 SQL(Not Only SQL) + +相比于关系型数据库,为什么用 NoSql? + +- 需要更好的扩展性,以应对非常大的数据集或高并发。 +- 关系模型不能很好地支持一些特殊的查询。 +- 关系模型有很多限制,不够灵活。 + +当前以及未来很长一段时间,关系型数据库和 NoSql 并存的混合持久化是一种常态。 + +复杂的应用程序可能会有更多的中间层,每层都通过提供一个简洁的数据模型来隐藏下层的复杂性。 + +如果数据大多是一对多关系(树结构数据)或者记录之间没有关系,那么文档模型是最合适的。 + +关系模型能够处理简单的多对多关系,但是随着数据之间的关联越来越复杂,将数据建模转化为图模型会更加自然。 + +#### 对象关系不匹配 + +使用面向对象语言,需要一个转换层,才能转成 SQL 数据模型。模型之间的脱离有时被称为阻抗失谐。 + +Hibernate 这样的 **对象关系映射(ORM)** 框架则减少这个转换层所需的样板代码量,但是它们不能完全隐藏这两个模型之间的差异。 + +对于一份简历而言,关系型模型描述一对多的关系需要多张表。 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630640250504-01ef3f97-39be-4c23-9a9e-ce17c1cde6a9.png) 对于简历这样的数据结构,主要是一个自包含的文档,用 JSON 表示非常合适。JSON 相比于多表模式,有更好的局部性,可以一次查询出一个用户的所有信息。JSON 其实是树形层级结构。![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630640396753-c7fed755-b19b-4948-9c84-53d232548633.png) + +#### 多对一和多对多的关系 + +使用 ID 的好处是,因为它对人类没有任何直接意义,所以永远不需要直接改变:即使 ID 标识的信息发生了变化,它也可以保持不变。 + +文档模型不适合表达多对一的关系。对于关系数据库,由于支持联结操作,可以更方便地通过 ID 来引用其他表的行。而在文档数据库中,一对多的树状结构不需要联结,即使支持联结通常也比较弱。 + +如果数据库本身不支持联结,则必须通过对数据库进行多次查询来模拟联结。 + +考虑以下可能对简历进行的修改或补充: + +- 组织和学校作为实体:组织、学校有各自的主页。 +- 推荐:用户可以推荐其他用户在自己的简历上。 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630641413918-4cee1b5a-9bd2-4375-b86a-8d6d7183ee34.png) + +#### 文档数据库是否在重演历史? + +20 世纪 70 年代,最受欢迎的是**层次模型(hierarchical model)**,它与文档数据库使用的 JSON 模型有很多相似之处。它将所有数据表示为嵌套在记录中的记录树。层次模型能很好地支持一对多的关系,但是很难支持多对多的关系,而且不支持联结。 + +为解决层次模型的局限性而提出的方案: + +- **关系模型(relational model)** - 后来,演变成了 SQL,并被广泛接受 +- **网络模型(network model)** - 最初很受关注,但最终被淡忘 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1630641447595-4315fa0f-8338-4596-88d1-e423e040ac62.png) + +##### 网络模型 + +每个记录可能有多个父节点。 + +网络模型中,记录之间的链接不是外键,而更像编程语言中的指针(会存储在磁盘上)。访问记录的唯一方法是选择一条始于根记录的路径,并沿着相关链接一次访问,这条链接链条也被称为**访问路径(access path)**。 + +最简单的情况下,访问路径类似遍历链表:从链表头开始,每次查看一条记录,直到找到所需的记录。但在多对多关系的情况中,存在多条不同的路径可以通向相同的记录,网络模型的程序员必须跟踪这些不同的访问路径。 + +缺点:查询和更新数据库非常麻烦。 + +##### 关系模型 + +关系模型定义了所有数据的格式:**关系(表)** 只是 **元组(行)** 的集合,仅此而已。 + +在关系数据库中,查询优化器自动决定以何种顺序执行查询,以及使用哪些索引。 + +##### 文档数据库的比较 + +文档数据库是某种方式的层次模型:即在其负记录中保存了嵌套记录,而不是存储在单独的表中。 + +但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都由唯一的标识符引用,该标识符在关系模型中被称为**外键**,在文档模型中被称为**文档引用。**标识符可以查询时通过联结操作或相关后续查询来解析。 + +#### 关系数据库与文档数据库现状 + +支持文档数据模型的主要论据是**模式灵活性**,由于局部性而带来较好的性能。关系模型则强在联结操作、多对一和多对多关系更简洁的表达上。 + +##### 哪种数据模型的应用代码更简单 + +文档模型: + +- 优点: + - 如果应用程序中的数据具有类似**文档**的结构(即一对多关系树,通常一次性加载整个树),那么使用文档模型更为合适。而关系模型则倾向于数据分解,把文档结构分解为多个表。 +- 缺点: + - 不能直接引用文档中的嵌套的项目,而是需要说“用户 251 的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,这通常不是问题。 + - 文档数据库对联结的支持不足。这是否是问题取决于应用,如果应用程序使用多对多关系,那么文档模型就没不合适了。 + +对于高度关联的数据,文档模型不太适合,关系模型更适合。 + +##### 文档模型中的模式灵活性 + +文档模型是「读时模式」 + +- 文档数据库有时称为**无模式(schemaless)**,但这具有误导性,因为读取数据的代码通常假定某种结构——即存在隐式模式,但不由数据库强制执行。 +- 一个更精确的术语是**读时模式(schema-on-read)**(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是**写时模式(schema-on-write)**(传统的关系数据库方法中,模式明确,且数据库确保数据写入时都必须遵循)。 +- 读时模式类似于编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时)类型检查。 + +模式变更 + +- 读时模式变更字段很容易,只用改应用代码 +- 写时模式变更字段速度很慢,而且要求停运。它的这种坏名誉并不是完全应得的:大多数关系数据库系统可在几毫秒内执行 ALTER TABLE 语句。MySQL 是一个值得注意的例外,它执行 ALTER TABLE 时会复制整个表,这可能意味着在更改一个大型表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具来解决这个限制。 + +##### 查询的数据局部性 + +文档通常存储为编码为 JSON、XML 或其二进制变体(如 MongoDB 的 BSON)的连续字符串。 + +读文档: + +- 如果应用需要频繁访问整个文档,则存储局部性具有性能优势。 +- 局部性优势仅适用于需要同时访问文档大部分内容的场景。 + +写文档: + +- 更新文档时,通常需要重写整个文档。 +- 通常建议文档应该尽量小且避免写入时增加文档大小。 + +##### 文档数据库与关系数据库的融合 + +- MySQL 等逐步增加了对 JSON 和 XML 的支持 +- 融合关系模型与文档模型是未来数据库发展的一条很好的途径。 + +### 数据查询语言 + +- 关系模型包含了一种查询数据的新方法:SQL 是一种 **声明式** 查询语言,而 IMS 和 CODASYL 使用 **命令式** 代码来查询数据库。 + +- **命令式语言**告诉计算机以特定顺序执行某些操作,比如常见的编程语言。 + +- **声明式查询语言**只需指定所需的数据模式,结果需要满足哪些条件,以及如何转换数据(例如,排序,分组和集合) ,而不需指明如何实现这一目标 + +#### Web 上的声明式查询(略) + +#### MapReduce 查询 + +MapReduce 是一种编程模型,用于在许多机器上批量处理海量数据。一些 NoSQL 支持有限的 MapReduce 方式在大量文档上执行只读查询。 + +### 图数据模型(略) + +### 本章小结 + +历史上,数据最初被表示为一棵大树(层次模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。 最近,开发人员发现一些应用程序也不适合采用关系模型。新的非关系型“NoSQL”数据存储在两个主要方向上存在分歧: + +- 文档数据库的应用场景是:数据来自于自包含文档,且文档之间的关联很少。 +- 图数据库则的应用场景是:所有数据都可能会相互关联。 + +文档模型、关系模型和图模型,都应用广泛。不同模型之间可以相互模拟,但是处理起来比较笨拙。 + +文档数据库和图数据库有一个共同点,那就是它们通常不会对存储的数据强加某个模式,这样比较灵活。 + +## 第三章:存储与检索 + +从最基本的层面看,数据库只需做两件事情:存储和检索。 + +### 数据库核心:数据结构 + +为了高效地查找数据库中特定键的值, 需要新的数据结构: 索引。 + +存储系统的设计权衡:适当的索引可以加速读取查询,但每个索引都会减慢写速度。数据库通常不会对所有内容进行索引。 + +索引类型: + +- 哈希索引 +- B+ 树 +- LSM 树 +- 等等 + +> 扩展阅读:[检索技术核心 20 讲](https://time.geekbang.org/column/intro/100048401) + +### 事务处理与分析处理 + +### 列式存储 + +如果表中有数以万亿行、PB 大小的数据,则适合用于存储在列式存储中。 + +## 第四章:数据编码与演化 + +本章节主要介绍各种序列化、反序列化方式。略 + +### 数据编码格式 + +### 数据流模式 + +向前和向后的兼容对于可演化性来说非常重要。 + +#### 基于数据库的数据流 + +##### 在不同的时间写入不同的值 + +数据库通常支持在不同的时间写入不同的值。 + +在集群中部署新版本是一个逐一的过程,必然存在这样的时间段:集群中部分是新机器,部分是老机器。 + +当旧版本的应用视图更新新版本的应用所写入的数据时,可能会丢失数据。 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1633665482803-0f0f81b1-9abc-4171-b532-577637eecfe6.png) + +##### 归档数据 + +生成数据库快照时,数据转储通常使用最新的模式进行编码。 + +#### 基于服务的数据流:REST 和 RPC + +- 最常见的网络通信方式:C/S 架构(客户端+服务端)。 +- Web 服务:收、发 GET 和 POST 请求。 +- 将大型应用分而治之:微服务架构。 +- 微服务架构的一个关键设计目标:服务可以独立部署和演化。 + +##### Web 服务 + +- 当 HTTP 被用作与服务通信的底层协议时,它被称为 Web 服务 +- 有两种流行的 Web 服务方法:REST 和 SOAP。 + +REST 不是一种协议,而是一个基于 HTTP 原则的设计理念。它强调简单的数据格式,使用 URL 来标识资源,并使用 HTTP 功能进行缓存控制,身份验证和内容类型协商。与 SOAP 相比,REST 已经越来越受欢迎,至少在跨组织服务集成的背景下,并经常与微服务相关。根据 REST 原则设计的 API 称为 RESTful。 + +SOAP 是一种基于 XML 的协议,用于发送网络 API 请求。虽然,它最常用于 HTTP,但其目的是独立于 HTTP,并避免使用大多数 HTTP 功能。SOAP Web 服务的 API 使用 WSDL 语言来描述。 WSDL 支持代码生成,客户端可以使用本地类和方法调用(编码为 XML 消息并由框架再次解码)访问远程服务。尽管 SOAP 及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题。 + +##### 远程过程调用(RPC)的问题 + +RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。 + +RPC 的缺陷: + +- 本地函数调用是可预测的,并且成功或失败仅取决于控制的参数。而网络请求是不可预知的。 +- 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会没有返回结果。这种情况下,无法得知发生了什么。 +- 如果重试失败的网络请求,可能会发生请求实际上已经完成,只有响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非在协议中建立重复数据消除( **幂等(idempotence)**)机制。本地函数调用没有这个问题。 +- 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求慢得多,不可预知。 +- 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当发出网络请求时,所有这些参数都需要被编码成可以通过网络发送的字节序列。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。 +- 客户端和服务端可以用不同的编程语言实现。所以,RPC 框架必须将数据类型从一种语言转换成另一种语言。 + +RPC 比 REST 性能好。但是,REST 更加方便,不限定特定的语言,有更好的通用性。因此,REST 是公共 API 的主流;RPC 框架则侧重于同一组织内多个服务间的请求,且通常在同一数据中心。 + +#### 基于消息传递的数据流 + +##### 消息代理 + +通常,消息代理的使用方式如下: + +生产者向指定的队列或主题发消息;消息代理确保消息被传递给队列或主题的一个或多个消费者或订阅者。同一主题上,可以有多个生产者和多个消费者。 + +##### 分布式 Actor 框架 + +Actor 模型是用于单个进程中并发的编程模型。每个 Actor 通常代表一个客户端或实体,它可能具有某些本地状态,并且它通过发送和接受异步消息与其他 Actor 通信。 + +分布式的 Actor 框架实质上时将消息代理和 Actor 编程模型集成到单个框架中。 + +三种流行的分布式 Actor 框架: + +- Akka 使用 Java 的内置序列化,它不提供向前或向后兼容性。但是,可以用类似 Protocol Buffer 替代; +- Orleans 不支持滚动升级部署的自定义数据编码格式; +- Erlang OTP,很难对记录模式进行更改。 + +### 小结 + +许多服务需要支持滚动升级:向前、向后兼容性。 + +我们讨论了几种数据编码格式及其兼容性属性: + +- 编程语言特定的编码仅限于单一编程语言,往往无法提供前向和后向兼容性。 +- JSON,XML 和 CSV 等文本格式非常普遍,其兼容性取决于您如何使用它们。它们有可选的模式语言,这有时是有用的,有时却是一个障碍。这些格式对某些数据类型的支持有些模糊,必须小心数字和二进制字符串等问题。 +- 像 Thrift,Protocol Buffers 和 Avro 这样的二进制模式驱动格式,支持使用清晰定义的前向和后向兼容性语义进行紧凑,高效的编码。这些模式对于静态类型语言中的文档和非常有用。但是,他们有一个缺点,就是在数据可读之前需要对数据进行解码。 + +我们还讨论了数据流的几种模式,说明了数据编码重要性的不同场景: + +- 数据库,写入数据库的进程对数据进行编码,并从数据库读取进程对其进行解码。 +- RPC 和 REST API,客户端对请求进行编码,服务器对请求进行解码并对响应进行编码,客户端最终对响应进行解码。 +- 异步消息传递(使用消息代理或 Actor),节点之间通过互发消息进行通信,消息由发送者编码并由接收者解码。 + +结论:前向兼容性和滚动升级在某种程度上是可以实现的。 + +## 参考资料 + +- [**数据密集型应用系统设计**](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/01.\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\270\200.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\272\214.md" similarity index 61% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/01.\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\270\200.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\272\214.md" index d392002dbf..ee88f582a0 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/01.\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\270\200.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\225\260\346\215\256\345\257\206\351\233\206\345\236\213\345\272\224\347\224\250\347\263\273\347\273\237\350\256\276\350\256\241\347\254\224\350\256\260\344\272\214.md" @@ -1,5 +1,5 @@ --- -title: 《数据密集型应用系统设计》笔记一之分布式数据系统 +title: 《数据密集型应用系统设计》笔记二 date: 2021-08-26 23:32:00 order: 01 categories: @@ -9,47 +9,26 @@ categories: tags: - 数据库 - 原理 -permalink: /pages/7e2a8f/ +permalink: /pages/c8ba9f57/ --- -# 《数据密集型应用系统设计》笔记一之分布式数据系统 +# 《数据密集型应用系统设计》笔记二 -出于以下目的,我们需要在多台机器上分布数据: +## 第五章:数据复制 -- 扩展性:当数据量或者读写负载巨大,严重超出了单台机器的处理上限,需要将负载分散到多台机器上。 -- 容错与高可用性:当单台机器(或者多台,以及网络甚至整个数据中心)出现故障,还希望应用系统可以继续工作,这时需要采用多台机器提供冗余。这样某些组件失效之后,冗余组件可以迅速接管。 -- 延迟考虑:如果客户遍布世界各地,通常需要考虑在全球范围内部署服务,以方便用户就近访问最近数据中心所提供的服务,从而避免数据请求跨越了半个地球才能到达目标。 +**复制**主要指通过网络在多台机器上保存相同数据的副本。通过复制,可以达到以下目的: -将数据分布在多节点时有两种常见的方式: +- 使数据在地理位置上更接近用户,从而降低访问延迟。如:CDN +- 当部分组件出现故障,系统依然可以继续工作,从而提高可用性。 +- 扩展至多台机器以同事提供数据访问服务,从而提高读吞吐量。 -- **复制**:在多个节点上保存相同数据的副本,每个副本具体的存储位置可能不尽相同。复制方住可以提供冗余 -- **分区**:将一个大块头的数据库拆分成多个较小的子集即分区,不同的分区分配给不同的节点(也称为分片)。我们在第 6 章主要介绍分区技术。 +主流的复制模式:主从复制、多主复制、无主复制。 -## 单主节点数据复制 - -复制主要指通过网络在多台机器上保存相同数据的副本。 - -数据复制的作用: - -- 使数据在地理位置上更接近用户,从而**降低访问延迟**。 -- 当部分组件出现位障,系统依然可以继续工作,从而**提高可用性**。 -- 扩展至多台机器以同时提供数据访问服务,从而**提高读吞吐量**。 - -复制方式: - -- 主从复制 -- 多主节点复制 -- 无主节点复制 - -复制需要考虑的问题: - -- 同步还是异步 -- 如何处理失败的副本 -- 如何保证数据一致 +复制需要考虑的细节:同步复制还是异步复制?如何处理失败的副本(故障转移)?处理策略通常采用可配置项来调整。 ### 主节点与从节点 -每个保存数据库完整数据集的节点称之为副本。有了多副本,必然会面临一个问题:如何确保所有副本之间的数据是一致的? +每个保存数据库完整数据集的节点称之为**副本**。有了多副本,必然会面临一个问题:如何确保所有副本之间的数据是一致的? 主从复制的工作原理如下: @@ -59,20 +38,21 @@ permalink: /pages/7e2a8f/ ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202101.png) -典型应用: +支持主从复制的案例: -- 数据库:MySql、MongoDB 等 +- 关系型数据库:MySql、SQL Server、PostgreSQL 等 +- Nosql:MongoDB、Redis 等 - 消息队列:Kafka、RabbitMQ 等 -### 同步复制与异步复制 +#### 同步复制与异步复制 -基本流程是,客户将更新请求发送给主节点,主节点接收到请求,接下来将数据更新转发给从节点。最后,由 +复制的基本流程是,客户将更新请求发送给主节点,主节点接收到请求,接下来将数据更新转发给从节点。最后,由 主节点来通知客户更新完成。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302202158.png) 通常情况下, 复制速度会非常快,例如多数数据库系统可以在一秒之内完成所有从节点的更新。但是,系统其 -实并没有保证一定会在多段时间内完成复制。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如,由于从节点刚从故障中恢复,或者系统已经接近最大设计上限,或者节点之间的网络出现问题。 +实并没有保证一定会在多长时间内完成复制。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如,由于从节点刚从故障中恢复,或者系统已经接近最大设计上限,或者节点之间的网络出现问题。 - **同步复制的优点**: 一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。 - **同步复制的缺点**:如果同步的从节点无法完成确认(例如由于从节点发生崩愤,或者网络故障,或任何其他原因), 写入就不能视为成功。主节点会阻塞其后所有的写操作,直到同步副本确认完成。 @@ -86,9 +66,9 @@ permalink: /pages/7e2a8f/ 主从复制还经常会被配置为全异步模式。此时如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了写操作, 却无法保证数据的持久化。 -### 配置新的从节点 +#### 配置新的从节点 -配置新的从节点的主要操作步骤: +要做到在不停机、服务不中断的前提下,完成配置新的从节点,主要操作步骤是: 1. 在某个时间点对主节点的数据副本产生一个一致性快照,这样避免长时间锁定整个数据库。目前大多数数据库都支持此功能,快照也是系统备份所必需的。而在某些情况下,可能需要第三方工具, 如 MySQL 的 innobackupex。 2. 将此快照拷贝到新的从节点。 @@ -97,19 +77,19 @@ permalink: /pages/7e2a8f/ 建立新的从副本具体操作步骤可能因数据库系统而异。 -### 处理节点失效 +#### 处理节点失效 如何通过主从复制技术来实现系统高可用呢? -#### 从节点失效: 追赶式恢复 +##### 从节点失效: 追赶式恢复 从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,或者主从节点之间的网络发生暂时中断(闪断),则恢复比较容易,根据副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。 -#### 主节点失效:节点切换 +##### 主节点失效:节点切换 选择某个从节点将其提升为主节点;客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接受来自新的主节点上的变更数据,这一过程称之为切换。 -步骤通常如下: +自动切换的步骤通常如下: 1. **确认主节点失效**。有很多种出错可能性,所以大多数系统都采用了基于超时的机制:节点间频繁地互相发生发送心跳悄息,如果发现某一个节点在一段比较长时间内(例如 30s )没有响应,即认为该节点发生失效。 2. **选举新的主节点**。可以通过选举的方式(超过多数的节点达成共识)来选举新的主节点,或者由之前选定的某控制节点来指定新的主节点。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。让所有节点同意新的主节点是个典型的共识问题。 @@ -119,14 +99,14 @@ permalink: /pages/7e2a8f/ - 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线并加入到集群,接下来的写操作会发生什么?新的主节点很可能会收到冲突的写请求,这是因为原主节点未意识的角色变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是,原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。 - 如果在数据库之外有其他系统依赖于数据库的内容并在一起协同使用,丢弃数据的方案就特别危险。例如,在 GitHub 的一个事故中,某个数据并非完全同步的 MySQL 从节点被提升为主副本,数据库使用了自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点( 即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部 Redis 所引用,结果出现 MySQL 和 Redis 之间的不一致,最后导致了某些私有数据被错误地泄露给了其他用户。 -- 在某些故障情况下,可能会发生两个节点同时-都自认为是主节点。这种情况被称为**脑裂**,它非常危险:两个主节点都可能接受写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏。作为一种安全应急方案,有些系统会采取措施来强制关闭其中一个节点。然而,如果设计或者实现考虑不周,可能会出现两个节点都被关闭的情况。 +- 在某些故障情况下,可能会发生两个节点同时都自认为是主节点。这种情况被称为**脑裂**,它非常危险:两个主节点都可能接受写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏。作为一种安全应急方案,有些系统会采取措施来强制关闭其中一个节点。然而,如果设计或者实现考虑不周,可能会出现两个节点都被关闭的情况。 - 如何设置合适的超时来检测主节点失效呢? 主节点失效后,超时时间设置得越长也意味着总体恢复时间就越长。但如果超时设置太短,可能会导致很多不必要的切换。例如,突发的负载峰值会导致节点的响应时间变长甚至超肘,或者由于网络故障导致延迟增加。如果系统此时已经处于高负载压力或网络已经出现严重拥塞,不必要的切换操作只会使总体情况变得更糟。 -### 复制日志的实现 +#### 复制日志的实现 -#### 基于语句的复制 +##### 基于语句的复制 -最简单的情况,主节点记录所执行的每个写请求(操作语句)井将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个 INSERT 、UPDATE 或 DELETE 语句都会转发给从节点,并且每个从节点都会分析井执行这些 SQU 吾句,如同它们是来自客户端那样。 +最简单的情况,主节点记录所执行的每个写请求(操作语句)井将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个 INSERT 、UPDATE 或 DELETE 语句都会转发给从节点,并且每个从节点都会分析井执行这些 SQL 语句,如同它们是来自客户端那样。 听起来很合理也不复杂,但这种复制方式有一些不适用的场景: @@ -138,7 +118,7 @@ permalink: /pages/7e2a8f/ MySQL 5.1 版本之前采用基于操作语句的复制。现在由于逻辑紧凑,依然在用,但是默认情况下,如果语句中存在一些不确定性操作,则 MySQL 会切换到基于行的复制(稍后讨论)。VoltDB 使用基于语句的复制,它通过事务级别的确定性来保证复制的安全。 -#### 基于预写日志(WAL)传输 +##### 基于预写日志(WAL)传输 通常每个写操作都是以追加写的方式写入到日志中: @@ -147,9 +127,9 @@ MySQL 5.1 版本之前采用基于操作语句的复制。现在由于逻辑紧 不管哪种情况,所有对数据库写入的字节序列都被记入日志。因此可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外, 主节点还可以通过网络将其发送给从节点。 -PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要缺点是日志描述的数据结果非常底层: 一个 WAL 包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密搞合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无能支持主从节点上运行不同版本的软件。 +PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要缺点是日志描述的数据结果非常底层: 一个 WAL 包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本的软件。 -#### 基于行的逻辑日志复制 +##### 基于行的逻辑日志复制 关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求: @@ -163,7 +143,7 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 对于外部应用程序来说,逻辑日志格式也更容易解析。 -#### 基于触发器的复制 +##### 基于触发器的复制 在某些情况下,我们可能需要更高的灵活性。例如,只想复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要订制、管理冲突解决逻辑( 参阅本章后面的“处理写冲突”),则需要将复制控制交给应用程序层。 @@ -173,26 +153,25 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 ### 复制滞后问题 -主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询。对于读操作密集的负载(如 Web ),这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载井允许读取请求就近满足。 +**主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询**。对于读操作密集的负载(如 Web ),这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载井允许读取请求就近满足——读写分离。 在这种扩展体系下,只需添加更多的从副本,就可以提高读请求的服务吞吐量。但是,这种方法实际上只能用于异步复制,如果试图同步复制所有的从副本,则单个节点故障或网络中断将使整个系统无法写入。而且节点越多,发生故障的概率越高,所以完全同步的配置现实中反而非常不可靠。 -不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显的不一致:由于并非所有的写入都反映在从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。经过一段时间之后,从节点最终会赶上并与主节点数据保持一致。这种效应也被称为最终一致性。 +不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显的不一致:由于并非所有的写入都反映在从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。经过一段时间之后,从节点最终会赶上并与主节点数据保持一致。这种效应也被称为**最终一致性**。 #### 读自己的写 许多应用让用户提交一些数据,接下来查看他们自己所提交的内容。例如客户数据库中的记录,亦或者是讨论主题的评论等。提交新数据须发送到主节点,但是当用户读取数据时,数据可能来自从节点。这对于读密集和偶尔写入的负载是个非常合适的方案。 -然而对于异步复制存在这样一个问题,如图 5-3 所示,用户在写人不久即查看数据,则新数据可能尚未到达从节点。对用户来讲, 看起来似乎是刚刚提交的数据丢失了,显然用户不会高兴。 +然而对于异步复制存在这样一个问题,用户在写入不久即查看数据,则新数据可能尚未到达从节点。对用户来讲, 看起来似乎是刚刚提交的数据丢失了。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220302204836.png) 对于这种情况,我们需要强一致性。如何实现呢?有以下方案: -- 如果用户访问可能会被修改的内容,从主节点读取; 否则,在从节点读取。这背后就要求有一些方法在实际执行查询之前,就已经知道内容是否可能会被修改。例如,社交网络上的用户首页信息通常只能由所有者编辑,而其他人无法编辑。因此,这就形成一个简单的规则: 总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件。 +- **如果用户访问可能会被修改的内容,从主节点读取; 否则,在从节点读取**。这背后就要求有一些方法在实际执行查询之前,就已经知道内容是否可能会被修改。例如,社交网络上的用户首页信息通常只能由所有者编辑,而其他人无法编辑。因此,这就形成一个简单的规则: 总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件。 - 如果应用的大部分内容都可能被所有用户修改,那么上述方法将不太有效,它会导致大部分内容都必须经由主节点,这就丧失了读操作的扩展性。此时需要其他方案来判断是否从主节点读取。例如,跟踪最近更新的时间,如果更新后一分钟之内,则总是在主节点读取;井监控从节点的复制滞后程度,避免从那些滞后时间超过一分钟的从节点读取。 -- 客户端还可以记住最近更新时的时间戳,井附带在读请求中,据此信息,系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理,要么等待直到副本接收到了最近的更新。时间戳可以是逻辑时间戳(例如用来指示写入顺序的日志序列号)或实际系统时钟(在这 - 种情况下,时钟同步又称为一个关键点, 请参阅第 8 章“不可靠的时钟”)。 +- 客户端还可以记住最近更新时的时间戳,井附带在读请求中,据此信息,系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理,要么等待直到副本接收到了最近的更新。时间戳可以是逻辑时间戳(例如用来指示写入顺序的日志序列号)或实际系统时钟。 - 如果副本分布在多数据中心(例如考虑与用户的地理接近,以及高可用性),情况会更复杂些。必须先把请求路由到主节点所在的数据中心(该数据中心可能离用户很远)。 如果同一用户可能会从多个设备访问数据,情况会更加复杂。 @@ -204,27 +183,35 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303093658.png) -用户看到了最新内窑之后又读到了过期的内容,好像时间被回拨, 此时需要单调读一致性。 +假设用户从不同副本进行了多次读取,可能会出现:用户看到了最新内容之后又读到了过期的内容,好像时间被回拨, 此时需要单调读一致性。 -单调读一致性可以确保不会发生这种异常。这是一个比强一致性弱,但比最终一致性强的保证。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回攘现象,即在读取较新值之后又发生读旧值的情况。 +单调读一致性可以确保不会发生这种异常。**单调读一致性是一个比强一致性弱,但比最终一致性强的保证**。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回滚现象,即在读取较新值之后又发生读旧值的情况。 实现单调读的一种方式是,确保每个用户总是从固定的同一副本执行读取(而不同的用户可以从不同的副本读取)。 #### 前缀一致读 -前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序。 +**前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序**。 如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生这种反常。然而,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序。这就导致当用户从数据库中读数据时,可能会看到数据库的某部分旧值和另一部分新值。 -## 多主节点复制 +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1634010909693-6cc91fe9-aa0c-4e73-82c9-585646a15281.png) + +一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但该方案效率很低。此外,还有一些算法来显示地跟踪事件因果关系(Happend Before)。 + +#### 复制滞后的解决方案 + +解决方案是分布式事务。 -主从复制存在一个明显的缺点:系统只有一个主节点,而所有写人都必须经由主节点。主从复制方案就会影响所有的写入操作。 +### 多主节点复制 -### 适用场景 +主从复制存在一个明显的缺点:系统只有一个主节点,而所有写入都必须经由主节点。主从复制方案就会影响所有的写入操作。典型代表:ZooKeeper。 + +#### 适用场景 在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。 -以下场景这种配置则是合理的: +但是,以下场景这种配置则是合理的: - 多数据中心 - 离线客户端操作 @@ -232,15 +219,35 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 #### 多数据中心 -部署单主节点的主从复制方案与多主复制方案之间的差异 +为了容忍整个数据中心级别故障或更接近用户,可以把数据库的副本横跨多个数据中心。 + +在每个数据中心内,采用常规的主从复制;而在数据中心之间,由各数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1634094581707-9d2d4ff2-0d5d-43b2-8210-2cf7b2d16684.png) + +主从复制和多主复制的差异: - **性能**:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,井基本偏离了采用多数据中心的初衷(即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。因此,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。 - **容忍数据中心失效**:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。 - **容忍网络问题**:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络可靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。 -### 处理写冲突 +#### 离线客户端操作 + +- 多主复制的另一适用场景:应用在断网后仍然需要继续工作。典型代表:各种电子笔记软件。 +- 在这种情况下,每个设备都有一个充当领导者的本地数据库(用来接受写请求),然后在所有设备上采用异步方式同步这些多主节点的副本,同步滞后可能是几小时或数天。 +- 从架构层面来看,每个设备相当于一个“数据中心”。 + +#### 协同编辑 + +实时协作编辑应用允许多个用户同时编辑文档。 + +协同编辑和数据库复制有很多相似之处:用户对文档的编辑立即应用到其本地副本,并异步复制到服务器和编辑同一文档的其他用户。 + +如果想避免编辑发生冲突,则应该对编辑的内容加锁,此时其他用户不能编辑该内容。为了减少锁冲突,锁的粒度应尽可能小:可能是 word 文档的一行内容,也可能是 Excel 的某一单元格。 -多主复制的最大问题是可能发生写冲突。 +#### 处理写冲突 + +**多主复制的最大问题是可能发生写冲突**。 #### 同步与异步冲突检测 @@ -260,28 +267,66 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 实现收敛的冲突解决有以下可能的方式: -- 给每个写入分配唯一的 ID ,例如, 一个时间戳, 二个足够长的随机数, 一个 UUID 或者一个基于键-值的 Jl 合希,挑选最高 ID 的写入作为胜利者,并将其他写入丢弃。如果基于时间戳,这种技术被称为最后写入者获胜。虽然这种方陆很流行,但是很容易造成数据丢失。 -- 为每个副本分配一个唯一的 ID ,井制定规则,例如序号高的副本写入始终优先于序号低的副本。这种方法也可能会导致数据丢失。 -- 以某种方式将这些值合并在一起。例如,按字母顺序排序,然后拼接在一起 -- 利用预定义好的格式来记录和保留冲突相关的所有信息,然后依靠应用层的逻辑,事后解决冲突(可能会提示用户) 。 +- **给每个写入分配唯一的 ID** ,例如, 一个时间戳, 二个足够长的随机数, 一个 UUID 或者一个基于键-值的 Jl 合希,**优先挑选最高 ID 的写入**,并将其他写入丢弃。如果基于时间戳,这种技术被称为最后写入者获胜。虽然这种方陆很流行,但是很容易造成数据丢失。 +- **为每个副本分配一个唯一的 ID ,井制定规则,例如序号高的副本写入始终优先于序号低的副本**。这种方法也可能会导致数据丢失。 +- **以某种方式将这些值合并在一起**。例如,按字母顺序排序,然后拼接在一起 +- **利用预定义好的格式来记录和保留冲突相关的所有信息,然后依靠应用层的逻辑,事后解决冲突**(可能会提示用户) 。 #### 自定义冲突解决逻辑 解决冲突最合适的方式可能还是依靠应用层,所以大多数多主节点复制模型都有工具来让用户编写应用代码来解决冲突。可以在写入时或在读取时执行这些代码逻辑: -- 在写入时执行:只要数据库系统在复制变更日志时检测到冲突,就会调用应用层的冲突处理程序。 -- 在读取时执行:当检测到冲突时,所有冲突写入值都会暂时保存下来。下一次读取数据时,会将数据的多个版本读返回给应用层。应用层可能会提示用户或自动解决冲突, 井将最后的结果返回到数据库。 +- **在写入时执行**:只要数据库系统在复制变更日志时检测到冲突,就会调用应用层的冲突处理程序。 +- **在读取时执行**:当检测到冲突时,所有冲突写入值都会暂时保存下来。下一次读取数据时,会将数据的多个版本读返回给应用层。应用层可能会提示用户或自动解决冲突, 井将最后的结果返回到数据库。 + +#### 什么是冲突? + +- 显而易见的冲突:两个写操作并发地修改了同一条记录中的同一个字段。 +- 微秒的冲突:一个房间接受了两个预定。 + +### 拓扑结构 + +- **复制拓扑**(replication topology)描述写入从一个节点传播到另一个节点的通信路径。 +- 只有两个领导者时,只有一个合理的拓扑:互相写入。 +- 当有两个以上的领导,拓扑很多样: + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1634524795875-25e044c1-fc7d-47dc-a871-1af2fb3f5edd.png) + +- 最普遍的是全部到全部; +- MySQL 仅支持环形拓扑。 + +防止无限复制循环: + +- 圆形和星型拓扑,节点需要转发从其他节点收到的数据更改。 +- 防止无限复制循环:每个节点都有唯一的标识符,在复制日志中,每个写入都标记了所有已经过的节点的标识符。 + +环形和星形拓扑的问题 -## 无主节点复制 +- 一个节点故障,可能中断其他节点之间的复制消息流。 +- 拓扑结构可以重新配置,但是需要手动操作。 +- 全部到全部的容错性更好,避免单点故障。 + +全部到全部拓扑的问题 + +- 网络问题导致消息顺序错乱 + +![image.png](https://picture-bed-1251805293.file.myqcloud.com/1634525242078-5d57538c-fd13-4851-8962-c1934768e7fb.png) + +- 写入时添加时间戳是不够的。 +- 解决办法是**版本向量技术**。 +- 有些数据库没有该功能。 + +### 无主复制 客户端将写请求发送到多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。 -## 数据分区 +## 第六章:分区 -> 在不同系统中,分区有着不同的称呼,例如它对应于 MongoDB, Elasticsearch 和 SolrCloud 中的 shard, HBase 的 region, Bigtable 中的 -> tablet, Cassandra 和 Riak 中的 vnode ,以及 Couch base 中的 vBucket。总体而言,分区是最普遍的术语。 +> 在不同系统中,分区有着不同的称呼,例如它对应于 MongoDB, Elasticsearch 和 SolrCloud 中的 shard, HBase 的 region, Bigtable 中的 tablet, Cassandra 和 Riak 中的 vnode ,以及 Couch base 中的 vBucket。总体而言,分区是最普遍的术语。 -采用数据分区的主要目的是提高可扩展性。不同的分区可以放在不同的节点上,这样一个大数据集可以分散在更多的磁盘上,查询负载也随之分布到更多的处理器上。 +分区定义:每条数据只属于特定分区。 + +采用数据分区的主要目的是**提高可扩展性**。不同的分区可以放在一个无共享集群的不同节点上,这样一个大数据集可以分散在更多的节点上,查询负载也随之分散。 对单个分区进行查询时,每个节点对自己所在分区可以独立执行查询操作,因此添加更多的节点可以提高查询吞吐量。超大而复杂的查询尽管比较困难,但也可能做到跨节点的并行处理。 @@ -289,13 +334,15 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而同样的内容会保存在不同的节点上以提高系统的容错性。 -一个节点上可能存储了多个分区。每个分区都有自己的主副本,例如被分配给某节点,而从副本则分配在其他一些节点。一个节点可能即是某些分区的主副本,同时又是其他分区的从副本。 +一个节点上可能存储了多个分区。每个分区都有自己的主副本,而从副本则分配在其他一些节点。一个节点可能既是某些分区的主副本,同时又是其他分区的从副本。 ### 键-值数据的分区 分区的主要目标是将数据和查询负载均匀分布在所有节点上。如果节点平均分担负载,那么理论上 10 个节点应该能够处理 10 倍的数据量和 10 倍于单个节点的读写吞吐量(忽略复制) 。 -而如果分区不均匀,则会出现某些分区节点比其他分区承担更多的数据量或查询负载,称之为倾斜。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10 个节点 9 个空闲,系统的瓶颈在最繁忙的那个节点上。这种负载严重不成比例的分区即成为系统热点。 +而如果分区不均匀,则会出现某些分区节点比其他分区承担更多的数据量或查询负载,称之为**倾斜**。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10 个节点 9 个空闲,系统的瓶颈在最繁忙的那个节点上。这种负载严重不成比例的分区即成为系统热点。 + +避免热点最简单的方法是将记录随机分配给所有节点。这种方法的缺点是:当视图读取特定数据时,无法知道数据保存在哪个节点,所以不得不并行查询所有节点。 #### 基于关键字区间分区 @@ -303,25 +350,35 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 关键字的区间段不一定非要均匀分布,这主要是因为数据本身可能就不均匀。 -每个分区内可以按照关键字排序保存(参阅第 3 章的“ SSTables 和 LSM Trees ”)。这样可以轻松支持区间查询,即将关键字作为一个拼接起来的索引项从而一次查询得到多个相关记录。 +分区边界可以由手动选择或自动选择。采用这种分区策略的数据存储有:Bigtable、HBase、RethinkDB、2.4 版本前的 MongoDB。 -然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围,所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。为了避免上述问题,需要使用时间戳以外的其他内容作为关键字的第一项。 +每个分区内可以按照关键字排序保存。这样可以轻松支持区间查询,即将关键字作为一个拼接起来的索引项从而一次查询得到多个相关记录。 + +然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围。所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。为了避免上述问题,需要使用时间戳以外的其他内容作为关键字的第一项。 #### 基于关键字晗希值分区 -一且找到合适的关键宇 H 合希函数,就可以为每个分区分配一个哈希范围(而不是直接作用于关键宇范围),关键字根据其哈希值的范围划分到不同的分区中。 +一个好的哈希函数可以处理数据倾斜并使其均匀分布。 + +用于数据分区目的的哈希函数不需要在加密方面很强。 + +一且找到合适的关键宇哈希函数,就可以为每个分区分配一个哈希范围(而不是直接作用于关键宇范围),关键字根据其哈希值的范围划分到不同的分区中。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303105925.png) -这种方总可以很好地将关键字均匀地分配到多个分区中。分区边界可以是均匀间隔,也可以是伪随机选择( 在这种情况下,该技术有时被称为一致性哈希) 。 +这种方总可以很好地将关键字均匀地分配到多个分区中。分区边界可以是均匀间隔,也可以是伪随机选择( 在这种情况下,该技术有时被称为**一致性哈希**) 。 + +然而,通过关键宇哈希进行分区,我们丧失了良好的区间查询特性。即使关键字相邻,但经过哈希之后会分散在不同的分区中,区间查询就失去了原有的有序相邻的特性。在 MongoDB 中,如果启用了基于哈希的分片模式,则区间查询会发送到所有的分片上,而 Riak、Couchbase 和 Voldemort 直接就不支持关键字上的区间查询。 -然而,通过关键宇 II 合希进行分区,我们丧失了良好的区间查询特性。即使关键字相邻,但经过哈希之后会分散在不同的分区中,区间查询就失去了原有的有序相邻的特性。 +Cassandra 中的表可以声明为由多个列组成的复合主键。复合主键只有第一部分可用于哈希分区,而其他列则用作组合索引来对 Cassandra SSTable 中的数据进行排序。 #### 负载倾斜与热点 -基于哈希的分区方能可以减轻热点,但无住做到完全避免。一个极端情况是,所有的读/写操作都是针对同一个关键字,则最终所有请求都将被路由到同一个分区。 +基于哈希的分区方能可以减轻热点,但无住做到完全避免。一个极端情况是,所有的读/写操作都是针对同一个关键字,则最终所有请求都将被路由到同一个分区。典型:名人的热点事件(关键字是名人的 ID)。 + +一个简单的规避热点的技术就是:在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到 100 个不同的关键字上,从而分配到不同的分区上。 -一个简单的技术就是在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到 100 个不同的关键字上,从而分配到不同的分区上。但是,随之而来的问题是,之后的任何读取都需要些额外的工作,必须从所有 100 个关键字中读取数据然后进行合井。因此通常只对少量的热点关键字附加随机数才有意义。 +但是,随之而来的问题是,之后的任何读取都需要些额外的工作,必须从所有 100 个关键字中读取数据然后进行合井。因此通常只对少量的热点关键字附加随机数才有意义;而对于写入吞吐量低的绝大多数关键宇,这些都意味着不必要的开销。此外,还需要额外的元数据来标记哪些关键字进行了特殊处理 。 ### 分区与二级索引 @@ -335,61 +392,64 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 在这种索引方法中,每个分区完全独立,各自维护自己的二级索引,且只负责自己分区内的文档而不关心其他分区中数据。每当需要写数据库时,包括添加,删除或更新文档等,只需要处理包含目标文档 ID 的那一个分区。因此文档分区索引也被称为本地索引,而不是全局索引。 -这种查询分区数据库的方陆有时也称为分散/聚集,显然这种二级索引的查询代价高昂。即使采用了并行查询,也容易导致读延迟显著放大。尽管如此,它还是广泛用于实践: MongoDB 、Riak、Cassandra、Elasticsearch 、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。 +这种查询分区数据库的方法有时也称为分散/聚集,显然这种二级索引的查询代价高昂。即使采用了并行查询,也容易导致读延迟显著放大。尽管如此,它还是被广泛应用: MongoDB 、Riak、Cassandra、Elasticsearch 、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。 #### 基于词条的二级索引分区 可以对所有的数据构建全局索引,而不是每个分区维护自己的本地索引。 -为避免成为瓶颈,不能将全局索引存储在一个节点上,否则就破坏了设计分区均衡的目标。所以,全局索引也必须进行分区,且可以与数据关键字采用不同的分区策略。 +为避免成为瓶颈,不能将全局索引存储在一个节点上,否则就破坏了设计分区均衡的目标。所以,**全局索引也必须进行分区**,且可以与数据关键字采用不同的分区策略。 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220303112708.png) -可以直接通过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是可以支持高效的区间查询;而采用哈希的方式则可以更均句的划分分区。 +可以直接通过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是可以支持高效的区间查询;而采用哈希的方式则可以更均匀的划分分区。 -这种全局的词条分区相比于文档分区索引的主要优点是,它的读取更为高效,即它不需要采用 scatter/gather 对所有的分区都执行一遍查询。 +词条索引分区 vs. 文档索引分区 -然而全局索引的不利之处在于, 写入速度较慢且非常复杂,主要因为单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能完全不同甚至在不同的节点上,由此势必引人显著的写放大。 +- 优点:它的**读取更为高效**,客户端不需要对所有的分区都执行一遍查询,只需要向包含词条的那一个分区发出读请求。 +- 缺点:**写入速度较慢且非常复杂**,主要因为单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能完全不同甚至在不同的节点上,由此势必引入显著的写放大。 -理想情况下,索引应该时刻保持最新,即写入的数据要立即反映在最新的索引上。但是,对于词条分区来讲,这需要一个跨多个相关分区的分布式事务支持,写入速度会受到极大的影响,所以现有的数据库都不支持同步更新二级索引。 +理想情况下,索引应该时刻保持最新,即写入的数据要立即反映在最新的索引上。但是,对于词条分区来讲,这需要一个跨多个相关分区的分布式事务支持,写入速度会受到极大的影响,所以现有的数据库都不支持同步更新二级索引。**对全局 二级索引的更新往往都是异步的** 。 ### 分区再均衡 随着时间的推移,数据库可能总会出现某些变化: -- 查询压力增加,因此需要更多的 C PU 来处理负载。 -- 数据规模增加,因此需要更多的磁盘和内存来存储数据。 -- 节点可能出现故障,因此需要其他机器来接管失效的节点。 +- **查询压力增加**,因此需要更多的 C PU 来处理负载。 +- **数据规模增加**,因此需要更多的磁盘和内存来存储数据。 +- **节点可能出现故障**,因此需要其他机器来接管失效的节点。 -所有这些变化都要求数据和请求可以从一个节点转移到另一个节点。这样一个迁移负载的过程称为再平衡(或者动态平衡)。无论对于哪种分区方案, 分区再平衡通常至少要满足: +所有这些变化都要求数据和请求可以从一个节点转移到另一个节点。这样一个迁移负载的过程称为再均衡(或者动态均衡)。无论对于哪种分区方案, 分区再均衡通常至少要满足: -- 平衡之后,负载、数据存储、读写请求等应该在集群范围更均匀地分布。 -- 再平衡执行过程中,数据库应该可以继续正常提供读写服务。 -- 避免不必要的负载迁移,以加快动态再平衡,井尽量减少网络和磁盘 I/O 影响。 +- 均衡之后,负载、数据存储、读写请求等应该在集群范围**更均匀地分布**。 +- **再均衡执行过程中**,数据库应该**可以继续正常提供读写服务**。 +- **避免不必要的负载迁移**,以加快动态再均衡,井尽量减少网络和磁盘 I/O 影响。 -#### 动态再平衡的策略 +#### 动态再均衡的策略 ##### 为什么不用取模? 最好将哈希值划分为不同的区间范围,然后将每个区间分配给一个分区。 -为什么不直接使用 mod,对节点数取模方怯的问题是,如果节点数 N 发生了变化,会导致很多关键字需要从现有的节点迁移到另一个节点。 +对节点数取模方法的问题是:如果节点数 N 发生了变化,会导致很多关键字需要从现有的节点迁移到另一个节点。 ##### 固定数量的分区 创建远超实际节点数的分区数,然后为每个节点分配多个分区。 -接下来, 如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。 +如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。 + +选中的整个分区会在节点之间迁移,但分区的总数量仍维持不变, 也不会改变关键字到分区的映射关系。这里唯一要调整的是分区与节点的对应关系。考虑到节点间通过网络传输数据总是需要些时间,这样调整可以逐步完成,在此期间,旧的分区仍然可以接收读写请求。 ##### 动态分区 -对于采用关键宇区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。 +对于采用关键字区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。 -因此, 一些数据库如 HBas e 和 RethinkDB 等采用了动态创建分区。当分区的数据增长超过一个可配的参数阔值( HBase 上默认值是 lOGB ),它就拆分为两个分区,每个承担一半的数据量[26]。相反,如果大量数据被删除,并且分区缩小到某个阑值以下,则将其与相邻分区进行合井。 +一些数据库如 HBase 和 RethinkDB 等采用了动态创建分区。当分区的数据增长超过一个可配的参数阔值( HBase 默认值是 10 GB ),它就拆分为两个分区,每个承担一半的数据量;相反,如果大量数据被删除,并且分区缩小到某个阈值以下,则将其与相邻分区进行合井。 -每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。 +每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。对于 HBase ,分区文件的传输需要借助 HDFS。 -但是,需要注意的是,对于一个空的数据库, 因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理, 而其他节点则处于空闲状态。 +需要注意的是,对于一个空的数据库, 因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理, 而其他节点则处于空闲状态。为了缓解这个问题,HBase 和 MongoDB 允许在一个空的数据库上配置一组初始分区(这被称为预分裂)。 ##### 按节点比例分区 @@ -397,7 +457,7 @@ PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要 Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每个节点具有固定数量的分区。此时, 当节点数不变时,每个分区的大小与数据集大小保持正比的增长关系; 当节点数增加时,分区则会调整变得更小。较大的数据量通常需要大量的节点来存储,因此这种方陆也使每个分区大小保持稳定。当一个 -新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界) +新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界)。 #### 自动与手动再平衡操作 @@ -407,9 +467,9 @@ Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点 ### 请求路由 -处理策略 +路由处理策略 -1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,贝 lj 直接处理该请求:否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。 +1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,则直接处理该请求:否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。 2. 将所有客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层本身不处理任何请求,它仅充一个分区感知的负载均衡器。 3. 客户端感知分区和节点分配关系。此时,客户端可以直接连接到目标节点,而不需要任何中介。 @@ -419,19 +479,41 @@ Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点 ![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220304163629.png) -## 事务 +Linkedln的Espresso使用 Helix进行集群管理(底层是ZooKeeper ), 实现了请求路由 层。 HBase, SolrCloud和Kafka也使用 ZooKeeper来跟踪分区分配情况 。 MongoDB有类似的设计,但它依赖于自己的配置服务器和mongos守护进程来充当路由层。 + +Cassandra和 Riak 在节点之间使用 gossip协议来同步群集状态的变化。请求可以发送到任何节点,由该节点负责将其转发到目标分区节点。这种方式增加了数据库节点的复杂性,但是避免了对ZooKeeper之类的外部协调服务的依赖。 + +### 小结 + +分区的目地是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。 + +- 基于关键字区间的分区 - 先对关键字进行排序,每个分区只负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以**支持高效的区间查询**,但是如果应用程序经常访问与排序一致的某段关键字,就会**存在热点**的风险。采用这种方怯,当分区太大时,通常将其**分裂**为两个子区间,从而动态地再平衡分区。 +- 哈希分区 - 将哈希函数作用于每个关键字,每个分区负责一定范围的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载。采用哈希分区时,通常事先创建好足够多(但固定数量)的分区, 让每个节点承担多个分区,当添加或删除节点时将某些分区从一个节点迁移到另一个节点,也可以支持动态分区。 + +混合上述两种基本方法也是可行的,例如使用复合键:键的一部分来标识分区,而另一部分来记录排序后的顺序 。 + +二级索引也需要进行分区,有两种方法: + +- **基于文档来分区二级索引**。 二级索引存储在与关键字相 同的分区中 ,这意味着写入时我们只需要更新一个分区,但缺点是读取二级索引时需要在所有分区上执行scatter/gather。 +- **基于词条来分区二级索引**。它是基于索引的值而进行的独立分区。二级索引中的条目可能包含来自关键字的多个分区里的记录。在写入时 ,不得不更新二级索引的多个分区;但读取时 ,则可以从单个分区直接快速提取数据。 + +## 第七章:事务 事务中的所有读写是一个执行的整体,整事务要么成功(提交)、要么失败(中止或回滚)。如果失败,应用程序可以安全地重试。 ### 深入理解事务 -ACID:原子性( Atomicity ), 一致性( Consistency ),隔离性( Isolation )与持久性( Durability ) +ACID,分别代表原子性( Atomicity ), 一致性( Consistency ),隔离性( Isolation )与持久性( Durability )的首字母。 + +#### 原子性 -### 若隔离级别 +原子是指不可分解为更小粒度的东西。 + +### 弱隔离级别 ### 串行化 -## 分布式系统的挑战 +## 第八章:分布式系统的挑战 **所有可能出错的事情一定会出错**。 @@ -449,65 +531,10 @@ ACID:原子性( Atomicity ), 一致性( Consistency ),隔离性( 检测到错误之后,让系统容忍失效也不容易。在典型的分布式环境下,没有全局变量, 没有共享内存,没有约定的尝试或其他跨节点的共享状态。节点甚至不太清楚现在的准确时间, 更不用说其他更高级的了。信息从一个节点流动到另一个节点只能是通过不可靠的网络来发送。单个节点无住安全的做出任何决策,而是需要多个节点之 间的共识协议,井争取达到法定票数(通常为半数以上)。 -## 一致性和共识 +## 第九章:一致性与共识 分布式系统最重要的抽象之一就是共识: 所有的节点就某一项提议达成一致。 -### 一致性保证 - -分布式一致性则主要是针对延迟和故障等问题来协调副本之间的状态。 - -### 可线性化 - -在最终一致性数据库中,同时查陶两个不同的副本可能会得到两个不同的答案。 - -线性化(一种流行的一致性模型) : 其目标是使多副本对外看起来好像是单一副本,然后所有操作以原子方 -式运行,就像一个单线程程序操作变量一样。 - -在有些场景下,线性化对于保证系统正确工作至关重要。 - -**加锁与主节点选举**:主从复制的系统需要确保有且只有一个主节点,否则会产生脑裂。选举新的主节点常见的方住是使用锁:即每个启动的节点都试图获得锁,其中只有一个可以成功即成为主节点 。不管锁具体如何实现,它必须满足可线性化:所有节点都必须同意哪个节点持有锁,否则就会出现问题。 - -**约束与唯一性保证**:常见如关系型数据库中主键的约束,则需要线性化保证。其他如外键或属性约束,则并不要求一定线性化 。 - -**跨通道的时间依赖**: - -#### 实现线性化系统 - -线性化本质上意味着“表现得好像只有一个数据副本,且其上的所有操作都是原子的,所以最简单的方案自然是只用一个数据副本。 - -- 主从复制(部分支持可线性化):只有主节点承担数据写入,从节点则在各自节点上维护数据的备份副本。如果从主节点或者同步更新的从节点上读取,则可以满足线性化 -- 共识算挂(可线性化):共识协议通常内置一些措施来防止裂脑和过期的副本。 -- 多主复制(不可线性化):具有多主节点复制的系统通常无怯线性化的,主要由于它们同时在多个节点上执行并发写入,并将数据异步复制到其他节点。因此它们可能会产生冲突的写入。 -- 无主复制(可能不可线性化) - -#### 线性化的代价 - -多主复制非常适合多数据中心。 - -如果两个数据中心之间发生网络中断,会发生什么情况? - -基于多主复制的数据库,每个数据中心内都可以继续正常运行: 由于从一个数据中心到另一个数据中心的复制是异步,期间发生的写操作都暂存在本地队列,等网络恢复之后再继续同步。 - -与之对比,如果是主从复制,则主节点肯定位于其中的某一个数据中心。所有写请求和线性化读取都必须发送给主节点,因此,对于那些连接到非主节点所在数据中心的客户端,读写请求都必须通过数据中心之间的网络,同步发送到主节点所在的数据中。 - -对于这样的主从复制系统,数据中心之间的网络一旦中断,连接到从数据中心的客户端无怯再联系上主节点,也就无法完成任何数据库写入和线性化读取。从节点可以提供读服务,但内容可能是过期的(非线性化保证)。 - -#### CAP 理论 - -### 顺序保证 - -因果关系对事件进行了某种排序(根据事件发生的原因-结果依赖关系)。线性化是将所有操作都放在唯一的、全局有序时间线上,而因果性则不同,它为我们提供了一个弱一致性模型: 允许存在某些井发事件,所以版本历史 -是一个包含多个分支与合井的时间线。因果一致性避免了线性化昂贵的协调开销,且对网络延迟的敏感性要低很多。 - -### 分布式事务与共识 - -共识意味着就某一项提议,所有节点做出一致的决定,而且决定不可撤销。 - -如果系统只存在一个节点, 或者愿意把所有决策功能者都委托给某一个节点,那么事情就变得很简单。这和主从复制数据库的情形是一样的,即由主节点负责所有的决策事直,正因如此,这样的数据库可以提供线性化操作、H 住一性约束、完全有序的复制日志等。 - -然而,如果唯一的主节点发生故障,或者出现网络中断而导致主节点不可达,这样的系统就会陷入停顿状态。有以下三种基本思路来处理这种情况: +## 参考资料 -- 系统服务停止,等待主节点恢复 -- 人为介入选择新主节点,并重新配置系统使之生效 -- 采用算 i 法来自动选择新的主节点。这需要一个共识算法 \ No newline at end of file +- [**数据密集型应用系统设计**](https://book.douban.com/subject/30329536/) - 这可能是目前最好的分布式存储书籍,强力推荐【进阶】 diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/01.\345\210\206\345\270\203\345\274\217\345\215\217\350\256\256\344\270\216\347\256\227\346\263\225\345\256\236\346\210\230\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\210\206\345\270\203\345\274\217\345\215\217\350\256\256\344\270\216\347\256\227\346\263\225\345\256\236\346\210\230\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/01.\345\210\206\345\270\203\345\274\217\345\215\217\350\256\256\344\270\216\347\256\227\346\263\225\345\256\236\346\210\230\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\210\206\345\270\203\345\274\217\345\215\217\350\256\256\344\270\216\347\256\227\346\263\225\345\256\236\346\210\230\347\254\224\350\256\260.md" index 0726e6a25b..0809338998 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/01.\345\210\206\345\270\203\345\274\217\345\215\217\350\256\256\344\270\216\347\256\227\346\263\225\345\256\236\346\210\230\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\210\206\345\270\203\345\274\217\345\215\217\350\256\256\344\270\216\347\256\227\346\263\225\345\256\236\346\210\230\347\254\224\350\256\260.md" @@ -1,18 +1,18 @@ --- -title: 《分布式协议与算法实战》笔记 +title: 《极客时间教程 - 分布式协议与算法实战》笔记 date: 2022-06-27 11:49:01 order: 01 categories: - 笔记 - 分布式 - - 分布式理论 + - 分布式综合 tags: - 分布式 - 理论 -permalink: /pages/53d3f7/ +permalink: /pages/b0ef7561/ --- -# 《分布式协议与算法实战》笔记 +# 《极客时间教程 - 分布式协议与算法实战》笔记 ## 拜占庭将军问题 diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/02.\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\347\256\227\346\263\225\350\247\243\346\236\220\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\347\256\227\346\263\225\350\247\243\346\236\220\347\254\224\350\256\260.md" similarity index 98% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/02.\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\347\256\227\346\263\225\350\247\243\346\236\220\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\347\256\227\346\263\225\350\247\243\346\236\220\347\254\224\350\256\260.md" index 0888a18442..11166aa190 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/01.\345\210\206\345\270\203\345\274\217\347\220\206\350\256\272/02.\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\347\256\227\346\263\225\350\247\243\346\236\220\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\344\270\216\347\256\227\346\263\225\350\247\243\346\236\220\347\254\224\350\256\260.md" @@ -1,18 +1,18 @@ --- -title: 《分布式技术原理与算法解析》笔记 +title: 《极客时间教程 - 分布式技术原理与算法解析》笔记 date: 2023-06-07 13:49:02 order: 02 categories: - 笔记 - 分布式 - - 分布式理论 + - 分布式综合 tags: - 分布式 - 理论 -permalink: /pages/80055a/ +permalink: /pages/57d0896b/ --- -# 《分布式技术原理与算法解析》笔记 +# 《极客时间教程 - 分布式技术原理与算法解析》笔记 ## 开篇词丨四纵四横,带你透彻理解分布式技术 diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\265\205\345\207\272\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\265\205\345\207\272\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..b08c7ade11 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/00.\345\210\206\345\270\203\345\274\217\347\273\274\345\220\210/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\267\261\345\205\245\346\265\205\345\207\272\345\210\206\345\270\203\345\274\217\346\212\200\346\234\257\345\216\237\347\220\206\347\254\224\350\256\260.md" @@ -0,0 +1,310 @@ +--- +title: 《极客时间教程 - 深入浅出分布式技术原理》笔记 +date: 2024-05-07 06:30:37 +order: 03 +categories: + - 笔记 + - 分布式 + - 分布式综合 +tags: + - 分布式 + - 理论 +permalink: /pages/c97b3a9c/ +--- + +# 《极客时间教程 - 深入浅出分布式技术原理》笔记 + +## 开篇词 掌握好学习路径,分布式系统原来如此简单 + +## 导读:以前因后果为脉络,串起网状知识体系 + +### 分布式系统解决了什么问题 + +- 首先,分布式系统解决了单机性能瓶颈导致的成本问题。——水平扩展 +- 然后,解决了用户量和数据量爆炸性地增大导致的成本问题。——水平扩展 +- 接着,满足了业务高可用的要求。——解决单点问题,鸡蛋不要都放在一个篮子里 +- 最后,分布式系统解决了大规模软件系统的迭代效率和成本的问题。——分而治之,化繁为简 + +### 如何思考和处理分布式系统引入的新问题 + +- 怎么找到服务——服务注册和发现 +- 怎么找到实例——路由、负载均衡 +- 怎么管理配置——配置中心 +- 怎么进行协同——分布式锁 +- 怎么确保请求只执行一次——重试+幂等 +- 怎么避免雪崩——限流、熔断、降级、快速失败、弹性扩容 +- 怎么监控告警和故障恢复——分布式链路追踪 + +### 分布式存储如何内部协调 + +- 首先,理解 ACID、CAP、BASE +- 然后,确定分片策略,常见方案有 Hash、Region 分片 +- 接着,确定复制方案,常见方案有: + - 中心化方案:主从复制、一致性协议,比如 Raft 和 Paxos 等 + - 去中心化方案: Quorum 和 Vector Clock +- 最后,如何处理分布式事务 + - 分布式 ID + - 2PC、3PC 等分布式事务方案 + +## 新的挑战:分布式系统是银弹吗?我看未必! + +- 故障处理 +- 网络不可靠——超时处理 +- 时间不可靠——NTP、逻辑时钟 +- 共识协同——共识性算法 + +## CAP 理论:分布式场景下我们真的只能三选二吗? + +**在一个分布式系统中,当发生网络分区时,那么强一致性和可用性只能二选一**。 + +## 注册发现: AP 系统和 CP 系统哪个更合适? + +服务注册的关键: + +- **统一的中介存储**:调用方在唯一的地方获得被调用服务的所有实例的信息。 +- **状态更新与通知**:服务实例的信息能够及时更新并且通知到服务调用方。 + +注册中心的特性要求: + +- **可用性要求非常高**:因为服务注册发现是整个分布式系统的基石,如果它出现问题,整个分布式系统将不可用。 +- **性能要求中等**:只要设计得当,整体的性能要求还是可控的,不过需要注意的是性能要求会随分布式系统的实例数量变多而提高。 +- **数据容量要求低**:因为主要是存储实例的 IP 和 Port 等元数据,单个实例存储的数据量非常小。 +- **API 友好程度**:是否能很好支持服务注册发现场景的“发布/订阅”模式,将被调用服务实例的 IP 和 Port 信息同步给调用方。 + +注册中心选择 AP 还是 CP: + +因为服务发现是整个分布式系统的基石,所以可用性是最关键的设计目标。 + +## 负载均衡:从状态的角度重新思考负载均衡 + +负载均衡策略: + +- 轮询 +- 随机 +- 加权轮询/随机 +- 最少连接/请求 +- 最少响应时间 +- Hash +- 一致性 Hash +- 虚拟一致性 Hash + +## 配置中心:如何确保配置的强一致性呢? + +配置中心的关键挑战: + +- **统一的配置存储**:一个带版本管理的存储系统,按服务的维度,存储和管理整个分布式系统的配置信息,这样可以很方便地对服务的配置信息,进行搜索、查询和修改。 +- **配置信息的同步**:所有的实例,本地都不存储配置信息,实例能够从配置中心获得服务的配置信息,在配置修改后,能够及时将最新的配置,同步给服务的每一个实例。 + +配置中心特性要求: + +- **可用性要求非常高** +- **性能要求中等** +- **数据容量要求低** +- **API 友好程度** + +## 分布式锁:所有的分布式锁都是错误的? + +## 重试幂等:让程序 Exactly-once 很难吗? + +在分布式系统中,程序不能保证 Exactly-once:响应超时的情况下,请求方无法判断接收方是否处理过这个请求。过程中有可能出现网络丢包问题或服务端故障。 + +幂等设计要点: + +- 使用唯一性 ID 来标记请求,通过 ID 进行去重 +- 保存状态快照+回滚模式——代价太高,一般不会用 + +## 雪崩(一):熔断,让故障自适应地恢复 + +在服务调用链中,服务调用时由于某一个服务故障,导致级联服务故障,并逐步扩散引起大范围服务故障的现象,称为雪崩效应。 + +在熔断机制的模式下,服务调用方需要为每一个调用对象,可以是服务、实例和接口,维护一个状态机,在这个状态机中有三种状态。 + +首先,是闭合状态( Closed )。在这种状态下,我们需要一个计数器来记录调用失败的次数和总的请求次数,如果在一个时间窗口内,请求的特定错误码的比例达到预设的阈值,就切换到断开状态。 + +其次,是断开状态( Open )。在该状态下,发起请求时会立即返回错误,也可以返回一个降级的结果,我们会在后面的课程“降级”中再详细讨论。在断开状态下,会启动一个超时计时器,当计时器超时后,状态切换到半打开状态。 + +最后,是半打开状态( Half-Open )。在该状态下,允许应用程序将一定数量的请求发往被调用服务,如果这些调用正常,那么就可以认为被调用服务已经恢复正常,此时熔断器切换到闭合状态,同时需要重置计数。如果这部分仍有调用失败的情况,我们就认为被调用方仍然没有恢复,熔断器会切换到断开状态,然后重置计数器。所以半打开状态能够有效防止正在恢复中的服务,被突然出现的大量请求再次打垮的情况。 + +## 雪崩(二):限流,抛弃超过设计容量的请求 + +常见限流算法 + +- 固定窗口限流 +- 滑动窗口限流 +- 漏桶限流 +- 令牌桶限流 + +## 雪崩(三):降级,无奈的丢车保帅之举 + +**降级机制能从全局角度对资源进行调配,通过牺牲非核心服务来保障核心服务的稳定性**。 + +如何实现降级: + +- 手动降级 +- 自动降级:当系统的某些指标或接口调用出现错误时,直接启动降级逻辑 + +## 雪崩(四):扩容,没有用钱解决不了的问题 + +如何实现动态扩容 + +通过可观测性系统监控核心指标 + +过载判断 - 一旦核心指标达到阈值,触发扩容 + +自动扩容 - 利用 K8S 进行容器化扩容 + +## 可观测性(一):如何监控一个复杂的分布式系统? + +搭建一个可观测性平台,主要通过对日志( Logs )、链路( Traces )与指标( Metrics )这三类数据进行采集、计算和展示。 + +- 日志信息( Logs ) - 代表:ELK +- 追踪链路( Traces ) - 代表:Jaeger、Zipkin、SkyWalking +- 指标信息( Metrics ) - 代表:Prometheus + Grafana + +四个黄金指标:延迟、流量、错误和饱和度 + +## 可观测性(二):如何设计一个高效的告警系统? + +告警系统的评价指标: + +- 信噪比:指有效告警通知数和无效告警通知数的比例,信噪比越高越好,是用来评估“多报”问题的。 +- 覆盖率:指被告警系统通知的故障占全部线上故障的比例,同样,覆盖率也是越高越好,是用来评估“漏报”问题的。 +- 转交率:指被转交的告警通知数占全部告警通知数的比例,转交率越低越好,是用来评估“对比人”问题的。 + +## 故障(一):预案管理竟然能让被动故障自动恢复? + +故障评价标准: + +- **平均出现故障的频率**:指平均多少时间出现一次故障,这个频率越低越好。 +- **平均故障恢复的时间**:指出现故障后,系统在多长时间恢复到正常状态,这个时间越短越好,并且,我认为这是一个更关键的指标。 + +被动故障的来源: + +- DNS 解析问题:用户本地网络的 DNS 服务不能将我们的域名正确解析到 IP 地址。 +- 网络连通性问题:用户已经解析到正确的 IP 地址,但是从用户网络到我们服务器的 IP 地址之间的网络慢或者不通。 +- 系统内部的硬件设施故障:比如机器突然宕机,内部网线中断等。 +- 系统依赖的各种第三方服务:比如 CDN 服务、短信网关、语音识别等第三方服务故障。 + +- + +## 故障(二):变更管理,解决主动故障的高效思维方式 + +主动故障的来源: + +- 程序发布变更:指服务器、App 和 Web 等发布了新版本的程序和服务。 +- 实例数目变更:指服务器新增实例和下线实例。 +- 配置发布变更:指发布了新版本的配置。 +- 运营策略变更:指举办了导致用户流量增长的运营活动,比如购买了新的推广广告等。 + +## 分片(一):如何选择最适合的水平分片方式? + +略 + +## 分片(二):垂直分片和混合分片的 trade-off + +略 + +## 复制(一):主从复制从副本的数据可以读吗? + +复制的三种方案: + +- **主从复制**:整个系统中只有一个主副本,其他的都为从副本。 +- **多主复制**:系统中存在多个主副本,客户端将写请求发送给其中的一个主副本,该主副本负责将数据变更发送到其他所有的主副本。 +- **无主复制**:系统中不存在主副本,每一个副本都能接受客户端的写请求,接受写请求的副本不会将数据变更同步到其他的副本。 + +Mysql、PostgreSql、Redis、MongoDB、Kafka 都支持主从复制。 + +主从复制的关键在于采用同步复制还是异步复制。 + +## 复制(二):多主复制的多主副本同时修改了怎么办? + +为什么需要多主复制——为了提供更好的容灾能力,需要多机房、多数据中心来进行冗余,这就需要多主复制或无主复制。 + +如何实现多主复制 + +- 首先,每一个主从复制单元内部是一个常规的主从复制模式,这里的主副本、从副本之间的复制可以是同步的,也可以是异步的。 +- 其次,多个主从复制单元之间,每一个主副本都会将自己的修改复制到其他的主副本,主副本之间的复制可以是同步的,也可以是异步的。 + +> 问题: +> +> 同步会导致整个模式退化为主从复制的形式。 +> +> 异步模式的多主复制会存在数据一致性的问题。 + +如何解决冲突 + +写入冲突是由于多个主副本同时接受写入,并且主副本之间异步复制导致的。 + +> 注:文中并未给出完整的解决方案。 + +## 复制(三):最早的数据复制方式竟然是无主复制? + +无主复制由于写入不依赖主节点,所以在主节点故障时,不会出现不可用的情况。但是,也是由于写入不依赖主节点,可能导致副本之间的写入顺序不相同,会影响数据的一致性。 + +在实现无主复制时,有两个关键问题:数据读写和数据修复。数据读写是通过仲裁条件 w + r > n 来保证的,如果满足 w + r > n ,那么读副本和写副本之间就一定有交集,即一定能读取到最新的写入。而数据修复是通过读修复和反熵过程实现的,这两个方法在数据的持久性和一致性方面存在一定的问题,如果对数据有强一致性的要求,就要谨慎采用无主复制。 + +然后,我们了解了 Sloppy Quorum ,它相比于传统的 Quorum ,为了系统的可用性而牺牲了数据的一致性,这里我们可以进一步得出,**无主复制是一个可用性优先的复制模型**。 + +## 事务(一):一致性,事务的集大成者 + +事务是一个或多个操作的组合操作,它需要保证这组操作要么都执行,要么都不执行。 + +## 事务(二):原子性,对应用层提供的完美抽象 + +简单介绍了 2PC + +## 事务(三):隔离性,正确与性能之间权衡的艺术 + +简单介绍了事务隔离级别 + +## 事务(四):持久性,吃一碗粉就付一碗粉的钱 + +简单介绍了 Redo Log + WAL + +## 一致性与共识(一):数据一致性都有哪些级别? + +按照一致性强度由高到低,有以下模型: + +线性一致性——现在可以实现的一致性级别最强的是线性一致性,它是指所有进程看到的事件历史一致有序,并符合时间先后顺序, 单个进程遵守 program order,并且有 total order。 + +顺序一致性——它是指所有进程看到的事件历史一致有序,但不需要符合时间先后顺序, 单个进程遵守 program order,也有 total order。 + +因果一致性——它是指所有进程看到的因果事件历史一致有序,单个进程遵守 program order,不对没有因果关系的并发排序。 + +最终一致性——它是指所有进程互相看到的写无序,但最终一致。不对跨进程的消息排序。 + +## 一致性与共识(二):它们是鸡生蛋还是蛋生鸡? + +略 + +## 一致性与共识(三):共识与事务之间道不明的关系 + +略 + +## 分布式计算技术的发展史:从单进程服务到 Service Mesh + +略 + +## 分布式存储技术的发展史:从 ACID 到 NewSQL + +略 + +## 春节加餐 技术债如房贷,是否借贷怎样取舍? + +略 + +## 春节加餐 深入聊一聊计算机系统的时间 + +略 + +## 春节加餐 系统性思维,高效学习和工作的利器 + +略 + +## 结束语 在分布式技术的大潮流中自由冲浪吧! + +略 + +## 参考资料 + +- [深入浅出分布式技术原理](https://time.geekbang.org/column/intro/100104701) \ No newline at end of file diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\267\261\345\205\245\347\220\206\350\247\243Sentinel\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\267\261\345\205\245\347\220\206\350\247\243Sentinel\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..87aa979156 --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/12.\345\210\206\345\270\203\345\274\217\350\260\203\345\272\246/\346\267\261\345\205\245\347\220\206\350\247\243Sentinel\347\254\224\350\256\260.md" @@ -0,0 +1,153 @@ +--- +title: 《深入理解 Sentinel》笔记 +date: 2024-05-27 06:58:23 +order: 1 +categories: + - 笔记 + - 分布式 + - 分布式调度 +tags: + - 分布式 + - 调度 + - 限流 + - 熔断 + - 降级 + - Sentinel +permalink: /pages/be9a2557/ +--- + +# 《深入理解 Sentinel》笔记 + +## 开篇词:一次服务雪崩问题排查经历 + +> **什么是服务雪崩** + +**服务雪崩**是指:在微服务项目中指由于突发流量导致某个服务不可用,从而导致上游服务不可用,并产生级联效应,最终导致整个系统不可用。 + +当一切正常时,整体系统如下所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202401280931974.png) + +在分布式系统架构下,这些强依赖的子服务稳定与否对系统的影响非常大。但是,依赖的子服务可能有很多不可控问题:如网络连接、资源繁忙、服务宕机等。例如:下图中有一个 QPS 为 50 的依赖服务 I 出现不可用,但是其他依赖服务是可用的。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202401280931939.png) + +当流量很大的情况下,某个依赖的阻塞,会导致上游服务请求被阻塞。当这种级联故障愈演愈烈,就可能造成整个线上服务不可用的雪崩效应,如下图。这种情况若持续恶化,如果上游服务本身还被其他服务所依赖,就可能出现多米洛骨牌效应,导致多个服务都无法正常工作。 + +![img](https://github.com/Netflix/Hystrix/wiki/images/soa-3-640.png) + +## 为什么需要服务降级以及常见的几种降级方式 + +服务降级是为了保障服务能够稳定运行的一种保护方式,应对流量突增用降级牺牲一些流量换取系统的稳定。常见的服务降级实现方式有:开关降级、限流降级、熔断降级。 + +限流降级与熔断降级都可以实现在消费端限流或者服务端限流,限流可以根据流量控制策略处理超过阈值的流量。 + +- 限流即便没有达到系统的瓶颈,只要流量达到设定的阈值,就会触发限流; +- 熔断尽最大的可能去完成所有的请求,容忍一些失败,熔断也能自动恢复。熔断的常见降级策略: + - 在每秒请求异常数超过多少时触发熔断降级 + - 在每秒请求异常错误率超过多少时触发熔断降级 + - 在每秒请求平均耗时超过多少时触发熔断降级 + +开关降级适用于促销活动这种可以明确预估到并发会突增的场景。 + +## 为什么选择 Sentinel,Sentinel 与 Hystrix 的对比 + +| | Sentinel | Hystrix | +| :------------- | :--------------------------------------------- | :---------------------------- | +| 社区活跃度 | Github 13K star | 官方停止维护 | +| 隔离策略 | 信号量隔离 | 线程池隔离/信号量隔离 | +| 熔断降级策略 | 基于响应时间或失败比率 | 基于失败比率 | +| 实时指标实现 | 滑动窗口 | 滑动窗口(基于 RxJava) | +| 规则配置 | 支持多种数据源 | 支持多种数据源 | +| 扩展性 | 多个 SPI 扩展点 | 插件的形式 | +| 基于注解的支持 | 支持 | 支持 | +| 限流 | 基于 QPS,支持基于调用关系的限流 | 有限的支持 | +| 流量整形 | 支持慢启动、匀速器模式 | 不支持 | +| 系统负载保护 | 支持 | 不支持 | +| 控制台 | 开箱即用,可配置规则、查看秒级监控、机器发现等 | 不完善 | +| 常见框架的适配 | Servlet、Spring Cloud、Dubbo、gRPC 等 | Servlet、Spring Cloud Netflix | + +## Sentinel 基于滑动窗口的实时指标数据统计 + +- WindowWrap 用于包装 Bucket,随着 Bucket 一起创建。 +- WindowWrap 数组实现滑动窗口,Bucket 只负责统计各项指标数据,WindowWrap 用于记录 Bucket 的时间窗口信息。 +- 定位 Bucket 实际上是定位 WindowWrap,拿到 WindowWrap 就能拿到 Bucket。 + +## Sentinel 的一些概念与核心类介绍 + +- 资源:资源是 Sentinel 的关键概念。资源,可以是一个方法、一段代码、由应用提供的接口,或者由应用调用其它应用的接口。 +- 规则:围绕资源的实时状态设定的规则,包括流量控制规则、熔断降级规则以及系统保护规则、自定义规则。 +- 降级:在流量剧增的情况下,为保证系统能够正常运行,根据资源的实时状态、访问流量以及系统负载有策略的拒绝掉一部分流量。 + +核心类: + +`ResourceWrapper` 类用于表示资源。 + +`Node` 用于持有实时统计的指标数据。它有几个实现类:`DefaultNode`、`ClusterNode`、`EntranceNode`、`StatisticNode`。 + +- StatisticNode 是实现实时指标数据统计 Node。 +- DefaultNode 是实现以资源为维度的指标数据统计的 Node。 +- ClusterNode 统计每个资源全局的指标数据,以及统计该资源按调用来源区分的指标数据。 +- EntranceNode 继承 DefaultNode,用于维护一颗树,从根节点到每个叶子节点都是不同请求的调用链路,所经过的每个节点都对应着调用链路上被 Sentinel 保护的资源,一个请求调用链路上的节点顺序正是资源被访问的顺序。 +- Context 代表调用链路上下文,贯穿一次调用链路中的所有 Entry。Context 维持着入口节点(entranceNode)、本次调用链路的 curNode、调用来源(origin)等信息。Context 名称即为调用链路入口名称。Context 通过 ThreadLocal 传递,只在调用链路的入口处创建。 +- Entry 维护了当前资源的 DefaultNode,以及调用来源的 StatisticNode。 +- ProcessorSlot 直译就是处理器插槽,是 Sentinel 实现限流降级、熔断降级、系统自适应降级等功能的切入点。Sentinel 提供的 ProcessorSlot 可以分为两类,一类是辅助完成资源指标数据统计的切入点,一类是实现降级功能的切入点。实现降级功能的 ProcessorSlot: + - AuthoritySlot:实现黑白名单降级 + - SystemSlot:实现系统自适应降级 + - FlowSlot:实现限流降级 + - DegradeSlot:实现熔断降级 + +## Sentinel 中的责任链模式与 Sentinel 的整体工作流程 + +Sentinel 的工作流就是使用责任链模式将所有的 ProcessorSlot 按照一定的顺序串成一个单向链表。 + +实现将 ProcessorSlot 串成一个单向链表的是 ProcessorSlotChain,这个 ProcessorSlotChain 是由 SlotChainBuilder 构造的。 + +## Java SPI 及 SPI 在 Sentinel 中的应用 + +SPI 全称是 Service Provider Interface,直译就是服务提供者接口,是一种服务发现机制,是 Java 的一个内置标准,允许不同的开发者去实现某个特定的服务。SPI 的本质是将接口实现类的全限定名配置在文件中,由服务加载器读取配置文件,加载实现类,实现在运行时动态替换接口的实现类。 + +在 sentinel-core 模块的 resources 资源目录下,有一个 META-INF/services 目录,该目录下有两个以接口全名命名的文件,其中 com.alibaba.csp.sentinel.slotchain.SlotChainBuilder 文件用于配置 SlotChainBuilder 接口的实现类。 + +## 08 资源指标数据统计的实现全解析(上) + +NodeSelectorSlot 负责为资源的首次访问创建 DefaultNode,以及维护 Context.curNode 和调用树。NodeSelectorSlot 被放在 ProcessorSlotChain 链表的第一个位置,这是因为后续的 ProcessorSlot 都需要依赖这个 ProcessorSlot。 + +## 09 资源指标数据统计的实现全解析(下) + +- 一个调用链路上只会创建一个 Context,在调用链路的入口创建(一个调用链路上第一个被 Sentinel 保护的资源)。 +- 一个 Context 名称只创建一个 EntranceNode,也是在调用链路的入口创建,调用 Context#enter 方法时创建。 +- 与方法调用的入栈出栈一样,一个线程上调用多少次 SphU#entry 方法就会创建多少个 CtEntry,前一个 CtEntry 作为当前 CtEntry 的父节点,当前 CtEntry 作为前一个 CtEntry 的子节点,构成一个双向链表。Context.curEntry 保存的是当前的 CtEntry,在调用当前的 CtEntry#exit 方法时,由当前 CtEntry 将 Context.curEntry 还原为当前 CtEntry 的父节点 CtEntry。 +- 一个调用链路上,如果多次调用 SphU#entry 方法传入的资源名称都相同,那么只会创建一个 DefaultNode,如果资源名称不同,会为每个资源名称创建一个 DefaultNode,当前 DefaultNode 会作为调用链路上的前一个 DefaultNode 的子节点。 +- 一个资源有且只有一个 ProcessorSlotChain,一个资源有且只有一个 ClusterNode。 +- 一个 ClusterNode 负责统计一个资源的全局指标数据。 +- StatisticSlot 负责记录请求是否被放行、请求是否被拒绝、请求是否处理异常、处理请求的耗时等指标数据,在 StatisticSlot 调用 DefaultNode 用于记录某项指标数据的方法时,DefaultNode 也会调用 ClusterNode 的相对应方法,完成两份指标数据的收集。 +- DefaultNode 统计当前资源的各项指标数据的维度是同一个 Context(名称相同),而 ClusterNode 统计当前资源各项指标数据的维度是全局。 + +## 10 限流降级与流量效果控制器(上) + +## 11 限流降级与流量效果控制器(中) + +## 12 限流降级与流量效果控制器(下) + +## 13 熔断降级与系统自适应限流 + +## 14 黑白名单限流与热点参数限流 + +## 15 自定义 ProcessorSlot 实现开关降级 + +## 16 Sentinel 动态数据源:规则动态配置 + +## 17 Sentinel 主流框架适配 + +## 18 Sentinel 集群限流的实现(上) + +## 19 Sentinel 集群限流的实现(下) + +## 20 结束语:Sentinel 对应用的性能影响如何? + +## 21 番外篇:Sentinel 1.8.0 熔断降级新特性解读 + +## 资料 + +https://wujiuye.com/album/52c96863a60441829497e98226e2c337 diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.Dubbo\346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/Dubbo\346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.Dubbo\346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/Dubbo\346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" index 50c1131f8e..b7ac6f2bcd 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/02.Dubbo\346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/Dubbo\346\272\220\347\240\201\350\247\243\350\257\273\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" @@ -1,7 +1,6 @@ --- title: 《Dubbo 源码解读与实战》笔记 date: 2023-06-25 19:24:38 -order: 02 categories: - 笔记 - 分布式 @@ -11,7 +10,7 @@ tags: - 分布式通信 - RPC - Dubbo -permalink: /pages/10b5b8/ +permalink: /pages/f190fea2/ --- # 《Dubbo 源码解读与实战》笔记 @@ -355,4 +354,4 @@ EventLoopGroup ## 参考资料 -- [《Dubbo 源码解读与实战》](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=393) \ No newline at end of file +- [《Dubbo 源码解读与实战》](https://kaiwu.lagou.com/course/courseInfo.htm?courseId=393) diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/15.RocketMQ\346\212\200\346\234\257\345\206\205\345\271\225\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/RocketMQ\346\212\200\346\234\257\345\206\205\345\271\225\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/15.RocketMQ\346\212\200\346\234\257\345\206\205\345\271\225\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/RocketMQ\346\212\200\346\234\257\345\206\205\345\271\225\347\254\224\350\256\260.md" index ce32f4d71c..91f4bb76e9 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/15.RocketMQ\346\212\200\346\234\257\345\206\205\345\271\225\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/RocketMQ\346\212\200\346\234\257\345\206\205\345\271\225\347\254\224\350\256\260.md" @@ -1,7 +1,6 @@ --- title: 《RocketMQ 技术内幕》笔记 date: 2022-07-12 16:58:58 -order: 15 categories: - 笔记 - 分布式 @@ -11,7 +10,7 @@ tags: - 分布式通信 - MQ - RocketMQ -permalink: /pages/9708e2/ +permalink: /pages/6e5ddacf/ --- # 《RocketMQ 技术内幕》笔记 @@ -472,4 +471,4 @@ tryToFindTopicPublishInfo 是查找主题的路由信息的方法。 ## 参考资料 -- [RocketMQ 技术内幕](https://book.douban.com/subject/35626441/) \ No newline at end of file +- [RocketMQ 技术内幕](https://book.douban.com/subject/35626441/) diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" new file mode 100644 index 0000000000..842c2e2e0e --- /dev/null +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Kafka\346\240\270\345\277\203\346\212\200\346\234\257\344\270\216\345\256\236\346\210\230\347\254\224\350\256\260.md" @@ -0,0 +1,638 @@ +--- +title: 《Kafka 核心技术与实战》笔记 +date: 2025-02-14 17:08:28 +categories: + - 笔记 + - 分布式 + - 分布式通信 +tags: + - 分布式 + - 分布式通信 + - MQ + - Kafka +permalink: /pages/be7a7dd7/ +--- + +# 《Kafka 核心技术与实战》笔记 + +## 开篇词 为什么要学习 Kafka? + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170734255.jpeg) + +## 消息引擎系统 ABC + +消息引擎系统的作用: + +- 消息引擎传输的对象是消息; +- 如何传输消息属于消息引擎设计机制的一部分。 + +设计消息引擎系统的关键点: + +- **序列化** - 决定了在网络中传输数据的形式。 + - 代表:CSV、XML、JSON、Protocol Buffer、Thrift。 + - kafka 默认使用纯二进制的字节序列。 +- **传输模型**:Kafka 同时支持以下两种模型 + - **点对点模型** + - **发布/订阅模型** + +消息引擎的作用: + +- **异步处理** +- **削峰填谷** +- **系统解耦** +- **系统间通信** +- **数据缓冲** +- **最终一致性** + +## 一篇文章带你快速搞定 Kafka 术语 + +Kafka 术语: + +- **消息** - Record。Kafka 是消息引擎嘛,这里的消息就是指 Kafka 处理的主要对象。 +- **主题** - Topic。主题是承载消息的逻辑容器,在实际使用中多用来区分具体的业务。 +- **分区** - Partition。一个有序不变的消息序列。每个主题下可以有多个分区。 +- **消息位移** - Offset。表示分区中每条消息的位置信息,是一个单调递增且不变的值。 +- **副本** - Replica。Kafka 中同一条消息能够被拷贝到多个地方以提供数据冗余,这些地方就是所谓的副本。副本还分为领导者副本和追随者副本,各自有不同的角色划分。副本是在分区层级下的,即每个分区可配置多个副本实现高可用。 +- **生产者** - Producer。向主题发布新消息的应用程序。 +- **消费者** - Consumer。从主题订阅新消息的应用程序。 +- **消费者位移** - Consumer Offset。表征消费者消费进度,每个消费者都有自己的消费者位移。 +- **消费者组** - Consumer Group。多个消费者实例共同组成的一个组,同时消费多个分区以实现高吞吐。 +- **分区再均衡** - Rebalance。消费者组内某个消费者实例挂掉后,其他消费者实例自动重新分配订阅主题分区的过程。Rebalance 是 Kafka 消费者端实现高可用的重要手段。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170734395.jpeg) + +Kafka 的三层消息架构: + +- 第一层是主题层,每个主题可以配置 M 个分区,而每个分区又可以配置 N 个副本。 +- 第二层是分区层,每个分区的 N 个副本中只能有一个充当领导者角色,对外提供服务;其他 N-1 个副本是追随者副本,只是提供数据冗余之用。 +- 第三层是消息层,分区中包含若干条消息,每条消息的位移从 0 开始,依次递增。 +- 最后,客户端程序只能与分区的领导者副本进行交互。 + +## Kafka 只是消息引擎系统吗? + +Kafka 在设计之初就旨在提供三个方面的特性: + +- 提供一套 API 实现生产者和消费者; +- 降低网络传输和磁盘存储开销; +- 实现高伸缩性架构。 + +作为流处理平台,Kafka 与其他主流大数据流式计算框架相比,优势在哪里呢? + +- **更容易实现端到端的正确性(Correctness)** - 因为所有的数据流转和计算都在 Kafka 内部完成,故 Kafka 可以实现端到端的精确一次处理语义。 +- **Kafka 自己对于流式计算的定位** - 官网上明确标识 Kafka Streams 是一个用于搭建实时流处理的客户端库而非是一个完整的功能系统。 + +## 我应该选择哪种 Kafka? + +- Apache Kafka,也称社区版 Kafka。优势在于迭代速度快,社区响应度高,使用它可以让你有更高的把控度;缺陷在于仅提供基础核心组件,缺失一些高级的特性。 +- Confluent Kafka,Confluent 公司提供的 Kafka。优势在于集成了很多高级特性且由 Kafka 原班人马打造,质量上有保证;缺陷在于相关文档资料不全,普及率较低,没有太多可供参考的范例。 +- CDH/HDP Kafka,大数据云公司提供的 Kafka,内嵌 Apache Kafka。优势在于操作简单,节省运维成本;缺陷在于把控度低,演进速度较慢。 + +## 聊聊 Kafka 的版本号 + +Kafka 有以下重大版本: + +- 0.7 - 只提供了最基础的消息队列功能 +- 0.8 + - 正式引入了副本机制 + - 至少升级到 0.8.2.2 +- 0.9 + - 增加了基础的安全认证 / 权限功能 + - 用 Java 重写了新版本消费者 API + - 引入了 Kafka Connect 组件 + - 新版本 Producer API 在这个版本中算比较稳定 +- 0.10 + - 引入了 Kafka Streams,正式升级成分布式流处理平台 + - 至少升级到 0.10.2.2 + - 修复了一个可能导致 Producer 性能降低的 Bug +- 0.11 + - 提供幂等性 Producer API 以及事务 + - 对 Kafka 消息格式做了重构 + - 至少升级到 0.11.0.3 +- 1.0 和 2.0 - Kafka Streams 的改进 + +## Kafka 线上集群部署方案怎么做? + +**系统** + +在 Linux 部署 Kafka 能够享受到零拷贝技术所带来的快速数据传输特性。 + +**磁盘** + +使用机械磁盘完全能够胜任 Kafka 线上环境。 + +**磁盘容量** + +假设你所在公司有个业务每天需要向 Kafka 集群发送 1 亿条消息,每条消息保存两份以防止数据丢失,另外消息默认保存两周时间。现在假设消息的平均大小是 1KB,那么你能说出你的 Kafka 集群需要为这个业务预留多少磁盘空间吗? + +我们来计算一下:每天 1 亿条 1KB 大小的消息,保存两份且留存两周的时间,那么总的空间大小就等于`1 亿 * 1KB * 2 / 1000 / 1000 = 200GB`。一般情况下 Kafka 集群除了消息数据还有其他类型的数据,比如索引数据等,故我们再为这些数据预留出 10%的磁盘空间,因此总的存储容量就是 220GB。既然要保存两周,那么整体容量即为 `220GB * 14`,大约 3TB 左右。Kafka 支持数据的压缩,假设压缩比是 0.75,那么最后你需要规划的存储空间就是 `0.75 * 3 = 2.25TB`。 + +总之在规划磁盘容量时你需要考虑下面这几个元素: + +- 新增消息数 +- 消息留存时间 +- 平均消息大小 +- 备份数 +- 是否启用压缩 + +**带宽** + +通常使用的都是普通的以太网络,带宽也主要有两种:1Gbps 的千兆网络和 10Gbps 的万兆网络。 + +假设你公司的机房环境是千兆网络,即 1Gbps,现在你有个业务,其业务目标或 SLA 是在 1 小时内处理 1TB 的业务数据。那么问题来了,你到底需要多少台 Kafka 服务器来完成这个业务呢? + +让我们来计算一下,由于带宽是 1Gbps,即每秒处理 1Gb 的数据,假设每台 Kafka 服务器都是安装在专属的机器上,也就是说每台 Kafka 机器上没有混部其他服务,毕竟真实环境中不建议这么做。通常情况下你只能假设 Kafka 会用到 70%的带宽资源,因为总要为其他应用或进程留一些资源。 + +根据实际使用经验,超过 70%的阈值就有网络丢包的可能性了,故 70%的设定是一个比较合理的值,也就是说单台 Kafka 服务器最多也就能使用大约 700Mb 的带宽资源。 + +稍等,这只是它能使用的最大带宽资源,你不能让 Kafka 服务器常规性使用这么多资源,故通常要再额外预留出 2/3 的资源,即单台服务器使用带宽 700Mb / 3 ≈ 240Mbps。需要提示的是,这里的 2/3 其实是相当保守的,你可以结合你自己机器的使用情况酌情减少此值。 + +好了,有了 240Mbps,我们就可以计算 1 小时内处理 1TB 数据所需的服务器数量了。根据这个目标,我们每秒需要处理 2336Mb 的数据,除以 240,约等于 10 台服务器。如果消息还需要额外复制两份,那么总的服务器台数还要乘以 3,即 30 台。 + +## 最最最重要的集群参数配置(上) + +**与存储信息相关的参数** + +- `log.dirs`:这是非常重要的参数,指定了 Broker 需要使用的若干个文件目录路径。要知道这个参数是没有默认值的,这说明什么?这说明它必须由你亲自指定。 +- `log.dir`:注意这是 dir,结尾没有 s,说明它只能表示单个路径,它是补充上一个参数用的。 + +只要设置`log.dirs`,即第一个参数就好了,不要设置`log.dir`。而且更重要的是,在线上生产环境中一定要为`log.dirs`配置多个路径,具体格式是一个 CSV 格式,也就是用逗号分隔的多个路径,比如`/home/kafka1,/home/kafka2,/home/kafka3`这样。如果有条件的话你最好保证这些目录挂载到不同的物理磁盘上。这样做有两个好处: + +- 提升读写性能:比起单块磁盘,多块物理磁盘同时读写数据有更高的吞吐量。 +- 能够实现故障转移:即 Failover。这是 Kafka 1.1 版本新引入的强大功能。要知道在以前,只要 Kafka Broker 使用的任何一块磁盘挂掉了,整个 Broker 进程都会关闭。但是自 1.1 开始,这种情况被修正了,坏掉的磁盘上的数据会自动地转移到其他正常的磁盘上,而且 Broker 还能正常工作。还记得上一期我们关于 Kafka 是否需要使用 RAID 的讨论吗?这个改进正是我们舍弃 RAID 方案的基础:没有这种 Failover 的话,我们只能依靠 RAID 来提供保障。 + +**与 ZooKeeper 相关的参数** + +`zookeeper.connect`。这也是一个 CSV 格式的参数,比如我可以指定它的值为`zk1:2181,zk2:2181,zk3:2181`。2181 是 ZooKeeper 的默认端口。 + +如果我让多个 Kafka 集群使用同一套 ZooKeeper 集群,那么这个参数应该怎么设置呢?这时候 chroot 就派上用场了。这个 chroot 是 ZooKeeper 的概念,类似于别名。 + +如果你有两套 Kafka 集群,假设分别叫它们 kafka1 和 kafka2,那么两套集群的`zookeeper.connect`参数可以这样指定:`zk1:2181,zk2:2181,zk3:2181/kafka1`和`zk1:2181,zk2:2181,zk3:2181/kafka2`。切记 chroot 只需要写一次,而且是加到最后的。我经常碰到有人这样指定:`zk1:2181/kafka1,zk2:2181/kafka2,zk3:2181/kafka3`,这样的格式是不对的。 + +**与 Broker 连接相关的参数** + +- `listeners`:学名叫监听器,其实就是告诉外部连接者要通过什么协议访问指定主机名和端口开放的 Kafka 服务。 +- `advertised.listeners`:和 listeners 相比多了个 advertised。Advertised 的含义表示宣称的、公布的,就是说这组监听器是 Broker 用于对外发布的。 +- `host.name/port`:列出这两个参数就是想说你把它们忘掉吧,压根不要为它们指定值,毕竟都是过期的参数了。 + +**关于 Topic 管理的参数** + +- `auto.create.topics.enable`:是否允许自动创建 Topic。 +- `unclean.leader.election.enable`:是否允许 Unclean Leader 选举。 +- `auto.leader.rebalance.enable`:是否允许定期进行 Leader 选举。 + +**关于数据留存的参数** + +- `log.retention.{hours|minutes|ms}`:这是个“三兄弟”,都是控制一条消息数据被保存多长时间。从优先级上来说 ms 设置最高、minutes 次之、hours 最低。 +- `log.retention.bytes`:这是指定 Broker 为消息保存的总磁盘容量大小。 +- `message.max.bytes`:控制 Broker 能够接收的最大消息大小。 + +## 最最最重要的集群参数配置(下) + +**Topic 级别参数** + +- `retention.ms`:规定了该 Topic 消息被保存的时长。默认是 7 天,即该 Topic 只保存最近 7 天的消息。一旦设置了这个值,它会覆盖掉 Broker 端的全局参数值。 +- `retention.bytes`:规定了要为该 Topic 预留多大的磁盘空间。和全局参数作用相似,这个值通常在多租户的 Kafka 集群中会有用武之地。当前默认值是-1,表示可以无限使用磁盘空间。 + +JVM 参数 + +- `KAFKA_HEAP_OPTS`:指定堆大小。 +- `KAFKA_JVM_PERFORMANCE_OPTS`:指定 GC 参数。 + +操作系统参数 + +- 文件描述符限制 - 通常情况下将它设置成一个超大的值是合理的做法,比如`ulimit -n 1000000`。 +- 文件系统类型 - 生产环境最好还是使用 XFS +- Swappiness - 建议将 swappniess 配置成一个接近 0 但不为 0 的值,比如 1。 +- 提交时间 + +## 生产者消息分区机制原理剖析 + +Kafka 的消息组织方式实际上是三级结构:主题-分区-消息。主题下的每条消息只会保存在某一个分区中,而不会在多个分区中被保存多份。 + +分区是实现负载均衡以及高吞吐量的关键。 + +所谓分区策略,就是决定生产者将消息发送到哪个分区的算法。Kafka 提供了默认的分区策略,同时也支持自定义分区策略。 + +## 生产者压缩算法面面观 + +压缩秉承了用时间去换空间的思想。具体来说,就是用 CPU 时间去换磁盘空间或网络 I/O 传输量,希望以较小的 CPU 开销带来更少的磁盘占用或更少的网络 I/O 传输。 + +Kafka 压缩、解压流程:**Producer 端压缩、Broker 端保持、Consumer 端解压缩**。 + +每个压缩过的消息集合在 Broker 端写入时都要发生解压缩操作,目的就是为了对消息执行各种验证。 + +让 Broker 重新压缩消息的 2 种例外:Broker 端指定了和 Producer 端不同的压缩算法;Broker 发生了消息格式转换。 + +在 Kafka 2.1.0 版本之前,Kafka 支持 3 种压缩算法:GZIP、Snappy 和 LZ4。从 2.1.0 开始,Kafka 正式支持 Zstandard 算法(简写为 zstd)。 + +对于 Kafka 而言,它们的性能测试结果却出奇得一致: + +- 在吞吐量方面:`LZ4 > Snappy > zstd 和 GZIP`; +- 在压缩比方面,`zstd > LZ4 > GZIP > Snappy`。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170735260.png) + +## 无消息丢失配置怎么实现? + +**Kafka 只对“已提交”的消息(committed message)做有限度的持久化保证。** + +- 生产阶段使用异步回调方式发送消息,业务侧做好对于发送失败的容错处理。 + - 不要使用 `producer.send(msg)`,而要使用 `producer.send(msg, callback)`。记住,一定要使用带有回调通知的 `send` 方法。 + - 设置 `retries` 为一个较大的值。这里的 `retries` 同样是 Producer 的参数,对应前面提到的 Producer 自动重试。当出现网络的瞬时抖动时,消息发送可能会失败,此时配置了 `retries > 0` 的 Producer 能够自动重试消息发送,避免消息丢失。 +- 存储阶段需要保证写入数据同步副本,以及可靠的故障恢复。 + - 设置 `acks = all`。`acks` 是 Producer 的一个参数,代表了你对“已提交”消息的定义。如果设置成 all,则表明所有副本 Broker 都要接收到消息,该消息才算是“已提交”。这是最高等级的“已提交”定义。 + - 设置 `unclean.leader.election.enable = false`。这是 Broker 端的参数,它控制的是哪些 Broker 有资格竞选分区的 Leader。如果一个 Broker 落后原先的 Leader 太多,那么它一旦成为新的 Leader,必然会造成消息的丢失。故一般都要将该参数设置成 false,即不允许这种情况的发生。 + - 设置 `replication.factor >= 3`。这也是 Broker 端的参数。其实这里想表述的是,最好将消息多保存几份,毕竟目前防止消息丢失的主要机制就是冗余。 + - 设置 `min.insync.replicas > 1`。这依然是 Broker 端参数,控制的是消息至少要被写入到多少个副本才算是“已提交”。设置成大于 1 可以提升消息持久性。在实际环境中千万不要使用默认值 1。 + - 确保 `replication.factor > min.insync.replicas`。如果两者相等,那么只要有一个副本挂机,整个分区就无法正常工作了。我们不仅要改善消息的持久性,防止数据丢失,还要在不降低可用性的基础上完成。推荐设置成 `replication.factor = min.insync.replicas + 1`。 +- 消费阶段确保消息消费完成再提交。Consumer 端有个参数 `enable.auto.commit`,最好把它设置成 false,并采用手动提交位移的方式。就像前面说的,这对于单 Consumer 多线程处理的场景而言是至关重要的。 + +## 客户端都有哪些不常见但是很高级的功能? + +拦截器基本思想就是允许应用程序在不修改逻辑的情况下,动态地实现一组可插拔的事件处理逻辑链。它能够在主业务操作的前后多个时间点上插入对应的“拦截”逻辑。 + +**Kafka 拦截器分为生产者拦截器和消费者拦截器**。生产者拦截器允许你在发送消息前以及消息提交成功后植入你的拦截器逻辑;而消费者拦截器支持在消费消息前以及提交位移后编写特定逻辑。**指定拦截器类时要指定它们的全限定名**。 + +**Kafka 拦截器可以应用于包括客户端监控、端到端系统性能检测、消息审计等多种功能在内的场景**。 + +## Java 生产者是如何管理 TCP 连接的? + +开发客户端时,能够利用 TCP 本身提供的一些高级功能,比如多路复用请求以及同时轮询多个连接的能力。 + +对最新版本的 Kafka(2.1.0)而言,Java Producer 端管理 TCP 连接的方式是: + +1. KafkaProducer 实例创建时启动 Sender 线程,从而创建与 bootstrap.servers 中所有 Broker 的 TCP 连接。 + - **不需要把集群中所有的 Broker 信息都配置到 bootstrap.servers 中**,通常你指定 3~4 台就足以了。因为 Producer 一旦连接到集群中的任一台 Broker,就能拿到整个集群的 Broker 信息,故没必要为 bootstrap.servers 指定所有的 Broker。 +2. **TCP 连接还可能在两个地方被创建:一个是在更新元数据后,另一个是在消息发送时**。 + 1. KafkaProducer 实例首次更新元数据信息之后,还会再次创建与集群中所有 Broker 的 TCP 连接。 + 2. 如果 Producer 端发送消息到某台 Broker 时发现没有与该 Broker 的 TCP 连接,那么也会立即创建连接。 +3. Producer 端关闭 TCP 连接的方式有两种:**一种是用户主动关闭;一种是 Kafka 自动关闭**。如果设置 Producer 端 connections.max.idle.ms 参数大于 0,则步骤 1 中创建的 TCP 连接会被自动关闭;如果设置该参数=-1,那么步骤 1 中创建的 TCP 连接将无法被关闭,从而成为“僵尸”连接。 + +## 幂等生产者和事务生产者是一回事吗? + +消息可靠性保证有以下几种: + +- 最多一次(at most once):消息可能会丢失,但绝不会被重复发送。 +- 至少一次(at least once):消息不会丢失,但有可能被重复发送。 +- 精确一次(exactly once):消息不会丢失,也不会被重复发送。 + +大部分 MQ 都支持 at least once,要实现 exactly once,需要消费方保证,通常是通过幂等性设计来实现。 + +Kafka 也提供了一些相关的功能: + +幂等性 Producer 只能保证单分区上的幂等性,同时也只能实现单会话上的幂等性。 + +事务型 Producer 能够保证将消息原子性地写入到多个分区中,而且不惧进程的重启。 + +## 消费者组到底是什么? + +**Consumer Group 是 Kafka 提供的可扩展且具有容错性的消费者机制**。 + +Consumer Group 特性: + +- Consumer Group 下可以有一个或多个 Consumer 实例。这里的实例可以是一个单独的进程,也可以是同一进程下的线程。在实际场景中,使用进程更为常见一些。 +- Group ID 是一个字符串,在一个 Kafka 集群中,它标识唯一的一个 Consumer Group。 +- Consumer Group 下所有实例订阅的主题的单个分区,只能分配给组内的某个 Consumer 实例消费。这个分区当然也可以被其他的 Group 消费。 + +**Kafka 仅仅使用 Consumer Group 这一种机制,却同时实现了传统消息引擎系统的两大模型**:如果所有实例都属于同一个 Group,那么它实现的就是消息队列模型;如果所有实例分别属于不同的 Group,那么它实现的就是发布/订阅模型。 + +**理想情况下,Consumer 实例的数量应该等于该 Group 订阅主题的分区总数。** + +分区再均衡**规定了一个 Consumer Group 下的所有 Consumer 如何达成一致,来分配订阅 Topic 的每个分区**。 + +Rebalance 的触发条件: + +1. 组成员数发生变更。 +2. 订阅主题数发生变更。 +3. 订阅主题的分区数发生变更。 + +Rebalance 的问题: + +- 在 Rebalance 过程中,所有 Consumer 实例都会停止消费,等待 Rebalance 完成。 +- Rebalance 的设计是所有 Consumer 实例共同参与,全部重新分配所有分区。其实更高效的做法是尽量减少分配方案的变动。 +- Rebalance 实在是太慢了。 + +最好的解决方案就是避免 Rebalance 的发生吧。 + +## 揭开神秘的“位移主题”面纱 + +**consumer_offsets 在 Kafka 源码中有个更为正式的名字,叫位移主题,即 Offsets Topic。** + +老版本 Consumer 的位移管理是依托于 Apache ZooKeeper 的,它会自动或手动地将位移数据提交到 ZooKeeper 中保存。当 Consumer 重启后,它能自动从 ZooKeeper 中读取位移数据,从而在上次消费截止的地方继续消费。这种设计使得 Kafka Broker 不需要保存位移数据,减少了 Broker 端需要持有的状态空间,因而有利于实现高伸缩性。但是,**ZooKeeper 其实并不适用于这种高频的写操作**。 + +新版本 Consumer 的位移管理机制其实也很简单,就是**将 Consumer 的位移数据作为一条条普通的 Kafka 消息,提交到 consumer_offsets 中。可以这么说,consumer_offsets 的主要作用是保存 Kafka 消费者的位移信息。**它要求这个提交过程不仅要实现高持久性,还要支持高频的写操作。显然,Kafka 的主题设计天然就满足这两个条件,因此,使用 Kafka 主题来保存位移这件事情,实际上就是一个水到渠成的想法了。 + +虽说位移主题是一个普通的 Kafka 主题,但**它的消息格式却是 Kafka 自己定义的**,不能随意地向这个主题写消息。 + +**当 Kafka 集群中的第一个 Consumer 程序启动时,Kafka 会自动创建位移主题**。 + +Kafka 使用** Compact 策略**来删除位移主题中的过期消息,避免该主题无限期膨胀。 + +## 消费者组重平衡能避免吗? + +Rebalance 就是让一个 Consumer Group 下所有的 Consumer 实例就如何消费订阅主题的所有分区达成共识的过程。在 Rebalance 过程中,所有 Consumer 实例共同参与,在协调者组件的帮助下,完成订阅主题分区的分配。 + +Consumer 端应用程序在提交位移时,其实是向 Coordinator 所在的 Broker 提交位移。 + +**第一类非必要 Rebalance 是因为未能及时发送心跳,导致 Consumer 被“踢出”Group 而引发的**。 + +- 设置 session.timeout.ms = 6s。 +- 设置 heartbeat.interval.ms = 2s。 +- 要保证 Consumer 实例在被判定为“dead”之前,能够发送至少 3 轮的心跳请求,即 session.timeout.ms >= 3 \* heartbeat.interval.ms。 + +**第二类非必要 Rebalance 是 Consumer 消费时间过长导致的**。 + +**max.poll.interval.ms** 参数值要大于下游最大处理时间。 + +## Kafka 中位移提交那些事儿 + +**Consumer 需要向 Kafka 汇报自己的位移数据,这个汇报过程被称为提交位移**(Committing Offsets)。因为 Consumer 能够同时消费多个分区的数据,所以位移的提交实际上是在分区粒度上进行的,即** Consumer 需要为分配给它的每个分区提交各自的位移数据**。 + +位移提交分为自动提交和手动提交,而手动提交又分为同步提交和异步提交。 + +## CommitFailedException 异常怎么处理? + +**CommitFailedException,就是 Consumer 客户端在提交位移时出现了错误或异常,而且还是那种不可恢复的严重异常**。 + +CommitFailedException 最常见的场景:当消息处理的总时间超过预设的 max.poll.interval.ms 参数值时,Kafka Consumer 端会抛出 CommitFailedException 异常。 + +## 多线程开发消费者实例 + +**消费者程序启动多个线程,每个线程维护专属的 KafkaConsumer 实例,负责完整的消息获取、消息处理流程**。如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170735535.jpeg) + +**消费者程序使用单或多线程获取消息,同时创建多个消费线程执行消息处理逻辑**。获取消息的线程可以是一个,也可以是多个,每个线程维护专属的 KafkaConsumer 实例,处理消息则交由**特定的线程池**来做,从而实现消息获取与消息处理的真正解耦。具体架构如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170735276.jpeg) + +方案对比: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170735905.jpeg) + +## Java 消费者是如何管理 TCP 连接的 + +**和生产者不同的是,构建 KafkaConsumer 实例时是不会创建任何 TCP 连接的**。 + +**TCP 连接是在调用 KafkaConsumer.poll 方法时被创建的**。再细粒度地说,在 poll 方法内部有 3 个时机可以创建 TCP 连接。 + +- 发起 FindCoordinator 请求时 +- 连接协调者时 +- 消费数据时 + +消费者程序会创建 3 类 TCP 连接: + +- 确定协调者和获取集群元数据 +- 连接协调者,令其执行组成员管理操作 +- 执行实际的消息获取 + +## 消费者组消费进度监控都怎么实现? + +对于 Kafka 消费者来说,最重要的事情就是监控它们的消费进度了,或者说是监控它们消费的滞后程度。**所谓滞后程度,就是指消费者当前落后于生产者的程度**。 + +监控消费者组以及独立消费者程序消费进度的 3 种方法: + +1. 使用 Kafka 自带的命令行工具 kafka-consumer-groups 脚本 +2. 使用 Kafka Java Consumer API 编程 +3. 使用 Kafka 自带的 JMX 监控指标 + +## Kafka 副本机制详解 + +### 副本 + +副本机制好处: + +1. **提供数据冗余**。即使系统部分组件失效,系统依然能够继续运转,因而增加了整体可用性以及数据持久性。 +2. **提供高伸缩性**。支持横向扩展,能够通过增加机器的方式来提升读性能,进而提高读操作吞吐量。 +3. **改善数据局部性**。允许将数据放入与用户地理位置相近的地方,从而降低系统延时。 + +**所谓副本(Replica),本质就是一个只能追加写消息的提交日志**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170736678.jpeg) + +基于领导者的副本机制 + +在 Kafka 中,副本分成两类:领导者副本(Leader Replica)和追随者副本(Follower Replica)。每个分区在创建时都要选举一个副本,称为领导者副本,其余的副本自动称为追随者副本。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170736956.jpeg) + +### In-sync Replicas(ISR) + +追随者副本不提供服务,只是定期地异步拉取领导者副本中的数据而已。 + +Kafka 引入了 In-sync Replicas(ISR)机制来明确追随者副本到底在什么条件下才算与 Leader 同步。 + +**ISR 不只是追随者副本集合,它必然包括 Leader 副本。甚至在某些情况下,ISR 只有 Leader 这一个副本**。 + +**Broker 端参数 replica.lag.time.max.ms** 用于配置 Follower 副本能够落后 Leader 副本的最长时间间隔,当前默认值是 10 秒。这就是说,只要一个 Follower 副本落后 Leader 副本的时间不连续超过 10 秒,那么 Kafka 就认为该 Follower 副本与 Leader 是同步的,即使此时 Follower 副本中保存的消息明显少于 Leader 副本中的消息。 + +### Unclean 领导者选举(Unclean Leader Election) + +因为 Leader 副本天然就在 ISR 中,如果 ISR 为空了,就说明 Leader 副本也“挂掉”了,Kafka 需要重新选举一个新的 Leader。**Broker 端参数 unclean.leader.election.enable 控制是否允许 Unclean 领导者选举**。 + +开启 Unclean 领导者选举可能会造成数据丢失,但好处是,它使得分区 Leader 副本一直存在,不至于停止对外提供服务,因此提升了高可用性。反之,禁止 Unclean 领导者选举的好处在于维护了数据的一致性,避免了消息丢失,但牺牲了高可用性。 + +**Kafka 把所有不在 ISR 中的存活副本都称为非同步副本**。 + +## 请求是怎么被处理的? + +Kafka 所有的请求都是通过 TCP 网络以 Socket 的方式进行通讯的。 + +**Reactor 模式是事件驱动架构的一种实现方式,特别适合应用于处理多个客户端并发向服务器端发送请求的场景**。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170736577.jpeg) + +Kafka 采用了类 Reactor 架构 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170736471.jpeg) + +Acceptor 线程采用轮询的方式将入站请求公平地发到所有网络线程中,因此,在实际使用过程中,这些线程通常都有相同的几率被分配到待处理请求。 + +当网络线程拿到请求后,将请求放入到一个共享请求队列中。Broker 端还有个 IO 线程池,负责从该队列中取出请求,执行真正的处理。如果是 PRODUCE 生产请求,则将消息写入到底层的磁盘日志中;如果是 FETCH 请求,则从磁盘或页缓存中读取消息。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170736893.jpeg) + +Purgatory 是用来**缓存延时请求**(Delayed Request)的。**所谓延时请求,就是那些一时未满足条件不能立刻处理的请求**。比如设置了 acks=all 的 PRODUCE 请求,一旦设置了 acks=all,那么该请求就必须等待 ISR 中所有副本都接收了消息后才能返回,此时处理该请求的 IO 线程就必须等待其他 Broker 的写入结果。当请求不能立刻处理时,它就会暂存在 Purgatory 中。稍后一旦满足了完成条件,IO 线程会继续处理该请求,并将 Response 放入对应网络线程的响应队列中。 + +## 消费者组重平衡全流程解析 + +重平衡的 3 个触发条件: + +1. 组成员数量发生变化。 +2. 订阅主题数量发生变化。 +3. 订阅主题的分区数发生变化。 + +消费者端重平衡流程: + +Rebalance 是通过消费者群组中的称为“群主”消费者客户端进行的\*\*。 + +(1)选择群主 + +当消费者要加入群组时,会向群组协调器发送一个 JoinGroup 请求。第一个加入群组的消费者将成为“群主”。**群主从协调器那里获取群组的活跃成员列表,并负责给每一个消费者分配分区**。 + +> 所谓协调者,在 Kafka 中对应的术语是 Coordinator,它专门为 Consumer Group 服务,负责为 Group 执行 Rebalance 以及提供位移管理和组成员管理等。具体来讲,Consumer 端应用程序在提交位移时,其实是向 Coordinator 所在的 Broker 提交位移。同样地,当 Consumer 应用启动时,也是向 Coordinator 所在的 Broker 发送各种请求,然后由 Coordinator 负责执行消费者组的注册、成员管理记录等元数据管理操作。 + +(2)消费者通过向被指派为群组协调器(Coordinator)的 Broker 定期发送心跳来维持它们和群组的从属关系以及它们对分区的所有权。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502070723810.png) + +(3)群主从群组协调器获取群组成员列表,然后给每一个消费者进行分配分区 Partition。有两种分配策略:Range 和 RoundRobin。 + +- **Range 策略**,就是把若干个连续的分区分配给消费者,如存在分区 1-5,假设有 3 个消费者,则消费者 1 负责分区 1-2, 消费者 2 负责分区 3-4,消费者 3 负责分区 5。 +- **RoundRoin 策略**,就是把所有分区逐个分给消费者,如存在分区 1-5,假设有 3 个消费者,则分区 1->消费 1,分区 2->消费者 2,分区 3>消费者 3,分区 4>消费者 1,分区 5->消费者 2。 + +(4)群主分配完成之后,把分配情况发送给群组协调器。 + +(5)群组协调器再把这些信息发送给消费者。**每个消费者只能看到自己的分配信息,只有群主知道所有消费者的分配信息**。 + +## 你一定不能错过的 Kafka 控制器 + +**控制器组件(Controller),是 Apache Kafka 的核心组件。它的主要作用是在 Apache ZooKeeper 的帮助下管理和协调整个 Kafka 集群**。每台 Broker 都能充当控制器,**第一个成功创建 `/controller` 节点的 Broker 会被指定为控制器**。 + +**ZooKeeper 是一个提供高可靠性的分布式协调服务框架**。ZooKeeper 常被用来实现**集群成员管理、分布式锁、领导者选举**等功能。Kafka 控制器大量使用 Watch 功能实现对集群的协调管理。 + +下图展示了 Kafka 在 ZooKeeper 中创建的 znode 分布: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170738496.jpeg) + +控制器的职责: + +- **主题管理(创建、删除、增加分区)** +- **分区重分配** +- **Preferred 领导者选举** +- **集群成员管理(新增 Broker、Broker 主动关闭、Broker 宕机)** +- **数据服务** + +控制器保存的数据: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170738150.jpeg) + +控制器故障转移 + +**故障转移指的是,当运行中的控制器突然宕机或意外终止时,Kafka 能够快速地感知到,并立即启用备用控制器来代替之前失败的控制器**。这个过程就被称为 Failover,该过程是自动完成的,无需你手动干预。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170739678.jpeg) + +## 关于高水位和 Leader Epoch 的讨论 + +水位一词多用于流式处理领域,比如,Spark Streaming 或 Flink 框架中都有水位的概念。 + +水位是一个单调增加且表征最早未完成工作(oldest work not yet completed)的时间戳。 + +### 高水位的作用 + +在 Kafka 中,高水位的作用主要有 2 个。 + +- 定义消息可见性,即用来标识分区下的哪些消息是可以被消费者消费的。 + - 在分区高水位以下的消息被认为是已提交消息,反之就是未提交消息。消费者只能消费已提交消息。 + - **同一个副本对象,其高水位值不会大于 LEO 值**。 + - **分区的高水位就是其 Leader 副本的高水位**。 +- 帮助 Kafka 完成副本同步。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170739112.jpeg) + +### 高水位更新机制 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170740521.jpeg) + +Broker 0 上保存了某分区的 Leader 副本和所有 Follower 副本的 LEO 值,而 Broker 1 上仅仅保存了该分区的某个 Follower 副本。Kafka 把 Broker 0 上保存的这些 Follower 副本又称为**远程副本**(Remote Replica)。Kafka 副本机制在运行过程中,会更新 Broker 1 上 Follower 副本的高水位和 LEO 值,同时也会更新 Broker 0 上 Leader 副本的高水位和 LEO 以及所有远程副本的 LEO,但它不会更新远程副本的高水位值,也就是我在图中标记为灰色的部分。 + +为什么要在 Broker 0 上保存这些远程副本呢?其实,它们的主要作用是,**帮助 Leader 副本确定其高水位,也就是分区高水位**。 + +### 副本同步机制解析 + +首先是初始状态。下面这张图中的 remote LEO 就是刚才的远程副本的 LEO 值。在初始状态时,所有值都是 0。 + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170740419.jpeg) + +当生产者给主题分区发送一条消息后,状态变更为: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170741966.jpeg) + +此时,Leader 副本成功将消息写入了本地磁盘,故 LEO 值被更新为 1。 + +Follower 再次尝试从 Leader 拉取消息。和之前不同的是,这次有消息可以拉取了,因此状态进一步变更为: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170741603.jpeg) + +这时,Follower 副本也成功地更新 LEO 为 1。此时,Leader 和 Follower 副本的 LEO 都是 1,但各自的高水位依然是 0,还没有被更新。**它们需要在下一轮的拉取中被更新**,如下图所示: + +![](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170741850.jpeg) + +在新一轮的拉取请求中,由于位移值是 0 的消息已经拉取成功,因此 Follower 副本这次请求拉取的是位移值=1 的消息。Leader 副本接收到此请求后,更新远程副本 LEO 为 1,然后更新 Leader 高水位为 1。做完这些之后,它会将当前已更新过的高水位值 1 发送给 Follower 副本。Follower 副本接收到以后,也将自己的高水位值更新成 1。至此,一次完整的消息同步周期就结束了。事实上,Kafka 就是利用这样的机制,实现了 Leader 和 Follower 副本之间的同步。 + +### Leader Epoch + +所谓 Leader Epoch,我们大致可以认为是 Leader 版本。它由两部分数据组成。 + +1. Epoch。一个单调增加的版本号。每当副本领导权发生变更时,都会增加该版本号。小版本号的 Leader 被认为是过期 Leader,不能再行使 Leader 权力。 +2. 起始位移(Start Offset)。Leader 副本在该 Epoch 值上写入的首条消息的位移。 + +## 主题管理知多少 + +**Kafka 提供了自带的 kafka-topics 脚本,用于帮助用户创建主题**。 + +特殊主题: + +- consumer_offsets +- transaction_state + +## Kafka 动态配置了解下? + +略 + +## 怎么重设消费者组位移? + +略 + +## 常见工具脚本大汇总 + +略 + +## KafkaAdminClient:Kafka 的运维利器 + +略 + +## Kafka 认证机制用哪家? + +略 + +## 云环境下的授权该怎么做? + +略 + +## 跨集群备份解决方案 MirrorMaker + +略 + +## 你应该怎么监控 Kafka? + +略 + +## 主流的 Kafka 监控框架 + +略 + +## 调优 Kafka,你做到了吗? + +略 + +## 从 0 搭建基于 Kafka 的企业级实时日志流处理平台 + +略 + +## Kafka Streams 与其他流处理平台的差异在哪里? + +略 + +## Kafka Streams DSL 开发实例 + +略 + +## Kafka Streams 在金融领域的应用 + +略 + +## 参考资料 + +- [**极客时间教程 - Kafka 核心技术与实战**](https://time.geekbang.org/column/intro/100029201) diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/13.Kafka\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\350\257\273\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Kafka\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\350\257\273\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/13.Kafka\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\350\257\273\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Kafka\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\350\257\273\347\254\224\350\256\260.md" index 60bab6d270..621fa908b7 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/13.Kafka\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\350\257\273\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-Kafka\346\240\270\345\277\203\346\272\220\347\240\201\350\247\243\350\257\273\347\254\224\350\256\260.md" @@ -1,7 +1,6 @@ --- title: 《Kafka 核心源码解读》笔记 date: 2022-07-03 14:53:05 -order: 13 categories: - 笔记 - 分布式 @@ -11,7 +10,7 @@ tags: - 分布式通信 - MQ - Kafka -permalink: /pages/f5f5ef/ +permalink: /pages/5ad2bb8a/ --- # 《Kafka 核心源码解读》笔记 @@ -436,4 +435,4 @@ read 方法中有 4 个参数: ## 参考资料 -- [Kafka 核心源码解读](https://time.geekbang.org/column/intro/304) \ No newline at end of file +- [Kafka 核心源码解读](https://time.geekbang.org/column/intro/304) diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC\345\256\236\346\210\230\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-RPC\345\256\236\346\210\230\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC\345\256\236\346\210\230\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-RPC\345\256\236\346\210\230\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\347\254\224\350\256\260.md" index ff11be5a2e..28f2ecc650 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/01.RPC\345\256\236\346\210\230\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-RPC\345\256\236\346\210\230\344\270\216\346\240\270\345\277\203\345\216\237\347\220\206\347\254\224\350\256\260.md" @@ -1,7 +1,6 @@ --- title: 《RPC 实战与核心原理》笔记 date: 2022-06-19 09:48:17 -order: 01 categories: - 笔记 - 分布式 @@ -10,7 +9,7 @@ tags: - 分布式 - 分布式通信 - RPC -permalink: /pages/4b43b4/ +permalink: /pages/cb0fe01f/ --- # 《RPC 实战与核心原理》笔记 @@ -600,4 +599,4 @@ class GenericService { ## 参考资料 -- [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) \ No newline at end of file +- [RPC 实战与核心原理](https://time.geekbang.org/column/intro/100046201) diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/11.\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260.md" similarity index 76% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/11.\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260.md" index 52196d7e02..84ed481a8e 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/11.\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/21.\345\210\206\345\270\203\345\274\217\351\200\232\344\277\241/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\266\210\346\201\257\351\230\237\345\210\227\351\253\230\346\211\213\350\257\276\347\254\224\350\256\260.md" @@ -1,7 +1,6 @@ --- title: 《消息队列高手课》笔记 date: 2022-05-11 20:59:25 -order: 11 categories: - 笔记 - 分布式 @@ -10,7 +9,7 @@ tags: - 分布式 - 分布式通信 - MQ -permalink: /pages/e3eb31/ +permalink: /pages/8b8c04a6/ --- # 《消息队列高手课》笔记 @@ -19,11 +18,12 @@ permalink: /pages/e3eb31/ 消息队列的应用 -- 异步处理 - - 快速响应 - - 减少等待,提升性能 -- 流量控制 -- 服务解耦 +- **异步处理** +- **系统解耦** +- **流量削峰** +- **系统间通信** +- **数据缓冲** +- **数据一致性** ## 该如何选择消息队列? @@ -32,7 +32,7 @@ permalink: /pages/e3eb31/ - **技术生态适配性**:客户端对各种编程语言的支持。比如:如果使用 MQ 的都是 Java 应用,那么 ActiveMQ、RabbitMQ、RocketMQ、Kafka 都可以。如果需要支持其他语言,那么 RMQ 比较合适,因为它支持的编程语言比较丰富。如果 MQ 是应用于大数据或流式计算,那么 Kafka 几乎是标配。如果是应用于在线业务系统,那么 Kafka 就不合适了,可以考虑 RabbitMQ、 RocketMQ 很合适。 - **高可用**:应用于线上的准入标准。 - **高性能**:具备足够好的性能,能满足绝大多数场景的性能要求。 -- **业务场景的适应性**:不同业务场景,会有不同的诉求,此时要根据不同 MQ 的特性针对性选择。 +- **可靠传输** ### 主流 MQ @@ -48,23 +48,31 @@ permalink: /pages/e3eb31/ | 支持编程语言 | | 非常多 | Java | Scala、Java | | 学习成本 | | 采用 ErLang 开发,比较小众,不利于扩展和二次开发 | 采用 Java 开发,且贡献者多为中国人,容易读懂源码 | 使用 Scala 和 Java 开发,容易读懂源码 | -RabbitMQ - -突出亮点 - -1. 支持的编程语言最多 -2. 支持非常灵活的路由配置 - -明显短板 - -1. 对消息堆积的支持并不好 -2. 性能差强人意 +- RabbitMQ + - 优点 + - 支持的编程语言最多 + - 支持非常灵活的路由配置 + - 缺点 + - 对消息堆积的支持并不好 + - 性能差强人意 +- RocketMQ + - 优点 + - 有着不错的性能,稳定性和可靠性 + - 支持事务 + - 缺点 + - 国外认同弱于其他流行 MQ +- Kafka + - 优点 + - 可靠、稳定、性能高 + - 技术生态最健全,尤其是在大数据和流计算领域 + - 缺点 + - 同步收发响应延时比较高,不太适合在线业务 ## 消息模型:主题和队列有什么区别? ### 队列模型 -最初的消息队列,就是一个严格意义上的队列。在计算机领域,“队列(Queue)”是一种数据结构,有完整而严格的定义。 +队列是先进先出(FIFO, First-In-First-Out)的线性表(Linear List)。在具体应用中通常用链表或者数组来实现。队列只允许在后端(称为 rear)进行插入操作,在前端(称为 front)进行删除操作。 **早期的消息队列,就是按照“队列”的数据结构来设计的。**生产者(Producer)发消息就是入队操作,消费者(Consumer)收消息就是出队也就是删除操作,服务端存放消息的容器自然就称为“队列”。 @@ -72,79 +80,39 @@ RabbitMQ 如果需要将一份消息数据分发给多个消费者,要求每个消费者都能收到全量的消息。此时,单个队列就满足不了需求,一个可行的解决方式是,为每个消费者创建一个单独的队列,让生产者发送多份。显然这是个比较蠢的做法,同样的一份消息数据被复制到多个队列中会浪费资源,更重要的是,生产者必须知道有多少个消费者。为每个消费者单独发送一份消息,这实际上违背了消息队列“解耦”这个设计初衷。 -### 发布 - 订阅模型(Publish-Subscribe Pattern) +### 发布 订阅模型(Publish-Subscribe Pattern) 在发布 - 订阅模型中,消息的发送方称为发布者(Publisher),消息的接收方称为订阅者(Subscriber),服务端存放消息的容器称为主题(Topic)。发布者将消息发送到主题中,订阅者在接收消息之前需要先“订阅主题”。“订阅”在这里既是一个动作,同时还可以认为是主题在消费时的一个逻辑副本,每份订阅中,订阅者都可以接收到主题的所有消息。 -队列模型和发布 - 订阅模型最大的区别就是:**一份消息数据能不能被消费多次的问题**。 +**队列模型和发布订阅模型最大的区别就是:一份消息数据能不能被消费多次的问题**。 ### RabbitMQ 的消息模型 -RabbitMQ,它是少数依然坚持使用队列模型的产品之一。那它是怎么解决多个消费者的问题呢? - 在 RabbitMQ 中,Exchange 位于生产者和队列之间,生产者并不关心将消息发送给哪个队列,而是将消息发送给 Exchange,由 Exchange 上配置的策略来决定将消息投递到哪些队列中。 -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220511211021.jfif) +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220511211021.jfif) 同一份消息如果需要被多个消费者来消费,需要配置 Exchange 将消息发送到多个队列,每个队列中都存放一份完整的消息数据,可以为一个消费者提供消费服务。这也可以变相地实现新发布 - 订阅模型中,“一份消息数据可以被多个订阅者来多次消费”这样的功能。 ### RocketMQ 的消息模型 -RocketMQ 使用的消息模型是标准的发布 - 订阅模型 - -但是,在 RocketMQ 也有队列(Queue)这个概念,并且队列在 RocketMQ 中是一个非常重要的概念 - -几乎所有的消息队列产品都使用一种非常朴素的“请求 - 确认”机制,确保消息不会在传递过程中由于网络或服务器故障丢失。具体的做法也非常简单。在生产端,生产者先将消息发送给服务端,也就是 Broker,服务端在收到消息并将消息写入主题或者队列中后,会给生产者发送确认的响应。 - -如果生产者没有收到服务端的确认或者收到失败的响应,则会重新发送消息;在消费端,消费者在收到消息并完成自己的消费业务逻辑(比如,将数据保存到数据库中)后,也会给服务端发送消费成功的确认,服务端只有收到消费确认后,才认为一条消息被成功消费,否则它会给消费者重新发送这条消息,直到收到对应的消费成功确认。 - -这个确认机制很好地保证了消息传递过程中的可靠性,但是,引入这个机制在消费端带来了一个不小的问题。什么问题呢?为了确保消息的有序性,在某一条消息被成功消费之前,下一条消息是不能被消费的,否则就会出现消息空洞,违背了有序性这个原则。 - -也就是说,每个主题在任意时刻,至多只能有一个消费者实例在进行消费,那就没法通过水平扩展消费者的数量来提升消费端总体的消费性能。为了解决这个问题,RocketMQ 在主题下面增加了队列的概念。 - -**每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。**需要注意的是,RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。 - -RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现的。每个消费组都消费主题中一份完整的消息,不同消费组之间消费进度彼此不受影响,也就是说,一条消息被 Consumer Group1 消费过,也会再给 Consumer Group2 消费。 - -消费组中包含多个消费者,同一个组内的消费者是竞争消费的关系,每个消费者负责消费组内的一部分消息。如果一条消息被消费者 Consumer1 消费了,那同组的其他消费者就不会再收到这条消息。 +RocketMQ 使用的消息模型是标准的发布 - 订阅模型。但是,在 RocketMQ 也有队列(Queue)这个概念。**每个主题包含多个队列,通过多个队列来实现多实例并行生产和消费。**需要注意的是,RocketMQ 只在队列上保证消息的有序性,主题层面是无法保证消息的严格顺序的。 在 Topic 的消费过程中,由于消息需要被不同的组进行多次消费,所以消费完的消息并不会立即被删除,这就需要 RocketMQ 为每个消费组在每个队列上维护一个消费位置(Consumer Offset),这个位置之前的消息都被消费过,之后的消息都没有被消费过,每成功消费一条消息,消费位置就加一。这个消费位置是非常重要的概念,我们在使用消息队列的时候,丢消息的原因大多是由于消费位置处理不当导致的。 +> Kafka 的消息模型和 RocketMQ 是完全一样的。只是在 Kafka 中,将 Queue 这个概念称为分区(Partition) + ## 如何利用事务消息实现分布式事务? 事务消息需要消息队列提供相应的功能才能实现,Kafka 和 RocketMQ 都提供了事务相关功能。 -- **Kafka** 的解决方案是:直接抛出异常,让用户自行处理。用户可以在业务代码中反复重试提交,直到提交成功,或者删除之前修改的数据记录进行事务补偿。 -- **RocketMQ** 的解决方案是:通过事务反查机制来解决事务消息提交失败的问题。如果 Producer 在提交或者回滚事务消息时发生网络异常,RocketMQ 的 Broker 没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。为了支撑这个事务反查机制,业务代码需要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是成功还是失败。 - -### RocketMQ 事务消息流程 - -基于 MQ 的分布式事务方案其实是对本地消息表的封装,将本地消息表基于 MQ 内部,其他方面的协议基本与本地消息表一致。下面主要基于 RocketMQ 4.3 之后的版本介绍 MQ 的分布式事务方案。 - -在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ 的事务消息相对于普通 MQ,相对于提供了 2PC 的提交接口,方案如下: +**Kafka** 的解决方案是:直接抛出异常,让用户自行处理。用户可以在业务代码中反复重试提交,直到提交成功,或者删除之前修改的数据记录进行事务补偿。 -**正常情况——事务主动方发消息** 这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下: +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170725456.jpeg) -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220512194221.png) +**RocketMQ** 的解决方案是:通过事务反查机制来解决事务消息提交失败的问题。如果 Producer 在提交或者回滚事务消息时发生网络异常,RocketMQ 的 Broker 没有收到提交或者回滚的请求,Broker 会定期去 Producer 上反查这个事务对应的本地事务的状态,然后根据反查结果决定提交或者回滚这个事务。为了支撑这个事务反查机制,业务代码需要实现一个反查本地事务状态的接口,告知 RocketMQ 本地事务是成功还是失败。 -1. 发送方向 MQ 服务端(MQ Server)发送 half 消息。 -2. MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。 -3. 发送方开始执行本地事务逻辑。 -4. 发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。 -5. MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。 - -**异常情况——事务主动方消息恢复** 在断网或者应用重启等异常情况下,图中 4 提交的二次确认超时未到达 MQ Server,此时处理逻辑如下: - -![img](https://raw.githubusercontent.com/dunwu/images/master/snap/20220512194230.png) - -5. MQ Server 对该消息发起消息回查。 -6. 发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。 -7. 发送方根据检查得到的本地事务的最终状态再次提交二次确认 -8. MQ Server 基于 commit / rollback 对消息进行投递或者删除 - -> **思考**:为什么不等待写业务表成功后再向消息队列发送提交消息呢? -> -> 因为可能存在这样情况:写业务表成功了,但是还没来得及发消息,节点就宕机了。 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170726651.jpeg) ### MQ 事务方案总结 @@ -155,45 +123,43 @@ RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现 缺点是: -- 一次消息发送需要两次网络请求(half 消息 + commit/rollback 消息) +- 一次消息发送需要两次网络请求 (half 消息 + commit/rollback 消息) - 业务处理服务需要实现消息状态回查接口 -## 如何确保消息不会丢失? +## 如何确保消息不会丢失? + +检测消息丢失方法: -- 在生产阶段,你需要捕获消息发送的错误,并重发消息。 -- 在存储阶段,你可以通过配置刷盘和复制相关的参数,让消息写入到多个副本的磁盘上,来确保消息不会因为某个 Broker 宕机或者磁盘损坏而丢失。 -- 在消费阶段,你需要在处理完全部消费业务逻辑之后,再发送消费确认。 +**利用消息队列的有序性来验证是否有消息丢失**:在 Producer 端,我们给每个发出的消息附加一个连续递增的序号,然后在 Consumer 端来检查这个序号的连续性。 + +确保消息不丢失: + +- **生产**阶段:捕获消息发送的错误,并针对性进行容错处理。 +- **存储**阶段:数据必须设置副本,并且写数据需要保证所有副本都写入成功才视为提交成功。这样可以保证,即使主副本不可用,使用从副本替代,也包含最新数据。 +- **消费**阶段:所有数据处理完毕,再手动提交消费偏移量。 ## 如何处理消费过程中的重复消息? 在 MQTT 协议中,给出了三种传递消息时能够提供的服务质量标准,这三种服务质量从低到高依次是: -- **At most once**: 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,**没什么消息可靠性保证,允许丢消息**。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。 -- **At least once**: 至少一次。消息在传递时,至少会被送达一次。也就是说,**不允许丢消息,但是允许有少量重复消息**出现。 -- **Exactly once**:恰好一次。消息在传递时,只会被送达一次,**不允许丢失也不允许重复**,这个是最高的等级。 +- **At most once**:- 至多一次。消息在传递时,最多会被送达一次。换一个说法就是,**没什么消息可靠性保证,允许丢消息**。一般都是一些对消息可靠性要求不太高的监控场景使用,比如每分钟上报一次机房温度数据,可以接受数据少量丢失。 +- **At least once**:- 至少一次。消息在传递时,至少会被送达一次。也就是说,**不允许丢消息,但是允许有少量重复消息**出现。 +- **Exactly once** - 恰好一次。消息在传递时,只会被送达一次,**不允许丢失也不允许重复**,这个是最高的等级。 现在常用的绝大部分消息队列提供的服务质量都是 At least once,包括 RocketMQ、RabbitMQ 和 Kafka 都是这样。也就是说,消息队列很难保证消息不重复。 -一般解决重复消息的办法是,在消费端,让我们消费消息的操作具备幂等性。一个幂等操作的特点是,**其任意多次执行所产生的影响均与一次执行的影响相同。** - -如果我们系统消费消息的业务逻辑具备幂等性,那就不用担心消息重复的问题了,因为同一条消息,消费一次和消费多次对系统的影响是完全一样的。也就可以认为,消费多次等于消费一次。 +一般解决重复消息的办法是,在消费端,让我们消费消息的操作具备幂等性。一个幂等操作的特点是,**其任意多次执行所产生的影响均与一次执行的影响相同。**如果我们系统消费消息的业务逻辑具备幂等性,那就不用担心消息重复的问题了,因为同一条消息,消费一次和消费多次对系统的影响是完全一样的。也就可以认为,消费多次等于消费一次。 从对系统的影响结果来说:**At least once + 幂等消费 = Exactly once。** 常用的设计幂等操作的方法: -1. **利用数据库的唯一约束实现幂等** -2. **为更新的数据设置前置条件**:设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。 +1. **利用数据库的唯一约束实现幂等**:INSERT IF NOT EXIST +2. **为更新的数据设置前置条件**:设置一个前置条件,如果满足条件就更新数据,否则拒绝更新数据,在更新数据的时候,同时变更前置条件中需要判断的数据。例如:采用乐观锁方式,为数据增加版本号,每次更数据前,比较当前数据的版本号是否和消息中的版本号一致,如果不一致就拒绝更新数据,更新数据的同时将版本号 +1,一样可以实现幂等更新。 3. **记录并检查操作**:在发送消息时,给每条消息指定一个全局唯一的 ID,消费时,先根据这个 ID 检查这条消息是否有被消费过,如果没有消费过,才更新数据,然后将消费状态置为已消费。——此处涉及分布式 ID 知识点,可以使用类似 GUID、雪花算法 等方式来实现 ## 消息积压了该如何处理? -在使用消息队列的系统中,对于性能的优化,主要体现在生产者和消费者这一收一发两部分的业务逻辑中。对于消息队列本身的性能,不需要太关注。 - -主要原因是,对于绝大多数使用消息队列的业务来说,消息队列本身的处理能力要远大于业务系统的处理能力。主流消息队列的单个节点,消息收发的性能可以达到每秒钟处理几万至几十万条消息的水平,还可以通过水平扩展 Broker 的实例数成倍地提升处理能力。 - -而一般的业务系统需要处理的业务逻辑远比消息队列要复杂,单个节点每秒钟可以处理几百到几千次请求,已经可以算是性能非常好的了。所以,对于消息队列的性能优化,我们更关注的是,**在消息的收发两端,我们的业务代码怎么和消息队列配合,达到一个最佳的性能。** - ### 发送端性能优化 **发送消息的性能上不去,你需要优先检查一下,是不是发消息之前的业务逻辑耗时太多导致的**。对于发送消息的业务逻辑,只需要注意设置合适的并发和批量大小,就可以达到很好的发送性能。 @@ -210,11 +176,11 @@ RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现 需要先分析消息积压的原因:是发送变快了,还是消费变慢了。大部分消息队列都内置了监控的功能,只要通过监控数据,很容易确定是哪种原因。 -如果是因为促销或抢购等原因,导致消息陡增,短时间内不太可能优化消费端的代码来提升消费性能,唯一的方法是通过扩容消费端的实例数来提升总体的消费能力。 +- 如果是因为促销或抢购等原因,导致消息陡增,短时间内不太可能优化消费端的代码来提升消费性能,唯一的方法是通过扩容消费端的实例数来提升总体的消费能力。 -如果短时间内没有足够的服务器资源进行扩容,没办法的办法是,将系统降级,通过关闭一些不重要的业务,减少发送方发送的数据量,最低限度让系统还能正常运转,服务一些重要业务。 +- 如果短时间内没有足够的服务器资源进行扩容,没办法的办法是,将系统降级,通过关闭一些不重要的业务,减少发送方发送的数据量,最低限度让系统还能正常运转,服务一些重要业务。 -如果监控到消费变慢了,你需要检查你的消费实例,分析一下是什么原因导致消费变慢。优先检查一下日志是否有大量的消费错误,如果没有错误的话,可以通过打印堆栈信息,看一下你的消费线程是不是卡在什么地方不动了,比如触发了死锁或者卡在等待某些资源上了。 +- 如果监控到消费变慢了,你需要检查你的消费实例,分析一下是什么原因导致消费变慢。优先检查一下日志是否有大量的消费错误,如果没有错误的话,可以通过打印堆栈信息,看一下你的消费线程是不是卡在什么地方不动了,比如触发了死锁或者卡在等待某些资源上了。 ## 学习开源代码该如何入手? @@ -225,23 +191,21 @@ RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现 - 这个项目如何使用 - 这个项目适用于什么场景 - 这个项目有哪些优点、缺点 -- 。。。 +- 。 (2)由点及面的阅读源码 -不要泛泛而读,容易迷失。 - -最好带着目的性,带着问题去阅读源码,最好是带着问题的答案去读源码 +不要泛泛而读,容易迷失。最好带着目的性,带着问题去阅读源码,最好是带着问题的答案去读源码。 ## 如何使用异步设计提升系统性能? 异步编程,可以减少或者避免线程等待,从而提高处理速度。但是,其增加了程序复杂度,应酌情使用。 -## 如何实现高性能的异步网络传输? +Java 中比较常用的异步框架有 Java8 内置的 [CompletableFuture](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/CompletableFuture.html) 和 ReactiveX 的 [RxJava](https://github.com/ReactiveX/RxJava)。 -系统一般可以分为 IO 密集型应用和计算密集型应用。 +## 如何实现高性能的异步网络传输? -大多数业务系统都属于 IO 密集型应用。最常用的 IO 资源有磁盘 IO 和带宽 IO。由于 IO 相较于内存计算,耗时较高,所以往往成为性能优化的关键。 +系统一般可以分为 IO 密集型应用和计算密集型应用。大多数业务系统都属于 IO 密集型应用。最常用的 IO 资源有磁盘 IO 和带宽 IO。由于 IO 相较于内存计算,耗时较高,所以往往成为性能优化的关键。 提升 IO 效率的关键在于减少 IO 等待时间,在大量连接请求的时候,如果单线程,显然阻塞时间较长,所以,一般应采用并发 IO 模型。但是,线程数过多时,线程本身造成的 CPU 上下文切换,竞态造成的冲突都会造成额外的开销,导致 CPU 负载升高,从而降低系统整体性能。所以,理想的 IO 模型应该是一个能够复用少量线程的并发 IO 模型。这个模型的当前答案就是 NIO,其最具代表性的框架就是 Netty。其核心原理就是通过多路复用,来提升 IO 效率。 @@ -263,8 +227,14 @@ RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现 **使用顺序读写提升磁盘 IO 性能** +操作系统每次从磁盘读写数据的时候,需要先寻址,也就是先要找到数据在磁盘上的物理位置,然后再进行数据读写。如果是机械硬盘,这个寻址需要比较长的时间,因为它要移动磁头,这是个机械运动,机械硬盘工作的时候会发出咔咔的声音,就是移动磁头发出的声音。 + +顺序读写相比随机读写省去了大部分的寻址时间,它只要寻址一次,就可以连续地读写下去,所以说,性能要比随机读写要好很多。 + **利用 PageCache 加速消息读写** +在 Kafka 中,它会利用 PageCache 加速消息读写。 + - PageCache 就是操作系统在内存中给磁盘上的文件建立的缓存。调用系统的 API 读写文件的时候,不会直接去读写磁盘上的文件,应用程序实际操作的都是 PageCache,也就是文件在内存中缓存的副本。 - 应用程序在写入文件的时候,操作系统会先把数据写入到内存中的 PageCache,然后再一批一批地写到磁盘上。读取文件的时候,也是从 PageCache 中来读取数据,这时候会出现两种可能情况。一种是 PageCache 中有数据,那就直接读取,这样就节省了从磁盘上读取数据的时间;另一种情况是,PageCache 中没有数据,这时候操作系统会引发一个缺页中断,应用程序的读取线程会被阻塞,操作系统把数据从文件中复制到 PageCache 中,然后应用程序再从 PageCache 中继续把数据读出来,这时会真正读一次磁盘上的文件,这个读的过程就会比较慢。 - 用户的应用程序在使用完某块 PageCache 后,操作系统并不会立刻就清除这个 PageCache,而是尽可能地利用空闲的物理内存保存这些 PageCache,除非系统内存不够用,操作系统才会清理掉一部分 PageCache。清理的策略一般是 LRU 或它的变种算法,这个算法我们不展开讲,它保留 PageCache 的逻辑是:优先保留最近一段时间最常使用的那些 PageCache。 @@ -285,6 +255,8 @@ RocketMQ 中,订阅者的概念是通过消费组(Consumer Group)来体现 Kafka 使用零拷贝技术可以把这个复制次数减少一次,上面的 2、3 步骤两次复制合并成一次复制。直接从 PageCache 中把数据复制到 Socket 缓冲区中,这样不仅减少一次数据复制,更重要的是,由于不用把数据复制到用户内存空间,DMA 控制器可以直接完成数据复制,不需要 CPU 参与,速度更快。 +零拷贝操作,实际上是调用系统 API `sendfile` 实现的。 + ## 缓存策略:如何使用缓存来减少磁盘 IO? 略 @@ -301,13 +273,13 @@ Kafka 使用零拷贝技术可以把这个复制次数减少一次,上面的 2 **数据压缩不仅能节省存储空间,还可以用于提升网络传输性能。** -压缩和解压的操作都是计算密集型的操作,非常耗费 CPU 资源。如果你的应用处理业务逻辑就需要耗费大量的 CPU 资源,就不太适合再进行压缩和解压。数据压缩,它本质上是用 CPU 资源换取存储资源,或者说是用压缩解压的时间来换取存储的空间,这个买卖是不是划算,需要根据实际情况先衡量一下。 +压缩和解压的操作都是计算密集型的操作,非常耗费 CPU 资源。如果你的应用处理业务逻辑就需要耗费大量的 CPU 资源,就不太适合再进行压缩和解压。**数据压缩本质上是用时间换空间**。这个买卖是不是划算,需要根据实际情况先衡量一下。 目前常用的压缩算法包括:ZIP,GZIP,SNAPPY,LZ4 等等。在选择压缩算法的时候,需要综合考虑压缩时间和压缩率两个因素,被压缩数据的内容也是影响压缩时间和压缩率的重要因素,必要的时候可以先用业务数据做一个压缩测试,这样有助于选择最合适的压缩算法。一般来说,压缩率越高的算法,压缩耗时也越高。如果是对性能要求高的系统,可以选择压缩速度快的算法,比如 LZ4;如果需要更高的压缩比,可以考虑 GZIP 或者压缩率更高的 XZ 等算法。 另外一个影响压缩率的重要因素是压缩分段的大小,你需要根据业务情况选择一个合适的分段策略,在保证不错的压缩率的前提下,尽量减少解压浪费。 -Kafka 在生产者上,对每批消息进行压缩,批消息在服务端不解压,消费者在收到消息之后再进行解压。简单地说,Kafka 的压缩和解压都是在客户端完成的。 +Kafka 在生产者上,对每批消息进行压缩,批消息在服务端不解压,消费者在收到消息之后再进行解压。简单地说,**Kafka 的压缩和解压都是在客户端完成的**。 ## RocketMQ Producer 源码分析:消息生产的实现过程 @@ -322,7 +294,7 @@ Kafka 消费模型的几个要点: - Kafka 的每个 Consumer(消费者)实例属于一个 ConsumerGroup(消费组); - 在消费时,ConsumerGroup 中的每个 Consumer 独占一个或多个 Partition(分区); - 对于每个 ConsumerGroup,在任意时刻,每个 Partition 至多有 1 个 Consumer 在消费; -- 每个 ConsumerGroup 都有一个 Coordinator(协调者)负责分配 Consumer 和 Partition 的对应关系,当 Partition 或是 Consumer 发生变更是,会触发 reblance(重新分配)过程,重新分配 Consumer 与 Partition 的对应关系; +- 每个 ConsumerGroup 都有一个 Coordinator(协调者)负责分配 Consumer 和 Partition 的对应关系,当 Partition 或是 Consumer 发生变更是,会触发 reblance(重新分配)过程,重新分配 Consumer 与 Partition 的对应关系; - Consumer 维护与 Coordinator 之间的心跳,这样 Coordinator 就能感知到 Consumer 的状态,在 Consumer 故障的时候及时触发 rebalance。 发送请求时,构建 Request 对象,暂存入发送队列,但不立即发送,而是等待合适的时机批量发送。并且,用回调或者 RequestFeuture 方式,预先定义好如何处理响应的逻辑。在收到 Broker 返回的响应之后,也不会立即处理,而是暂存在队列中,择机处理。那这个择机策略就比较复杂了,有可能是需要读取响应的时候,也有可能是缓冲区满了或是时间到了,都有可能触发一次真正的网络请求,也就是在 poll() 方法中发送所有待发送 Request 并处理所有 Response。 @@ -335,6 +307,8 @@ Kafka 消费模型的几个要点: ### RocketMQ 如何实现复制 +在 RocketMQ 中,复制的基本单位是 Broker,也就是服务端的进程。复制采用的也是主从方式,通常情况下配置成一主一从,也可以支持一主多从。 + RocketMQ 提供新、老两种复制方式:传统的主从模式和新的基于 Dledger 的复制方式。传统的主从模式性能更好,但灵活性和可用性稍差,而基于 Dledger 的复制方式,在 Broker 故障的时候可以自动选举出新节点,可用性更好,性能稍差,并且资源利用率更低一些。 RocketMQ 引入 Dledger,通过 Dledger 来完成复制。Dledger 在写入消息的时候,要求至少消息复制到半数以上的节点之后,才给客户端返回写入成功,并且它是支持通过选举来动态切换主节点的。 @@ -349,14 +323,20 @@ Kafka 使用 ZooKeeper 来监控每个分区的多个节点,如果发现某个 ### RocketMQ 客户端如何在集群中找到正确的节点? -NameServer 在集群中起到的一个核心作用就是,为客户端提供路由信息,帮助客户端找到对应的 Broker。每个 NameServer 节点上都保存了集群所有 Broker 的路由信息,可以独立提供服务。Broker 会与所有 NameServer 节点建立长连接,定期上报 Broker 的路由信息。客户端会选择连接某一个 NameServer 节点,定期获取订阅主题的路由信息,用于 Broker 寻址。 +任何一个弹性分布式集群,都需要一个类似于 NameServer 服务,来帮助访问集群的客户端寻找集群中的节点。 -不仅仅是 RocketMQ,任何一个弹性分布式集群,都需要一个类似于 NameServer 服务,来帮助访问集群的客户端寻找集群中的节点,这个服务一般称为 NamingService。 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170725763.jpeg) 在 RocketMQ 中,NameServer 是一个独立的进程,为 Broker、生产者和消费者提供服务。NameServer 最主要的功能就是,为客户端提供寻址服务,协助客户端找到主题对应的 Broker 地址。此外,NameServer 还负责监控每个 Broker 的存活状态。 NameServer 支持只部署一个节点,也支持部署多个节点组成一个集群,这样可以避免单点故障。在集群模式下,NameServer 各节点之间是不需要任何通信的,也不会通过任何方式互相感知,每个节点都可以独立提供全部服务。 +每个 Broker 都需要和所有的 NameServer 节点进行通信。当 Broker 保存的 Topic 信息发生变化的时候,它会主动通知所有的 NameServer 更新路由信息,为了保证数据一致性,Broker 还会定时给所有的 NameServer 节点上报路由信息。这个上报路由信息的 RPC 请求,也同时起到 Broker 与 NameServer 之间的心跳作用,NameServer 依靠这个心跳来确定 Broker 的健康状态。 + +因为每个 NameServer 节点都可以独立提供完整的服务,所以,对于客户端来说,包括生产者和消费者,只需要选择任意一个 NameServer 节点来查询路由信息就可以了。客户端在生产或消费某个主题的消息之前,会先从 NameServer 上查询这个主题的路由信息,然后根据路由信息获取到当前主题和队列对应的 Broker 物理地址,再连接到 Broker 节点上进行生产或消费。 + +如果 NameServer 检测到与 Broker 的连接中断了,NameServer 会认为这个 Broker 不再能提供服务。NameServer 会立即把这个 Broker 从路由信息中移除掉,避免客户端连接到一个不可用的 Broker 上去。而客户端在与 Broker 通信失败之后,会重新去 NameServer 上拉取路由信息,然后连接到其他 Broker 上继续生产或消费消息,这样就实现了自动切换失效 Broker 的功能。 + ### NameServer 的总体结构 - **NamesrvStartup**:程序入口。 @@ -401,7 +381,7 @@ Kafka 和 RocketMQ 都是基于两阶段提交来实现的事务,都利用了 RocketMQ 和 Kafka 的事务,它们的适用场景是不一样的,RocketMQ 的事务适用于解决本地事务和发消息的数据一致性问题,而 Kafka 的事务则是用于实现它的 Exactly Once 机制,应用于实时计算的场景中。 -## MQTT 协议:如何支持海量的在线 IoT 设备? +## MQTT 协议:如何支持海量的在线 IoT 设备? MQTT 是专门为物联网设备设计的一套标准的通信协议。这套协议在消息模型和功能上与普通的消息队列协议是差不多的,最大的区别在于应用场景不同。在物联网应用场景中,IoT 设备性能差,网络连接不稳定。服务端面临的挑战主要是,需要支撑海量的客户端和主题。 @@ -409,10 +389,18 @@ MQTT 是专门为物联网设备设计的一套标准的通信协议。这套协 自行构建集群,最关键的技术点就是,通过前置的 Proxy 集群来解决海量连接、会话管理和海量主题这三个问题。前置 Proxy 负责在 Broker 和客户端之间转发消息,通过这种方式,将海量客户端连接收敛为少量的 Proxy 与 Broker 之间的连接,解决了海量客户端连接数的问题。维护会话的实现原理,和 Tomcat 维护 HTTP 会话是一样的。对于海量主题,可以在后端部署多组 Broker 小集群,每个小集群分担一部分主题这样的方式来解决。 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170724393.jpeg) + +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170724863.jpeg) + ## Pulsar 的存储计算分离设计:全新的消息队列设计思路 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170724276.png) + Pulsar 和其他消息队列最大的区别是,它采用了存储计算分离的设计。存储消息的职责从 Broker 中分离出来,交给专门的 BookKeeper 存储集群。这样 Broker 就变成了无状态的节点,在集群调度和故障恢复方面更加简单灵活。 +无论是 RocketMQ、RabbitMQ 还是 Kafka,消息都是存储在 Broker 的磁盘或者内存中。客户端在访问某个主题分区之前,必须先找到这个分区所在 Broker,然后连接到这个 Broker 上进行生产和消费。在集群模式下,为了避免单点故障导致丢消息,Broker 在保存消息的时候,必须也把消息复制到其他的 Broker 上。当某个 Broker 节点故障的时候,并不是集群中任意一个节点都能替代这个故障的节点,只有那些“和这个故障节点拥有相同数据的节点”才能替代这个故障的节点。原因就是,每一个 Broker 存储的消息数据是不一样的,或者说,每个节点上都存储了状态(数据)。这种节点称为“有状态的节点(Stateful Node)”。 + 存储计算分离是一种设计思想,它将系统的存储职责和计算职责分离开,存储节点只负责数据存储,而计算节点只负责计算,计算节点是无状态的。无状态的计算节点,具有易于开发、调度灵活的优点,故障转移和恢复也更加简单快速。这种设计的缺点是,系统总体的复杂度更高,性能也更差。不过对于大部分分布式的业务系统来说,由于它不需要自己开发存储系统,采用存储计算分离的设计,既可以充分利用这种设计的优点,整个系统也不会因此变得过于复杂,综合评估优缺点,利大于弊,更加划算。 ## 流计算与消息(一):通过 Flink 理解流计算的原理 @@ -449,6 +437,8 @@ RocketMQ 的存储以 Broker 为单位。它的存储也是分为消息文件和 ### Kafka 和 RocketMQ 的存储结构比较 +![img](https://raw.githubusercontent.com/dunwu/images/master/snap/202502170723879.png) + 对比这两种存储结构,你可以看到它们有很多共通的地方,都是采用消息文件 + 索引文件的存储方式,索引文件的名字都是第一条消息的索引序号,索引中记录了消息的位置等等。 在消息文件的存储粒度上,Kafka 以分区为单位,粒度更细,优点是更加灵活,很容易进行数据迁移和扩容。RocketMQ 以 Broker 为单位,较粗的粒度牺牲了灵活性,带来的好处是,在写入的时候,同时写入的文件更少,有更好的批量(不同主题和分区的数据可以组成一批一起写入),更多的顺序写入,尤其是在 Broker 上有很多主题和分区的情况下,有更好的写入性能。 @@ -457,4 +447,4 @@ RocketMQ 的存储以 Broker 为单位。它的存储也是分为消息文件和 ## 参考资料 -- [消息队列高手课](https://time.geekbang.org/column/intro/100032301) \ No newline at end of file +- [消息队列高手课](https://time.geekbang.org/column/intro/100032301) diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.24\350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/24\350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.24\350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/24\350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.md" index ec2699a7ef..a9dd3f207d 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.24\350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/24\350\256\262\345\220\203\351\200\217\345\210\206\345\270\203\345\274\217\346\225\260\346\215\256\345\272\223.md" @@ -9,7 +9,7 @@ categories: tags: - 分布式 - 分布式存储 -permalink: /pages/e08961/ +permalink: /pages/5ce84336/ --- # 《24 讲吃透分布式数据库》笔记 diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.hbase-a-nosql-database.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/hbase-a-nosql-database.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.hbase-a-nosql-database.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/hbase-a-nosql-database.md" index 355cfa9124..2d60b15dcf 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/01.hbase-a-nosql-database.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/hbase-a-nosql-database.md" @@ -10,7 +10,7 @@ tags: - 分布式 - 分布式存储 - HBASE -permalink: /pages/b2f10e/ +permalink: /pages/b9f4e5f0/ --- # 《HBase: A NoSQL database》笔记 diff --git "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/02.the-log-structured-merge-tree.md" "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/the-log-structured-merge-tree.md" similarity index 97% rename from "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/02.the-log-structured-merge-tree.md" rename to "source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/the-log-structured-merge-tree.md" index 9d8178e457..85c75d6126 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/02.the-log-structured-merge-tree.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/15.\345\210\206\345\270\203\345\274\217/22.\345\210\206\345\270\203\345\274\217\345\255\230\345\202\250/the-log-structured-merge-tree.md" @@ -10,7 +10,7 @@ tags: - 分布式 - 分布式存储 - HBASE -permalink: /pages/d780e2/ +permalink: /pages/0a571d7d/ --- # 《The Log-Structured Merge-Tree (LSM-Tree)》笔记 diff --git "a/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/01.\344\273\2160\345\274\200\345\247\213\345\255\246\345\244\247\346\225\260\346\215\256\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\345\244\247\346\225\260\346\215\256\347\254\224\350\256\260.md" similarity index 99% rename from "source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/01.\344\273\2160\345\274\200\345\247\213\345\255\246\345\244\247\346\225\260\346\215\256\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\345\244\247\346\225\260\346\215\256\347\254\224\350\256\260.md" index 48540725cf..6dcc9baf4e 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/01.\344\273\2160\345\274\200\345\247\213\345\255\246\345\244\247\346\225\260\346\215\256\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\344\273\2160\345\274\200\345\247\213\345\255\246\345\244\247\346\225\260\346\215\256\347\254\224\350\256\260.md" @@ -1,5 +1,5 @@ --- -title: 《从 0 开始学大数据》笔记 +title: 《极客时间教程 - 从 0 开始学大数据》笔记 date: 2023-03-13 17:01:51 order: 01 categories: @@ -7,10 +7,10 @@ categories: - 大数据 tags: - 大数据 -permalink: /pages/fa4495/ +permalink: /pages/9d7ebc40/ --- -# 《从 0 开始学大数据》笔记 +# 《极客时间教程 - 从 0 开始学大数据》笔记 ## 预习模块 @@ -505,4 +505,4 @@ K-means 算法 ## 参考资料 -- [从 0 开始学大数据](https://time.geekbang.org/column/intro/100020201) \ No newline at end of file +- [极客时间教程 - 从 0 开始学大数据](https://time.geekbang.org/column/intro/100020201) diff --git "a/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/02.\345\244\247\350\247\204\346\250\241\346\225\260\346\215\256\345\244\204\347\220\206\345\256\236\346\210\230\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\244\247\350\247\204\346\250\241\346\225\260\346\215\256\345\244\204\347\220\206\345\256\236\346\210\230\347\254\224\350\256\260.md" similarity index 95% rename from "source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/02.\345\244\247\350\247\204\346\250\241\346\225\260\346\215\256\345\244\204\347\220\206\345\256\236\346\210\230\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\244\247\350\247\204\346\250\241\346\225\260\346\215\256\345\244\204\347\220\206\345\256\236\346\210\230\347\254\224\350\256\260.md" index 2579281155..29b6965f53 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/02.\345\244\247\350\247\204\346\250\241\346\225\260\346\215\256\345\244\204\347\220\206\345\256\236\346\210\230\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/16.\345\244\247\346\225\260\346\215\256/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\345\244\247\350\247\204\346\250\241\346\225\260\346\215\256\345\244\204\347\220\206\345\256\236\346\210\230\347\254\224\350\256\260.md" @@ -1,5 +1,5 @@ --- -title: 《大规模数据处理实战》笔记 +title: 《极客时间教程 - 大规模数据处理实战》笔记 date: 2023-03-15 15:15:07 order: 02 categories: @@ -7,10 +7,10 @@ categories: - 大数据 tags: - 大数据 -permalink: /pages/0493ff/ +permalink: /pages/87b5b9fb/ --- -# 《大规模数据处理实战》笔记 +# 《极客时间教程 - 大规模数据处理实战》笔记 ## 00 丨开篇词丨从这里开始,带你走上硅谷一线系统架构师之路 @@ -136,4 +136,4 @@ Spark Streaming 用时间片拆分了无限的数据流,然后对每一个数 ## 参考资料 -- [大规模数据处理实战](https://time.geekbang.org/column/intro/100025301) \ No newline at end of file +- [极客时间教程 - 大规模数据处理实战](https://time.geekbang.org/column/intro/100025301) diff --git "a/source/_posts/99.\347\254\224\350\256\260/17.\344\272\272\345\267\245\346\231\272\350\203\275/01.\346\234\272\345\231\250\345\255\246\344\271\24040\350\256\262\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/17.\344\272\272\345\267\245\346\231\272\350\203\275/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\234\272\345\231\250\345\255\246\344\271\24040\350\256\262\347\254\224\350\256\260.md" similarity index 94% rename from "source/_posts/99.\347\254\224\350\256\260/17.\344\272\272\345\267\245\346\231\272\350\203\275/01.\346\234\272\345\231\250\345\255\246\344\271\24040\350\256\262\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/17.\344\272\272\345\267\245\346\231\272\350\203\275/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\234\272\345\231\250\345\255\246\344\271\24040\350\256\262\347\254\224\350\256\260.md" index 7693ed7002..a275549d15 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/17.\344\272\272\345\267\245\346\231\272\350\203\275/01.\346\234\272\345\231\250\345\255\246\344\271\24040\350\256\262\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/17.\344\272\272\345\267\245\346\231\272\350\203\275/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\346\234\272\345\231\250\345\255\246\344\271\24040\350\256\262\347\254\224\350\256\260.md" @@ -1,5 +1,5 @@ --- -title: 《机器学习 40 讲》笔记 +title: 《极客时间教程 - 机器学习 40 讲》笔记 date: 2023-02-09 21:08:48 order: 01 categories: @@ -8,10 +8,10 @@ categories: tags: - 人工智能 - 机器学习 -permalink: /pages/c3ab9e/ +permalink: /pages/b16aa4aa/ --- -# 《机器学习 40 讲》笔记 +# 《极客时间教程 - 机器学习 40 讲》笔记 ## 开篇词 | 打通修炼机器学习的任督二脉 @@ -67,4 +67,4 @@ permalink: /pages/c3ab9e/ 如果训练数据中的每组输入都有其对应的输出结果,这类学习任务就是**监督学习(supervised learning)**,对没有输出的数据进行学习则是**无监督学习(unsupervised learning)**。监督学习具有更好的预测精度,无监督学习则可以发现数据中隐含的结构特性,起到的也是分类的作用,只不过没有给每个类别赋予标签而已。无监督学习可以用于对数据进行聚类或者密度估计,也可以完成异常检测这类监督学习中的预处理操作。直观地看,监督学习适用于预测任务,无监督学习适用于描述任务。 -## 04 丨计算学习理论 \ No newline at end of file +## 04 丨计算学习理论 diff --git "a/source/_posts/99.\347\254\224\350\256\260/21.\350\275\257\344\273\266\345\267\245\347\250\213/01.\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/21.\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216\347\254\224\350\256\260.md" similarity index 97% rename from "source/_posts/99.\347\254\224\350\256\260/21.\350\275\257\344\273\266\345\267\245\347\250\213/01.\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/21.\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216\347\254\224\350\256\260.md" index ed47ebc45d..e0b537cbc1 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/21.\350\275\257\344\273\266\345\267\245\347\250\213/01.\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/21.\350\275\257\344\273\266\345\267\245\347\250\213/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\350\275\257\344\273\266\345\267\245\347\250\213\344\271\213\347\276\216\347\254\224\350\256\260.md" @@ -1,5 +1,5 @@ --- -title: 《软件工程之美》笔记 +title: 《极客时间教程 - 软件工程之美》笔记 date: 2022-07-12 13:20:31 order: 01 categories: @@ -7,10 +7,10 @@ categories: - 软件工程 tags: - 软件工程 -permalink: /pages/06f95a/ +permalink: /pages/7a852c27/ --- -# 《软件工程之美》笔记 +# 《极客时间教程 - 软件工程之美》笔记 ## 到底应该怎样理解软件工程? @@ -151,4 +151,4 @@ Git 本来只是源代码管理工具,但是其强大的分支管理和灵活 ## 参考资料 -- [软件工程之美](https://time.geekbang.org/column/intro/100023701) \ No newline at end of file +- [极客时间教程 - 软件工程之美](https://time.geekbang.org/column/intro/100023701) diff --git "a/source/_posts/99.\347\254\224\350\256\260/96.\345\267\245\344\275\234/01.\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\347\254\224\350\256\260.md" "b/source/_posts/99.\347\254\224\350\256\260/96.\345\267\245\344\275\234/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\347\254\224\350\256\260.md" similarity index 93% rename from "source/_posts/99.\347\254\224\350\256\260/96.\345\267\245\344\275\234/01.\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\347\254\224\350\256\260.md" rename to "source/_posts/99.\347\254\224\350\256\260/96.\345\267\245\344\275\234/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\347\254\224\350\256\260.md" index e43c03c4ba..1055b731e0 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/96.\345\267\245\344\275\234/01.\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\347\254\224\350\256\260.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/96.\345\267\245\344\275\234/\346\236\201\345\256\242\346\227\266\351\227\264\346\225\231\347\250\213-\350\201\214\345\234\272\346\261\202\347\224\237\346\224\273\347\225\245\347\254\224\350\256\260.md" @@ -1,5 +1,5 @@ --- -title: 《职场求生攻略》笔记 +title: 《极客时间教程 - 职场求生攻略》笔记 date: 2022-07-11 07:23:21 order: 01 categories: @@ -8,10 +8,10 @@ categories: tags: - 工作 - 职场 -permalink: /pages/420981/ +permalink: /pages/36f444f4/ --- -# 《职场求生攻略》笔记 +# 《极客时间教程 - 职场求生攻略》笔记 ## 学会如何工作,和学习技术同等重要 @@ -77,4 +77,4 @@ permalink: /pages/420981/ ## 责任的边界:程序员的职责范围仅仅只是被安排的任务吗? -![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220711070722.png) \ No newline at end of file +![](https://raw.githubusercontent.com/dunwu/images/master/snap/20220711070722.png) diff --git "a/source/_posts/99.\347\254\224\350\256\260/README.md" "b/source/_posts/99.\347\254\224\350\256\260/README.md" index 4bf7f4593e..359596edea 100644 --- "a/source/_posts/99.\347\254\224\350\256\260/README.md" +++ "b/source/_posts/99.\347\254\224\350\256\260/README.md" @@ -5,7 +5,7 @@ categories: - 笔记 tags: - 笔记 -permalink: /pages/aa2c27/ +permalink: /pages/dd6b016c/ hidden: true index: false --- @@ -16,54 +16,76 @@ index: false ### Java -- [《玩转 Spring 全家桶》笔记](01.Java/01.玩转Spring全家桶笔记.md) +- [《极客时间教程 - Java 并发编程实战》笔记一](01.Java/极客时间教程-Java并发编程实战笔记一.md) +- [《极客时间教程 - Java 并发编程实战》笔记二](01.Java/极客时间教程-Java并发编程实战笔记二.md) +- [《极客时间教程 - Java 并发编程实战》笔记三](01.Java/极客时间教程-Java并发编程实战笔记三.md) +- [《极客时间教程 - Java 并发编程实战》笔记四](01.Java/极客时间教程-Java并发编程实战笔记四.md) +- [《极客时间教程 - 深入拆解 Java 虚拟机》笔记](01.Java/极客时间教程-深入拆解Java虚拟机笔记.md) +- [《极客时间教程 - 深入浅出 Java 虚拟机》笔记](01.Java/极客时间教程-深入浅出Java虚拟机笔记.md) +- [《极客时间教程 - 玩转 Spring 全家桶》笔记](01.Java/极客时间教程-玩转Spring全家桶笔记.md) ### 设计 -- [《从 0 开始学架构》笔记](03.设计/01.从0开始学架构笔记.md) -- [《架构实战案例解析》笔记](03.设计/02.架构实战案例解析笔记.md) -- [《左耳听风学习》笔记](03.设计/03.左耳听风笔记.md) -- [《从 0 开始学微服务》笔记](03.设计/10.从0开始学微服务.md) -- [《微服务架构核心 20 讲》笔记](03.设计/11.微服务架构核心20讲笔记.md) -- [《高并发系统设计 40 问》笔记](03.设计/21.高并发系统设计40问笔记.md) +- [《极客时间教程 - 从 0 开始学架构》笔记](03.设计/极客时间教程-从0开始学架构笔记.md) +- [《极客时间教程 - 架构实战案例解析》笔记](03.设计/极客时间教程-架构实战案例解析笔记.md) +- [《极客时间教程 - 左耳听风学习》笔记](03.设计/极客时间教程-左耳听风笔记.md) +- [《极客时间教程 - 从 0 开始学微服务》笔记](03.设计/极客时间教程-从0开始学微服务.md) +- [《极客时间教程 - 微服务架构核心 20 讲》笔记](03.设计/极客时间教程-微服务架构核心20讲笔记.md) +- [《极客时间教程 - 高并发系统设计 40 问》笔记](03.设计/极客时间教程-高并发系统设计40问笔记.md) +- [《极客时间教程 - 秒杀系统》笔记](03.设计/极客时间教程-秒杀系统笔记.md) ### 数据库 -- [《后端存储实战课》笔记](12.数据库/01.后端存储实战课笔记.md) -- [《SQL 必知必会》笔记](12.数据库/02.SQL必知必会.md) -- [《MySQL 实战 45 讲》笔记](12.数据库/03.MySQL实战45讲.md) -- [《检索技术核心 20 讲》笔记](12.数据库/11.检索技术核心20讲笔记.md) +-[《SQL 必知必会》笔记](12.数据库/SQL必知必会笔记.md) +-[《高性能 MySQL》笔记](12.数据库/高性能MySQL笔记.md) +-[《极客时间教程 - SQL 必知必会》笔记](12.数据库/极客时间教程-SQL必知必会.md) +-[《极客时间教程 - MySQL 实战 45 讲》笔记](12.数据库/极客时间教程-MySQL实战45讲.md) +-[《Elasticsearch 实战》笔记](12.数据库/Elasticsearch实战笔记.md) +-[《极客时间教程 - Elasticsearch 核心技术与实战》笔记一](12.数据库/极客时间教程-Elasticsearch核心技术与实战笔记一.md) +-[《极客时间教程 - Elasticsearch 核心技术与实战》笔记二](12.数据库/极客时间教程-Elasticsearch核心技术与实战笔记二.md) +-[《MongoDB 权威指南》笔记一](12.数据库/MongoDB权威指南笔记一.md) +-[《MongoDB 权威指南》笔记二](12.数据库/MongoDB权威指南笔记二.md) +-[《极客时间教程 - MongoDB 高手课》笔记一](12.数据库/极客时间教程-MongoDB高手课笔记一.md) +-[《极客时间教程 - MongoDB 高手课》笔记二](12.数据库/极客时间教程-MongoDB高手课笔记二.md) +-[《极客时间教程 - 后端存储实战课》笔记](12.数据库/极客时间教程-后端存储实战课笔记.md) +-[《极客时间教程 - 检索技术核心 20 讲》笔记](12.数据库/极客时间教程-检索技术核心20讲笔记.md) ### 分布式 - **分布式综合** - - [《数据密集型应用系统设计》笔记之分布式数据系统](15.分布式/00.分布式综合/01.数据密集型应用系统设计笔记一.md) - - [《数据密集型应用系统设计》笔记之数据系统基础](15.分布式/00.分布式综合/02.数据密集型应用系统设计笔记二.md) -- **分布式理论** - - [《分布式协议与算法实战》笔记](15.分布式/01.分布式理论/01.分布式协议与算法实战笔记.md) + - [《数据密集型应用系统设计》笔记一](15.分布式/00.分布式综合/数据密集型应用系统设计笔记一.md) + - [《数据密集型应用系统设计》笔记二](15.分布式/00.分布式综合/数据密集型应用系统设计笔记二.md) + - [《极客时间教程 - 分布式协议与算法实战》笔记](15.分布式/00.分布式综合/极客时间教程-分布式协议与算法实战笔记.md) + - [《极客时间教程 - 分布式技术原理与算法解析》笔记](15.分布式/00.分布式综合/极客时间教程-分布式技术原理与算法解析笔记.md) + - [《极客时间教程 - 深入浅出分布式技术原理》笔记](15.分布式/00.分布式综合/极客时间教程-深入浅出分布式技术原理笔记.md) +- **分布式调度** + - [《深入理解 Sentinel》笔记](15.分布式/12.分布式调度/深入理解Sentinel笔记.md) - **分布式通信** - - [《RPC 实战与核心原理》](15.分布式/21.分布式通信/01.RPC实战与核心原理笔记.md) - - [《Dubbo 源码解读与实战笔记》](15.分布式/21.分布式通信/02.Dubbo源码解读与实战笔记.md) - - [《消息队列高手课学习》笔记](15.分布式/21.分布式通信/11.消息队列高手课笔记.md) - - [《Kafka 核心源码解读》笔记](15.分布式/21.分布式通信/13.Kafka核心源码解读笔记.md) - - [《RocketMQ 技术内幕》笔记](15.分布式/21.分布式通信/15.RocketMQ技术内幕笔记.md) + - [《Dubbo 源码解读与实战》笔记](15.分布式/21.分布式通信/Dubbo源码解读与实战笔记.md) + - [《RocketMQ 技术内幕》笔记](15.分布式/21.分布式通信/RocketMQ技术内幕笔记.md) + - 《极客时间教程 - Kafka 核心技术与实战》笔记 + - [《极客时间教程 - Kafka 核心源码解读》笔记](15.分布式/21.分布式通信/极客时间教程-Kafka核心源码解读笔记.md) + - [《RPC 实战与核心原理》](15.分布式/21.分布式通信/极客时间教程-RPC实战与核心原理笔记.md) + - [《消息队列高手课学习》笔记](15.分布式/21.分布式通信/极客时间教程-消息队列高手课笔记.md) +- **分布式存储** + - [《拉勾教育 - 24 讲吃透分布式数据库》笔记](15.分布式/22.分布式存储/24讲吃透分布式数据库.md) ### 大数据 -- [《从 0 开始学大数据》笔记](16.大数据/01.从0开始学大数据笔记.md) -- [《大规模数据处理实战》笔记](16.大数据/02.大规模数据处理实战笔记.md) +- [《极客时间教程 - 从 0 开始学大数据》笔记](16.大数据/极客时间教程-从0开始学大数据笔记) +- [《极客时间教程 - 大规模数据处理实战》笔记](16.大数据/极客时间教程-大规模数据处理实战笔记) ### 人工智能 -- [机器学习 40 讲](17.人工智能/01.机器学习40讲笔记.md) +- [极客时间教程 - 机器学习 40 讲](17.人工智能/极客时间教程-机器学习40讲笔记) ### 软件工程 -- [《软件工程之美》笔记](21.软件工程/01.软件工程之美笔记.md) +- [《极客时间教程 - 软件工程之美》笔记](21.软件工程/极客时间教程-软件工程之美笔记) ### 工作 -- [《职场求生攻略》笔记](96.工作/01.职场求生攻略笔记.md) +- [《极客时间教程 - 职场求生攻略》笔记](96.工作/极客时间教程-职场求生攻略笔记) ## 🚪 传送