Compare commits

..

7 Commits

Author SHA1 Message Date
土豆兄弟 4c744ab351 feat(master): nocos + admin
nocos 基本配置
admin 基础配置
2 months ago
土豆兄弟 3e350dc766 feat(master): OpenFeign相关的学习
OpenFeign 的基本认知和使用
2 months ago
土豆兄弟 6b69d06381 feat(master): restTemplate
更新 restTemplate 相关的代码
2 months ago
土豆兄弟 2b77ab5f8f feat(master): mongo分片创建
完成相关学习
2 months ago
土豆兄弟 0373debde9 feat(master): mongo分片创建
完成相关学习
2 months ago
土豆兄弟 8a665b7d57 feat(master): 添加 MongoDB 文档结构设计
完成存储结构的相关学习
3 months ago
土豆兄弟 c527cba4cf feat(master): 添加 MongoDB 文档结构设计
添加MongoDB 存储结构的设计的相关内容
3 months ago

@ -530,7 +530,7 @@ document.querySelector("button#save").onclick = function (){
picture.className = filtersSelect.value;
...
```
- 没错,只需要这样简单的一行代码,你就可以将不同的滤镜应用于获取的照片上
- 只需要这样简单的一行代码,你就可以将不同的滤镜应用于获取的照片上

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

@ -0,0 +1,251 @@
# 编译原理
## 0. 目录
- 编译原理之美
- 编译原理实战
### 编译原理之美
### 编译原理实战
#### 1. 编译的全过程都悄悄做了哪些事情?
- 编译,其实就是把源代码变成目标代码的过程。
- 编译器翻译源代码,也需要经过多个处理步骤
- ![编译器翻译步骤](pic/编译器翻译步骤.png)
- 词法分析Lexical Analysis
- 第一步,就是要像读文章一样,先把里面的单词和标点符号识别出来。程序里面的单词叫做 Token它可以分成关键字、标识符、字面量、操作符号等多个种类。**把字符串转换
为 Token 的这个过程,就叫做词法分析**。
- ![词法分析](pic/词法分析.png)
- 语法分析Syntactic Analysis
- 下一步,**我们需要让编译器像理解自然语言一样,理解它的语法结构**。这就是第二步,**语法分析**。
- 给一个句子划分语法结构, 比如说:“我喜欢又聪明又勇敢的你”,
- ![语法结构](pic/语法结构.png)
- 在编译器里,语法分析阶段也会把 Token 串,转换成**一个体现语法规则的、树状的数据结构**,这个数据结构叫做**抽象语法树ASTAbstract Syntax Tree**。我们前面的示例
程序转换为 AST 以后,大概是下面这个样子:
- ![语法结构AST](pic/语法结构AST.png)
- 这样的一棵 AST 反映了示例程序的语法结构。比如说我们知道一个函数的定义包括了返回值类型、函数名称、0 到多个参数和函数体等。这棵抽象语法树的顶部就是一个函数节
点,它包含了四个子节点,刚好反映了函数的语法。
- 再进一步,函数体里面还可以包含多个语句,如变量声明语句、返回语句,它们构成了函数体的子节点。然后,每个语句又可以进一步分解,直到叶子节点,就不可再分解了。而叶子
节点,就是词法分析阶段生成的 Token图中带边框的节点。对这棵 AST 做深度优先的遍历,你就能依次得到原来的 Token。
- 语义分析Semantic Analysis
- 很小的时候就能够给大人读报纸。因为他们懂得发音规则,能念出单词来(词法分析),也基本理解语法结构(他们不见得懂主谓宾这样的术语,但是凭经验已
经知道句子有不同的组成部分),可以读得抑扬顿挫(语法分析),但是他们不懂报纸里说的是什么,也就是不懂语义。这就是编译器解读源代码的下一步工作,语义分析。
- 那么,怎样理解源代码的语义呢?
- 参考对应的语言规范标准,例如 ECMAScript也就是 JavaScript标准
- 深度遍历 AST并执行每个节点附带的语义规则我们也就能够理解一个句子的含义、一个函数的含义乃至整段源代码的含义。
- 这也就是说AST 加上这些语义规则,就能完整地反映源代码的含义。这个时候,你就可以做很多事情了。比如,你可以深度优先地遍历 AST并且一边遍历一边执行语法规
add 节点:把两个子节点的值相加,作为自己的值;变量节点(在等号右边的话):取出变量的值;数字字面量节点:返回这个字面量代表的值。则。那么这个遍历过程,就是解释执行代码的过程。你相当于写了一个基于 AST 的解释器。
- 这里的语义分析是要解决什么问题呢?
- 而把“a+3”中的 a跟正确的变量定义关联的过程就叫做引用消解Resolve
- 变量有点像自然语言里的代词,比如说,“我喜欢又聪明又勇敢的你”中的“我”和“你”,指的是谁呢?如果这句话前面有两句话,“我是春娇,你是志明”,那
这句话的意思就比较清楚了,是“春娇喜欢又聪明又勇敢的志明”。
- 引用消解需要在上下文中查找某个标识符的定义与引用的关系,所以我们现在可以回答前面的问题了,**语义分析的重要特点,就是做上下文相关的分析**。
- 在语义分析阶段编译器还会识别出数据的类型。比如在计算“a+3”的时候我们必须知道 a 和 3 的类型是什么。因为**即使同样是加法运算,对于整型和浮点型数据,其计算方
法也是不一样的**。
- 语义分析获得的一些信息(引用消解信息、类型信息等),会附加到 AST 上。**这样的 AST叫做带有标注信息的 AST**Annotated AST/Decorated AST用于更全面地反映源代码的含义。
- ![带有标注的AST](pic/带标注的AST.png)
- 在语义分析阶段,编译器还要做很多语义方面的检查工作。
- 总结起来,在语义分析阶段,编译器会做**`语义理解和语义检查`**这两方面的工作。词法分析、语法分析和语义分析,统称编译器的**前端**,它完成的是对源代码的理解工作。
- 做完语义分析以后,接下来编译器要做什么呢?
- 生成目标代码的工作,叫做后端工作。
- 熟练掌握汇编代码对于初学者来说会有一定的难度。但更麻烦的是对于不同架构的CPU还需要生成不同的汇编代码这使得我们的工作量更大。所以我们通常要在这个
时候增加一个环节先翻译成中间代码Intermediate RepresentationIR
- 中间代码Intermediate Representation
- 我们倾向于使用 IR 有两个原因。
- 第一个原因,是很多解释型的语言,可以直接执行 IR比如 Python 和 Java。这样的话 编译器生成 IR 以后就完成任务了,没有必要生成最终的汇编代码。
- 第二个原因更加重要。我们生成代码的时候,需要做大量的优化工作。而很多优化工作没有必要基于汇编代码来做,而是可以基于 IR用统一的算法来完成。
- 优化Optimization
- 那为什么需要做优化工作呢?这里又有两大类的原因。
- 第一个原因,是**源语言和目标语言有差异**。源语言的设计目的是方便人类表达和理解,而目标语言是为了让机器理解。在源语言里很复杂的一件事情,到了目标语言里,有可能很简单
地就表达出来了。
- 以 Java 为例,我们经常为某个类定义属性,然后再定义获取或修改这些属性的方法:
- 如果你在程序里用“person.getName()”来获取 Person 的 name 字段,会是一个开销很大的操作,因为它涉及函数调用。在汇编代码里,实现一次函数调用会做下面这一大堆事情:
- ![person.getName()汇编逻辑](pic/person.getName()汇编逻辑.png)
- 你看了这段伪代码,就会发现,简单的一个 getName() 方法,开销真的很大。保存和恢复寄存器的值、保存和读取返回地址,等等,这些操作会涉及好几次读写内存的操作,要花费大量的时钟周期。但这个逻辑其实是可以简化的。
- 怎样简化呢?就是**跳过方法的调用**。我们直接根据对象的地址计算出 name 属性的地址,然后直接从内存取值就行。这样优化之后,性能会提高好多倍。
- 这种优化方法就叫做**内联inlining**,也就是**把原来程序中的函数调用去掉,把函数内的逻辑直接嵌入函数调用者的代码中**。
- 第二个需要优化工作的原因,是程序员写的代码不是最优的,而编译器会帮你做纠正。
- ![编译器代码优化](pic/编译器代码优化.png)
- 而采用中间代码来编写优化算法的好处,**是可以把大部分的优化算法,写成与具体 CPU 架构无关的形式,从而大大降低编译器适配不同 CPU 的工作量**。并且,如果**采用像 LLVM 这
样的工具,我们还可以让多种语言的前端生成相同的中间代码,这样就可以复用中端和后端的程序了**。
- 生成目标代码
- 编译器最后一个阶段的工作,是生成高效率的目标代码,也就是汇编代码。这个阶段,编译器也有几个重要的工作。
- 第一,是要选择合适的指令,生成性能最高的代码。
- 第二,是要优化寄存器的分配,让频繁访问的变量(比如循环变量)放到寄存器里,因为访问寄存器要比访问内存快 100 倍左右。
- 第三,是在不改变运行结果的情况下,对指令做重新排序,从而充分运用 CPU 内部的多个功能部件的并行计算能力。
- 目标代码生成以后,整个编译过程就完成了。
#### 2. 词法分析:用两种方式构造有限自动机
- 词法分析的原理
- 你已经很熟悉词法分析的任务了:输入的是字符串,输出的是 Token 串。所以,词法分析器在英文中一般叫做 Tokenizer。
- ![词法分析的原理](pic/词法分析的原理图.png)
- 但具体如何实现呢?这里要有一个计算模型,叫做**有限自动机**Finite-state AutomatonFSA或者叫做有限状态自动机Finite-state MachineFSM
- 有限自动机这个名字,听上去可能比较陌生。但大多数程序员,肯定都接触过另一个词:**状态机**。假设你要做一个电商系统,那么订单状态的迁移,就是一个状态机。
- ![状态机的例子](pic/状态机的例子.png)
- 有限自动机就是这样的状态机,它的状态数量是有限的。当它收到一个新字符的时候,会导致状态的迁移。比如说,下面的这个状态机能够区分标识符和数字字面量。
- ![有限自动机](pic/有限自动机.png)
- 在这样一个状态机里,我用单线圆圈表示临时状态,双线圆圈表示接受状态。接受状态就是一个合格的 Token比如图 3 中的状态 1数字字面量和状态 2标识符。当这两个
状态遇到空白字符的时候,就可以记下一个 Token并回到初始态状态 0开始识别其他 Token。
- 可以看出,**词法分析的过程,其实就是对一个字符串进行模式匹配的过程**。说起字符串的模式匹配,你能想到什么工具吗?对的,**正则表达式工具**。
- 同样地,正则表达式也可以用来描述词法规则。这种描述方法,我们叫做**正则文法Regular Grammar**。比如,数字字面量和标识符的正则文法描述是这样的:
```js
IntLiteral : [0-9]+; //至少有一个数字
Id : [A-Za-z][A-Za-z0-9]*; //以字母开头,后面可以是字符或数字
```
- 与普通的正则表达式工具不同的是,词法分析器要用到很多个词法规则,每个词法规则都采用 **“Token 类型: 正则表达式”** 这样一种格式,用于匹配一种 Token。
- 然而当我们采用了多条词法规则的时候有可能会出现词法规则冲突的情况。比如说int 关键字其实也是符合标识符的词法规则的。
```js
Int : int; //int关键字
For : for; //for关键字
Id : [A-Za-z][A-Za-z0-9]*; //以字母开头,后面可以是字符或数字
```
- 所以,词法规则里面要有优先级,比如排在前面的词法规则优先级更高。这样的话,我们就能够设计出区分 int 关键字和标识符的有限自动机了,可以画成下面的样子。其中,状态
1、2 和 3 都是标识符,而状态 4 则是 int 关键字。
- ![识别int关键字和标识符的有限自动机](pic/识别int关键字和标识符的有限自动机.png)
- 从正则表达式生成有限自动机
- 我们能否只写出词法规则,就自动生成相对应的有限自动机呢?
- 当然是可以的,实际上,正则表达式工具就是这么做的。此外,词法分析器生成工具 lex及 GNU 版本的 flex也能够基于规则自动生成词法分析器。
- 它的具体实现思路是这样的:把一个正则表达式翻译成 NFA然后把 NFA 转换成 DFA。
- 先说说 **DFA**它是“Deterministic Finite Automaton”的缩写即**确定的有限自动机**。它的特点是:该状态机在任何一个状态,基于输入的字符,都能做一个确定的状态转换。前
面例子中的有限自动机,都属于 DFA。
- 再说说 **NFA**它是“Nondeterministic Finite Automaton”的缩写即**不确定的有限自动机**。它的特点是:该状态机中存在某些状态,针对某些输入,不能做一个确定的转换。
- 这又细分成两种情况:
- 对于一个输入,它有两个状态可以转换。
- 存在ε转换的情况也就是没有任何字符输入的情况下NFA 也可以从一个状态迁移到另一个状态。
- 比如“a[a-zA-Z0-9]*bc”这个正则表达式对字符串的要求是以 a 开头,以 bc 结尾a 和 bc 之间可以有任意多个字母或数字。可以看到,在图 5 中,状态 1 的节点输入 b
时,这个状态是有两条路径可以选择的:一条是迁移到状态 2另一条是仍然保持在状态 1。所以这个有限自动机是一个 NFA。
- ![NFA的例子](pic/NFA的例子.png)
- 这个 NFA 还有引入ε转换的画法,如图 6 所示,它跟图 5 的画法是等价的。实际上,图 6 表示的 NFA 可以用我们下面马上要讲到的算法,通过正则表达式自动生成出来。
- ![另一个NFA的例子](pic/另一个NFA的例子.png)
- 需要注意的是,无论是 NFA 还是 DFA都等价于正则表达式。也就是说**所有的正则表达式都能转换成 NFA 或 DFA而所有的 NFA 或 DFA也都能转换成正则表达式**。
- 理解了 NFA 和 DFA 以后,接下来我再大致说一下算法。
- 首先,一个正则表达式可以机械地翻译成一个 NFA。它的翻译方法如下
- 识别字符 i 的 NFA。
- 当接受字符 i 的时候,引发一个转换,状态图的边上标注 i。其中第一个状态iinitial是初始状态第二个状态 (ffinal) 是接受状态。
- ![识别i的DFA](pic/识别i的DFA.png)
- 转换“s|t”这样的正则表达式。
- 它的意思是,或者 s或者 t二者选一。s 和 t 本身是两个子表达式,我们可以增加两个新的状态:开始状态和接受状态。然后,用ε转换分别连接代表 s 和 t 的子图。它的含义也
比较直观,要么走上面这条路径,那就是 s要么走下面这条路径那就是 t
- ![识别s|t的NFA](pic/识别st的NFA.png)
- 转换“st”这样的正则表达式
- s 之后接着出现 t转换规则是把 s 的开始状态变成 st 整体的开始状态,把 t 的结束状态变成 st 整体的结束状态,并且把 s 的结束状态和 t 的开始状态合二为一。这样就把两个子图
衔接了起来,走完 s 接着走 t。
- ![识别st的NFA1](pic/识别st的NFA1.png)
- 对于“?”“*”和“+”这样的符号,它们的意思是可以重复 0 次、0 到多次、1 到多次转换时要增加额外的状态和边。以“s*”为例,我们可以做下面的转换:
- ![识别s星号的NFA](pic/识别s星号的NFA.png)
- 你能看出,它可以从 i 直接到 f也就是对 s 匹配 0 次,也可以在 s 的起止节点上循环多次。
- 如果是“s+”,那就没有办法跳过 ss 至少要经过一次:\
- ![识别s加好的NFA](pic/识别s加好的NFA.png)
- 通过这样的转换,所有的正则表达式,都可以转换为一个 NFA。
- 基于 NFA你仍然可以实现一个词法分析器只不过算法会跟基于 DFA 的不同:当某个状态存在一条以上的转换路径的时候,你要先尝试其中的一条;如果匹配不上,再退回来,尝
试其他路径。这种试探不成功再退回来的过程,叫做**回溯Backtracking**。
- 基于 NFA你也可以写一个正则表达式工具。实际上我在示例程序中已经写了一个简单的正则表达式工具使用了 **Regex.java中的 regexToNFA** 方法。如下所示,我用了一
个测试用的正则表达式,它能识别 int 关键字、标识符和数字字面量。在示例程序中,这个正则表达式首先被表示为一个内部的树状数据结构,然后可以转换成 NFA。
```js
int | [a-zA-Z][a-zA-Z0-9]* | [0-9]*
```
- 示例程序也会将生成的 NFA 打印输出下面的输出结果中列出了所有的状态以及每个状态到其他状态的转换比如“0 ε -> 2”的意思是从状态 0 通过 ε 转换,到达状态 2
```txt
NFA states:
0 ε -> 2
ε -> 8
ε -> 14
2 i -> 3
3 n -> 5
5 t -> 7
7 ε -> 1
1 (end)
acceptable
8 [a-z]|[A-Z] -> 9
9 ε -> 10
ε -> 13
10 [0-9]|[a-z]|[A-Z] -> 11
11 ε -> 10
ε -> 13
13 ε -> 1
14 [0-9] -> 15
15 ε -> 14
ε -> 1
```
- 我用图片来直观展示一下输出结果,分为上、中、下三条路径,你能清晰地看出解析 int 关键字、标识符和数字字面量的过程:
- ![由算法自动生成的NFA](pic/由算法自动生成的NFA.png)
- 那么生成 NFA 之后,我们要如何利用它,来识别某个字符串是否符合这个 NFA 代表的正则表达式呢?
- 还是以图 12 为例当我们解析“intA”这个字符串时首先选择最上面的路径进行匹配匹配完 int 这三个字符以后,来到状态 7若后面没有其他字符就可以到达接受状态 1
返回匹配成功的信息。
- 可实际上int 后面是有 A 的,所以第一条路径匹配失败。失败之后不能直接返回“匹配失败”的结果,因为还有其他路径,所以我们要回溯到状态 0去尝试第二条路径在第二
条路径中,我们尝试成功了。
- 运行 Regex.java 中的 matchWithNFA() 方法,你可以用 NFA 来做正则表达式的匹配。其中在匹配“intA”时你会看到它的回溯过程
```js
NFA matching: 'intA'
trying state : 0, index =0
trying state : 2, index =0 //先走第一条路径即int关键字这个路径
trying state : 3, index =1
trying state : 5, index =2
trying state : 7, index =3
trying state : 1, index =3 //到了末尾,发现还有字符'A'没有匹配上
trying state : 8, index =0 //回溯,尝试第二条路径,即标识符
trying state : 9, index =1
trying state : 10, index =1 //在10和11这里循环多次
trying state : 11, index =2
trying state : 10, index =2
trying state : 11, index =3
trying state : 10, index =3
true
```
- **从中你可以看到用 NFA 算法的特点**因为存在多条可能的路径所以需要试探和回溯在比较极端的情况下回溯次数会非常多性能会变得非常差。特别是当处理类似“s*”这样
的语句时,因为 s 可以重复 0 到无穷次,所以在匹配字符串时,可能需要尝试很多次。
- NFA 的运行可能导致大量的回溯,**那么能否将 NFA 转换成 DFA让字符串的匹配过程更简单呢** 如果能的话,那整个过程都可以自动化,从正则表达式到 NFA再从 NFA 到 DFA。
- 方法是有的,这个算法就是**子集构造法**。
- 总之,只要有了准确的正则表达式,是可以根据算法自动生成对字符串进行匹配的程序的,这就是正则表达式工具的基本原理,也是有些工具(比如 ANTLR 和 flex能够自动给你生
成一个词法分析器的原理。
#### 3. 语法分析:两个基本功和两种算法思路

@ -0,0 +1,86 @@
## 1. 基本架构:一个键值数据库包含什么?
- 更好的学习方式就是先建立起**系统观**。
- 这也就是说,如果我们想要深入理解和优化 Redis就必须要对它的总体架构和关键模块有一个全局的认知然后再深入到具体的技术点。
- 开始构造 SimpleKV 时,首先就要考虑里面可以存什么样的数据,对数据可以做什么样的操作,也就是数据模型和操作接口。它们看似简单,实际上却是我们理解 Redis 经常被用
于缓存、秒杀、分布式锁等场景的重要基础。
- 理解了数据模型,你就会明白,为什么在有些场景下,原先使用关系型数据库保存的数据,也可以用键值数据库保存。例如,用户信息(用户 ID、姓名、年龄、性别等通常用
关系型数据库保存,在这个场景下,一个用户 ID 对应一个用户信息集合,这就是键值数据库的一种数据模型,它同样能完成这一存储需求。
- 但是,如果你只知道数据模型,而不了解操作接口的话,可能就无法理解,为什么在有些场景中,使用键值数据库又不合适了。例如,同样是在上面的场景中,如果你要对多个用
户的年龄计算均值,键值数据库就无法完成了。因为它只提供简单的操作接口,无法支持复杂的聚合计算。
- 可以存哪些数据?
- 对于键值数据库而言,基本的数据模型是 key-value 模型。
- 不同键值数据库支持的 key 类型一般差异不大,而 value 类型则有较大差别。
- 我们在对键值数据库进行选型时,一个重要地考虑因素是**它支持的 value 类型**。
- 例如Memcached 支持的 value 类型仅为 String 类型,而 Redis 支持的 value 类型包括了 String、哈希表、列表、集合等。**Redis 能够在实际业务场景中得到广泛的应用,
就是得益于支持多样化类型的 value**。
- 从使用的角度来说,不同 value 类型的实现,不仅可以支撑不同业务的数据需求,而且也隐含着不同数据结构在性能、空间效率等方面的差异,从而导致不同的 value 操作之间存
在着差异。
- 可以对数据做什么操作?
- 我们先来了解下 SimpleKV 需要支持的 3 种基本操作,即 PUT、GET 和 DELETE。
- PUT新写入或更新一个 key-value 对;
- GET根据一个 key 读取相应的 value 值;
- DELETE根据一个 key 删除整个 key-value 对。
- 需要注意的是,**有些键值数据库的新写 / 更新操作叫 SET**。新写入和更新虽然是用一个操作接口,但在实际执行时,会根据 key 是否存在而执行相应的新写或更新流程。
- 在实际的业务场景中,我们经常会碰到这种情况:查询一个用户在一段时间内的访问记录。这种操作在键值数据库中属于 SCAN 操作,即**根据一段 key 的范围返回相应的 value
值。因此PUT/GET/DELETE/SCAN 是一个键值数据库的基本操作集合**。
- 我们就要更进一步,考虑一个非常重要的设计问题:**键值对保存在内存还是外存**
- 保存在内存的好处是读写很快,毕竟内存的访问速度一般都在百 ns 级别。但是,潜在的风险是一旦掉电,所有的数据都会丢失。
- 保存在外存,虽然可以避免数据丢失,但是受限于磁盘的慢速读写(通常在几 ms 级别),键值数据库的整体性能会被拉低。
- 因此,如何进行设计选择,**我们通常需要考虑键值数据库的主要应用场景**。
- SimpleKV 的基本组件
- 大体来说,一个键值数据库包括了**访问框架、索引模块、操作模块和存储模块**四部分
- ![SimpleKV基本内部结构](pic/SimpleKV基本内部结构.png)
- 采用什么访问模式?
- 访问模式通常有两种:一种是**通过函数库调用的方式供外部应用使用**,比如,上图中的 libsimplekv.so就是以动态链接库的形式链接到我们自己的程序中提供键值存储功能
- 另一种是**通过网络框架以 Socket 通信的形式对外提供键值对操作**,这种形式可以提供广泛的键值存储服务。在上图中,我们可以看到,网络框架中包括 Socket Server 和协议解析。
- 不同的键值数据库服务器和客户端交互的协议并不相同,我们在对键值数据库进行二次开发、新增功能时,必须要了解和掌握键值数据库的通信协议,这样才能开发出兼容的客户端。
- 通过网络框架提供键值存储服务,一方面扩大了键值数据库的受用面,但另一方面,也给键值数据库的性能、运行模型提供了不同的设计选择,带来了一些潜在的问题。
- 如何定位键值对的位置?
- 索引的作用是让键值数据库根据 key 找到相应 value 的存储位置,进而执行操作。
- 不同操作的具体逻辑是怎样的?
- 对于 GET/SCAN 操作而言,此时根据 value 的存储位置返回 value 值即可;
- 对于 PUT 一个新的键值对数据而言SimpleKV 需要为该键值对分配内存空间;
- 对于 DELETE 操作SimpleKV 需要删除键值对,并释放相应的内存空间,这个过程由分配器完成。
- 如何实现重启后快速提供服务?
- SimpleKV 采用了常用的内存分配器 glibc 的 malloc 和 free因此SimpleKV 并不需要特别考虑内存空间的管理问题。
- 因此分配器是键值数据库中的一个关键因素。Redis 的内存分配器提供了多种选择,分配效率也不一样。
- SimpleKV 只是周期性地把内存中的键值数据保存到文件中,这样可以避免频繁写盘操作的性能影响。但是,一个潜在的代价是 SimpleKV 的数据仍然有丢失的风险。
- ![SimpleKV演进到Redis](pic/SimpleKV演进到Redis.png)

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

@ -1,4 +1,28 @@
# 服务容器化-Docker
# 微服务治理
## 0. 目录
## 1. 服务编排流程
- 流程
- 服务 Docker 化
- Docker 仓库
- Mesos, Swarm, Kubernetes 管理
- 服务 Docker 化
- 安装基础镜像 https://hub.docker.com/
- JDK
- 拉取 : openjdk: docker pull openjdk:8u102-jre
- 查看 : docker image | grep jdk
- 运行【运行并进入】 : docker run -it --name java-8 --entrypoint bash openjdk:8u102-jre 或 【后台运行】 docker run -d -it --name java-8 openjdk:8u102-jre
- 配置打包插件, 把需要打包的服务打成 jar 包
- 编写 Docker File
-

@ -38,6 +38,14 @@
</dependencies>
</dependencyManagement>
<dependencies>
<!-- 添加对javacTrees的依赖 -->
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
<dependency>
<groupId>org.example</groupId>
<artifactId>dev-protocol-common</artifactId>

@ -1,6 +1,6 @@
package com.baiye.demo.case24.info;
import org.geekbang.time.commonmistakes.common.Utils;
import com.baiye.demo.utils.Utils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@ -1,6 +1,6 @@
package com.baiye.demo.case24.metrics;
import org.geekbang.time.commonmistakes.common.Utils;
import com.baiye.demo.utils.Utils;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@ -0,0 +1,53 @@
# 项目目录
- research01: 插入式注解研究
## research01
- 插入式注解处理器在《深入理解Java虚拟机》一书中有一些介绍
- 需求
- 我们为公司提供了一套通用的JAVA基础组件包组件包内有不同的模块比如熔断模块、负载均模块、rpc模块等等这些模块均会被打成jar包然后发布到公司的内部代码仓库中供其他人引入使用。
- 这份代码会不断的迭代我们希望可以通过promethus来监控现在公司内使用各版本代码库的比例
- 我们希望看到每一个版本的使用率,这有利于我们做版本兼容,必要的时候可以对古早版本使用者溯源
- 问题
- 但真要获取自身的jar版本号还是挺麻烦的有个比较简单但阴间的办法就是给每一个组件都加上当前的jar版本号写到配置文件里或者直接设置成常量
- 这样上报promethus时就可以直接获取到jar包版本号了这个方法虽然可以解决问题但每次迭代版本都要跟着改一遍所有组件包的版本号数据过于麻烦。
- 有没有更好的解决办法呢比如我们可不可以在gradle打包构建时拿到jar包的版本号然后注入到每个组件中去呢就像lombok那样不需要写get、set方法只需要加个注解标记就可以自动注入get、set方法。
- 解决
- java中解析一个注解的方式主要有两种编译期扫描、运行期反射这是lombok @Setter的实现:
```java
@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.SOURCE)
public @interface Setter {
// 略...
}
```
- 可以看到@Setter的Retention是SOURCE类型的也就是说这个注解只在编译期有效它甚至不会被编入class文件所以lombok无疑是第一种解析方式那用什么方式可以在编译期就让注解被解析到并执行我们的解析代码呢
- 答案就是定义插入式注解处理器通过JSR-269提案定义的Pluggable Annotation Processing API实现
- ![插入式注解处理器的触发点.png](pic/插入式注解处理器的触发点.png)
- 也就是说插入式注解处理器可以帮助我们在编译期修改抽象语法树AST所以现在我们只需要自定义一个这样的处理器然后其内部拿到jar版本信息因为是编译期可以找到源码的path源码里随便搞个文件存放版本号
- 然后用java io读取进来即可再将注解对应语法树上的常量值设置成jar包版本号语法树变了最终生成的字节码也会跟着变这样就实现了我们想在编译期给常量version注入值的愿望。
- 自定义一个插入式注解处理器也很简单,首先要将自己的注解定义出来:[TrisceliVersion.java](research01/TrisceliVersion.java)
- 然后定义一个继承了AbstractProcessor的处理器 [TrisceliVersionProcessor.java](research01/TrisceliVersionProcessor.java)
- 在 Java 编译器中JavacTrees 类是一个强大的工具,它允许开发人员访问和操作 Java 源代码的抽象语法树AST。抽象语法树是源代码的结构化表示形式它将代码分解为一系列的节点并以树状结构呈现。通过使用 JavacTrees您可以在编译过程中分析和修改 Java 代码,从而实现许多有用的功能。
- 记得给项目添加 tools.jar
```xml
<dependency>
<groupId>com.sun</groupId>
<artifactId>tools</artifactId>
<version>1.8.0</version>
<scope>system</scope>
<systemPath>${java.home}/../lib/tools.jar</systemPath>
</dependency>
```
- 定义好的处理器需要SPI机制被发现所以需要定义META.services[javax.annotation.processing.Processor] 里面定义为注解的引用路径

@ -0,0 +1,8 @@
package com.baiye.research.research01;
public class Test {
// 设置注解, 然后编译
@TrisceliVersion
public static final String version = "";
}

@ -0,0 +1,12 @@
package com.baiye.research.research01;
import java.lang.annotation.*;
/**
*
*/
@Documented
@Retention(RetentionPolicy.SOURCE) //只在编译期有效最终不会打进class文件中
@Target({ElementType.FIELD}) //仅允许作用于类属性之上
public @interface TrisceliVersion {
}

@ -0,0 +1,96 @@
package com.baiye.research.research01;
import com.sun.tools.javac.api.JavacTrees;
import com.sun.tools.javac.processing.JavacProcessingEnvironment;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.TreeMaker;
import com.sun.tools.javac.util.Context;
import lombok.SneakyThrows;
import javax.lang.model.element.Element;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.annotation.processing.RoundEnvironment;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic;
import java.util.HashSet;
import java.util.Set;
/**
* {@link AbstractProcessor} Pluggable Annotation Processing API
*/
public class TrisceliVersionProcessor extends AbstractProcessor {
private JavacTrees javacTrees;
private TreeMaker treeMaker;
private ProcessingEnvironment processingEnv;
/**
*
*
* @param processingEnv
*/
@SneakyThrows // @SneakyThrows 是 Lombok 提供的一个注解,用于在方法上自动抛出异常。使用 @SneakyThrows 注解可以使方法在遇到异常时,自动将异常转换为 java.lang.RuntimeException 并抛出,而无需显式地在方法中编写异常处理代码。
@Override
public synchronized void init(ProcessingEnvironment processingEnv) {
super.init(processingEnv);
this.processingEnv = processingEnv;
this.javacTrees = JavacTrees.instance(processingEnv);
Context context = ((JavacProcessingEnvironment) processingEnv).getContext();
this.treeMaker = TreeMaker.instance(context);
}
@Override
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}
/**
*
* @return
*/
@Override
public Set<String> getSupportedAnnotationTypes() {
HashSet<String> set = new HashSet<>();
set.add(TrisceliVersion.class.getName()); // 支持解析的注解
return set;
}
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement t : annotations) {
// 获取到给定注解的elementelement可以是一个类、方法、包等
for (Element e : roundEnv.getElementsAnnotatedWith(t)) {
// JCVariableDecl为字段/变量定义语法树节点
JCTree.JCVariableDecl jcv = (JCTree.JCVariableDecl) javacTrees.getTree(e);
String varType = jcv.vartype.type.toString();
if (!"java.lang.String".equals(varType)) { // 限定变量类型必须是String类型否则抛异常
printErrorMessage(e, "Type '" + varType + "'" + " is not support.");
}
jcv.init = treeMaker.Literal(getVersion()); // 给这个字段赋值也就是getVersion的返回值
}
}
return true;
}
/**
* processingEnvMessager
*
* @param e element
* @param m error message
*/
private void printErrorMessage(Element e, String m) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, m, e);
}
private String getVersion() {
/*
versionfixme
*/
return "v1.0.1";
}
}

@ -0,0 +1,879 @@
## 数据模型设计
### 1. 模型设计基础
- 数据模型设计的元素
- 实体
- 描述业务的主要数据集合
- 谁, 什么, 何时, 何地, 为何, 如何
- 属性
- 描述实体里面的单个信息
- 关系
- 描述实体与实体之间的数据规则
- 结构规则: 1-N, N-1, N-N
- 引用规则: 电话号码不能单独存在
- 传统模型设计
- ![传统模型设计.png](pic/传统模型设计.png)
- 架构师要产出 LDM 给开发者
- 逻辑模型
- 实体, 属性, 函数名称, 实体间关系
- 开发者: 第三范式下的物理模型
- 数据在库里尽量不可能存在冗余
### 2. JSON 文档模型的设计特点
- MongoDB 文档模型设计的三个误区
- 不需要模型设计
- MongoDB 应用用一个超级大文档来组织所有数据
- MongoDB 不支持关联或者事务
- 关于 JSON 文档模型设计
- 文档模型设计处于是物理模型设计阶段 (PDM)
- JSON 文档模型通过内嵌数组或引用字段来表示关系
- 文档模型设计不遵从第三范式, 允许冗余
- 流程: 概念建模 -> 逻辑建模 -> 文档模型 / 关系模型
- 为什么人们都说 MongoDB 是无模式
- 严格来说, MongoDB 同样需要概念/逻辑建模
- 文档建模设计的物理层结构可以和逻辑层类似
- MongoDB 无模式由来:
- **可以省略物理建模的具体过程**
- 概念模型 -> 逻辑模型 -(复用)-> 物理模型
- JSON 模型和逻辑模型对比
- ![JSON 模型和逻辑模型对比.png](pic/JSON%20模型和逻辑模型对比.png)
- **文档模型的设计原则: 性能和易用**
- 关系模型 vs 文档模型
- ![关系模型 vs 文档模型.png](pic/关系模型%20vs%20文档模型.png)
### 3. 文档模型三步走
- MongoDB 文档模型设计三步曲 - 总览
- ![MongoDB文档模型设计三步曲.png](pic/MongoDB文档模型设计三步曲.png)
- **第一步: 建立基础文档模型**
- 根据概念模型或者业务需求推导出逻辑模型 - 找到对象
- 列出实体之间的关系(及基数) - 明确关系
- 套用逻辑设计原则来决定内嵌方式 - 进行建模
- 完成基础模型构建
- 业务需求及逻辑模型 -(逻辑导向)-> 基础建模 -> 集合/字段/基础形状
- 举例
- 找到对象:
- Contacts
- Groups
- Address
- Portraits
- 明确关系
- 一个联系人有一个头像(1-1)
- 一个联系人可以有多个地址(1-N)
- 一个联系人可以属于多个组, 一个组可以有多个联系人(N-N)
- 表示:
- Groups(name) <-[N-N]-> Contacts (name,company,title) <-[1-N]-> Addresses(type,street,city,state,zip_code)
- Contacts(...) <-[1-1]-> Portraits(mine_type,date)
- 1-1 关系建模: portraits
- 基本原则: **一对一关系以内嵌为主, 作为子文档形式或者直接在顶级不涉及到数据冗余**
- 例外情况: **如果内嵌后导致文档大小超过16MB**
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
}
```
- 1-N 关系建模: Addresses
- 基本原则: **一对多关系同样以内嵌为主, 用数组来表示一对多不涉及到数据冗余**
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
- Groups(name) <-[N-N]->
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
}
```
- N-N 关系建模: 内嵌数组模式
- 基本原则: **不需要映射, 一般用内嵌数组来表示一对多, 通过冗余来实现 N-N**
- 例外情况: **内嵌后导致文档大小超过 16 MB, 数组长度太大(数万或更多) 数组长度不确定**
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
- **第二步: 根据读写工况细化**
- 技术需求, 读写比例, 方式及数量 -(技术导向)-> 工况细化 -> [引用及关联]
- 考虑的问题
- 最频繁的数据查询模式
- 最常用的查询参数
- 最频繁的数据写入模式
- 读写操作的比例
- 数据量的大小
- 基于内嵌的文档模型
- 根据业务需求
- 使用引用来避免性能瓶颈
- 使用冗余来优化访问性能
- 举例
- 联系人管理应用分组需求
- 用于客户营销
- 有千万级联系人
- 需要频繁变动分组的信息, 如增加分组及修改名称及描述以及营销状态
- 一个分组可以有百万级联系人
```json
name: "TJ Tang",
company: "TAPDATA",
title: "CTO",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
groups: {
{name: :"Friends"}
{name: :"Surfers"}
}
```
- 一个分组(groups)的改动意味着百万级的DB操作
- 解决方案: Group 使用单独的集合
- 类似于关系型设计
- 用 id 或者唯一键关联
- 使用 $lookup 来提供一次查询多表的能力(类似关联)
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
portraits: {
mimetype: xxx,
date: xxxx
},
addresses: {
{type: home, ...},
{type: work, ...},
},
group_ids: [1,2,3]
```
```json
Groups
group_id
name
```
- 使用 group_id 进行关联
- 此时查询语句
```spring-mongodb-json
db.contacts.aggregate([
{
$lookup:{
from: "groups",
localField: "group_ids",
foreignField: "group_id",
as: "groups"
}
}])
```
- aggregate 的 $lookup 来实现关联查询
- 例子2: 联系人的头像: 引用模式
- 头像使用高保真, 大小在 5MB - 10MB
- 头像一旦上传, 一个月不可变更
- 基础信息查询(不含头像) 和 头像查询的比例为 9 1
- 建议: 使用引用方式, 把头像数据放到另外一个集合, 可以显著提升 90% 的查询效率
```json
name: "Mona Jang",
company: "HUAXIA",
title: "DIRECTOR",
addresses: {
{type: home, ...},
{type: work, ...},
},
group_ids: [1,2,3]
```
```json
Contact_Portrait: {
_id: 123,
mimetype: xxx,
data: xxxx
}
```
- 什么时候该使用引用方式?
- 内嵌文档太大, 数 MB 或者超过 16MB
- 内嵌文档或数组元素会频繁修改
- 内嵌数组元素会持续增长并且没有封顶
- MongoDB 引用设计的限制
- MongoDB 对使用引用的集合之间并无主外键检查
- MongoDB 使用聚合框架的 $lookup 来模仿关联查询
- $lookup 只支持 left outer join
- $lookup 的关联目标(from) 不能是分片表
- **第三步: 套用设计模式**
- 文档模式: 无范式, 无思维定式, 充分发挥想象力
- 设计模式: 实战过屡试不爽的设计技巧, 快速应用
- 举例: 一个 loT 场景的分桶设计模式, 可以帮助把存储空间降低10倍并且查询效率提升数十倍
- 经验和学习 -(模式导向)-> 套用设计模式 -> 优化的模型
- 问题: 物联网场景下的海量数据处理 - 飞机监控数据
```json
{
"_id" : "20160101050000:CA2790",
"icao": "CA2790",
"callsign": "CA2790",
"ts": ISODate("2016-01-01T05:00:00.000+0000"),
"events": {
"a": 31418,
"b" : 173,
"s" :91,
"v" : 80
}
}
```
- 实际问题: 520亿条, 10TB - 海量数据
- 10万架飞机
- 1年的数据
- 每分钟1条
- ![飞机-海量数据.png](pic/飞机-海量数据.png)
- 52.6B = 100000 * 365 * 24 * 60
- 6364GB = 100000 * 365 * 24 * 60 * 130
- 4503GB = 100000 * 365 * 24 * 60 * 92
- 解决方案: 分桶设计
```json
{
" id" :"20160101050000:WG9943",
"icao":"WG9943",
"ts":ISODate("2016-01-01T05:00:00.000+0000"),
"events":[
{
"a":24293, "b":319, "p":[41,70], "s":56,
"t": ISODate("2016-01-01T05:00:00.000+0000")
},
{
"a":33663, "b":134, "p":[-38, -30], "s":385,
"t": ISODate("2016-01-01T05:00:01.000+0000")
}
]
}
```
- 60 events == 1小时的数据
- 一个文档: 一架飞机一个小时的数据
- ![飞机-海量数据-分桶.png](pic/飞机-海量数据-分桶.png)
- 可视化表现24小时的飞行数据
- 1440次读
- 模式小结:
- 场景
- 时序数据
- 物联网
- 智慧城市
- 智慧交通
- 痛点
- 数据点采频繁, 数据量太多
- 设计模式的方案及优点
- 利用文档内嵌数组, 将一个时间段的数据聚合到一个文档里
- 大量减少文档数量
- 大量减少索引占用空间
### 4. 设计模式集锦
- 问题: 大文档, 很多字段, 很多索引
```json
{
title: "Dunkirk",
release USA:"2017/07/23",
release UK:"2017/08/01",
release France:"2017/08/01",
release Festival San Jose:"2017/07/22"
}
```
- 需要很多索引
```json
{release_UsA:1}
{release_UK:1}
{release_France:1}
{release_Festival_San_Jose:1 }
```
- 电影的上映时间
- 解决方法: 列转行
```json
{
title: "Dunkirk",
releases:[
{country:"USA",date:"2017/7/23"},
{country:"Uk",date:"2017/08/01”}
]
}
```
- 转换后: 字段数变少了
- 创建索引: db.movies.createIndex({"releases.country":1, "releases.date":1})
- 模式小结:
- 场景: 产品属性多, 多语言(多国家属性)
- 痛点: 文档中有很多类似的字段, 会用于组合查询搜索, 需要创建很多索引
- 设计模式方案及优点: 转化为数组, 一个索引解决所有查询问题
- 问题: 模型灵活了, 如何管理文档的不同版本
```json
"id":ObiectId("5de26f197edd62c5d388babb"),
"name":"TJ",
"Tapdata""company":
```
- v1.0
```json
"id":obiectId("5de26f197edd62c5d388babb")
"name":"TJ"
"company":"Tapdata"
"wechat":"titang826"
"schema_version": "2.0"
```
- v2.0
- 解决方案schema_version 使用这个字段进行区分标识
- 模式小结:版本字段
- 场景: 任何有版本衍生的数据库
- 痛点: 文档模型格式多, 无法知道其合理性, 升级时候需要更新太多文档
- 设计模式及优点: 增加一个版本号字段, 快速过滤掉不需要升级的文档, 升级时候对不同的文档做不同的升级
- 问题: 统计网页点击流量
- 每访问一个页面就会产生一次数据库计数更新操作
- 统计数字准确性并不十分重要
- 解决方案: 用近似计算
- 每隔 10(X)次写一次
- Increment by 10(X)
- {$inc:{views:1}}
- if random(0,9) == 0 increment by 10
- 模式小结: 近似计算
- 场景:
- 网页计数
- 各种结果不需要准确的排名
- 痛点:
- 写入太频繁, 消耗系统资源
- 设计模式方案及优点
- 间隔写入, 每隔10次或者100次
- 大量减少写入需求
- 问题: 业绩排名, 游戏排名, 商品统计等精确统计
- 热销榜: 某个商品今天卖了多少, 这个星期卖了多少, 这个月卖了多少?
- 电影排行: 观影者, 场次统计
- 传统解决方案: 通过聚合计算
- 痛点: 消耗资源多, 聚合计算时间长
- 解决方案: 用预聚合字段
```json
{
product:"Bike"
sku:“abc123456”
quantitiy:20394,
daily_sales: 40,
weekly sales:302,
monthly_sales:1419
}
```
```spring-mongodb-json
db.inventory.update({_id:123},{
$inc:{
quantity:-1,
daily_sales: 1,
weekly_sales:1,
monthly_sales: 1
}
})
```
- 模式小结: 预聚合
- 场景: 准确排名, 排行榜
- 痛点: 统计计算耗时, 计算时间长
- 设计模式方案及优点: 模型中直接增加统计字段, 每次更新数据时候同时更新统计值
## 分片和集群 - [运维]
### 1. 分片集群机制及原理
- MongoDB 常见的部署架构
- ![MongoDB常见的部署架构.png](pic/MongoDB常见的部署架构.png)
- 为什么要使用分片集群?
- 数据容量日益增大, 访问性能日渐降低, 怎么破?
- 新品上线异常火爆, 如何支撑更多的并发用户?
- 单库已有 10TB 数据, 恢复需要 1-2 天, 如何加速?
- 地理分布数据
- 分片如何解决?
- 银行交易单表内10亿笔资料超负荷运转
- 交易号 0 - 1,000,000,000
- 思路:
- 交易号: 0 - 500,000,000 -> mongod[分片1]
- 交易号: 500,000,000 - 1,000,000,000 -> mongod[分片2]
- 还可以再细分[最多1024片]
- 分片集群剖析 - 路由节点 mongos
- ![分片节点剖析.png](pic/分片节点剖析.png)
- 分片集群剖析 - 配置节点 mongod
- ![配置节点mongod.png](pic/配置节点mongod.png)
- 分片集群剖析 - 数据节点 mongod
- ![数据节点mongod.png](pic/数据节点mongod.png)
- MongoDB 分片集群特点
- 应用全透明, 无特殊处理
- 数据自动均衡
- 动态扩容, 无须下线
- 提供三种分片方式
- 分片集群数据分布方式
- 基于范围
- ![分片集群数据分布方式-基于范围.png](pic/分片集群数据分布方式-基于范围.png)
- 基于 Hash
- ![分片集群数据分布方式-基于Hash.png](pic/分片集群数据分布方式-基于Hash.png)
- 基于 zone/tag
- ![分片集群数据分布方式-基于zone.png](pic/分片集群数据分布方式-基于zone.png)
- 小结:
- 分片集群可以有效解决性能瓶颈及系统扩容问题
- 分片消耗较多, 管理复杂, 尽量不要分片
- 详细了解学习后再进行分片
### 2. 分片集群的设计
- 如何用好分片集群
- 合理的架构
- 是否需要分片?
- 需要多少分片?
- 数据的分布规则
- 正确的姿势
- 选择需要分片的表
- 选择正确的片键
- 使用合适的均衡策略
- 足够的资源
- CPU
- RAM
- 存储
- 合理的架构 - 分片大小
- 分片的基本标准
- 关于数据: 数据量不超过 3TB, 尽可能保持在 2TB一个片;
- 关于索引: 常用索引必须容纳进内存
- 按照以上标准初步确定分片后, 还需要考虑业务压力, 随着压力增大, CPU, RAM, 磁盘中的任何一项出现瓶颈时, 都可以通过添加更多分片来解决。
- 合理的架构 - 需要多少个分片
- A = 所需存储总量/单服务器可挂载容量 8TB/2TB = 4
- B = 工作集大小/单服务器内存容量 400GB/ (256G*0.6) = 3
- C = 并发量总数/ (单服务器并发量*0.7) 30000/(9000*0.7) = 6 [额外开销]
- 分片数量 = max(A, B, C)=6
- 合理的架构 - 其他需求
- 考虑分片的分布:
- 是否需要跨机房分布分片?
- 是否需要容灾?
- 高可用的要求如何?
- 正确的姿势
- ![正确的姿势-概念图.png](pic/正确的姿势-概念图.png)
- 各种概念由小到大
- 片键 shard key: 文档中的一个字段
- 文档 doc: 包含 shard key 的一行数据
- 块 Chunk: 包含 n个文档
- 分片 Shard: 包含 n个 chunk
- 集群 Cluster: 包含 n个 分片
- 正确的姿势 - 选择合适的片键
- 影响片键效率的主要因素
- 取值基数(Cardinality)
- 取值分布
- 分散写, 集中读
- 被尽可能的业务场景用到
- 避免单调递增或递减的片键
- 正确的姿势 - 选择基数大的片键
- 对于小基数的片键:
- 因为备选值有限, 那么块的总数量就有限
- 随着数据增多, 块的大小会越来越大
- 太大的块, 会导致水平扩展时移动块会非常困难
- 例如: 存储一个高中的师生数据, 以年龄(假设年龄范围为15-65岁)作为片键, 那么:
- 15 <= 年龄 <= 65, 且只为整数
- 最多只会有 51个 chunk
- 结论: 取值基数要大
- 正确的姿势 - 选择分布均匀的片键
- 对于分布不均匀的片键
- 造成某些块的数据量急剧增大
- 这些块压力随之增大
- 数据均衡以chunk为单位, 所以系统无能为力
- 例如: 存储一个学校的师生数据, 以年龄(假设年龄范围为15-65岁)作为片键,那么:
- 15 <= 年龄 <= 65, 且只为整数
- 大部分的年龄范围为 15-18岁(学生)
- 15, 16, 17, 18 四个chunk的数据量, 访问压力远大于其他chunk
- 结论: 取值分布应尽可能均匀
- 正确的姿势 - 定向性好
- 考虑:
- 4个分片的集群, 你希望读某条特定的数据
- 如果你用片键作为条件查询, mongos 可以直接定位到具体的分片
- 如果你不用片键, mongos 需要把查询发到4个分片
- 等最后一个分片响应, mongos 才能响应应用端
- 结论: 对主要查询要具有定向能力
- 比如用组合片键来解决这个问题
- 片键: {user_id: 1, time: 1}
- 足够的资源
- mongos 与 config 通常消耗很少的资源, 可以选择低规格虚拟机
- 资源的重点在于 shard 服务器
- 需要足以容纳热数据索引的内存
- 正确创建索引后CPU通常不会成为瓶颈, 除非涉及非常多的计算
- 磁盘尽量选用SSD
- 最后, 实际测试是最好的检验, 来看你的资源配置是否完备
- 即使项目初期已经具备了足够的资源, 仍然需要考虑在合适的时候扩展
- 建议监控各项资源使用情况, 无论哪一项达到 60% 以上, 则开始考虑扩展, 因为:
- 扩展需要新的资源, 申请新资源需要时间
- 扩展后数据需要均衡, 均衡需要时间, 应保证新数据入库速度慢于均衡速度
- 均衡需要资源, 如果资源即将或已经耗尽, 均衡会很低效的
### 3. 分片集群的搭建及扩容 - 实验
- 实验目标及流程
- 目标: 学习如何搭建一个2个分片集群
- 环境: 3台Linux 虚拟机, 4Core 8GB
- 步骤
- 配置域名解析
- 准备分片目录
- 创建第一个分片复制集并初始化
- 创建config复制集并初始化
- 初始化分片集群, 加入第一个分片
- 创建分片表
- 加入第二个分片
- 实验架构
- testdemo01
- Shard1{Primary 27010} Shard2{Primary 27011} Config1 27019 mongos 27017
- testdemo02
- Secondary 27010 Secondary 27011 Config2 27019
- testdemo03
- Secondary 27010 Secondary 27011 Config3 27019
- ![实验架构.png](pic/实验架构.png)
- 1 - **配置域名解析**
- 在三台虚拟机上分别执行以下3条命令, 注意替换实际IP地址
- echo "192.168.1.1 testdemo01 member1.example.com member2.example.com" >> /etc/hosts
- echo "192.168.1.2 testdemo02 member3.example.com member4.example.com" >> /etc/hosts
- echo "192.168.1.3 testdemo03 member5.example.com member6.example.com" >> /etc/hosts
- 2 - 准备分片目录
- 在各服务器上创建数据目录, 我们使用 /data, 按自己的需求修改为其他目录:
- 在 member1 / member3 / member5 上执行以下命令:
- mkdir -p /data/shard1/
- mkdir -p /data/config/
- 在 member2 / member4 / member6 上执行以下命令:
- mkdir -p /data/shard2/
- mkdir -p /data/mongos/
- 3 - 创建第一个分片用的复制集
- 在 member1 / member3 / member5 上执行以下命令:
- mongod --bind_ip 0.0.0.0 --replSet shard1 --dbpath /data/shard1 --logpath /data/shard1/mongod.log --port 27010 --fork --shardsvr --wiredTigerCacheSizeGB 1
- 检查是否成功运行: mongo localhost:27010
- 4 - 初始化第一个分片复制集
- mongo --host member1.example.com:27010
```spring-mongodb-json
rs.initiate({
_id: "shard1",
"members": [
{
"_id": 0,
"host": "member1.example.com:27010"
},
{
"_id": 1,
"host": "member3.example.com:27010"
},
{
"_id": 2,
"host": "member5.example.com:27010"
},
]
})
```
- 查看结果: rs.status()
- 5 - 创建 config server 复制集
- 在member1 / member3 / member5 上执行以下命令
- mongod --bind_ip 0.0.0.0 --replSet config --dbpath /data/config --logpath /data/config/mongod.log --port 27019 --fork --configsvr --wiredTigerCacheSizeGB 1
- 6 - 初始化 config server 复制集
- mongo --host member1.example.com:27019
```spring-mongodb-json
rs.initiate({
_id: "config",
"members": [
{
"_id": 0,
"host": "member1.example.com:27019"
},
{
"_id": 1,
"host": "member3.example.com:27019"
},
{
"_id": 2,
"host": "member5.example.com:27019"
},
]
})
```
- 查看结果: rs.status()
- 7 - 在第一台机器上搭建 mongos[也可以直接在别的机器上进行运行, 一般至少需要2个来做高可用]
- mongos --bind_ip 0.0.0.0 --logpath /data/mongos/mongos.log --port 27017 --fork --configdb config/member1.example.com:27019,member3.example.com:27019,member5.example.com:27019
- 连接到 mongos, 添加分片
- mongo --host member1.example.com:27017
- mongos>
- sh.addShard("shard1/member1.example.com:27010,member3.example.com:27010,member5.example.com:27010");
- 查看运行结果: sh.status()
- 8 - 创建分片表
- 连接到 mongos, 创建分片集合
- mongo --host memeber1.example.com:27017
- mongos> sh.status()
- mongos> sh.enableSharding("foo"); 指定库名
- mongos> sh.shardCollection("foo.bar", {_id:'hashed'}); 指定集合和分片键
- mongos> sh.status();
- 插入测试数据
- use foo
- for(var i=0;i < 10000; i++){ db.bar.insert({i:i});}
- 查看结果: sh.status()
- 9 - 创建第二个分片的复制集 - 扩容
- 在 member2 / member4 / member6 上执行以下命令
- mongod --bind_ip 0.0.0.0 --replSet shard2 --dbpath /data/shard2 --logpath /data/shard2/mongod.log --port 27011 --fork --shardsvr --wiredTigerCacheSizeGB 1
- 10 - 初始化第二个分片复制集 - 扩容
- mongo --host member2.example.com:27011
```spring-mongodb-json
rs.initiate({
_id: "shard2",
"members": [
{
"_id": 0,
"host": "member2.example.com:27011"
},
{
"_id": 1,
"host": "member4.example.com:27011"
},
{
"_id": 2,
"host": "member6.example.com:27011"
},
]
})
```
- 查看结果: rs.status()
- 11 - 加入第二个分片 - 扩容
- 连接到 mongos, 添加分片
- mongo --host member1.example.com:27017
- mongos>
- sh.addShard("shard2/member2.example.com:27011,member4.example.com:27011,member6.example.com:27011");
- 查看运行结果: sh.status()
### 4. MongoDB 监控最佳实践
- 常用的监控工具和手段
- MongoDB Ops Manager
- Percona
- 通用监控平台
- 程序脚本
- 如何获取监控数据
- 监控信息的来源
- db.serverStatus()(主要)
- db.isMaster()(次要)
- mongostats 命令行工具(只有部分信息)
- 注意: db.serverStatus() 包含的监控信息是从上次开机到现在为止的累计数据, 因此不能简单使用
- serverStatus() Output
- ![serverStatus.png](pic/serverStatus.png)
- serverStatus() 主要信息
- connections: 关于连接数的信息
- locks: 关于MongoDB使用的锁情况
- network: 网络使用情况统计
- opcounters: CRUD 的执行次数统计
- repl: 复制集配置信息
- wiredTiger: 包含大量 wiredTiger 执行情况的信息
- block-manager: WT数据块的读写情况
- session: session 使用数量
- cocurrentTransactions: Ticket 使用情况
- mem: 内存使用情况
- metrics: 一系列性能指标统计信息
- 监控报警的考量
- 具备一定的容错机制以减少误报的发生
- 总结应用各指标峰值
- 适时调整报警阈值
- 留出足够的处理时间
- 建议监控的指标
- ![建议监控的指标.png](pic/建议监控的指标.png)
- ![建议监控的指标.png](pic/建议监控的指标1.png)
- ![建议监控的指标.png](pic/建议监控的指标2.png)
### 5. MongoDB 备份和恢复
- 为何备份
- 备份的目的:
- 防止硬件故障引起的数据丢失
- 防止人为错误误删数据
- 时间回溯
- 监管要求
- 第一点MongoDB生产集群已经通过复制集的多节点实现, 本讲的备份主要是为其他几个目的
- MongoDB 的备份
- MongoDB的备份机制分为:
- 延迟节点备份
- 全量备份 + oplog 增量
- 最常见的全量备份方式包括:
- mongodump
- 复制数据文件
- 文件系统快照
- 方案1: 延迟节点备份
- ![延迟节点备份.png](pic/延迟节点备份.png)
- ![延迟节点备份.png](pic/延迟节点备份1.png)
- 方案2: 全量备份加 oplog
- ![全量备份加oplog.png](pic/全量备份加oplog.png)
- ![全量备份加oplog.png](pic/全量备份加oplog1.png)
- 复制文件全量备份注意事项
- 复制数据库文件:
- 必须先关闭节点才能复制, 否则复制到的文件无效;
- 也可以选择db.fsyncLock()锁定节点, 但完成后不要忘记db.fsyncUnLock()解锁
- 可以且应该在从节点上完成
- 该方法时间上会暂时宕机一个从节点, 所以整个过程中应注意投票节点总数
- 全量备份加oplog注意事项
- 文件系统快照
- MongoDB支持使用文件系统快照直接获取数据文件在某一时刻的镜像
- 快照过程中可以不用停机
- 数据文件和Journal必须在同一个卷上
- 快照完成后请尽快复制文件并删除快照
- 全量备份Mongodump注意事项
- mongodump
- 使用 mongodump 备份最灵活, 但速度上也是最慢的
- mongodump 出来的数据不能表示某个时间点, 只是某个时间段
- ![mongodump.png](pic/mongodump.png)
- 解决方案: 幂等性
- ![解决方案-幂等性.png](pic/解决方案-幂等性.png)
### 6. MongoDB 备份和恢复的操作
- 备份和恢复工具参数
- 几个重要参数:
- mongodump
- --oplog:复制 mongodump 开始到结束过程中的所有 oplog 并输出到结果中。输出文件位于 dump/oplog.bson
- mongorestore
- --oplogReplay: 恢复完数据文件后再重放 oplog。默认重放 dump/oplog.bson=><dump-directory>/local/oplog.rs.bson。如果 oplog 不在这,则可以:
- --oplogFile:指定需要重放的 oplog 文件位置
- --oplogLimit: 重放 oplog 时截止到指定时间点
- 实际操作:mongodump/mongorestore
- 创建一个数据集, test
- 为了模拟dump过程中的数据变化我们开启一个循环插入数据的线程:
- for(var i=0;i<100000; i++){db.random.insertOne({x: Math.random()* 100000});}
- 在另一个窗口中我们对其进行mongodump:
- mongodump -h 127.0.0.1:27017 --oplog
- 回到第一个表, 进行删除表 - (模拟删除)
- db.random.drop()
- db.random.count() 查看是否删除成功
- 在第二台机器, 模拟恢复
- mongorestore --host localhost:27017 --oplogReplay
- dump 文件的目录结构
- ![dump文件的目录结构.png](pic/dump文件的目录结构.png)
- ![dump文件操作详解.png](pic/dump文件操作详解.png)
- 更复杂的重放 oplog 方式 - 增量重放
- ![更复杂的重放oplog方式.png](pic/更复杂的重放oplog方式.png)
- 分片集备份
- 分片集备份大致与复制集原理相同,不过存在以下差异:
- 应分别为每个片和config备份;
- 分片集备份不仅要考虑一个分片内的一致性问题,还要考虑分片间的一致性问题,因此每个片要能够恢复到同一个时间点;
- 分片集的增量备份
- 尽管理论上我们可以使用与复制集同样的方式来为分片集完成增量备份,但实际上分片集的情况更加复杂。这种复杂性来自两个方面:
- 各个数据节点的时间不一致:每个数据节点很难完全恢复到一个真正的一致时间点上,通常只能做到大致一致,而这种大致一致通常足够好,除了以下情况;
- 分片间的数据迁移:当一部分数据从一个片迁移到另一个片时最终数据到底在哪里取决于config中的元数据。
如果元数据与数据节点之间的时间差异正好导致数据实际已经迁移到新分片上,而元数据仍然认为数据在旧分片上,就会导致数据丢失情况发生。虽然这种情况发生的概率很小,但仍有可能导致问题。
- 要避免上述问题的发生,只有定期停止均衡器;只有在均衡器停止期间,增量恢复才能保证正确。
### 7. MongoDB 安全架构

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 421 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 284 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 266 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 355 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 303 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 217 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 365 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 567 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 418 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 372 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

