开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

【实战🚀】手摸手教你Bomb Lab

发表于 2020-05-25

手摸手教你Bomb Lab

阅读须知: 网上关于CMU配套教材开源的版本的bomb的教程已经很多了,比如我科学长的这两篇写得相当好

深入理解计算机系统(CS:APP) - Bomb Lab详解, 深入理解计算机系统BombLab实验报告. 我也不想做重复的劳动,去写别人已经写过的东西,这一篇博客是基于学校给我发的bomb版本来写的, 相关的可执行文件等实验结束后会上传到我的github上面, 如果你想要一个其他版本的bomb来练习的话,那么可以用我的这一份.

这个实验一共有6个阶段以及一个隐藏阶段,我目前没有做那个隐藏阶段,如果老师说做隐藏阶段加分的话,我或许会补上^_^! ps:写博客比做实验花时间多系列QwQ

首先搬运一下指导书上面的实验简介:

本实验中,你要使用课程所学知识拆除一个“binary bombs”来增强对程序的机器级表示、汇编语言、调试器和逆向工程等方面原理与技能的掌握。

一个“binary bombs”(二进制炸弹,下文将简称为炸弹)是一个Linux可执行C程序,包含了6个阶段(phase1~phase6)。炸弹运行的每个阶段要求你输入一个特定的字符串,若你的输入符合程序预期的输入,该阶段的炸弹就被“拆除”,否则炸弹“爆炸”并打印输出 “BOOM!!!”字样。实验的目标是拆除尽可能多的炸弹层次。

每个炸弹阶段考察了机器级语言程序的一个不同方面,难度逐级递增:

* 阶段1:字符串比较

* 阶段2:循环

* 阶段3:条件/分支

* 阶段4:递归调用和栈

* 阶段5:指针

* 阶段6:链表/指针/结构

另外还有一个隐藏阶段,但只有当你在第4阶段的解之后附加一特定字符串后才会出现。

为了完成二进制炸弹拆除任务,你需要使用gdb调试器和objdump来反汇编炸弹的可执行文件,并单步跟踪调试每一阶段的机器代码,从中理解每一汇编语言代码的行为或作用,进而设法“推断”出拆除炸弹所需的目标字符串。这可能需要你在每一阶段的开始代码前和引爆炸弹的函数前设置断点,以便于调试。

实验语言:C语言

实验环境:linux

然后对实验中用到的一些文件做一个简单的说明:

  • bomb:bomb的可执行程序。
  • bomb.c:bomb程序的main函数。

「运行./bomb」:./bomb是一个可执行程序,需要0或1个命令行参数(详见bomb.c源文件中的main()函数)。如果运行时不指定参数,则该程序打印出欢迎信息后,期待你按行输入每一阶段用来拆除炸弹的字符串,并根据你当前输入的字符串决定你是通过相应阶段还是炸弹爆炸导致任务失败。

你也可将拆除每一阶段炸弹的字符串按行组织在一个文本文件中(比如:result.txt),然后作为运行程序时的唯一一个命令行参数传给程序(./bomb result.txt),程序会自动读取文本文件中的字符串,并依次检查对应每一阶段的字符串来决定炸弹拆除成败。

正文开始

首先将可执行程序bomb进行反汇编:

1
复制代码objdump -d bomb > disassemble.asm

然后用你喜欢的编辑器打开这个文件, 我用的是VsCode

可以装这个插件来高亮显示代码


主函数比较长,反正我是没看,直接Ctrl + F搜索phase_1进入阶段1

阶段一

阶段1比较简单,我分配到的题目如下所示:

1
2
3
4
5
6
7
8
9
10
11
复制代码08048b33 <phase_1>:
8048b33: 83 ec 14 sub $0x14,%esp
8048b36: 68 a4 9f 04 08 push $0x8049fa4
8048b3b: ff 74 24 1c pushl 0x1c(%esp)
8048b3f: e8 5e 04 00 00 call 8048fa2 <strings_not_equal>
8048b44: 83 c4 10 add $0x10,%esp
8048b47: 85 c0 test %eax,%eax
8048b49: 74 05 je 8048b50 <phase_1+0x1d>
8048b4b: e8 49 05 00 00 call 8049099 <explode_bomb>
8048b50: 83 c4 0c add $0xc,%esp
8048b53: c3 ret

我们来分析一下这个代码,可以看到有两个push操作,然后有一个call操作,所以猜测这两个push操作是在为这个strings_not_equal函数传递参数, 我们可以用gdb调试一下看看是不是这样:

1
2
3
4
5
复制代码gdb bomb
b phase_1 //打断点
b explode_bomb
run
layout asm //提供图形界面, 可以更好的观察执行到哪一条语句了

我输入的字符串是hello, 然后通过调试可以看到,0x1c(%esp)压进栈中的字符串就是hello

1
复制代码$1 = 0x804c3e0 <input_strings> "hello"

所以我们可以猜测我们输入的字符串需要与0x8049fa4处的字符串进行比对,如果相等,那么就可以成功拆除炸弹了,现在查看这个地址处的字符串

1
复制代码x /s 0x8049fa4

执行后获得以下的输出, All your base are belong to us.即为正确答案

1
2
复制代码(gdb) x /s 0x8049fa4
0x8049fa4: "All your base are belong to us."

总的来说,这个任务还算是非常简单的.

阶段二

阶段2考察的是循环,代码量相对于阶段1要大很多,首先贴一下阶段二完整的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
复制代码08048b54 <phase_2>:
8048b54: 56 push %esi
8048b55: 53 push %ebx
8048b56: 83 ec 2c sub $0x2c,%esp
8048b59: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048b5f: 89 44 24 24 mov %eax,0x24(%esp)
8048b63: 31 c0 xor %eax,%eax
8048b65: 8d 44 24 0c lea 0xc(%esp),%eax
8048b69: 50 push %eax
8048b6a: ff 74 24 3c pushl 0x3c(%esp)
8048b6e: e8 4b 05 00 00 call 80490be <read_six_numbers>
8048b73: 83 c4 10 add $0x10,%esp
8048b76: 83 7c 24 04 01 cmpl $0x1,0x4(%esp)
8048b7b: 74 05 je 8048b82 <phase_2+0x2e>
8048b7d: e8 17 05 00 00 call 8049099 <explode_bomb>
8048b82: 8d 5c 24 04 lea 0x4(%esp),%ebx
8048b86: 8d 74 24 18 lea 0x18(%esp),%esi
8048b8a: 8b 03 mov (%ebx),%eax
8048b8c: 01 c0 add %eax,%eax
8048b8e: 39 43 04 cmp %eax,0x4(%ebx)
8048b91: 74 05 je 8048b98 <phase_2+0x44>
8048b93: e8 01 05 00 00 call 8049099 <explode_bomb>
8048b98: 83 c3 04 add $0x4,%ebx
8048b9b: 39 f3 cmp %esi,%ebx
8048b9d: 75 eb jne 8048b8a <phase_2+0x36>
8048b9f: 8b 44 24 1c mov 0x1c(%esp),%eax
8048ba3: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048baa: 74 05 je 8048bb1 <phase_2+0x5d>
8048bac: e8 df fb ff ff call 8048790 <__stack_chk_fail@plt>
8048bb1: 83 c4 24 add $0x24,%esp
8048bb4: 5b pop %ebx
8048bb5: 5e pop %esi
8048bb6: c3 ret

开始的几行就是一些准备工作,不用去研究它是干什么的,爷也不想研究.

下面从第8行开始看起,第8行用了一条lea语句,这是获得0xc(%esp)处的地址, 然后就把这个地址压栈了, 可以猜测这个地址是用来存放读取的6个数字的,后面的一个pushl是干啥的我也8太清楚.然后调用read_six_numbers函数读取6个数字.

第13行, cmpl $0x1,0x4(%esp)这一条语句是比较读入的第一个参数和1的大小关系,至于为什么是第一个参数,可以通过自己模拟栈的push pop知道. 这里第一个参数必须等于1, 否则就会引爆炸弹. 然后跳转到16行进行执行,.

16, 17行分别将指向第1个数和第6个数的指针存进了ebx 和 esi寄存器中. 从这里可以看出, esi寄存器的值可以看做循环终止的条件.

18, 19行两行是将ebx寄存器中的数字变为原来的两倍后放进了eax寄存器中,然后比较ebx寄存器中数字的后一个数字是否与eax寄存器中的值相等, 也就是比较后一个数字是否是前一个数字的两倍, 如果不是, 那么引爆炸弹, 否则循环继续, 指针往后挪一位, 知道挪到了最后一个.

所以最后的答案就是1 2 4 8 16 32, 这一题也比较简单

阶段三

阶段三考察的是条件与分支, 也比较简单, 先来看一看阶段三的源代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
复制代码 8048bb7:	83 ec 1c             	sub    $0x1c,%esp
8048bba: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048bc0: 89 44 24 0c mov %eax,0xc(%esp)
8048bc4: 31 c0 xor %eax,%eax
8048bc6: 8d 44 24 08 lea 0x8(%esp),%eax
8048bca: 50 push %eax
8048bcb: 8d 44 24 08 lea 0x8(%esp),%eax
8048bcf: 50 push %eax
8048bd0: 68 6f a1 04 08 push $0x804a16f
8048bd5: ff 74 24 2c pushl 0x2c(%esp)
8048bd9: e8 32 fc ff ff call 8048810 <__isoc99_sscanf@plt>
8048bde: 83 c4 10 add $0x10,%esp
8048be1: 83 f8 01 cmp $0x1,%eax
8048be4: 7f 05 jg 8048beb <phase_3+0x34>
8048be6: e8 ae 04 00 00 call 8049099 <explode_bomb>
8048beb: 83 7c 24 04 07 cmpl $0x7,0x4(%esp)
8048bf0: 77 3c ja 8048c2e <phase_3+0x77>
8048bf2: 8b 44 24 04 mov 0x4(%esp),%eax
8048bf6: ff 24 85 00 a0 04 08 jmp *0x804a000(,%eax,4)
8048bfd: b8 f3 01 00 00 mov $0x1f3,%eax
8048c02: eb 3b jmp 8048c3f <phase_3+0x88>
8048c04: b8 c0 00 00 00 mov $0xc0,%eax
8048c09: eb 34 jmp 8048c3f <phase_3+0x88>
8048c0b: b8 57 01 00 00 mov $0x157,%eax
8048c10: eb 2d jmp 8048c3f <phase_3+0x88>
8048c12: b8 9b 01 00 00 mov $0x19b,%eax
8048c17: eb 26 jmp 8048c3f <phase_3+0x88>
8048c19: b8 5c 03 00 00 mov $0x35c,%eax
8048c1e: eb 1f jmp 8048c3f <phase_3+0x88>
8048c20: b8 38 02 00 00 mov $0x238,%eax
8048c25: eb 18 jmp 8048c3f <phase_3+0x88>
8048c27: b8 67 01 00 00 mov $0x167,%eax
8048c2c: eb 11 jmp 8048c3f <phase_3+0x88>
8048c2e: e8 66 04 00 00 call 8049099 <explode_bomb>
8048c33: b8 00 00 00 00 mov $0x0,%eax
8048c38: eb 05 jmp 8048c3f <phase_3+0x88>
8048c3a: b8 83 02 00 00 mov $0x283,%eax
8048c3f: 3b 44 24 08 cmp 0x8(%esp),%eax
8048c43: 74 05 je 8048c4a <phase_3+0x93>
8048c45: e8 4f 04 00 00 call 8049099 <explode_bomb>
8048c4a: 8b 44 24 0c mov 0xc(%esp),%eax
8048c4e: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048c55: 74 05 je 8048c5c <phase_3+0xa5>
8048c57: e8 34 fb ff ff call 8048790 <__stack_chk_fail@plt>
8048c5c: 83 c4 1c add $0x1c,%esp
8048c5f: c3 ret

从第5行开始看, 这里的两个lea操作很显然就是传地址, 类似与&a, 然后供sacnf函数使用

这里注意第9行push $0x804a16f, 这里压入了一个地址常量, 根据后面调用的scanf猜测这个应该是格式字符串, 我们通过gdb查看这个字符串的值

1
2
复制代码(gdb) x /s 0x804a16f
0x804a16f: "%d %d"

从输出可以看到, scanf接受的是两个int类型的整数.

接着看第13行, 有一个eax寄存器与1的比较操作, 一般eax寄存器存储函数的返回值, 而sacnf函数的返回值就是成功读取的元素个数,于是可知,这里读取的元素个数小于等于1的话就会引爆炸弹.

接着看第16行, 是比较读入的第一个参数和7的大小关系, 注意后面一行的ja是无符号比较, 所以可知第一个参数的1值必须在0~7之间, 否则就会引爆炸弹

接着的18, 19行, 有意思的地方来了, 这里是一个典型的跳转表的实现,比如中断向量表, switch都是用的这种思想.比如说我输入的第一个数字是1, 那么我跳转到的地址需要在gdb中通过如下方式获得:

1
2
复制代码(gdb) print /x *(int*)(0x804a000+4)
$1 = 0x8048bfd

然后再在19~36行中寻找对应的地址跳转过去就行, 比如0x8048bfd是第20行,对应的代码是

1
2
复制代码8048bfd:	b8 f3 01 00 00       	mov    $0x1f3,%eax
8048c02: eb 3b jmp 8048c3f <phase_3+0x88>

所以eax的值就是0x1f3, 也就是499

通过第38行的代码

1
2
3
复制代码 8048c3f:	3b 44 24 08          	cmp    0x8(%esp),%eax
8048c43: 74 05 je 8048c4a <phase_3+0x93>
8048c45: e8 4f 04 00 00 call 8049099 <explode_bomb>

可以知道, 我们需要输入的第二个数与eax寄存器中的值相等,不然就会引爆炸弹, 于是一个可能的答案就是1 499, 当然, 还有其他的答案.

阶段四

阶段四需要你进行逆向出对应的C代码, 这样解决起来会简单许多,这一题我很快就逆向出C代码了,但是没想到被scanf函数坑了, 它的第一个参数是你输入的第二个, 第二个参数是你输入的第一个, 搞得我还一直以为我逆向错了,后来在车大牛的提醒下, 单步调试终于发现了这个坑爹的BUG

下面我就来讲一讲我是如何逆向的, 以及我是如何调试的

首先我们先贴上phase_4的代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
复制代码08048ca3 <phase_4>:
8048ca3: 83 ec 1c sub $0x1c,%esp
8048ca6: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048cac: 89 44 24 0c mov %eax,0xc(%esp)
8048cb0: 31 c0 xor %eax,%eax
8048cb2: 8d 44 24 04 lea 0x4(%esp),%eax
8048cb6: 50 push %eax
8048cb7: 8d 44 24 0c lea 0xc(%esp),%eax
8048cbb: 50 push %eax
8048cbc: 68 6f a1 04 08 push $0x804a16f
8048cc1: ff 74 24 2c pushl 0x2c(%esp)
8048cc5: e8 46 fb ff ff call 8048810 <__isoc99_sscanf@plt>
8048cca: 83 c4 10 add $0x10,%esp
8048ccd: 83 f8 02 cmp $0x2,%eax
8048cd0: 75 0c jne 8048cde <phase_4+0x3b>
8048cd2: 8b 44 24 04 mov 0x4(%esp),%eax
8048cd6: 83 e8 02 sub $0x2,%eax
8048cd9: 83 f8 02 cmp $0x2,%eax
8048cdc: 76 05 jbe 8048ce3 <phase_4+0x40>
8048cde: e8 b6 03 00 00 call 8049099 <explode_bomb>
8048ce3: 83 ec 08 sub $0x8,%esp
8048ce6: ff 74 24 0c pushl 0xc(%esp)
8048cea: 6a 09 push $0x9
8048cec: e8 6f ff ff ff call 8048c60 <func4>
8048cf1: 83 c4 10 add $0x10,%esp
8048cf4: 3b 44 24 08 cmp 0x8(%esp),%eax
8048cf8: 74 05 je 8048cff <phase_4+0x5c>
8048cfa: e8 9a 03 00 00 call 8049099 <explode_bomb>
8048cff: 8b 44 24 0c mov 0xc(%esp),%eax
8048d03: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048d0a: 74 05 je 8048d11 <phase_4+0x6e>
8048d0c: e8 7f fa ff ff call 8048790 <__stack_chk_fail@plt>
8048d11: 83 c4 1c add $0x1c,%esp
8048d14: c3 ret

从第6行开始看起, 这里是熟悉的lea, 熟悉的push, 熟悉的scanf, 然后用相同的伎俩可以知道格式化字符串还是%d %d, 于是我们知道这里也是读入两个数字, 但是, 这里先push进去的是低地址的那一个…

14行比较了一下返回值, 不是2就引爆炸弹, 也再一次证明了要读两个数

16-18行比较了scanf的第一个参数, 也就是我们输入的第二个数减2后与2的大小关系, 这里需要特别注意19行是jbe, 是无符号数比较, 所以我们输入的第二个参数的值必须在2~4之间.

21~23行是在为调用func4准备参数, 查看func4的源代码后我们可以知道,先push进去的是右边的参数

假设我们输入的两个数是b a, 那么这里的函数调用就是func4(9, a), 至于为什么你自己画一下这里的栈就知道了, 电脑上画图太麻烦, 我就不画了.(对了, 在自己分析栈时要注意函数调用时会将返回地址压栈)

我们先不看func4, 先接着看phase_4, 由26到28行我们可以看到我们需要我们输入的第一个参数的值等于调用func4后的返回值, 不然就会引爆炸弹, 于是, 接下来, 我们逆向func4

先上func4的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码08048c60 <func4>:
8048c60: 57 push %edi
8048c61: 56 push %esi
8048c62: 53 push %ebx
8048c63: 8b 5c 24 10 mov 0x10(%esp),%ebx
8048c67: 8b 7c 24 14 mov 0x14(%esp),%edi
8048c6b: 85 db test %ebx,%ebx
8048c6d: 7e 2b jle 8048c9a <func4+0x3a>
8048c6f: 89 f8 mov %edi,%eax
8048c71: 83 fb 01 cmp $0x1,%ebx
8048c74: 74 29 je 8048c9f <func4+0x3f>
8048c76: 83 ec 08 sub $0x8,%esp
8048c79: 57 push %edi
8048c7a: 8d 43 ff lea -0x1(%ebx),%eax
8048c7d: 50 push %eax
8048c7e: e8 dd ff ff ff call 8048c60 <func4>
8048c83: 83 c4 08 add $0x8,%esp
8048c86: 8d 34 07 lea (%edi,%eax,1),%esi
8048c89: 57 push %edi
8048c8a: 83 eb 02 sub $0x2,%ebx
8048c8d: 53 push %ebx
8048c8e: e8 cd ff ff ff call 8048c60 <func4>
8048c93: 83 c4 10 add $0x10,%esp
8048c96: 01 f0 add %esi,%eax
8048c98: eb 05 jmp 8048c9f <func4+0x3f>
8048c9a: b8 00 00 00 00 mov $0x0,%eax
8048c9f: 5b pop %ebx
8048ca0: 5e pop %esi
8048ca1: 5f pop %edi
8048ca2: c3 ret

前面几行是在为函数调用准备, 不做分析.

5, 6两行是从栈中取出参数, 如果前面的栈你自己分析了, 那么这里也不难分析得出ebx放的是第一个参数x1, edi放的是第二个参数x2.

第7, 8两行的作用就是判断x1是否≤0, 经过分析不难得出这几句汇编代码对应的C语言代码, 注意:eax寄存器存储的是函数的返回值

1
复制代码if (x1 <= 0) return 0;

接下来第9行将eax寄存器赋值为x2, 然后又对x1做了一个判断, 如果等于0, 那么跳转到第27行执行, 于是我们可以据此逆向出下面的C语言代码:

1
复制代码if (x1 == 1) return x2;

接下来调用func4(x1 - 1, x2), 由18行可知然后将返回值加上x2赋值给了esi

19~21行又在为调用func4函数准备参数, 稍加分析可以知道接下来调用的是func4(x1 -2, x2)

最后由24行可知, 将第二次调用func4的返回值加上esi的值得到最终的返回值, 于是总的逆向程序代码如下所示:

1
2
3
4
5
6
复制代码int func(int x1, int x2) {
if (x1 <= 0) return 0;
if (x1 == 1) return x2;

return func(x1 - 1, x2) + x1 + func(x1 - 2, x2);
}

非常的优雅.

当然了, 如果你对上面的有些难以理解, 那么我可以给出另一份比较笨, 但容易理解的代码, 我们注意到, 在调用的过程中eax寄存器的值是没有保存的, 所以, 我们可以把它看做全局变量, 令它为变量eax, 于是有下面的程序, 完全对应汇编语句的丑陋的C程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
复制代码int eax;

void func(int x1, int x2) {
if (x1 <= 0) {
eax = 0;
return;
}

eax = x2;
if (x1 == 1) return;
eax = x1 - 1;
func(eax, x2);
int temp = x2 + eax;
x1 -= 2;
func(x1, x2);
eax += temp;
return;
}

int main() {
printf("%d", eax);
}

如果你仔细对比的话, 可以看出这个程序的每一条语句的行为都是和汇编一一对应的, 可能更容易理解一点.

最后的结果是264 3

下面讲一下我在一开始被scanf坑到时是如何进行调试的.

首先gdb进入调试界面, 然后在explode_bomb处打一个断点, phase_4处也打一个断点, 然后单步调试, 然后观察寄存器的值, 以及是在哪里炸的, 好吧, 我在说废话, 不这么调试还怎么调试, gdb相关命令直接看文档就行, 不用背.

阶段五

你可能注意到了, 前面的程序我都没有任何的注释, 因为确实也不需要注释, 但是从阶段五开始, 我开始写注释了, 不然自己都不知道自己在看什么

不过阶段五难度还好, 我不到一个小时就做出来了.

先上阶段五的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
复制代码08048d15 <phase_5>:
8048d15: 53 push %ebx
8048d16: 83 ec 14 sub $0x14,%esp
8048d19: 8b 5c 24 1c mov 0x1c(%esp),%ebx
8048d1d: 53 push %ebx
8048d1e: e8 60 02 00 00 call 8048f83 <string_length>
8048d23: 83 c4 10 add $0x10,%esp
8048d26: 83 f8 06 cmp $0x6,%eax
8048d29: 74 05 je 8048d30 <phase_5+0x1b>
8048d2b: e8 69 03 00 00 call 8049099 <explode_bomb>
8048d30: 89 d8 mov %ebx,%eax;eax指向了字符串的开始
8048d32: 83 c3 06 add $0x6,%ebx;ebx现在指向了字符串的结尾的后一个,象征着结束标志
8048d35: b9 00 00 00 00 mov $0x0,%ecx
8048d3a: 0f b6 10 loop: movzbl (%eax),%edx;取出了eax指向的那个字符,放到了edx中,而且是0扩展
8048d3d: 83 e2 0f and $0xf,%edx;将低四位保留,高四位清零了
8048d40: 03 0c 95 20 a0 04 08 add 0x804a020(,%edx,4),%ecx;这个有点switch的感觉
8048d47: 83 c0 01 add $0x1,%eax
8048d4a: 39 d8 cmp %ebx,%eax
8048d4c: 75 ec jne 8048d3a <phase_5+0x25>
8048d4e: 83 f9 2c cmp $0x2c,%ecx;需要ecx加6次等于0x2c 10 + 10 + 10 + 10 + 2 + 2
8048d51: 74 05 je 8048d58 <phase_5+0x43>
8048d53: e8 41 03 00 00 call 8049099 <explode_bomb>
8048d58: 83 c4 08 add $0x8,%esp
8048d5b: 5b pop %ebx
8048d5c: c3 ret

