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

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


  • 首页

  • 归档

  • 搜索

MySQL索引

发表于 2021-11-15

一.索引的概述

1.为什么要使用索引?

在我们对海量的数据进行查询时,由于数据量十分庞大,会导致查询的速度缓慢,那么为了提高我们的查询速度,可以在某些字段上加上索引,那么根据这些加了索引的字段查询数据,速度就会更快。

2.索引是什么?

2.索引存放的位置

image.png

3.索引的分类

  • 主键索引:主键自带索引效果,也就是说通过主键来查询数据,性能是非常好的。
  • 普通索引:为普通列创建的索引。
  • 唯一索引:为某一列创建索引(列中的数据是唯一的)。
  • 组合索引:为多个字段创建的索引,需要遵守最左前缀法则才能命中索引。注意:一般不建议组合索引超过五个字段。
  • 全文索引:进行查询的时候,数据源可能来自于不同的字段不同的表,比如说百度某个词条,查处的结果词条可能在标题中,也可能在内容里,说明数据源来自不同的字段或者不同的表。在实际生产环境中,一般不会使用MySQL提供的MyISAM存储引擎的全文索引功能来实现全文查找。而是会使用第三方的搜索引擎中间件比如ElasticSearch(多),Solr。

3.索引为什么快?

image.png
如上图所示,由于MySQL数据存储在磁盘中,当我们查询数据时,会分批量对磁盘进行多次io来查询数据,当数据量多的时候io次数变多,开销会非常大,从而查询效率非常缓慢。

image.png
当我们为某个字段创建索引后,根据此字段去查询时会先通过字段值拿到对应数据在磁盘上的物理地址(MyISAM),从而直接拿到对应的数据,大大减少了磁盘io的次数,提高了查询效率,至于为什么查询索引会比直接查询数据要快,就要提到索引的数据结构了(B+树),下面会讲到。

  • 使用索引要注意什么?

二.索引使用的数据结构

数据存储的结构被称为数据结构,常用的数据结构有线性表,栈,堆,树等。
下面我们简单回顾一下各个数据结构的特点:

1.线性表:线性表主要有两种数据结构支撑:线性顺序表和线性链式表。

image.png

  • 线性顺序表:相邻数据的逻辑关系和数据的物理地址相关。
  • 线性链式表:相邻数据的逻辑关系和数据的物理地址不相关。
    • 单项链表:能够通过当前节点找到下一个节点,以此来维护表之间的逻辑关系。
    • 双向链表:能够通过当前节点找到上一个节点或者下一个节点,双向都可以找。
      顺序表和链式表的区别(以数组和链表为例):
  • 数组:因为数组是在内存上连续的一片区域,所以可以通过下标快速定位到元素,随机查询的效率很高,但是由于增删时,后于的每一个元素都要后移或者前移,导致增删的效率比较低。(查询的时间复杂度:O(1))
  • 链表:由于链表在内存上是不连续的,所以想要查询元素,无法直接拿到内存地址,必须要从头开始遍历链表,所以链表的查询效率比较低,但是由于增删时只需要修改节点的指针指向,所以增删的效率比较高。(查询的时间复杂度:O(n))

2.栈,队列,串,广义表

  • 栈:先进后出,有顺序栈和链式栈
  • 队列:先进先出,有顺序队列和链式队列
  • 串:String定长串,StringBuffer/StringBuilder动态串
  • 广义表:更加灵活的多维数组,可以在不同的元素中创建不同维度的数组。

3.树(重要)

1)多叉树:非二叉树

2)二叉树:一个节点最多只能有两个子节点

3)二叉查找树:二叉查找树的根节点是比所有左子树的节点要大的,比所有右子树的节点要小。这样的规律同样满足于它的子树。二叉查找树的查询性能性能和树的高读有关。

image.png

4)平衡二叉树:由于二叉查找树的根节点的左子树和右子树的节点相差过大时,会导致高度过高的子树查询效率降低,那么为了能够更好的维护二叉查找树的节点,我们引入了平衡二叉树。

image.png

平衡二叉树又称为AVL树。平衡二叉树遵循以下两个特点:

  1. 每棵子树中的左子树和右子树的深度差不能超过1
  2. 二叉树中每棵子树都要求是平衡二叉树

image.png
平衡因子︰每个结点都有其各自的平衡因子,表示的就是其左子树深度同右子树深度的差。平衡二叉树中各结点平衡因子的取值只可能是:0、1和-1。

如图所示,其中(a)的两棵二叉树中由于各个结点的平衡因子数的绝对值都不超过1,所以(a)中两棵二叉树都是平衡二叉树;而(b)的两棵二叉树中有结点的平衡因子数的绝对值超过1,所以都不是平衡二叉树。

二叉排序树转化为平衡二叉树

如果平衡二叉树不满足以上两个特点,则需要通过自旋来满足上述两个特点,自旋分为三种:左旋、右旋、双向旋转(先左后右,先右后左)。具体可以看这篇:平衡二叉树如何通过自旋保持平衡

5)红黑树(平衡二叉树的一种体现)

红黑树是一种特化的AVL树(平衡二叉树),都是在进行插入和删除操作时通过特定操作保持二叉查找树的平衡,从而获得较高的查找性能。
在二叉查找树强制一般要求以外,对于任何有效的红黑树我们增加了如下的额外要求:

  • 性质1.结点是红色或黑色。
  • 性质2.根结点是黑色。
  • 性质3.不可能有连在一起的红色节点。
  • 性质4.每个红色结点的两个子结点都是黑色。叶子结点都是黑色(nil-黑色的空节点)
    平衡二叉树为了维护树的平衡,在一旦不满足平衡的情况就要进行自旋,但是自旋会造成一定的系统开销。因此红黑树在自旋造成的系统开销和减少查询次数之间做了权衡。因此红黑树有时候并不是一颗平衡二叉树。

image.png

这些约束强制了红黑树的关键性质:从根到叶子的最长的可能路径不多于最短的可能路径的两倍长。结果是这个树大致上是平衡的。因为操作比如插入、删除和查找某个值的最坏情况时间都要求与树的高度成比例,这个在高度上的理论上限允许红黑树在最坏情况下都是高效的,而不同于普通的二叉查找树。

红黑树已经是在查询性能上得到了优化,但索引依然没有使用红黑树作为数据结构来存储数据,因为红黑树在每一层上存放的数据内容是有限的,导致数据量一大,树的深度就变得非常大,于是查询性能非常差。因此索引没有使用红黑树。

6)B树

B树允许一个结点存放多个数据。这样可以使更小的树的深度来存放更多的数据。但是,B树的一个结点中到底能存放多少个数据,决定了树的深度。

image.png

image.png
通过数值计算,B树的一个结点最多只能存放15个数据,因此B树依然不能满足海量数据的查询性能优化。

7)B+树

image.png

  • B+树的特点:
    • 非叶子结点冗余了叶子结点中的键。
    • 叶子结点是从小到大、从左到右排列的。
    • 叶子结点之间提供了指针,提高了区间访问的性能。
    • 只有叶子节点存放数据,非叶子节点是不存放数据的,只存放键。

image.png
B+树的非叶子节点不存储索引键的值,只在叶子节点上存储索引的键值,相同的节点数量相较于其他的结构能够存储的键更多,树的深度更浅,通过计算可以发现,三层结构就可以存储两千多万条数据,相比于其他的数据结构能够支持海量的数据,同时在相邻的叶子节点之间同过指针进行指向,那么查询连续查询两个相邻的数据时,第二次查询不会再从根节点遍历,而是根据第一次查询到的叶子节点持有的指针快速找到相邻的节点,大大提高了查询效率。同时叶子节点之间的指针可以支持范围查找,通过找到边界点的叶子节点,然后再根据相邻叶子节点的指针去直接遍历整个区域,不需要每次都从根节点遍历,大大提高了范围查找的速度。

7)哈希表

使用哈希表来存取数据性能是最快的,但是由于哈希表是根据哈希值来确定存储的位置,那么逻辑上相邻的数据在物理位置上不相邻,所以不支持范围查找(区间访问),比如找某个范围内的值,B+树结构的索引可以先找到边界值再根据叶子节点之间的指针快速访问,但是哈希表结构就只能每次都计算哈希值来查找数据。同时哈希表结构会产生哈希冲突,生成链之后会导致查询效率变慢。所以这也是为什么MySQL存在hash索引和b+树索引但是我们通常都使用b+树索引。
image.png

三.InnoDB和MyISAM的区别

1.InnoDB存储引擎(聚集索引)

把索引和数据存放在一个文件中,通过找到索引后就能直接在索引树上的叶子结点中获得完整的数据。
可以实现行锁/表锁。支持外键和事务。
image.png

2.MyISAM存储引擎(非聚集索引)

把索引和数据存放在两个文件中,查找到索引后还要去另一个文件中找数据,性能会慢一些。除此之外,MyISAM天然支持表锁,而且支持全文索引。不支持外键和事务。

image.png

四.一些面试题

1.问题一:为什么非主键索引的叶子节点存放的数据是主键值

image.png
如果普通索引中不存放主键,而存放完整数据,那么就会造成:
数据冗余,虽然提升了查询性能,但是需要更多的空间来存放冗余的数据·维护麻烦:一个地方修改数据,需要在多棵索引树上修改。

2.问题二:为什么lnnoDB表必须创建主键

创建InnoDB表不使用主键能创建成功吗?如果能创建功能,能不能为这张表的普通列创建索引?

如果没有主键,MySQL优化器会给一个虚拟的主键,于是普通索引会使用这个虚拟主键——也会造成性能开销。为了性能考虑,和设计初衷,那么创建表的时候就应该创建主键。

3.问题三:为什么使用主键时推荐使用整型的自增主键

1)为什么要使用整型:

主键-主键索引树-树里的叶子结点和非叶子结点的键存放的是主键的值,而且这颗树是一个二叉查找树。数据的存放是有大小顺序的。整型:大小顺序是很好比较的
·字符串:字符串的自然顺序的比较是要进行一次编码成为数值后再进行比较的
(字符串的自然顺序,AZ)

2)为什么要自增:

如果不用自增:使用不规律的整数来作为主键,那么主键索引树会使用更多的自旋次数来保证树索引树的叶子节点中的数据是从小到大-从左到右排列,因此性能必然比使用了自增主键的性能要差

五.联合索引