@ -0,0 +1,81 @@
# 分布式数据库
## 1. 基础
### 1.1 什么是分布式数据库?
- 由表及里、由外到内是人们认识事物的普遍规律,所以我们让也从内外部两个视角来观察。
- 外部视角:外部特性
- 外部视角,就是看看分布式数据库具备哪些特性,能解决什么问题。
- 业务应用系统可以按照交易类型分为联机交易OLTP场景和联机分析OLAP场景两大类。OLTP 是面向交易的处理过程,单笔交易的数据量小,但是要在很短的时间内
给出结果,典型场景包括购物、缴费、转账等;而 OLAP 场景通常是基于大数据集的运算,典型场景包括生成个人年度账单和企业财务报表等。
- 定义 1.0 OLTP 关系型数据库
- OLTP 场景的通常有三个特点:
- **写多读少**,而且读操作的复杂度较低,一般不涉及大数据集的汇总计算;
- **低延时**,用户对于延时的容忍度较低,通常在 500 毫秒以内,稍微放大一些也就是秒级,超过 5 秒的延时通常是无法接受的;
- **高并发**,并发量随着业务量而增长,没有理论上限。
- **分布式数据库是服务于写多读少、低延时、高并发的 OLTP 场景的数据库。**
- 定义 2.0 + 海量并发
- 这个“海量并发”的下限大致是 10,000TPS
- 基于这些理解,我们可以再得到一个 2.0 版本的定义:**分布式数据库是服务于写多读少、低延时、海量并发 OLTP 场景的关系型数据库**。
- 定义 3.0 + 高可靠
- 可靠性还要更复杂一点包括两个度量指标恢复时间目标Recovery Time Objective, RTO和恢复点目标Recovery Point Objective,
RPO。RTO 是指故障恢复所花费的时间可以等同于可靠性RPO 则是指恢复服务后丢失数据的数量。
- 数据库高可靠意味着 RPO 等于 0RTO 小于 5 分钟。
- 3.0 版本的定义,**分布式数据库是服务于写多读少、低延时、海量并发 OLTP 场景的,高可靠的关系型数据库**。
- 定义 4.0 + 海量存储
- 最后,我们终于得到一个 4.0 终极版本的定义,**分布式数据库是服务于写多读少、低延时、海量并发 OLTP 场景的,具备海量数据存储能力和高可靠性的关系型数据库**。
- 内部视角:内部构成
- 4.0 版本的定义很相似。但是,它们向用户暴露了太多的内部复杂性。在我看来,对用户约束太多、使用过程太复杂、不够内聚的方案,不能称为成熟的产品。
- 客户端组件 + 单体数据库
- 通过独立的逻辑层建立数据分片和路由规则,实现单体数据库的初步管理,使应用能够对接多个单体数据库,实现并发、存储能力的扩展。其作为应用系统的一部分,对业务侵入
较为深。
- 这种客户端组件的典型产品是 Sharding-JDBC。
- ![分布式数据库ShardingJDBC](pic/分布式数据库ShardingJDBC.png)
- 代理中间件 + 单体数据库
- 以独立中间件的方式,管理数据规则和路由规则,以独立进程存在,与业务应用层和单体数据库相隔离,减少了对应用的影响。随着代理中间件的发展,还会衍生出部分分布式事
务处理能力。这种中间件的典型产品是 MyCat。
- ![分布式数据库MyCAT](pic/分布式数据库MyCAT.png
- 单元化架构 + 单体数据库
- 单元化架构是对业务应用系统的彻底重构,应用系统被拆分成若干实例,配置独立的单体数据库,让每个实例管理一定范围的数据。例如对于银行贷款系统,可以为每个支行搭建
独立的应用实例,管理支行各自的用户,当出现跨支行业务时,由应用层代码通过分布式事务组件保证事务的 ACID 特性。
- ![分布式数据库单元化架构](pic/分布式数据库单元化架构.png)
- 根据不同的分布式事务模型,应用系统要配合改造,复杂性也相应增加。例如 TCC 模型下,应用必须能够提供幂等操作。
- 在分布式数据库出现前,一些头部互联网公司使用过这种架构风格,该方案的应用系统的改造量最大,实施难度也最高。
- **分布式数据库则是将技术细节收敛到产品内部,以一个整体面对业务应用**。

@ -0,0 +1,21 @@
# 项目大致说明
- 微服务注册与配置中心 - (Alibaba Nacos)
- [dev-protocol-springcloud-nacos](dev-protocol-springcloud-nacos)
- 微服务应用监控 - (SpringBoot Admin)
- [dev-protocol-springcloud-admin](dev-protocol-springcloud-admin)
- 微服务通信 - (RestTemplate/Ribbon/Feign/OpenFeign)
- [dev-protocol-springcloud-communication](dev-protocol-springcloud-communication)
- 微服务网关 - (Gateway)
- [dev-protocol-springcloud-gateway](dev-protocol-springcloud-gateway)
- 分布式链路、日志追踪 - (Sleuth + Zipkin)
- todo
- 微服务容错 - (SpringCloud Netflix Hystrix)
- todo
- 消息驱动微服务 - (SpringCloud Stream)
- todo
- 分布式事务 - (SpringCloud Alibaba Seata)
- todo
- 网关动态限流 - (SpringCloud Alibaba Sentinel)
- todo
- 微服务工程部署与整体可用性验证
- todo

@ -0,0 +1,57 @@
## 1. 搭建 SpringBoot Admin 监控服务器
- 认识 SpringBoot Actuator
- Actuator Endpoints(端点)
- Endpoints 是 Actuator 的核心部分, 它用来监视应用程序及交互, SpringBoot Actuator 内置了很多 Endpoints, 并支持扩展
- SpringBoot Actuator 提供的原生端点有三类:
- 应用配置类:自动配置信息、Spring Bean 信息、yml 文件信息、环境信息等等
- 度量指标类:主要是运行期间的动态信息例如堆、健康指标、metrics 信息等等
- 操作控制类:主要是指 shutdown用户可以发送一个请求将应用的监控功能关闭
---
- 搭建 SpringBoot Admin 监控服务器
- 搭建监控服务器的步骤
- 添加 SpringBoot Admin Starter 自动配置依赖
- spring-boot-admin-starter-server
- 添加启动注解:@EnableAdminServer
---
- SpringBoot Admin 的访问地址: 127.0.0.1:7001/dev-protocol-springcloud-admin
- 其他要被监控的服务加入
```yml
spring:
# ...
cloud:
nacos:
discovery:
enabled: true
server-addr: 127.0.0.1:8848
namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *, 可以开放所有端点
endpoint:
health:
show-details:
```
## 2. 监控中心服务器添加安全访问控制
- 要记住要户名和密码, 防止之后忘记用户名和密码
## 3. SpringBoot Admin 应用监控总结
- 自定义告警
- 需要有邮箱服务器, 来进行使用邮件告警
- 其他的定制可以自己通过 AbstractEventNotifier 进行定制

@ -0,0 +1,91 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>dev-protocol</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>dev-protocol-springcloud-admin</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<!-- 模块名及描述信息 -->
<name>dev-protocol-springcloud-admin</name>
<description>监控服务器</description>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- spring cloud alibaba nacos discovery 依赖 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- SpringBoot Admin -->
<!-- 实现对 Spring Boot Admin Server 的自动化配置 -->
<!--
包含
1. spring-boot-admin-server: Server 端
2. spring-boot-admin-server-ui: UI 界面
3. spring-boot-admin-server-cloud: 对 Spring Cloud 的接入
-->
<dependency>
<groupId>de.codecentric</groupId>
<artifactId>spring-boot-admin-starter-server</artifactId>
<version>2.2.0</version>
</dependency>
<!-- 开启登录认证功能 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--
实现对 Java Mail 的自动化配置
完成自动化报警
-->
<!-- <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-mail</artifactId>
</dependency>-->
<!-- <dependency>
<groupId>com.imooc.ecommerce</groupId>
<artifactId>e-commerce-mvc-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>-->
</dependencies>
<!--
SpringBoot的Maven插件, 能够以Maven的方式为应用提供SpringBoot的支持可以将
SpringBoot应用打包为可执行的jar或war文件, 然后以通常的方式运行SpringBoot应用
-->
<build>
<finalName>${artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

@ -0,0 +1,16 @@
package org.example;
import de.codecentric.boot.admin.server.config.EnableAdminServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* <h1></h1>
* */
@EnableAdminServer
@SpringBootApplication
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
}
}

