适配器模式是一种结构型设计模式, 它能使接口不兼容的对象能够相互合作。

适配器设计模式

问题

假如你正在开发一款股票市场监测程序, 它会从不同来源下载 XML 格式的股票数据, 然后向用户呈现出美观的图表。

在开发过程中, 你决定在程序中整合一个第三方智能分析函数库。 但是遇到了一个问题, 那就是分析函数库只兼容 JSON 格式的数据。

整合分析函数库之前的程序结构

你无法 “直接” 使用分析函数库, 因为它所需的输入数据格式与你的程序不兼容。

你可以修改程序库来支持 XML。 但是, 这可能需要修改部分依赖该程序库的现有代码。 甚至还有更糟糕的情况, 你可能根本没有程序库的源代码, 从而无法对其进行修改。

解决方案

你可以创建一个适配器。 这是一个特殊的对象, 能够转换对象接口, 使其能与其他对象进行交互。

适配器模式通过封装对象将复杂的转换过程隐藏于幕后。 被封装的对象甚至察觉不到适配器的存在。 例如, 你可以使用一个将所有数据转换为英制单位 (如英尺和英里) 的适配器封装运行于米和千米单位制中的对象。

适配器不仅可以转换不同格式的数据, 其还有助于采用不同接口的对象之间的合作。 它的运作方式如下:

  1. 适配器实现与其中一个现有对象兼容的接口。

  2. 现有对象可以使用该接口安全地调用适配器方法。

  3. 适配器方法被调用后将以另一个对象兼容的格式和顺序将请求传递给该对象。

有时你甚至可以创建一个双向适配器来实现双向转换调用。

适配器解决方案

让我们回到股票市场程序。 为了解决数据格式不兼容的问题, 你可以为分析函数库中的每个类创建将 XML 转换为 JSON 格式的适配器, 然后让客户端仅通过这些适配器来与函数库进行交流。 当某个适配器被调用时, 它会将传入的 XML 数据转换为 JSON 结构, 并将其传递给被封装分析对象的相应方法。

真实世界类比

适配器模式的示例

出国旅行前后的旅行箱。

如果你是第一次从美国到欧洲旅行, 那么在给笔记本充电时可能会大吃一惊。 不同国家的电源插头和插座标准不同。 美国插头和德国插座不匹配。 同时提供美国标准插座和欧洲标准插头的电源适配器可以解决你的难题。

适配器模式结构

对象适配器

实现时使用了构成原则: 适配器实现了其中一个对象的接口, 并对另一个对象进行封装。 所有流行的编程语言都可以实现适配器。

适配器设计模式的结构(对象适配器)
  1. 客户端 (Client) 是包含当前程序业务逻辑的类。

  2. 客户端接口 (Client Interface) 描述了其他类与客户端代码合作时必须遵循的协议。

  3. 服务 (Service) 中有一些功能类 (通常来自第三方或遗留系统)。 客户端与其接口不兼容, 因此无法直接调用其功能。

  4. 适配器 (Adapter) 是一个可以同时与客户端和服务交互的类: 它在实现客户端接口的同时封装了服务对象。 适配器接受客户端通过适配器接口发起的调用, 并将其转换为适用于被封装服务对象的调用。

  5. 客户端代码只需通过接口与适配器交互即可, 无需与具体的适配器类耦合。 因此, 你可以向程序中添加新类型的适配器而无需修改已有代码。 这在服务类的接口被更改或替换时很有用: 你无需修改客户端代码就可以创建新的适配器类。

类适配器

这一实现使用了继承机制: 适配器同时继承两个对象的接口。 请注意, 这种方式仅能在支持多重继承的编程语言中实现, 例如 C++。

适配器设计模式(类适配器)
  1. 类适配器不需要封装任何对象, 因为它同时继承了客户端和服务的行为。 适配功能在重写的方法中完成。 最后生成的适配器可替代已有的客户端类进行使用。

伪代码

下列适配器模式演示基于经典的 “方钉和圆孔” 问题。

适配器模式结构的示例

让方钉适配圆孔。

适配器假扮成一个圆钉 (Round­Peg), 其半径等于方钉 (Square­Peg) 横截面对角线的一半 (即能够容纳方钉的最小外接圆的半径)。

适配器模式适合应用场景

当你希望使用某个类, 但是其接口与其他代码不兼容时, 可以使用适配器类。

适配器模式允许你创建一个中间层类, 其可作为代码与遗留类、 第三方类或提供怪异接口的类之间的转换器。

如果您需要复用这样一些类, 他们处于同一个继承体系, 并且他们又有了额外的一些共同的方法, 但是这些共同的方法不是所有在这一继承体系中的子类所具有的共性。

你可以扩展每个子类, 将缺少的功能添加到新的子类中。 但是, 你必须在所有新子类中重复添加这些代码, 这样会使得代码有坏味道

将缺失功能添加到一个适配器类中是一种优雅得多的解决方案。 然后你可以将缺少功能的对象封装在适配器中, 从而动态地获取所需功能。 如要这一点正常运作, 目标类必须要有通用接口, 适配器的成员变量应当遵循该通用接口。 这种方式同装饰模式非常相似。

实现方式

  1. 确保至少有两个类的接口不兼容:

    • 一个无法修改 (通常是第三方、 遗留系统或者存在众多已有依赖的类) 的功能性服务类。

    • 一个或多个将受益于使用服务类的客户端类。

  2. 声明客户端接口, 描述客户端如何与服务交互。

  3. 创建遵循客户端接口的适配器类。 所有方法暂时都为空。

  4. 在适配器类中添加一个成员变量用于保存对于服务对象的引用。 通常情况下会通过构造函数对该成员变量进行初始化, 但有时在调用其方法时将该变量传递给适配器会更方便。

  5. 依次实现适配器类客户端接口的所有方法。 适配器会将实际工作委派给服务对象, 自身只负责接口或数据格式的转换。

  6. 客户端必须通过客户端接口使用适配器。 这样一来, 你就可以在不影响客户端代码的情况下修改或扩展适配器。

适配器模式优缺点

  • 单一职责原则你可以将接口或数据转换代码从程序主要业务逻辑中分离。

  • 开闭原则。 只要客户端代码通过客户端接口与适配器进行交互, 你就能在不修改现有客户端代码的情况下在程序中添加新类型的适配器。

  • 代码整体复杂度增加, 因为你需要新增一系列接口和类。 有时直接更改服务类使其与其他代码兼容会更简单。

与其他模式的关系

  • 桥接模式通常会于开发前期进行设计, 使你能够将程序的各个部分独立开来以便开发。 另一方面, 适配器模式通常在已有程序中使用, 让相互不兼容的类能很好地合作。

  • 适配器可以对已有对象的接口进行修改, 装饰模式则能在不改变对象接口的前提下强化对象功能。 此外, 装饰还支持递归组合, 适配器则无法实现。

  • 适配器能为被封装对象提供不同的接口, 代理模式能为对象提供相同的接口, 装饰则能为对象提供加强的接口。

  • 外观模式为现有对象定义了一个新接口, 适配器则会试图运用已有的接口。 适配器通常只封装一个对象, 外观通常会作用于整个对象子系统上。

  • 桥接状态模式策略模式 (在某种程度上包括适配器) 模式的接口非常相似。 实际上, 它们都基于组合模式——即将工作委派给其他对象, 不过也各自解决了不同的问题。 模式并不只是以特定方式组织代码的配方, 你还可以使用它们来和其他开发者讨论模式所解决的问题

