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

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


  • 首页

  • 归档

  • 搜索

String中方法的源码整理 String

发表于 2021-07-28

String

String在Java中实际上是以字符数组的方式进行存储的,并且通过源码可以看出。

1
java复制代码char val[] = value;

在String中每次调用都是将它的value重新复制给另一个新的字符数组,所以我们不能更改它原本的值,而且value也不能被子类继承,所以String是不可更改的。

有一种情况String的值可以进行修改【反射】。后面在说

String类中的属性

1
2
3
4
5
6
7
8
9
10
11
java复制代码public final class String implements java.io.Serializable, Comparable<String>, CharSequence {
//字符数组,存储字符串的值
private final char value[];
//字符串类型的hash值,默认是0;
private int hash;
//String类的序列化ID,通过ID来识别软件版本
private static final long serialVersionUID = -6849794470754667710L;

private static final ObjectStreamField[] serialPersistentFields = new ObjectStreamField[0];

}
1
2
3
4
5
6
7
8
java复制代码/*
*StringBuffer:线程安全的带缓存的String,可变的字符串
*/
String str = "abc"; //创建了一个对象

String str1 = new String("def");//创建了两个对象

str1 = str1 + str;//创建了一个"abcdef"字符串对象,底层使用了StringBuffer的append();
1
2
3
java复制代码//String str = "abc";
char data[] = {'a','b','c'};
String str == new String(data);

构造函数

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
java复制代码/*无参构造*/
//不能进行追加,删除等操作,只能进行覆盖操作
public String(){
this.value = "".value;
}

//将参数的value和hash值都直接赋值给新创建的对象
public String (String original){
this.value = original.value;
this.hash = original.hash;
}

//截取字符串的子串,substring就是调用此构造函数
public String(char value[]/*截取字符串的字符数组*/, int offset/*开始的位置*/, int count/*截取的长度*/){
if(offset < 0){
throws new StringIndexOutOfBoundsException(offset);
}
if(count <= 0){
//截取长度不能小于0
if(count < 0){
throws new StringIndexOutOfBoundsException(count);
}
//count == 0时就代表没有进行截取,返回传入的value值
if(offset <= value.length){
this.value = "".value;
return;
}
}
//如果offset超届抛出异常
if(offset > value.length - count){
throws new StringIndexOutOfBoundsException(offset+count);
}
//都没有问题的时候
this.value = Arrays.copyOfRange(value, offset, offset+count);
}

String类的方法

hashCode

获取字符串的hash码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码//String中的hashCode
//在String中进行重写
public int hashCode(){
int h = hash;
if(h == 0 && value.length > 0){
char val[] = value;
for(int i = 0; i < value.length; i ++){
h = 31 * h + val[i];
}
hash = h;
}
return h;
}

//Object中的hashCode
//看不见具体的实现方法,它自动给你返回一个hash值
public native int hashCode();

equals

字符串进行比较,看是否相等

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复制代码//Object中的equals
public boolean equals(Object obj){
return (this == obj) //比较内存值
}

//String中的equals
public boolean equals(Object anObject){
if(this == anObject){
return true;
}
//测试anObject是否是String的实例
if(anObject instanceof String){
//将anObject强制转换成String类
String anotherString = (String)anObject;
int n = value.length;
//判断他们的长度是否相等
if(n == anotherString.value.length){
char v1[] = value;
char v2[] = anotherString.value;
int i = 0;
//将他们的每一个元素进行对比
while(n-- != 0){
if(v1[i] != v2[i]){
//一旦有元素不同,就证明他们不是同一对象
return false;
}
i ++;
}
return true;
}
}
return false;
}

charAt

获取想要位置的字符

1
2
3
4
5
6
7
8
java复制代码public char charAt(int index){
if((index < 0) || (index >= value.length)){
//索引位置超出界限,抛出异常
throw new StringIndexOutOfBoudsException(index);
}
//返回索引位置的字符
return value[index];
}

substring

剪下一段字符串

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码public String substring(int beginIndex){
if(beginIndex < 0){
//超出界限异常
throw new StringIndexOutOfBoudsException(beginIndex);
}
//剪切的长度
int subLen = value.length - beginIndex;
//剪切的长度不能超过字符串的长度
if(subLen < 0){
throws new StringIndexOutOfBoudsException(subLen);
}
//如果index从0开始就返回字符串全部,不然就新建一个字符串返回
return (beginIndex == 0) ? this : new String(value,beginIndex,subLen);
}

indexOf

查找字符在字符串中的位置

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
java复制代码public int indexOf(int ch/*传入字符时自动转化为int*/){
return indexOf(int ch, int fromIndex/*开始查找的位置*/);
}
//进行重载
public int indexOf(int ch, int fromIndex){
final int max = value.length;
if(fromIndex < 0){
//如果输入错误的index那么让函数从头开始查找
fromIndex = 0;
} else if(fromIndex >= max){
//选择范围超出了界限
return -1;
}
//查找通用字符,在java中存储的值通常都是小于MIN的
//MIN_SUPPLEMENTARY_CODE_POINT = 0x010000
if(ch < Character.MIN_SUPPLEMENTARY_CODE_POINT){
final char[] value = this.value;
for(int i = fromIndex; i < max; i ++){
//找到后返回下标
if(value[i] == ch){
return i;
}
}
//没有找到
return -1;
} else {
//查找的是补充字符,不是通用字符
return indexOfSupplementary(ch, fromIndex);
}
}

replace

替换字符

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
java复制代码public String replace(char oldChar, char newChar){
//要进行替换的字符不能相同,相同的话就相当于没有替换
if(oldChar != newChar){
int len = value.length;
int i = -1;//记录是否有需要替换的字符
char[] val = value;//避免直接操作原字符串
//遍历查看需要替换字符的位置
while(++i < len){
if(val[i] == oldChar){
break;
}
}
//进行替换
if(i < len){
//替换后的字符串
char buf[] = new char[len];
//在遇到第一个需要替换的字符前直接进行复制
for(int j = 0; j < i; j ++){
buf[j] = val[j];
}
while(i < len){
char c = val[i];
buf[i] = (c == oldChar) ? newChar : c;
i ++;
}
//返回替换后的字符串对象
return new String (buf, true);
}
}
return this;
}

trim

去除字符串中两头的空格,在ASCII码中,空格是可见字符的最小值。

image-20210728145758913

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码public String trim(){
int len = value.length;
int st = 0;//字符串的开始位置
char[] val = value;
//找到可见字符的起始位置
while((st < len) && (val[st] <= ' ')){
//遇见不可见字符则起始位置向后加
st ++;
}
//找到可见字符的结束位置
while((st < len) && val[len - 1] <= ' '){
//遇见不可见字符则结尾向前加
len --;
}
//将字符串从st开始到len结束进行剪切
return ((st > 0) || (len < value.length))/*最少有一个空格被解决*/ ? substring(st,len) : this/*没有空格被消除*/;
}

split

按照给定的规则将字符串分割成数组

image-20210728152854156

image-20210728152937214

image-20210728152956631

image-20210728153014483

image-20210728153034596

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
java复制代码public String[] split(String regex){
return split(regex, 0); //进行重载,从字符串头开始
}

public String[] split(String regex, int limit){
char ch = 0;
/*
*regex为一个字符时,这个字符不能为正则的元字符,例如"^$.+\\?{}[]"等
*regex是两个字符的时候并且第一个是'\'时,第二个不能时数字或字母
*/
if(((regex.value.length == 1 &&
".$|()[{^?*+\\".indexOf(ch = regex.charAt(0)) == -1)/*regex为一个字符时,这个字符不能为正则的元字符*/ ||
(regex.length() == 2 &&
regex.charAt(0) == '\\' &&
(((ch = regex.charAt(1))-'0')|('9'-ch)) < 0 &&
((ch-'a')|('z'-ch)) < 0 &&
((ch-'A')|('Z'-ch)) < 0)) &&/*regex是两个字符的时候并且第一个是'\'时,第二个不能时数字或字母*/
(ch < Character.MIN_HIGH_SURROGATE ||
ch > Character.MAX_LOW_SURROGATE)){
int off = 0;
int next = 0;
//匹配的次数,没有设置时默认查询到字符串结束
boolean limited = limit > 0;
//存放结果的数组
ArrayList<String> list = new ArrayList<>();

while((next = indexOf(ch, off)) != -1/*查询regex在String中的位置,确认String中有regex的存在,等于this.indexOf(ch, off)*/){
if(!limited || list.size() < limit - 1){
//没有进行次数初始化,或者当前数组的大小小于规定的次数
//将不包含regex的子字符串放入数组中
list.add(substring(off,next));
//off直接到已经查到的regex位置加1,跳过现在的regex
off = next + 1;
} else {
//现在数组的大小已经等于limit - 1,只需要进行最后一次操作
list.add(substring(off, value.length));
off = value.length;
break;
}
}
if(off == 0){
//没有找到字符串中的regex
return new String[]{this};
}
if(!limited || list.size() < limit){
//传入的limit > 字符串中regex的次数,需要将剩余的部分添加到数组中
list.add(substring(off, value.length));
}
//构造结果
int resultSize = list.size();
if(limit == 0){
//没有传入参数或者传入的时0时,将list最后面的空数据删除掉
while (resultSize > 0 && list.get(resultSize - 1).length() == 0) {
resultSize --;
}
}
//返回结果
String result[] = new String[resultSize];
return list.subList(0, resultSize).toArray(result);
}
return Pattern.compile(regex).split(this, limit);
}

如果没有使用第51行的代码的话将会造成下面的这个结果

1
2
3
java复制代码"boo:foo:boo".split("0",0)
-----------result--------------
{"b","",":f","",":b",""}

本文转载自: 掘金

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

MySQL show processlist说明

发表于 2021-07-28

show processlist和show full processlist

如果阻塞了,可以kill id;

processlist命令的输出结果显示了有哪些线程在运行,不仅可以查看当前所有的连接数,还可以查看当前的连接状态帮助识别出有问题的查询语句等。

如果是root帐号,能看到所有用户的当前连接。如果是其他普通帐号,则只能看到自己占用的连接。showprocesslist只能列出当前100条。如果想全部列出,可以使用SHOW FULL PROCESSLIST命令

d4a8d1abc1952d0e9bc1224aed98b47.png

各个列的含义:

①.id列,用户登录mysql时,系统分配的”connection_id”,可以使用函数connection_id()查看

②.user列,显示当前用户。如果不是root,这个命令就只显示用户权限范围的sql语句

③.host列,显示这个语句是从哪个ip的哪个端口上发的,可以用来跟踪出现问题语句的用户

④.db列,显示这个进程目前连接的是哪个数据库

⑤.command列,显示当前连接的执行的命令,一般取值为休眠(sleep),查询(query),连接(connect)等