@ -0,0 +1,60 @@
package org.example.conf;
import de.codecentric.boot.admin.server.config.AdminServerProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
/**
* <h1>, 便</h1>
* Spring Security
* */
@Configuration
public class SecuritySecureConfig extends WebSecurityConfigurerAdapter {
/** 应用上下文路径 */
private final String adminContextPath;
public SecuritySecureConfig(AdminServerProperties adminServerProperties) {
this.adminContextPath = adminServerProperties.getContextPath();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
// 成功授权后的 successHandler 的处理
SavedRequestAwareAuthenticationSuccessHandler successHandler =
new SavedRequestAwareAuthenticationSuccessHandler();
successHandler.setTargetUrlParameter("redirectTo");
successHandler.setDefaultTargetUrl(adminContextPath + "/");
http.authorizeRequests()
// 1. 配置所有的静态资源和登录页可以公开访问
.antMatchers(adminContextPath + "/assets/**").permitAll()
.antMatchers(adminContextPath + "/login").permitAll()
// 2. 其他请求, 必须要经过认证
.anyRequest().authenticated()
.and()
// 3. 配置登录和登出路径
.formLogin().loginPage(adminContextPath + "/login")
.successHandler(successHandler)
.and()
.logout().logoutUrl(adminContextPath + "/logout")
.and()
// 4. 开启 http basic 支持, 其他的服务模块注册时需要使用
.httpBasic()
.and()
// 5. 开启基于 cookie 的 csrf 保护
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// 6. 忽略这些路径的 csrf 保护以便其他的模块可以实现注册
.ignoringAntMatchers(
adminContextPath + "/instances",
adminContextPath + "/actuator/**"
);
}
}

