-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
473 lines (249 loc) · 751 KB
/
atom.xml
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
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>JovehawkinG✨</title>
<link href="https://jovehawking.fun/atom.xml" rel="self"/>
<link href="https://jovehawking.fun/"/>
<updated>2024-10-20T10:50:21.244Z</updated>
<id>https://jovehawking.fun/</id>
<author>
<name>JovehawkinG✨</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>股票-均线</title>
<link href="https://jovehawking.fun/posts/1de64287.html"/>
<id>https://jovehawking.fun/posts/1de64287.html</id>
<published>2024-10-20T09:15:09.000Z</published>
<updated>2024-10-20T10:50:21.244Z</updated>
<content type="html"><![CDATA[<h1>均线</h1><p><img src="../image/post/image-20241020171527113.png" alt="image-20241020171527113"></p><p><img src="../image/post/image-20241020172258283.png" alt="image-20241020172258283"></p><p><img src="../image/post/image-20241020172307798.png" alt="image-20241020172307798"></p><hr><h1>缺口</h1><p><img src="../image/post/image-20241020174757070.png" alt="image-20241020174757070"></p><p><img src="../image/post/image-20241020174930138.png" alt="image-20241020174930138"></p><h1>RSI(短线参考)</h1><p><img src="../image/post/image-20241020180057285.png" alt="image-20241020180057285"></p><p><img src="../image/post/image-20241020180540378.png" alt="image-20241020180540378"></p><h1>MACD(中长期参考)</h1><p><img src="../image/post/image-20241020183024605.png" alt="image-20241020183024605"></p><p><img src="../image/post/image-20241020182801641.png" alt="image-20241020182801641"></p><h1>KDJ(随机震荡;短期参考;参考难度大)</h1><p><img src="../image/post/image-20241020183404831.png" alt="image-20241020183404831"></p><p><img src="../image/post/image-20241020183602450.png" alt="image-20241020183602450"></p><p><img src="../image/post/image-20241020183831350.png" alt="image-20241020183831350"></p><ul><li>以50为分界线</li></ul><h1>筹码(缺点多,参考)</h1><p><img src="../image/post/image-20241020184036651.png" alt="image-20241020184036651"></p><p><img src="../image/post/image-20241020184717591.png" alt="image-20241020184717591"></p><p><img src="../image/post/image-20241020184901585.png" alt="image-20241020184901585"></p><p><img src="../image/post/image-20241020185012649.png" alt="image-20241020185012649"></p>]]></content>
<summary type="html">股票均线解析</summary>
<category term="股票inG" scheme="https://jovehawking.fun/categories/%E8%82%A1%E7%A5%A8inG/"/>
<category term="股票" scheme="https://jovehawking.fun/tags/%E8%82%A1%E7%A5%A8/"/>
<category term="均线" scheme="https://jovehawking.fun/tags/%E5%9D%87%E7%BA%BF/"/>
</entry>
<entry>
<title>股票-K线图</title>
<link href="https://jovehawking.fun/posts/f137d402.html"/>
<id>https://jovehawking.fun/posts/f137d402.html</id>
<published>2024-10-20T06:56:09.000Z</published>
<updated>2024-10-20T09:14:22.637Z</updated>
<content type="html"><![CDATA[<h1>K线图</h1><hr><ul><li>第二天实体包住第一天全部</li></ul><p><img src="../image/post/image-20241020152346634.png" alt="image-20241020152346634"></p><p><img src="../image/post/image-20241020153050199.png" alt="image-20241020153050199"></p><hr><ul><li>第一天实体包住第二天全部</li></ul><p><img src="../image/post/image-20241020153926999.png" alt="image-20241020153926999"></p><p><img src="../image/post/image-20241020154338927.png" alt="image-20241020154338927"></p><p><img src="../image/post/image-20241020154614954.png" alt="image-20241020154614954"></p><p><img src="../image/post/image-20241020154722075.png" alt="image-20241020154722075"></p><p><img src="../image/post/image-20241020154941090.png" alt="image-20241020154941090"></p><hr><p><img src="../image/post/image-20241020170415559.png" alt="image-20241020170415559"></p><p><img src="../image/post/image-20241020170951221.png" alt="image-20241020170951221"></p>]]></content>
<summary type="html">股票K线图解析</summary>
<category term="股票inG" scheme="https://jovehawking.fun/categories/%E8%82%A1%E7%A5%A8inG/"/>
<category term="股票" scheme="https://jovehawking.fun/tags/%E8%82%A1%E7%A5%A8/"/>
<category term="K线图" scheme="https://jovehawking.fun/tags/K%E7%BA%BF%E5%9B%BE/"/>
</entry>
<entry>
<title>软考-要点-项目管理</title>
<link href="https://jovehawking.fun/posts/52a5e4b2.html"/>
<id>https://jovehawking.fun/posts/52a5e4b2.html</id>
<published>2024-10-09T12:10:22.000Z</published>
<updated>2024-10-09T12:11:56.516Z</updated>
<content type="html"><![CDATA[<h1>项目管理</h1><h2 id="进度管理">进度管理</h2><p><strong>进度管理包括哪些过程</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">1. 活动定义:确定完成项目的各个可交付成果所必须进行的各项具体活动</span><br><span class="line">2. 活动排序</span><br><span class="line">3. 活动资源估计</span><br><span class="line">4. 活动历时估算</span><br><span class="line">5. 制定进度计划</span><br><span class="line">6. 进度控制</span><br></pre></td></tr></table></figure><h2 id="盈亏分析">盈亏分析</h2><p><strong>正常情况、盈亏平衡时的公式</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">正常情况:销售额 = 固定成本 + 可变成本 + 税费 + 利润</span><br><span class="line">盈亏平衡:销售额 = 固定成本 + 可变成本 + 税费 + 0</span><br></pre></td></tr></table></figure><h2 id="软件配置管理">软件配置管理</h2><p><strong>产品配置有哪些配置项</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">基线配置项(可交付成果):需求文档、设计文档、源代码、测试用例、运行所需数据等</span><br><span class="line">非基线配置项:各类计划、各类报告</span><br><span class="line"></span><br><span class="line">产品配置就是一个产品在其生命周期各个阶段所产生的各种形式和各种版本的集合。</span><br></pre></td></tr></table></figure><p><strong>软件配置管理(SCM)的核心内容</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">版本控制和变更控制</span><br></pre></td></tr></table></figure><h2 id="软件质量管理">软件质量管理</h2><p><strong>软件质量保证的主要内容</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">软件质量保证(SQA):</span><br><span class="line">1. SQA审计和评审</span><br><span class="line">2. SQA报告</span><br><span class="line">3. 处理不符合问题</span><br></pre></td></tr></table></figure><p><strong>软件能力成熟度模型集成CMMI等级</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">成熟度由低到高</span><br><span class="line">1. 初始级:随意且混乱、组织成功依赖于个人能力</span><br><span class="line">2. 已管理级:项目级可重复【建立了项目级的控制过程】</span><br><span class="line">3. 已定义级:组织级,文档化、标准化</span><br><span class="line">4. 定量管理级:量化式管理【过程性能可预测】</span><br><span class="line">5. 优化级:持续优化</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">整理软考思维导图重难点-项目管理</summary>
<category term="软考inG" scheme="https://jovehawking.fun/categories/%E8%BD%AF%E8%80%83inG/"/>
<category term="软考" scheme="https://jovehawking.fun/tags/%E8%BD%AF%E8%80%83/"/>
<category term="思维导图知识点" scheme="https://jovehawking.fun/tags/%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%E7%9F%A5%E8%AF%86%E7%82%B9/"/>
</entry>
<entry>
<title>软考-要点-软件工程</title>
<link href="https://jovehawking.fun/posts/15e280ad.html"/>
<id>https://jovehawking.fun/posts/15e280ad.html</id>
<published>2024-10-08T11:41:22.000Z</published>
<updated>2024-10-09T00:03:10.220Z</updated>
<content type="html"><![CDATA[<h1>软件工程</h1><h2 id="开发模型">开发模型</h2><p><strong>软件工程有哪些开发模型</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">1. 瀑布模型【*】</span><br><span class="line">- 严格区分阶段,每个阶段因果关系紧密相连</span><br><span class="line">- 只适合需求明确的项目</span><br><span class="line">2. 原型模型【*】</span><br><span class="line">- 原型模型两个阶段:原型开发阶段和目标软件开发阶段</span><br><span class="line">- 适合需求不明确的项目</span><br><span class="line">- 分为抛弃型原型(快速原型)和演化性原型</span><br><span class="line">3. V模型</span><br><span class="line">- 测试贯穿于始终</span><br><span class="line">- 需求分析对应验收测试与系统测试;概要设计对应集成测试,详细设计对应单元测试。</span><br><span class="line">4. 迭代与增量</span><br><span class="line">5. 螺旋模型【*】</span><br><span class="line">- 以快速原型为基础 + 瀑布模型</span><br><span class="line">- 考虑了风险问题</span><br><span class="line">6. 基于构件的开发模型(CBSD)</span><br><span class="line">- 易拓展、易重用、降低成本、安排任务更灵活</span><br><span class="line">- 要求架构师能力高</span><br><span class="line">7. 基于构件的软件工程(CBSE)</span><br><span class="line">- 体现了购买而不是重新构造的哲学</span><br><span class="line">8. 快速应用开发(RAD)</span><br><span class="line">- SDLC(瀑布) + CBSD(基于构件)</span><br><span class="line">9. 软件统一过程(UP/RUP)【*】</span><br><span class="line">10. 敏捷模型【*】</span><br></pre></td></tr></table></figure><p><strong>软件工程有哪些过程模型</strong>(新版)</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 瀑布模型</span><br><span class="line">2. 原型模型</span><br><span class="line">3. 软件统一过程(UP/RUP)</span><br><span class="line">4. 敏捷模型</span><br></pre></td></tr></table></figure><p><strong>CBSE构件所应该具备的特征</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1. 可组装性:所有外部交互必须通过公开定义的接口进行</span><br><span class="line">2. 可部署性:构件总是二进制形式的,能作为一个独立的实体在平台上运行</span><br><span class="line">3. 文档化:用户根据文档来判断构件是否满足需求</span><br><span class="line">4. 独立性:可以在无其他特殊构件的情况下进行组装和部署</span><br><span class="line">5. 标准化:符合某种标准化的构件模型</span><br></pre></td></tr></table></figure><p><strong>CBSE构件的组装顺序</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 顺序组装:按顺序调用已存在的构件,可以用两个已经存在的构件来创造一个新的构件</span><br><span class="line">2. 层次组装:被调用构件“提供”的接口必须和调用构件的“请求”接口兼容</span><br><span class="line">3. 叠加组装:多个构件合并形成新的构件,新构件整合原构件的功能,对外提供新的接口</span><br></pre></td></tr></table></figure><p><strong>RUP模型的几个阶段</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">初始->细化->构造->移交</span><br></pre></td></tr></table></figure><p><img src="../image/post/image-20241008201333612.png" alt="image-20241008201333612"></p><p><strong>RUP的4+1视图模型</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">逻辑、实现、用例、进程、部署</span><br><span class="line">P50</span><br></pre></td></tr></table></figure><p><strong>净室软件工程(CSE)的技术手段</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 统计过程控制下的增量式开发:控制迭代</span><br><span class="line">2. 基于函数的规范和设计:盒子结构</span><br><span class="line">3. 正确性验证:净室软件工程的核心</span><br><span class="line">4. 统计测试和软件认证:使用统计学原理,总体太大时采用抽样</span><br></pre></td></tr></table></figure><p><strong>净室软件工程(CSE)的缺点</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 太过理论化,正确性验证的步骤困难且耗时</span><br><span class="line">2. 不进行传统的模块测试</span><br><span class="line">3. 带有传统软件工程的弊端</span><br></pre></td></tr></table></figure><h2 id="需求工程">需求工程</h2><p><strong>需求工程有几个阶段</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">1. 需求获取</span><br><span class="line">2. 需求分析</span><br><span class="line">3. 形成需求规格(需求文档化)【SRS】</span><br><span class="line">4. 需求的确认与验证【形成需求基线】</span><br><span class="line">5. 需求管理【变更控制、版本控制、需求跟踪、需求状态跟踪】</span><br><span class="line"></span><br><span class="line">注:1-4 也叫需求开发。5需求管理是对需求基线进行管理</span><br></pre></td></tr></table></figure><p><strong>需求的分类</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">分层维度:</span><br><span class="line">1. 业务需求</span><br><span class="line">2. 用户需求</span><br><span class="line">3. 功能需求</span><br><span class="line">QFD(项目管理维度):</span><br><span class="line">1. 基本需求</span><br><span class="line">2. 期望需求</span><br><span class="line">3. 兴奋需求</span><br></pre></td></tr></table></figure><p><strong>需求获取方法</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">用户面谈:成本高,有领域知识</span><br><span class="line">联合需求计划(JRP):交互好,各方参与</span><br><span class="line">问卷调查:用户多,成本低</span><br><span class="line">现场观察:针对复杂流程</span><br><span class="line">原型化方法:解决早期需求不明确</span><br><span class="line">头脑风暴:新业务,发散思维</span><br></pre></td></tr></table></figure><p><strong>需求分析(系统分析/设计)的方法</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 结构化方法</span><br><span class="line">2. 面向对象方法</span><br><span class="line">3. 其他方法(软件重用)</span><br><span class="line">4. 逆向工程</span><br></pre></td></tr></table></figure><p><strong>结构化分析方法使用的手段</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">数据流图DFD</span><br><span class="line">状态转换图</span><br><span class="line">ER图</span><br></pre></td></tr></table></figure><p><img src="../image/post/image-20241008203421467.png" alt="image-20241008203421467"></p><p><img src="../image/post/image-20241008203428644.png" alt="image-20241008203428644"></p><p><img src="../image/post/image-20241008203438039.png" alt="image-20241008203438039"></p><p><strong>面向对象方法使用的手段</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UML</span><br></pre></td></tr></table></figure><p><strong>UML的4+1视图</strong></p><p><img src="../image/post/image-20241008203907964.png" alt="image-20241008203907964"></p><p><strong>需求定义(需求文档化所用到的方法)</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">1. 严格定义法</span><br><span class="line">- 所有需求都能被预先定义</span><br><span class="line">- 开发人员和用户之间能够准确而清晰地交流</span><br><span class="line">- 采用文字/图形充分体现最终系统</span><br><span class="line">2. 原型法</span><br><span class="line">- 开发前需求不明确</span><br><span class="line">- 交流困难</span><br><span class="line">- 需要实际的、可供用户参与的系统模型</span><br><span class="line">- 有合适的系统开发环境</span><br></pre></td></tr></table></figure><h2 id="软件设计">软件设计</h2><p><strong>软件设计分为哪几类</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 软件系统建模</span><br><span class="line">2. 结构化设计</span><br><span class="line">3. 面向对象设计</span><br><span class="line">4. 界面设计</span><br></pre></td></tr></table></figure><p><strong>有哪几种软件系统建模方法</strong></p><p><img src="../image/post/image-20241008204917703.png" alt="image-20241008204917703"></p><p><strong>结构化设计的分类和原则</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">分类:</span><br><span class="line">1. 概要设计【外部设计】:功能需求分配给软件模块,确定每个模块的功能和调用关系,形成模块结构图</span><br><span class="line">2. 详细设计【内部设计】:为每个具体任务选择适当的技术手段和处理方法</span><br><span class="line"></span><br><span class="line">原则:</span><br><span class="line">1. 模块独立性原则(高内聚,低耦合)</span><br><span class="line">2. 保持模块大小适中</span><br><span class="line">3. 多扇入,少扇出</span><br><span class="line">4. 深度和宽度均不宜过高</span><br></pre></td></tr></table></figure><p><strong>结构化设计中模块的四个要素</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 输入和输出</span><br><span class="line">2. 处理功能</span><br><span class="line">3. 内部数据</span><br><span class="line">4. 程序代码</span><br></pre></td></tr></table></figure><p><strong>结构化设计中内聚、耦合的类型</strong></p><p><img src="../image/post/image-20241008205344398.png" alt="image-20241008205344398"></p><p><img src="../image/post/image-20241008205351767.png" alt="image-20241008205351767"></p><p><strong>面向对象设计的基本过程</strong></p><p><img src="../image/post/image-20241008205448865.png" alt="image-20241008205448865"></p><p><strong>面向对象设计中类的分类</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 边界类:API接口;用户界面;显示屏;二维码;购物车;</span><br><span class="line">2. 控制类:排除法</span><br><span class="line">3. 实体类:学员;课程</span><br></pre></td></tr></table></figure><p><strong>界面设计的法则</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 置于用户控制之下</span><br><span class="line">2. 减少用户的记忆负担</span><br><span class="line">3. 保持界面的一致性</span><br></pre></td></tr></table></figure><h2 id="测试">测试</h2><p><strong>测试的分类(类型)</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">动态测试【计算机运行】</span><br><span class="line">- 白盒</span><br><span class="line">- 黑盒</span><br><span class="line">- 灰盒</span><br><span class="line">静态测试【人工监测和计算机辅助分析】</span><br><span class="line">- 桌前检查</span><br><span class="line">- 代码审查</span><br><span class="line">- 代码走查</span><br></pre></td></tr></table></figure><p><strong>测试的方法</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">白盒测试【结构测试】:主要用于单元测试</span><br><span class="line">- 控制流测试(语句覆盖最弱,路径测试覆盖最强)</span><br><span class="line">- 数据流测试</span><br><span class="line">- 程序变异测试</span><br><span class="line">黑盒测试【功能测试】:主要用于集成、确认、系统测试</span><br><span class="line">- 等价类划分</span><br><span class="line">- 边界值分析</span><br><span class="line">- 错误推测</span><br><span class="line">- 判定表</span><br><span class="line">- 因果图</span><br></pre></td></tr></table></figure><p><strong>测试有哪些阶段</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">V模型</span><br></pre></td></tr></table></figure><p><img src="../image/post/image-20241008210305238.png" alt="image-20241008210305238"></p><h2 id="维护">维护</h2><p><strong>遗留系统演化策略是什么</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">高水平低价值【信息孤岛】:集成</span><br><span class="line">- 遗留系统的技术含量较高,但其业务价值较低,可能只完成某个部门(或子公司)的业务管理。这种系统在各自的局部领域里工作良好,但对于整个企业来说,存在多个这样的系统,不同的系统基于不同的平台、不同的数据模型,形成了一个个信息孤岛,对这种遗留系统的演化策略为集成。</span><br><span class="line"></span><br><span class="line">高水平高价值:改造(包括系统功能增强和数据模型改造)</span><br><span class="line">- 遗留系统具有较高的业务价值,基本上能够满足企业业务运作和决策支持的需要。这种系统可能建成的时间还很短,对这种遗留系统的演化策略为改造。改造包括系统功能的增强和数据模型的改造两个方面。系统功能的增强是指在原有系统的基础上增加新的应用要求,对遗留系统本身不做改变;数据模型的改造是指将遗留系统的旧的数据模型向新的数据模型的转化。</span><br><span class="line"></span><br><span class="line">低水平低价值:淘汰</span><br><span class="line">- 遗留系统的技术含量较低,且具有较低的业务价值。对遗留系统的完全淘汰是企业资源的根本浪费,系统分析师应该善于“变废为宝”,通过对遗留系统功能的理解和借鉴,可以帮助新系统的设计,降低新系统开发的风险。</span><br><span class="line"></span><br><span class="line">低水平高价值:继承</span><br><span class="line">- 遗留系统的技术含量较低,已经满足企业运作的功能或性能要求,但具有较高的商业价值,目前企业的业务尚紧密依赖该系统。对这种遗留系统的演化策略为继承。在开发新系统时,需要完全兼容遗留系统的功能模型和数据模型。为了保证业务的连续性,新老系统必须并行运行一段时间,再逐渐切换到新系统上运行。</span><br><span class="line"></span><br><span class="line">开发新系统时,需要完全兼容遗留系统的功能模型和数据模型</span><br></pre></td></tr></table></figure><p><strong>软件维护有哪些类型</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 正确性维护【修BUG】:识别和纠正软件的错误/缺陷</span><br><span class="line">2. 适应性维护【应变】:应用软件适应环境变化而进行的修改</span><br><span class="line">3. 完善性维护【新需求】:扩充功能和改善性能而进行的修改</span><br><span class="line">4. 预防性维护【针对未来】:为了适应未来的软硬件环境的变化</span><br></pre></td></tr></table></figure><p><strong>影响可维护性的因素有哪些</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1. 可理解性</span><br><span class="line">2. 可修改性</span><br><span class="line">3. 可测试性</span><br><span class="line">4. 可靠性</span><br><span class="line">5. 可移植性</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">整理软考思维导图重难点-软件工程</summary>
<category term="软考inG" scheme="https://jovehawking.fun/categories/%E8%BD%AF%E8%80%83inG/"/>
<category term="软考" scheme="https://jovehawking.fun/tags/%E8%BD%AF%E8%80%83/"/>
<category term="思维导图知识点" scheme="https://jovehawking.fun/tags/%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%E7%9F%A5%E8%AF%86%E7%82%B9/"/>
</entry>
<entry>
<title>软考-要点-系统工程和信息系统基础</title>
<link href="https://jovehawking.fun/posts/55148790.html"/>
<id>https://jovehawking.fun/posts/55148790.html</id>
<published>2024-10-07T01:41:22.000Z</published>
<updated>2024-10-07T08:57:49.769Z</updated>
<content type="html"><![CDATA[<h1>系统工程与信息系统基础</h1><h2 id="系统工程">系统工程</h2><p><strong>系统工程的概念和特点</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">1. 系统工程利用计算机作为工具,对系统的结构、元素、信息和反馈等进行分析,以达到最优规划、最优设计、最优管理和最优控制的目的</span><br><span class="line">2. 从整体出发、从系统观念出发,以求整体最优</span><br></pre></td></tr></table></figure><p><strong>系统工程有哪些方法</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">1. 霍尔三维结构</span><br><span class="line">2. 切克兰德方法</span><br><span class="line">3. 并行工程方法</span><br><span class="line">4. 综合集成方法</span><br><span class="line">5. WSR系统方法</span><br></pre></td></tr></table></figure><p><strong>霍尔三维结构是哪三维?各维度特点?</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line">1. 逻辑维</span><br><span class="line">2. 时间维</span><br><span class="line">3. 知识维</span><br><span class="line"></span><br><span class="line">逻辑维:会定义和分析系统的要素、子系统、过程、功能以及相互之间如何协作</span><br><span class="line">- 明确问题</span><br><span class="line">- 确定目标(建立价值体系或评价体系)</span><br><span class="line">- 系统综合</span><br><span class="line">- 系统分析</span><br><span class="line">- 优化(解决方案的优化选择)</span><br><span class="line">- 系统决策</span><br><span class="line">- 实施计划</span><br><span class="line"></span><br><span class="line">时间维:这个维度强调过程、阶段、状态、趋势和系统的生命周期。会使用时序图、甘特图或者PERT图等工具来规划和监控项目进度。</span><br><span class="line">- 规划阶段(调研,谋求活动的规划与战略)</span><br><span class="line">- 拟定方案(提出具体的计划方案)</span><br><span class="line">- 研制阶段(完成研制方案及生产计划)</span><br><span class="line">- 生产阶段(生产零部件及提出安装计划)</span><br><span class="line">- 安装阶段(安装完毕,完成系统的运行计划)</span><br><span class="line">- 运行阶段(系统按照预期的用途开展服务)</span><br><span class="line">- 更新阶段(改进原有系统、或消亡原有系统)</span><br><span class="line"></span><br><span class="line">知识维:知识维关注于“我们如何了解和控制系统”,即所需的技术、原则、理论、数据和经验。</span><br></pre></td></tr></table></figure><p><strong>切克兰德方法的过程</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">1. 认识问题</span><br><span class="line">实地考察公园、与公园管理员、游客、维护人员等进行交流,收集他们对现状的看 法,如公园保洁不足、娱乐设施老化等问题。</span><br><span class="line">2. 根底定义</span><br><span class="line">对于每个关键问题制定根底定义,比如,“公园维护系统”可能有这样的根底定 义:“一个旨在提供清洁、安全、愉悦环境的服务系统。”</span><br><span class="line">3. 建立概念模型</span><br><span class="line">依据“公园维护系统”的根底定义,构建一个包括垃圾回收、设施检查、游客反馈处理等活动的概念模型。</span><br><span class="line">4. 比较与探寻</span><br><span class="line">把概念模型与实际的公园运营情况相比较,识别出哪些是按照概念模型运作得好的地方,哪些是存在差异的地方,并探讨为什么会有这种差异。</span><br><span class="line">5. 选择</span><br><span class="line">决定一系列改变,例如增加保洁人员、更新娱乐设施、创建游客反馈系统等,这些都是在现实条件下可行且期望达到的改变。</span><br><span class="line">6. 设计与实施</span><br><span class="line">制定具体行动计划来实施这些改变,比如招聘更多的保洁人员、购置新的设施或开发一个在线反馈平台。</span><br><span class="line">7. 评估与反馈</span><br><span class="line">在实施后,持续对改变效果进行监测和评估,收集游客、员工的反馈,并根据反馈结果调整行动计划。</span><br></pre></td></tr></table></figure><p><strong>并行工程的特点</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">1. 产品设计开发期间,最快速度按质完成</span><br><span class="line">2. 各项工作期间问题协调解决</span><br><span class="line">3. 适当的信息系统工具</span><br></pre></td></tr></table></figure><p><strong>综合集成方法的原则</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 整体论原则</span><br><span class="line">2. 相互联系原则</span><br><span class="line">3. 有序性原则</span><br><span class="line">4. 动态原则</span><br></pre></td></tr></table></figure><p><strong>WSR系统方法的三个关键维度</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">技术层面(物理)</span><br><span class="line">管理层面(事理)</span><br><span class="line">人文层面(人理)</span><br></pre></td></tr></table></figure><p><strong>系统工程生命周期阶段及方法</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">探索性研究</span><br><span class="line">概念阶段</span><br><span class="line">开发阶段</span><br><span class="line">生产阶段</span><br><span class="line">使用阶段</span><br><span class="line">保障阶段</span><br><span class="line">退役阶段</span><br></pre></td></tr></table></figure><h2 id="信息系统基础">信息系统基础</h2><p><strong>信息系统的生命周期是什么</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">1. 产生</span><br><span class="line">2. 开发</span><br><span class="line"> - 总体规划</span><br><span class="line"> - 系统分析</span><br><span class="line"> - 系统设计</span><br><span class="line"> - 系统实施</span><br><span class="line"> - 系统验收</span><br><span class="line">3. 运行</span><br><span class="line">4. 消亡</span><br></pre></td></tr></table></figure><p><strong>信息系统的建设原则</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">1. 高层管理人员介入</span><br><span class="line">2. 用户参与开发</span><br><span class="line">3. 自顶向下</span><br><span class="line">4. 工程化</span><br></pre></td></tr></table></figure><p><strong>信息系统开发方法</strong>(BOOK)</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">// 以书本为主,一下内容参考 //</span><br><span class="line"></span><br><span class="line">1. 结构化方法</span><br><span class="line">特点:</span><br><span class="line">- 自顶向下,逐步分解求精</span><br><span class="line">- 开发目标清晰化</span><br><span class="line">- 工作阶段程式化</span><br><span class="line">- 开发文档规范化</span><br><span class="line">- 设计方法结构化</span><br><span class="line">- 应变能力差</span><br><span class="line">2. 面向对象方法</span><br><span class="line">特点:</span><br><span class="line">- 自底向上</span><br><span class="line">- 阶段界线不明</span><br><span class="line">- 更好的应变、更好的复用</span><br><span class="line">- 符合人们的思维习惯</span><br><span class="line">3. 面向服务方法</span><br><span class="line">特点:</span><br><span class="line">- 粗粒度、松耦合</span><br><span class="line">- 标准化和构件化</span><br><span class="line">- 抽象级别:操作->服务->业务流程</span><br><span class="line">4. 形式化方法</span><br><span class="line">- 应用数学和逻辑的严格技术,以确保软件或系统设计的正确性和一致性。</span><br><span class="line">5. 统一过程方法(RUP)</span><br><span class="line">特点:</span><br><span class="line">- 是一个迭代和增量的软件开发框架,旨在通过结构化的阶段和多个迭代来逐步细化和完善软件产品。</span><br><span class="line">- 以架构为中心,用例驱动,迭代与增量</span><br><span class="line">- 四个阶段:初始、细化、构造、交付</span><br><span class="line">6. 敏捷方法</span><br><span class="line">- 一组灵活、迭代的软件开发实践,旨在快速响应需求变化并促进跨职能团队之间的密切协作。</span><br><span class="line">7. 基于架构的开发方法(ABSD)</span><br><span class="line">- 以软件系统的结构设计为核心,围绕创建、维护和演化系统架构来组织开发活动的方法。</span><br></pre></td></tr></table></figure><p><strong>信息系统的分类</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">1. 业务处理系统【TPS】</span><br><span class="line">- 又称为电子数据处理系统</span><br><span class="line">- 在服务于组织管理层次中最底层,最基础的信息系统</span><br><span class="line">- 包含五个活动:数据输入、业务处理、文件和数据库处理、文件和报告产生、查询处理活动</span><br><span class="line">2. 信息管理系统【MIS】</span><br><span class="line">3. 决策支持系统【DSS】</span><br><span class="line">4. 专家系统【ES】</span><br><span class="line">5. 办公自动化系统【OA】</span><br><span class="line">6. 企业资源计划【ERP】</span><br></pre></td></tr></table></figure><h2 id="企业信息化">企业信息化</h2><p><strong>企业信息化(组织信息化)的目的、需求是什么</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">目的:提高企业的竞争力</span><br><span class="line">需求:</span><br><span class="line">1. 战略需求:提升组织的竞争能力</span><br><span class="line">2. 运作需求:实现信息化战略目标、运作策略、人才培养的需要</span><br><span class="line">3. 技术需求:信息技术层面上对系统的完善、升级、集成</span><br></pre></td></tr></table></figure><p><strong>企业信息化有哪些方法</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">1. 业务流程重构方法:彻底的、根本性的重新设计流程</span><br><span class="line">2. 核心业务应用方法:围绕核心业务推动信息化</span><br><span class="line">3. 信息系统建设方法:建设信息系统作为企业信息化的重点和关键</span><br><span class="line">4. 主题数据库方法:建立面向企业核心业务的数据库,消除信息孤岛</span><br><span class="line">5. 资源管理方法:切入点是为了企业资源管理提供强大的工具</span><br><span class="line">6. 人力资本投资方法:把一部分企业的优秀员工看作是一种资本</span><br></pre></td></tr></table></figure><p><strong>企业信息化体系全览图</strong></p><p><img src="../image/post/image-20241007111459421.png" alt="image-20241007111459421"></p><p><strong>企业信息化模型有哪些</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">1. 企业资源计划ERP</span><br><span class="line">- 进化流程:物料需求管理->制造资源计划(核心是物流)->企业资源计划(重心转移到财务上)</span><br><span class="line">- 主要功能模块:财会管理;物流管理;生产控制管理;人力资源管理</span><br><span class="line">2. 客户关系管理CRM</span><br><span class="line">- 目的:提高收入;核心思想:以客户为中心</span><br><span class="line">3. 供应链管理SCM</span><br><span class="line">- 强强联合,打通企业间的”信息孤岛“</span><br><span class="line">- 信息化的“三流”。信息流、资金流、物流</span><br><span class="line">4. 商业智能BI</span><br><span class="line">5. 数据湖</span><br><span class="line">6. 业务流程重组BPR和业务流程管理BPM</span><br><span class="line">- 前者是颠覆原有流程、后者循环改进</span><br></pre></td></tr></table></figure><p><strong>商业智能BI和数据湖是什么,二者有什么区别</strong>(BOOK)</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">// 以书本为主,一下内容参考 //</span><br><span class="line"></span><br><span class="line">1. 商业智能:用于决策分析,使用OLAP在线分析处理</span><br><span class="line">- 应用:数据仓库和数据挖掘</span><br><span class="line"></span><br><span class="line">2. 数据湖:存储企业的各种各样原始数据的大型仓库,其中的数据可供存取、处理、分析和传输。</span><br><span class="line">- 数据分类:结构化数据(表格数据)、非结构化数据(图片、视频、音频、文档)</span><br><span class="line"></span><br><span class="line">二者区别:</span><br><span class="line">数据仓库:仅支持数据分析处理。</span><br><span class="line">数据湖:即支持数据分析处理,也支持事务处理。</span><br><span class="line">具体见下图:</span><br></pre></td></tr></table></figure><p><img src="../image/post/image-20241007164314099.png" alt="image-20241007164314099"></p><h2 id="信息系统战略规划">信息系统战略规划</h2><p><strong>信息系统战略规划和企业信息化规划的区别</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">企业信息化规划:涉及多个领域的融合,它是企业战略、管理规划、业务流程重组等内容的综合规划活动。其中,企业战略规划是评价环境和企业现状,进而选择和确定企业的总体和长远目标,制定和抉择实现目标的行动方案。</span><br><span class="line"></span><br><span class="line">信息系统战略规划:关注如何通过信息系统来支撑业务流程的运作,进而实现企业的关键业务目标。</span><br></pre></td></tr></table></figure><p><strong>信息系统战略规划的三个阶段</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">第一阶段:以数据处理为核心围绕职能部门需求</span><br><span class="line">第二阶段:以企业内部MIS为核心围绕企业整体需求</span><br><span class="line">第三阶段:综合考虑企业内外环境以集成为核心,围绕企业的战略需求</span><br></pre></td></tr></table></figure><p><strong>信息系统战略规划每个阶段的方法</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">第一阶段:</span><br><span class="line">- 关键成功因素法CSF</span><br><span class="line">- 战略集合转化法SST</span><br><span class="line">- 企业系统规划法BSP</span><br><span class="line">第二阶段:</span><br><span class="line">- 战略数据规划法SDP:主题数据库</span><br><span class="line">- 信息工程法IE</span><br><span class="line">- 战略栅格法SG</span><br><span class="line">第三阶段:</span><br><span class="line">- 价值链分析VCA</span><br><span class="line">- 战略一致性模型</span><br><span class="line"></span><br></pre></td></tr></table></figure><h2 id="企业应用集成EAI">企业应用集成EAI</h2><p><strong>什么是企业应用集成,以及作用</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">企业各系统互联互通,就是企业应用集成。</span><br><span class="line">作用:传统企业系统未互联互通,存在信息孤岛,这种架构也被称为烟囱架构。企业应用集成就是用于消除信息孤岛。</span><br></pre></td></tr></table></figure><p><strong>有哪些企业集成方式</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">业界没有一个统一的标准</span><br><span class="line">普遍:</span><br><span class="line">1. 表示集成(界面集成)</span><br><span class="line">2. 数据集成</span><br><span class="line">3. 控制集成(应用集成,API集成)</span><br><span class="line">4. 业务流程集成(过程集成,B2B)</span><br><span class="line">5. 门户集成</span><br></pre></td></tr></table></figure><p><strong>各集成方式对比</strong></p><p><img src="../image/post/image-20241007165425096.png" alt="image-20241007165425096"></p><p><strong>门户集成的分类</strong></p><p><img src="../image/post/image-20241007165531356.png" alt="image-20241007165531356"></p><h2 id="信息系统新技术">信息系统新技术</h2><p><strong>有哪些信息系统的新技术</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">1. 数字化</span><br><span class="line">2. 智能制造</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">整理软考思维导图重难点-系统工程和信息系统基础</summary>
<category term="软考inG" scheme="https://jovehawking.fun/categories/%E8%BD%AF%E8%80%83inG/"/>
<category term="软考" scheme="https://jovehawking.fun/tags/%E8%BD%AF%E8%80%83/"/>
<category term="思维导图知识点" scheme="https://jovehawking.fun/tags/%E6%80%9D%E7%BB%B4%E5%AF%BC%E5%9B%BE%E7%9F%A5%E8%AF%86%E7%82%B9/"/>
</entry>
<entry>
<title>W-函数参数的直接传递和匿名传递</title>
<link href="https://jovehawking.fun/posts/be585466.html"/>
<id>https://jovehawking.fun/posts/be585466.html</id>
<published>2024-08-29T12:01:59.000Z</published>
<updated>2024-09-02T13:22:00.089Z</updated>
<content type="html"><![CDATA[<h1>函数参数的直接传递和匿名传递</h1><p>notifier():前端消息处理器,用来统一处理WebSocket消息。</p><p>notifier#attachMessageEvent:添加消息事件,参数为事件event</p><p>事件event:由各个控制器定义,不同controller内自己定义、实现。比如新建newProjectController、刷新updateController</p><p><strong>添加方式:假设事件实现为notifierMessage,如果notifierMessage内代码有this(使用了controller中的对对象),则只能使用箭头函数</strong></p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// updateController类中</span></span><br><span class="line"><span class="comment">// 1. 箭头函数(匿名函数)</span></span><br><span class="line"><span class="title function_">notifier</span>().<span class="title function_">attachMessageEvent</span>(<span class="function"><span class="params">message</span> =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">notifierMessage</span>(message);</span><br><span class="line">});</span><br><span class="line"></span><br><span class="line"><span class="comment">// 2. 有引用的箭头函数(匿名函数)</span></span><br><span class="line"><span class="variable language_">this</span>.<span class="property">_messageFunction</span> = <span class="function"><span class="params">message</span> =></span> {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="title function_">notifierMessage</span>(message);</span><br><span class="line">};</span><br><span class="line"><span class="title function_">notifier</span>().<span class="title function_">attachMessageEvent</span>(<span class="variable language_">this</span>.<span class="property">_messageFunction</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 3. 直接传递</span></span><br><span class="line"><span class="title function_">notifier</span>().<span class="title function_">attachMessageEvent</span>(<span class="variable language_">this</span>.<span class="property">notifierMessage</span>);</span><br><span class="line"></span><br><span class="line">前两种:</span><br><span class="line"> <span class="variable language_">this</span> 关键字指的是箭头函数外部的 <span class="variable language_">this</span> 上下文,也就是定义 notifierMessage 方法的对象的上下文。</span><br><span class="line"> 箭头函数继承了其外部作用域的 <span class="variable language_">this</span> 值,因此 <span class="variable language_">this</span>.<span class="property">notifierMessage</span> 将引用正确的 </span><br><span class="line"> notifierMessage 方法。</span><br><span class="line">【第二种有个好处是可以获取到匿名函数,因为匿名函数有了一个引用<span class="variable language_">this</span>.<span class="property">_messageFunction</span>, </span><br><span class="line"> 通过<span class="variable language_">this</span>.<span class="property">_messageFunction</span> 即可获取这个匿名函数。比如需要notifier取消绑定该事件消息时,</span><br><span class="line"> 需要根据引用,只能使用第二种方式】</span><br><span class="line"></span><br><span class="line">最后一种:<span class="variable language_">this</span> 关键字同样指的是定义 notifierMessage 方法的对象的上下文。</span><br><span class="line"> 但是,需要注意的是,在非严格模式下,当一个函数作为普通函数调用</span><br><span class="line"> 时(而不是作为对象的方法),<span class="variable language_">this</span> 的值通常会被设置为</span><br><span class="line"> 全局对象(在浏览器中是 <span class="variable language_">window</span>)。因此,如果没有明确绑定 </span><br><span class="line"> <span class="variable language_">this</span>,notifierMessage 可能不会按照预期工作。</span><br><span class="line">【因为是将event传递给notifier,后续由notifier调用,所以作为普通函数调用。】</span><br></pre></td></tr></table></figure><p><strong>区别总结</strong></p><ul><li><p><strong>代码片段一</strong>:</p></li><li><ul><li>使用了一个箭头函数来包裹对 <code>this.notifierMessage</code> 的调用。<ul><li>保证了 <code>this</code> 的正确上下文,即使 <code>notifierMessage</code> 方法内部依赖于 <code>this</code> 的值。</li></ul></li></ul></li><li><p><strong>代码片段二</strong>:</p></li><li><ul><li>直接传递 <code>this.notifierMessage</code> 方法给 <code>attachMessageEvent</code>。<ul><li>如果 <code>notifierMessage</code> 内部依赖于 <code>this</code> 的值,并且该方法不是作为对象的方法被调用,可能会导致 <code>this</code> 的值不符合预期。</li></ul></li></ul></li></ul><blockquote><p><strong>注意:</strong></p><p>notifier 添加的事件是 controller中的函数,所以当controller对象被回收时(没有controller对象引用时)这个事件也就失效了。即**对象被垃圾回收以后,其他 引用该对象方法的 方法,也会失效。**比如,controller 被回收以后,notifier中注册的该 controller 的 this.notifierMessage 方法也失效了。</p></blockquote>]]></content>
<summary type="html">开发时所遇到关于JS函数参数问题</summary>
<category term="工作inG" scheme="https://jovehawking.fun/categories/%E5%B7%A5%E4%BD%9CinG/"/>
<category term="JS函数参数" scheme="https://jovehawking.fun/tags/JS%E5%87%BD%E6%95%B0%E5%8F%82%E6%95%B0/"/>
</entry>
<entry>
<title>JAVAWeb-Spring高级知识学习</title>
<link href="https://jovehawking.fun/posts/20fd9c70.html"/>
<id>https://jovehawking.fun/posts/20fd9c70.html</id>
<published>2024-08-24T01:44:25.000Z</published>
<updated>2024-09-05T13:16:18.443Z</updated>
<content type="html"><![CDATA[<h1>Spring高级知识点</h1><blockquote><p>参考视频:<a href="https://www.bilibili.com/video/BV1P44y1N7QG/?p=8&spm_id_from=pageDriver&vd_source=85ac5ee1b07df12a44b648a8751d30f6">https://www.bilibili.com/video/BV1P44y1N7QG/?p=8&spm_id_from=pageDriver&vd_source=85ac5ee1b07df12a44b648a8751d30f6</a></p><p>参考文章:<a href="https://mofan212.github.io/posts/Spring-Forty-Nine-Lectures-Container-And-Bean/">https://mofan212.github.io/posts/Spring-Forty-Nine-Lectures-Container-And-Bean/</a></p></blockquote><h2 id="Spring容器">Spring容器</h2><p>以 SpringBoot 的启动类为例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">A01Application</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> SpringApplication.run(A01Application.class, args);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中,<code>run()</code>方法存在返回值,返回 ConfigurableApplicationContext 容器</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">ConfigurableApplicationContext</span> <span class="variable">context</span> <span class="operator">=</span> SpringApplication.run(Application.class, args);</span><br></pre></td></tr></table></figure><p><img src="../image/post/image-20240824102311015.png" alt="image-20240824102311015"></p><p>ConfigurableApplicationContext 接口继承了 ApplicationContext 接口,而 ApplicationContext 接口又间接地继承了 BeanFactory 接口,除此之外还继承了其他很多接口,相当于对 BeanFactory 进行了拓展。</p><h3 id="BeanFactory">BeanFactory</h3><ul><li>是 ApplicationContext 的父接口</li><li>是 Spring 的核心容器,主要的 ApplicationContext 实现 组合 了它的功能,也就是说,BeanFactory 是 ApplicationContext 中的一个成员变量。</li></ul><p>常用的 context.getBean(“xxx”) 方法,其实是调用了 BeanFactory 的 getBean() 方法。</p><p>其他方法:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">preInstantiateSingletons():预先初始化单例对象</span><br><span class="line">addEmbeddedValueResolver():给beanFactory加入解析器</span><br><span class="line"> 常见参数:</span><br><span class="line"> - <span class="keyword">new</span> <span class="title class_">StandardEnvironment</span>()::resolvePlaceholders : ${} 解析器</span><br></pre></td></tr></table></figure><p>基于它的子接口:</p><ul><li>ListableBeanFactory:提供获取 Bean 集合的能力,比如一个接口可能有多个实现,通过该接口下的方法就能获取某种类型的所有 Bean;</li><li>HierarchicalBeanFactory:Hierarchical 意为“层次化”,通常表示一种具有层级结构的概念或组织方式,这种层次化结构可以通过父子关系来表示对象之间的关联,比如树、图、文件系统、组织架构等。根据该接口下的方法可知,能够获取到父容器,说明 BeanFactory 有父子容器概念;</li><li>AutowireCapableBeanFactory:提供了创建 Bean、自动装配 Bean、属性填充、Bean 初始化、依赖注入等能力,比如 @Autowired 注解的底层实现就依赖于该接口的 resolveDependency() 方法;</li><li>ConfigurableBeanFactory:该接口并未直接继承至 BeanFactory,而是继承了 HierarchicalBeanFactory。</li></ul><blockquote><p><strong>BeanFactory不会</strong>:</p><ul><li>主动调用 BeanFactory 后置处理器;</li><li>主动添加 Bean 后置处理器;</li><li>主动初始化单例对象;</li><li>解析 ${} 和 #{}</li></ul></blockquote><h4 id="DefaultListableBeanFactory">DefaultListableBeanFactory</h4><p><img src="../image/post/image-20240824102509577.png" alt="image-20240824102509577"></p><ul><li>DefaultListableBeanFactory 实现了 BeanFactory 接口,它能管理 Spring 中所有的 Bean,当然也包含 Spring 容器中的那些单例对象。</li><li>DefaultListableBeanFactory 还继承了 DefaultSingletonBeanRegistry 类,这个类就是用来管理 Spring 容器中的单例对象。</li><li>通过 DefaultListableBeanFactory#registerBeanDefinition 可以注册bean到容器中</li></ul><h4 id="BeanFactoryPostProcessor">BeanFactoryPostProcessor</h4><p>BeanFactory后置处理器,典型的有</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">org.springframework.context.annotation.internalConfigurationAnnotationProcessor</span><br></pre></td></tr></table></figure><p>比如:internalConfigurationAnnotationProcessor就是用来处理 @Configuration 和 @Bean 注解的,将配置类中定义的 Bean 信息补充到 BeanFactory 中。</p><h4 id="BeanPostProcessor">BeanPostProcessor</h4><p>Bean后置处理器,典型的有</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">org.springframework.context.annotation.internalAutowiredAnnotationProcessor</span><br><span class="line">org.springframework.context.annotation.internalCommonAnnotationProcessor</span><br></pre></td></tr></table></figure><p>前者用于解析 @Autowired 注解,后者用于解析 @Resource 注解,它们都有一个共同的类型 BeanPostProcessor。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ConfigurationClassPostProcessor.class</span><br></pre></td></tr></table></figure><p>用于解析 @ComponentScan @Bean @Import @ImportResource</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">MapperScannerConfigurer.class</span><br></pre></td></tr></table></figure><p>用于解析 @MapperScan</p><h4 id="DefaultSingletonBeanRegistry">DefaultSingletonBeanRegistry</h4><p>用来管理 Spring 容器中的单例对象。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">final</span> Map<String, Object> singletonObjects = <span class="keyword">new</span> <span class="title class_">ConcurrentHashMap</span><>(<span class="number">256</span>);</span><br></pre></td></tr></table></figure><p>Map 的 key 就是 Bean 的名字,而 value 是对应的 Bean,即单例对象。</p><h3 id="BeanDefinition">BeanDefinition</h3><p>BeanDefinition 也是一个接口,它封装了 Bean 的定义,Spring 根据 Bean 的定义,就能创建出符合要求的 Bean。</p><p>读取 BeanDefinition 可以通过下列两种类完成:</p><ul><li>BeanDefinitionReader</li><li>ClassPathBeanDefinitionScanner</li></ul><h4 id="BeanDefinitionReader">BeanDefinitionReader</h4><p>该接口中对 loadBeanDefinitions() 方法进行了多种重载,支持传入一个或多个 Resource 对象、资源位置来加载 BeanDefinition。</p><p>它有一系列相关实现,比如:</p><ul><li>XmlBeanDefinitionReader:通过读取 XML 文件来加载;</li><li>PropertiesBeanDefinitionReader:通过读取 properties 文件来加载,此类已经被 @Deprecated 注解标记;</li></ul><p>除此之外,还有一个 AnnotatedBeanDefinitionReader,尽管它并不是 BeanDefinition 的子类,但它们俩长得很像,根据其类注释可知:它能够通过编程的方式对 Bean 进行注册,是 ClassPathBeanDefinitionScanner 的替代方案,能读取通过注解定义的 Bean。</p><h4 id="ClassPathBeanDefinitionScanner">ClassPathBeanDefinitionScanner</h4><p>通过扫描指定包路径下的 @Component 及其派生注解来注册 Bean,是 @ComponentScan 注解的底层实现。</p><p>比如 MyBatis 通过继承 ClassPathBeanDefinitionScanner 实现通过 @MapperScan 注解来扫描指定包下的 Mapper 接口。</p><h4 id="BeanDefinitionRegistry">BeanDefinitionRegistry</h4><p>AnnotatedBeanDefinitionReader 和 ClassPathBeanDefinitionScanner 中都有一个 BeanDefinitionRegistry 类型的成员变量,它是一个接口,提供了 BeanDefinition 的增加、删除和查找功能。</p><h3 id="ApplicationContext">ApplicationContext</h3><p>ApplicationContext 除了继承 BeanFactory 外,还继承了:</p><ul><li>MessageSource:使其具备处理国际化资源的能力</li><li>ResourcePatternResolver:使其具备使用通配符进行资源匹配的能力</li><li>EnvironmentCapable:使其具备读取 Spring 环境信息、配置文件信息的能力</li><li>ApplicationEventPublisher:使其具备发布事件的能力</li><li>ListableBeanFactory:提供了获取某种类型的 Bean 集合的能力</li><li>HierarchicalBeanFactory:提供了获取父容器的能力</li></ul><blockquote><p>虽然 ApplicationContext 继承了很多接口,但这些能力的实现是通过一种委派(Delegate)的方式实现的,这种方式也被叫做委派模式。</p><p>委派模式:实现获取资源的方式并不是由实现类自身完成,而是交给其内部的一个成员变量完成,这样的方式就是委派(这和对象适配器模式很相似)。在日常编码遇到这样的实现逻辑时,类名可以以 Delegate 结尾。</p></blockquote><h4 id="ApplicationContext的相关实现"><strong>ApplicationContext的相关实现</strong></h4><ul><li>ClassPathXmlApplicationContext:基于 classpath 下的 xml 格式的配置文件来创建Bean</li><li>FileSystemXmlApplicationContext:基于磁盘路径下 xml 格式的配置文件来创建Bean</li></ul><blockquote><p>本质都是利用 XmlBeanDefinitionReader#loadBeanDefinitions 加载Bean</p></blockquote><ul><li>AnnotationConfigApplicationContext:基于 Java 配置类来创建</li><li>AnnotationConfigServletWebServerApplicationContext:基于 Java 配置类来创建,用于 web 环境</li></ul><h4 id="ConfigurableApplicationContext">ConfigurableApplicationContext</h4><p>ApplicationContext 有一个子接口 ConfigurableApplicationContext,从类名就可以看出,它提供了对 ApplicationContext 进行配置的能力,浏览其内部方法可知,提供了诸如设置父容器、设置 Environment 等能力。</p><h4 id="AbstractApplicationContext">AbstractApplicationContext</h4><p>ApplicationContext 有一个非常重要的抽象实现 AbstractApplicationContext,其他具体实现都会继承这个抽象实现,在其内部通过委派的方式实现了一些接口的能力,除此之外还有一个与 Spring Bean 的生命周期息息相关的方法:refresh()。</p><h3 id="Bean">Bean</h3><h4 id="生命周期">生命周期</h4><p>初始化和销毁 Bean 的实现有三种:</p><ol><li>依赖于后置处理器提供的拓展功能</li><li>相关接口的功能</li><li>使用 @Bean 注解中的属性进行指定</li></ol><p>当同时存在以上三种方式时,它们的执行顺序也将按照上述顺序进行执行。</p><p>通过实现以下 BeanPostProcessor 接口,可以增强 Bean</p><ul><li>InstantiationAwareBeanPostProcessor</li><li>DestructionAwareBeanPostProcessor</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyBeanPostProcessor</span> <span class="keyword">implements</span> <span class="title class_">InstantiationAwareBeanPostProcessor</span>, DestructionAwareBeanPostProcessor {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">postProcessBeforeDestruction</span><span class="params">(Object o, String beanName)</span> <span class="keyword">throws</span> BeansException {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"lifeCycleBean"</span>.equals(beanName)) {</span><br><span class="line"> log.info(<span class="string">"<<<<<<<<<< 销毁执行之前,如 @PreDestroy"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">postProcessBeforeInstantiation</span><span class="params">(Class<?> beanClass, String beanName)</span> <span class="keyword">throws</span> BeansException {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"lifeCycleBean"</span>.equals(beanName)) {</span><br><span class="line"> log.info(<span class="string">"<<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">null</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">postProcessAfterInstantiation</span><span class="params">(Object bean, String beanName)</span> <span class="keyword">throws</span> BeansException {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"lifeCycleBean"</span>.equals(beanName)) {</span><br><span class="line"> log.info(<span class="string">"<<<<<<<<<< 实例化之后执行,如果返回 false 会跳过依赖注入节点"</span>);</span><br><span class="line"> <span class="comment">// return false;</span></span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">true</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> PropertyValues <span class="title function_">postProcessProperties</span><span class="params">(PropertyValues pvs, Object bean, String beanName)</span> <span class="keyword">throws</span> BeansException {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"lifeCycleBean"</span>.equals(beanName)) {</span><br><span class="line"> log.info(<span class="string">"<<<<<<<<<< 依赖注入阶段执行,如 @Autowired、@Value、@Resource"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> pvs;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">postProcessBeforeInitialization</span><span class="params">(Object bean, String beanName)</span> <span class="keyword">throws</span> BeansException {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"lifeCycleBean"</span>.equals(beanName)) {</span><br><span class="line"> log.info(<span class="string">"<<<<<<<<<< 初始化执行之前,这里返回的对象会替换掉原本的 bean,如 @PostConstruct、@ConfigurationProperties"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> bean;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">postProcessAfterInitialization</span><span class="params">(Object bean, String beanName)</span> <span class="keyword">throws</span> BeansException {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"lifeCycleBean"</span>.equals(beanName)) {</span><br><span class="line"> log.info(<span class="string">"<<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强"</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> bean;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">---------------------------------------------------------------------------------------------</span><br><span class="line">输出:</span><br><span class="line"> </span><br><span class="line">indi.mofan.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 实例化之前执行,这里返回的对象会替换掉原本的 bean</span><br><span class="line">indi.mofan.bean.a03.LifeCycleBean : 构造</span><br><span class="line">indi.mofan.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 实例化之后执行,如果返回 <span class="literal">false</span> 会跳过依赖注入节点</span><br><span class="line">indi.mofan.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 依赖注入阶段执行,如 <span class="meta">@Autowired</span>、<span class="meta">@Value</span>、<span class="meta">@Resource</span></span><br><span class="line">indi.mofan.bean.a03.LifeCycleBean : 依赖注入: D:\environment\JDK1<span class="number">.8</span></span><br><span class="line">indi.mofan.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 初始化执行之前,这里返回的对象会替换掉原本的 bean,如 <span class="meta">@PostConstruct</span>、<span class="meta">@ConfigurationProperties</span></span><br><span class="line">indi.mofan.bean.a03.LifeCycleBean : 初始化</span><br><span class="line">indi.mofan.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 初始化之后执行,这里返回的对象会替换掉原本的 bean,如代理增强</span><br><span class="line">indi.mofan.bean.a03.MyBeanPostProcessor : <<<<<<<<<< 销毁执行之前,如 <span class="meta">@PreDestroy</span></span><br><span class="line">indi.mofan.bean.a03.LifeCycleBean : 销毁</span><br></pre></td></tr></table></figure><h4 id="设计模式">设计模式</h4><p>为什么实现了 BeanPostProcessor 接口后就能够在 Bean 生命周期的各个阶段进行拓展呢?</p><p>因为使用了模板方法设计模式。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">MyBeanFactory</span> {</span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">getBean</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Object</span> <span class="variable">bean</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"> System.out.println(<span class="string">"构造 "</span> + bean);</span><br><span class="line"> System.out.println(<span class="string">"依赖注入 "</span> + bean);</span><br><span class="line"> <span class="keyword">for</span> (BeanPostProcessor processor : processors) {</span><br><span class="line"> processor.inject(bean);</span><br><span class="line"> }</span><br><span class="line"> System.out.println(<span class="string">"初始化 "</span> + bean);</span><br><span class="line"> <span class="keyword">return</span> bean;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> List<BeanPostProcessor> processors = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">addProcessor</span><span class="params">(BeanPostProcessor processor)</span> {</span><br><span class="line"> processors.add(processor);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">// 之后如果需要拓展,调用 MyBeanFactory 实例的 addProcessor() 方法添加拓展逻辑即可:</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">MyBeanFactory</span> <span class="variable">beanFactory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MyBeanFactory</span>();</span><br><span class="line"> beanFactory.addProcessor(bean -> System.out.println(<span class="string">"解析 @Autowired"</span>));</span><br><span class="line"> beanFactory.addProcessor(bean -> System.out.println(<span class="string">"解析 @Resource"</span>));</span><br><span class="line"> beanFactory.getBean();</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><img src="../image/post/image-20240824172029016.png" alt="image-20240824172029016"></p><h4 id="ConfigurationProperties-注解">@ConfigurationProperties 注解</h4><p>使用 @ConfigurationProperties 可以指定配置信息的前缀,使得配置信息的读取更加简单。</p><h4 id="AutowiredAnnotationBeanPostProcessor">AutowiredAnnotationBeanPostProcessor</h4><p>用于解析 @Autowired 和 @Value 注解</p><p><strong>AutowiredAnnotationBeanPostProcessor#postProcessProperties()方法</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> PropertyValues <span class="title function_">postProcessProperties</span><span class="params">(PropertyValues pvs, Object bean, String beanName)</span> {</span><br><span class="line"> <span class="type">InjectionMetadata</span> <span class="variable">metadata</span> <span class="operator">=</span> findAutowiringMetadata(beanName, bean.getClass(), pvs);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> metadata.inject(bean, beanName, pvs);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">catch</span> (BeanCreationException ex) {</span><br><span class="line"> <span class="keyword">throw</span> ex;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">catch</span> (Throwable ex) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">BeanCreationException</span>(beanName, <span class="string">"Injection of autowired dependencies failed"</span>, ex);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> pvs;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中的 findAutowiringMetadata() 用于查找指定的 bean 对象中哪些地方使用了 @Autowired、@Value 等与注入相关的注解,并将这些信息封装在 InjectionMetadata 对象中,之后调用其 inject() 方法利用反射完成注入。</p><blockquote><p>InjectionMetadata 对象中有一个名为 injectedElements 的集合类型成员变量,根据上图所示,injectedElements 存储了被相关注解标记的成员变量、方法的信息,因为 Bean1 中的 bean3 成员变量、setBean2() 和 setHome() 方法恰好被 @Autowired 注解标记。</p></blockquote><h4 id="Scope">Scope</h4><p>Scope 用于指定 Bean 的作用范围,有如下五个取值:</p><ol><li>singleton:单例(默认值)。容器启动时创建(未设置延迟),容器关闭时销毁</li><li>prototype:多例。每次使用时创建,不会自动销毁,需要调用 DefaultListableBeanFactory#destroyBean() 进行销毁</li><li>request:作用于 Web 应用的请求范围。每次请求用到此 Bean 时创建,请求结束时销毁</li><li>session:作用于 Web 应用的会话范围。每个会话用到此 Bean 时创建,会话结束时销毁</li><li>application:作用于 Web 应用的 ServletContext。Web 容器用到此 Bean 时创建,容器关闭时销毁</li></ol><blockquote><p>application 的作用范围是 ServletContext,要想 application scope 发生变化可以重启程序。</p></blockquote><h3 id="Aware-接口">Aware 接口</h3><p>Aware 接口用于注入一些与容器相关的信息,比如:</p><ul><li>BeanNameAware 注入 Bean 的名字</li><li>BeanFactoryAware 注入 BeanFactory 容器</li><li>ApplicationContextAware 注入 ApplicationContext 容器</li><li>EmbeddedValueResolverAware 解析 ${}</li></ul><blockquote><ol><li>Aware 接口提供了一种 内置 的注入手段,可以注入 BeanFactory、ApplicationContext;</li><li>InitializingBean 接口提供了一种 内置 的初始化手段;</li><li>内置的注入和初始化不受拓展功能的影响,总会被执行,因此 Spring 框架内部的类总是使用这些接口。</li></ol></blockquote><h4 id="Aware-相关接口">Aware 相关接口</h4><p><strong>BeanNameAware, ApplicationContextAware</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@author</span> mofan</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@date</span> 2023/1/8 16:12</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyBean</span> <span class="keyword">implements</span> <span class="title class_">BeanNameAware</span>, ApplicationContextAware {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setBeanName</span><span class="params">(String name)</span> {</span><br><span class="line"> log.info(<span class="string">"当前 Bean: "</span> + <span class="built_in">this</span> + <span class="string">"名字叫: "</span> + name);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setApplicationContext</span><span class="params">(ApplicationContext applicationContext)</span> <span class="keyword">throws</span> BeansException {</span><br><span class="line"> log.info(<span class="string">"当前 Bean: "</span> + <span class="built_in">this</span> + <span class="string">"容器是: "</span> + applicationContext);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>InitializingBean</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyBean</span> <span class="keyword">implements</span> <span class="title class_">BeanNameAware</span>, ApplicationContextAware, InitializingBean {</span><br><span class="line"> <span class="comment">// --snip--</span></span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">afterPropertiesSet</span><span class="params">()</span> <span class="keyword">throws</span> Exception {</span><br><span class="line"> log.info(<span class="string">"当前 Bean: "</span> + <span class="built_in">this</span> + <span class="string">" 初始化"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当同时实现 Aware 接口和 InitializingBean 接口时,会先执行 Aware 接口。</p><p>BeanFactoryAware 、ApplicationContextAware 和 EmbeddedValueResolverAware 三个接口的功能可以使用 @Autowired 注解实现,InitializingBean 接口的功能也可以使用 @PostConstruct 注解实现,为什么还要使用接口呢?</p><h4 id="为何有这些接口">为何有这些接口</h4><p>@Autowired 和 @PostConstruct 注解的解析需要使用 Bean 后置处理器,属于拓展功能,而这些接口属于内置功能,不加任何拓展 Spring 就能识别。在某些情况下,拓展功能会失效,而内容功能不会失效。</p><p>对于 context.refresh(); 方法来说,它主要按照以下顺序干了三件事:</p><ol><li>执行 BeanFactory 后置处理器;</li><li>添加 Bean 后置处理器;</li><li>创建和初始化单例对象。</li></ol><p><strong>失效场景</strong></p><p>当 Java 配置类中定义了BeanFactoryPostProcessor 时,如果要创建配置类中的 BeanFactoryPostProcessor 就必须 提前 创建和初始化 Java 配置类。</p><p>在创建和初始化 Java 配置类时,由于 BeanPostProcessor 还未准备好,无法解析配置类中的 @Autowired 等注解,导致 @Autowired 等注解失效:</p><blockquote><p>具体场景参考博客。</p></blockquote><p><strong>如果一个单例对象的成员变量是多例,怎么办才能在getBean的时候,获取的成员变量是多例的</strong></p><p>eg:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// class1</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">F1</span> {</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// class2</span></span><br><span class="line"><span class="meta">@Getter</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">E</span> {</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> F1 f1;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// main</span></span><br><span class="line"><span class="meta">@Slf4j</span></span><br><span class="line"><span class="meta">@ComponentScan("indi.mofan.bean.a09")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">A09Application</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="type">AnnotationConfigApplicationContext</span> <span class="variable">context</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AnnotationConfigApplicationContext</span>(A09Application.class);</span><br><span class="line"></span><br><span class="line"> <span class="type">E</span> <span class="variable">e</span> <span class="operator">=</span> context.getBean(E.class);</span><br><span class="line"> log.info(<span class="string">"{}"</span>, e.getF1());</span><br><span class="line"> log.info(<span class="string">"{}"</span>, e.getF1());</span><br><span class="line"> log.info(<span class="string">"{}"</span>, e.getF1());</span><br><span class="line"></span><br><span class="line"> context.close();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line">----------------------------------------------------------------------------------------</span><br><span class="line"><span class="comment">// 输出的F1为同一个</span></span><br></pre></td></tr></table></figure><p>原因:对于单例对象来说,依赖注入仅发生了一次,后续不会再注入其他的 f1,因此 e 始终使用的是第一次注入的 f1</p><p>解决:</p><ol><li>可以<strong>使用 @Lazy 注解,因为 @Lazy 生成的是代理对象</strong>,虽然代理对象依旧是同一个,但每次使用代理对象中的方法时,会由代理对象创建新的目标对象</li><li>其他推荐方式(Object工程、Context容器)参考博客</li></ol><h2 id="AOP">AOP</h2><h3 id="前置基础">前置基础</h3><ol><li>aop的实现方式(除jdk、cglib外的实现方式)</li><li>aop源码、原理</li></ol><blockquote><p>以上参考博客(9-13):</p><p><a href="https://mofan212.github.io/posts/Spring-Forty-Nine-Lectures-AOP/">https://mofan212.github.io/posts/Spring-Forty-Nine-Lectures-AOP/</a></p></blockquote><h3 id="Pointcut(切点)">Pointcut(切点)</h3><p>在 Spring 中,切点通过接口 org.springframework.aop.Pointcut 来表示</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Pointcut</span> {</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 根据类型过滤</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">ClassFilter <span class="title function_">getClassFilter</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 根据方法匹配</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line">MethodMatcher <span class="title function_">getMethodMatcher</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Canonical Pointcut instance that always matches.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="type">Pointcut</span> <span class="variable">TRUE</span> <span class="operator">=</span> TruePointcut.INSTANCE;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Pointcut 接口有很多实现类,比如:</p><ul><li>AnnotationMatchingPointcut:通过注解进行匹配</li><li>AspectJExpressionPointcut:通过 AspectJ 表达式进行匹配(本节的选择)</li><li>StaticMethodMatcherPointcut:通过注解进行匹配(视频中匹配 @Transactional 时使用)</li></ul><p>无论是 AspectJExpressionPointcut 还是 StaticMethodMatcherPointcut,它们都实现了 MethodMatcher 接口,用来执行方法的匹配。</p><h4 id="AspectJExpressionPointcut">AspectJExpressionPointcut</h4><p>判断编写的 AspectJ 表达式是否与某一方法匹配可以使用其 matches() 方法。</p><h4 id="StaticMethodMatcherPointcut">StaticMethodMatcherPointcut</h4><blockquote><p>@Transactional 是 Spring 中使用频率非常高的注解,那它底层是通过 AspectJExpressionPointcut 与 @annotation() 切点表达式相结合对目标方法进行匹配的吗?</p></blockquote><p>答案是否定的。@Transactional 注解除了可以作用在方法上,还可以作用在类(或接口)上。</p><p>在底层 @Transactional 注解的匹配使用到了 StaticMethodMatcherPointcut</p><h3 id="Advice(通知)">Advice(通知)</h3><p>MethodInterceptor:这个接口实现的通知属于环绕通知。</p><h3 id="Aspect(切面)">Aspect(切面)</h3><p>DefaultPointcutAdvisor:创建这种切面时,传递一个节点和通知。</p><blockquote><p>/*</p><ul><li><p>多个切面:</p></li><li><p>aspect =</p></li><li><p>通知 1 (advice) + 切点 1(pointcut)</p></li><li><p>通知 2 (advice) + 切点 2(pointcut)</p></li><li><p>通知 3 (advice) + 切点 3(pointcut)</p></li><li><p>…</p></li><li><p>advisor = 更细粒度的切面,包含一个通知和切点</p></li><li><p>*/</p></li></ul></blockquote><h3 id="ProxyFactory">ProxyFactory</h3><blockquote><p>Spring 是根据什么信息来选择不同的动态代理实现呢?</p></blockquote><p>ProxyFactory 的父类 ProxyConfig 中有个名为 proxyTargetClass 的布尔类型成员变量:</p><ul><li>当 proxyTargetClass == false,并且目标对象所在类实现了接口时,将选择 JDK 动态代理;</li><li>当 proxyTargetClass == false,但目标对象所在类未实现接口时,将选择 CGLib 动态代理;</li><li>当 proxyTargetClass == true,总是选择 CGLib 动态代理。</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 设置实现的接口</span></span><br><span class="line">factory.setInterfaces(target.getClass().getInterfaces());</span><br><span class="line"><span class="comment">// 设置proxyTargetClass </span></span><br><span class="line">factory.setProxyTargetClass(<span class="literal">true</span>);</span><br></pre></td></tr></table></figure><hr><p>ProxyFactory 是用来创建代理的核心实现,使用 AopProxyFactory 选择具体的代理实现:</p><ul><li>JdkDynamicAopProxy</li><li>ObjenesisCglibAopProxy</li></ul><p>AopProxyFactory 根据 proxyTargetClass 等设置选择 AopProxy 实现,AopProxy 通过 getProxy() 方法创建代理对象。</p><p>上述类图中的类与接口都实现了 Advised 接口,能够获得关联的切面集合与目标(实际上是从 ProxyFactory 中获取的)。</p><p>调用代理方法时,会借助 ProxyFactory 统一将通知转换为环绕通知 MethodInterceptor。。</p><h3 id="AnnotationAwareAspectJAutoProxyCreator">AnnotationAwareAspectJAutoProxyCreator</h3><p>Bean 后置处理器。尽管它的名称中没有 BeanPostProcessor 的字样,但它确实是实现了 BeanPostProcessor 接口的。</p><p>AnnotationAwareAspectJAutoProxyCreator 有两个主要作用:</p><ol><li>找到容器中所有的切面,针对高级切面,将其转换为低级切面;</li><li>根据切面信息,利用 ProxyFactory 创建代理对象。</li></ol><p>AnnotationAwareAspectJAutoProxyCreator 实现了 BeanPostProcessor,可以在 Bean 生命周期中的一些阶段对 Bean 进行拓展。AnnotationAwareAspectJAutoProxyCreator 可以在 Bean 进行 <strong>依赖注入之前、Bean 初始化之后</strong> 对 Bean 进行拓展。</p><p><strong>重点介绍 AnnotationAwareAspectJAutoProxyCreator 中的两个方法:</strong></p><ul><li>findEligibleAdvisors</li><li>wrapIfNecessary</li></ul><h4 id="findEligibleAdvisors">findEligibleAdvisors</h4><p>位于父类 AbstractAdvisorAutoProxyCreator 中,用于找到符合条件的切面类。低级切面直接添加,高级切面转换为低级切面再添加。</p><p>findEligibleAdvisors() 方法接收两个参数:</p><ul><li>beanClass:配合切面使用的目标类 Class 信息</li><li>beanName:当前被代理的 Bean 的名称</li></ul><h4 id="wrapIfNecessary">wrapIfNecessary</h4><p>wrapIfNecessary() 方法内部调用了 findEligibleAdvisors() 方法,若 findEligibleAdvisors() 方法返回的集合不为空,则表示需要创建代理对象。</p><p>如果需要创建对象,wrapIfNecessary() 方法返回的是代理对象,否则仍然是原对象。</p><p>wrapIfNecessary() 方法接收三个参数:</p><ul><li>bean:原始 Bean 实例</li><li>beanName:Bean 的名称</li><li>cacheKey:用于元数据访问的缓存 key</li></ul><h3 id="Order">@Order</h3><p>根据上述打印的信息可知,低级切面相比于高级切面先一步被执行,这个执行顺序是可以被控制的。</p><p>针对高级切面来说,可以在类上使用 <code>@Order</code> 注解</p><p>在高级切面中,@Order 只有放在类上才生效,放在方法上不会生效。比如高级切面中有多个前置通知,这些前置通知对应的方法上使用 @Order 注解是无法生效的。</p><p>针对低级切面,需要设置 advisor 的 order 值,而不是向高级切面那样使用 @Order 注解,使用 @Order 注解设置在 advisor3() 方法上是无用的。</p><h3 id="代理对象创建时机">代理对象创建时机</h3><p>使用 AnnotationAwareAspectJAutoProxyCreator Bean 后置处理器创建代理对象的时机有以下两个选择:</p><ul><li>Bean 的依赖注入之前</li><li>Bean 初始化完成之后</li></ul><p>代理对象的创建时机:</p><ul><li>无循环依赖时,在 Bean 初始化阶段之后创建;</li><li>有循环依赖时,在 Bean 实例化后、依赖注入之前创建,并将代理对象暂存于二级缓存。</li></ul><p>Bean 的依赖注入阶段和初始化阶段不应该被增强,仍应被施加于原始对象。</p><h3 id="高级切面转低级切面">高级切面转低级切面</h3><p>调用 AnnotationAwareAspectJAutoProxyCreator 对象的 findEligibleAdvisors() 方法时,获取能配合目标 Class 使用的切面,最终返回 Advisor 列表。在搜索过程中,如果遇到高级切面,则会将其转换成低级切面。</p><p>以解析 @Before 注解为例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> Throwable {</span><br><span class="line"> <span class="comment">// 切面对象实例工厂,用于后续反射调用切面中的方法</span></span><br><span class="line"> <span class="type">AspectInstanceFactory</span> <span class="variable">factory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SingletonAspectInstanceFactory</span>(<span class="keyword">new</span> <span class="title class_">Aspect</span>());</span><br><span class="line"> <span class="comment">// 高级切面转低级切面类</span></span><br><span class="line"> List<Advisor> list = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"> <span class="keyword">for</span> (Method method : Aspect.class.getDeclaredMethods()) {</span><br><span class="line"> <span class="keyword">if</span> (method.isAnnotationPresent(Before.class)) {</span><br><span class="line"> <span class="comment">// 解析切点</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">expression</span> <span class="operator">=</span> method.getAnnotation(Before.class).value();</span><br><span class="line"> <span class="type">AspectJExpressionPointcut</span> <span class="variable">pointcut</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AspectJExpressionPointcut</span>();</span><br><span class="line"> pointcut.setExpression(expression);</span><br><span class="line"> <span class="comment">// 通知类。前置通知对应的通知类是 AspectJMethodBeforeAdvice</span></span><br><span class="line"> <span class="type">AspectJMethodBeforeAdvice</span> <span class="variable">advice</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">AspectJMethodBeforeAdvice</span>(method, pointcut, factory);</span><br><span class="line"> <span class="comment">// 切面(advice转换成advisor)</span></span><br><span class="line"> <span class="type">Advisor</span> <span class="variable">advisor</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DefaultPointcutAdvisor</span>(pointcut, advice);</span><br><span class="line"> list.add(advisor);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">for</span> (Advisor advisor : list) {</span><br><span class="line"> System.out.println(advisor);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>@Before 标记的前置通知会被转换成原始的 AspectJMethodBeforeAdvice 形式,该对象包含了以下信息:</p><ul><li>通知对应的方法信息</li><li>切点信息</li><li>通知对象如何创建,本例公用一个 Aspect 对象</li></ul><table><thead><tr><th style="text-align:center">注解</th><th style="text-align:center">对应的原始通知类</th></tr></thead><tbody><tr><td style="text-align:center">@Before</td><td style="text-align:center">AspectJMethodBeforeAdvice</td></tr><tr><td style="text-align:center">@AfterReturning</td><td style="text-align:center">AspectJAfterReturningAdvice</td></tr><tr><td style="text-align:center">@AfterThrowing</td><td style="text-align:center">AspectJAfterThrowingAdvice</td></tr><tr><td style="text-align:center">@After</td><td style="text-align:center">AspectJAfterAdvice</td></tr><tr><td style="text-align:center">@Around</td><td style="text-align:center">AspectJAroundAdvice</td></tr></tbody></table><h3 id="静态通知调用">静态通知调用</h3><p>详情见参考博客</p><h3 id="动态通知调用">动态通知调用</h3><p>详情见参考博客</p><h2 id="MVC">MVC</h2><h3 id="RequestMappingHandlerMapping">RequestMappingHandlerMapping</h3><p>HandlerMapping,即处理器映射器,用于建立请求路径与控制器方法的映射关系。</p><p>RequestMappingHandlerMapping 是 HandlerMapping 的一种实现,根据类名可知,它是通过 @RequestMapping 注解来实现路径映射。</p><p>当 Spring 容器中没有 HandlerMapping 的实现时,尽管 DispatcherServlet 在初始化时会添加一些默认的实现,但这些实现不会交由 Spring 管理,而是作为 DispatcherServlet 的成员变量。</p><h3 id="RequestMappingHandlerAdapter">RequestMappingHandlerAdapter</h3><p>RequestMappingHandlerAdapter 实现了 HandlerAdapter 接口,HandlerAdapter 用于执行控制器方法,而 RequestMapping 表明 RequestMappingHandlerAdapter 用于执行被 @RequestMapping 注解标记的控制器方法。</p><blockquote><p>实现控制器方法的调用很简单,但如何将请求参数与方法参数相绑定的呢?</p></blockquote><p>显然是需要解析 @RequestParam 注解。</p><p>Spring 支持许多种类的控制器方法参数,不同种类的参数使用不同的解析器,使用 RequestMappingHandlerAdapter 的 getArgumentResolvers() 方法获取所有参数解析器。</p><p>Spring 也支持许多种类的控制器方法返回值类型,使用 RequestMappingHandlerAdapter 的 getReturnValueHandlers() 方法获取所有返回值处理器。</p><h3 id="参数解析器">参数解析器</h3><h4 id="RequestParam">@RequestParam</h4><p>@RequestParam 注解的解析需要使用到 RequestParamMethodArgumentResolver 参数解析器。构造时需要两个参数:</p><ul><li>beanFactory:Bean 工厂对象。需要解析 ${} 时,就需要指定 Bean 工厂对象</li><li>useDefaultResolution:布尔类型参数。为 false 表示只解析添加了 @RequestParam 注解的参数,为 true 针对未添加 @RequestParam 注解的参数也使用该参数解析器进行解析。</li></ul><p>RequestParamMethodArgumentResolver 利用 resolveArgument() 方法完成参数的解析,该方法需要传递四个参数:</p><ul><li>parameter:参数对象</li><li>mavContainer:ModelAndView 容器,用来存储中间的 Model 结果</li><li>webRequest:由 ServletWebRequest 封装后的请求对象</li><li>binderFactory:数据绑定工厂,用于完成对象绑定和类型转换,比如将字符串类型的 18 转换成整型</li></ul><h4 id="PathVariable">@PathVariable</h4><p>@PathVariable 注解的解析需要使用到 PathVariableMethodArgumentResolver 参数解析器。构造时无需传入任何参数。</p><p>使用该解析器需要一个 Map 集合,该 Map 集合是 @RequestMapping 注解上指定的路径和实际 URL 路径进行匹配后,得到的路径上的参数与实际路径上的值的关系(获取这个 Map 并将其设置给 request 作用域由 HandlerMapping 完成)。</p><h4 id="RequestHeader">@RequestHeader</h4><p>@RequestHeader 注解的解析需要使用到 RequestHeaderMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。</p><h4 id="CookieValue">@CookieValue</h4><p>@CookieValue 注解的解析需要使用到 ServletCookieValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。</p><h4 id="Value">@Value</h4><p>@Value 注解的解析需要使用到 ExpressionValueMethodArgumentResolver 参数解析器。构造时需要传入一个Bean 工厂对象。</p><h4 id="HttpServletRequest">HttpServletRequest</h4><p>HttpServletRequest 类型的参数的解析需要使用到 ServletRequestMethodArgumentResolver 参数解析器。构造时无需传入任何参数。</p><p>ServletRequestMethodArgumentResolver 参数解析器不仅可以解析 HttpServletRequest 类型的参数,还支持许多其他类型的参数,其支持的参数类型可在 supportsParameter() 方法中看到:</p><h4 id="ModelAttribute">@ModelAttribute</h4><p>@ModelAttribute 注解的解析需要使用到 ServletModelAttributeMethodProcessor 参数解析器。构造时需要传入一个布尔类型的值。为 false 时,表示 @ModelAttribute 不是不必须的,即是必须的。</p><p>针对 @ModelAttribute(“abc”) User user1 和 User user2 两种参数来说,尽管后者没有使用 @ModelAttribute 注解,但它们使用的是同一种解析器。</p><p>添加两个 ServletModelAttributeMethodProcessor 参数解析器,先解析带 @ModelAttribute 注解的参数,再解析不带 @ModelAttribute 注解的参数。</p><p>通过 ServletModelAttributeMethodProcessor 解析得到的数据还会被存入 ModelAndViewContainer 中。存储的数据结构是一个 Map,其 key 为 @ModelAttribute 注解指定的 value 值,在未显式指定的情况下,默认为对象类型的首字母小写对应的字符串。</p><p>@RequestBody User user3 参数也被 ServletModelAttributeMethodProcessor 解析了,如果想使其数据通过 JSON 数据转换而来,则需要使用另一个参数解析器。</p><h4 id="RequestBody">@RequestBody</h4><p>@RequestBody 注解的解析需要使用到 RequestResponseBodyMethodProcessor 参数解析器。构造时需要传入一个消息转换器列表。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">先添加解析 <span class="meta">@ModelAttribute</span> 注解的解析器,再添加解析 <span class="meta">@RequestBody</span> 注解的解析器,最后添加解析省略了 <span class="meta">@ModelAttribute</span> 注解的解析器。如果更换最后两个解析器的顺序,那么 <span class="meta">@RequestBody</span> User user3 将会被 ServletModelAttributeMethodProcessor 解析,而不是 RequestResponseBodyMethodProcessor。</span><br></pre></td></tr></table></figure><h3 id="获取参数名">获取参数名</h3><h4 id="DefaultParameterNameDiscoverer">DefaultParameterNameDiscoverer</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DefaultParameterNameDiscoverer</span> <span class="keyword">extends</span> <span class="title class_">PrioritizedParameterNameDiscoverer</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">DefaultParameterNameDiscoverer</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (KotlinDetector.isKotlinReflectPresent() && !NativeDetector.inNativeImage()) {</span><br><span class="line"> addDiscoverer(<span class="keyword">new</span> <span class="title class_">KotlinReflectionParameterNameDiscoverer</span>());</span><br><span class="line"> }</span><br><span class="line"> addDiscoverer(<span class="keyword">new</span> <span class="title class_">StandardReflectionParameterNameDiscoverer</span>());</span><br><span class="line"> addDiscoverer(<span class="keyword">new</span> <span class="title class_">LocalVariableTableParameterNameDiscoverer</span>());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>需要获取参数名称的原因:(java编译的时候,如果不加-par-parameters 即不是【javac -parameters .\Bean2.java】编译的,则不会保留参数名称)</strong></p><p>在项目的 src 目录外创建一个 Bean2.java 文件,使其不会被 IDEA 自动编译</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> indi.mofan.a22;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Bean2</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">foo</span><span class="params">(String name, <span class="type">int</span> age)</span> {</span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>将命令行切换到 Bean2.java 文件所在目录的位置,执行 javac .\Bean2.java 命令手动编译 Bean2.java。查看 Bean2.class 文件的内容</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> indi.mofan.a22;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Bean2</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Bean2</span><span class="params">()</span> {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">foo</span><span class="params">(String var1, <span class="type">int</span> var2)</span> {</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>编译生成的 class 文件中的 foo() 方法的参数名称不再是 name 和 age,也就是说直接使用 javac 命令进行编译得到的字节码文件不会保存方法的参数名称。</p></blockquote>]]></content>
<summary type="html">JAVAWeb-Spring高级知识(源码)相关学习笔记</summary>
<category term="JAVAWebing" scheme="https://jovehawking.fun/categories/JAVAWebing/"/>
<category term="JAVAWeb" scheme="https://jovehawking.fun/tags/JAVAWeb/"/>
<category term="Spring" scheme="https://jovehawking.fun/tags/Spring/"/>
</entry>
<entry>
<title>读书-Effective Java系列(五)</title>
<link href="https://jovehawking.fun/posts/5b5ad457.html"/>
<id>https://jovehawking.fun/posts/5b5ad457.html</id>
<published>2024-08-24T01:01:50.000Z</published>
<updated>2024-08-25T09:24:04.208Z</updated>
<content type="html"><![CDATA[<h1>五</h1>]]></content>
<summary type="html">Effective Java书籍第五章笔记整理</summary>
<category term="读书inG" scheme="https://jovehawking.fun/categories/%E8%AF%BB%E4%B9%A6inG/"/>
<category term="读书inG" scheme="https://jovehawking.fun/tags/%E8%AF%BB%E4%B9%A6inG/"/>
<category term="Effective Java" scheme="https://jovehawking.fun/tags/Effective-Java/"/>
</entry>
<entry>
<title>读书-Effective Java系列(三、四)</title>
<link href="https://jovehawking.fun/posts/61549102.html"/>
<id>https://jovehawking.fun/posts/61549102.html</id>
<published>2024-07-31T12:12:50.000Z</published>
<updated>2024-08-25T09:24:04.206Z</updated>
<content type="html"><![CDATA[<h1>三、四</h1><h2 id="10-覆盖quals时请遵守通用规定">10. 覆盖quals时请遵守通用规定</h2><blockquote><p>尽管Object是一个具体类,但设计它主要是为了扩展。它所有的非final方法(equals、hashCode、toString、clone和finalize)都有明确的通用约定(general contract),因为它们设计成是要被覆盖(override)的。任何任何一个类,它在覆盖这些方法的时候,都有责任遵守这些通用约定;如果不能做到这一点,其他依赖于这些约定的类(例如HashMap和HashSet)就无法结合该类一起正常运作。</p><p>本章将讲述何时以及如何覆盖这些非final的Object方法。本章不再讨论finalize方法,因为第8条已经讨论过这个方法了。而Comparable.compareTo虽然不是Object方法,但是本章也将对其进行讨论,因为它具有类似的特点。</p></blockquote><p>覆盖 equals 方法看起来似乎很简单,但是有许多覆盖方式会导致错误,并且后果非常严重。最容易避免这类问题的办法就是不覆盖 equals 方法,在这种情况下,类的每个实例都只与它自身相等。如果满足了以下任何一个条件,就可以不覆盖 equals 方法:</p><ul><li>类的每个实例本质上都是唯一的。对于代表活动实体而不是值(value)的类来说确实如此,例如 Thread。Object 提供的 equals 实现对于这些类来说正是正确的行为。</li><li>类没有必要提供“逻辑相等”的测试功能。例如,java.util.regex.Pattern 可以覆盖 equals,以检查两个 Pattern 实例是否代表同一个正则表达式,但是设计者并不认为客户需要或者期望这样的功能。在这类情况之下,从 Object 继承得到的 equals 实现已经足够了。</li></ul><p>超类已经覆盖了 equals,超类的行为对于这个类也是合适的。例如,大多数的 Set 实现从 AbstractSet 继承 equals 实现,List 实现从 AbstractList 继承 equals 实现,Map 实现从 AbstractMap 继承 equals 实现。</p><p>类是私有的,或者是包级私有的,可以确定它的 equals 方法永远不会被调用。如果你非常想规避风险,可以覆盖 equals 方法,确保它不会被意外调用:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">equals</span><span class="params">(Object o)</span> {</span><br><span class="line"><span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">AssertionError</span>(); <span class="comment">// Method is never called</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>那么什么时候应该覆盖 equals 方法呢?</strong></p><p>如果类具有自己特有的“逻辑相等”概念(不同于对象等同的概念),而且超类还没有覆盖 equals。这通常属于“值类”(value class)的情形。值类仅仅是一个表示值的类,例如 Integer 或者 String。程序员在利用 equals 方法来比较值对象的引用时,希望知道它们在逻辑上是否相等,而不是了解它们是否指向同一个对象。为了满足程序员的要求,不仅必须覆盖 equals 方法,而且这样做也使得这个类的实例可以用作映射表(map)的键(key),或者集合(set)的元素,使映射或者集合表现出预期的行为。</p><p><strong>在覆盖 equals 方法的时候,必须要遵守它的通用约定。下面是约定的内容,来自 Object 的规范。</strong></p><p>equals 方法实现了等价关系,其属性如下:</p><ul><li>自反性(reflexive):对于任何非 null 的引用值 x,x.equals(x) 必须返回 true。</li><li>对称性(symmetric):对于任何非 null 的引用值 x 和 y,当且仅当 y.equals(x) 返回 true 时,x.equals(y) 必须返回 true。</li><li>传递性(transitive):对于任何非 null 的引用值 x、y 和 z,如果 x.equals(y) 返回 true,并且 y.equals(z) 也返回 true,那么 x.equals(z) 也必须返回 true。</li><li>一致性(consistent):对于任何非 null 的引用值 x 和 y,只要 equals 的比较操作在对象中所用的信息没有被修改,多次调用 x.equals(y) 就会一致地返回 true,或者一致地返回 false。</li><li>对于任何非 null 的引用值 x,x.equals(null) 必须返回 false。</li></ul><p><strong>实现高质量 equals 方法的诀窍:</strong></p><ol><li>使用 == 操作符检查“参数是否为这个对象的引用”。 如果是,则返回 true。 这只不过是一种性能优化,如果比较操作有可能很昂贵,就值得这么做。</li><li>使用 instanceof 操作符检查“参数是否为正确的类型”。 如果不是,则返回 false 一般说来,所谓“正确的类型”是指equals 方法所在的那个类。 某些情况下,是指该类所实现的某个接口。 如果类实现的接口改进了 equals 约定,允许在实现了该接口的类之间进行比较,那么就使用接口。 集合接口如Set、List、Map和Map.E口try具有这样的特性。</li><li>把参数转换成正确的类型。 因为转换之前进行过且stanceof测试,所以确保会成功。</li><li>对于该类中的每个“关键”( significant )域,检查参数中的域是否与该对象中对应的域相匹配。 如果这些测试全部成功,则返回 true;否则返回 false。 如果第2步中的类型是个接口,就必须通过接口方法访问参数中的域;如果该类型是个类,也许就能够直接访问参数中的域,这要取决于它们的可访问性。</li><li>在编写完 equals 方法之后,应该问自己三个问题:它是否是对称的、传递的、一致的? 并且不要只是自问,还要编写单元测试来检验这些特性</li></ol><p><strong>下面是最后的一些告诫:</strong></p><ul><li><p>覆盖equals 时总要覆盖hashCode</p></li><li><p>不要企图让equals 方法过于智能。 如果只是简单地测试域中的值是否相等,则不难做到遵守 equals 约定。 如果想过度地去寻求各种等价关系,则很容易陷入麻烦之中。把任何一种别名形式考虑到等价的范围内,往往不会是个好主意。 例如,File类不应该试图把指向同一个文件的符号链接(symbolic link)当作相等的对象来看待。 所幸File 类没有这样做</p></li><li><p>不要将equals 声明中的 Object 对象替换为其他的类型。 比如:</p><blockquote><p>// Broken - parameter type must be Object!<br>public boolean equals(MyClass o) {<br>…<br>}</p><p>问题在于,这个方法并没有覆盖 Object. equals,因为它的参数应该 是Object 类型,相反,它重载了 Object.equals。 在正常 equals 方法的基础上,再提供一个“强类型”的 equals 方法。</p><p>增加@Override注解可以在编译前发现问题</p><p>// Still broken, but won’t compile<br>@Override<br>public boolean equals(MyClass o) {<br>…<br>}</p></blockquote></li><li><p>编写和测试 equals(及hashCode)方法都是十分繁琐的,得到的代码也很琐碎。 代替手工编写和测试这些方法的最佳途径,是使用Google开源的<code>AutoValue框架</code>,它会自动替你生成这些方法,通过类中的单个注解就能触发。 在大多数情况下,AutoValue生成的方法本质上与你亲自编写的方法是一样的。</p></li></ul><p><strong>总之,除非必须,否则不要覆盖 equals 方法:在许多情况下,从 Object 继承而来的实现正是你想要的。如果你确实覆盖了 equals,那么一定要比较类的所有重要字段,并以保留 equals 约定的所有 5 项规定的方式进行比较。</strong></p><h2 id="11-覆盖equals时总要覆盖hashCode">11. 覆盖equals时总要覆盖hashCode</h2><p><strong>在每一个覆盖了equals方法的类中,总要覆盖hashCode方法</strong>。如果不这样做的话,就会违反hashCode的通用约定,</p><blockquote><p>Object规范:</p><ul><li>在应用程序的执行期间,只要对象的equals方法的比较操作所用到的信息没有被修改,那么对同一个对象的多次调用,hashCode方法都必须始终返回同一个值。在一个应用程序与另一个程序的执行过程中,执行hashCode方法所返回的值可以不一致。</li><li>如果两个对象根据equals(Object)方法比较是相等的,那么调用这两个对象中的hashCode方法都必须产生同样的整数结果。</li><li>如果两个对象根据equals(Object)方法比较是不相等的,那么调用这两个对象中的hashCode方法,则不一定要求hashCode方法必须产生不同的结果。 但是程序员应该知道,给不相等的对象产生截然不同的整数结果,有可能提高散列表(hashtable)的性能。</li></ul></blockquote><p>因没有覆盖hashCode 而违反的关键约定是第二条:相等的对象必须具有相等的散到码( hashcode )。</p><p>比如:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Map<PhoneNumber, String> m = <span class="keyword">new</span> <span class="title class_">HashMap</span><>();</span><br><span class="line">m.put(<span class="keyword">new</span> <span class="title class_">PhoneNumber</span>(<span class="number">707</span>, <span class="number">867</span>, <span class="number">5309</span>), <span class="string">"Jenny"</span>);</span><br></pre></td></tr></table></figure><p>此时,你可能期望 <code>m.get(new PhoneNumber(707, 867,5309))</code> 返回「Jenny」,但是它返回 null。</p><blockquote><p>注意,这里涉及两个PhoneNumber实例:第一个被插入HashMap中,第二个实例与第一个相等,用于从Map中根据PhoneNumber去获取用户名字。</p><p>**由于PhoneNumber类没有覆盖hashCode方法,从而导致两个相等的实例具有不相等的散列码,违反了hashCode的约定。**因此,put方法把电话号码对象存放在一个散列桶(hash bucket)中,get方法却在另一个散列桶中查找这个电话号码。即使这两个实例正好被放到同一个散列桶中,get方法也必定会返回null,因为HashMap有一项优化,可以将与每个项相关的散列码缓存起来,如果散列码不匹配,也就不再去检验对象的等同性。</p></blockquote><p>修正这个问题十分简单,只需要给PhoneNumber类提供一个适当的hashCode方法即可。</p><p><strong>不要试图从散列码计算中排除掉一个对象的关键域来提高性能。</strong> 虽然这样得到的散列函数运行起来可能更快,但是它的效果不见得会好,可能会导致散列表慢到根本无法使用。特别是在实践中,散列函数可能面临大量的实例,在你选择忽略的区域之中,这些实例仍然区别非常大。 如果是这样,散列函数就会把所有这些实例映射到极少数的散列码上,原本应 该以线性级时间运行的程序,将会以平方级的时间运行。</p><p>**不要对hashCode方法的返回值做出具体的规定,因此客户端无法理所当然地依赖它;**这样可以为修改提供灵活性。 Java类库中的许多类,比如 String 和 Integer,都可以把 它们的 hashCode 方法返回的确切值规定为该实例值的一个函数。一般来说,这并不是个好主意,因为这样做严格地限制了在未来的版本中改进散列函数的能力。 如果没有规定散列函数的细节,那么当你发现了它的内部缺陷时,或者发现了更好的散列函数时,就可以在后面的发行版本中修正它。</p><h2 id="12-始终要覆盖toString">12. 始终要覆盖toString</h2><p>虽然Object提供了 toString 方法的一个实现,但它返回的字符串通常并不是类的用户所期望看到的。它包含类的名称,以及一个“@”符号,接着是散列码的无符号十六进制表示法,例如PhoneNumber@163b91。 toString的通用约定指出,被返回的字符串应该是一个“简洁的但信息丰富,并且易于阅读的表达形式”。</p><ul><li>遵守toString约定并不像遵守 equals 和 hashCode 的约定那么重要,但是,提供好的toString实现可以便类用起来更加舒适,使用了这个类的系统也更易于调试。</li><li>在实际应用中, toString 方法应该返回对象中包含的所有值得关注的信息。</li><li>无论是否指定返回值(字符串)格式,都应该在文档注释中明确地表明你的意图。</li><li>无论是否指定返回值(字符串)格式,都为toString返回值中包含的所有信息提供一种可以通过编程访问到的途径。 即提供方法获取toString中的某一部分数据,而不需要去解析toString。</li></ul><h2 id="13-谨慎地覆盖clone">13. 谨慎地覆盖clone</h2><p>Cloneable接口的目的是作为对象的UI个mixin接口,表明这样的对象允许克隆。但是这个接口没有定义clone方法,并且Object的clone方法是受保护的。所以,<strong>如果不借助反射,就不能仅仅因为一个对象实现了Cloneable接口,就调用clone方法。</strong></p><blockquote><p>Cloneable接口并没有包含任何方法,那么这个接口有什么作用呢?</p><p>这个接口是一个标记接口(空接口),<strong>他决定了Object中受保护的clone方法实现的行为。<strong>如果一个类实现了Cloneable接口,Object的clone方法就返回该对象的</strong>浅拷贝</strong>;如果没有实现Cloneable接口就会抛出CloneNotSupportedException异常。</p><p>【Object的clone方法被 protected 和 native 修饰 (见相关博客)】</p></blockquote><p>事实上,实现Cloneable接口的类都是为了提供一个功能适当的公有的clone方法。</p><hr><p>注意事项:</p><ul><li><p>**不可变的类永远都不应该提供clone方法。**它只会激发不必要的克隆。</p></li><li><p>clone默认是浅拷贝,如果对象的域引用了可变的对象(数组),需要重写clone方法深拷贝。</p></li><li><p>**Cloneable架构与引用可变对象的final域的正常用法是不兼容的。**clone方法被禁止给final域对象赋新值。</p></li><li><p>**公有的clone方法应该省略throws声明。**因为不会抛出受检异常的方法使用起来更加轻松。</p></li><li><p><strong>对象拷贝的更好的办法是提供一个拷贝构造器或者拷贝工厂</strong></p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Copy constructor</span></span><br><span class="line"><span class="keyword">public</span> <span class="title function_">Yum</span><span class="params">(Yum yum)</span> { ... };</span><br><span class="line"><span class="comment">// Copy factory</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> Yum <span class="title function_">newInstance</span><span class="params">(Yum yum)</span> { ... };</span><br></pre></td></tr></table></figure></li><li><p>使用clone方法克隆复杂对象的一种方法:首先,调用super.clone方法(归根结底是Object的clone方法,浅拷贝);然后把对象中的所有域(属性)设置成初始状态,然后调用子类的方法给域(属性)重新赋值。</p></li></ul><p>相关博客:<a href="https://blog.csdn.net/m0_68402491/article/details/130675660">java实现简单的克隆-CSDN博客</a></p><h2 id="14-考虑实现Comparable接口">14. 考虑实现Comparable接口</h2><p>实现Comparable接口主要是重写compareTo方法。该方法的目的不但允许进行简单的等同性比较,而且允许执行顺序比较。</p><ol><li>如果一个对象没有实现Comparable接口,或者需要使用一个非标准的排序关系,就可以使用一个显示的Comparator来代替,或者编写自己的比较器,或者使用已有的比较器。</li><li>在compareTo方法中使用关系操作符<、>(大于号、小于号)是非常繁琐的,并且容易出错,所以不建议使用。应该在装箱基本类型的类中使用静态的compare方法,或者Comparator接口中使用比较器构造方法。</li></ol><h2 id="15-使类和成员的可访问性最小化">15. 使类和成员的可访问性最小化</h2><p>区分一个组件设计的好不好,唯一重要的因素是:它对于外部的其他组件而言,是否隐藏了其内部数据和其他细节实现。好的组件会隐藏所有的实现细节,把API与实现清晰地隔离出来,组件之间只通过API进行通信,一个模块不需要知道其他模块的内部工作情况。这种设计也被称为信息隐藏或封装。</p><p>Java提供了许多机制来协助信息隐藏。<strong>访问控制</strong>机制通过访问修饰符决定类、接口和成员的可访问性。</p><p><strong>规则:</strong></p><ol><li><p>尽可能地使每个类或者成员不被外界访问</p><ul><li><p>类和接口:只有两种访问级别,包级私有(无修饰)和公有(public修饰)。<strong>如果能把类/接口做成包级私有,就需要定义为包级私有。</strong></p><blockquote><p>如果一个包级私有的顶层类/接口<strong>只在</strong>某一个类的内部被用到,就应该考虑使它成为唯一使用的类的私有嵌套类</p></blockquote></li><li><p>成员(域、方法、嵌套类、嵌套接口):私有(private)、包级私有(无修饰)、受保护的(protected修饰)、公有(public修饰)</p></li></ul></li><li><p>公有类的实例域决不能是公有的。如果公有类的实例域一旦公有,你就等于放弃了对存储在这个域中的值进行限制的能力;<strong>并且包含公有可变域的类通常并不是线程安全的。</strong></p><blockquote><p>让类具有公有的静态final数据域,或者返回这种域的访问方法,是错误的。</p></blockquote></li></ol><h2 id="16-要在公有类而非公有域中使用访问方法">16. 要在公有类而非公有域中使用访问方法</h2><p>对于公有类,他的域尽可能的私有。所以,如果需要访问域时,需要共有类提供访问方法,比如getXXX()。</p><p>不过,如果类是包级私有的,或者是私有的嵌套类,直接暴露它的数据域并没有本质的错误。</p><h2 id="17-使可变性最小化">17. 使可变性最小化</h2><p>不可变类是指其实例不能被修改的类,每个实例中包含的所有信息都必须在创建该实例的时候就提供,并且在对象的整个生命周期内固定不变。</p><p><strong>如果要使类成为不可变类,必须要遵循的规则:</strong></p><ol><li>不要提供任何会修改对象状态的方法</li><li>保证类不会被拓展(一般是用final声明)</li><li>声明所有域都是final的</li><li>声明所有域都是私有的</li><li>确保对任何可变组件的互斥访问。如果类具有指向可变对象的域,则必须确保该类的客户端无法获得指向这些对象的引用。</li></ol><p><strong>不可变类的优点:</strong></p><ol><li>不可变类比较简单,只有一种状态,即被创建时的状态。</li><li>不可变对象本质是线程安全的,他们不要求同步。</li><li>不可变对象可以被自由的共享,甚至可以共享类的内部信息。</li><li>不可变对象为其他对象提供了大量的构件。</li><li>不可变对象无偿的提供了失败的原子性,不存在临时不一致的可能性。</li></ol><p>**不可变类的缺点:**真正唯一的缺点是对于每个不同的值,都需要一个单独的对象。</p><p><strong>类创建原则:</strong></p><ol><li>除非有很好的理由要让类成为可变的类,否则他就应该是不可变的。</li><li>如果类不能被做成不可变的,仍然应该尽可能地限制他的可变性。</li><li>除非有令人信服的理由要使域变成是非final的,否则每个域都应该是private final的。</li><li>构造器应该创建完全初始化的对象,并建立起所有的约束关系。</li></ol><h2 id="18-复合优先于继承">18. 复合优先于继承</h2><p><code>本小节的继承指:一个类拓展另一个类的时候。(不考虑一个类实现一个接口或者一个接口扩展另一个接口的时候)</code></p><p>继承是实现代码重用的有利手段,但并非永远是最佳工具。</p><p>**因为和方法调用不同,继承打破了封装性。**子类依赖于其超类中特定功能的实现细节。超类的实现有可能会随着发行版本的不同而有所变化,如果真的发生了变化,子类可能会遭到破坏,即使它的代码完全没有改变。 因而,子类必须要跟着其 超类的更新而演变,除非超类是专门为了扩展而设计的,并且具有很好的文挡说明。</p><blockquote><p>只有当子类真正是超类的子类型时,才适合用继承。</p></blockquote><p>**复合:**不扩展现有类,而是在新的类中增加一个私有域,引用现有类的一个实例。新类中的每个实例方法都可以调用被包含的先有类中对应的方法,并返回它的结果。这被称为转发,新类中的方法被称为转发方法。</p><h2 id="19-要么设计继承并提供文档说明,要么禁止继承">19. 要么设计继承并提供文档说明,要么禁止继承</h2><p>对于专门为了继承而设计并且具有良好文档说明的类而言:</p><ol><li>该类必须有文档说明它可覆盖的方法的自用型</li></ol><blockquote><p>好的API文档应该描述一个给定的方法做了什么工作,而不是描述它是如何做到的。</p></blockquote><ol start="2"><li>类必须以精心挑选的受保护的方法的形式,提供适当的钩子(hook),以便进入其内部工作中。</li></ol><blockquote><p>钩子(hook):允许程序员在程序运行的不同阶段插入额外的代码,以实现对程序行为的控制和定制化。JAVA中的hook通常通过回调函数或者监听器的方式实现。当程序到达某个特定的状态或者事件发生时,钩子会触发相应的回调函数或者事件处理方法,从而执行额外的逻辑。</p></blockquote><ol start="2"><li>对于为了继承而设计的类,唯一的测试方式就是编写子类。换句话说,必须在发布类之前先编写子类对类进行测试。</li><li><strong>构造器决不能调用可被覆盖的方法。</strong></li><li>如果类实现了Cloneable或者Serializable接口,无论是clone还是readObject方法,都不可以调用可覆盖的方法,不管是以直接还是间接的方式。</li><li>对于那些并非为了安全地进行子类化而设计和编写文档的类,要禁止子类化。</li></ol><h2 id="20-接口优于抽象类">20. 接口优于抽象类</h2><p>Java提供了两种机制,可以用来定义允许多个实现的类型:接口和抽象类。并且Java 8以后,接口引入了缺省方法。这两种机制都允许为某些实例方法提供实现。</p><ol><li><p>现有的类可以很容易被更新,以实现新的接口</p></li><li><p>接口是定义mixin(混合类型)的理想选择</p><blockquote><p>混合类型:类除了实现它的“基本类型”以外,还可以实现mixin类型,以表明它提供了某些可选择的行为。</p></blockquote></li><li><p>接口允许构造非层次结构的类型框架</p></li><li><p>接口使得安全地增强类的功能成为可能(包装类模式)</p></li><li><p><strong>通过对接口提供一个抽象的骨架实现类(即抽象类实现接口,使用类继承抽象类)</strong>,可以把接口和抽象类的优点结合起来。接口负责定义类型,也可以提供一些缺省方法,抽象类则负责实现除基本类型接口方法以外的方法。(模板方法模式)</p></li><li><p>对于骨架实现类而言,好的文档绝对是非常必要的</p></li></ol><h2 id="21-为后代设计接口">21. 为后代设计接口</h2><p>在Java 8发行之前,如果不破坏现有的实现,是不可能给接口添加方法的。如果给某个接口添加了一个新的方法,就会导致编译错误。Java 8以后,增加了缺省方法构造,目的就是允许给现有的接口添加方法。</p><p>注意:</p><ol><li>并非每一个可能实现的所有变体,始终都可以编写出一个缺省方法</li><li>有了缺省方法,接口的现有实现就不会出现编译时没有报错或警告,运行时却失败的情况。</li><li>谨慎设计接口仍然是至关重要的</li></ol><h2 id="22-接口只用于定义类型">22. 接口只用于定义类型</h2><p>当类实现接口时,接口就充当可以引用这个类的实例的类型。因此,类实现了接口,就表明客户端可以对这个类的实例实施某些动作。</p><p>注意:</p><ol><li><p>常量接口模式是对接口的不良使用</p><blockquote><p>有一种接口被称为常量接口(constant interface),它不满足上面的条件。这种接口不包含任何方法,它只包含静态的final域,每个域都导出一个常量。使用这些常量的类实现这个接口,以避免用类名来修饰常量名。</p></blockquote></li><li><p>如果要导出常量类,可以把这些常量添加到这个类或者接口中,比如Integer的MAX_VALUE;如果这些常量最好被看做枚举类型的成员,就应该用枚举类型;如果不行,应该使用不可实例化的工具类来导出这些常量;如果大量利用工具类导出的常量,可以通过利用<strong>静态导入</strong>机制,避免使用类名修饰常量名。</p></li></ol><h2 id="23-类层次优于标签类">23. 类层次优于标签类</h2><p>标签类过于冗长、容易出错,并且效率低下。</p><p>标签类属于类层次的一种</p><h2 id="24-静态成员类优于非静态成员类">24. 静态成员类优于非静态成员类</h2><p>嵌套类(nested class)是指指定在另一个类的内部的类。嵌套类存在的目的应该只是为了它的外围(enclosing class)提供服务。如果嵌套类将来可能会用于其他的某个环境中,它就应该顶层类(top-level class)。嵌套类有四种:静态成员类(static member class)、非静态成员类(nonstatic member class)、匿名名(anonymous class)和局部类(local class)。除了第一种之外,其他三种都称为内部类(inner class)。</p><p>静态成员类是最简单的一种嵌套类。最好把它看作是普通类,只是碰巧被声明在另一个类的内部而已,它可以访问外围的所有成员,包括那些声明为私有的成员。静态成员类是外围的一个静态成员,与其他的静态成员一样,也遵守同样的可访问性规则。如果它被声明为私有的,它就只能在外围类的内部才能被访问,等等。静态成员类的一种常见用法是作为公有的辅助类,只有与它的外部类一起使用才有意义。</p><p>非静态成员类的一种常见用法是定义一个Adapter,它允许外部类的实例被看作是另一个不相关的类的实例。 如果声明成员类不要求访问外围实例,就要始终把修饰符 static 放在它的声明中, 使它应当把嵌套类声明成静态成员类,而不是非静态成员类。如果省略了 static 修饰符,则每个实例都将包含一个额外的指向外围对象的引用。如前所述,保存这份引用要消耗时间和空间,并且会导致外围实例在符合垃圾回收时仍然得以保留。由此造成的内存泄漏可能是灾难性的。但是常常难以发现,因为这个引用是不可见的。</p><p>如果相关的类是导出类的公有或受保护的成员,毫无疑问,在静态和非静态成员类之间做出正确的选择是非常重要的。在这种情况下,该成员类就是导出的 API 元素,在后续的发行版本中,如果不违背向后兼容性,就无法从非静态成员类变为静态成员类。</p><p>匿名类是没有名字的。它不是外围类的一个成员。它并非与其他的成员一起被声明,而是在使用的同时被声明和实例化。匿名类可以出现在代码中任何允许存在表达式的地方。当且仅当匿名类出现在非静态的环境中时,它才拥有外围实例。但是即使它们出现在静态的环境中,也不可能拥有任何静态成员,而是拥有常数变量(constant variable),常数变量是 final 基本类型,或者是被初始化为常量表达式[JLS, 4.12.4]的字符串字面量。</p><p>匿名类的运用受到诸多的限制。除了在它们被声明的时候之外,是无法将它们实例化的。不能执行 instanceof 测试,或者做任何需要命名类的其他事情。无法声明一个匿名类来实现多个接口,或者扩展一个类,并同时扩展类和实现接口。除了从超类型继承得到之外,匿名类的客户端无法调用任何成员。由于匿名类出现在表达式中,它们必须保持简短(大约 10 行或者更少),否则会影响程序的可读性。</p><p>局部类是四种嵌套类中使用最少的类。在任何“可以声明局部变量”的地方,都可以声明局部类,并且局部类也遵守同样的作用域规则。局部类与其他三种嵌套类中的每一种都有一些共同的属性。与成员类一样,局部类有名字,可以被重复使用。与匿名类一样,只有当局部类是在非静态态环境中定义的时候,才有外围实例,它们也不能包含静态成员。与匿名类一样,它们必须非常简短以便不会影响可读性。</p><p>总而言之,共有四种不同的嵌套类,每一种都有自己的用途。如果一个嵌套类需要在单个方法之外仍然是可见的,或者它太长了,不适合放在方法内部,就应该使用成员类。如果成员类的每个实例都需要一个指向其外围实例的引用,就把成员类做成非静态的;否则,就做成静态的。假设这个嵌套类属于一个方法的内部,如果你只需要在一个地方创建实例,并且已经有了一个预设的类型可以说明这个类的特征,就把它做成匿名类;否则,就做成分部类。</p><h2 id="25-限制源文件为单个顶级类">25. 限制源文件为单个顶级类</h2><p>永远不要把多个顶级类或者接口放在一个源文件中。遵循这个规则可以确保编译时一个类不会有多重定义。这么做反过来也能确保编译产生的类文件,以及程序的结果的行为,都不会受到源文件被传送给编译器时的顺序的影响。</p>]]></content>
<summary type="html">Effective Java书籍第三、四章笔记整理</summary>
<category term="读书inG" scheme="https://jovehawking.fun/categories/%E8%AF%BB%E4%B9%A6inG/"/>
<category term="读书inG" scheme="https://jovehawking.fun/tags/%E8%AF%BB%E4%B9%A6inG/"/>
<category term="Effective Java" scheme="https://jovehawking.fun/tags/Effective-Java/"/>
</entry>
<entry>
<title>读书-Effective Java系列(二)</title>
<link href="https://jovehawking.fun/posts/fee147ff.html"/>
<id>https://jovehawking.fun/posts/fee147ff.html</id>
<published>2024-07-16T23:22:51.000Z</published>
<updated>2024-08-24T01:03:30.945Z</updated>
<content type="html"><![CDATA[<h1>一</h1><h2 id="1-以静态工厂方法代替构造函数">1. 以静态工厂方法代替构造函数</h2><p>客户端获取类实例的方法:</p><ol><li>构造器</li><li>静态工厂方法</li></ol><p>推荐使用静态工厂方法,而不是构造器方法。</p><p>静态工厂优点:</p><ol><li><p>静态工厂方法有确切的名称,构造器只能是类名</p></li><li><p>静态工厂方法可以通过不需要在每次调用时创建新的对象。类似于享元模式</p></li><li><p>静态工厂方法可以获取返回类型的子类对象</p></li><li><p>静态工厂方法返回对象的类可以随调用的不同而变化,作为输入参数的函数</p><blockquote><p>比如:</p><p>构造方法:Map<String, List<String>> m = new HasMap<String, List<String>>();</p><p>静态工厂方法:</p><p>public static <K, V> HashMap<K, V> newInstance() {</p><p>return new HashMap<K, V>();</p><p>}</p></blockquote></li></ol><p>静态工厂缺点:</p><ol><li><p>没有公共或受保护构造函数的类不能被子类化</p></li><li><p>和其他静态方法实际上没有任何区别</p><blockquote><p>为了加以区别,静态工厂使用一些惯用名称</p><ul><li>valueOf:该方法返回的实例和参数的数值相同。实际上是一种类型转换方法</li><li>of:valueOf的简洁替代</li><li>getInstance:返回的实例通过参数来描述。对于单例模式,该方法没有参数并返回唯一实例</li><li>newInstance:多例模式,每次返回新值</li><li>getType:和getInstance类似,但是在工厂方法处于不同的类中使用</li><li>newType:和newInstance类似,但是在工厂方法处于不同的类中使用</li></ul></blockquote></li></ol><h2 id="2-遇到多个构造器参数时考虑用构建器">2. 遇到多个构造器参数时考虑用构建器</h2><p>静态工厂和构造器有一个共同的缺陷,他们都不能很好的扩展到大量的可选参数。如果一个类新建有大量的可选参数,通常采用<strong>重叠构造器模式</strong>,多个构造器。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Telescoping constructor pattern - does not scale well!</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">NutritionFacts</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> servingSize; <span class="comment">// (mL) required</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> servings; <span class="comment">// (per container) required</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> calories; <span class="comment">// (per serving) optional</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> fat; <span class="comment">// (g/serving) optional</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> sodium; <span class="comment">// (mg/serving) optional</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> carbohydrate; <span class="comment">// (g/serving) optional</span></span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">NutritionFacts</span><span class="params">(<span class="type">int</span> servingSize, <span class="type">int</span> servings)</span> {</span><br><span class="line"> <span class="built_in">this</span>(servingSize, servings, <span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">NutritionFacts</span><span class="params">(<span class="type">int</span> servingSize, <span class="type">int</span> servings, <span class="type">int</span> calories)</span> {</span><br><span class="line"> <span class="built_in">this</span>(servingSize, servings, calories, <span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">NutritionFacts</span><span class="params">(<span class="type">int</span> servingSize, <span class="type">int</span> servings, <span class="type">int</span> calories, <span class="type">int</span> fat)</span> {</span><br><span class="line"> <span class="built_in">this</span>(servingSize, servings, calories, fat, <span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">NutritionFacts</span><span class="params">(<span class="type">int</span> servingSize, <span class="type">int</span> servings, <span class="type">int</span> calories, <span class="type">int</span> fat, <span class="type">int</span> sodium)</span> {</span><br><span class="line"> <span class="built_in">this</span>(servingSize, servings, calories, fat, sodium, <span class="number">0</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">NutritionFacts</span><span class="params">(<span class="type">int</span> servingSize, <span class="type">int</span> servings, <span class="type">int</span> calories, <span class="type">int</span> fat, <span class="type">int</span> sodium, <span class="type">int</span> carbohydrate)</span> {</span><br><span class="line"> <span class="built_in">this</span>.servingSize = servingSize;</span><br><span class="line"> <span class="built_in">this</span>.servings = servings;</span><br><span class="line"> <span class="built_in">this</span>.calories = calories;</span><br><span class="line"> <span class="built_in">this</span>.fat = fat;</span><br><span class="line"> <span class="built_in">this</span>.sodium = sodium;</span><br><span class="line"> <span class="built_in">this</span>.carbohydrate = carbohydrate;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当创建实例的时候,就需要利于利用参数列表最短的构造器,但是这个列表包含了要设置的所有参数。</p><blockquote><p>重叠构造器模式可行,但是当有许多参数的时候,客户端代码会很难编写,并且仍然难以阅读。如果客户端颠倒参数的位置,有可能编译器也不会报错,但是程序运行时错误。</p></blockquote><p>替代方法二:<strong>JavaBeans模式</strong>。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// JavaBeans Pattern - allows inconsistency, mandates mutability</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">NutritionFacts</span> {</span><br><span class="line"> <span class="comment">// Parameters initialized to default values (if any)</span></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">servingSize</span> <span class="operator">=</span> -<span class="number">1</span>; <span class="comment">// Required; no default value</span></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">servings</span> <span class="operator">=</span> -<span class="number">1</span>; <span class="comment">// Required; no default value</span></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">calories</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">fat</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">sodium</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">carbohydrate</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">NutritionFacts</span><span class="params">()</span> { }</span><br><span class="line"> <span class="comment">// Setters</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setServingSize</span><span class="params">(<span class="type">int</span> val)</span> { servingSize = val; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setServings</span><span class="params">(<span class="type">int</span> val)</span> { servings = val; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setCalories</span><span class="params">(<span class="type">int</span> val)</span> { calories = val; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setFat</span><span class="params">(<span class="type">int</span> val)</span> { fat = val; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setSodium</span><span class="params">(<span class="type">int</span> val)</span> { sodium = val; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setCarbohydrate</span><span class="params">(<span class="type">int</span> val)</span> { carbohydrate = val; }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>先调用一个无参构造器创建对象,然后调用setter方法设置每个必要的参数。</p><blockquote><p>JavaBeans模式自身有很严重的缺点</p><ol><li>因为构造过程被分到了几个调用中,<strong>在构造的过程中JavaBeans可能处于不一致的状态</strong>。</li><li>JavaBeans模式阻止了把类做成不可变的可能,需要额外的处理保证线程的安全。</li></ol></blockquote><p>最佳替代:构建器(Builder模式)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Builder Pattern</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">NutritionFacts</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> servingSize;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> servings;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> calories;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> fat;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> sodium;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> carbohydrate;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">Builder</span> {</span><br><span class="line"> <span class="comment">// Required parameters</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> servingSize;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> servings;</span><br><span class="line"> <span class="comment">// Optional parameters - initialized to default values</span></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">calories</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">fat</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">sodium</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">carbohydrate</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Builder</span><span class="params">(<span class="type">int</span> servingSize, <span class="type">int</span> servings)</span> {</span><br><span class="line"> <span class="built_in">this</span>.servingSize = servingSize;</span><br><span class="line"> <span class="built_in">this</span>.servings = servings;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Builder <span class="title function_">calories</span><span class="params">(<span class="type">int</span> val)</span> {</span><br><span class="line"> calories = val;</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Builder <span class="title function_">fat</span><span class="params">(<span class="type">int</span> val)</span> {</span><br><span class="line"> fat = val;</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Builder <span class="title function_">sodium</span><span class="params">(<span class="type">int</span> val)</span> {</span><br><span class="line"> sodium = val;</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Builder <span class="title function_">carbohydrate</span><span class="params">(<span class="type">int</span> val)</span> {</span><br><span class="line"> carbohydrate = val;</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> NutritionFacts <span class="title function_">build</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">NutritionFacts</span>(<span class="built_in">this</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">NutritionFacts</span><span class="params">(Builder builder)</span> {</span><br><span class="line"> servingSize = builder.servingSize;</span><br><span class="line"> servings = builder.servings;</span><br><span class="line"> calories = builder.calories;</span><br><span class="line"> fat = builder.fat;</span><br><span class="line"> sodium = builder.sodium;</span><br><span class="line"> carbohydrate = builder.carbohydrate;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//----------------使用----------------------</span></span><br><span class="line"><span class="type">NutritionFacts</span> <span class="variable">cocaCola</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">NutritionFacts</span>.Builder(<span class="number">240</span>, <span class="number">8</span>)</span><br><span class="line">.calories(<span class="number">100</span>).sodium(<span class="number">35</span>).carbohydrate(<span class="number">27</span>).build();</span><br></pre></td></tr></table></figure><p>不直接生成对象,而是让客户端利用所有必要的参数调动构造器(静态工厂),得到一个builder对象,然后在builder对象上调用类似setter的方法,最后调用无参的builder()方法生成不可变对象。builder类是它创建类的静态成员类。</p><blockquote><p>优点:builder模式十分灵活,可以利用单个builder创建多个对象,可以自动填充某些属性。</p><p>缺点:性能开销</p></blockquote><h2 id="3-用私有构造器或者枚举类型强化Singleton属性">3. 用私有构造器或者枚举类型强化Singleton属性</h2><p>Singleton单例是一个只实例化一次的类。</p><p>实现单例有两种常见的方法。两者都基于保持构造函数私有和导出公共静态成员以提供对唯一实例的访问。</p><ol><li>在第一种方法中,成员是一个 final 字段:</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Singleton with public final field</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Elvis</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Elvis</span> <span class="variable">INSTANCE</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Elvis</span>();</span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">Elvis</span><span class="params">()</span> { ... }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">leaveTheBuilding</span><span class="params">()</span> { ... }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>私有构造函数只调用一次,用于初始化 public static final 修饰的 Elvis 类型字段 INSTANCE。不使用 public 或 protected 的构造函数保证了「独一无二」的空间:一旦初始化了 Elvis 类,就只会存在一个 Elvis 实例,不多也不少。客户端所做的任何事情都不能改变这一点,但有一点需要注意:拥有特殊权限的客户端可以借助 AccessibleObject.setAccessible 方法利用反射调用私有构造函数。如果需要防范这种攻击,请修改构造函数,使其在请求创建第二个实例时抛出异常。</p><p><strong>使用 <code>AccessibleObject.setAccessible</code> 方法调用私有构造函数示例:</strong></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">Constructor<?>[] constructors = Elvis.class.getDeclaredConstructors();</span><br><span class="line">AccessibleObject.setAccessible(constructors, <span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line">Arrays.stream(constructors).forEach(name -> {</span><br><span class="line"> <span class="keyword">if</span> (name.toString().contains(<span class="string">"Elvis"</span>)) {</span><br><span class="line"> <span class="type">Elvis</span> <span class="variable">instance</span> <span class="operator">=</span> (Elvis) name.newInstance();</span><br><span class="line"> instance.leaveTheBuilding();</span><br><span class="line"> }</span><br><span class="line">});</span><br></pre></td></tr></table></figure><ol start="2"><li>在实现单例的第二种方法中,公共成员是一种静态工厂方法</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Singleton with static factory</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Elvis</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Elvis</span> <span class="variable">INSTANCE</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Elvis</span>();</span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">Elvis</span><span class="params">()</span> { ... }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Elvis <span class="title function_">getInstance</span><span class="params">()</span> { <span class="keyword">return</span> INSTANCE; }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">leaveTheBuilding</span><span class="params">()</span> { ... }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>所有对 <code>getInstance()</code> 方法的调用都返回相同的对象引用,并且不会创建其他 Elvis 实例(与前面提到的警告相同)。</p><p>静态工厂方法的一个优点是,它可以在不更改 API 的情况下决定类是否是单例。工厂方法返回唯一的实例,但是可以对其进行修改,为调用它的每个线程返回一个单独的实例。第二个优点是,如果应用程序需要的话,可以编写泛型的单例工厂。使用静态工厂的最后一个优点是方法引用能够作为一个提供者,例如 <code>Elvis::getInstance</code> 是 <code>Supplier<Elvis></code> 的提供者。除非能够与这些优点沾边,否则使用 public 字段的方式更可取。</p><p><strong>但是要使单例类使用这两种方法中的任何一种实现可序列化,仅仅在其声明中添加实现 serializable 是不够的</strong>。</p><p> 要维护单例保证,应声明所有实例字段为 transient,并提供 readResolve 方法。否则,每次反序列化实例时,都会创建一个新实例,在我们的示例中,这会导致出现虚假的 Elvis。为了防止这种情况发生,将这个 readResolve 方法添加到 Elvis 类中:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// readResolve method to preserve singleton property</span></span><br><span class="line"><span class="keyword">private</span> Object <span class="title function_">readResolve</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// Return the one true Elvis and let the garbage collector</span></span><br><span class="line"> <span class="comment">// take care of the Elvis impersonator.</span></span><br><span class="line"> <span class="keyword">return</span> INSTANCE;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><ol start="3"><li>从1.5开始,实现Singleton还可以编写一个包含单个元素的枚举类型</li></ol><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Enum singleton - the preferred approach</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">enum</span> <span class="title class_">Elvis</span> {</span><br><span class="line"> INSTANCE;</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">leaveTheBuilding</span><span class="params">()</span> { ... }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种方法类似于 public 字段方法,但是它更简洁,默认提供了序列化机制,提供了对多个实例化的严格保证,即使面对复杂的序列化或反射攻击也是如此。这种方法可能有点不自然,但是<strong>单元素枚举类型通常是实现单例的最佳方法。</strong></p><blockquote><p>注意:如果你的单例必须扩展一个超类而不是 Enum(尽管你可以声明一个 Enum 来实现接口),你就不能使用这种方法。</p><hr><p>拓展:<a href="https://www.yisu.com/ask/53336890.html">如何使用java枚举实现单例模式 - 问答 - 亿速云 (yisu.com)</a></p></blockquote><h2 id="4-通过私有构造器强化不可实例化的能力">4. 通过私有构造器强化不可实例化的能力</h2><p>对于某些类比如<code>java.lang.Math、java.util.Arrays</code>等工具类,不希望被实例化,因为实例化没有任何意义。然而如果在缺少显示构造器的情况下,编译器会自动补全一个公有、无参的缺省(default)构造器。</p><blockquote><p>同时,<strong>虽然将类声明成抽象类后就不能实例化,不能将不希望被实例化的类做成抽象类</strong>。因为该类可以被子类化,子类可以被实例化。这种做法会误导用户,以为这种类是专门为了继承而设计的。</p></blockquote><p>我们可以将类声明一个私有的构造器,他就不能被实例化。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Noninstantiable utility class</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">UtilityClass</span> {</span><br><span class="line"> <span class="comment">// Suppress default constructor for noninstantiability</span></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">UtilityClass</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">AssertionError</span>();</span><br><span class="line"> } ... <span class="comment">// Remainder omitted</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因为显式构造函数是私有的,所以在类之外是不可访问的。AssertionError 不是严格要求的,但是它提供了保障,以防构造函数意外地被调用。它保证类在任何情况下都不会被实例化。这个习惯用法有点违反常规,因为构造函数是明确提供的,但不能调用它。因此,最好在代码中增加注释。</p><blockquote><p>缺点:使一个类不能被子类化。</p><p>因为所有的构造器都必须显示或者隐式的调用超类(super)构造器,这种情况下,子类就没有可访问的超类构造器可调用。</p></blockquote><h2 id="5-优先考虑依赖注入来引入资源">5. 优先考虑依赖注入来引入资源</h2><p>有许多类会依赖一个或多个底层的资源。<strong>不要用Singleton和静态工具类来实现依赖一个或多个底层资源的类;也不要直接用这个类来创建这些资源。应该将这些资源或者工厂传给构造器(或者静态工厂,或者构建器),通过他们来创建类。这种方式也叫作<code>依赖注入</code>。</strong></p><p>以拼写检查程序为例,拼写检查依赖字典。常见的做法是将该类作为静态实用工具类</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Inappropriate use of static utility - inflexible & untestable!</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpellChecker</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Lexicon</span> <span class="variable">dictionary</span> <span class="operator">=</span> ...;</span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">SpellChecker</span><span class="params">()</span> {} <span class="comment">// Noninstantiable</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isValid</span><span class="params">(String word)</span> { ... }</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> List<String> <span class="title function_">suggestions</span><span class="params">(String typo)</span> { ... }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>或者是其单例实现</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Inappropriate use of singleton - inflexible & untestable!</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpellChecker</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">Lexicon</span> <span class="variable">dictionary</span> <span class="operator">=</span> ...;</span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">SpellChecker</span><span class="params">(...)</span> {}</span><br><span class="line"> <span class="keyword">public</span> <span class="type">static</span> <span class="variable">INSTANCE</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SpellChecker</span>(...);</span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">isValid</span><span class="params">(String word)</span> { ... }</span><br><span class="line"> <span class="keyword">public</span> List<String> <span class="title function_">suggestions</span><span class="params">(String typo)</span> { ... }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>缺点:他们都假设只使用了一个字典,但是在实际应用中,每种语言都有自己的字典,特殊的字典用于特殊的词汇表。</p><hr><p>可以尝试让SpellChecker 支持多个字典:</p><p>首先,取消 dictionary 字段的 final 修饰,并在现有的拼写检查器中添加更改 dictionary 的方法。但是在<code>并发环境</code>中这种做法是笨拙的、容易出错的和不可行的。<strong>静态实用工具类和单例不适用于由底层资源参数化的类。</strong></p><p>其次,需要的是能够支持类的多个实例(拼写检查器),每一个实例都是用客户端指定的资源(字典)。满足该需求最简单的模式就是,**当创建一个新的实例时,就将该资源传到构造器中。这种形式也是<code>依赖注入</code>的一种。**词典是拼写检查器的一个依赖,在创建拼写检查器的时候就将词典注入其中。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Dependency injection provides flexibility and testability</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SpellChecker</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Lexicon dictionary;</span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">SpellChecker</span><span class="params">(Lexicon dictionary)</span> {</span><br><span class="line"> <span class="built_in">this</span>.dictionary = Objects.requireNonNull(dictionary);</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">isValid</span><span class="params">(String word)</span> { ... }</span><br><span class="line"> <span class="keyword">public</span> List<String> <span class="title function_">suggestions</span><span class="params">(String typo)</span> { ... }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p>**依赖注入同样适用于构造器、静态工厂和构建器。**这种模式另一种变体就是将资源工厂传递给构造器,工厂是可以被重复调用来创建类型实例的一个对象。这类工厂具体表现为工厂方法。</p><blockquote><p>在Java 8中增加的接口<code>Supplier<T></code>,最适合用于表示工厂。带有 <code>Supplier<T></code> 的方法通常应该使用有界通配符类型来约束工厂的类型参数,以允许客户端传入创建指定类型的任何子类型的工厂。</p></blockquote><p>比如,下面一个生产马赛克的方法,利用客户端提供的工厂来生产每一篇马赛克。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Mosaic <span class="title function_">create</span><span class="params">(Supplier<? extends Tile> tileFactory)</span> { ... }</span><br></pre></td></tr></table></figure><hr><p>尽管依赖注入极大地提高了灵活性和可测试性,但它可能会使大型项目变得混乱,这些项目通常包含数千个依赖项。通过使用依赖注入框架(如 Dagger、Guice 或 Spring),几乎可以消除这种混乱。这些框架的使用超出了本书的范围,<strong>但是请注意,设计成手动依赖注入的API,一般都适用于这些框架。</strong></p><h2 id="6-避免创建不必要的对象">6. 避免创建不必要的对象</h2><p><strong>一般来说,最好能重用对象而不是在每次需要的时候就创建一个功能相同的新对象。</strong></p><figure class="highlight dart"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">String</span> s = <span class="keyword">new</span> <span class="built_in">String</span>(<span class="string">"bikini"</span>); <span class="comment">// DON'T DO THIS!‘</span></span><br><span class="line"><span class="built_in">String</span> s = <span class="string">"bikini"</span>;</span><br></pre></td></tr></table></figure><p>第一条语句每次执行时都会创建一个新的 String 实例,而这些对象创建都不是必需的。String 构造函数的参数 <code>("bikini")</code> 本身就是一个 String 实例,在功能上与构造函数创建的所有对象相同。如果这种用法发生在循环或频繁调用的方法中,创建大量 String 实例是不必要的。</p><p>第二条语句使用单个 String 实例,而不是每次执行时都创建一个新的实例。此外,可以保证在同一虚拟机中运行的其他代码都可以复用该对象,只要恰好包含相同的字符串字面量</p><hr><p><strong>对于同时提供静态工厂方法和构造器的不可变类,通常可以使用静态工厂方法而不是构造器,以避免创建不必要的对象</strong></p><p>因为构造器每一次在被调用的时候都会创建一个新的对象,而静态工厂方法则从来不要求这样做。</p><hr><p><strong>有些对象创建的成本比其他对象要高得多,如果重复使用这类“昂贵的对象”,建议将它缓存下来重用。</strong></p><p>比如,使用一个正则表达式:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Performance can be greatly improved!</span></span><br><span class="line"><span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isRomanNumeral</span><span class="params">(String s)</span> {</span><br><span class="line"> <span class="keyword">return</span> s.matches(<span class="string">"^(?=.)M*(C[MD]|D?C{0,3})"</span> + <span class="string">"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"</span>);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这个问题在于String#matches方法。这个方法每次调用都会在内部为正则表达式创建一个Pattern实例(需要将正则表达式编译成一个有限状态机),但是只使用了一次就回收,成本很高。</p><p>为了提升性能,需要显示地将正则表达式编译成一个Pattern实例,让它成为类初始化的一部分,并缓存起来、</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Reusing expensive object for improved performance</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RomanNumerals</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Pattern</span> <span class="variable">ROMAN</span> <span class="operator">=</span> Pattern.compile(<span class="string">"^(?=.)M*(C[MD]|D?C{0,3})"</span> + <span class="string">"(X[CL]|L?X{0,3})(I[XV]|V?I{0,3})$"</span>);</span><br><span class="line"> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">isRomanNumeral</span><span class="params">(String s)</span> {</span><br><span class="line"> <span class="keyword">return</span> ROMAN.matcher(s).matches();</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">即 变量.matches(常量) 变为 常量.matcher(变量).matches()</span><br></pre></td></tr></table></figure><p>如果RomanNumerals类被初始化但是isRomanNumeral方法没有被使用过,则ROMAN对象理论上将不需要初始化。可以使用懒加载ROMAN对象。<code>但是,不建议懒加载ROMAN,会使方法实现更加复杂。</code></p><blockquote><p>类加载通常指的是类的生命周期中加载、连接、初始化三个阶段。当方法没有在类加载过程中被使用时,可以不初始化与之相关的字段</p></blockquote><p>当一个对象是不可变的,很明显,它可以安全地复用,但在其他情况下,它远不那么明显,甚至违反直觉。考虑适配器的情况,也称为视图。适配器是委托给支持对象的对象,提供了一个替代接口。因为适配器的状态不超过其支持对象的状态,所以<strong>不需要为给定对象创建一个给定适配器的多个实例。</strong></p><p>例如,Map 接口的 keySet 方法返回 Map 对象的 Set 视图,其中包含 Map 中的所有键。天真的是,对 keySet 的每次调用都必须创建一个新的 Set 实例,但是对给定 Map 对象上的 keySet 的每次调用都可能返回相同的 Set 实例。虽然返回的 Set 实例通常是可变的,但所有返回的对象在功能上都是相同的:当返回的对象之一发生更改时,所有其他对象也会发生更改,因为它们都由相同的 Map 实例支持。虽然创建 keySet 视图对象的多个实例基本上是无害的,但这是不必要的,也没有好处。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// keySet源码</span></span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 获取HashMap的键的集合,以Set<K>保存</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 返回key的集合</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> Set<K> <span class="title function_">keySet</span><span class="params">()</span> {</span><br><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> 说明:</span></span><br><span class="line"><span class="comment">1.可以看到其实该方法中,并没有将HashMap中的键添加到Set集合中,那么是如何实现的呢?</span></span><br><span class="line"><span class="comment">2.但实际上,我们访问Set集合,根本就无法通过索引,而是需要通过迭代器Iterator才能访问到元素,foreach本质上也是迭代器</span></span><br><span class="line"><span class="comment">3.这里的 ks 就仅仅只是一个Set引用,指向HashMap内部类KeySet的一个实例,重点在于该实例拥有自己的迭代器,当我们在使用增强for循环时才会调用该迭代器,也才会输出我们想要的东西</span></span><br><span class="line"><span class="comment">*/</span></span><br><span class="line"> Set<K> ks = keySet;</span><br><span class="line"> <span class="keyword">if</span> (ks == <span class="literal">null</span>) {</span><br><span class="line"> ks = <span class="keyword">new</span> <span class="title class_">KeySet</span>();</span><br><span class="line"> keySet = ks;</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> ks;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line">来源:https:<span class="comment">//blog.csdn.net/cnds123321/article/details/113791846</span></span><br></pre></td></tr></table></figure><p>另一种创建不必要对象的方法是自动装箱,它允许程序员混合基本类型和包装类型,根据需要自动装箱和拆箱。<strong>自动装箱模糊了基本类型和包装类型之间的区别,</strong> 两者有细微的语义差别和不明显的性能差别。考虑下面的方法,它计算所有正整数的和。为了做到这一点,程序必须使用 long,因为 int 值不够大,不足以容纳所有正整数值的和:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// Hideously slow! Can you spot the object creation?</span></span><br><span class="line"><span class="keyword">private</span> <span class="keyword">static</span> <span class="type">long</span> <span class="title function_">sum</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Long</span> <span class="variable">sum</span> <span class="operator">=</span> <span class="number">0L</span>;</span><br><span class="line"> <span class="keyword">for</span> (<span class="type">long</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">0</span>; i <= Integer.MAX_VALUE; i++)</span><br><span class="line"> sum += i;</span><br><span class="line"> <span class="keyword">return</span> sum;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>教训很清楚:<strong>基本类型优于包装类,还应提防意外的自动装箱。</strong></p><blockquote><p>本条目不应该被曲解为是在暗示创建对象是成本昂贵的,应该避免。相反,创建和回收这些小对象的构造函数成本是很低廉的,尤其是在现代 JVM 实现上。创建额外的对象来增强程序的清晰性、简单性或功能通常是件好事。</p><p>相反,通过维护自己的对象池来避免创建对象不是一个好主意,除非池中的对象非常重量级。证明对象池是合理的对象的典型例子是数据库连接。建立连接的成本非常高,因此复用这些对象是有意义的。然而,一般来说,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代 JVM 实现具有高度优化的垃圾收集器,在轻量级对象上很容易胜过这样的对象池。</p></blockquote><p>当前项的描述是:「在应该复用现有对象时不要创建新对象」,而 Item 50 的描述则是:「在应该创建新对象时不要复用现有对象」。请注意,当需要进行防御性复制时,复用对象所受到的惩罚远远大于不必要地创建重复对象所受到的惩罚。在需要时不制作防御性副本可能导致潜在的 bug 和安全漏洞;而不必要地创建对象只会影响样式和性能。</p><h2 id="7-消除过期的对象引用">7. 消除过期的对象引用</h2><p>即使Java有内存管理机制,对于开发者来说仍需要考虑内存管理的问题。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.util.Arrays;</span><br><span class="line"><span class="keyword">import</span> java.util.EmptyStackException;</span><br><span class="line"></span><br><span class="line"><span class="comment">// Can you spot the "memory leak"?</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Stack</span> {</span><br><span class="line"> <span class="keyword">private</span> Object[] elements;</span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">size</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">int</span> <span class="variable">DEFAULT_INITIAL_CAPACITY</span> <span class="operator">=</span> <span class="number">16</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Stack</span><span class="params">()</span> {</span><br><span class="line"> elements = <span class="keyword">new</span> <span class="title class_">Object</span>[DEFAULT_INITIAL_CAPACITY];</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">push</span><span class="params">(Object e)</span> {</span><br><span class="line"> ensureCapacity();</span><br><span class="line"> elements[size++] = e;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Object <span class="title function_">pop</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (size == <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">EmptyStackException</span>();</span><br><span class="line"> <span class="keyword">return</span> elements[--size];</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * Ensure space for at least one more element, roughly</span></span><br><span class="line"><span class="comment"> * doubling the capacity each time the array needs to grow.</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">ensureCapacity</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (elements.length == size)</span><br><span class="line"> elements = Arrays.copyOf(elements, <span class="number">2</span> * size + <span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面这段代码没有明显的错误。但是程序中隐藏着一个问题,可能会存在“内存泄露”。<code>如果一个栈先是增长(push),然后收缩(pop),被栈弹出的对象不会被当做垃圾回收。(因为这个弹出只是逻辑上的弹出,弹出的对象仍然在数组中)。</code>所以存在内存泄露的风险。这种引用也被称为过期引用</p><blockquote><p>过期引用:本文的过期引用指逻辑上应该被清空,但是物理上还存在的引用。</p></blockquote><p>这类问题的修复很简单,一旦对象引用过期,只需要手动清空这些引用即可。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> Object <span class="title function_">pop</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (size == <span class="number">0</span>)</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">EmptyStackException</span>();</span><br><span class="line"> <span class="type">Object</span> <span class="variable">result</span> <span class="operator">=</span> elements[--size];</span><br><span class="line"> elements[size] = <span class="literal">null</span>; <span class="comment">// Eliminate obsolete reference</span></span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>清空过期引用有另一个好处:如果这些过期引用又被错误的使用,程序就会立刻抛出NullPointerException异常,而不是悄悄的错误运行下去。</p><p>但是<code>也没有必要过分小心:对于每一个对象引用,一旦程序不再使用,就把它清空</code>。因为这样会把代码弄得很乱,后期也不容易维护。**清空对象引用应该是一种例外,而不是一种规范的行为。**消除过期引用最好的方法就是让包含该引用的变量结束其生命周期。</p><p><strong>但为何上述Stack存在内存泄露的风险?</strong></p><p>简单地说,它管理自己的内存。存储池包含元素数组的元素(element, 对象引用单元,而不是对象本身)。数组的活动部分(size)中的元素被分配,而数组其余部分中的元素是空闲的。垃圾收集器没有办法知道这一点;对于垃圾收集器,元素数组中的所有对象引用都同样有效。只有程序员知道数组的非活动部分不重要。只要数组元素成为非活动部分的一部分,程序员就可以通过手动清空数组元素,有效地将这个事实传递给垃圾收集器。</p><p><strong>注意事项:</strong></p><p>所以,**只要类是自己管理内存,程序员就应该警惕内存泄露问题。**一旦元素被释放掉,则该元素中包含的任何对象引用都应该被清空。</p><p>同时,**也要注意缓存,因为内存泄露的另一个常见来源是缓存。**一旦将对象引用放入缓存中,就很容易忘记它就在那里,并且后续不在使用的时候仍在留在缓冲中一段时间。</p><blockquote><p>避免方式:</p><ol><li><p>可以在缓存中使用弱引用,保证缓存的自动清除。当缓存对象只有缓存的弱引用时,就会在下一个垃圾回收时回收。</p></li><li><p>增加一个后台线程(比如ScheduledThreadPoolExecutor),或者在缓存添加新条目的时候顺带清除掉没用的对象。</p></li></ol></blockquote><p>还有,**缓存泄露的第三个常见来源是监听器和其他回调。**比如,实现了一个API,客户端在这个API中注册了回调,但是没有显示的取消注册,那么除非采取某些动作,否则他们就会不断的堆积起来。</p><blockquote><p>避免方式:</p><p>确保回调立即被当做垃圾回收的最佳方式就是只保存它们的弱引用,比如只将它们保存成WeakHashMap中的键。</p></blockquote><p>最后,可以考虑借助Heap剖析工具(Heap profiler)发现内存泄露问题。</p><h2 id="8-避免使用终结方法和清除方法">8. 避免使用终结方法和清除方法</h2><blockquote><p>本小节内所表述的使用终结方法、清除方法是指像普通方法一样使用,不作为安全网时使用。</p></blockquote><p>终结方法通常是不可预测的,一般情况下不建议使用。及时在Java 9中用清除方法代替了终结方法,但是仍是不可预测、运行缓慢的。</p><p>类的终结方法、清除方法的缺点:</p><ol><li><p>不能保证会被及时执行。从一个对象变得不可到达开始,到他的终结方法被执行,所花费的这段时间是任意长的。<strong>注重时间的任务不应该由终结方法或者清除方法来完成</strong>。比如文件类型。</p><p>Java语言规范不仅不保证终结方法或者清除方法会被及时的执行,甚至根本就不保证他们会被执行。有一种可能:当程序终止的时候,某些对象的终结方法还没有被执行。<strong>永远不应该依赖终结方法或者清除方法来更新重要的持久状态。</strong></p><blockquote><p>即使Java底层有System.gc和System.runFinalization这两个方法,这也只是增加了终结方法和清除方法被执行的机会,并不能保证终结方法或者清除方法一定被执行。</p></blockquote></li><li><p>使用终结方法,无法捕获该方法中的异常,甚至连警告都不会打印出来。(清除方法没有这个问题:因为使用清除方法的一个类库在控制它的线程)。</p></li><li><p><strong>使用终结方法和清除方法会有非常严重的性能损失。</strong>(但是如果把终结方法、清除方法作为安全网,则效率不会性能很多)</p></li><li><p>**终结方法有一个严重的安全问题,为终结方法攻击提供了可能。**即如果对象在执行构造方法的时候抛出异常,终结方法也会运行,即使这个类的构造方法没有执行完。总之:从构造器抛出的异常,应该足以防止对象继续存在;但有了终结方法,这点就做不到了。</p></li></ol><p>如果类对象中封装了资源(比如文件或者线程),确实终止。<strong>只需让类实现AutoCloseable</strong>,并重写close方法,客户端在实例不需要的时候调用close方法即可。</p><blockquote><p>值得提及的一个细节是,该实例必须记录下自己是否已经被关闭了:close方法必须在一个私有域中记录下“该对象已经不再有效”。 如果这些方法是在对象已经终止之后被调用,其他的方法就必须检查这个域,并抛出 IllegalStateException 异常。</p></blockquote><p>类的终结方法、清除方法的优点:</p><ol><li>当资源的所有者忘记调用close方法时,终结方法或者清除方法可以充当“安全网”。虽然不能保证方法被及时的运行。</li><li>方便关闭对象的本地对等体。本地对等体就是<code>一个本地(非Java的)对象</code>,是普通对象(Java对等体)通过本地方法委托给一个本地对象。因为本地对象不会被Java的垃圾回收器回收,所以可以在Java对等体被回收的时候,使用终结方法、清除方法关闭本地对等体。</li></ol><hr><p>清除方法的使用有一定的技巧,下面以简单的Room类为例,假设房间在回收之前必须被清除。Room类实现了AutoCloseable,并利用清除方法自动清除。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> sun.misc.Cleaner;</span><br><span class="line"></span><br><span class="line"><span class="comment">// An autocloseable class using a cleaner as a safety net</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Room</span> <span class="keyword">implements</span> <span class="title class_">AutoCloseable</span> {</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Cleaner</span> <span class="variable">cleaner</span> <span class="operator">=</span> Cleaner.create();</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Resource that requires cleaning. Must not refer to Room!</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">class</span> <span class="title class_">State</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span> {</span><br><span class="line"> <span class="type">int</span> numJunkPiles; <span class="comment">// Number of junk piles in this room</span></span><br><span class="line"></span><br><span class="line"> State(<span class="type">int</span> numJunkPiles) {</span><br><span class="line"> <span class="built_in">this</span>.numJunkPiles = numJunkPiles;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// Invoked by close method or cleaner</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">run</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"Cleaning room"</span>);</span><br><span class="line"> numJunkPiles = <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">// The state of this room, shared with our cleanable</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> State state;</span><br><span class="line"> <span class="comment">// Our cleanable. Cleans the room when it’s eligible for gc</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Cleaner.Cleanable cleanable;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">Room</span><span class="params">(<span class="type">int</span> numJunkPiles)</span> {</span><br><span class="line"> state = <span class="keyword">new</span> <span class="title class_">State</span>(numJunkPiles);</span><br><span class="line"> cleanable = cleaner.register(<span class="built_in">this</span>, state);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">close</span><span class="params">()</span> {</span><br><span class="line"> cleanable.clean();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>内嵌的静态类State保存清除方法清除房间所需的资源;numJunkPiles域,表示房间的杂乱度。State实现了Runnable接口,他的run方法最多被Cleanable调用一次。有两种情况会触发run方法的调用:1. 调用Room的close方法,本质是调用cleanable.clean()。2. 如果Room实例应该被垃圾回收时,客户端没有调用close方法,清除方法就会调用State的run方法。</p><blockquote><p>State实例没有引用它的Room实例。 如果它引用了就会造成循环,阻止Room实例被垃圾回收(以及防止被自动清除)。因此State必须是一个静态的嵌套类,因为非静态的嵌套类包含了对其外围实例的引用。同样地,也不建议使用lambda, 因为它们很容易捕捉到对外围对象的引用。</p></blockquote><p>就像之前说的,Room类的清除器只是用作安全网。如果客户端将所有Room实例包围在带有资源的try块中,则永远不需要自动清理。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Adult</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="keyword">try</span> (<span class="type">Room</span> <span class="variable">myRoom</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Room</span>(<span class="number">7</span>)) {</span><br><span class="line"> System.out.println(<span class="string">"Goodbye"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>但如果是下面这种场景,则不会打印出"Cleaning room"。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Teenager</span> {</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String[] args)</span> {</span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Room</span>(<span class="number">99</span>);</span><br><span class="line"> System.out.println(<span class="string">"Peace out"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>因为Cleaner规范:清除方法在System.exit期间的行为和实现有关,但是不确保清除方法是否会被调用。</p><blockquote><p>在sout之前,添加System.gc(),就可以让其在退出之前打印出Cleaning room。</p></blockquote><p><strong>总而言之,除非是作为安全网,或者是为了终止非关键的本地资源,否则请不要使用清除方法,对于在Java 9之前的发行版本,则尽量不要使用终结方法。若使用了终结方法或者清除方法,则要注意它的不确定性和性能后果。</strong></p><h2 id="9-try-with-resource优先于try-finally">9. try-with-resource优先于try-finally</h2><p>Java库包含许多必须通过调用close方法手动关闭的资源。常见的有InputStream、OutputStream 和 java.sql.Connection。客户端经常会忽略资源的关闭,而造成严重的性能后果。虽然许多资源用终结方法作为安全网,但是这种方法也不是很理想。</p><p>根据经验,try-finally 语句是确保正确关闭资源的最佳方法,即使在出现异常或返回时也是如此:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// try-finally - No longer the best way to close resources!</span></span><br><span class="line"><span class="keyword">static</span> String <span class="title function_">firstLineOfFile</span><span class="params">(String path)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line"> <span class="type">BufferedReader</span> <span class="variable">br</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BufferedReader</span>(<span class="keyword">new</span> <span class="title class_">FileReader</span>(path));</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">return</span> br.readLine();</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> br.close();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这可能看起来好像也不坏,但添加第二个资源时,情况会变得更糟:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// try-finally is ugly when used with more than one resource!</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">copy</span><span class="params">(String src, String dst)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line"> <span class="type">InputStream</span> <span class="variable">in</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileInputStream</span>(src);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="type">OutputStream</span> <span class="variable">out</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileOutputStream</span>(dst);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="type">byte</span>[] buf = <span class="keyword">new</span> <span class="title class_">byte</span>[BUFFER_SIZE];</span><br><span class="line"> <span class="type">int</span> n;</span><br><span class="line"> <span class="keyword">while</span> ((n = in.read(buf)) >= <span class="number">0</span>)</span><br><span class="line"> out.write(buf, <span class="number">0</span>, n);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> out.close();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">finally</span> {</span><br><span class="line"> in.close();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>即使try-finally语句正确地关闭了资源,但是也存在一些不足:</p><p>以第一个firstLineOfFile为例,如果运行时物理设备异常,则调用readLine方法时就会抛出异常,但是由于同样的原因,close方法也会异常。这种场景下,第二个异常完全抹除了第一个异常,在异常堆栈里找不到第一个异常。<strong>这时候有可能会使调试变得非常复杂</strong>。</p><hr><p>当 Java 7 引入 try-with-resources 语句时,所有这些问题都一次性解决了。要使用这个结构,资源必须实现 AutoCloseable 接口,它包含了单个返回void的 close 方法组成。Java 库和第三方库中的许多类和接口现在都实现或扩展了 AutoCloseable。如果你编写的类存在必须关闭的资源,那么也应该实现 AutoCloseable。</p><p>下面是使用 try-with-resources 的第一个示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// try-with-resources - the the best way to close resources!</span></span><br><span class="line"><span class="keyword">static</span> String <span class="title function_">firstLineOfFile</span><span class="params">(String path)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line"> <span class="keyword">try</span> (<span class="type">BufferedReader</span> <span class="variable">br</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BufferedReader</span>(<span class="keyword">new</span> <span class="title class_">FileReader</span>(path))) {</span><br><span class="line"> <span class="keyword">return</span> br.readLine();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面是使用 try-with-resources 的第二个示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// try-with-resources on multiple resources - short and sweet</span></span><br><span class="line"><span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">copy</span><span class="params">(String src, String dst)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line"> <span class="keyword">try</span> (<span class="type">InputStream</span> <span class="variable">in</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileInputStream</span>(src);<span class="type">OutputStream</span> <span class="variable">out</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">FileOutputStream</span>(dst)) {</span><br><span class="line"> <span class="type">byte</span>[] buf = <span class="keyword">new</span> <span class="title class_">byte</span>[BUFFER_SIZE];</span><br><span class="line"> <span class="type">int</span> n;</span><br><span class="line"> <span class="keyword">while</span> ((n = in.read(buf)) >= <span class="number">0</span>)</span><br><span class="line"> out.write(buf, <span class="number">0</span>, n);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>和使用 try-finally 的最开始的两个代码相比,try-with-resources 为开发者提供了更好的诊断方式。如果使用 try-with-resources ,这时候运行 firstLineOfFile 方法,如果异常是由 readLine 调用和不可见的 close 抛出的,第二个异常就会被禁止,第一个异常就会被保留(如果有多个异常,也只会保留第一个)。这些被禁止的异常并不是被简单的抛弃,而是被打印在堆栈轨迹中,并注明是被禁止的异常。通过编程调用getSuppressed方法还可以访问到它们。【getSuppressed方法也已经添加在java 7的Throwable中了】。</p><p>try-with-resources 中也可以使用 catch 子句,比如:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// try-with-resources with a catch clause</span></span><br><span class="line"><span class="keyword">static</span> String <span class="title function_">firstLineOfFile</span><span class="params">(String path, String defaultVal)</span> {</span><br><span class="line"> <span class="keyword">try</span> (<span class="type">BufferedReader</span> <span class="variable">br</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">BufferedReader</span>(<span class="keyword">new</span> <span class="title class_">FileReader</span>(path))) {</span><br><span class="line"> <span class="keyword">return</span> br.readLine();</span><br><span class="line"> } <span class="keyword">catch</span> (IOException e) {</span><br><span class="line"> <span class="keyword">return</span> defaultVal;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>所以:在处理必须关闭的资源时,始终要优先考虑用 try-with-resources,而不是try-finally。</strong></p>]]></content>
<summary type="html">Effective Java书籍第二章笔记整理</summary>
<category term="读书inG" scheme="https://jovehawking.fun/categories/%E8%AF%BB%E4%B9%A6inG/"/>
<category term="读书inG" scheme="https://jovehawking.fun/tags/%E8%AF%BB%E4%B9%A6inG/"/>
<category term="Effective Java" scheme="https://jovehawking.fun/tags/Effective-Java/"/>
</entry>
<entry>
<title>W-RPC开发问题</title>
<link href="https://jovehawking.fun/posts/57e465a9.html"/>
<id>https://jovehawking.fun/posts/57e465a9.html</id>
<published>2024-07-14T03:52:59.000Z</published>
<updated>2024-07-14T09:55:00.932Z</updated>
<content type="html"><![CDATA[<h1>手搓RPC</h1><h2 id="序列化、反序列化问题">序列化、反序列化问题</h2><p>RPC远程调用时,需要将对象进行序列化/反序列化操作。</p><p>比如RPC框架的response、request对象需要传递给服务提供方/服务消费方。所以需要对RPC的response、request对象进行序列化、反序列化操作。</p><p>实现方式:</p><ul><li>第三方序列化框架<ul><li>推荐。比较方便,但是该项目不需要太复杂,所以暂不使用。</li></ul></li><li>继承Serializable接口<ul><li><code>Serializable</code>接口本身不包含任何方法或字段,它是一个标记接口(marker interface)</li><li>具体的序列化方法主要通过<code>ObjectOutputStream</code>和<code>ObjectInputStream</code>这两个类来完成</li><li>如果更好的控制序列化过程,还需要使用<code>writeObject()</code>和<code>readObject()</code>方法</li><li>每一个继承该接口,同时想要精细化控制的类都需要实现<code>writeObject()</code>和<code>readObject()</code>方法</li></ul></li><li>自定义一个Serializer序列化器<ul><li>自己定义序列化、反序列化方法,将控制序列化的方法都写在序列化器中,不需要每个类自己写一遍。</li></ul></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">package</span> org.example.serializer;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> java.io.*;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">JDKSerializer</span> <span class="keyword">implements</span> <span class="title class_">Serializer</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <T> <span class="type">byte</span>[] serialize(T obj) <span class="keyword">throws</span> IOException {</span><br><span class="line"> <span class="type">ByteArrayOutputStream</span> <span class="variable">byteArrayOutputStream</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ByteArrayOutputStream</span>();</span><br><span class="line"> <span class="type">ObjectOutputStream</span> <span class="variable">objectOutputStream</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectOutputStream</span>(byteArrayOutputStream);</span><br><span class="line"> objectOutputStream.writeObject(obj);</span><br><span class="line"> objectOutputStream.close();</span><br><span class="line"> <span class="keyword">return</span> byteArrayOutputStream.toByteArray();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <T> T <span class="title function_">deserialize</span><span class="params">(<span class="type">byte</span>[] bytes, Class<T> clazz)</span> <span class="keyword">throws</span> IOException {</span><br><span class="line"> <span class="type">ByteArrayInputStream</span> <span class="variable">byteArrayInputStream</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ByteArrayInputStream</span>(bytes);</span><br><span class="line"> <span class="type">ObjectInputStream</span> <span class="variable">objectInputStream</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ObjectInputStream</span>(byteArrayInputStream);</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> <span class="keyword">return</span> (T) objectInputStream.readObject();</span><br><span class="line"> } <span class="keyword">catch</span> (ClassNotFoundException e) {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(e);</span><br><span class="line"> } <span class="keyword">finally</span> {</span><br><span class="line"> objectInputStream.close();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意:</p><ul><li>流的<code>flush方法</code>和<code>close方法</code></li></ul><blockquote><ul><li><p><code>flush()</code>方法的主要作用是立即将缓冲区中的所有数据写入到目标输出流中。这保证了即使在程序异常终止的情况下,已经调用过的方法所要写入的数据能够被保存下来。</p></li><li><p><code>close()</code>方法不仅会调用<code>flush()</code>方法来清空缓冲区,还会关闭底层的输出流,释放与之关联的所有系统资源。一旦流被关闭,就不能再进行任何读写操作,否则会抛出<code>IOException</code>。</p></li></ul><p><strong>使用建议</strong></p><p>在实际编程中,推荐的做法是在完成所有写入操作后,总是调用<code>close()</code>方法来关闭流。由于<code>close()</code>方法内部会调用<code>flush()</code>,所以通常不需要显式地调用<code>flush()</code>,除非有特定的需求(例如,在多次写入之间需要立即同步数据)。</p></blockquote><h2 id="消费方发起调用的方法">消费方发起调用的方法</h2><p>rpc本质就是远程调用,比如服务提供方A,消费方B、C…,无论消费方有多少,获取提供方A的某个数据所需要的请求代码都是相同的。所以可以采用代理的方法(代理本来要用作方法的增强,在这里直接调用相同的代码逻辑)。</p><p>缺点:这样实现的接口<code>UserService</code>必须在common包里定义,提供方实现这个接口才可以。因为消费方也需要。</p>]]></content>
<summary type="html">学习RPC开发时所遇到一些问题</summary>
<category term="工作inG" scheme="https://jovehawking.fun/categories/%E5%B7%A5%E4%BD%9CinG/"/>
<category term="手搓RPC" scheme="https://jovehawking.fun/tags/%E6%89%8B%E6%90%93RPC/"/>
</entry>
<entry>
<title>W-子组件调整element#message的高度</title>
<link href="https://jovehawking.fun/posts/87d480c0.html"/>
<id>https://jovehawking.fun/posts/87d480c0.html</id>
<published>2024-07-13T01:28:59.000Z</published>
<updated>2024-07-13T01:30:01.761Z</updated>
<content type="html"><![CDATA[<h1>子组件调整element#message的高度</h1><h2 id="message组件">message组件</h2><p>Element UI 的 <code>Message</code> 组件默认是在全局范围内显示消息提示,可以在子组件中使用它,但是Message是在全局注册,所以位置参数都是针对整个app.vue(页面来显示的)</p><h2 id="调整高度">调整高度</h2><p><code>message</code>有属性offset,可以用来调整高度。所以需要每次this.$message时,给offset属性赋值即可。</p><h2 id="计算子组件高度">计算子组件高度</h2><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">updateComponentPosition() {</span><br><span class="line"> // myComponent是子组件</span><br><span class="line"> const componentRect = this.$refs.myComponent.getBoundingClientRect();</span><br><span class="line"> // 如果使用该vue文件自身,需要用getElementById或者getElementByClassName获取到该vue文件的dom元素</span><br><span class="line">const componentRect = document.getElementBy('myElement');</span><br><span class="line"></span><br><span class="line"> this.componentTopPosition = componentRect.top;</span><br><span class="line"> this.componentHeight = componentRect.height;</span><br><span class="line">},</span><br><span class="line"></span><br><span class="line">this.$message({</span><br><span class="line"> ...</span><br><span class="line"> // 此时位置在子组件的最上方</span><br><span class="line"> offset: this.componentTopPosition + this.componentHeight</span><br><span class="line"> ...</span><br><span class="line">})</span><br></pre></td></tr></table></figure><p><code>getBoundingClientRect()</code> 是一个 DOM 元素的方法,用于获取一个元素的大小及其相对于视口的位置。这个方法返回一个对象,该对象包含了元素的 top、right、bottom、left、width 和 height 属性,这些属性都是相对于视口的坐标系。</p><h3 id="返回的对象属性说明">返回的对象属性说明</h3><ul><li><code>top</code>: 元素上边界到视口顶部的距离。</li><li><code>right</code>: 视口右侧到元素右边界的距离(不是元素到视口右边界的距离)。</li><li><code>bottom</code>: 视口底部到元素下边界的距离(不是元素到视口底边界的距离)。</li><li><code>left</code>: 元素左边界到视口左侧的距离。</li><li><code>width</code>: 元素的宽度。</li><li><code>height</code>: 元素的高度。</li></ul><h3 id="注意事项">注意事项</h3><ul><li><code>getBoundingClientRect()</code> 返回的值是相对于视口的,而不是相对于文档的。这意味着如果页面有滚动,返回的坐标会考虑到滚动的影响。</li><li>方法返回的坐标是浮点数,即使元素的样式是整数。</li><li>如果元素不在视口内(例如,完全被其他元素遮挡或在折叠面板后面),<code>getBoundingClientRect()</code> 仍然可以返回准确的坐标信息。</li><li>在移动设备上,坐标可能因缩放而发生变化。</li></ul><blockquote><p>视口(Viewport):</p><p>指的是用户通过浏览器或其他客户端软件查看网页内容时可见的区域。视口的概念尤其重要于响应式设计和移动设备的网页布局中,因为它直接影响了网页内容如何适应不同的屏幕尺寸和设备特性。</p><p>视口可以分为几种类型,每种类型在网页布局和响应式设计中扮演着不同的角色:</p><ol><li><strong>布局视口(Layout Viewport)</strong>:</li></ol><ul><li>在早期的网页设计中,布局视口通常指的是未缩放的浏览器窗口大小。然而,在移动设备上,为了兼容传统桌面网页的宽度,布局视口经常被设置为一个固定的宽度(如980px或1024px),这使得网页可以按预期显示,而不必做额外的缩放。</li></ul><ol start="2"><li><strong>视觉视口(Visual Viewport)</strong>:</li></ol><ul><li>视觉视口是用户当前在屏幕上实际看到的网页部分。当用户缩放页面时,视觉视口的大小会改变,但布局视口的大小通常保持不变。这意味着即使用户缩放页面,网页的布局计算仍然基于布局视口的大小。</li></ul><ol start="3"><li><strong>理想视口(Ideal Viewport)</strong>:</li></ol><ul><li>理想视口是为了使网页在移动设备上有最佳浏览体验而设定的视口大小。理想视口的宽度通常等于设备的实际屏幕宽度(以CSS像素计),这样网页可以完全填充屏幕,无需水平滚动。</li></ul><ol start="4"><li><strong>设备视口(Device Viewport)</strong>:</li></ol><ul><li>设备视口是指设备本身的屏幕尺寸,包括其物理像素尺寸和设备像素比(DPR)。设备视口对于确定网页在设备上的最终呈现至关重要。</li></ul></blockquote><h3 id="使用场景">使用场景</h3><ul><li>动态调整元素位置或大小。</li><li>检测元素是否在视口内,用于懒加载图片等。</li><li>实现拖拽效果或碰撞检测。</li><li>计算元素之间的距离或重叠部分。</li></ul><h3 id="浏览器兼容性">浏览器兼容性</h3><p><code>getBoundingClientRect()</code> 方法在现代浏览器中(包括 IE9+)广泛支持,但在一些较老的浏览器中可能不可用,这时可以使用 <code>getBoundingClientRectNoScroll</code> 作为兼容方案,不过这种方法在现代开发中已经很少使用。</p><p>总之,<code>getBoundingClientRect()</code> 是一个非常实用的方法,可以用于获取元素的精确位置和尺寸信息,对于实现响应式设计、动态布局和其他交互效果都非常有用。</p>]]></content>
<summary type="html">前端开发时遇到的问题</summary>
<category term="工作inG" scheme="https://jovehawking.fun/categories/%E5%B7%A5%E4%BD%9CinG/"/>
<category term="组件" scheme="https://jovehawking.fun/tags/%E7%BB%84%E4%BB%B6/"/>
</entry>
<entry>
<title>DataBase-Redis初级学习</title>
<link href="https://jovehawking.fun/posts/57902a6a.html"/>
<id>https://jovehawking.fun/posts/57902a6a.html</id>
<published>2024-07-06T09:00:48.000Z</published>
<updated>2024-07-07T08:24:51.135Z</updated>
<content type="html"><![CDATA[<h1>Redis初级</h1><h2 id="目标">目标</h2><ol><li>线程模型</li><li>数据结构</li><li>lua</li><li>虚拟内存</li><li>持久化</li><li>事务机制</li><li>内存淘汰</li><li>缓存穿透、击穿、雪崩</li><li>分布式锁</li><li>redission(watchdog/lock/tryLock/jedis)</li><li>key过期</li></ol><h2 id="线程模型">线程模型</h2><blockquote><p>参考文章:</p><p><a href="https://javaguide.cn/database/redis/redis-questions-01.html#redis-%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B-%E9%87%8D%E8%A6%81">https://javaguide.cn/database/redis/redis-questions-01.html#redis-线程模型-重要</a></p><p><a href="https://www.xiaolincoding.com/redis/base/redis_interview.html#redis-%E7%BA%BF%E7%A8%8B%E6%A8%A1%E5%9E%8B">https://www.xiaolincoding.com/redis/base/redis_interview.html#redis-线程模型</a></p></blockquote><h2 id="数据结构">数据结构</h2><h3 id="字符串">字符串</h3><h4 id="SDS">SDS</h4><p>Redis 构建了简单动态字符串(SDS)的数据类型,作为 Redis 的默认字符串表示,包含字符串的键值对在底层都是由 SDS 实现</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sdshdr</span> {</span></span><br><span class="line"> <span class="comment">// 记录buf数组中已使用字节的数量,等于 SDS 所保存字符串的长度</span></span><br><span class="line"> <span class="type">int</span> len;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 记录buf数组中未使用字节的数量</span></span><br><span class="line"> <span class="type">int</span> <span class="built_in">free</span>;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 【字节】数组,用于保存字符串(不是字符数组)</span></span><br><span class="line"> <span class="type">char</span> buf[];</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>SDS 遵循 C 字符串<strong>以空字符结尾</strong>的惯例, 保存空字符的 1 字节不计算在 len 属性,SDS 会自动为空字符分配额外的 1 字节空间和添加空字符到字符串末尾,所以空字符对于 SDS 的使用者来说是完全透明的</p><p><img src="../image/post/Redis-SDS%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><hr><h4 id="对比">对比</h4><p>常数复杂度获取字符串长度:</p><ul><li>C 字符串不记录自身的长度,获取时需要遍历整个字符串,遇到空字符串为止,时间复杂度为 O(N)</li><li>SDS 获取字符串长度的时间复杂度为 O(1),设置和更新 SDS 长度由函数底层自动完成</li></ul><p>杜绝缓冲区溢出:</p><ul><li><p>C 字符串调用 strcat 函数拼接字符串时,如果字符串内存不够容纳目标字符串,就会造成缓冲区溢出(Buffer Overflow)</p><p>s1 和 s2 是内存中相邻的字符串,执行 <code>strcat(s1, " Cluster")</code>(有空格):</p><p><img src="../image/post/Redis-%E5%86%85%E5%AD%98%E6%BA%A2%E5%87%BA%E9%97%AE%E9%A2%98.png" alt=""></p></li><li><p>SDS 空间分配策略:当对 SDS 进行修改时,首先检查 SDS 的空间是否满足修改所需的要求, 如果不满足会自动将 SDS 的空间扩展至执行修改所需的大小,然后执行实际的修改操作, 避免了缓冲区溢出的问题</p></li></ul><p>二进制安全:</p><ul><li>C 字符串中的字符必须符合某种编码(比如 ASCII)方式,除了字符串末尾以外其他位置不能包含空字符,否则会被误认为是字符串的结尾,所以只能保存文本数据</li><li>SDS 的 API 都是二进制安全的,使用字节数组 buf 保存一系列的二进制数据,使用 len 属性来判断数据的结尾,所以可以保存图片、视频、压缩文件等二进制数据</li></ul><p>兼容 C 字符串的函数:SDS 会在为 buf 数组分配空间时多分配一个字节来保存空字符,所以可以重用一部分 C 字符串函数库的函数</p><hr><h4 id="内存">内存</h4><p>C 字符串<strong>每次</strong>增长或者缩短都会进行一次内存重分配,拼接操作通过重分配扩展底层数组空间,截断操作通过重分配释放不使用的内存空间,防止出现内存泄露</p><p>SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,在 SDS 中 buf 数组的长度不一定就是字符数量加一, 数组里面可以包含未使用的字节,字节的数量由 free 属性记录</p><p>内存重分配涉及复杂的算法,需要执行系统调用,是一个比较耗时的操作,SDS 的两种优化策略:</p><ul><li><p>空间预分配:当 SDS 的 API 进行修改并且需要进行空间扩展时,程序不仅会为 SDS 分配修改所必需的空间, 还会为 SDS 分配额外的未使用空间</p><ul><li><p>对 SDS 修改之后,SDS 的长度(len 属性)小于 1MB,程序分配和 len 属性同样大小的未使用空间,此时 len 和 free 相等</p><p>s 为 Redis,执行 <code>sdscat(s, " Cluster")</code> 后,len 变为 13 字节,所以也分配了 13 字节的 free 空间,总长度变为 27 字节(额外的一字节保存空字符,13 + 13 + 1 = 27)</p><p><img src="../image/post/Redis-SDS%E5%86%85%E5%AD%98%E9%A2%84%E5%88%86%E9%85%8D.png" alt=""></p></li><li><p>对 SDS 修改之后,SDS 的长度大于等于 1MB,程序会分配 1MB 的未使用空间</p></li></ul><p>在扩展 SDS 空间前,API 会先检查 free 空间是否足够,如果足够就无需执行内存重分配,所以通过预分配策略,SDS 将连续增长 N 次字符串所需内存的重分配次数从<strong>必定 N 次降低为最多 N 次</strong></p></li><li><p>惰性空间释放:当 SDS 的 API 需要缩短字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是使用 free 属性将这些字节的数量记录起来,并等待将来使用</p><p>SDS 提供了相应的 API 来真正释放 SDS 的未使用空间,所以不用担心空间惰性释放策略造成的内存浪费问题</p></li></ul><hr><h3 id="链表">链表</h3><p>链表提供了高效的节点重排能力,C 语言并没有内置这种数据结构,所以 Redis 构建了链表数据类型</p><p>链表节点:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> {</span></span><br><span class="line"> <span class="comment">// 前置节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">prev</span>;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 后置节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">next</span>;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 节点的值</span></span><br><span class="line"> <span class="type">void</span> *value</span><br><span class="line">} listNode;</span><br></pre></td></tr></table></figure><p>多个 listNode 通过 prev 和 next 指针组成<strong>双端链表</strong>:</p><p><img src="../image/post/Redis-%E9%93%BE%E8%A1%A8%E8%8A%82%E7%82%B9%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><p>list 链表结构:提供了表头指针 head 、表尾指针 tail 以及链表长度计数器 len</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">list</span> {</span></span><br><span class="line"> <span class="comment">// 表头节点</span></span><br><span class="line"> listNode *head;</span><br><span class="line"> <span class="comment">// 表尾节点</span></span><br><span class="line"> listNode *tail;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 链表所包含的节点数量</span></span><br><span class="line"> <span class="type">unsigned</span> <span class="type">long</span> len;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 节点值复制函数,用于复制链表节点所保存的值</span></span><br><span class="line"> <span class="type">void</span> *(*dup) (<span class="type">void</span> *ptr);</span><br><span class="line"> <span class="comment">// 节点值释放函数,用于释放链表节点所保存的值</span></span><br><span class="line"> <span class="type">void</span> (*<span class="built_in">free</span>) (<span class="type">void</span> *ptr);</span><br><span class="line"> <span class="comment">// 节点值对比函数,用于对比链表节点所保存的值和另一个输入值是否相等</span></span><br><span class="line"> <span class="type">int</span> (*match) (<span class="type">void</span> *ptr, <span class="type">void</span> *key);</span><br><span class="line">} <span class="built_in">list</span>;</span><br></pre></td></tr></table></figure><p><img src="../image/post/Redis-%E9%93%BE%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><p>Redis 链表的特性:</p><ul><li>双端:链表节点带有 prev 和 next 指针,获取某个节点的前置节点和后置节点的时间复杂度都是 O(1)</li><li>无环:表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问以 NULL 为终点</li><li>带表头指针和表尾指针: 通过 list 结构的 head 指针和 tail 指针,获取链表的表头节点和表尾节点的时间复杂度为 O(1)</li><li>带链表长度计数器:使用 len 属性来对 list 持有的链表节点进行计数,获取链表中节点数量的时间复杂度为 O(1)</li><li>多态:链表节点使用 void * 指针来保存节点值, 并且可以通过 dup、free 、match 三个属性为节点值设置类型特定函数,所以链表可以保存各种<strong>不同类型的值</strong></li></ul><hr><h3 id="字典">字典</h3><h4 id="哈希表">哈希表</h4><p>Redis 字典使用的哈希表结构:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictht</span> {</span></span><br><span class="line"> <span class="comment">// 哈希表数组,数组中每个元素指向 dictEntry 结构</span></span><br><span class="line">dictEntry **table;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 哈希表大小,数组的长度</span></span><br><span class="line"><span class="type">unsigned</span> <span class="type">long</span> size;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 哈希表大小掩码,用于计算索引值,总是等于 【size-1】</span></span><br><span class="line"><span class="type">unsigned</span> <span class="type">long</span> sizemask;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 该哈希表已有节点的数量 </span></span><br><span class="line"><span class="type">unsigned</span> <span class="type">long</span> used;</span><br><span class="line">} dictht;</span><br></pre></td></tr></table></figure><p>哈希表节点结构:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> {</span></span><br><span class="line"> <span class="comment">// 键</span></span><br><span class="line"><span class="type">void</span> *key;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 值,可以是一个指针,或者整数</span></span><br><span class="line"><span class="class"><span class="keyword">union</span> {</span></span><br><span class="line"> <span class="type">void</span> *val;<span class="comment">// 指针</span></span><br><span class="line"> <span class="type">uint64_t</span> u64;</span><br><span class="line"> <span class="type">int64_t</span> s64;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 指向下个哈希表节点,形成链表,用来解决冲突问题</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> *<span class="title">next</span>;</span></span><br><span class="line">} dictEntry;</span><br></pre></td></tr></table></figure><p><img src="../image/post/Redis-%E5%93%88%E5%B8%8C%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><hr><h4 id="字典结构">字典结构</h4><p>字典,又称为符号表、关联数组、映射(Map),用于保存键值对的数据结构,字典中的每个键都是独一无二的。底层采用哈希表实现,一个哈希表包含多个哈希表节点,每个节点保存一个键值对</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dict</span> {</span></span><br><span class="line"> <span class="comment">// 类型特定函数</span></span><br><span class="line"> dictType *type;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 私有数据</span></span><br><span class="line"> <span class="type">void</span> *privdata;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 哈希表,数组中的每个项都是一个dictht哈希表,</span></span><br><span class="line"> <span class="comment">// 一般情况下字典只使用 ht[0] 哈希表, ht[1] 哈希表只会在对 ht[0] 哈希表进行 rehash 时使用</span></span><br><span class="line"> dictht ht[<span class="number">2</span>];</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// rehash 索引,当 rehash 不在进行时,值为 -1</span></span><br><span class="line"> <span class="type">int</span> rehashidx;</span><br><span class="line">} dict;</span><br></pre></td></tr></table></figure><p>type 属性和 privdata 属性是针对不同类型的键值对, 为创建多态字典而设置的:</p><ul><li>type 属性是指向 dictType 结构的指针, 每个 dictType 结构保存了一簇用于操作特定类型键值对的函数, Redis 会为用途不同的字典设置不同的类型特定函数</li><li>privdata 属性保存了需要传给那些类型特定函数的可选参数</li></ul><p><img src="../image/post/Redis-%E5%AD%97%E5%85%B8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><hr><h4 id="哈希冲突">哈希冲突</h4><p>Redis 使用 MurmurHash 算法来计算键的哈希值,这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快</p><p>将一个新的键值对添加到字典里,需要先根据键 key 计算出哈希值,然后进行取模运算(取余):</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">index = hash & dict->ht[x].sizemask</span><br></pre></td></tr></table></figure><p>当有两个或以上数量的键被分配到了哈希表数组的同一个索引上时,就称这些键发生了哈希冲突(collision)</p><p>Redis 的哈希表使用链地址法(separate chaining)来解决键哈希冲突, 每个哈希表节点都有一个 next 指针,多个节点通过 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题</p><p>dictEntry 节点组成的链表没有指向链表表尾的指针,为了速度考虑,程序总是将新节点添加到链表的表头位置(<strong>头插法</strong>),时间复杂度为 O(1)</p><p><img src="../image/post/Redis-%E5%AD%97%E5%85%B8%E8%A7%A3%E5%86%B3%E5%93%88%E5%B8%8C%E5%86%B2%E7%AA%81.png" alt=""></p><hr><h4 id="负载因子">负载因子</h4><p>负载因子的计算方式:哈希表中的<strong>节点数量</strong> / 哈希表的大小(<strong>长度</strong>)</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">load_factor = ht[<span class="number">0</span>].used / ht[<span class="number">0</span>].size</span><br></pre></td></tr></table></figure><p>为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时 ,程序会自动对哈希表的大小进行相应的扩展或者收缩</p><p>哈希表执行扩容的条件:</p><ul><li><p>服务器没有执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 1</p></li><li><p>服务器正在执行 BGSAVE 或者 BGREWRITEAOF 命令,哈希表的负载因子大于等于 5</p><p>原因:执行该命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,通过提高执行扩展操作的负载因子,尽可能地避免在子进程存在期间进行哈希表扩展操作,可以避免不必要的内存写入操作,最大限度地节约内存</p></li></ul><p>哈希表执行收缩的条件:负载因子小于 0.1(自动执行,servreCron 中检测),缩小为字典中数据个数的 50% 左右</p><hr><h4 id="重新散列">重新散列</h4><p>扩展和收缩哈希表的操作通过 rehash(重新散列)来完成,步骤如下:</p><ul><li>为字典的 ht[1] 哈希表分配空间,空间大小的分配情况:<ul><li>如果执行的是扩展操作,ht[1] 的大小为第一个大于等于 $ht[0].used * 2$ 的 $2^n$</li><li>如果执行的是收缩操作,ht[1] 的大小为第一个大于等于 $ht[0].used$ 的 $2^n$</li></ul></li><li>将保存在 ht[0] 中所有的键值对重新计算哈希值和索引值,迁移到 ht[1] 上</li><li>当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后(ht[0]变为空表), 释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 创建一个新的空白哈希表,为下一次 rehash 做准备</li></ul><p>如果哈希表里保存的键值对数量很少,rehash 就可以在瞬间完成,但是如果哈希表里数据很多,那么要一次性将这些键值对全部 rehash 到 ht[1] 需要大量计算,可能会导致服务器在一段时间内停止服务</p><p>Redis 对 rehash 做了优化,使 rehash 的动作并不是一次性、集中式的完成,而是分多次,渐进式的完成,又叫<strong>渐进式 rehash</strong></p><ul><li>为 ht[1] 分配空间,此时字典同时持有 ht[0] 和 ht[1] 两个哈希表</li><li>在字典中维护了一个索引计数器变量 rehashidx,并将变量的值设为 0,表示 rehash 正式开始</li><li>在 rehash 进行期间,每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1],rehash 完成之后<strong>将 rehashidx 属性的值增一</strong></li><li>随着字典操作的不断执行,最终在某个时间点上 ht[0] 的所有键值对都被 rehash 至 ht[1],这时程序将 rehashidx 属性的值设为 -1,表示 rehash 操作已完成</li></ul><p>渐进式 rehash 采用<strong>分而治之</strong>的方式,将 rehash 键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式 rehash 带来的庞大计算量</p><p>渐进式 rehash 期间的哈希表操作:</p><ul><li>字典的查找、删除、更新操作会在两个哈希表上进行,比如查找一个键会先在 ht[0] 上查找,查找不到就去 ht[1] 继续查找</li><li>字典的添加操作会直接在 ht[1] 上添加,不在 ht[0] 上进行任何添加</li></ul><hr><h3 id="跳跃表">跳跃表</h3><h4 id="底层结构">底层结构</h4><p>跳跃表(skiplist)是一种有序(<strong>默认升序</strong>)的数据结构,在链表的基础上<strong>增加了多级索引以提升查找的效率</strong>,索引是占内存的,所以是一个<strong>空间换时间</strong>的方案,跳表平均 O(logN)、最坏 O(N) 复杂度的节点查找,效率与平衡树相当但是实现更简单</p><p>原始链表中存储的有可能是很大的对象,而索引结点只需要存储关键值和几个指针,并不需要存储对象,因此当节点本身比较大或者元素数量比较多的时候,其优势可以被放大,而缺点(占内存)则可以忽略</p><p>Redis 只在两个地方应用了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplist</span> {</span></span><br><span class="line"> <span class="comment">// 表头节点和表尾节点,O(1) 的时间复杂度定位头尾节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">skiplistNode</span> *<span class="title">head</span>, *<span class="title">tail</span>;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 表的长度,也就是表内的节点数量 (表头节点不计算在内)</span></span><br><span class="line"> <span class="type">unsigned</span> <span class="type">long</span> length;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 表中层数最大的节点的层数 (表头节点的层高不计算在内)</span></span><br><span class="line"> <span class="type">int</span> level</span><br><span class="line">} zskiplist;</span><br></pre></td></tr></table></figure><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> {</span></span><br><span class="line"> <span class="comment">// 层</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistLevel</span> {</span></span><br><span class="line"> <span class="comment">// 前进指针</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">forward</span>;</span></span><br><span class="line"> <span class="comment">// 跨度</span></span><br><span class="line"> <span class="type">unsigned</span> <span class="type">int</span> span;</span><br><span class="line"> } level[];</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 后退指针</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">backward</span>;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 分值</span></span><br><span class="line"> <span class="type">double</span> score;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 成员对象</span></span><br><span class="line"> robj *obj;</span><br><span class="line">} zskiplistNode;</span><br></pre></td></tr></table></figure><p><img src="../image/post/Redis-%E8%B7%B3%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><hr><h4 id="属性分析">属性分析</h4><p>层:level 数组包含多个元素,每个元素包含指向其他节点的指针。根据幕次定律(power law,越大的数出现的概率越小)<strong>随机</strong>生成一个介于 1 和 32 之间的值(Redis5 之后最大为 64)作为 level 数组的大小,这个大小就是层的高度,节点的第一层是 level[0] = L1</p><p>前进指针:forward 用于从表头到表尾方向正序(升序)遍历节点,遇到 NULL 停止遍历</p><p>跨度:span 用于记录两个节点之间的距离,用来<strong>计算排位(rank)</strong>:</p><ul><li><p>两个节点之间的跨度越大相距的就越远,指向 NULL 的所有前进指针的跨度都为 0</p></li><li><p>在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,结果就是目标节点在跳跃表中的排位,按照上图所示:</p><p>查找分值为 3.0 的节点,沿途经历的层:查找的过程只经过了一个层,并且层的跨度为 3,所以目标节点在跳跃表中的排位为 3</p><p>查找分值为 2.0 的节点,沿途经历的层:经过了两个跨度为 1 的节点,因此可以计算出目标节点在跳跃表中的排位为 2</p></li></ul><p>后退指针:backward 用于从表尾到表头方向逆序(降序)遍历节点</p><p>分值:score 属性一个 double 类型的浮点数,跳跃表中的所有节点都<strong>按分值从小到大来排序</strong></p><p>成员对象:obj 属性是一个指针,指向一个 SDS 字符串对象。同一个跳跃表中,各个节点保存的成员对象必须是唯一的,但是多个节点保存的分值可以是相同的,分值相同的节点将按照成员对象在字典序中的大小来进行排序(从小到大)</p><p>个人笔记:JUC → 并发包 → ConcurrentSkipListMap 详解跳跃表</p><hr><h3 id="整数集合">整数集合</h3><h4 id="底层结构-2">底层结构</h4><p>整数集合(intset)是用于保存整数值的集合数据结构,是 Redis 集合键的底层实现之一</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">intset</span> {</span></span><br><span class="line"><span class="comment">// 编码方式</span></span><br><span class="line"><span class="type">uint32_t</span> encoding;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 集合包含的元素数量,也就是 contents 数组的长度</span></span><br><span class="line"><span class="type">uint32_t</span> length;</span><br><span class="line"> </span><br><span class="line"><span class="comment">// 保存元素的数组</span></span><br><span class="line"> <span class="type">int8_t</span> contents[];</span><br><span class="line">} intset;</span><br></pre></td></tr></table></figure><p>encoding 取值为三种:INTSET_ENC_INT16、INTSET_ENC_INT32、INTSET_ENC_INT64</p><p>整数集合的每个元素都是 contents 数组的一个数组项(item),在数组中按值的大小从小到大<strong>有序排列</strong>,并且数组中<strong>不包含任何重复项</strong>。虽然 contents 属性声明为 int8_t 类型,但实际上数组并不保存任何 int8_t 类型的值, 真正类型取决于 encoding 属性</p><p><img src="../image/post/Redis-%E6%95%B4%E6%95%B0%E9%9B%86%E5%90%88%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><p>说明:底层存储结构是数组,所以为了保证有序性和不重复性,每次添加一个元素的时间复杂度是 O(N)</p><hr><h4 id="升级降级">升级降级</h4><p>整数集合添加的新元素的类型比集合现有所有元素的类型都要长时,需要先进行升级(upgrade),升级流程:</p><ul><li><p>根据新元素的类型长度以及集合元素的数量(包括新元素在内),扩展整数集合底层数组的空间大小</p></li><li><p>将底层数组现有的所有元素都转换成与新元素相同的类型,并将转换后的元素放入正确的位置,放置过程保证数组的有序性</p><p>图示 32 * 4 = 128 位,首先将 3 放入索引 2(64 位 - 95 位),然后将 2 放置索引 1,将 1 放置在索引 0,从后向前依次放置在对应的区间,最后放置 65535 元素到索引 3(96 位- 127 位),修改 length 属性为 4</p></li><li><p>将新元素添加到底层数组里</p></li></ul><p><img src="../image/post/Redis-%E6%95%B4%E6%95%B0%E9%9B%86%E5%90%88%E5%8D%87%E7%BA%A7.png" alt=""></p><p>每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为 O(N)</p><p>引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个新元素的值要么就大于所有现有元素,要么就小于所有现有元素,升级之后新元素的摆放位置:</p><ul><li>在新元素小于所有现有元素的情况下,新元素会被放置在底层数组的最开头(索引 0)</li><li>在新元素大于所有现有元素的情况下,新元素会被放置在底层数组的最末尾(索引 length-1)</li></ul><p>整数集合升级策略的优点:</p><ul><li><p>提升整数集合的灵活性:C 语言是静态类型语言,为了避免类型错误通常不会将两种不同类型的值放在同一个数据结构里面,整数集合可以自动升级底层数组来适应新元素,所以可以随意的添加整数</p></li><li><p>节约内存:要让数组可以同时保存 int16、int32、int64 三种类型的值,可以直接使用 int64_t 类型的数组作为整数集合的底层实现,但是会造成内存浪费,整数集合可以确保升级操作只会在有需要的时候进行,尽量节省内存</p></li></ul><p>整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态</p><hr><h3 id="压缩列表">压缩列表</h3><h4 id="底层结构-3">底层结构</h4><p>压缩列表(ziplist)是 Redis 为了节约内存而开发的,是列表键和哈希键的底层实现之一。是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构,一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值</p><p><img src="../image/post/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E5%BA%95%E5%B1%82%E7%BB%93%E6%9E%84.png" alt=""></p><ul><li>zlbytes:uint32_t 类型 4 字节,记录整个压缩列表占用的内存字节数,在对压缩列表进行内存重分或者计算 zlend 的位置时使用</li><li>zltail:uint32_t 类型 4 字节,记录压缩列表表尾节点距离起始地址有多少字节,通过这个偏移量程序无须遍历整个压缩列表就可以确定表尾节点的地址</li><li>zllen:uint16_t 类型 2 字节,记录了压缩列表包含的节点数量,当该属性的值小于 UINT16_MAX (65535) 时,该值就是压缩列表中节点的数量;当这个值等于 UINT16_MAX 时节点的真实数量需要遍历整个压缩列表才能计算得出</li><li>entryX:列表节点,压缩列表中的各个节点,<strong>节点的长度由节点保存的内容决定</strong></li><li>zlend:uint8_t 类型 1 字节,是一个特殊值 0xFF (255),用于标记压缩列表的末端</li></ul><p><img src="../image/post/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E7%A4%BA%E4%BE%8B.png" alt=""></p><p>列表 zlbytes 属性的值为 0x50 (十进制 80),表示压缩列表的总长为 80 字节,列表 zltail 属性的值为 0x3c (十进制 60),假设表的起始地址为 p,计算得出表尾节点 entry3 的地址 p + 60</p><hr><h4 id="列表节点">列表节点</h4><p>列表节点 entry 的数据结构:</p><p><img src="../image/post/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E8%8A%82%E7%82%B9.png" alt=""></p><p>previous_entry_length:以字节为单位记录了压缩列表中前一个节点的长度,程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址,完成从表尾向表头遍历操作</p><ul><li>如果前一节点的长度小于 254 字节,该属性的长度为 1 字节,前一节点的长度就保存在这一个字节里</li><li>如果前一节点的长度大于等于 254 字节,该属性的长度为 5 字节,其中第一字节会被设置为 0xFE(十进制 254),之后的四个字节则用于保存前一节点的长度</li></ul><p>encoding:记录了节点的 content 属性所保存的数据类型和长度</p><ul><li><p>长度为 1 字节、2 字节或者 5 字节,值的最高位为 00、01 或者 10 的是字节数组编码,数组的长度由编码除去最高两位之后的其他位记录,下划线 <code>_</code> 表示留空,而 <code>b</code>、<code>x</code> 等变量则代表实际的二进制数据</p><p><img src="../image/post/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E5%AD%97%E8%8A%82%E6%95%B0%E7%BB%84%E7%BC%96%E7%A0%81.png" alt=""></p></li><li><p>长度为 1 字节,值的最高位为 11 的是整数编码,整数值的类型和长度由编码除去最高两位之后的其他位记录</p><p><img src="../image/post/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E6%95%B4%E6%95%B0%E7%BC%96%E7%A0%81.png" alt=""></p></li></ul><p>content:每个压缩列表节点可以保存一个字节数组或者一个整数值</p><ul><li><p>字节数组可以是以下三种长度的其中一种:</p><ul><li><p>长度小于等于 $63 (2^6-1)$ 字节的字节数组</p></li><li><p>长度小于等于 $16383(2^{14}-1)$ 字节的字节数组</p></li><li><p>长度小于等于 $4294967295(2^{32}-1)$ 字节的字节数组</p></li></ul></li><li><p>整数值则可以是以下六种长度的其中一种:</p><ul><li><p>4 位长,介于 0 至 12 之间的无符号整数</p></li><li><p>1 字节长的有符号整数</p></li><li><p>3 字节长的有符号整数</p></li><li><p>int16_t 类型整数</p></li><li><p>int32_t 类型整数</p></li><li><p>int64_t 类型整数</p></li></ul></li></ul><hr><h4 id="连锁更新">连锁更新</h4><p>Redis 将在特殊情况下产生的连续多次空间扩展操作称之为连锁更新(cascade update)</p><p>假设在一个压缩列表中,有多个连续的、长度介于 250 到 253 字节之间的节点 e1 至 eN。将一个长度大于等于 254 字节的新节点 new 设置为压缩列表的头节点,new 就成为 e1 的前置节点。e1 的 previous_entry_length 属性仅为 1 字节,无法保存新节点 new 的长度,所以要对压缩列表执行空间重分配操作,并将 e1 节点的 previous_entry_length 属性从 1 字节长扩展为 5 字节长。由于 e1 原本的长度介于 250 至 253 字节之间,所以扩展后 e1 的长度就变成了 254 至 257 字节之间,导致 e2 的 previous_entry_length 属性无法保存 e1 的长度,程序需要不断地对压缩列表执行空间重分配操作,直到 eN 为止</p><p><img src="../image/post/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E8%BF%9E%E9%94%81%E6%9B%B4%E6%96%B01.png" alt=""></p><p>删除节点也可能会引发连锁更新,big.length >= 254,small.length < 254,删除 small 节点</p><p><img src="../image/post/Redis-%E5%8E%8B%E7%BC%A9%E5%88%97%E8%A1%A8%E8%BF%9E%E9%94%81%E6%9B%B4%E6%96%B02.png" alt=""></p><p>连锁更新在最坏情况下需要对压缩列表执行 N 次空间重分配,每次重分配的最坏复杂度为 O(N),所以连锁更新的最坏复杂度为 O(N^2)</p><p>说明:尽管连锁更新的复杂度较高,但出现的记录是非常低的,即使出现只要被更新的节点数量不多,就不会对性能造成影响</p><hr><h2 id="Lua脚本">Lua脚本</h2><p>Lua是一种轻量级的、高效的、可扩展的脚本语言,常用于游戏开发、系统管理、Web应用等场景。</p><h3 id="基本使用">基本使用</h3><h4 id="数据类型">数据类型</h4><p>Lua支持多种数据类型,包括数字(整数和浮点数)、字符串、布尔值、table(类似于数组和哈希表)、函数等。</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 定义变量</span></span><br><span class="line"><span class="keyword">local</span> x = <span class="number">10</span></span><br><span class="line"><span class="keyword">local</span> y = <span class="string">"Hello, World!"</span></span><br><span class="line"><span class="keyword">local</span> z = <span class="literal">true</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 多个变量可以在一行定义</span></span><br><span class="line"><span class="keyword">local</span> a, b = <span class="number">1</span>, <span class="number">2</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 打印变量</span></span><br><span class="line"><span class="built_in">print</span>(x)</span><br><span class="line"><span class="built_in">print</span>(y)</span><br><span class="line"><span class="built_in">print</span>(z)</span><br></pre></td></tr></table></figure><h4 id="控制结构">控制结构</h4><p>Lua提供常见的控制结构,如<code>if</code>、<code>for</code>、<code>while</code>、<code>repeat-until</code>等。</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- if 语句</span></span><br><span class="line"><span class="keyword">if</span> x > <span class="number">5</span> <span class="keyword">then</span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"x is greater than 5"</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- for 循环</span></span><br><span class="line"><span class="keyword">for</span> i = <span class="number">1</span>, <span class="number">10</span> <span class="keyword">do</span></span><br><span class="line"> <span class="built_in">print</span>(i)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- while 循环</span></span><br><span class="line"><span class="keyword">local</span> count = <span class="number">1</span></span><br><span class="line"><span class="keyword">while</span> count <= <span class="number">5</span> <span class="keyword">do</span></span><br><span class="line"> <span class="built_in">print</span>(count)</span><br><span class="line"> count = count + <span class="number">1</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- repeat-until 循环</span></span><br><span class="line"><span class="keyword">local</span> num = <span class="number">1</span></span><br><span class="line"><span class="keyword">repeat</span></span><br><span class="line"> <span class="built_in">print</span>(num)</span><br><span class="line"> num = num + <span class="number">1</span></span><br><span class="line"><span class="keyword">until</span> num > <span class="number">5</span></span><br></pre></td></tr></table></figure><h4 id="函数">函数</h4><p>在Lua中,函数是第一类公民,可以作为参数传递,也可以从其他函数返回。</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 定义函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">greet</span><span class="params">(name)</span></span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Hello, "</span> .. name .. <span class="string">"!"</span>)</span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="comment">-- 调用函数</span></span><br><span class="line">greet(<span class="string">"Alice"</span>)</span><br><span class="line"></span><br><span class="line"><span class="comment">-- 返回函数</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">createGreeting</span><span class="params">(name)</span></span></span><br><span class="line"> <span class="keyword">return</span> <span class="function"><span class="keyword">function</span><span class="params">()</span></span></span><br><span class="line"> <span class="built_in">print</span>(<span class="string">"Hi, "</span> .. name .. <span class="string">"!"</span>)</span><br><span class="line"> <span class="keyword">end</span></span><br><span class="line"><span class="keyword">end</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">local</span> greeting = createGreeting(<span class="string">"Bob"</span>)</span><br><span class="line">greeting()</span><br></pre></td></tr></table></figure><h3 id="Redis中使用">Redis中使用</h3><p>为什么使用Lua脚本?</p><ol><li><strong>减少网络延迟</strong>:通过将多个Redis命令封装在一个Lua脚本中,可以减少网络传输次数,提高响应速度。</li><li><strong>原子性</strong>:Lua脚本在Redis服务器内部以原子方式执行,这意味着脚本内的所有操作要么全部成功,要么全部失败,这有助于避免竞态条件。</li><li><strong>灵活性</strong>:Lua脚本提供了比Redis原生命令更丰富的编程能力,可以实现复杂的业务逻辑。</li></ol><h4 id="编写脚本">编写脚本</h4><p>首先,需要编写Lua脚本。脚本通常接受一些键名和参数,使用<code>KEYS</code>和<code>ARGV</code>数组来引用它们。例如,以下脚本将两个数值相加并存储结果:</p><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">-- 将两个数值相加并存储结果</span></span><br><span class="line"><span class="keyword">local</span> sum = <span class="built_in">tonumber</span>(ARGV[<span class="number">1</span>]) + <span class="built_in">tonumber</span>(ARGV[<span class="number">2</span>])</span><br><span class="line">redis.call(<span class="string">'SET'</span>, KEYS[<span class="number">1</span>], sum)</span><br><span class="line"><span class="keyword">return</span> sum</span><br></pre></td></tr></table></figure><h4 id="执行脚本">执行脚本</h4><p><strong>使用<code>EVAL</code>命令执行脚本</strong></p><p>一旦脚本编写完成,你可以使用<code>EVAL</code>命令将其发送给Redis服务器执行。<code>EVAL</code>命令接收三个参数:</p><ul><li>脚本的文本</li><li>脚本要访问的键的数量</li><li>脚本要使用的键和参数</li></ul><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">EVAL "local sum = tonumber(ARGV[1]) + tonumber(ARGV[2]); redis.call('SET', KEYS[1], sum); return sum;" 1 myKey 10 20</span><br><span class="line"></span><br><span class="line">脚本的文本:"local sum = ...... return sum;"</span><br><span class="line">键的数量:1</span><br><span class="line">要使用的键:myKey</span><br><span class="line">使用的参数:10 20</span><br></pre></td></tr></table></figure><p><strong>使用<code>EVALSHA</code>命令执行脚本</strong></p><p>为了提高性能,Redis允许你通过计算脚本的SHA1校验和来缓存脚本,之后你可以使用<code>EVALSHA</code>命令通过校验和来执行脚本,而无需每次都发送脚本文本。这可以减少网络带宽的使用。</p><p>例如,如果脚本的SHA1校验和是<code>abcdef1234567890</code>,则可以使用:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EVALSHA "abcdef1234567890" 1 myKey 10 20</span><br></pre></td></tr></table></figure><h4 id="脚本调试">脚本调试</h4><p>Redis从2.8.0版本开始支持脚本调试。你可以使用<code>DEBUG SCRIPT</code>命令来查看脚本是否被正确加载,以及脚本的详细信息。</p><h4 id="注意事项"><strong>注意事项</strong></h4><ul><li>Lua脚本在Redis服务器中是单线程执行的,这意味着在执行脚本期间,Redis不能同时处理其他命令。</li><li>应确保脚本的执行时间尽可能短,以避免阻塞Redis服务器。</li></ul><h3 id="SpringBoot中使用">SpringBoot中使用</h3><ol><li><p>创建一个<code>.lua</code>文件,例如<code>scripts.lua</code>,并在其中编写你的Lua脚本。例如,一个简单的脚本可能看起来像这样:</p></li><li><p>在你的Spring Boot应用中,注入<code>StringRedisTemplate</code>或<code>ReactiveStringRedisTemplate</code>,然后使用<code>execute</code>方法执行Lua脚本。这里以<code>StringRedisTemplate</code>为例:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> org.springframework.beans.factory.annotation.Autowired;</span><br><span class="line"><span class="keyword">import</span> org.springframework.data.redis.core.StringRedisTemplate;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Service;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Service</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> StringRedisTemplate stringRedisTemplate;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">public</span> <span class="title function_">RedisService</span><span class="params">(StringRedisTemplate stringRedisTemplate)</span> {</span><br><span class="line"> <span class="built_in">this</span>.stringRedisTemplate = stringRedisTemplate;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">long</span> <span class="title function_">setIfAbsent</span><span class="params">(String key, String value)</span> {</span><br><span class="line"> DefaultRedisScript<Long> script = <span class="keyword">new</span> <span class="title class_">DefaultRedisScript</span><>();</span><br><span class="line"> script.setScriptSource(<span class="keyword">new</span> <span class="title class_">ResourceScriptSource</span>(<span class="keyword">new</span> <span class="title class_">ClassPathResource</span>(<span class="string">"scripts.lua"</span>)));</span><br><span class="line"> script.setResultType(Long.class);</span><br><span class="line"> </span><br><span class="line"> <span class="keyword">return</span> stringRedisTemplate.execute(script, Collections.singletonList(key), value);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上述代码中,<code>setIfAbsent</code>方法使用了<code>DefaultRedisScript</code>来执行Lua脚本。<code>ClassPathResource</code>用于从类路径加载Lua脚本文件。</p></li></ol><h2 id="虚拟内存">虚拟内存</h2><p>Redis的虚拟内存(VM)机制是一种允许Redis将不经常访问的数据从内存移动到磁盘的技术,以此来节省服务器的RAM资源。</p><p><strong>在Redis从2.4版本开始废弃虚拟内存功能,并且在更高版本中不推荐使用。</strong></p><p>要在Redis中启用虚拟内存,你需要在配置文件<code>redis.conf</code>中设置以下选项:</p><ul><li><code>vm-enabled yes</code>:启用虚拟内存功能。</li><li><code>vm-max-memory <bytes></code>:设置Redis可以使用的最大虚拟字节数。当数据集的大小超过这个值时,Redis会开始将不常访问的数据移到磁盘上。</li><li><code>vm-page-size <bytes></code>:设定虚拟内存页面的大小,这影响到数据的分页方式。</li><li><code>vm-pages <number></code>:指定可以使用的虚拟内存页面数量。</li><li><code>vm-swap-file <path></code>:指定虚拟内存交换文件的路径和文件名。</li><li><code>vm-max-cache-size <bytes></code>:设置在内存中缓存的已分页数据的最大大小,以加速数据的读取。</li><li><code>vm-min-cache-ttl <seconds></code>:设置在内存中缓存的已分页数据的最小生存时间。</li></ul><h2 id="持久化">持久化</h2><h3 id="概述">概述</h3><p>持久化:利用永久性存储介质将数据进行保存,在特定的时间将保存的数据进行恢复的工作机制称为持久化</p><p>作用:持久化用于防止数据的意外丢失,确保数据安全性,因为 Redis 是内存级,所以需要持久化到磁盘</p><p>计算机中的数据全部都是二进制,保存一组数据有两种方式<br><img src="../image/post/Redis-%E6%8C%81%E4%B9%85%E5%8C%96%E7%9A%84%E4%B8%A4%E7%A7%8D%E6%96%B9%E5%BC%8F.png" style="zoom: 33%;" /></p><p>RDB:将当前数据状态进行保存,快照形式,存储数据结果,存储格式简单</p><p>AOF:将数据的操作过程进行保存,日志形式,存储操作过程,存储格式复杂</p><hr><h3 id="RDB">RDB</h3><h4 id="文件创建">文件创建</h4><p>RDB 持久化功能所生成的 RDB文件 是一个经过压缩的紧凑二进制文件,通过该文件可以还原生成 RDB 文件时的数据库状态,有两个 Redis 命令可以生成 RDB 文件,一个是 SAVE,另一个是 BGSAVE</p><h5 id="SAVE">SAVE</h5><p>SAVE 指令:手动执行一次保存操作,该指令的执行会阻塞当前 Redis 服务器,客户端发送的所有命令请求都会被拒绝,直到当前 RDB 过程完成为止,有可能会造成长时间阻塞,线上环境不建议使用</p><p>工作原理:Redis 是个<strong>单线程的工作模式</strong>,会创建一个任务队列,所有的命令都会进到这个队列排队执行。当某个指令在执行的时候,队列后面的指令都要等待,所以这种执行方式会非常耗时</p><p>配置 redis.conf:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">dir</span> path<span class="comment">#设置存储.rdb文件的路径,通常设置成存储空间较大的目录中,目录名称data</span></span><br><span class="line">dbfilename <span class="string">"x.rdb"</span><span class="comment">#设置本地数据库文件名,默认值为dump.rdb,通常设置为dump-端口号.rdb</span></span><br><span class="line">rdbcompression <span class="built_in">yes</span>|no<span class="comment">#设置存储至本地数据库时是否压缩数据,默认yes,设置为no节省CPU运行时间</span></span><br><span class="line">rdbchecksum <span class="built_in">yes</span>|no<span class="comment">#设置读写文件过程是否进行RDB格式校验,默认yes</span></span><br></pre></td></tr></table></figure><hr><h5 id="BGSAVE">BGSAVE</h5><p>BGSAVE:bg 是 background,代表后台执行,命令的完成需要两个进程,<strong>进程之间不相互影响</strong>,所以持久化期间 Redis 正常工作</p><p>工作原理:</p><img src="../image/post/Redis-bgsave%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.png" style="zoom:67%;" /><p>流程:客户端发出 BGSAVE 指令,Redis 服务器使用 fork 函数创建一个子进程,然后响应后台已经开始执行的信息给客户端。子进程会去执行持久化的操作,持久化过程是先将数据写入到一个临时文件中,持久化操作结束再用这个临时文件<strong>替换</strong>上次持久化的文件</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 创建子进程</span></span><br><span class="line">pid = fork()</span><br><span class="line"><span class="keyword">if</span> pid == <span class="number">0</span>:</span><br><span class="line"> <span class="comment"># 子进程负责创建 RDB 文件</span></span><br><span class="line"> rdbSave()</span><br><span class="line"> <span class="comment"># 完成之后向父进程发送信号</span></span><br><span class="line"> signal_parent()</span><br><span class="line"><span class="keyword">elif</span> pid > <span class="number">0</span>:</span><br><span class="line"> <span class="comment"># 父进程继续处理命令请求,并通过轮询等待子进程的信号</span></span><br><span class="line"> handle_request_and_wait_signal()</span><br><span class="line"><span class="keyword">else</span>:</span><br><span class="line"> <span class="comment"># 处理出错恃况</span></span><br><span class="line"> handle_fork_error() </span><br></pre></td></tr></table></figure><p>配置 redis.conf</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">stop-writes-on-bgsave-error <span class="built_in">yes</span>|no<span class="comment">#后台存储过程中如果出现错误,是否停止保存操作,默认yes</span></span><br><span class="line">dbfilename filename </span><br><span class="line"><span class="built_in">dir</span> path </span><br><span class="line">rdbcompression <span class="built_in">yes</span>|no </span><br><span class="line">rdbchecksum <span class="built_in">yes</span>|no</span><br></pre></td></tr></table></figure><p>注意:BGSAVE 命令是针对 SAVE 阻塞问题做的优化,Redis 内部所有涉及到 RDB 操作都采用 BGSAVE 的方式,SAVE 命令放弃使用</p><p>在 BGSAVE 命令执行期间,服务器处理 SAVE、BGSAVE、BGREWRITEAOF 三个命令的方式会和平时有所不同</p><ul><li>SAVE 命令会被服务器拒绝,服务器禁止 SAVE 和 BGSAVE 命令同时执行是为了避免父进程(服务器进程)和子进程同时执行两个 rdbSave 调用,产生竞争条件</li><li>BGSAVE 命令也会被服务器拒绝,也会产生竞争条件</li><li>BGREWRITEAOF 和 BGSAVE 两个命令不能同时执行<ul><li>如果 BGSAVE 命令正在执行,那么 BGREWRITEAOF 命令会被延迟到 BGSAVE 命令执行完毕之后执行</li><li>如果 BGREWRITEAOF 命令正在执行,那么 BGSAVE 命令会被服务器拒绝</li></ul></li></ul><hr><h5 id="特殊指令">特殊指令</h5><p>RDB 特殊启动形式的指令(客户端输入)</p><ul><li><p>服务器运行过程中重启</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">debug reload</span><br></pre></td></tr></table></figure></li><li><p>关闭服务器时指定保存数据</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">shutdown save</span><br></pre></td></tr></table></figure><p>默认情况下执行 shutdown 命令时,自动执行 bgsave(如果没有开启 AOF 持久化功能)</p></li><li><p>全量复制:主从复制部分详解</p></li></ul><hr><h4 id="文件载入">文件载入</h4><p>RDB 文件的载入工作是在服务器启动时自动执行,期间 Redis 会一直处于阻塞状态,直到载入完成</p><p>Redis 并没有专门用于载入 RDB 文件的命令,只要服务器在启动时检测到 RDB 文件存在,就会自动载入 RDB 文件</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[7379] 30 Aug 21:07:01.289 * DB loaded from disk: 0.018 seconds <span class="comment"># 服务器在成功载入 RDB 文件之后打印</span></span><br></pre></td></tr></table></figure><p>AOF 文件的更新频率通常比 RDB 文件的更新频率高:</p><ul><li>如果服务器开启了 AOF 持久化功能,那么会优先使用 AOF 文件来还原数据库状态</li><li>只有在 AOF 持久化功能处于关闭状态时,服务器才会使用 RDB 文件来还原数据库状态</li></ul><hr><h4 id="自动保存">自动保存</h4><h5 id="配置文件">配置文件</h5><p>Redis 支持通过配置服务器的 save 选项,让服务器每隔一段时间自动执行一次 BGSAVE 命令</p><p>配置 redis.conf:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">save second changes <span class="comment">#设置自动持久化条件,满足限定时间范围内key的变化数量就进行持久化(bgsave)</span></span><br></pre></td></tr></table></figure><ul><li>second:监控时间范围</li><li>changes:监控 key 的变化量</li></ul><p>默认三个条件:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">save 900 1<span class="comment"># 900s内1个key发生变化就进行持久化</span></span><br><span class="line">save 300 10</span><br><span class="line">save 60 10000</span><br></pre></td></tr></table></figure><p>判定 key 变化的依据:</p><ul><li>对数据产生了影响,不包括查询</li><li>不进行数据比对,比如 name 键存在,重新 set name seazean 也算一次变化</li></ul><p>save 配置要根据实际业务情况进行设置,频度过高或过低都会出现性能问题,结果可能是灾难性的</p><hr><h5 id="自动原理">自动原理</h5><p>服务器状态相关的属性:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">redisServer</span> {</span></span><br><span class="line"> <span class="comment">// 记录了保存条件的数组</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">saveparam</span> *<span class="title">saveparams</span>;</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 修改计数器</span></span><br><span class="line"> <span class="type">long</span> <span class="type">long</span> dirty;</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 上一次执行保存的时间 </span></span><br><span class="line"> <span class="type">time_t</span> lastsave;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><ul><li><p>Redis 服务器启动时,可以通过指定配置文件或者传入启动参数的方式设置 save 选项, 如果没有自定义就设置为三个默认值(上节提及),设置服务器状态 redisServe.saveparams 属性,该数组每一项为一个 saveparam 结构,代表 save 的选项设置</p> <figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">saveparam</span> {</span></span><br><span class="line"> <span class="comment">// 秒数</span></span><br><span class="line"> <span class="type">time_t</span> seconds</span><br><span class="line"> <span class="comment">// 修改数</span></span><br><span class="line"> <span class="type">int</span> changes;</span><br><span class="line">};</span><br></pre></td></tr></table></figure></li><li><p>dirty 计数器记录距离上一次成功执行 SAVE 或者 BGSAVE 命令之后,服务器中的所有数据库进行了多少次修改(包括写入、删除、更新等操作),当服务器成功执行一个修改指令,该命令修改了多少次数据库, dirty 的值就增加多少</p></li><li><p>lastsave 属性是一个 UNIX 时间戳,记录了服务器上一次成功执行 SAVE 或者 BGSAVE 命令的时间</p></li></ul><p>Redis 的服务器周期性操作函数 serverCron 默认每隔 100 毫秒就会执行一次,该函数用于对正在运行的服务器进行维护</p><p>serverCron 函数的其中一项工作是检查 save 选项所设置的保存条件是否满足,会遍历 saveparams 数组中的<strong>所有保存条件</strong>,只要有任意一个条件被满足服务器就会执行 BGSAVE 命令</p><p><img src="../image/post/Redis-BGSAVE%E6%89%A7%E8%A1%8C%E5%8E%9F%E7%90%86.png" alt=""></p><hr><h4 id="文件结构">文件结构</h4><p>RDB 的存储结构:图示全大写单词标示常量,用全小写单词标示变量和数据</p><p><img src="../image/post/Redis-RDB%E6%96%87%E4%BB%B6%E7%BB%93%E6%9E%84.png" alt=""></p><ul><li>REDIS:长度为 5 字节,保存着 <code>REDIS</code> 五个字符,是 RDB 文件的开头,在载入文件时可以快速检查所载入的文件是否 RDB 文件</li><li>db_version:长度为 4 字节,是一个用字符串表示的整数,记录 RDB 的版本号</li><li>database:包含着零个或任意多个数据库,以及各个数据库中的键值对数据</li><li>EOF:长度为 1 字节的常量,标志着 RDB 文件正文内容的结束,当读入遇到这个值时,代表所有数据库的键值对都已经载入完毕</li><li>check_sum:长度为 8 字节的无符号整数,保存着一个校验和,该值是通过 REDIS、db_version、databases、EOF 四个部分的内容进行计算得出。服务器在载入 RDB 文件时,会将载入数据所计算出的校验和与 check_sum 所记录的校验和进行对比,来检查 RDB 文件是否有出错或者损坏</li></ul><p>Redis 本身带有 RDB 文件检查工具 redis-check-dump</p><hr><h3 id="AOF">AOF</h3><h4 id="基本概述">基本概述</h4><p>AOF(append only file)持久化:以独立日志的方式记录每次写命令(不记录读)来记录数据库状态,<strong>增量保存</strong>只许追加文件但不可以改写文件,<strong>与 RDB 相比可以理解为由记录数据改为记录数据的变化</strong></p><p>AOF 主要作用是解决了<strong>数据持久化的实时性</strong>,目前已经是 Redis 持久化的主流方式</p><p>AOF 写数据过程:</p><img src="../image/post/Redis-AOF%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86.png" style="zoom:67%;" /><hr><h4 id="持久实现">持久实现</h4><p>AOF 持久化功能的实现可以分为命令追加(append)、文件写入、文件同步(sync)三个步骤</p><h5 id="命令追加">命令追加</h5><p>启动 AOF 的基本配置:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">appendonly <span class="built_in">yes</span>|no<span class="comment">#开启AOF持久化功能,默认no,即不开启状态</span></span><br><span class="line">appendfilename filename<span class="comment">#AOF持久化文件名,默认appendonly.aof,建议设置appendonly-端口号.aof</span></span><br><span class="line"><span class="built_in">dir</span><span class="comment">#AOF持久化文件保存路径,与RDB持久化文件路径保持一致即可</span></span><br></pre></td></tr></table></figure><p>当 AOF 持久化功能处于打开状态时,服务器在执行完一个写命令之后,会以协议格式将被执行的写命令<strong>追加</strong>到服务器状态的 aof_buf 缓冲区的末尾</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">redisServer</span> {</span></span><br><span class="line"> <span class="comment">// AOF 缓冲区</span></span><br><span class="line"> sds aof_buf;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><hr><h5 id="文件写入">文件写入</h5><p>服务器在处理文件事件时可能会执行写命令,追加一些内容到 aof_buf 缓冲区里,所以服务器每次结束一个事件循环之前,就会执行 flushAppendOnlyFile 函数,判断是否需要<strong>将 aof_buf 缓冲区中的内容写入和保存到 AOF 文件</strong>里</p><p>flushAppendOnlyFile 函数的行为由服务器配置的 appendfsync 选项的值来决定</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">appendfsync always|everysec|no<span class="comment">#AOF写数据策略:默认为everysec</span></span><br></pre></td></tr></table></figure><ul><li><p>always:每次写入操作都将 aof_buf 缓冲区中的所有内容<strong>写入并同步</strong>到 AOF 文件</p><p>特点:安全性最高,数据零误差,但是性能较低,不建议使用</p></li><li><p>everysec:先将 aof_buf 缓冲区中的内容写入到 AOF 文件,判断上次同步 AOF 文件的时间距离现在超过一秒钟,再次对 AOF 文件进行同步,这个同步操作是由一个(子)线程专门负责执行的</p><p>特点:在系统突然宕机的情况下丢失 1 秒内的数据,准确性较高,性能较高,建议使用,也是默认配置</p></li><li><p>no:将 aof_buf 缓冲区中的内容写入到 AOF 文件,但并不对 AOF 文件进行同步,何时同步由操作系统来决定</p><p>特点:<strong>整体不可控</strong>,服务器宕机会丢失上次同步 AOF 后的所有写指令</p></li></ul><hr><h5 id="文件同步">文件同步</h5><p>在现代操作系统中,当用户调用 write 函数将数据写入文件时,操作系统通常会将写入数据暂时保存在一个内存缓冲区空间,等到缓冲区<strong>写满或者到达特定时间周期</strong>,才真正地将缓冲区中的数据写入到磁盘里面(刷脏)</p><ul><li>优点:提高文件的写入效率</li><li>缺点:为写入数据带来了安全问题,如果计算机发生停机,那么保存在内存缓冲区里面的写入数据将会丢失</li></ul><p>系统提供了 fsync 和 fdatasync 两个同步函数做<strong>强制硬盘同步</strong>,可以让操作系统立即将缓冲区中的数据写入到硬盘里面,函数会阻塞到写入硬盘完成后返回,保证了数据持久化</p><p>异常恢复:AOF 文件损坏,通过 redis-check-aof–fix appendonly.aof 进行恢复,重启 Redis,然后重新加载</p><hr><h4 id="文件载入-2">文件载入</h4><p>AOF 文件里包含了重建数据库状态所需的所有写命令,所以服务器只要读入并重新执行一遍 AOF 文件里的命令,就还原服务器关闭之前的数据库状态,服务器在启动时,还原数据库状态打印的日志:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[8321] 05 Sep 11:58:50.449 * DB loaded from append only file: 0.000 seconds </span><br></pre></td></tr></table></figure><p>AOF 文件里面除了用于指定数据库的 SELECT 命令是服务器自动添加的,其他都是通过客户端发送的命令</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">* 2\r\n<span class="variable">$6</span>\r\nSELECT\r\n<span class="variable">$1</span>\r\n0\r\n<span class="comment"># 服务器自动添加</span></span><br><span class="line">* 3\r\n<span class="variable">$3</span>\r\nSET\r\n<span class="variable">$3</span>\r\nmsg\r\n<span class="variable">$5</span>\r\nhello\r\n</span><br><span class="line">* 5\r\n<span class="variable">$4</span>\r\nSADD\r\n<span class="variable">$6</span>\r\nfruits\r\n<span class="variable">$5</span>\r\napple\r\n<span class="variable">$6</span>\r\nbanana\r\n<span class="variable">$6</span>\r\ncherry\r\n</span><br></pre></td></tr></table></figure><p>Redis 读取 AOF 文件并还原数据库状态的步骤:</p><ul><li>创建一个<strong>不带网络连接的伪客户端</strong>(fake client)执行命令,因为 Redis 的命令只能在客户端上下文中执行, 而载入 AOF 文件时所使用的命令来源于本地 AOF 文件而不是网络连接</li><li>从 AOF 文件分析并读取一条写命令</li><li>使用伪客户端执行被读出的写命令,然后重复上述步骤</li></ul><hr><h4 id="重写实现">重写实现</h4><h5 id="重写策略">重写策略</h5><p>随着命令不断写入 AOF,文件会越来越大,很可能对 Redis 服务器甚至整个宿主计算机造成影响,为了解决这个问题 Redis 引入了 AOF 重写机制压缩文件体积</p><p>AOF 重写:读取服务器当前的数据库状态,<strong>生成新 AOF 文件来替换旧 AOF 文件</strong>,不会对现有的 AOF 文件进行任何读取、分析或者写入操作,而是直接原子替换。新 AOF 文件不会包含任何浪费空间的冗余命令,所以体积通常会比旧 AOF 文件小得多</p><p>AOF 重写规则:</p><ul><li><p>进程内具有时效性的数据,并且数据已超时将不再写入文件</p></li><li><p>对同一数据的多条写命令合并为一条命令,因为会读取当前的状态,所以直接将当前状态转换为一条命令即可。为防止数据量过大造成客户端缓冲区溢出,对 list、set、hash、zset 等集合类型,<strong>单条指令</strong>最多写入 64 个元素</p><p>如 lpushlist1 a、lpush list1 b、lpush list1 c 可以转化为:lpush list1 a b c</p></li><li><p>非写入类的无效指令将被忽略,只保留最终数据的写入命令,但是 select 指令虽然不更改数据,但是更改了数据的存储位置,此类命令同样需要记录</p></li></ul><p>AOF 重写作用:</p><ul><li>降低磁盘占用量,提高磁盘利用率</li><li>提高持久化效率,降低持久化写时间,提高 IO 性能</li><li>降低数据恢复的用时,提高数据恢复效率</li></ul><hr><h5 id="重写原理">重写原理</h5><p>AOF 重写程序 aof_rewrite 函数可以创建一个新 AOF 文件, 但是该函数会进行大量的写入操作,调用这个函数的线程将被长时间阻塞,所以 Redis 将 AOF 重写程序放到 fork 的子进程里执行,不会阻塞父进程,重写命令:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">bgrewriteaof</span><br></pre></td></tr></table></figure><ul><li><p>子进程进行 AOF 重写期间,服务器进程(父进程)可以继续处理命令请求</p></li><li><p>子进程带有服务器进程的数据副本,使用子进程而不是线程,可以在避免使用锁的情况下, 保证数据的安全性</p><p><img src="../image/post/Redis-AOF%E6%89%8B%E5%8A%A8%E9%87%8D%E5%86%99%E5%8E%9F%E7%90%86.png" alt=""></p></li></ul><p>子进程在进行 AOF 重写期间,服务器进程还需要继续处理命令请求,而新命令可能会对现有的数据库状态进行修改,从而使得服务器当前的数据库状态和重写后的 AOF 文件所保存的数据库状态不一致,所以 Redis 设置了 AOF 重写缓冲区</p><p>工作流程:</p><ul><li>Redis 服务器执行完一个写命令,会同时将该命令追加到 AOF 缓冲区和 AOF 重写缓冲区(从创建子进程后才开始写入)</li><li>当子进程完成 AOF 重写工作之后,会向父进程发送一个信号,父进程在接到该信号之后, 会调用一个信号处理函数,该函数执行时会<strong>对服务器进程(父进程)造成阻塞</strong>(影响很小),主要工作:<ul><li>将 AOF 重写缓冲区中的所有内容写入到新 AOF 文件中, 这时新 AOF 文件所保存的状态将和服务器当前的数据库状态一致</li><li>对新的 AOF 文件进行改名,<strong>原子地(atomic)覆盖</strong>现有的 AOF 文件,完成新旧两个 AOF 文件的替换</li></ul></li></ul><hr><h5 id="自动重写">自动重写</h5><p>触发时机:Redis 会记录上次重写时的 AOF 大小,默认配置是当 AOF 文件大小是上次重写后大小的一倍且文件大于 64M 时触发</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">auto-aof-rewrite-min-size size<span class="comment">#设置重写的基准值,最小文件 64MB,达到这个值开始重写</span></span><br><span class="line">auto-aof-rewrite-percentage percent<span class="comment">#触发AOF文件执行重写的增长率,当前AOF文件大小超过上一次重写的AOF文件大小的百分之多少才会重写,比如文件达到 100% 时开始重写就是两倍时触发</span></span><br></pre></td></tr></table></figure><p>自动重写触发比对参数( 运行指令 <code>info Persistence</code> 获取具体信息 ):</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">aof_current_size<span class="comment">#AOF文件当前尺寸大小(单位:字节)</span></span><br><span class="line">aof_base_size<span class="comment">#AOF文件上次启动和重写时的尺寸大小(单位:字节)</span></span><br></pre></td></tr></table></figure><p>自动重写触发条件公式:</p><ul><li>aof_current_size > auto-aof-rewrite-min-size</li><li>(aof_current_size - aof_base_size) / aof_base_size >= auto-aof-rewrite-percentage</li></ul><hr><h3 id="对比-2">对比</h3><p>RDB 的特点</p><ul><li><p>RDB 优点:</p><ul><li>RDB 是一个紧凑压缩的二进制文件,存储效率较高,但存储数据量较大时,存储效率较低</li><li>RDB 内部存储的是 Redis 在某个时间点的数据快照,非常<strong>适合用于数据备份,全量复制、灾难恢复</strong></li><li>RDB 恢复数据的速度要比 AOF 快很多,因为是快照,直接恢复</li></ul></li><li><p>RDB 缺点:</p><ul><li>BGSAVE 指令每次运行要执行 fork 操作创建子进程,会牺牲一些性能</li><li>RDB 方式无论是执行指令还是利用配置,无法做到实时持久化,具有丢失数据的可能性,最后一次持久化后的数据可能丢失</li><li>Redis 的众多版本中未进行 RDB 文件格式的版本统一,可能出现各版本之间数据格式无法兼容</li></ul></li></ul><p>AOF 特点:</p><ul><li>AOF 的优点:数据持久化有较好的实时性,通过 AOF 重写可以降低文件的体积</li><li>AOF 的缺点:文件较大时恢复较慢</li></ul><p>AOF 和 RDB 同时开启,系统默认取 AOF 的数据(数据不会存在丢失)</p><p>应用场景:</p><ul><li><p>对数据<strong>非常敏感</strong>,建议使用默认的 AOF 持久化方案,AOF 持久化策略使用 everysecond,每秒钟 fsync 一次,该策略 Redis 仍可以保持很好的处理性能</p><p>注意:AOF 文件存储体积较大,恢复速度较慢,因为要执行每条指令</p></li><li><p>数据呈现<strong>阶段有效性</strong>,建议使用 RDB 持久化方案,可以做到阶段内无丢失,且恢复速度较快</p><p>注意:利用 RDB 实现紧凑的数据持久化,存储数据量较大时,存储效率较低</p></li></ul><p>综合对比:</p><ul><li>RDB 与 AOF 的选择实际上是在做一种权衡,每种都有利有弊</li><li>灾难恢复选用 RDB</li><li>如不能承受数分钟以内的数据丢失,对业务数据非常敏感,选用 AOF;如能承受数分钟以内的数据丢失,且追求大数据集的恢复速度,选用 RDB</li><li>双保险策略,同时开启 RDB 和 AOF,重启后 Redis 优先使用 AOF 来恢复数据,降低丢失数据的量</li><li>不建议单独用 AOF,因为可能会出现 Bug,如果只是做纯内存缓存,可以都不用</li></ul><hr><h3 id="fork">fork</h3><h4 id="介绍">介绍</h4><p>fork() 函数创建一个子进程,子进程与父进程几乎是完全相同的进程,系统先给子进程分配资源,然后把父进程的所有数据都复制到子进程中,只有少数值与父进程的值不同,相当于克隆了一个进程</p><p>在完成对其调用之后,会产生 2 个进程,且每个进程都会<strong>从 fork() 的返回处开始执行</strong>,这两个进程将执行相同的程序段,但是拥有各自不同的堆段,栈段,数据段,每个子进程都可修改各自的数据段,堆段,和栈段</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span><span class="string"><unistd.h></span></span></span><br><span class="line"><span class="type">pid_t</span> <span class="title function_">fork</span><span class="params">(<span class="type">void</span>)</span>;</span><br><span class="line"><span class="comment">// 父进程返回子进程的pid,子进程返回0,错误返回负值,根据返回值的不同进行对应的逻辑处理</span></span><br></pre></td></tr></table></figure><p>fork 调用一次,却能够<strong>返回两次</strong>,可能有三种不同的返回值:</p><ul><li>在父进程中,fork 返回新创建子进程的进程 ID</li><li>在子进程中,fork 返回 0</li><li>如果出现错误,fork 返回一个负值,错误原因:<ul><li>当前的进程数已经达到了系统规定的上限,这时 errno 的值被设置为 EAGAIN</li><li>系统内存不足,这时 errno 的值被设置为 ENOMEM</li></ul></li></ul><p>fpid 的值在父子进程中不同:进程形成了链表,父进程的 fpid 指向子进程的进程 id,因为子进程没有子进程,所以其 fpid 为0</p><p>创建新进程成功后,系统中出现两个基本完全相同的进程,这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的调度策略</p><p>每个进程都有一个独特(互不相同)的进程标识符 process ID,可以通过 getpid() 函数获得;还有一个记录父进程 pid 的变量,可以通过 getppid() 函数获得变量的值</p><hr><h4 id="使用">使用</h4><p>基本使用:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><unistd.h></span> </span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span> </span></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span> <span class="params">()</span> </span><br><span class="line">{ </span><br><span class="line"> <span class="type">pid_t</span> fpid; <span class="comment">// fpid表示fork函数返回的值 </span></span><br><span class="line"> <span class="type">int</span> count = <span class="number">0</span>; </span><br><span class="line"> fpid = fork(); </span><br><span class="line"> <span class="keyword">if</span> (fpid < <span class="number">0</span>) </span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"error in fork!"</span>); </span><br><span class="line"> <span class="keyword">else</span> <span class="keyword">if</span> (fpid == <span class="number">0</span>) { </span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"i am the child process, my process id is %d/n"</span>, getpid()); </span><br><span class="line"> count++; </span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">else</span> { </span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"i am the parent process, my process id is %d/n"</span>, getpid()); </span><br><span class="line"> count++; </span><br><span class="line"> } </span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"count: %d/n"</span>,count);<span class="comment">// 1 </span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>; </span><br><span class="line">} </span><br><span class="line"><span class="comment">/* 输出内容:</span></span><br><span class="line"><span class="comment"> i am the child process, my process id is 5574</span></span><br><span class="line"><span class="comment"> count: 1</span></span><br><span class="line"><span class="comment"> i am the parent process, my process id is 5573</span></span><br><span class="line"><span class="comment"> count: 1</span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure><p>进阶使用:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><unistd.h></span> </span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string"><stdio.h></span> </span></span><br><span class="line"><span class="type">int</span> <span class="title function_">main</span><span class="params">(<span class="type">void</span>)</span> </span><br><span class="line">{ </span><br><span class="line"> <span class="type">int</span> i = <span class="number">0</span>; </span><br><span class="line"> <span class="comment">// ppid 指当前进程的父进程pid </span></span><br><span class="line"> <span class="comment">// pid 指当前进程的pid, </span></span><br><span class="line"> <span class="comment">// fpid 指fork返回给当前进程的值,在这可以表示子进程</span></span><br><span class="line"> <span class="keyword">for</span>(i = <span class="number">0</span>; i < <span class="number">2</span>; i++){ </span><br><span class="line"> <span class="type">pid_t</span> fpid = fork(); </span><br><span class="line"> <span class="keyword">if</span>(fpid == <span class="number">0</span>) </span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d child %4d %4d %4d/n"</span>,i, getppid(), getpid(), fpid); </span><br><span class="line"> <span class="keyword">else</span> </span><br><span class="line"> <span class="built_in">printf</span>(<span class="string">"%d parent %4d %4d %4d/n"</span>,i, getppid(), getpid(),fpid); </span><br><span class="line"> } </span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>; </span><br><span class="line">} </span><br><span class="line"><span class="comment">/*输出内容:</span></span><br><span class="line"><span class="comment">i 父id id 子id</span></span><br><span class="line"><span class="comment">0 parent 2043 3224 3225</span></span><br><span class="line"><span class="comment"> 0 child 3224 3225 0</span></span><br><span class="line"><span class="comment"> 1 parent 2043 3224 3226</span></span><br><span class="line"><span class="comment"> 1 parent 3224 3225 3227</span></span><br><span class="line"><span class="comment"> 1 child 1 3227 0</span></span><br><span class="line"><span class="comment"> 1 child 1 3226 0 </span></span><br><span class="line"><span class="comment">*/</span></span><br></pre></td></tr></table></figure><img src="../image/post/Redis-fork%E5%87%BD%E6%95%B0%E4%BD%BF%E7%94%A8%E6%BC%94%E7%A4%BA.png" style="zoom: 80%;" /><p>在 p3224 和 p3225 执行完第二个循环后,main 函数退出,进程死亡。所以 p3226,p3227 就没有父进程了,成为孤儿进程,所以 p3226 和 p3227 的父进程就被置为 ID 为 1的 init 进程(笔记 Tool → Linux → 进程管理详解)</p><p>参考文章:<a href="https://blog.csdn.net/love_gaohz/article/details/41727415">https://blog.csdn.net/love_gaohz/article/details/41727415</a></p><hr><h4 id="内存-2">内存</h4><p>fork() 调用之后父子进程的内存关系</p><p>早期 Linux 的 fork() 实现时,就是全部复制,这种方法效率太低,而且造成了很大的内存浪费,现在 Linux 实现采用了两种方法:</p><ul><li><p>父子进程的代码段是相同的,所以代码段是没必要复制的,只需内核将代码段标记为只读,父子进程就共享此代码段。fork() 之后在进程创建代码段时,子进程的进程级页表项都指向和父进程相同的物理页帧</p> <img src="../image/post/Redis-fork%E4%BB%A5%E5%90%8E%E5%86%85%E5%AD%98%E5%85%B3%E7%B3%BB1.png" style="zoom: 67%;" /></li><li><p>对于父进程的数据段,堆段,栈段中的各页,由于父子进程相互独立,采用<strong>写时复制 COW</strong> 的技术,来提高内存以及内核的利用率</p><p>在 fork 之后两个进程用的是相同的物理空间(内存区),子进程的代码段、数据段、堆栈都是指向父进程的物理空间,<strong>两者的虚拟空间不同,但其对应的物理空间是同一个</strong>,当父子进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。如果两者的代码完全相同,代码段继续共享父进程的物理空间;而如果两者执行的代码不同,子进程的代码段也会分配单独的物理空间。</p><p>fork 之后内核会将子进程放在队列的前面,让子进程先执行,以免父进程执行导致写时复制,而后子进程再执行,因无意义的复制而造成效率的下降</p> <img src="../image/post/Redis-fork%E4%BB%A5%E5%90%8E%E5%86%85%E5%AD%98%E5%85%B3%E7%B3%BB2.png" style="zoom:67%;" /></li></ul><p>补充知识:</p><p>vfork(虚拟内存 fork virtual memory fork):调用 vfork() 父进程被挂起,子进程使用父进程的地址空间。不采用写时复制,如果子进程修改父地址空间的任何页面,这些修改过的页面对于恢复的父进程是可见的</p><p>参考文章:<a href="https://blog.csdn.net/Shreck66/article/details/47039937">https://blog.csdn.net/Shreck66/article/details/47039937</a></p><hr><h2 id="事务机制">事务机制</h2><h3 id="基本操作">基本操作</h3><p>Redis 事务的主要作用就是串联多个命令防止别的命令插队</p><ul><li><p>开启事务</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">multi<span class="comment">#设定事务的开启位置,此指令执行后,后续的所有指令均加入到事务中</span></span><br></pre></td></tr></table></figure></li><li><p>执行事务</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">exec</span><span class="comment">#设定事务的结束位置,同时执行事务,与multi成对出现,成对使用</span></span><br></pre></td></tr></table></figure><p>加入事务的命令暂时进入到任务队列中,并没有立即执行,只有执行 exec 命令才开始执行</p></li><li><p>取消事务</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">discard<span class="comment">#终止当前事务的定义,发生在multi之后,exec之前</span></span><br></pre></td></tr></table></figure><p>一般用于事务执行过程中输入了错误的指令,直接取消这次事务,类似于回滚</p></li></ul><p>Redis 事务的三大特性:</p><ul><li>Redis 事务是一个单独的隔离操作,将一系列预定义命令包装成一个整体(一个队列),当执行时按照添加顺序依次执行,中间不会被打断或者干扰</li><li>Redis 事务<strong>没有隔离级别</strong>的概念,队列中的命令在事务没有提交之前都不会实际被执行</li><li>Redis 单条命令式保存原子性的,但是事务<strong>不保证原子性</strong>,事务中如果有一条命令执行失败,其后的命令仍然会被执行,没有回滚</li></ul><hr><h3 id="工作流程">工作流程</h3><p>事务机制整体工作流程:</p><p><img src="../image/post/image-20240707155933038.png" alt="image-20240707155933038"></p><p>几种常见错误:</p><ul><li><p>定义事务的过程中,命令格式输入错误,出现语法错误造成,<strong>整体事务中所有命令均不会执行</strong>,包括那些语法正确的命令</p></li><li><p>定义事务的过程中,命令执行出现错误,例如对字符串进行 incr 操作,能够正确运行的命令会执行,运行错误的命令不会被执行</p></li><li><p>已经执行完毕的命令对应的数据不会自动回滚,需要程序员在代码中实现回滚,应该尽可能避免:</p><p>事务操作之前记录数据的状态</p><ul><li>单数据:string</li><li>多数据:hash、list、set、zset</li></ul><p>设置指令恢复所有的被修改的项</p><ul><li>单数据:直接 set(注意周边属性,例如时效)</li><li>多数据:修改对应值或整体克隆复制</li></ul></li></ul><hr><h3 id="监控锁">监控锁</h3><p>对 key 添加监视锁,是一种乐观锁,在执行 exec 前如果其他客户端的操作导致 key 发生了变化,执行结果为 nil</p><ul><li><p>添加监控锁</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">watch key1 [key2……]<span class="comment">#可以监控一个或者多个key</span></span><br></pre></td></tr></table></figure></li><li><p>取消对所有 key 的监视</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">unwatch</span><br></pre></td></tr></table></figure></li></ul><p>应用:基于状态控制的批量任务执行,防止其他线程对变量的修改</p><hr><h2 id="内存淘汰">内存淘汰</h2><h3 id="逐出算法">逐出算法</h3><p>数据淘汰策略:当新数据进入 Redis 时,在执行每一个命令前,会调用 <strong>freeMemoryIfNeeded()</strong> 检测内存是否充足。如果内存不满足新加入数据的最低存储要求,Redis 要临时删除一些数据为当前指令清理存储空间,清理数据的策略称为<strong>逐出算法</strong></p><p>逐出数据的过程不是 100% 能够清理出足够的可使用的内存空间,如果不成功则反复执行,当对所有数据尝试完毕,如不能达到内存清理的要求,<strong>出现 Redis 内存打满异常</strong>:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">(error) OOM <span class="built_in">command</span> not allowed when used memory ><span class="string">'maxmemory'</span></span><br></pre></td></tr></table></figure><hr><h3 id="策略配置">策略配置</h3><p>Redis 如果不设置最大内存大小或者设置最大内存大小为 0,在 64 位操作系统下不限制内存大小,在 32 位操作系统默认为 3GB 内存,一般推荐设置 Redis 内存为最大物理内存的四分之三</p><p>内存配置方式:</p><ul><li><p>通过修改文件配置(永久生效):修改配置文件 maxmemory 字段,单位为字节</p></li><li><p>通过命令修改(重启失效):</p><ul><li><p><code>config set maxmemory 104857600</code>:设置 Redis 最大占用内存为 100MB</p></li><li><p><code>config get maxmemory</code>:获取 Redis 最大占用内存</p></li><li><p><code>info</code> :可以查看 Redis 内存使用情况,<code>used_memory_human</code> 字段表示实际已经占用的内存,<code>maxmemory</code> 表示最大占用内存</p></li></ul></li></ul><p>影响数据淘汰的相关配置如下,配置 conf 文件:</p><ul><li><p>每次选取待删除数据的个数,采用随机获取数据的方式作为待检测删除数据,防止全库扫描,导致严重的性能消耗,降低读写性能</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">maxmemory-samples count</span><br></pre></td></tr></table></figure></li><li><p>达到最大内存后的,对被挑选出来的数据进行删除的策略</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">maxmemory-policy policy</span><br></pre></td></tr></table></figure><p>数据删除的策略 policy:3 类 8 种</p><p>第一类:检测易失数据(可能会过期的数据集 server.db[i].expires):</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">volatile-lru<span class="comment"># 对设置了过期时间的 key 选择最近最久未使用使用的数据淘汰</span></span><br><span class="line">volatile-lfu<span class="comment"># 对设置了过期时间的 key 选择最近使用次数最少的数据淘汰</span></span><br><span class="line">volatile-ttl<span class="comment"># 对设置了过期时间的 key 选择将要过期的数据淘汰</span></span><br><span class="line">volatile-random<span class="comment"># 对设置了过期时间的 key 选择任意数据淘汰</span></span><br></pre></td></tr></table></figure><p>第二类:检测全库数据(所有数据集 server.db[i].dict ):</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">allkeys-lru<span class="comment"># 对所有 key 选择最近最少使用的数据淘汰</span></span><br><span class="line">allkeLyRs-lfu<span class="comment"># 对所有 key 选择最近使用次数最少的数据淘汰</span></span><br><span class="line">allkeys-random<span class="comment"># 对所有 key 选择任意数据淘汰,相当于随机</span></span><br></pre></td></tr></table></figure><p>第三类:放弃数据驱逐</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">no-enviction<span class="comment">#禁止驱逐数据(redis4.0中默认策略),会引发OOM(Out Of Memory)</span></span><br></pre></td></tr></table></figure></li></ul><p>数据淘汰策略配置依据:使用 INFO 命令输出监控信息,查询缓存 hit 和 miss 的次数,根据需求调优 Redis 配置</p><hr><h2 id="缓存方案">缓存方案</h2><h3 id="缓存模式">缓存模式</h3><h4 id="旁路缓存">旁路缓存</h4><p>缓存本质:弥补 CPU 的高算力和 IO 的慢读写之间巨大的鸿沟</p><p>旁路缓存模式 Cache Aside Pattern 是平时使用比较多的一个缓存读写模式,比较适合读请求比较多的场景</p><p>Cache Aside Pattern 中服务端需要同时维系 DB 和 cache,并且是以 DB 的结果为准</p><ul><li>写操作:先更新 DB,然后直接删除 cache</li><li>读操作:从 cache 中读取数据,读取到就直接返回;读取不到就从 DB 中读取数据返回,并放到 cache</li></ul><p>时序导致的不一致问题:</p><ul><li><p>在写数据的过程中,不能先删除 cache 再更新 DB,因为会造成缓存的不一致。比如请求 1 先写数据 A,请求 2 随后读数据 A,当请求 1 删除 cache 后,请求 2 直接读取了 DB,此时请求 1 还没写入 DB(延迟双删)</p></li><li><p>在写数据的过程中,先更新 DB 再删除 cache 也会出现问题,但是概率很小,因为缓存的写入速度非常快</p></li></ul><p>旁路缓存的缺点:</p><ul><li>首次请求数据一定不在 cache 的问题,一般采用缓存预热的方法,将热点数据可以提前放入 cache 中</li><li>写操作比较频繁的话导致 cache 中的数据会被频繁被删除,影响缓存命中率</li></ul><hr><h4 id="读写穿透">读写穿透</h4><p>读写穿透模式 Read/Write Through Pattern:服务端把 cache 视为主要数据存储,从中读取数据并将数据写入其中,cache 负责将此数据同步写入 DB,从而减轻了应用程序的职责</p><ul><li><p>写操作:先查 cache,cache 中不存在,直接更新 DB;cache 中存在则先更新 cache,然后 cache 服务更新 DB(同步更新 cache 和 DB)</p></li><li><p>读操作:从 cache 中读取数据,读取到就直接返回 ;读取不到先从 DB 加载,写入到 cache 后返回响应</p><p>Read-Through Pattern 实际只是在 Cache-Aside Pattern 之上进行了封装。在 Cache-Aside Pattern 下,发生读请求的时候,如果 cache 中不存在对应的数据,是由客户端负责把数据写入 cache,而 Read Through Pattern 则是 cache 服务自己来写入缓存的,对客户端是透明的</p></li></ul><p>Read-Through Pattern 也存在首次不命中的问题,采用缓存预热解决</p><hr><h4 id="异步缓存">异步缓存</h4><p>异步缓存写入 Write Behind Pattern 由 cache 服务来负责 cache 和 DB 的读写,对比读写穿透不同的是 Write Behind Caching 是只更新缓存,不直接更新 DB,改为<strong>异步批量</strong>的方式来更新 DB,可以减小写的成本</p><p>缺点:这种模式对数据一致性没有高要求,可能出现 cache 还没异步更新 DB,服务就挂掉了</p><p>应用:</p><ul><li><p>DB 的写性能非常高,适合一些数据经常变化又对数据一致性要求不高的场景,比如浏览量、点赞量</p></li><li><p>MySQL 的 InnoDB Buffer Pool 机制用到了这种策略</p></li></ul><hr><h3 id="缓存一致">缓存一致</h3><p>使用缓存代表不需要强一致性,只需要最终一致性</p><p>缓存不一致的方法:</p><ul><li>数据库和缓存数据强一致场景:<ul><li>更新 DB 时同样更新 cache,加一个锁来保证更新 cache 时不存在线程安全问题,这样可以增加命中率</li><li>延迟双删:先淘汰缓存再写数据库,休眠 1 秒再次淘汰缓存,可以将 1 秒内造成的缓存脏数据再次删除</li><li>CDC 同步:通过 canal 订阅 MySQL binlog 的变更上报给 Kafka,系统监听 Kafka 消息触发缓存失效</li></ul></li><li>可以短暂允许数据库和缓存数据不一致场景:更新 DB 的时候同样更新 cache,但是给缓存加一个比较短的过期时间,这样就可以保证即使数据不一致影响也比较小</li></ul><p>参考文章:<a href="http://cccboke.com/archives/2020-09-30-21-29-56">http://cccboke.com/archives/2020-09-30-21-29-56</a></p><hr><h3 id="企业方案">企业方案</h3><h4 id="缓存预热">缓存预热</h4><p>场景:宕机,服务器启动后迅速宕机</p><p>问题排查:</p><ol><li><p>请求数量较高,大量的请求过来之后都需要去从缓存中获取数据,但是缓存中又没有,此时从数据库中查找数据然后将数据再存入缓存,造成了短期内对 redis 的高强度操作从而导致问题</p></li><li><p>主从之间数据吞吐量较大,数据同步操作频度较高</p></li></ol><p>解决方案:</p><ul><li><p>前置准备工作:</p><ol><li><p>日常例行统计数据访问记录,统计访问频度较高的热点数据</p></li><li><p>利用 LRU 数据删除策略,构建数据留存队列例如:storm 与 kafka 配合</p></li></ol></li><li><p>准备工作:</p><ol><li><p>将统计结果中的数据分类,根据级别,redis 优先加载级别较高的热点数据</p></li><li><p>利用分布式多服务器同时进行数据读取,提速数据加载过程</p></li><li><p>热点数据主从同时预热</p></li></ol></li><li><p>实施:</p><ol start="4"><li><p>使用脚本程序固定触发数据预热过程</p></li><li><p>如果条件允许,使用了 CDN(内容分发网络),效果会更好</p></li></ol></li></ul><p>总的来说:缓存预热就是系统启动前,提前将相关的缓存数据直接加载到缓存系统。避免在用户请求的时候,先查询数据库,然后再将数据缓存的问题,用户直接查询事先被预热的缓存数据!</p><hr><h4 id="缓存雪崩">缓存雪崩</h4><p>场景:数据库服务器崩溃,一连串的问题会随之而来</p><p>问题排查:在一个较短的时间内,<strong>缓存中较多的 key 集中过期</strong>,此周期内请求访问过期的数据 Redis 未命中,Redis 向数据库获取数据,数据库同时收到大量的请求无法及时处理。</p><p>解决方案:</p><ol><li>加锁,慎用</li><li>设置热点数据永远不过期,如果缓存数据库是分布式部署,将热点数据均匀分布在不同搞得缓存数据库中</li><li>缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生</li><li>构建<strong>多级缓存</strong>架构,Nginx 缓存 + Redis 缓存 + ehcache 缓存</li><li>灾难预警机制,监控 Redis 服务器性能指标,CPU 使用率、内存容量、平均响应时间、线程数</li><li>限流、降级:短时间范围内牺牲一些客户体验,限制一部分请求访问,降低应用服务器压力,待业务低速运转后再逐步放开访问</li></ol><p>总的来说:缓存雪崩就是瞬间过期数据量太大,导致对数据库服务器造成压力。如能够有效避免过期时间集中,可以有效解决雪崩现象的出现(约 40%),配合其他策略一起使用,并监控服务器的运行数据,根据运行记录做快速调整。</p><hr><h4 id="缓存击穿">缓存击穿</h4><p>场景:系统平稳运行过程中,数据库连接量瞬间激增,Redis 服务器无大量 key 过期,Redis 内存平稳无波动,Redis 服务器 CPU 正常,但是数据库崩溃</p><p>问题排查:</p><ol><li><p><strong>Redis 中某个 key 过期,该 key 访问量巨大</strong></p></li><li><p>多个数据请求从服务器直接压到 Redis 后,均未命中</p></li><li><p>Redis 在短时间内发起了大量对数据库中同一数据的访问</p></li></ol><p>简而言之两点:单个 key 高热数据,key 过期</p><p>解决方案:</p><ol><li><p>预先设定:以电商为例,每个商家根据店铺等级,指定若干款主打商品,在购物节期间,加大此类信息 key 的过期时长 注意:购物节不仅仅指当天,以及后续若干天,访问峰值呈现逐渐降低的趋势</p></li><li><p>现场调整:监控访问量,对自然流量激增的数据<strong>延长过期时间或设置为永久性 key</strong></p></li><li><p>后台刷新数据:启动定时任务,高峰期来临之前,刷新数据有效期,确保不丢失</p></li><li><p><strong>二级缓存</strong>:设置不同的失效时间,保障不会被同时淘汰就行</p></li><li><p>加锁:分布式锁,防止被击穿,但是要注意也是性能瓶颈,慎重</p></li></ol><p>总的来说:缓存击穿就是单个高热数据过期的瞬间,数据访问量较大,未命中 Redis 后,发起了大量对同一数据的数据库访问,导致对数据库服务器造成压力。应对策略应该在业务数据分析与预防方面进行,配合运行监控测试与即时调整策略,毕竟单个 key 的过期监控难度较高,配合雪崩处理策略即可</p><hr><h4 id="缓存穿透">缓存穿透</h4><p>场景:系统平稳运行过程中,应用服务器流量随时间增量较大,Redis 服务器命中率随时间逐步降低,Redis 内存平稳,内存无压力,Redis 服务器 CPU 占用激增,数据库服务器压力激增,数据库崩溃</p><p>问题排查:</p><ol><li><p>Redis 中大面积出现未命中</p></li><li><p>出现非正常 URL 访问</p></li></ol><p>问题分析:</p><ul><li>访问了不存在的数据,跳过了 Redis 缓存,数据库页查询不到对应数据</li><li>Redis 获取到 null 数据未进行持久化,直接返回</li><li>出现黑客攻击服务器</li></ul><p>解决方案:</p><ol><li><p>缓存 null:对查询结果为 null 的数据进行缓存,设定短时限,例如 30-60 秒,最高 5 分钟</p></li><li><p>白名单策略:提前预热各种分类<strong>数据 id 对应的 bitmaps</strong>,id 作为 bitmaps 的 offset,相当于设置了数据白名单。当加载正常数据时放行,加载异常数据时直接拦截(效率偏低),也可以使用布隆过滤器(有关布隆过滤器的命中问题对当前状况可以忽略)</p></li><li><p>实时监控:实时监控 Redis 命中率(业务正常范围时,通常会有一个波动值)与 null 数据的占比</p><ul><li>非活动时段波动:通常检测 3-5 倍,超过 5 倍纳入重点排查对象</li><li>活动时段波动:通常检测10-50 倍,超过 50 倍纳入重点排查对象</li></ul><p>根据倍数不同,启动不同的排查流程。然后使用黑名单进行防控</p></li><li><p>key 加密:临时启动防灾业务 key,对 key 进行业务层传输加密服务,设定校验程序,过来的 key 校验;例如每天随机分配 60 个加密串,挑选 2 到 3 个,混淆到页面数据 id 中,发现访问 key 不满足规则,驳回数据访问</p></li></ol><p>总的来说:缓存击穿是指访问了不存在的数据,跳过了合法数据的 Redis 数据缓存阶段,每次访问数据库,导致对数据库服务器造成压力。通常此类数据的出现量是一个较低的值,当出现此类情况以毒攻毒,并及时报警。无论是黑名单还是白名单,都是对整体系统的压力,警报解除后尽快移除</p><p>参考视频:<a href="https://www.bilibili.com/video/BV15y4y1r7X3">https://www.bilibili.com/video/BV15y4y1r7X3</a></p><hr><h3 id="性能指标">性能指标</h3><p>Redis 中的监控指标如下:</p><ul><li><p>性能指标:Performance</p><p>响应请求的平均时间:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">latency</span><br></pre></td></tr></table></figure><p>平均每秒处理请求总数:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">instantaneous_ops_per_sec</span><br></pre></td></tr></table></figure><p>缓存查询命中率(通过查询总次数与查询得到非nil数据总次数计算而来):</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">hit_rate(calculated)</span><br></pre></td></tr></table></figure></li><li><p>内存指标:Memory</p><p>当前内存使用量:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">used_memory</span><br></pre></td></tr></table></figure><p>内存碎片率(关系到是否进行碎片整理):</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mem_fragmentation_ratio</span><br></pre></td></tr></table></figure><p>为避免内存溢出删除的key的总数量:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">evicted_keys</span><br></pre></td></tr></table></figure><p>基于阻塞操作(BLPOP等)影响的客户端数量:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">blocked_clients</span><br></pre></td></tr></table></figure></li><li><p>基本活动指标:Basic_activity</p><p>当前客户端连接总数:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">connected_clients</span><br></pre></td></tr></table></figure><p>当前连接 slave 总数:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">connected_slaves</span><br></pre></td></tr></table></figure><p>最后一次主从信息交换距现在的秒:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">master_last_io_seconds_ago</span><br></pre></td></tr></table></figure><p>key 的总数:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keyspace</span><br></pre></td></tr></table></figure></li><li><p>持久性指标:Persistence</p><p>当前服务器其最后一次 RDB 持久化的时间:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rdb_last_save_time</span><br></pre></td></tr></table></figure><p>当前服务器最后一次 RDB 持久化后数据变化总量:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rdb_changes_since_last_save</span><br></pre></td></tr></table></figure></li><li><p>错误指标:Error</p><p>被拒绝连接的客户端总数(基于达到最大连接值的因素):</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">rejected_connections</span><br></pre></td></tr></table></figure><p>key未命中的总次数:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">keyspace_misses</span><br></pre></td></tr></table></figure><p>主从断开的秒数:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">master_link_down_since_seconds</span><br></pre></td></tr></table></figure></li></ul><p>要对 Redis 的相关指标进行监控,我们可以采用一些用具:</p><ul><li>CloudInsight Redis</li><li>Prometheus</li><li>Redis-stat</li><li>Redis-faina</li><li>RedisLive</li><li>zabbix</li></ul><p>命令工具:</p><ul><li><p>benchmark</p><p>测试当前服务器的并发性能:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-benchmark [-h ] [-p ] [-c ] [-n <requests]> [-k ]</span><br></pre></td></tr></table></figure><p>范例:100 个连接,5000 次请求对应的性能</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">redis-benchmark -c 100 -n 5000</span><br></pre></td></tr></table></figure></li><li><p>redis-cli</p><p>monitor:启动服务器调试信息</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">monitor</span><br></pre></td></tr></table></figure><p>slowlog:慢日志</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">slowlog [operator] <span class="comment">#获取慢查询日志</span></span><br></pre></td></tr></table></figure><ul><li>get :获取慢查询日志信息</li><li>len :获取慢查询日志条目数</li><li>reset :重置慢查询日志</li></ul><p>相关配置:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">slowlog-log-slower-than 1000 <span class="comment">#设置慢查询的时间下线,单位:微妙</span></span><br><span class="line">slowlog-max-len 100<span class="comment">#设置慢查询命令对应的日志显示长度,单位:命令数</span></span><br></pre></td></tr></table></figure></li></ul><hr><h2 id="分布式锁">分布式锁</h2><blockquote><p>参考文章:<a href="https://www.pdai.tech/md/arch/arch-z-lock.html#%E5%9F%BA%E4%BA%8Eredis%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81-%E6%9C%89%E4%BB%80%E4%B9%88%E7%BC%BA%E9%99%B7">https://www.pdai.tech/md/arch/arch-z-lock.html#基于redis如何实现分布式锁-有什么缺陷</a></p></blockquote><h3 id="基本操作-2">基本操作</h3><p>由于分布式系统多线程并发分布在不同机器上,这将使单机部署情况下的并发控制锁策略失效,需要分布式锁</p><p>Redis 分布式锁的基本使用,悲观锁</p><ul><li><p>使用 setnx 设置一个公共锁</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">setnx lock-key value<span class="comment"># value任意数,返回为1设置成功,返回为0设置失败</span></span><br></pre></td></tr></table></figure><ul><li>对于返回设置成功的,拥有控制权,进行下一步的具体业务操作</li><li>对于返回设置失败的,不具有控制权,排队或等待</li></ul><p><code>NX</code>:只在键不存在时,才对键进行设置操作,<code>SET key value NX</code> 效果等同于 <code>SETNX key value</code></p><p><code>XX</code> :只在键已经存在时,才对键进行设置操作</p><p><code>EX</code>:设置键 key 的过期时间,单位时秒</p><p><code>PX</code>:设置键 key 的过期时间,单位时毫秒</p><p>说明:由于 <code>SET</code> 命令加上选项已经可以完全取代 SETNX、SETEX、PSETEX 的功能,Redis 不推荐使用这几个命令</p></li><li><p>操作完毕通过 del 操作释放锁</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">del lock-key </span><br></pre></td></tr></table></figure></li><li><p>使用 expire 为锁 key 添加存活(持有)时间,过期自动删除(放弃)锁</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">expire lock-key second </span><br><span class="line">pexpire lock-key milliseconds</span><br></pre></td></tr></table></figure><p>通过 expire 设置过期时间缺乏原子性,如果在 setnx 和 expire 之间出现异常,锁也无法释放</p></li><li><p>在 set 时指定过期时间</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET key value [EX seconds | PX milliseconds] NX</span><br></pre></td></tr></table></figure></li></ul><p>应用:解决抢购时出现超卖现象</p><hr><h3 id="防误删">防误删</h3><p>setnx 获取锁时,设置一个指定的唯一值(uuid),释放前获取这个值,判断是否自己的锁,防止出现线程之间误删了其他线程的锁</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 加锁, unique_value作为客户端唯一性的标识</span></span><br><span class="line">SET lock_key unique_value NX PX <span class="number">10000</span></span><br></pre></td></tr></table></figure><p>unique_value 是客户端的<strong>唯一标识</strong>,可以用一个随机生成的字符串来表示,PX 10000 则表示 lock_key 会在 10s 后过期,以免客户端在这期间发生异常而无法释放锁</p><hr><h2 id="redission(watchdog-lock-tryLock-jedis)">redission(watchdog/lock/tryLock/jedis)</h2><blockquote><p>参考文章:<a href="https://javaguide.cn/distributed-system/distributed-lock-implementations.html#%E5%9F%BA%E4%BA%8E-redis-%E5%AE%9E%E7%8E%B0%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81">https://javaguide.cn/distributed-system/distributed-lock-implementations.html#基于-redis-实现分布式锁</a></p></blockquote><hr><h2 id="key过期(过期删除)">key过期(过期删除)</h2><h3 id="删除策略">删除策略</h3><p>删除策略就是<strong>针对已过期数据的处理策略</strong>,已过期的数据不一定被立即删除,在不同的场景下使用不同的删除方式会有不同效果,在内存占用与 CPU 占用之间寻找一种平衡,顾此失彼都会造成整体 Redis 性能的下降,甚至引发服务器宕机或内存泄露</p><p>针对过期数据有三种删除策略:</p><ul><li>定时删除</li><li>惰性删除</li><li>定期删除</li></ul><p><strong>Redis 采用<code>惰性删除</code>和<code>定期删除</code>策略的<code>结合使用</code></strong></p><hr><h3 id="定时删除">定时删除</h3><p>在设置键的过期时间的同时,创建一个定时器(timer),让定时器在键的过期时间到达时,立即执行对键的删除操作</p><ul><li>优点:节约内存,到时就删除,快速释放掉不必要的内存占用</li><li>缺点:对 CPU 不友好,无论 CPU 此时负载多高均占用 CPU,会影响 Redis 服务器响应时间和指令吞吐量</li><li>总结:用处理器性能换取存储空间(拿时间换空间)</li></ul><p>创建一个定时器需要用到 Redis 服务器中的时间事件,而时间事件的实现方式是无序链表,查找一个事件的时间复杂度为 O(N),并不能高效地处理大量时间事件,所以采用这种方式并不现实</p><hr><h3 id="惰性删除">惰性删除</h3><p>数据到达过期时间不做处理,等下次访问到该数据时执行 <strong>expireIfNeeded()</strong> 判断:</p><ul><li>如果输入键已经过期,那么 expireIfNeeded 函数将输入键从数据库中删除,接着访问就会返回空</li><li>如果输入键未过期,那么 expireIfNeeded 函数不做动作</li></ul><p>所有的 Redis 读写命令在执行前都会调用 expireIfNeeded 函数进行检查,该函数就像一个过滤器,在命令真正执行之前过滤掉过期键</p><p>惰性删除的特点:</p><ul><li>优点:节约 CPU 性能,删除的目标仅限于当前处理的键,不会在删除其他无关的过期键上花费任何 CPU 时间</li><li>缺点:内存压力很大,出现长期占用内存的数据,如果过期键永远不被访问,这种情况相当于内存泄漏</li><li>总结:用存储空间换取处理器性能(拿空间换时间)</li></ul><hr><h3 id="定期删除">定期删除</h3><p>定期删除策略是每隔一段时间执行一次删除过期键操作,并通过限制删除操作执行的时长和频率来减少删除操作对 CPU 时间的影响</p><ul><li>如果删除操作执行得太频繁,或者执行时间太长,就会退化成定时删除策略,将 CPU 时间过多地消耗在删除过期键上</li><li>如果删除操作执行得太少,或者执行时间太短,定期删除策略又会和惰性删除策略一样,出现浪费内存的情况</li></ul><p>所以采用定期删除策略的话,服务器必须根据情况合理地设置删除操作的执行时长和执行频率</p><p>定期删除是<strong>周期性轮询 Redis 库中的时效性</strong>数据,从过期字典中随机抽取一部分键检查,利用过期数据占比的方式控制删除频度</p><ul><li><p>Redis 启动服务器初始化时,读取配置 server.hz 的值,默认为 10,执行指令 info server 可以查看,每秒钟执行 server.hz 次 <code>serverCron() → activeExpireCycle()</code></p></li><li><p>activeExpireCycle() 对某个数据库中的每个 expires 进行检测,工作模式:</p><ul><li><p>轮询每个数据库,从数据库中取出一定数量的随机键进行检查,并删除其中的过期键</p></li><li><p>全局变量 current_db 用于记录 activeExpireCycle() 的检查进度(哪一个数据库),下一次调用时接着该进度处理</p></li><li><p>随着函数的不断执行,服务器中的所有数据库都会被检查一遍,这时将 current_db 重置为 0,然后再次开始新一轮的检查</p></li></ul></li></ul><p>定期删除特点:</p><ul><li>CPU 性能占用设置有峰值,检测频度可自定义设置</li><li>内存压力不是很大,长期占用内存的<strong>冷数据会被持续清理</strong></li><li>周期性抽查存储空间(随机抽查,重点抽查)</li></ul><hr>]]></content>
<summary type="html">DataBase-Redis初级相关知识学习,以Hillos为纲,JavaNote为主体整理的相关笔记。</summary>
<category term="DataBase" scheme="https://jovehawking.fun/tags/DataBase/"/>
<category term="数据库" scheme="https://jovehawking.fun/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="Redis初级" scheme="https://jovehawking.fun/tags/Redis%E5%88%9D%E7%BA%A7/"/>
</entry>
<entry>
<title>W-nextTick 和 async await</title>
<link href="https://jovehawking.fun/posts/288ab87b.html"/>
<id>https://jovehawking.fun/posts/288ab87b.html</id>
<published>2024-06-23T07:29:17.000Z</published>
<updated>2024-06-23T07:38:40.442Z</updated>
<content type="html"><![CDATA[<h1>$nextTick</h1><h2 id="nextTick-和-async-await">$nextTick 和 async await</h2><p>this.$nextTick 和 async/await 在Vue中都是用于处理异步操作的重要工具,但它们服务于不同的目的和场景。</p><h2 id="nextTick-使用场景">$nextTick 使用场景</h2><p>nextTick是vue提供出来更新视图之后回调的函数,也就是说我们在操作dom更新视图的时候,由于vue的视图渲染是异步的,可能会导致一些视图已经更新了,但是我们获取到的视图数据信息不是最新的,使用nextTick可以保证视图在下一次更新之后进行调用</p><p>参考文章:<a href="https://cloud.tencent.com/developer/article/2008569">https://cloud.tencent.com/developer/article/2008569</a></p>]]></content>
<summary type="html">前端开发时遇到的问题</summary>
<category term="工作inG" scheme="https://jovehawking.fun/categories/%E5%B7%A5%E4%BD%9CinG/"/>
<category term="前端理论" scheme="https://jovehawking.fun/tags/%E5%89%8D%E7%AB%AF%E7%90%86%E8%AE%BA/"/>
</entry>
<entry>
<title>W-vue中的this指向</title>
<link href="https://jovehawking.fun/posts/9619dfa9.html"/>
<id>https://jovehawking.fun/posts/9619dfa9.html</id>
<published>2024-06-23T07:28:59.000Z</published>
<updated>2024-06-23T07:38:40.439Z</updated>
<content type="html"><![CDATA[<h1>vue中的this指向</h1><h1>this指向</h1><p><strong>js:</strong></p><ul><li>普通函数,谁调用的它,this就指向谁,</li><li>箭头函数没有this,它的this指向一般就是上下文中,与谁调用它没关系。</li></ul><p><strong>vue:</strong></p><ul><li>methods、生命周期函数中如果用的是正常函数,那么它的this就指向<strong>Vue实例</strong>;</li><li>如果是箭头函数,在非严格模式下this就指向window对象,严格模式下是undefind。</li></ul><p>原因:vue 内部实际上对methods属性中的方法进行了遍历,将对应的方法通过bind绑定了this,使得this指向Vue实例</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// vue1.vue</span></span><br><span class="line">...</span><br><span class="line"><span class="title function_">function1</span>() {</span><br><span class="line"> <span class="variable language_">this</span>.<span class="property">xxx</span>=xxx;</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// vue2.vue</span></span><br><span class="line">...</span><br><span class="line"><span class="title function_">function2</span>(function1) {</span><br><span class="line"> <span class="title function_">function1</span>();</span><br><span class="line">}</span><br><span class="line">...</span><br><span class="line"></span><br><span class="line"><span class="comment">// vue2调用function1()时,this扔指向vue1,不指向vue2</span></span><br></pre></td></tr></table></figure><p>参考文章:<a href="https://juejin.cn/post/7109889547537743886"><strong>https://juejin.cn/post/7109889547537743886</strong></a></p>]]></content>
<summary type="html">前端开发时遇到的问题</summary>
<category term="工作inG" scheme="https://jovehawking.fun/categories/%E5%B7%A5%E4%BD%9CinG/"/>
<category term="前端理论" scheme="https://jovehawking.fun/tags/%E5%89%8D%E7%AB%AF%E7%90%86%E8%AE%BA/"/>
</entry>
<entry>
<title>DataBase-MyBatis初级学习</title>
<link href="https://jovehawking.fun/posts/a8bbac52.html"/>
<id>https://jovehawking.fun/posts/a8bbac52.html</id>
<published>2024-06-23T07:19:51.000Z</published>
<updated>2024-07-06T09:06:32.081Z</updated>
<content type="html"><![CDATA[<h1>MyBatis初级</h1><h2 id="目标">目标</h2><ol><li>优点</li><li># 和 $</li><li>插件机制</li><li>缓存机制</li><li>连接池</li><li>动态SQL</li><li>分页</li><li>MyBatisPlus</li></ol><h2 id="基本介绍">基本介绍</h2><p>ORM(Object Relational Mapping): 对象关系映射,指的是持久化数据和实体对象的映射模式,解决面向对象与关系型数据库存在的互不匹配的现象</p><p><img src="../image/post/MyBatis-ORM%E4%BB%8B%E7%BB%8D.png" alt=""></p><p><strong>MyBatis</strong>:</p><ul><li><p>MyBatis 是一个优秀的基于 Java 的持久层框架,它内部封装了 JDBC,使开发者只需关注 SQL 语句本身,而不需要花费精力去处理加载驱动、创建连接、创建 Statement 等过程。</p></li><li><p>MyBatis 通过 XML 或注解的方式将要执行的各种 Statement 配置起来,并通过 Java 对象和 Statement 中 SQL 的动态参数进行映射生成最终执行的 SQL 语句。</p></li><li><p>MyBatis 框架执行 SQL 并将结果映射为 Java 对象并返回。采用 ORM 思想解决了实体和数据库映射的问题,对 JDBC 进行了封装,屏蔽了 JDBC 底层 API 的调用细节,使我们不用操作 JDBC API,就可以完成对数据库的持久化操作。</p></li></ul><h2 id="和">#{}和${}</h2><p><strong>#{}:<strong>占位符,传入的内容会作为字符串</strong>加上引号</strong>,以<strong>预编译</strong>的方式传入,将 sql 中的 #{} 替换为 ? 号,调用 PreparedStatement 的 set 方法来赋值,有效的防止 SQL 注入,提高系统安全性</p><p><strong>${}:<strong>拼接符,传入的内容会</strong>直接替换</strong>拼接,不会加上引号,可能存在 sql 注入的安全隐患</p><ul><li><p>能用 #{} 的地方就用 #{},不用或少用 ${}</p></li><li><p>必须使用 ${} 的情况:</p><ul><li>表名作参数时,如:<code>SELECT * FROM ${tableName}</code></li><li>order by 时,如:<code>SELECT * FROM t_user ORDER BY ${columnName}</code></li></ul></li><li><p>sql 语句使用 #{},properties 文件内容获取使用 ${}</p></li></ul><h2 id="缓存机制">缓存机制</h2><h3 id="缓存概述">缓存概述</h3><p>缓存:缓存就是一块内存空间,保存临时数据</p><p>作用:将数据源(数据库或者文件)中的数据读取出来存放到缓存中,再次获取时直接从缓存中获取,可以减少和数据库交互的次数,提升程序的性能</p><p>缓存适用:</p><ul><li>适用于缓存的:经常查询但不经常修改的,数据的正确与否对最终结果影响不大的</li><li>不适用缓存的:经常改变的数据 , 敏感数据(例如:股市的牌价,银行的汇率,银行卡里面的钱)等等</li></ul><p>缓存类别:</p><ul><li>一级缓存:SqlSession 级别的缓存,又叫本地会话缓存,自带的(不需要配置),一级缓存的生命周期与 SqlSession 一致。在操作数据库时需要构造 SqlSession 对象,<strong>在对象中有一个数据结构(HashMap)用于存储缓存数据</strong>,不同的 SqlSession 之间的缓存数据区域是互相不影响的</li><li>二级缓存:mapper(namespace)级别的缓存,二级缓存的使用,需要手动开启(需要配置)。多个 SqlSession 去操作同一个 Mapper 的 SQL 可以共用二级缓存,二级缓存是跨 SqlSession 的</li></ul><p>开启缓存:配置核心配置文件中 <settings> 标签</p><ul><li>cacheEnabled:true 表示全局性地开启所有映射器配置文件中已配置的任何缓存,默认 true</li></ul><p><img src="../image/post/MyBatis-%E7%BC%93%E5%AD%98%E7%9A%84%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86.png" alt=""></p><p>参考文章:<a href="https://www.cnblogs.com/ysocean/p/7342498.html">https://www.cnblogs.com/ysocean/p/7342498.html</a></p><hr><h3 id="一级缓存">一级缓存</h3><p>一级缓存是 SqlSession 级别的缓存</p><img src="../image/post/MyBatis-%E4%B8%80%E7%BA%A7%E7%BC%93%E5%AD%98.png" style="zoom: 67%;" /><p>工作流程:第一次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,如果没有,从数据库查询用户信息,得到用户信息,将用户信息存储到一级缓存中;第二次发起查询用户 id 为 1 的用户信息,先去找缓存中是否有 id 为 1 的用户信息,缓存中有,直接从缓存中获取用户信息。</p><p>一级缓存的失效:</p><ul><li>SqlSession 不同</li><li>SqlSession 相同,查询条件不同时(还未缓存该数据)</li><li>SqlSession 相同,手动清除了一级缓存,调用 <code>sqlSession.clearCache()</code></li><li>SqlSession 相同,执行 commit 操作或者执行插入、更新、删除,清空 SqlSession 中的一级缓存,这样做的目的为了让缓存中存储的是最新的信息,<strong>避免脏读</strong></li></ul><p>Spring 整合 MyBatis 后,一级缓存作用:</p><ul><li>未开启事务的情况,每次查询 Spring 都会创建新的 SqlSession,因此一级缓存失效</li><li>开启事务的情况,Spring 使用 ThreadLocal 获取当前资源绑定同一个 SqlSession,因此此时一级缓存是有效的</li></ul><p>测试一级缓存存在</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testFirstLevelCache</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//1. 获取sqlSession对象</span></span><br><span class="line"> <span class="type">SqlSession</span> <span class="variable">sqlSession</span> <span class="operator">=</span> SqlSessionFactoryUtils.openSession();</span><br><span class="line"> <span class="comment">//2. 通过sqlSession对象获取UserDao接口的代理对象</span></span><br><span class="line"> <span class="type">UserDao</span> <span class="variable">userDao1</span> <span class="operator">=</span> sqlSession.getMapper(UserDao.class);</span><br><span class="line"> <span class="comment">//3. 调用UserDao接口的代理对象的findById方法获取信息</span></span><br><span class="line"><span class="type">User</span> <span class="variable">user1</span> <span class="operator">=</span> userDao1.findById(<span class="number">1</span>);</span><br><span class="line">System.out.println(user1);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//sqlSession.clearCache() 清空缓存</span></span><br><span class="line"> </span><br><span class="line"> <span class="type">UserDao</span> <span class="variable">userDao2</span> <span class="operator">=</span> sqlSession.getMapper(UserDao.class);</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> userDao.findById(<span class="number">1</span>);</span><br><span class="line"> System.out.println(user2);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//4.测试两次结果是否一样</span></span><br><span class="line"> System.out.println(user1 == user2);<span class="comment">//true</span></span><br><span class="line"> </span><br><span class="line"> <span class="comment">//5. 提交事务关闭资源</span></span><br><span class="line"> SqlSessionFactoryUtils.commitAndClose(sqlSession);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><h3 id="二级缓存">二级缓存</h3><h4 id="基本介绍-2">基本介绍</h4><p>二级缓存是 mapper 的缓存,只要是同一个命名空间(namespace)的 SqlSession 就共享二级缓存的内容,并且可以操作二级缓存</p><p>作用:作用范围是整个应用,可以跨线程使用,适合缓存一些修改较少的数据</p><p>工作流程:一个会话查询数据,这个数据就会被放在当前会话的一级缓存中,如果<strong>会话关闭或提交</strong>一级缓存中的数据会保存到二级缓存</p><p>二级缓存的基本使用:</p><ol><li><p>在 MyBatisConfig.xml 文件开启二级缓存,<strong>cacheEnabled 默认值为 true</strong>,所以这一步可以省略不配置</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!--配置开启二级缓存--></span></span><br><span class="line"><span class="tag"><<span class="name">settings</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">setting</span> <span class="attr">name</span>=<span class="string">"cacheEnabled"</span> <span class="attr">value</span>=<span class="string">"true"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">settings</span>></span></span><br></pre></td></tr></table></figure></li><li><p>配置 Mapper 映射文件</p><p><code><cache></code> 标签表示当前这个 mapper 映射将使用二级缓存,区分的标准就看 mapper 的 namespace 值</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">mapper</span> <span class="attr">namespace</span>=<span class="string">"dao.UserDao"</span>></span></span><br><span class="line"> <span class="comment"><!--开启user支持二级缓存--></span></span><br><span class="line"> <span class="tag"><<span class="name">cache</span> <span class="attr">eviction</span>=<span class="string">"FIFO"</span> <span class="attr">flushInterval</span>=<span class="string">"6000"</span> <span class="attr">readOnly</span>=<span class="string">""</span> <span class="attr">size</span>=<span class="string">"1024"</span>/></span></span><br><span class="line"><span class="tag"><<span class="name">cache</span>></span><span class="tag"></<span class="name">cache</span>></span> <span class="comment"><!--则表示所有属性使用默认值--></span></span><br><span class="line"><span class="tag"></<span class="name">mapper</span>></span></span><br></pre></td></tr></table></figure><p>eviction(清除策略):</p><ul><li><code>LRU</code> – 最近最少使用:移除最长时间不被使用的对象,默认</li><li><code>FIFO</code> – 先进先出:按对象进入缓存的顺序来移除它们</li><li><code>SOFT</code> – 软引用:基于垃圾回收器状态和软引用规则移除对象</li><li><code>WEAK</code> – 弱引用:更积极地基于垃圾收集器状态和弱引用规则移除对象</li></ul><p>flushInterval(刷新间隔):可以设置为任意的正整数, 默认情况是不设置,也就是没有刷新间隔,缓存仅仅会在调用语句时刷新</p><p>size(引用数目):缓存存放多少元素,默认值是 1024</p><p>readOnly(只读):可以被设置为 true 或 false</p><ul><li>只读的缓存会给所有调用者返回缓存对象的相同实例,因此这些对象不能被修改,促进了性能提升</li><li>可读写的缓存会(通过序列化)返回缓存对象的拷贝, 速度上会慢一些,但是更安全,因此默认值是 false</li></ul><p>type:指定自定义缓存的全类名,实现 Cache 接口即可</p></li><li><p>要进行二级缓存的类必须实现 java.io.Serializable 接口,可以使用序列化方式来保存对象。</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> <span class="keyword">implements</span> <span class="title class_">Serializable</span>{}</span><br></pre></td></tr></table></figure></li></ol><hr><h4 id="相关属性">相关属性</h4><ol><li><p>select 标签的 useCache 属性</p><p>映射文件中的 <code><select></code> 标签中设置 <code>useCache="true"</code> 代表当前 statement 要使用二级缓存(默认)</p><p>注意:如果每次查询都需要最新的数据 sql,要设置成 useCache=false,禁用二级缓存</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">select</span> <span class="attr">id</span>=<span class="string">"findAll"</span> <span class="attr">resultType</span>=<span class="string">"user"</span> <span class="attr">useCache</span>=<span class="string">"true"</span>></span></span><br><span class="line"> select * from user</span><br><span class="line"><span class="tag"></<span class="name">select</span>></span></span><br></pre></td></tr></table></figure></li><li><p>每个增删改标签都有 flushCache 属性,默认为 true,代表在<strong>执行增删改之后就会清除一、二级缓存</strong>,保证缓存的一致性;而查询标签默认值为 false,所以查询不会清空缓存</p></li><li><p>localCacheScope:本地缓存作用域,<settings> 中的配置项,默认值为 SESSION,当前会话的所有数据保存在会话缓存中,设置为 STATEMENT 禁用一级缓存</p></li></ol><hr><h4 id="源码解析">源码解析</h4><p>事务提交二级缓存才生效:DefaultSqlSession 调用 commit() 时会回调 <code>executor.commit()</code></p><ul><li><p>CachingExecutor#query():执行查询方法,查询出的数据会先放入 entriesToAddOnCommit 集合暂存</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// 从二缓存中获取数据,获取不到去一级缓存获取</span></span><br><span class="line">List<E> list = (List<E>) tcm.getObject(cache, key);</span><br><span class="line"><span class="keyword">if</span> (list == <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">// 回调 BaseExecutor#query</span></span><br><span class="line"> list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);</span><br><span class="line"> <span class="comment">// 将数据放入 entriesToAddOnCommit 集合暂存,此时还没放入二级缓存</span></span><br><span class="line"> tcm.putObject(cache, key, list);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>commit():事务提交,<strong>清空一级缓存,放入二级缓存</strong>,二级缓存使用 TransactionalCacheManager(tcm)管理</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">commit</span><span class="params">(<span class="type">boolean</span> required)</span> <span class="keyword">throws</span> SQLException {</span><br><span class="line"> <span class="comment">// 首先调用 BaseExecutor#commit 方法,【清空一级缓存】</span></span><br><span class="line"> delegate.commit(required);</span><br><span class="line"> tcm.commit();</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>TransactionalCacheManager#commit:查询出的数据放入二级缓存</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">commit</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 获取所有的缓存事务,挨着进行提交</span></span><br><span class="line"> <span class="keyword">for</span> (TransactionalCache txCache : transactionalCaches.values()) {</span><br><span class="line"> txCache.commit();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">commit</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (clearOnCommit) {</span><br><span class="line"> delegate.clear();</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 将 entriesToAddOnCommit 中的数据放入二级缓存</span></span><br><span class="line"> flushPendingEntries();</span><br><span class="line"> <span class="comment">// 清空相关集合</span></span><br><span class="line"> reset();</span><br><span class="line">}</span><br></pre></td></tr></table></figure> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">flushPendingEntries</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">for</span> (Map.Entry<Object, Object> entry : entriesToAddOnCommit.entrySet()) {</span><br><span class="line"> <span class="comment">// 将数据放入二级缓存</span></span><br><span class="line"> delegate.putObject(entry.getKey(), entry.getValue());</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><p>增删改操作会清空缓存:</p><ul><li><p>update():CachingExecutor 的更新操作</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="type">int</span> <span class="title function_">update</span><span class="params">(MappedStatement ms, Object parameterObject)</span> <span class="keyword">throws</span> SQLException {</span><br><span class="line"> flushCacheIfRequired(ms);</span><br><span class="line"> <span class="comment">// 回调 BaseExecutor#update 方法,也会清空一级缓存</span></span><br><span class="line"> <span class="keyword">return</span> delegate.update(ms, parameterObject);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>flushCacheIfRequired():判断是否需要清空二级缓存</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title function_">flushCacheIfRequired</span><span class="params">(MappedStatement ms)</span> {</span><br><span class="line"> <span class="type">Cache</span> <span class="variable">cache</span> <span class="operator">=</span> ms.getCache();</span><br><span class="line"> <span class="comment">// 判断二级缓存是否存在,然后判断标签的 flushCache 的值,增删改操作的 flushCache 属性默认为 true</span></span><br><span class="line"> <span class="keyword">if</span> (cache != <span class="literal">null</span> && ms.isFlushCacheRequired()) {</span><br><span class="line"> <span class="comment">// 清空二级缓存</span></span><br><span class="line"> tcm.clear(cache);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><hr><h3 id="自定义缓存">自定义缓存</h3><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">cache</span> <span class="attr">type</span>=<span class="string">"com.domain.something.MyCustomCache"</span>/></span></span><br></pre></td></tr></table></figure><p>type 属性指定的类必须实现 org.apache.ibatis.cache.Cache 接口,且提供一个接受 String 参数作为 id 的构造器</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">Cache</span> {</span><br><span class="line"> String <span class="title function_">getId</span><span class="params">()</span>;</span><br><span class="line"> <span class="type">int</span> <span class="title function_">getSize</span><span class="params">()</span>;</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">putObject</span><span class="params">(Object key, Object value)</span>;</span><br><span class="line"> Object <span class="title function_">getObject</span><span class="params">(Object key)</span>;</span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">hasKey</span><span class="params">(Object key)</span>;</span><br><span class="line"> Object <span class="title function_">removeObject</span><span class="params">(Object key)</span>;</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">clear</span><span class="params">()</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>缓存的配置,只需要在缓存实现中添加公有的 JavaBean 属性,然后通过 cache 元素传递属性值,例如在缓存实现上调用一个名为 <code>setCacheFile(String file)</code> 的方法:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">cache</span> <span class="attr">type</span>=<span class="string">"com.domain.something.MyCustomCache"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"cacheFile"</span> <span class="attr">value</span>=<span class="string">"/tmp/my-custom-cache.tmp"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">cache</span>></span></span><br></pre></td></tr></table></figure><ul><li>可以使用所有简单类型作为 JavaBean 属性的类型,MyBatis 会进行转换。</li><li>可以使用占位符(如 <code>${cache.file}</code>),以便替换成在配置文件属性中定义的值</li></ul><p>MyBatis 支持在所有属性设置完毕之后,调用一个初始化方法, 如果想要使用这个特性,可以在自定义缓存类里实现 <code>org.apache.ibatis.builder.InitializingObject</code> 接口</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">InitializingObject</span> {</span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">initialize</span><span class="params">()</span> <span class="keyword">throws</span> Exception;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>注意:对缓存的配置(如清除策略、可读或可读写等),不能应用于自定义缓存</p><p>对某一命名空间的语句,只会使用该命名空间的缓存进行缓存或刷新,在多个命名空间中共享相同的缓存配置和实例,可以使用 cache-ref 元素来引用另一个缓存</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">cache-ref</span> <span class="attr">namespace</span>=<span class="string">"com.someone.application.data.SomeMapper"</span>/></span></span><br></pre></td></tr></table></figure><h2 id="构造语句">构造语句</h2><h3 id="动态-SQL">动态 SQL</h3><h4 id="基本介绍-3">基本介绍</h4><p>动态 SQL 是 MyBatis 强大特性之一,逻辑复杂时,MyBatis 映射配置文件中,SQL 是动态变化的,所以引入动态 SQL 简化拼装 SQL 的操作</p><p>DynamicSQL 包含的标签:</p><ul><li>if</li><li>where</li><li>set</li><li>choose (when、otherwise)</li><li>trim</li><li>foreach</li></ul><p>各个标签都可以进行灵活嵌套和组合</p><p>OGNL:Object Graphic Navigation Language(对象图导航语言),用于对数据进行访问</p><p>参考文章:<a href="https://www.cnblogs.com/ysocean/p/7289529.html">https://www.cnblogs.com/ysocean/p/7289529.html</a></p><hr><h4 id="where">where</h4><p><where>:条件标签,有动态条件则使用该标签代替 WHERE 关键字,封装查询条件</p><p>作用:如果标签返回的内容是以 AND 或 OR 开头的,标签内会剔除掉</p><p>表结构:</p><p><img src="../image/post/MyBatis-%E5%8A%A8%E6%80%81sql%E7%94%A8%E6%88%B7%E8%A1%A8.png" alt=""></p><hr><h4 id="if">if</h4><p>基本格式:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">“条件判断”</span>></span></span><br><span class="line">查询条件拼接</span><br><span class="line"><span class="tag"></<span class="name">if</span>></span></span><br></pre></td></tr></table></figure><p>我们根据实体类的不同取值,使用不同的 SQL 语句来进行查询。比如在 id 如果不为空时可以根据 id 查询,如果username 不同空时还要加入用户名作为条件,这种情况在我们的多条件组合查询中经常会碰到。</p><ul><li><p>UserMapper.xml</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"UTF-8"</span> ?></span></span><br><span class="line"><span class="meta"><!DOCTYPE <span class="keyword">mapper</span></span></span><br><span class="line"><span class="meta"> <span class="keyword">PUBLIC</span> <span class="string">"-//mybatis.org//DTD Mapper 3.0//EN"</span></span></span><br><span class="line"><span class="meta"> <span class="string">"http://mybatis.org/dtd/mybatis-3-mapper.dtd"</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">mapper</span> <span class="attr">namespace</span>=<span class="string">"mapper.UserMapper"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectCondition"</span> <span class="attr">resultType</span>=<span class="string">"user"</span> <span class="attr">parameterType</span>=<span class="string">"user"</span>></span></span><br><span class="line"> SELECT * FROM user</span><br><span class="line"> <span class="tag"><<span class="name">where</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"id != null "</span>></span></span><br><span class="line"> id = #{id}</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"username != null "</span>></span></span><br><span class="line"> AND username = #{username}</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"sex != null "</span>></span></span><br><span class="line"> AND sex = #{sex}</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">where</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">select</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">mapper</span>></span></span><br></pre></td></tr></table></figure></li><li><p>MyBatisConfig.xml,引入映射配置文件</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">mappers</span>></span></span><br><span class="line"> <span class="comment"><!--mapper引入指定的映射配置 resource属性执行的映射配置文件的名称--></span></span><br><span class="line"> <span class="tag"><<span class="name">mapper</span> <span class="attr">resource</span>=<span class="string">"UserMapper.xml"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">mappers</span>></span></span><br></pre></td></tr></table></figure></li><li><p>DAO 层 Mapper 接口</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">UserMapper</span> {</span><br><span class="line"> <span class="comment">//多条件查询</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">abstract</span> List<User> <span class="title function_">selectCondition</span><span class="params">(Student stu)</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>实现类</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DynamicTest</span> {</span><br><span class="line"> <span class="meta">@Test</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">selectCondition</span><span class="params">()</span> <span class="keyword">throws</span> Exception{</span><br><span class="line"> <span class="comment">//1.加载核心配置文件</span></span><br><span class="line"> <span class="type">InputStream</span> <span class="variable">is</span> <span class="operator">=</span> Resources.getResourceAsStream(<span class="string">"MyBatisConfig.xml"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//2.获取SqlSession工厂对象</span></span><br><span class="line"> <span class="type">SqlSessionFactory</span> <span class="variable">ssf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SqlSessionFactoryBuilder</span>().build(is);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//3.通过工厂对象获取SqlSession对象</span></span><br><span class="line"> <span class="type">SqlSession</span> <span class="variable">sqlSession</span> <span class="operator">=</span> ssf.openSession(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//4.获取StudentMapper接口的实现类对象</span></span><br><span class="line"> <span class="type">UserMapper</span> <span class="variable">mapper</span> <span class="operator">=</span> sqlSession.getMapper(UserMapper.class);</span><br><span class="line"></span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">User</span>();</span><br><span class="line"> user.setId(<span class="number">2</span>);</span><br><span class="line"> user.setUsername(<span class="string">"李四"</span>);</span><br><span class="line"> <span class="comment">//user.setSex(男); AND 后会自动剔除</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">//5.调用实现类的方法,接收结果</span></span><br><span class="line"> List<Student> list = mapper.selectCondition(user);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//6.处理结果</span></span><br><span class="line"> <span class="keyword">for</span> (User user : list) {</span><br><span class="line"> System.out.println(user);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//7.释放资源</span></span><br><span class="line"> sqlSession.close();</span><br><span class="line"> is.close();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="set">set</h4><p><set>:进行更新操作的时候,含有 set 关键词,使用该标签</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- 根据 id 更新 user 表的数据 --></span></span><br><span class="line"><span class="tag"><<span class="name">update</span> <span class="attr">id</span>=<span class="string">"updateUserById"</span> <span class="attr">parameterType</span>=<span class="string">"com.ys.po.User"</span>></span></span><br><span class="line"> UPDATE user u</span><br><span class="line"> <span class="tag"><<span class="name">set</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"username != null and username != ''"</span>></span></span><br><span class="line"> u.username = #{username},</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"sex != null and sex != ''"</span>></span></span><br><span class="line"> u.sex = #{sex}</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">set</span>></span></span><br><span class="line"> WHERE id=#{id}</span><br><span class="line"><span class="tag"></<span class="name">update</span>></span></span><br></pre></td></tr></table></figure><ul><li>如果第一个条件 username 为空,那么 sql 语句为:update user u set u.sex=? where id=?</li><li>如果第一个条件不为空,那么 sql 语句为:update user u set u.username = ? ,u.sex = ? where id=?</li></ul><hr><h4 id="choose">choose</h4><p>假如不想用到所有的查询条件,只要查询条件有一个满足即可,使用 choose 标签可以解决此类问题,类似于 Java 的 switch 语句</p><p>标签:<when>,<otherwise></p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectUserByChoose"</span> <span class="attr">resultType</span>=<span class="string">"user"</span> <span class="attr">parameterType</span>=<span class="string">"user"</span>></span></span><br><span class="line"> SELECT * FROM user</span><br><span class="line"> <span class="tag"><<span class="name">where</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">choose</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">when</span> <span class="attr">test</span>=<span class="string">"id !='' and id != null"</span>></span></span><br><span class="line"> id=#{id}</span><br><span class="line"> <span class="tag"></<span class="name">when</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">when</span> <span class="attr">test</span>=<span class="string">"username !='' and username != null"</span>></span></span><br><span class="line"> AND username=#{username}</span><br><span class="line"> <span class="tag"></<span class="name">when</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">otherwise</span>></span></span><br><span class="line"> AND sex=#{sex}</span><br><span class="line"> <span class="tag"></<span class="name">otherwise</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">choose</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">where</span>></span></span><br><span class="line"><span class="tag"></<span class="name">select</span>></span></span><br></pre></td></tr></table></figure><p>有三个条件,id、username、sex,只能选择一个作为查询条件</p><ul><li><p>如果 id 不为空,那么查询语句为:select * from user where id=?</p></li><li><p>如果 id 为空,那么看 username 是否为空</p><ul><li>如果不为空,那么语句为:select * from user where username=?</li><li>如果 username 为空,那么查询语句为 select * from user where sex=?</li></ul></li></ul><hr><h4 id="trim">trim</h4><p>trim 标记是一个格式化的标记,可以完成 set 或者是 where 标记的功能,自定义字符串截取</p><ul><li>prefix:给拼串后的整个字符串加一个前缀,trim 标签体中是整个字符串拼串后的结果</li><li>prefixOverrides:去掉整个字符串前面多余的字符</li><li>suffix:给拼串后的整个字符串加一个后缀</li><li>suffixOverrides:去掉整个字符串后面多余的字符</li></ul><p>改写 if + where 语句:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectUserByUsernameAndSex"</span> <span class="attr">resultType</span>=<span class="string">"user"</span> <span class="attr">parameterType</span>=<span class="string">"com.ys.po.User"</span>></span></span><br><span class="line"> SELECT * FROM user</span><br><span class="line"> <span class="tag"><<span class="name">trim</span> <span class="attr">prefix</span>=<span class="string">"where"</span> <span class="attr">prefixOverrides</span>=<span class="string">"and | or"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"username != null"</span>></span></span><br><span class="line"> AND username=#{username}</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"sex != null"</span>></span></span><br><span class="line"> AND sex=#{sex}</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">trim</span>></span></span><br><span class="line"><span class="tag"></<span class="name">select</span>></span></span><br></pre></td></tr></table></figure><p>改写 if + set 语句:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!-- 根据 id 更新 user 表的数据 --></span></span><br><span class="line"><span class="tag"><<span class="name">update</span> <span class="attr">id</span>=<span class="string">"updateUserById"</span> <span class="attr">parameterType</span>=<span class="string">"com.ys.po.User"</span>></span></span><br><span class="line"> UPDATE user u</span><br><span class="line"> <span class="tag"><<span class="name">trim</span> <span class="attr">prefix</span>=<span class="string">"set"</span> <span class="attr">suffixOverrides</span>=<span class="string">","</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"username != null and username != ''"</span>></span></span><br><span class="line"> u.username = #{username},</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">if</span> <span class="attr">test</span>=<span class="string">"sex != null and sex != ''"</span>></span></span><br><span class="line"> u.sex = #{sex},</span><br><span class="line"> <span class="tag"></<span class="name">if</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">trim</span>></span></span><br><span class="line"> WHERE id=#{id}</span><br><span class="line"><span class="tag"></<span class="name">update</span>></span></span><br></pre></td></tr></table></figure><hr><h4 id="foreach">foreach</h4><p>基本格式:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">foreach</span>></span>:循环遍历标签。适用于多个参数或者的关系。</span><br><span class="line"> <span class="tag"><<span class="name">foreach</span> <span class="attr">collection</span>=<span class="string">“”open</span>=<span class="string">“”close</span>=<span class="string">“”item</span>=<span class="string">“”separator</span>=<span class="string">“”</span>></span></span><br><span class="line">获取参数</span><br><span class="line"><span class="tag"></<span class="name">foreach</span>></span></span><br></pre></td></tr></table></figure><p>属性:</p><ul><li>collection:参数容器类型, (list-集合, array-数组)</li><li>open:开始的 SQL 语句</li><li>close:结束的 SQL 语句</li><li>item:参数变量名</li><li>separator:分隔符</li></ul><p>需求:循环执行 sql 的拼接操作,<code>SELECT * FROM user WHERE id IN (1,2,5)</code></p><ul><li><p>UserMapper.xml片段</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectByIds"</span> <span class="attr">resultType</span>=<span class="string">"user"</span> <span class="attr">parameterType</span>=<span class="string">"list"</span>></span></span><br><span class="line"> SELECT * FROM student</span><br><span class="line"> <span class="tag"><<span class="name">where</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">foreach</span> <span class="attr">collection</span>=<span class="string">"list"</span> <span class="attr">open</span>=<span class="string">"id IN("</span> <span class="attr">close</span>=<span class="string">")"</span> <span class="attr">item</span>=<span class="string">"id"</span> <span class="attr">separator</span>=<span class="string">","</span>></span></span><br><span class="line"> #{id}</span><br><span class="line"> <span class="tag"></<span class="name">foreach</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">where</span>></span></span><br><span class="line"><span class="tag"></<span class="name">select</span>></span></span><br></pre></td></tr></table></figure></li><li><p>测试代码片段</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//4.获取StudentMapper接口的实现类对象</span></span><br><span class="line"><span class="type">UserMapper</span> <span class="variable">mapper</span> <span class="operator">=</span> sqlSession.getMapper(UserMapper.class);</span><br><span class="line"></span><br><span class="line">List<Integer> ids = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line">Collections.addAll(list, <span class="number">1</span>, <span class="number">2</span>);</span><br><span class="line"><span class="comment">//5.调用实现类的方法,接收结果</span></span><br><span class="line">List<User> list = mapper.selectByIds(ids);</span><br><span class="line"></span><br><span class="line"><span class="keyword">for</span> (User user : list) {</span><br><span class="line"> System.out.println(user);</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="SQL片段">SQL片段</h4><p>将一些重复性的 SQL 语句进行抽取,以达到复用的效果</p><p>格式:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">sql</span> <span class="attr">id</span>=<span class="string">“片段唯一标识”</span>></span>抽取的SQL语句<span class="tag"></<span class="name">sql</span>></span><span class="comment"><!--抽取标签--></span></span><br><span class="line"><span class="tag"><<span class="name">include</span> <span class="attr">refid</span>=<span class="string">“片段唯一标识”/</span>></span><span class="comment"><!--引入标签--></span></span><br></pre></td></tr></table></figure><p>使用:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">sql</span> <span class="attr">id</span>=<span class="string">"select"</span>></span>SELECT * FROM user<span class="tag"></<span class="name">sql</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">select</span> <span class="attr">id</span>=<span class="string">"selectByIds"</span> <span class="attr">resultType</span>=<span class="string">"user"</span> <span class="attr">parameterType</span>=<span class="string">"list"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">include</span> <span class="attr">refid</span>=<span class="string">"select"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">where</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">foreach</span> <span class="attr">collection</span>=<span class="string">"list"</span> <span class="attr">open</span>=<span class="string">"id IN("</span> <span class="attr">close</span>=<span class="string">")"</span> <span class="attr">item</span>=<span class="string">"id"</span> <span class="attr">separator</span>=<span class="string">","</span>></span></span><br><span class="line"> #{id}</span><br><span class="line"> <span class="tag"></<span class="name">foreach</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">where</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">select</span>></span></span><br></pre></td></tr></table></figure><hr><h3 id="逆向工程">逆向工程</h3><p>MyBatis 逆向工程,可以针对<strong>单表</strong>自动生成 MyBatis 执行所需要的代码(mapper.java、mapper.xml、pojo…)</p><p>generatorConfig.xml</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta"><?xml version=<span class="string">"1.0"</span> encoding=<span class="string">"UTF-8"</span>?></span></span><br><span class="line"><span class="meta"><!DOCTYPE <span class="keyword">generatorConfiguration</span></span></span><br><span class="line"><span class="meta"> <span class="keyword">PUBLIC</span> <span class="string">"-//mybatis.org//DTD MyBatis Generator Configuration 1.0//EN"</span></span></span><br><span class="line"><span class="meta"> <span class="string">"http://mybatis.org/dtd/mybatis-generator-config_1_0.dtd"</span>></span></span><br><span class="line"> </span><br><span class="line"><span class="tag"><<span class="name">generatorConfiguration</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">context</span> <span class="attr">id</span>=<span class="string">"testTables"</span> <span class="attr">targetRuntime</span>=<span class="string">"MyBatis3"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">commentGenerator</span>></span></span><br><span class="line"> <span class="comment"><!-- 是否去除自动生成的注释 true:是 : false:否 --></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"suppressAllComments"</span> <span class="attr">value</span>=<span class="string">"true"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">commentGenerator</span>></span></span><br><span class="line"> <span class="comment"><!--数据库连接的信息:驱动类、连接地址、用户名、密码 --></span></span><br><span class="line"> <span class="tag"><<span class="name">jdbcConnection</span> <span class="attr">driverClass</span>=<span class="string">"com.mysql.jdbc.Driver"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">connectionURL</span>=<span class="string">"jdbc:mysql://localhost:3306/mybatisrelation"</span> <span class="attr">userId</span>=<span class="string">"root"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">password</span>=<span class="string">"root"</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">jdbcConnection</span>></span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"><!-- 默认false,把JDBC DECIMAL 和 NUMERIC 类型解析为 Integer,为 true时把JDBC DECIMAL和NUMERIC类型解析为java.math.BigDecimal --></span></span><br><span class="line"> <span class="tag"><<span class="name">javaTypeResolver</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"forceBigDecimals"</span> <span class="attr">value</span>=<span class="string">"false"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">javaTypeResolver</span>></span></span><br><span class="line"> </span><br><span class="line"> <span class="comment"><!-- targetProject:生成PO类的位置!! --></span></span><br><span class="line"> <span class="tag"><<span class="name">javaModelGenerator</span> <span class="attr">targetPackage</span>=<span class="string">"com.ys.po"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">targetProject</span>=<span class="string">".\src"</span>></span></span><br><span class="line"> <span class="comment"><!-- enableSubPackages:是否让schema作为包的后缀 --></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"enableSubPackages"</span> <span class="attr">value</span>=<span class="string">"false"</span> /></span></span><br><span class="line"> <span class="comment"><!-- 从数据库返回的值被清理前后的空格 --></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"trimStrings"</span> <span class="attr">value</span>=<span class="string">"true"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">javaModelGenerator</span>></span></span><br><span class="line"> <span class="comment"><!-- targetProject:mapper映射文件生成的位置!! --></span></span><br><span class="line"> <span class="tag"><<span class="name">sqlMapGenerator</span> <span class="attr">targetPackage</span>=<span class="string">"com.ys.mapper"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">targetProject</span>=<span class="string">".\src"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"enableSubPackages"</span> <span class="attr">value</span>=<span class="string">"false"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">sqlMapGenerator</span>></span></span><br><span class="line"> <span class="comment"><!-- targetPackage:mapper接口生成的位置,重要!! --></span></span><br><span class="line"> <span class="tag"><<span class="name">javaClientGenerator</span> <span class="attr">type</span>=<span class="string">"XMLMAPPER"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">targetPackage</span>=<span class="string">"com.ys.mapper"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">targetProject</span>=<span class="string">".\src"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"enableSubPackages"</span> <span class="attr">value</span>=<span class="string">"false"</span> /></span></span><br><span class="line"> <span class="tag"></<span class="name">javaClientGenerator</span>></span></span><br><span class="line"> <span class="comment"><!-- 指定数据库表,要生成哪些表,就写哪些表,要和数据库中对应,不能写错! --></span></span><br><span class="line"> <span class="tag"><<span class="name">table</span> <span class="attr">tableName</span>=<span class="string">"items"</span>></span><span class="tag"></<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">table</span> <span class="attr">tableName</span>=<span class="string">"orders"</span>></span><span class="tag"></<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">table</span> <span class="attr">tableName</span>=<span class="string">"orderdetail"</span>></span><span class="tag"></<span class="name">table</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">table</span> <span class="attr">tableName</span>=<span class="string">"user"</span>></span><span class="tag"></<span class="name">table</span>></span> </span><br><span class="line"> <span class="tag"></<span class="name">context</span>></span></span><br><span class="line"><span class="tag"></<span class="name">generatorConfiguration</span>></span></span><br></pre></td></tr></table></figure><p>生成代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testGenerator</span><span class="params">()</span> <span class="keyword">throws</span> Exception{</span><br><span class="line"> List<String> warnings = <span class="keyword">new</span> <span class="title class_">ArrayList</span><String>();</span><br><span class="line"> <span class="type">boolean</span> <span class="variable">overwrite</span> <span class="operator">=</span> <span class="literal">true</span>;</span><br><span class="line"> <span class="comment">//指向逆向工程配置文件</span></span><br><span class="line"> <span class="type">File</span> <span class="variable">configFile</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">File</span>(GeneratorTest.class.</span><br><span class="line"> getResource(<span class="string">"/generatorConfig.xml"</span>).getFile());</span><br><span class="line"> <span class="type">ConfigurationParser</span> <span class="variable">cp</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ConfigurationParser</span>(warnings);</span><br><span class="line"> <span class="type">Configuration</span> <span class="variable">config</span> <span class="operator">=</span> cp.parseConfiguration(configFile);</span><br><span class="line"> <span class="type">DefaultShellCallback</span> <span class="variable">callback</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">DefaultShellCallback</span>(overwrite);</span><br><span class="line"> <span class="type">MyBatisGenerator</span> <span class="variable">myBatisGenerator</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">MyBatisGenerator</span>(config,</span><br><span class="line"> callback, warnings);</span><br><span class="line"> myBatisGenerator.generate(<span class="literal">null</span>);</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>参考文章:<a href="https://www.cnblogs.com/ysocean/p/7360409.html">https://www.cnblogs.com/ysocean/p/7360409.html</a></p><hr><h3 id="构建-SQL">构建 SQL</h3><h4 id="基础语法">基础语法</h4><p>MyBatis 提供了 org.apache.ibatis.jdbc.SQL 功能类,专门用于构建 SQL 语句</p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td>SELECT(String… columns)</td><td>根据字段拼接查询语句</td></tr><tr><td>FROM(String… tables)</td><td>根据表名拼接语句</td></tr><tr><td>WHERE(String… conditions)</td><td>根据条件拼接语句</td></tr><tr><td>INSERT_INTO(String tableName)</td><td>根据表名拼接新增语句</td></tr><tr><td>INTO_VALUES(String… values)</td><td>根据值拼接新增语句</td></tr><tr><td>UPDATE(String table)</td><td>根据表名拼接修改语句</td></tr><tr><td>DELETE_FROM(String table)</td><td>根据表名拼接删除语句</td></tr></tbody></table><p>增删改查注解:</p><ul><li>@SelectProvider:生成查询用的 SQL 语句</li><li>@InsertProvider:生成新增用的 SQL 语句</li><li>@UpdateProvider:生成修改用的 SQL 语句注解</li><li>@DeleteProvider:生成删除用的 SQL 语句注解。<ul><li>type 属性:生成 SQL 语句功能类对象</li><li>method 属性:指定调用方法</li></ul></li></ul><hr><h4 id="基本操作">基本操作</h4><ul><li><p>MyBatisConfig.xml 配置</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"> <span class="comment"><!-- mappers引入映射配置文件 --></span></span><br><span class="line"><span class="tag"><<span class="name">mappers</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">package</span> <span class="attr">name</span>=<span class="string">"mapper"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">mappers</span>></span></span><br></pre></td></tr></table></figure></li><li><p>Mapper 类</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">StudentMapper</span> {</span><br><span class="line"> <span class="comment">//查询全部</span></span><br><span class="line"> <span class="meta">@SelectProvider(type = ReturnSql.class, method = "getSelectAll")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">abstract</span> List<Student> <span class="title function_">selectAll</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//新增数据</span></span><br><span class="line"> <span class="meta">@InsertProvider(type = ReturnSql.class, method = "getInsert")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">abstract</span> Integer <span class="title function_">insert</span><span class="params">(Student student)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//修改操作</span></span><br><span class="line"> <span class="meta">@UpdateProvider(type = ReturnSql.class, method = "getUpdate")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">abstract</span> Integer <span class="title function_">update</span><span class="params">(Student student)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">//删除操作</span></span><br><span class="line"> <span class="meta">@DeleteProvider(type = ReturnSql.class, method = "getDelete")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">abstract</span> Integer <span class="title function_">delete</span><span class="params">(Integer id)</span>;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>ReturnSQL 类</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ReturnSql</span> {</span><br><span class="line"> <span class="comment">//定义方法,返回查询的sql语句</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getSelectAll</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SQL</span>() {</span><br><span class="line"> {</span><br><span class="line"> SELECT(<span class="string">"*"</span>);</span><br><span class="line"> FROM(<span class="string">"student"</span>);</span><br><span class="line"> }</span><br><span class="line"> }.toString();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//定义方法,返回新增的sql语句</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getInsert</span><span class="params">(Student stu)</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SQL</span>() {</span><br><span class="line"> {</span><br><span class="line"> INSERT_INTO(<span class="string">"student"</span>);</span><br><span class="line"> INTO_VALUES(<span class="string">"#{id},#{name},#{age}"</span>);</span><br><span class="line"> }</span><br><span class="line"> }.toString();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//定义方法,返回修改的sql语句</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getUpdate</span><span class="params">(Student stu)</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SQL</span>() {</span><br><span class="line"> {</span><br><span class="line"> UPDATE(<span class="string">"student"</span>);</span><br><span class="line"> SET(<span class="string">"name=#{name}"</span>,<span class="string">"age=#{age}"</span>);</span><br><span class="line"> WHERE(<span class="string">"id=#{id}"</span>);</span><br><span class="line"> }</span><br><span class="line"> }.toString();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//定义方法,返回删除的sql语句</span></span><br><span class="line"> <span class="keyword">public</span> String <span class="title function_">getDelete</span><span class="params">(Integer id)</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">SQL</span>() {</span><br><span class="line"> {</span><br><span class="line"> DELETE_FROM(<span class="string">"student"</span>);</span><br><span class="line"> WHERE(<span class="string">"id=#{id}"</span>);</span><br><span class="line"> }</span><br><span class="line"> }.toString();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>功能实现类</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SqlTest</span> {</span><br><span class="line"><span class="meta">@Test</span> <span class="comment">//查询全部</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">selectAll</span><span class="params">()</span> <span class="keyword">throws</span> Exception{</span><br><span class="line"> <span class="comment">//1.加载核心配置文件</span></span><br><span class="line"> <span class="type">InputStream</span> <span class="variable">is</span> <span class="operator">=</span> Resources.getResourceAsStream(<span class="string">"MyBatisConfig.xml"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//2.获取SqlSession工厂对象</span></span><br><span class="line"> <span class="type">SqlSessionFactory</span> <span class="variable">sqlSessionFactory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SqlSessionFactoryBuilder</span>().build(is);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//3.通过工厂对象获取SqlSession对象</span></span><br><span class="line"> <span class="type">SqlSession</span> <span class="variable">sqlSession</span> <span class="operator">=</span> sqlSessionFactory.openSession(<span class="literal">true</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//4.获取StudentMapper接口的实现类对象</span></span><br><span class="line"> <span class="type">StudentMapper</span> <span class="variable">mapper</span> <span class="operator">=</span> sqlSession.getMapper(StudentMapper.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//5.调用实现类对象中的方法,接收结果</span></span><br><span class="line"> List<Student> list = mapper.selectAll();</span><br><span class="line"></span><br><span class="line"> <span class="comment">//6.处理结果</span></span><br><span class="line"> <span class="keyword">for</span> (Student student : list) {</span><br><span class="line"> System.out.println(student);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//7.释放资源</span></span><br><span class="line"> sqlSession.close();</span><br><span class="line"> is.close();</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Test</span> <span class="comment">//新增</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">insert</span><span class="params">()</span> <span class="keyword">throws</span> Exception{</span><br><span class="line"> <span class="comment">//1 2 3 4获取StudentMapper接口的实现类对象</span></span><br><span class="line"> <span class="type">StudentMapper</span> <span class="variable">mapper</span> <span class="operator">=</span> sqlSession.getMapper(StudentMapper.class);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//5.调用实现类对象中的方法,接收结果 ->6 7</span></span><br><span class="line"> <span class="type">Student</span> <span class="variable">stu</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Student</span>(<span class="number">4</span>,<span class="string">"赵六"</span>,<span class="number">26</span>);</span><br><span class="line"> <span class="type">Integer</span> <span class="variable">result</span> <span class="operator">=</span> mapper.insert(stu);</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> <span class="meta">@Test</span> <span class="comment">//修改</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">update</span><span class="params">()</span> <span class="keyword">throws</span> Exception{</span><br><span class="line"> <span class="comment">//1 2 3 4 5调用实现类对象中的方法,接收结果 ->6 7 </span></span><br><span class="line"><span class="type">Student</span> <span class="variable">stu</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Student</span>(<span class="number">4</span>,<span class="string">"赵六wq"</span>,<span class="number">36</span>);</span><br><span class="line"> <span class="type">Integer</span> <span class="variable">result</span> <span class="operator">=</span> mapper.update(stu);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Test</span> <span class="comment">//删除</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">delete</span><span class="params">()</span> <span class="keyword">throws</span> Exception{</span><br><span class="line"> <span class="comment">//1 2 3 4 5 6 7</span></span><br><span class="line"> <span class="type">Integer</span> <span class="variable">result</span> <span class="operator">=</span> mapper.delete(<span class="number">4</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><hr><h2 id="插件使用">插件使用</h2><h3 id="插件原理">插件原理</h3><p>实现原理:插件是按照插件配置顺序创建层层包装对象,执行目标方法的之后,按照逆向顺序执行(栈)</p><img src="../image/post/MyBatis-%E6%8F%92%E4%BB%B6%E5%8E%9F%E7%90%86.png" style="zoom:50%;" /><p>在四大对象创建时:</p><ul><li>每个创建出来的对象不是直接返回的,而是 <code>interceptorChain.pluginAll(parameterHandler)</code></li><li>获取到所有 Interceptor(插件需要实现的接口),调用 <code>interceptor.plugin(target)</code>返回 target 包装后的对象</li><li>插件机制可以使用插件为目标对象创建一个代理对象,代理对象可以<strong>拦截到四大对象的每一个执行</strong></li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Intercepts(</span></span><br><span class="line"><span class="meta">{</span></span><br><span class="line"><span class="meta">@Signature(type=StatementHandler.class,method="parameterize",args=java.sql.Statement.class)</span></span><br><span class="line"><span class="meta">})</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MyFirstPlugin</span> <span class="keyword">implements</span> <span class="title class_">Interceptor</span>{</span><br><span class="line"></span><br><span class="line"><span class="comment">//intercept:拦截目标对象的目标方法的执行</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Object <span class="title function_">intercept</span><span class="params">(Invocation invocation)</span> <span class="keyword">throws</span> Throwable {</span><br><span class="line">System.out.println(<span class="string">"MyFirstPlugin...intercept:"</span> + invocation.getMethod());</span><br><span class="line"><span class="comment">//动态的改变一下sql运行的参数:以前1号员工,实际从数据库查询11号员工</span></span><br><span class="line"><span class="type">Object</span> <span class="variable">target</span> <span class="operator">=</span> invocation.getTarget();</span><br><span class="line">System.out.println(<span class="string">"当前拦截到的对象:"</span> + target);</span><br><span class="line"><span class="comment">//拿到:StatementHandler==>ParameterHandler===>parameterObject</span></span><br><span class="line"><span class="comment">//拿到target的元数据</span></span><br><span class="line"><span class="type">MetaObject</span> <span class="variable">metaObject</span> <span class="operator">=</span> SystemMetaObject.forObject(target);</span><br><span class="line"><span class="type">Object</span> <span class="variable">value</span> <span class="operator">=</span> metaObject.getValue(<span class="string">"parameterHandler.parameterObject"</span>);</span><br><span class="line">System.out.println(<span class="string">"sql语句用的参数是:"</span> + value);</span><br><span class="line"><span class="comment">//修改完sql语句要用的参数</span></span><br><span class="line">metaObject.setValue(<span class="string">"parameterHandler.parameterObject"</span>, <span class="number">11</span>);</span><br><span class="line"><span class="comment">//执行目标方法</span></span><br><span class="line"><span class="type">Object</span> <span class="variable">proceed</span> <span class="operator">=</span> invocation.proceed();</span><br><span class="line"><span class="comment">//返回执行后的返回值</span></span><br><span class="line"><span class="keyword">return</span> proceed;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// plugin:包装目标对象的,为目标对象创建一个代理对象</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> Object <span class="title function_">plugin</span><span class="params">(Object target)</span> {</span><br><span class="line"><span class="comment">//可以借助 Plugin 的 wrap 方法来使用当前 Interceptor 包装我们目标对象</span></span><br><span class="line">System.out.println(<span class="string">"MyFirstPlugin...plugin:mybatis将要包装的对象"</span> + target);</span><br><span class="line"><span class="type">Object</span> <span class="variable">wrap</span> <span class="operator">=</span> Plugin.wrap(target, <span class="built_in">this</span>);</span><br><span class="line"><span class="comment">//返回为当前target创建的动态代理</span></span><br><span class="line"><span class="keyword">return</span> wrap;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">// setProperties:将插件注册时的property属性设置进来</span></span><br><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">setProperties</span><span class="params">(Properties properties)</span> {</span><br><span class="line">System.out.println(<span class="string">"插件配置的信息:"</span> + properties);</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>核心配置文件:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!--plugins:注册插件 --></span></span><br><span class="line"><span class="tag"><<span class="name">plugins</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">plugin</span> <span class="attr">interceptor</span>=<span class="string">"mybatis.dao.MyFirstPlugin"</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"username"</span> <span class="attr">value</span>=<span class="string">"root"</span>/></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"password"</span> <span class="attr">value</span>=<span class="string">"123456"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">plugin</span>></span></span><br><span class="line"><span class="tag"></<span class="name">plugins</span>></span></span><br></pre></td></tr></table></figure><hr><h3 id="分页插件">分页插件</h3><p><img src="../image/post/%E5%88%86%E9%A1%B5%E4%BB%8B%E7%BB%8D.png" alt=""></p><ul><li>分页可以将很多条结果进行分页显示。如果当前在第一页,则没有上一页。如果当前在最后一页,则没有下一页,需要明确当前是第几页,这一页中显示多少条结果。</li><li>MyBatis 是不带分页功能的,如果想实现分页功能,需要手动编写 LIMIT 语句,不同的数据库实现分页的 SQL 语句也是不同,手写分页 成本较高。</li><li>PageHelper:第三方分页助手,将复杂的分页操作进行封装,从而让分页功能变得非常简单</li></ul><hr><h3 id="分页操作">分页操作</h3><p>开发步骤:</p><ol><li><p>导入 PageHelper 的 Maven 坐标</p></li><li><p>在 MyBatis 核心配置文件中配置 PageHelper 插件</p><p>注意:分页助手的插件配置在通用 Mapper 之前</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">plugins</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">plugin</span> <span class="attr">interceptor</span>=<span class="string">"com.github.pagehelper.PageInterceptor"</span>></span></span><br><span class="line"> <span class="comment"><!-- 指定方言 --></span></span><br><span class="line"> <span class="tag"><<span class="name">property</span> <span class="attr">name</span>=<span class="string">"dialect"</span> <span class="attr">value</span>=<span class="string">"mysql"</span>/></span></span><br><span class="line"> <span class="tag"></<span class="name">plugin</span>></span> </span><br><span class="line"><span class="tag"></<span class="name">plugins</span>></span></span><br><span class="line"><span class="tag"><<span class="name">mappers</span>></span>.........<span class="tag"></<span class="name">mappers</span>></span></span><br></pre></td></tr></table></figure></li><li><p>与 MySQL 分页查询页数计算公式不同</p><p><code>static <E> Page<E> startPage(int pageNum, int pageSize)</code>:pageNum第几页,pageSize页面大小</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Test</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">selectAll</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">//第一页:显示2条数据</span></span><br><span class="line"> PageHelper.startPage(<span class="number">1</span>,<span class="number">2</span>);</span><br><span class="line"> List<Student> students = sqlSession.selectList(<span class="string">"StudentMapper.selectAll"</span>);</span><br><span class="line"> <span class="keyword">for</span> (Student student : students) {</span><br><span class="line"> System.out.println(student);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ol><hr><h3 id="参数获取">参数获取</h3><p>PageInfo构造方法:</p><ul><li><code>PageInfo<Student> info = new PageInfo<>(list)</code> : list 是 SQL 执行返回的结果集合,参考上一节</li></ul><p>PageInfo相关API:</p><ol><li>startPage():设置分页参数</li><li>PageInfo:分页相关参数功能类。</li><li>getTotal():获取总条数</li><li>getPages():获取总页数</li><li>getPageNum():获取当前页</li><li>getPageSize():获取每页显示条数</li><li>getPrePage():获取上一页</li><li>getNextPage():获取下一页</li><li>isIsFirstPage():获取是否是第一页</li><li>isIsLastPage():获取是否是最后一页</li></ol>]]></content>
<summary type="html">DataBase-MyBatis初级相关知识学习,以Hillos为纲,JavaNote为主体整理的相关笔记。</summary>
<category term="DataBaseing" scheme="https://jovehawking.fun/categories/DataBaseing/"/>
<category term="DataBase" scheme="https://jovehawking.fun/tags/DataBase/"/>
<category term="数据库" scheme="https://jovehawking.fun/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="MyBatis初级" scheme="https://jovehawking.fun/tags/MyBatis%E5%88%9D%E7%BA%A7/"/>
</entry>
<entry>
<title>DataBase-MySQL初级学习</title>
<link href="https://jovehawking.fun/posts/2866a1c7.html"/>
<id>https://jovehawking.fun/posts/2866a1c7.html</id>
<published>2024-06-23T01:43:11.000Z</published>
<updated>2024-07-06T09:05:27.844Z</updated>
<content type="html"><![CDATA[<h1>MySQL初级</h1><h2 id="目标">目标</h2><ol><li>Inonodb</li><li>索引原理</li><li>锁原理</li><li>事务&隔离级别</li><li>日志</li><li>回表</li><li>索引失效&错选索引</li><li>orderby</li><li>bufferPool</li><li>死锁</li><li>慢SQL排查</li><li>join</li></ol><h2 id="体系架构">体系架构</h2><h3 id="整体架构">整体架构</h3><p>体系结构详解:</p><ul><li>第一层:网络连接层<ul><li>一些客户端和链接服务,包含本地 Socket 通信和大多数基于客户端/服务端工具实现的 TCP/IP 通信,主要完成一些类似于连接处理、授权认证、及相关的安全方案</li><li>在该层上引入了<strong>连接池</strong> Connection Pool 的概念,管理缓冲用户连接,线程处理等需要缓存的需求</li><li>在该层上实现基于 SSL 的安全链接,服务器也会为安全接入的每个客户端验证它所具有的操作权限</li></ul></li></ul><ul><li>第二层:核心服务层<ul><li>查询缓存、分析器、优化器、执行器等,涵盖 MySQL 的大多数核心服务功能,所有的内置函数(日期、数学、加密函数等)<ul><li>Management Serveices & Utilities:系统管理和控制工具,备份、安全、复制、集群等</li><li>SQL Interface:接受用户的 SQL 命令,并且返回用户需要查询的结果</li><li>Parser:SQL 语句分析器</li><li>Optimizer:查询优化器</li><li>Caches & Buffers:查询缓存,服务器会查询内部的缓存,如果缓存空间足够大,可以在大量读操作的环境中提升系统性能</li></ul></li><li>所有<strong>跨存储引擎的功能</strong>在这一层实现,如存储过程、触发器、视图等</li><li>在该层服务器会解析查询并创建相应的内部解析树,并对其完成相应的优化如确定表的查询顺序,是否利用索引等, 最后生成相应的执行操作</li><li>MySQL 中服务器层不管理事务,<strong>事务是由存储引擎实现的</strong></li></ul></li><li>第三层:存储引擎层<ul><li>Pluggable Storage Engines:存储引擎接口,MySQL 区别于其他数据库的重要特点就是其存储引擎的架构模式是插件式的(存储引擎是基于表的,而不是数据库)</li><li>存储引擎<strong>真正的负责了 MySQL 中数据的存储和提取</strong>,服务器通过 API 和存储引擎进行通信</li><li>不同的存储引擎具有不同的功能,共用一个 Server 层,可以根据开发的需要,来选取合适的存储引擎</li></ul></li><li>第四层:系统文件层<ul><li>数据存储层,主要是将数据存储在文件系统之上,并完成与存储引擎的交互</li><li>File System:文件系统,保存配置文件、数据文件、日志文件、错误文件、二进制文件等</li></ul></li></ul><p><img src="../image/post/MySQL-%E4%BD%93%E7%B3%BB%E7%BB%93%E6%9E%84.png" alt=""></p><hr><h3 id="工作流程">工作流程</h3><p>当执行完全相同的 SQL 语句的时候,服务器就会直接从缓存中读取结果,当数据被修改,之前的缓存会失效,修改比较频繁的表不适合做查询缓存</p><p>查询过程:</p><ol><li>客户端发送一条查询给服务器</li><li>服务器先会检查查询缓存,如果命中了缓存,则立即返回存储在缓存中的结果(一般是 K-V 键值对),否则进入下一阶段</li><li>分析器进行 SQL 分析,再由优化器生成对应的执行计划</li><li>MySQL 根据优化器生成的执行计划,调用存储引擎的 API 来执行查询</li><li>将结果返回给客户端</li></ol><p>大多数情况下不建议使用查询缓存,因为查询缓存往往弊大于利</p><ul><li>查询缓存的<strong>失效非常频繁</strong>,只要有对一个表的更新,这个表上所有的查询缓存都会被清空。因此很可能费力地把结果存起来,还没使用就被一个更新全清空了,对于更新压力大的数据库来说,查询缓存的命中率会非常低</li><li>除非业务就是有一张静态表,很长时间才会更新一次,比如一个系统配置表,那这张表上的查询才适合使用查询缓存</li></ul><hr><h3 id="优化器">优化器</h3><h4 id="成本分析">成本分析</h4><p>优化器是在表里面有多个索引的时候,决定使用哪个索引;或者在一个语句有多表关联(join)的时候,决定各个表的连接顺序</p><ul><li>根据搜索条件找出所有可能的使用的索引</li><li>成本分析,执行成本由 I/O 成本和 CPU 成本组成,计算全表扫描和使用不同索引执行 SQL 的代价</li><li>找到一个最优的执行方案,用最小的代价去执行语句</li></ul><p>在数据库里面,扫描行数是影响执行代价的因素之一,扫描的行数越少意味着访问磁盘的次数越少,消耗的 CPU 资源越少,优化器还会结合是否使用临时表、是否排序等因素进行综合判断</p><hr><h4 id="统计数据">统计数据</h4><p>MySQL 中保存着两种统计数据:</p><ul><li>innodb_table_stats 存储了表的统计数据,每一条记录对应着一个表的统计数据</li><li>innodb_index_stats 存储了索引的统计数据,每一条记录对应着一个索引的一个统计项的数据</li></ul><p>MySQL 在真正执行语句之前,并不能精确地知道满足条件的记录有多少条,只能根据统计信息来估算记录,统计信息就是索引的区分度,一个索引上不同的值的个数(比如性别只能是男女,就是 2 ),称之为基数(cardinality),<strong>基数越大说明区分度越好</strong></p><p>通过<strong>采样统计</strong>来获取基数,InnoDB 默认会选择 N 个数据页,统计这些页面上的不同值得到一个平均值,然后乘以这个索引的页面数,就得到了这个索引的基数</p><p>在 MySQL 中,有两种存储统计数据的方式,可以通过设置参数 <code>innodb_stats_persistent</code> 的值来选择:</p><ul><li>ON:表示统计信息会持久化存储(默认),采样页数 N 默认为 20,可以通过 <code>innodb_stats_persistent_sample_pages</code> 指定,页数越多统计的数据越准确,但消耗的资源更大</li><li>OFF:表示统计信息只存储在内存,采样页数 N 默认为 8,也可以通过系统变量设置(不推荐,每次重新计算浪费资源)</li></ul><p>数据表是会持续更新的,两种统计信息的更新方式:</p><ul><li>设置 <code>innodb_stats_auto_recalc</code> 为 1,当发生变动的记录数量超过表大小的 10% 时,自动触发重新计算,不过是<strong>异步进行</strong></li><li>调用 <code>ANALYZE TABLE t</code> 手动更新统计信息,只对信息做<strong>重新统计</strong>(不是重建表),没有修改数据,这个过程中加了 MDL 读锁并且是同步进行,所以会暂时阻塞系统</li></ul><p><strong>EXPLAIN 执行计划在优化器阶段生成</strong>,如果 explain 的结果预估的 rows 值跟实际情况差距比较大,可以执行 analyze 命令重新修正信息</p><hr><h4 id="错选索引">错选索引</h4><p>采样统计本身是估算数据,或者 SQL 语句中的字段选择有问题时,可能导致 MySQL 没有选择正确的执行索引</p><p>解决方法:</p><ul><li><p>采用 force index 强行选择一个索引</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> <span class="keyword">user</span> FORCE INDEX(name) <span class="keyword">WHERE</span> NAME<span class="operator">=</span><span class="string">'seazean'</span>;</span><br></pre></td></tr></table></figure></li><li><p>可以考虑修改 SQL 语句,引导 MySQL 使用期望的索引</p></li><li><p>新建一个更合适的索引,来提供给优化器做选择,或删掉误用的索引</p></li></ul><hr><h3 id="终止流程">终止流程</h3><h4 id="终止语句">终止语句</h4><p>终止线程中正在执行的语句:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">KILL QUERY thread_id</span><br></pre></td></tr></table></figure><p>KILL 不是马上终止的意思,而是告诉执行线程这条语句已经不需要继续执行,可以开始执行停止的逻辑(类似于打断)。因为对表做增删改查操作,会在表上加 MDL 读锁,如果线程被 KILL 时就直接终止,那这个 MDL 读锁就没机会被释放了</p><p>命令 <code>KILL QUERYthread_id_A</code> 的执行流程:</p><ul><li>把 session A 的运行状态改成 THD::KILL_QUERY(将变量 killed 赋值为 THD::KILL_QUERY)</li><li>给 session A 的执行线程发一个信号,让 session A 来处理这个 THD::KILL_QUERY 状态</li></ul><p>会话处于等待状态(锁阻塞),必须满足是一个可以被唤醒的等待,必须有机会去<strong>判断线程的状态</strong>,如果不满足就会造成 KILL 失败</p><p>典型场景:innodb_thread_concurrency 为 2,代表并发线程上限数设置为 2</p><ul><li>session A 执行事务,session B 执行事务,达到线程上限;此时 session C 执行事务会阻塞等待,session D 执行 kill query C 无效</li><li>C 的逻辑是每 10 毫秒判断是否可以进入 InnoDB 执行,如果不行就调用 nanosleep 函数进入 sleep 状态,没有去判断线程状态</li></ul><p><code>补充:执行 Ctrl+C 的时候,是 MySQL 客户端另外启动一个连接,然后发送一个 KILL QUERY 命令</code></p><hr><h4 id="终止连接">终止连接</h4><p>断开线程的连接:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">KILL CONNECTION id</span><br></pre></td></tr></table></figure><p>断开连接后执行 SHOW PROCESSLIST 命令,如果这条语句的 Command 列显示 Killed,代表线程的状态是 KILL_CONNECTION,说明这个线程有语句正在执行,当前状态是停止语句执行中,终止逻辑耗时较长</p><ul><li>超大事务执行期间被 KILL,这时回滚操作需要对事务执行期间生成的所有新数据版本做回收操作,耗时很长</li><li>大查询回滚,如果查询过程中生成了比较大的临时文件,删除临时文件可能需要等待 IO 资源,导致耗时较长</li><li>DDL 命令执行到最后阶段被 KILL,需要删除中间过程的临时文件,也可能受 IO 资源影响耗时较久</li></ul><p>总结:KILL CONNECTION 本质上只是把客户端的 SQL 连接断开,后面的终止流程还是要走 KILL QUERY</p><p>一个事务被 KILL 之后,持续处于回滚状态,不应该强行重启整个 MySQL 进程,应该等待事务自己执行完成,因为重启后依然继续做回滚操作的逻辑</p><h2 id="存储引擎">存储引擎</h2><h3 id="基本介绍">基本介绍</h3><p>对比其他数据库,MySQL 的架构可以在不同场景应用并发挥良好作用,主要体现在存储引擎,插件式的存储引擎架构将查询处理和其他的系统任务以及数据的存储提取分离,可以针对不同的存储需求可以选择最优的存储引擎</p><p>存储引擎的介绍:</p><ul><li>MySQL 数据库使用不同的机制存取表文件 , 机制的差别在于不同的存储方式、索引技巧、锁定水平等不同的功能和能力,在 MySQL 中,将这些不同的技术及配套的功能称为存储引擎</li><li>Oracle、SqlServer 等数据库只有一种存储引擎,MySQL <strong>提供了插件式的存储引擎架构</strong>,所以 MySQL 存在多种存储引擎 , 就会让数据库采取了不同的处理数据的方式和扩展功能</li><li>在关系型数据库中数据的存储是以表的形式存进行,所以存储引擎也称为表类型(存储和操作此表的类型)</li><li>通过选择不同的引擎,能够获取最佳的方案, 也能够获得额外的速度或者功能,提高程序的整体效果。</li></ul><p>MySQL 支持的存储引擎:</p><ul><li>MySQL 支持的引擎包括:InnoDB、MyISAM、MEMORY、Archive、Federate、CSV、BLACKHOLE 等</li><li>MySQL5.5 之前的默认存储引擎是 MyISAM,5.5 之后就改为了 InnoDB</li></ul><hr><h3 id="引擎对比">引擎对比</h3><p>MyISAM 存储引擎:</p><ul><li>特点:不支持事务和外键,读取速度快,节约资源</li><li>应用场景:查询和插入操作为主,只有很少更新和删除操作,并对事务的完整性、并发性要求不高</li><li>存储方式:<ul><li>每个 MyISAM 在磁盘上存储成 3 个文件,其文件名都和表名相同,拓展名不同</li><li>表的定义保存在 .frm 文件,表数据保存在 .MYD (MYData) 文件中,索引保存在 .MYI (MYIndex) 文件中</li></ul></li></ul><p>InnoDB 存储引擎:(MySQL5.5 版本后默认的存储引擎)</p><ul><li>特点:<strong>支持事务</strong>和外键操作,支持并发控制。对比 MyISAM 的存储引擎,InnoDB 写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引</li><li>应用场景:对事务的完整性有比较高的要求,在并发条件下要求数据的一致性,读写频繁的操作</li><li>存储方式:<ul><li>使用共享表空间存储, 这种方式创建的表的表结构保存在 .frm 文件中, 数据和索引保存在 innodb_data_home_dir 和 innodb_data_file_path 定义的表空间中,可以是多个文件</li><li>使用多表空间存储,创建的表的表结构存在 .frm 文件中,每个表的数据和索引单独保存在 .ibd 中</li></ul></li></ul><p>MERGE 存储引擎:</p><ul><li><p>特点:</p><ul><li>是一组 MyISAM 表的组合,这些 MyISAM 表必须结构完全相同,通过将不同的表分布在多个磁盘上</li><li>MERGE 表本身并没有存储数据,对 MERGE 类型的表可以进行查询、更新、删除操作,这些操作实际上是对内部的 MyISAM 表进行的</li></ul></li><li><p>应用场景:将一系列等同的 MyISAM 表以逻辑方式组合在一起,并作为一个对象引用他们,适合做数据仓库</p></li><li><p>操作方式:</p><ul><li>插入操作是通过 INSERT_METHOD 子句定义插入的表,使用 FIRST 或 LAST 值使得插入操作被相应地作用在第一或者最后一个表上;不定义这个子句或者定义为 NO,表示不能对 MERGE 表执行插入操作</li><li>对 MERGE 表进行 DROP 操作,但是这个操作只是删除 MERGE 表的定义,对内部的表是没有任何影响的</li></ul></li></ul><table><thead><tr><th>特性</th><th>MyISAM</th><th>InnoDB</th><th>MEMORY</th></tr></thead><tbody><tr><td>存储限制</td><td>有(平台对文件系统大小的限制)</td><td>64TB</td><td>有(平台的内存限制)</td></tr><tr><td><strong>事务安全</strong></td><td><strong>不支持</strong></td><td><strong>支持</strong></td><td><strong>不支持</strong></td></tr><tr><td><strong>锁机制</strong></td><td><strong>表锁</strong></td><td><strong>表锁/行锁</strong></td><td><strong>表锁</strong></td></tr><tr><td>B+Tree 索引</td><td>支持</td><td>支持</td><td>支持</td></tr><tr><td>哈希索引</td><td>不支持</td><td>不支持</td><td>支持</td></tr><tr><td>全文索引</td><td>支持</td><td>支持</td><td>不支持</td></tr><tr><td>集群索引</td><td>不支持</td><td>支持</td><td>不支持</td></tr><tr><td>数据索引</td><td>不支持</td><td>支持</td><td>支持</td></tr><tr><td>数据缓存</td><td>不支持</td><td>支持</td><td>N/A</td></tr><tr><td>索引缓存</td><td>支持</td><td>支持</td><td>N/A</td></tr><tr><td>数据可压缩</td><td>支持</td><td>不支持</td><td>不支持</td></tr><tr><td>空间使用</td><td>低</td><td>高</td><td>N/A</td></tr><tr><td>内存使用</td><td>低</td><td>高</td><td>中等</td></tr><tr><td>批量插入速度</td><td>高</td><td>低</td><td>高</td></tr><tr><td><strong>外键</strong></td><td><strong>不支持</strong></td><td><strong>支持</strong></td><td><strong>不支持</strong></td></tr></tbody></table><p>MyISAM 和 InnoDB 的区别?</p><ul><li>事务:InnoDB 支持事务,MyISAM 不支持事务</li><li>外键:InnoDB 支持外键,MyISAM 不支持外键</li><li>索引:InnoDB 是聚集(聚簇)索引,MyISAM 是非聚集(非聚簇)索引</li><li>锁粒度:InnoDB 最小的锁粒度是行锁,MyISAM 最小的锁粒度是表锁</li><li>存储结构:参考本节上半部分</li></ul><hr><h3 id="引擎操作">引擎操作</h3><ul><li><p>查询数据库支持的存储引擎</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SHOW ENGINES;</span><br><span class="line">SHOW VARIABLES LIKE '%storage_engine%'; -- 查看Mysql数据库默认的存储引擎 </span><br></pre></td></tr></table></figure></li><li><p>查询某个数据库中所有数据表的存储引擎</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW TABLE STATUS FROM 数据库名称;</span><br></pre></td></tr></table></figure></li><li><p>查询某个数据库中某个数据表的存储引擎</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW TABLE STATUS FROM 数据库名称 WHERE NAME = '数据表名称';</span><br></pre></td></tr></table></figure></li><li><p>创建数据表,指定存储引擎</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE 表名(</span><br><span class="line">列名,数据类型,</span><br><span class="line"> ...</span><br><span class="line">)ENGINE = 引擎名称;</span><br></pre></td></tr></table></figure></li><li><p>修改数据表的存储引擎</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ALTER TABLE 表名 ENGINE = 引擎名称;</span><br></pre></td></tr></table></figure></li></ul><h2 id="索引机制">索引机制</h2><h3 id="索引介绍">索引介绍</h3><h4 id="基本介绍-2">基本介绍</h4><p>MySQL 官方对索引的定义为:索引(index)是帮助 MySQL 高效获取数据的一种数据结构,**本质是排好序的快速查找数据结构。**在表数据之外,数据库系统还维护着满足特定查找算法的数据结构,这些数据结构以某种方式指向数据, 这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引。</p><p><strong>索引是在存储引擎层实现的</strong>,所以并没有统一的索引标准,即不同存储引擎的索引的工作方式并不一样</p><p>索引使用:一张数据表,用于保存数据;一个索引配置文件,用于保存索引;每个索引都指向了某一个数据<br><img src="../image/post/MySQL-%E7%B4%A2%E5%BC%95%E7%9A%84%E4%BB%8B%E7%BB%8D.png" alt=""></p><p>左边是数据表,一共有两列七条记录,最左边的是数据记录的物理地址(注意逻辑上相邻的记录在磁盘上也并不是一定物理相邻的)。为了加快 Col2 的查找,可以维护一个右边所示的二叉查找树,每个节点分别包含索引键值和一个指向对应数据的物理地址的指针,这样就可以运用二叉查找快速获取到相应数据</p><p>索引的优点:</p><ul><li>类似于书籍的目录索引,提高数据检索的效率,降低数据库的 IO 成本</li><li>通过索引列对数据进行排序,降低数据排序的成本,降低 CPU 的消耗</li></ul><p>索引的缺点:</p><ul><li>一般来说索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式<strong>存储在磁盘</strong>上</li><li>虽然索引大大提高了查询效率,同时却也降低更新表的速度。对表进行 INSERT、UPDATE、DELETE 操作,MySQL 不仅要保存数据,还要保存一下索引文件每次更新添加了索引列的字段,还会调整因为更新所带来的键值变化后的索引信息,<strong>但是更新数据也需要先从数据库中获取</strong>,索引加快了获取速度,所以可以相互抵消一下。</li><li>索引会影响到 WHERE 的查询条件和排序 ORDER BY 两大功能</li></ul><hr><h4 id="索引分类">索引分类</h4><p>索引一般的分类如下:</p><ul><li><p>功能分类</p><ul><li>主键索引:一种特殊的唯一索引,不允许有空值,一般在建表时同时创建主键索引</li><li>单列索引:一个索引只包含单个列,一个表可以有多个单列索引(普通索引)</li><li>联合索引:顾名思义,就是将单列索引进行组合</li><li>唯一索引:索引列的值必须唯一,<strong>允许有空值</strong>,如果是联合索引,则列值组合必须唯一<ul><li>NULL 值可以出现多次,因为两个 NULL 比较的结果既不相等,也不不等,结果仍然是未知</li><li>可以声明不允许存储 NULL 值的非空唯一索引</li></ul></li><li>外键索引:只有 InnoDB 引擎支持外键索引,用来保证数据的一致性、完整性和实现级联操作</li></ul></li><li><p>结构分类</p><ul><li>BTree 索引:MySQL 使用最频繁的一个索引数据结构,是 InnoDB 和 MyISAM 存储引擎默认的索引类型,底层基于 B+Tree</li><li>Hash 索引:MySQL中 Memory 存储引擎默认支持的索引类型</li><li>R-tree 索引(空间索引):空间索引是 MyISAM 引擎的一个特殊索引类型,主要用于地理空间数据类型</li><li>Full-text 索引(全文索引):快速匹配全部文档的方式。MyISAM 支持, InnoDB 不支持 FULLTEXT 类型的索引,但是 InnoDB 可以使用 sphinx 插件支持全文索引,MEMORY 引擎不支持</li></ul><table><thead><tr><th>索引</th><th>InnoDB</th><th>MyISAM</th><th>Memory</th></tr></thead><tbody><tr><td>BTREE</td><td>支持</td><td>支持</td><td>支持</td></tr><tr><td>HASH</td><td>不支持</td><td>不支持</td><td>支持</td></tr><tr><td>R-tree</td><td>不支持</td><td>支持</td><td>不支持</td></tr><tr><td>Full-text</td><td>5.6 版本之后支持</td><td>支持</td><td>不支持</td></tr></tbody></table></li></ul><p>联合索引图示:根据身高年龄建立的组合索引(height,age)</p><p><img src="../image/post/MySQL-%E7%BB%84%E5%90%88%E7%B4%A2%E5%BC%95%E5%9B%BE.png" alt=""></p><hr><h3 id="索引操作">索引操作</h3><p>索引在创建表的时候可以同时创建, 也可以随时增加新的索引</p><ul><li><p>创建索引:如果一个表中有一列是主键,那么会<strong>默认为其创建主键索引</strong>(主键列不需要单独创建索引)</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">CREATE [UNIQUE|FULLTEXT] INDEX 索引名称 [USING 索引类型] ON 表名(列名...);</span><br><span class="line">-- 索引类型默认是 B+TREE</span><br></pre></td></tr></table></figure></li><li><p>查看索引</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW INDEX FROM 表名;</span><br></pre></td></tr></table></figure></li><li><p>添加索引</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">-- 单列索引</span><br><span class="line">ALTER TABLE 表名 ADD INDEX 索引名称(列名);</span><br><span class="line"></span><br><span class="line">-- 组合索引</span><br><span class="line">ALTER TABLE 表名 ADD INDEX 索引名称(列名1,列名2,...);</span><br><span class="line"></span><br><span class="line">-- 主键索引</span><br><span class="line">ALTER TABLE 表名 ADD PRIMARY KEY(主键列名); </span><br><span class="line"></span><br><span class="line">-- 外键索引(添加外键约束,就是外键索引)</span><br><span class="line">ALTER TABLE 表名 ADD CONSTRAINT 外键名 FOREIGN KEY (本表外键列名) REFERENCES 主表名(主键列名);</span><br><span class="line"></span><br><span class="line">-- 唯一索引</span><br><span class="line">ALTER TABLE 表名 ADD UNIQUE 索引名称(列名);</span><br><span class="line"></span><br><span class="line">-- 全文索引(mysql只支持文本类型)</span><br><span class="line">ALTER TABLE 表名 ADD FULLTEXT 索引名称(列名);</span><br></pre></td></tr></table></figure></li><li><p>删除索引</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DROP INDEX 索引名称 ON 表名;</span><br></pre></td></tr></table></figure></li></ul><h3 id="聚簇索引">聚簇索引</h3><h4 id="对比">对比</h4><p>聚簇索引是一种数据存储方式,并不是一种单独的索引类型</p><ul><li><p>聚簇索引的叶子节点存放的是主键值和数据行,支持覆盖索引</p></li><li><p>非聚簇索引的叶子节点存放的是主键值或指向数据行的指针(由存储引擎决定)</p></li></ul><p>在 Innodb 下主键索引是聚簇索引,在 MyISAM 下主键索引是非聚簇索引</p><hr><h4 id="Innodb">Innodb</h4><h5 id="聚簇索引-2">聚簇索引</h5><p>在 Innodb 存储引擎,B+ 树索引可以分为聚簇索引(也称聚集索引、clustered index)和辅助索引(也称非聚簇索引或二级索引、secondary index、non-clustered index)</p><p>InnoDB 中,聚簇索引是按照每张表的主键构造一颗 B+ 树,叶子节点中存放的就是整张表的数据,将聚簇索引的叶子节点称为数据页</p><ul><li>这个特性决定了<strong>数据也是索引的一部分</strong>,所以一张表只能有一个聚簇索引</li><li>辅助索引的存在不影响聚簇索引中数据的组织,所以一张表可以有多个辅助索引</li></ul><p>聚簇索引的优点:</p><ul><li>数据访问更快,聚簇索引将索引和数据保存在同一个 B+ 树中,因此从聚簇索引中获取数据比非聚簇索引更快</li><li>聚簇索引对于主键的排序查找和范围查找速度非常快</li></ul><p>聚簇索引的缺点:</p><ul><li><p>插入速度严重依赖于插入顺序,按照主键的顺序(递增)插入是最快的方式,否则将会出现页分裂,严重影响性能,所以对于 InnoDB 表,一般都会定义一个自增的 ID 列为主键</p></li><li><p>更新主键的代价很高,将会导致被更新的行移动,所以对于 InnoDB 表,一般定义主键为不可更新</p></li><li><p>二级索引访问需要两次索引查找,第一次找到主键值,第二次根据主键值找到行数据</p></li></ul><h5 id="辅助索引">辅助索引</h5><p>在聚簇索引之上创建的索引称之为辅助索引,非聚簇索引都是辅助索引,像复合索引、前缀索引、唯一索引等</p><p>辅助索引叶子节点存储的是主键值,而不是数据的物理地址,所以访问数据需要二次查找,推荐使用覆盖索引,可以减少回表查询</p><p><strong>检索过程</strong>:辅助索引找到主键值,再通过聚簇索引(二分)找到数据页,最后通过数据页中的 Page Directory(二分)找到对应的数据分组,遍历组内所所有的数据找到数据行</p><p>补充:无索引走全表查询,查到数据页后和上述步骤一致</p><hr><h5 id="索引实现">索引实现</h5><p>InnoDB 使用 B+Tree 作为索引结构,并且 InnoDB 一定有索引</p><p>主键索引:</p><ul><li><p>在 InnoDB 中,表数据文件本身就是按 B+Tree 组织的一个索引结构,这个索引的 key 是数据表的主键,叶子节点 data 域保存了完整的数据记录</p></li><li><p>InnoDB 的表数据文件<strong>通过主键聚集数据</strong>,如果没有定义主键,会选择非空唯一索引代替,如果也没有这样的列,MySQL 会自动为 InnoDB 表生成一个<strong>隐含字段 row_id</strong> 作为主键,这个字段长度为 6 个字节,类型为长整形</p></li></ul><p>辅助索引:</p><ul><li><p>InnoDB 的所有辅助索引(二级索引)都引用主键作为 data 域</p></li><li><p>InnoDB 表是基于聚簇索引建立的,因此 InnoDB 的索引能提供一种非常快速的主键查找性能。不过辅助索引也会包含主键列,所以不建议使用过长的字段作为主键,<strong>过长的主索引会令辅助索引变得过大</strong></p></li></ul><p><img src="../image/post/MySQL-InnoDB%E8%81%9A%E7%B0%87%E5%92%8C%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84.png" alt=""></p><hr><h4 id="MyISAM">MyISAM</h4><h5 id="非聚簇">非聚簇</h5><p>MyISAM 的主键索引使用的是非聚簇索引,索引文件和数据文件是分离的,<strong>索引文件仅保存数据的地址</strong></p><ul><li>主键索引 B+ 树的节点存储了主键,辅助键索引 B+ 树存储了辅助键,表数据存储在独立的地方,这两颗 B+ 树的叶子节点都使用一个地址指向真正的表数据,对于表数据来说,这两个键没有任何差别</li><li>由于索引树是独立的,通过辅助索引检索<strong>无需回表查询</strong>访问主键的索引树</li></ul><p><img src="../image/post/MySQL-%E8%81%9A%E7%B0%87%E7%B4%A2%E5%BC%95%E5%92%8C%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95%E6%A3%80%E9%94%81%E6%95%B0%E6%8D%AE%E5%9B%BE.jpg" alt=""></p><hr><h5 id="索引实现-2">索引实现</h5><p>MyISAM 的索引方式也叫做非聚集的,之所以这么称呼是为了与 InnoDB 的聚集索引区分</p><p>主键索引:MyISAM 引擎使用 B+Tree 作为索引结构,叶节点的 data 域存放的是数据记录的地址</p><p>辅助索引:MyISAM 中主索引和辅助索引(Secondary key)在结构上没有任何区别,只是主索引要求 key 是唯一的,而辅助索引的 key 可以重复</p><p><img src="../image/post/MySQL-MyISAM%E4%B8%BB%E9%94%AE%E5%92%8C%E8%BE%85%E5%8A%A9%E7%B4%A2%E5%BC%95%E7%BB%93%E6%9E%84.png" alt=""></p><p>参考文章:<a href="https://blog.csdn.net/lm1060891265/article/details/81482136">https://blog.csdn.net/lm1060891265/article/details/81482136</a></p><hr><h3 id="索引结构">索引结构</h3><h4 id="数据页">数据页</h4><p>文件系统的最小单元是块(block),一个块的大小是 4K,系统从磁盘读取数据到内存时是以磁盘块为基本单位的,位于同一个磁盘块中的数据会被一次性读取出来,而不是需要什么取什么</p><p>InnoDB 存储引擎中有页(Page)的概念,页是 MySQL 磁盘管理的最小单位</p><ul><li><strong>InnoDB 存储引擎中默认每个页的大小为 16KB,索引中一个节点就是一个数据页</strong>,所以会一次性读取 16KB 的数据到内存</li><li>InnoDB 引擎将若干个地址连接磁盘块,以此来达到页的大小 16KB</li><li>在查询数据时如果一个页中的每条数据都能有助于定位数据记录的位置,这将会减少磁盘 I/O 次数,提高查询效率</li></ul><p>数据页物理结构,从上到下:</p><ul><li>File Header:上一页和下一页的指针、该页的类型(索引页、数据页、日志页等)、<strong>校验和</strong>、LSN(最近一次修改当前页面时的系统 lsn 值,事务持久性部分详解)等信息</li><li>Page Header:记录状态信息</li><li>Infimum + Supremum:当前页的最小记录和最大记录(头尾指针),Infimum 所在分组只有一条记录,Supremum 所在分组可以有 1 ~ 8 条记录,剩余的分组可以有 4 ~ 8 条记录</li><li>User Records:存储数据的记录</li><li>Free Space:尚未使用的存储空间</li><li>Page Directory:分组的目录,可以通过目录快速定位(二分法)数据的分组</li><li>File Trailer:检验和字段,在刷脏过程中,页首和页尾的校验和一致才能说明页面刷新成功,二者不同说明刷新期间发生了错误;LSN 字段,也是用来校验页面的完整性</li></ul><p>数据页中包含数据行,数据的存储是基于数据行的,数据行有 next_record 属性指向下一个行数据,所以是可以遍历的,但是一组数据至多 8 个行,通过 Page Directory 先定位到组,然后遍历获取所需的数据行即可</p><p>数据行中有三个隐藏字段:trx_id、roll_pointer、row_id(在事务章节会详细介绍它们的作用)</p><hr><h4 id="BTree">BTree</h4><p>BTree 的索引类型是基于 B+Tree 树型数据结构的,B+Tree 又是 BTree 数据结构的变种,用在数据库和操作系统中的文件系统,特点是能够保持数据稳定有序</p><p>BTree 又叫多路平衡搜索树,一颗 m 叉的 BTree 特性如下:</p><ul><li>树中每个节点最多包含 m 个孩子</li><li>除根节点与叶子节点外,每个节点至少有 [ceil(m/2)] 个孩子</li><li>若根节点不是叶子节点,则至少有两个孩子</li><li>所有的叶子节点都在同一层</li><li>每个非叶子节点由 n 个 key 与 n+1 个指针组成,其中 [ceil(m/2)-1] <= n <= m-1</li></ul><p>5 叉,key 的数量 [ceil(m/2)-1] <= n <= m-1 为 2 <= n <=4 ,当 n>4 时中间节点分裂到父节点,两边节点分裂</p><p>插入 C N G A H E K Q M F W L T Z D P R X Y S 数据的工作流程:</p><ul><li><p>插入前 4 个字母 C N G A</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B1.png" alt=""></p></li><li><p>插入 H,n>4,中间元素 G 字母向上分裂到新的节点</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B2.png" alt=""></p></li><li><p>插入 E、K、Q 不需要分裂</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B3.png" alt=""></p></li><li><p>插入 M,中间元素 M 字母向上分裂到父节点 G</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B4.png" alt=""></p></li><li><p>插入 F,W,L,T 不需要分裂</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B5.png" alt=""></p></li><li><p>插入 Z,中间元素 T 向上分裂到父节点中</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B6.png" alt=""></p></li><li><p>插入 D,中间元素 D 向上分裂到父节点中,然后插入 P,R,X,Y 不需要分裂</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B7.png" alt=""></p></li><li><p>最后插入 S,NPQR 节点 n>5,中间节点 Q 向上分裂,但分裂后父节点 DGMT 的 n>5,中间节点 M 向上分裂</p><p><img src="../image/post/MySQL-BTree%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B8.png" alt=""></p></li></ul><p>BTree 树就已经构建完成了,BTree 树和二叉树相比, 查询数据的效率更高, 因为对于相同的数据量来说,<strong>BTree 的层级结构比二叉树少</strong>,所以搜索速度快</p><p>BTree 结构的数据可以让系统高效的找到数据所在的磁盘块,定义一条记录为一个二元组 [key, data] ,key 为记录的键值,对应表中的主键值,data 为一行记录中除主键外的数据。对于不同的记录,key 值互不相同,BTree 中的每个节点根据实际情况可以包含大量的关键字信息和分支<br><img src="../image/post/%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8E%9F%E7%90%861.png" alt=""></p><p>缺点:当进行范围查找时会出现回旋查找</p><hr><h4 id="B-Tree">B+Tree</h4><h5 id="数据结构">数据结构</h5><p>BTree 数据结构中每个节点中不仅包含数据的 key 值,还有 data 值。磁盘中每一页的存储空间是有限的,如果 data 数据较大时将会导致每个节点(即一个页)能存储的 key 的数量很小,当存储的数据量很大时同样会导致 B-Tree 的深度较大,增大查询时的磁盘 I/O 次数,进而影响查询效率,所以引入 B+Tree</p><p>B+Tree 为 BTree 的变种,B+Tree 与 BTree 的区别为:</p><ul><li>n 叉 B+Tree 最多含有 n 个 key(哈希值),而 BTree 最多含有 n-1 个 key</li></ul><ul><li>所有<strong>非叶子节点只存储键值 key</strong> 信息,只进行数据索引,使每个非叶子节点所能保存的关键字大大增加</li><li>所有<strong>数据都存储在叶子节点</strong>,所以每次数据查询的次数都一样</li><li><strong>叶子节点按照 key 大小顺序排列,左边结尾数据都会保存右边节点开始数据的指针,形成一个链表</strong></li><li>所有节点中的 key 在叶子节点中也存在(比如 5),<strong>key 允许重复</strong>,B 树不同节点不存在重复的 key</li></ul><img src="../image/post/MySQL-B%E5%8A%A0Tree%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84.png" style="zoom:67%;" /><p>B* 树:是 B+ 树的变体,在 B+ 树的非根和非叶子结点再增加指向兄弟的指针</p><hr><h5 id="优化结构">优化结构</h5><p>MySQL 索引数据结构对经典的 B+Tree 进行了优化,在原 B+Tree 的基础上,增加一个指向相邻叶子节点的链表指针,就形成了带有顺序指针的 B+Tree,<strong>提高区间访问的性能,防止回旋查找</strong></p><p>区间访问的意思是访问索引为 5 - 15 的数据,可以直接根据相邻节点的指针遍历</p><p>B+ 树的<strong>叶子节点是数据页</strong>(page),一个页里面可以存多个数据行</p><p><img src="../image/post/%E7%B4%A2%E5%BC%95%E7%9A%84%E5%8E%9F%E7%90%862.png" alt=""></p><p>通常在 B+Tree 上有两个头指针,<strong>一个指向根节点,另一个指向关键字最小的叶子节点</strong>,而且所有叶子节点(即数据节点)之间是一种链式环结构。可以对 B+Tree 进行两种查找运算:</p><ul><li>有范围:对于主键的范围查找和分页查找</li><li>有顺序:从根节点开始,进行随机查找,顺序查找</li></ul><p>InnoDB 中每个数据页的大小默认是 16KB,</p><ul><li>索引行:一般表的主键类型为 INT(4 字节)或 BIGINT(8 字节),指针大小在 InnoDB 中设置为 6 字节节,也就是说一个页大概存储 16KB/(8B+6B)=1K 个键值(估值)。则一个深度为 3 的 B+Tree 索引可以维护 <code>10^3 * 10^3 * 10^3 = 10亿</code> 条记录</li><li>数据行:一行数据的大小可能是 1k,一个数据页可以存储 16 行</li></ul><p>实际情况中每个节点可能不能填充满,因此在数据库中,B+Tree 的高度一般都在 2-4 层。MySQL 的 InnoDB 存储引擎在设计时是<strong>将根节点常驻内存的</strong>,也就是说查找某一键值的行记录时最多只需要 1~3 次磁盘 I/O 操作</p><p>B+Tree 优点:提高查询速度,减少磁盘的 IO 次数,树形结构较小</p><hr><h5 id="索引维护">索引维护</h5><p>B+ 树为了保持索引的有序性,在插入新值的时候需要做相应的维护</p><p>每个索引中每个块存储在磁盘页中,可能会出现以下两种情况:</p><ul><li>如果所在的数据页已经满了,这时候需要申请一个新的数据页,然后挪动部分数据过去,这个过程称为<strong>页分裂</strong>,原本放在一个页的数据现在分到两个页中,降低了空间利用率</li><li>当相邻两个页由于删除了数据,利用率很低之后,会将数据页做<strong>页合并</strong>,合并的过程可以认为是分裂过程的逆过程</li><li>这两个情况都是由 B+ 树的结构决定的</li></ul><p>一般选用数据小的字段做索引,字段长度越小,普通索引的叶子节点就越小,普通索引占用的空间也就越小</p><p>自增主键的插入数据模式,可以让主键索引尽量地保持递增顺序插入,不涉及到挪动其他记录,<strong>避免了页分裂</strong></p><h3 id="设计原则">设计原则</h3><p>索引的设计可以遵循一些已有的原则,创建索引的时候请尽量考虑符合这些原则,便于提升索引的使用效率</p><p>创建索引时的原则:</p><ul><li>对查询频次较高,且数据量比较大的表建立索引</li><li>使用唯一索引,区分度越高,使用索引的效率越高</li><li>索引字段的选择,最佳候选列应当从 where 子句的条件中提取,使用覆盖索引</li><li>使用短索引,索引创建之后也是使用硬盘来存储的,因此提升索引访问的 I/O 效率,也可以提升总体的访问效率。假如构成索引的字段总长度比较短,那么在给定大小的存储块内可以存储更多的索引值,相应的可以有效的提升 MySQL 访问索引的 I/O 效率</li><li>索引可以有效的提升查询数据的效率,但索引数量不是多多益善,索引越多,维护索引的代价越高。对于插入、更新、删除等 DML 操作比较频繁的表来说,索引过多,会引入相当高的维护代价,降低 DML 操作的效率,增加相应操作的时间消耗;另外索引过多的话,MySQL 也会犯选择困难病,虽然最终仍然会找到一个可用的索引,但提高了选择的代价</li></ul><ul><li><p>MySQL 建立联合索引时会遵守<strong>最左前缀匹配原则</strong>,即最左优先,在检索数据时从联合索引的最左边开始匹配</p><p>N 个列组合而成的组合索引,相当于创建了 N 个索引,如果查询时 where 句中使用了组成该索引的<strong>前</strong>几个字段,那么这条查询 SQL 可以利用组合索引来提升查询效率</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">-- 对name、address、phone列建一个联合索引</span><br><span class="line">ALTER TABLE user ADD INDEX index_three(name,address,phone);</span><br><span class="line">-- 查询语句执行时会依照最左前缀匹配原则,检索时分别会使用索引进行数据匹配。</span><br><span class="line">(name,address,phone)</span><br><span class="line">(name,address)</span><br><span class="line">(name,phone)-- 只有name字段走了索引</span><br><span class="line">(name)</span><br><span class="line"></span><br><span class="line">-- 索引的字段可以是任意顺序的,优化器会帮助我们调整顺序,下面的SQL语句可以命中索引</span><br><span class="line">SELECT * FROM user WHERE address = '北京' AND phone = '12345' AND name = '张三';</span><br></pre></td></tr></table></figure> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-- 如果联合索引中最左边的列不包含在条件查询中,SQL语句就不会命中索引,比如:</span><br><span class="line">SELECT * FROM user WHERE address = '北京' AND phone = '12345'; </span><br></pre></td></tr></table></figure></li></ul><p>哪些情况不要建立索引:</p><ul><li>记录太少的表</li><li>经常增删改的表</li><li>频繁更新的字段不适合创建索引</li><li>where 条件里用不到的字段不创建索引</li></ul><hr><h3 id="索引相关概念">索引相关概念</h3><h4 id="覆盖索引">覆盖索引</h4><p>覆盖索引:包含所有满足查询需要的数据的索引(SELECT 后面的字段刚好是索引字段),可以利用该索引返回 SELECT 列表的字段,而不必根据索引去聚簇索引上读取数据文件</p><p>回表查询:要查找的字段不在非主键索引树上时,需要通过叶子节点的主键值去主键索引上获取对应的行数据</p><p>使用覆盖索引,防止回表查询:</p><ul><li><p>表 user 主键为 id,普通索引为 age,查询语句:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM user WHERE age = 30;</span><br></pre></td></tr></table></figure><p>查询过程:先通过普通索引 age=30 定位到主键值 id=1,再通过聚集索引 id=1 定位到行记录数据,需要两次扫描 B+ 树</p></li><li><p>使用覆盖索引:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">DROP INDEX idx_age ON user;</span><br><span class="line">CREATE INDEX idx_age_name ON user(age,name);</span><br><span class="line">SELECT id,age FROM user WHERE age = 30;</span><br></pre></td></tr></table></figure><p>在一棵索引树上就能获取查询所需的数据,无需回表速度更快</p></li></ul><p>使用覆盖索引,要注意 SELECT 列表中只取出需要的列,不可用 SELECT *,所有字段一起做索引会导致索引文件过大,查询性能下降</p><hr><h4 id="索引下推">索引下推</h4><p>索引条件下推优化(Index Condition Pushdown,ICP)是 MySQL5.6 添加,可以在索引遍历过程中,对索引中包含的字段先做判断,直接过滤掉不满足条件的记录,减少回表次数</p><p>索引下推充分利用了索引中的数据,在查询出整行数据之前过滤掉无效的数据,再去主键索引树上查找</p><ul><li><p>不使用索引下推优化时存储引擎通过索引检索到数据,然后回表查询记录返回给 Server 层,<strong>服务器判断数据是否符合条件</strong></p><p><img src="../image/post/MySQL-%E4%B8%8D%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8.png" alt=""></p></li><li><p>使用索引下推优化时,如果<strong>存在某些被索引的列的判断条件</strong>时,由存储引擎在索引遍历的过程中判断数据是否符合传递的条件,将符合条件的数据进行回表,检索出来返回给服务器,由此减少 IO 次数</p><p><img src="../image/post/MySQL-%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8.png" alt=""></p></li></ul><p><strong>适用条件</strong>:</p><ul><li>需要存储引擎将索引中的数据与条件进行判断(所以<strong>条件列必须都在同一个索引中</strong>),所以优化是基于存储引擎的,只有特定引擎可以使用,适用于 InnoDB 和 MyISAM</li><li>存储引擎没有调用跨存储引擎的能力,跨存储引擎的功能有存储过程、触发器、视图,所以调用这些功能的不可以进行索引下推优化</li><li>对于 InnoDB 引擎只适用于二级索引,InnoDB 的聚簇索引会将整行数据读到缓冲区,不再需要去回表查询了,索引下推的目的减少回表的 IO 次数也就失去了意义</li></ul><p>工作过程:用户表 user,(name, age) 是联合索引</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM user WHERE name LIKE '张%' AND age = 10;-- 头部模糊匹配会造成索引失效</span><br></pre></td></tr></table></figure><ul><li><p>优化前:在非主键索引树上找到满足第一个条件的行,然后通过叶子节点记录的主键值再回到主键索引树上查找到对应的行数据,再对比 AND 后的条件是否符合,符合返回数据,需要 4 次回表</p><p><img src="../image/post/MySQL-%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8%E4%BC%98%E5%8C%961.png" alt=""></p></li><li><p>优化后:检查索引中存储的列信息是否符合索引条件,然后交由存储引擎用剩余的判断条件判断此行数据是否符合要求,<strong>不满足条件的不去读取表中的数据</strong>,满足下推条件的就根据主键值进行回表查询,2 次回表<br><img src="../image/post/MySQL-%E7%B4%A2%E5%BC%95%E4%B8%8B%E6%8E%A8%E4%BC%98%E5%8C%962.png" alt=""></p></li></ul><p>当使用 EXPLAIN 进行分析时,如果使用了索引条件下推,Extra 会显示 Using index condition</p><p>参考文章:<a href="https://blog.csdn.net/sinat_29774479/article/details/103470244">https://blog.csdn.net/sinat_29774479/article/details/103470244</a></p><p>参考文章:<a href="https://time.geekbang.org/column/article/69636">https://time.geekbang.org/column/article/69636</a></p><hr><h4 id="前缀索引">前缀索引</h4><p>当要索引的列字符很多时,索引会变大变慢,可以只索引列开始的部分字符串,节约索引空间,提高索引效率</p><p>注意:使用前缀索引就系统就忽略覆盖索引对查询性能的优化了</p><p>优化原则:<strong>降低重复的索引值</strong></p><p>比如地区表:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">areagdpcode</span><br><span class="line">chinaShanghai100aaa</span><br><span class="line">chinaDalian200bbb</span><br><span class="line">usaNewYork300ccc</span><br><span class="line">chinaFuxin400ddd</span><br><span class="line">chinaBeijing500eee</span><br></pre></td></tr></table></figure><p>发现 area 字段很多都是以 china 开头的,那么如果以前 1-5 位字符做前缀索引就会出现大量索引值重复的情况,索引值重复性越低,查询效率也就越高,所以需要建立前 6 位字符的索引:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CREATE INDEX idx_area ON table_name(area(7));</span><br></pre></td></tr></table></figure><p>场景:存储身份证</p><ul><li>直接创建完整索引,这样可能比较占用空间</li><li>创建前缀索引,节省空间,但会增加查询扫描次数,并且不能使用覆盖索引</li><li>倒序存储,再创建前缀索引,用于绕过字符串本身前缀的区分度不够的问题(前 6 位相同的很多)</li><li>创建 hash 字段索引,查询性能稳定,有额外的存储和计算消耗,跟第三种方式一样,都不支持范围扫描</li></ul><hr><h4 id="索引合并">索引合并</h4><p>使用多个索引来完成一次查询的执行方法叫做索引合并 index merge</p><ul><li><p>Intersection 索引合并:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> table_test <span class="keyword">WHERE</span> key1 <span class="operator">=</span> <span class="string">'a'</span> <span class="keyword">AND</span> key3 <span class="operator">=</span> <span class="string">'b'</span>; # key1 和 key3 列都是单列索引、二级索引</span><br></pre></td></tr></table></figure><p>从不同索引中扫描到的记录的 id 值取<strong>交集</strong>(相同 id),然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序</p></li><li><p>Union 索引合并:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> table_test <span class="keyword">WHERE</span> key1 <span class="operator">=</span> <span class="string">'a'</span> <span class="keyword">OR</span> key3 <span class="operator">=</span> <span class="string">'b'</span>;</span><br></pre></td></tr></table></figure><p>从不同索引中扫描到的记录的 id 值取<strong>并集</strong>,然后执行回表操作,要求从每个二级索引获取到的记录都是按照主键值排序</p></li><li><p>Sort-Union 索引合并</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SELECT</span> <span class="operator">*</span> <span class="keyword">FROM</span> table_test <span class="keyword">WHERE</span> key1 <span class="operator"><</span> <span class="string">'a'</span> <span class="keyword">OR</span> key3 <span class="operator">></span> <span class="string">'b'</span>;</span><br></pre></td></tr></table></figure><p>先将从不同索引中扫描到的记录的主键值进行排序,再按照 Union 索引合并的方式进行查询</p></li></ul><p>索引合并算法的效率并不好,通过将其中的一个索引改成联合索引会优化效率</p><h3 id="索引优化">索引优化</h3><h4 id="创建索引">创建索引</h4><p>索引是数据库优化最重要的手段之一,通过索引通常可以帮助用户解决大多数的 MySQL 的性能优化问题</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE `tb_seller` (</span><br><span class="line">`sellerid` varchar (100),</span><br><span class="line">`name` varchar (100),</span><br><span class="line">`nickname` varchar (50),</span><br><span class="line">`password` varchar (60),</span><br><span class="line">`status` varchar (1),</span><br><span class="line">`address` varchar (100),</span><br><span class="line">`createtime` datetime,</span><br><span class="line"> PRIMARY KEY(`sellerid`)</span><br><span class="line">)ENGINE=INNODB DEFAULT CHARSET=utf8mb4;</span><br><span class="line">INSERT INTO `tb_seller` (`sellerid`, `name`, `nickname`, `password`, `status`, `address`, `createtime`) values('xiaomi','小米科技','小米官方旗舰店','e10adc3949ba59abbe56e057f20f883e','1','西安市','2088-01-01 12:00:00');</span><br><span class="line">CREATE INDEX idx_seller_name_sta_addr ON tb_seller(name, status, address); # 联合索引</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%95%E7%8E%AF%E5%A2%83%E5%87%86%E5%A4%87.png" alt=""></p><hr><h4 id="避免失效">避免失效</h4><h5 id="语句错误">语句错误</h5><ul><li><p>全值匹配:对索引中所有列都指定具体值,这种情况索引生效,执行效率高</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1' AND address='西安市';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%951.png" alt=""></p></li><li><p><strong>最左前缀法则</strong>:联合索引遵守最左前缀法则</p><p>匹配最左前缀法则,走索引:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技';</span><br><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status='1';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%952.png" alt=""></p><p>违法最左前缀法则 , 索引失效:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE status='1';</span><br><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE status='1' AND address='西安市';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%953.png" alt=""></p><p>如果符合最左法则,但是出现跳跃某一列,只有最左列索引生效:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND address='西安市';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%954.png" alt=""></p><p>虽然索引列失效,但是系统会<strong>使用了索引下推进行了优化</strong></p></li><li><p><strong>范围查询</strong>右边的列,不能使用索引:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status>'1' AND address='西安市';</span><br></pre></td></tr></table></figure><p>根据前面的两个字段 name , status 查询是走索引的, 但是最后一个条件 address 没有用到索引,使用了索引下推</p><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%955.png" alt=""></p></li><li><p>在索引列上<strong>函数或者运算(+ - 数值)操作</strong>, 索引将失效:会破坏索引值的有序性</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE SUBSTRING(name,3,2) = '科技';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%956.png" alt=""></p></li><li><p><strong>字符串不加单引号</strong>,造成索引失效:隐式类型转换,当字符串和数字比较时会<strong>把字符串转化为数字</strong></p><p>在查询时,没有对字符串加单引号,查询优化器会调用 CAST 函数将 status 转换为 int 进行比较,造成索引失效</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' AND status = 1;</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%957.png" alt=""></p><p>如果 status 是 int 类型,SQL 为 <code>SELECT * FROM tb_seller WHERE status = '1' </code> 并不会造成索引失效,因为会将 <code>'1'</code> 转换为 <code>1</code>,并<strong>不会对索引列产生操作</strong></p></li><li><p>多表连接查询时,如果两张表的<strong>字符集不同</strong>,会造成索引失效,因为会进行类型转换</p><p>解决方法:CONVERT 函数是加在输入参数上、修改表的字符集</p></li><li><p><strong>用 OR 分割条件,索引失效</strong>,导致全表查询:</p><p>OR 前的条件中的列有索引而后面的列中没有索引或 OR 前后两个列是同一个复合索引,都造成索引失效</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' OR createtime = '2088-01-01 12:00:00';</span><br><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='小米科技' OR status='1';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9510.png" alt=""></p><p><strong>AND 分割的条件不影响</strong>:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name='阿里巴巴' AND createtime = '2088-01-01 12:00:00';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9511.png" alt=""></p></li><li><p><strong>以 % 开头的 LIKE 模糊查询</strong>,索引失效:</p><p>如果是尾部模糊匹配,索引不会失效;如果是头部模糊匹配,索引失效</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name like '%科技%';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9512.png" alt=""></p><p>解决方案:通过覆盖索引来解决</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT sellerid,name,status FROM tb_seller WHERE name like '%科技%';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9513.png" alt=""></p><p>原因:在覆盖索引的这棵 B+ 数上只需要进行 like 的匹配,或者是基于覆盖索引查询再进行 WHERE 的判断就可以获得结果</p></li></ul><hr><h5 id="系统优化">系统优化</h5><p>系统优化为全表扫描:</p><ul><li><p>如果 MySQL 评估使用索引比全表更慢,则不使用索引,索引失效:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">CREATE INDEX idx_address ON tb_seller(address);</span><br><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE address='西安市';</span><br><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE address='北京市';</span><br></pre></td></tr></table></figure><p>北京市的键值占 9/10(区分度低),所以优化为全表扫描,type = ALL</p><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9514.png" alt=""></p></li><li><p>IS NULL、IS NOT NULL <strong>有时</strong>索引失效:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name IS NULL;</span><br><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE name IS NOT NULL;</span><br></pre></td></tr></table></figure><p>NOT NULL 失效的原因是 name 列全部不是 null,优化为全表扫描,当 NULL 过多时,IS NULL 失效</p><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E4%BD%BF%E7%94%A8%E7%B4%A2%E5%BC%9515.png" alt=""></p></li><li><p>IN 肯定会走索引,但是当 IN 的取值范围较大时会导致索引失效,走全表扫描:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE sellerId IN ('alibaba','huawei');-- 都走索引</span><br><span class="line">EXPLAIN SELECT * FROM tb_seller WHERE sellerId NOT IN ('alibaba','huawei');</span><br></pre></td></tr></table></figure></li><li><p><a href="https://time.geekbang.org/column/article/74687">MySQL 实战 45 讲</a>该章节最后提出了一种场景,获取到数据以后 Server 层还会做判断</p></li></ul><hr><h4 id="底层原理">底层原理</h4><p>索引失效一般是针对联合索引,联合索引一般由几个字段组成,排序方式是先按照第一个字段进行排序,然后排序第二个,依此类推,图示(a, b)索引,<strong>a 相等的情况下 b 是有序的</strong></p><img src="../image/post/MySQL-%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%861.png" style="zoom:67%;" /><ul><li><p>最左前缀法则:当不匹配前面的字段的时候,后面的字段都是无序的。这种无序不仅体现在叶子节点,也会<strong>导致查询时扫描的非叶子节点也是无序的</strong>,因为索引树相当于忽略的第一个字段,就无法使用二分查找</p></li><li><p>范围查询右边的列,不能使用索引,比如语句: <code>WHERE a > 1 AND b = 1 </code>,在 a 大于 1 的时候,b 是无序的,a > 1 是扫描时有序的,但是找到以后进行寻找 b 时,索引树就不是有序的了</p> <img src="../image/post/MySQL-%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%862.png" style="zoom:67%;" /></li><li><p>以 % 开头的 LIKE 模糊查询,索引失效,比如语句:<code>WHERE a LIKE '%d'</code>,前面的不确定,导致不符合最左匹配,直接去索引中搜索以 d 结尾的节点,所以没有顺序<br><img src="../image/post/MySQL-%E7%B4%A2%E5%BC%95%E5%A4%B1%E6%95%88%E5%BA%95%E5%B1%82%E5%8E%9F%E7%90%863.png" alt=""></p></li></ul><p>参考文章:<a href="https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ">https://mp.weixin.qq.com/s/B_M09dzLe9w7cT46rdGIeQ</a></p><hr><h4 id="查看索引">查看索引</h4><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SHOW STATUS LIKE 'Handler_read%';</span><br><span class="line">SHOW GLOBAL STATUS LIKE 'Handler_read%';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E4%BC%98%E5%8C%96SQL%E6%9F%A5%E7%9C%8B%E7%B4%A2%E5%BC%95%E4%BD%BF%E7%94%A8%E6%83%85%E5%86%B5.png" alt=""></p><ul><li>Handler_read_first:索引中第一条被读的次数,如果较高,表示服务器正执行大量全索引扫描(这个值越低越好)</li><li>Handler_read_key:如果索引正在工作,这个值代表一个行被索引值读的次数,值越低表示索引不经常使用(这个值越高越好)</li><li>Handler_read_next:按照键顺序读下一行的请求数,如果范围约束或执行索引扫描来查询索引列,值增加</li><li>Handler_read_prev:按照键顺序读前一行的请求数,该读方法主要用于优化 ORDER BY … DESC</li><li>Handler_read_rnd:根据固定位置读一行的请求数,如果执行大量查询并对结果进行排序则该值较高,可能是使用了大量需要 MySQL 扫描整个表的查询或连接,这个值较高意味着运行效率低,应该建立索引来解决</li><li>Handler_read_rnd_next:在数据文件中读下一行的请求数,如果正进行大量的表扫描,该值较高,说明表索引不正确或写入的查询没有利用索引</li></ul><hr><h2 id="事务机制">事务机制</h2><h3 id="基本介绍-3">基本介绍</h3><p>事务(Transaction)是访问和更新数据库的程序执行单元;事务中可能包含一个或多个 SQL 语句,这些语句要么都执行,要么都不执行,作为一个关系型数据库,MySQL 支持事务。</p><p>单元中的每条 SQL 语句都相互依赖,形成一个整体</p><ul><li><p>如果某条 SQL 语句执行失败或者出现错误,那么整个单元就会回滚,撤回到事务最初的状态</p></li><li><p>如果单元中所有的 SQL 语句都执行成功,则事务就顺利执行</p></li></ul><p>事务的四大特征:ACID</p><ul><li>原子性 (atomicity)</li><li>一致性 (consistency)</li><li>隔离性 (isolaction)</li><li>持久性 (durability)</li></ul><p>事务的几种状态:</p><ul><li>活动的(active):事务对应的数据库操作正在执行中</li><li>部分提交的(partially committed):事务的最后一个操作执行完,但是内存还没刷新至磁盘</li><li>失败的(failed):当事务处于活动状态或部分提交状态时,如果数据库遇到了错误或刷脏失败,或者用户主动停止当前的事务</li><li>中止的(aborted):失败状态的事务回滚完成后的状态</li><li>提交的(committed):当处于部分提交状态的事务刷脏成功,就处于提交状态</li></ul><hr><h3 id="事务管理">事务管理</h3><h4 id="基本操作">基本操作</h4><p>事务管理的三个步骤</p><ol><li><p>开启事务:记录回滚点,并通知服务器,将要执行一组操作,要么同时成功、要么同时失败</p></li><li><p>执行 SQL 语句:执行具体的一条或多条 SQL 语句</p></li><li><p>结束事务(提交|回滚)</p><ul><li>提交:没出现问题,数据进行更新</li><li>回滚:出现问题,数据恢复到开启事务时的状态</li></ul></li></ol><p>事务操作:</p><ul><li><p>显式开启事务</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">START TRANSACTION [READ ONLY|READ WRITE|WITH CONSISTENT SNAPSHOT]; #可以跟一个或多个状态,最后的是一致性读</span><br><span class="line">BEGIN [WORK];</span><br></pre></td></tr></table></figure><p>说明:不填状态默认是读写事务</p></li><li><p>回滚事务,用来手动中止事务</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ROLLBACK;</span><br></pre></td></tr></table></figure></li><li><p>提交事务,显示执行是手动提交,MySQL 默认为自动提交</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">COMMIT;</span><br></pre></td></tr></table></figure></li><li><p>保存点:在事务的执行过程中设置的还原点,调用 ROLLBACK 时可以指定回滚到哪个点</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">SAVEPOINT point_name;#设置保存点</span><br><span class="line">RELEASE point_name#删除保存点</span><br><span class="line">ROLLBACK [WORK] TO [SAVEPOINT] point_name#回滚至某个保存点,不填默认回滚到事务执行之前的状态</span><br></pre></td></tr></table></figure></li><li><p>操作演示</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">-- 开启事务</span><br><span class="line">START TRANSACTION;</span><br><span class="line"></span><br><span class="line">-- 张三给李四转账500元</span><br><span class="line">-- 1.张三账户-500</span><br><span class="line">UPDATE account SET money=money-500 WHERE NAME='张三';</span><br><span class="line">-- 2.李四账户+500</span><br><span class="line">UPDATE account SET money=money+500 WHERE NAME='李四';</span><br><span class="line"></span><br><span class="line">-- 回滚事务(出现问题)</span><br><span class="line">ROLLBACK;</span><br><span class="line"></span><br><span class="line">-- 提交事务(没出现问题)</span><br><span class="line">COMMIT;</span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="提交方式">提交方式</h4><p>提交方式的相关语法:</p><ul><li><p>查看事务提交方式</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT @@AUTOCOMMIT; -- 会话,1 代表自动提交 0 代表手动提交</span><br><span class="line">SELECT @@GLOBAL.AUTOCOMMIT;-- 系统</span><br></pre></td></tr></table></figure></li><li><p>修改事务提交方式</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SET @@AUTOCOMMIT=数字;-- 系统</span><br><span class="line">SET AUTOCOMMIT=数字;-- 会话</span><br></pre></td></tr></table></figure></li><li><p><strong>系统变量的操作</strong>:</p> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SET</span> [<span class="keyword">GLOBAL</span><span class="operator">|</span>SESSION] 变量名 <span class="operator">=</span> 值;<span class="comment">-- 默认是会话</span></span><br><span class="line"><span class="keyword">SET</span> @@[(<span class="keyword">GLOBAL</span><span class="operator">|</span>SESSION).]变量名 <span class="operator">=</span> 值;<span class="comment">-- 默认是系统</span></span><br></pre></td></tr></table></figure> <figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">SHOW</span> [<span class="keyword">GLOBAL</span><span class="operator">|</span>SESSION] VARIABLES [<span class="keyword">LIKE</span> <span class="string">'变量%'</span>]; <span class="comment">-- 默认查看会话内系统变量值</span></span><br></pre></td></tr></table></figure></li></ul><p>工作原理:</p><ul><li>自动提交:如果没有 START TRANSACTION 显式地开始一个事务,那么<strong>每条 SQL 语句都会被当做一个事务执行提交操作</strong>;显式开启事务后,会在本次事务结束(提交或回滚)前暂时关闭自动提交</li><li>手动提交:不需要显式的开启事务,所有的 SQL 语句都在一个事务中,直到执行了提交或回滚,然后进入下一个事务</li><li>隐式提交:存在一些特殊的命令,在事务中执行了这些命令会马上<strong>强制执行 COMMIT 提交事务</strong><ul><li><strong>DDL 语句</strong> (CREATE/DROP/ALTER)、LOCK TABLES 语句、LOAD DATA 导入数据语句、主从复制语句等</li><li>当一个事务还没提交或回滚,显式的开启一个事务会隐式的提交上一个事务</li></ul></li></ul><hr><h4 id="事务-ID">事务 ID</h4><p>事务在执行过程中对某个表执行了<strong>增删改操作或者创建表</strong>,就会为当前事务分配一个独一无二的事务 ID(对临时表并不会分配 ID),如果当前事务没有被分配 ID,默认是 0</p><p>说明:只读事务不能对普通的表进行增删改操作,但是可以对临时表增删改,读写事务可以对数据表执行增删改查操作</p><p>事务 ID 本质上就是一个数字,服务器在内存中维护一个全局变量:</p><ul><li>每当需要为某个事务分配 ID,就会把全局变量的值赋值给事务 ID,然后变量自增 1</li><li>每当变量值为 256 的倍数时,就将该变量的值刷新到系统表空间的 Max Trx ID 属性中,该属性占 8 字节</li><li>系统再次启动后,会读取表空间的 Max Trx ID 属性到内存,加上 256 后赋值给全局变量,因为关机时的事务 ID 可能并不是 256 的倍数,会比 Max Trx ID 大,所以需要加上 256 保持事务 ID 是一个<strong>递增的数字</strong></li></ul><p><strong>聚簇索引</strong>的行记录除了完整的数据,还会自动添加 trx_id、roll_pointer 隐藏列,如果表中没有主键并且没有非空唯一索引,也会添加一个 row_id 的隐藏列作为聚簇索引</p><hr><h3 id="隔离级别">隔离级别</h3><h4 id="四种级别">四种级别</h4><p>事务的隔离级别:多个客户端操作时,各个客户端的事务之间应该是隔离的,<strong>不同的事务之间不该互相影响</strong>,而如果多个事务操作同一批数据时,则需要设置不同的隔离级别,否则就会产生问题。</p><p>隔离级别分类:</p><table><thead><tr><th>隔离级别</th><th>名称</th><th>会引发的问题</th><th>数据库默认隔离级别</th></tr></thead><tbody><tr><td>Read Uncommitted</td><td>读未提交</td><td>脏读、不可重复读、幻读</td><td></td></tr><tr><td>Read Committed</td><td>读已提交</td><td>不可重复读、幻读</td><td>Oracle / SQL Server</td></tr><tr><td>Repeatable Read</td><td>可重复读</td><td>幻读</td><td>MySQL</td></tr><tr><td>Serializable</td><td>可串行化</td><td>无</td><td></td></tr></tbody></table><p>一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差</p><ul><li><p>脏写 (Dirty Write):当两个或多个事务选择同一行,最初的事务修改的值被后面事务修改的值覆盖,所有的隔离级别都可以避免脏写(又叫丢失更新),因为有行锁</p></li><li><p>脏读 (Dirty Reads):在一个事务处理过程中读取了另一个<strong>未提交</strong>的事务中修改过的数据</p></li><li><p>不可重复读 (Non-Repeatable Reads):在一个事务处理过程中读取了另一个事务中修改并<strong>已提交</strong>的数据</p><blockquote><p>可重复读的意思是不管读几次,结果都一样,可以重复的读,可以理解为快照读,要读的数据集不会发生变化</p></blockquote></li><li><p>幻读 (Phantom Reads):在事务中按某个条件先后两次查询数据库,后一次查询查到了前一次查询没有查到的行,<strong>数据条目</strong>发生了变化。比如查询某数据不存在,准备插入此记录,但执行插入时发现此记录已存在,无法插入</p></li></ul><p>隔离级别操作语法:</p><ul><li><p>查询数据库隔离级别</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT @@TX_ISOLATION;-- 会话</span><br><span class="line">SELECT @@GLOBAL.TX_ISOLATION;-- 系统</span><br></pre></td></tr></table></figure></li><li><p>修改数据库隔离级别</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET GLOBAL TRANSACTION ISOLATION LEVEL 级别字符串;</span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="加锁分析">加锁分析</h4><p>InnoDB 存储引擎支持事务,所以加锁分析是基于该存储引擎</p><ul><li><p>Read Uncommitted 级别,任何操作都不会加锁</p></li><li><p>Read Committed 级别,增删改操作会加写锁(行锁),读操作不加锁</p><p>在 Server 层过滤条件时发现不满足的记录会调用 unlock_row 方法释放该记录的行锁,保证最后只有满足条件的记录加锁,但是扫表过程中每条记录的<strong>加锁操作不能省略</strong>。所以对数据量很大的表做批量修改时,如果无法使用相应的索引(全表扫描),在Server 过滤数据时就会特别慢,出现虽然没有修改某些行的数据,但是还是被锁住了的现象(锁表),这种情况同样适用于 RR</p></li><li><p>Repeatable Read 级别,增删改操作会加写锁,读操作不加锁。因为读写锁不兼容,<strong>加了读锁后其他事务就无法修改数据</strong>,影响了并发性能,为了保证隔离性和并发性,MySQL 通过 MVCC 解决了读写冲突。RR 级别下的锁有很多种,锁机制章节详解</p></li><li><p>Serializable 级别,读加共享锁,写加排他锁,读写互斥,使用的悲观锁的理论,实现简单,数据更加安全,但是并发能力非常差</p><ul><li>串行化:让所有事务按顺序单独执行,写操作会加写锁,读操作会加读锁</li><li>可串行化:让所有操作相同数据的事务顺序执行,通过加锁实现</li></ul></li></ul><p>参考文章:<a href="https://tech.meituan.com/2014/08/20/innodb-lock.html">https://tech.meituan.com/2014/08/20/innodb-lock.html</a></p><hr><h3 id="原子特性">原子特性</h3><h4 id="实现方式">实现方式</h4><p>原子性是指事务是一个不可分割的工作单位,事务的操作如果成功就必须要完全应用到数据库,失败则不能对数据库有任何影响。比如事务中一个 SQL 语句执行失败,则已执行的语句也必须回滚,数据库退回到事务前的状态</p><p>InnoDB 存储引擎提供了两种事务日志:redo log(重做日志)和 undo log(回滚日志)</p><ul><li>redo log 用于保证事务持久性</li><li>undo log 用于保证事务原子性和隔离性</li></ul><p>undo log 属于逻辑日志,根据每行操作进行记录,记录了 SQL 执行相关的信息,用来回滚行记录到某个版本</p><p>当事务对数据库进行修改时,InnoDB 会先记录对应的 undo log,如果事务执行失败或调用了 rollback 导致事务回滚,InnoDB 会根据 undo log 的内容<strong>做与之前相反的操作</strong>:</p><ul><li><p>对于每个 insert,回滚时会执行 delete</p></li><li><p>对于每个 delete,回滚时会执行 insert</p></li><li><p>对于每个 update,回滚时会执行一个相反的 update,把数据修改回去</p></li></ul><p>参考文章:<a href="https://www.cnblogs.com/kismetv/p/10331633.html">https://www.cnblogs.com/kismetv/p/10331633.html</a></p><hr><h4 id="DML-解析">DML 解析</h4><h5 id="INSERT">INSERT</h5><p>乐观插入:当前数据页的剩余空间充足,直接将数据进行插入</p><p>悲观插入:当前数据页的剩余空间不足,需要进行页分裂,申请一个新的页面来插入数据,会造成更多的 redo log,undo log 影响不大</p><p>当向某个表插入一条记录,实际上需要向聚簇索引和所有二级索引都插入一条记录,但是 undo log <strong>只针对聚簇索引记录</strong>,在回滚时会根据聚簇索引去所有的二级索引进行回滚操作</p><p>roll_pointer 是一个指针,<strong>指向记录对应的 undo log 日志</strong>,一条记录就是一个数据行,行格式中的 roll_pointer 就指向 undo log</p><hr><h5 id="DELETE">DELETE</h5><p>插入到页面中的记录会根据 next_record 属性组成一个单向链表,这个链表称为正常链表,被删除的记录也会通过 next_record 组成一个垃圾链表,该链表中所占用的存储空间可以被重新利用,并不会直接清除数据</p><p>在页面 Page Header 中,PAGE_FREE 属性指向垃圾链表的头节点,删除的工作过程:</p><ul><li><p>将要删除的记录的 delete_flag 位置为 1,其他不做修改,这个过程叫 <strong>delete mark</strong></p></li><li><p>在事务提交前,delete_flag = 1 的记录一直都会处于中间状态</p></li><li><p>事务提交后,有专门的线程将 delete_flag = 1 的记录从正常链表移除并加入垃圾链表,这个过程叫 <strong>purge</strong></p><p>purge 线程在执行删除操作时会创建一个 ReadView,根据事务的可见性移除数据(隔离特性部分详解)</p></li></ul><p>当有新插入的记录时,首先判断 PAGE_FREE 指向的头节点是否足够容纳新纪录:</p><ul><li>如果可以容纳新纪录,就会直接重用已删除的记录的存储空间,然后让 PAGE_FREE 指向垃圾链表的下一个节点</li><li>如果不能容纳新纪录,就直接向页面申请新的空间存储,并不会遍历垃圾链表</li></ul><p>重用已删除的记录空间,可能会造成空间碎片,当数据页容纳不了一条记录时,会判断将碎片空间加起来是否可以容纳,判断为真就会重新组织页内的记录:</p><ul><li>开辟一个临时页面,将页内记录一次插入到临时页面,此时临时页面时没有碎片的</li><li>把临时页面的内容复制到本页,这样就解放出了内存碎片,但是会耗费很大的性能资源</li></ul><hr><h5 id="UPDATE">UPDATE</h5><p>执行 UPDATE 语句,对于更新主键和不更新主键有两种不同的处理方式</p><p>不更新主键的情况:</p><ul><li><p>就地更新(in-place update),如果更新后的列和更新前的列占用的存储空间一样大,就可以直接在原记录上修改</p></li><li><p>先删除旧纪录,再插入新纪录,这里的删除不是 delete mark,而是直接将记录加入垃圾链表,并且修改页面的相应的控制信息,执行删除的线程不是 purge,是执行更新的用户线程,插入新记录时可能造成页空间不足,从而导致页分裂</p></li></ul><p>更新主键的情况:</p><ul><li>将旧纪录进行 delete mark,在更新语句提交后由 purge 线程移入垃圾链表</li><li>根据更新的各列的值创建一条新纪录,插入到聚簇索引中</li></ul><p>在对一条记录修改前会<strong>将记录的隐藏列 trx_id 和 roll_pointer 的旧值记录到 undo log 对应的属性中</strong>,这样当前记录的 roll_pointer 指向当前 undo log 记录,当前 undo log 记录的 roll_pointer 指向旧的 undo log 记录,<strong>形成一个版本链</strong></p><p>UPDATE、DELETE 操作产生的 undo 日志可能会用于其他事务的 MVCC 操作,所以不能立即删除</p><hr><h4 id="回滚日志">回滚日志</h4><p>undo log 是采用段的方式来记录,Rollback Segement 称为回滚段,本质上就是一个类型是 Rollback Segement Header 的页面</p><p>每个回滚段中有 1024 个 undo slot,每个 slot 存放 undo 链表页面的头节点页号,每个链表对应一个叫 undo log segment 的段</p><ul><li>在以前老版本,只支持 1 个 Rollback Segement,只能记录 1024 个 undo log segment</li><li>MySQL5.5 开始支持 128 个 Rollback Segement,支持 128*1024 个 undo 操作</li></ul><p>工作流程:</p><ul><li><p>事务执行前需要到系统表空间第 5 号页面中分配一个回滚段(页),获取一个 Rollback Segement Header 页面的地址</p></li><li><p>回滚段页面有 1024 个 undo slot,首先去回滚段的两个 cached 链表获取缓存的 slot,缓存中没有就在回滚段页面中找一个可用的 undo slot 分配给当前事务</p></li><li><p>如果是缓存中获取的 slot,则该 slot 对应的 undo log segment 已经分配了,需要重新分配,然后从 undo log segment 中申请一个页面作为日志链表的头节点,并填入对应的 slot 中</p></li><li><p>每个事务 undo 日志在记录的时候<strong>占用两个 undo 页面的组成链表</strong>,分别为 insert undo 链表和 update undo 链表,链表的头节点页面为 first undo page 会包含一些管理信息,其他页面为 normal undo page</p><p>说明:事务执行过程的临时表也需要两个 undo 链表,不和普通表共用,这些链表并不是事务开始就分配,而是按需分配</p></li></ul><hr><h3 id="隔离特性">隔离特性</h3><h4 id="实现方式-2">实现方式</h4><p>隔离性是指,事务内部的操作与其他事务是隔离的,多个并发事务之间要相互隔离,不能互相干扰</p><ul><li><p>严格的隔离性,对应了事务隔离级别中的 serializable,实际应用中对性能考虑很少使用可串行化</p></li><li><p>与原子性、持久性侧重于研究事务本身不同,隔离性研究的是<strong>不同事务</strong>之间的相互影响</p></li></ul><p>隔离性让并发情形下的事务之间互不干扰:</p><ul><li>一个事务的写操作对另一个事务的写操作(写写):锁机制保证隔离性</li><li>一个事务的写操作对另一个事务的读操作(读写):MVCC 保证隔离性</li></ul><p>锁机制:事务在修改数据之前,需要先获得相应的锁,获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁(详解见锁机制)</p><hr><h4 id="并发控制">并发控制</h4><p>MVCC 全称 Multi-Version Concurrency Control,即多版本并发控制,用来<strong>解决读写冲突的无锁并发控制</strong>,可以在发生读写请求冲突时不用加锁解决,这个读是指的快照读(也叫一致性读或一致性无锁读),而不是当前读:</p><ul><li>快照读:实现基于 MVCC,因为是多版本并发,所以快照读读到的数据不一定是当前最新的数据,有可能是历史版本的数据</li><li>当前读:又叫加锁读,读取数据库记录是当前<strong>最新的版本</strong>(产生幻读、不可重复读),可以对读取的数据进行加锁,防止其他事务修改数据,是悲观锁的一种操作,读写操作加共享锁或者排他锁和串行化事务的隔离级别都是当前读</li></ul><p>数据库并发场景:</p><ul><li><p>读-读:不存在任何问题,也不需要并发控制</p></li><li><p>读-写:有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读</p></li><li><p>写-写:有线程安全问题,可能会存在脏写(丢失更新)问题</p></li></ul><p>MVCC 的优点:</p><ul><li>在并发读写数据库时,做到在读操作时不用阻塞写操作,写操作也不用阻塞读操作,提高了并发读写的性能</li><li>可以解决脏读,不可重复读等事务隔离问题(加锁也能解决),但不能解决更新丢失问题(写锁会解决)</li></ul><p>提高读写和写写的并发性能:</p><ul><li>MVCC + 悲观锁:MVCC 解决读写冲突,悲观锁解决写写冲突</li><li>MVCC + 乐观锁:MVCC 解决读写冲突,乐观锁解决写写冲突</li></ul><p>参考文章:<a href="https://www.jianshu.com/p/8845ddca3b23">https://www.jianshu.com/p/8845ddca3b23</a></p><hr><h4 id="实现原理">实现原理</h4><h5 id="隐藏字段">隐藏字段</h5><p>实现原理主要是隐藏字段,undo日志,Read View 来实现的</p><p>InnoDB 存储引擎,数据库中的<strong>聚簇索引</strong>每行数据,除了自定义的字段,还有数据库隐式定义的字段:</p><ul><li>DB_TRX_ID:最近修改事务 ID,记录创建该数据或最后一次修改该数据的事务 ID</li><li>DB_ROLL_PTR:回滚指针,<strong>指向记录对应的 undo log 日志</strong>,undo log 中又指向上一个旧版本的 undo log</li><li>DB_ROW_ID:隐含的自增 ID(<strong>隐藏主键</strong>),如果数据表没有主键,InnoDB 会自动以 DB_ROW_ID 作为聚簇索引</li></ul><p><img src="../image/post/MySQL-MVCC%E7%89%88%E6%9C%AC%E9%93%BE%E9%9A%90%E8%97%8F%E5%AD%97%E6%AE%B5.png" alt=""></p><hr><h5 id="版本链">版本链</h5><p>undo log 是逻辑日志,记录的是每个事务对数据执行的操作,而不是记录的全部数据,要<strong>根据 undo log 逆推出以往事务的数据</strong></p><p>undo log 的作用:</p><ul><li>保证事务进行 rollback 时的原子性和一致性,当事务进行回滚的时候可以用 undo log 的数据进行恢复</li><li>用于 MVCC 快照读,通过读取 undo log 的历史版本数据可以实现不同事务版本号都拥有自己独立的快照数据版本</li></ul><p>undo log 主要分为两种:</p><ul><li><p>insert undo log:事务在 insert 新记录时产生的 undo log,只在事务回滚时需要,并且在事务提交后可以被立即丢弃</p></li><li><p>update undo log:事务在进行 update 或 delete 时产生的 undo log,在事务回滚时需要,在快照读时也需要。不能随意删除,只有在当前读或事务回滚不涉及该日志时,对应的日志才会被 purge 线程统一清除</p></li></ul><p>每次对数据库记录进行改动,都会产生的新版本的 undo log,随着更新次数的增多,所有的版本都会被 roll_pointer 属性连接成一个链表,把这个链表称之为<strong>版本链</strong>,版本链的头节点就是当前的最新的 undo log,链尾就是最早的旧 undo log</p><p>说明:因为 DELETE 删除记录,都是移动到垃圾链表中,不是真正的删除,所以才可以通过版本链访问原始数据</p><img src="../image/post/MySQL-MVCC%E7%89%88%E6%9C%AC%E9%93%BE.png" style="zoom: 80%;" /><p>注意:undo 是逻辑日志,这里只是直观的展示出来</p><p>工作流程:</p><ul><li>有个事务插入 persion 表一条新记录,name 为 Jerry,age 为 24</li><li>事务 1 修改该行数据时,数据库会先对该行加排他锁,然后先记录 undo log,然后修改该行 name 为 Tom,并且修改隐藏字段的事务 ID 为当前事务 1 的 ID(默认为 1 之后递增),回滚指针指向拷贝到 undo log 的副本记录,事务提交后,释放锁</li><li>以此类推</li></ul><hr><h5 id="读视图">读视图</h5><p>Read View 是事务进行读数据操作时产生的读视图,该事务执行快照读的那一刻会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的 ID,用来做可见性判断,根据视图判断当前事务能够看到哪个版本的数据</p><p>注意:这里的快照并不是把所有的数据拷贝一份副本,而是由 undo log 记录的逻辑日志,根据库中的数据进行计算出历史数据</p><p>工作流程:将版本链的头节点的事务 ID(最新数据事务 ID,大概率不是当前线程)DB_TRX_ID 取出来,与系统当前活跃事务的 ID 对比进行可见性分析,不可见就通过 DB_ROLL_PTR 回滚指针去取出 undo log 中的下一个 DB_TRX_ID 比较,直到找到最近的满足可见性的 DB_TRX_ID,该事务 ID 所在的旧记录就是当前事务能看见的最新的记录</p><p>Read View 几个属性:</p><ul><li>m_ids:生成 Read View 时当前系统中活跃的事务 id 列表(未提交的事务集合,当前事务也在其中)</li><li>min_trx_id:生成 Read View 时当前系统中活跃的最小的事务 id,也就是 m_ids 中的最小值(已提交的事务集合)</li><li>max_trx_id:生成 Read View 时当前系统应该分配给下一个事务的 id 值,m_ids 中的最大值加 1(未开始事务)</li><li>creator_trx_id:生成该 Read View 的事务的事务 id,就是判断该 id 的事务能读到什么数据</li></ul><p>creator 创建一个 Read View,进行可见性算法分析:(解决了读未提交)</p><ul><li><p>db_trx_id == creator_trx_id:表示这个数据就是当前事务自己生成的,自己生成的数据自己肯定能看见,所以此数据对 creator 是可见的</p></li><li><p>db_trx_id < min_trx_id:该版本对应的事务 ID 小于 Read view 中的最小活跃事务 ID,则这个事务在当前事务之前就已经被提交了,对 creator 可见(因为比已提交的最大事务 ID 小的并不一定已经提交,所以应该先判断是否在活跃事务列表)</p></li><li><p>db_trx_id >= max_trx_id:该版本对应的事务 ID 大于 Read view 中当前系统的最大事务 ID,则说明该数据是在当前 Read view 创建之后才产生的,对 creator 不可见</p></li><li><p>min_trx_id<= db_trx_id < max_trx_id:判断 db_trx_id 是否在活跃事务列表 m_ids 中</p><ul><li>在列表中,说明该版本对应的事务正在运行,数据不能显示(<strong>不能读到未提交的数据</strong>)</li><li>不在列表中,说明该版本对应的事务已经被提交,数据可以显示(<strong>可以读到已经提交的数据</strong>)</li></ul></li></ul><hr><h5 id="工作流程-2">工作流程</h5><p>表 user 数据</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">id</span>nameage</span><br><span class="line">1张三 18</span><br></pre></td></tr></table></figure><p>Transaction 20:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">START TRANSACTION;-- 开启事务</span><br><span class="line">UPDATE user SET name = '李四' WHERE id = 1;</span><br><span class="line">UPDATE user SET name = '王五' WHERE id = 1;</span><br></pre></td></tr></table></figure><p>Transaction 60:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">START TRANSACTION;-- 开启事务</span><br><span class="line">-- 操作表的其他数据</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-MVCC%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B1.png" alt=""></p><p>ID 为 0 的事务创建 Read View:</p><ul><li>m_ids:20、60</li><li>min_trx_id:20</li><li>max_trx_id:61</li><li>creator_trx_id:0</li></ul><p><img src="../image/post/MySQL-MVCC%E5%B7%A5%E4%BD%9C%E6%B5%81%E7%A8%8B2.png" alt=""></p><p>只有红框部分才复合条件,所以只有张三对应的版本的数据可以被看到</p><p>参考视频:<a href="https://www.bilibili.com/video/BV1t5411u7Fg">https://www.bilibili.com/video/BV1t5411u7Fg</a></p><hr><h5 id="二级索引">二级索引</h5><p>只有在聚簇索引中才有 trx_id 和 roll_pointer 的隐藏列,对于二级索引判断可见性的方式:</p><ul><li>二级索引页面的 Page Header 中有一个 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,SELECT 语句访问某个二级索引时会判断 ReadView 的 min_trx_id 是否大于该属性,大于说明该页面的所有属性对 ReadView 可见</li><li>如果属性判断不可见,就需要利用二级索引获取主键值进行<strong>回表操作</strong>,得到聚簇索引后按照聚簇索引的可见性判断的方法操作</li></ul><hr><h4 id="RC-RR">RC RR</h4><p>Read View 用于支持 RC(Read Committed,读已提交)和 RR(Repeatable Read,可重复读)隔离级别的实现,所以 <strong>SELECT 在 RC 和 RR 隔离级别使用 MVCC 读取记录</strong></p><p>RR、RC 生成时机:</p><ul><li>RC 隔离级别下,每次读取数据前都会生成最新的 Read View(当前读)</li><li>RR 隔离级别下,在第一次数据读取时才会创建 Read View(快照读)</li></ul><p>RC、RR 级别下的 InnoDB 快照读区别</p><ul><li><p>RC 级别下,事务中每次快照读都会新生成一个 Read View,这就是在 RC 级别下的事务中可以看到别的事务提交的更新的原因</p></li><li><p>RR 级别下,某个事务的对某条记录的<strong>第一次快照读</strong>会创建一个 Read View, 将当前系统活跃的其他事务记录起来,此后在调用快照读的时候,使用的是同一个 Read View,所以一个事务的查询结果每次都是相同的</p><p>RR 级别下,通过 <code>START TRANSACTION WITH CONSISTENT SNAPSHOT</code> 开启事务,会在执行该语句后立刻生成一个 Read View,不是在执行第一条 SELECT 语句时生成(所以说 <code>START TRANSACTION</code> 并不是事务的起点,执行第一条语句才算起点)</p></li></ul><p>解决幻读问题:</p><ul><li><p>快照读:通过 MVCC 来进行控制的,在可重复读隔离级别下,普通查询是快照读,是不会看到别的事务插入的数据的,但是<strong>并不能完全避免幻读</strong></p><p>场景:RR 级别,T1 事务开启,创建 Read View,此时 T2 去 INSERT 新的一行然后提交,然后 T1 去 UPDATE 该行会发现更新成功,并且把这条新记录的 trx_id 变为当前的事务 id,所以对当前事务就是可见的。因为 <strong>Read View 并不能阻止事务去更新数据,更新数据都是先读后写并且是当前读</strong>,读取到的是最新版本的数据</p></li><li><p>当前读:通过 next-key 锁(行锁 + 间隙锁)来解决问题</p></li></ul><hr><h3 id="持久特性">持久特性</h3><h4 id="实现方式-3">实现方式</h4><p>持久性是指一个事务一旦被提交了,那么对数据库中数据的改变就是永久性的,接下来的其他操作或故障不应该对其有任何影响。</p><p>Buffer Pool 的使用提高了读写数据的效率,但是如果 MySQL 宕机,此时 Buffer Pool 中修改的数据还没有刷新到磁盘,就会导致数据的丢失,事务的持久性无法保证,所以引入了 redo log 日志:</p><ul><li>redo log <strong>记录数据页的物理修改</strong>,而不是某一行或某几行的修改,用来恢复提交后的数据页,只能<strong>恢复到最后一次提交</strong>的位置</li><li>redo log 采用的是 WAL(Write-ahead logging,<strong>预写式日志</strong>),所有修改要先写入日志,再更新到磁盘,保证了数据不会因 MySQL 宕机而丢失,从而满足了持久性要求</li><li>简单的 redo log 是纯粹的物理日志,负责的 redo log 会存在物理日志和逻辑日志</li></ul><p>工作过程:MySQL 发生了宕机,InnoDB 会判断一个数据页在崩溃恢复时丢失了更新,就会将它读到内存,然后根据 redo log 内容更新内存,更新完成后,内存页变成脏页,然后进行刷脏</p><p>缓冲池的<strong>刷脏策略</strong>:</p><ul><li>redo log 文件是固定大小的,如果写满了就要擦除以前的记录,在擦除之前需要把对应的更新持久化到磁盘中</li><li>Buffer Pool 内存不足,需要淘汰部分数据页(LRU 链表尾部),如果淘汰的是脏页,就要先将脏页写到磁盘(要避免大事务)</li><li>系统空闲时,后台线程会自动进行刷脏(Flush 链表部分已经详解)</li><li>MySQL 正常关闭时,会把内存的脏页都刷新到磁盘上</li></ul><hr><h4 id="重做日志">重做日志</h4><h5 id="日志缓冲">日志缓冲</h5><p>服务器启动时会向操作系统申请一片连续内存空间作为 redo log buffer(重做日志缓冲区),可以通过 <code>innodb_log_buffer_size</code> 系统变量指定 redo log buffer 的大小,默认是 16MB</p><p>log buffer 被划分为若干 redo log block(块,类似数据页的概念),每个默认大小 512 字节,每个 block 由 12 字节的 log block head、496 字节的 log block body、4 字节的 log block trailer 组成</p><ul><li>当数据修改时,先修改 Change Buffer 中的数据,然后在 redo log buffer 记录这次操作,写入 log buffer 的过程是<strong>顺序写入</strong>的(先写入前面的 block,写满后继续写下一个)</li><li>log buffer 中有一个指针 buf_free,来标识该位置之前都是填满的 block,该位置之后都是空闲区域(<strong>碰撞指针</strong>)</li></ul><p>MySQL 规定对底层页面的一次原子访问称为一个 Mini-Transaction(MTR),比如在 B+ 树上插入一条数据就算一个 MTR</p><ul><li><p>一个事务包含若干个 MTR,一个 MTR 对应一组若干条 redo log,一组 redo log 是不可分割的,在进行数据恢复时也把一组 redo log 当作一个不可分割的整体处理</p></li><li><p>所以不是每生成一条 redo 日志就将其插入到 log buffer 中,而是一个 MTR 结束后<strong>将一组 redo 日志写入 log buffer</strong></p></li></ul><p>InnoDB 的 redo log 是<strong>固定大小</strong>的,redo 日志在磁盘中以文件组的形式存储,同一组中的每个文件大小一样格式一样,</p><ul><li><code>innodb_log_group_home_dir</code> 代表磁盘存储 redo log 的文件目录,默认是当前数据目录</li><li><code>innodb_log_file_size</code> 代表文件大小,默认 48M,<code>innodb_log_files_in_group</code> 代表文件个数,默认 2 最大 100,所以日志的文件大小为 <code>innodb_log_file_size * innodb_log_files_in_group</code></li></ul><p>redo 日志文件也是由若干个 512 字节的 block 组成,日志文件的前 2048 个字节(前 4 个 block)用来存储一些管理信息,以后的用来存储 log buffer 中的 block 镜像</p><p>注意:block 并不代表一组 redo log,一组日志可能占用不到一个 block 或者几个 block,依赖于 MTR 的大小</p><hr><h5 id="日志刷盘">日志刷盘</h5><p>redo log 需要在事务提交时将日志写入磁盘,但是比将内存中的 Buffer Pool 修改的数据写入磁盘的速度快,原因:</p><ul><li>刷脏是随机 IO,因为每次修改的数据位置随机;redo log 和 binlog 都是<strong>顺序写</strong>,磁盘的顺序 IO 比随机 IO 速度要快</li><li>刷脏是以数据页(Page)为单位的,一个页上的一个小修改都要整页写入;redo log 中只包含真正需要写入的部分,减少无效 IO</li><li><strong>组提交机制</strong>,可以大幅度降低磁盘的 IO 消耗</li></ul><p>InnoDB 引擎会在适当的时候,把内存中 redo log buffer 持久化(fsync)到磁盘,具体的<strong>刷盘策略</strong>:</p><ul><li>在事务提交时需要进行刷盘,通过修改参数 <code>innodb_flush_log_at_trx_commit</code> 设置:<ul><li>0:表示当提交事务时,并不将缓冲区的 redo 日志写入磁盘,而是等待<strong>后台线程每秒刷新一次</strong></li><li>1:在事务提交时将缓冲区的 redo 日志<strong>同步写入</strong>到磁盘,保证一定会写入成功(默认值)</li><li>2:在事务提交时将缓冲区的 redo 日志异步写入到磁盘,不能保证提交时肯定会写入,只是有这个动作。日志已经在操作系统的缓存,如果操作系统没有宕机而 MySQL 宕机,也是可以恢复数据的</li></ul></li><li>写入 redo log buffer 的日志超过了总容量的一半,就会将日志刷入到磁盘文件,这会影响执行效率,所以开发中应<strong>避免大事务</strong></li><li>服务器关闭时</li><li>checkpoint 时(下小节详解)</li><li>并行的事务提交(组提交)时,会将将其他事务的 redo log 持久化到磁盘。假设事务 A 已经写入 redo log buffer 中,这时另外一个线程的事务 B 提交,如果 innodb_flush_log_at_trx_commit 设置的是 1,那么事务 B 要把 redo log buffer 里的日志全部持久化到磁盘,<strong>因为多个事务共用一个 redo log buffer</strong>,所以一次 fsync 可以刷盘多个事务的 redo log,提升了并发量</li></ul><p>服务器启动后 redo 磁盘空间不变,所以 redo 磁盘中的日志文件是被<strong>循环使用</strong>的,采用循环写数据的方式,写完尾部重新写头部,所以要确保头部 log 对应的修改已经持久化到磁盘</p><hr><h5 id="日志序号">日志序号</h5><p>lsn (log sequence number) 代表已经写入的 redo 日志量、flushed_to_disk_lsn 指刷新到磁盘中的 redo 日志量,两者都是<strong>全局变量</strong>,如果两者的值相同,说明 log buffer 中所有的 redo 日志都已经持久化到磁盘</p><p>工作过程:写入 log buffer 数据时,buf_free 会进行偏移,偏移量就会加到 lsn 上</p><p>MTR 的执行过程中修改过的页对应的控制块会加到 Buffer Pool 的 flush 链表中,链表中脏页是按照第一次修改的时间进行排序的(头插),控制块中有两个指针用来记录脏页被修改的时间:</p><ul><li>oldest_modification:第一次修改 Buffer Pool 中某个缓冲页时,将修改该页的 MTR <strong>开始时</strong>对应的 lsn 值写入这个属性,所以链表页是以该值进行排序的</li><li>newest_modification:每次修改页面,都将 MTR 结束时全局的 lsn 值写入这个属性,所以该值是该页面最后一次修改后的 lsn 值</li></ul><p>全局变量 checkpoint_lsn 表示<strong>当前系统可以被覆盖的 redo 日志总量</strong>,当 redo 日志对应的脏页已经被刷新到磁盘后,该文件空间就可以被覆盖重用,此时执行一次 checkpoint 来更新 checkpoint_lsn 的值存入管理信息(刷脏和执行一次 checkpoint 并不是同一个线程),该值的增量就代表磁盘文件中当前位置向后可以被覆盖的文件的量,所以该值是一直增大的</p><p><strong>checkpoint</strong>:从 flush 链表尾部中找出还未刷脏的页面,该页面是当前系统中最早被修改的脏页,该页面之前产生的脏页都已经刷脏,然后将该页 oldest_modification 值赋值给 checkpoint_lsn,因为 lsn 小于该值时产生的 redo 日志都可以被覆盖了</p><p>但是在系统忙碌时,后台线程的刷脏操作不能将脏页快速刷出,导致系统无法及时执行 checkpoint ,这时需要用户线程从 flush 链表中把最早修改的脏页刷新到磁盘中,然后执行 checkpoint</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">write pos ------- checkpoint_lsn <span class="comment">// 两值之间的部分表示可以写入的日志量,当 pos 追赶上 lsn 时必须执行 checkpoint</span></span><br></pre></td></tr></table></figure><p>使用命令可以查看当前 InnoDB 存储引擎各种 lsn 的值:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW ENGINE INNODB STATUS\G</span><br></pre></td></tr></table></figure><hr><h5 id="崩溃恢复">崩溃恢复</h5><p>恢复的起点:在从 redo 日志文件组的管理信息中获取最近发生 checkpoint 的信息,<strong>从 checkpoint_lsn 对应的日志文件开始恢复</strong></p><p>恢复的终点:扫描日志文件的 block,block 的头部记录着当前 block 使用了多少字节,填满的 block 总是 512 字节, 如果某个 block 不是 512 字节,说明该 block 就是需要恢复的最后一个 block</p><p>恢复的过程:按照 redo log 依次执行恢复数据,优化方式</p><ul><li>使用哈希表:根据 redo log 的 space ID 和 page number 属性计算出哈希值,将对同一页面的修改放入同一个槽里,可以一次性完成对某页的恢复,<strong>避免了随机 IO</strong></li><li>跳过已经刷新到磁盘中的页面:数据页的 File Header 中的 FILE_PAGE_LSN 属性(类似 newest_modification)表示最近一次修改页面时的 lsn 值,如果在 checkpoint 后,数据页被刷新到磁盘中,那么该页 lsn 属性肯定大于 checkpoint_lsn</li></ul><p>总结:先写 redo buffer,在写 change buffer,先刷 redo log,再刷脏,在删除完成刷脏 redo log</p><p>参考书籍:<a href="https://book.douban.com/subject/35231266/">https://book.douban.com/subject/35231266/</a></p><hr><h4 id="工作流程-3">工作流程</h4><h5 id="日志对比">日志对比</h5><p>MySQL 中还存在 binlog(二进制日志)也可以记录写操作并用于数据的恢复,<strong>保证数据不丢失</strong>,二者的区别是:</p><ul><li>作用不同:redo log 是用于 crash recovery (故障恢复),保证 MySQL 宕机也不会影响持久性;binlog 是用于 point-in-time recovery 的,保证服务器可以基于时间点恢复数据,此外 binlog 还用于主从复制</li><li>层次不同:redo log 是 InnoDB 存储引擎实现的,而 binlog 是MySQL的 Server 层实现的,同时支持 InnoDB 和其他存储引擎</li><li>内容不同:redo log 是物理日志,内容基于磁盘的 Page;binlog 的内容是二进制的,根据 binlog_format 参数的不同,可能基于SQL 语句、基于数据本身或者二者的混合(日志部分详解)</li><li>写入时机不同:binlog 在事务提交时一次写入;redo log 的写入时机相对多元</li></ul><p>binlog 为什么不支持奔溃恢复?</p><ul><li>binlog 记录的是语句,并不记录数据页级的数据(哪个页改了哪些地方),所以没有能力恢复数据页</li><li>binlog 是追加写,保存全量的日志,没有标志确定从哪个点开始的数据是已经刷盘了,而 redo log 只要在 checkpoint_lsn 后面的就是没有刷盘的</li></ul><hr><h5 id="更新记录">更新记录</h5><p>更新一条记录的过程:写之前一定先读</p><ul><li><p>在 B+ 树中定位到该记录(这个过程也被称作加锁读),如果该记录所在的页面不在 Buffer Pool 里,先将其加载进内存</p></li><li><p>首先更新该记录对应的聚簇索引,更新聚簇索引记录时:</p><ul><li><p>更新记录前向 undo 页面写 undo 日志,由于这是更改页面,所以需要记录一下相应的 redo 日志</p><p>注意:修改 undo页面也是在<strong>修改页面</strong>,事务凡是修改页面就需要先记录相应的 redo 日志</p></li><li><p>然后<strong>先记录对应的的 redo 日志</strong>(等待 MTR 提交后写入 redo log buffer),<strong>最后进行真正的更新记录</strong></p></li></ul></li><li><p>更新其他的二级索引记录,不会再记录 undo log,只记录 redo log 到 buffer 中</p></li><li><p>在一条更新语句执行完成后(也就是将所有待更新记录都更新完了),就会开始记录该语句对应的 binlog 日志,此时记录的 binlog 并没有刷新到硬盘上,还在内存中,在事务提交时才会统一将该事务运行过程中的所有 binlog 日志刷新到硬盘</p></li></ul><p>假设表中有字段 id 和 a,存在一条 <code>id = 1, a = 2</code> 的记录,此时执行更新语句:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> <span class="keyword">table</span> <span class="keyword">set</span> a<span class="operator">=</span><span class="number">2</span> <span class="keyword">where</span> id<span class="operator">=</span><span class="number">1</span>;</span><br></pre></td></tr></table></figure><p>InnoDB 会真正的去执行把值修改成 (1,2) 这个操作,先加行锁,在去更新,并不会提前判断相同就不修改了</p><p>参考文章:<a href="https://mp.weixin.qq.com/s/wcJ2KisSaMnfP4nH5NYaQA">https://mp.weixin.qq.com/s/wcJ2KisSaMnfP4nH5NYaQA</a></p><hr><h5 id="两段提交">两段提交</h5><p>当客户端执行 COMMIT 语句或者在自动提交的情况下,MySQL 内部开启一个 XA 事务,分两阶段来完成 XA 事务的提交:</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">update</span> T <span class="keyword">set</span> c<span class="operator">=</span>c<span class="operator">+</span><span class="number">1</span> <span class="keyword">where</span> ID<span class="operator">=</span><span class="number">2</span>;</span><br></pre></td></tr></table></figure><img src="../image/post/MySQL-update%E7%9A%84%E6%89%A7%E8%A1%8C%E6%B5%81%E7%A8%8B.png" style="zoom: 33%;" /><p>流程说明:执行引擎将这行新数据读入到内存中(Buffer Pool)后,先将此次更新操作记录到 redo log buffer 里,然后更新记录。最后将 redo log 刷盘后事务处于 prepare 状态,执行器会生成这个操作的 binlog,并<strong>把 binlog 写入磁盘</strong>,完成提交</p><p>两阶段:</p><ul><li>Prepare 阶段:存储引擎将该事务的 <strong>redo 日志刷盘</strong>,并且将本事务的状态设置为 PREPARE,代表执行完成随时可以提交事务</li><li>Commit 阶段:先将事务执行过程中产生的 binlog 刷新到硬盘,再执行存储引擎的提交工作,引擎把 redo log 改成提交状态</li></ul><p>redo log 和 binlog 都可以用于表示事务的提交状态,而<strong>两阶段提交就是让这两个状态保持逻辑上的一致</strong>,也有利于主从复制,更好的保持主从数据的一致性</p><hr><h5 id="数据恢复">数据恢复</h5><p>系统崩溃前没有提交的事务的 redo log 可能已经刷盘(定时线程或者 checkpoint),怎么处理崩溃恢复?</p><p>工作流程:通过 undo log 在服务器重启时将未提交的事务回滚掉。首先定位到 128 个回滚段遍历 slot,获取 undo 链表首节点页面的 undo segement header 中的 TRX_UNDO_STATE 属性,表示当前链表的事务属性,事务状态是活跃的就全部回滚,如果是 PREPARE 状态,就需要根据 binlog 的状态进行判断:</p><ul><li>如果在时刻 A 发生了崩溃(crash),由于此时 binlog 还没完成,所以需要进行回滚</li><li>如果在时刻 B 发生了崩溃,redo log 和 binlog 有一个共<strong>同的数据字段叫 XID</strong>,崩溃恢复的时候,会按顺序扫描 redo log:<ul><li>如果 redo log 里面的事务是完整的,也就是已经有了 commit 标识,说明 binlog 也已经记录完整,直接从 redo log 恢复数据</li><li>如果 redo log 里面的事务只有 prepare,就根据 XID 去 binlog 中判断对应的事务是否存在并完整,如果完整可以恢复数据,提交事务</li></ul></li></ul><p>判断一个事务的 binlog 是否完整的方法:</p><ul><li>statement 格式的 binlog,最后会有 COMMIT</li><li>row 格式的 binlog,最后会有一个 XID event</li><li>MySQL 5.6.2 版本以后,引入了 binlog-checksum 参数用来验证 binlog 内容的正确性(可能日志中间出错)</li></ul><p>参考文章:<a href="https://time.geekbang.org/column/article/73161">https://time.geekbang.org/column/article/73161</a></p><hr><h4 id="刷脏优化">刷脏优化</h4><p>系统在进行刷脏时会占用一部分系统资源,会影响系统的性能,<strong>产生系统抖动</strong></p><ul><li>一个查询要淘汰的脏页个数太多,会导致查询的响应时间明显变长</li><li>日志写满,更新全部堵住,写性能跌为 0,这种情况对敏感业务来说,是不能接受的</li></ul><p>InnoDB 刷脏页的控制策略:</p><ul><li><code>innodb_io_capacity</code> 参数代表磁盘的读写能力,建议设置成磁盘的 IOPS(每秒的 IO 次数)</li><li>刷脏速度参考两个因素:脏页比例和 redo log 写盘速度<ul><li>参数 <code>innodb_max_dirty_pages_pct</code> 是脏页比例上限,默认值是 75%,InnoDB 会根据当前的脏页比例,算出一个范围在 0 到 100 之间的数字</li><li>InnoDB 每次写入的日志都有一个序号,当前写入的序号跟 checkpoint 对应的序号之间的差值,InnoDB 根据差值算出一个范围在 0 到 100 之间的数字</li><li>两者较大的值记为 R,执行引擎按照 innodb_io_capacity 定义的能力乘以 R% 来控制刷脏页的速度</li></ul></li><li><code>innodb_flush_neighbors</code> 参数置为 1 代表控制刷脏时检查相邻的数据页,如果也是脏页就一起刷脏,并检查邻居的邻居,这个行为会一直蔓延直到不是脏页,在 MySQL 8.0 中该值的默认值是 0,不建议开启此功能</li></ul><hr><h3 id="一致特性">一致特性</h3><p>一致性是指事务执行前后,数据库的完整性约束没有被破坏,事务执行的前后都是合法的数据状态。</p><p>数据库的完整性约束包括但不限于:实体完整性(如行的主键存在且唯一)、列完整性(如字段的类型、大小、长度要符合要求)、外键约束、用户自定义完整性(如转账前后,两个账户余额的和应该不变)</p><p>实现一致性的措施:</p><ul><li>保证原子性、持久性和隔离性,如果这些特性无法保证,事务的一致性也无法保证</li><li>数据库本身提供保障,例如不允许向整形列插入字符串值、字符串长度不能超过列的限制等</li><li>应用层面进行保障,例如如果转账操作只扣除转账者的余额,而没有增加接收者的余额,无论数据库实现的多么完美,也无法保证状态的一致</li></ul><hr><h2 id="锁机制">锁机制</h2><h3 id="基本介绍-4">基本介绍</h3><p>锁机制:数据库为了保证数据的一致性,在共享的资源被并发访问时变得安全有序所设计的一种规则</p><p>利用 MVCC 性质进行读取的操作叫<strong>一致性读</strong>,读取数据前加锁的操作叫<strong>锁定读</strong></p><p>锁的分类:</p><ul><li>按操作分类:<ul><li>共享锁:也叫读锁。对同一份数据,多个事务读操作可以同时加锁而不互相影响 ,但不能修改数据</li><li>排他锁:也叫写锁。当前的操作没有完成前,会阻断其他操作的读取和写入</li></ul></li><li>按粒度分类:<ul><li>表级锁:会锁定整个表,开销小,加锁快;不会出现死锁;锁定力度大,发生锁冲突概率高,并发度最低,偏向 MyISAM</li><li>行级锁:会锁定当前操作行,开销大,加锁慢;会出现死锁;锁定力度小,发生锁冲突概率低,并发度高,偏向 InnoDB</li><li>页级锁:锁的力度、发生冲突的概率和加锁开销介于表锁和行锁之间,会出现死锁,并发性能一般</li></ul></li><li>按使用方式分类:<ul><li>悲观锁:每次查询数据时都认为别人会修改,很悲观,所以查询时加锁</li><li>乐观锁:每次查询数据时都认为别人不会修改,很乐观,但是更新时会判断一下在此期间别人有没有去更新这个数据</li></ul></li></ul><ul><li><p>不同存储引擎支持的锁</p><table><thead><tr><th>存储引擎</th><th>表级锁</th><th>行级锁</th><th>页级锁</th></tr></thead><tbody><tr><td>MyISAM</td><td>支持</td><td>不支持</td><td>不支持</td></tr><tr><td>InnoDB</td><td><strong>支持</strong></td><td><strong>支持</strong></td><td>不支持</td></tr><tr><td>MEMORY</td><td>支持</td><td>不支持</td><td>不支持</td></tr><tr><td>BDB</td><td>支持</td><td>不支持</td><td>支持</td></tr></tbody></table></li></ul><p>从锁的角度来说:表级锁更适合于以查询为主,只有少量按索引条件更新数据的应用,如 Web 应用;而行级锁则更适合于有大量按索引条件并发更新少量不同数据,同时又有并查询的应用,如一些在线事务处理系统</p><hr><h3 id="内存结构">内存结构</h3><p>对一条记录加锁的本质就是<strong>在内存中</strong>创建一个锁结构与之关联,结构包括</p><ul><li>事务信息:锁对应的事务信息,一个锁属于一个事务</li><li>索引信息:对于行级锁,需要记录加锁的记录属于哪个索引</li><li>表锁和行锁信息:表锁记录着锁定的表,行锁记录了 Space ID 所在表空间、Page Number 所在的页号、n_bits 使用了多少比特</li><li>type_mode:一个 32 比特的数,被分成 lock_mode、lock_type、rec_lock_type 三个部分<ul><li>lock_mode:锁模式,记录是共享锁、排他锁、意向锁之类</li><li>lock_type:代表表级锁还是行级锁</li><li>rec_lock_type:代表行锁的具体类型和 is_waiting 属性,is_waiting = true 时表示当前事务尚未获取到锁,处于等待状态。事务获取锁后的锁结构是 is_waiting 为 false,释放锁时会检查是否与当前记录关联的锁结构,如果有就唤醒对应事务的线程</li></ul></li></ul><p>一个事务可能操作多条记录,为了节省内存,满足下面条件的锁使用同一个锁结构:</p><ul><li>在同一个事务中的加锁操作</li><li>被加锁的记录在同一个页面中</li><li>加锁的类型是一样的</li><li>加锁的状态是一样的</li></ul><hr><h3 id="Server">Server</h3><p>MySQL 里面表级别的锁有两种:一种是表锁,一种是元数据锁(meta data lock,MDL)</p><p>MDL 叫元数据锁,主要用来保护 MySQL 内部对象的元数据,保证数据读写的正确性,<strong>当对一个表做增删改查的时候,加 MDL 读锁;当要对表做结构变更操作 DDL 的时候,加 MDL 写锁</strong>,两种锁不相互兼容,所以可以保证 DDL、DML、DQL 操作的安全</p><p>说明:DDL 操作执行前会隐式提交当前会话的事务,因为 DDL 一般会在若干个特殊事务中完成,开启特殊事务前需要提交到其他事务</p><p>MDL 锁的特性:</p><ul><li><p>MDL 锁不需要显式使用,在访问一个表的时候会被自动加上,在事务开始时申请,整个事务提交后释放(执行完单条语句不释放)</p></li><li><p>MDL 锁是在 Server 中实现,不是 InnoDB 存储引擎层能直接实现的锁</p></li><li><p>MDL 锁还能实现其他粒度级别的锁,比如全局锁、库级别的锁、表空间级别的锁</p></li></ul><p>FLUSH TABLES WITH READ LOCK 简称(FTWRL),全局读锁,让整个库处于只读状态,DDL DML 都被阻塞,工作流程:</p><ol><li>上全局读锁(lock_global_read_lock)</li><li>清理表缓存(close_cached_tables)</li><li>上全局 COMMIT 锁(make_global_read_lock_block_commit)</li></ol><p>该命令主要用于备份工具做<strong>一致性备份</strong>,由于 FTWRL 需要持有两把全局的 MDL 锁,并且还要关闭所有表对象,因此杀伤性很大</p><hr><h3 id="MyISAM-2">MyISAM</h3><h4 id="表级锁">表级锁</h4><p>MyISAM 存储引擎只支持表锁,这也是 MySQL 开始几个版本中唯一支持的锁类型</p><p>MyISAM 引擎在执行查询语句之前,会<strong>自动</strong>给涉及到的所有表加读锁,在执行增删改之前,会<strong>自动</strong>给涉及的表加写锁,这个过程并不需要用户干预,所以用户一般不需要直接用 LOCK TABLE 命令给 MyISAM 表显式加锁</p><ul><li><p>加锁命令:(对 InnoDB 存储引擎也适用)</p><p>读锁:所有连接只能读取数据,不能修改</p><p>写锁:其他连接不能查询和修改数据</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">-- 读锁</span><br><span class="line">LOCK TABLE table_name READ;</span><br><span class="line"></span><br><span class="line">-- 写锁</span><br><span class="line">LOCK TABLE table_name WRITE;</span><br></pre></td></tr></table></figure></li><li><p>解锁命令:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-- 将当前会话所有的表进行解锁</span><br><span class="line">UNLOCK TABLES;</span><br></pre></td></tr></table></figure></li></ul><p>锁的兼容性:</p><ul><li>对 MyISAM 表的读操作,不会阻塞其他用户对同一表的读请求,但会阻塞对同一表的写请求</li><li>对 MyISAM 表的写操作,则会阻塞其他用户对同一表的读和写操作</li></ul><p><img src="../image/post/MySQL-MyISAM%20%E9%94%81%E7%9A%84%E5%85%BC%E5%AE%B9%E6%80%A7.png" alt=""></p><p>锁调度:<strong>MyISAM 的读写锁调度是写优先</strong>,因为写锁后其他线程不能做任何操作,大量的更新会使查询很难得到锁,从而造成永远阻塞,所以 MyISAM 不适合做写为主的表的存储引擎</p><hr><h4 id="锁操作">锁操作</h4><h5 id="读锁">读锁</h5><p>两个客户端操作 Client 1和 Client 2,简化为 C1、C2</p><ul><li><p>数据准备:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE `tb_book` (</span><br><span class="line"> `id` INT(11) AUTO_INCREMENT,</span><br><span class="line"> `name` VARCHAR(50) DEFAULT NULL,</span><br><span class="line"> `publish_time` DATE DEFAULT NULL,</span><br><span class="line"> `status` CHAR(1) DEFAULT NULL,</span><br><span class="line"> PRIMARY KEY (`id`)</span><br><span class="line">) ENGINE=MYISAM DEFAULT CHARSET=utf8 ;</span><br><span class="line"></span><br><span class="line">INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'java编程思想','2088-08-01','1');</span><br><span class="line">INSERT INTO tb_book (id, NAME, publish_time, STATUS) VALUES(NULL,'mysql编程思想','2088-08-08','0');</span><br></pre></td></tr></table></figure></li><li><p>C1、C2 加读锁,同时查询可以正常查询出数据</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">LOCK TABLE tb_book READ;-- C1、C2</span><br><span class="line">SELECT * FROM tb_book;-- C1、C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-MyISAM%20%E8%AF%BB%E9%94%811.png" alt=""></p></li><li><p>C1 加读锁,C1、C2 查询未锁定的表,C1 报错,C2 正常查询</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">LOCK TABLE tb_book READ;-- C1</span><br><span class="line">SELECT * FROM tb_user;-- C1、C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-MyISAM%20%E8%AF%BB%E9%94%812.png" alt=""></p><p>C1、C2 执行插入操作,C1 报错,C2 等待获取</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO tb_book VALUES(NULL,'Spring高级','2088-01-01','1');-- C1、C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-MyISAM%20%E8%AF%BB%E9%94%813.png" alt=""></p><p>当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 INSERT 语句立即执行</p></li></ul><hr><h5 id="写锁">写锁</h5><p>两个客户端操作 Client 1和 Client 2,简化为 C1、C2</p><ul><li><p>C1 加写锁,C1、C2查询表,C1 正常查询,C2 需要等待</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">LOCK TABLE tb_book WRITE;-- C1</span><br><span class="line">SELECT * FROM tb_book;-- C1、C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-MyISAM%20%E5%86%99%E9%94%811.png" alt=""></p><p>当在 C1 中释放锁指令 UNLOCK TABLES,C2 中的 SELECT 语句立即执行</p></li><li><p>C1、C2 同时加写锁</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">LOCK TABLE tb_book WRITE;</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-MyISAM%20%E5%86%99%E9%94%812.png" alt=""></p></li><li><p>C1 加写锁,C1、C2查询未锁定的表,C1 报错,C2 正常查询</p></li></ul><hr><h4 id="锁状态">锁状态</h4><ul><li><p>查看锁竞争:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW OPEN TABLES;</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E9%94%81%E4%BA%89%E7%94%A8%E6%83%85%E5%86%B5%E6%9F%A5%E7%9C%8B1.png" alt=""></p><p>In_user:表当前被查询使用的次数,如果该数为零,则表是打开的,但是当前没有被使用</p><p>Name_locked:表名称是否被锁定,名称锁定用于取消表或对表进行重命名等操作</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">LOCK TABLE tb_book READ;-- 执行命令</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E9%94%81%E4%BA%89%E7%94%A8%E6%83%85%E5%86%B5%E6%9F%A5%E7%9C%8B2.png" alt=""></p></li><li><p>查看锁状态:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW STATUS LIKE 'Table_locks%';</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-MyISAM%20%E9%94%81%E7%8A%B6%E6%80%81.png" alt=""></p><p>Table_locks_immediate:指的是能立即获得表级锁的次数,每立即获取锁,值加 1</p><p>Table_locks_waited:指的是不能立即获取表级锁而需要等待的次数,每等待一次,该值加 1,此值高说明存在着较为严重的表级锁争用情况</p></li></ul><hr><h3 id="InnoDB">InnoDB</h3><h4 id="行级锁">行级锁</h4><h5 id="记录锁">记录锁</h5><p>InnoDB 与 MyISAM 的最大不同有两点:一是支持事务;二是采用了行级锁,<strong>InnoDB 同时支持表锁和行锁</strong></p><p>行级锁,也称为记录锁(Record Lock),InnoDB 实现了以下两种类型的行锁:</p><ul><li>共享锁 (S):又称为读锁,简称 S 锁,多个事务对于同一数据可以共享一把锁,都能访问到数据,但是只能读不能修改</li><li>排他锁 (X):又称为写锁,简称 X 锁,不能与其他锁并存,如一个事务获取了一个数据行的排他锁,其他事务就不能再获取该行的其他锁,包括共享锁和排他锁,只有获取排他锁的事务是可以对数据读取和修改</li></ul><p>RR 隔离界别下,对于 UPDATE、DELETE 和 INSERT 语句,InnoDB 会<strong>自动给涉及数据集加排他锁</strong>(行锁),在 commit 时自动释放;对于普通 SELECT 语句,不会加任何锁(只是针对 InnoDB 层来说的,因为在 Server 层会<strong>加 MDL 读锁</strong>),通过 MVCC 防止并发冲突</p><p>在事务中加的锁,并不是不需要了就释放,而是在事务中止或提交时自动释放,这个就是<strong>两阶段锁协议</strong>。所以一般将更新共享资源(并发高)的 SQL 放到事务的最后执行,可以让其他线程尽量的减少等待时间</p><p>锁的兼容性:</p><ul><li>共享锁和共享锁 兼容</li><li>共享锁和排他锁 冲突</li><li>排他锁和排他锁 冲突</li><li>排他锁和共享锁 冲突</li></ul><p>显式给数据集加共享锁或排他锁:<strong>加锁读就是当前读,读取的是最新数据</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE-- 共享锁</span><br><span class="line">SELECT * FROM table_name WHERE ... FOR UPDATE-- 排他锁</span><br></pre></td></tr></table></figure><p>注意:<strong>锁默认会锁聚簇索引(锁就是加在索引上)</strong>,但是当使用覆盖索引时,加共享锁只锁二级索引,不锁聚簇索引</p><hr><h5 id="锁操作-2">锁操作</h5><p>两个客户端操作 Client 1和 Client 2,简化为 C1、C2</p><ul><li><p>环境准备</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE test_innodb_lock(</span><br><span class="line">id INT(11),</span><br><span class="line">name VARCHAR(16),</span><br><span class="line">sex VARCHAR(1)</span><br><span class="line">)ENGINE = INNODB DEFAULT CHARSET=utf8;</span><br><span class="line"></span><br><span class="line">INSERT INTO test_innodb_lock VALUES(1,'100','1');</span><br><span class="line">-- ..........</span><br><span class="line"></span><br><span class="line">CREATE INDEX idx_test_innodb_lock_id ON test_innodb_lock(id);</span><br><span class="line">CREATE INDEX idx_test_innodb_lock_name ON test_innodb_lock(name);</span><br></pre></td></tr></table></figure></li><li><p>关闭自动提交功能:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET AUTOCOMMIT=0;-- C1、C2</span><br></pre></td></tr></table></figure><p>正常查询数据:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM test_innodb_lock;-- C1、C2</span><br></pre></td></tr></table></figure></li><li><p>查询 id 为 3 的数据,正常查询:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM test_innodb_lock WHERE id=3;-- C1、C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E6%93%8D%E4%BD%9C1.png" alt=""></p></li><li><p>C1 更新 id 为 3 的数据,但不提交:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE test_innodb_lock SET name='300' WHERE id=3;-- C1</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E6%93%8D%E4%BD%9C2.png" alt=""></p><p>C2 查询不到 C1 修改的数据,因为隔离界别为 REPEATABLE READ,C1 提交事务,C2 查询:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">COMMIT;-- C1</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E6%93%8D%E4%BD%9C3.png" alt=""></p><p>提交后仍然查询不到 C1 修改的数据,因为隔离级别可以防止脏读、不可重复读,所以 C2 需要提交才可以查询到其他事务对数据的修改:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">COMMIT;-- C2</span><br><span class="line">SELECT * FROM test_innodb_lock WHERE id=3;-- C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E6%93%8D%E4%BD%9C4.png" alt=""></p></li><li><p>C1 更新 id 为 3 的数据,但不提交,C2 也更新 id 为 3 的数据:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">UPDATE test_innodb_lock SET name='3' WHERE id=3;-- C1</span><br><span class="line">UPDATE test_innodb_lock SET name='30' WHERE id=3;-- C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E6%93%8D%E4%BD%9C5.png" alt=""></p><p>当 C1 提交,C2 直接解除阻塞,直接更新</p></li><li><p>操作不同行的数据:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">UPDATE test_innodb_lock SET name='10' WHERE id=1;-- C1</span><br><span class="line">UPDATE test_innodb_lock SET name='30' WHERE id=3;-- C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E6%93%8D%E4%BD%9C6.png" alt=""></p><p>由于 C1、C2 操作的不同行,获取不同的行锁,所以都可以正常获取行锁</p></li></ul><hr><h4 id="锁分类">锁分类</h4><h5 id="间隙锁">间隙锁</h5><p>InnoDB 会对间隙(GAP)进行加锁,就是间隙锁 (RR 隔离级别下才有该锁)。间隙锁之间不存在冲突关系,<strong>多个事务可以同时对一个间隙加锁</strong>,但是间隙锁会阻止往这个间隙中插入一个记录的操作</p><p>InnoDB 加锁的基本单位是 next-key lock,该锁是行锁和 gap lock 的组合(X or S 锁),但是加锁过程是分为间隙锁和行锁两段执行</p><ul><li>可以<strong>保护当前记录和前面的间隙</strong>,遵循左开右闭原则,单纯的是间隙锁左开右开</li><li>假设有 10、11、13,那么可能的间隙锁包括:(负无穷,10]、(10,11]、(11,13]、(13,正无穷)</li></ul><p>几种索引的加锁情况:</p><ul><li>唯一索引加锁在值存在时是行锁,next-key lock 会退化为行锁,值不存在会变成间隙锁</li><li>普通索引加锁会继续向右遍历到不满足条件的值为止,next-key lock 退化为间隙锁</li><li>范围查询无论是否是唯一索引,都需要访问到不满足条件的第一个值为止</li><li>对于联合索引且是唯一索引,如果 where 条件只包括联合索引的一部分,那么会加间隙锁</li></ul><p>间隙锁优点:RR 级别下间隙锁可以解决事务的一部分的<strong>幻读问题</strong>,通过对间隙加锁,可以防止读取过程中数据条目发生变化。一部分的意思是不会对全部间隙加锁,只能加锁一部分的间隙。</p><p>间隙锁危害:</p><ul><li>当锁定一个范围的键值后,即使某些不存在的键值也会被无辜的锁定,造成在锁定的时候无法插入锁定键值范围内的任何数据,在某些场景下这可能会对性能造成很大的危害,影响并发度</li><li>事务 A B 同时锁住一个间隙后,A 往当前间隙插入数据时会被 B 的间隙锁阻塞,B 也执行插入间隙数据的操作时就会<strong>产生死锁</strong></li></ul><p>现场演示:</p><ul><li><p>关闭自动提交功能:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET AUTOCOMMIT=0;-- C1、C2</span><br></pre></td></tr></table></figure></li><li><p>查询数据表:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM test_innodb_lock;</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%97%B4%E9%9A%99%E9%94%811.png" alt=""></p></li><li><p>C1 根据 id 范围更新数据,C2 插入数据:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">UPDATE test_innodb_lock SET name='8888' WHERE id < 4;-- C1</span><br><span class="line">INSERT INTO test_innodb_lock VALUES(2,'200','2');-- C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%97%B4%E9%9A%99%E9%94%812.png" alt=""></p><p>出现间隙锁,C2 被阻塞,等待 C1 提交事务后才能更新</p></li></ul><hr><h5 id="意向锁">意向锁</h5><p>InnoDB 为了支持多粒度的加锁,允许行锁和表锁同时存在,支持在不同粒度上的加锁操作,InnoDB 增加了意向锁(Intention Lock)</p><p>意向锁是将锁定的对象分为多个层次,意向锁意味着事务希望在更细粒度上进行加锁,意向锁分为两种:</p><ul><li>意向共享锁(IS):事务有意向对表加共享锁</li><li>意向排他锁(IX):事务有意向对表加排他锁</li></ul><p><strong>IX,IS 是表级锁</strong>,不会和行级的 X,S 锁发生冲突,意向锁是在加表级锁之前添加,为了在加表级锁时可以快速判断表中是否有记录被上锁,比如向一个表添加表级 X 锁的时:</p><ul><li>没有意向锁,则需要遍历整个表判断是否有锁定的记录</li><li>有了意向锁,首先判断是否存在意向锁,然后判断该意向锁与即将添加的表级锁是否兼容即可,因为意向锁的存在代表有表级锁的存在或者即将有表级锁的存在</li></ul><p>兼容性如下所示:</p><p><img src="../image/post/MySQL-%E6%84%8F%E5%90%91%E9%94%81%E5%85%BC%E5%AE%B9%E6%80%A7.png" alt=""></p><p><strong>插入意向锁</strong> Insert Intention Lock 是在插入一行记录操作之前设置的一种间隙锁,是行级锁</p><p>插入意向锁释放了一种插入信号,即多个事务在相同的索引间隙插入时如果不是插入相同的间隙位置就不需要互相等待。假设某列有索引,只要两个事务插入位置不同,如事务 A 插入 3,事务 B 插入 4,那么就可以同时插入</p><hr><h5 id="自增锁">自增锁</h5><p>系统会自动给 AUTO_INCREMENT 修饰的列进行递增赋值,实现方式:</p><ul><li>AUTO_INC 锁:表级锁,执行插入语句时会自动添加,在该语句执行完成后释放,并不是事务结束</li><li>轻量级锁:为插入语句生成 AUTO_INCREMENT 修饰的列时获取该锁,生成以后释放掉,不需要等到插入语句执行完后释放</li></ul><p>系统变量 <code>innodb_autoinc_lock_mode</code> 控制采取哪种方式:</p><ul><li>0:全部采用 AUTO_INC 锁</li><li>1:全部采用轻量级锁</li><li>2:混合使用,在插入记录的数量确定是采用轻量级锁,不确定时采用 AUTO_INC 锁</li></ul><hr><h5 id="隐式锁">隐式锁</h5><p>一般情况下 INSERT 语句是不需要在内存中生成锁结构的,会进行隐式的加锁,保护的是插入后的安全</p><p>注意:如果插入的间隙被其他事务加了间隙锁,此次插入会被阻塞,并在该间隙插入一个插入意向锁</p><ul><li>聚簇索引:索引记录有 trx_id 隐藏列,表示最后改动该记录的事务 id,插入数据后事务 id 就是当前事务。其他事务想获取该记录的锁时会判断当前记录的事务 id 是否是活跃的,如果不是就可以正常加锁;如果是就创建一个 X 的锁结构,该锁的 is_waiting 是 false,为自己的事务创建一个锁结构,is_waiting 是 true(类似 Java 中的锁升级)</li><li>二级索引:获取数据页 Page Header 中的 PAGE_MAX_TRX_ID 属性,代表修改当前页面的最大的事务 ID,如果小于当前活跃的最小事务 id,就证明插入该数据的事务已经提交,否则就需要获取到主键值进行回表操作</li></ul><p>隐式锁起到了延迟生成锁的效果,如果其他事务与隐式锁没有冲突,就可以避免锁结构的生成,节省了内存资源</p><p>INSERT 在两种情况下会生成锁结构:</p><ul><li><p>重复键:在插入主键或唯一二级索引时遇到重复的键值会报错,在报错前需要对对应的聚簇索引进行加锁</p><ul><li>隔离级别 <= Read Uncommitted,加 S 型 Record Lock</li><li>隔离级别 >= Repeatable Read,加 S 型 next_key 锁</li></ul></li><li><p>外键检查:如果待插入的记录在父表中可以找到,会对父表的记录加 S 型 Record Lock。如果待插入的记录在父表中找不到</p><ul><li>隔离级别 <= Read Committed,不加锁</li><li>隔离级别 >= Repeatable Read,加间隙锁</li></ul></li></ul><hr><h4 id="锁优化">锁优化</h4><h5 id="优化锁">优化锁</h5><p>InnoDB 存储引擎实现了行级锁定,虽然在锁定机制的实现方面带来了性能损耗可能比表锁会更高,但是在整体并发处理能力方面要远远优于 MyISAM 的表锁,当系统并发量较高的时候,InnoDB 的整体性能远远好于 MyISAM</p><p>但是使用不当可能会让 InnoDB 的整体性能表现不仅不能比 MyISAM 高,甚至可能会更差</p><p>优化建议:</p><ul><li>尽可能让所有数据检索都能通过索引来完成,避免无索引行锁升级为表锁</li><li>合理设计索引,尽量缩小锁的范围</li><li>尽可能减少索引条件及索引范围,避免间隙锁</li><li>尽量控制事务大小,减少锁定资源量和时间长度</li><li>尽可使用低级别事务隔离(需要业务层面满足需求)</li></ul><hr><h5 id="锁升级">锁升级</h5><p>索引失效造成<strong>行锁升级为表锁</strong>,不通过索引检索数据,全局扫描的过程中 InnoDB 会将对表中的所有记录加锁,实际效果和<strong>表锁</strong>一样,实际开发过程应避免出现索引失效的状况</p><ul><li><p>查看当前表的索引:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW INDEX FROM test_innodb_lock;</span><br></pre></td></tr></table></figure></li><li><p>关闭自动提交功能:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SET AUTOCOMMIT=0;-- C1、C2</span><br></pre></td></tr></table></figure></li><li><p>执行更新语句:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">UPDATE test_innodb_lock SET sex='2' WHERE name=10;-- C1</span><br><span class="line">UPDATE test_innodb_lock SET sex='2' WHERE id=3;-- C2</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E5%8D%87%E7%BA%A7.png" alt=""></p><p>索引失效:执行更新时 name 字段为 varchar 类型,造成索引失效,最终行锁变为表锁</p></li></ul><hr><h5 id="死锁">死锁</h5><p>不同事务由于互相持有对方需要的锁而导致事务都无法继续执行的情况称为死锁</p><p>死锁情况:线程 A 修改了 id = 1 的数据,请求修改 id = 2 的数据,线程 B 修改了 id = 2 的数据,请求修改 id = 1 的数据,产生死锁</p><p>解决策略:</p><ul><li><p>直接进入等待直到超时,超时时间可以通过参数 innodb_lock_wait_timeout 来设置,默认 50 秒,但是时间的设置不好控制,超时可能不是因为死锁,而是因为事务处理比较慢,所以一般不采取该方式</p></li><li><p>主动死锁检测,发现死锁后<strong>主动回滚死锁链条中较小的一个事务</strong>,让其他事务得以继续执行,将参数 <code>innodb_deadlock_detect</code> 设置为 on,表示开启该功能(事务较小的意思就是事务执行过程中插入、删除、更新的记录条数)</p><p>死锁检测并不是每个语句都要检测,只有在加锁访问的行上已经有锁时,当前事务被阻塞了才会检测,也是从当前事务开始进行检测</p></li></ul><p>通过执行 <code>SHOW ENGINE INNODB STATUS</code> 可以查看最近发生的一次死循环,全局系统变量 <code>innodb_print_all_deadlocks</code> 设置为 on,就可以将每个死锁信息都记录在 MySQL 错误日志中</p><p>死锁一般是行级锁,当表锁发生死锁时,会在事务中访问其他表时<strong>直接报错</strong>,破坏了持有并等待的死锁条件</p><hr><h4 id="锁状态-2">锁状态</h4><p>查看锁信息</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW STATUS LIKE 'innodb_row_lock%';</span><br></pre></td></tr></table></figure><img src="../image/post/MySQL-InnoDB%20%E9%94%81%E4%BA%89%E7%94%A8.png" style="zoom: 80%;" /><p>参数说明:</p><ul><li><p>Innodb_row_lock_current_waits:当前正在等待锁定的数量</p></li><li><p>Innodb_row_lock_time:从系统启动到现在锁定总时间长度</p></li><li><p>Innodb_row_lock_time_avg:每次等待所花平均时长</p></li><li><p>Innodb_row_lock_time_max:从系统启动到现在等待最长的一次所花的时间</p></li><li><p>Innodb_row_lock_waits:系统启动后到现在总共等待的次数</p></li></ul><p>当等待的次数很高,而且每次等待的时长也不短的时候,就需要分析系统中为什么会有如此多的等待,然后根据分析结果制定优化计划</p><p>查看锁状态:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM information_schema.innodb_locks;#锁的概况</span><br><span class="line">SHOW ENGINE INNODB STATUS\G; #InnoDB整体状态,其中包括锁的情况</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-InnoDB%E6%9F%A5%E7%9C%8B%E9%94%81%E7%8A%B6%E6%80%81.png" alt=""></p><p>lock_id 是锁 id;lock_trx_id 为事务 id;lock_mode 为 X 代表排它锁(写锁);lock_type 为 RECORD 代表锁为行锁(记录锁)</p><hr><h3 id="乐观锁">乐观锁</h3><p>悲观锁:在整个数据处理过程中,将数据处于锁定状态,为了保证事务的隔离性,就需要一致性锁定读。读取数据时给加锁,其它事务无法修改这些数据,修改删除数据时也加锁,其它事务同样无法读取这些数据</p><p>悲观锁和乐观锁使用前提:</p><ul><li>对于读的操作远多于写的操作的时候,一个更新操作加锁会阻塞所有的读取操作,降低了吞吐量,最后需要释放锁,锁是需要一些开销的,这时候可以选择乐观锁</li><li>如果是读写比例差距不是非常大或者系统没有响应不及时,吞吐量瓶颈的问题,那就不要去使用乐观锁,它增加了复杂度,也带来了业务额外的风险,这时候可以选择悲观锁</li></ul><p>乐观锁的实现方式:就是 CAS,比较并交换</p><ul><li><p>版本号</p><ol><li><p>给数据表中添加一个 version 列,每次更新后都将这个列的值加 1</p></li><li><p>读取数据时,将版本号读取出来,在执行更新的时候,比较版本号</p></li><li><p>如果相同则执行更新,如果不相同,说明此条数据已经发生了变化</p></li><li><p>用户自行根据这个通知来决定怎么处理,比如重新开始一遍,或者放弃本次更新</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">-- 创建city表</span><br><span class="line">CREATE TABLE city(</span><br><span class="line">id INT PRIMARY KEY AUTO_INCREMENT, -- 城市id</span><br><span class="line">NAME VARCHAR(20), -- 城市名称</span><br><span class="line">VERSION INT -- 版本号</span><br><span class="line">);</span><br><span class="line"></span><br><span class="line">-- 添加数据</span><br><span class="line">INSERT INTO city VALUES (NULL,'北京',1),(NULL,'上海',1),(NULL,'广州',1),(NULL,'深圳',1);</span><br><span class="line"></span><br><span class="line">-- 修改北京为北京市</span><br><span class="line">-- 1.查询北京的version</span><br><span class="line">SELECT VERSION FROM city WHERE NAME='北京';</span><br><span class="line">-- 2.修改北京为北京市,版本号+1。并对比版本号</span><br><span class="line">UPDATE city SET NAME='北京市',VERSION=VERSION+1 WHERE NAME='北京' AND VERSION=1;</span><br></pre></td></tr></table></figure></li></ol></li><li><p>时间戳</p><ul><li>和版本号方式基本一样,给数据表中添加一个列,名称无所谓,数据类型需要是 <strong>timestamp</strong></li><li>每次更新后都将最新时间插入到此列</li><li>读取数据时,将时间读取出来,在执行更新的时候,比较时间</li><li>如果相同则执行更新,如果不相同,说明此条数据已经发生了变化</li></ul></li></ul><p>乐观锁的异常情况:如果 version 被其他事务抢先更新,则在当前事务中更新失败,trx_id 没有变成当前事务的 ID,当前事务再次查询还是旧值,就会出现<strong>值没变但是更新不了</strong>的现象(anomaly)</p><p>解决方案:每次 CAS 更新不管成功失败,就结束当前事务;如果失败则重新起一个事务进行查询更新</p><h2 id="日志">日志</h2><h3 id="日志分类">日志分类</h3><p>在任何一种数据库中,都会有各种各样的日志,记录着数据库工作的过程,可以帮助数据库管理员追踪数据库曾经发生过的各种事件。</p><p>MySQL日志主要包括六种:</p><ol><li>重做日志(redo log)</li><li>回滚日志(undo log)</li><li>归档日志(binlog)(二进制日志)</li><li>错误日志(errorlog)</li><li>慢查询日志(slow query log)</li><li>一般查询日志(general log)</li><li>中继日志(relay log)</li></ol><hr><h3 id="错误日志">错误日志</h3><p>错误日志是 MySQL 中最重要的日志之一,记录了当 mysql 启动和停止时,以及服务器在运行过程中发生任何严重错误时的相关信息。当数据库出现任何故障导致无法正常使用时,可以首先查看此日志</p><p>该日志是默认开启的,默认位置是:<code>/var/log/mysql/error.log</code></p><p>查看指令:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SHOW VARIABLES LIKE 'log_error%';</span><br></pre></td></tr></table></figure><p>查看日志内容:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">tail</span> -f /var/log/mysql/error.log</span><br></pre></td></tr></table></figure><hr><h3 id="归档日志">归档日志</h3><h4 id="基本介绍-5">基本介绍</h4><p>归档日志(BINLOG)也叫二进制日志,是因为采用二进制进行存储,记录了所有的 DDL(数据定义语言)语句和 DML(数据操作语言)语句,但<strong>不包括数据查询语句,在事务提交前的最后阶段写入</strong></p><p>作用:<strong>灾难时的数据恢复和 MySQL 的主从复制</strong></p><p>归档日志默认情况下是没有开启的,需要在 MySQL 配置文件中开启,并配置 MySQL 日志的格式:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cd</span> /etc/mysql</span><br><span class="line">vim my.cnf</span><br><span class="line"></span><br><span class="line"><span class="comment"># 配置开启binlog日志, 日志的文件前缀为 mysqlbin -----> 生成的文件名如: mysqlbin.000001</span></span><br><span class="line">log_bin=mysqlbin</span><br><span class="line"><span class="comment"># 配置二进制日志的格式</span></span><br><span class="line">binlog_format=STATEMENT</span><br></pre></td></tr></table></figure><p>日志存放位置:配置时给定了文件名但是没有指定路径,日志默认写入MySQL 的数据目录</p><p>日志格式:</p><ul><li><p>STATEMENT:该日志格式在日志文件中记录的都是 <strong>SQL 语句</strong>,每一条对数据进行修改的 SQL 都会记录在日志文件中,通过 mysqlbinlog 工具,可以查看到每条语句的文本。主从复制时,从库会将日志解析为原语句,并在从库重新执行一遍</p><p>缺点:可能会导致主备不一致,因为记录的 SQL 在不同的环境中可能选择的索引不同,导致结果不同</p></li><li><p>ROW:该日志格式在日志文件中记录的是每一行的<strong>数据变更</strong>,而不是记录 SQL 语句。比如执行 SQL 语句 <code>update tb_book set status='1'</code>,如果是 STATEMENT,在日志中会记录一行 SQL 语句; 如果是 ROW,由于是对全表进行更新,就是每一行记录都会发生变更,ROW 格式的日志中会记录每一行的数据变更</p><p>缺点:记录的数据比较多,占用很多的存储空间</p></li><li><p>MIXED:这是 MySQL 默认的日志格式,混合了STATEMENT 和 ROW 两种格式,MIXED 格式能尽量利用两种模式的优点,而避开它们的缺点</p></li></ul><hr><h4 id="日志刷盘-2">日志刷盘</h4><p>事务执行过程中,先将日志写(write)到 binlog cache,事务提交时再把 binlog cache 写(fsync)到 binlog 文件中,一个事务的 binlog 是不能被拆开的,所以不论这个事务多大也要确保一次性写入</p><p>事务提交时执行器把 binlog cache 里的完整事务写入到 binlog 中,并清空 binlog cache</p><p>write 和 fsync 的时机由参数 sync_binlog 控制的:</p><ul><li>sync_binlog=0:表示每次提交事务都只 write,不 fsync</li><li>sync_binlog=1:表示每次提交事务都会执行 fsync</li><li>sync_binlog=N(N>1):表示每次提交事务都 write,但累积 N 个事务后才 fsync,但是如果主机发生异常重启,会丢失最近 N 个事务的 binlog 日志</li></ul><hr><h4 id="日志读取">日志读取</h4><p>日志文件存储位置:/var/lib/mysql</p><p>由于日志以二进制方式存储,不能直接读取,需要用 mysqlbinlog 工具来查看,语法如下:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqlbinlog log-file;</span><br></pre></td></tr></table></figure><p>查看 STATEMENT 格式日志:</p><ul><li><p>执行插入语句:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO tb_book VALUES(NULL,'Lucene','2088-05-01','0');</span><br></pre></td></tr></table></figure></li><li><p><code>cd /var/lib/mysql</code>:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-rw-r----- 1 mysql mysql 177 5月 23 21:08 mysqlbin.000001</span><br><span class="line">-rw-r----- 1 mysql mysql 18 5月 23 21:04 mysqlbin.index</span><br></pre></td></tr></table></figure><p>mysqlbin.index:该文件是日志索引文件 , 记录日志的文件名;</p><p>mysqlbing.000001:日志文件</p></li><li><p>查看日志内容:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqlbinlog mysqlbing.000001;</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%961.png" alt=""></p><p>日志结尾有 COMMIT</p></li></ul><p>查看 ROW 格式日志:</p><ul><li><p>修改配置:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 配置二进制日志的格式</span></span><br><span class="line">binlog_format=ROW</span><br></pre></td></tr></table></figure></li><li><p>插入数据:</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO tb_book VALUES(NULL,'SpringCloud实战','2088-05-05','0');</span><br></pre></td></tr></table></figure></li><li><p>查看日志内容:日志格式 ROW,直接查看数据是乱码,可以在 mysqlbinlog 后面加上参数 -vv</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqlbinlog -vv mysqlbin.000002</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%962.png" alt=""></p></li></ul><hr><h4 id="日志删除">日志删除</h4><p>对于比较繁忙的系统,生成日志量大,这些日志如果长时间不清除,将会占用大量的磁盘空间,需要删除日志</p><ul><li><p>Reset Master 指令删除全部 binlog 日志,删除之后,日志编号将从 xxxx.000001重新开始</p> <figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Reset Master-- MySQL指令</span><br></pre></td></tr></table></figure></li><li><p>执行指令 <code>PURGE MASTER LOGS TO 'mysqlbin.***</code>,该命令将删除 <code> ***</code> 编号之前的所有日志</p></li><li><p>执行指令 <code>PURGE MASTER LOGS BEFORE 'yyyy-mm-dd hh:mm:ss'</code> ,该命令将删除日志为 <code>yyyy-mm-dd hh:mm:ss</code> 之前产生的日志</p></li><li><p>设置参数 <code>--expire_logs_days=#</code>,此参数的含义是设置日志的过期天数,过了指定的天数后日志将会被自动删除,这样做有利于减少管理日志的工作量,配置 my.cnf 文件:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">log_bin=mysqlbin</span><br><span class="line">binlog_format=ROW</span><br><span class="line">--expire_logs_days=3</span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="数据恢复-2">数据恢复</h4><p>误删库或者表时,需要根据 binlog 进行数据恢复,</p><p>一般情况下数据库有定时的全量备份,假如每天 0 点定时备份,12 点误删了库,恢复流程:</p><ul><li>取最近一次全量备份,用备份恢复出一个临时库</li><li>从日志文件中取出凌晨 0 点之后的日志</li><li>把除了误删除数据的语句外日志,全部应用到临时库</li></ul><p>跳过误删除语句日志的方法:</p><ul><li>如果原实例没有使用 GTID 模式,只能在应用到包含 12 点的 binlog 文件的时候,先用 –stop-position 参数执行到误操作之前的日志,然后再用 –start-position 从误操作之后的日志继续执行</li><li>如果实例使用了 GTID 模式,假设误操作命令的 GTID 是 gtid1,那么只需要提交一个空事务先将这个 GTID 加到临时实例的 GTID 集合,之后按顺序执行 binlog 的时就会自动跳过误操作的语句</li></ul><hr><h3 id="查询日志">查询日志</h3><p>查询日志中记录了客户端的所有操作语句,而二进制日志不包含查询数据的 SQL 语句</p><p>默认情况下,查询日志是未开启的。如果需要开启查询日志,配置 my.cnf:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 该选项用来开启查询日志,可选值0或者1,0代表关闭,1代表开启 </span></span><br><span class="line">general_log=1</span><br><span class="line"><span class="comment"># 设置日志的文件名,如果没有指定,默认的文件名为host_name.log,存放在/var/lib/mysql</span></span><br><span class="line">general_log_file=mysql_query.log</span><br></pre></td></tr></table></figure><p>配置完毕之后,在数据库执行以下操作:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM tb_book;</span><br><span class="line">SELECT * FROM tb_book WHERE id = 1;</span><br><span class="line">UPDATE tb_book SET name = 'lucene入门指南' WHERE id = 5;</span><br><span class="line">SELECT * FROM tb_book WHERE id < 8</span><br></pre></td></tr></table></figure><p>执行完毕之后, 再次来查询日志文件:</p><p><img src="../image/post/MySQL-%E6%9F%A5%E8%AF%A2%E6%97%A5%E5%BF%97.png" alt=""></p><hr><h3 id="慢日志">慢日志</h3><p>慢查询日志记录所有执行时间超过 long_query_time 并且扫描记录数不小于 min_examined_row_limit 的所有的 SQL 语句的日志。long_query_time 默认为 10 秒,最小为 0, 精度到微秒</p><p>慢查询日志默认是关闭的,可以通过两个参数来控制慢查询日志,配置文件 <code>/etc/mysql/my.cnf</code>:</p><figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 该参数用来控制慢查询日志是否开启,可选值0或者1,0代表关闭,1代表开启 </span></span><br><span class="line">slow_query_log=1 </span><br><span class="line"></span><br><span class="line"><span class="comment"># 该参数用来指定慢查询日志的文件名,存放在 /var/lib/mysql</span></span><br><span class="line">slow_query_log_file=slow_query.log</span><br><span class="line"></span><br><span class="line"><span class="comment"># 该选项用来配置查询的时间限制,超过这个时间将认为值慢查询,将需要进行日志记录,默认10s</span></span><br><span class="line">long_query_time=10</span><br></pre></td></tr></table></figure><p>日志读取:</p><ul><li><p>直接通过 cat 指令查询该日志文件:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="built_in">cat</span> slow_query.log</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E6%85%A2%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%961.png" alt=""></p></li><li><p>如果慢查询日志内容很多,直接查看文件比较繁琐,可以借助 mysql 自带的 mysqldumpslow 工具对慢查询日志进行分类汇总:</p> <figure class="highlight sh"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysqldumpslow slow_query.log</span><br></pre></td></tr></table></figure><p><img src="../image/post/MySQL-%E6%85%A2%E6%97%A5%E5%BF%97%E8%AF%BB%E5%8F%962.png" alt=""></p></li></ul><hr>]]></content>
<summary type="html">DataBase-MySQL初级相关知识学习,以Hillos为纲,JavaNote为主体整理的相关笔记。</summary>
<category term="DataBaseing" scheme="https://jovehawking.fun/categories/DataBaseing/"/>
<category term="DataBase" scheme="https://jovehawking.fun/tags/DataBase/"/>
<category term="数据库" scheme="https://jovehawking.fun/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
<category term="MySQL初级" scheme="https://jovehawking.fun/tags/MySQL%E5%88%9D%E7%BA%A7/"/>
</entry>
<entry>
<title>JAVAWeb-Listener学习笔记</title>
<link href="https://jovehawking.fun/posts/e902e3a2.html"/>
<id>https://jovehawking.fun/posts/e902e3a2.html</id>
<published>2024-06-22T08:55:14.000Z</published>
<updated>2024-06-29T09:11:44.819Z</updated>
<content type="html"><![CDATA[<h1>Listener</h1><h2 id="Listener">Listener</h2><h3 id="观察者设计者">观察者设计者</h3><p>所有的监听器都是基于观察者设计模式的。</p><p>观察者模式通常由以下三部分组成:</p><ul><li><p>事件源:触发事件的对象。</p></li><li><p>事件:触发的动作,里面封装了事件源。</p></li><li><p>监听器:当事件源触发事件后,可以完成的功能。一般是一个接口,由使用者来实现。(此处的思想还涉及了一个策略模式)</p></li></ul><hr><h3 id="监听器分类">监听器分类</h3><p>在程序当中,我们可以对:对象的创建销毁、域对象中属性的变化、会话相关内容进行监听。</p><p>Servlet规范中共计8个监听器,<strong>监听器都是以接口形式提供</strong>,具体功能需要我们自己完成</p><h4 id="监听对象">监听对象</h4><ul><li><p>ServletContextListener:用于监听ServletContext对象的创建和销毁</p><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td>void contextInitialized(ServletContextEvent sce)</td><td>对象创建时执行该方法</td></tr><tr><td>void contextDestroyed(ServletContextEvent sce)</td><td>对象销毁时执行该方法</td></tr></tbody></table><p>参数ServletContextEvent 代表事件对象,事件对象中封装了事件源ServletContext,真正的事件指的是创建或者销毁ServletContext对象的操作</p></li><li><p>HttpSessionListener:用于监听HttpSession对象的创建和销毁</p><table><thead><tr><th>方法</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td>void sessionCreated(HttpSessionEvent se)</td><td style="text-align:left">对象创建时执行该方法</td></tr><tr><td>void sessionDestroyed(HttpSessionEvent se)</td><td style="text-align:left">对象销毁时执行该方法</td></tr></tbody></table><p>参数HttpSessionEvent 代表事件对象,事件对象中封装了事件源HttpSession,真正的事件指的是创建或者销毁HttpSession对象的操作</p></li><li><p>ServletRequestListener:用于监听ServletRequest对象的创建和销毁</p><table><thead><tr><th>方法</th><th style="text-align:left">作用</th></tr></thead><tbody><tr><td>void requestInitialized(ServletRequestEvent sre)</td><td style="text-align:left">对象创建时执行该方法</td></tr><tr><td>void requestDestroyed(ServletRequestEvent sre)</td><td style="text-align:left">对象销毁时执行该方法</td></tr></tbody></table><p>参数ServletRequestEvent 代表事件对象,事件对象中封装了事件源ServletRequest,真正的事件指的是创建或者销毁ServletRequest对象的操作</p></li></ul><hr><h4 id="监听域对象属性">监听域对象属性</h4><ul><li><p>ServletContextAttributeListener:用于监听ServletContext应用域中属性的变化</p><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td>void attributeAdded(ServletContextAttributeEvent event)</td><td>域中添加属性时执行该方法</td></tr><tr><td>void attributeRemoved(ServletContextAttributeEvent event)</td><td>域中移除属性时执行该方法</td></tr><tr><td>void attributeReplaced(ServletContextAttributeEvent event)</td><td>域中替换属性时执行该方法</td></tr></tbody></table><p>参数ServletContextAttributeEvent 代表事件对象,事件对象中封装了事件源ServletContext,真正的事件指的是添加、移除、替换应用域中属性的操作</p></li><li><p>HttpSessionAttributeListener:用于监听HttpSession会话域中属性的变化</p><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td>void attributeAdded(HttpSessionBindingEvent event)</td><td>域中添加属性时执行该方法</td></tr><tr><td>void attributeRemoved(HttpSessionBindingEvent event)</td><td>域中移除属性时执行该方法</td></tr><tr><td>void attributeReplaced(HttpSessionBindingEvent event)</td><td>域中替换属性时执行该方法</td></tr></tbody></table><p>参数HttpSessionBindingEvent 代表事件对象,事件对象中封装了事件源HttpSession,真正的事件指的是添加、移除、替换应用域中属性的操作</p></li><li><p>ServletRequestAttributeListener:用于监听ServletRequest请求域中属性的变化</p><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td>void attributeAdded(ServletRequestAttributeEvent srae)</td><td>域中添加属性时执行该方法</td></tr><tr><td>void attributeRemoved(ServletRequestAttributeEvent srae)</td><td>域中移除属性时执行该方法</td></tr><tr><td>void attributeReplaced(ServletRequestAttributeEvent srae)</td><td>域中替换属性时执行该方法</td></tr></tbody></table><p>参数ServletRequestAttributeEvent 代表事件对象,事件对象中封装了事件源ServletRequest,真正的事件指的是添加、移除、替换应用域中属性的操作</p></li><li><p>页面域对象没有监听器</p></li></ul><hr><h4 id="感知型监听器">感知型监听器</h4><p>监听会话相关的感知型监听器,和会话域相关的两个感知型监听器是无需配置(注解)的,可以直接编写代码</p><ul><li><p>HttpSessionBindingListener:用于感知对象和会话域绑定的监听器</p><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td>void valueBound(HttpSessionBindingEvent event)</td><td>数据添加到会话域中(绑定)时执行该方法</td></tr><tr><td>void valueUnbound(HttpSessionBindingEvent event)</td><td>数据从会话域中移除(解绑)时执行该方法</td></tr></tbody></table><p>参数HttpSessionBindingEvent 代表事件对象,事件对象中封装了事件源HttpSession,真正的事件指的是添加、移除、替换应用域中属性的操作</p></li><li><p>HttpSessionActivationListener:用于感知会话域中对象和钝化和活化的监听器</p><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td>void sessionWillPassivate(HttpSessionEvent se)</td><td>会话域中数据钝化时执行该方法</td></tr><tr><td>void sessionDidActivate(HttpSessionEvent se)</td><td>会话域中数据活化时执行该方法</td></tr></tbody></table></li></ul><hr><h3 id="监听器使用">监听器使用</h3><h4 id="ServletContextListener">ServletContextListener</h4><p>ServletContext对象的创建和销毁的监听器</p><p>注解方式:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebListener</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletContextListenerDemo</span> <span class="keyword">implements</span> <span class="title class_">ServletContextListener</span> {</span><br><span class="line"> <span class="comment">//创建时执行此方法</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">contextInitialized</span><span class="params">(ServletContextEvent sce)</span> {</span><br><span class="line"> System.out.println(<span class="string">"监听到对象的创建...."</span>);<span class="comment">//启动服务器就创建</span></span><br><span class="line"></span><br><span class="line"> <span class="type">ServletContext</span> <span class="variable">servletContext</span> <span class="operator">=</span> sce.getServletContext();</span><br><span class="line"> System.out.println(servletContext);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//销毁时执行的方法</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">contextDestroyed</span><span class="params">(ServletContextEvent sce)</span> {</span><br><span class="line"> System.out.println(<span class="string">"监听到对象的销毁..."</span>);<span class="comment">//关闭服务器就销毁</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>配置web.xml</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">web-app</span>></span></span><br><span class="line"><span class="comment"><!--配置监听器--></span></span><br><span class="line"> <span class="tag"><<span class="name">listener</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">listener-class</span>></span>listener.ServletContextAttributeListenerDemo<span class="tag"></<span class="name">listener-class</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">listener</span>></span></span><br><span class="line"><span class="tag"></<span class="name">web-app</span>></span></span><br></pre></td></tr></table></figure><hr><h4 id="ServletContextAttributeListener">ServletContextAttributeListener</h4><p>应用域对象中的属性变化的监听器</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletContextAttributeListenerDemo</span> <span class="keyword">implements</span> <span class="title class_">ServletContextAttributeListener</span>{</span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 向应用域对象中添加属性时执行此方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">attributeAdded</span><span class="params">(ServletContextAttributeEvent scae)</span> {</span><br><span class="line"> System.out.println(<span class="string">"监听到了属性的添加..."</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//获取应用域对象</span></span><br><span class="line"> <span class="type">ServletContext</span> <span class="variable">servletContext</span> <span class="operator">=</span> scae.getServletContext();</span><br><span class="line"> <span class="comment">//获取属性</span></span><br><span class="line"> <span class="type">Object</span> <span class="variable">value</span> <span class="operator">=</span> servletContext.getAttribute(<span class="string">"username"</span>);</span><br><span class="line"> System.out.println(value);<span class="comment">//zhangsan </span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 向应用域对象中替换属性时执行此方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">attributeReplaced</span><span class="params">(ServletContextAttributeEvent scae)</span> {</span><br><span class="line"> System.out.println(<span class="string">"监听到了属性的替换..."</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//获取应用域对象</span></span><br><span class="line"> <span class="type">ServletContext</span> <span class="variable">servletContext</span> <span class="operator">=</span> scae.getServletContext();</span><br><span class="line"> <span class="comment">//获取属性</span></span><br><span class="line"> <span class="type">Object</span> <span class="variable">value</span> <span class="operator">=</span> servletContext.getAttribute(<span class="string">"username"</span>);</span><br><span class="line"> System.out.println(value);<span class="comment">//lisi</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 向应用域对象中移除属性时执行此方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">attributeRemoved</span><span class="params">(ServletContextAttributeEvent scae)</span> {</span><br><span class="line"> System.out.println(<span class="string">"监听到了属性的移除..."</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//获取应用域对象</span></span><br><span class="line"> <span class="type">ServletContext</span> <span class="variable">servletContext</span> <span class="operator">=</span> scae.getServletContext();</span><br><span class="line"> <span class="comment">//获取属性</span></span><br><span class="line"> <span class="type">Object</span> <span class="variable">value</span> <span class="operator">=</span> servletContext.getAttribute(<span class="string">"username"</span>);</span><br><span class="line"> System.out.println(value);<span class="comment">//null</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletContextListenerDemo</span> <span class="keyword">implements</span> <span class="title class_">ServletContextListener</span>{</span><br><span class="line"> <span class="comment">//ServletContext对象创建的时候执行此方法</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">contextInitialized</span><span class="params">(ServletContextEvent sce)</span> {</span><br><span class="line"> System.out.println(<span class="string">"监听到了对象的创建..."</span>);</span><br><span class="line"> <span class="comment">//获取对象</span></span><br><span class="line"> <span class="type">ServletContext</span> <span class="variable">servletContext</span> <span class="operator">=</span> sce.getServletContext();</span><br><span class="line"></span><br><span class="line"> <span class="comment">//添加属性</span></span><br><span class="line"> servletContext.setAttribute(<span class="string">"username"</span>,<span class="string">"zhangsan"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//替换属性</span></span><br><span class="line"> servletContext.setAttribute(<span class="string">"username"</span>,<span class="string">"lisi"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//移除属性</span></span><br><span class="line"> servletContext.removeAttribute(<span class="string">"username"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">//ServletContext对象销毁的时候执行此方法</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">contextDestroyed</span><span class="params">(ServletContextEvent sce)</span> {</span><br><span class="line"> System.out.println(<span class="string">"监听到了对象的销毁..."</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>控制台输出:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">监听到了对象的创建...</span><br><span class="line">监听到了属性的添加...</span><br><span class="line">zhangsan</span><br><span class="line">监听到了属性的替换</span><br><span class="line">lisi</span><br><span class="line">监听到属性的移除</span><br><span class="line">null</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">JAVAWeb-Listener学习笔记</summary>
<category term="JAVAWebing" scheme="https://jovehawking.fun/categories/JAVAWebing/"/>
<category term="JAVAWeb" scheme="https://jovehawking.fun/tags/JAVAWeb/"/>
<category term="后端开发" scheme="https://jovehawking.fun/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>JAVAWeb-Filter学习笔记</title>
<link href="https://jovehawking.fun/posts/4f0925cd.html"/>
<id>https://jovehawking.fun/posts/4f0925cd.html</id>
<published>2024-06-22T08:55:05.000Z</published>
<updated>2024-06-29T09:10:10.248Z</updated>
<content type="html"><![CDATA[<h1>Filter</h1><h2 id="Filter">Filter</h2><h3 id="过滤器">过滤器</h3><p>Filter:过滤器,是 JavaWeb 三大组件之一,另外两个是 Servlet 和 Listener</p><p>工作流程:在程序访问服务器资源时,当一个请求到来,服务器首先判断是否有过滤器与去请求资源相关联,如果有过滤器可以将请求拦截下来,完成一些特定的功能,再由过滤器决定是否交给请求资源,如果没有就直接请求资源,响应同理</p><p>作用:过滤器一般用于完成通用的操作,例如:登录验证、统一编码处理、敏感字符过滤等</p><hr><h3 id="相关类">相关类</h3><h4 id="Filter-2">Filter</h4><p>Filter是一个接口,如果想实现过滤器的功能,必须实现该接口</p><ul><li><p>核心方法</p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td>void init(FilterConfig filterConfig)</td><td>初始化,开启过滤器</td></tr><tr><td>void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)</td><td>对请求资源和响应资源过滤</td></tr><tr><td>void destroy()</td><td>销毁过滤器</td></tr></tbody></table></li><li><p>配置方式</p><p>注解方式</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebFilter("/*")</span></span><br><span class="line">()内填拦截路径,<span class="comment">/*代表全部路径</span></span><br></pre></td></tr></table></figure><p>配置文件</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo01<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-class</span>></span>filter.FilterDemo01<span class="tag"></<span class="name">filter-class</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo01<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/*<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter-mapping</span>></span></span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="FilterChain">FilterChain</h4><ul><li><p>FilterChain 是一个接口,代表过滤器对象。由Servlet容器提供实现类对象,直接使用即可。</p></li><li><p>过滤器可以定义多个,就会组成过滤器链</p></li><li><p>核心方法:<code>void doFilter(ServletRequest request, ServletResponse response)</code> 用来放行方法</p><p>如果有多个过滤器,在第一个过滤器中调用下一个过滤器,以此类推,直到到达最终访问资源。<br>如果只有一个过滤器,放行时就会直接到达最终访问资源。</p></li></ul><h4 id="FilterConfig">FilterConfig</h4><p>FilterConfig 是一个接口,代表过滤器的配置对象,可以加载一些初始化参数</p><table><thead><tr><th>方法</th><th>作用</th></tr></thead><tbody><tr><td>String getFilterName()</td><td>获取过滤器对象名称</td></tr><tr><td>String getInitParameter(String name)</td><td>获取指定名称的初始化参数的值,不存在返回null</td></tr><tr><td>Enumeration<String> getInitParameterNames()</td><td>获取所有参数的名称</td></tr><tr><td>ServletContext getServletContext()</td><td>获取应用上下文对象</td></tr></tbody></table><hr><h3 id="Filter使用">Filter使用</h3><h4 id="设置页面编码">设置页面编码</h4><p>请求先被过滤器拦截进行相关操作</p><p>过滤器放行之后执行完目标资源,仍会回到过滤器中</p><ul><li><p>Filter 代码:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebFilter("/*")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FilterDemo01</span> <span class="keyword">implements</span> <span class="title class_">Filter</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException {</span><br><span class="line"> System.out.println(<span class="string">"filterDemo01拦截到请求..."</span>);</span><br><span class="line"> <span class="comment">//处理乱码</span></span><br><span class="line"> servletResponse.setContentType(<span class="string">"text/html;charset=UTF-8"</span>);</span><br><span class="line"> <span class="comment">//过滤器放行</span></span><br><span class="line"> filterChain.doFilter(servletRequest,servletResponse);</span><br><span class="line"> System.out.println(<span class="string">"filterDemo1放行之后,又回到了doFilter方法"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>Servlet 代码:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebServlet("/servletDemo01")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletDemo01</span> <span class="keyword">extends</span> <span class="title class_">HttpServlet</span> {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> System.out.println(<span class="string">"servletDemo01执行了..."</span>);</span><br><span class="line"> resp.getWriter().write(<span class="string">"servletDemo01执行了..."</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doPost</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> doGet(req,resp);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>控制台输出:</p> <figure class="highlight gcode"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">filterDem<span class="meta">o01</span>拦截到请求...</span><br><span class="line">servletDem<span class="meta">o01</span>执行了...</span><br><span class="line">filterDem<span class="meta">o1</span>放行之后,又回到了doFilter方法 </span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="多过滤器顺序">多过滤器顺序</h4><p>多个过滤器使用的顺序,取决于过滤器映射的顺序。</p><ul><li><p>两个 Filter 代码:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FilterDemo01</span> <span class="keyword">implements</span> <span class="title class_">Filter</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException {</span><br><span class="line"> System.out.println(<span class="string">"filterDemo01执行了..."</span>);</span><br><span class="line"> filterChain.doFilter(servletRequest,servletResponse);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FilterDemo02</span> <span class="keyword">implements</span> <span class="title class_">Filter</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException {</span><br><span class="line"> System.out.println(<span class="string">"filterDemo02执行了..."</span>);</span><br><span class="line"> filterChain.doFilter(servletRequest,servletResponse);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>Servlet代码:<code>System.out.println("servletDemo02执行了...");</code></p></li><li><p>web.xml配置:</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo01<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-class</span>></span>filter.FilterDemo01<span class="tag"></<span class="name">filter-class</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo01<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/*<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter-mapping</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo02<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-class</span>></span>filter.FilterDemo02<span class="tag"></<span class="name">filter-class</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo02<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/*<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter-mapping</span>></span></span><br></pre></td></tr></table></figure></li><li><p>控制台输出:</p> <figure class="highlight gcode"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">filterDem<span class="meta">o01</span>执行了</span><br><span class="line">filterDem<span class="meta">o02</span>执行了</span><br><span class="line">servletDem<span class="meta">o02</span>执行了...</span><br></pre></td></tr></table></figure></li></ul><p>在过滤器的配置中,有过滤器的声明和过滤器的映射两部分,到底是声明决定顺序,还是映射决定顺序呢?</p><p>答案是:<code><filter-mapping></code>的配置前后顺序决定过滤器的调用顺序,也就是由映射配置顺序决定。</p><hr><h4 id="Filter生命周期">Filter生命周期</h4><p>**创建:**当应用加载时实例化对象并执行init()初始化方法</p><p>**服务:**对象提供服务的过程,执行doFilter()方法</p><p><strong>销毁</strong>:当应用卸载时或服务器停止时对象销毁,执行destroy()方法</p><ul><li><p>Filter代码:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebFilter("/*")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FilterDemo03</span> <span class="keyword">implements</span> <span class="title class_">Filter</span>{</span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 初始化方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">(FilterConfig filterConfig)</span> {</span><br><span class="line"> System.out.println(<span class="string">"对象初始化成功了..."</span>);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 提供服务方法</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException {</span><br><span class="line"> System.out.println(<span class="string">"filterDemo03执行了..."</span>);</span><br><span class="line"> <span class="comment">//过滤器放行</span></span><br><span class="line"> filterChain.doFilter(servletRequest,servletResponse);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/*</span></span><br><span class="line"><span class="comment"> 对象销毁方法,关闭Tomcat服务器</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">destroy</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"对象销毁了..."</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure></li><li><p>Servlet 代码:<code>System.out.println("servletDemo03执行了...");</code></p></li><li><p>控制台输出:</p> <figure class="highlight erlang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">对象初始化成功了...</span><br><span class="line">filterDemo03执行了...</span><br><span class="line">servletDemo03执行了...</span><br><span class="line">对象销毁了</span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="FilterConfig使用">FilterConfig使用</h4><p>Filter初始化函数init的参数是FilterConfig 对象</p><ul><li><p>Filter代码:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FilterDemo04</span> <span class="keyword">implements</span> <span class="title class_">Filter</span>{</span><br><span class="line"></span><br><span class="line"><span class="comment">//初始化方法</span></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">init</span><span class="params">(FilterConfig filterConfig)</span> {</span><br><span class="line"> System.out.println(<span class="string">"对象初始化成功了..."</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//获取过滤器名称</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">filterName</span> <span class="operator">=</span> filterConfig.getFilterName();</span><br><span class="line"> System.out.println(filterName);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//根据name获取value</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> filterConfig.getInitParameter(<span class="string">"username"</span>);</span><br><span class="line"> System.out.println(username);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException {</span><br><span class="line"> System.out.println(<span class="string">"filterDemo04执行了..."</span>);</span><br><span class="line"> filterChain.doFilter(servletRequest,servletResponse);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">destroy</span><span class="params">()</span> {}</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>web.xml配置</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo04<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-class</span>></span>filter.FilterDemo04<span class="tag"></<span class="name">filter-class</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">init-param</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-name</span>></span>username<span class="tag"></<span class="name">param-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">param-value</span>></span>zhangsan<span class="tag"></<span class="name">param-value</span>></span></span><br><span class="line"> <span class="tag"></<span class="name">init-param</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>filterDemo04<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/*<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter-mapping</span>></span></span><br></pre></td></tr></table></figure></li><li><p>控制台输出:</p> <figure class="highlight erlang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">对象初始化成功了...</span><br><span class="line">filterDemo04</span><br><span class="line">zhangsan</span><br></pre></td></tr></table></figure></li></ul><hr><h3 id="拦截行为">拦截行为</h3><p>Filter过滤器默认拦截的是请求,但是在实际开发中,我们还有请求转发和请求包含,以及由服务器触发调用的全局错误页面。默认情况下过滤器是不参与过滤的,需要配置web.xml</p><p>开启功能后,当访问页面发生相关行为后,会执行过滤器的操作</p><p>五种拦截行为:</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"><!--配置过滤器--></span></span><br><span class="line"><span class="tag"><<span class="name">filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>FilterDemo5<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-class</span>></span>filter.FilterDemo5<span class="tag"></<span class="name">filter-class</span>></span></span><br><span class="line"> <span class="comment"><!--配置开启异步支持,当dispatcher配置ASYNC时,需要配置此行--></span></span><br><span class="line"> <span class="tag"><<span class="name">async-supported</span>></span>true<span class="tag"></<span class="name">async-supported</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>FilterDemo5<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/error.jsp<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"> <span class="comment"><!--<url-pattern>/index.jsp</url-pattern>--></span></span><br><span class="line"> <span class="comment"><!--过滤请求:默认值。--></span></span><br><span class="line"> <span class="tag"><<span class="name">dispatcher</span>></span>REQUEST<span class="tag"></<span class="name">dispatcher</span>></span></span><br><span class="line"> <span class="comment"><!--过滤全局错误页面:开启后,当由服务器调用全局错误页面时,过滤器工作--></span></span><br><span class="line"> <span class="tag"><<span class="name">dispatcher</span>></span>ERROR<span class="tag"></<span class="name">dispatcher</span>></span></span><br><span class="line"> <span class="comment"><!--过滤请求转发:开启后,当请求转发时,过滤器工作。--></span></span><br><span class="line"> <span class="tag"><<span class="name">dispatcher</span>></span>FORWARD<span class="tag"></<span class="name">dispatcher</span>></span></span><br><span class="line"> <span class="comment"><!--过滤请求包含:当请求包含时,过滤器工作。它只能过滤动态包含,jsp的include指令是静态包含--></span></span><br><span class="line"> <span class="tag"><<span class="name">dispatcher</span>></span>INCLUDE<span class="tag"></<span class="name">dispatcher</span>></span></span><br><span class="line"> <span class="comment"><!--过滤异步类型,它要求我们在filter标签中配置开启异步支持--></span></span><br><span class="line"> <span class="tag"><<span class="name">dispatcher</span>></span>ASYNC<span class="tag"></<span class="name">dispatcher</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter-mapping</span>></span></span><br><span class="line"></span><br></pre></td></tr></table></figure><ul><li><p>web.xml:</p> <figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="tag"><<span class="name">filter</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>FilterDemo5<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-class</span>></span>filter.FilterDemo5<span class="tag"></<span class="name">filter-class</span>></span></span><br><span class="line"> <span class="comment"><!--配置开启异步支持,当dispatcher配置ASYNC时,需要配置此行--></span></span><br><span class="line"> <span class="tag"><<span class="name">async-supported</span>></span>true<span class="tag"></<span class="name">async-supported</span>></span></span><br><span class="line"><span class="tag"></<span class="name">filter</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">filter-name</span>></span>FilterDemo5<span class="tag"></<span class="name">filter-name</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">url-pattern</span>></span>/error.jsp<span class="tag"></<span class="name">url-pattern</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">dispatcher</span>></span>ERROR<span class="tag"></<span class="name">dispatcher</span>></span></span><br><span class="line"><span class="tag"><<span class="name">filter-mapping</span>></span> </span><br></pre></td></tr></table></figure></li><li><p>ServletDemo03:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> System.out.println(<span class="string">"servletDemo03执行了..."</span>);</span><br><span class="line"> <span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> <span class="number">1</span>/ <span class="number">0</span>;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure></li><li><p>FilterDemo05:</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">FilterDemo05</span> <span class="keyword">implements</span> <span class="title class_">Filter</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">doFilter</span><span class="params">(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain)</span> <span class="keyword">throws</span> IOException, ServletException {</span><br><span class="line"> System.out.println(<span class="string">"filterDemo05执行了..."</span>);</span><br><span class="line"> <span class="comment">//放行</span></span><br><span class="line"> filterChain.doFilter(servletRequest,servletResponse);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>访问URL:<a href="http://localhost:8080/filter/servletDemo03">http://localhost:8080/filter/servletDemo03</a></p></li><li><p>控制台输出(注意输出顺序):</p> <figure class="highlight erlang"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">servletDemo03执行了...</span><br><span class="line">filterDemo05执行了...</span><br></pre></td></tr></table></figure></li></ul><hr><h3 id="对比Servlet">对比Servlet</h3><table><thead><tr><th>方法/类型</th><th>Servlet</th><th>Filter</th><th>备注</th></tr></thead><tbody><tr><td>初始化 方法</td><td>void init(ServletConfig);</td><td>void init(FilterConfig);</td><td>几乎一样,都是在web.xml中配置参数,用该对象的方法可以获取到。</td></tr><tr><td>提供服务方法</td><td>void service(request,response);</td><td>void dofilter(request,response,FilterChain)</td><td>Filter比Servlet多了一个FilterChain,它不仅能完成Servlet的功能,而且还可以决定程序是否能继续执行。所以过滤器比Servlet更为强大。 在Struts2中,核心控制器就是一个过滤器。</td></tr><tr><td>销毁方法</td><td>void destroy();</td><td>void destroy();</td><td>方法/类型</td></tr></tbody></table><hr><h2 id="Liste">Liste</h2>]]></content>
<summary type="html">JAVAWeb-Filter学习笔记</summary>
<category term="JAVAWebing" scheme="https://jovehawking.fun/categories/JAVAWebing/"/>
<category term="JAVAWeb" scheme="https://jovehawking.fun/tags/JAVAWeb/"/>
<category term="后端开发" scheme="https://jovehawking.fun/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
</entry>
<entry>
<title>JAVAWeb-Web相关存储技术学习笔记</title>
<link href="https://jovehawking.fun/posts/a64f54ba.html"/>
<id>https://jovehawking.fun/posts/a64f54ba.html</id>
<published>2024-06-22T08:54:25.000Z</published>
<updated>2024-06-25T22:40:27.832Z</updated>
<content type="html"><![CDATA[<h1>Cookie && Session</h1><h2 id="Cookie">Cookie</h2><h3 id="会话技术">会话技术</h3><p><strong>会话</strong>:浏览器和服务器之间的多次请求和响应</p><p>浏览器和服务器可能产生多次的请求和响应,从浏览器访问服务器开始,到访问服务器结束(关闭浏览器、到了过期时间),这期间产生的多次请求和响应加在一起称为浏览器和服务器之间的一次对话</p><p>作用:保存用户各自的数据(以浏览器为单位),在多次请求间实现数据共享</p><p><strong>常用的会话管理技术</strong>:</p><ul><li><p>Cookie:客户端会话管理技术,用户浏览的信息以键值对(key=value)的形式保存在浏览器上。如果没有关闭浏览器,再次访问服务器,会把 cookie 带到服务端,服务端就可以做相应的处理</p></li><li><p>Session:服务端会话管理技术。当客户端第一次请求 session 对象时,服务器为每一个浏览器开辟一块内存空间,并将通过特殊算法算出一个 session 的 ID,用来标识该 session 对象。由于内存空间是每一个浏览器独享的,所有用户在访问的时候,可以把信息保存在 session 对象中,同时服务器会把 sessionId 写到 cookie 中,再次访问的时候,浏览器会把 cookie(sessionId) 带过来,找到对应的 session 对象即可</p><p>tomcat 生成的 sessionID 叫做 jsessionID</p></li></ul><p>两者区别:</p><ul><li><p>Cookie 存储在客户端中,而 Session 存储在服务器上,相对来说 Session 安全性更高。如果要在 Cookie 中存储一些敏感信息,不要直接写入 Cookie,应该将 Cookie 信息加密然后使用到的时候再去服务器端解密</p></li><li><p>Cookie 一般用来保存用户信息,在 Cookie 中保存已经登录过得用户信息,下次访问网站的时候就不需要重新登录,因为用户登录的时候可以存放一个 Token 在 Cookie 中,下次登录的时候只需要根据 Token 值来查找用户即可(为了安全考虑,重新登录一般要将 Token 重写),所以登录一次网站后访问网站其他页面不需要重新登录</p></li><li><p>Session 通过服务端记录用户的状态,服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户</p></li><li><p>Cookie 只能存储 ASCII 码,而 Session 可以存储任何类型的数据</p></li></ul><p>参考文章:<a href="https://blog.csdn.net/weixin_43625577/article/details/92393581">https://blog.csdn.net/weixin_43625577/article/details/92393581</a></p><hr><h3 id="基本介绍">基本介绍</h3><p>Cookie:客户端会话管理技术,把要共享的数据保存到了客户端(也就是浏览器端)。每次请求时,把会话信息带到服务器,从而实现多次请求的数据共享。</p><p>作用:保存客户浏览器访问网站的相关内容(需要客户端不禁用 Cookie),从而在每次访问同一个内容时,先从本地缓存获取,使资源共享,提高效率。</p><p><img src="../image/post/Cookie%E7%B1%BB%E8%AE%B2%E8%A7%A3.png" alt=""></p><hr><h3 id="基本使用">基本使用</h3><h4 id="常用API">常用API</h4><ul><li><p><strong>Cookie属性:</strong></p><table><thead><tr><th>属性名称</th><th>属性作用</th><th>是否重要</th></tr></thead><tbody><tr><td>name</td><td>cookie的名称</td><td>必要属性</td></tr><tr><td>value</td><td>cookie的值(不能是中文)</td><td>必要属性</td></tr><tr><td>path</td><td>cookie的路径</td><td>重要</td></tr><tr><td>domain</td><td>cookie的域名</td><td>重要</td></tr><tr><td>maxAge</td><td>cookie的生存时间</td><td>重要</td></tr><tr><td>version</td><td>cookie的版本号</td><td>不重要</td></tr><tr><td>comment</td><td>cookie的说明</td><td>不重要</td></tr></tbody></table><p>注意:Cookie 有大小,个数限制。每个网站最多只能存20个 Cookie,且大小不能超过 4kb。同时所有网站的 Cookie 总数不超过300个。</p></li><li><p><strong>Cookie类API:</strong></p><ul><li><p><code>Cookie(String name, String value)</code> : 构造方法创建 Cookie 对象</p></li><li><p>Cookie 属性对应的 set 和 get 方法,name 属性被 final 修饰,没有 set 方法</p></li></ul></li><li><p>HttpServletResponse 类 API:</p><ul><li><code>void addCookie(Cookie cookie)</code>:向客户端添加 Cookie,Adds cookie to the response</li></ul></li><li><p>HttpServletRequest类API:</p><ul><li><code>Cookie[] getCookies()</code>:获取所有的 Cookie 对象,client sent with this request</li></ul></li></ul><hr><h4 id="有效期">有效期</h4><p>如果不设置过期时间,表示这个 Cookie 生命周期为浏览器会话期间,只要关闭浏览器窗口 Cookie 就消失,这种生命期为浏览会话期的 Cookie 被称为会话 Cookie,会话 Cookie 一般不保存在硬盘上而是保存在内存里。</p><p>如果设置过期时间,浏览器就会把 Cookie 保存到硬盘上,关闭后再次打开浏览器,这些 Cookie 依然有效直到超过设定的过期时间。存储在硬盘上的 Cookie 可以在<strong>不同的浏览器进程间共享</strong>,比如两个 IE 窗口,而对于保存在内存的 Cookie,不同的浏览器有不同的处理方式</p><p>设置 Cookie 存活时间 API:<code>void setMaxAge(int expiry)</code></p><ul><li>-1:默认。代表 Cookie 数据存到浏览器关闭(保存在浏览器文件中)</li><li>0:代表删除 Cookie,如果要删除 Cookie 要确保<strong>路径一致</strong>。</li><li>正整数:以秒为单位保存数据有有效时间(把缓存数据保存到磁盘中)</li></ul><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebServlet("/servletDemo01")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletDemo01</span> <span class="keyword">extends</span> <span class="title class_">HttpServlet</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> <span class="comment">//1.通过响应对象写出提示信息</span></span><br><span class="line"> resp.setContentType(<span class="string">"text/html;charset=UTF-8"</span>);</span><br><span class="line"> <span class="type">PrintWriter</span> <span class="variable">pw</span> <span class="operator">=</span> resp.getWriter();</span><br><span class="line"> pw.write(<span class="string">"欢迎访问本网站,您的最后访问时间为:<br>"</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//2.创建Cookie对象,用于记录最后访问时间</span></span><br><span class="line"> <span class="type">Cookie</span> <span class="variable">cookie</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Cookie</span>(<span class="string">"time"</span>,System.currentTimeMillis()+<span class="string">""</span>);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//3.设置最大存活时间</span></span><br><span class="line"> cookie.setMaxAge(<span class="number">3600</span>);</span><br><span class="line"> <span class="comment">//cookie.setMaxAge(0); // 立即清除</span></span><br><span class="line"></span><br><span class="line"> <span class="comment">//4.将cookie对象添加到客户端</span></span><br><span class="line"> resp.addCookie(cookie);</span><br><span class="line"></span><br><span class="line"> <span class="comment">//5.获取cookie</span></span><br><span class="line"> Cookie[] cookies = req.getCookies();</span><br><span class="line"> <span class="keyword">for</span>(Cookie c : cookies) {</span><br><span class="line"> <span class="keyword">if</span>(<span class="string">"time"</span>.equals(c.getName())) {</span><br><span class="line"> <span class="comment">//6.获取cookie对象中的value,进行写出</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">value</span> <span class="operator">=</span> c.getValue();</span><br><span class="line"> <span class="type">SimpleDateFormat</span> <span class="variable">sdf</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SimpleDateFormat</span>(<span class="string">"yyyy-MM-dd HH:mm:ss"</span>);</span><br><span class="line"> pw.write(sdf.format(Long.parseLong(value)));</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doPost</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> doGet(req,resp);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><hr><h4 id="有效路径">有效路径</h4><p><code>setPath(String url)</code> : Cookie 设置有效路径</p><p>有效路径作用 :</p><ol><li>保证不会携带别的网站/项目里面的 Cookie 到我们自己的项目</li><li>路径不一样,Cookie 的 key 可以相同</li><li>保证自己的项目可以合理的利用自己项目的 Cookie</li></ol><p>判断路径是否携带 Cookie:请求资源 URI.startWith(cookie的path),返回 true 就带</p><table><thead><tr><th>访问URL</th><th>URI部分</th><th>Cookie的Path</th><th>是否携带Cookie</th><th>能否取到Cookie</th></tr></thead><tbody><tr><td><a href="http://localhost:8080/servlet/servletDemo02">servletDemo02</a></td><td>/servlet/servletDemo02</td><td>/servlet/</td><td>带</td><td>能取到</td></tr><tr><td><a href="http://localhost:8080/servlet/servletDemo03">servletDemo03</a></td><td>/servlet/servletDemo03</td><td>/servlet/</td><td>带</td><td>能取到</td></tr><tr><td><a href="http://localhost:8080/servlet/aaa/servletDemo03">servletDemo04</a></td><td>/servlet/aaa/servletDemo04</td><td>/servlet/</td><td>带</td><td>能取到</td></tr><tr><td><a href="http://localhost:8080/bbb/servletDemo03">servletDemo05</a></td><td>/bbb/servletDemo04</td><td>/servlet/</td><td>不带</td><td>不能取到</td></tr></tbody></table><p>只有当访问资源的 url 包含此 cookie 的有效 path 的时候,才会携带这个 cookie</p><p>想要当前项目下的 Servlet 可以使用该 cookie,一般设置:<code>cookie.setPath(request.getContextPath())</code></p><hr><h4 id="安全性">安全性</h4><p>如果 Cookie 中设置了 HttpOnly 属性,通过 js 脚本将无法读取到 cookie 信息,这样能有效的防止 XSS 攻击,窃取 cookie 内容,这样就增加了安全性,即便是这样,也不要将重要信息存入cookie。</p><p>XSS 全称 Cross SiteScript,跨站脚本攻击,是Web程序中常见的漏洞,XSS 属于被动式且用于客户端的攻击方式,所以容易被忽略其危害性。其原理是攻击者向有 XSS 漏洞的网站中输入(传入)恶意的 HTML 代码,当其它用户浏览该网站时,这段HTML代码会自动执行,从而达到攻击的目的。如盗取用户 Cookie、破坏页面结构、重定向到其它网站等。</p><hr><h2 id="Session">Session</h2><h3 id="基本介绍-2">基本介绍</h3><p>Session:服务器端会话管理技术,本质也是采用客户端会话管理技术,不过在客户端保存的是一个特殊标识,共享的数据保存到了服务器的内存对象中。每次请求时,会将特殊标识带到服务器端,根据标识来找到对应的内存空间,从而实现数据共享。简单说它就是一个服务端会话对象,用于存储用户的会话数据。</p><p>Session 域(会话域)对象是 Servlet 规范中四大域对象之一,并且它也是用于实现数据共享的</p><table><thead><tr><th>域对象</th><th>功能</th><th>创建</th><th>销毁</th><th>使用场景</th></tr></thead><tbody><tr><td>ServletContext</td><td>应用域</td><td>服务器启动</td><td>服务器关闭</td><td>在整个应用之间实现数据共享<br />(记录网站访问次数,聊天室)</td></tr><tr><td>ServletRequest</td><td>请求域</td><td>请求到来</td><td>响应了这个请求</td><td>在当前请求或者请求转发之间实现数据共享</td></tr><tr><td>HttpSession</td><td>会话域</td><td>getSession()</td><td>session过期,调用invalidate(),服务器关闭</td><td>在当前会话范围中实现数据共享,可以在多次请求中实现数据共享。<br />(验证码校验, 保存用户登录状态等)</td></tr></tbody></table><hr><h3 id="基本使用-2">基本使用</h3><h4 id="获取会话">获取会话</h4><p>HttpServletRequest类获取Session:</p><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td>HttpSession getSession()</td><td>获取HttpSession对象</td></tr><tr><td>HttpSession getSession(boolean creat)</td><td>获取HttpSession对象,未获取到是否自动创建</td></tr></tbody></table><img src="../image/post/Session%E8%8E%B7%E5%8F%96%E7%9A%84%E4%B8%A4%E4%B8%AA%E6%96%B9%E6%B3%95.png" style="zoom: 80%;" /><hr><h4 id="常用API-2">常用API</h4><table><thead><tr><th>方法</th><th>说明</th></tr></thead><tbody><tr><td>void setAttribute(String name, Object value)</td><td>设置会话域中的数据</td></tr><tr><td>Object getAttribute(String name)</td><td>获取指定名称的会话域数据</td></tr><tr><td>Enumeration<String> getAttributeNames()</td><td>获取所有会话域所有属性的名称</td></tr><tr><td>void removeAttribute(String name)</td><td>移除会话域中指定名称的数据</td></tr><tr><td>String getId()</td><td>获取唯一标识名称,Jsessionid的值</td></tr><tr><td>void invalidate()</td><td>立即失效session</td></tr></tbody></table><hr><h4 id="实现会话">实现会话</h4><p>通过第一个Servlet设置共享的数据用户名,并在第二个Servlet获取到。</p><p>项目执行完以后,去浏览器抓包,Request Headers 中的 Cookie JSESSIONID的值是一样的</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebServlet("/servletDemo01")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletDemo01</span> <span class="keyword">extends</span> <span class="title class_">HttpServlet</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> <span class="comment">//1.获取请求的用户名</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">username</span> <span class="operator">=</span> req.getParameter(<span class="string">"username"</span>);</span><br><span class="line"> <span class="comment">//2.获取HttpSession的对象</span></span><br><span class="line"> <span class="type">HttpSession</span> <span class="variable">session</span> <span class="operator">=</span> req.getSession();</span><br><span class="line"> System.out.println(session);</span><br><span class="line"> System.out.println(session.getId());</span><br><span class="line"> <span class="comment">//3.将用户名信息添加到共享数据中</span></span><br><span class="line"> session.setAttribute(<span class="string">"username"</span>,username);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doPost</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> doGet(req,resp);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebServlet("/servletDemo02")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletDemo02</span> <span class="keyword">extends</span> <span class="title class_">HttpServlet</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> <span class="comment">//1.获取HttpSession对象</span></span><br><span class="line"> <span class="type">HttpSession</span> <span class="variable">session</span> <span class="operator">=</span> req.getSession();</span><br><span class="line"> <span class="comment">//2.获取共享数据</span></span><br><span class="line"> <span class="type">Object</span> <span class="variable">username</span> <span class="operator">=</span> session.getAttribute(<span class="string">"username"</span>);</span><br><span class="line"> <span class="comment">//3.将数据响应给浏览器</span></span><br><span class="line"> resp.getWriter().write(username+<span class="string">""</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doPost</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> doGet(req,resp);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><h4 id="生命周期">生命周期</h4><p>Session 的创建:一个常见的错误是以为 Session 在有客户端访问时就被创建,事实是直到某 server 端程序(如Servlet)调用 <code>HttpServletRequest.getSession(true)</code> 这样的语句时才会被创建</p><p>Session 在以下情况会被删除:</p><ul><li>程序调用 HttpSession.invalidate()</li><li>距离上一次收到客户端发送的 session id 时间间隔超过了 session 的最大有效时间</li><li>服务器进程被停止</li></ul><p>注意事项:</p><ul><li>客户端只保存 sessionID 到 cookie 中,而不会保存 session</li><li>关闭浏览器只会使存储在客户端浏览器内存中的 cookie 失效,不会使服务器端的 session 对象失效,同样也不会使已经保存到硬盘上的持久化cookie消失</li></ul><p>打开两个浏览器窗口访问应用程序会使用的是不同的session,通常 session cookie 是不能跨窗口使用,当新开了一个浏览器窗口进入相同页面时,系统会赋予一个新的 session id,实现跨窗口信息共享:</p><ul><li>先把 session id 保存在 persistent cookie 中(通过设置session的最大有效时间)</li><li>在新窗口中读出来,就可以得到上一个窗口的 session id,这样通过 session cookie 和 persistent cookie 的结合就可以实现跨窗口的会话跟踪</li></ul><hr><h3 id="会话问题">会话问题</h3><h4 id="禁用Cookie">禁用Cookie</h4><p>浏览器禁用Cookie解决办法:</p><ul><li><p>方式一:通过提示信息告知用户</p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@WebServlet("/servletDemo03")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ServletDemo03</span> <span class="keyword">extends</span> <span class="title class_">HttpServlet</span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> <span class="comment">//1.获取HttpSession对象</span></span><br><span class="line"> <span class="type">HttpSession</span> <span class="variable">session</span> <span class="operator">=</span> req.getSession(<span class="literal">false</span>);</span><br><span class="line"> System.out.println(session);</span><br><span class="line"> <span class="keyword">if</span>(session == <span class="literal">null</span>) {</span><br><span class="line"> resp.setContentType(<span class="string">"text/html;charset=UTF-8"</span>);</span><br><span class="line"> resp.getWriter().write(<span class="string">"为了不影响正常的使用,请不要禁用浏览器的Cookie~"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doPost</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> doGet(req,resp);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure></li><li><p>方式二:访问时拼接 jsessionid 标识,通过 encodeURL() 方法<strong>重写地址</strong></p> <figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Override</span></span><br><span class="line"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">doGet</span><span class="params">(HttpServletRequest req, HttpServletResponse resp)</span> <span class="keyword">throws</span> ServletException, IOException {</span><br><span class="line"> <span class="type">HttpSession</span> <span class="variable">session</span> <span class="operator">=</span> req.getSession();</span><br><span class="line"> <span class="comment">//实现url重写 相当于在地址栏后面拼接了一个jsessionid</span></span><br><span class="line"> resp.getWriter().write(<span class="string">"<a href='"</span>+ resp.encodeURL</span><br><span class="line"> (<span class="string">"http://localhost:8080/session/servletDemo03"</span>) +</span><br><span class="line"> <span class="string">"'>go servletDemo03</a>"</span>);</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure></li></ul><hr><h4 id="钝化活化">钝化活化</h4><p>Session 存放在服务器端的内存中,可以做持久化管理。</p><p>钝化:序列化,持久态。把长时间不用,但还不到过期时间的 HttpSession 进行序列化写到磁盘上。</p><p>活化:相反的状态</p><p>何时钝化:</p><ul><li>当访问量很大时,服务器会根据getLastAccessTime来进行排序,对长时间不用,但是还没到过期时间的HttpSession进行序列化(持久化)</li><li>当服务器进行重启的时候,为了保持客户HttpSession中的数据,也要对HttpSession进行序列化(持久化)</li></ul><p>注意:</p><ul><li>HttpSession的持久化由服务器来负责管理,我们不用关心</li><li>只有实现了序列化接口的类才能被序列化</li></ul>]]></content>
<summary type="html">JAVAWeb-Cookies、Session相关学习笔记</summary>
<category term="JAVAWebing" scheme="https://jovehawking.fun/categories/JAVAWebing/"/>
<category term="JAVAWeb" scheme="https://jovehawking.fun/tags/JAVAWeb/"/>
<category term="后端开发" scheme="https://jovehawking.fun/tags/%E5%90%8E%E7%AB%AF%E5%BC%80%E5%8F%91/"/>
</entry>
</feed>