⑥.time列,显示这个状态持续的时间,单位是秒

⑦.state列,显示使用当前连接的sql语句的状态,很重要的列。state描述的是语句执行中的某一个状态。一个sql语句,以查询为例,可能需要经过copying to tmp table、sorting result、sending data等状态才可以完成

⑧.info列,显示这个sql语句,是判断问题语句的一个重要依据

在主从复制环境中,show processlist或show full processlist对于判断状态很有帮助,例如下面的state列:

1fc7f31c927505449b9bfef0382ff52.png

本文转载自: 掘金

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

MySQL模糊查询再也不用like+%了

发表于 2021-07-28

前言

我们都知道 InnoDB 在模糊查询数据时使用 “%xx” 会导致索引失效,但有时需求就是如此,类似这样的需求还有很多,例如,搜索引擎需要根基用户数据的关键字进行全文查找,电子商务网站需要根据用户的查询条件,在可能需要在商品的详细介绍中进行查找,这些都不是B+树索引能很好完成的工作。

通过数值比较,范围过滤等就可以完成绝大多数我们需要的查询了。但是,如果希望通过关键字的匹配来进行查询过滤,那么就需要基于相似度的查询,而不是原来的精确数值比较,全文索引就是为这种场景设计的。

全文索引(Full-Text Search)是将存储于数据库中的整本书或整篇文章中的任意信息查找出来的技术。它可以根据需要获得全文中有关章、节、段、句、词等信息,也可以进行各种统计和分析。

在早期的 MySQL 中,InnoDB 并不支持全文检索技术,从 MySQL 5.6 开始,InnoDB 开始支持全文检索。

倒排索引

全文检索通常使用倒排索引(inverted index)来实现,倒排索引同 B+Tree 一样,也是一种索引结构。它在辅助表中存储了单词与单词自身在一个或多个文档中所在位置之间的映射,这通常利用关联数组实现,拥有两种表现形式:

  • inverted file index:{单词,单词所在文档的id}
  • full inverted index:{单词,(单词所在文档的id,再具体文档中的位置)}

对于 inverted file index 的关联数组

上图为 inverted file index 关联数组,可以看到其中单词”code”存在于文档1,4中,这样存储再进行全文查询就简单了,可以直接根据 Documents 得到包含查询关键字的文档;而 full inverted index 存储的是对,即(DocumentId,Position),因此其存储的倒排索引如下图,如关键字”code”存在于文档1的第6个单词和文档4的第8个单词。相比之下,full inverted index 占用了更多的空间,但是能更好的定位数据,并扩充一些其他搜索特性。

image.png

全文检索

创建全文索引

1、创建表时创建全文索引语法如下:

1
2
sql复制代码CREATE TABLE table_name ( id INT UNSIGNED AUTO_INCREMENT NOT NULL PRIMARY KEY, author VARCHAR(200), 
title VARCHAR(200), content TEXT(500), FULLTEXT full_index_name (col_name) ) ENGINE=InnoDB;

输入查询语句:

1
2
sql复制代码SELECT table_id, name, space from INFORMATION_SCHEMA.INNODB_TABLES
WHERE name LIKE 'test/%';

image.png

上述六个索引表构成倒排索引,称为辅助索引表。当传入的文档被标记化时,单个词与位置信息和关联的DOC_ID,根据单词的第一个字符的字符集排序权重,在六个索引表中对单词进行完全排序和分区。

2、在已创建的表上创建全文索引语法如下:

1
sql复制代码CREATE FULLTEXT INDEX full_index_name ON table_name(col_name);

使用全文索引

MySQL 数据库支持全文检索的查询,全文索引只能在 InnoDB 或 MyISAM 的表上使用,并且只能用于创建 char,varchar,text 类型的列。

其语法如下:

1
2
3
4
5
6
7
8
sql复制代码MATCH(col1,col2,...) AGAINST(expr[search_modifier])
search_modifier:
{
IN NATURAL LANGUAGE MODE
| IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION
| IN BOOLEAN MODE
| WITH QUERY EXPANSION
}

全文搜索使用 MATCH() AGAINST()语法进行,其中,MATCH() 采用逗号分隔的列表,命名要搜索的列。AGAINST()接收一个要搜索的字符串,以及一个要执行的搜索类型的可选修饰符。全文检索分为三种类型:自然语言搜索、布尔搜索、查询扩展搜索,下面将对各种查询模式进行介绍。

Natural Language

自然语言搜索将搜索字符串解释为自然人类语言中的短语,MATCH()默认采用 Natural Language 模式,其表示查询带有指定关键字的文档。

接下来结合demo来更好的理解Natural Language

1
2
3
4
5
6
sql复制代码SELECT
count(*) AS count
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( 'MySQL' );

image.png

上述语句,查询 title,body 列中包含 ‘MySQL’ 关键字的行数量。上述语句还可以这样写:

1
2
3
4
5
sql复制代码SELECT
count(IF(MATCH ( title, body )
against ( 'MySQL' ), 1, NULL )) AS count
FROM
`fts_articles`;

上述两种语句虽然得到的结果是一样的,但从内部运行来看,第二句SQL的执行速度更快些,因为第一句SQL(基于where索引查询的方式)还需要进行相关性的排序统计,而第二种方式是不需要的。

还可以通过SQL语句查询相关性:

1
2
3
4
5
sql复制代码SELECT
*,
MATCH ( title, body ) against ( 'MySQL' ) AS Relevance
FROM
fts_articles;

image.png

相关性的计算依据以下四个条件:

  • word 是否在文档中出现
  • word 在文档中出现的次数
  • word 在索引列中的数量
  • 多少个文档包含该 word

对于 InnoDB 存储引擎的全文检索,还需要考虑以下的因素:

  • 查询的 word 在 stopword 列中,忽略该字符串的查询
  • 查询的 word 的字符长度是否在区间 [innodb_ft_min_token_size,innodb_ft_max_token_size] 内

如果词在 stopword 中,则不对该词进行查询,如对 ‘for’ 这个词进行查询,结果如下所示:

1
2
3
4
5
sql复制代码SELECT
*,
MATCH ( title, body ) against ( 'for' ) AS Relevance
FROM
fts_articles;

image.png

可以看到,’for’虽然在文档 2,4中出现,但由于其是 stopword ,故其相关性为0

参数 innodb_ft_min_token_size 和 innodb_ft_max_token_size 控制 InnoDB 引擎查询字符的长度,当长度小于 innodb_ft_min_token_size 或者长度大于 innodb_ft_max_token_size 时,会忽略该词的搜索。在 InnoDB 引擎中,参数 innodb_ft_min_token_size 的默认值是3,innodb_ft_max_token_size的默认值是84

Boolean

布尔搜索使用特殊查询语言的规则来解释搜索字符串,该字符串包含要搜索的词,它还可以包含指定要求的运算符,例如匹配行中必须存在或不存在某个词,或者它的权重应高于或低于通常情况。例如,下面的语句要求查询有字符串”Pease”但没有”hot”的文档,其中+和-分别表示单词必须存在,或者一定不存在。

1
sql复制代码select * from fts_test where MATCH(content) AGAINST('+Pease -hot' IN BOOLEAN MODE);

Boolean 全文检索支持的类型包括:

  • +:表示该 word 必须存在
  • -:表示该 word 必须不存在
  • (no operator)表示该 word 是可选的,但是如果出现,其相关性会更高
  • @distance表示查询的多个单词之间的距离是否在 distance 之内,distance 的单位是字节,这种全文检索的查询也称为 Proximity Search,如 MATCH(context) AGAINST('"Pease hot"@30' IN BOOLEAN MODE)语句表示字符串 Pease 和 hot 之间的距离需在30字节内
  • :表示出现该单词时增加相关性

  • <:表示出现该单词时降低相关性
  • ~:表示允许出现该单词,但出现时相关性为负
  • * :表示以该单词开头的单词,如 lik*,表示可以是 lik,like,likes
  • “ :表示短语

下面是一些demo,看看 Boolean Mode 是如何使用的。

demo1:+ -

1
2
3
4
5
6
sql复制代码SELECT
*
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( '+MySQL -YourSQL' IN BOOLEAN MODE );

上述语句,查询的是包含 ‘MySQL’ 但不包含 ‘YourSQL’ 的信息

image.png

demo2: no operator

1
2
3
4
5
6
sql复制代码SELECT
*
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( 'MySQL IBM' IN BOOLEAN MODE );

上述语句,查询的 ‘MySQL IBM’ 没有 ‘+’,’-‘的标识,代表 word 是可选的,如果出现,其相关性会更高

image.png

demo3:@

1
2
3
4
5
6
sql复制代码SELECT
*
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( '"DB2 IBM"@3' IN BOOLEAN MODE );

上述语句,代表 “DB2” ,”IBM”两个词之间的距离在3字节之内

image.png

demo4:> <

1
2
3
4
5
6
sql复制代码SELECT
*
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( '+MySQL +(>database <DBMS)' IN BOOLEAN MODE );

上述语句,查询同时包含 ‘MySQL’,’database’,’DBMS’ 的行信息,但不包含’DBMS’的行的相关性高于包含’DBMS’的行。

image.png

demo5: ~

1
2
3
4
5
6
sql复制代码SELECT
*
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( 'MySQL ~database' IN BOOLEAN MODE );

上述语句,查询包含 ‘MySQL’ 的行,但如果该行同时包含 ‘database’,则降低相关性。

image.png

demo6:*

1
2
3
4
5
6
sql复制代码SELECT
*
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( 'My*' IN BOOLEAN MODE );

上述语句,查询关键字中包含’My’的行信息。

image.png

demo7:”

1
2
3
4
5
6
sql复制代码SELECT
*
FROM
`fts_articles`
WHERE
MATCH ( title, body ) AGAINST ( '"MySQL Security"' IN BOOLEAN MODE );

上述语句,查询包含确切短语 ‘MySQL Security’ 的行信息。

image.png

Query Expansion

查询扩展搜索是对自然语言搜索的修改,这种查询通常在查询的关键词太短,用户需要 implied knowledge(隐含知识)时进行,例如,对于单词 database 的查询,用户可能希望查询的不仅仅是包含 database 的文档,可能还指那些包含 MySQL、Oracle、RDBMS 的单词,而这时可以使用 Query Expansion 模式来开启全文检索的 implied knowledge

通过在查询语句中添加 WITH QUERY EXPANSION / IN NATURAL LANGUAGE MODE WITH QUERY EXPANSION 可以开启 blind query expansion(又称为 automatic relevance feedback),该查询分为两个阶段。

  • 第一阶段:根据搜索的单词进行全文索引查询
  • 第二阶段:根据第一阶段产生的分词再进行一次全文检索的查询