1
2
3
4
5
6
7
8
package org.pt.design.adapterdesign;
/*
公共接口
*/
public interface AdvancedMediaPlayer{
public void playVlc(String fileName);
public void playMp4(String fileName);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package org.pt.design.adapterdesign;

/*
接口实现类1
*/
public class Mp4Player implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {

}

@Override
public void playMp4(String fileName) {
System.out.println("Playing mp4 file. Name: "+ fileName);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package org.pt.design.adapterdesign;
/*
接口实现类2
*/
public class VlcPlayer implements AdvancedMediaPlayer{
@Override
public void playVlc(String fileName) {
System.out.println("Playing vlc file. Name: "+ fileName);
}

@Override
public void playMp4(String fileName) {

}
}
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
package org.pt.design.adapterdesign;

/*
适配器模式就是适配器类里面有一个公共的接口,在构造方法里面,根据这个接口
的不同实现类将这个接口引用指向具体类
*/

public class MediaAdapter implements MediaPlayer{
AdvancedMediaPlayer advancedMusicPlayer;
public MediaAdapter(String audioType){
if(audioType.equalsIgnoreCase("vlc") ){
advancedMusicPlayer = new VlcPlayer();
} else if (audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer = new Mp4Player();
}
}
@Override
public void play(String audioType, String fileName) {
if(audioType.equalsIgnoreCase("vlc")){
advancedMusicPlayer.playVlc(fileName);
}else if(audioType.equalsIgnoreCase("mp4")){
advancedMusicPlayer.playMp4(fileName);
}
}
}

BeanDefinition

一.知识储备

1. MetadataReader

MetadataReader 是 Spring 提供的一个接口,用于读取类的元数据信息。它可以用于扫描类文件,获取类的基本信息,如类名、类的注解等。在注解驱动的开发中,`MetadataReader` 通常用于扫描包中的类,并从这些类中提取注解信息,以便配置 Spring Bean。[点击查看](https://github.com/xuchengsheng/spring-reading/tree/master/spring-metadata/spring-metadata-metadataReader)

2. AnnotationMetadata.introspect

AnnotationMetadata 接口中的 introspect 方法用于深入分析类的注解信息。它可以帮助我们获取类上的注解、类的方法上的注解、类的字段上的注解等。在 Spring 中,`introspect` 方法通常用于解析被 @Component, @Configuration 和其他注解标记的类,以确定它们如何被实例化并配置为 Spring Bean

二、基本描述

BeanDefinition 是 Spring 框架中的关键构建块,它是一种配置元数据,用于详细描述和定义应用程序中的 Bean 对象,包括 Bean 的类名、作用域、依赖关系、构造函数参数、属性值、初始化方法、销毁方法等信息,从而允许 Spring 容器准确地实例化、配置和管理这些 Bean。通过`BeanDefinition`,我们可以灵活地配置应用程序中的组件,使其能够实现依赖注入、AOP 切面、作用域控制等核心功能,促进松耦合、可维护和可扩展的应用程序开发。

三、主要功能

1. 定义 Bean 的类

用于指定要实例化的 Bean 的类名。它告诉 Spring 容器要创建哪个 Java 类的对象。

2. 定义 Bean 的作用域

允许我们指定 Bean 的作用域,例如 singleton(单例)或 prototype(多例)。这影响了 Bean 在容器中的生命周期。

3. 构造函数参数和属性值

允许我们指定 Bean 的构造函数参数和属性值,以便在实例化 Bean 时传递参数或设置属性。

4. 定义初始化和销毁方法

定义 Bean 的初始化方法和销毁方法,以确保在 Bean 创建和销毁时执行特定的逻辑。

5. Bean 的延迟初始化

允许我们设置 Bean 是否延迟初始化,即在第一次请求时创建 Bean 实例。

6. 依赖关系

允许我们指定 Bean 之间的依赖关系,以确保在创建 Bean 时正确注入依赖的其他 Bean。

7. 描述 Bean 的角色

允许我们为 Bean 指定一个角色(role),通常包括应用程序 Bean、基础设施 Bean、测试 Bean 等。

8. Bean 的属性覆盖

允许我们使用属性覆盖机制,通过不同的配置源(如属性文件或环境变量)覆盖已定义的属性值。

9. Bean 的注解和元数据

可以包含关于 Bean 的注解信息和元数据,这对于处理注解驱动的开发非常有用。

10. 动态创建和注册 Bean

允许我们在运行时动态创建和注册 Bean,而不仅仅是静态配置。

四、接口源码

从`BeanDefinition` 接口源码来看,它描述和配置 Spring 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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
/**

* BeanDefinition 描述一个 Bean 实例,包括属性值、构造函数参数值以及具体实现提供的更多信息。

*

* 这只是一个最小的接口:主要意图是允许 BeanFactoryPostProcessor 检查和修改属性值以及其他 Bean 元数据。

*

* @author Juergen Hoeller

* @author Rob Harrop

* @since 2004-03-19

* @see ConfigurableListableBeanFactory#getBeanDefinition

* @see org.springframework.beans.factory.support.RootBeanDefinition

* @see org.springframework.beans.factory.support.ChildBeanDefinition

*/

public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement {

/**

* 标准单例范围的范围标识符:value。

* 请注意,扩展的 Bean 工厂可能支持更多范围。

* @see #setScope

* @see ConfigurableBeanFactory#SCOPE_SINGLETON

*/

String SCOPE_SINGLETON = ConfigurableBeanFactory.SCOPE_SINGLETON;

/**

* 标准原型范围的范围标识符:value。

* 请注意,扩展的 Bean 工厂可能支持更多范围。

* @see #setScope

* @see ConfigurableBeanFactory#SCOPE_PROTOTYPE

*/

String SCOPE_PROTOTYPE = ConfigurableBeanFactory.SCOPE_PROTOTYPE;

/**

* 表示 BeanDefinition 是应用程序的主要部分的角色提示。

* 通常对应于用户定义的 Bean。

*/

int ROLE_APPLICATION = 0;

/**

* 表示 BeanDefinition 是某个更大配置的支持部分的角色提示,通常是外部

* org.springframework.beans.factory.parsing.ComponentDefinition。

* 当在特定的 org.springframework.beans.factory.parsing.ComponentDefinition 上查看时,这些"SUPPORT" Bean 被认为足够重要,

* 但在查看应用程序的整体配置时,它们无关紧要。

*/

int ROLE_SUPPORT = 1;

/**

* 表示 BeanDefinition 提供了完全背景角色,与最终用户无关。

* 当注册完全属于 org.springframework.beans.factory.parsing.ComponentDefinition 内部工作的 Bean 时使用此提示。

*/

int ROLE_INFRASTRUCTURE = 2;

/**

* 设置此 Bean 定义的父定义的名称,如果有的话。

*/

void setParentName(@Nullable String parentName);

/**

* 返回此 Bean 定义的父定义的名称,如果有的话。

*/

@Nullable

String getParentName();

/**

* 指定此 Bean 定义的 Bean 类名称。

* 在 Bean 工厂后处理期间,可以修改类名,通常用解析后的类名替换原始类名。

*/

void setBeanClassName(@Nullable String beanClassName);

/**

* 返回此 Bean 定义的当前 Bean 类名。

* 请注意,这不一定是运行时实际使用的类名,在子定义从父定义继承类名的情况下。

* 此外,在工厂方法上调用的类可能为空。

* 因此,不要将其视为运行时的确定 Bean 类型,而只在个别 Bean 定义级别用于解析目的。

*/

@Nullable

String getBeanClassName();

/**

* 覆盖此 Bean 的目标范围,指定新的范围名称。

* @see #SCOPE_SINGLETON

* @see #SCOPE_PROTOTYPE

*/

void setScope(@Nullable String scope);

/**

* 返回此 Bean 的当前目标范围名称,如果尚未知,则返回 null。

*/

@Nullable

String getScope();

/**

* 设置此 Bean 是否应懒初始化。

* 如果为 false,则 Bean 将由执行单例的 Bean 工厂在启动时实例化。

*/

void setLazyInit(boolean lazyInit);

/**

* 返回此 Bean 是否应懒初始化,即不在启动时急切实例化。仅适用于单例 Bean。

*/

boolean isLazyInit();

/**

* 设置此 Bean 依赖于初始化的 Bean 的名称。

* Bean 工厂将保证这些 Bean 首先得到初始化。

*/

void setDependsOn(@Nullable String... dependsOn);

/**

* 返回此 Bean 依赖的 Bean 名称。

*/

@Nullable

String[] getDependsOn();

/**

* 设置此 Bean 是否是自动装配候选 Bean。

* 请注意,此标志仅用于影响基于类型的自动装配。

* 它不影响名称上的显式引用,如果指定的 Bean 未标记为自动装配候选 Bean,则名称匹配仍然会注入 Bean。

*/

void setAutowireCandidate(boolean autowireCandidate);

/**

* 返回此 Bean 是否是自动装配候选 Bean。

*/

boolean isAutowireCandidate();

/**

* 设置此 Bean 是否是主要自动装配候选 Bean。

* 如果多个匹配的候选 Bean 中有一个 Bean 的此值为 true,则它将作为补充选项。

*/

void setPrimary(boolean primary);

/**

* 返回此 Bean 是否是主要自动装配候选 Bean。

*/

boolean isPrimary();

/**

* 指定要使用的工厂 Bean,如果有的话。

* 这是要调用指定工厂方法的 Bean 的名称。

* @see #setFactoryMethodName

*/

void setFactoryBeanName(@Nullable String factoryBeanName);

/**

* 返回工厂 Bean 名称,如果有的话。

*/

@Nullable

String getFactoryBeanName();

/**

* 指定工厂方法,如果有的话。此方法将使用构造函数参数调用,

* 或者如果未指定参数,则使用没有参数调用。

* 该方法将在指定工厂 Bean 上调用,如果没有指定工厂 Bean,则在本地 Bean 类上调用。

* @see #setFactoryBeanName

* @see #setBeanClassName

*/

void setFactoryMethodName(@Nullable String factoryMethodName);

/**

* 返回工厂方法,如果有的话。

*/

@Nullable

String getFactoryMethodName();

/**

* 返回此 Bean 的构造函数参数值。

* 返回的实例可以在 Bean 工厂后处理期间修改。

* @return 构造函数参数值对象(永不为 null)

*/

ConstructorArgumentValues getConstructorArgumentValues();

/**

* 返回是否为此 Bean 定义定义了构造函数参数值。

* @since 5.0.2

*/

default boolean hasConstructorArgumentValues() {

return !getConstructorArgumentValues().isEmpty();

}

/**

* 返回要应用于 Bean 新实例的属性值。

* 返回的实例可以在 Bean 工厂后处理期间修改。

* @return 可变属性值对象(永不为 null)

*/

MutablePropertyValues getPropertyValues();

/**

* 返回是否为此 Bean 定义定义了属性值。

* @since 5.0.2

*/

default boolean hasPropertyValues() {

return !getPropertyValues().isEmpty();

}

/**

* 设置初始化方法的名称。

* @since 5.1

*/

void setInitMethodName(@Nullable String initMethodName);

/**

* 返回初始化方法的名称。

* @since 5.1

*/

@Nullable

String getInitMethodName();

/**

* 设置销毁方法的名称。

* @since 5.1

*/

void setDestroyMethodName(@Nullable String destroyMethodName);

/**

* 返回销毁方法的名称。

* @since 5.1

*/

@Nullable

String getDestroyMethodName();

/**

* 设置 BeanDefinition 的角色提示。角色提示提供框架和工具一个有关特定 BeanDefinition 的角色和重要性的指示。

* @since 5.1

* @see #ROLE_APPLICATION

* @see #ROLE_SUPPORT

* @see #ROLE_INFRASTRUCTURE

*/

void setRole(int role);

/**

* 获取 BeanDefinition 的角色提示。角色提示提供框架和工具一个有关特定 BeanDefinition 的角色和重要性的指示。

* @see #ROLE_APPLICATION

* @see #ROLE_SUPPORT

* @see #ROLE_INFRASTRUCTURE

*/

int getRole();

/**

* 设置 BeanDefinition 的人类可读描述。

* @since 5.1

*/

void setDescription(@Nullable String description);

/**

* 返回 BeanDefinition 的人类可读描述。

*/

@Nullable

String getDescription();

/**

* 基于 Bean 类或其他特定元数据返回可解析的类型的类型。

* 这通常在运行时合并的 Bean 定义上完全解析,但不一定在配置时定义实例上解析。

* @return 可解析类型(可能为 ResolvableType#NONE)

* @since 5.2

* @see ConfigurableBeanFactory#getMergedBeanDefinition

*/

ResolvableType getResolvableType();

/**

* 返回是否为Singleton,在所有调用上返回单个共享实例。

* @see #SCOPE_SINGLETON

*/

boolean isSingleton();

/**

* 返回是否为Prototype,每次调用都返回独立的实例。

* @since 3.0

* @see #SCOPE_PROTOTYPE

*/

boolean isPrototype();

/**

* 返回此 Bean 是否是"抽象"的,即不应该被实例化。

*/

boolean isAbstract();

/**

* 返回此 Bean 定义所来自的资源的描述(以便在出现错误时显示上下文)。

*/

@Nullable

String getResourceDescription();

/**

* 返回原始的 Bean 定义,如果没有则返回 {@code null}。

* 允许检索已装饰的 Bean 定义(如果有)。

* 请注意,此方法返回最直接的起源。遍历起源链以找到用户定义的原始 Bean 定义。

*/

@Nullable

BeanDefinition getOriginatingBeanDefinition();



}

五、主要实现

1. GenericBeanDefinition

  • 描述通用的 Bean 定义,可以用于大多数类型的 Bean。

  • 具有灵活的属性配置,可以设置类名、作用域、初始化和销毁方法、构造函数参数、属性值等。

  • 通常用于手动配置 Bean 或需要自定义 Bean 定义的情况。

2. RootBeanDefinition

  • 用于表示独立的根级 Bean 定义,通常直接定义 Bean。

  • 可以继承 GenericBeanDefinition,并支持配置 Bean 类的其他信息。

  • 通常用于定义应用程序中的独立 Bean。

3. ChildBeanDefinition

  • 用于表示派生的子级 Bean 定义,继承父级 Bean 定义的配置。

  • 通常用于创建一个 Bean 的变体,继承父 Bean 的配置并进行部分覆盖或修改。

  • 具有指向父 Bean 的引用。

4. AnnotatedGenericBeanDefinition

  • 用于基于注解的 Bean 定义,通常用于扫描组件和配置类。

  • 可以表示使用注解定义的 Bean,支持类级别的注解配置。

  • 通常用于自动扫描和注册组件。

5. ConfigurationClassBeanDefinition

  • 用于表示配置类(`@Configuration` 注解)的 Bean 定义,通常用于 Spring 配置。

  • 表示配置类作为 Bean 定义,支持包含其他 Bean 定义的配置类。

  • 通常由 Spring 容器自动创建,以支持 @Configuration 注解的配置。

6. ScannedGenericBeanDefinition

  • 用于表示扫描到的 Bean 的 Bean 定义,通常用于自动扫描组件。

  • 通常在组件扫描过程中创建,表示被发现的组件类。

  • 通常用于自动注册组件,如 @Component 注解的类。

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
classDiagram

direction BT



class BeanDefinition {

<<interface>>

}



class AnnotatedBeanDefinition {

<<interface>>

}



class AbstractBeanDefinition {

<<Abstract>>

}

class GenericBeanDefinition {

}



class RootBeanDefinition {

}



class ChildBeanDefinition {

}



class AnnotatedGenericBeanDefinition {

}



class ScannedGenericBeanDefinition {

}



class ConfigurationClassBeanDefinition {

}







AnnotatedBeanDefinition ..|> BeanDefinition

AbstractBeanDefinition --|> BeanDefinition

GenericBeanDefinition ..|> AbstractBeanDefinition



RootBeanDefinition ..|> AbstractBeanDefinition

ChildBeanDefinition ..|> AbstractBeanDefinition



AnnotatedGenericBeanDefinition ..|> GenericBeanDefinition

ScannedGenericBeanDefinition ..|> GenericBeanDefinition

ConfigurationClassBeanDefinition ..|> RootBeanDefinition

六、最佳实践

创建了一个Spring容器(`DefaultListableBeanFactory`),然后通过`createBeanDefinition()`方法创建并配置了一个自定义的Bean定义(`ScannedGenericBeanDefinition`),包括作用域、初始化方法、销毁方法、属性值等。接着,它注册这个Bean定义到容器中,并使用容器获取Bean实例,打印出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
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
public class BeanDefinitionDemo {

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

DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();

beanFactory.registerBeanDefinition("myBean", createBeanDefinition());

// 获取MyBean

MyBean myChildBean = beanFactory.getBean("myBean", MyBean.class);

// 打印Bean对象

System.out.println("MyBean = " + myChildBean);

// 销毁myBean

beanFactory.destroySingleton("myBean");

}

private static BeanDefinition createBeanDefinition() throws IOException {

SimpleMetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();

MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(MyBean.class.getName());

ScannedGenericBeanDefinition beanDefinition = new ScannedGenericBeanDefinition(metadataReader);

beanDefinition.setScope("singleton");

beanDefinition.setLazyInit(true);

beanDefinition.setPrimary(true);

beanDefinition.setAbstract(false);

beanDefinition.setInitMethodName("init");

beanDefinition.setDestroyMethodName("destroy");

beanDefinition.setAutowireCandidate(true);

beanDefinition.setRole(BeanDefinition.ROLE_APPLICATION);

beanDefinition.setDescription("This is a custom bean definition");

beanDefinition.setResourceDescription("com.xcs.spring.BeanDefinitionDemo");

beanDefinition.getPropertyValues().add("name", "lex");

beanDefinition.getPropertyValues().add("age", "18");

return beanDefinition;

}

}
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
MyBean 的Java类,代表了一个简单的Java Bean。


public class MyBean {

private String name;

private String age;

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

public String getAge() {

return age;

}

public void setAge(String age) {

this.age = age;

}

public void init(){

System.out.println("execute com.xcs.spring.bean.MyBean.init");

}

public void destroy(){

System.out.println("execute com.xcs.spring.bean.MyBean.destroy");

}

@Override

public String toString() {

return "MyBean{" +

"name='" + name + '\'' +

", age='" + age + '\'' +

'}';

}

}

运行结果发现,与`BeanDefinition`配置相关,初始化方法和销毁方法的调用以及属性值的设置受`BeanDefinition`中相应的配置项影响,`BeanDefinition`用于定义和配置Bean的元信息,使Spring容器可以正确管理Bean的生命周期和属性。

1
2
3
4
5
execute com.xcs.spring.bean.MyBean.init

MyBean = MyBean{name='lex', age='18'}

execute com.xcs.spring.bean.MyBean.destroy

七、与其他组件的关系

1. DefaultListableBeanFactory

负责管理Bean的创建、初始化和销毁,而BeanDefinition提供了描述Bean的元信息的方式,DefaultListableBeanFactory使用BeanDefinition来创建和管理Bean实例

2. BeanPostProcessor

拦截Bean初始化过程的接口,它可以在Bean创建后、初始化前后对Bean进行处理。BeanDefinition的信息可以在BeanPostProcessor中使用,例如在初始化前修改Bean的属性值。

3. BeanDefinitionRegistry

注册和管理BeanDefinition的接口,定义了BeanDefinition的注册和访问方法。BeanFactory和ApplicationContext实现了BeanDefinitionRegistry接口,通过它们可以注册和获取BeanDefinition。

4. BeanDefinitionReader

从外部配置文件(如`XML、YAML、Properties`文件)中读取BeanDefinition的工具。它将外部配置信息解析成BeanDefinition并注册到`BeanFactory`中。

八、常见问题

1. BeanDefinition的作用是什么?

主要作用是定义和配置Bean的属性和行为,以便Spring容器可以根据这些信息动态地创建、初始化和管理Bean实例。

2. BeanDefinition的生命周期是怎样的?

生命周期与Spring容器相同,它在容器启动时进行注册和解析,然后被用于创建和管理Bean实例的生命周期。

3. BeanDefinition如何注册?

通过BeanDefinitionRegistry接口的实现类(如DefaultListableBeanFactory)进行注册,或者通过BeanDefinitionReader从外部配置文件中加载并注册。

4. BeanDefinition的属性有哪些?

属性包括Bean的类名、作用域、初始化方法、销毁方法、属性值等。具体属性取决于Bean的配置需求。

5. BeanDefinition如何修改?

可以在注册后通过编程方式进行修改,例如更改属性值、作用域、初始化方法、销毁方法等。

6. BeanDefinition的作用域有哪些?

包括Singleton(单例)、Prototype(原型)、Request(请求)、Session(会话)、等等,可以根据需求选择合适的作用域。

7. BeanDefinition的注册和加载有什么区别?

注册是将已创建的BeanDefinition添加到容器中,而加载是从外部配置文件中读取Bean的元信息并注册到容器中。

8. 如何使用BeanDefinition来实现依赖注入?

通过设置BeanDefinition中的属性,如构造函数参数、属性值、引用其他Bean的方式,可以实现依赖注入。

9. BeanDefinition是否可以动态生成?

可以在运行时动态生成并注册到容器中,这在某些复杂的情况下非常有用,例如基于条件的Bean注册。

一、基本介绍

缓冲区(Buffer):缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel 提供从文件、网络读取数据的渠道,但是读取或写入的数据必须经由 Buffer ,如下图所示:
*

二、Buffer类及其子类

1)、在 NIO 中,Buffer 是一个顶级父类,它是一个抽象类,类的层级关系图:
*

1、 ByteBuffer,存储字节数据到缓冲区;
2、 ShortBuffer,存储短整型数据到缓冲区;
3、 CharBuffer,存储字符数据到缓冲区;
4、 IntBuffer,存储整型数据到缓冲区;
5、 LongBuffer,存储长整型数据到缓冲区;
6、 DoubleBuffer,存储小数数据到缓冲区;
7、 FloatBuffer,存储小数数据到缓冲区;

2)、Buffer 类定义了所有的缓冲区都具有的四个属性来提供关于其所包含的数据元素的信息。