在使用一个索引来实现多个表中字段的索引效果。

⒉.联合索引是如何存储的

image.png
具体联合索引在B+树上的结构可以借鉴这篇博客:联合索引是如何存储在B+树上的

本文转载自: 掘金

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

程序员最喜欢的一句话?当然是New对象啦~ Java随笔

发表于 2021-11-15

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」


相关文章

Java随笔记:Java随笔记


  • 兄弟萌,我真撑不住了,最近忙的要死,实在没时间写啊!!!!这万年老存稿(水文)都被逼得发出来了!啊!
    1、面向对象概念:
    举例:大象装进冰箱。
    面向过程:强调的是过程(动作)。
    打开冰箱
    存储大象
    关上冰箱
1
2
3
4
markdown复制代码  面向对象:强调的是对象(实体)。冰箱自带打开、存储、关闭等功能。
冰箱打开
冰箱存储
冰箱关闭

特点:
面向对象就是一种常见的思想,符合人们的思考习惯。
面向对象的出现,将复杂的问题简单化。
面向对象的出现,让曾经在过程中的执行者,变成了对象中的指挥者。

2、类与对象的关系:
类是拥有相同行为特征对象的一个抽象概念。事物的描述。
对象是类这个抽象概念中事实存在的个体。该类事物的描述。
在java中对象通过new来创建的。

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
typescript复制代码范例:
public class Car {
private int num;//汽车轮子数量
private String color;//汽车车身颜色

public int getNum() {
return num;
}

public String getColor() {
return color;
}

public void setNum(int num) {
this.num = num;
}

public void setColor(String color) {
this.color = color;
}

@Override
public String toString() {
return "Car{" +
"num=" + num +
", color='" + color + '\'' +
'}';
}
}

主函数{
Car car = new Car();//car就是一个类类型的引用变量,指向了该类的对象
}

匿名对象:当对象方法仅进行一次调用时,就可以简化成匿名对象。
范例:
new Car().getColor();

3、封装(三大特性之一):
是指隐藏对象的属性和实现细节,仅对外提供公共访问方式。

1
2
3
4
5
6
7
8
9
10
11
12
markdown复制代码好处:
将变化隔离。
便于使用。
提高重用性。
提高安全性。
封装原则:
将不需要对外提供的内容都隐藏起。
把属性都隐藏,提供公共方法对其访问。

private:
私有,是一个权限修饰符,用于修饰成员。
私有的内容只在本类中有效。

4、构造函数:
特点:
1)函数名与类名相同。
2)不用定义返回值类型。
3)没有具体的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
markdown复制代码作用:
给对象进行初始化。————构建创造对象时调用的函数。

注意:
1)默认构造函数的特点。
创建对象都必须要通过构造函数初始化。
一个类中如果没有定义过构造函数,那么类中会有一个默认的空构造函数。如果定义了制定的构造函数,那么类中的默认构造函数就没有了。

2)多个构造函数是以重载的形式存在的。

和一般函数区别:
构造函数:对象创建时,就会调用与之对应的构造函数,对对象进行初始化。
一般函数:对象创建后,需要函数功能时才调用。

构造函数:对象创建时,会调用只调用一次。
一般函数:对象创建后,可调用多次。

什么时候定义构造函数呢?
在描述事物时,该事物已存在就具备的一些内容,这些内容都定义在构造函数中。

5、this关键字:
特点:
this关键字代表其所在函数所属对象的引用。即:本类对象的引用。
也就是,哪个对象调用了this所在的函数,this就代表哪个对象。

1
2
3
4
5
6
7
8
9
10
11
12
kotlin复制代码什么时候使用this关键字呢?
当在函数类需要调用该函数的对象时。
当局部变量和成员变量重名,可以用关键字this来区分。

this调用构造函数时注意:
只能定义在构造函数的第一行,因为初始化动作要先执行。

基本应用:
判断是否为同龄人:
public boolean compare(Person p){
return this.age == p.age;
}

6、static关键字:
主要用于修饰成员(成员变量和成员函数)。

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
markdown复制代码被修饰后的成员具有以下特点:
1)随着类的加载而加载。
2)优先于对象存在。
3)被所有对象共享。
4)可以直接被类名调用。

使用注意:
1)静态方法只能访问静态成员。
2)静态方法中不可以写this、super关键字。
3)主函数是静态的。

成员变量和静态变量的区别?
1) 两个变量的生命周期不一样:
成员变量随着对象的创建而存在,随着对象的被回收而释放。
静态变量随着类的加载而存在,随着类的消失而消失。
2) 调用方式不同:
成员变量只能被对象调用。
静态变量可以被对象调用,还可以被类名调用。
3) 别名不同:
成员变量也被成为实例变量。
静态变量被成为类变量。
4) 数据存储位置不同:
成员变量数据存储在堆内存的对象中,所以也叫对象的特有数据。
静态变量数据存储在方法区中。(方法区中的静态区)

静态代码块:
随着类的加载而加载,而且只执行一次。

作用:
用于给类进行初始化。因为有的类不需要对象,可直接调用静态代码块。


静态方法和构造方法的区别?
静态方法:随着类的加载而加载,而且只执行一次。
构造方法:是给对应的对象进行针对性的初始化。
构造代码块:可以给所有对象进行初始化。

7、继承(三大特性之二):
java中支持单继承,不直接支持多集成,但对c++中的多继承机制进行改良。
单继承:一个子类只能有一个直接父类。
多继承:一个子类可以有多个直接父类。————不直接支持,是因为当两个父类中有同名的方法时,就会有调用不确定性。
在java中通过多实现的方式来体现它!

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
scala复制代码java支持多层(多重继承)。
c继承b,b继承a。就会出现继承体系,当要使用一个继承体系时:
查看该体系中的顶层类,了解该体系的基本功能。
创建体系中的最子类对象,完成功能的使用。


好处:
提高了代码的复用性。
让类与类之间产生了关系,给第三个特征多态提供了前提。

什么时候定义继承呢?
当类与类之间存在所属关系的时候,就定义继承。xxx是yyy中的一种,xxx就可以继承yyy的一种。
所属关系: is a 关系。

具体的体现:
在子父类之中:
1)成员变量。
当本类中的成员和局部变量同名重名用this区分。
当子父类中的成员变量同名用super区分为父类。

this和super的区别:
this代表本类对象的引用。
super代表父类的空间。
范例:
calss fu{
int num = 4;
}

class zi extends fu{
int num = 5;
void show(){
sout(this.num + "加" + super.num);
}
}
主函数:{
zi z = new zi();
z.show();
}
打印:4加5

2)成员函数。
当子父类中出现成员函数一模一样的情况下:
会运行子类的函数。这种现象被称为覆盖操作。
函数的两个特性:
1)重载:同一个类中。overload。
2)覆盖(重写):子类中,覆盖也成为重写(复写)。override。

重写注意事项:
1)子类方法重写父类方法时,子类权限必须要大于等于父类的权限。
2)静态只能重写静态,或被静态重写。

什么时候使用重写操作?
当对一个类进行子类的扩展时,子类需要保留的功能声明,但需要定义子类中该功能的特有内容时,就使用重写操作完成。

范例:
calss fu{
void show(){
sout("old num");
}
}

class zi extends fu{
void show(){
sout("new pic");
sout("new img");
this.show();
}
}
主函数:{
zi z = new zi();
z.show();
}

3)构造函数。
在子类构造对象时,发现访问子类构造函数时,父类也运行了,原因是:在子类的构造函数中,第一行有一个隐式语句。super();————调用父类的构造函数。

子类的实例化过程:子类中所有的构造函数默认都会访问父类中的空参数的构造函数。

8、final关键字
继承弊端:打破了封装性。用final禁止继承。
1)可以修饰类。方法、变量。
2)修饰的类不可以被继承。
3)修饰的方法不可以被重写。
4)修饰的变量是一个常量。只能在初始化时被赋值一次。
为什么要使用final修饰变量?
如果在程序中有一个数据是固定的,那么直接1使用这个数据就可以了,并且这个变量名称和值不能变化,加上final固定。
写法规范: 常量所有字母都大写,多个单词,中间用 _ 连接。
5)内部类只能访问被修饰的局部变量。

9、抽象类(基于继承)————abstract
抽象:笼统,模糊,不具体。

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
scala复制代码特点:
1)方法只有声明没有实现时,该方法就是抽象方法,需要被abstract修饰,抽象方法必须定义在抽象类中。该类必须也被abstract修饰。
2)抽象类不可以被实例化。为什么?因为调用抽象方法没有意义。
3)抽象类必须由其子类重写了所有的抽象方法后,该子类才可以实例化,否则,这个子类还是抽象类。

范例:
abstract calss 犬科{
abstract void 吼叫();
}

class 狗 extends 犬科{
sout("汪汪");
}

class 狼 extends 犬科{
sout("嗷~~嗷~~");
}

提出抽象类的五个问题:
1)抽象类中有构造函数吗?
有,用于给其子类进行初始化。

2)抽象类可以不定义抽象方法吗?
可以,但是很少见,目的就是不让该类创建对象。AWT的适配器对象就是这种类。通常这个类中有方法体,但是没有内容。

3)抽象关键字不可以和哪些关键字共存?
private 不行 ———— 因为抽象方法必须被重写。非法修饰符组合。
static 不行 ———— 如果成员变静态,则不需要对象,抽象方法即无意义。
final 不行 ———— 因为无法被重写。

4)抽象类和一般类的异同点?
异: 一般类有足够的信息描述事物。抽象类描述事物的信息可能不足。
一般类中不能第一抽象方法,只能定义非抽象方法。抽象类中可以定义抽象方法,同时可以定义非抽象方法。
一般类可以被实例化。抽象类不可以被实例化。
同:都是用来描述事物的,都在内部定义了成员。

5)抽象类一定是一个父类吗?
一定是父类。
因为某个类继承了抽象类,一定要重写其所有抽象方法才可以对其实例化,否则该类还是抽象类!

10、接口 interface
当一个抽象类中的方法都是抽象方法时,这时候可以将抽象类定义成接口。

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
csharp复制代码格式:
interface {}

接口中的成员修饰符都是固定的:
1)成员常量(全局变量):public static final
2)成员函数(抽象方法):public abstract
3)发现接口中的成员都是public的
由此得出结论:接口中的成员都是公共的权限。