接着来看一个例子,看看 Query Expansion 是如何使用的。

1
2
sql复制代码-- 创建索引
create FULLTEXT INDEX title_body_index on fts_articles(title,body);
1
2
3
4
5
6
7
sql复制代码-- 使用 Natural Language 模式查询
SELECT
*
FROM
`fts_articles`
WHERE
MATCH(title,body) AGAINST('database');

使用 Query Expansion 前查询结果如下:

image.png

1
2
3
4
5
6
7
sql复制代码-- 当使用 Query Expansion 模式查询
SELECT
*
FROM
`fts_articles`
WHERE
MATCH(title,body) AGAINST('database' WITH QUERY expansion);

使用 Query Expansion 后查询结果如下:

image.png

由于 Query Expansion 的全文检索可能带来许多非相关性的查询,因此在使用时,用户可能需要非常谨慎。

删除全文索引

1、直接删除全文索引语法如下:

1
sql复制代码DROP INDEX full_idx_name ON db_name.table_name;

2、使用 alter table 删除全文索引语法如下:

1
sql复制代码ALTER TABLE db_name.table_name DROP INDEX full_idx_name;

小结

本文从理论与实践结合的角度对 fulltext index 做了介绍,如对 MySQL 感兴趣可继续关注 MySQL 专栏。

本文转载自: 掘金

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

SaaS系统从0到1搭建,02多数据源 目标:车队SaaS平

发表于 2021-07-28

目标:车队SaaS平台

车队管理类似的SaaS平台,从0到1,咱继续..

上一篇说到,客观因素,选择的数据库隔离方案,但作为优秀程序猿,必须支持各种临时场景,能够快速相应,不然会被拉取祭天…

那么现在的产品在沟通、在设计,那优秀的程序猿也要同时做好准备,不然怎么保持流弊与神秘~~

好,话说回来,数据隔离方案既然先定业务字段隔离了,那么咱的框架也得支持多数据源吧,万一哪天某个客户他就是要求自己的数据自己单独隔离呢,是吧,这种有可能的吧,比如我们地推人员,想拉个大客户上来,人家有要求,那必须满足。

(我是后半夜Java,在这分享下经验,那些靠copy的搬运作者,不要随意copy到其他平台了,哪天闲的就去投诉)

上菜:多多指教~~

数据库隔离,对后端程序猿来说,那就是多数据源,多数据在网上也能找到一些方案,一些做法,甚至一些踩过的坑,咱汇集过一些,还是有好的代码。

那么咱(粘贴一代码)也要具备下多数据源的功能支持,

上码:

首先定义下多数据源配置

1
2
3
4
5
6
7
8
9
10
11
12
13
yaml复制代码##多数据源的配置
dynamic:
datasource:
slave1:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:3306/yydsCarBase
username: root
password: 123456
slave2:
driver-class-name: org.postgresql.Driver
url: jdbc:postgresql://localhost:3306/yydsCar1
username: root
password: 123456

优美的代码,总有那么几个注解

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码import java.lang.annotation.*;

/**
* 多数据源注解
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
public @interface DataSource {
String value() default "";
}

配置下多数据源

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
typescript复制代码
/**
* 配置多数据源
*/
@Configuration
@EnableConfigurationProperties(DynamicDataSourceProperties.class)
public class DynamicDataSourceConfig {
@Autowired
private DynamicDataSourceProperties properties;
@Bean
@ConfigurationProperties(prefix = "spring.datasource.druid")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
public DynamicDataSource dynamicDataSource(DataSourceProperties dataSourceProperties) {
DynamicDataSource dynamicDataSource = new DynamicDataSource();
dynamicDataSource.setTargetDataSources(getDynamicDataSource());
//配置默认的数据源,无论有几个数据源,都要配置一个默认的, 不然跑不起来
DruidDataSource defaultDataSource = DynamicDataSourceFactory.buildDruidDataSource(dataSourceProperties);
dynamicDataSource.setDefaultTargetDataSource(defaultDataSource);
return dynamicDataSource;
}
/**
* 多数据源的其他数据源配置汇总
* 基本都是map<key,value> <数据源名可以按租户名配置,数据库链接信息>
* @return
*/
private Map<Object, Object> getDynamicDataSource(){
Map<String, DataSourceProperties> dataSourcePropertiesMap = properties.getDatasource();
Map<Object, Object> targetDataSources = new HashMap<>(dataSourcePropertiesMap.size());
dataSourcePropertiesMap.forEach((k, v) -> {
DruidDataSource druidDataSource = DynamicDataSourceFactory.buildDruidDataSource(v);
targetDataSources.put(k, druidDataSource);
});
return targetDataSources;
}
}

/**
* 多数据源
*/
public class DynamicDataSource extends AbstractRoutingDataSource {
@Override
protected Object determineCurrentLookupKey() {
return DynamicContextHolder.peek();
}

}

数据库链接配置

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
scss复制代码public class DynamicDataSourceFactory {

/**
* 创建数据库链接的基本配置,这里如果是不同类型的数据库,需要留意下,比如Oracle、sqlservice啥的
* @param properties
* @return
*/
public static DruidDataSource buildDruidDataSource(DataSourceProperties properties) {
DruidDataSource druid = new DruidDataSource();
try {
druid.setDriverClassName(properties.getDriverClassName());
druid.setUrl(properties.getUrl());
druid.setUsername(properties.getUsername());
druid.setPassword(properties.getPassword());
druid.setInitialSize(properties.getInitialSize());
druid.setMaxActive(properties.getMaxActive());
druid.setMinIdle(properties.getMinIdle());
druid.setMaxWait(properties.getMaxWait());
druid.setTimeBetweenEvictionRunsMillis(properties.getTimeBetweenEvictionRunsMillis());
druid.setMinEvictableIdleTimeMillis(properties.getMinEvictableIdleTimeMillis());
druid.setMaxEvictableIdleTimeMillis(properties.getMaxEvictableIdleTimeMillis());
druid.setValidationQuery(properties.getValidationQuery());
druid.setValidationQueryTimeout(properties.getValidationQueryTimeout());
druid.setTestOnBorrow(properties.isTestOnBorrow());
druid.setTestOnReturn(properties.isTestOnReturn());
druid.setPoolPreparedStatements(properties.isPoolPreparedStatements());
druid.setMaxOpenPreparedStatements(properties.getMaxOpenPreparedStatements());
druid.setSharePreparedStatements(properties.isSharePreparedStatements());
druidDataSource.setFilters(properties.getFilters());
druidDataSource.init();
} catch (SQLException e) {
//其实这里还是有点问题,如果捕获异常了,还是返回数据库链接,外头还是链接不上的,Mark下
e.printStackTrace();
}
return druidDataSource;
}
}

多数据源的支持

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
typescript复制代码public class DynamicContextHolder {
private static final ThreadLocal<Deque<String>> CONTEXT_HOLDER = new ThreadLocal() {
@Override
protected Object initialValue() {
return new ArrayDeque();
}
};
/**
* 获得当前线程数据源
* @return 数据源名称
*/
public static String peek() {
return CONTEXT_HOLDER.get().peek();
}
/**
* 设置当前线程数据源
* @param dataSource 数据源名称
*/
public static void push(String dataSource) {
CONTEXT_HOLDER.get().push(dataSource);
}
/**
* 清空当前线程数据源
*/
public static void poll() {
Deque<String> deque = CONTEXT_HOLDER.get();
deque.poll();
if (deque.isEmpty()) {
CONTEXT_HOLDER.remove();
}
}
}

最后,统一处理AOP

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
less复制代码/**
* 多数据源,切面处理类
*/
@Aspect
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class DataSourceAspect {

@Pointcut("@annotation(io.renren.commons.dynamic.datasource.annotation.DataSource) " +
"|| @within(io.renren.commons.dynamic.datasource.annotation.DataSource)")
public void dataSourcePointCut() {
}

@Around("dataSourcePointCut()")
public Object around(ProceedingJoinPoint point) throws Throwable {
MethodSignature signature = (MethodSignature) point.getSignature();
Class targetClass = point.getTarget().getClass();
Method method = signature.getMethod();
DataSource targetDataSource = (DataSource)targetClass.getAnnotation(DataSource.class);
DataSource methodDataSource = method.getAnnotation(DataSource.class);
if(targetDataSource != null || methodDataSource != null){
String value;
if(methodDataSource != null){
value = methodDataSource.value();
}else {
value = targetDataSource.value();
}
DynamicContextHolder.push(value);
}
try {
return point.proceed();
} finally {
DynamicContextHolder.poll();
}
}
}

总结

多数据源的处理,网上很多方法或开源的,都可以直接用,只是从概念上或者思想上,需要去了解下他是怎么运作的。
其他其实不去深入到修改底层源码的需要,基本能用就可以了。

SaaS系统从0到1搭建,下篇继续….

本文转载自: 掘金

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

JavaWEB基础知识大览

发表于 2021-07-28

前言

Java Web是指用Java语言来解决相关Web领域的技术总和,一个Web应用程序包括Web客户端和Web服务器两个部分,即基于B/S(浏览器/服务器)架构的应用程序。

一、两端

在这里插入图片描述

1.1 Web客户端

Web客户端通常是指用户机上的浏览器,如微软的IE浏览器或火狐浏览器等。客户端不需要开发任何用户界面,而统一采用浏览器即可。

1.2 Web服务器

Web服务器是一台或多台可运行Web应用程序的计算机,通常我们在浏览器中输入的网站地址,即Web服务器的地址。当用户在浏览器的地址栏中输入网站地址并按回车键后,请求即被发送到 Web服务器。服务器接收到请求后,会返回给用户带有请求资源的响应消息。Java在服务器端的应用非常丰富,如Servlet、JSP和第三方框架等。

二、两站

2.1 静态网站

早期的Web应用主要是静态页面的浏览,即静态网站。
这些网站使用HTML语言来编写,放在Web服务器上。
用户使用浏览器通过HTTP协议请求服务器上的Web页面,Web服务器处理接收到的用户请求后发送给客户端浏览器显示给用户。工作原理如图:
在这里插入图片描述

2.2 动态网站

用户所访问的资源已不局限于服务器中保存的静态网页。更多的内容需要根据用户的请求动态生成页面信息,即动态网站。
这些网站通常使用HTML语言和动态脚本语言(如JSP、ASP或PHP等)编写,并将编写后的程序部署到Web服务器中。
由Web服务器处理动态脚本代码并将其转换为浏览器可以解析的HTML代码,最后返回客户端浏览器显示给用户,其工作流程如图:
在这里插入图片描述

三、两结构

3.1 C/S结构

客户端则需要安装专用的客户端软件。如图所示:
在这里插入图片描述

3.2 B/S结构