1
2
3
4
5
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
属性 描述
capacity 容量,既可以容纳的最大数据量,在缓冲区创建时被设定且不能改变
limit 表示缓冲区的当前终点,不能对缓冲区超过极限的位置进行读写操作,且极限是可以修改的
position 当前位置,下一个要被读或写的元素的索引,每次读写缓冲区数据时都会改变该值,为下次读写做准备
mark 标记

3)、常用方法

方法 描述
int capacity() 返回此缓冲区的容量
int position() 返回此缓冲区的位置
Buffer position(int newPosition) 设置此缓冲区的位置
int limit() 返回此缓冲区的限制
Buffer limit(int newLimit) 设置此缓冲区的限制
Buffer mark() 在此缓冲区的位置设置标记
Buffer reset() 将此缓冲区的位置重置为以前标记的位置
Buffer clear() 清除此缓冲区,即将各个标记恢复到初始状态,但是数据并没有真正擦除
Buffer flip() 反转此缓冲区
Buffer rewind() 重绕此缓冲区
int remaining() 返回当前位置与限制之间的元素数
boolean hasRemaining() 告知在当前位置和限制之间是否有元素
boolean isReadOnly() 告知此缓冲区是否为只读缓冲区
boolean hasArray() 告知此缓冲区是否具有可访问的底层实现数组
Object array() 返回此缓冲区的底层实现数组
int arrayOffset() 返回此缓冲区的底层实现数组中第一个缓冲区元素的偏移量
boolean isDirect() 告知此缓冲区是否为直接缓冲区
1
2
3
4
5
6
7
// 从 Buffer 读取数据
intBuffer.flip(); // 将 Buffer 转换,读写切换(!!!)
intBuffer.position(1);// 1、2、3、4、5、6、7
intBuffer.limit(8);
while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}

4)、ByteBuffer

从前面可以看出对于 Java 中的基本数据类型(boolean除外),都有一个 Buffer 类型与之相对应,最常用的自然是 ByteBuffer 类(二进制数据),该类的主要方法如下:

方法 描述
ByteBuffer allocateDirect(int capacity) 创建直接缓冲区
ByteBuffer allocate(int capacity) 设置缓冲区的初始容量
ByteBuffer wrap(byte[] array) 把一个数组放到缓冲区中使用
ByteBuffer wrap(byte[] array,int offset, int length) 构造初始化位置offset和上界length的缓冲区
byte get() 从当前位置position上get,get之后,position会自动+1
byte get(int index) 从绝对位置get
ByteBuffer put(byte b) 从当前位置上put,put之后,position会自动+1
ByteBuffer put(int index, byte b) 从绝对位置上put

三、Buffer执行原理*

1)、当我们用 XxxBuffer 申请 allocate(10) 了一个缓冲区后,我们得到了一个capacity为指定参数的大小的数组缓冲区。当前我们什么都没做,position当然保持在初始0的位置,因为初始状态没有切换到读模式,没有什么需要标记其后不能读的,写只要不超过capacity就可以,所以limit放在最后,与capacity值一样。

2)、当我们put()写入5个数值之后,position变为5(0到4已经写了数,若要继续写需要从5开始)。因为没有切换到读模式,所以没有什么需要声明其后不能读,写只要不超过capacity就可以,所以limit放在最后,与capacity值一样。

3)、当我们需要读取我们刚才写入缓冲区的数据时,首先需要做的是调用缓冲区的flip()方法,将缓冲区切换到读模式。切换到读模式之后,我们要做的是将刚才的写入的5个数据读取出来,很自然地,postion会变回0,因为0是写入的起始位置;因为刚才只写入了5个,因此数据只写到0-4,所以从第5个开始之后的不能被读取,所以limit变成5,限定5开始之后的缓冲区空间不能够被读取。

1
2
3
4
5
6
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}

Dockerfile

什么是 Dockerfile?

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明

以定制一个 nginx 镜像

构建好的镜像内会有一个 /usr/share/nginx/html/index.html 文件)

在一个空目录下,新建一个名为 Dockerfile 文件,并在文件内添加以下内容

1
2
FROM nginx
RUN echo '这是一个本地构建的nginx镜像' > /usr/share/nginx/html/index.html
FROM 和 RUN 指令的作用

FROM:定制的镜像都是基于 FROM 的镜像,这里的 nginx 就是定制需要的基础镜像。后续的操作都是基于 nginx。

RUN:用于执行后面跟着的命令行命令。有以下俩种格式

shell 格式:

RUN <命令行命令>

# <命令行命令> 等同于,在终端操作的 shell 命令。

exec 格式:

RUN ["可执行文件", "参数1", "参数2"]
# 例如:
# RUN ["./test.php", "dev", "offline"] 等价于 RUN ./test.php dev offline

注意:Dockerfile 的指令每执行一次都会在 docker 上新建一层。所以过多无意义的层,会造成镜像膨胀过大。例如:

FROM centos
RUN yum -y install wget
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
RUN tar -xvf redis.tar.gz

以上执行会创建 3 层镜像。可简化为以下格式:

FROM centos
RUN yum -y install wget \
    && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
    && tar -xvf redis.tar.gz

如上,以 && 符号连接命令,这样执行后,只会创建 1 层镜像

开始构建镜像

在 Dockerfile 文件的存放目录下,执行构建动作。

以下示例,通过目录下的 Dockerfile 构建一个 my-nginx:tag-1(镜像名称:镜像标签)。

:最后的 . 代表本次执行的上下文路径,下面会介绍

docker build -t my-nginx:tag-1 .
上下文路径

上面有提到指令最后一个 . 是上下文路径,那么什么是上下文路径呢?

上下文路径,是指 docker 在构建镜像,有时候想要使用到本机的文件(比如复制),docker build 命令得知这个路径后,会将路径下的所有内容打包

解析:由于 docker 的运行模式是 C/S。我们本机是 C,docker 引擎是 S。实际的构建过程是在 docker 引擎下完成的,所以这个时候无法用到我们本机的文件。这就需要把我们本机的指定目录下的文件一起打包提供给 docker 引擎使用。

如果未说明最后一个参数,那么默认上下文路径就是 Dockerfile 所在的位置。

注意上下文路径下不要放无用的文件,因为会一起打包发送给 docker 引擎,如果文件过多会造成过程缓慢

指令详解
COPY

复制指令,从上下文目录中复制文件或者目录到容器里指定路径。

格式

COPY [--chown=<user>:<group>] <源路径1>...  <目标路径>
COPY [--chown=<user>:<group>] ["<源路径1>",...  "<目标路径>"]

[–chown=<user>:<group>]:可选参数,用户改变复制到容器内文件的拥有者和属组。

<源路径>:源文件或者源目录,这里可以是通配符表达式,其通配符规则要满足 Go 的 filepath.Match 规则。例如

COPY hom* /mydir/
COPY hom?.txt /mydir/

<目标路径>:容器内的指定路径,该路径不用事先建好,路径不存在的话,会自动创建

ADD

ADD 指令和 COPY 的使用格类似(同样需求下,官方推荐使用 COPY)。功能也类似,不同之处如下:

  • ADD 的优点:在执行 <源文件> 为 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,会自动复制并解压到 <目标路径>。

  • ADD 的缺点:在不解压的前提下,无法复制 tar 压缩文件。会令镜像构建缓存失效,从而可能会令镜像构建变得比较缓慢。具体是否使用,可以根据是否需要自动解压来决定

CMD

类似于 RUN 指令,用于运行程序,但二者运行的时间点不同:

  • CMD 在docker run 时运行。

  • RUN 是在 docker build。

作用:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。

格式

作用:为启动的容器指定默认要运行的程序,程序运行结束,容器也就结束。CMD 指令指定的程序可被 docker run 命令行参数中指定要运行的程序所覆盖。

注意:如果 Dockerfile 中如果存在多个 CMD 指令,仅最后一个生效。

格式

CMD <shell 命令> 
CMD ["<可执行文件或命令>","<param1>","<param2>",...] 
CMD ["<param1>","<param2>",...]  # 该写法是为 ENTRYPOINT 指令指定的程序提供默认参数

推荐使用第二种格式,执行过程比较明确。第一种格式实际上在运行的过程中也会自动转换成第二种格式运行,并且默认可执行文件是 sh

ENTRYPOINT

类似于 CMD 指令,但其不会被 docker run 的命令行参数指定的指令所覆盖,而且这些命令行参数会被当作参数送给 ENTRYPOINT 指令指定的程序。

但是, 如果运行 docker run 时使用了 –entrypoint 选项,将覆盖 ENTRYPOINT 指令指定的程序。

优点:在执行 docker run 的时候可以指定 ENTRYPOINT 运行所需的参数。

注意:如果 Dockerfile 中如果存在多个 ENTRYPOINT 指令,仅最后一个生效。

格式:

ENTRYPOINT ["<executeable>","<param1>","<param2>",...]

可以搭配 CMD 命令使用:一般是变参才会使用 CMD ,这里的 CMD 等于是在给 ENTRYPOINT 传参,以下示例会提到。

示例:

假设已通过 Dockerfile 构建了 nginx:test 镜像:

FROM nginx

ENTRYPOINT ["nginx", "-c"] # 定参
CMD ["/etc/nginx/nginx.conf"] # 变参 

不传参运行

$ docker run  nginx:test

传参运行

$ docker run  nginx:test -c /etc/nginx/new.conf

容器内会默认运行以下命令,启动主进程(/etc/nginx/new.conf:假设容器内已有此文件)

nginx -c /etc/nginx/new.conf
ENV

设置环境变量,定义了环境变量,那么在后续的指令中,就可以使用这个环境变量。格式:

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

以下示例设置 NODE_VERSION = 7.2.0 , 在后续的指令中可以通过 $NODE_VERSION 引用

ENV NODE_VERSION 7.2.0

RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \
  && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc"
ARG

构建参数,与 ENV 作用一致。不过作用域不一样。ARG 设置的环境变量仅对 Dockerfile 内有效,也就是说只有 docker build 的过程中有效,构建好的镜像内不存在此环境变量。

构建命令 docker build 中可以用 –build-arg <参数名>=<值> 来覆盖。

格式

ARG <参数名>[=<默认值>]
VOLUME

定义匿名数据卷。在启动容器时忘记挂载数据卷,会自动挂载到匿名卷。

作用:

  • 避免重要的数据,因容器重启而丢失,这是非常致命的。

  • 避免容器不断变大。

格式

VOLUME ["<路径1>", "<路径2>"...]
VOLUME <路径>
WORKDIR

指定工作目录。用 WORKDIR 指定的工作目录,会在构建镜像的每一层中都存在。以后各层的当前目录就被改为指定的目录,如该目录不存在,WORKDIR 会帮你建立目录。

docker build 构建镜像过程中的,每一个 RUN 命令都是新建的一层。只有通过 WORKDIR 创建的目录才会一直存在。

格式

WORKDIR <工作目录路径>
USER

用于指定执行后续命令的用户和用户组,这边只是切换后续命令执行的用户(用户和用户组必须提前已经存在)。

格式:

USER <用户名>[:<用户组>]
HEALTHCHECK

用于指定某个程序或者指令来监控 docker 容器服务的运行状态。

格式:

HEALTHCHECK [选项] CMD <命令>:设置检查容器健康状况的命令
HEALTHCHECK NONE:如果基础镜像有健康检查指令,使用这行可以屏蔽掉其健康检查指令

HEALTHCHECK [选项] CMD <命令> : 这边 CMD 后面跟随的命令使用,可以参考 CMD 的用法。
ONBUILD

用于延迟构建命令的执行。简单的说,就是 Dockerfile 里用 ONBUILD 指定的命令,在本次构建镜像的过程中不会执行(假设镜像为 test-build)。当有新的 Dockerfile 使用了之前构建的镜像 FROM test-build ,这时执行新镜像的 Dockerfile 构建时候,会执行 test-build 的 Dockerfile 里的 ONBUILD 指定的命令。

格式

ONBUILD <其它指令>
LABEL

LABEL 指令用来给镜像添加一些元数据(metadata),以键值对的形式,语法格式如下

LABEL <key>=<value> <key>=<value> <key>=<value> ...

比如我们可以添加镜像的作者:

LABEL org.opencontainers.image.authors="pt"

Explain介绍

使用EXPLAIN关键字可以模拟优化器执行SQL语句,分查询语句或是结构的性能瓶颈

在select 语句之前增加 explain 关键字,MySQL 会在查询上设置一个标记,执行查询会返回执行计划的信息,而不是执行这条SQL。

如果from 中包含子查询,仍会执行该子查询,将结果放入临时表中 。


测试数据

DBVersion

1
2
3
4
5
6
7
8
9
 mysql> select version();
+------------+
| version() |
+------------+
| 5.7.29-log |
+------------+
1 row in set (0.00 sec)

mysql>
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
DROP TABLE IF EXISTS actor;

