什么是csrf?

CSRF,全称为Cross-Site Request Forgery(跨站请求伪造),是一种Web安全攻击。在CSRF攻击中,攻击者通过诱使受害者在已登录的情况下,访问一个恶意网站或点击恶意链接,来利用受害者的身份在受攻击网站上执行未经授权的操作。

具体来说,CSRF攻击利用了用户的浏览器对于同一站点的认可,即使用户在访问正常站点时是经过授权的(例如登录了一个网站),但用户在访问其他站点时,浏览器仍然会发送相同的认证信息(如Cookie),从而被攻击者利用来执行攻击。攻击者可以以受害者的名义执行例如转账、更改密码等操作,而受害者可能对此毫无察觉。

如何防范CSRF?

为了防范CSRF攻击,常见的措施包括:

  1. CSRF令牌(Token): 服务器生成一个随机的令牌,嵌入到每个表单或者每个请求中,攻击者由于无法获取到这个随机的令牌,无法构造一个有效的请求。

  2. SameSite Cookie属性: 控制浏览器是否在跨站点请求时发送Cookie,可以设置为Strict或者Lax以限制Cookie的发送。

  3. Referer检查: 服务器验证请求来源的Referer头部,但这种方法可被伪造和篡改。

  4. 双重提交Cookie: 将一个随机生成的Cookie和Form表单中的字段进行比较

这篇文章讲解csrf令牌?

在使用Spring Security进行CSRF保护时,确实需要在每个请求中携带CSRF令牌,并在服务器端验证令牌的正确性。以下是关键步骤和策略:

1. 生成和携带CSRF令牌

当用户登录成功后,Spring Security会生成一个CSRF令牌,并将其包含在响应中,通常是作为Cookie的一部分(名为`XSRF-TOKEN`)

- 客户端(通常是浏览器)收到这个令牌后,会将它存储起来,并在后续的请求中自动发送给服务器。

2. 在请求中包含CSRF令牌

在每个涉及到修改数据或执行敏感操作的请求中,客户端需要将CSRF令牌作为参数或者请求头的一部分发送给服务器。

通常情况下,Spring Security会期望CSRF令牌以名为 _csrf 的参数名发送,或者作为名为 X-XSRF-TOKEN 的请求头发送。

3. 服务器端验证CSRF令牌

Spring Security会在后台进行CSRF令牌的验证。验证主要是通过比对请求中的CSRF令牌和服务器端存储的令牌(通常是存储在Cookie中)来实现的。

在Spring Security的配置中,设置了CsrfTokenRequestAttributeHandlerCookieCsrfTokenRepository 来处理和存储CSRF令牌。具体来说:

CsrfTokenRequestAttributeHandler 用于处理请求中的CSRF令牌参数

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
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.csrf;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.function.Supplier;
import org.springframework.util.Assert;

public class CsrfTokenRequestAttributeHandler implements CsrfTokenRequestHandler {
private String csrfRequestAttributeName = "_csrf";

public CsrfTokenRequestAttributeHandler() {
}

public final void setCsrfRequestAttributeName(String csrfRequestAttributeName) {
this.csrfRequestAttributeName = csrfRequestAttributeName;
}

public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> deferredCsrfToken) {
Assert.notNull(request, "request cannot be null");
Assert.notNull(response, "response cannot be null");
Assert.notNull(deferredCsrfToken, "deferredCsrfToken cannot be null");
request.setAttribute(HttpServletResponse.class.getName(), response);
CsrfToken csrfToken = new SupplierCsrfToken(deferredCsrfToken);
request.setAttribute(CsrfToken.class.getName(), csrfToken);
String csrfAttrName = this.csrfRequestAttributeName != null ? this.csrfRequestAttributeName : csrfToken.getParameterName();
request.setAttribute(csrfAttrName, csrfToken);
}

private static final class SupplierCsrfToken implements CsrfToken {
private final Supplier<CsrfToken> csrfTokenSupplier;

private SupplierCsrfToken(Supplier<CsrfToken> csrfTokenSupplier) {
this.csrfTokenSupplier = csrfTokenSupplier;
}

public String getHeaderName() {
return this.getDelegate().getHeaderName();
}

public String getParameterName() {
return this.getDelegate().getParameterName();
}

public String getToken() {
return this.getDelegate().getToken();
}

private CsrfToken getDelegate() {
CsrfToken delegate = (CsrfToken)this.csrfTokenSupplier.get();
if (delegate == null) {
throw new IllegalStateException("csrfTokenSupplier returned null delegate");
} else {
return delegate;
}
}
}
}

CookieCsrfTokenRepository 用于存储和检索CSRF令牌,它默认将CSRF令牌存储在名为 XSRF-TOKEN 的Cookie中,并在需要时使用该Cookie中的令牌值来验证请求中的CSRF令牌

可以查看源码

1
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()))

这里是默认的存储和检索csrf令牌的方式,我们进去之后

可以看到返回一个CookieCsrfTokenRepository对象,我们在查看这个对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.security.web.csrf;

import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.util.UUID;
import java.util.function.Consumer;
import org.springframework.http.ResponseCookie;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import org.springframework.web.util.WebUtils;

public final class CookieCsrfTokenRepository implements CsrfTokenRepository {
static final String DEFAULT_CSRF_COOKIE_NAME = "XSRF-TOKEN";
static final String DEFAULT_CSRF_PARAMETER_NAME = "_csrf";
static final String DEFAULT_CSRF_HEADER_NAME = "X-XSRF-TOKEN";
private static final String CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME = CookieCsrfTokenRepository.class.getName().concat(".REMOVED");
private String parameterName = "_csrf";
private String headerName = "X-XSRF-TOKEN";
private String cookieName = "XSRF-TOKEN";
private boolean cookieHttpOnly = true;
private String cookiePath;
private String cookieDomain;
private Boolean secure;
private int cookieMaxAge = -1;
private Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer = (builder) -> {
};

public CookieCsrfTokenRepository() {
}

public void setCookieCustomizer(Consumer<ResponseCookie.ResponseCookieBuilder> cookieCustomizer) {
Assert.notNull(cookieCustomizer, "cookieCustomizer must not be null");
this.cookieCustomizer = cookieCustomizer;
}

public CsrfToken generateToken(HttpServletRequest request) {
return new DefaultCsrfToken(this.headerName, this.parameterName, this.createNewToken());
}

public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) {
String tokenValue = token != null ? token.getToken() : "";
ResponseCookie.ResponseCookieBuilder cookieBuilder = ResponseCookie.from(this.cookieName, tokenValue).secure(this.secure != null ? this.secure : request.isSecure()).path(StringUtils.hasLength(this.cookiePath) ? this.cookiePath : this.getRequestContext(request)).maxAge(token != null ? (long)this.cookieMaxAge : 0L).httpOnly(this.cookieHttpOnly).domain(this.cookieDomain);
this.cookieCustomizer.accept(cookieBuilder);
Cookie cookie = this.mapToCookie(cookieBuilder.build());
response.addCookie(cookie);
if (!StringUtils.hasLength(tokenValue)) {
request.setAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME, Boolean.TRUE);
} else {
request.removeAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME);
}

}

public CsrfToken loadToken(HttpServletRequest request) {
if (Boolean.TRUE.equals(request.getAttribute(CSRF_TOKEN_REMOVED_ATTRIBUTE_NAME))) {
return null;
} else {
Cookie cookie = WebUtils.getCookie(request, this.cookieName);
if (cookie == null) {
return null;
} else {
String token = cookie.getValue();
return !StringUtils.hasLength(token) ? null : new DefaultCsrfToken(this.headerName, this.parameterName, token);
}
}
}

public void setParameterName(String parameterName) {
Assert.notNull(parameterName, "parameterName cannot be null");
this.parameterName = parameterName;
}

public void setHeaderName(String headerName) {
Assert.notNull(headerName, "headerName cannot be null");
this.headerName = headerName;
}

public void setCookieName(String cookieName) {
Assert.notNull(cookieName, "cookieName cannot be null");
this.cookieName = cookieName;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setCookieHttpOnly(boolean cookieHttpOnly) {
this.cookieHttpOnly = cookieHttpOnly;
}

private String getRequestContext(HttpServletRequest request) {
String contextPath = request.getContextPath();
return contextPath.length() > 0 ? contextPath : "/";
}

public static CookieCsrfTokenRepository withHttpOnlyFalse() {
CookieCsrfTokenRepository result = new CookieCsrfTokenRepository();
result.cookieHttpOnly = false;
return result;
}

private String createNewToken() {
return UUID.randomUUID().toString();
}

private Cookie mapToCookie(ResponseCookie responseCookie) {
Cookie cookie = new Cookie(responseCookie.getName(), responseCookie.getValue());
cookie.setSecure(responseCookie.isSecure());
cookie.setPath(responseCookie.getPath());
cookie.setMaxAge((int)responseCookie.getMaxAge().getSeconds());
cookie.setHttpOnly(responseCookie.isHttpOnly());
if (StringUtils.hasLength(responseCookie.getDomain())) {
cookie.setDomain(responseCookie.getDomain());
}

if (StringUtils.hasText(responseCookie.getSameSite())) {
cookie.setAttribute("SameSite", responseCookie.getSameSite());
}

return cookie;
}

public void setCookiePath(String path) {
this.cookiePath = path;
}

public String getCookiePath() {
return this.cookiePath;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setCookieDomain(String cookieDomain) {
this.cookieDomain = cookieDomain;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setSecure(Boolean secure) {
this.secure = secure;
}

/** @deprecated */
@Deprecated(
since = "6.1"
)
public void setCookieMaxAge(int cookieMaxAge) {
Assert.isTrue(cookieMaxAge != 0, "cookieMaxAge cannot be zero");
this.cookieMaxAge = cookieMaxAge;
}
}

可以看到里面主要有三个方法,一个是generateToken,saveToken,loadToken见名思义,一个是生成令牌,一个是保存令牌,最后大概是校验令牌了,csrfFilter 的处理流程很清晰,当一个请求到达时,首先会调用csrfTokenRepository 的loadToken方法加载该会话的CsrfToken值。如果加载不到,则证明请求是首次发起的,应该生成并保存一个新的CsrfToken 值。如果可以加载到CsrfToken 值,那么先排除部分不需要验证CSRF攻击的请求方法(默认忽略了GET、HEAD、TRACE和OPTIONS)

可以看到这里把令牌存到cookie中了

4.实现CSRF令牌验证的步骤

当客户端发送带有CSRF令牌的请求到服务器时,Spring Security会自动进行CSRF令牌验证。你不需要显式地在每个请求处理器中验证CSRF令牌,因为Spring Security框架已经集成了这一功能。

如果CSRF令牌验证失败,Spring Security会阻止请求的执行,并返回相应的错误状态码(通常是403 Forbidden),表明请求被拒绝。

5.自定义处理 CSRF 验证错误

要处理诸如kInvalidCsrfTokenException之类的AccessDeniedException的你可以使用以下配置配置自定义拒绝访问页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.exceptionHandling((exceptionHandling) -> exceptionHandling
.accessDeniedPage("/access-denied")
);
return http.build();
}
}

6.禁用 CSRF 保护

默认情况下,CSRF 保护是启用的,这会影响 与后台的集成 和 应用程序的 测试。在禁用 CSRF 保护之前,请考虑这 对你的应用程序是否有意义。

你还可以考虑是否只有某些端点不需要 CSRF 保护,并配置忽略规则,如下例所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf
.ignoringRequestMatchers("/api/*")
);
return http.build();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// ...
.csrf((csrf) -> csrf.disable());
return http.build();
}
}

7.前后端分离同时开启Csrf认证

实现思路,当我们配置csrf时候,登录地址,不需要拦截,登录成功后会得到一个XSRF-TOKEN,前端发送请求之前获取到这个cookie,并将其添加到请求头中,如下图所示

8.额外注意事项

- 确保前端(例如JavaScript应用程序)能够正确地从Cookie中获取CSRF令牌,并将其添加到每个请求的请求头中。

- 在前后端分离的应用中,跨域请求可能需要特别处理,以确保CSRF令牌的正确传递和验证。

总结来说,Spring Security的CSRF保护机制会自动处理CSRF令牌的生成、发送和验证,你只需确保客户端和服务器之间正确地处理CSRF令牌的传递和使用即可。

  • Demo Table

  • in的逻辑

  • 优化原则

  • exists的逻辑

Demo Table

1
2
3
4
5
6
7
8
9
CREATE TABLE t1 (
id int(11) NOT NULL AUTO_INCREMENT,
a int(11) DEFAULT NULL,
b int(11) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_a (a)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

create table t2 like t1;

两个表t1 和 t2 , 一样的,包括索引信息

数据量t1 ,t2 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> select count(1) from t1;
+----------+
| count(1) |
+----------+
| 10000 |
+----------+
1 row in set

mysql> select count(1) from t2;
+----------+
| count(1) |
+----------+
| 100 |
+----------+
1 row in set

mysql>

in的逻辑

1
select * from t1 where id in (select id from t2) ;

这个SQL,先执行哪个呢?

看看执行计划

*

可以理解为

1
2
3
for(select id from t2){
select * from t1 where t1.id = t2.id
}

优化原则

原则:小表驱动大表,即小的数据集驱动大的数据集

当T2表的数据集小于T1表的数据集时,in优于exists


exists的逻辑

1
select * from A where exists (select 1 from B where B.id = A.id)

可以理解为

1
2
3
for(select * from A){
select * from B where B.id = A.id
}

当A表的数据集小于B表的数据集时,exists优于in

将主查询A的数据,放到子查询B中做条件验证,根据验证结果(true或false)来决定主查询的数据是否保留

1、 EXISTS(subquery)只返回TRUE或FALSE,因此子查询中的SELECT*也可以用SELECT1替换,官方说法是实际执行时会忽略SELECT清单,因此没有区别;
2、 EXISTS子查询的实际执行过程可能经过了优化而不是我们理解上的逐条对比;
3、 EXISTS子查询往往也可以用JOIN来代替,何种最优需要具体问题具体分析;

1
2
3
4
5
6
7
8
mysql> explain select * from t2 where exists (select 1 from t1 where t1.id = t2.id) ;
+----+--------------------+-------+------------+--------+---------------+---------+---------+---------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------------+-------+------------+--------+---------------+---------+---------+---------------+------+----------+-------------+
| 1 | PRIMARY | t2 | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100 | Using where |
| 2 | DEPENDENT SUBQUERY | t1 | NULL | eq_ref | PRIMARY | PRIMARY | 4 | artisan.t2.id | 1 | 100 | Using index |
+----+--------------------+-------+------------+--------+---------------+---------+---------+---------------+------+----------+-------------+
2 rows in set

Io模型

1.1 基本说明

1)、I/O 模型简单的理解:就是用什么样的通道进行数据的发送和接收,很大程度上决定了程序通信的性能。

2)、Java共支持3种网络编程模型/IO模式:BIO、NIO、AIO。

3)、Java BIO:同步并阻塞(传统阻塞性),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销。
*

4)、Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求都会注册到多路复用器(Selector)上,多路复用器轮询到连接有I/O请求就进行处理。
*

5)、Java AIO:异步非阻塞,AIO 引入异步通道的概念,采用了 Proactor 模式,简化了程序编写,对有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。

1.2 适用场景分析

1)、BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

2)、NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等,编程比较复杂,JDK1.4开始支持。

3)、AIO方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。

二、Java BIO 详解

2.1 基本介绍

1)、Java BIO 就是传统的java io编程,其相关的类和接口在 java.io。

2)、BIO(blocking I/O):同步阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制改善(实现多个客户端连接服务器)。

3)、BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序简单易理解。

2.2 工作原理

*

1、 服务器端启动一个ServerSocket;
2、 客户端启动Socket对服务器进行通信,默认情况下服务器端需要对每个客户端建立一个线程与之通讯;
3、 客户端发出请求后,先咨询服务器是否有线程响应,如果没有则会等待或者被拒绝;
4、 如果有响应,客户端线程会等待请求结束后,在继续执行;

2.3 应用实例

2.3.1 实例说明

1、 使用BIO模型编写一个服务器端,监听6666端口,当有客户端连接时,就启动一个线程与之通讯;
2、 要求使用线程池机制改善,可以连接多个客户端;
3、 服务器端可以接收客户端发送的数据(telnet实现即可);

2.3.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
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
public class BIOServer {

public static void main(String[] args) throws IOException {

// 线程池机制

// 思路
// 1. 创建一个线程池
// 2. 如果有客户端连接,就创建一个线程,与之通讯

ExecutorService threadPool = Executors.newCachedThreadPool();

// 创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);
System.out.println("服务器启动了");

while (true){
System.out.println("线程信息 id = " + Thread.currentThread().getId() + ",名字 = "+ Thread.currentThread().getName());
// 监听,等待客户端连接
System.out.println("等待连接......");
final Socket socket = serverSocket.accept();
System.out.println("连接到一个客户端");

// 创建一个线程,与之通讯
threadPool.execute(new Runnable() {
public void run() { // 重写该方法
// 可以和客户端通讯
handler(socket);
}
});
}
}

// 编写一个handler方法,和客户端通讯
public static void handler(Socket socket){
try{
System.out.println("线程信息 id = " + Thread.currentThread().getId() + ",名字 = "+ Thread.currentThread().getName());

byte[] bytes = new byte[1024];
// 通过 socket 获取一个输入流
InputStream inputStream = socket.getInputStream();
// 循环读取客户端发送的数据
while (true){
System.out.println("线程信息 id = " + Thread.currentThread().getId() + ",名字 = "+ Thread.currentThread().getName());

System.out.println("read......");
int read = inputStream.read(bytes);
if(read != -1){
System.out.println(new String(bytes,0,read)); // 输出客户端发送的数据
}else{
break;
}
}
}catch (Exception e){
e.printStackTrace();
}finally {
System.out.println("关闭和client的连接");
try {
socket.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
2.3.3 telnet 使用

1、 进入到cmd;
2、 输入以下命令:telnet127.0.0.1:6666;
3、 快捷键进入消息发送页面:ctrl+];
4、 消息发送:sendok1000;

2.3.4 问题分析

1)、每个请求都需要创建独立的线程,与对应的客户端进行数据 Read,业务处理,数据 Write。

2)、当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大。

3)、连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在 Read 操作上,造成线程资源浪费。

Jconsole,JDK自带的,一种基于JMX的可视化监视、管理工具。

可以在系统有一定负荷的情况下使用;对垃圾回收算法有很详细的跟踪。