@ -0,0 +1,44 @@
package org.example.notifier;
import de.codecentric.boot.admin.server.domain.entities.Instance;
import de.codecentric.boot.admin.server.domain.entities.InstanceRepository;
import de.codecentric.boot.admin.server.domain.events.InstanceEvent;
import de.codecentric.boot.admin.server.domain.events.InstanceStatusChangedEvent;
import de.codecentric.boot.admin.server.notify.AbstractEventNotifier;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
/**
* <h1></h1>
* */
@Slf4j
@Component
@SuppressWarnings("all")
public class QNotifier extends AbstractEventNotifier {
protected QNotifier(InstanceRepository repository) {
super(repository);
}
/**
* <h2></h2>
* */
@Override
protected Mono<Void> doNotify(InstanceEvent event, Instance instance) {
return Mono.fromRunnable(() -> {
if (event instanceof InstanceStatusChangedEvent) {
// todo 当状态发生改变的时候, 后面自己设置发送邮件或者短信啥的
log.info("Instance Status Change: [{}], [{}], [{}]",
instance.getRegistration().getName(), event.getInstance(),
((InstanceStatusChangedEvent) event).getStatusInfo().getStatus());
} else {
log.info("Instance Info: [{}], [{}], [{}]",
instance.getRegistration().getName(), event.getInstance(),
event.getType());
}
});
}
}

