mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-20 22:17:09 +08:00
commit
24e46d352c
@ -217,6 +217,7 @@
|
|||||||
### Redis
|
### Redis
|
||||||
|
|
||||||
2. [Redis 常见问题总结](docs/database/Redis/redis-all.md)
|
2. [Redis 常见问题总结](docs/database/Redis/redis-all.md)
|
||||||
|
3. [面试/工作必备!3种常用的缓存读写策略!](docs/database/Redis/3种常用的缓存读写策略.md)
|
||||||
|
|
||||||
## 系统设计
|
## 系统设计
|
||||||
|
|
||||||
@ -239,9 +240,10 @@
|
|||||||
|
|
||||||
**重要知识点详解:**
|
**重要知识点详解:**
|
||||||
|
|
||||||
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)**
|
2. **[Spring 事务总结](docs/system-design/framework/spring/Spring事务总结.md)**
|
||||||
3. [Spring 中都用到了那些设计模式?](docs/system-design/framework/spring/Spring-Design-Patterns.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
|
#### MyBatis
|
||||||
|
|
||||||
|
@ -88,7 +88,7 @@
|
|||||||
|
|
||||||
**栈** (stack)只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 **后进先出(LIFO, Last In First Out)** 的原理运作。**在栈中,push 和 pop 的操作都发生在栈顶。**
|
**栈** (stack)只允许在有序的线性数据集合的一端(称为栈顶 top)进行加入数据(push)和移除数据(pop)。因而按照 **后进先出(LIFO, Last In First Out)** 的原理运作。**在栈中,push 和 pop 的操作都发生在栈顶。**
|
||||||
|
|
||||||
栈常用一维数组或链表来实现,用数组实现的队列叫作 **顺序栈** ,用链表实现的队列叫作 **链式栈** 。
|
栈常用一维数组或链表来实现,用数组实现的栈叫作 **顺序栈** ,用链表实现的栈叫作 **链式栈** 。
|
||||||
|
|
||||||
```java
|
```java
|
||||||
假设堆栈中有n个元素。
|
假设堆栈中有n个元素。
|
||||||
@ -305,6 +305,6 @@ myStack.pop();//报错:java.lang.IllegalArgumentException: Stack is empty.
|
|||||||
- **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。
|
- **阻塞队列:** 阻塞队列可以看成在队列基础上加了阻塞操作的队列。当队列为空的时候,出队操作阻塞,当队列满的时候,入队操作阻塞。使用阻塞队列我们可以很容易实现“生产者 - 消费者“模型。
|
||||||
- **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。
|
- **线程池中的请求/任务队列:** 线程池中没有空闲线程时,新的任务请求线程资源时,线程池该如何处理呢?答案是将这些请求放在队列中,当有空闲线程的时候,会循环中反复从队列中获取任务来执行。队列分为无界队列(基于链表)和有界队列(基于数组)。无界队列的特点就是可以一直入列,除非系统资源耗尽,比如 :`FixedThreadPool` 使用无界队列 `LinkedBlockingQueue`。但是有界队列就不一样了,当队列满的话后面再有任务/请求就会拒绝,在 Java 中的体现就是会抛出`java.util.concurrent.RejectedExecutionException` 异常。
|
||||||
- Linux 内核进程队列(按优先级排队)
|
- Linux 内核进程队列(按优先级排队)
|
||||||
- 实现生活中的派对,播放器上的播放列表;
|
- 现实生活中的派对,播放器上的播放列表;
|
||||||
- 消息队列
|
- 消息队列
|
||||||
- 等等......
|
- 等等......
|
108
docs/database/Redis/3种常用的缓存读写策略.md
Normal file
108
docs/database/Redis/3种常用的缓存读写策略.md
Normal file
@ -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 。
|
||||||
|
|
||||||
|
简单画了一张图帮助大家理解写的步骤。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**读** :
|
||||||
|
|
||||||
|
- 从 cache 中读取数据,读取到就直接返回
|
||||||
|
- cache中读取不到的话,就从 DB 中读取数据返回
|
||||||
|
- 再把数据放到 cache 中。
|
||||||
|
|
||||||
|
简单画了一张图帮助大家理解读的步骤。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
你仅仅了解了上面这些内容的话是远远不够的,我们还要搞懂其中的原理。
|
||||||
|
|
||||||
|
比如说面试官很可能会追问:“**在写数据的过程中,可以先删除 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**)。
|
||||||
|
|
||||||
|
简单画了一张图帮助大家理解写的步骤。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**读(Read Through):**
|
||||||
|
|
||||||
|
- 从 cache 中读取数据,读取到就直接返回 。
|
||||||
|
- 读取不到的话,先从 DB 加载,写入到 cache 后返回响应。
|
||||||
|
|
||||||
|
简单画了一张图帮助大家理解读的步骤。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
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 的写性能非常高,非常适合一些数据经常变化又对数据一致性要求没那么高的场景,比如浏览量、点赞量。
|
@ -1,4 +1,5 @@
|
|||||||
## 什么是索引?
|
## 什么是索引?
|
||||||
|
|
||||||
**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。**
|
**索引是一种用于快速查询和检索数据的数据结构。常见的索引结构有: B 树, B+树和 Hash。**
|
||||||
|
|
||||||
索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
|
索引的作用就相当于目录的作用。打个比方: 我们在查字典的时候,如果没有目录,那我们就只能一页一页的去找我们需要查的那个字,速度很慢。如果有目录了,我们只需要先去目录里查找字的位置,然后直接翻到那一页就行了。
|
||||||
@ -6,17 +7,19 @@
|
|||||||
## 为什么要用索引?索引的优缺点分析
|
## 为什么要用索引?索引的优缺点分析
|
||||||
|
|
||||||
### 索引的优点
|
### 索引的优点
|
||||||
|
|
||||||
**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
|
**可以大大加快 数据的检索速度(大大减少的检索的数据量), 这也是创建索引的最主要的原因。毕竟大部分系统的读请求总是大于写请求的。** 另外,通过创建唯一性索引,可以保证数据库表中每一行数据的唯一性。
|
||||||
|
|
||||||
### 索引的缺点
|
### 索引的缺点
|
||||||
|
|
||||||
1. **创建索引和维护索引需要耗费许多时间**:当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
|
1. **创建索引和维护索引需要耗费许多时间**:当对表中的数据进行增删改的时候,如果数据有索引,那么索引也需要动态的修改,会降低 SQL 执行效率。
|
||||||
2. **占用物理存储空间** :索引需要使用物理文件存储,也会耗费一定空间。
|
2. **占用物理存储空间** :索引需要使用物理文件存储,也会耗费一定空间。
|
||||||
|
|
||||||
## B 树和 B+树区别
|
## B 树和 B+树区别
|
||||||
|
|
||||||
* B树的所有节点既存放 键(key) 也存放 数据(data);而B+树只有叶子节点存放 key 和 data,其他内节点只存放key。
|
- B 树的所有节点既存放 键(key) 也存放 数据(data);而 B+树只有叶子节点存放 key 和 data,其他内节点只存放 key。
|
||||||
* B树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
|
- B 树的叶子节点都是独立的;B+树的叶子节点有一条引用链指向与它相邻的叶子节点。
|
||||||
* B树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
|
- B 树的检索的过程相当于对范围内的每个节点的关键字做二分查找,可能还没有到达叶子节点,检索就结束了。而 B+树的检索效率就很稳定了,任何查找都是从根节点到叶子节点的过程,叶子节点的顺序检索很明显。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
@ -34,9 +37,9 @@ Hash索引指的就是Hash表,最大的优点就是能够在很短的时间内
|
|||||||
|
|
||||||
试想一种情况:
|
试想一种情况:
|
||||||
|
|
||||||
````text
|
```text
|
||||||
SELECT * FROM tb1 WHERE id < 500;
|
SELECT * FROM tb1 WHERE id < 500;
|
||||||
````
|
```
|
||||||
|
|
||||||
B+树是有序的,在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。
|
B+树是有序的,在这种范围查询中,优势非常大,直接遍历比 500 小的叶子节点就够了。而 Hash 索引是根据 hash 算法来定位的,难不成还要把 1 - 499 的数据,每个都进行一次 hash 计算来定位吗?这就是 Hash 最大的缺点了。
|
||||||
|
|
||||||
@ -45,6 +48,7 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比
|
|||||||
## 索引类型
|
## 索引类型
|
||||||
|
|
||||||
### 主键索引(Primary Key)
|
### 主键索引(Primary Key)
|
||||||
|
|
||||||
**数据表的主键列使用的就是主键索引。**
|
**数据表的主键列使用的就是主键索引。**
|
||||||
|
|
||||||
**一张数据表有只能有一个主键,并且主键不能为 null,不能重复。**
|
**一张数据表有只能有一个主键,并且主键不能为 null,不能重复。**
|
||||||
@ -52,6 +56,7 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比
|
|||||||
**在 mysql 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。**
|
**在 mysql 的 InnoDB 的表中,当没有显示的指定表的主键时,InnoDB 会自动先检查表中是否有唯一索引的字段,如果有,则选择该字段为默认的主键,否则 InnoDB 将会自动创建一个 6Byte 的自增主键。**
|
||||||
|
|
||||||
### 二级索引(辅助索引)
|
### 二级索引(辅助索引)
|
||||||
|
|
||||||
**二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。**
|
**二级索引又称为辅助索引,是因为二级索引的叶子节点存储的数据是主键。也就是说,通过二级索引,可以定位主键的位置。**
|
||||||
|
|
||||||
唯一索引,普通索引,前缀索引等索引属于二级索引。
|
唯一索引,普通索引,前缀索引等索引属于二级索引。
|
||||||
@ -65,19 +70,22 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比
|
|||||||
4. **全文索引(Full Text)** :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。
|
4. **全文索引(Full Text)** :全文索引主要是为了检索大文本数据中的关键字的信息,是目前搜索引擎数据库使用的一种技术。Mysql5.6 之前只有 MYISAM 引擎支持全文索引,5.6 之后 InnoDB 也支持了全文索引。
|
||||||
|
|
||||||
二级索引:
|
二级索引:
|
||||||
.png)
|
.png>)
|
||||||
|
|
||||||
## 聚集索引与非聚集索引
|
## 聚集索引与非聚集索引
|
||||||
|
|
||||||
### 聚集索引
|
### 聚集索引
|
||||||
|
|
||||||
**聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。**
|
**聚集索引即索引结构和数据一起存放的索引。主键索引属于聚集索引。**
|
||||||
|
|
||||||
在 Mysql 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
|
在 Mysql 中,InnoDB 引擎的表的 `.ibd`文件就包含了该表的索引和数据,对于 InnoDB 引擎表来说,该表的索引(B+树)的每个非叶子节点存储索引,叶子节点存储索引和索引对应的数据。
|
||||||
|
|
||||||
#### 聚集索引的优点
|
#### 聚集索引的优点
|
||||||
|
|
||||||
聚集索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。
|
聚集索引的查询速度非常的快,因为整个 B+树本身就是一颗多叉平衡树,叶子节点也都是有序的,定位到索引的节点,就相当于定位到了数据。
|
||||||
|
|
||||||
#### 聚集索引的缺点
|
#### 聚集索引的缺点
|
||||||
|
|
||||||
1. **依赖于有序的数据** :因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
|
1. **依赖于有序的数据** :因为 B+树是多路平衡树,如果索引的数据不是有序的,那么就需要在插入时排序,如果数据是整型还好,否则类似于字符串或 UUID 这种又长又难比较的数据,插入或查找的速度肯定比较慢。
|
||||||
2. **更新代价大** : 如果对索引列的数据被修改时,那么对应的索引也将会被修改,
|
2. **更新代价大** : 如果对索引列的数据被修改时,那么对应的索引也将会被修改,
|
||||||
而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的,
|
而且况聚集索引的叶子节点还存放着数据,修改代价肯定是较大的,
|
||||||
@ -93,13 +101,15 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比
|
|||||||
> 该表的索引(B+树)的每个叶子非叶子节点存储索引,
|
> 该表的索引(B+树)的每个叶子非叶子节点存储索引,
|
||||||
> 叶子节点存储索引和索引对应数据的指针,指向.MYD 文件的数据。
|
> 叶子节点存储索引和索引对应数据的指针,指向.MYD 文件的数据。
|
||||||
>
|
>
|
||||||
**非聚集索引的叶子节点并不一定存放数据的指针,
|
> **非聚集索引的叶子节点并不一定存放数据的指针,
|
||||||
因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。**
|
> 因为二级索引的叶子节点就存放的是主键,根据主键再回表查数据。**
|
||||||
|
|
||||||
#### 非聚集索引的优点
|
#### 非聚集索引的优点
|
||||||
|
|
||||||
**更新代价比聚集索引要小** 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的
|
**更新代价比聚集索引要小** 。非聚集索引的更新代价就没有聚集索引那么大了,非聚集索引的叶子节点是不存放数据的
|
||||||
|
|
||||||
#### 非聚集索引的缺点
|
#### 非聚集索引的缺点
|
||||||
|
|
||||||
1. 跟聚集索引一样,非聚集索引也依赖于有序的数据
|
1. 跟聚集索引一样,非聚集索引也依赖于有序的数据
|
||||||
2. **可能会二次查询(回表)** :这应该是非聚集索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
|
2. **可能会二次查询(回表)** :这应该是非聚集索引最大的缺点了。 当查到索引对应的指针或主键后,可能还需要根据指针或主键再到数据文件或表中查询。
|
||||||
|
|
||||||
@ -112,13 +122,14 @@ B+树是有序的,在这种范围查询中,优势非常大,直接遍历比
|
|||||||