jconsole [ -interval=n ] [ -notile ] [ -pluginpath <path> ] [ -version ] [ connection ... ]

  -interval   将更新间隔设置为 n 秒 (默认值为 4 秒)
  -notile     初始不平铺窗口 (对于两个或多个连接)
  -pluginpath 指定 jconsole 用于查找插件的路径
  -version    输出程序版本

  connection = pid || host:port || JMX URL (service:jmx:<协议>://...)
  pid         目标进程的进程 ID
  host        远程主机名或 IP 地址
  port        远程连接的端口号

  -J          指定运行 jconsole 的 Java 虚拟机
              的输入参数

执行命令后,打开新建连接窗口,有两种

  (1)本地进程

  (2)远程进程

1.概述

显示有关Java VM和监视值的概述信息,包括CPU使用情况,内存使用情况,线程计数以及Java VM中加载的类的图形监视信息

2.内存

显示有关内存消耗和内存池的信息

“内存”选项卡具有“执行GC”按钮,可以随时单击该按钮以执行垃圾回收。

该图表显示了Java VM随时间的内存使用情况,堆和非堆内存以及特定内存池的内存使用情况。

Java VM管理两种内存:堆内存和非堆内存,这两种内存都是在Java VM启动时创建的。

(1)堆内存是运行时数据区,Java VM从中为所有类实例和数组分配内存。堆可以是固定的或可变的大小。垃圾收集器是一个自动内存管理系统,可回收对象的堆内存。

  A.Eden Space:

    伊甸区,对象被创建的时候首先放到Eden Space,进行垃圾回收后,不能被回收的对象被放入到空的Survivor区域

  B.Survivor Space:

    幸存者区,用于保存在eden space内存区域中经过垃圾回收后没有被回收的对象

    Survivor Space分为两个空间大小一样的区域,分别是To Survivor和From Survivor,并且始终保持一个Survivor是空的

  Eden Space和Survivor Space都属于新生代 

  对新生代 进行垃圾回收被称为Minor GC(或Young GC),每一次Minor GC后留下来的对象age(就是用来判断对象是否进入老年的标志)加1

  C.Old Gen:

    老年代,用于存放新生代中经过多次垃圾回收仍然存活的对象,也有可能是新生代分配不了内存的大对象会直接进入老年代。

    经过多次垃圾回收都没有被回收的对象,这些对象的age已经足够old了,就会放入到老年代。

    当老年代被放满之后,虚拟机会进行垃圾回收,称之为Major GC。由于Major GC除并发GC外均需对整个堆进行扫描和回收,因此又称为Full GC

默认的,新生代 ( Young ) 与老年代 ( Old ) 的比例的值为 1:2 ( 该值可以通过参数 –XX:NewRatio 来指定 ),即:新生代 ( Young ) = 1/3 的堆空间大小。 

老年代 ( Old ) = 2/3 的堆空间大小。其中,新生代 ( Young ) 被细分为 Eden 和 两个 Survivor 区域,这两个 Survivor 区域分别被命名为 from 和 to,以示区分。 

默认的,Edem : from : to = 8 : 1 : 1 ( 可以通过参数 –XX:SurvivorRatio 来设定 ),即: Eden = 8/10 的新生代空间大小,from = to = 1/10 的新生代空间大小。*
*

heap区即堆内存,整个堆大小=年轻代大小 + 老年代大小

堆内存默认为物理内存的1/64(<1GB);默认空余堆内存小于40%时,JVM就会增大堆直到-Xmx的最大限制,可以通过MinHeapFreeRatio参数进行调整;默认空余堆内存大于70%时,JVM会减少堆直到-Xms的最小限制,可以通过MaxHeapFreeRatio参数进行调整。

(2)非堆内存包括在Java VM的内部处理或优化所需的所有线程和内存之间共享的方法区域。

  它存储每类结构,例如运行时常量池,字段和方法数据,以及方法和构造函数的代码。

  方法区域在逻辑上是堆的一部分,但是根据实现,Java VM可能不会垃圾收集或压缩它。

  与堆存储器一样,方法区域可以是固定的或可变的大小。方法区域的内存不需要是连续的。

  A.Metaspace:

    元空间,是方法区的在HotSpot jvm 中的实现,方法区主要用于存储类的信息、常量池、方法数据、方法代码等。方法区逻辑上属于堆的一部分,但是为了与堆进行区分,通常又叫“非堆”。

  B.Code Cache:

    HotSpot Java VM还包括代码缓存,其中包含用于编译和存储本机代码的内存。

  C.Compressed Class Space:

    压缩类空间

3.线程

显示有关线程使用的信息

红色:峰值线程数,蓝色:活动线程数。

左下角的线程列表列出了所有活动线程

单击“线程”列表中的线程名称,以显示有关该线程的信息,包括线程名称,状态和堆栈跟踪。

4.类监控加载

显示有关类加载的信息

红线是加载的类的总数(包括随后卸载的类),蓝线是当前加载的类的数量。

“详细信息”部分显示自Java VM启动以来加载的类的总数,当前加载的数量和卸载的数量。

通过选中右上角的复选框将类加载跟踪设置为详细输出

5.VM信息
提供有关Java VM的信息

6.MBean

显示了所有在platform. MBeanserver上注册的MBeans的信息

左边的树形结构显示了所有的MBean

选择了一个MBean之后,其属性、操作、通知和其他信息会在右边显示

  • Demo Table

  • 表关联常见有两种算法

  • 嵌套循环连接 Nested-Loop Join(NLJ) 算法 (NLP)

  • 定义

    • 示例

    • 执行过程

    • 规律

  • 基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法

  • 定义

    • 示例

    • 执行过程

    • join_buffer 放不下怎么办?

  • 被驱动表的关联字段没索引为什么要选择使用 BNL 算法而不使用 Nested-Loop Join 呢?

  • 如何界定大表 小表

  • 关联sql的优化的两个核心点

Demo Table

1
2
3
4
5
6
7
8
9
CREATE TABLE t1 (
id int(11) NOT NULL AUTO_INCREMENT,
a int(11) DEFAULT NULL,
b int(11) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_a (a)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

create table t2 like t1;

两个表t1 和 t2 , 一样的,包括索引信息 a 字段有索引 b字段没有索引。

数据量t1 ,t2 如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> select count(1) from t1;
+----------+
| count(1) |
+----------+
| 10000 |
+----------+
1 row in set

mysql> select count(1) from t2;
+----------+
| count(1) |
+----------+
| 100 |
+----------+
1 row in set

mysql>

表关联常见有两种算法

嵌套循环连接 Nested-Loop Join(NLJ) 算法 (NLP)

定义

一次一行循环地从第一张表(称为驱动表)中读取行,在这行数据中取到关联字段,根据关联字段在另一张表(被驱动表)里取出满足条件的行,然后取出两张表的结果合集。


示例

举个例子来说明一下

【关联字段a有索引】

1
2
3
4
5
6
7
8
9
10
mysql> EXPLAIN select * from t1 inner join t2 on t1.a= t2.a;
+----+-------------+-------+------------+------+---------------+-------+---------+--------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-------+---------+--------------+------+----------+-------------+
| 1 | SIMPLE | t2 | NULL | ALL | idx_a | NULL | NULL | NULL | 100 | 100 | Using where |
| 1 | SIMPLE | t1 | NULL | ref | idx_a | idx_a | 5 | artisan.t2.a | 1 | 100 | NULL |
+----+-------------+-------+------------+------+---------------+-------+---------+--------------+------+----------+-------------+
2 rows in set

mysql>

从执行计划中可以看出

  • 驱动表是 t2,被驱动表是 t1 。 从执行顺序上来看,先执行的就是驱动表,所以id=1 的是t2 ,如果id相同,从上到下顺序执行。 (id越大,优先级越高越先执行)

  • 使用了 NLJ算法 . 一般 join 语句中,如果执行计划 Extra 中未出现 Using join buffer 则表示使用的 join 算法是 NLJ。


执行过程

1
2
3
4
5
6
7
8
9
10
mysql> EXPLAIN select * from t1 inner join t2 on t1.a= t2.a;
+----+-------------+-------+------------+------+---------------+-------+---------+--------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+-------+---------+--------------+------+----------+-------------+
| 1 | SIMPLE | t2 | NULL | ALL | idx_a | NULL | NULL | NULL | 100 | 100 | Using where |
| 1 | SIMPLE | t1 | NULL | ref | idx_a | idx_a | 5 | artisan.t2.a | 1 | 100 | NULL |
+----+-------------+-------+------------+------+---------------+-------+---------+--------------+------+----------+-------------+
2 rows in set

mysql>

执行过程如下

1、 先从t2驱动表里取出一条记录(如果有where条件,则按where条件过滤后的结果集中取出一行);
2、 拿到t2结果集中的一条记录中的关联字段a,去t1表中查找;
3、 取出t1中满足条件的行,跟t2中获取到的结果合并,作为结果返回给客户端;
4、 重复上述步骤;

我们来算一下这个操作MySQL要读取多少行数据

首先读取 t2 表的所有数据 100条记录 ,然后遍历这每行数据中字段 a 的值,根据 t2 表中 a 的值索引扫描 t1 表中的对应行(扫描100次 t1 表的索引(idx_a ),1次扫描可以认为最终只扫描 t1 表一行完整数据,也就是总共 t1 表也扫描了100行), 因此整个过程扫描了 200 行。 (估算的)

如果被驱动表的关联字段没索引,使用NLJ算法性能会比较低 ,mysql会选择Block Nested-Loop Join算法。


规律

  • 优化器一般会优先选择小表做驱动表。所以使用 inner join 时,排在前面的表并不一定就是驱动表。

  • 当使用left join时,左表是驱动表,右表是被驱动表

  • 当使用right join时,右表时驱动表,左表是被驱动表

  • 当使用join时,mysql会选择数据量比较小的表作为驱动表,大表作为被驱动表。


基于块的嵌套循环连接 Block Nested-Loop Join(BNL)算法

定义

把驱动表的数据读入到 join_buffer 中,然后扫描被驱动表,把被驱动表每一行取出来跟 join_buffer 中的数据做对比。


示例

【关联字段b无索引】

1
2
3
4
5
6
7
8
9
10
mysql> EXPLAIN select * from t1 inner join t2 on t1.b= t2.b;
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------------------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------------------------------------------+
| 1 | SIMPLE | t2 | NULL | ALL | NULL | NULL | NULL | NULL | 100 | 100 | NULL |
| 1 | SIMPLE | t1 | NULL | ALL | NULL | NULL | NULL | NULL | 10337 | 10 | Using where; Using join buffer (Block Nested Loop) |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+----------------------------------------------------+
2 rows in set

mysql>
*

从执行计划中可以看出

  • 驱动表是 t2,被驱动表是 t1 。 从执行顺序上来看,先执行的就是驱动表,所以id=1 的是t2 ,如果id相同,从上到下顺序执行。

  • 使用了BNL算法 . Extra 中 的Using join buffer (Block Nested Loop)说明该关联查询


执行过程

1、 把t2的所有数据放入到join_buffer中;
2、 把表t1中每一行取出来,跟join_buffer中的数据做对比;
3、 返回满足join条件的数据;

我们来算一下这个操作MySQL要读取多少行数据

整个过程对表 t1 和 t2 都做了一次全表扫描,因此扫描的总行数为10000(表 t1 的数据总量) + 100(表 t2 的数据总量) = 10100。

join_buffer 里的数据是无序的,极端情况下对表 t1 中的每一行,都要做 100 次判断,所以内存中的判断次数是 100 * 10000= 100 万次。


join_buffer 放不下怎么办?

我们这个例子里表 t2 才 100 行,要是表 t2 是一个大表,join_buffer 放不下怎么办呢

join_buffer 的大小是由参数 join_buffer_size 设定的,默认值是 256k。

1
2
3
4
5
6
7
8
9
mysql> show variables like '%join_buffer_size%';
+------------------+--------+
| Variable_name | Value |
+------------------+--------+
| join_buffer_size | 262144 |
+------------------+--------+
1 row in set

mysql>

如果放不下表 t2 的所有数据话,策略很简单,就是分段放

举个例子

比如 t2 表有1000行记录, join_buffer 一次只能放800行数据,那么执行过程就是先往 join_buffer 里放800行记录,然后从 t1 表里取数据跟 join_buffer 中数据对比得到部分结果,然后清空 join_buffer ,再放入 t2 表剩余200行记录,再次从 t1 表里取数据跟 join_buffer 中数据对比。所以就多扫了一次 t1 表。


被驱动表的关联字段没索引为什么要选择使用 BNL 算法而不使用 Nested-Loop Join 呢?

如果上面第二条sql使用 Nested-Loop Join,那么扫描行数为 100 * 10000 = 100万次,因为没有索引,所以这个100万磁盘扫描。

虽然BNL也是100万,但是是内存中计算 ,肯定要快

所以,用BNL磁盘扫描次数少很多,相比于磁盘扫描,BNL的内存计算会快得多。

因此MySQL对于被驱动表的关联字段没索引的关联查询,一般都会使用 BNL 算法。如果有索引一般选择 NLJ 算法,有索引的情况下 NLJ 算法比 BNL算法性能更高


如何界定大表 小表

不是按照表中的数量来决定大表小表,而是根据参与计算的表的数量来决定大表还是小表。

在决定哪个表做驱动表的时候,应该是两个表按照各自的条件过滤,过滤完成之后,计算参与 join 的各个字段的总数据量,数据量小的那个表,就是“小表”,应该作为驱动表。


关联sql的优化的两个核心点

  • 关联字段加索引,让mysql做join操作时尽量选择NLJ算法

  • 小表驱动大表,写多表连接sql时如果明确知道哪张表是小表可以用straight_join写法固定连接驱动方式,省去mysql优化器自己判断的时间.

举个例子

比如:select * from t2 straight_join t1 on t2.a = t1.a; 代表指定mysql选着 t2 表作为驱动表。

  • straight_join只适用于inner join,并不适用于left join,right join。 因为left join,right join已经代表指定了表的执行顺序

  • 尽可能让优化器去判断,因为大部分情况下mysql优化器是比人要聪明的。使用straight_join一定要慎重,因为部分情况下人为指定的执行顺序并不一定会比优化引擎好。

由于MySQL是作为存储层部署在业务系统的最后端,所有的业务数据最终都要入库落盘,但随着一个项目在线上运行的时间越来越久,数据库中的数据量自然会越来越多,而数据体积出现增长后,当需要从表查询一些数据时,效率会越发低下。在正常情况下,表的查询性能和数据量是成反比的,也就是数据越多,查询越慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT * FROM zz_student;
+------------+--------+------+--------+
|student_id|name|sex|height|
+------------+--------+------+--------+
|1|竹子||185cm|
|2|熊猫||170cm|
|3|子竹||182cm|
|4|棕熊||187cm|
|5|黑豹||177cm|
|6|脑斧||178cm|
|7|兔纸||165cm|
+------------+--------+------+--------+

SELECT * FROM zz_student WHERE name="脑斧";

上面给出了一张学生表,其中有七位学生信息,而此时要查询姓名为「脑斧」的学生信息时,MySQL底层是如何检索数据的呢?会触发磁盘IO,对表中的数据进行逐条读取并判断,也就是说,在这里想要查找到符合要求的数据,至少要经过六次磁盘IO才能检索到目标(暂且先不考虑局部性读取原理与随机IO)。

  • • 那假设这个表中有1000W条数据呢?要查的目标数据位于表的900W行以后怎么办?岂不是要触发几百万次磁盘IO才能检索到数据啊,如果真的这样去干,其效率大家可想而知。

在这种情况下,又该如何去提升数据库的查询性能呢?因为查询往往都是一个业务系统中最频繁的操作,一般项目的写/读请求比例都遵循三七定律,也就是30%的请求会涉及到写库操作,另外70%则属于查库类型的操作。

在思考如何提升查询性能前,咱们不妨先回想一下小时候的场景,小时候由于刚接触汉字,很多字都不认识,所以通常每个人小时候都会拥有一本「新华字典」,但一本字典那么厚,我们是一页页去翻的吗?并不是,字典中有目录索引,我们可以根据音节、偏旁等方式查找不认识的字。

* 新华字典

在「新华字典」中一页页翻找某个汉字,就类似于我们前面给出的全表扫描方式,效率特别特别低,而通过目录索引则能够在很短的时间内找到目标汉字。

既然字典中都存在目录索引页,能帮助小时候的我们快速检索汉字,那这个思想能否应用到数据库中来呢?答案是当然可以,并且MySQL也提供了索引机制,索引是数据库中的核心组件之一,一张表中建立了合适的索引后,往往在面对海量数据查询时,能够事半功倍,接下来一起聊一聊MySQL的索引。

一、MySQL索引机制概述

对于MySQL索引机制的作用,经过上述「新华字典」的案例后可得知:索引就是用来帮助表快速检索目标数据的。此时先来简单回顾一下MySQL中索引是如何使用的呢?首先需要创建索引,MySQL可以通过CREATE、ALTER、DDL三种方式创建一个索引。

1.1、MySQL索引的创建方式

  • [ ]

    使用CREATE语句创建

1
CREATE INDEX indexName ON tableName(columnName(length) [ASC|DESC]);

这种创建方式可以给一张已存在的表结构添加索引,其中需要指定几个值:

  • indexName:当前创建的索引,创建成功后叫啥名字。

  • tableName:要在哪张表上创建一个索引,这里指定表名。

  • columnName:要为表中的哪个字段创建索引,这里指定字段名。

  • length:如果字段存储的值过长,选用值的前多少个字符创建索引。

  • ASC|DESC:指定索引的排序方式,ASC是升序,DESC是降序,默认ASC。

当然,上述语句中的INDEX也可更改为KEY,作用都是创建一个普通索引,而对于其他的索引类型,这点在后续的索引分类中再聊。

  • [ ]

    使用ALTER语句创建

1
ALTER TABLE tableName ADD INDEX indexName(columnName(length) [ASC|DESC]);

这里的参数都相同,所以不再重复赘述。

  • [ ]

    建表时DDL语句中创建

1
2
3
4
5
6
CREATE TABLE tableName (
columnName1 INT(8) NOT NULL,
columnName2 ...,
...,
INDEX indexName(columnName(length))
);

这种方式就比较适合在库表设计时,已经确定了索引项的情况下建立。

1.2、查询、删除、指定索引

但不管通过哪种方式建立索引,本质上创建的索引都是相同的,当索引创建完成后,可通过SHOW INDEX FROM tableName;这条命令查询一个表中拥有的索引,如下:

1
2
3
4
5
6
7
8
9
10
CREATE TABLE zz_user (
user_id INT(8) NOT NULL AUTO_INCREMENT,
user_name VARCHAR(255) NULL DEFAULT "",
user_sex VARCHAR(255) NULL DEFAULT "",
user_phone VARCHAR(255) NULL DEFAULT "",
PRIMARY KEY (user_id) USING BTREE
) ENGINE=InnoDB
CHARACTER SET=utf8
COLLATE=utf8_general_ci
ROW_FORMAT=Compact;

在上述的建表SQL中,为user_id创建了一个主键索引,然后来查一下当前表的索引信息:

* 索引查询

简单的概述一下查询后,每个字段的含义:

  • Table:当前索引属于那张表。

  • Non_unique:目前索引是否属于唯一索引,0代表是的,1代表不是。

  • Key_name:当前索引的名字。

  • Seq_in_index:如果当前是联合索引,目前字段在联合索引中排第几个。

  • Column_name:当前索引是位于哪个字段上建立的。

  • Collation:字段值以什么方式存储在索引中,A表示有序存储,NULL表无序。

  • Cardinality:当前索引的散列程度,也就是索引中存储了多少个不同的值。

  • Sub_part:当前索引使用了字段值的多少个字符建立,NULL表示全部。

  • Packed:表示索引在存储字段值时,以什么方式压缩,NULL表示未压缩,

  • Null:当前作为索引字段的值中,是否存在NULL值,YES表示存在。

  • Index_type:当前索引的结构(BTREE, FULLTEXT, HASH, RTREE)。

  • Comment:创建索引时,是否对索引有备注信息。

这条命令在后续排除问题、性能调优时,会有不小的作用,比如可以通过分析其中的Cardinality字段值,如果该值少于数据的实际行数,那目前索引有可能失效

MySQL中并未提供修改索引的命令,也就说当你建错了索引,只能先删再重新建立一次,删除索引的语句如下:

1
DROP INDEX indexName ON tableName;

当然,当建立了一条索引后,也可以强制性的为SELECT语句指定索引,如下:

1
2
3
SELECT *
FROM table_name FORCE INDEX (index_name)
WHERE ...;

FORCE INDEX关键字可以为一条查询语句强制指定走哪个索引查询,但要牢记的是:如果当前的查询SQL压根不会走指定的索引字段,哪这种方式是行不通的,这个关键字的用法是:一条查询语句在有多个索引可以检索数据时,显式指定一个索引,减少优化器选择索引的耗时。

1.3、数据库索引的本质

前面一直在聊创建、查看、删除、指定等一些索引的基本操作,但索引本质上在数据库中是什么呢?大家都知道,数据库是基于磁盘工作的,所有的数据都会放到磁盘上存储,而索引也是数据的一种,因此与表数据相同,最终创建出的索引也会在磁盘生成本地文件。

不过索引文件在磁盘中究竟以何种方式存储,这是由索引的数据结构来决定的。同时,由于索引机制最终是由存储引擎实现,因此不同存储引擎下的索引文件,其保存在本地的格式也并不相同。

因为刚刚聊过,索引本质上和表是一样的,都是磁盘中的文件,那也就代表着创建一个索引,并不像单纯的给一张表加个约束那么简单,而是会基于原有的表数据,重新在磁盘中创建新的本地索引文件。假设表中有一千万条数据,那创建索引时,就需要将索引字段上的1000W个值全部拷贝到本地索引文件中,同时做好排序并与表数据产生映射关系。

二、MySQL的索引分类

在前面我为什么用多样化去形容数据库索引呢?因为确实如此,先列一些大家都听说过的索引称呼:聚簇索引、非聚簇索引、唯一索引、主键索引、联合索引、全文索引、单列索引、多列索引、复合索引、普通索引、二级索引、辅助索引、次级索引、有序索引、B+Tree索引、R-Tree索引、T-Tree索引、Hash索引、空间索引、前缀索引.

但实际上MySQL中真的有这么多索引类型吗?其实并没有,上述列出的索引称呼中,有几个称呼对应的索引是同一个,有一部分只是逻辑上的索引,那索引究竟该如何分类呢?其实从不同的层面上来说,可以将索引划分为不同的类型,接下来重点聊一聊。

2.1、数据结构层次

前面聊索引本质的时候提到过,索引建立后也会在磁盘生成索引文件,那每个具体的索引节点该如何在本地文件中存放呢?这点是由索引的数据结构来决定的。比如索引的底层结构是数组,那所有的索引节点都会以Node1 Node2 Node3 Node4....这样的形式,存储在磁盘同一块物理空间中,不过MySQL的索引不支持数组结构,或者说数组结构不适合作为索引结构,MySQL索引支持的数据结构如下:

  • B+Tree类型:MySQL中最常用的索引结构,大部分引擎支持,有序。

  • Hash类型:大部分存储引擎都支持,字段值不重复的情况下查询最快,无序。

  • R-Tree类型:MyISAM引擎支持,也就是空间索引的默认结构类型。

  • T-Tree类型:NDB-Cluster引擎支持,主要用于MySQL-Cluster服务中。

在上述的几种索引结构中,B+树和哈希索引是最常见的索引结构,几乎大部分存储引擎都实现了,对于后续两种索引结构在某些情况下也较为常见,但除开列出的几种索引结构外,MySQL索引支持的数据结构还有R+、R*、QR、SS、X树等结构。

当然,也正因为索引结构由存储引擎决定,而MySQL引擎层在《MySQL架构篇》中提到过,属于可拔插式引擎,所以如果你有能力自己实现一个引擎,那你甚至可以让引擎的索引机制支持任何数据结构。

1
CREATE INDEX indexName ON tableName(columnName(length) [ASC|DESC]) USING HASH;

也就是在创建索引时,通过USING关键字显示指定索引的数据结构(必须要为当前引擎支持的结构)。

同时索引会被分为有序索引和无序索引,这是指索引文件中存储索引节点时,会不会按照字段值去排序。那一个索引到底是有序还是无序,就是依据数据结构决定的,例如B+Tree、R-Tree等树结构都是有序,而哈希结构则是无序的。

2.2、字段数量层次

前面从索引的数据结构层次出发,可以将索引分为不同结构的类型,而从表字段的层次来看,索引又可以分为单列索引和多列索引,这两个称呼也比较好理解,单列索引是指索引是基于一个字段建立的,多列索引则是指由多个字段组合建立的索引。

单列索引也会分为很多类型,比如:

  • 唯一索引:指索引中的索引节点值不允许重复,一般配合唯一约束使用。

  • 主键索引:主键索引是一种特殊的唯一索引,和普通唯一索引的区别在于不允许有空值。

  • 普通索引:通过KEY、INDEX关键字创建的索引就是这个类型,没啥限制,单纯的可以让查询快一点。

多列索引的概念前面解释过了,不过它也有很多种叫法,例如:

  • 组合索引、联合索引、复合索引、多值索引….

但不管名称咋变,描述的含义都是相同的,即由多个字段组合建立的索引。

比如在用户表中,通过id、name、age三个字段建立一个多列索引,什么情况下会使用索引,什么时候不会呢?如下:

1
2
3
4
5
6
7
8
9
无法使用多列索引的SQL语句
SELECT ***
FROM zz_user
WHERE name = "竹子" AND age = "18";

能命中多列索引的SQL语句
SELECT ***
FROM zz_user
WHERE name = "竹子" AND id = 6;

OK,到这里就根据字段数量的层面出发,简单讲明了单列和多列索引的概念,但无论是单列还是多列,都可以存在一个前缀索引的概念,啥叫前缀索引呢?还记得创建索引时指定的length字段吗?

  • length:如果字段存储的值过长,选用值的前多少个字符创建索引。

使用一个字段值中的前N个字符创建出的索引,就可以被称为前缀索引,前缀索引能够在很大程度上,节省索引文件的存储空间,也能很大程度上提升索引的性能,这是为什么呢?后面分析索引实现原理的时候细聊。

2.3、功能逻辑层次

相信大家在面试时,如果问到了MySQL索引机制,相信一定会问如下这道面试题:

这题的答案该怎么回答呢?其实主要就是指MySQL索引从逻辑上可以分为那些类型,以功能逻辑划分索引类型,这也是最常见的划分方式,从这个维度来看主要可划分为五种:

  • 普通索引、唯一索引、主键索引、全文索引、空间索引

对于普通索引、唯一索引、主键索引都介绍过了,就不再过多阐述,但稍微提一嘴,在主键字段上建立的索引被称为主键索引,非主键字段上建立的索引一般被称为辅助索引或、二级索引或次级索引,接着重点聊一下全文索引和空间索引。

全文索引和空间索引都是MySQL5.7版本后开始支持的索引类型,不过这两种索引都只有MyISAM引擎支持,其他引擎要么我没用过,要么就由于自身实现的原因不支持,例如InnoDB。对于全文索引而言,其实在MySQL5.6版本中就有了,但当时并不支持汉字检索,到了5.7.6版本的时候才内嵌ngram全文解析器,才支持亚洲语种的分词,同时InnoDB引擎也开始支持全文索引,在5.7版本之前,只有MyISAM引擎支持。

全文索引

全文索引类似于ES、Solr搜索中间件中的分词器,或者说和之前常用的like+%模糊查询很类似,它只能创建在CHAR、VARCHAR、TEXT等这些文本类型字段上,而且使用全文索引查询时,条件字符数量必须大于3才生效。当然,还是举个栗子才有感觉:

1
2
3
4
5
6
7
8
+------------+--------------------------------------------+------------------+
|*article_id*|*article_name*******************************|*special_column***|
+------------+--------------------------------------------+------------------+
|**********1*|*MySQL架构篇:自顶向下深入剖析MySQL整体架构*|*《全解MySQL》****|
|**********2*|*MySQL执行篇:一条SQL语句从诞生至结束的历程*|*《全解MySQL》****|
|**********3*|*MySQL设计篇:数据库六范式与反范式设计准则!|*《全解MySQL》****|
|**********4*|*MySQL索引篇:索引概述、分类及建立索引的原则|*《全解MySQL》****|
+------------+--------------------------------------------+------------------+

比如现在用户想要搜索一篇文章,但是忘记文章全称了,只记得「诞生至结束」这个词汇,此时用户搜索这个词汇,走全文索引的情况下,照样能够定位到上表中的第二条记录。

空间索引

空间索引这玩意儿其实用的不多,至少大部分项目的业务中不会用到,想要弄清楚空间索引,那么首先得知道一个概念:GIS空间数据,GIS是什么意思呢?是地理信息系统,这是一门新的学科,基于了计算机、信息学、地理学等多科构建的,主要就是用于管理地理信息的数据结构,在国土、规划、出行、配送、地图等和地理有关的项目中,应用较为频繁。

地理空间数据主要包含矢量数据、3D模型、影像文件、坐标数据等,说简单点,空间数据也就是可以将地理信息以模型的方式,在地图上标注出来。在MySQL中总共支持GEOMETRY、POINT、LINESTRING、POLYGON四种空间数据类型,而空间索引则是基于这些类型的字段建立的,也就是可以帮助我们快捷检索空间数据。

2.4、存储方式层次

上面聊完了三种不同层次的索引划分后,接着从存储方式的层面再聊聊,从存储方式来看,MySQL的索引主要可分为两大类:

  • 聚簇索引:也被称为聚集索引、簇类索引

  • 非聚簇索引:也叫非聚集索引、非簇类索引、二级索引、辅助索引、次级索引

重点说一说这两类索引存储方式的区别,在说之前先回忆一下数组和链表的区别:

  • 数组是物理空间上的连续,存储的所有元素都会按序存放在同一块内存区域中。

  • 链表是逻辑上的连续,存储的所有元素可能不在同一块内存,元素之间以指针连接。

为啥要说这个呢?因为聚簇索引和非聚簇索引的区别也大致是相同的:

  • 聚簇索引:逻辑上连续且物理空间上的连续。

  • 非聚簇索引:逻辑上的连续,物理空间上不连续。

当然,这里的连续和数组不同,因为索引大部分都是使用B+Tree结构存储,所以在磁盘中数据是以树结构存放的,所以连续并不是指索引节点,而是指索引数据和表数据,也就是说聚簇索引中,索引数据和表数据在磁盘中的位置是一起的,而非聚簇索引则是分开的,索引节点和表数据之间,用物理地址的方式维护两者的联系

不过一张表中只能存在一个聚簇索引,一般都会选用主键作为聚簇索引,其他字段上建立的索引都属于非聚簇索引,或者称之为辅助索引、次级索引。但也不要走进一个误区,虽然MySQL默认会使用主键上建立的索引作为聚簇索引,但也可以指定其他字段上的索引为聚簇索引,一般聚簇索引要求索引必须是非空唯一索引才行。

其实就算表中没有定义主键,InnoDB中会选择一个唯一的非空索引作为聚簇索引,但如果非空唯一索引也不存在,InnoDB隐式定义一个主键来作为聚簇索引。

当然,主键或者说聚簇索引,一般适合采用带有自增性的顺序值。

对于聚簇、非聚簇索引的区别、两者的查找过程、隐式主键、为何主键适合自增值等这些问题,在后续的《索引原理篇》中会详细讲解。

2.5、索引分类小结

至此,对于MySQL“多样化”的索引机制,一大堆索引名词,就已经梳理清楚啦!相信到这里为止,大家也对MySQL的索引机制有了系统化的认知,其实最开始给出的一大堆索引名词,只是从不同角度划分出来的,在上述中分别从数据结构、字段数量、功能逻辑以及存储方式多个层面进行了描述。当然,要牢记的是,以功能逻辑的层次来划分索引,这也是最常用的方式。

三、MySQL其他索引的创建使用方式

前面的案例中,聊到了咱们有三种方式创建索引,在创建时可通过INDEX、KEY两个关键字创建,但这种方式建立的索引仅是普通索引,接着再来聊一聊MySQL数据库其他类型的索引该如何创建以及使用。

但不管是何种类型的索引,都可以通过前面聊到的三种方式创建。

3.1、唯一索引的创建与使用

唯一索引在创建时,需要通过UNIQUE关键字创建:如下:

1
2
3
4
5
6
7
8
9
10
CREATE UNIQUE INDEX indexName ON tableName(columnName(length));

ALTER TABLE tableName ADD UNIQUE INDEX indexName(columnName);

CREATE TABLE tableName (
columnName1 INT(8) NOT NULL,
columnName2 ...,
...,
UNIQUE INDEX indexName(columnName(length))
);

在已有的表基础上创建唯一索引时要注意,如果选用的字段,表中字段的值存在相同值时,这时唯一索引是无法创建的,比如:

1
2
3
4
5
6
7
8
9
10
11
12
SELECT * FROM zz_article;
+------------+--------------------------+-------------------+
|*article_id*|*article_name*************|*special_column****|
+------------+--------------------------+-------------------+
|**********1*|*MySQL架构篇:.......*****|*《全解MySQL》*****|
|**********2*|*MySQL执行篇:.......*****|*《全解MySQL》*****|
|**********3*|*MySQL设计篇:.......*****|*《全解MySQL》*****|
|**********4*|*MySQL索引篇:.......*****|*《全解MySQL》*****|
|**********5*|*MySQL索引篇:.......*****|*《全解MySQL》*****|
+------------+--------------------------+-------------------+

CREATE UNIQUE INDEX i_article_name ON zz_article (article_name);

比如上述文章表中,第4、5条数据是重复的,此时创建利用SQL语句创建唯一索引,就会抛出1062错误码:

1
ERROR 1062 (23000): Duplicate*entry 'MySQL索引篇:.......' for key 'i_article_name'

在这种情况下,就只能先删除重复数据,然后才能创建唯一索引成功。

同时,当唯一索引创建成功后,它同时会对表具备唯一约束的作用,当再使用INSERT语句插入相同值时,会同样会抛出1062错误码:

1
2
3
INSERT INTO zz_article VALUES(6,"MySQL索引篇:.......","《全解MySQL》");

1062 -Duplicate*entry 'MySQL索引篇:.......' for key 'i_article_name'

这里会提示你插入的哪个值,已经在表中存在,因此无法插入当前这条数据。

3.2、主键索引的创建与使用

前面聊到过,主键索引其实是一种特殊的唯一索引,但主键索引却并不是通过UNIQUE关键字创建的,而是通过PRIMARY关键字创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ALTER TABLE tableName ADD PRIMARY KEY (columnName);

CREATE TABLE tableName (
columnName1 INT(8) NOT NULL,
columnName2 ...,
...,
PRIMARY KEY (columnName(length))
);

CREATE TABLE tableName (
columnName1 INT(8) NOT NULL,
columnName2 ...,
...,
CONSTRAINT indexName PRIMARY KEY (columnName(length))
);

在这里要注意:

  • 创建主键索引时,必须要将索引字段先设为主键,否则会抛1068错误码。

  • 这里也不能使用CREATE语句创建索引,否则会提示1064语法错误。

  • 同时创建索引时,关键字要换成KEY,并非INDEX,否则也会提示语法错误。

还是以之前的文章表为例,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 对非主键字段创建主键索引
ALTER TABLE zz_article ADD PRIMARY KEY i_special_column(special_column);
-- 报错信息如下:
-- 1068 - Multiple primary key defined

-- 使用CREATE关键字创建主键索引
CREATE PRIMARY KEY i_article_id ON zz_article (article_id);
-- 报错信息如下:
-- 1064 - You have an error in your SQL syntax; check....

-- 使用INDEX关键字创建索引
ALTER TABLE zz_article ADD PRIMARY INDEX i_article_id(article_id);
-- 报错信息如下:
-- 1064 - You have an error in your SQL syntax; check....

-- 创建主键索引正确的方式
ALTER TABLE zz_article ADD PRIMARY KEY i_article_id(article_id);

当然,一般主键索引都会在建表的DDL语句中创建,不会在表已经建立后再创建。

但似乎无论在讲普通索引,还是唯一索引、主键索引的时候,我们都没有讲如何使用这些创建好的索引查询数据,其实这一点无需咱们考虑,参考之前《SQL执行篇》中查询语句的执行流程,在一条SELECT语句来到MySQL时,会经历优化器优化的过程,而优化器则会自动帮咱们选择一个最合适的索引查询数据。当然,前提是查询条件中涉及到了索引字段才行。

前面也说过,你不想让优化器自动选择,也可以手动通过FORCE INDEX关键字强制指定。

3.3、全文索引的创建与使用

全文索引和其他索引不同,首先如果你想要创建全文索引,那么MySQL版本必须要在5.7及以上,同时使用时也需要手动指定,一起来先看看如何创建全文索引,此时需要使用FULLTEXT关键字:

1
2
3
ALTER TABLE tableName ADD FULLTEXT INDEX indexName(columnName);

CREATE FULLTEXT INDEX indexName ON tableName(columnName);

不过在创建全文索引时,有三个注意点:

  • 5.6版本的MySQL中,存储引擎必须为MyISAM才能创建。

  • 创建全文索引的字段,其类型必须要为CHAR、VARCHAR、TEXT等文本类型。

  • 如果想要创建出的全文索引支持中文,需要在最后指定解析器:with parser ngram。

此时还依旧是以文章表为例,为文章名称字段创建一个全文索引,命令如下:

1
ALTER TABLE zz_article ADD FULLTEXT INDEX ft_article_name(article_name) WITH PARSER NGRAM;

创建好全文索引后,当你想要使用全文索引时,优化器这时不能自动选择,因为全文索引有自己的语法,但在了解如何使用之前,得先清楚两个概念:最小搜索长度和最大搜索长度,先来看看全文索引的一些参数,可通过show variables like '%ft%';命令查询,如下:

* 全文索引参数

多余的参数就不介绍了,重点讲一下其中的几个重要参数:

  • ft_min_word_len:使用MyISAM引擎的表中,全文索引最小搜索长度。

  • ft_max_word_len:使用MyISAM引擎的表中,全文索引最大搜索长度。

  • ft_query_expansion_limit:MyISAM中使用with query expansion搜索的最大匹配数。

  • innodb_ft_min_token_size:InnoDB引擎的表中,全文索引最小搜索长度。

  • innodb_ft_max_token_size:InnoDB引擎的表中,全文索引最大搜索长度。

那么究竟做最小搜索长度、最大搜索长度的作用是什么呢?其实这个是一个限制,对于长度小于最小搜索长度和大于最大搜索长度的词语,都无法触发全文索引。也就是说,如果想要使用全文索引对一个词语进行搜索,那这个词语的长度必须在这两个值之间。

其实这两个值自己可以手动调整的,最小值可以手动调整为1MyISAM引擎的最大值可以调整为3600,但InnoDB引擎最大似乎就是84

OK~,了解全文索引中的一些概念后,接下来看看如何使用全文索引,全文索引中有两个专门用于检索的关键字,即MATCH(column)、AGAINST(关键字),同时这两个检索函数也支持三种搜索模式:

  • 自然语言模式(默认搜索模式)

  • 布尔搜索模式

  • 查询拓展搜索

MATCH()主要是负责指定要搜索的列,这里要指定创建全文索引的字段,AGAINST()则指定要搜索的关键字,也就是要搜索的词语,接下来简单的讲一下三种搜索模式。

自然语言模式

这种模式也是在使用全文索引时,默认的搜索模式,使用方法如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
+------------+--------------------------+-------------------+
|*article_id*|*article_name*************|*special_column****|
+------------+--------------------------+-------------------+
|**********1*|*MySQL架构篇:.......*****|*《全解MySQL》*****|
|**********2*|*MySQL执行篇:.......*****|*《全解MySQL》*****|
|**********3*|*MySQL设计篇:.......*****|*《全解MySQL》*****|
|**********4*|*MySQL索引篇:.......*****|*《全解MySQL》*****|
+------------+--------------------------+-------------------+

SELECT * COUNT(article_id) AS '搜索结果数量'
FROM zz_article
WHERE MATCH(article_name) AGAINST('MySQL');

--*运行结果如下:
+--------------+
|*搜索结果数量*|
+--------------+
|***********4*|
+--------------+

一眼看过去,SQL就能看懂,毕竟都可以排版了一下SQL,不过多介绍了。唯一要注意的是,如果给定的关键词长度小于默认的最小搜索长度,那是无法使用全文索引的,比如下述这条SQL就不会触发:

1
2
SELECT * COUNT(article_id) AS '搜索结果数量'
FROM zz_article WHERE MATCH(article_name) AGAINST('M');

布尔搜索模式

布尔搜索模式有些特殊,因为在这种搜索模式中,还需要掌握特定的搜索语法:

  • +:表示必须匹配的行数据必须要包含相应关键字。

  • -:和上面的+相反,表示匹配的数据不能包含相应的关键字。

  • >`:提升指定关键字的相关性,在查询结果中靠前显示。

  • `<:降低指定关键字的相关性,在查询结果中靠后显示。

  • ~:表示允许出现指定关键字,但出现时相关性为负。

  • :表示以该关键字开头的词语,如A,可以匹配A、AB、ABC….

  • “”:双引号中的关键字作为整体,检索时不允许再分词。

  • “X Y”@n:””包含的多个词语之间的距离必须要在n之间,单位-字节,如:

  • 竹子 熊猫@10:表示竹子和熊猫两个词语之间的距离要在10字节内。

  • …….

举个几个例子使用一下,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
### 查询文章名中包含 [MySQL] 但不包含 [设计] 的数据
SELECT * FROM zz_article WHERE MATCH(article_name) AGAINST('+MySQL -设计' IN BOOLEAN MODE);

### 查询文章名中包含 [MySQL] 和 [篇] 的数据,但两者间的距离不能超过10字节
SELECT * FROM zz_article WHERE MATCH(article_name) AGAINST('"MySQL 篇"@10' IN BOOLEAN MODE);


### 查询文章名中包含 [MySQL] 的数据,但包含 [执行] 关键字的行相关性要高于包含 [索引] 关键字的行数据
SELECT * FROM zz_article WHERE MATCH(article_name) AGAINST('+MySQL +(>执行 <索引)' IN BOOLEAN MODE);


### 查询文章名中包含 [MySQL] 的数据,但包含 [设计] 时则将相关性降为负
SELECT * FROM zz_article WHERE MATCH(article_name) AGAINST('+MySQL ~设计' IN BOOLEAN MODE);

### 查询文章名中包含 [执行] 关键字的行数据
SELECT * FROM zz_article WHERE MATCH(article_name) AGAINST('执行' IN BOOLEAN MODE);

### 查询文章名中必须要包含 [MySQL架构篇] 关键字的数据
SELECT * FROM zz_article WHERE MATCH(article_name) AGAINST('"MySQL架构篇"' IN BOOLEAN MODE);

同样的,上述的SQL语句应该都能看明白,最后的IN BOOLEAN MODE表示使用布尔搜索模式,除此外,大家唯一疑惑的就在于:相关性这个词,其实这个词也不难理解,就是检索数据后,数据的优先级顺序,当相关性越高,对应数据在结果中越靠前,当相关性为负,则相应的数据排到最后。

查询拓展搜索

查询拓展搜索其实是对自然语言搜索模式的拓展,比如举个例子:

1
SELECT COUNT(article_id) AS '搜索结果数量' FROM zz_article WHERE MATCH(article_name) AGAINST('MySQL' WITH QUERY EXPANSION);

在自然语言模式的查询语句基础上,最后面多加一个WITH QUERY EXPANSION表示使用查询拓展搜索,这种模式下会比自然语言模式多一次检索过程,比如上述的例子中:

  • 首先会根据指定的关键字MySQL进行一次全文检索。

  • 然后第二阶段还会对指定的关键进行分词,然后再进行一次全文检索。

之前介绍全文索引参数时,也列出来了一个名为ft_query_expansion_limit的参数,这个参数就是控制拓展搜索时的拓展行数的,最大可以调整到1000。但由于Query Expansion的全文检索可能带来许多非相关性的查询结果,因此在实际情况中要慎用!!!

实际上,全文索引引入MySQL后,可以用它代替之前的like%模糊查询,效率会更高。

3.4、空间索引的创建与使用

空间索引这玩意儿实际上很多项目不会用到,我用的次数也不多,但如果你要用到这个索引,那可以通过SPATIAL关键字创建,如下:

1
ALTER TABLE tableName ADD SPATIAL KEY indexName(columnName);

但在创建空间索引的时候,有几个注意点需要牢记:

  • 目前MySQL常用引擎中,仅有MyISAM支持空间索引,所以表引擎必须要为它。

  • 空间索引必须要建立在类型为GEOMETRY、POINT、LINESTRING、POLYGON的字段上。

这个用的较少,就不展开细聊了~

3.5、联合索引的创建与使用

联合索引呢,实际上并不是一种逻辑索引分类,它是索引的一种特殊结构,前面给出的所有案例中,都仅仅是在单个字段的基础上建立索引,而联合索引的意思是可以使用多个字段建立索引。那该如何创建联合索引呢,不需要特殊的关键字,方法如下:

1
2
CREATE INDEX indexName ON tableName (column1(length), column2...);
ALTER TABLE tableName ADD INDEX indexName (column1(length), column2...);
  • 你可以使用INDEX关键字,让多个列组成一个普通联合索引

  • 也可以使用UNIQUE INDEX关键字,让多个列组成一个唯一联合索引

  • 甚至还可以使用FULLTEXT INDEX关键字,让多个列组成一个全文联合索引

  • • …….

但是前面也提过,SELECT语句的查询条件中,必须包含组成联合索引的第一个字段,此时才会触发联合索引,否则是无法使用联合索引的。

登录和退出MySQL服务器

1
2
3
4
5
# 登录MySQL
$ mysql -u root -p12345612

# 退出MySQL数据库服务器
exit;

基本语法

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
-- 显示所有数据库
show databases;

-- 创建数据库
CREATE DATABASE test;

-- 切换数据库
use test;

-- 显示数据库中的所有表
show tables;

-- 创建数据表
CREATE TABLE pet (
  name VARCHAR(20),
  owner VARCHAR(20),
  species VARCHAR(20),
  sex CHAR(1),
  birth DATE,
  death DATE
);

-- 查看数据表结构
-- describe pet;
desc pet;

-- 查询表
SELECT * from pet;

-- 插入数据
INSERT INTO pet VALUES ('puffball', 'Diane', 'hamster', 'f', '1990-03-30', NULL);

-- 修改数据
UPDATE pet SET name = 'squirrel' where owner = 'Diane';

-- 删除数据
DELETE FROM pet where name = 'squirrel';

-- 删除表
DROP TABLE myorder;

建表约束

主键约束

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
-- 主键约束
-- 使某个字段不重复且不得为空,确保表内所有数据的唯一性。
CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20)
);