接口不再使用继承,使用 implements 来实现接口。

范例:
interface demo{
public static final int num = 4;
void show();
}

calss demoImpl implements demo{
@Override
public void show(){
sout("我是一个接口中抽象方法的实现方法");
}
}

在java中不支持多继承,因为会出现调用的不确定性。
所以java将多继承机制进行了改良,在java中变成了多实现。
一个类可以实现多个接口。

范例:
class Test implements 接口一,接口二{
方法体;
}

一个类在继承另一个类的同时,还可以实现多个接口。接口的出现避免了单继承的局限性。

细节:
类与类之间是继承关系。
类与接口之间是实现关系。
接口与接口之间是继承关系。而且接口可以多继承。实现子接口时需重写子接口及所有父接口的所有抽象方法。

特点:
1)接口是对外暴露的规则。
2)接口是程序的功能拓展。
3)接口的出现降低了耦合性。
4)接口可以用来多实现。
5)类与接口之间是实现关系,而且类可以继承一个类的同时实现多个接口。
6)接口和接口之间可以有继承关系。

凡是对外暴露的东西都可以被称之为接口。


接口类与抽象类的异同点?

同:都是不断向上抽取而来的。

异: 1)抽象类需要被继承,而且只能单继承。接口需要被实现,而且可以多实现。
2) 抽象类中可以定义抽象方法和非抽象方法,子类继承后,可以直接使用非抽象方法。
接口中只能定义抽象方法,必须由子类区实现。
3) 抽象类的继承是 is a 关系。在定义该体系基本共性内容。
接口的实现是 like a 关系。在定义该体系额外功能。

11、多态
定义:某一种事物的多种存在形态。

1
2
3
4
5
6
7
8
9
10
11
12
ini复制代码例子:父类 名 = new 子类();
动物中猫。狗。
猫这个对象对应的类型就是猫类型。猫 x = new 猫();
同时猫也是动物中的一种们也可以把猫称为动物。动物 y = new 猫();
动物是猫和狗等具体事物中抽取出来的父类型,父类型引用指向了子类对象。

对象的多态性。
猫这类事物即具有猫的形态,又具备着动物的形态。这就是对象的多态性。
简单说就是一个对象对应着不同类型。

多态在代码中的体现:
父类或者接口的引用指向其子类的对象。

路漫漫其修远兮,吾必将上下求索~

如果你认为i博主写的不错!写作不易,请点赞、关注、评论给博主一个鼓励吧~hahah

本文转载自: 掘金

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

Python matplotlib 绘制散点图 复习回顾 1

发表于 2021-11-15

这是我参与11月更文挑战的第14天,活动详情查看:2021最后一次更文挑战

复习回顾

我们在往前几期中对matplotlib模块学习,对常用的反映数据变化的折线图,对比数据类型差异的柱状图和反应数据频率分布情况的直方图。

往前内容快速查看

  • matplotlib 模块概述:matplotlib 模块常用方法汇总
  • matplotlib 模块底层原理:matplotlib 模块脚本层、美工层及后端层讲解
  • matplotlib 绘制折线图:折线图相关属性和方法汇总
  • matplotlib 绘制柱状图:柱状图相关属性和方法汇总
  • matplotlib 绘制直方图:直方图相关属性和方法汇总

在数据统计图表中,有一种图表是散列点分布在坐标中,反应数据随着自变量变化的趋势。

image.png

本期,我们将详细学习matplotlib 绘制散点图相关属性的学习,let’s go~

  1. 散点图概述

  • 什么是散点图?

+ 散点图用于在水平轴和垂直轴上绘制数据点,数据以点状分布在左标系中
+ 散点图表示因变量随着自变量而变化的大致趋势
+ 散点图由多个左坐标点构成,考察坐标点的分布,判断是否存在某种关联或者分布模式
+ 对于不同类别的点,则由图表中不同形状或颜色的标记符表示
+ 散点图主要分为散点图矩阵、三维散点图、ArcGIS散点图
  • 散点图使用场景

+ 散点图用于比较跨类别的聚合数据
+ 散点图用于分析数据线性、多项式趋势情况
+ 散点图用于四象限分析
+ 散点图用于找到数据趋势公式
+ 散点图可以为后期精确的图标进行辅助
  • 绘制散点图步骤

1. 导入matplotlib.pyplot模块
2. 准备数据,可以使用numpy/pandas整理数据
3. 调用pyplot.scatter()绘制散点图
  • 案例展示

本次案例我们将分析某产品不同定价销售额分布情况

  • 案例需要准备两组数据x和y轴,其中x,y轴的数据量要保持一致
1
2
python复制代码x_value = np.random.randint(50,100,50)
y_value = np.random.randint(500,1000,50)
  • 绘制散点
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
python复制代码import matplotlib.pyplot as plt
import numpy as np

plt.rcParams["font.sans-serif"]=['SimHei'] plt.rcParams["axes.unicode_minus"]=False

x_value = np.random.randint(50,100,50)
y_value = np.random.randint(500,1000,50)

plt.scatter(x_value,y_value)

plt.title("data analyze")
plt.xlabel("销售价格")
plt.ylabel("销售额")

plt.show()

image.png

  1. 散点图属性

  • 设置散点大小

+ 关键字:s
+ 传入数据类型为list或者数字,默认为20
  • 设置散点颜色

+ 关键字:c
+ 默认颜色为蓝色
+ 取值范围
    - 表示颜色的英文单词:如红色"red"
    - 表示颜色单词的简称如:红色"r",黄色"y"
    - RGB格式:十六进制格式如"#88c999";(r,g,b)元组形式
    - 也可以传入颜色列表
  • 设置散点样式

+ 关键字:marker
+ 系统默认为'o'小圆圈
+ 取值还可以取:('o', 'v', '^', '<', '>', '8', 's', 'p', '\*', 'h', 'H', 'D', 'd', 'P', 'X')
  • 设置透明度

+ 关键字:alpha
+ 取值范围:0~1
  • 设置散点边框

+ 关键字: edgecolor
+ 默认为face
+ 取值选项:
    - "face"|"none"
    - 表示颜色的英文单词、简写或者rgb
  • 我们结合上一节的案例,设置散点大小,散点边框为粉色,散点颜色为#88c999
1
2
python复制代码size = (20*np.random.rand(50))**2
plt.scatter(x_value,y_value,s=area,c="#88c999",edgecolors="pink")

image.png

  1. 添加折线散点图

我们在查看散点图时,有时候会借助折线图来辅助分析。我们继续拿第一节的数据来分析。

  • 我们使用np.random.rand()来生成100个随机数据
1
2
python复制代码x_value = 100*np.random.rand(100)
y_value = 100*np.random.rand(100)
  • 需要借助我们高中的数学公司如sin\cos函数等(高中数学都还给老师了)
  • 使用pyplot.plot()方法来绘制曲线图
1
2
3
4
python复制代码r0 = 80
plt.scatter(x_value,y_value,c="hotpink",edgecolors="blue")
the = np.arange(0,np.pi/2, 0.01)
plt.plot(r0*np.cos(the),r0*np.sin(the))

image.png

  1. 多类型散点图

我们在观察数据的时候,会同时比较多个类型数据,因此我们可以通过颜色或者散点样式来区分表示

  • 方式一: 使用颜色来区分不同类别时,我们需要再添加新的数据和scatter方法
1
2
3
4
5
6
7
python复制代码x_value = 100*np.random.rand(100)
y_value = 100*np.random.rand(100)
y1_value = 100*np.random.rand(100)

plt.scatter(x_value,y_value, c="hotpink",edgecolors="blue",label="A产品")

plt.scatter(x_value,y1_value, c="#88c999", edgecolors="y",label="B产品")

image.png

  • 方式二:我们可以使用marker来标记不同类型,例如我们使用上一节的案例再添加一个scatter()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
python复制代码r0 = 80

size = (20*np.random.rand(100))**2

r = np.sqrt(x_value**2+y_value**2)
area = np.ma.masked_where(r > r0,size)
area1 = np.ma.masked_where(r <= r0, size)

plt.scatter(x_value,y_value,s=area,c="hotpink",edgecolors="blue",label="A产品")

plt.scatter(x_value, y_value, s=area1, c="red", edgecolors="y",marker="^",label="B产品")

the = np.arange(0,np.pi/2, 0.01)
plt.plot(r0*np.cos(the),r0*np.sin(the))

image.png

  1. 颜色条散点图

在散点图表中,我们为了对每个点颜色深浅进行表示,我们可以借助cmap颜色条来进行添加

  • 颜色条显示关键字:cmap
  • 默认为viridis,可选值如accent_r,blues_r,brbg_r,greens_r等等
  • 表示每种颜色从0~100的值

当要显示颜色列表时,我们需要调用pyplot.colorbar()

例如,我们对散点图添加一个红色系的颜色列表

1
2
3
4
python复制代码size = (20*np.random.rand(100))**2
color = np.random.randint(0,100,100)
plt.scatter(x_value,y_value, s=size, c=color,label="A产品",cmap="afmhot_r")
plt.colorbar()

image.png

  1. 曲线散点图

散点图都是由一个一个坐标点组成的,当这些点具有一定规律时,我们可以使用散点图来绘制曲线。

我们使用scatter()绘制一个2次方的幂函数

1
2
3
4
5
6
python复制代码x_value = list(range(1, 100))
y_value = [x ** 2 for x in x_value]

plt.scatter(x_value,y_value,c=y_value,cmap="hot_r",edgecolors="none",s=50)

plt.show()

image.png

总结

本期,我们对matplotlib.pyplot 绘制散点图scatter方法及相关属性进行详细的学习。对于暂时没有找到规律的数据来说,使用散点图可以快速发现数据的分布情况

以上是本期内容,欢迎大佬们点赞评论,下期见~

本文转载自: 掘金

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

为什么你的 SpringBoot 自动配置失效了

发表于 2021-11-15

这是我参与11月更文挑战的第 10 天,活动详情查看:2021最后一次更文挑战

本文源自近期项目中遇到的问题, bug 总是出现在你自以为是的地方…

问题描述

下面是一个简单复现的代码片段,在你没有阅读完本文时,如果能做出正确的判断,那恭喜你可以节省阅读本文的时间了。