@ -0,0 +1,56 @@
server:
port: 7001
servlet:
context-path: /dev-protocol-springcloud-admin
spring:
application:
name: dev-protocol-springcloud-admin
# 添加访问控制
security:
user:
name: baiye-test
password: 88888888
cloud:
nacos:
discovery:
enabled: true
server-addr: 127.0.0.1:8848
namespace: 1bc13fd5-843b-4ac0-aa55-695c25bc0ac6
metadata:
management:
context-path: ${server.servlet.context-path}/actuator
# 添加访问控制 和上面配置的保持一致
user.name: baiye-test
user.password: 88888888
# vue 的检查配置
thymeleaf:
check-template: false
check-template-location: false
# 被监控的应用状态变更为 DOWN、OFFLINE、UNKNOWN 时, 会自动发出告警: 实例的状态、原因、实例地址等信息
# 需要在 pom.xml 文件中添加 spring-boot-starter-mail 依赖
# 配置发送告警的邮箱服务器
# 但是, 这个要能连接上, 否则会报错
# mail:
# host: qinyi.imooc.com
# username: qinyi@imooc.com
# password: QinyiZhang
# default-encoding: UTF-8
# 监控告警通知
# boot:
# admin:
# notify:
# mail:
# from: ${spring.mail.username}
# to: qinyi@imooc.com
# cc: qinyi@imooc.com
# 暴露端点
management:
endpoints:
web:
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 *, 可以开放所有端点
endpoint:
health:
show-details: always

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

