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

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


  • 首页

  • 归档

  • 搜索

Gradle安装与配置,Spring源码导入Idea

发表于 2021-01-29

Gradle安装与配置,Spring源码导入

Gradle安装背景

  1. 由于需要在本地编译spring源码,所以需要安装配置Gradle环境

下载

  1. 前往官网下载压缩包:
  2. 分为两种下载方式,第一种是 binary-only,二进制代码;第二种 complete包含文档等内容,如有需要可下载complete版本

!

安装

  1. 解压下载后的压缩包到你想要安装的目录 D:\development

本地配置

  1. 编辑系统变量,创建 GRADLE_HOME,指定gradle解压后的目录(起始就是bin目录所在的目录)

  1. 编辑系统变量,创建GRADLE_USER_HOME,指定gradle仓库,或者直接指定为maven仓库

  1. 修改环境变量Path,添加 %GRADLE_HOME%\bin

配置Gradle仓库源

在gradle的安装目录下的,init.d文件夹下,新建一个init.gradle 文件,粘贴以下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
text复制代码allprojects {
repositories {
maven { url 'file:///D:/develop/gradle_repository'}
mavenLocal()
maven { name "Alibaba" ; url "https://maven.aliyun.com/repository/public" }
maven { name "Bstek" ; url "http://nexus.bsdn.org/content/groups/public/" }
mavenCentral()
}

buildscript {
repositories {
maven { name "Alibaba" ; url 'https://maven.aliyun.com/repository/public' }
maven { name "Bstek" ; url 'http://nexus.bsdn.org/content/groups/public/' }
maven { name "M2" ; url 'https://plugins.gradle.org/m2/' }
}
}
}

解释:repositories 中填写的是gradle获取依赖的顺序

maven: 表示的是本地仓库地址

mavenLocal():同样表示的是获取本地仓库的地址

接下来依次是阿里的和国外的仓库地址

mavenCentral(): 中央仓库

Spring源码

Spring源码下载

  1. 点击Spring源码仓库地址
  2. 选择需要下载的版本

  1. 复制地址,下载到你的目录下

Spring源码编译

  1. 可参考spring源码仓库中提供的 Build 文档

  1. 确保本地环境已经准备完成,我的是jdk1.8,Gradle 6.8.1, win10
  2. 修改项目根目录下的 build.gradle 文件,打开并且添加阿里云的仓库,下载依赖会更快

1
2
3
4
5
6
7
ruby复制代码repositories {
mavenCentral()
maven { url "https://repo.spring.io/libs-spring-framework-build" }
maven { url "https://repo.spring.io/snapshot" } // Reactor
maven {url 'https://maven.aliyun.com/nexus/content/groups/public/'} //阿里云
maven {url 'https://maven.aliyun.com/nexus/content/repositories/jcenter'}
}
  1. windows环境下,查看spring-framework 根目录,运行名称为 gradlew.bat 的脚本,建议使用命令的方式执行,可以看到执行过程,如果报错了,可以看到错误信息
  2. 在导入到idea中的时候,需要预编译一下oxm这个项目,在cmd中运行 gradlew :spring-oxm:compileTestJava

导入到idea

  1. idea配置修改

  1. 打开项目

  1. idea将会建立所以请内心等待,笔者在使用idea导入的时候,idea版本为2019.3.1时,报错如下,后将idea版本更换为2020.3.2,问题解决

  1. 正确构建后,效果如下

本文转载自: 掘金

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

面试官:如何找出字符串中无重复最长子串? 前言 “无重复字符

发表于 2021-01-28

前言

LeetCode第3题,“无重复字符的最长子串”,曾经面试的过程中遇到过的一道算法题。通过这道题,我们能够学到算法中一个比较常见的解题方法:滑动窗口算法。

由于LeetCode中很多题都是基于“滑动窗口算法”进行解答,因此本篇文章将重点放在“滑动窗口”上,而不仅仅是这道算法题。当理解了滑动窗口的基本原理之后,所有类似的题都可以轻易解答。

下面来看具体的题目和解题方法。

“无重复字符的最长子串”

题目链接:leetcode-cn.com/problems/lo…

题目描述:

给定一个字符串,请你找出其中不含有重复字符的 最长子串 的长度。

示例:

1
2
3
ini复制代码输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

题目说明

题目很简单,就是从一个字符串中找出不包含重复字符的最长子串的长度。

该题如果用暴利破解的方法进行循环判断,则时间复杂度直接变为O(n^2),是比较恐怖的。因此,可采取滑动窗口的方法来降低时间复杂度。

什么是滑动窗口?

滑动窗口算法是在一个特定大小的字符串或数组上进行操作,而不在整个字符串和数组上操作,这就降低了问题的复杂度,从而也降低了循环的嵌套深度。滑动窗口主要应用在数组和字符串的场景。

简单示例

先通过一个简单的示例来看一下滑动窗口的运作,比如有一个数组[1,3,5,6,2,2],设定滑动窗口(window)大小为3,那么当窗口从数组开始位置滑动到最终位置时依次计算每个窗口内3个元素的和,表示为sum。

面试官:如何找出字符串中无重复最长子串?

上图我们可以看出,随着窗口在数组上向右移动,窗口内的数据也在不断变化,我们只用对窗口内连续区间内的数据进行处理即可。由于区间是连续的,因此当窗口移动时只用对旧窗口的数据进行裁剪处理,这样便减少了重复计算,降低了时间复杂度。

以上图为例,当窗口位于[1,3,5]时,处理完该窗口的数据之后,将窗口向右移动一格,等于是将原有窗口左边的1裁剪掉,然后将窗口右边的6添加上,而整个过程看起来就像窗口在向右移动一样。

对于类似“请找到满足 xx 的最 x 的区间(子串、子数组)的 xx ”这类问题都可以使用该方法进行解决。

滑动窗口的基本步骤

需要注意的是:窗口的移动是按照移动的顺序来进行的;窗口的大小不一定是固定的,可以不断缩小或变大的。

对于滑动窗口算法的基本解题思路,以字符串S示例如下:

  • (1)采用双指针来指定窗口的范围,初始化left=right=0,而索引闭区间[left,right]便是一个窗口。
  • (2)不断增大窗口的right指针,直到窗口中的字符串满足条件。
  • (3)此时,停止right的增加,转而不断增加left指针,用于缩小窗口[left,right],直到窗口中的字符串不再符合要求。每增加一次left,需要更新一轮结果。
  • (4)重复第2和第3步,直到right到达字符串的尽头。

其中,第2步相当于在寻找一个「可行解」,然后第3步在优化这个「可行解」,最终找到最优解。左右指针轮流前进,窗口大小增增减减,窗口不断向右滑动。

解题

学习了窗口滑动之后,我们回到LeetCode的题目上,是将上述示例的固定窗口变为了变化大小的窗口,而将求和换成了判断是否包含指定字符。

因此,套用上面的思路来进行分析。以字符串“dvdf”为例,通过下图来演示滑动的过程。

面试官:如何找出字符串中无重复最长子串?

在上述流程中,可分解为以下步骤:

  • (1)选定初始值left=right=0,也就是窗口[0,0]。
  • (2)判断right右边字符在窗口内是否已经存在;
  • (3)发现字符v在窗口中没有,则可right右移一位,窗口变为[0,1];
  • (4)继续扩展右边界,right=2,发现d已经存在于窗口当中,则停止继续右移;
  • (5)此时窗口的长度便是以d开通的子串的长度,比较当前窗口长度和历史max(默认值0)大小,发现2>0,于是更新max为2。
  • (6)开始移动left,窗口变为[1,1];
  • (7)继续扩展右边界,发现d不存在于窗口当中,此时窗口变为[1,2];
  • (8)继续扩展右边界,发现f不存在于窗口当中,此时窗口变为[1,3];
  • (9)到达字符串的最大长度,停止右移,此时比较当前窗口长度和历史max大小,发现3>2,于是更新max为3。
  • (10)得出,不包含重复字符的最长子串的长度3。

理解了上述步骤,我们再来看原题的Java代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
arduino复制代码class Solution {
public int lengthOfLongestSubstring(String s) {
int n = s.length();
int k = 0, max = 0;
Set<Character> set = new HashSet<>();
for(int i=0; i< n; i++){
if(i != 0){
set.remove(s.charAt(i-1));
}

while(k < n && !set.contains(s.charAt(k))){
set.add(s.charAt(k));
k++;
}
max = Math.max(max,set.size());
}
return max;
}
}

上述代码中for循环中的i相当于left,k相当于right,而max是存储历史最长字符串的值。而窗口内的字符通过Set来存储,判重通过Set的contains方法,获取最大值通过Math的max方法来操作。

最后,此算法的时间复杂度为O(n),其中n是字符串的长度。左指针和右指针分别会遍历整个字符串一次。

小结

本篇文章我们重点学习的应该是滑动窗口的原理及步骤,当掌握了滑动窗口的知识之后,LeetCode的题只不过是对滑动窗口的一个示例而已。对于算法题,千万不要死记硬背解题代码。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 java烂猪皮 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀
  4. .关注后回复【666】扫码即可获取学习资料包

本文转载自: 掘金

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

Netty源码之内存管理(二)(4144) Netty源

发表于 2021-01-28

Netty源码之内存管理(二)(4.1.44)

前面做了很多铺垫(Netty源码之内存管理(一)),带着大家熟悉了与内存分配相关的类的定义和分配逻辑。但并没有真正落实到 jemalloc 思想在源码是如何体现的。本章就是对 PoolChunk 逐字解析,死扣细节。在分析源码之前我们需要对分配的内存级别有一个清晰的定位,当分配 Huge 级别对象,直接使用 PoolChunk 包装,并没有复杂的分配逻辑。而对于 tiny&small&normal 级别来说,进行精细化的内存管理十分有必要的。
开局一张图:
满二叉树结构示意图.png
PoolChunk 本质就是维护这一棵满二叉树,这棵树默认管理 16MB 内存(这个值是可以手动设置)。memoryMap[] 是可变的数组,Netty 在这个数组上逻辑构建一棵满二叉树(当然也可以用链表之类的数据结构,但是随机索引效率不高,我们可以根据数组索引快速定位到某一层的第一个节点。但链表是不能做到的),depthMap[] 表示每个节点对应的深度,这是不可变的。一个 Long 型被分成高、低两部分,高 32 位记录小内存分配信息,低 32 位记录节点下标值。当分配 normal 级别内存时,只有低 32 位信息有用,它的值表示节点序号(起始值为 1),当分配 tiny&normal 级别内存时,高、低两部分确定某个 page 下的某个 subpage。
还有一个比较有意思的是任意节点所管理的内存大小都是 2 的次幂,因此 Netty 会对用户申请的内存大小进行规格化的原因就在这里。任意规格值(当然不能超过 PoolChunkSize)都能找到合适的节点,除非没有节点可满足当前内存申请,那只能新创建一个 PoolChunk。还有另外一个疑问就是如何更新 memoryMap[] 数组呢,请看大屏幕:
内存分配流程图.png
index 表示节点索引,value 表示对应 memoryMap[index] 。
用户第一次申请 4MB 大小内存,由于内存大小确定,因此所在的层的位置也可以通过 maxOrder - (log2(normCapacity) - pageShifts) 确定,4MB 对应层数(也可理解为深度)为 2。
内存分配过程如下,其实对应方法 allocateNode(int) 实现逻辑: 首先判断节点 1 的使用状态: memoryMap1 != unusable(12) 表示节点 1 可用,但由于层数不匹配,所以获取子树节点 2,同时判断使用状态,发现 != 12 且层数不匹配,那就继续获取子节点 4,发现 !=12 且层数匹配,所以节点 4 就用作此次内存分配的节点,并更新 memoryMap[4]=12 表示节点 4 已使用,变量 handle 的低 32 位记录子节点位置信息,同时循环更新父节点的 memoryMap,父节点的值是子节点的 memoryMap 的最小值,所以 memoryMap[2]=2,memoryMap[1] =1。这样,第一次申请 4MB 大小内存就算完成了。
第二次申请 4MB 大小内存,当在第 2 层判断节点 4 的 memoryMap 值等于 12,会判断兄弟节点 5 是否满足分配。大家好好休会。

PoolChunk 内存分配

PoolChunk 是 jemalloc3.x 算法思想的体现,里面以 allocate 开头的 API 就是内存分配算法的实现。入口方法是 allocate(PooledByteBuf, int, int)。

allocate(PooledByteBuf, int, int)

这个方法做的事情有:

  • 根据申请内存大小选择合适的分配策略。具体为如果 >=pageSize,使用 allocateRun() 方法分配,否则使用 allocateSubpage() 分配,它们都会返回句柄值 handle。
  • 初始化 ByteBuf。

源码如下:

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
java复制代码// io.netty.buffer.PoolChunk#allocate
/**
*
* @param buf 「ByteBuf」对象,它是物理内存的承托
* @param reqCapacity 用户所需内存大小
* @param normCapacity 规格值
* @return
*/
boolean allocate(PooledByteBuf<T> buf, int reqCapacity, int normCapacity) {

// 低32位: 节点索引
// 高32位: 位图索引
final long handle;
// 位操作判断大小
if ((normCapacity & subpageOverflowMask) != 0) {
// #1 申请>=8KB
handle = allocateRun(normCapacity);
} else {
// #2 申请<8KB
handle = allocateSubpage(normCapacity);
}

if (handle < 0) {
return false;
}

// #3 如果「PoolChunk」存在缓存的「ByteBuffer」就复用
ByteBuffer nioBuffer = cachedNioBuffers != null ? cachedNioBuffers.pollLast() : null;

// #4 初始化ByteBuf内存相关信息
initBuf(buf, nioBuffer, handle, reqCapacity);
return true;
}

allocateRun(int)

方法 allocateRun(int) 做的事情也不多,主要有:

  • 根据规格值计算所对应的深度 d
  • 调用 allocateNode(d) 完成内存分配
  • 更新剩余空闲值
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
java复制代码// io.netty.buffer.PoolChunk#allocateRun
/**
* 申请大小为「norCapacity」的内存块
*/
private long allocateRun(int normCapacity) {
// #1 计算当前规格值所对应树的深度d
// log2(normCapacity): 获取当前值所对应最高位1的序号,
// pageShifts: 默认值为 13,也就是 pageSize=8192 最高位为1的序号,因为这里分配的是 >=8192,
// 所以需要减去它的偏移量,即从0开始。
// maxOrder: 默认值为 11,maxOrder - 偏移量 = 确切(合适)的树高度
// 可以想象normCapacity 从 8192 不断向上增长,那树的高度也不断变小
int d = maxOrder - (log2(normCapacity) - pageShifts);

// #2 △在深度d的节点中寻找空闲节点并分配内存
int id = allocateNode(d);
if (id < 0) {
return id;
}

// #3 更新剩余空闲值
freeBytes -= runLength(id);

// #4 返回信息
return id;
}

private static final int INTEGER_SIZE_MINUS_ONE = Integer.SIZE - 1; // 31
/**
* 获取以2为底的对数值
* 思路是数有多少个0
*/
private static int log2(int val) {
// compute the (0-based, with lsb = 0) position of highest set bit i.e, log2
// Integer.numberOfLeadingZeros(int): 返回无符号整型的最高非零位前面的0的个数(包括符号位在内)
// 31-0位数量=非0位数量,比如 0000...1000 Integer.numberOfLeadingZeros(0000...1000) = 28,
// 31-28=3,其实就是获取最高位1的序号(从右至左,起始序号为0)
return INTEGER_SIZE_MINUS_ONE - Integer.numberOfLeadingZeros(val);
}

allocateNode(int)

终于到了内存分配的重头戏,它属于节点粒度的分配逻辑。整体思路并不难,前面也通过图解讲述过,但由于采用了太多位运算所以看起来有点头晕。所以我们先熟悉一下部分位运算公式,规定

  • id^=1: id 为奇数则 -1,id 为偶数则 +1。这里用来获取偶数的兄弟节点。比如 id=2,则其兄弟节点为 id^=1 = 3。
  • id<<=1: 相当于 id=id*2,目的是跳转到节点 id 的左子节点。比如 id = 2,它的左子节点值为 4。
  • 1<<d: 表示 1*2 。对在任意深度为d的节点,节点的索引值在 2 到 2-1 范围内。比如深度为 1,则索引值在 [2, 3],当深度为 2,索引值在 [4, 7] 范围内。
  • initial=-(1 << d): 对 2取反,目的是用来判断与目标深度值 d 是否匹配,可以把 initial 可以看成是掩码。当匹配目标深度,有 id & initial == initial,若当前深度<目标深度,有 id & initial ==0。比如目标深度为 2,那 initial=-4,当id=1时,id&initial=0,说明还没有到达目标深度,获取最左子节点(id=id*2)2,此时 2 & initial=0,说明还没有到达目标深度,继续获取最左子节点 4,此时 4&-4=4,此时就找到了目标深度。然后就可以从左到右找寻空闲节点并进行内存分配。