1、自动配置类:AutoTestConfiguration

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration
@EnableConfigurationProperties(TestProperties.class)
@ConditionalOnProperty(prefix = "test", name = "enable")
public class AutoTestConfiguration {
@Bean
@ConditionalOnMissingBean
public TestBean testBean(TestProperties properties){
System.out.println("this is executed.....");
return new TestBean();
}
}

2、配置类 TestProperties

1
2
3
4
5
6
7
8
9
10
java复制代码@ConfigurationProperties(prefix = "test")
public class TestProperties {
private boolean enable = true;
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
}

这两个类都在 root package 下,可以保证能够正常被 Spring 扫描到;那么问题是 TestBean 会不会被正常创建?当然这里的结论是不会。

可能有的同学会说你的 TestProperties 没有加 @Configuration 注解,Spring 不认识它,那真的是这样吗?很显然也不是。

在排查这个问题的过程中,也有遇到其他问题,也是之前没有遇到过的;即使 Spring 源码我看过很多遍,但是仍然会有一些边边角角让你意想不到的地方;下面就针对这个问题,慢慢来揭开它的面纱。

@EnableConfigurationProperties 注解行为

在之前的版本中,TestProperties 是有被 @Configuration 注解标注的

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Configuration // 可以被 spring 扫描
@ConfigurationProperties(prefix = "test")
public class TestProperties {
private boolean enable = true;
public boolean isEnable() {
return enable;
}
public void setEnable(boolean enable) {
this.enable = enable;
}
}

常规的思路是,当 TestProperties 被扫描到之后,spring env 中就会有 test.enable=true 的 k-v 存在,当执行 AutoTestConfiguration 自动配置类刷新时,@ConditionalOnProperty(prefix = "test", name = "enable") 则会生效,进而 TestBean 被正常创建。

但事实并非如此,下面是对于此问题的验证

配置有效,AutoTestConfiguration 未刷新

两个点:

  • 1、AutoTestConfiguration#testBean 执行会输出一个 log(用于判断 AutoTestConfiguration 是否正常刷新)
  • 监听 ApplicationReadyEvent 事件,拿 test.enable 值(用于判端配置是否正常加载,也就是 TestProperties 是否被正常刷新)

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@SpringBootApplication
public class Application implements ApplicationListener<ApplicationReadyEvent> {
@Autowired
private ApplicationContext applicationContext;
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
System.out.println(this.applicationContext.getEnvironment().getProperty("test.enable") + "------");
}
}

执行得到的结果是 AutoTestConfiguration#testBean 没有被执行,但test.enable 为 true。

这里说明 TestProperties 是有被刷新的,但是并没有对 @ConditionalOnProperty 起到作用,那么这里基本可以猜到是自动配置类上的 @ConditionalOnProperty 和 @EnableConfigurationProperties 的作用顺序问题。

在验证顺序问题之前,我尝试在 application.properties 中增加如下配置,re run 项目:

1
properties复制代码test.enable=true

到这里我得到了另一个 bean 冲突的问题。

prefix-type

异常提示如下:

1
2
3
ruby复制代码Parameter 0 of method testBean in com.glmapper.bridge.boot.config.AutoTestConfiguration required a single bean, but 2 were found:
- testProperties: defined in file [/Users/glmapper/Documents/project/exception-guides/target/classes/com/glmapper/bridge/boot/config/TestProperties.class]
- test-com.glmapper.bridge.boot.config.TestProperties: defined in null

这里出现了 test-com.glmapper.bridge.boot.config.TestProperties 这个 name 的 bean。我尝试在代码中去检查是否有显示给定这个 bean 名字,但是没有找到,那只有一种可能,就是这个是被 spring 自己创建的。

这个过程在 spring 刷新阶段非常靠前,在排查这个问题时,还是耽误了一些时间,最后还是把问题定位一致前置到 beandefinitions 初始化才找到。

这里是 @EnableConfigurationProperties 注解的一个行为,依赖 EnableConfigurationPropertiesRegistrar,源码如下:

1
2
3
4
5
6
7
8
9
10
11
java复制代码class EnableConfigurationPropertiesRegistrar implements ImportBeanDefinitionRegistrar {
.getQualifiedAttributeName(EnableConfigurationPropertiesRegistrar.class, "methodValidationExcludeFilter");

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
registerInfrastructureBeans(registry);
registerMethodValidationExcludeFilter(registry);
ConfigurationPropertiesBeanRegistrar beanRegistrar = new ConfigurationPropertiesBeanRegistrar(registry);
// to register
getTypes(metadata).forEach(beanRegistrar::register);
}

通过代码比较容易看出,EnableConfigurationPropertiesRegistrar 会将目标 metadata 注册成 bean;继续 debug,找到了产生 prefix-type 格式 name 的 bean。

image.png

下面是 getName 的具体代码

1
2
3
4
5
6
java复制代码private String getName(Class<?> type, MergedAnnotation<ConfigurationProperties> annotation) {
// 拿 prefix
String prefix = annotation.isPresent() ? annotation.getString("prefix") : "";
// prefix + "-" + 类全限定名
return (StringUtils.hasText(prefix) ? prefix + "-" + type.getName() : type.getName());
}

到这里我们先明确一个问题:

如果你使用 @EnableConfigurationProperties 来开启配置类,那么就不要在配置类上使用@Configuration 等能够被 Spring scan 识别到的注解,以免在后续的使用中同一类型的 bean 多个实例

@ConditionalOnProperty

在回到配置不生效问题上来,这里在官方 issue 是有记录的:github.com/spring-proj…

不过这里还是通过分析代码来还原下问题产生的根本原因;这里主要从两个方面来分析:

  • @ConditionalOnProperty match 值逻辑,需要明确在匹配 value 时,从哪些 PropertySource 读取的。
  • @ConditionalOnProperty match 失败和 bean 刷新的逻辑

@ConditionalOnProperty match 逻辑

首先是 @ConditionalOnProperty 在执行计算时,匹配 value 的值来源问题,通过 debug 代码很容易就得到了所有的 source 来源,如下图:

image.png

从 debug 看,本案例有 4 个来源(具体如上图),实际上从源码来看,source 涵盖了 spring env 所有来源:

1
2
3
4
5
6
arduino复制代码[ConfigurationPropertySourcesPropertySource {name='configurationProperties'}, 
StubPropertySource {name='servletConfigInitParams'},
StubPropertySource {name='servletContextInitParams'},
PropertiesPropertySource {name='systemProperties'}, OriginAwareSystemEnvironmentPropertySource {name='systemEnvironment'},
RandomValuePropertySource {name='random'},
OriginTrackedMapPropertySource {name='Config resource 'class path resource [application.properties]' via location 'optional:classpath:/''}]

所以本文案例中不生效原因就是上面这些 PropertySource 都没有 test.enable,也就是 TestProperties 没被刷新,或者其在自动配置类之后才刷新。

@ConditionalOnProperty skip 逻辑

这里主要解释 @ConditionalOnPropert 和 bean 被刷新的逻辑关系,具体实现在 ConditionEvaluator 类中

1
2
3
4
java复制代码public boolean shouldSkip(@Nullable AnnotatedTypeMetadata metadata, @Nullable ConfigurationPhase phase) {
// 1、没有 Conditional 注解,则扫描时不会跳过当前 bean
// 2、遍历 conditions 进行判断是否满足
}

所以对于自动配置类上的注解,Conditional 是作为当前类是否允许被刷新的前提,只有 Conditional 条件满足,才会将当前的自动配置类加入到待刷新 bean 列表中去,如果 Conditional 不满足,这个 bean 将直接被跳过,不会被放到 BeandefinitonMap 中去,也就不会有后续的刷新动作。

@ConditionalOnProperty 作用时机在 BeanDefiniton 被创建之前,其执行时机要比 @EnableConfigurationProperties 作用要早,这也就说明了,为什么 TestProperties 中 test.enable=true, AutoTestConfiguration 也不会刷新的原因了。

总结

本文通过一个简单 case,对于项目中遇到的 SpringBoot 配置失效导致 bean 未被刷新问题进行了回溯,总结如下:

  • Conditional 相关注解对于自动配置类来说,作用时机较早,用于决定当前自动配置类是否允许被刷新
  • @EnableConfigurationProperties enable 的类,会默认注册一个 bean,bean 名字格式为 prefix-type

本文转载自: 掘金

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

性能优化-内存篇

发表于 2021-11-15

上篇文章说了我们性能优化的CPU维度的优化方向,及CPU指标的基本知识 性能优化CPU篇,本文我们着手,从内存的角度讲解一下优化的方向。

内存原理

内存概括

日常生活常说的内存是什么

  • 比方说,我的笔记本电脑内存就是 8GB 的
  • 这个内存其实是物理内存
  • 物理内存也称为主存 ,大多数计算机用的主存都是动态 随机访问内存 (DRAM)

虚拟地址空间
Linux 内核给每个进程都提供了一个独立的虚拟地址空间,并且这个地址空间是连续的。 这样,进程就可以很方便 地访问内存,更确切地说是访问虚拟内存。