-- 联合主键
-- 联合主键中的每个字段都不能为空,并且加起来不能和已设置的联合主键重复。
CREATE TABLE user (
id INT,
name VARCHAR(20),
password VARCHAR(20),
PRIMARY KEY(id, name)
);

-- 自增约束
-- 自增约束的主键由系统自动递增分配。
CREATE TABLE user (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(20)
);

-- 添加主键约束
-- 如果忘记设置主键,还可以通过SQL语句设置(两种方式):
ALTER TABLE user ADD PRIMARY KEY(id);
ALTER TABLE user MODIFY id INT PRIMARY KEY;

-- 删除主键
ALTER TABLE user drop PRIMARY KEY;

唯一主键

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 建表时创建唯一主键
CREATE TABLE user (
id INT,
name VARCHAR(20),
UNIQUE(name)
);

-- 添加唯一主键
-- 如果建表时没有设置唯一建,还可以通过SQL语句设置(两种方式):
ALTER TABLE user ADD UNIQUE(name);
ALTER TABLE user MODIFY name VARCHAR(20) UNIQUE;

-- 删除唯一主键
ALTER TABLE user DROP INDEX name;

非空约束

1
2
3
4
5
6
7
8
9
-- 建表时添加非空约束
-- 约束某个字段不能为空
CREATE TABLE user (
id INT,
name VARCHAR(20) NOT NULL
);