allocateNode(int depth) 目标是在深度 d 中找到空闲的节点并,如果存在按规则更新 memoryMap 相应节点的值并返回节点序号。思路也是比较清晰,从头结点开始判断,如果可分配但深度不匹配则获取左子节点,如果不可分配就返回 -1。继续判断左子节点是否可分配以及深度是否匹配,如果都不匹配,继续重复上面步骤。如果深度匹配但当前节点不可分配(val>d=true),那就获取兄弟节点继续重复上述步骤。如果深度匹配且当前节点可分配,则该节点就是此次申请的目标节点,并将它设置为 unusable 不可用状态,同时,按公式 memoryMap[父节点] = Min(子节点1,子节点2) 循环更新 memoryMap。
还有一个有意思的点需要注意,就是 memoryMap 存储的值,它是这棵树的核心。初始化的值以及后续更新也做得非常巧妙,我说不上来,大家慢慢休会吧。通过内存分配示意图再来休会一下上面的文字描述:
分配示意图_1.png
分配示意图_2.png
相关源码解析如下
PoolChunk_AllocateNode.png

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
java复制代码// io.netty.buffer.PoolChunk#allocateNode
/**
* 在深度「d」查找空闲节点
* @param d
* @return 空闲节点索引,如果返回-1表示当前「PoolChunk」无法满足此次内存分配
*/
private int allocateNode(int d) {
int id = 1;
// 用于判断节点是否匹配深度「d」
int initial = - (1 << d);

// #1 判断根节点是否可用
byte val = value(id);
if (val > d) {
// 根节点不可用,返回「-1」
return -1;
}

// #2 从上往下、从左往右遍历节点。直到找到合适的空闲节点并返回索引值。
// id&initial=0: 表明节点id 匹配 深度d,否则不匹配
// val<d: 表明节点id有空闲内存可分配,若val>d表明节点id无空闲内存可分配
// 循环退出条件是 val>d 或 id&initial !=0,当匹配到空闲节点时,通常 val<d 且 id&initial !=0
while (val < d || (id & initial) == 0) {

// id = id * 2;
id <<= 1;
// 获取momeryMap[id]的值
val = value(id);

// 如果val>d,表明节点「id」不满足此次分配
// 这个判断十分巧妙,前面也出现过一次,用来判断根节点是否满足深度为d的内存申请,
// 如果val>d则不满足,因为,父节点的val值是两个子节点的最小值,
// 因此如果父节点的val>d那说明子节点无法满足当前深度d的内存申请
if (val > d) {
// 获取兄弟节点, 继续查找
id ^= 1;
val = value(id);
}
}
byte value = value(id);
assert value == d && (id & initial) == 1 << d : String.format("val = %d, id & initial = %d, d = %d",
value, id & initial, d);
// #3 将选中的节点标记为「unusable」
setValue(id, unusable);

// #4 循环更新父节点「memoryMap」的值,其值取两个子节点最小的那个值
updateParentsAlloc(id);

// #5 Return
return id;
}

private byte value(int id) {
return memoryMap[id];
}

/**
* 从节点id开始一直到根节点,更新对应的memoryMap的值
*/
private void updateParentsAlloc(int id) {
while (id > 1) {
int parentId = id >>> 1;
byte val1 = value(id);
byte val2 = value(id ^ 1);
byte val = val1 < val2 ? val1 : val2;
setValue(parentId, val);
id = parentId;
}
}

allocateNode(int depth) 是分配 Normal 级别的核心方法,本质是维护 memoryMap[] 数组,遍历树查找空闲内存满足本次内存申请。

allocateSubpage(int)

这个方法是申请 Tiny&Small 级别内存。上一章节讲过对该内存分配的思想: 简单一句话,将某个空闲的 page 拆分成若干个 subpage,使用对象 PoolSubpage 对这些若干个 subpage 进行管理。
PoolChunk#allocateSubpage.png

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
java复制代码// io.netty.buffer.PoolChunk#allocateSubpage
/**
* 申请「tiny&small」级别内存
*/
private long allocateSubpage(int normCapacity) {

// #1 先去「PoolSubpagePools[]」数组中是否能找到合适的「PoolSubpage」
// 如果存在,则使用现成的,否则得新建一个「PoolSubpage」对象后再放入「PoolSubpagePools[]」数组中
// 「PoolSubpage[]」每个节点都会初始化一个head节点,所以这里返回一定不为空
PoolSubpage<T> head = arena.findSubpagePoolHead(normCapacity);

// 子页直接可叶子节点层查找
int d = maxOrder;
// 对头结点上锁,通过数组减少锁的粒度,提高并发性能
synchronized (head) {
// #2 在深度d获取空闲子叶
int id = allocateNode(d);
if (id < 0) {
return id;
}

// 获取成功,对该「page」进行拆分改造
final PoolSubpage<T>[] subpages = this.subpages;
final int pageSize = this.pageSize;

freeBytes -= pageSize;

// #3 获取偏移量(相对于 maxSubpageAlloc)
// 可以理解为 subpageIdx = id - maxSubpageAllocs
int subpageIdx = subpageIdx(id);
// 定位到「PoolChunk」内部的「PoolSubpage[]」
PoolSubpage<T> subpage = subpages[subpageIdx];

if (subpage == null) {
// 如果不存在,则创建
subpage = new PoolSubpage<T>(head, // 这个头结点来自「PoolArena」
this, // 当前「PoolChunk」
id, // 节点「id」
runOffset(id), // 节点「id」字节偏移量
pageSize, // 页大小
normCapacity); // 每个「subpage」等份大小
// 记录新创建的「PoolSubpage」
subpages[subpageIdx] = subpage;
} else {
subpage.init(head, normCapacity);
}

// #4 使用「PoolSubpage」分配内存
return subpage.allocate();
}
}

/**
* 移除最高位,获得偏移量
*/
private int subpageIdx(int memoryMapIdx) {
return memoryMapIdx ^ maxSubpageAllocs; // remove highest set bit, to get offset
}

/**
* 获取节点「id」字节偏移量。
*/
private int runOffset(int id) {
// << 优先级高于 ^
// depth(id): 获取节点「id」对应的深度
// int index = 1 << depth(id) => 2^depth(id),获取节点「id」所在层的最左节点的索引值
// id ^ index => |id-index|,节点id相对所在层的最左节点的偏移量
int shift = id ^ 1 << depth(id);

// runLength(id): 获取节点id的单位值(byte)比如,对于序号为4的节点,它所对应的值为 4194304,即 4MB
// shift * runLength(id): 也就是前面已用的数
// size=runLength(id): 节点id所分配内存大小
// shift*size: 字节偏移量
return shift * runLength(id);
}

/**
* 获取节点「id」所分配的内存大小,单位:字节
* 等价于 chunkSize/(2^maxOrder)
*/
private int runLength(int id) {
// log2ChunkSize=log2chunkSize
return 1 << (log2ChunkSize - depth(id));
}

这里对 allocateSubpage(int) 源码做个小总结: 这个方法主要的目的是创建一个 PoolSubpage 对象,然后委托这个对象完成 tiny&small 级别内存分配。PoolChunk 在内部使用 PoolSubage[] 数组保存 PoolSubpage 对象引用。PoolSubage[] 长度和二叉树的叶子节点个数相同,它们一一对应。PoolSubpage 对象是 page 的化身,它拥有管理 pageSize 大小内存的能力。PoolChunk 只需管理 page,两者分工明确。

PoolSubpage 内存分配

PoolSubpage 内部相关变量之前已经解释过,在这里解释的。它管理内存的逻辑是将 pageSize 大小的内存块等分成若干个了块,子块个数是根据本次申请内存大小所决定,比如申请 1KB 内存,那么会找到一个空闲的 page 并将其拆分成 8 等份(8KB/1KB=8)。并使用位图记录每份子块的使用状态,1 表示已使用,0 表示未使用。最多可分为 512 等份,底层使用 long[] 数组存储位图信息。64 位的句柄值的高 32 位存储位信息,低 32 位存储储节点索引值。因此,核心的问题是如何使用 long[] 数组记录使用情况呢?
源码之下无秘密:
PoolSubpage#allocate.png

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
java复制代码// io.netty.buffer.PoolSubpage#allocate
/**
* 「PoolSubpage」内存分配入口
* @return 返回此次内存分配在bitmap的索引值
*/
long allocate() {
if (elemSize == 0) {
return toHandle(0);
}

// 无可用分片,返回-1
if (numAvail == 0 || !doNotDestroy) {
return -1;
}

// #1 获取下一个可用的分片索引(绝对值)
// 第一个索引值为0
final int bitmapIdx = getNextAvail();

// #2 除以64,确定bitmap[]哪一个
// 第一个bitmap索引值为0
int q = bitmapIdx >>> 6;

// #3 &63: 确认64位长度long的哪一位
// 除以64取余,获取当前绝对 id 的偏移量
// 63: 0011 1111
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) == 0;

// 更新第r位的值为1
// << 优先级高于 |=
bitmap[q] |= 1L << r;

// 更新可用数量
if (-- numAvail == 0) {
// 如果可用数量为0,表示子页中再无可分配的空间
// 需要从双向链表中移除
removeFromPool();
}

// 将bitmapIdx 转换为long存储,long 高32位存储的是小内存位置索引
return toHandle(bitmapIdx);
}

/**
* 获取下一个可用的「分片内存块」
*/
private int getNextAvail() {
int nextAvail = this.nextAvail;
// nextAvail>=0,表明可以直接使用
if (nextAvail >= 0) {
this.nextAvail = -1;
return nextAvail;
}
// nextAvaild<0,需要寻找下一个可用的「分片内存块」
return findNextAvail();
}

/**
* 获取下一个可用的「分片内存块」
* 本质是搜索 bitmap[] 数组为0的索引值
*/
private int findNextAvail() {
final long[] bitmap = this.bitmap;
final int bitmapLength = this.bitmapLength;
// 循环遍历
for (int i = 0; i < bitmapLength; i ++) {
long bits = bitmap[i];

// #1 先判断整个bits是否有「0」位
// 不可用时bits为「0XFFFFFFFFFFFFFFFF」,~bits=0
// 可用时~bits !=0
if (~bits != 0) {
// #2 找寻可用的位
return findNextAvail0(i, bits);
}
}
return -1;
}

/**
* 搜索下一个可用位
*
*/
private int findNextAvail0(int i, long bits) {
final int maxNumElems = this.maxNumElems;

// i << 6 => i * 2^6=i*64
// 想象把long[]展开,baseVal就是基址
final int baseVal = i << 6;

for (int j = 0; j < 64; j ++) {
// bits & 1: 判断最低位是否为0
if ((bits & 1) == 0) {
// 找到空闲子块,组装数据
// baseVal|j => baseVal + j,基址+位的偏移值
int val = baseVal | j;
// 不能越界
if (val < maxNumElems) {
return val;
} else {
break;
}
}

// 无符号右移1位
bits >>>= 1;
}
return -1;
}

// io.netty.buffer.PoolSubpage#toHandle
/**
* 将bitmap索引信息写入高32位,memoryMapIdx信息写入低32位
*
* 0x4000000000000000L: 最高位为1,其他所有位为0。
* 为什么使用0x4000000000000000L数值?
* 是因为对于第一次小内存分配情况,如果高32位为0,则返回句柄值的高位为 0,
* 低32位为 2048(第11层的第一个节点的索引值),但是这个返回值并不会当成子页来处理,从而影响后续的逻辑判断
* 详见 https://blog.csdn.net/wangwei19871103/article/details/104356566
* @param bitmapIdx bitmap索引值
* @return 句柄值
*/
private long toHandle(int bitmapIdx) {
return 0x4000000000000000L | (long) bitmapIdx << 32 | memoryMapIdx;
}

bitmap 填充示意图
bitmap填充图.png
小结上面的源码: PoolSubpage 使用 8 个 long 值存储子块的使用情况,句柄高 32 位存储位图索引值,低 32 位存储 节点索引值。通过大量的位运算提高了性能,通过源码阅读,也提升了位编程应用技巧。
这有一个关于 PoolSubpage 的是如何和 PoolArena 配合使用,因为我们知道,PoolArena 也存有 PoolSubpage[] 数组对象,这些数组对象是怎么被添加的呢?答案在在初始化 PoolSubpage 时就添加到对应的 PoolArena#poolsubpage[] 中了。源码如下:

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
java复制代码// io.netty.buffer.PoolSubpage
/**
* 「PoolSubpage」构造器
* @param head 从「PoolArena」对象中获取「PoolSubpage」结点做头结点
* @param chunk 当前「PoolSubpage」属性的「PoolChunk」对象
* @param memoryMapIdx 所属的「Page」的节点值
* @param runOffset 对存储容器为「byte[]」有用,表示偏移量
* @param pageSize 页大小,默认值为: 8KB
* @param elemSize 元素个数
*/
PoolSubpage(PoolSubpage<T> head,
PoolChunk<T> chunk,
int memoryMapIdx, int runOffset, int pageSize, int elemSize) {
this.chunk = chunk;
this.memoryMapIdx = memoryMapIdx;
this.runOffset = runOffset;
this.pageSize = pageSize;
bitmap = new long[pageSize >>> 10]; // pageSize / 16 / 64

// 添加到「head」链表中
init(head, elemSize);
}

// 初始化「PoolSubpage」
void init(PoolSubpage<T> head, int elemSize) {
doNotDestroy = true;
this.elemSize = elemSize;
if (elemSize != 0) {
// 初始化各类参数
maxNumElems = numAvail = pageSize / elemSize;
nextAvail = 0;
// 根据元素个数确定所需要bitmap个数,即确认「bitmapLength」值
bitmapLength = maxNumElems >>> 6;
if ((maxNumElems & 63) != 0) {
bitmapLength ++;
}

for (int i = 0; i < bitmapLength; i ++) {
bitmap[i] = 0;
}
}
// 添加至双向链表中,供后续分配使用
addToPool(head);
}

private void addToPool(PoolSubpage<T> head) {
assert prev == null && next == null;
prev = head;
next = head.next;
next.prev = this;
head.next = this;
}

以上,就是对 PoolSubpage 内存分析的源码解析,理清思路,功能拆解之后并不困难。

PoolChunk 如何回收内存

讲完了物理内存分配,还没有讲 PoolChunk 是如何回收内存。

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
java复制代码// io.netty.buffer.PoolChunk#free
/**
* 「PoolChunk」释放「handle」表示的内存块。
* 本质是修改对应节点的「memoryMap」值。
* 参数「nioByteBuf」如果不为空,则会放入「Deque」队列中缓存,减少GC
*/
void free(long handle, ByteBuffer nioBuffer) {

// #1 获取句柄「handle」低32位数值,该值表示节点id
int memoryMapIdx = memoryMapIdx(handle);

// #2 获取句柄「handle」高32位数值,该值表示bitmap索引值
int bitmapIdx = bitmapIdx(handle);

// #3 如果bitmqpIdx不为0,说明当前属于subpage释放
if (bitmapIdx != 0) { // free a subpage
PoolSubpage<T> subpage = subpages[subpageIdx(memoryMapIdx)];
assert subpage != null && subpage.doNotDestroy;

// #4 别忘记,「PoolArena」对象中也存有「PoolSubpage」的引用哦
PoolSubpage<T> head = arena.findSubpagePoolHead(subpage.elemSize);
synchronized (head) {
// 交给「PoolSubpage」专业人员释放吧
// 0x3FFFFFFF: 0011 1111 1111 1111 1111 1111 1111 1111,
// bitmapIdx & 0x3FFFFFFF: 保留低30位的值
// 为什么要抹去最高2位呢?因为生成handle是通过 |0x4000000000000000L 操作
if (subpage.free(head, bitmapIdx & 0x3FFFFFFF)) {
return;
}
}
}

// #5 更新空闲内存信息
freeBytes += runLength(memoryMapIdx);

// #6 更新memoryMap信息
setValue(memoryMapIdx, depth(memoryMapIdx));

// #6 循环更新父节点的值
// 注意: 当更新父节点值时,有可能遇到两个兄弟节点的值都为初始值,
// 此时,父节点的值也为初始化而非两者之中最小值
updateParentsFree(memoryMapIdx);

// #7 缓存「ByteBuffer」对象
if (nioBuffer != null && cachedNioBuffers != null &&
cachedNioBuffers.size() < PooledByteBufAllocator.DEFAULT_MAX_CACHED_BYTEBUFFERS_PER_CHUNK) {
cachedNioBuffers.offer(nioBuffer);
}
}