虚拟地址空间内部

  • 虚拟地址空间的内部又被分为 内核空间 和 用户空间 两部分
  • 不同字长(单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同,例如32位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间
    而 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的

image.png

进程的用户态和内核态
进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空 间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很 方便 地访问内核空间内存。
为什么会有内存映射
既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实 际的物理内存大得多。 所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使 用的虚拟内存才分配物理内存,并且分配后的物 理内存,是通过内存映射来管理的。
什么是内存映射
内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每 个进程都维护了一张页 表,记录虚拟地址与物理地址的映射关系,如下图所示:

image.png

SWAP运行原理

Swap 是把一块磁盘空间或者一个本地文件,当成内存来使用。它包括换出和换入两个过程。

  • 所谓换出,就是把进程暂时不用的内存数据存储到磁盘中,并释放这些数据占用的内存。
  • 而换入,则是在进程再次访问这些内存的时候,把它们从磁盘读到内存中来。

一个很典型的场景就是,即使内存不足时,有些应用程序也并不想被 OOM 杀死,而是希望能缓一段时间,等待人工介入,或者等系统自动释放其他进程的内存,再分配给它。除此之外,我们常见的笔记本电脑的休眠和快速开机 的功能,也基于 Swap 。休眠时,把系统的内存存入磁盘,这样等到再次开机时,只要从磁盘中加载内存就可以。 这样就省去了很多应用程序的初始化过程,加快了开机速度。话说回来,既然 Swap 是为了回收内存,那么 Linux 到底在什么时候需要回收内存呢?前面一直在说内存资源紧张,又该怎么来衡量内存是不是紧张呢?

一个最容易想到的场景就是,有新的大块内存分配请求,但是剩余内存不足。这个时候系统就需要回收一部分内 存,进而尽可能地满足新内存请求。这个过程通常被称为 直接内存回收。

除了直接内存回收,还有一个专门的内核线程用来 定期回收内存 ,也就是 kswapd0。为了衡量内存的使用情况, kswapd0 定义了三个内存阈值(watermark,也称为水位),分别是页最小阈值(pages_min)、页低阈值 (pages_low)和页高阈值(pages_high)。剩余内存,则使用 pages_free 表示

image.png

kswapd0 定期扫描内存的使用情况,并根据剩余内存落在这三个阈值的空间位置,进行内存的回收操作。

  • 剩余内存小于页最小阈值,说明进程可用内存都耗尽了,只有内核才可以分配内存。
  • 剩余内存落在页最小阈值和页低阈值中间,说明内存压力比较大,剩余内存不多了。这时 kswapd0 会执行内 存回收,直到剩余内存大于高阈值为止。
  • 剩余内存落在页低阈值和页高阈值中间,说明内存有一定压力,但还可以满足新内存请求。
  • 剩余内存大于页高阈值,说明剩余内存比较多,没有内存压力。
    可以看到,一旦剩余内存小于页低阈值,就会触发内存的回收。这个页低阈值,其实可以通过内核选项 /proc/sys/vm/min_free_kbytes 来间接设置。min_free_kbytes 设置了页最小阈值,而其他两个阈值,都是根据页最 小阈值计算生成的,计算方法如下 :
1
2
yml复制代码pages_low = pages_min*5/4
pages_high = pages_min*3/2

内存性能统计信息

内存使用量&调优
  • free

image.png
所有数值默认都是以字节(kb)为单位

  • 第一行 Mem:物理内存; 第二行 Swap:交换分区

free = total - used - shared - buffcache

image.png
例:每隔 2s 输出一次统计信息,总共输出 2 次,并且人性化输出所有数值

1
2
shell复制代码
[root@centos7-2 ~]# free -h -c 2 -s 2
  • top命令

image.png

性能剖析

内存性能指标

  1. 系统内存使用情况

如已用内存、剩余内存、共享内存、可用内存、缓存和缓冲区的用量等。

  • 已用内存和剩余内存很容易理解,就是已经使用和还未使用的内存。
  • 共享内存是通过 tmpfs (内存的文件系统 )实现的,所以它的大小也就是 tmpfs 使用的内存大小。tmpfs 其实

也是一种特殊的缓存。

  • 可用内存是新进程可以使用的最大内存,它包括剩余内存和可回收缓存。
  • 缓存包括两部分,一部分是磁盘读取文件的页缓存,用来缓存从磁盘读取的数据,可以加快以后再次访问的

速度。另一部分,则是 Slab 分配器中的可回收内存。

  • 缓冲区是对原始磁盘块的临时存储,用来缓存将要写入磁盘的数据。这样,内核就可以把分散的写集中起

来,统一优化磁盘写入。

2.进程内存使用情况 比如进程的虚拟内存、常驻内存、共享内存以及 Swap 内存等

  • 虚拟内存,包括了进程代码段、数据段、共享内存、已经申请的堆内存和已经换出的内存等。这里要注意,

已经申请的内存,即使还没有分配物理内存,也算作虚拟内存。

  • 常驻内存是进程实际使用的物理内存,不过,它不包括 Swap 和共享内存。
  • 共享内存,既包括与其他进程共同使用的真实的共享内存,还包括了加载的动态链接库以及程序的代码段等

Swap 内存,是指通过 Swap 换出到磁盘的内存

3.缺页异常

系统调用内存分配请求后,并不会立刻为其分配物理内存,而是在请求首次访问时,通过缺页异

常来分配。缺页异常又分为下面两种场景。

  • 可以直接从物理内存中分配时,被称为次缺页异常。
  • 需要磁盘 I/O 介入(比如 Swap)时,被称为主缺页异常。

4.Swap 的使用情况

如 Swap 的已用空间、剩余空间、换入速度和换出速度等

  • 已用空间和剩余空间很好理解,就是字面上的意思,已经使用和没有使用的内存空间。
  • 换入和换出速度,则表示每秒钟换入和换出内存的大小。
内存调优策略

常见的优化思路有这么几种。

1)最好禁止 Swap。如果必须开启 Swap,降低 swappiness 的值,减少内存回收时 Swap 的使用倾向。

2)减少内存的动态分配。比如,可以使用内存池、大页(HugePage)等。

3)尽量使用缓存和缓冲区来访问数据。比如,可以使用堆栈明确声明内存空间,来存储需要缓存的数据;或者用

Redis 这类的外部缓存组件,优化数据的访问。

4)使用 cgroups 等方式限制进程的内存使用情况。这样,可以确保系统内存不会被异常进程耗尽。

5)通过 /proc/pid/oom_adj ,调整核心应用的 oom_score。这样,可以保证即使内存紧张,核心应用也不会被 OOM杀死。

本文转载自: 掘金

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

MySQL这样写UPDATE语句,它能成功执行么?

发表于 2021-11-15

这是我参与11月更文挑战的第11天,活动详情查看:2021最后一次更文挑战

这样的细节错误,你遇到过么?

真是细节决定成败!

最近好几次有开发同学在钉钉上问我,比如下图:

0da3c2f81d4fdd6a23144665724ead9e.png

问题归纳起来就是:在MySQL里面update一条记录,语法都正确的,但记录并没有被更新…

刚遇到这个问题的时候,我拿到这条语句直接在测试库里面执行了一把,发现确实有问题,但和开发描述的还是 有区别 ,这里我用测试数据来模拟下:

有问题的SQL语句

39c3592bbf2b06a9dadadb6a2956aa7a.png

执行之前的记录是这样的:

a3e8e6e70214fd22f07c5ed1284e2353.png

执行之后的记录是这样的:

653d2a350b4e08a60e38c47720f4f4d5.png

可以看到,结果并不像这位开发同学说的“好像没有效果”,实际上是有效果的:

9adb8b28621ca4113453e67c52365ffc.png

why?

看起来,语法是完全没有问题,翻了翻MySQL官方文档的update语法:

0bff54d6f4efc8a5539da189bc052464.png

看到assignment_list的格式是以逗号分隔的col_name=value列表,一下子豁然开朗,开发同学想要的多字段更新语句应该这样写:

92c1a6540b2899ede14c61326acd1a06.png

倒回去再重试验一把

74349cd7ee2afe4b6ece48a2f3dac422.png

果然,这下得到了想要的结果!

小结

在一条UPDATE语句中,如果要更新多个字段,字段间不能使用“AND”,而应该用逗号分隔。

后面等有空的时候,又回过头来看了一下,为什么使用“AND”分隔的时候,会出现owner_code=0的奇怪结果?多次尝试之后发现:

90cd0efa674df5ba9ed48f377ad7dc31.png

等价于:

4e46ad41dff846625aca4f2e784daf00.png

而 (‘43212’ and owner_name=’李四’) 是一个逻辑表达式,而这里不难知道owner_name并不是‘李四’。因此,这个逻辑表达式的结果为 false , false在MySQL中等价于0!

本文转载自: 掘金

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

温故而知新,Spring事务的失效场景以及新的发现

发表于 2021-11-15

这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战

写在前面

我们都知道添加@Transactional注解便能实现事物(不考虑分布式事务),但是有些时候,却失效了,明明加着注解却没作用。

今天我们分为两部分: 温故、知新

温故,重新整理下那几种失效场景。知新,我们也能发现一点什么。接着往下看

我们先说 Spring事务的失效原因大概分为六种。

  • 数据库引擎不支持事务(我们大部分用Mysql天然支持事务,所以该情况极少见)
  • 没有被 Spring 管理
  • 方法不是 public 的
  • 自身调用问题 (常见)
  • 异常被吃了 (常见)
  • 异常类型错误

下面我们来一一举例说明:

必须被 Spring 管理

1
2
3
4
5
6
7
8
java复制代码 // @Service
public class OrderServiceImpl implements OrderService {

@Transactional
public void updateOrder(Order order) {
// update order
}
}

如果此时把 @Service 注解注释掉,这个类就不会被加载成一个 Bean,那这个类就不会被 Spring 管理了,事务自然就失效了。

方法必须是 public 的

以下来自 Spring 官方文档:

When using proxies, you should apply the @Transactional annotation only to methods with public visibility. If you do annotate protected, private or package-visible methods with the @Transactional annotation, no error is raised, but the annotated method does not exhibit the configured transactional settings. Consider the use of AspectJ (see below) if you need to annotate non-public methods.

大概意思就是 @Transactional 只能用于 public 的方法上,否则事务不会失效,如果要用在非 public 方法上,可以开启 AspectJ 代理模式。

自己不能再调自己

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Service
public class OrderServiceImpl implements OrderService {

public void update(Order order) {
updateOrder(order);
}

@Transactional
public void updateOrder(Order order) {
// update order 逻辑
}
}

update方法上面没有加 @Transactional 注解,调用有 @Transactional 注解的 updateOrder 方法,updateOrder 方法上的事务管用吗?

答案是不管用的,因为它们发生了自身调用,就调该类自己的方法,而没有经过 Spring 的代理类,默认只有在外部调用事务才会生效,这也是老生常谈的经典问题了。

解决方案:

1
2
3
4
5
6
java复制代码@Resource
private UpdateOrder updateOrder;

public void update(Order order) {
updateOrder.updateOrder(order);
}

这样就就交给spring管理了。

不能try catch

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码@Service
public class OrderServiceImpl implements OrderService {

@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {

}
}
}

我们经常这样写代码。但是事务会失效,原因是异常被吃掉了。人家事务就是根据你的异常去回滚事物的 结果你给吃掉了。

但是我们可以抛出来。在catch里面 抛出来 就可以解决 但是也不是任何抛异常都可以的。接下来看异常类型错误

异常类型需谨慎

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class OrderServiceImpl implements OrderService {

@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
throw new Exception("更新错误");
}
}

}

这样事务也是不生效的,因为默认回滚的是:RuntimeException,如果你想触发其他异常的回滚,需要在注解上配置一下,如:

1
java复制代码@Transactional(rollbackFor = Exception.class)

好了 失效场景就讲到这里,我们接下来考虑一件事情,就是我们能不能在try catch情况下也能实现事务,答案是可以的。我们接下来看新的发现

新的解决方案

在上面事务回滚的前提是添加@Transactional注解的方法中不含有try{…}catch{…}捕获异常,使得程序运行过程中出现异常能顺利抛出,从而触发事务回滚。

但是在实际开发中,我们往往需要在方法中进行异常的捕获,从而对异常进行判断,为客户端返回提示信息。但是此时由于异常的被捕获,导致事务的回滚没有被触发,导致事务的失败。

那么这该怎么做呢,下面给出三种办法

1、使用@Transactional注解,抛出@Transactional注解默认识别的RuntimeException

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class OrderServiceImpl implements OrderService {

@Transactional
public void updateOrder(Order order) {
try {
// update order
} catch {
logger.error("更改订单失败")
throw new RuntimeException("RuntimeException");
}
}
}

2、使用@Transactional(rollbackFor = { Exception.class }),也能抛出捕获的非RuntimeException异常

方法上使用@Transactional(rollbackFor = { Exception.class })注解声明事务回滚级别,在捕获到异常时在catch语句中直接抛出所捕获的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
java复制代码@Service
public class OrderServiceImpl implements OrderService {

@Transactional(rollbackFor = { Exception.class })
public void updateOrder(Order order) {
try {
// update order
} catch {
logger.error("更改订单失败")
throw e;
}
}
}

不知道有没有发现上面两个在catch{…}中抛出异常的方法都有个不足之处,就是不能在catch{…}中存在return子句,比如我们需要一些return一些信息,那么就必须设置手动回滚,当捕获到异常时,手动回滚,同时返回前台提示信息。

手动回滚

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码@Service
public class OrderServiceImpl implements OrderService {

@Transactional
public int updateOrder(Order order) {
try {
// update order
} catch {
logger.error("更改订单失败")
//手动回滚
TransactionAspectSupport.currentTransactionStatus()
.setRollbackOnly();
return 0;
}
return 1;
}
}

OK。今天的讲解就到这里,我们下期再见

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

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

C语言详解:指针

发表于 2021-11-15

这是我参与11月更文挑战的第7天,活动详情查看:2021最后一次更文挑战

指针

这次的指针(Pointer),比初识C语言里的指针更深入一点,但也不是全部内容,因为后面的进阶部分还会讲到。

指针定义

内存划分

内存是一块很大的空间,由一个个小的占一字节的内存单元组成,每一个内存单元对应绑定着一个地址,即对内存单元的编号。像是身份证号一样,通过地址我们就可以唯一确定地找到一块内存单元。如:

内存单元示例

指针与指针变量

地址直接指向了存储在内存的另一个值。由于能通过地址找到所需的变量单元,地址指向了唯一确定的内存单元,故将地址形象化称为指针。

现在我们定义了一个整型变量a,在内存中给他分配了4个字节。由此我们也能看出定义变量的本质就是在内存中分配空间。变量a的第一个字节的地址为0x0012ff40,它就代表变量a的地址。

指针变量示例

那什么是指针变量呢?现在我们去定义一个“指针”指向一个变量a。

我们用&a把变量a的地址取出来,再放到变量pa中,由于变量pa中存的是地址,所以用类型int *去定义变量pa。

1
c复制代码int * pa = &a;

这样变量pa也是真实存在于内存中的一个变量,其中存储的是地址编号。这样的变量叫指针变量。

总结
  1. 指针即地址,地址即指针。
  2. 指针变量是存放地址的变量,其中的内容都被当作地址处理。

指针变量经常被人们简称为指针,我们要去从语境中区分他人说的是指针还是指针变量。

指针大小
  • 一个内存单元有多大?
  • 地址是如何进行编号?

首先我们分析一下,内存单元的大小为什么是一个字节。

对于32位机器,即32根地址线,每一个地址线在寻址时产生的电信号(正电/负电)转化为数字信号 ,正点就是1,负电就是0。更通俗来说,通电即为1,没通电就是0。

那么32根地址线有多少种01组合呢,高中的排列知识就可以说明共有 2322^{32}232 种01序列。即从32个全0到32个全1。

00000000 00000000 00000000 00000000

00000000 00000000 00000000 00000001

… …

11111111 11111111 11111111 11111110

11111111 11111111 11111111 11111111

当然64位机器,就有 2642^{64}264 种排列组合。

既然我们32位机器上,有 2322^{32}232 种排列组合。

每一个二进制序列就是一个内存单元的编号,那么就有 2322^{32}232 个内存单元可供使用,转化为十进制就是4,294,967,2964,294,967,2964,294,967,296。

如果每个内存单元是1bit大小的话,那么除以8就有536,870,912536,870,912536,870,912个byte,就有524,288524,288524,288个kb,再除以1024就是我们熟悉的512512512个MB,约含半个GB。这样的话一个char类型的变量就需要8个地址,是不是太浪费了?

如果每个内存单元是1个byte的话,转化到最后正好是4个GB。这就正好了,最早期的时候只有1个或者2个GB。

指针变量用来存储地址,一个地址就是32个比特位,那么正好需要4个字节。所以无论是什么类型,指针变量的大小都是4个字节。

当然,32位机器指针的大小为4个字节,64位机器下指针大小为8个字节。

指针类型

1
2
c复制代码int a = 10;
int * pa = &a;
  • * 代表 pa 是指针
  • int 代表pa所指向的变量类型为int

变量有不同的类型,很明显指针变量也有不同的类型。可是依据前面的推导,不管什么类型的指针变量,32位平台下大小都是4个字节,那指针的类型有什么作用呢?体现在两个方面,一是指针解引用,二是指针加减整数。

指针解引用方面
1
2
3
c复制代码int a = 0x11223344;
int* pa = &a;
*pa = 0;

我们先创建一个变量a,并用指针变量pa指向它,然后再对pa解引用把a置为0,我们可以从内存中看到:

指针类型作用

这个结果大家都能猜到,那么接下来我们对指针变量的类型稍作修改,把int * pa改成char * pa。

改变指针类型作用2

这样的话,区别就有了,int* 的指针访问并修改了4个字节的内容,而char* 的指针只修改了1个字节的内容。

指针±正数方面

现在我们再用不同类型的指针分别指向同一个变量,对其+1。如:

不同类型指针+-整数示例

可以看到int* 型的指针+1向后跳过了4个字节,char* 型的指针+1向后跳过了1个字节。

总结

指针类型决定了:

  1. 指针解引用操作时能够访问的字节(内存大小)。
  2. 指针±整数时能跳过几个字节(步长)。

这样的话,我们用不同类型的指针,就可以实现跳过不同的字节,继而更细致的访问变量内容。如:

1
2
3
4
5
6
7
8
9
c复制代码int arr[10] = { 0 };
//1.
int* pa = arr;
//2.
char * pa = arr;
for (int i = 0; i < 10; i++)
{
*(pa + i) = 1;
}

两种不同的指针,带来不同的效果,如图所示:

改变指针类型遍历数组作用示例.

第一种是一个整型一个整型访问数组元素,第二个是一个字符一个字符地访问数组。如:

改变指针类型遍历数组示意图

野指针

野指针定义

指向不明确的位置(随机的,不正确的,无明确限制的)的指针是野指针。

不正确的位置:指向了没有分配的内存空间,造成越界访问。

野指针成因
  1. 指针未初始化
1
2
c复制代码 int* p;//未初始化 
*p = 20;
  1. 指针越界访问
1
2
3
4
5
6
c复制代码int arr[10] = { 0 };
int* p = arr;
for (int i = 0; i <= 10; i++)//越界访问
{
*(p + i) = i;
}

越界可以,但不能越界访问^_^。

  • 例题
1
2
3
4
5
6
7
8
9
c复制代码 int* test(){
int a = 10;
return &a;
}
int main(){
int* p = test();
printf("%d\n", *p);//野指针越界访问
return 0;
}

这里的a是test函数中定义的,出了作用域就会被销毁,所以我们这里打印*p就属于越界访问。

但我们这执行程序仍能发现结果是10,这是为什么呢?

原因是a变量所占的空间回收后操作系统还未将其销毁,编译器对其作一次保留。而且传参先行于调用,所以再调用printf函数之前就*p就已经替换为10。

  • 我们稍作修改,在打印*p的前面再调用一次printf函数,如:
1
2
3
4
5
6
7
8
9
10
c复制代码int* test(){
int a = 10;
return &a;
}
int main(){
int* p = test();
printf("hehe\n");
printf("%d\n", *p);
return 0;
}

这次调用printf函数,使得原来分配给a的空间被覆盖,又分配给了printf函数。栈区的使用习惯就是压栈弹栈(如果不了解的话可以去看看栈区空间的开辟和销毁)。

  • 那如果我们把打印printf("%d\n", *p);改为赋值语句*p = 20;的话,如:
1
2
3
4
5
6
7
8
9
c复制代码int* test(){
int a = 10;
return &a;
}
int main(){
int* p = test();
*p = 20;//访问非法内存
return 0;
}

编译器就直接检测出这块空间是非法内存,就会直接报错。

  1. 指针指向空间已释放

从上面的例子也可以看出,指针p指向test函数原先占有的已被释放的内存空间,这也是一件非常危险的事情,必然会成为野指针。动态内存开辟的地方也会将指向动态开辟的内存的指针free掉,这也是防止其成为野指针。

如何规避野指针
  1. 明确指针初始化,确定指向
1
2
c复制代码 int* p = &a;
int* p =NULL;//不知道该指向何处时,置为空NULL
  1. 谨防指针越界
  2. 指针指向空间释后,立即置为NULL
  3. 避免函数返回局部变量地址
  4. 检查指针有效性

空指针不可解引用。

1
2
3
c复制代码if(p != NULL){
*p=20;//检验不为空指针,再使用
}

或者直接用assert断言函数,assert(p)判断指针p是否为空指针,如有误返回错误信息。

指针运算

当然指针的解引用操作也算是指针的运算,但我们这里仅考虑一下三类,毕竟指针解引用是基本运算。

指针加减整数所得还是指针,就像日期加天数后还是日期。而指针减指针所得为元素个数,就像日期减日期为天数,从这个例子也可以看出来指针加指针是没有意义的。