在B/S结构中客户端不需要开发任何用户界面,而统一采用IE或火狐等浏览器。通过Web浏览器向Web服务器发送请求,由Web服务器处理并将处理结果逐级传回客户端,如图所示。
在这里插入图片描述

内置对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
markdown复制代码1.Request对象
Request对象是javax.servlet.http.HttpServletRequest类的实例。代表请求对象,主要用于接受客户端通过HTTP协议连接传输到服务器端的数据。比如表单中的数据、网页地址后带的参数等。
2.Response对象
Response对象是javax.servlet.http.HttpServletResponse类的实例。代表响应对象,主要用于向客户端发送数据。
3.Out对象
Out对象是javax.servlet.jsp.JspWriter类的实例。主要用于向客户端浏览器输出数据。
4.session对象
Session 对象是javax.servlet.http.HttpSession类的实例。主要用来保持在服务器与一个客户端之间需要保留的数据,比如在会话期间保持用户的登录信息等,会话状态维持是Web应用开发者必须面对的问题。当客户端关闭网站的所有网页或关闭浏览器时,session对象中保存的数据会自动清除。由于Htp协议是一个无状态协议,不保留会话间的数据,因此通过session对象扩展了htp的功能。比如用户登录一个网站之后,登录信息会暂时保存在session对象中,打开不同的页面时,登录信息是可以共享的,一旦用户关闭浏览器或退出登录,就会清除session对象中保存的登录信息。
5.Application对象
Application对象是javax.servlet.ServletContext类的实例。主要用于保存用户信息,代码片段的运行环境;它是一个共享的内置对象,即一个容器中的多个用户共享一个application对象,故其保存的信息被所有用户所共享。
6.PageContext对象
PageContext对象是javax.servlet.jsp.PageContext类的实例。用来管理网页属性,为JSP页面包装页面的上下文,管理对属于JSP中特殊可见部分中已命名对象的访问,它的创建和初始化都是由JSP容器来完成的。
7.Config对象
Config对象是javax.servlet.ServletConfig类的实例。是代码片段配置对象,表示Servlet的配置。
8.Page(相当于this)对象
Page对象是javax.servlet.jsp.HttpJspPage类的实例。用来处理JSP网页,它指的是JSP页面对象本身,或者说代表编译后的servlet对象,只有在JSP页面范围之内才是合法的。
9.Exception对象
Exception对象是java.lang.Throwable类的实例。处理JSP文件执行时发生的错误和异常只有在JSP页面的page指令中指定isErrorPage=“true”后,才可以在本页面使用exception对象。

四、JavaBean

4.1 JavaBean的产生背景

在JSP网页开发的初级阶段并没有框架与逻辑分层概念,需要将Java代码嵌入到网页中处理JSP页面中的一些业务逻辑,如字符串处理和数据库操作等,其开发流程如图所示。
在这里插入图片描述

4.2 JavaBean的作用

如果使HTML与Java代码相分离,将Java代码单独封装成为一个处理某种业务逻辑的类。然后在JSP页面中调用此类,则可以降低HTML与Java代码之间的耦合度,并且简化JSP页面,提高Java程序代码的重用性及灵活性。这种与HTML代码相分离,而使用Java代码封装的类就是一个JavaBean组件。
在Java Web开发可以使用该组件来完成业务逻辑的处理,应用JavaBean与JSP组合的开发模式如图所示。
在这里插入图片描述

4.3 JavaBean的应用

JavaBean是用Java语言所写成的可重用组件,其应用十分广泛,可以应用于系统的很多层中,如PO、VO、DTO和POJO等。

五、Servlet

用户通过单击某个链接或者直接在浏览器的地址栏中输入URL来访问Servlet,Web服务器接收到请求后,并不是将请求直接交给Servlet容器。Servlet容器实例化Servlet,调用Servlet的一个特定方法( service() ),并产生一个响应。这个响应有Servlet容器返回给Web服务器,Web服务器包装这个响应,以HTTP响应的形式发送给Web浏览器。整个过程如图:
在这里插入图片描述

六、如何让服务器知道你来过?

6.1 Cookie技术

Cookie的作用:通俗地说就是当一个用户通过HTTP协议访问一个服务器的时候,这个服务器会将一些Key/Value键值对返回给客户端浏览器,并给这些数据加上一些限制条件,在条件符合时这个用户下次访问这个服务器的时候,数据又被完整地带回给服务器。
这个作用就像你去超市购物时,第一次给你办张购物卡,这个购物卡里存放了一些你的个人信息,下次你再来这个连锁超市时,超市会识别你的购物卡,下次直接购物就好了。当初W3C在设计Cookie时实际上考虑的是为了记录用户在一段时间内访问Web应用的行为路径。由于HTTP协议是一种无状态协议,当用户的一次访问请求结束后,后端服务器就无法知道下一次来访问的还是不是上次访问的用户,在设计应用程序时,我们很容易想到两次访问是同一人访问与不同的两个人访问对程序设计和性能来说有很大的不同。例如,在一个很短的时间内,如果与用户相关的数据被频繁访问,可以针对这个数据做缓存,这样可以大大提高数据的访问性能。Cookie的作用正是在此,由于是同一个客户端发出的请求,每次发出的请求都会带有第一次访问时服务端设置的信息,这样服务端就可以根据Cookie值来划分访问的用户了。
在这里插入图片描述

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
java复制代码@WebServlet("/CookieTest")
public class CookieTest extends HttpServlet {
private static final long serialVersionUID = 1L;
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
Cookie[] cookies = request.getCookies();

PrintWriter out = response.getWriter();

if(cookies!=null){
out.println("上次访问的时间是:");
for (Cookie cookie : cookies) {
if("lastTime".equals(cookie.getName())){
long lastTime = Long.parseLong(cookie.getValue());
Date date = new Date(lastTime);
out.println(date.toLocaleString());
}
}
}else{
out.println("你是第一来");
}

Cookie cookie = new Cookie("lastTime", String.valueOf(System.currentTimeMillis()));
//给cookie设置一些信息
//cookie.setMaxAge(500); //有效期
//cookie.setPath(uri);
//服务器端给客户端一个Cookie
response.addCookie(cookie);
}

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}

解码与编码(与Cookie无关,只是解决乱码的一种方式)

1
2
3
4
java复制代码//传中文,避免乱码。可以进行编码
Cookie cookie = new Cookie("lastTime",URLEncoder.encode("尚", "utf-8"));
//取出Cookie值,需要解码
URLDecoder.decode(cookie.getValue(),"utf-8");

6.2 Session技术

为什么需要Session?

前面已经介绍了Cookie可以让服务端程序跟踪每个客户端的访问,但是每次客户端的访问都必须传回这些Cookie,如果Cookie很多,这无形地增加了客户端与服务端的数据传输量,而Session的出现正是为了解决这个问题。
同一个客户端每次和服务端交互时,不需要每次都传回所有的Cookie值,而是只要传回一个ID,这个ID是客户端第一次访问服务器的时候生成的,而且每个客户端是唯一的。这样每个客户端就有了一个唯一的ID,客户端只要传回这个ID就行了,这个ID通常是NANE为JSESIONID的一个Cookie。
在这里插入图片描述

一个浏览器去服务器租房子,服务器记录一下浏览器的行为和数据,然后给了浏览器一把房间的钥匙
然后,每次浏览器可以使用自己的钥匙去打开自己的房间,使用房间的所有东西。(当然,你不能去开别人的房间,何况也打不开)


七、上下文

7.1 SeveletContext或者ApplicationContext的由来

浏览器想锻炼身体,愉悦心情。服务器心想我不能给你们每一个人的房间增加一套体育设备吧,那我的经济压力多大。服务器想了想,决定建设一个公开场所,体育馆,所有浏览器都可以使用这些共享资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码@WebServlet("/SessionTest1")
public class SessionTest extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.write("Session的ID为:");
// 获取 Session
HttpSession session = request.getSession();
out.write(session.getId());
session.setAttribute("name", "shang");
//设置当前会话多久结束,单位秒。如果设置的值为零或负数,则表示会话将永远不会超时。常用于设置当前会话时间。
// session.setMaxInactiveInterval(1);
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
java复制代码@WebServlet("/SessionTest2")
public class SessionTest2 extends HttpServlet {
private static final long serialVersionUID = 1L;

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");

HttpSession session = request.getSession();
//移除session的数据
session.removeAttribute("name");
//手动注销当前会话
session.invalidate();
}
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
doGet(request, response);
}
}

7.2 Servlet上下文

运行在Java虚拟机中的每一个Web应用程序都有一个与之相关的Servlet上下文。Java Servlet API提供了一个ServletContext接口用来表示上下文。在这个接口中定义了一组方法,Servlet可以使用这些方法与它的Servlet容器进行通信,例如,得到文件的MIME类型,转发请求,或者向日志文件中写入日志消息。
ServletContext对象是Web服务器中的一个已知路径的根。 比如,Servlet上下文被定位于http://localhost:8080/ch02。以/ch02请求路径(称为上下文路径)开始的所有请求被发送到与此ServletContext关联的Web应用程序。再比如,我们平常使用的http://localhost:8080/。以/请求路径(称为上下文路径)开始的所有请求被发送到与此ServletContext关联的Web应用程序。

在这里插入图片描述
ServletContext: 这个是来自于servlet规范里的概念,它是servlet用来与容器间进行交互的接口的组合,也就是说,这个接口定义了一系列的方法,servlet通过这些方法可以很方便地与自己所在的容器进行一些交互。在一个应用中(一个JVM),servlet容器可以有多个,而所有的servlet容器共享一个ServletContext。
在这里插入图片描述

八、两个时代

8.1 Model1时代

最初的JSP开发模式为Model 1模式:JSP+JavaBean
在这里插入图片描述

8.2 Model2时代

慢慢演变成了Model 2模式:JSP+Servlet+JavaBean

模型2符合MVC架构模式,MVC即模型-视图-控制器(Model-View-Controller)。

  • 模型代表应用程序的数据以及用于访问控制和修改这些数据的业务规则。当模型发生改变时,它会通知视图,并为视图提供查询模型相关状态的能力。同时,它也为控制器提供访问封装在模型内部的应用程序功能的能力。
  • 视图用来组织模型的内容。它从模型那里获得数据并指定这些数据如何表现。当模型变化时,视图负责维护数据表现的一致性。视图同时将用户的请求通知控制器。
  • 控制器定义了应用程序的行为。它负责对来自视图的用户请求进行解释,并把这些请求映射成相应的行为,这些行为由模型负责实现。在独立运行的GUI客户端,用户的请求可能是一些鼠标单击或是菜单选择操作。在一个Web应用程序中,它们的表现形式可能是一些来自客户端的GET或POST的HTTP请求。模型所实现的行为包括处理业务和修改模型的状态。根据用户请求和模型行为的结果,控制器选择一个视图作为对用户请求的响应。如图所示:
    在这里插入图片描述
    在这里插入图片描述