@ -0,0 +1,76 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.example</groupId>
<artifactId>dev-protocol</artifactId>
<version>1.0-SNAPSHOT</version>
<relativePath>../../pom.xml</relativePath>
</parent>
<artifactId>dev-protocol-springcloud-communication</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jjwt.version>0.9.1</jjwt.version>
</properties>
<dependencies>
<!-- ribbon -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
<!-- openfeign -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-micrometer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- feign 替换 JDK 默认的 URLConnection 为 okhttp -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- 使用原生的 Feign Api 做的自定义配置, encoder 和 decoder -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-gson</artifactId>
<version>12.1</version>
</dependency>
<!-- springboot -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!-- tools -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
</dependencies>
</project>

@ -0,0 +1,17 @@
package org.example;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.cloud.openfeign.EnableFeignClients;
@SpringBootApplication
@EnableDiscoveryClient // fixme 可以不使用服务发现的方式进行配置, 直接使用 http 的方式进行配置也可
@EnableFeignClients // 用于扫描包中使用了 @FeignClient 注解的类
@RefreshScope // 刷新配置
public class OpenFeignDemoApplication {
public static void main(String[] args) {
SpringApplication.run(OpenFeignDemoApplication.class, args);
}
}

@ -0,0 +1,19 @@
package org.example.common.constant;
/**
* <h1></h1>
* */
public final class CommonConstant {
/** RSA 公钥 */
public static final String PUBLIC_KEY = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnV+iGlE1e8Z825G+ChIwRJ2H2jOMCBu" +
"HV7BPrUE8dAGjqAlRtCaxMyJw7NV9NIUl/rY7RWBUQwelkGmGuQomnUAFIgN9f8UxSC6G935lo1ZoBVJWYmfs5ToXLz+fQugmqHZvF+Vc5l" +
"UEo1YapeiaymkOxDORMGjzQBoxoBt316IAwNEPIvcV+F6T+WNFJX/p5Xj48Z1rtmbOQ8ffF+pEWKZGsYg/9b+pKiqFJtuyHqwj/9oxFBE98" +
"MCu5RfK6M7Ff9/1dyNen1HKjI7Awj8ZnSceVUldcXEdnP89YagevbhtSl/+CvCsKwHq5+ZLkcuONSxE4dIFWTjxA92wJjYf9wIDAQAB";
/** JWT 中存储用户信息的 key */
public static final String JWT_USER_INFO_KEY = "e-commerce-user";
/** 授权中心的 service-id */
public static final String AUTHORITY_CENTER_SERVICE_ID = "e-commerce-authority-center";
}