// 无符号右移32位即可
private static int bitmapIdx(long handle) {
return (int) (handle >>> Integer.SIZE);
}

从源码可看出,PoolChunk 回收一块内存十分简单。回收 PoolSubpage 稍微麻烦一点,因为还需要和 PoolArena 中的 PoolSubpage 保持同步。

PoolSubpage 如何回收内存

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
java复制代码// io.netty.buffer.PoolSubpage#free
/**
*「PoolSubpage」释放内存块
* 目标是修改相应「bitmap」的值
*
* @param head 来自「PooArena#PoolSubpage[]」数组的head节点
* @param bitmapIdx bitmap索引值
*/
boolean free(PoolSubpage<T> head, int bitmapIdx) {
if (elemSize == 0) {
return true;
}

// 确认bitmap[] 数组索引值
int q = bitmapIdx >>> 6;
// 确认在64位中的哪一位
int r = bitmapIdx & 63;
assert (bitmap[q] >>> r & 1) != 0;

// 修改对应位为0
bitmap[q] ^= 1L << r;

// 设置可用位信息,待下次分配时直接使用
setNextAvail(bitmapIdx);

// 因为当numAvail=0时,表示无可用内存块,则会从「PoolArena#PoolSubpage[]」数组中移除
// 这次添加后就可以从新回到「PoolArena[]」队列中
if (numAvail ++ == 0) {
// 添加队列
addToPool(head);
return true;
}

if (numAvail != maxNumElems) {
// 还没有到达饱和,即完成这次分配后还有可用空闲,那直接返回
return true;
} else {
// 达到饱和,无内存块可用
if (prev == next) {
// 如果当前链表只有这么一个PoolSubpage对象,就不移除了
return true;
}

// 移除链表
doNotDestroy = false;
removeFromPool();
return false;
}
}

PoolSubpage 需要照顾到 PoolArena 的 PoolSubpage[] 变量,所以稍微代码量多一点。但逻辑十分清楚。看代码就十分明白,我就不强行总结了。

如何回收整个 PoolChunk

核心代码在 PoolArena,根据有无 Cleaner 释放内存 memory 对象即可。

1
2
3
4
5
6
7
8
9
java复制代码// io.netty.buffer.PoolArena.DirectArena#destroyChunk
@Override
protected void destroyChunk(PoolChunk<ByteBuffer> chunk) {
if (PlatformDependent.useDirectBufferNoCleaner()) {
PlatformDependent.freeDirectNoCleaner(chunk.memory);
} else {
PlatformDependent.freeDirectBuffer(chunk.memory);
}
}

小结

Netty 的内存回收是庞大的,两篇文章从 ByteBuf 体系结构讲到源码级的内存分配实现,似乎还是没有讲全讲透。这里只不过把最核心的代码拧出来给大家口味,最终还是希望看到这些文章的各位 DEBUG 调试走一遍。我深知自己的知识能力水平有限,文章部分地方的表达能力欠缺,有些简单的地方描述过于复杂,而恰恰需要讲清楚的地方一笔带过,敬请读者斧正。

我的公众号

搜索 小道一下

本文转载自: 掘金

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

硬核!我花5小时肝出这篇Redis缓存解决方案,带你起飞!

发表于 2021-01-28

写在前面

对于缓存穿透,雪崩相信很多小伙伴都有听过,不管是工作中还是面试都热点问题,本文重点带大家分析这些问题,给位看官请往下看!

同时用XMind画了一张导图记录Redis的学习笔记和一些面试解析(源文件对部分节点有详细备注和参考资料,欢迎关注我的公众号:阿风的架构笔记 后台发送【导图】拿下载链接, 已经完善更新):

一、缓存穿透

1. 什么是缓存穿透?

为了缓解持久层数据库的压力,在服务器和存储层之间添加了一层缓存;

一个简单的正常请求: 当客户端发起请求时,服务器响应处理,会先从redis缓存层查询客户端需要的请求数据,如果缓存层有缓存的数据,会将数据返回给服务器,服务器再返回给客户端;如果缓存层中没有客户端需要的数据,则会去底层存储层查找,再返回给服务器;

缓存穿透就是: 当客户端想要查询一个数据,发现redis缓存层中没有(即缓存没有命中),于是向持久层数据库查询,发现也没有,于是本次查询失败;当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,此时会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。

2. 解决办法

在缓存层加布隆过滤器,通俗简述一下其作用:将数据库中的 id ,通过某方式映射到布隆过滤器,当处理不存在的 id 时,布隆过滤器会将该请求过直接过滤出去,不会到数据库做操作。

3. 布隆过滤器

1)概述: 布隆过滤器是一种数据结构,比较巧妙的概率型数据结构,实际上是一个很长的二进制向量和一系列随机映射函数,特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”,相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

2)返回结果的不确切性: 布隆过滤器是一个 bit 向量或者说 bit 数组:假设有8位

映射数据1: 使用多个不同的哈希函数生成多个哈希值,并对每个生成的哈希值指向的 bit 位置为 1,比如三次hash完后,data1 将1、3、6位,置为1;

映射数据2: data2 将2、3、6位,置为1,此时由于hash为随机性,所以6位和 data1 有重复的,便会覆盖 data1 的第6位的1;

问题来了!!

6 这个 bit 位由于两个值的哈希函数都返回了这个 bit 位,因此它被覆盖了,当我们如果想查询 data3这个值是否存在,假设哈希函数返回了 1、5、6三个值,结果我们发现 5 这个 bit 位上的值为 0,说明没有任何一个值映射到这个 bit 位上,因此我们可以很确定地说 data3 这个值不存在。而当我们需要查询 data1 这个值是否存在的话,那么哈希函数必然会返回 1、3、6,然后我们检查发现这三个 bit 位上的值均为 1,那么我们是否可以说 data1 存在了么?答案是不可以,只能是 data1 这个值可能存在!因为随着增加的值越来越多,被置为 1 的 bit 位也会越来越多,这样某个值 data4 即使没有被存储过,但是万一哈希函数返回的三个 bit 位都被其他位置位了 1 ,那么程序还是会判断 data4 这个值存在。

所以: 布隆过滤器的长度会直接影响误报率,布隆过滤器越长且误报率越小。

3)简单剖析布隆过滤器源码

导入guava的包:

1
2
3
4
5
xml复制代码<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>23.0</version>
</dependency>

源码: BloomFilter一共四个create方法,最终都是走向第四个方法;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public static <T> BloomFilter<T> create(Funnel<? super T> funnel, int expectedInsertions) {
return create(funnel, (long) expectedInsertions);
}

public static <T> BloomFilter<T> create(Funnel<? super T> funnel, long expectedInsertions) {
return create(funnel, expectedInsertions, 0.03); // FYI, for 3%, we always get 5 hash functions
}

public static <T> BloomFilter<T> create(
Funnel<? super T> funnel, long expectedInsertions, double fpp) {
return create(funnel, expectedInsertions, fpp, BloomFilterStrategies.MURMUR128_MITZ_64);
}

static <T> BloomFilter<T> create(
Funnel<? super T> funnel, long expectedInsertions, double fpp, Strategy strategy) {
......
}

参数类型: funnel:数据类型;expectedInsertions:期望插入的值的个数;fpp:错误率(默认值为0.03);strategy:哈希算法。

总结: 错误率越大,所需空间和时间越小;反之错误率越小,所需空间和时间越大!

二、缓存击穿

1、什么是缓存击穿?

在平常高并发的系统中,大量的请求同时查询一个key时,此时这个key正好失效了,就会导致大量的请求都打到数据库上面去。这种现象我们称为缓存击穿

2、问题排查

  1. Redis中某个key过期,该key访问量巨大
  2. 多个数据请求从服务器直接压到Redis后,均未命中
  3. Redis在短时间内发起了大量对数据库中同一数据的访问

3、如何解决

1. 使用互斥锁(mutex key)

这种解决方案思路比较简单,就是只让一个线程构建缓存,其他线程等待构建缓存的线程执行完,重新从缓存获取数据就可以了。如果是单机,可以用synchronized或者lock来处理,如果是分布式环境可以用分布式锁就可以了(分布式锁,可以用memcache的add, redis的setnx, zookeeper的添加节点操作)。

在这里插入图片描述

2. “提前”使用互斥锁(mutex key)

在value内部设置1个超时值(timeout1), timeout1比实际的redis timeout(timeout2)小。当从cache读取到timeout1发现它已经过期时候,马上延长timeout1并重新设置到cache。然后再从数据库加载数据并设置到cache中

3. “永远不过期”

  • 从redis上看,确实没有设置过期时间,这就保证了,不会出现热点key过期问题,也就是“物理”不过期。
  • 从功能上看,如果不过期,那不就成静态的了吗?所以我们把过期时间存在key对应的value里,如果发现要过期了,通过一个后台的异步线程进行缓存的构建,也就是“逻辑”过期

img

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
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
89
90
91
92
93
94
95
java复制代码class MyCache{

private ConcurrentHashMap<String, String> map;

private CountDownLatch countDownLatch;

private AtomicInteger atomicInteger;

public MyCache(ConcurrentHashMap<String, String> map, CountDownLatch countDownLatch,
AtomicInteger atomicInteger) {
this.map = map;
this.countDownLatch = countDownLatch;
this.atomicInteger = atomicInteger;
}

public String get(String key){

String value = map.get(key);
if (value != null){
System.out.println(Thread.currentThread().getName()+"\t 线程获取value值 value="+value);
return value;
}
// 如果没获取到值
// 首先尝试获取token,然后去查询db,初始化化缓存;
// 如果没有获取到token,超时等待
if (atomicInteger.compareAndSet(0,1)){
System.out.println(Thread.currentThread().getName()+"\t 线程获取token");
return null;
}

// 其他线程超时等待
try {
System.out.println(Thread.currentThread().getName()+"\t 线程没有获取token,等待中。。。");
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
// 初始化缓存成功,等待线程被唤醒
// 等待线程等待超时,自动唤醒
System.out.println(Thread.currentThread().getName()+"\t 线程被唤醒,获取value ="+map.get("key"));
return map.get(key);
}

public void put(String key, String value){

try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}

map.put(key, value);

// 更新状态
atomicInteger.compareAndSet(1, 2);

// 通知其他线程
countDownLatch.countDown();
System.out.println();
System.out.println(Thread.currentThread().getName()+"\t 线程初始化缓存成功!value ="+map.get("key"));
}

}

class MyThread implements Runnable{

private MyCache myCache;

public MyThread(MyCache myCache) {
this.myCache = myCache;
}

@Override
public void run() {
String value = myCache.get("key");
if (value == null){
myCache.put("key","value");
}

}
}

public class CountDownLatchDemo {
public static void main(String[] args) {

MyCache myCache = new MyCache(new ConcurrentHashMap<>(), new CountDownLatch(1), new AtomicInteger(0));

MyThread myThread = new MyThread(myCache);

ExecutorService executorService = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
executorService.execute(myThread);
}
}
}

4.总结

缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中redis后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个key的过期监控难度较高,配合雪崩处理策略即可。

三、缓存雪崩

1. 什么是缓存雪崩?

缓存雪崩是指: 某一时间段,缓存集中过期失效,即缓存层出现了错误,不能正常工作了;于是所有的请求都会达到存储层,存储层的调用量会暴增,造成 “雪崩”;

**比如:**双十二临近12点,抢购商品,此时会设置商品在缓存区,设置过期时间为1小时,当到了1点时,缓存过期,所有的请求会落到存储层,此时数据库可能扛不住压力,自然 “挂掉”。

2. 解决办法

redis高可用

这个思想的含义是,既然redis有可能挂掉,那我多增设几台redis,这样一台挂掉之后其他的还可以继续工作,其实就是搭建的集群。

限流降级

这个解决方案的思想是,在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。

数据预热

数据预热的含义就是在正式部署之前,我先把可能的数据先预先访问一遍,这样部分可能大量访问的数据就会加载到缓存中,在即将发生大并发访问前手动触发加载缓存不同的key,设置不同的过期时间,让缓存失效的时间点尽量均匀。

四、缓存预热

1.什么是缓存预热

缓存预热就是系统上线后,将相关的缓存数据直接加载到缓存系统。这样就可以避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题。用户直接查询事先被预热的缓存数据。如图所示:

img

如果不进行预热, 那么 Redis 初识状态数据为空,系统上线初期,对于高并发的流量,都会访问到数据库中, 对数据库造成流量的压力。

2.问题排查

  1. 请求数量较高
  2. 主从之间数据吞吐量较大,数据同步操作频度较高

3.有什么解决方案?

前置准备工作:

  1. 日常例行统计数据访问记录,统计访问频度较高的热点数据
  2. 利用LRU数据删除策略,构建数据留存队列
1
复制代码  例如:storm与kafka配合

准备工作:

  1. 将统计结果中的数据分类,根据级别,redis优先加载级别较高的热点数据
  2. 利用分布式多服务器同时进行数据读取,提速数据加载过程
  3. 热点数据主从同时预热

实施:

  1. 使用脚本程序固定触发数据预热过程
  2. 如果条件允许,使用了CDN(内容分发网络),效果会更好

4.总结

缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题!用户直接查询事先被预热的缓存数据

五、缓存降级

降级的情况,就是缓存失效或者缓存服务挂掉的情况下,我们也不去访问数据库。我们直接访问内存部分数据缓存或者直接返回默认数据。

举例来说:

对于应用的首页,一般是访问量非常大的地方,首页里面往往包含了部分推荐商品的展示信息。这些推荐商品都会放到缓存中进行存储,同时我们为了避免缓存的异常情况,对热点商品数据也存储到了内存中。同时内存中还保留了一些默认的商品信息。如下图所示:

img

降级一般是有损的操作,所以尽量减少降级对于业务的影响程度。

看完三件事❤️

如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
​

  1. 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
  2. 关注公众号 『 阿风的架构笔记 』,不定期分享原创知识。
  3. 同时可以期待后续文章ing🚀

本文转载自: 掘金

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

try-catch-finally中的4个大坑,不小心就栽进

发表于 2021-01-28

在 Java 语言中 try-catch-finally 看似简单,一副人畜无害的样子,但想要真正的“掌控”它,却并不是一件容易的事。别的不说,咱就拿 fianlly 来说吧,别看它的功能单一,但使用起来却“暗藏杀机”,若您不信,咱来看下面的这几个例子…

坑1:finally中使用return

若在 finally 中使用 return,那么即使 try-catch 中有 return 操作,也不会立马返回结果,而是再执行完 finally 中的语句再返回。此时问题就产生了:如果 finally 中存在 return 语句,则会直接返回 finally 中的结果,从而无情的丢弃了 try 中的返回值。

① 反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public static void main(String[] args) throws FileNotFoundException {
System.out.println("执行结果:" + test());
}

private static int test() {
int num = 0;
try {
// num=1,此处不返回
num++;
return num;
} catch (Exception e) {
// do something
} finally {
// num=2,返回此值
num++;
return num;
}
}

以上代码的执行结果如下:

image.png

② 原因分析

如果在 finally 中存在 return 语句,那么 try-catch 中的 return 值都会被覆盖,如果程序员在写代码的时候没有发现这个问题,那么就会导致程序的执行结果出错。

③ 解决方案

如果 try-catch-finally 中存在 return 返回值的情况,一定要确保 return 语句只在方法的尾部出现一次。

④ 正例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
java复制代码public static void main(String[] args) throws FileNotFoundException {
System.out.println("执行结果:" + testAmend());
}
private static int testAmend() {
int num = 0;
try {
num = 1;
} catch (Exception e) {
// do something
} finally {
// do something
}
// 确保 return 语句只在此处出现一次
return num;
}

坑2:finally中的代码“不执行”

如果说上面的示例比较简单,那么下面这个示例会给你不同的感受,直接来看代码。

① 反例代码

1
2
3
4
5
6
7
8
9
10
11
java复制代码public static void main(String[] args) throws FileNotFoundException {
System.out.println("执行结果:" + getValue());
}
private static int getValue() {
int num = 1;
try {
return num;
} finally {
num++;
}
}

以上代码的执行结果如下:
image.png

② 原因分析

**本以为执行的结果会是 2,但万万没想到竟然是 1 **,用马大师的话来讲:「我大意了啊,没有闪」。