九、文件的上传下载

9.1 文件的上传

  1. 导入jar包commons-io.jar 与commons-fileupload.jar
  2. 表单一定要标记enctype="multipart/form-data"
1
2
3
4
5
6
7
8
9
html复制代码  <%--
${pageContext.request.contextPath}:保证我发布在项目在服务器上也能被访问
--%>

<form action="${pageContext.request.contextPath}/upload.do" method="post" enctype="multipart/form-data">
上传用户:<input type="text" name="username"><br/>
<input type="file" name="file1"><br/>
<input type="submit" value="提交"> | <input type="reset" value="重置">
</form>
  1. ServletFileUpload负责处理上传的文件数据,并将表单中每个输入项封装成一个FileItem对象, 在使用ServletFileUpload对象解析请求时需要DiskFileItemFactory对象。 所以,我们需要在进行解析工作前构造好DiskFileItemFactory()对象,通过ServletFileUpload对象的构造方法或setFileItemFactory()方法设置ServletFileUpload对象的fileItemFactory属性。
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
java复制代码package com.shang.servlet;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.FileUploadException;
import org.apache.commons.fileupload.ProgressListener;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.List;
import java.util.UUID;

public class FileServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//判断上传的表单是普通表单还是带文件的表单
if(!ServletFileUpload.isMultipartContent(request)){
return;
}
//创建上传文件的保存路径,建议在WEB-INF路径下,安全,用户无法直接访问上传的文件
String uploadPath = this.getServletContext().getRealPath("/WEB-INF/upload");
File uploadFile = new File(uploadPath);
if(!uploadFile.exists()){
uploadFile.mkdir();
}

//缓存,临时文件
//临时路径,假如文件超出了预期的大小,我们就把他放到一个临时文件中,过几天自动删除,或者提醒用户转存为永久
String tmpPath = this.getServletContext().getRealPath("/WEB-INF/tmp");
File tmpFile = new File(tmpPath);
if(!tmpFile.exists()){
tmpFile.mkdir();
}

//处理上传的文件,一般都需要通过流来获取,我们可以使用request.getInputStream(),原生态的文件上传流获取,十分麻烦。但是我们都建议使用Apache的文件上传组件来实现,common-fileupload,它需要依赖于commons-io组件:
//1. 创建DiskFileItemFactory对象,处理文件上传路径或者大小限制的
DiskFileItemFactory factory = getDiskFileItemFactory(tmpFile);
//2.获取ServletFileUpload
ServletFileUpload upload = getServletFileUpload(factory);
//3.处理上传的文件
String msg = null;
try {
msg = uploadParseRequest(upload, request, response, uploadPath);
} catch (FileUploadException e) {
e.printStackTrace();
}
//servlet请求转发消息
request.setAttribute("msg", msg);
request.getRequestDispatcher("info.jsp").forward(request, response);

}

protected void doGet( HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
}

public static DiskFileItemFactory getDiskFileItemFactory(File file){
//通过这个工厂设置一个缓冲区,当上传的文件大于这个缓冲区的时候,将他放到临时文件中
DiskFileItemFactory factory = new DiskFileItemFactory();
factory.setSizeThreshold(1024 * 1024);//缓冲区大小为1M
factory.setRepository(file);//临时目录的保存目录,需要一个File
return factory;
}

public static ServletFileUpload getServletFileUpload(DiskFileItemFactory factory){
ServletFileUpload upload = new ServletFileUpload(factory);
//监听文件上传进度
upload.setProgressListener(new ProgressListener() {
@Override
//pBytesRead:已经读取到的文件大小
//pContentLength:文件大小
public void update(long pBytesRead, long pContentLength, int pItems) {
System.out.println("总大小:"+pContentLength+"已上传:"+pBytesRead);
}
});
//处理乱码问题
upload.setHeaderEncoding("UTF-8");
//设置单个文件的最大值
upload.setFileSizeMax(1024 * 1024 * 10);
//设置总共能够上传文件的大小
//1024 = 1kb * 1024 =1M * 10 =10M
upload.setSizeMax(1024 * 1024 * 10);
return upload;
}

public static String uploadParseRequest(ServletFileUpload upload,HttpServletRequest request,HttpServletResponse response,String uploadPath) throws FileUploadException, IOException {
String msg = "";

//把前端请求解析,封装成一个FileItem对象
List<FileItem> fileItems = upload.parseRequest(request);
for (FileItem fileItem : fileItems) {
if(fileItem.isFormField()){ //判断上传文件是普通表单还是带文件的表单
//getFiledName指的是前端表单控件的name
String name = fileItem.getFieldName();
String value = fileItem.getString("UTF-8"); //处理乱码
System.out.println(name+":"+value);
}else {//判断它是上传文件
//========处理文件======
String uploadFileName = fileItem.getName();
System.out.println("上传的文件名:"+uploadFileName);

if(uploadFileName.trim().equals("")||uploadFileName==null){
continue;
}
//获取上传的文件名 /images/boy/cool.jpg
String fileName = uploadFileName.substring(uploadFileName.lastIndexOf("/") + 1);
//获取文件的后缀
String fileExtName = uploadFileName.substring(uploadFileName.lastIndexOf(".") + 1);
/*
* 如果文件后缀名 fileExtName 不是我们所需要的,
* 就直接return,不处理,告诉用户文件类型不对
* */
System.out.println("文件信息 [件名:"+fileName+"----文件类型"+fileExtName+"]");

//可以使用UUID(唯一识别的通用码),保证文件的唯一
//,UUID.randomUUID(),随机生成一个唯一识别的通用码
String uuidPath = UUID.randomUUID().toString();

//===========================处理文件完毕========
//存到哪? uploadPath
//文件真实存在的路径 realPath
String realPath = uploadPath +"/"+uuidPath;
//给每个文件创建一个对应的文件夹
File realPathFile = new File(realPath);
if(!realPathFile.exists()){
realPathFile.mkdir();
}

//存放地址完毕

//=======文件传输=====
//获取文件的上传流
InputStream inputStream = fileItem.getInputStream();
//创建文件输出流
//realPath 真实的文件夹
//差一个文件;加上输出的名字+"/"+uuidFileName
FileOutputStream fos = new FileOutputStream(realPath + "/" + fileName);

//判断是否读取完毕
int len = 0;
//创建一个缓冲区
byte[] buffer = new byte[1024*1024];
//如果不等于-1说明还存在数据
while((len = buffer.length)!= -1){
fos.write(buffer, 0, len);
}

//关闭流
fos.close();
inputStream.close();
msg = "文件上传成功!";
fileItem.delete();// 上传成功,清除临时文件
}
}
return msg;
}
}

9.2 文件的下载

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
java复制代码public class HelloServlet extends HttpServlet {
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

}

protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//1.获取下载文件的路径
//String path = this.getServletContext().getRealPath("/1.jpg");
String path = "G:\\MyIDEProject\\JavaWeb\\UploadFile\\resources\\1.jpg";
System.out.println("要下载的文件路径"+path);
//2.下载的文件名
String filename = path.substring(path.lastIndexOf("\\") + 1);
//3.设置让浏览器支持我们要下载的东西
response.setHeader("Content-Disposition", "attachment;filename="+filename);
//4.获取下载文件的输入流
File file = new File(path);
FileInputStream fis = new FileInputStream(file);
//5.创建缓冲区
byte[] buffer = new byte[1024];
int len = 0;
//6.获取OutputStream对象
ServletOutputStream fos = response.getOutputStream();
//7.将FileOutputStream流写入到buffer缓冲区
while((len = buffer.length)!=-1){
//8.使用OutputStream将缓冲区中的数据输入到客户端
fos.write(buffer, 0, len);
}
//9. 关闭流
fos.close();
fis.close();
}
}

本文转载自: 掘金

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

Elasticsearch-核心篇(9)-集群配置 一、集群

发表于 2021-07-28

一、集群管理

1.1 单机&集群

  1. 单台Elasticsearch服务器提供服务,往往都有最大的负载能力,超过这个阈值,服务器性能就会大大降低甚至不可用,所以生产环境中,一般都是运行在指定服务器集群中
  2. 除了负载能力,单点服务器也存在其他问题
    • 单台机器存储容量有限
    • 单服务器容易出现单点故障,无法实现高可用
    • 单服务的并发处理能力有限
  3. 配置服务器集群时,集群中节点数量没有限制,大于等于2个节点就可以看做是集群了
  4. 一般出于高性能及高可用方面来考虑集群中节点数量都是3个以上。

1.2 集群Cluster

  1. 一个集群就是由一个或多个服务器节点组织在一起,共同持有整个的数据,并一起提供索引和搜索功能
  2. 一个Elasticsearch集群有一个唯一的名字标识,这个名字默认就是”elasticsearch”
  3. 这个名字很重要,一个节点只能通过指定某个集群的名字来加入这个集群

1.3 节点Node

  1. 集群中包含很多服务器,一个节点就是其中的一个服务器
  2. 作为集群的一部分,它存储数据,参与集群的索引和搜索功能。
  3. 一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。这个名字对于管理工作来说比较重要,因为在这个管理过程中,会去确定网络中的哪些服务器对应于Elasticsearch集群中的哪些节点
  4. 一个节点可以通过配置集群名称的方式来加入一个指定的集群,默认情况下,每个节点都会被安排加入到一个叫做“elasticsearch”的集群中,这意味着,如果你在你的网络中启动了若干个节点,并假定它们能够相互发现彼此,它们将会自动地形成并加入到一个叫做“elasticsearch”的集群中
  5. 在一个集群里,可以拥有任意多个节点,如果当前你的网络中没有运行任何Elasticsearch节点,这时启动一个节点,会默认创建并加入一个叫做“elasticsearch”的集群
  6. 集群健康状态
    • green:所有主分配和副本分片都正常运行
    • yellow:主分片正常,但存在副本分片不正常
    • red:存在主分配没有正常运行

二、Windows集群

  1. 复制份ES安装文件,并按照端口号分别按照如下命名,具体按照自己需求命名即可

image.png

  1. 节点说明,该集群测试一个主节点带两个数据节点
    • 9200:master节点
    • 9201:数据节点
    • 9202:数据节点
  2. 集群操作是建立在单节点操作上,下面的集群做增量配置,前置如jdk等配置参考单节点安装,先要保证单节点启动运行正常

2.1 主节点

  1. 修改config/elasticsearch.yml文件,增加以下配置
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
yaml复制代码# 集群节点1的配置信息
# 集群名称,节点之间要保持一致
cluster.name: elasticsearch
# 节点名称,集群内要唯一
node.name: node-9200
# 集群master
node.master: true
# 数据节点
node.data: true