这一题考察的是指针, 不做点注释我都不知道我自己指到哪里去了.

首先看到第6行, 这个函数名一看就知道是计算字符串的长度, 而这个字符串肯定就是我们输的字符串, 不要问我为什么, 男人的直觉, 就是这么准.

第8行中, 比较了eax与6的大小,eax的值就是string_length函数的返回值, 不信的话你可以自己调试看看. 所以从这里我们可以获得一个重要信息, 这个字符串的长度是6

对了, 第5行也有重要信息, 这个push语句很显然是在为调用string_length函数准备参数, 所以ebx寄存器中存储的就是指向字符串首字符的指针, 有内味了!

然后第11行的赋值让eax也指向了字符串首, 由于字符串长度为6, 所以由12行可知, 这里指向了串尾的后一个, 很显然是我们常用的str[i] != 0的结束标志

14行15行注释说的很清楚了, 就是对字符做了一些处理

第16行就是将ecx加上对应地址处的值, 这里又出现了前面的那个跳转表有木有, 我们可以在gdb中看一看那个地址处的值是啥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码(gdb) print (int)*(0x804a020)
$2 = 2
(gdb) print (int)*(0x804a024)
$3 = 10
(gdb) print (int)*(0x804a028)
$4 = 6
(gdb) print (int)*(0x804a02c)
$5 = 1
(gdb) print (int)*(0x804a030)
$6 = 12
(gdb) print (int)*(0x804a034)
$7 = 16
(gdb) print (int)*(0x804a038)
$8 = 9
(gdb) print (int)*(0x804a03c)
$9 = 3
(gdb) ...

17~19行我们可以知道一共要加6次

20行中我们可以看出加了6次后的值要等于0x2c

所以一个可能的结果就是10 + 10 + 10 + 10 + 2 + 2

10代表对应的字符低四位为1, 2代表低四位的值为0, 这个是由跳转表地址以及对应值的关系得到的,所以一个可能的答案是111100

最后, 到了这次实验中最难的一题, 我写了不少注释才看懂这一题, 这一题大概花了我两个多小时

阶段六

这一题的代码巨长无比, 如果不是因为要交作业, 我是不会去做这个的QwQ

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
复制代码08048d5d <phase_6>:
8048d5d: 56 push %esi
8048d5e: 53 push %ebx
8048d5f: 83 ec 4c sub $0x4c,%esp
8048d62: 65 a1 14 00 00 00 mov %gs:0x14,%eax
8048d68: 89 44 24 44 mov %eax,0x44(%esp)
8048d6c: 31 c0 xor %eax,%eax
8048d6e: 8d 44 24 14 lea 0x14(%esp),%eax
8048d72: 50 push %eax
8048d73: ff 74 24 5c pushl 0x5c(%esp)
8048d77: e8 42 03 00 00 call 80490be <read_six_numbers>
8048d7c: 83 c4 10 add $0x10,%esp
8048d7f: be 00 00 00 00 mov $0x0,%esi
8048d84: 8b 44 b4 0c mov 0xc(%esp,%esi,4),%eax
8048d88: 83 e8 01 sub $0x1,%eax
8048d8b: 83 f8 05 cmp $0x5,%eax
8048d8e: 76 05 jbe 8048d95 <phase_6+0x38>
8048d90: e8 04 03 00 00 call 8049099 <explode_bomb>
8048d95: 83 c6 01 add $0x1,%esi;这里就是限定了数的取值范围
8048d98: 83 fe 06 cmp $0x6,%esi
8048d9b: 74 1b je 8048db8 <phase_6+0x5b>
8048d9d: 89 f3 mov %esi,%ebx
8048d9f: 8b 44 9c 0c mov 0xc(%esp,%ebx,4),%eax
8048da3: 39 44 b4 08 cmp %eax,0x8(%esp,%esi,4)
8048da7: 75 05 jne 8048dae <phase_6+0x51>
8048da9: e8 eb 02 00 00 call 8049099 <explode_bomb>
8048dae: 83 c3 01 add $0x1,%ebx;这里限定了数字两两不能相等
8048db1: 83 fb 05 cmp $0x5,%ebx
8048db4: 7e e9 jle 8048d9f <phase_6+0x42>
8048db6: eb cc jmp 8048d84 <phase_6+0x27>
8048db8: 8d 44 24 0c lea 0xc(%esp),%eax;指向第1个数
8048dbc: 8d 5c 24 24 lea 0x24(%esp),%ebx;指向第6个数的后一个,也就是end标志
8048dc0: b9 07 00 00 00 mov $0x7,%ecx
8048dc5: 89 ca mov %ecx,%edx;an = 7 - an, 后面记得还要再变回来
8048dc7: 2b 10 sub (%eax),%edx
8048dc9: 89 10 mov %edx,(%eax)
8048dcb: 83 c0 04 add $0x4,%eax;指向下一个
8048dce: 39 c3 cmp %eax,%ebx
8048dd0: 75 f3 jne 8048dc5 <phase_6+0x68>;;
8048dd2: bb 00 00 00 00 mov $0x0,%ebx


8048dd7: eb 16 jmp 8048def <phase_6+0x92>
8048dd9: 8b 52 08 mov 0x8(%edx),%edx;大于1跳到这里, 这里目测是链表的 p = p -> next操作
8048ddc: 83 c0 01 add $0x1,%eax
8048ddf: 39 c8 cmp %ecx,%eax
8048de1: 75 f6 jne 8048dd9 <phase_6+0x7c>
8048de3: 89 54 b4 24 mov %edx,0x24(%esp,%esi,4);小于等于1跳到这里,这里的操作就是把链表指针依次放到这6个数的后面


8048de7: 83 c3 01 add $0x1,%ebx
8048dea: 83 fb 06 cmp $0x6,%ebx
8048ded: 74 17 je 8048e06 <phase_6+0xa9>;这里的处理完后,这6个数后面的链表指针的大小1排序就对应着这6个数的排序
`8048def: 89 de mov %ebx,%esi
8048df1: 8b 4c 9c 0c mov 0xc(%esp,%ebx,4),%ecx;从输入后处理完的第一个数开始遍历
8048df5: b8 01 00 00 00 mov $0x1,%eax
8048dfa: ba 3c c1 04 08 mov $0x804c13c,%edx;这个是链表的首地址
8048dff: 83 f9 01 cmp $0x1,%ecx;这个数和1进行比较
8048e02: 7f d5 jg 8048dd9 <phase_6+0x7c>;大于跳转
8048e04: eb dd jmp 8048de3 <phase_6+0x86>;小于或者等于跳转
8048e06: 8b 5c 24 24 mov 0x24(%esp),%ebx;这个值就是这6个数后面的第一个链表指针的值
8048e0a: 8d 44 24 24 lea 0x24(%esp),%eax;指向了后面第一个链表指针
8048e0e: 8d 74 24 38 lea 0x38(%esp),%esi;指向最后一个链表指针
8048e12: 89 d9 mov %ebx,%ecx
8048e14: 8b 50 04 mov 0x4(%eax),%edx;指向栈上第二个链表指针
8048e17: 89 51 08 mov %edx,0x8(%ecx);让栈上第一个链表指针指向栈上第二个链表指针
8048e1a: 83 c0 04 add $0x4,%eax
8048e1d: 89 d1 mov %edx,%ecx
8048e1f: 39 c6 cmp %eax,%esi
8048e21: 75 f1 jne 8048e14 <phase_6+0xb7>;这个循环就是让栈上这6个链表节点按栈上面的顺序建立连接关系
8048e23: c7 42 08 00 00 00 00 movl $0x0,0x8(%edx);最后一个指向null
8048e2a: be 05 00 00 00 mov $0x5,%esi
8048e2f: 8b 43 08 mov 0x8(%ebx),%eax
8048e32: 8b 00 mov (%eax),%eax;现在eax指向了第二个节点了
8048e34: 39 03 cmp %eax,(%ebx);比较这两个节点的值?
8048e36: 7d 05 jge 8048e3d <phase_6+0xe0>;前一个需要大于或者等于后面一个
8048e38: e8 5c 02 00 00 call 8049099 <explode_bomb>
8048e3d: 8b 5b 08 mov 0x8(%ebx),%ebx
8048e40: 83 ee 01 sub $0x1,%esi
8048e43: 75 ea jne 8048e2f <phase_6+0xd2>
8048e45: 8b 44 24 3c mov 0x3c(%esp),%eax
8048e49: 65 33 05 14 00 00 00 xor %gs:0x14,%eax
8048e50: 74 05 je 8048e57 <phase_6+0xfa>
8048e52: e8 39 f9 ff ff call 8048790 <__stack_chk_fail@plt>
8048e57: 83 c4 44 add $0x44,%esp
8048e5a: 5b pop %ebx
8048e5b: 5e pop %esi
8048e5c: c3 ret`

写的有点累了, 这个不太想细讲了, 注释也写了蛮多了, 注意, 注释中说的链表指的是栈上面接在那6个数后面的部分.

从第8行开始看起, 这里又是熟悉的lea操作, 熟悉的push和call操作, 这个函数读了六个数字到栈上的指定地址.

后面用了很多代码就是进行了两个很无聊的操作, 一是限定了6个数的取值范围必须在1~6, 二是限定了6个数互不相同

然后进行了变换的操作

56行注意到有一个地址常量赋值的操作, 这个地址值就是链表头所在的地址, 通过gdb我们可以看到如下的信息


每个节点占12个字节, 第一个字节就是值, 第二个字节是id号, 第三个字节就是下一个节点的地址.

后面进行了将链表的节点按一定顺序放到栈上那6个数后面的操作, 然后还进行了栈上的链表的连接关系的建立操作, 也就是重新规定了每一个节点的下一个节点是谁(是它在栈上的后一个),最后有一个重大的信息, 就是这6个节点的顺序是按照值从大到小的,, 于是我们通过上图可以知道节点的id按照节点的值从大到小的顺序为6 2 4 3 1 5, 最后执行an = 7 - an的还原操作得到最终答案 1 5 3 4 6 2!

完结撒花!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring源码分析四、Aop源码分析

发表于 2020-05-25

引入

1
2
复制代码在讲解循环依赖的解决方案之前, 我们必须要对AOP的源码有一定的了解, 因为循环依赖涉及到的二级缓存就跟
AOP源码有关系, 在源码分析的时候, 会适当的删减一部分的代码(如log日志及一些不会影响主线的代码)

调用后置处理器完成AOP的初始化工作的步骤分析

从整体上分析初始化工作

一、由创建单例bean的入口代码讲起
1
2
3
4
5
6
7
8
9
复制代码if (mbd.isSingleton()) {
sharedInstance = getSingleton(beanName, () -> {
return createBean(beanName, mbd, args);
});
bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
}

可以看到, 最终是通过调用createBean方法来创建bean的, 换句话说, 在getSingleton方法中调用了这个lamda
表达式中的方法, 从而开始了创建bean的流程
二、由createBean引出bean的后置处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码protected Object createBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// 解析获得bean对应的class对象
RootBeanDefinition mbdToUse = mbd;
Class<?> resolvedClass = resolveBeanClass(mbd, beanName);

// 在真正创建bean之前, 先执行一次bean的后置处理器
// Give BeanPostProcessors a chance to return a proxy instead of the target bean instance.
Object bean = resolveBeforeInstantiation(beanName, mbdToUse);
if (bean != null) {
return bean;
}

// 开始真正的创建bean
Object beanInstance = doCreateBean(beanName, mbdToUse, args);
return beanInstance;
}

分析:
可以看到在真正创建bean之前, 即调用doCreateBean之前, 会先调用一次bean的后置处理器来解析, 根据源
代码中的英文注释可以看到, 给予BeanPostProcessors一个机会去返回一个代理对象来替换目标bean实例,
根据这个注释, 我们也许会想到, 难道AOP的代理对象就是在这里产生的吗?其实不是的, 这里是给予程序员去
扩展的, 如果我们自己来实现一个代理逻辑, 那么可以在这里执行后置处理器的时候返回一个对象, 这样Spring
就不会去创建bean对象了, 但是在这个方法中, Spring也做了一件很重要的事情, 接下来就来看看这个
resolveBeforeInstantiation方法
三、由resolveBeforeInstantiation方法来看看在真正创建bean之前Spring的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
复制代码protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
Object bean = null;
// 当我们通过后置处理器生成了bean对象的时候, 就会把beanDefinition中设置该变量为true
if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {

// 如果bean不是合成的, 即由应用程序自己生成的, 并且有InstantiationAwareBeanPostProcessor
// 后置处理器在容器中, 那么就会开始调用后置处理器
if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
Class<?> targetType = determineTargetType(beanName, mbd);
if (targetType != null) {
// 开始调用后置处理器的before方法, 如果该方法返回了一个对象, 那么说明创建了bean对象, 此时
// 才会去调用after方法, 由上面的结论可以得知, Spring在这里是不会创建bean对象的, 换句话说,
// after方法通常情况下是不会被调用的, 当我们自己实现了后置处理器并编写了代理逻辑的时候,
// 才会调用到after方法
bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
if (bean != null) {
bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
}
}
}
mbd.beforeInstantiationResolved = (bean != null);
}
return bean;
}

分析:
整个resolveBeforeInstantiation方法最为核心的就是applyBeanPostProcessorsBeforeInstantiation
方法的调用, 通常情况下, 这个方法的返回值为null, 仅仅当这个方法返回了一个bean对象的时候, 才会去
调用afterInitialization方法, 换句话说, 对于一个bean对象的创建, 需要在该bean创建之前调用后置处
理器的before方法, 在bean创建之后调用后置处理器的after方法, 根据之前的英文注释, Spring提供这个扩
展点给程序员在此时此刻实现自己的代理逻辑, 接下来我们看看applyBeanPostProcessorsBeforeInstantiation
方法的调用
四、applyBeanPostProcessorsBeforeInstantiation方法调用后置处理器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码protected Object applyBeanPostProcessorsBeforeInstantiation(Class<?> beanClass, String beanName) {
for (BeanPostProcessor bp : getBeanPostProcessors()) {
if (bp instanceof InstantiationAwareBeanPostProcessor) {
InstantiationAwareBeanPostProcessor ibp = (InstantiationAwareBeanPostProcessor) bp;
Object result = ibp.postProcessBeforeInstantiation(beanClass, beanName);
if (result != null) {
return result;
}
}
}
return null;
}

分析:
applyBeanPostProcessorsBeforeInstantiation方法的主要工作就是调用InstantiationAwareBeanPostProcessor
这个后置处理器的postProcessBeforeInstantiation方法, 即允许程序员在此处实现自己的代理逻辑, 通过
该后置处理器的所有实现类可以看到, 基本上都是返回null

AbstractAutoProxyCreator的postProcessBeforeInstantiation方法

一、postProcessBeforeInstantiation方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
复制代码public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {
Object cacheKey = getCacheKey(beanClass, beanName);

if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
if (this.advisedBeans.containsKey(cacheKey)) {
return null;
}

if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return null;
}
}

return null;
}

分析:
对于InstantiationAwareBeanPostProcessor所有实现类来说, 大家可以一个个去查看, 会发现, 几乎所有
的实现类中这个方法都是返回null的, 因为并不需要在此时此刻创建bean, 这是Spring提供给程序员扩展自己
的代理逻辑用的, 但是对于AOP的实现原理中, 一个核心的类AbstractAutoProxyCreator在实现这个方法的
时候, 除了返回null之外, 还做了一些额外的事情, 这些事情跟AOP是息息相关的


AOP在此时做的事情:
首先我们需要明白的一点, 当我们开启了AOP后, 所有的beanDefinition都会执行到这个before方法, 所以
所有的beanDefinition都会进行这些判断
<1> Object cacheKey = getCacheKey(beanClass, beanName), 对于这一行代码, 大家可以看看其里面
的实现,其实就是用于获取这个beanClass真正的beanName而已

<2> advisedBeans, 这个属性是一个Map<Object, Boolean>, 用于存储所有不用经过AOP代理判断的类以及
已经被AOP代理过的类, 如果是前者, 那么value为false, 如果是后者, 那么value为true

<3> 我们可以想象一下, 如果一个类被@Aspect注解标注了, 说明是一个切面类, 那么对于一个切面类, AOP
肯定是不用去进行代理的, 这就是所谓的不用经过AOP代理判断的类, 而这个before方法的最重要的作用
就是在每一个beanDefinition调用该方法的时候, 筛选出所有不用经过AOP代理判断的类, 并将其放入到
advisedBeans这个map中
<4> 但是对于一个bean来说, 如果没有被AOP的PointCut所包含, 其仍然也会放入到这个advisedBean中,
value为false, 换句话说, 在这个Map中, 当value为true的对象就是需要被AOP代理的对象, value为
false的则不用, 在此时此刻, 仅仅将一些特殊的对象(如被@Aspect注解标注的对象)放入到这里而已,
同时value均为false, 仅仅在创建代理对象的时候, 将代理对象放入到这个Map时, 对应的value为true
<5> 利用shouldSkip方法完成了两件事情, 第一个是扫描整个容器中的类, 找出通知类(被@Aspect标注的类),
以及通知类中的一个个通知, 将这些通知类的名字和这些类中的一个个通知缓存起来, 之后在AOP的时候
就可以直接拿来用了
二、isInfrastructureClass筛选出所有的通知类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码// 调用AnnotationAwareAspectJAutoProxyCreator的isInfrastructureClass
protected boolean isInfrastructureClass(Class<?> beanClass) {
return (super.isInfrastructureClass(beanClass) ||
(this.aspectJAdvisorFactory != null && this.aspectJAdvisorFactory.isAspect(beanClass)));
}

// 调用super的isInfrastructureClass即调用AbstractAutoProxyCreator的isInfrastructureClass方法
protected boolean isInfrastructureClass(Class<?> beanClass) {
boolean retVal = Advice.class.isAssignableFrom(beanClass) ||
Pointcut.class.isAssignableFrom(beanClass) ||
Advisor.class.isAssignableFrom(beanClass) ||
AopInfrastructureBean.class.isAssignableFrom(beanClass);
return retVal;
}

分析:
根据上面的代码可以看到, isInfrastructureClass方法在遇到一个类被@Aspect注解标注以及是Pointcut、
Advice、Advisor、AopInfrastructureBean的子类时将会返回true
三、shouldSkip方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码protected boolean shouldSkip(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
for (Advisor advisor : candidateAdvisors) {
if (advisor instanceof AspectJPointcutAdvisor &&
((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
return true;
}
}
return super.shouldSkip(beanClass, beanName);
}

分析:
在findCandidateAdvisors方法中Spring完成的工作是查找出所有的通知类以及这些类中的通知方法, 然后
在下面的for循环中如果一个通知是AspectJPointcutAdvisor类型的(通常不是), 并且这个通知的切面名字
和bean的名字相同, 也是需要跳过的, 之后调用父类的shouldSkip方法, 在父类的方法中, 就是判断一个类
是否以ORIGINAL(笔者也不知道是干嘛的)

总而言之, 这里我们需要知道的是, shouldSkip方法完成了所有通知类和通知方法的扫描, 并进行了缓存

findCandidateAdvisors方法分析

findCandidateAdvisors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
复制代码protected List<Advisor> findCandidateAdvisors() {
// Add all the Spring advisors found according to superclass rules.
List<Advisor> advisors = super.findCandidateAdvisors();
// Build Advisors for all AspectJ aspects in the bean factory.
if (this.aspectJAdvisorsBuilder != null) {
advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
}
return advisors;
}

分析:
整个findCandidateAdvisors方法是为了查出所有的通知, 并将其进行缓存起来

对于super.findCandidateAdvisors()方法来说, 里面其实就是遍历了整个容器中的Advisor类, 如果该类
的实例还没创建, 即没有走Spring的创建流程, 那么就会调用getBean方法进行创建, 之后将它们放在List中
返回

buildAspectJAdvisors方法才是开始构建我们自己提供的通知, 并将其放在缓存中, 然后将它们返回, 从而
整合上一步骤的advisors, 即找到所有的通知advisors
buildAspectJAdvisors方法构建程序员提供的通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
复制代码public List<Advisor> buildAspectJAdvisors() {
List<String> aspectNames = this.aspectBeanNames;

if (aspectNames == null) {
.......开始构建......
}

if (aspectNames.isEmpty()) {
return Collections.emptyList();
}
List<Advisor> advisors = new ArrayList<>();
for (String aspectName : aspectNames) {
List<Advisor> cachedAdvisors = this.advisorsCache.get(aspectName);
if (cachedAdvisors != null) {
advisors.addAll(cachedAdvisors);
}
else {
MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName);
advisors.addAll(this.advisorFactory.getAdvisors(factory));
}
}
return advisors;
}

分析:
先从整体上看这个方法, Spring首先从缓存中拿到所有被@Aspect标注的类beanName, 如果缓存中没有, 在
if判断中就会开始查找所有被@Aspect标注的类, 同时解析里面的通知, 将其变为一个Advisors, 然后放入
缓存中

换句话说, 在整个if判断中, 做了两件事情, 第一是找到所有被@Aspect标注的类, 第二是解析里面的通知方
法, 将其变成一个个的Advisor, 之后将这两件事情得到的结果分别缓存起来, @Aspect标注的类的beanName
就会缓存到this.aspectBeanNames中, 解析出来的Advisor缓存到this.advisorsCache中, 这是一个Map,
key是beanName, value是一个List, 存储了这个bean所有的通知方法, 最后返回所有的通知

如果缓存中取到了, 那么就不会进if里面, 从而Spring就开始遍历一个个被@Aspect标注的beanName, 然后
根据这个beanName从advisorsCache中获取通知, 整合所有的通知并返回

接下来我们分析下if里面的代码-----------------------

List<Advisor> advisors = new ArrayList<>();
aspectNames = new ArrayList<>();
String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
this.beanFactory, Object.class, true, false);

分析:
Spring利用BeanFactoryUtils根据类型从容器中拿到所有的beanName, 即所有Object类

for (String beanName : beanNames) {
Class<?> beanType = this.beanFactory.getType(beanName);
if (this.advisorFactory.isAspect(beanType)) {
aspectNames.add(beanName);
AspectMetadata amd = new AspectMetadata(beanType, beanName);
if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
MetadataAwareAspectInstanceFactory factory =
new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
if (this.beanFactory.isSingleton(beanName)) {
this.advisorsCache.put(beanName, classAdvisors);
}
else {
this.aspectFactoryCache.put(beanName, factory);
}
advisors.addAll(classAdvisors);
}
}
}
this.aspectBeanNames = aspectNames;
return advisors;

分析:
Spring遍历所有的bean, 如果这个bean被@Aspect注解标注了, 那么就会将其加入到aspectNames中, 在最后
可以看到, Spring会把所有被@Aspect标注的类的beanName缓存起来, 与此同时, Spring以beanName为参数
创建了一个MetadataAwareAspectInstanceFactory, 利用advisorFactory的getAdvisors方法对该bean
中的所有通知进行解析, 解析完成后将其放入到advisorsCache中, 最后将所有的通知返回

在getAdvisors中, Spring会遍历该bean中的所有方法, 找到被通知注解(如@Before这样的)标注的方法, 将
其封装成一个个的Advisor后返回
小小的总结
1
2
复制代码在findCandidateAdvisors方法中, Spring对容器中所有的类进行扫描, 查找出了所有通知类及通知方法, 并
将结果缓存了起来, 并且还查找了容器中Advisor类型的类(这些也是通知)

小小的总结