CREATE TABLE actor (
id int(11) NOT NULL,
name varchar(45) DEFAULT NULL,
update_time datetime DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO actor (id, name, update_time) VALUES (1,'a','2017-12-22 15:27:18'), (2,'b','2017-12-22 15:27:18'), (3,'c','2017-12-22 15:27:18');

###############################

DROP TABLE IF EXISTS film;

CREATE TABLE film (
id int(11) NOT NULL AUTO_INCREMENT,
name varchar(10) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO film (id, name) VALUES (3,'film0'),(1,'film1'),(2,'film2');

###############################

DROP TABLE IF EXISTS film_actor;

CREATE TABLE film_actor (
id int(11) NOT NULL,
film_id int(11) NOT NULL,
actor_id int(11) NOT NULL,
remark varchar(255) DEFAULT NULL,
PRIMARY KEY (id),
KEY idx_film_actor_id (film_id,actor_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO film_actor (id, film_id, actor_id) VALUES (1,1,1),(2,1,2),(3,2,1);

explain 使用

explain 两个扩展的使用

explain extended: 提供: 额外一些查询优化的信息 (‘EXTENDED’ is deprecated and will be removed in a future release.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
mysql> explain extended select * from film where id=1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | film | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 2 warnings (0.00 sec)
# 2 warnings

# 可以通过 show warnings 命令查看
mysql> show warnings;
+---------+------+----------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------------------------------------------+
| Warning | 1681 | 'EXTENDED' is deprecated and will be removed in a future release. |
| Note | 1003 | /* select#1 */ select '1' AS id,'film1' AS name from dbtest.film where 1 |
+---------+------+----------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

mysql>

filtered 列: 百分比,计算公式 rows * filtered/100 可以估算出将要和 explain 中前一个表进行连接的行数(前一个表指 explain 中的id值比当前表id值小的表) , 供参考


第二个**‘PARTITIONS’ is deprecated and will be removed in a future**

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> explain partitions select * from film where id=1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | film | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 2 warnings (0.00 sec)

mysql> show warnings;
+---------+------+----------------------------------------------------------------------------------+
| Level | Code | Message |
+---------+------+----------------------------------------------------------------------------------+
| Warning | 1681 | 'PARTITIONS' is deprecated and will be removed in a future release. |
| Note | 1003 | /* select#1 */ select '1' AS id,'film1' AS name from dbtest.film where 1 |
+---------+------+----------------------------------------------------------------------------------+
2 rows in set (0.00 sec)

mysql>

所以只使用explain就足够了 。


explain重要列说明

1
2
3
4
5
6
7
8
mysql> explain select * from film_actor a where a.actor_id  = (select id from actor where name = 'a');
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | PRIMARY | a | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
| 2 | SUBQUERY | actor | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

id

id列的编号是 select 的序列号,有几个 select 就有几个id,并且id的顺序是按 select 出现的顺序增长的。

执行顺序:

id越大执行优先级越高,id相同则从上往下执行,id为NULL最后执行


select_type

表示的对应行是简单还是复杂的查询

simple

简单查询,查询不包含子查询和union

1
2
3
4
5
6
7
8
9
mysql> explain select * from film where id=1;
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
| 1 | SIMPLE | film | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+-------+------------+-------+---------------+---------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql>

primary

复杂查询中最外层的 select

1
2
3
4
5
6
7
8
mysql> explain select * from film_actor a where a.actor_id  = (select id from actor where name = 'a');
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | PRIMARY | a | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
| 2 | SUBQUERY | actor | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

subquery

包含在select 中的子查询(不在 from 子句中)

1
2
3
4
5
6
7
8
mysql> explain select * from film_actor a where a.actor_id  = (select id from actor where name = 'a');
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | PRIMARY | a | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
| 2 | SUBQUERY | actor | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

derived

包含在 from 子句中的子查询。

MySQL会将结果存放在一个临时表中,也称为派生表

derived: 衍生的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mysql> set session optimizer_switch='derived_merge=off'; #关闭mysql5.7新特性对衍生表的合并优化
Query OK, 0 rows affected (0.00 sec)

mysql> explain select (select 1 from actor where id = 1 ) from (select * from film where id =1 ) t ;
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------------+
| 1 | PRIMARY | <derived3> | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 3 | DERIVED | film | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
| 2 | SUBQUERY | actor | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | Using index |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)

mysql>

union

在union 中的第二个和随后的 select

1
2
3
4
5
6
7
8
9
10
11
mysql> EXPLAIN select 1 union select 1 ;
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
| 2 | UNION | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
| NULL | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)

mysql>

table

表示explain 的一行正在访问哪个表

1
2
3
4
5
6
7
8
9
mysql> explain select (select 1 from  actor where id = 1 ) from (select * from film where id =1 ) t ;
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------------+
| 1 | PRIMARY | <derived3> | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 3 | DERIVED | film | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
| 2 | SUBQUERY | actor | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | Using index |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------------+
3 rows in set, 1 warning (0.00 sec)

当from 子句中有子查询时,table列是 <derivenN> 格式,表示当前查询依赖 id=N 的查询,于是先执行 id=N 的查询。

1
2
3
4
5
6
7
8
9
10
11
mysql> EXPLAIN select 1 union select 1 ;
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | PRIMARY | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
| 2 | UNION | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | No tables used |
| NULL | UNION RESULT | <union1,2> | NULL | ALL | NULL | NULL | NULL | NULL | NULL | NULL | Using temporary |
+----+--------------+------------+------------+------+---------------+------+---------+------+------+----------+-----------------+
3 rows in set, 1 warning (0.00 sec)

mysql>

当有 union 时,UNION RESULT 的 table 列的值为<union1,2>,1和2表示参与 union 的 select 行id。


type

表示关联类型或访问类型,即MySQL决定如何查找表中的行,查找数据行记录的大概范围。

依次从最优到最差分别为:system > const > eq_ref > ref > range > index > ALL

一般来说,得保证查询达到range级别,最好达到ref


NULL

mysql能够在优化阶段分解查询语句,在执行阶段用不着再访问表或索引.

例如:在索引列中选取最小值,可以单独查找索引来完成,不需要在执行时访问表

1
2
3
4
5
6
7
mysql> explain select min(id) from actor;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
| 1 | SIMPLE | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | NULL | Select tables optimized away |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+------------------------------+
1 row in set, 1 warning (0.00 sec)

const, system

mysql能对查询的某部分进行优化并将其转化成一个常量(可以看show warnings 的结果)。用于primary key 或 unique key 的所有列与常数比较时,所以表最多有一个匹配行,读取1次,速度比较快。

system是const的特例,表里只有一条元组匹配时为system.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> EXPLAIN select * from (select * from film where id=1) t ;
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------+
| 1 | PRIMARY | <derived2> | NULL | system | NULL | NULL | NULL | NULL | 1 | 100.00 | NULL |
| 2 | DERIVED | film | NULL | const | PRIMARY | PRIMARY | 4 | const | 1 | 100.00 | NULL |
+----+-------------+------------+------------+--------+---------------+---------+---------+-------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

mysql> show warnings;
+-------+------+---------------------------------------------------------------+
| Level | Code | Message |
+-------+------+---------------------------------------------------------------+
| Note | 1003 | /* select#1 */ select '1' AS id,'film1' AS name from dual |
+-------+------+---------------------------------------------------------------+
1 row in set (0.00 sec)

mysql>

eq_ref

primary key 或 unique key 索引的所有部分被连接使用 ,最多只会返回一条符合条件的记录。这可能是在const 之外最好的联接类型了,简单的 select 查询不会出现这种 type。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
mysql> EXPLAIN select * from film_actor a  left join film b on a.film_id = b.id ;
+----+-------------+-------+------------+--------+---------------+---------+---------+------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------------+------+----------+-------+
| 1 | SIMPLE | a | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | NULL |
| 1 | SIMPLE | b | NULL | eq_ref | PRIMARY | PRIMARY | 4 | dbtest.a.film_id | 1 | 100.00 | NULL |
+----+-------------+-------+------------+--------+---------------+---------+---------+------------------+------+----------+-------+
2 rows in set, 1 warning (0.00 sec)

mysql> show warnings;
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| Note | 1003 | /* select#1 */ select dbtest.a.id AS id,dbtest.a.film_id AS film_id,dbtest.a.actor_id AS actor_id,dbtest.a.remark AS remark,dbtest.b.id AS id,dbtest.b.name AS name from dbtest.film_actor a left join dbtest.film b on((dbtest.b.id = dbtest.a.film_id)) where 1 |
+-------+------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql>

ref

相比 eq_ref,不使用唯一索引,而是使用普通索引或者唯一性索引的部分前缀,索引要和某个值相比较,可能会找到多个符合条件的行.

【简单 select 查询,name是普通索引(非唯一索引)】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> show INDEX  from  film ;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| film | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
| film | 1 | idx_name | 1 | name | A | 3 | NULL | NULL | YES | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
2 rows in set (0.00 sec)

mysql>
mysql> EXPLAIN select * from film a where a.name = 'film0';
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | a | NULL | ref | idx_name | idx_name | 33 | const | 1 | 100.00 | Using index |
+----+-------------+-------+------------+------+---------------+----------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql>

【关联表查询,idx_film_actor_id是film_id和actor_id的联合索引,这里使用到了film_actor的左边前缀film_id部分。】

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
mysql> show index from film_actor;
+------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| film_actor | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
| film_actor | 1 | idx_film_actor_id | 1 | film_id | A | 2 | NULL | NULL | | BTREE | | |
| film_actor | 1 | idx_film_actor_id | 2 | actor_id | A | 3 | NULL | NULL | | BTREE | | |
+------------+------------+-------------------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
3 rows in set (0.00 sec)

mysql>
mysql> EXPLAIN select film_id from film left join film_actor on film.id =film_actor.film_id ;
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+----------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+----------------+------+----------+-------------+
| 1 | SIMPLE | film | NULL | index | NULL | idx_name | 33 | NULL | 3 | 100.00 | Using index |
| 1 | SIMPLE | film_actor | NULL | ref | idx_film_actor_id | idx_film_actor_id | 4 | dbtest.film.id | 1 | 100.00 | Using index |
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+----------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

mysql>

range

范围扫描通常出现在 in(), between ,> ,<, >= 等操作中。使用一个索引来检索给定范围的行。

1
2
3
4
5
6
7
mysql> explain select * from actor  where id > 1 ;
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
| 1 | SIMPLE | actor | NULL | range | PRIMARY | PRIMARY | 4 | NULL | 2 | 100.00 | Using where |
+----+-------------+-------+------------+-------+---------------+---------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

index

扫描全索引就能拿到结果,一般是扫描某个二级索引,这种扫描不会从索引树根节点开始快速查找,而是直接对二级索引的叶子节点遍历和扫描,速度还是比较慢的,这种查询一般为使用覆盖索引,二级索引一般比较小,所以这种通常比ALL快一些

1
2
3
4
5
6
7
mysql> explain select * from film;
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| 1 | SIMPLE | film | NULL | index | NULL | idx_name | 33 | NULL | 3 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

ALL

全表扫描,扫描你的聚簇索引的所有叶子节点。通常情况下这需要增加索引来进行优化了

1
2
3
4
5
6
7
mysql> explain select * from actor ;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
| 1 | SIMPLE | actor | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | NULL |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

possible_keys

显示查询可能使用哪些索引来查找

explain 时可能出现 possible_keys 有列,而 key 显示 NULL 的情况,这种情况是因为表中数据不多,mysql认为索引对此查询帮助不大,选择了全表查询。

如果该列是NULL,则没有相关的索引。在这种情况下,可以通过检查 where 子句看是否可以创造一个适当的索引来提高查询性能,然后用 explain 查看效果。


key

mysql实际采用哪个索引来优化对该表的访问。

如果没有使用索引,则该列是 NULL。如果想强制mysql使用或忽视possible_keys列中的索引,在查询中使用 force index、ignore index。


key_len

显示了mysql在索引里使用的字节数,通过这个值可以算出具体使用了索引中的哪些列。

举个例子 :

film_actor的联合索引 idx_film_actor_id 由 film_id 和 actor_id 两个int列组成,并且每个int是4字节。

1
2
3
4
5
6
7
8
9
mysql> explain select * from film_actor where film_id=1;
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | film_actor | NULL | ref | idx_film_actor_id | idx_film_actor_id | 4 | const | 2 | 100.00 | NULL |
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)

mysql>

通过结果中的key_len=4可推断出查询使用了第一个列:film_id列来执行索引查找。

key_len计算规则

【字符串】

  • char(n):n字节长度

  • varchar(n):如果是utf-8,则长度 3n + 2 字节,加的2字节用来存储字符串长度

【数值类型】

  • tinyint:1字节

  • smallint:2字节

  • int:4字节

  • bigint:8字节

【时间类型】

  • date:3字节

  • timestamp:4字节

  • datetime:8字节

如果字段允许为 NULL,需要1字节记录是否为 NULL

索引最大长度是768字节,当字符串过长时,mysql会做一个类似左前缀索引的处理,将前半部分的字符提取出来做索引


ref

显示了在key列记录的索引中,表查找值所用到的列或常量,常见的有:const(常量),字段名(例:film.id)

1
2
3
4
5
6
7
mysql> explain select * from film_actor where film_id=1;
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------+
| 1 | SIMPLE | film_actor | NULL | ref | idx_film_actor_id | idx_film_actor_id | 4 | const | 2 | 100.00 | NULL |
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------+
1 row in set, 1 warning (0.00 sec)
1
2
3
4
5
6
7
8
mysql> EXPLAIN select film_id from film  left join  film_actor  on film.id =film_actor.film_id ;
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+----------------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+----------------+------+----------+-------------+
| 1 | SIMPLE | film | NULL | index | NULL | idx_name | 33 | NULL | 3 | 100.00 | Using index |
| 1 | SIMPLE | film_actor | NULL | ref | idx_film_actor_id | idx_film_actor_id | 4 | dbtest.film.id | 1 | 100.00 | Using index |
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+----------------+------+----------+-------------+
2 rows in set, 1 warning (0.00 sec)

rows

mysql估计要读取并检测的行数,注意这个不是结果集里的行数。


Extra

展示的是额外信息。

列举几个常见的值

Using index

使用覆盖索引 : 无需回表

mysql执行计划explain结果里的key有使用索引,如果select后面查询的字段都可以从这个索引的树中获取,这种情况一般可以说是用到了覆盖索引,extra里一般都有using index;

覆盖索引一般针对的是辅助索引,整个查询结果只通过辅助索引就能拿到结果,不需要通过辅助索引树找到主键,再通过主键去主键索引树里获取其它字段值

1
2
3
4
5
6
7
8
9
mysql> explain select film_id from film_actor where film_id=1;
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
| 1 | SIMPLE | film_actor | NULL | ref | idx_film_actor_id | idx_film_actor_id | 4 | const | 2 | 100.00 | Using index |
+----+-------------+------------+------------+------+-------------------+-------------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql>

Using where

使用 where 语句来处理结果,并且查询的列未被索引覆盖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql> show index from actor;
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
| actor | 0 | PRIMARY | 1 | id | A | 3 | NULL | NULL | | BTREE | | |
+-------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+
1 row in set (0.00 sec)

mysql> explain select * from actor where name = 'a';
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| 1 | SIMPLE | actor | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 33.33 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql>

Using index condition

查询的列不完全被索引覆盖,where条件中是一个前导列的范围;

1
2
3
4
5
6
7
8
9
mysql> explain select * from film_actor where film_id > 1 ;
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------+
| 1 | SIMPLE | film_actor | NULL | range | idx_film_actor_id | idx_film_actor_id | 4 | NULL | 1 | 100.00 | Using index condition |
+----+-------------+------------+------------+-------+-------------------+-------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

mysql>

Using temporary

mysql需要创建一张临时表来处理查询。出现这种情况一般是要进行优化的,首先是想到用索引来优化。

【actor.name没有索引,此时创建了张临时表来distinct】

1
2
3
4
5
6
7
mysql> explain select distinct name from actor;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
| 1 | SIMPLE | actor | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | Using temporary |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-----------------+
1 row in set, 1 warning (0.00 sec)

【film.name建立了idx_name索引,此时查询时extra是using index,没有用临时表】

1
2
3
4
5
6
7
8
9
mysql> explain select distinct name from film;
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| 1 | SIMPLE | film | NULL | index | idx_name | idx_name | 33 | NULL | 3 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql>

Using filesort

将用外部排序而不是索引排序,数据较小时从内存排序,否则需要在磁盘完成排序。这种情况下一般也是要考虑使用索引来优化的

【actor.name未创建索引,会浏览actor整个表,保存排序关键字name和对应的id,然后排序name并检索行记录】

1
2
3
4
5
6
7
8
9
mysql> explain select * from actor order by name;
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
| 1 | SIMPLE | actor | NULL | ALL | NULL | NULL | NULL | NULL | 3 | 100.00 | Using filesort |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+----------------+
1 row in set, 1 warning (0.00 sec)

mysql>

【film.name建立了idx_name索引,此时查询时extra是using index】

1
2
3
4
5
6
7
8
9
mysql> explain select * from film order by name ;
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | filtered | Extra |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
| 1 | SIMPLE | film | NULL | index | NULL | idx_name | 33 | NULL | 3 | 100.00 | Using index |
+----+-------------+-------+------------+-------+---------------+----------+---------+------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

mysql>

Select tables optimized away

使用某些聚合函数(比如 max、min)来访问存在索引的某个字段

*

一、NIO基本介绍

1)、Java NIO 全称 java non-blocking IO,是值 JDK 提供的新API。从 JDK1.4 开始,Java 提供了一系列改进的输入/输出的新特性,被统称为 NIO(即 New IO),是同步非阻塞的。

2)、NIO 相关类都被放在 java.nio 包及子包下,并且对原 java.io 包中的很多类进行改写。