指针+ -整数
1
2
3
4
5
6
c复制代码float values[N_VALUE];
float* vp = values;
for (vp = &values[0]; vp < &values[N_VALUE];)
{
*vp++=0;
}

上述代码,依靠指向数组的指针循环遍历置零。

  • 循环体内,vp先++后*,尽管++的优先级比*要高,但是后置++是先使用再++。

所以仿佛是对指针先解引用再++的。

  • float类型指针的加一,跳过一个float类型的长度,故跳到下一个元素。

不论指针是什么类型,指针++,都是跳过一个类型的长度。

  • 当vp指向数组最后一个元素其后的地址,不满足条件,结束循环。

该地址虽不属于数组,但仅是用所判断大小的条件(地址有高低),没有访问该地址的内容,所以不算越界访问。

指针++遍历数组示例

本例子涉及到了两个指针的运算:指针加减整数,也就是指针++。指针的关系运算,指针相互比大小作判断条件。

指针加整数即指针向后跳整数个类型大小的字节,再来看看指针减整数。

指针-整数代码示例

如图所示,指针p-1就是指针p向前跳一个类型的大小。

指针加减整数即指针向后或向前跳过整数个类型大小的字节。

指针-指针

指针可以减去指针,代表两个地址之间的”差距“。那可以用指针加上指针吗?相当于两个地址相加是没有意义的。

1
2
c复制代码int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
printf("%d\n", &arr[9] - &arr[0]);

这题答案是什么?是36还是9?

答案是9,语法规定指针-指针,得到的是两地址之间的元素个数(下标相减)。

当然两地址间的元素个数,也可以理解为所占字节大小除以类型大小。

那要是在不同的数组中运算呢?如:

1
2
3
c复制代码int arr[] = { 1,2,3,4,5,6,7,8,9,10 };
char ch[] = { '1','2','3' };
printf("%d\n", &arr[9] - &ch[0]);

编译器不会报错,因为没有语法错误。但是所得到的数字即元素的个数,该元素是int类型还是char类型的呢?所以这数字根本就是没有意义的。

所以我们得到指针相减运算的前提:是两指针指向同一块空间,如同一个数组。

  • 指针-指针的前提:是两指针指向同一块空间。
  • 指针-指针,得到的数字的绝对值是两地址之间的元素个数。

应用:实现strlen函数

1
2
3
4
5
c复制代码int my_strlen(const char* s){
char* begin = s;//标记开头
while(*s++);//s先++再判断是否为\0
return s - begin - 1;//指针相减
}
指针关系运算

将指针加减整数代码例子拿过来稍作修改。

1
2
3
4
c复制代码//1.
for(vp = &values[N_VALUE];vp > &values[0];){
*--vp = 0;
}

把数组后面的空间,也想象成数组内容根据数组下标拿取是可以的,毕竟数组在内存中是连续存放的。从后往前遍历,先--再解引用,就不会造成数组越界访问。我们再稍作修改:

1
2
3
4
c复制代码//2.
for(vp = &values[N_VALUE-1];vp >= &values[0];vp--){
*vp = 0;
}

最后一次遍历时,指针指向values[0]前面的一块地址,当然再回来判断时不满足条件,就退出循环。

但是我们要尽量选择第一种方法,因为C语言标准规定:允许指向数组的指针,与指向数组最后元素之后的内存位置进行比较,但不允许与首元素之前的位置进行比较。如图:

在这里插入图片描述

原因是编译器可能会在数组前的位置存储和数组有关的信息,如数组元素个数等。这样可能会影响到程序的运行。

指针也是地址,地址是编号是数字,就可以进行比较大小。指针的关系运算就是比较大小。

指针和数组

指针和数组之间有什么区别,有什么联系吗?

  • 数组是一个相同类型元素的集合,其中元素存放在连续的空间中。数组的大小取决于元素类型和元素个数。
  • 指针存储地址,是一个变量。指针的大小固定为4 (32bit) / 8 (64bit)。
1
2
3
c复制代码int arr[10] = { 0 };
printf("%p\n", arr);//0x0012ff40
printf("%p\n", &arr[0]);//0x0012ff40

由此可得:数组名就是数组首元素的地址。

ps:以下两种情况数组名代表整个数组,除该两种情况外,数组名都代表首元素地址。

  1. sizeof(arr)
  2. &arr

数组名可以作为地址,存放在指针变量中,我们就可以通过指针访问数组。

事实上,数组作函数形参时,都是降级优化为指针的,一整个数组是传不过去的。不过这也是后话了。

1
2
3
4
5
6
c复制代码int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };
int* p = arr;
for (int i = 0; i < sz; i++)
{
printf("&arr[%d] = %p <===> p+%d = %p\n", i, &arr[i], i, p + i);
}

指针指向数组取地址示例

也就是说,p+i其实就是数组arr下标为i的地址,本质上二者就是一回事。

二级指针

顾名思义,二级指针就是用来存放一级指针的地址的,通过二级指针也可以访问到一级指针。

二级指针内存存储示例

  1. 首先创建了一个变量a,存了10,所以它的类型为int ,变量的地址为0x0012ff40。
  2. 然后取出a的地址,再创建了一个新的变量pa,并把&a存了进去,所以它的类型为int*(一级指针),变量的地址为0x004ffabc。
  3. 最后又创建了一个新的变量ppa,把&p存了进去,所以它的类型为int**(二级指针)。

通过ppa里p的地址,可以找到p,通过p里a的地址,也可以找到a。

类型中“*”的含义

多级指针类型中的星含义示例

灰框中的*代表变量是一个指针变量。

  • 一级指针p前面的int表示p指向的对象a是int型的。
  • 二级指针pp前面的int*表示pp指向的对象p的类型是int*型的。
  • 三级指针ppp前面的int**表示ppp指向的对象pp的类型是int**型的。
多级指针解引用操作
1
2
3
c复制代码*p = 1;
* *pp = 2;
* * *ppp = 3;

如上述代码所示,我们一级一级分析。

  • 对一级指针p解引用*p,找到a。
  • 对二级指针pp解引用*pp,找到p,再解引用**pp,找到a。
  • 对三级指针ppp解引用*ppp找到pp,再解引用**ppp,找到p,再解一次引用***ppp,找到a。

所以可以看出,有多少级指针,就要解多少次引用。

指针数组

指针数组定义

在回答何为指针数组前,我们先来看何为整型数组,何为字符数组。

1
2
3
4
c复制代码int arr[10] = {0};
//整型数组 - 存放整型变量的数组
char ch[10] = {'0'};
//字符数组 - 存放字符变量的数组

通过类比整型数组和字符数组,可以得到指针数组就是存放指针变量的数组。

数组名前的类型int和char表示,数组元素的类型是int或者char。所以指针数组名前的类型名就是int*或者是char*。如:

1
2
3
4
c复制代码//整型指针数组
int* parr[10];
//字符型指针数组
char* pch[5];

对于整型指针数组,每个元素都是整型变量的地址,对于字符型指针数组,每个元素都是字符型变量的地址。由此也可以看出,指针数组的大小,仅取决于数组元素个数。

指针数组存储示例

指针数组使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
c复制代码int arr[] = { 10,20,30 };
int* parr[5] = {NULL};
//输入
for (int i = 0; i < 3; i++)
{
parr[i] = &arr[i];
}
//输出1.
for (int i = 0; i < 3 ; i++)
{
printf("%d ", *parr[i]);
}
//输出2.
for (int i = 0; i < 3; i++)
{
printf("%d ", **(parr+i));
}
  1. 切记要么初始化要么指定大小。指针数组记得内容初始化为空指针。
  2. 指针数组遍历数组元素打印时,记得要解引用。用数组名+i遍历数组元素时,就要解两层引用。

目前对应指针数组就理解到这个层次,后续还会学习指针的进阶。

本文转载自: 掘金

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

SpringBoot整合阿里云OSS对象存储

发表于 2021-11-15

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」。

前言

附件上传相信大家都了解的,整合阿里云OSS对象存储时踩了点小坑。这里记录正确整合步骤,方便以后快速整合。官方文档,感兴趣大家可以自行阅读,这里不赘述了。

附件上传

常见的上传逻辑应该是,Web端上传文件到应用服务器,应用服务器再把文件上传到OSS。具体流程如下图所示。

image.png

但是这种方案存在以下缺点:

  • 速度慢,经过两道传输,时间起码增加一倍
  • 浪费性能,万一用户群体变大,服务器将成为我们的瓶颈

最佳方案 客户端签名直传

由于OSS上行流量是免费的,如果数据直传到OSS,速度会大幅缩减,并且节约了服务器资源,缓解了服务端压力。
image.png

  1. 用户向应用服务器请求上传Policy。
  2. 应用服务器返回上传Policy和签名给用户。
  3. 用户直接向OSS发送文件上传请求。

整合步骤

1. 开通阿里云OSS对象存储服务

这个就不介绍了,登录阿里云平台,跟着引导操作就行

2. 获取以下四个参数信息

  • endpoint
  • accessKeyId
  • accessKeySecret
  • bucketName
  • endpoint*
    点击Bucket列表 –> 新建Bucket –> 填好bucket名称 –> 选择地域 –> 根据地域不同会给出不同的endpoint
    image.png
    到这里我们已经有了bucketName、 endpoint

如果忘记了可以在以下路径查找
image.png

accessKeyId和accessKeySecret

点击右上角账户头像 –> 点击AccessKey管理

image.png

通过创建子账号的方式创建一个只有OSS相关权限的用户

image.png

创建用户

image.png

创建accesskey,生成完一定要记录下来,页面关掉就只能重新生成了
image.png

给用户添加权限

image.png

添加读写权限
image.png

到这里所有准备工作就完成了,接下来代码整合

SpringBoot代码整合

1. 引入依赖

1
2
3
4
5
6
xml复制代码<!--   阿里云oss存储     -->
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>3.13.2</version>
</dependency>

2. 配置文件定义属性值

application.yml中添加配置属性

1
2
3
4
5
6
7
yml复制代码# 阿里云oss
aliyun:
oss:
endpoint: oss-cn-shanghai.aliyuncs.com
accessKeyId: LTAI5tH********Cr88dn
accessKeySecret: ayKgKy4BY********0gLaUI9
bucketName: yourname

3. 添加配置对象

自动配置OSSClient对象

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

