-
Notifications
You must be signed in to change notification settings - Fork 0
/
atom.xml
482 lines (248 loc) · 320 KB
/
atom.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<title>Qian0817</title>
<link href="http://qian0817.top/atom.xml" rel="self"/>
<link href="http://qian0817.top/"/>
<updated>2021-02-01T13:23:51.493Z</updated>
<id>http://qian0817.top/</id>
<author>
<name>qianlei</name>
</author>
<generator uri="https://hexo.io/">Hexo</generator>
<entry>
<title>Kubernetes调度器</title>
<link href="http://qian0817.top/2021/02/01/Kubernetes%E8%B0%83%E5%BA%A6%E5%99%A8/"/>
<id>http://qian0817.top/2021/02/01/Kubernetes%E8%B0%83%E5%BA%A6%E5%99%A8/</id>
<published>2021-02-01T13:21:23.000Z</published>
<updated>2021-02-01T13:23:51.493Z</updated>
<content type="html"><![CDATA[<p>在Kubernetes中,调度器的主要职责就是为一个新创建出来的Pod寻找一个合适的节点Node,其工作流程可以下图表示。</p><p><img data-src="%E8%B0%83%E5%BA%A6%E6%9C%BA%E5%88%B6.jpg" alt="img"></p><p>Kubernetes 的调度器由两个相互独立的控制循环组成。</p><p>第一个控制循环称为Informer Path,其主要目的在于启动一系列的Informer,用来监听Etcd中的Pod、Node、Service等对象的变化。例如当一个Pod被创建出来后,调度器会通过Pod Informer将待调度的Pod添加到调度队列之中。</p><p>第二个控制循环称为Scheduling Path。其作用是不断的从调度队列中出对一个Pod,调用Predicates算法进行过滤。通过这一步过滤得到的一组Node就是所有可以运行这个Pod的宿主机列表。接下来就可以调用Priorities算法为Node打分,分数从0到10,得分最高的Node就会作为本次调度的结果。在调度算法完成以后,调度器就会将Pod对象的nodeName字段修改为Node的名字,该步骤称为Bind。</p><h2 id="Predicates"><a href="#Predicates" class="headerlink" title="Predicates"></a>Predicates</h2><p>Predicates在调度过程中的作用就是从所有节点中去除一系列符合条件的节点。Kubernetes默认的调度策略有三种。</p><h3 id="GeneralPredicates"><a href="#GeneralPredicates" class="headerlink" title="GeneralPredicates"></a>GeneralPredicates</h3><p>GeneralPredicates负责最基础的调度策略,例如计算宿主机剩余的CPU和内存资源是否足够使用,检查宿主机的名字是否和Pod的spec.nodeName一致,申请的宿主机端口是否跟已经被使用的端口冲突,Pod 的 nodeSelector 或者 nodeAffinity 指定的节点,是否与待考察节点匹配。GeneralPredicates检查的是一个Pod能不能运行在Node上最基本的过滤条件。</p><h3 id="Volume-相关的过滤规则"><a href="#Volume-相关的过滤规则" class="headerlink" title="Volume 相关的过滤规则"></a>Volume 相关的过滤规则</h3><p>负责与容器持久化Volume相关的调度策略。例如多个 Pod 声明挂载的持久化 Volume 是否有冲突、一个节点上某种类型的持久化 Volume 是不是已经超过了一定数目、持久化 Volume 的 Zone(高可用域)标签,是否与待考察节点的 Zone 标签相匹配、Pod 对应的 PV 的 nodeAffinity 字段,是否跟某个节点的标签相匹配。</p><h3 id="宿主机相关的过滤规则"><a href="#宿主机相关的过滤规则" class="headerlink" title="宿主机相关的过滤规则"></a>宿主机相关的过滤规则</h3><p>检查Pod是否满足Node本身的一些条件,例如检查Pod 的Toleration字段与Node的Taint字段是否匹配,当前节点的内存是否足够。</p><h3 id="Pod-相关的过滤规则"><a href="#Pod-相关的过滤规则" class="headerlink" title="Pod 相关的过滤规则"></a>Pod 相关的过滤规则</h3><p>大部分与GeneralPredicates重合,特殊的在于还需检查待调度 Pod 与 Node 上的已有 Pod 之间的亲密(affinity)和反亲密(anti-affinity)关系。</p><p>上面四种类型的Predicates就构成了调度器确定一个 Node 可以运行待调度 Pod 的基本策略。</p><h2 id="Priorities"><a href="#Priorities" class="headerlink" title="Priorities"></a>Priorities</h2><p>Priorities阶段会根据不同Node的空闲资源,所有节点里各种资源分配最均衡,容器镜像是否存在以及容器镜像大小进行打分,打分的范围是 0-10 分,得分最高的节点就是最后被 Pod 绑定的最佳节点。</p><h2 id="优先级和抢占机制"><a href="#优先级和抢占机制" class="headerlink" title="优先级和抢占机制"></a>优先级和抢占机制</h2><p>正常情况下,当一个Pod调度失败后,会被暂时搁置起来,直到Pod被更新或者集群状态发生变化,调度器才会对这个Pod进行重新调度。但是有时候希望一个高级别的Pod调度失败以后,该Pod并不会被搁置,而是挤走某个Node上的低优先级Pod,保证这个高优先级的Pod调度成功。</p><p>在调度其中维护着一个调度队列,当Pod拥有了优先级以后,高优先级的Pod可能会比低优先级的Pod提前出队,从而尽早地完成调度过程。</p><p>当一个高优先级的Pod调度失败的时候,调度器的抢占能力就会被出发,调度器试图从当前集群中寻找一个节点,使得当这个节点上的低优先级Pod被删除后,待调度的高优先级Pod能够被调度到该节点上,这个过程就被称为抢占。</p><p>在Kubernetes中,在调度队列的视线中有两个不同的队列,第一个队列叫做activeQ,存储在下一个调度周期需要调度的对象。第二个队列叫做unschedulableQ,专门存放调度失败的Pod。当一个 unschedulableQ 里的 Pod 被更新之后,调度器会自动把这个 Pod 移动到 activeQ 里,重新进行调度。</p><p>当发生调度失败时,该Pod就会被放到unschedulableQ中,触发调度器为抢占者寻找牺牲者的流程。首先会检查这次失败事件的原因,确认抢占是不是可以帮助抢占者找到一个新节点。如果确定抢占可以发生,那么调度器就会把自己缓存的所有节点信息复制一份,然后使用这个副本来模拟抢占过程。在模拟过程中,调度器会检查每一个节点,从节点的最低优先级的Pod开始,逐一删除这些Pod,每删除一个低优先级Pod,调度器就会检查抢占者能够运行在该Node上。调度器会在模拟的所有抢占结果中做出一个选择,判断抢占对整个系统的影响,找出最佳结果。</p><p>当得到了最佳的抢占结果后,调度器就会真正的开始抢占的操作,删除被抢占者,让抢占者在下一个调度周期重新进入调度流程,之后调度器就会通过正常的调度流程把抢占者调度成功。</p>]]></content>
<summary type="html"><p>在Kubernetes中,调度器的主要职责就是为一个新创建出来的Pod寻找一个合适的节点Node,其工作流程可以下图表示。</p>
<p><img data-src="%E8%B0%83%E5%BA%A6%E6%9C%BA%E5%88%B6.jpg" alt="img"><</summary>
<category term="Kubernetes" scheme="http://qian0817.top/categories/Kubernetes/"/>
<category term="Kubernetes" scheme="http://qian0817.top/tags/Kubernetes/"/>
</entry>
<entry>
<title>HTTP各版本特性</title>
<link href="http://qian0817.top/2021/01/30/HTTP%E5%90%84%E7%89%88%E6%9C%AC%E7%89%B9%E6%80%A7/"/>
<id>http://qian0817.top/2021/01/30/HTTP%E5%90%84%E7%89%88%E6%9C%AC%E7%89%B9%E6%80%A7/</id>
<published>2021-01-30T04:24:04.000Z</published>
<updated>2021-01-30T04:24:04.118Z</updated>
<content type="html"><![CDATA[<p>HTTP(超文本传输协议)定义了浏览器如何向服务器请求万维网文档以及服务器如何将文档传送给浏览器。</p><h2 id="HTTP-0-9"><a href="#HTTP-0-9" class="headerlink" title="HTTP 0.9"></a>HTTP 0.9</h2><p>HTTP 0.9在请求时不支持请求头,并且只支持GET方法。</p><h2 id="HTTP-1-0"><a href="#HTTP-1-0" class="headerlink" title="HTTP 1.0"></a>HTTP 1.0</h2><p>HTTP1.0 加入了以下几个内容</p><ul><li><p>在请求中加入了HTTP版本号信息</p></li><li><p>加入了请求头</p></li><li><p>增加了HTTP Status Code作为状态码</p></li></ul><p>但是HTTP1.0在性能上有很大的问题,每次请求都需要建立一个TCP连接,并且是串行执行请求。</p><h2 id="HTTP-1-1"><a href="#HTTP-1-1" class="headerlink" title="HTTP 1.1"></a>HTTP 1.1</h2><p>HTTP 1.1的变化主要有以下几个内容</p><ol><li>设置 keepalive 让 HTTP 重用 TCP 连接,省下了每次握手都要进行 TCP 3 次握手的重大开销。也就是 HTTP 长连接。</li><li>支持以流水线的形式进行请求发送,只要一个请求发出去,不必等到其回来就可以发送第二个请求出去。</li><li>增加了 cache control 机制。</li><li>增加 HOST 头,服务器可以知道你在请求哪个网站。</li><li>加入了 OPTIONS 方法,用于 CORS 应用。</li></ol><h2 id="HTTPS"><a href="#HTTPS" class="headerlink" title="HTTPS"></a>HTTPS</h2><p>HTTPS在传输过程中通过加密来保护数据安全,避免用户敏感信息被第三方获取。其过程如下</p><ol><li>客户端发起HTTPS请求,告诉客户端自己所支持的加密套件列表以及希望使用的TLS选项。</li><li>服务端选择客户端的一个加密套件,会有一套数字证书,就是一对公钥和私钥。</li><li>传输证书,将公钥信息传递给客户端。</li><li>客户端解析证书,会验证公钥是否有效。一般操作系统和浏览器都会内置一个知名证书颁发机构的名单,也可以通过手动指定证书的方式来添加证书。如果证书存在问题,那么会弹出警示框告诉用户该网站不安全。如果证书没有问题,那么生成一个随机值作为会话密钥。</li><li>将随机值通过公钥进行加密,然后将加密后的随机值传输给服务端</li><li>服务端用私钥解密,获取到的随机值就是会话密钥。双方使用该密钥进行对称加密通信。</li></ol><h2 id="HTTP-2-0"><a href="#HTTP-2-0" class="headerlink" title="HTTP 2.0"></a>HTTP 2.0</h2><p>HTTP 1.1虽然可以重用TCP连接,但是还是需要串行发送请求以保证其顺序。但是大量的网页请求中都是些资源类的东西,如果能够并行这些请求,那就会增加更大的网络吞吐和性能。同时HTTP1.1 在传输数据时使用的是文本的方式,借助zip压缩方式以减少网络带宽,但是会消耗前端和后端的CPU资源。</p><p>与HTTP2.0的新特性包括</p><ol><li>HTTP/2是一个二进制协议,增加了数据传输的效率。</li><li>可以在一个TCP连接中并发请求多个HTTP请求。</li><li>使用HPACK算法压缩头部信息,如果多个请求的头是相识的,协议会消除重复的部分。</li><li>允许服务端PUSH。例如请求X,服务端知道X依赖于Y,会同时将Y一起返回客户端。</li></ol><h2 id="HTTP-3-0"><a href="#HTTP-3-0" class="headerlink" title="HTTP 3.0"></a>HTTP 3.0</h2><p>虽然HTTP2.0能够多个HTTP请求复用一个TCP连接,但是底层的TCP协议不知道上层有多少个HTTP请求,一旦出现丢包,会造成所有的HTTP请求都必需等待丢了的包被重传回来,即使这个包不是本次HTTP请求的。</p><p>为了解决这一问题,HTTP 3.0 将HTTP的底层由TCP协议改为了UDP协议,并且使用了QUIC。因为UDP不需要管理顺序,不管丢包。QUIC的任务就是要有一个像TCP一样的丢包重传机制。在基于UDP以后,QUIC直接将TCP和TLS需要做的事情合并为了三次握手。因此QUIC可以将其视为一个在UDP之上的伪TCP +TLS +HTTP/2的多路复用的协议。</p>]]></content>
<summary type="html"><p>HTTP(超文本传输协议)定义了浏览器如何向服务器请求万维网文档以及服务器如何将文档传送给浏览器。</p>
<h2 id="HTTP-0-9"><a href="#HTTP-0-9" class="headerlink" title="HTTP 0.9"></a>HTTP </summary>
<category term="网络" scheme="http://qian0817.top/categories/%E7%BD%91%E7%BB%9C/"/>
<category term="网络" scheme="http://qian0817.top/tags/%E7%BD%91%E7%BB%9C/"/>
<category term="HTTP" scheme="http://qian0817.top/tags/HTTP/"/>
</entry>
<entry>
<title>netty常用概念</title>
<link href="http://qian0817.top/2021/01/28/netty%E5%B8%B8%E7%94%A8%E6%A6%82%E5%BF%B5/"/>
<id>http://qian0817.top/2021/01/28/netty%E5%B8%B8%E7%94%A8%E6%A6%82%E5%BF%B5/</id>
<published>2021-01-28T06:32:27.000Z</published>
<updated>2021-01-28T06:32:27.487Z</updated>
<content type="html"><![CDATA[<h2 id="channel"><a href="#channel" class="headerlink" title="channel"></a>channel</h2><p>Channel接口是Netty对网络操作的抽象类,代表一个到实体(如硬件设备、网络套接字、文件等等)的开放连接,包含基本的读写操作。常用的Channel接口实现类包括NioServerSocketChannel和NioSocketChannel。这两个相当于BIO编程模型中的ServerSocket以及Socket两个概念。</p><h2 id="Future"><a href="#Future" class="headerlink" title="Future"></a>Future</h2><p>Netty是异步非阻塞的,所有的IO操作都是异步的,因此无法立即得到操作是否执行成功。通过Future提供了一种在操作完成时通知应用程序的方法,这个对象可以看作是一个异步操作的结果的占位符。</p><p>Netty提供了自己的实现ChannelFuture,其中addListener()方法可以注册一个ChannelFutureListener,当操作执行完成时,会自动触发返回结果。还可以通过sync()方法将异步的操作转换为同步的。</p><h2 id="ChannelHandler"><a href="#ChannelHandler" class="headerlink" title="ChannelHandler"></a>ChannelHandler</h2><p>Netty使用不同的事件来通知我们状态的改变或者是操作的状态。ChannelHandler 是消息的具体处理器。他负责处理读写操作、客户端连接等事情。</p><p>ChannelPipeline是ChannelHandler构成的链条,可以通过ChannelPipeline的add()方法添加ChannelHandler。</p><p>Netty也提供了预定义的ChannelHandler实现,包括实现HTTP,TLS的ChannelHandler。</p><h2 id="Eventloop"><a href="#Eventloop" class="headerlink" title="Eventloop"></a>Eventloop</h2><p>EventLoop 的主要作用实际就是负责监听网络事件并调用事件处理器进行相关 I/O 操作的处理。</p><p>EventLoopGroup 包含多个 EventLoop。</p><p>当客户端连接服务端时,bossGroup会处理客户端连接请求。当客户端处理完成后,会将这个连接提交workerGroup来处理,然后workerGroup负责处理其 IO 相关操作。</p><h2 id="Bootstrap"><a href="#Bootstrap" class="headerlink" title="Bootstrap"></a>Bootstrap</h2><p>Bootstrap 是客户端的启动引导类,通过connect()方法连接到远程的主机和端口,作为TCP连接的客户端,也可以通过bind()方法绑定一个本地端口,作为UDP协议通信中的一端。</p><p>ServerBootstrap是服务端的启动引导类,使用bind放大绑定到本地端口上,等待客户端的连接。 </p><h2 id="线程模型"><a href="#线程模型" class="headerlink" title="线程模型"></a>线程模型</h2><p>Netty线程模型基于Reactor模式设计开发。Reactor 模式基于事件驱动,采用多路复用将事件分发给相应的Handler处理。Netty主要靠NioEventLoopGroup线程池来实现具体的线程模型的。在实现服务端的时候,一般会初始化两个线程组,分别为bossGroup用于接受连接以及workerGroup负责具体的处理。其中bossGroup和workerGroup可以共用同一个EventLoopGroup。</p><h2 id="长连接与心跳机制"><a href="#长连接与心跳机制" class="headerlink" title="长连接与心跳机制"></a>长连接与心跳机制</h2><p>短连接指的是server与client端建立连接以后,读写完成之后就关闭掉连接,如果下次还需要继续发送消息,那么就要重新建立连接。</p><p>长连接说的就是 client 向 server 双方建立连接之后,它们之间的连接并不会主动关闭,后续的读写操作会继续使用这个连接。长连接的可以省去较多的 TCP 建立和关闭的操作,降低对网络资源的消耗。</p><p>在保存长连接的过程中,如果遇到断网等情况,需要使用到心跳机制判断对方是否已经掉线。如果client与server之间在一定时间内没有数据交互时,会发送一个特殊的数据包给对方,当接收方收到这个数据报文后, 也立即发送一个特殊的数据报文。当某一端收到心跳消息以后,就可以知道对方仍然在线,确保TCP连接的有效性。</p>]]></content>
<summary type="html">介绍Netty中的常用概念</summary>
<category term="Netty" scheme="http://qian0817.top/categories/Netty/"/>
<category term="网络" scheme="http://qian0817.top/tags/%E7%BD%91%E7%BB%9C/"/>
<category term="Netty" scheme="http://qian0817.top/tags/Netty/"/>
</entry>
<entry>
<title>Kafka基本概念</title>
<link href="http://qian0817.top/2021/01/26/kafka%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/"/>
<id>http://qian0817.top/2021/01/26/kafka%E5%9F%BA%E6%9C%AC%E6%A6%82%E5%BF%B5/</id>
<published>2021-01-26T05:45:35.000Z</published>
<updated>2021-01-26T05:45:35.684Z</updated>
<content type="html"><![CDATA[<p>一个经典的Kafka架构包括若干个Producer、Broker、Consumer和ZooKeeper集群。</p><p>ZooKeeper是Kafka用来负责集群元数据的管理以及控制器的选举。</p><p>Producer将消息发送到Broker中去,Broker负责将收到的消息存储到磁盘当中,然后Consumer负责从Broker订阅并且消费消息。</p><p>Kafka还有两个重要的概念就是主题Topic以及分区Partition。Kafka中的消息以主题为单位进行归类,生产者负责将消息发送到特定的主题,消费者负责订阅主题并进行消费。</p><p>主题是一个逻辑上的概念,还可以细分为多个分区,一个分区只属于单个主题。同一主题下不同分区包含的消息是不同的。分区在存储层面可以看作一个可以追加的日志,消息在追加到分区日志文件的时候会分配一个特定的偏移量offset。offset是消息在分区中的唯一标识,Kafka通过offset来保证消息在分区中的顺序性。但是offset不跨越分区,因此Kafka只保证分区有序而不保证主题有序。</p><p>Kafka的分区可以分布在不同的服务器上,横跨多个Broker,以此来提供比单个Broker更强大的性能。当消息发送到Broker之前,会根据分区规则选择存储到哪个分区。</p><p>Kafka为分区引入了多副本(Replica)机制,通过增加副本数量可以提升容灾能力。同一分区的不同副本中保存的是相同的消息,但在同一时刻副本之间的消息可能并非完全相同,副本之间是一主多从的关系,其中leader副本负责处理读写请求,follower副本只负责与leader副本消息同步。副本处于不同的broker中,当leader副本出现故障时,从follower副本中重新选举新的leader副本继续对外提供服务。通过该机制使得Kafka集群中某个Broker失效时仍然能够对外提供服务。</p><p>分区中所有的副本称为AR(Assigned Replicas),所有与leader副本保持一定程度同步的副本组成ISR(In-Sync-Replicas)。当消费者发送消息时,会先发送到leader节点,然后follower节点才能从leader副本中拉取消息进行同步,同步期间follower副本相对于leader副本会有一定程度上的落后。与leader副本同步滞后过多的副本称为OSR(Out-of-Sync Replicas)。当leader副本发生故障时,只有ISR集合中的副本才有资格选举为新的leader。</p><p>Kafka的消费端也有容灾能力,Consumer使用拉(Pull)模式从服务端拉取消息,并且保存消费的具体位置。当消费者宕机重启时可以根据之前保存的消费位置重新拉取需要的消息进行消费。</p>]]></content>
<summary type="html"><p>一个经典的Kafka架构包括若干个Producer、Broker、Consumer和ZooKeeper集群。</p>
<p>ZooKeeper是Kafka用来负责集群元数据的管理以及控制器的选举。</p>
<p>Producer将消息发送到Broker中去,Broker负责</summary>
<category term="Kafka" scheme="http://qian0817.top/categories/Kafka/"/>
<category term="Kafka" scheme="http://qian0817.top/tags/Kafka/"/>
</entry>
<entry>
<title>操作系统文件管理</title>
<link href="http://qian0817.top/2020/12/10/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86/"/>
<id>http://qian0817.top/2020/12/10/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86/</id>
<published>2020-12-10T06:16:05.000Z</published>
<updated>2020-12-30T04:35:50.221Z</updated>
<content type="html"><![CDATA[<p>文件系统是操作系统中文件的管理者。文件系统为上层用户提供文件抽象并实现文件访问所需要的接口,同时以特定的存储格式在下层存储设备中维护每个文件的数据以及元数据。操作系统将存储设备抽象为块设备,块设备上的的存储空间被划分为固定大小的存储块,每个存储块有一个地址被称为块号。</p><h2 id="基于inode的文件系统"><a href="#基于inode的文件系统" class="headerlink" title="基于inode的文件系统"></a>基于inode的文件系统</h2><h3 id="inode"><a href="#inode" class="headerlink" title="inode"></a>inode</h3><p>inode是“index node”的简写,即索引节点,记录了一个文件所对应的所有存储块号。inode中保存了三种存储指针,第一种指针为直接指针,其直接指向数据块,数据块中保存了文件数据;第二种指针为间接指针,指向了一个一级索引块,一级索引块中存放者指向数据块的指针。第三种指针为二级间接指针,指向一个二级索引块,二级索引块的每个指针都指向一个一级索引块,进而指向多个数据块。</p><p><img data-src="%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E6%96%87%E4%BB%B6%E7%AE%A1%E7%90%86%5Cinode%E6%95%B0%E6%8D%AE%E7%B4%A2%E5%BC%95.png" alt="inode数据索引"></p><p>通过上述的组织方式可以看出,一个文件系统所支持的最大文件大小收到文件数据组织方式的限制,通过调整inode的设计可以改变其能够管理的最大文件大小。为了支持更大的文件,inode还可以启用三级或者四级间接指针。</p><p>inode还会记录该文件的其他元数据,例如文件模式、链接数、文件大小、文件访问时间等。</p><h3 id="存储结构"><a href="#存储结构" class="headerlink" title="存储结构"></a>存储结构</h3><p>文件系统以特定的存储布局将文件数据以及元数据保存在存储设备之中。为了高效的管理这些文件数据以及元数据,文件系统通常将存储空间划分为不同区域,分别用于不同功能。通常一个存储设备会被划分为超级块、块分配信息、inode分配信息、inode表以及文件数据块等区域。</p><p>超级块记录了整个文件系统的全局元数据,例如文件系统的版本、所管理空间大小、最后一次的挂载时间以及一些统计信息(例如最大inode数、空闲inode数、最大块数量、空闲块数量)。</p><p>块分配信息使用位图(bitmap)的格式来标记文件数据块区域中各个块的使用情况,如果为比特位为1则表示对应的数据块已经被分配和使用,若为0则代表对应的数据块空闲。inode分配信息与块分配信息类似,其对应的是每个inode的使用情况。</p><p>inode表以数组的形式存放整个文件系统的所有inode结构,文件系统使用inode在此表中的索引对不同inode进行区分。</p><p>剩余的存储空间为文件数据块区域,用于保存文件的数据。</p><h2 id="虚拟文件系统VFS"><a href="#虚拟文件系统VFS" class="headerlink" title="虚拟文件系统VFS"></a>虚拟文件系统VFS</h2><p>在一个计算机之中可能存在多个文件系统,需要使用到虚拟文件系统(VFS)对多种文件系统进行管理和协调,允许它们在同一个操作系统上共同工作。VFS定义了一系列内存数据结构,并要求底层的不同文件系统提供指定的方法,利用这些方法将不同文件系统的元数据统一转化为VFS的内存数据结构。</p>]]></content>
<summary type="html">介绍操作系统文件管理</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title>操作系统同步原语</title>
<link href="http://qian0817.top/2020/12/05/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%90%8C%E6%AD%A5%E5%8E%9F%E8%AF%AD/"/>
<id>http://qian0817.top/2020/12/05/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%90%8C%E6%AD%A5%E5%8E%9F%E8%AF%AD/</id>
<published>2020-12-05T10:15:50.000Z</published>
<updated>2020-12-30T04:35:50.099Z</updated>
<content type="html"><![CDATA[<p>并行处理同一任务意味着对于共享资源的并发访问,为了保证共享资源状态的正确性,需要正确的在这些子任务中进行同步。为了正确的高效的解决这些同步问题,抽象出了同步原语这一概念。</p><h2 id="互斥锁"><a href="#互斥锁" class="headerlink" title="互斥锁"></a>互斥锁</h2><p>在任意时刻只允许最多一个线程访问的方式被称为互斥访问,保证互斥访问共享资源的代码区域被称为临界区。在同一时刻只能有一个线程可以执行临界区中的代码。因此在临界区中,一个线程可以安全的对共享资源进行操作。为了达到这样的目的,需要设计一个协议来保证临界区的互斥性,保证互斥访问、优先等待以及空闲让出。</p><h3 id="硬件实现:关闭中断"><a href="#硬件实现:关闭中断" class="headerlink" title="硬件实现:关闭中断"></a>硬件实现:关闭中断</h3><p>在单核条件中,关闭中断意味着当前执行的线程不能被其他线程抢占。因此如果在进入临界区之前关闭中断并在离开临界区之后在开启中断,那么就能避免当前执行临界区代码的线程被打断,保证了只有一个线程执行临界区,也就保证了互斥访问。</p><p>但是在多线程环境中,关闭中断的方式不再适用。即使关闭了所有核心的中断,也不能阻塞其他核心上正在运行的线程执行。</p><h3 id="软件实现:皮特森算法"><a href="#软件实现:皮特森算法" class="headerlink" title="软件实现:皮特森算法"></a>软件实现:皮特森算法</h3><p>皮特森算法是一种纯软件方法,可以解决两个线程的临界区问题。其代码如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">bool</span> flag[<span class="number">2</span>] = {<span class="literal">false</span>,<span class="literal">false</span>};</span><br><span class="line"><span class="keyword">int</span> trun = <span class="number">0</span>;</span><br><span class="line"><span class="comment">// 线程1</span></span><br><span class="line"><span class="keyword">while</span>(<span class="literal">true</span>) {</span><br><span class="line"> flag[<span class="number">0</span>] = <span class="literal">true</span>;</span><br><span class="line"> turn = <span class="number">1</span>;</span><br><span class="line"> <span class="keyword">while</span>(flag[<span class="number">1</span>] == <span class="literal">true</span> && turn == <span class="number">1</span>);</span><br><span class="line"> <span class="comment">// 临界区部分</span></span><br><span class="line"> flag[<span class="number">0</span>] = <span class="literal">false</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 class="comment">// 线程2</span></span><br><span class="line"><span class="keyword">while</span>(<span class="literal">true</span>) {</span><br><span class="line"> flag[<span class="number">1</span>] = <span class="literal">true</span>;</span><br><span class="line"> turn = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">while</span>(flag[<span class="number">0</span>] == <span class="literal">true</span> && turn == <span class="number">0</span>);</span><br><span class="line"> <span class="comment">// 临界区部分</span></span><br><span class="line"> flag[<span class="number">1</span>] = <span class="literal">false</span>;</span><br><span class="line"> <span class="comment">// 其他部分</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>其中有两个重要的变量,一个是全局数组flag,其有两个布尔成员(flag[0]和flag[1]),分别代表线程1和线程2是否尝试进入临界区。第二个是全局变量turn。如果两个线程都申请进入临界区,那么turn将决定最终进入临界区的线程编号。线程在进入临界区之前,以线程1为例,必须满足flag[1]==false(表示线程1没有申请进入临界区)或者trun==0(表示turn决定线程1可以进入临界区),方能进入临界区。</p><p>皮特森算法要求访存严格按照程序顺序执行,然而现代CPU为了达到更好的性能允许访存操作乱序执行,因此皮森特算法在这些CPU上无法正常的进行工作。</p><h3 id="软硬件协同:原子操作"><a href="#软硬件协同:原子操作" class="headerlink" title="软硬件协同:原子操作"></a>软硬件协同:原子操作</h3><p>硬件提供的原子操作也可以解决临界区问题。原子操作指的是不可被打断的一个或者一系列的操作,最常见的是比较与置换(CAS),利用该特性可以实现互斥锁。</p><h4 id="自旋锁"><a href="#自旋锁" class="headerlink" title="自旋锁"></a>自旋锁</h4><p>自旋锁利用一个变量lock来表示锁的状态,在加锁时,线程会通过CAS判断lock是否空闲,如果空闲则上锁,否则一遍遍的进行重试。代码如下所示:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_init</span><span class="params">(<span class="keyword">int</span>* lock)</span></span>{</span><br><span class="line"> *lock = <span class="number">0</span>;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock</span><span class="params">(<span class="keyword">int</span>* lock)</span></span>{</span><br><span class="line"> <span class="keyword">while</span> (atomic_CAS(lock,<span class="number">0</span>,<span class="number">1</span>) != <span class="number">0</span>);</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock</span><span class="params">(<span class="keyword">int</span>* lock)</span></span>{</span><br><span class="line"> *lock = <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>自旋锁的实现简单,在竞争程度低的时候非常高效,依然广泛的运用在各种软件之中。</p><h2 id="条件变量"><a href="#条件变量" class="headerlink" title="条件变量"></a>条件变量</h2><p>条件变量可以让一个线程停止使用CPU并将自己挂起。当等待的条件满足时,其他线程会唤醒该挂起的线程让其继续执行。使用条件变量能够避免无谓的循环等待。</p><p>条件变量的实现如下,每个条件变量的结构体中都包含了一个wait_list,用于记录等待在该条件变量上的线程。线程在调用cond_wait挂起自己时,需要先将当前线程加入到等待队列之中,然后原子的挂起当前线程并且释放锁。这个操作需要利用到操作系统辅助完成,例如使用Linux系统下的futex机制。当该线程被其他线程唤醒以后,其应当再次获取锁并且进入临界区然后返回。cond_signal函数会利用操作系统提供的唤醒服务将线程唤醒。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">cond</span> {</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">thread</span> *<span class="title">wait_list</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">cond_wait</span><span class="params">(struct cond *cond, struct lock *mutex)</span> </span>{</span><br><span class="line"> list_append(cond->wait_list,current_thread());</span><br><span class="line"> atomic_block_unlock(mutex); <span class="comment">// yuan'zi挂起</span></span><br><span class="line"> lock(mutex);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">cond_signal</span><span class="params">(struct cond *cond, struct lock *mutex)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (!list_empty(cond->wait_list)){</span><br><span class="line"> wakeup(list_remove(cond->wait_list));</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">cond_broadcast</span><span class="params">(struct cond *cond)</span></span>{</span><br><span class="line"> <span class="keyword">while</span> (!list_empty(cond->wait_list)){</span><br><span class="line"> wakeup(list_remove(cond->wait_list));</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="信号量"><a href="#信号量" class="headerlink" title="信号量"></a>信号量</h2><p>信号量在不同的线程之间充当信号灯,其根据剩余资源数量控制不同线程的执行或者等待。信号量定义了两个操作分别为P和V,因此信号量又被称为PV原语。一般使用wait和signal来表示信号量的P和V操作。信号量的语义如下:</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">wait</span><span class="params">(<span class="keyword">int</span>* S)</span></span>{</span><br><span class="line"><span class="keyword">while</span>(*S <= <span class="number">0</span>); <span class="comment">// 循环忙等</span></span><br><span class="line"> *S = *S<span class="number">-1</span>;</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">signal</span><span class="params">(<span class="keyword">int</span>* S)</span></span>{</span><br><span class="line"> *S = *S + <span class="number">1</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>上面的代码仅仅是信号量的语义,并非信号量的真正实现。该代码存在两个问题。首先,可能存在多个线程同时更新共享信号量的情况导致更新丢失的问题。同时当一次资源被释放时,同时通知多个线程的效率比较低,让多余的线程挂起并且放弃CPU才是正确的选择。</p><p>实现一个正确的信号量需要组合使用互斥锁和条件变量。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><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="class"><span class="keyword">struct</span> <span class="title">sem</span> {</span></span><br><span class="line"> <span class="comment">/* </span></span><br><span class="line"><span class="comment"> 没有线程等待时,value 为整数或零,表示剩余的资源数量</span></span><br><span class="line"><span class="comment"> 有线程等待时,value 为负数,代表正在等待资源的线程数量</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"> <span class="keyword">int</span> value;</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">int</span> wakeup;</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">sem_lock</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">cond</span> <span class="title">sem_cond</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">wait</span><span class="params">(struct sem *S)</span> </span>{</span><br><span class="line"> lock(&S->sem_lock);</span><br><span class="line"> S->value--;</span><br><span class="line"> <span class="keyword">if</span> (S->value < <span class="number">0</span>) { <span class="comment">// 代表没有空闲的资源</span></span><br><span class="line"> <span class="keyword">do</span> {</span><br><span class="line"> cond_wait(&S->sem_cond, &S->sem_lock);</span><br><span class="line"> } <span class="keyword">while</span> (S->wakeup == <span class="number">0</span>);</span><br><span class="line"> S->wakeup--;</span><br><span class="line"> }</span><br><span class="line"> unlock(&S->sem_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">signal</span><span class="params">(struct sem *S)</span> </span>{</span><br><span class="line"> lock(&S->sem_lock);</span><br><span class="line"> S->value++;</span><br><span class="line"> <span class="keyword">if</span> (S->value <= <span class="number">0</span>) {</span><br><span class="line"> S->wakeup++;</span><br><span class="line"> cond_signal(&S->sem_cond);</span><br><span class="line"> }</span><br><span class="line"> unlock(&S->sem_lock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在调用wait操作时,该线程会在获取该信号量的互斥锁后,先将value数值减1。若其值大于等于0,说明有空闲的资源,wait操作成功。如果value小于0,那么需要使用到wakeup来判断是否与空闲的资源来消耗。如果wakeup也为0,需要将当前线程挂起等待。</p><p>信号量与互斥锁有着相似之处。如果信号量只允许其值在0和1之间变化时,那么可以把信号量当作互斥锁来使用。</p><h2 id="读写锁"><a href="#读写锁" class="headerlink" title="读写锁"></a>读写锁</h2><p>当一些线程仅仅需要读取而非修改共享资源时,就没有必要保证它们之间的互斥性。在这种情况下直接使用互斥锁不会带来任何正确性的问题,但是会削减读者之间的并行度,造成性能损失。读写锁就是为了解决这一问题而提出的同步原语。</p><p>读写锁允许多个读者同时进入读临界区,因此会带来倾向性的问题。假设在某一时刻,已经有一些读者在写临界区,那么此时有一个读者和一个写者同时申请进入临界区,那么是否允许该读者进入写临界区。如果允许读者进入写临界区,称为偏向读者的读写锁,否则称为偏向写者的读写锁。</p><p>下面的代码实现了偏向读写的读写锁。读者在上锁的时候需要使用到reader_lock来保证reader_cnt更新的原子性。如果是第一个读者还需要获取writer_lock保证读者和写者的互斥。同样如果读者在释放锁时发现自己是最后一个读者还需要释放write_lock取消对于写者的阻塞。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">rwlock</span> {</span></span><br><span class="line"> <span class="keyword">int</span> reader_cnt;</span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">reader_lock</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">writer_lock</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->reader_lock);</span><br><span class="line"> lock->reader_cnt++;</span><br><span class="line"> <span class="keyword">if</span> (lock->reader_cnt == <span class="number">1</span>) { <span class="comment">// 第一个读者</span></span><br><span class="line"> lock(&lock->writer_lock);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->reader_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->reader_lock);</span><br><span class="line"> lock->reader_cnt--;</span><br><span class="line"> <span class="keyword">if</span> (lock->reader_cnt == <span class="number">0</span>) { <span class="comment">// 最后一个读者</span></span><br><span class="line"> unlock(&lock->writer_lock);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->reader_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->writer_lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> unlock(&lock->writer_lock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>偏向写者的读写锁实现更加复杂。在rwlock结构中需要添加一个布尔变量has_writer来表示当前是否有写者到达。当有写者获得锁时,会将has_writer设置为true。读者通过判断has_writer的值来决定是否需要等待前序的写者。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">rwlock</span> {</span></span><br><span class="line"> <span class="keyword">volatile</span> <span class="keyword">int</span> reader_cnt;</span><br><span class="line"> <span class="keyword">volatile</span> <span class="keyword">bool</span> has_writer;</span><br><span class="line"> <span class="comment">/* 读者写者需要共享锁 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">lock</span> <span class="title">lock</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">cond</span> <span class="title">reader_cond</span>;</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">cond</span> <span class="title">writer_cond</span>;</span></span><br><span class="line">};</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> <span class="keyword">while</span> (lock->has_writer){</span><br><span class="line"> cond_wait(&lock->writer_cond,&lock->lock);</span><br><span class="line"> }</span><br><span class="line"> lock->reader_cnt++;</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_reader</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> lock->reader_cnt--;</span><br><span class="line"> <span class="keyword">if</span> (lock->reader_cnt == <span class="number">0</span>) { <span class="comment">// 最后一个读者</span></span><br><span class="line"> cond_signal(&lock->reader_cond);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">lock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> <span class="keyword">while</span> (lock->has_writer){</span><br><span class="line"> cond_wait(&lock->writer_cond,&lock->lock);</span><br><span class="line"> }</span><br><span class="line"> lock->has_writer= <span class="literal">true</span>;</span><br><span class="line"> <span class="keyword">while</span> (lock->reader_cnt><span class="number">0</span>){</span><br><span class="line"> cond_wait(&lock->reader_cond,&lock->lock);</span><br><span class="line"> }</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">unlock_writer</span><span class="params">(struct rwlock *lock)</span> </span>{</span><br><span class="line"> lock(&lock->lock);</span><br><span class="line"> lock->has_writer= <span class="literal">false</span>;</span><br><span class="line"> cond_broadcast(lock->writer_cond);</span><br><span class="line"> unlock(&lock->lock);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>偏向读者的锁能够大幅的提升读者之间的并行度,但是会面临很高的写延迟,偏向写者的锁正好相反。因此在实际使用场景中需要根据具体需求选择合适的读写锁。</p><h2 id="死锁"><a href="#死锁" class="headerlink" title="死锁"></a>死锁</h2><p>死锁指的是一组线程中每一个线程都在等待其他线程释放资源而导致的无限等待。下列的代码就演示了死锁出现的场景。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">thread_a</span><span class="params">()</span></span>{</span><br><span class="line">lock(lock_a);</span><br><span class="line"> lock(lock_b);</span><br><span class="line"> <span class="comment">//do something</span></span><br><span class="line">unlock(lock_b);</span><br><span class="line"> unlock(lock_a);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">thread_b</span><span class="params">()</span></span>{</span><br><span class="line">lock(lock_b);</span><br><span class="line"> lock(lock_a);</span><br><span class="line"> <span class="comment">//do something</span></span><br><span class="line">unlock(lock_a);</span><br><span class="line"> unlock(lock_b);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="产生原因"><a href="#产生原因" class="headerlink" title="产生原因"></a>产生原因</h3><p>导致死锁出现一共有四个必要条件</p><ol><li>互斥访问:一个共享资源在同一时刻只能被至多一个线程访问。</li><li>持有并等待:线程持有一些资源的同时在等待一些资源。</li><li>资源非抢占:一旦一个资源被持有,除非持有者主动放弃,否则其他竞争者得不到这个资源。</li><li>循环等待:在一系列线程中,例如T<sub>0</sub>,T<sub>1</sub>,T<sub>2</sub>。其中T<sub>0</sub>等待T<sub>1</sub>,T<sub>1</sub>等待T<sub>2</sub>,T<sub>2</sub>等待T<sub>0</sub>,形成了一个循环。</li></ol><h3 id="死锁检测与恢复"><a href="#死锁检测与恢复" class="headerlink" title="死锁检测与恢复"></a>死锁检测与恢复</h3><p>当死锁出现时,需要一个第三方来打破僵局,帮助进行死锁恢复。操作系统往往会扮演这个第三方的角色。</p><p>前面死锁的四个条件之中只有循环等待这一条件与实际运行状态相关,因此是检测死锁的关键。为了确认系统中是否存在循环等待,需要获取到系统中资源分配以及线程等待的相关信息,通过这两张表建立图,判断图中是否存在环,就可以进行死锁的检测。</p><p>再找到的环中的任意线程,终止该线程并释放其占有的所有资源就可以进行死锁的恢复。</p><h3 id="死锁预防"><a href="#死锁预防" class="headerlink" title="死锁预防"></a>死锁预防</h3><p>死锁预防指的是通过合理的资源分配算法,从源头上预防死锁。为了实现这个目标,需要保证死锁的四个条件不能被同时满足。</p><ol><li>避免互斥访问:</li><li>不允许持有并等待:可以让线程在真正开始操作前一次性申请所有的资源,一旦任意资源不能获取则释放掉所有资源并进行重试。该方法会导致资源利用率低,出现饥饿的情况。</li><li>允许资源被抢占:允许一个资源抢占其他线程已经占有的资源,难点在于如何保证被抢占的线程正确的进行恢复。</li><li>避免循环等待:可以要求线程按照一定的顺序进行资源获取,例如对锁进行编号,要求所有线程按照顺序获取锁,可以避免发生死锁。</li></ol><h3 id="死锁避免"><a href="#死锁避免" class="headerlink" title="死锁避免"></a>死锁避免</h3><p>死锁避免是通过系统运行时跟踪资源分配过程来避免出现死锁。银行家算法是一种具体的死锁避免算法。银行家算法通过模拟分配资源后的状态来判断系统是否处于安全状态。例如假设系统中存在M个资源,线程有N个,那么会有以下四个数据结构。</p><ol><li>全局可利用资源:Available[M]</li><li>每个线程的最大需求量:Max[N][M]</li><li>已分配资源数量:Allocation[N][M]</li><li>还需要资源数量:Need[N][M]</li></ol><p>银行家算法执行检查的流程如下:</p><ol><li>建立一个临时数组Available_temp,其初始值与数组Available数组一致,用于模拟系统可用资源数量。</li><li>找到系统当前剩余资源能够满足的线程,称为线程X,如果找不到对应的线程,说明系统处于非安全状态。</li><li>对于Available_temp中的所有成员m,都执行Available_temp[m]+=Allocation[x][m]。</li><li>遍历所有线程,查看是否还有未被标记执行结束的线程。如果有则回到第二步,否则代表系统处于安全状态。</li></ol>]]></content>
<summary type="html"><p>并行处理同一任务意味着对于共享资源的并发访问,为了保证共享资源状态的正确性,需要正确的在这些子任务中进行同步。为了正确的高效的解决这些同步问题,抽象出了同步原语这一概念。</p>
<h2 id="互斥锁"><a href="#互斥锁" class="headerlink" </summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title>进程间通信方式</title>
<link href="http://qian0817.top/2020/12/03/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F/"/>
<id>http://qian0817.top/2020/12/03/%E8%BF%9B%E7%A8%8B%E9%97%B4%E9%80%9A%E4%BF%A1%E6%96%B9%E5%BC%8F/</id>
<published>2020-12-03T06:52:57.000Z</published>
<updated>2020-12-30T04:35:50.183Z</updated>
<content type="html"><![CDATA[<p>进程间通信是多进程协作的基础。在该过程中至少需要两个进程参与,这两方分别被称为发送者和接收者,也可以被称为调用者与被调用者、客户端和服务端。常见的进程间通信方式主要有七种。</p><h2 id="管道"><a href="#管道" class="headerlink" title="管道"></a>管道</h2><p>管道是两个进程间的一条通道,一个负责投递,一个负责接收。管道是单向的通信方式,在操作系统内核之中有缓冲区来缓冲消息,通信的数据是字节流,需要应用自己去解码。一个管道有且只有两端,一个负责输入,一个负责输出。</p><p>在UNIX中管道分为两类:命名管道和匿名管道。</p><p>匿名管道通过pipe的系统调用来创建,在创建的同时拿到两个读写的端口(文件描述符),然后只能使用这两个文件描述符来使用它。因此在这种情况下通常需要结合fork创建子进程的方式来建立父子进程间的连接。该方式适合父子进程这样的场景,不适合两个关系较远的进程。</p><p>命名管道由命令mkfifo进行创建,在创建过程中会指定一个全局的文件名来代指一个具体的管道。通过该方式任意两个进程通过一个相同的管道名就可以建立管道的通信连接。</p><h2 id="消息队列"><a href="#消息队列" class="headerlink" title="消息队列"></a>消息队列</h2><p>消息队列是以消息为数据抽象的通信方式。消息队列比较灵活,可以同时支持同时存在多个发送者和接收者。</p><p>当创建一个新的消息队列时,内核将从系统中分配一个队列数据结构,作为消息队列的内核对象。一旦一个队列被创建,那么除非内核重启或者该队列被主动删除,否则其数据会被一直保留。</p><h2 id="信号量"><a href="#信号量" class="headerlink" title="信号量"></a>信号量</h2><p>信号量一般来说仅有一个共享的整形计数器,该计数器由内核维护,对信号量的操作需要经过内核系统调用。</p><p>信号量的两个主要操作为P和V。P表示尝试一个操作,该操作的失败会将当前线程进入阻塞状态,直到其他进程执行了V操作。V操作表示增加,将该计数器加1,同时唤醒因P操作而陷入阻塞的进程。</p><p>信号量可以见到的满足进程间同步的要求,如果希望两个进程A和B,A执行完相关代码以后B再执行,那么可以使用信号量机制。</p><h2 id="共享内存"><a href="#共享内存" class="headerlink" title="共享内存"></a>共享内存</h2><p>共享内存为需要通信的进程建立共享区域,一旦共享区域完成建立,内核就不再需要参与进程间通信,通信的多方可以直接使用共享区域上的数据。</p><p>共享内存的机制就是允许多个进程在其所在的虚拟空间映射相同的物理内存页,从而进行通信。当进程不在希望共享内存时,也可以取消共享内核和虚拟内存之间的映射(之影响当前进程的映射,不影响其他仍在使用的共享内存的进程)。</p><h2 id="信号"><a href="#信号" class="headerlink" title="信号"></a>信号</h2><p>使用信号,一个进程可以随时的发送一个事件到特定的进程、线程或者进程组中。同时接受该事件的进程不需要阻塞等待该消息,内核会帮助其切换到对应的处理函数中响应信号事件,并在完成后恢复之前的上下文。</p><p>信号在Linux中应用十分广泛,例如在控制台中使用Ctrl+C终止一个程序时,其背后的逻辑就是发出一个SIGINT信号,导致默认信号处理函数结束了对应的进程。</p><h2 id="套接字"><a href="#套接字" class="headerlink" title="套接字"></a>套接字</h2><p>套接字是一种既可以用于本地也可以跨网络使用的通信机制,应用程序可以用相同的套接字接口来实现本地进程通信和跨机器的网络通信。</p><p>在套接字进程间通信的模式下,客户端进程通过一个特定的网络地址来找到想要调用的服务端进程。套接字可以使用不同的协议例如TCP和UDP来对通信进行控制。</p><p>进程间通信的对比如下</p><table><thead><tr><th>通信机制</th><th>数据抽象</th><th>参与者</th><th>方向</th><th>内核实现</th></tr></thead><tbody><tr><td>管道</td><td>字节流</td><td>两个进程</td><td>单向</td><td>FIFO的缓冲区</td></tr><tr><td>消息队列</td><td>消息</td><td>多进程</td><td>双向</td><td>队列的组织方式</td></tr><tr><td>信号量</td><td>计数器</td><td>多进程</td><td>双向</td><td>共享计数器</td></tr><tr><td>共享内存</td><td>内存区间</td><td>多进程</td><td>双向</td><td>共享的内存空间</td></tr><tr><td>信号</td><td>时间编号</td><td>多进程</td><td>单向</td><td>信号等待队列</td></tr><tr><td>套接字</td><td>数据报文</td><td>两个进程</td><td>双向</td><td>网络栈</td></tr></tbody></table>]]></content>
<summary type="html">介绍操作系统的进程间通信方式</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="进程" scheme="http://qian0817.top/tags/%E8%BF%9B%E7%A8%8B/"/>
</entry>
<entry>
<title>操作系统调度</title>
<link href="http://qian0817.top/2020/11/25/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E8%B0%83%E5%BA%A6/"/>
<id>http://qian0817.top/2020/11/25/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E8%B0%83%E5%BA%A6/</id>
<published>2020-11-25T09:34:46.000Z</published>
<updated>2020-12-30T04:35:50.173Z</updated>
<content type="html"><![CDATA[<p>操作系统会同时处理多个请求,但是硬件的运行资源是有限的。调度就是用来协调每个请求对于资源的使用方法。</p><p>一般调度器会通过维护运行队列来管理任务。运行队列不一定需要使用先进先出的队列,例如在Linux中使用的调度器就会使用红黑树来运行队列。当任务在执行时出发一定的条件(例如运行了指定的时间片以后或者发起了IO请求),那么会被加入到运行队列之中,等待再次被调度执行。</p><h2 id="调度指标"><a href="#调度指标" class="headerlink" title="调度指标"></a>调度指标</h2><p>在计算机处理的任务之中,有一类任务被称为批处理任务(比如复杂的科学计算等等),这些任务在执行时无需与用户进行交互,其主要的调度指标为任务处理的吞吐量,需要让吞吐量尽可能高,周转周期(即花费的时间)尽可能短。</p><p>还有一类任务为交互式任务,,需要在执行过程中对用户的操作做出响应。这类任务需要保证响应时间足够短使用户获得良好体验。</p><p>同时还有一类有截止时间要求的实时任务(例如车载操作系统需要检测汽车与外部物体的距离)。调度器必须让逝世人物在截止时间前完成,满足实时性的要求。</p><p>同时在移动设备中,调度器还需要尽可能地降低能耗。在通常情况下,调度器需要保证每个任务都有执行的机会,即满足公平性。调度器做出决策的时间也必须短,降低调度开销。</p><p>因此调度器在不同场景下有着不同的调度指标,需要在诸多方面做出权衡。</p><h2 id="调度机制"><a href="#调度机制" class="headerlink" title="调度机制"></a>调度机制</h2><p>进程可能处于不同的状态,这些状态包括新生、预备、运行、阻塞、终止。进程调度机制就负责在这些状态间进行转换。进程调度根据职责的不同,分为长期、中期、短期调度。</p><p>当用户像操作系统提交了执行某个程序的请求以后,系统可能不会立即处理该请求,这一决策由长期调度负责。长期调度用于限制系统中真正被短期调度管理的进程数量,避免短期调度的开销过大。只有当长期调度为某个程序创建了进程并且将状态设置为预备状态以后,才会由短期调度进一步管理该线程。</p><p>短期调度负责进程在预备状态、运行状态以及阻塞状态之间的转换。在短期调度决定执行某个进程以后,会将该进程从预备状态设置为运行状态。短期调度会使用适当的调度策略,尽可能地满足系统的调度指标。</p><p>中期调度实际上是换页机制的一部分。当系统中的进程已经占用了大量的内存资源以后,中期调度会挂起系统中被短期调度管理的进程,从而降低进程占用的内存总量。中期调度策略会根据策略来选择将要被挂起的进程,设置为对应的挂起状态,使其不再被调度执行。同时中期调度也会监控当前的内存使用情况,在适当的时机激活此前挂起的进程,使其可以重新被调度。</p><p>在引入了调度机制以后,进程转换的示意图变为如下所示。与之前的示意图相比,其区别在与引入了两个额外的进程状态:预备挂起状态以及挂起阻塞状态。</p><p><img data-src="%E5%9F%BA%E4%BA%8E%E8%BF%9B%E7%A8%8B%E8%B0%83%E5%BA%A6%E7%9A%84%E8%BF%9B%E7%A8%8B%E8%BD%AC%E6%8D%A2.png" alt="基于进程调度的进程转换"></p><p>在进程调度中,长期中期短期相互协作,分别以不同的目标对进程进行调度。长期调度负责决定是否将一个新的进程纳入调度管理。中期调度负责限制系统中进程的内存占用。短期调度细粒度的调度进程的执行,做出对应的调度策略。</p><h2 id="单核调度策略"><a href="#单核调度策略" class="headerlink" title="单核调度策略"></a>单核调度策略</h2><h3 id="经典调度"><a href="#经典调度" class="headerlink" title="经典调度"></a>经典调度</h3><h4 id="先到先得"><a href="#先到先得" class="headerlink" title="先到先得"></a>先到先得</h4><p>先到先得策略也叫先进先出策略。该策略在系统中维护一个运行队列,在执行任务的时候,选取队列中的第一个任务,将其移除队列并且执行,当任务阻塞时,将其放入运行队列队尾,当一个任务执行完后,再执行下一个任务。</p><p>该策略的弊端在于在长短任务的场景下对于短任务不友好,会导致短任务的周转时间与运行时间之比过大。同时其对IO密集型任务也不够友好,可能会导致I/O密集型任务长时间内无法执行。</p><h4 id="最短时间优先-最短完成时间任务优先"><a href="#最短时间优先-最短完成时间任务优先" class="headerlink" title="最短时间优先/最短完成时间任务优先"></a>最短时间优先/最短完成时间任务优先</h4><p>先到先得策略出现的问题提示我们需要让短任务立即进行执行。根据这一思想,得出的策略就是最短任务优先和最短完成时间任务优先。这两种策略的区别在于是否是抢占式的。前者必须等一个任务执行完成才能开始下一调度,后者会中断正在执行的进程转而执行其他任务。</p><p>这两种调度的弊端在于必须先预知任务的运行时间。同时前者调度策略其表现与任务到达时间点有着严重的依赖,后者调度策略会导致长任务饥饿。</p><h4 id="时间片轮转"><a href="#时间片轮转" class="headerlink" title="时间片轮转"></a>时间片轮转</h4><p>在时间片流转策略中需要为任务设置时间片,限定任务每次执行的时间。当任务执行完时间片以后,就切换到运行队列的下一个任务,这就是时间片轮转策略。由于时间片一般会设的足够小,所有任务都可以在一定时间内执行并且响应用户,从而将响应时间在可接受范围内。</p><p>该策略的弊端在于任务运行时间形似的场景下平均周转周期高,但是如果仅考虑响应时间,那么该策略会有很好的效果。</p><h3 id="优先级调度"><a href="#优先级调度" class="headerlink" title="优先级调度"></a>优先级调度</h3><p>如果操作系统可以分别交互式任务以及批处理任务,那么可以设置让交互式任务优先执行的调度策略,为此调度器引入了优先级的概念。通过优先级概念,调度器可以确定哪些任务应该优先执行。</p><h4 id="多级队列"><a href="#多级队列" class="headerlink" title="多级队列"></a>多级队列</h4><p>每个任务都会被分配预先设置好的优先级,每个优先级一个队列,任务会被存储在对应的优先级队列之中。如果优先级不同的任务同时处于预备状态,那么调度器会选择优先级较高的任务进行调度。一个任务必须等到所有优先级比它高的任务调度完才可以被调度。</p><p>该策略会导致低优先级任务饥饿的问题,很容易出现因为大量的高优先级任务不断地进入系统导致低优先级任务饥饿。</p><h4 id="多级反馈队列"><a href="#多级反馈队列" class="headerlink" title="多级反馈队列"></a>多级反馈队列</h4><p>在多级队列的基础上,多级反馈队列增加了动态设置任务优先级的策略。该策略会先对人物的运行时间进行评估,其中短任务会拥有更高的优先级。但是在真实世界中无法预知任务的完成时间,为此多级反馈队列策略会统计每个任务已经执行多长的时间,以此判断该任务是长任务还是短任务。当任务进入系统时,该策略会默认该任务为短任务,为其设置最高优先级。然后该策略会为每个任务队列设置任务的最大运行时间,如果任务在当前队列运行的总时间最终超过了队列允许的最大时间,那么会认为该任务是运行时间较长的任务,将该任务的优先级减一。</p><p>多级反馈队列策略能够动态的评估任务的运行时间,适配大致的任务优先级。同时在该策略中会定时的将所有任务的优先级提升至最高,保证不会出现任务饥饿的情况。该调度策略能够达到低平均周转周期的同时保证任务的响应时间,避免任务饥饿,在许多的操作系统中得到了应用,例如早期的Linux。</p><h2 id="多核调度策略"><a href="#多核调度策略" class="headerlink" title="多核调度策略"></a>多核调度策略</h2><p>相比于单核调度,多核调度策略还需要解决在哪个CPU上执行的问题。</p><h3 id="负载分担"><a href="#负载分担" class="headerlink" title="负载分担"></a>负载分担</h3><p>沿用单核跳读策略的思路,假设多核共享一个全局运行队列,当一个CPU核心需要调度任务时,从全局运行队列中获取一个任务作为下一个由他执行的任务。</p><p>该策略的优点在于设计实现简单,可以将多和调度策略问题规约为单核调度策略问题,同时不会出现CPU资源浪费的情况。但是也存在对应的问题,例如在多核处理器中,多核之间如何同步全局共享运行队列的信息或导致调度开销变得不可忽视。同时在多个CPU核心之间来回切换的开销很大,人物在不同CPU核心之间切换会导致诸如重新载入缓存、TLB刷新等问题。</p><h3 id="协同调度"><a href="#协同调度" class="headerlink" title="协同调度"></a>协同调度</h3><p>多线程为了充分利用多核处理器,通常会将工作量较大的任务切分为多个子任务,这些子任务中可能会存在依赖关系。协同调度的目的是尽可能让一组任务并行执行,避免调度器调度有依赖关系的两组任务。</p><p>群组调度是协同调度的一个经典策略,其基本思想是将关联任务设置为一组,并且以组为单位调度人物在多个CPU核心上执行,使他们的开始时间与结束时间尽可能保持接近。通过将任务以组为单位在多核处理器上进行调度,群组调度策略可以提升特定应用场景下的任务执行性能。</p><h3 id="两级调度"><a href="#两级调度" class="headerlink" title="两级调度"></a>两级调度</h3><p>由于全局调度器在不同CPU核心上切换执行会造成切换开销,为了减少开销,每个人物都尽可能只在一个CPU核心上进行调度。因此新的调度策略改为每个CPU核心都引入一个本地调度器,并用它管理对应核心上执行的任务。这种调度策略同时使用全局调度器和本地调度器,被称为两级调度。</p><p>当一个任务进入系统时,会根据系统的当前信息,决定该任务在哪个CPU核心上被执行。当一个任务被分配到给定的CPU核心时,将一直被该核心的本地调度器管理,不会迁移到其他CPU核心上运行。</p><h3 id="负载均衡"><a href="#负载均衡" class="headerlink" title="负载均衡"></a>负载均衡</h3><p>两级调度将任务绑定在特定的CPU核心上调度执行,避免了任务在多核间切换,有着良好的性能。但是两级调度没有任务在CPU核心间切换的机制,会导致多核间的负载不均匀。为了解决这一问题,引入了负载均衡策略,通过追踪每个CPU核心的负载情况,将处于高CPU负载核心管理的任务迁移到低负载CPU核心上,保证每个核心负载大致相同。</p>]]></content>
<summary type="html"><p>操作系统会同时处理多个请求,但是硬件的运行资源是有限的。调度就是用来协调每个请求对于资源的使用方法。</p>
<p>一般调度器会通过维护运行队列来管理任务。运行队列不一定需要使用先进先出的队列,例如在Linux中使用的调度器就会使用红黑树来运行队列。当任务在执行时出发一定的</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
</entry>
<entry>
<title>进程线程与纤程</title>
<link href="http://qian0817.top/2020/11/21/%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B%E4%B8%8E%E7%BA%A4%E7%A8%8B/"/>
<id>http://qian0817.top/2020/11/21/%E8%BF%9B%E7%A8%8B%E7%BA%BF%E7%A8%8B%E4%B8%8E%E7%BA%A4%E7%A8%8B/</id>
<published>2020-11-21T11:08:01.000Z</published>
<updated>2020-12-30T04:35:50.160Z</updated>
<content type="html"><![CDATA[<h2 id="进程"><a href="#进程" class="headerlink" title="进程"></a>进程</h2><h3 id="进程状态"><a href="#进程状态" class="headerlink" title="进程状态"></a>进程状态</h3><p>线程可以处于以下几种状态</p><ul><li>新生状态:表示一个线程刚被创建出来还未完成初始化,不能被调度执行。在初始化完成以后,进入预备状态。</li><li>预备状态:表示进程可以被调度运行,但还未被调度器选择。在被调度器选择以后进入运行状态。</li><li>运行状态:表示线程正在CPU上运行。当运行一段时间后,调度器可以将其重新放回调度队列之中,就会迁移至预备状态。进程结束时迁移至终止状态。当需要等待外部事件时,迁移至阻塞状态。</li><li>阻塞状态:表示线程需要等待外部事件,暂时无法被调度。当线程等待的外部事件完成以后,迁移至预备状态。</li><li>终止状态:表示进程已经完成执行,不会再被调度。</li></ul><p><img data-src="%E8%BF%9B%E7%A8%8B%E7%8A%B6%E6%80%81.png" alt="进程状态"></p><h3 id="进程内存空间布局"><a href="#进程内存空间布局" class="headerlink" title="进程内存空间布局"></a>进程内存空间布局</h3><p>进程拥有独立的虚拟内存空间。包括以下几个部分</p><ul><li>用户栈:保存进程需要使用的各种临时数据。其扩展方向为自顶向下,栈底在高地址上。</li><li>代码库:进程执行所依赖的共享的代码库(比如libc),这些地址会映射在用户栈下方并标记为只读。</li><li>用户堆:管理进程动态分配的内存。其扩展方向为自底向上,堆顶在高地址上。</li><li>数据与代码段:保存全局变量的值以及进程执行所需要的代码,处于较低地址。</li><li>内核部分:每个进程的虚拟地址空间都映射了相同的地址空间,处于进程地址空间的最顶端。只有当程序进入内核态时才能访问内核内存。</li></ul><p><img data-src="%E8%BF%9B%E7%A8%8B%E5%86%85%E5%AD%98%E7%A9%BA%E9%97%B4%E5%B8%83%E5%B1%80.png" alt="进程内存空间布局"></p><h3 id="进程控制块"><a href="#进程控制块" class="headerlink" title="进程控制块"></a>进程控制块</h3><p>在内核中,每一个进程都通过一个名为进程控制块的数据结构来保存相关状态。在Linux系统中PCB对应的数据结构为task_struct,定义在[<a href="https://github.com/torvalds/linux/blob/master/include/linux/sched.h">sched.h</a>]文件中。下面代码列举了几个重要字段。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">task_struct</span>{</span></span><br><span class="line"> <span class="comment">/* 进程状态 */</span></span><br><span class="line"><span class="keyword">volatile</span> <span class="keyword">long</span>state;</span><br><span class="line"> <span class="comment">/* 虚拟内存状态 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">mm_struct</span> *<span class="title">mm</span>;</span></span><br><span class="line"> <span class="comment">/* 进程标识符 */</span></span><br><span class="line"> <span class="keyword">pid_t</span> pid;</span><br><span class="line"> <span class="comment">/* 进程组标识符 */</span></span><br><span class="line"> <span class="keyword">pid_t</span> tgid;</span><br><span class="line"> <span class="comment">/* 进程间关系 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">task_struct</span> __<span class="title">rcu</span> *<span class="title">parent</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">list_head</span> <span class="title">children</span>;</span></span><br><span class="line"> <span class="comment">/* 打开的文件 */</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">files_struct</span> *<span class="title">files</span>;</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><h3 id="上下文切换"><a href="#上下文切换" class="headerlink" title="上下文切换"></a>上下文切换</h3><p>进程的上下文包括进程运行时的寄存器状态,能够用于保存和恢复一个进程在处理器上运行的状态。当操作系统需要切换当前执行的进程时,就会使用上下文切换机制,将起一个进程的寄存器保存到PCB之中,从而切换该线程执行。</p><p>下图展示了线程的上下文切换过程,当进程1用于中断或者系统调用进入内核以后,操作系统将进行上下文切换,保存将线程1的上下文保存到对应的PCB之中去,之后取出线程2对应的PCB的上下文,将其中的值恢复到寄存器中,最后操作系统回到用户态,继续进程2的运行。</p><p><img data-src="%E8%BF%9B%E7%A8%8B%E4%B8%8A%E4%B8%8B%E6%96%87%E5%88%87%E6%8D%A2.png" alt="进程上下文切换"></p><h3 id="Linux进程操作"><a href="#Linux进程操作" class="headerlink" title="Linux进程操作"></a>Linux进程操作</h3><h4 id="进程创建fork"><a href="#进程创建fork" class="headerlink" title="进程创建fork"></a>进程创建fork</h4><p>在Linux中使用fork函数创建新进程,对于父进程fork函数返回当前进程的PID,子进程的返回值为0。以下是fork的一个示例。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><unistd.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">pid_t</span> rc = fork();</span><br><span class="line"> <span class="keyword">if</span> (rc < <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"fork 失败"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"当前为子进程, pid: "</span> << getpid() << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"当前为父进程, pid: "</span> << getpid() << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h4 id="进程执行exec"><a href="#进程执行exec" class="headerlink" title="进程执行exec"></a>进程执行exec</h4><p>exec由一系列接口组成,存在多个变种,功能最全面的是execve:</p><figure class="highlight c++"><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">#<span class="meta-keyword">include</span><span class="meta-string"><unistd.h></span></span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">execve</span> <span class="params">(<span class="keyword">const</span> <span class="keyword">char</span> *__path, <span class="keyword">char</span> *<span class="keyword">const</span> __argv[], <span class="keyword">char</span> *<span class="keyword">const</span> __envp[])</span></span></span><br></pre></td></tr></table></figure><p>其共接受三个参数,第一个参数path是进程执行需要载入的可执行文件的路径,第二个参数argv是进程执行所需的参数,第三个参数envp是进程的环境变量,一般以键值对字符串的形式传入。</p><h4 id="进程管理"><a href="#进程管理" class="headerlink" title="进程管理"></a>进程管理</h4><p>在Linux中,进程都由fork创建,操作系统会以fork作为线索记录进程之间的关系,PID会记录每个进程的父进程和子进程。</p><p>处于进程树根部的是init进程,是操作系统创建的第一个进程,之后所有的进程都是由他直接或间接创建出来的。</p><p>为了方便应用程序进行管理,内核还定义了多个进程组合而成的进程组和会话。</p><p>进程组是进程的集合,由一个或多个进程组成。task_struct结构中的tgid即为进程对应的进程组标识符。如果进程想要脱离当前的进程组,可以通过调用setpgid修改自己所在的进程组。</p><p>会话是进程组的集合。由一个或多个进程组构成。会话将进程组根据执行状态分为了前台进程组、后台进程组。控制终端是会话与外界进行交互的窗口,负责接受由用户发来的输入。</p><h4 id="进程监控wait"><a href="#进程监控wait" class="headerlink" title="进程监控wait"></a>进程监控wait</h4><p>进程可以通过wait操作来对子进程进行监控。wait函数有很多变种,以waitpid为例:</p><figure class="highlight c++"><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">#<span class="meta-keyword">include</span><span class="meta-string"><sts/wait.h></span></span></span><br><span class="line"><span class="function"><span class="keyword">__pid_t</span> <span class="title">waitpid</span> <span class="params">(<span class="keyword">__pid_t</span> __pid, <span class="keyword">int</span> *__stat_loc, <span class="keyword">int</span> __options)</span></span>;</span><br></pre></td></tr></table></figure><p>其中第一个参数表示需要等待的子进程,第二个参数用来表示子进程的状态,第三个参数表示选项。wait可以回收已经运行结束的子进程和释放资源。使用wait的一个示例如下</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><unistd.h></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><sys/wait.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">pid_t</span> rc = fork();</span><br><span class="line"> <span class="keyword">if</span> (rc < <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"fork 失败"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (rc == <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"子进程退出"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">int</span> status = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">if</span> (waitpid(rc, &status, <span class="number">0</span>) < <span class="number">0</span>) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"waitpid 失败"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">if</span> (WIFEXITED(status)) {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"父进程: 子进程退出"</span> << <span class="built_in">std</span>::<span class="built_in">endl</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>如果父进程没有调用wait操作,就算子进程已经终止,所占用的资源不会完全释放,这种进程被称为僵尸进程。内核会为僵尸进程保留其进程描述符和中止时信息,以便父进程在调用wait时监控子进程的状态。如果一个进程大量创建子进程且不调用wait,那么会导致僵尸进程占据可用的PID,使得后续的fork因为内核资源不足而失败。直到父线程退出,那么子进程将不会再被父进程使用,所有父进程创建的僵尸进程都会被内核的init进程调用wait进行回收。</p><h2 id="线程"><a href="#线程" class="headerlink" title="线程"></a>线程</h2><p>随着计算机技术的发展,进程显得过于笨重,主要体现在以下几点。</p><ol><li>创建进程的开销较大,需要完成创建独立的地址空间、载入数据和代码段、初始化堆等步骤。</li><li>进程用于独立的地址空间,在进程间进行数据的共享和同步比较麻烦</li></ol><p>因此提出了在进程之内添加可独立执行的单元,他们共享进程的地址空间,但又各自保存运行时的上下文状态,称为线程。</p><h3 id="线程地址空间布局"><a href="#线程地址空间布局" class="headerlink" title="线程地址空间布局"></a>线程地址空间布局</h3><p>进程为每个线程都准备了不同的栈,供它们存放临时的数据。进程除了栈只玩的所有其他区域都由该进程的所有线程共享,包括堆数据段以及代码段。</p><h3 id="线程控制块"><a href="#线程控制块" class="headerlink" title="线程控制块"></a>线程控制块</h3><p>与进程类似,线程也有自己的线程控制块(TCB),用于保存与自身相关的信息。</p><h2 id="纤程"><a href="#纤程" class="headerlink" title="纤程"></a>纤程</h2><p>随着计算机的发展,应用程序变得越来越复杂,在复杂。与操作系统调度器相比,应用程序对于线程的语义以及执行状态更加了解,因此可以做出更加的调度策略。此外用户态线程更加轻量级,创建与上下文切换的开销更小。在这样的背景下,操作系统开始提供对用户态线程(即纤程)的支持。</p><p>纤程的上下文切换的触发机制与内核态线程有较大不同。内核态线程的切换时强制的,因此称为抢占式多任务处理。而纤程在遇到阻塞时会放弃CPU,允许其他纤程的调度,这种调度方式称为合作式多任务处理。</p><p>除了操作系统以外,很多程序语言也提供了对于纤程的支持,方便应用程序创建和管理轻量级的上下文,从而支持更复杂的应用。一般将程序语言提供的纤程支持称为协程。</p><h3 id="POSIX的协程支持"><a href="#POSIX的协程支持" class="headerlink" title="POSIX的协程支持"></a>POSIX的协程支持</h3><p>POSIX中用来支持纤程的是ucontext.h中的接口。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span><span class="meta-string"><ucontext.h></span></span></span><br><span class="line"><span class="comment">/* 保存当前上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">getcontext</span> <span class="params">(<span class="keyword">ucontext_t</span> *__ucp)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 切换到另一个上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">setcontext</span> <span class="params">(<span class="keyword">const</span> <span class="keyword">ucontext_t</span> *__ucp)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 切换上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">swapcontext</span> <span class="params">(<span class="keyword">ucontext_t</span> *__restrict __oucp,<span class="keyword">const</span> <span class="keyword">ucontext_t</span> *__restrict __ucp)</span></span>;</span><br><span class="line"></span><br><span class="line"><span class="comment">/* 创建全新的上下文 */</span></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">makecontext</span> <span class="params">(<span class="keyword">ucontext_t</span> *__ucp, <span class="keyword">void</span> (*__func) (<span class="keyword">void</span>),<span class="keyword">int</span> __argc, ...)</span></span>;</span><br></pre></td></tr></table></figure><p>下面的代码实现了生产者消费者模型。在主函数中使用makecontext创建了两个上下文context1和context2,分别用于调用produce和consume函数。此后程序通过setcontext不断在produce和consume函数之间跳转,不断重复的生产和消费。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><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></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><sys/wait.h></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><ucontext.h></span></span></span><br><span class="line"></span><br><span class="line"><span class="keyword">ucontext_t</span> context1, context2;</span><br><span class="line"><span class="keyword">int</span> current = <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">produce</span><span class="params">()</span> </span>{</span><br><span class="line"> current++;</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"生产者当前值为"</span> << current << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> setcontext(&context2);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">consumer</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"消费者当前值为"</span> << current << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> setcontext(&context1);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">char</span> iteratorStack1[SIGSTKSZ];</span><br><span class="line"> <span class="keyword">char</span> iteratorStack2[SIGSTKSZ];</span><br><span class="line"> getcontext(&context1);</span><br><span class="line"> context1.uc_link = <span class="literal">nullptr</span>;</span><br><span class="line"> context1.uc_stack.ss_sp = iteratorStack1;</span><br><span class="line"> context1.uc_stack.ss_size = <span class="keyword">sizeof</span>(iteratorStack1);</span><br><span class="line"> makecontext(&context1, produce, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> getcontext(&context2);</span><br><span class="line"> context2.uc_link = <span class="literal">nullptr</span>;</span><br><span class="line"> context2.uc_stack.ss_sp = iteratorStack2;</span><br><span class="line"> context2.uc_stack.ss_size = <span class="keyword">sizeof</span>(iteratorStack2);</span><br><span class="line"> makecontext(&context2, consumer, <span class="number">0</span>);</span><br><span class="line"></span><br><span class="line"> setcontext(&context1);</span><br><span class="line"> <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过利用纤程,可以对生产者消费者这样的多个模块协作的场景进行有效的支持。在生产者完成任务以后,可以立即切换到消费者继续执行。由于该切换由用户态线程库完成,不需要操作系统调用,可以达到很好的性能。</p>]]></content>
<summary type="html">介绍进程线程与纤程</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="线程" scheme="http://qian0817.top/tags/%E7%BA%BF%E7%A8%8B/"/>
<category term="协程" scheme="http://qian0817.top/tags/%E5%8D%8F%E7%A8%8B/"/>
</entry>
<entry>
<title>操作系统内存管理</title>
<link href="http://qian0817.top/2020/11/21/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/"/>
<id>http://qian0817.top/2020/11/21/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86/</id>
<published>2020-11-21T06:31:38.000Z</published>
<updated>2020-12-30T04:35:50.057Z</updated>
<content type="html"><![CDATA[<p>为了让不同的应用程序能够既高效又安全的共同使用内存,现代操作系统的普遍做法是在应用程序和物理内存之间加入一个新的抽象:虚拟内存。应用程序面向虚拟内存进行编写而不是面向物理内存进行编写,应用程序在运行时只能够使用虚拟内存,操作系统负责虚拟内存和物理内存之间的映射,CPU负责将虚拟内存翻译为物理内存。</p><p>CPU中的组件内存管理单元(MMU)负责虚拟地址到物理地址的转换。程序在CPU上运行时,它使用的虚拟地址都会由MMU进行翻译。当需要访问物理内存设备的时候,MMU翻译出的物理地址信息将会通过总线传递到相应的物理内存设备中去,从而完成相对应的物理内存读写请求。</p><h2 id="分段与分页机制"><a href="#分段与分页机制" class="headerlink" title="分段与分页机制"></a>分段与分页机制</h2><p>MMU将虚拟地址转换为物理地址的主要机制有两种,分别为分段机制和分页机制</p><h3 id="分段"><a href="#分段" class="headerlink" title="分段"></a>分段</h3><p>在分段机制下,操作系统以段为的形式管理物理内存。当CPU访问虚拟内存地址空间中的某一段时,MMU会通过查询段表得到该段对应的物理内存区域。虚拟地址由两个部分组成,第一个部分表示段号,标识着该虚拟地址属于整个虚拟地址空间的哪一段;第二部分表示段内地址,即相对于该段起始地址的偏移量。在段表中会存放着一个虚拟地址空间中每一个分段的信息,包括段起始地址和段长。在翻译虚拟地址的过程之中,MMU首先会通过段表基址寄存器来找到段表的位置,结合代翻译虚拟地址中的段号找到对应段的信息,然后取出该段的起始地址,加上段内地址就能得到最终的物理地址。段表中还存有段长信息来检查虚拟地址是否超过合法范围。</p><p><img data-src="%E5%88%86%E6%AE%B5.png" alt="分段机制"></p><p>在分段机制下,虚拟地址和物理地址都以段为单位进行分配。这种分配方式会导致物理内存的外部碎片,在段与段之间留下碎片空间,从而造成物理资源利用率的降低。例如一个3GB的内存被分为3段进行分配,第一段为0-1GB,第二段为1-2GB,第三段为2-3GB。如果第一段和第三段被释放,之后又分配一个2GB的段会出现分配失败的情况。</p><h3 id="分页"><a href="#分页" class="headerlink" title="分页"></a>分页</h3><p>分页机制时现代操作系统广泛采用的机制。其思想是将应用程序的虚拟地址空间和物理内存都划分为连续的等长的虚拟页(分段机制下位不同长度的段),由操作系统为每个应用程序构造虚拟内存到物理页的映射关系表(即页表)。虚拟地址由虚拟页号和页内偏移量两个部分组成。在翻译过程中,MMU首先会解析得到虚拟地址的虚拟页号,并通过虚拟页号去页表(存放于页表基地址寄存器)中取出对应的物理页号,然后与虚拟地址中的页内偏移量相加得到最终的物理地址。</p><h4 id="多级页表"><a href="#多级页表" class="headerlink" title="多级页表"></a>多级页表</h4><p>页表负责记录虚拟页到物理页的映射关系。对于63位的虚拟地址空间,假设页的大小为4KB,页表中每一项大小为8字节,那么一张页表的大小为2^64/4*8字节大小即32PB。为了对页表进行压缩,操作系统引入了多级页表的结构,用来满足虚拟内存在高效性上的要求。</p><p>在使用多级页表的时候,一个虚拟地址仍然包括虚拟页号以及业内偏移量,其中虚拟页号被划分为了多个部分,虚拟页号i对应着该虚拟地址在第i级页表中的索引。当任意一级的页表中的某一个条目为空时,该条目下一级的页表就不需要存在,因此多级页表的设计极大地减少了页表占用的空间大小。</p><p>以五层页表为例,通常在当MMU翻译一个虚拟地址的时候,会先根据页表基地址寄存器中的物理地址找到第0级页表页,将第0级的虚拟页号作为页表项索引,读取对应的页表项,该页表项中存储着下一级的页表项的物理地址,按照类似的方式依次读取第2级页表,第3级页表。MMU将在第3级页表项之中的页表项里面找到该虚拟地址对应的物理页号,结合虚拟地址中的业内偏移量获取最终的物理地址。</p><h4 id="TLB"><a href="#TLB" class="headerlink" title="TLB"></a>TLB</h4><p>多级页表能够压缩页表大小,但是会导致页表翻译时间的增加。为了减少地址翻译的访存次数,MMU引入了TLB机制来加速地址翻译的过程。在TLB之中缓存了虚拟页号到物理页号的映射关系,可以简单地将TLB简化成为一个键为虚拟页号值为物理页号的哈希表。</p><h4 id="换页与缺页异常"><a href="#换页与缺页异常" class="headerlink" title="换页与缺页异常"></a>换页与缺页异常</h4><p>换页机制是当物理内容容量不够的时候,操作系统将物理页的数据写入到磁盘之中,然后回收这些物理页并继续使用。当应用程序访问已分配但是未映射到物理内存的虚拟页时,就会触发缺页异常。此时操作系统会找到空闲的物理页,将之前写到磁盘上的数据重新加载到物理页之中,并在页表之中填写虚拟地址到这一物理页的映射,之后CPU回到发生缺页中断的地方继续运行。</p><h4 id="页替换策略"><a href="#页替换策略" class="headerlink" title="页替换策略"></a>页替换策略</h4><p>当分配物理页时,如果空闲的物理页已经用完,操作系统将会根据页替换策略分配一个物理页换出到磁盘中以让出空间。</p><p>OPT策略在选择被换出的页时,优先选择未来最长事件不会再访问的页。这种策略是最优的页替换策略,但是在实际中因为页访问策略取决于应用程序,操作系统无法预知应用程序的行为,所以很难进行实现。</p><p>FIFO策略优先选择最先换入的页进行换出,是最简单的页替换策略之一,因为简单其时间开销很低,但是在实际使用中常常表现不佳。</p><p>LRU策略优先选择最久未被访问的页。在实际实现中,需要精确的记录时间信息记录CPU访问了哪些物理页,其实现开销比较大。</p><p>时钟算法策略将换入物理内存的页号排成一个时钟的形状。该时钟有一个手臂只相信换入内存的后一个,同时为每一个页号维护一个访问标志位,每次需要选择换出页号时,该算法从针臂所指的页号开始检查,如果当前页号的访问位没有色湖之,就将该页替换,否则将访问位清空。如此重复直到找到一个访问位未被置上的页号。</p><h4 id="工作集模型"><a href="#工作集模型" class="headerlink" title="工作集模型"></a>工作集模型</h4><p>如果选择的页替换算法与真实的工作负载不匹配,那么会导致出现颠簸现象,造成性能损失。</p><p>工作集模型能够有效的避免颠簸现象的发生。工作集的定义是一个应用程序在时刻t的工作集W为它在时间区间[t-x,t]使用的内存页集合,也视为它在下一段x时间内会访问的内存页集合。该模型将应用程序的工作集要么全部都在物理内存之中,要么全部被换出,从而减少应用程序工作时发生换页的次数。</p><h2 id="虚拟内存"><a href="#虚拟内存" class="headerlink" title="虚拟内存"></a>虚拟内存</h2><p>虚拟内存使得应用程序能够拥有独立且连续的内存空间。其功能包括共享内存,写时拷贝,内存去重,内存压缩,内存大页等等。</p><p>共享内存允许同一个物理页在不同的应用程序之间共享,且能够同时看到对方修改的内容。共享内存的作用是让不同的应用程序相互通信和传递数据。</p><p>写时拷贝允许多个应用程序以只读的方式共享同一段物理内存。一旦某一个应用程序对该内存进行修改,就会发生缺页异常,操作系统会将对应的物理页重新拷贝一份重新映射给触发异常的应用程序,此后再恢复应用程序的执行。</p><p>基于写时拷贝技术,操作系统进一步设计了内存去重功能,在内存中扫描具有相同内容的物理页,找到映射到这些物理页的虚拟页,然后只保留其中一个物理页,将其他虚拟页以写时拷贝的方式映射到这个物理页。在Linux系统中该功能称为KSM。</p><p>当内存资源不足时,操作系统会使用一些最近不太会使用的内存页进行压缩,从而释放出更多的空闲内存。当应用程序需要进行访问的时候操作系统则将其解压,Linux中的zswap机制就使用到了内存压缩技术。</p>]]></content>
<summary type="html">介绍操作系统的内存管理机制</summary>
<category term="操作系统" scheme="http://qian0817.top/categories/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="操作系统" scheme="http://qian0817.top/tags/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F/"/>
<category term="内存" scheme="http://qian0817.top/tags/%E5%86%85%E5%AD%98/"/>
</entry>
<entry>
<title>JVM中的垃圾回收算法</title>
<link href="http://qian0817.top/2020/10/03/JVM%E4%B8%AD%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/"/>
<id>http://qian0817.top/2020/10/03/JVM%E4%B8%AD%E7%9A%84%E5%9E%83%E5%9C%BE%E5%9B%9E%E6%94%B6%E7%AE%97%E6%B3%95/</id>
<published>2020-10-03T14:33:57.000Z</published>
<updated>2020-12-30T04:35:50.193Z</updated>
<content type="html"><![CDATA[<p>JVM内存运行时区域分为程序计数区、虚拟机栈、本地方法栈、堆区和方法区,其中前三个区域随着线程的生命周期而变化,对于这些区域不需要过多考虑内存回收的问题,当线程结束时,内存也就会被释放了。但是Java堆和方法区则需要使用到垃圾回收器去进行回收。</p><h2 id="判断对象是否存活"><a href="#判断对象是否存活" class="headerlink" title="判断对象是否存活"></a>判断对象是否存活</h2><p>在对垃圾进行回收时,需要先判断哪些对象存活,哪些对象需要进行回收。</p><h3 id="引用计数算法"><a href="#引用计数算法" class="headerlink" title="引用计数算法"></a>引用计数算法</h3><p>通过在对象中添加一个引用计数器,每有一个对象引用时计数器值加上一,当引用失效时,计数器值减去一。其原理简单且判定效率高,例如C++中的shared_ptr就是使用到了引用计数算法。但是引用计数算法无法解决循环引用的问题,例如以下的C++代码A和B最终都不会调用析构函数,因为A和B相互引用对方,即使A和B已经在最后是无法被访问到的。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><iostream></span></span></span><br><span class="line"><span class="meta">#<span class="meta-keyword">include</span> <span class="meta-string"><memory></span></span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span>;</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">B</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><A> a;</span><br><span class="line"> ~B() {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"free B"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">A</span> {</span></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><B> b;</span><br><span class="line"> ~A() {</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">cout</span> << <span class="string">"free A"</span> << <span class="built_in">std</span>::<span class="built_in">endl</span>;</span><br><span class="line"> }</span><br><span class="line">};</span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">main</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><A> a = <span class="built_in">std</span>::make_shared<A>();</span><br><span class="line"> <span class="built_in">std</span>::<span class="built_in">shared_ptr</span><B> b = <span class="built_in">std</span>::make_shared<B>();</span><br><span class="line"> a->b = b;</span><br><span class="line"> b->a = a;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="可达性分析算法"><a href="#可达性分析算法" class="headerlink" title="可达性分析算法"></a>可达性分析算法</h3><p>Java使用的是可达性分析算法来判定对象是否存活。从GC ROOT最为起始节点,从这些节点开始根据引用关系进行搜索,如果某个对象到GC ROOT之间没有引用链相连,那么说明该对象是不可达的即需要回收的对象。</p><p>在JVM中以下对象可以作为GC ROOT</p><ul><li>虚拟机栈中引用的对象。</li><li>方法区中类静态属性引用的对象。</li><li>方法区中常量引用的对象。</li><li>本地方法栈中JNI引用的对象。</li><li>Java虚拟机内部的引用,例如基本数据类型对应的Class对象,常驻的异常对象(例如NullPointException),系统类加载器。</li><li>被同步锁持有的对象。</li><li>反应JVM内存情况的JMXBean、JVMTI中注册的回调,本地代码缓存。</li></ul><h2 id="引用类型"><a href="#引用类型" class="headerlink" title="引用类型"></a>引用类型</h2><p>Java将引用分为了强引用、软引用、弱引用和虚引用,这四种引用强度依次递减。</p><p>强引用指的是代码中普遍存在的引用赋值,例如<code> Object obj = new Object();</code>这种引用关系。只要强引用关系还存在,那么垃圾回收器就不会回收掉被引用的对象。</p><p>软引用描述那些还有用但是非必须的对象。在系统将要发生内存溢出异常之前,会把这些对象进行第二次回收。JDK提供了SoftReference类来实现软引用。</p><p>弱引用描述那些非必要对象,被弱引用关联的对象只能生存到下一次垃圾回收为止。JDK提供了WeakReference来实现弱引用。</p><p>虚引用是最弱的引用关系,虚引用在任何时候都可能会被垃圾回收器回收,主要作用是跟踪对象被垃圾回收的状态。JDK提供了PhantomReference来实现虚引用。</p><h3 id="finalize方法"><a href="#finalize方法" class="headerlink" title="finalize方法"></a>finalize方法</h3><p>在进行可达性分析以后,会判断需要进行回收的对象有无覆盖finalize()方法以及finalize()方法是否已经被调用。如果有必要执行finalize()方法,那么该对象会放置到F-Queue队列之中,由Finalizer线程去执行finalize()方法。JVM不一定会等到finalize()执行完成,同时finalize方法只会执行一次。</p><h2 id="分代收集理论"><a href="#分代收集理论" class="headerlink" title="分代收集理论"></a>分代收集理论</h2><p>大多数垃圾回收器都遵循分代收集理论进行设计,其建立在两个假说之上。</p><ol><li>绝大多数对象都是朝生夕灭的</li><li>熬过越多次垃圾收集过程的对象越难以消亡。</li></ol><p>在划分出不同的区域以后,垃圾回收器就可以每次只回收某一个部分。通常将会把Java堆分为新生代和老年代两个区域。针对不同的区域的特点,有着不同的垃圾收集算法,主要有标记-清除算法、标记-复制算法以及标记-整理算法。</p><h2 id="垃圾收集算法"><a href="#垃圾收集算法" class="headerlink" title="垃圾收集算法"></a>垃圾收集算法</h2><h3 id="标记-清除算法"><a href="#标记-清除算法" class="headerlink" title="标记-清除算法"></a>标记-清除算法</h3><p>标记-清除算法是最早出现的垃圾收集算法,其过程为:首先标记出所有需要回收的对象,在标记完成以后,统一回收掉所有未被标记的对象。标记-清除算法的缺点是执行效率不稳定和内存碎片化问题。</p><h3 id="标记-复制算法"><a href="#标记-复制算法" class="headerlink" title="标记-复制算法"></a>标记-复制算法</h3><p>标记-复制算法将内存容量分为两部分,当一部分的内存用完时,就将还存活的对象复制到另一块上去,将已使用过的内存空间一次性清理掉。其实现简单但是浪费内存空间。</p><p>通常使用这种算法来回收新生代,因为新生代中的大量对象无法熬过第一轮收集,所以不需要按照1:1的比例来划分两个区域的大小。通常将新生代区域分为一块较大的Eden区和两块较小Survivor区,每次内存分配只使用Eden区和一个Survivor区。当进行垃圾回收时,将Eden区和一块Survivor区的仍然存活对象复制到另一块Survivor区,然后清理掉Eden区和已使用的Survivor区空间。当Survivor空间不足以容纳存活的对象时,就需要依赖内存其他区域进行分配担保,通常直接将这些对象进入老年代。</p><h3 id="标记-整理算法"><a href="#标记-整理算法" class="headerlink" title="标记-整理算法"></a>标记-整理算法</h3><p>将所有存活的对象向内存空间的一端移动,直接清理掉边界以外的内存,成为标记-整理算法。其与标记-清除算法的区别在于是否需要移动对象。通常关注吞吐量的垃圾回收器(如Parallel Scavenge)使用的是标记整理算法,而关注停顿时间的收集器(如CMS)使用的是标记-清除算法。</p><h2 id="根节点枚举"><a href="#根节点枚举" class="headerlink" title="根节点枚举"></a>根节点枚举</h2><p>所有的收集器在根节点枚举这一步骤中都需要暂停用户线程,根节点的枚举必须在一个能够保障一致性的快照中进行。HotSpot使用一个OopMap的数据结构来保存哪些地方存在着对象引用,不必要一个不漏的检查完所有的执行上下文和全局的引用位置。当类加载完成时,HotSpot会把对象内什么偏移量上是什么类型的数据计算出来,这样收集器在扫描时可以知道栈里和寄存器里哪些位置是引用了。</p><h2 id="安全点和安全区域"><a href="#安全点和安全区域" class="headerlink" title="安全点和安全区域"></a>安全点和安全区域</h2><p>由于引用关系的变化,导致OopMap的内容变化的指令非常多,如果为每一个指令都生成对应的OopMap,那么会需要占用大量的额外存储中间。</p><p>因此HotSpot只在特定的位置记录这些信息,这些位置被称为安全点。因为安全点的设定,导致程序必须到达安全点之后才能够暂停,而不是在任何位置都能够停下来开始垃圾收集。虚拟机通常使用主动式中断的方式来让用户线程到达安全点,其思路为在需要中断线程的时候,设置一个标志位,各个线程在执行过程中轮询这个标志,一旦发现标志为真时主动运行到最近的安全点进行中断挂起。</p><p>如果在程序不执行的时候(例如线程处于Sleep状态或者Blocked状态),那么这时候线程无法走到安全点去中断挂起自己。因此需要引入安全区域解决这一问题。在安全区域中,引用关系不会发生变化,因此在该区域中的任意位置开始垃圾回收都是安全的。当用户线程进入安全区域中时,会先标识自己进入了安全区域,离开安全区域时,会检查虚拟机是否在垃圾收集过程中需要暂停用户线程的阶段。</p><h2 id="记忆集与卡表"><a href="#记忆集与卡表" class="headerlink" title="记忆集与卡表"></a>记忆集与卡表</h2><p>为了解决跨代引用(或者跨Region)导致的问题,垃圾收集器会建立名为记忆集的数据结构。记忆集用于记录从非收集区域指向收集区域的指针集合的数据结构。在垃圾收集的场景中,不需要记录全部的跨代引用对象,只需要判断某一块非收集区域是否存在指向收集区域的指针。</p><p>通常使用卡表来实现记忆集,其最简单的形式可以只是一个字节数组。字节数组上的每一个元素代表着其标识的内存区域中一块特定大小的内存块,成为卡页。如果一个卡页中存在跨代指针,那么将对应卡页的值标识为1,称为这个元素变脏,否则标识为0。其伪代码如下,使用的卡页为2的9次幂:</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">CARD_TABLE [<span class="keyword">this</span> address >> <span class="number">9</span>] = IS_DIRTY;</span><br></pre></td></tr></table></figure><p>在垃圾收集的过程中,只要筛选出卡页变脏的元素,就能知道哪些卡页内存中包含跨代指针,将其加入GC Roots中一并进行扫描。</p><p>在HotSpot虚拟机中使用写屏障来维护卡表状态,在引用赋值的前后产生一个环形的通知(类似于AOP),供程序执行额外的动作。虚拟机会为赋值操作生成相应的指令,在写屏障中增加了更新卡表操作。其简化逻辑如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">void</span> <span class="title">oop_field_store</span><span class="params">(oop* field, oop new_value)</span></span>{</span><br><span class="line"><span class="comment">// 引用字段赋值操作</span></span><br><span class="line"> *field = new_value;</span><br><span class="line"> <span class="comment">// 写后屏障,完成卡表更新操作</span></span><br><span class="line"> post_write_barrier(field, new_value);</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="keyword">if</span> (CARD_TABLE [<span class="keyword">this</span> address >> <span class="number">9</span>] != DIRTY) </span><br><span class="line">CARD_TABLE [<span class="keyword">this</span> address >> <span class="number">9</span>] = DIRTY;</span><br></pre></td></tr></table></figure><h2 id="并发的可达性分析"><a href="#并发的可达性分析" class="headerlink" title="并发的可达性分析"></a>并发的可达性分析</h2><p>当用户线程与收集器并发工作时,收集器在标记对象是否为垃圾时,用户线程同时也会修改引用关系。此时会出现两种后果:</p><ol><li>将本应该消亡的对象标记为存货</li><li>将存活的对象标记为已消亡</li></ol><p>对于前一种情况,是可以容忍的,但是对于后一种情况会导致运行出错。当且仅当以下两个条件满足时,会产生对象消失的问题:</p><ol><li>赋值器插入了多条从已经被垃圾收集器扫描过的对象到未被垃圾收集器访问的对象的新引用</li><li>赋值器删除了全部从正在被访问过的对象到未被垃圾收集器访问过的对象的直接或间接引用</li></ol><p>因此要解决并发扫描时的对象消失问题,那么只需要破坏其中一个条件即可。对此有两种解决方案:增量更新和原始快照(SATB)。</p><p>增量更新当出现第一种情况时,将新插入的引用记录下来,等扫描结束以后,以这些被记录下来的对象为根,重新扫描一次。</p><p>原始快照当出现第二种情况时,就将被删除的引用记录下来。在扫描结束以后,以这些记录的引用对象为根,重新扫描一次。</p>]]></content>
<summary type="html"><p>JVM内存运行时区域分为程序计数区、虚拟机栈、本地方法栈、堆区和方法区,其中前三个区域随着线程的生命周期而变化,对于这些区域不需要过多考虑内存回收的问题,当线程结束时,内存也就会被释放了。但是Java堆和方法区则需要使用到垃圾回收器去进行回收。</p>
<h2 id="判断</summary>
<category term="JVM" scheme="http://qian0817.top/categories/JVM/"/>
<category term="JVM" scheme="http://qian0817.top/tags/JVM/"/>
</entry>
<entry>
<title>Redis中的哨兵机制</title>
<link href="http://qian0817.top/2020/09/27/Redis%E4%B8%AD%E7%9A%84%E5%93%A8%E5%85%B5%E6%9C%BA%E5%88%B6/"/>
<id>http://qian0817.top/2020/09/27/Redis%E4%B8%AD%E7%9A%84%E5%93%A8%E5%85%B5%E6%9C%BA%E5%88%B6/</id>
<published>2020-09-27T06:25:45.000Z</published>
<updated>2020-12-30T04:35:50.402Z</updated>
<content type="html"><![CDATA[<p> 哨兵是Redis高可用的解决方案,由Sentinel实例组成的哨兵系统可以监视主服务器和从服务器。当被监视的主服务器处于下线状态时,自动将某个从服务器升级为新的主服务器,然后由新的主服务器代替已下线的主服务器处理命令请求。</p><h2 id="启动Sentinel节点"><a href="#启动Sentinel节点" class="headerlink" title="启动Sentinel节点"></a>启动Sentinel节点</h2><p>使用如下的命令可以启动Sentinel节点。其中./sentinel.conf代表sentinel的配置文件位置。</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">redis-sentinel ./sentinel.conf</span><br><span class="line"><span class="meta">#</span><span class="bash"> 或</span></span><br><span class="line">redis-server ./sentinel.conf --sentinel</span><br></pre></td></tr></table></figure><p>当一个Sentinel节点启动时,会执行以下的步骤:</p><h3 id="初始化服务器"><a href="#初始化服务器" class="headerlink" title="初始化服务器"></a>初始化服务器</h3><p>Redis的Sentinel节点本质就是一个Redis服务器,启动Sentinel节点的第一步就是初始化一个Redis服务器。与普通的Redis服务器不同,Sentinel节点不会载入RDB和AOF文件。初始化完成以后,将普通服务器的代码替换成为Sentinel节点的代码,替换的部分包括服务器运行的端口、可执行的命令表等等。</p><h3 id="初始化sentinel状态"><a href="#初始化sentinel状态" class="headerlink" title="初始化sentinel状态"></a>初始化sentinel状态</h3><p>接下来,服务器会初始化一个sentinelState结构,保存所有和Sentinel功能相关的状态。其结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sentinelState</span> {</span></span><br><span class="line"> <span class="comment">// 当前纪元</span></span><br><span class="line"> <span class="keyword">uint64_t</span> current_epoch;</span><br><span class="line"> <span class="comment">// 保存了所有被这个 sentinel 监视的主服务器</span></span><br><span class="line"> <span class="comment">// 字典的键是主服务器的名字</span></span><br><span class="line"> <span class="comment">// 字典的值则是一个指向 sentinelRedisInstance 结构的指针</span></span><br><span class="line"> dict *masters;</span><br><span class="line"> <span class="comment">// 是否进入了 TILT 模式?</span></span><br><span class="line"> <span class="keyword">int</span> tilt;</span><br><span class="line"> <span class="comment">// 目前正在执行的脚本的数量</span></span><br><span class="line"> <span class="keyword">int</span> running_scripts;</span><br><span class="line"> <span class="comment">// 进入 TILT 模式的时间</span></span><br><span class="line"> <span class="keyword">mstime_t</span> tilt_start_time;</span><br><span class="line"> <span class="comment">// 最后一次执行时间处理器的时间</span></span><br><span class="line"> <span class="keyword">mstime_t</span> previous_time;</span><br><span class="line"> <span class="comment">// 一个 FIFO 队列,包含了所有需要执行的用户脚本</span></span><br><span class="line"> <span class="built_in">list</span> *scripts_queue;</span><br><span class="line">} sentinel;</span><br></pre></td></tr></table></figure><p>其中masters字典保存了所有所有被Sentinel节点监视的主服务器的相关信息。其键为监视主服务器的名字,值是一个sentinelRedisInstance结构的指针,代表一个被Sentinel节点监视的Redis服务器实例,可以是主服务器、从服务器或者另外一个Sentinel节点。sentinelRedisInstance结构中会保存实例的IP地址、端口号、实例运行ID等信息。</p><h3 id="创建网络连接"><a href="#创建网络连接" class="headerlink" title="创建网络连接"></a>创建网络连接</h3><p>初始化Sentinel节点的最后一步就是创建连向被监视主服务器的网络连接。</p><p>对于每个被监视的主服务器来说,Sentinel节点会创建两个连向主服务器的异步网络连接。</p><ul><li>命令连接:用于向主服务发送命令,并接受命令回复</li><li>订阅连接:用于订阅主服务器的__sentine__:hello频道</li></ul><p>通过这两个连接,就可以来与被监视主服务器进行通信。</p><h2 id="获取服务器信息"><a href="#获取服务器信息" class="headerlink" title="获取服务器信息"></a>获取服务器信息</h2><h3 id="获取主服务器信息"><a href="#获取主服务器信息" class="headerlink" title="获取主服务器信息"></a>获取主服务器信息</h3><p>Sentinel节点会以每10秒1次的频率,向被监视的主服务器发送INFO信息,通过主服务器回复Sentinel节点可以获取主服务器本身的信息和主服务器属下所有从服务器的信息。通过主服务器传递的从服务器信息,Sentinel无需用户提供服务器的地址信息就可以自动发现从服务器。</p><p>根据主服务器传递的信息,Sentinel会对主服务器的实例结构进行更新。例如主服务器重启以后,运行ID就会和之前保存的ID不同,就需要更新运行ID信息。</p><h3 id="获取从服务器信息"><a href="#获取从服务器信息" class="headerlink" title="获取从服务器信息"></a>获取从服务器信息</h3><p>Sentinel节点在分析INFO命令中的从服务器信息时,会检查从服务器的实例结构是否已经存在于slaves字典之中。如果对应的实例结构已经存在,那么就对已保存的实例结构进行更新。否则说明这个从服务器是新发现的从服务器,会在slaves字典之中为从服务器创建一个新的实例结构。</p><p>当Sentinel发现有新的从服务器出现时,除了会创建对应的实例结构外,还会创建到从服务器的命令连接以及订阅连接。在创建连接之后,Sentinel会以每10秒1次的频率,向从服务器发送INFO命令。根据INFO命令的回复,Sentinel会获得以下的信息:</p><ul><li>从服务器运行ID</li><li>从服务器角色</li><li>主服务器IP和端口号</li><li>主从服务器连接状态</li><li>从服务器优先级</li><li>从服务器的复制偏移量 </li></ul><h2 id="发送与接受信息"><a href="#发送与接受信息" class="headerlink" title="发送与接受信息"></a>发送与接受信息</h2><p>Sentinel会以两秒一次的频率,通过命令连接向所有被监视的主服务器和从服务器发送一条信息,信息的内容包括:</p><ul><li>Sentinel的IP地址</li><li>Sentinel的端口号</li><li>Sentinel的运行ID</li><li>Sentinel的配置纪元</li><li>主服务器的名字</li><li>主服务器的IP地址</li><li>主服务器的端口号</li><li>主服务器当前的配置纪元</li></ul><p>当Sentinel与服务器建立起订阅连接以后,Sentinel会通过订阅连接,向服务器发送以下命令</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SUBSCRIBE __sentinel__:hello</span><br></pre></td></tr></table></figure><p>对于监视一个同服务器的多个Sentinel来说,一个Sentinel发送的信息会被其他Sentinel接收到,用于更新其他Sentinel对于发送信息Sentinel的认知和其他Senttinel对于被监视服务器的认知。</p><p>例如sentinel1、sentinel2、sentinel3都在监视同一个服务器,当sentinel1向服务器的__sentinel__:hello频道发送一条信息时,所有的sentinel都会收到这条信息。</p><p>当Sentinel从__sentinel__:hello频道接收到一条信息时,会对该信息进行分析,提取从中的Sentinel的运行ID,如果与自己的运行ID相同,说明是自己发送的,不做进一步处理。否则说明是其他的Sentinel发送的,更新信息中的各个参数。如果在此过程中发现了新的Sentinel节点,会创建连向其他Sentinel的命令连接,使得最终监视同一服务器的Sentinel形成相互连接的网络。</p><h2 id="检测下线状态"><a href="#检测下线状态" class="headerlink" title="检测下线状态"></a>检测下线状态</h2><p>Sentinel会以每秒一次的频率向所有与它创建了命令连接的实例(包括主服务器,从服务器,Sentinel节点)发送PING命令。通过PING命令的回复来判断实例是否在线。如果一个实例在一定时间内连续向Sentinel返回无效回复,那么会将该实例状态设置为主观下线状态。</p><p>当Sentinel将一个主服务器判断为主观下线之后,为了确认主服务是否真的下线,同样会对监视这一主服务器的Sentinel进行询问。如果从其他Sentinel中接收到足够多的下线判断后,会将服务器判定为客观下线,对主服务器进行故障转移操作。</p><h2 id="选举领头"><a href="#选举领头" class="headerlink" title="选举领头"></a>选举领头</h2><p>当一个主服务器被判断为客观下线之后,监视这个下线服务器的Sentinel会进行协商,选举出一个领头Sentinel。由该领头Sentinel对下线服务器进行故障转移操作。选举领头的方式采用的是Raft算法。</p><h2 id="故障转移"><a href="#故障转移" class="headerlink" title="故障转移"></a>故障转移</h2><p>在选举出领头Sentinel之后,领头Sentinel会对已下线的主服务器进行故障转移操作,其步骤如下:</p><h3 id="选出主服务器"><a href="#选出主服务器" class="headerlink" title="选出主服务器"></a>选出主服务器</h3><p>在主服务器下属的的所有从服务器中,选择出一个状态良好并且数据完整的从服务器,向这个从服务器发送SLAVE no one命令,将从服务器转换为主服务器。在发送完成以后,领头Sentinel会向被升级的从服务器发送INFO命令,观察其角色信息。如果返回的角色信息变为了master,则进行下一步操作。</p><h3 id="修改从服务器的复制目标"><a href="#修改从服务器的复制目标" class="headerlink" title="修改从服务器的复制目标"></a>修改从服务器的复制目标</h3><p>当新的主服务器出现以后,通过向所有的从服务器发送SLAVEOF命令,让所有的从服务器去复制新的主服务器。</p><h3 id="将旧的主服务器变为从服务器"><a href="#将旧的主服务器变为从服务器" class="headerlink" title="将旧的主服务器变为从服务器"></a>将旧的主服务器变为从服务器</h3><p>当旧的主服务器重新上线之后,Sentinel会向其发送SLAVEOF命令,让它成为新的主服务器的从服务器。</p>]]></content>
<summary type="html">介绍Redis中的哨兵机制原理</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
</entry>
<entry>
<title>Redis实现主从复制原理</title>
<link href="http://qian0817.top/2020/09/11/Redis%E5%AE%9E%E7%8E%B0%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E5%8E%9F%E7%90%86/"/>
<id>http://qian0817.top/2020/09/11/Redis%E5%AE%9E%E7%8E%B0%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6%E5%8E%9F%E7%90%86/</id>
<published>2020-09-11T03:07:35.000Z</published>
<updated>2020-12-30T04:35:50.473Z</updated>
<content type="html"><![CDATA[<p>在Redis中可以通过SLAVEOF(5.0版本以后可使用REPLICAOF)命令让一个服务器去复制另一个服务器。被复制的服务器被称为主服务器,对主服务器进行复制的服务器成为从服务器。Redis进行主从同步的过程如下:</p><h2 id="建立套接字连接"><a href="#建立套接字连接" class="headerlink" title="建立套接字连接"></a>建立套接字连接</h2><p>使用以下命令时</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SLAVEOF 127.0.0.1 6379</span><br></pre></td></tr></table></figure><p>服务器会将IP地址127.0.0.1和6379端口保存到redisServer结构体之中。在执行完成之后,服务器根据命令所设置的IP地址以及端口,创建套接字连接。</p><p>如果套接字成功连接,那么主服务器将会为该套接字创建对应的客户端状态,并将从服务器看作一个连接到主服务器的客户端来对待。从服务器会为套接字关联一个用于处理复制工作的文件事件处理器,处理后续的复制工作,比如接受RDB文件,接受主服务器传来的写命令等等。</p><h2 id="发送PING命令"><a href="#发送PING命令" class="headerlink" title="发送PING命令"></a>发送PING命令</h2><p>在创建完成套接字以后,从服务器会像主服务发送一个PING请求,这个PING请求的作用有</p><ol><li>检查套接字的读写状态是否正常。</li><li>检查主服务器是否可以正常处理命令请求。</li></ol><p>如果主服务器返回了PONG,说明网络状态正常并且主服务器可以正常处理请求,从服务器可以继续执行下个步骤。如果出现了回复超市或者主服务器返回了一个错误,从服务器会断开连接并且尝试重连主服务器。</p><h2 id="身份验证"><a href="#身份验证" class="headerlink" title="身份验证"></a>身份验证</h2><p>在返回PONG之后,下一步决定是否进行身份验证。如果主从服务器都没有设置密码或者主从服务器设置了相同的密码,那么直接进入下一步。否则如果,主从服务器设置了不同的密码或者一个设置了密码一个没有设置,那么会身份验证失败,进行重试或者退出。</p><h2 id="发送端口信息"><a href="#发送端口信息" class="headerlink" title="发送端口信息"></a>发送端口信息</h2><p>在完成身份验证以后,从服务器将向主服务器发送从服务器的监听端口号。主服务器接收到该请求以后,会将端口号记录在客户端状态下的slave_listening_port属性中。</p><h2 id="同步"><a href="#同步" class="headerlink" title="同步"></a>同步</h2><p>在这一步中,从服务器会向主服务发送同步命令,执行同步操作,将自己的数据库更新为主数据库当前所处的状态。可以通过SYNC和PSYNC两种命令来实现主从同步。</p><h3 id="SYNC命令"><a href="#SYNC命令" class="headerlink" title="SYNC命令"></a>SYNC命令</h3><h4 id="SYNC命令实现"><a href="#SYNC命令实现" class="headerlink" title="SYNC命令实现"></a>SYNC命令实现</h4><p>SYNC命令的执行步骤如下:</p><ol><li><p>从服务器向主服务器发送SYNC命令</p></li><li><p>收到SYNC命令的主服务器执行BGSAVE命令,生成RDB文件,同时创建一个缓冲区记录从现在开始执行的所有写命令。</p></li><li><p>当BGSAVE命令执行完毕之后,主服务器会将RDB文件发送给从服务器,从服务器载入该RDB文件,将数据库状态更新至执行BGSAVE命令时的数据库状态。</p></li><li><p>主服务器将记录在缓冲区的所有写命令发送给从服务器,从服务器执行这些命令,更新至主服务器当前所处的状态。</p></li></ol><h4 id="SYNC命令的缺陷"><a href="#SYNC命令的缺陷" class="headerlink" title="SYNC命令的缺陷"></a>SYNC命令的缺陷</h4><p>在出现断线后重新复制的情况时,SYNC命令的效率十分低。因为在断线后重连时,主从服务器的大部分数据是一致,从服务器将自己更新为主服务器的状态时,只需要更新中断期间的数据即可。SYNC命令没有利用好这些特性,导致了需要进行全量的RDB备份才能保持数据一致,因此Redis引入了PSYNC命令来代替SYNC命令。</p><h3 id="PSYNC命令"><a href="#PSYNC命令" class="headerlink" title="PSYNC命令"></a>PSYNC命令</h3><p>PSYNC具有完整重同步和部分重同步两种模式,其中完整重同步用于处理初次复制的情况,部分重同步用于处理断线后的重复制情况。完全重同步的过程与SYNC命令的实现类似,部分重同步模式解决了SYNC模式的缺陷。部分重同步的功能有以下三个部分组成</p><h4 id="复制偏移量"><a href="#复制偏移量" class="headerlink" title="复制偏移量"></a>复制偏移量</h4><p>执行复制的双方会分别维护一个复制偏移量。当主服务器每次向从服务器传播N个字节的数据时,就将自己的复制偏移量加上N。从服务器接收到N个字节的数据时,也将自己的复制偏移量加上N。通过对比两者的复制偏移量,可以得出两者的数据是否一致。</p><h4 id="复制积压缓冲区"><a href="#复制积压缓冲区" class="headerlink" title="复制积压缓冲区"></a>复制积压缓冲区</h4><p>复制积压缓冲区是一个由主服务器维护的一个固定长度的先进先出的队列,默认大小为1MB。当主服务器进行命令传播时,会将写命令入队到复制积压缓冲区之中,同时记录队列中每个字节记录的相应的复制偏移量。</p><p>当从服务器重新连接上主服务器的时候,从服务器会将自己的复制偏移量发送给主服务器。如果该复制偏移量仍然存在于复制积压缓冲区,则执行部分重同步操作,否则执行完整重同步操作。</p><h4 id="服务器运行ID"><a href="#服务器运行ID" class="headerlink" title="服务器运行ID"></a>服务器运行ID</h4><p>每个Redis服务器都会有自己的运行ID,由40个随机的十六进制字符组成,在服务器启动时自动生成。当从服务器进行初次复制的时候,主服务器会将自己的运行ID传送给服务器。当从服务器断线重连时,从服务器发送之前保存的运行ID。如果主服务器接收到的运行ID与自己的运行ID相同,说明从服务器断线之前复制的就是当前连接的主服务器,则执行部分重同步操作,否则执行完整重同步操作。</p><h2 id="命令传播"><a href="#命令传播" class="headerlink" title="命令传播"></a>命令传播</h2><p>当同步操作完成以后,主从服务器的数据库达到一致的水平,但是每当主服务器执行客户端发送的写命令时,主服务器的数据库状态会被修改,导致主从服务器状态不一致。</p><p>为了让主从服务器保持一致的数据,主服务需要对从服务器执行命令保存操作,将主服务器的写指令发送给从服务器,当从服务器执行了该指令时,主从重新回到了一致的状态。</p><h3 id="心跳检测"><a href="#心跳检测" class="headerlink" title="心跳检测"></a>心跳检测</h3><p>在命令传播阶段,从服务器会以每秒一次的频率向主服务器发送命令,告诉主服务器目前从服务器当前的复制偏移量。其作用有:</p><ol><li>检测主从服务器之间的网络连接</li><li>辅助实现min-slaves选项</li><li>检测命令丢失</li></ol><p>如果在心跳检测的过程中检测出命令丢失的情况,那么主服务器会根据从服务器提交的命令偏移量,在复制挤压缓冲区中找到从服务器中缺少的数据,并将这些数据重新发送给从服务器。</p><h3 id="主从级联分担全量复制压力"><a href="#主从级联分担全量复制压力" class="headerlink" title="主从级联分担全量复制压力"></a>主从级联分担全量复制压力</h3><p>在一次全量复制中,对于主库由两个耗时的操作:生成 RDB 文件和传输 RDB 文件。</p><p>如果从库数量很多,会导致出现主库忙于生成RDB文件,同时占用大量的网络资源用于传输RDB文件。可以通过“主 - 从 - 从”模式将主库生成 RDB 和传输 RDB 的压力,以级联的方式分散到从库上。</p><p>具体的方式为在部署主从集群的时候,可以选择通过手动选择一个从库用于级联其他从库。这些从库在进行同步时,不用再和主库进行交互了,只要和级联的从库进行写操作同步就行了,这就可以减轻主库上的压力。</p>]]></content>
<summary type="html">介绍Redis如何实现主从复制原理</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
</entry>
<entry>
<title>Redis中的客户端与服务器实现</title>
<link href="http://qian0817.top/2020/09/10/Redis%E4%B8%AD%E7%9A%84%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%9E%E7%8E%B0/"/>
<id>http://qian0817.top/2020/09/10/Redis%E4%B8%AD%E7%9A%84%E5%AE%A2%E6%88%B7%E7%AB%AF%E4%B8%8E%E6%9C%8D%E5%8A%A1%E5%99%A8%E5%AE%9E%E7%8E%B0/</id>
<published>2020-09-10T08:08:16.000Z</published>
<updated>2020-12-30T04:35:50.494Z</updated>
<content type="html"><![CDATA[<p>Redis服务器可以与多个客户端建立网络连接,每个客户端也可以向服务器发送命令请求。Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。</p><h2 id="客户端"><a href="#客户端" class="headerlink" title="客户端"></a>客户端</h2><h3 id="客户端结构"><a href="#客户端结构" class="headerlink" title="客户端结构"></a>客户端结构</h3><p>每个进行连接的客户端,都会创建一个redisClient结构,保存了客户端当前的信息。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br><span class="line">82</span><br><span class="line">83</span><br><span class="line">84</span><br><span class="line">85</span><br><span class="line">86</span><br><span class="line">87</span><br><span class="line">88</span><br><span class="line">89</span><br><span class="line">90</span><br><span class="line">91</span><br><span class="line">92</span><br><span class="line">93</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">redisClient</span> {</span></span><br><span class="line"> <span class="comment">// 套接字描述符</span></span><br><span class="line"> <span class="comment">// -1代表伪客户端,来自于AOF文件或者LUA脚本</span></span><br><span class="line"> <span class="comment">// 普通客户端为大于-1的整数,记录套接字描述符</span></span><br><span class="line"> <span class="keyword">int</span> fd;</span><br><span class="line"> <span class="comment">// 当前正在使用的数据库</span></span><br><span class="line"> redisDb *db;</span><br><span class="line"> <span class="comment">// 当前正在使用的数据库的 id (号码)</span></span><br><span class="line"> <span class="keyword">int</span> dictid;</span><br><span class="line"> <span class="comment">// 客户端的名字</span></span><br><span class="line"> robj *name; <span class="comment">/* As set by CLIENT SETNAME */</span></span><br><span class="line"> <span class="comment">// 查询缓冲区</span></span><br><span class="line"> sds querybuf;</span><br><span class="line"> <span class="comment">// 查询缓冲区长度峰值</span></span><br><span class="line"> <span class="keyword">size_t</span> querybuf_peak; <span class="comment">/* Recent (100ms or more) peak of querybuf size */</span></span><br><span class="line"> <span class="comment">// 参数数量</span></span><br><span class="line"> <span class="keyword">int</span> argc;</span><br><span class="line"> <span class="comment">// 参数对象数组</span></span><br><span class="line"> robj **argv;</span><br><span class="line"> <span class="comment">// 记录被客户端执行的命令</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">redisCommand</span> *<span class="title">cmd</span>, *<span class="title">lastcmd</span>;</span></span><br><span class="line"> <span class="comment">// 请求的类型:内联命令还是多条命令</span></span><br><span class="line"> <span class="keyword">int</span> reqtype;</span><br><span class="line"> <span class="comment">// 剩余未读取的命令内容数量</span></span><br><span class="line"> <span class="keyword">int</span> multibulklen; <span class="comment">/* number of multi bulk arguments left to read */</span></span><br><span class="line"> <span class="comment">// 命令内容的长度</span></span><br><span class="line"> <span class="keyword">long</span> bulklen; <span class="comment">/* length of bulk argument in multi bulk request */</span></span><br><span class="line"> <span class="comment">// 回复链表</span></span><br><span class="line"> <span class="built_in">list</span> *reply;</span><br><span class="line"> <span class="comment">// 回复链表中对象的总大小</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">long</span> reply_bytes; <span class="comment">/* Tot bytes of objects in reply list */</span></span><br><span class="line"> <span class="comment">// 已发送字节,处理 short write 用</span></span><br><span class="line"> <span class="keyword">int</span> sentlen; <span class="comment">/* Amount of bytes already sent in the current</span></span><br><span class="line"><span class="comment"> buffer or object being sent. */</span></span><br><span class="line"> <span class="comment">// 创建客户端的时间</span></span><br><span class="line"> <span class="keyword">time_t</span> ctime; <span class="comment">/* Client creation time */</span></span><br><span class="line"> <span class="comment">// 客户端最后一次和服务器互动的时间</span></span><br><span class="line"> <span class="keyword">time_t</span> lastinteraction; <span class="comment">/* time of the last interaction, used for timeout */</span></span><br><span class="line"> <span class="comment">// 客户端的输出缓冲区超过软性限制的时间</span></span><br><span class="line"> <span class="keyword">time_t</span> obuf_soft_limit_reached_time;</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">int</span> flags; <span class="comment">/* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */</span></span><br><span class="line"> <span class="comment">// 当 server.requirepass 不为 NULL 时</span></span><br><span class="line"> <span class="comment">// 代表认证的状态</span></span><br><span class="line"> <span class="comment">// 0 代表未认证, 1 代表已认证</span></span><br><span class="line"> <span class="keyword">int</span> authenticated; <span class="comment">/* when requirepass is non-NULL */</span></span><br><span class="line"> <span class="comment">// 复制状态</span></span><br><span class="line"> <span class="keyword">int</span> replstate; <span class="comment">/* replication state if this is a slave */</span></span><br><span class="line"> <span class="comment">// 用于保存主服务器传来的 RDB 文件的文件描述符</span></span><br><span class="line"> <span class="keyword">int</span> repldbfd; <span class="comment">/* replication DB file descriptor */</span></span><br><span class="line"> <span class="comment">// 读取主服务器传来的 RDB 文件的偏移量</span></span><br><span class="line"> <span class="keyword">off_t</span> repldboff; <span class="comment">/* replication DB file offset */</span></span><br><span class="line"> <span class="comment">// 主服务器传来的 RDB 文件的大小</span></span><br><span class="line"> <span class="keyword">off_t</span> repldbsize; <span class="comment">/* replication DB file size */</span></span><br><span class="line"> sds replpreamble; <span class="comment">/* replication DB preamble. */</span></span><br><span class="line"> <span class="comment">// 主服务器的复制偏移量</span></span><br><span class="line"> <span class="keyword">long</span> <span class="keyword">long</span> reploff; <span class="comment">/* replication offset if this is our master */</span></span><br><span class="line"> <span class="comment">// 从服务器最后一次发送 REPLCONF ACK 时的偏移量</span></span><br><span class="line"> <span class="keyword">long</span> <span class="keyword">long</span> repl_ack_off; <span class="comment">/* replication ack offset, if this is a slave */</span></span><br><span class="line"> <span class="comment">// 从服务器最后一次发送 REPLCONF ACK 的时间</span></span><br><span class="line"> <span class="keyword">long</span> <span class="keyword">long</span> repl_ack_time;<span class="comment">/* replication ack time, if this is a slave */</span></span><br><span class="line"> <span class="comment">// 主服务器的 master run ID</span></span><br><span class="line"> <span class="comment">// 保存在客户端,用于执行部分重同步</span></span><br><span class="line"> <span class="keyword">char</span> replrunid[REDIS_RUN_ID_SIZE+<span class="number">1</span>]; <span class="comment">/* master run id if this is a master */</span></span><br><span class="line"> <span class="comment">// 从服务器的监听端口号</span></span><br><span class="line"> <span class="keyword">int</span> slave_listening_port; <span class="comment">/* As configured with: SLAVECONF listening-port */</span></span><br><span class="line"> <span class="comment">// 事务状态</span></span><br><span class="line"> multiState mstate; <span class="comment">/* MULTI/EXEC state */</span></span><br><span class="line"> <span class="comment">// 阻塞类型</span></span><br><span class="line"> <span class="keyword">int</span> btype; <span class="comment">/* Type of blocking op if REDIS_BLOCKED. */</span></span><br><span class="line"> <span class="comment">// 阻塞状态</span></span><br><span class="line"> blockingState bpop; <span class="comment">/* blocking state */</span></span><br><span class="line"> <span class="comment">// 最后被写入的全局复制偏移量</span></span><br><span class="line"> <span class="keyword">long</span> <span class="keyword">long</span> woff; <span class="comment">/* Last write global replication offset. */</span></span><br><span class="line"> <span class="comment">// 被监视的键</span></span><br><span class="line"> <span class="built_in">list</span> *watched_keys; <span class="comment">/* Keys WATCHED for MULTI/EXEC CAS */</span></span><br><span class="line"> <span class="comment">// 这个字典记录了客户端所有订阅的频道</span></span><br><span class="line"> <span class="comment">// 键为频道名字,值为 NULL</span></span><br><span class="line"> <span class="comment">// 也即是,一个频道的集合</span></span><br><span class="line"> dict *pubsub_channels; <span class="comment">/* channels a client is interested in (SUBSCRIBE) */</span></span><br><span class="line"> <span class="comment">// 链表,包含多个 pubsubPattern 结构</span></span><br><span class="line"> <span class="comment">// 记录了所有订阅频道的客户端的信息</span></span><br><span class="line"> <span class="comment">// 新 pubsubPattern 结构总是被添加到表尾</span></span><br><span class="line"> <span class="built_in">list</span> *pubsub_patterns; <span class="comment">/* patterns a client is interested in (SUBSCRIBE) */</span></span><br><span class="line"> sds peerid; <span class="comment">/* Cached peer ID. */</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">int</span> bufpos;</span><br><span class="line"> <span class="comment">// 回复缓冲区</span></span><br><span class="line"> <span class="keyword">char</span> buf[REDIS_REPLY_CHUNK_BYTES];</span><br><span class="line">} redisClient;</span><br></pre></td></tr></table></figure><p>Redis服务器的clients属性是一个链表,保存了所有客户端的状态。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">redisServer</span>{</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="built_in">list</span>* clients;</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line">};</span><br></pre></td></tr></table></figure><h4 id="套接字描述符"><a href="#套接字描述符" class="headerlink" title="套接字描述符"></a>套接字描述符</h4><p>fd属性记录了客户端正在使用的套接字描述符。根据客户端的不同类型,fd的值可以为-1或者大于-1的整数。</p><p>如果fd的值为-1,代表该客户端为伪客户端。伪客户端的请求来源于AOF文件或者LUA脚本而不是来自于网络。这种客户端不需要使用套接字进行连接。普通客户端的fd属性的值为大于-1的整数,记录客户端套接字的描述符。</p><h4 id="名字"><a href="#名字" class="headerlink" title="名字"></a>名字</h4><p>客户端的名称记录在name属性中。在默认情况下,客户端是没有名字的。通过CLIENT SETNAME命令可以设置名字。</p><figure class="highlight text"><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">127.0.0.1:6379> CLIENT LIST</span><br><span class="line">id=5 addr=127.0.0.1:42718 fd=8 name= ...</span><br><span class="line">127.0.0.1:6379> CLIENT SETNAME test-name</span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> CLIENT LIST</span><br><span class="line">id=5 addr=127.0.0.1:42718 fd=8 name=test-name ...</span><br></pre></td></tr></table></figure><p>如果客户端没有设置名字,那么name属性将会指向NULL,否则指向一个字符串对象,保存客户端的名字。</p><h4 id="标志"><a href="#标志" class="headerlink" title="标志"></a>标志</h4><p>客户端的flag属性记录了客户端的角色,其值可以为单个标志或者多个标志的二进制值。</p><h4 id="命令请求"><a href="#命令请求" class="headerlink" title="命令请求"></a>命令请求</h4><p>客户端的输入输出缓冲区用于保存客户端发送的命令请求,用querybuf属性进行保存。</p><p>例如当客户端发送了SET key value命令,那么querybuf的内容将为以下值</p><figure class="highlight text"><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">*3</span><br><span class="line">$3</span><br><span class="line">set</span><br><span class="line">$3</span><br><span class="line">key</span><br><span class="line">$3</span><br><span class="line">value</span><br></pre></td></tr></table></figure><p>querybuf的大小最大为1GB,如果超过,Redis会关闭该连接。</p><p>在保存到缓冲区之后,会对请求的内容进行分析,并将得出的命令参数和个数保存到argv属性和argc属性中去。 例如对于刚才的命令,argc的值会设置为3,argv会设置为[set,key,value]数组。</p><p>当分析出argc和argv之后,服务器会根据argv[0]的值,在命令表中查找对应的命令实现函数,然后将cmd属性指向该命令结构。然后使用cmd属性所指向的redisCommand结构并设置命令参数信息,调用命令实现函数,执行对应的命令。</p><h4 id="输出缓冲区"><a href="#输出缓冲区" class="headerlink" title="输出缓冲区"></a>输出缓冲区</h4><p>执行命令完成后,命令回复会被保存到输出缓冲区之中。每个客户端都有一个可变大小的缓冲区和固定大小的缓冲区。</p><p>其中固定大小的缓冲区有buf和bufpos两个属性组成。buf属性是一个字节数组,bufpos记录了buf数组目前已使用的字节数量。其作用是保存那些长度比较少的回复。</p><p>可变大小的缓冲区有reply属性保存。reply是一个链表连接多个字符串对象,用于保存那些长度比较大的回复,例如非常长的字符串,很多项组成的列表,很多元素的集合。</p><h4 id="身份验证"><a href="#身份验证" class="headerlink" title="身份验证"></a>身份验证</h4><p>authenticated属性记录了客户端是否通过了身份验证。如果其值为0代表未通过身份验证。</p><h3 id="客户端的创建与关闭"><a href="#客户端的创建与关闭" class="headerlink" title="客户端的创建与关闭"></a>客户端的创建与关闭</h3><h4 id="创建普通客户端"><a href="#创建普通客户端" class="headerlink" title="创建普通客户端"></a>创建普通客户端</h4><p>客户端使用connect函数创建连接时,服务器会调用连接事件处理器,为客户端创建相应的客户端状态,并将该客户端状态添加到服务器状态结构clients链表的末尾。</p><h4 id="关闭普通客户端"><a href="#关闭普通客户端" class="headerlink" title="关闭普通客户端"></a>关闭普通客户端</h4><p>当出现以下情况时会出现客户端关闭:</p><ul><li>客户端与服务器的网络连接被关闭。</li><li>发送了带有不符合协议格式的命令请求。</li><li>客户端成为CLIENT KILL命令的目标</li><li>发送的命令请求大小超过了输入缓冲区的大小</li><li>命令回复超过了输出缓冲的大小</li><li>用户为服务器设置了timeout配置选项。当空转时间超过该选项值时会被关闭。</li></ul><h4 id="伪客户端"><a href="#伪客户端" class="headerlink" title="伪客户端"></a>伪客户端</h4><p>服务器在初始化时会创建执行Lua脚本的Redis命令的伪客户端,并将该伪客户端关联在服务器状态的lua_client属性值中。在服务器关闭时,该客户端才会被关闭。</p><p>服务器在载入AOF文件时,会创建执行AOF文件的伪客户端,在执行完成后,立即关闭该客户端。</p><h2 id="服务器"><a href="#服务器" class="headerlink" title="服务器"></a>服务器</h2><h3 id="命令请求过程"><a href="#命令请求过程" class="headerlink" title="命令请求过程"></a>命令请求过程</h3><h4 id="发送命令请求"><a href="#发送命令请求" class="headerlink" title="发送命令请求"></a>发送命令请求</h4><p>Redis服务器的命令请求来自与Redis客户端。客户端会将命令请求转化为协议格式,通过连接到服务器的套接字,将协议的命令请求发送给服务器。</p><h4 id="读取命令请求"><a href="#读取命令请求" class="headerlink" title="读取命令请求"></a>读取命令请求</h4><p>当套接字变得可读时,会调用命令处理器来执行以下操作</p><ul><li><p>读取套接字中协议格式的命令请求,并将其保存到客户端状态的输入缓冲区中。</p></li><li><p>对输入缓冲区的命令请求进行分析,提取命令参数。</p></li><li><p>调用命令执行器执行指定的命令。</p></li></ul><h4 id="命令执行器"><a href="#命令执行器" class="headerlink" title="命令执行器"></a>命令执行器</h4><p>先根据客户端的argv[0]参数,在命令表中查找参数指定的命令,并保存到cmd属性值中。cmd的属性是redisCommand,其结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">redisCommand</span> {</span></span><br><span class="line"> <span class="comment">// 命令名字</span></span><br><span class="line"> <span class="keyword">char</span> *name;</span><br><span class="line"> <span class="comment">// 实现函数</span></span><br><span class="line"> redisCommandProc *proc;</span><br><span class="line"> <span class="comment">// 参数个数</span></span><br><span class="line"> <span class="keyword">int</span> arity;</span><br><span class="line"> <span class="comment">// 字符串表示的 FLAG</span></span><br><span class="line"> <span class="keyword">char</span> *sflags;</span><br><span class="line"> <span class="comment">// 实际 FLAG</span></span><br><span class="line"> <span class="keyword">int</span> flags;</span><br><span class="line"> <span class="comment">// 从命令中判断命令的键参数。在 Redis 集群转向时使用。</span></span><br><span class="line"> redisGetKeysProc *getkeys_proc;</span><br><span class="line"> <span class="comment">// 指定哪些参数是 key</span></span><br><span class="line"> <span class="keyword">int</span> firstkey; <span class="comment">/* The first argument that's a key (0 = no keys) */</span></span><br><span class="line"> <span class="keyword">int</span> lastkey; <span class="comment">/* The last argument that's a key */</span></span><br><span class="line"> <span class="keyword">int</span> keystep; <span class="comment">/* The step between first and last key */</span></span><br><span class="line"> <span class="comment">// microseconds 记录了命令执行耗费的总毫微秒数</span></span><br><span class="line"> <span class="comment">// calls 是命令被执行的总次数</span></span><br><span class="line"> <span class="keyword">long</span> <span class="keyword">long</span> microseconds, calls;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>在查找到命令以后,执行预备操作,例如检查cmd指针是否为NULL,参数个数是否正确等等。当完成预备操作后,服务器才开始真正的执行命令。redisCommandProc是一个函数指针的别名,参数为redisClient。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="keyword">typedef</span> <span class="keyword">void</span> <span class="title">redisCommandProc</span><span class="params">(redisClient *c)</span></span>;</span><br></pre></td></tr></table></figure><p>在服务器真正实行命令时,只需要调用以下语句即可。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">client->cmd->proc(client);</span><br></pre></td></tr></table></figure><p>被调用的命令实现函数会执行指定的操作,产生相应的命令回复,保存到输出缓冲区之中。</p><p>在执行完成以后,服务器还需要执行一些后续操作,例如打印慢日志,AOF日志输出。完成后续操作以后,就可以处理下一个命令请求了。</p><h3 id="serverCron函数"><a href="#serverCron函数" class="headerlink" title="serverCron函数"></a>serverCron函数</h3><p>Redis中的serverCron函数会每隔一段执行一次,负责管理服务器的资源,保证服务器运转良好。</p><p>在serverCron会进行例如更新服务器时间缓存,执行持久化操作,关闭客户端等等。</p>]]></content>
<summary type="html">介绍Redis如何实现客户端与服务器</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
</entry>
<entry>
<title>Redis中的对象系统</title>
<link href="http://qian0817.top/2020/09/09/Redis%E4%B8%AD%E7%9A%84%E5%AF%B9%E8%B1%A1%E7%B3%BB%E7%BB%9F/"/>
<id>http://qian0817.top/2020/09/09/Redis%E4%B8%AD%E7%9A%84%E5%AF%B9%E8%B1%A1%E7%B3%BB%E7%BB%9F/</id>
<published>2020-09-09T14:33:57.000Z</published>
<updated>2020-12-30T04:35:50.415Z</updated>
<content type="html"><![CDATA[<p>Redis中的对象系统包含字符串对象,列表对象,集合对象,有序集合对象,哈希对象这五种类型。同时在Redis中还实现了基于引用计数的内存回收机制和对象共享机制。Redis中的对象带有访问时间记录信息,能够用于计算数据库键的空转时常。</p><h3 id="对象类型"><a href="#对象类型" class="headerlink" title="对象类型"></a>对象类型</h3><p>Redis使用对象来表示数据库中的键和值。每次新创键一个键值对时,会创建两个对象,一个用作键,一个用作值。</p><h3 id="对象类型结构"><a href="#对象类型结构" class="headerlink" title="对象类型结构"></a>对象类型结构</h3><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">redisObject</span> {</span></span><br><span class="line"> <span class="comment">// 类型</span></span><br><span class="line"> <span class="keyword">unsigned</span> type:<span class="number">4</span>;</span><br><span class="line"> <span class="comment">// 编码</span></span><br><span class="line"> <span class="keyword">unsigned</span> encoding:<span class="number">4</span>;</span><br><span class="line"> <span class="comment">// 对象最后一次被访问的时间</span></span><br><span class="line"> <span class="keyword">unsigned</span> lru:REDIS_LRU_BITS;</span><br><span class="line"> <span class="comment">// 引用计数</span></span><br><span class="line"> <span class="keyword">int</span> refcount;</span><br><span class="line"> <span class="comment">// 指向实际值的指针</span></span><br><span class="line"> <span class="keyword">void</span> *ptr;</span><br><span class="line">} robj;</span><br></pre></td></tr></table></figure><p>其中type属性判断该对象是字符串对象,列表对象,集合对象,有序集合对象,哈希对象中的哪一个类型。encoding属性决定ptr指针指向的对象的底层实现数据结构,其对应的方式为:</p><table><thead><tr><th>类型</th><th>编码</th><th>对象</th></tr></thead><tbody><tr><td>REDIS_STRING</td><td>REDIS_ENCODING_INT</td><td>整数值的字符串对象</td></tr><tr><td>REDIS_STRING</td><td>REDIS_ENCODING_EMBSTR</td><td>使用embstr编码的字符串对象</td></tr><tr><td>REDIS_STRING</td><td>REDIS_ENCODING_RAW</td><td>使用简单字符串实现的字符串对象</td></tr><tr><td>REDIS_LIST</td><td>REDIS_ENCODING_ZIPLIST</td><td>使用压缩列表实现的列表对象</td></tr><tr><td>REDIS_LIST</td><td>REDIS_ENCODING_LINKEDLIST</td><td>使用链表实现的列表对象</td></tr><tr><td>REDIS_HASH</td><td>REDIS_ENCODING_HT</td><td>使用字典实现的哈希对象</td></tr><tr><td>REDIS_HASH</td><td>REDIS_ENCODING_ZIPLIST</td><td>使用压缩列表实现的哈希对象</td></tr><tr><td>REDIS_SET</td><td>REDIS_ENCODING_HT</td><td>使用字典实现的集合对象</td></tr><tr><td>REDIS_SET</td><td>REDIS_ENCODING_INTSET</td><td>使用整数集合实现的集合对象</td></tr><tr><td>REDIS_ZSET</td><td>REDIS_ENCODING_ZIPLIST</td><td>使用压缩列表实现的有序集合对象</td></tr><tr><td>REDIS_ZSET</td><td>REDIS_ENCODING_SKIPLIST</td><td>使用跳跃表和字典实现的有序集合对象</td></tr></tbody></table><p>使用命令OBJECT ENCODING可以查看一个数据库键值对象的编码。</p><figure class="highlight text"><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">127.0.0.1:6379> set testKey testValue </span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING testKey </span><br><span class="line">"embstr"</span><br></pre></td></tr></table></figure><p>通过encoding属性来设定对象所使用的编码,提高了Redis的灵活性。例如在列表元素比较少的时候,Redis会使用压缩列表来作为底层实现,因为此时压缩列表比链表更加节省内存,当元素越来越多时,则会转换为功能更强大的双向链表来实现。</p><p>Redis在自己的对象系统中使用了引用计数的方法来实现内存回收。通过这一机制,程序可以通过对象的引用计数信息,在适当的时候释放对象并进行垃圾回收。refcount属性记录了引用计数的数量,当创建一个新对象时,该值会初始化为1,当该对象被一个新的程序使用时,会增1,不再被一个程序使用时减1。当调用decrRefCount函数使其值为1时,会释放该对象。在Redis中,可以使用OBJECT REFCOUNT命令来查看对象的引用计数。</p><figure class="highlight text"><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">127.0.0.1:6379> SET a 100 </span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> OBJECT REFCOUNT a </span><br><span class="line">(integer) 2147483647</span><br><span class="line">127.0.0.1:6379> SET a 'test' </span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> OBJECT REFCOUNT a </span><br><span class="line">(integer) 1</span><br></pre></td></tr></table></figure><p>Redis在初始服务器的时候,会创建10000个字符串对象,其值为0-9999中所有的整数值。当需要用到这些对象的时候,会共享这些对象而不是新创键对象,这些值的引用计数会固定为2147483647。</p><p>lru属性记录了对象最后一次被命令程序访问的时间,通过OBJECT IDLETIME命令可以打印给定键的空转时长。如果服务器打开了maxmemory选项,当服务器占用的内存超过了所设定的上限值,空转时长比较长的那部分键会优先被释放,从而回收内存。</p><h3 id="字符串对象"><a href="#字符串对象" class="headerlink" title="字符串对象"></a>字符串对象</h3><p>字符串的编码方式可以为int,raw或者embstr。</p><p>如果一个字符串保存的对象为整数值,并且该对象可以用long类型来表示,那么该字符串对象会将整数值保存在ptr属性中。</p><p>如果一个字符串保存的对象为一个字符串值,如果该字符串长度大于44字节(不同Redis版本该值不同),那么会使用SDS来保存这个字符串值,并将编码设置为raw。否则会使用embstr编码的方式来保存这个字符串值。</p><p>embstr编码专门用于保存短字符串。其与raw编码的区别在于SDS和RedisObject的内存布局是否在一起。embstr将SDS和RedisObject合成一块连续的内存布局,其示意图如下:</p><p><img data-src="embstr%E7%BC%96%E7%A0%81%E4%B8%8Eraw%E7%BC%96%E7%A0%81.jpg" alt="embstr编码与raw编码"></p><p>采用embstr有以下好处</p><ul><li><p>内存分配与释放次数由两次变为一次</p></li><li><p>所有数据保存在连续的内存之中,能够更好地利用缓存。</p></li></ul><p>对一个int编码的字符串执行了命令使其不再是一个整数值或者对embstr对象执行任何修改命令时,Redis会将这些对象的编码转化为raw。</p><figure class="highlight text"><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">127.0.0.1:6379> SET number 1 </span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING number </span><br><span class="line">"int"</span><br><span class="line">127.0.0.1:6379> APPEND number 'a' </span><br><span class="line">(integer) 2</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING number </span><br><span class="line">"raw"</span><br><span class="line"></span><br><span class="line">127.0.0.1:6379> SET s "a" </span><br><span class="line">OK</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING s </span><br><span class="line">"embstr"</span><br><span class="line">127.0.0.1:6379> APPEND s "b" </span><br><span class="line">(integer) 2</span><br><span class="line">127.0.0.1:6379> OBJECT ENCODING s </span><br><span class="line">"raw"</span><br></pre></td></tr></table></figure><h3 id="列表对象"><a href="#列表对象" class="headerlink" title="列表对象"></a>列表对象</h3><p>列表对象的编码可以为ziplist或者linkedlist。</p><p>当同时满足以下条件时,使用ziplist作为底层实现:</p><ul><li>列表对象保存的所有字符串长度都小于64字节</li><li>保存的元素个数小于512个</li></ul><h3 id="哈希对象"><a href="#哈希对象" class="headerlink" title="哈希对象"></a>哈希对象</h3><p>列表对象的编码可以为ziplist或者hashtable。</p><p>当同时满足以下条件时,使用ziplist作为底层实现:</p><ul><li>哈希对象保存的所有键值对的键和值的字符串长度都小于64字节</li><li>哈希对象保存的键值对数量小于512个</li></ul><h3 id="集合对象"><a href="#集合对象" class="headerlink" title="集合对象"></a>集合对象</h3><p>集合对象的编码可以为intset或者hashtable。</p><p>当同时满足以下条件时,使用intset作为顶层实现:</p><ul><li>集合对象的所有元素都是整数值</li><li>集合对象保存的元素个数不超过512个</li></ul><h3 id="有序集合对象"><a href="#有序集合对象" class="headerlink" title="有序集合对象"></a>有序集合对象</h3><p>有序对象的编码可以为ziplist和skiplist。</p><p>在使用skiplist作为底层实现的时候,Redis会同时将元素保存到字典和跳跃表中。其中dict字典为有序集合创建了一个从成员到分值的映射,字典的键保存了成员,值保存了元素。通过该字典,使得程序可以通过O(1)的复杂度查找到给定成员的分值。</p><p>当有序集合满足以下两个条件时,对象使用ziplist</p><ul><li>有序集合保存的元素个数小于128个</li><li>有序集合保存的所有元素个数都小于64字节</li></ul>]]></content>
<summary type="html">介绍Redis中的对象系统</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
</entry>
<entry>
<title>Redis如何删除过期键</title>
<link href="http://qian0817.top/2020/09/09/Redis%E5%A6%82%E4%BD%95%E5%88%A0%E9%99%A4%E8%BF%87%E6%9C%9F%E9%94%AE/"/>
<id>http://qian0817.top/2020/09/09/Redis%E5%A6%82%E4%BD%95%E5%88%A0%E9%99%A4%E8%BF%87%E6%9C%9F%E9%94%AE/</id>
<published>2020-09-09T08:55:08.000Z</published>
<updated>2020-12-30T04:35:50.459Z</updated>
<content type="html"><![CDATA[<p>通过EXPIRE,PEXPIRE,SETEX等指令,Redis可以为数据库中的某键设置生存时间,在经过指定时间之后,服务器就会删除生存时间为0的键。</p><h2 id="设置过期时间"><a href="#设置过期时间" class="headerlink" title="设置过期时间"></a>设置过期时间</h2><h3 id="保存过期时间"><a href="#保存过期时间" class="headerlink" title="保存过期时间"></a>保存过期时间</h3><p>redisDb结构的expires字典保存了数据库中所有键的过期时间。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">redisDb</span> {</span></span><br><span class="line"> <span class="comment">// 数据库键空间,保存着数据库中的所有键值对</span></span><br><span class="line"> dict *dict; <span class="comment">/* The keyspace for this DB */</span></span><br><span class="line"> <span class="comment">// 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳</span></span><br><span class="line"> dict *expires; <span class="comment">/* Timeout of keys with a timeout set */</span></span><br><span class="line"> <span class="comment">// ...</span></span><br><span class="line">} redisDb;</span><br></pre></td></tr></table></figure><p>过期字典的键是一个指针指向键空间中的某一个键对象,值是一个long long类型的整数,保存了毫秒精度的UNIX时间戳。当执行设置过期时间的命令时,服务器会在数据库的过期字典中关联给定的数据库键以及过期时间。</p><h3 id="移除过期时间"><a href="#移除过期时间" class="headerlink" title="移除过期时间"></a>移除过期时间</h3><p>PERSIST命令可以移除一个键的过期时间。当需要移除过期时间时,会从expires字典之中移除给定键的键值对关联。</p><h3 id="返回过期时间"><a href="#返回过期时间" class="headerlink" title="返回过期时间"></a>返回过期时间</h3><p>TTL和PTTL命令可以返回秒或毫秒为单位的剩余生存时间。其都是通过计算两个键的过期时间与当前时间之差来实现的。 </p><h2 id="过期键删除策略"><a href="#过期键删除策略" class="headerlink" title="过期键删除策略"></a>过期键删除策略</h2><h3 id="可能的删除策略"><a href="#可能的删除策略" class="headerlink" title="可能的删除策略"></a>可能的删除策略</h3><h4 id="定时删除"><a href="#定时删除" class="headerlink" title="定时删除"></a>定时删除</h4><p>通过在设置键的同时,创建一个定时器。在定时器结束时,立即删除该键。该策略对内存友好,可以保证过期键最快的被删除。但是在比较多过期键的情况下,会很占用CPU资源,导致服务器的响应时间降低。</p><h4 id="惰性删除"><a href="#惰性删除" class="headerlink" title="惰性删除"></a>惰性删除</h4><p>惰性删除指的是当从键空间中获取值时,检查该键是否过期,如果过期的话,则删除该键,否则返回该键。惰性删除策略不会在删除其他无关键上花费CPU时间,属于CPU友好型策略。但是如果一个键已经过期但是一致没有被访问到,那么该键永远不会过期,所占用的内存永远不会释放。</p><h4 id="定期删除"><a href="#定期删除" class="headerlink" title="定期删除"></a>定期删除</h4><p>每隔一段时间,对数据库进行检查,删除其中的过期键。其难点在于确定执行的时长与频率。如果执行的太频繁,会导致占用过多CPU,如果执行的太少,会导致内存浪费的情况。</p><h3 id="Redis中的删除策略实现"><a href="#Redis中的删除策略实现" class="headerlink" title="Redis中的删除策略实现"></a>Redis中的删除策略实现</h3><p>Redis中整合了惰性删除和定期删除两种策略,取得了合理使用CPU时间和内存空间之间的平衡。</p><h4 id="惰性删除实现"><a href="#惰性删除实现" class="headerlink" title="惰性删除实现"></a>惰性删除实现</h4><p>所有读写redis命令在执行之前都会调用惰性删除的代码进行判断,惰性删除的代码如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">/*</span></span><br><span class="line"><span class="comment"> * 检查 key 是否已经过期,如果是的话,将它从数据库中删除。</span></span><br><span class="line"><span class="comment"> *</span></span><br><span class="line"><span class="comment"> * 返回 0 表示键没有过期时间,或者键未过期。</span></span><br><span class="line"><span class="comment"> * 返回 1 表示键已经因为过期而被删除了。</span></span><br><span class="line"><span class="comment"> */</span></span><br><span class="line"><span class="function"><span class="keyword">int</span> <span class="title">expireIfNeeded</span><span class="params">(redisDb *db, robj *key)</span> </span>{</span><br><span class="line"> <span class="comment">// 取出键的过期时间</span></span><br><span class="line"> <span class="keyword">mstime_t</span> when = getExpire(db,key);</span><br><span class="line"> <span class="keyword">mstime_t</span> now;</span><br><span class="line"> <span class="comment">// 没有过期时间</span></span><br><span class="line"> <span class="keyword">if</span> (when < <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>; </span><br><span class="line"> <span class="comment">// 如果服务器正在进行载入,那么不进行任何过期检查</span></span><br><span class="line"> <span class="keyword">if</span> (server.loading) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"><span class="comment">// 当前时间</span></span><br><span class="line"> now = server.lua_caller ? server.lua_time_start : mstime();</span><br><span class="line"> <span class="comment">// 当服务器运行在 replication 模式时</span></span><br><span class="line"> <span class="comment">// 附属节点并不主动删除 key</span></span><br><span class="line"> <span class="comment">// 它只返回一个逻辑上正确的返回值</span></span><br><span class="line"> <span class="comment">// 真正的删除操作要等待主节点发来删除命令时才执行</span></span><br><span class="line"> <span class="comment">// 从而保证数据的同步</span></span><br><span class="line"> <span class="keyword">if</span> (server.masterhost != <span class="literal">NULL</span>) <span class="keyword">return</span> now > when;</span><br><span class="line"> <span class="comment">// 运行到这里,表示键带有过期时间,并且服务器为主节点</span></span><br><span class="line"> <span class="comment">// 如果未过期,返回 0</span></span><br><span class="line"> <span class="keyword">if</span> (now <= when) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"> server.stat_expiredkeys++;</span><br><span class="line"> <span class="comment">// 向 AOF 文件和附属节点传播过期信息</span></span><br><span class="line"> propagateExpire(db,key);</span><br><span class="line"> <span class="comment">// 发送事件通知</span></span><br><span class="line"> notifyKeyspaceEvent(REDIS_NOTIFY_EXPIRED,</span><br><span class="line"> <span class="string">"expired"</span>,key,db->id);</span><br><span class="line"> <span class="comment">// 将过期键从数据库中删除</span></span><br><span class="line"> <span class="keyword">return</span> dbDelete(db,key);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果键已经过期,那么该函数会将输入的键从数据库之中删除,否则不做任何动作。当处理后的键仍然存在时,命令会按照存在的情况继续执行,否则按照不存在的情况执行。</p><h4 id="定期删除实现"><a href="#定期删除实现" class="headerlink" title="定期删除实现"></a>定期删除实现</h4><p>每当Redis的周期操作函数serverCron函数执行时,会调用activeExpireCycle函数进行过期键的清理。其会在规定的时间内,分次遍历数据库中的各个数据库,从expires字典中随机检查一部分过期时间,并删除其中的过期键。在扫描的时候,会提供一个全局变量记录上一次扫描的进度,并在下一次调用时,接着上一次的进度进行新一轮的检查工作。</p><h2 id="AOF-RDB和复制功能处理过期键"><a href="#AOF-RDB和复制功能处理过期键" class="headerlink" title="AOF,RDB和复制功能处理过期键"></a>AOF,RDB和复制功能处理过期键</h2><h3 id="AOF文件处理"><a href="#AOF文件处理" class="headerlink" title="AOF文件处理"></a>AOF文件处理</h3><p>当一个键已经过期时并被删除时,Redis会像AOF日志之中添加一条DEL命令,来记录该键已经被删除。在进行AOF重写的时候,程序会对数据库中的键进行检查,已过期的键不会保存到重写以后的AOF日志之中。</p><h3 id="RDB文件处理"><a href="#RDB文件处理" class="headerlink" title="RDB文件处理"></a>RDB文件处理</h3><p>在生成RDB文件的时候,如果一个键已经过期,那么不会被保存到RDB文件之中。在载入RDB文件的时候,如果以主服务器的方式运行,那么过期的键不会被载入到数据库之中。如果以从服务器的方式运行,那么无论键是否过期都会被载入,因为在于主服务器同步的时候,从服务器的数据就会清空,所以对从服务器也没有影响。</p><h3 id="复制功能处理"><a href="#复制功能处理" class="headerlink" title="复制功能处理"></a>复制功能处理</h3><p>当处于服务器处于复制模式下时,服务器的过期删除键由主服务器控制。在主服务器删除一个过期键之后,会向从服务器发送一个DEL命令,告诉从服务器删除该键。从服务器即使键到达了过期时间也不会删除,知道收到DEL命令时才进行删除。通过主服务器统一的删除过期键可以保证主从一致性。</p>]]></content>
<summary type="html">介绍Redis如何删除过期键</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
</entry>
<entry>
<title>设计模式之创建型模式</title>
<link href="http://qian0817.top/2020/09/09/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/"/>
<id>http://qian0817.top/2020/09/09/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/</id>
<published>2020-09-09T08:54:29.000Z</published>
<updated>2020-12-30T04:35:50.372Z</updated>
<content type="html"><![CDATA[<p>行为型模式主要涉及算法和对象间的职责分配。通过使用对象组合,行为型模式可以描述一组对象应该如何协作来完成一个整体任务。</p><h2 id="观察者模式"><a href="#观察者模式" class="headerlink" title="观察者模式"></a>观察者模式</h2><p>观察者模式可以当一个对象出现改变的时候,会执行对应的方法。一般情况下,被依赖的对象称为被观察者,依赖的对象称为观察者。在kotlin中可以很方便的使用可观察属性Observable来使用观察者模式,具体代码如下:</p><figure class="highlight kotlin"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//kotlin 1.4.0</span></span><br><span class="line"><span class="keyword">import</span> kotlin.properties.Delegates</span><br><span class="line"><span class="keyword">var</span> observer :String <span class="keyword">by</span> Delegates.observable(<span class="string">""</span>){ _, old, new -></span><br><span class="line"> println(<span class="string">"<span class="variable">$old</span> -> <span class="variable">$new</span>"</span>)</span><br><span class="line">}</span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span>{</span><br><span class="line"> observer=<span class="string">"a"</span></span><br><span class="line"> observer=<span class="string">"b"</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>观察者模式可以将发送通知的一方与接收通知的一方进行分离,使其互不影响。</p><h2 id="模板模式"><a href="#模板模式" class="headerlink" title="模板模式"></a>模板模式</h2><p>模板模式在一个方法中定义一个算法骨架,将某些步骤推迟到子类中实现。模板方法可以让子类在不改变算法整体结构的情况下,重新定义算法中的某些步骤。</p><h3 id="模板模式实现"><a href="#模板模式实现" class="headerlink" title="模板模式实现"></a>模板模式实现</h3><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></pre></td><td class="code"><pre><span class="line"><span class="comment">//java 11</span></span><br><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">AbstractClass</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> AbstractClass demo = <span class="keyword">new</span> ConcreteClass1();</span><br><span class="line"> demo.templateMethod();</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">final</span> <span class="keyword">void</span> <span class="title">templateMethod</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> method1();</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> method2();</span><br><span class="line"> <span class="comment">//...</span></span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title">method1</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">abstract</span> <span class="keyword">void</span> <span class="title">method2</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ConcreteClass1</span> <span class="keyword">extends</span> <span class="title">AbstractClass</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">method1</span><span class="params">()</span> </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 class="meta">@Override</span> <span class="function"><span class="keyword">protected</span> <span class="keyword">void</span> <span class="title">method2</span><span class="params">()</span> </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>为了避免子类重写,templateMethod()函数需要定义为final。method1()和method2()定义为abstract,强迫子类去实现。templateMethod就是算法的骨架即模板方法。</p><h3 id="模板模式使用场景"><a href="#模板模式使用场景" class="headerlink" title="模板模式使用场景"></a>模板模式使用场景</h3><p>模板模式的的作用在于方便复用和扩展。在例子中可以将算法中不变的流程抽象到父类的模板方法templateMethod()中,将可变的部分留给子类来实现,这方便了代码的复用。同时允许子类在实现的方法中处理自己的逻辑,这增加了框架的扩展性。</p><h2 id="策略模式"><a href="#策略模式" class="headerlink" title="策略模式"></a>策略模式</h2><p>策略模式的作用在于可以用于避免冗长的if-else或switch分支判断,同时可以为框架提供扩展点。</p><h3 id="策略模式实现"><a href="#策略模式实现" class="headerlink" title="策略模式实现"></a>策略模式实现</h3><p>策略模式的定义比较简单,所有的策略类都实现相同的接口,通过基于接口编程,可以灵活的替换不同的策略。在使用策略的时候,一般需要通过类型来判断创建哪个模式来使用。可以把根据 type 创建策略的逻辑抽离出来,放到工厂类中。示例代码如下所示:</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">//java 11</span></span><br><span class="line"><span class="comment">//定义策略</span></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Strategy</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">algorithmInterface</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ConcreteStrategyA</span> <span class="keyword">implements</span> <span class="title">Strategy</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">algorithmInterface</span><span class="params">()</span> </span>{<span class="comment">/*具体的算法...*/</span>}</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ConcreteStrategyB</span> <span class="keyword">implements</span> <span class="title">Strategy</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">algorithmInterface</span><span class="params">()</span> </span>{<span class="comment">/*具体的算法...*/</span>}</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="class"><span class="keyword">class</span> <span class="title">StrategyFactory</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> Strategy <span class="title">getStrategy</span><span class="params">(String type)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (type != <span class="keyword">null</span>) {</span><br><span class="line"> <span class="keyword">if</span> (type.equals(<span class="string">"A"</span>)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ConcreteStrategyA();</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (type.equals(<span class="string">"B"</span>)) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> ConcreteStrategyB();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> <span class="keyword">throw</span> <span class="keyword">new</span> IllegalArgumentException(<span class="string">"错误的类型"</span>+type);</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[]args)</span></span>{</span><br><span class="line"> <span class="comment">//使用</span></span><br><span class="line"> StrategyFactory.getStrategy(<span class="string">"A"</span>).algorithmInterface();</span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>可以看出策略模式应该由三个部分组成(定义,创建,使用)。策略类的定义比较简单,包含一个策略接口以及实现这一接口的子类。策略模式的创建则由一个工厂类来进行完成,封装了策略创建的细节。在策略模式的使用时,动态的传入类型选择需要使用哪个策略。</p><h3 id="策略模式使用场景"><a href="#策略模式使用场景" class="headerlink" title="策略模式使用场景"></a>策略模式使用场景</h3><h2 id="责任链模式"><a href="#责任链模式" class="headerlink" title="责任链模式"></a>责任链模式</h2><p>责任链模式将请求的发送和接收解藕,让多个接收对象都有机会处理这个请求。将这些对象串成一条链,并沿着这条链传递这个请求,直到链上出现能够处理的对象为止。</p><h3 id="责任链模式实现"><a href="#责任链模式实现" class="headerlink" title="责任链模式实现"></a>责任链模式实现</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//java 11</span></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">IHandler</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">boolean</span> <span class="title">handle</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HandlerA</span> <span class="keyword">implements</span> <span class="title">IHandler</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">handle</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"handleA"</span>);</span><br><span class="line"> <span class="keyword">return</span> Math.random() < <span class="number">0.5</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HandlerB</span> <span class="keyword">implements</span> <span class="title">IHandler</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">handle</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"handleB"</span>);</span><br><span class="line"> <span class="keyword">return</span> Math.random() < <span class="number">0.5</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HandlerChain</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> List<IHandler> handlers = <span class="keyword">new</span> ArrayList<>();</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">addHandler</span><span class="params">(IHandler handler)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.handlers.add(handler);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">for</span> (IHandler handler : handlers) {</span><br><span class="line"> <span class="keyword">boolean</span> handled = handler.handle();</span><br><span class="line"> <span class="keyword">if</span> (handled) {</span><br><span class="line"> <span class="keyword">break</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="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> HandlerChain chain = <span class="keyword">new</span> HandlerChain();</span><br><span class="line"> chain.addHandler(<span class="keyword">new</span> HandlerA());</span><br><span class="line"> chain.addHandler(<span class="keyword">new</span> HandlerB());</span><br><span class="line"> chain.handle();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在HandlerChain类中使用一个数组来保存所有的处理器,然后在handle()方法中依次调用handle()函数,同时判断是否需要将责任链请求继续传递下去。</p><h3 id="责任链模式使用场景"><a href="#责任链模式使用场景" class="headerlink" title="责任链模式使用场景"></a>责任链模式使用场景</h3><p>通常使用责任链模式来做一些工作例如日志记录,鉴权等等。Java中Servlet规范定义的filter以及Spring的拦截器Interceptor都是使用责任链模式来进行实现的。</p><h2 id="状态模式"><a href="#状态模式" class="headerlink" title="状态模式"></a>状态模式</h2><p>状态模式经常用在带有状态的对象中,用于实现状态机。<br>//TODO</p><h2 id="迭代器模式"><a href="#迭代器模式" class="headerlink" title="迭代器模式"></a>迭代器模式</h2><p>提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。一个完整的迭代器模式需要容器以及容器迭代器两部分的内容。在很多语言中其实已经提供了遍历容器的迭代器类,在平时使用的情况下直接使用即可。在java中使用迭代器的代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">// java 11</span></span><br><span class="line">List<Integer> list = List.of(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>, <span class="number">4</span>);</span><br><span class="line"><span class="comment">// 使用迭代器遍历</span></span><br><span class="line"><span class="keyword">for</span> (<span class="keyword">var</span> it = list.iterator(); it.hasNext(); ) {</span><br><span class="line"> System.out.println(it.next());</span><br><span class="line">}</span><br><span class="line"><span class="comment">// 简写形式</span></span><br><span class="line"><span class="keyword">for</span> (Integer n : list) {</span><br><span class="line"> System.out.println(n);</span><br><span class="line">}</span><br><span class="line"></span><br></pre></td></tr></table></figure><h3 id="实现迭代器模式"><a href="#实现迭代器模式" class="headerlink" title="实现迭代器模式"></a>实现迭代器模式</h3><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Iterator</span><<span class="title">E</span>> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">boolean</span> <span class="title">hasNext</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">next</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function">E <span class="title">current</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ArrayIterator</span><<span class="title">E</span>> <span class="keyword">implements</span> <span class="title">Iterator</span><<span class="title">E</span>> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">int</span> index = <span class="number">0</span>;</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> E[] array;</span><br><span class="line"></span><br><span class="line"> <span class="meta">@SafeVarargs</span> ArrayIterator(E... array) {</span><br><span class="line"> <span class="keyword">this</span>.array = array;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">boolean</span> <span class="title">hasNext</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> index != array.length;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">next</span><span class="params">()</span> </span>{</span><br><span class="line"> index++;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> E <span class="title">current</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> array[index];</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> ArrayIterator<Integer> iterator = <span class="keyword">new</span> ArrayIterator<>(<span class="number">1</span>, <span class="number">2</span>, <span class="number">3</span>);</span><br><span class="line"> <span class="keyword">while</span> (iterator.hasNext()) {</span><br><span class="line"> System.out.println(iterator.current());</span><br><span class="line"> iterator.next();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>为了实现迭代器模式,需要先定义一个Iterator接口,在其中定义迭代器需要实现的方法hasNext,next以及current。ArrayIterator的实现非常简单,将需要遍历的容器对象传递给迭代器类,在通常的实现中,可以在容器类之中定义一个iterator()方法来创建对应的迭代器。</p><h3 id="迭代器模式使用场景"><a href="#迭代器模式使用场景" class="headerlink" title="迭代器模式使用场景"></a>迭代器模式使用场景</h3><p>迭代器模式用于遍历容器内的内容。他可以让调用者对集合内部的数据结构一无所知,总是以相同的接口遍历各种不同类型的集合。在复杂的数据结构之中,例如在一棵树中可以定义前序,中序,后序三种迭代器,如果客户端来分别实现三种算法,会导致维护成本的上升,而将这三种算法拆分到三种迭代器中,则降低的系统的复杂度。如果客户端需要换一种遍历算法,那么只需要将前序的迭代器转变为后序的迭代器即可。</p><h2 id="访问者模式"><a href="#访问者模式" class="headerlink" title="访问者模式"></a>访问者模式</h2><p>访问者模式表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。</p><h3 id="访问者模式实现"><a href="#访问者模式实现" class="headerlink" title="访问者模式实现"></a>访问者模式实现</h3><p>以访问一个目录下所有的文件为例,如下的代码使用的访问者模式来实现该操作:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">import</span> java.io.File;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> FileStructure fs = <span class="keyword">new</span> FileStructure(<span class="keyword">new</span> File(<span class="string">"."</span>));</span><br><span class="line"> fs.handle(<span class="keyword">new</span> PrintFileNameVisitor());</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">FileVisitor</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">handleDirectory</span><span class="params">(File dir)</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">handleFile</span><span class="params">(File file)</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">PrintFileNameVisitor</span> <span class="keyword">implements</span> <span class="title">FileVisitor</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handleDirectory</span><span class="params">(File dir)</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"目录名: "</span> + dir);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handleFile</span><span class="params">(File file)</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"文件名 "</span> + file);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FileStructure</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> File path;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">FileStructure</span><span class="params">(File path)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.path = path;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(FileVisitor visitor)</span> </span>{</span><br><span class="line"> handle(<span class="keyword">this</span>.path, visitor);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">void</span> <span class="title">handle</span><span class="params">(File file, FileVisitor visitor)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (file.isDirectory()) {</span><br><span class="line"> visitor.handleDirectory(file);</span><br><span class="line"> <span class="keyword">for</span> (File sub : file.listFiles()) {</span><br><span class="line"> handle(sub, visitor);</span><br><span class="line"> }</span><br><span class="line"> } <span class="keyword">else</span> <span class="keyword">if</span> (file.isFile()) {</span><br><span class="line"> visitor.handleFile(file);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在这段代码中,先定义了一个访问者的接口FileVisitor,然后实现handleDirectory和handleFile这两个方法分别用于处理不同的文件类型的情况。最后给FileStructure这个类添加了handle方法,传入一个访问者,这样子就把访问者的行为抽象了出来。</p><h3 id="访问者模式优势"><a href="#访问者模式优势" class="headerlink" title="访问者模式优势"></a>访问者模式优势</h3><p>访问者模式可以用于解藕操作和对象本身,保持类的职责单一,满足开闭原则。</p><h2 id="备忘录模式"><a href="#备忘录模式" class="headerlink" title="备忘录模式"></a>备忘录模式</h2><p>备忘录模式可以在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。</p><h3 id="备忘录模式实现"><a href="#备忘录模式实现" class="headerlink" title="备忘录模式实现"></a>备忘录模式实现</h3><p>例如在一个文本编辑器中需要实现撤销的操作,可以用如下代码进行实现:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Text</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> StringBuilder text = <span class="keyword">new</span> StringBuilder();</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">add</span><span class="params">(String s)</span> </span>{</span><br><span class="line"> text.append(s);</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">delete</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (text.length() > <span class="number">0</span>) {</span><br><span class="line"> text.deleteCharAt(text.length() - <span class="number">1</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Snapshot <span class="title">createSnapshot</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> Snapshot(text.toString());</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">loadFromSnapshot</span><span class="params">(Snapshot snapshot)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.text.replace(<span class="number">0</span>, text.length(), snapshot.getText());</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Snapshot</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> String text;</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="title">Snapshot</span><span class="params">(String text)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.text = text;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> String <span class="title">getText</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> text;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SnapshotHolder</span> </span>{</span><br><span class="line"> <span class="comment">//最大记录数</span></span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="keyword">int</span> MAX_SIZE = <span class="number">20</span>;</span><br><span class="line"> <span class="keyword">private</span> Deque<Snapshot> snapshots = <span class="keyword">new</span> ArrayDeque<>(MAX_SIZE);</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> Snapshot <span class="title">pop</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> snapshots.removeLast();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">push</span><span class="params">(Snapshot snapshot)</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (snapshots.size() > MAX_SIZE) {</span><br><span class="line"> snapshots.removeFirst();</span><br><span class="line"> }</span><br><span class="line"> snapshots.addLast(snapshot);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="备忘录模式使用场景"><a href="#备忘录模式使用场景" class="headerlink" title="备忘录模式使用场景"></a>备忘录模式使用场景</h3><p>备忘录模式主要用来防止丢失,实现撤销以及恢复功能。</p><h2 id="命令模式"><a href="#命令模式" class="headerlink" title="命令模式"></a>命令模式</h2><p>TODO</p><h2 id="解释器模式"><a href="#解释器模式" class="headerlink" title="解释器模式"></a>解释器模式</h2><p>TODO</p><h2 id="中介模式"><a href="#中介模式" class="headerlink" title="中介模式"></a>中介模式</h2><p>TODO</p>]]></content>
<summary type="html">介绍设计模式中的行为型模式,包括观察者模式,模板模式,策略模式,责任链模式,状态模式,迭代器模式,访问者模式,备忘录模式,命令模式,解释器模式,中介模式。</summary>
<category term="设计模式" scheme="http://qian0817.top/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
<category term="设计模式" scheme="http://qian0817.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
</entry>
<entry>
<title>Redis中的数据结构</title>
<link href="http://qian0817.top/2020/09/08/Redis%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
<id>http://qian0817.top/2020/09/08/Redis%E4%B8%AD%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/</id>
<published>2020-09-08T06:00:39.000Z</published>
<updated>2020-12-30T04:35:50.426Z</updated>
<content type="html"><![CDATA[<h2 id="简单字符串SDS"><a href="#简单字符串SDS" class="headerlink" title="简单字符串SDS"></a>简单字符串SDS</h2><p>因为C语言使用的字符串无法满足Redis的需求,Redis构建了一种简单字符串来代替C字符串。在Redis的数据库中,所有包含字符串的地方都会使用到SDS来实现。</p><h3 id="SDS定义"><a href="#SDS定义" class="headerlink" title="SDS定义"></a>SDS定义</h3><p>SDS的结构如下所示:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">sdshdr</span> {</span></span><br><span class="line"> <span class="comment">// 已占用空间的长度</span></span><br><span class="line"> <span class="keyword">int</span> len;</span><br><span class="line"> <span class="comment">// 剩余可用空间的长度</span></span><br><span class="line"> <span class="keyword">int</span> <span class="built_in">free</span>;</span><br><span class="line"> <span class="comment">// 保存的字符串</span></span><br><span class="line"> <span class="keyword">char</span> buf[];</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>free属性记录了buf数组中未使用字节的数量,len属性记录了已经使用字节的数量,buf数组存储字符串。</p><p>为了重用C字符串函数库中的函数,buf字段遵循C语言中以空字符结尾的惯例,最后为空字符’/0’。</p><h3 id="SDS优点"><a href="#SDS优点" class="headerlink" title="SDS优点"></a>SDS优点</h3><p>通过SDS这一数据结构,带来了例如可以用常数复杂度获取字符串长度,杜绝了缓冲区溢出,减少内存重新分配次数,二进制安全等等好处。</p><p>其中通过未使用空间,Redis实现了空间预分配以及惰性空间释放两种策略来减少内存分配次数。</p><h4 id="空间预分配"><a href="#空间预分配" class="headerlink" title="空间预分配"></a>空间预分配</h4><p>当对一个SDS进行修改的时候,Redis不仅会分配修改所必须的空间,同时还会分配额外的未使用空间。空间的分配方法为:</p><ul><li>SDS长度小于1MB时,程序分配和len属性同样大小的未使用空间。此时len属性的值与free属性相同。</li><li>SDS长度大于1MB时,程序会固定分配1MB的未使用空间。</li></ul><p>通过空间预分配策略,减少了连续执行字符串增长所需的内存分配时间。在扩展SDS之前,会先检查未使用空间是否足够,如果足够的话,会直接使用未分配空间,而无需再次分配。</p><h4 id="惰性空间释放"><a href="#惰性空间释放" class="headerlink" title="惰性空间释放"></a>惰性空间释放</h4><p>当一个SDS需要缩短时,会先使用free属性来将这些字节记录起来。通过惰性空间释放,SDS避免了缩短字符串时所需的内存重新分配操作,并为将来的增长操作提供了优化。</p><h2 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h2><p>链表作为一种常用的数据结构,在Redis中应用十分广泛。</p><h3 id="链表结构"><a href="#链表结构" class="headerlink" title="链表结构"></a>链表结构</h3><p>链表节点的实现如下所示:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> {</span></span><br><span class="line"> <span class="comment">// 前置节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">prev</span>;</span></span><br><span class="line"> <span class="comment">// 后置节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">listNode</span> *<span class="title">next</span>;</span></span><br><span class="line"> <span class="comment">// 节点的值</span></span><br><span class="line"> <span class="keyword">void</span> *value;</span><br><span class="line">} listNode;</span><br></pre></td></tr></table></figure><p>从数据结构定义可以看出其结构是一个双端队列。list结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">list</span> {</span></span><br><span class="line"> <span class="comment">// 表头节点</span></span><br><span class="line"> listNode *head;</span><br><span class="line"> <span class="comment">// 表尾节点</span></span><br><span class="line"> listNode *tail;</span><br><span class="line"> <span class="comment">// 节点值复制函数</span></span><br><span class="line"> <span class="keyword">void</span> *(*dup)(<span class="keyword">void</span> *ptr);</span><br><span class="line"> <span class="comment">// 节点值释放函数</span></span><br><span class="line"> <span class="keyword">void</span> (*<span class="built_in">free</span>)(<span class="keyword">void</span> *ptr);</span><br><span class="line"> <span class="comment">// 节点值对比函数</span></span><br><span class="line"> <span class="keyword">int</span> (*match)(<span class="keyword">void</span> *ptr, <span class="keyword">void</span> *key);</span><br><span class="line"> <span class="comment">// 链表所包含的节点数量</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">long</span> len;</span><br><span class="line">} <span class="built_in">list</span>;</span><br></pre></td></tr></table></figure><p>同时list结构为链表提供了表头节点head,表尾节点tail,链表长度len的信息。同时在list中放了三个函数dup用于复制链表节点,free用于释放链表节点,match用于对比所保存的的值与另一个输入值是否相等。</p><h2 id="字典"><a href="#字典" class="headerlink" title="字典"></a>字典</h2><p>字典用于保存K-V键值对,在Redis中的使用十分广泛。对数据库的增删改查以及哈希键的实现就使用到了字典。</p><h3 id="字典结构"><a href="#字典结构" class="headerlink" title="字典结构"></a>字典结构</h3><p>哈希表节点的实现代码如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> {</span></span><br><span class="line"> <span class="comment">// 键</span></span><br><span class="line"> <span class="keyword">void</span> *key;</span><br><span class="line"> <span class="comment">// 值</span></span><br><span class="line"> <span class="keyword">union</span> {</span><br><span class="line"> <span class="keyword">void</span> *val;</span><br><span class="line"> <span class="keyword">uint64_t</span> u64;</span><br><span class="line"> <span class="keyword">int64_t</span> s64;</span><br><span class="line"> } v;</span><br><span class="line"> <span class="comment">// 指向下个哈希表节点,形成链表</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">dictEntry</span> *<span class="title">next</span>;</span></span><br><span class="line">} dictEntry;</span><br></pre></td></tr></table></figure><p>每个dictEntry都保存着一个键值对。其中key属性保存着键值对中的键,v属性保存着键值对中的值。因为Redis中的字典使用了链地址法来解决哈希冲突,所以需要next属性来形成链表。</p><p>哈希表的结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictht</span> {</span></span><br><span class="line"> <span class="comment">// 哈希表数组</span></span><br><span class="line"> dictEntry **table;</span><br><span class="line"> <span class="comment">// 哈希表大小</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">long</span> size;</span><br><span class="line"> <span class="comment">// 哈希表大小掩码,用于计算索引值</span></span><br><span class="line"> <span class="comment">// 总是等于 size - 1</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">long</span> sizemask;</span><br><span class="line"> <span class="comment">// 该哈希表已有节点的数量</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">long</span> used;</span><br><span class="line">} dictht;</span><br></pre></td></tr></table></figure><p>table属性是一个数组,每个元素都是一个dictEntry。size属性记录了这个哈希表的大小,也就是table的长度。sizemask的值总数等于size-1,这个属性决定了一个键值对应该被放到哪个索引中去。used属性记录了已经有多少个键值对被插入。</p><p>字典的结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dict</span> {</span></span><br><span class="line"> <span class="comment">// 类型特定函数</span></span><br><span class="line"> dictType *type;</span><br><span class="line"> <span class="comment">// 私有数据</span></span><br><span class="line"> <span class="keyword">void</span> *privdata;</span><br><span class="line"> <span class="comment">// 哈希表</span></span><br><span class="line"> dictht ht[<span class="number">2</span>];</span><br><span class="line"> <span class="comment">// rehash 索引,当不再 rehash 过程时值为-1</span></span><br><span class="line"> <span class="keyword">int</span> rehashidx;</span><br><span class="line">} dict;</span><br></pre></td></tr></table></figure><p>其中type属性是指向dictType类型的指针,在其中保存了操作特定类型键值对的操作。</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">dictType</span> {</span></span><br><span class="line"> <span class="comment">// 计算哈希值的函数</span></span><br><span class="line"> <span class="function"><span class="keyword">unsigned</span> <span class="title">int</span> <span class="params">(*hashFunction)</span><span class="params">(<span class="keyword">const</span> <span class="keyword">void</span> *key)</span></span>;</span><br><span class="line"> <span class="comment">// 复制键的函数</span></span><br><span class="line"> <span class="keyword">void</span> *(*keyDup)(<span class="keyword">void</span> *privdata, <span class="keyword">const</span> <span class="keyword">void</span> *key);</span><br><span class="line"> <span class="comment">// 复制值的函数</span></span><br><span class="line"> <span class="keyword">void</span> *(*valDup)(<span class="keyword">void</span> *privdata, <span class="keyword">const</span> <span class="keyword">void</span> *obj);</span><br><span class="line"> <span class="comment">// 对比键的函数</span></span><br><span class="line"> <span class="keyword">int</span> (*keyCompare)(<span class="keyword">void</span> *privdata, <span class="keyword">const</span> <span class="keyword">void</span> *key1, <span class="keyword">const</span> <span class="keyword">void</span> *key2);</span><br><span class="line"> <span class="comment">// 销毁键的函数</span></span><br><span class="line"> <span class="keyword">void</span> (*keyDestructor)(<span class="keyword">void</span> *privdata, <span class="keyword">void</span> *key);</span><br><span class="line"> <span class="comment">// 销毁值的函数</span></span><br><span class="line"> <span class="keyword">void</span> (*valDestructor)(<span class="keyword">void</span> *privdata, <span class="keyword">void</span> *obj);</span><br><span class="line">} dictType;</span><br></pre></td></tr></table></figure><p>privdata属性保存了需要传给那些特定类型的可选参数。rehashidx记录了rehash目前的进度,如果没有进行rehash,那么值为-1。ht属性是包含两个项的数组,当没有进行rehash时,字典只使用ht[0]哈希表,ht[1]只会在rehash过程中使用。</p><h3 id="哈希算法"><a href="#哈希算法" class="headerlink" title="哈希算法"></a>哈希算法</h3><p>Redis中使用MurmurHash算法来计算键的哈希值。这种算法的优势在于在提供很好的随机分布性的同时计算速度也十分的快。</p><h3 id="渐进式rehash"><a href="#渐进式rehash" class="headerlink" title="渐进式rehash"></a>渐进式rehash</h3><p>当出现以下情况时,会进行rehash操作:</p><ul><li><p>服务器没有执行BGSAVE命令或者BGREWRITEAOF命令,负载因子大于1</p></li><li><p>服务器执行BGSAVE命令或者BGREWRITEAOF命令,负载因子大于5</p></li><li><p>负载因子小于0.1</p></li></ul><p>为了避免rehash对Redis的性能造成影响,Redis实际上会分多次将ht[0]中的数据转移到ht[1]之中,具体的过程如下:</p><ol><li><p>为ht[1]分配空间,设置rehashidx属性的值为0。</p></li><li><p>每当对字典进行操作的时候,程序在执行指定操作的同时,会将在rehashidx索引上的键值对rehash到ht[1]之中。当rehash工作完成时,rehashidx属性加一。</p></li><li><p>当ht[0]中所有键值对被rehash到ht[1]中时,将rehashidx设置为-1,结束rehash。</p></li></ol><p>在rehash过程中,如果需要进行增删改查等操作的时候,会先在ht[0]中进行查找,如果没找到则进入ht[1]中进行查找。同时新添加的键值对会被记录到ht[1]之中。</p><h2 id="跳跃表"><a href="#跳跃表" class="headerlink" title="跳跃表"></a>跳跃表</h2><p>跳跃表是一种有序的数据结构,能够做到快速的访问节点。在大部分情况下,跳表的速率与平衡树相差不大,但是实现更为简单,因此Redis中使用了跳表来代替平衡树。Redis使用跳表来作为有序集合键。</p><h3 id="跳表结构"><a href="#跳表结构" class="headerlink" title="跳表结构"></a>跳表结构</h3><p>跳表节点的定义如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> {</span></span><br><span class="line"> <span class="comment">// 成员对象</span></span><br><span class="line"> robj *obj;</span><br><span class="line"> <span class="comment">// 分值</span></span><br><span class="line"> <span class="keyword">double</span> score;</span><br><span class="line"> <span class="comment">// 后退指针</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">backward</span>;</span></span><br><span class="line"> <span class="comment">// 层</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistLevel</span> {</span></span><br><span class="line"> <span class="comment">// 前进指针</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">forward</span>;</span></span><br><span class="line"> <span class="comment">// 跨度</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">int</span> span;</span><br><span class="line"> } level[];</span><br><span class="line">} zskiplistNode;</span><br></pre></td></tr></table></figure><p>其中obj成员是一个指针指向一个Redis对象。score属性代表该节点的分值,跳跃表中的节点都会按照该分值由大到小进行排列。在跳跃表中,成员对象obj是唯一的但是score可以相同。如果出现分值相同的节点,那么Redis会按照成员对象的字典序大小进行排序。后退指针backward用于从表尾向表头方向对成员进行访问。</p><p>层属性level是一个数组,其中每个元素可以包含前进指针和跨度。前进指针用于从表头向表尾进行访问。跨度用于记录两个节点之间的距离,在查找时跨度用来计算排位(Rank)。</p><p>跳跃表的结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplist</span> {</span></span><br><span class="line"> <span class="comment">// 表头节点和表尾节点</span></span><br><span class="line"> <span class="class"><span class="keyword">struct</span> <span class="title">zskiplistNode</span> *<span class="title">header</span>, *<span class="title">tail</span>;</span></span><br><span class="line"> <span class="comment">// 表中节点的数量</span></span><br><span class="line"> <span class="keyword">unsigned</span> <span class="keyword">long</span> length;</span><br><span class="line"> <span class="comment">// 表中层数最大的节点的层数</span></span><br><span class="line"> <span class="keyword">int</span> level;</span><br><span class="line">} zskiplist;</span><br></pre></td></tr></table></figure><p>其中header和tail分别表示跳跃表的表头和表尾节点。level记录了目前跳跃表内层次最大的那个节点的层数,其值大于1小于32。length属性记录了跳跃表的长度。</p><h2 id="整数集合"><a href="#整数集合" class="headerlink" title="整数集合"></a>整数集合</h2><p>整数集合用于实现集合键。当一个集合只包含有整数值且数量不多时,那么会使用整数集合作为集合的底层实现。其可以保存int_16t,int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。</p><h3 id="整数集合实现"><a href="#整数集合实现" class="headerlink" title="整数集合实现"></a>整数集合实现</h3><p>整数集合的结构如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">typedef</span> <span class="class"><span class="keyword">struct</span> <span class="title">intset</span> {</span></span><br><span class="line"> <span class="comment">// 编码方式</span></span><br><span class="line"> <span class="keyword">uint32_t</span> encoding;</span><br><span class="line"> <span class="comment">// 集合包含的元素数量</span></span><br><span class="line"> <span class="keyword">uint32_t</span> length;</span><br><span class="line"> <span class="comment">// 保存元素的数组</span></span><br><span class="line"> <span class="keyword">int8_t</span> contents[];</span><br><span class="line">} intset;</span><br></pre></td></tr></table></figure><p>encoding表示整数的编码方式。虽然将contents数组声明为int8_t类型,但是contents数组的真正类型取决于encoding类型。其中encoding类型可以为INTSET_ENC_INT16,INTSET_ENC_INT32,INTSET_ENC_INT64。length表示所包含的元素个数。contents是用于保存元素的,其值会按照由小到大的顺序进行排列。</p><h3 id="整数集合升级"><a href="#整数集合升级" class="headerlink" title="整数集合升级"></a>整数集合升级</h3><p>每当一个新的元素添加到整数集合之中时,如果新元素的类型比现有类型要长时,那么需要进行升级,才能将新元素添加到整数集合中去。升级整数集合的步骤如下:</p><ol><li>根据新元素的类型扩展数组的空间大小,分配对应的空间。</li><li>将原数组之中所有的元素的类型进行转换,将转换后的元素放到正确的位置上去。同时在放置的过程中,需要确保有序性质不变。</li><li>将新的元素添加到整数集合中去。</li></ol><p>整数集合的升级是为了提升集合的灵活性以及节约内存的需要。</p><h2 id="压缩列表"><a href="#压缩列表" class="headerlink" title="压缩列表"></a>压缩列表</h2><p>压缩列表用于实现列表键和哈希键。如果一个列表键中只包含少量的元素并且每个元素是小整数或者短字符串,会选择使用压缩列表来作为列表键的底层实现。</p><h3 id="压缩列表实现"><a href="#压缩列表实现" class="headerlink" title="压缩列表实现"></a>压缩列表实现</h3><p>压缩列表由以下部分构成。</p><table><thead><tr><th>属性</th><th>类型</th><th>长度</th><th>用途</th></tr></thead><tbody><tr><td>zlbytes</td><td>uint32_t</td><td>4字节</td><td>记录整个压缩列表占用的内存字节数</td></tr><tr><td>zltail</td><td>uint32_t</td><td>4字节</td><td>记录表尾节点距离压缩列表的起始位置有多远</td></tr><tr><td>zllen</td><td>uint16_t</td><td>2字节</td><td>压缩列表包含的节点个数</td></tr><tr><td>entryX</td><td>列表节点</td><td></td><td>压缩列表包含的各个节点</td></tr><tr><td>zlend</td><td>uint8_t</td><td>1字节</td><td>用于标记压缩列表的末端</td></tr></tbody></table><p>其中每个压缩列表节点可以保存一个字节数组或者整数值。每个压缩列表节点由以下部分构成。</p><table><thead><tr><th>属性</th><th>长度</th><th>作用</th></tr></thead><tbody><tr><td>previous_entry_length</td><td>1字节或5字节</td><td>记录前一个节点的长度</td></tr><tr><td>encoding</td><td>1字节或2字节或5字节</td><td>记录content属性所保存的数据类型及长度</td></tr><tr><td>content</td><td></td><td>保存节点的值</td></tr></tbody></table><p>对于previos_entry_length属性,如果前一个节点的长度小于254则占一个字节,否则占用五个字节,并且第一个字节会被设置为254,后四个节点则用于保存前一个节点的长度。通过previos_entry_length属性,程序可以通过指针运算计算出前一个节点的起始位置,实现从表尾到表头的遍历过程。</p><p>对于encoding属性,其记录了所保存数据的类型的以及长度。</p><p>如果编码最高位为00,01或者10,说明所保存的类型是字节数组。数组的长度由编码除去最高两位之后的记录。如果最高位为00,那么编码长度为1,储存的是小于等于63字节的字节数组。如果最高位为01,那么编码长度为2,储存的是小于等于16383字节的字节数组。如果最高位为10,那么编码长度为5,储存的是小于等于4294967295字节的字节数组。</p><p>如果最高位以11开头,那么说明保存的是整数值。保存的整数值类型与长度由除去最高两位的其他位记录。整数编码的方式如下:</p><table><thead><tr><th>编码</th><th>编码长度</th><th>保存的属性类型</th></tr></thead><tbody><tr><td>11000000</td><td>1字节</td><td>int16_t类型整数</td></tr><tr><td>11010000</td><td>1字节</td><td>int32_t类型整数</td></tr><tr><td>11100000</td><td>1字节</td><td>int64_t类型整数</td></tr><tr><td>11110000</td><td>1字节</td><td>24位有符号整数</td></tr><tr><td>11111110</td><td>1字节</td><td>8位有符号整数</td></tr><tr><td>1111xxxx</td><td>1字节</td><td>xxxx就是保存的值,无需content</td></tr></tbody></table><h3 id="连锁更新"><a href="#连锁更新" class="headerlink" title="连锁更新"></a>连锁更新</h3><p>previos_entry_length属性记录了前一个节点的长度。在一些特殊的情况下,例如在一个压缩列表中存在连续多个长度介于250-253字节的节点e1-eN。因为这些节点的长度都小于254字节,记录这些长度的previos_entry_length只需要1字节。但是如果在e1之前新插入一个长度大于254字节的节点,会导致e1节点的previos_entry_length属性需要扩容到5个字节,但是以此类推会导致e1-eN的所有节点都会需要进行扩容。这种连续多次空间扩展操作被称为连锁更新。</p><p>当出现连锁更新的时候,最坏情况下需要对压缩列表进行N次重新分配空间操作,每次空间分配的最坏时间复杂度为O(N),最终导致连锁更新的时间复杂度为O(N<sup>2</sup>)。虽然时间复杂度较高,但是因为在实际中这种情况不多见,即使出现如果被更新的节点不多,也不会对性能产生影响。</p>]]></content>
<summary type="html">介绍在Redis实现时采用的数据结构</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
<category term="数据结构" scheme="http://qian0817.top/tags/%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84/"/>
</entry>
<entry>
<title>Redis中的持久化机制</title>
<link href="http://qian0817.top/2020/09/05/Redis%E4%B8%AD%E7%9A%84%E6%8C%81%E4%B9%85%E5%8C%96%E6%9C%BA%E5%88%B6/"/>
<id>http://qian0817.top/2020/09/05/Redis%E4%B8%AD%E7%9A%84%E6%8C%81%E4%B9%85%E5%8C%96%E6%9C%BA%E5%88%B6/</id>
<published>2020-09-05T09:44:02.000Z</published>
<updated>2020-12-30T04:35:50.447Z</updated>
<content type="html"><![CDATA[<p>Redis的数据全部存放在内存之中,如果一个服务器出现了宕机,那么内存中的数据将会全部丢失。为了解决这一个问题,需要用到Redis的持久化机制。Redis的持久化主要有两大机制,即AOF日志和RDB快照。</p><h2 id="AOF日志"><a href="#AOF日志" class="headerlink" title="AOF日志"></a>AOF日志</h2><p>Redis提供了AOF日志来实现持久化的功能,AOF是通过记录保存到redis中的命令来记录数据库状态的。</p><h3 id="AOF日志格式"><a href="#AOF日志格式" class="headerlink" title="AOF日志格式"></a>AOF日志格式</h3><p>在AOF日志中记录了Redis收到的每一条命令,这些命令使用文本形式进行保存。例如当Redis收到set testKey testValue 这样的命令之后,会在AOF日志中记录如下的内容:</p><figure class="highlight text"><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">*3</span><br><span class="line">$3</span><br><span class="line">set</span><br><span class="line">$7</span><br><span class="line">testKey</span><br><span class="line">$9</span><br><span class="line">testValue</span><br></pre></td></tr></table></figure><p>其中*3代表当前命令中有三个部分,每个部分以$开头,后面的数字代表该部分命令有多少字节,下一行代表该部分具体的命令。</p><p>Redis在向AOF里面记录日志的时候,不会先去对这些命令进行语法检查,因此Redis采用了写后才记录日志的方式,避免记录了错误的日志被记录下来。</p><h3 id="AOF日志的实现"><a href="#AOF日志的实现" class="headerlink" title="AOF日志的实现"></a>AOF日志的实现</h3><p>AOF日志的实现可分为命令追加,文件写入以及文件同步。</p><h4 id="命令追加"><a href="#命令追加" class="headerlink" title="命令追加"></a>命令追加</h4><p>在服务器状态redisServer结构体中,有aof_buf字段。当需要记录AOF日志的时候,会将被执行的命令追加到服务器状态中的aof_buf缓冲区的末尾。</p><h4 id="文件写入与同步"><a href="#文件写入与同步" class="headerlink" title="文件写入与同步"></a>文件写入与同步</h4><p>在redis结束每一个时间循环之前,会调用flushAppendOnlyFile函数,判断是否需要将aof_buf缓冲区中的保存到AOF文件之中。Redis为AOF日志提供了三种写回策略,可以通过appendfsync选项来设置,其值分别为Always,No,Everysec。</p><p>其中Always策略可以做到基本不丢数据,但是在每一个写命令后都必须保存到磁盘上,会导致性能的降低。</p><p>No的写回时机由操作系统控制,在写到缓冲区之后,就可以继续执行后续的命令。但是何时保存到磁盘的时间是不确定的,一旦出现宕机,那么可能会有大量的数据丢失。</p><p>Everysec采用每秒钟写一次的策略,在Always和No策略之间进行了折中。这也是Redis默认的AOF策略。</p><table><thead><tr><th>配置项</th><th>写回时机</th><th>优点</th><th>缺点</th></tr></thead><tbody><tr><td>Always</td><td>同步写回</td><td>可靠性高,数据基本不会丢失</td><td>性能影响大</td></tr><tr><td>No</td><td>操作系统控制的写回</td><td>性能高</td><td>丢失数据多</td></tr><tr><td>Everysec</td><td>每秒写回</td><td>性能适中</td><td>会丢失1秒内的数据</td></tr></tbody></table><h4 id="AOF重写"><a href="#AOF重写" class="headerlink" title="AOF重写"></a>AOF重写</h4><p>随着AOF越来越大,会出现AOF文件过大而导致的问题。例如操作系统无法保存过大的文件,通过在追加记录的时候也会导致速率变慢。如果出现宕机,会导致通过AOF日志恢复过程十分缓慢。为了解决这些问题,就需要使用到AOF的重写机制。</p><p>AOF重写机制就是在重写时,将根据数据库的现状来创建一个新的AOF日志。重写机制将旧日志文件中的多条命令,在重写后的新日志变成了一条新的命令。例如以下六条命令,在经历AOF重写之后变成了一条指令。</p><figure class="highlight text"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line"># 重写前</span><br><span class="line">RPUSH list "A" "B"</span><br><span class="line">RPUSH list "C"</span><br><span class="line">RPUSH list "D" "E"</span><br><span class="line">LPOP list</span><br><span class="line">LPOP list</span><br><span class="line">RPUSH list "F" "G"</span><br><span class="line"></span><br><span class="line"># 重写后</span><br><span class="line">RPUSH list "C" "D" "E" "F" "G"</span><br></pre></td></tr></table></figure><p>在实际实现之中,因为避免在重写过程中造成客户端的缓冲区溢出,会检查键所包含的元素数量。如果元素的数量超过了一定的值,那么会使用多条命令记录键的值,而不是单单使用一条命令。</p><p>但是AOF重写是一个非常耗时的操作,为了避免阻塞主线程,AOF重写需要由子进程来完成的。但是在AOF重写的过程中,也需要继续处理命令请求,而新的命令请求可能导致当前数据库状态与重写后的数据库状态不一致。</p><p>为了解决这一问题,Redis设置了一个AOF重写缓冲区。在Redis处理玩命令以后,会将这些命令同时发送到AOF缓冲区和AOF重写缓冲区。</p><p>在子进程完成AOF重写工作以后,会向父进程发送一个信号。父进程会在收到该信号以后,将重写缓冲区中的内容写到新的AOF文件之中,同时原子的对新的AOF文件进行改名,完成新旧文件的替换。</p><h2 id="RDB持久化"><a href="#RDB持久化" class="headerlink" title="RDB持久化"></a>RDB持久化</h2><p>单独使用AOF日志在进行故障恢复的时候需要逐一将操作日志执行一遍。如果恢复的量比较大,会导致恢复速度缓慢,此时可以使用RDB内存快照的机制。RDB记录的是某一时刻的数据而不是像AOF日志一样记录操作,在进行数据恢复的时候,可以直接将RDB的数据导入Redis中,很快速的完成数据的恢复。</p><h3 id="RDB文件结构"><a href="#RDB文件结构" class="headerlink" title="RDB文件结构"></a>RDB文件结构</h3><h4 id="REDIS字符"><a href="#REDIS字符" class="headerlink" title="REDIS字符"></a>REDIS字符</h4><p>一个RDB文件最开头是REDIS五个字符,在载入RDB文件的时候,可以通过这五个字符来快速的判定是否为RDB文件。</p><h4 id="DB-VERSION"><a href="#DB-VERSION" class="headerlink" title="DB_VERSION"></a>DB_VERSION</h4><p>在REDIS字符之后是一个db_version字段,长度为4字节,记录了RDB文件的版本号。</p><h4 id="DATABASES"><a href="#DATABASES" class="headerlink" title="DATABASES"></a>DATABASES</h4><p>之后是databases部分,包含着任意个数据库中的键值对数据。如果服务器的数据库状态都为空,那么这个部分也为空。同时每一个数据库的保存内容也可以被分为SELECTDB,db_number,key_value_pairs三个部分。</p><p>SELECTDB是一个常量,当读入这个值说明接下来要读入的是一个数据库号码。</p><p>db_number保存着一个数据库号码,根据号码的不同,长度可以为1,2,5字节。当读到db_number字段的时候,会进行数据库切换。</p><p>key_value_pairs部分保存着数据库中所有键值对数据。其由EXPIRETIME_MS,ms,TYPE,key,value五个部分组成。其中EXPIRETIME_MS和ms仅仅在带有过期时间的键值对中出现。</p><p>EXPIRETIME_MS是一个常量字段,告诉读入程序接下来是一个过期时间字段。</p><p>ms是一个8字节的整数,以毫秒为单位的UNIX时间戳,记录着键值对的过期时间。</p><p>Type字段记录了value的类型,长度为1个字节,代表这当前value数据的类型。</p><p>key是一个字符串对象,总是以REDIS_RDB_TYPE_STRING的形式保存。</p><p>value根据TYPE的值不同,Redis会根据不同的类型来进行不同方式的保存。</p><h4 id="EOF"><a href="#EOF" class="headerlink" title="EOF"></a>EOF</h4><p>EOF字段代表着RDB文件的正文正式结束。当REDIS读入RDB文件的时候,如果读到该字段,说明所有数据库的键值对已经载入完毕了。</p><h4 id="CHECK-SUM"><a href="#CHECK-SUM" class="headerlink" title="CHECK_SUM"></a>CHECK_SUM</h4><p>最后是一个部分是check_sum,通过对前面四个部分通过CRC64算法计算得出。在载入RDB文件的时候,服务器会通过该字段来判断是否出现了文件损坏。</p><h3 id="RDB实现"><a href="#RDB实现" class="headerlink" title="RDB实现"></a>RDB实现</h3><p>Redis提供了save和bgsave命令来生成RDB文件,其中save命令在主线程执行RDB的操作,会阻塞主线程的执行,而bgsave会创建一个子进程,专门用于写入新的RDB文件,避免主线程的阻塞。</p><p>在执行RDB记录的时候,Redis会使用操作系统提供的写时复制技术(COW)来实现在生成RDB文件的同时可以正常的处理请求。如果主线程对Redis中的数据执行的是读操作,那么主线程与子线程互不影响。如果主线程时写操作,那么该数据会被复制一份,生成该数据的副本,提供给子线程,子线程会把这个副本的数据写入到RDB文件之中,而在这个过程中,主线程仍然可以修改原来的数据。这一机制既保证了数据的完整性,同时允许主线程进行数据修改。</p><h3 id="RDB自动保存"><a href="#RDB自动保存" class="headerlink" title="RDB自动保存"></a>RDB自动保存</h3><p>在Redis服务器启动的时候,可以通过传递配置文件等方式设置save选项。根据设置的条件,Redis会设置服务器状态中的saveParams属性,其类型为saveparam的数组。saveparam结构体有两个属性,表示了在多少秒内发生了多少次修改会触发RDB保存,saveparam的源码如下:</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"><span class="class"><span class="keyword">struct</span> <span class="title">saveparam</span> {</span></span><br><span class="line"> <span class="comment">// 多少秒之内</span></span><br><span class="line"> <span class="keyword">time_t</span> seconds;</span><br><span class="line"> <span class="comment">// 发生多少次修改</span></span><br><span class="line"> <span class="keyword">int</span> changes;</span><br><span class="line">};</span><br></pre></td></tr></table></figure><p>同时在服务器状态中还维护dirty计数器以及leavesave属性。dirty属性记录了距离上次RDB保存之后还执行了多少次修改,leavesave属性记录了上次保存的时间。在serverCron函数中会遍历saveparam,检查save的条件是否满足,一旦有一个条件满足,那么就会自动执行bgsave命令。</p><h2 id="RDB与AOF混合使用"><a href="#RDB与AOF混合使用" class="headerlink" title="RDB与AOF混合使用"></a>RDB与AOF混合使用</h2><p>Redis中可以将了混合使用AOF和RDB的方法。RDB可以通过混合使用AOF日志来实现增量的更新。在两次RDB操作之间,使用AOF日志来记录这之间所有的命令操作。通过这样的方法,在完成之后清空AOF日志中的内容。通过这种方式,RDB不需要很频繁的执行,同时减少了AOF日志的文件大小。</p>]]></content>
<summary type="html">介绍Redis中的AOF和RDB机制。</summary>
<category term="Redis" scheme="http://qian0817.top/categories/Redis/"/>
<category term="日志" scheme="http://qian0817.top/tags/%E6%97%A5%E5%BF%97/"/>
<category term="Redis" scheme="http://qian0817.top/tags/Redis/"/>
<category term="持久化" scheme="http://qian0817.top/tags/%E6%8C%81%E4%B9%85%E5%8C%96/"/>
</entry>
<entry>
<title>设计模式之结构型模式</title>
<link href="http://qian0817.top/2020/09/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/"/>
<id>http://qian0817.top/2020/09/04/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%E4%B9%8B%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/</id>
<published>2020-09-04T02:58:31.000Z</published>
<updated>2020-12-30T04:35:50.359Z</updated>
<content type="html"><![CDATA[<p>结构型模式主要总结了一些类或对象组合在一起的经典结构,这些经典的结构可以解决特定应用场景的问题。</p><h2 id="代理模式"><a href="#代理模式" class="headerlink" title="代理模式"></a>代理模式</h2><p>代理模式在不改变原始类的情况下,通过添加代理类来给原始类添加附加功能。代理模式经常用在比如监控,统计,鉴权这样的非业务逻辑之中。同时RPC框架也可以看成一种代理模式,被称为远程代理,在使用RPC的时候,不需要考虑与服务器交互的细节,只关注业务逻辑即可。</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><span class="line">36</span><br><span class="line">37</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//java 11</span></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ANeedProxy</span> <span class="keyword">implements</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="comment">//TODO</span></span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">AProxy</span> <span class="keyword">implements</span> <span class="title">A</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> A a;</span><br><span class="line"></span><br><span class="line"> AProxy(A a) {</span><br><span class="line"> <span class="keyword">this</span>.a = a;</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="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">if</span> (checkPermission()) {</span><br><span class="line"> <span class="keyword">this</span>.a.doSomething();</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> SecurityException(<span class="string">"Forbidden"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">checkPermission</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> Math.random() < <span class="number">0.5</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> AProxy proxy = <span class="keyword">new</span> AProxy(<span class="keyword">new</span> ANeedProxy());</span><br><span class="line"> proxy.doSomething();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在上面代码之中,代理类AProxy和原始类B都实现了A接口,在使用的时候将B对象使用构造函数传入AProxy中,然后调用原始类来执行代码,同时在调用代码之前检查相关的权限信息。</p><h3 id="动态代理"><a href="#动态代理" class="headerlink" title="动态代理"></a>动态代理</h3><p>在刚才实现的代理模式中,需要将原始类中的所有方法都重新实现一遍。同时如果有多个类需要使用代理,那么就需要写多个代理类,会导致类的个数不必要的增多,可以使用动态代理来解决这一个问题。Java本身就已经提供了动态代理的语法,使用jdk动态代理实现的代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//java 11</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ADynamicProxyHandler</span> <span class="keyword">implements</span> <span class="title">InvocationHandler</span> </span>{</span><br><span class="line"> <span class="keyword">private</span> <span class="keyword">final</span> Object proxiedObject;</span><br><span class="line"></span><br><span class="line"> ADynamicProxyHandler(Object proxiedObject) {</span><br><span class="line"> <span class="keyword">this</span>.proxiedObject = proxiedObject;</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="function"><span class="keyword">public</span> Object <span class="title">invoke</span><span class="params">(Object proxy, Method method, Object[] args)</span> <span class="keyword">throws</span> Throwable </span>{</span><br><span class="line"> <span class="keyword">if</span> (checkPermission()) {</span><br><span class="line"> <span class="keyword">return</span> method.invoke(proxiedObject, args);</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> SecurityException(<span class="string">"Forbidden"</span>);</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">boolean</span> <span class="title">checkPermission</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> Math.random() < <span class="number">0.5</span>;</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> B b = <span class="keyword">new</span> B();</span><br><span class="line"> ADynamicProxyHandler handler = <span class="keyword">new</span> ADynamicProxyHandler(b);</span><br><span class="line"> A a = (A) Proxy.newProxyInstance(b.getClass().getClassLoader(), b.getClass().getInterfaces(), handler);</span><br><span class="line"> a.doSomething();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>动态代理十分有用,可以帮助在编写代码时减少模板代码,减少维护和开发的成本,Spring AOP的底层实现原理就是基于这样的动态代理的。</p><h2 id="桥接模式"><a href="#桥接模式" class="headerlink" title="桥接模式"></a>桥接模式</h2><p>桥接模式是将抽象部分与它的实现部分分离,使它们都可以独立地变化,其思路是不要过度使用继承,而是优先拆分部分组件,通过组合的方式来进行扩展。桥接模式的目的是为了避免直接继承带来的子类爆炸。</p><p>例如如果对于图形类而言,有是什么形状这一维度,也有颜色这一维度,那么就可以使用桥接模式来编写代码。具体代码如下:</p><figure class="highlight kotlin"><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">//kotlin 1.4.0</span></span><br><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">Color</span> <span class="keyword">protected</span> <span class="keyword">constructor</span></span>(<span class="keyword">protected</span> <span class="keyword">val</span> name: String) {</span><br><span class="line"> <span class="keyword">abstract</span> <span class="function"><span class="keyword">fun</span> <span class="title">bepaint</span><span class="params">(shape: <span class="type">String</span>)</span></span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Grey</span> : <span class="type">Color</span></span>(<span class="string">"灰色"</span>) {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">bepaint</span><span class="params">(shape: <span class="type">String</span>)</span></span> = println(<span class="string">"灰色的<span class="variable">$shape</span>"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Green</span> : <span class="type">Color</span></span>(<span class="string">"绿色"</span>) {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">bepaint</span><span class="params">(shape: <span class="type">String</span>)</span></span> = println(<span class="string">"绿色的<span class="variable">$shape</span>"</span>);</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">Shape</span> <span class="keyword">protected</span> <span class="keyword">constructor</span></span>(<span class="keyword">protected</span> <span class="keyword">val</span> color: Color) {</span><br><span class="line"> <span class="keyword">abstract</span> <span class="function"><span class="keyword">fun</span> <span class="title">draw</span><span class="params">()</span></span></span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Circle</span></span>(color: Color) : Shape(color) {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">draw</span><span class="params">()</span></span> = color.bepaint(<span class="string">"正方形"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Rectangle</span></span>(color: Color) : Shape(color) {</span><br><span class="line"> <span class="keyword">override</span> <span class="function"><span class="keyword">fun</span> <span class="title">draw</span><span class="params">()</span></span> = color.bepaint(<span class="string">"长方形"</span>)</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">fun</span> <span class="title">main</span><span class="params">()</span></span> {</span><br><span class="line"> <span class="keyword">val</span> white = Grey()</span><br><span class="line"> <span class="keyword">val</span> rectangle = Rectangle(white)</span><br><span class="line"> rectangle.draw()</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在实际使用中,如果出现一个类出现多个可以单个可以单独变化的维度,那么可以使用桥接模式进行设计。</p><h2 id="装饰器模式"><a href="#装饰器模式" class="headerlink" title="装饰器模式"></a>装饰器模式</h2><p>装饰器模式能够在运行器动态的给某个对象增加功能,同时将核心功能与附加功能分开。Java标准库的IO库就是装饰器模式的经典案例。</p><p>例如我们需要给FileInputStream添加缓存功能以及GZIP解压缩功能,那么实现的代码如下:</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="comment">//java 11</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Decorator</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> <span class="keyword">throws</span> IOException </span>{</span><br><span class="line"> InputStream in = <span class="keyword">new</span> GZIPInputStream( <span class="comment">//提供解压缩</span></span><br><span class="line"> <span class="keyword">new</span> BufferedInputStream( <span class="comment">//提供缓存</span></span><br><span class="line"> <span class="keyword">new</span> FileInputStream(<span class="string">"a.txt"</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>这样的代码就是典型的装饰器模式的代码。通过查看源码可以看到GZIPInputStream,BufferedInputStream,FileInputStream都是InputStream的子类。在这个例子中,核心功能指的就是FileInputStream这个真正读取数据的源头,附加工作指的是缓冲和压缩这两个功能。如果我们仍然需要新增附加功能,就可以继承InputStream独立的进行扩展。</p><p>在装饰器模式中,装饰器类和原始类都继承自同样的父类,装饰器模式有个特点就是可以对原始类嵌套使用多个装饰器。</p><h2 id="适配器模式"><a href="#适配器模式" class="headerlink" title="适配器模式"></a>适配器模式</h2><p>适配器模式用于将不兼容的接口转换为可兼容的接口,让原本不能一起工作的类一起工作。适配器模式可以通过继承以及组合的方式进行实现,通过继承来实现适配器模式的代码如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//java 11</span></span><br><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">ITarget</span></span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">fa</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Adaptee</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">f1</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Adaptor</span> <span class="keyword">extends</span> <span class="title">Adaptee</span> <span class="keyword">implements</span> <span class="title">ITarget</span></span>{</span><br><span class="line"> <span class="meta">@Override</span></span><br><span class="line"> <span class="function"><span class="keyword">void</span> <span class="title">fa</span><span class="params">()</span></span>{</span><br><span class="line"> <span class="keyword">super</span>.f1();</span><br><span class="line"> } </span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>在这段代码之中,ITarget表示要转化成的接口的定义,Adaptee是一组不兼容的ITarget接口的类,Adaptor将Adaptee转化成为符合ITarget接口定义的类。</p><p>在实际应用之中,适配器模式可以看作一种补救设计上缺陷的方法。例如Java中有很多日志框架例如log4j,logback,大部分的日志都提供了相似的功能但没有实现统一的接口。Slf4j提供了打印日志的统一接口规范,像log4j这样的日志框架需要将接口改为符合slf4j的接口规范,就使用到了适配器模式。 </p><h2 id="门面模式"><a href="#门面模式" class="headerlink" title="门面模式"></a>门面模式</h2><p>门面模式为子系统提供一组统一的接口,定义一组高层接口让子系统更易用。例如一个系统A,提供了a、b、c、d 四个接口。系统B需要调用a、b、d三个接口,利用门面模式,可以提供一个包含a、b、d三个接口调用的门面接口x,供B系统使用,这就是门面模式。</p><p>门面模式不仅让子系统更加的易用,有时还可以解决性能上的问题。例如客户端APP之前可能需要使用三次调用服务器端的接口才能获取到想要的数据,而使用门面模式之后,就可以只发送一次请求,提升了响应速度。</p><h2 id="组合模式"><a href="#组合模式" class="headerlink" title="组合模式"></a>组合模式</h2><p>组合模式将对象组织成为树形的结构,表示部分与整体之间的层次结构。例如对于文件系统,可以将文件和目录进行区分,定义成为File和Directory两个类,在下面的代码中实现了打印当前目录下递归打印文件的功能,就使用了组合模式。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//java 11</span></span><br><span class="line"><span class="keyword">abstract</span> <span class="class"><span class="keyword">class</span> <span class="title">FileNode</span> </span>{</span><br><span class="line"> <span class="keyword">protected</span> <span class="keyword">final</span> String path;</span><br><span class="line"> <span class="function"><span class="keyword">protected</span> <span class="title">FileNode</span><span class="params">(String path)</span> </span>{</span><br><span class="line"> <span class="keyword">this</span>.path = path;</span><br><span class="line"> }</span><br><span class="line"> <span class="function"><span class="keyword">abstract</span> List<FileNode> <span class="title">list</span><span class="params">()</span></span>;</span><br><span class="line"> <span class="function"><span class="keyword">abstract</span> String <span class="title">toTreeString</span><span class="params">()</span></span>;</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">File</span> <span class="keyword">extends</span> <span class="title">FileNode</span> </span>{</span><br><span class="line"> File(String path) {</span><br><span class="line"> <span class="keyword">super</span>(path);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span> <span class="function">List<FileNode> <span class="title">list</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> Collections.emptyList();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="meta">@Override</span> <span class="function">String <span class="title">toTreeString</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">super</span>.path + <span class="string">"\n"</span>;</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Directory</span> <span class="keyword">extends</span> <span class="title">FileNode</span> </span>{</span><br><span class="line"> Directory(String path) {</span><br><span class="line"> <span class="keyword">super</span>(path);</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span> <span class="function">List<FileNode> <span class="title">list</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">var</span> file = <span class="keyword">new</span> java.io.File(<span class="keyword">super</span>.path);</span><br><span class="line"> <span class="keyword">return</span> Arrays.stream(file.listFiles()).map(f -> {</span><br><span class="line"> <span class="keyword">if</span> (f.isFile()) {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> File(f.getPath());</span><br><span class="line"> } <span class="keyword">else</span> {</span><br><span class="line"> <span class="keyword">return</span> <span class="keyword">new</span> Directory(f.getPath());</span><br><span class="line"> }</span><br><span class="line"> }).collect(Collectors.toList());</span><br><span class="line"> }</span><br><span class="line"> <span class="meta">@Override</span> <span class="function">String <span class="title">toTreeString</span><span class="params">()</span> </span>{</span><br><span class="line"> StringBuilder ret = <span class="keyword">new</span> StringBuilder();</span><br><span class="line"> ret.append(<span class="keyword">super</span>.path).append(<span class="string">"\n"</span>);</span><br><span class="line"> list().forEach(node -> ret.append(node.toTreeString()));</span><br><span class="line"> <span class="keyword">return</span> ret.toString();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>组合模式将一组对象抽象成树形结构,将单个对象和组合对象都看做树中的节点,以统一处理逻辑,并且它利用树形结构的特点,递归地处理每个子树,依次简化代码实现。使用场景必须要能够表示成为树形结构是其前提,因此其使用场景比较有局限性。</p><h2 id="享元模式"><a href="#享元模式" class="headerlink" title="享元模式"></a>享元模式</h2><p>享元模式指的是复用被共享的变量,其意图在于复用对象,节省内存。如果在一个系统中存在大量重复对象,且这些对象是不可变变量,就可以将其设计为享元,在内存中只保留一份实例,供多处代码引用。因此,享元模式就是通过工厂方法创建对象,在工厂方法内部,很可能返回缓存的实例,而不是新创建实例,从而实现不可变实例的复用。</p><p>享元模式在Java标准库中有许多的的应用,例如Integer.valueOf这个静态工厂创建实例,当传入的int范围在-128到+127的范围之间时,会直接返回缓存的Integer实例:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//java 11</span></span><br><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Main</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> Integer n1 = Integer.valueOf(<span class="number">100</span>);</span><br><span class="line"> Integer n2 = Integer.valueOf(<span class="number">100</span>);</span><br><span class="line"> System.out.println(n1 == n2); <span class="comment">// true</span></span><br><span class="line"> Integer n3 = Integer.valueOf(<span class="number">200</span>);</span><br><span class="line"> Integer n4 = Integer.valueOf(<span class="number">200</span>);</span><br><span class="line"> System.out.println(n3 == n4); <span class="comment">// false</span></span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure>]]></content>
<summary type="html">介绍设计模式中的结构型模式,包括代理模式,桥接模式,装饰器模式,适配器模式,门面模式,组合模式,享元模式。</summary>
<category term="设计模式" scheme="http://qian0817.top/categories/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
<category term="设计模式" scheme="http://qian0817.top/tags/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/"/>
</entry>
</feed>