3)、NIO 有三大核心部分:Channel(通道)Buffer(缓冲区)Selector(选择器)
*

4)、NIO 是面向缓冲区或者面向块编程的。数据读取到一个它稍后处理的缓冲区,需要时可在缓冲区中前后移动,这就增加了处理过程中的灵活性,使用它可以提供非阻塞式的高伸缩性网络。

5)、Java NIO的非阻塞模式,一个线程从某通道发送请求读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。

6)、通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞IO那样,非得分配10000个。

7)、HTTP2.0使用了多路复用的技术,做到同一个连接并发处理多个请求,而且并发请求的数量比HTTP1.1大了好几个数量级。

二、Buffer代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public class BasicBuffer {

public static void main(String[] args) {
// 举例说明 Buffer 的使用(简单说明)

// 创建一个 Buffer,大小为10,即可以存放10个int
IntBuffer intBuffer = IntBuffer.allocate(10);

// 向 Buffer 中存放数据
/*intBuffer.put(10);
intBuffer.put(11);
intBuffer.put(12);
intBuffer.put(13);
intBuffer.put(14);
intBuffer.put(15);
intBuffer.put(16);
intBuffer.put(17);
intBuffer.put(18);
intBuffer.put(19);*/
for(int i = 0; i < intBuffer.capacity(); i++){
intBuffer.put( i * 2 );
}

// 从 Buffer 读取数据
intBuffer.flip(); // 将 Buffer 转换,读写切换(!!!)

while (intBuffer.hasRemaining()){
System.out.println(intBuffer.get());
}
}
}

三、NIO 和 BIO 的比较

1)、BIO 以流的方式处理数据,而 NIO 以块的方式处理数据,块 I/O 的效率比 流 I/O 高很多。

2)、BIO 是阻塞的,NIO 则是非阻塞的。

3)、BIO 基于字节流和字符流进行操作,而 NIO 基于 Channel(通道)和 Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求、数据到达等),因此使用单个线程就可以监听多个客户端通道。

四、NIO 三大核心组件关系

* **Selector、Channel和Buffer的关系图说明:**

1、 每个Channel都会对应一个Buffer;
2、 Selector会对应一个线程,一个线程对应多个Channel(连接);
3、 该图反映了有三个Channel注册到了Selector上;
4、 程序切换到哪个Channel是由事件(Event)决定的,Event是一个非常重要的概念;
5、 Selector会根据不同的事件,在各个通道上切换;
6、 Buffer就是一个内存块,底层是有一个数组的;
7、 数据的读取和写入是通过Buffer实现的,这个和BIO是有本质区别的,BIO中可以是输入流或者是输出流,不能是双向的,但是NIO的Buffer既可以是读,也可以是写,需要使用flip函数进行切换;
8、 Channel是双向的,可以反映底层操作系统的情况,比如LINUX底层的操作系统通道就是双向的;

主要内容

  • 概述

  • 对象已死?

  • 垃圾收集器算法

概述

前面介绍了Java内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭,栈中的栈桢随着方法的进入和退出而有条不絮执行着出栈和入栈操作。每一个栈桢分配多少内存基本上是类结构确定下来时就已知的,尽管运行期由JIT编译器进行优化,因此这几个区域的内存分配和回收都举办确定性,在这几个区域不需要过多考虑内存回收问题。而java堆和方法区则不一样,一个接口多个实现类需要的内存不一样,一个方法中的多个分支需要的内存也不一样,只有在程序运行期间才知道会创建那些对象,这部分内存回收动态的

对象是否存活

引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就减1,任何时刻计数器都为0的一些就是不可能在被使用的,客观地说,引用计数算法的实现简单,判定的效率也挺高,但是,java语言中没有选用引用计数算法来管理内存,其中最主要的原因时它很难结局是对象实现的相互引用问题

举个简单的例子,代码如下,对象ObjA和对象ObjB都有字段instance,赋值令objA.instance = objB及obj.instance=objA,除此之外,这两个对象在无任何引用,实际上这两个对象已经不肯在被访问,但是因为互相引用这对方,导致他们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收他们

public class ReferenceCountingGC {
    public Object instance=null;
​
    private static final int _1MB=1024*1024;
​
    /*
     这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
     */
​
    private byte[] bigSize = new byte[2 * _1MB];
​
    public static void main(String[] args) {
        ReferenceCountingGC.testGC();
    }
    public static void testGC(){
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC ();
        objA. instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
        //假设在这行发生GC,那么objA和objB是否能被回收?
        System.gc ();
    }
}
​

运行结果

[0.001s][warning][gc] -XX:+PrintGCDetails is deprecated. Will use -Xlog:gc* instead.
[0.006s][info   ][gc] Using G1
[0.006s][info   ][gc,init] Version: 17.0.8+9-LTS-211 (release)
[0.006s][info   ][gc,init] CPUs: 8 total, 8 available
[0.006s][info   ][gc,init] Memory: 8192M
[0.006s][info   ][gc,init] Large Page Support: Disabled
[0.006s][info   ][gc,init] NUMA Support: Disabled
[0.006s][info   ][gc,init] Compressed Oops: Enabled (Zero based)
[0.006s][info   ][gc,init] Heap Region Size: 1M
[0.006s][info   ][gc,init] Heap Min Capacity: 8M
[0.006s][info   ][gc,init] Heap Initial Capacity: 128M
[0.006s][info   ][gc,init] Heap Max Capacity: 2G
[0.006s][info   ][gc,init] Pre-touch: Disabled
[0.006s][info   ][gc,init] Parallel Workers: 8
[0.006s][info   ][gc,init] Concurrent Workers: 2
[0.006s][info   ][gc,init] Concurrent Refinement Workers: 8
[0.006s][info   ][gc,init] Periodic GC: Disabled
[0.010s][info   ][gc,metaspace] CDS archive(s) mapped at: [0x0000007000000000-0x0000007000be4000-0x0000007000be4000), size 12468224, SharedBaseAddress: 0x0000007000000000, ArchiveRelocationMode: 1.
[0.010s][info   ][gc,metaspace] Compressed class space mapped at: 0x0000007001000000-0x0000007041000000, reserved size: 1073741824
[0.010s][info   ][gc,metaspace] Narrow klass base: 0x0000007000000000, Narrow klass shift: 0, Narrow klass range: 0x100000000
[0.047s][info   ][gc,task     ] GC(0) Using 3 workers of 8 for full compaction
[0.047s][info   ][gc,start    ] GC(0) Pause Full (System.gc())
[0.047s][info   ][gc,phases,start] GC(0) Phase 1: Mark live objects
[0.048s][info   ][gc,phases      ] GC(0) Phase 1: Mark live objects 0.800ms
[0.048s][info   ][gc,phases,start] GC(0) Phase 2: Prepare for compaction
[0.048s][info   ][gc,phases      ] GC(0) Phase 2: Prepare for compaction 0.184ms
[0.048s][info   ][gc,phases,start] GC(0) Phase 3: Adjust pointers
[0.049s][info   ][gc,phases      ] GC(0) Phase 3: Adjust pointers 0.682ms
[0.049s][info   ][gc,phases,start] GC(0) Phase 4: Compact heap
[0.049s][info   ][gc,phases      ] GC(0) Phase 4: Compact heap 0.143ms
[0.050s][info   ][gc,heap        ] GC(0) Eden regions: 2->0(3)
[0.050s][info   ][gc,heap        ] GC(0) Survivor regions: 0->0(0)
[0.050s][info   ][gc,heap        ] GC(0) Old regions: 0->2
[0.050s][info   ][gc,heap        ] GC(0) Archive regions: 2->2
[0.050s][info   ][gc,heap        ] GC(0) Humongous regions: 6->0
[0.050s][info   ][gc,metaspace   ] GC(0) Metaspace: 404K(576K)->404K(576K) NonClass: 380K(448K)->380K(448K) Class: 23K(128K)->23K(128K)
[0.050s][info   ][gc             ] GC(0) Pause Full (System.gc()) 8M->1M(14M) 2.550ms
[0.050s][info   ][gc,cpu         ] GC(0) User=0.01s Sys=0.00s Real=0.00s
[0.051s][info   ][gc,heap,exit   ] Heap
[0.051s][info   ][gc,heap,exit   ]  garbage-first heap   total 14336K, used 1514K [0x0000000780000000, 0x0000000800000000)
[0.051s][info   ][gc,heap,exit   ]   region size 1024K, 1 young (1024K), 0 survivors (0K)
[0.051s][info   ][gc,heap,exit   ]  Metaspace       used 410K, committed 576K, reserved 1114112K
[0.051s][info   ][gc,heap,exit   ]   class space    used 24K, committed 128K, reserved 1048576K
​

以下是对日志中关键部分的解释:

  1. Deprecation Warning:

    • -XX:+PrintGCDetails is deprecated: 表示-XX:+PrintGCDetails参数已被弃用,JVM将使用新的日志记录系统-Xlog:gc*代替。
  2. GC Initialization:

    • Using G1: 表示JVM使用的是G1垃圾收集器。

    • Version: JVM的版本信息。

    • CPUs: 可用的CPU核心数。

    • Memory: 系统总内存。

    • Large Page SupportNUMA Support: 大页支持和非统一内存访问(NUMA)支持的状态。

    • Compressed Oops: 是否启用了对象指针压缩。

    • Heap Region Size: 堆区域大小。

    • Heap Min/Initial/Max Capacity: 堆的最小、初始和最大容量。

  3. Metaspace Initialization:

    • CDS archive: 类数据共享(Class Data Sharing)存档信息。

    • Compressed class space: 压缩类空间的内存映射和保留大小。

    • Narrow klass: 有关对象头中类元数据压缩的信息。

  4. GC Event:

    • GC(0) Using 3 workers of 8 for full compaction: 第0次GC使用3个工作线程进行全堆压缩。

    • Pause Full (System.gc()): 由System.gc()触发的全停顿GC。

  5. GC Phases:

    • Phase 1: Mark live objects: 标记存活对象阶段。

    • Phase 2: Prepare for compaction: 准备压缩阶段。

    • Phase 3: Adjust pointers: 调整指针阶段。

    • Phase 4: Compact heap: 堆压缩阶段。

  6. GC Details:

    • Eden regions: Eden区的使用情况,从2个区域减少到0,共有3个区域。

    • Survivor regions: Survivor区的使用情况,没有变化。

    • Old regions: Old区的使用情况,从0增加到2个区域。

    • Archive regions: 存档区的使用情况,没有变化。

    • Humongous regions: 大对象区域的使用情况,从6个区域减少到0。

  7. Metaspace Details:

    • 显示了元空间的使用情况,包括非类空间(NonClass)和类空间(Class)的使用、提交和保留的大小。
  8. GC Summary:

    • Pause Full (System.gc()) 8M->1M(14M) 2.550ms: GC前后的堆使用情况,从8MB减少到1MB,总共有14MB的堆,GC暂停时间为2.550毫秒。
  9. CPU Time:

    • User=0.01s Sys=0.00s Real=0.00s: 用户时间、系统时间和实际时间,显示GC操作的持续时间。
  10. Heap at GC Exit:

    • 显示了GC退出时的堆信息,包括总大小、已使用大小、区域大小、年轻代和元空间的使用情况。

总结来说,日志显示了JVM的启动信息、G1垃圾收集器的使用、堆和元空间的配置,以及一次由System.gc()触发的全堆压缩GC事件的详细过程和结果。这次GC有效地减少了堆的使用量,并且在短时间内完成。

