From 4d54264ffde63f3b8e49090bd73f87c0615c3504 Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 7 Jan 2021 21:32:22 +0800 Subject: [PATCH 01/14] =?UTF-8?q?3=E7=A7=8D=E5=B8=B8=E7=94=A8=E7=9A=84?= =?UTF-8?q?=E7=BC=93=E5=AD=98=E8=AF=BB=E5=86=99=E7=AD=96=E7=95=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../Redis/3种常用的缓存读写策略.md | 108 ++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 docs/database/Redis/3种常用的缓存读写策略.md diff --git a/README.md b/README.md index affffd40..06409f1e 100644 --- a/README.md +++ b/README.md @@ -217,6 +217,7 @@ ### Redis 2. [Redis 常见问题总结](docs/database/Redis/redis-all.md) +3. [面试/工作必备!3种常用的缓存读写策略!](docs/database/Redis/3种常用的缓存读写策略.md) ## 系统设计 diff --git a/docs/database/Redis/3种常用的缓存读写策略.md b/docs/database/Redis/3种常用的缓存读写策略.md new file mode 100644 index 00000000..931e5cfe --- /dev/null +++ b/docs/database/Redis/3种常用的缓存读写策略.md @@ -0,0 +1,108 @@ +看到很多小伙伴简历上写了“**熟练使用缓存**”,但是被我问到“**缓存常用的3种读写策略**”的时候却一脸懵逼。 + +在我看来,造成这个问题的原因是我们在学习 Redis 的时候,可能只是简单了写一些 Demo,并没有去关注缓存的读写策略,或者说压根不知道这回事。 + +但是,搞懂3种常见的缓存读写策略对于实际工作中使用缓存以及面试中被问到缓存都是非常有帮助的! + +下面我会简单介绍一下自己对于这 3 种缓存读写策略的理解。 + +另外,**这3 种缓存读写策略各有优劣,不存在最佳,需要我们根据具体的业务场景选择更适合的。** + +*个人能力有限。如果文章有任何需要补充/完善/修改的地方,欢迎在评论区指出,共同进步!——爱你们的 Guide 哥* + +### Cache Aside Pattern(旁路缓存模式) + +**Cache Aside Pattern 是我们平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景。** + +Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准。 + +下面我们来看一下这个策略模式下的缓存读写步骤。 + +**写** : + +- 先更新 DB +- 然后直接删除 cache 。 + +简单画了一张图帮助大家理解写的步骤。 + +![](https://img-blog.csdnimg.cn/img_convert/5687fe759a1dac9ed9554d27e3a23b6d.png) + +**读** : + +- 从 cache 中读取数据,读取到就直接返回 +- cache中读取不到的话,就从 DB 中读取数据返回 +- 再把数据放到 cache 中。 + +简单画了一张图帮助大家理解读的步骤。 + +![](https://img-blog.csdnimg.cn/img_convert/a8c18b5f5b1aed03234bcbbd8c173a87.png) + + +你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。 + +比如说面试官很可能会追问:“**在写数据的过程中,可以先删除 cache ,后更新 DB 么?**” + +**答案:** 那肯定是不行的!因为这样可能会造成**数据库(DB)和缓存(Cache)数据不一致**的问题。为什么呢?比如说请求1 先写数据A,请求2随后读数据A的话就很有可能产生数据不一致性的问题。这个过程可以简单描述为: + +> 请求1先把cache中的A数据删除 -> 请求2从DB中读取数据->请求1再把DB中的A数据更新。 + +当你这样回答之后,面试官可能会紧接着就追问:“**在写数据的过程中,先更新DB,后删除cache就没有问题了么?**” + +**答案:** 理论上来说还是可能会出现数据不一致性的问题,不过概率非常小,因为缓存的写入速度是比数据库的写入速度快很多! + +比如请求1先读数据 A,请求2随后写数据A,并且数据A不在缓存中的话也有可能产生数据不一致性的问题。这个过程可以简单描述为: + +> 请求1从DB读数据A->请求2写更新数据 A 到数据库并把删除cache中的A数据->请求1将数据A写入cache。 + +现在我们再来分析一下 **Cache Aside Pattern 的缺陷**。 + +**缺陷1:首次请求数据一定不在 cache 的问题** + +解决办法:可以将热点数据可以提前放入cache 中。 + +**缺陷2:写操作比较频繁的话导致cache中的数据会被频繁被删除,这样会影响缓存命中率 。** + +解决办法: + +- 数据库和缓存数据强一致场景 :更新DB的时候同样更新cache,不过我们需要加一个锁/分布式锁来保证更新cache的时候不存在线程安全问题。 +- 可以短暂地允许数据库和缓存数据不一致的场景 :更新DB的时候同样更新cache,但是给缓存加一个比较短的过期时间,这样的话就可以保证即使数据不一致的话影响也比较小。 + +### Read/Write Through Pattern(读写穿透) + +Read/Write Through Pattern 中服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中。cache 服务负责将此数据读取和写入 DB,从而减轻了应用程序的职责。 + +这种缓存读写策略小伙伴们应该也发现了在平时在开发过程中非常少见。抛去性能方面的影响,大概率是因为我们经常使用的分布式缓存 Redis 并没有提供 cache 将数据写入DB的功能。 + +**写(Write Through):** + +- 先查 cache,cache 中不存在,直接更新 DB。 +- cache 中存在,则先更新 cache,然后 cache 服务自己更新 DB(**同步更新 cache 和 DB**)。 + +简单画了一张图帮助大家理解写的步骤。 + +![](https://img-blog.csdnimg.cn/img_convert/d4d4114af2fb7eba8936eba1deb96cd4.png) + +**读(Read Through):** + +- 从 cache 中读取数据,读取到就直接返回 。 +- 读取不到的话,先从 DB 加载,写入到 cache 后返回响应。 + +简单画了一张图帮助大家理解读的步骤。 + +![](https://img-blog.csdnimg.cn/img_convert/9ada757c78614934aca11306f334638d.png) + +Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端自己负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,这对客户端是透明的。 + +和 Cache Aside Pattern 一样, Read-Through Pattern 也有首次请求数据一定不再 cache 的问题,对于热点数据可以提前放入缓存中。 + +### Write Behind Pattern(异步缓存写入) + +Write Behind Pattern 和 Read/Write Through Pattern 很相似,两者都是由 cache 服务来负责 cache 和 DB 的读写。 + +但是,两个又有很大的不同:**Read/Write Through 是同步更新 cache 和 DB,而 Write Behind Caching 则是只更新缓存,不直接更新 DB,而是改为异步批量的方式来更新 DB。** + +很明显,这种方式对数据一致性带来了更大的挑战,比如cache数据可能还没异步更新DB的话,cache服务可能就就挂掉了。 + +这种策略在我们平时开发过程中也非常非常少见,但是不代表它的应用场景少,比如消息队列中消息的异步写入磁盘、MySQL 的 InnoDB Buffer Pool 机制都用到了这种策略。 + +Write Behind Pattern 下 DB 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。 From 3e8b992532ff8dc76d5769bdbe530eede42d6a95 Mon Sep 17 00:00:00 2001 From: DONTWATTOSLEEP <44776937+DONTWANTTOSLEEP@users.noreply.github.com> Date: Fri, 8 Jan 2021 15:53:12 +0800 Subject: [PATCH 02/14] =?UTF-8?q?Update=20=E7=BA=BF=E6=80=A7=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E7=BB=93=E6=9E=84.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 89 队列->栈 308 实现->现实 --- .../data-structure/线性数据结构.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/dataStructures-algorithms/data-structure/线性数据结构.md b/docs/dataStructures-algorithms/data-structure/线性数据结构.md index 5084139c..93baddfe 100644 --- a/docs/dataStructures-algorithms/data-structure/线性数据结构.md +++ b/docs/dataStructures-algorithms/data-structure/线性数据结构.md @@ -88,7 +88,7 @@ **栈** (stack)只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 **后进先出(LIFO, Last In First Out)** 的原理运作。**在栈中,push 和 pop 的操作都发生在栈顶。** -栈常用一维数组或链表来实现,用数组实现的队列叫作 **顺序栈** ,用链表实现的队列叫作 **链式栈** 。 +栈常用一维数组或链表来实现,用数组实现的栈叫作 **顺序栈** ,用链表实现的栈叫作 **链式栈** 。 ```java 假设堆栈中有n个元素。 @@ -305,6 +305,6 @@ myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty. - **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。 - **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。 - Linux 内核进程队列(按优先级排队) -- 实现生活中的派对,播放器上的播放列表; +- 现实生活中的派对,播放器上的播放列表; - 消息队列 -- 等等...... \ No newline at end of file +- 等等...... From cefd773915de41684bf8e4cc02c1fda827bd2175 Mon Sep 17 00:00:00 2001 From: guide Date: Sat, 9 Jan 2021 19:54:11 +0800 Subject: [PATCH 03/14] =?UTF-8?q?Update=20Java=E9=9B=86=E5=90=88=E6=A1=86?= =?UTF-8?q?=E6=9E=B6=E5=B8=B8=E8=A7=81=E9=9D=A2=E8=AF=95=E9=A2=98.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/collection/Java集合框架常见面试题.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/java/collection/Java集合框架常见面试题.md b/docs/java/collection/Java集合框架常见面试题.md index f9481b60..d9fd2bf0 100644 --- a/docs/java/collection/Java集合框架常见面试题.md +++ b/docs/java/collection/Java集合框架常见面试题.md @@ -55,7 +55,9 @@ 并且,以 `Map` 结尾的类都实现了 `Map` 接口。 -![](./images/Java-Collections.jpeg) +![](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/source-code/dubbo/java-collection-hierarchy.png) + +