@ -0,0 +1,63 @@
package org.example.common.util;
import com.alibaba.fastjson.JSON;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import org.example.common.constant.CommonConstant;
import org.example.common.vo.LoginUserInfo;
import sun.misc.BASE64Decoder;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.spec.X509EncodedKeySpec;
import java.util.Calendar;
/**
* <h1>JWT Token </h1>
* */
public class TokenParseUtil {
/**
* <h2> JWT Token LoginUserInfo </h2>
* */
public static LoginUserInfo parseUserInfoFromToken(String token) throws Exception {
if (null == token) {
return null;
}
Jws<Claims> claimsJws = parseToken(token, getPublicKey());
Claims body = claimsJws.getBody();
// 如果 Token 已经过期了, 返回 null
if (body.getExpiration().before(Calendar.getInstance().getTime())) {
return null;
}
// 返回 Token 中保存的用户信息
return JSON.parseObject(
body.get(CommonConstant.JWT_USER_INFO_KEY).toString(),
LoginUserInfo.class
);
}
/**
* <h2> JWT Token</h2>
* */
private static Jws<Claims> parseToken(String token, PublicKey publicKey) {
return Jwts.parser().setSigningKey(publicKey).parseClaimsJws(token);
}
/**
* <h2> PublicKey </h2>
* */
private static PublicKey getPublicKey() throws Exception {
X509EncodedKeySpec keySpec = new X509EncodedKeySpec(
new BASE64Decoder().decodeBuffer(CommonConstant.PUBLIC_KEY)
);
return KeyFactory.getInstance("RSA").generatePublic(keySpec);
}
}

@ -0,0 +1,36 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* <h1></h1>
* {
* "code": 0,
* "message": "",
* "data": {}
* }
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResponse<T> implements Serializable {
/** 错误码 */
private Integer code;
/** 错误消息 */
private String message;
/** 泛型响应数据 */
private T Data;
public CommonResponse(Integer code, String message) {
this.code = code;
this.message = message;
}
}

@ -0,0 +1,17 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <h1> Token</h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class JwtToken {
/** JWT */
private String token;
}

@ -0,0 +1,20 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <h1></h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUserInfo {
/** 用户 id */
private Long id;
/** 用户名 */
private String username;
}

@ -0,0 +1,20 @@
package org.example.common.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* <h1></h1>
* */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsernameAndPassword {
/** 用户名 */
private String username;
/** 密码 */
private String password;
}

@ -0,0 +1,70 @@
package org.example.controller;
import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword;
import org.example.feign.UseFeignApi;
import org.example.service.AuthorityFeignClient;
import org.example.service.UseRestTemplateService;
import org.example.service.UseRibbonService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* <h1> Controller</h1>
* */
@RestController
@RequestMapping("/communication")
public class CommunicationController {
private final UseRestTemplateService restTemplateService;
private final UseRibbonService ribbonService;
private final AuthorityFeignClient feignClient;
private final UseFeignApi useFeignApi;
public CommunicationController(UseRestTemplateService restTemplateService,
UseRibbonService ribbonService,
AuthorityFeignClient feignClient,
UseFeignApi useFeignApi
) {
this.restTemplateService = restTemplateService;
this.ribbonService = ribbonService;
this.feignClient = feignClient;
this.useFeignApi = useFeignApi;
}
@PostMapping("/rest-template")
public JwtToken getTokenFromAuthorityService(
@RequestBody UsernameAndPassword usernameAndPassword) {
return restTemplateService.getTokenFromAuthorityService(usernameAndPassword);
}
@PostMapping("/rest-template-load-balancer")
public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(
@RequestBody UsernameAndPassword usernameAndPassword) {
return restTemplateService.getTokenFromAuthorityServiceWithLoadBalancer(
usernameAndPassword);
}
@PostMapping("/ribbon")
public JwtToken getTokenFromAuthorityServiceByRibbon(
@RequestBody UsernameAndPassword usernameAndPassword) {
return ribbonService.getTokenFromAuthorityServiceByRibbon(usernameAndPassword);
}
@PostMapping("/thinking-in-ribbon")
public JwtToken thinkingInRibbon(@RequestBody UsernameAndPassword usernameAndPassword) {
return ribbonService.thinkingInRibbon(usernameAndPassword);
}
@PostMapping("/token-by-feign")
public JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword) {
return feignClient.getTokenByFeign(usernameAndPassword);
}
@PostMapping("/thinking-in-feign")
public JwtToken thinkingInFeign(@RequestBody UsernameAndPassword usernameAndPassword) {
return useFeignApi.thinkingInFeign(usernameAndPassword);
}
}

@ -0,0 +1,17 @@
package org.example.feign;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
*
*/
@Component
@FeignClient(name = "nacos-provider", configuration = MyFeignConfiguration.class) // name 对应的是服务注册中心的服务名称
public interface EchoService {
@GetMapping(value = "/echo/{string}")
String echo(@PathVariable String string);
}

@ -0,0 +1,13 @@
package org.example.feign;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
/**
*
*/
public interface EchoServiceManually {
@GetMapping(value = "/echo/{string}")
public String echo(@PathVariable String string);
}

@ -0,0 +1,38 @@
package org.example.feign;
import feign.Client;
import feign.Contract;
import feign.Feign;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.context.annotation.Import;
@Import(FeignClientsConfiguration.class)
public class FeignClientsConfig {
@Autowired
private Client client;
@Autowired
private Encoder encoder;
@Autowired
private Decoder decoder;
@Autowired
private Contract contract;
public EchoServiceManually buidService(){
EchoServiceManually serviceManually = Feign.builder()
.client(client)
.encoder(encoder)
.decoder(decoder)
.contract(contract)
// url : ip+port 不可以, 使用服务名称
// .target(EchoServiceManually.class, "http://192.168.10.227:8081");
.target(EchoServiceManually.class, "http://nacos-provider");
return serviceManually;
}
}

