-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathatom.xml
532 lines (264 loc) · 365 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
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Hexo & github</title>
<link href="http://example.com/atom.xml" rel="self"/>
<link href="http://example.com/"/>
<updated>2023-08-06T09:40:43.889Z</updated>
<id>http://example.com/</id>
<author>
<name>其然乐衣</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title></title>
<link href="http://example.com/2023/08/06/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8-%E5%8E%9F%E5%AD%90%E6%80%A7%E3%80%81%E5%8F%AF%E8%A7%81%E6%80%A7%E3%80%81%E6%9C%89%E5%BA%8F%E6%80%A7/"/>
<id>http://example.com/2023/08/06/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8-%E5%8E%9F%E5%AD%90%E6%80%A7%E3%80%81%E5%8F%AF%E8%A7%81%E6%80%A7%E3%80%81%E6%9C%89%E5%BA%8F%E6%80%A7/</id>
<published>2023-08-06T09:38:57.547Z</published>
<updated>2023-08-06T09:40:43.889Z</updated>
<content type="html"><![CDATA[<h1 id="多线程篇-线程安全-原子性-可见性-有序性解析"><a class="markdownIt-Anchor" href="#多线程篇-线程安全-原子性-可见性-有序性解析"></a> 多线程篇-线程安全-原子性、可见性、有序性解析</h1><p>在程序中使用多线程的目的是什么?</p><blockquote><p>1、提高效率,增加任务的吞吐量<br>2、提升CPU等资源的利用率,减少CPU的空转</p></blockquote><p>多线程的应用在日常开发中很多,带来了很多的便利,让我们以前研究下在多线程场景中要注意问题吧,一般主要从这三个方面考虑</p><blockquote><p>1、原子性<br>2、可见性<br>3、有序性</p></blockquote><p>如果不能保证原子性、可见性和顺序性会有什么问题?这些问题怎么解决呢?让我们一起来看下</p><h2 id="一-原子性"><a class="markdownIt-Anchor" href="#一-原子性"></a> <strong>一、原子性</strong></h2><p><strong>原子性的操作是不可被中断的一个或一系列操作。</strong></p><p>个人理解,严格的原子性的操作,其他线程获取操作的变量时,<strong>只能获取操作前的变量值和操作后的变量值</strong>,不能获取到操作过程中的中间值,在操作过程中其他操作需要获取变量值,需要进入<strong>阻塞状态</strong>等待操作结束。</p><p>如果不能保证原子性会有什么问题呢</p><p>让我们一块看一个简单例子吧</p><p>首先写一个简单的线程,代码如下</p><figure class="highlight java"><table><tbody><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ThreadDemo</span> <span class="keyword">implements</span> <span class="title class_">Runnable</span>{</span><br><span class="line"> <span class="type">int</span> <span class="variable">no</span> <span class="operator">=</span> <span class="number">0</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"> no++;</span><br><span class="line"> System.out.println(no);</span><br><span class="line"> }</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><br><span class="line"> <span class="type">ThreadDemo</span> <span class="variable">demo</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ThreadDemo</span>();</span><br><span class="line"> <span class="keyword">for</span>(<span class="type">int</span> i=<span class="number">0</span>;i<<span class="number">1000</span>;i++)</span><br><span class="line"> {</span><br><span class="line"> <span class="type">Thread</span> <span class="variable">task</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Thread</span>(demo);</span><br><span class="line"> task.start();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>Main函数中启动了1000个线程,每个线程都对no进行了一次+1操作,理想情况下no最后的结果应该是1000,可是我的运行最终结果只有996,执行了1000次+1操作,最后的结果为什么不是1000呢,这就要说到no++操作不是原子的问题了(no可见性的问题在下一个小结讨论)</p><p>先对no++这个操作分解,可以分为三步:取值,加一,赋值,这三个操作都是原子的,不过合在一起就不行了。两个线程A、B一起来操作no,no初始值是1,线程A读取no值是1,然后做no+1,这时线程B对 no取值还是1,然后A将2赋值给no,B操作no+1结果是2,也将2赋值给no。对1做了两次+1操作,最后结果是2。这个过程可以参考下图</p><p><img src="https://pic2.zhimg.com/80/v2-877c8a1698a75c0846f90a8cdf17b50d_720w.webp" alt="img"></p><p>所以,需要保证这种不可分割操作的原子性,那要怎么做才能保证原子性呢,有两种方式</p><p><strong>1、加锁synchronized,保证同一时间只有一个线程操作变量,其他线程需等待操作结束才能使用临界资源</strong></p><p><strong>2、使用CAS操作,变量计算前保留一份旧值a,计算完成后结果值为b,把b刷到内存之前先比较a是否和内存中变量一致,如果一致,就把内存中的变量赋值为b,不一样,重新获取内存中变量值,重复一遍操作,一直到a和内存中一致,操作结束。</strong></p><p><strong>Lock和原子类(AtomicInteger等)是通过使用unsafe的compareAndSwap方法实现CAS操作保证原子性的。</strong></p><h2 id="二-可见性"><a class="markdownIt-Anchor" href="#二-可见性"></a> 二、可见性</h2><p>线程变量的可见性问题,需要从操作系统的CPU、缓存、内存的矛盾开始说起。读写性能上 <strong>CPU>缓存>内存>I/O</strong>。CPU/缓存/内存的结构看下图。</p><p><img src="https://pic3.zhimg.com/80/v2-997799f7ae637d414a38e8b59b8cf9ce_720w.webp" alt="img"></p><p>CPU和内存之间隔着缓存和CPU寄存器。缓存还分为一级、二级、三级缓存。CPU的读写性能上要大于内存,为了提高效率会将数据先取到缓存中,<strong>CPU处理完数据后会先放到缓存中,然后同步到内存中。</strong></p><p>如果不理解CPU缓存这部分内容的话,可以简单的认为每个线程都有自己的本地工作内存,变量会先缓存到<strong>本地工作内存</strong>中使用,修改后会先修改工作内存中的存储,然后在同步到主内存中。结构如下图</p><p><img src="https://pic4.zhimg.com/80/v2-1aa035a190a0ae36aca940fcafd5655f_720w.webp" alt="img"></p><p>这种内存结构会引起什么问题呢,现在有一个变量var,线程A对var做了一次修改,刚放到缓存(工作内存)还未同步到内存时,另外一个线程B也来使用var,读取到的还是var未修改值。</p><p>共享的变量需要保证可见性,怎么保证共享变量的可见性呢</p><p><strong>1、加锁(加锁是万能的操作)synchronized和Lock都可以保证。</strong></p><p><strong>线程在加锁时,会清空工作内存中共享变量的值,共享变量使用是需要从主内存中重新获取。</strong></p><p><strong>线程解锁是,会把共享变量重新刷新到主内存中。</strong></p><p><strong>2、使用volatile修饰共享变量,volatile修饰的共享变量在修改后会立即被更新到内存中,其他线程使用共享变量会去内存中读取</strong></p><p>优先使用volatile来解决可见性问题,加锁需要消耗的资源太多。</p><h2 id="三-有序性"><a class="markdownIt-Anchor" href="#三-有序性"></a> 三、有序性</h2><p>为了优化程序性能,编译器、处理器和运行时会对代码指令进行重排,重排过程中会遵循as-if-serial语义,即不影响单线程的运行结果。</p><p><strong>扩展一下 指令重排为什么会提高程序性能呢,我个人理解是CPU是多核处理的,为了保证处理器资源的充分利用,对代码指令进行乱序处理,即可以多个处理器并行处理指令,防止不相关的指令需要等待上一个指令结束才能开始。</strong></p><p>代码执行顺序被重排会是什么效果呢,举个简单例子</p><figure class="highlight jsp"><table><tbody><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="type">int</span> <span class="variable">a</span> <span class="operator">=</span><span class="number">0</span>;</span><br><span class="line">a=<span class="number">2</span>;</span><br><span class="line"><span class="type">int</span> <span class="variable">b</span> <span class="operator">=</span><span class="number">1</span>;</span><br><span class="line"><span class="type">int</span> c= a+b;</span><br></pre></td></tr></tbody></table></figure><p>这段代码中变量a和b 是相互不影响的,优化后可以是如下代码,只要保证执行结果不变,有依赖的变量c在变量a和b之后处理即可</p><figure class="highlight text"><table><tbody><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">int b =1;</span><br><span class="line">int a =0;</span><br><span class="line">a=2;</span><br><span class="line">int c= a+b;</span><br></pre></td></tr></tbody></table></figure><p>在单线程中这样是没有问题的,如果是多线程呢,看下如下代码</p><figure class="highlight java"><table><tbody><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_">Task</span> {</span><br><span class="line"> <span class="keyword">static</span> <span class="type">Object</span> <span class="variable">val</span> <span class="operator">=</span><span class="literal">null</span>;</span><br><span class="line"> <span class="keyword">static</span> <span class="type">boolean</span> <span class="variable">finish</span> <span class="operator">=</span><span class="literal">false</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><br><span class="line"> <span class="type">Runnable</span> <span class="variable">task1</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Runnable</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"> <span class="keyword">if</span>(finish)</span><br><span class="line"> {</span><br><span class="line"> System.out.println(val.toString());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"> <span class="type">Runnable</span> <span class="variable">task2</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Runnable</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"> val = <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"> finish =<span class="literal">true</span>;</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></tbody></table></figure><p>分别创建两个任务task1和task2,他们共享两个变量val和finish,按现在的顺序执行的话,task1不会出现val为null时被使用的情况。不过进行了指令重排之后呢,task2中val和finish操作顺序调整对单线程来说是没有任何影响的,所以task2的代码可能会变成</p><figure class="highlight java"><table><tbody><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="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"> finish =<span class="literal">true</span>;</span><br><span class="line"> val = <span class="keyword">new</span> <span class="title class_">Object</span>();</span><br><span class="line"> }</span><br></pre></td></tr></tbody></table></figure><p>这样task1中,就会出现finish为true,val为null的情况了。</p><p>那么怎么保证多个线程中的代码顺序一致性呢</p><p><strong>1、加锁(还是加锁)synchronized和Lock,保证同一时刻只有一个线程进行操作</strong></p><p><strong>2、使用volatile修饰变量,在JMM中volatile的读和写都会插入内存屏障来禁止处理器的重排</strong></p><p>这样原子性、可见性、有序性就基本讲完了,其中有很多的知识点没有详细的说,例如:</p><p>CAS、volatile、synchronized、lock等等,这些会在后边的文章中慢慢研究。</p><p>注:<strong>如果弄不清楚原子性和可见性的区别,只要记住下边两点内容</strong></p><p><strong>1、原子性针对完整的操作过程,其他操作只能获取操作前或操作后的变量数据</strong></p><p><strong>2、可见性主要是变量修改,变量修改后,马上刷新到内存中,而其他线程能感知到变量的修改</strong></p>]]></content>
<summary type="html"><h1 id="多线程篇-线程安全-原子性-可见性-有序性解析"><a class="markdownIt-Anchor" href="#多线程篇-线程安全-原子性-可见性-有序性解析"></a> 多线程篇-线程安全-原子性、可见性、有序性解析</h1>
<p>在程序中使用多线程</summary>
</entry>
<entry>
<title>RabbitMQ架构模型</title>
<link href="http://example.com/2023/07/29/MQ/RabbitMQ%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%9E%8B/"/>
<id>http://example.com/2023/07/29/MQ/RabbitMQ%E6%9E%B6%E6%9E%84%E6%A8%A1%E5%9E%8B/</id>
<published>2023-07-29T03:02:23.816Z</published>
<updated>2023-07-29T10:21:01.801Z</updated>
<content type="html"><![CDATA[<p>RabbitMQ是采用Erlang语言实现AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件。</p><h1 id="rabbitmq架构模型"><a class="markdownIt-Anchor" href="#rabbitmq架构模型"></a> RabbitMQ架构模型</h1><p><img src="../../images/image-20230729110344452.png" alt="image-20230729110344452"></p><p><img src="../../images/image-20230729110358056.png" alt="image-20230729110358056"></p><h2 id="producer生产者"><a class="markdownIt-Anchor" href="#producer生产者"></a> # Producer:生产者</h2><p>就是投递消息的一方,生产者创建消息发布到RabbitMQ中。</p><p>消息包含2部分:</p><p><strong>消息体(payload):</strong></p><p>一般是一个带有业务逻辑结构的数据,比如一个JSON字符串。</p><p><strong>标签(Label)</strong></p><p>用来描述这条消息,比如一个交换器的名称和一个路由键。</p><p>生产者把消息交由RabbitMQ,RabbitMQ之后会根据标签吧消息发送给感兴趣的消费者</p><h2 id="consumer消费方"><a class="markdownIt-Anchor" href="#consumer消费方"></a> # Consumer:消费方</h2><p>就是接受消息一方</p><p>消费者连接到RabbitMQ服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体(payload)。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体。</p><p>消息如果只是存储在队列里是没有任何用处的。被应用消费掉,消息的价值才能够体现。在 AMQP 0-9-1 模型中,有两种途径可以达到此目的:</p><ol><li>将消息投递给应用 (“push API”)</li><li>应用根据需要主动获取消息 (“pull API”)</li></ol><h2 id="broker服务节点"><a class="markdownIt-Anchor" href="#broker服务节点"></a> # Broker:服务节点</h2><p>可以将一个RabbitMQ Broker看作一台RabbitMQ服务器。</p><p><img src="../../images/image-20230729110532514.png" alt="image-20230729110532514"></p><h2 id="queue队列"><a class="markdownIt-Anchor" href="#queue队列"></a> # Queue队列:</h2><p>用于存储消息</p><p><img src="../../images/image-20230729110551724.png" alt="image-20230729110551724"></p><h3 id="队列属性"><a class="markdownIt-Anchor" href="#队列属性"></a> <strong>队列属性</strong></h3><p>队列跟交换机共享某些属性,但是队列也有一些另外的属性。</p><ul><li>Name</li><li>Durable(消息代理重启后,队列依旧存在)</li><li>Exclusive(只被一个连接(connection)使用,而且当连接关闭后队列即被删除)</li><li>Auto-delete(当最后一个消费者退订后即被删除)</li><li>Arguments(一些消息代理用他来完成类似与 TTL 的某些额外功能)</li></ul><h3 id="队列创建"><a class="markdownIt-Anchor" href="#队列创建"></a> 队列创建</h3><p>队列在声明(declare)后才能被使用。</p><p>如果一个队列尚不存在,声明一个队列会创建它。</p><p>如果声明的队列已经存在,并且属性完全相同,那么此次声明不会对原有队列产生任何影响。</p><p>如果声明中的属性与已存在队列的属性有差异,那么一个错误代码为 406 的通道级异常就会被抛出。</p><h3 id="队列持久化"><a class="markdownIt-Anchor" href="#队列持久化"></a> 队列持久化</h3><p>持久化队列(Durable queues)会被存储在磁盘上,当消息代理(broker)重启的时候,它依旧存在。没有被持久化的队列称作暂存队列(Transient queues)。并不是所有的场景和案例都需要将队列持久化。</p><p>持久化的队列并不会使得路由到它的消息也具有持久性。倘若消息代理挂掉了,重新启动,那么在重启的过程中持久化队列会被重新声明,无论怎样,只有经过持久化的消息才能被重新恢复。</p><h2 id="exchange交换器"><a class="markdownIt-Anchor" href="#exchange交换器"></a> # Exchange:交换器</h2><p>生产者将消息发送到Exchange,由交换器将消息路由到一个或者多个队列中,如果路由不到,或许返回给生产者,或许直接丢弃。</p><p><img src="../../images/image-20230729110643513.png" alt="image-20230729110643513"></p><h3 id="交换机有两种状态"><a class="markdownIt-Anchor" href="#交换机有两种状态"></a> 交换机有两种状态:</h3><h4 id="持久durable"><a class="markdownIt-Anchor" href="#持久durable"></a> 持久(durable):</h4><p>持久化的交换机会在消息代理(broker)重启后依旧存在,而暂存的交换机则不会(它们需要在代理再次上线后重新被声明)。</p><p>并不是所有的应用场景都需要持久化的交换机。</p><h4 id="暂存transient"><a class="markdownIt-Anchor" href="#暂存transient"></a> 暂存(transient)</h4><h3 id="类型"><a class="markdownIt-Anchor" href="#类型"></a> 类型:</h3><h4 id="fanout广播"><a class="markdownIt-Anchor" href="#fanout广播"></a> –fanout广播</h4><p>将所有发送到交换器的消息路由到所有与该交换器绑定对的队列中【扇形交换机,不再判断routeKey,直接将消息分发到所有绑定的队列】。</p><h4 id="direct直连"><a class="markdownIt-Anchor" href="#direct直连"></a> –direct直连</h4><p>把消息路由到哪些BindKey和RoutingKey完全匹配的队列中。【判断touteKey的规则是完全 匹配模式,即发送消息时指定的routeKey要等于绑定的routeKey】。</p><h4 id="topic主题"><a class="markdownIt-Anchor" href="#topic主题"></a> –topic主题</h4><p>主题交换机(topic exchanges)通过对消息的路由键和队列到交换机的绑定模式之间的匹配,将消息路由给一个或多个队列。主题交换机经常用来实现各种分发/订阅模式及其变种。主题交换机通常用来实现消息的多播路由(multicast routing)【判断routeKey的规则是模糊匹配方式】</p><p>RoutingKey为一个点号“.”分割的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词)</p><p>BindingKey中可以存在两种特殊的字符串“*”和“#”,用于做模糊匹配,</p><p>匹配0个或者多个单词</p><ul><li>匹配一个单词</li></ul><p><img src="../../images/image-20230729110759035.png" alt="image-20230729110759035"></p><h4 id="headers头交换机"><a class="markdownIt-Anchor" href="#headers头交换机"></a> –headers头交换机</h4><p>(不依赖与路由键的匹配规则,基本不用了)</p><p>绑定队列与交换器的时候指定一个键值对,当交换器在分发消息的时候会先解开消息体里的headers 数据,然后判断里面是否有所设置的键值对,如果发现匹配成功,才将消息分发到队列中;这种交换器类型在性能上相对来说较差,在实际工作中很少会用到</p><h2 id="routingkey路由键"><a class="markdownIt-Anchor" href="#routingkey路由键"></a> # RoutingKey:路由键</h2><p>生产者将消息发送给交换器的时候,一般会指定一个RoutingKey,用来指定消息的路由规则,而这个RoutingKey需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。</p><p>在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以发送消息给交换器时,通过指定RoutingKey来决定消息流向哪里。</p><h2 id="bindingkey绑定"><a class="markdownIt-Anchor" href="#bindingkey绑定"></a> # BindingKey:绑定</h2><p>RabbitMQ通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就知道如何正确的将消息路由到队列了。</p><p><img src="../../images/image-20230729110830440.png" alt="image-20230729110830440"></p><h2 id="connection连接"><a class="markdownIt-Anchor" href="#connection连接"></a> # Connection:连接</h2><p>:Connection 和Channel 。我们知道无论是生产者还是消费者,都需要和RabbitMQ Broker 建立连接,这个连接就是一条<font color="red"><strong>TCP</strong></font> 连接,也就是Connection 。</p><p><img src="../../images/image-20230729110854942.png" alt="image-20230729110854942"></p><h2 id="channel信道"><a class="markdownIt-Anchor" href="#channel信道"></a> # Channel:信道</h2><p>一旦TCP 连接建立起来,客户端紧接着可以创建一个AMQP 信道(Channel) ,每个信道都会被指派一个唯一的ID 。信道是建立在Connection 之上的<strong>虚拟连接</strong>, RabbitMQ 处理的每条AMQP 指令都是通过信道完成的。</p><p><strong>我们完全可以直接使用Connection 就能完成信道的工作,为什么还要引入信道呢?</strong></p><p>一个应用程序中有很多个线程需要从RabbitMQ 中消费消息,或者生产消息,那么必然需要建立很多个Connection ,也就是许多个TCP 连接。然而对于操作系统而言,建立和销毁TCP 连接是非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。RabbitMQ 采用类似NIO’ (Non-blocking 1/0) 的做法,选择TCP 连接复用,不仅可以减少性能开销,同时也便于管理。</p><p>每个线程把持一个信道,所以信道复用了Connection 的TCP 连接。同时RabbitMQ 可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection 可以在产生性能瓶颈的情况下有效地节省TCP 连接资源。但是当信道本身的流量很大时,这时候多个信道复用一个Connection 就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多个Connection ,将这些信道均摊到这些Connection 中, 至于这些相关的调优策略需要根据业务自身的实际情况进行调节。</p><p>信道在AMQP 中是一个很重要的概念,大多数操作都是在信道这个层面展开的。在代码清单中也可以看出一些端倪,</p><p>比如channel.exchangeDeclare 、channel .queueDeclare 、channel.basicPublish 和channel.basicConsume 等方法。</p><p>RabbitMQ 相关的API 与AMQP紧密相连,比如channel.basicPublish 对应AMQP 的Basic.Publish 命令.</p><h2 id="vhosts虚拟主机"><a class="markdownIt-Anchor" href="#vhosts虚拟主机"></a> # vhosts:虚拟主机</h2><p>为了在一个单独的代理上实现多个隔离的环境(用户、用户组、交换机、队列 等),AMQP 提供了一个虚拟主机(virtual hosts - vhosts)的概念。这跟 Web servers 虚拟主机概念非常相似,这为 AMQP 实体提供了完全隔离的环境。当连接被建立的时候,AMQP 客户端来指定使用哪个虚拟主机。</p><h2 id="rabbitmq运转流程"><a class="markdownIt-Anchor" href="#rabbitmq运转流程"></a> # RabbitMQ运转流程</h2><h3 id="生产者发送消息的时候"><a class="markdownIt-Anchor" href="#生产者发送消息的时候"></a> 生产者发送消息的时候</h3><p>(1) 生产者连接到RabbitMQ Broker , 建立一个连接( Connection) ,开启一个信道(Channel)</p><p>(2) 生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等</p><p>(3) 生产者声明一个队列井设置相关属性,比如是否排他、是否持久化、是否自动删除等</p><p>(4) 生产者通过路由键将交换器和队列绑定起来</p><p>(5) 生产者发送消息至RabbitMQ Broker,其中包含路由键、交换器等信息</p><p>(6) 相应的交换器根据接收到的路由键查找相匹配的队列。</p><p>(7) 如果找到,则将从生产者发送过来的消息存入相应的队列中。</p><p>(8) 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者</p><p>(9) 关闭信道。</p><p>(10) 关闭连接。</p><h3 id="消费者接收消息的过程"><a class="markdownIt-Anchor" href="#消费者接收消息的过程"></a> 消费者接收消息的过程:</h3><p>(1) 消费者连接到RabbitMQ Broker ,建立一个连接(Connection ) ,开启一个信道(Channel) 。</p><p>(2) 消费者向RabbitMQ Broker 请求消费相应队列中的消息,可能会设置相应的回调函数,<br>以及做一些准备工作</p><p>(3) 等待RabbitMQ Broker 回应并投递相应队列中的消息, 消费者接收消息。</p><p>(4) 消费者确认( ack) 接收到的消息。</p><p>(5) RabbitMQ 从队列中删除相应己经被确认的消息。</p><p>(6) 关闭信道。</p><p>(7) 关闭连接</p><h2 id="amqp协议"><a class="markdownIt-Anchor" href="#amqp协议"></a> # AMQP协议</h2><p>RabbitMQ是采用Erlang语言实现AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件。</p><p>AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。</p><p>AMQP 说到底还是一个通信协议,通信协议都会涉及报文交互,从low-level 举例来说,AMQP 本身是应用层的协议,其填充于TCP 协议层的数据部分。而从high-level 来说, AMQP是通过协议命令进行交互的。AMQP 协议可以看作一系列结构化命令的集合,这里的命令代表一种操作,类似于HTTP 中的方法(GET 、POST 、PUT 、DELETE 等)</p><h2 id="消息机制"><a class="markdownIt-Anchor" href="#消息机制"></a> # 消息机制</h2><h3 id="消息确认"><a class="markdownIt-Anchor" href="#消息确认"></a> 消息确认</h3><p>消费者应用(Consumer applications) - 用来接受和处理消息的应用 - 在处理消息的时候偶尔会失败或者有时会直接崩溃掉。而且网络原因也有可能引起各种问题。这就给我们出了个难题,AMQP 代理在什么时候删除消息才是正确的?AMQP 0-9-1 规范给我们两种建议:</p><p>自动确认模式:当消息代理(broker)将消息发送给应用后立即删除。(使用 AMQP 方法:basic.deliver 或 basic.get-ok))<br>显式确认模式:待应用(application)发送一个确认回执(acknowledgement)后再删除消息。(使用 AMQP 方法:basic.ack)<br>如果一个消费者在尚未发送确认回执的情况下挂掉了,那 AMQP 代理会将消息重新投递给另一个消费者。如果当时没有可用的消费者了,消息代理会死等下一个注册到此队列的消费者,然后再次尝试投递。</p><h3 id="拒绝消息"><a class="markdownIt-Anchor" href="#拒绝消息"></a> 拒绝消息</h3><p>当一个消费者接收到某条消息后,处理过程有可能成功,有可能失败。应用可以向消息代理表明,本条消息由于 “拒绝消息(Rejecting Messages)” 的原因处理失败了(或者未能在此时完成)。</p><p>当拒绝某条消息时,应用可以告诉消息代理如何处理这条消息——销毁它或者重新放入队列。</p><p>当此队列只有一个消费者时,请确认不要由于拒绝消息并且选择了重新放入队列的行为而引起消息在同一个消费者身上无限循环的情况发生。</p><p>在 AMQP 中,basic.reject 方法用来执行拒绝消息的操作。但 basic.reject 有个限制:你不能使用它决绝多个带有确认回执(acknowledgements)的消息。但是如果你使用的是 RabbitMQ,那么你可以使用被称作 negative acknowledgements(也叫 nacks)的 AMQP 0-9-1 扩展来解决这个问题。</p><h3 id="预取消息"><a class="markdownIt-Anchor" href="#预取消息"></a> 预取消息</h3><p>在多个消费者共享一个队列的案例中,明确指定在收到下一个确认回执前每个消费者一次可以接受多少条消息是非常有用的。这可以在试图批量发布消息的时候起到简单的负载均衡和提高消息吞吐量的作用。For example, if a producing application sends messages every minute because of the nature of the work it is doing.(例如,如果生产应用每分钟才发送一条消息,这说明处理工作尚在运行。)</p><p>注意,RabbitMQ 只支持通道级的预取计数,而不是连接级的或者基于大小的预取。</p><h3 id="消息属性"><a class="markdownIt-Anchor" href="#消息属性"></a> 消息属性</h3><p>AMQP 模型中的消息(Message)对象是带有属性(Attributes)的。有些属性及其常见,以至于 AMQP 0-9-1 明确的定义了它们,并且应用开发者们无需费心思思考这些属性名字所代表的具体含义。例如:</p><ul><li>Content type(内容类型)</li><li>Content encoding(内容编码)</li><li>Routing key(路由键)</li><li>Delivery mode (persistent or not)</li><li>投递模式(持久化 或 非持久化)</li><li>Message priority(消息优先权)</li><li>Message publishing timestamp(消息发布的时间戳)</li><li>Expiration period(消息有效期)</li><li>Publisher application id(发布应用的 ID)</li></ul><p>有些属性是被 AMQP 代理所使用的,但是大多数是开放给接收它们的应用解释器用的。有些属性是可选的也被称作消息头(headers)。他们跟 HTTP 协议的 X-Headers 很相似。消息属性需要在消息被发布的时候定义。</p><h3 id="消息主体"><a class="markdownIt-Anchor" href="#消息主体"></a> 消息主体</h3><p>AMQP 的消息除属性外,也含有一个有效载荷 - Payload(消息实际携带的数据),它被 AMQP 代理当作不透明的字节数组来对待。</p><p>消息代理不会检查或者修改有效载荷。消息可以只包含属性而不携带有效载荷。它通常会使用类似 JSON 这种序列化的格式数据,为了节省,协议缓冲器和 MessagePack 将结构化数据序列化,以便以消息的有效载荷的形式发布。AMQP 及其同行者们通常使用 “content-type” 和 “content-encoding” 这两个字段来与消息沟通进行有效载荷的辨识工作,但这仅仅是基于约定而已。</p><h3 id="消息持久化"><a class="markdownIt-Anchor" href="#消息持久化"></a> 消息持久化</h3><p>消息能够以持久化的方式发布,AMQP 代理会将此消息存储在磁盘上。如果服务器重启,系统会确认收到的持久化消息未丢失。</p><p>简单地将消息发送给一个持久化的交换机或者路由给一个持久化的队列,并不会使得此消息具有持久化性质:它完全取决与消息本身的持久模式(persistence mode)。将消息以持久化方式发布时,会对性能造成一定的影响(就像数据库操作一样,健壮性的存在必定造成一些性能牺牲)。</p>]]></content>
<summary type="html"><p>RabbitMQ是采用Erlang语言实现AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件。</p>
<h1 id="rabbitmq架构模型"><a class="markdownIt-Anchor" href=</summary>
<category term="MQ" scheme="http://example.com/categories/MQ/"/>
<category term="rabbitmq" scheme="http://example.com/categories/MQ/rabbitmq/"/>
<category term="rabbitmq" scheme="http://example.com/tags/rabbitmq/"/>
</entry>
<entry>
<title></title>
<link href="http://example.com/2023/07/29/MQ/MQ%E5%A6%82%E4%BD%95%E7%A1%AE%E4%BF%9D%E6%B6%88%E6%81%AF%E7%9A%84%E9%87%8D%E5%A4%8D%E6%B6%88%E8%B4%B9/"/>
<id>http://example.com/2023/07/29/MQ/MQ%E5%A6%82%E4%BD%95%E7%A1%AE%E4%BF%9D%E6%B6%88%E6%81%AF%E7%9A%84%E9%87%8D%E5%A4%8D%E6%B6%88%E8%B4%B9/</id>
<published>2023-07-29T01:43:28.903Z</published>
<updated>2023-07-29T02:56:09.706Z</updated>
<content type="html"><![CDATA[<p>幂等:一个数据或者一个请求,重复了多次,确保对应的数据是不会改变的,不能出错</p><p>思路:</p><ul><li>如果是redis,就没问题,因为每次都是set(会覆盖),天然幂等性</li><li>生产者发送消息的时候会带上一个确据唯一的id,消费者拿到消息之后,先根据这个id去redis里查一下,之前有没有消费过,没后消费过就处理,并且写入这个唯一id到redis中,如果消费过,则不处理</li><li>基于数据库做 <strong>去重表</strong><ul><li>利用数据库表单的特性来实现幂等,常用的一个思路是在表上构建唯一性索引,保证某一类数据一旦执行完毕,后续同样的请求不再重复处理了(利用一张日志表来记录已经处理成功的消息ID,如果新到的消息ID,如果新到的消息ID已经在日志表中,那么就不再处理这条消息)</li><li>以电商平台为例子,电商平台上的订单 id 就是最适合的 token。当用户下单时,会经历多个环节,比如生成订单,)减库存,减优惠券等等。每一个环节执行时都先检测一下该订单 id 是否已经执行过这一步骤,对未执行的请求,执行操作并缓存结果,而对已经执行过的 id,则直接返回之前的执行结果,不做任何操作。这样可以在最大程度上避免操作的重复执行问题,缓存起来的执行结果也能用于事务的控制等。</li><li><img src="../../images/image-20230729101655318.png" alt="image-20230729101655318"></li></ul></li></ul>]]></content>
<summary type="html"><p>幂等:一个数据或者一个请求,重复了多次,确保对应的数据是不会改变的,不能出错</p>
<p>思路:</p>
<ul>
<li>如果是redis,就没问题,因为每次都是set(会覆盖),天然幂等性</li>
<li>生产者发送消息的时候会带上一个确据唯一的id,消费者拿到消息</summary>
</entry>
<entry>
<title></title>
<link href="http://example.com/2023/07/27/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%8D%8F%E7%A8%8B/"/>
<id>http://example.com/2023/07/27/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E5%8D%8F%E7%A8%8B/</id>
<published>2023-07-26T16:44:26.972Z</published>
<updated>2023-07-26T17:03:17.361Z</updated>
<content type="html"><![CDATA[<h2 id="什么是协程"><a class="markdownIt-Anchor" href="#什么是协程"></a> 什么是协程?</h2><h1 id="协程"><a class="markdownIt-Anchor" href="#协程"></a> 协程</h1><p>协程(Coroutines)是一种比线程更加轻量级的存在。协程完全由程序所控制(在<mark><strong>用户态</strong></mark>执行),带来的好处是性能大幅度的提升。 一个操作系统中可以有多个进程;一个进程可以有多个线程;同理,一个线程可以有多个协程。</p><p>协程是一个特殊的<mark><strong>函数</strong></mark>,这个函数可以在某个地方挂起,并且可以重新在挂起处继续运行。 <strong>一个线程内的多个协程的运行是串行的,这点和多进程(多线程)在多核CPU上执行时是不同的。</strong> 多进程(多线程)在多核CPU上是可以并行的。<strong>当线程内的某一个协程运行时,其它协程必须挂起。</strong></p><h2 id="协程切换"><a class="markdownIt-Anchor" href="#协程切换"></a> 协程切换</h2><p>由于<strong>协程切换是在线程内完成的,涉及到的资源比较少</strong>。不像内核级线程(进程)切换那样,上下文的内容比较多,切换代价较大。协程本身是非常轻巧的,可以简单理解为只是切换了寄存器和协程栈的内容。这样代价就非常小。</p><p>如果有不了解进程(线程)切换的,可以参考下面的资料:</p><p><a href="https://cloud.tencent.com/developer/tools/blog-entry?target=https://blog.csdn.net/21cnbao/article/details/108860584">深入理解Linux内核进程上下文切换</a></p><p><a href="https://cloud.tencent.com/developer/tools/blog-entry?target=https://blog.csdn.net/williamgavin/article/details/83062645">操作系统(四) – 用户级线程与核心级线程(线程的切换)</a></p><p><a href="https://cloud.tencent.com/developer/tools/blog-entry?target=https://blog.csdn.net/zy010101/article/details/83547157">线程</a></p><h3 id="协程切换的问题"><a class="markdownIt-Anchor" href="#协程切换的问题"></a> 协程切换的问题</h3><p>实际上协程只有在等待IO的过程中才能重复利用线程,也就是说协程本质是通过多路复用来完成的。但是有个问题是,协程本身不是线程,只是一个特殊的函数,它不能被操作系统感知到(操作系统只能感知到进程和内核级线程),如果某个线程中的协程调用了阻塞IO,那么将会导致线程切换发生。因此只有协程是不够的,是无法解决问题的。还需要异步来配合协程。 <strong>因此,实际上我们可以把协程可以看做是一种用户级线程的实现。</strong> <strong>协程+异步才能发挥出协程的最大作用</strong></p><h2 id="协程的使用"><a class="markdownIt-Anchor" href="#协程的使用"></a> 协程的使用</h2><ul><li>计算型的操作,利用协程来回切换执行,没有任何意义,来回切换并保存状态 反倒会降低性能。</li><li><strong>IO型的操作,利用协程在IO等待时间就去切换执行其他任务,当IO操作结束后再自动回调</strong>,那么就会大大节省资源并提供性能,从而实现<strong>异步编程</strong>(不等待任务结束就可以去执行其他代码)。</li></ul><p><strong>协程不是什么银弹,要注意使用场景。</strong></p>]]></content>
<summary type="html"><h2 id="什么是协程"><a class="markdownIt-Anchor" href="#什么是协程"></a> 什么是协程?</h2>
<h1 id="协程"><a class="markdownIt-Anchor" href="#协程"></a> 协程</h1>
</summary>
</entry>
<entry>
<title></title>
<link href="http://example.com/2023/07/22/interview/Spring%E9%9D%A2%E8%AF%95/Spring%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93/"/>
<id>http://example.com/2023/07/22/interview/Spring%E9%9D%A2%E8%AF%95/Spring%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98%E6%80%BB%E7%BB%93/</id>
<published>2023-07-22T07:08:03.374Z</published>
<updated>2023-08-01T15:53:58.156Z</updated>
<content type="html"><![CDATA[<h1 id="spring常见面试题总结"><a class="markdownIt-Anchor" href="#spring常见面试题总结"></a> Spring常见面试题总结</h1><hr><p>此页内容</p><ul><li><a href="">Spring 基础</a></li><li><ul><li><a href="">什么是 Spring 框架?</a></li><li><a href="">Spring 包含的模块有哪些?</a></li><li><a href="">Spring,Spring MVC,Spring Boot 之间什么关系?</a></li></ul></li><li><a href="">Spring IoC</a></li><li><ul><li><a href="">谈谈自己对于 Spring IoC 的了解</a></li><li><a href="">什么是 Spring Bean?</a></li><li><a href="">将一个类声明为 Bean 的注解有哪些?</a></li><li><a href="">@Component 和 @Bean 的区别是什么?</a></li><li><a href="">注入 Bean 的注解有哪些?</a></li><li><a href="">@Autowired 和 @Resource 的区别是什么?</a></li><li><a href="">Bean 的作用域有哪些?</a></li><li><a href="">Bean 是线程安全的吗?</a></li><li><a href="">Bean 的生命周期了解么?</a></li></ul></li><li><a href="">Spring AoP</a></li><li><ul><li><a href="">谈谈自己对于 AOP 的了解</a></li><li><a href="">Spring AOP 和 AspectJ AOP 有什么区别?</a></li><li><a href="">AspectJ 定义的通知类型有哪些?</a></li><li><a href="">多个切面的执行顺序如何控制?</a></li></ul></li><li><a href="">Spring MVC</a></li><li><ul><li><a href="">说说自己对于 Spring MVC 了解?</a></li><li><a href="">Spring MVC 的核心组件有哪些?</a></li><li><a href="">SpringMVC 工作原理了解吗?</a></li><li><a href="">统一异常处理怎么做?</a></li></ul></li><li><a href="">Spring 框架中用到了哪些设计模式?</a></li><li><a href="">Spring 事务</a></li><li><ul><li><a href="">Spring 管理事务的方式有几种?</a></li><li><a href="">Spring 事务中哪几种事务传播行为?</a></li><li><a href="">Spring 事务中的隔离级别有哪几种?</a></li><li><a href="">@Transactional(rollbackFor = Exception.class)注解了解吗?</a></li></ul></li><li><a href="">Spring Data JPA</a></li><li><ul><li><a href="">如何使用 JPA 在数据库中非持久化一个字段?</a></li><li><a href="">JPA 的审计功能是做什么的?有什么用?</a></li><li><a href="">实体之间的关联关系注解有哪些?</a></li></ul></li><li><a href="">Spring Security</a></li><li><ul><li><a href="">有哪些控制请求访问权限的方法?</a></li><li><a href="">hasRole 和 hasAuthority 有区别吗?</a></li><li><a href="">如何对密码进行加密?</a></li><li><a href="">如何优雅更换系统使用的加密算法?</a></li></ul></li><li><a href="">参考</a></li></ul><p>这是一则或许对你有用的小广告</p><ul><li><strong>面试专版</strong>:准备 Java 面试的小伙伴可以考虑面试专版:<strong><a href="https://javaguide.cn/zhuanlan/java-mian-shi-zhi-bei.html">《Java 面试指北 》open in new window</a></strong> (质量很高,专为面试打造,配合 JavaGuide 食用)。</li><li><strong>知识星球</strong>:专属面试小册/一对一交流/简历修改/专属求职指南,欢迎加入 <strong><a href="https://javaguide.cn/about-the-author/zhishixingqiu-two-years.html">JavaGuide 知识星球open in new window</a></strong>(点击链接即可查看星球的详细介绍,一定一定一定确定自己真的需要再加入,一定一定要看完详细介绍之后再加我)。</li></ul><p>这篇文章主要是想通过一些问题,加深大家对于 Spring 的理解,所以不会涉及太多的代码!</p><p>下面的很多问题我自己在使用 Spring 的过程中也并没有注意,自己也是临时查阅了很多资料和书籍补上的。网上也有一些很多关于 Spring 常见问题/面试题整理的文章,我感觉大部分都是互相 copy,而且很多问题也不是很好,有些回答也存在问题。所以,自己花了一周的业余时间整理了一下,希望对大家有帮助。</p><h2 id=""><a class="markdownIt-Anchor" href="#"></a> <a href="#spring-%E5%9F%BA%E7%A1%80">#</a> Spring 基础</h2><h3 id="-2"><a class="markdownIt-Anchor" href="#-2"></a> <a href="#%E4%BB%80%E4%B9%88%E6%98%AF-spring-%E6%A1%86%E6%9E%B6">#</a> 什么是 Spring 框架?</h3><p>Spring 是一款开源的轻量级 Java 开发框架,旨在提高开发人员的开发效率以及系统的可维护性。</p><p>我们一般说 Spring 框架指的都是 Spring Framework,它是很多模块的集合,使用这些模块可以很方便地协助我们进行开发,比如说 Spring 支持 IoC(Inversion of Control:控制反转) 和 AOP(Aspect-Oriented Programming:面向切面编程)、可以很方便地对数据库进行访问、可以很方便地集成第三方组件(电子邮件,任务,调度,缓存等等)、对单元测试支持比较好、支持 RESTful Java 应用程序的开发。</p><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/38ef122122de4375abcd27c3de8f60b4.png" alt="img"></p><p>Spring 最核心的思想就是不重新造轮子,开箱即用,提高开发效率。</p><p>Spring 翻译过来就是春天的意思,可见其目标和使命就是为 Java 程序员带来春天啊!感动!</p><p>🤐 多提一嘴:<strong>语言的流行通常需要一个杀手级的应用,Spring 就是 Java 生态的一个杀手级的应用框架。</strong></p><p>Spring 提供的核心功能主要是 IoC 和 AOP。学习 Spring ,一定要把 IoC 和 AOP 的核心思想搞懂!</p><ul><li>Spring 官网:<a href="https://spring.io/">https://spring.io/open in new window</a></li><li>GitHub 地址: <a href="https://github.com/spring-projects/spring-framework">https://github.com/spring-projects/spring-framework</a></li></ul><h3 id="-3"><a class="markdownIt-Anchor" href="#-3"></a> <a href="#spring-%E5%8C%85%E5%90%AB%E7%9A%84%E6%A8%A1%E5%9D%97%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> Spring 包含的模块有哪些?</h3><p><strong>Spring4.x 版本</strong>:</p><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/jvme0c60b4606711fc4a0b6faf03230247a.png" alt="Spring4.x主要模块">Spring4.x主要模块</p><p><strong>Spring5.x 版本</strong>:</p><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/20200831175708.png" alt="Spring5.x主要模块">Spring5.x主要模块</p><p>Spring5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。</p><p>Spring 各个模块的依赖关系如下:</p><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/20200902100038.png" alt="Spring 各个模块的依赖关系">Spring 各个模块的依赖关系</p><h4 id="-4"><a class="markdownIt-Anchor" href="#-4"></a> <a href="#core-container">#</a> Core Container</h4><p>Spring 框架的核心模块,也可以说是基础模块,主要提供 IoC 依赖注入功能的支持。Spring 其他所有的功能基本都需要依赖于该模块,我们从上面那张 Spring 各个模块的依赖关系图就可以看出来。</p><ul><li><strong>spring-core</strong>:Spring 框架基本的核心工具类。</li><li><strong>spring-beans</strong>:提供对 bean 的创建、配置和管理等功能的支持。</li><li><strong>spring-context</strong>:提供对国际化、事件传播、资源加载等功能的支持。</li><li><strong>spring-expression</strong>:提供对表达式语言(Spring Expression Language) SpEL 的支持,只依赖于 core 模块,不依赖于其他模块,可以单独使用。</li></ul><h4 id="-5"><a class="markdownIt-Anchor" href="#-5"></a> <a href="#aop">#</a> AOP</h4><ul><li><strong>spring-aspects</strong>:该模块为与 AspectJ 的集成提供支持。</li><li><strong>spring-aop</strong>:提供了面向切面的编程实现。</li><li><strong>spring-instrument</strong>:提供了为 JVM 添加代理(agent)的功能。 具体来讲,它为 Tomcat 提供了一个织入代理,能够为 Tomcat 传递类文 件,就像这些文件是被类加载器加载的一样。没有理解也没关系,这个模块的使用场景非常有限。</li></ul><h4 id="-6"><a class="markdownIt-Anchor" href="#-6"></a> <a href="#data-access-integration">#</a> Data Access/Integration</h4><ul><li><strong>spring-jdbc</strong>:提供了对数据库访问的抽象 JDBC。不同的数据库都有自己独立的 API 用于操作数据库,而 Java 程序只需要和 JDBC API 交互,这样就屏蔽了数据库的影响。</li><li><strong>spring-tx</strong>:提供对事务的支持。</li><li><strong>spring-orm</strong>:提供对 Hibernate、JPA、iBatis 等 ORM 框架的支持。</li><li><strong>spring-oxm</strong>:提供一个抽象层支撑 OXM(Object-to-XML-Mapping),例如:JAXB、Castor、XMLBeans、JiBX 和 XStream 等。</li><li><strong>spring-jms</strong> : 消息服务。自 Spring Framework 4.1 以后,它还提供了对 spring-messaging 模块的继承。</li></ul><h4 id="-7"><a class="markdownIt-Anchor" href="#-7"></a> <a href="#spring-web">#</a> Spring Web</h4><ul><li><strong>spring-web</strong>:对 Web 功能的实现提供一些最基础的支持。</li><li><strong>spring-webmvc</strong>:提供对 Spring MVC 的实现。</li><li><strong>spring-websocket</strong>:提供了对 WebSocket 的支持,WebSocket 可以让客户端和服务端进行双向通信。</li><li><strong>spring-webflux</strong>:提供对 WebFlux 的支持。WebFlux 是 Spring Framework 5.0 中引入的新的响应式框架。与 Spring MVC 不同,它不需要 Servlet API,是完全异步。</li></ul><h4 id="-8"><a class="markdownIt-Anchor" href="#-8"></a> <a href="#messaging">#</a> Messaging</h4><p><strong>spring-messaging</strong> 是从 Spring4.0 开始新加入的一个模块,主要职责是为 Spring 框架集成一些基础的报文传送应用。</p><h4 id="-9"><a class="markdownIt-Anchor" href="#-9"></a> <a href="#spring-test">#</a> Spring Test</h4><p>Spring 团队提倡测试驱动开发(TDD)。有了控制反转 (IoC)的帮助,单元测试和集成测试变得更简单。</p><p>Spring 的测试模块对 JUnit(单元测试框架)、TestNG(类似 JUnit)、Mockito(主要用来 Mock 对象)、PowerMock(解决 Mockito 的问题比如无法模拟 final, static, private 方法)等等常用的测试框架支持的都比较好。</p><h3 id="-10"><a class="markdownIt-Anchor" href="#-10"></a> <a href="#spring-spring-mvc-spring-boot-%E4%B9%8B%E9%97%B4%E4%BB%80%E4%B9%88%E5%85%B3%E7%B3%BB">#</a> Spring,Spring MVC,Spring Boot 之间什么关系?</h3><p>很多人对 Spring,Spring MVC,Spring Boot 这三者傻傻分不清楚!这里简单介绍一下这三者,其实很简单,没有什么高深的东西。</p><p>Spring 包含了多个功能模块(上面刚刚提到过),其中最重要的是 Spring-Core(主要提供 IoC 依赖注入功能的支持) 模块, Spring 中的其他模块(比如 Spring MVC)的功能实现基本都需要依赖于该模块。</p><p>下图对应的是 Spring4.x 版本。目前最新的 5.x 版本中 Web 模块的 Portlet 组件已经被废弃掉,同时增加了用于异步响应式处理的 WebFlux 组件。</p><p><img src="https://oss.javaguide.cn/github/javaguide/jvme0c60b4606711fc4a0b6faf03230247a.png" alt="Spring主要模块">Spring主要模块</p><p>Spring MVC 是 Spring 中的一个很重要的模块,主要赋予 Spring 快速构建 MVC 架构的 Web 程序的能力。MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。</p><p><img src="https://oss.javaguide.cn/java-guide-blog/image-20210809181452421.png" alt="img"></p><p>使用 Spring 进行开发各种配置过于麻烦比如开启某些 Spring 特性时,需要用 XML 或 Java 进行显式配置。于是,Spring Boot 诞生了!</p><p>Spring 旨在简化 J2EE 企业应用程序开发。Spring Boot 旨在简化 Spring 开发(减少配置文件,开箱即用!)。</p><p>Spring Boot 只是简化了配置,如果你需要构建 MVC 架构的 Web 程序,你还是需要使用 Spring MVC 作为 MVC 框架,只是说 Spring Boot 帮你简化了 Spring MVC 的很多配置,真正做到开箱即用!</p><h3 id="springspring-mvcspring-boot-有什么区别"><a class="markdownIt-Anchor" href="#springspring-mvcspring-boot-有什么区别"></a> Spring,Spring MVC,Spring Boot 有什么区别?</h3><p><img src="../../../images/image-20230722172641950.png" alt="image-20230722172641950"></p><h2 id="-11"><a class="markdownIt-Anchor" href="#-11"></a> <a href="#spring-ioc">#</a> Spring IoC</h2><h3 id="-12"><a class="markdownIt-Anchor" href="#-12"></a> <a href="#%E8%B0%88%E8%B0%88%E8%87%AA%E5%B7%B1%E5%AF%B9%E4%BA%8E-spring-ioc-%E7%9A%84%E4%BA%86%E8%A7%A3">#</a> 谈谈自己对于 Spring IoC 的了解</h3><p><strong>IoC(Inversion of Control:控制反转)</strong> 是一种设计思想,而不是一个具体的技术实现。IoC 的思想就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理。不过, IoC 并非 Spring 特有,在其他语言中也有应用。</p><p><strong>为什么叫控制反转?</strong></p><ul><li><strong>控制</strong>:指的是对象创建(实例化、管理)的权力</li><li><strong>反转</strong>:控制权交给外部环境(Spring 框架、IoC 容器)</li></ul><p><img src="https://oss.javaguide.cn/java-guide-blog/frc-365faceb5697f04f31399937c059c162.png" alt="IoC 图解">IoC 图解</p><p>将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。这样可以很大程度上简化应用的开发,把应用从复杂的依赖关系中解放出来。 IoC 容器就像是一个工厂一样,当我们需要创建一个对象的时候,只需要配置好配置文件/注解即可,完全不用考虑对象是如何被创建出来的。</p><p>在实际项目中一个 Service 类可能依赖了很多其他的类,假如我们需要实例化这个 Service,你可能要每次都要搞清这个 Service 所有底层类的构造函数,这可能会把人逼疯。如果利用 IoC 的话,你只需要配置好,然后在需要的地方引用就行了,这大大增加了项目的可维护性且降低了开发难度。</p><p>在 Spring 中, IoC 容器是 Spring 用来实现 IoC 的载体, IoC 容器实际上就是个 <font color="red"><strong>Map(key,value)</strong></font>,Map 中存放的是各种对象。</p><p>Spring 时代我们一般通过 XML 文件来配置 Bean,后来开发人员觉得 XML 文件来配置不太好,于是 SpringBoot 注解配置就慢慢开始流行起来。</p><p>相关阅读:</p><ul><li><a href="https://javadoop.com/post/spring-ioc">IoC 源码阅读open in new window</a></li><li><a href="https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247486938&idx=1&sn=c99ef0233f39a5ffc1b98c81e02dfcd4&chksm=cea24211f9d5cb07fa901183ba4d96187820713a72387788408040822ffb2ed575d28e953ce7&token=1736772241&lang=zh_CN#rd">面试被问了几百遍的 IoC 和 AOP ,还在傻傻搞不清楚?open in new window</a></li></ul><h3 id="-13"><a class="markdownIt-Anchor" href="#-13"></a> <a href="#%E4%BB%80%E4%B9%88%E6%98%AF-spring-bean">#</a> 什么是 Spring Bean?</h3><p>简单来说,Bean 代指的就是那些被 IoC 容器所管理的对象。</p><p>我们需要告诉 IoC 容器帮助我们管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML 文件、注解或者 Java 配置类。</p><figure class="highlight xml"><table><tbody><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"><!-- Constructor-arg with 'value' attribute --></span></span><br><span class="line"><span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"..."</span> <span class="attr">class</span>=<span class="string">"..."</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">constructor-arg</span> <span class="attr">value</span>=<span class="string">"..."</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">bean</span>></span></span><br></pre></td></tr></tbody></table></figure><p>下图简单地展示了 IoC 容器如何使用配置元数据来管理对象。</p><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/062b422bd7ac4d53afd28fb74b2bc94d.png" alt="img"></p><p><code>org.springframework.beans</code>和 <code>org.springframework.context</code> 这两个包是 IoC 实现的基础,如果想要研究 IoC 相关的源码的话,可以去看看</p><h3 id="-14"><a class="markdownIt-Anchor" href="#-14"></a> <a href="#%E5%B0%86%E4%B8%80%E4%B8%AA%E7%B1%BB%E5%A3%B0%E6%98%8E%E4%B8%BA-bean-%E7%9A%84%E6%B3%A8%E8%A7%A3%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> 将一个类声明为 Bean 的注解有哪些?</h3><ul><li><code>@Component</code>:通用的注解,可标注任意类为 <code>Spring</code> 组件。如果一个 Bean 不知道属于哪个层,可以使用<code>@Component</code> 注解标注。</li><li><code>@Repository</code> : 对应持久层即 Dao 层,主要用于数据库相关操作。</li><li><code>@Service</code> : 对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。</li><li><code>@Controller</code> : 对应 Spring MVC 控制层,主要用于接受用户请求并调用 <code>Service</code> 层返回数据给前端页面。</li></ul><h3 id="-15"><a class="markdownIt-Anchor" href="#-15"></a> <a href="#component-%E5%92%8C-bean-%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88">#</a> @Component 和 @Bean 的区别是什么?</h3><ul><li><code>@Component</code> 注解作用于类,而<code>@Bean</code>注解作用于方法。</li><li><code>@Component</code>通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中(我们可以使用 <code>@ComponentScan</code> 注解定义要扫描的路径从中找出标识了需要装配的类自动装配到 Spring 的 bean 容器中)。<code>@Bean</code> 注解通常是我们在标有该注解的方法中定义产生这个 bean,<code>@Bean</code>告诉了 Spring 这是某个类的实例,当我需要用它的时候还给我。</li><li><code>@Bean</code> 注解比 <code>@Component</code> 注解的自定义性更强,而且很多地方我们只能通过 <code>@Bean</code> 注解来注册 bean。比如当我们引用第三方库中的类需要装配到 <code>Spring</code>容器时,则只能通过 <code>@Bean</code>来实现。</li></ul><p><code>@Bean</code>注解使用示例:</p><figure class="highlight java"><table><tbody><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="meta">@Configuration</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AppConfig</span> {</span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> TransferService <span class="title function_">transferService</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">TransferServiceImpl</span>();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>上面的代码相当于下面的 xml 配置</p><figure class="highlight xml"><table><tbody><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">beans</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">bean</span> <span class="attr">id</span>=<span class="string">"transferService"</span> <span class="attr">class</span>=<span class="string">"com.acme.TransferServiceImpl"</span>/></span></span><br><span class="line"><span class="tag"></<span class="name">beans</span>></span></span><br></pre></td></tr></tbody></table></figure><p>下面这个例子是通过 <code>@Component</code> 无法实现的。</p><figure class="highlight java"><table><tbody><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="meta">@Bean</span></span><br><span class="line"><span class="keyword">public</span> OneService <span class="title function_">getService</span><span class="params">(status)</span> {</span><br><span class="line"> <span class="keyword">case</span> (status) {</span><br><span class="line"> when <span class="number">1</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">serviceImpl1</span>();</span><br><span class="line"> when <span class="number">2</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">serviceImpl2</span>();</span><br><span class="line"> when <span class="number">3</span>:</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">serviceImpl3</span>();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="-16"><a class="markdownIt-Anchor" href="#-16"></a> <a href="#%E6%B3%A8%E5%85%A5-bean-%E7%9A%84%E6%B3%A8%E8%A7%A3%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> 注入 Bean 的注解有哪些?</h3><p>Spring 内置的 <code>@Autowired</code> 以及 JDK 内置的 <code>@Resource</code> 和 <code>@Inject</code> 都可以用于注入 Bean。</p><table><thead><tr><th>Annotaion</th><th>Package</th><th>Source</th></tr></thead><tbody><tr><td><code>@Autowired</code></td><td><code>org.springframework.bean.factory</code></td><td>Spring 2.5+</td></tr><tr><td><code>@Resource</code></td><td><code>javax.annotation</code></td><td>Java JSR-250</td></tr><tr><td><code>@Inject</code></td><td><code>javax.inject</code></td><td>Java JSR-330</td></tr></tbody></table><p><code>@Autowired</code> 和<code>@Resource</code>使用的比较多一些。</p><h3 id="-17"><a class="markdownIt-Anchor" href="#-17"></a> <a href="#autowired-%E5%92%8C-resource-%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88">#</a> @Autowired 和 @Resource 的区别是什么?</h3><p><code>Autowired</code> 属于 Spring 内置的注解,默认的注入方式为<code>byType</code>(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。</p><p><strong>这会有什么问题呢?</strong> 当一个接口存在多个实现类的话,<code>byType</code>这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。</p><p>这种情况下,注入方式会变为 <code>byName</code>(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 <code>smsService</code> 就是我这里所说的名称,这样应该比较好理解了吧。</p><figure class="highlight java"><table><tbody><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="comment">// smsService 就是我们上面所说的名称</span></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> SmsService smsService;</span><br></pre></td></tr></tbody></table></figure><p>举个例子,<code>SmsService</code> 接口有两个实现类: <code>SmsServiceImpl1</code>和 <code>SmsServiceImpl2</code>,且它们都已经被 Spring 容器所管理。</p><figure class="highlight java"><table><tbody><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">// 报错,byName 和 byType 都无法匹配到 bean</span></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> SmsService smsService;</span><br><span class="line"><span class="comment">// 正确注入 SmsServiceImpl1 对象对应的 bean</span></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> SmsService smsServiceImpl1;</span><br><span class="line"><span class="comment">// 正确注入 SmsServiceImpl1 对象对应的 bean</span></span><br><span class="line"><span class="comment">// smsServiceImpl1 就是我们上面所说的名称</span></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="meta">@Qualifier(value = "smsServiceImpl1")</span></span><br><span class="line"><span class="keyword">private</span> SmsService smsService;</span><br></pre></td></tr></tbody></table></figure><p>我们还是建议通过 <code>@Qualifier</code> 注解来显式指定名称而不是依赖变量的名称。</p><p><code>@Resource</code>属于 JDK 提供的注解,默认注入方式为 <code>byName</code>。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为<code>byType</code>。</p><p><code>@Resource</code> 有两个比较重要且日常开发常用的属性:<code>name</code>(名称)、<code>type</code>(类型)。</p><figure class="highlight java"><table><tbody><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="meta">@interface</span> Resource {</span><br><span class="line"> String <span class="title function_">name</span><span class="params">()</span> <span class="keyword">default</span> <span class="string">""</span>;</span><br><span class="line"> Class<?> type() <span class="keyword">default</span> Object.class;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>如果仅指定 <code>name</code> 属性则注入方式为<code>byName</code>,如果仅指定<code>type</code>属性则注入方式为<code>byType</code>,如果同时指定<code>name</code> 和<code>type</code>属性(不建议这么做)则注入方式为<code>byType</code>+<code>byName</code>。</p><figure class="highlight java"><table><tbody><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">// 报错,byName 和 byType 都无法匹配到 bean</span></span><br><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> SmsService smsService;</span><br><span class="line"><span class="comment">// 正确注入 SmsServiceImpl1 对象对应的 bean</span></span><br><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> SmsService smsServiceImpl1;</span><br><span class="line"><span class="comment">// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)</span></span><br><span class="line"><span class="meta">@Resource(name = "smsServiceImpl1")</span></span><br><span class="line"><span class="keyword">private</span> SmsService smsService;</span><br></pre></td></tr></tbody></table></figure><p>简单总结一下:</p><ul><li><code>@Autowired</code> 是 Spring 提供的注解,<code>@Resource</code> 是 JDK 提供的注解。</li><li><code>Autowired</code> 默认的注入方式为<code>byType</code>(根据类型进行匹配),<code>@Resource</code>默认注入方式为 <code>byName</code>(根据名称进行匹配)。</li><li>当一个接口存在多个实现类的情况下,<code>@Autowired</code> 和<code>@Resource</code>都需要通过名称才能正确匹配到对应的 Bean。<code>Autowired</code> 可以通过 <code>@Qualifier</code> 注解来显式指定名称,<code>@Resource</code>可以通过 <code>name</code> 属性来显式指定名称。</li></ul><h3 id="-18"><a class="markdownIt-Anchor" href="#-18"></a> <a href="#bean-%E7%9A%84%E4%BD%9C%E7%94%A8%E5%9F%9F%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> Bean 的作用域有哪些?</h3><p>Spring 中 Bean 的作用域通常有下面几种:</p><ul><li><strong>singleton</strong> : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。</li><li><strong>prototype</strong> : 每次获取都会创建一个新的 bean 实例。也就是说,连续 <code>getBean()</code> 两次,得到的是不同的 Bean 实例。</li><li><strong>request</strong> (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。</li><li><strong>session</strong> (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。</li><li><strong>application/global-session</strong> (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。</li><li><strong>websocket</strong> (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。</li></ul><p><strong>如何配置 bean 的作用域呢?</strong></p><p>xml 方式:</p><figure class="highlight xml"><table><tbody><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">bean</span> <span class="attr">id</span>=<span class="string">"..."</span> <span class="attr">class</span>=<span class="string">"..."</span> <span class="attr">scope</span>=<span class="string">"singleton"</span>></span><span class="tag"></<span class="name">bean</span>></span></span><br></pre></td></tr></tbody></table></figure><p>注解方式:</p><figure class="highlight java"><table><tbody><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="meta">@Bean</span></span><br><span class="line"><span class="meta">@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)</span></span><br><span class="line"><span class="keyword">public</span> Person <span class="title function_">personPrototype</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Person</span>();</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="-19"><a class="markdownIt-Anchor" href="#-19"></a> <a href="#bean-%E6%98%AF%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E7%9A%84%E5%90%97">#</a> Bean 是线程安全的吗?</h3><p>Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。</p><p>我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。</p><p>prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。</p><p>不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。</p><p>对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:</p><ol><li>在 Bean 中尽量避免定义可变的成员变量。</li><li>在类中定义一个 <code>ThreadLocal</code> 成员变量,将需要的可变成员变量保存在 <code>ThreadLocal</code> 中(推荐的一种方式)。</li></ol><h3 id="-20"><a class="markdownIt-Anchor" href="#-20"></a> <a href="#bean-%E7%9A%84%E7%94%9F%E5%91%BD%E5%91%A8%E6%9C%9F%E4%BA%86%E8%A7%A3%E4%B9%88">#</a> Bean 的生命周期了解么?</h3><ul><li>Bean 容器找到配置文件中 Spring Bean 的定义。</li><li>Bean 容器利用 Java Reflection API 创建一个 Bean 的实例。</li><li>如果涉及到一些属性值 利用 <code>set()</code>方法设置一些属性值。</li><li>如果 Bean 实现了 <code>BeanNameAware</code> 接口,调用 <code>setBeanName()</code>方法,传入 Bean 的名字。</li><li>如果 Bean 实现了 <code>BeanClassLoaderAware</code> 接口,调用 <code>setBeanClassLoader()</code>方法,传入 <code>ClassLoader</code>对象的实例。</li><li>如果 Bean 实现了 <code>BeanFactoryAware</code> 接口,调用 <code>setBeanFactory()</code>方法,传入 <code>BeanFactory</code>对象的实例。</li><li>与上面的类似,如果实现了其他 <code>*.Aware</code>接口,就调用相应的方法。</li><li>如果有和加载这个 Bean 的 Spring 容器相关的 <code>BeanPostProcessor</code> 对象,执行<code>postProcessBeforeInitialization()</code> 方法</li><li>如果 Bean 实现了<code>InitializingBean</code>接口,执行<code>afterPropertiesSet()</code>方法。</li><li>如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。</li><li>如果有和加载这个 Bean 的 Spring 容器相关的 <code>BeanPostProcessor</code> 对象,执行<code>postProcessAfterInitialization()</code> 方法</li><li>当要销毁 Bean 的时候,如果 Bean 实现了 <code>DisposableBean</code> 接口,执行 <code>destroy()</code> 方法。</li><li>当要销毁 Bean 的时候,如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的方法。</li></ul><p>图示:</p><p><img src="https://images.xiaozhuanlan.com/photo/2019/24bc2bad3ce28144d60d9e0a2edf6c7f.jpg" alt="Spring Bean 生命周期">Spring Bean 生命周期</p><p>与之比较类似的中文版本:</p><p><img src="https://images.xiaozhuanlan.com/photo/2019/b5d264565657a5395c2781081a7483e1.jpg" alt="Spring Bean 生命周期">Spring Bean 生命周期</p><h2 id="-21"><a class="markdownIt-Anchor" href="#-21"></a> <a href="#spring-aop">#</a> Spring AoP</h2><h3 id="-22"><a class="markdownIt-Anchor" href="#-22"></a> <a href="#%E8%B0%88%E8%B0%88%E8%87%AA%E5%B7%B1%E5%AF%B9%E4%BA%8E-aop-%E7%9A%84%E4%BA%86%E8%A7%A3">#</a> 谈谈自己对于 AOP 的了解</h3><p>AOP(Aspect-Oriented Programming:面向切面编程)能够将那些与业务无关,却为业务模块所共同调用的逻辑或责任(例如事务处理、日志管理、权限控制等)封装起来,便于减少系统的重复代码,降低模块间的耦合度,并有利于未来的可拓展性和可维护性。</p><p>Spring AOP 就是基于动态代理的,如果要代理的对象,实现了某个接口,那么 Spring AOP 会使用 <strong>JDK Proxy</strong>,去创建代理对象,而对于没有实现接口的对象,就无法使用 JDK Proxy 去进行代理了,这时候 Spring AOP 会使用 <strong>Cglib</strong> 生成一个被代理对象的子类来作为代理,如下图所示:</p><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/230ae587a322d6e4d09510161987d346.jpeg" alt="SpringAOPProcess">SpringAOPProcess</p><p>当然你也可以使用 <strong>AspectJ</strong> !Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。</p><p>AOP 切面编程设计到的一些专业术语:</p><table><thead><tr><th style="text-align:left">术语</th><th style="text-align:center">含义</th></tr></thead><tbody><tr><td style="text-align:left">目标(Target)</td><td style="text-align:center">被通知的对象</td></tr><tr><td style="text-align:left">代理(Proxy)</td><td style="text-align:center">向目标对象应用通知之后创建的代理对象</td></tr><tr><td style="text-align:left">连接点(JoinPoint)</td><td style="text-align:center">目标对象的所属类中,定义的所有方法均为连接点</td></tr><tr><td style="text-align:left">切入点(Pointcut)</td><td style="text-align:center">被切面拦截 / 增强的连接点(切入点一定是连接点,连接点不一定是切入点)</td></tr><tr><td style="text-align:left">通知(Advice)</td><td style="text-align:center">增强的逻辑 / 代码,也即拦截到目标对象的连接点之后要做的事情</td></tr><tr><td style="text-align:left">切面(Aspect)</td><td style="text-align:center">切入点(Pointcut)+通知(Advice)</td></tr><tr><td style="text-align:left">Weaving(织入)</td><td style="text-align:center">将通知应用到目标对象,进而生成代理对象的过程动作</td></tr></tbody></table><h3 id="-23"><a class="markdownIt-Anchor" href="#-23"></a> <a href="#spring-aop-%E5%92%8C-aspectj-aop-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB">#</a> Spring AOP 和 AspectJ AOP 有什么区别?</h3><p><strong>Spring AOP 属于运行时增强,而 AspectJ 是编译时增强。</strong> Spring AOP 基于代理(Proxying),而 AspectJ 基于字节码操作(Bytecode Manipulation)。</p><p>Spring AOP 已经集成了 AspectJ ,AspectJ 应该算的上是 Java 生态系统中最完整的 AOP 框架了。AspectJ 相比于 Spring AOP 功能更加强大,但是 Spring AOP 相对来说更简单,</p><p>如果我们的切面比较少,那么两者性能差异不大。但是,当切面太多的话,最好选择 AspectJ ,它比 Spring AOP 快很多。</p><h3 id="-24"><a class="markdownIt-Anchor" href="#-24"></a> <a href="#aspectj-%E5%AE%9A%E4%B9%89%E7%9A%84%E9%80%9A%E7%9F%A5%E7%B1%BB%E5%9E%8B%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> AspectJ 定义的通知类型有哪些?</h3><ul><li><strong>Before</strong>(前置通知):目标对象的方法调用之前触发</li><li><strong>After</strong> (后置通知):目标对象的方法调用之后触发</li><li><strong>AfterReturning</strong>(返回通知):目标对象的方法调用完成,在返回结果值之后触发</li><li><strong>AfterThrowing</strong>(异常通知):目标对象的方法运行中抛出 / 触发异常后触发。AfterReturning 和 AfterThrowing 两者互斥。如果方法调用成功无异常,则会有返回值;如果方法抛出了异常,则不会有返回值。</li><li><strong>Around</strong> (环绕通知):编程式控制目标对象的方法调用。环绕通知是所有通知类型中可操作范围最大的一种,因为它可以直接拿到目标对象,以及要执行的方法,所以环绕通知可以任意的在目标对象的方法调用前后搞事,甚至不调用目标对象的方法</li></ul><h3 id="-25"><a class="markdownIt-Anchor" href="#-25"></a> <a href="#%E5%A4%9A%E4%B8%AA%E5%88%87%E9%9D%A2%E7%9A%84%E6%89%A7%E8%A1%8C%E9%A1%BA%E5%BA%8F%E5%A6%82%E4%BD%95%E6%8E%A7%E5%88%B6">#</a> 多个切面的执行顺序如何控制?</h3><p>1、通常使用<code>@Order</code> 注解直接定义切面顺序</p><figure class="highlight java"><table><tbody><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">// 值越小优先级越高</span></span><br><span class="line"><span class="meta">@Order(3)</span></span><br><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoggingAspect</span> <span class="keyword">implements</span> <span class="title class_">Ordered</span> {</span><br></pre></td></tr></tbody></table></figure><p><strong>2、实现<code>Ordered</code> 接口重写 <code>getOrder</code> 方法。</strong></p><figure class="highlight java"><table><tbody><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">@Component</span></span><br><span class="line"><span class="meta">@Aspect</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">LoggingAspect</span> <span class="keyword">implements</span> <span class="title class_">Ordered</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">// ....</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="type">int</span> <span class="title function_">getOrder</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">// 返回值越小优先级越高</span></span><br><span class="line"> <span class="keyword">return</span> <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="-26"><a class="markdownIt-Anchor" href="#-26"></a> <a href="#spring-mvc">#</a> Spring MVC</h2><h3 id="-27"><a class="markdownIt-Anchor" href="#-27"></a> <a href="#%E8%AF%B4%E8%AF%B4%E8%87%AA%E5%B7%B1%E5%AF%B9%E4%BA%8E-spring-mvc-%E4%BA%86%E8%A7%A3">#</a> 说说自己对于 Spring MVC 了解?</h3><p>MVC 是模型(Model)、视图(View)、控制器(Controller)的简写,其核心思想是通过将业务逻辑、数据、显示分离来组织代码。</p><p><img src="https://oss.javaguide.cn/java-guide-blog/image-20210809181452421.png" alt="img"></p><p>网上有很多人说 MVC 不是设计模式,只是软件设计规范,我个人更倾向于 MVC 同样是众多设计模式中的一种。<strong><a href="https://github.com/iluwatar/java-design-patterns">java-design-patternsopen in new window</a></strong> 项目中就有关于 MVC 的相关介绍。</p><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/159b3d3e70dd45e6afa81bf06d09264e.png" alt="img"></p><p>想要真正理解 Spring MVC,我们先来看看 Model 1 和 Model 2 这两个没有 Spring MVC 的时代。</p><p><strong>Model 1 时代</strong></p><p>很多学 Java 后端比较晚的朋友可能并没有接触过 Model 1 时代下的 JavaWeb 应用开发。在 Model1 模式下,整个 Web 应用几乎全部用 JSP 页面组成,只用少量的 JavaBean 来处理数据库连接、访问等操作。</p><p>这个模式下 JSP 即是控制层(Controller)又是表现层(View)。显而易见,这种模式存在很多问题。比如控制逻辑和表现逻辑混杂在一起,导致代码重用率极低;再比如前端和后端相互依赖,难以进行测试维护并且开发效率极低。</p><p><img src="https://oss.javaguide.cn/java-guide-blog/mvc-mode1.png" alt="mvc-mode1">mvc-mode1</p><p><strong>Model 2 时代</strong></p><p>学过 Servlet 并做过相关 Demo 的朋友应该了解“Java Bean(Model)+ JSP(View)+Servlet(Controller) ”这种开发模式,这就是早期的 JavaWeb MVC 开发模式。</p><ul><li>Model:系统涉及的数据,也就是 dao 和 bean。</li><li>View:展示模型中的数据,只是用来展示。</li><li>Controller:处理用户请求都发送给 ,返回数据给 JSP 并展示给用户。</li></ul><p><img src="https://oss.javaguide.cn/java-guide-blog/mvc-model2.png" alt="img"></p><p>Model2 模式下还存在很多问题,Model2 的抽象和封装程度还远远不够,使用 Model2 进行开发时不可避免地会重复造轮子,这就大大降低了程序的可维护性和复用性。</p><p>于是,很多 JavaWeb 开发相关的 MVC 框架应运而生比如 Struts2,但是 Struts2 比较笨重。</p><p><strong>Spring MVC 时代</strong></p><p>随着 Spring 轻量级开发框架的流行,Spring 生态圈出现了 Spring MVC 框架, Spring MVC 是当前最优秀的 MVC 框架。相比于 Struts2 , Spring MVC 使用更加简单和方便,开发效率更高,并且 Spring MVC 运行速度更快。</p><p>MVC 是一种设计模式,Spring MVC 是一款很优秀的 MVC 框架。Spring MVC 可以帮助我们进行更简洁的 Web 层的开发,并且它天生与 Spring 框架集成。Spring MVC 下我们一般把后端项目分为 Service 层(处理业务)、Dao 层(数据库操作)、Entity 层(实体类)、Controller 层(控制层,返回数据给前台页面)。</p><h3 id="-28"><a class="markdownIt-Anchor" href="#-28"></a> <a href="#spring-mvc-%E7%9A%84%E6%A0%B8%E5%BF%83%E7%BB%84%E4%BB%B6%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> Spring MVC 的核心组件有哪些?</h3><p>记住了下面这些组件,也就记住了 SpringMVC 的工作原理。</p><ul><li><strong><code>DispatcherServlet</code></strong>:<strong>核心的中央处理器</strong>,负责接收请求、分发,并给予客户端响应。</li><li><strong><code>HandlerMapping</code></strong>:<strong>处理器映射器</strong>,根据 uri 去匹配查找能处理的 <code>Handler</code> ,并会将请求涉及到的拦截器和 <code>Handler</code> 一起封装。</li><li><strong><code>HandlerAdapter</code></strong>:<strong>处理器适配器</strong>,根据 <code>HandlerMapping</code> 找到的 <code>Handler</code> ,适配执行对应的 <code>Handler</code>;</li><li><strong><code>Handler</code></strong>:<strong>请求处理器</strong>,处理实际请求的处理器。</li><li><strong><code>ViewResolver</code></strong>:<strong>视图解析器</strong>,根据 <code>Handler</code> 返回的逻辑视图 / 视图,解析并渲染真正的视图,并传递给 <code>DispatcherServlet</code> 响应客户端</li></ul><h3 id="-29"><a class="markdownIt-Anchor" href="#-29"></a> <a href="#springmvc-%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E4%BA%86%E8%A7%A3%E5%90%97">#</a> SpringMVC 工作原理了解吗?</h3><p><strong>Spring MVC 原理如下图所示:</strong></p><blockquote><p>SpringMVC 工作原理的图解我没有自己画,直接图省事在网上找了一个非常清晰直观的,原出处不明。</p></blockquote><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/de6d2b213f112297298f3e223bf08f28.png" alt="img"></p><p><strong>流程说明(重要):</strong></p><ol><li>客户端(浏览器)发送请求, <code>DispatcherServlet</code>拦截请求。</li><li><code>DispatcherServlet</code> 根据请求信息调用 <code>HandlerMapping</code> 。<code>HandlerMapping</code> 根据 uri 去匹配查找能处理的 <code>Handler</code>(也就是我们平常说的 <code>Controller</code> 控制器) ,并会将请求涉及到的拦截器和 <code>Handler</code> 一起封装。</li><li><code>DispatcherServlet</code> 调用 <code>HandlerAdapter</code>适配器执行 <code>Handler</code> 。</li><li><code>Handler</code> 完成对用户请求的处理后,会返回一个 <code>ModelAndView</code> 对象给<code>DispatcherServlet</code>,<code>ModelAndView</code> 顾名思义,包含了数据模型以及相应的视图的信息。<code>Model</code> 是返回的数据对象,<code>View</code> 是个逻辑上的 <code>View</code>。</li><li><code>ViewResolver</code> 会根据逻辑 <code>View</code> 查找实际的 <code>View</code>。</li><li><code>DispaterServlet</code> 把返回的 <code>Model</code> 传给 <code>View</code>(视图渲染)。</li><li>把 <code>View</code> 返回给请求者(浏览器)</li></ol><h3 id="-30"><a class="markdownIt-Anchor" href="#-30"></a> <a href="#%E7%BB%9F%E4%B8%80%E5%BC%82%E5%B8%B8%E5%A4%84%E7%90%86%E6%80%8E%E4%B9%88%E5%81%9A">#</a> 统一异常处理怎么做?</h3><p>推荐使用注解的方式统一异常处理,具体会使用到 <code>@ControllerAdvice</code> + <code>@ExceptionHandler</code> 这两个注解 。</p><figure class="highlight java"><table><tbody><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">@ControllerAdvice</span></span><br><span class="line"><span class="meta">@ResponseBody</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GlobalExceptionHandler</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ExceptionHandler(BaseException.class)</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<?> handleAppException(BaseException ex, HttpServletRequest request) {</span><br><span class="line"> <span class="comment">//......</span></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@ExceptionHandler(value = ResourceNotFoundException.class)</span></span><br><span class="line"> <span class="keyword">public</span> ResponseEntity<ErrorReponse> <span class="title function_">handleResourceNotFoundException</span><span class="params">(ResourceNotFoundException ex, HttpServletRequest request)</span> {</span><br><span class="line"> <span class="comment">//......</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>这种异常处理方式下,会给所有或者指定的 <code>Controller</code> 织入异常处理的逻辑(AOP),当 <code>Controller</code> 中的方法抛出异常的时候,由被<code>@ExceptionHandler</code> 注解修饰的方法进行处理。</p><p><code>ExceptionHandlerMethodResolver</code> 中 <code>getMappedMethod</code> 方法决定了异常具体被哪个被 <code>@ExceptionHandler</code> 注解修饰的方法处理异常。</p><figure class="highlight java"><table><tbody><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Nullable</span></span><br><span class="line"><span class="keyword">private</span> Method <span class="title function_">getMappedMethod</span><span class="params">(Class<? extends Throwable> exceptionType)</span> {</span><br><span class="line">List<Class<? <span class="keyword">extends</span> <span class="title class_">Throwable</span>>> matches = <span class="keyword">new</span> <span class="title class_">ArrayList</span><>();</span><br><span class="line"> <span class="comment">//找到可以处理的所有异常信息。mappedMethods 中存放了异常和处理异常的方法的对应关系</span></span><br><span class="line"><span class="keyword">for</span> (Class<? <span class="keyword">extends</span> <span class="title class_">Throwable</span>> mappedException : <span class="built_in">this</span>.mappedMethods.keySet()) {</span><br><span class="line"><span class="keyword">if</span> (mappedException.isAssignableFrom(exceptionType)) {</span><br><span class="line">matches.add(mappedException);</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="keyword">if</span> (!matches.isEmpty()) {</span><br><span class="line"> <span class="comment">// 按照匹配程度从小到大排序</span></span><br><span class="line">matches.sort(<span class="keyword">new</span> <span class="title class_">ExceptionDepthComparator</span>(exceptionType));</span><br><span class="line"> <span class="comment">// 返回处理异常的方法</span></span><br><span class="line"><span class="keyword">return</span> <span class="built_in">this</span>.mappedMethods.get(matches.get(<span class="number">0</span>));</span><br><span class="line">}</span><br><span class="line"><span class="keyword">else</span> {</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></pre></td></tr></tbody></table></figure><p>从源代码看出:<strong><code>getMappedMethod()</code>会首先找到可以匹配处理异常的所有方法信息,然后对其进行从小到大的排序,最后取最小的那一个匹配的方法(即匹配度最高的那个)。</strong></p><h2 id="-31"><a class="markdownIt-Anchor" href="#-31"></a> <a href="#spring-%E6%A1%86%E6%9E%B6%E4%B8%AD%E7%94%A8%E5%88%B0%E4%BA%86%E5%93%AA%E4%BA%9B%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F">#</a> Spring 框架中用到了哪些设计模式?</h2><blockquote><p>关于下面这些设计模式的详细介绍,可以看我写的 <a href="https://javaguide.cn/system-design/framework/spring/spring-design-patterns-summary.html">Spring 中的设计模式详解open in new window</a> 这篇文章。</p></blockquote><ul><li><strong>工厂设计模式</strong> : Spring 使用工厂模式通过 <code>BeanFactory</code>、<code>ApplicationContext</code> 创建 bean 对象。</li><li><strong>代理设计模式</strong> : Spring AOP 功能的实现。</li><li><strong>单例设计模式</strong> : Spring 中的 Bean 默认都是单例的。</li><li><strong>模板方法模式</strong> : Spring 中 <code>jdbcTemplate</code>、<code>hibernateTemplate</code> 等以 Template 结尾的对数据库操作的类,它们就使用到了模板模式。</li><li><strong>包装器设计模式</strong> : 我们的项目需要连接多个数据库,而且不同的客户在每次访问中根据需要会去访问不同的数据库。这种模式让我们可以根据客户的需求能够动态切换不同的数据源。</li><li><strong>观察者模式:</strong> Spring 事件驱动模型就是观察者模式很经典的一个应用。</li><li><strong>适配器模式</strong> : Spring AOP 的增强或通知(Advice)使用到了适配器模式、spring MVC 中也是用到了适配器模式适配<code>Controller</code>。</li><li>…</li></ul><h2 id="-32"><a class="markdownIt-Anchor" href="#-32"></a> <a href="#spring-%E4%BA%8B%E5%8A%A1">#</a> Spring 事务</h2><p>关于 Spring 事务的详细介绍,可以看我写的 <a href="https://javaguide.cn/system-design/framework/spring/spring-transaction.html">Spring 事务详解open in new window</a> 这篇文章。</p><h3 id="-33"><a class="markdownIt-Anchor" href="#-33"></a> <a href="#spring-%E7%AE%A1%E7%90%86%E4%BA%8B%E5%8A%A1%E7%9A%84%E6%96%B9%E5%BC%8F%E6%9C%89%E5%87%A0%E7%A7%8D">#</a> Spring 管理事务的方式有几种?</h3><ul><li><strong>编程式事务</strong>:在代码中硬编码(不推荐使用) : 通过 <code>TransactionTemplate</code>或者 <code>TransactionManager</code> 手动管理事务,实际应用中很少使用,但是对于你理解 Spring 事务管理原理有帮助。</li><li><strong>声明式事务</strong>:在 XML 配置文件中配置或者直接基于注解(推荐使用) : 实际是通过 AOP 实现(基于<code>@Transactional</code> 的全注解方式使用最多)</li></ul><h3 id="-34"><a class="markdownIt-Anchor" href="#-34"></a> <a href="#spring-%E4%BA%8B%E5%8A%A1%E4%B8%AD%E5%93%AA%E5%87%A0%E7%A7%8D%E4%BA%8B%E5%8A%A1%E4%BC%A0%E6%92%AD%E8%A1%8C%E4%B8%BA">#</a> Spring 事务中哪几种事务传播行为?</h3><p><strong>事务传播行为是为了解决业务层方法之间互相调用的事务问题</strong>。</p><p>当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。</p><p>正确的事务传播行为可能的值如下:</p><p><strong>1.<code>TransactionDefinition.PROPAGATION_REQUIRED</code></strong></p><p>使用的最多的一个事务传播行为,我们平时经常使用的<code>@Transactional</code>注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。</p><p><strong><code>2.TransactionDefinition.PROPAGATION_REQUIRES_NEW</code></strong></p><p>创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,<code>Propagation.REQUIRES_NEW</code>修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。</p><p><strong>3.<code>TransactionDefinition.PROPAGATION_NESTED</code></strong></p><p>如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于<code>TransactionDefinition.PROPAGATION_REQUIRED</code>。</p><p><strong>4.<code>TransactionDefinition.PROPAGATION_MANDATORY</code></strong></p><p>如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)</p><p>这个使用的很少。</p><p>若是错误的配置以下 3 种事务传播行为,事务将不会发生回滚:</p><ul><li><strong><code>TransactionDefinition.PROPAGATION_SUPPORTS</code></strong>: 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。</li><li><strong><code>TransactionDefinition.PROPAGATION_NOT_SUPPORTED</code></strong>: 以非事务方式运行,如果当前存在事务,则把当前事务挂起。</li><li><strong><code>TransactionDefinition.PROPAGATION_NEVER</code></strong>: 以非事务方式运行,如果当前存在事务,则抛出异常。</li></ul><h3 id="-35"><a class="markdownIt-Anchor" href="#-35"></a> <a href="#spring-%E4%BA%8B%E5%8A%A1%E4%B8%AD%E7%9A%84%E9%9A%94%E7%A6%BB%E7%BA%A7%E5%88%AB%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D">#</a> Spring 事务中的隔离级别有哪几种?</h3><p>和事务传播行为这块一样,为了方便使用,Spring 也相应地定义了一个枚举类:<code>Isolation</code></p><figure class="highlight java"><table><tbody><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">enum</span> <span class="title class_">Isolation</span> {</span><br><span class="line"></span><br><span class="line"> DEFAULT(TransactionDefinition.ISOLATION_DEFAULT),</span><br><span class="line"></span><br><span class="line"> READ_UNCOMMITTED(TransactionDefinition.ISOLATION_READ_UNCOMMITTED),</span><br><span class="line"></span><br><span class="line"> READ_COMMITTED(TransactionDefinition.ISOLATION_READ_COMMITTED),</span><br><span class="line"></span><br><span class="line"> REPEATABLE_READ(TransactionDefinition.ISOLATION_REPEATABLE_READ),</span><br><span class="line"></span><br><span class="line"> SERIALIZABLE(TransactionDefinition.ISOLATION_SERIALIZABLE);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">int</span> value;</span><br><span class="line"></span><br><span class="line"> Isolation(<span class="type">int</span> value) {</span><br><span class="line"> <span class="built_in">this</span>.value = value;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="type">int</span> <span class="title function_">value</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="built_in">this</span>.value;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>下面我依次对每一种事务隔离级别进行介绍:</p><ul><li><strong><code>TransactionDefinition.ISOLATION_DEFAULT</code></strong> :使用后端数据库默认的隔离级别,MySQL 默认采用的 <code>REPEATABLE_READ</code> 隔离级别 Oracle 默认采用的 <code>READ_COMMITTED</code> 隔离级别.</li><li><strong><code>TransactionDefinition.ISOLATION_READ_UNCOMMITTED</code></strong> :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,<strong>可能会导致脏读、幻读或不可重复读</strong></li><li><strong><code>TransactionDefinition.ISOLATION_READ_COMMITTED</code></strong> : 允许读取并发事务已经提交的数据,<strong>可以阻止脏读,但是幻读或不可重复读仍有可能发生</strong></li><li><strong><code>TransactionDefinition.ISOLATION_REPEATABLE_READ</code></strong> : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,<strong>可以阻止脏读和不可重复读,但幻读仍有可能发生。</strong></li><li><strong><code>TransactionDefinition.ISOLATION_SERIALIZABLE</code></strong> : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,<strong>该级别可以防止脏读、不可重复读以及幻读</strong>。但是这将严重影响程序的性能。通常情况下也不会用到该级别。</li></ul><h3 id="-36"><a class="markdownIt-Anchor" href="#-36"></a> <a href="#transactional-rollbackfor-exception-class-%E6%B3%A8%E8%A7%A3%E4%BA%86%E8%A7%A3%E5%90%97">#</a> @Transactional(rollbackFor = Exception.class)注解了解吗?</h3><p><code>Exception</code> 分为运行时异常 <code>RuntimeException</code> 和非运行时异常。事务管理对于企业应用来说是至关重要的,即使出现异常情况,它也可以保证数据的一致性。</p><p>当 <code>@Transactional</code> 注解作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,我们也可以在方法级别使用该标注来覆盖类级别的定义。如果类或者方法加了这个注解,那么这个类里面的方法抛出异常,就会回滚,数据库里面的数据也会回滚。</p><p>在 <code>@Transactional</code> 注解中如果不配置<code>rollbackFor</code>属性,那么事务只会在遇到<code>RuntimeException</code>的时候才会回滚,加上 <code>rollbackFor=Exception.class</code>,可以让事务在遇到非运行时异常时也回滚。</p><h2 id="-37"><a class="markdownIt-Anchor" href="#-37"></a> <a href="#spring-data-jpa">#</a> Spring Data JPA</h2><p>JPA 重要的是实战,这里仅对小部分知识点进行总结。</p><h3 id="-38"><a class="markdownIt-Anchor" href="#-38"></a> <a href="#%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8-jpa-%E5%9C%A8%E6%95%B0%E6%8D%AE%E5%BA%93%E4%B8%AD%E9%9D%9E%E6%8C%81%E4%B9%85%E5%8C%96%E4%B8%80%E4%B8%AA%E5%AD%97%E6%AE%B5">#</a> 如何使用 JPA 在数据库中非持久化一个字段?</h3><p>假如我们有下面一个类:</p><figure class="highlight java"><table><tbody><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">@Entity(name="USER")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">User</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Id</span></span><br><span class="line"> <span class="meta">@GeneratedValue(strategy = GenerationType.AUTO)</span></span><br><span class="line"> <span class="meta">@Column(name = "ID")</span></span><br><span class="line"> <span class="keyword">private</span> Long id;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Column(name="USER_NAME")</span></span><br><span class="line"> <span class="keyword">private</span> String userName;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Column(name="PASSWORD")</span></span><br><span class="line"> <span class="keyword">private</span> String password;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> String secrect;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>如果我们想让<code>secrect</code> 这个字段不被持久化,也就是不被数据库存储怎么办?我们可以采用下面几种方法:</p><figure class="highlight java"><table><tbody><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">static</span> String transient1; <span class="comment">// not persistent because of static</span></span><br><span class="line"><span class="keyword">final</span> <span class="type">String</span> <span class="variable">transient2</span> <span class="operator">=</span> <span class="string">"Satish"</span>; <span class="comment">// not persistent because of final</span></span><br><span class="line"><span class="keyword">transient</span> String transient3; <span class="comment">// not persistent because of transient</span></span><br><span class="line"><span class="meta">@Transient</span></span><br><span class="line">String transient4; <span class="comment">// not persistent because of @Transient</span></span><br></pre></td></tr></tbody></table></figure><p>一般使用后面两种方式比较多,我个人使用注解的方式比较多。</p><h3 id="-39"><a class="markdownIt-Anchor" href="#-39"></a> <a href="#jpa-%E7%9A%84%E5%AE%A1%E8%AE%A1%E5%8A%9F%E8%83%BD%E6%98%AF%E5%81%9A%E4%BB%80%E4%B9%88%E7%9A%84-%E6%9C%89%E4%BB%80%E4%B9%88%E7%94%A8">#</a> JPA 的审计功能是做什么的?有什么用?</h3><p>审计功能主要是帮助我们记录数据库操作的具体行为比如某条记录是谁创建的、什么时间创建的、最后修改人是谁、最后修改时间是什么时候。</p><figure class="highlight java"><table><tbody><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></pre></td><td class="code"><pre><span class="line"><span class="meta">@Data</span></span><br><span class="line"><span class="meta">@AllArgsConstructor</span></span><br><span class="line"><span class="meta">@NoArgsConstructor</span></span><br><span class="line"><span class="meta">@MappedSuperclass</span></span><br><span class="line"><span class="meta">@EntityListeners(value = AuditingEntityListener.class)</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">AbstractAuditBase</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@CreatedDate</span></span><br><span class="line"> <span class="meta">@Column(updatable = false)</span></span><br><span class="line"> <span class="meta">@JsonIgnore</span></span><br><span class="line"> <span class="keyword">private</span> Instant createdAt;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@LastModifiedDate</span></span><br><span class="line"> <span class="meta">@JsonIgnore</span></span><br><span class="line"> <span class="keyword">private</span> Instant updatedAt;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@CreatedBy</span></span><br><span class="line"> <span class="meta">@Column(updatable = false)</span></span><br><span class="line"> <span class="meta">@JsonIgnore</span></span><br><span class="line"> <span class="keyword">private</span> String createdBy;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@LastModifiedBy</span></span><br><span class="line"> <span class="meta">@JsonIgnore</span></span><br><span class="line"> <span class="keyword">private</span> String updatedBy;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><ul><li><p><code>@CreatedDate</code>: 表示该字段为创建时间字段,在这个实体被 insert 的时候,会设置值</p></li><li><p><code>@CreatedBy</code> :表示该字段为创建人,在这个实体被 insert 的时候,会设置值</p><p><code>@LastModifiedDate</code>、<code>@LastModifiedBy</code>同理。</p></li></ul><h3 id="-40"><a class="markdownIt-Anchor" href="#-40"></a> <a href="#%E5%AE%9E%E4%BD%93%E4%B9%8B%E9%97%B4%E7%9A%84%E5%85%B3%E8%81%94%E5%85%B3%E7%B3%BB%E6%B3%A8%E8%A7%A3%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> 实体之间的关联关系注解有哪些?</h3><ul><li><code>@OneToOne </code> : 一对一。</li><li><code>@ManyToMany</code>:多对多。</li><li><code>@OneToMany</code> : 一对多。</li><li><code>@ManyToOne</code>:多对一。</li></ul><p>利用 <code>@ManyToOne</code> 和 <code>@OneToMany</code> 也可以表达多对多的关联关系。</p><h2 id="-41"><a class="markdownIt-Anchor" href="#-41"></a> <a href="#spring-security">#</a> Spring Security</h2><p>Spring Security 重要的是实战,这里仅对小部分知识点进行总结。</p><h3 id="-42"><a class="markdownIt-Anchor" href="#-42"></a> <a href="#%E6%9C%89%E5%93%AA%E4%BA%9B%E6%8E%A7%E5%88%B6%E8%AF%B7%E6%B1%82%E8%AE%BF%E9%97%AE%E6%9D%83%E9%99%90%E7%9A%84%E6%96%B9%E6%B3%95">#</a> 有哪些控制请求访问权限的方法?</h3><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/image-20220728201854641.png" alt="img"></p><ul><li><code>permitAll()</code>:无条件允许任何形式访问,不管你登录还是没有登录。</li><li><code>anonymous()</code>:允许匿名访问,也就是没有登录才可以访问。</li><li><code>denyAll()</code>:无条件决绝任何形式的访问。</li><li><code>authenticated()</code>:只允许已认证的用户访问。</li><li><code>fullyAuthenticated()</code>:只允许已经登录或者通过 remember-me 登录的用户访问。</li><li><code>hasRole(String)</code> : 只允许指定的角色访问。</li><li><code>hasAnyRole(String) </code> : 指定一个或者多个角色,满足其一的用户即可访问。</li><li><code>hasAuthority(String)</code>:只允许具有指定权限的用户访问</li><li><code>hasAnyAuthority(String)</code>:指定一个或者多个权限,满足其一的用户即可访问。</li><li><code>hasIpAddress(String)</code> : 只允许指定 ip 的用户访问。</li></ul><h3 id="-43"><a class="markdownIt-Anchor" href="#-43"></a> <a href="#hasrole-%E5%92%8C-hasauthority-%E6%9C%89%E5%8C%BA%E5%88%AB%E5%90%97">#</a> hasRole 和 hasAuthority 有区别吗?</h3><p>可以看看松哥的这篇文章:<a href="https://mp.weixin.qq.com/s/GTNOa2k9_n_H0w24upClRw">Spring Security 中的 hasRole 和 hasAuthority 有区别吗?open in new window</a>,介绍的比较详细。</p><h3 id="-44"><a class="markdownIt-Anchor" href="#-44"></a> <a href="#%E5%A6%82%E4%BD%95%E5%AF%B9%E5%AF%86%E7%A0%81%E8%BF%9B%E8%A1%8C%E5%8A%A0%E5%AF%86">#</a> 如何对密码进行加密?</h3><p>如果我们需要保存密码这类敏感数据到数据库的话,需要先加密再保存。</p><p>Spring Security 提供了多种加密算法的实现,开箱即用,非常方便。这些加密算法实现类的父类是 <code>PasswordEncoder</code> ,如果你想要自己实现一个加密算法的话,也需要继承 <code>PasswordEncoder</code>。</p><p><code>PasswordEncoder</code> 接口一共也就 3 个必须实现的方法。</p><figure class="highlight java"><table><tbody><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">public</span> <span class="keyword">interface</span> <span class="title class_">PasswordEncoder</span> {</span><br><span class="line"> <span class="comment">// 加密也就是对原始密码进行编码</span></span><br><span class="line"> String <span class="title function_">encode</span><span class="params">(CharSequence var1)</span>;</span><br><span class="line"> <span class="comment">// 比对原始密码和数据库中保存的密码</span></span><br><span class="line"> <span class="type">boolean</span> <span class="title function_">matches</span><span class="params">(CharSequence var1, String var2)</span>;</span><br><span class="line"> <span class="comment">// 判断加密密码是否需要再次进行加密,默认返回 false</span></span><br><span class="line"> <span class="keyword">default</span> <span class="type">boolean</span> <span class="title function_">upgradeEncoding</span><span class="params">(String encodedPassword)</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p><img src="https://oss.javaguide.cn/github/javaguide/system-design/framework/spring/image-20220728183540954.png" alt="img"></p><p>官方推荐使用基于 bcrypt 强哈希函数的加密算法实现类。</p><h3 id="-45"><a class="markdownIt-Anchor" href="#-45"></a> <a href="#%E5%A6%82%E4%BD%95%E4%BC%98%E9%9B%85%E6%9B%B4%E6%8D%A2%E7%B3%BB%E7%BB%9F%E4%BD%BF%E7%94%A8%E7%9A%84%E5%8A%A0%E5%AF%86%E7%AE%97%E6%B3%95">#</a> 如何优雅更换系统使用的加密算法?</h3><p>如果我们在开发过程中,突然发现现有的加密算法无法满足我们的需求,需要更换成另外一个加密算法,这个时候应该怎么办呢?</p><p>推荐的做法是通过 <code>DelegatingPasswordEncoder</code> 兼容多种不同的密码加密方案,以适应不同的业务需求。</p><p>从名字也能看出来,<code>DelegatingPasswordEncoder</code> 其实就是一个代理类,并非是一种全新的加密算法,它做的事情就是代理上面提到的加密算法实现类。在 Spring Security 5.0 之后,默认就是基于 <code>DelegatingPasswordEncoder</code> 进行密码加密的。</p><h2 id="-46"><a class="markdownIt-Anchor" href="#-46"></a> <a href="#%E5%8F%82%E8%80%83">#</a> 参考</h2><hr><p>著作权归Guide所有 原文链接:<a href="https://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html">https://javaguide.cn/system-design/framework/spring/spring-knowledge-and-questions-summary.html</a></p>]]></content>
<summary type="html"><h1 id="spring常见面试题总结"><a class="markdownIt-Anchor" href="#spring常见面试题总结"></a> Spring常见面试题总结</h1>
<hr>
<p>此页内容</p>
<ul>
<li><a href="">Sprin</summary>
</entry>
<entry>
<title></title>
<link href="http://example.com/2023/07/19/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/juc/"/>
<id>http://example.com/2023/07/19/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/juc/</id>
<published>2023-07-19T15:49:17.232Z</published>
<updated>2023-08-06T09:38:51.908Z</updated>
<content type="html"><![CDATA[<h1 id="线程池状态"><a class="markdownIt-Anchor" href="#线程池状态"></a> # 线程池状态</h1><p><img src="https://pic2.zhimg.com/80/v2-9ad6dad52f1bf581fed962a762ee312d_1440w.webp" alt="img"></p><ul><li>RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务。</li><li>SHUTDOWN:指调用了 shutdown() 方法,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。</li><li>STOP:指调用了 shutdownNow() 方法,不再接受新提交的任务,同时抛弃阻塞队列里的所有任务并中断所有正在执行任务。</li><li>TIDYING: 所有任务都执行完毕,workerCount 有效线程数为 0。</li><li>TERMINATED:终止状态,当执行 terminated() 后会更新为这个状态。</li></ul><h1 id="线程状态"><a class="markdownIt-Anchor" href="#线程状态"></a> # 线程状态</h1><p>线程实际上是分为六种状态的,既</p><ul><li><p><strong>1.初始状态(NEW)</strong></p><ul><li>线程被构建,但是还没有调用start方法</li></ul></li><li><p><strong>2.运行状态(RUNNABLE)</strong></p><ul><li>Java线程把操作系统中就绪和运行两种状态统一称为“运行中”</li></ul></li><li><p><strong>3.阻塞状态(BLOCKED)</strong><br>表示线程进入等待状态,也就是线程因为某种原因放弃了CPU的使用权,阻塞也分为几种情况(当一个线程试图获取一个内部的对象锁(非java.util.concurrent库中的锁),而该锁被其他线程持有,则该线程进入阻塞状态。)</p></li></ul><p> <strong>等待阻塞</strong>:运行的线程执行了Thread.sleep、wait、join等方法,JVM会把当前线程设置为等待状态,当sleep结束,join线程终止或者线程被唤醒后,该线程从等待状态进入阻塞状态,重新占用锁后进行线程恢复</p><p> <strong>同步阻塞</strong>:运行的线程在获取对象的同步锁时,若该同步锁被其他线程锁占用了,那么JVM会把当前项城放入到锁池中</p><p> <strong>其他阻塞</strong>:发出I/O请求,JVM会把当前线程设置为阻塞状态,当I/O处理完毕则线程恢复</p><ul><li><strong>4.等待(WAITING)</strong><ul><li>等待状态,没有超时时间(无限等待),要被其他线程或者有其他的中断操作</li></ul></li></ul><p> 执行wait、join、LockSupport.park()</p><ul><li><strong>5.超时等待(TIME_WAITING)</strong><ul><li>与等待不同的是,不是无限等待,超时后自动返回</li></ul></li></ul><p> 执行sleep,带参数的wait等可以实现</p><ul><li><strong>6.终止(Teminated)</strong><br>代表线程执行完毕</li></ul><p><img src="../../../images/image-20230719235443525.png" alt="image-20230719235443525"></p><h1 id="核心线程-和-救急线程的区别"><a class="markdownIt-Anchor" href="#核心线程-和-救急线程的区别"></a> # 核心线程 和 救急线程的区别</h1><p>救急线程是有个生存时间的,它执行完任务了,过了一段时间,没有新任务了,救急线程就会销毁掉,变成结束的状态</p><p>核心线程没有生存时间,它执行完任务后,它仍然会被保存在线程池中,不会让核心线程结束,会让核心线程一直去运行</p><p>KeepAliveTime 生存时间、unit时间单位,这两个参数就是针对于救急线程的</p><p><img src="../../../images/image-20221014221100359.png" alt="image-20221014221100359"></p><p>使用救急线程的前提,是要配合有界队列的使用。</p><p>如果队列选择了有界队列,那么任务超过了队列大小时,会创建 maximumPoolSize - corePoolSize 数目的线程来救急。</p><p>如果队列选择的是无界队列,那么就不会用到救急线程,任务会一直存入无界队列,然后由核心线程来轮流去处理无界队列里的任务。</p><p>如果线程到达 maximumPoolSize 仍然有新任务这时会执行拒绝策略。拒绝策略 jdk 提供了 4 种实现,</p><p>但是很多第三方框架都不是使用的jdk提供的,而是选择使用 更功能上的增强,在这些 功能上进行扩展</p><p><img src="../../../images/image-20221014222804634.png" alt="image-20221014222804634"></p><h1 id="executors-固定大小线程池"><a class="markdownIt-Anchor" href="#executors-固定大小线程池"></a> # Executors-固定大小线程池</h1><p><img src="../../../images/image-20221015090229540.png" alt="image-20221015090229540"></p><h1 id="executors-单线程线程池"><a class="markdownIt-Anchor" href="#executors-单线程线程池"></a> # Executors-单线程线程池</h1><p><img src="../../../images/image-20221015091919686.png" alt="image-20221015091919686"></p><h1 id="线程池中shutdown和shutdownnow方法的区别"><a class="markdownIt-Anchor" href="#线程池中shutdown和shutdownnow方法的区别"></a> # 线程池中shutdown()和shutdownNow()方法的区别</h1><p>一般情况下,当我们频繁的使用线程的时候,为了节约资源快速响应需求,我们都会考虑使用线程池,线程池使用完毕都会想着关闭,关闭的时候一般情况下会用到shutdown和shutdownNow,这两个函数都能够用来关闭线程池,那么他们俩之间的区别是什么呢?下面我就用一句话来说明白shutdown和shutdownNow的区别。</p><p>一句话说明白shutdown和shutdownNow的区别:</p><p>shutdown只是将线程池的状态设置为SHUTWDOWN状态,正在执行的任务会继续执行下去,没有被执行的则中断。</p><p>而shutdownNow则是将线程池的状态设置为STOP,正在执行的任务则被停止,没被执行任务的则返回。</p><p> 例子:举个工人吃包子的例子,一个厂的工人(Workers)正在吃包子(可以理解为任务),假如接到shutdown的命令,那么这个厂的工人们则会把手头上的包子给吃完,没有拿到手里的笼子里面的包子则不能吃!而如果接到shutdownNow的命令以后呢,这些工人们立刻停止吃包子,会把手头上没吃完的包子放下,更别提笼子里的包子了。</p><p>1、<strong>shutDown()</strong></p><p>当线程池调用该方法时,线程池的状态则立刻变成SHUTDOWN状态。此时,则不能再往线程池中添加任何任务,否则将会抛出RejectedExecutionException异常。但是,此时线程池不会立刻退出,<strong>直到添加到线程池中的任务都已经处理完成,才会退出。</strong></p><p>2、<strong>shutdownNow()</strong></p><p>执行该方法,线程池的状态立刻变成STOP状态,并试图停止所有正在执行的线程,<strong>不再处理还在池队列中等待的任务</strong>,当然,它会返回那些未执行的任务。<br>它试图终止线程的方法是通过调用Thread.interrupt()方法来实现的,但是大家知道,这种方法的作用有限,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。</p><p><img src="../../../images/image-20221015095349872.png" alt="image-20221015095349872"></p><p><img src="../../../images/image-20221015095425618.png" alt="image-20221015095425618"></p><h1 id="aba-问题及解决"><a class="markdownIt-Anchor" href="#aba-问题及解决"></a> # ABA 问题及解决</h1><p><strong>CAS引发的ABA问题</strong></p><p>ABA问题是指在CAS操作时,其他线程将变量值A改为了B,但是又被改回了A,等到本线程使用期望值A与当前变量进行比较时,发现变量A没有变,于是CAS就将A值进行了交换操作,但是实际上该值已经被其他线程改变过,这与乐观锁的设计思想不符合。ABA问题的解决思路是,每次变量更新的时候把变量的版本号加1,那么A-B-A就会变成A1-B2-A3,只要变量被某一线程修改过,改变量对应的版本号就会发生递增变化,从而解决了ABA问题。在JDK的java.util.concurrent.atomic包中提供了<strong>AtomicStampedReference</strong>来解决ABA问题,该类的compareAndSet是该类的核心方法,实现如下:</p><figure class="highlight java"><table><tbody><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="keyword">public</span> <span class="type">boolean</span> <span class="title function_">compareAndSet</span><span class="params">(V expectedReference,</span></span><br><span class="line"><span class="params"> V newReference,</span></span><br><span class="line"><span class="params"> <span class="type">int</span> expectedStamp,</span></span><br><span class="line"><span class="params"> <span class="type">int</span> newStamp)</span> {</span><br><span class="line"> Pair<V> current = pair;</span><br><span class="line"> <span class="keyword">return</span></span><br><span class="line"> expectedReference == current.reference &&</span><br><span class="line"> expectedStamp == current.stamp &&</span><br><span class="line"> ((newReference == current.reference &&</span><br><span class="line"> newStamp == current.stamp) ||</span><br><span class="line"> casPair(current, Pair.of(newReference, newStamp)));</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>我们可以发现,该类检查了当前引用与当前标志是否与预期相同,如果全部相等,才会以原子方式将该引用和该标志的值设为新的更新值,这样CAS操作中的比较就不依赖于变量的值了。</p><p>但是有时候,并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了</p><p><strong>AtomicMarkableReference</strong>:<br>基本和AtomicStampedReference差不多,AtomicStampedReference主要关注版本号,即reference的值被修改了多少次;AtomicMarkableReference是使用boolean mark来标记reference是否被修改过</p><h1 id="synchronized-和-lock-的主要区别"><a class="markdownIt-Anchor" href="#synchronized-和-lock-的主要区别"></a> # Synchronized 和 Lock 的主要区别</h1><p>Synchronzied 和 Lock 的主要区别如下:</p><ul><li><p><strong>存在层面</strong>:Syncronized 是Java 中的一个关键字,存在于 JVM 层面,Lock 是 Java 中的一个接口</p></li><li><p><strong>锁的释放条件</strong>:1. 获取锁的线程执行完同步代码后,自动释放;2. 线程发生异常时,JVM会让线程释放锁;Lock 必须在 finally 关键字中释放锁,不然容易造成线程死锁</p></li><li><p><strong>锁的获取</strong>: 在 Syncronized 中,假设线程 A 获得锁,B 线程等待。如果 A 发生阻塞,那么 B 会一直等待。在 Lock 中,会分情况而定,Lock 中有尝试获取锁的方法,如果尝试获取到锁,则不用一直等待</p></li><li><p><strong>锁的状态</strong>:Synchronized 无法判断锁的状态,Lock 则可以判断</p></li><li><p><strong>锁的类型</strong>:Synchronized 是可重入,不可中断,非公平锁;Lock 锁则是 可重入,可判断,可公平锁</p></li><li><p><strong>锁的性能</strong>:Synchronized 适用于少量同步的情况下,性能开销比较大。Lock 锁适用于大量同步阶段:</p></li><li><p>Lock 锁可以提高多个线程进行读的效率(使用 readWriteLock)</p></li><li><p>在竞争不是很激烈的情况下,Synchronized的性能要优于ReetrantLock,但是在资源竞争很激烈的情况下,Synchronized的性能会下降几十倍,但是ReetrantLock的性能能维持常态;</p></li><li><p>ReetrantLock 提供了多样化的同步,比如有时间限制的同步,可以被Interrupt的同步(synchronized的同步是不能Interrupt的)等。</p></li></ul>]]></content>
<summary type="html"><h1 id="线程池状态"><a class="markdownIt-Anchor" href="#线程池状态"></a> # 线程池状态</h1>
<p><img src="https://pic2.zhimg.com/80/v2-9ad6dad52f1bf581fed962</summary>
</entry>
<entry>
<title>Redis的缓存穿透、缓存击穿、缓存雪崩</title>
<link href="http://example.com/2023/07/15/interview/redis/Redis%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E3%80%81%E5%87%BB%E7%A9%BF%E3%80%81%E9%9B%AA%E5%B4%A9/"/>
<id>http://example.com/2023/07/15/interview/redis/Redis%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E3%80%81%E5%87%BB%E7%A9%BF%E3%80%81%E9%9B%AA%E5%B4%A9/</id>
<published>2023-07-15T05:43:47.665Z</published>
<updated>2023-07-28T15:13:14.026Z</updated>
<content type="html"><![CDATA[<h1 id="redis的缓存穿透-缓存击穿-缓存雪崩"><a class="markdownIt-Anchor" href="#redis的缓存穿透-缓存击穿-缓存雪崩"></a> Redis的缓存穿透、缓存击穿、缓存雪崩</h1><h1 id="一-概述"><a class="markdownIt-Anchor" href="#一-概述"></a> 一、概述</h1><p><strong>① 缓存穿透</strong>:大量请求根本不存在的key</p><p><strong>② 缓存雪崩</strong>:Redis中大量key集体过期</p><p><strong>③ 缓存击穿</strong>:Redis中一个热点key过期</p><p>三者出现的根本原因:Redis命中率下降,请求直接打在DB上<br>正常情况下,大量的资源请求都会被redis响应,在redis得不到响应的小部分请求才会去请求DB,这样DB的压力是非常小的,是可以正常工作的(如下图)</p><p><img src="../../../images/image-20230715135117005.png" alt="image-20230715135117005"></p><p> 如果大量的请求在redis上得不到响应,那么就会导致这些请求会直接去访问DB,导致DB的压力瞬间变大而卡死或者宕机。如下图:</p><p>① 大量的高并发的请求打在redis上</p><p>② 这些请求发现redis上并没有需要请求的资源,redis命中率降低</p><p>③ 因此这些大量的高并发请求转向DB(数据库服务器)请求对应的资源</p><p>④ DB压力瞬间增大,直接将DB打垮,进而引发一系列“灾害”</p><p><img src="../../../images/image-20230715135155569.png" alt="image-20230715135155569"></p><p>那么为什么redis会没有需要访问的数据呢?通过分析大致可以总结为三种情况,也就对应着redis的雪崩、穿透和击穿(下文开始进行详解)</p><p><img src="../../../images/image-20230715135236580.png" alt="image-20230715135236580"></p><h1 id="二-情景分析-详解"><a class="markdownIt-Anchor" href="#二-情景分析-详解"></a> 二、情景分析 (详解)</h1><h2 id="一缓存击穿"><a class="markdownIt-Anchor" href="#一缓存击穿"></a> (一)缓存击穿</h2><h3 id="概念"><a class="markdownIt-Anchor" href="#概念"></a> 概念:</h3><p>产生缓存雪崩的原因:redis中的某个热点key过期,但是此时有大量的用户访问该过期key</p><p><img src="../../../images/image-20230715135655943.png" alt="image-20230715135655943"></p><h3 id="情景"><a class="markdownIt-Anchor" href="#情景"></a> 情景:</h3><p>缓存击穿的原因通常有以下几种:</p><ol><li><p>缓存中不存在所需的热点数据:当系统中某个热点数据需要被频繁访问时,如果这个热点数据最开始没有被缓存,那么就会导致系统每次请求都需要直接查询数据库,造成数据库负担。</p></li><li><p>缓存的热点数据过期:当一个热点数据过期并需要重新缓存时,如果此时有大量请求,那么就会导致所有请求都要直接查询数据库。</p><pre><code> 类似于“某男明星塌房事件”上了热搜,这时候大量的“粉丝”都在访问该热点事件,但是可能由于某种原因,redis的这个热点key过期了,那么这时候大量高并发对于该key的请求就得不到redis的响应,那么就会将请求直接打在DB服务器上,导致整个DB瘫痪。</code></pre></li></ol><h3 id="解决方案"><a class="markdownIt-Anchor" href="#解决方案"></a> 解决方案:</h3><h4 id="1设置永不过期提前对热点数据进行设置"><a class="markdownIt-Anchor" href="#1设置永不过期提前对热点数据进行设置"></a> 1.设置永不过期(提前对热点数据进行设置)</h4><p>类似于新闻、某博等软件都需要对热点数据进行预先设置在redis中</p><h4 id="2加锁排队"><a class="markdownIt-Anchor" href="#2加锁排队"></a> 2.加锁排队</h4><h5 id="方式一双重检查锁"><a class="markdownIt-Anchor" href="#方式一双重检查锁"></a> (方式一)双重检查锁:</h5><p>只有一个请求A可以获取到互斥锁,其它请求在外排队,然后线程A到DB中将数据查询并返回到Redis,之后所有请求就可以从Redis中得到响应(这些请求有两种情况:一,已经进入排队的请求获得锁之后,可在第二重查询redis中获取数据;二,没有进入排队的请求【也就是没有通过 if(obj == null) 而进入争取锁的队列中的请求】,直接在外部的查询redis获取到数据)</p><p><img src="../../../images/image-20230715140125819.png" alt="image-20230715140125819"></p><h5 id="方式二分布式锁"><a class="markdownIt-Anchor" href="#方式二分布式锁"></a> (方式二)分布式锁:</h5><h5 id="不好之处"><a class="markdownIt-Anchor" href="#不好之处"></a> 不好之处:</h5><p>高并发的情况下,影响性能。但大多数情况下访问是可以从外层就可以获取到缓存数据的了,而只有在偶尔的情况下会因为key突然过期,才会导致那个时间的请求进入锁机制,而且进入排队的,也有二重检查来减轻对数据库的压力。</p><h4 id="3监控数据适时调整"><a class="markdownIt-Anchor" href="#3监控数据适时调整"></a> 3.监控数据,适时调整</h4><p>监控哪些数据是热门数据,实时的调整key的过期时长</p><h2 id="二缓存雪崩"><a class="markdownIt-Anchor" href="#二缓存雪崩"></a> (二)缓存雪崩</h2><h3 id="概念-2"><a class="markdownIt-Anchor" href="#概念-2"></a> 概念:</h3><p>缓存雪崩产生的原因:redis中大量的key集体过期</p><p><img src="../../../images/image-20230715141420609.png" alt="image-20230715141420609"></p><p>举例:</p><pre><code> 当redis中的大量key集体过期,可以理解为redis中的大部分数据都被清空了(失效了),那么这时候如果有大量并发的请求来到,那么redis就无法进行有效的响应(命中率急剧下降),请求就都打到DB上了,到时DB直接崩溃</code></pre><h3 id="情景-2"><a class="markdownIt-Anchor" href="#情景-2"></a> 情景:</h3><ul><li><p><strong>大量key集体过期</strong></p><ul><li>解决方法<ul><li>1.加锁排队 + 将失效时间分散开</li><li>2.使用多级缓存架构</li><li>3.设置缓存标记</li></ul></li></ul></li><li><p><strong>Redis服务宕机</strong></p><ul><li>解决方法:redis高可用(集群、哨兵模式)</li></ul></li><li><p><strong>机房断电</strong></p><ul><li>解决方法:提前做好灾备,做好多机房,一个机房挂掉了,马上切换到另外一个地方的机房</li></ul></li></ul><h3 id="解决方式"><a class="markdownIt-Anchor" href="#解决方式"></a> 解决方式:</h3><h4 id="1加锁排队-将失效时间分散开"><a class="markdownIt-Anchor" href="#1加锁排队-将失效时间分散开"></a> 1.加锁排队 + 将失效时间分散开</h4><p>通过使用自动生成随机数使得key的过期时间是随机的,防止集体过期</p><p><img src="../../../images/image-20230715142038202.png" alt="image-20230715142038202"></p><h4 id="2使用多级架构"><a class="markdownIt-Anchor" href="#2使用多级架构"></a> 2.使用多级架构</h4><p>使用nginx缓存+redis缓存+其他缓存,不同层使用不同(过期时间)的缓存,可靠性更强</p><h4 id="3设置缓存标记"><a class="markdownIt-Anchor" href="#3设置缓存标记"></a> 3.设置缓存标记</h4><p>记录缓存数据是否过期,如果过期会去跟新实际的key。</p><p>(1)<strong>不另外启一个线程</strong>,而是在value里面,储存了个逻辑过期时间(相当于实际过期时间我们设置1小时,但逻辑过期时间可能是50分钟),取值的时候,判断 实际时间 > 逻辑时间,则进行加锁更新,其余的线程,拿不到锁的先全部返回旧数据。</p><p>(2)<strong>异步处理</strong>:但判断 实际时间 > 逻辑时间,通知另外的线程进行更新</p><h4 id="4redis高可用集群-哨兵模式"><a class="markdownIt-Anchor" href="#4redis高可用集群-哨兵模式"></a> 4.redis高可用(集群、哨兵模式)</h4><p>如果是Redis服务宕机,那就需要提前给Redis做好集群,并做好哨兵模式,发现宕机,另外的补上。</p><h2 id="三缓存穿透"><a class="markdownIt-Anchor" href="#三缓存穿透"></a> (三)缓存穿透</h2><h3 id="概念-3"><a class="markdownIt-Anchor" href="#概念-3"></a> 概念:</h3><p>缓存穿透产生的原因:请求根本不存在的资源(DB本身就不存在,Redis更是不存在)</p><p><img src="../../../images/image-20230715143842809.png" alt="image-20230715143842809"></p><p>举例(情景在线):客户端发送大量的不可响应的请求(如下图)</p><p><img src="../../../images/image-20230715143928303.png" alt="image-20230715143928303"></p><pre><code> 当大量的客户端发出类似于:http://localhost:8080/user/19833?id=-3872 的请求,就可能导致出现缓存穿透的情况。因为数据库DB中本身就没有id=-3872的用户的数据,所以Redis也没有对应的数据,那么这些请求在redis就得不到响应,就会直接打在DB上,导致DB压力过大而卡死情景在线或宕机。 缓存穿透很有可能是黑客攻击所为,黑客通过发送大量的高并发的无法响应的请求给服务器,由于请求的资源根本就不存在,DB就很容易被打垮了。</code></pre><h3 id="解决方式-2"><a class="markdownIt-Anchor" href="#解决方式-2"></a> 解决方式:</h3><h4 id="1缓存空对象加锁排队-将失效时间分散开"><a class="markdownIt-Anchor" href="#1缓存空对象加锁排队-将失效时间分散开"></a> 1.缓存空对象(+加锁排队 + 将失效时间分散开)</h4><p><img src="../../../images/image-20230715144221629.png" alt="image-20230715144221629"></p><ul><li>类似于上面的例子,虽然数据库中没有id=-3872的用户的数据,但是在redis中对他进行缓存(key=-3872,value=null),这样当请求到达redis的时候就会直接返回一个null的值给客户端,避免了大量无法访问的数据直接打在DB上<ul><li>注意:<ul><li>使用空值作为缓存的时候,key设置的过期时间不能太长,防止占用太多redis资源(比如大量的恶意攻击)</li><li>当前访问的数据可能当时数据库中没有,但后面可能会有,所以设置过期时间不能太长,建议随机的短时间</li></ul></li></ul></li></ul><h4 id="2布隆过滤器"><a class="markdownIt-Anchor" href="#2布隆过滤器"></a> 2.布隆过滤器</h4><ul><li>黑名单:把请求不存在的数据存进黑名单,下次访问数据前先判断布隆过滤器中是都存在该key,存在则拒绝访问。</li><li>白名单:把数据库存在的数据存进布隆过滤器,请求访问判断到布隆过滤器中有才释放后续访问数据,不存在则拒绝后续访问。</li></ul><p><strong>注意:</strong></p><ol><li><p>要做好数据同步,因为不是所有的数据都是一直在黑名单或白名单的,增删改会导致变动。所以这种方式的缺点就是要做数据同步。</p></li><li><p>布隆过滤器是有一定的误差,所以一般需要配合一些接口流量的限制(规定用户在一段时间内访问的频率)、权限校验、黑名单等来解决缓存穿透的问题</p></li></ol><h4 id="3实时监控"><a class="markdownIt-Anchor" href="#3实时监控"></a> 3.实时监控:</h4><p> 对redis进行实时监控,当发现redis中的命中率下降的时候进行原因的排查,配合运维人员对访问对象和访问数据进行分析查询,从而进行黑名单的设置限制服务(拒绝黑客攻击)</p><h4 id="4接口校验"><a class="markdownIt-Anchor" href="#4接口校验"></a> 4.接口校验</h4><p> 类似于用户权限的拦截,对于id=-3872这些无效访问就直接拦截,不允许这些请求到达Redis、DB上。</p>]]></content>
<summary type="html"><h1 id="redis的缓存穿透-缓存击穿-缓存雪崩"><a class="markdownIt-Anchor" href="#redis的缓存穿透-缓存击穿-缓存雪崩"></a> Redis的缓存穿透、缓存击穿、缓存雪崩</h1>
<h1 id="一-概述"><a clas</summary>
<category term="面试题" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="中间件" scheme="http://example.com/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
<category term="Redis" scheme="http://example.com/categories/%E4%B8%AD%E9%97%B4%E4%BB%B6/Redis/"/>
<category term="Redis" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/Redis/"/>
<category term="面试题" scheme="http://example.com/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="Redis" scheme="http://example.com/tags/Redis/"/>
<category term="中间件" scheme="http://example.com/tags/%E4%B8%AD%E9%97%B4%E4%BB%B6/"/>
<category term="数据库" scheme="http://example.com/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
</entry>
<entry>
<title></title>
<link href="http://example.com/2023/07/14/interview/redis/Redis%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E3%80%81%E5%87%BB%E7%A9%BF%E3%80%81%E9%9B%AA%E5%B4%A9/Redis%E7%BC%93%E5%AD%98/"/>
<id>http://example.com/2023/07/14/interview/redis/Redis%E7%BC%93%E5%AD%98%E7%A9%BF%E9%80%8F%E3%80%81%E5%87%BB%E7%A9%BF%E3%80%81%E9%9B%AA%E5%B4%A9/Redis%E7%BC%93%E5%AD%98/</id>
<published>2023-07-14T13:07:17.849Z</published>
<updated>2023-07-28T15:13:21.786Z</updated>
<content type="html"><![CDATA[<h1 id="spring-boot中如何解决redis的缓存穿透-缓存击穿-缓存雪崩"><a class="markdownIt-Anchor" href="#spring-boot中如何解决redis的缓存穿透-缓存击穿-缓存雪崩"></a> Spring Boot中如何解决Redis的缓存穿透、缓存击穿、缓存雪崩?</h1><p>大家好,我是飘渺!今天给大家介绍一下如何在SpringBoot中解决Redis的缓存穿透、缓存击穿、缓存雪崩的问题。</p><h2 id="缓存穿透"><a class="markdownIt-Anchor" href="#缓存穿透"></a> 缓存穿透</h2><h3 id="什么是缓存穿透"><a class="markdownIt-Anchor" href="#什么是缓存穿透"></a> 什么是缓存穿透</h3><p>缓存穿透指的是一个缓存系统无法缓存某个查询的数据,从而导致这个查询每一次都要访问数据库。</p><p>常见的Redis缓存穿透场景包括:</p><ol><li>查询一个不存在的数据:攻击者可能会发送一些无效的查询来触发缓存穿透。</li><li>查询一些非常热门的数据:如果一个数据被访问的非常频繁,那么可能会导致缓存系统无法处理这些请求,从而造成缓存穿透。</li><li>查询一些异常数据:这种情况通常发生在数据服务出现故障或异常时,从而造成缓存系统无法访问相关数据,从而导致缓存穿透。</li></ol><h3 id="如何解决"><a class="markdownIt-Anchor" href="#如何解决"></a> 如何解决</h3><p>我们可以使用Guava在内存中维护一个布隆过滤器。具体步骤如下:</p><ol><li>添加Guava和Redis依赖:</li></ol><figure class="highlight xml"><table><tbody><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">xml复制代码<span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>com.google.guava<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>guava<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>29.0-jre<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-data-redis<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></tbody></table></figure><ol><li>创建一个BloomFilterUtil类,用于在缓存中维护Bloom Filter。</li></ol><figure class="highlight java"><table><tbody><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">java复制代码<span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">BloomFilterUtil</span> {</span><br><span class="line"> <span class="comment">// 布隆过滤器的预计容量</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">expectedInsertions</span> <span class="operator">=</span> <span class="number">1000000</span>;</span><br><span class="line"> <span class="comment">// 布隆过滤器误判率</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">double</span> <span class="variable">fpp</span> <span class="operator">=</span> <span class="number">0.001</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> BloomFilter<String> bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), expectedInsertions, fpp);</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 向Bloom Filter中添加元素</span></span><br><span class="line"><span class="comment"> */</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_">add</span><span class="params">(String key)</span>{</span><br><span class="line"> bloomFilter.put(key);</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 判断元素是否存在于Bloom Filter中</span></span><br><span class="line"><span class="comment"> */</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_">mightContain</span><span class="params">(String key)</span>{</span><br><span class="line"> <span class="keyword">return</span> bloomFilter.mightContain(key);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><ol><li>在Controller中查询数据时,先根据请求参数进行Bloom Filter的过滤</li></ol><figure class="highlight java"><table><tbody><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></pre></td><td class="code"><pre><span class="line">java复制代码<span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> RedisTemplate<String, Object> redisTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping("/user/{id}")</span></span><br><span class="line"><span class="keyword">public</span> User <span class="title function_">getUserById</span><span class="params">(<span class="meta">@PathVariable</span> Long id)</span>{</span><br><span class="line"> <span class="comment">// 先从布隆过滤器中判断此id是否存在</span></span><br><span class="line"> <span class="keyword">if</span>(!BloomFilterUtil.mightContain(id.toString())){</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 class="comment">// 查询缓存数据</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">userKey</span> <span class="operator">=</span> <span class="string">"user_"</span>+id.toString();</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> (User) redisTemplate.opsForValue().get(userKey);</span><br><span class="line"> <span class="keyword">if</span>(user == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 查询数据库</span></span><br><span class="line"> user = userRepository.findById(id).orElse(<span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">if</span>(user != <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 将查询到的数据加入缓存</span></span><br><span class="line"> redisTemplate.opsForValue().set(userKey, user, <span class="number">300</span>, TimeUnit.SECONDS);</span><br><span class="line"> }<span class="keyword">else</span>{</span><br><span class="line"> <span class="comment">// 查询结果为空,将请求记录下来,并在布隆过滤器中添加</span></span><br><span class="line"> BloomFilterUtil.add(id.toString());</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> user;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="缓存击穿"><a class="markdownIt-Anchor" href="#缓存击穿"></a> 缓存击穿</h2><h3 id="什么是缓存击穿"><a class="markdownIt-Anchor" href="#什么是缓存击穿"></a> 什么是缓存击穿</h3><p>缓存击穿指的是在一些高并发访问下,一个热点数据从缓存中不存在,每次请求都要直接查询数据库,从而导致数据库压力过大,并且系统性能下降的现象。</p><p>缓存击穿的原因通常有以下几种:</p><ol><li>缓存中不存在所需的热点数据:当系统中某个热点数据需要被频繁访问时,如果这个热点数据最开始没有被缓存,那么就会导致系统每次请求都需要直接查询数据库,造成数据库负担。</li><li>缓存的热点数据过期:当一个热点数据过期并需要重新缓存时,如果此时有大量请求,那么就会导致所有请求都要直接查询数据库。</li></ol><h3 id="如何解决-2"><a class="markdownIt-Anchor" href="#如何解决-2"></a> 如何解决</h3><p>主要思路 : <strong>在遇到缓存击穿问题时,我们可以在查询数据库之前,先判断一下缓存中是否已有数据,如果没有数据则使用Redis的单线程特性,先查询数据库然后将数据写入缓存中。</strong></p><ol><li>添加Redis依赖</li></ol><figure class="highlight xml"><table><tbody><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">xml复制代码<span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-data-redis<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></tbody></table></figure><ol><li>在Controller中查询数据时,先从缓存中查询数据,如果缓存中无数据则进行锁操作</li></ol><figure class="highlight java"><table><tbody><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">java复制代码<span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> RedisTemplate<String, Object> redisTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping("/user/{id}")</span></span><br><span class="line"><span class="keyword">public</span> User <span class="title function_">getUserById</span><span class="params">(<span class="meta">@PathVariable</span> Long id)</span>{</span><br><span class="line"> <span class="comment">// 先从缓存中获取值</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">userKey</span> <span class="operator">=</span> <span class="string">"user_"</span>+id.toString();</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> (User) redisTemplate.opsForValue().get(userKey);</span><br><span class="line"> <span class="keyword">if</span>(user == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 查询数据库之前加锁</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">lockKey</span> <span class="operator">=</span> <span class="string">"lock_user_"</span>+id.toString();</span><br><span class="line"> <span class="type">String</span> <span class="variable">lockValue</span> <span class="operator">=</span> UUID.randomUUID().toString();</span><br><span class="line"> <span class="keyword">try</span>{</span><br><span class="line"> <span class="type">Boolean</span> <span class="variable">lockResult</span> <span class="operator">=</span> redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, <span class="number">60</span>, TimeUnit.SECONDS);</span><br><span class="line"> <span class="keyword">if</span>(lockResult != <span class="literal">null</span> && lockResult){</span><br><span class="line"> <span class="comment">// 查询数据库</span></span><br><span class="line"> user = userRepository.findById(id).orElse(<span class="literal">null</span>);</span><br><span class="line"> <span class="keyword">if</span>(user != <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 将查询到的数据加入缓存</span></span><br><span class="line"> redisTemplate.opsForValue().set(userKey, user, <span class="number">300</span>, TimeUnit.SECONDS);</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"> <span class="comment">// 释放锁</span></span><br><span class="line"> <span class="keyword">if</span>(lockValue.equals(redisTemplate.opsForValue().get(lockKey))){</span><br><span class="line"> redisTemplate.delete(lockKey);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> user;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h2 id="缓存雪崩"><a class="markdownIt-Anchor" href="#缓存雪崩"></a> 缓存雪崩</h2><h3 id="什么是缓存雪崩"><a class="markdownIt-Anchor" href="#什么是缓存雪崩"></a> 什么是缓存雪崩</h3><p>指缓存中大量数据的失效时间集中在某一个时间段,导致在这个时间段内缓存失效并额外请求数据库查询数据的请求大量增加,从而对数据库造成极大的压力和负荷。</p><p>常见的Redis缓存雪崩场景包括:</p><ol><li>缓存服务器宕机:当缓存服务器宕机或重启时,大量的访问请求将直接命中数据库,并在同一时间段内导致大量的数据库查询请求,从而将数据库压力大幅提高。</li><li>缓存数据同时失效:在某个特定时间点,缓存中大量数据的失效时间集中在一起,这些数据会在同一时间段失效,并且这些数据被高频访问,将导致大量的访问请求去查询数据库。</li><li>缓存中数据过期时间设计不合理:当缓存中的数据有效时间过短,且数据集中在同一时期失效时,就容易导致大量的请求直接查询数据库,加剧数据库压力。</li><li>波动式的访问过程:当数据的访问存在波动式特征时,例如输出某些活动物品或促销商品时,将会带来高频的查询请求访问,导致缓存大量失效并产生缓存雪崩。</li></ol><h3 id="如何解决-3"><a class="markdownIt-Anchor" href="#如何解决-3"></a> 如何解决</h3><p>在遇到缓存雪崩时,我们可以使用两种方法:一种是将缓存过期时间分散开,即为不同的数据设置不同的过期时间;另一种是使用Redis的多级缓存架构,通过增加一层代理层来解决。具体步骤如下:</p><ol><li>添加相关依赖</li></ol><figure class="highlight xml"><table><tbody><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">xml复制代码<span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>org.springframework.boot<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>spring-boot-starter-data-redis<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br><span class="line"><span class="tag"><<span class="name">dependency</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">groupId</span>></span>net.sf.ehcache<span class="tag"></<span class="name">groupId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">artifactId</span>></span>ehcache<span class="tag"></<span class="name">artifactId</span>></span></span><br><span class="line"> <span class="tag"><<span class="name">version</span>></span>2.10.6<span class="tag"></<span class="name">version</span>></span></span><br><span class="line"><span class="tag"></<span class="name">dependency</span>></span></span><br></pre></td></tr></tbody></table></figure><ol><li>在application.properties中配置Ehcache缓存</li></ol><figure class="highlight properties"><table><tbody><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="attr">properties</span></span><br><span class="line"><span class="attr">复制代码spring.cache.type</span>=<span class="string">ehcache</span></span><br></pre></td></tr></tbody></table></figure><ol><li>创建一个CacheConfig类,用于配置Ehcache:</li></ol><figure class="highlight java"><table><tbody><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">java复制代码<span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@EnableCaching</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CacheConfig</span> {</span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> EhCacheCacheManager <span class="title function_">ehCacheCacheManager</span><span class="params">(CacheManager cm)</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">EhCacheCacheManager</span>(cm);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Bean</span></span><br><span class="line"> <span class="keyword">public</span> CacheManager <span class="title function_">ehCacheManager</span><span class="params">()</span>{</span><br><span class="line"> <span class="type">EhCacheManagerFactoryBean</span> <span class="variable">cmfb</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">EhCacheManagerFactoryBean</span>();</span><br><span class="line"> cmfb.setConfigLocation(<span class="keyword">new</span> <span class="title class_">ClassPathResource</span>(<span class="string">"ehcache.xml"</span>));</span><br><span class="line"> cmfb.setShared(<span class="literal">true</span>);</span><br><span class="line"> <span class="keyword">return</span> cmfb.getObject();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><ol><li>在ehcache.xml中添加缓存配置</li></ol><figure class="highlight xml"><table><tbody><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">xml复制代码<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="tag"><<span class="name">ehcache</span> <span class="attr">xmlns:xsi</span>=<span class="string">"http://www.w3.org/2001/XMLSchema-instance"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">xsi:noNamespaceSchemaLocation</span>=<span class="string">"http://ehcache.org/ehcache.xsd"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">updateCheck</span>=<span class="string">"true"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">monitoring</span>=<span class="string">"autodetect"</span></span></span><br><span class="line"><span class="tag"> <span class="attr">dynamicConfig</span>=<span class="string">"true"</span>></span></span><br><span class="line"></span><br><span class="line"> <span class="tag"><<span class="name">cache</span> <span class="attr">name</span>=<span class="string">"userCache"</span> <span class="attr">maxEntriesLocalHeap</span>=<span class="string">"10000"</span> <span class="attr">timeToLiveSeconds</span>=<span class="string">"60"</span> <span class="attr">timeToIdleSeconds</span>=<span class="string">"30"</span>/></span></span><br><span class="line"></span><br><span class="line"><span class="tag"></<span class="name">ehcache</span>></span></span><br></pre></td></tr></tbody></table></figure><ol><li>在Controller中查询数据时,先从Ehcache缓存中获取,如果缓存中无数据则再从Redis缓存中获取数据</li></ol><figure class="highlight java"><table><tbody><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">java复制代码<span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> RedisTemplate<String, Object> redisTemplate;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Autowired</span></span><br><span class="line"><span class="keyword">private</span> CacheManager ehCacheManager;</span><br><span class="line"></span><br><span class="line"><span class="meta">@GetMapping("/user/{id}")</span></span><br><span class="line"><span class="meta">@Cacheable(value = "userCache", key = "#id")</span></span><br><span class="line"><span class="keyword">public</span> User <span class="title function_">getUserById</span><span class="params">(<span class="meta">@PathVariable</span> Long id)</span>{</span><br><span class="line"> <span class="comment">// 先从Ehcache缓存中获取</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">userKey</span> <span class="operator">=</span> <span class="string">"user_"</span>+id.toString();</span><br><span class="line"> <span class="type">User</span> <span class="variable">user</span> <span class="operator">=</span> (User) ehCacheManager.getCache(<span class="string">"userCache"</span>).get(userKey).get();</span><br><span class="line"> <span class="keyword">if</span>(user == <span class="literal">null</span>){</span><br><span class="line"> <span class="comment">// 再从Redis缓存中获取</span></span><br><span class="line"> user = (User) redisTemplate.opsForValue().get(userKey);</span><br><span class="line"> <span class="keyword">if</span>(user != <span class="literal">null</span>){</span><br><span class="line"> ehCacheManager.getCache(<span class="string">"userCache"</span>).put(userKey, user);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">return</span> user;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>以上就是使用SpringBoot时如何解决Redis的缓存穿透、缓存击穿、缓存雪崩的常用方法。</p>]]></content>
<summary type="html"><h1 id="spring-boot中如何解决redis的缓存穿透-缓存击穿-缓存雪崩"><a class="markdownIt-Anchor" href="#spring-boot中如何解决redis的缓存穿透-缓存击穿-缓存雪崩"></a> Spring Boot中如何解</summary>
</entry>
<entry>
<title>为何要用分布式锁&Redis实现分布式锁</title>
<link href="http://example.com/2023/06/04/redis/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/"/>
<id>http://example.com/2023/06/04/redis/%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81/</id>
<published>2023-06-04T02:28:56.289Z</published>
<updated>2023-07-13T15:19:58.999Z</updated>
<content type="html"><![CDATA[<h1 id="为何要用分布式锁"><a class="markdownIt-Anchor" href="#为何要用分布式锁"></a> 为何要用分布式锁</h1><h2 id="一-为什么要使用分布式锁"><a class="markdownIt-Anchor" href="#一-为什么要使用分布式锁"></a> <strong>一、为什么要使用分布式锁</strong></h2><p>为了保证一个方法在高并发情况下的同一时间只能被同一个线程执行,在传统单体应用单机部署的情况下,可以使用Java并发处理相关的API(如ReentrantLcok或synchronized)进行互斥控制。但是,随着业务发展的需要,原单体单机部署的系统被演化成分布式系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,为了解决这个问题就需要一种<strong>跨JVM的互斥机制</strong>来控制共享资源的访问,这就是分布式锁要解决的问题</p><h2 id="二-分布式锁应该具备哪些条件"><a class="markdownIt-Anchor" href="#二-分布式锁应该具备哪些条件"></a> <strong>二、分布式锁应该具备哪些条件</strong></h2><p>在分析分布式锁的三种实现方式之前,先了解一下分布式锁应该具备哪些条件:<br>1、在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行;</p><p>2、高可用的获取锁与释放锁;</p><p>3、高性能的获取锁与释放锁;</p><p>4、具备可重入特性;</p><p>5、具备锁失效机制,防止死锁;</p><p>6、具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败。</p><p><img src="../../images/image-20230604103343211.png" alt="image-20230604103343211"></p><p><img src="../../images/image-20230604103310644.png" alt="image-20230604103310644"></p><h1 id="数据库实现分布式锁原理"><a class="markdownIt-Anchor" href="#数据库实现分布式锁原理"></a> 数据库实现分布式锁原理:</h1><p><img src="../../images/image-20230604102928951.png" alt="image-20230604102928951"></p><h1 id="redis实现分布式锁原理"><a class="markdownIt-Anchor" href="#redis实现分布式锁原理"></a> Redis实现分布式锁原理:</h1><p><img src="../../images/image-20230604103157783.png" alt="image-20230604103157783"></p><p><img src="../../images/image-20230604103116683.png" alt="image-20230604103116683"></p><h1 id="问题注意"><a class="markdownIt-Anchor" href="#问题注意"></a> 问题注意:</h1><ul><li>业务失败锁还在,就会产生死锁,可以加一个过期时间自动释放锁,但是自动释放可能出现释放掉<strong>其他jvm锁</strong>的情况,所以要给锁加一个唯一标识,删除前先看看是不是本机持有的锁,是的话再删除,还要保证查询和删除是一个原子操作,可以使用lua脚本<img src="https://i0.hdslb.com/bfs/emote/bf7e00ecab02171f8461ee8cf439c73db9797748.png@48w_48h.webp" alt="[脱单doge]"></li><li>产生死锁现象,导致虚拟机实例无法再次获取资源,可以设置失效时间,缺陷:因为不确定业务执行时间的长短,所以失效时间的设置具有不确定性。优化:使用try catch finally 语句块,在finally语句中调用del方法删除key完成释放锁的目的,这样下次虚拟机实例请求资源时便能通过setNx()方法获取到锁,执行响应业务逻辑!<img src="https://i0.hdslb.com/bfs/emote/bf7e00ecab02171f8461ee8cf439c73db9797748.png@48w_48h.webp" alt="[脱单doge]"></li></ul><p>看门狗:</p><h3 id="对于redis集群而言可能存在的问题"><a class="markdownIt-Anchor" href="#对于redis集群而言可能存在的问题"></a> 对于Redis集群而言可能存在的问题:</h3><ul><li><strong>问题</strong>:主从切换的时候,主从同步延迟,可能锁信息没有同步到新主</li><li><strong>解决:</strong><ul><li>利用多个redis实例来存储共享,加锁时给每个redis都加锁<ul><li>第一步:获取当前系统时间 (主要为了计算客户端对多个实例加锁所耗费的一个总时间)</li><li>第二步:依次对多个实例进行加锁,加锁完成后,计算客户端对多个实例加锁所耗费的一个总时耗时</li><li>如果加锁的总耗时比锁设置的有效时间短,说明加锁成功</li></ul></li></ul></li></ul><h1 id="redis分布式锁的正确实现方式"><a class="markdownIt-Anchor" href="#redis分布式锁的正确实现方式"></a> Redis分布式锁的正确实现方式</h1><h2 id="前言"><a class="markdownIt-Anchor" href="#前言"></a> 前言</h2><p>分布式锁一般有三种实现方式:</p><ol><li>数据库乐观锁;</li><li>基于Redis的分布式锁;</li><li>基于ZooKeeper的分布式锁。本篇博客将介绍第二种方式,基于Redis实现分布式锁。虽然网上已经有各种介绍Redis分布式锁实现的博客,然而他们的实现却有着各种各样的问题,为了避免误人子弟,本篇博客将详细介绍如何正确地实现Redis分布式锁。</li></ol><hr><h2 id="可靠性"><a class="markdownIt-Anchor" href="#可靠性"></a> 可靠性</h2><p>首先,为了确保分布式锁可用,我们至少要确保锁的实现同时满足以下四个条件:</p><ol><li>互斥性。在任意时刻,只有一个客户端能持有锁。</li><li>不会发生死锁。即使有一个客户端在持有锁的期间崩溃而没有主动解锁,也能保证后续其他客户端能加锁。</li><li>具有容错性。只要大部分的Redis节点正常运行,客户端就可以加锁和解锁。</li><li>解铃还须系铃人。加锁和解锁必须是同一个客户端,客户端自己不能把别人加的锁给解了。</li></ol><hr><h2 id="代码实现"><a class="markdownIt-Anchor" href="#代码实现"></a> 代码实现</h2><h3 id="组件依赖"><a class="markdownIt-Anchor" href="#组件依赖"></a> 组件依赖</h3><p>首先我们要通过Maven引入<code>Jedis</code>开源组件,在<code>pom.xml</code>文件加入下面的代码:</p><figure class="highlight plaintext"><table><tbody><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"><dependency></span><br><span class="line"> <groupId>redis.clients</groupId></span><br><span class="line"> <artifactId>jedis</artifactId></span><br><span class="line"> <version>2.9.0</version></span><br><span class="line"></dependency></span><br></pre></td></tr></tbody></table></figure><h2 id="加锁代码"><a class="markdownIt-Anchor" href="#加锁代码"></a> 加锁代码</h2><h3 id="正确姿势"><a class="markdownIt-Anchor" href="#正确姿势"></a> 正确姿势</h3><p>Talk is cheap, show me the code。先展示代码,再带大家慢慢解释为什么这样实现:</p><figure class="highlight java"><table><tbody><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">RedisTool</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">LOCK_SUCCESS</span> <span class="operator">=</span> <span class="string">"OK"</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">SET_IF_NOT_EXIST</span> <span class="operator">=</span> <span class="string">"NX"</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">SET_WITH_EXPIRE_TIME</span> <span class="operator">=</span> <span class="string">"PX"</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 class="doctag">@param</span> jedis Redis客户端</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> lockKey 锁</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> requestId 请求标识</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> expireTime 超期时间</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 是否获取成功</span></span><br><span class="line"><span class="comment"> */</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_">tryGetDistributedLock</span><span class="params">(Jedis jedis, String lockKey, String requestId, <span class="type">int</span> expireTime)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="type">String</span> <span class="variable">result</span> <span class="operator">=</span> jedis.set(lockKey, requestId, SET_IF_NOT_EXIST, SET_WITH_EXPIRE_TIME, expireTime);</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (LOCK_SUCCESS.equals(result)) {</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 class="keyword">return</span> <span class="literal">false</span>;</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></tbody></table></figure><p>可以看到,我们加锁就一行代码:<code>jedis.set(String key, String value, String nxxx, String expx, int time)</code>,这个set()方法一共有五个形参:</p><ul><li>第一个为key,我们使用key来当锁,因为key是唯一的。</li><li>第二个为value,我们传的是requestId,很多童鞋可能不明白,有key作为锁不就够了吗,为什么还要用到value?原因就是我们在上面讲到可靠性时,分布式锁要满足第四个条件解铃还须系铃人,通过给value赋值为requestId,我们就知道这把锁是哪个请求加的了,在解锁的时候就可以有依据。requestId可以使用<code>UUID.randomUUID().toString()</code>方法生成。</li><li>第三个为nxxx,这个参数我们填的是NX,意思是SET IF NOT EXIST,即当key不存在时,我们进行set操作;若key已经存在,则不做任何操作;</li><li>第四个为expx,这个参数我们传的是PX,意思是我们要给这个key加一个过期的设置,具体时间由第五个参数决定。</li><li>第五个为time,与第四个参数相呼应,代表key的过期时间。</li></ul><p>总的来说,执行上面的set()方法就只会导致两种结果:1. 当前没有锁(key不存在),那么就进行加锁操作,并对锁设置个有效期,同时value表示加锁的客户端。2. 已有锁存在,不做任何操作。</p><p>心细的童鞋就会发现了,我们的加锁代码满足我们可靠性里描述的三个条件。首先,set()加入了NX参数,可以保证如果已有key存在,则函数不会调用成功,也就是只有一个客户端能持有锁,满足互斥性。其次,由于我们对锁设置了过期时间,即使锁的持有者后续发生崩溃而没有解锁,锁也会因为到了过期时间而自动解锁(即key被删除),不会发生死锁。最后,因为我们将value赋值为requestId,代表加锁的客户端请求标识,那么在客户端在解锁的时候就可以进行校验是否是同一个客户端。由于我们只考虑Redis单机部署的场景,所以容错性我们暂不考虑。</p><h3 id="错误示例1"><a class="markdownIt-Anchor" href="#错误示例1"></a> 错误示例1</h3><p>比较常见的错误示例就是使用<code>jedis.setnx()</code>和<code>jedis.expire()</code>组合实现加锁,代码如下:</p><figure class="highlight java"><table><tbody><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">static</span> <span class="keyword">void</span> <span class="title function_">wrongGetLock1</span><span class="params">(Jedis jedis, String lockKey, String requestId, <span class="type">int</span> expireTime)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="type">Long</span> <span class="variable">result</span> <span class="operator">=</span> jedis.setnx(lockKey, requestId);</span><br><span class="line"> <span class="keyword">if</span> (result == <span class="number">1</span>) {</span><br><span class="line"> <span class="comment">// 若在这里程序突然崩溃,则无法设置过期时间,将发生死锁</span></span><br><span class="line"> jedis.expire(lockKey, expireTime);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>setnx()方法作用就是SET IF NOT EXIST,expire()方法就是给锁加一个过期时间。乍一看好像和前面的set()方法结果一样,然而由于这是两条Redis命令,不具有原子性,如果程序在执行完setnx()之后突然崩溃,导致锁没有设置过期时间。那么将会发生死锁。网上之所以有人这样实现,是因为低版本的jedis并不支持多参数的set()方法。</p><h3 id="错误示例2"><a class="markdownIt-Anchor" href="#错误示例2"></a> 错误示例2</h3><figure class="highlight java"><table><tbody><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">static</span> <span class="type">boolean</span> <span class="title function_">wrongGetLock2</span><span class="params">(Jedis jedis, String lockKey, <span class="type">int</span> expireTime)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="type">long</span> <span class="variable">expires</span> <span class="operator">=</span> System.currentTimeMillis() + expireTime;</span><br><span class="line"> <span class="type">String</span> <span class="variable">expiresStr</span> <span class="operator">=</span> String.valueOf(expires);</span><br><span class="line"></span><br><span class="line"> <span class="comment">// 如果当前锁不存在,返回加锁成功</span></span><br><span class="line"> <span class="keyword">if</span> (jedis.setnx(lockKey, expiresStr) == <span class="number">1</span>) {</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="comment">// 如果锁存在,获取锁的过期时间</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">currentValueStr</span> <span class="operator">=</span> jedis.get(lockKey);</span><br><span class="line"> <span class="keyword">if</span> (currentValueStr != <span class="literal">null</span> && Long.parseLong(currentValueStr) < System.currentTimeMillis()) {</span><br><span class="line"> <span class="comment">// 锁已过期,获取上一个锁的过期时间,并设置现在锁的过期时间</span></span><br><span class="line"> <span class="type">String</span> <span class="variable">oldValueStr</span> <span class="operator">=</span> jedis.getSet(lockKey, expiresStr);</span><br><span class="line"> <span class="keyword">if</span> (oldValueStr != <span class="literal">null</span> && oldValueStr.equals(currentValueStr)) {</span><br><span class="line"> <span class="comment">// 考虑多线程并发的情况,只有一个线程的设置值和当前值相同,它才有权利加锁</span></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><br><span class="line"> <span class="comment">// 其他情况,一律返回加锁失败</span></span><br><span class="line"> <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>这一种错误示例就比较难以发现问题,而且实现也比较复杂。实现思路:使用<code>jedis.setnx()</code>命令实现加锁,其中key是锁,value是锁的过期时间。执行过程:1. 通过setnx()方法尝试加锁,如果当前锁不存在,返回加锁成功。2. 如果锁已经存在则获取锁的过期时间,和当前时间比较,如果锁已经过期,则设置新的过期时间,返回加锁成功。代码如下:</p><p>那么这段代码问题在哪里?1. 由于是客户端自己生成过期时间,所以需要强制要求分布式下每个客户端的时间必须同步。 2. 当锁过期的时候,如果多个客户端同时执行<code>jedis.getSet()</code>方法,那么虽然最终只有一个客户端可以加锁,但是这个客户端的锁的过期时间可能被其他客户端覆盖。3. 锁不具备拥有者标识,即任何客户端都可以解锁。</p><h2 id="解锁代码"><a class="markdownIt-Anchor" href="#解锁代码"></a> 解锁代码</h2><h3 id="正确姿势-2"><a class="markdownIt-Anchor" href="#正确姿势-2"></a> 正确姿势</h3><p>还是先展示代码,再带大家慢慢解释为什么这样实现:</p><figure class="highlight java"><table><tbody><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_">RedisTool</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">Long</span> <span class="variable">RELEASE_SUCCESS</span> <span class="operator">=</span> <span class="number">1L</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 class="doctag">@param</span> jedis Redis客户端</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> lockKey 锁</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> requestId 请求标识</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@return</span> 是否释放成功</span></span><br><span class="line"><span class="comment"> */</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_">releaseDistributedLock</span><span class="params">(Jedis jedis, String lockKey, String requestId)</span> {</span><br><span class="line"></span><br><span class="line"> <span class="type">String</span> <span class="variable">script</span> <span class="operator">=</span> <span class="string">"if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end"</span>;</span><br><span class="line"> <span class="type">Object</span> <span class="variable">result</span> <span class="operator">=</span> jedis.eval(script, Collections.singletonList(lockKey), Collections.singletonList(requestId));</span><br><span class="line"></span><br><span class="line"> <span class="keyword">if</span> (RELEASE_SUCCESS.equals(result)) {</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 class="keyword">return</span> <span class="literal">false</span>;</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></tbody></table></figure><p>可以看到,我们解锁只需要两行代码就搞定了!第一行代码,我们写了一个简单的Lua脚本代码,上一次见到这个编程语言还是在《黑客与画家》里,没想到这次居然用上了。第二行代码,我们将Lua代码传到<code>jedis.eval()</code>方法里,并使参数KEYS[1]赋值为lockKey,ARGV[1]赋值为requestId。eval()方法是将Lua代码交给Redis服务端执行。</p><p>那么这段Lua代码的功能是什么呢?其实很简单,首先获取锁对应的value值,检查是否与requestId相等,如果相等则删除锁(解锁)。那么为什么要使用Lua语言来实现呢?因为要确保上述操作是原子性的。关于非原子性会带来什么问题,可以阅读<a href="http://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/#releaseLock-wrongDemo2">【解锁代码-错误示例2】</a> 。那么为什么执行eval()方法可以确保原子性,源于Redis的特性,下面是官网对eval命令的部分解释:</p><p><img src="http://o7x0ygc3f.bkt.clouddn.com/Redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F/Redis%E5%88%86%E5%B8%83%E5%BC%8F%E9%94%81%E7%9A%84%E6%AD%A3%E7%A1%AE%E5%AE%9E%E7%8E%B0%E6%96%B9%E5%BC%8F_01.png" alt="img"></p><p>简单来说,就是在eval命令执行Lua代码的时候,Lua代码将被当成一个命令去执行,并且直到eval命令执行完成,Redis才会执行其他命令。</p><h3 id="错误示例1-2"><a class="markdownIt-Anchor" href="#错误示例1-2"></a> 错误示例1</h3><p>最常见的解锁代码就是直接使用<code>jedis.del()</code>方法删除锁,这种不先判断锁的拥有者而直接解锁的方式,会导致任何客户端都可以随时进行解锁,即使这把锁不是它的。</p><figure class="highlight plaintext"><table><tbody><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">public static void wrongReleaseLock1(Jedis jedis, String lockKey) {</span><br><span class="line"> jedis.del(lockKey);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><h3 id="错误示例2-2"><a class="markdownIt-Anchor" href="#错误示例2-2"></a> 错误示例2</h3><p>这种解锁代码乍一看也是没问题,甚至我之前也差点这样实现,与正确姿势差不多,唯一区别的是分成两条命令去执行,代码如下:</p><figure class="highlight java"><table><tbody><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">static</span> <span class="keyword">void</span> <span class="title function_">wrongReleaseLock2</span><span class="params">(Jedis jedis, String lockKey, String requestId)</span> {</span><br><span class="line"> </span><br><span class="line"> <span class="comment">// 判断加锁与解锁是不是同一个客户端</span></span><br><span class="line"> <span class="keyword">if</span> (requestId.equals(jedis.get(lockKey))) {</span><br><span class="line"> <span class="comment">// 若在此时,这把锁突然不是这个客户端的,则会误解锁</span></span><br><span class="line"> jedis.del(lockKey);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>如代码注释,问题在于如果调用<code>jedis.del()</code>方法的时候,这把锁已经不属于当前客户端的时候会解除他人加的锁。那么是否真的有这种场景?答案是肯定的,比如客户端A加锁,一段时间之后客户端A解锁,在执行<code>jedis.del()</code>之前,锁突然过期了,此时客户端B尝试加锁成功,然后客户端A再执行del()方法,则将客户端B的锁给解除了。</p><hr><h2 id="总结"><a class="markdownIt-Anchor" href="#总结"></a> 总结</h2><p>本文主要介绍了如何使用Java代码正确实现Redis分布式锁,对于加锁和解锁也分别给出了两个比较经典的错误示例。其实想要通过Redis实现分布式锁并不难,只要保证能满足可靠性里的四个条件。互联网虽然给我们带来了方便,只要有问题就可以google,然而网上的答案一定是对的吗?其实不然,所以我们更应该时刻保持着质疑精神,多想多验证。</p><p>如果你的项目中Redis是多机部署的,那么可以尝试使用<code>Redisson</code>实现分布式锁,这是Redis官方提供的Java组件,链接在<a href="http://wudashan.cn/2017/10/23/Redis-Distributed-Lock-Implement/#%E5%8F%82%E8%80%83%E9%98%85%E8%AF%BB">参考阅读</a>章节已经给出。</p><p>其它博文参考:<a href="https://zhuanlan.zhihu.com/p/165118929">深入解析 Redis 分布式锁原理 - 知乎 (zhihu.com)</a></p><h1 id="看门狗给锁续时"><a class="markdownIt-Anchor" href="#看门狗给锁续时"></a> 看门狗,给锁续时</h1><p><a href="https://www.bilibili.com/video/BV1gD4y1u7eC/?spm_id_from=333.788.recommend_more_video.11&vd_source=746b9336d739b80b11820809545b6604">【Java进阶】五分钟梳理看门狗的实现原理,手写Redis锁续期功能,打造核心竞争力_哔哩哔哩_bilibili</a></p><ul><li>方案:可基于<strong>HashedWheelTimer</strong>,加上自旋的方式来实现</li><li><strong>HashedWheelTimer</strong>:时间轮,异步的延时执行任务的工具类</li></ul><p><a href="https://zhuanlan.zhihu.com/p/268290754">Redis分布式锁解决高并发场景 - 知乎 (zhihu.com)</a></p>]]></content>
<summary type="html"><h1 id="为何要用分布式锁"><a class="markdownIt-Anchor" href="#为何要用分布式锁"></a> 为何要用分布式锁</h1>
<h2 id="一-为什么要使用分布式锁"><a class="markdownIt-Anchor" href="</summary>
<category term="redis" scheme="http://example.com/categories/redis/"/>
<category term="redis" scheme="http://example.com/tags/redis/"/>
<category term="springboot" scheme="http://example.com/tags/springboot/"/>
</entry>
<entry>
<title></title>
<link href="http://example.com/2023/05/20/Netty/IM%E4%BB%BF%E5%BE%AE%E4%BF%A1%E8%81%8A%E5%A4%A9/IM/"/>
<id>http://example.com/2023/05/20/Netty/IM%E4%BB%BF%E5%BE%AE%E4%BF%A1%E8%81%8A%E5%A4%A9/IM/</id>
<published>2023-05-20T08:12:31.462Z</published>
<updated>2023-05-20T08:42:13.662Z</updated>
<content type="html"><![CDATA[<p><img src="../../../images/image-20230520161301132.png" alt="image-20230520161301132"></p><h1 id="网络通信层"><a class="markdownIt-Anchor" href="#网络通信层"></a> 网络通信层</h1><p><strong>Bootstrap</strong></p><p>负责客户端启动并用来连接远程Netty Server</p><p>但这里不用Bootstrap,因为 IM实战的通讯方式是通过WebSocket前端网页来链接连接我们的netty的server端的</p><p><img src="../../../images/image-20230520162251684.png" alt="image-20230520162251684"></p><p><img src="../../../images/image-20230520162457445.png" alt="image-20230520162457445"></p><p><img src="../../../images/image-20230520162426147.png" alt="image-20230520162426147"></p><p>上图的远离机制:</p><p>Client这三个客户端,会发送消息到BossGroup;BossGroup是一个线程池,中有一个Selector主要作用是会生成 SocketChannel;SocketChannel会封装成NIOSocketChannel;NIOSocketChannel会注册到工作线程中的Selector;若要读数据或写数据,那么工作线程中selector就会分发到不同对应的Handler中进行处理</p>]]></content>
<summary type="html"><p><img src="../../../images/image-20230520161301132.png" alt="image-20230520161301132"></p>
<h1 id="网络通信层"><a class="markdownIt-Anchor" hre</summary>
</entry>
<entry>
<title>浏览器从输入网址到页面展示的过程</title>
<link href="http://example.com/2023/04/29/interview/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%A2%98/%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BB%8E%E8%BE%93%E5%85%A5URL%E5%88%B0%E9%A1%B5%E9%9D%A2%E5%B1%95%E7%A4%BA%E7%9A%84%E8%BF%87%E7%A8%8B/"/>
<id>http://example.com/2023/04/29/interview/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%A2%98/%E6%B5%8F%E8%A7%88%E5%99%A8%E4%BB%8E%E8%BE%93%E5%85%A5URL%E5%88%B0%E9%A1%B5%E9%9D%A2%E5%B1%95%E7%A4%BA%E7%9A%84%E8%BF%87%E7%A8%8B/</id>
<published>2023-04-29T02:18:01.535Z</published>
<updated>2023-06-04T15:45:55.440Z</updated>
<content type="html"><![CDATA[<h1 id="浏览器从输入网址到页面展示的过程"><a class="markdownIt-Anchor" href="#浏览器从输入网址到页面展示的过程"></a> 浏览器从输入网址到页面展示的过程</h1><p>完整高频题库仓库地址:<a href="https://github.com/hzfe/awesome-interview"><strong>https://github.com/hzfe/awesome-interview</strong></a></p><p>完整高频题库阅读地址:<a href="https://febook.hzfe.org/"><strong>https://febook.hzfe.org/</strong></a></p><h2 id="回答关键点"><a class="markdownIt-Anchor" href="#回答关键点"></a> <strong>回答关键点</strong></h2><figure class="highlight plaintext"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">URL` `DNS` `TCP` `渲染</span><br></pre></td></tr></tbody></table></figure><p>浏览器从输入网址到渲染页面主要分为以下几个过程</p><ul><li>URL 输入</li><li>DNS 解析</li><li>建立 TCP 连接</li><li>发送 HTTP / HTTPS 请求(建立 TLS 连接)</li><li>服务器响应请求</li><li>浏览器解析渲染页面</li><li>HTTP 请求结束,断开 TCP 连接</li></ul><h2 id="知识点深入"><a class="markdownIt-Anchor" href="#知识点深入"></a> <strong>知识点深入</strong></h2><h3 id="1-url-输入"><a class="markdownIt-Anchor" href="#1-url-输入"></a> <strong>1. URL 输入</strong></h3><p><img src="https://ask.qcloudimg.com/http-save/4474523/2911d132881a03e20edc7c8942e65fed.png?" alt="img"></p><p>URL地址</p><p>URL(统一资源定位符,Uniform Resource Locator)用于定位互联网上资源,俗称网址。</p><p>我们在地址栏输入 HZFE 官方网址 <a href="http://hzfe.org">hzfe.org</a> 后敲下回车,浏览器会对输入的信息进行以下判断:</p><ol><li>检查输入的内容是否是一个合法的 URL 链接。</li><li>是,则判断输入的 URL 是否完整。如果不完整,浏览器可能会对域进行猜测,补全前缀或者后缀。</li><li>否,将输入内容作为搜索条件,使用用户设置的默认搜索引擎来进行搜索。</li></ol><p>大部分浏览器会从历史记录、书签等地方开始查找我们输入的网址,并给出智能提示。</p><h3 id="2-dnsdomain-name-system解析"><a class="markdownIt-Anchor" href="#2-dnsdomain-name-system解析"></a> <strong>2. DNS(Domain Name System)解析</strong></h3><p>因为浏览器不能直接通过域名找到对应的服务器 IP 地址,所以需要进行 DNS 解析,查找到对应的 IP 地址进行访问。</p><p>DNS 解析流程如下:</p><p><img src="https://ask.qcloudimg.com/http-save/yehe-4474523/eb1b6b726e6cbe7c04beb6b7885202e4.png?" alt="img"></p><p>DNS 解析</p><ol><li>在浏览器中输入 <a href="http://hzfe.org">hzfe.org</a> 域名,操作系统检查浏览器缓存和本地的 hosts 文件中,是否有这个网址记录,有则从记录里面找到对应的 IP 地址,完成域名解析。</li><li>查找本地 DNS 解析器缓存中,是否有这个网址记录,有则从记录里面找到对应的 IP 地址,完成域名解析。</li><li>使用 TCP/IP 参数中设置的 DNS 服务器进行查询。如果要查询的域名包含在本地配置区域资源中,则返回解析结果,完成域名解析。</li><li>检查本地 DNS 服务器是否缓存该网址记录,有则返回解析结果,完成域名解析。</li><li>本地 DNS 服务器发送查询报文至根 DNS 服务器,根 DNS 服务器收到请求后,用顶级域 DNS 服务器地址进行响应。</li><li>本地 DNS 服务器发送查询报文至顶级域 DNS 服务器。顶级域 DNS 服务器收到请求后,用权威 DNS 服务器地址进行响应。</li><li>本地 DNS 服务器发送查询报文至权威 DNS 服务器,权威 DNS 服务器收到请求后,用 <a href="http://hzfe.org">hzfe.org</a> 的 IP 地址进行响应,完成域名解析。</li></ol><p>查询通常遵循以上流程,从请求主机到本地 DNS 服务器的查询是递归查询,DNS 服务器获取到所需映射的查询过程是迭代查询。</p><h3 id="3-建立-tcp-连接"><a class="markdownIt-Anchor" href="#3-建立-tcp-连接"></a> <strong>3. 建立 TCP 连接</strong></h3><blockquote><p>世界上几乎所有的 HTTP 通信都是由 TCP/IP 承载的,TCP/IP 是全球计算机及网络设备都在使用的一种常用的分组交换网络分层。 HTTP 的连接实际上就是 TCP 连接以及其使用规则。 –《HTTP 权威指南》</p></blockquote><p>当浏览器获取到服务器的 IP 地址后,浏览器会用一个随机的端口(1024 < 端口 < 65535)向服务器 80 端口发起 TCP 连接请求(注:HTTP 默认约定 80 端口,HTTPS 为 443 端口)。这个连接请求到达服务端后,通过 TCP 三次握手,建立 TCP 的连接。</p><h4 id="31-分层模型"><a class="markdownIt-Anchor" href="#31-分层模型"></a> <strong>3.1 分层模型</strong></h4><figure class="highlight javascript"><table><tbody><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><br><span class="line"><span class="number">7</span>| 应用层 | | <span class="variable constant_">HTTP</span> |</span><br><span class="line"></span><br><span class="line"><span class="number">6</span>| 表示层 | 应用层 |</span><br><span class="line"></span><br><span class="line"><span class="number">5</span>| 会话层 | | |</span><br><span class="line"> ---------------------------------</span><br><span class="line"><span class="number">4</span>| 传输层 | 传输层 | <span class="variable constant_">TCP</span> <span class="variable constant_">TLS</span> |</span><br><span class="line"> ---------------------------------</span><br><span class="line"><span class="number">3</span>| 网络层 | 网络层 | <span class="variable constant_">IP</span> |</span><br><span class="line"> ---------------------------------</span><br><span class="line"><span class="number">2</span>| 数据链路层</span><br><span class="line"> | 链路层</span><br><span class="line"><span class="number">1</span>| 物理层</span><br><span class="line"> --------------------------------</span><br><span class="line"> [<span class="variable constant_">OSI</span>] | [<span class="variable constant_">TCP</span>/<span class="variable constant_">IP</span>]</span><br></pre></td></tr></tbody></table></figure><p>复制</p><h4 id="32-tcp-三次握手"><a class="markdownIt-Anchor" href="#32-tcp-三次握手"></a> <strong>3.2 TCP 三次握手</strong></h4><figure class="highlight javascript"><table><tbody><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="variable constant_">SYN</span> 是建立连接时的握手信号,<span class="variable constant_">TCP</span> 中发送第一个 <span class="variable constant_">SYN</span> 包的为客户端,接收的为服务端</span><br><span class="line"># <span class="variable constant_">TCP</span> 中,当发送端数据到达接收端时,接收端返回一个已收到消息的通知。这个消息叫做确认应答 <span class="variable constant_">ACK</span></span><br><span class="line"></span><br><span class="line"> 假设有客户端A,服务端B。我们要建立可靠的数据传输。</span><br><span class="line"> <span class="title function_">SYN</span>(=j) <span class="comment">// SYN: A 请求建立连接</span></span><br><span class="line"> A ----------> B</span><br><span class="line"> |</span><br><span class="line"> <span class="title function_">ACK</span>(=j+<span class="number">1</span>) | <span class="comment">// ACK: B 确认应答 A 的 SYN</span></span><br><span class="line"> <span class="title function_">SYN</span>(=k) | <span class="comment">// SYN: B 发送一个 SYN</span></span><br><span class="line"> A <-----------</span><br><span class="line"> |</span><br><span class="line"> | <span class="title function_">ACK</span>(=k+<span class="number">1</span>)</span><br><span class="line"> -----------> B <span class="comment">// ACK: A 确认应答 B 的包</span></span><br></pre></td></tr></tbody></table></figure><p>复制</p><ol><li>客户端发送 SYN 包(seq = j)到服务器,并进入 SYN_SEND 状态,等待服务器确认。</li><li>服务器收到 SYN 包,必须确认客户的 SYN(ACK = k + 1),同时自己也发送一个 SYN 包(seq = k),即 SYN+ACK 包,此时服务器进入 SYN_RECV 状态。</li><li>客户端收到服务器的 SYN+ACK 包,向服务器发送确认包 ACK(ACK = k + 1),此包发送完毕,客户端和服务器进入 ESTABLISHED 状态,完成三次握手。</li></ol><h3 id="4-tls-协商"><a class="markdownIt-Anchor" href="#4-tls-协商"></a> <strong>4. TLS 协商</strong></h3><p><img src="https://ask.qcloudimg.com/http-save/4474523/cdb05918db5147cc4f92f12cad6f5d15.png?imageView2/2/w/2560/h/7000" alt="img"></p><p>TLS协商</p><p>建立连接后就可以通过 HTTP 进行数据传输。如果使用 HTTPS,会在 TCP 与 HTTP 之间多添加一层协议做加密及认证的服务。HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport Layer Security) 协议,保障了信息的安全。</p><ul><li>SSL<ul><li>认证用户和服务器,确保数据发送到正确的客户端和服务器。</li><li>加密数据防止数据中途被窃取。</li><li>维护数据的完整性,确保数据在传输过程中不被改变。</li></ul></li><li>TLS<ul><li>用于在两个通信应用程序之间提供保密性和数据完整性。该协议由两层组成:TLS 记录协议(TLS Record)和 TLS 握手协议(TLS Handshake)。较低的层为 TLS 记录协议,位于某个可靠的传输协议(例如 TCP)上面。</li></ul></li></ul><h4 id="41-tls-握手协议"><a class="markdownIt-Anchor" href="#41-tls-握手协议"></a> <strong>4.1 TLS 握手协议</strong></h4><p><img src="https://ask.qcloudimg.com/http-save/yehe-4474523/3ac0cf1dcd3100b2b96c55bf4e827adf.png?imageView2/2/w/2560/h/7000" alt="img"></p><p>TLS握手协议</p><ol><li>客户端发出一个 client hello 消息,携带的信息包括:所支持的 SSL/TLS 版本列表;支持的与加密算法;所支持的数据压缩方法;随机数 A。</li><li>服务端响应一个 server hello 消息,携带的信息包括:协商采用的 SSL/TLS 版本号;会话 ID;随机数 B;服务端数字证书 serverCA;由于双向认证需求,服务端需要对客户端进行认证,会同时发送一个 client certificate request,表示请求客户端的证书。</li><li>客户端校验服务端的数字证书;校验通过之后发送随机数 C,该随机数称为 pre-master-key,使用数字证书中的公钥加密后发出;由于服务端发起了 client certificate request,客户端使用私钥加密一个随机数 clientRandom 随客户端的证书 clientCA 一并发出。</li><li>服务端校验客户端的证书,并成功将客户端加密的随机数 clientRandom 解密;根据随机数 A/随机数 B/随机数 C(pre-master-key) 产生动态密钥 master-key,加密一个 finish 消息发至客户端。</li><li>客户端根据同样的随机数和算法生成 master-key,加密一个 finish 消息发送至服务端。</li><li>服务端和客户端分别解密成功,至此握手完成,之后的数据包均采用 master-key 进行加密传输。</li></ol><h3 id="5-服务器响应"><a class="markdownIt-Anchor" href="#5-服务器响应"></a> <strong>5. 服务器响应</strong></h3><p>当浏览器到 web 服务器的连接建立后,浏览器会发送一个初始的 HTTP GET 请求,请求目标通常是一个 HTML 文件。服务器收到请求后,将<strong>发回一个 HTTP 响应报文</strong>,内容包括相关响应头和 HTML 正文。</p><figure class="highlight javascript"><table><tbody><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"><html></span><br><span class="line"> <span class="language-xml"><span class="tag"><<span class="name">head</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">meta</span> <span class="attr">charset</span>=<span class="string">"UTF-8"</span>/></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">title</span>></span>我的博客<span class="tag"></<span class="name">title</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">link</span> <span class="attr">rel</span>=<span class="string">"stylesheet"</span> <span class="attr">src</span>=<span class="string">"styles.css"</span>/></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">scrIPt</span> <span class="attr">src</span>=<span class="string">"index.js"</span>></span><span class="tag"></<span class="name">scrIPt</span>></span></span></span><br><span class="line"><span class="language-xml"><span class="tag"></<span class="name">head</span>></span></span></span><br><span class="line"><span class="language-xml"><span class="tag"><<span class="name">body</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">h1</span> <span class="attr">class</span>=<span class="string">"heading"</span>></span>首页<span class="tag"></<span class="name">h1</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">p</span>></span>A paragraph with a <span class="tag"><<span class="name">a</span> <span class="attr">href</span>=<span class="string">"https://hzfe.org/"</span>></span>link<span class="tag"></<span class="name">a</span>></span><span class="tag"></<span class="name">p</span>></span></span></span><br><span class="line"><span class="language-xml"> <span class="tag"><<span class="name">scrIPt</span> <span class="attr">src</span>=<span class="string">"index.js"</span>></span><span class="tag"></<span class="name">scrIPt</span>></span></span></span><br><span class="line"><span class="language-xml"><span class="tag"></<span class="name">body</span>></span></span></span><br><span class="line"></html></span><br></pre></td></tr></tbody></table></figure><p>复制</p><h4 id="51-状态码"><a class="markdownIt-Anchor" href="#51-状态码"></a> <strong>5.1 状态码</strong></h4><p>状态码是由 3 位数组成,第一个数字定义了响应的类别,且有五类可能取值</p><ul><li>1xx:指示信息——表示请求已接收,继续处理</li><li>2xx:成功——表示请求已被成功接收、理解、接受</li><li>3xx:重定向——要完成请求必须进行更进一步的操作</li><li>4xx:客户端错误——请求有语法错误或请求无法实现</li><li>5xx:服务器端错误——服务器未能实现合法的请求</li></ul><p><a href="https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Status">HTTP 响应状态码 - HTTP | MDN (mozilla.org)</a></p><h4 id="52-常见的请求头和字段"><a class="markdownIt-Anchor" href="#52-常见的请求头和字段"></a> <strong>5.2 常见的请求头和字段</strong></h4><ul><li>Cache-Control:must-revalidate、no-cache、private(是否需要缓存资源)</li><li>Connection:keep-alive(保持连接)</li><li>Content-Encoding:gzip(web 服务器支持的返回内容压缩编码类型)</li><li>Content-Type:text/html;charset=UTF-8(文件类型和字符编码格式)</li><li>Date:Sun, 21 Sep 2021 06:18:21 GMT(服务器消息发出的时间)</li><li>Transfer-Encoding:chunked(服务器发送的资源的方式是分块发送)</li></ul><h4 id="53-http-响应报文"><a class="markdownIt-Anchor" href="#53-http-响应报文"></a> <strong>5.3 HTTP 响应报文</strong></h4><p>响应报文由四部分组成(响应行 + 响应头 + 空行 + 响应体)</p><ul><li>状态行:HTTP 版本 + 空格 + 状态码 + 空格 + 状态码描述 + 回车符(CR) + 换行符(LF)</li><li>响应头:字段名 + 冒号 + 值 + 回车符 + 换行符</li><li>空行:回车符 + 换行符</li><li>响应体:由用户自定义添加,如 post 的 body 等</li></ul><h3 id="6-浏览器解析并绘制"><a class="markdownIt-Anchor" href="#6-浏览器解析并绘制"></a> <strong>6. 浏览器解析并绘制</strong></h3><p>不同的浏览器引擎渲染过程都不太一样,这里以 Chrome 浏览器渲染方式为例。</p><p><img src="https://ask.qcloudimg.com/http-save/yehe-4474523/601ac9a205ada2bd56a06ed97996b301.png?imageView2/2/w/2560/h/7000" alt="img"></p><ol><li>处理 HTML 标记并构建 DOM 树。</li><li>处理 CSS 标记并构建 CSSOM 树。</li><li>将 DOM 与 CSSOM 合并成一个渲染树。</li><li>根据渲染树来布局,以计算每个节点的几何信息。</li><li>将各个节点绘制到屏幕上。</li></ol><h3 id="7-tcp-断开连接"><a class="markdownIt-Anchor" href="#7-tcp-断开连接"></a> <strong>7. TCP 断开连接</strong></h3><p>现在的页面为了优化请求的耗时,默认都会开启持久连接(keep-alive),那么一个 TCP 连接确切关闭的时机,是这个 tab 标签页关闭的时候。这个关闭的过程就是<strong>四次挥手</strong>。关闭是一个全双工的过程,发包的顺序是不一定的。一般来说是客户端主动发起的关闭,过程如下图所示:</p><p><img src="https://ask.qcloudimg.com/http-save/yehe-4474523/cd2cb58d98ca10faed28f809eab5b461.png?imageView2/2/w/2560/h/7000" alt="img"></p><ol><li>主动关闭方发送一个 FIN,用来关闭主动方到被动关闭方的数据传送,也就是主动关闭方告诉被动关闭方:我已经不会再给你发数据了(在 FIN 包之前发送出去的数据,如果没有收到对应的 ACK 确认报文,主动关闭方依然会重发这些数据),但此时主动关闭方还可以接受数据。</li><li>被动关闭方收到 FIN 包后,发送一个 ACK 给对方,确认序号为收到序号+1(与 SYN 相同,一个 FIN 占用一个序号)。</li><li>被动关闭方发送一个 FIN,用来关闭被动关闭方到主动关闭方的数据传送,也就是告诉主动关闭方,我的数据也发送完了,不会再给你发数据了。</li><li>主动关闭方收到 FIN 后,发送一个 ACK 给被动关闭方,确认序号为收到序号+1,至此,完成四次挥手</li></ol>]]></content>
<summary type="html"><h1 id="浏览器从输入网址到页面展示的过程"><a class="markdownIt-Anchor" href="#浏览器从输入网址到页面展示的过程"></a> 浏览器从输入网址到页面展示的过程</h1>
<p>完整高频题库仓库地址:<a href="https://gi</summary>
<category term="面试题" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="计算机网络" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
<category term="面试题" scheme="http://example.com/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="计算机网络" scheme="http://example.com/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
</entry>
<entry>
<title>计算机网络基础(上)</title>
<link href="http://example.com/2023/04/27/interview/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%A2%98/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98%E4%B8%8A/"/>
<id>http://example.com/2023/04/27/interview/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E9%9D%A2%E8%AF%95%E9%A2%98/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%B8%B8%E8%A7%81%E9%9D%A2%E8%AF%95%E9%A2%98%E4%B8%8A/</id>
<published>2023-04-26T16:40:12.263Z</published>
<updated>2023-06-08T15:13:52.978Z</updated>
<content type="html"><![CDATA[<h2 id="计算机网络基础"><a class="markdownIt-Anchor" href="#计算机网络基础"></a> 计算机网络基础</h2><h3 id=""><a class="markdownIt-Anchor" href="#"></a> <a href="#%E7%BD%91%E7%BB%9C%E5%88%86%E5%B1%82%E6%A8%A1%E5%9E%8B">#</a> 网络分层模型</h3><h4 id="-2"><a class="markdownIt-Anchor" href="#-2"></a> <a href="#osi-%E4%B8%83%E5%B1%82%E6%A8%A1%E5%9E%8B%E6%98%AF%E4%BB%80%E4%B9%88-%E6%AF%8F%E4%B8%80%E5%B1%82%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88">#</a> OSI 七层模型是什么?每一层的作用是什么?</h4><p><strong>OSI 七层模型</strong> 是国际标准化组织提出一个网络分层模型,其大体结构以及每一层提供的功能如下图所示:</p><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/osi-7-model.png" alt="OSI 七层模型"></p><p>每一层都专注做一件事情,并且每一层都需要使用下一层提供的功能比如传输层需要使用网络层提供的路由和寻址功能,这样传输层才知道把数据传输到哪里去。</p><p><strong>OSI 的七层体系结构概念清楚,理论也很完整,但是它比较复杂而且不实用,而且有些功能在多个层中重复出现。</strong></p><p>上面这种图可能比较抽象,再来一个比较生动的图片。下面这个图片是我在国外的一个网站上看到的,非常赞!</p><p><img src="../../../images/osi%E4%B8%83%E5%B1%82%E6%A8%A1%E5%9E%8B2.png" alt="osi七层模型2"></p><h4 id="-3"><a class="markdownIt-Anchor" href="#-3"></a> <a href="#tcp-ip-%E5%9B%9B%E5%B1%82%E6%A8%A1%E5%9E%8B%E6%98%AF%E4%BB%80%E4%B9%88-%E6%AF%8F%E4%B8%80%E5%B1%82%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88">#</a> TCP/IP 四层模型是什么?每一层的作用是什么?</h4><p><strong>TCP/IP 四层模型</strong> 是目前被广泛采用的一种模型,我们可以将 TCP / IP 模型看作是 OSI 七层模型的精简版本,由以下 4 层组成:</p><ol><li>应用层</li><li>传输层</li><li>网络层</li><li>网络接口层</li></ol><p>需要注意的是,我们并不能将 TCP/IP 四层模型 和 OSI 七层模型完全精确地匹配起来,不过可以简单将两者对应起来,如下图所示:</p><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/tcp-ip-4-model.png" alt="TCP/IP 四层模型"></p><p>关于每一层作用的详细介绍,请看 <a href="https://javaguide.cn/cs-basics/network/osi&tcp-ip-model.html">OSI 和 TCP/IP 网络分层模型详解(基础)open in new window</a> 这篇文章。</p><h4 id="-4"><a class="markdownIt-Anchor" href="#-4"></a> <a href="#%E4%B8%BA%E4%BB%80%E4%B9%88%E7%BD%91%E7%BB%9C%E8%A6%81%E5%88%86%E5%B1%82">#</a> 为什么网络要分层?</h4><p>说到分层,我们先从我们平时使用框架开发一个后台程序来说,我们往往会按照每一层做不同的事情的原则将系统分为三层(复杂的系统分层会更多):</p><ol><li>Repository(数据库操作)</li><li>Service(业务操作)</li><li>Controller(前后端数据交互)</li></ol><p><strong>复杂的系统需要分层,因为每一层都需要专注于一类事情。网络分层的原因也是一样,每一层只专注于做一类事情。</strong></p><p>好了,再来说回:“为什么网络要分层?”。我觉得主要有 3 方面的原因:</p><ol><li><strong>各层之间相互独立</strong>:各层之间相互独立,各层之间不需要关心其他层是如何实现的,只需要知道自己如何调用下层提供好的功能就可以了(可以简单理解为接口调用)<strong>。这个和我们对开发时系统进行分层是一个道理。</strong></li><li><strong>提高了整体灵活性</strong> :每一层都可以使用最适合的技术来实现,你只需要保证你提供的功能以及暴露的接口的规则没有改变就行了。<strong>这个和我们平时开发系统的时候要求的高内聚、低耦合的原则也是可以对应上的。</strong></li><li><strong>大问题化小</strong> : 分层可以将复杂的网络问题分解为许多比较小的、界线比较清晰简单的小问题来处理和解决。这样使得复杂的计算机网络系统变得易于设计,实现和标准化。 <strong>这个和我们平时开发的时候,一般会将系统功能分解,然后将复杂的问题分解为容易理解的更小的问题是相对应的,这些较小的问题具有更好的边界(目标和接口)定义。</strong></li></ol><p>我想到了计算机世界非常非常有名的一句话,这里分享一下:</p><blockquote><p>计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决,计算机整个体系从上到下都是按照严格的层次结构设计的。</p></blockquote><h3 id="-5"><a class="markdownIt-Anchor" href="#-5"></a> <a href="#%E5%B8%B8%E8%A7%81%E7%BD%91%E7%BB%9C%E5%8D%8F%E8%AE%AE">#</a> 常见网络协议</h3><h4 id="-6"><a class="markdownIt-Anchor" href="#-6"></a> <a href="#%E5%BA%94%E7%94%A8%E5%B1%82%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E8%A7%81%E7%9A%84%E5%8D%8F%E8%AE%AE">#</a> 应用层有哪些常见的协议?</h4><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/application-layer-protocol.png" alt="应用层常见协议"></p><ul><li><strong>HTTP(Hypertext Transfer Protocol,超文本传输协议)</strong> :基于 TCP 协议,是一种用于传输超文本和多媒体内容的协议,主要是为 Web 浏览器与 Web 服务器之间的通信而设计的。当我们使用浏览器浏览网页的时候,我们网页就是通过 HTTP 请求进行加载的。</li><li><strong>SMTP(Simple Mail Transfer Protocol,简单邮件发送协议)</strong> :基于 TCP 协议,是一种用于发送电子邮件的协议。注意 ⚠️:SMTP 协议只负责邮件的发送,而不是接收。要从邮件服务器接收邮件,需要使用 POP3 或 IMAP 协议。</li><li><strong>POP3/IMAP(邮件接收协议)</strong> :基于 TCP 协议,两者都是负责邮件接收的协议。IMAP 协议是比 POP3 更新的协议,它在功能和性能上都更加强大。IMAP 支持邮件搜索、标记、分类、归档等高级功能,而且可以在多个设备之间同步邮件状态。几乎所有现代电子邮件客户端和服务器都支持 IMAP。</li><li><strong>FTP(File Transfer Protocol,文件传输协议)</strong> : 基于 TCP 协议,是一种用于在计算机之间传输文件的协议,可以屏蔽操作系统和文件存储方式。注意 ⚠️:FTP 是一种不安全的协议,因为它在传输过程中不会对数据进行加密。建议在传输敏感数据时使用更安全的协议,如 SFTP。</li><li><strong>Telnet(远程登陆协议)</strong> :基于 TCP 协议,用于通过一个终端登陆到其他服务器。Telnet 协议的最大缺点之一是所有数据(包括用户名和密码)均以明文形式发送,这有潜在的安全风险。这就是为什么如今很少使用 Telnet,而是使用一种称为 SSH 的非常安全的网络传输协议的主要原因。</li><li><strong>SSH(Secure Shell Protocol,安全的网络传输协议)</strong> :基于 TCP 协议,通过加密和认证机制实现安全的访问和文件传输等业务</li><li><strong>RTP(Real-time Transport Protocol,实时传输协议)</strong>:通常基于 UDP 协议,但也支持 TCP 协议。它提供了端到端的实时传输数据的功能,但不包含资源预留存、不保证实时传输质量,这些功能由 WebRTC 实现。</li><li><strong>DNS(Domain Name System,域名管理系统)</strong>: 基于 UDP 协议,用于解决域名和 IP 地址的映射问题。</li></ul><p>关于这些协议的详细介绍请看 <a href="">应用层常见协议总结(应用层)</a> 这篇文章。</p><h4 id="-7"><a class="markdownIt-Anchor" href="#-7"></a> <a href="#%E4%BC%A0%E8%BE%93%E5%B1%82%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E8%A7%81%E7%9A%84%E5%8D%8F%E8%AE%AE">#</a> 传输层有哪些常见的协议?</h4><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/transport-layer-protocol.png" alt="传输层常见协议"></p><ul><li><strong>TCP(Transmission Control Protocol,传输控制协议 )</strong>:提供 <strong>面向连接</strong> 的,<strong>可靠</strong> 的数据传输服务。</li><li><strong>UDP(User Datagram Protocol,用户数据协议)</strong>:提供 <strong>无连接</strong> 的,<strong>尽最大努力</strong> 的数据传输服务(不保证数据传输的可靠性),简单高效。</li></ul><h4 id="-8"><a class="markdownIt-Anchor" href="#-8"></a> <a href="#%E7%BD%91%E7%BB%9C%E5%B1%82%E6%9C%89%E5%93%AA%E4%BA%9B%E5%B8%B8%E8%A7%81%E7%9A%84%E5%8D%8F%E8%AE%AE">#</a> 网络层有哪些常见的协议?</h4><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/nerwork-layer-protocol.png" alt="网络层常见协议"></p><ul><li><strong>IP(Internet Protocol,网际协议)</strong> : TCP/IP 协议中最重要的协议之一,属于网络层的协议,主要作用是定义数据包的格式、对数据包进行路由和寻址,以便它们可以跨网络传播并到达正确的目的地。目前 IP 协议主要分为两种,一种是过去的 IPv4,另一种是较新的 IPv6,目前这两种协议都在使用,但后者已经被提议来取代前者。</li><li><strong>ARP(Address Resolution Protocol,地址解析协议)</strong> :ARP 协议解决的是网络层地址和链路层地址之间的转换问题。因为一个 IP 数据报在物理上传输的过程中,总是需要知道下一跳(物理上的下一个目的地)该去往何处,但 IP 地址属于逻辑地址,而 MAC 地址才是物理地址,ARP 协议解决了 IP 地址转 MAC 地址的一些问题。</li><li><strong>ICMP(Internet Control Message Protocol,互联网控制报文协议)</strong> :一种用于传输网络状态和错误消息的协议,常用于网络诊断和故障排除。例如,Ping 工具就使用了 ICMP 协议来测试网络连通性。</li><li><strong>NAT(Network Address Translation,网络地址转换协议)</strong> :NAT 协议的应用场景如同它的名称——网络地址转换,应用于内部网到外部网的地址转换过程中。具体地说,在一个小的子网(局域网,LAN)内,各主机使用的是同一个 LAN 下的 IP 地址,但在该 LAN 以外,在广域网(WAN)中,需要一个统一的 IP 地址来标识该 LAN 在整个 Internet 上的位置。</li><li><strong>OSPF(Open Shortest Path First,开放式最短路径优先)</strong> ):一种内部网关协议(Interior Gateway Protocol,IGP),也是广泛使用的一种动态路由协议,基于链路状态算法,考虑了链路的带宽、延迟等因素来选择最佳路径。</li><li><strong>RIP(Routing Information Protocol,路由信息协议)</strong> :一种内部网关协议(Interior Gateway Protocol,IGP),也是一种动态路由协议,基于距离向量算法,使用固定的跳数作为度量标准,选择跳数最少的路径作为最佳路径。</li><li><strong>BGP(Border Gateway Protocol,边界网关协议)</strong> :一种用来在路由选择域之间交换网络层可达性信息(Network Layer Reachability Information,NLRI)的路由选择协议,具有高度的灵活性和可扩展性。</li></ul><h2 id="-9"><a class="markdownIt-Anchor" href="#-9"></a> <a href="#http">#</a> HTTP</h2><h3 id="-10"><a class="markdownIt-Anchor" href="#-10"></a> <a href="#%E4%BB%8E%E8%BE%93%E5%85%A5-url-%E5%88%B0%E9%A1%B5%E9%9D%A2%E5%B1%95%E7%A4%BA%E5%88%B0%E5%BA%95%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88-%E9%9D%9E%E5%B8%B8%E9%87%8D%E8%A6%81">#</a> 从输入 URL 到页面展示到底发生了什么?(非常重要)</h3><blockquote><p>类似的问题:打开一个网页,整个过程会使用哪些协议?</p></blockquote><p>图解(图片来源:《图解 HTTP》):</p><p><img src="https://oss.javaguide.cn/github/javaguide/url%E8%BE%93%E5%85%A5%E5%88%B0%E5%B1%95%E7%A4%BA%E5%87%BA%E6%9D%A5%E7%9A%84%E8%BF%87%E7%A8%8B.jpg" alt="img"></p><blockquote><p>上图有一个错误,请注意,是 OSPF 不是 OPSF。 <strong>OSPF(Open Shortest Path First,ospf)开放最短路径优先协议</strong>, 是由 Internet 工程任务组开发的路由选择协议</p></blockquote><p>总体来说分为以下几个过程:</p><ol><li>DNS 解析</li><li>TCP 连接</li><li>发送 HTTP 请求</li><li>服务器处理请求并返回 HTTP 报文</li><li>浏览器解析渲染页面</li><li>连接结束</li></ol><p>具体可以参考下面这两篇文章:</p><ul><li><a href="https://segmentfault.com/a/1190000006879700">从输入 URL 到页面加载发生了什么?open in new window</a></li><li><a href="https://cloud.tencent.com/developer/article/1879758">浏览器从输入网址到页面展示的过程open in new window</a></li></ul><h3 id="-11"><a class="markdownIt-Anchor" href="#-11"></a> <a href="#http-%E7%8A%B6%E6%80%81%E7%A0%81%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> HTTP 状态码有哪些?</h3><p>HTTP 状态码用于描述 HTTP 请求的结果,比如 2xx 就代表请求被成功处理。</p><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-status-code.png" alt="常见 HTTP 状态码"></p><p>关于 HTTP 状态码更详细的总结,可以看我写的这篇文章:<a href="">HTTP 常见状态码总结(应用层)</a>。</p><h3 id="-12"><a class="markdownIt-Anchor" href="#-12"></a> <a href="#http-header-%E4%B8%AD%E5%B8%B8%E8%A7%81%E7%9A%84%E5%AD%97%E6%AE%B5%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> HTTP Header 中常见的字段有哪些?</h3><table><thead><tr><th style="text-align:left">请求头字段名</th><th style="text-align:left">说明</th><th style="text-align:left">示例</th></tr></thead><tbody><tr><td style="text-align:left">Accept</td><td style="text-align:left">能够接受的回应内容类型(Content-Types)。</td><td style="text-align:left">Accept: text/plain</td></tr><tr><td style="text-align:left">Accept-Charset</td><td style="text-align:left">能够接受的字符集</td><td style="text-align:left">Accept-Charset: utf-8</td></tr><tr><td style="text-align:left">Accept-Datetime</td><td style="text-align:left">能够接受的按照时间来表示的版本</td><td style="text-align:left">Accept-Datetime: Thu, 31 May 2007 20:35:00 GMT</td></tr><tr><td style="text-align:left">Accept-Encoding</td><td style="text-align:left">能够接受的编码方式列表。参考 HTTP 压缩。</td><td style="text-align:left">Accept-Encoding: gzip, deflate</td></tr><tr><td style="text-align:left">Accept-Language</td><td style="text-align:left">能够接受的回应内容的自然语言列表。</td><td style="text-align:left">Accept-Language: en-US</td></tr><tr><td style="text-align:left">Authorization</td><td style="text-align:left">用于超文本传输协议的认证的认证信息</td><td style="text-align:left">Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==</td></tr><tr><td style="text-align:left">Cache-Control</td><td style="text-align:left">用来指定在这次的请求/响应链中的所有缓存机制 都必须 遵守的指令</td><td style="text-align:left">Cache-Control: no-cache</td></tr><tr><td style="text-align:left">Connection</td><td style="text-align:left">该浏览器想要优先使用的连接类型</td><td style="text-align:left">Connection: keep-alive Connection: Upgrade</td></tr><tr><td style="text-align:left">Content-Length</td><td style="text-align:left">以 八位字节数组 (8 位的字节)表示的请求体的长度</td><td style="text-align:left">Content-Length: 348</td></tr><tr><td style="text-align:left">Content-MD5</td><td style="text-align:left">请求体的内容的二进制 MD5 散列值,以 Base64 编码的结果</td><td style="text-align:left">Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ==</td></tr><tr><td style="text-align:left">Content-Type</td><td style="text-align:left">请求体的 多媒体类型 (用于 POST 和 PUT 请求中)</td><td style="text-align:left">Content-Type: application/x-www-form-urlencoded</td></tr><tr><td style="text-align:left">Cookie</td><td style="text-align:left">之前由服务器通过 Set- Cookie (下文详述)发送的一个 超文本传输协议 Cookie</td><td style="text-align:left">Cookie: $Version=1; Skin=new;</td></tr><tr><td style="text-align:left">Date</td><td style="text-align:left">发送该消息的日期和时间(按照 RFC 7231 中定义的"超文本传输协议日期"格式来发送)</td><td style="text-align:left">Date: Tue, 15 Nov 1994 08:12:31 GMT</td></tr><tr><td style="text-align:left">Expect</td><td style="text-align:left">表明客户端要求服务器做出特定的行为</td><td style="text-align:left">Expect: 100-continue</td></tr><tr><td style="text-align:left">From</td><td style="text-align:left">发起此请求的用户的邮件地址</td><td style="text-align:left">From: <a href="mailto:[email protected]">[email protected]</a></td></tr><tr><td style="text-align:left">Host</td><td style="text-align:left">服务器的域名(用于虚拟主机 ),以及服务器所监听的传输控制协议端口号。如果所请求的端口是对应的服务的标准端口,则端口号可被省略。</td><td style="text-align:left">Host: <a href="http://en.wikipedia.org:80">en.wikipedia.org:80</a></td></tr><tr><td style="text-align:left">If-Match</td><td style="text-align:left">仅当客户端提供的实体与服务器上对应的实体相匹配时,才进行对应的操作。主要作用时,用作像 PUT 这样的方法中,仅当从用户上次更新某个资源以来,该资源未被修改的情况下,才更新该资源。</td><td style="text-align:left">If-Match: “737060cd8c284d8af7ad3082f209582d”</td></tr><tr><td style="text-align:left">If-Modified-Since</td><td style="text-align:left">允许在对应的内容未被修改的情况下返回 304 未修改( 304 Not Modified )</td><td style="text-align:left">If-Modified-Since: Sat, 29 Oct 1994 19:43:31 GMT</td></tr><tr><td style="text-align:left">If-None-Match</td><td style="text-align:left">允许在对应的内容未被修改的情况下返回 304 未修改( 304 Not Modified )</td><td style="text-align:left">If-None-Match: “737060cd8c284d8af7ad3082f209582d”</td></tr><tr><td style="text-align:left">If-Range</td><td style="text-align:left">如果该实体未被修改过,则向我发送我所缺少的那一个或多个部分;否则,发送整个新的实体</td><td style="text-align:left">If-Range: “737060cd8c284d8af7ad3082f209582d”</td></tr><tr><td style="text-align:left">If-Unmodified-Since</td><td style="text-align:left">仅当该实体自某个特定时间已来未被修改的情况下,才发送回应。</td><td style="text-align:left">If-Unmodified-Since: Sat, 29 Oct 1994 19:43:31 GMT</td></tr><tr><td style="text-align:left">Max-Forwards</td><td style="text-align:left">限制该消息可被代理及网关转发的次数。</td><td style="text-align:left">Max-Forwards: 10</td></tr><tr><td style="text-align:left">Origin</td><td style="text-align:left">发起一个针对 跨来源资源共享 的请求。</td><td style="text-align:left">Origin: <a href="http://www.example-social-network.com/">http://www.example-social-network.comopen in new window</a></td></tr><tr><td style="text-align:left">Pragma</td><td style="text-align:left">与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生多种效果。</td><td style="text-align:left">Pragma: no-cache</td></tr><tr><td style="text-align:left">Proxy-Authorization</td><td style="text-align:left">用来向代理进行认证的认证信息。</td><td style="text-align:left">Proxy-Authorization: Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==</td></tr><tr><td style="text-align:left">Range</td><td style="text-align:left">仅请求某个实体的一部分。字节偏移以 0 开始。参见字节服务。</td><td style="text-align:left">Range: bytes=500-999</td></tr><tr><td style="text-align:left">Referer</td><td style="text-align:left">表示浏览器所访问的前一个页面,正是那个页面上的某个链接将浏览器带到了当前所请求的这个页面。</td><td style="text-align:left">Referer: <a href="https://en.wikipedia.org/wiki/Main_Page">http://en.wikipedia.org/wiki/Main_Pageopen in new window</a></td></tr><tr><td style="text-align:left">TE</td><td style="text-align:left">浏览器预期接受的传输编码方式:可使用回应协议头 Transfer-Encoding 字段中的值;</td><td style="text-align:left">TE: trailers, deflate</td></tr><tr><td style="text-align:left">Upgrade</td><td style="text-align:left">要求服务器升级到另一个协议。</td><td style="text-align:left">Upgrade: HTTP/2.0, SHTTP/1.3, IRC/6.9, RTA/x11</td></tr><tr><td style="text-align:left">User-Agent</td><td style="text-align:left">浏览器的浏览器身份标识字符串</td><td style="text-align:left">User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:12.0) Gecko/20100101 Firefox/21.0</td></tr><tr><td style="text-align:left">Via</td><td style="text-align:left">向服务器告知,这个请求是由哪些代理发出的。</td><td style="text-align:left">Via: 1.0 fred, 1.1 <a href="http://example.com">example.com</a> (Apache/1.1)</td></tr><tr><td style="text-align:left">Warning</td><td style="text-align:left">一个一般性的警告,告知,在实体内容体中可能存在错误。</td><td style="text-align:left">Warning: 199 Miscellaneous warning</td></tr></tbody></table><h3 id="-13"><a class="markdownIt-Anchor" href="#-13"></a> <a href="#http-%E5%92%8C-https-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB-%E9%87%8D%E8%A6%81">#</a> HTTP 和 HTTPS 有什么区别?(重要)</h3><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/http-vs-https.png" alt="HTTP 和 HTTPS 对比"></p><ul><li><strong>端口号</strong> :HTTP 默认是 80,HTTPS 默认是 443。</li><li><strong>URL 前缀</strong> :HTTP 的 URL 前缀是 <code>http://</code>,HTTPS 的 URL 前缀是 <code>https://</code>。</li><li><strong>安全性和资源消耗</strong> : HTTP 协议运行在 TCP 之上,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份。HTTPS 是运行在 SSL/TLS 之上的 HTTP 协议,SSL/TLS 运行在 TCP 之上。所有传输的内容都经过加密,<strong>加密采用对称加密,但对称加密的密钥用服务器方的证书进行了非对称加密</strong>。所以说,HTTP 安全性没有 HTTPS 高,但是 HTTPS 比 HTTP 耗费更多服务器资源。</li><li><strong>SEO(搜索引擎优化)</strong> :搜索引擎通常会更青睐使用 HTTPS 协议的网站,因为 HTTPS 能够提供更高的安全性和用户隐私保护。使用 HTTPS 协议的网站在搜索结果中可能会被优先显示,从而对 SEO 产生影响。</li></ul><p>关于 HTTP 和 HTTPS 更详细的对比总结,可以看我写的这篇文章:<a href="">HTTP vs HTTPS(应用层)</a> 。</p><h3 id="-14"><a class="markdownIt-Anchor" href="#-14"></a> <a href="#http-1-0-%E5%92%8C-http-1-1-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB">#</a> HTTP/1.0 和 HTTP/1.1 有什么区别?</h3><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.0-vs-http1.1.png" alt="HTTP/1.0 和 HTTP/1.1 对比"></p><ul><li><strong>连接方式</strong> : HTTP/1.0 为短连接,HTTP/1.1 支持长连接。</li><li><strong>状态响应码</strong> : HTTP/1.1 中新加入了大量的状态码,光是错误响应状态码就新增了 24 种。比如说,<code>100 (Continue)</code>——在请求大资源前的预热请求,<code>206 (Partial Content)</code>——范围请求的标识码,<code>409 (Conflict)</code>——请求与当前资源的规定冲突,<code>410 (Gone)</code>——资源已被永久转移,而且没有任何已知的转发地址。</li><li><strong>缓存机制</strong> : 在 HTTP/1.0 中主要使用 Header 里的 If-Modified-Since,Expires 来做为缓存判断的标准,HTTP/1.1 则引入了更多的缓存控制策略例如 Entity tag,If-Unmodified-Since, If-Match, If-None-Match 等更多可供选择的缓存头来控制缓存策略。</li><li><strong>带宽</strong> :HTTP/1.0 中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP/1.1 则在请求头引入了 range 头域,它允许只请求资源的某个部分,即返回码是 206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。</li><li><strong>Host 头(Host Header)处理</strong> :HTTP/1.1 引入了 Host 头字段,允许在同一 IP 地址上托管多个域名,从而支持虚拟主机的功能。而 HTTP/1.0 没有 Host 头字段,无法实现虚拟主机。</li></ul><p>关于 HTTP/1.0 和 HTTP/1.1 更详细的对比总结,可以看我写的这篇文章:<a href="">HTTP/1.0 vs HTTP/1.1(应用层)</a> 。</p><h3 id="-15"><a class="markdownIt-Anchor" href="#-15"></a> <a href="#http-1-1-%E5%92%8C-http-2-0-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB">#</a> HTTP/1.1 和 HTTP/2.0 有什么区别?</h3><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/http1.1-vs-http2.0.png" alt="HTTP/1.0 和 HTTP/1.1 对比"></p><ul><li><strong>IO 多路复用(Multiplexing)</strong> :HTTP/2.0 在同一连接上可以同时传输多个请求和响应(可以看作是 HTTP/1.1 中长链接的升级版本)。HTTP/1.1 则使用串行方式,每个请求和响应都需要独立的连接。这使得 HTTP/2.0 在处理多个请求时更加高效,减少了网络延迟和提高了性能。</li><li><strong>二进制帧(Binary Frames)</strong> :HTTP/2.0 使用二进制帧进行数据传输,而 HTTP/1.1 则使用文本格式的报文。二进制帧更加紧凑和高效,减少了传输的数据量和带宽消耗。</li><li><strong>头部压缩(Header Compression)</strong> :HTTP/1.1 支持<code>Body</code>压缩,<code>Header</code>不支持压缩。HTTP/2.0 支持对<code>Header</code>压缩,减少了网络开销。</li><li><strong>服务器推送(Server Push)</strong>:HTTP/2.0 支持服务器推送,可以在客户端请求一个资源时,将其他相关资源一并推送给客户端,从而减少了客户端的请求次数和延迟。而 HTTP/1.1 需要客户端自己发送请求来获取相关资源。</li></ul><h3 id="-16"><a class="markdownIt-Anchor" href="#-16"></a> <a href="#http-2-0-%E5%92%8C-http-3-0-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB">#</a> HTTP/2.0 和 HTTP/3.0 有什么区别?</h3><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/http2.0-vs-http3.0.png" alt="HTTP/2.0 和 HTTP/3.0 对比"></p><ul><li><strong>传输协议</strong> :HTTP/2.0 是基于 TCP 协议实现的,HTTP/3.0 新增了<mark> <strong>QUIC</strong>(Quick UDP Internet Connections)</mark> 协议来实现可靠的传输,提供与 TLS/SSL 相当的安全性,具有较低的连接和传输延迟。你可以将 QUIC 看作是 UDP 的升级版本,在其基础上新增了很多功能比如加密、重传等等。HTTP/3.0 之前名为 HTTP-over-QUIC,从这个名字中我们也可以发现,HTTP/3 最大的改造就是使用了 QUIC。</li><li><strong>连接建立</strong> :HTTP/2.0 需要经过经典的 TCP 三次握手过程(一般是 3 个 RTT)。由于 QUIC 协议的特性,HTTP/3.0 可以避免 TCP 三次握手的延迟,允许在第一次连接时发送数据(0 个 RTT ,零往返时间)。</li><li><strong>队头阻塞</strong> :HTTP/2.0 多请求复用一个 TCP 连接,一旦发生丢包,就会阻塞住所有的 HTTP 请求。由于 QUIC 协议的特性,HTTP/3.0 在一定程度上解决了队头阻塞(Head-of-Line blocking, 简写:HOL blocking)问题,一个连接建立多个不同的数据流,这些数据流之间独立互不影响,某个数据流发生丢包了,其它数据流不受影响(本质上是多路复用+轮询)。</li><li><strong>错误恢复</strong> :HTTP/3.0 具有更好的错误恢复机制,当出现丢包、延迟等网络问题时,可以更快地进行恢复和重传。而 HTTP/2.0 则需要依赖于 TCP 的错误恢复机制。</li><li><strong>安全性</strong> :HTTP/2.0 和 HTTP/3.0 在安全性上都有较高的要求,支持加密通信,但在实现上有所不同。HTTP/2.0 使用 TLS 协议进行加密,而 HTTP/3.0 基于 QUIC 协议,包含了内置的加密和身份验证机制,可以提供更强的安全性。</li></ul><h3 id="-17"><a class="markdownIt-Anchor" href="#-17"></a> <a href="#http-%E6%98%AF%E4%B8%8D%E4%BF%9D%E5%AD%98%E7%8A%B6%E6%80%81%E7%9A%84%E5%8D%8F%E8%AE%AE-%E5%A6%82%E4%BD%95%E4%BF%9D%E5%AD%98%E7%94%A8%E6%88%B7%E7%8A%B6%E6%80%81">#</a> HTTP 是不保存状态的协议, 如何保存用户状态?</h3><p>HTTP 是一种不保存状态,即无状态(stateless)协议。也就是说 HTTP 协议自身不对请求和响应之间的通信状态进行保存。那么我们保存用户状态呢?Session 机制的存在就是为了解决这个问题,Session 的主要作用就是通过服务端记录用户的状态。典型的场景是购物车,当你要添加商品到购物车的时候,系统不知道是哪个用户操作的,因为 HTTP 协议是无状态的。服务端给特定的用户创建特定的 Session 之后就可以标识这个用户并且跟踪这个用户了(一般情况下,服务器会在一定时间内保存这个 Session,过了时间限制,就会销毁这个 Session)。</p><p>在服务端保存 Session 的方法很多,最常用的就是内存和数据库(比如是使用内存数据库 redis 保存)。既然 Session 存放在服务器端,那么我们如何实现 Session 跟踪呢?大部分情况下,我们都是通过在 Cookie 中附加一个 Session ID 来方式来跟踪。</p><p><strong>Cookie 被禁用怎么办?</strong></p><p>最常用的就是利用 URL 重写把 Session ID 直接附加在 URL 路径的后面。</p><h3 id="-18"><a class="markdownIt-Anchor" href="#-18"></a> <a href="#uri-%E5%92%8C-url-%E7%9A%84%E5%8C%BA%E5%88%AB%E6%98%AF%E4%BB%80%E4%B9%88">#</a> URI 和 URL 的区别是什么?</h3><ul><li>URI(Uniform Resource Identifier) 是统一资源标志符,可以唯一标识一个资源。</li><li>URL(Uniform Resource Locator) 是统一资源定位符,可以提供该资源的路径。它是一种具体的 URI,即 URL 可以用来标识一个资源,而且还指明了如何 locate 这个资源。</li></ul><p>URI 的作用像身份证号一样,URL 的作用更像家庭住址一样。URL 是一种具体的 URI,它不仅唯一标识资源,而且还提供了定位该资源的信息。</p><h3 id="-19"><a class="markdownIt-Anchor" href="#-19"></a> <a href="#cookie-%E5%92%8C-session-%E6%9C%89%E4%BB%80%E4%B9%88%E5%8C%BA%E5%88%AB">#</a> Cookie 和 Session 有什么区别?</h3><p>准确点来说,这个问题属于认证授权的范畴,你可以在 <a href="https://javaguide.cn/system-design/security/basis-of-authority-certification.html">认证授权基础概念详解open in new window</a> 这篇文章中找到详细的答案。</p><h2 id="-20"><a class="markdownIt-Anchor" href="#-20"></a> <a href="#ping">#</a> PING</h2><h3 id="-21"><a class="markdownIt-Anchor" href="#-21"></a> <a href="#ping-%E5%91%BD%E4%BB%A4%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88">#</a> PING 命令的作用是什么?</h3><p>PING 命令是一种常用的网络诊断工具,经常用来测试网络中主机之间的连通性和网络延迟。</p><p>这里简单举一个例子,我们来 PING 一下百度。</p><figure class="highlight bash"><table><tbody><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"># 发送4个PING请求数据包到 www.baidu.com</span></span><br><span class="line">❯ ping -c 4 www.baidu.com</span><br><span class="line"></span><br><span class="line">PING www.a.shifen.com (14.119.104.189): 56 data bytes</span><br><span class="line">64 bytes from 14.119.104.189: icmp_seq=0 ttl=54 time=27.867 ms</span><br><span class="line">64 bytes from 14.119.104.189: icmp_seq=1 ttl=54 time=28.732 ms</span><br><span class="line">64 bytes from 14.119.104.189: icmp_seq=2 ttl=54 time=27.571 ms</span><br><span class="line">64 bytes from 14.119.104.189: icmp_seq=3 ttl=54 time=27.581 ms</span><br><span class="line"></span><br><span class="line">--- www.a.shifen.com ping statistics ---</span><br><span class="line">4 packets transmitted, 4 packets received, 0.0% packet loss</span><br><span class="line">round-trip min/avg/max/stddev = 27.571/27.938/28.732/0.474 ms</span><br></pre></td></tr></tbody></table></figure><p>PING 命令的输出结果通常包括以下几部分信息:</p><ol><li><strong>ICMP Echo Request(请求报文)信息</strong> :序列号、TTL(Time to Live)值。</li><li><strong>目标主机的域名或 IP 地址</strong> :输出结果的第一行。</li><li><strong>往返时间(RTT,Round-Trip Time)</strong> :从发送 ICMP Echo Request(请求报文)到接收到 ICMP Echo Reply(响应报文)的总时间,用来衡量网络连接的延迟。</li><li><strong>统计结果(Statistics)</strong> :包括发送的 ICMP 请求数据包数量、接收到的 ICMP 响应数据包数量、丢包率、往返时间(RTT)的最小、平均、最大和标准偏差值。</li></ol><p>如果 PING 对应的目标主机无法得到正确的响应,则表明这两个主机之间的连通性存在问题。如果往返时间(RTT)过高,则表明网络延迟过高。</p><h3 id="-22"><a class="markdownIt-Anchor" href="#-22"></a> <a href="#ping-%E5%91%BD%E4%BB%A4%E7%9A%84%E5%B7%A5%E4%BD%9C%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88">#</a> PING 命令的工作原理是什么?</h3><p>PING 基于网络层的 <strong>ICMP(Internet Control Message Protocol,互联网控制报文协议)</strong>,其主要原理就是通过在网络上发送和接收 ICMP 报文实现的。</p><p>ICMP 报文中包含了类型字段,用于标识 ICMP 报文类型。ICMP 报文的类型有很多种,但大致可以分为两类:</p><ul><li><strong>查询报文类型</strong> :向目标主机发送请求并期望得到响应。</li><li><strong>差错报文类型</strong> :向源主机发送错误信息,用于报告网络中的错误情况。</li></ul><p>PING 用到的 ICMP Echo Request(类型为 8 ) 和 ICMP Echo Reply(类型为 0) 属于查询报文类型 。</p><ul><li>PING 命令会向目标主机发送 ICMP Echo Request。</li><li>如果两个主机的连通性正常,目标主机会返回一个对应的 ICMP Echo Reply。</li></ul><h2 id="-23"><a class="markdownIt-Anchor" href="#-23"></a> <a href="#dns">#</a> DNS</h2><h3 id="-24"><a class="markdownIt-Anchor" href="#-24"></a> <a href="#dns-%E7%9A%84%E4%BD%9C%E7%94%A8%E6%98%AF%E4%BB%80%E4%B9%88">#</a> DNS 的作用是什么?</h3><p>DNS(Domain Name System)域名管理系统,是当用户使用浏览器访问网址之后,使用的第一个重要协议。DNS 要解决的是<strong>域名和 IP 地址的映射问题</strong>。</p><p><img src="https://oss.javaguide.cn/github/javaguide/cs-basics/network/dns-overview.png" alt="DNS:域名系统"></p><p>在实际使用中,有一种情况下,浏览器是可以不必动用 DNS 就可以获知域名和 IP 地址的映射的。浏览器在本地会维护一个<code>hosts</code>列表,一般来说浏览器要先查看要访问的域名是否在<code>hosts</code>列表中,如果有的话,直接提取对应的 IP 地址记录,就好了。如果本地<code>hosts</code>列表内没有域名-IP 对应记录的话,那么 DNS 就闪亮登场了。</p><p>目前 DNS 的设计采用的是分布式、层次数据库结构,<strong>DNS 是应用层协议,基于 UDP 协议之上,端口为 53</strong> 。</p><h3 id="-25"><a class="markdownIt-Anchor" href="#-25"></a> <a href="#dns-%E6%9C%8D%E5%8A%A1%E5%99%A8%E6%9C%89%E5%93%AA%E4%BA%9B">#</a> DNS 服务器有哪些?</h3><p>DNS 服务器自底向上可以依次分为以下几个层级(所有 DNS 服务器都属于以下四个类别之一):</p><ul><li>根 DNS 服务器。根 DNS 服务器提供 TLD 服务器的 IP 地址。目前世界上只有 13 组根服务器,我国境内目前仍没有根服务器。</li><li>顶级域 DNS 服务器(TLD 服务器)。顶级域是指域名的后缀,如<code>com</code>、<code>org</code>、<code>net</code>和<code>edu</code>等。国家也有自己的顶级域,如<code>uk</code>、<code>fr</code>和<code>ca</code>。TLD 服务器提供了权威 DNS 服务器的 IP 地址。</li><li>权威 DNS 服务器。在因特网上具有公共可访问主机的每个组织机构必须提供公共可访问的 DNS 记录,这些记录将这些主机的名字映射为 IP 地址。</li><li>本地 DNS 服务器。每个 ISP(互联网服务提供商)都有一个自己的本地 DNS 服务器。当主机发出 DNS 请求时,该请求被发往本地 DNS 服务器,它起着代理的作用,并将该请求转发到 DNS 层次结构中。严格说来,不属于 DNS 层级结构</li></ul><h3 id="-26"><a class="markdownIt-Anchor" href="#-26"></a> <a href="#dns-%E8%A7%A3%E6%9E%90%E7%9A%84%E8%BF%87%E7%A8%8B%E6%98%AF%E4%BB%80%E4%B9%88%E6%A0%B7%E7%9A%84">#</a> DNS 解析的过程是什么样的?</h3><p>整个过程的步骤比较多,我单独写了一篇文章详细介绍:<a href="">DNS 域名系统详解(应用层)</a> 。</p>]]></content>
<summary type="html"><h2 id="计算机网络基础"><a class="markdownIt-Anchor" href="#计算机网络基础"></a> 计算机网络基础</h2>
<h3 id=""><a class="markdownIt-Anchor" href="#"></a> <a href</summary>
<category term="面试题" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="计算机网络" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
<category term="面试题" scheme="http://example.com/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="计算机网络" scheme="http://example.com/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
</entry>
<entry>
<title>操作系统问题</title>
<link href="http://example.com/2023/04/05/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<id>http://example.com/2023/04/05/interview/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E9%9D%A2%E8%AF%95%E9%A2%98/</id>
<published>2023-04-05T15:08:34.638Z</published>
<updated>2023-08-02T15:07:58.331Z</updated>
<content type="html"><![CDATA[<p><a href="https://cloud.tencent.com/developer/article/1815965">这 50 道操作系统面试题,真牛批! - 腾讯云开发者社区-腾讯云 (tencent.com)</a></p><h2 id="1-进程和线程的区别"><a class="markdownIt-Anchor" href="#1-进程和线程的区别"></a> 1. 进程和线程的区别?</h2><ul><li>调度:进程是资源管理的基本单位,线程是程序执行的基本单位。</li><li>切换:线程上下文切换比进程上下文切换要快得多。</li><li>拥有资源: 进程是拥有资源的一个独立单位,线程不拥有系统资源,但是可以访问隶属于进程的资源。</li><li>系统开销: 创建或撤销进程时,系统都要为之分配或回收系统资源,如内存空间,I/O设备等,OS所付出的开销显著大于在创建或撤销线程时的开销,进程切换的开销也远大于线程切换的开销。</li></ul><hr><p>线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。</p><ul><li>**根本区别:**进程是<mark>操作系统资源分配</mark>的基本单位,而线<mark>程是处理器任务调度和执行</mark>的基本单位</li><li><strong>资源开销</strong>:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。</li><li>**包含关系:**如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。</li><li>**内存分配:**同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的</li><li>**影响关系:**一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。</li><li>**执行过程:**每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行</li></ul><h2 id="什么是协程"><a class="markdownIt-Anchor" href="#什么是协程"></a> 什么是协程</h2><p>协程,是一种比线程更加轻量级的存在。正如一个进程可以拥有多个线程一样,一个线程也可以拥有多个协程。</p><p>最重要的是,协程不是被操作系统内核所管理,而完全是由程序所控制(也就是在<strong>用户态</strong>中执行)。</p><p>这样带来的好处就是性能得到了很大的提升,不会像线程切换那样消耗资源。</p><p>(Java的原生语法中并没有实现协程)</p><p>协程是一种用户态的轻量级线程。协程的调度完全由用户控制,协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其它地方,在切回来的时候,恢复先前保存的寄存器上下文和栈,直接操作栈则基本没有内核切换的开销,可以不加锁的访问全局变量,所以上下文的切换非常快。</p><h2 id="2-协程与线程的区别"><a class="markdownIt-Anchor" href="#2-协程与线程的区别"></a> 2. 协程与线程的区别?</h2><ul><li><p>线程和进程都是同步机制,而协程是异步机制。</p></li><li><p>线程是抢占式,而协程是非抢占式的。需要用户释放使用权切换到其他协程,因此同一时间其实只有一个协程拥有运行权,相当于单线程的能力。</p></li><li><p>一个线程可以有多个协程,一个进程也可以有多个协程。</p></li><li><p>协程不被操作系统内核管理,而完全是由程序控制。线程是被分割的CPU资源,协程是组织好的代码流程,线程是协程的资源。但协程不会直接使用线程,协程直接利用的是执行器关联任意线程或线程池。</p></li><li><p>协程能保留上一次调用时的状态。</p></li></ul><ol><li>由于协程的特性, 适合执行大量的<strong>I/O 密集型任务</strong>, 而线程在这方面弱于协程</li><li>协程涉及到函数的切换, 多线程涉及到线程的切换, 所以都有<strong>执行上下文</strong>, 但是协程不是被操作系统内核所管理, 而完全是由程序所控制(也就是在<strong>用户态</strong>执行), 这样带来的好处就是性能得到了很大的提升, 不会像线程那样需要<strong>在内核态进行上下文切换</strong>来消耗资源,因此<strong>协程的开销远远小于线程的开销</strong></li><li>同一时间, 在多核处理器的环境下, <strong>多个线程是可以并行的</strong>,但是<strong>运行的协程的函数却只能有一个</strong>,<strong>其他的协程的函数都被suspend</strong>, 即<strong>协程是并发的</strong></li><li>由于协程在同一个线程中, 所以不需要用来守卫<a href="https://zh.wikipedia.org/wiki/%E5%85%B3%E9%94%AE%E5%8C%BA%E6%AE%B5"><strong>临界区段</strong></a>的同步性原语(primitive)比如<a href="https://zh.wikipedia.org/wiki/%E4%BA%92%E6%96%A5%E9%94%81">互斥锁</a>、信号量等,并且<strong>不需要来自操作系统的支持</strong></li><li>在协程之间的<a href="https://zh.wikipedia.org/wiki/%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2">切换</a>不需要涉及任何<a href="https://zh.wikipedia.org/wiki/%E7%B3%BB%E7%BB%9F%E8%B0%83%E7%94%A8">系统调用</a>或任何<a href="https://zh.wikipedia.org/w/index.php?title=%E9%98%BB%E5%A1%9E_(%E8%AE%A1%E7%AE%97)&action=edit&redlink=1">阻塞</a>调用</li><li><strong>通常的线程是抢先式(即由操作系统分配执行权)</strong>, 而协程是<strong>由程序分配执行权</strong></li></ol><h2 id="协程地好处"><a class="markdownIt-Anchor" href="#协程地好处"></a> 协程地好处:</h2><ul><li>协程能<strong>保留上一次调用时的状态</strong>。</li><li>线程和进程都是同步机制,而协程是<strong>异步机制</strong>。</li></ul><h2 id="3-并发和并行有什么区别"><a class="markdownIt-Anchor" href="#3-并发和并行有什么区别"></a> 3. 并发和并行有什么区别?</h2><p>并发就是在一段时间内,多个任务都会被处理;但在某一时刻,只有一个任务在执行。单核处理器可以做到并发。比如有两个进程<code>A</code>和<code>B</code>,<code>A</code>运行一个时间片之后,切换到<code>B</code>,<code>B</code>运行一个时间片之后又切换到<code>A</code>。因为切换速度足够快,所以宏观上表现为在一段时间内能同时运行多个程序。</p><p>并行就是在同一时刻,有多个任务在执行。这个需要多核处理器才能完成,在微观上就能同时执行多条指令,不同的程序被放到不同的处理器上运行,这个是物理上的多个进程同时进行。</p><h2 id="4-进程与线程的切换流程"><a class="markdownIt-Anchor" href="#4-进程与线程的切换流程"></a> 4. 进程与线程的切换流程?</h2><p>进程切换分两步:</p><p>1、切换<strong>页表</strong>以使用新的地址空间,一旦去切换上下文,处理器中所有已经缓存的内存地址一瞬间都作废了。</p><p>2、切换内核栈和硬件上下文。</p><p>对于linux来说,线程和进程的最大区别就在于地址空间,对于线程切换,第1步是不需要做的,第2步是进程和线程切换都要做的。</p><p>因为每个进程都有自己的虚拟地址空间,而线程是共享所在进程的虚拟地址空间的,因此同一个进程中的线程进行线程切换时不涉及虚拟地址空间的转换。</p><h2 id="5-为什么虚拟地址空间切换会比较耗时"><a class="markdownIt-Anchor" href="#5-为什么虚拟地址空间切换会比较耗时"></a> 5. 为什么虚拟地址空间切换会比较耗时?</h2><p>进程都有自己的虚拟地址空间,把虚拟地址转换为物理地址需要查找页表,页表查找是一个很慢的过程,因此通常使用Cache来缓存常用的地址映射,这样可以加速页表查找,这个Cache就是TLB(translation Lookaside Buffer,TLB本质上就是一个Cache,是用来加速页表查找的)。</p><p>由于每个进程都有自己的虚拟地址空间,那么显然每个进程都有自己的页表,那么<strong>当进程切换后页表也要进行切换,页表切换后TLB就失效了</strong>,Cache失效导致命中率降低,那么虚拟地址转换为物理地址就会变慢,表现出来的就是程序运行会变慢,而线程切换则不会导致TLB失效,因为线程无需切换地址空间,因此我们通常说线程切换要比较进程切换块,原因就在这里。</p><h2 id="6-进程间通信方式有哪些"><a class="markdownIt-Anchor" href="#6-进程间通信方式有哪些"></a> 6. 进程间通信方式有哪些?</h2><ul><li><p>管道:管道这种通讯方式有两种限制,一是半双工的通信,数据只能单向流动,二是只能在具有亲缘关系的进程间使用。进程的亲缘关系通常是指父子进程关系。</p><p>管道可以分为两类:匿名管道和命名管道。匿名管道是单向的,只能在有亲缘关系的进程间通信;命名管道以磁盘文件的方式存在,可以实现本机任意两个进程通信。</p></li><li><p>信号 : 信号是一种比较复杂的通信方式,信号可以在任何时候发给某一进程,而无需知道该进程的状态。</p><p><strong>Linux系统中常用信号</strong>:<br>(1)<strong>SIGHUP</strong>:用户从终端注销,所有已启动进程都将收到该进程。系统缺省状态下对该信号的处理是终止进程。</p><p>(2)<strong>SIGINT</strong>:程序终止信号。程序运行过程中,按<code>Ctrl+C</code>键将产生该信号。</p><p>(3)<strong>SIGQUIT</strong>:程序退出信号。程序运行过程中,按<code>Ctrl+\\</code>键将产生该信号。</p><p>(4)<strong>SIGBUS和SIGSEGV</strong>:进程访问非法地址。</p><p>(5)<strong>SIGFPE</strong>:运算中出现致命错误,如除零操作、数据溢出等。</p><p>(6)<strong>SIGKILL</strong>:用户终止进程执行信号。shell下执行<code>kill -9</code>发送该信号。</p><p>(7)<strong>SIGTERM</strong>:结束进程信号。shell下执行<code>kill 进程pid</code>发送该信号。</p><p>(8)<strong>SIGALRM</strong>:定时器信号。</p><p>(9)<strong>SIGCLD</strong>:子进程退出信号。如果其父进程没有忽略该信号也没有处理该信号,则子进程退出后将形成僵尸进程。</p></li><li><p>信号量:信号量是一个<strong>计数器</strong>,可以用来控制多个进程对共享资源的访问。它常作为一种<strong>锁机制</strong>,防止某进程正在访问共享资源时,其他进程也访问该资源。因此,主要作为进程间以及同一进程内不同线程之间的同步手段。</p></li><li><p>消息队列:消息队列是消息的链接表,包括Posix消息队列和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺点。</p></li><li><p>共享内存:共享内存就是映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问。共享内存是最快的 IPC 方式,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。</p></li><li><p>Socket:与其他通信机制不同的是,它可用于不同机器间的进程通信。</p></li></ul><p><strong>优缺点</strong>:</p><ul><li><p>管道:速度慢,容量有限;</p></li><li><p>Socket:任何进程间都能通讯,但速度慢;</p></li><li><p>消息队列:容量受到系统限制,且要注意第一次读的时候,要考虑上一次没有读完数据的问题;</p></li><li><p>信号量:不能传递复杂消息,只能用来同步;</p></li><li><p>共享内存区:能够很容易控制容量,速度快,但要保持同步,比如一个进程在写的时候,另一个进程要注意读写的问题,相当于线程中的线程安全,当然,共享内存区同样可以用作线程间通讯,不过没这个必要,线程间本来就已经共享了同一进程内的一块内存。</p></li></ul><h2 id="7-进程间同步的方式有哪些"><a class="markdownIt-Anchor" href="#7-进程间同步的方式有哪些"></a> 7. 进程间同步的方式有哪些?</h2><p>1、临界区:通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。</p><p>优点:保证在某一时刻只有一个线程能访问数据的简便办法。</p><p>缺点:虽然临界区同步速度很快,但却只能用来同步本进程内的线程,而不可用来同步多个进程中的线程。</p><p>2、互斥量:为协调共同对一个共享资源的单独访问而设计的。互斥量跟临界区很相似,比临界区复杂,互斥对象只有一个,只有拥有互斥对象的线程才具有访问资源的权限。</p><p>优点:使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。</p><p>缺点:</p><ul><li><p>互斥量是可以命名的,也就是说它可以跨越进程使用,所以创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量。</p></li><li><p>通过互斥量可以指定资源被独占的方式使用,但如果有下面一种情况通过互斥量就无法处理,比如现在一位用户购买了一份三个并发访问许可的数据库系统,可以根据用户购买的访问许可数量来决定有多少个线程/进程能同时进行数据库操作,这时候如果利用互斥量就没有办法完成这个要求,信号量对象可以说是一种资源计数器。</p></li></ul><p>3、信号量:为控制一个具有有限数量用户资源而设计。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。互斥量是信号量的一种特殊情况,当信号量的最大资源数=1就是互斥量了。</p><p>优点:适用于对Socket(套接字)程序中线程的同步。</p><p>缺点:</p><ul><li><p>信号量机制必须有公共内存,不能用于分布式操作系统,这是它最大的弱点;</p></li><li><p>信号量机制功能强大,但使用时对信号量的操作分散, 而且难以控制,读写和维护都很困难,加重了程序员的编码负担;</p></li><li><p>核心操作P-V分散在各用户程序的代码中,不易控制和管理,一旦错误,后果严重,且不易发现和纠正。</p></li></ul><p>4、事件: 用来通知线程有一些事件已发生,从而启动后继任务的开始。</p><p>优点:事件对象通过通知操作的方式来保持线程的同步,并且可以实现不同进程中的线程同步操作。</p><h2 id="8-线程同步的方式有哪些"><a class="markdownIt-Anchor" href="#8-线程同步的方式有哪些"></a> 8. 线程同步的方式有哪些?</h2><p>1、临界区:当多个线程访问一个独占性共享资源时,可以使用临界区对象。拥有临界区的线程可以访问被保护起来的资源或代码段,其他线程若想访问,则被挂起,直到拥有临界区的线程放弃临界区为止,以此达到用原子方式操 作共享资源的目的。</p><p>2、事件:事件机制,则允许一个线程在处理完一个任务后,主动唤醒另外一个线程执行任务。</p><p>3、互斥量:互斥对象和临界区对象非常相似,只是其允许在进程间使用,而临界区只限制与同一进程的各个线程之间使用,但是更节省资源,更有效率。</p><p>4、信号量:当需要一个计数器来限制可以使用某共享资源的线程数目时,可以使用“信号量”对象。</p><p>区别:</p><ul><li><p>互斥量与临界区的作用非常相似,但互斥量是可以命名的,也就是说互斥量可以跨越进程使用,但创建互斥量需要的资源更多,所以如果只为了在进程内部是用的话使用临界区会带来速度上的优势并能够减少资源占用量 。因为互斥量是跨进程的互斥量一旦被创建,就可以通过名字打开它。</p></li><li><p>互斥量,信号量,事件都可以被跨越进程使用来进行同步数据操作。</p></li></ul><h2 id="9-线程的分类"><a class="markdownIt-Anchor" href="#9-线程的分类"></a> 9. 线程的分类?</h2><p>从线程的运行空间来说,分为用户级线程(user-level thread, ULT)和内核级线程(kernel-level, KLT)</p><p><strong>内核级线程</strong>:这类线程依赖于内核,又称为内核支持的线程或轻量级进程。无论是在用户程序中的线程还是系统进程中的线程,它们的创建、撤销和切换都由内核实现。比如英特尔i5-8250U是4核8线程,这里的线程就是内核级线程</p><p><strong>用户级线程</strong>:它仅存在于用户级中,这种线程是<strong>不依赖于操作系统核心</strong>的。应用进程利用<strong>线程库来完成其创建和管理</strong>,速度比较快,<strong>操作系统内核无法感知用户级线程的存在</strong>。</p><h2 id="10-什么是临界区如何解决冲突"><a class="markdownIt-Anchor" href="#10-什么是临界区如何解决冲突"></a> 10. 什么是临界区,如何解决冲突?</h2><p>每个进程中访问临界资源的那段程序称为临界区,<strong>一次仅允许一个进程使用的资源称为临界资源。</strong></p><p>解决冲突的办法:</p><ul><li><p>如果有若干进程要求进入空闲的临界区,<strong>一次仅允许一个进程进入</strong>,如已有进程进入自己的临界区,则其它所有试图进入临界区的进程必须等待;</p></li><li><p>进入临界区的进程要在<strong>有限时间内退出</strong>。</p></li><li><p>如果进程不能进入自己的临界区,则应<strong>让出CPU</strong>,避免进程出现“忙等”现象。</p></li></ul><h2 id="11-什么是死锁死锁产生的条件"><a class="markdownIt-Anchor" href="#11-什么是死锁死锁产生的条件"></a> 11. 什么是死锁?死锁产生的条件?</h2><p><strong>什么是死锁</strong>:</p><p>在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的一种状态。</p><p><strong>死锁产生的四个必要条件</strong>:(有一个条件不成立,则不会产生死锁)</p><ul><li><p>互斥条件:一个资源一次只能被一个进程使用</p></li><li><p>请求与保持条件:一个进程因请求资源而阻塞时,对已获得资源保持不放</p></li><li><p>不剥夺条件:进程获得的资源,在未完全使用完之前,不能强行剥夺</p></li><li><p>循环等待条件:若干进程之间形成一种头尾相接的环形等待资源关系</p></li></ul><p><strong>如何处理死锁问题</strong>:</p><ul><li><p><strong>忽略该问题</strong>。例如鸵鸟算法,该算法可以应用在极少发生死锁的的情况下。为什么叫鸵鸟算法呢,因为传说中鸵鸟看到危险就把头埋在地底下,可能鸵鸟觉得看不到危险也就没危险了吧。跟掩耳盗铃有点像。</p></li><li><p><strong>检测死锁并且恢复。</strong></p></li><li><p>仔细地对资源进行动态分配,以<strong>避免死锁</strong>。</p></li><li><p><strong>通过破除死锁四个必要条件之一,来防止死锁产生。</strong></p></li></ul><h2 id="12-进程调度策略有哪几种"><a class="markdownIt-Anchor" href="#12-进程调度策略有哪几种"></a> 12. 进程调度策略有哪几种?</h2><ul><li><p><strong>先来先服务</strong>:非抢占式的调度算法,按照请求的顺序进行调度。有利于长作业,但不利于短作业,因为短作业必须一直等待前面的长作业执行完毕才能执行,而长作业又需要执行很长时间,造成了短作业等待时间过长。另外,对<code>I/O</code>密集型进程也不利,因为这种进程每次进行<code>I/O</code>操作之后又得重新排队。</p></li><li><p><strong>短作业优先</strong>:非抢占式的调度算法,按估计运行时间最短的顺序进行调度。长作业有可能会饿死,处于一直等待短作业执行完毕的状态。因为如果一直有短作业到来,那么长作业永远得不到调度。</p></li><li><p><strong>最短剩余时间优先</strong>:最短作业优先的抢占式版本,按剩余运行时间的顺序进行调度。 当一个新的作业到达时,其整个运行时间与当前进程的剩余时间作比较。如果新的进程需要的时间更少,则挂起当前进程,运行新的进程。否则新的进程等待。</p></li><li><p><strong>时间片轮转</strong>:将所有就绪进程按 <code>FCFS</code> 的原则排成一个队列,每次调度时,把 <code>CPU</code> 时间分配给队首进程,该进程可以执行一个时间片。当时间片用完时,由计时器发出时钟中断,调度程序便停止该进程的执行,并将它送往就绪队列的末尾,同时继续把 <code>CPU</code> 时间分配给队首的进程。</p><p>时间片轮转算法的效率和时间片的大小有很大关系:因为进程切换都要保存进程的信息并且载入新进程的信息,如果时间片太小,会导致进程切换得太频繁,在进程切换上就会花过多时间。 而如果时间片过长,那么实时性就不能得到保证。</p></li><li><p><strong>优先级调度</strong>:为每个进程分配一个优先级,按优先级进行调度。为了防止低优先级的进程永远等不到调度,可以随着时间的推移增加等待进程的优先级。</p></li></ul><h2 id="13-进程有哪些状态"><a class="markdownIt-Anchor" href="#13-进程有哪些状态"></a> 13. 进程有哪些状态?</h2><p>进程一共有<code>5</code>种状态,分别是创建、就绪、运行(执行)、终止、阻塞。</p><p><img src="https://pic4.zhimg.com/80/v2-b3bbb0af6ff6e3e01bda67b3d09537cb_1440w.webp" alt="img"></p><ul><li><p>运行状态就是进程正在<code>CPU</code>上运行。在单处理机环境下,每一时刻最多只有一个进程处于运行状态。</p></li><li><p>就绪状态就是说进程已处于准备运行的状态,即进程获得了除<code>CPU</code>之外的一切所需资源,一旦得到<code>CPU</code>即可运行。</p></li><li><p>阻塞状态就是进程正在等待某一事件而暂停运行,比如等待某资源为可用或等待<code>I/O</code>完成。即使<code>CPU</code>空闲,该进程也不能运行。</p></li></ul><p><strong>运行态→阻塞态</strong>:往往是由于等待外设,等待主存等资源分配或等待人工干预而引起的。<br><strong>阻塞态→就绪态</strong>:则是等待的条件已满足,只需分配到处理器后就能运行。<br><strong>运行态→就绪态</strong>:不是由于自身原因,而是由外界原因使运行状态的进程让出处理器,这时候就变成就绪态。例如时间片用完,或有更高优先级的进程来抢占处理器等。<br><strong>就绪态→运行态</strong>:系统按某种策略选中就绪队列中的一个进程占用处理器,此时就变成了运行态。</p><h2 id="14-什么是分页"><a class="markdownIt-Anchor" href="#14-什么是分页"></a> 14. 什么是分页?</h2><p>把内存空间划分为<strong>大小相等且固定的块</strong>,作为主存的基本单位。因为程序数据存储在不同的页面中,而页面又离散的分布在内存中,<strong>因此需要一个页表来记录映射关系,以实现从页号到物理块号的映射。</strong></p><p>访问分页系统中内存数据需要<strong>两次的内存访问</strong> (一次是从内存中访问页表,从中找到指定的物理块号,加上页内偏移得到实际物理地址;第二次就是根据第一次得到的物理地址访问内存取出数据)。</p><p><img src="https://pic3.zhimg.com/80/v2-77e2da59cc403feb97881b397e4b611e_1440w.webp" alt="img"></p><h2 id="15-什么是分段"><a class="markdownIt-Anchor" href="#15-什么是分段"></a> 15. 什么是分段?</h2><p><strong>分页是为了提高内存利用率,而分段是为了满足程序员在编写代码的时候的一些逻辑需求(比如数据共享,数据保护,动态链接等)。</strong></p><p>分段内存管理当中,<strong>地址是二维的,一维是段号,二维是段内地址;其中每个段的长度是不一样的,而且每个段内部都是从0开始编址的</strong>。由于分段管理中,每个段内部是连续内存分配,但是段和段之间是离散分配的,因此也存在一个逻辑地址到物理地址的映射关系,相应的就是段表机制。</p><p><img src="https://pic1.zhimg.com/80/v2-ca9c4e730792fdb3d1489c68f03ecd80_1440w.webp" alt="img"></p><h2 id="16-分页和分段有什区别"><a class="markdownIt-Anchor" href="#16-分页和分段有什区别"></a> 16. 分页和分段有什区别?</h2><ul><li><p>分页对程序员是透明的,但是分段需要程序员显式划分每个段。</p></li><li><p>分页的地址空间是一维地址空间,分段是二维的。</p></li><li><p>页的大小不可变,段的大小可以动态改变。</p></li><li><p>分页主要用于实现虚拟内存,从而获得更大的地址空间;分段主要是为了使程序和数据可以被划分为逻辑上独立的地址空间并且有助于共享和保护。</p></li></ul><h2 id="17-什么是交换空间"><a class="markdownIt-Anchor" href="#17-什么是交换空间"></a> 17. 什么是交换空间?</h2><p>操作系统把物理内存(physical RAM)分成一块一块的小内存,每一块内存被称为<strong>页(page)</strong>。当内存资源不足时,<strong>Linux把某些页的内容转移至硬盘上的一块空间上,以释放内存空间</strong>。硬盘上的那块空间叫做<strong>交换空间</strong>(swap space),而这一过程被称为交换(swapping)。<strong>物理内存和交换空间的总容量就是虚拟内存的可用容量。</strong></p><p>用途:</p><ul><li><p>物理内存不足时一些不常用的页可以被交换出去,腾给系统。</p></li><li><p>程序启动时很多内存页被用来初始化,之后便不再需要,可以交换出去。</p></li></ul><h2 id="16-页面替换算法有哪些"><a class="markdownIt-Anchor" href="#16-页面替换算法有哪些"></a> 16. 页面替换算法有哪些?</h2><p>在程序运行过程中,如果要访问的页面不在内存中,就发生缺页中断从而将该页调入内存中。此时如果内存已无空闲空间,系统必须从内存中调出一个页面到磁盘对换区中来腾出空间。</p><p>包括以下算法:</p><ul><li><p><strong>最佳算法</strong>:所选择的被换出的页面将是最长时间内不再被访问,通常可以保证获得最低的缺页率。这是一种理论上的算法,因为无法知道一个页面多长时间不再被访问。</p></li><li><p><strong>先进先出</strong>:选择换出的页面是最先进入的页面。该算法将那些经常被访问的页面也被换出,从而使缺页率升高。</p></li><li><p><strong>LRU</strong>:虽然无法知道将来要使用的页面情况,但是可以知道过去使用页面的情况。<code>LRU</code> 将最近最久未使用的页面换出。为了实现 LRU,需要在内存中维护一个所有页面的链表。当一个页面被访问时,将这个页面移到链表表头。这样就能保证链表表尾的页面是最近最久未访问的。因为每次访问都需要更新链表,因此这种方式实现的 <code>LRU</code> 代价很高。</p></li><li><p><strong>时钟算法</strong>:时钟算法使用环形链表将页面连接起来,再使用一个指针指向最老的页面。它将整个环形链表的每一个页面做一个标记,如果标记是<code>0</code>,那么暂时就不会被替换,然后时钟算法遍历整个环,遇到标记为<code>1</code>的就替换,否则将标记为<code>0</code>的标记为<code>1</code>。</p></li></ul><h2 id="18-什么是缓冲区溢出有什么危害"><a class="markdownIt-Anchor" href="#18-什么是缓冲区溢出有什么危害"></a> 18. 什么是缓冲区溢出?有什么危害?</h2><p>缓冲区溢出是指当计算机向缓冲区填充数据时超出了缓冲区本身的容量,溢出的数据覆盖在合法数据上。</p><p>危害有以下两点:</p><ul><li><p>程序崩溃,导致拒绝额服务</p></li><li><p>跳转并且执行一段恶意代码</p></li></ul><p>造成缓冲区溢出的主要原因是程序中没有仔细检查用户输入。</p><h2 id="19-什么是虚拟内存"><a class="markdownIt-Anchor" href="#19-什么是虚拟内存"></a> 19. 什么是虚拟内存?</h2><p>虚拟内存就是说,让物理内存扩充成更大的逻辑内存,从而让程序获得更多的可用内存。虚拟内存使用部分加载的技术,让一个进程或者资源的某些页面加载进内存,从而能够加载更多的进程,甚至能加载比内存大的进程,这样看起来好像内存变大了,这部分内存其实包含了磁盘或者硬盘,并且就叫做虚拟内存。</p><h2 id="20-讲一讲io多路复用"><a class="markdownIt-Anchor" href="#20-讲一讲io多路复用"></a> 20. 讲一讲IO多路复用?</h2><p><strong>IO多路复用是指内核一旦发现进程指定的一个或者多个IO条件准备读取,它就通知该进程。IO多路复用适用如下场合</strong>:</p><ul><li><p>当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用。</p></li><li><p>当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现。</p></li><li><p>如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用。</p></li><li><p>如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用。</p></li><li><p>如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。</p></li><li><p>与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。</p></li></ul><h2 id="21-硬链接和软链接有什么区别"><a class="markdownIt-Anchor" href="#21-硬链接和软链接有什么区别"></a> 21. 硬链接和软链接有什么区别?</h2><ul><li><p>硬链接就是在目录下创建一个条目,记录着文件名与 <code>inode</code> 编号,这个 <code>inode</code> 就是源文件的 <code>inode</code>。删除任意一个条目,文件还是存在,只要引用数量不为 <code>0</code>。但是硬链接有限制,它不能跨越文件系统,也不能对目录进行链接。</p></li><li><p>符号链接文件保存着源文件所在的绝对路径,在读取时会定位到源文件上,可以理解为 <code>Windows</code> 的快捷方式。当源文件被删除了,链接文件就打不开了。因为记录的是路径,所以可以为目录建立符号链接。</p></li></ul><h2 id="22-中断的处理过程"><a class="markdownIt-Anchor" href="#22-中断的处理过程"></a> 22. 中断的处理过程?</h2><ol><li><p>保护现场:将当前执行程序的相关数据保存在寄存器中,然后入栈。</p></li><li><p>开中断:以便执行中断时能响应较高级别的中断请求。</p></li><li><p>中断处理</p></li><li><p>关中断:保证恢复现场时不被新中断打扰</p></li><li><p>恢复现场:从堆栈中按序取出程序数据,恢复中断前的执行状态。</p></li></ol><h2 id="23-中断和轮询有什么区别"><a class="markdownIt-Anchor" href="#23-中断和轮询有什么区别"></a> 23. 中断和轮询有什么区别?</h2><ul><li><p>轮询:CPU对<strong>特定设备</strong>轮流询问。中断:通过<strong>特定事件</strong>提醒CPU。</p></li><li><p>轮询:效率低等待时间长,CPU利用率不高。中断:容易遗漏问题,CPU利用率不高。</p></li></ul>]]></content>
<summary type="html"><p><a href="https://cloud.tencent.com/developer/article/1815965">这 50 道操作系统面试题,真牛批! - 腾讯云开发者社区-腾讯云 (tencent.com)</a></p>
<h2 id="1-进程和线程的区别"</summary>
<category term="面试题" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="操作系统" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="面试题" scheme="http://example.com/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="操作系统" scheme="http://example.com/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title></title>
<link href="http://example.com/2023/03/27/SpringCloud%E5%BE%AE%E6%9C%8D%E5%8A%A1/Sentinel/QPS/"/>
<id>http://example.com/2023/03/27/SpringCloud%E5%BE%AE%E6%9C%8D%E5%8A%A1/Sentinel/QPS/</id>
<published>2023-03-27T13:13:31.090Z</published>
<updated>2023-03-27T13:31:01.516Z</updated>
<content type="html"><![CDATA[<h1 id="性能测试-峰值qps和计算公式"><a class="markdownIt-Anchor" href="#性能测试-峰值qps和计算公式"></a> 性能测试-峰值QPS和计算公式</h1><p><strong>峰值QPS和计算公式</strong><br><strong>概述</strong><br>因特网上,经常用每秒查询率来衡量域名系统服务器的机器的性能,其即为QPS。 对应fetches/sec,即每秒的响应请求数,也即是最大吞吐能力。</p><p>计算关系: QPS = 并发量 / 平均响应时间 并发量 = QPS * 平均响应时间</p><p>通常QPS用来表达和衡量当前系统的负载,也可以用RPS来表示, 我们形容当前系统的运行状态时可以说当前QPS已经达到多少多少了, 在系统环境不变的情况下存在支持的最大QPS,但并不应该用来形容机器的性能。 可以通过提高TPS来提升当前系统的处理能力,来增加最大QPS的支持。 TPS用来形容机器的性能。</p><p><strong>QPS计算原理</strong><br>QPS = req/sec = 请求数/秒</p><p>原理:每天80%的访问集中在20%的时间里,这20%时间叫做峰值时间<br>公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数>(QPS)<br>机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器<br><strong>实例</strong><br>问:每天300w PV 的在单台机器上,这台机器需要多少QPS?</p><p>答:( 3000000 * 0.8 ) / (86400 * 0.2 ) = 139 (QPS)</p><p>问:如果一台机器的QPS是58,需要几台机器来支持?</p>]]></content>
<summary type="html"><h1 id="性能测试-峰值qps和计算公式"><a class="markdownIt-Anchor" href="#性能测试-峰值qps和计算公式"></a> 性能测试-峰值QPS和计算公式</h1>
<p><strong>峰值QPS和计算公式</strong><br>
<</summary>
</entry>
<entry>
<title>进程和线程的区别</title>
<link href="http://example.com/2023/03/27/interview/%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB/"/>
<id>http://example.com/2023/03/27/interview/%E8%BF%9B%E7%A8%8B%E5%92%8C%E7%BA%BF%E7%A8%8B%E7%9A%84%E5%8C%BA%E5%88%AB/</id>
<published>2023-03-27T10:14:46.027Z</published>
<updated>2023-06-05T13:03:54.094Z</updated>
<content type="html"><![CDATA[<h1 id="进程和线程"><a class="markdownIt-Anchor" href="#进程和线程"></a> 进程和线程</h1><h2 id="进程"><a class="markdownIt-Anchor" href="#进程"></a> 进程</h2><p>一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。</p><p><img src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL0pvdXJXb24vaW1hZ2UvbWFzdGVyL0phdmElRTUlQjklQjYlRTUlOEYlOTElRTclQkMlOTYlRTclQTglOEItJUU1JTlGJUJBJUU3JUExJTgwJUU3JTlGJUE1JUU4JUFGJTg2LyVFNCVCQiVCQiVFNSU4QSVBMSVFNyVBRSVBMSVFNyU5MCU4NiVFNSU5OSVBOC5wbmc?x-oss-process=image/format,png" alt="任务管理器"></p><h2 id="线程"><a class="markdownIt-Anchor" href="#线程"></a> 线程</h2><p>进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。</p><p>与进程不同的是同类的多个线程共享进程的堆和方法区资源,但每个线程有自己的程序计数器、虚拟机栈和本地方法栈,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多,也正因为如此,线程也被称为轻量级进程。</p><p>Java 程序天生就是多线程程序,我们可以通过 JMX 来看一下一个普通的 Java 程序有哪些线程,代码如下。</p><figure class="highlight java"><table><tbody><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="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MultiThread</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="comment">// 获取 Java 线程管理 MXBean</span></span><br><span class="line"><span class="type">ThreadMXBean</span> <span class="variable">threadMXBean</span> <span class="operator">=</span> ManagementFactory.getThreadMXBean();</span><br><span class="line"><span class="comment">// 不需要获取同步的 monitor 和 synchronizer 信息,仅获取线程和线程堆栈信息</span></span><br><span class="line">ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(<span class="literal">false</span>, <span class="literal">false</span>);</span><br><span class="line"><span class="comment">// 遍历线程信息,仅打印线程 ID 和线程名称信息</span></span><br><span class="line"><span class="keyword">for</span> (ThreadInfo threadInfo : threadInfos) {</span><br><span class="line">System.out.println(<span class="string">"["</span> + threadInfo.getThreadId() + <span class="string">"] "</span> + threadInfo.getThreadName());</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>上述程序输出如下(输出内容可能不同,不用太纠结下面每个线程的作用,只用知道 main 线程执行 main 方法即可):</p><figure class="highlight plaintext"><table><tbody><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">[6] Monitor Ctrl-Break //监听线程转储或“线程堆栈跟踪”的线程</span><br><span class="line">[5] Attach Listener //负责接收到外部的命令,而对该命令进行执行的并且把结果返回给发送者</span><br><span class="line">[4] Signal Dispatcher // 分发处理给 JVM 信号的线程</span><br><span class="line">[3] Finalizer //在垃圾收集前,调用对象 finalize 方法的线程</span><br><span class="line">[2] Reference Handler //用于处理引用对象本身(软引用、弱引用、虚引用)的垃圾回收的线程</span><br><span class="line">[1] main //main 线程,程序入口</span><br></pre></td></tr></tbody></table></figure><p>从上面的输出内容可以看出:一个 Java 程序的运行是 main 线程和多个其他线程同时运行。</p><h2 id="进程与线程的区别总结"><a class="markdownIt-Anchor" href="#进程与线程的区别总结"></a> 进程与线程的区别总结</h2><p>线程具有许多传统进程所具有的特征,故又称为轻型进程(Light—Weight Process)或进程元;而把传统的进程称为重型进程(Heavy—Weight Process),它相当于只有一个线程的任务。在引入了线程的操作系统中,通常一个进程都有若干个线程,至少包含一个线程。</p><ul><li><p>**根本区别:**进程是操作系统资源分配的基本单位,而线程是处理器任务调度和执行的基本单位</p></li><li><p><strong>资源开销</strong>:每个进程都有独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销;线程可以看做轻量级的进程,同一类线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。</p></li><li><p>**包含关系:**如果一个进程内有多个线程,则执行过程不是一条线的,而是多条线(线程)共同完成的;线程是进程的一部分,所以线程也被称为轻权进程或者轻量级进程。</p></li><li><p>**内存分配:**同一进程的线程共享本进程的地址空间和资源,而进程之间的地址空间和资源是相互独立的</p></li><li><p>**影响关系:**一个进程崩溃后,在保护模式下不会对其他进程产生影响,但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。</p></li><li><p>**执行过程:**每个独立的进程有程序运行的入口、顺序执行序列和程序出口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制,两者均可并发执行</p></li></ul><h2 id="从-jvm-角度说进程和线程之间的关系重要"><a class="markdownIt-Anchor" href="#从-jvm-角度说进程和线程之间的关系重要"></a> 从 JVM 角度说进程和线程之间的关系(重要)</h2><h3 id="图解进程和线程的关系"><a class="markdownIt-Anchor" href="#图解进程和线程的关系"></a> 图解进程和线程的关系</h3><p>下图是 Java 内存区域,通过下图我们从 JVM 的角度来说一下线程和进程之间的关系。</p><p><img src="https://img-blog.csdnimg.cn/20191105205545651.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly90aGlua3dvbi5ibG9nLmNzZG4ubmV0,size_16,color_FFFFFF,t_70" alt="在这里插入图片描述"></p><p>从上图可以看出:一个进程中可以有多个线程,多个线程共享进程的堆和方法区 (JDK1.8 之后的元空间)资源,但是每个线程有自己的程序计数器、虚拟机栈 和 本地方法栈。</p><h2 id="程序计数器为什么是私有的"><a class="markdownIt-Anchor" href="#程序计数器为什么是私有的"></a> 程序计数器为什么是私有的?</h2><p>程序计数器主要有下面两个作用:</p><ol><li>字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制,如:顺序执行、选择、循环、异常处理。</li><li>在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。</li></ol><p>需要注意的是,如果执行的是 native 方法,那么程序计数器记录的是 undefined 地址,只有执行的是 Java 代码时程序计数器记录的才是下一条指令的地址。</p><p>所以,程序计数器私有主要是为了线程切换后能恢复到正确的执行位置。</p><h3 id="虚拟机栈和本地方法栈为什么是私有的"><a class="markdownIt-Anchor" href="#虚拟机栈和本地方法栈为什么是私有的"></a> 虚拟机栈和本地方法栈为什么是私有的?</h3><ul><li>**虚拟机栈:**每个 Java 方法在执行的同时会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。</li><li>**本地方法栈:**和虚拟机栈所发挥的作用非常相似,区别是: 虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。<br>所以,为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。</li></ul><h2 id="一句话简单了解堆和方法区"><a class="markdownIt-Anchor" href="#一句话简单了解堆和方法区"></a> 一句话简单了解堆和方法区</h2><p>堆和方法区是所有线程共享的资源,其中堆是进程中最大的一块内存,主要用于存放新创建的对象 (所有对象都在这里分配内存),方法区主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。</p><h2 id="多进程和多线程区别"><a class="markdownIt-Anchor" href="#多进程和多线程区别"></a> 多进程和多线程区别</h2><p>多进程:操作系统中同时运行的多个程序</p><p>多线程:在同一个进程中同时运行的多个任务</p><p>举个例子,多线程下载软件,可以同时运行多个线程,但是通过程序运行的结果发现,每一次结果都不一致。 因为多线程存在一个特性:随机性。造成的原因:CPU在瞬间不断切换去处理各个线程而导致的,可以理解成多个线程在抢CPU资源。</p><p>多线程提高CPU使用率</p><p><img src="https://imgconvert.csdnimg.cn/aHR0cHM6Ly9yYXcuZ2l0aHVidXNlcmNvbnRlbnQuY29tL0pvdXJXb24vaW1hZ2UvbWFzdGVyL0phdmElRTUlQjklQjYlRTUlOEYlOTElRTclQkMlOTYlRTclQTglOEItJUU1JTlGJUJBJUU3JUExJTgwJUU3JTlGJUE1JUU4JUFGJTg2LyVFNSVBNCU5QSVFNyVCQSVCRiVFNyVBOCU4Qi5wbmc?x-oss-process=image/format,png" alt="多线程"></p><p>多线程并不能提高运行速度,但可以提高运行效率,让CPU的使用率更高。但是如果多线程有安全问题或出现频繁的上下文切换时,运算速度可能反而更低。</p><h2 id="java中的多线程"><a class="markdownIt-Anchor" href="#java中的多线程"></a> Java中的多线程</h2><p>Java程序的进程里有几个线程:主线程,垃圾回收线程(后台线程)等</p><p>在 Java 中,当我们启动 main 函数时其实就是启动了一个 JVM 的进程,而 main 函数所在的线程就是这个进程中的一个线程,也称主线程。</p><p>Java支持多线程,当Java程序执行main方法的时候,就是在执行一个名字叫做main的线程,可以在main方法执行时,开启多个线程A,B,C,多个线程 main,A,B,C同时执行,相互抢夺CPU,Thread类是java.lang包下的一个常用类,每一个Thread类的对象,就代表一个处于某种状态的线程</p>]]></content>
<summary type="html"><h1 id="进程和线程"><a class="markdownIt-Anchor" href="#进程和线程"></a> 进程和线程</h1>
<h2 id="进程"><a class="markdownIt-Anchor" href="#进程"></a> 进程</h2>
<</summary>
<category term="面试题" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="面试题" scheme="http://example.com/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
</entry>
<entry>
<title>多态</title>
<link href="http://example.com/2023/03/19/interview/java%E5%9F%BA%E7%A1%80/%E5%A4%9A%E6%80%81/"/>
<id>http://example.com/2023/03/19/interview/java%E5%9F%BA%E7%A1%80/%E5%A4%9A%E6%80%81/</id>
<published>2023-03-19T03:45:19.800Z</published>
<updated>2023-03-19T10:56:48.321Z</updated>
<content type="html"><![CDATA[<h2 id="多态的概述"><a class="markdownIt-Anchor" href="#多态的概述"></a> 多态的概述</h2><p><img src="../../../images/image-20230319114544383.png" alt="image-20230319114544383"></p><h2 id="多态的格式与使用"><a class="markdownIt-Anchor" href="#多态的格式与使用"></a> 多态的格式与使用</h2><p><img src="../../../images/image-20230319115224624.png" alt="image-20230319115224624"></p><p><img src="../../../images/image-20230319114924748.png" alt="image-20230319114924748"></p><p><img src="../../../images/image-20230319115144427.png" alt="image-20230319115144427"></p><h2 id="多态中的成员变量的使用特点其实没有任何变化"><a class="markdownIt-Anchor" href="#多态中的成员变量的使用特点其实没有任何变化"></a> 多态中的成员变量的使用特点(其实没有任何变化)</h2><p>访问成员变量的两种方式:</p><ul><li>直接通过对象名称访问成员变量:看等号左边是谁,优先用谁,没有则向上找</li><li>间接通过成员方法访问成员变量:看该方法属于谁,优先用谁,没有则向上找</li></ul><p>多态中,成员变量不可以发生覆盖重写(比如,可以在子类的成员变量上添加@Override,会发现报错),只有方法可以发生覆盖重写</p><p><img src="../../../images/image-20230319115914276.png" alt="image-20230319115914276"></p><p><img src="../../../images/image-20230319115854891.png" alt="image-20230319115854891"></p><p><img src="../../../images/image-20230319115831384.png" alt="image-20230319115831384"></p><h2 id="多态中的成员方法的使用特点"><a class="markdownIt-Anchor" href="#多态中的成员方法的使用特点"></a> 多态中的成员方法的使用特点</h2><p>在多态的代码中,成员方法的访问规则:</p><ul><li>看 new 的是谁,就优先用谁,没有则向上找</li></ul><p>口诀:编译看左边,运行看右边</p><p>对比:</p><ul><li>成员变量:编译看左边,运行还看左边</li><li>成员方法:编译看左边,运行看右边</li></ul><p><img src="../../../images/image-20230319120823517.png" alt="image-20230319120823517"></p><h2 id="多态的好处"><a class="markdownIt-Anchor" href="#多态的好处"></a> 多态的好处</h2><p><img src="../../../images/image-20230319181831562.png" alt="image-20230319181831562"></p><h2 id="对象的向上转型"><a class="markdownIt-Anchor" href="#对象的向上转型"></a> 对象的向上转型</h2><p><img src="../../../images/image-20230319182510068.png" alt="image-20230319182510068"></p><h2 id="对象的向下转型"><a class="markdownIt-Anchor" href="#对象的向下转型"></a> 对象的向下转型</h2><p><img src="../../../images/image-20230319183246604.png" alt="image-20230319183246604"></p><p><img src="../../../images/image-20230319184116304.png" alt="image-20230319184116304"></p><h3 id="instanceof"><a class="markdownIt-Anchor" href="#instanceof"></a> instanceof</h3><p>如何知道一个父类引用的对象本来是哪个子类,怎么知道向哪个子类进行向下转型呢?</p><p>方案:用 instanceof</p><p>格式: <strong>对象 instanceof 类名称</strong> 这会得到一个boolean值的结果,也就是判断前面的对象能不能当作后面类型的实例</p><p><img src="../../../images/image-20230319185037444.png" alt="image-20230319185037444"></p><p>向下转型时,在未知原本类型时,一定要用 instanceof,否则容易出异常</p>]]></content>
<summary type="html"><h2 id="多态的概述"><a class="markdownIt-Anchor" href="#多态的概述"></a> 多态的概述</h2>
<p><img src="../../../images/image-20230319114544383.png" alt="ima</summary>
<category term="java基础" scheme="http://example.com/categories/java%E5%9F%BA%E7%A1%80/"/>
<category term="java基础" scheme="http://example.com/tags/java%E5%9F%BA%E7%A1%80/"/>
</entry>
<entry>
<title>Spring摘要记录</title>
<link href="http://example.com/2023/03/16/interview/Spring%E9%9D%A2%E8%AF%95/Spring/"/>
<id>http://example.com/2023/03/16/interview/Spring%E9%9D%A2%E8%AF%95/Spring/</id>
<published>2023-03-16T14:30:14.984Z</published>
<updated>2023-07-22T14:54:05.995Z</updated>
<content type="html"><![CDATA[<h1 id="spring中controller单例"><a class="markdownIt-Anchor" href="#spring中controller单例"></a> Spring中Controller单例</h1><ul><li>controller默认是单例的,不要使用非静态的成员变量,否则会发生数据逻辑混乱。 正因为单例所以不是线程安全的。</li></ul><p>我们下面来简单的验证下:</p><figure class="highlight java"><table><tbody><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">package</span> com.riemann.springbootdemo.controller;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Scope;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Controller;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ScopeTestController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">num</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@RequestMapping("/testScope")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testScope</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(++num);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@RequestMapping("/testScope2")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testScope2</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(++num);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><ul><li>我们首先访问 <a href="http://localhost:8080/testScope%EF%BC%8C%E5%BE%97%E5%88%B0%E7%9A%84%E7%AD%94%E6%A1%88%E6%98%AF1%EF%BC%9B">http://localhost:8080/testScope,得到的答案是1;</a></li><li>然后我们再访问<a href="http://localhost:8080/testScope2%EF%BC%8C%E5%BE%97%E5%88%B0%E7%9A%84%E7%AD%94%E6%A1%88%E6%98%AF">http://localhost:8080/testScope2,得到的答案是</a> 2。</li></ul><p><strong>得到的不同的值,这是线程不安全的。</strong></p><p>接下来我们再来给controller增加作用多例 <mark>@Scope(“prototype”)</mark></p><figure class="highlight java"><table><tbody><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">package</span> com.riemann.springbootdemo.controller;</span><br><span class="line"></span><br><span class="line"><span class="keyword">import</span> org.springframework.context.annotation.Scope;</span><br><span class="line"><span class="keyword">import</span> org.springframework.stereotype.Controller;</span><br><span class="line"><span class="keyword">import</span> org.springframework.web.bind.annotation.RequestMapping;</span><br><span class="line"></span><br><span class="line"><span class="meta">@Controller</span></span><br><span class="line"><span class="meta">@Scope("prototype")</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ScopeTestController</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="type">int</span> <span class="variable">num</span> <span class="operator">=</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@RequestMapping("/testScope")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testScope</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(++num);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@RequestMapping("/testScope2")</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">testScope2</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(++num);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><ul><li>我们依旧首先访问 <a href="http://localhost:8080/testScope%EF%BC%8C%E5%BE%97%E5%88%B0%E7%9A%84%E7%AD%94%E6%A1%88%E6%98%AF1%EF%BC%9B">http://localhost:8080/testScope,得到的答案是1;</a></li><li>然后我们再访问 <a href="http://localhost:8080/testScope2%EF%BC%8C%E5%BE%97%E5%88%B0%E7%9A%84%E7%AD%94%E6%A1%88%E8%BF%98%E6%98%AF">http://localhost:8080/testScope2,得到的答案还是</a> 1。</li></ul><p>相信大家不难发现 :</p><p><a href="https://so.csdn.net/so/search?q=%E5%8D%95%E4%BE%8B&spm=1001.2101.3001.7020">单例</a>是不安全的,会导致属性重复使用。</p><p>解决方案</p><ol><li>不要在controller中定义成员变量。</li><li>万一必须要定义一个非静态成员变量时候,则通过注解@Scope(“prototype”),将其设置为多例模式。</li><li>在Controller中使用ThreadLocal变量</li></ol><p><font color="red">Spring MVC默认是单例模式,Controller、Service、Dao都是单例</font>所以在使用不当存在一定的安全隐患。Controller单例模式的好处在与:</p><ul><li>提高性能,不用每次创建Controller实例,减少了对象创建和垃圾收集的时间</li><li>没多例的必要,由于只有一个Controller的实例,当多个线程同时调用它的时候,它的成员变量就不是线程安全的。<br>当然在大多数情况下,<strong>我们根本不需要Controller考虑线程安全的问题,除非在类中声明了<mark>成员变量</mark></strong>。因此Spring MVC的Controller在编码时,尽量避免使用实例变量。如果一定要使用实例变量,则可以改用以下方式:<br>Controller中声明 scope=”prototype”,即设置为多例模式<br>在Controller中使用ThreadLocal变量,如:private ThreadLocal count = new ThreadLocal();</li></ul><h1 id="springmvc-singleton有几种解决方法"><a class="markdownIt-Anchor" href="#springmvc-singleton有几种解决方法"></a> springmvc singleton有几种解决方法:</h1><p>1、在控制器中不使用实例变量(可以使用方法参数的形式解决,参考博文 Spring Bean Scope 有状态的Bean 无状态的Bean)<br>2、将控制器的作用域从单例改为原型,即在spring配置文件Controller中声明 <font color="blue"><strong>scope=“prototype”</strong></font>,每次都创建新的controller<br>3、在Controller中使用<font color="blue"><strong>ThreadLocal</strong></font>变量</p>]]></content>
<summary type="html"><h1 id="spring中controller单例"><a class="markdownIt-Anchor" href="#spring中controller单例"></a> Spring中Controller单例</h1>
<ul>
<li>controller默认是单例</summary>
<category term="面试题" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="Spring" scheme="http://example.com/categories/%E9%9D%A2%E8%AF%95%E9%A2%98/Spring/"/>
<category term="面试题" scheme="http://example.com/tags/%E9%9D%A2%E8%AF%95%E9%A2%98/"/>
<category term="Spring" scheme="http://example.com/tags/Spring/"/>
</entry>
<entry>
<title>ThreadLocal使用与原理</title>
<link href="http://example.com/2023/03/16/juc/ThreadLocal%E4%BD%BF%E7%94%A8%E4%B8%8E%E5%8E%9F%E7%90%86/"/>
<id>http://example.com/2023/03/16/juc/ThreadLocal%E4%BD%BF%E7%94%A8%E4%B8%8E%E5%8E%9F%E7%90%86/</id>
<published>2023-03-16T14:14:34.460Z</published>
<updated>2023-03-16T14:22:11.642Z</updated>
<content type="html"><![CDATA[<p>在处理多线程并发安全的方法中,最常用的方法,就是使用锁,通过锁来控制多个不同线程对临界区的访问。</p><p>但是,无论是什么样的锁,乐观锁或者悲观锁,都会在并发冲突的时候对性能产生一定的影响。</p><p>那有没有一种方法,可以彻底避免竞争呢?</p><p>答案是肯定的,这就是ThreadLocal。</p><p>从字面意思上看,ThreadLocal可以解释成线程的局部变量,也就是说一个ThreadLocal的变量只有当前自身线程可以访问,别的线程都访问不了,那么自然就避免了线程竞争。</p><p>因此,ThreadLocal提供了一种与众不同的线程安全方式,它不是在发生线程冲突时想办法解决冲突,而是彻底的<mark>避免了冲突的发生</mark>。</p><h2 id="threadlocal的基本使用"><a class="markdownIt-Anchor" href="#threadlocal的基本使用"></a> ThreadLocal的基本使用</h2><p>创建一个ThreadLocal对象:</p><figure class="highlight java"><table><tbody><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> ThreadLocal<Integer> localInt = <span class="keyword">new</span> <span class="title class_">ThreadLocal</span><>();</span><br></pre></td></tr></tbody></table></figure><p>上述代码创建一个localInt变量,由于ThreadLocal是一个泛型类,这里指定了localInt的类型为整数。</p><p>下面展示了如果设置和获取这个变量的值:</p><figure class="highlight java"><table><tbody><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="type">int</span> <span class="title function_">setAndGet</span><span class="params">()</span>{</span><br><span class="line"> localInt.set(<span class="number">8</span>);</span><br><span class="line"> <span class="keyword">return</span> localInt.get();</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>上述代码设置变量的值为8,接着取得这个值。</p><p>由于ThreadLocal里设置的值,只有当前线程自己看得见,这意味着你不可能通过其他线程为它初始化值。为了弥补这一点,ThreadLocal提供了一个withInitial()方法统一初始化所有线程的ThreadLocal的值:</p><figure class="highlight java"><table><tbody><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> ThreadLocal<Integer> localInt = ThreadLocal.withInitial(() -> <span class="number">6</span>);</span><br></pre></td></tr></tbody></table></figure><p>上述代码将ThreadLocal的初始值设置为6,这对全体线程都是可见的。</p><h2 id="threadlocal的实现原理"><a class="markdownIt-Anchor" href="#threadlocal的实现原理"></a> ThreadLocal的实现原理</h2><p>ThreadLocal变量只在单个线程内可见,那它是如何做到的呢?我们先从最基本的get()方法说起:</p><figure class="highlight java"><table><tbody><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> T <span class="title function_">get</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">//获得当前线程</span></span><br><span class="line"> <span class="type">Thread</span> <span class="variable">t</span> <span class="operator">=</span> Thread.currentThread();</span><br><span class="line"> <span class="comment">//每个线程 都有一个自己的ThreadLocalMap,</span></span><br><span class="line"> <span class="comment">//ThreadLocalMap里就保存着所有的ThreadLocal变量</span></span><br><span class="line"> <span class="type">ThreadLocalMap</span> <span class="variable">map</span> <span class="operator">=</span> getMap(t);</span><br><span class="line"> <span class="keyword">if</span> (map != <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">//ThreadLocalMap的key就是当前ThreadLocal对象实例,</span></span><br><span class="line"> <span class="comment">//多个ThreadLocal变量都是放在这个map中的</span></span><br><span class="line"> ThreadLocalMap.<span class="type">Entry</span> <span class="variable">e</span> <span class="operator">=</span> map.getEntry(<span class="built_in">this</span>);</span><br><span class="line"> <span class="keyword">if</span> (e != <span class="literal">null</span>) {</span><br><span class="line"> <span class="meta">@SuppressWarnings("unchecked")</span></span><br><span class="line"> <span class="comment">//从map里取出来的值就是我们需要的这个ThreadLocal变量</span></span><br><span class="line"> <span class="type">T</span> <span class="variable">result</span> <span class="operator">=</span> (T)e.value;</span><br><span class="line"> <span class="keyword">return</span> result;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">// 如果map没有初始化,那么在这里初始化一下</span></span><br><span class="line"> <span class="keyword">return</span> setInitialValue();</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>可以看到,所谓的ThreadLocal变量就是保存在每个线程的map中的。这个map就是Thread对象中的threadLocals字段。如下:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ThreadLocal.<span class="type">ThreadLocalMap</span> <span class="variable">threadLocals</span> <span class="operator">=</span> <span class="literal">null</span>;</span><br></pre></td></tr></tbody></table></figure><p>ThreadLocal.ThreadLocalMap是一个比较特殊的Map,它的每个Entry的key都是一个弱引用:</p><figure class="highlight java"><table><tbody><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">static</span> <span class="keyword">class</span> <span class="title class_">Entry</span> <span class="keyword">extends</span> <span class="title class_">WeakReference</span><ThreadLocal<?>> {</span><br><span class="line"> <span class="comment">/** The value associated with this ThreadLocal. */</span></span><br><span class="line"> Object value;</span><br><span class="line"> <span class="comment">//key就是一个弱引用</span></span><br><span class="line"> Entry(ThreadLocal<?> k, Object v) {</span><br><span class="line"> <span class="built_in">super</span>(k);</span><br><span class="line"> value = v;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>这样设计的好处是,如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value,依然是强引用,如何回收,见下文分解)。</p><h2 id="理解threadlocal中的内存泄漏问题"><a class="markdownIt-Anchor" href="#理解threadlocal中的内存泄漏问题"></a> 理解ThreadLocal中的内存泄漏问题</h2><p>虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候,就会自动被回收,但是Entry中的value依然是强引用。这个value的引用链条如下:</p><p><img src="https://img-blog.csdnimg.cn/img_convert/68348e17e2588eec1a2c2f03939c5e4b.png" alt="img"></p><p>可以看到,只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理:</p><p>以getEntry()为例:</p><figure class="highlight java"><table><tbody><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">private</span> Entry <span class="title function_">getEntry</span><span class="params">(ThreadLocal<?> key)</span> {</span><br><span class="line"> <span class="type">int</span> <span class="variable">i</span> <span class="operator">=</span> key.threadLocalHashCode & (table.length - <span class="number">1</span>);</span><br><span class="line"> <span class="type">Entry</span> <span class="variable">e</span> <span class="operator">=</span> table[i];</span><br><span class="line"> <span class="keyword">if</span> (e != <span class="literal">null</span> && e.get() == key)</span><br><span class="line"> <span class="comment">//如果找到key,直接返回</span></span><br><span class="line"> <span class="keyword">return</span> e;</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="comment">//如果找不到,就会尝试清理,如果你总是访问存在的key,那么这个清理永远不会进来</span></span><br><span class="line"> <span class="keyword">return</span> getEntryAfterMiss(key, i, e);</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>下面是getEntryAfterMiss()的实现:</p><figure class="highlight java"><table><tbody><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">private</span> Entry <span class="title function_">getEntryAfterMiss</span><span class="params">(ThreadLocal<?> key, <span class="type">int</span> i, Entry e)</span> {</span><br><span class="line"> Entry[] tab = table;</span><br><span class="line"> <span class="type">int</span> <span class="variable">len</span> <span class="operator">=</span> tab.length;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">while</span> (e != <span class="literal">null</span>) {</span><br><span class="line"> <span class="comment">// 整个e是entry ,也就是一个弱引用</span></span><br><span class="line"> ThreadLocal<?> k = e.get();</span><br><span class="line"> <span class="comment">//如果找到了,就返回</span></span><br><span class="line"> <span class="keyword">if</span> (k == key)</span><br><span class="line"> <span class="keyword">return</span> e;</span><br><span class="line"> <span class="keyword">if</span> (k == <span class="literal">null</span>)</span><br><span class="line"> <span class="comment">//如果key为null,说明弱引用已经被回收了</span></span><br><span class="line"> <span class="comment">//那么就要在这里回收里面的value了</span></span><br><span class="line"> expungeStaleEntry(i);</span><br><span class="line"> <span class="keyword">else</span></span><br><span class="line"> <span class="comment">//如果key不是要找的那个,那说明有hash冲突,这里是处理冲突,找下一个entry</span></span><br><span class="line"> i = nextIndex(i, len);</span><br><span class="line"> e = tab[i];</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></pre></td></tr></tbody></table></figure><p>真正用来回收value的是expungeStaleEntry()方法,在remove()和set()方法中,都会直接或者间接调用到这个方法进行value的清理:</p><p>从这里可以看到,ThreadLocal为了避免内存泄露,也算是花了一番大心思。不仅使用了弱引用维护key,还会在每个操作上检查key是否被回收,进而再回收value。</p><p>但是从中也可以看到,ThreadLocal并不能100%保证不发生内存泄漏。</p><p>比如,很不幸的,你的get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行,如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。</p><p>因此,一个良好的习惯依然是:当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的。</p><h2 id="threadlocalmap中的hash冲突处理"><a class="markdownIt-Anchor" href="#threadlocalmap中的hash冲突处理"></a> ThreadLocalMap中的Hash冲突处理</h2><p>ThreadLocalMap作为一个HashMap和java.util.HashMap的实现是不同的。对于java.util.HashMap使用的是链表法来处理冲突:</p><p><img src="https://img-blog.csdnimg.cn/img_convert/ed997859830e2a95ad92f272c84e8714.png" alt="img"></p><p>但是,对于ThreadLocalMap,它使用的是简单的线性探测法,如果发生了元素冲突,那么就使用下一个槽位存放:</p><p><img src="https://img-blog.csdnimg.cn/img_convert/aa2d77a8245005b80f2da0c3ad65b492.png" alt="img"></p><p>具体来说,整个set()的过程如下:</p><p><img src="https://img-blog.csdnimg.cn/img_convert/4ea4acee93016a8918a2af70a55943bc.png" alt="img"></p><h2 id="可以被继承的threadlocalinheritablethreadlocal"><a class="markdownIt-Anchor" href="#可以被继承的threadlocalinheritablethreadlocal"></a> 可以被继承的ThreadLocal——InheritableThreadLocal</h2><p>在实际开发过程中,我们可能会遇到这么一种场景。主线程开了一个子线程,但是我们希望在子线程中可以访问主线程中的ThreadLocal对象,也就是说有些数据需要进行父子线程间的传递。比如像这样:</p><figure class="highlight java"><table><tbody><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">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">ThreadLocal</span> <span class="variable">threadLocal</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ThreadLocal</span>();</span><br><span class="line"> IntStream.range(<span class="number">0</span>,<span class="number">10</span>).forEach(i -> {</span><br><span class="line"> <span class="comment">//每个线程的序列号,希望在子线程中能够拿到</span></span><br><span class="line"> threadLocal.set(i);</span><br><span class="line"> <span class="comment">//这里来了一个子线程,我们希望可以访问上面的threadLocal</span></span><br><span class="line"> <span class="keyword">new</span> <span class="title class_">Thread</span>(() -> {</span><br><span class="line"> System.out.println(Thread.currentThread().getName() + <span class="string">":"</span> + threadLocal.get());</span><br><span class="line"> }).start();</span><br><span class="line"> <span class="keyword">try</span> {</span><br><span class="line"> Thread.sleep(<span class="number">1000</span>);</span><br><span class="line"> } <span class="keyword">catch</span> (InterruptedException e) {</span><br><span class="line"> e.printStackTrace();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><p>执行上述代码,你会看到:</p><figure class="highlight java"><table><tbody><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">Thread-<span class="number">0</span>:<span class="literal">null</span></span><br><span class="line">Thread-<span class="number">1</span>:<span class="literal">null</span></span><br><span class="line">Thread-<span class="number">2</span>:<span class="literal">null</span></span><br><span class="line">Thread-<span class="number">3</span>:<span class="literal">null</span></span><br></pre></td></tr></tbody></table></figure><p>因为在子线程中,是没有threadLocal的。如果我们希望子线可以看到父线程的ThreadLocal,那么就可以使用InheritableThreadLocal。顾名思义,这就是一个支持线程间父子继承的ThreadLocal,将上述代码中的threadLocal使用InheritableThreadLocal:</p><figure class="highlight java"><table><tbody><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">InheritableThreadLocal</span> <span class="variable">threadLocal</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">InheritableThreadLocal</span>();</span><br></pre></td></tr></tbody></table></figure><p>再执行,就能看到:</p><figure class="highlight java"><table><tbody><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">Thread-<span class="number">0</span>:<span class="number">0</span></span><br><span class="line">Thread-<span class="number">1</span>:<span class="number">1</span></span><br><span class="line">Thread-<span class="number">2</span>:<span class="number">2</span></span><br><span class="line">Thread-<span class="number">3</span>:<span class="number">3</span></span><br><span class="line">Thread-<span class="number">4</span>:<span class="number">4</span></span><br></pre></td></tr></tbody></table></figure><p>可以看到,每个线程都可以访问到从父进程传递过来的一个数据。虽然InheritableThreadLocal看起来挺方便的,但是依然要注意以下几点:</p><p>变量的传递是发生在线程创建的时候,如果不是新建线程,而是用了线程池里的线程,就不灵了<br>变量的赋值就是从主线程的map复制到子线程,它们的value是同一个对象,如果这个对象本身不是线程安全的,那么就会有线程安全问题</p><h2 id="写在最后的话"><a class="markdownIt-Anchor" href="#写在最后的话"></a> 写在最后的话</h2><p>今天,我们介绍了ThreadLocal,ThreadLocal在Java的多线程开发中有着十分重要的作用。</p><p>在这里,我们介绍了ThreadLocal的基本使用和实现原理,尤其重点介绍了基于当前实现原理下可能存在的内存泄漏问题。</p><p>最后,还介绍了一个用于在父子线程间传递数据的特殊的ThreadLocal实现,希望对大家有所帮助。</p>]]></content>
<summary type="html"><p>在处理多线程并发安全的方法中,最常用的方法,就是使用锁,通过锁来控制多个不同线程对临界区的访问。</p>
<p>但是,无论是什么样的锁,乐观锁或者悲观锁,都会在并发冲突的时候对性能产生一定的影响。</p>
<p>那有没有一种方法,可以彻底避免竞争呢?</p>
<p>答案是肯</summary>
<category term="juc" scheme="http://example.com/categories/juc/"/>
<category term="juc" scheme="http://example.com/tags/juc/"/>
</entry>
<entry>
<title>CAS</title>
<link href="http://example.com/2023/03/16/juc/CAS/"/>
<id>http://example.com/2023/03/16/juc/CAS/</id>
<published>2023-03-16T01:34:43.961Z</published>
<updated>2023-07-05T16:37:40.438Z</updated>
<content type="html"><![CDATA[<h1 id="并发编程的灵魂cas机制详解"><a class="markdownIt-Anchor" href="#并发编程的灵魂cas机制详解"></a> 并发编程的灵魂:CAS机制详解</h1><p>Java中提供了很多原子操作类来保证共享变量操作的原子性。这些原子操作的底层原理都是使用了CAS机制。在使用一门技术之前,了解这个技术的底层原理是非常重要的,所以本篇文章就先来讲讲什么是CAS机制,CAS机制存在的一些问题以及在Java中怎么使用CAS机制。</p><p>其实Java并发框架的基石一共有两块,一块是本文介绍的CAS,另一块就是AQS,后续也会写文章介绍。</p><h2 id="什么是cas机制"><a class="markdownIt-Anchor" href="#什么是cas机制"></a> 什么是CAS机制</h2><p><strong>CAS机制是一种数据更新的方式</strong>。在具体讲什么是CAS机制之前,我们先来聊下在多线程环境下,对共享变量进行数据更新的两种模式:悲观锁模式和乐观锁模式。</p><p>悲观锁更新的方式认为:在更新数据的时候大概率会有其他线程去争夺共享资源,所以悲观锁的做法是:第一个获取资源的线程会将资源锁定起来,其他没争夺到资源的线程只能进入阻塞队列,等第一个获取资源的线程释放锁之后,这些线程才能有机会重新争夺资源。synchronized就是java中悲观锁的典型实现,synchronized使用起来非常简单方便,但是会使没争抢到资源的线程进入阻塞状态,线程在阻塞状态和Runnable状态之间切换效率较低(比较慢)。比如你的更新操作其实是非常快的,这种情况下你还用synchronized将其他线程都锁住了,线程从Blocked状态切换回Runnable花的时间可能比你的更新操作的时间还要长。</p><p>乐观锁更新方式认为:在更新数据的时候其他线程争抢这个共享变量的概率非常小,所以更新数据的时候不会对共享数据加锁。但是<strong>在正式更新数据之前会检查数据是否被其他线程改变过,如果未被其他线程改变过就将共享变量更新成最新值,如果发现共享变量已经被其他线程更新过了,就重试,直到成功为止</strong>。<mark>CAS机制就是乐观锁的典型实现</mark>。</p><p>CAS,是Compare and Swap的简称,在这个机制中有三个核心的参数:</p><ul><li>主内存中存放的共享变量的值:V(一般情况下这个V是内存的地址值,通过这个地址可以获得内存中的值)</li><li>工作内存中共享变量的副本值,也叫预期值:A</li><li>需要将共享变量更新到的最新值:B</li></ul><p><img src="https://pic1.zhimg.com/80/v2-f32df3fde2dc0132c05533572e2b659c_720w.webp" alt="img"></p><p>如上图中,主存中保存V值,线程中要使用V值要先从主存中读取V值到线程的工作内存A中,然后计算后变成B值,最后再把B值写回到内存V值中。多个线程共用V值都是如此操作。CAS的核心是在将B值写入到V之前要比较A值和V值是否相同,如果不相同证明此时V值已经被其他线程改变,重新将V值赋给A,并重新计算得到B,如果相同,则将B值赋给V。</p><p>值得注意的是CAS机制中的这步步骤是原子性的(从指令层面提供的原子操作),所以CAS机制可以解决多线程并发编程对共享变量读写的原子性问题。</p><h2 id="cas机制优缺点"><a class="markdownIt-Anchor" href="#cas机制优缺点"></a> CAS机制优缺点</h2><h3 id="缺点"><a class="markdownIt-Anchor" href="#缺点"></a> 缺点</h3><p><strong>1. ABA问题</strong><br>ABA问题:CAS在操作的时候会检查变量的值是否被更改过,如果没有则更新值,但是带来一个问题,最开始的值是A,接着变成B,最后又变成了A。经过检查这个值确实没有修改过,因为最后的值还是A,但是实际上这个值确实已经被修改过了。为了解决这个问题,在每次进行操作的时候加上一个版本号,每次操作的就是两个值,一个版本号和某个值,A——>B——>A问题就变成了1A——>2B——>3A。在jdk中提供了AtomicStampedReference类解决ABA问题,用Pair这个内部类实现,包含两个属性,分别代表版本号和引用,在compareAndSet中先对当前引用进行检查,再对版本号标志进行检查,只有全部相等才更新值。</p><p><strong>2. 可能会消耗较高的CPU</strong><br>看起来CAS比锁的效率高,从阻塞机制变成了非阻塞机制,减少了线程之间等待的时间。每个方法不能绝对的比另一个好,在线程之间竞争程度大的时候,如果使用CAS,每次都有很多的线程在竞争,也就是说CAS机制不能更新成功。这种情况下CAS机制会一直重试,这样就会比较耗费CPU。因此可以看出,如果线程之间竞争程度小,使用CAS是一个很好的选择;但是如果竞争很大,使用锁可能是个更好的选择。在并发量非常高的环境中,如果仍然想通过原子类来更新的话,可以使用AtomicLong的替代类:LongAdder。</p><p><strong>3. 不能保证代码块的原子性</strong><br>Java中的CAS机制只能保证共享变量操作的原子性,而不能保证代码块的原子性。</p><h3 id="优点"><a class="markdownIt-Anchor" href="#优点"></a> 优点</h3><ul><li>可以保证变量操作的原子性;</li><li>并发量不是很高的情况下,使用CAS机制比使用锁机制效率更高;</li><li>在线程对共享资源占用时间较短的情况下,使用CAS机制效率也会较高。</li></ul><h2 id="java提供的cas操作类unsafe类"><a class="markdownIt-Anchor" href="#java提供的cas操作类unsafe类"></a> Java提供的CAS操作类–Unsafe类</h2><p>从Java5开始引入了对CAS机制的底层的支持,在这之前需要开发人员编写相关的代码才可以实现CAS。在原子变量类Atomic<strong>中(例如AtomicInteger、AtomicLong)可以看到CAS操作的代码,在这里的代码都是调用了底层(核心代码调用native修饰的方法)的实现方法。在AtomicInteger源码中可以看getAndSet方法和compareAndSet方法之间的关系,compareAndSet方法调用了底层的实现,该方法可以实现与一个volatile变量的读取和写入相同的效果。在前面说到了volatile不支持例如i++这样的复合操作,在Atomic</strong>中提供了实现该操作的方法。JVM对CAS的支持通过这些原子类(Atomic)暴露出来,供我们使用。</p><p>而Atomic系类的类底层调用的是Unsafe类的API,Unsafe类提供了一系列的compareAndSwap*方法,下面就简单介绍下Unsafe类的API:</p><ul><li>long objectFieldOffset(Field field)方法:返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该Unsafe函数中访问指定字段时使用。如下代码使用Unsafe类获取变量value在AtomicLong对象中的内存偏移。</li></ul><figure class="highlight text"><table><tbody><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">static {</span><br><span class="line"> try {</span><br><span class="line"> valueOffset = unsafe.objectFieldOffset</span><br><span class="line"> (AtomicInteger.class.getDeclaredField("value"));</span><br><span class="line"> } catch (Exception ex) { throw new Error(ex); }</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><ul><li>int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。</li><li>int arrayIndexScale(Class arrayClass)方法:获取数组中一个元素占用的字节。</li><li>boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:比较对象obj中偏移量为offset的变量的值是否与expect相等,相等则使用update值更新,然后返回true,否则返回false。</li><li>public native long getLongvolatile(Object obj, long offset)方法:获取对象obj中偏移量为offset的变量对应volatile语义的值。</li><li>void putLongvolatile(Object obj, long offset, long value)方法:设置obj对象中offset偏移的类型为long的field的值为value,支持volatile语义。</li><li>void putOrderedLong(Object obj, long offset, long value)方法:设置obj对象中offset偏移地址对应的long型field的值为value。这是一个有延迟的putLongvolatile方法,并且不保证值修改对其他线程立刻可见。只有在变量使用volatile修饰并且预计会被意外修改时才使用该方法。</li><li>void park(boolean isAbsolute, long time)方法:阻塞当前线程,其中参数isAbsolute等于false且time等于0表示一直阻塞。time大于0表示等待指定的time后阻塞线程会被唤醒,这个time是个相对值,是个增量值,也就是相对当前时间累加time后当前线程就会被唤醒。如果isAbsolute等于true,并且time大于0,则表示阻塞的线程到指定的时间点后会被唤醒,这里time是个绝对时间,是将某个时间点换算为ms后的值。另外,当其他线程调用了当前阻塞线程的interrupt方法而中断了当前线程时,当前线程也会返回,而当其他线程调用了unPark方法并且把当前线程作为参数时当前线程也会返回。</li><li>void unpark(Object thread)方法:唤醒调用park后阻塞的线程。</li></ul><p>下面是JDK8新增的函数,这里只列出Long类型操作。</p><ul><li>long getAndSetLong(Object obj, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。</li></ul><figure class="highlight text"><table><tbody><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">//这个方法只是封装了compareAndSwapLong的使用,不需要自己写重试机制</span><br><span class="line">public final long getAndSetLong(Object var1, long var2, long var4) {</span><br><span class="line"> long var6;</span><br><span class="line"> do {</span><br><span class="line"> var6 = this.getLongVolatile(var1, var2);</span><br><span class="line"> } while(!this.compareAndSwapLong(var1, var2, var6, var4));</span><br><span class="line"></span><br><span class="line"> return var6;</span><br><span class="line">}</span><br></pre></td></tr></tbody></table></figure><ul><li>long getAndAddLong(Object obj, long offset, long addValue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addValue,原理和上面的方法类似。</li></ul><h2 id="cas使用场景"><a class="markdownIt-Anchor" href="#cas使用场景"></a> CAS使用场景</h2><ul><li>使用一个变量统计网站的访问量;</li><li>Atomic类操作;</li><li>数据库乐观锁更新。</li></ul>]]></content>
<summary type="html"><h1 id="并发编程的灵魂cas机制详解"><a class="markdownIt-Anchor" href="#并发编程的灵魂cas机制详解"></a> 并发编程的灵魂:CAS机制详解</h1>
<p>Java中提供了很多原子操作类来保证共享变量操作的原子性。这些原子操作</summary>
<category term="juc" scheme="http://example.com/categories/juc/"/>
<category term="juc" scheme="http://example.com/tags/juc/"/>
</entry>
<entry>
<title>聚合的分类</title>
<link href="http://example.com/2023/03/13/elasticsearch/%E8%81%9A%E5%90%88%E7%9A%84%E5%88%86%E7%B1%BB/"/>
<id>http://example.com/2023/03/13/elasticsearch/%E8%81%9A%E5%90%88%E7%9A%84%E5%88%86%E7%B1%BB/</id>
<published>2023-03-13T14:10:42.070Z</published>
<updated>2023-03-13T14:36:00.032Z</updated>
<content type="html"><![CDATA[<p><img src="../../images/image-20230313221322812.png" alt="image-20230313221322812"></p><p><img src="../../images/image-20230313221351783.png" alt="image-20230313221351783"></p>]]></content>
<summary type="html"><p><img src="../../images/image-20230313221322812.png" alt="image-20230313221322812"></p>
<p><img src="../../images/image-20230313221351783.</summary>
<category term="Elasticsearch" scheme="http://example.com/categories/Elasticsearch/"/>
<category term="DSL" scheme="http://example.com/categories/Elasticsearch/DSL/"/>
<category term="Elasticsearch" scheme="http://example.com/tags/Elasticsearch/"/>
</entry>
</feed>