有人可能会问:如果把代码换成 ++num,那么结果会不会是 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
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
java复制代码// class version 52.0 (52)
// access flags 0x21
public class com/example/basic/FinallyExample {

// compiled from: FinallyExample.java

// access flags 0x1
public <init>()V
L0
LINENUMBER 5 L0
ALOAD 0
INVOKESPECIAL java/lang/Object.<init> ()V
RETURN
L1
LOCALVARIABLE this Lcom/example/basic/FinallyExample; L0 L1 0
MAXSTACK = 1
MAXLOCALS = 1

// access flags 0x9
public static main([Ljava/lang/String;)V throws java/io/FileNotFoundException
L0
LINENUMBER 13 L0
GETSTATIC java/lang/System.out : Ljava/io/PrintStream;
NEW java/lang/StringBuilder
DUP
INVOKESPECIAL java/lang/StringBuilder.<init> ()V
LDC "\u6267\u884c\u7ed3\u679c:"
INVOKEVIRTUAL java/lang/StringBuilder.append (Ljava/lang/String;)Ljava/lang/StringBuilder;
INVOKESTATIC com/example/basic/FinallyExample.getValue ()I
INVOKEVIRTUAL java/lang/StringBuilder.append (I)Ljava/lang/StringBuilder;
INVOKEVIRTUAL java/lang/StringBuilder.toString ()Ljava/lang/String;
INVOKEVIRTUAL java/io/PrintStream.println (Ljava/lang/String;)V
L1
LINENUMBER 14 L1
RETURN
L2
LOCALVARIABLE args [Ljava/lang/String; L0 L2 0
MAXSTACK = 3
MAXLOCALS = 1

// access flags 0xA
private static getValue()I
TRYCATCHBLOCK L0 L1 L2 null
L3
LINENUMBER 18 L3
ICONST_1
ISTORE 0
L0
LINENUMBER 20 L0
ILOAD 0
ISTORE 1
L1
LINENUMBER 22 L1
IINC 0 1
L4
LINENUMBER 20 L4
ILOAD 1
IRETURN
L2
LINENUMBER 22 L2
FRAME FULL [I] [java/lang/Throwable]
ASTORE 2
IINC 0 1
L5
LINENUMBER 23 L5
ALOAD 2
ATHROW
L6
LOCALVARIABLE num I L0 L6 0
MAXSTACK = 1
MAXLOCALS = 3
}

这些字节码的简易版本如下图所示:
image.png
想要读懂这些字节码,首先要搞懂这些字节码所代表的含义,这些内容可以从 Oracle 的官网查询到(英文文档):docs.oracle.com/javase/spec…

磊哥在这里对这些字节码做一个简单的翻译:

iconst 是将 int 类型的值压入操作数栈。
istore 是将 int 存储到局部变量。
iload 从局部变量加载 int 值。
iinc 通过下标递增局部变量。
ireturn 从操作数堆栈中返回 int 类型的值。
astore 将引用存储到局部变量中。

有了这些信息之后,我们来翻译一下上面的字节码内容:

1
2
3
4
5
6
7
java复制代码 0 iconst_1   在操作数栈中存储数值 1
1 istore_0 将操作数栈中的数据存储在局部变量的位置 0
2 iload_0 从局部变量读取值到操作数栈
3 istore_1 将操作数栈中存储 1 存储在局部变量的位置 1
4 iinc 0 by 1 把局部变量位置 0 的元素进行递增(+1)操作
7 iload_1 将局部位置 1 的值加载到操作数栈中
8 ireturn 返回操作数栈中的 int 值

通过以上信息也许你并不能直观的看出此方法的内部执行过程,没关系磊哥给你准备了方法执行流程图:

image.png
image.png
image.png
通过以上图片我们可以看出:在 finally 语句(iinc 0, 1)执行之前,本地变量表中存储了两个信息,位置 0 和位置 1 都存储了一个值为 1 的 int 值。而在执行 finally(iinc 0, 1)之前只把位置 0 的值进行了累加,之后又将位置 1 的值(1)返回给了操作数栈,所以当执行返回操作(ireturn)时会从操作数栈中读到返回值为 1 的结果,因此最终的执行是 1 而不是 2。

③ 解决方案

关于 Java 虚拟机是如何编译 finally 语句块的问题,有兴趣的读者可以参考《The JavaTM Virtual Machine Specification, Second Edition》中 7.13 节 Compiling finally。那里详细介绍了 Java 虚拟机是如何编译 finally 语句块。

实际上,Java 虚拟机会把 finally 语句块作为 subroutine(对于这个 subroutine 不知该如何翻译为好,干脆就不翻译了,免得产生歧义和误解)直接插入到 try 语句块或者 catch 语句块的控制转移语句之前。但是,还有另外一个不可忽视的因素,那就是在执行 subroutine(也就是 finally 语句块)之前,try 或者 catch 语句块会保留其返回值到本地变量表(Local Variable Table)中,待 subroutine 执行完毕之后,再恢复保留的返回值到操作数栈中,然后通过 return 或者 throw 语句将其返回给该方法的调用者(invoker)。

因此如果在 try-catch-finally 中如果有 return 操作,**一定要确保 return 语句只在方法的尾部出现一次!**这样就能保证 try-catch-finally 中所有操作代码都会生效。

④ 正例代码

1
2
3
4
5
6
7
8
9
10
11
java复制代码private static int getValueByAmend() {
int num = 1;
try {
// do something
} catch (Exception e) {
// do something
} finally {
num++;
}
return num;
}

坑3:finally中的代码“非最后”执行

① 反例代码

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public static void main(String[] args) throws FileNotFoundException {
execErr();
}
private static void execErr() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
e.printStackTrace();
} finally {
System.out.println("执行 finally.");
}
}

以上代码的执行结果如下:
image.png
从以上结果可以看出 finally 中的代码并不是最后执行的,而是在 catch 打印异常之前执行的,这是为什么呢?

② 原因分析

产生以上问题的真实原因其实并不是因为 try-catch-finally,当我们打开 e.printStackTrace 的源码就能看出一些端倪了,源码如下:
image.png
从上图可以看出,当执行 e.printStackTrace() 和 finally 输出信息时,使用的并不是同一个对象。finally 使用的是标准输出流:System.out,而 e.printStackTrace() 使用的却是标准错误输出流:System.err.println,它们执行的效果等同于:

1
2
3
4
java复制代码public static void main(String[] args) {
System.out.println("我是标准输出流");
System.err.println("我是标准错误输出流");
}

而以上代码执行结果的顺序也是随机的,而产生这一切的原因,我们或许可以通过标准错误输出流(System.err)的注释和说明文档中看出:
image.png
image.png
我们简单的对以上的注释做一个简单的翻译:

“标准”错误输出流。该流已经打开,并准备接受输出数据。
通常,此流对应于主机环境或用户指定的显示输出或另一个输出目标。按照惯例,即使主要输出流(out 输出流)已重定向到文件或其他目标位置,该输出流(err 输出流)也能用于显示错误消息或其他信息,这些信息应引起用户的立即注意。

从源码的注释信息可以看出,标准错误输出流(System.err)和标准输出流(System.out)使用的是不同的流对象,即使标准输出流并定位到其他的文件,也不会影响到标准错误输出流。那么我们就可以大胆的猜测:二者是独立执行的,并且为了更高效的输出流信息,二者在执行时是并行执行的,因此我们看到的结果是打印顺序总是随机的。

为了验证此观点,我们将标准输出流重定向到某个文件,然后再来观察 System.err 能不能正常打印,实现代码如下:

1
2
3
4
5
6
java复制代码public static void main(String[] args) throws FileNotFoundException {
// 将标准输出流的信息定位到 log.txt 中
System.setOut(new PrintStream(new FileOutputStream("log.txt")));
System.out.println("我是标准输出流");
System.err.println("我是标准错误输出流");
}

以上代码的执行结果如下:
image.png
当程序执行完成之后,我们发现在项目的根目录出现了一个新的 log.txt 文件,打开此文件看到如下结果:
image.png
从以上结果可以看出标准输出流和标准错误输出流是彼此独立执行的,且 JVM 为了高效的执行会让二者并行运行,所以最终我们看到的结果是 finally 在 catch 之前执行了。

③ 解决方案

知道了原因,那么问题就好处理,我们只需要将 try-catch-finally 中的输出对象,改为统一的输出流对象就可以解决此问题了。

④ 正例代码

1
2
3
4
5
6
7
8
9
java复制代码private static void execErr() {
try {
throw new RuntimeException();
} catch (RuntimeException e) {
System.out.println(e);
} finally {
System.out.println("执行 finally.");
}
}

改成了统一的输出流对象之后,我手工执行了 n 次,并没有发现任何问题。

坑4:finally中的代码“不执行”

finally 中的代码一定会执行吗?如果是之前我会毫不犹豫的说“是的”,但在遭受了社会的毒打之后,我可能会这样回答:正常情况下 finally 中的代码一定会执行的,但如果遇到特殊情况 finally 中的代码就不一定会执行了,比如下面这些情况:

  • 在 try-catch 语句中执行了 System.exit;
  • 在 try-catch 语句中出现了死循环;
  • 在 finally 执行之前掉电或者 JVM 崩溃了。

如果发生了以上任意一种情况,finally 中的代码就不会执行了。虽然感觉这一条有点“抬杠”的嫌疑,但墨菲定律告诉我们,如果一件事有可能会发生,那么他就一定会发生。所以从严谨的角度来说,这个观点还是成立的,尤其是对于新手来说,神不知鬼不觉的写出一个自己发现不了的死循环是一件很容易的事,不是嘛?

① 反例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码public static void main(String[] args) {
noFinally();
}
private static void noFinally() {
try {
System.out.println("我是 try~");
System.exit(0);
} catch (Exception e) {
// do something
} finally {
System.out.println("我是 fially~");
}
}

以上代码的执行结果如下:
image.png
从以上结果可以看出 finally 中的代码并没有执行。

② 解决方案

排除掉代码中的 System.exit 代码,除非是业务需要,但也要注意如果在 try-cacth 中出现了 System.exit 的代码,那么 finally 中的代码将不会被执行。

总结

本文我们展示了 finally 中存在的一些问题,有很实用的干货,也有一些看似“杠精”的示例,但这些都从侧面印证了一件事,那就是想完全掌握的 try-catch-finally 并不是一件简单的事。最后,在强调一点,如果 try-catch-finally 中存在 return 返回值的操作,那么一定要确保 return 语句只在方法的尾部出现一次!

参考 & 鸣谢

阿里巴巴《Java开发手册》

developer.ibm.com/zh/articles/j-lo-finally

关注公众号「Java中文社群」发现更多干货。

查看 Github 发现更多精彩:github.com/vipstone/al…

本文转载自: 掘金

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

使用RequestBodyAdvice、ResponseBo

发表于 2021-01-28

前言

在前后端分离的项目中,前端与后台一般会约定好固定的参数和响应的数据结构,如:

参数:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码public class ApiRequest<T> {

private String token;

private String version;

/**
* 业务参数
*/
private T data;

}

响应:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class ApiResult<T> {

/**
* 编码,当code=0时代表成功,其他则为失败"
*/
private Integer code;

/**
* 描述信息
*/
private String msg;

/**
* 成功后返回的数据
*/
private T data;

...

}

一般传统的做法是在 Controller层方法直接接收ApiRequest参数和直接返回ApiResult的实例:

  • 在参数中传入ApiRequest对象,然后手动获取业务参数data进行处理
  • 每个接口手动生成ApiResult对象并返回。

这一部分工作其实是重复也无太多意义的,那么有没有一种方法可以自动做到 我们只关注 ApiRequest.data和 ApiResult.data,让程序自动将参数传入到 业务参数data中和 控制层方法只返回 data,程序自动封装成ApiResult并返回呢?那么今天的主人公 RequestBodyAdvice,ResponseBodyAdvice就登场了。

RequestBodyAdvice

RequestBodyAdvice是SpringMVC4.2提供的一个接口,它允许请求体被读取并转换为对象,并将处理结果对象作为@RequestBody参数或者 @HttpEntity方法参数。由此可见,它的作用范围为:

  • 使用@RequestBody进行标记的参数
  • 参数为HttpEntity
提供的方法
1
2
java复制代码boolean supports(MethodParameter methodParameter, Type targetType,
Class<? extends HttpMessageConverter<?>> converterType);

该方法返回true时,才会进去下面的系列方法

1
2
java复制代码HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException;

body数据读取之前调用,一般在此方法中对body数据进行修改

1
2
java复制代码Object afterBodyRead(Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

body读取之后操作,一般直接返回原实例

1
2
java复制代码Object handleEmptyBody(@Nullable Object body, HttpInputMessage inputMessage, MethodParameter parameter,
Type targetType, Class<? extends HttpMessageConverter<?>> converterType);

当body问empty时操作

实现步骤
  • 编写一个实现类实现RequestBodyAdvice接口
  • 分别实现对应的方法
  • 实现类上添加注解标记:ControllerAdvice
实例
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
java复制代码@ControllerAdvice
public class RequestInterceptor implements RequestBodyAdvice {

@Override
public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
RequestAdvice requestAdvice = methodParameter.getMethodAnnotation(RequestAdvice.class);
if (requestAdvice == null) {
requestAdvice = methodParameter.getDeclaringClass().getAnnotation(RequestAdvice.class);
}
return requestAdvice != null;
}

@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
String bodyStr = IOUtils.toString(httpInputMessage.getBody(), StandardCharsets.UTF_8);
return new HttpInputMessage() {
@Override
public InputStream getBody() throws IOException {
ApiRequest<Object> request = JsonUtils.json2Obj(bodyStr, new TypeReference<ApiRequest<Object>>() {
});
String body = bodyStr;
if (request != null && request.getData() != null) {
body = JsonUtils.obj2Json(request.getData());
}
return new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8));
}

@Override
public HttpHeaders getHeaders() {
return httpInputMessage.getHeaders();
}
};
}

@Override
public Object afterBodyRead(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return body;
}

@Override
public Object handleEmptyBody(Object body, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
return body;
}

上面实例中,supports添加了支持条件:当控制层方法或者类上有标记注解 @RequestAdvice注解时,才会进入其他相关方法。当不需要任何限制时,supports直接返回true即可。

ResponseBodyAdvice

ResponseBodyAdvice是SpringMVC4.1提供的一个接口,它允许在 执行 @ResponseBody后自定义返回数据,或者将返回@ResponseEntity的 Controller Method在写入主体前使用 HttpMessageConverter进行自定义操作。由此可见,它的作用范围为:

  • 使用@ResponseBody注解进行标记
  • 返回@ResponseEntity
提供的方法
  • 1
    java复制代码boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

该方法返回true时,才会进去下面的 beforeBodyWrite方法。该方法可以添加一些判断条件,比如 方法上有 xxx 注解的才会生效等等。

  • 1
    2
    3
    java复制代码T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
    Class<? extends HttpMessageConverter<?>> selectedConverterType,
    ServerHttpRequest request, ServerHttpResponse response);

body写入前的操作。

实现步骤
  • 编写一个实现类实现ResponseBodyAdvice接口
  • 重写supports和boforeBodyWrite
  • 实现类上添加注解标记:ControllerAdvice
实例
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
java复制代码@ControllerAdvice
public class ResponseInterceptor implements ResponseBodyAdvice {

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
ResponseAdvice responseAdvice = returnType.getMethodAnnotation(ResponseAdvice.class);
if (responseAdvice == null) {
responseAdvice = returnType.getDeclaringClass().getAnnotation(ResponseAdvice.class);
}
return responseAdvice != null;
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
try {
ApiResult<Object> result = new ApiResult<>();
result.setCode(0);
result.setMsg("success");
result.setdData(body);
return result;
} catch (Exception e) {
e.printStackTrace();
}
return body;
}
}

上面实例中,supports添加了支持条件:当控制层方法或者类上有标记注解 @ResponseAdvice直接时,才会进入beforeBodyWrite方法。当不需要任何限制时,supports直接返回true即可。

其他

上述只是针对 公共参数、公共返回这一种情况对 RequestBodyAdvice、ResponseBodyAdvice进行了说明,当然这两种接口不止这一种应用场景,比如对参数或者返回进行加解密都可以使用这两种接口进行实现。具体使用场景用户可根据实际情况进行使用。

本文转载自: 掘金

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

良心推荐!Python小白必看的7本书籍,三天完成入门到精通

发表于 2021-01-28

当年我看过不下十本适合小白的编程圣经,却发现里面有一部分是浪得虚名,被吹上了天。

唯独有这么七本书,确实配得上“零基础编程圣经”的名号,不枉我花半年时间一本一本啃下来。

以下推荐,童叟无欺,仙仙出品,必属精品,篇幅不长,三分钟就能看完。

为了防止你回头忘了书名,可以点个赞并收藏起来,上个双保险。

以下书籍我都整理成电子版PDF,需要的关注公众号/Python小白集训营/回复/电子书/,即可自动获取。

仙仙敲黑板提醒!Python入门书籍真的不用看太多!

小白最容易犯的毛病,就是盲目看推荐,抓一本看一本。你看的书一定要匹配自己学习Python的方向,或者说你对什么方向感兴趣。