1
2
3
4
5
6
复制代码第一次调用后置处理器完成AOP的初始化工作即最终会调用InstantiationAwareBeanPostProcessor接口的
postProcessBeforeInstantiation方法, 该方法是Spring提供给开发者的一个扩展点, 允许开发者自己实现
代理逻辑, 从而不采用Spring提供的代理逻辑, 但是在该方法的实现类中, 同时也做了一些AOP的初始化工作,
即会对所有的beanDefinition进行筛选, 将不用经过AOP代理的类筛选出来, 放入到一个advisedBeans的map中,
与此同时, 在第一次调用这个后置处理器, 即整个容器中第一个bd走到这个后置处理器的时候, 就会将整个容器
中的bd扫描一遍, 找出所有的通知类和通知, 将它们缓存起来

AOP的实现原理步骤分析

一、doCreateBean简单分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码在上面AOP初始化工作的第二步中可以看到, 调用完resolveBeforeInstantiation完成了AOP的初始化之后, 就
调用了doCreateBean方法, 接下来我们简单的来解释下这个方法

protected Object doCreateBean(String beanName, RootBeanDefinition mbd, Object[] args) {
// Instantiate the bean
BeanWrapper instanceWrapper = createBeanInstance(beanName, mbd, args);

final Object bean = instanceWrapper.getWrappedInstance();
Class<?> beanType = instanceWrapper.getWrappedClass();

Object exposedObject = bean;
populateBean(beanName, mbd, instanceWrapper);
exposedObject = initializeBean(beanName, exposedObject, mbd);

.......
return exposedObject;
}

分析:
doCreateBean方法由于涉及到许多内容, 包括循环依赖的解决原理, 以及属性注入等, 这些我们之后的章节会
进行详细的介绍, 在这里我们删除了许多的代码, 仅仅提取出一部分代码, 从而引入AOP真正的执行逻辑
<1> createBeanInstance方法是Spring中真正用来创建bean对象的方法, 在该方法中, Spring同样是通过
后置处理器来决定使用哪个构造方法来创建bean对象, 这个在之前bean的后置处理器章节有简单的提到,
在创建完对象后, 封装成了一个BeanWrapper返回了, 之后便通过调用getWrappedInstance以及
getWrappedClass方法来分别获取到bean对象以及bean对象对应的Class对象

<2> 调用populateBean方法来对属性进行填充, 即对@Autowire进行解析以及set方法和get方法注入

<3> 到此为止, 一个bean对象基本就被创建完了, 同时其属性也基本填充完成了
<4> 调用initializeBean来对bean对象进行后续的操作, 包括InitializedBean接口方法的调用以及
@PostContructor注解的解析, 还有我们本章内容的核心-AOP

二、initializeBean方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码protected Object initializeBean(final String beanName, final Object bean, @Nullable RootBeanDefinition mbd) {
// 调用各种xxxAwarexx接口的方法
invokeAwareMethods(beanName, bean);
Object wrappedBean = bean;

// 执行BeanPostProcessor的before方法, 其中完成了@PostContructor注解的处理
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsBeforeInitialization(wrappedBean, beanName);
}

// 调用InitializingBean的afterPropertiesSet方法
invokeInitMethods(beanName, wrappedBean, mbd);

// 一个bean对象已经完成创建, 属性完成填充, 初始化方法完成调用, 此时开始执行AOP代理
if (mbd == null || !mbd.isSynthetic()) {
wrappedBean = applyBeanPostProcessorsAfterInitialization(wrappedBean, beanName);
}

return wrappedBean;
}

调用AbstractAutoProxyCreator的postProcessAfterInitialization方法完成AOP

postProcessAfterInitialization方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
if (bean != null) {
// 获取bean对象真正的beanName
Object cacheKey = getCacheKey(bean.getClass(), beanName);

// 通常情况下这个earlyProxyReferences是不会包含的, 仅仅在出现循环依赖的时候才会出现
// 因为在循环依赖出现的时候, 会提前进行代理, 那么此时就不用再代理了, 抛弃循环依赖的情况, 这一步
// 是一定会进入的
if (!this.earlyProxyReferences.contains(cacheKey)) {
return wrapIfNecessary(bean, beanName, cacheKey);
}
}
return bean;
}
wrapIfNecessary
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
// 这三个判断是对上面AOP初始化工作的一个承前启后, 在AOP初始化工作的时候, 会筛选出所有不用
// 参与AOP代理的类, 比如@Aspect注解标注的类, 到了这一步, 就会直接return bean, 从而不再进行AOP
if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
return bean;
}
if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
return bean;
}
if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}

// 开始创建AOP代理, 获取通知类型, 调用createProxy方法完成代理对象的创建
// Create proxy if we have advice.
Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
if (specificInterceptors != DO_NOT_PROXY) {
// 如果一个对象被AOP通知所包含, 则先添加到advisedBean中, value为true
this.advisedBeans.put(cacheKey, Boolean.TRUE);
Object proxy = createProxy(
bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
this.proxyTypes.put(cacheKey, proxy.getClass());
return proxy;
}

// 如果一个对象没有被AOP通知所包含, 则添加到advisedBean中, value为false
this.advisedBeans.put(cacheKey, Boolean.FALSE);
return bean;
}
getAdvicesAndAdvisorsForBean获取所有适用于当前bean的通知
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
复制代码protected Object[] getAdvicesAndAdvisorsForBean(
Class<?> beanClass, String beanName, @Nullable TargetSource targetSource) {

List<Advisor> advisors = findEligibleAdvisors(beanClass, beanName);
if (advisors.isEmpty()) {
return DO_NOT_PROXY;
}
return advisors.toArray();
}

分析:
通过findEligibleAdvisors方法找到合适的通知, 如果找到了, 那么就转为数组并返回, 如果没找到, 则返
回DO_NOT_PROXY(是一个null)

protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {
List<Advisor> candidateAdvisors = findCandidateAdvisors();
List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);
extendAdvisors(eligibleAdvisors);
if (!eligibleAdvisors.isEmpty()) {
eligibleAdvisors = sortAdvisors(eligibleAdvisors);
}
return eligibleAdvisors;
}

分析:
可以看到Spring会调用findCandidateAdvisors方法, 这个方法在之前描述AOP准备工作的shouldSkip方法
中已经详细介绍过了, 就是找出所有的通知, 并将其缓存起来, 但是此时此刻调用这个方法则是从缓存中拿取
然后调用findAdvisorsThatCanApply方法查找出适用于当前类的通知, extendAdvisors方法是个空方法,
用来扩展的, 如果找到了合适的通知, 则进行排序, 因为之后在调用这些通知方法的时候是有顺序的, 比如
@Before在@After之前执行
findAdvisorsThatCanApply方法找到合适的通知
1
2
3
复制代码在该方法中, 有多个嵌套调用, 不太方便将代码放上来, 笔者通过文字来描述, Spring会循环所有的通知, 对
每一个通知都会做一件事情, 那就是循环当前类的所有方法, 判断是否满足该通知, 如果满足, 那么就是一个合
适的通知, 换句话说, 这个方法里面就是一个双层循环, 外层是对通知的循环, 内层是对方法的循环
createProxy创建代理对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
复制代码/**
* createProxy方法中最主要的工作就是创建一个代理工厂, 在这个代理工厂中定义了AOP对象创建的规则,
* 比如是否强制采用Cglib动态代理等, 最后调用这个工厂类对象的getProxy来创建代理对象
*/
protected Object createProxy(Class<?> beanClass, String beanName,
Object[] specificInterceptors, TargetSource targetSource) {

ProxyFactory proxyFactory = new ProxyFactory();
proxyFactory.copyFrom(this);

if (!proxyFactory.isProxyTargetClass()) {
if (shouldProxyTargetClass(beanClass, beanName)) {
proxyFactory.setProxyTargetClass(true);
}
else {
evaluateProxyInterfaces(beanClass, proxyFactory);
}
}

Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
proxyFactory.addAdvisors(advisors);
proxyFactory.setTargetSource(targetSource);
customizeProxyFactory(proxyFactory);

proxyFactory.setFrozen(this.freezeProxy);
if (advisorsPreFiltered()) {
proxyFactory.setPreFiltered(true);
}

return proxyFactory.getProxy(getProxyClassLoader());
}

分析:
在这个方法中, Spring构建一个代理工厂来创建代理对象, 在代理工厂中, Spring会将所有的通知放入到工厂
中, 这些合适的通知会放入到代理对象所调用的InvocationHandler中, 作为成员变量存储, 即后面创建的
JdkDynamicAopProxy对象(是一个InvocationHandler), 在这些对象中, 都会存储着对应其原始对象的通知

与此同时, 在代理工厂中还会设置一个targtSource, 这个targetSource是createProxy传入的, 是Spring
通过未代理的对象作为参数创建的一个SingletonTargetSource, 这个对象也会存入到代理对象所调用的
InvocationHandler中, 即后面创建的JdkDynamicAopProxy, 这样在invoke方法中就可以通过targetSource
来获取原始对象了
getProxy方法创建代理对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
复制代码public Object getProxy(@Nullable ClassLoader classLoader) {
return createAopProxy().getProxy(classLoader);
}

protected final synchronized AopProxy createAopProxy() {
return getAopProxyFactory().createAopProxy(this);
}

public AopProxyFactory getAopProxyFactory() {
return this.aopProxyFactory;
}

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
Class<?> targetClass = config.getTargetClass();

if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
return new JdkDynamicAopProxy(config);
}
return new ObjenesisCglibAopProxy(config);
}
else {
return new JdkDynamicAopProxy(config);
}
}

分析:
可以根据上面四个方法看到, getProxy中的createAopProxy方法其实就是获取到AOP的代理工厂, 然后创建
对应的AOP代理策略, 即决定是通过JDK动态代理还是Cglib动态代理, 之后调用这些策略的getProxy方法来
完成代理对象的创建, 对于JDK动态代理来说, 其实就是调用Proxy.newInstance方法, 对于Cglib来说, 就是
创建Enhancer对象, 然后完成代理对象的创建, 这里就不再进行深入的分析了

总结

1
2
3
4
5
复制代码AOP源码的分析就差不多了, 我们从AOP初始化的一些配置开始讲起, 然后讲到了AOP真正的实现即BeanPostProcessor
的postProcessAfterInitialization方法, 在该方法中通过选取代理策略, 最终完成了AOP代理对象的创建,
在这里笔者需要提醒大家的是, wrapIfNecessary这个方法很重要, 根据名称可以看到, 如果必要的话则对bean
进行包装, 所以说, 在这个方法中, 则是判断一个对象是否需要代理, 如果需要代理, 则创建代理对象, 这个方
法大家需要多多注意, 因为其是我们理解循环依赖的二级缓存的关键!!!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Spring Cloud 和 Dubbo 哪个会被淘汰?

发表于 2020-05-25

今天在知乎上看到了这样一个问题:Spring Cloud 和 Dubbo哪个会被淘汰?看了几个回答,都觉得不在点子上,所以要么就干脆写篇小文瞎逼叨一下。

简单说说个人观点

我认为这两个框架大概率会长期都存在。

时至今日,这两个框架放到现在,已经不存在谁取代谁这一说了。由于Spring Cloud Alibaba的出现,Dubbo已经很好的融入到了Spring Cloud体系,所以围绕Spring Cloud生态的各种周边产品都是可以无缝整合到一起来玩的。

Dubbo无缝整合Spring Cloud生态是啥意思呢?主要两方面:

  1. 如果你原来是Dubbo用户,那么现在可以把Spring Cloud引入进来。轻松便捷地整合Spring Cloud的配置中心、注册中心以及诸如分布式跟踪等好用的周边产品来管理你的分布式服务集群,与其他Spring Cloud Netflix用户享受同等的生态优势。
  2. 如果你原来不是Dubbo用户,但是你的场景在使用HTTP调用时候觉得不够效率不够经济,那么就可以考虑引入Dubbo,来提升你服务减调用的RPC性能。

到这里,可能有的看官要说了,你都是站在融合的角度来说的,我就是不喜欢Dubbo那种接口依赖的方式,坚决捍卫Spring Cloud原始生态!

行!这种坚持也是可以的,并没有什么错,通过HTTP契约方式管理服务接口,不用接口提供方的JAR,这在编译层面上就不会产生耦合,这点确实一直是目前不用Dubbo的一个重要论据。个人也觉得这种选择在很多方面是有优势的,但是对接口的兼容设计也是有非常高要求的,只要能执行到位,任何一种方案都可以做的很流畅。

但是,我认为Spring Cloud用户对这种方案的坚持并不会影响Dubbo生态的消亡。主要两点:

  1. Dubbo的原始用户群巨大,在Spring Cloud布道之前,Dubbo就拥有了极大的用户群体,现在既然有很好的融合方案,那么融合的考虑肯定要比重构的考虑要更为稳妥的。
  2. 有很多用户会质疑阿里巴巴的开源项目容易太监,这次Dubbo重新维护,又能坚持多久?其实这点这次就不用过多的担心,因为目前的Dubbo已经给了Apache基金会,由于Apache对开源项目在是否可长期维护的评估上有很高的要求(活跃度、贡献比例等),能在Apache毕业的项目,除非出现了一个在各方面都能超越它的东西出现,不然就会很长时间的存在且并应用。

不论从Spring Cloud用户来说,还是Dubbo用户来说,都没有绝对要消亡另一方的场景存在。所以,个人认为这两个极大可能会成为好基友,尤其在国内的应用上。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【译】【Google工程师】 详解 FragmentFact

发表于 2020-05-25

前言

  • 原标题: Android Fragments: FragmentFactory
  • 原文地址: proandroiddev.com/fragmentfac…
  • 原文作者:Husayn Hakeem

在之前的文章 [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析 介绍了 Fragment 1.3.0 中添加了几个重要的 API。

继续上一篇文章,介绍一下 FragmentFactory 和 FragmentContainerView 以及如何和 Koin 一起使用, 这是 Google 在 Fragment 1.2.0 上做的重要的更新,强烈建议大家去使用

通过这篇文章你将学习到以下内容,将在译者思考部分会给出相应的答案

  • FragmentFactory 是什么?
  • 什么情况下使用 FragmentFactory?
  • FragmentContainerView 是什么?
  • 为什么 Google 强烈建议使用 FragmentContainerView?
  • Koin 如何和 FragmentFactory 一起使用以及部分源码分析?
  • 如何处理嵌套 Fragment?

这篇文章涉及很多重要新的知识点,带着自己理解,请耐心读下去,应该可以从中学到很多技巧。

译文

现在我们可以使用 FragmentFactory 来完成 Fragment 构造函数的注入,但是这不是开发人员必须使用的 API, 在某些情况下,它可以被认为是一种很好的设计方法,帮助我们测试带有外部依赖项的 Fragment。

这篇文章将会解释什么是 FragmentFactory,什么时候以及如何使用它,如何处理嵌套 Fragment。

什么是 FragmentFactory?

之前 Fragment 的实例都是通过使用默认的空的构造函数进行实例化,这是因为系统需要在某些情况下重新初始化它,比如配置更改或者 App 的进程重建,如果没有默认构造函数的限制,系统不知道如何重新初始化 Fragment 实例。

FragmentFactory 出现就是为了解决这个限制,通过向其提供实例化 Fragment 所需的参数/依赖关系,FragmentFactory 可以帮助系统创建 Fragment 实例。

如何使用 FragmentFactory?

如果你的 Fragment 没有空的构造函数,您需要创建一个 FragmentFactory 来处理初始化它,通过继承 FragmentFactory 并且覆盖 FragmentFactory#instantiate() 来完成。

1
2
3
4
5
6
7
8
kotlin复制代码class CustomFragmentFactory(private val dependency: Dependency) : FragmentFactory() {
override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
if (className == CustomFragment::class.java.name) {
return CustomFragment(dependency)
}
return super.instantiate(classLoader, className)
}
}

Fragment 是由 FragmentManagers 来管理的,所以为了使用 FragmentFactory 需要关联 FragmentManager,更具体的说它必须分配给包含 Fragment 组件的 FragmentManager,它可以是 Activity 或者 Fragment。

什么时候 FragmentFactory 和 FragmentManager 做关联

FragmentFactory 负责在 Activity 和 parent Fragment 初始化 Fragment,所以应该在创建 Fragment 之前设置它。

  • 在创建 component’s View 之前:如果在 XML 中定义 Fragment,应该使用 Fragment 的 tag <fragment> 或者 FragmentContainerView。
  • 在创建 Fragment 之前:如果 Fragment 是动态添加的应该使用 FragmentTransaction。
  • 在系统恢复 Fragment 之前:如果是因为配置更改或者 App 的进程重建,导致 Fragment 重建。

有了这些限制,可以在 Activity#onCreate() 和 Fragment#onCreate() 之前关联 FragmentFactory 和 FragmentManager,在这两个调用处 view 创建之前会重新初始化 Fragment。

这也就意味着应该在 super#onCreate() 之前关联 FragmentFactory 和 FragmentManager。

  • 在 Activity 关联 FragmentFactory 和 FragmentManager
1
2
3
4
5
6
7
8
9
kotlin复制代码class HostActivity : AppCompatActivity() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())

override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
// ...
}
}
  • 在 Fragment 关联 FragmentFactory 和 FragmentManager
1
2
3
4
5
6
7
8
9
kotlin复制代码class ParentFragment : Fragment() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())

override fun onCreate(savedInstanceState: Bundle?) {
childFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
// ...
}
}

需要使用 FragmentFactory 吗?

到目前为止,您可能已经使用它们的默认构造函数创建 Fragment,然后使用 Dagger 或 Koin 这样的库注入它们需要的依赖项,或者在它们被使用之前在 Fragment 中初始化它们。

如果你的 Fragment 有一个默认的空构造函数,那么就不需要使用 FragmentFactory,如果在 Fragment 构造函数中接受参数,必须使用 FragmentFactory,否者会抛出 Fragment.InstantiationException 异常

如何同时使用 Fragment 和 FragmentFactory?

只需要在创建 Fragment 之前,设置 FragmentFactory,它就会被用来实例化,这意味着在添加 Fragments 之前使用自定义的 FragmentFactory。

  • 静态添加: 使用 Fragment 的 tag <fragment> 和 FragmentContainerView。
1
2
3
4
5
6
7
8
ini复制代码<?xml version="1.0" encoding="utf-8"?>
<androidx.fragment.app.FragmentContainerView
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/customFragment"
android:name="com.example.CustomFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:tag="custom_fragment" />

设置 FragmentFactory 用于初始化在Fragment 声明的 FragmentContainerView

1
2
3
4
5
6
7
8
9
kotlin复制代码class HostActivity : AppCompatActivity() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())

override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_with_fragment_container_view)
}
}
  • 动态添加: 使用 FragmentTransaction#add() 方法动态的添加 Fragment
1
2
3
4
5
6
7
8
9
10
11
12
13
14
kotlin复制代码class HostActivity : AppCompatActivity() {
private val customFragmentFactory = CustomFragmentFactory(Dependency())

override fun onCreate(savedInstanceState: Bundle?) {
supportFragmentManager.fragmentFactory = customFragmentFactory
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_with_empty_frame_layout)
if (savedInstanceState == null) {
supportFragmentManager.beginTransaction()
.add(R.id.content, CustomFragment::class.java, arguments)
.commit()
}
}
}

FragmentFactory 和嵌套的 Fragment

如果 parent Fragment 包含嵌套的 Fragment 或者多层次嵌套的 Fragment,它们都会使用 parent Fragment 的相同 FragmentFactory,嵌套 Fragment 需要调用 Fragment#childFragmentManager.fragmentFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
kotlin复制代码class ParentFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
childFragmentManager.fragmentFactory = parentFragmentFactory
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
// Add NestedFragment
}
}
}

class NestedFragment : Fragment() {
override fun onCreate(savedInstanceState: Bundle?) {
childFragmentManager.fragmentFactory = childFragmentFactory
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
// Add NestedNestedFragment
}
}
}

class NestedNestedFragment : Fragment()

译者思考

我们来总结一下 Fragment 几个重要的更新,以及在什么情况下使用:

  • 之前 Fragment 的实例都是通过使用默认的空的构造函数进行实例化的,FragmentFactory 出现就是为了解决这个限制。
  • FragmentFactory 不是必须要使用的,如果在 Fragment 构造函数中接受参数,必须使用 FragmentFactory
  • FragmentFactory 需要在 Activity 或者 Fragment 中使用,并且需要在 Activity#onCreate() 和 Fragment#onCreate() 之前和 FragmentManager 做关联
  • 嵌套的 Fragment 或者多层次嵌套的 Fragment,使用的是相同 FragmentFactory
  • 正因为 FragmentFactory 出现,可以在 Fragment 构造函数中传递参数,意味着可以使用 Koin 等框架,可以实现构造函数依赖注入,后面我会演示如何使用

接下来一起了解一下什么 FragmentContainerView,为什么 Google 强烈建议使用 FragmentContainerView 容器来存储动态添加的 Fragment。

FragmentContainerView 是什么?为什么 Google 强烈建议使用?

我们先来看一下 Google 的更新说明:

FragmentContainerView: FragmentContainerView 是一个自定义 View 继承 FrameLayout,与 ViewGroups 不同,它只接受 Fragment Views。

为什么 Google 强烈建议使用?

之前在 Google issue 提了一个 fragment z-ordering 的问题,就是说 Fragment 进入和退出动画会导致一个问题,进入的 Fragment 会在退出的 Fragment下面,直到它完全退出屏幕,这会导致在 Fragment 之间切换时产生错误的动画。

使用 FragmentContainerView 带来的好处是改进了对 fragment z-ordering 的处理。这是 Google 演示的例子,优化了两个 Fragment 退出和进入过渡不会互相重叠,使用 FragmentContainerView 将先开启退出动画然后才是进入动画。

Koin 如何和 FragmentFactory 一起使用以及源码分析

在之前的文章 [译][2.4K Start] 放弃 Dagger 拥抱 Koin 分析了 Koin 性能,如果没有看过,建议可以去了解一下。

Koin 团队在 2.1.0 版本开始支持 Fragment 的依赖注入,截图如下所示:

1. 添加 Koin Fragment 依赖

1
arduino复制代码implementation "org.koin:koin-androidx-fragment:2.1.5"

2. 创建 Fragment 并传递 ViewModel

1
2
3
arduino复制代码class FragmentTest(val mainViewModel: MainViewModel) : Fragment(){
......
}

3. 创建 Fragment modules

1
2
3
4
5
6
7
8
9
ini复制代码val viewModelsModule = module {
viewModel { MainViewModel() }
}

val fragmentModules = module {
fragment { FragmentTest(get()) }
}

val appModules = listOf(fragmentModules, viewModelsModule)

4. 在调用 startKoin 方法时设置 KoinFragmentFactory

1
2
3
4
5
6
less复制代码startKoin {
AndroidLogger(Level.DEBUG)
androidContext(this@App)
fragmentFactory()
loadKoinModules(appModules)
}

fragmentFactory 是 KoinApplication 的扩展函数,提供了 KoinFragmentFactory 代码如下所示:

1
2
3
4
less复制代码koin.loadModules(listOf(module {
single<FragmentFactory> { KoinFragmentFactory() }
}))
}

一起来分析 KoinFragmentFactory 内部的源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
kotlin复制代码class KoinFragmentFactory(val scope: Scope? = null) : FragmentFactory(), KoinComponent {

override fun instantiate(classLoader: ClassLoader, className: String): Fragment {
val clazz = Class.forName(className).kotlin
val instance = if (scope != null) {
scope.getOrNull<Fragment>(clazz)
}else{
getKoin().getOrNull<Fragment>(clazz)
}
return instance ?: super.instantiate(classLoader, className)
}

}

继承 FragmentFactory 并且重写了 FragmentFactory#instantiate() 方法,在这个函数中,我们使用 className 作为参数获取 Fragment,并尝试从 Koin 中检索 Fragment 实例

5. 在 onCreate 方法之前 调用 setupKoinFragmentFactory 绑定 FragmentFactory