|

|
||||||
|
|
||||||
### 非聚集索引一定回表查询吗(覆盖索引)?
|
### 非聚集索引一定回表查询吗(覆盖索引)?
|
||||||
|
|
||||||
**非聚集索引不一定回表查询。**
|
**非聚集索引不一定回表查询。**
|
||||||
|
|
||||||
> 试想一种情况,用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。
|
> 试想一种情况,用户准备使用 SQL 查询用户名,而用户名字段正好建立了索引。
|
||||||
|
|
||||||
````text
|
```text
|
||||||
SELECT name FROM table WHERE username='guang19';
|
SELECT name FROM table WHERE name='guang19';
|
||||||
````
|
```
|
||||||
|
|
||||||
> 那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。
|
> 那么这个索引的 key 本身就是 name,查到对应的 name 直接返回就行了,无需回表查询。
|
||||||
|
|
||||||
@ -151,9 +162,11 @@ SELECT id FROM table WHERE id=1;
|
|||||||
## 索引创建原则
|
## 索引创建原则
|
||||||
|
|
||||||
### 单列索引
|
### 单列索引
|
||||||
|
|
||||||
单列索引即由一列属性组成的索引。
|
单列索引即由一列属性组成的索引。
|
||||||
|
|
||||||
### 联合索引(多列索引)
|
### 联合索引(多列索引)
|
||||||
|
|
||||||
联合索引即由多列属性组成索引。
|
联合索引即由多列属性组成索引。
|
||||||
|
|
||||||
### 最左前缀原则
|
### 最左前缀原则
|
||||||
@ -168,7 +181,7 @@ ALTER TABLE table ADD INDEX index_name (num,name,age)
|
|||||||
|
|
||||||
> 但可能由于版本原因(我的 mysql 版本为 8.0.x),我创建的联合索引,相当于在联合索引的每个字段上都创建了相同的索引:
|
> 但可能由于版本原因(我的 mysql 版本为 8.0.x),我创建的联合索引,相当于在联合索引的每个字段上都创建了相同的索引:
|
||||||
|
|
||||||
.png)
|
.png>)
|
||||||
|
|
||||||
无论是否符合最左前缀原则,每个字段的索引都生效:
|
无论是否符合最左前缀原则,每个字段的索引都生效:
|
||||||
|
|
||||||
|
@ -160,17 +160,13 @@ JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有
|
|||||||
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
|
- Java 有自动内存管理垃圾回收机制(GC),不需要程序员手动释放无用内存
|
||||||
- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符`'\0'`来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189)
|
- **在 C 语言中,字符串或字符数组最后都会有一个额外的字符`'\0'`来表示结束。但是,Java 语言中没有结束符这一概念。** 这是一个值得深度思考的问题,具体原因推荐看这篇文章: [https://blog.csdn.net/sszgg2006/article/details/49148189](https://blog.csdn.net/sszgg2006/article/details/49148189)
|
||||||
|
|
||||||
#### 1.1.5. 什么是 Java 程序的主类 应用程序和小程序的主类有何不同?
|
#### 1.1.5. import java 和 javax 有什么区别?
|
||||||
|
|
||||||
一个程序中可以有多个类,但只能有一个类是主类。在 Java 应用程序中,这个主类是指包含 `main()` 方法的类。而在 Java 小程序中,这个主类是一个继承自系统类 JApplet 或 Applet 的子类。应用程序的主类不一定要求是 public 类,但小程序的主类要求必须是 public 类。主类是 Java 程序执行的入口点。
|
|
||||||
|
|
||||||
#### 1.1.6. import java 和 javax 有什么区别?
|
|
||||||
|
|
||||||
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。
|
刚开始的时候 JavaAPI 所必需的包是 java 开头的包,javax 当时只是扩展 API 包来使用。然而随着时间的推移,javax 逐渐地扩展成为 Java API 的组成部分。但是,将扩展从 javax 包移动到 java 包确实太麻烦了,最终会破坏一堆现有的代码。因此,最终决定 javax 包将成为标准 API 的一部分。
|
||||||
|
|
||||||
所以,实际上 java 和 javax 没有区别。这都是一个名字。
|
所以,实际上 java 和 javax 没有区别。这都是一个名字。
|
||||||
|
|
||||||
#### 1.1.7. 为什么说 Java 语言“编译与解释并存”?
|
#### 1.1.6. 为什么说 Java 语言“编译与解释并存”?
|
||||||
|
|
||||||
高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读,
|
高级编程语言按照程序的执行方式分为编译型和解释型两种。简单来说,编译型语言是指编译器针对特定的操作系统将源代码一次性翻译成可被该平台执行的机器码;解释型语言是指解释器对源程序逐行解释成特定平台的机器码并立即执行。比如,你想阅读一本英文名著,你可以找一个英文翻译人员帮助你阅读,
|
||||||
有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
|
有两种选择方式,你可以先等翻译人员将全本的英文名著(也就是源码)都翻译成汉语,再去阅读,也可以让翻译人员翻译一段,你在旁边阅读一段,慢慢把书读完。
|
||||||
@ -1012,9 +1008,11 @@ public class Student {
|
|||||||
|
|
||||||
#### 2.5.1. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
|
#### 2.5.1. String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?
|
||||||
|
|
||||||
|
**可变性**
|
||||||
|
|
||||||
简单的来说:`String` 类中使用 final 关键字修饰字符数组来保存字符串,`private final char value[]`,所以`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` 关键字修饰,所以这两种对象都是可变的。
|
而 `StringBuilder` 与 `StringBuffer` 都继承自 `AbstractStringBuilder` 类,在 `AbstractStringBuilder` 中也是使用字符数组保存字符串`char[]value` 但是没有用 `final` 关键字修饰,所以这两种对象都是可变的。
|
||||||
|
|
||||||
@ -1221,7 +1219,7 @@ Java 代码在编译过程中,如果受检查异常没有被 `catch`/`throw`
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。
|
除了`RuntimeException`及其子类以外,其他的`Exception`类及其子类都属于受检查异常 。常见的受检查异常有: IO 相关的异常、`ClassNotFoundException` 、`SQLException`...。
|
||||||
|
|
||||||
**不受检查异常**
|
**不受检查异常**
|
||||||
|
|
||||||
|
@ -65,15 +65,16 @@ public class Pizza {
|
|||||||
首先,让我们看一下以下代码段中的运行时安全性,其中 `==` 运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException:
|
首先,让我们看一下以下代码段中的运行时安全性,其中 `==` 运算符用于比较状态,并且如果两个值均为null 都不会引发 NullPointerException。相反,如果使用equals方法,将抛出 NullPointerException:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
if(testPz.getStatus().equals(Pizza.PizzaStatus.DELIVERED));
|
Pizza.PizzaStatus pizza = null;
|
||||||
if(testPz.getStatus() == Pizza.PizzaStatus.DELIVERED);
|
System.out.println(pizza.equals(Pizza.PizzaStatus.DELIVERED));//空指针异常
|
||||||
|
System.out.println(pizza == Pizza.PizzaStatus.DELIVERED);//正常运行
|
||||||
```
|
```
|
||||||
|
|
||||||
对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较,使用equal方法比较结果确定为true,因为`getStatus`方法的枚举值与另一个类型枚举值一致,但逻辑上应该为false。这个问题可以使用==操作符避免。因为编译器会表示类型不兼容错误:
|
对于编译时安全性,我们看另一个示例,两个不同枚举类型进行比较:
|
||||||
|
|
||||||
```java
|
```java
|
||||||
if(testPz.getStatus().equals(TestColor.GREEN));
|
if (Pizza.PizzaStatus.DELIVERED.equals(TestColor.GREEN)); // 编译正常
|
||||||
if(testPz.getStatus() == TestColor.GREEN);
|
if (Pizza.PizzaStatus.DELIVERED == TestColor.GREEN); // 编译失败,类型不匹配
|
||||||
```
|
```
|
||||||
|
|
||||||
## 4.在 switch 语句中使用枚举类型
|
## 4.在 switch 语句中使用枚举类型
|
||||||
|
@ -55,7 +55,9 @@
|
|||||||
|
|
||||||
并且,以 `Map` 结尾的类都实现了 `Map` 接口。
|
并且,以 `Map` 结尾的类都实现了 `Map` 接口。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
<p style="text-align:center;font-size:13px;color:gray">https://www.javatpoint.com/collections-in-java</p>
|
||||||
|
|
||||||
### 1.1.2. 说说 List,Set,Map 三者的区别?
|
### 1.1.2. 说说 List,Set,Map 三者的区别?
|
||||||
|
|
||||||
|
@ -323,9 +323,9 @@ Semaphore 有两种模式,公平模式和非公平模式。
|
|||||||
|
|
||||||
### 4 CountDownLatch (倒计时器)
|
### 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 的两种典型用法
|
#### 4.1 CountDownLatch 的两种典型用法
|
||||||
|
|
||||||
@ -726,4 +726,3 @@ ReentrantLock 和 synchronized 的区别在上面已经讲过了这里就不多
|
|||||||
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
**Java 工程师必备学习资源:** 一些 Java 工程师常用学习资源公众号后台回复关键字 **“1”** 即可免费无套路获取。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
@ -47,7 +47,7 @@
|
|||||||
|
|
||||||
`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。
|
`ZooKeeper` 由 `Yahoo` 开发,后来捐赠给了 `Apache` ,现已成为 `Apache` 顶级项目。`ZooKeeper` 是一个开源的分布式应用程序协调服务器,其为分布式系统提供一致性服务。其一致性是通过基于 `Paxos` 算法的 `ZAB` 协议完成的。其主要功能包括:配置维护、分布式同步、集群管理、分布式事务等。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔
|
简单来说, `ZooKeeper` 是一个 **分布式协调服务框架** 。分布式?协调服务?这啥玩意?🤔🤔
|
||||||
|
|
||||||
@ -55,15 +55,15 @@
|
|||||||
|
|
||||||
比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。
|
比如,我现在有一个秒杀服务,并发量太大单机系统承受不住,那我加几台服务器也 **一样** 提供秒杀服务,这个时候就是 **`Cluster` 集群** 。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。
|
但是,我现在换一种方式,我将一个秒杀服务 **拆分成多个子服务** ,比如创建订单服务,增加积分服务,扣优惠券服务等等,**然后我将这些子服务都部署在不同的服务器上** ,这个时候就是 **`Distributed` 分布式** 。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。
|
而我为什么反驳同学所说的分布式就是加机器呢?因为我认为加机器更加适用于构建集群,因为它真是只有加机器。而对于分布式来说,你首先需要将业务进行拆分,然后再加机器(不仅仅是加机器那么简单),同时你还要去解决分布式带来的一系列问题。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。
|
比如各个分布式组件如何协调起来,如何减少各个系统之间的耦合度,分布式事务的处理,如何去配置整个分布式系统等等。`ZooKeeper` 主要就是解决这些问题的。
|
||||||
|
|
||||||
@ -73,7 +73,7 @@
|
|||||||
|
|
||||||
理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。
|
理解起来其实很简单,比如说把一个班级作为整个系统,而学生是系统中的一个个独立的子系统。这个时候班里的小红小明偷偷谈恋爱被班里的大嘴巴小花发现了,小花欣喜若狂告诉了周围的人,然后小红小明谈恋爱的消息在班级里传播起来了。当在消息的传播(散布)过程中,你抓到一个同学问他们的情况,如果回答你不知道,那么说明整个班级系统出现了数据不一致的问题(因为小花已经知道这个消息了)。而如果他直接不回答你,因为整个班级有消息在进行传播(为了保证一致性,需要所有人都知道才可提供服务),这个时候就出现了系统的可用性问题。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。
|
而上述前者就是 `Eureka` 的处理方式,它保证了AP(可用性),后者就是我们今天所要讲的 `ZooKeeper` 的处理方式,它保证了CP(数据一致性)。
|
||||||
|
|
||||||
@ -83,7 +83,7 @@
|
|||||||
|
|
||||||
这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢,对吧?
|
这时候请你思考一个问题,同学之间如果采用传纸条的方式去传播消息,那么就会出现一个问题——我咋知道我的小纸条有没有传到我想要传递的那个人手中呢?万一被哪个小家伙给劫持篡改了呢,对吧?
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
这个时候就引申出一个概念—— **拜占庭将军问题** 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的**, 所以所有的一致性算法的 **必要前提** 就是安全可靠的消息通道。
|
这个时候就引申出一个概念—— **拜占庭将军问题** 。它意指 **在不可靠信道上试图通过消息传递的方式达到一致性是不可能的**, 所以所有的一致性算法的 **必要前提** 就是安全可靠的消息通道。
|
||||||
|
|
||||||
@ -109,11 +109,11 @@
|
|||||||
|
|
||||||
而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 **回滚事务的 `rollback` 请求**,参与者收到之后将会 **回滚它在第一阶段所做的事务处理** ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。
|
而如果在第一阶段并不是所有参与者都返回了准备好了的消息,那么此时协调者将会给所有参与者发送 **回滚事务的 `rollback` 请求**,参与者收到之后将会 **回滚它在第一阶段所做的事务处理** ,然后再将处理情况返回给协调者,最终协调者收到响应后便给事务发起者返回处理失败的结果。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。
|
个人觉得 2PC 实现得还是比较鸡肋的,因为事实上它只解决了各个事务的原子性问题,随之也带来了很多的问题。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
* **单点故障问题**,如果协调者挂了那么整个系统都处于不可用的状态了。
|
* **单点故障问题**,如果协调者挂了那么整个系统都处于不可用的状态了。
|
||||||
* **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。
|
* **阻塞问题**,即当协调者发送 `prepare` 请求,参与者收到之后如果能处理那么它将会进行事务的处理但并不提交,这个时候会一直占用着资源不释放,如果此时协调者挂了,那么这些资源都不会再释放了,这会极大影响性能。
|
||||||
@ -129,7 +129,7 @@
|
|||||||
2. **PreCommit阶段**:协调者根据参与者返回的响应来决定是否可以进行下面的 `PreCommit` 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 `PreCommit` 预提交请求,**参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中** ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 **任何一个 NO** 的信息,或者 **在一定时间内** 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。
|
2. **PreCommit阶段**:协调者根据参与者返回的响应来决定是否可以进行下面的 `PreCommit` 操作。如果上面参与者返回的都是 YES,那么协调者将向所有参与者发送 `PreCommit` 预提交请求,**参与者收到预提交请求后,会进行事务的执行操作,并将 `Undo` 和 `Redo` 信息写入事务日志中** ,最后如果参与者顺利执行了事务则给协调者返回成功的响应。如果在第一阶段协调者收到了 **任何一个 NO** 的信息,或者 **在一定时间内** 并没有收到全部的参与者的响应,那么就会中断事务,它会向所有参与者发送中断请求(abort),参与者收到中断请求之后会立即中断事务,或者在一定时间内没有收到协调者的请求,它也会中断事务。
|
||||||
3. **DoCommit阶段**:这个阶段其实和 `2PC` 的第二阶段差不多,如果协调者收到了所有参与者在 `PreCommit` 阶段的 YES 响应,那么协调者将会给所有参与者发送 `DoCommit` 请求,**参与者收到 `DoCommit` 请求后则会进行事务的提交工作**,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 `PreCommit` 阶段 **收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应** ,那么就会进行中断请求的发送,参与者收到中断请求后则会 **通过上面记录的回滚日志** 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。
|
3. **DoCommit阶段**:这个阶段其实和 `2PC` 的第二阶段差不多,如果协调者收到了所有参与者在 `PreCommit` 阶段的 YES 响应,那么协调者将会给所有参与者发送 `DoCommit` 请求,**参与者收到 `DoCommit` 请求后则会进行事务的提交工作**,完成后则会给协调者返回响应,协调者收到所有参与者返回的事务提交成功的响应之后则完成事务。若协调者在 `PreCommit` 阶段 **收到了任何一个 NO 或者在一定时间内没有收到所有参与者的响应** ,那么就会进行中断请求的发送,参与者收到中断请求后则会 **通过上面记录的回滚日志** 来进行事务的回滚操作,并向协调者反馈回滚状况,协调者收到参与者返回的消息后,中断事务。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
> 这里是 `3PC` 在成功的环境下的流程图,你可以看到 `3PC` 在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能 **减少同步阻塞的时间** 。还有需要注意的是,**`3PC` 在 `DoCommit` 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交**。为什么这么做呢?是因为这个时候我们肯定**保证了在第一阶段所有的协调者全部返回了可以执行事务的响应**,这个时候我们有理由**相信其他系统都能进行事务的执行和提交**,所以**不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。
|
> 这里是 `3PC` 在成功的环境下的流程图,你可以看到 `3PC` 在很多地方进行了超时中断的处理,比如协调者在指定时间内为收到全部的确认消息则进行事务中断的处理,这样能 **减少同步阻塞的时间** 。还有需要注意的是,**`3PC` 在 `DoCommit` 阶段参与者如未收到协调者发送的提交事务的请求,它会在一定时间内进行事务的提交**。为什么这么做呢?是因为这个时候我们肯定**保证了在第一阶段所有的协调者全部返回了可以执行事务的响应**,这个时候我们有理由**相信其他系统都能进行事务的执行和提交**,所以**不管**协调者有没有发消息给参与者,进入第三阶段参与者都会进行事务的提交操作。
|
||||||
|
|
||||||
@ -150,7 +150,7 @@
|
|||||||
|
|
||||||
> 下面是 `prepare` 阶段的流程图,你可以对照着参考一下。
|
> 下面是 `prepare` 阶段的流程图,你可以对照着参考一下。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
#### 4.3.2. accept 阶段
|
#### 4.3.2. accept 阶段
|
||||||
|
|
||||||
@ -158,11 +158,11 @@
|
|||||||
|
|
||||||
表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 `accept` 该提案(此时执行提案内容但不提交),随后将情况返回给 `Proposer` 。如果不满足则不回应或者返回 NO 。
|
表决者收到提案请求后会再次比较本身已经批准过的最大提案编号和该提案编号,如果该提案编号 **大于等于** 已经批准过的最大提案编号,那么就 `accept` 该提案(此时执行提案内容但不提交),随后将情况返回给 `Proposer` 。如果不满足则不回应或者返回 NO 。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
当 `Proposer` 收到超过半数的 `accept` ,那么它这个时候会向所有的 `acceptor` 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 `acceptor` 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要**向未批准的 `acceptor` 发送提案内容和提案编号并让它无条件执行和提交**,而对于前面已经批准过该提案的 `acceptor` 来说 **仅仅需要发送该提案的编号** ,让 `acceptor` 执行提交就行了。
|
当 `Proposer` 收到超过半数的 `accept` ,那么它这个时候会向所有的 `acceptor` 发送提案的提交请求。需要注意的是,因为上述仅仅是超过半数的 `acceptor` 批准执行了该提案内容,其他没有批准的并没有执行该提案内容,所以这个时候需要**向未批准的 `acceptor` 发送提案内容和提案编号并让它无条件执行和提交**,而对于前面已经批准过该提案的 `acceptor` 来说 **仅仅需要发送该提案的编号** ,让 `acceptor` 执行提交就行了。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
而如果 `Proposer` 如果没有收到超过半数的 `accept` 那么它将会将 **递增** 该 `Proposal` 的编号,然后 **重新进入 `Prepare` 阶段** 。
|
而如果 `Proposer` 如果没有收到超过半数的 `accept` 那么它将会将 **递增** 该 `Proposal` 的编号,然后 **重新进入 `Prepare` 阶段** 。
|
||||||
|
|
||||||
@ -176,7 +176,7 @@
|
|||||||
|
|
||||||
就这样无休无止的永远提案下去,这就是 `paxos` 算法的死循环问题。
|
就这样无休无止的永远提案下去,这就是 `paxos` 算法的死循环问题。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。
|
那么如何解决呢?很简单,人多了容易吵架,我现在 **就允许一个能提案** 就行了。
|
||||||
|
|
||||||
@ -186,7 +186,7 @@
|
|||||||
|
|
||||||
作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Automic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。
|
作为一个优秀高效且可靠的分布式协调框架,`ZooKeeper` 在解决分布式数据一致性问题时并没有直接使用 `Paxos` ,而是专门定制了一致性协议叫做 `ZAB(ZooKeeper Automic Broadcast)` 原子广播协议,该协议能够很好地支持 **崩溃恢复** 。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 5.2. `ZAB` 中的三个角色
|
### 5.2. `ZAB` 中的三个角色
|
||||||
|
|
||||||
@ -204,11 +204,11 @@
|
|||||||
|
|
||||||
不就是 **在整个集群中保持数据的一致性** 嘛?如果是你,你会怎么做呢?
|
不就是 **在整个集群中保持数据的一致性** 嘛?如果是你,你会怎么做呢?
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
废话,第一步肯定需要 `Leader` 将写请求 **广播** 出去呀,让 `Leader` 问问 `Followers` 是否同意更新,如果超过半数以上的同意那么就进行 `Follower` 和 `Observer` 的更新(和 `Paxos` 一样)。当然这么说有点虚,画张图理解一下。
|
废话,第一步肯定需要 `Leader` 将写请求 **广播** 出去呀,让 `Leader` 问问 `Followers` 是否同意更新,如果超过半数以上的同意那么就进行 `Follower` 和 `Observer` 的更新(和 `Paxos` 一样)。当然这么说有点虚,画张图理解一下。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
嗯。。。看起来很简单,貌似懂了🤥🤥🤥。这两个 `Queue` 哪冒出来的?答案是 **`ZAB` 需要让 `Follower` 和 `Observer` 保证顺序性** 。何为顺序性,比如我现在有一个写请求A,此时 `Leader` 将请求A广播出去,因为只需要半数同意就行,所以可能这个时候有一个 `Follower` F1因为网络原因没有收到,而 `Leader` 又广播了一个请求B,因为网络原因,F1竟然先收到了请求B然后才收到了请求A,这个时候请求处理的顺序不同就会导致数据的不同,从而 **产生数据不一致问题** 。
|
嗯。。。看起来很简单,貌似懂了🤥🤥🤥。这两个 `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` 根本没收到所以会丢弃。
|
假设 `Leader (server2)` 发送 `commit` 请求(忘了请看上面的消息广播模式),他发送给了 `server3`,然后要发给 `server1` 的时候突然挂了。这个时候重新选举的时候我们如果把 `server1` 作为 `Leader` 的话,那么肯定会产生数据不一致性,因为 `server3` 肯定会提交刚刚 `server2` 发送的 `commit` 请求的提案,而 `server1` 根本没收到所以会丢弃。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
那怎么解决呢?
|
那怎么解决呢?
|
||||||
|
|
||||||
@ -260,7 +260,7 @@
|
|||||||
|
|
||||||
假设 `Leader (server2)` 此时同意了提案N1,自身提交了这个事务并且要发送给所有 `Follower` 要 `commit` 的请求,却在这个时候挂了,此时肯定要重新进行 `Leader` 的选举,比如说此时选 `server1` 为 `Leader` (这无所谓)。但是过了一会,这个 **挂掉的 `Leader` 又重新恢复了** ,此时它肯定会作为 `Follower` 的身份进入集群中,需要注意的是刚刚 `server2` 已经同意提交了提案N1,但其他 `server` 并没有收到它的 `commit` 信息,所以其他 `server` 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 **该提案N1最终需要被抛弃掉** 。
|
假设 `Leader (server2)` 此时同意了提案N1,自身提交了这个事务并且要发送给所有 `Follower` 要 `commit` 的请求,却在这个时候挂了,此时肯定要重新进行 `Leader` 的选举,比如说此时选 `server1` 为 `Leader` (这无所谓)。但是过了一会,这个 **挂掉的 `Leader` 又重新恢复了** ,此时它肯定会作为 `Follower` 的身份进入集群中,需要注意的是刚刚 `server2` 已经同意提交了提案N1,但其他 `server` 并没有收到它的 `commit` 信息,所以其他 `server` 不可能再提交这个提案N1了,这样就会出现数据不一致性问题了,所以 **该提案N1最终需要被抛弃掉** 。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 6. Zookeeper的几个理论知识
|
## 6. Zookeeper的几个理论知识
|
||||||
|
|
||||||
@ -272,7 +272,7 @@
|
|||||||
|
|
||||||
`zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。
|
`zookeeper` 数据存储结构与标准的 `Unix` 文件系统非常相似,都是在根节点下挂很多子节点(树型)。但是 `zookeeper` 中没有文件系统中目录与文件的概念,而是 **使用了 `znode` 作为数据节点** 。`znode` 是 `zookeeper` 中的最小数据单元,每个 `znode` 上都可以保存数据,同时还可以挂载子节点,形成一个树形化命名空间。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
每个 `znode` 都有自己所属的 **节点类型** 和 **节点状态**。
|
每个 `znode` 都有自己所属的 **节点类型** 和 **节点状态**。
|
||||||
|
|
||||||
@ -317,13 +317,13 @@
|
|||||||
|
|
||||||
`Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。
|
`Watcher` 为事件监听器,是 `zk` 非常重要的一个特性,很多功能都依赖于它,它有点类似于订阅的方式,即客户端向服务端 **注册** 指定的 `watcher` ,当服务端符合了 `watcher` 的某些事件或要求则会 **向客户端发送事件通知** ,客户端收到通知后找到自己定义的 `Watcher` 然后 **执行相应的回调方法** 。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 7. Zookeeper的几个典型应用场景
|
## 7. Zookeeper的几个典型应用场景
|
||||||
|
|
||||||
前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。
|
前面说了这么多的理论知识,你可能听得一头雾水,这些玩意有啥用?能干啥事?别急,听我慢慢道来。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### 7.1. 选主
|
### 7.1. 选主
|
||||||
|
|
||||||
@ -335,7 +335,7 @@
|
|||||||
|
|
||||||
你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?`master` 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 `watcher` 吗?我们是不是可以 **让其他不是 `master` 的节点监听节点的状态** ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表 `master` 挂了,这个时候我们 **触发回调函数进行重新选举** ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断 `master` 是否挂了等等。
|
你想想为什么我们要创建临时节点?还记得临时节点的生命周期吗?`master` 挂了是不是代表会话断了?会话断了是不是意味着这个节点没了?还记得 `watcher` 吗?我们是不是可以 **让其他不是 `master` 的节点监听节点的状态** ,比如说我们监听这个临时节点的父节点,如果子节点个数变了就代表 `master` 挂了,这个时候我们 **触发回调函数进行重新选举** ,或者我们直接监听节点的状态,我们可以通过节点是否已经失去连接来判断 `master` 是否挂了等等。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。
|
总的来说,我们可以完全 **利用 临时节点、节点状态 和 `watcher` 来实现选主的功能**,临时节点主要用来选举,节点状态和`watcher` 可以用来判断 `master` 的活性和进行重新选举。
|
||||||
|
|
||||||
@ -377,13 +377,13 @@
|
|||||||
|
|
||||||
而 `zookeeper` 天然支持的 `watcher` 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 `watcher` 进行状态监控和回调。
|
而 `zookeeper` 天然支持的 `watcher` 和 临时节点能很好的实现这些需求。我们可以为每条机器创建临时节点,并监控其父节点,如果子节点列表有变动(我们可能创建删除了临时节点),那么我们可以使用在其父节点绑定的 `watcher` 进行状态监控和回调。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
至于注册中心也很简单,我们同样也是让 **服务提供者** 在 `zookeeper` 中创建一个临时节点并且将自己的 `ip、port、调用方式` 写入节点,当 **服务消费者** 需要进行调用的时候会 **通过注册中心找到相应的服务的地址列表(IP端口什么的)** ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。
|
至于注册中心也很简单,我们同样也是让 **服务提供者** 在 `zookeeper` 中创建一个临时节点并且将自己的 `ip、port、调用方式` 写入节点,当 **服务消费者** 需要进行调用的时候会 **通过注册中心找到相应的服务的地址列表(IP端口什么的)** ,并缓存到本地(方便以后调用),当消费者调用服务时,不会再去请求注册中心,而是直接通过负载均衡算法从地址列表中取一个服务提供者的服务器调用服务。
|
||||||
|
|
||||||
当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 `Eureka` 会先试错,然后再更新)。
|
当服务提供者的某台服务器宕机或下线时,相应的地址会从服务提供者地址列表中移除。同时,注册中心会将新的服务地址列表发送给服务消费者的机器并缓存在消费者本机(当然你可以让消费者进行节点监听,我记得 `Eureka` 会先试错,然后再更新)。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
## 8. 总结
|
## 8. 总结
|
||||||
|
|
||||||
@ -391,7 +391,7 @@
|
|||||||
|
|
||||||
不知道大家是否还记得我讲了什么😒。
|
不知道大家是否还记得我讲了什么😒。
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
这篇文章中我带大家入门了 `zookeeper` 这个强大的分布式协调框架。现在我们来简单梳理一下整篇文章的内容。
|
这篇文章中我带大家入门了 `zookeeper` 这个强大的分布式协调框架。现在我们来简单梳理一下整篇文章的内容。
|
||||||
|
|
||||||
|
@ -111,13 +111,14 @@ AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无
|
|||||||
|
|
||||||
### 5.2 Spring 中的单例 bean 的线程安全问题了解吗?
|
### 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 的区别是什么?
|
### 5.3 @Component 和 @Bean 的区别是什么?
|
||||||
|
Loading…
x
Reference in New Issue
Block a user