-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
374 lines (186 loc) · 371 KB
/
search.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
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>数据库学习之事务操作</title>
<link href="/2023/02/26/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AD%A6%E4%B9%A0%E4%B9%8B%E4%BA%8B%E5%8A%A1%E6%93%8D%E4%BD%9C/"/>
<url>/2023/02/26/%E6%95%B0%E6%8D%AE%E5%BA%93%E5%AD%A6%E4%B9%A0%E4%B9%8B%E4%BA%8B%E5%8A%A1%E6%93%8D%E4%BD%9C/</url>
<content type="html"><</p></blockquote><blockquote><p><strong>设置隔离级别:</strong></p></blockquote><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SET [SESSION | GLOBAL] TRANSACTION ISOLATION LEVEL {</span><br><span class="line">READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}</span><br></pre></td></tr></table></figure><p>session:设置完成之后不需要重启,且session.tx_isolation 和 tx_isolation都是session设置的内容,且Linux的另一个窗口不会收到影响<br>global:设置完成之后,需要重启,且Linux的所有窗口下都会被设置未global设置的内容</p><h3 id="2-读未提交【Read-Uncommitted】"><a href="#2-读未提交【Read-Uncommitted】" class="headerlink" title="2. 读未提交【Read Uncommitted】"></a>2. 读未提交【Read Uncommitted】</h3><p>几乎没有加锁,虽然效率高,但是问题太多,严重不建议采用</p><p>一个事务在执行中,读到另一个执行中事务的更新(或其他操作)但是未commit的数据,这种现象叫做脏读(dirty read)</p><h3 id="3-读提交【Read-Committed】"><a href="#3-读提交【Read-Committed】" class="headerlink" title="3. 读提交【Read Committed】"></a>3. 读提交【Read Committed】</h3><p>一个事务只有被commit,才能被读取到</p><p>1.读提交<br>一旦提交,别人能读到 不应该等价于 你已经提交,和你“并行运行”的事务也能读到<br>2.不可重复读问题–>应用层会有什么问题?<br>同一个事务,同样的读取,在不同的时间段,读取到了不同的值<br>原因:<br>两个终端,一个修改数据,一个查询数据,可能会因为时间的不同而访问到不同的数据</p><h3 id="4-可重复读【Repeatable-Read】"><a href="#4-可重复读【Repeatable-Read】" class="headerlink" title="4. 可重复读【Repeatable Read】"></a>4. 可重复读【Repeatable Read】</h3><p>为了解决不可重复读问题</p><p>两个终端分别启动事务<br>第一个终端修改数据,然后commit<br>另一个终端分别在第一个终端的commit前后,查询数据,发现数据还是那个数据,当这个终端commit后,才能查询到第一个终端修改之后的数据</p><h3 id="5-串行化【serializable】"><a href="#5-串行化【serializable】" class="headerlink" title="5. 串行化【serializable】"></a>5. 串行化【serializable】</h3><p>对所有操作全部加锁,进行串行化,不会有问题,但是只要串行化,效率很低,几乎完全不会被采用</p><p>两个事务进行串行化</p><p>实验:<br>两个事务依次进行begin<br>两个事务都可以进行查询数据select<br>当第一个事务进行修改数据的时候,是会直接卡住的,此时当第二个事务commit,第一个事务则才会进行修改数据<br>(当第一个事务卡住的时候,长时间不对第二个事务进行commit,然后第二个事务commit,此时第一个事务也是不能执行成功的—-超时机制)</p><h3 id="6-总结"><a href="#6-总结" class="headerlink" title="6. 总结"></a>6. 总结</h3><p>读未提交:<br>有脏读问题,幻读,不可重复读<br>读提交:<br>不可重读读,幻读<br>可重复读:<br>MySQL不存在任何问题,但是别人数据库可能存在幻读问题<br>幻读是专门针对插入的</p><p>隔离性:<br>MySQL的内部机制,让“同时”启动,并发执行的各个事务,看到不同的数据修改(增删改),就叫做隔离性<br>我们作为一个事务,可以看到不同可见性的数据,程度的不同,叫做隔离级别<br>为何要存在隔离级别?<br>为了安全与效率,结合实际情况</p><h3 id="7-一致性"><a href="#7-一致性" class="headerlink" title="7. 一致性"></a>7. 一致性</h3><p>事务执行的结果,必须使数据库从一个一致性状态,变到另一个一致性状态。当数据库只包含事务成功提交的结果时,数据库处于一致性状态。如果系统运行发生中断,某个事务尚未完成而被迫中断,而改未完成的事务对数据库所做的修改已被写入数据库,此时数据库就处于一种不正确(不一致)的状态。因此一致性是通过原子性来保证的。<br>其实一致性和用户的业务逻辑强相关,由用户和MySQL共同决定<br>而技术上,通过AID保证C</p><p>原子性,持久性,隔离性,他们三个都保证了一致性<br>事务的最终目标:一致性</p><h2 id="如何理解隔离性?"><a href="#如何理解隔离性?" class="headerlink" title="如何理解隔离性?"></a>如何理解隔离性?</h2><p>数据库并发场景:<br>读-读 :不存在任何问题,也不需要并发控制<br>读-写 :有线程安全问题,可能会造成事务隔离性问题,可能遇到脏读,幻读,不可重复读<br>写-写 :有线程安全问题,可能会存在更新丢失问题<br>只能通过加锁解决,两个写都没问题,但是第一个完成写的终端,进行了数据回滚,那么就有可能数据丢失</p><h2 id="读写"><a href="#读写" class="headerlink" title="读写"></a>读写</h2><p>多版本并发控制( MVCC )是一种用来解决 读-写冲突 的无锁并发控制<br>为事务分配单向增长的事务ID,为每个修改保存一个版本,版本与事务ID关联,读操作只读该事务开始前的数据库的快照</p><p>为事务分配单向增长的事务ID:可以通过事务id数字,来区分事务的先后</p><h3 id="1-预备知识"><a href="#1-预备知识" class="headerlink" title="1. 预备知识"></a>1. 预备知识</h3><blockquote><p>3个记录隐藏列字段<br><strong>DB_TRX_ID :</strong>6 byte,最近修改( 修改/插入 )事务ID,记录创建这条记录/最后一次修改该记录的事务ID(记录的这个事务id,修改了这列数据)<br><strong>DB_ROLL_PTR :</strong> 7 byte,回滚指针,指向这条记录的上一个版本(简单理解成,指向历史版本就行,这些数据一般在 undo log 中)<br><strong>DB_ROW_ID :</strong> 6 byte,隐含的自增ID(隐藏主键),如果数据表没有主键, InnoDB 会自动以 <strong>DB_ROW_ID</strong> 产生一个聚簇索引,(如果有那么就不需要增加)<br><strong>补充:</strong>实际还有一个删除flag隐藏字段, 既记录被更新或删除并不代表真的删除,而是删除flag变了</p></blockquote><blockquote><p>undo日志<br>MySQL 中的一段内存缓冲区,用来保存日志数据的就行</p></blockquote><blockquote><p>MVCC模拟</p><p></p><p>一个基于链表记录的历史<strong>版本</strong>链。所谓的回滚,无非就是用历史数据,覆盖当前数据。</p></blockquote><p>上面的一个一个版本,我们可以称之为一个一个的快照(历史版本)<br>最新版本:当前值</p><h3 id="2-思考"><a href="#2-思考" class="headerlink" title="2. 思考"></a>2. 思考</h3><blockquote><p>上面是以更新(<code>upadte</code>)主讲的,如果是<code>delete</code>呢?一样的,别忘了,删数据不是清空,而是设置flag为删除即可。也可以形成版本。<br><strong>问题:</strong> delete也可以恢复数据,保证操作的原子性</p></blockquote><blockquote><p>如果是insert呢?因为insert是插入,也就是之前没有数据,那么insert也就没有历史版本。但是一般为了回滚操作,insert的数据也是要被放入undo log中,如果当前事务commit了,那么这个undo log 的历史insert记录就可以被清空了。<br>当我们第一次insert时,数据库本身没有这段数据,我们可以先插入一段空记录,如果需要回滚,可以查看历史undo日志,</p></blockquote><blockquote><p>select不会对数据做任何修改,所以,为select维护多版本,没有意义。不过,此时有个问题,就是:select读取,是读取最新的版本呢?还是读取历史版本?–>都有可能<br>不同事务,观看到不同的版本<br>select看的是最古老的版本,修改则是最新的版本<br>隔离级别的不同,让你看到的是历史的不同版本<br>当前读:读取最新的记录,就是当前读。增删改,都叫做当前读,select也有可能当前读,比如:select lock in sharemode(共享锁), select for update (这个好理解,我们后面不讨论)<br>快照读:读取历史版本(一般而言),就叫做快照读。</p></blockquote><blockquote><p>我们可以看到,在多个事务同时删改查的时候,都是当前读,是要加锁的。那同时有select过来,如果也要读取最新版(当前读),那么也就需要加锁,这就是串行化。<br>但如果是快照读,读取历史版本的话,是不受加锁限制的。也就是可以并行执行!换言之,提高了效率,即MVCC的意义所在</p></blockquote><p>隔离级别决定了,select是当前读,还是快照!</p><p>为什么要有隔离级别呢?事务都是原子的。所以,无论如何,事务总有先有后。</p><p>但是经过上面的操作我们发现,事务从begin->CURD->commit,是有一个阶段的。也就是事务有执行前,执行中,执行后的阶段。但,不管怎么启动多个事务,总是有先有后的。</p><p>那么多个事务在执行中,CURD操作是会交织在一起的。那么,为了保证事务的“有先有后”,是不是应该让不同的事务看到它该看到的内容,这就是所谓的隔离性与隔离级别要解决的问题。</p><p>先来的事务,应不应该看到后来的事务所做的修改呢?</p><h3 id="3-read-view"><a href="#3-read-view" class="headerlink" title="3. read view"></a>3. read view</h3><p>Read View就是事务进行 快照读 操作的时候生产的 读视图 (Read View),在该事务执行的快照读的那一刻,会生成数据库系统当前的一个快照,记录并维护系统当前活跃事务的ID(当每个事务开启时,都会被分配一个ID, 这个ID是递增的,所以最新的事务,ID值越大)</p><p>Read View 在 MySQL 源码中,就是一个类,本质是用来进行可见性判断的。 即当我们某个事务执行快照读的时候,对该记录创建一个 Read View 读视图,把它比作条件,用来判断当前事务能够看到哪个版本的数据,既可能是当前最新的数据,也有可能是该行记录的 undo log 里面的某个版本的数据。</p><p>当你.的事务到来的时候,形成事务id,你所能看到mysql 内部的各种数据,就要被确定下来!<br>当我们的事务到来的时候,我们能看到MySQL内部的各种数据,类比成一张照片</p><h2 id="RR和RC的本质区别"><a href="#RR和RC的本质区别" class="headerlink" title="RR和RC的本质区别"></a>RR和RC的本质区别</h2><p>事务中快照读的结果是非常依赖该事务首次出现快照读的地方,即某个事务中首次出现快照读,决定该事务后续快照读结果的能力<br>delete同样如此</p><p><strong>RR和RC的本质区别:</strong><br>正是Read View生成时机的不同,从而造成RC,RR级别下快照读的结果的不同<br>在RR级别下的某个事务的对某条记录的第一次快照读会创建一个快照及Read View, 将当前系统活跃的其他事务记录起来<br>此后在调用快照读的时候,还是使用的是同一个Read View,所以只要当前事务在其他事务提交更新之前使用过快照读,那么之后的快照读使用的都是同一个Read View,所以对之后的修改不可见;<br>即RR级别下,快照读生成Read View时,Read View会记录此时所有其他活动事务的快照,这些事务的修改对于当前事务都是不可见的。而早于Read View创建的事务所做的修改均是可见<br>而在<strong>RC级别下</strong>的,事务中,<strong>每次快照读都会新生成一个快照和Read View,</strong> 这就是我们在RC级别下的事务中可以看到别的事务提交的更新的原因<br>总之在RC隔离级别下,是每个快照读都会生成并获取最新的Read View;而在RR隔离级别下,则是同一个事务中的第一个快照读才会创建Read View, 之后的快照读获取的都是同一个Read View。<br>正是RC每次快照读,都会形成Read View,所以,RC才会有不可重复读问题。</p><p>事务的基本验证<br>事务如何操作<br>如何启动一个事务,如何提交一个事务<br>会操作</p><p>根据事务常见操作,引发的各种验证实验<br>回滚性质,安全性质,mysql各种隔离级别<br>设置隔离级别的各种表现<br>通过实现如何理解四个性质</p><p>从源码中理解隔离性</p>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> 数据库 </tag>
</tags>
</entry>
<entry>
<title>java同时访问一个meshod会起几个线程</title>
<link href="/2023/02/26/java%E5%90%8C%E6%97%B6%E8%AE%BF%E9%97%AE%E4%B8%80%E4%B8%AAmeshod%E4%BC%9A%E8%B5%B7%E5%87%A0%E4%B8%AA%E7%BA%BF%E7%A8%8B/"/>
<url>/2023/02/26/java%E5%90%8C%E6%97%B6%E8%AE%BF%E9%97%AE%E4%B8%80%E4%B8%AAmeshod%E4%BC%9A%E8%B5%B7%E5%87%A0%E4%B8%AA%E7%BA%BF%E7%A8%8B/</url>
<content type="html"><![CDATA[<h1 id="java同时访问一个meshod会起几个线程"><a href="#java同时访问一个meshod会起几个线程" class="headerlink" title="java同时访问一个meshod会起几个线程"></a>java同时访问一个meshod会起几个线程</h1><p>首先看文章之前你可以在脑海中模拟一下,两个请求并发访问一个方法,从浏览器到后台的大致流程是怎么样的,模拟的越详细越好。</p><p>我相信很大一部分人可能都会被这个问题难住,不管是刚毕业的大学生,还是工作两三年的朋友们。</p><p>可能我们平时太多的关注业务逻辑,关注各种炫酷的框架,亦或者是公司太忙,忙到我们没时间成长等等等,从而忽略了这些最基础的东西。不过可能正是因为这些最基础的东西,才是我们为什么薪资比别人低了一点,为什么机会比别人少了一点,为什么差距和别人越来越大。</p><p>学而不思则罔,思而不学则殆。</p><p>这个问题其实分两步<br>第一步是用户点击页面,并发送请求到服务器的步骤,这个步骤很复杂,涉及到网络协议很多东西,我们暂且不讲。<br>第二步就是服务器收到请求,我们代码执行的过程,我们具体说一下这一步。<br>如下图这个简单的方法</p><p><img src="/../images/image-20230226233402320.png" alt="image-20230226233402320"></p><p>有两个请求并发访问,也就是说有两个线程同时准备进入方法printA()。那么问题来了</p><p>是线程1先进入方法执行完毕后再让线程2进入执行吗。</p><p>不是的,是两个线程同时去执行这一段代码</p><p>那有同学可能会问了,如果两个线程同时进入这个方法,</p><p>线程1执行到a = a + 10;此时a的值为11,</p><p>然后线程2进入方法执行int a = 1; 那线程1打印出来a是不是就变成1了。</p><p>不是的,线程1打印a还是为11,为什么呢,因为a是定义在方法里面的,是局部变量。</p><p>然后每个线程是不是都有一个私有的本地内存(Local Memory),这个私有本地内存是不是存放这个局部变量的,答案是肯定的,既然赋值操作都是在我自己的地盘弄的,那肯定不会影响到别人。</p><p>但是如果我在这个类中加入一个全局变量,如下图</p><p><img src="/../images/image-20230226233421609.png" alt="image-20230226233421609"></p><p>两个线程同时进入这个方法,那输出的c肯定是不一样的,为什么呢,相信你已经猜到了,没错。</p><p>这是因为线程之间的共享变量存储在主内存(Main Memory)中,变量c是全局变量,会放在主存中。</p><p>类初始化的时候全局变量会被加载到主存中,线程1需要对c赋值计算的时候,从主存中拷贝一个c的副本,放到自己的私有内存中,计算完毕后c的值发生了变化,回写到主存中,这时候线程2需要对c赋值计算了,再把c拷贝一个副本到自己的私有内存里,计算完毕后,再回写到主存中,最后的结果就是对c进行了两次操作。其实这个过程就是线程之间的通讯过程</p><p>如下草图</p><p><img src="/../images/image-20230226233437115.png" alt="image-20230226233437115"></p><p>好了,差不多到这里这个简易的流程就算是结束了。涉及到了几个知识点,线程内存模型,全局变量和成员变量的区别。<br>写这篇文章的目的是想帮助对这一块概念比较模糊的同学,大家都知道线程的定义,变量的定义,但是结合在一起可能会迷糊一点,这里借一个简单的小例子可以帮大家把学到的东西串起来。<br>知其然,知其所以然,才可以更好的敲代码。</p>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> java </tag>
</tags>
</entry>
<entry>
<title>Go语言的类_结构体和方法</title>
<link href="/2023/02/26/Go%E8%AF%AD%E8%A8%80%E7%9A%84%E7%B1%BB-%E7%BB%93%E6%9E%84%E4%BD%93%E5%92%8C%E6%96%B9%E6%B3%95/"/>
<url>/2023/02/26/Go%E8%AF%AD%E8%A8%80%E7%9A%84%E7%B1%BB-%E7%BB%93%E6%9E%84%E4%BD%93%E5%92%8C%E6%96%B9%E6%B3%95/</url>
<content type="html"><![CDATA[<h1 id="Go语言的类-结构体和方法"><a href="#Go语言的类-结构体和方法" class="headerlink" title="Go语言的类_结构体和方法"></a>Go语言的类_结构体和方法</h1><h2 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h2><p>GO语言中没有类的概念,也不支持类的继承等面向对象的概念。GO语言中通过结构体的内嵌再配合接口比面向对象具有更高的扩展性和灵活性。</p><h2 id="一、结构体的基础知识"><a href="#一、结构体的基础知识" class="headerlink" title="一、结构体的基础知识"></a>一、结构体的基础知识</h2><h3 id="1-结构体的定义"><a href="#1-结构体的定义" class="headerlink" title="1. 结构体的定义"></a>1. 结构体的定义</h3><p>GO语言中的基础数据类型可以表示一些事物的基本属性,但是当我们想表达一个事物的部分或者全部属性时,单一的基本数据类型明显就不满足需求了。GO语言中提供了一种自定义的数据类型,可以封装多个基本数据类型,这种数据类型就是结构体(struct)。</p><p>GO语言中通过type和struct关键字来定义结构体,例:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> person <span class="keyword">struct</span>{</span><br><span class="line">name<span class="type">string</span></span><br><span class="line">age<span class="type">int</span></span><br><span class="line">sex<span class="type">string</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="2-结构体实例化"><a href="#2-结构体实例化" class="headerlink" title="2. 结构体实例化"></a>2. 结构体实例化</h3><p>GO语言中只有当结构体实例化时才会分配内存,因此必须实例化后才能使用结构体字段。例:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//var 结构体实例 结构体类型</span></span><br><span class="line"><span class="keyword">type</span> person <span class="keyword">struct</span>{</span><br><span class="line">name<span class="type">string</span></span><br><span class="line">age<span class="type">int</span></span><br><span class="line">sex<span class="type">string</span></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span> <span class="params">()</span></span>{</span><br><span class="line"><span class="comment">//第一种</span></span><br><span class="line"><span class="keyword">var</span> p person </span><br><span class="line">p.name = <span class="string">"GOgo"</span></span><br><span class="line">p.age = <span class="number">20</span></span><br><span class="line">p.sex = <span class="string">"男"</span></span><br><span class="line"><span class="comment">//第二种</span></span><br><span class="line">p1 := person{</span><br><span class="line">name : <span class="string">"GOgo"</span>,</span><br><span class="line">age : <span class="number">20</span>,</span><br><span class="line">sex : <span class="string">"男"</span>,</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="3-构造函数"><a href="#3-构造函数" class="headerlink" title="3. 构造函数"></a>3. 构造函数</h3><p>GO语言种没有结构体的构造函数,但是我们可以自己实现构造函数,以上文的<code>person</code>为例:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newPerson</span><span class="params">(name <span class="type">string</span> , age <span class="type">int</span>, sex <span class="type">string</span>)</span></span>*person{</span><br><span class="line"><span class="keyword">return</span> &person{</span><br><span class="line">name : name,</span><br><span class="line">age : age,</span><br><span class="line">sex : sex,</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>调用构造函数:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">p := newPerson(<span class="string">"GOgo"</span>,<span class="number">20</span>,<span class="string">"男"</span>)</span><br></pre></td></tr></table></figure><h2 id="二、方法和接收者"><a href="#二、方法和接收者" class="headerlink" title="二、方法和接收者"></a>二、方法和接收者</h2><h3 id="1-方法定义"><a href="#1-方法定义" class="headerlink" title="1.方法定义"></a>1.方法定义</h3><p>GO语言中的方法(Method)是一种作用于特殊变量类型的函数,这种特定类型变量称为接收者(Receiver)。接收者的概念类似于其他语言的<code>this</code>和<code>self</code></p><h3 id="2-读入数据"><a href="#2-读入数据" class="headerlink" title="2.读入数据"></a>2.读入数据</h3><p>代码如下(示例):</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">data = pd.read_csv(</span><br><span class="line"> <span class="string">'https://labfile.oss.aliyuncs.com/courses/1283/adult.data.csv'</span>)</span><br><span class="line"><span class="built_in">print</span>(data.head())</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>方法的定义格式如下:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(接收者变量 接收者类型)</span></span> 方法名(参数列表)(返回参数){</span><br><span class="line">函数体</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>注意:</strong></p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="number">1.</span>接收者变量:接收者中的参数变量名在命名时,官方建议使用接收者类型名的第一个小写字母,而不是self、this之类的命名。例如,Person类型的接收者变量应该命名为 p,Connector类型的接收者变量应该命名为c等。</span><br><span class="line"><span class="number">2.</span>接收者类型:接收者类型和参数类似,可以是指针类型和非指针类型。</span><br><span class="line"><span class="number">3.</span>方法名、参数列表、返回参数:具体格式与函数定义相同。</span><br></pre></td></tr></table></figure><p>举个例子:</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">type</span> person <span class="keyword">struct</span>{</span><br><span class="line">name <span class="type">string</span></span><br><span class="line">sex <span class="type">string</span></span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">newPerson</span><span class="params">(name , sex <span class="type">string</span>)</span></span> *person{</span><br><span class="line"><span class="keyword">return</span> &person{</span><br><span class="line">name : name,</span><br><span class="line">sex : sex,</span><br><span class="line">}</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(p person)</span></span> Speake(){</span><br><span class="line">fmt.Printf(<span class="string">"%s说他一定要学好GO语言!"</span>,p.name)</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> p := newPerson(<span class="string">"GOgo"</span>, <span class="number">20</span>)</span><br><span class="line"> p1.Speake()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="3-任意类型添加方法"><a href="#3-任意类型添加方法" class="headerlink" title="3.任意类型添加方法"></a>3.任意类型添加方法</h3><p>在Go语言中,接收者的类型可以是任何类型,不仅仅是结构体,任何类型都可以拥有方法。 举个例子,我们基于内置的int类型使用type关键字可以定义新的自定义类型,然后为我们的自定义类型添加方法。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//MyInt 将int定义为自定义MyInt类型</span></span><br><span class="line"><span class="keyword">type</span> MyInt <span class="type">int</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//SayHello 为MyInt添加一个SayHello的方法</span></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(m MyInt)</span></span> SayHello() {</span><br><span class="line"> fmt.Println(<span class="string">"Hello, 我是一个int。"</span>)</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> m1 MyInt</span><br><span class="line"> m1.SayHello() <span class="comment">//Hello, 我是一个int。</span></span><br><span class="line"> m1 = <span class="number">100</span></span><br><span class="line"> fmt.Printf(<span class="string">"%#v %T\n"</span>, m1, m1) <span class="comment">//100 main.MyInt</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="4-嵌套结构体"><a href="#4-嵌套结构体" class="headerlink" title="4.嵌套结构体"></a>4.嵌套结构体</h3><p>一个结构体中可以嵌套包含另一个结构体或<a href="https://so.csdn.net/so/search?q=%E7%BB%93%E6%9E%84%E4%BD%93%E6%8C%87%E9%92%88&spm=1001.2101.3001.7020">结构体指针</a>。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//Address 地址结构体</span></span><br><span class="line"><span class="keyword">type</span> Address <span class="keyword">struct</span> {</span><br><span class="line"> Province <span class="type">string</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//Person 结构体</span></span><br><span class="line"><span class="keyword">type</span> Person <span class="keyword">struct</span> {</span><br><span class="line"> Name <span class="type">string</span></span><br><span class="line"> Sex <span class="type">string</span></span><br><span class="line"> Address Address</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> p1 := Person{</span><br><span class="line"> Name : <span class="string">"GOgo"</span>,</span><br><span class="line"> Sex : <span class="string">"男"</span>,</span><br><span class="line"> Address: Address{</span><br><span class="line"> Province: <span class="string">"河南"</span>,</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><br></pre></td></tr></table></figure><h3 id="5-嵌套结构体的字段冲突"><a href="#5-嵌套结构体的字段冲突" class="headerlink" title="5.嵌套结构体的字段冲突"></a>5.嵌套结构体的字段冲突</h3><p>嵌套结构体内部可能存在相同的字段名。这个时候为了避免歧义需要指定具体的内嵌结构体的字段。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//Address 地址结构体</span></span><br><span class="line"><span class="keyword">type</span> Address <span class="keyword">struct</span> {</span><br><span class="line"> Province <span class="type">string</span></span><br><span class="line"> City <span class="type">string</span></span><br><span class="line"> CreateTime <span class="type">string</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//Email 邮箱结构体</span></span><br><span class="line"><span class="keyword">type</span> Email <span class="keyword">struct</span> {</span><br><span class="line"> Account <span class="type">string</span></span><br><span class="line"> CreateTime <span class="type">string</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//User 用户结构体</span></span><br><span class="line"><span class="keyword">type</span> User <span class="keyword">struct</span> {</span><br><span class="line"> Name <span class="type">string</span></span><br><span class="line"> Gender <span class="type">string</span></span><br><span class="line"> Address</span><br><span class="line"> Email</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">var</span> user3 User</span><br><span class="line"> user3.Name = <span class="string">"pprof"</span></span><br><span class="line"> user3.Gender = <span class="string">"女"</span></span><br><span class="line"> <span class="comment">// user3.CreateTime = "2019" //ambiguous selector user3.CreateTime</span></span><br><span class="line"> user3.Address.CreateTime = <span class="string">"2000"</span> <span class="comment">//指定Address结构体中的CreateTime</span></span><br><span class="line"> user3.Email.CreateTime = <span class="string">"2000"</span> <span class="comment">//指定Email结构体中的CreateTime</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="6-结构体的“继承”"><a href="#6-结构体的“继承”" class="headerlink" title="6.结构体的“继承”"></a>6.结构体的“继承”</h3><p>Go语言中使用结构体也可以实现其他编程语言中面向对象的继承。</p><figure class="highlight go"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//Animal 动物</span></span><br><span class="line"><span class="keyword">type</span> Animal <span class="keyword">struct</span> {</span><br><span class="line"> name <span class="type">string</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(a *Animal)</span></span> move() {</span><br><span class="line"> fmt.Printf(<span class="string">"%s会动!\n"</span>, a.name)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//Dog 狗</span></span><br><span class="line"><span class="keyword">type</span> Dog <span class="keyword">struct</span> {</span><br><span class="line"> Feet <span class="type">int8</span></span><br><span class="line"> *Animal <span class="comment">//通过嵌套匿名结构体实现继承</span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="params">(d *Dog)</span></span> wang() {</span><br><span class="line"> fmt.Printf(<span class="string">"%s会汪汪汪~\n"</span>, d.name)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">func</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> d1 := &Dog{</span><br><span class="line"> Feet: <span class="number">4</span>,</span><br><span class="line"> Animal: &Animal{ <span class="comment">//注意嵌套的是结构体指针</span></span><br><span class="line"> name: <span class="string">"乐乐"</span>,</span><br><span class="line"> },</span><br><span class="line"> }</span><br><span class="line"> d1.wang() <span class="comment">//乐乐会汪汪汪~</span></span><br><span class="line"> d1.move() <span class="comment">//乐乐会动!</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> GoLand </tag>
</tags>
</entry>
<entry>
<title>nginx详解.md</title>
<link href="/2023/02/06/nginx%E8%AF%A6%E8%A7%A3-md/"/>
<url>/2023/02/06/nginx%E8%AF%A6%E8%A7%A3-md/</url>
<content type="html"><![CDATA[<h1 id="Nginx详解"><a href="#Nginx详解" class="headerlink" title="Nginx详解"></a>Nginx详解</h1><ul><li>引言</li><li>一、性能怪兽-Nginx概念深入浅出</li><li>二、Nginx环境搭建</li><li>三、Nginx反向代理-负载均衡</li><li>四、Nginx动静分离</li><li>五、Nginx资源压缩</li><li>六、Nginx缓冲区</li><li>七、Nginx缓存机制</li><li>八、Nginx实现IP黑白名单</li><li>九、Nginx跨域配置</li><li>十、Nginx防盗链设计</li><li>十一、Nginx大文件传输配置</li><li>十二、Nginx配置SLL证书</li><li>十三、Nginx的高可用</li><li>十四、Nginx性能优化</li><li>十五、放在最后的结尾</li></ul><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>早期的业务都是基于单体节点部署,由于前期访问流量不大,因此单体结构也可满足需求,但随着业务增长,流量也越来越大,那么最终单台服务器受到的访问压力也会逐步增高。时间一长,单台服务器性能无法跟上业务增长,就会造成线上频繁宕机的现象发生,最终导致系统瘫痪无法继续处理用户的请求。</p><blockquote><p>“</p><p>从上面的描述中,主要存在两个问题:①单体结构的部署方式无法承载日益增长的业务流量。②当后端节点宕机后,整个系统会陷入瘫痪,导致整个项目不可用。</p></blockquote><p>因此在这种背景下,引入负载均衡技术可带来的收益:</p><ul><li><strong>「系统的高可用:」</strong> 当某个节点宕机后可以迅速将流量转移至其他节点。</li><li><strong>「系统的高性能:」</strong> 多台服务器共同对外提供服务,为整个系统提供了更高规模的吞吐。</li><li><strong>「系统的拓展性:」</strong> 当业务再次出现增长或萎靡时,可再加入/减少节点,灵活伸缩。</li></ul><p>OK~,既然引入负载均衡技术可给我们带来如此巨大的好处,那么又有那些方案可供选择呢?主要有两种负载方案,<strong>「「硬件层面与软件层面」」</strong> ,比较常用的硬件负载器有<code>A10、F5</code>等,但这些机器动辄大几万乃至几十万的成本,因此一般大型企业会采用该方案,如银行、国企、央企等。而成本有限,但依旧想做负载均衡的项目,那么可在软件层面实现,如典型的<code>Nginx</code>等,软件层的负载也是本文的重点,毕竟<code>Boss</code>们的准则之一就是:<strong>「「能靠技术实现的就尽量不花钱。」」</strong></p><blockquote><p>“</p><p>当然,如果你认为本文对你而言有帮助,记得点赞、收藏、关注三连噢!</p></blockquote><h2 id="一、性能怪兽-Nginx概念深入浅出"><a href="#一、性能怪兽-Nginx概念深入浅出" class="headerlink" title="一、性能怪兽-Nginx概念深入浅出"></a>一、性能怪兽-Nginx概念深入浅出</h2><p><code>Nginx</code>是目前负载均衡技术中的主流方案,几乎绝大部分项目都会使用它,<code>Nginx</code>是一个轻量级的高性能<code>HTTP</code>反向代理服务器,同时它也是一个通用类型的代理服务器,支持绝大部分协议,如<code>TCP、UDP、SMTP、HTTPS</code>等。</p><p><img src="/../images/image-20230205142503114.png" alt="image-20230205142503114"></p><p><code>Nginx</code>与Redis相同,都是基于多路复用模型构建出的产物,因此它与<code>Redis</code>同样具备 <strong>「「资源占用少、并发支持高」」</strong> 的特点,在理论上单节点的<code>Nginx</code>同时支持<code>5W</code>并发连接,而实际生产环境中,硬件基础到位再结合简单调优后确实能达到该数值。</p><p>先来看看<code>Nginx</code>引入前后,客户端请求处理流程的对比:</p><p><img src="/../images/image-20230205142536692.png" alt="image-20230205142536692"></p><p>原本客户端是直接请求目标服务器,由目标服务器直接完成请求处理工作,但加入<code>Nginx</code>后,所有的请求会先经过<code>Nginx</code>,再由其进行分发到具体的服务器处理,处理完成后再返回<code>Nginx</code>,最后由<code>Nginx</code>将最终的响应结果返回给客户端。</p><p>了解了<code>Nginx</code>的基本概念后,再来快速搭建一下环境,以及了解一些<code>Nginx</code>的高级特性,如动静分离、资源压缩、缓存配置、<code>IP</code>黑名单、高可用保障等。</p><h2 id="二、Nginx环境搭建"><a href="#二、Nginx环境搭建" class="headerlink" title="二、Nginx环境搭建"></a>二、Nginx环境搭建</h2><p>❶首先创建<code>Nginx</code>的目录并进入:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[root<span class="meta">@localhost</span>]# mkdir /soft && mkdir /soft/nginx/ </span><br><span class="line">[root<span class="meta">@localhost</span>]# cd /soft/nginx/ </span><br></pre></td></tr></table></figure><p>❷下载<code>Nginx</code>的安装包,可以通过<code>FTP</code>工具上传离线环境包,也可通过<code>wget</code>命令在线获取安装包:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[root<span class="meta">@localhost</span>]# wget https:<span class="comment">//nginx.org/download/nginx-1.21.6.tar.gz </span></span><br></pre></td></tr></table></figure><p>没有<code>wget</code>命令的可通过<code>yum</code>命令安装:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[root<span class="meta">@localhost</span>]# yum -y install wget </span><br></pre></td></tr></table></figure><p>❸解压<code>Nginx</code>的压缩包:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[root<span class="meta">@localhost</span>]# tar -xvzf nginx-<span class="number">1.21</span><span class="number">.6</span>.tar.gz </span><br></pre></td></tr></table></figure><p>❹下载并安装<code>Nginx</code>所需的依赖库和包:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">[root<span class="meta">@localhost</span>]# yum install --downloadonly --downloaddir=/soft/nginx/ gcc-c++ </span><br><span class="line">[root<span class="meta">@localhost</span>]# yum install --downloadonly --downloaddir=/soft/nginx/ pcre pcre-devel4 </span><br><span class="line">[root<span class="meta">@localhost</span>]# yum install --downloadonly --downloaddir=/soft/nginx/ zlib zlib-devel </span><br><span class="line">[root<span class="meta">@localhost</span>]# yum install --downloadonly --downloaddir=/soft/nginx/ openssl openssl-devel </span><br></pre></td></tr></table></figure><p>也可以通过<code>yum</code>命令一键下载(推荐上面哪种方式):</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[root<span class="meta">@localhost</span>]# yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel </span><br></pre></td></tr></table></figure><p>执行完成后,然后<code>ls</code>查看目录文件,会看一大堆依赖:</p><p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/xVsWr7feY090btJAY42ARzO3YU9qHPJHq3VYXIibU9Moohr3Fv7BW9MS7Nw1Y1cLXIduaHQ2L3KWOl8EaxSjXdw/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><p>紧接着通过<code>rpm</code>命令依次将依赖包一个个构建,或者通过如下指令一键安装所有依赖包:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[root<span class="meta">@localhost</span>]# rpm -ivh --nodeps *.rpm </span><br></pre></td></tr></table></figure><p>❺进入解压后的<code>nginx</code>目录,然后执行<code>Nginx</code>的配置脚本,为后续的安装提前配置好环境,默认位于<code>/usr/local/nginx/</code>目录下(可自定义目录):</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cd</span> nginx-1.21.6</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">./configure --prefix=/soft/nginx/</span> </span><br></pre></td></tr></table></figure><p>❻编译并安装<code>Nginx</code>:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">make && make install</span> </span><br></pre></td></tr></table></figure><p>❼最后回到前面的<code>/soft/nginx/</code>目录,输入<code>ls</code>即可看见安装<code>nginx</code>完成后生成的文件。</p><p>❽修改安装后生成的<code>conf</code>目录下的<code>nginx.conf</code>配置文件:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">vi conf/nginx.conf</span> </span><br><span class="line"> 修改端口号:listen 80; </span><br><span class="line"> 修改IP地址:server_name 你当前机器的本地IP(线上配置域名); </span><br></pre></td></tr></table></figure><p>❾制定配置文件并启动<code>Nginx</code>:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">sbin/nginx -c conf/nginx.conf</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">ps aux | grep nginx</span> </span><br></pre></td></tr></table></figure><p><code>Nginx</code>其他操作命令:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">sbin/nginx -t -c conf/nginx.conf # 检测配置文件是否正常 </span><br><span class="line">sbin/nginx -s reload -c conf/nginx.conf # 修改配置后平滑重启 </span><br><span class="line">sbin/nginx -s quit # 优雅关闭Nginx,会在执行完当前的任务后再退出 </span><br><span class="line">sbin/nginx -s stop # 强制终止Nginx,不管当前是否有任务在执行 </span><br></pre></td></tr></table></figure><p>❿开放<code>80</code>端口,并更新防火墙:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">firewall-cmd --zone=public --add-port=80/tcp --permanent</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">firewall-cmd --reload</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">firewall-cmd --zone=public --list-ports</span> </span><br></pre></td></tr></table></figure><p>⓫在<code>Windows/Mac</code>的浏览器中,直接输入刚刚配置的<code>IP</code>地址访问<code>Nginx</code>:</p><p>最终看到如上的<code>Nginx</code>欢迎界面,代表<code>Nginx</code>安装完成。</p><h2 id="三、Nginx反向代理-负载均衡"><a href="#三、Nginx反向代理-负载均衡" class="headerlink" title="三、Nginx反向代理-负载均衡"></a>三、Nginx反向代理-负载均衡</h2><p>首先通过<code>SpringBoot+Freemarker</code>快速搭建一个<code>WEB</code>项目:springboot-web-nginx,然后在该项目中,创建一个<code>IndexNginxController.java</code>文件,逻辑如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><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_">IndexNginxController</span> { </span><br><span class="line"> <span class="meta">@Value("${server.port}")</span> </span><br><span class="line"> <span class="keyword">private</span> String port; </span><br><span class="line"> </span><br><span class="line"> <span class="meta">@RequestMapping("/")</span> </span><br><span class="line"> <span class="keyword">public</span> ModelAndView <span class="title function_">index</span><span class="params">()</span>{ </span><br><span class="line"> <span class="type">ModelAndView</span> <span class="variable">model</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ModelAndView</span>(); </span><br><span class="line"> model.addObject(<span class="string">"port"</span>, port); </span><br><span class="line"> model.setViewName(<span class="string">"index"</span>); </span><br><span class="line"> <span class="keyword">return</span> model; </span><br><span class="line"> } </span><br><span class="line">} </span><br></pre></td></tr></table></figure><p>在该<code>Controller</code>类中,存在一个成员变量:<code>port</code>,它的值即是从<code>application.properties</code>配置文件中获取<code>server.port</code>值。当出现访问<code>/</code>资源的请求时,跳转前端<code>index</code>页面,并将该值携带返回。</p><p>前端的<code>index.ftl</code>文件代码如下:</p><figure class="highlight html"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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="tag"><<span class="name">html</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">head</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">title</span>></span>Nginx演示页面<span class="tag"></<span class="name">title</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">link</span> <span class="attr">href</span>=<span class="string">"nginx_style.css"</span> <span class="attr">rel</span>=<span class="string">"stylesheet"</span> <span class="attr">type</span>=<span class="string">"text/css"</span>/></span> </span><br><span class="line"> <span class="tag"></<span class="name">head</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">body</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">style</span>=<span class="string">"border: 2px solid red;margin: auto;width: 800px;text-align: center"</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">div</span> <span class="attr">id</span>=<span class="string">"nginx_title"</span>></span> </span><br><span class="line"> <span class="tag"><<span class="name">h1</span>></span>欢迎来到熊猫高级会所,我是竹子${port}号!<span class="tag"></<span class="name">h1</span>></span> </span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span> </span><br><span class="line"> <span class="tag"></<span class="name">div</span>></span> </span><br><span class="line"> <span class="tag"></<span class="name">body</span>></span> </span><br><span class="line"><span class="tag"></<span class="name">html</span>></span> </span><br></pre></td></tr></table></figure><p>从上可以看出其逻辑并不复杂,仅是从响应中获取了<code>port</code>输出。</p><p>OK~,前提工作准备就绪后,再简单修改一下<code>nginx.conf</code>的配置即可:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">upstream</span> nginx_boot{ </span><br><span class="line"> <span class="comment"># 30s内检查心跳发送两次包,未回复就代表该机器宕机,请求分发权重比为1:2 </span></span><br><span class="line"> <span class="attribute">server</span> <span class="number">192.168.0.000:8080</span> weight=<span class="number">100</span> max_fails=<span class="number">2</span> fail_timeout=<span class="number">30s</span>; </span><br><span class="line"> <span class="attribute">server</span> <span class="number">192.168.0.000:8090</span> weight=<span class="number">200</span> max_fails=<span class="number">2</span> fail_timeout=<span class="number">30s</span>; </span><br><span class="line"> <span class="comment"># 这里的IP请配置成你WEB服务所在的机器IP </span></span><br><span class="line">} </span><br><span class="line"> </span><br><span class="line"><span class="section">server</span> { </span><br><span class="line"> <span class="section">location</span> / { </span><br><span class="line"> <span class="attribute">root</span> html; </span><br><span class="line"> <span class="comment"># 配置一下index的地址,最后加上index.ftl。</span></span><br><span class="line"> <span class="attribute">index</span> index.html index.htm index.jsp index.ftl; </span><br><span class="line"> <span class="attribute">proxy_set_header</span> Host <span class="variable">$host</span>; </span><br><span class="line"> <span class="attribute">proxy_set_header</span> X-Real-IP <span class="variable">$remote_addr</span>; </span><br><span class="line"> <span class="attribute">proxy_set_header</span> X-Forwarded-For <span class="variable">$proxy_add_x_forwarded_for</span>; </span><br><span class="line"> <span class="comment"># 请求交给名为nginx_boot的upstream上 </span></span><br><span class="line"> <span class="attribute">proxy_pass</span> http://nginx_boot; </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><blockquote><p>“</p><p>至此,所有的前提工作准备就绪,紧接着再启动<code>Nginx</code>,然后再启动两个<code>web</code>服务,第一个<code>WEB</code>服务启动时,在<code>application.properties</code>配置文件中,将端口号改为<code>8080</code>,第二个<code>WEB</code>服务启动时,将其端口号改为<code>8090</code>。</p></blockquote><p>最终来看看效果:</p><p><img src="/../images/image-20230205142718329.png" alt="image-20230205142718329"></p><p>因为配置了请求分发的权重,<code>8080、8090</code>的权重比为<code>2:1</code>,因此请求会根据权重比均摊到每台机器,也就是<code>8080</code>一次、<code>8090</code>两次、<code>8080</code>一次……</p><h4 id="Nginx请求分发原理"><a href="#Nginx请求分发原理" class="headerlink" title="Nginx请求分发原理"></a>Nginx请求分发原理</h4><p>客户端发出的请求<code>192.168.12.129</code>最终会转变为:<code>http://192.168.12.129:80/</code>,然后再向目标<code>IP</code>发起请求,流程如下:</p><p><img src="/../images/image-20230205142743176.png" alt="image-20230205142743176"></p><ul><li>由于<code>Nginx</code>监听了<code>192.168.12.129</code>的<code>80</code>端口,所以最终该请求会找到<code>Nginx</code>进程;</li><li><code>Nginx</code>首先会根据配置的<code>location</code>规则进行匹配,根据客户端的请求路径<code>/</code>,会定位到<code>location /{}</code>规则;</li><li>然后根据该<code>location</code>中配置的<code>proxy_pass</code>会再找到名为<code>nginx_boot</code>的<code>upstream</code>;</li><li>最后根据<code>upstream</code>中的配置信息,将请求转发到运行<code>WEB</code>服务的机器处理,由于配置了多个<code>WEB</code>服务,且配置了权重值,因此<code>Nginx</code>会依次根据权重比分发请求。</li></ul><h2 id="四、Nginx动静分离"><a href="#四、Nginx动静分离" class="headerlink" title="四、Nginx动静分离"></a>四、Nginx动静分离</h2><p>动静分离应该是听的次数较多的性能优化方案,那先思考一个问题:<strong>「「为什么需要做动静分离呢?它带来的好处是什么?」」</strong> 其实这个问题也并不难回答,当你搞懂了网站的本质后,自然就理解了动静分离的重要性。先来以淘宝为例分析看看:</p><p><img src="/../images/image-20230205142818165.png" alt="image-20230205142818165"></p><p>当浏览器输入<code>www.taobao.com</code>访问淘宝首页时,打开开发者调试工具可以很明显的看到,首页加载会出现<code>100+</code>的请求数,而正常项目开发时,静态资源一般会放入到<code>resources/static/</code>目录下:</p><p><img src="/../images/image-20230205142918381.png" alt="image-20230205142918381"></p><p>在项目上线部署时,这些静态资源会一起打成包,那此时思考一个问题:<strong>「「假设淘宝也是这样干的,那么首页加载时的请求最终会去到哪儿被处理?」」</strong> 答案毋庸置疑,首页<code>100+</code>的所有请求都会来到部署<code>WEB</code>服务的机器处理,那则代表着一个客户端请求淘宝首页,就会对后端服务器造成<code>100+</code>的并发请求。毫无疑问,这对于后端服务器的压力是尤为巨大的。</p><blockquote><p>“</p><p>但此时不妨分析看看,首页<code>100+</code>的请求中,是不是至少有<code>60+</code>是属于<code>*.js、*.css、*.html、*.jpg.....</code>这类静态资源的请求呢?答案是<code>Yes</code>。</p></blockquote><p>既然有这么多请求属于静态的,这些资源大概率情况下,长时间也不会出现变动,那为何还要让这些请求到后端再处理呢?能不能在此之前就提前处理掉?当然<code>OK</code>,因此经过分析之后能够明确一点:<strong>「「做了动静分离之后,至少能够让后端服务减少一半以上的并发量。」」</strong> 到此时大家应该明白了动静分离能够带来的性能收益究竟有多大。</p><p>OK~,搞清楚动静分离的必要性之后,如何实现动静分离呢?其实非常简单,实战看看。</p><p>①先在部署<code>Nginx</code>的机器,<code>Nginx</code>目录下创建一个目录<code>static_resources</code>:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mkdir static_resources </span><br></pre></td></tr></table></figure><p>②将项目中所有的静态资源全部拷贝到该目录下,而后将项目中的静态资源移除重新打包。</p><p>③稍微修改一下<code>nginx.conf</code>的配置,增加一条<code>location</code>匹配规则:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> <span class="regexp">~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css)</span>{ </span><br><span class="line"> <span class="attribute">root</span> /soft/nginx/static_resources; </span><br><span class="line"> <span class="attribute">expires</span> <span class="number">7d</span>; </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>然后照常启动<code>nginx</code>和移除了静态资源的<code>WEB</code>服务,你会发现原本的样式、<code>js</code>效果、图片等依旧有效,如下:</p><p><img src="/../images/image-20230205143126938.png" alt="image-20230205143126938"></p><p>其中<code>static</code>目录下的<code>nginx_style.css</code>文件已被移除,但效果依旧存在(绿色字体+蓝色大边框):</p><p><img src="/../images/image-20230205143142089.png" alt="image-20230205143142089"></p><p><img src="/../images/image-20230205143155183.png" alt="image-20230205143155183"></p><p>最后解读一下那条location规则:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> <span class="regexp">~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css)</span></span><br></pre></td></tr></table></figure><ul><li><code>~</code>代表匹配时区分大小写</li><li><code>.*</code>代表任意字符都可以出现零次或多次,即资源名不限制</li><li><code>\.</code>代表匹配后缀分隔符.</li><li><code>(html|...|css)</code>代表匹配括号里所有静态资源类型</li></ul><p>综上所述,简单一句话概述:该配置表示匹配以<code>.html~.css</code>为后缀的所有资源请求。</p><p><strong>「最后提一嘴,也可以将静态资源上传到文件服务器中,然后<code>location</code>中配置一个新的<code>upstream</code>指向。」</strong></p><h2 id="五、Nginx资源压缩"><a href="#五、Nginx资源压缩" class="headerlink" title="五、Nginx资源压缩"></a>五、Nginx资源压缩</h2><p>建立在动静分离的基础之上,如果一个静态资源的<code>Size</code>越小,那么自然传输速度会更快,同时也会更节省带宽,因此我们在部署项目时,也可以通过<code>Nginx</code>对于静态资源实现压缩传输,一方面可以节省带宽资源,第二方面也可以加快响应速度并提升系统整体吞吐。</p><p>在<code>Nginx</code>也提供了三个支持资源压缩的模块<code>ngx_http_gzip_module、ngx_http_gzip_static_module、ngx_http_gunzip_module</code>,其中<code>ngx_http_gzip_module</code>属于内置模块,代表着可以直接使用该模块下的一些压缩指令,后续的资源压缩操作都基于该模块,先来看看压缩配置的一些参数/指令:</p><p><img src="/../images/image-20230206104654139.png" alt="image-20230206104654139"></p><p>了解了<code>Nginx</code>中的基本压缩配置后,接下来可以在<code>Nginx</code>中简单配置一下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">http{</span><br><span class="line"> <span class="comment"># 开启压缩机制</span></span><br><span class="line"> <span class="attribute">gzip</span> <span class="literal">on</span>;</span><br><span class="line"> <span class="comment"># 指定会被压缩的文件类型(也可自己配置其他类型)</span></span><br><span class="line"> <span class="attribute">gzip_types</span> text/plain application/javascript text/css application/xml text/javascript image/jpeg image/gif image/png;</span><br><span class="line"> <span class="comment"># 设置压缩级别,越高资源消耗越大,但压缩效果越好</span></span><br><span class="line"> <span class="attribute">gzip_comp_level</span> <span class="number">5</span>;</span><br><span class="line"> <span class="comment"># 在头部中添加Vary: Accept-Encoding(建议开启)</span></span><br><span class="line"> <span class="attribute">gzip_vary</span> <span class="literal">on</span>;</span><br><span class="line"> <span class="comment"># 处理压缩请求的缓冲区数量和大小</span></span><br><span class="line"> <span class="attribute">gzip_buffers</span> <span class="number">16</span> <span class="number">8k</span>;</span><br><span class="line"> <span class="comment"># 对于不支持压缩功能的客户端请求不开启压缩机制</span></span><br><span class="line"> <span class="attribute">gzip_disable</span> <span class="string">"MSIE [1-6]\."</span>; <span class="comment"># 低版本的IE浏览器不支持压缩</span></span><br><span class="line"> <span class="comment"># 设置压缩响应所支持的HTTP最低版本</span></span><br><span class="line"> <span class="attribute">gzip_http_version</span> <span class="number">1</span>.<span class="number">1</span>;</span><br><span class="line"> <span class="comment"># 设置触发压缩的最小阈值</span></span><br><span class="line"> <span class="attribute">gzip_min_length</span> <span class="number">2k</span>;</span><br><span class="line"> <span class="comment"># 关闭对后端服务器的响应结果进行压缩</span></span><br><span class="line"> <span class="attribute">gzip_proxied</span> <span class="literal">off</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上述的压缩配置中,最后一个<code>gzip_proxied</code>选项,可以根据系统的实际情况决定,总共存在多种选项:</p><ul><li><code>off</code>:关闭<code>Nginx</code>对后台服务器的响应结果进行压缩。</li><li><code>expired</code>:如果响应头中包含<code>Expires</code>信息,则开启压缩。</li><li><code>no-cache</code>:如果响应头中包含<code>Cache-Control:no-cache</code>信息,则开启压缩。</li><li><code>no-store</code>:如果响应头中包含<code>Cache-Control:no-store</code>信息,则开启压缩。</li><li><code>private</code>:如果响应头中包含<code>Cache-Control:private</code>信息,则开启压缩。</li><li><code>no_last_modified</code>:如果响应头中不包含<code>Last-Modified</code>信息,则开启压缩。</li><li><code>no_etag</code>:如果响应头中不包含<code>ETag</code>信息,则开启压缩。</li><li><code>auth</code>:如果响应头中包含<code>Authorization</code>信息,则开启压缩。</li><li><code>any</code>:无条件对后端的响应结果开启压缩机制。</li></ul><p>OK~,简单修改好了<code>Nginx</code>的压缩配置后,可以在原本的<code>index</code>页面中引入一个<code>jquery-3.6.0.js</code>文件:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><script type=<span class="string">"text/javascript"</span> src=<span class="string">"jquery-3.6.0.js"</span>></script> </span><br></pre></td></tr></table></figure><p>分别来对比下压缩前后的区别:</p><p><img src="/../images/image-20230206111323467.png" alt="image-20230206111323467"></p><p>从图中可以很明显看出,未开启压缩机制前访问时,<code>js</code>文件的原始大小为<code>230K</code>,当配置好压缩后再重启<code>Nginx</code>,会发现文件大小从<code>230KB→69KB</code>,效果立竿见影!</p><blockquote><p>“</p><p>注意点:①对于图片、视频类型的数据,会默认开启压缩机制,因此一般无需再次开启压缩。②对于<code>.js</code>文件而言,需要指定压缩类型为<code>application/javascript</code>,而并非<code>text/javascript、application/x-javascript</code>。</p></blockquote><h2 id="六、Nginx缓冲区"><a href="#六、Nginx缓冲区" class="headerlink" title="六、Nginx缓冲区"></a>六、Nginx缓冲区</h2><p>先来思考一个问题,接入<code>Nginx</code>的项目一般请求流程为:“客户端→<code>Nginx</code>→服务端”,在这个过程中存在两个连接:“客户端→<code>Nginx</code>、<code>Nginx</code>→服务端”,那么两个不同的连接速度不一致,就会影响用户的体验(比如浏览器的加载速度跟不上服务端的响应速度)。</p><p>其实也就类似电脑的内存跟不上<code>CPU</code>速度,所以对于用户造成的体验感极差,因此在<code>CPU</code>设计时都会加入三级高速缓冲区,用于缓解<code>CPU</code>和内存速率不一致的矛盾。在<code>Nginx</code>也同样存在缓冲区的机制,主要目的就在于:<strong>「「用来解决两个连接之间速度不匹配造成的问题」」</strong>,有了缓冲后,<code>Nginx</code>代理可暂存后端的响应,然后按需供给数据给客户端。先来看看一些关于缓冲区的配置项:</p><ul><li><p><code>proxy_buffering</code>:是否启用缓冲机制,默认为<code>on</code>关闭状态。</p></li><li><p><code>client_body_buffer_size</code>:设置缓冲客户端请求数据的内存大小。</p></li><li><p><code>proxy_buffers</code>:为每个请求/连接设置缓冲区的数量和大小,默认<code>4 4k/8k</code>。</p></li><li><p><code>proxy_buffer_size</code>:设置用于存储响应头的缓冲区大小。</p></li><li><p><code>proxy_busy_buffers_size</code>:在后端数据没有完全接收完成时,<code>Nginx</code>可以将<code>busy</code>状态的缓冲返回给客户端,该参数用来设置<code>busy</code>状态的<code>buffer</code>具体有多大,默认为<code>proxy_buffer_size*2</code>。</p></li><li><p><code>proxy_temp_path</code>:当内存缓冲区存满时,可以将数据临时存放到磁盘,该参数是设置存储缓冲数据的目录。</p></li><li><p><code>path</code>是临时目录的路径。</p></li><li></li><li><ul><li>语法:<code>proxy_temp_path path;</code> path是临时目录的路径</li></ul></li><li><p><code>proxy_temp_file_write_size</code>:设置每次写数据到临时文件的大小限制。</p></li><li><p><code>proxy_max_temp_file_size</code>:设置临时的缓冲目录中允许存储的最大容量。</p></li><li><p>非缓冲参数项:</p></li><li></li><li><ul><li><code>proxy_connect_timeout</code>:设置与后端服务器建立连接时的超时时间。</li><li><code>proxy_read_timeout</code>:设置从后端服务器读取响应数据的超时时间。</li><li><code>proxy_send_timeout</code>:设置向后端服务器传输请求数据的超时时间。</li></ul></li></ul><p>具体的<code>nginx.conf</code>配置如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">http{ </span><br><span class="line"> <span class="attribute">proxy_connect_timeout</span> <span class="number">10</span>; </span><br><span class="line"> <span class="attribute">proxy_read_timeout</span> <span class="number">120</span>; </span><br><span class="line"> <span class="attribute">proxy_send_timeout</span> <span class="number">10</span>; </span><br><span class="line"> <span class="attribute">proxy_buffering</span> <span class="literal">on</span>; </span><br><span class="line"> <span class="attribute">client_body_buffer_size</span> <span class="number">512k</span>; </span><br><span class="line"> <span class="attribute">proxy_buffers</span> <span class="number">4</span> <span class="number">64k</span>; </span><br><span class="line"> <span class="attribute">proxy_buffer_size</span> <span class="number">16k</span>; </span><br><span class="line"> <span class="attribute">proxy_busy_buffers_size</span> <span class="number">128k</span>; </span><br><span class="line"> <span class="attribute">proxy_temp_file_write_size</span> <span class="number">128k</span>; </span><br><span class="line"> <span class="attribute">proxy_temp_path</span> /soft/nginx/temp_buffer; </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上述的缓冲区参数,是基于每个请求分配的空间,而并不是所有请求的共享空间。当然,具体的参数值还需要根据业务去决定,要综合考虑机器的内存以及每个请求的平均数据大小。</p><blockquote><p>“</p><p>最后提一嘴:使用缓冲也可以减少即时传输带来的带宽消耗。</p></blockquote><h2 id="七、Nginx缓存机制"><a href="#七、Nginx缓存机制" class="headerlink" title="七、Nginx缓存机制"></a>七、Nginx缓存机制</h2><p>对于性能优化而言,缓存是一种能够大幅度提升性能的方案,因此几乎可以在各处都能看见缓存,如客户端缓存、代理缓存、服务器缓存等等,<code>Nginx</code>的缓存则属于代理缓存的一种。对于整个系统而言,加入缓存带来的优势额外明显:</p><ul><li>减少了再次向后端或文件服务器请求资源的带宽消耗。</li><li>降低了下游服务器的访问压力,提升系统整体吞吐。</li><li>缩短了响应时间,提升了加载速度,打开页面的速度更快。</li></ul><p>那么在<code>Nginx</code>中,又该如何配置代理缓存呢?先来看看缓存相关的配置项:</p><p><strong>「proxy_cache_path」</strong>:代理缓存的路径。</p><p>语法:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">proxy_cache_path path [levels=levels] [use_temp_path=on|off] keys_zone=name:size [inactive=time] [max_size=size] [manager_files=number] [manager_sleep=time] [manager_threshold=time] [loader_files=number] [loader_sleep=time] [loader_threshold=time] [purger=on|off] [purger_files=number] [purger_sleep=time] [purger_threshold=time];</span><br></pre></td></tr></table></figure><p>是的,你没有看错,就是这么长….,解释一下每个参数项的含义:</p><ul><li><code>path</code>:缓存的路径地址。</li><li><code>levels</code>:缓存存储的层次结构,最多允许三层目录。</li><li><code>use_temp_path</code>:是否使用临时目录。</li><li><code>keys_zone</code>:指定一个共享内存空间来存储热点Key(1M可存储8000个Key)。</li><li><code>inactive</code>:设置缓存多长时间未被访问后删除(默认是十分钟)。</li><li><code>max_size</code>:允许缓存的最大存储空间,超出后会基于LRU算法移除缓存,Nginx会创建一个Cache manager的进程移除数据,也可以通过purge方式。</li><li><code>manager_files</code>:manager进程每次移除缓存文件数量的上限。</li><li><code>manager_sleep</code>:manager进程每次移除缓存文件的时间上限。</li><li><code>manager_threshold</code>:manager进程每次移除缓存后的间隔时间。</li><li><code>loader_files</code>:重启Nginx载入缓存时,每次加载的个数,默认100。</li><li><code>loader_sleep</code>:每次载入时,允许的最大时间上限,默认200ms。</li><li><code>loader_threshold</code>:一次载入后,停顿的时间间隔,默认50ms。</li><li><code>purger</code>:是否开启purge方式移除数据。</li><li><code>purger_files</code>:每次移除缓存文件时的数量。</li><li><code>purger_sleep</code>:每次移除时,允许消耗的最大时间。</li><li><code>purger_threshold</code>:每次移除完成后,停顿的间隔时间。</li></ul><p><strong>「proxy_cache」</strong>:开启或关闭代理缓存,开启时需要指定一个共享内存区域。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache</span> zone | <span class="literal">off</span>;</span><br></pre></td></tr></table></figure><p>zone为内存区域的名称,即上面中keys_zone设置的名称。</p><p><strong>「proxy_cache_key」</strong>:定义如何生成缓存的键。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_key</span> string;</span><br></pre></td></tr></table></figure><p>string为生成Key的规则,如<code>$scheme$proxy_host$request_uri</code>。</p><p><strong>「proxy_cache_valid」</strong>:缓存生效的状态码与过期时间。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_valid</span> [code ...] time;</span><br></pre></td></tr></table></figure><p>code为状态码,time为有效时间,可以根据状态码设置不同的缓存时间。</p><p>例如:<code>proxy_cache_valid 200 302 30m;</code></p><p><strong>「proxy_cache_min_uses」</strong>:设置资源被请求多少次后被缓存。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_min_uses</span> number;</span><br></pre></td></tr></table></figure><p>number为次数,默认为1。</p><p><strong>「proxy_cache_use_stale」</strong>:当后端出现异常时,是否允许Nginx返回缓存作为响应。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_use_stale</span> <span class="literal">error</span>;</span><br></pre></td></tr></table></figure><p>error为错误类型,可配置<code>timeout|invalid_header|updating|http_500...</code>。</p><p><strong>「proxy_cache_lock」</strong>:对于相同的请求,是否开启锁机制,只允许一个请求发往后端。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_lock</span> <span class="literal">on</span> | <span class="literal">off</span>;</span><br></pre></td></tr></table></figure><p><strong>「proxy_cache_lock_timeout」</strong>:配置锁超时机制,超出规定时间后会释放请求。</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_lock_timeout</span> time;</span><br></pre></td></tr></table></figure><p><strong>「proxy_cache_methods」</strong>:设置对于那些HTTP方法开启缓存。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_methods</span> method;</span><br></pre></td></tr></table></figure><p>method为请求方法类型,如GET、HEAD等。</p><p><strong>「proxy_no_cache」</strong>:定义不存储缓存的条件,符合时不会保存。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_no_cache</span> string...;</span><br></pre></td></tr></table></figure><p>string为条件,例如<code>$cookie_nocache $arg_nocache $arg_comment;</code></p><p><strong>「proxy_cache_bypass」</strong>:定义不读取缓存的条件,符合时不会从缓存中读取。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">proxy_cache_bypass</span> string...;</span><br></pre></td></tr></table></figure><p>和上面<code>proxy_no_cache</code>的配置方法类似。</p><p><strong>「add_header」</strong>:往响应头中添加字段信息。</p><p>语法:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">add_header</span> fieldName fieldValue;</span><br></pre></td></tr></table></figure><p><strong>「$upstream_cache_status」</strong>:记录了缓存是否命中的信息,存在多种情况:</p><ul><li><code>MISS</code>:请求未命中缓存。</li><li><code>HIT</code>:请求命中缓存。</li><li><code>EXPIRED</code>:请求命中缓存但缓存已过期。</li><li><code>STALE</code>:请求命中了陈旧缓存。</li><li><code>REVALIDDATED</code>:Nginx验证陈旧缓存依然有效。</li><li><code>UPDATING</code>:命中的缓存内容陈旧,但正在更新缓存。</li><li><code>BYPASS</code>:响应结果是从原始服务器获取的。</li></ul><blockquote><p>“</p><p>PS:这个和之前的不同,之前的都是参数项,这个是一个Nginx内置变量。</p></blockquote><p>OK~,对于<code>Nginx</code>中的缓存配置项大概了解后,接着来配置一下<code>Nginx</code>代理缓存:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line">http{ </span><br><span class="line"> <span class="comment"># 设置缓存的目录,并且内存中缓存区名为hot_cache,大小为128m, </span></span><br><span class="line"> <span class="comment"># 三天未被访问过的缓存自动清楚,磁盘中缓存的最大容量为2GB。</span></span><br><span class="line"> <span class="attribute">proxy_cache_path</span> /soft/nginx/cache levels=<span class="number">1</span>:<span class="number">2</span> keys_zone=hot_cache:<span class="number">128m</span> inactive=<span class="number">3d</span> max_size=<span class="number">2g</span>; </span><br><span class="line"> </span><br><span class="line"> server{ </span><br><span class="line"> <span class="section">location</span> / { </span><br><span class="line"> <span class="comment"># 使用名为nginx_cache的缓存空间 </span></span><br><span class="line"> <span class="attribute">proxy_cache</span> hot_cache; </span><br><span class="line"> <span class="comment"># 对于200、206、304、301、302状态码的数据缓存1天 </span></span><br><span class="line"> <span class="attribute">proxy_cache_valid</span> <span class="number">200</span> <span class="number">206</span> <span class="number">304</span> <span class="number">301</span> <span class="number">302</span> <span class="number">1d</span>; </span><br><span class="line"> <span class="comment"># 对于其他状态的数据缓存30分钟 </span></span><br><span class="line"> <span class="attribute">proxy_cache_valid</span> any <span class="number">30m</span>; </span><br><span class="line"> <span class="comment"># 定义生成缓存键的规则(请求的url+参数作为key) </span></span><br><span class="line"> <span class="attribute">proxy_cache_key</span> <span class="variable">$host</span><span class="variable">$uri</span><span class="variable">$is_args</span><span class="variable">$args</span>; </span><br><span class="line"> <span class="comment"># 资源至少被重复访问三次后再加入缓存 </span></span><br><span class="line"> <span class="attribute">proxy_cache_min_uses</span> <span class="number">3</span>; </span><br><span class="line"> <span class="comment"># 出现重复请求时,只让一个去后端读数据,其他的从缓存中读取 </span></span><br><span class="line"> <span class="attribute">proxy_cache_lock</span> <span class="literal">on</span>; </span><br><span class="line"> <span class="comment"># 上面的锁超时时间为3s,超过3s未获取数据,其他请求直接去后端 </span></span><br><span class="line"> <span class="attribute">proxy_cache_lock_timeout</span> <span class="number">3s</span>; </span><br><span class="line"> <span class="comment"># 对于请求参数或cookie中声明了不缓存的数据,不再加入缓存 </span></span><br><span class="line"> <span class="attribute">proxy_no_cache</span> <span class="variable">$cookie_nocache</span> <span class="variable">$arg_nocache</span> <span class="variable">$arg_comment</span>; </span><br><span class="line"> <span class="comment"># 在响应头中添加一个缓存是否命中的状态(便于调试) </span></span><br><span class="line"> <span class="attribute">add_header</span> Cache-status <span class="variable">$upstream_cache_status</span>; </span><br><span class="line"> } </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>接着来看一下效果,如下:</p><p><img src="/../images/image-20230206111830028.png" alt="image-20230206111830028"></p><p>第一次访问时,因为还没有请求过资源,所以缓存中没有数据,因此没有命中缓存。第二、三次,依旧没有命中缓存,直至第四次时才显示命中,这是为什么呢?因为在前面的缓存配置中,我们配置了加入缓存的最低条件为:<strong>「「资源至少要被请求三次以上才会加入缓存。」」</strong>这样可以避免很多无效缓存占用空间。</p><h4 id="缓存清理"><a href="#缓存清理" class="headerlink" title="缓存清理"></a>缓存清理</h4><p>当缓存过多时,如果不及时清理会导致磁盘空间被“吃光”,因此我们需要一套完善的缓存清理机制去删除缓存,在之前的<code>proxy_cache_path</code>参数中有<code>purger</code>相关的选项,开启后可以帮我们自动清理缓存,但遗憾的是:**<code>purger</code>系列参数只有商业版的<code>NginxPlus</code>才能使用,因此需要付费才可使用。**</p><p>不过天无绝人之路,我们可以通过强大的第三方模块<code>ngx_cache_purge</code>来替代,先来安装一下该插件:①首先去到<code>Nginx</code>的安装目录下,创建一个<code>cache_purge</code>目录:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">mkdir</span> cache_purge && <span class="built_in">cd</span> cache_purge</span> </span><br></pre></td></tr></table></figure><p>②通过<code>wget</code>指令从<code>github</code>上拉取安装包的压缩文件并解压:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">wget https://github.com/FRiCKLE/ngx_cache_purge/archive/2.3.tar.gz</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">tar -xvzf 2.3.tar.gz</span> </span><br></pre></td></tr></table></figure><p>③再次去到之前<code>Nginx</code>的解压目录下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cd</span> /soft/nginx/nginx1.21.6</span> </span><br></pre></td></tr></table></figure><p>④重新构建一次<code>Nginx</code>,通过<code>--add-module</code>的指令添加刚刚的第三方模块:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">./configure --prefix=/soft/nginx/ --add-module=/soft/nginx/cache_purge/ngx_cache_purge-2.3/</span> </span><br></pre></td></tr></table></figure><p>⑤重新根据刚刚构建的<code>Nginx</code>,再次编译一下,<strong>「但切记不要<code>make install</code>」</strong> :</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">make</span> </span><br></pre></td></tr></table></figure><p>⑥删除之前<code>Nginx</code>的启动文件,不放心的也可以移动到其他位置:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">rm</span> -rf /soft/nginx/sbin/nginx</span> </span><br></pre></td></tr></table></figure><p>⑦从生成的<code>objs</code>目录中,重新复制一个<code>Nginx</code>的启动文件到原来的位置:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cp</span> objs/nginx /soft/nginx/sbin/nginx</span> </span><br></pre></td></tr></table></figure><p>至此,第三方缓存清除模块<code>ngx_cache_purge</code>就安装完成了,接下来稍微修改一下<code>nginx.conf</code>配置,再添加一条<code>location</code>规则:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">location ~ /purge(/.*) { </span><br><span class="line"><span class="meta prompt_"> # </span><span class="language-bash">配置可以执行清除操作的IP(线上可以配置成内网机器)</span> </span><br><span class="line"><span class="meta prompt_"> # </span><span class="language-bash">allow 127.0.0.1; <span class="comment"># 代表本机</span></span> </span><br><span class="line"> allow all; # 代表允许任意IP清除缓存 </span><br><span class="line"> proxy_cache_purge $host$1$is_args$args; </span><br><span class="line">} </span><br></pre></td></tr></table></figure><p>然后再重启<code>Nginx</code>,接下来即可通过<code>http://xxx/purge/xx</code>的方式清除缓存。</p><h2 id="八、Nginx实现IP黑白名单"><a href="#八、Nginx实现IP黑白名单" class="headerlink" title="八、Nginx实现IP黑白名单"></a>八、Nginx实现IP黑白名单</h2><p>有时候往往有些需求,可能某些接口只能开放给对应的合作商,或者购买/接入<code>API</code>的合作伙伴,那么此时就需要实现类似于<code>IP</code>白名单的功能。而有时候有些恶意攻击者或爬虫程序,被识别后需要禁止其再次访问网站,因此也需要实现<code>IP</code>黑名单。那么这些功能无需交由后端实现,可直接在<code>Nginx</code>中处理。</p><p><code>Nginx</code>做黑白名单机制,主要是通过<code>allow、deny</code>配置项来实现:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">allow</span> xxx.xxx.xxx.xxx; <span class="comment"># 允许指定的IP访问,可以用于实现白名单。 </span></span><br><span class="line"><span class="attribute">deny</span> xxx.xxx.xxx.xxx; <span class="comment"># 禁止指定的IP访问,可以用于实现黑名单。 </span></span><br></pre></td></tr></table></figure><p>要同时屏蔽/开放多个<code>IP</code>访问时,如果所有<code>IP</code>全部写在<code>nginx.conf</code>文件中定然是不显示的,这种方式比较冗余,那么可以新建两个文件<code>BlocksIP.conf、WhiteIP.conf</code>:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># --------黑名单:BlocksIP.conf--------- </span></span><br><span class="line"><span class="attribute">deny</span> <span class="number">192.177.12.222</span>; <span class="comment"># 屏蔽192.177.12.222访问 </span></span><br><span class="line"><span class="attribute">deny</span> <span class="number">192.177.44.201</span>; <span class="comment"># 屏蔽192.177.44.201访问 </span></span><br><span class="line"><span class="attribute">deny</span> <span class="number">127.0.0.0</span>/<span class="number">8</span>; <span class="comment"># 屏蔽127.0.0.1到127.255.255.254网段中的所有IP访问 </span></span><br><span class="line"> </span><br><span class="line"><span class="comment"># --------白名单:WhiteIP.conf--------- </span></span><br><span class="line"><span class="attribute">allow</span> <span class="number">192.177.12.222</span>; <span class="comment"># 允许192.177.12.222访问 </span></span><br><span class="line"><span class="attribute">allow</span> <span class="number">192.177.44.201</span>; <span class="comment"># 允许192.177.44.201访问 </span></span><br><span class="line"><span class="attribute">allow</span> <span class="number">127.45.0.0</span>/<span class="number">16</span>; <span class="comment"># 允许127.45.0.1到127.45.255.254网段中的所有IP访问 </span></span><br><span class="line"><span class="attribute">deny</span> all; <span class="comment"># 除开上述IP外,其他IP全部禁止访问 </span></span><br></pre></td></tr></table></figure><p>分别将要禁止/开放的<code>IP</code>添加到对应的文件后,可以再将这两个文件在<code>nginx.conf</code>中导入:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">http{ </span><br><span class="line"> <span class="comment"># 屏蔽该文件中的所有IP </span></span><br><span class="line"> <span class="attribute">include</span> /soft/nginx/IP/BlocksIP.conf; </span><br><span class="line"> server{ </span><br><span class="line"> <span class="section">location</span> xxx { </span><br><span class="line"> <span class="comment"># 某一系列接口只开放给白名单中的IP </span></span><br><span class="line"> <span class="attribute">include</span> /soft/nginx/IP/blockip.conf; </span><br><span class="line"> } </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于文件具体在哪儿导入,这个也并非随意的,如果要整站屏蔽/开放就在<code>http</code>中导入,如果只需要一个域名下屏蔽/开放就在<code>sever</code>中导入,如果只需要针对于某一系列接口屏蔽/开放<code>IP</code>,那么就在<code>location</code>中导入。</p><blockquote><p>“</p><p>当然,上述只是最简单的<code>IP</code>黑/白名单实现方式,同时也可以通过<code>ngx_http_geo_module、ngx_http_geo_module</code>第三方库去实现(这种方式可以按地区、国家进行屏蔽,并且提供了<code>IP</code>库)。</p></blockquote><h2 id="九、Nginx跨域配置"><a href="#九、Nginx跨域配置" class="headerlink" title="九、Nginx跨域配置"></a>九、Nginx跨域配置</h2><p>跨域问题在之前的单体架构开发中,其实是比较少见的问题,除非是需要接入第三方<code>SDK</code>时,才需要处理此问题。但随着现在前后端分离、分布式架构的流行,跨域问题也成为了每个Java开发必须要懂得解决的一个问题。</p><h4 id="跨域问题产生的原因"><a href="#跨域问题产生的原因" class="headerlink" title="跨域问题产生的原因"></a>跨域问题产生的原因</h4><p>产生跨域问题的主要原因就在于 <strong>「同源策略」</strong> ,为了保证用户信息安全,防止恶意网站窃取数据,同源策略是必须的,否则<code>cookie</code>可以共享。由于<code>http</code>无状态协议通常会借助<code>cookie</code>来实现有状态的信息记录,例如用户的身份/密码等,因此一旦<code>cookie</code>被共享,那么会导致用户的身份信息被盗取。</p><p>同源策略主要是指三点相同,<strong>「「协议+域名+端口」」</strong> 相同的两个请求,则可以被看做是同源的,但如果其中任意一点存在不同,则代表是两个不同源的请求,同源策略会限制了不同源之间的资源交互。</p><h4 id="Nginx解决跨域问题"><a href="#Nginx解决跨域问题" class="headerlink" title="Nginx解决跨域问题"></a>Nginx解决跨域问题</h4><p>弄明白了跨域问题的产生原因,接下来看看<code>Nginx</code>中又该如何解决跨域呢?其实比较简单,在<code>nginx.conf</code>中稍微添加一点配置即可:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">location</span> / { </span><br><span class="line"> <span class="comment"># 允许跨域的请求,可以自定义变量$http_origin,*表示所有 </span></span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Access-Control-Allow-Origin'</span> *; </span><br><span class="line"> <span class="comment"># 允许携带cookie请求 </span></span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Access-Control-Allow-Credentials'</span> <span class="string">'true'</span>; </span><br><span class="line"> <span class="comment"># 允许跨域请求的方法:GET,POST,OPTIONS,PUT </span></span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Access-Control-Allow-Methods'</span> <span class="string">'GET,POST,OPTIONS,PUT'</span>; </span><br><span class="line"> <span class="comment"># 允许请求时携带的头部信息,*表示所有 </span></span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Access-Control-Allow-Headers'</span> *; </span><br><span class="line"> <span class="comment"># 允许发送按段获取资源的请求 </span></span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Access-Control-Expose-Headers'</span> <span class="string">'Content-Length,Content-Range'</span>; </span><br><span class="line"> <span class="comment"># 一定要有!!!否则Post请求无法进行跨域!</span></span><br><span class="line"> <span class="comment"># 在发送Post跨域请求前,会以Options方式发送预检请求,服务器接受时才会正式请求 </span></span><br><span class="line"> <span class="attribute">if</span> (<span class="variable">$request_method</span> = <span class="string">'OPTIONS'</span>) { </span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Access-Control-Max-Age'</span> <span class="number">1728000</span>; </span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Content-Type'</span> <span class="string">'text/plain; charset=utf-8'</span>; </span><br><span class="line"> <span class="attribute">add_header</span> <span class="string">'Content-Length'</span> <span class="number">0</span>; </span><br><span class="line"> <span class="comment"># 对于Options方式的请求返回204,表示接受跨域请求 </span></span><br><span class="line"> <span class="attribute">return</span> <span class="number">204</span>; </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在<code>nginx.conf</code>文件加上如上配置后,跨域请求即可生效了。</p><blockquote><p>“</p><p>但如果后端是采用分布式架构开发的,有时候RPC调用也需要解决跨域问题,不然也同样会出现无法跨域请求的异常,因此可以在你的后端项目中,通过继承<code>HandlerInterceptorAdapter</code>类、实现<code>WebMvcConfigurer</code>接口、添加<code>@CrossOrgin</code>注解的方式实现接口之间的跨域配置。</p></blockquote><h2 id="十、Nginx防盗链设计"><a href="#十、Nginx防盗链设计" class="headerlink" title="十、Nginx防盗链设计"></a>十、Nginx防盗链设计</h2><p>首先了解一下何谓盗链:<strong>「「盗链即是指外部网站引入当前网站的资源对外展示」」</strong> ,来举个简单的例子理解:</p><blockquote><p>“</p><p>好比壁纸网站<code>X</code>站、<code>Y</code>站,<code>X</code>站是一点点去购买版权、签约作者的方式,从而积累了海量的壁纸素材,但<code>Y</code>站由于资金等各方面的原因,就直接通过<code><img src="X站/xxx.jpg" /></code>这种方式照搬了<code>X</code>站的所有壁纸资源,继而提供给用户下载。</p></blockquote><p>那么如果我们自己是这个<code>X</code>站的<code>Boss</code>,心中必然不爽,那么此时又该如何屏蔽这类问题呢?那么接下来要叙说的<strong>「「防盗链」」</strong> 登场了!</p><p><code>Nginx</code>的防盗链机制实现,跟一个头部字段:<code>Referer</code>有关,该字段主要描述了当前请求是从哪儿发出的,那么在<code>Nginx</code>中就可获取该值,然后判断是否为本站的资源引用请求,如果不是则不允许访问。<code>Nginx</code>中存在一个配置项为<code>valid_referers</code>,正好可以满足前面的需求,语法如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">valid_referers</span> <span class="literal">none</span> | <span class="literal">blocked</span> | server_names | string ...;</span><br></pre></td></tr></table></figure><ul><li><code>none</code>:表示接受没有<code>Referer</code>字段的<code>HTTP</code>请求访问。</li><li><code>blocked</code>:表示允许<code>http://</code>或<code>https//</code>以外的请求访问。</li><li><code>server_names</code>:资源的白名单,这里可以指定允许访问的域名。</li><li><code>string</code>:可自定义字符串,支配通配符、正则表达式写法。</li></ul><p>简单了解语法后,接下来的实现如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 在动静分离的location中开启防盗链机制 </span></span><br><span class="line"><span class="section">location</span> <span class="regexp">~ .*\.(html|htm|gif|jpg|jpeg|bmp|png|ico|txt|js|css)</span>{ </span><br><span class="line"> <span class="comment"># 最后面的值在上线前可配置为允许的域名地址 </span></span><br><span class="line"> <span class="attribute">valid_referers</span> <span class="literal">blocked</span> <span class="number">192.168.12.129</span>; </span><br><span class="line"> <span class="attribute">if</span> (<span class="variable">$invalid_referer</span>) { </span><br><span class="line"> <span class="comment"># 可以配置成返回一张禁止盗取的图片 </span></span><br><span class="line"> <span class="comment"># rewrite ^/ http://xx.xx.com/NO.jpg; </span></span><br><span class="line"> <span class="comment"># 也可直接返回403 </span></span><br><span class="line"> <span class="attribute">return</span> <span class="number">403</span>; </span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> <span class="attribute">root</span> /soft/nginx/static_resources; </span><br><span class="line"> <span class="attribute">expires</span> <span class="number">7d</span>; </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据上述中的内容配置后,就已经通过<code>Nginx</code>实现了最基本的防盗链机制,最后只需要额外重启一下就好啦!当然,对于防盗链机制实现这块,也有专门的第三方模块<code>ngx_http_accesskey_module</code>实现了更为完善的设计,感兴趣的小伙伴可以自行去看看。</p><blockquote><p>“</p><p>PS:防盗链机制也无法解决爬虫伪造<code>referers</code>信息的这种方式抓取数据。</p></blockquote><h2 id="十一、Nginx大文件传输配置"><a href="#十一、Nginx大文件传输配置" class="headerlink" title="十一、Nginx大文件传输配置"></a>十一、Nginx大文件传输配置</h2><p>在某些业务场景中需要传输一些大文件,但大文件传输时往往都会会出现一些<code>Bug</code>,比如文件超出限制、文件传输过程中请求超时等,那么此时就可以在<code>Nginx</code>稍微做一些配置,先来了解一些关于大文件传输时可能会用的配置项:</p><p><img src="/../images/image-20230206132910430.png" alt="image-20230206132910430"></p><p>在传输大文件时,<code>client_max_body_size</code>、<code>client_header_timeout</code>、<code>proxy_read_timeout</code>、<code>proxy_send_timeout</code>这四个参数值都可以根据自己项目的实际情况来配置。</p><blockquote><p>“</p><p>上述配置仅是作为代理层需要配置的,因为最终客户端传输文件还是直接与后端进行交互,这里只是把作为网关层的<code>Nginx</code>配置调高一点,调到能够“容纳大文件”传输的程度。当然,<code>Nginx</code>中也可以作为文件服务器使用,但需要用到一个专门的第三方模块<code>nginx-upload-module</code>,如果项目中文件上传的作用处不多,那么建议可以通过<code>Nginx</code>搭建,毕竟可以节省一台文件服务器资源。但如若文件上传/下载较为频繁,那么还是建议额外搭建文件服务器,并将上传/下载功能交由后端处理。</p></blockquote><h2 id="十二、Nginx配置SLL证书"><a href="#十二、Nginx配置SLL证书" class="headerlink" title="十二、Nginx配置SLL证书"></a>十二、Nginx配置SLL证书</h2><p>随着越来越多的网站接入<code>HTTPS</code>,因此<code>Nginx</code>中仅配置<code>HTTP</code>还不够,往往还需要监听<code>443</code>端口的请求,<code>HTTPS</code>为了确保通信安全,所以服务端需配置对应的数字证书,当项目使用<code>Nginx</code>作为网关时,那么证书在<code>Nginx</code>中也需要配置,接下来简单聊一下关于<code>SSL</code>证书配置过程:</p><p>①先去CA机构或从云控制台中申请对应的<code>SSL</code>证书,审核通过后下载<code>Nginx</code>版本的证书。</p><p>②下载数字证书后,完整的文件总共有三个:<code>.crt、.key、.pem</code>:</p><ul><li><code>.crt</code>:数字证书文件,<code>.crt</code>是<code>.pem</code>的拓展文件,因此有些人下载后可能没有。</li><li><code>.key</code>:服务器的私钥文件,及非对称加密的私钥,用于解密公钥传输的数据。</li><li><code>.pem</code>:<code>Base64-encoded</code>编码格式的源证书文本文件,可自行根需求修改拓展名。</li></ul><p>③在<code>Nginx</code>目录下新建<code>certificate</code>目录,并将下载好的证书/私钥等文件上传至该目录。</p><p>④最后修改一下<code>nginx.conf</code>文件即可,如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># ----------HTTPS配置----------- </span></span><br><span class="line"><span class="section">server</span> { </span><br><span class="line"> <span class="comment"># 监听HTTPS默认的443端口 </span></span><br><span class="line"> <span class="attribute">listen</span> <span class="number">443</span>; </span><br><span class="line"> <span class="comment"># 配置自己项目的域名 </span></span><br><span class="line"> <span class="attribute">server_name</span> www.xxx.com; </span><br><span class="line"> <span class="comment"># 打开SSL加密传输 </span></span><br><span class="line"> <span class="attribute">ssl</span> <span class="literal">on</span>; </span><br><span class="line"> <span class="comment"># 输入域名后,首页文件所在的目录 </span></span><br><span class="line"> <span class="attribute">root</span> html; </span><br><span class="line"> <span class="comment"># 配置首页的文件名 </span></span><br><span class="line"> <span class="attribute">index</span> index.html index.htm index.jsp index.ftl; </span><br><span class="line"> <span class="comment"># 配置自己下载的数字证书 </span></span><br><span class="line"> <span class="attribute">ssl_certificate</span> certificate/xxx.pem; </span><br><span class="line"> <span class="comment"># 配置自己下载的服务器私钥 </span></span><br><span class="line"> <span class="attribute">ssl_certificate_key</span> certificate/xxx.key; </span><br><span class="line"> <span class="comment"># 停止通信时,加密会话的有效期,在该时间段内不需要重新交换密钥 </span></span><br><span class="line"> <span class="attribute">ssl_session_timeout</span> <span class="number">5m</span>; </span><br><span class="line"> <span class="comment"># TLS握手时,服务器采用的密码套件 </span></span><br><span class="line"> <span class="attribute">ssl_ciphers</span> ECDHE-RSA-AES128-GCM-SHA256:ECDHE:ECDH:AES:HIGH:!NULL:!aNULL:!MD5:!ADH:!RC4; </span><br><span class="line"> <span class="comment"># 服务器支持的TLS版本 </span></span><br><span class="line"> <span class="attribute">ssl_protocols</span> TLSv1 TLSv1.<span class="number">1</span> TLSv1.<span class="number">2</span>; </span><br><span class="line"> <span class="comment"># 开启由服务器决定采用的密码套件 </span></span><br><span class="line"> <span class="attribute">ssl_prefer_server_ciphers</span> <span class="literal">on</span>; </span><br><span class="line"> </span><br><span class="line"> <span class="section">location</span> / { </span><br><span class="line"> .... </span><br><span class="line"> } </span><br><span class="line">} </span><br><span class="line"> </span><br><span class="line"><span class="comment"># ---------HTTP请求转HTTPS------------- </span></span><br><span class="line"><span class="section">server</span> { </span><br><span class="line"> <span class="comment"># 监听HTTP默认的80端口 </span></span><br><span class="line"> <span class="attribute">listen</span> <span class="number">80</span>; </span><br><span class="line"> <span class="comment"># 如果80端口出现访问该域名的请求 </span></span><br><span class="line"> <span class="attribute">server_name</span> www.xxx.com; </span><br><span class="line"> <span class="comment"># 将请求改写为HTTPS(这里写你配置了HTTPS的域名) </span></span><br><span class="line"> <span class="attribute">rewrite</span><span class="regexp"> ^(.*)$</span> https://www.xxx.com; </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>OK~,根据如上配置了<code>Nginx</code>后,你的网站即可通过<code>https://</code>的方式访问,并且当客户端使用<code>http://</code>的方式访问时,会自动将其改写为<code>HTTPS</code>请求。</p><h2 id="十三、Nginx的高可用"><a href="#十三、Nginx的高可用" class="headerlink" title="十三、Nginx的高可用"></a>十三、Nginx的高可用</h2><p>线上如果采用单个节点的方式部署<code>Nginx</code>,难免会出现天灾人祸,比如系统异常、程序宕机、服务器断电、机房爆炸、地球毁灭….哈哈哈,夸张了。但实际生产环境中确实存在隐患问题,由于<code>Nginx</code>作为整个系统的网关层接入外部流量,所以一旦<code>Nginx</code>宕机,最终就会导致整个系统不可用,这无疑对于用户的体验感是极差的,因此也得保障<code>Nginx</code>高可用的特性。</p><blockquote><p>“</p><p>接下来则会通过<code>keepalived</code>的<code>VIP</code>机制,实现<code>Nginx</code>的高可用。<code>VIP</code>并不是只会员的意思,而是指<code>Virtual IP</code>,即虚拟<code>IP</code>。</p></blockquote><p><code>keepalived</code>在之前单体架构开发时,是一个用的较为频繁的高可用技术,比如<code>MySQL、Redis、MQ、Proxy、Tomcat</code>等各处都会通过<code>keepalived</code>提供的<code>VIP</code>机制,实现单节点应用的高可用。</p><h4 id="Keepalived-重启脚本-双机热备搭建"><a href="#Keepalived-重启脚本-双机热备搭建" class="headerlink" title="Keepalived+重启脚本+双机热备搭建"></a>Keepalived+重启脚本+双机热备搭建</h4><p>①首先创建一个对应的目录并下载<code>keepalived</code>到<code>Linux</code>中并解压:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">mkdir</span> /soft/keepalived && <span class="built_in">cd</span> /soft/keepalived</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">wget https://www.keepalived.org/software/keepalived-2.2.4.tar.gz</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">tar -zxvf keepalived-2.2.4.tar.gz</span> </span><br></pre></td></tr></table></figure><p>②进入解压后的<code>keepalived</code>目录并构建安装环境,然后编译并安装:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cd</span> keepalived-2.2.4</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">./configure --prefix=/soft/keepalived/</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">make && make install</span> </span><br></pre></td></tr></table></figure><p>③进入安装目录的<code>/soft/keepalived/etc/keepalived/</code>并编辑配置文件:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cd</span> /soft/keepalived/etc/keepalived/</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">vi keepalived.conf</span> </span><br></pre></td></tr></table></figure><p>④编辑主机的<code>keepalived.conf</code>核心配置文件,如下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line">global_defs { </span><br><span class="line"> # 自带的邮件提醒服务,建议用独立的监控或第三方SMTP,也可选择配置邮件发送。</span><br><span class="line"> notification_email { </span><br><span class="line"> root@localhost </span><br><span class="line"> } </span><br><span class="line"> notification_email_from root@localhost </span><br><span class="line"> smtp_server localhost </span><br><span class="line"> smtp_connect_timeout 30 </span><br><span class="line"> # 高可用集群主机身份标识(集群中主机身份标识名称不能重复,建议配置成本机IP) </span><br><span class="line"> router_id 192.168.12.129 </span><br><span class="line">} </span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">定时运行的脚本文件配置</span> </span><br><span class="line">vrrp_script check_nginx_pid_restart { </span><br><span class="line"> # 之前编写的nginx重启脚本的所在位置 </span><br><span class="line"> script "/soft/scripts/keepalived/check_nginx_pid_restart.sh" </span><br><span class="line"> # 每间隔3秒执行一次 </span><br><span class="line"> interval 3 </span><br><span class="line"> # 如果脚本中的条件成立,重启一次则权重-20 </span><br><span class="line"> weight -20 </span><br><span class="line">} </span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">定义虚拟路由,VI_1为虚拟路由的标示符(可自定义名称)</span> </span><br><span class="line">vrrp_instance VI_1 { </span><br><span class="line"> # 当前节点的身份标识:用来决定主从(MASTER为主机,BACKUP为从机) </span><br><span class="line"> state MASTER </span><br><span class="line"> # 绑定虚拟IP的网络接口,根据自己的机器的网卡配置 </span><br><span class="line"> interface ens33 </span><br><span class="line"> # 虚拟路由的ID号,主从两个节点设置必须一样 </span><br><span class="line"> virtual_router_id 121 </span><br><span class="line"> # 填写本机IP </span><br><span class="line"> mcast_src_ip 192.168.12.129 </span><br><span class="line"> # 节点权重优先级,主节点要比从节点优先级高 </span><br><span class="line"> priority 100 </span><br><span class="line"> # 优先级高的设置nopreempt,解决异常恢复后再次抢占造成的脑裂问题 </span><br><span class="line"> nopreempt </span><br><span class="line"> # 组播信息发送间隔,两个节点设置必须一样,默认1s(类似于心跳检测) </span><br><span class="line"> advert_int 1 </span><br><span class="line"> authentication { </span><br><span class="line"> auth_type PASS </span><br><span class="line"> auth_pass 1111 </span><br><span class="line"> } </span><br><span class="line"> # 将track_script块加入instance配置块 </span><br><span class="line"> track_script { </span><br><span class="line"> # 执行Nginx监控的脚本 </span><br><span class="line"> check_nginx_pid_restart </span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> virtual_ipaddress { </span><br><span class="line"> # 虚拟IP(VIP),也可扩展,可配置多个。</span><br><span class="line"> 192.168.12.111 </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>⑤克隆一台之前的虚拟机作为从(备)机,编辑从机的<code>keepalived.conf</code>文件,如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">global_defs</span> { </span><br><span class="line"> <span class="comment"># 自带的邮件提醒服务,建议用独立的监控或第三方SMTP,也可选择配置邮件发送。</span></span><br><span class="line"> <span class="section">notification_email</span> { </span><br><span class="line"> root@<span class="attribute">localhost</span> </span><br><span class="line"> } </span><br><span class="line"> notification_email_from root<span class="variable">@localhost</span> </span><br><span class="line"> smtp_server localhost </span><br><span class="line"> smtp_connect_timeout <span class="number">30</span> </span><br><span class="line"> <span class="comment"># 高可用集群主机身份标识(集群中主机身份标识名称不能重复,建议配置成本机IP) </span></span><br><span class="line"> router_id <span class="number">192.168.12.130</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">vrrp_script check_nginx_pid_restart { </span><br><span class="line"> <span class="comment"># 之前编写的nginx重启脚本的所在位置 </span></span><br><span class="line"> <span class="attribute">script</span> <span class="string">"/soft/scripts/keepalived/check_nginx_pid_restart.sh"</span> </span><br><span class="line"> <span class="comment"># 每间隔3秒执行一次 </span></span><br><span class="line"> interval <span class="number">3</span> </span><br><span class="line"> <span class="comment"># 如果脚本中的条件成立,重启一次则权重-20 </span></span><br><span class="line"> weight -<span class="number">20</span> </span><br><span class="line">} </span><br><span class="line"> </span><br><span class="line"><span class="comment"># 定义虚拟路由,VI_1为虚拟路由的标示符(可自定义名称) </span></span><br><span class="line">vrrp_instance VI_1 { </span><br><span class="line"> <span class="comment"># 当前节点的身份标识:用来决定主从(MASTER为主机,BACKUP为从机) </span></span><br><span class="line"> <span class="attribute">state</span> BACKUP </span><br><span class="line"> <span class="comment"># 绑定虚拟IP的网络接口,根据自己的机器的网卡配置 </span></span><br><span class="line"> interface ens33 </span><br><span class="line"> <span class="comment"># 虚拟路由的ID号,主从两个节点设置必须一样 </span></span><br><span class="line"> virtual_router_id <span class="number">121</span> </span><br><span class="line"> <span class="comment"># 填写本机IP </span></span><br><span class="line"> mcast_src_ip <span class="number">192.168.12.130</span> </span><br><span class="line"> <span class="comment"># 节点权重优先级,主节点要比从节点优先级高 </span></span><br><span class="line"> priority <span class="number">90</span> </span><br><span class="line"> <span class="comment"># 优先级高的设置nopreempt,解决异常恢复后再次抢占造成的脑裂问题 </span></span><br><span class="line"> nopreempt </span><br><span class="line"> <span class="comment"># 组播信息发送间隔,两个节点设置必须一样,默认1s(类似于心跳检测) </span></span><br><span class="line"> advert_int <span class="number">1</span> </span><br><span class="line"> authentication { </span><br><span class="line"> <span class="attribute">auth_type</span> PASS </span><br><span class="line"> auth_pass <span class="number">1111</span> </span><br><span class="line"> } </span><br><span class="line"> <span class="comment"># 将track_script块加入instance配置块 </span></span><br><span class="line"> track_script { </span><br><span class="line"> <span class="comment"># 执行Nginx监控的脚本 </span></span><br><span class="line"> <span class="attribute">check_nginx_pid_restart</span> </span><br><span class="line"> } </span><br><span class="line"> </span><br><span class="line"> virtual_ipaddress { </span><br><span class="line"> <span class="comment"># 虚拟IP(VIP),也可扩展,可配置多个。</span></span><br><span class="line"> 192.168.12.111 </span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>⑥新建<code>scripts</code>目录并编写<code>Nginx</code>的重启脚本,<code>check_nginx_pid_restart.sh</code>:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">mkdir</span> /soft/scripts /soft/scripts/keepalived</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">touch</span> /soft/scripts/keepalived/check_nginx_pid_restart.sh</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">vi /soft/scripts/keepalived/check_nginx_pid_restart.sh</span> </span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_">#</span><span class="language-bash">!/bin/sh</span> </span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">通过ps指令查询后台的nginx进程数,并将其保存在变量nginx_number中</span> </span><br><span class="line">nginx_number=`ps -C nginx --no-header | wc -l` </span><br><span class="line"><span class="meta prompt_"># </span><span class="language-bash">判断后台是否还有Nginx进程在运行</span> </span><br><span class="line">if [ $nginx_number -eq 0 ];then </span><br><span class="line"> # 如果后台查询不到`Nginx`进程存在,则执行重启指令 </span><br><span class="line"> /soft/nginx/sbin/nginx -c /soft/nginx/conf/nginx.conf </span><br><span class="line"> # 重启后等待1s后,再次查询后台进程数 </span><br><span class="line"> sleep 1 </span><br><span class="line"> # 如果重启后依旧无法查询到nginx进程 </span><br><span class="line"> if [ `ps -C nginx --no-header | wc -l` -eq 0 ];then </span><br><span class="line"> # 将keepalived主机下线,将虚拟IP漂移给从机,从机上线接管Nginx服务 </span><br><span class="line"> systemctl stop keepalived.service </span><br><span class="line"> fi </span><br><span class="line">fi </span><br></pre></td></tr></table></figure><p>⑦编写的脚本文件需要更改编码格式,并赋予执行权限,否则可能执行失败:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">vi /soft/scripts/keepalived/check_nginx_pid_restart.sh</span> </span><br><span class="line"> </span><br><span class="line">:set fileformat=unix # 在vi命令里面执行,修改编码格式 </span><br><span class="line">:set ff # 查看修改后的编码格式 </span><br><span class="line"><span class="meta prompt_"> </span></span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">chmod</span> +x /soft/scripts/keepalived/check_nginx_pid_restart.sh</span> </span><br></pre></td></tr></table></figure><p>⑧由于安装<code>keepalived</code>时,是自定义的安装位置,因此需要拷贝一些文件到系统目录中:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">mkdir</span> /etc/keepalived/</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cp</span> /soft/keepalived/etc/keepalived/keepalived.conf /etc/keepalived/</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cp</span> /soft/keepalived/keepalived-2.2.4/keepalived/etc/init.d/keepalived /etc/init.d/</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash"><span class="built_in">cp</span> /soft/keepalived/etc/sysconfig/keepalived /etc/sysconfig/</span> </span><br></pre></td></tr></table></figure><p>⑨将<code>keepalived</code>加入系统服务并设置开启自启动,然后测试启动是否正常:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">chkconfig keepalived on</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">systemctl daemon-reload</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">systemctl <span class="built_in">enable</span> keepalived.service</span> </span><br><span class="line"><span class="meta prompt_">[root@localhost]# </span><span class="language-bash">systemctl start keepalived.service</span> </span><br></pre></td></tr></table></figure><p>其他命令:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">systemctl disable keepalived.service # 禁止开机自动启动 </span><br><span class="line">systemctl restart keepalived.service # 重启keepalived </span><br><span class="line">systemctl stop keepalived.service # 停止keepalived </span><br><span class="line">tail -f /var/log/messages # 查看keepalived运行时日志 </span><br></pre></td></tr></table></figure><p>⑩最后测试一下<code>VIP</code>是否生效,通过查看本机是否成功挂载虚拟<code>IP</code>:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[root@localhost]# ip addr </span><br></pre></td></tr></table></figure><p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/xVsWr7feY090btJAY42ARzO3YU9qHPJHIb4K4zOBC09ibSLECeNzXcykoMGWjBSoibRjr6RUWKvicrkmP7DWxibiaHQ/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><blockquote><p>“</p><p>从上图中可以明显看见虚拟<code>IP</code>已经成功挂载,但另外一台机器<code>192.168.12.130</code>并不会挂载这个虚拟<code>IP</code>,只有当主机下线后,作为从机的<code>192.168.12.130</code>才会上线,接替<code>VIP</code>。最后测试一下外网是否可以正常与<code>VIP</code>通信,即在<code>Windows</code>中直接<code>ping VIP</code>:</p></blockquote><p><img src="https://mmbiz.qpic.cn/mmbiz_png/GjuWRiaNxhnR3QRibEiaAcrUzDMywtgjrxGPtZ4pjiboFzf87TTxyWWhL2bsW7pwu0eHHRLzf2wlXXZqer31ym8mibw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><p>外部通过<code>VIP</code>通信时,也可以正常<code>Ping</code>通,代表虚拟<code>IP</code>配置成功。</p><h4 id="Nginx高可用性测试"><a href="#Nginx高可用性测试" class="headerlink" title="Nginx高可用性测试"></a>Nginx高可用性测试</h4><p>经过上述步骤后,<code>keepalived</code>的<code>VIP</code>机制已经搭建成功,在上个阶段中主要做了几件事:</p><ul><li>一、为部署<code>Nginx</code>的机器挂载了<code>VIP</code>。</li><li>二、通过<code>keepalived</code>搭建了主从双机热备。</li><li>三、通过<code>keepalived</code>实现了<code>Nginx</code>宕机重启。</li></ul><p>由于前面没有域名的原因,因此最初<code>server_name</code>配置的是当前机器的<code>IP</code>,所以需稍微更改一下<code>nginx.conf</code>的配置:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">sever{ </span><br><span class="line"> <span class="attribute">listen</span> <span class="number">80</span>; </span><br><span class="line"> <span class="comment"># 这里从机器的本地IP改为虚拟IP </span></span><br><span class="line"> <span class="attribute">server_name</span> <span class="number">192.168.12.111</span>; </span><br><span class="line"> <span class="comment"># 如果这里配置的是域名,那么则将域名的映射配置改为虚拟IP </span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最后来实验一下效果:</p><p><img src="/../images/image-20230206133542283.png" alt="image-20230206133542283"></p><blockquote><p>“</p><p>在上述过程中,首先分别启动了<code>keepalived、nginx</code>服务,然后通过手动停止<code>nginx</code>的方式模拟了<code>Nginx</code>宕机情况,过了片刻后再次查询后台进程,我们会发现<code>nginx</code>依旧存活。</p></blockquote><p>从这个过程中不难发现,<code>keepalived</code>已经为我们实现了<code>Nginx</code>宕机后自动重启的功能,那么接着再模拟一下服务器出现故障时的情况:</p><p><img src="/../images/image-20230206133620120.png" alt="image-20230206133620120"></p><blockquote><p>“</p><p>在上述过程中,我们通过手动关闭<code>keepalived</code>服务模拟了机器断电、硬件损坏等情况(因为机器断电等情况=主机中的<code>keepalived</code>进程消失),然后再次查询了一下本机的<code>IP</code>信息,很明显会看到<code>VIP</code>消失了!</p></blockquote><p>现在再切换到另外一台机器:<code>192.168.12.130</code>来看看情况:</p><p>’ fill=’%23FFFFFF’%3E%3Crect x=’249’ y=’126’ width=’1’ height=’1’%3E%3C/rect%3E%3C/g%3E%3C/g%3E%3C/svg%3E)</p><blockquote><p>“</p><p>此刻我们会发现,在主机<code>192.168.12.129</code>宕机后,VIP自动从主机飘移到了从机<code>192.168.12.130</code>上,而此时客户端的请求就最终会来到<code>130</code>这台机器的<code>Nginx</code>上。</p></blockquote><p><strong>「「最终,利用<code>Keepalived</code>对<code>Nginx</code>做了主从热备之后,无论是遇到线上宕机还是机房断电等各类故障时,都能够确保应用系统能够为用户提供<code>7x24</code>小时服务。」」</strong></p><h2 id="十四、Nginx性能优化"><a href="#十四、Nginx性能优化" class="headerlink" title="十四、Nginx性能优化"></a>十四、Nginx性能优化</h2><p>到这里文章的篇幅较长了,最后再来聊一下关于<code>Nginx</code>的性能优化,主要就简单说说收益最高的几个优化项,在这块就不再展开叙述了,毕竟影响性能都有多方面原因导致的,比如网络、服务器硬件、操作系统、后端服务、程序自身、数据库服务等。</p><h4 id="优化一:打开长连接配置"><a href="#优化一:打开长连接配置" class="headerlink" title="优化一:打开长连接配置"></a>优化一:打开长连接配置</h4><p>通常Nginx作为代理服务,负责分发客户端的请求,那么建议开启<code>HTTP</code>长连接,用户减少握手的次数,降低服务器损耗,具体如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">upstream</span> xxx { </span><br><span class="line"> <span class="comment"># 长连接数 </span></span><br><span class="line"> <span class="attribute">keepalive</span> <span class="number">32</span>; </span><br><span class="line"> <span class="comment"># 每个长连接提供的最大请求数 </span></span><br><span class="line"> <span class="attribute">keepalived_requests</span> <span class="number">100</span>; </span><br><span class="line"> <span class="comment"># 每个长连接没有新的请求时,保持的最长时间 </span></span><br><span class="line"> <span class="attribute">keepalive_timeout</span> <span class="number">60s</span>; </span><br><span class="line">} </span><br></pre></td></tr></table></figure><h4 id="优化二、开启零拷贝技术"><a href="#优化二、开启零拷贝技术" class="headerlink" title="优化二、开启零拷贝技术"></a>优化二、开启零拷贝技术</h4><p>零拷贝这个概念,在大多数性能较为不错的中间件中都有出现,例如<code>Kafka、Netty</code>等,而<code>Nginx</code>中也可以配置数据零拷贝技术,如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">sendfile</span> <span class="literal">on</span>; <span class="comment"># 开启零拷贝机制 </span></span><br></pre></td></tr></table></figure><p>零拷贝读取机制与传统资源读取机制的区别:</p><ul><li><strong>「传统方式:」</strong> 硬件–>内核–>用户空间–>程序空间–>程序内核空间–>网络套接字</li><li><strong>「零拷贝方式:」</strong> 硬件–>内核–>程序内核空间–>网络套接字</li></ul><p>从上述这个过程对比,很轻易就能看出两者之间的性能区别。</p><h4 id="优化三、开启无延迟或多包共发机制"><a href="#优化三、开启无延迟或多包共发机制" class="headerlink" title="优化三、开启无延迟或多包共发机制"></a>优化三、开启无延迟或多包共发机制</h4><p>在<code>Nginx</code>中有两个较为关键的性能参数,即<code>tcp_nodelay、tcp_nopush</code>,开启方式如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">tcp_nodelay</span> <span class="literal">on</span>; </span><br><span class="line"><span class="attribute">tcp_nopush</span> <span class="literal">on</span>; </span><br></pre></td></tr></table></figure><p><code>TCP/IP</code>协议中默认是采用了Nagle算法的,即在网络数据传输过程中,每个数据报文并不会立马发送出去,而是会等待一段时间,将后面的几个数据包一起组合成一个数据报文发送,但这个算法虽然提高了网络吞吐量,但是实时性却降低了。</p><blockquote><p>“</p><p>因此你的项目属于交互性很强的应用,那么可以手动开启<code>tcp_nodelay</code>配置,让应用程序向内核递交的每个数据包都会立即发送出去。但这样会产生大量的<code>TCP</code>报文头,增加很大的网络开销。</p></blockquote><p>相反,有些项目的业务对数据的实时性要求并不高,追求的则是更高的吞吐,那么则可以开启<code>tcp_nopush</code>配置项,这个配置就类似于“塞子”的意思,首先将连接塞住,使得数据先不发出去,等到拔去塞子后再发出去。设置该选项后,内核会尽量把小数据包拼接成一个大的数据包(一个<code>MTU</code>)再发送出去.</p><blockquote><p>“</p><p>当然若一定时间后(一般为<code>200ms</code>),内核仍然没有积累到一个<code>MTU</code>的量时,也必须发送现有的数据,否则会一直阻塞。</p></blockquote><p><code>tcp_nodelay、tcp_nopush</code>两个参数是“互斥”的,如果追求响应速度的应用推荐开启<code>tcp_nodelay</code>参数,如<code>IM</code>、金融等类型的项目。如果追求吞吐量的应用则建议开启<code>tcp_nopush</code>参数,如调度系统、报表系统等。</p><blockquote><p>“</p><p>注意:①<code>tcp_nodelay</code>一般要建立在开启了长连接模式的情况下使用。②<code>tcp_nopush</code>参数是必须要开启<code>sendfile</code>参数才可使用的。</p></blockquote><h4 id="优化四、调整Worker工作进程"><a href="#优化四、调整Worker工作进程" class="headerlink" title="优化四、调整Worker工作进程"></a>优化四、调整Worker工作进程</h4><p><code>Nginx</code>启动后默认只会开启一个<code>Worker</code>工作进程处理客户端请求,而我们可以根据机器的CPU核数开启对应数量的工作进程,以此来提升整体的并发量支持,如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 自动根据CPU核心数调整Worker进程数量 </span></span><br><span class="line"><span class="attribute">worker_processes</span> auto; </span><br></pre></td></tr></table></figure><blockquote><p>“</p><p>工作进程的数量最高开到<code>8</code>个就OK了,<code>8</code>个之后就不会有再大的性能提升。</p></blockquote><p>同时也可以稍微调整一下每个工作进程能够打开的文件句柄数:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment"># 每个Worker能打开的文件描述符,最少调整至1W以上,负荷较高建议2-3W </span></span><br><span class="line"><span class="attribute">worker_rlimit_nofile</span> <span class="number">20000</span>; </span><br></pre></td></tr></table></figure><blockquote><p>“</p><p>操作系统内核(<code>kernel</code>)都是利用文件描述符来访问文件,无论是打开、新建、读取、写入文件时,都需要使用文件描述符来指定待操作的文件,因此该值越大,代表一个进程能够操作的文件越多(但不能超出内核限制,最多建议<code>3.8W</code>左右为上限)。</p></blockquote><h4 id="优化五、开启CPU亲和机制"><a href="#优化五、开启CPU亲和机制" class="headerlink" title="优化五、开启CPU亲和机制"></a>优化五、开启CPU亲和机制</h4><p>对于并发编程较为熟悉的伙伴都知道,因为进程/线程数往往都会远超出系统CPU的核心数,因为操作系统执行的原理本质上是采用时间片切换机制,也就是一个CPU核心会在多个进程之间不断频繁切换,造成很大的性能损耗。</p><p>而CPU亲和机制则是指将每个<code>Nginx</code>的工作进程,绑定在固定的CPU核心上,从而减小CPU切换带来的时间开销和资源损耗,开启方式如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="attribute">worker_cpu_affinity</span> auto; </span><br></pre></td></tr></table></figure><h4 id="优化六、开启epoll模型及调整并发连接数"><a href="#优化六、开启epoll模型及调整并发连接数" class="headerlink" title="优化六、开启epoll模型及调整并发连接数"></a>优化六、开启epoll模型及调整并发连接数</h4><p>在最开始就提到过:<code>Nginx、Redis</code>都是基于多路复用模型去实现的程序,但最初版的多路复用模型<code>select/poll</code>最大只能监听<code>1024</code>个连接,而<code>epoll</code>则属于<code>select/poll</code>接口的增强版,因此采用该模型能够大程度上提升单个<code>Worker</code>的性能,如下:</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="section">events</span> { </span><br><span class="line"> <span class="comment"># 使用epoll网络模型 </span></span><br><span class="line"> <span class="attribute">use</span> <span class="literal">epoll</span>; </span><br><span class="line"> <span class="comment"># 调整每个Worker能够处理的连接数上限 </span></span><br><span class="line"> <span class="attribute">worker_connections</span> <span class="number">10240</span>; </span><br><span class="line">} </span><br></pre></td></tr></table></figure><blockquote><p>“</p><p>这里对于<code>select/poll/epoll</code>模型就不展开细说了,后面的IO模型文章中会详细剖析。</p></blockquote><h2 id="十五、放在最后的结尾"><a href="#十五、放在最后的结尾" class="headerlink" title="十五、放在最后的结尾"></a>十五、放在最后的结尾</h2><p>至此,<code>Nginx</code>的大部分内容都已阐述完毕,关于最后一小节的性能优化内容,其实在前面就谈到的动静分离、分配缓冲区、资源缓存、防盗链、资源压缩等内容,也都可归纳为性能优化的方案。</p>]]></content>
<tags>
<tag> Nginx </tag>
</tags>
</entry>
<entry>
<title>HTTP详解.md</title>
<link href="/2023/01/15/HTTP%E8%AF%A6%E8%A7%A3-md/"/>
<url>/2023/01/15/HTTP%E8%AF%A6%E8%A7%A3-md/</url>
<content type="html"><![CDATA[<h3 id="一-什么是HTTP协议:"><a href="#一-什么是HTTP协议:" class="headerlink" title="一. 什么是HTTP协议:"></a><strong>一. 什么是HTTP协议:</strong></h3><blockquote><p>超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议。所有的WWW文件都必须遵守这个标准。设计HTTP最初的目的是为了提供一种发布和接收HTML页面的方法。</p></blockquote><ul><li>HTTP属于OSI网络七层协议模型中的”最上层”:应用层协议。由请求和响应构成,是一个标准的客户端服务器模型。HTTP是一个无状态的协议 ( HTTP无状态协议,是指协议对于事务处理没有记忆能力。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大。另一方面,在服务器不需要先前信息时它的应答就较快 ) 。</li><li>HTTP默认端口号为80。它也可以承载在TLS和SSL之上,通过加密、认证的方式实现数据传输的安全,称为HTTPS,HTTPS默认端口号为443。</li><li>早期HTTP用于传输网页HTML文件,发展到现在,应用变得广泛,客户端软件(PC,Android,iOS等)大部分通过HTTP传输数据。</li></ul><h3 id="二-从浏览器输入地址到呈现页面中间发生了什么事情-通信过程"><a href="#二-从浏览器输入地址到呈现页面中间发生了什么事情-通信过程" class="headerlink" title="二.从浏览器输入地址到呈现页面中间发生了什么事情(通信过程)"></a><strong>二.从浏览器输入地址到呈现页面中间发生了什么事情(通信过程)</strong></h3><h4 id="2-1-简单来说"><a href="#2-1-简单来说" class="headerlink" title="2.1 简单来说"></a><strong>2.1 简单来说</strong></h4><ol><li>浏览器(客户端)进行地址解析。</li><li>将解析出的域名进行dns解析。</li><li>通过ip寻址和arp,找到目标(服务器)地址。</li><li>进行tcp三次握手,建立tcp连接。</li><li>浏览器发送数据,等待服务器响应。</li><li>服务器处理请求,并对请求做出响应。</li><li>浏览器收到服务器响应,得到html代码。</li><li>渲染页面。</li></ol><p>通过以上这些步骤,就完成了一次完整的http请求</p><h4 id="2-2-深入来说"><a href="#2-2-深入来说" class="headerlink" title="2.2. 深入来说"></a><strong>2.2. 深入来说</strong></h4><h5 id="一-浏览器(客户端)进行了地址解析。"><a href="#一-浏览器(客户端)进行了地址解析。" class="headerlink" title="一. 浏览器(客户端)进行了地址解析。"></a><strong>一. 浏览器(客户端)进行了地址解析。</strong></h5><p>当我们在浏览器中输入一个地址,按下回车后,浏览器获取到的是一个字符串。浏览器此时要对这个地址进行解析,获取协议,主机,端口,路径等信息。</p><p>URL的一般格式为(手记会自动过滤尖括号,所以只能上传图片了):</p><p>例如:</p><p><a href="http://www.imooc.com/article/draft/id/430">http://www.imooc.com/article/draft/id/430</a> 这个网址缺少了一些东西,端口号,用户名,密码,query和flag都没有。这些东西都是非必须的,甚至协议、路径都可以不要,最简洁的方式为imooc.com,浏览器会对一些默认的东西进行补齐。例如:互联网url默认端口号为80,浏览器默认补齐功能会补齐协议http,有些还会直接在域名前面补上www。所以实际上,即使我们输入的是imooc.com,然而实际访问的却是<a href="http://www.imooc.com./">http://www.imooc.com。</a></p><h5 id="二-将解析出的域名进行dns解析。"><a href="#二-将解析出的域名进行dns解析。" class="headerlink" title="二. 将解析出的域名进行dns解析。"></a><strong>二. 将解析出的域名进行dns解析。</strong></h5><p>第一步地址解析中我们已经获取到服务器的域名。此时就需要将域名换成对应的ip地址,这就是dns解析。dns解析分为以下几个步骤:</p><ol><li>先查看浏览器dns缓存中是否有域名对应的ip。</li><li>如果没有,则产看操作系统dns缓存中是否有对应的ip(例如windows的hosts文件)。</li><li>依旧没有就对本地区的dns服务器发起请求,</li><li>如果还是没有,就直接到Root Server域名服务器请求解析。</li></ol><p>这里面有几点需要关注:</p><p><1>、DNS在进行区域传输的时候使用TCP协议,其它时候则使用UDP协议;</p><p><2>、全球只有十三台<strong>逻辑</strong>根服务器,为什么是十三台,请参考<a href="https://www.zhihu.com/question/22587247?answer_deleted_redirect=true%E3%80%82%E5%85%B6%E4%B8%AD%E4%BB%BB%E4%BD%95%E4%B8%80%E6%AC%A1%E8%A7%A3%E6%9E%90%E6%88%90%E5%8A%9F%E5%B0%B1%E8%BF%94%E5%9B%9E%E5%AF%B9%E5%BA%94%E7%9A%84ip%E5%9C%B0%E5%9D%80%E3%80%82">https://www.zhihu.com/question/22587247?answer_deleted_redirect=true。其中任何一次解析成功就返回对应的ip地址。</a></p><h5 id="三-通过ip寻址和arp,找到目标(服务器)地址。"><a href="#三-通过ip寻址和arp,找到目标(服务器)地址。" class="headerlink" title="三. 通过ip寻址和arp,找到目标(服务器)地址。"></a><strong>三. 通过ip寻址和arp,找到目标(服务器)地址。</strong></h5><p>第二步获取到了ip,此时直接通过ip寻址找到ip对应的服务器,然后通过arp协议找到服务器的mac地址。</p><p>这里有几点需要注意:</p><ol><li>ip地址(ipv4, 32位)。ip地址是IP协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。ip地址分为A、B、C、D、E五大类:</li></ol><p>A类地址:一个字节(8位)的网络地址和三个字节的主机地址。地址范围为:1.0.0.0~126.255.255.255。</p><p>B类地址:二个字节的网络地址和二个字节的主机地址。地址范围为:128.0.0.0~191.255.255.255。</p><p>C类地址:三个字节的网络地址和一个字节的主机地址。地址范围为:192.0.0.0~223.255.255.255。</p><p>D类地址:D类地址用于多点广播(Multicast),D类IP地址第一个字节以“lll0”开始,它是一个专门保留的地址。地址范围为:224.0.0.0~239.255.255.255。</p><p>E类地址:E类IP地址 以“llll0”开始,为将来使用保留。地址范围为:240.0.0.0~255.255.255.254。,255.255.255.255用于广播地址。</p><p>其中缺失了两部分,一个是0开头的,“0”表示该地址是本地主机,不能传送。一个是127开头的,127开头的是网卡自身,常用于测试。这里为什么是十进制的数字,为什么中间有‘.’,其实这都是为了方便人类而人为加上去的。转化为计算机语言就是二进制的,每一个字节八位,八位二进制能表示的最大数字就是255,这样ip地址就齐全了。可能有些人还发现ip地址为 10.170.8.61/23 ,这里涉及到局域网、保留地址和子网掩码。这里的意思是,前23位表示为该台主机的网络地址,该网络有 2^(32-23) = 512台主机。具体就不展开讲了,涉及的内容太深,太多。感兴趣的可以参考<a href="https://www.zhihu.com/question/56895036">https://www.zhihu.com/question/56895036</a></p><ol><li>IP寻址如何工作?</li></ol><p>ip寻址主要有两种方式,一种是同一网段,一种是不同网段。要判断两个IP地址是不是在同一个网段,就将它们的IP地址分别与子网掩码做与运算,得到的结果一网络号,如果网络号相同,就在同一子网,否则,不在同一子网。</p><p>同一网段的情况:</p><p>主机A和主机B,首先主机A通过本机的hosts表或者wins系统或dns系统先将主机B的计算机名 转换为Ip地址,然后用自己的 Ip地址与子网掩码计算出自己所出的网段,比较目的主机B的ip地址与自己的子网掩码,发现与自己是出于相同的网段,于是在自己的ARP缓存中查找是否有主机B 的mac地址,如果能找到就直接做数据链路层封装并且通过网卡将封装好的以太网帧发送有物理线路上去:如果arp缓存中没有主机B的的mac地址,主机A将启动arp协议通过在本地网络上的arp广播来查询主机B的mac地址,获得主机B的mac地址厚写入arp缓存表,进行数据链路层的封装,发送数据。</p><p>不同网段的情况:</p><p>不同的数据链路层网络必须分配不同网段的Ip地址并且由路由器将其连接起来。和上面一样,主机A发现和主机B不在同一个网段,于是主机A将知道应该将次数据包发送给自己的缺省网关,即路由器的本地接口。主机A在自己的ARP缓存中查找是否有缺省网关的MAC地址,如果能够找到就直接做数据链路层封装并通过网卡 将封装好的以太网数据帧发送到物理线路上去,如果arp缓存表中没有缺省网关的Mac地址,主机A将启动arp协议通过在本地网络上的arp广播来查询缺省网关的mac地址,获得缺省网关的mac地址后写入arp缓存表,进行数据链路层的封装,发送数据。数据帧到达路由器的接受接口后首先解封装,变成ip数据包,对ip 包进行处理,根据目的Ip地址查找路由表,决定转发接口后做适应转发接口数据链路层协议帧的封装,并且发送到下一跳路由器,次过程继续直至到达目的的网络与目的主机。整个过程有点像dns解析,只是dns服务器换成了下一跳路由器,udp编程了tcp,其他差别不大。</p><ol><li>arp。arp就是地址转化协议,也就是把ip地址转化为mac地址。和dns很像,先查缓存,然后查路由器。</li><li>mac地址。mac地址就是计算机的物理地址,每个网卡出厂时,被生产厂家烧制在网卡上。采用十六进制数表示,共六个字节(48位)。三个字节是由IEEE的注册管理机构RA负责给不同厂家分配的代码(高位24位),也称为“编制上唯一的标识符”(Organizationally Unique Identifier),后三个字节(低位24位)由各厂家自行指派给生产的适配器接口,称为扩展标识符(唯一性)。如何修改mac地址呢?一个方法就是直接修改网卡上烧制的mac地址,自己烧制。这个基本不靠谱,失误性也高。另一个方法就是修改注册表中的mac地址,因为网络中访问的mac地址都是访问的注册表中的mac地址,不会直接访问网卡。这个比较简单直接。</li><li>为什么有了ip地址,还要mac地址?这个问题很关键,就像是我有驾驶证了你非要让我提供身份证。这个涉及一些历史问题,因为一开始没有互联网的时候就只有mac地址,还不存在ip地址。后来互联网越来越大之后,发现mac地址找起来太麻烦,并且耗时也越来越久,就发明了ip地址。并且mac地址在一个局域网中还是很有用的,所以就两个一起存在了。详细的信息,大家可以参考<a href="https://www.zhihu.com/question/21546408%E3%80%82">https://www.zhihu.com/question/21546408。</a></li></ol><h5 id="四-进行tcp三次握手,建立tcp连接。"><a href="#四-进行tcp三次握手,建立tcp连接。" class="headerlink" title="四. 进行tcp三次握手,建立tcp连接。"></a><strong>四. 进行tcp三次握手,建立tcp连接。</strong></h5><p>简述一下,第三步我们找到了目标ip,并获得了服务器ip的mac地址。此时浏览器就会请求和服务器连接,用来传输数据。tcp 是稳定双向面向连接的,断开时也会分两边分别断开。面向连接不是说tcp一个双方一直开着的通道,而是维持一个连接的状态,让它看起来有连接。</p><h5 id="五-浏览器发送数据,等待服务器响应。"><a href="#五-浏览器发送数据,等待服务器响应。" class="headerlink" title="五. 浏览器发送数据,等待服务器响应。"></a><strong>五. 浏览器发送数据,等待服务器响应。</strong></h5><p>第四步已经建立了连接,此时就要发送数据了。浏览器会对请求进行包装,包装成请求报文。请求报文的格式如下:</p><p>起始行:如 GET / HTTP/1.0 (请求的方法 请求的URL 请求所使用的协议)</p><p>头部信息:User-Agent Host等成对出现的值</p><p>主体</p><p>请求头部和主体之间有一个回车换行。如果是get请求,则没有主体部分,post请求有主体部分。当然里面还有些请求头部比较重要</p><h5 id="六-服务器处理请求,并对请求做出响应。"><a href="#六-服务器处理请求,并对请求做出响应。" class="headerlink" title="六. 服务器处理请求,并对请求做出响应。"></a><strong>六. 服务器处理请求,并对请求做出响应。</strong></h5><p>浏览器请求报文到达服务器之后,服务器接口会对请求报文进行处理,执行接口对应的代码,处理完成后响应客户端。由于http是无状态的,正常情况下,客户端收到响应后就会直接断开连接,然后一次http请求就完成了。但是http1.0有一个keep-alive的请求字段,可以在一定时间内不断开连接(有时时间甚至很长)。http1.1直接就默认开启了keep-alive选项。这导致了一个后果是服务器已经处理完了请求,但是客户端不会主动断开连接,这就导致服务器资源一直被占用。这时服务器就不得不自己主动断开连接,而主动断开连接的一方会出现TIME_WAIT,占用连接池,这就是产生SYN Flood攻击的原因。</p><p>此时有三种处理方式,第一是客户端主动断开连接,第二是服务器主动断开连接,第三是对tcp连接经行设置。第一种情况,如果服务器返回的数据都有确定的content-length属性,或者客户端知道服务器返回的内容终止,则客户端主动断开连接。第二种情况,服务器可以通过设置一个最大超市时间,可以主动断开tcp连接。第三种情况,调整t三个tcp参数,第一个是:tcp_synack_retries 可以用他来减少重试次数;第二个是:tcp_max_syn_backlog,可以增大syn连接数;第三个是:tcp_abort_on_overflow 处理不过来干脆就直接拒绝连接了。</p><h5 id="七-浏览器收到服务器响应,得到html代码。"><a href="#七-浏览器收到服务器响应,得到html代码。" class="headerlink" title="七. 浏览器收到服务器响应,得到html代码。"></a><strong>七. 浏览器收到服务器响应,得到html代码。</strong></h5><p>其实你心里有疑问,这一步有什么好说的。其实这里面有很多需要注意的点。浏览器发出请求时,请求报文如下:</p><p>你需要关注一个报文头–accept。accept代表发送端(客户端)希望接受的数据类型,这是浏览器自动封装的请求头。如果服务器返回的content-type是accept中的任何一个,浏览器都能解析,并直接展示在网页上。如果服务器返回的content-type是其他类型,此时浏览器有三种处理状态:</p><ol><li>正常显示。例如返回类型为text/javascript,浏览器能直接处理并展示。</li><li>下载。例如返回类型为application/octet-stream(二进制流,不知道下载文件类型),这种浏览器不能直接处理的,会被下载。</li><li>报错。当我们返回一个字符串hello world,却使用text/xml,格式时,浏览器不能正确解析,就会报错,并把报错信息呈现在网页中。</li></ol><p>浏览器能直接处理很多种格式,并直接呈现在网页中,并不限于accept中规定的字段,具体有哪些,就需要自己亲自动手试试了。</p><p>附上一张content-type常用对照表地址:http content-type常用对照表</p><h5 id="八-渲染页面。"><a href="#八-渲染页面。" class="headerlink" title="八. 渲染页面。"></a><strong>八. 渲染页面。</strong></h5><p>获取到服务器相应之后,浏览器会根据相应的content-type字段对响应字符串进行解析。能够解析并成功解析就显示,能够解析但解析错误就报错,不能解析就下载。由于浏览器采用至上而下的方式解析,所以会先解析html,直到遇到外部样式和外部脚本。这时会阻塞浏览器的解析,外部样式和外部脚本(在没有async、defer属性下)会并行加载,但是外部样式会阻塞外部脚本的执行,dom加载完毕,js脚本执行成功后dom树构建完成(DOMContentLoaded),之后就加载dom中引用的图片等静态资源。(参考文章地址:<a href="http://blog.csdn.net/u014168594/article/details/52196460%EF%BC%89">http://blog.csdn.net/u014168594/article/details/52196460)</a></p><p>即:</p><ol><li>html解析->外部样式、脚本加载->外部样式执行->外部脚本执行->html继续解析->dom树构建完成->加载图片->页面加载完成。</li><li>情况一:如果是动态脚本(即内联脚本)则不受样式影响,在解析到它时会执行。</li><li>情况二:外部样式后续外部脚本含有async属性(IE下为defer),外部样式不会阻塞该脚本的加载与执行</li></ol><p>在外部样式执行完毕后,css附着于DOM,创建了一个渲染树(渲染树是一些被渲染对象的集)。每个渲染对象都包含了与之对应的计算过样式的DOM对象,对于每个渲染元素来说,位置都经过计算,所以这里被叫做“布局”。然后将“布局”显示在浏览器窗口,称之为“绘制”。</p><ol><li>接着脚本的执行完毕后,DOM树构建完成。这时,可以触发DOMContentLoaded事件。DOMContentLoaded事件的触发条件是:在所有的DOM全部加载完毕并且JS加载执行后触发。</li></ol><p>1.情况一:如果脚本是动态加载,则不会影响DOMContentLoaded时间的触发,浏览器会等css加载完成后再加载图片,因为不确定图片的样式会如何。</p><ol><li>要点一:CSS样式表会阻塞图片的加载,如果想让图片尽快加载,就不要给图片使用样式,比如宽高采用标签属性即可。</li><li>要点二:脚本不会阻塞图片的加载</li></ol><p>最后页面加载完成,页面load。</p><blockquote><p>总结一下:<a href="https://cloud.tencent.com/solution/operation?from=10680">运维</a>人员需要处理页面缓存、cdn及keep-alive引起的连接池占用等问题;后端人员需要处理代码逻辑、缓存、传输优化、报错等问题;前段人员需要做好前端性能优化和配合运维、后端做好借口调试,缓存处理等问题。所以无论是前端、后台、运维都应该很清楚整个流程中的每一步,才能在配合时,得心应手,才能在出现问题时,快速准确的定位问题解决问题,才能在需要优化时,迅速完整的给出方案。</p><p>ps:本篇文章之介绍了http事物,如果是https事物,整个流程和http事物大致相同,唯一不同的就是在http层和tcp层多了一个ssl层,所以在发送数据前会有个ssl握手,发送数据时会有个ssl层的加密。ssl涉及到的东西也不少,例如ssl握手,加密技术,还要ssl层到底在tcp/ip四层协议哪一层的问题等等</p></blockquote><h3 id="三-HTTP请求和响应详解"><a href="#三-HTTP请求和响应详解" class="headerlink" title="三. HTTP请求和响应详解"></a><strong>三. HTTP请求和响应详解</strong></h3><h4 id="客户端请求消息"><a href="#客户端请求消息" class="headerlink" title="客户端请求消息"></a><strong>客户端请求消息</strong></h4><p>客户端请求消息客户端发送一个HTTP请求到服务器的请求消息包括以下格式:</p><p><code>请求行(request line)</code>、<code>请求头部(header)</code>、<code>空行</code>和<code>请求数据</code>四个部分组成</p><p>下图给出了请求报文的一般格式</p><ol><li><strong>请求行</strong>:请求报文的第一行,用来说明以什么方式请求、请求的地址和HTTP版本</li><li><strong>头部字段</strong>:每个头部字段都包含一个名字和值,二者之间采用“:”连接,如:Connection:Keep-Alive</li><li><strong>请求数据</strong>:请求的主体根据不同的请求方式请求主体不同</li></ol><h4 id="服务器响应消息"><a href="#服务器响应消息" class="headerlink" title="服务器响应消息"></a><strong>服务器响应消息</strong></h4><p>HTTP响应也由四个部分组成,分别是:<code>状态行</code>、<code>消息报头</code>、<code>空行</code>和<code>响应正文</code></p><ol><li><strong>状态行</strong>:由HTTP版本、响应状态码、响应状态描述;如:HTTP/1.1 200 OK</li><li><strong>响应报文头部</strong>:使用关键字和值表示,二者使用“:”隔开;如:Content-Type:text/html</li><li><strong>响应内容</strong>:请求空行之后就是请求内容</li></ol><h3 id="四-HTTP发展史-包括版本"><a href="#四-HTTP发展史-包括版本" class="headerlink" title="四. HTTP发展史(包括版本)"></a><strong>四. HTTP发展史(包括版本)</strong></h3><ul><li>HTTP/0.9:1991年发布,极其简单,只有一个get命令</li><li>HTTP/1.0:1996年5月发布,增加了大量内容</li><li>HTTP/1.1:1997年1月发布,进一步完善HTTP协议,是目前最流行的版本</li><li>SPDY :2009年谷歌发布SPDY协议,主要解决HTTP/1.1效率不高的问题</li><li>HTTP/2 :2015年借鉴SPDY的HTTP/2发布</li></ul><h3 id="五-HTTP-1-0和1-1的区别"><a href="#五-HTTP-1-0和1-1的区别" class="headerlink" title="五. HTTP/1.0和1.1的区别"></a><strong>五. HTTP/1.0和1.1的区别</strong></h3><ol><li>缓存处理:HTTP/1.0 使用 Pragma:no-cache + Last-Modified/If-Modified-Since来作为缓存判断的标准;HTTP/1.1 引入了更多的缓存控制策略:Cache-Control、Etag/If-None-Match等</li><li>错误状态管理:HTTP/1.1新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。</li><li>范围请求:HTTP/1.1在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接,支持断点续传</li><li>Host头:HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname)。但随着<a href="https://cloud.tencent.com/product/lighthouse?from=10680">虚拟主机</a>技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request)。有了Host字段,就可以将请求发往同一台服务器上的不同网站,为虚拟主机的兴起打下了基础</li><li>持久连接:HTTP/1.1 最大的变化就是引入了持久连接(persistent connection),在HTTP/1.1中默认开启 Connection: keep-alive,即TCP连接默认不关闭,可以被多个请求复用</li></ol><p>客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接。客户端和服务器发现对方一段时间没有活动,就可以主动关闭连接。不过,规范的做法是,客户端在最后一个请求时,发送Connection: close,明确要求服务器关闭TCP连接</p><ol><li>管道机制:HTTP/1.1中引入了管道机制(pipelining),即在同一个TCP连接中,客户端可以同时发送多个请求</li></ol><h3 id="六-HTTP-1-1的缺点"><a href="#六-HTTP-1-1的缺点" class="headerlink" title="六. HTTP/1.1的缺点"></a><strong>六. HTTP/1.1的缺点</strong></h3><p>HTTP/1.1 的持久连接和管道机制允许复用TCP连接,在一个TCP连接中,也可以同时发送多个请求,但是所有的数据通信都是按次序完成的,服务器只有处理完一个回应,才会处理下一个回应。比如客户端需要A、B两个资源,管道机制允许浏览器同时发出A请求和B请求,但服务器还是按照顺序,先回应A请求,完成后再回应B请求,这样如果前面的回应特别慢,后面就会有很多请求排队等着,这称为”队头阻塞(Head-of-line blocking)”</p><h3 id="七-HTTP-2"><a href="#七-HTTP-2" class="headerlink" title="七. HTTP/2"></a><strong>七. HTTP/2</strong></h3><p>HTTP/2以Google发布的SPDY协议为基础,于2015年发布。它不叫HTTP/2.0,因为标准委员会不打算再发布子版本了,下一个新版本将是HTTP/3。HTTP/2协议只在HTTPS环境下才有效,升级到HTTP/2,必须先启用HTTPS。HTTP/2解决了HTTP/1.1的性能问题,主要特点如下:</p><ol><li>二进制分帧:HTTP/1.1的头信息是文本(ASCII编码),数据体可以是文本,也可以是二进制;HTTP/2 头信息和数据体都是二进制,统称为“帧”:头信息帧和数据帧;</li><li>多路复用(双工通信):通过单一的 HTTP/2 连接发起多重的请求-响应消息,即在一个连接里,客户端和浏览器都可以同时发送多个请求和响应,而不用按照顺序一一对应,这样避免了“队头堵塞”。HTTP/2 把 HTTP 协议通信的基本单位缩小为一个一个的帧,这些帧对应着逻辑流中的消息。并行地在同一个 TCP 连接上双向交换消息。</li><li>数据流:因为 HTTP/2 的数据包是不按顺序发送的,同一个连接里面连续的数据包,可能属于不同的回应。因此,必须要对数据包做标记,指出它属于哪个回应。HTTP/2 将每个请求或回应的所有数据包,称为一个数据流(stream)。每个数据流都有一个独一无二的编号。数据包发送的时候,都必须标记数据流ID,用来区分它属于哪个数据流。另外还规定,客户端发出的数据流,ID一律为奇数,服务器发出的,ID为偶数。数据流发送到一半的时候,客户端和服务器都可以发送信号(RST_STREAM帧),取消这个数据流。HTTP/1.1取消数据流的唯一方法,就是关闭TCP连接。这就是说,HTTP/2 可以取消某一次请求,同时保证TCP连接还打开着,可以被其他请求使用。客户端还可以指定数据流的优先级。优先级越高,服务器就会越早回应。</li><li>首部压缩:HTTP 协议不带有状态,每次请求都必须附上所有信息。所以,请求的很多字段都是重复的,,一模一样的内容,每次请求都必须附带,这会浪费很多带宽,也影响速度。HTTP/2 对这一点做了优化,引入了头信息压缩机制(header compression)。一方面,头信息压缩后再发送(SPDY 使用的是通用的DEFLATE 算法,而 HTTP/2 则使用了专门为首部压缩而设计的 HPACK 算法)。;另一方面,客户端和服务器同时维护一张头信息表,所有字段都会存入这个表,生成一个索引号,以后就不发送同样字段了,只发送索引号,这样就提高速度了。</li><li>服务端推送:HTTP/2 允许服务器未经请求,主动向客户端发送资源,这叫做服务器推送(server push)。常见场景是客户端请求一个网页,这个网页里面包含很多静态资源。正常情况下,客户端必须收到网页后,解析HTML源码,发现有静态资源,再发出静态资源请求。其实,服务器可以预期到客户端请求网页后,很可能会再请求静态资源,所以就主动把这些静态资源随着网页一起发给客户端了。</li></ol><h3 id="八-HTTPS"><a href="#八-HTTPS" class="headerlink" title="八. HTTPS"></a><strong>八. HTTPS</strong></h3><p>HTTPS可以说是安全版的HTTP,HTTPS基于安全SSL/TLS(安全套接层Secure Sockets Layer/安全传输层Transport Layer Security)层,即在传统的HTTP和TCP之间加了一层用于加密解密的SSL/TLS层。HTTP默认使用80端口,HTTPS默认使用443端口。</p><p>不使用SSL/TLS的HTTP通信,所有信息明文传播,会带来三大风险:</p><p>窃听风险:第三方可以获取通信内容;篡改风险:第三方可以修改通信内容;冒充风险:第三方可以冒充他人进行通信。SSL/TLS协议是为了解决这三大风险而设计的,以期达到:</p><p>信息加密传输:第三方无法窃听;校验机制:一旦被篡改,通信双方会立刻发现;身份证书:防止身份被冒充。</p><h4 id="SSL-TLS发展"><a href="#SSL-TLS发展" class="headerlink" title="SSL/TLS发展"></a><strong>SSL/TLS发展</strong></h4><p>SSL/1.0:1994年NetScape公司设计,未发布;SSL/2.0:1995年NetScape公司发布,但存在严重漏洞;SSL/3.0:1996年NetScape公司发布,得到大规模应用;TLS/1.0:1999年互联网标准化组织(ISOC)接替NetScape公司,发布SSL的升级版TLS/1.0;TLS/1.1:2006年发布;TLS/1.2:2008年发布;TLS/1.2修订版:2011年发布。目前,应用最广泛的是 TLS/1.0 和 SSL/3.0,且主流浏览器已实现 TLS/1.2的支持。</p><h4 id="SSL-TLS运行机制"><a href="#SSL-TLS运行机制" class="headerlink" title="SSL/TLS运行机制"></a><strong>SSL/TLS运行机制</strong></h4><p>SSL/TLS的基本思路是公钥加密法:客户端先向服务器索要并验证公钥,然后用公钥加密传输来协商生成“对话秘钥”(非对称加密),双方采用“对话秘钥”进行加密通信(对称加密)。</p><p>通信过程如下:</p><ol><li>客户端发出请求:给出支持的协议版本、支持的加密方法(如RSA公钥加密)以及一个客户端生成的随机数(Client random);</li><li>服务端回应:确认双方通信的协议版本、加密方法,并给出<a href="https://cloud.tencent.com/product/ssl?from=10680">服务器证书</a>以及一个服务器生成的随机数(Server random);</li><li>客户端回应:客户端确认证书有效,取出证书中的公钥,然后生成一个新的随机数(Premaster secret),使用公钥加密这个随机数,发送给服务端;</li><li>服务端回应:服务端使用自己的私钥解密客户端发来的随机数(Premaster secret),客户端和服务端根据约定的加密方法,使用三个随机数,生成“对话秘钥”;</li><li>会话通信:客户端和服务端使用“对话秘钥”加密通信,这个过程完全使用普通的HTTP协议,只不过用“会话秘钥”加密内容。</li></ol><p>前四步称为握手阶段,用于客户端和服务端建立连接和交换参数。整个通信过程可用下图所示:</p><h4 id="HTTPS特点"><a href="#HTTPS特点" class="headerlink" title="HTTPS特点"></a><strong>HTTPS特点</strong></h4><p>缓存:只要在HTTP头中使用特定命令,就可以缓存HTTPS;延迟:HTTP耗时 = TCP握手;HTTPS耗时 = TCP握手 + SSL握手。SSL握手耗时大概是TCP握手耗时的三倍左右。</p>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> HTTP </tag>
</tags>
</entry>
<entry>
<title>SpringBoot常用注解</title>
<link href="/2023/01/01/SpringBoot%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3/"/>
<url>/2023/01/01/SpringBoot%E5%B8%B8%E7%94%A8%E6%B3%A8%E8%A7%A3/</url>
<content type="html"><![CDATA[<h1 id="SoringBoot常用注解"><a href="#SoringBoot常用注解" class="headerlink" title="SoringBoot常用注解"></a>SoringBoot常用注解</h1><p><strong>@RequestMapping</strong></p><p>@RequestMapping注解的主要用途是将Web请求与请求处理类中的方法进行映射。Spring MVC和Spring WebFlux都通过<code>RquestMappingHandlerMapping</code>和<code>RequestMappingHndlerAdapter</code>两个类来提供对@RequestMapping注解的支持。</p><p><code>@RequestMapping</code>注解对请求处理类中的请求处理方法进行标注;<code>@RequestMapping</code>注解拥有以下的六个配置属性:</p><ul><li><code>value</code>:映射的请求URL或者其别名</li><li><code>method</code>:兼容HTTP的方法名</li><li><code>params</code>:根据HTTP参数的存在、缺省或值对请求进行过滤</li><li><code>header</code>:根据HTTP Header的存在、缺省或值对请求进行过滤</li><li><code>consume</code>:设定在HTTP请求正文中允许使用的媒体类型</li><li><code>product</code>:在HTTP响应体中允许使用的媒体类型</li></ul><p>提示:在使用@RequestMapping之前,请求处理类还需要使用@Controller或@RestController进行标记</p><p>下面是使用@RequestMapping的两个示例:</p><p><img src="/../images/image-20230101183243145.png" alt="image-20230101183243145"></p><p>@RequestMapping还可以对类进行标记,这样类中的处理方法在映射请求路径时,会自动将类上@RequestMapping设置的value拼接到方法中映射路径之前,如下:</p><p><img src="/../images/image-20230101183257786.png" alt="image-20230101183257786"></p><hr><p><strong>@RequestBody</strong></p><p>@RequestBody在处理请求方法的参数列表中使用,它可以将请求主体中的参数绑定到一个对象中,请求主体参数是通过<code>HttpMessageConverter</code>传递的,根据请求主体中的参数名与对象的属性名进行匹配并绑定值。此外,还可以通过@Valid注解对请求主体中的参数进行校验。</p><p>下面是一个使用<code>@RequestBody</code>的示例:</p><p><img src="/../images/image-20230101183316017.png" alt="image-20230101183316017"></p><hr><p><strong>@GetMapping</strong></p><p><code>@GetMapping</code>注解用于处理HTTP GET请求,并将请求映射到具体的处理方法中。具体来说,@GetMapping是一个组合注解,它相当于是<code>@RequestMapping(method=RequestMethod.GET)</code>的快捷方式。</p><p>下面是<code>@GetMapping</code>的一个使用示例:</p><p><img src="/../images/image-20230101183332725.png" alt="image-20230101183332725"></p><hr><p><strong>@PostMapping</strong></p><p><code>@PostMapping</code>注解用于处理HTTP POST请求,并将请求映射到具体的处理方法中。@PostMapping与@GetMapping一样,也是一个组合注解,它相当于是<code>@RequestMapping(method=HttpMethod.POST)</code>的快捷方式。</p><p>下面是使用<code>@PostMapping</code>的一个示例:</p><p><img src="/../images/image-20230101183350327.png" alt="image-20230101183350327"></p><hr><p><strong>@PutMapping</strong></p><p><code>@PutMapping</code>注解用于处理HTTP PUT请求,并将请求映射到具体的处理方法中,@PutMapping是一个组合注解,相当于是<code>@RequestMapping(method=HttpMethod.PUT)</code>的快捷方式。</p><p>下面是使用<code>@PutMapping</code>的一个示例:</p><p><img src="/../images/image-20230101183420380.png" alt="image-20230101183420380"></p><hr><p><strong>@DeleteMapping</strong></p><p><code>@DeleteMapping</code>注解用于处理HTTP DELETE请求,并将请求映射到删除方法中。@DeleteMapping是一个组合注解,它相当于是<code>@RequestMapping(method=HttpMethod.DELETE)</code>的快捷方式。</p><p>下面是使用<code>@DeleteMapping</code>的一个示例:</p><p><img src="/../images/image-20230101183435129.png" alt="image-20230101183435129"></p><hr><p><strong>@PatchMapping</strong></p><p><code>@PatchMapping</code>注解用于处理HTTP PATCH请求,并将请求映射到对应的处理方法中。@PatchMapping相当于是<code>@RequestMapping(method=HttpMethod.PATCH)</code>的快捷方式。</p><p>下面是一个简单的示例:</p><p><img src="/../images/image-20230101183454534.png" alt="image-20230101183454534"></p><hr><p><strong>@ControllerAdvice</strong></p><p><code>@ControllerAdvice</code>是@Component注解的一个延伸注解,Spring会自动扫描并检测被@ControllerAdvice所标注的类。<code>@ControllerAdvice</code>需要和<code>@ExceptionHandler</code>、<code>@InitBinder</code>以及<code>@ModelAttribute</code>注解搭配使用,主要是用来处理控制器所抛出的异常信息。</p><p>首先,我们需要定义一个被<code>@ControllerAdvice</code>所标注的类,在该类中,定义一个用于处理具体异常的方法,并使用@ExceptionHandler注解进行标记。</p><p>此外,在有必要的时候,可以使用<code>@InitBinder</code>在类中进行全局的配置,还可以使用@ModelAttribute配置与视图相关的参数。使用<code>@ControllerAdvice</code>注解,就可以快速的创建统一的,自定义的异常处理类。</p><p>下面是一个使用<code>@ControllerAdvice</code>的示例代码:</p><p><img src="/../images/image-20230101183518239.png" alt="image-20230101183518239"></p><hr><p><strong>@ResponseBody</strong></p><p><code>@ResponseBody</code>会自动将控制器中方法的返回值写入到HTTP响应中。特别的,<code>@ResponseBody</code>注解只能用在被<code>@Controller</code>注解标记的类中。如果在被<code>@RestController</code>标记的类中,则方法不需要使用<code>@ResponseBody</code>注解进行标注。<code>@RestController</code>相当于是<code>@Controller</code>和<code>@ResponseBody</code>的组合注解。</p><p>下面是使用该注解的一个示例</p><p><img src="/../images/image-20230101183534413.png" alt="image-20230101183534413"></p><hr><p><strong>@ExceptionHandler</strong></p><p><code>@ExceptionHander</code>注解用于标注处理特定类型异常类所抛出异常的方法。当控制器中的方法抛出异常时,Spring会自动捕获异常,并将捕获的异常信息传递给被<code>@ExceptionHandler</code>标注的方法。</p><p>下面是使用该注解的一个示例:</p><p><img src="/../images/image-20230101183551017.png" alt="image-20230101183551017"></p><hr><p><strong>@ResponseStatus</strong></p><p><code>@ResponseStatus</code>注解可以标注请求处理方法。使用此注解,可以指定响应所需要的HTTP STATUS。特别地,我们可以使用HttpStauts类对该注解的value属性进行赋值。</p><p>下面是使用<code>@ResponseStatus</code>注解的一个示例:</p><p><img src="/../images/image-20230101183617313.png" alt="image-20230101183617313"></p><hr><p><strong>@PathVariable</strong></p><p><code>@PathVariable</code>注解是将方法中的参数绑定到请求URI中的模板变量上。可以通过<code>@RequestMapping</code>注解来指定URI的模板变量,然后使用<code>@PathVariable</code>注解将方法中的参数绑定到模板变量上。</p><p>特别地,<code>@PathVariable</code>注解允许我们使用value或name属性来给参数取一个别名。下面是使用此注解的一个示例:</p><p><img src="/../images/image-20230101183710807.png" alt="image-20230101183710807"></p><p>模板变量名需要使用<code>{ }</code>进行包裹,如果方法的参数名与URI模板变量名一致,则在<code>@PathVariable</code>中就可以省略别名的定义。</p><p>下面是一个简写的示例:</p><p><img src="/../images/image-20230101183731444.png" alt="image-20230101183731444"></p><p>提示:如果参数是一个非必须的,可选的项,则可以在<code>@PathVariable</code>中设置<code>require = false</code></p><hr><p><strong>@RequestParam</strong></p><p><code>@RequestParam</code>注解用于将方法的参数与Web请求的传递的参数进行绑定。使用<code>@RequestParam</code>可以轻松的访问HTTP请求参数的值。</p><p>下面是使用该注解的代码示例:</p><p><img src="/../images/image-20230101183838260.png" alt="image-20230101183838260"></p><p>该注解的其他属性配置与<code>@PathVariable</code>的配置相同,特别的,如果传递的参数为空,还可以通过defaultValue设置一个默认值。示例代码如下:</p><p><img src="/../images/image-20230101183903004.png" alt="image-20230101183903004"></p><hr><p><strong>@Controller</strong></p><p><code>@Controller</code>是<code>@Component</code>注解的一个延伸,Spring会自动扫描并配置被该注解标注的类。此注解用于标注Spring MVC的控制器。下面是使用此注解的示例代码:</p><p><img src="/../images/image-20230101183930364.png" alt="image-20230101183930364"></p><hr><p><strong>@RestController</strong></p><p><code>@RestController</code>是在Spring 4.0开始引入的,这是一个特定的控制器注解。此注解相当于<code>@Controller</code>和<code>@ResponseBody</code>的快捷方式。当使用此注解时,不需要再在方法上使用<code>@ResponseBody</code>注解。</p><p>下面是使用此注解的示例代码:</p><p><img src="/../images/image-20230101184106015.png" alt="image-20230101184106015"></p><hr><p><strong>@ModelAttribute</strong></p><p>通过此注解,可以通过模型索引名称来访问已经存在于控制器中的model。下面是使用此注解的一个简单示例:</p><p><img src="/../images/image-20230101184149235.png" alt="image-20230101184149235"></p><p>与<code>@PathVariable</code>和<code>@RequestParam</code>注解一样,如果参数名与模型具有相同的名字,则不必指定索引名称,简写示例如下:</p><p><img src="/../images/image-20230101184219322.png" alt="image-20230101184219322"></p><p>特别地,如果使用<code>@ModelAttribute</code>对方法进行标注,Spring会将方法的返回值绑定到具体的Model上。示例如下:</p><p><img src="/../images/image-20230101184240076.png" alt="image-20230101184240076"></p><p>在Spring调用具体的处理方法之前,被<code>@ModelAttribute</code>注解标注的所有方法都将被执行。</p><hr><p><strong>@CrossOrigin</strong></p><p><code>@CrossOrigin</code>注解将为请求处理类或请求处理方法提供跨域调用支持。如果我们将此注解标注类,那么类中的所有方法都将获得支持跨域的能力。使用此注解的好处是可以微调跨域行为。使用此注解的示例如下:</p><p><img src="/../images/image-20230101184426236.png" alt="image-20230101184426236"></p><hr><p><strong>@InitBinder</strong></p><p><code>@InitBinder</code>注解用于标注初始化<strong>WebDataBinider</strong>的方法,该方法用于对Http请求传递的表单数据进行处理,如时间格式化、字符串处理等。下面是使用此注解的示例:</p><p><img src="/../images/image-20230101190158545.png" alt="image-20230101190158545"></p><h2 id="二、Spring-Bean-注解"><a href="#二、Spring-Bean-注解" class="headerlink" title="二、Spring Bean 注解"></a>二、Spring Bean 注解</h2><p>在本小节中,主要列举与Spring Bean相关的4个注解以及它们的使用方式。</p><p><strong>@ComponentScan</strong></p><p><code>@ComponentScan</code>注解用于配置Spring需要扫描的被组件注解注释的类所在的包。可以通过配置其basePackages属性或者value属性来配置需要扫描的包路径。value属性是basePackages的别名。此注解的用法如下:</p><hr><p><strong>@Component</strong></p><p>@Component注解用于标注一个普通的组件类,它没有明确的业务范围,只是通知Spring被此注解的类需要被纳入到Spring Bean容器中并进行管理。此注解的使用示例如下:</p><p><img src="/../images/image-20230101190241003.png" alt="image-20230101190241003"></p><hr><p><strong>@Service</strong></p><p><code>@Service</code>注解是<code>@Component</code>的一个延伸(特例),它用于标注业务逻辑类。与<code>@Component</code>注解一样,被此注解标注的类,会自动被Spring所管理。下面是使用<code>@Service</code>注解的示例:</p><p><img src="/../images/image-20230101190313588.png" alt="image-20230101190313588"></p><hr><p><strong>@Repository</strong></p><p><code>@Repository</code>注解也是<code>@Component</code>注解的延伸,与<code>@Component</code>注解一样,被此注解标注的类会被Spring自动管理起来,<code>@Repository</code>注解用于标注DAO层的数据持久化类。此注解的用法如下:</p><p><img src="/../images/image-20230101190340354.png" alt="image-20230101190340354"></p><h2 id="三、Spring-Dependency-Inject-与-Bean-Scops注解"><a href="#三、Spring-Dependency-Inject-与-Bean-Scops注解" class="headerlink" title="三、Spring Dependency Inject 与 Bean Scops注解"></a>三、Spring Dependency Inject 与 Bean Scops注解</h2><h3 id="Spring-DI注解"><a href="#Spring-DI注解" class="headerlink" title="Spring DI注解"></a>Spring DI注解</h3><p><strong>@DependsOn</strong></p><p><code>@DependsOn</code>注解可以配置Spring IoC容器在初始化一个Bean之前,先初始化其他的Bean对象。下面是此注解使用示例代码:</p><p><img src="/../images/640.jpeg" alt="图片"></p><hr><p><strong>@Bean</strong></p><p>@Bean注解主要的作用是告知Spring,被此注解所标注的类将需要纳入到Bean管理工厂中。@Bean注解的用法很简单,在这里,着重介绍@Bean注解中<code>initMethod</code>和<code>destroyMethod</code>的用法。示例如下:</p><p><img src="/../images/640-20230101190420869.jpeg" alt="图片"></p><h3 id="Scops注解"><a href="#Scops注解" class="headerlink" title="Scops注解"></a>Scops注解</h3><p><strong>@Scope</strong></p><p>@Scope注解可以用来定义@Component标注的类的作用范围以及@Bean所标记的类的作用范围。@Scope所限定的作用范围有:<code>singleton</code>、<code>prototype</code>、<code>request</code>、<code>session</code>、<code>globalSession</code>或者其他的自定义范围。这里以prototype为例子进行讲解。</p><p>当一个Spring Bean被声明为prototype(原型模式)时,在每次需要使用到该类的时候,Spring IoC容器都会初始化一个新的改类的实例。在定义一个Bean时,可以设置Bean的scope属性为<code>prototype:scope=“prototype”</code>,也可以使用@Scope注解设置,如下:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">@Scope(value=ConfigurableBeanFactory.SCOPE_PROPTOTYPE)</span><br></pre></td></tr></table></figure><p>下面将给出两种不同的方式来使用@Scope注解,示例代码如下:</p><p><img src="/../images/640-20230101190433197.jpeg" alt="图片"></p><hr><p><strong>@Scope 单例模式</strong></p><p>当@Scope的作用范围设置成Singleton时,被此注解所标注的类只会被Spring IoC容器初始化一次。在默认情况下,Spring IoC容器所初始化的类实例都为singleton。同样的原理,此情形也有两种配置方式,示例代码如下:</p><p><img src="/../images/640.png" alt="图片"></p><h2 id="四、容器配置注解"><a href="#四、容器配置注解" class="headerlink" title="四、容器配置注解"></a>四、容器配置注解</h2><h3 id="Autowired"><a href="#Autowired" class="headerlink" title="@Autowired"></a>@Autowired</h3><p>@Autowired注解用于标记Spring将要解析和注入的依赖项。此注解可以作用在构造函数、字段和setter方法上。</p><p><strong>作用于构造函数</strong></p><p>下面是@Autowired注解标注构造函数的使用示例:</p><p><img src="/../images/640-20230101190624900.png" alt="图片"></p><hr><p><strong>作用于setter方法</strong></p><p>下面是@Autowired注解标注setter方法的示例代码:</p><p><img src="/../images/640-20230101190635336.png" alt="图片"></p><hr><p><strong>作用于字段</strong></p><p>@Autowired注解标注字段是最简单的,只需要在对应的字段上加入此注解即可,示例代码如下:</p><p><img src="/../images/640-20230101190641056.png" alt="图片"></p><h3 id="Primary"><a href="#Primary" class="headerlink" title="@Primary"></a>@Primary</h3><p>当系统中需要配置多个具有相同类型的bean时,@Primary可以定义这些Bean的优先级。下面将给出一个实例代码来说明这一特性:</p><p><img src="/../images/640-20230101190649206.png" alt="图片"></p><p>输出结果:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">this is send DingDing method message.</span><br></pre></td></tr></table></figure><h3 id="PostConstruct与-PreDestroy"><a href="#PostConstruct与-PreDestroy" class="headerlink" title="@PostConstruct与@PreDestroy"></a>@PostConstruct与@PreDestroy</h3><p>值得注意的是,这两个注解不属于Spring,它们是源于JSR-250中的两个注解,位于<code>common-annotations.jar</code>中。@PostConstruct注解用于标注在Bean被Spring初始化之前需要执行的方法。@PreDestroy注解用于标注Bean被销毁前需要执行的方法。下面是具体的示例代码:</p><p><img src="/../images/640-20230101190715952.png" alt="图片"></p><h3 id="Qualifier"><a href="#Qualifier" class="headerlink" title="@Qualifier"></a>@Qualifier</h3><p>当系统中存在同一类型的多个Bean时,@Autowired在进行依赖注入的时候就不知道该选择哪一个实现类进行注入。此时,我们可以使用@Qualifier注解来微调,帮助@Autowired选择正确的依赖项。下面是一个关于此注解的代码示例:</p><p><img src="/../images/640-20230101190752562.png" alt="图片"></p><h2 id="五、Spring-Boot注解"><a href="#五、Spring-Boot注解" class="headerlink" title="五、Spring Boot注解"></a><strong>五、Spring Boot注解</strong></h2><p><strong>@SpringBootApplication</strong></p><p><code>@SpringBootApplication</code>注解是一个快捷的配置注解,在被它标注的类中,可以定义一个或多个Bean,并自动触发自动配置Bean和自动扫描组件。此注解相当于<code>@Configuration</code>、<code>@EnableAutoConfiguration</code>和<code>@ComponentScan</code>的组合。</p><p>在Spring Boot应用程序的主类中,就使用了此注解。示例代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@SpringBootApplication</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Application</span>{</span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title function_">main</span><span class="params">(String [] args)</span>{</span><br><span class="line"> SpringApplication.run(Application.class,args);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@EnableAutoConfiguration</strong></p><p>@EnableAutoConfiguration注解用于通知Spring,根据当前类路径下引入的依赖包,自动配置与这些依赖包相关的配置项。</p><hr><p><strong>@ConditionalOnClass与@ConditionalOnMissingClass</strong></p><p>这两个注解属于类条件注解,它们根据是否存在某个类作为判断依据来决定是否要执行某些配置。下面是一个简单的示例代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Configuration</span></span><br><span class="line"><span class="meta">@ConditionalOnClass(DataSource.class)</span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">MySQLAutoConfiguration</span> {</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@ConditionalOnBean与@ConditionalOnMissingBean</strong></p><p>这两个注解属于对象条件注解,根据是否存在某个对象作为依据来决定是否要执行某些配置方法。示例代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><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="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalOnBean(name="dataSource")</span></span><br><span class="line">LocalContainerEntityManagerFactoryBean <span class="title function_">entityManagerFactory</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalOnMissingBean</span></span><br><span class="line"><span class="keyword">public</span> MyBean <span class="title function_">myBean</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@ConditionalOnProperty</strong></p><p>@ConditionalOnProperty注解会根据Spring配置文件中的配置项是否满足配置要求,从而决定是否要执行被其标注的方法。示例代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalOnProperty(name="alipay",havingValue="on")</span></span><br><span class="line">Alipay <span class="title function_">alipay</span><span class="params">()</span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> <span class="title class_">Alipay</span>();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@ConditionalOnResource</strong></p><p>此注解用于检测当某个配置文件存在使,则触发被其标注的方法,下面是使用此注解的代码示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ConditionalOnResource(resources = "classpath:website.properties")</span></span><br><span class="line">Properties <span class="title function_">addWebsiteProperties</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@ConditionalOnWebApplication与@ConditionalOnNotWebApplication</strong></p><p>这两个注解用于判断当前的应用程序是否是Web应用程序。如果当前应用是Web应用程序,则使用Spring WebApplicationContext,并定义其会话的生命周期。下面是一个简单的示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@ConditionalOnWebApplication</span></span><br><span class="line">HealthCheckController <span class="title function_">healthCheckController</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@ConditionalExpression</strong></p><p>此注解可以让我们控制更细粒度的基于表达式的配置条件限制。当表达式满足某个条件或者表达式为真的时候,将会执行被此注解标注的方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Bean</span></span><br><span class="line"><span class="meta">@ConditionalException("${localstore} && ${local == 'true'}")</span></span><br><span class="line">LocalFileStore <span class="title function_">store</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@Conditional</strong></p><p>@Conditional注解可以控制更为复杂的配置条件。在Spring内置的条件控制注解不满足应用需求的时候,可以使用此注解定义自定义的控制条件,以达到自定义的要求。下面是使用该注解的简单示例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Conditioanl(CustomConditioanl.class)</span></span><br><span class="line">CustomProperties <span class="title function_">addCustomProperties</span><span class="params">()</span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><hr><p><strong>@Before && @After</strong></p><p>JUnit 4 开始使用 Java 5 中的注解(annotation),常用的几个 annotation 介绍: </p><p>@BeforeClass:针对所有测试,只执行一次,且必须为static void </p><p>@Before:初始化方法 </p><p>@Test:测试方法,在这里可以测试期望异常和超时时间 </p><p>@After:释放资源 </p><p>@AfterClass:针对所有测试,只执行一次,且必须为static void </p><p>@Ignore:忽略的测试方法 </p><p>一个单元测试用例执行顺序为: @BeforeClass –> @Before –> @Test –> @After –> @AfterClass </p><p>每一个测试方法的调用顺序为: @Before –> @Test –> @After</p><hr><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a><strong>总结</strong></h2><p>本次课程总结了Spring Boot中常见的各类型注解的使用方式,让大家能够统一的对Spring Boot常用注解有一个全面的了解。</p>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> java </tag>
</tags>
</entry>
<entry>
<title>设计模式</title>
<link href="/2022/12/25/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
<url>/2022/12/25/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/</url>
<content type="html"><![CDATA[<h1 id="设计模式"><a href="#设计模式" class="headerlink" title="设计模式"></a>设计模式</h1><h2 id="单例模式"><a href="#单例模式" class="headerlink" title="单例模式"></a>单例模式</h2><p>单例模式是指一个类在一个进程中只有一个实例对象(但也不一定,比如Spring中的Bean的单例是指在一个容器中是单例的)</p><p>单例模式创建分为饿汉式和懒汉式,总共大概有8种写法。但是在开源项目中使用最多的主要有两种写法:</p><h4 id="1、静态常量"><a href="#1、静态常量" class="headerlink" title="1、静态常量"></a>1、静态常量</h4><p>静态常量方式属于饿汉式,以静态变量的方式声明对象。这种单例模式在Spring中使用的比较多,举个例子,在Spring中对于Bean的名称生成有个类AnnotationBeanNameGenerator就是单例的。</p><p><img src="/../images/image-20221225131221433.png" alt="image-20221225131221433">AnnotationBeanNameGenerator</p><h4 id="2、双重检查机制"><a href="#2、双重检查机制" class="headerlink" title="2、双重检查机制"></a>2、双重检查机制</h4><p>除了上面一种,还有一种双重检查机制在开源项目中也使用的比较多,而且在面试中也比较喜欢问。双重检查机制方式属于懒汉式,代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">Singleton</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">volatile</span> <span class="keyword">static</span> Singleton INSTANCE;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="title function_">Singleton</span><span class="params">()</span> {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">static</span> Singleton <span class="title function_">getInstance</span><span class="params">()</span> {</span><br><span class="line"> <span class="keyword">if</span> (INSTANCE == <span class="literal">null</span>) {</span><br><span class="line"> <span class="keyword">synchronized</span> (Singleton.class) {</span><br><span class="line"> <span class="keyword">if</span> (INSTANCE == <span class="literal">null</span>) {</span><br><span class="line"> INSTANCE = <span class="keyword">new</span> <span class="title class_">Singleton</span>();</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> INSTANCE;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>之所以这种方式叫双重检查机制,主要是在创建对象的时候进行了两次INSTANCE == null的判断。</p><h5 id="疑问讲解"><a href="#疑问讲解" class="headerlink" title="疑问讲解"></a>疑问讲解</h5><p>这里解释一下双重检查机制的三个疑问:</p><ul><li>外层判断null的作用</li><li>内层判断null的作用</li><li>变量使用volatile关键字修饰的作用</li></ul><p><strong>外层判断null的作用</strong>:其实就是为了减少进入同步代码块的次数,提高效率。你想一下,其实去了外层的判断其实是可以的,但是每次获取对象都需要进入同步代码块,实在是没有必要。</p><p><strong>内层判断null的作用</strong>:防止多次创建对象。假设AB同时走到同步代码块,A先抢到锁,进入代码,创建了对象,释放锁,此时B进入代码块,如果没有判断null,那么就会直接再次创建对象,那么就不是单例的了,所以需要进行判断null,防止重复创建单例对象。</p><p><strong>volatile关键字的作用</strong>:防止重排序。因为创建对象的过程不是原子,大概会分为三个步骤</p><ul><li>第一步:分配内存空间给Singleton这个对象</li><li>第二步:初始化对象</li><li>第三步:将INSTANCE变量指向Singleton这个对象内存地址</li></ul><p>假设没有使用volatile关键字发生了重排序,第二步和第三步执行过程被调换了,也就是先将INSTANCE变量指向Singleton这个对象内存地址,再初始化对象。这样在发生并发的情况下,另一个线程经过第一个if非空判断时,发现已经为不为空,就直接返回了这个对象,但是此时这个对象还未初始化,内部的属性可能都是空值,一旦被使用的话,就很有可能出现空指针这些问题。</p><h2 id="建造者模式"><a href="#建造者模式" class="headerlink" title="建造者模式"></a>建造者模式</h2><p>将一个复杂对象的构造与它的表示分离,使同样的构建过程可以创建不同的表示,这样的设计模式被称为建造者模式。它是将一个复杂的对象分解为多个简单的对象,然后一步一步构建而成。</p><p>上面的意思看起来很绕,其实在实际开发中,其实建造者模式使用的还是比较多的,比如有时在创建一个pojo对象时,就可以使用建造者模式来创建:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">PersonDTO</span> <span class="variable">personDTO</span> <span class="operator">=</span> PersonDTO.builder()</span><br><span class="line"> .name(<span class="string">"三友的java日记"</span>)</span><br><span class="line"> .age(<span class="number">18</span>)</span><br><span class="line"> .sex(<span class="number">1</span>)</span><br><span class="line"> .phone(<span class="string">"188****9527"</span>)</span><br><span class="line"> .build();</span><br></pre></td></tr></table></figure><p>上面这段代码就是通过建造者模式构建了一个PersonDTO对象,所以建造者模式又被称为Budiler模式。</p><p>这种模式在创建对象的时候看起来比较优雅,当构造参数比较多的时候,适合使用建造者模式。</p><p>接下来就来看看建造者模式在开源项目中是如何运用的</p><h5 id="1、在Spring中的运用"><a href="#1、在Spring中的运用" class="headerlink" title="1、在Spring中的运用"></a>1、在Spring中的运用</h5><p>我们都知道,Spring在创建Bean之前,会将每个Bean的声明封装成对应的一个BeanDefinition,而BeanDefinition会封装很多属性,所以Spring为了更加优雅地创建BeanDefinition,就提供了BeanDefinitionBuilder这个建造者类。</p><p><img src="/../images/image-20221225131235339.png" alt="image-20221225131235339">BeanDefinitionBuilder</p><blockquote><p><a href="https://m.runoob.com/design-pattern/builder-pattern.html?ivk_sa=1024320u">https://m.runoob.com/design-pattern/builder-pattern.html?ivk_sa=1024320u</a></p></blockquote><h2 id="工厂模式"><a href="#工厂模式" class="headerlink" title="工厂模式"></a>工厂模式</h2><p>工厂模式在开源项目中也使用的非常多,具体的实现大概可以细分为三种:</p><ul><li>简单工厂模式</li><li>工厂方法模式</li><li>抽象工厂模式</li></ul><h4 id="简单工厂模式"><a href="#简单工厂模式" class="headerlink" title="简单工厂模式"></a>简单工厂模式</h4><p>简单工厂模式,就跟名字一样,的确很简单。比如说,现在有个动物接口Animal,具体的实现有猫Cat、狗Dog等等,而每个具体的动物对象创建过程很复杂,有各种各样地步骤,此时就可以使用简单工厂来封装对象的创建过程,调用者不需要关心对象是如何具体创建的。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SimpleAnimalFactory</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> Animal <span class="title function_">createAnimal</span><span class="params">(String animalType)</span> {</span><br><span class="line"> <span class="keyword">if</span> (<span class="string">"cat"</span>.equals(animalType)) {</span><br><span class="line"> <span class="type">Cat</span> <span class="variable">cat</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Cat</span>();</span><br><span class="line"> <span class="comment">//一系列复杂操作</span></span><br><span class="line"> <span class="keyword">return</span> cat;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (<span class="string">"dog"</span>.equals(animalType)) {</span><br><span class="line"> <span class="type">Dog</span> <span class="variable">dog</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Dog</span>();</span><br><span class="line"> <span class="comment">//一系列复杂操作</span></span><br><span class="line"> <span class="keyword">return</span> dog;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> <span class="title class_">RuntimeException</span>(<span class="string">"animalType="</span> + animalType + <span class="string">"无法创建对应对象"</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></table></figure><p>当需要使用这些对象,调用者就可以直接通过简单工厂创建就行。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">SimpleAnimalFactory</span> <span class="variable">animalFactory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">SimpleAnimalFactory</span>();</span><br><span class="line"><span class="type">Animal</span> <span class="variable">cat</span> <span class="operator">=</span> animalFactory.createAnimal(<span class="string">"cat"</span>);</span><br></pre></td></tr></table></figure><p>需要注意的是,一般来说如果每个动物对象的创建只需要简单地new一下就行了,那么其实就无需使用工厂模式,工厂模式适合对象创建过程复杂的场景。</p><h4 id="工厂方法模式"><a href="#工厂方法模式" class="headerlink" title="工厂方法模式"></a>工厂方法模式</h4><p>上面说的简单工厂模式看起来没啥问题,但是还是违反了七大设计原则的OCP原则,也就是开闭原则。所谓的开闭原则就是对修改关闭,对扩展开放。</p><p>什么叫对修改关闭?就是尽可能不修改的意思。就拿上面的例子来说,如果现在新增了一种动物兔子,那么createAnimal方法就得修改,增加一种类型的判断,那么就此时就出现了修改代码的行为,也就违反了对修改关闭的原则。</p><p>所以解决简单工厂模式违反开闭原则的问题,就可以使用工厂方法模式来解决。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 工厂接口</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">AnimalFactory</span> {</span><br><span class="line"> Animal <span class="title function_">createAnimal</span><span class="params">()</span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 小猫实现</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">CatFactory</span> <span class="keyword">implements</span> <span class="title class_">AnimalFactory</span> {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Animal <span class="title function_">createAnimal</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Cat</span> <span class="variable">cat</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Cat</span>();</span><br><span class="line"> <span class="comment">//一系列复杂操作</span></span><br><span class="line"> <span class="keyword">return</span> cat;</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="comment"> * 小狗实现</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">DogFactory</span> <span class="keyword">implements</span> <span class="title class_">AnimalFactory</span> {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> Animal <span class="title function_">createAnimal</span><span class="params">()</span> {</span><br><span class="line"> <span class="type">Dog</span> <span class="variable">dog</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">Dog</span>();</span><br><span class="line"> <span class="comment">//一系列复杂操作</span></span><br><span class="line"> <span class="keyword">return</span> dog;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种方式就是工厂方法模式。他将动物工厂提取成一个接口AnimalFactory,具体每个动物都各自实现这个接口,每种动物都有各自的创建工厂,如果调用者需要创建动物,就可以通过各自的工厂来实现。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"><span class="type">AnimalFactory</span> <span class="variable">animalFactory</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">CatFactory</span>();</span><br><span class="line"><span class="type">Animal</span> <span class="variable">cat</span> <span class="operator">=</span> animalFactory.createAnimal();</span><br></pre></td></tr></table></figure><p>此时假设需要新增一个动物兔子,那么只需要实现AnimalFactory接口就行,对于原来的猫和狗的实现,其实代码是不需要修改的,遵守了对修改关闭的原则,同时由于是对扩展开放,实现接口就是扩展的意思,那么也就符合扩展开放的原则。</p><h4 id="抽象工厂模式"><a href="#抽象工厂模式" class="headerlink" title="抽象工厂模式"></a>抽象工厂模式</h4><p>工厂方法模式其实是创建一个产品的工厂,比如上面的例子中,AnimalFactory其实只创建动物这一个产品。而抽象工厂模式特点就是创建一系列产品,比如说,不同的动物吃的东西是不一样的,那么就可以加入食物这个产品,通过抽象工厂模式来实现。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">AnimalFactory</span> {</span><br><span class="line"></span><br><span class="line"> Animal <span class="title function_">createAnimal</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"> Food <span class="title function_">createFood</span><span class="params">()</span>;</span><br><span class="line"> </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在动物工厂中,新增了创建食物的接口,小狗小猫的工厂去实现这个接口,创建狗粮和猫粮,这里就不去写了。</p><h5 id="1、工厂模式在Mybatis的运用"><a href="#1、工厂模式在Mybatis的运用" class="headerlink" title="1、工厂模式在Mybatis的运用"></a>1、工厂模式在Mybatis的运用</h5><p>在Mybatis中,当需要调用Mapper接口执行sql的时候,需要先获取到SqlSession,通过SqlSession再获取到Mapper接口的动态代理对象,而SqlSession的构造过程比较复杂,所以就提供了SqlSessionFactory工厂类来封装SqlSession的创建过程。</p><p><img src="/../images/image-20221225131249726.png" alt="image-20221225131249726">SqlSessionFactory及默认实现DefaultSqlSessionFactory</p><p>对于使用者来说,只需要通过SqlSessionFactory来获取到SqlSession,而无需关心SqlSession是如何创建的。</p><h5 id="2、工厂模式在Spring中的运用"><a href="#2、工厂模式在Spring中的运用" class="headerlink" title="2、工厂模式在Spring中的运用"></a>2、工厂模式在Spring中的运用</h5><p>我们知道Spring中的Bean是通过BeanFactory创建的。</p><p><img src="/../images/image-20221225131302047.png" alt="image-20221225131302047"></p><p>BeanFactory就是Bean生成的工厂。一个Spring Bean在生成过程中会经历复杂的一个生命周期,而这些生命周期对于使用者来说是无需关心的,所以就可以将Bean创建过程的逻辑给封装起来,提取出一个Bean的工厂。</p><h2 id="策略模式"><a href="#策略模式" class="headerlink" title="策略模式"></a>策略模式</h2><p>策略模式也比较常见,就比如说在Spring源码中就有很多地方都使用到了策略模式。</p><p>在讲策略模式是什么之前先来举个例子,这个例子我在之前的<a href="https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247530835&idx=1&sn=6bc43645c2249eb5b0ee9a9fca5077b7&chksm=cea13698f9d6bf8e54e9c9207e9525ecf2bde92b9b68d7a9765b44ffdb49b67162745c39c716&token=9220217&lang=zh_CN&scene=21#wechat_redirect">《写出漂亮代码的45个小技巧》</a>文章提到过。</p><p>假设现在有一个需求,需要将消息推送到不同的平台。</p><p>最简单的做法其实就是使用if else来做判断就行了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">notifyMessage</span><span class="params">(User user, String content, <span class="type">int</span> notifyType)</span> {</span><br><span class="line"> <span class="keyword">if</span> (notifyType == <span class="number">0</span>) {</span><br><span class="line"> <span class="comment">//调用短信通知的api发送短信</span></span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (notifyType == <span class="number">1</span>) {</span><br><span class="line"> <span class="comment">//调用app通知的api发送消息</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>根据不同的平台类型进行判断,调用对应的api发送消息。</p><p>虽然这样能实现功能,但是跟上面的提到的简单工厂的问题是一样的,同样违反了开闭原则。当需要增加一种平台类型,比如邮件通知,那么就得修改notifyMessage的方法,再次进行else if的判断,然后调用发送邮件的邮件发送消息。</p><p>此时就可以使用策略模式来优化了。</p><p>首先设计一个策略接口:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">MessageNotifier</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 是否支持改类型的通知的方式</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> notifyType 0:短信 1:app</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="type">boolean</span> <span class="title function_">support</span><span class="params">(<span class="type">int</span> notifyType)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 通知</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> user</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> content</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">notify</span><span class="params">(User user, String content)</span>;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>短信通知实现:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">SMSMessageNotifier</span> <span class="keyword">implements</span> <span class="title class_">MessageNotifier</span> {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">support</span><span class="params">(<span class="type">int</span> notifyType)</span> {</span><br><span class="line"> <span class="keyword">return</span> notifyType == <span class="number">0</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">notify</span><span class="params">(User user, String content)</span> {</span><br><span class="line"> <span class="comment">//调用短信通知的api发送短信</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>app通知实现:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">AppMessageNotifier</span> <span class="keyword">implements</span> <span class="title class_">MessageNotifier</span> {</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="type">boolean</span> <span class="title function_">support</span><span class="params">(<span class="type">int</span> notifyType)</span> {</span><br><span class="line"> <span class="keyword">return</span> notifyType == <span class="number">1</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">notify</span><span class="params">(User user, String content)</span> {</span><br><span class="line"> <span class="comment">//调用通知app通知的api</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>最后notifyMessage的实现只需要要循环调用所有的MessageNotifier的support方法,一旦support方法返回true,说明当前MessageNotifier支持该类的消息发送,最后再调用notify发送消息就可以了。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Resource</span></span><br><span class="line"><span class="keyword">private</span> List<MessageNotifier> messageNotifiers;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">notifyMessage</span><span class="params">(User user, String content, <span class="type">int</span> notifyType)</span> {</span><br><span class="line"> <span class="keyword">for</span> (MessageNotifier messageNotifier : messageNotifiers) {</span><br><span class="line"> <span class="keyword">if</span> (messageNotifier.support(notifyType)) {</span><br><span class="line"> messageNotifier.notify(user, content);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>那么如果现在需要支持通过邮件通知,只需要实现MessageNotifier接口,注入到Spring容器就行,其余的代码根本不需要有任何变动。</p><p>到这其实可以更好的理解策略模式了。就拿上面举的例子来说,短信通知,app通知等其实都是发送消息一种策略,而策略模式就是需要将这些策略进行封装,抽取共性,使这些策略之间相互替换。</p><h4 id="策略模式在SpringMVC中的运用"><a href="#策略模式在SpringMVC中的运用" class="headerlink" title="策略模式在SpringMVC中的运用"></a>策略模式在SpringMVC中的运用</h4><h5 id="1、对接口方法参数的处理"><a href="#1、对接口方法参数的处理" class="headerlink" title="1、对接口方法参数的处理"></a>1、对接口方法参数的处理</h5><p>比如说,我们经常在写接口的时候,会使用到了@PathVariable、@RequestParam、@RequestBody等注解,一旦我们使用了注解,SpringMVC会处理注解,从请求中获取到参数,然后再调用接口传递过来,而这个过程,就使用到了策略模式。</p><p>对于这类参数的解析,SpringMVC提供了一个策略接口HandlerMethodArgumentResolver</p><p><img src="/../images/image-20221225131325869.png" alt="image-20221225131325869">HandlerMethodArgumentResolver</p><p>这个接口的定义就跟我们上面定义的差不多,不同的参数处理只需要实现这个解决就行,比如上面提到的几个注解,都有对应的实现。</p><p>比如处理@RequestParam注解的RequestParamMethodArgumentResolver的实现。</p><p><img src="/../images/image-20221225131338351.png" alt="image-20221225131338351">RequestParamMethodArgumentResolver</p><p>当然还有其它很多的实现,如果想知道各种注解处理的过程,只需要找到对应的实现类就行了。</p><h5 id="2、对接口返回值的处理"><a href="#2、对接口返回值的处理" class="headerlink" title="2、对接口返回值的处理"></a>2、对接口返回值的处理</h5><p>同样,SpringMVC对于返回值的处理也是基于策略模式来实现的。</p><p><img src="/../images/image-20221225131349308.png" alt="image-20221225131349308">HandlerMethodReturnValueHandler</p><p>HandlerMethodReturnValueHandler接口定义跟上面都是同一种套路。</p><p>比如说,常见的对于@ResponseBody注解处理的实现RequestResponseBodyMethodProcessor。</p><p><img src="/../images/image-20221225131400617.png" alt="image-20221225131400617">ResponseBody注解处理的实现RequestResponseBodyMethodProcessor</p><p>同样,HandlerMethodReturnValueHandler的实现也有很多,这里就不再举例了。</p><p>策略模式在Spring的运用远不止这两处,就比如我在<a href="https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247530447&idx=1&sn=87ea04d63cc101761a3eadadbf8966ea&chksm=cea13804f9d6b112e16fb0e73d93f270f20c5294270285fffa486e5a21c26b2bf622aaa3340f&token=9220217&lang=zh_CN&scene=21#wechat_redirect">《三万字盘点Spring/Boot的那些常用扩展点》</a>文章提到过对于配置文件的加载PropertySourceLoader也是策略模式的运用。</p><h2 id="模板方法模式"><a href="#模板方法模式" class="headerlink" title="模板方法模式"></a>模板方法模式</h2><p>模板方法模式是指,在父类中定义一个操作中的框架,而操作步骤的具体实现交由子类做。其核心思想就是,对于功能实现的顺序步骤是一定的,但是具体每一步如何实现交由子类决定。</p><p>比如说,对于旅游来说,一般有以下几个步骤:</p><ul><li>做攻略,选择目的地</li><li>收拾行李</li><li>乘坐交通工具去目的地</li><li>玩耍、拍照</li><li>乘坐交通工具去返回</li></ul><p>但是对于去哪,收拾什么东西都,乘坐什么交通工具,都是由具体某个旅行来决定。</p><p>那么对于旅游这个过程使用模板方法模式翻译成代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">Travel</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">travel</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">//做攻略</span></span><br><span class="line"> makePlan();</span><br><span class="line"></span><br><span class="line"> <span class="comment">//收拾行李</span></span><br><span class="line"> packUp();</span><br><span class="line"></span><br><span class="line"> <span class="comment">//去目的地</span></span><br><span class="line"> toDestination();</span><br><span class="line"></span><br><span class="line"> <span class="comment">//玩耍、拍照</span></span><br><span class="line"> play();</span><br><span class="line"></span><br><span class="line"> <span class="comment">//乘坐交通工具去返回</span></span><br><span class="line"> backHome();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">makePlan</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">packUp</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">toDestination</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">play</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">backHome</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>对于某次旅行来说,只需要重写每个步骤该做的事就行,比如说这次可以选择去杭州西湖,下次可以去长城,但是对于旅行过程来说是不变了,对于调用者来说,只需要调用暴露的travel方法就行。</p><p>可能这说的还是比较抽象,我再举两个模板方法模式在源码中实现的例子。</p><h4 id="模板方法模式在源码中的使用"><a href="#模板方法模式在源码中的使用" class="headerlink" title="模板方法模式在源码中的使用"></a>模板方法模式在源码中的使用</h4><h5 id="1、模板方法模式在HashMap中的使用"><a href="#1、模板方法模式在HashMap中的使用" class="headerlink" title="1、模板方法模式在HashMap中的使用"></a>1、模板方法模式在HashMap中的使用</h5><p>HashMap我们都很熟悉,可以通过put方法存元素,并且在元素添加成功之后,会调用一下afterNodeInsertion方法。</p><p><img src="/../images/image-20221225131416617.png" alt="image-20221225131416617"></p><p>而afterNodeInsertion其实是在HashMap中是空实现,什么事都没干。</p><p><img src="/../images/image-20221225131429048.png" alt="image-20221225131429048">afterNodeInsertion</p><p>这其实就是模板方法模式。HashMap定义了一个流程,那就是当元素成功添加之后会调用afterNodeInsertion,子类如果需要在元素添加之后做什么事,那么重写afterNodeInsertion就行。</p><p>正巧,JDK中的LinkedHashMap重写了这个方法。</p><p><img src="/../images/image-20221225131441285.png" alt="image-20221225131441285"></p><p>而这段代码主要干的一件事就是可能会移除最老的元素,至于到底会不会移除,得看if是否成立。</p><p>添加元素移除最老的元素,基于这种特性其实可以实现LRU算法,比如Mybatis的LruCache就是基于LinkedHashMap实现的,有兴趣的可以扒扒源码,这里就不再展开讲了。</p><h5 id="2、模板方法模式在Spring中的运用"><a href="#2、模板方法模式在Spring中的运用" class="headerlink" title="2、模板方法模式在Spring中的运用"></a>2、模板方法模式在Spring中的运用</h5><p>我们都知道,在Spring中,ApplicationContext在使用之前需要调用一下refresh方法,而refresh方法就定义了整个容器刷新的执行流程代码。</p><p><img src="/../images/image-20221225131457674.png" alt="image-20221225131457674">refresh方法部分截图</p><p>在整个刷新过程有一个onRefresh方法</p><p><img src="/../images/image-20221225131510614.png" alt="image-20221225131510614">onRefresh方法</p><p>而onRefresh方法默认是没有做任何事,并且在注释上有清楚两个单词Template method,翻译过来就是模板方法的意思,所以onRefresh就是一个模板方法,并且方法内部的注释也表明了,这个方法是为了子类提供的。</p><p>在Web环境下,子类会重写这个方法,然后创建一个Web服务器。</p><p><img src="/../images/image-20221225131521073.png" alt="image-20221225131521073"></p><h6 id="3、模板方法模式在Mybatis中的使用"><a href="#3、模板方法模式在Mybatis中的使用" class="headerlink" title="3、模板方法模式在Mybatis中的使用"></a>3、模板方法模式在Mybatis中的使用</h6><p>在Mybatis中,是使用Executor执行Sql的。</p><p><img src="/../images/image-20221225131532212.png" alt="image-20221225131532212">Executor</p><p>而Mybatis一级缓存就在Executor的抽象实现中BaseExecutor实现的。如图所示,红圈就是一级缓存</p><p><img src="/../images/image-20221225131542152.png" alt="image-20221225131542152">BaseExecutor</p><p>比如在查询的时候,如果一级缓存有,那么就处理缓存的数据,没有的话就调用queryFromDatabase从数据库查</p><p><img src="/../images/image-20221225131555133.png" alt="image-20221225131555133"></p><p>queryFromDatabase会调用doQuery方法从数据库查数据,然后放入一级缓存中。</p><p><img src="/../images/image-20221225131604176.png" alt="image-20221225131604176"></p><p>而doQuery是个抽象方法</p><p><img src="/../images/image-20221225131613607.png" alt="image-20221225131613607"></p><p>所以doQuery其实就是一个模板方法,需要子类真正实现从数据库中查询数据,所以这里就使用了模板方法模式。</p><h2 id="责任链模式"><a href="#责任链模式" class="headerlink" title="责任链模式"></a>责任链模式</h2><p>在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链。请求在这个链上传递,由该链上的某一个对象或者某几个对象决定处理此请求,每个对象在整个处理过程中值扮演一个小小的角色。</p><p>举个例子,现在有个请假的审批流程,根据请假的人的级别审批到的领导不同,比如有有组长、主管、HR、分管经理等等。</p><p>先需要定义一个处理抽象类,抽象类有个下一个处理对象的引用,提供了抽象处理方法,还有一个对下一个处理对象的调用方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">class</span> <span class="title class_">ApprovalHandler</span> {</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 责任链中的下一个处理对象</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">protected</span> ApprovalHandler next;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 设置下一个处理对象</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> approvalHandler</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">nextHandler</span><span class="params">(ApprovalHandler approvalHandler)</span> {</span><br><span class="line"> <span class="built_in">this</span>.next = approvalHandler;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 处理</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> approvalContext</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title function_">approval</span><span class="params">(ApprovalContext approvalContext)</span>;</span><br><span class="line"></span><br><span class="line"> <span class="comment">/**</span></span><br><span class="line"><span class="comment"> * 调用下一个处理对象</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * <span class="doctag">@param</span> approvalContext</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">void</span> <span class="title function_">invokeNext</span><span class="params">(ApprovalContext approvalContext)</span> {</span><br><span class="line"> <span class="keyword">if</span> (next != <span class="literal">null</span>) {</span><br><span class="line"> next.approval(approvalContext);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>几种审批人的实现</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//组长审批实现</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">GroupLeaderApprovalHandler</span> <span class="keyword">extends</span> <span class="title class_">ApprovalHandler</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_">approval</span><span class="params">(ApprovalContext approvalContext)</span> {</span><br><span class="line"> System.out.println(<span class="string">"组长审批"</span>);</span><br><span class="line"> <span class="comment">//调用下一个处理对象进行处理</span></span><br><span class="line"> invokeNext(approvalContext);</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">public</span> <span class="keyword">class</span> <span class="title class_">DirectorApprovalHandler</span> <span class="keyword">extends</span> <span class="title class_">ApprovalHandler</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_">approval</span><span class="params">(ApprovalContext approvalContext)</span> {</span><br><span class="line"> System.out.println(<span class="string">"主管审批"</span>);</span><br><span class="line"> <span class="comment">//调用下一个处理对象进行处理</span></span><br><span class="line"> invokeNext(approvalContext);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="comment">//hr审批实现</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">HrApprovalHandler</span> <span class="keyword">extends</span> <span class="title class_">ApprovalHandler</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_">approval</span><span class="params">(ApprovalContext approvalContext)</span> {</span><br><span class="line"> System.out.println(<span class="string">"hr审批"</span>);</span><br><span class="line"> <span class="comment">//调用下一个处理对象进行处理</span></span><br><span class="line"> invokeNext(approvalContext);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>有了这几个实现之后,接下来就需要对对象进行组装,组成一个链条,比如在Spring中就可以这么玩。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">@Component</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">ApprovalHandlerChain</span> {</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> GroupLeaderApprovalHandler groupLeaderApprovalHandler;</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> DirectorApprovalHandler directorApprovalHandler;</span><br><span class="line"> <span class="meta">@Autowired</span></span><br><span class="line"> <span class="keyword">private</span> HrApprovalHandler hrApprovalHandler;</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> ApprovalHandler <span class="title function_">getChain</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">//组长处理完下一个处理对象是主管</span></span><br><span class="line"> groupLeaderApprovalHandler.nextHandler(directorApprovalHandler);</span><br><span class="line"> <span class="comment">//主管处理完下一个处理对象是hr</span></span><br><span class="line"> directorApprovalHandler.nextHandler(hrApprovalHandler);</span><br><span class="line"> </span><br><span class="line"> <span class="comment">//返回组长,这样就从组长开始审批,一条链就完成了</span></span><br><span class="line"> <span class="keyword">return</span> groupLeaderApprovalHandler;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>之后对于调用方而言,只需要获取到链条,开始处理就行。</p><p>一旦后面出现需要增加或者减少审批人,只需要调整链条中的节点就行,对于调用者来说是无感知的。</p><h4 id="责任链模式在开源项目中的使用"><a href="#责任链模式在开源项目中的使用" class="headerlink" title="责任链模式在开源项目中的使用"></a>责任链模式在开源项目中的使用</h4><h5 id="1、在SpringMVC中的使用"><a href="#1、在SpringMVC中的使用" class="headerlink" title="1、在SpringMVC中的使用"></a>1、在SpringMVC中的使用</h5><p>在SpringMVC中,可以通过使用HandlerInterceptor对每个请求进行拦截。</p><p>HandlerInterceptor</p><p>而HandlerInterceptor其实就使用到了责任链模式,但是这种责任链模式的写法跟上面举的例子写法不太一样。</p><p>对于HandlerInterceptor的调用是在HandlerExecutionChain中完成的。</p><p>HandlerExecutionChain</p><p>比如说,对于请求处理前的拦截,就在是这样调用的。</p><p>其实就是循环遍历每个HandlerInterceptor,调用preHandle方法。</p><h5 id="2、在Sentinel中的使用"><a href="#2、在Sentinel中的使用" class="headerlink" title="2、在Sentinel中的使用"></a>2、在Sentinel中的使用</h5><p>Sentinel是阿里开源的一个流量治理组件,而Sentinel核心逻辑的执行其实就是一条责任链。</p><p>在Sentinel中,有个核心抽象类AbstractLinkedProcessorSlot</p><p>AbstractLinkedProcessorSlot</p><p>这个组件内部也维护了下一个节点对象,这个类扮演的角色跟例子中的ApprovalHandler类是一样的,写法也比较相似。这个组件有很多实现</p><p>比如有比较核心的几个实现</p><ul><li>DegradeSlot:熔断降级的实现</li><li>FlowSlot:流量控制的实现</li><li>StatisticSlot:统计的实现,比如统计请求成功的次数、异常次数,为限流提供数据来源</li><li>SystemSlot:根据系统规则来进行流量控制</li></ul><p>整个链条的组装的实现是由DefaultSlotChainBuilder实现的</p><p>DefaultSlotChainBuilder</p><p>并且内部是使用了SPI机制来加载每个处理节点</p><p>所以,如果你想自定一些处理逻辑,就可以基于SPI机制来扩展。</p><p>除了上面的例子,比如Gateway网关、Dubbo、MyBatis等等框架中都有责任链模式的身影,所以责任链模式使用的还是比较多的。</p><h2 id="代理模式"><a href="#代理模式" class="headerlink" title="代理模式"></a>代理模式</h2><p>代理模式也是开源项目中很常见的使用的一种设计模式,这种模式可以在不改变原有代码的情况下增加功能。</p><p>举个例子,比如现在有个PersonService接口和它的实现类PersonServiceImpl</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//接口</span></span><br><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">PersonService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">savePerson</span><span class="params">(PersonDTO person)</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">public</span> <span class="keyword">class</span> <span class="title class_">PersonServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">PersonService</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_">savePerson</span><span class="params">(PersonDTO person)</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></table></figure><p>这个类刚开始运行的好好的,但是突然之前不知道咋回事了,有报错,需要追寻入参,所以此时就可以这么写。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PersonServiceImpl</span> <span class="keyword">implements</span> <span class="title class_">PersonService</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_">savePerson</span><span class="params">(PersonDTO person)</span> {</span><br><span class="line"> log.info(<span class="string">"savePerson接口入参:{}"</span>, JSON.toJSONString(person));</span><br><span class="line"> <span class="comment">//保存人员信息</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这么写,就修改了代码,万一以后不需要打印日志了呢,岂不是又要修改代码,不符和之前说的开闭原则,那么怎么写呢?可以这么玩。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">PersonServiceProxy</span> <span class="keyword">implements</span> <span class="title class_">PersonService</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">PersonService</span> <span class="variable">personService</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">PersonServiceImpl</span>();</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">savePerson</span><span class="params">(PersonDTO person)</span> {</span><br><span class="line"> log.info(<span class="string">"savePerson接口入参:{}"</span>, JSON.toJSONString(person));</span><br><span class="line"> personService.savePerson(person);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以实现一个代理类PersonServiceProxy,对PersonServiceImpl进行代理,这个代理类干的事就是打印日志,最后调用PersonServiceImpl进行人员信息的保存,这就是代理模式。</p><p>当需要打印日志就使用PersonServiceProxy,不需要打印日志就使用PersonServiceImpl,这样就行了,不需要改原有代码的实现。</p><p>讲到了代理模式,就不得不提一下Spring AOP,Spring AOP其实跟静态代理很像,最终其实也是调用目标对象的方法,只不过是动态生成的,这里就不展开讲解了。</p><h5 id="代理模式在Mybtais中的使用"><a href="#代理模式在Mybtais中的使用" class="headerlink" title="代理模式在Mybtais中的使用"></a>代理模式在Mybtais中的使用</h5><p>前面在说模板方法模式的时候,举了一个BaseExecutor使用到了模板方法模式的例子,并且在BaseExecutor这里面还完成了一级缓存的操作。</p><p>其实不光是一级缓存是通过Executor实现的,二级缓存其实也是,只不过不在BaseExecutor里面实现,而是在CachingExecutor中实现的。</p><p>CachingExecutor</p><p>CachingExecutor中内部有一个Executor类型的属性delegate,delegate单词的意思就是代理的意思,所以CachingExecutor显然就是一个代理类,这里就使用到了代理模式。</p><p>CachingExecutor的实现原理其实很简单,先从二级缓存查,查不到就通过被代理的对象查找数据,而被代理的Executor在Mybatis中默认使用的是SimpleExecutor实现,SimpleExecutor继承自BaseExecutor。</p><p>这里思考一下二级缓存为什么不像一级缓存一样直接写到BaseExecutor中?</p><p>这里我猜测一下是为了减少耦合。</p><p>我们知道Mybatis的一级缓存默认是开启的,一级缓存写在BaseExecutor中的话,那么只要是继承了BaseExecutor,就拥有了一级缓存的能力。</p><p>但二级缓存默认是不开启的,如果写在BaseExecutor中,讲道理也是可以的,但不符和单一职责的原则,类的功能过多,同时会耦合很多判断代码,比如开启二级缓存走什么逻辑,不开启二级缓存走什么逻辑。而使用代理模式很好的解决了这一问题,只需要在创建的Executor的时候判断是否开启二级缓存,开启的话就用CachingExecutor代理一下,不开启的话老老实实返回未被代理的对象就行,默认是SimpleExecutor。</p><p>如图所示,是构建Executor对象的源码,一旦开启了二级缓存,就会将前面创建的Executor进行代理,构建一个CachingExecutor返回。</p><h2 id="适配器模式"><a href="#适配器模式" class="headerlink" title="适配器模式"></a>适配器模式</h2><p>适配器模式使得原本由于接口不兼容而不能一起工作的哪些类可以一起工作,将一个类的接口转换成客户希望的另一个接口。</p><p>举个生活中的例子,比如手机充电器接口类型有USB TypeC接口和Micro USB接口等。现在需要给一个Micro USB接口的手机充电,但是现在只有USB TypeC接口的充电器,这怎么办呢?</p><p>其实一般可以弄个一个USB TypeC转Micro USB接口的转接头,这样就可以给Micro USB接口手机充电了,代码如下</p><p>USBTypeC接口充电</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">USBTypeC</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">chargeTypeC</span><span class="params">()</span> {</span><br><span class="line"> System.out.println(<span class="string">"开启充电了"</span>);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>MicroUSB接口</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">interface</span> <span class="title class_">MicroUSB</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">void</span> <span class="title function_">charge</span><span class="params">()</span>;</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>适配实现,最后是调用USBTypeC接口来充电</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MicroUSBAdapter</span> <span class="keyword">implements</span> <span class="title class_">MicroUSB</span> {</span><br><span class="line"></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> <span class="type">USBTypeC</span> <span class="variable">usbTypeC</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">USBTypeC</span>();</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">charge</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">//使用usb来充电</span></span><br><span class="line"> usbTypeC.chargeTypeC();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>方然除了上面这种写法,还有一种继承的写法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MicroUSBAdapter</span> <span class="keyword">extends</span> <span class="title class_">USBTypeC</span> <span class="keyword">implements</span> <span class="title class_">MicroUSB</span> {</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="keyword">public</span> <span class="keyword">void</span> <span class="title function_">charge</span><span class="params">()</span> {</span><br><span class="line"> <span class="comment">//使用usb来充电</span></span><br><span class="line"> <span class="built_in">this</span>.chargeTypeC();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这两种写法主要是继承和组合(聚合)的区别。</p><p>这样就可以通过适配器(转接头)就可以实现USBTypeC给MicroUSB接口充电。</p><h5 id="适配器模式在日志中的使用"><a href="#适配器模式在日志中的使用" class="headerlink" title="适配器模式在日志中的使用"></a>适配器模式在日志中的使用</h5><p>在日常开发中,日志是必不可少的,可以帮助我们快速快速定位问题,但是日志框架比较多,比如Slf4j、Log4j等等,一般同一系统都使用一种日志框架。</p><p>但是像Mybatis这种框架来说,它本身在运行的过程中也需要产生日志,但是Mybatis框架在设计的时候,无法知道项目中具体使用的是什么日志框架,所以只能适配各种日志框架,项目中使用什么框架,Mybatis就使用什么框架。</p><p>为此Mybatis提供一个Log接口</p><p><img src="/../images/image-20221225131631041.png" alt="image-20221225131631041"></p><p>而不同的日志框架,只需要适配这个接口就可以了</p><p><img src="/../images/image-20221225131641350.png" alt="image-20221225131641350">Slf4jLoggerImpl</p><p>就拿Slf4j的实现来看,内部依赖了一个Slf4j框架中的Logger对象,最后所有日志的打印都是通过Slf4j框架中的Logger对象来实现的。</p><p>此外,Mybatis还提供了如下的一些实现</p><p><img src="/../images/image-20221225131652837.png" alt="image-20221225131652837"></p><p>这样,Mybatis在需要打印日志的时候,只需要从Mybatis自己的LogFactory中获取到Log对象就行,至于最终获取到的是什么Log实现,由最终项目中使用日志框架来决定。</p><h2 id="观察者模式"><a href="#观察者模式" class="headerlink" title="观察者模式"></a>观察者模式</h2><p>当对象间存在一对多关系时,则使用观察者模式(Observer Pattern)。比如,当一个对象被修改时,则会自动通知依赖它的对象。</p><p>这是什么意思呢,举个例子来说,假设发生了火灾,可能需要打119、救人,那么就可以基于观察者模式来实现,打119、救人的操作只需要观察火灾的发生,一旦发生,就触发相应的逻辑。</p><p><img src="/../images/image-20221225131702109.png" alt="image-20221225131702109"></p><p>观察者的核心优点就是观察者和被观察者是解耦合的。就拿上面的例子来说,火灾事件(被观察者)根本不关系有几个监听器(观察者),当以后需要有变动,只需要扩展监听器就行,对于事件的发布者和其它监听器是无需做任何改变的。</p><p>观察者模式实现起来比较复杂,这里我举一下Spring事件的例子来说明一下。</p><h5 id="观察者模式在Spring事件中的运用"><a href="#观察者模式在Spring事件中的运用" class="headerlink" title="观察者模式在Spring事件中的运用"></a>观察者模式在Spring事件中的运用</h5><p>Spring事件,就是Spring基于观察者模式实现的一套API,如果有不知道不知道Spring事件的小伙伴,可以看看<a href="https://mp.weixin.qq.com/s?__biz=Mzg2OTA0Njk0OA==&mid=2247530447&idx=1&sn=87ea04d63cc101761a3eadadbf8966ea&chksm=cea13804f9d6b112e16fb0e73d93f270f20c5294270285fffa486e5a21c26b2bf622aaa3340f&token=9220217&lang=zh_CN&scene=21#wechat_redirect">《三万字盘点Spring/Boot的那些常用扩展点》</a>这篇文章,里面有对Spring事件的详细介绍,这里就不对使用进行介绍了。</p><p>Spring事件的实现比较简单,其实就是当Bean在生成完成之后,会将所有的ApplicationListener接口实现(监听器)添加到ApplicationEventMulticaster中。</p><p>ApplicationEventMulticaster可以理解为一个调度中心的作用,可以将事件通知给监听器,触发监听器的执行。</p><p><img src="/../images/image-20221225131712519.png" alt="image-20221225131712519">ApplicationEventMulticaster可以理解为一个总线</p><p>retrieverCache中存储了事件类型和对应监听器的缓存。当发布事件的时候,会通过事件的类型找到对应的监听器,然后循环调用监听器。</p><p><img src="/../images/image-20221225131722555.png" alt="image-20221225131722555"></p><p>所以,Spring的观察者模式实现的其实也不复杂。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>本文通过对设计模式的讲解加源码举例的方式介绍了9种在代码设计中常用的设计模式:</p><ul><li>单例模式</li><li>建造者模式</li><li>工厂模式</li><li>策略模式</li><li>模板方法模式</li><li>责任链模式</li><li>代理模式</li><li>适配器模式</li><li>观察者模式</li></ul><p>其实这些设计模式不仅在源码中常见在平时工作中也是可以经常使用到的。</p><p>设计模式其实还是一种思想,或者是套路性的东西,至于设计模式具体怎么用、如何用、代码如何写还得依靠具体的场景来进行灵活的判断。</p><p>最后,本文又是前前后后花了一周多的时间完成,如果对你有点帮助,还请帮忙点赞、在看、转发、非常感谢。</p>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> java </tag>
</tags>
</entry>
<entry>
<title>Localhost详解</title>
<link href="/2022/12/25/Localhost%E8%AF%A6%E8%A7%A3/"/>
<url>/2022/12/25/Localhost%E8%AF%A6%E8%A7%A3/</url>
<content type="html"><![CDATA[<h1 id="Localhost-amp-127-0-0-1"><a href="#Localhost-amp-127-0-0-1" class="headerlink" title="Localhost & 127.0.0.1"></a>Localhost & 127.0.0.1</h1><p>你<strong>女神爱不爱你</strong>,你问她,她可能不会告诉你。</p><p>但<strong>网通不通</strong>,你 <code>ping</code> 一下就知道了。</p><p>可能看到标题,你就知道答案了,但是你了解背后的原因吗?</p><p>那如果把 <code>127.0.0.1</code> 换成 <code>0.0.0.0</code> 或 <code>localhost</code> 会怎么样呢?你知道这几个<code>IP</code>有什么区别吗?</p><p>以前面试的时候就遇到过这个问题,大家看个动图了解下面试官和我当时的场景,求当时我的心里阴影面积。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_gif/FmVWPHrDdnmz3VSM4JH6FE0VFtydTmORTBibRrFfn1mfX9SU1hciciahTZdCFfDRRdasytF7NLattxdibdXcp8VOWA/640?wx_fmt=gif&wxfrom=5&wx_lazy=1" alt="图片"></p><p>话不多说,我们直接开车。</p><p>拔掉网线,断网。</p><p><img src="https://mmbiz.qpic.cn/mmbiz_jpg/FmVWPHrDdnkr3eLdxxIK0eujAOibyGS3aSoMibKbQFRGpTqNbMaaPUxY1icSvhv0rel4R4O3ib99hQdJ1P7AYHBplQ/640?wx_fmt=jpeg&wxfrom=5&wx_lazy=1&wx_co=1" alt="图片"></p><p>然后在控制台输入<code>ping 127.0.0.1</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">$ ping 127.0.0.1</span><br><span class="line">PING 127.0.0.1 (127.0.0.1): 56 data bytes</span><br><span class="line">64 bytes from 127.0.0.1: icmp_seq=0 ttl=64 time=0.080 ms</span><br><span class="line">64 bytes from 127.0.0.1: icmp_seq=1 ttl=64 time=0.093 ms</span><br><span class="line">64 bytes from 127.0.0.1: icmp_seq=2 ttl=64 time=0.074 ms</span><br><span class="line">64 bytes from 127.0.0.1: icmp_seq=3 ttl=64 time=0.079 ms</span><br><span class="line">64 bytes from 127.0.0.1: icmp_seq=4 ttl=64 time=0.079 ms</span><br><span class="line">^C</span><br><span class="line">--- 127.0.0.1 ping statistics ---</span><br><span class="line">5 packets transmitted, 5 packets received, 0.0% packet loss</span><br><span class="line">round-trip min/avg/max/stddev = 0.074/0.081/0.093/0.006 ms</span><br></pre></td></tr></table></figure><p>说明,拔了网线,<code>ping 127.0.0.1</code> 是<strong>能ping通的</strong>。</p><p>其实这篇文章看到这里,标题前半个问题已经被回答了。但是我们可以再想深一点。</p><p>为什么断网了还能 <code>ping</code> 通 <code>127.0.0.1</code> 呢?</p><p><strong>这能说明你不用交网费就能上网吗?</strong></p><p><strong>不能。</strong></p><p>首先我们需要进入基础科普环节。</p><p>不懂的同学看了就懂了,懂的看了就当查漏补缺吧。</p><h3 id="什么是127-0-0-1"><a href="#什么是127-0-0-1" class="headerlink" title="什么是127.0.0.1"></a>什么是127.0.0.1</h3><p>首先,这是个 <code>IPV4</code> 地址。</p><p><code>IPV4</code> 地址有 <code>32</code> 位,一个字节有 <code>8</code> 位,共 <code>4</code> 个字节。</p><p>其中<strong>127 开头的都属于回环地址</strong>,也是 <code>IPV4</code> 的特殊地址,没什么道理,就是人为规定的。</p><p>而<code>127.0.0.1</code>是<strong>众多</strong>回环地址中的一个。之所以不是 <code>127.0.0.2</code> ,而是 <code>127.0.0.1</code>,是因为源码里就是这么定义的,也没什么道理。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">/* Address to loopback in software to local host. */</span><br><span class="line">#define INADDR_LOOPBACK 0x7f000001 /* 127.0.0.1 */</span><br></pre></td></tr></table></figure><p><img src="/../images/image-20221225125054345.png" alt="image-20221225125054345"></p><p><code>IPv4</code> 的地址是 <code>32</code> 位的,2的32次方,大概是<code>40+亿</code>。地球光人口就76亿了,40亿IP这点量,<strong>塞牙缝都不够</strong>,实际上<strong>IP也确实用完</strong>了。</p><p>所以就有了<code>IPV6</code>, <code>IPv6</code> 的地址是 <code>128</code> 位的,大概是2的128次方≈<strong>10的38次方</strong>。据说地球的沙子数量大概是 <strong>10的23次方</strong>,所以IPV6的IP可以认为用不完。</p><p>IPV4以8位一组,每组之间用 <strong>.</strong> 号隔开。</p><p>IPV6就以16位为一组,每组之间用 <strong>:</strong> 号隔开。如果全是0,那么可以省略不写。</p><p><img src="/../images/image-20221225125109980.png" alt="image-20221225125109980">ipv6回环地址</p><p>在IPV4下的回环地址是 <code>127.0.0.1</code>,在<code>IPV6</code>下,表达为 <code>::1</code> 。中间把<strong>连续的0</strong>给省略了,之所以不是<strong>7个 冒号</strong>,而是<strong>2个冒号:</strong> , 是因为一个 IPV6 地址中<strong>只允许出现⼀次两个连续的冒号</strong>。</p><blockquote><p>多说一句:在IPV4下用的是 <strong>ping 127.0.0.1</strong> 命令。在IPV6下用的是 <strong>ping6 ::1</strong> 命令。</p></blockquote><h3 id="什么是-ping"><a href="#什么是-ping" class="headerlink" title="什么是 ping"></a>什么是 ping</h3><p>ping 是应用层命令,可以理解为它跟游戏或者聊天软件属于同一层。只不过聊天软件可以收发消息,还能点个赞什么的,有很多复杂的功能。而 ping 作为一个小软件,它的功能比较简单,就是<strong>尝试</strong>发送一个小小的消息到目标机器上,判断目的机器是否<strong>可达</strong>,其实也就是判断目标机器网络是否能连通。</p><p>ping应用的底层,用的是网络层的<strong>ICMP协议</strong>。</p><p><img src="/../images/image-20221225125123739.png" alt="image-20221225125123739">IP和ICMP和Ping所在分层</p><p>虽然ICMP协议和IP协议<strong>都属于网络层协议</strong>,但其实<strong>ICMP也是利用了IP协议进行消息的传输</strong>。</p><p><img src="/../images/image-20221225125134840.png" alt="image-20221225125134840">ip和icmp的关系</p><p>所以,大家在这里完全可以简单的理解为 ping 某个IP 就是往某个IP地址发个消息。</p><h3 id="TCP发数据和ping的区别"><a href="#TCP发数据和ping的区别" class="headerlink" title="TCP发数据和ping的区别"></a>TCP发数据和ping的区别</h3><p>一般情况下,我们会使用 TCP 进行网络数据传输,那么我们可以看下它和 ping 的区别。</p><p><img src="/../images/image-20221225125205127.png" alt="image-20221225125205127"></p><p>ping和普通发消息的关系</p><p>ping和其他应用层软件都属于<strong>应用层</strong>。</p><p>那么我们横向对比一下,比方说聊天软件,如果用的是TCP的方式去发送消息。</p><p>为了发送消息,那就得先知道往哪发。linux里万物皆文件,那你要发消息的目的地,也是个文件,这里就引出了socket 的概念。</p><p>要使用 <code>socket</code> , 那么首先需要创建它。</p><p>在 TCP 传输中创建的方式是 <code>socket(AF_INET, SOCK_STREAM, 0);</code>,其中 <code>AF_INET</code> 表示将使用 IPV4 里 <strong>host:port</strong> 的方式去解析待会你输入的网络地址。<code>SOCK_STREAM</code> 是指使用面向字节流的 TCP 协议,<strong>工作在传输层</strong>。</p><p>创建好了 <code>socket</code> 之后,就可以愉快的把要传输的数据写到这个文件里。调用 socket 的<code>sendto</code>接口的过程中进程会从<strong>用户态进入到内核态</strong>,最后会调用到 <code>sock_sendmsg</code> 方法。</p><p>然后进入传输层,带上<code>TCP</code>头。网络层带上<code>IP</code>头,数据链路层带上 <code>MAC</code>头等一系列操作后。进入网卡的<strong>发送队列 ring buffer</strong> ,顺着网卡就发出去了。</p><p>回到 <code>ping</code> , 整个过程也基本跟 <code>TCP</code> 发数据类似,差异的地方主要在于,创建 <code>socket</code> 的时候用的是 <code>socket(AF_INET,SOCK_RAW,IPPROTO_ICMP)</code>,<code>SOCK_RAW</code> 是原始套接字 ,<strong>工作在网络层</strong>, 所以构建<code>ICMP</code>(网络层协议)的数据,是再合适不过了。ping 在进入内核态后最后也是调用的 <code>sock_sendmsg</code> 方法,进入到网络层后加上<strong>ICMP和IP头</strong>后,数据链路层加上<strong>MAC头</strong>,也是顺着网卡发出。因此 本质上ping 跟 普通应用发消息 在程序流程上没太大差别。</p><p>这也解释了<strong>为什么当你发现怀疑网络有问题的时候,别人第一时间是问你能ping通吗?</strong>因为可以简单理解为ping就是自己组了个数据包,让系统按着其他软件发送数据的路径往外发一遍,能通的话说明其他软件发的数据也能通。</p><h3 id="为什么断网了还能-ping-通-127-0-0-1"><a href="#为什么断网了还能-ping-通-127-0-0-1" class="headerlink" title="为什么断网了还能 ping 通 127.0.0.1"></a>为什么断网了还能 ping 通 127.0.0.1</h3><p>前面提到,有网的情况下,ping 最后是<strong>通过网卡</strong>将数据发送出去的。</p><p>那么断网的情况下,网卡已经不工作了,ping 回环地址却一切正常,我们可以看下这种情况下的工作原理。</p><p><img src="/../images/image-20221225125226127.png" alt="image-20221225125226127">ping回环地址</p><p>从应用层到传输层再到网络层。这段路径跟ping外网的时候是几乎是一样的。到了网络层,系统会根据目的IP,在路由表中获取对应的<strong>路由信息</strong>,而这其中就包含选择<strong>哪个网卡</strong>把消息发出。</p><p>当发现<strong>目标IP是外网IP</strong>时,会从”真网卡”发出。</p><p>当发现<strong>目标IP是回环地址</strong>时,就会选择<strong>本地网卡</strong>。</p><p>本地网卡,其实就是个**”假网卡”<strong>,它不像”真网卡”那样有个<code>ring buffer</code>什么的,”假网卡”会把数据推到一个叫 <code>input_pkt_queue</code> 的 <strong>链表</strong> 中。这个链表,其实是所有网卡共享的,上面挂着发给本机的各种消息。消息被发送到这个链表后,会再触发一个</strong>软中断**。</p><p>专门处理软中断的工具人**”ksoftirqd”** (这是个<strong>内核线程</strong>),它在收到软中断后就会立马去链表里把消息取出,然后顺着数据链路层、网络层等层层往上传递最后给到应用程序。</p><p><img src="/../images/image-20221225125244671.png" alt="image-20221225125244671">工具人ksoftirqd</p><p>ping 回环地址和<strong>通过TCP等各种协议发送数据到回环地址</strong>都是走这条路径。整条路径从发到收,都没有经过”真网卡”。<strong>之所以127.0.0.1叫本地回环地址,可以理解为,消息发出到这个地址上的话,就不会出网络,在本机打个转就又回来了。</strong>所以断网,依然能 <code>ping</code> 通 <code>127.0.0.1</code>。</p><h3 id="ping回环地址和ping本机地址有什么区别"><a href="#ping回环地址和ping本机地址有什么区别" class="headerlink" title="ping回环地址和ping本机地址有什么区别"></a>ping回环地址和ping本机地址有什么区别</h3><p>我们在mac里执行 <code>ifconfig</code> 。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">$ ifconfig</span><br><span class="line">lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384</span><br><span class="line"> inet 127.0.0.1 netmask 0xff000000</span><br><span class="line"> ...</span><br><span class="line">en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500</span><br><span class="line"> inet 192.168.31.6 netmask 0xffffff00 broadcast 192.168.31.255</span><br><span class="line"> ...</span><br></pre></td></tr></table></figure><p>能看到 <strong>lo0</strong>,表示本地回环接口,对应的地址,就是我们前面提到的 <strong>127.0.0.1</strong> ,也就是<strong>回环地址</strong>。</p><p>和 <strong>eth0</strong>,表示本机第一块网卡,对应的IP地址是<strong>192.168.31.6</strong>,管它叫<strong>本机IP</strong>。</p><p>之前一直认为ping本机IP的话会通过”真网卡”出去,然后遇到第一个路由器,再发回来到本机。</p><p>为了验证这个说法,可以进行抓包,但结果跟上面的说法并不相同。</p><p><img src="/../images/image-20221225125303230.png" alt="image-20221225125303230">ping 127.0.0.1</p><p><img src="/../images/image-20221225125324149.png" alt="image-20221225125324149">ping 本机地址</p><p>可以看到 ping 本机IP 跟 ping 回环地址一样,相关的网络数据,都是走的 <strong>lo0</strong>,本地回环接口,也就是前面提到的**”假网卡”**。</p><p>只要走了本地回环接口,那数据都不会发送到网络中,在本机网络协议栈中兜一圈,就发回来了。因此 <strong>ping回环地址和ping本机地址没有区别</strong>。</p><h3 id="127-0-0-1-和-localhost-以及-0-0-0-0-有区别吗"><a href="#127-0-0-1-和-localhost-以及-0-0-0-0-有区别吗" class="headerlink" title="127.0.0.1 和 localhost 以及 0.0.0.0 有区别吗"></a>127.0.0.1 和 localhost 以及 0.0.0.0 有区别吗</h3><p>回到文章开头动图里的提问,算是面试八股文里的老常客了。</p><p>以前第一次用 <code>nginx</code> 的时候,发现用这几个 <code>IP</code>,都能正常访问到 <code>nginx</code> 的欢迎网页。一度认为这几个 <code>IP</code> 都是一样的。</p><p><img src="/../images/image-20221225125340582.png" alt="image-20221225125340582">访问127.0.0.1:80</p><p><img src="/../images/image-20221225125352222.png" alt="image-20221225125352222">访问localhost:80</p><p><img src="/../images/image-20221225125404667.png" alt="image-20221225125404667">访问0.0.0.0:80</p><p><img src="/../images/image-20221225125416747.png" alt="image-20221225125416747">访问本机的IP地址</p><p>但本质上还是有些区别的。</p><p>首先 <code>localhost</code> 就不叫 <code>IP</code>,它是一个域名,就跟 <code>"baidu.com"</code>,是一个形式的东西,只不过默认会把它解析为 <code>127.0.0.1</code> ,当然这可以在 <code>/etc/hosts</code> 文件下进行修改。</p><p>所以默认情况下,使用 <code>localhost</code> 跟使用 <code>127.0.0.1</code> 确实是没区别的。</p><p>其次就是 <code>0.0.0.0</code>,执行 ping 0.0.0.0 ,是会失败的,因为它在<code>IPV4</code>中表示的是无效的<strong>目标地址</strong>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">$ ping 0.0.0.0</span><br><span class="line">PING 0.0.0.0 (0.0.0.0): 56 data bytes</span><br><span class="line">ping: sendto: No route to host</span><br><span class="line">ping: sendto: No route to host</span><br></pre></td></tr></table></figure><p>但它还是很有用处的,回想下,我们启动服务器的时候,一般会 <code>listen</code> 一个 IP 和端口,等待客户端的连接。</p><p>如果此时 <code>listen</code> 的是本机的 <code>0.0.0.0</code> , 那么它表示本机上的<strong>所有IPV4地址</strong>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">/* Address to accept any incoming messages. */</span><br><span class="line">#define INADDR_ANY ((unsigned long int) 0x00000000) /* 0.0.0.0 */</span><br></pre></td></tr></table></figure><p>举个例子。刚刚提到的 <code>127.0.0.1</code> 和 <code>192.168.31.6</code> ,都是本机的IPV4地址,如果监听 <code>0.0.0.0</code> ,那么用上面两个地址,都能访问到这个服务器。</p><p>当然, 客户端 <code>connect</code> 时,不能使用 <code>0.0.0.0</code> 。必须指明要连接哪个服务器IP。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><ul><li><code>127.0.0.1</code> 是<strong>回环地址</strong>。<code>localhost</code>是<strong>域名</strong>,但默认等于 <code>127.0.0.1</code>。</li><li><code>ping</code> 回环地址和 <code>ping</code> 本机地址,是一样的,走的是<strong>lo0 “假网卡”<strong>,都会经过网络层和数据链路层等逻辑,最后在快要出网卡前</strong>狠狠拐了个弯</strong>, 将数据插入到一个<strong>链表</strong>后就<strong>软中断</strong>通知 <strong>ksoftirqd</strong> 来进行<strong>收数据</strong>的逻辑,<strong>压根就不出网络</strong>。所以断网了也能 <code>ping</code> 通回环地址。</li><li>如果服务器 <code>listen</code> 的是 <code>0.0.0.0</code>,那么此时用<code>127.0.0.1</code>和本机地址<strong>都可以</strong>访问到服务。</li></ul>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> 计算机网络 </tag>
</tags>
</entry>
<entry>
<title>ThreadLocal内存泄漏</title>
<link href="/2022/12/25/ThreadLocal%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/"/>
<url>/2022/12/25/ThreadLocal%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F/</url>
<content type="html"><![CDATA[<h1 id="ThreadLocaln内存泄漏"><a href="#ThreadLocaln内存泄漏" class="headerlink" title="ThreadLocaln内存泄漏"></a>ThreadLocaln内存泄漏</h1><h1 id="ThreadLocal是什么"><a href="#ThreadLocal是什么" class="headerlink" title="ThreadLocal是什么"></a>ThreadLocal是什么</h1><p>ThreadLocal是一个本地线程副本变量工具类。主要用于将私有线程和该线程存放的副本对象做一个映射,各个线程之间的变量互不干扰,在高并发场景下,可以实现无状态的调用,特别适用于各个线程依赖不通的变量值完成操作的场景。</p><p>下图为ThreadLocal的内部结构图</p><p><img src="/../images/image-20221225124856643.png" alt="image-20221225124856643"> </p><p><strong>从上面的结构图,我们已经窥见ThreadLocal的核心机制:</strong></p><ul><li>每个Thread线程内部都有一个Map。</li><li>Map里面存储线程本地对象(key)和线程的变量副本(value)</li><li>但是,Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。</li></ul><p>所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。</p><h1 id="ThreadLocalMap"><a href="#ThreadLocalMap" class="headerlink" title="ThreadLocalMap"></a>ThreadLocalMap</h1><p><img src="/../images/image-20221225124921832.png" alt="image-20221225124921832"><br>ThreadLocalMap是ThreadLocal的内部类,没有实现Map接口,用独立的方式实现了Map的功能,其内部的Entry也独立实现。</p><p>和HashMap的最大的不同在于,ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表的方式,而是<code>采用线性探测的方式</code>。(<strong>ThreadLocalMap如何解决冲突?</strong>)</p><p>在ThreadLocalMap中,也是用Entry来保存K-V结构数据的。但是Entry中key只能是ThreadLocal对象,这点被Entry的构造方法已经限定死了。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">static class Entry extends WeakReference<ThreadLocal> {</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> /** The value associated with this ThreadLocal. */</span><br><span class="line"> Object value;</span><br><span class="line"></span><br><span class="line"> Entry(ThreadLocal k, Object v) {</span><br><span class="line"> </span><br><span class="line"> </span><br><span class="line"> super(k);</span><br><span class="line"> value = v;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>注意了!!</code><br><strong>Entry继承自WeakReference(<code>弱引用,生命周期只能存活到下次GC前</code>),但只有Key是弱引用类型的,Value并非弱引用。</strong>(<code>问题马上就来了</code>)</p><p>由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,<strong>发生GC时弱引用Key会被回收,而Value不会回收</strong>。</p><p>当线程没有结束,但是ThreadLocal已经被回收,则可能导致线程中存在ThreadLocalMap<null, Object>的键值对,<strong>造成内存泄露。</strong>(<code>ThreadLocal被回收,ThreadLocal关联的线程共享变量还存在</code>)。</p><h1 id="如何避免泄漏"><a href="#如何避免泄漏" class="headerlink" title="如何避免泄漏"></a>如何避免泄漏</h1><p>为了防止此类情况的出现,我们有两种手段。</p><p>1、使用完线程共享变量后,显示调用ThreadLocalMap.remove方法清除线程共享变量;</p><p>既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再<strong>调用remove方法,将Entry节点和Map的引用关系移除</strong>,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。</p><p>2、JDK建议ThreadLocal定义为private static,这样ThreadLocal的弱引用问题则不存在了。</p>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> java </tag>
</tags>
</entry>
<entry>
<title>ThreadLocal</title>
<link href="/2022/12/24/ThreadLocal/"/>
<url>/2022/12/24/ThreadLocal/</url>
<content type="html"><![CDATA[<p><img src="/../images/image-20221224231606153.png" alt="image-20221224231606153"></p><ul><li>ThreadLocal的作用以及应用场景</li><li>使用场景</li><li>原理分析</li><li>ThreadLocalMap的底层结构</li><li>内存泄露产生的原因</li><li>解决Hash冲突</li><li>使用ThreadLocal时对象存在哪里?</li></ul><hr><h2 id="ThreadLocal的作用以及应用场景"><a href="#ThreadLocal的作用以及应用场景" class="headerlink" title="ThreadLocal的作用以及应用场景"></a><strong>ThreadLocal的作用以及应用场景</strong></h2><p><code>ThreadLocal</code>算是一种并发容器吧,因为他的内部是有<code>ThreadLocalMap</code>组成,<code>ThreadLocal</code>是为了解决多线程情况下变量不能被共享的问题,也就是多线程共享变量的问题。</p><p><code>ThreadLocal</code>和<code>Lock</code>以及<code>Synchronized</code>的区别是:<code>ThreadLocal</code>是给每个线程分配一个变量(对象),各个线程都存有变量的副本,这样每个线程都是使用自己(变量)对象实例,使线程与线程之间进行隔离;而<code>Lock</code>和<code>Synchronized</code>的方式是使线程有顺序的执行。</p><p>举一个简单的例子:目前有100个学生等待签字,但是老师只有一个笔,那老师只能按顺序的分给每个学生,等待A学生签字完成然后将笔交给B学生,这就类似<code>Lock</code>,<code>Synchronized</code>的方式。而<code>ThreadLocal</code>是,老师直接拿出一百个笔给每个学生;再效率提高的同事也要付出一个内存消耗;也就是以空间换时间的概念</p><h2 id="使用场景"><a href="#使用场景" class="headerlink" title="使用场景"></a><strong>使用场景</strong></h2><p>Spring的事务隔离就是使用<code>ThreadLocal</code>和AOP来解决的;主要是<code>TransactionSynchronizationManager</code>这个类;</p><p>解决<code>SimpleDateFormat</code>线程不安全问题;</p><p>当我们使用<code>SimpleDateFormat</code>的<code>parse()</code>方法的时候,<code>parse()</code>方法会先调用<code>Calendar.clear()</code>方法,然后调用<code>Calendar.add()</code>方法,如果一个线程先调用了<code>add()</code>方法,然后另一个线程调用了<code>clear()</code>方法;这时候<code>parse()</code>方法就会出现解析错误;如果不信我们可以来个例子:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br></pre></td><td class="code"><pre><span class="line">public class SimpleDateFormatTest {</span><br><span class="line"></span><br><span class="line"> private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");</span><br><span class="line"></span><br><span class="line"> public static void main(String[] args) {</span><br><span class="line"> for (int i = 0; i < 50; i++) {</span><br><span class="line"> Thread thread = new Thread(new Runnable() {</span><br><span class="line"> @Override</span><br><span class="line"> public void run() {</span><br><span class="line"> dateFormat();</span><br><span class="line"> }</span><br><span class="line"> });</span><br><span class="line"> thread.start();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> /**</span><br><span class="line"> * 字符串转成日期类型</span><br><span class="line"> */</span><br><span class="line"> public static void dateFormat() {</span><br><span class="line"> try {</span><br><span class="line"> simpleDateFormat.parse("2021-5-27");</span><br><span class="line"> } catch (ParseException 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></table></figure><p>这里我们只启动了50个线程问题就会出现,其实看巧不巧,有时候只有10个线程的情况就会出错:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">Exception in thread "Thread-40" java.lang.NumberFormatException: For input string: ""</span><br><span class="line"> at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)</span><br><span class="line"> at java.lang.Long.parseLong(Long.java:601)</span><br><span class="line"> at java.lang.Long.parseLong(Long.java:631)</span><br><span class="line"> at java.text.DigitList.getLong(DigitList.java:195)</span><br><span class="line"> at java.text.DecimalFormat.parse(DecimalFormat.java:2084)</span><br><span class="line"> at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)</span><br><span class="line"> at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)</span><br><span class="line"> at java.text.DateFormat.parse(DateFormat.java:364)</span><br><span class="line"> at cn.haoxy.use.lock.sdf.SimpleDateFormatTest.dateFormat(SimpleDateFormatTest.java:36)</span><br><span class="line"> at cn.haoxy.use.lock.sdf.SimpleDateFormatTest$1.run(SimpleDateFormatTest.java:23)</span><br><span class="line"> at java.lang.Thread.run(Thread.java:748)</span><br><span class="line">Exception in thread "Thread-43" java.lang.NumberFormatException: multiple points</span><br><span class="line"> at sun.misc.FloatingDecimal.readJavaFormatString(FloatingDecimal.java:1890)</span><br><span class="line"> at sun.misc.FloatingDecimal.parseDouble(FloatingDecimal.java:110)</span><br><span class="line"> at java.lang.Double.parseDouble(Double.java:538)</span><br><span class="line"> at java.text.DigitList.getDouble(DigitList.java:169)</span><br><span class="line"> at java.text.DecimalFormat.parse(DecimalFormat.java:2089)</span><br><span class="line"> at java.text.SimpleDateFormat.subParse(SimpleDateFormat.java:1869)</span><br><span class="line"> at java.text.SimpleDateFormat.parse(SimpleDateFormat.java:1514)</span><br><span class="line"> at java.text.DateFormat.parse(DateFormat.java:364)</span><br><span class="line"> at .............</span><br></pre></td></tr></table></figure><p>其实解决这个问题很简单,让每个线程new一个自己的<code>SimpleDateFormat</code>,但是如果100个线程都要new100个<code>SimpleDateFormat</code>吗?</p><p>当然我们不能这么做,我们可以借助线程池加上<code>ThreadLocal</code>来解决这个问题:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br></pre></td><td class="code"><pre><span class="line">public class SimpleDateFormatTest {</span><br><span class="line"></span><br><span class="line"> private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {</span><br><span class="line"> @Override</span><br><span class="line"> //初始化线程本地变量</span><br><span class="line"> protected SimpleDateFormat initialValue() {</span><br><span class="line"> return new SimpleDateFormat("yyyy-MM-dd");</span><br><span class="line"> }</span><br><span class="line"> };</span><br><span class="line"></span><br><span class="line"> public static void main(String[] args) {</span><br><span class="line"> ExecutorService es = Executors.newCachedThreadPool();</span><br><span class="line"> for (int i = 0; i < 500; i++) {</span><br><span class="line"> es.execute(() -> {</span><br><span class="line"> //调用字符串转成日期方法</span><br><span class="line"> dateFormat();</span><br><span class="line"> });</span><br><span class="line"> }</span><br><span class="line"> es.shutdown();</span><br><span class="line"> }</span><br><span class="line"> /**</span><br><span class="line"> * 字符串转成日期类型</span><br><span class="line"> */</span><br><span class="line"> public static void dateFormat() {</span><br><span class="line"> try {</span><br><span class="line"> //ThreadLocal中的get()方法</span><br><span class="line"> local.get().parse("2021-5-27");</span><br><span class="line"> } catch (ParseException 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></table></figure><p>这样就优雅的解决了线程安全问题;</p><p>解决过度传参问题;例如一个方法中要调用好多个方法,每个方法都需要传递参数;例如下面示例:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">void work(User user) {</span><br><span class="line"> getInfo(user);</span><br><span class="line"> checkInfo(user);</span><br><span class="line"> setSomeThing(user);</span><br><span class="line"> log(user);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>用了<code>ThreadLocal</code>之后:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">public class ThreadLocalStu {</span><br><span class="line"></span><br><span class="line"> private static ThreadLocal<User> userThreadLocal = new ThreadLocal<>();</span><br><span class="line"></span><br><span class="line"> void work(User user) {</span><br><span class="line"> try {</span><br><span class="line"> userThreadLocal.set(user);</span><br><span class="line"> getInfo();</span><br><span class="line"> checkInfo();</span><br><span class="line"> someThing();</span><br><span class="line"> } finally {</span><br><span class="line"> userThreadLocal.remove();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> void setInfo() {</span><br><span class="line"> User u = userThreadLocal.get();</span><br><span class="line"> //.....</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> void checkInfo() {</span><br><span class="line"> User u = userThreadLocal.get();</span><br><span class="line"> //....</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> void someThing() {</span><br><span class="line"> User u = userThreadLocal.get();</span><br><span class="line"> //....</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>每个线程内需要保存全局变量(比如在登录成功后将用户信息存到<code>ThreadLocal</code>里,然后当前线程操作的业务逻辑直接get取就完事了,有效的避免的参数来回传递的麻烦之处),一定层级上减少代码耦合度。</p><ul><li>比如存储 交易id等信息。每个线程私有。</li><li>比如aop里记录日志需要before记录请求id,end拿出请求id,这也可以。</li><li>比如jdbc连接池(很典型的一个<code>ThreadLocal</code>用法)</li><li>….等等….</li></ul><h2 id="原理分析"><a href="#原理分析" class="headerlink" title="原理分析"></a><strong>原理分析</strong></h2><p>上面我们基本上知道了<code>ThreadLocal</code>的使用方式以及应用场景,当然应用场景不止这些这只是工作中常用到的场景;下面我们对它的原理进行分析;</p><p>我们先看一下它的<code>set()</code>方法;</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">public void set(T value) {</span><br><span class="line"> Thread t = Thread.currentThread();</span><br><span class="line"> ThreadLocalMap map = getMap(t);</span><br><span class="line"> if (map != null)</span><br><span class="line"> map.set(this, value);</span><br><span class="line"> else</span><br><span class="line"> createMap(t, value);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>是不是特别简单,首先获取当前线程,用当前线程作为key,去获取<code>ThreadLocalMap</code>,然后判断map是否为空,不为空就将当前线程作为key,传入的value作为map的value值;如果为空就创建一个<code>ThreadLocalMap</code>,然后将key和value方进去;从这里可以看出value值是存放到<code>ThreadLocalMap</code>中;</p><p>然后我们看看<code>ThreadLocalMap</code>是怎么来的?先看下<code>getMap()</code>方法:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">//在Thread类中维护了threadLocals变量,注意是Thread类</span><br><span class="line">ThreadLocal.ThreadLocalMap threadLocals = null; </span><br><span class="line"></span><br><span class="line">//在ThreadLocal类中的getMap()方法</span><br><span class="line">ThreadLocalMap getMap(Thread t) {</span><br><span class="line"> return t.threadLocals;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>这就能解释每个线程中都有一个<code>ThreadLocalMap</code>,因为<code>ThreadLocalMap</code>的引用在Thread中维护;这就确保了线程间的隔离;</p><p>我们继续回到<code>set()</code>方法,看到当map等于空的时候<code>createMap(t, value);</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">void createMap(Thread t, T firstValue) {</span><br><span class="line"> t.threadLocals = new ThreadLocalMap(this, firstValue);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>这里就是new了一个<code>ThreadLocalMap</code>然后赋值给<code>threadLocals</code>成员变量;<code>ThreadLocalMap</code>构造方法:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {</span><br><span class="line"> //初始化一个Entry </span><br><span class="line"> table = new Entry[INITIAL_CAPACITY];</span><br><span class="line"> //计算key应该存放的位置</span><br><span class="line"> int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);</span><br><span class="line"> //将Entry放到指定位置</span><br><span class="line"> table[i] = new Entry(firstKey, firstValue);</span><br><span class="line"> size = 1;</span><br><span class="line"> //设置数组的大小 16*2/3=10,类似HashMap中的0.75*16=12</span><br><span class="line"> setThreshold(INITIAL_CAPACITY);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>这里写有个大概的印象,后面对<code>ThreadLocalMap</code>内部结构还会进行详细的讲解;</p><p>下面我们再去看一下<code>get()</code>方法:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">public T get() {</span><br><span class="line"> Thread t = Thread.currentThread();</span><br><span class="line"> //用当前线程作为key去获取ThreadLocalMap</span><br><span class="line"> ThreadLocalMap map = getMap(t);</span><br><span class="line"> if (map != null) {</span><br><span class="line"> //map不为空,然后获取map中的Entry</span><br><span class="line"> ThreadLocalMap.Entry e = map.getEntry(this);</span><br><span class="line"> if (e != null) {</span><br><span class="line"> @SuppressWarnings("unchecked")</span><br><span class="line"> //如果Entry不为空就获取对应的value值</span><br><span class="line"> T result = (T)e.value;</span><br><span class="line"> return result;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> //如果map为空或者entry为空的话通过该方法初始化,并返回该方法的value</span><br><span class="line"> return setInitialValue();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><code>get()</code>方法和<code>set()</code>都比较容易理解,如果map等于空的时候或者entry等于空的时候我们看看<code>setInitialValue()</code>方法做了什么事:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">private T setInitialValue() {</span><br><span class="line"> //初始化变量值 由子类去实现并初始化变量</span><br><span class="line"> T value = initialValue();</span><br><span class="line"> Thread t = Thread.currentThread();</span><br><span class="line"> //这里再次getMap();</span><br><span class="line"> ThreadLocalMap map = getMap(t);</span><br><span class="line"> if (map != null)</span><br><span class="line"> map.set(this, value);</span><br><span class="line"> else</span><br><span class="line"> //和set()方法中的</span><br><span class="line"> createMap(t, value);</span><br><span class="line"> return value;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>下面我们再去看一下<code>ThreadLocal</code>中的<code>initialValue()</code>方法:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">protected T initialValue() {</span><br><span class="line"> return null;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>设置初始值,由子类去实现;就例如我们上面的例子,重写<code>ThreadLocal</code>类中的<code>initialValue()</code>方法:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">private static ThreadLocal<SimpleDateFormat> local = new ThreadLocal<SimpleDateFormat>() {</span><br><span class="line"> @Override</span><br><span class="line"> //初始化线程本地变量</span><br><span class="line"> protected SimpleDateFormat initialValue() {</span><br><span class="line"> return new SimpleDateFormat("yyyy-MM-dd");</span><br><span class="line"> }</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p><code>createMap()</code>方法和上面<code>set()</code>方法中<code>createMap()</code>方法同一个,就不过多的叙述了;剩下还有一个<code>removve()</code>方法</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">public void remove() {</span><br><span class="line"> ThreadLocalMap m = getMap(Thread.currentThread());</span><br><span class="line"> if (m != null)</span><br><span class="line"> //2. 从map中删除以当前threadLocal实例为key的键值对</span><br><span class="line"> m.remove(this);</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>源码的讲解就到这里,也都比较好理解,下面我们看看<code>ThreadLocalMap</code>的底层结构</p><h2 id="ThreadLocalMap的底层结构"><a href="#ThreadLocalMap的底层结构" class="headerlink" title="ThreadLocalMap的底层结构"></a><strong>ThreadLocalMap的底层结构</strong></h2><p>上面我们已经了解了<code>ThreadLocal</code>的使用场景以及它比较重要的几个方法;下面我们再去它的内部结构;经过上的源码分析我们可以看到数据其实都是存放到了<code>ThreadLocal</code>中的内部类<code>ThreadLocalMap</code>中;而<code>ThreadLocalMap</code>中又维护了一个Entry对象,也就说数据最终是存放到Entry对象中的;</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line">static class ThreadLocalMap {</span><br><span class="line"></span><br><span class="line"> static class Entry extends WeakReference<ThreadLocal<?>> {</span><br><span class="line"> /** The value associated with this ThreadLocal. */</span><br><span class="line"> Object value;</span><br><span class="line"></span><br><span class="line"> Entry(ThreadLocal<?> k, Object v) {</span><br><span class="line"> super(k);</span><br><span class="line"> value = v;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> }</span><br><span class="line"> ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {</span><br><span class="line"> table = new Entry[INITIAL_CAPACITY];</span><br><span class="line"> int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);</span><br><span class="line"> table[i] = new Entry(firstKey, firstValue);</span><br><span class="line"> size = 1;</span><br><span class="line"> setThreshold(INITIAL_CAPACITY);</span><br><span class="line"> }</span><br><span class="line"> // ....................</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>Entry的构造方法是以当前线程为key,变量值Object为value进行存储的;在上面的源码中<code>ThreadLocalMap</code>的构造方法中也涉及到了Entry;看到Entry是一个数组;初始化长度为<code>INITIAL_CAPACITY = 16;</code>因为 Entry 继承了 <code>WeakReference</code>,在 Entry 的构造方法中,调用了<code> super(k)</code>方法就会将 <code>threadLocal</code> 实例包装成一个 <code>WeakReferenece</code>。这也是<code>ThreadLocal</code>会产生内存泄露的原因;</p><h2 id="内存泄露产生的原因"><a href="#内存泄露产生的原因" class="headerlink" title="内存泄露产生的原因"></a><strong>内存泄露产生的原因</strong></h2><p><img src="/../images/image-20221224232314960.png" alt="image-20221224232314960"></p><p>如图所示存在一条引用链:<code>Thread Ref->Thread->ThreadLocalMap->Entry->Key:Value</code>,经过上面的讲解我们知道<code>ThreadLocal</code>作为Key,但是被设置成了弱引用,弱引用在JVM垃圾回收时是优先回收的,就是说无论内存是否足够弱引用对象都会被回收;弱引用的生命周期比较短;当发生一次GC的时候就会变成如下:</p><p><img src="/../images/image-20221224232336814.png" alt="image-20221224232336814"></p><p><code>TreadLocalMap</code>中出现了Key为null的Entry,就没有办法访问这些key为null的Entry的value,如果线程迟迟不结束(也就是说这条引用链无意义的一直存在)就会造成value永远无法回收造成内存泄露;如果当前线程运行结束Thread,<code>ThreadLocalMap</code>,Entry之间没有了引用链,在垃圾回收的时候就会被回收;但是在开发中我们都是使用线程池的方式,线程池的复用不会主动结束;所以还是会存在内存泄露问题;</p><p>解决方法也很简单,就是在使用完之后主动调用<code>remove()</code>方法释放掉;</p><h2 id="解决Hash冲突"><a href="#解决Hash冲突" class="headerlink" title="解决Hash冲突"></a><strong>解决Hash冲突</strong></h2><p>记得在大学学习数据结构的时候学习了很多种解决hash冲突的方法;例如:</p><p><strong>线性探测法(开放地址法的一种):</strong> 计算出的散列地址如果已被占用,则按顺序找下一个空位。如果找到末尾还没有找到空位置就从头重新开始找;</p><p><img src="/../images/image-20221224232357557.png" alt="image-20221224232357557"></p><p>图片</p><p><strong>二次探测法(开放地址法的一种)</strong></p><p><img src="/../images/image-20221224232414070.png" alt="image-20221224232414070"></p><p>图片</p><p>链地址法:链地址是对每一个同义词都建一个单链表来解决冲突,HashMap采用的是这种方法;</p><p><img src="/../images/image-20221224232429196.png" alt="image-20221224232429196"></p><p><strong>多重Hash法:</strong> 在key冲突的情况下多重hash,直到不冲突为止,这种方式不易产生堆积但是计算量太大;</p><p><strong>公共溢出区法:</strong> 这种方式需要两个表,一个存基础数据,另一个存放冲突数据称为溢出表;</p><p>上面的图片都是在网上找到的一些资料,和大学时学习时的差不多我就直接拿来用了;也当自己复习了一遍;</p><p>介绍了那么多解决Hash冲突的方法,那<code>ThreadLocalMap</code>使用的哪一种方法呢?我们可以看一下源码:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">private void set(ThreadLocal<?> key, Object value) {</span><br><span class="line"> Entry[] tab = table;</span><br><span class="line"> int len = tab.length;</span><br><span class="line"> //根据HashCode & 数组长度 计算出数组该存放的位置</span><br><span class="line"> int i = key.threadLocalHashCode & (len-1);</span><br><span class="line"> //遍历Entry数组中的元素</span><br><span class="line"> for (Entry e = tab[i];</span><br><span class="line"> e != null;</span><br><span class="line"> e = tab[i = nextIndex(i, len)]) {</span><br><span class="line"> ThreadLocal<?> k = e.get();</span><br><span class="line"> //如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;</span><br><span class="line"> if (k == key) {</span><br><span class="line"> e.value = value;</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> // entry!=null,key==null时,说明threadLcoal这key已经被GC了,这里就是上面说到</span><br><span class="line"> //会有内存泄露的地方,当然作者也知道这种情况的存在,所以这里做了一个判断进行解决脏的</span><br><span class="line"> //entry(数组中不想存有过时的entry),但是也不能解决泄露问题,因为旧value还存在没有消失</span><br><span class="line"> if (k == null) {</span><br><span class="line"> //用当前插入的值代替掉这个key为null的“脏”entry</span><br><span class="line"> replaceStaleEntry(key, value, i);</span><br><span class="line"> return;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> //新建entry并插入table中i处</span><br><span class="line"> tab[i] = new Entry(key, value);</span><br><span class="line"> int sz = ++size;</span><br><span class="line"> if (!cleanSomeSlots(i, sz) && sz >= threshold)</span><br><span class="line"> rehash();</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>从这里我们可以看出使用的是线性探测的方式来解决hash冲突!</p><p>源码中通过<code>nextIndex(i, len)</code>方法解决 hash 冲突的问题,该方法为<code>((i + 1 < len) ? i + 1 : 0);</code>,也就是不断往后线性探测,直到找到一个空的位置,当到哈希表末尾的时候还没有找到空位置再从 0 开始找,成环形!</p><h2 id="使用ThreadLocal时对象存在哪里?"><a href="#使用ThreadLocal时对象存在哪里?" class="headerlink" title="使用ThreadLocal时对象存在哪里?"></a><strong>使用ThreadLocal时对象存在哪里?</strong></h2><p>在java中,栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有变量,而堆内存中的变量对所有线程可见,可以被所有线程访问!</p><p>那么<code>ThreadLocal</code>的实例以及它的值是不是存放在栈上呢?其实不是的,因为<code>ThreadLocal</code>的实例实际上也是被其创建的类持有,(更顶端应该是被线程持有),而<code>ThreadLocal</code>的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。</p>]]></content>
<categories>
<category> 计算机技术 </category>
</categories>
<tags>
<tag> java </tag>
</tags>
</entry>
<entry>
<title></title>
<link href="/css/custom.css"/>
<url>/css/custom.css</url>
<content type="html"><![CDATA[/* 页脚与头图透明 */#footer { background: transparent !important;}#page-header { background: transparent !important;}/* 白天模式遮罩透明 */#footer::before { background: transparent !important;}#page-header::before { background: transparent !important;}/* 夜间模式遮罩透明 */[data-theme="dark"] #footer::before { background: transparent !important;}[data-theme="dark"] #page-header::before { background: transparent !important;}/* 翻页按钮居中 */#pagination { width: 100%; margin: auto;}/* 一级菜单居中 */#nav .menus_items { position: absolute !important; width: fit-content !important; left: 50% !important; transform: translateX(-50%) !important;}/* 子菜单横向展示 */#nav .menus_items .menus_item:hover .menus_item_child { display: flex !important;}/* 这里的2是代表导航栏的第2个元素,即有子菜单的元素,可以按自己需求修改 */.menus_items .menus_item:nth-child(2) .menus_item_child { left: -125px;}/* 夜间模式菜单栏发光字 */[data-theme="dark"] #nav .site-page,[data-theme="dark"] #nav .menus_items .menus_item .menus_item_child li a { text-shadow: 0 0 2px var(--theme-color) !important;}/* 手机端适配 */[data-theme="dark"] #sidebar #sidebar-menus .menus_items .site-page { text-shadow: 0 0 2px var(--theme-color) !important;}/* 侧边栏个人信息卡片动态渐变色 */#aside-content > .card-widget.card-info { background: linear-gradient( -45deg, #e8d8b9, #eccec5, #a3e9eb, #bdbdf0, #eec1ea ); box-shadow: 0 0 5px rgb(66, 68, 68); position: relative; background-size: 400% 400%; -webkit-animation: Gradient 10s ease infinite; -moz-animation: Gradient 10s ease infinite; animation: Gradient 10s ease infinite !important;}@-webkit-keyframes Gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; }}@-moz-keyframes Gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; }}@keyframes Gradient { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; }}/* 黑夜模式适配 */[data-theme="dark"] #aside-content > .card-widget.card-info { background: #191919ee;}/* 个人信息Follow me按钮 */#aside-content > .card-widget.card-info > #card-info-btn { background-color: #3eb8be; border-radius: 8px;}]]></content>
</entry>
<entry>
<title>about</title>
<link href="/about/index.html"/>
<url>/about/index.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title></title>
<link href="/css/universe.css"/>
<url>/css/universe.css</url>
<content type="html"><![CDATA[/* 背景宇宙星光 */#universe{ display: block; position: fixed; margin: 0; padding: 0; border: 0; outline: 0; left: 0; top: 0; width: 100%; height: 100%; pointer-events: none; /* 这个是调置顶的优先级的,-1在文章页下面,背景上面,个人推荐这种 */ z-index: -1;}]]></content>
</entry>
<entry>
<title>categories</title>
<link href="/categories/index.html"/>
<url>/categories/index.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>Gallery</title>
<link href="/Gallery/index.html"/>
<url>/Gallery/index.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title></title>
<link href="/js/light.js"/>
<url>/js/light.js</url>
<content type="html"><![CDATA[// 霓虹灯效果// 颜色数组var arr = ["#39c5bb", "#f14747", "#f1a247", "#f1ee47", "#b347f1", "#1edbff", "#ed709b", "#5636ed"];// 颜色索引var idx = 0;// 切换颜色function changeColor() { // 仅夜间模式才启用 if (document.getElementsByTagName('html')[0].getAttribute('data-theme') == 'dark') { if (document.getElementById("site-name")) document.getElementById("site-name").style.textShadow = arr[idx] + " 0 0 15px"; if (document.getElementById("site-title")) document.getElementById("site-title").style.textShadow = arr[idx] + " 0 0 15px"; if (document.getElementById("site-subtitle")) document.getElementById("site-subtitle").style.textShadow = arr[idx] + " 0 0 10px"; if (document.getElementById("post-info")) document.getElementById("post-info").style.textShadow = arr[idx] + " 0 0 5px"; try { document.getElementsByClassName("author-info__name")[0].style.textShadow = arr[idx] + " 0 0 12px"; document.getElementsByClassName("author-info__description")[0].style.textShadow = arr[idx] + " 0 0 12px"; } catch { } idx++; if (idx == 8) { idx = 0; } } else { // 白天模式恢复默认 if (document.getElementById("site-name")) document.getElementById("site-name").style.textShadow = "#1e1e1ee0 1px 1px 1px"; if (document.getElementById("site-title")) document.getElementById("site-title").style.textShadow = "#1e1e1ee0 1px 1px 1px"; if (document.getElementById("site-subtitle")) document.getElementById("site-subtitle").style.textShadow = "#1e1e1ee0 1px 1px 1px"; if (document.getElementById("post-info")) document.getElementById("post-info").style.textShadow = "#1e1e1ee0 1px 1px 1px"; try { document.getElementsByClassName("author-info__name")[0].style.textShadow = ""; document.getElementsByClassName("author-info__description")[0].style.textShadow = ""; } catch { } } } // 开启计时器 window.onload = setInterval(changeColor, 1200);]]></content>
</entry>
<entry>
<title></title>
<link href="/js/universe.js"/>
<url>/js/universe.js</url>
<content type="html"><![CDATA[function dark() {window.requestAnimationFrame=window.requestAnimationFrame||window.mozRequestAnimationFrame||window.webkitRequestAnimationFrame||window.msRequestAnimationFrame;var n,e,i,h,t=.05,s=document.getElementById("universe"),o=!0,a="180,184,240",r="226,225,142",d="226,225,224",c=[];function f(){n=window.innerWidth,e=window.innerHeight,i=.216*n,s.setAttribute("width",n),s.setAttribute("height",e)}function u(){h.clearRect(0,0,n,e);for(var t=c.length,i=0;i<t;i++){var s=c[i];s.move(),s.fadeIn(),s.fadeOut(),s.draw()}}function y(){this.reset=function(){this.giant=m(3),this.comet=!this.giant&&!o&&m(10),this.x=l(0,n-10),this.y=l(0,e),this.r=l(1.1,2.6),this.dx=l(t,6*t)+(this.comet+1-1)*t*l(50,120)+2*t,this.dy=-l(t,6*t)-(this.comet+1-1)*t*l(50,120),this.fadingOut=null,this.fadingIn=!0,this.opacity=0,this.opacityTresh=l(.2,1-.4*(this.comet+1-1)),this.do=l(5e-4,.002)+.001*(this.comet+1-1)},this.fadeIn=function(){this.fadingIn&&(this.fadingIn=!(this.opacity>this.opacityTresh),this.opacity+=this.do)},this.fadeOut=function(){this.fadingOut&&(this.fadingOut=!(this.opacity<0),this.opacity-=this.do/2,(this.x>n||this.y<0)&&(this.fadingOut=!1,this.reset()))},this.draw=function(){if(h.beginPath(),this.giant)h.fillStyle="rgba("+a+","+this.opacity+")",h.arc(this.x,this.y,2,0,2*Math.PI,!1);else if(this.comet){h.fillStyle="rgba("+d+","+this.opacity+")",h.arc(this.x,this.y,1.5,0,2*Math.PI,!1);for(var t=0;t<30;t++)h.fillStyle="rgba("+d+","+(this.opacity-this.opacity/20*t)+")",h.rect(this.x-this.dx/4*t,this.y-this.dy/4*t-2,2,2),h.fill()}else h.fillStyle="rgba("+r+","+this.opacity+")",h.rect(this.x,this.y,this.r,this.r);h.closePath(),h.fill()},this.move=function(){this.x+=this.dx,this.y+=this.dy,!1===this.fadingOut&&this.reset(),(this.x>n-n/4||this.y<0)&&(this.fadingOut=!0)},setTimeout(function(){o=!1},50)}function m(t){return Math.floor(1e3*Math.random())+1<10*t}function l(t,i){return Math.random()*(i-t)+t}f(),window.addEventListener("resize",f,!1),function(){h=s.getContext("2d");for(var t=0;t<i;t++)c[t]=new y,c[t].reset();u()}(),function t(){document.getElementsByTagName('html')[0].getAttribute('data-theme')=='dark'&&u(),window.requestAnimationFrame(t)}()};dark()]]></content>
</entry>
<entry>
<title>link</title>
<link href="/link/index.html"/>
<url>/link/index.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>movies</title>
<link href="/movies/index.html"/>
<url>/movies/index.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>音乐</title>
<link href="/music/index.html"/>
<url>/music/index.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
<entry>
<title>tags</title>
<link href="/tags/index.html"/>
<url>/tags/index.html</url>
<content type="html"><![CDATA[]]></content>
</entry>
</search>