Python这门语言的应用领域本身就非常广泛,比方说可以用来做数据分析、机器学习,也可以用来做后端开发、还可以做Web开发、前端、人工智能、大数据等等。

一个明确的定位和规划,真的能够帮助你少走很多弯路。

基础部分
《Python编程:入门到实践》

理论和实践恰到好处,行文逻辑流畅,不跳跃,手把手教的感觉,又不啰嗦,非常适合入门。

仙仙强烈推荐这本书,其实很多大佬都在推,因为书中涵盖的内容是比较精简的,没有艰深晦涩的概念,最重要的是每个小结都附带有”动手试一试”环节,学编程本来就该多动手实践,一个只会理论知识的程序员可不是好程序员。

《Python基础教程》

学习一门编程语言的最好方法就是在实践中不断思考改进。

这本书内容涉及的范围较广,既能为初学者夯实基础,又能帮助程序员提升技能,适合各个层次的Python开发人员阅读参考。

看似是一本基础教程,实际上是一本启蒙宝典,无论你处在哪个阶段,只要翻开它,都能从中获取一些灵感和启发,个人还是蛮喜欢这类书的。

《笨办法学Python》

这本书,实战性要强一些,从一个个小例子入手,难度逐步加大,不单是教你写Python代码,还会培养你的编程思维,由内到外提升你的编程技巧。

这确实是一本Python入门书籍,别看它实操性强,它适合对那些计算机了解不多,没有接触过编程的同学。

简单介绍一下内容,从简单的打印一直讲到完整项目的实现,让小白在不断学习的过程中体验到成功开发软件的那种喜悦,以此激励初学者不断向前。

所有书籍我都整理成电子版PDF,需要的关注我后评论回复“学习”,后台私信回复“1”,即可自动获取。

《Python for data analysis》

想利用Python进行数据分析?没问题,看这本书就行,一步到位!

内容包含Python控制、处理、整理、分析结构化数据,并且配有大量的课后实操。

在读完这本书后,你将学会如何利用各种Python库高效地解决各式各样的数据分析问题。它主要介绍了ipython 、notebook、Numpy、Scipy和Pandas包的使用等,如果只是为了提升工作效率,做一些数据分析和自动化办公的话,只要掌握python的基本语法就可以直接运用到实际工作中。

进阶书籍
《流畅的Python》

对于想要扩充知识的中级和高级Python程序员来说,这本书是充满了实用编程技巧的宝藏,当然我只是简单翻了翻,没有深入去实操,因为它太烧我滴脑壳了。

推荐这本书的主要目的,是里面讲解了Python的基本惯用法,可以让你的代码简洁、高效且可读,也就是业内常说的Pythonic,通过这种方式来培养你成为一名熟练的 Python 程序员。

《Python核心编程》

这一本是Python的进阶书籍,现在已经出到第三版了,强推!

内容简洁,但又涵盖了开发所用到的一些基本的库,以此提升你的编程水平。

主要分为三个部分:

第一部分为讲解了Python的一些通用应用,包括正则表达式、网络编程、Internet客户端编程、多线程编程、GUI编程等;

第二部分讲解了与Web开发相关的,主要包括Web客户端和服务器、Django Web框架、云计算等。第3部分则为一个补充/实验章节,包括文本处理以及一些其他内容。

《编写高质量Python代码的59个有效方法》

Python本身就具备简洁的特性,它更贴合我们的自然语法,所以使用起来较为流畅,这也是它流行起来的原因。

不过,你要是想掌握Python所特有的优势、魅力和表达能力,还是相当困难,而且语言中还有很多隐藏的陷阱,容易令开发者犯错。

这本书的59个技巧就是来帮助开发者拓展思维,让他们在遇到问题时可以举一反三,这便是作者的用意。

以下书籍我都整理成电子版PDF,需要的关注公众号/Python小白集训营/回复/电子书/,即可自动获取。

学习编程,单单只是看书是不够的,更重要的还是要多动手,多写代码,能够找个项目实践那就更好了。

从实际应用场景出发,用程序解决手头的一些繁琐复杂问题,这样才能加强自己对语言的理解能力。

本文转载自: 掘金

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

SpringMVC Json自定义序列化和反序列化

发表于 2021-01-28

需求背景

需求一:SpringMVC构建的微服务系统,数据库对日期的存储是Long类型的时间戳,前端之前是默认使用Long类型时间,现在前端框架改动,要求后端响应数据时,Long类型的时间自动变成标准时间格式(yyyy-MM-dd HH:mm:ss)。

涉及到这个转换的范围挺大,所有的实体表都有创建时间createTime和修改时间updateTime,目前的主要诉求也是针对这两个字段,并且在实体详情数据和列表数据都存在,需要一个统一的方法,对这两个字段进行处理。

需求二:前端请求上传的JSON报文,String类型的内容,可能会出现前后有空格的现象,如果前端框架未对此问题进行处理,后端收到的JSON请求反序列化为对象时,就会出现String类型的值,前后有空格,现需要一个统一的处理方法,对接收的String类型属性执行trim方法。

解决方案

SpringMVC默认的JSON框架为jackson,也可以使用fastjson。

jackson框架

自定义序列化

如果项目使用jackson框架做json序列化,推荐的方案是使用@JsonSerialize注解,示例代码如下:

1
2
3
4
5
java复制代码@JsonSerialize(using = CustomDateSerializer.class)  
private Long createTime;

@JsonSerialize(using = CustomDateSerializer.class)
private Long updateTime;

CustomDateSerializer类的实现示例如下:

1
2
3
4
5
6
7
8
9
java复制代码public class CustomDateSerializer extends JsonSerializer<Long> {

@Override
public void serialize(Long aLong, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
Date date = new Date(aLong);
jsonGenerator.writeString(sdf.format(date));
}
}

这种方案的好处如下:

  1. 自定义的实现类可以复用
  2. 精准到需要转换处理的字段,不受限于createTime和updateTime,更贴近于需求

缺点就是需要转换的字段都需要使用注解,工作量有点大

当然有其他的统一处理方案,这里不赘述。

自定义反序列化

在jackson框架上实现自定义序列化,也是非常方便的,继承SimpleModule类即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码@Component
public class StringTrimModule extends SimpleModule {

public StringTrimModule() {
addDeserializer(String.class, new StdScalarDeserializer<String>(String.class) {
@Override
public String deserialize(JsonParser jsonParser, DeserializationContext ctx) throws IOException {
String value = jsonParser.getValueAsString();
if (StringUtils.isEmpty(value)) {
return value;
}
return value.trim();
}
});
}
}

fastjson框架

如果工程里出现这个依赖:

1
2
3
4
5
java复制代码<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.62</version>
</dependency>

说明此工程使用的json框架为fastjson,那么jackson的@JsonSerialize就不会有触发入口了,我们来看看fastjson的处理方式。

自定义序列化

相应的,使用fastjson会有相应的配置类,示例如下:

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
java复制代码/**
* 统一输出是采用fastJson
*
* @return
*/
@Bean
public HttpMessageConverters fastJsonHttpMessageConverters() {
//convert转换消息的对象
FastJsonHttpMessageConverter fastConverter = new FastJsonHttpMessageConverter();

//处理中文乱码问题
List<MediaType> fastMediaTypes = new ArrayList<>();
fastMediaTypes.add(MediaType.APPLICATION_JSON_UTF8);
fastConverter.setSupportedMediaTypes(fastMediaTypes);

//是否要格式化返回的json数据
FastJsonConfig fastJsonConfig = new FastJsonConfig();
fastJsonConfig.setSerializerFeatures(SerializerFeature.PrettyFormat);
// 添加指定字段的值转换处理
fastJsonConfig.setSerializeFilters(new CustomerDateFilter());
// FastJson禁用autoTypeSupport
fastJsonConfig.getParserConfig().setAutoTypeSupport(false);
fastConverter.setFastJsonConfig(fastJsonConfig);

return new HttpMessageConverters(fastConverter);
}

这里需要添加fastjson对字段值的处理(上述代码已添加这行代码),如

1
2
java复制代码// 添加指定字段的值转换处理
fastJsonConfig.setSerializeFilters(new CustomerDateFilter());

CustomerDateFilter为自行实现的类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码public class CustomerDateFilter implements ValueFilter {

@Override
public Object process(Object object, String name, Object value) {
if (FieldConstants.CREATE_TIME.equalsIgnoreCase(name) || FieldConstants.UPDATE_TIME.equalsIgnoreCase(name)) {
// 属性名为createTime, updateTime进行转换处理
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdf.setTimeZone(TimeZone.getTimeZone("GMT+8"));

if(value instanceof Long) {
Long time = (Long) value;
Date date = new Date(time);
return sdf.format(date);
} else {
return value;
}
}
return value;
}
}

这样就可以把所有响应对象中出现的createTime和updateTime字段统一处理了,无论列表数据还是单个对象数据,非常方便。缺点就是除此之外的字段,如果还做不到全系统统一,就需要单独处理。

SerializeFilter定制序列化

支持SerializeFilter定制序列化的扩展编程接口有以下几个,可根据实际需要进行扩展:

  • PropertyPreFilter: 根据PropertyName判断是否序列化;
  • PropertyFilter: 根据PropertyName和PropertyValue来判断是否序列化;
  • NameFilter: 修改Key,如果需要修改Key,process返回值则可;
  • ValueFilter: 修改Value;
  • BeforeFilter: 序列化时在最前添加内容;
  • AfterFilter: 序列化时在最后添加内容;
自定义反序列化

fastJson提供了序列化过滤器,来实现自定义序列化改造,但没有提供反序列化过滤器,来实现对应的功能。

方案:@JSONField注解

回到对JSON报文String类型的值执行trim操作,官网支持@JSONField注解的属性设置(要求fastJson版本1.2.36以上):

1
2
java复制代码@JSONField(format="trim")
private String name;

在JSON报文反序列化时,该实体的name属性会自动执行trim方法进行处理。

此方案只有逐个添加注解,工作量较大。

方案:实现ObjectDeserializer接口

ObjectDeserializer接口为可以实现自定义反序列化实现接口,配合ParserConfig的全局设置,也可以达到预期的效果,合建StringTrimDeserializer类,对String进行处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码/**
* @title: StringTrimDeserializer
* @description: 把String类型的内容统一做trim操作
*/
public class StringTrimDeserializer implements ObjectDeserializer {

@Override
public <T> T deserialze(DefaultJSONParser parser, Type type, Object fieldName) {
// JSON String反序列化的逻辑比较复杂,在StringCodec的基础上,对其结果调用trim方法
Object obj = StringCodec.instance.deserialze(parser, type, fieldName);
if (obj instanceof String) {
String str = (String) obj;
return (T) str.trim();
}
return (T) obj;
}

@Override
public int getFastMatchToken() {
return JSONToken.LITERAL_STRING;
}
}

相应在,在HttpMessageConverters类fastJsonHttpMessageConverters方法内中增加String类的反序列化设置:

1
2
java复制代码// 设置String类的全局反序列化规则:自动完成trim操作
ParserConfig.getGlobalInstance().putDeserializer(String.class, new StringTrimDeserializer());

tips:
在StringTrimDeserializer类实现方法中为什么不直接parser.getLexer().stringVal()得到值后执行trim方法,而是调用StringCodec.instance的实现方法?

StringCodec是fastJson默认的String类型的反序列化逻辑类,里面要处理的类型有String、StringBuffer、StringBuilder等,还有各种的集合、数组结构,涉及的nextToken值都不相同,总之,对String文本的反序列化,实现逻辑和应对的场景都比较复杂,而此次的需求只是对String执行trim操作,复杂的逻辑还是交给StringCodec来处理,站在StringCodec的基础上,对其结果执行trim方法就可以达到预期目标。

小结

今天这篇是记录Json自定义序列化和反序列化的实践方案,开始实施前先确认工程里使用的框架是哪个,否则就会出现添加了@JsonSerialize注解,搞了大半天没有效果,回头一看框架是fastjson,没有触发入口,当然得不到预期效果,小小建议,希望对你有帮助。

本文转载自: 掘金

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

分布式日志搜集ELK

发表于 2021-01-28
  • github项目地址
  • ELK是ElasticSearch、Logstash、Kibana三大开源框架首字母大写简称。市面上也被称为Elastic Stack。其中ElasticSearch是一个基于Lucene、分布式、通过RESTful方式进行交互的接近实时搜索平台框架。类似谷歌、百度这种大数据全文搜索引擎的场景都可以使用ElasticSearch作为底层支持框架,可见ElasticSearch提供的搜索能力确实强大,世面上很多时候我们简称ElasticSearch为es。Logstash是ELK的中央数据流引擎,用于从不同目标(文件/数据存储/MQ)收集的不同格式数据,经过过滤后支持输出到不同目的的(文件/MQ/redis/elasticsearch/kafka等)。Kibana可以将elasticsearch的数据通过友好的页面展示出来,提供实时分析的功能
  • 市面上很多开发只要提到ELK能够一直说出它是一个日志分析架构技术栈总称,但实际上ELK不仅仅适用于日志分析,它还可以支持其它任何数据分析和收集的场景,日志分析和收集只是更具代表性,并非唯一性
  • image-20200915171853797
  • 收集清洗数据—搜索,存储—Kibana

ElasticSearch

Lucene简介

概述

  • Lucene是一套信息检索工具包,是jar包。不包含搜索引擎!包含索引结构、读写索引的工具、排序、搜索规则……(Solr)
  • Java编写,目标是为各种中小型应用软件加入全文检索功能

与ElasticSearch关系

  • ElasticSearch 是基于Lucene 做了一些封装和增强

ElasticSearch简介

概述

  • ElasticSearch,简称es,es是一个开源的高扩展的分布式全文检索引擎,它可以近乎实时的存储、检索数据;本身扩展性很好,它可以扩展到上百台服务器,处理PB级别的数据。es也使用java开发并使用Lucene作为其核心来实现所有索引和搜索功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单
  • 据国际权威的数据库产品评测机构DB Engines的统计,在2016年1月,ElasticSearch已超过

谁在使用

  • 维基百科,全文检索、高亮、搜索推荐(权重)
  • 新闻网站,类似搜狐新闻,用户行为日志(点击,浏览,收藏,评论)+社交网络数据(对***新闻的相关看法),数据分析,给到每篇新闻文章的作者,让他们知道他的文章的公众反馈(好文、水文、热门)
  • Stack Overflow(国外程序员异常讨论论坛)
  • Github(开源代码管理),搜索上千亿行代码
  • 电商网站,检索商品
  • 日志数据分析,logstash采集日志,ES进行复制的数据分析,ELK技术(ElasticSearch+logstash+Kibana)
  • 商品价格监控网站,用户设定某商品价格的阈值,当低于该阈值时,发送通知消息给用户
  • BI系统,商业智能,Business Intelligence。比如某大型商场,BI 分析一下某区域最近3年的用户消费金额的趋势以及用户群体的组成结构,产出相关的数据报表,ES执行数据分析与挖掘,kibana进行数据可视化
  • 国内:站内搜索(电商、招聘、门户等等),IT系统搜索(OA、CRM、ERP等等),数据分析(ES热门的一个使用场景)

Solr和ES的差别

ElasticSearch简介
  • ElasticSearch是一个实时分布式搜索和分析引擎。它让你以前所未有的速度处理大数据成为可能
  • 它用于全文搜索、结构化搜索、分析以及将这三者混合使用:
    • 维基百科使用ElasticSearch提供全文搜索并高亮关键字,以及输入实时搜索(Search-asyou-type)和搜索纠错(did-you-mean)等搜索建议功能
    • 英国卫报使用ElasticSearch结合用户日志和社交网络数据提供给他们的编辑以实时的反馈,以便及时了解公众对新发表的文章的回应
    • Stack Overflow结合全文搜索与地理位置查询,以及more-like-this功能来找到相关的问题和答案
    • Github使用ElasticSearch检索1300亿行代码
  • ElasticSearch是一个基于Apache Lucene(TM)的开源搜索引擎,无论在开源还是专有领域,Lucene可以被认为是迄今为止最先进、性能最好的、功能最全的搜索引擎库
    • 但Lucene只是一个库,想要使用它,你必须使用java代码来作为开发语言并将其直接集成到你的应用中,更糟糕的是,Lucene非常复杂,你需要深入了解检索的相关知识来理解它是如何工作的
    • ElasticSearch也使用Java开发并使用Lucene作为其核心来实现所有索引和搜索功能,但是它的目的是通过简单的RESTful API来隐藏Lucene的复杂性,从而让全文搜索变得简单