@ -0,0 +1,57 @@
package org.example.feign;
import feign.Logger;
import feign.Request;
import feign.Retryer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* <h1>OpenFeign </h1>
* */
@Configuration
public class FeignConfig {
/**
* <h2> OpenFeign </h2>
* */
@Bean
public Logger.Level feignLogger() {
return Logger.Level.FULL; // 需要注意, 日志级别需要修改成 debug, 因为 feign 基本都是 debug 级别的日志, info 的日志很少
}
/**
* <h2>OpenFeign </h2>
* period = 100 , ms
* maxPeriod = 1000 , ms
* maxAttempts = 5
* */
@Bean
public Retryer feignRetryer() {
return new Retryer.Default(
100,
SECONDS.toMillis(1),
5
);
}
public static final int CONNECT_TIMEOUT_MILLS = 5000;
public static final int READ_TIMEOUT_MILLS = 5000;
/**
* <h2></h2>
* */
@Bean
public Request.Options options() {
return new Request.Options(
CONNECT_TIMEOUT_MILLS, TimeUnit.MICROSECONDS,
READ_TIMEOUT_MILLS, TimeUnit.MILLISECONDS,
// 转发请求是否要进行限制
true
);
}
}

@ -0,0 +1,39 @@
package org.example.feign;
import feign.Feign;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* <h1>OpenFeign 使 OkHttp </h1>
* Feign
* */
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
/**
* <h2> OkHttp, </h2>
* */
@Bean
public okhttp3.OkHttpClient okHttpClient() {
return new OkHttpClient.Builder()
.connectTimeout(5, TimeUnit.SECONDS) // 设置连接超时
.readTimeout(5, TimeUnit.SECONDS) // 设置读超时
.writeTimeout(5, TimeUnit.SECONDS) // 设置写超时
.retryOnConnectionFailure(true) // 是否自动重连
// 配置连接池中的最大空闲线程个数为 10, 并保持 5 分钟
.connectionPool(new ConnectionPool(
10, 5L, TimeUnit.MINUTES))
.build();
}
}

@ -0,0 +1,87 @@
package org.example.feign;
import feign.Feign;
import feign.Logger;
import feign.gson.GsonDecoder;
import feign.gson.GsonEncoder;
import lombok.extern.slf4j.Slf4j;
import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword;
import org.example.service.AuthorityFeignClient;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.lang.annotation.Annotation;
import java.util.Random;
/**
* <h1>使 Feign Api, OpenFeign = Feign + Ribbon</h1>
* */
@Slf4j
@Service
public class UseFeignApi {
private final DiscoveryClient discoveryClient;
public UseFeignApi(DiscoveryClient discoveryClient) {
this.discoveryClient = discoveryClient;
}
/**
* <h2>使 Feign api </h2>
* Feign
* */
public JwtToken thinkingInFeign(UsernameAndPassword usernameAndPassword) {
// 通过反射去拿 serviceId
String serviceId = null;
Annotation[] annotations = AuthorityFeignClient.class.getAnnotations();
for (Annotation annotation : annotations) {
if (annotation.annotationType().equals(FeignClient.class)) {
serviceId = ((FeignClient) annotation).value();
log.info("get service id from AuthorityFeignClient: [{}]", serviceId);
break;
}
}
// 如果服务 id 不存在, 直接抛异常
if (null == serviceId) {
throw new RuntimeException("can not get serviceId");
}
// 通过 serviceId 去拿可用服务实例 OpenFeign 是使用 Ribbon 做这个事
List<ServiceInstance> targetInstances = discoveryClient.getInstances(serviceId);
if (CollectionUtils.isEmpty(targetInstances)) {
throw new RuntimeException("can not get target instance from serviceId: " +
serviceId);
}
// 随机选择一个服务实例: 负载均衡
ServiceInstance randomInstance = targetInstances.get(
new Random().nextInt(targetInstances.size())
);
log.info("choose service instance: [{}], [{}], [{}]", serviceId,
randomInstance.getHost(), randomInstance.getPort());
// Feign 客户端初始化, 必须要配置 encoder、decoder、contract
// 默认的 encoder、decoder 不支持对象,不能实现编解码, 所以要自己进行定义
// 默认的 contract 默认的协议不支持 SpringCloud 对应的 Feign 接口
AuthorityFeignClient feignClient = Feign.builder() // 1. Feign 默认配置初始化
.encoder(new GsonEncoder()) // 2.1 设置自定义配置
.decoder(new GsonDecoder()) // 2.2 设置自定义配置
.logLevel(Logger.Level.FULL) // 2.3 设置自定义配置
.contract(new SpringMvcContract())
.target( // 3 生成代理对象
AuthorityFeignClient.class,
String.format("http://%s:%s",
randomInstance.getHost(), randomInstance.getPort())
);
return feignClient.getTokenByFeign(usernameAndPassword);
}
}

@ -0,0 +1,22 @@
package org.example.ribbon;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
/**
* <h1>使 Ribbon , RestTemplate</h1>
* */
@Component
public class RibbonConfig {
/**
* <h2> RestTemplate</h2>
* */
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

@ -0,0 +1,31 @@
package org.example.service;
import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
/**
* <h1> Authority Feign Client </h1>
* */
@FeignClient(
// contextId 是对 FeignClient 的声明, 每一个进行的通信都要进行定义, value 表示需要进行通信的服务id是什么
contextId = "AuthorityFeignClient", value = "e-commerce-authority-center"
// fallback = AuthorityFeignClientFallback.class
// fallbackFactory = AuthorityFeignClientFallbackFactory.class
)
public interface AuthorityFeignClient {
/**
* <h2> OpenFeign 访 Authority Token</h2>
*
* value , ip + port
* consumes, produces: , , 使 Api ,
* */
@RequestMapping(value = "/ecommerce-authority-center/authority/token",
method = RequestMethod.POST,
consumes = "application/json", produces = "application/json")
JwtToken getTokenByFeign(@RequestBody UsernameAndPassword usernameAndPassword);
}

@ -0,0 +1,80 @@
package org.example.service;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.example.common.constant.CommonConstant;
import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
/**
* <h1>使 RestTemplate </h1>
* */
@Slf4j
@Service
public class UseRestTemplateService {
private final LoadBalancerClient loadBalancerClient;
public UseRestTemplateService(LoadBalancerClient loadBalancerClient) {
this.loadBalancerClient = loadBalancerClient;
}
/**
* <h2> JwtToken</h2>
* */
public JwtToken getTokenFromAuthorityService(UsernameAndPassword usernameAndPassword) {
// 第一种方式: 写死 url
String requestUrl = "http://127.0.0.1:7000/ecommerce-authority-center" +
"/authority/token";
log.info("RestTemplate request url and body: [{}], [{}]",
requestUrl, JSON.toJSONString(usernameAndPassword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new RestTemplate().postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
/**
* <h2> JwtToken, </h2>
* */
public JwtToken getTokenFromAuthorityServiceWithLoadBalancer(
UsernameAndPassword usernameAndPassword
) {
// 第二种方式: 通过注册中心拿到服务的信息(是所有的实例), 再去发起调用
ServiceInstance serviceInstance = loadBalancerClient.choose(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
log.info("Nacos Client Info: [{}], [{}], [{}]",
serviceInstance.getServiceId(), serviceInstance.getInstanceId(),
JSON.toJSONString(serviceInstance.getMetadata()));
String requestUrl = String.format(
"http://%s:%s/ecommerce-authority-center/authority/token",
serviceInstance.getHost(),
serviceInstance.getPort()
);
log.info("login request url and body: [{}], [{}]", requestUrl,
JSON.toJSONString(usernameAndPassword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
return new RestTemplate().postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
}

@ -0,0 +1,116 @@
package org.example.service;
import com.alibaba.fastjson.JSON;
import com.netflix.loadbalancer.*;
import com.netflix.loadbalancer.reactive.LoadBalancerCommand;
import lombok.extern.slf4j.Slf4j;
import org.example.common.constant.CommonConstant;
import org.example.common.vo.JwtToken;
import org.example.common.vo.UsernameAndPassword;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.discovery.DiscoveryClient;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import rx.Observable;
import java.util.ArrayList;
import java.util.List;
/**
* <h1>使 Ribbon </h1>
* */
@Slf4j
@Service
public class UseRibbonService {
private final RestTemplate restTemplate;
private final DiscoveryClient discoveryClient;
public UseRibbonService(RestTemplate restTemplate,
DiscoveryClient discoveryClient) {
this.restTemplate = restTemplate;
this.discoveryClient = discoveryClient;
}
/**
* <h2> Ribbon Authority Token, [{使}]</h2>
* */
public JwtToken getTokenFromAuthorityServiceByRibbon(
UsernameAndPassword usernameAndPassword) {
// 注意到 url 中的 ip 和端口换成了服务名称
String requestUrl = String.format(
"http://%s/ecommerce-authority-center/authority/token",
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
log.info("login request url and body: [{}], [{}]", requestUrl,
JSON.toJSONString(usernameAndPassword));
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 这里一定要使用自己注入的 RestTemplate
return restTemplate.postForObject(
requestUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
JwtToken.class
);
}
/**
* <h2>使 Ribbon Api, Ribbon : + [{使}]</h2>
* */
public JwtToken thinkingInRibbon(UsernameAndPassword usernameAndPassword) {
String urlFormat = "http://%s/ecommerce-authority-center/authority/token";
// 1. 找到服务提供方的地址和端口号
List<ServiceInstance> targetInstances = discoveryClient.getInstances(
CommonConstant.AUTHORITY_CENTER_SERVICE_ID
);
// 构造 Ribbon 服务列表
List<Server> servers = new ArrayList<>(targetInstances.size());
targetInstances.forEach(i -> {
servers.add(new Server(i.getHost(), i.getPort()));
log.info("found target instance: [{}] -> [{}]", i.getHost(), i.getPort());
});
// 2. 使用负载均衡策略实现远端服务调用
// 构建 Ribbon 负载实例
BaseLoadBalancer loadBalancer = LoadBalancerBuilder.newBuilder()
.buildFixedServerListLoadBalancer(servers);
// 设置负载均衡策略 - 重试的策略
loadBalancer.setRule(new RetryRule(new RandomRule(), 300));
// 发起请求
String result = LoadBalancerCommand.builder().withLoadBalancer(loadBalancer)
.build().submit(server -> {
String targetUrl = String.format(
urlFormat,
String.format("%s:%s", server.getHost(), server.getPort())
);
log.info("target request url: [{}]", targetUrl);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
String tokenStr = new RestTemplate().postForObject(
targetUrl,
new HttpEntity<>(JSON.toJSONString(usernameAndPassword), headers),
String.class
);
return Observable.just(tokenStr);
}).toBlocking().first().toString();
return JSON.parseObject(result, JwtToken.class);
}
}

@ -0,0 +1,18 @@
# Feign 的相关配置
feign:
# feign 开启 gzip 压缩
compression:
request:
enabled: true # 对请求的数据开启压缩
mime-types: text/xml,application/xml,application/json # 压缩的数据类型
min-request-size: 1024 # 最小多大的请求数据开启压缩
response:
enabled: true # 对响应的数据开启压缩
# 禁用默认的 http, 启用 okhttp
httpclient:
enabled: false
okhttp:
enabled: true
# # OpenFeign 集成 Hystrix
# hystrix:
# enabled: true

@ -0,0 +1,56 @@
### 获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/rest-template
Content-Type: application/json
{
"username": "123@126.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 获取 Token, 带有负载均衡
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/rest-template-load-balancer
Content-Type: application/json
{
"username": "123@126.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 通过 Ribbon 去获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/ribbon
Content-Type: application/json
{
"username": "Qinyi@imooc.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 通过原生 Ribbon Api 去获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/thinking-in-ribbon
Content-Type: application/json
{
"username": "Qinyi@imooc.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 通过 OpenFeign 获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/token-by-feign
Content-Type: application/json
{
"username": "123@126.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}
### 通过原生 Feign Api 获取 Token
POST http://127.0.0.1:8000/ecommerce-nacos-client/communication/thinking-in-feign
Content-Type: application/json
{
"username": "123@126.com",
"password": "25d55ad283aa400af464c76d713c07ad"
}

@ -0,0 +1,21 @@
package org.example;
import lombok.extern.slf4j.Slf4j;
import org.example.feign.EchoService;
import org.junit.Test;
import org.springframework.boot.test.context.SpringBootTest;
import javax.annotation.Resource;
@Slf4j
@SpringBootTest
class EchoServiceManuallyTest {
@Resource
private EchoService echoService;
@Test
public void testOpenFeignInit(){
log.info("echoService = {}", echoService);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save