https://www.javatpoint.com/collections-in-java

### 1.1.2. 说说 List,Set,Map 三者的区别? From 53333951e553a0d52befea045a6129dc12f7be10 Mon Sep 17 00:00:00 2001 From: TTL <1050636648@qq.com> Date: Wed, 13 Jan 2021 20:32:23 +0800 Subject: [PATCH 04/14] =?UTF-8?q?=E7=AC=A6=E5=8F=B7=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/jvm/JVM垃圾回收.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/jvm/JVM垃圾回收.md b/docs/java/jvm/JVM垃圾回收.md index b979285a..ed559142 100644 --- a/docs/java/jvm/JVM垃圾回收.md +++ b/docs/java/jvm/JVM垃圾回收.md @@ -481,7 +481,7 @@ G1 收集器的运作大致分为以下几个步骤: - **最终标记** - **筛选回收** -**G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)**。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 +**G1 收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的 Region(这也就是它的名字 Garbage-First 的由来)** 。这种使用 Region 划分内存空间以及有优先级的区域回收方式,保证了 G1 收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。 ### 4.8 ZGC 收集器 From ecfe12ca481cdd9cb047385a719e4f1392dbaf61 Mon Sep 17 00:00:00 2001 From: TimorYang Date: Thu, 14 Jan 2021 14:44:43 +0800 Subject: [PATCH 05/14] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80=E7=9F=A5?= =?UTF-8?q?=E8=AF=86.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 根据上下文语境 应该将 “除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于检查异常” 修改为 “除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常” --- docs/java/basis/Java基础知识.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/java/basis/Java基础知识.md b/docs/java/basis/Java基础知识.md index 5077a9d6..20c32d75 100644 --- a/docs/java/basis/Java基础知识.md +++ b/docs/java/basis/Java基础知识.md @@ -1221,7 +1221,7 @@ Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw` ![check-exception](https://guide-blog-images.oss-cn-shenzhen.aliyuncs.com/2020-12/check-exception.png) -除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。 +除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。 **不受检查异常** From 710c582142db23d11e44f1c59a9dbbc6b32c0aa6 Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 14 Jan 2021 22:42:50 +0800 Subject: [PATCH 06/14] =?UTF-8?q?Update=20=E7=94=A8=E5=A5=BDJava=E4=B8=AD?= =?UTF-8?q?=E7=9A=84=E6=9E=9A=E4=B8=BE=E7=9C=9F=E7=9A=84=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E9=82=A3=E4=B9=88=E7=AE=80=E5=8D=95.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../用好Java中的枚举真的没有那么简单.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/java/basis/用好Java中的枚举真的没有那么简单.md b/docs/java/basis/用好Java中的枚举真的没有那么简单.md index e88f0b5c..23e47ef6 100644 --- a/docs/java/basis/用好Java中的枚举真的没有那么简单.md +++ b/docs/java/basis/用好Java中的枚举真的没有那么简单.md @@ -65,15 +65,16 @@ public class Pizza { 首先,让我们看一下以下代码段中的运行时安全性,其中 `==` 运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException: ```java -if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED)); -if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED); +Pizza.PizzaStatus pizza = null; +System.out.println(pizza.equals(Pizza.PizzaStatus.DELIVERED));//空指针异常 +System.out.println(pizza == Pizza.PizzaStatus.DELIVERED);//正常运行 ``` -对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为`getStatus`方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误: +对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较: ```java -if(testPz.getStatus().equals(TestColor.GREEN)); -if(testPz.getStatus() == TestColor.GREEN); +if (Pizza.PizzaStatus.DELIVERED.equals(TestColor.GREEN)); // 编译正常 +if (Pizza.PizzaStatus.DELIVERED == TestColor.GREEN); // 编译失败,类型不匹配 ``` ## 4.在 switch 语句中使用枚举类型 From ec2003c16f948efb66b57722a950013a21069350 Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 14 Jan 2021 22:42:52 +0800 Subject: [PATCH 07/14] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80=E7=9F=A5?= =?UTF-8?q?=E8=AF=86.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/basis/Java基础知识.md | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/docs/java/basis/Java基础知识.md b/docs/java/basis/Java基础知识.md index 5077a9d6..288a2ad3 100644 --- a/docs/java/basis/Java基础知识.md +++ b/docs/java/basis/Java基础知识.md @@ -160,17 +160,13 @@ JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有 - Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存 - **在 C 语言中,字符串或字符数组最后都会有一个额外的字符`'\0'`来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189) -#### 1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同? - -一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 `main()` 方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。 - -#### 1.1.6. import java 和 javax 有什么区别? +#### 1.1.5. import java 和 javax 有什么区别? 刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。 所以,实际上 java 和 javax 没有区别。这都是一个名字。 -#### 1.1.7. 为什么说 Java 语言“编译与解释并存”? +#### 1.1.6. 为什么说 Java 语言“编译与解释并存”? 高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读, 有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。 From a10b6043cd04fd88dd8425a6e6f37ab903470c5a Mon Sep 17 00:00:00 2001 From: guide Date: Sun, 17 Jan 2021 15:18:54 +0800 Subject: [PATCH 08/14] =?UTF-8?q?Update=20=E6=95=B0=E6=8D=AE=E5=BA=93?= =?UTF-8?q?=E7=B4=A2=E5=BC=95.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/database/数据库索引.md | 119 +++++++++++++++++-------------- 1 file changed, 66 insertions(+), 53 deletions(-) diff --git a/docs/database/数据库索引.md b/docs/database/数据库索引.md index 4e5a5657..73b3251b 100644 --- a/docs/database/数据库索引.md +++ b/docs/database/数据库索引.md @@ -1,84 +1,92 @@ ## 什么是索引? -**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B树, B+树和Hash。** + +**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。** 索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。 ## 为什么要用索引?索引的优缺点分析 ### 索引的优点 -**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 + +**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。 ### 索引的缺点 -1. **创建索引和维护索引需要耗费许多时间**:当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低SQL执行效率。 + +1. **创建索引和维护索引需要耗费许多时间**:当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。 2. **占用物理存储空间** :索引需要使用物理文件存储,也会耗费一定空间。 -## B树和B+树区别 +## B 树和 B+树区别 -* B树的所有节点既存放 键(key) 也存放 数据(data);而B+树只有叶子节点存放 key 和 data,其他内节点只存放key。 -* B树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。 -* B树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。 +- B 树的所有节点既存放 键(key) 也存放 数据(data);而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。 +- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。 +- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。 ![B+树](../../media/pictures/database/B+树.png) -## Hash索引和 B+树索引优劣分析 +## Hash 索引和 B+树索引优劣分析 -**Hash索引定位快** +**Hash 索引定位快** -Hash索引指的就是Hash表,最大的优点就是能够在很短的时间内,根据Hash函数定位到数据所在的位置,这是B+树所不能比的。 +Hash 索引指的就是 Hash 表,最大的优点就是能够在很短的时间内,根据 Hash 函数定位到数据所在的位置,这是 B+树所不能比的。 -**Hash冲突问题** +**Hash 冲突问题** -知道HashMap或HashTable的同学,相信都知道它们最大的缺点就是Hash冲突了。不过对于数据库来说这还不算最大的缺点。 +知道 HashMap 或 HashTable 的同学,相信都知道它们最大的缺点就是 Hash 冲突了。不过对于数据库来说这还不算最大的缺点。 -**Hash索引不支持顺序和范围查询(Hash索引不支持顺序和范围查询是它最大的缺点。** +**Hash 索引不支持顺序和范围查询(Hash 索引不支持顺序和范围查询是它最大的缺点。** 试想一种情况: -````text +```text SELECT * FROM tb1 WHERE id < 500; -```` +``` -B+树是有序的,在这种范围查询中,优势非常大,直接遍历比500小的叶子节点就够了。而Hash索引是根据hash算法来定位的,难不成还要把 1 - 499的数据,每个都进行一次hash计算来定位吗?这就是Hash最大的缺点了。 +B+树是有序的,在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。 --- ## 索引类型 ### 主键索引(Primary Key) + **数据表的主键列使用的就是主键索引。** -**一张数据表有只能有一个主键,并且主键不能为null,不能重复。** +**一张数据表有只能有一个主键,并且主键不能为 null,不能重复。** -**在mysql的InnoDB的表中,当没有显示的指定表的主键时,InnoDB会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则InnoDB将会自动创建一个6Byte的自增主键。** +**在 mysql 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。** ### 二级索引(辅助索引) + **二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。** 唯一索引,普通索引,前缀索引等索引属于二级索引。 **PS:不懂的同学可以暂存疑,慢慢往下看,后面会有答案的,也可以自行搜索。** -1. **唯一索引(Unique Key)** :唯一索引也是一种约束。**唯一索引的属性列不能出现重复的数据,但是允许数据为NULL,一张表允许创建多个唯一索引。** 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 -2. **普通索引(Index)** :**普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和NULL。** +1. **唯一索引(Unique Key)** :唯一索引也是一种约束。**唯一索引的属性列不能出现重复的数据,但是允许数据为 NULL,一张表允许创建多个唯一索引。** 建立唯一索引的目的大部分时候都是为了该属性列的数据的唯一性,而不是为了查询效率。 +2. **普通索引(Index)** :**普通索引的唯一作用就是为了快速查询数据,一张表允许创建多个普通索引,并允许数据重复和 NULL。** 3. **前缀索引(Prefix)** :前缀索引只适用于字符串类型的数据。前缀索引是对文本的前几个字符创建索引,相比普通索引建立的数据更小, 因为只取前几个字符。 -4. **全文索引(Full Text)** :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6之前只有MYISAM引擎支持全文索引,5.6之后InnoDB也支持了全文索引。 +4. **全文索引(Full Text)** :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。 二级索引: -![B+树](../../media/pictures/database/B+树二级索引(辅助索引).png) +![B+树](<../../media/pictures/database/B+树二级索引(辅助索引).png>) ## 聚集索引与非聚集索引 ### 聚集索引 + **聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。** -在 Mysql 中,InnoDB引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。 +在 Mysql 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。 #### 聚集索引的优点 -聚集索引的查询速度非常的快,因为整个B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。 + +聚集索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。 #### 聚集索引的缺点 -1. **依赖于有序的数据** :因为B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或UUID这种又长又难比较的数据,插入或查找的速度肯定比较慢。 + +1. **依赖于有序的数据** :因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。 2. **更新代价大** : 如果对索引列的数据被修改时,那么对应的索引也将会被修改, 而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的, 所以对于主键索引来说,主键一般都是不可被修改的。 @@ -89,21 +97,23 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比 **二级索引属于非聚集索引。** ->MYISAM引擎的表的.MYI文件包含了表的索引, ->该表的索引(B+树)的每个叶子非叶子节点存储索引, ->叶子节点存储索引和索引对应数据的指针,指向.MYD文件的数据。 +> MYISAM 引擎的表的.MYI 文件包含了表的索引, +> 该表的索引(B+树)的每个叶子非叶子节点存储索引, +> 叶子节点存储索引和索引对应数据的指针,指向.MYD 文件的数据。 > -**非聚集索引的叶子节点并不一定存放数据的指针, -因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。** +> **非聚集索引的叶子节点并不一定存放数据的指针, +> 因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。** #### 非聚集索引的优点 + **更新代价比聚集索引要小** 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的 #### 非聚集索引的缺点 + 1. 跟聚集索引一样,非聚集索引也依赖于有序的数据 2. **可能会二次查询(回表)** :这应该是非聚集索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。 -这是Mysql的表的文件截图: +这是 Mysql 的表的文件截图: ![Mysql表文件截图](../../media/pictures/database/Mysql索引文件截图.png) @@ -112,36 +122,37 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比 ![B+树](../../media/pictures/database/B+树索引.png) ### 非聚集索引一定回表查询吗(覆盖索引)? + **非聚集索引不一定回表查询。** ->试想一种情况,用户准备使用SQL查询用户名,而用户名字段正好建立了索引。 +> 试想一种情况,用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。 -````text - SELECT name FROM table WHERE username='guang19'; -```` +```text + SELECT name FROM table WHERE name='guang19'; +``` ->那么这个索引的key本身就是name,查到对应的name直接返回就行了,无需回表查询。 +> 那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。 -**即使是MYISAM也是这样,虽然MYISAM的主键索引确实需要回表, -因为它的主键索引的叶子节点存放的是指针。但是如果SQL查的就是主键呢?** +**即使是 MYISAM 也是这样,虽然 MYISAM 的主键索引确实需要回表, +因为它的主键索引的叶子节点存放的是指针。但是如果 SQL 查的就是主键呢?** ```text SELECT id FROM table WHERE id=1; ``` -主键索引本身的key就是主键,查到返回就行了。这种情况就称之为覆盖索引了。 +主键索引本身的 key 就是主键,查到返回就行了。这种情况就称之为覆盖索引了。 ## 覆盖索引 -如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道在InnoDB存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! +如果一个索引包含(或者说覆盖)所有需要查询的字段的值,我们就称之为“覆盖索引”。我们知道在 InnoDB 存储引擎中,如果不是主键索引,叶子节点存储的是主键+列值。最终还是要“回表”,也就是要通过主键再查找一次。这样就会比较慢覆盖索引就是把要查询出的列和索引是对应的,不做回表操作! **覆盖索引即需要查询的字段正好是索引的字段,那么直接根据该索引,就可以查到数据了, 而无需回表查询。** ->如主键索引,如果一条SQL需要查询主键,那么正好根据主键索引就可以查到主键。 +> 如主键索引,如果一条 SQL 需要查询主键,那么正好根据主键索引就可以查到主键。 > ->再如普通索引,如果一条SQL需要查询name,name字段正好有索引, ->那么直接根据这个索引就可以查到数据,也无需回表。 +> 再如普通索引,如果一条 SQL 需要查询 name,name 字段正好有索引, +> 那么直接根据这个索引就可以查到数据,也无需回表。 覆盖索引: ![B+树覆盖索引](../../media/pictures/database/B+树覆盖索引.png) @@ -151,14 +162,16 @@ SELECT id FROM table WHERE id=1; ## 索引创建原则 ### 单列索引 + 单列索引即由一列属性组成的索引。 ### 联合索引(多列索引) + 联合索引即由多列属性组成索引。 ### 最左前缀原则 -假设创建的联合索引由三个字段组成: +假设创建的联合索引由三个字段组成: ```text ALTER TABLE table ADD INDEX index_name (num,name,age) @@ -166,9 +179,9 @@ ALTER TABLE table ADD INDEX index_name (num,name,age) 那么当查询的条件有为:num / (num AND name) / (num AND name AND age)时,索引才生效。所以在创建联合索引时,尽量把查询最频繁的那个字段作为最左(第一个)字段。查询的时候也尽量以这个字段为第一条件。 -> 但可能由于版本原因(我的mysql版本为8.0.x),我创建的联合索引,相当于在联合索引的每个字段上都创建了相同的索引: +> 但可能由于版本原因(我的 mysql 版本为 8.0.x),我创建的联合索引,相当于在联合索引的每个字段上都创建了相同的索引: -![联合索引(多列索引)](../../media/pictures/database/联合索引(多列索引).png) +![联合索引(多列索引)](<../../media/pictures/database/联合索引(多列索引).png>) 无论是否符合最左前缀原则,每个字段的索引都生效: @@ -178,22 +191,22 @@ ALTER TABLE table ADD INDEX index_name (num,name,age) ### 最左前缀原则 -虽然我目前的Mysql版本较高,好像不遵守最左前缀原则,索引也会生效。 +虽然我目前的 Mysql 版本较高,好像不遵守最左前缀原则,索引也会生效。 但是我们仍应遵守最左前缀原则,以免版本更迭带来的麻烦。 ### 选择合适的字段 -#### 1.不为NULL的字段 +#### 1.不为 NULL 的字段 -索引字段的数据应该尽量不为NULL,因为对于数据为NULL的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为NULL,建议使用0,1,true,false这样语义较为清晰的短值或短字符作为替代。 +索引字段的数据应该尽量不为 NULL,因为对于数据为 NULL 的字段,数据库较难优化。如果字段频繁被查询,但又避免不了为 NULL,建议使用 0,1,true,false 这样语义较为清晰的短值或短字符作为替代。 #### 2.被频繁查询的字段 我们创建索引的字段应该是查询操作非常频繁的字段。 -#### 3.被作为条件查询的字段 +#### 3.被作为条件查询的字段 -被作为WHERE条件查询的字段,应该被考虑建立索引。 +被作为 WHERE 条件查询的字段,应该被考虑建立索引。 #### 4.被经常频繁用于连接的字段 @@ -210,7 +223,7 @@ ALTER TABLE table ADD INDEX index_name (num,name,age) #### 3.尽可能的考虑建立联合索引而不是单列索引 -因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 +因为索引是需要占用磁盘空间的,可以简单理解为每个索引都对应着一颗 B+树。如果一个表的字段过多,索引过多,那么当这个表的数据达到一个体量后,索引占用的空间也是很多的,且修改索引时,耗费的时间也是较多的。如果是联合索引,多个字段在一个索引上,那么将会节约很大磁盘空间,且修改数据的操作效率也会提升。 #### 4.注意避免冗余索引 @@ -222,4 +235,4 @@ ALTER TABLE table ADD INDEX index_name (num,name,age) ### 使用索引一定能提高查询性能吗? -大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 +大多数情况下,索引查询都是比全表扫描要快的。但是如果数据库的数据量不大,那么使用索引也不一定能够带来很大提升。 \ No newline at end of file From c7808b56028c2a1c3fddd5db07ae757b8ab669c0 Mon Sep 17 00:00:00 2001 From: guide Date: Mon, 18 Jan 2021 19:25:58 +0800 Subject: [PATCH 09/14] =?UTF-8?q?Update=20Spring=E5=B8=B8=E8=A7=81?= =?UTF-8?q?=E9=97=AE=E9=A2=98=E6=80=BB=E7=BB=93.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../framework/spring/Spring常见问题总结.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/system-design/framework/spring/Spring常见问题总结.md b/docs/system-design/framework/spring/Spring常见问题总结.md index fb265f99..da2652f9 100644 --- a/docs/system-design/framework/spring/Spring常见问题总结.md +++ b/docs/system-design/framework/spring/Spring常见问题总结.md @@ -111,13 +111,14 @@ AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无 ### 5.2 Spring 中的单例 bean 的线程安全问题了解吗? -大部分时候我们并没有在系统中使用多线程,所以很少有人会关注这个问题。单例 bean 存在线程问题,主要是因为当多个线程操作同一个对象的时候,对这个对象的非静态成员变量的写操作会存在线程安全问题。 +的确是存在安全问题的。因为,当多个线程操作同一个对象的时候,对这个对象的成员变量的写操作会存在线程安全问题。 -常见的有两种解决办法: +但是,一般情况下,我们常用的 `Controller`、`Service`、`Dao` 这些 Bean 是无状态的。无状态的 Bean 不能保存数据,因此是线程安全的。 -1. 在Bean对象中尽量避免定义可变的成员变量(不太现实)。 +常见的有 2 种解决办法: -2. 在类中定义一个ThreadLocal成员变量,将需要的可变成员变量保存在 ThreadLocal 中(推荐的一种方式)。 +2. 在类中定义一个 `ThreadLocal` 成员变量,将需要的可变成员变量保存在 `ThreadLocal` 中(推荐的一种方式)。 +2. 改变 Bean 的作用域为 “prototype”:每次请求都会创建一个新的 bean 实例,自然不会存在线程安全问题。 ### 5.3 @Component 和 @Bean 的区别是什么? From 9f639bf093a8abab62bdcf6d4241608e6774d440 Mon Sep 17 00:00:00 2001 From: guide Date: Mon, 18 Jan 2021 19:47:20 +0800 Subject: [PATCH 10/14] =?UTF-8?q?Update=20AQS=E5=8E=9F=E7=90=86=E4=BB=A5?= =?UTF-8?q?=E5=8F=8AAQS=E5=90=8C=E6=AD=A5=E7=BB=84=E4=BB=B6=E6=80=BB?= =?UTF-8?q?=E7=BB=93.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../AQS原理以及AQS同步组件总结.md | 41 +++++++++---------- 1 file changed, 20 insertions(+), 21 deletions(-) diff --git a/docs/java/multi-thread/AQS原理以及AQS同步组件总结.md b/docs/java/multi-thread/AQS原理以及AQS同步组件总结.md index facba05d..8ca43dd0 100644 --- a/docs/java/multi-thread/AQS原理以及AQS同步组件总结.md +++ b/docs/java/multi-thread/AQS原理以及AQS同步组件总结.md @@ -4,20 +4,20 @@ - [1 AQS 简单介绍](#1-aqs-简单介绍) - [2 AQS 原理](#2-aqs-原理) - - [2.1 AQS 原理概览](#21-aqs-原理概览) - - [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式) - - [2.3 AQS 底层使用了模板方法模式](#23-aqs-底层使用了模板方法模式) + - [2.1 AQS 原理概览](#21-aqs-原理概览) + - [2.2 AQS 对资源的共享方式](#22-aqs-对资源的共享方式) + - [2.3 AQS 底层使用了模板方法模式](#23-aqs-底层使用了模板方法模式) - [3 Semaphore(信号量)-允许多个线程同时访问](#3-semaphore信号量-允许多个线程同时访问) - [4 CountDownLatch (倒计时器)](#4-countdownlatch-倒计时器) - - [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法) - - [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例) - - [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足) - - [4.4 CountDownLatch 常见面试题](#44-countdownlatch-相常见面试题) + - [4.1 CountDownLatch 的三种典型用法](#41-countdownlatch-的三种典型用法) + - [4.2 CountDownLatch 的使用示例](#42-countdownlatch-的使用示例) + - [4.3 CountDownLatch 的不足](#43-countdownlatch-的不足) + - [4.4 CountDownLatch 常见面试题](#44-countdownlatch-相常见面试题) - [5 CyclicBarrier(循环栅栏)](#5-cyclicbarrier循环栅栏) - - [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景) - - [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例) - - [5.3 `CyclicBarrier`源码分析](#53-cyclicbarrier源码分析) - - [5.4 CyclicBarrier 和 CountDownLatch 的区别](#54-cyclicbarrier-和-countdownlatch-的区别) + - [5.1 CyclicBarrier 的应用场景](#51-cyclicbarrier-的应用场景) + - [5.2 CyclicBarrier 的使用示例](#52-cyclicbarrier-的使用示例) + - [5.3 `CyclicBarrier`源码分析](#53-cyclicbarrier源码分析) + - [5.4 CyclicBarrier 和 CountDownLatch 的区别](#54-cyclicbarrier-和-countdownlatch-的区别) - [6 ReentrantLock 和 ReentrantReadWriteLock](#6-reentrantlock-和-reentrantreadwritelock) - [参考](#参考) - [公众号](#公众号) @@ -238,7 +238,7 @@ tryReleaseShared(int)//共享方式。尝试释放资源,成功则返回true ### 3 Semaphore(信号量)-允许多个线程同时访问 -**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** +**synchronized 和 ReentrantLock 都是一次只允许一个线程访问某个资源,Semaphore(信号量)可以指定多个线程同时访问某个资源。** 示例代码如下: @@ -316,16 +316,16 @@ Semaphore 有两种模式,公平模式和非公平模式。 **这两个构造方法,都必须提供许可的数量,第二个构造方法可以指定是公平模式还是非公平模式,默认非公平模式。** -[issue645补充内容](https://github.com/Snailclimb/JavaGuide/issues/645) :Semaphore与CountDownLatch一样,也是共享锁的一种实现。它默认构造AQS的state为permits。当执行任务的线程数量超出permits,那么多余的线程将会被放入阻塞队列Park,并自旋判断state是否大于0。只有当state大于0的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行release方法,release方法使得state的变量会加1,那么自旋的线程便会判断成功。 -如此,每次只有最多不超过permits数量的线程能自旋成功,便限制了执行任务线程的数量。 +[issue645 补充内容](https://github.com/Snailclimb/JavaGuide/issues/645) :Semaphore 与 CountDownLatch 一样,也是共享锁的一种实现。它默认构造 AQS 的 state 为 permits。当执行任务的线程数量超出 permits,那么多余的线程将会被放入阻塞队列 Park,并自旋判断 state 是否大于 0。只有当 state 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 release 方法,release 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。 +如此,每次只有最多不超过 permits 数量的线程能自旋成功,便限制了执行任务线程的数量。 由于篇幅问题,如果对 Semaphore 源码感兴趣的朋友可以看下这篇文章:https://juejin.im/post/5ae755366fb9a07ab508adc6 ### 4 CountDownLatch (倒计时器) -CountDownLatch允许 count 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。在 Java 并发中,countdownlatch 的概念是一个常见的面试题,所以一定要确保你很好的理解了它。 +`CountDownLatch` 允许 `count` 个线程阻塞在一个地方,直至所有线程的任务都执行完毕。 -CountDownLatch是共享锁的一种实现,它默认构造 AQS 的 state 值为 count。当线程使用countDown方法时,其实使用了`tryReleaseShared`方法以CAS的操作来减少state,直至state为0就代表所有的线程都调用了countDown方法。当调用await方法的时候,如果state不为0,就代表仍然有线程没有调用countDown方法,那么就把已经调用过countDown的线程都放入阻塞队列Park,并自旋CAS判断state == 0,直至最后一个线程调用了countDown,使得state == 0,于是阻塞的线程便判断成功,全部往下执行。 +`CountDownLatch` 是共享锁的一种实现,它默认构造 AQS 的 `state` 值为 `count`。当线程使用 `countDown()` 方法时,其实使用了`tryReleaseShared`方法以 CAS 的操作来减少 `state`,直至 `state` 为 0 。当调用 `await()` 方法的时候,如果 `state` 不为 0,那就证明任务还没有执行完毕,`await()` 方法就会一直阻塞,也就是说 `await()` 方法之后的语句不会被执行。然后,`CountDownLatch` 会自旋 CAS 判断 `state == 0`,如果 `state == 0` 的话,就会释放所有等待的线程,`await()` 方法之后的语句得到执行。 #### 4.1 CountDownLatch 的两种典型用法 @@ -383,7 +383,7 @@ public class CountDownLatchExample1 { 其他 N 个线程必须引用闭锁对象,因为他们需要通知 `CountDownLatch` 对象,他们已经完成了各自的任务。这种通知机制是通过 `CountDownLatch.countDown()`方法来完成的;每调用一次这个方法,在构造函数中初始化的 count 值就减 1。所以当 N 个线程都调 用了这个方法,count 的值等于 0,然后主线程就能通过 `await()`方法,恢复执行自己的任务。 -再插一嘴:`CountDownLatch` 的 `await()` 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为: +再插一嘴:`CountDownLatch` 的 `await()` 方法使用不当很容易产生死锁,比如我们上面代码中的 for 循环改为: ```java for (int i = 0; i < threadCount-1; i++) { @@ -391,9 +391,9 @@ for (int i = 0; i < threadCount-1; i++) { } ``` -这样就导致 `count` 的值没办法等于 0,然后就会导致一直等待。 +这样就导致 `count` 的值没办法等于 0,然后就会导致一直等待。 -如果对CountDownLatch源码感兴趣的朋友,可以查看: [【JUC】JDK1.8源码分析之CountDownLatch(五)](https://www.cnblogs.com/leesf456/p/5406191.html) +如果对 CountDownLatch 源码感兴趣的朋友,可以查看: [【JUC】JDK1.8 源码分析之 CountDownLatch(五)](https://www.cnblogs.com/leesf456/p/5406191.html) #### 4.3 CountDownLatch 的不足 @@ -413,7 +413,7 @@ CountDownLatch 类中主要的方法? CyclicBarrier 和 CountDownLatch 非常类似,它也可以实现线程间的技术等待,但是它的功能比 CountDownLatch 更加复杂和强大。主要应用场景和 CountDownLatch 类似。 -> CountDownLatch的实现是基于AQS的,而CycliBarrier是基于 ReentrantLock(ReentrantLock也属于AQS同步器)和 Condition 的. +> CountDownLatch 的实现是基于 AQS 的,而 CycliBarrier 是基于 ReentrantLock(ReentrantLock 也属于 AQS 同步器)和 Condition 的. CyclicBarrier 的字面意思是可循环使用(Cyclic)的屏障(Barrier)。它要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续干活。CyclicBarrier 默认的构造方法是 `CyclicBarrier(int parties)`,其参数表示屏障拦截的线程数量,每个线程调用`await`方法告诉 CyclicBarrier 我已经到达了屏障,然后当前线程被阻塞。 @@ -726,4 +726,3 @@ ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多 **Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。 ![我的公众号](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/167598cd2e17b8ec.png) - From 348270b5fc6880ed4ac4d2793d7ccad7588bbca1 Mon Sep 17 00:00:00 2001 From: guide Date: Mon, 18 Jan 2021 19:53:11 +0800 Subject: [PATCH 11/14] =?UTF-8?q?Update=20Java=E5=9F=BA=E7=A1=80=E7=9F=A5?= =?UTF-8?q?=E8=AF=86.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/java/basis/Java基础知识.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/java/basis/Java基础知识.md b/docs/java/basis/Java基础知识.md index a3b24d02..7fd82e86 100644 --- a/docs/java/basis/Java基础知识.md +++ b/docs/java/basis/Java基础知识.md @@ -1008,9 +1008,11 @@ public class Student { #### 2.5.1. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的? +**可变性** + 简单的来说:`String` 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以`String` 对象是不可变的。 -> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,String 类的实现改用 byte 数组存储字符串 `private final byte[] value`; +> 补充(来自[issue 675](https://github.com/Snailclimb/JavaGuide/issues/675)):在 Java 9 之后,String 、`StringBuilder` 与 `StringBuffer` 的实现改用 byte 数组存储字符串 `private final byte[] value` 而 `StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串`char[]value` 但是没有用 `final` 关键字修饰,所以这两种对象都是可变的。 From fbf3ce21c29327681e4ccb78102cd078942f49b2 Mon Sep 17 00:00:00 2001 From: guide Date: Wed, 20 Jan 2021 20:35:12 +0800 Subject: [PATCH 12/14] Update zookeeper-plus.md --- .../zookeeper/zookeeper-plus.md | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/system-design/distributed-system/zookeeper/zookeeper-plus.md b/docs/system-design/distributed-system/zookeeper/zookeeper-plus.md index 36772048..b70ab4c0 100644 --- a/docs/system-design/distributed-system/zookeeper/zookeeper-plus.md +++ b/docs/system-design/distributed-system/zookeeper/zookeeper-plus.md @@ -47,7 +47,7 @@ `ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。 -![zookeeper](http://img.francisqiang.top/img/Zookeeper.jpg) +![zookeeper](https://img-blog.csdnimg.cn/img_convert/7c349891b773671514a68f057b2e14f8.png) 简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔 @@ -55,15 +55,15 @@ 比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。 -![cluster](http://img.francisqiang.top/img/cluster.jpg) +![cluster](https://img-blog.csdnimg.cn/img_convert/ffcb080eb66f242ffcd8d2047a7f46aa.png) 但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。 -![distributed](http://img.francisqiang.top/img/distributed.jpg) +![distributed](https://img-blog.csdnimg.cn/img_convert/07191f38aa947b0075e5c0a6a019a11d.png) 而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。 -![](http://img.francisqiang.top/img/miao.jpg) +![](https://img-blog.csdnimg.cn/img_convert/2b2fbc21abfb3f6547a2121f28c6d00f.png) 比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。 @@ -73,7 +73,7 @@ 理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。 -![](http://img.francisqiang.top/img/垃圾例子.jpg) +![](https://img-blog.csdnimg.cn/img_convert/34ffff41f6ca4f221ca9d9ad6f0b5470.png) 而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。 @@ -83,7 +83,7 @@ 这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢,对吧? -![](http://img.francisqiang.top/img/neigui.jpg) +![](https://img-blog.csdnimg.cn/img_convert/b0e01fe3213dcc1535c31298ba9bdfbc.png) 这个时候就引申出一个概念—— **拜占庭将军问题** 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的**, 所以所有的一致性算法的 **必要前提** 就是安全可靠的消息通道。 @@ -109,11 +109,11 @@ 而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 **回滚事务的 `rollback` 请求**,参与者收到之后将会 **回滚它在第一阶段所做的事务处理** ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。 -![2PC流程](http://img.francisqiang.top/img/2PC.jpg) +![2PC流程](https://img-blog.csdnimg.cn/img_convert/7ce4e40b68d625676bb42c29efce046a.png) 个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。 -![](http://img.francisqiang.top/img/laji.jpg) +![](https://img-blog.csdnimg.cn/img_convert/9af1ad68517561a8e9f5d3455a32132d.png) * **单点故障问题**,如果协调者挂了那么整个系统都处于不可用的状态了。 * **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。 @@ -129,7 +129,7 @@ 2. **PreCommit阶段**:协调者根据参与者返回的响应来决定是否可以进行下面的 `PreCommit` 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 `PreCommit` 预提交请求,**参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中** ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 **任何一个 NO** 的信息,或者 **在一定时间内** 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。 3. **DoCommit阶段**:这个阶段其实和 `2PC` 的第二阶段差不多,如果协调者收到了所有参与者在 `PreCommit` 阶段的 YES 响应,那么协调者将会给所有参与者发送 `DoCommit` 请求,**参与者收到 `DoCommit` 请求后则会进行事务的提交工作**,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 `PreCommit` 阶段 **收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应** ,那么就会进行中断请求的发送,参与者收到中断请求后则会 **通过上面记录的回滚日志** 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。 -![3PC流程](http://img.francisqiang.top/img/3PC.jpg) +![3PC流程](https://img-blog.csdnimg.cn/img_convert/d0b44361c746593f70a6e42c298b413a.png) > 这里是 `3PC` 在成功的环境下的流程图,你可以看到 `3PC` 在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能 **减少同步阻塞的时间** 。还有需要注意的是,**`3PC` 在 `DoCommit` 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交**。为什么这么做呢?是因为这个时候我们肯定**保证了在第一阶段所有的协调者全部返回了可以执行事务的响应**,这个时候我们有理由**相信其他系统都能进行事务的执行和提交**,所以**不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。 @@ -150,7 +150,7 @@ > 下面是 `prepare` 阶段的流程图,你可以对照着参考一下。 -![paxos第一阶段](http://img.francisqiang.top/img/paxos1.jpg) +![paxos第一阶段](https://img-blog.csdnimg.cn/img_convert/22e8d512d954676bdf0cc92d200af8ef.png) #### 4.3.2. accept 阶段 @@ -158,11 +158,11 @@ 表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 `accept` 该提案(此时执行提案内容但不提交),随后将情况返回给 `Proposer` 。如果不满足则不回应或者返回 NO 。 -![paxos第二阶段1](http://img.francisqiang.top/img/paxos2.jpg) +![paxos第二阶段1](https://img-blog.csdnimg.cn/img_convert/b82536f956f70a584c6a20c10113f225.png) 当 `Proposer` 收到超过半数的 `accept` ,那么它这个时候会向所有的 `acceptor` 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 `acceptor` 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要**向未批准的 `acceptor` 发送提案内容和提案编号并让它无条件执行和提交**,而对于前面已经批准过该提案的 `acceptor` 来说 **仅仅需要发送该提案的编号** ,让 `acceptor` 执行提交就行了。 -![paxos第二阶段2](http://img.francisqiang.top/img/paxos3.jpg) +![paxos第二阶段2](https://img-blog.csdnimg.cn/img_convert/743889b97485fdfe2094e5ef0af6b141.png) 而如果 `Proposer` 如果没有收到超过半数的 `accept` 那么它将会将 **递增** 该 `Proposal` 的编号,然后 **重新进入 `Prepare` 阶段** 。 @@ -176,7 +176,7 @@ 就这样无休无止的永远提案下去,这就是 `paxos` 算法的死循环问题。 -![](http://img.francisqiang.top/img/chaojia.jpg) +![](https://img-blog.csdnimg.cn/img_convert/72ccf65cdc107346ff2a1a881d296a2b.png) 那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。 @@ -186,7 +186,7 @@ 作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Automic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。 -![Zookeeper架构](http://img.francisqiang.top/img/Zookeeper架构.jpg) +![Zookeeper架构](https://img-blog.csdnimg.cn/img_convert/0c38d08ea026e25bf3849cc7654a4e79.png) ### 5.2. `ZAB` 中的三个角色 @@ -204,11 +204,11 @@ 不就是 **在整个集群中保持数据的一致性** 嘛?如果是你,你会怎么做呢? -![](http://img.francisqiang.top/img/zenmezhidao.jpg) +![](https://img-blog.csdnimg.cn/img_convert/e6064aea729dcc2d927d5d81c4797e74.png) 废话,第一步肯定需要 `Leader` 将写请求 **广播** 出去呀,让 `Leader` 问问 `Followers` 是否同意更新,如果超过半数以上的同意那么就进行 `Follower` 和 `Observer` 的更新(和 `Paxos` 一样)。当然这么说有点虚,画张图理解一下。 -![消息广播](http://img.francisqiang.top/img/消息广播1.jpg) +![消息广播](https://img-blog.csdnimg.cn/img_convert/08ccce48190fe4edcbcbb223d6231876.png) 嗯。。。看起来很简单,貌似懂了🤥🤥🤥。这两个 `Queue` 哪冒出来的?答案是 **`ZAB` 需要让 `Follower` 和 `Observer` 保证顺序性** 。何为顺序性,比如我现在有一个写请求A,此时 `Leader` 将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 `Follower` F1因为网络原因没有收到,而 `Leader` 又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 **产生数据不一致问题** 。 @@ -250,7 +250,7 @@ 假设 `Leader (server2)` 发送 `commit` 请求(忘了请看上面的消息广播模式),他发送给了 `server3`,然后要发给 `server1` 的时候突然挂了。这个时候重新选举的时候我们如果把 `server1` 作为 `Leader` 的话,那么肯定会产生数据不一致性,因为 `server3` 肯定会提交刚刚 `server2` 发送的 `commit` 请求的提案,而 `server1` 根本没收到所以会丢弃。 -![崩溃恢复](http://img.francisqiang.top/img/崩溃恢复1.jpg) +![崩溃恢复](https://img-blog.csdnimg.cn/img_convert/ffcb12c6fb2bad76ac7105696655e85c.png) 那怎么解决呢? @@ -260,7 +260,7 @@ 假设 `Leader (server2)` 此时同意了提案N1,自身提交了这个事务并且要发送给所有 `Follower` 要 `commit` 的请求,却在这个时候挂了,此时肯定要重新进行 `Leader` 的选举,比如说此时选 `server1` 为 `Leader` (这无所谓)。但是过了一会,这个 **挂掉的 `Leader` 又重新恢复了** ,此时它肯定会作为 `Follower` 的身份进入集群中,需要注意的是刚刚 `server2` 已经同意提交了提案N1,但其他 `server` 并没有收到它的 `commit` 信息,所以其他 `server` 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 **该提案N1最终需要被抛弃掉** 。 -![崩溃恢复](http://img.francisqiang.top/img/崩溃恢复2.jpg) +![崩溃恢复](https://img-blog.csdnimg.cn/img_convert/abb6efc7d4df9c82b162cbecb129a6e3.png) ## 6. Zookeeper的几个理论知识 @@ -272,7 +272,7 @@ `zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。 -![zk数据模型](http://img.francisqiang.top/img/zk数据模型.jpg) +![zk数据模型](https://img-blog.csdnimg.cn/img_convert/8f35dba8c44c4a10d81e3395df971ce7.png) 每个 `znode` 都有自己所属的 **节点类型** 和 **节点状态**。 @@ -317,13 +317,13 @@ `Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。 -![watcher机制](http://img.francisqiang.top/img/watcher机制.jpg) +![watcher机制](https://img-blog.csdnimg.cn/img_convert/241ab8cc37571034fa984322b753c7ba.png) ## 7. Zookeeper的几个典型应用场景 前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。 -![](http://img.francisqiang.top/img/feijie.jpg) +![](https://img-blog.csdnimg.cn/img_convert/9c9bd2a892e23e0b7582370c50117d8c.png) ### 7.1. 选主 @@ -335,7 +335,7 @@ 你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?`master` 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 `watcher` 吗?我们是不是可以 **让其他不是 `master` 的节点监听节点的状态** ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表 `master` 挂了,这个时候我们 **触发回调函数进行重新选举** ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断 `master` 是否挂了等等。 -![选主](http://img.francisqiang.top/img/选主.jpg) +![选主](https://img-blog.csdnimg.cn/img_convert/a94707028c5581c815f72fba0f50f43a.png) 总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。 @@ -377,13 +377,13 @@ 而 `zookeeper` 天然支持的 `watcher` 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 `watcher` 进行状态监控和回调。 -![集群管理](http://img.francisqiang.top/img/集群管理.jpg) +![集群管理](https://img-blog.csdnimg.cn/img_convert/6115820219c35c68bcb2c9a855ebace3.png) 至于注册中心也很简单,我们同样也是让 **服务提供者** 在 `zookeeper` 中创建一个临时节点并且将自己的 `ip、port、调用方式` 写入节点,当 **服务消费者** 需要进行调用的时候会 **通过注册中心找到相应的服务的地址列表(IP端口什么的)** ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。 当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 `Eureka` 会先试错,然后再更新)。 -![注册中心](http://img.francisqiang.top/img/注册中心.jpg) +![注册中心](https://img-blog.csdnimg.cn/img_convert/0b5b3911a7c2dae23391d17c91416b29.png) ## 8. 总结 @@ -391,7 +391,7 @@ 不知道大家是否还记得我讲了什么😒。 -![](http://img.francisqiang.top/img/masmmei.jpg) +![](https://img-blog.csdnimg.cn/img_convert/b0bde9f3979b8f19187e6c374ad98993.png) 这篇文章中我带大家入门了 `zookeeper` 这个强大的分布式协调框架。现在我们来简单梳理一下整篇文章的内容。 @@ -405,4 +405,4 @@ * `zookeeper` 的典型应用场景,比如选主,注册中心等等。 - 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 + 如果忘了可以回去看看再次理解一下,如果有疑问和建议欢迎提出🤝🤝🤝。 \ No newline at end of file From 5864fcf97ae281be8015aa542f68fa80f563307b Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 21 Jan 2021 00:14:01 +0800 Subject: [PATCH 13/14] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06409f1e..f0e70662 100644 --- a/README.md +++ b/README.md @@ -240,7 +240,7 @@ **重要知识点详解:** -1. **[Spring/Spring 常用注解总结!安排!](./docs/system-design/framework/spring/SpringBoot+Spring常用注解总结.md)** +1. **[Spring/Spring Boot 常用注解总结!安排!](./docs/system-design/framework/spring/SpringBoot+Spring常用注解总结.md)** 2. **[Spring 事务总结](docs/system-design/framework/spring/Spring事务总结.md)** 3. [Spring 中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md) From 32f8144cc6a27cb92be4c3b8a9f1684ba59b7536 Mon Sep 17 00:00:00 2001 From: guide Date: Thu, 21 Jan 2021 10:58:48 +0800 Subject: [PATCH 14/14] =?UTF-8?q?=E9=9D=A2=E8=AF=95=E5=B8=B8=E9=97=AE?= =?UTF-8?q?=EF=BC=9A=E2=80=9C=E8=AE=B2=E8=BF=B0=E4=B8=80=E4=B8=8B=20Spring?= =?UTF-8?q?Boot=20=E8=87=AA=E5=8A=A8=E8=A3=85=E9=85=8D=E5=8E=9F=E7=90=86?= =?UTF-8?q?=EF=BC=9F=E2=80=9D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f0e70662..e3e30ca8 100644 --- a/README.md +++ b/README.md @@ -243,6 +243,7 @@ 1. **[Spring/Spring Boot 常用注解总结!安排!](./docs/system-design/framework/spring/SpringBoot+Spring常用注解总结.md)** 2. **[Spring 事务总结](docs/system-design/framework/spring/Spring事务总结.md)** 3. [Spring 中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.md) +4. [面试常问:“讲述一下 SpringBoot 自动装配原理?”](https://www.cnblogs.com/javaguide/p/springboot-auto-config.html) #### MyBatis