-- 移除非空约束
ALTER TABLE user MODIFY name VARCHAR(20);

默认约束

1
2
3
4
5
6
7
8
9
10
-- 建表时添加默认约束
-- 约束某个字段的默认值
CREATE TABLE user2 (
id INT,
name VARCHAR(20),
age INT DEFAULT 10
);

-- 移除非空约束
ALTER TABLE user MODIFY age INT;

外键约束

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 班级
CREATE TABLE classes (
id INT PRIMARY KEY,
name VARCHAR(20)
);

-- 学生表
CREATE TABLE students (
id INT PRIMARY KEY,
name VARCHAR(20),
-- 这里的 class_id 要和 classes 中的 id 字段相关联
class_id INT,
-- 表示 class_id 的值必须来自于 classes 中的 id 字段值
FOREIGN KEY(class_id) REFERENCES classes(id)
);

-- 1. 主表(父表)classes 中没有的数据值,在副表(子表)students 中,是不可以使用的;
-- 2. 主表中的记录被副表引用时,主表不可以被删除。

数据库的三大设计范式

1NF

只要字段值还可以继续拆分,就不满足第一范式。

范式设计得越详细,对某些实际操作可能会更好,但并非都有好处,需要对项目的实际情况进行设定。

2NF

在满足第一范式的前提下,其他列都必须完全依赖于主键列。如果出现不完全依赖,只可能发生在联合主键的情况下:

1
2
3
4
5
6
7
8
-- 订单表
CREATE TABLE myorder (
product_id INT,
customer_id INT,
product_name VARCHAR(20),
customer_name VARCHAR(20),
PRIMARY KEY (product_id, customer_id)
);

实际上,在这张订单表中,product_name 只依赖于 product_idcustomer_name 只依赖于 customer_id 。也就是说,product_namecustomer_id 是没用关系的,customer_nameproduct_id 也是没有关系的。

这就不满足第二范式:其他列都必须完全依赖于主键列!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE myorder (
order_id INT PRIMARY KEY,
product_id INT,
customer_id INT
);

CREATE TABLE product (
id INT PRIMARY KEY,
name VARCHAR(20)
);

CREATE TABLE customer (
id INT PRIMARY KEY,
name VARCHAR(20)
);

拆分之后,myorder 表中的 product_idcustomer_id 完全依赖于 order_id 主键,而 productcustomer 表中的其他字段又完全依赖于主键。满足了第二范式的设计!

3NF

在满足第二范式的前提下,除了主键列之外,其他列之间不能有传递依赖关系。

1
2
3
4
5
6
CREATE TABLE myorder (
order_id INT PRIMARY KEY,
product_id INT,
customer_id INT,
customer_phone VARCHAR(15)
);

表中的 customer_phone 有可能依赖于 order_idcustomer_id 两列,也就不满足了第三范式的设计:其他列之间不能有传递依赖关系。

1
2
3
4
5
6
7
8
9
10
11
CREATE TABLE myorder (
order_id INT PRIMARY KEY,
product_id INT,
customer_id INT
);

CREATE TABLE customer (
id INT PRIMARY KEY,
name VARCHAR(20),
phone VARCHAR(15)
);

修改后就不存在其他列之间的传递依赖关系,其他列都只依赖于主键列,满足了第三范式的设计!

查询练习

准备数据

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
-- 创建数据库
CREATE DATABASE select_test;
-- 切换数据库
USE select_test;

-- 创建学生表
CREATE TABLE student (
no VARCHAR(20) PRIMARY KEY,
name VARCHAR(20) NOT NULL,
sex VARCHAR(10) NOT NULL,
birthday DATE, -- 生日
class VARCHAR(20) -- 所在班级
);

-- 创建教师表
CREATE TABLE teacher (
no VARCHAR(20) PRIMARY KEY,
name VARCHAR(20) NOT NULL,
sex VARCHAR(10) NOT NULL,
birthday DATE,
profession VARCHAR(20) NOT NULL, -- 职称
department VARCHAR(20) NOT NULL -- 部门
);

-- 创建课程表
CREATE TABLE course (
no VARCHAR(20) PRIMARY KEY,
name VARCHAR(20) NOT NULL,
t_no VARCHAR(20) NOT NULL, -- 教师编号
-- 表示该 tno 来自于 teacher 表中的 no 字段值
FOREIGN KEY(t_no) REFERENCES teacher(no)
);

-- 成绩表
CREATE TABLE score (
s_no VARCHAR(20) NOT NULL, -- 学生编号
c_no VARCHAR(20) NOT NULL, -- 课程号
degree DECIMAL, -- 成绩
-- 表示该 s_no, c_no 分别来自于 student, course 表中的 no 字段值
FOREIGN KEY(s_no) REFERENCES student(no),
FOREIGN KEY(c_no) REFERENCES course(no),
-- 设置 s_no, c_no 为联合主键
PRIMARY KEY(s_no, c_no)
);

-- 查看所有表
SHOW TABLES;

-- 添加学生表数据
INSERT INTO student VALUES('101', '曾华', '男', '1977-09-01', '95033');
INSERT INTO student VALUES('102', '匡明', '男', '1975-10-02', '95031');
INSERT INTO student VALUES('103', '王丽', '女', '1976-01-23', '95033');
INSERT INTO student VALUES('104', '李军', '男', '1976-02-20', '95033');
INSERT INTO student VALUES('105', '王芳', '女', '1975-02-10', '95031');
INSERT INTO student VALUES('106', '陆军', '男', '1974-06-03', '95031');
INSERT INTO student VALUES('107', '王尼玛', '男', '1976-02-20', '95033');
INSERT INTO student VALUES('108', '张全蛋', '男', '1975-02-10', '95031');
INSERT INTO student VALUES('109', '赵铁柱', '男', '1974-06-03', '95031');

-- 添加教师表数据
INSERT INTO teacher VALUES('804', '李诚', '男', '1958-12-02', '副教授', '计算机系');
INSERT INTO teacher VALUES('856', '张旭', '男', '1969-03-12', '讲师', '电子工程系');
INSERT INTO teacher VALUES('825', '王萍', '女', '1972-05-05', '助教', '计算机系');
INSERT INTO teacher VALUES('831', '刘冰', '女', '1977-08-14', '助教', '电子工程系');

-- 添加课程表数据
INSERT INTO course VALUES('3-105', '计算机导论', '825');
INSERT INTO course VALUES('3-245', '操作系统', '804');
INSERT INTO course VALUES('6-166', '数字电路', '856');
INSERT INTO course VALUES('9-888', '高等数学', '831');

-- 添加添加成绩表数据
INSERT INTO score VALUES('103', '3-105', '92');
INSERT INTO score VALUES('103', '3-245', '86');
INSERT INTO score VALUES('103', '6-166', '85');
INSERT INTO score VALUES('105', '3-105', '88');
INSERT INTO score VALUES('105', '3-245', '75');
INSERT INTO score VALUES('105', '6-166', '79');
INSERT INTO score VALUES('109', '3-105', '76');
INSERT INTO score VALUES('109', '3-245', '68');
INSERT INTO score VALUES('109', '6-166', '81');

-- 查看表结构
SELECT * FROM course;
SELECT * FROM score;
SELECT * FROM student;
SELECT * FROM teacher;

1 到 10

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
-- 查询 student 表的所有行
SELECT * FROM student;

-- 查询 student 表中的 name、sex 和 class 字段的所有行
SELECT name, sex, class FROM student;

-- 查询 teacher 表中不重复的 department 列
-- department: 去重查询
SELECT DISTINCT department FROM teacher;

-- 查询 score 表中成绩在60-80之间的所有行(区间查询和运算符查询)
-- BETWEEN xx AND xx: 查询区间, AND 表示 "并且"
SELECT * FROM score WHERE degree BETWEEN 60 AND 80;
SELECT * FROM score WHERE degree > 60 AND degree < 80;

-- 查询 score 表中成绩为 85, 86 或 88 的行
-- IN: 查询规定中的多个值
SELECT * FROM score WHERE degree IN (85, 86, 88);

-- 查询 student 表中 '95031' 班或性别为 '女' 的所有行
-- or: 表示或者关系
SELECT * FROM student WHERE class = '95031' or sex = '女';

-- 以 class 降序的方式查询 student 表的所有行
-- DESC: 降序,从高到低
-- ASC(默认): 升序,从低到高
SELECT * FROM student ORDER BY class DESC;
SELECT * FROM student ORDER BY class ASC;

-- 以 c_no 升序、degree 降序查询 score 表的所有行
SELECT * FROM score ORDER BY c_no ASC, degree DESC;

-- 查询 "95031" 班的学生人数
-- COUNT: 统计
SELECT COUNT(*) FROM student WHERE class = '95031';

-- 查询 score 表中的最高分的学生学号和课程编号(子查询或排序查询)。
-- (SELECT MAX(degree) FROM score): 子查询,算出最高分
SELECT s_no, c_no FROM score WHERE degree = (SELECT MAX(degree) FROM score);