# ip地址
network.host: localhost
# http端口
http.port: 9200
# tcp监听端口
transport.tcp.port: 9300

# 新节点用于加入集群的主节点列表,注意端口号是tcp端口,多个master节点时指定
#discovery.seed_hosts: ["localhost:9300", "localhost:9301","localhost:9302"]
#discovery.zen.fd.ping_timeout: 1m
#discovery.zen.fd.ping_retries: 5

# 集群内的可以被选为主节点的节点名称列表,需要是node.master
#cluster.initial_master_nodes: ["node-9200", "node-9201","node-9202"]

#跨域配置
#action.destructive_requires_name: true
http.cors.enabled: true
http.cors.allow-origin: "*"
  1. 启动主节点,观察是否正常启动

2.2 数据节点

  1. 两个数据节点配置相同,只需要按照各自端口改一下http和transport端口
  2. 修改config/elasticsearch.yml文件,增加以下配置
    • 注意:node.master: false,只是作为数据节点,如果是主节点需要开启
    • 注意:discovery.seed_hosts需要配置主节点列表
    • elasticsearch-9201配置
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
yaml复制代码# 集群节点2的配置信息
# 集群名称,节点之间要保持一致
cluster.name: elasticsearch
# 节点名称,集群内要唯一
node.name: node-9201
# 集群master
node.master: false
# 数据节点
node.data: true

# ip地址
network.host: localhost
# http端口
http.port: 9201
# tcp监听端口
transport.tcp.port: 9301

# 新节点用于加入集群的主节点列表
discovery.seed_hosts: ["localhost:9300"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5

# 集群内的可以被选为主节点的节点名称列表,需要是node.master
#cluster.initial_master_nodes: ["node-9200", "node-9201","node-9202"]

#跨域配置
#action.destructive_requires_name: true
http.cors.enabled: true
http.cors.allow-origin: "*"
  • elasticsearch-9202配置
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
yaml复制代码# 集群节点3的配置信息
# 集群名称,节点之间要保持一致
cluster.name: elasticsearch
# 节点名称,集群内要唯一
node.name: node-9202
# 集群master
node.master: true
# 数据节点
node.data: true

# ip地址
network.host: localhost
# http端口
http.port: 9202
# tcp监听端口
transport.tcp.port: 9302

# 新节点用于加入集群的主节点列表,注意端口号是tcp端口,多个master节点时指定
discovery.seed_hosts: ["localhost:9300"]
discovery.zen.fd.ping_timeout: 1m
discovery.zen.fd.ping_retries: 5

# 集群内的可以被选为主节点的节点名称列表,需要是node.master
#cluster.initial_master_nodes: ["node-9200", "node-9201","node-9202"]

#跨域配置
#action.destructive_requires_name: true
http.cors.enabled: true
http.cors.allow-origin: "*"
  1. 分别启动来两个数据节点,然后观察3个节点是否都正常

2.3 集群测试

  1. 获取集群健康信息
    • GET _cluster/health
    • number_of_nodes:集群节点此时是3
    • number_of_data_nodes:数据节点此时也是3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
json复制代码{
"cluster_name" : "elasticsearch",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 3,
"number_of_data_nodes" : 3,
"active_primary_shards" : 7,
"active_shards" : 14,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}
  1. 向主节点写入数据
1
2
3
4
5
json复制代码PUT cluster/_doc/1
{
"name": "集群测试",
"message": "主节点写入"
}
  1. 两个从节点查询数据,可以看到数据成功同步
    • http://localhost:9201/cluster/_search
    • http://localhost:9202/cluster/_search
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
json复制代码{
"took": 25,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": {
"value": 1,
"relation": "eq"
},
"max_score": 1,
"hits": [
{
"_index": "cluster",
"_type": "_doc",
"_id": "1",
"_score": 1,
"_source": {
"name": "集群测试",
"message": "主节点写入"
}
}
]
}
}

三、Linux集群

  1. 准备三台Linux服务器
  2. 准备一份es服务文件,修改config/elasticsearch.yml文件,配置以下内容
    • 注意前置jdk等环境参考核心模块单机安装先配置完成
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
yaml复制代码# 集群名称
cluster.name: cluster-es
# 节点名称,每个节点的名称不能重复
node.name: node-1
# ip地址,每个节点的地址不能重复
network.host: linux1
# 主节点
node.master: true
node.data: true
http.port: 9200
# 跨域配置
http.cors.allow-origin: "*"
http.cors.enabled: true
http.max_content_length: 200mb
# es7.x 之后新增的配置,初始化一个新的集群时需要此配置来选举master
cluster.initial_master_nodes: ["node-1"]
# es7.x 之后新增的配置,节点发现
discovery.seed_hosts: ["linux1:9300","linux2:9300","linux3:9300"]
gateway.recover_after_nodes: 2
network.tcp.keep_alive: true
network.tcp.no_delay: true
transport.tcp.compress: true
# 集群内同时启动的数据任务个数,默认是2个
cluster.routing.allocation.cluster_concurrent_rebalance: 16
# 添加或删除节点及负载均衡时并发恢复的线程个数,默认4个
cluster.routing.allocation.node_concurrent_recoveries: 16
# 初始化数据恢复时,并发恢复线程的个数,默认4个
cluster.routing.allocation.node_initial_primaries_recoveries: 16
  1. 将配置的es服务,分发到剩下两个服务器,不需要改动任何配置
  2. 分别启动三台服务器,然后按照windows集群下的集群测试即可

本文转载自: 掘金

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

Elasticsearch-核心篇(8)-中文分词器(IK)

发表于 2021-07-28

一、系统分词器

  1. 可以使用GET发送_analyze命令,指定分析器和需要分析的文本内容
  2. 标准分析器,按照最小粒度
1
2
3
4
5
json复制代码GET _analyze
{
"analyzer": "standard",
"text": ["中国人ABC"]
}
  • 分析结果
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
json复制代码{
"tokens" : [
{
"token" : "中",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "国",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "人",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "abc",
"start_offset" : 3,
"end_offset" : 6,
"type" : "<ALPHANUM>",
"position" : 3
}
]
}
  1. 作为关键词,关键词不会拆分
1
2
3
4
5
json复制代码GET _analyze
{
"analyzer": "keyword",
"text": ["中国人ABC"]
}
  • 分析结果
1
2
3
4
5
6
7
8
9
10
11
json复制代码{
"tokens" : [
{
"token" : "中国人ABC",
"start_offset" : 0,
"end_offset" : 6,
"type" : "word",
"position" : 0
}
]
}

二、IK分词器

2.1 IK分词器说明

  1. IK分词器提供两个分词算法:ik_smart、ik_max_word
    • ik_smart:最少拆分
    • ik_max_word:最为细粒度切分

2.2 IK分词器安装

  1. 下载地址:github.com/medcl/elast…
  2. 注意事项:版本一定要和ES版本一致
  3. 解压ik分词器到es/plugins中,文件夹名称用ik

image.png

  1. 重启Elasticsearch,安装完成,在界面启动时将会有插件加载信息

image.png

2.3 IK分词器使用

  1. 可以通过_analyze来测试分词器的使用
1
2
3
4
5
json复制代码GET _analyze 
{
"analyzer": "分词器类型",
"text": "我是中国人码坐标"
}

2.3.1 ik_smart

  • 最少拆分
1
2
3
4
5
json复制代码GET _analyze 
{
"analyzer": "ik_max_word",
"text": "我是中国人码坐标"
}
  • 拆分结果
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
json复制代码{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "码",
"start_offset" : 5,
"end_offset" : 6,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "坐标",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 4
}
]
}

2.3.2 ik_max_word

  • 最为细粒度拆分
1
2
3
4
5
json复制代码GET _analyze 
{
"analyzer": "ik_max_word",
"text": "我是中国人码坐标"
}
  • 拆分结果
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
json复制代码{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "中国",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "国人",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "码",
"start_offset" : 5,
"end_offset" : 6,
"type" : "CN_CHAR",
"position" : 5
},
{
"token" : "坐标",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 6
}
]
}

2.4 自定义数据词典

  1. 在elasticsearch/plugins/ik/config下新建.dic文件,例如此处为codecoord.dic
  2. 编辑codecoord.dic文件,在其中加入词典,加入的信息在分词器中将会作为一个词语使用,不会进行拆分

image.png

  1. 编辑ik/config/IKAnalyzer.cfg.xml文件,在ext_dict中加入刚刚创建的codecoord.dic词典,多个使用逗号分开

image.png

  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
json复制代码{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "码坐标",
"start_offset" : 5,
"end_offset" : 8,
"type" : "CN_CHAR",
"position" : 3
},
{
"token" : "坐标",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 4
}
]
}

2.5 IK分词器查询

  1. 创建索引并指定分析器
1
2
3
4
5
6
7
8
9
10
11
12
json复制代码PUT index
{
"mappings": {
"properties": {
"content": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
}
}
}
}
  1. 创建文档
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
json复制代码POST index/_doc/1
{
"content": "美国留给伊拉克的是个烂摊子吗"
}

POST index/_doc/2
{
"content": "公安部:各地校车将享最高路权"
}

POST index/_doc/3
{
"content": "中韩渔警冲突调查:韩警平均每天扣1艘中国渔船"
}

POST index/_doc/4
{
"content": "中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"
}
  1. 搜索时指定高亮信息
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
json复制代码GET index/_search
{
"query": {
"match": {
"content": "中国"
}
},
"highlight" : {
"pre_tags" : ["<tag1>", "<tag2>"],
"post_tags" : ["</tag1>", "</tag2>"],
"fields" : {
"content" : {}
}
}
}
  1. 将会在高亮highlight中返回高亮信息
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复制代码{
"took" : 50,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 2,
"relation" : "eq"
},
"max_score" : 0.642793,
"hits" : [
{
"_index" : "index",
"_type" : "_doc",
"_id" : "3",
"_score" : 0.642793,
"_source" : {
"content" : "中韩渔警冲突调查:韩警平均每天扣1艘中国渔船"
},
"highlight" : {
"content" : [
"中韩渔警冲突调查:韩警平均每天扣1艘<tag1>中国</tag1>渔船"
]
}
},
{
"_index" : "index",
"_type" : "_doc",
"_id" : "4",
"_score" : 0.642793,
"_source" : {
"content" : "中国驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"
},
"highlight" : {
"content" : [
"<tag1>中国</tag1>驻洛杉矶领事馆遭亚裔男子枪击 嫌犯已自首"
]
}
}
]
}
}

本文转载自: 掘金

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

SaaS系统从0到1搭建,01前期准备-数据隔离方案

发表于 2021-07-28