Solr简介
  • Solr 是Apache下的一个顶级开源项目,采用Java开发,它是基于Lucene的全文搜索服务器,Solr提供了比Lucene更为丰富的查询语言,同时实现可配置、可扩展、并对索引、搜索性能进行了优化
  • Solr 可以独立运行,运行在jetty、tomcat等这些Servlet容器中,Solr索引的实现方法很简单,用POST方法向Solr服务器发送一个描述Field及其内容的XML文档,Solr根据XML文档添加、删除、更新索引。Solr搜索只需要发送HTTP GET请求,然后通过对Solr返回XML、json等格式的查询结果进行解析,组织页面布局。Solr不提供构建UI的功能,Solr提供了一个管理界面,通过管理界面可以查询Solr的配置和运行情况
  • Solr是基于Lucene开发企业级搜索服务器,实际上就是封装了Lucene
  • Solr是一个独立的企业级搜索应用服务器,它是对外提供类似Web-Service的API接口,用户可以通过http请求,想搜索引擎服务器提交一定格式的文件,生成索引;也可以通过提出查找请求,并得到返回结果
ElasricSearch和Solr比较
  • 当单纯的对已有数据进行搜索时,Solr更快
  • 当实时建立索引时,Solr会产生IO阻塞,查询性能较差,ElasticSearch具有明显的优势
  • 随着数据量的增加,Solr的搜索效率会变得更低,而ElasticSearch却没有明显的变化
  • 转变我们的搜索基础设施后,从Solr ElasticSearch,可以发现~50倍提高搜索性能
ElasticSearch vs Solr总结
  • es基本开箱即用,非常简单。Solr安装略微复杂一点点
  • Solr利用Zookeeper进行分布式管理 ,而ElasticSearch自身带分布式协调管理功能
  • Solr支持更多格式的数据,比如JSON、XML、CSV,而ElasticSearch仅支持json文件格式
  • Solr官方提供的功能更多,而ElasticSearch本身更注重于核心功能,高级功能多有第三方插件提供,例如图形化界面需要Kibana友好支撑
  • Solr查询更快,但更新索引时慢(即插入删除慢),用于电商等查询多的应用
    • ES建立索引快(即查询慢),实时性查询快,用于Facebook、新浪等搜索
    • Solr是传统搜索应用的有力解决方案,但ElasticSearch更适用于新兴的实时搜索应用
  • Solr比较成熟,有一个更大,更成熟的用户、开发和贡献者社区,而ElasticSearch相对开发维护这较少,更新太快,学习使用成本较高

倒排索引(*)

  • 传统检索 正排索引 全文检索:倒排索引
  • 这种索引表中的每一项都包括一个属性值和具有该属性值的各记录的地址。由于不是由记录来确定属性值,而是由属性值来确定记录的位置,因而称为倒排索引(inverted index)
  • 倒排索引有两种不同的反向索引形式:
+ 一条记录的水平反向索引(或者反向档案索引)包含每个引用单词的文档的列表
+ 一个单词的水平反向索引(或者完全反向索引)又包含每个单词在一个文档中的位置
  • 如下例所示:

image-20200921115511448

倒排索引会对以上文档内容进行关键字分词,可以使用关键词直接定位到文档内容

image-20200921115724587

ElasticSearch安装

  • 声明:JDK1.8,最低要求!ElasticSearch客户端,界面工具
  • Java开发,ElasticSearch的版本和我们之后对应的java的核心jar包!版本对应,JDK环境是正常的

下载

  • 官网下载地址

安装ES

windows环境
  • 解压
  • 目录文件

image-20200915161308669

+ bin 启动文件
+ config 配置文件
    - log4j2 日志配置文件
    - jvm options java虚拟机相关的配置(默认需要1G内存)
    - elasticsearch.yml elasticsearch的配置文件(默认端口9200等)
+ lib 相关jar包
+ logs 日志
+ modules 功能模块
+ plugins 插件(\*)
Linux环境
  • tar.gz 安装包解压
1
shell复制代码tar -zxvf ***.tar.gz
  • 默认情况下 ES不支持ip访问,修改config下的elasticsearch.yml
1
2
yaml复制代码network.host: 192.168.83.133
cluster.initial_master_nodes: ["node-1", "node-2"]
  • 安装包启动方式需要额外配置参数
+ 修改文件句柄的限制



1
2
3
4
shell复制代码##修改限制
sudo vi /etc/sysctl.conf
##查看是否生效
sudo sysctl -p
![image-20200921103819733](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/5da51a78a900889dc48161fa93637031a0c06321282ba8aee5d218facbfda410) + 每个进程最大同时打开文件数太小,修改打开文件数的大小
1
shell复制代码sudo vi /etc/security/limits.conf
添加内容
1
2
3
4
shell复制代码 *               soft    nproc           4096
* hard nproc 4096
* soft nofile 65536
* hard nofile 65536
1
2
3
4
shell复制代码##通过命令查看软限制大小
ulimit -Sn
##通过命令查看硬限制大小
ulimit -Hn
+ 重启电脑后,再重启ElasticSearch

启动ES

  • 双击elasticsearch.bat 启动ElasticSearch

image-20200915161909224

  • 默认对外暴露的端口9200image-20200915162110843
  • 访问浏览器 127.0.0.1:9200

image-20200915162248445

安装可视化界面Head

下载地址
  • 需要有node环境
  • Head下载地址
编译运行image-20200915163246335

image-20200915163327864

  • 访问9100,出现跨域问题,导致未连接到9200image-20200915163427387
  • 解决跨域问题 elasticsearch.yml添加如下配置
1
2
yaml复制代码http.cors.enabled: true
http.cors.allow-origin: "*"
+ ![image-20200915163933283](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/9d2976fa0257801ea45b9b5edae537a61085adf9212f84bdce8780b0938bcb9e)
+ 重启ElasticSearch![image-20200915163857176](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/0ddd4c1cd8e952522952f48589fff3ebaf3d57e1bde22bb6e3edce890722a560)
  • 初学者,可以先把es看成一个数据库,可以建立索引(表),文档(表中的数据)

head 把它当做一个数据展示工具。后续所有的查询都在Kibana做

Kibana

Kibana简介

  • Kibana是一个针对ElasticSearch的开源分析及可视化平台,用来搜索、查看交互存储在Elasticsearch索引中的数据。使用Kibana,可以通过各种图表进行高级数据分析及展示。Kibana让海量数据更容易理解,它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示Elasticsearch查询动态。设置Kibana非常简单。无需编码或者额外的基础架构,几分钟内就能完成Kibana安装与启动ElasticSearch索引监测

Kibana安装

下载

  • kibana下载地址

安装

windows环境
  • 解压
  • 是一个标准的工程 bin/kibana.bat

image-20200915174201672

Linux环境
  • 解压kibana-7.6.1-linux-x86_64.tar.gz
  • 修改相关配置:vim kibana.yml

image-20200921110206452

image-20200921110139968

  • 1
    2
    3
    shell复制代码cd /usr/local/elk/kibana-7.6.1-linux-x86_64/bin/
    #启动
    ./kibana --allow-root

image-20200921110300552

启动Kibana

  • bin/kibana.bat 双击

image-20200915174623480

  • 访问测试 http://localhost:5601

image-20200915174708903

  • 开发工具
+ PostMan
+ curl
+ head
+ 谷歌浏览器插件测试(支持汉化)![image-20200916092128838](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/bc6ebd2c3476550372fb224d4938ca71db5316f425bf9239faff2ad13f218d73)


之后的所有操作都在这里进行操作

ES核心概念

概述

  • 上面内容已经知道es是什么,同时也把es的服务已经安装启动,那么es是如何存储数据,数据结构是什么,又是如何实现搜索的呢?

ES的相关概念

  • 集群
  • 节点
  • 分片
+ 节点和分片是如何工作的


    - 一个集群至少有一个节点,而一个节点就是一个es的进程,节点可以有多个索引默认的,如果你创建索引,那么索引将会有5个分片(primary shard,又称主分片)构成的,每一个主分片会有一个副本(replica shard,又称复制分片)![image-20200916101045597](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/0ffa3bdced373d100c953d5003dcfee6d130ee227391035e7a067e02fcad1c0a)
    - 上图是一个有3个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样即使某个节点挂了,数据也不至于丢失。实际上,一个分片是一个Lucene索引,一个包含**倒排索引**的文件目录,倒排索引的结构使得es在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字
    - > 倒排索引


    es使用的是一种称为倒排索引的结构,采用Lucene倒排索引作为底层。这种结构**适用于快速的全文搜索**,一个索引由文档中所有不重复的列表构成,对于每一个词,都有一个包含它的文档列表。例如,现在有两个文档,每个文档包含如下内容:



    
1
2
sql复制代码Study every day,good good up to forever #文档1包含的内容
To forever,study every day,good good up #文档2包含的内容
为了创建倒排索引,**将每个文档拆分成独立的词(或称为词条或者tokens)**,然后创建一个包含所有不重复的词条的排序列表,然后列出每个词条出现在哪个文档: ![image-20200916102505162](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/8e03ae8c263667bc57ccd5b5810a0a80904adf4ecd91722e590913fdde608fa1) 现在我们试图搜索 to forever ,只需要查看包含每个词条的文档![image-20200916102616953](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/5cae944709c0eab412362304febe22caf70fd48a975cfc095553a3327e1110a8) - 再来一个示例,比如我们通过博客标签来搜索博客文章,那么倒排索引列表就是这样的一个结构:![image-20200916102808701](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/eb355854abf5ea6bba7b05928962b64ec13b4409a06e2a0f0d3ca477a76fe117) * ·如果要搜索含有 python 标签的文章,那相对于查找所有原始数据而言,**查找倒排索引后的数据将会快得多**。只需要查看标签这一栏,然后获取相关的文章ID即可,完全过滤掉无关的所有数据 ,提高效率 - elasticsearch的索引和Lucene的索引对比 * 在elasticsearch中,索引(库)这个词被频繁使用,这就是术语的使用。在elasticsearch中,索引被分为多个分片,**每份分片是一个Lucene的索引**。**所以一个elasticsearch索引是由多个Lucene索引组成的**
  • 索引
+ 就是数据库
+ 索引是映射类型的容器,es中的索引是一个非常大的文档集合。索引存储了映射类型的字段和其他设置。然后它们被存储到了各个分片上
  • 类型
+ 类型是文档的逻辑容器。就像关系型数据库一样,表是行的容器,类型中对于字段的定义称为映射,比如name映射为字符串类型
  • 文档
+ 之前说es是面向文档的,那么就意味着索引和搜索数据的最小单位是文档。es中,文档有几个重要属性:
    - **自我包含**,一篇文档同时包含字段和对应的值,也就是同时包含key:value!
    - **可以是层次型的**,一个文档中包含自文档,复杂的逻辑实体就是这么来的
    - **灵活的结构**,文档不依赖预先定义的模式,在关系型数据库中需要预先定义字段才能使用,在es中,对于字段是非常灵活的,有时候可以忽略该字段,或者动态的添加一个新的字段
+ 尽管我们可以随意的新增或者忽略某个字段,但是,**每个字段类型非常重要**。比如一个年龄字段类型,可以是字符串类型也可以是整数型。因为ES会保存字段和类型之间的映射以及其他的设置,这种映射具体到每个映射的每种类型,这也是为什么在es中,类型有时候也被称为映射类型
  • 映射

MySQL和ElasticSearch对比

elasticsearch是面向文档,关系型数据库与elasticsearch的客观对比

MySQL ElasticSearch
数据库(database) 索引(indices)
表(tables) types
行(rows) document
字段(columns) field

elasticsearch(集群)中可以包含多个索引(数据库),每个索引中可以包含多个类型(表),每个类型下又包含多个文档(行),每个文档又包含多个field(列)

物理设计

  • elasticsearch在后台把每个索引划分成多个分片,每个分片可以在集群中的不同服务器间迁移

逻辑设计

  • 一个索引类型中,包含多个文档,比如说文档1,文档2.当我们索引一篇文档时,可以通过这样的顺序找到它:索引>>类型>>文档ID,通过这个组合我们就能索引到某个具体的文档。注意:ID不必是整数,实际上它是个字符串

9200与9300区别

  • 9300端口:ES节点之间通讯使用
  • 9200端口:ES节点和外部通讯使用
  • 9300是TCP协议端口号,ES集群之间的通讯端口号。9200暴露ES RESTful接口的端口号

IK分词器插件

是什么

  • 分词:即把一段中文或者别的划分成一个个的关键字,我们在搜索时会把自己的信息进行分词,会把数据库中或者索引库中的数据进行分词,然后进行一个匹配操作,默认的中文分词是将每个字看成一个词,比如“我爱编程”会被分为 “我”、“爱”、“编”、“程”,这显然是不符合要求的,所以我们需要安装中文分词器ik来解决这个问题
  • IK提供了两个分词算法:ik_smart 和 ik_max_word,其中 ik_smart 为最少切分,ik_max_word 为最细颗粒度划分

IK分词器安装

下载

github下载地址

Windows安装

  • 解压下载的文件
  • 在es的plugins目录下新建文件夹ik
  • 将解压的文件放入ik文件夹中
  • image-20200916110503438

Linux安装

  • 跟windows基本一致
  • 将解压后的 重新命名为ik的文件夹拷贝至 plugins文件夹下

image-20200921141516456

重启观察ES

  • 看到ik分词器插件被加载image-20200916110641088
  • elasticsearch-plugin list 命令

image-20200916110840229

Kibana中测试IK分词器

  • kibana Dev Tools
+ ik\_smart (最少切分)


![image-20200916112003501](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/2b9a939f49cd2d52e7e8aae94c66df30cd4b78d5214febccca42851fb4bd7cb6)
+ ik\_max\_word(最细粒度划分)


![image-20200916112056827](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/bf3cd7bee83b283ba1327a566e26b0f348cecba88123ec56be6d6a08ad9c4084)
  • 发现问题:需要组合在一起的词,可能会被拆分开。这种个性化的词,需要我们自己加到分词器的字典中

IK分词器增加自己的配置

+ ik/config/IKAnalyzer.cfg.xml![image-20200916113545675](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/e242eec959b0792212e3fe18c0631b937c0e1bcbd59332c6fd5a98e3f3b83a09)
+ 新增自定义字典touchair,并注入扩展配置中,然后重启ES![image-20200916113904790](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/38d78195b3355587c778ea409be622ba55c7caac7cc885b7505e3dadb803416e)
+ 观察启动日志,发现加载了touchair.dic ,现在再次测试分词效果![image-20200916114202197](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/758692baa578502a3274835b679d8fb345b585d6bde0b6b6b9e47e14c6f48f77)
+ 测试结果


    - 添加自定义字典前:触达被拆分为触、达![image-20200916114353919](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/2c2b008f1aab97fad701821d3157edd11903ccb9ea935ec5dd4f4ea5dbb4ed78)
    - 配置后,可以拆分成自己想要的结果![image-20200916114600866](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/3a0e4823bbc32cdb81aece16c81091c16e4f3cee78bccc4bc914ae2297ea235e)

REST风格说明

  • 一种软件架构风格,而不是标准,只是提供一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制
  • 基本REST命令说明:
methood url 描述
PUT localhost:9200/索引名称/类型名称/文档id 创建文档(指定文档id)
POST localhost:9200/索引名称/类型名称 创建文档(随机文档id)
POST localhost:9200/索引名称/类型名称/文档id/_update 修改文档
DELETE localhost:9200/索引名称/类型名称/文档id 删除指定文档
GET localhost:9200/索引名称/类型名称/文档id 查询文档通过文档id
POST localhost:9200/索引名称/类型名称/_search 查询所有数据

索引基本操作

  • 创建一个索引(POST)
1
2
bash复制代码PUT /索引名/~类型名~/文档id
{请求体}

image-20200916134439645

image-20200916134507041

创建索引的同时,插入了一条数据image-20200916134858682

文档映射

  • 动态映射:在关系型数据库中,需要事先创建数据库,然后在该数据库实例下创建数据表,然后才能在该数据表中插入数据。而ElasticSearch中不需要事先定义映射(Mapping),文档写入ElasticSearch时,会根据文档字段自动识别类型,这种机制称为动态映射
  • 静态映射:在ElasticSearch中也可以事先定义好映射,包含文档的各个字段及其类型等,这种方式称之为静态映射
  • 类型分类:
+ 字符串类型:**text、keyword**



> `text` 类型会被分词器分割 `keyword` 不会被分割
+ 数值类型:long、integer、short、byte、double、float、half、scaled、
+ 日期类型:date
+ 布尔值类型:boolean
+ 二进制类型:binary
+ 数组类型:array
+ 复杂类型


    - 地理位置类型(Geo datatypes)
        * 地理坐标类型(Geo-point datatype):geo\_point 用于经纬度坐标
        * 地理形状类型(Geo-Shape datatype):geo\_shape 用于类似于多边形的复杂形状
    - 特定类型(Specialised datatypes)
        * Pv4 类型(IPv4 datatype):ip 用于IPv4地址
        * Completion类型:completion 提供自动补全建议
        * Token count 类型:用于统计做子标记的字段的index数目,该值会一直增加,不会因为过滤条件而减少
        * mapper-number3 类型:通过插件,可以通过\_number3来计算index 的哈希值
        * 附加类型(Attachment datatype):采用mapper-attachments插件,可支持\_attachments 索引,例如Microsoft office格式,Open Document格式,ePub,HTML等
  • 创建并指定字段类型(POST)