-- 排序查询
-- LIMIT r, n: 表示从第r行开始,查询n条数据
SELECT s_no, c_no, degree FROM score ORDER BY degree DESC LIMIT 0, 1;

分组计算平均成绩

查询每门课的平均成绩。

1
2
3
4
5
6
7
-- AVG: 平均值
SELECT AVG(degree) FROM score WHERE c_no = '3-105';
SELECT AVG(degree) FROM score WHERE c_no = '3-245';
SELECT AVG(degree) FROM score WHERE c_no = '6-166';

-- GROUP BY: 分组查询
SELECT c_no, AVG(degree) FROM score GROUP BY c_no;

分组条件与模糊查询

查询 score 表中至少有 2 名学生选修,并以 3 开头的课程的平均分数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT * FROM score;
-- c_no 课程编号
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 | 92 |
| 103 | 3-245 | 86 |
| 103 | 6-166 | 85 |
| 105 | 3-105 | 88 |
| 105 | 3-245 | 75 |
| 105 | 6-166 | 79 |
| 109 | 3-105 | 76 |
| 109 | 3-245 | 68 |
| 109 | 6-166 | 81 |
+------+-------+--------+

分析表发现,至少有 2 名学生选修的课程是 3-1053-2456-166 ,以 3 开头的课程是 3-1053-245 。也就是说,我们要查询所有 3-1053-245degree 平均分。

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
-- 首先把 c_no, AVG(degree) 通过分组查询出来
SELECT c_no, AVG(degree) FROM score GROUP BY c_no
+-------+-------------+
| c_no | AVG(degree) |
+-------+-------------+
| 3-105 | 85.3333 |
| 3-245 | 76.3333 |
| 6-166 | 81.6667 |
+-------+-------------+

-- 再查询出至少有 2 名学生选修的课程
-- HAVING: 表示持有
HAVING COUNT(c_no) >= 2

-- 并且是以 3 开头的课程
-- LIKE 表示模糊查询,"%" 是一个通配符,匹配 "3" 后面的任意字符。
AND c_no LIKE '3%';

-- 把前面的SQL语句拼接起来,
-- 后面加上一个 COUNT(*),表示将每个分组的个数也查询出来。
SELECT c_no, AVG(degree), COUNT(*) FROM score GROUP BY c_no
HAVING COUNT(c_no) >= 2 AND c_no LIKE '3%';
+-------+-------------+----------+
| c_no | AVG(degree) | COUNT(*) |
+-------+-------------+----------+
| 3-105 | 85.3333 | 3 |
| 3-245 | 76.3333 | 3 |
+-------+-------------+----------+

多表查询 - 1

查询所有学生的 name,以及该学生在 score 表中对应的 c_nodegree

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
SELECT no, name FROM student;
+-----+-----------+
| no | name |
+-----+-----------+
| 101 | 曾华 |
| 102 | 匡明 |
| 103 | 王丽 |
| 104 | 李军 |
| 105 | 王芳 |
| 106 | 陆军 |
| 107 | 王尼玛 |
| 108 | 张全蛋 |
| 109 | 赵铁柱 |
+-----+-----------+

SELECT s_no, c_no, degree FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 | 92 |
| 103 | 3-245 | 86 |
| 103 | 6-166 | 85 |
| 105 | 3-105 | 88 |
| 105 | 3-245 | 75 |
| 105 | 6-166 | 79 |
| 109 | 3-105 | 76 |
| 109 | 3-245 | 68 |
| 109 | 6-166 | 81 |
+------+-------+--------+

通过分析可以发现,只要把 score 表中的 s_no 字段值替换成 student 表中对应的 name 字段值就可以了,如何做呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- FROM...: 表示从 student, score 表中查询
-- WHERE 的条件表示为,只有在 student.no 和 score.s_no 相等时才显示出来。
SELECT name, c_no, degree FROM student, score
WHERE student.no = score.s_no;
+-----------+-------+--------+
| name | c_no | degree |
+-----------+-------+--------+
| 王丽 | 3-105 | 92 |
| 王丽 | 3-245 | 86 |
| 王丽 | 6-166 | 85 |
| 王芳 | 3-105 | 88 |
| 王芳 | 3-245 | 75 |
| 王芳 | 6-166 | 79 |
| 赵铁柱 | 3-105 | 76 |
| 赵铁柱 | 3-245 | 68 |
| 赵铁柱 | 6-166 | 81 |
+-----------+-------+--------+

多表查询 - 2

查询所有学生的 no 、课程名称 ( course 表中的 name ) 和成绩 ( score 表中的 degree ) 列。

只有 score 关联学生的 no ,因此只要查询 score 表,就能找出所有和学生相关的 nodegree

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT s_no, c_no, degree FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 | 92 |
| 103 | 3-245 | 86 |
| 103 | 6-166 | 85 |
| 105 | 3-105 | 88 |
| 105 | 3-245 | 75 |
| 105 | 6-166 | 79 |
| 109 | 3-105 | 76 |
| 109 | 3-245 | 68 |
| 109 | 6-166 | 81 |
+------+-------+--------+

然后查询 course 表:

1
2
3
4
5
6
7
8
+-------+-----------------+
| no | name |
+-------+-----------------+
| 3-105 | 计算机导论 |
| 3-245 | 操作系统 |
| 6-166 | 数字电路 |
| 9-888 | 高等数学 |
+-------+-----------------+

只要把 score 表中的 c_no 替换成 course 表中对应的 name 字段值就可以了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
-- 增加一个查询字段 name,分别从 score、course 这两个表中查询。
-- as 表示取一个该字段的别名。
SELECT s_no, name as c_name, degree FROM score, course
WHERE score.c_no = course.no;
+------+-----------------+--------+
| s_no | c_name | degree |
+------+-----------------+--------+
| 103 | 计算机导论 | 92 |
| 105 | 计算机导论 | 88 |
| 109 | 计算机导论 | 76 |
| 103 | 操作系统 | 86 |
| 105 | 操作系统 | 75 |
| 109 | 操作系统 | 68 |
| 103 | 数字电路 | 85 |
| 105 | 数字电路 | 79 |
| 109 | 数字电路 | 81 |
+------+-----------------+--------+

三表关联查询

查询所有学生的 name 、课程名 ( course 表中的 name ) 和 degree

只有 score 表中关联学生的学号和课堂号,我们只要围绕着 score 这张表查询就好了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT * FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 | 92 |
| 103 | 3-245 | 86 |
| 103 | 6-166 | 85 |
| 105 | 3-105 | 88 |
| 105 | 3-245 | 75 |
| 105 | 6-166 | 79 |
| 109 | 3-105 | 76 |
| 109 | 3-245 | 68 |
| 109 | 6-166 | 81 |
+------+-------+--------+

只要把 s_noc_no 替换成 studentsrouse 表中对应的 name 字段值就好了。

首先把 s_no 替换成 student 表中的 name 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
SELECT name, c_no, degree FROM student, score WHERE student.no = score.s_no;
+-----------+-------+--------+
| name | c_no | degree |
+-----------+-------+--------+
| 王丽 | 3-105 | 92 |
| 王丽 | 3-245 | 86 |
| 王丽 | 6-166 | 85 |
| 王芳 | 3-105 | 88 |
| 王芳 | 3-245 | 75 |
| 王芳 | 6-166 | 79 |
| 赵铁柱 | 3-105 | 76 |
| 赵铁柱 | 3-245 | 68 |
| 赵铁柱 | 6-166 | 81 |
+-----------+-------+--------+

再把 c_no 替换成 course 表中的 name 字段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 课程表
SELECT no, name FROM course;
+-------+-----------------+
| no | name |
+-------+-----------------+
| 3-105 | 计算机导论 |
| 3-245 | 操作系统 |
| 6-166 | 数字电路 |
| 9-888 | 高等数学 |
+-------+-----------------+

-- 由于字段名存在重复,使用 "表名.字段名 as 别名" 代替。
SELECT student.name as s_name, course.name as c_name, degree
FROM student, score, course
WHERE student.NO = score.s_no
AND score.c_no = course.no;

子查询加分组求平均分

查询 95031 班学生每门课程的平均成绩。

score 表中根据 student 表的学生编号筛选出学生的课堂号和成绩:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- IN (..): 将筛选出的学生号当做 s_no 的条件查询
SELECT s_no, c_no, degree FROM score
WHERE s_no IN (SELECT no FROM student WHERE class = '95031');
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 105 | 3-105 | 88 |
| 105 | 3-245 | 75 |
| 105 | 6-166 | 79 |
| 109 | 3-105 | 76 |
| 109 | 3-245 | 68 |
| 109 | 6-166 | 81 |
+------+-------+--------+

这时只要将 c_no 分组一下就能得出 95031 班学生每门课的平均成绩:

1
2
3
4
5
6
7
8
9
10
SELECT c_no, AVG(degree) FROM score
WHERE s_no IN (SELECT no FROM student WHERE class = '95031')
GROUP BY c_no;
+-------+-------------+
| c_no | AVG(degree) |
+-------+-------------+
| 3-105 | 82.0000 |
| 3-245 | 71.5000 |
| 6-166 | 80.0000 |
+-------+-------------+

子查询 - 1

查询在 3-105 课程中,所有成绩高于 109 号同学的记录。

首先筛选出课堂号为 3-105 ,在找出所有成绩高于 109 号同学的的行。

1
2
3
SELECT * FROM score 
WHERE c_no = '3-105'
AND degree > (SELECT degree FROM score WHERE s_no = '109' AND c_no = '3-105');

子查询 - 2

查询所有成绩高于 109 号同学的 3-105 课程成绩记录。

1
2
3
-- 不限制课程号,只要成绩大于109号同学的3-105课程成绩就可以。
SELECT * FROM score
WHERE degree > (SELECT degree FROM score WHERE s_no = '109' AND c_no = '3-105');

YEAR 函数与带 IN 关键字查询

查询所有和 101108 号学生同年出生的 nonamebirthday 列。

1
2
3
-- YEAR(..): 取出日期中的年份
SELECT no, name, birthday FROM student
WHERE YEAR(birthday) IN (SELECT YEAR(birthday) FROM student WHERE no IN (101, 108));

多层嵌套子查询

查询 '张旭' 教师任课的学生成绩表。

首先找到教师编号:

1
SELECT NO FROM teacher WHERE NAME = '张旭'

通过 sourse 表找到该教师课程号:

1
SELECT NO FROM course WHERE t_no = ( SELECT NO FROM teacher WHERE NAME = '张旭' );

通过筛选出的课程号查询成绩表:

1
2
3
4
5
SELECT * FROM score WHERE c_no = (
SELECT no FROM course WHERE t_no = (
SELECT no FROM teacher WHERE NAME = '张旭'
)
);

多表查询

查询某选修课程多于5个同学的教师姓名。

首先在 teacher 表中,根据 no 字段来判断该教师的同一门课程是否有至少5名学员选修:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 查询 teacher 表
SELECT no, name FROM teacher;
+-----+--------+
| no | name |
+-----+--------+
| 804 | 李诚 |
| 825 | 王萍 |
| 831 | 刘冰 |
| 856 | 张旭 |
+-----+--------+

SELECT name FROM teacher WHERE no IN (
-- 在这里找到对应的条件
);

查看和教师编号有有关的表的信息:

1
2
3
4
5
6
7
8
9
10
SELECT * FROM course;
-- t_no: 教师编号
+-------+-----------------+------+
| no | name | t_no |
+-------+-----------------+------+
| 3-105 | 计算机导论 | 825 |
| 3-245 | 操作系统 | 804 |
| 6-166 | 数字电路 | 856 |
| 9-888 | 高等数学 | 831 |
+-------+-----------------+------+

我们已经找到和教师编号有关的字段就在 course 表中,但是还无法知道哪门课程至少有5名学生选修,所以还需要根据 score 表来查询:

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
-- 在此之前向 score 插入一些数据,以便丰富查询条件。
INSERT INTO score VALUES ('101', '3-105', '90');
INSERT INTO score VALUES ('102', '3-105', '91');
INSERT INTO score VALUES ('104', '3-105', '89');

-- 查询 score 表
SELECT * FROM score;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 | 90 |
| 102 | 3-105 | 91 |
| 103 | 3-105 | 92 |
| 103 | 3-245 | 86 |
| 103 | 6-166 | 85 |
| 104 | 3-105 | 89 |
| 105 | 3-105 | 88 |
| 105 | 3-245 | 75 |
| 105 | 6-166 | 79 |
| 109 | 3-105 | 76 |
| 109 | 3-245 | 68 |
| 109 | 6-166 | 81 |
+------+-------+--------+

-- 在 score 表中将 c_no 作为分组,并且限制 c_no 持有至少 5 条数据。
SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5;
+-------+
| c_no |
+-------+
| 3-105 |
+-------+

根据筛选出来的课程号,找出在某课程中,拥有至少5名学员的教师编号:

1
2
3
4
5
6
7
8
SELECT t_no FROM course WHERE no IN (
SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5
);
+------+
| t_no |
+------+
| 825 |
+------+

teacher 表中,根据筛选出来的教师编号找到教师姓名:

1
2
3
4
5
6
SELECT name FROM teacher WHERE no IN (
-- 最终条件
SELECT t_no FROM course WHERE no IN (
SELECT c_no FROM score GROUP BY c_no HAVING COUNT(*) > 5
)
);

子查询 - 3

查询 “计算机系” 课程的成绩表。

思路是,先找出 course 表中所有 计算机系 课程的编号,然后根据这个编号查询 score 表。

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
-- 通过 teacher 表查询所有 `计算机系` 的教师编号
SELECT no, name, department FROM teacher WHERE department = '计算机系'
+-----+--------+--------------+
| no | name | department |
+-----+--------+--------------+
| 804 | 李诚 | 计算机系 |
| 825 | 王萍 | 计算机系 |
+-----+--------+--------------+

-- 通过 course 表查询该教师的课程编号
SELECT no FROM course WHERE t_no IN (
SELECT no FROM teacher WHERE department = '计算机系'
);
+-------+
| no |
+-------+
| 3-245 |
| 3-105 |
+-------+