目标:SaaS系统

最近因项目需要,会做个SaaS系统,方向是车队管理类似的,需求还不知道,只是有个方向。

大体上就是要SaaS,然后用户量级也不会很庞大,毕竟开大货车啥车的,人数的量就在那。

(我是后半夜Java,在掘金这分享下经验,那些靠copy的搬运作者,不要随意copy到其他平台了,哪天闲的就去投诉)

所以提前预研了下,总结以前的经验,希望不在去踩坑,在这也分享下建设思路,欢迎大家提意见哈。

既然选择SaaS了,那应该已经对SaaS有所了解了,这里不再重复SaaS的概念了。

数据隔离方案 业内

那既然是开建的准备,先梳理下数据的隔离性:

业内三种方法:

1、数据库隔离,也就是一个租户一个数据库(database)。好处是数据有问题恢复快,但成本高啊。

2、数据库的schema隔离(用MySQL),道理上还是一个库(DB),只是不同的user账号,好处是就一个库,但数据有问题恢复起来会相互影响。

3、同个数据库、同个schema(user),采用业务字段隔离。共享程度最高,隔离级别最低。好处就是一个数据库就足够支持N多的租户,缺点嘛,安全性在设计与撸码的时候一定一定一定要考虑好,记得要codereview。然后这个的数据库备份,数据库还原啥的也是比较有难度的。

确定个方案

好的,那这3种方案,咱这考虑直接选第三种,原因有下:

1、本身的业务类型所限,不会有太多的量。

2、用一个库多个租户,足够撑起近期2-3年的业务量,如果真的做好了,发展好了,估计大概率会有重构的,是吧,按互联网发展的经验。

3、第一个肯定先排除,因为刚开始每个服务商都要新建个数据库,成本有限哈。

4、估计后面会引人ES,不然第三个方案的统计类的,作为运营设计会比较方便。

那继续分析下前期准备,一个完整的业务请求流程大体上,是由用户发起请求、网络传输、防火墙(网关)、业务逻辑执行、返回数据。那去掉网络这块 和 运维的灾备等处理 交给运维网管团队,咱研发业务实现的就是考虑中间这个环节。

高并发思考

为了突出咱架构设计上的合理性,前沿性,也就是3高,高性能、高可用、高扩展,这3高做好了才是个高并发的完美框架(至少当前互联网发展的top是如此了)。

高性能:后面代码实现上,要做好就是。

高可用:集群搞上去。

高扩展:如果有高峰期,有活动啥的,需要临时扩展服务器,那需要立马上去。

选好方案后,下篇继续….

建个库先
image.png

将租户主表也先建个

image.png
CREATE TABLE car_tenant_base( idint(11) DEFAULT NULL, tenant_codevarchar(255) DEFAULT NULL COMMENT '租户编码', tenant_namevarchar(255) DEFAULT NULL COMMENT '租户名', contact_addressvarchar(255) DEFAULT NULL COMMENT 'l联系人地址', contact_phonevarchar(255) DEFAULT NULL COMMENT 'l联系人电话', contact_emailvarchar(255) DEFAULT NULL COMMENT '联系人邮箱', contact_namevarchar(255) DEFAULT NULL COMMENT '联系人', legal_namevarchar(255) DEFAULT NULL COMMENT '法人姓名', create_timedatetime DEFAULT NULL, update_timedatetime DEFAULT NULL, create_uservarchar(255) DEFAULT NULL, update_userdatetime DEFAULT NULL, delete_falg` tinyint(4) DEFAULT ‘0’ COMMENT ‘逻辑删除0-默认,1-删除’
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

`

本文转载自: 掘金

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

RabbitMq如何实现---流量削峰?(一)

发表于 2021-07-28

**搭建环境:springBoot + maven + RabbitMQ 3.8.14 + Erlang 23.2.7

注意:安装时rabbitMq和erlang版本号必须对应,以免引起不必要的bug。**

1、应用场景

应用解耦:当要调用远程系统时候,当存在订单系统和库存系统时,订单系统下单,库存系统需要收到订单后库存减一,这时候如果系统宕机,会造成订单丢失,吧订单消息发入mq,库存系统再去mq消费,就能解决这一问题。
异步消费:传统的模式:用户下单—>邮件发送—>短信提醒,三个步骤全部完成,才能返回用户消费成功,因为后面两个步骤完全没有必须是当前时间完成,可以用户下单成功后,直接发送给mq,返回给用户消费成功,之后邮件发送和短信提醒,可以其他时间段来消费发送给用户。
流量削峰:大型双11活动时候,0点有上亿并发,这时候数据库并不能承载那么大的数据冲击,而专门为高并发设计的mq可以承受住海量的请求,发送给mq,存储成功后,再消费。

2、流量削峰

本文主要介绍流量削峰实例,先创建两个表get_redpack和send_redpack。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE复制代码	 id int not null AUTO_INCREMENT,
user_id varchar(32) not null comment '发红包用户',
money decimal(10,2) not null comment '红包金额',
unit_money decimal(10,2) not null comment '单个红包金额',
total int not null comment '红包个数',
remain int not null comment '红包剩余个数',
send_date datetime not null comment '发红包时间',
primary key(id)
);
INSERT INTO send_redpack(user_id,money, unit_money,total,remain,send_date)
VALUES("001",10000.00,10.00,1000,1000,now());

CREATE TABLE get_redpack(
id int not null AUTO_INCREMENT,
user_id varchar(32) not null comment '抢红包用户',
send_redpack_id int not null comment '发红包记录id',
money decimal(10,2) not null comment '抢的红包金额',
get_date datetime not null comment '抢红包时间',
primary key(id)
);

本人用的是mac电脑brew安装的rabbitMq,启动rabbitMq用brew services strat rabbitmq,启动之后访问: http://localhost:15672/

登入的账号密码用guest,登入后可以在admin里面添加一个admin管理员,配置权限,在queues里面创建一个队列redpack,​供项目发用户ID到队列中。

image.png
image.png
上面的流程处理完之后,就可以在springboot项目中引入rabbitMq包,配置文件,及其新建上面表的实体类。

1
2
3
4
5
6
7
8
9
10
11
12
13
>复制代码        <dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>



# ----- RabbitMq -------- #
spring.rabbitmq.virtual-host=/
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
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
arduino复制代码/**
* 抢红包
*
* @author keying
* @date 2021/6/22
*/
@Data
public class GetRedpack {
/**
*
*/
private Integer id;

/**
* 抢红包用户
*/
private String userId;

/**
* 发红包用户
*/
private String sendRedpackId;

/**
* 抢的红包金额
*/
private BigDecimal money;

/**
* 抢红包时间
*/
private Date getDate;
}
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
php复制代码/**
* 发红包
* @author keying
* @date 2021/6/22
*/
@Data
public class SendRedpack {
/**
*
*/
private Integer id;

/**
* 发红包用户
*/
private String userId;

/**
* 红包金额
*/
private BigDecimal money;

/**
* 单个红包金额
*/
private BigDecimal unitMoney;

/**
* 红包个数
*/
private Integer total;

/**
* 红包剩余个数
*/
private Integer remain;

/**
* 发红包时间
*/
private Date sendDate;

}

下面先写provider生产者的代码,进入接口发送红包,发送100个,然后把收红包的用户id发给mq:

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
less复制代码/**
* 生产者
*
* @author keying
* @date 2021/6/22
*/
@RestController
@Slf4j
@RequestMapping("/provider")
public class ProviderController {

@Resource
private RabbitMqService rabbitMqService;

@RequestMapping("/send_redpack")
public void sendRedpack(){
for (int i = 0; i < 100; i++) {
rabbitMqService.sendRedpack(i);
}

}

}


@Resource
private RabbitTemplate rabbitTemplate;

@Override
public void sendRedpack(int i) {
rabbitTemplate.convertAndSend("redpack", i);
}

然后写消费者代码,用@rabbitListener监听queue,队列就是刚刚在mq管路页面创建的redpack,定义的发红包用户为001,为了方便测试,在代码里写死,给消费者的类加一个@Component的注解,交给spring容器管理,消费逻辑大致就是:

1、先查看红包剩余数,大于0则继续,否则结束。

2、吧红包剩余数-1.

3、抢红包信息存入get_redpack表,存储抢红包详情。

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
java复制代码/**
* 消费
*
* @author keying
* @date 2021/6/22
*/
@Component
@Slf4j
public class ConsumerRabbitMq {

@Resource
private RabbitMqMapper rabbitMqMapper;

private static final String sendUserId = "001";
/**
* 需在RabbitMQ中手动创建redpack 队列,否则报错
* @param message
*/
@RabbitListener(queues = "redpack")
public void getRedpack(String message){
log.info("接收的消费红包人员: {}", message);
try {
//查询红包剩余个数是否大于0
int remain = rabbitMqMapper.getRemain(sendUserId);
if(remain > 0) {
//扣减红包个数
int result = rabbitMqMapper.deleteOne(sendUserId);
if(result > 0) {
//3.新增用户抢红包记录
GetRedpack getRedpack = new GetRedpack();
getRedpack.setUserId(message);
getRedpack.setSendRedpackId(sendUserId);
getRedpack.setGetDate(new Date());
getRedpack.setMoney(new BigDecimal("10"));
rabbitMqMapper.insertGetRedpack(getRedpack);
}

}
//异步通知用户抢红包成功
} catch (Exception e) {
log.error("处理抢单异常:" + e.getMessage());
throw new RuntimeException("处理抢单异常");
}
}
}



<select id="getRemain" parameterType="java.lang.String" resultType="java.lang.Integer">
select remain from send_redpack where user_id = #{sendUserId}
</select>

<delete id="deleteOne" parameterType="java.lang.String">
update send_redpack set remain = remain-1 where user_id = #{sendUserId}
</delete>