还可以指定分词器类型

image-20200916140058390

  • 获得这个规则(GET)image-20200916140249705
  • 查看默认的信息
1
2
3
4
5
6
7
json复制代码PUT /test3/_doc/1
#_doc 是默认类型的显示说明 ,可以省略
{
"name":"touchair-3",
"age":"19",
"birth":"2020-09-16"
}

image-20200916140707302

查看image-20200916140819199

如果自己的文档字段没有指定类型,ES会给我们默认配置字段类型!

  • 扩展:通过命令 GET _cat 可以获得ES的很多当前信息

  • GET _cat/health 查看健康信息

image-20200916141207680

  • GET _cat/indices?v 查看所有image-20200916141420242
  • 修改数据(POST/PUT)
+ 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码PUT /test3/_doc/1
{
"name":"touchair-3-put",
"age":"20",
"birth":"2020-09-15"
}


POST /test3/_doc/1/_update
{
"doc":{
"name":"touchair-3-post"
}
}
+ PUT 覆盖型![image-20200916142010369](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/42d5e062be5a777a89df80c5d4a7fa939d95120dc6148fe3f085e8ad73bfd07f) + POST 更新![image-20200916142359122](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/6b6064ac4c4cc0b0d135d623cca51e5ecf69d1f90d717a29f37c90b3e6fa92d7) + 结果查看![image-20200916142832368](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/db1b36ba4876eb01a3125911a6965b20c7b069d2d8bc2b536a1fa5a0a783be22)
  • 删除索引 (DELETE) 根据请求url来判断是删除索引还是删除文档记录

image-20200916142933503

文档的基本操作(*)

ElasticSearch版本控制

  • version 字段
  • 为什么要进行版本控制CAS无锁

为了保证数据再多线程操作下的准确性

  • 悲观锁和乐观锁
+ 悲观锁:假设一定会有并发冲突,屏蔽一切可能违反数据准确性的操作
+ 乐观锁:假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性
  • 内部版本控制和外部版本控制
+ 内部版本:\_version 自增长,修改数据后,version会自动加1
+ 外部版本:为了保持version与外部版本控制的数值一致,使用version\_type=external检查数据当前的version值是否小于请求中的version值

简单操作

添加测试数据
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
json复制代码PUT /touchair/user/1
{
"name":"z3",
"age": 11,
"desc":"这里是z3",
"tags":["技术宅","老直男","加班狗"]
}

PUT /touchair/user/2
{
"name":"l4",
"age": 12,
"desc":"这里是l4",
"tags":["奋斗逼","渣男","杭州"]
}

PUT /touchair/user/3
{
"name":"w5",
"age": 30,
"desc":"这里是w5",
"tags":["靓仔","扑街","旅游"]
}

PUT /touchair/user/4
{
"name":"w55",
"age": 31,
"desc":"这里是w55",
"tags":["靓女","看电影","旅游"]
}

PUT /touchair/user/5
{
"name":"学习Java",
"age": 32,
"desc":"这里是学习Java",
"tags":["钓鱼","读写","写字"]
}

PUT /touchair/user/6
{
"name":"学习Node.js",
"age": 33,
"desc":"这里是学习Node.js",
"tags":["上课","睡觉","打游戏"]
}

image-20200916145223819

获取数据(GET)
1
json复制代码GET touchair/user/1

image-20200916145332398

更新数据 (POST)
1
2
3
4
5
6
7
8
json复制代码POST touchair/user/2/_update
{
"doc":{
"name":"l4-2"
}
}

GET touchair/user/2

image-20200916145619786

image-20200916145718248

简单的查询
  • 条件查询
1
json复制代码GET touchair/user/_search?q=name:w5

image-20200916150845213

复杂操作

复杂查询 select(排序、分页、高亮、模糊查询、精准查询!)

  • hits中的属性 _score代表匹配度,匹配度越高,分值越高
  • hit:
+ 索引和文档的信息
+ 查询的结果总数
+ 查询出来的具体的文档
+ 都可以遍历得出
+ 可以通过score,得出谁更符合条件
匹配 match
1
2
3
4
5
6
7
8
json复制代码GET touchair/user/_search
{
"query": {
"match": {
"name": "w5"
}
}
}

image-20200916153835628

结果返回字段 不需要那么多 _source
1
2
3
4
5
6
7
8
9
json复制代码GET touchair/user/_search
{
"query": {
"match": {
"name": "学习"
}
},
"_source": ["name","desc"]
}

image-20200916160115007

排序

按年龄倒叙

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码GET touchair/user/_search
{
"query": {
"match": {
"name": "学习"
}
},
"sort": [
{
"age": {
"order": "desc"
}
}
]
}

image-20200917092647042

分页

from size 相当于MySQL limit语句的两个参数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码GET touchair/user/_search
{
"query": {
"match": {
"name": "学习"
}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 0,
"size": 1
}

image-20200917094223144

条件匹配
  • 精确查询 must,等价于MySQL的 and 操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码GET touchair/user/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "学习"
}
},
{
"match": {
"age": "32"
}
}
]
}
}
}

image-20200917094853364

  • should 等价于MySQL的 or 操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码GET touchair/user/_search
{
"query": {
"bool": {
"should": [
{
"match": {
"name": "学习"
}
},
{
"match": {
"age": "11"
}
}
]
}
}
}

image-20200917095412169

  • must_not 等价于MySQL not 操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
json复制代码GET touchair/user/_search
{
"query": {
"bool": {
"must_not": [
{
"match": {
"age": 33
}
}
]
}
}
}

image-20200917095741382

匹配数据过滤 filter
  • range 区间
  • gte 大于等于
  • lte 小于等于
  • gt 大于
  • lt 小于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
json复制代码GET touchair/user/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name":"学习"
}
}
],
"filter": {
"range": {
"age": {
"lte": 32
}
}
}
}
}
}

image-20200917100251028

多条件查询 match
  • match 等价于 like
1
2
3
4
5
6
7
8
json复制代码GET touchair/user/_search
{
"query": {
"match": {
"tags": "男 技术"
}
}
}

image-20200917101229000

精确查询 term
  • 等价于 equals
  • 查询是直接通过倒排索引指定的词条进行精确查找的
  • keyword 类型字段 只能被精确查找

关于分词:

trem,直接查询精确地

match,会使用分词器解析(先分析文档,在通过分析的文档进行查询)

两个类型

text 类型会被分词器分割

keyword 不会被分割 只能被精确查找

image-20200917102841536

高亮查询
  • highlight
1
2
3
4
5
6
7
8
9
10
11
12
13
json复制代码GET touchair/user/_search
{
"query": {
"match": {
"name": "学习"
}
},
"highlight": {
"fields": {
"name":{}
}
}
}

image-20200917103619084

  • 自定义高亮包围的标签
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码GET touchair/user/_search
{
"query": {
"match": {
"name": "学习"
}
},
"highlight": {
"pre_tags": "<p class='key',style='color:red'>",
"post_tags": "</p>",
"fields": {
"name":{}
}
}
}

image-20200917103920555

Term与Match区别
  • Term查询不会对字段进行分词查询,会采用精确匹配(Equals)
  • Match会根据该字段的分词器,进行分词查询(Like)

ES集成SpringBoot

官方文档

文档地址

  • ElasticSearch 7.6 客户端文档

maven依赖

  • pom.xml
  • 1
    2
    3
    4
    5
    xml复制代码<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>7.6.2</version>
    </dependency>

初始化

  • image-20200917105446501

创建项目

  • 新建springboot项目
  • 选择依赖 — 最主要的NoSQL中的 ElasticSearch

API调用测试

索引的操作

创建索引
判断索引是否存在
获取索引
删除索引

文档的CRUD

创建文档
获取文档
更新文档
删除文档
批量插入文档
文档查询(*)
  • 1
    2
    3
    4
    5
    scss复制代码SearchRequest 搜索请求
    SearchSourceBuilder 条件构造
    highlighter 高亮
    matchAllQuery 匹配所有
    termQuery() 精确查找
  • 测试类代码

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
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
java复制代码package com.touchair.elk;

import cn.hutool.json.JSONUtil;
import com.touchair.elk.pojo.User;
import org.assertj.core.util.Lists;
import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.support.master.AcknowledgedResponse;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.action.update.UpdateResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.client.indices.GetIndexRequest;
import org.elasticsearch.client.indices.GetIndexResponse;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.MatchAllQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.test.context.SpringBootTest;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

@SpringBootTest
class ElkApplicationTests {

public static final String INDEX_NAME = "java_touchair_index";

@Resource
@Qualifier("restHighLevelClient")
private RestHighLevelClient restHighLevelClient;


/**
* 测试创建索引
*
* @throws IOException
*/
@Test
void testCreateIndex() throws IOException {
//创建索引的请求
CreateIndexRequest indexRequest = new CreateIndexRequest("java_touchair_index");
//客户端执行请求 IndicesClient,请求后获取响应
CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(indexRequest, RequestOptions.DEFAULT);
System.out.println(createIndexResponse.toString());
}

/**
* 测试 获取索引
*
* @throws IOException
*/
@Test
void testGetIndex() throws IOException {
GetIndexRequest getIndexRequest = new GetIndexRequest("java_touchair_index");
boolean exists = restHighLevelClient.indices().exists(getIndexRequest, RequestOptions.DEFAULT);
if (exists) {
GetIndexResponse getIndexResponse = restHighLevelClient.indices().get(getIndexRequest, RequestOptions.DEFAULT);
System.out.println(getIndexResponse);
} else {
System.out.println("索引不存在");
}
}

/**
* 测试 删除索引
*
* @throws IOException
*/
@Test
void testDeleteIndex() throws IOException {
DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("test2");
AcknowledgedResponse acknowledgedResponse = restHighLevelClient.indices().delete(deleteIndexRequest, RequestOptions.DEFAULT);
System.out.println(acknowledgedResponse.isAcknowledged());
}


/**
* 测试 文档创建
*
* @throws IOException
*/
@Test
void testAddDocument() throws IOException {
//创建对象
User user = new User("java", 23);
//创建请求
IndexRequest indexRequest = new IndexRequest("java_touchair_index");

//规则 put /java_touchair_index/_doc/1
indexRequest.id("1");
indexRequest.timeout(TimeValue.timeValueSeconds(1));

//将数据放入请求
indexRequest.source(JSONUtil.toJsonPrettyStr(user), XContentType.JSON);

//客户端发送请求,获取
IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
System.out.println(indexResponse.toString());
System.out.println(indexResponse.status());
}

/**
* 测试 获取文档
*
* @throws IOException
*/
@Test
void testGetDocument() throws IOException {
//判断文档是否存在 get /index/_doc/1
GetRequest getRequest = new GetRequest(INDEX_NAME, "1");
// //不获取返回的 _source 的上下文了
// getRequest.fetchSourceContext(new FetchSourceContext(false));
// getRequest.storedFields("_none_");
boolean exists = restHighLevelClient.exists(getRequest, RequestOptions.DEFAULT);
if (exists) {
GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
System.out.println(getResponse.toString());
//打印文档内容
//返回的全部内容和命令行结果一模一样
System.out.println(getResponse.getSourceAsString());
} else {
System.out.println("文档不存在");
}
}

/**
* 测试 更新文档信息
*
* @throws IOException
*/
@Test
void testUpdateDocument() throws IOException {
UpdateRequest updateRequest = new UpdateRequest(INDEX_NAME, "1");
updateRequest.timeout("1s");
User user = new User("ES搜索引擎", 24);
updateRequest.doc(JSONUtil.toJsonPrettyStr(user), XContentType.JSON);
UpdateResponse updateResponse = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(updateResponse.status());
}

/**
* 测试 删除文档
*
* @throws IOException
*/
@Test
void testDeleteDocument() throws IOException {
DeleteRequest deleteRequest = new DeleteRequest(INDEX_NAME, "1");
deleteRequest.timeout("1s");
DeleteResponse deleteResponse = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(deleteResponse.getResult());
}


/**
* 特殊 ,真实项目一般都是批量插入数据
*
* @throws IOException
*/
@Test
void testBulkRequest() throws IOException {
BulkRequest bulkRequest = new BulkRequest();
bulkRequest.timeout("10s");

ArrayList<User> userList = Lists.newArrayList();

userList.add(new User("Java", 11));
userList.add(new User("javaScript", 12));
userList.add(new User("Vue", 13));
userList.add(new User("Mysql", 14));
userList.add(new User("Docker", 15));
userList.add(new User("MongoDB", 16));
userList.add(new User("Redis", 17));
userList.add(new User("Tomcat", 18));

for (int i = 0; i < userList.size(); i++) {
//批量更新和批量删除 只需在这里修改对应的请求即可
bulkRequest.add(new IndexRequest(INDEX_NAME)
.id("" + i + 1)
.source(JSONUtil.toJsonPrettyStr(userList.get(i)), XContentType.JSON));

}
BulkResponse bulkResponse = restHighLevelClient.bulk(bulkRequest, RequestOptions.DEFAULT);
System.out.println((bulkResponse.hasFailures())); //是否失败 返回false 则成功
}


/**
* 查询
* SearchRequest 搜索请求
* searchSourceBuilder 条件构造
* highlighter 高亮
* matchAllQuery 匹配所有
* termQuery() 精确查找
*
* @throws IOException
*/
@Test
void testSearch() throws IOException {
SearchRequest searchRequest = new SearchRequest(INDEX_NAME);
//构建搜索条件
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
//查询条件,可以使用QueryBuilders工具 快速查询
//QueryBuilders.matchAllQuery 匹配所有
//QueryBuilders.termQuery() 精确查找
// TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("age", 11);
MatchAllQueryBuilder matchAllQueryBuilder = QueryBuilders.matchAllQuery();
searchSourceBuilder.query(matchAllQueryBuilder);
searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
// searchSourceBuilder.highlighter();
// searchSourceBuilder.size();
// searchSourceBuilder.from();
searchRequest.source(searchSourceBuilder);
SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

for (SearchHit searchHits : searchResponse.getHits().getHits()) {
System.out.println(searchHits.getSourceAsMap());
}
}

}

仿商城搜索

  • 爬取网页数据
  • 分页搜索
  • 高亮
  • 效果图image-20200918152734862

分布式日志收集

ELK分布式日志收集原理(*)

  • 每台服务器集群节点安装Logstash日志收集系统插件
  • 每台服务器节点将日志输入到Logstash中
  • Logstash将该日志格式化为json格式,根据每天创建不同的索引,输出到ElasticSearc中
  • 浏览器安装使用Kibana查询日志信息

image-20200918160002489

环境安装

  • 1、安装ElasticSearch
  • 2、安装Logstash
  • 3、安装Kibana

Logstash

介绍

  • Logstash是一个完全开源的工具,它可以对你的日志进行收集、过滤、分析,支持大量的数据获取方法,并将其存储供以后使用(如搜索)。说到搜索,Logstash带有一个web界面,搜索和展示所有日志。一般工作方式为c/s架构,client端安装在需要收集日志的主机上,server端负责将收到的各个节点日志进行过滤、修改等操作再一并发往ElasticSearch中
  • 核心流程:
+ Logstash事件处理有三个阶段:input-->filters-->outputs
+ 是一个接收、处理、转发日志的工具
+ 支持系统日志、webserver日志、错误日志、应用日志,总之包括所有可以抛出来的日志类型![image-20200918162250605](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/859604a4066a4c6b4c5e841641d829a8834681647b24f1cf0b3432d5ab7adbbf)

Logstash环境搭建

  • 下载地址
  • 解压

Logstash测试

  • 将elsaticsearch的日志输入进logstash
+ 进入logstash的config目录下,创建touchair.conf


![image-20200921162218753](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/ac4c2dec0d281a37a7e7d988d90c9a78b75ecbc8f469cc7c1e243e66a75efd53)
+ 添加以下内容并保存



1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码input {
# 从文件读取日志信息,输送到控制台
file{
path => "/usr/local/elk/elasticsearch-7.6.1/logs/elasticsearch.log"
#以JSON格式读取日志
codec => "json"
type => "elasticsearch"
start_position =>"beginning"
}
}