从运行结果中可以清楚地看到GC日志中包含“4603K->210K”,意味着虚拟机并没 有因为这两个对象互相引用就不回收它们,这也从侧面说明虚拟机并不是通过引用计数 算法来判断对象是否存活的。

根搜索算法

在主流的商用程序语言中(Java 和C#,甚至包括前面提到的古老的Lisp),都是使 用 根 搜 索 算 法 (GCRootsTracing ) 判 定 对 象 是 否 存 活 的 。 这 个 算 法 的 基 本 思 路 就 是 通 过一系列的名为“GCRoots” 的对象作为起始点,从这些节点开始向下搜索,搜索所 走 过 的 路 径 称 为 引 用 链 (ReferenceChain), 当 一个 对 象 到 GCRoots 没 有 任 何 引 用 链 相 连(用图论的话来说就是从GCRoots 到这个对象不可达)时,则证明此对象是不可用 的。如图3-1所示,对象object 5、object 6、object 7虽然互相有关联,但是它们到GC Roots 是不可达的,所以它们将会被判定为是可回收的对象。 在 Java 语 言 里 , 可 作 为 GCRoots 的 对 象 包 括 下面 几 种 :

  • 虚拟机栈 (栈帧中的本地变量表)中的引用的对象。

  • 方法区中的类静态属性引用的对象。

  • 方法区中的常量引用的对象。

  • 本地方法栈中JNI (即一般说的Native方法)的引用的对象

在谈引用

无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引 用链是否可达,判定对象是否存活都与“引用” 有关。在JDK1.2之前,Java中的引用 的定义很传统:如果reference 类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着 一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这 种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“ 食之无味,弃之可 惜”的对象就显得无能为力。我们希望能描述这样一类对象 :当内存空间还足够时,则 能保留在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。 很多系统的缓存功能都符合这样的应用场景。

在 JDK1 . 2 之 后 , Java 对 引 用 的 概 念 进 行 了 扩 充 , 将 引 用 分 为 强 引 用 (Strong Reference)、 软 引 用 (SoftReference )、 弱 引 用 (WeakReference )、 虚 引 用 (PhantomReference )四种,这四种引用强度依次逐渐减弱。

  • 强引用就是指在程序代码之中普遍存在的,类似“Objectobj=newObject()” 这 类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象

  • 软引用用来描述一些还有用,但并非必需的对象。对 于软引用关联着的对象,系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二 次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2 之后,提供了SoftReference类来实现软引用

  • 弱引用也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用 关 联 的 对 象 只 能 生 存 到 下 一次 垃 圾 收 集 发 生 之 前 。 当 垃 圾 收 集 器 工 作 时 , 无 论 当 前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2 之后,提供了W ea k R e f e r e n c e 类来 实 现 弱 引 用

  • 虚引用它是最弱的 一种引用关系。 一个对象是否 有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得 一 个对象实例。为 一个对象设置虚引用关联的唯 一目的就是希望能在这个对象被收 集器回收时收到 一个系统通知。在JDK1.2之后,提供了PhantomReference类来 实现虚引用

生存还是死亡

在根搜索算法中不可达的对象,也并非是“非死不可” 的,这时候它们暂时处于 “缓刑” 阶段,要真正宣告 一个对象死亡,至少要经历两次标记过程:如果对象在进行 根搜索后发现没有与GCRoots相连接的引用链,那它将会被第 一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize 方法。当对象没有覆盖finalize() 方法,或者finalize(方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要 执行”。如果这个对象被判定为有必要执行finalize ()方法,那么这个对象将会被放置在 一个 名为F -Queue 的队列之中,并在稍后由一条由虚拟机自动建立的、低优先级的Finaliz er 线程去执行。这里所谓的“执行” 是指虚拟机会触发这个方法,但并不承诺会等待它运 行结束。这样做的原因是,如果 一个对象在finalize(方法中执行缓慢,或者发生了死 循 环 (更 极 端 的 情 况 ), 将 很 可 能 会 导 致 F - Q u e u e 队 列 中 的 其 他 对 象 永 久 处 于等 待 状 态 , 甚至导致整个内存回收系统崩溃。finalize (方法是对象逃脱死亡命运的最后一次机会, 稍后 G C将对F- Qu eue中的对象进行第二次小规模的标记,如果对象要在finalize(中成 功拯救自己 ,只要重新与引用链 上的任何 一个对象建立关联即可,<u>譬如把自己 (this 关键字)赋值给某个类变量或对象的成员变量,那在第二次标记时它将被移除出“ 即将回收” 的集合;如果对象这时候还没有逃脱,那它就真的离死不远了。从下面代码中 中我们可以看到 一个对象的finalize()被执行,但是它仍然可以存活。

public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;
​
    public void isAlive() {
        System.out.println("yes, i am still alive :) ");
    }
/*
    finalize()方法通常用于在对象被垃圾收集前进行清理工作
     */
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize mehtod executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }
​
    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK=new FinalizeEscapeGC();
        // 对象第一次拯救自己
        SAVE_HOOK=null;
        System.gc();
        // 因为Finalize方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no,i am dead");
        }
        SAVE_HOOK=null;
        System.gc();
        // 因为Finalize方法优先级很低,暂停0.5秒,以等待它
        Thread.sleep(500);
        if(SAVE_HOOK!=null){
            SAVE_HOOK.isAlive();
        }else {
            System.out.println("no,i am dead");
        }
    }
}
运行结果
finalize mehtod executed!
yes, i am still alive :) 
no,i am dead

