mirror of
https://github.com/Snailclimb/JavaGuide
synced 2025-06-20 22:17:09 +08:00
commit
edd7932e00
@ -22,7 +22,7 @@
|
||||
</a >
|
||||
</p>
|
||||
|
||||
推荐使用 https://snailclimb.github.io/JavaGuide/ 在线阅读(访问速度慢的话,请使用 https://snailclimb.gitee.io/javaguide ),在线阅读内容本仓库同步一致。这种方式阅读的优势在于:有侧边栏阅读体验更好,Gitee pages 的访问速度相对来说也比较快。
|
||||
推荐使用 https://snailclimb.gitee.io/javaguide 在线阅读,在线阅读内容本仓库同步一致。这种方式阅读的优势在于:阅读体验会更好。
|
||||
|
||||
## 目录
|
||||
|
||||
@ -103,6 +103,8 @@
|
||||
* [四 类文件结构](docs/java/jvm/类文件结构.md)
|
||||
* **[五 类加载过程](docs/java/jvm/类加载过程.md)**
|
||||
* [六 类加载器](docs/java/jvm/类加载器.md)
|
||||
* **[【待完成】八 最重要的 JVM 参数指南(翻译完善了一半)](docs/java/jvm/最重要的JVM参数指南.md)**
|
||||
* [九 JVM 配置常用参数和常用 GC 调优策略](docs/java/jvm/GC调优参数.md)
|
||||
|
||||
### I/O
|
||||
|
||||
@ -298,8 +300,9 @@
|
||||
|
||||
- [Github 上热门的 Spring Boot 项目实战推荐](docs/data/spring-boot-practical-projects.md)
|
||||
|
||||
### Github 历史榜单
|
||||
### Github
|
||||
|
||||
- [Github 上 Star 数最多的 10 个项目,看完之后很意外!](docs/tools/github/github-star-ranking.md)
|
||||
- [Java 项目月榜单](docs/github-trending/JavaGithubTrending.md)
|
||||
|
||||
***
|
||||
@ -336,7 +339,7 @@ Markdown 格式参考:[Github Markdown格式](https://guides.github.com/featur
|
||||
|
||||
1. 笔记内容大多是手敲,所以难免会有笔误,你可以帮我找错别字。
|
||||
2. 很多知识点我可能没有涉及到,所以你可以对其他知识点进行补充。
|
||||
3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点的修改/补充。
|
||||
3. 现有的知识点难免存在不完善或者错误,所以你可以对已有知识点进行修改/补充。
|
||||
|
||||
### 为什么要做这个开源文档?
|
||||
|
||||
|
237
docs/dataStructures-algorithms/data-structure/bloom-filter.md
Normal file
237
docs/dataStructures-algorithms/data-structure/bloom-filter.md
Normal file
@ -0,0 +1,237 @@
|
||||
最近,当我在做一个项目的时候需要过滤掉重复的 URL ,为了完成这个任务,我学到了一种称为 Bloom Filter (布隆过滤器)的东西,然后我学会了它并写下了这个博客。
|
||||
|
||||
下面我们将分为几个方面来介绍布隆过滤器:
|
||||
|
||||
1. 什么是布隆过滤器?
|
||||
2. 布隆过滤器的原理介绍。
|
||||
3. 布隆过滤器使用场景。
|
||||
4. 通过 Java 编程手动实现布隆过滤器。
|
||||
5. 利用Google开源的Guava中自带的布隆过滤器。
|
||||
6. Redis 中的布隆过滤器。
|
||||
7. 总结。
|
||||
|
||||
### 1.什么是布隆过滤器?
|
||||
|
||||
首先,我们需要了解布隆过滤器的概念。
|
||||
|
||||
布隆过滤器(Bloom Filter)是一个叫做 Bloom 的老哥于1970年提出的。我们可以把它看作由二进制向量(或者说位数组)和一系列随机映射函数(哈希函数)两部分组成的数据结构。相比于我们平时常用的的 List、Map 、Set 等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。理论情况下添加到集合中的元素越多,误报的可能性就越大。并且,存放在布隆过滤器的数据不容易删除。
|
||||
|
||||

|
||||
|
||||
位数组中的每个元素都只占用 1 bit ,并且每个元素只能是 0 或者 1。这样申请一个 100w 个元素的位数组只占用 1000000 / 8 = 125000 B = 15625 byte ≈ 15.3kb 的空间。
|
||||
|
||||
总结:**一个名叫 Bloom 的人提出了一种来检索元素是否在给定大集合中的数据结构,这种数据结构是高效且性能很好的,但缺点是具有一定的错误识别率和删除难度。并且,理论情况下,添加到集合中的元素越多,误报的可能性就越大。**
|
||||
|
||||
### 2.布隆过滤器的原理介绍
|
||||
|
||||
**当一个元素加入布隆过滤器中的时候,会进行如下操作:**
|
||||
|
||||
1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
|
||||
2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。
|
||||
|
||||
**当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:**
|
||||
|
||||
1. 对给定元素再次进行相同的哈希计算;
|
||||
2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||
|
||||
举个简单的例子:
|
||||
|
||||
|
||||
|
||||

|
||||
|
||||
如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为1,所以很容易知道此值已经存在(去重非常方便)。
|
||||
|
||||
如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||
|
||||
**不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。**
|
||||
|
||||
综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**
|
||||
|
||||
### 3.布隆过滤器使用场景
|
||||
|
||||
1. 判断给定数据是否存在:比如判断一个数字是否在于包含大量数字的数字集中(数字集很大,5亿以上!)、 防止缓存穿透(判断请求的数据是否有效避免直接绕过缓存请求数据库)等等、邮箱的垃圾邮件过滤、黑名单功能等等。
|
||||
2. 去重:比如爬给定网址的时候对已经爬取过的 URL 去重。
|
||||
|
||||
### 4.通过 Java 编程手动实现布隆过滤器
|
||||
|
||||
我们上面已经说了布隆过滤器的原理,知道了布隆过滤器的原理之后就可以自己手动实现一个了。
|
||||
|
||||
如果你想要手动实现一个的话,你需要:
|
||||
|
||||
1. 一个合适大小的位数组保存数据
|
||||
2. 几个不同的哈希函数
|
||||
3. 添加元素到位数组(布隆过滤器)的方法实现
|
||||
4. 判断给定元素是否存在于位数组(布隆过滤器)的方法实现。
|
||||
|
||||
下面给出一个我觉得写的还算不错的代码(参考网上已有代码改进得到,对于所有类型对象皆适用):
|
||||
|
||||
```java
|
||||
import java.util.BitSet;
|
||||
|
||||
public class MyBloomFilter {
|
||||
|
||||
/**
|
||||
* 位数组的大小
|
||||
*/
|
||||
private static final int DEFAULT_SIZE = 2 << 24;
|
||||
/**
|
||||
* 通过这个数组可以创建 6 个不同的哈希函数
|
||||
*/
|
||||
private static final int[] SEEDS = new int[]{3, 13, 46, 71, 91, 134};
|
||||
|
||||
/**
|
||||
* 位数组。数组中的元素只能是 0 或者 1
|
||||
*/
|
||||
private BitSet bits = new BitSet(DEFAULT_SIZE);
|
||||
|
||||
/**
|
||||
* 存放包含 hash 函数的类的数组
|
||||
*/
|
||||
private SimpleHash[] func = new SimpleHash[SEEDS.length];
|
||||
|
||||
/**
|
||||
* 初始化多个包含 hash 函数的类的数组,每个类中的 hash 函数都不一样
|
||||
*/
|
||||
public MyBloomFilter() {
|
||||
// 初始化多个不同的 Hash 函数
|
||||
for (int i = 0; i < SEEDS.length; i++) {
|
||||
func[i] = new SimpleHash(DEFAULT_SIZE, SEEDS[i]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加元素到位数组
|
||||
*/
|
||||
public void add(Object value) {
|
||||
for (SimpleHash f : func) {
|
||||
bits.set(f.hash(value), true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断指定元素是否存在于位数组
|
||||
*/
|
||||
public boolean contains(Object value) {
|
||||
boolean ret = true;
|
||||
for (SimpleHash f : func) {
|
||||
ret = ret && bits.get(f.hash(value));
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
/**
|
||||
* 静态内部类。用于 hash 操作!
|
||||
*/
|
||||
public static class SimpleHash {
|
||||
|
||||
private int cap;
|
||||
private int seed;
|
||||
|
||||
public SimpleHash(int cap, int seed) {
|
||||
this.cap = cap;
|
||||
this.seed = seed;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 hash 值
|
||||
*/
|
||||
public int hash(Object value) {
|
||||
int h;
|
||||
return (value == null) ? 0 : Math.abs(seed * (cap - 1) & ((h = value.hashCode()) ^ (h >>> 16)));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
测试:
|
||||
|
||||
```java
|
||||
String value1 = "https://javaguide.cn/";
|
||||
String value2 = "https://github.com/Snailclimb";
|
||||
MyBloomFilter filter = new MyBloomFilter();
|
||||
System.out.println(filter.contains(value1));
|
||||
System.out.println(filter.contains(value2));
|
||||
filter.add(value1);
|
||||
filter.add(value2);
|
||||
System.out.println(filter.contains(value1));
|
||||
System.out.println(filter.contains(value2));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```
|
||||
false
|
||||
false
|
||||
true
|
||||
true
|
||||
```
|
||||
|
||||
测试:
|
||||
|
||||
```java
|
||||
Integer value1 = 13423;
|
||||
Integer value2 = 22131;
|
||||
MyBloomFilter filter = new MyBloomFilter();
|
||||
System.out.println(filter.contains(value1));
|
||||
System.out.println(filter.contains(value2));
|
||||
filter.add(value1);
|
||||
filter.add(value2);
|
||||
System.out.println(filter.contains(value1));
|
||||
System.out.println(filter.contains(value2));
|
||||
```
|
||||
|
||||
Output:
|
||||
|
||||
```java
|
||||
false
|
||||
false
|
||||
true
|
||||
true
|
||||
```
|
||||
|
||||
### 5.利用Google开源的 Guava中自带的布隆过滤器
|
||||
|
||||
自己实现的目的主要是为了让自己搞懂布隆过滤器的原理,Guava 中布隆过滤器的实现算是比较权威的,所以实际项目中我们不需要手动实现一个布隆过滤器。
|
||||
|
||||
首先我们需要在项目中引入 Guava 的依赖:
|
||||
|
||||
```java
|
||||
<dependency>
|
||||
<groupId>com.google.guava</groupId>
|
||||
<artifactId>guava</artifactId>
|
||||
<version>28.0-jre</version>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
实际使用如下:
|
||||
|
||||
我们创建了一个最多存放 最多 1500个整数的布隆过滤器,并且我们可以容忍误判的概率为百分之(0.01)
|
||||
|
||||
```java
|
||||
// 创建布隆过滤器对象
|
||||
BloomFilter<Integer> filter = BloomFilter.create(
|
||||
Funnels.integerFunnel(),
|
||||
1500,
|
||||
0.01);
|
||||
// 判断指定元素是否存在
|
||||
System.out.println(filter.mightContain(1));
|
||||
System.out.println(filter.mightContain(2));
|
||||
// 将元素添加进布隆过滤器
|
||||
filter.put(1);
|
||||
filter.put(2);
|
||||
System.out.println(filter.mightContain(1));
|
||||
System.out.println(filter.mightContain(2));
|
||||
```
|
||||
|
||||
在我们的示例中,当`mightContain()` 方法返回*true*时,我们可以99%确定该元素在过滤器中,当过滤器返回*false*时,我们可以100%确定该元素不存在于过滤器中。
|
||||
|
||||
### 6.Redis 中的布隆过滤器
|
||||
|
||||
- https://juejin.im/post/5bc7446e5188255c791b3360
|
||||
|
||||
### 8.其他推荐阅读
|
||||
|
||||
1. 详解布隆过滤器的原理,使用场景和注意事项:https://zhuanlan.zhihu.com/p/43263751
|
||||
2.
|
@ -257,11 +257,15 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
|
||||
|
||||
### 缓存雪崩和缓存穿透问题解决方案
|
||||
|
||||
**缓存雪崩**
|
||||
#### **缓存雪崩**
|
||||
|
||||
**什么是缓存雪崩?**
|
||||
|
||||
简介:缓存同一时间大面积的失效,所以,后面的请求都会落到数据库上,造成数据库短时间内承受大量请求而崩掉。
|
||||
|
||||
解决办法(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
|
||||
**有哪些解决办法?**
|
||||
|
||||
(中华石杉老师在他的视频中提到过,视频地址在最后一个问题中有提到):
|
||||
|
||||
- 事前:尽量保证整个 redis 集群的高可用性,发现机器宕机尽快补上。选择合适的内存淘汰策略。
|
||||
- 事中:本地ehcache缓存 + hystrix限流&降级,避免MySQL崩掉
|
||||
@ -269,16 +273,46 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
|
||||
|
||||

|
||||
|
||||
#### **缓存穿透**
|
||||
|
||||
**缓存穿透**
|
||||
**什么是缓存穿透?**
|
||||
|
||||
简介:一般是黑客故意去请求缓存中不存在的数据,导致所有的请求都落到数据库上,造成数据库短时间内承受大量请求而崩掉。
|
||||
缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
|
||||
|
||||
解决办法: 有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数 据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
|
||||
一般MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%'; `命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了。
|
||||
|
||||
**有哪些解决办法?**
|
||||
|
||||
最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
|
||||
|
||||
**1)缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如何黑客恶意攻击,每次构建的不同的请求key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
|
||||
|
||||
另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。
|
||||
|
||||
如果用 Java 代码展示的话,差不多是下面这样的:
|
||||
|
||||
```java
|
||||
public Object getObjectInclNullById(Integer id) {
|
||||
// 从缓存中获取数据
|
||||
Object cacheValue = cache.get(id);
|
||||
// 缓存为空
|
||||
if (cacheValue != null) {
|
||||
// 从数据库中获取
|
||||
Object storageValue = storage.get(key);
|
||||
// 缓存空对象
|
||||
cache.set(key, storageValue);
|
||||
// 如果存储数据为空,需要设置一个过期时间(300秒)
|
||||
if (storageValue == null) {
|
||||
// 必须设置过期时间,否则有被攻击的风险
|
||||
cache.expire(key, 60 * 5);
|
||||
}
|
||||
return storageValue;
|
||||
}
|
||||
return cacheValue;
|
||||
}
|
||||
```
|
||||
|
||||
参考:
|
||||
|
||||
- [https://blog.csdn.net/zeb_perfect/article/details/54135506](https://blog.csdn.net/zeb_perfect/article/details/54135506)
|
||||
|
||||
### 如何解决 Redis 的并发竞争 Key 问题
|
||||
|
||||
@ -308,6 +342,11 @@ Redis 通过 MULTI、EXEC、WATCH 等命令来实现事务(transaction)功能。
|
||||
|
||||
**参考:** Java工程师面试突击第1季(可能是史上最好的Java面试突击课程)-中华石杉老师!公众号后台回复关键字“1”即可获取该视频内容。
|
||||
|
||||
### 参考
|
||||
|
||||
- 《Redis开发与运维》
|
||||
- Redis 命令总结:http://redisdoc.com/string/set.html
|
||||
|
||||
## 公众号
|
||||
|
||||
如果大家想要实时关注我更新的文章以及分享的干货的话,可以关注我的公众号。
|
||||
|
@ -1,19 +1,68 @@
|
||||
相关阅读:
|
||||
|
||||
- [史上最全Redis高可用技术解决方案大全](https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247484850&idx=1&sn=3238360bfa8105cf758dcf7354af2814&chksm=cea24a79f9d5c36fb2399aafa91d7fb2699b5006d8d037fe8aaf2e5577ff20ae322868b04a87&token=1082669959&lang=zh_CN&scene=21#wechat_redirect)
|
||||
- [Raft协议实战之Redis Sentinel的选举Leader源码解析](http://weizijun.cn/2015/04/30/Raft%E5%8D%8F%E8%AE%AE%E5%AE%9E%E6%88%98%E4%B9%8BRedis%20Sentinel%E7%9A%84%E9%80%89%E4%B8%BELeader%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/)
|
||||
|
||||
目录:
|
||||
|
||||
<!-- TOC -->
|
||||
|
||||
- [Redis 集群以及应用](#redis-集群以及应用)
|
||||
- [集群](#集群)
|
||||
- [主从复制](#主从复制)
|
||||
- [主从链(拓扑结构)](#主从链拓扑结构)
|
||||
- [复制模式](#复制模式)
|
||||
- [问题点](#问题点)
|
||||
- [哨兵机制](#哨兵机制)
|
||||
- [拓扑图](#拓扑图)
|
||||
- [节点下线](#节点下线)
|
||||
- [Leader选举](#Leader选举)
|
||||
- [故障转移](#故障转移)
|
||||
- [读写分离](#读写分离)
|
||||
- [定时任务](#定时任务)
|
||||
- [分布式集群(Cluster)](#分布式集群cluster)
|
||||
- [拓扑图](#拓扑图)
|
||||
- [通讯](#通讯)
|
||||
- [集中式](#集中式)
|
||||
- [Gossip](#gossip)
|
||||
- [寻址分片](#寻址分片)
|
||||
- [hash取模](#hash取模)
|
||||
- [一致性hash](#一致性hash)
|
||||
- [hash槽](#hash槽)
|
||||
- [使用场景](#使用场景)
|
||||
- [热点数据](#热点数据)
|
||||
- [会话维持 Session](#会话维持-session)
|
||||
- [分布式锁 SETNX](#分布式锁-setnx)
|
||||
- [表缓存](#表缓存)
|
||||
- [消息队列 list](#消息队列-list)
|
||||
- [计数器 string](#计数器-string)
|
||||
- [缓存设计](#缓存设计)
|
||||
- [更新策略](#更新策略)
|
||||
- [更新一致性](#更新一致性)
|
||||
- [缓存粒度](#缓存粒度)
|
||||
- [缓存穿透](#缓存穿透)
|
||||
- [解决方案](#解决方案)
|
||||
- [缓存雪崩](#缓存雪崩)
|
||||
- [出现后应对](#出现后应对)
|
||||
- [请求过程](#请求过程)
|
||||
|
||||
<!-- /MarkdownTOC -->
|
||||
|
||||
# Redis 集群以及应用
|
||||
|
||||
## 集群
|
||||
|
||||
### 主从复制
|
||||
|
||||
#### 主从链(拓扑结构)
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
#### 复制模式
|
||||
- 全量复制:master 全部同步到 slave
|
||||
- 部分复制:slave 数据丢失进行备份
|
||||
- 全量复制:Master 全部同步到 Slave
|
||||
- 部分复制:Slave 数据丢失进行备份
|
||||
|
||||
#### 问题点
|
||||
- 同步故障
|
||||
@ -26,124 +75,194 @@
|
||||
- 优化参数不一致:内存不一致.
|
||||
- 避免全量复制
|
||||
- 选择小主节点(分片)、低峰期间操作.
|
||||
- 如果节点运行 id 不匹配(如主节点重启、运行 id 发送变化),此时要执行全量复制,应该配合哨兵和集群解决.
|
||||
- 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足),可增大复制缓冲区( rel_backlog_size 参数).
|
||||
- 如果节点运行 id 不匹配(如主节点重启、运行 id 发送变化),此时要执行全量复制,应该配合哨兵和集群解决.
|
||||
- 主从复制挤压缓冲区不足产生的问题(网络中断,部分复制无法满足),可增大复制缓冲区( rel_backlog_size 参数).
|
||||
- 复制风暴
|
||||
|
||||
### 哨兵机制
|
||||
|
||||
#### 拓扑图
|
||||
|
||||

|
||||
|
||||
#### 节点下线
|
||||
- 客观下线
|
||||
- 所有 Sentinel 节点对 Redis 节点失败要达成共识,即超过 quorum 个统一.
|
||||
|
||||
- 主观下线
|
||||
- 即 Sentinel 节点对 Redis 节点失败的偏见,超出超时时间认为 Master 已经宕机.
|
||||
#### leader选举
|
||||
- 选举出一个 Sentinel 作为 Leader:集群中至少有三个 Sentinel 节点,但只有其中一个节点可完成故障转移.通过以下命令可以进行失败判定或领导者选举.
|
||||
- 即 Sentinel 节点对 Redis 节点失败的偏见,超出超时时间认为 Master 已经宕机。
|
||||
- Sentinel 集群的每一个 Sentinel 节点会定时对 Redis 集群的所有节点发心跳包检测节点是否正常。如果一个节点在 `down-after-milliseconds` 时间内没有回复 Sentinel 节点的心跳包,则该 Redis 节点被该 Sentinel 节点主观下线。
|
||||
- 客观下线
|
||||
- 所有 Sentinel 节点对 Redis 节点失败要达成共识,即超过 quorum 个统一。
|
||||
- 当节点被一个 Sentinel 节点记为主观下线时,并不意味着该节点肯定故障了,还需要 Sentinel 集群的其他 Sentinel 节点共同判断为主观下线才行。
|
||||
- 该 Sentinel 节点会询问其它 Sentinel 节点,如果 Sentinel 集群中超过 quorum 数量的 Sentinel 节点认为该 Redis 节点主观下线,则该 Redis 客观下线。
|
||||
|
||||
#### Leader选举
|
||||
|
||||
- 选举出一个 Sentinel 作为 Leader:集群中至少有三个 Sentinel 节点,但只有其中一个节点可完成故障转移.通过以下命令可以进行失败判定或领导者选举。
|
||||
- 选举流程
|
||||
1. 每个主观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者.
|
||||
1. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝.
|
||||
1. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum,则它成为领导者.
|
||||
1. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举.
|
||||
1. 每个主观下线的 Sentinel 节点向其他 Sentinel 节点发送命令,要求设置它为领导者.
|
||||
2. 收到命令的 Sentinel 节点如果没有同意通过其他 Sentinel 节点发送的命令,则同意该请求,否则拒绝。
|
||||
3. 如果该 Sentinel 节点发现自己的票数已经超过 Sentinel 集合半数且超过 quorum,则它成为领导者。
|
||||
4. 如果此过程有多个 Sentinel 节点成为领导者,则等待一段时间再重新进行选举。
|
||||
|
||||
#### 故障转移
|
||||
|
||||
- 转移流程
|
||||
1. Sentinel 选出一个合适的 Slave 作为新的 Master(slaveof no one 命令).
|
||||
1. 向其余 Slave 发出通知,让它们成为新 Master 的 Slave( parallel-syncs 参数).
|
||||
1. 等待旧 Master 复活,并使之称为新 Master 的 Slave.
|
||||
1. 向客户端通知 Master 变化.
|
||||
1. Sentinel 选出一个合适的 Slave 作为新的 Master(slaveof no one 命令)。
|
||||
2. 向其余 Slave 发出通知,让它们成为新 Master 的 Slave( parallel-syncs 参数)。
|
||||
3. 等待旧 Master 复活,并使之称为新 Master 的 Slave。
|
||||
4. 向客户端通知 Master 变化。
|
||||
- 从 Slave 中选择新 Master 节点的规则(slave 升级成 master 之后)
|
||||
1. 选择 slave-priority 最高的节点.
|
||||
1. 选择复制偏移量最大的节点(同步数据最多).
|
||||
1. 选择 runId 最小的节点.
|
||||
1. 选择 slave-priority 最高的节点。
|
||||
2. 选择复制偏移量最大的节点(同步数据最多)。
|
||||
3. 选择 runId 最小的节点。
|
||||
|
||||
>Sentinel 集群运行过程中故障转移完成,所有 Sentinel 又会恢复平等。Leader 仅仅是故障转移操作出现的角色。
|
||||
|
||||
#### 读写分离
|
||||
|
||||
#### 定时任务
|
||||
- 每 1s 每个 Sentinel 对其他 Sentinel 和 Redis 执行 ping,进行心跳检测.
|
||||
- 每 2s 每个 Sentinel 通过 Master 的 Channel 交换信息(pub - sub).
|
||||
- 每 10s 每个 Sentinel 对 Master 和 Slave 执行 info,目的是发现 Slave 节点、确定主从关系.
|
||||
|
||||
- 每 1s 每个 Sentinel 对其他 Sentinel 和 Redis 执行 ping,进行心跳检测。
|
||||
- 每 2s 每个 Sentinel 通过 Master 的 Channel 交换信息(pub - sub)。
|
||||
- 每 10s 每个 Sentinel 对 Master 和 Slave 执行 info,目的是发现 Slave 节点、确定主从关系。
|
||||
|
||||
### 分布式集群(Cluster)
|
||||
|
||||
#### 拓扑图
|
||||
|
||||

|
||||
|
||||
#### 通讯
|
||||
|
||||
##### 集中式
|
||||
> 将集群元数据(节点信息、故障等等)几种存储在某个节点上.
|
||||
|
||||
> 将集群元数据(节点信息、故障等等)几种存储在某个节点上。
|
||||
- 优势
|
||||
1. 元数据的更新读取具有很强的时效性,元数据修改立即更新
|
||||
1. 元数据的更新读取具有很强的时效性,元数据修改立即更新
|
||||
- 劣势
|
||||
1. 数据集中存储
|
||||
|
||||
##### Gossip
|
||||
|
||||

|
||||
|
||||
- [Gossip 协议](https://www.jianshu.com/p/8279d6fd65bb)
|
||||
|
||||
#### 寻址分片
|
||||
|
||||
##### hash取模
|
||||
|
||||
- hash(key)%机器数量
|
||||
- 问题
|
||||
1. 机器宕机,造成数据丢失,数据读取失败
|
||||
1. 机器宕机,造成数据丢失,数据读取失败
|
||||
1. 伸缩性
|
||||
|
||||
##### 一致性hash
|
||||
|
||||
- 
|
||||
|
||||
- 问题
|
||||
1. 一致性哈希算法在节点太少时,容易因为节点分布不均匀而造成缓存热点的问题。
|
||||
- 解决方案
|
||||
- 可以通过引入虚拟节点机制解决:即对每一个节点计算多个 hash,每个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。
|
||||
|
||||
##### hash槽
|
||||
|
||||
- CRC16(key)%16384
|
||||
-
|
||||

|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## 使用场景
|
||||
|
||||
### 热点数据
|
||||
### 会话维持 session
|
||||
|
||||
存取数据优先从 Redis 操作,如果不存在再从文件(例如 MySQL)中操作,从文件操作完后将数据存储到 Redis 中并返回。同时有个定时任务后台定时扫描 Redis 的 key,根据业务规则进行淘汰,防止某些只访问一两次的数据一直存在 Redis 中。
|
||||
>例如使用 Zset 数据结构,存储 Key 的访问次数/最后访问时间作为 Score,最后做排序,来淘汰那些最少访问的 Key。
|
||||
|
||||
如果企业级应用,可以参考:[阿里云的 Redis 混合存储版][1]
|
||||
|
||||
### 会话维持 Session
|
||||
|
||||
会话维持 Session 场景,即使用 Redis 作为分布式场景下的登录中心存储应用。每次不同的服务在登录的时候,都会去统一的 Redis 去验证 Session 是否正确。但是在微服务场景,一般会考虑 Redis + JWT 做 Oauth2 模块。
|
||||
>其中 Redis 存储 JWT 的相关信息主要是留出口子,方便以后做统一的防刷接口,或者做登录设备限制等。
|
||||
|
||||
### 分布式锁 SETNX
|
||||
|
||||
命令格式:`SETNX key value`:当且仅当 key 不存在,将 key 的值设为 value。若给定的 key 已经存在,则 SETNX 不做任何动作。
|
||||
|
||||
1. 超时时间设置:获取锁的同时,启动守护线程,使用 expire 进行定时更新超时时间。如果该业务机器宕机,守护线程也挂掉,这样也会自动过期。如果该业务不是宕机,而是真的需要这么久的操作时间,那么增加超时时间在业务上也是可以接受的,但是肯定有个最大的阈值。
|
||||
2. 但是为了增加高可用,需要使用多台 Redis,就增加了复杂性,就可以参考 Redlock:[Redlock分布式锁](Redlock分布式锁.md#怎么在单节点上实现分布式锁)
|
||||
|
||||
### 表缓存
|
||||
|
||||
Redis 缓存表的场景有黑名单、禁言表等。访问频率较高,即读高。根据业务需求,可以使用后台定时任务定时刷新 Redis 的缓存表数据。
|
||||
|
||||
### 消息队列 list
|
||||
|
||||
主要使用了 List 数据结构。
|
||||
List 支持在头部和尾部操作,因此可以实现简单的消息队列。
|
||||
1. 发消息:在 List 尾部塞入数据。
|
||||
2. 消费消息:在 List 头部拿出数据。
|
||||
|
||||
同时可以使用多个 List,来实现多个队列,根据不同的业务消息,塞入不同的 List,来增加吞吐量。
|
||||
|
||||
### 计数器 string
|
||||
|
||||
主要使用了 INCR、DECR、INCRBY、DECRBY 方法。
|
||||
|
||||
|
||||
|
||||
INCR key:给 key 的 value 值增加一
|
||||
DECR key:给 key 的 value 值减去一
|
||||
|
||||
## 缓存设计
|
||||
|
||||
### 更新策略
|
||||
- LRU、LFU、FIFO 算法自动清除:一致性最差,维护成本低.
|
||||
- 超时自动清除(key expire):一致性较差,维护成本低.
|
||||
- 主动更新:代码层面控制生命周期,一致性最好,维护成本高.
|
||||
|
||||
- LRU、LFU、FIFO 算法自动清除:一致性最差,维护成本低。
|
||||
- 超时自动清除(key expire):一致性较差,维护成本低。
|
||||
- 主动更新:代码层面控制生命周期,一致性最好,维护成本高。
|
||||
|
||||
在 Redis 根据在 redis.conf 的参数 `maxmemory` 来做更新淘汰策略:
|
||||
1. noeviction: 不删除策略, 达到最大内存限制时, 如果需要更多内存, 直接返回错误信息。大多数写命令都会导致占用更多的内存(有极少数会例外, 如 DEL 命令)。
|
||||
2. allkeys-lru: 所有 key 通用; 优先删除最近最少使用(less recently used ,LRU) 的 key。
|
||||
3. volatile-lru: 只限于设置了 expire 的部分; 优先删除最近最少使用(less recently used ,LRU) 的 key。
|
||||
4. allkeys-random: 所有key通用; 随机删除一部分 key。
|
||||
5. volatile-random: 只限于设置了 expire 的部分; 随机删除一部分 key。
|
||||
6. volatile-ttl: 只限于设置了 expire 的部分; 优先删除剩余时间(time to live,TTL) 短的key。
|
||||
|
||||
### 更新一致性
|
||||
- 读请求:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应.
|
||||
- 写请求:先删除缓存,然后再更新数据库(避免大量地写、却又不经常读的数据导致缓存频繁更新).
|
||||
|
||||
- 读请求:先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应。
|
||||
- 写请求:先删除缓存,然后再更新数据库(避免大量地写、却又不经常读的数据导致缓存频繁更新)。
|
||||
|
||||
### 缓存粒度
|
||||
- 通用性:全量属性更好.
|
||||
- 占用空间:部分属性更好.
|
||||
- 代码维护成本.
|
||||
|
||||
- 通用性:全量属性更好。
|
||||
- 占用空间:部分属性更好。
|
||||
- 代码维护成本。
|
||||
|
||||
### 缓存穿透
|
||||
> 当大量的请求无命中缓存、直接请求到后端数据库(业务代码的 bug、或恶意攻击),同时后端数据库也没有查询到相应的记录、无法添加缓存.
|
||||
这种状态会一直维持,流量一直打到存储层上,无法利用缓存、还会给存储层带来巨大压力.
|
||||
>
|
||||
|
||||
> 当大量的请求无命中缓存、直接请求到后端数据库(业务代码的 bug、或恶意攻击),同时后端数据库也没有查询到相应的记录、无法添加缓存。
|
||||
> 这种状态会一直维持,流量一直打到存储层上,无法利用缓存、还会给存储层带来巨大压力。
|
||||
|
||||
#### 解决方案
|
||||
|
||||
1. 请求无法命中缓存、同时数据库记录为空时在缓存添加该 key 的空对象(设置过期时间),缺点是可能会在缓存中添加大量的空值键(比如遭到恶意攻击或爬虫),而且缓存层和存储层数据短期内不一致;
|
||||
1. 使用布隆过滤器在缓存层前拦截非法请求、自动为空值添加黑名单(同时可能要为误判的记录添加白名单).但需要考虑布隆过滤器的维护(离线生成/ 实时生成).
|
||||
2. 使用布隆过滤器在缓存层前拦截非法请求、自动为空值添加黑名单(同时可能要为误判的记录添加白名单).但需要考虑布隆过滤器的维护(离线生成/ 实时生成)。
|
||||
|
||||
### 缓存雪崩
|
||||
> 缓存崩溃时请求会直接落到数据库上,很可能由于无法承受大量的并发请求而崩溃,此时如果只重启数据库,或因为缓存重启后没有数据,新的流量进来很快又会把数据库击倒
|
||||
>
|
||||
|
||||
> 缓存崩溃时请求会直接落到数据库上,很可能由于无法承受大量的并发请求而崩溃,此时如果只重启数据库,或因为缓存重启后没有数据,新的流量进来很快又会把数据库击倒。
|
||||
|
||||
#### 出现后应对
|
||||
- 事前:Redis 高可用,主从 + 哨兵,Redis Cluster,避免全盘崩溃.
|
||||
- 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免数据库承受太多压力.
|
||||
- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据.
|
||||
|
||||
- 事前:Redis 高可用,主从 + 哨兵,Redis Cluster,避免全盘崩溃。
|
||||
- 事中:本地 ehcache 缓存 + hystrix 限流 & 降级,避免数据库承受太多压力。
|
||||
- 事后:Redis 持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。
|
||||
|
||||
#### 请求过程
|
||||
1. 用户请求先访问本地缓存,无命中后再访问 Redis,如果本地缓存和 Redis 都没有再查数据库,并把数据添加到本地缓存和 Redis;
|
||||
1. 由于设置了限流,一段时间范围内超出的请求走降级处理(返回默认值,或给出友情提示).
|
||||
|
||||
|
||||
1. 用户请求先访问本地缓存,无命中后再访问 Redis,如果本地缓存和 Redis 都没有再查数据库,并把数据添加到本地缓存和 Redis;
|
||||
2. 由于设置了限流,一段时间范围内超出的请求走降级处理(返回默认值,或给出友情提示)。
|
||||
|
||||
[1]: https://promotion.aliyun.com/ntms/act/redishybridstorage.html?spm=5176.54432.1380373.5.41921cf20pcZrZ&aly_as=ArH4VaEb
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,141 @@
|
||||
本文的内容都是根据读者投稿的真实面试经历改编而来,首次尝试这种风格的文章,花了几天晚上才总算写完,希望对你有帮助。本文主要涵盖下面的内容:
|
||||
|
||||
1. 分布式商城系统:架构图讲解;
|
||||
2. 消息队列相关:削峰和解耦;
|
||||
3. Redis 相关:缓存穿透问题的解决;
|
||||
4. 一些 Java 基础问题;
|
||||
|
||||
面试开始,坐在我前面的就是这次我的面试官吗?这发量看着根本不像程序员啊?我心里正嘀咕着,只听见面试官说:“小伙,下午好,我今天就是你的面试官,咱们开始面试吧!”。
|
||||
|
||||
### 第一面开始
|
||||
|
||||
**面试官:** 我也不用多说了,你先自我介绍一下吧,简历上有的就不要再说了哈。
|
||||
|
||||
**我:** 内心 os:"果然如我所料,就知道会让我先自我介绍一下,还好我看了 [JavaGuide](https://github.com/Snailclimb/JavaGuide "JavaGuide") ,学到了一些套路。套路总结起来就是:**最好准备好两份自我介绍,一份对 hr 说的,主要讲能突出自己的经历,会的编程技术一语带过;另一份对技术面试官说的,主要讲自己会的技术细节,项目经验,经历那些就一语带过。** 所以,我按照这个套路准备了一个还算通用的模板,毕竟我懒嘛!不想多准备一个自我介绍,整个通用的多好!
|
||||
|
||||
> 面试官,您好!我叫小李子。大学时间我主要利用课外时间学习 Java 相关的知识。在校期间参与过一个校园图书馆系统的开发,另外,我自己在学习过程中也参照网上的教程写过一个电商系统的网站,写这个电商网站主要是为了能让自己接触到分布式系统的开发。在学习之余,我比较喜欢通过博客整理分享自己所学知识。我现在已经是某社区的认证作者,写过一系列关于 线程池使用以及源码分析的文章深受好评。另外,我获得过省级编程比赛二等奖,我将这个获奖项目开源到 Github 还收获了 2k 的 Star 呢?
|
||||
|
||||
**面试官:** 你刚刚说参考网上的教程做了一个电商系统?你能画画这个电商系统的架构图吗?
|
||||
|
||||
**我:** 内心 os: "这可难不倒我!早知道写在简历上的项目要重视了,提前都把这个系统的架构图画了好多遍了呢!"
|
||||
|
||||
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/商城系统-架构图plus.png" style="zoom:50%;" />
|
||||
|
||||
做过分布式电商系统的一定很熟悉上面的架构图(目前比较流行的是微服务架构,但是如果你有分布式开发经验也是非常加分的!)。
|
||||
|
||||
**面试官:** 简单介绍一下你做的这个系统吧!
|
||||
|
||||
**我:** 我一本正经的对着我刚刚画的商城架构图开始了满嘴造火箭的讲起来:
|
||||
|
||||
> 本系统主要分为展示层、服务层和持久层这三层。表现层顾名思义主要就是为了用来展示,比如我们的后台管理系统的页面、商城首页的页面、搜索系统的页面等等,这一层都只是作为展示,并没有提供任何服务。
|
||||
>
|
||||
> 展示层和服务层一般是部署在不同的机器上来提高并发量和扩展性,那么展示层和服务层怎样才能交互呢?在本系统中我们使用 Dubbo 来进行服务治理。Dubbo 是一款高性能、轻量级的开源 Java RPC 框架。Dubbo 在本系统的主要作用就是提供远程 RPC 调用。在本系统中服务层的信息通过 Dubbo 注册给 ZooKeeper,表现层通过 Dubbo 去 ZooKeeper 中获取服务的相关信息。Zookeeper 的作用仅仅是存放提供服务的服务器的地址和一些服务的相关信息,实现 RPC 远程调用功能的还是 Dubbo。如果需要引用到某个服务的时候,我们只需要在配置文件中配置相关信息就可以在代码中直接使用了,就像调用本地方法一样。假如说某个服务的使用量增加时,我们只用为这单个服务增加服务器,而不需要为整个系统添加服务。
|
||||
>
|
||||
> 另外,本系统的数据库使用的是常用的 MySQL,并且用到了数据库中间件 MyCat。另外,本系统还用到 redis 内存数据库来作为缓存来提高系统的反应速度。假如用户第一次访问数据库中的某些数据,这个过程会比较慢,因为是从硬盘上读取的。将该用户访问的数据存在数缓存中,这样下一次再访问这些数据的时候就可以直接从缓存中获取了。操作缓存就是直接操作内存,所以速度相当快。
|
||||
>
|
||||
> 系统还用到了 Elasticsearch 来提供搜索功能。使用 Elasticsearch 我们可以非常方便的为我们的商城系统添加必备的搜索功能,并且使用 Elasticsearch 还能提供其它非常实用的功能,并且很容易扩展。
|
||||
|
||||
**面试官:** 我看你的系统里面还用到了消息队列,能说说为什么要用它吗?
|
||||
|
||||
**我:**
|
||||
|
||||
> 使用消息队列主要是为了:
|
||||
>
|
||||
> 1. 减少响应所需时间和削峰。
|
||||
> 2. 降低系统耦合性(解耦/提升系统可扩展性)。
|
||||
|
||||
**面试官:** 你这说的太简单了!能不能稍微详细一点,最好能画图给我解释一下。
|
||||
|
||||
**我:** 内心 os:"都 2019 年了,大部分面试者都能对消息队列的为系统带来的这两个好处倒背如流了,如果你想走的更远就要别别人懂的更深一点!"
|
||||
|
||||
> 当我们不使用消息队列的时候,所有的用户的请求会直接落到服务器,然后通过数据库或者缓存响应。假如在高并发的场景下,如果没有缓存或者数据库承受不了这么大的压力的话,就会造成响应速度缓慢,甚至造成数据库宕机。但是,在使用消息队列之后,用户的请求数据发送给了消息队列之后就可以立即返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库,不过要确保消息不被重复消费还要考虑到消息丢失问题。由于消息队列服务器处理速度快于数据库,因此响应速度得到大幅改善。
|
||||
>
|
||||
> 文字 is too 空洞,直接上图吧!下图展示了使用消息前后系统处理用户请求的对比(ps:我自己都被我画的这个图美到了,如果你也觉得这张图好看的话麻烦来个素质三连!)。
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧:
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。
|
||||
|
||||
**面试官:** 你觉得它有什么缺点吗?或者说怎么考虑用不用消息队列?
|
||||
|
||||
**我:** 内心 os: "面试官真鸡贼!这不是勾引我上钩么?还好我准备充分。"
|
||||
|
||||
> 我觉得可以从下面几个方面来说:
|
||||
>
|
||||
> 1. **系统可用性降低:** 系统可用性在某种程度上降低,为什么这样说呢?在加入MQ之前,你不用考虑消息丢失或者说MQ挂掉等等的情况,但是,引入MQ之后你就需要去考虑了!
|
||||
> 2. **系统复杂性提高:** 加入MQ之后,你需要保证消息没有被重复消费、处理消息丢失的情况、保证消息传递的顺序性等等问题!
|
||||
> 3. **一致性问题:** 我上面讲了消息队列可以实现异步,消息队列带来的异步确实可以提高系统响应速度。但是,万一消息的真正消费者并没有正确消费消息怎么办?这样就会导致数据不一致的情况了!
|
||||
|
||||
**面试官**:做项目的过程中遇到了什么问题吗?解决了吗?如果解决的话是如何解决的呢?
|
||||
|
||||
**我** : 内心 os: "做的过程中好像也没有遇到什么问题啊!怎么办?怎么办?突然想到可以说我在使用 Redis 过程中遇到的问题,毕竟我对 Redis 还算熟悉嘛,把面试官往这个方向吸引,准没错。"
|
||||
|
||||
> 我在使用 Redis 对常用数据进行缓冲的过程中出现了缓存穿透。然后,我通过谷歌搜索相关的解决方案来解决的。
|
||||
|
||||
**面试官:** 你还知道缓存穿透啊?不错啊!来说说什么是缓存穿透以及你最后的解决办法。
|
||||
|
||||
**我:** 我先来谈谈什么是缓存穿透吧!
|
||||
|
||||
> 缓存穿透说简单点就是大量请求的 key 根本不存在于缓存中,导致请求直接到了数据库上,根本没有经过缓存这一层。举个例子:某个黑客故意制造我们缓存中不存在的 key 发起大量请求,导致大量请求落到数据库。
|
||||
>
|
||||
> 总结一下就是:
|
||||
>
|
||||
> 1. 缓存层不命中。
|
||||
> 2. 存储层不命中,不将空结果写回缓存。
|
||||
> 3. 返回空结果给客户端。
|
||||
>
|
||||
> 一般 MySQL 默认的最大连接数在 150 左右,这个可以通过 `show variables like '%max_connections%';`命令来查看。最大连接数一个还只是一个指标,cpu,内存,磁盘,网络等无力条件都是其运行指标,这些指标都会限制其并发能力!所以,一般 3000 个并发请求就能打死大部分数据库了。
|
||||
|
||||
**面试官:** 小伙子不错啊!还准备问你:“为什么 3000 的并发能把支持最大连接数 4000 数据库压死?”想不到你自己就提前回答了!不错!
|
||||
|
||||
**我:** 别夸了!别夸了!我再来说说我知道的一些解决办法以及我最后采用的方案吧!您帮忙看看有没有问题。
|
||||
|
||||
> 最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。
|
||||
>
|
||||
> 解决方案:
|
||||
>
|
||||
> 1. **缓存无效 key** : 如果缓存和数据库都查不到某个 key 的数据就写一个到 redis 中去并设置过期时间,具体命令如下:`SET key value EX 10086`。这种方式可以解决请求的 key 变化不频繁的情况,如何黑客恶意攻击,每次构建的不同的请求 key,会导致 redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。
|
||||
>
|
||||
> 另外,这里多说一嘴,一般情况下我们是这样设计 key 的: `表名:列名:主键名:主键值`。
|
||||
>
|
||||
> 2. **布隆过滤器:** **布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在与海量数据中。我们需要的机会判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。**
|
||||
|
||||
**面试官:** 不错不错!你还知道布隆过滤器啊!来给我谈一谈。
|
||||
|
||||
**我:** 内心os:“如果你准备过海量数据处理的面试题,你一定对:“如何确定一个数字是否在于包含大量数字的数字集中(数字集很大,5亿以上!)?”这个题目很了解了!解决这道题目就要用到布隆过滤器。”
|
||||
|
||||
> 布隆过滤器在针对海量数据去重或者验证数据合法性的时候非常有用。**布隆过滤器的本质实际上是 “位(bit)数组”,也就是说每一个存入布隆过滤器的数据都只占一位。相比于我们平时常用的的 List、Map 、Set等数据结构,它占用空间更少并且效率更高,但是缺点是其返回的结果是概率性的,而不是非常准确的。**
|
||||
>
|
||||
> **当一个元素加入布隆过滤器中的时候,会进行如下操作:**
|
||||
>
|
||||
> 1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
|
||||
> 2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。
|
||||
>
|
||||
> **当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行如下操作:**
|
||||
>
|
||||
> 1. 对给定元素再次进行相同的哈希计算;
|
||||
> 2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||
>
|
||||
> 举个简单的例子:
|
||||
>
|
||||
>
|
||||
>
|
||||
> 
|
||||
>
|
||||
> 如图所示,当字符串存储要加入到布隆过滤器中时,该字符串首先由多个哈希函数生成不同的哈希值,然后在对应的位数组的下表的元素设置为 1(当位数组初始化时 ,所有位置均为0)。当第二次存储相同字符串时,因为先前的对应位置已设置为1,所以很容易知道此值已经存在(去重非常方便)。
|
||||
>
|
||||
> 如果我们需要判断某个字符串是否在布隆过滤器中时,只需要对给定字符串再次进行相同的哈希计算,得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。
|
||||
>
|
||||
> **不同的字符串可能哈希出来的位置相同,这种情况我们可以适当增加位数组大小或者调整我们的哈希函数。**
|
||||
>
|
||||
> 综上,我们可以得出:**布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。**
|
||||
|
||||
**面试官:** 看来你对布隆过滤器了解的还挺不错的嘛!那你快说说你最后是怎么利用它来解决缓存穿透的。
|
@ -164,8 +164,17 @@ JRE 是 Java运行时环境。它是运行已编译 Java 程序所需的所有
|
||||
|
||||
## 10. 重载和重写的区别
|
||||
|
||||
- **重载:** 发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同,发生在编译时。
|
||||
- **重写:** 发生在父子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类;如果父类方法访问修饰符为 private 则子类就不能重写该方法。
|
||||
#### 重载
|
||||
|
||||
发生在同一个类中,方法名必须相同,参数类型不同、个数不同、顺序不同,方法返回值和访问修饰符可以不同。
|
||||
|
||||
下面是《Java核心技术》对重载这个概念的介绍:
|
||||
|
||||

|
||||
|
||||
#### 重写
|
||||
|
||||
重写是子类对父类的允许访问的方法的实现过程进行重新编写,发生在子类中,方法名、参数列表必须相同,返回值范围小于等于父类,抛出的异常范围小于等于父类,访问修饰符范围大于等于父类。另外,如果父类方法访问修饰符为 private 则子类就不能重写该方法。**也就是说方法提供的行为改变,而方法的外貌并没有改变。**
|
||||
|
||||
## 11. Java 面向对象编程三大特性: 封装 继承 多态
|
||||
|
||||
|
@ -54,10 +54,9 @@ AQS使用一个int成员变量来表示同步状态,通过内置的FIFO队列
|
||||
private volatile int state;//共享变量,使用volatile修饰保证线程可见性
|
||||
```
|
||||
|
||||
状态信息通过protected类型的getState,setState,compareAndSetState进行操作
|
||||
状态信息通过protected类型的`getState`,`setState`,`compareAndSetState`进行操作
|
||||
|
||||
```java
|
||||
|
||||
//返回同步状态的当前值
|
||||
protected final int getState() {
|
||||
return state;
|
||||
@ -76,10 +75,125 @@ protected final boolean compareAndSetState(int expect, int update) {
|
||||
|
||||
**AQS定义两种资源共享方式**
|
||||
|
||||
- **Exclusive**(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
|
||||
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
|
||||
- 非公平锁:当线程要获取锁时,无视队列顺序直接去抢锁,谁抢到就是谁的
|
||||
- **Share**(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
|
||||
**1)Exclusive**(独占)
|
||||
|
||||
只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁,ReentrantLock 同时支持两种锁,下面以 ReentrantLock 对这两种锁的定义做介绍:
|
||||
|
||||
- 公平锁:按照线程在队列中的排队顺序,先到者先拿到锁
|
||||
- 非公平锁:当线程要获取锁时,先通过两次 CAS 操作去抢锁,如果没抢到,当前线程再加入到队列中等待唤醒。
|
||||
|
||||
> 说明:下面这部分关于 `ReentrantLock` 源代码内容节选自:https://www.javadoop.com/post/AbstractQueuedSynchronizer-2,这是一篇很不错文章,推荐阅读。
|
||||
|
||||
**下面来看 ReentrantLock 中相关的源代码:**
|
||||
|
||||
ReentrantLock 默认采用非公平锁,因为考虑获得更好的性能,通过 boolean 来决定是否用公平锁(传入 true 用公平锁)。
|
||||
|
||||
```java
|
||||
/** Synchronizer providing all implementation mechanics */
|
||||
private final Sync sync;
|
||||
public ReentrantLock() {
|
||||
// 默认非公平锁
|
||||
sync = new NonfairSync();
|
||||
}
|
||||
public ReentrantLock(boolean fair) {
|
||||
sync = fair ? new FairSync() : new NonfairSync();
|
||||
}
|
||||
```
|
||||
|
||||
ReentrantLock 中公平锁的 `lock` 方法
|
||||
|
||||
```java
|
||||
static final class FairSync extends Sync {
|
||||
final void lock() {
|
||||
acquire(1);
|
||||
}
|
||||
// AbstractQueuedSynchronizer.acquire(int arg)
|
||||
public final void acquire(int arg) {
|
||||
if (!tryAcquire(arg) &&
|
||||
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
|
||||
selfInterrupt();
|
||||
}
|
||||
protected final boolean tryAcquire(int acquires) {
|
||||
final Thread current = Thread.currentThread();
|
||||
int c = getState();
|
||||
if (c == 0) {
|
||||
// 1. 和非公平锁相比,这里多了一个判断:是否有线程在等待
|
||||
if (!hasQueuedPredecessors() &&
|
||||
compareAndSetState(0, acquires)) {
|
||||
setExclusiveOwnerThread(current);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (current == getExclusiveOwnerThread()) {
|
||||
int nextc = c + acquires;
|
||||
if (nextc < 0)
|
||||
throw new Error("Maximum lock count exceeded");
|
||||
setState(nextc);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
非公平锁的 lock 方法:
|
||||
|
||||
```java
|
||||
static final class NonfairSync extends Sync {
|
||||
final void lock() {
|
||||
// 2. 和公平锁相比,这里会直接先进行一次CAS,成功就返回了
|
||||
if (compareAndSetState(0, 1))
|
||||
setExclusiveOwnerThread(Thread.currentThread());
|
||||
else
|
||||
acquire(1);
|
||||
}
|
||||
// AbstractQueuedSynchronizer.acquire(int arg)
|
||||
public final void acquire(int arg) {
|
||||
if (!tryAcquire(arg) &&
|
||||
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
|
||||
selfInterrupt();
|
||||
}
|
||||
protected final boolean tryAcquire(int acquires) {
|
||||
return nonfairTryAcquire(acquires);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Performs non-fair tryLock. tryAcquire is implemented in
|
||||
* subclasses, but both need nonfair try for trylock method.
|
||||
*/
|
||||
final boolean nonfairTryAcquire(int acquires) {
|
||||
final Thread current = Thread.currentThread();
|
||||
int c = getState();
|
||||
if (c == 0) {
|
||||
// 这里没有对阻塞队列进行判断
|
||||
if (compareAndSetState(0, acquires)) {
|
||||
setExclusiveOwnerThread(current);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (current == getExclusiveOwnerThread()) {
|
||||
int nextc = c + acquires;
|
||||
if (nextc < 0) // overflow
|
||||
throw new Error("Maximum lock count exceeded");
|
||||
setState(nextc);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
总结:公平锁和非公平锁只有两处不同:
|
||||
|
||||
1. 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
|
||||
2. 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
|
||||
|
||||
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
|
||||
|
||||
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。
|
||||
|
||||
**2)Share**(共享)
|
||||
|
||||
多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatCh、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
|
||||
|
||||
ReentrantReadWriteLock 可以看成是组合式,因为ReentrantReadWriteLock也就是读写锁允许多个线程同时对某一资源进行读。
|
||||
|
||||
|
@ -509,7 +509,192 @@ public interface Callable<V> {
|
||||
- **`ThreadPoolExecutor.DiscardPolicy`:** 不处理新任务,直接丢弃掉。
|
||||
- **`ThreadPoolExecutor.DiscardOldestPolicy`:** 此策略将丢弃最早的未处理的任务请求。
|
||||
|
||||
举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了。)
|
||||
举个例子: Spring 通过 `ThreadPoolTaskExecutor` 或者我们直接通过 `ThreadPoolExecutor` 的构造函数创建线程池的时候,当我们不指定 `RejectedExecutionHandler` 饱和策略的话来配置线程池的时候默认使用的是 `ThreadPoolExecutor.AbortPolicy`。在默认情况下,`ThreadPoolExecutor` 将抛出 `RejectedExecutionException` 来拒绝新来的任务 ,这代表你将丢失对这个任务的处理。 对于可伸缩的应用程序,建议使用 `ThreadPoolExecutor.CallerRunsPolicy`。当最大池被填满时,此策略为我们提供可伸缩队列。(这个直接查看 `ThreadPoolExecutor` 的构造函数源码就可以看出,比较简单的原因,这里就不贴代码了)
|
||||
|
||||
### 4.6 一个简单的线程池Demo:`Runnable`+`ThreadPoolExecutor`
|
||||
|
||||
为了让大家更清楚上面的面试题中的一些概念,我写了一个简单的线程池 Demo。
|
||||
|
||||
首先创建一个 `Runnable` 接口的实现类(当然也可以是 `Callable` 接口,我们上面也说了两者的区别。)
|
||||
|
||||
`MyRunnable.java`
|
||||
|
||||
```java
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 这是一个简单的Runnable类,需要大约5秒钟来执行其任务。
|
||||
* @author shuang.kou
|
||||
*/
|
||||
public class MyRunnable implements Runnable {
|
||||
|
||||
private String command;
|
||||
|
||||
public MyRunnable(String s) {
|
||||
this.command = s;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
System.out.println(Thread.currentThread().getName() + " Start. Time = " + new Date());
|
||||
processCommand();
|
||||
System.out.println(Thread.currentThread().getName() + " End. Time = " + new Date());
|
||||
}
|
||||
|
||||
private void processCommand() {
|
||||
try {
|
||||
Thread.sleep(5000);
|
||||
} catch (InterruptedException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.command;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
编写测试程序,我们这里以阿里巴巴推荐的使用 `ThreadPoolExecutor` 构造函数自定义参数的方式来创建线程池。
|
||||
|
||||
`ThreadPoolExecutorDemo.java`
|
||||
|
||||
```java
|
||||
import java.util.concurrent.ArrayBlockingQueue;
|
||||
import java.util.concurrent.ThreadPoolExecutor;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class ThreadPoolExecutorDemo {
|
||||
|
||||
private static final int CORE_POOL_SIZE = 5;
|
||||
private static final int MAX_POOL_SIZE = 10;
|
||||
private static final int QUEUE_CAPACITY = 100;
|
||||
private static final Long KEEP_ALIVE_TIME = 1L;
|
||||
public static void main(String[] args) {
|
||||
|
||||
//使用阿里巴巴推荐的创建线程池的方式
|
||||
//通过ThreadPoolExecutor构造函数自定义参数创建
|
||||
ThreadPoolExecutor executor = new ThreadPoolExecutor(
|
||||
CORE_POOL_SIZE,
|
||||
MAX_POOL_SIZE,
|
||||
KEEP_ALIVE_TIME,
|
||||
TimeUnit.SECONDS,
|
||||
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
|
||||
new ThreadPoolExecutor.CallerRunsPolicy());
|
||||
|
||||
for (int i = 0; i < 10; i++) {
|
||||
//创建WorkerThread对象(WorkerThread类实现了Runnable 接口)
|
||||
Runnable worker = new MyRunnable("" + i);
|
||||
//执行Runnable
|
||||
executor.execute(worker);
|
||||
}
|
||||
//终止线程池
|
||||
executor.shutdown();
|
||||
while (!executor.isTerminated()) {
|
||||
}
|
||||
System.out.println("Finished all threads");
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
可以看到我们上面的代码指定了:
|
||||
|
||||
1. `corePoolSize`: 核心线程数为 5。
|
||||
2. `maximumPoolSize` :最大线程数 10
|
||||
3. `keepAliveTime` : 等待时间为 1L。
|
||||
4. `unit`: 等待时间的单位为 TimeUnit.SECONDS。
|
||||
5. `workQueue`:任务队列为 `ArrayBlockingQueue`,并且容量为 100;
|
||||
6. `handler`:饱和策略为 `CallerRunsPolicy`。
|
||||
|
||||
**Output:**
|
||||
|
||||
```
|
||||
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:44 CST 2019
|
||||
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:44 CST 2019
|
||||
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:44 CST 2019
|
||||
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:44 CST 2019
|
||||
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:44 CST 2019
|
||||
pool-1-thread-5 End. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-3 End. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-2 End. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-4 End. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-1 End. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-2 Start. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-1 Start. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-4 Start. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-3 Start. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-5 Start. Time = Tue Nov 12 20:59:49 CST 2019
|
||||
pool-1-thread-2 End. Time = Tue Nov 12 20:59:54 CST 2019
|
||||
pool-1-thread-3 End. Time = Tue Nov 12 20:59:54 CST 2019
|
||||
pool-1-thread-4 End. Time = Tue Nov 12 20:59:54 CST 2019
|
||||
pool-1-thread-5 End. Time = Tue Nov 12 20:59:54 CST 2019
|
||||
pool-1-thread-1 End. Time = Tue Nov 12 20:59:54 CST 2019
|
||||
|
||||
```
|
||||
|
||||
### 4.7 线程池原理分析
|
||||
|
||||
承接 4.6 节,我们通过代码输出结果可以看出:**线程池每次会同时执行 5 个任务,这 5 个任务执行完之后,剩余的 5 个任务才会被执行。** 大家可以先通过上面讲解的内容,分析一下到底是咋回事?(自己独立思考一会)
|
||||
|
||||
现在,我们就分析上面的输出内容来简单分析一下线程池原理。
|
||||
|
||||
**为了搞懂线程池的原理,我们需要首先分析一下 `execute`方法。**在 4.6 节中的 Demo 中我们使用 `executor.execute(worker)`来提交一个任务到线程池中去,这个方法非常重要,下面我们来看看它的源码:
|
||||
|
||||
```java
|
||||
// 存放线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount)
|
||||
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
|
||||
|
||||
private static int workerCountOf(int c) {
|
||||
return c & CAPACITY;
|
||||
}
|
||||
|
||||
private final BlockingQueue<Runnable> workQueue;
|
||||
|
||||
public void execute(Runnable command) {
|
||||
// 如果任务为null,则抛出异常。
|
||||
if (command == null)
|
||||
throw new NullPointerException();
|
||||
// ctl 中保存的线程池当前的一些状态信息
|
||||
int c = ctl.get();
|
||||
|
||||
// 下面会涉及到 3 步 操作
|
||||
// 1.首先判断当前线程池中之行的任务数量是否小于 corePoolSize
|
||||
// 如果小于的话,通过addWorker(command, true)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
|
||||
if (workerCountOf(c) < corePoolSize) {
|
||||
if (addWorker(command, true))
|
||||
return;
|
||||
c = ctl.get();
|
||||
}
|
||||
// 2.如果当前之行的任务数量大于等于 corePoolSize 的时候就会走到这里
|
||||
// 通过 isRunning 方法判断线程池状态,线程池处于 RUNNING 状态才会被并且队列可以加入任务,该任务才会被加入进去
|
||||
if (isRunning(c) && workQueue.offer(command)) {
|
||||
int recheck = ctl.get();
|
||||
// 再次获取线程池状态,如果线程池状态不是 RUNNING 状态就需要从任务队列中移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
|
||||
if (!isRunning(recheck) && remove(command))
|
||||
reject(command);
|
||||
// 如果当前线程池为空就新创建一个线程并执行。
|
||||
else if (workerCountOf(recheck) == 0)
|
||||
addWorker(null, false);
|
||||
}
|
||||
//3. 通过addWorker(command, false)新建一个线程,并将任务(command)添加到该线程中;然后,启动该线程从而执行任务。
|
||||
//如果addWorker(command, false)执行失败,则通过reject()执行相应的拒绝策略的内容。
|
||||
else if (!addWorker(command, false))
|
||||
reject(command);
|
||||
}
|
||||
```
|
||||
|
||||
通过下图可以更好的对上面这 3 步做一个展示,下图是我为了省事直接从网上找到,原地址不明。
|
||||
|
||||

|
||||
|
||||
现在,让我们在回到 4.6 节我们写的 Demo, 现在应该是不是很容易就可以搞懂它的原理了呢?
|
||||
|
||||
没搞懂的话,也没关系,可以看看我的分析:
|
||||
|
||||
> 我们在代码中模拟了 10 个任务,我们配置的核心线程数为 5 、等待队列容量为 100 ,所以每次只可能存在 5 个任务同时执行,剩下的 5 个任务会被放到等待队列中去。当前的 5 个任务之行完成后,才会之行剩下的 5 个任务。
|
||||
|
||||
## 5. Atomic 原子类
|
||||
|
||||
|
58
docs/java/jvm/GC调优参数.md
Normal file
58
docs/java/jvm/GC调优参数.md
Normal file
@ -0,0 +1,58 @@
|
||||
> 原文地址: https://juejin.im/post/5c94a123f265da610916081f。
|
||||
|
||||
## JVM 配置常用参数
|
||||
|
||||
1. 堆参数;
|
||||
2. 回收器参数;
|
||||
3. 项目中常用配置;
|
||||
4. 常用组合;
|
||||
|
||||
### 堆参数
|
||||
|
||||

|
||||
|
||||
### 回收器参数
|
||||
|
||||

|
||||
|
||||
如上表所示,目前**主要有串行、并行和并发三种**,对于大内存的应用而言,串行的性能太低,因此使用到的主要是并行和并发两种。并行和并发 GC 的策略通过 `UseParallelGC `和` UseConcMarkSweepGC` 来指定,还有一些细节的配置参数用来配置策略的执行方式。例如:`XX:ParallelGCThreads`, `XX:CMSInitiatingOccupancyFraction` 等。 通常:Young 区对象回收只可选择并行(耗时间),Old 区选择并发(耗 CPU)。
|
||||
|
||||
### 项目中常用配置
|
||||
|
||||
> 备注:在Java8中永久代的参数`-XX:PermSize` 和`-XX:MaxPermSize`已经失效。
|
||||
|
||||

|
||||
|
||||
### 常用组合
|
||||
|
||||

|
||||
|
||||
## 常用 GC 调优策略
|
||||
|
||||
1. GC 调优原则;
|
||||
2. GC 调优目的;
|
||||
3. GC 调优策略;
|
||||
|
||||
### GC 调优原则
|
||||
|
||||
在调优之前,我们需要记住下面的原则:
|
||||
|
||||
> 多数的 Java 应用不需要在服务器上进行 GC 优化; 多数导致 GC 问题的 Java 应用,都不是因为我们参数设置错误,而是代码问题; 在应用上线之前,先考虑将机器的 JVM 参数设置到最优(最适合); 减少创建对象的数量; 减少使用全局变量和大对象; GC 优化是到最后不得已才采用的手段; 在实际使用中,分析 GC 情况优化代码比优化 GC 参数要多得多。
|
||||
|
||||
### GC 调优目的
|
||||
|
||||
将转移到老年代的对象数量降低到最小; 减少 GC 的执行时间。
|
||||
|
||||
### GC 调优策略
|
||||
|
||||
**策略 1:**将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
|
||||
|
||||
**策略 2:**大对象进入老年代,虽然大部分情况下,将对象分配在新生代是合理的。但是对于大对象这种做法却值得商榷,大对象如果首次在新生代分配可能会出现空间不足导致很多年龄不够的小对象被分配的老年代,破坏新生代的对象结构,可能会出现频繁的 full gc。因此,对于大对象,可以设置直接进入老年代(当然短命的大对象对于垃圾回收老说简直就是噩梦)。`-XX:PretenureSizeThreshold` 可以设置直接进入老年代的对象大小。
|
||||
|
||||
**策略 3:**合理设置进入老年代对象的年龄,`-XX:MaxTenuringThreshold` 设置对象进入老年代的年龄大小,减少老年代的内存占用,降低 full gc 发生的频率。
|
||||
|
||||
**策略 4:**设置稳定的堆大小,堆大小设置有两个参数:`-Xms` 初始化堆大小,`-Xmx` 最大堆大小。
|
||||
|
||||
**策略5:**注意: 如果满足下面的指标,**则一般不需要进行 GC 优化:**
|
||||
|
||||
> MinorGC 执行时间不到50ms; Minor GC 执行不频繁,约10秒一次; Full GC 执行时间不到1s; Full GC 执行频率不算频繁,不低于10分钟1次。
|
@ -136,15 +136,54 @@ Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共
|
||||
|
||||
Java 堆是垃圾收集器管理的主要区域,因此也被称作**GC 堆(Garbage Collected Heap)**.从垃圾回收的角度,由于现在收集器基本都采用分代垃圾收集算法,所以 Java 堆还可以细分为:新生代和老年代:再细致一点有:Eden 空间、From Survivor、To Survivor 空间等。**进一步划分的目的是更好地回收内存,或者更快地分配内存。**
|
||||
|
||||
<div align="center">
|
||||
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-3堆结构.png" width="400px"/>
|
||||
</div>
|
||||
在 JDK 7 版本及JDK 7 版本之前,堆内存被通常被分为下面三部分:
|
||||
|
||||
上图所示的 eden 区、s0 区、s1 区都属于新生代,tentired 区属于老年代。大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。
|
||||
1. 新生代内存(Young Ceneration)
|
||||
2. 老生代(Old Generation)
|
||||
3. 永生代(Permanent Generation)
|
||||
|
||||

|
||||
|
||||
JDK 8 版本之后方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。
|
||||
|
||||

|
||||
|
||||
**上图所示的 Eden 区、两个 Survivor 区都属于新生代(为了区分,这两个 Survivor 区域按照顺序被命名为 from 和 to),中间一层属于老年代。**
|
||||
|
||||
大部分情况,对象都会首先在 Eden 区域分配,在一次新生代垃圾回收后,如果对象还存活,则会进入 s0 或者 s1,并且对象的年龄还会加 1(Eden 区->Survivor 区后对象的初始年龄变为 1),当它的年龄增加到一定程度(默认为 15 岁),就会被晋升到老年代中。对象晋升到老年代的年龄阈值,可以通过参数 `-XX:MaxTenuringThreshold` 来设置。
|
||||
|
||||
> 修正([issue552](https://github.com/Snailclimb/JavaGuide/issues/552)):“Hotspot遍历所有对象时,按照年龄从小到大对其所占用的大小进行累积,当累积的某个年龄大小超过了survivor区的一半时,取这个年龄和MaxTenuringThreshold中更小的一个值,作为新的晋升年龄阈值”。
|
||||
>
|
||||
> **动态年龄计算的代码如下**
|
||||
>
|
||||
> ```c++
|
||||
> uint ageTable::compute_tenuring_threshold(size_t survivor_capacity) {
|
||||
> //survivor_capacity是survivor空间的大小
|
||||
> size_t desired_survivor_size = (size_t)((((double) survivor_capacity)*TargetSurvivorRatio)/100);
|
||||
> size_t total = 0;
|
||||
> uint age = 1;
|
||||
> while (age < table_size) {
|
||||
> total += sizes[age];//sizes数组是每个年龄段对象大小
|
||||
> if (total > desired_survivor_size) break;
|
||||
> age++;
|
||||
> }
|
||||
> uint result = age < MaxTenuringThreshold ? age : MaxTenuringThreshold;
|
||||
> ...
|
||||
> }
|
||||
>
|
||||
> ```
|
||||
>
|
||||
>
|
||||
|
||||
堆这里最容易出现的就是 OutOfMemoryError 异常,并且出现这种异常之后的表现形式还会有几种,比如:
|
||||
|
||||
1. **`OutOfMemoryError: GC Overhead Limit Exceeded`** : 当JVM花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。
|
||||
2. **`java.lang.OutOfMemoryError: Java heap space`** :假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发`java.lang.OutOfMemoryError: Java heap space` 错误。(和本机物理内存无关,和你配置的对内存大小有关!)
|
||||
3. ......
|
||||
|
||||
### 2.5 方法区
|
||||
|
||||
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 Java 虚拟机规范把方法区描述为堆的一个逻辑部分,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。
|
||||
方法区与 Java 堆一样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。虽然 **Java 虚拟机规范把方法区描述为堆的一个逻辑部分**,但是它却有一个别名叫做 **Non-Heap(非堆)**,目的应该是与 Java 堆区分开来。
|
||||
|
||||
方法区也被称为永久代。很多人都会分不清方法区和永久代的关系,为此我也查阅了文献。
|
||||
|
||||
@ -176,7 +215,7 @@ JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1
|
||||
|
||||
#### 2.5.3 为什么要将永久代 (PermGen) 替换为元空间 (MetaSpace) 呢?
|
||||
|
||||
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 java.lang.OutOfMemoryError。你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
|
||||
整个永久代有一个 JVM 本身设置固定大小上限,无法进行调整,而元空间使用的是直接内存,受本机可用内存的限制,并且永远不会得到 `java.lang.OutOfMemoryError`。你可以使用 `-XX:MaxMetaspaceSize` 标志设置最大元空间大小,默认值为 unlimited,这意味着它只受系统内存的限制。`-XX:MetaspaceSize` 调整标志定义元空间的初始大小如果未指定此标志,则 Metaspace 将根据运行时的应用程序需求动态地重新调整大小。
|
||||
|
||||
当然这只是其中一个原因,还有很多底层的原因,这里就不提了。
|
||||
|
||||
|
137
docs/java/jvm/最重要的JVM参数指南.md
Normal file
137
docs/java/jvm/最重要的JVM参数指南.md
Normal file
@ -0,0 +1,137 @@
|
||||
> 本文由 JavaGuide 翻译自 https://www.baeldung.com/jvm-parameters,并对文章进行了大量的完善补充。翻译不易,如需转载请注明出处为: 作者: 。
|
||||
|
||||
## 1.概述
|
||||
|
||||
在本篇文章中,你将掌握最常用的 JVM 参数配置。如果对于下面提到了一些概念比如堆、
|
||||
|
||||
## 2.堆内存相关
|
||||
|
||||
>Java 虚拟机所管理的内存中最大的一块,Java 堆是所有线程共享的一块内存区域,在虚拟机启动时创建。**此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。**
|
||||
>
|
||||
|
||||
### 2.1.显式指定堆内存`–Xms`和`-Xmx`
|
||||
|
||||
与性能有关的最常见实践之一是根据应用程序要求初始化堆内存。如果我们需要指定最小和最大堆大小(推荐显示指定大小),以下参数可以帮助你实现:
|
||||
|
||||
```
|
||||
-Xms<heap size>[unit]
|
||||
-Xmx<heap size>[unit]
|
||||
```
|
||||
|
||||
- **heap size** 表示要初始化内存的具体大小。
|
||||
- **unit** 表示要初始化内存的单位。单位为***“ g”*** (GB) 、***“ m”***(MB)、***“ k”***(KB)。
|
||||
|
||||
举个栗子🌰,如果我们要为JVM分配最小2 GB和最大5 GB的堆内存大小,我们的参数应该这样来写:
|
||||
|
||||
```
|
||||
-Xms2G -Xmx5G
|
||||
```
|
||||
|
||||
### 2.2.显式新生代内存(Young Ceneration)
|
||||
|
||||
根据[Oracle官方文档](https://docs.oracle.com/javase/8/docs/technotes/guides/vm/gctuning/sizing.html),在堆总可用内存配置完成之后,第二大影响因素是为 `Young Generation` 在堆内存所占的比例。默认情况下,YG 的最小大小为 1310 *MB*,最大大小为*无限制*。
|
||||
|
||||
一共有两种指定 新生代内存(Young Ceneration)大小的方法:
|
||||
|
||||
**1.通过`-XX:NewSize`和`-XX:MaxNewSize`指定**
|
||||
|
||||
```
|
||||
-XX:NewSize=<young size>[unit]
|
||||
-XX:MaxNewSize=<young size>[unit]
|
||||
```
|
||||
|
||||
举个栗子🌰,如果我们要为 新生代分配 最小256m 的内存,最大 1024m的内存我们的参数应该这样来写:
|
||||
|
||||
```
|
||||
-XX:NewSize=256m
|
||||
-XX:MaxNewSize=1024m
|
||||
```
|
||||
|
||||
**2.通过`-Xmn<young size>[unit] `指定**
|
||||
|
||||
举个栗子🌰,如果我们要为 新生代分配256m的内存(NewSize与MaxNewSize设为一致),我们的参数应该这样来写:
|
||||
|
||||
```
|
||||
-Xmn256m
|
||||
```
|
||||
|
||||
GC 调优策略中很重要的一条经验总结是这样说的:
|
||||
|
||||
> 将新对象预留在新生代,由于 Full GC 的成本远高于 Minor GC,因此尽可能将对象分配在新生代是明智的做法,实际项目中根据 GC 日志分析新生代空间大小分配是否合理,适当通过“-Xmn”命令调节新生代大小,最大限度降低新对象直接进入老年代的情况。
|
||||
|
||||
另外,你还可以通过**`-XX:NewRatio=<int>`**来设置新生代和老年代内存的比值。
|
||||
|
||||
比如下面的参数就是设置新生代(包括Eden和两个Survivor区)与老年代的比值为1。也就是说:新生代与老年代所占比值为1:1,新生代占整个堆栈的 1/2。
|
||||
|
||||
```
|
||||
-XX:NewRatio=1
|
||||
```
|
||||
|
||||
### 2.3.显示指定永久代/元空间的大小
|
||||
|
||||
**从Java 8开始,如果我们没有指定 Metaspace 的大小,随着更多类的创建,虚拟机会耗尽所有可用的系统内存(永久代并不会出现这种情况)。**
|
||||
|
||||
JDK 1.8 之前永久代还没被彻底移除的时候通常通过下面这些参数来调节方法区大小
|
||||
|
||||
```java
|
||||
-XX:PermSize=N //方法区 (永久代) 初始大小
|
||||
-XX:MaxPermSize=N //方法区 (永久代) 最大大小,超过这个值将会抛出 OutOfMemoryError 异常:java.lang.OutOfMemoryError: PermGen
|
||||
```
|
||||
|
||||
相对而言,垃圾收集行为在这个区域是比较少出现的,但并非数据进入方法区后就“永久存在”了。
|
||||
|
||||
**JDK 1.8 的时候,方法区(HotSpot 的永久代)被彻底移除了(JDK1.7 就已经开始了),取而代之是元空间,元空间使用的是直接内存。**
|
||||
|
||||
下面是一些常用参数:
|
||||
|
||||
```java
|
||||
-XX:MetaspaceSize=N //设置 Metaspace 的初始(和最小大小)
|
||||
-XX:MaxMetaspaceSize=N //设置 Metaspace 的最大大小,如果不指定大小的话,随着更多类的创建,虚拟机会耗尽所有可用的系统内存。
|
||||
```
|
||||
|
||||
## 3.垃圾收集相关
|
||||
|
||||
### 3.1.垃圾回收器
|
||||
|
||||
为了提高应用程序的稳定性,选择正确的[垃圾收集](http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/gc01/index.html)算法至关重要。
|
||||
|
||||
JVM具有四种类型的*GC*实现:
|
||||
|
||||
- 串行垃圾收集器
|
||||
- 并行垃圾收集器
|
||||
- CMS垃圾收集器
|
||||
- G1垃圾收集器
|
||||
|
||||
可以使用以下参数声明这些实现:
|
||||
|
||||
```
|
||||
-XX:+UseSerialGC
|
||||
-XX:+UseParallelGC
|
||||
-XX:+USeParNewGC
|
||||
-XX:+UseG1GC
|
||||
```
|
||||
|
||||
有关*垃圾回收*实施的更多详细信息,请参见[此处](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/jvm/JVM%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6.md)。
|
||||
|
||||
### 3.2.GC记录
|
||||
|
||||
为了严格监控应用程序的运行状况,我们应该始终检查JVM的*垃圾回收*性能。最简单的方法是以人类可读的格式记录*GC*活动。
|
||||
|
||||
使用以下参数,我们可以记录*GC*活动:
|
||||
|
||||
```
|
||||
-XX:+UseGCLogFileRotation
|
||||
-XX:NumberOfGCLogFiles=< number of log files >
|
||||
-XX:GCLogFileSize=< file size >[ unit ]
|
||||
-Xloggc:/path/to/gc.log
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 推荐阅读
|
||||
|
||||
- [CMS GC 默认新生代是多大?](https://www.jianshu.com/p/832fc4d4cb53)
|
||||
- [CMS GC启动参数优化配置](https://www.cnblogs.com/hongdada/p/10277782.html)
|
||||
- [从实际案例聊聊Java应用的GC优化-美团技术团队](https://tech.meituan.com/2017/12/29/jvm-optimize.html)
|
||||
- [JVM性能调优详解](https://www.choupangxia.com/2019/11/11/interview-jvm-gc-08/) (2019-11-11)
|
||||
- [JVM参数使用手册](https://segmentfault.com/a/1190000010603813)
|
@ -1,5 +1,3 @@
|
||||
如遇链接无法打开,建议使用 https://github.com/Snailclimb/JavaGuide/blob/master/docs/questions/java-learning-path-and-methods.md 这个链接进行阅读。
|
||||
|
||||
到目前为止,我觉得不管是在公众号后台、知乎还是微信上面我被问的做多的就是:“大佬,有没有 Java 学习路线和方法”。所以,这部分单独就自己的学习经历来说点自己的看法。
|
||||
|
||||
## 前言
|
||||
|
@ -39,21 +39,22 @@
|
||||
|
||||
### (1) 通过异步处理提高系统性能(削峰、减少响应所需时间)
|
||||
|
||||

|
||||

|
||||
如上图,**在不使用消息队列服务器的时候,用户的请求数据直接写入数据库,在高并发的情况下数据库压力剧增,使得响应速度变慢。但是在使用消息队列之后,用户的请求数据发送给消息队列之后立即 返回,再由消息队列的消费者进程从消息队列中获取数据,异步写入数据库。由于消息队列服务器处理速度快于数据库(消息队列也比数据库有更好的伸缩性),因此响应速度得到大幅改善。**
|
||||
|
||||
通过以上分析我们可以得出**消息队列具有很好的削峰作用的功能**——即**通过异步处理,将短时间高并发产生的事务消息存储在消息队列中,从而削平高峰期的并发事务。** 举例:在电子商务一些秒杀、促销活动中,合理使用消息队列可以有效抵御促销活动刚开始大量订单涌入对系统的冲击。如下图所示:
|
||||

|
||||
|
||||

|
||||
|
||||
因为**用户请求数据写入消息队列之后就立即返回给用户了,但是请求数据在后续的业务校验、写数据库等操作中可能失败**。因此使用消息队列进行异步处理之后,需要**适当修改业务流程进行配合**,比如**用户在提交订单之后,订单数据写入消息队列,不能立即返回用户订单提交成功,需要在消息队列的订单消费者进程真正处理完该订单之后,甚至出库后,再通过电子邮件或短信通知用户订单成功**,以免交易纠纷。这就类似我们平时手机订火车票和电影票。
|
||||
|
||||
### (2) 降低系统耦合性
|
||||
|
||||
我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。
|
||||
使用消息队列还可以降低系统耦合性。我们知道如果模块之间不存在直接调用,那么新增模块或者修改模块就对其他模块影响较小,这样系统的可扩展性无疑更好一些。还是直接上图吧:
|
||||
|
||||
我们最常见的**事件驱动架构**类似生产者消费者模式,在大型网站中通常用利用消息队列实现事件驱动结构。如下图所示:
|
||||
|
||||

|
||||

|
||||
|
||||
生产者(客户端)发送消息到消息队列中去,接受者(服务端)处理消息,需要消费的系统直接去消息队列取消息进行消费即可而不需要和其他系统有耦合, 这显然也提高了系统的扩展性。
|
||||
|
||||
**消息队列使利用发布-订阅模式工作,消息发送者(生产者)发布消息,一个或多个消息接受者(消费者)订阅消息。** 从上图可以看到**消息发送者(生产者)和消息接受者(消费者)之间没有直接耦合**,消息发送者将消息发送至分布式消息队列即结束对消息的处理,消息接受者从分布式消息队列获取该消息后进行后续处理,并不需要知道该消息从何而来。**对新增业务,只要对该类消息感兴趣,即可订阅该消息,对原有系统和业务没有任何影响,从而实现网站业务的可扩展性设计**。
|
||||
|
||||
|
@ -1,202 +0,0 @@
|
||||
|
||||
|
||||
> 本文由JavaGuide整理翻译自(做了适当删减、修改和补充):
|
||||
>
|
||||
> - https://www.javaguides.net/2018/11/spring-boot-interview-questions-and-answers.html
|
||||
> - https://www.algrim.co/posts/101-spring-boot-interview-questions
|
||||
|
||||
### 1. 什么是 Spring Boot?
|
||||
|
||||
首先,重要的是要理解 Spring Boot 并不是一个框架,它是一种创建独立应用程序的更简单方法,只需要很少或没有配置(相比于 Spring 来说)。Spring Boot最好的特性之一是它利用现有的 Spring 项目和第三方项目来开发适合生产的应用程序。
|
||||
|
||||
### 2. 说出使用Spring Boot的主要优点
|
||||
|
||||
1. 开发基于 Spring 的应用程序很容易。
|
||||
2. Spring Boot 项目所需的开发或工程时间明显减少,通常会提高整体生产力。
|
||||
3. Spring Boot不需要编写大量样板代码、XML配置和注释。
|
||||
4. Spring引导应用程序可以很容易地与Spring生态系统集成,如Spring JDBC、Spring ORM、Spring Data、Spring Security等。
|
||||
5. Spring Boot遵循“固执己见的默认配置”,以减少开发工作(默认配置可以修改)。
|
||||
6. Spring Boot 应用程序提供嵌入式HTTP服务器,如Tomcat和Jetty,可以轻松地开发和测试web应用程序。(这点很赞!普通运行Java程序的方式就能运行基于Spring Boot web 项目,省事很多)
|
||||
7. Spring Boot提供命令行接口(CLI)工具,用于开发和测试Spring Boot应用程序,如Java或Groovy。
|
||||
8. Spring Boot提供了多种插件,可以使用内置工具(如Maven和Gradle)开发和测试Spring Boot应用程序。
|
||||
|
||||
### 3. 为什么需要Spring Boot?
|
||||
|
||||
Spring Framework旨在简化J2EE企业应用程序开发。Spring Boot Framework旨在简化Spring开发。
|
||||
|
||||

|
||||
|
||||
### 4. 什么是 Spring Boot Starters?
|
||||
|
||||
Spring Boot Starters 是一系列依赖关系的集合,因为它的存在,项目的依赖之间的关系对我们来说变的更加简单了。举个例子:在没有Spring Boot Starters之前,我们开发REST服务或Web应用程序时; 我们需要使用像Spring MVC,Tomcat和Jackson这样的库,这些依赖我们需要手动一个一个添加。但是,有了 Spring Boot Starters 我们只需要一个只需添加一个**spring-boot-starter-web**一个依赖就可以了,这个依赖包含的字依赖中包含了我们开发REST 服务需要的所有依赖。
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
### 5. 如何在Spring Boot应用程序中使用Jetty而不是Tomcat?
|
||||
|
||||
Spring Boot Web starter使用Tomcat作为默认的嵌入式servlet容器, 如果你想使用 Jetty 的话只需要修改pom.xml(Maven)或者build.gradle(Gradle)就可以了。
|
||||
|
||||
**Maven:**
|
||||
|
||||
```xml
|
||||
<!--从Web启动器依赖中排除Tomcat-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<!--添加Jetty依赖-->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-jetty</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
**Gradle:**
|
||||
|
||||
```groovy
|
||||
compile("org.springframework.boot:spring-boot-starter-web") {
|
||||
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-tomcat'
|
||||
}
|
||||
compile("org.springframework.boot:spring-boot-starter-jetty")
|
||||
```
|
||||
|
||||
说个题外话,从上面可以看出使用 Gradle 更加简洁明了,但是国内目前还是 Maven 使用的多一点,我个人觉得 Gradle 在很多方面都要好很多。
|
||||
|
||||
### 6. 介绍一下@SpringBootApplication注解
|
||||
|
||||
```java
|
||||
package org.springframework.boot.autoconfigure;
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@SpringBootConfiguration
|
||||
@EnableAutoConfiguration
|
||||
@ComponentScan(excludeFilters = {
|
||||
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
|
||||
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
|
||||
public @interface SpringBootApplication {
|
||||
......
|
||||
}
|
||||
```
|
||||
|
||||
```java
|
||||
package org.springframework.boot;
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Configuration
|
||||
public @interface SpringBootConfiguration {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
可以看出大概可以把 `@SpringBootApplication `看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan ` 注解的集合。根据 SpringBoot官网,这三个注解的作用分别是:
|
||||
|
||||
- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制
|
||||
- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的bean,注解默认会扫描该类所在的包下所有的类。
|
||||
- `@Configuration`:允许在上下文中注册额外的bean或导入其他配置类
|
||||
|
||||
### 7. (重要)Spring Boot 的自动配置是如何实现的?
|
||||
|
||||
这个是因为`@SpringBootApplication `注解的原因,在上一个问题中已经提到了这个注解。我们知道 `@SpringBootApplication `看作是 `@Configuration`、`@EnableAutoConfiguration`、`@ComponentScan ` 注解的集合。
|
||||
|
||||
- `@EnableAutoConfiguration`:启用 SpringBoot 的自动配置机制
|
||||
- `@ComponentScan`: 扫描被`@Component` (`@Service`,`@Controller`)注解的bean,注解默认会扫描该类所在的包下所有的类。
|
||||
- `@Configuration`:允许在上下文中注册额外的bean或导入其他配置类
|
||||
|
||||
`@EnableAutoConfiguration`是启动自动配置的关键,源码如下(建议自己打断点调试,走一遍基本的流程):
|
||||
|
||||
```java
|
||||
import java.lang.annotation.Documented;
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Inherited;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
@Target({ElementType.TYPE})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Documented
|
||||
@Inherited
|
||||
@AutoConfigurationPackage
|
||||
@Import({AutoConfigurationImportSelector.class})
|
||||
public @interface EnableAutoConfiguration {
|
||||
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
|
||||
|
||||
Class<?>[] exclude() default {};
|
||||
|
||||
String[] excludeName() default {};
|
||||
}
|
||||
```
|
||||
|
||||
`@EnableAutoConfiguration` 注解通过Spring 提供的 `@Import` 注解导入了`AutoConfigurationImportSelector`类(`@Import` 注解可以导入配置类或者Bean到当前类中)。
|
||||
|
||||
` ``AutoConfigurationImportSelector`类中`getCandidateConfigurations`方法会将所有自动配置类的信息以 List 的形式返回。这些配置信息会被 Spring 容器作 bean 来管理。
|
||||
|
||||
```java
|
||||
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
|
||||
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
|
||||
getBeanClassLoader());
|
||||
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
|
||||
+ "are using a custom packaging, make sure that file is correct.");
|
||||
return configurations;
|
||||
}
|
||||
```
|
||||
|
||||
自动配置信息有了,那么自动配置还差什么呢?
|
||||
|
||||
`@Conditional` 注解。`@ConditionalOnClass`(指定的类必须存在于类路径下),`@ConditionalOnBean`(容器中是否有指定的Bean)等等都是对`@Conditional`注解的扩展。拿 Spring Security 的自动配置举个例子:
|
||||
|
||||
`SecurityAutoConfiguration`中导入了`WebSecurityEnablerConfiguration`类,`WebSecurityEnablerConfiguration`源代码如下:
|
||||
|
||||
```java
|
||||
@Configuration
|
||||
@ConditionalOnBean(WebSecurityConfigurerAdapter.class)
|
||||
@ConditionalOnMissingBean(name = BeanIds.SPRING_SECURITY_FILTER_CHAIN)
|
||||
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
|
||||
@EnableWebSecurity
|
||||
public class WebSecurityEnablerConfiguration {
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
`WebSecurityEnablerConfiguration`类中使用`@ConditionalOnBean`指定了容器中必须还有`WebSecurityConfigurerAdapter` 类或其实现类。所以,一般情况下 Spring Security 配置类都会去实现 `WebSecurityConfigurerAdapter`,这样自动将配置就完成了。
|
||||
|
||||
更多内容可以参考这篇文章:https://sylvanassun.github.io/2018/01/08/2018-01-08-spring_boot_auto_configure/
|
||||
|
||||
### 8. Spring Boot支持哪些嵌入式web容器?
|
||||
|
||||
Spring Boot支持以下嵌入式servlet容器:
|
||||
|
||||
| **Name** | **Servlet Version** |
|
||||
| ------------ | ------------------- |
|
||||
| Tomcat 9.0 | 4.0 |
|
||||
| Jetty 9.4 | 3.1 |
|
||||
| Undertow 2.0 | 4.0 |
|
||||
|
||||
您还可以将Spring引导应用程序部署到任何Servlet 3.1+兼容的 Web 容器中。
|
||||
|
||||
这就是你为什么可以通过直接像运行 普通 Java 项目一样运行 SpringBoot 项目。这样的确省事了很多,方便了我们进行开发,降低了学习难度。
|
||||
|
||||
### 9. 什么是Spring Security ?
|
||||
|
||||
Spring Security 应该属于 Spring 全家桶中学习曲线比较陡峭的几个模块之一,下面我将从起源和定义这两个方面来简单介绍一下它。
|
||||
|
||||
- **起源:** Spring Security 实际上起源于 Acegi Security,这个框架能为基于 Spring 的企业应用提供强大而灵活安全访问控制解决方案,并且框架这个充分利用 Spring 的 IoC 和 AOP 功能,提供声明式安全访问控制的功能。后面,随着这个项目发展, Acegi Security 成为了Spring官方子项目,后来被命名为 “Spring Security”。
|
||||
- **定义:**Spring Security 是一个功能强大且高度可以定制的框架,侧重于为Java 应用程序提供身份验证和授权。——[官方介绍](https://spring.io/projects/spring-security)。
|
||||
|
||||
### 10. JPA 和 Hibernate 有哪些区别?JPA 可以支持动态 SQL 吗?
|
||||
|
@ -13,6 +13,7 @@
|
||||
- [Git 使用快速入门](#git-使用快速入门)
|
||||
- [获取 Git 仓库](#获取-git-仓库)
|
||||
- [记录每次更新到仓库](#记录每次更新到仓库)
|
||||
- [一个好的 Git 提交消息](#一个好的-Git-提交消息)
|
||||
- [推送改动到远程仓库](#推送改动到远程仓库)
|
||||
- [远程仓库的移除与重命名](#远程仓库的移除与重命名)
|
||||
- [查看提交历史](#查看提交历史)
|
||||
@ -143,6 +144,17 @@ Git 有三种状态,你的文件可能处于其中之一:
|
||||
6. **移除文件** :`git rm filename` (从暂存区域移除,然后提交。)
|
||||
7. **对文件重命名** :`git mv README.md README`(这个命令相当于`mv README.md README`、`git rm README.md`、`git add README` 这三条命令的集合)
|
||||
|
||||
### 一个好的 Git 提交消息
|
||||
一个好的 Git 提交消息如下:
|
||||
|
||||
标题行:用这一行来描述和解释你的这次提交
|
||||
|
||||
主体部分可以是很少的几行,来加入更多的细节来解释提交,最好是能给出一些相关的背景或者解释这个提交能修复和解决什么问题。
|
||||
|
||||
主体部分当然也可以有几段,但是一定要注意换行和句子不要太长。因为这样在使用 "git log" 的时候会有缩进比较好看。
|
||||
|
||||
提交的标题行描述应该尽量的清晰和尽量的一句话概括。这样就方便相关的 Git 日志查看工具显示和其他人的阅读。
|
||||
|
||||
### 推送改动到远程仓库
|
||||
|
||||
- 如果你还没有克隆现有仓库,并欲将你的仓库连接到某个远程服务器,你可以使用如下命令添加:·`git remote add origin <server>` ,比如我们要让本地的一个仓库和 Github 上创建的一个仓库关联可以这样`git remote add origin https://github.com/Snailclimb/test.git`
|
||||
@ -257,3 +269,4 @@ git push origin
|
||||
- [猴子都能懂得Git入门](https://backlog.com/git-tutorial/cn/intro/intro1_1.html)
|
||||
- https://git-scm.com/book/en/v2
|
||||
- [Generating a new SSH key and adding it to the ssh-agent](https://help.github.com/en/articles/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent)
|
||||
- [一个好的 Git 提交消息,出自 Linus 之手](https://github.com/torvalds/subsurface-for-dirk/blob/a48494d2fbed58c751e9b7e8fbff88582f9b2d02/README#L88)
|
||||
|
94
docs/tools/github/github-star-ranking.md
Normal file
94
docs/tools/github/github-star-ranking.md
Normal file
@ -0,0 +1,94 @@
|
||||
## 题外话
|
||||
|
||||
先来点题外话吧!如果想看正文的话可以直接看滑到下面正文。
|
||||
|
||||
来三亚旅行也有几天了,总体感觉很不错,后天就要返航回家了。偶尔出来散散心真的挺不错,放松一下自己的心情,感受一下大自然。个人感觉冬天的时候来三亚度假还是很不错的选择,但是不要 1 月份的时候过来(差不多就过年那会儿),那时候属于大旺季,各种东西特别是住宿都贵很多。而且,那时候的机票也很贵。很多人觉得来三亚会花很多钱,实际上你不是在大旺季来的话,花不了太多钱。我和我女朋友在这边玩的几天住的酒店都还不错(干净最重要!),价格差不多都在 200元左右,有一天去西岛和天涯海角那边住的全海景房间也才要 200多,不过过年那会儿可能会达到 1000+。
|
||||
|
||||
现在是晚上 7 点多,刚从外面玩耍完回来。女朋友拿着我的手机拼着图片,我一个只能玩玩电脑。这篇文章很早就想写了,毕竟不费什么事,所以逞着晚上有空写一下。
|
||||
|
||||
如果有读者想看去三亚拍的美照包括我和我女朋友的合照,可以在评论区扣个 “想看”,我可以整篇推文分享一下。
|
||||
|
||||
## 正文
|
||||
|
||||
> 下面的 10 个项目还是很推荐的!JS 的项目占比挺大,其他基本都是文档/学习类型的仓库。
|
||||
|
||||
说明:数据统计于 2019-11-27。
|
||||
|
||||
### 1. freeCodeCamp
|
||||
|
||||
- **Github地址**:[https://github.com/freeCodeCamp/freeCodeCamp](https://github.com/freeCodeCamp/freeCodeCamp)
|
||||
- **star**: 307 k
|
||||
- **介绍**: 开放源码代码库和课程。与数百万人一起免费学习编程。网站:[https://www.freeCodeCamp.org](https://www.freecodecamp.org/) (一个友好的社区,您可以在这里免费学习编码。它由捐助者支持、非营利组织运营,以帮助数百万忙碌的成年人学习编程技术。这个社区已经帮助10,000多人获得了第一份开发人员的工作。这里的全栈Web开发课程是完全免费的,并且可以自行调整进度。这里还有数以千计的交互式编码挑战,可帮助您扩展技能。)
|
||||
|
||||
比如我想学习 ES6 的语法,学习界面是下面这样的,你可以很方便地边练习边学习:
|
||||
|
||||

|
||||
|
||||
### 2. 996.ICU
|
||||
|
||||
- **Github地址**:[https://github.com/996icu/996.ICU](https://github.com/996icu/996.ICU)
|
||||
- **star**: 248 k
|
||||
- **介绍**: `996.ICU` 是指“工作 996, 生病 ICU” 。这是中国程序员之间的一种自嘲说法,意思是如果按照 996 的模式工作,那以后就得进 ICU 了。这个项目最早是某个中国程序员发起的,然后就火遍全网,甚至火到了全世界很多其他国家,其网站被翻译成了多种语言。网站地址:[https://996.icu](https://996.icu/)。
|
||||
|
||||

|
||||
|
||||
### 3. vue
|
||||
|
||||
- **Github地址**:[https://github.com/vuejs/vue](https://github.com/vuejs/vue)
|
||||
- **star**: 153 k
|
||||
- **介绍**: 尤大的前端框架。国人用的最多(容易上手,文档比较丰富),所以 Star 数量比较多还是有道理的。Vue (读音 /vjuː/,类似于 **view**) 是一套用于构建用户界面的**渐进式框架**。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与[现代化的工具链](https://cn.vuejs.org/v2/guide/single-file-components.html)以及各种[支持类库](https://github.com/vuejs/awesome-vue#libraries--plugins)结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。
|
||||
|
||||
### 4. React
|
||||
|
||||
- **Github地址**:[https://gitstar-ranking.com/facebook/react](https://gitstar-ranking.com/facebook/react)
|
||||
- **star**: 140 k
|
||||
- **介绍**: Facebook 开源的,大公司有保障。用于构建用户界面的声明式、基于组件开发,高效且灵活的JavaScript框架。我司大部分项目的前端都是 React ,我自己也用过一段时间,感觉还不错,但是也有一些小坑。
|
||||
|
||||
### 5. tensorflow
|
||||
|
||||
- **Github地址**:[https://github.com/tensorflow/tensorflow](https://github.com/tensorflow/tensorflow)
|
||||
- **star**: 138 k
|
||||
- **介绍**: 适用于所有人的开源机器学习框架。[TensorFlow](https://www.tensorflow.org/)是用于机器学习的端到端开源平台。TensorFlow最初是由Google机器智能研究组织内Google Brain团队的研究人员和工程师开发的,用于进行机器学习和深度神经网络研究。该系统具有足够的通用性,也可以适用于多种其他领域。TensorFlow提供了稳定的[Python](https://www.tensorflow.org/api_docs/python) 和[C ++](https://www.tensorflow.org/api_docs/cc) API,以及[其他语言的](https://www.tensorflow.org/api_docs)非保证的向后兼容API 。
|
||||
|
||||
### 6. bootstrap
|
||||
|
||||
- **Github地址**:[https://github.com/twbs/bootstrap](https://github.com/twbs/bootstrap)
|
||||
- **star**: 137 k
|
||||
- **介绍**: 相信初学前端的时候,大家一定或多或少地接触过这个框架。官网说它是最受欢迎的HTML,CSS和JavaScript框架,用于在网络上开发响应式,移动优先项目。
|
||||
|
||||
### 7. free-programming-books
|
||||
|
||||
- **Github地址**:[https://github.com/EbookFoundation/free-programming-books](https://github.com/EbookFoundation/free-programming-books)
|
||||
- **star**: 132 k
|
||||
- **介绍**: 免费提供的编程书籍。我自己没太搞懂为啥这个项目 Star 数这么多,知道的麻烦评论区吱一声。
|
||||
|
||||
### 8. Awesome
|
||||
|
||||
- **Github地址** : [https://github.com/sindresorhus/awesome](https://github.com/sindresorhus/awesome)
|
||||
- **star**: 120 k
|
||||
- **介绍**: github 上很多的各种 Awesome 系列合集。
|
||||
|
||||
下面是这个开源仓库的目录,可以看出其涵盖了很多方面的内容。
|
||||
|
||||
<img src="https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-11/awsome-contents.jpg" style="zoom:50%;" />
|
||||
|
||||
举个例子,这个仓库里面就有两个让你的电脑更好用的开源仓库,Mac 和 Windows都有:
|
||||
|
||||
- Awesome Mac:https://github.com/jaywcjlove/awesome-mac/blob/master/README-zh.m
|
||||
- Awsome Windows: https://github.com/Awesome-Windows/Awesome/blob/master/README-cn.md
|
||||
|
||||
### 9. You-Dont-Know-JS
|
||||
|
||||
- **Github地址**:[https://github.com/getify/You-Dont-Know-JS](https://github.com/getify/You-Dont-Know-JS)
|
||||
- **star**: 112 k
|
||||
- **介绍**: 您还不认识JS(书籍系列)-第二版
|
||||
|
||||
### 10. oh-my-zsh
|
||||
|
||||
- **Github地址**:[https://github.com/ohmyzsh/ohmyzsh](https://github.com/ohmyzsh/ohmyzsh)
|
||||
- **star**: 99.4 k
|
||||
- **介绍**: 一个令人愉快的社区驱动的框架(拥有近1500个贡献者),用于管理zsh配置。包括200多个可选插件(rails, git, OSX, hub, capistrano, brew, ant, php, python等),140多个主题,可为您的早晨增光添彩,以及一个自动更新工具,可让您轻松保持与来自社区的最新更新……
|
||||
|
||||
下面就是 oh-my-zsh 提供的一个花里胡哨的主题:
|
||||
|
||||

|
Loading…
x
Reference in New Issue
Block a user