1
2
3
4
5
kotlin复制代码override fun onCreate(savedInstanceState: Bundle?) {
setupKoinFragmentFactory()
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

6. 添加 Fragment 并传递 Bundle

1
2
3
4
5
6
7
scss复制代码val arguments = Bundle().apply {
putString(FragmentTest.KEY_NAME, "来源于 MainActivity")
}

supportFragmentManager.beginTransaction()
.replace(R.id.container, FragmentTest::class.java, arguments)
.commit()

相关源码已经上传到 JDataBinding 中, 可以查看 App、MainActivity、AppModule 和 FragmentTest 这几个类

参考文献

  • github.com/InsertKoinI…
  • Android Fragments: FragmentFactory
  • developer.android.com/jetpack/and…

结语

致力于分享一系列 Android 系统源码、逆向分析、算法、翻译相关的文章,目前正在翻译一系列欧美精选文章,请持续关注,除了翻译还有对每篇欧美文章思考,如果对你有帮助,请帮我点个赞,感谢!!!期待与你一起成长。

计划建立一个最全、最新的 AndroidX Jetpack 相关组件的实战项目 以及 相关组件原理分析文章,目前已经包含了 App Startup、Paging3、Hilt 等等,正在逐渐增加其他 Jetpack 新成员,仓库持续更新,可以前去查看:AndroidX-Jetpack-Practice, 如果这个仓库对你有帮助,请帮我点个赞,我会陆续完成更多 Jetpack 新成员的项目实践。

算法

由于 LeetCode 的题库庞大,每个分类都能筛选出数百道题,由于每个人的精力有限,不可能刷完所有题目,因此我按照经典类型题目去分类、和题目的难易程度去排序。

  • 数据结构: 数组、栈、队列、字符串、链表、树……
  • 算法: 查找算法、搜索算法、位运算、排序、数学、……

每道题目都会用 Java 和 kotlin 去实现,并且每道题目都有解题思路、时间复杂度和空间复杂度,如果你同我一样喜欢算法、LeetCode,可以关注我 GitHub 上的 LeetCode 题解:Leetcode-Solutions-with-Java-And-Kotlin,一起来学习,期待与你一起成长。

Android 10 源码系列

  • 0xA01 Android 10 源码分析:APK 是如何生成的
  • 0xA02 Android 10 源码分析:APK 的安装流程
  • 0xA03 Android 10 源码分析:APK 加载流程之资源加载
  • 0xA04 Android 10 源码分析:APK 加载流程之资源加载(二)
  • 0xA05 Android 10 源码分析:Dialog 加载绘制流程以及在 Kotlin、DataBinding 中的使用
  • 0xA06 Android 10 源码分析:WindowManager 视图绑定以及体系结构

Android 应用系列

  • 如何高效获取视频截图
  • 如何在项目中封装 Kotlin + Android Databinding
  • [译][Google工程师] 刚刚发布了 Fragment 的新特性 “Fragment 间传递数据的新方式” 以及源码分析
  • [译][2.4K Start] 放弃 Dagger 拥抱 Koin
  • [译][5k+] Kotlin 的性能优化那些事

工具系列

  • 为数不多的人知道的 AndroidStudio 快捷键(一)
  • 为数不多的人知道的 AndroidStudio 快捷键(二)
  • 关于 adb 命令你所需要知道的
  • 10分钟入门 Shell 脚本编程

逆向系列

  • 基于 Smali 文件 Android Studio 动态调试 APP
  • 解决在 Android Studio 3.2 找不到 Android Device Monitor 工具

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

从零开始搭建一个PaaS平台 - 我们要做什么

发表于 2020-05-24

这将是一个系列文章,目录在这里

前言

从最开始的小公司做小网站,到现在进入现在的公司做项目,发现小公司里很多很多工作都是重复的劳动(增删改查),不过想想也是,业务软件最基础的东西不就是增删改查吗。

但是很多时候,这种业务逻辑其实没有必要挨个重写。总不能说你的增删改查比我的高级很多。很大程度上,复杂的问题只是数据太多了怎么优化。

简介

在真的开始做之前,先来简单介绍几个概念。简单介绍一下PaaS是什么,大概意思就是已经做好了一个大的平台,你可以在上边快速的配置、扩展你的服务。

详细的介绍推荐看一下阮一峰老师的博客 www.ruanyifeng.com/blog/2017/0…

概念上

我想从零开始搭建一个能够配置定义业务,通过代码扩展业务的平台。在这个平台上,简单的需求,不写代码。复杂需求,只写与标准不同的代码。

有啥好处

提高生产力

其实,做软件的大部分时候,都是在写增删改查,实在是太简单了。搬砖谁不会对吧,要想搬得快,不需要你有多么好的脚力,更多的时候,你可能需要一个塔吊。

稳定的高负载

PaaS的设计之初,就是为了比较大的数据量来考虑的。项目小的时候,怎么着都行,但是,数据量一旦上来之后。小的项目可能根本没法用,如果是PaaS平台的话,你可能只需要多几台机器就完了,还是基础组搞的事情。

分工明确

提到了高负载,其实很大程度上都是底层的事情。普通的开发,更多的好处只是性能的提升。那么就需要两拨能力不同的人来共同完成这件事情。搞底层的更专注性能、扩展,搞业务的就更关注自己的核心业务就完了。

更少的服务代价

这个指的是客户花销,也是PaaS对于传统软件的优势。PaaS平台一旦做完,他肯定已经有平台了,如果要开发新的功能,可能并不需要占用更多的资源,只是在原有的资源上增加点业务而已。况且PaaS服务商与客户更多的是提供服务的续租模式,多一个客户少一个客户,其实对于服务器来说并没有啥压力,同一个团队能够服务与更多的人。

开发更快

就算是往小里做,如果你有这么一个PaaS的框架,你想要在上边直接搞一个业务的话。其实也就是搞点配置,然后作为一个单机软件部署,纯定制开发也会变得更快。

具体点 我们要做什么

假设我们现在要做一个人员管理系统,我们一般需要以下内容。

  • 增加数据

可以配置一个或者多个新增数据的页面,点击保存就保存了数据

  • 删除数据

可以配置个按钮,点击一下就把相关数据删除掉

  • 修改数据

可以配置个按钮,点击一下出现一个编辑页面,里边会出现对应的数据,你可以修改,然后点击一下更新,数据就更新了

  • 查

– 列表页面

你可以在列表页面,配置几个筛选项,然后你修改完数据之后,点击搜索,就会根据你的数据来改变列表内容数据

– 详情页面

你可以在列表页面点击名称(点击哪个可以配置)然后,就会自动跳转到详情页面

详情页面要展示哪些内容也可以通过配置来进行修改

NoCode能力

这个是整个业务的核心,也是PaaS之所以可以将几个月的工作量浓缩为数周的原因所在。

其实就是一个简单想法的转变,原本我们要实现我上边画的几张图,都是考改变代码来实现,比如说列表页面应该是战士什么Title、列表要不要出现选择框、列表究竟展示那几列、右上角究竟有什么按钮等等。

现在将这些原本需要写到代码里边的逻辑整理到配置里边,然后通过解释这些配置,渲染出页面,渲染出逻辑。

LowCode能力

当然了,上述的情况太过于简单了,基本上就是一个数据库的内容简单展示而已,如果我们需要更复杂一点的内容呢?

比如说我们需要输出这个人的年龄分层(幼儿、少年、青年、中年、老年),我们要怎么做呢?

很显然这个状态不应该被存放在数据库中的,因为这个实际上是通过年龄动态计算出来的,过一年之后这个展示状态可能就会过期了,这个时候我们就需要能够动态插入逻辑根据年龄计算这几个值,然后输出结果。

当然这并不是全部了,其他还有很多需要解决的事情。比如

  • 使用配置来实现渲染,配置数据,读取起来是不是要比写代码慢很多?
  • 搜索条件可能有很多,怎么实现这些条件可用呢?
  • 如果默认的页面满足不了我的需求怎么办?
  • 业务权限要怎么处理?总不能进入系统的人都有权限吧?
  • 开发完了这个玩意怎么发布到线上去?
  • … …

这个玩意有点庞大,一口气说不完。这次内容就这么多,我也只能一边整理一边写博客,这可能会是一个很长,也可能是做不下去很短的系列。

写的不好,能力有限多多见谅

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

100道MySQL数据库经典面试题解析(收藏版)

发表于 2020-05-24

前言

100道MySQL数据库经典面试题解析,已经上传github啦

github.com/whx123/Java…

公众号:捡田螺的小男孩

数据库

1. MySQL 索引使用有哪些注意事项呢?

可以从三个维度回答这个问题:索引哪些情况会失效,索引不适合哪些场景,索引规则

索引哪些情况会失效

  • 查询条件包含or,可能导致索引失效
  • 如何字段类型是字符串,where时一定用引号括起来,否则索引失效
  • like通配符可能导致索引失效。
  • 联合索引,查询时的条件列不是联合索引中的第一个列,索引失效。
  • 在索引列上使用mysql的内置函数,索引失效。
  • 对索引列运算(如,+、-、*、/),索引失效。
  • 索引字段上使用(!= 或者 < >,not in)时,可能会导致索引失效。
  • 索引字段上使用is null, is not null,可能导致索引失效。
  • 左连接查询或者右连接查询查询关联的字段编码格式不一样,可能导致索引失效。
  • mysql估计使用全表扫描要比使用索引快,则不使用索引。

后端程序员必备:索引失效的十大杂症

索引不适合哪些场景

  • 数据量少的不适合加索引
  • 更新比较频繁的也不适合加索引
  • 区分度低的字段不适合加索引(如性别)

索引的一些潜规则

  • 覆盖索引
  • 回表
  • 索引数据结构(B+树)
  • 最左前缀原则
  • 索引下推

2. MySQL 遇到过死锁问题吗,你是如何解决的?

我排查死锁的一般步骤是酱紫的:

  • 查看死锁日志show engine innodb status;
  • 找出死锁Sql
  • 分析sql加锁情况
  • 模拟死锁案发
  • 分析死锁日志
  • 分析死锁结果

可以看我这两篇文章哈:

  • 手把手教你分析Mysql死锁问题
  • Mysql死锁如何排查:insert on duplicate死锁一次排查分析过程

3. 日常工作中你是怎么优化SQL的?

可以从这几个维度回答这个问题:

  • 加索引
  • 避免返回不必要的数据
  • 适当分批量进行
  • 优化sql结构
  • 分库分表
  • 读写分离

可以看我这篇文章哈:
后端程序员必备:书写高质量SQL的30条建议

4. 说说分库与分表的设计

分库分表方案,分库分表中间件,分库分表可能遇到的问题

分库分表方案:

  • 水平分库:以字段为依据,按照一定策略(hash、range等),将一个库中的数据拆分到多个库中。
  • 水平分表:以字段为依据,按照一定策略(hash、range等),将一个表中的数据拆分到多个表中。
  • 垂直分库:以表为依据,按照业务归属不同,将不同的表拆分到不同的库中。
  • 垂直分表:以字段为依据,按照字段的活跃性,将表中字段拆到不同的表(主表和扩展表)中。

常用的分库分表中间件:

  • sharding-jdbc(当当)
  • Mycat
  • TDDL(淘宝)
  • Oceanus(58同城数据库中间件)
  • vitess(谷歌开发的数据库中间件)
  • Atlas(Qihoo 360)

分库分表可能遇到的问题

  • 事务问题:需要用分布式事务啦
  • 跨节点Join的问题:解决这一问题可以分两次查询实现
  • 跨节点的count,order by,group by以及聚合函数问题:分别在各个节点上得到结果后在应用程序端进行合并。
  • 数据迁移,容量规划,扩容等问题
  • ID问题:数据库被切分后,不能再依赖数据库自身的主键生成机制啦,最简单可以考虑UUID
  • 跨分片的排序分页问题(后台加大pagesize处理?)

个人觉得网上这两篇文章不错,小伙伴们可以去看一下哈:

  • MySQL数据库之互联网常用分库分表方案
  • 分库分表需要考虑的问题及方案

5. InnoDB与MyISAM的区别

  • InnoDB支持事务,MyISAM不支持事务
  • InnoDB支持外键,MyISAM不支持外键
  • InnoDB 支持 MVCC(多版本并发控制),MyISAM 不支持
  • select count(*) from table时,MyISAM更快,因为它有一个变量保存了整个表的总行数,可以直接读取,InnoDB就需要全表扫描。
  • Innodb不支持全文索引,而MyISAM支持全文索引(5.7以后的InnoDB也支持全文索引)
  • InnoDB支持表、行级锁,而MyISAM支持表级锁。
  • InnoDB表必须有主键,而MyISAM可以没有主键
  • Innodb表需要更多的内存和存储,而MyISAM可被压缩,存储空间较小,。
  • Innodb按主键大小有序插入,MyISAM记录插入顺序是,按记录插入顺序保存。
  • InnoDB 存储引擎提供了具有提交、回滚、崩溃恢复能力的事务安全,与 MyISAM 比 InnoDB 写的效率差一些,并且会占用更多的磁盘空间以保留数据和索引

6. 数据库索引的原理,为什么要用 B+树,为什么不用二叉树?

可以从几个维度去看这个问题,查询是否够快,效率是否稳定,存储数据多少,以及查找磁盘次数,为什么不是二叉树,为什么不是平衡二叉树,为什么不是B树,而偏偏是B+树呢?

为什么不是一般二叉树?

如果二叉树特殊化为一个链表,相当于全表扫描。平衡二叉树相比于二叉查找树来说,查找效率更稳定,总体的查找速度也更快。

为什么不是平衡二叉树呢?

我们知道,在内存比在磁盘的数据,查询效率快得多。如果树这种数据结构作为索引,那我们每查找一次数据就需要从磁盘中读取一个节点,也就是我们说的一个磁盘块,但是平衡二叉树可是每个节点只存储一个键值和数据的,如果是B树,可以存储更多的节点数据,树的高度也会降低,因此读取磁盘的次数就降下来啦,查询效率就快啦。

那为什么不是B树而是B+树呢?

1)B+树非叶子节点上是不存储数据的,仅存储键值,而B树节点中不仅存储键值,也会存储数据。innodb中页的默认大小是16KB,如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快。

2)B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链表连着的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。

可以看这篇文章哈:
再有人问你为什么MySQL用B+树做索引,就把这篇文章发给她

7. 聚集索引与非聚集索引的区别

  • 一个表中只能拥有一个聚集索引,而非聚集索引一个表可以存在多个。
  • 聚集索引,索引中键值的逻辑顺序决定了表中相应行的物理顺序;非聚集索引,索引中索引的逻辑顺序与磁盘上行的物理存储顺序不同。
  • 索引是通过二叉树的数据结构来描述的,我们可以这么理解聚簇索引:索引的叶节点就是数据节点。而非聚簇索引的叶节点仍然是索引节点,只不过有一个指针指向对应的数据块。
  • 聚集索引:物理存储按照索引排序;非聚集索引:物理存储不按照索引排序;

何时使用聚集索引或非聚集索引?

8. limit 1000000 加载很慢的话,你是怎么解决的呢?

方案一:如果id是连续的,可以这样,返回上次查询的最大记录(偏移量),再往下limit

1
复制代码select id,name from employee where id>1000000 limit 10.

方案二:在业务允许的情况下限制页数:

建议跟业务讨论,有没有必要查这么后的分页啦。因为绝大多数用户都不会往后翻太多页。

方案三:order by + 索引(id为索引)

1
复制代码select id,name from employee order by id  limit 1000000,10

方案四:利用延迟关联或者子查询优化超多分页场景。(先快速定位需要获取的id段,然后再关联)

1
复制代码SELECT a.* FROM employee a, (select id from employee where 条件 LIMIT 1000000,10 ) b where a.id=b.id

9. 如何选择合适的分布式主键方案呢?

  • 数据库自增长序列或字段。
  • UUID。
  • Redis生成ID
  • Twitter的snowflake算法
  • 利用zookeeper生成唯一ID
  • MongoDB的ObjectId

10. 事务的隔离级别有哪些?MySQL的默认隔离级别是什么?

  • 读未提交(Read Uncommitted)
  • 读已提交(Read Committed)
  • 可重复读(Repeatable Read)
  • 串行化(Serializable)

Mysql默认的事务隔离级别是可重复读(Repeatable Read)

可以看我这篇文章哈:一文彻底读懂MySQL事务的四大隔离级别

11. 什么是幻读,脏读,不可重复读呢?

  • 事务A、B交替执行,事务A被事务B干扰到了,因为事务A读取到事务B未提交的数据,这就是脏读
  • 在一个事务范围内,两个相同的查询,读取同一条记录,却返回了不同的数据,这就是不可重复读。
  • 事务A查询一个范围的结果集,另一个并发事务B往这个范围中插入/删除了数据,并静悄悄地提交,然后事务A再次查询相同的范围,两次读取得到的结果集不一样了,这就是幻读。

可以看我这篇文章哈:一文彻底读懂MySQL事务的四大隔离级别

12. 在高并发情况下,如何做到安全的修改同一行数据?

要安全的修改同一行数据,就要保证一个线程在修改时其它线程无法更新这行记录。一般有悲观锁和乐观锁两种方案~

使用悲观锁

悲观锁思想就是,当前线程要进来修改数据时,别的线程都得拒之门外~
比如,可以使用select…for update ~

1
复制代码select * from User where name=‘jay’ for update

以上这条sql语句会锁定了User表中所有符合检索条件(name=‘jay’)的记录。本次事务提交之前,别的线程都无法修改这些记录。

使用乐观锁

乐观锁思想就是,有线程过来,先放过去修改,如果看到别的线程没修改过,就可以修改成功,如果别的线程修改过,就修改失败或者重试。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。

可以看一下我这篇文章,主要是思路哈~
CAS乐观锁解决并发问题的一次实践

13. 数据库的乐观锁和悲观锁。

悲观锁:

悲观锁她专一且缺乏安全感了,她的心只属于当前事务,每时每刻都担心着它心爱的数据可能被别的事务修改,所以一个事务拥有(获得)悲观锁后,其他任何事务都不能对数据进行修改啦,只能等待锁被释放才可以执行。

乐观锁:

乐观锁的“乐观情绪”体现在,它认为数据的变动不会太频繁。因此,它允许多个事务同时对数据进行变动。实现方式:乐观锁一般会使用版本号机制或CAS算法实现。

之前转载了的这篇文章,觉得作者写得挺详细的~

图文并茂的带你彻底理解悲观锁与乐观锁

14. SQL优化的一般步骤是什么,怎么看执行计划(explain),如何理解其中各个字段的含义。

  • show status 命令了解各种 sql 的执行频率
  • 通过慢查询日志定位那些执行效率较低的 sql 语句
  • explain 分析低效 sql 的执行计划(这点非常重要,日常开发中用它分析Sql,会大大降低Sql导致的线上事故)

看过这篇文章,觉得很不错:
优化sql 语句的一般步骤

15. select for update有什么含义,会锁表还是锁行还是其他。

select for update 含义

select查询语句是不会加锁的,但是select for update除了有查询的作用外,还会加锁呢,而且它是悲观锁哦。至于加了是行锁还是表锁,这就要看是不是用了索引/主键啦。

没用索引/主键的话就是表锁,否则就是是行锁。

select for update 加锁验证

表结构:

1
2
3
4
5
6
7
8
复制代码//id 为主键,name为唯一索引
CREATE TABLE `account` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`balance` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1570068 DEFAULT CHARSET=utf8

id为主键,select for update 1270070这条记录时,再开一个事务对该记录更新,发现更新阻塞啦,其实是加锁了。如下图:

我们再开一个事务对另外一条记录1270071更新,发现更新成功,因此,如果查询条件用了索引/主键,会加行锁~

我们继续一路向北吧,换普通字段balance吧,发现又阻塞了。因此,没用索引/主键的话,select for update加的就是表锁

16. MySQL事务得四大特性以及实现原理

  • 原子性: 事务作为一个整体被执行,包含在其中的对数据库的操作要么全部被执行,要么都不执行。
  • 一致性: 指在事务开始之前和事务结束以后,数据不会被破坏,假如A账户给B账户转10块钱,不管成功与否,A和B的总金额是不变的。
  • 隔离性: 多个事务并发访问时,事务之间是相互隔离的,即一个事务不影响其它事务运行效果。简言之,就是事务之间是进水不犯河水的。
  • 持久性: 表示事务完成以后,该事务对数据库所作的操作更改,将持久地保存在数据库之中。

事务ACID特性的实现思想

  • 原子性:是使用 undo log来实现的,如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态。
  • 持久性:使用 redo log来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复。
  • 隔离性:通过锁以及MVCC,使事务相互隔离开。
  • 一致性:通过回滚、恢复,以及并发情况下的隔离性,从而实现一致性。

17. 如果某个表有近千万数据,CRUD比较慢,如何优化。

分库分表

某个表有近千万数据,可以考虑优化表结构,分表(水平分表,垂直分表),当然,你这样回答,需要准备好面试官问你的分库分表相关问题呀,如

  • 分表方案(水平分表,垂直分表,切分规则hash等)
  • 分库分表中间件(Mycat,sharding-jdbc等)
  • 分库分表一些问题(事务问题?跨节点Join的问题)
  • 解决方案(分布式事务等)

索引优化

除了分库分表,优化表结构,当然还有所以索引优化等方案~

有兴趣可以看我这篇文章哈~
后端程序员必备:书写高质量SQL的30条建议

18. 如何写sql能够有效的使用到复合索引。

复合索引,也叫组合索引,用户可以在多个列上建立索引,这种索引叫做复合索引。

当我们创建一个组合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。

1
复制代码select * from table where k1=A AND k2=B AND k3=D

有关于复合索引,我们需要关注查询Sql条件的顺序,确保最左匹配原则有效,同时可以删除不必要的冗余索引。

19. mysql中in 和exists的区别。

这个,跟一下demo来看更刺激吧,啊哈哈

假设表A表示某企业的员工表,表B表示部门表,查询所有部门的所有员工,很容易有以下SQL:

1
复制代码select * from A where deptId in (select deptId from B);

这样写等价于:

先查询部门表B

select deptId from B

再由部门deptId,查询A的员工

select * from A where A.deptId = B.deptId

可以抽象成这样的一个循环:

1
2
3
4
5
6
7
8
9
复制代码   List<> resultSet ;
for(int i=0;i<B.length;i++) {
for(int j=0;j<A.length;j++) {
if(A[i].id==B[j].id) {
resultSet.add(A[i]);
break;
}
}
}

显然,除了使用in,我们也可以用exists实现一样的查询功能,如下:

1
复制代码select * from A where exists (select 1 from B where A.deptId = B.deptId);

因为exists查询的理解就是,先执行主查询,获得数据后,再放到子查询中做条件验证,根据验证结果(true或者false),来决定主查询的数据结果是否得意保留。

那么,这样写就等价于:

select * from A,先从A表做循环

select * from B where A.deptId = B.deptId,再从B表做循环.

同理,可以抽象成这样一个循环:

1
2
3
4
5
6
7
8
9
复制代码   List<> resultSet ;
for(int i=0;i<A.length;i++) {
for(int j=0;j<B.length;j++) {
if(A[i].deptId==B[j].deptId) {
resultSet.add(A[i]);
break;
}
}
}

数据库最费劲的就是跟程序链接释放。假设链接了两次,每次做上百万次的数据集查询,查完就走,这样就只做了两次;相反建立了上百万次链接,申请链接释放反复重复,这样系统就受不了了。即mysql优化原则,就是小表驱动大表,小的数据集驱动大的数据集,从而让性能更优。

因此,我们要选择最外层循环小的,也就是,如果B的数据量小于A,适合使用in,如果B的数据量大于A,即适合选择exists,这就是in和exists的区别。