-- 根据筛选出来的课程号查询成绩表
SELECT * FROM score WHERE c_no IN (
SELECT no FROM course WHERE t_no IN (
SELECT no FROM teacher WHERE department = '计算机系'
)
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-245 | 86 |
| 105 | 3-245 | 75 |
| 109 | 3-245 | 68 |
| 101 | 3-105 | 90 |
| 102 | 3-105 | 91 |
| 103 | 3-105 | 92 |
| 104 | 3-105 | 89 |
| 105 | 3-105 | 88 |
| 109 | 3-105 | 76 |
+------+-------+--------+

UNION 和 NOTIN 的使用

查询 计算机系电子工程系 中的不同职称的教师。

1
2
3
4
5
6
7
8
9
-- NOT: 代表逻辑非
SELECT * FROM teacher WHERE department = '计算机系' AND profession NOT IN (
SELECT profession FROM teacher WHERE department = '电子工程系'
)
-- 合并两个集
UNION
SELECT * FROM teacher WHERE department = '电子工程系' AND profession NOT IN (
SELECT profession FROM teacher WHERE department = '计算机系'
);

ANY 表示至少一个 - DESC ( 降序 )

查询课程 3-105 且成绩 至少 高于 3-245score 表。

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
SELECT * FROM score WHERE c_no = '3-105';
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 | 90 |
| 102 | 3-105 | 91 |
| 103 | 3-105 | 92 |
| 104 | 3-105 | 89 |
| 105 | 3-105 | 88 |
| 109 | 3-105 | 76 |
+------+-------+--------+

SELECT * FROM score WHERE c_no = '3-245';
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-245 | 86 |
| 105 | 3-245 | 75 |
| 109 | 3-245 | 68 |
+------+-------+--------+

-- ANY: 符合SQL语句中的任意条件。
-- 也就是说,在 3-105 成绩中,只要有一个大于从 3-245 筛选出来的任意行就符合条件,
-- 最后根据降序查询结果。
SELECT * FROM score WHERE c_no = '3-105' AND degree > ANY(
SELECT degree FROM score WHERE c_no = '3-245'
) ORDER BY degree DESC;
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 | 92 |
| 102 | 3-105 | 91 |
| 101 | 3-105 | 90 |
| 104 | 3-105 | 89 |
| 105 | 3-105 | 88 |
| 109 | 3-105 | 76 |
+------+-------+--------+

表示所有的 ALL

查询课程 3-105 且成绩高于 3-245score 表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-- 只需对上一道题稍作修改。
-- ALL: 符合SQL语句中的所有条件。
-- 也就是说,在 3-105 每一行成绩中,都要大于从 3-245 筛选出来全部行才算符合条件。
SELECT * FROM score WHERE c_no = '3-105' AND degree > ALL(
SELECT degree FROM score WHERE c_no = '3-245'
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 | 90 |
| 102 | 3-105 | 91 |
| 103 | 3-105 | 92 |
| 104 | 3-105 | 89 |
| 105 | 3-105 | 88 |
+------+-------+--------+

复制表的数据作为条件查询

查询某课程成绩比该课程平均成绩低的 score 表。

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
-- 查询平均分
SELECT c_no, AVG(degree) FROM score GROUP BY c_no;
+-------+-------------+
| c_no | AVG(degree) |
+-------+-------------+
| 3-105 | 87.6667 |
| 3-245 | 76.3333 |
| 6-166 | 81.6667 |
+-------+-------------+

-- 查询 score 表
SELECT degree FROM score;
+--------+
| degree |
+--------+
| 90 |
| 91 |
| 92 |
| 86 |
| 85 |
| 89 |
| 88 |
| 75 |
| 79 |
| 76 |
| 68 |
| 81 |
+--------+

-- 将表 b 作用于表 a 中查询数据
-- score a (b): 将表声明为 a (b),
-- 如此就能用 a.c_no = b.c_no 作为条件执行查询了。
SELECT * FROM score a WHERE degree < (
(SELECT AVG(degree) FROM score b WHERE a.c_no = b.c_no)
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 105 | 3-245 | 75 |
| 105 | 6-166 | 79 |
| 109 | 3-105 | 76 |
| 109 | 3-245 | 68 |
| 109 | 6-166 | 81 |
+------+-------+--------+

子查询 - 4

查询所有任课 ( 在 course 表里有课程 ) 教师的 namedepartment

1
2
3
4
5
6
7
8
9
SELECT name, department FROM teacher WHERE no IN (SELECT t_no FROM course);
+--------+-----------------+
| name | department |
+--------+-----------------+
| 李诚 | 计算机系 |
| 王萍 | 计算机系 |
| 刘冰 | 电子工程系 |
| 张旭 | 电子工程系 |
+--------+-----------------+

条件加组筛选

查询 student 表中至少有 2 名男生的 class

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
-- 查看学生表信息
SELECT * FROM student;
+-----+-----------+-----+------------+-------+
| no | name | sex | birthday | class |
+-----+-----------+-----+------------+-------+
| 101 | 曾华 || 1977-09-01 | 95033 |
| 102 | 匡明 || 1975-10-02 | 95031 |
| 103 | 王丽 || 1976-01-23 | 95033 |
| 104 | 李军 || 1976-02-20 | 95033 |
| 105 | 王芳 || 1975-02-10 | 95031 |
| 106 | 陆军 || 1974-06-03 | 95031 |
| 107 | 王尼玛 || 1976-02-20 | 95033 |
| 108 | 张全蛋 || 1975-02-10 | 95031 |
| 109 | 赵铁柱 || 1974-06-03 | 95031 |
| 110 | 张飞 || 1974-06-03 | 95038 |
+-----+-----------+-----+------------+-------+

-- 只查询性别为男,然后按 class 分组,并限制 class 行大于 1。
SELECT class FROM student WHERE sex = '男' GROUP BY class HAVING COUNT(*) > 1;
+-------+
| class |
+-------+
| 95033 |
| 95031 |
+-------+

NOTLIKE 模糊查询取反

查询 student 表中不姓 “王” 的同学记录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- NOT: 取反
-- LIKE: 模糊查询
mysql> SELECT * FROM student WHERE name NOT LIKE '王%';
+-----+-----------+-----+------------+-------+
| no | name | sex | birthday | class |
+-----+-----------+-----+------------+-------+
| 101 | 曾华 || 1977-09-01 | 95033 |
| 102 | 匡明 || 1975-10-02 | 95031 |
| 104 | 李军 || 1976-02-20 | 95033 |
| 106 | 陆军 || 1974-06-03 | 95031 |
| 108 | 张全蛋 || 1975-02-10 | 95031 |
| 109 | 赵铁柱 || 1974-06-03 | 95031 |
| 110 | 张飞 || 1974-06-03 | 95038 |
+-----+-----------+-----+------------+-------+

YEAR 与 NOW 函数

查询 student 表中每个学生的姓名和年龄。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 使用函数 YEAR(NOW()) 计算出当前年份,减去出生年份后得出年龄。
SELECT name, YEAR(NOW()) - YEAR(birthday) as age FROM student;
+-----------+------+
| name | age |
+-----------+------+
| 曾华 | 42 |
| 匡明 | 44 |
| 王丽 | 43 |
| 李军 | 43 |
| 王芳 | 44 |
| 陆军 | 45 |
| 王尼玛 | 43 |
| 张全蛋 | 44 |
| 赵铁柱 | 45 |
| 张飞 | 45 |
+-----------+------+

MAX 与 MIN 函数

查询 student 表中最大和最小的 birthday 值。

1
2
3
4
5
6
SELECT MAX(birthday), MIN(birthday) FROM student;
+---------------+---------------+
| MAX(birthday) | MIN(birthday) |
+---------------+---------------+
| 1977-09-01 | 1974-06-03 |
+---------------+---------------+

多段排序

classbirthday 从大到小的顺序查询 student 表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
SELECT * FROM student ORDER BY class DESC, birthday;
+-----+-----------+-----+------------+-------+
| no | name | sex | birthday | class |
+-----+-----------+-----+------------+-------+
| 110 | 张飞 || 1974-06-03 | 95038 |
| 103 | 王丽 || 1976-01-23 | 95033 |
| 104 | 李军 || 1976-02-20 | 95033 |
| 107 | 王尼玛 || 1976-02-20 | 95033 |
| 101 | 曾华 || 1977-09-01 | 95033 |
| 106 | 陆军 || 1974-06-03 | 95031 |
| 109 | 赵铁柱 || 1974-06-03 | 95031 |
| 105 | 王芳 || 1975-02-10 | 95031 |
| 108 | 张全蛋 || 1975-02-10 | 95031 |
| 102 | 匡明 || 1975-10-02 | 95031 |
+-----+-----------+-----+------------+-------+

子查询 - 5

查询 “男” 教师及其所上的课程。

1
2
3
4
5
6
7
SELECT * FROM course WHERE t_no in (SELECT no FROM teacher WHERE sex = '男');
+-------+--------------+------+
| no | name | t_no |
+-------+--------------+------+
| 3-245 | 操作系统 | 804 |
| 6-166 | 数字电路 | 856 |
+-------+--------------+------+

MAX 函数与子查询

查询最高分同学的 score 表。

1
2
3
4
5
6
7
8
9
10
11
-- 找出最高成绩(该查询只能有一个结果)
SELECT MAX(degree) FROM score;

-- 根据上面的条件筛选出所有最高成绩表,
-- 该查询可能有多个结果,假设 degree 值多次符合条件。
SELECT * FROM score WHERE degree = (SELECT MAX(degree) FROM score);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 103 | 3-105 | 92 |
+------+-------+--------+

子查询 - 6

查询和 “李军” 同性别的所有同学 name

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- 首先将李军的性别作为条件取出来
SELECT sex FROM student WHERE name = '李军';
+-----+
| sex |
+-----+
||
+-----+

-- 根据性别查询 name 和 sex
SELECT name, sex FROM student WHERE sex = (
SELECT sex FROM student WHERE name = '李军'
);
+-----------+-----+
| name | sex |
+-----------+-----+
| 曾华 ||
| 匡明 ||
| 李军 ||
| 陆军 ||
| 王尼玛 ||
| 张全蛋 ||
| 赵铁柱 ||
| 张飞 ||
+-----------+-----+

子查询 - 7

查询和 “李军” 同性别且同班的同学 name

1
2
3
4
5
6
7
8
9
10
11
12
SELECT name, sex, class FROM student WHERE sex = (
SELECT sex FROM student WHERE name = '李军'
) AND class = (
SELECT class FROM student WHERE name = '李军'
);
+-----------+-----+-------+
| name | sex | class |
+-----------+-----+-------+
| 曾华 || 95033 |
| 李军 || 95033 |
| 王尼玛 || 95033 |
+-----------+-----+-------+

子查询 - 8

查询所有选修 “计算机导论” 课程的 “男” 同学成绩表。

需要的 “计算机导论” 和性别为 “男” 的编号可以在 coursestudent 表中找到。

1
2
3
4
5
6
7
8
9
10
11
12
13
SELECT * FROM score WHERE c_no = (
SELECT no FROM course WHERE name = '计算机导论'
) AND s_no IN (
SELECT no FROM student WHERE sex = '男'
);
+------+-------+--------+
| s_no | c_no | degree |
+------+-------+--------+
| 101 | 3-105 | 90 |
| 102 | 3-105 | 91 |
| 104 | 3-105 | 89 |
| 109 | 3-105 | 76 |
+------+-------+--------+

按等级查询

建立一个 grade 表代表学生的成绩等级,并插入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE grade (
low INT(3),
upp INT(3),
grade char(1)
);

INSERT INTO grade VALUES (90, 100, 'A');
INSERT INTO grade VALUES (80, 89, 'B');
INSERT INTO grade VALUES (70, 79, 'C');
INSERT INTO grade VALUES (60, 69, 'D');
INSERT INTO grade VALUES (0, 59, 'E');

SELECT * FROM grade;
+------+------+-------+
| low | upp | grade |
+------+------+-------+
| 90 | 100 | A |
| 80 | 89 | B |
| 70 | 79 | C |
| 60 | 69 | D |
| 0 | 59 | E |
+------+------+-------+

查询所有学生的 s_noc_nograde 列。

思路是,使用区间 ( BETWEEN ) 查询,判断学生的成绩 ( degree ) 在 grade 表的 lowupp 之间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
SELECT s_no, c_no, grade FROM score, grade 
WHERE degree BETWEEN low AND upp;
+------+-------+-------+
| s_no | c_no | grade |
+------+-------+-------+
| 101 | 3-105 | A |
| 102 | 3-105 | A |
| 103 | 3-105 | A |
| 103 | 3-245 | B |
| 103 | 6-166 | B |
| 104 | 3-105 | B |
| 105 | 3-105 | B |
| 105 | 3-245 | C |
| 105 | 6-166 | C |
| 109 | 3-105 | C |
| 109 | 3-245 | D |
| 109 | 6-166 | B |
+------+-------+-------+

连接查询

准备用于测试连接查询的数据:

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
CREATE DATABASE testJoin;

CREATE TABLE person (
id INT,
name VARCHAR(20),
cardId INT
);

CREATE TABLE card (
id INT,
name VARCHAR(20)
);

INSERT INTO card VALUES (1, '饭卡'), (2, '建行卡'), (3, '农行卡'), (4, '工商卡'), (5, '邮政卡');
SELECT * FROM card;
+------+-----------+
| id | name |
+------+-----------+
| 1 | 饭卡 |
| 2 | 建行卡 |
| 3 | 农行卡 |
| 4 | 工商卡 |
| 5 | 邮政卡 |
+------+-----------+

INSERT INTO person VALUES (1, '张三', 1), (2, '李四', 3), (3, '王五', 6);
SELECT * FROM person;
+------+--------+--------+
| id | name | cardId |
+------+--------+--------+
| 1 | 张三 | 1 |
| 2 | 李四 | 3 |
| 3 | 王五 | 6 |
+------+--------+--------+

分析两张表发现,person 表并没有为 cardId 字段设置一个在 card 表中对应的 id 外键。如果设置了的话,personcardId 字段值为 6 的行就插不进去,因为该 cardId 值在 card 表中并没有。

内连接

要查询这两张表中有关系的数据,可以使用 INNER JOIN ( 内连接 ) 将它们连接在一起。

1
2
3
4
5
6
7
8
9
10
11
12
-- INNER JOIN: 表示为内连接,将两张表拼接在一起。
-- on: 表示要执行某个条件。
SELECT * FROM person INNER JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id | name | cardId | id | name |
+------+--------+--------+------+-----------+
| 1 | 张三 | 1 | 1 | 饭卡 |
| 2 | 李四 | 3 | 3 | 农行卡 |
+------+--------+--------+------+-----------+

-- 将 INNER 关键字省略掉,结果也是一样的。
-- SELECT * FROM person JOIN card on person.cardId = card.id;

注意:card 的整张表被连接到了右边。

左外连接

完整显示左边的表 ( person ) ,右边的表如果符合条件就显示,不符合则补 NULL

1
2
3
4
5
6
7
8
9
-- LEFT JOIN 也叫做 LEFT OUTER JOIN,用这两种方式的查询结果是一样的。
SELECT * FROM person LEFT JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id | name | cardId | id | name |
+------+--------+--------+------+-----------+
| 1 | 张三 | 1 | 1 | 饭卡 |
| 2 | 李四 | 3 | 3 | 农行卡 |
| 3 | 王五 | 6 | NULL | NULL |
+------+--------+--------+------+-----------+

右外链接

完整显示右边的表 ( card ) ,左边的表如果符合条件就显示,不符合则补 NULL

1
2
3
4
5
6
7
8
9
10
SELECT * FROM person RIGHT JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id | name | cardId | id | name |
+------+--------+--------+------+-----------+
| 1 | 张三 | 1 | 1 | 饭卡 |
| 2 | 李四 | 3 | 3 | 农行卡 |
| NULL | NULL | NULL | 2 | 建行卡 |
| NULL | NULL | NULL | 4 | 工商卡 |
| NULL | NULL | NULL | 5 | 邮政卡 |
+------+--------+--------+------+-----------+

全外链接

完整显示两张表的全部数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- MySQL 不支持这种语法的全外连接
-- SELECT * FROM person FULL JOIN card on person.cardId = card.id;
-- 出现错误:
-- ERROR 1054 (42S22): Unknown column 'person.cardId' in 'on clause'

-- MySQL全连接语法,使用 UNION 将两张表合并在一起。
SELECT * FROM person LEFT JOIN card on person.cardId = card.id
UNION
SELECT * FROM person RIGHT JOIN card on person.cardId = card.id;
+------+--------+--------+------+-----------+
| id | name | cardId | id | name |
+------+--------+--------+------+-----------+
| 1 | 张三 | 1 | 1 | 饭卡 |
| 2 | 李四 | 3 | 3 | 农行卡 |
| 3 | 王五 | 6 | NULL | NULL |
| NULL | NULL | NULL | 2 | 建行卡 |
| NULL | NULL | NULL | 4 | 工商卡 |
| NULL | NULL | NULL | 5 | 邮政卡 |
+------+--------+--------+------+-----------+

事务

在 MySQL 中,事务其实是一个最小的不可分割的工作单元。事务能够保证一个业务的完整性

比如我们的银行转账:

1
2
3
4
5
-- a -> -100
UPDATE user set money = money - 100 WHERE name = 'a';

-- b -> +100
UPDATE user set money = money + 100 WHERE name = 'b';

在实际项目中,假设只有一条 SQL 语句执行成功,而另外一条执行失败了,就会出现数据前后不一致。

因此,在执行多条有关联 SQL 语句时,事务可能会要求这些 SQL 语句要么同时执行成功,要么就都执行失败。

如何控制事务 - COMMIT / ROLLBACK

在 MySQL 中,事务的自动提交状态默认是开启的。

1
2
3
4
5
6
7
-- 查询事务的自动提交状态
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
| 1 |
+--------------+

自动提交的作用:当我们执行一条 SQL 语句的时候,其产生的效果就会立即体现出来,且不能回滚

什么是回滚?举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
CREATE DATABASE bank;

USE bank;

CREATE TABLE user (
id INT PRIMARY KEY,
name VARCHAR(20),
money INT
);

INSERT INTO user VALUES (1, 'a', 1000);

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+

可以看到,在执行插入语句后数据立刻生效,原因是 MySQL 中的事务自动将它提交到了数据库中。那么所谓回滚的意思就是,撤销执行过的所有 SQL 语句,使其回滚到最后一次提交数据时的状态。

在 MySQL 中使用 ROLLBACK 执行回滚:

1
2
3
4
5
6
7
8
9
-- 回滚到最后一次提交
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+

由于所有执行过的 SQL 语句都已经被提交过了,所以数据并没有发生回滚。那如何让数据可以发生回滚?

1
2
3
4
5
6
7
8
9
10
-- 关闭自动提交
SET AUTOCOMMIT = 0;

-- 查询自动提交状态
SELECT @@AUTOCOMMIT;
+--------------+
| @@AUTOCOMMIT |
+--------------+
| 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
INSERT INTO user VALUES (2, 'b', 1000);

-- 关闭 AUTOCOMMIT 后,数据的变化是在一张虚拟的临时数据表中展示,
-- 发生变化的数据并没有真正插入到数据表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+

-- 数据表中的真实数据其实还是:
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+

-- 由于数据还没有真正提交,可以使用回滚
ROLLBACK;

-- 再次查询
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
+----+------+-------+

那如何将虚拟的数据真正提交到数据库中?使用 COMMIT :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
INSERT INTO user VALUES (2, 'b', 1000);
-- 手动提交数据(持久性),
-- 将数据真正提交到数据库中,执行后不能再回滚提交过的数据。
COMMIT;

-- 提交后测试回滚
ROLLBACK;

-- 再次查询(回滚无效了)
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+

总结

  1. 自动提交

    • 查看自动提交状态:SELECT @@AUTOCOMMIT

    • 设置自动提交状态:SET AUTOCOMMIT = 0

  2. 手动提交

    @@AUTOCOMMIT = 0 时,使用 COMMIT 命令提交事务。

  3. 事务回滚

    @@AUTOCOMMIT = 0 时,使用 ROLLBACK 命令回滚事务。

事务的实际应用,让我们再回到银行转账项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 转账
UPDATE user set money = money - 100 WHERE name = 'a';

-- 到账
UPDATE user set money = money + 100 WHERE name = 'b';

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
+----+------+-------+

这时假设在转账时发生了意外,就可以使用 ROLLBACK 回滚到最后一次提交的状态:

1
2
3
4
5
6
7
8
9
10
-- 假设转账发生了意外,需要回滚。
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+

这时我们又回到了发生意外之前的状态,也就是说,事务给我们提供了一个可以反悔的机会。假设数据没有发生意外,这时可以手动将数据真正提交到数据表中:COMMIT

手动开启事务 - BEGIN / START TRANSACTION

事务的默认提交被开启 ( @@AUTOCOMMIT = 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
-- 使用 BEGIN 或者 START TRANSACTION 手动开启一个事务
-- START TRANSACTION;
BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';

-- 由于手动开启的事务没有开启自动提交,
-- 此时发生变化的数据仍然是被保存在一张临时表中。
SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
+----+------+-------+

-- 测试回滚
ROLLBACK;

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 1000 |
| 2 | b | 1000 |
+----+------+-------+

仍然使用 COMMIT 提交数据,提交后无法再发生本次事务的回滚。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
BEGIN;
UPDATE user set money = money - 100 WHERE name = 'a';
UPDATE user set money = money + 100 WHERE name = 'b';

SELECT * FROM user;
+----+------+-------+
| id | name | money |
+----+------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
+----+------+-------+

-- 提交数据
COMMIT;

-- 测试回滚(无效,因为表的数据已经被提交)
ROLLBACK;

事务的 ACID 特征与使用

事务的四大特征:

  • A 原子性:事务是最小的单位,不可以再分割;

  • C 一致性:要求同一事务中的 SQL 语句,必须保证同时成功或者失败;

  • I 隔离性:事务1 和 事务2 之间是具有隔离性的;

  • D 持久性:事务一旦结束 ( COMMIT ) ,就不可以再返回了 ( ROLLBACK ) 。

事务的隔离性

事务的隔离性可分为四种 ( 性能从低到高 )

  1. READ UNCOMMITTED ( 读取未提交 )

    如果有多个事务,那么任意事务都可以看见其他事务的未提交数据

  2. READ COMMITTED ( 读取已提交 )

    只能读取到其他事务已经提交的数据

  3. REPEATABLE READ ( 可被重复读 )

    如果有多个连接都开启了事务,那么事务之间不能共享数据记录,否则只能共享已提交的记录。

  4. SERIALIZABLE ( 串行化 )

    所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作

查看当前数据库的默认隔离级别:

1
2
3
4
5
6
7
8
9
10
11
12
-- MySQL 8.x, GLOBAL 表示系统级别,不加表示会话级别。
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
SELECT @@TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ | -- MySQL的默认隔离级别,可以重复读。
+--------------------------------+

-- MySQL 5.x
SELECT @@GLOBAL.TX_ISOLATION;
SELECT @@TX_ISOLATION;

修改隔离级别:

1
2
3
4
5
6
7
8
9
10
-- 设置系统隔离级别,LEVEL 后面表示要设置的隔离级别 (READ UNCOMMITTED)。
SET GLOBAL TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;

-- 查询系统隔离级别,发现已经被修改。
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-UNCOMMITTED |
+--------------------------------+

脏读

测试 READ UNCOMMITTED ( 读取未提交 ) 的隔离性:

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
INSERT INTO user VALUES (3, '小明', 1000);
INSERT INTO user VALUES (4, '淘宝店', 1000);

SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
+----+-----------+-------+

-- 开启一个事务操作数据
-- 假设小明在淘宝店买了一双800块钱的鞋子:
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘宝店';

-- 然后淘宝店在另一方查询结果,发现钱已到账。
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 200 |
| 4 | 淘宝店 | 1800 |
+----+-----------+-------+

由于小明的转账是在新开启的事务上进行操作的,而该操作的结果是可以被其他事务(另一方的淘宝店)看见的,因此淘宝店的查询结果是正确的,淘宝店确认到账。但就在这时,如果小明在它所处的事务上又执行了 ROLLBACK 命令,会发生什么?

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 小明所处的事务
ROLLBACK;

-- 此时无论对方是谁,如果再去查询结果就会发现:
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
+----+-----------+-------+

这就是所谓的脏读,一个事务读取到另外一个事务还未提交的数据。这在实际开发中是不允许出现的。

读取已提交

把隔离级别设置为 READ COMMITTED

1
2
3
4
5
6
7
SET GLOBAL TRANSACTION ISOLATION LEVEL READ COMMITTED;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| READ-COMMITTED |
+--------------------------------+

这样,再有新的事务连接进来时,它们就只能查询到已经提交过的事务数据了。但是对于当前事务来说,它们看到的还是未提交的数据,例如:

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
-- 正在操作数据事务(当前事务)
START TRANSACTION;
UPDATE user SET money = money - 800 WHERE name = '小明';
UPDATE user SET money = money + 800 WHERE name = '淘宝店';

-- 虽然隔离级别被设置为了 READ COMMITTED,但在当前事务中,
-- 它看到的仍然是数据表中临时改变数据,而不是真正提交过的数据。
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 200 |
| 4 | 淘宝店 | 1800 |
+----+-----------+-------+


-- 假设此时在远程开启了一个新事务,连接到数据库。
$ mysql -u root -p12345612

-- 此时远程连接查询到的数据只能是已经提交过的
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
+----+-----------+-------+

但是这样还有问题,那就是假设一个事务在操作数据时,其他事务干扰了这个事务的数据。例如:

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
-- 小张在查询数据的时候发现:
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 200 |
| 4 | 淘宝店 | 1800 |
+----+-----------+-------+

-- 在小张求表的 money 平均值之前,小王做了一个操作:
START TRANSACTION;
INSERT INTO user VALUES (5, 'c', 100);
COMMIT;

-- 此时表的真实数据是:
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
| 5 | c | 100 |
+----+-----------+-------+

-- 这时小张再求平均值的时候,就会出现计算不相符合的情况:
SELECT AVG(money) FROM user;
+------------+
| AVG(money) |
+------------+
| 820.0000 |
+------------+

虽然 READ COMMITTED 让我们只能读取到其他事务已经提交的数据,但还是会出现问题,就是在读取同一个表的数据时,可能会发生前后不一致的情况。这被称为不可重复读现象 ( READ COMMITTED )

幻读

将隔离级别设置为 REPEATABLE READ ( 可被重复读取 ) :

1
2
3
4
5
6
7
SET GLOBAL TRANSACTION ISOLATION LEVEL REPEATABLE READ;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| REPEATABLE-READ |
+--------------------------------+

测试 REPEATABLE READ ,假设在两个不同的连接上分别执行 START TRANSACTION :

1
2
3
4
5
6
7
8
9
-- 小张 - 成都
START TRANSACTION;
INSERT INTO user VALUES (6, 'd', 1000);

-- 小王 - 北京
START TRANSACTION;

-- 小张 - 成都
COMMIT;

当前事务开启后,没提交之前,查询不到,提交后可以被查询到。但是,在提交之前其他事务被开启了,那么在这条事务线上,就不会查询到当前有操作事务的连接。相当于开辟出一条单独的线程。

无论小张是否执行过 COMMIT ,在小王这边,都不会查询到小张的事务记录,而是只会查询到自己所处事务的记录:

1
2
3
4
5
6
7
8
9
10
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
| 5 | c | 100 |
+----+-----------+-------+

这是因为小王在此之前开启了一个新的事务 ( START TRANSACTION ) ,那么在他的这条新事务的线上,跟其他事务是没有联系的,也就是说,此时如果其他事务正在操作数据,它是不知道的。

然而事实是,在真实的数据表中,小张已经插入了一条数据。但是小王此时并不知道,也插入了同一条数据,会发生什么呢?

1
2
INSERT INTO user VALUES (6, 'd', 1000);
-- ERROR 1062 (23000): Duplicate entry '6' for key 'PRIMARY'

报错了,操作被告知已存在主键为 6 的字段。这种现象也被称为幻读,一个事务提交的数据,不能被其他事务读取到

串行化

顾名思义,就是所有事务的写入操作全都是串行化的。什么意思?把隔离级别修改成 SERIALIZABLE :

1
2
3
4
5
6
7
SET GLOBAL TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SELECT @@GLOBAL.TRANSACTION_ISOLATION;
+--------------------------------+
| @@GLOBAL.TRANSACTION_ISOLATION |
+--------------------------------+
| SERIALIZABLE |
+--------------------------------+

还是拿小张和小王来举例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
-- 小张 - 成都
START TRANSACTION;

-- 小王 - 北京
START TRANSACTION;

-- 开启事务之前先查询表,准备操作数据。
SELECT * FROM user;
+----+-----------+-------+
| id | name | money |
+----+-----------+-------+
| 1 | a | 900 |
| 2 | b | 1100 |
| 3 | 小明 | 1000 |
| 4 | 淘宝店 | 1000 |
| 5 | c | 100 |
| 6 | d | 1000 |
+----+-----------+-------+

-- 发现没有 7 号王小花,于是插入一条数据:
INSERT INTO user VALUES (7, '王小花', 1000);

此时会发生什么呢?由于现在的隔离级别是 SERIALIZABLE ( 串行化 ) ,串行化的意思就是:假设把所有的事务都放在一个串行的队列中,那么所有的事务都会按照固定顺序执行,执行完一个事务后再继续执行下一个事务的写入操作 ( 这意味着队列中同时只能执行一个事务的写入操作 ) 。

根据这个解释,小王在插入数据时,会出现等待状态,直到小张执行 COMMIT 结束它所处的事务,或者出现等待超时。

SpringIoc注入

Spring IOC 手动装配(注入)

Spring 支持的注入方式共有四种:set 注入、构造器注入、静态工厂注入、实例化工厂注入。

set方法注入

注:

  • 属性字段需要提供set方法

  • 四种方式,推荐使用set方法注入

业务对象 JavaBean
  1. 属性字段提供set方法
1
2
3
4
5
6
7
8
public class UserService {

// 业务对象UserDao set注入(提供set方法)
private UserDao userDao;
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}
}
public class UserService {

    // 业务对象UserDao set注入(提供set方法)
    private UserDao userDao;
    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}

配置文件的bean标签设置property标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!--
IOC通过property标签手动装配(注入):
Set方法注入
name:bean对象中属性字段的名称
ref:指定bean标签的id属性值
-->
<bean id="userDao" class="com.xxxx.dao.UserDao"></bean>
<bean id="userService" class="com.xxxx.service.UserService">
<!--业务对象 注入-->
<property name="userDao" ref="userDao"/>
</bean>
</beans>
常用对象和基本类型

1.属性字段提供set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class UserService {

// 常用对象String set注入(提供set方法)
private String host;
public void setHost(String host) {
this.host = host;
}

// 基本类型Integer set注入(提供set方法)
private Integer port;
public void setPort(Integer port) {
this.port = port;
}
}

2.配置文件的bean标签设置property标签

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!--
IOC通过property标签手动装配(注入):
Set方法注入
name:bean对象中属性字段的名称
value:具体的值(基本类型 常用对象|日期 集合)
-->
<bean id="userService" class="com.xxxx.service.UserService">
<!--常用对象String 注入-->
<property name="host" value="127.0.0.1"/>
<!--基本类型注入-->
<property name="port" value="8080"/>
</bean>

</beans>
集合类型和属性对象

1.属性字段提供set方法

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
public class UserService {

// List集合 set注入(提供set方法)
public List<String> list;
public void setList(List<String> list) {
this.list = list;
}


// Set集合 set注入(提供set方法)
private Set<String> set;
public void setSet(Set<String> set) {
this.set = set;
}


// Map set注入(提供set方法)
private Map<String,Object> map;
public void setMap(Map<String, Object> map) {
this.map = map;
}


// Properties set注入(提供set方法)
private Properties properties;
public void setProperties(Properties properties) {
this.properties = properties;
}

}

2.配置文件的bean标签设置property标签

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
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<!--
IOC通过property标签手动装配(注入):
Set方法注入
name:bean对象中属性字段的名称
value:具体的值(基本类型 常用对象|日期 集合)
-->
<!--List集合 注入-->
<property name="list">
<list>
<value>上海</value>
<value>北京</value>
<value>杭州</value>
</list>
</property>

<!--Set集合注入-->
<property name="set">
<set>
<value>上海SH</value>
<value>北京BJ</value>
<value>杭州HZ</value>
</set>
</property>

<!--Map注入-->
<property name="map">
<map>
<entry>
<key><value>周杰伦</value></key>
<value>我是如此相信</value>
</entry>
<entry>
<key><value>林俊杰</value></key>
<value>可惜没如果</value>
</entry>
<entry>
<key><value>陈奕迅</value></key>
<value>十年</value>
</entry>
</map>
</property>

<!--Properties注入-->
<property name="properties">
<props>
<prop key="上海">东方明珠</prop>
<prop key="北京">天安门</prop>
<prop key="杭州">西湖</prop>
</props>
</property>

</beans>
多个Bean对象作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class UserService {

private UserDao userDao; // JavaBean 对象
private AccountDao accountDao // JavaBean 对象

public UserService(UserDao userDao, AccountDao accountDao) {
this.userDao = userDao;
this.accountDao = accountDao;
}

public void test(){
System.out.println("UserService Test...");

userDao.test();
accountDao.test();
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
IOC通过构造器注入:
通过constructor-arg标签进行注入
name:属性名称
ref:指定bean标签的id属性值
-->
<bean id="userDao" class="com.xxxx.dao.UserDao" ></bean>
<bean id="accountDao" class="com.xxxx.dao.AccountDao" ></bean>

<bean id="userService" class="com.xxxx.service.UserService">
<constructor-arg name="userDao" ref="userDao"></constructor-arg>
<constructor-arg name="accountDao" ref="accountDao"></constructor-arg>
</bean>

</beans>
Bean对象和常用对象作为参数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class UserService {

private UserDao userDao; // JavaBean 对象
private AccountDao accountDao; // JavaBean 对象
private String uname; // 字符串类型

public UserService(UserDao userDao, AccountDao accountDao, String uname) {
this.userDao = userDao;
this.accountDao = accountDao;
this.uname = uname;
}

public void test(){
System.out.println("UserService Test...");

userDao.test();
accountDao.test();
System.out.println("uname:" + uname);
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">
<!--
IOC通过构造器注入:
通过constructor-arg标签进行注入
name:属性名称
ref:指定bean标签的id属性值
value:基本类型 常用对象的值
index:构造器中参数的下标,从0开始
-->
<bean id="userDao" class="com.xxxx.dao.UserDao" ></bean>
<bean id="accountDao" class="com.xxxx.dao.AccountDao" ></bean>
<bean id="userService" class="com.xxxx.service.UserService">
<constructor-arg name="userDao" ref="userDao"></constructor-arg>
<constructor-arg name="accountDao" ref="accountDao"></constructor-arg>
<constructor-arg name="uname" value="admin"></constructor-arg>
</bean>

</beans>
循环依赖问题

Bean通过构造器注入,之间彼此相互依赖对方导致bean无法实例化

问题展示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class AccountService {

private RoleService roleService;

public AccountService(RoleService roleService) {
this.roleService = roleService;
}

public void test() {
System.out.println("AccountService Test...");
}
}

public class RoleService {

private AccountService accountService;

public RoleService(AccountService accountService) {
this.accountService = accountService;
}

public void test() {
System.out.println("RoleService Test...");
}
}

XML配置

1
2
3
4
5
6
7
8
9
10
11
<!--
如果多个bean对象中互相注入,则会出现循环依赖的问题
可以通过set方法注入解决
-->
<bean id="accountService" class="com.xxxx.service.AccountService">
<constructor-arg name="roleService" ref="roleService"/>
</bean>

<bean id="roleService" class="com.xxxx.service.RoleService">
<constructor-arg name="accountService" ref="accountService"/>
</bean>

如何解决:将构造器注入改为set方法注入

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
public class AccountService {

private RoleService roleService;

/* public AccountService(RoleService roleService) {
this.roleService = roleService;
}*/

public void setRoleService(RoleService roleService) {
this.roleService = roleService;
}

public void test() {
System.out.println("AccountService Test...");
}
}

public class RoleService {

private AccountService accountService;

/* public RoleService(AccountService accountService) {
this.accountService = accountService;
}*/

public void setAccountService(AccountService accountService) {
this.accountService = accountService;
}

public void test() {
System.out.println("RoleService Test...");
}
}

XML配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<!--
<bean id="accountService" class="com.xxxx.service.AccountService">
<constructor-arg name="roleService" ref="roleService"/>
</bean>

<bean id="roleService" class="com.xxxx.service.RoleService">
<constructor-arg name="accountService" ref="accountService"/>
</bean>
-->
<!--修改为set方法注入-->
<bean id="accountService" class="com.xxxx.service.AccountService">
<property name="roleService" ref="roleService"/>
</bean>

<bean id="roleService" class="com.xxxx.service.RoleService">
<property name="accountService" ref="accountService"/>
</bean>

@Resource注解

@Resource注解实现自动注入(反射)

  • 默认根据属性字段名称查找对应的bean对象 (属性字段的名称与bean标签的id属性值相等)

  • 如果属性字段名称未找到,则会通过类型(Class类型)查找

  • 属性可以提供set方法,也可以不提供set方法

  • 注解可以声明在属性级别 或 set方法级别

  • 可以设置name属性,name属性值必须与bean标签的id属性值一致;如果设置了name属性值,就只会按照name属性值查找bean对象

  • 当注入接口时,如果接口只有一个实现则正常实例化;如果接口存在多个实现,则需要使用name属性指定需要被实例化的bean对象

1.默认根据属性字段名称查找对应的bean对象 (属性字段的名称与bean标签的id属性值相等)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Resource注解实现自动注入(反射)
* 默认根据属性字段名称查找对应的bean对象 (属性字段的名称与bean标签的id属性值相等)
*/
public class UserService {

@Resource
private UserDao userDao; // 属性字段的名称与bean标签的id属性值相等

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void test() {
// 调用UserDao的方法
userDao.test();
}
}

2.如果属性字段名称未找到,则会通过类型(Class类型)查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Resource注解实现自动注入(反射)
* 如果属性字段名称未找到,则会通过类型(Class类型)查找
*/
public class UserService {

@Resource
private UserDao ud; // 当在配置文件中属性字段名(ud)未找到,则会查找对应的class(UserDao类型)

public void setUd(UserDao ud) {
this.ud = ud;
}

public void test() {
// 调用UserDao的方法
ud.test();
}
}

3.属性可以提供set方法,也可以不提供set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @Resource注解实现自动注入(反射)
* 属性可以提供set方法,也可以不提供set方法
*/
public class UserService {

@Resource
private UserDao userDao; // 不提供set方法


public void test() {
// 调用UserDao的方法
userDao.test();
}
}

4.注解可以声明在属性级别 或 set方法级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Resource注解实现自动注入(反射)
* 注解可以声明在属性级别 或 set方法级别
*/
public class UserService {

private UserDao userDao;

@Resource // 注解也可设置在set方法上
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void test() {
// 调用UserDao的方法
userDao.test();
}
}

可以设置name属性,name属性值必须与bean标签的id属性值一致;如果设置了name属性值,就只会按照name属性值查找bean对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Resource注解实现自动注入(反射)
* 可以设置name属性,name属性值必须与bean的id属性值一致;
* 如果设置了name属性值,就只会按照name属性值查找bean对象
*/
public class UserService {

@Resource(name = "userDao") // name属性值与配置文件中bean标签的id属性值一致
private UserDao ud;


public void test() {
// 调用UserDao的方法
ud.test();
}
}

当注入接口时,如果接口只有一个实现则正常实例化;如果接口存在多个实现,则需要使用name属性指定需要被实例化的bean对象

定义接口类 IUserDao.java

1
2
3
4
5
6
7
8
9
10
/**
* 接口实现类
*/
public class UserDao02 implements IUserDao {

@Override
public void test(){
System.out.println("UserDao02...");
}
}

XML配置文件

1
2
3
4
5
6
7
<!--开启自动化装配(注入)-->
<context:annotation-config/>

<bean id="userService" class="com.xxxx.service.UserService"></bean>

<bean id="userDao01" class="com.xxxx.dao.UserDao01"></bean>
<bean id="userDao02" class="com.xxxx.dao.UserDao01"></bean>

使用注解 UserService.java

1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @Resource注解实现自动注入(反射)
* 当注入接口时,如果接口只有一个实现则正常实例化;如果接口存在多个实现,则需要使用name属性指定需要被实例化的bean对象
*/
public class UserService {

@Resource(name = "userDao01") // name属性值与其中一个实现类的bean标签的id属性值一致
private IUserDao iUserDao; // 注入接口(接口存在多个实现)

public void test() {
iUserDao.test();
}
}

@Autowired注解

@Autowired注解实现自动化注入:

  • 默认通过类型(Class类型)查找bean对象 与属性字段的名称无关

  • 属性可以提供set方法,也可以不提供set方法

  • 注解可以声明在属性级别 或 set方法级别

  • 可以添加@Qualifier结合使用,通过value属性值查找bean对象(value属性值必须要设置,且值要与bean标签的id属性值对应)

默认通过类型(Class类型)查找bean对象 与属性字段的名称无关

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Autowired注解实现自动化注入
* 默认通过类型(Class类型)查找bean对象 与属性字段的名称无关
*/
public class UserService {

@Autowired
private UserDao userDao; // 默认通过类型(Class类型)查找bean对象 与属性字段的名称无关

public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void test() {
// 调用UserDao的方法
userDao.test();
}
}

属性可以提供set方法,也可以不提供set方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* @Autowired注解实现自动化注入
* 属性可以提供set方法,也可以不提供set方法
*/
public class UserService {

@Autowired
private UserDao userDao; // 不提供set方法

public void test() {
// 调用UserDao的方法
userDao.test();
}
}

注解可以声明在属性级别 或 set方法级别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @Autowired注解实现自动化注入
* 注解可以声明在属性级别 或 set方法级别
*/
public class UserService {

private UserDao userDao;

@Autowired// 注解可以声明在set方法级别
public void setUserDao(UserDao userDao) {
this.userDao = userDao;
}

public void test() {
// 调用UserDao的方法
userDao.test();
}
}

可以添加@Qualifier结合使用,通过value属性值查找bean对象(value属性值必须要设置,且值要与bean标签的id属性值对应)

/**
 * @Autowired注解实现自动化注入
 *  可以添加@Qualifier结合使用,通过value属性值查找bean对象
        value属性值必须要设置,且值要与bean标签的id属性值对应
 */
public class UserService {

    @Autowired
    @Qualifier(value="userDao") // value属性值必须要设置,且值要与bean标签的id属性值对应
    private UserDao userDao;

    public void test() {
        userDao.test();
    }
}

推荐使用@Resource 注解是属于J2EE的,减少了与Spring的耦合

  • 初始化基本流程

  • setInitializers设置初始化器

  • getSpringFactoriesInstances获取Spring工厂实例

  • SpringFactoriesLoader的loadFactoryNames

  • loadSpringFactories加载META-INF/spring.factories

    • PropertiesLoaderUtils的loadProperties加载资源文件

    • SpringApplication的createSpringFactoriesInstances创建相关类型的实例

    • AnnotationAwareOrderComparator的sort根据优先级排序

  • SpringApplication的setInitializers

初始化基本流程

*

getSpringFactoriesInstances(BootstrapRegistryInitializer.class))(初始化引导注册表初始化器

我们来看看这一句话做了什么:

1
this.bootstrapRegistryInitializers = new ArrayList<>(               getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
package org.springframework.boot;

回调接口,可用于在{@link BootstrapRegistry}使用前对其进行初始化。


/**
 * Callback interface that can be used to initialize a {@link BootstrapRegistry} before it
 * is used.
 *
 * @author Phillip Webb
 * @since 2.4.5
 * @see SpringApplication#addBootstrapRegistryInitializer(BootstrapRegistryInitializer)
 * @see BootstrapRegistry
 */
@FunctionalInterface
public interface BootstrapRegistryInitializer {

    /**
     * Initialize the given {@link BootstrapRegistry} with any required registrations.
     * @param registry the registry to initialize
     */
    void initialize(BootstrapRegistry registry);

}

给SpringApplication这个类的private final List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;这个属性赋值,看看这个getSpringFactoriesInstances方法

实际是调用下面这个,SpringFactoriesLoader.forDefaultResourceLocation(getClassLoader()).load(type, argumentResolver);

  • forDefaultResourceLocation()方法注释是创建一个 {@link SpringFactoriesLoader} 实例,该实例将加载并实例化来自{@value #FACTORIES_RESOURCE_LOCATION}的工厂实现,使用给定的类加载器

返回一个SpringFactoriesLoader对象,调用的是forResourceLocation,第一个参数是资源目录,资源目录是

  • getClassLoader()查看当前类加载器

  • Map<String, SpringFactoriesLoader> loaders = cache.computeIfAbsent( resourceClassLoader, key -> new ConcurrentReferenceHashMap<>());

这段代码的大概意思是从map缓存中获取SpringFactoriesLoader工厂实例,如果指定的键(key)在映射中尚未关联值,则计算该键的值并将其存储在映射中。如果键已经存在,则直接返回现有的值,cache类型为static final Map<ClassLoader, Map<String, SpringFactoriesLoader>> cache = new ConcurrentReferenceHashMap<>();先利用resourceClassLoader获取到Map<String, SpringFactoriesLoader>类型的value,如果value有值的话就返回现在的值,没有的话new ConcurrentReferenceHashMap<>()

1
2
return loaders.computeIfAbsent(resourceLocation, key ->
new SpringFactoriesLoader(classLoader, loadFactoriesResource(resourceClassLoader, resourceLocation)));

这一步与上面类似,如果对应的key值有value的话就直接返回,没有的话new一个SpringFactoriesLoader,可以看到接收两个参数,一个classloader和一个loadFactoriesResource(resourceClassLoader, resourceLocation)),这个方法的具体方法内容为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
protected static Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
Map<String, List<String>> result = new LinkedHashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(resourceLocation);
while (urls.hasMoreElements()) {
UrlResource resource = new UrlResource(urls.nextElement());
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
properties.forEach((name, value) -> {
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String) value);
List<String> implementations = result.computeIfAbsent(((String) name).trim(),
key -> new ArrayList<>(factoryImplementationNames.length));
Arrays.stream(factoryImplementationNames).map(String::trim).forEach(implementations::add);
});
}
result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
}
return Collections.unmodifiableMap(result);
}

可以轻松看到这是加载springboot jar包下的spring.factories文件

最终结果为19条,但是springboot下面的spring.factories的文件只有15条,这是什么原因呢?

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
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver

# ConfigData Loaders
org.springframework.boot.context.config.ConfigDataLoader=\
org.springframework.boot.context.config.ConfigTreeConfigDataLoader,\
org.springframework.boot.context.config.StandardConfigDataLoader

# Application Context Factories
org.springframework.boot.ApplicationContextFactory=\
org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContextFactory,\
org.springframework.boot.web.servlet.context.ServletWebServerApplicationContextFactory

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.env.EnvironmentPostProcessorApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor,\
org.springframework.boot.env.RandomValuePropertySourceEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.ReactorEnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.context.config.ConfigDataNotFoundFailureAnalyzer,\
org.springframework.boot.context.properties.IncompatibleConfigurationFailureAnalyzer,\
org.springframework.boot.context.properties.NotConstructorBoundInjectionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.AotInitializerNotFoundFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.MissingParameterNamesFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.MutuallyExclusiveConfigurationPropertiesFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PatternParseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\
org.springframework.boot.liquibase.LiquibaseChangelogMissingFailureAnalyzer,\
org.springframework.boot.web.context.MissingWebServerFactoryBeanFailureAnalyzer,\
org.springframework.boot.web.embedded.tomcat.ConnectorStartFailureAnalyzer

# Failure Analysis Reporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter

# Database Initializer Detectors
org.springframework.boot.sql.init.dependency.DatabaseInitializerDetector=\
org.springframework.boot.flyway.FlywayDatabaseInitializerDetector,\
org.springframework.boot.jdbc.init.DataSourceScriptDatabaseInitializerDetector,\
org.springframework.boot.liquibase.LiquibaseDatabaseInitializerDetector,\
org.springframework.boot.orm.jpa.JpaDatabaseInitializerDetector,\
org.springframework.boot.r2dbc.init.R2dbcScriptDatabaseInitializerDetector

# Depends On Database Initialization Detectors
org.springframework.boot.sql.init.dependency.DependsOnDatabaseInitializationDetector=\
org.springframework.boot.sql.init.dependency.AnnotationDependsOnDatabaseInitializationDetector,\
org.springframework.boot.jdbc.SpringJdbcDependsOnDatabaseInitializationDetector,\
org.springframework.boot.jooq.JooqDependsOnDatabaseInitializationDetector,\
org.springframework.boot.orm.jpa.JpaDependsOnDatabaseInitializationDetector

# Resource Locator Protocol Resolvers
org.springframework.core.io.ProtocolResolver=\
org.springframework.boot.io.Base64ProtocolResolver

回到源码debug一下,发现还加载了autoconfig(9条),aop(1条)

最终结果为19条,是因为去掉了重复项

获取到之后,调用load(type, argumentResolver);这个方法进去

  • 调用load方法,大致说明是:/**

    * 从{@value #FACTORIES_RESOURCE_LOCATION}加载并实例化给定类型的工厂实现,使用配置的类加载器和给定的参数解析器。

    * <p>返回的工厂通过{@link AnnotationAwareOrderComparator}进行排序。

    * <p>从Spring Framework 5.3开始,如果在给定的工厂类型中发现重复的实现类名,只会实例化一个重复实现类型的实例。

    * @param factoryType 表示工厂的接口或抽象类

    * @param argumentResolver 用于通过类型解析构造函数参数的策略

    * @throws IllegalArgumentException 如果任何工厂实现类无法加载,或者在实例化任何工厂时发生错误

    * @since 6.0

    */

依次调用上面几个方法,debug一下,这里是根据factoryType类型再次筛选,类型为getSpringFactoriesInstances(BootstrapRegistryInitializer.class))这里的BootstrapRegistryInitializer.class,具体筛选方法为loadFactory(),里面调用getOrDefault(),自己查看源码,不做赘述

我们通过main方法调用一下由于

由于我们没有实现给定的类型的类加载器,所以size为0;

setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));(设置应用上下文初始化器

这个与上面类似,给private List<ApplicationContextInitializer<?>> initializers属性赋值,也是加载工厂,只不过类型为ApplicationContextInitializer<?>这个接口进去看一下

回调接口,用于在Spring的{@link ConfigurableApplicationContext}被{@linkplain ConfigurableApplicationContext#refresh()刷新}之前进行初始化。

* <p>通常用于需要对应用程序上下文进行一些程序化初始化的Web应用程序。例如,对{@linkplain ConfigurableApplicationContext#getEnvironment()上下文的环境}注册属性源或激活配置文件。参见{@code ContextLoader}和{@code FrameworkServlet}支持声明"contextInitializerClasses"上下文参数和init-param。
* <p>鼓励{@code ApplicationContextInitializer}处理器检测是否实现了Spring的{@link org.springframework.core.Ordered Ordered}接口,或者是否存在{@link org.springframework.core.annotation.Order @Order}注解,并在调用前相应地对实例进行排序。
* @author Chris Beams
* @since 3.1
* @param <C> 应用程序上下文类型
* @see org.springframework.web.context.ContextLoader#customizeContext
* @see org.springframework.web.context.ContextLoader#CONTEXT_INITIALIZER_CLASSES_PARAM
* @see org.springframework.web.servlet.FrameworkServlet#setContextInitializerClasses
* @see org.springframework.web.servlet.FrameworkServlet#applyInitializers

执行过程与上面一样,只不是工厂接口不一样,不做赘述,我们也用main方法测试一下

这次运行有七个spring工厂实例

这里是对每个实例的简要说明:

1. `org.springframework.boot.context.config.DelegatingApplicationContextInitializer@5622fdf`:
   - 这个初始化器负责将Spring Boot的`ApplicationContext`代理给Spring的原生`ConfigurableApplicationContext`。这允许在Spring Boot应用中使用Spring框架的原生功能。

2. `org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer@4883b407`:
   - 这个初始化器确保所有上下文共享同一个`MetadataReaderFactory`实例,这是Spring用来读取类元数据的工厂。

3. `org.springframework.boot.context.ContextIdApplicationContextInitializer@7d9d1a19`:
   - 这个初始化器设置应用上下文的ID,通常是从`application.properties`或`application.yml`配置文件中读取的。

4. `org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer@39c0f4a`:
   - 这个初始化器负责在启动时打印出关于配置问题的警告,例如不推荐的配置属性。

5. `org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer@1794d431`:
   - 如果你的应用使用了Spring Boot的RSocket支持,这个初始化器会在启动时注册RSocket服务器的端口信息。

6. `org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer@42e26948`:
   - 这个初始化器会在启动时注册Web服务器的端口信息,例如Tomcat的端口。

7. `org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener@57baeedf`:
   - 这个监听器用于在启动时记录条件评估报告,这有助于调试自动配置的条件。

这些实例不是“奇怪”的,而是Spring Boot框架的一部分,它们在应用启动时提供了额外的初始化逻辑。这些初始化器和监听器通过`META-INF/spring.factories`文件注册,Spring Boot在启动时会自动加载它们。每个实例后面的`@xxxx`是对象的哈希码,用于唯一标识实例。

setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));(设置事件监听器

事件监听器

1
2
3
4
5
6
7
8
9
10
11
12
应用事件监听器需要实现的接口。

* <p>基于标准的{@link java.util.EventListener}接口,用于观察者设计模式。
* <p>{@code ApplicationListener}可以泛型声明它感兴趣的事件类型。当注册到Spring的{@code ApplicationContext}时,事件将相应地进行过滤,只有匹配的事件对象才会触发监听器的调用。
* @author Rod Johnson
* @author Juergen Hoeller
* @param <E> 要监听的特定{@code ApplicationEvent}子类的类型
* @see org.springframework.context.ApplicationEvent
* @see org.springframework.context.event.ApplicationEventMulticaster
* @see org.springframework.context.event.SmartApplicationListener
* @see org.springframework.context.event.GenericApplicationListener
* @see org.springframework.context.event.EventListener

运行main方法看一下,有8个

1
2
3
4
5
List<ApplicationListener> load2 = SpringFactoriesLoader.forDefaultResourceLocation(loader).load(ApplicationListener.class, (SpringFactoriesLoader.ArgumentResolver) null);
for (ApplicationListener l:load2) {
System.out.println(l.toString());
}
System.out.println(load2.size());

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
这些是Spring Boot中实现`ApplicationListener`接口的事件监听器的实例,每个实例都用于处理Spring应用上下文中的不同事件。以下是每个监听器的简要说明:

1. `org.springframework.boot.env.EnvironmentPostProcessorApplicationListener@6325a3ee`:
- 这个监听器用于在Spring环境准备完成后,但在应用上下文刷新之前,对环境进行额外的处理。

2. `org.springframework.boot.context.config.AnsiOutputApplicationListener@1d16f93d`:
- 这个监听器处理ANSI输出设置,确保控制台输出的颜色和格式化在不同的操作系统上都能正确显示。

3. `org.springframework.boot.context.logging.LoggingApplicationListener@67b92f0a`:
- 这个监听器用于在应用启动时初始化日志系统,并在应用关闭时清理日志系统。

4. `org.springframework.boot.autoconfigure.BackgroundPreinitializer@2b9627bc`:
- 这个监听器用于在Spring Boot应用启动前预初始化一些背景任务,以加快启动速度。

5. `org.springframework.boot.context.config.DelegatingApplicationListener@65e2dbf3`:
- 这个监听器可能是用于代理其他监听器的事件处理,以便在不同的上下文中应用相同的事件处理逻辑。

6. `org.springframework.boot.builder.ParentContextCloserApplicationListener@4f970963`:
- 这个监听器确保在关闭子应用上下文时,其父上下文也会被正确关闭。

7. `org.springframework.boot.ClearCachesApplicationListener@61f8bee4`:
- 这个监听器用于在应用上下文刷新后清除Spring框架内部的一些缓存,以减少内存占用。

8. `org.springframework.boot.context.FileEncodingApplicationListener@7b49cea0`:
- 这个监听器用于设置文件编码,确保文件读写操作使用正确的字符编码。

this.mainApplicationClass = deduceMainApplicationClass();

这里就是推断启动类的,直接抛出异常,然后找到main方法所在的类,用于推断Spring Boot应用的主程序类。这个方法会检查各种线索,如传递给SpringApplication.run()方法的参数、应用的类路径等,以确定哪个类包含main方法

1
2
3
4
5
private Class<?> deduceMainApplicationClass() {
return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE)
.walk(this::findMainClass)
.orElse(null);
}
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
返回一个具有指定选项的`StackWalker`实例,该选项确定它可以访问的栈帧信息。

* <p>
* 如果存在安全管理器,并且给定的`option`是{@link Option#RETAIN_CLASS_REFERENCE Option.RETAIN_CLASS_REFERENCE},它会调用其{@link SecurityManager#checkPermission checkPermission}方法,传入{@code RuntimePermission("getStackWalkerWithClassReference")}。

* @param option 栈遍历选项{@link Option}
*
* @return 配置了给定选项的`StackWalker`
*
* @throws SecurityException 如果存在安全管理器,并且其`checkPermission`方法拒绝访问权限。


public static StackWalker getInstance(Option option) {
return getInstance(EnumSet.of(Objects.requireNonNull(option)));
}

public static StackWalker getInstance(Set<Option> options) {
if (options.isEmpty()) {
return DEFAULT_WALKER;
}

EnumSet<Option> optionSet = toEnumSet(options);
checkPermission(optionSet);
return new StackWalker(optionSet);
}

Wireshark提供了两种过滤器:

1、捕获过滤器

捕获过滤器:在抓包之前就设定好过滤条件,然后只抓取符合条件的数据包。

2、显示过滤器

显示过滤器:在已捕获的数据包集合中设置过滤条件,隐藏不想显示的数据包,只显示符合条件的数据包。

注意:这两种过滤器所使用的语法是完全不同的,想想也知道,捕捉网卡数据的其实并不是Wireshark,而是WinPcap,当然要按WinPcap的规则来,显示过滤器就是Wireshark对已捕捉的数据进行筛选。

使用捕获过滤器的主要原因就是性能。如果你知道并不需要分析某个类型的流量,那么可以简单地使用捕获过滤器过滤掉它,从而节省那些会被用来捕获这些数据包的处理器资源。当处理大量数据的时候,使用捕获过滤器是相当好用的。

Wireshark拦截通过网卡访问的所有数据,前提是没有设置任何代理。
Wireshark不能拦截本地回环访问的请求,即127.0.0.1或者localhost。

过滤器具体写法

显示过滤器写法

1、过滤值比较符号及表达式之间的组合

2、针对ip的过滤

  • 对源地址进行过滤
1
ip.src == 192.168.0.1
  • 对目的地址进行过滤
1
ip.dst == 192.168.0.1
  • 对源地址或者目的地址进行过滤
1
ip.addr == 192.168.0.1
  • 如果想排除以上的数据包,只需要将其用括号囊括,然后使用 “!” 即可
1
!(ip.addr == 192.168.0.1)

3、针对协议的过滤

  • 获某种协议的数据包,表达式很简单仅仅需要把协议的名字输入即可
1
http

注意:是否区分大小写?答:区分,只能为小写

  • 捕获多种协议的数据包
1
http or telnet
  • 排除某种协议的数据包
1
not arp   或者   !tcp

4、针对端口的过滤(视传输协议而定)

  • 捕获某一端口的数据包(以tcp协议为例)
1
tcp.port == 80
  • 捕获多端口的数据包,可以使用and来连接,下面是捕获高于某端口的表达式(以udp协议为例)
1
udp.port >= 2048

5、针对长度和内容的过滤

  • 针对长度的过虑(这里的长度指定的是数据段的长度)
1
2
udp.length < 20   
http.content_length <=30
  • 针对uri 内容的过滤
1
http.request.uri matches "user" (请求的uri中包含“user”关键字的)

注意:matches 后的关键字是不区分大小写的!

1
http.request.uri contains "User" (请求的uri中包含“user”关键字的)

注意:contains 后的关键字是区分大小写的!

5、针对http请求的一些过滤实例。

  • 过滤出请求地址中包含“user”的请求,不包括域名;
1
http.request.uri contains "User"
  • 精确过滤域名
1
http.host==baidu.com
  • 模糊过滤域名
1
http.host contains "baidu"
  • 过滤请求的content_type类型
1
http.content_type =="text/html"
  • 过滤http请求方法
1
http.request.method=="POST"
  • 过滤tcp端口
1
tcp.port==80
1
http && tcp.port==80 or tcp.port==5566
  • 过滤http响应状态码
1
http.response.code==302
  • 过滤含有指定cookie的http数据包
1
http.cookie contains "userid"

捕捉过滤器写法

在wireshark的工具栏中点击捕获捕获过滤器,可以看到一些过滤器的写法,如下图:

1、比较符号

1
2
3
与:&&或者and
或:||或者or
非:!或者not

实例:

1
src or dst portrange 6000-8000 && tcp or ip6

2、常用表达式实例

  • 源地址过滤
1
src www.baidu.com
  • 目的地址过滤
1
dst www.baidu.com
  • 目的地址端口过滤
1
dst post 80
  • 协议过滤
1
udp
0%