<insert id="insertGetRedpack" parameterType="com.alibaba.first.model.GetRedpack">
insert into get_redpack (user_id,send_redpack_id,money,get_date)
values (#{userId},#{sendRedpackId},#{money},#{getDate})
</insert>

重点、重点、重点、注意点事项(重要的事要说三遍),踩坑总结:

1、安装时候,rabbitMq和erlang版本号对应一致。

2、springboot集成rabbitMq,guest只能登入localoal,远程ip,需要创建admin用户,用admin用户登入。

最后,看到这里的读者,喜欢的话安排一波(点赞,收藏,关注),原创不易,每周定期分享编程笔记。

本文转载自: 掘金

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

python🏅多线程超细详解 项目中多线程的目的 实战操作

发表于 2021-07-28

本文已参与好文召集令活动,点击查看:后端、大前端双赛道投稿,2万元奖池等你挑战!

项目中多线程的目的

具体原理我觉得这应该是可以跳到操作系统里面了一点点小小总结大家还可以找本操作系统的书看看,老规矩找不到的话找我主页,有一个关于资源的文章里面有资源当然也可以私信我。
我们再说说我们项目里面用多线程的目的:
1.线程之间共享内存非常容易。
2·使用多线程来实现多任务并发执行比使用多进程的效率高
3·有时候可以节省运行时间,这个一会在下面就会知道
4·当你一个文件要同时执行多个功能时就可以用到多线程

1
复制代码python语言内置了多线程功能支持,而不是单纯地作为底层操作系统的调度方式,从而简化了python的多线程编程。

实战操作

说这么多不如实际动手练练,首先导入线程
特别注意 :大家在见建文件的时候名字千万别和导入的包threading一样不然会出错的。

小知识

1
python复制代码import threading

让我们先看看自己程序现在有多少个进程

1
2
3
4
5
6
python复制代码import threading
def main():
print(threading.current_thread())

if __name__ == '__main__':
main()
1
python复制代码结果:1#我的就一个你的呢?

如果你的进程不为一的话还可以这样查看每一个进程名

1
2
3
4
5
6
7
8
python复制代码import threading
def main():
print(threading.active_count())
print(threading.enumerate())


if __name__ == '__main__':
main()
1
python复制代码>>>[<_MainThread(MainThread, started 36004)>]#返回的是一个列表因为我的目前就一个所以就一个主进程

还可以查看正在运行的进程

1
2
3
4
5
6
7
8
python复制代码import threading
def main():
print(threading.active_count())
print(threading.enumerate())
print(threading.current_thread())

if __name__ == '__main__':
main()
1
2
3
python复制代码>1
[<_MainThread(MainThread, started 36004)>]
<_MainThread(MainThread, started 36004)>

创建一个简单的线程

首先我们先介绍一下threading.Thread()里面的参数,大家学python每个模块的功能时最好还是看一下源文件内容,这样有助于提高你的编程能力:
在这里插入图片描述

需要注意的点我已经打上标记了

1
2
3
4
5
6
7
8
9
10
11
12
python复制代码import threading
def first():
print("frist active")
print("frist finish")

def main():
first_thread=threading.Thread(target=first,name="T1")
first_thread.start()#开始的标志
print("main")

if __name__ == '__main__':
main()
1
2
3
4
5
6
7
8
9
python复制代码结果:
第一次运行
frist active
main
frist finish
第二次运行
frist active
frist finish
main

每次的结果不一样就已经表明Frist和main是同时运行的了。
如果说效果不太明显的话,我们改进一下接下来我们引入

1
python复制代码import time
1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码import threading
import time
def first():
print("frist active")
time.sleep(3)
print("frist finish")

def main():
first_thread=threading.Thread(target=first,name="T1")
first_thread.start()
print("main")

if __name__ == '__main__':
main()
1
2
3
4
python复制代码结果:
frist active
main
frist finish

因为执行到Frist active的时候Frist线程要睡3秒这个时候main还在执行所以这样每次都是这个结果了。
特别强调target=first不是导入Frist函数从源文件我们就已经看出是通过run()方法进行的,这里解释我引用一位大佬解释
在这里插入图片描述

链接

这位大佬大家应该都熟悉,就是顶顶大名的雷学委各位大佬可以去他的主页看看可能会学到些新知识
当然如果你觉得这样不行的话你也可以重写threading.Thresd里的run方法来个自定义线程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码    class MyThread(threading.Thread):
def __init__(self,n):
super(MyThread,self).__init__() #重构run函数必须写
self.n = n

def run(self):
print('task',self.n)
time.sleep(1)
print('2s')
time.sleep(1)
print('1s')
time.sleep(1)
print('0s')
time.sleep(1)

if __name__ == '__main__':
t1 = MyThread('t1')
t2 = MyThread('t2')
t1.start()
t2.start()
1
2
3
4
5
6
7
8
9
python复制代码结果:
task t1
task t2
2s
2s
1s
1s
0s
0s

守护线程

1
makefile复制代码所谓’线程守护’,就是主线程不管该线程的执行情况,只要是其他子线程结束且主线程执行完毕,主线程都会关闭。也就是说:主线程不等待该守护线程的执行完再去关闭。

不好理解的话来看看例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码import threading
import time
def first():
print("frist active")
time.sleep(3)
print("frist finish")

def second():
print("second active")
print("second finish")

def main():
first_thread=threading.Thread(target=first,name="T1")
second_thresd=threading.Thread(target=second,name="T2")
first_thread.setDaemon(True)#一定要在start()前开始
first_thread.start()
second_thresd.start()
print("main")

if __name__ == '__main__':
main()
1
2
3
4
5
python复制代码结果
frist active
second active
second finishjiemeijieshu
main

当主线程和其他子线程都结束不管守护线程first_thread 结没结束程序都结束
当设second_thresd为守护线程的时候情况是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码import threading
import time
def first():
print("frist active")
time.sleep(3)
print("frist finish")

def second():
print("second active")
print("second finish")

def main():
first_thread=threading.Thread(target=first,name="T1")
second_thresd=threading.Thread(target=second,name="T2")
second_thresd.setDaemon(True)#一定要在start()前开始
first_thread.start()
second_thresd.start()
print("main")

if __name__ == '__main__':
main()
1
2
3
4
5
python复制代码frist active
second active
second finish
main
frist finish #尽管输出这个要等三秒

主进程等待子进程结束

为了让守护线程执行结束之后,主线程再结束,我们可以使用join方法,让主线程等待子线程执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
python复制代码import threading
import time
def first():
print("frist active")
time.sleep(3)
print("frist finish")

def second():
print("second active")
print("second finish")

def main():
first_thread=threading.Thread(target=first,name="T1")
second_thresd=threading.Thread(target=second,name="T2")
first_thread.start()
second_thresd.start()
first_thread.join()

print("main")

if __name__ == '__main__':
main()
1
2
3
4
5
6
7
8
9
10
11
12
python复制代码结果:
frist active
second active
second finish
frist finish
main
不加join是这样的结果
frist active
second active
second finish
main
frist finish

共享全局变量的特性

这里定义一个全局变量A

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
python复制代码import threading
import time
def first():
global A
print("frist active")
time.sleep(3)
A=A+3
print("frist:%d"%A)
print("frist finish")

def second():
global A
print("second active")
A=A+6
print("second:%d"%A)
print("second finish")

def main():
global A
first_thread=threading.Thread(target=first,name="T1")
second_thresd=threading.Thread(target=second,name="T2")
first_thread.start()
second_thresd.start()
#first_thread.join()

print("main")
A=A+3
print("mian:%d"%A)

if __name__ == '__main__':
A=0
main()

来看一下结果

1
2
3
4
5
6
7
8
python复制代码frist active
second active
second:6
second finish
main
mian:9
frist:12
frist finish

锁

由上面的例子可以看出,输出A的值的时候不同进程之间的资源是共享的这就导致了变量A的值不固定造成了脏数据的情况,不理解的话我们就来个例子。
在这里插入图片描述

在没有互斥锁的情况下,假设账户有一万元钱,存钱和取钱同时进行可能账户余额会有一万一千元。这样我当然高兴只是银行不答应。为了避免这种情况我们引入锁的概念,下面我们简单的介绍几种编程里面常用的。

互斥锁

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
python复制代码import threading
import time
def first():
global A,lock
lock.acquire()
print("frist active")
time.sleep(3)
A=A+3
print("frist:%d"%A)
print("frist finish")
lock.release()

def second():
global A,lock
lock.acquire()
print("second active")
A=A+6
print("second:%d"%A)
print("second finish")
lock.release

def main():
global A,lock
lock=threading.Lock()
first_thread=threading.Thread(target=first,name="T1")
second_thresd=threading.Thread(target=second,name="T2")
first_thread.start()
second_thresd.start()
#first_thread.join()

print("main")
A=A+3
print("mian:%d"%A)

if __name__ == '__main__':
A=0
main()

结果

1
2
3
4
5
6
7
8
python复制代码frist active
main
mian:3
frist:6
frist finish
second active
second:12
second finish

是不是这样看着就舒服多了,如果例子不够明显我们再来一个

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
python复制代码import threading
import time
def first():
global A,lock
lock.acquire()
print("frist active")
time.sleep(3)
A=A+3
print("frist1:%d"%A)
A = A + 3
print("frist2:%d" % A)
print("frist finish")
lock.release()

def second():
global A,lock
lock.acquire()
print("second active")
A=A+6
print("second1:%d"%A)
A=A+6
print("second2:%d"%A)

print("second finish")
lock.release()

def main():
global A,lock
lock=threading.Lock()
first_thread=threading.Thread(target=first,name="T1")
second_thresd=threading.Thread(target=second,name="T2")
first_thread.start()
second_thresd.start()
#first_thread.join()

print("main")
A=A+3
print("mian:%d"%A)

if __name__ == '__main__':
A=0
main()

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
python复制代码frist active
main
mian:3
frist1:6
frist2:9
frist finish
second active
second1:15
second2:21
second finish

去掉锁以后结果

frist active
second active
second1:6
second2:12
second finish
main
mian:15
frist1:18
frist2:21
frist finish

很明显去掉锁以后结果杂乱的很

信号量

我相信对操作系统有一定了解的肯定,在刚才提到锁的时候就想到了信号量毕竟考研题经常会出现,同步,互斥和信号量机制。我们就来说一说信号量锁,其实道理很简单假如你现在,在中国结婚了你只能娶一个老婆吧,尽管你可以去找别的女人但他们不能称为老婆她们会被称为小三,二奶啊等等,这里老婆这个信号量在中国就是==一==只能有一个,别的再来就不可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
python复制代码import threading
import time

def run(n,semaphore):
semaphore.acquire() #加锁
time.sleep(3)
print('run the thread:%s\n' % n)
semaphore.release() #释放


if __name__== '__main__':
num=0
semaphore = threading.BoundedSemaphore(3) #最多允许3个线程同时运行
for i in range(10):
t = threading.Thread(target=run,args=('t-%s' % i,semaphore))
t.start()
while threading.active_count() !=1:
pass
else:
print('----------all threads done-----------')

结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
python复制代码run the thread:t-2
run the thread:t-1


run the thread:t-0

run the thread:t-3
run the thread:t-5
run the thread:t-4



run the thread:t-6

run the thread:t-7

run the thread:t-8

run the thread:t-9

----------all threads done-----------

送点资源

肥学觉得python里面这些操作还是有点简单的,如果各位大佬想继续深究线程又没有资源的话可以我这里有一本关于操作系统的书,领取可以私信我哦,好了今天的学习就到这里吧别忘了==点赞三联哦==

本文转载自: 掘金

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

1…589590591…956

开发者博客

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