代码运行结果可以看到,SAVE_HOOK对象的finalizeO方法确实被 G C 收 集 器 触 发 过 , 并 且 在 被 收 集 前 成 功 逃 脱 了。 另外 一个值得注意的地方就是,代码中有两段完全 一样的代码片段,执行结果却是 一次逃脱成功, 一次失败,这是因为任何 一个对象的finalize(方法都只会被系统自动调 用 一次,如果对象面临下一次回收,它的finalize(方法不会被再次执行,因此第 二段代 码的自救行动失败 了。 需要特别说明的是,上面关于对象死亡时finalize()方法的描述可能带有悲情的艺术 色彩,笔者并不鼓励大家使用这种方法来拯救对象。相反,笔者建议大家尽量避免使用 它 , 因 为 它 不 是 C / C + + 中 的 析 构 函 数 , 而 是 J ava 刚 诞 生 时 为 了使 C / C + + 程 序 员 更 容 易 接受它所做出的 一个妥协。它的运行代价高昂,不确定性大,无法保证各个对象的调用 顺序。有些教材中提到它适合做“关闭外部资源” 之类的工作,这完全是对这种方法的 用途的 一种自我安慰。finalize()能做的所有工作,使用try-finally或其他方式都可以做 得更好、更及时,大家完全可以忘掉Java 语言中还有这个方法的存在。

回收方法区

很多人认为方法区 (或者HotSpot虚拟机中的永久代)是没有垃圾收集的,Java虚 拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区进行垃圾 收集的 “性价比” 一般比较低:在堆中,尤其是在新生代中,常规应用进行一次垃圾收集一般可以回收70%~ 95%的空间,而永久代的垃圾收集效率远低于此。 永久代的垃圾收集主要回收两部分内容:废弃常量和无用的类。回收废弃常量 与 回 收Java 堆 中 的 对 象 非 常 类 似 。 以 常 量 池 中 字面 量 的 回 收 为 例 , 假 如 一 个 字 符 串 “abc” 已经进入了常量池中,但是当前系统没有任何一个String对象是叫做“abc” 的,换句话说是没有任何String 对象引用常量池中的“abe” 常量,也没有其他地方 引用了这个字面量,如果在这时候发生内存回收,而且必要的话,这个“abc” 常量 就会被系统“请” 出常量池。常量池中的其他类(接口)、方法、字段的符号引用也 与此类似。 判定一个常量是否是“废弃常量” 比较简单,而要判定一个类是否是“ 无用的类” 的条件则相对苛刻许多。类需要同时满足下面3 个条件才能算是“ 无用的类” :

  • 该类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例。

  • 加 载 该 类的 C l a s s L o a d e r 已 经 被 回收 。

  • 该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法。

  • 虚拟机可以对满足上述3 个条件的无用类进行回收,这里说的仅仅是“ 可以”,而 不是和对象 一样,不使用了就必然会回收。是否对类进行回收,HotSpot 虚拟机提供 了 - X noclassge 参 数 进 行 控 制 , 还 可 以 使 用 -verbose : class 及 - X X :+ TraceClassL oading 、 - X X :+TraceClassUnLoading 查 看 类 的 加 载 和 卸 载 信 息 。 -verbore : class 和 - X X : +TraceClassLoading 可以在Product 版的虚拟机中使用,但是-XX:+TraceClassLoa ding 参数需要f ast dcbug 版的虚拟机支持。 在大量使用反射、动态代理、CGLi b 等bytecode 框架的场景,以及动态生成JSP 和 OSGi 这类频繁自定义ClassLoader 的场景都需要虚拟机具备类卸载的功能,以保证永久 代不会溢出。

垃圾收集算法

标记-清除算法

最 基 础 的 收 集 算 法 是 “ 标 记 一清 除 ” (Mark - Sweep ) 算 法 , 如 它 的 名 字 一 样 , 算 法 分 为“标记” 和“清除” 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回 收掉所有被标记的对象,它的标记过程其实在前 一节讲述对象标记判定时已经基本介绍过 了。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺 点进行改进而得到的。它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都 不高:另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多 可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而 不 得 不 提 前 触 发 另 一次 垃 圾 收 集 动 作 。 标 记 一 清 除 算 法 的 执 行 过 程 如 下图所示

标记-整理算法

“标记一整理”(Mark-Compact)算法, 标记过程仍然与“标记一清除” 算法一样,但后续步骤不是直接对可回收对象进行清 理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

当前商业虚拟机的垃圾收集都采用 “分代收集”(GenerationalCollection)算法,这 种算法并没有什么新的思想,只是根据对象的存活周期的不同将内存划分为几块。 一般是把Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算 法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复 制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存 活率高 、没有额外空间对它进行分配担保,就必须使用“标记 一清理” 或“标记一整 理” 算法来进行回收

垃圾收集器

Serial收集器

该收集器是 一个单线程的收集器,但它 的“单线程” 的意义并不仅仅是说明它只会使用一个CPU或一条收集线程去完成垃圾收 集 工 作 , 更 重 要 的 是 在 它 进 行 垃 圾 收 集 时 , 必 须 暂 停 其 他 所 有 的 工 作 线 程 (S u n 将 这 件 事 情 称 之 为 “ S t o p T h e W o r l d ” ), 直 到 它 收 集 结 束

ParNew收集器

P a r N e w 收 集 器 其 实 就 是 S e r i a l 收 集 器 的 多 线 程 版 本 , 除 了使 用 多 条 线 程 进 行 垃 圾 收集之外,其余行为包括Serial 收集器可用的所有控制参数 (例如:-XX:SurvivorRatio、 - X X : P r e t e n u r e S i z e T h r e s h o l d 、 - X X : H a n d l e P r o m o t i o n F a i l u r e 等 )、 收 集 算 法 、 S t o p T h e World、对象分配规则、回收策略等都与Serial 收集器完全 一样,实现上这两种收集器也 共 用 了相 当 多的 代 码 。 P a r N e w 收 集 器 的 工作 过 程 如 下图 所 示

ParNew收集器除 了多线程收集之外,其他与Serial收集器相比并没有太多创新之 处,但它却是许多运行在Server 模式下的虚拟机中首选的新生代收集器,其中有一个与 性能无关但很重要的原因是,除 了Serial 收集器外,目前只有它能与CMS收集器配合 工作

Parallel Scavenge收集器

Parallel Scavenge收集器也是 一个新生代收集器,它也是使用复制算法的收集器, 又是并行的多线程收集器……看上去和ParNew 都一样,那它有什么特别之处呢? Parallel Scavenge 收集器的特点是它的关注点与其他收集器不同,CMS 等收集器的 关注点尽可能地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目 标则是达到 一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代 码的时间与CPU总消耗时间的比值,即吞吐量= 运行用户代码时间/ (运行用户代码时 间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐 量就是99%。 停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体 验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适 合在后台运算而不需要太多交互的任务。Parallel Scavenge 收集器提供 了两 个参数用 于精确控制吞吐量,分别是控制 最 大 垃 圾 收 集 停 顿 时 间 的 - X X: M a x G C P a u s e M i l l i s 参 数 及 直 接 设 置 吞 吐 量 大 小 的-XX:GCTimeRatio 参数。Max GCPauseMillis 参数允许的值是 一个大于0的毫秒数,收集器将尽力保证内存回 收花费的时间不超过设定值。不过大家不要异想天开地认为如果把这个参数的值设置得 稍小 一点就能使得系统的垃圾收集速度变得更快,GC停顿时间缩短是以牺牲吞吐量和 新生代空间来换取的:系统把新生代调小 一些,收集300MB新生代肯定比收集500MB 快 吧 , 这 也 直 接 导 致 垃 圾 收 集 发 生 得 更 频 繁 一些 , 原 来 1 0 秒 收 集 一次 、 每 次 停 顿 1 0 0 毫秒,现在变成5秒收集一次、每次停顿70毫秒 。停顿时间的确在下降,但吞吐量也 降 下来 了。 GCTimeRatio 参数的值应当是一个大于0 小 于10 0的整数,也就是垃圾收集时间占 总 时 间 的 比 率 , 相 当 于是 吞 吐 量 的 倒 数 。 如 果 把 此 参 数 设 置 为 1 9 , 那 允 许 的 最 大 G C 时 间就占总时间的5% (即1/ (1+19),默认值为99,就是允许最大1% (即1/ (1+99) 的垃圾收集时间。由于与吞吐量关系密切,Parallel Scavenge收集器也经常被称为“吞吐量 优先” 收集器。除上述两个参数之外,Parallel Scavenge收集器还有 一个参数XX:+UseAdaptiveSizePolicy 值得关注。这是一个开关参数,当这个参数打开之后,就 不 需 要 手 工 指 定 新 生 代 的 大 小 ( - X m n )、 E d e n 与 S u r v i v o r 区 的 比 例 ( - X X : S u r v i v o r R a t i o )、 晋 升 老 年 代 对 象 年 龄 (- X X : P r e t e n u r e S i z e T h r e s h o l d ) 等 细 节 参 数 了 , 虚 拟 机 会 根 据 当 前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或 最 大的 吞 吐 量 , 这 种 调 节 方 式 称 为 G C 自 适 应 的 调 节 策 略 (G C E r g o n o m i c s )®。 如 果 读者对于收集器运作原理不太了解,手工优化存在困难的时候,使用Parallel Scavenge 收集器配合自适应调节策略,把内存管理的调优任务交给虚拟机去完成将是 一个很 不错的选择。只需要把基本的内存数据设置好(如-Xmx设置最大堆),然后使用 MaxGCPauseMillis 参数 (更关注最大停顿时间)或GCTimeRatio参数 (更关注吞吐量) 给虚拟机设立 一个优化目标,那具体细节参数的调节工作就由虚拟机完成了。自适应调节策略也是ParallelScavenge收集器与ParNew收集器的 一个重要区别

Parallel Old收集器

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和“标记一整理” 算法。这个收集器是在JDK 1.6 中才开始提供的,在此之前,新生代的Paral lel Scavenge 收集器一直处于比较尴尬的状态。原因是,如果新生代选择了Parallel Scavenge收集器, 老 年 代 除 了 S e r i a l O l d (P S M a r k S w e e p ) 收 集 器 外 别 无 选 择 (还 记 得 上 面 说 过 P a r a l l e l Scavenge收集器无法与CMS收集器配合工作吗?)。由于单线程的老年代Serial Old收 集 器 在 服 务 端 应 用 性 能 上的 “ 拖 累 ” , 即 便 使 用 了 P a r a l l e l S c a v e n g e 收 集 器 也 未 必 能 在 整 体 应 用 上获 得 吞 吐 量 最 大 化 的 效 果 , 又 因 为 老 年 代 收 集 中 无 法 充 分 利 用 服 务 器 多 C P U 的 处理能力,在老年代很大而且硬件比较高级的环境中,这种组合的吞吐量甚至还不 一定 有ParNew加CMS的组合“给力”。 直 到 P a r a l l e l O l d 收 集 器 出 现 后 , “ 吞 吐 量 优 先 ” 收 集 器 终 于 有 了比 较 名 副 其 实 的 应 用组合,在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge 加 Parallel Old收集器。

CMS收集器

CMS (Con current Mark Sweep )收集器是一种以获取最短回收停顿时间 为目标的收 集器。目前很大一部分的Java 应用都集中在互联网站或B/S 系统的服务端 上,这类应用 尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收 集器就非常符合这类应用的需求。

从 名 字 (包 含 “ M a r k S w e e p ” ) 上 就 可 以 看 出 C M S 收 集 器 是 基 于 “ 标 记 一 清 除 ” 算

法实现的,它的运作过程相对于前面几种收集器来说要更复杂 一些,整个过程分为4个 步骤,包括:

  • 初始 标记(CMSinitialmark)

  • 并 发 标 记 (C M S c o n c u r r e n t m a r k )

  • 重 新 标 记 (C M S r e m a r k )

  • 并 发 清 除 (C M S c o n c u r r e n t s w e ep ) 其中初始标记、重新标记这两个步骤仍然需要“St op The World”。初始标记仅仅 只是标记一下GCRoots 能直接关联到的对象,速度很快,并发标记阶段就是进行GC Roots Tracing的过程,而重新标记阶段则是为了修正并发标记期间,因用户程序继续运 作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间 一般会比初始 标记阶段稍长 一些,但远比并发标记的时间短。 由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户 线程 一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程 一起并发地 执行的。 CMS是 一款优秀的收集器,它的最主要优点在名字上已经体现出来了:并发收集、 低停顿,Sun的 一些官方文档里面也称之为并发低停顿收集器(ConcurrentLowPause C o l l e c t o r )。 但 是 C M S 还 远 达 不 到 完 美 的 程 度 , 它 有 以 下 三 个 显 著 的 缺 点 :

    • C M S 收 集 器 对 C P U 资 源 非 常 敏 感 。 其实 , 面 向 并 发 设 计 的 程 序 都 对 C P U 资 源 比 较敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分 线 程 (或 者 说 C P U 资 源 ) 而 导 致 应 用 程 序 变 慢 , 总 吞 吐 量 会 降 低 。

    • CMS 收集器无法处理浮动垃圾 (Floating Garbage ),可能出现 “ Concurrent Mode Failure” 失败而导致另 一次Full GC的产生。由于CMS 并发清理阶段用户线程还 在运行着,伴随程序的运行自然还会有新的垃圾不断产生,这 一部分垃圾出现在 标记过程之后 ,CMS 无法在本次收集中处理掉它们,只好留待下 一次 GC时再将 其清理掉。这一部分垃圾就称为“ 浮动垃圾”。也是由于在垃圾收集阶段用户线 程还需要运行,即还需要预留足够的内存空间给用户线程使用,因此CMS收集 器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留 一 部分空间提供并发收集时的程序运作使用。在默认设置下,CMS 收集器在老年代 使用了68%的空间后就会被激活,这是 一个偏保守的设置,如果在应用中老年代 增长不是太快,可以适当调高参数 -XX:CMSInitiatingOccupancyFraction 的值来 提高触发百分比,以便降低内存回收次数以获取更好的性能。要是CMS 运行期 间预留的内存无法满足程序需要,就会出现 一次 “Concurrent ModeFailure” 失 败,这时候虚拟机将启动后备预案:临时启用Serial Ol d收集器来重新进行老年 代的垃圾收集,这样停顿时间就很长了。所以说参数-XX:CMSInitiating Occupan cyFraction 设置得太高将会很容易导致大量“ Concurrent Mode Failure” 失败,性 能反而降低。

    • 还 有 最 后 一个 缺 点 , 在 本 节 在 开 头 说 过 , C M S 是 一款 基 于 “ 标 记 一 清 除 ” 算 法 实 现的收集器,如果读者对前面这种算法介绍还有印象的话,就可能想到这意味着 收集结束时会产生大量空间碎片。空间碎片过多时,将会给大对象分配带来很大 的麻烦,往往会出现老年代还有很大的空间剩余,但是无法找到足够大的连续空 间 来 分 配 当 前 对 象 , 不 得 不 提 前 触 发 一次 F u l l G C 。 为 了 解 决 这 个 问 题 , C M S 收 集 器 提 供 了 一 个 - X X :+ U s e C M S C o m p a c t A t F u l l C o l l e c t i o n 开 关 参 数 , 用 于 在 “ 享 64 第 二部分自动内存管理机制 受” 完Full GC服务之后额外免费附送一个碎片整理过程,内存整理的过程是无 法并发的。空间碎片问题没有了,但停顿时间不得不变长了。虚拟机设计者们还 提供了另外 一个参数XX:CMSFulIGCsBeforeCompaction,这个参数用于设置在执 行 多 少 次 不 压 缩 的 F u l l G C 后 , 跟 着 来 一次 带 压 缩 的

    G1收集器

    简单介绍”。 G1收集器是垃圾收集器理论进一步发展的产物,它与前面的CMS收集器相比有两 个显著的改进:一是G1收集器是基于“标记一整理” 算法实现的收集器,也就是说它 不会产生空间碎片,这对于长时间运行的应用系统来说非常重要。 二是它可以非常精确 地控制停顿,既能让使用者明确指定在 一个长度为M毫秒的时间片段内,消耗在垃圾收 集上的时间不得超过N 毫秒,这几乎已经是实时Java (RTSJ)的垃圾收集器的特征了。 G1收集器可以实现在基本不牺性吞吐量的前提下完成低停顿的内存回收,这是由于 它能够极力地避免全区域的垃圾收集,之前的收集器进行收集的范围都是整个新生代或 老年代,而GI 将整个Java堆 (包括新生代、老年代)划分为多个大小固定的独立区域 (R e g i o n ), 并 且 跟 踪 这 些 区 域 里 面 的 垃 圾 堆 积 程 度 , 在 后 台 维 护 一 个 优 先 列 表 , 每 次 根 据允许的收集时间,优先回收垃圾最多的区域(这就是GarbageFirst 名称的来由)。区 域划分及有优先级的区域回收,保证了G1收集器在有限的时间内可以获得最高的收集 效率

文章目录

  • MyISAM索引实现

  • 非聚簇(非聚集)索引

  • 索引原理图

  • InnoDB索引实现

  • 聚簇(聚集)索引

  • 索引原理图

  • 常见面试题

  • 为什么建议InnoDB表必须建主键,并且推荐使用整型的自增主键?

  • 为什么非主键索引结构叶子节点存储的是主键值?(一致性和节省存储空间)

  • 搞定MySQL

*

MySQL中,索引属于存储引擎级别的概念,不同存储引擎对索引的实现方式是不同的,我们这里主要讨论MyISAM和InnoDB两个存储引擎的索引实现方式。


MyISAM索引实现

非聚簇(非聚集)索引

我们建立一个myIsam存储引擎的表,看磁盘上的文件存储如下

*

我这个是8.0的MYSQL, 5.7版本 不是sdi结尾的文件,而是frm (framework)

可以看到MyISAM存储引擎的索引文件 MYI 和数据文件 MYD 是分离的(非聚集)

这就是非聚簇索引的含义, MYI 和 MYD 分开存储 ,同样的 InnoDB都存在.idb文件中,所以InnoDB存储引擎的索引就是聚簇索引。


索引原理图

MyISAM引擎使用B+Tree作为索引结构,叶节点的data域存放的是数据记录的地址。

*

上图就是 MyISAM索引的原理图。

上图一共有三列,假设我们以Col1为主键,则上图是一个MyISAM表的主索引(Primary key)示意。可以看出MyISAM的索引文件仅仅保存数据记录的地址

在MyISAM中,主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求key是唯一的,而辅助索引的key可以重复。

如果我们在Col2上建立一个辅助索引,则此索引的结构如下图所示:

*

同样也是一颗B+Tree,data域保存数据记录的地址。

因此,MyISAM中索引检索的算法为首先按照B+Tree搜索算法搜索索引,如果指定的Key存在,则取出其data域的值,然后以data域的值为地址,去另外一个文件中MYD读取相应数据记录。

MyISAM的索引方式也叫做“非聚集”的,之所以这么称呼是为了与InnoDB的聚集索引区分。


InnoDB索引实现

聚簇(聚集)索引

建立一个innodb存储引擎的表,看磁盘上的数据文件如下

*

这个ibd就是 数据和索引,这两个存储在一个文件中

第一个重大区别是InnoDB的数据文件本身就是索引文件 ,因为就只有一个ibd文件啊。

  • MyISAM索引文件和数据文件是分离的,索引文件仅保存数据记录的地址。

  • InnoDB中,表数据文件本身就是按B+Tree组织的一个索引结构,这棵树的叶节点data域保存了完整的数据记录。这个索引的key是数据表的主键,因此InnoDB表数据文件本身就是主索引。

InnoDB也使用B+Tree作为索引结构,但具体实现方式却与MyISAM 不同。


索引原理图

*

上图就是InnoDB主索引(同时也是数据文件)的示意图,可以看到叶节点包含了完整的数据记录。这种索引叫做聚集索引。


第二个与MyISAM索引的不同是InnoDB的辅助索引data域存储相应记录主键的值而不是地址。换句话说,InnoDB的所有辅助索引都引用主键作为data域

*

上图为定义在Col3上的一个辅助索引 观察叶子节点 : data域存储相应记录主键的值而不是地址

Col3字段上的索引,以英文字符的ASCII码作为比较准则。

聚集索引这种实现方式使得按主键的搜索十分高效,但是辅助索引搜索需要检索两遍索引:首先检索辅助索引获得主键,然后用主键到主索引中检索获得记录。


常见面试题

为什么建议InnoDB表必须建主键,并且推荐使用整型的自增主键?

因为InnoDB的数据文件本身要按主键聚集,所以InnoDB要求表必须有主键(MyISAM可以没有),如果没有显式指定,则MySQL系统会自动选择一个可以唯一标识数据记录的列作为主键,如果不存在这种列,则MySQL自动为InnoDB表生成一个隐含字段作为主键,这个字段长度为6个字节,类型为长整形

至于是整型,主要是构建B+Tree的时候,从左到右递增的属性,你如果用过UUID,不仅占用空间,还要转换成assic码进行比较,效率自然不行。


为什么非主键索引结构叶子节点存储的是主键值?(一致性和节省存储空间)

知道了InnoDB的索引实现后,就很容易明白为什么不建议使用过长的字段作为主键,因为所有辅助索引都引用主索引,过长的主索引会令辅助索引变得过大,占用空间

再比如用非单调(可重复)的字段作为主键在InnoDB中是不推荐的,因为InnoDB数据文件本身是一颗B+Tree,可重复的主键会造成在插入新记录时数据文件为了维持B+Tree的特性而频繁的分裂调整,十分低效,所以推荐使用自增主键。

pom依赖

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>netty-http</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>netty :: Http</name>
<description>netty实现高性能http服务器</description>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<netty-all.version>4.1.100.Final</netty-all.version>
<logback.version>1.1.7</logback.version>
<commons.codec.version>1.10</commons.codec.version>
<fastjson.version>1.2.51</fastjson.version>
<java.version>17</java.version>
</properties>

<dependencies>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.100.Final</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>${logback.version}</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.2</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>${commons.codec.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>

</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.7.0</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>utf-8</encoding>
</configuration>
</plugin>
</plugins>
</build>


</project>

Server

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
package com.example.netty;

import org.slf4j.LoggerFactory;
import org.slf4j.Logger;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.channel.Channel;
import io.netty.channel.ChannelOption;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.logging.LogLevel;
import io.netty.handler.logging.LoggingHandler;

public final class HttpServer {
private static final Logger logger = LoggerFactory.getLogger(HttpServer.class);
// 创建一个日志记录器实例,用于记录日志信息。
static final int PORT = 8888;
// 定义服务器监听的端口号为8888。
public static void main(String[] args) throws Exception {
/*
bossGroup:这个事件循环组专门用于接受客户端的连接。在你的代码中,bossGroup 是通过 new NioEventLoopGroup(1) 创建的,
这意味着它只有一个线程。这个单线程(或单个EventLoop)将负责监听服务器端口,并接受所有进入的连接。
由于它只处理连接的接受,而不处理其他I/O操作,通常一个线程就足够了,这也是为什么你看到创建时传递了 1 作为参数。
workerGroup:这个事件循环组用于处理已被 bossGroup 接受的连接之后的所有I/O操作。
例如,它将负责读取请求数据、处理请求和发送响应。在你的代码中,workerGroup 是通过 new NioEventLoopGroup() 创建的,
没有指定线程数量,这意味着Netty将根据默认的I/O工作器数量来创建EventLoops。这个组可以根据你的服务器硬件和负载情况配置多个线程,
以提高并发处理能力。
*/
EventLoopGroup bossGroup = new NioEventLoopGroup(1); // 监听端口
EventLoopGroup workerGroup = new NioEventLoopGroup(5); // 处理连接
try {
ServerBootstrap b = new ServerBootstrap();
// 使用ServerBootstrap辅助启动类来初始化服务器。
b.option(ChannelOption.SO_BACKLOG, 1024);
// 设置服务器可接受的连接队列大小为1024。
b.childOption(ChannelOption.TCP_NODELAY, true);
// 设置TCP_NODELAY选项为true,禁用Nagle算法,确保数据立即发送。
b.childOption(ChannelOption.SO_KEEPALIVE, true);
// 设置SO_KEEPALIVE选项为true,启用TCP保活机制。
b.group(bossGroup, workerGroup)
// 设置接受连接的事件循环组和处理连接的事件循环组。
.channel(NioServerSocketChannel.class)
// 设置用于创建服务器通道的类。
.handler(new LoggingHandler(LogLevel.INFO))
// 添加日志处理器,记录服务器的日志信息。
.childHandler(new HttpServerInitializer());
// 设置用于初始化每个新连接的Channel的ChannelInitializer。
Channel ch = b.bind(PORT).sync().channel();
// 绑定服务器到指定端口,并同步等待直到绑定成功。
logger.info("Netty http server listening on port " + PORT);
// 记录日志信息,告知服务器正在监听指定端口。
ch.closeFuture().sync();
// 等待直到Channel关闭。
} finally {
bossGroup.shutdownGracefully();
// 优雅地关闭bossGroup,释放资源。
workerGroup.shutdownGracefully();
// 优雅地关闭workerGroup,释放资源。
}
}
}

ServerHandler

package com.example.netty;

import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Objects;

import com.example.netty.pojo.User;
import com.example.netty.serialize.impl.JSONSerializer;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.util.AsciiString;
import org.apache.commons.codec.Charsets;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import static io.netty.handler.codec.http.HttpMethod.GET;
import static io.netty.handler.codec.http.HttpMethod.POST;

/**
 * @author pengtao
 */
public class HttpServerHandler extends SimpleChannelInboundHandler<HttpObject> {
    private static final Logger logger = LoggerFactory.getLogger(HttpServerHandler.class);
    private static final String FAVICON_ICO = "/favicon.ico";
    private static final AsciiString CONTENT_TYPE_HDR = AsciiString.cached("Content-Type");
    private static final AsciiString CONTENT_LENGTH_HDR = AsciiString.cached("Content-Length");
    private static final AsciiString CONNECTION_HDR = AsciiString.cached("Connection");
    private static final AsciiString KEEP_ALIVE_VAL = AsciiString.cached("keep-alive");

    private HttpRequest request;
    private HttpHeaders headers;

    @Override
    protected void channelRead0(ChannelHandlerContext ctx, HttpObject msg) throws Exception {
        // 检查是否为HttpRequest对象
        if (!(msg instanceof HttpRequest)) {
            return; // 如果不是HttpRequest,不进行处理
        }
        // 将HttpObject转换为HttpRequest对象
        request = (HttpRequest) msg;
        // 从HttpRequest对象中获取HttpHeaders
        headers = request.headers();
        // 调用自定义方法来进一步处理请求
        handleRequest(ctx); // ctx是ChannelHandlerContext对象,提供了对Channel的引用
    }

    private void handleRequest(ChannelHandlerContext ctx) throws Exception {
        String uri = request.uri();
        if (uri.equals(FAVICON_ICO)) {
            return;
        }
        HttpMethod method = request.method();
        User user = new User();
        user.setUserName("pengtao");
        user.setDate(new Date());
        user.setMethod(method.name());
        if (method.equals(GET)) {
            handleGetRequest();
        } else if (method.equals(POST)) {
            handlePostRequest(ctx);
        } else {
            throw new UnsupportedOperationException("HTTP method " + method + " is not supported");
        }
        writeResponse(ctx, serializeUser(user));
    }

    private void handleGetRequest() {
        logger.info("get请求....");
        QueryStringDecoder queryDecoder = new QueryStringDecoder(request.uri(), Charsets.UTF_8);
        for (Map.Entry<String, List<String>> param : queryDecoder.parameters().entrySet()) {
            for (String value : param.getValue()) {
                logger.info("{}={}", param.getKey(), value);
            }
        }
    }

    private void handlePostRequest(ChannelHandlerContext ctx) throws Exception {
        logger.info("post请求....");
        dealWithContentType(ctx);
    }

    private void writeResponse(ChannelHandlerContext ctx, byte[] content) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);
        response.content().writeBytes(content);
        response.headers().set(CONTENT_TYPE_HDR, "application/json; charset=UTF-8");
        response.headers().set(CONTENT_LENGTH_HDR, response.content().readableBytes());
        boolean keepAlive = HttpUtil.isKeepAlive(request);
        response.headers().set(CONNECTION_HDR, keepAlive ? KEEP_ALIVE_VAL : null);
        ctx.writeAndFlush(response)
                .addListener(keepAlive ? ChannelFutureListener.CLOSE : ChannelFutureListener.CLOSE_ON_FAILURE);
    }

    private byte[] serializeUser(User user) {
        try {
            return new JSONSerializer().serialize(user);
        } catch (Exception e) {
            logger.error("Error serializing user", e);
            throw e;
        }
    }

    private void dealWithContentType(ChannelHandlerContext ctx) throws Exception {
        String contentType = getContentType();
        if (Objects.isNull(contentType)) return;
        switch (Objects.requireNonNull(contentType)) {
            case "application/json":
                parseJsonRequest(ctx);
                break;
            case "application/x-www-form-urlencoded":
                parseFormRequest(ctx);
                break;
            case "multipart/form-data":
                parseMultipartRequest(ctx);
                break;
            default:
                logger.warn("Unsupported content type: {}", contentType);
                sendError(ctx);
                break;
        }
    }

    private String getContentType() {
        return headers.get(CONTENT_TYPE_HDR) == null ? null : headers.get(CONTENT_TYPE_HDR).toString().split(";")[0].trim();
    }

    private void parseJsonRequest(ChannelHandlerContext ctx) throws Exception {
    }

    private void parseFormRequest(ChannelHandlerContext ctx) throws Exception {

    }

    private void parseMultipartRequest(ChannelHandlerContext ctx) throws Exception {

    }

    private void sendError(ChannelHandlerContext ctx) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.UNSUPPORTED_MEDIA_TYPE);
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }


    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
        logger.error("Exception caught", cause);
        ctx.close();
    }

    @Override
    public void channelReadComplete(ChannelHandlerContext ctx) {
        ctx.flush();
    }
}

HttpServerInitializer

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
package com.example.netty;

import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpServerCodec;
import io.netty.handler.codec.http.HttpServerExpectContinueHandler;


public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {

@Override
public void initChannel(SocketChannel ch) {
ChannelPipeline p = ch.pipeline();
/**
* HttpServerCodec是Netty提供的HTTP请求和响应编解码器,
* 它将处理HTTP请求和响应的编码和解码。
*/
p.addLast(new HttpServerCodec());

/**
* HttpObjectAggregator是Netty提供的处理器,用于聚合HTTP片段,
* 例如将HttpRequest和随后的HttpContent聚合成一个FullHttpRequest,
* 便于后续处理。这里设置的聚合字节大小为1MB。
*/
p.addLast(new HttpObjectAggregator(1024 * 1024));

/**
* HttpServerExpectContinueHandler用于处理HTTP 'Expect: 100-continue' 头部,
* 当客户端发送的请求包含这个头部时,服务器将先发送100 Continue响应,
* 然后客户端再发送请求体。这可以用于支持需要分块传输的大型请求体。
*/
p.addLast(new HttpServerExpectContinueHandler());

/**
* HttpHelloWorldServerHandler是我们自定义的HTTP请求处理器,
* 它将根据接收到的HTTP请求生成响应。
*/
p.addLast(new HttpServerHandler());
}

源码demo:https://github.com/Breeze1203/netty-learning/tree/master/netty-http

一、案例要求

1、 编写一个NIO群聊系统,实现服务器端和客户端之间的数据简单通讯(非阻塞);
2、 实现多人群聊;
3、 服务器端:可以检测用户上线,离线,并实现消息转发功能;
4、 客户端:通过channel可以无阻塞发送消息给其它所有用户,同时可以接收其它用户发送的消息(由服务器转发得到);
5、 目的:进一步理解NIO非阻塞网络编程机制;

二、代码演示

server
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
package org.example.chat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Scanner;

public class GroupChatServer {
// 定义属性
private Selector selector;
private ServerSocketChannel listenChannel;
private static final int PORT = 6667;

// 构造器,完成初始化操作
public GroupChatServer(){
try {

// 得到选择器
selector = Selector.open();
// 初始化 ServerSocketChannel
listenChannel = ServerSocketChannel.open();
// 绑定端口
listenChannel.socket().bind(new InetSocketAddress(PORT));
// 设置非阻塞模式
listenChannel.configureBlocking(false);
// 将 listenChannel 注册到 selector
listenChannel.register(selector, SelectionKey.OP_ACCEPT);

}catch (IOException e){
e.printStackTrace();
}
}

// 监听
public void listen(){
try {
// 循环处理
while (true){
int count = selector.select();
if(count > 0){ // 有事件处理

// 遍历得到的 selectionKey 集合
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
// 取出 selectionKey
SelectionKey key = iterator.next();

// 监听到 accept 事件
if(key.isAcceptable()){
SocketChannel sc = listenChannel.accept();
sc.configureBlocking(false);
// 将 sc 注册到 selector 上
sc.register(selector,SelectionKey.OP_READ);
// 提示
System.out.println(sc.getRemoteAddress() + " 上线");
}

// 监听到 read 事件,即通道是可读的状态
if(key.isReadable()){
// 处理读(专门写方法...)
readData(key);
}

// 删除当前 key,防止重复处理
iterator.remove();
}
}else{
System.out.println("等待....");
}
}
}catch (IOException e){
e.printStackTrace();
}finally {

}
}

// 读取客户端消息
private void readData(SelectionKey key){
// 定义一个 SocketChannel
SocketChannel channel = null;
try {
// 获取到关联的 channel
channel = (SocketChannel)key.channel();
// 创建 buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
int count = channel.read(buffer);
// 根据 count 的值做处理
if(count > 0){
buffer.flip(); // 切换为读模式
byte[] bytes = new byte[buffer.limit()];
buffer.get(bytes); // 读取数据到byte数组
String msg = new String(bytes, 0, count, "UTF-8"); // 指定字符集解码
System.out.println("from client:" + msg);
// 向其它的客户端转发消息(去掉自己),专门写一个方法来处理
sendInfoToOtherClients(msg,channel);
}
}catch (IOException e){
if(channel != null){
try {
System.out.println(channel.getRemoteAddress() + " 离线了");
// 取消注册
key.cancel();
// 关闭通道
channel.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
}

// 转发消息给其它的客户端(通道)
private void sendInfoToOtherClients(String msg,SocketChannel self) throws IOException {

System.out.println("服务器转发消息中...");
// 遍历所有注册到 selector 上的 socketChannel,并排除 self
for(SelectionKey key:selector.keys()){
// 通过 key 取出对应的 SocketChannel
SelectableChannel channel = key.channel();
// 排除自己,TODO
if(channel instanceof SocketChannel && channel != self){
// 转型
SocketChannel dest = (SocketChannel) channel;
// 将 msg 存储到 buffer
ByteBuffer buffer = ByteBuffer.wrap(msg.getBytes());
// 将 buffer 的数据写入通道
dest.write(buffer);
}
}

}

public static void main(String[] args) {

// 创建服务器对象
GroupChatServer chatServer = new GroupChatServer();
chatServer.listen();
}
}
client
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
package org.example.chat;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class GroupChatClient {

// 定义相关的属性
private final String HOST = "127.0.0.1";
private final int PORT = 6667;
private Selector selector;
private SocketChannel socketChannel;
private String username;

// 构造器,完成初始化操作
public GroupChatClient() throws IOException {
selector = Selector.open();
// 连接服务器
socketChannel = SocketChannel.open(new InetSocketAddress(HOST,PORT));
// 设置非阻塞
socketChannel.configureBlocking(false);
// 将 channel 注册到 selector
socketChannel.register(selector, SelectionKey.OP_READ);
// 得到 username
username = socketChannel.getLocalAddress().toString().substring(1);
System.out.println(username + " is ok...");
}

// 向服务器发送消息
public void sendInfo(String info){
info = username + " 说:" + info;

try {
socketChannel.write(ByteBuffer.wrap(info.getBytes()));
}catch (IOException e){
e.printStackTrace();
}
}

// 读取从服务器端回复的消息
public void readInfo(){
try {
int readChannels = selector.select();
if(readChannels > 0){ // 有可以用的通道
Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
while (iterator.hasNext()){
SelectionKey key = iterator.next();
if(key.isReadable()){
// 得到相关的通道
SocketChannel sc = (SocketChannel)key.channel();
// 得到一个buffer
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取
sc.read(buffer);
// 把读到的缓冲区的数据转成字符串
String msg = new String(buffer.array());
System.out.println(msg.trim());
}

iterator.remove(); // 删除当前的 selectionKey,防止重复操作
}
}else{
//System.out.println("没有可以用的通道...");

}
}catch (IOException e){
e.printStackTrace();
}
}

public static void main(String[] args) throws IOException {
// 启动客户端
GroupChatClient chatClient = new GroupChatClient();

// 启动一个线程,每隔3秒,读取从服务器端发送的数据
new Thread(){
public void run(){
while (true){
chatClient.readInfo();
try {
Thread.currentThread().sleep(3000);
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}.start();

// 客户端发送数据给服务器端
Scanner scanner = new Scanner(System.in);
while (scanner.hasNextLine()){
String s = scanner.next();
chatClient.sendInfo(s);
}
}

}
效果图
0%