20. 数据库自增主键可能遇到什么问题。

  • 使用自增主键对数据库做分库分表,可能出现诸如主键重复等的问题。解决方案的话,简单点的话可以考虑使用UUID哈
  • 自增主键会产生表锁,从而引发问题
  • 自增主键可能用完问题。

21. MVCC熟悉吗,它的底层原理?

MVCC,多版本并发控制,它是通过读取历史版本的数据,来降低并发事务冲突,从而提高并发性能的一种机制。

MVCC需要关注这几个知识点:

  • 事务版本号
  • 表的隐藏列
  • undo log
  • read view

可以看我这篇文章哈:一文彻底读懂MySQL事务的四大隔离级别

22. 数据库中间件了解过吗,sharding jdbc,mycat?

  • sharding-jdbc目前是基于jdbc驱动,无需额外的proxy,因此也无需关注proxy本身的高可用。
  • Mycat 是基于 Proxy,它复写了 MySQL 协议,将 Mycat Server 伪装成一个 MySQL 数据库,而 Sharding-JDBC 是基于 JDBC 接口的扩展,是以 jar 包的形式提供轻量级服务的。

有网友推荐这篇文章:

深度认识Sharding-JDBC:做最轻量级的数据库中间层

23. MYSQL的主从延迟,你怎么解决?

嘻嘻,先复习一下主从复制原理吧,如图:

主从复制分了五个步骤进行:

  • 步骤一:主库的更新事件(update、insert、delete)被写到binlog
  • 步骤二:从库发起连接,连接到主库。
  • 步骤三:此时主库创建一个binlog dump thread,把binlog的内容发送到从库。
  • 步骤四:从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log
  • 步骤五:还会创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

有兴趣的小伙伴也可以看看我这篇文章:
后端程序员必备:mysql数据库相关流程图/原理图

主从同步延迟的原因

一个服务器开放N个链接给客户端来连接的,这样有会有大并发的更新操作, 但是从服务器的里面读取binlog的线程仅有一个,当某个SQL在从服务器上执行的时间稍长 或者由于某个SQL要进行锁表就会导致,主服务器的SQL大量积压,未被同步到从服务器里。这就导致了主从不一致, 也就是主从延迟。

主从同步延迟的解决办法

  • 主服务器要负责更新操作,对安全性的要求比从服务器要高,所以有些设置参数可以修改,比如sync_binlog=1,innodb_flush_log_at_trx_commit = 1 之类的设置等。
  • 选择更好的硬件设备作为slave。
  • 把一台从服务器当度作为备份使用, 而不提供查询, 那边他的负载下来了, 执行relay log 里面的SQL效率自然就高了。
  • 增加从服务器喽,这个目的还是分散读的压力,从而降低服务器负载。

可以看这篇文章哈~
MySQL 主从同步延迟的原因及解决办法

24. 说一下大表查询的优化方案

  • 优化shema、sql语句+索引;
  • 可以考虑加缓存,memcached, redis,或者JVM本地缓存;
  • 主从复制,读写分离;
  • 分库分表;

25. 什么是数据库连接池?为什么需要数据库连接池呢?

连接池基本原理:
数据库连接池原理:在内部对象池中,维护一定数量的数据库连接,并对外暴露数据库连接的获取和返回方法。

应用程序和数据库建立连接的过程:

  • 通过TCP协议的三次握手和数据库服务器建立连接
  • 发送数据库用户账号密码,等待数据库验证用户身份
  • 完成身份验证后,系统可以提交SQL语句到数据库执行
  • 把连接关闭,TCP四次挥手告别。

数据库连接池好处:

  • 资源重用 (连接复用)
  • 更快的系统响应速度
  • 新的资源分配手段
  • 统一的连接管理,避免数据库连接泄漏

有兴趣的伙伴可以看看我这篇文章哈~
数据库连接池内存泄漏问题的分析和解决方案

26. 一条SQL语句在MySQL中如何执行的?

先看一下Mysql的逻辑架构图吧~

查询语句:

  • 先检查该语句是否有权限
  • 如果没有权限,直接返回错误信息
  • 如果有权限,在 MySQL8.0 版本以前,会先查询缓存。
  • 如果没有缓存,分析器进行词法分析,提取 sql 语句select等的关键元素。然后判断sql 语句是否有语法错误,比如关键词是否正确等等。
  • 优化器进行确定执行方案
  • 进行权限校验,如果没有权限就直接返回错误信息,如果有权限就会调用数据库引擎接口,返回执行结果。

这篇文章非常不错,大家去看一下吧:
一条SQL语句在MySQL中如何执行的

27. InnoDB引擎中的索引策略,了解过吗?

  • 覆盖索引
  • 最左前缀原则
  • 索引下推

索引下推优化是 MySQL 5.6 引入的, 可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数。

这篇文章非常不错,大家去看一下吧:
聊一聊 InnoDB 引擎中的这些索引策略

28. 数据库存储日期格式时,如何考虑时区转换问题?

  • datetime类型适合用来记录数据的原始的创建时间,修改记录中其他字段的值,datetime字段的值不会改变,除非手动修改它。
  • timestamp类型适合用来记录数据的最后修改时间,只要修改了记录中其他字段的值,timestamp字段的值都会被自动更新。

如何考虑时区转换问题/看一下这个吧:
数据库存储日期格式时,如何考虑时区转换问题?

29. 一条sql执行过长的时间,你如何优化,从哪些方面入手?

  • 查看是否涉及多表和子查询,优化Sql结构,如去除冗余字段,是否可拆表等
  • 优化索引结构,看是否可以适当添加索引
  • 数量大的表,可以考虑进行分离/分表(如交易流水表)
  • 数据库主从分离,读写分离
  • explain分析sql语句,查看执行计划,优化sql
  • 查看mysql执行日志,分析是否有其他方面的问题

30. MYSQL数据库服务器性能分析的方法命令有哪些?

  • Show status, 一些值得监控的变量值:
  • Bytes_received和Bytes_sent 和服务器之间来往的流量。
  • Com_*服务器正在执行的命令。
  • Created_*在查询执行期限间创建的临时表和文件。
  • Handler_*存储引擎操作。
  • Select_*不同类型的联接执行计划。
  • Sort_*几种排序信息。
  • Show profiles 是MySql用来分析当前会话SQL语句执行的资源消耗情况

31. Blob和text有什么区别?

  • Blob用于存储二进制数据,而Text用于存储大字符串。
  • Blob值被视为二进制字符串(字节字符串),它们没有字符集,并且排序和比较基于列值中的字节的数值。
  • text值被视为非二进制字符串(字符字符串)。它们有一个字符集,并根据字符集的排序规则对值进行排序和比较。

32. mysql里记录货币用什么字段类型比较好?

  • 货币在数据库中MySQL常用Decimal和Numric类型表示,这两种类型被MySQL实现为同样的类型。他们被用于保存与金钱有关的数据。
  • salary DECIMAL(9,2),9(precision)代表将被用于存储值的总的小数位数,而2(scale)代表将被用于存储小数点后的位数。存储在salary列中的值的范围是从-9999999.99到9999999.99。
  • DECIMAL和NUMERIC值作为字符串存储,而不是作为二进制浮点数,以便保存那些值的小数精度。

33. Mysql中有哪几种锁,列举一下?

如果按锁粒度划分,有以下3种:

  • 表锁: 开销小,加锁快;锁定力度大,发生锁冲突概率高,并发度最低;不会出现死锁。
  • 行锁: 开销大,加锁慢;会出现死锁;锁定粒度小,发生锁冲突的概率低,并发度高。
  • 页锁: 开销和加锁速度介于表锁和行锁之间;会出现死锁;锁定粒度介于表锁和行锁之间,并发度一般

有兴趣的小伙伴可以看我这篇文章,有介绍到各种锁哈:

后端程序员必备:mysql数据库相关流程图/原理图

34. Hash索引和B+树区别是什么?你在设计索引是怎么抉择的?

  • B+树可以进行范围查询,Hash索引不能。
  • B+树支持联合索引的最左侧原则,Hash索引不支持。
  • B+树支持order by排序,Hash索引不支持。
  • Hash索引在等值查询上比B+树效率更高。
  • B+树使用like 进行模糊查询的时候,like后面(比如%开头)的话可以起到优化的作用,Hash索引根本无法进行模糊查询。

35. mysql 的内连接、左连接、右连接有什么区别?

  • Inner join 内连接,在两张表进行连接查询时,只保留两张表中完全匹配的结果集
  • left join 在两张表进行连接查询时,会返回左表所有的行,即使在右表中没有匹配的记录。
  • right join 在两张表进行连接查询时,会返回右表所有的行,即使在左表中没有匹配的记录。

36. 说说MySQL 的基础架构图

Mysql逻辑架构图主要分三层:

  • 第一层负责连接处理,授权认证,安全等等
  • 第二层负责编译并优化SQL
  • 第三层是存储引擎。

37. 什么是内连接、外连接、交叉连接、笛卡尔积呢?

  • 内连接(inner join):取得两张表中满足存在连接匹配关系的记录。
  • 外连接(outer join):取得两张表中满足存在连接匹配关系的记录,以及某张表(或两张表)中不满足匹配关系的记录。
  • 交叉连接(cross join):显示两张表所有记录一一对应,没有匹配关系进行筛选,也被称为:笛卡尔积。

38. 说一下数据库的三大范式

  • 第一范式:数据表中的每一列(每个字段)都不可以再拆分。
  • 第二范式:在第一范式的基础上,分主键列完全依赖于主键,而不能是依赖于主键的一部分。
  • 第三范式:在满足第二范式的基础上,表中的非主键只依赖于主键,而不依赖于其他非主键。

39. mysql有关权限的表有哪几个呢?

MySQL服务器通过权限表来控制用户对数据库的访问,权限表存放在mysql数据库里,由mysql_install_db脚本初始化。这些权限表分别user,db,table_priv,columns_priv和host。

  • user权限表:记录允许连接到服务器的用户帐号信息,里面的权限是全局级的。
  • db权限表:记录各个帐号在各个数据库上的操作权限。
  • table_priv权限表:记录数据表级的操作权限。
  • columns_priv权限表:记录数据列级的操作权限。
  • host权限表:配合db权限表对给定主机上数据库级操作权限作更细致的控制。这个权限表不受GRANT和REVOKE语句的影响。

40. Mysql的binlog有几种录入格式?分别有什么区别?

有三种格式哈,statement,row和mixed。

  • statement,每一条会修改数据的sql都会记录在binlog中。不需要记录每一行的变化,减少了binlog日志量,节约了IO,提高性能。由于sql的执行是有上下文的,因此在保存的时候需要保存相关的信息,同时还有一些使用了函数之类的语句无法被记录复制。
  • row,不记录sql语句上下文相关信息,仅保存哪条记录被修改。记录单元为每一行的改动,基本是可以全部记下来但是由于很多操作,会导致大量行的改动(比如alter table),因此这种模式的文件保存的信息太多,日志量太大。
  • mixed,一种折中的方案,普通操作使用statement记录,当无法使用statement的时候使用row。

41. InnoDB引擎的4大特性,了解过吗

  • 插入缓冲(insert buffer)
  • 二次写(double write)
  • 自适应哈希索引(ahi)
  • 预读(read ahead)

42. 索引有哪些优缺点?

优点:

  • 唯一索引可以保证数据库表中每一行的数据的唯一性
  • 索引可以加快数据查询速度,减少查询时间

缺点:

  • 创建索引和维护索引要耗费时间
  • 索引需要占物理空间,除了数据表占用数据空间之外,每一个索引还要占用一定的物理空间
  • 以表中的数据进行增、删、改的时候,索引也要动态的维护。

43. 索引有哪几种类型?

  • 主键索引: 数据列不允许重复,不允许为NULL,一个表只能有一个主键。
  • 唯一索引: 数据列不允许重复,允许为NULL值,一个表允许多个列创建唯一索引。
  • 普通索引: 基本的索引类型,没有唯一性的限制,允许为NULL值。
  • 全文索引:是目前搜索引擎使用的一种关键技术,对文本的内容进行分词、搜索。
  • 覆盖索引:查询列要被所建的索引覆盖,不必读取数据行
  • 组合索引:多列值组成一个索引,用于组合搜索,效率大于索引合并

44. 创建索引有什么原则呢?

  • 最左前缀匹配原则
  • 频繁作为查询条件的字段才去创建索引
  • 频繁更新的字段不适合创建索引
  • 索引列不能参与计算,不能有函数操作
  • 优先考虑扩展索引,而不是新建索引,避免不必要的索引
  • 在order by或者group by子句中,创建索引需要注意顺序
  • 区分度低的数据列不适合做索引列(如性别)
  • 定义有外键的数据列一定要建立索引。
  • 对于定义为text、image数据类型的列不要建立索引。
  • 删除不再使用或者很少使用的索引

45. 创建索引的三种方式

  • 在执行CREATE TABLE时创建索引