@Value("${aliyun.oss.endpoint}")
private String endpoint;

@Value("${aliyun.oss.bucketName}")
private String bucketName;

@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;

@Value("${aliyun.oss.accessKeySecret}")
private String accessKeySecret;

@Bean
public OSS getOSSClient() {
return new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
}
}

4. 编写获取policy接口

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
java复制代码@RestController
@RequestMapping("/oss")
public class OSSController {

@Autowired
OSS ossClient;

@Value("${aliyun.oss.endpoint}")
private String endpoint;

@Value("${aliyun.oss.bucketName}")
private String bucketName;

@Value("${aliyun.oss.accessKeyId}")
private String accessKeyId;

@ApiOperation("获取签名policy")
@GetMapping("/policy")
public Map<String, String> policy() {
// host的格式为 bucketname.endpoint
String host = "https://" + bucketName + "." + endpoint;
// callbackUrl为上传回调服务器的URL,请将下面的IP和Port配置为您自己的真实信息。
// String callbackUrl = "http://88.88.88.88:8888";
String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
// 按日期分文件夹存储
String dir = today + "/"; // 用户上传文件时指定的前缀。
Map<String, String> respMap = null;
try {
long expireTime = 30;
long expireEndTime = System.currentTimeMillis() + expireTime * 1000;
Date expiration = new Date(expireEndTime);
// PostObject请求最大可支持的文件大小为5 GB,即CONTENT_LENGTH_RANGE为5*1024*1024*1024。
PolicyConditions policyConds = new PolicyConditions();
policyConds.addConditionItem(PolicyConditions.COND_CONTENT_LENGTH_RANGE, 0, 1048576000);
policyConds.addConditionItem(MatchMode.StartWith, PolicyConditions.COND_KEY, dir);

String postPolicy = ossClient.generatePostPolicy(expiration, policyConds);
byte[] binaryData = postPolicy.getBytes(StandardCharsets.UTF_8);
String encodedPolicy = BinaryUtil.toBase64String(binaryData);
String postSignature = ossClient.calculatePostSignature(postPolicy);

respMap = new LinkedHashMap<String, String>();
respMap.put("accessid", accessKeyId);
respMap.put("policy", encodedPolicy);
respMap.put("signature", postSignature);
respMap.put("dir", dir);
respMap.put("host", host);
respMap.put("expire", String.valueOf(expireEndTime / 1000));
// respMap.put("expire", formatISO8601Date(expiration));
} catch (Exception e) {
// Assert.fail(e.getMessage());
System.out.println(e.getMessage());
} finally {
ossClient.shutdown();
}
return respMap;
}
}

到这里服务端的工作就完成了

5. 测试

调用获取policy接口返回

1
2
3
4
5
6
7
8
json复制代码{
"accessid": "LTAI5tHZ1ro3zUUuZmCr88dn",
"policy": "eyJleHBpcmF0aW9uIj1iMjAyMS0xMS0xNVQwOTozODowOS4zOThaIiwiY29uZGl0aW9ucyI6W1siY29udGVudC1sZW5ndGgtcmFuZ2UiLDAsMTA0ODU3NjAwMF0sWyJzdGFydHMtd2l0aCIsIiRrZXkiLCIyMDIxLTExLTE1LyJdXX0=",
"signature": "3K/6hXZPCYCNBwUFBaec7CmAa70=",
"dir": "2021-11-15/",
"host": "https://thinkfon-member.oss-cn-shanghai.aliyuncs.com",
"expire": "1636969089"
}
字段 描述
accessid 用户请求的AccessKey ID。
host 用户发送上传请求的域名。
policy 用户表单上传的策略(Policy),Policy为经过Base64编码过的字符串。详情请参见Post Policy。
signature 对Policy签名后的字符串。详情请参见Post Signature。
expire 由服务器端指定的Policy过期时间,格式为Unix时间戳(自UTC时间1970年01月01号开始的秒数)。
dir 限制上传的文件前缀。

本文转载自: 掘金

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

SpringBoot系列之拦截器注入Bean的几种姿势

发表于 2021-11-15

「这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战」。

之前介绍过一篇拦截器的基本使用姿势: 【WEB系列】SpringBoot之拦截器Interceptor使用姿势介绍

在SpringBoot中,通过实现WebMvcConfigurer的addInterceptors方法来注册拦截器,那么当我们的拦截器中希望使用Bean时,可以怎么整?

I. 项目搭建

本项目借助SpringBoot 2.2.1.RELEASE + maven 3.5.3 + IDEA进行开发

开一个web服务用于测试

1
2
3
4
5
6
7
xml复制代码<dependencies>
<!-- 邮件发送的核心依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

II.拦截器

实现拦截器比较简单,实现HandlerInterceptor接口就可以了,比如我们实现一个基础的权限校验的拦截器,通过从请求头中获取参数,当满足条件时表示通过

0.安全校验拦截器

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
java复制代码@Slf4j
public class SecurityInterceptor implements HandlerInterceptor {
/**
* 在执行具体的Controller方法之前调用
*
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 一个简单的安全校验,要求请求头中必须包含 req-name : yihuihui
String header = request.getHeader("req-name");
if ("yihuihui".equals(header)) {
return true;
}

log.info("请求头错误: {}", header);
return false;
}

/**
* controller执行完毕之后被调用,在 DispatcherServlet 进行视图返回渲染之前被调用,
* 所以我们可以在这个方法中对 Controller 处理之后的 ModelAndView 对象进行操作。
* <p>
* preHandler 返回false,这个也不会执行
*
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info("执行完毕!");
response.setHeader("res", "postHandler");
}


/**
* 方法需要在当前对应的 Interceptor 类的 preHandle 方法返回值为 true 时才会执行。
* 顾名思义,该方法将在整个请求结束之后,也就是在 DispatcherServlet 渲染了对应的视图之后执行。此方法主要用来进行资源清理。
*
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info("回收");
}
}

接下来是这个拦截器的注册

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码@RestController
@SpringBootApplication
public class Application implements WebMvcConfigurer {

public static void main(String[] args) {
SpringApplication.run(Application.class);
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SecurityInterceptor()).addPathPatterns("/**");
}

@GetMapping(path = "show")
public String show() {
return UUID.randomUUID().toString();
}
}

接下来问题来了,我们希望这个用于校验的值放在配置文件中,不是在代码中写死,可以怎么整?

1. 指定配置

在项目资源文件中,添加一个配置用于表示校验的请求头

application.yml

1
2
yaml复制代码security:
check: yihuihui

配置的读取,可以使用 Envrioment.getProperty(),也可以使用 @Value注解

但是注意上面的拦截器注册,直接构造的一个方法,添加到InterceptorRegistry,在拦截器中,即使添加@Value, @Autowired注解也不会生效(归根结底就是这个拦截器并没有受Spring上下文管理)

2. 拦截器注入Bean

那么在拦截器中如果想使用Spring容器中的bean对象,可以怎么整?

2.1 新增静态的ApplicationContext容器类

一个可行的方法就是在项目中维护一个工具类,其内部持有ApplicationContext的引用,通过这个工具类来访问bean对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Component
public class SpringUtil implements ApplicationContextAware, EnvironmentAware {
private static ApplicationContext applicationContext;
private static Environment environment;

@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtil.applicationContext = applicationContext;
}

@Override
public void setEnvironment(Environment environment) {
SpringUtil.environment = environment;
}

public static <T> T getBean(Class<T> clz) {
return applicationContext.getBean(clz);
}

public static String getProperty(String key) {
return environment.getProperty(key);
}
}

基于此,在拦截器中,如果想要获取配置,直接改成下面这样既可

1
2
3
4
5
6
7
8
9
10
11
java复制代码@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 一个简单的安全校验,要求请求头中必须包含 req-name : yihuihui
String header = request.getHeader("req-name");
if (Objects.equals(SpringUtil.getProperty("security.check"), header)) {
return true;
}

log.info("请求头错误: {}", header);
return false;
}

这种方式来访问bean,优点就是通用性更强,适用范围广

2.2 拦截器注册为bean

上面的方法虽然可行,但是看起来总归不那么优雅,那么有办法直接将拦截器声明为bean对象,然后直接使用@Autowired注解来注入依赖的bean么

当然是可行的,注意bean注册的几种姿势,我们这里采用下面这种方式来注册拦截器

1
2
3
4
5
6
7
8
9
java复制代码@Bean
public SecurityInterceptor securityInterceptor() {
return new SecurityInterceptor();
}

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(securityInterceptor()).addPathPatterns("/**");
}

上面通过配置类的方式来声明bean,然后在注册拦截器的地方,不直接使用构造方法来创建实例;上面的用法表示是使用spring的bean容器来注册,基于这种方式来实现拦截器的bean声明

因此在拦截器中就可以注入其他依赖了

测试就比较简单了,如下

1
2
3
4
5
6
7
bash复制代码yihui@M-162D9NNES031U:SpringBlog git:(master) $ curl 'http://127.0.0.1:8080/show' -H 'req-name:yihuihui' -i
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 36
Date: Mon, 15 Nov 2021 10:56:30 GMT

6610e593-7c60-4dab-97b7-cc671c27762d%

3. 小结

本文虽说介绍的是如何在拦截器中注入bean,实际上的知识点依然是创建bean对象的几种姿势;上面提供了两种常见的方式,一个SpringUtil持有SpringContext,然后借助这个工具类来访问bean对象,巧用它可以省很多事;

另外一个就是将拦截器声明为bean,这种方式主要需要注意的点是拦截器的注册时,不能直接new 拦截器;当然bean的创建,除了上面这个方式之外,还有其他的case,有兴趣的小伙伴可以尝试一下

III. 不能错过的源码和相关知识点

0. 项目

  • 工程:github.com/liuyueyi/sp…
  • 源码:github.com/liuyueyi/sp…

1. 微信公众号: 一灰灰Blog

尽信书则不如,以上内容,纯属一家之言,因个人能力有限,难免有疏漏和错误之处,如发现bug或者有更好的建议,欢迎批评指正,不吝感激

下面一灰灰的个人博客,记录所有学习和工作中的博文,欢迎大家前去逛逛

  • 一灰灰Blog个人博客 blog.hhui.top
  • 一灰灰Blog-Spring专题博客 spring.hhui.top

本文转载自: 掘金

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

1…335336337…956

开发者博客

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