output {
# 标准输出
#stdout{}
# 输出进行格式化,采用Ruby库来解析日志
stdout { codec => rubydebug }
}
+ 启动logstash,观察控制台 logstash的bin目录下
1
shell复制代码./logstash -f ../config/touchair.conf
![image-20200921162426453](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/cec5269b73a9b32326ccf62bb6907ad8bfb3cac7747a9c31afeef70c0b9ae08a)

将日志输出到ES中

  • 创建并修改 touchair.es.conf文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
c复制代码input {
# 从文件读取日志信息,输送到控制台
file{
path => "/usr/local/elk/elasticsearch-7.6.1/logs/elasticsearch.log"
#以JSON格式读取日志
codec => "json"
type => "elasticsearch"
start_position =>"beginning"
}
}

output {
# 标准输出
#stdout{}
# 输出进行格式化,采用Ruby库来解析日志
stdout { codec => rubydebug }
elasticsearch {
hosts => ["192.168.83.133:9200"]
index => "es-%{+YYYY.MM.dd}"
}
}
  • 启动logstash
1
shell复制代码 ./logstash -f ../config/touchair.es.conf

Logstash整合Springboot

​ 单行

  • 1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    c复制代码# tcp -> Logstash -> Elasticsearch pipeline.
    input {
    tcp {
    mode => "server"
    host => "0.0.0.0"
    port => 4560
    codec => json_lines
    }
    }
    output {
    elasticsearch {
    hosts => ["192.168.83.133:9200"]
    index => "robot-java-%{+YYYY.MM.dd}"
    }
    }

运行日志多行才能准确定位问题

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
c复制代码input {
tcp {
mode => "server"
host => "0.0.0.0"
port => 4560
codec => multiline{
pattern => "^\["
negate => false
what => "next"
}
}
}
filter {
json {
source => "message"
}
mutate {
add_field => {
"language" => "%{[type]}"
}
}
}
output{
if [language]=="java" {
elasticsearch {
hosts => ["172.17.0.8:9200"]
index => "robot-java-%{+YYYY.MM.dd}"
}
}

if [language]=="ros" {
elasticsearch {
hosts => ["172.17.0.8:9200"]
index => "robot-ros-%{+YYYY.MM.dd}"
}
}
if [language]=="rec" {
elasticsearch {
hosts => ["172.17.0.8:9200"]
index => "robot-rec-%{+YYYY.MM.dd}"
}
}
}

ELK docker部署

安装ElasticSearch

拉取镜像
1
复制代码docker pull elasticsearch:7.6.1
运行容器
  • 运行命令创建启动容器:
1
2
shell复制代码docker run -d --name es -p 9200:9200 -p 9300:9300 \
-e "discovery.type=single-node" elasticsearch:7.6.1
  • 将配置文件、数据目录拷出来做挂载
1
2
shell复制代码docker cp es:/usr/share/elasticsearch/config/ /var/elk/elasticsearch/config
docker cp es:/usr/share/elasticsearch/data/ /var/elk/elasticsearch/data
  • 设置允许跨域访问
+ 
1
2
3
4
5
shell复制代码vim  /var/elk/elasticsearch/config/elasticsearch.yml

#添加这2行
http.cors.enabled: true
http.cors.allow-origin: "*"
  • 销毁容器,重新以挂载方式运行
1
2
3
4
5
6
7
8
9
shell复制代码#销毁
docker rm -f es

#挂载配置文件
docker run -d --name es -p 9200:9200 -p 9300:9300 \
-v /var/elk/elasticsearch/config/:/usr/share/elasticsearch/config/ \
-v /var/elk/elasticsearch/data/:/usr/share/elasticsearch/data/ \
-e "discovery.type=single-node" \
elasticsearch:7.6.1
  • 访问宿主机ip的9200端口,查看是否启动成功

image-20200924105615012

安装Kibana

拉取镜像
1
shell复制代码docker pull kibana:7.6.1
运行容器
  • 先运行容器
1
shell复制代码docker run -d --name kibana -p 5601:5601 kibana:7.6.1
  • 拷出配置文件,后面做挂载
1
2
3
4
5
6
7
8
shell复制代码# 拷贝
docker cp kibana:/usr/share/kibana/config/ /var/elk/kibana/config

#查看es容器的内部ip
docker exec -it es ifconfig

#修改配置
vim kibana.yml

image-20200924113238385

image-20200924113958460

  • 挂载运行
1
2
3
4
5
6
7
shell复制代码# 先销毁容器
docker rm -f kibana

# 运行容器
docker run -d --name kibana -p 5601:5601 \
-v /var/elk/kibana/config:/usr/share/kibana/config \
kibana:7.6.1
  • 宿主机ip:5601,查看kibana图形化界面

安装LogStash

拉取镜像
1
shell复制代码docker pull logstash:7.6.1
运行容器
  • 先运行容器
1
shell复制代码docker run --name logstash -d -p 4560:4560 -p 9600:9600 logstash:7.6.1
  • 拷出配置文件,后面做挂载
1
shell复制代码docker cp logstash:/usr/share/logstash/config /var/elk/logstash/config
+ 添加自定义的conf 文件



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
c复制代码input {
tcp {
mode => "server"
host => "0.0.0.0"
port => 4560
codec => multiline{
pattern => "^\["
negate => false
what => "next"
}
}
}
filter {
json {
source => "message"
}
mutate {
add_field => {
"language" => "%{[type]}"
}
}
}
output{
if [language]=="java" {
elasticsearch {
hosts => ["172.17.0.8:9200"]
index => "robot-java-%{+YYYY.MM.dd}"
}
}

if [language]=="ros" {
elasticsearch {
hosts => ["172.17.0.8:9200"]
index => "robot-ros-%{+YYYY.MM.dd}"
}
}
if [language]=="rec" {
elasticsearch {
hosts => ["172.17.0.8:9200"]
index => "robot-rec-%{+YYYY.MM.dd}"
}
}
}
+ 修改配置文件 logstash.yml
1
shell复制代码vim logstash.yml
![image-20200924130614018](https://gitee.com/songjianzaina/juejin_p9/raw/master/img/8f0f41c1da19bd4d88ed76ebaaeaca76e1751207e0d8ee765c38ee62fc159dba)
  • 挂载运行
1
2
3
4
5
6
7
8
shell复制代码#删除容器
docker rm -f logstash

#重新启动容器
docker run --name logstash -d -p 4560:4560 -p 9600:9600 \
-v /var/elk/logstash/config:/usr/share/logstash/config \
logstash:7.6.1 \
-f /usr/share/logstash/config/robot.conf

一旦ES容器内部IP变化,需改动kibana.yml 以及logstash.yml和自定义的conf文件中的ES服务地址,并重启kibana和Logstash

本文转载自: 掘金

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

MySQL索引条件下推优化实战

发表于 2021-01-28

先说一下文章安排,首先介绍索引条件下推(ICP,以下均用ICP简称),然后解决线上的一个慢查询。先卖个关子,这个慢查询非常奇怪。

ICP(Index Condition Pushdown) - 索引条件下推

什么是ICP?

不如换个问法,ICP的作用是什么?
一句话总结:索引条件下推ICP就是尽可量利用二级索引筛除不符合where条件的记录,如此一来减少需要回表继续判断的次数

With ICP enabled, and if parts of the WHERE condition can be evaluated by using only columns from the index, the MySQL server pushes this part of the WHERE condition down to the storage engine.The storage engine then evaluates the pushed index condition by using the index entry and only if this is satisfied is the row read from the table. ICP can reduce the number of times the storage engine must access the base table and the number of times the MySQL server must access the storage engine.

如何确定某条语句使用了ICP?

Explain的输出项的Extra会显示Using index condition

官方示例 - 初次体会ICP

示例如下,这个例子来自MySQL官方文档:
Suppose:假设这个表有联合索引INDEX(zipcode, lastname, firstname)

1
2
3
4
mysql复制代码SELECT * FROM people
WHERE zipcode='95054'
AND lastname LIKE '%etrunia%'
AND address LIKE '%Main Street%';
  • 不用ICP,只使用最左匹配原则。那么只能使用联合索引的zipcode,回表记录不能有效去除。
  • 使用ICP,除了匹配zipcode的条件之外,额外匹配联合索引的lastname,看其是否符合where条件中的'%etrunia%',然后进行回表。如此一来,使用联合索引就可以尽可量排除不符合where条件的记录。这就是ICP优化的真谛。

With Index Condition Pushdown, MySQL checks the lastname LIKE ‘%etrunia%’ part before reading the full table row. This avoids reading full rows corresponding to index tuples that match the zipcode condition but not the lastname condition.

自造示例 - explain输出

创建一个示例,select语句就是想要尽可量利用索引去掉不符合where条件的记录,输出其explain结果,看是否真的按照预期那样使用了ICP

再次总结,重要的事情多说几遍:ICP的实质就是通过二级索引尽可能的过滤不符合条件的记录,哪怕不符合最左匹配原则,减少回表,降低执行成本

线上问题

问题描述

根据监控,查询到慢查询日志。这个慢查询最奇怪的地方在于,它本应该使用ICP,但却无论如何都没能使用ICP。

表名 用到的索引
tbl_checkin_followers_partion idx_query(user_id, event_id, follower_id)联合索引

这张表用来记录好友关系,下面是这个慢查询语句:

1
2
3
4
5
6
mysql复制代码EXPLAIN 
select user_id, follower_id, event_id, is_notice, forbid
from tbl_checkin_followers_partion
where follower_id = 26407612
and user_id in (16388902,28532449,25771785,22383199,7331499,22057702,5913050,21043345,16841923,20954615,29327264,20428921,7008534,23268045,29081660,25542251,22481256,20884749,25770459,20200680,14144433,20452427,15762152,7270131,23102328,20288857,14275884,16161824,21886294,20007161,20785940,22115882,27661758,14602042,17261674,23177914,16889488,20887424,21042544,13615355,23870465,19223005,14718767,28303768,23741136,25175839,6426020,28237698,27967073,26407612)
;

查看执行计划,发现并没有使用索引条件下推(ICP)。

如何确定没有使用ICP?

  • Extra: 没有Using index condition

为什么应该使用索引下推?

首先联合索引idx_query(user_id, event_id, follower_id),其次搜索条件为 user_id in (...) and follower_id = 26407612。完全可以在联合索引idx_query上使用ICP,通过匹配user_id和follower_id两者进行回表,符合条件的记录数相比只使用user_id进行过滤然后回表的记录数一定会少很多。

但是根据explain的结果,Extra只有Using Where && key_len = 4(说明联合索引三个字段只用到了第一个user_id)该语句只是根据user_id进行回表,因为每个用户user_id有非常多的follower_id,回表的记录会非常多,并且这么多记录可能分布在聚促索引的多个页面,这就是随机I/O啊。一下子就将该查询语句变成慢查询。

为什么没有使用?

按照对ICP的理解,它就是尽量利用二级索引减少回表的记录数。在这个语句中,明明可以使用ICP,为什么没有使用呢?讲道理,它就应该使用ICP

排查过程

1、确定线上MySQL的版本,查看是5.6,ok,是支持ICP的。

2、抓耳挠腮。查了查,搞了搞,甚至看了源码,发现在ICP的使用条件中提到了分区,啊,分区!然后查了一下官方文档,才发现确实是分区表的问题。

ICP can be used for InnoDB and MyISAM tables. (Exception: ICP is not supported with partitioned tables in MySQL 5.6; this issue is resolved in MySQL 5.7.)

而我们这个表-tbl_checkin_followers_partion,是使用分区表的。

这是MySQL 5.6 关于ICP的页面

这是MySQL 5.7 关于ICP的页面

PS:我唯一的不满,就是为啥MySQL 5.6关于分区表的说明没有加粗。我当时只是粗看了一下5.6的文档,我主要在看5.7的文档,以为它俩一样的…

问题解决

MySQL 5.6版本分区表不能支持索引条件下推(ICP),那该如何是好?

1、直接将5.6升级到5.7,这样可能会被打死吧。

2、其实办法很简单,那就是结合业务场景+利用MySQL的范围查找。

tips:联合索引中的event_id只有两个固定值,表示早上和晚上

1
2
3
4
5
6
7
mysql复制代码EXPLAIN 
select user_id, follower_id, event_id, is_notice, forbid
from tbl_checkin_followers_partion
where follower_id = 26407612
and user_id in (16388902,28532449,25771785,22383199,7331499,22057702,5913050,21043345,16841923,20954615,29327264,20428921,7008534,23268045,29081660,25542251,22481256,20884749,25770459,20200680,14144433,20452427,15762152,7270131,23102328,20288857,14275884,16161824,21886294,20007161,20785940,22115882,27661758,14602042,17261674,23177914,16889488,20887424,21042544,13615355,23870465,19223005,14718767,28303768,23741136,25175839,6426020,28237698,27967073,26407612)

AND event_id in (1,3);


其实就是在搜索条件中增加了AND event_id in (1,3),如何确定这么修改之后,查询会变快。注意看explain中的key_len项,没有添加event_id搜索条件前key_len值是4,现在值为12。

这有啥区别呢?

区别大了去了,key_len就是用来判别使用联合索引时,我到底发挥作用的是几个列的值?因为联合索引的列数大于等于1,user_id、event_id、follower_id全都是int型,12就相当于该联合索引中的所有项都被用到。

如此一来,联合索引所有的字段值都被用到,也能够减少回表的记录数。

  • 当user_id=16388902时,

MySQL会搜满足条件:user_id=16388902 and event_id = 1 and follower_id=26407612

和 user_id=16388902 and event_id = 3 and follower_id=26407612

  • 当user_id=28532449

…

以此类推,此时就相当于区间只有1个值的多个区间范围查找。

使用ICP

我将表中的数据复制到测试数据库,并且测试数据库的版本为MySQL5.7,即分区表支持ICP的情况。
同样的初始查询,得到查询的json格式的执行计划:

1
2
3
4
5
6
mysql复制代码EXPLAIN 
select user_id, follower_id, event_id, is_notice, forbid
from tbl_checkin_followers_partion
where follower_id = 26407612
and user_id in (16388902,28532449,25771785,22383199,7331499,22057702,5913050,21043345,16841923,20954615,29327264,20428921,7008534,23268045,29081660,25542251,22481256,20884749,25770459,20200680,14144433,20452427,15762152,7270131,23102328,20288857,14275884,16161824,21886294,20007161,20785940,22115882,27661758,14602042,17261674,23177914,16889488,20887424,21042544,13615355,23870465,19223005,14718767,28303768,23741136,25175839,6426020,28237698,27967073,26407612)
;
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
json复制代码{
"query_block": {
"select_id": 1,
"cost_info": {
"query_cost": "110.01"
},
"table": {
"table_name": "tbl_checkin_followers_partion",
"partitions": [ // 用到的表的分区
"tbl_checkin_followers1",
"tbl_checkin_followers2",
"tbl_checkin_followers3",
"tbl_checkin_followers4",
"tbl_checkin_followers5"
],
"access_type": "range", // 访问方法
"possible_keys": [
"idx_query" // 可能用到的索引
],
"key": "idx_query", // 实际使用的索引
"used_key_parts": [
"user_id",
"event_id"
],
"key_length": "8",
"rows_examined_per_scan": 50,
"rows_produced_per_join": 0,
"filtered": "0.10",
"index_condition": "((`test`.`tbl_checkin_followers_partion`.`follower_id` = 26407612) and (`test`.`tbl_checkin_followers_partion`.`user_id` in (16388902,28532449,25771785,22383199,7331499,22057702,5913050,21043345,16841923,20954615,29327264,20428921,7008534,23268045,29081660,25542251,22481256,20884749,25770459,20200680,14144433,20452427,15762152,7270131,23102328,20288857,14275884,16161824,21886294,20007161,20785940,22115882,27661758,14602042,17261674,23177914,16889488,20887424,21042544,13615355,23870465,19223005,14718767,28303768,23741136,25175839,6426020,28237698,27967073,26407612)) and (`test`.`tbl_checkin_followers_partion`.`event_id` > 0))", // 在索引上可以使用的条件,也就是在索引上就能消灭的where条件。发现全都用了,ok,索引条件下推
"cost_info": {
"read_cost": "110.00",
"eval_cost": "0.00",
"prefix_cost": "110.01",
"data_read_per_join": "1"
},
"used_columns": [
"user_id",
"follower_id",
"event_id",
"forbid",
"is_notice"
]
}
}
}

请注意上面的index_condition,在存储层的索引上,已经用到了联合索引的所有字段进行过滤了。

我的疑问:

  • 为何used_key_parts与index_condition不太一样呢?这是个坑吗?
  • 为何5.6分区表不支持ICP呢?这和分区表的实现有关吗?

我在继续死磕MySQL中,如果心得继续更新叻。

参考

1、MySQL官方文档

2、掘金小册《MySQL是如何运行的》

本文转载自: 掘金

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

1…727728729…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%