1
2
3
4
5
6
7
8
9
复制代码CREATE TABLE `employee` (
`id` int(11) NOT NULL,
`name` varchar(255) DEFAULT NULL,
`age` int(11) DEFAULT NULL,
`date` datetime DEFAULT NULL,
`sex` int(1) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `idx_name` (`name`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • 使用ALTER TABLE命令添加索引
1
复制代码ALTER TABLE table_name ADD INDEX index_name (column);
  • 使用CREATE INDEX命令创建
1
复制代码CREATE INDEX index_name ON table_name (column);

46. 百万级别或以上的数据,你是如何删除的?

  • 我们想要删除百万数据的时候可以先删除索引
  • 然后批量删除其中无用数据
  • 删除完成后重新创建索引。

47. 什么是最左前缀原则?什么是最左匹配原则?

  • 最左前缀原则,就是最左优先,在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边。
  • 当我们创建一个组合索引的时候,如(k1,k2,k3),相当于创建了(k1)、(k1,k2)和(k1,k2,k3)三个索引,这就是最左匹配原则。。

48. B树和B+树的区别,数据库为什么使用B+树而不是B树?

  • 在B树中,键和值即存放在内部节点又存放在叶子节点;在B+树中,内部节点只存键,叶子节点则同时存放键和值。
  • B+树的叶子节点有一条链相连,而B树的叶子节点各自独立的。
  • B+树索引的所有数据均存储在叶子节点,而且数据是按照顺序排列的,链表连着的。那么B+树使得范围查找,排序查找,分组查找以及去重查找变得异常简单。.
  • B+树非叶子节点上是不存储数据的,仅存储键值,而B树节点中不仅存储键值,也会存储数据。innodb中页的默认大小是16KB,如果不存储数据,那么就会存储更多的键值,相应的树的阶数(节点的子节点树)就会更大,树就会更矮更胖,如此一来我们查找数据进行磁盘的IO次数有会再次减少,数据查询的效率也会更快.

49. 覆盖索引、回表等这些,了解过吗?

  • 覆盖索引: 查询列要被所建的索引覆盖,不必从数据表中读取,换句话说查询列要被所使用的索引覆盖。
  • 回表:二级索引无法直接查询所有列的数据,所以通过二级索引查询到聚簇索引后,再查询到想要的数据,这种通过二级索引查询出来的过程,就叫做回表。

网上这篇文章讲得很清晰:
mysql覆盖索引与回表

50. B+树在满足聚簇索引和覆盖索引的时候不需要回表查询数据?

  • 在B+树的索引中,叶子节点可能存储了当前的key值,也可能存储了当前的key值以及整行的数据,这就是聚簇索引和非聚簇索引。 在InnoDB中,只有主键索引是聚簇索引,如果没有主键,则挑选一个唯一键建立聚簇索引。如果没有唯一键,则隐式的生成一个键来建立聚簇索引。
  • 当查询使用聚簇索引时,在对应的叶子节点,可以获取到整行数据,因此不用再次进行回表查询。

51. 何时使用聚簇索引与非聚簇索引

52. 非聚簇索引一定会回表查询吗?

不一定,如果查询语句的字段全部命中了索引,那么就不必再进行回表查询(哈哈,覆盖索引就是这么回事)。

举个简单的例子,假设我们在学生表的上建立了索引,那么当进行select age from student where age < 20的查询时,在索引的叶子节点上,已经包含了age信息,不会再次进行回表查询。

53. 组合索引是什么?为什么需要注意组合索引中的顺序?

组合索引,用户可以在多个列上建立索引,这种索引叫做组合索引。

因为InnoDB引擎中的索引策略的最左原则,所以需要注意组合索引中的顺序。

54. 什么是数据库事务?

数据库事务(简称:事务),是数据库管理系统执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成,这些操作要么全部执行,要么全部不执行,是一个不可分割的工作单位。

55. 隔离级别与锁的关系

回答这个问题,可以先阐述四种隔离级别,再阐述它们的实现原理。隔离级别就是依赖锁和MVCC实现的。

可以看我这篇文章哈:一文彻底读懂MySQL事务的四大隔离级别

56. 按照锁的粒度分,数据库锁有哪些呢?锁机制与InnoDB锁算法

  • 按锁粒度分有:表锁,页锁,行锁
  • 按锁机制分有:乐观锁,悲观锁

57. 从锁的类别角度讲,MySQL都有哪些锁呢?

从锁的类别上来讲,有共享锁和排他锁。

  • 共享锁: 又叫做读锁。当用户要进行数据的读取时,对数据加上共享锁。共享锁可以同时加上多个。
  • 排他锁: 又叫做写锁。当用户要进行数据的写入时,对数据加上排他锁。排他锁只可以加一个,他和其他的排他锁,共享锁都相斥。

锁兼容性如下:

58. MySQL中InnoDB引擎的行锁是怎么实现的?

基于索引来完成行锁的。

1
复制代码select * from t where id = 666 for update;

for update 可以根据条件来完成行锁锁定,并且 id 是有索引键的列,如果 id 不是索引键那么InnoDB将实行表锁。

59. 什么是死锁?怎么解决?

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。看图形象一点,如下:

死锁有四个必要条件:互斥条件,请求和保持条件,环路等待条件,不剥夺条件。
解决死锁思路,一般就是切断环路,尽量避免并发形成环路。

  • 如果不同程序会并发存取多个表,尽量约定以相同的顺序访问表,可以大大降低死锁机会。
  • 在同一个事务中,尽可能做到一次锁定所需要的所有资源,减少死锁产生概率;
  • 对于非常容易产生死锁的业务部分,可以尝试使用升级锁定颗粒度,通过表级锁定来减少死锁产生的概率;
  • 如果业务处理不好可以用分布式事务锁或者使用乐观锁
  • 死锁与索引密不可分,解决索引问题,需要合理优化你的索引,

有兴趣的朋友,可以看我的这篇死锁分析:
手把手教你分析Mysql死锁问题

60. 为什么要使用视图?什么是视图?

为什么要使用视图?

为了提高复杂SQL语句的复用性和表操作的安全性,MySQL数据库管理系统提供了视图特性。

什么是视图?

视图是一个虚拟的表,是一个表中的数据经过某种筛选后的显示方式,视图由一个预定义的查询select语句组成。

61. 视图有哪些特点?哪些使用场景?

视图特点:

  • 视图的列可以来自不同的表,是表的抽象和在逻辑意义上建立的新关系。
  • 视图是由基本表(实表)产生的表(虚表)。
  • 视图的建立和删除不影响基本表。
  • 对视图内容的更新(添加,删除和修改)直接影响基本表。
  • 当视图来自多个基本表时,不允许添加和删除数据。

视图用途: 简化sql查询,提高开发效率,兼容老的表结构。

视图的常见使用场景:

  • 重用SQL语句;
  • 简化复杂的SQL操作。
  • 使用表的组成部分而不是整个表;
  • 保护数据
  • 更改数据格式和表示。视图可返回与底层表的表示和格式不同的数据。

62. 视图的优点,缺点,讲一下?

  • 查询简单化。视图能简化用户的操作
  • 数据安全性。视图使用户能以多种角度看待同一数据,能够对机密数据提供安全保护
  • 逻辑数据独立性。视图对重构数据库提供了一定程度的逻辑独立性

63. count(1)、count(*) 与 count(列名) 的区别?

  • count(*)包括了所有的列,相当于行数,在统计结果的时候,不会忽略列值为NULL
  • count(1)包括了忽略所有列,用1代表代码行,在统计结果的时候,不会忽略列值为NULL
  • count(列名)只包括列名那一列,在统计结果的时候,会忽略列值为空(这里的空不是只空字符串或者0,而是表示null)的计数,即某个字段值为NULL时,不统计。

64. 什么是游标?

游标提供了一种对从表中检索出的数据进行操作的灵活手段,就本质而言,游标实际上是一种能从包括多条数据记录的结果集中每次提取一条记录的机制。

65. 什么是存储过程?有哪些优缺点?

存储过程,就是一些编译好了的SQL语句,这些SQL语句代码像一个方法一样实现一些功能(对单表或多表的增删改查),然后给这些代码块取一个名字,在用到这个功能的时候调用即可。

优点:

  • 存储过程是一个预编译的代码块,执行效率比较高
  • 存储过程在服务器端运行,减少客户端的压力
  • 允许模块化程序设计,只需要创建一次过程,以后在程序中就可以调用该过程任意次,类似方法的复用
  • 一个存储过程替代大量T_SQL语句 ,可以降低网络通信量,提高通信速率
  • 可以一定程度上确保数据安全

缺点:

  • 调试麻烦
  • 可移植性不灵活
  • 重新编译问题

66. 什么是触发器?触发器的使用场景有哪些?

触发器,指一段代码,当触发某个事件时,自动执行这些代码。

使用场景:

  • 可以通过数据库中的相关表实现级联更改。
  • 实时监控某张表中的某个字段的更改而需要做出相应的处理。
  • 例如可以生成某些业务的编号。
  • 注意不要滥用,否则会造成数据库及应用程序的维护困难。

67. MySQL中都有哪些触发器?

MySQL 数据库中有六种触发器:

  • Before Insert
  • After Insert
  • Before Update
  • After Update
  • Before Delete
  • After Delete

68. 超键、候选键、主键、外键分别是什么?

  • 超键:在关系模式中,能唯一知标识元组的属性集称为超键。
  • 候选键:是最小超键,即没有冗余元素的超键。
  • 主键:数据库表中对储存数据对象予以唯一和完整标识的数据列或属性的组合。一个数据列只能有一个主键,且主键的取值不能缺失,即不能为空值(Null)。
  • 外键:在一个表中存在的另一个表的主键称此表的外键。。

69. SQL 约束有哪几种呢?

  • NOT NULL: 约束字段的内容一定不能为NULL。
  • UNIQUE: 约束字段唯一性,一个表允许有多个 Unique 约束。
  • PRIMARY KEY: 约束字段唯一,不可重复,一个表只允许存在一个。
  • FOREIGN KEY: 用于预防破坏表之间连接的动作,也能防止非法数据插入外键。
  • CHECK: 用于控制字段的值范围。

70. 谈谈六种关联查询,使用场景。

  • 交叉连接
  • 内连接
  • 外连接
  • 联合查询
  • 全连接
  • 交叉连接

71. varchar(50)中50的涵义

  • 字段最多存放 50 个字符
  • 如 varchar(50) 和 varchar(200) 存储 “jay” 字符串所占空间是一样的,后者在排序时会消耗更多内存

72. mysql中int(20)和char(20)以及varchar(20)的区别

  • int(20) 表示字段是int类型,显示长度是 20
  • char(20)表示字段是固定长度字符串,长度为 20
  • varchar(20) 表示字段是可变长度字符串,长度为 20

73. drop、delete与truncate的区别

delete truncate drop
类型 DML DDL DDL
回滚 可回滚 不可回滚 不可回滚
删除内容 表结构还在,删除表的全部或者一部分数据行 表结构还在,删除表中的所有数据 从数据库中删除表,所有的数据行,索引和权限也会被删除
删除速度 删除速度慢,逐行删除 删除速度快 删除速度最快

74. UNION与UNION ALL的区别?

  • Union:对两个结果集进行并集操作,不包括重复行,同时进行默认规则的排序;
  • Union All:对两个结果集进行并集操作,包括重复行,不进行排序;
  • UNION的效率高于 UNION ALL

75. SQL的生命周期?

  • 服务器与数据库建立连接
  • 数据库进程拿到请求sql
  • 解析并生成执行计划,执行
  • 读取数据到内存,并进行逻辑处理
  • 通过步骤一的连接,发送结果到客户端
  • 关掉连接,释放资源

76. 一条Sql的执行顺序?

77. 列值为NULL时,查询是否会用到索引?

列值为NULL也是可以走索引的

计划对列进行索引,应尽量避免把它设置为可空,因为这会让 MySQL 难以优化引用了可空列的查询,同时增加了引擎的复杂度

78. 关心过业务系统里面的sql耗时吗?统计过慢查询吗?对慢查询都怎么优化过?

  • 我们平时写Sql时,都要养成用explain分析的习惯。
  • 慢查询的统计,运维会定期统计给我们

优化慢查询:

  • 分析语句,是否加载了不必要的字段/数据。
  • 分析SQl执行句话,是否命中索引等。
  • 如果SQL很复杂,优化SQL结构
  • 如果表数据量太大,考虑分表

可以看我这篇文章哈:
后端程序员必备:书写高质量SQL的30条建议

79. 主键使用自增ID还是UUID,为什么?

如果是单机的话,选择自增ID;如果是分布式系统,优先考虑UUID吧,但还是最好自己公司有一套分布式唯一ID生产方案吧。

  • 自增ID:数据存储空间小,查询效率高。但是如果数据量过大,会超出自增长的值范围,多库合并,也有可能有问题。
  • uuid:适合大量数据的插入和更新操作,但是它无序的,插入数据效率慢,占用空间大。

80. mysql自增主键用完了怎么办?

自增主键一般用int类型,一般达不到最大值,可以考虑提前分库分表的。

81. 字段为什么要求定义为not null?

null值会占用更多的字节,并且null有很多坑的。

82. 如果要存储用户的密码散列,应该使用什么字段进行存储?

密码散列,盐,用户身份证号等固定长度的字符串,应该使用char而不是varchar来存储,这样可以节省空间且提高检索效率。

83. Mysql驱动程序是什么?

这个jar包: mysql-connector-java-5.1.18.jar

Mysql驱动程序主要帮助编程语言与 MySQL服务端进行通信,如连接、传输数据、关闭等。

84. 如何优化长难的查询语句?有实战过吗?

  • 将一个大的查询分为多个小的相同的查询
  • 减少冗余记录的查询。
  • 一个复杂查询可以考虑拆成多个简单查询
  • 分解关联查询,让缓存的效率更高。

85. 优化特定类型的查询语句

平时积累吧:

  • 比如使用select 具体字段代替 select *
  • 使用count(*) 而不是count(列名)
  • 在不影响业务的情况,使用缓存
  • explain 分析你的SQL

可以看我这篇文章哈:
后端程序员必备:书写高质量SQL的30条建议

86. MySQL数据库cpu飙升的话,要怎么处理呢?

排查过程:

  • 使用top 命令观察,确定是mysqld导致还是其他原因。
  • 如果是mysqld导致的,show processlist,查看session情况,确定是不是有消耗资源的sql在运行。
  • 找出消耗高的 sql,看看执行计划是否准确, 索引是否缺失,数据量是否太大。

处理:

  • kill 掉这些线程(同时观察 cpu 使用率是否下降),
  • 进行相应的调整(比如说加索引、改 sql、改内存参数)
  • 重新跑这些 SQL。

其他情况:

也有可能是每个 sql 消耗资源并不多,但是突然之间,有大量的 session 连进来导致 cpu 飙升,这种情况就需要跟应用一起来分析为何连接数会激增,再做出相应的调整,比如说限制连接数等

87. 读写分离常见方案?

  • 应用程序根据业务逻辑来判断,增删改等写操作命令发给主库,查询命令发给备库。
  • 利用中间件来做代理,负责对数据库的请求识别出读还是写,并分发到不同的数据库中。(如:amoeba,mysql-proxy)

88. MySQL的复制原理以及流程

主从复制原理,简言之,就三步曲,如下:

  • 主数据库有个bin-log二进制文件,纪录了所有增删改Sql语句。(binlog线程)
  • 从数据库把主数据库的bin-log文件的sql语句复制过来。(io线程)
  • 从数据库的relay-log重做日志文件中再执行一次这些sql语句。(Sql执行线程)

如下图所示:

上图主从复制分了五个步骤进行:

步骤一:主库的更新事件(update、insert、delete)被写到binlog

步骤二:从库发起连接,连接到主库。

步骤三:此时主库创建一个binlog dump thread,把binlog的内容发送到从库。

步骤四:从库启动之后,创建一个I/O线程,读取主库传过来的binlog内容并写入到relay log

步骤五:还会创建一个SQL线程,从relay log里面读取内容,从Exec_Master_Log_Pos位置开始执行读取到的更新事件,将更新内容写入到slave的db

89. MySQL中DATETIME和TIMESTAMP的区别

存储精度都为秒

区别:

  • DATETIME 的日期范围是 1001——9999 年;TIMESTAMP 的时间范围是 1970——2038 年
  • DATETIME 存储时间与时区无关;TIMESTAMP 存储时间与时区有关,显示的值也依赖于时区
  • DATETIME 的存储空间为 8 字节;TIMESTAMP 的存储空间为 4 字节
  • DATETIME 的默认值为 null;TIMESTAMP 的字段默认不为空(not null),默认值为当前时间(CURRENT_TIMESTAMP)

90. Innodb的事务实现原理?

  • 原子性:是使用 undo log来实现的,如果事务执行过程中出错或者用户执行了rollback,系统通过undo log日志返回事务开始的状态。
  • 持久性:使用 redo log来实现,只要redo log日志持久化了,当系统崩溃,即可通过redo log把数据恢复。
  • 隔离性:通过锁以及MVCC,使事务相互隔离开。
  • 一致性:通过回滚、恢复,以及并发情况下的隔离性,从而实现一致性。

91. 谈谈MySQL的Explain

Explain 执行计划包含字段信息如下:分别是 id、select_type、table、partitions、type、possible_keys、key、key_len、ref、rows、filtered、Extra 等12个字段。

我们重点关注的是type,它的属性排序如下:

1
2
3
复制代码system  > const > eq_ref > ref  > ref_or_null >
index_merge > unique_subquery > index_subquery >
range > index > ALL

推荐大家看这篇文章哈:
面试官:不会看 Explain执行计划,简历敢写 SQL 优化?

92. Innodb的事务与日志的实现方式

有多少种日志

innodb两种日志redo和undo。

日志的存放形式

  • redo:在页修改的时候,先写到 redo log buffer 里面, 然后写到 redo log 的文件系统缓存里面(fwrite),然后再同步到磁盘文件( fsync)。
  • Undo:在 MySQL5.5 之前, undo 只能存放在 ibdata文件里面, 5.6 之后,可以通过设置 innodb_undo_tablespaces 参数把 undo log 存放在 ibdata之外。

事务是如何通过日志来实现的

  • 因为事务在修改页时,要先记 undo,在记 undo 之前要记 undo 的 redo, 然后修改数据页,再记数据页修改的 redo。 Redo(里面包括 undo 的修改) 一定要比数据页先持久化到磁盘。
  • 当事务需要回滚时,因为有 undo,可以把数据页回滚到前镜像的 状态,崩溃恢复时,如果 redo log 中事务没有对应的 commit 记录,那么需要用 undo把该事务的修改回滚到事务开始之前。
  • 如果有 commit 记录,就用 redo 前滚到该事务完成时并提交掉。

93. MySQL中TEXT数据类型的最大长度

  • TINYTEXT:256 bytes
  • TEXT:65,535 bytes(64kb)
  • MEDIUMTEXT:16,777,215 bytes(16MB)
  • LONGTEXT:4,294,967,295 bytes(4GB)

94. 500台db,在最快时间之内重启。

  • 可以使用批量 ssh 工具 pssh 来对需要重启的机器执行重启命令。
  • 也可以使用 salt(前提是客户端有安装 salt)或者 ansible( ansible 只需要 ssh 免登通了就行)等多线程工具同时操作多台服务

95. 你是如何监控你们的数据库的?你们的慢日志都是怎么查询的?

监控的工具有很多,例如zabbix,lepus,我这里用的是lepus

96. 你是否做过主从一致性校验,如果有,怎么做的,如果没有,你打算怎么做?

主从一致性校验有多种工具 例如checksum、mysqldiff、pt-table-checksum等

97. 你们数据库是否支持emoji表情存储,如果不支持,如何操作?

更换字符集utf8–>utf8mb4

98. MySQL如何获取当前日期?

SELECT CURRENT_DATE();

99. 一个6亿的表a,一个3亿的表b,通过外间tid关联,你如何最快的查询出满足条件的第50000到第50200中的这200条数据记录。

1、如果A表TID是自增长,并且是连续的,B表的ID为索引
select * from a,b where a.tid = b.id and a.tid>500000 limit 200;

2、如果A表的TID不是连续的,那么就需要使用覆盖索引.TID要么是主键,要么是辅助索引,B表ID也需要有索引。
select * from b , (select tid from a limit 50000,200) a where b.id = a .tid;

100. Mysql一条SQL加锁分析

一条SQL加锁,可以分9种情况进行:

  • 组合一:id列是主键,RC隔离级别
  • 组合二:id列是二级唯一索引,RC隔离级别
  • 组合三:id列是二级非唯一索引,RC隔离级别
  • 组合四:id列上没有索引,RC隔离级别
  • 组合五:id列是主键,RR隔离级别
  • 组合六:id列是二级唯一索引,RR隔离级别
  • 组合七:id列是二级非唯一索引,RR隔离级别
  • 组合八:id列上没有索引,RR隔离级别
  • 组合九:Serializable隔离级别

公众号

  • 欢迎关注我个人公众号,交个朋友,一起学习哈~
  • 如果答案整理有错,欢迎指出哈,感激不尽~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

炸裂!万字长文拿下HTTP 我在字节跳动等你!

发表于 2020-05-24

本文将从以下几个方面进行分享。其中包括HTTP发展史,HTTP缓存代理机制,常用的web攻击,HTTP和HTTPS的流量识别,网络协议学习的工具推荐以及高频HTTP与HTTPS的高频面试题题解等,开工。ps(如果需要带目录pdf,私我)

提纲

提纲

1989年,蒂姆·伯纳斯 - 李(Tim Berners-Lee)在论文中提出可以在互联网上构建超链接文档,并提出了三点.

URI:统一资源标识符。互联网的唯一ID

HTML:超文本文档

HTTP:传输超文本的文本传输协议

1 HTTP应用在哪儿

学习一门知识,采用五分钟时间看看这个知识是干啥的可能会更加有目的性。HTTP可谓无处不在,这里例举出几个。

HTTP应用场景

HTTP应用场景

2 HTTP是什么

HTTP(hypertext transport protocol)翻译过来为”超文本传输协议”,文本可以理解为简单的字符文字组合,也可以理解为更为复杂的音频或者图像等。那么将这个词语拆分为三个部分。

超文本传输协议

超文本传输协议

“超文本”和”文本”相比多了一个字”超”,这样看来比文本丰富,因为它可以将多种文本/图像等进行混合,更重要的是可以从一个文本跳转到另一个文本(文本连接)。

“传输”,传输的过程中需要沟通,沟通即可能一对一沟通也可能一对多沟通(进行内容协商),无论怎么样,参加沟通的人数>1,想尽一切一切办法更快更好的完成相应的任务。

“协议”,无规矩不成方圆,做机密项目之前需要签署保密协议,找工作要签”三方协议”,三方协议是学校,公司,和个人组成的协议,都是为了让大家受一定的约束,违反了即有相应的惩罚。

三方协议

三方协议

3 不同版本的HTTP

HTTP/0.9

当时网络资源匮乏,0.9版本相对简单,采用纯文本格式,且设置为只读,所以当时只能使用”Get”的方式从服务器获得HTML文档,响应以后则关闭。如下图所示

1
复制代码GET /Mysite.html

响应中只包含了文档本身。响应内容无响应头,无错误码,无状态码,可以说是”裸奔”。

1
2
3
4
复制代码<HTML>  
Hello world
</HTML>
HTTP/1.0

此时HTTP/0.9请求过程如下

  • 应用层的HTTP建立在传输层的TCP之上并运用TCP可靠等特性,先三次握手建立连接
  • 客户端请求建立连接(此时只有GET)
  • 服务端响应请求,数据以 ASCII 字符流返回给客戶端
  • 传输完成,断开连接。

HTTP 0.9

HTTP 0.9

HTTP1.0

随着时代的进步,仅仅文本的传输无法满足需求,更多情况需要采用图文的方式才能生动的表达出自己的观点。随着1995年开发出Apache,同时其他的多媒体等技术发展迅速,从而进一步的促使HTTP新功能的出现。HTTP1.0在1996年诞生,增加了一下几个方面:

  • 之前只有Get方法,现在增加Post(加参数),Head方法
  • 加入协议版本号,同时添加文件处理类型
  • 加入HTTP Header,让HTTP处理请求更加灵活
  • 增加响应状态码,标记出错的原因
  • 提供国际化(不同语言)支持

典型的请求过程

1
2
3
4
5
6
7
8
9
10
复制代码GET /image.html HTTP/1.0  
User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64)

200 OK
Date: Tue, 17 Nov 2020 09:15:31 GMT
Content-Type: text/html
<HTML> 
一个包含图片的页面
  <IMG SRC="/image.gif">
</HTML>

HTTP1.0通信过程

HTTP1.0

HTTP1.0

HTTP /1.1

1995年是不平凡的一年,网景公司和微软开启浏览器大战,谁都想当老大。1999年HTTP/1.1发布并成为标准,写入RFC,以为以后不管是网关还是APP等,只要你要使用HTTP,就得遵守这个标准。

  • 继续增加了PUT等方法
  • 允许持久连接

随着文件越来越大,图片等信息越来越复杂,如果每一次上传下载文件都需要建立连接断开连接的过程将增加大量的开销。为此,提出了持久连接,也就是一次TCP连接可以具有多个HTTP请求。当然持久连接是可选择的,如果考虑关闭,只需要使用Connecttion:close关闭即可。长连接如下图所示

长连接

长连接

  • 强制要求Host头

我们知道,在电商系统中,经常会因为促销活动导致流量飙升,为了缓解流量,其中有种方法即加缓存或者加服务器。如果是单台服务器负载过大,数据库可能分库分表。数据结构算法中分而治之方法亦是如此。那么HTTP中,同样的道理,如果文件太大,就大文件切分为小文件块发送。

HTTP /2

HTTP/1.1的出现,几年间出来大量牛掰的互联网公司,发展实在是太快,但是HTTP1.1中这几点成为诟病

  • 原因1 TCP自带慢启动

顾名思义,”慢启动”从0到1循循渐进。轿车启动不会按下按钮就直接起飞,而是缓慢调节到适合的速度。这不是挺好的?为什么会带来性能问题呢。我们知道一个页面有静态数据,动态页面,很多小文件在加载的过程中就会直接发起请求,这样导致太多的请求都会经历慢启动过程,花费时间太多。

  • 原因2 多条TCP连接带宽竞争

带宽固定,多条TCP连接同时发起竞争带宽资源,由于各个TCP连接之间没有通信机制,也无法得知哪些资源优先级更高,从而导致想快速下载的资源反而延迟下载。

  • 原因3 头部阻塞

阻塞,在网络编程中,我们采用异步,多路复用(epoll)方式尽量让cpu少等待多干事。在HTTP1.1中,虽然大家共用了一条TCP通道,但是第一个请求没有结束,第二请求就阻塞等待,也就是说不能同时发送接收数据。那么一个网页很多数据文件,如果能够同时发出请求,让部分数据文件能够得到响应并预处理,这样就大大的利用了带宽和cpu的资源。基于这些因素,出现了HTTP2

如何解决头部阻塞呢?

HTTP是一问一答的模式,大家都在这个队列排队导致堵塞,那就多个队列并发进行,也就是”对同一个域名发起多个长连接”。举个例子,在火车站排队买票的时候,如果只有一个窗口可用,大家只能苦等,多开几个窗口就可缓解这个问题。

这个时候用户数 * 并发数(上限6-8)已经不错得效果,但是互联网速度太快,火车站就这么大,窗口也就这么多,怎么办,建新的火车站进行分流(大部分城市都有什么东站 西站)。在这里叫做”域名分片”,使用多个域名,这些域名指向指向同一服务器。

HTTP/3

HTTP/2看似很完美了吧,但是Google轮子哥可不服,其他人在研究HTTP/2的时候,它们就在琢磨QUIC。那QUIC有啥牛掰的地方呢

QUIC是Google开发的一个基于UDP且能像TCP一样具有可靠性特点的协议。具备像HTTP/2一样的应用数据二进制分帧传输。其主要解决的问题有两个。

  1. 进一步解决线头阻塞问题。通过独立不同流,让各个流之间实现相互独立传输,互不干扰
  2. 切换网络时的连接保持。wifi和3G/4G经常需要来回切换。基于TCP的协议,会因为网络的切换导致IP地址的改变。而基于UDP的QUIC协议,及时切换也可以恢复之前与服务器的连接。

4 HTTP报文详解

客户端与服务端进行交互的信息为报文。客户端为请求报文,服务端为响应报文。我们先用wireshark抓一个博客看看

报文层次结构

报文层次结构

1
2
3
4
5
6
7
8
9
10
复制代码GET /article/12 HTTP/1.1  
Host: www.xxx.cn
Connection: keep-alive
Cache-Control: max-age=0
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.106 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: SESSION=so9nlsvenminor5abs65sh9dsa
1
2
3
4
5
6
7
8
9
复制代码HTTP/1.1 200 OK  
Server: nginx
Date: Sun, 17 May 2020 17:04:29 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: keep-alive
Vary: Accept-Encoding
X-Powered-By: blade-2.0.6-BETA
Content-Encoding: gzip

请求报文

请求报文

请求报文

请求报文通常由三部分组成:

起始行:描述请求或者响应的基本信息

头部字段集合:key-value形式说明报文

消息正文:实际传输诸如图片等信息。具体如下图试试

1 请求方法:一共有八种方法选择,如下图所示。采用不同的方法获取不同的资源

HTTP请求方法详解

HTTP请求方法详解

说一下非常常见的几种请求方法

Get:从服务器中取资源。可以请求图片,视频等

HEAD:和Get类似,但是从服务器请求的资源不会返回请求的实体数据,只会返回响应头

POST/PUT:对应于GET,向服务器发送数据

2 URI

统一资源标识符(Uniform Resource Identifier),严格来说不等于网址,它包含URL和URN,可是URL太出名了以致于URL=”网址”。无论开发,测试运维配置都离不开URI,所以好好掌握。

网络层的IP主要目的是解决路由和寻址。现在的IP地址按照”.”分割,总共2的32次方大约42亿。对于计算机来说比较方便,但是对于人类来说还是不容易记忆,此时出现DNS了,他把IP地址映射为我们平时常见的”redis.org”,按照.分割域名,从左到右级别越高,最右边为”顶级域名”。如下图所示

域名体系

域名体系

好了,现在TCP提供可靠(数据不丢失)且字节流(数据完整性),而且也有方便我们记忆的域名,但是互联网资源千万种,也不知道访问什么(图片,文字,视频一大堆),这个时候URI(统一资源标识符)出现了,那长啥样?

URI格式

URI格式

协议名:HTTP协议,另外还有ftp等协议。告知访问资源时使用什么协议。

紧接着是分隔符:”://“

主机名:标记互联网主机,可以是IP也可以是域名,如果不写端口则使用默认端口,例如HTTP为80,HTTPS为443.

登录认证信息:登录主机时的用户名密码(不建议,直接告诉了别人你的隐私信息)

主机名:此处可以是域名也可以是IP,如果不写端口号则是默认端口。比如HTTP默认端口为80,HTTPS默认端口为443

资源所在位置:资源在主机上的位置,使用“/”分隔多级目录,在这里是“/en/download.html”。注意,必须”/“开头

参数:用”?”开始,表示额外的请求要求。通常使用”key=value”的方式存在,如果多个”key=value”则使用”&”相连。

看几个例子

http://nginx.org/en/download.html

file:///E:/Demo/index/

这里注意是三个”///“,因为前面”://“作为分隔符,资源路径按照”/“开头。

既然规则这么多,对于接收方而言需要完成的解析也需要遵守规则,全球用户很多使用HTTP,每个国家地区所使用语言不同,HTTP为了能对其进行统一处理,引入了URI编码,方法比较简单,将非ASCII或者特殊字符全部转换为十六进制字节值,同时在前面加入”%”。比如空格被转换为”%20”,”中国”就编码为”%E4%B8%AD%E5%9B%BD%0A”。

3 请求体

响应报文

响应报文

响应报文

状态行—-服务器响应的状态

<1> 版本号:使用的HTTP什么版本

<2> 状态码:不同数字代表不同的结果,就如我们在编码时,通过返回不同的值代表不同的语义。

状态码一共分为5类。

1
复制代码1××:处于中间状态,还需后续操作
1
复制代码2××:成功收到报文并正确处理

“200 OK”

最常见的成功状态码,表示一切正常,客户端获得期许的处理结果。如果不是Head请求,那么在响应头中通常会有body数据。

“204 No Content”

这个的含义和”200”很相似,不同之处在于它的响应头中没有body数据。

“206 Partial Content”

是 HTTP 分块下载或断点续传的基础,在客户端发送“范围请求”、要求获取资源的部分数据时出现,它与 200 一样,也是服务器成功处理了请求,但 body 里的数据不是资源的全部,而是其中的一部分。状态码 206 通常还会伴随着头字段“Content-Range”,表示响应报文里 body 数据的具体范围,供客户端确认,例如“Content-Range: bytes 0-99/5000”,意思是此次获取的是总计 5000 个字节的前 100 个字节。

1
复制代码3××:重定向到其他资源位置

“301 Moved Permanently”

“永久重定向”,意思是本地请求的资源以及不存在,使用新的URI再次访问。

“302 Found”

“Moved Temporarily”,“临时重定向”,临时则所请求的资源暂时还在,但是目前需要用另一个URI访问。

301 和 302 通过在字段Location中表明需要跳转的URI。两者最大的不同在于一个是临时改变,一个是永久改变。举个例子,有时候需要将网站全部升级为HTTPS,这种永久性改变就需要配置永久的”301”。有时候晚上更新系统,系统暂时不可用,可以配置”302”临时访问,此时不会做缓存优化,第二天还会访问原来的地址。

“304 Not Modified”

运用于缓存控制。它用于 If-Modified-Since 等条件请求,表示资源未修改,可以理解成“重定向已到缓存的文件”(即“缓存重定向”)。

1
复制代码4××:请求报文有误,服务器无法处理

“400 Bad Request”

通用错误码,表示请求报文有错误,但是这个错误过于笼统。不知道是客户端还是哪里的错误,所以在实际应用中,通常会返回含有明确含义的状态码。

“403 Forbidden”

注意了,这一个是表示服务器禁止访问资源。原因比如涉及到敏感词汇、法律禁止等。当然,如果能让客户端有一个清晰的认识,可以考虑说明拒绝的原因并返回即可。

“404 Not Found”

这可能是我们都知道且都不想看到的状态码之一,它的本意是想要的资源在本地未找到从而无法提供给服务端,但是现在,只要服务器”耍脾气”就会给你返回 404,而我们也无从得知后面到底是真的未找到,还是有什么别的原因,

“405 Method Not Allowed”

获取资源的方法好几种,我们可以对某些方法进行限制。例如不允许 POST 只能 GET;

“406 Not Acceptable”

客户端资源无法满足客户端请求的条件,例如请求中文但只有英文;

“408 Request Timeout”

请求超时,服务器等待了过长的时间;

“409 Conflict”:

多个请求发生了冲突,可以理解为多线程并发时的竞态;

413 Request Entity Too Large:

请求报文里的 body 太大;

414 Request-URI Too Long:请求行里的 URI 太大;

429 Too Many Requests:客户端发送了太多的请求,
通常是由于服务器的限连策略;

431 Request Header Fields Too Large:请求头某个字
段或总体太大;

1
复制代码5××:服务器错误,服务器对请求出的时候发生内部错误。

“500 Internal Server Error”

和400 类似,属于一个通用的错误码,但是服务器到底是什么错误我们不得而知。其实这是好事,尽量少的将服务器资源暴露外网,尽量保证服务器的安全。

“502 Bad Gateway”

通常是服务器作为网关或者代理时返回的错误码,表示服务器自身工作正常,访问后端服务器时发生了错误,但具体的错误原因也是不知道的。

“503 Service Unavailable”

表示服务器当前很忙,暂时无法响应服务,我们上网时有时候遇到的“网络服务正忙,请稍后重试”的提示信息就是状态码 503。

503 是一个“临时”的状态,

暂时比较忙,稍后提供服务。在响应报文中的“Retry-After”字段,指示客户端可以在多久以后再次尝试发送请求。

4 请求体

上面大部分都是涉及到header部分,还有非常重要的body,everybody

头字段注意事项

<1> 字段名不区分大小写,例如“Host”也可以写成“host”,但首字母大写的可读性更好;

<2> 字段名里不允许出现空格,可以使用连字符“-”,但不能使用下划线”_“。例如,“test-name”是合法的字段名,而“test name”“test_name”是不正确的字段名;

<3> 字段名后面必须紧接着“:”,不能有空格,而”:”后的字段值前可以有多个空格;

<4> 字段的顺序是没有意义的,可以任意排列不影响语义;

<5> 字段原则上不能重复,除非这个字段本身的语义允许,例如 Set-Cookie。

HTTP的body常常被分为这几种的类别

<1> text:超文本text/html,纯文本text/plain

<2> audio/video:音视频数据

<3> application: 可能是文本,也可能是二进制,交给上层应用处理

<4> image: 图像文件。image/png等

但是带宽一定,数据大了通常考虑使用压缩算法进行压缩,在HTTP中使用Encoding type表示,常用的压缩方式有下面几种

<1> gzip:

一种数据格式,默认且目前仅使用deflate算法压缩data部分

<2> deflate:

deflate是一种压缩算法,是huffman编码的一种加强

<3> br:

br通过变种的LZ77算法、Huffman编码以及二阶文本建模等方式进行数据压缩,其他压缩算法相比,它有着更高的压塑压缩效率

使用相应的压缩方法在带宽一定的情况下确实有不错的效果,但是gzip等主要针对文件压缩效果不错,但是对视频就不行了。这个时候是不是可以使用数据结构中常用的分而治之,大化小再合并的方式呢,

文件拆分

文件拆分

ok,在报文中使用”Transer-Encoding:chunked”表示,代表body部分数据是分块传输的。另外在body中存在一个content-length字段表示body的长度,两者不能共存,另外很多时候是流式数据,body中没有指明content-length,这个时候一般就是chunked传输了。

现在可以通过采用分块的方式增强带宽的利用率,那他的编码规则如何呢

<1> 每一个分块包含长度和数据块

<2> 长度头按照CRLF结束

<3> 数据块在长度快后,且最后CRLF结尾

<4> 使用长度0表示结束,”0\r\n\r\n”

我们还是看图加深印象

chunked分块

chunked分块

分块解决了咋们一部分问题,但是有的时候我们想截断发送怎么办呢。在HTTP中提供了使用字段“Accept - Ranges: bytes”,明确告知客户端:“我是支持范围请求的”。那么Range范围是怎样的呢,Range从0开始计算,比如Range:0-5则读取前6个字节,服务器收到了这个请求,将如何回应呢

<1> 合法性检查。比如一共只有20字节,但是请求range:100-200。此时会返回416—-“范围请求有误”

<2> 范围正常,则返回216,表示请求数据知识一部分

<3> 服务器端在相应投资端增加Content-Range,格式”bytes x-y/length”。

敲黑板:断点续传怎么操作?

<1> 查看服务器是否支持范围请求并记录文件大小

<2> 多个线程分别负责不同的range

<3> 下载同时记录进度,即使因为网络等原因中断也没事,Range请求剩余即可

现在我们通过MIME-TYPE和Encoding-type可以知道body部分的类型,下一步将是对内容进行协商。HTTP中,请求体中使用Accept告诉服务端需要什么类型数据(我能处理哪些类型数据),响应头中使用Content表明发送了什么类型数据,具体如下图所示

好了,为了各个国家民族顺利友好的沟通和明确的区分。HTTP请求头中使用”type-subtype”,注意此时分隔符是”-“。比如en-GB表示英式英语,zh-CN表示常用的汉语,那对于客户端而言,它通过Accept-Language来标记自己可以理解的自然语言,对应的服务端使用Content-Language表明实体数据使用的语言类型,如下图所示。

字符集和编码

字符集和编码

Cookie机制

HTTP是无状态、无记忆的,Cookie机制的出现让其有记忆功能,是怎么个实现呢

Cookie

Cookie

从上图我们可以知道Cookie是由浏览器负责存储,并不是操作系统负责,我们换个浏览器打开同样的网页,服务就认不出来了。

Cookie常见的应用一个是身份识别,一个是广告追踪,比如我们在访问网页视频或者图片的时候,广告商会悄悄给我们Cookie打上标记,方便做关联分析和行为分析,从而给我推荐一些相关内容。

HTTP代理

之前介绍的都是一问一答的情景,但是在大部分的情况下都会存在多台服务器进行通信服务。其中比较常见的就是在请求方与应答方中间增加一个中间代理。

代理

代理

代理作为中间位置,相对请求方为服务端,相当于后端服务端为请求方。代理常见的功能为负载均衡。在负载均衡中需要区分正向代理与反向代理,其中也就会涉及调度算法,比如轮询还是一致性哈希等。

正向代理与反向代理

正向代理与反向代理

那么问题来了,代理作为隐藏身份,相当于隐藏了真实的客户端与服务端,那在是不是

5 HTTPS

好人占多数,坏人也不少。总有些要搞坏事,因为HTTP是明文,所以需要想办法保护明文,从而出现了https。

安全是什么

安全四要素

安全四要素

机密性

对信息进行保密,只能可信的人可以访问(让我想起时间管理者)。

完整性

数据在传输过程中内容不被”篡改”。虽然机密性对数据进行保密了,但是有上策也有下策(hack)

身份认证

证明自己的身份是本人,保证其消息发给可信的人

不可否认

君子一言驷马难追,说话算数,说过的话做过的事要有所保证

HTTPS

HTTP和HTTPS

HTTP和HTTPS

从上图我们知道HTTPS无非是在传输层和应用层中间加了一层TLS,正是TLS紧跟当代密码学的步伐,尽全力的保障用户的安全。老规矩,我们用wireshark看看长什么样子。

TLS

TLS

可以看出在交互的过程中多了不少新东西,了解TLS,TLS由SSL握手协议,SSL修改密码规范协议,SSL警报协议,SSL记录协议组成。

TLS组成

TLS组成

SSL握手协议:

相对于三次握手

记录协议

记录为TLS发送接收数据的基本单位。它的自协议需要通过记录协议发出。如果多个纪录数据则可以一个TCP包一次性发出。

警报协议

类似HTTP状态码,通过反馈不同的消息进行不同的策略。

变更密码规范协议

告诉对方,从此刻开始,后续的数据将使用加密算法进行加密再传输。

对称加密与非对称加密

对称加密

对称加密,顾名思义,加密方与解密方使用同一钥匙(秘钥)。具体一些就是,发送方通过使用相应的加密算法和秘钥,对将要发送的信息进行加密;对于接收方而言,使用解密算法和相同的秘钥解锁信息,从而有能力阅读信息。

对称加密

对称加密

非对称加密

在对称加密中,发送方与接收方使用相同的秘钥。那么在非对称加密中则是发送方与接收方使用的不同的秘钥。其主要解决的问题是防止在秘钥协商的过程中发生泄漏。比如在对称加密中,小蓝将需要发送的消息加密,然后告诉你密码是123balala,ok,对于其他人而言,很容易就能劫持到密码是123balala。那么在非对称的情况下,小蓝告诉所有人密码是123balala,对于中间人而言,拿到也没用,因为没有私钥。所以,非对称密钥其实主要解决了密钥分发的难题。如下图

非对称加密

非对称加密

其实我们经常都在使用非对称加密,比如使用多台服务器搭建大数据平台hadoop,为了方便多台机器设置免密登录,是不是就会涉及到秘钥分发。再比如搭建docker集群也会使用相关非对称加密算法。

混合加密

非对称加密算法,大多数是从数学问题演变而来,运算速度较慢。混合加密所谓取长补短。通信过程中使用RSA等解决密钥交换问题,然后使用随机数产生的在对称算法中的会话密钥,最后使用加密。对方使用私钥解密得到的秘闻取出会话秘钥,这样就实现了密钥交换。

混合加密

混合加密

通过混淆加密等方式完成了机密性任务,作为Hack只需要伪造发布公钥或者作为之间人窃听密文。但是我们知道安全是四要素,还需要保证数据的完整性,身份认证等。

摘要

摘要算法可以理解为一种特殊的”单向”加密算法,无密钥,不可逆。在平时项目中,应该大家都是用过MD5,SHA-1。但是在TLS中使用SHA-2。

假设小A转账5000给小C,小A加上SHA-2摘要。网站计算摘要并对比,如果一致则完整可信。

摘要可信

摘要可信

此时小B想修改小A给的money,这个时候网站计算摘要就会发现不一样,不可信

摘要不可信

摘要不可信

HTTPS请求建立连接过程

HTTP握手过程

HTTP握手过程

注意:

  1. 首先通过非对称加密建立通信过程
  2. 在握手阶段,为什么使用3个随机数,一方面防止「随机数 C」被猜出,另一方增加Session key随机性
  3. Client发出支持的「对称/非对称加密」算法
  4. server返回选用的「对称/非对称加密」算法
  5. Client对算法进行确认
  6. Server对算法进行确认

根据wireshak结果,对TLS进一步剖析。TCP三次握手建立连接,作为礼貌,Client先打招呼”Client Hello”。里面包含了Client的版本号、所支持的密码套件和随机数,如下图所示

Client Hello

Client Hello

Server端表示尊重,回复”Server Hello”,同时进行版本校对,给出随机数(Server Random),从Client算法列表中选择一个密码套件,在这里选择的”TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256”。

cipher Suite

cipher Suite

这里的”TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256”什么意思呢

密码套件选择椭圆曲线加RSA、AES、SHA256

双方通过证书验证身份。因为本机服务器选用了ECDHE算法,为了实现密钥交换算法,它会发送证书后把椭圆曲线的公钥(Server Params)连带”Server Key Exchange”消息发送出去。

Server Key Exchange

Server Key Exchange

意思是,刚才混合加密套件比较复杂,给你个算法参数,好好记住,别弄丢了。

ServerHelloDone

ServerHelloDone

随后服务端回复”hello done”告知打招呼完毕

打完招呼完毕后,客户端对证书进行核实。然后根据密码套件也生成椭圆曲线的公钥,用”Client Key Exchange”消息发给服务器

Client Key Exchange

Client Key Exchange

此时客户端和服务端都有了密钥交换的两个参数(Client Params、ServerParams),然后通过 ECDHE 算法算出了一个新的值,叫“Pre-Master”

有了主密钥和会话密钥,客户端发送“Change Cipher Spec”和“Finished”消息,最后将所有消息加上摘要发送给服务器验证。

服务器同样发送“Change Cipher Spec”和“Finished”消息,握手结束,开始进行HTTP请求与响应

4 初探域名

我们知道域名的出现让我们更容易记忆,按照”.”分割,越靠近右边级别越高。域名本质是一个名字空间系统,采用多级域名的方式区分不同的国家,公司等,作为一种身份的标识。

根域名服务器(Root DNS Server):管理顶级域名服务器,返回“com”“net”“cn”等顶级域名服务器的 IP 地址;

顶级域名服务器(Top-level DNS Server):管理各自域名下的权威域名服务器,比如
com 顶级域名服务器可以返回 apple.com 域名服务器的 IP 地址;

权威域名服务器(Authoritative DNS Server):管理自己域名下主机的 IP 地址,比如apple.com 权威域名服务器可以返回 www.apple.com 的 IP 地址**

6 HTTP特点小结

写到这里,说它简单是假的,简单的东西通常更具有扩展的可能性。根据需求的变更,越来越复杂。

1:灵活且易扩展,他的头部字段很多都是可定制且可扩展

2:应用广泛。各个领域都有涉及。”跨平台,跨语言”

3:无状态。没有记忆功能,少功能即少占用资源。另外无状态更容易搭建集群,通过负载均衡将请求转发到任意一台服务器。缺点是无法支持需要连续步骤的”事务”操作。我们知道TCP协议有11种状态,不同状态代表通信过程中不同的含义。同样操作系统中的进程也有执行,就绪,活动阻塞等多种状态。但是HTTP全程都是”懵逼”无状态。比如小华请求服务器获取视频X,服务器觉得可行就发给小华。小华还想获取视频Y,这时服务器不会记录之前的状态,也就不知道这两个请求是否是同一个,所以小华还得告诉服务器自己的身份。

4:明文。优点是能让开发人员通过wireshark工具更直观的调试。缺点即裸奔互联网,没隐私可言。

5:可靠传输。HTTP为应用层协议,基于TCP/IP,而TCP为“可靠”传输协议,因此HTTP能在请求应答中”可靠”传输数据。

6:应用层协议。应用层协议很多,其中常用的邮件协议SMTP,上传下载文件ftp,默认端口22/23,SSH远程登录(XSHELL)。这些应用层协议都太专一,而HTTP通过各种头部字段,实体数据的组合,并综合缓存代理等功能,不得不说是网络中的冠希哥。

7 HTTP识别(还原)

这里说的识别,通过代码层面(libpcap封装)实现HTTP的识别,也能进一步体现TCP/IP协议栈的分层特性。先看回忆一下IP头部格式。

IP头部

IP头部

注意头部中的协议字段,如果此字段值为0x0600则为TCP分组。当知道了是TCP分组后,是不是可以通过TCP头部中端口(80)就可以判断为HTTP呢,不能的,很多情况都会使用动态端口的方式进行部署。此时可以通过HTTP中的关键字进行判断。如果为HTTP,再通过头部字段中的”Content-type”,charset等确认文本信息,编码方式,最后采用解码算法进行还原。

8 HTTPS(密文)识别

方法一也是比较直接的方法是直接通过抓包工具,插件配置即可。这里想给大家分享另一种思路和在Linux持续捕包的方法。

  • 数据集采集

使用python的dpkt库(pip install dpkt即可),dpkt库方便对每一层协议进行拆解,同时也能进行流的拆分以及特征的提取。下面举一个通过无头浏览的方式自动化采集流量(ps如果需要较大规模的流量采集则可以考虑使用docker集群的方式)

Read_pcap

Read_pcap

  • 根据所提特征生成npz(实际上是numpy提供的数组存储方式)
  • 使用开源skearn库进行模型训练并识别预测,此处假设使用SVM(仅使用默认参数)

SVM

SVM

  • 识别结果(参数进行适度调整定会更好的效果)

识别结果

识别结果

9 HTTP面试题测试

希望大家看完本文,下面的这些面试是不是可以秒杀了

  • Get和Post区别
  • HTTP与HTTPS区别
  • HTTP通信过程
  • 游览器输入一个地址。到页面展示中间经历了哪些步骤?
  • cookies机制和session机制的区别:
  • HTTP请求报文与响应报文格式
  • 一次完整的HTTP请求所经历的7个步骤
  • HTTP优化方案
  • 不同版本的HTTP区别
  • HTTP优点缺点
  • URI和URL的区别
  • 如何判断是否为http
  • HTTP 1.1引入分块传输编码提供了以下几点好处
  • 长连接与短连接的区别,以及应用场景
  • 常见web攻击
  • 站内跳转和外部重定向有何区别
  • HTTP的keep-alive是干什么的?
  • 关于Http 2.0 你知道多少?
  • 讲讲304缓存的原理
  • HTTP与RPC异同
  • 从传输协议来说

RPC既可以基于TCP也可以基于HTTP协议,但是HTTP通常都是基于HTTP

  • 从性能消耗来说

RPC可以基于thrift实现高效二进制传输。HTTP大部分通过json实现,无论从字节大小还是序列化耗时都比t’hrift耗时

  • 从负载均衡来说

RPC基本上自带负载均衡策略,而HTTP需要配置Nginx实现。

唠嗑

第一篇文章能肝到这么长,也终于体会到各位大佬写文的不容易,不想被「 白嫖」,文末点个「 赞」吧,让我们一起「 看世界」。

Persist

https://www.chainnews.com/articles/401950499827.htm
https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Basics\_of\_HTTP/Evolution\_of\_HTTP

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

使用过LAMP和WAMP后我果断卸载,自己动手配置Wordp

发表于 2020-05-23

​ 作为一名Java开发者,我平时也喜欢学习除Java以外的其他技术,例如PHP(有人说PHP是最好的编程语言,我无心考究)。对于任何一个开发者,在学习一门新的编程语言过程中,首先都会面对配置开发环境这样一个环节。如果你是一名编程新手,在面对各种让人眼花缭乱的配置项时,可能会犯怵,不知道从何处开始下手。经过一通搜索后你会发现,原来还有集成环境这样爽的东西存在,大呼过瘾。

​ 集成环境确实是个好东西,免去了繁琐的配置过程,还有可视化的操作界面,省时省力,点击几下按钮,一个完整的开发环境就配置好了。很多的集成环境都会告诉你一句话:”你专注于编程这件事,剩下的其他事情就交给我了”,集成环境就像一种治疗编程入门的特效药,而且立竿见影,但也不是没有副作用的。它们在为你省时省力的同时,也省去了其中的技术细节,你只知道点几下按钮,却不知道为何要这样做,点完按钮后,系统干了什么事情你也不得而知;从另外一个角度上看,你原本只是想学习一门编程语言,现在却得先学会使用另外一种软件,同时还得为此软件花费一定得硬件开销(虽然现在的硬件成本不高)。

​ 从我个人的体验或者说从一名开发者的角度来看,我是不怎么推荐使用集成环境的,至少在学习阶段不推荐使用集成环境。下面是我从业过程中经理的一些小故事:

故事一:同事老A学习PHP开发3个年头,期间做过很多PHP项目,因为平时使用Windows系统,所以从一开始就使用WAMP集成环境开发软件,项目上线时,甲方告知老A服务器是Linux环境,且已有MySQL数据库,而且存放了其他系统的数据,甲方要求使用已有的MySQL数据库存放新系统的数据,这时老A犯难了,平时都只关注如何干好编码的事情,没怎么碰过Linux下手动配置运行环境的事情(小公司没有专门的运维!),只得加班捣鼓了两三天,在服务器装上了LAMP环境,但数据库链接有问题,SSL没有配置,域名夹带着端口在狂奔…

故事二:给甲方开发了一套软件,后期上线甲方要求由自己公司的技术员工实时,一周过去了,收到甲方反馈,软件有问题(无法启动),甲方派了一个技术人员到公司进行沟通,要求现场进行本地调试演示。当我看到甲方人员电脑界面时,我内心是拒绝的,基本环境没有配置完全,使用了第三方的集成环境(某夕夕宝塔,我并非诋毁某个软件),凌乱的软件包和毫无章法的配置(不知道是不会配置还是在网上拷贝的代码)把我自己都搞凌乱了。甲方人员抱怨道:”我使用这个软件都好几年了,一直都好好的,是你们的软件有问题…”

​ 我不是一个说教者,列举这两个小案例的目的也不是排斥集成环境。集成环境自有它的好处和优点,但我们在学习的过程中,还要自己花时间去弥补集成环境的短板。在学习的一开始,就应该学会如何手动去配置相关的环境,者不仅能够让你对所学的编程语言有一个宏观的了解,还能为之后的工作开启一个”隐藏技能”。集成环境玩得溜,只能说明这款软件很强大,让你愿意花时间去研究,但不代表其涉及到得技术你玩得很溜。

​ 其实,有时候手动配置运行环境并不比使用集成软件耗时间,甚至用时更短。手动配置在一定程度上更存粹,集成软件在获得环境上比手动配置有时间和操作上的优势,但有额外的硬件性能开销、学习成本、兼容性、以及集成软件自生带来的升级和安全等问题。

​ 相比之下,手动配置的适应性更强,可控性更高,综合成本更低,唯一不足是使用的复杂度比集成软件高,但从学习和积累经验的角度看,复杂度高并不是一件坏事,反而是一件值得去尝试的事情。接下来,我将花几分钟的时间,分享一下如何在Windows下手动搭建Wordpress运行环境(Apache Server + PHP 7 + MySQL + Wordpress)。

一、Apache Http Server

要运行Wordpress,一个Http Server是必不可少的组件,本文以Apache Http Server为例进行讲解(你还可以选择Nginx作为Http Server)。


首先,访问Apache Http Server官网:httpd.apache.org/ ,进入页面后点击左侧的”Download!”链接,在新页面中找到”Files for Mincrosoft Windows”链接并点击进入,如下图:

进入下载页面后,选择”Apache Lounge”项进行下载,如下图:

然后,将下载到本地的压缩包解压到合适的目录中,例如:”E:/Apache/“目录中。解压完成后,进入”E:/Apache/http-2.4.43-win64-VS16/Apache24/conf”目录(具体路径根据你解压的目录为准),找到httpd.conf文件并打开,改文件是Apache Http Server的配置文件。打开文件后,找到Define SRVROOT配置项,配置Server的根目录如下:

1
复制代码Define SRVROOT "E:/Apache/http-2.4.43-win64-VS16/Apache24"

修改完成后,保存文件并退出,使用命令行工具进入”E:/Apache/http-2.4.43-win64-VS16/Apache24/bin”目录,执行以下命令启动Apache Http Server:

1
复制代码E:/Apache/http-2.4.43-win64-VS16/Apache24> httpd.exe -k start

提示:启动成功后,控制台不会有任何信息输出。

你可以使用 httpd.exe -k stop 命令终止Apache Http Server运行,还可以使用httpd.exe -k restart命令重启Apache Http Server。

最后,打开浏览器并并访问http://localhost,如页面出现如下下图所示信息,则表明Apache Http Server已安装成功。

提示:Apache Http Server默认监听的端口好为80端口

二、PHP

Wordpress是使用PHP语言编写的内容管理系统(Content Management System,简称CMS)。因此,想要运行Wordpress,还需接着配置PHP环境。

首先,访问PHP官网:www.php.net/,点击顶部”Donlo…“Thread Safe”版本并下载,如下图:

将下载到本地的文件解压缩到合适的目录,例如:”E:/PHP7/“。解压完成后,进入PHP解压目录,找到php.ini-production修改为php.ini文件并打开,然后配置PHP的扩展目录:

1
复制代码extension_dir = "E:\PHP7\php-7.4.6-win32-vc15-x64\ext"

同时,将以下几项配置前的”#”注释取消:

1
2
复制代码extension = php_mysql.dll
extension = php_mysqli.dll

至此,PHP安装配置完成。

三、Apache Http Server & PHP

PHP安装配置完成后,我们需要再次打开Apache Http Server配置文件(httpd.conf),然后添加对PHP的支持,配置代码如下:

1
2
3
4
5
复制代码LoadModel php7_module "E:/PHP7/php-7.4.6-win32-vc15-x64/php7apache2_4.dll"
<FilesMatch \.php$>
SetHandler application/x-httpd-php
<FilesMatch>
PHPIniDir "E:/PHP7/php-7.4.6-win32-vc15-x64/php.ini" #设置PHP配置文件路径

接下来,为验证Apache Http Server和PHP是否成功整合,在Apache Http Server安装目录中找到htdocs目录,在其下新建一个名为hello.php的文件,打开并编辑内容如下:

1
2
3
复制代码<?php
phpinfo();
?>

最后,打开命令行工具,使用如下命令重启Apache Http Server:

1
2
复制代码httpd.exe -k stop
httpd.exe -k start

重启服务后,打开浏览器并访问:http://localhost/index.php,如果正常显示PHP配置信息,则表明Apache Http Server与PHP整合成功,如下图:

四、Wordpress

由于MySQL数据库的安装配置比较简单,在此不再赘述。访问Wordpress官网并下载最新版本的软件包到本地:wordpress.org/download/。

接下来,将压缩文件解压到Apache Http Server的htdocs目录下(注意去掉wordpress文件夹),如下图:

解压完成后,重新启动Apache Http Server。接着打开浏览器并访问:http://localhost/wp-admin/setup-config.php,进入Wordpress安装页面。

最后,根据个人喜好和实际需求,填写对应的数据,完成Wordpress的安装。

五、注意事项

如果在安装Wordpress的过程中,出现类似**Index of /**的文件列表页面,如下图:

请修改以下文件:

httpd.conf

将httpd.conf文件中<Directory “${SRVROOT}/htdocs”>的 AllowOverride None配置改为AllowOverride All。如下图:

.htaccess

在Apache Http Server的htdocs目录中创建名为**.htaccess**的文件,然后编辑内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码DirectoryIndex index.php index.html
# BEGIN WordPress
# The directives (lines) between `BEGIN WordPress` and `END WordPress` are
# dynamically generated, and should only be modified via WordPress filters.
# Any changes to the directives between these markers will be overwritten.
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.php$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.php [L]
</IfModule>

# END WordPress

文件保存后,重新刷新Wordpress页面,之前的**Index of /**问题将得到解决。

六、总结

本文介绍了如何在Windows操作系统下手动配置Apache Http Server 和PHP7,以及Apache Http Server和PHP的整合步骤,在此基础上还介绍了WordPress的安装以及注意事项。从总体上来看,手动配置PHP运行环境并不比集成软件来得慢,所需要操作的地方也并不是想象中的那么多,那么复杂,反而显得得干净利落。

未经允许,请勿转载

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

19 一文搞懂 Go Modules 前世今生及入门使用

发表于 2020-05-23

Hi,大家好。

我是明哥,在自己学习 Golang 的这段时间里,我写了详细的学习笔记放在我的个人微信公众号 《Go编程时光》,对于 Go 语言,我也算是个初学者,因此写的东西应该会比较适合刚接触的同学,如果你也是刚学习 Go 语言,不防关注一下,一起学习,一起成长。

在线博客:golang.iswbm.com
Github:github.com/iswbm/GolangCodingTime


在以前,Go 语言的的包依赖管理一直都被大家所诟病,Go官方也在一直在努力为开发者提供更方便易用的包管理方案,从最初的 GOPATH 到 GO VENDOR,再到最新的 GO Modules,虽然走了不少的弯路,但最终还是拿出了 Go Modules 这样像样的解决方案。

目前最主流的包依赖管理方式是使用官方推荐的 Go Modules ,这不前段时间 Go 1.14 版本发布,官方正式放话,强烈推荐你使用 Go Modules,并且有自信可以用于生产中。

本文会大篇幅的讲解 Go Modules 的使用,但是在那之前,我仍然会简要介绍一下前两个解决方案 GOPATH 和 go vendor 到底是怎么回事?我认为这是有必要的,因为只有了解它的发展历程,才能知道 Go Modules 的到来是有多么的不容易,多么的意义非凡。

  1. 最古老的 GOPATH

GOPATH 应该很多人都很眼熟了,之前在配置环境的时候,都配置过吧?

你可以将其理解为工作目录,在这个工作目录下,通常有如下的目录结构

每个目录存放的文件,都不相同

  • bin:存放编译后生成的二进制可执行文件
  • pkg:存放编译后生成的 .a 文件
  • src:存放项目的源代码,可以是你自己写的代码,也可以是你 go get 下载的包

将你的包或者别人的包全部放在 $GOPATH/src 目录下进行管理的方式,我们称之为 GOPATH 模式。

在这个模式下,使用 go install 时,生成的可执行文件会放在 $GOPATH/bin 下

如果你安装的是一个库,则会生成 .a 文件到 $GOPATH/pkg 下对应的平台目录中(由 GOOS 和 GOARCH 组合而成),生成 .a 为后缀的文件。

GOOS,表示的是目标操作系统,有 darwin(Mac),linux,windows,android,netbsd,openbsd,solaris,plan9 等

而 GOARCH,表示目标架构,常见的有 arm,amd64 等

这两个都是 go env 里的变量,你可以通过 go env 变量名 进行查看

至此,你可能不会觉得上面的方案会产生什么样的问题,直到你开始真正使用 GOPATH 去开发程序,就不得不开始面临各种各样的问题,其中最严重的就是版本管理问题,因为 GOPATH 根本没有版本的概念。

以下几点是你使用 GOPATH 一定会碰到的问题:

  • 你无法在你的项目中,使用指定版本的包,因为不同版本的包的导入方法也都一样
  • 其他人运行你的开发的程序时,无法保证他下载的包版本是你所期望的版本,当对方使用了其他版本,有可能导致程序无法正常运行
  • 在本地,一个包只能保留一个版本,意味着你在本地开发的所有项目,都得用同一个版本的包,这几乎是不可能的。
  1. go vendor 模式的过渡

为了解决 GOPATH 方案下不同项目下无法使用多个版本库的问题,Go v1.5 开始支持 vendor 。

以前使用 GOPATH 的时候,所有的项目都共享一个 GOPATH,需要导入依赖的时候,都来这里找,正所谓一山不容二虎,在 GOPATH 模式下只能有一个版本的第三方库。

解决的思路就是,在每个项目下都创建一个 vendor 目录,每个项目所需的依赖都只会下载到自己vendor目录下,项目之间的依赖包互不影响。在编译时,v1.5 的 Go 在你设置了开启 GO15VENDOREXPERIMENT=1 (注:这个变量在 v1.6 版本默认为1,但是在 v1.7 后,已去掉该环境变量,默认开启 vendor 特性,无需你手动设置)后,会提升 vendor 目录的依赖包搜索路径的优先级(相较于 GOPATH)。

其搜索包的优先级顺序,由高到低是这样的

  • 当前包下的 vendor 目录
  • 向上级目录查找,直到找到 src 下的 vendor 目录
  • 在 GOROOT 目录下查找
  • 在 GOPATH 下面查找依赖包

虽然这个方案解决了一些问题,但是解决得并不完美。

  • 如果多个项目用到了同一个包的同一个版本,这个包会存在于该机器上的不同目录下,不仅对磁盘空间是一种浪费,而且没法对第三方包进行集中式的管理(分散在各个角落)。
  • 并且如果要分享开源你的项目,你需要将你的所有的依赖包悉数上传,别人使用的时候,除了你的项目源码外,还有所有的依赖包全部下载下来,才能保证别人使用的时候,不会因为版本问题导致项目不能如你预期那样正常运行。

这些看似不是问题的问题,会给我们的开发使用过程变得非常难受,虽然我是初学者,还未使用过 go vendor,但能有很明显的预感,这个方案照样会另我崩溃。

  1. go mod 的横空出世

go modules 在 v1.11 版本正式推出,在最新发布的 v1.14 版本中,官方正式发话,称其已经足够成熟,可以应用于生产上。

从 v1.11 开始,go env 多了个环境变量: GO111MODULE ,这里的 111,其实就是 v1.11 的象征标志, go 里好像很喜欢这样的命名方式,比如当初 vendor 出现的时候,也多了个 GO15VENDOREXPERIMENT环境变量,其中 15,表示的vendor 是在 v1.5 时才诞生的。

GO111MODULE 是一个开关,通过它可以开启或关闭 go mod 模式。

它有三个可选值:off、on、auto,默认值是auto。

  1. GO111MODULE=off禁用模块支持,编译时会从GOPATH和vendor文件夹中查找包。
  2. GO111MODULE=on启用模块支持,编译时会忽略GOPATH和vendor文件夹,只根据 go.mod下载依赖。
  3. GO111MODULE=auto,当项目在$GOPATH/src外且项目根目录有go.mod文件时,自动开启模块支持。

go mod 出现后, GOPATH(肯定没人使用了) 和 GOVENDOR 将会且正在被逐步淘汰,但是若你的项目仍然要使用那些即将过时的包依赖管理方案,请注意将 GO111MODULE 置为 off。

具体怎么设置呢?可以使用 go env 的命令,如我要开启 go mod ,就使用这条命令

1
shell复制代码$ go env -w GO111MODULE="on"
  1. go mod 依赖的管理

接下来,来演示一下 go modules 是如何来管理包依赖的。

go mod 不再依靠 $GOPATH,使得它可以脱离 GOPATH 来创建项目,于是我们在家目录下创建一个 go_test 的目录,用来创建我的项目,详细操作如下:

接下来,进入项目目录,执行如下命令进行 go modules 的初始化

接下来很重要的一点,我们要看看 go install 把下载的包安装到哪里了?

上面我们观察到,在使用 go modules 模式后,项目目录下会多生成两个文件也就是 go.mod 和 go.sum 。

这两个文件是 go modules 的核心所在,这里不得不好好介绍一下。

go.mod 文件

go.mod 的内容比较容易理解

  • 第一行:模块的引用路径
  • 第二行:项目使用的 go 版本
  • 第三行:项目所需的直接依赖包及其版本

在实际应用上,你会看见更复杂的 go.mod 文件,比如下面这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码module github.com/BingmingWong/module-test

go 1.14

require (
example.com/apple v0.1.2
example.com/banana v1.2.3
example.com/banana/v2 v2.3.4
example.com/pear // indirect
example.com/strawberry // incompatible
)

exclude example.com/banana v1.2.4
replace(
golang.org/x/crypto v0.0.0-20180820150726-614d502a4dac => github.com/golang/crypto v0.0.0-20180820150726-614d502a4dac
golang.org/x/net v0.0.0-20180821023952-922f4815f713 => github.com/golang/net v0.0.0-20180826012351-8a410e7b638d
golang.org/x/text v0.3.0 => github.com/golang/text v0.3.0
)

主要是多出了两个 flag:

  • exclude: 忽略指定版本的依赖包
  • replace:由于在国内访问golang.org/x的各个包都需要fan墙,你可以在go.mod中使用replace替换成github上对应的库。

go.sum 文件

反观 go.sum 文件,就比较复杂了,密密麻麻的。

可以看到,内容虽然多,但是也不难理解

每一行都是由 模块路径,模块版本,哈希检验值 组成,其中哈希检验值是用来保证当前缓存的模块不会被篡改。hash 是以h1:开头的字符串,表示生成checksum的算法是第一版的hash算法(sha256)。

值得注意的是,为什么有的包只有一行

1
复制代码<module> <version>/go.mod <hash>

而有的包却有两行呢

1
2
复制代码<module> <version> <hash>
<module> <version>/go.mod <hash>

那些有两行的包,区别就在于 hash 值不一行,一个是 h1:hash,一个是 go.mod h1:hash

而 h1:hash 和 go.mod h1:hash两者,要不就是同时存在,要不就是只存在 go.mod h1:hash。那什么情况下会不存在 h1:hash 呢,就是当 Go 认为肯定用不到某个模块版本的时候就会省略它的h1 hash,就会出现不存在 h1 hash,只存在 go.mod h1:hash 的情况。[引用自 3]

go.mod 和 go.sum 是 go modules 版本管理的指导性文件,因此 go.mod 和 go.sum 文件都应该提交到你的 Git 仓库中去,避免其他人使用你写项目时,重新生成的go.mod 和 go.sum 与你开发的基准版本的不一致。

  1. go mod 命令的使用

  • go mod init:初始化go mod, 生成go.mod文件,后可接参数指定 module 名,上面已经演示过。
  • go mod download:手动触发下载依赖包到本地cache(默认为$GOPATH/pkg/mod目录)
  • go mod graph: 打印项目的模块依赖结构

  • go mod tidy :添加缺少的包,且删除无用的包
  • go mod verify :校验模块是否被篡改过
  • go mod why: 查看为什么需要依赖
  • go mod vendor :导出项目所有依赖到vendor下

  • go mod edit :编辑go.mod文件,接 -fmt 参数格式化 go.mod 文件,接 -require=golang.org/x/text 添加依赖,接 -droprequire=golang.org/x/text 删除依赖,详情可参考 go help mod edit

  • go list -m -json all:以 json 的方式打印依赖详情

如何给项目添加依赖(写进 go.mod)呢?

有两种方法:

  • 你只要在项目中有 import,然后 go build 就会 go module 就会自动下载并添加。
  • 自己手工使用 go get 下载安装后,会自动写入 go.mod 。

  1. 总结写在最后

如果让我用一段话来评价 GOPATH 和 go vendor,我会说

GOPATH 做为 Golang 的第一个包管理模式,只能保证你能用,但不保证好用,而 go vendor 解决了 GOPATH 忽视包版的本管理,保证好用,但是还不够好用,直到 go mod 的推出后,才使 Golang 包的依赖管理有了一个能让 Gopher 都统一比较满意的方案,达到了能用且好用的标准。

如果是刚开始学习 Golang ,那么 GOPATH 和 go vendor 可以做适当了解,不必深入研究,除非你要接手的项目由于一些历史原因仍然在使用 go vender 械管理,除此之外,任何 Gopher 应该从此刻就投入 go modules 的怀抱。

以上是我在这几天的学习总结,希望对还未入门阶段的你,有所帮助。另外,本篇文章如有写得不对的,请后台批评指正,以免误导其他朋友,非常感谢。

  1. 推荐参考文章

  • Go语言之依赖管理
  • Go 包依赖管理工具 —— govendor
  • Go Modules 终极入门
  • 何处安放我们的 Go 代码

系列导读

01. 开发环境的搭建(Goland & VS Code)

02. 学习五种变量创建的方法

03. 详解数据类型:**整形与浮点型**

04. 详解数据类型:byte、rune与string

05. 详解数据类型:数组与切片

06. 详解数据类型:字典与布尔类型

07. 详解数据类型:指针

08. 面向对象编程:结构体与继承

09. 一篇文章理解 Go 里的函数

10. Go语言流程控制:if-else 条件语句

11. Go语言流程控制:switch-case 选择语句

12. Go语言流程控制:for 循环语句

13. Go语言流程控制:goto 无条件跳转

14. Go语言流程控制:defer 延迟调用

15. 面向对象编程:接口与多态

16. 关键字:make 和 new 的区别?

17. 一篇文章理解 Go 里的语句块与作用域

18. 学习 Go 协程:goroutine

19. 学习 Go 协程:详解信道/通道

20. 几个信道死锁经典错误案例详解

21. 学习 Go 协程:WaitGroup

22. 学习 Go 协程:互斥锁和读写锁

23. Go 里的异常处理:panic 和 recover

24. 超详细解读 Go Modules 前世今生及入门使用

25. Go 语言中关于包导入必学的 8 个知识点

26. 如何开源自己写的模块给别人用?

27. 说说 Go 语言中的类型断言?

28. 这五点带你理解Go语言的select用法


本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

别再问我Redis内存满了该怎么办了

发表于 2020-05-22

概述

Redis的文章,我之前写过一篇关于Redis的缓存的三大问题,累计阅读也快800了,对于还只有3k左右的粉丝量,能够达到这个阅读量,已经是比较难了。

这说明那篇文章写的还过得去,收到很多人的阅读肯定,感兴趣的看一下[]。

三大缓存问题只是Redis的其中的一小部分的知识点,想要深入学习Redis还要学习比较多的知识点。

那么今天就带来了一个面试常问的一个问题:假如你的Redis内存满了怎么办? 长期的把Redis作为缓存使用,总有一天会存满的时候对吧。

这个面试题不慌呀,在Redis中有配置参数maxmemory可以设置Redis内存的大小。

在Redis的配置文件redis.conf文件中,配置maxmemory的大小参数如下所示:

实际生产中肯定不是100mb的大小哈,不要给误导了,这里我只是让大家认识这个参数,一般小的公司都是设置为3G左右的大小。

除了在配置文件中配置生效外,还可以通过命令行参数的形式,进行配置,具体的配置命令行如下所示:

1
2
3
4
复制代码//获取maxmemory配置参数的大小
127.0.0.1:6379> config get maxmemory
//设置maxmemory参数为100mb
127.0.0.1:6379> config set maxmemory 100mb

倘若实际的存储中超出了Redis的配置参数的大小时,Redis中有淘汰策略,把需要淘汰的key给淘汰掉,整理出干净的一块内存给新的key值使用。

接下来我们就详细的聊一聊Redis中的淘汰策略,并且深入的理解每个淘汰策略的原理和应用的场景。

淘汰策略

Redis提供了6种的淘汰策略,其中默认的是noeviction,这6中淘汰策略如下:

  1. noeviction(默认策略):若是内存的大小达到阀值的时候,所有申请内存的指令都会报错。
  2. allkeys-lru:所有key都是使用LRU算法进行淘汰。
  3. volatile-lru:所有设置了过期时间的key使用LRU算法进行淘汰。
  4. allkeys-random:所有的key使用随机淘汰的方式进行淘汰。
  5. volatile-random:所有设置了过期时间的key使用随机淘汰的方式进行淘汰。
  6. volatile-ttl:所有设置了过期时间的key根据过期时间进行淘汰,越早过期就越快被淘汰。

假如在Redis中的数据有一部分是热点数据,而剩下的数据是冷门数据,或者我们不太清楚我们应用的缓存访问分布状况,这时可以使用allkeys-lru。

假如所有的数据访问的频率大概一样,就可以使用allkeys-random的淘汰策略。

假如要配置具体的淘汰策略,可以在redis.conf配置文件中配置,具体配置如下所示:

这只需要把注释给打开就可以,并且配置指定的策略方式,另一种的配置方式就是命令的方式进行配置,具体的执行命令如下所示:

1
2
3
4
复制代码// 获取maxmemory-policy配置
127.0.0.1:6379> config get maxmemory-policy
// 设置maxmemory-policy配置为allkeys-lru
127.0.0.1:6379> config set maxmemory-policy allkeys-lru

在介绍6种的淘汰策略方式的时候,说到了LRU算法,那么什么是LRU算法呢?

LRU算法

LRU(Least Recently Used)即表示最近最少使用,也就是在最近的时间内最少被访问的key,算法根据数据的历史访问记录来进行淘汰数据。

它的核心的思想就是:假如一个key值在最近很少被使用到,那么在将来也很少会被访问。

实际上Redis实现的LRU并不是真正的LRU算法,也就是名义上我们使用LRU算法淘汰键,但是实际上被淘汰的键并不一定是真正的最久没用的。

Redis使用的是近似的LRU算法,通过随机采集法淘汰key,每次都会随机选出5个key,然后淘汰里面最近最少使用的key。

这里的5个key只是默认的个数,具体的个数也可以在配置文件中进行配置,在配置文件中的配置如下图所示:

当近似LRU算法取值越大的时候就会越接近真实的LRU算法,可以这样理解,因为取值越大那么获取的数据就越全,淘汰中的数据的就越接近最近最少使用的数据。

那么为了实现根据时间实现LRU算法,Redis必须为每个key中额外的增加一个内存空间用于存储每个key的时间,大小是3字节。

在Redis 3.0中对近似的LRU算法做了一些优化,Redis中会维护大小是16的一个候选池的内存。

当第一次随机选取的采样数据,数据都会被放进候选池中,并且候选池中的数据会根据时间进行排序。

当第二次以后选取的数据,只有小于候选池内的最小时间的才会被放进候选池中。

当某一时刻候选池的数据满了,那么时间最大的key就会被挤出候选池。当执行淘汰时,直接从候选池中选取最近访问时间最小的key进行淘汰。

这样做的目的就是选取出最近似符合最近最少被访问的key值,能够正确的淘汰key值,因为随机选取的样本中的最小时间可能不是真正意义上的最小时间。

但是LRU算法有一个弊端:就是假如一个key值在以前都没有被访问到,然而最近一次被访问到了,那么就会认为它是热点数据,不会被淘汰。

然而有些数据以前经常被访问到,只是最近的时间内没有被访问到,这样就导致这些数据很可能被淘汰掉,这样一来就会出现误判而淘汰热点数据。

于是在Redis 4.0的时候除了LRU算法,新加了一种LFU算法,那么什么是LFU算法算法呢?

LFU算法

LFU(Least Frequently Used)即表示最近频繁被使用,也就是最近的时间段内,频繁被访问的key,它以最近的时间段的被访问次数的频率作为一种判断标准。

它的核心思想就是:根据key最近被访问的频率进行淘汰,比较少被访问的key优先淘汰,反之则优先保留。

LFU算法反映了一个key的热度情况,不会因为LRU算法的偶尔一次被访问被认为是热点数据。

在LFU算法中支持volatile-lfu策略和allkeys-lfu策略。

以上介绍了Redis的6种淘汰策略,这6种淘汰策略旨在告诉我们怎么做,但是什么时候做?这个还没说,下面我们就来详细的了解Redis什么时候执行淘汰策略。

删除过期键策略

在Redis种有三种删除的操作此策略,分别是:

  1. 定时删除:创建一个定时器,定时的执行对key的删除操作。
  2. 惰性删除:每次只有再访问key的时候,才会检查key的过期时间,若是已经过期了就执行删除。
  3. 定期删除:每隔一段时间,就会检查删除掉过期的key。

定时删除对于内存来说是友好的,定时清理出干净的空间,但是对于cpu来说并不是友好的,程序需要维护一个定时器,这就会占用cpu资源。

惰性的删除对于cpu来说是友好的,cpu不需要维护其它额外的操作,但是对于内存来说是不友好的,因为要是有些key一直没有被访问到,就会一直占用着内存。

定期删除是上面两种方案的折中方案**,每隔一段时间删除过期的key,也就是根据具体的业务,合理的取一个时间定期的删除key**。

通过最合理控制删除的时间间隔来删除key,减少对cpu的资源的占用消耗,使删除操作合理化。

RDB和AOF 的淘汰处理

在Redis中持久化的方式有两种RDB和AOF,具体这两种详细的持久化介绍,可以参考这一篇文章[]。

在RDB中是以快照的形式获取内存中某一时间点的数据副本,在创建RDB文件的时候可以通过save和bgsave命令执行创建RDB文件。

这两个命令都不会把过期的key保存到RDB文件中,这样也能达到删除过期key的效果。

当在启动Redis载入RDB文件的时候,Master不会把过期的key载入,而Slave会把过期的key载入。

在AOF模式下,Redis提供了Rewite的优化措施,执行的命令分别是REWRITEAOF和BGREWRITEAOF,这两个命令都不会把过期的key写入到AOF文件中,也能删除过期key。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…810811812…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%