-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
691 lines (327 loc) · 704 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>理解分布式流量防卫兵Sentinel</title>
<link href="/2019/10/27/li-jie-fen-bu-shi-liu-liang-fang-wei-bing-sentinel/"/>
<url>/2019/10/27/li-jie-fen-bu-shi-liu-liang-fang-wei-bing-sentinel/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/0.jpg" alt><br>Sentinel 承接了阿里巴巴近 10 年的双十一大促流量的核心场景</p><p>本文介绍阿里开源限流熔断方案Sentinel功能、原理、架构、快速入门以及相关框架比较</p><h1 id="基本介绍"><a href="#基本介绍" class="headerlink" title="基本介绍"></a>基本介绍</h1><h2 id="1-名词解释"><a href="#1-名词解释" class="headerlink" title="1 名词解释"></a>1 名词解释</h2><ul><li><p><strong>服务限流</strong> :当系统资源不够,不足以应对大量请求,对系统按照预设的规则进行流量限制或功能限制</p><ul><li><strong>服务熔断</strong>:当调用目标服务的请求和调用大量超时或失败,服务调用方为避免造成长时间的阻塞造成影响其他服务,后续对该服务接口的调用不再经过进行请求,直接执行本地的默认方法</li></ul></li><li><p><strong>服务降级</strong>:为了保证核心业务在大量请求下能正常运行,根据实际业务情况及流量,对部分服务降低优先级,有策略的不处理或用简单的方式处理</p></li></ul><p>服务降级的实现可以基于人工开关降级(秒杀、电商大促等)和自动检测(超时、失败次数、故障),熔断可以理解为一种服务故障降级处理</p><h2 id="2-为什么需要限流降级"><a href="#2-为什么需要限流降级" class="headerlink" title="2 为什么需要限流降级"></a>2 为什么需要限流降级</h2><p>系统承载的访问量是有限的,如果不做流量控制,会导致系统资源占满,服务超时,从而所有用户无法使用,通过服务限流控制请求的量,服务降级省掉非核心业务对系统资源的占用,最大化利用系统资源,尽可能服务更多用户</p><h2 id="3-Sentinel简介"><a href="#3-Sentinel简介" class="headerlink" title="3 Sentinel简介"></a>3 Sentinel简介</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/1.png" alt><br>Sentinel: 分布式系统的流量防卫兵,是阿里中间件团队2018年7月开源的,面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来保护系统服务的稳定性</p><p>Sentinel 的开源生态:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/2.png" alt></p><h1 id="功能特性"><a href="#功能特性" class="headerlink" title="功能特性"></a>功能特性</h1><h2 id="1-总体介绍"><a href="#1-总体介绍" class="headerlink" title="1 总体介绍"></a>1 总体介绍</h2><p>Sentinel 具有以下特征:</p><p><strong>丰富的应用场景</strong>:秒杀限流,消息削峰填谷、集群流量控制、实时熔断下游不可用应用等</p><p><strong>完备的实时监控</strong>:Sentinel 同时提供实时的监控功能。可以在控制台中看到接入应用的单台机器秒级数据,甚至 500 台以下规模的集群的汇总运行情况</p><p><strong>广泛的开源生态</strong>:Sentinel 提供开箱即用的与其它开源框架/库的整合模块,例如与 Spring Cloud、Dubbo、gRPC 的整合。只需要引入相应的依赖并进行简单的配置即可快速地接入 Sentinel</p><p><strong>完善的 SPI 扩展点</strong>:Sentinel 提供简单易用、完善的 SPI 扩展接口。可以通过实现扩展接口来快速地定制逻辑。例如定制规则管理、适配动态数据源等</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/3.png" alt><br>Sentinel 分为两个部分:</p><p><strong>控制台(Dashboard)</strong> 基于 Spring Boot 开发,打包后可以直接运行,不需要额外的 Tomcat 等应用容器</p><p><strong>核心库(Java 客户端)</strong> 不依赖任何框架/库,能够运行于所有 Java 运行时环境,同时对 Dubbo / Spring Cloud 等框架也有较好的支持</p><h2 id="2-控制台特性"><a href="#2-控制台特性" class="headerlink" title="2 控制台特性"></a>2 控制台特性</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/4.png" alt></p><ul><li><p>实时监控<br>支持自动发现集群机器列表、服务健康状态、服务调用通过/拒绝QPS、调用耗时、图表统计</p></li><li><p>规则管理及推送<br>支持在界面配置流控、降级、热点规则,并实时推送</p></li><li><p>鉴权<br>控制台支持自定义鉴权接口,提供基本登录功能</p></li></ul><h2 id="3-核心库功能特性"><a href="#3-核心库功能特性" class="headerlink" title="3 核心库功能特性"></a>3 核心库功能特性</h2><h3 id="1-应用流控"><a href="#1-应用流控" class="headerlink" title="(1) 应用流控"></a>(1) 应用流控</h3><p>针对指定应用实例的流量控制,监控应用流量QPS或并发线程数,当达到指定的阈值时对流量进行控制,以避免被瞬时的流量高峰冲垮,从而保障应用的高可用性</p><p>流量控制的手段包括:</p><ul><li>直接拒绝</li><li>Warm Up,即预热/冷启动方式,让通过的流量缓慢增加,在一定时间内逐渐增加到阈值上限,给冷系统一个预热的时间,避免冷系统被瞬间压垮</li><li>匀速排队,严格控制请求通过的间隔时间,让请求以均匀的速度通过</li></ul><h3 id="2-集群流控"><a href="#2-集群流控" class="headerlink" title="(2) 集群流控"></a>(2) 集群流控</h3><p>不同于应用流控根据单个应用实例阈值执行限流检查,集群流控只对整个集群调用总量进行限流,例如以下场景:</p><ul><li>限制某个用户调用某个API的总QPS,提供API的应用在多个机器上部署了多个实例</li><li>因为多个应用实例流量不均匀,导致集群调用总量没有到的情况下某些机器就开始限流</li></ul><p>仅靠单机维度去限制的话会无法精确地限制总体流量,通过集群精确地控制整个集群的调用总量,结合单机限流兜底,可以更好地发挥流量控制的效果</p><h3 id="3-网关流控"><a href="#3-网关流控" class="headerlink" title="(3) 网关流控"></a>(3) 网关流控</h3><p>Sentinel 支持对 Spring Cloud Gateway、Zuul 等主流的 API Gateway 进行限流</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/5.png" alt><br>网关流控针对 API网关的场景定制的限流规则,可以针对不同 route 或自定义的 API 分组进行限流,支持针对请求中的路径、参数、Header、来源 IP 等进行定制化的限流</p><h3 id="4-熔断降级"><a href="#4-熔断降级" class="headerlink" title="(4) 熔断降级"></a>(4) 熔断降级</h3><p>如果调用链路中的某个资源不稳定,最终会导致请求发生堆积,通过熔断降级能在调用链路中某个资源出现不稳定状态时(包括调用超时、异常比例升高、异常数升高),对这个资源的调用进行限制,让请求快速失败,避免影响到其它的资源而导致级联错误</p><p>当资源被降级后,在接下来的降级时间窗口之内,对该资源的调用都自动熔断(默认行为是抛出 DegradeException),经过时间窗口之后,退出熔断,并在下一次资源出现不稳定状态再次自动熔断</p><h3 id="5-热点参数限流"><a href="#5-热点参数限流" class="headerlink" title="(5) 热点参数限流"></a>(5) 热点参数限流</h3><p>热点即经常访问的数据,热点参数限流会统计传入参数中的热点参数,并根据配置的限流阈值与模式,对包含热点参数的资源调用进行限流<br>例如以下场景:</p><ul><li>用户ID为参数,限制用户对接口的范围QPS</li><li>商品ID为参数,限制商品下单接口频率<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/6.png" alt></li></ul><h3 id="6-系统自适应限流"><a href="#6-系统自适应限流" class="headerlink" title="(6) 系统自适应限流"></a>(6) 系统自适应限流</h3><p>为了解决传统方案:基于操作系统负载(load1,linux下用uptime查看)做进行自适应限流,带来的存在延时、系统性能恢复慢的问题,Sentinel采用新的思路:根据系统能够处理的请求,和允许进来的请求,来做平衡,而不是根据一个间接的指标(系统 load)来做限流</p><p>目标在于:<strong>在系统不被拖垮的情况下,尽可能提高系统的吞吐率,而不是 负载 一定要到低于某个阈值</strong></p><p>系统保护规则是从应用级别的入口流量进行控制,从单台机器的总体 Load、RT、入口 QPS 和线程数四个维度监控应用数据,当实际运行达到限定阈值进行限流保护,支持的阈值类型:</p><ul><li>Load:当系统 load1 超过阈值,且系统当前的并发线程数超过系统容量时才会触发系统保护。系统容量由系统时间运行监测到的的 maxQps * minRt (最小响应时间)计算得出</li><li>RT:当单台机器上所有入口流量的平均 RT(响应时间)</li><li>线程数:当单台机器上所有入口流量的并发线程数</li><li>入口 QPS:当单台机器上所有入口流量的 QPS </li></ul><h3 id="7-黑白名单控制"><a href="#7-黑白名单控制" class="headerlink" title="(7) 黑白名单控制"></a>(7) 黑白名单控制</h3><p>Sentinel黑白名单根据资源的请求来源(origin)限制资源是否通过,若配置白名单则只有请求来源位于白名单内时才可通过;若配置黑名单则请求来源位于黑名单时不通过,其余的请求通过</p><h1 id="快速入门"><a href="#快速入门" class="headerlink" title="快速入门"></a>快速入门</h1><h2 id="1-安装控制台"><a href="#1-安装控制台" class="headerlink" title="1 安装控制台"></a>1 安装控制台</h2><p>从github release页面(<a href="https://github.com/alibaba/Sentinel/releases)下载最新控制台jar包" target="_blank" rel="noopener">https://github.com/alibaba/Sentinel/releases)下载最新控制台jar包</a></p><p>命令行启动控制台:</p><pre><code>java -Dserver.port=8080 -Dcsp.sentinel.dashboard.server=localhost:8080 -Dproject.name=sentinel-dashboard -jar sentinel-dashboard.jar</code></pre><h2 id="2-应用接入Sentinel"><a href="#2-应用接入Sentinel" class="headerlink" title="2 应用接入Sentinel"></a>2 应用接入Sentinel</h2><p>Sentinel适配了常见主流框架,包括Dubbo、Spring Boot、Spring WebFlux、gRPC、Zuul、Spring Cloud Gateway、RocketMQ、Web Servlet,对于需要限流的资源,支持用原生Java的try-catch 接入或者使用注解</p><p>下面以常见的Spring Boot注解的方式作为示例:<br>引入sentinel适配Spring Cloud的依赖:</p><pre><code><dependency> <groupId>com.alibaba.cloud</groupId> <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> <version>2.1.0.RELEASE</version></dependency></code></pre><p>application.yml指定控制台地址:</p><pre><code>spring: cloud: sentinel: transport: dashboard: IP:端口号</code></pre><p>定义需要限流的资源:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token annotation punctuation">@RestController</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">TestController</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@GetMapping</span><span class="token punctuation">(</span>value <span class="token operator">=</span> <span class="token string">"/hello"</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// 定义需要限流的资源名称为hello</span> <span class="token annotation punctuation">@SentinelResource</span><span class="token punctuation">(</span><span class="token string">"hello"</span><span class="token punctuation">)</span> <span class="token keyword">public</span> String <span class="token function">hello</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token string">"Hello Sentinel"</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>请求一次上面的http hello接口后,触发Sentinel客户端初始化,才能在控制台看到接口</p><p>添加流控规则:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/7.png" alt><br>频繁请求接口,可以看到部分请求被拒绝:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/8.png" alt></p><p>注意:上面的配置方式是没有做持久化的,生产环境不建议使用 </p><h2 id="3-规则配置"><a href="#3-规则配置" class="headerlink" title="3 规则配置"></a>3 规则配置</h2><p>Sentinel 提供 动态规则数据源 支持来动态地管理、读取配置的规则。Sentinel 提供的 ReadableDataSource 和 WritableDataSource 接口简单易用,非常方便使用。</p><p>Sentinel 动态规则源针对常见的配置中心和远程存储进行适配,目前已支持 Nacos、ZooKeeper、Apollo、Redis 等多种动态规则源,可以覆盖到很多的生产场景</p><h1 id="实现原理"><a href="#实现原理" class="headerlink" title="实现原理"></a>实现原理</h1><p>下面介绍Sentinel客户端基本原理</p><h2 id="1-基本概念"><a href="#1-基本概念" class="headerlink" title="1 基本概念"></a>1 基本概念</h2><ul><li><p>Resource 资源<br>Sentinel中,需要被流量保护的方法、代码块都可以称为资源,每个资源都需要定义一个唯一的资源名词,用于匹配相关规则</p></li><li><p>Entry<br>Sentinel功能入口类,Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 SphU API 显式创建,创建后执行资源和规则匹配和校验</p></li><li><p>Slot<br>功能插槽,由Enty类创建,每个资源对应一系列Slot,Slot实现资源信息收集、规则匹配、校验的,多个Slot通过组成Slot Chain,在进入资源和退出资源时分别基于责任链模式调用entry()和exit()方法</p></li></ul><h2 id="2-工作原理"><a href="#2-工作原理" class="headerlink" title="2 工作原理"></a>2 工作原理</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/9.png" alt></p><p>一个简单的demo:</p><pre class="line-numbers language-java"><code class="language-java">String resourceName <span class="token operator">=</span> <span class="token string">"resourceName"</span><span class="token punctuation">;</span>Entry entry <span class="token operator">=</span> null<span class="token punctuation">;</span><span class="token keyword">try</span> <span class="token punctuation">{</span> entry <span class="token operator">=</span> SphU<span class="token punctuation">.</span><span class="token function">entry</span><span class="token punctuation">(</span>resourceName<span class="token punctuation">)</span><span class="token punctuation">;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"resource running"</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">BlockException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 限流</span> <span class="token keyword">throw</span> e<span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Throwable</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> e<span class="token punctuation">.</span><span class="token function">printStackTrace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">throw</span> e<span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>entry <span class="token operator">!=</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span> entry<span class="token punctuation">.</span><span class="token function">exit</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>主要流程如下:</p><ul><li>进入资源方法之前,基于SphU创建Entry,Entry获取查找资源关联的Slot Chain信息,如果找不到则创建,并基于责任链模式调用Slot的entry()方法</li><li>资源方法调用</li><li>资源方法调用完成后,通过Entry触发Slot的exit()逻辑</li></ul><h1 id="框架比较"><a href="#框架比较" class="headerlink" title="框架比较"></a>框架比较</h1><table><thead><tr><th></th><th>Sentinel</th><th>Hystrix</th><th>resilience4j</th></tr></thead><tbody><tr><td>隔离策略</td><td>信号量隔离(并发线程数限流)</td><td>线程池隔离/信号量隔离</td><td>信号量隔离</td></tr><tr><td>熔断降级策略</td><td>基于响应时间、异常比率、异常数</td><td>基于异常比率</td><td>基于异常比率、响应时间</td></tr><tr><td>实时统计实现</td><td>滑动窗口(LeapArray)</td><td>滑动窗口(基于 RxJava)</td><td>Ring Bit Buffer</td></tr><tr><td>动态规则配置</td><td>支持多种数据源</td><td>支持多种数据源</td><td>有限支持</td></tr><tr><td>扩展性</td><td>多个扩展点</td><td>插件的形式</td><td>接口的形式</td></tr><tr><td>基于注解的支持</td><td>支持</td><td>支持</td><td>支持</td></tr><tr><td>限流</td><td>基于 QPS,支持基于调用关系的限流</td><td>有限的支持</td><td>Rate Limiter</td></tr><tr><td>流量整形</td><td>支持预热模式、匀速器模式、预热排队模式</td><td>不支持</td><td>简单的 Rate Limiter 模式</td></tr><tr><td>系统自适应保护</td><td>支持</td><td>不支持</td><td>不支持</td></tr><tr><td>控制台</td><td>提供开箱即用的控制台,可配置规则、查看秒级监控、机器发现等</td><td>简单的监控查看</td><td>不提供控制台,可对接其它监控系统</td></tr></tbody></table><p>值得补充的是:相比Hystrix基于线程池隔离进行限流,这种方案虽然隔离性比较好,但是代价就是线程数目太多,线程上下文切换的 overhead 比较大,特别是对低延时的调用有比较大的影响。</p><p>Sentinel 并发线程数限流不负责创建和管理线程池,而是简单统计当前请求上下文的线程数目,如果超出阈值,新的请求会被立即拒绝,效果类似于信号量隔离</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p>《Sentinel官方文档》</p><p><a href="https://github.com/alibaba/Sentinel/wiki" target="_blank" rel="noopener">https://github.com/alibaba/Sentinel/wiki</a></p><p>《从 Hystrix 迁移到 Sentinel》</p><p><a href="https://github.com/alibaba/Sentinel/wiki/Guideline:-从-Hystrix-迁移到-Sentinel" target="_blank" rel="noopener">https://github.com/alibaba/Sentinel/wiki/Guideline:-从-Hystrix-迁移到-Sentinel</a></p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E6%B5%81%E9%87%8F%E9%98%B2%E5%8D%AB%E5%85%B5Sentinel/10.png" alt></p>]]></content>
<categories>
<category> 技术框架 </category>
</categories>
</entry>
<entry>
<title>理解Redis-Cluster集群</title>
<link href="/2019/10/27/li-jie-redis-cluster-ji-qun/"/>
<url>/2019/10/27/li-jie-redis-cluster-ji-qun/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/0.jpg" alt></p><p>Redis作为一款性能优异的内存数据库,支撑着微博亿级社交平台,也成为很多互联网公司的标配。这里将以Redis Cluster集群为核心,基于最新的Redis5版本,从原理再到实战,玩转Redis集群</p><h1 id="常见Redis集群方案"><a href="#常见Redis集群方案" class="headerlink" title="常见Redis集群方案"></a>常见Redis集群方案</h1><p>在介绍Redis Cluster集群方案之前,为了方便对比,先简单了解一下业界常见的Redis集群方案:</p><h2 id="1-基于客户端分片"><a href="#1-基于客户端分片" class="headerlink" title="1 基于客户端分片"></a>1 基于客户端分片</h2><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/1.png" alt><br>Redis Sharding是Redis Cluster出来之前,业界普遍使用的多Redis实例集群方法。其主要思想是基于哈希算法,根据Redis数据的key的哈希值对数据进行分片,将数据映射到各自节点上</p><p>优点在于实现简单,缺点在于当Redis集群调整,每个客户端都需要更新调整</p><h2 id="2-基于代理服务器分片"><a href="#2-基于代理服务器分片" class="headerlink" title="2 基于代理服务器分片"></a>2 基于代理服务器分片</h2><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/2.png" alt><br>客户端发送请求到独立部署代理组件,代理组件解析客户端的数据,并将请求转发至正确的节点,最后将结果回复给客户端</p><p>优点在于透明接入,容易集群扩展,缺点在于多了一层代理转发,性能有所损耗</p><h2 id="3-Redis-Sentinel-哨兵"><a href="#3-Redis-Sentinel-哨兵" class="headerlink" title="3 Redis Sentinel(哨兵)"></a>3 Redis Sentinel(哨兵)</h2><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/3.png" alt><br>Redis Sentinel是官方从Redis 2.6版本提供的高可用方案,在Redis主从复制集群的基础上,增加Sentinel集群监控整个Redis集群。当Redis集群master节点发生故障时,Sentinel进行故障切换,选举出新的master,同时Sentinel本身支持高可用集群部署</p><p>优点在于支持集群高可用,高性能读写,缺点在于没有实现数据分片,每个节点需要承载完整数据集,负载能力受当个Redis服务器限制,仅支持通过增加机器内存实现垂直扩容,不支持水平扩展</p><h1 id="Redis-Cluster设计"><a href="#Redis-Cluster设计" class="headerlink" title="Redis Cluster设计"></a>Redis Cluster设计</h1><h2 id="1-整体设计"><a href="#1-整体设计" class="headerlink" title="1 整体设计"></a>1 整体设计</h2><p>Redis Cluster 是 在 3.0 版本正式推出的高可用集群方案,相比Redis Sentinel,Redis Cluster方案不需要额外部署Sentinel集群,而是通过集群内部通信实现集群监控,故障时主从切换;同时,支持内部基于哈希实现数据分片,支持动态水平扩容</p><p>整体架构如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/4.png" alt><br>集群中有多个主节点,每个主节点有多个从节点,主从节点间数据一致,最少需要3个主节点,每个主节点最少需要1个从节点</p><ul><li><strong>高可用</strong>:当master节点故障时,自动主从切换</li><li><strong>高性能</strong>:主节点提供读写服务,从节点只读服务,提高系统吞吐量</li><li><strong>可扩展性</strong>:集群的数据分片存储,主节点间数据各不同,各自维护对应数据,可以为集群添加节点进行扩容,也可以下线部分节点进行水平缩容</li></ul><h2 id="2-数据分片"><a href="#2-数据分片" class="headerlink" title="2 数据分片"></a>2 数据分片</h2><p>将整个数据集按照一定规则分配到多个节点上,称为数据分片,Redis Cluster采用的分片方案是哈希分片</p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/5.png" alt><br>基本原理如下:<br>Redis Cluster首先定义了编号0 ~ 16383的区间,称为槽,所有的键根据哈希函数映射到0 ~ 16383整数槽内,计算公式:slot=CRC16(key)&16383。每一个节点负责维护一部分槽以及槽所映射的键值数据</p><p><strong>槽是 Redis 集群管理数据的基本单位</strong>,集群扩容收缩就是槽和数据在节点之间的移动</p><p>槽与节点映射关系如下:</p><ul><li>每个集群节点维护着一个16384 bit (2kB)的位数组,每个bit对应相同编号的槽,用 0 / 1标识对于某个槽自己是否拥有</li><li>集群节点同时还维护着槽到集群节点的映射,是由长度为16384,数组下标代表槽编号,值为节点信息的数组</li></ul><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/6.png" alt></p><h2 id="3-集群扩容"><a href="#3-集群扩容" class="headerlink" title="3 集群扩容"></a>3 集群扩容</h2><p>Redis Cluster支持不影响集群对外服务的情况下,对集群进行动态扩容或缩容,当Redis 新节点加入现有集群后,需要为其迁移槽和数据,确保迁移后每个节点负责相似数量的槽,使数据分布均匀在各节点上</p><p>整个数据迁移涉及系列操作,Redis提供了集群管理工具,包括基于Ruby的redis-trib.rb,还Redis5新提供的基于C语言redis-cli,下面的介绍以redis-cli为例</p><p>源节点将指定slot数据迁移到目标节点,基本流程如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/7.png" alt></p><ul><li>(1) redis-cli设置目标节点指定slot状态importing,让目标节点准备迁入slot数据</li><li>(2) redis-cli设置源节点指定slot状态migrating,让让源节点准备迁出slot的数据</li><li>(3) redis-cli批量迁移源节点指定slot中的数据到目标节点</li><li>(4) 数据迁移完后 redis-cli向集群所有主节点通知槽被分配给目标节点,主节点更新slot与节点映射关系信息</li></ul><p>通常情况下,如果客户端请求的数据不在节点上,节点会回复 MOVED 重定向信息,客户端根据该信息再请求正确的节点。对于正在迁移的slot数据,保证客户端仍然能正常访问的设计如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/8.png" alt></p><ul><li>(1) 迁移完成后才更新slot与节点映射关系信息,如果迁移进行中的映射信息保持与迁移前一致</li><li>(2) 如果客户端访问源节点,访问的key尚未迁出,则正常的处理该key</li><li>(3) 如果客户端访问源节点,访问的key尚已迁出,源节点返回ASK重定向信息</li><li>(4) 客户端根据ASK 重定向异常提取出目标节点信息,先向目标节点发送ASKING命令请求操作,再执行键命令</li></ul><p>ASK 和 MOVED 这2个重定向控制有如下区别:</p><ul><li>ASK 重定向说明集群正在进行 slot 数据迁移,客户端无法知道什么时候迁移完成,因此只能是临时性的重定向,客户端不会更新 slot 到 Redis 节点的映射缓存。</li><li>MOVED 重定向说明键对应的slot 已经明确指定到新的节点,因此需要更新 slot 到 Redis 节点的映射缓存</li></ul><h2 id="4-CAP取舍"><a href="#4-CAP取舍" class="headerlink" title="4 CAP取舍"></a>4 CAP取舍</h2><p>CAP包括:一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance),系统如果不能在时限内达成数据一致性,就意味着发生了分区的情况,必须在C和A之间做出选择</p><p>Redis Cluster选择了AP架构,为了保证可用性,Redis并不保证强一致性,在特定条件下会出现数据不一致甚至丢失写操作</p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/9.png" alt><br>第一个原因是:为了在性能和一致性上做出权衡,主从节点间数据同步是异步复制的,当客户端成功写入master节点,master返回成功,master节点才将写操作异步复制给slave节点</p><p>另外一个原因是,当集群发送网络分区,集群可能会分为两部分:多数派和少数派,假如masterA节点位于少数派,如果网络分区发生时间较短,那么集群将会继续正常运作;如果分区的时间足够长,让多数派中选举为新的master替代matsterA,那么分区期间写入masterA的数据就丢失了</p><p>在网络分区期间, 客户端可以向matsterA发送写命令的最大时间是有限制的, 这一时间限制称为节点超时时间(cluster-node-timeout),是 Redis集群的一个重要的配置选项</p><h1 id="集群搭建"><a href="#集群搭建" class="headerlink" title="集群搭建"></a>集群搭建</h1><p>2018年10月 Redis 发布了稳定版本的 5.0 版本,推出了各种新特性,其中一点是集群管理工具从基于Ruby的redis-trib.rb移植到基于C语言redis-cli中,方便集群的构建和管理</p><p>Redis Cluster集群运行至少需要包含3个主节点,实现高可用最少需要3主3从6个节点</p><p>以下步骤基于Redis 5.0.5版本,介绍如何在一台 Linux 服务器上搭建有3主3从的6节点的 Redis集群</p><ul><li><p><strong>步骤1 创建安装目录</strong></p><pre class="line-numbers language-shell"><code class="language-shell">mkdir -p /data/project/redis-cluster<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></li><li><p><strong>步骤2 下载源码并解压编译</strong></p><pre class="line-numbers language-shell"><code class="language-shell">cd /data/project/redis-clusterwget http://download.redis.io/releases/redis-5.0.5.tar.gztar xzf redis-5.0.5.tar.gzcd redis-5.0.5make<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>执行make后,如果报错“jemalloc/jemalloc.h:没有那个文件或目录”,可以改为用以下命令:</p><pre class="line-numbers language-shell"><code class="language-shell">make MALLOC=libc<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></li><li><p><strong>步骤3 创建6个Redis配置文件</strong><br>6个配置文件不能在同一个目录,下面Redis 6个节点分别安装在7000~7005端口<br>首先创建配置文件目录及文件,定义如下:</p><pre><code>mkdir -p /data/project/redis-cluster/nodes/7000mkdir -p /data/project/redis-cluster/nodes/7001mkdir -p /data/project/redis-cluster/nodes/7002mkdir -p /data/project/redis-cluster/nodes/7003mkdir -p /data/project/redis-cluster/nodes/7004mkdir -p /data/project/redis-cluster/nodes/7005</code></pre></li></ul><p>touch /data/project/redis-cluster/nodes/7000/redis.conf<br>touch /data/project/redis-cluster/nodes/7001/redis.conf<br>touch /data/project/redis-cluster/nodes/7002/redis.conf<br>touch /data/project/redis-cluster/nodes/7003/redis.conf<br>touch /data/project/redis-cluster/nodes/7004/redis.conf<br>touch /data/project/redis-cluster/nodes/7005/redis.conf</p><pre><code>redis.conf配置文件的内容为:```shell############################## 网络 ###############################端口port 7000#非保护模式,如果值为yes,则必须是 bind配置指定的ip的机器连接或者使用密码连接protected-mode no ############################## 通用 ###############################后台运行daemonize yes #记录redis进程pidpidfile /var/run/redis_7000.pid############################## 集群 ###############################启用集群模式cluster-enabled yes cluster-config-file nodes_7000.conf#集群节点如果在该超时时间(毫秒)内不可达,则认为节点处于故障状态cluster-node-timeout 15000############################## 持久化 ###############################AOF, RDB持久化文件目录dir /data/project/redis-cluster/nodes#开启AOF持久化appendonly yes#AOF文件名appendfilename "appendonly_7000.aof"#当目前aof文件大小超过上一次重写的aof文件大小的百分之多少进行重写auto-aof-rewrite-percentage 100#设置允许重写的最小aof文件大小,避免了达到约定百分比但尺寸仍然很小的情况还要重写auto-aof-rewrite-min-size 64mb#RDB文件名dbfilename dump_7000.rdb</code></pre><p>其中 port 、 pidfile、cluster-config-file、appendfilename、 dbfilename配置需要随着节点的不同而调整</p><p>配置项说明可以参考redis-5.0.5/redis.conf,每一项都介绍得很详细,推荐阅读</p><ul><li><strong>步骤4 启动节点</strong><pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-server /data/project/redis-cluster/nodes/7000/redis.conf/data/project/redis-cluster/redis-5.0.5/src/redis-server /data/project/redis-cluster/nodes/7001/redis.conf/data/project/redis-cluster/redis-5.0.5/src/redis-server /data/project/redis-cluster/nodes/7002/redis.conf/data/project/redis-cluster/redis-5.0.5/src/redis-server /data/project/redis-cluster/nodes/7003/redis.conf/data/project/redis-cluster/redis-5.0.5/src/redis-server /data/project/redis-cluster/nodes/7004/redis.conf/data/project/redis-cluster/redis-5.0.5/src/redis-server /data/project/redis-cluster/nodes/7005/redis.conf<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre>ps -ef|grep redis,可以看到6个redis进程已启动:</li></ul><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/10.png" alt></p><ul><li><strong>步骤5 启动集群</strong><br>使用如下命令启动集群,IP地址自行替换:<pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster create 192.168.56.102:7000 192.168.56.102:7001 192.168.56.102:7002 192.168.56.102:7003 192.168.56.102:7004 192.168.56.102:7005 --cluster-replicas 1<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></li></ul><p>启动成功信息如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/11.png" alt><br>到此,Redis Cluster 集群搭建完成</p><h1 id="集群信息查看"><a href="#集群信息查看" class="headerlink" title="集群信息查看"></a>集群信息查看</h1><p>Redis5的redis-cli新增系列集群运维功能,查看命令详情:</p><pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster help<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/12.png" alt><br>命令参数具体作用可以参考官方文档,下面会基于其中一些常用命令对集群进行管理</p><ul><li><p><strong>检查节点状态</strong></p><pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster check 192.168.56.102:7000<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/13.png" alt></p></li><li><p><strong>查看集群信息</strong></p><pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster info 192.168.56.102:7000<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/14.png" alt></p></li></ul><h1 id="集群扩容"><a href="#集群扩容" class="headerlink" title="集群扩容"></a>集群扩容</h1><p>集群现在有3主3从,下面新增4个节点扩容变成5主5从</p><ul><li><strong>步骤1 启动新节点</strong><br>创建4个Redis配置文件,端口号为7006~7009,然后启动节点(参考“集群搭建”的步骤3和步骤4)</li><li><strong>步骤2 新节点加入集群</strong><br>设置4个节点分别加入已有redis集群,2个为主节点,2个为从节点</li></ul><pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster add-node 192.168.56.102:7006 192.168.56.102:7005 /data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster add-node 192.168.56.102:7007 192.168.56.102:7005 #24e2c是节点7006的id,代表该节点加入集群并为7006的从节点/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster add-node 192.168.56.102:7008 192.168.56.102:7005 --cluster-slave --cluster-master-id 24e2c369678952b07d95c0a4b49c2d7a7b2e2bf7 #24e2c是节点7007的id,代表该节点加入集群并为7007的从节点/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster add-node 192.168.56.102:7009 192.168.56.102:7005 --cluster-slave --cluster-master-id ab0f74a19819a74238df7a510494e9418678cbe1<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>此时集群状态如下,其中主节点7006和主节点7007还没分配任何slot,在下面的步骤会进行分配:</p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/15.png" alt></p><ul><li><strong>步骤3 模拟slot重新平衡分配</strong><br>基于rebalance命令,增加–cluster-simulat参数,查看会迁移哪些slots,而不会真正执行迁移操作<pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster rebalance 192.168.56.102:7000 --cluster-threshold 1 --cluster-use-empty-masters --cluster-simulat<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre>返回以下迁移信息:<br><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/16.png" alt></li><li><strong>步骤4 执行slot重新平衡分配</strong><br>执行rebalance命令,平衡集群节点slot数量,重新分配slot( 去掉–cluster-simulat)<pre class="line-numbers language-shell"><code class="language-shell">/data/project/redis-cluster/redis-5.0.5/src/redis-cli --cluster rebalance 192.168.56.102:7000 --cluster-threshold 1 --cluster-use-empty-masters<span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/17.png" alt><br>至此,集群扩容完成,集群缩容的话,需要基于reshard将需被下线的结点中的slot移到其他结点,然后基于del-node命令删除结点</li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>至此,Redis Cluster集群介绍到这里,其实还有集群通信协议,内存,数据备份,主从复制等特性值得学习,是设计分布式系统的典范,有机会再展开介绍</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p>微博6年redis实践<br><a href="https://mp.weixin.qq.com/s/dBWIHwfmrs6Tt7INw-zSyA" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/dBWIHwfmrs6Tt7INw-zSyA</a></p><p>Redis 官网介绍cluster设计说明<br><a href="https://redis.io/topics/cluster-tutorial" target="_blank" rel="noopener">https://redis.io/topics/cluster-tutorial</a><br><a href="https://redis.io/topics/cluster-spec" target="_blank" rel="noopener">https://redis.io/topics/cluster-spec</a></p><p>redis cluster管理工具redis-trib-rb详解<br><a href="http://weizijun.cn/2016/01/08/redis%20cluster管理工具redis-trib-rb详解/" target="_blank" rel="noopener">http://weizijun.cn/2016/01/08/redis%20cluster管理工具redis-trib-rb详解/</a></p><p><img src="/images/%E7%90%86%E8%A7%A3Redis-Cluster%E9%9B%86%E7%BE%A4/18.png" alt></p>]]></content>
<categories>
<category> 技术框架 </category>
</categories>
</entry>
<entry>
<title>理解Java并发底层之AQS实现</title>
<link href="/2019/10/13/li-jie-java-bing-fa-di-ceng-zhi-aqs-shi-xian/"/>
<url>/2019/10/13/li-jie-java-bing-fa-di-ceng-zhi-aqs-shi-xian/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%B9%B6%E5%8F%91%E5%BA%95%E5%B1%82%E4%B9%8BAQS%E5%AE%9E%E7%8E%B0/0.png" alt><br>jdk的JUC包(java.util.concurrent)提供大量Java并发工具提供使用,基本由Doug Lea编写,很多地方值得学习和借鉴,是进阶升级必经之路</p><p>本文从JUC包中常用的对象锁、并发工具的使用和功能特性入手,带着问题,由浅到深,一步步剖析并发底层AQS抽象类具体实现</p><h1 id="名词解释"><a href="#名词解释" class="headerlink" title="名词解释"></a>名词解释</h1><h2 id="1-AQS"><a href="#1-AQS" class="headerlink" title="1 AQS"></a>1 AQS</h2><p>AQS是一个抽象类,类全路径java.util.concurrent.locks.AbstractQueuedSynchronizer,抽象队列同步器,是基于模板模式开发的并发工具抽象类,有如下并发类基于AQS实现:</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%B9%B6%E5%8F%91%E5%BA%95%E5%B1%82%E4%B9%8BAQS%E5%AE%9E%E7%8E%B0/1.png" alt></p><h2 id="2-CAS"><a href="#2-CAS" class="headerlink" title="2 CAS"></a>2 CAS</h2><p>CAS是Conmpare And Swap(比较和交换)的缩写,是一个原子操作指令</p><p>CAS机制当中使用了3个基本操作数:内存地址addr,预期旧的值oldVal,要修改的新值newVal<br>更新一个变量的时候,只有当变量的预期值oldVal和内存地址addr当中的实际值相同时,才会将内存地址addr对应的值修改为newVal</p><p>基于乐观锁的思路,通过CAS再不断尝试和比较,可以对变量值线程安全地更新</p><h2 id="3-线程中断"><a href="#3-线程中断" class="headerlink" title="3 线程中断"></a>3 线程中断</h2><p>线程中断是一种线程协作机制,用于协作其他线程中断任务的执行</p><p>当线程处于阻塞等待状态,例如调用了wait()、join()、sleep()方法之后,调用线程的interrupt()方法之后,线程会马上退出阻塞并收到InterruptedException;</p><p>当线程处于运行状态,调用线程的interrupt()方法之后,线程并不会马上中断执行,需要在线程的具体任务执行逻辑中通过调用isInterrupted() 方法检测线程中断标志位,然后主动响应中断,通常是抛出InterruptedException</p><h1 id="对象锁特性"><a href="#对象锁特性" class="headerlink" title="对象锁特性"></a>对象锁特性</h1><p>下面先介绍对象锁、并发工具有哪些基本特性,后面再逐步展开这些特性如何实现</p><h2 id="1-显式获取"><a href="#1-显式获取" class="headerlink" title="1 显式获取"></a>1 显式获取</h2><p>以ReentrantLock锁为例,主要支持以下4种方式显式获取锁</p><ul><li><p><strong>(1) 阻塞等待获取</strong></p><pre class="line-numbers language-java"><code class="language-java">ReentrantLock lock <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReentrantLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// 一直阻塞等待,直到获取成功</span>lock<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></li><li><p><strong>(2) 无阻塞尝试获取</strong></p><pre class="line-numbers language-java"><code class="language-java">ReentrantLock lock <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReentrantLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// 尝试获取锁,如果锁已被其他线程占用,则不阻塞等待直接返回false</span><span class="token comment" spellcheck="true">// 返回true - 锁是空闲的且被本线程获取,或者已经被本线程持有</span><span class="token comment" spellcheck="true">// 返回false - 获取锁失败</span><span class="token keyword">boolean</span> isGetLock <span class="token operator">=</span> lock<span class="token punctuation">.</span><span class="token function">tryLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p><strong>(3) 指定时间内阻塞等待获取</strong></p><pre class="line-numbers language-java"><code class="language-java">ReentrantLock lock <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReentrantLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 尝试在指定时间内获取锁</span> <span class="token comment" spellcheck="true">// 返回true - 锁是空闲的且被本线程获取,或者已经被本线程持有</span> <span class="token comment" spellcheck="true">// 返回false - 指定时间内未获取到锁</span> lock<span class="token punctuation">.</span><span class="token function">tryLock</span><span class="token punctuation">(</span><span class="token number">10</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">InterruptedException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 内部调用isInterrupted() 方法检测线程中断标志位,主动响应中断</span> e<span class="token punctuation">.</span><span class="token function">printStackTrace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p><strong>(4) 响应中断获取</strong></p><pre class="line-numbers language-java"><code class="language-java">ReentrantLock lock <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReentrantLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 响应中断获取锁</span> <span class="token comment" spellcheck="true">// 如果调用线程的thread.interrupt()方法设置线程中断,线程退出阻塞等待并抛出中断异常</span> lock<span class="token punctuation">.</span><span class="token function">lockInterruptibly</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">InterruptedException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> e<span class="token punctuation">.</span><span class="token function">printStackTrace</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li></ul><h2 id="2-显式释放"><a href="#2-显式释放" class="headerlink" title="2 显式释放"></a>2 显式释放</h2><pre class="line-numbers language-java"><code class="language-java">ReentrantLock lock <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReentrantLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>lock<span class="token punctuation">.</span><span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// ... 各种业务操作</span><span class="token comment" spellcheck="true">// 显式释放锁</span>lock<span class="token punctuation">.</span><span class="token function">unlock</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="3-可重入"><a href="#3-可重入" class="headerlink" title="3 可重入"></a>3 可重入</h2><p>已经获取到锁的线程,再次请求该锁可以直接获得</p><h2 id="4-可共享"><a href="#4-可共享" class="headerlink" title="4 可共享"></a>4 可共享</h2><p>指同一个资源允许多个线程共享,例如读写锁的读锁允许多个线程共享,共享锁可以让多个线程并发安全地访问数据,提高程序执效率</p><h2 id="5-公平、非公平"><a href="#5-公平、非公平" class="headerlink" title="5 公平、非公平"></a>5 公平、非公平</h2><p>公平锁:多个线程采用先到先得的公平方式竞争锁。每次加锁前都会检查等待队列里面有没有线程排队,没有才会尝试获取锁。<br>非公平锁:当一个线程采用非公平的方式获取锁时,该线程会首先去尝试获取锁而不是等待。如果没有获取成功,才会进入等待队列</p><p>因为非公平锁方式可以使后来的线程有一定几率直接获取锁,减少了线程挂起等待的几率,性能优于公平锁</p><h1 id="AQS实现原理"><a href="#AQS实现原理" class="headerlink" title="AQS实现原理"></a>AQS实现原理</h1><h2 id="1-基本概念"><a href="#1-基本概念" class="headerlink" title="1 基本概念"></a>1 基本概念</h2><h3 id="1-Condition接口"><a href="#1-Condition接口" class="headerlink" title="(1) Condition接口"></a>(1) Condition接口</h3><p>类似Object的wait()、wait(long timeout)、notify()以及notifyAll()的方法结合synchronized内置锁可以实现可以实现等待/通知模式,实现Lock接口的ReentrantLock、ReentrantReadWriteLock等对象锁也有类似功能:</p><p>Condition接口定义了await()、awaitNanos(long)、signal()、signalAll()等方法,配合对象锁实例实现等待/通知功能,原理是基于AQS内部类ConditionObject实现Condition接口,线程await后阻塞并进入CLH队列(下面提到),等待其他线程调用signal方法后被唤醒</p><h3 id="2-CLH队列"><a href="#2-CLH队列" class="headerlink" title="(2) CLH队列"></a>(2) CLH队列</h3><p>CLH队列,CLH是算法提出者Craig, Landin, Hagersten的名字简称</p><p>AQS内部维护着一个双向FIFO的CLH队列,AQS依赖它来管理等待中的线程,如果线程获取同步竞争资源失败时,会将线程阻塞,并加入到CLH同步队列;当竞争资源空闲时,基于CLH队列阻塞线程并分配资源</p><p>CLH的head节点保存当前占用资源的线程,或者是没有线程信息,其他节点保存排队线程信息</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%B9%B6%E5%8F%91%E5%BA%95%E5%B1%82%E4%B9%8BAQS%E5%AE%9E%E7%8E%B0/2.png" alt="CLH"><br>CLH中每一个节点的状态(waitStatus)取值如下:</p><ul><li><strong>CANCELLED(1)</strong>:表示当前节点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的节点将不会再变化</li><li><strong>SIGNAL(-1)</strong>:表示后继节点在等待当前节点唤醒。后继节点入队后进入休眠状态之前,会将前驱节点的状态更新为SIGNAL</li><li><strong>CONDITION(-2)</strong>:表示节点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的节点将从等待队列转移到同步队列中,等待获取同步锁</li><li><strong>PROPAGATE(-3)</strong>:共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能会唤醒后继的后继节点</li><li><strong>0</strong>:新节点入队时的默认状态</li></ul><h3 id="3-资源共享方式"><a href="#3-资源共享方式" class="headerlink" title="(3) 资源共享方式"></a>(3) 资源共享方式</h3><p>AQS定义两种资源共享方式:<br>Exclusive 独占,只有一个线程能执行,如ReentrantLock<br>Share 共享,多个线程可同时执行,如Semaphore/CountDownLatch</p><h3 id="4-阻塞-唤醒线程的方式"><a href="#4-阻塞-唤醒线程的方式" class="headerlink" title="(4) 阻塞/唤醒线程的方式"></a>(4) 阻塞/唤醒线程的方式</h3><p>AQS 基于sun.misc.Unsafe类提供的park方法阻塞线程,unpark方法唤醒线程,被park方法阻塞的线程能响应interrupt()中断请求退出阻塞</p><h2 id="2-基本设计"><a href="#2-基本设计" class="headerlink" title="2 基本设计"></a>2 基本设计</h2><p>核心设计思路:AQS提供一个框架,用于实现依赖于CLH队列的阻塞锁和相关的并发同步器。<strong>子类通过实现判定是否能获取/释放资源的protect方法,AQS基于这些protect方法实现对线程的排队、唤醒的线程调度策略</strong></p><p>AQS还提供一个支持线程安全原子更新的int类型变量作为同步状态值(state),子类可以根据实际需求,灵活定义该变量代表的意义进行更新</p><p>通过子类重新定义的系列protect方法如下:</p><ul><li>boolean tryAcquire(int) 独占方式尝试获取资源,成功则返回true,失败则返回false</li><li>boolean tryRelease(int) 独占方式尝试释放资源,成功则返回true,失败则返回false</li><li>int tryAcquireShared(int) 共享方式尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源</li><li>boolean tryReleaseShared(int) 共享方式尝试释放资源,如果释放后允许唤醒后续等待节点返回true,否则返回false</li></ul><p>这些方法始终由需要需要调度协作的线程来调用,子类须以非阻塞的方式重新定义这些方法</p><p>AQS基于上述tryXXX方法,对外提供下列方法来获取/释放资源:</p><ul><li>void acquire(int) 独占方式获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响</li><li>boolean release(int) 独占方式下线程释放资源,先释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源</li><li>void acquireShared(int) 独占方式获取资源</li><li>boolean releaseShared(int) 共享方式释放资源</li></ul><p>以独占模式为例:获取/释放资源的核心的实现如下:</p><pre class="line-numbers language-java"><code class="language-java"> Acquire<span class="token operator">:</span> <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">tryAcquire</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> 如果线程尚未排队,则将其加入队列; <span class="token punctuation">}</span> Release<span class="token operator">:</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">tryRelease</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">)</span> 唤醒CLH中第一个排队线程<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>到这里,有点绕,下面一张图把上面介绍到的设计思路再重新捋一捋:</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%B9%B6%E5%8F%91%E5%BA%95%E5%B1%82%E4%B9%8BAQS%E5%AE%9E%E7%8E%B0/3.png" alt="AQS基本设计"></p><h1 id="特性实现"><a href="#特性实现" class="headerlink" title="特性实现"></a>特性实现</h1><p>下面介绍基于AQS的对象锁、并发工具的一系列功能特性的实现原理</p><h2 id="1-显式获取-1"><a href="#1-显式获取-1" class="headerlink" title="1 显式获取"></a>1 显式获取</h2><p>该特性还是以ReentrantLock锁为例,ReentrantLock是可重入对象锁,线程每次请求获取成功一次锁,同步状态值state加1,释放锁state减1,state为0代表没有任何线程持有锁</p><p>ReentrantLock锁支持公平/非公平特性,下面的显式获取特性以公平锁为例</p><h3 id="1-阻塞等待获取"><a href="#1-阻塞等待获取" class="headerlink" title="(1) 阻塞等待获取"></a>(1) 阻塞等待获取</h3><p>基本实现如下:</p><ul><li>1、ReentrantLock实现AQS的tryAcquire(int)方法,先判断:如果没有任何线程持有锁,或者当前线程已经持有锁,则返回true,否则返回false</li><li>2、AQS的acquire(int)方法判断当前节点是否为head且基于tryAcquire(int)能否获得资源,如果不能获得,则加入CLH队列排队阻塞等待</li><li>3、ReentrantLock的lock()方法基于AQS的acquire(int)方法阻塞等待获取锁</li></ul><p>ReentrantLock中的tryAcquire(int)方法实现:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">protected</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> acquires<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">final</span> Thread current <span class="token operator">=</span> Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">int</span> c <span class="token operator">=</span> <span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 没有任何线程持有锁</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 通过CLH队列的head判断没有别的线程在比当前更早acquires</span> <span class="token comment" spellcheck="true">// 且基于CAS设置state成功(期望的state旧值为0)</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">hasQueuedPredecessors</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token function">compareAndSetState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> acquires<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 设置持有锁的线程为当前线程</span> <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>current<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 持有锁的线程为当前线程</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>current <span class="token operator">==</span> <span class="token function">getExclusiveOwnerThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 仅仅在当前线程,单线程,不用基于CAS更新</span> <span class="token keyword">int</span> nextc <span class="token operator">=</span> c <span class="token operator">+</span> acquires<span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>nextc <span class="token operator"><</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Maximum lock count exceeded"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setState</span><span class="token punctuation">(</span>nextc<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 其他线程已经持有锁</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>AQS的acquire(int)方法实现</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">final</span> <span class="token keyword">void</span> <span class="token function">acquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// tryAcquire检查释放能获取成功</span> <span class="token comment" spellcheck="true">// addWaiter 构建CLH的节点对象并入队</span> <span class="token comment" spellcheck="true">// acquireQueued线程阻塞等待</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">tryAcquire</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token function">acquireQueued</span><span class="token punctuation">(</span><span class="token function">addWaiter</span><span class="token punctuation">(</span>Node<span class="token punctuation">.</span>EXCLUSIVE<span class="token punctuation">)</span><span class="token punctuation">,</span> arg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// acquireQueued返回true,代表线程在获取资源的过程中被中断</span> <span class="token comment" spellcheck="true">// 则调用该方法将线程中断标志位设置为true</span> <span class="token function">selfInterrupt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">acquireQueued</span><span class="token punctuation">(</span><span class="token keyword">final</span> Node node<span class="token punctuation">,</span> <span class="token keyword">int</span> arg<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 标记是否成功拿到资源</span> <span class="token keyword">boolean</span> failed <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 标记等待过程中是否被中断过</span> <span class="token keyword">boolean</span> interrupted <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 循环直到资源释放</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token punctuation">;</span><span class="token punctuation">;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 拿到前驱节点</span> <span class="token keyword">final</span> Node p <span class="token operator">=</span> node<span class="token punctuation">.</span><span class="token function">predecessor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 如果前驱是head,即本节点是第二个节点,才有资格去尝试获取资源</span> <span class="token comment" spellcheck="true">// 可能是head释放完资源唤醒本节点,也可能被interrupt()</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>p <span class="token operator">==</span> head <span class="token operator">&&</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 成功获取资源</span> <span class="token function">setHead</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// help GC</span> p<span class="token punctuation">.</span>next <span class="token operator">=</span> null<span class="token punctuation">;</span> failed <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token keyword">return</span> interrupted<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 需要排队阻塞等待</span> <span class="token comment" spellcheck="true">// 如果在过程中线程中断,不响应中断</span> <span class="token comment" spellcheck="true">// 且继续排队获取资源,设置interrupted变量为true</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">shouldParkAfterFailedAcquire</span><span class="token punctuation">(</span>p<span class="token punctuation">,</span> node<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token function">parkAndCheckInterrupt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> interrupted <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>failed<span class="token punctuation">)</span> <span class="token function">cancelAcquire</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="2-无阻塞尝试获取"><a href="#2-无阻塞尝试获取" class="headerlink" title="(2) 无阻塞尝试获取"></a>(2) 无阻塞尝试获取</h3><p>ReentrantLock中的tryLock()的实现仅仅是非公平锁实现,实现逻辑基本与tryAcquire一致,不同的是没有通过hasQueuedPredecessors()检查CLH队列的head是否有其他线程在等待,这样当资源释放时,有线程请求资源能插队优先获取</p><p>ReentrantLock中tryLock()具体实现如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">tryLock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> sync<span class="token punctuation">.</span><span class="token function">nonfairTryAcquire</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">nonfairTryAcquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> acquires<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">final</span> Thread current <span class="token operator">=</span> Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">int</span> c <span class="token operator">=</span> <span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 没有任何线程持有锁</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 基于CAS设置state成功(期望的state旧值为0)</span> <span class="token comment" spellcheck="true">// 没有检查CLH队列中是否有线程在等待</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">compareAndSetState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> acquires<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>current<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 持有锁的线程为当前线程</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>current <span class="token operator">==</span> <span class="token function">getExclusiveOwnerThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 仅仅在当前线程,单线程,不用基于CAS更新</span> <span class="token keyword">int</span> nextc <span class="token operator">=</span> c <span class="token operator">+</span> acquires<span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>nextc <span class="token operator"><</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// overflow,整数溢出</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Maximum lock count exceeded"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setState</span><span class="token punctuation">(</span>nextc<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 其他线程已经持有锁</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="3-指定时间内阻塞等待获取"><a href="#3-指定时间内阻塞等待获取" class="headerlink" title="(3) 指定时间内阻塞等待获取"></a>(3) 指定时间内阻塞等待获取</h3><p>基本实现如下:</p><ul><li>1、ReentrantLock的tryLock(long, TimeUnit)调用AQS的tryAcquireNanos(int, long)方法</li><li>2、AQS的tryAcquireNanos先调用tryAcquire(int)尝试获取,获取不到再调用doAcquireNanos(int, long)方法</li><li>3、AQS的doAcquireNanos判断当前节点是否为head且基于tryAcquire(int)能否获得资源,如果不能获得且超时时间大于1微秒,则休眠一段时间后再尝试获取</li></ul><p>ReentrantLock中的实现如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">tryLock</span><span class="token punctuation">(</span><span class="token keyword">long</span> timeout<span class="token punctuation">,</span> TimeUnit unit<span class="token punctuation">)</span> <span class="token keyword">throws</span> InterruptedException <span class="token punctuation">{</span> <span class="token keyword">return</span> sync<span class="token punctuation">.</span><span class="token function">tryAcquireNanos</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">,</span> unit<span class="token punctuation">.</span><span class="token function">toNanos</span><span class="token punctuation">(</span>timeout<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">public</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">tryAcquireNanos</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">,</span> <span class="token keyword">long</span> nanosTimeout<span class="token punctuation">)</span> <span class="token keyword">throws</span> InterruptedException <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 如果线程已经被interrupt()方法设置中断 </span> <span class="token keyword">if</span> <span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">interrupted</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">InterruptedException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 先tryAcquire尝试获取锁 </span> <span class="token keyword">return</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span> <span class="token operator">||</span> <span class="token function">doAcquireNanos</span><span class="token punctuation">(</span>arg<span class="token punctuation">,</span> nanosTimeout<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>AQS中的实现如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">private</span> <span class="token keyword">boolean</span> <span class="token function">doAcquireNanos</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">,</span> <span class="token keyword">long</span> nanosTimeout<span class="token punctuation">)</span> <span class="token keyword">throws</span> InterruptedException <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>nanosTimeout <span class="token operator"><=</span> 0L<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 获取到资源的截止时间 </span> <span class="token keyword">final</span> <span class="token keyword">long</span> deadline <span class="token operator">=</span> System<span class="token punctuation">.</span><span class="token function">nanoTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> nanosTimeout<span class="token punctuation">;</span> <span class="token keyword">final</span> Node node <span class="token operator">=</span> <span class="token function">addWaiter</span><span class="token punctuation">(</span>Node<span class="token punctuation">.</span>EXCLUSIVE<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 标记是否成功拿到资源</span> <span class="token keyword">boolean</span> failed <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token punctuation">;</span><span class="token punctuation">;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 拿到前驱节点</span> <span class="token keyword">final</span> Node p <span class="token operator">=</span> node<span class="token punctuation">.</span><span class="token function">predecessor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 如果前驱是head,即本节点是第二个节点,才有资格去尝试获取资源</span> <span class="token comment" spellcheck="true">// 可能是head释放完资源唤醒本节点,也可能被interrupt()</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>p <span class="token operator">==</span> head <span class="token operator">&&</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 成功获取资源</span> <span class="token function">setHead</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// help GC</span> p<span class="token punctuation">.</span>next <span class="token operator">=</span> null<span class="token punctuation">;</span> failed <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 更新剩余超时时间</span> nanosTimeout <span class="token operator">=</span> deadline <span class="token operator">-</span> System<span class="token punctuation">.</span><span class="token function">nanoTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>nanosTimeout <span class="token operator"><=</span> 0L<span class="token punctuation">)</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 排队是否需要排队阻塞等待 </span> <span class="token comment" spellcheck="true">// 且超时时间大于1微秒,则线程休眠到超时时间到了再尝试获取</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">shouldParkAfterFailedAcquire</span><span class="token punctuation">(</span>p<span class="token punctuation">,</span> node<span class="token punctuation">)</span> <span class="token operator">&&</span> nanosTimeout <span class="token operator">></span> spinForTimeoutThreshold<span class="token punctuation">)</span> LockSupport<span class="token punctuation">.</span><span class="token function">parkNanos</span><span class="token punctuation">(</span><span class="token keyword">this</span><span class="token punctuation">,</span> nanosTimeout<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 如果线程已经被interrupt()方法设置中断</span> <span class="token comment" spellcheck="true">// 则不再排队,直接退出 </span> <span class="token keyword">if</span> <span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">interrupted</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">InterruptedException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>failed<span class="token punctuation">)</span> <span class="token function">cancelAcquire</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="4-响应中断获取"><a href="#4-响应中断获取" class="headerlink" title="(4) 响应中断获取"></a>(4) 响应中断获取</h3><p>ReentrantLock响应中断获取锁的方式是:当线程在park方法休眠中响应thead.interrupt()方法中断唤醒时,检查到线程中断标志位为true,主动抛出异常,核心实现在AQS的doAcquireInterruptibly(int)方法中</p><p>基本实现与阻塞等待获取类似,只是调用从AQS的acquire(int)方法,改为调用AQS的doAcquireInterruptibly(int)方法</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">doAcquireInterruptibly</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">)</span> <span class="token keyword">throws</span> InterruptedException <span class="token punctuation">{</span> <span class="token keyword">final</span> Node node <span class="token operator">=</span> <span class="token function">addWaiter</span><span class="token punctuation">(</span>Node<span class="token punctuation">.</span>EXCLUSIVE<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 标记是否成功拿到资源</span> <span class="token keyword">boolean</span> failed <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token punctuation">;</span><span class="token punctuation">;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 拿到前驱节点</span> <span class="token keyword">final</span> Node p <span class="token operator">=</span> node<span class="token punctuation">.</span><span class="token function">predecessor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 如果前驱是head,即本节点是第二个节点,才有资格去尝试获取资源</span> <span class="token comment" spellcheck="true">// 可能是head释放完资源唤醒本节点,也可能被interrupt()</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>p <span class="token operator">==</span> head <span class="token operator">&&</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 成功获取资源</span> <span class="token function">setHead</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> p<span class="token punctuation">.</span>next <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// help GC</span> failed <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token keyword">return</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 需要排队阻塞等待</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">shouldParkAfterFailedAcquire</span><span class="token punctuation">(</span>p<span class="token punctuation">,</span> node<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token comment" spellcheck="true">// 从排队阻塞中唤醒,如果检查到中断标志位为true</span> <span class="token function">parkAndCheckInterrupt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// 主动响应中断</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">InterruptedException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>failed<span class="token punctuation">)</span> <span class="token function">cancelAcquire</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="2-显式释放-1"><a href="#2-显式释放-1" class="headerlink" title="2 显式释放"></a>2 显式释放</h2><p>AQS资源共享方式分为独占式和共享式,这里先以ReentrantLock为例介绍独占式资源的显式释放,共享式后面会介绍到</p><p>与显式获取有类似之处,ReentrantLock显式释放基本实现如下:</p><ul><li>1、ReentrantLock实现AQS的tryRelease(int)方法,方法将state变量减1,如果state变成0代表没有任何线程持有锁,返回true,否则返回false</li><li>2、AQS的release(int)方法基于tryRelease(int)排队是否有任何线程持有资源,如果没有,则唤醒CLH队列中头节点的线程</li><li>3、被唤醒后的线程继续执行acquireQueued(Node,int)或者doAcquireNanos(int, long)或者doAcquireInterruptibly(int)中for(;;)中的逻辑,继续尝试获取资源</li></ul><p>ReentrantLock中tryRelease(int)方法实现如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">protected</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">tryRelease</span><span class="token punctuation">(</span><span class="token keyword">int</span> releases<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">int</span> c <span class="token operator">=</span> <span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> releases<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 只有持有锁的线程才有资格释放锁</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">!=</span> <span class="token function">getExclusiveOwnerThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">IllegalMonitorStateException</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 标识是否没有任何线程持有锁 </span> <span class="token keyword">boolean</span> free <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 没有任何线程持有锁</span> <span class="token comment" spellcheck="true">// 可重入锁每lock一次都需要对应一次unlock</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> free <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>null<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token function">setState</span><span class="token punctuation">(</span>c<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> free<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>AQS中的release(int)方法实现如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">release</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 尝试释放资源</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">tryRelease</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> Node h <span class="token operator">=</span> head<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 头节点不为空</span> <span class="token comment" spellcheck="true">// 后继节点入队后进入休眠状态之前,会将前驱节点的状态更新为SIGNAL(-1)</span> <span class="token comment" spellcheck="true">// 头节点状态为0,代表没有后继的等待节点</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>h <span class="token operator">!=</span> null <span class="token operator">&&</span> h<span class="token punctuation">.</span>waitStatus <span class="token operator">!=</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// 唤醒第二个节点</span> <span class="token comment" spellcheck="true">// 头节点是占用资源的线程,第二个节点才是首个等待资源的线程</span> <span class="token function">unparkSuccessor</span><span class="token punctuation">(</span>h<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="3-可重入-1"><a href="#3-可重入-1" class="headerlink" title="3 可重入"></a>3 可重入</h2><p>可重入的实现比较简单,以ReentrantLock为例,主要是在tryAcquire(int)方法中实现,持有锁的线程是不是当前线程,如果是,更新同步状态值state,并返回true,代表能获取锁</p><h2 id="4-可共享-1"><a href="#4-可共享-1" class="headerlink" title="4 可共享"></a>4 可共享</h2><p>可共享资源以ReentrantReadWriteLock为例,跟独占锁ReentrantLock的区别主要在于,获取的时候,多个线程允许共享读锁,当写锁释放时,多个阻塞等待读锁的线程能同时获取到</p><p>ReentrantReadWriteLock类中将AQS的state同步状态值定义为,高16位为读锁持有数,低16位为写锁持有锁</p><p>ReentrantReadWriteLock中tryAcquireShared(int)、tryReleaseShared(int)实现的逻辑较长,主要涉及读写互斥、可重入判断、读锁对写锁的让步,篇幅所限,这里就不展开了</p><p><strong>获取读锁(ReadLock.lock())主要实现如下</strong>:</p><ul><li>1、ReentrantReadWriteLock实现AQS的tryAcquireShared(int)方法,判断当前线程能否获得读锁</li><li>2、AQS的acquireShared(int)先基于tryAcquireShared(int)尝试获取资源,如果获取失败,则加入CLH队列排队阻塞等待</li><li>3、ReentrantReadWriteLock的ReadLock.lock()方法基于AQS的acquireShared(int)方法阻塞等待获取锁</li></ul><p>AQS共享模式获取资源的具体实现如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">final</span> <span class="token keyword">void</span> <span class="token function">acquireShared</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// tryAcquireShared返回负数代表获取共享资源失败</span> <span class="token comment" spellcheck="true">// 则通过进入等待队列,直到获取到资源为止才返回</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">tryAcquireShared</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span> <span class="token operator"><</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token function">doAcquireShared</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment" spellcheck="true">// 与前面介绍到的acquireQueued逻辑基本一致</span><span class="token comment" spellcheck="true">// 不同的是将tryAcquire改为tryAcquireShared</span><span class="token comment" spellcheck="true">// 还有资源获取成功后将传播给CLH队列上等待该资源的节点</span><span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">doAcquireShared</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">final</span> Node node <span class="token operator">=</span> <span class="token function">addWaiter</span><span class="token punctuation">(</span>Node<span class="token punctuation">.</span>SHARED<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 标记是否成功拿到资源</span> <span class="token keyword">boolean</span> failed <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token keyword">boolean</span> interrupted <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token punctuation">;</span><span class="token punctuation">;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">final</span> Node p <span class="token operator">=</span> node<span class="token punctuation">.</span><span class="token function">predecessor</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>p <span class="token operator">==</span> head<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">int</span> r <span class="token operator">=</span> <span class="token function">tryAcquireShared</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 资源获取成功</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>r <span class="token operator">>=</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 传播给CLH队列上等待该资源的节点 </span> <span class="token function">setHeadAndPropagate</span><span class="token punctuation">(</span>node<span class="token punctuation">,</span> r<span class="token punctuation">)</span><span class="token punctuation">;</span> p<span class="token punctuation">.</span>next <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// help GC</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>interrupted<span class="token punctuation">)</span> <span class="token function">selfInterrupt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> failed <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> <span class="token keyword">return</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 需要排队阻塞等待</span> <span class="token comment" spellcheck="true">// 如果在过程中线程中断,不响应中断</span> <span class="token comment" spellcheck="true">// 且继续排队获取资源,设置interrupted变量为true</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">shouldParkAfterFailedAcquire</span><span class="token punctuation">(</span>p<span class="token punctuation">,</span> node<span class="token punctuation">)</span> <span class="token operator">&&</span> <span class="token function">parkAndCheckInterrupt</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> interrupted <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token keyword">finally</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>failed<span class="token punctuation">)</span> <span class="token function">cancelAcquire</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token comment" spellcheck="true">// 资源传播给CLH队列上等待该资源的节点 </span><span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">setHeadAndPropagate</span><span class="token punctuation">(</span>Node node<span class="token punctuation">,</span> <span class="token keyword">int</span> propagate<span class="token punctuation">)</span> <span class="token punctuation">{</span> Node h <span class="token operator">=</span> head<span class="token punctuation">;</span> <span class="token function">setHead</span><span class="token punctuation">(</span>node<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>propagate <span class="token operator">></span> <span class="token number">0</span> <span class="token operator">||</span> h <span class="token operator">==</span> null <span class="token operator">||</span> h<span class="token punctuation">.</span>waitStatus <span class="token operator"><</span> <span class="token number">0</span> <span class="token operator">||</span> <span class="token punctuation">(</span>h <span class="token operator">=</span> head<span class="token punctuation">)</span> <span class="token operator">==</span> null <span class="token operator">||</span> h<span class="token punctuation">.</span>waitStatus <span class="token operator"><</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> Node s <span class="token operator">=</span> node<span class="token punctuation">.</span>next<span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>s <span class="token operator">==</span> null <span class="token operator">||</span> s<span class="token punctuation">.</span><span class="token function">isShared</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// 释放共享资源</span> <span class="token function">doReleaseShared</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><strong>释放读锁(ReadLock.unlock())主要实现如下</strong>:<br>ReentrantReadWriteLock共享资源的释放主要实现如下:</p><ul><li>1、ReentrantReadWriteLock实现AQS的tryReleaseShared(int)方法,判断读锁释放后是否还有线程持有读锁</li><li>2、AQS的releaseShared(int)基于tryReleaseShared(int)判断是否需要CLH队列中的休眠线程,如果需要就执行doReleaseShared()</li><li>3、ReentrantReadWriteLock的ReadLock.unlock()方法基于AQS的releaseShared(int)方法释放锁</li></ul><p>AQS共享模式释放资源具体实现如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">releaseShared</span><span class="token punctuation">(</span><span class="token keyword">int</span> arg<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 允许唤醒CLH中的休眠线程</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">tryReleaseShared</span><span class="token punctuation">(</span>arg<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 执行资源释放</span> <span class="token function">doReleaseShared</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">private</span> <span class="token keyword">void</span> <span class="token function">doReleaseShared</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">for</span> <span class="token punctuation">(</span><span class="token punctuation">;</span><span class="token punctuation">;</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> Node h <span class="token operator">=</span> head<span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>h <span class="token operator">!=</span> null <span class="token operator">&&</span> h <span class="token operator">!=</span> tail<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">int</span> ws <span class="token operator">=</span> h<span class="token punctuation">.</span>waitStatus<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 当前节点正在等待资源</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>ws <span class="token operator">==</span> Node<span class="token punctuation">.</span>SIGNAL<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 当前节点被其他线程唤醒了</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span><span class="token function">compareAndSetWaitStatus</span><span class="token punctuation">(</span>h<span class="token punctuation">,</span> Node<span class="token punctuation">.</span>SIGNAL<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">continue</span><span class="token punctuation">;</span> <span class="token function">unparkSuccessor</span><span class="token punctuation">(</span>h<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 进入else的条件是,当前节点刚刚成为头节点</span> <span class="token comment" spellcheck="true">// 尾节点刚刚加入CLH队列,还没在休眠前将前驱节点状态改为SIGNAL</span> <span class="token comment" spellcheck="true">// CAS失败是尾节点已经在休眠前将前驱节点状态改为SIGNAL</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>ws <span class="token operator">==</span> <span class="token number">0</span> <span class="token operator">&&</span> <span class="token operator">!</span><span class="token function">compareAndSetWaitStatus</span><span class="token punctuation">(</span>h<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">,</span> Node<span class="token punctuation">.</span>PROPAGATE<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">continue</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 每次唤醒后驱节点后,线程进入doAcquireShared方法,然后更新head</span> <span class="token comment" spellcheck="true">// 如果h变量在本轮循环中没有被改变,说明head == tail,队列中节点全部被唤醒</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>h <span class="token operator">==</span> head<span class="token punctuation">)</span> <span class="token keyword">break</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="5-公平、非公平-1"><a href="#5-公平、非公平-1" class="headerlink" title="5 公平、非公平"></a>5 公平、非公平</h2><p>这个特性实现比较简单,以ReentrantLock锁为例,公平锁直接基于AQS的acquire(int)获取资源,而非公平锁先尝试插队:基于CAS,期望state同步变量值为0(没有任何线程持有锁),更新为1,如果全部CAS更新失败再进行排队</p><pre class="line-numbers language-java"><code class="language-java"><span class="token comment" spellcheck="true">// 公平锁实现</span><span class="token keyword">final</span> <span class="token keyword">void</span> <span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">acquire</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment" spellcheck="true">// 非公平锁实现</span><span class="token keyword">final</span> <span class="token keyword">void</span> <span class="token function">lock</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 第1次CAS</span> <span class="token comment" spellcheck="true">// state值为0代表没有任何线程持有锁,直接插队获得锁</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">compareAndSetState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> <span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">else</span> <span class="token function">acquire</span><span class="token punctuation">(</span><span class="token number">1</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">protected</span> <span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">tryAcquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> acquires<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token function">nonfairTryAcquire</span><span class="token punctuation">(</span>acquires<span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment" spellcheck="true">// 在nonfairTryAcquire方法中再次CAS尝试获取锁</span><span class="token keyword">final</span> <span class="token keyword">boolean</span> <span class="token function">nonfairTryAcquire</span><span class="token punctuation">(</span><span class="token keyword">int</span> acquires<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">final</span> Thread current <span class="token operator">=</span> Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">int</span> c <span class="token operator">=</span> <span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>c <span class="token operator">==</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 第2次CAS尝试获取锁</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token function">compareAndSetState</span><span class="token punctuation">(</span><span class="token number">0</span><span class="token punctuation">,</span> acquires<span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">setExclusiveOwnerThread</span><span class="token punctuation">(</span>current<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">// 已经获得锁</span> <span class="token keyword">else</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>current <span class="token operator">==</span> <span class="token function">getExclusiveOwnerThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">int</span> nextc <span class="token operator">=</span> c <span class="token operator">+</span> acquires<span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>nextc <span class="token operator"><</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// overflow</span> <span class="token keyword">throw</span> <span class="token keyword">new</span> <span class="token class-name">Error</span><span class="token punctuation">(</span><span class="token string">"Maximum lock count exceeded"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token function">setState</span><span class="token punctuation">(</span>nextc<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> <span class="token boolean">true</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">return</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>AQS的state变量值的含义不一定代表资源,不同的AQS的继承类可以对state变量值有不同的定义</p><p>例如在countDownLatch类中,state变量值代表还需释放的latch计数(可以理解为需要打开的门闩数),需要每个门闩都打开,门才能打开,所有等待线程才会开始执行,每次countDown()就会对state变量减1,如果state变量减为0,则唤醒CLH队列中的休眠线程</p><p>学习类似底层源码建议先定几个问题,带着问题学习;通俗学习前建议先理解透彻整体设计,整体原理(可以先阅读相关文档资料),再研究和源码细节,避免一开始就扎进去源码,容易无功而返</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%B9%B6%E5%8F%91%E5%BA%95%E5%B1%82%E4%B9%8BAQS%E5%AE%9E%E7%8E%B0/4.png" alt></p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>理解Java内存模型</title>
<link href="/2019/10/13/li-jie-java-nei-cun-mo-xing/"/>
<url>/2019/10/13/li-jie-java-nei-cun-mo-xing/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/0.jpg" alt></p><p>最近重新学习一遍《深入学习Java虚拟机》,把之前Java内存模型中模糊的知识重新梳理一遍,这篇文章主要介绍模型产生的问题背景,解决的问题,处理思路,相关实现规则,环环相扣,希望读者看完这篇文章后能对Java内存模型体系产生一个相对清晰的理解,知其然而知其所以然。</p><h1 id="1-内存模型产生背景"><a href="#1-内存模型产生背景" class="headerlink" title="1 内存模型产生背景"></a>1 内存模型产生背景</h1><p>在介绍Java内存模型之前,我们先了解一下物理计算机中的并发问题,理解这些问题可以搞清楚内存模型产生的背景。物理机遇到的并发问题与虚拟机中的情况有不少相似之处,物理机的解决方案对虚拟机的实现有相当的参考意义。</p><h2 id="物理机的并发问题"><a href="#物理机的并发问题" class="headerlink" title="物理机的并发问题"></a>物理机的并发问题</h2><ul><li><strong>硬件的效率问题</strong></li></ul><p>计算机处理器处理绝大多数运行任务都不可能只靠处理器“计算”就能完成,处理器至少需要与<strong>内存交互</strong>,如读取运算数据、存储运算结果,这个I/O操作很难消除(无法仅靠寄存器完成所有运算任务)。</p><p>由于计算机的存储设备与处理器的运算速度有几个数量级的差距,为了避免处理器等待缓慢的内存读写操作完成,现代计算机系统通过加入一层读写速度尽可能接近处理器运算速度的高速缓存。缓存作为内存和处理器之间的缓冲:将运算需要使用到的数据复制到缓存中,让运算能快速运行,当运算结束后再从缓存同步回内存之中。<br><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/1.png" alt="CPU高速缓存"></p><ul><li><strong>缓存一致性问题</strong></li></ul><p>基于高速缓存的存储系统交互很好地解决了处理器与内存速度的矛盾,但是也为计算机系统带来更高的复杂度,因为引入了一个新问题:<strong>缓存一致性。</strong></p><p>在多处理器的系统中(或者单处理器多核的系统),每个处理器(每个核)都有自己的高速缓存,而它们有共享同一主内存(Main Memory)。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致。<br>为此,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议进行操作,来维护缓存的一致性。</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/2.png" alt="缓存一致性"></p><ul><li><strong>代码乱序执行优化问题</strong></li></ul><p>为了使得处理器内部的运算单元尽量被充分利用,提高运算效率,处理器可能会对输入的代码进行乱序执行,处理器会在计算之后将乱序执行的结果重组,<strong>乱序优化可以保证在单线程下该执行结果与顺序执行的结果是一致的</strong>,但不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/3.png" alt="代码执行乱序优化"><br>乱序执行技术是处理器为提高运算速度而做出违背代码原有顺序的优化。在单核时代,处理器保证做出的优化不会导致执行结果远离预期目标,但在多核环境下却并非如此。</p><p>多核环境下, 如果存在一个核的计算任务依赖另一个核 计的算任务的中间结果,而且对相关数据读写没做任何防护措施,那么其顺序性并不能靠代码的先后顺序来保证,处理器最终得出的结果和我们逻辑得到的结果可能会大不相同。</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/4.png" alt="代码乱序执行优化的问题"><br>以上图为例进行说明:CPU的core2中的逻辑B依赖core1中的逻辑A先执行</p><ul><li>正常情况下,逻辑A执行完之后再执行逻辑B。</li><li>在处理器乱序执行优化情况下,有可能导致flag提前被设置为true,导致逻辑B先于逻辑A执行。</li></ul><h1 id="2-Java内存模型的组成分析"><a href="#2-Java内存模型的组成分析" class="headerlink" title="2 Java内存模型的组成分析"></a>2 Java内存模型的组成分析</h1><h2 id="内存模型概念"><a href="#内存模型概念" class="headerlink" title="内存模型概念"></a>内存模型概念</h2><p>为了更好解决上面提到系列问题,内存模型被总结提出,我们可以把内存模型理解为<strong>在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象</strong>。</p><p>不同架构的物理计算机可以有不一样的内存模型,Java虚拟机也有自己的内存模型。Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,简称JMM)来<strong>屏蔽掉各种硬件和操作系统的内存访问差异</strong>,以实现<strong>让Java程序在各种平台下都能达到一致的内存访问效果</strong>,不必因为不同平台上的物理机的内存模型的差异,对各平台定制化开发程序。</p><p>更具体一点说,Java内存模型提出目标在于,<strong>定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节</strong>。此处的变量(Variables)与Java编程中所说的变量有所区别,它包括了实例字段、静态字段和构成数值对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的。(如果局部变量是一个reference类型,它引用的对象在Java堆中可被各个线程共享,但是reference本身在Java栈的局部变量表中,它是线程私有的)。</p><h2 id="Java内存模型的组成"><a href="#Java内存模型的组成" class="headerlink" title="Java内存模型的组成"></a>Java内存模型的组成</h2><ul><li><p>主内存<br>Java内存模型规定了所有变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件的主内存名字一样,两者可以互相类比,但此处仅是虚拟机内存的一部分)。</p></li><li><p>工作内存<br>每条线程都有自己的工作内存(Working Memory,又称本地内存,可与前面介绍的处理器高速缓存类比),线程的工作内存中保存了该线程使用到的变量的主内存中的共享变量的副本拷贝。<strong>工作内存是 JMM 的一个抽象概念,并不真实存在</strong>。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。</p></li></ul><p>Java内存模型抽象示意图如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/5.png" alt="Java内存模型抽象示意图"></p><h2 id="JVM内存操作的并发问题"><a href="#JVM内存操作的并发问题" class="headerlink" title="JVM内存操作的并发问题"></a>JVM内存操作的并发问题</h2><p>结合前面介绍的物理机的处理器处理内存的问题,可以类比总结出JVM内存操作的问题,下面介绍的Java内存模型的执行处理将围绕解决这2个问题展开:</p><ul><li><p><strong>1 工作内存数据一致性</strong><br>各个线程操作数据时会保存使用到的主内存中的共享变量副本,当多个线程的运算任务都涉及同一个共享变量时,将导致各自的的共享变量副本不一致,如果真的发生这种情况,数据同步回主内存以谁的副本数据为准?<br>Java内存模型主要通过一系列的数据同步协议、规则来保证数据的一致性,后面再详细介绍。</p></li><li><p><strong>2 指令重排序优化</strong><br>Java中重排序通常是编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。重排序分为两类:<strong>编译期重排序和运行期重排序</strong>,分别对应编译时和运行时环境。<br>同样的,指令重排序不是随意重排序,它需要满足以下两个条件: </p><ul><li>1 在单线程环境下不能改变程序运行的结果<br>即时编译器(和处理器)需要保证程序能够遵守 as-if-serial 属性。通俗地说,就是在单线程情况下,要给程序一个顺序执行的假象。即经过重排序的执行结果要与顺序执行的结果保持一致。</li><li>2 存在数据依赖关系的不允许重排序</li></ul></li></ul><p>多线程环境下,如果线程处理逻辑之间存在依赖关系,有可能因为指令重排序导致运行结果与预期不同,后面再展开Java内存模型如何解决这种情况。</p><h1 id="3-Java内存间的交互操作"><a href="#3-Java内存间的交互操作" class="headerlink" title="3 Java内存间的交互操作"></a>3 Java内存间的交互操作</h1><p>在理解Java内存模型的系列协议、特殊规则之前,我们先理解Java中内存间的交互操作。</p><h2 id="交互操作流程"><a href="#交互操作流程" class="headerlink" title="交互操作流程"></a>交互操作流程</h2><p>为了更好理解内存的交互操作,以线程通信为例,我们看看具体如何进行线程间值的同步:</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/6.png" alt="线程间交互操作"><br>线程1和线程2都有主内存中共享变量x的副本,初始时,这3个内存中x的值都为0。线程1中更新x的值为1之后同步到线程2主要涉及2个步骤:</p><ul><li>1 线程1把线程工作内存中更新过的x的值刷新到主内存中</li><li>2 线程2到主内存中读取线程1之前已更新过的x变量</li></ul><p>从整体上看,这2个步骤是线程1在向线程2发消息,这个通信过程必须经过主内存。线程对变量的所有操作(读取,赋值)都必须在<strong>工作内存中</strong>进行。不同线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成,实现各个线程共享变量的可见性。</p><h2 id="内存交互的基本操作"><a href="#内存交互的基本操作" class="headerlink" title="内存交互的基本操作"></a>内存交互的基本操作</h2><p>关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java内存模型中定义了下面介绍8种操作来完成。</p><p>虚拟机实现时必须保证下面介绍的每种操作都是原子的,不可再分的(对于double和long型的变量来说,load、store、read、和write操作在某些平台上允许有例外,后面会介绍)。</p><h3 id="8种基本操作"><a href="#8种基本操作" class="headerlink" title="8种基本操作"></a>8种基本操作</h3><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/7.png" alt="8种基本操作"></p><ul><li>lock (锁定)<br>作用于<strong>主内存</strong>的变量,它把一个变量标识为一条线程独占的状态。</li><li>unlock (解锁)<br>作用于<strong>主内存</strong>的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。</li><li>read (读取)<br>作用于<strong>主内存</strong>的变量,它把一个变量的值从主内存<strong>传输</strong>到线程的工作内存中,以便随后的load动作使用。</li><li>load (载入)<br>作用于<strong>工作内存</strong>的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。</li><li>use (使用)<br>作用于<strong>工作内存</strong>的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作。</li><li>assign (赋值)<br>作用于<strong>工作内存</strong>的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。</li><li>store (存储)<br>作用于<strong>工作内存</strong>的变量,它把工作内存中一个变量的值传送到主内存中,以便随后write操作使用。</li><li>write (写入)<br>作用于<strong>主内存</strong>的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。</li></ul><h1 id="4-Java内存模型运行规则"><a href="#4-Java内存模型运行规则" class="headerlink" title="4 Java内存模型运行规则"></a>4 Java内存模型运行规则</h1><h2 id="4-1-内存交互基本操作的3个特性"><a href="#4-1-内存交互基本操作的3个特性" class="headerlink" title="4.1 内存交互基本操作的3个特性"></a>4.1 内存交互基本操作的3个特性</h2><p>在介绍内存的交互的具体的8种基本操作之前,有必要先介绍一下操作的3个特性,Java内存模型是围绕着在并发过程中如何处理这3个特性来建立的,这里先给出定义和基本实现的简单介绍,后面会逐步展开分析。</p><ul><li><p><strong>原子性(Atomicity)</strong></p></li><li><p><em>即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行*</em>。即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。</p></li><li><p><strong>可见性(Visibility)</strong></p></li><li><p><em>是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值*</em>。<br>正如上面“交互操作流程”中所说明的一样,JMM是通过在线程1变量工作内存修改后将新值同步回主内存,线程2在变量读取前从主内存刷新变量值,这种<strong>依赖主内存作为传递媒介</strong>的方式来实现可见性。</p></li><li><p><strong>有序性(Ordering)</strong><br>有序性规则表现在以下两种场景: 线程内和线程间</p><ul><li>线程内<br>从某个线程的角度看方法的执行,指令会按照一种叫“串行”(as-if-serial)的方式执行,此种方式已经应用于顺序编程语言。</li><li>线程间<br>这个线程“观察”到其他线程并发地执行非同步的代码时,由于指令重排序优化,任何代码都有可能交叉执行。唯一起作用的约束是:对于同步方法,同步块(synchronized关键字修饰)以及volatile字段的操作仍维持相对有序。</li></ul></li></ul><p>Java内存模型的一系列运行规则看起来有点繁琐,但总结起来,是<strong>围绕原子性、可见性、有序性特征建立</strong>。归根究底,是为实现共享变量的在多个线程的工作内存的<strong>数据一致性</strong>,多线程并发,指令重排序优化的环境中程序能如预期运行。</p><h2 id="4-2-happens-before关系"><a href="#4-2-happens-before关系" class="headerlink" title="4.2 happens-before关系"></a>4.2 happens-before关系</h2><p>介绍系列规则之前,首先了解一下happens-before关系:用于描述下2个操作的内存可见性:<strong>如果操作A happens-before 操作B,那么A的结果对B可见</strong>。happens-before关系的分析需要分为<strong>单线程和多线程</strong>的情况:</p><ul><li><p><strong>单线程下的 happens-before</strong><br>字节码的先后顺序天然包含happens-before关系:因为单线程内共享一份工作内存,不存在数据一致性的问题。<br>在程序控制流路径中靠前的字节码 happens-before 靠后的字节码,即靠前的字节码执行完之后操作结果对靠后的字节码可见。然而,这并不意味着前者一定在后者之前执行。实际上,如果后者不依赖前者的运行结果,那么它们可能会被重排序。</p></li><li><p><strong>多线程下的 happens-before</strong><br>多线程由于每个线程有共享变量的副本,如果没有对共享变量做同步处理,线程1更新执行操作A共享变量的值之后,线程2开始执行操作B,此时操作A产生的结果对操作B不一定可见。</p></li></ul><p>为了方便程序开发,Java内存模型实现了下述支持happens-before关系的操作:</p><ul><li>程序次序规则<br>一个线程内,按照代码顺序,书写在前面的操作 happens-before 书写在后面的操作。</li><li>锁定规则<br>一个unLock操作 happens-before 后面对同一个锁的lock操作。</li><li>volatile变量规则<br>对一个变量的写操作 happens-before 后面对这个变量的读操作。</li><li>传递规则<br>如果操作A happens-before 操作B,而操作B又 happens-before 操作C,则可以得出操作A happens-before 操作C。</li><li>线程启动规则<br>Thread对象的start()方法 happens-before 此线程的每个一个动作。</li><li>线程中断规则<br>对线程interrupt()方法的调用 happens-before 被中断线程的代码检测到中断事件的发生。</li><li>线程终结规则<br>线程中所有的操作都 happens-before 线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。</li><li>对象终结规则<br>一个对象的初始化完成 happens-before 他的finalize()方法的开始</li></ul><h2 id="4-3-内存屏障"><a href="#4-3-内存屏障" class="headerlink" title="4.3 内存屏障"></a>4.3 内存屏障</h2><p>Java中如何保证底层操作的有序性和可见性?可以通过内存屏障。</p><p>内存屏障是被插入两个CPU指令之间的一种指令,用来禁止处理器指令发生重排序(像屏障一样),从而保障<strong>有序性</strong>的。另外,为了达到屏障的效果,它也会使处理器写入、读取值之前,将主内存的值写入高速缓存,清空无效队列,从而保障<strong>可见性</strong>。</p><p>举个例子:</p><pre class="line-numbers language-java"><code class="language-java">Store1<span class="token punctuation">;</span> Store2<span class="token punctuation">;</span> Load1<span class="token punctuation">;</span> StoreLoad<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//内存屏障</span>Store3<span class="token punctuation">;</span> Load2<span class="token punctuation">;</span> Load3<span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>对于上面的一组CPU指令(Store表示写入指令,Load表示读取指令),StoreLoad屏障之前的Store指令无法与StoreLoad屏障之后的Load指令进行交换位置,即<strong>重排序</strong>。但是StoreLoad屏障之前和之后的指令是可以互换位置的,即Store1可以和Store2互换,Load2可以和Load3互换。</p><p>常见有4种屏障</p><ul><li>LoadLoad屏障:<br>对于这样的语句 Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。</li><li>StoreStore屏障:<br>对于这样的语句 Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。</li><li>LoadStore屏障:<br>对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被执行前,保证Load1要读取的数据被读取完毕。</li><li>StoreLoad屏障:<br>对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的(冲刷写缓冲器,清空无效化队列)。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。</li></ul><p>Java中对内存屏障的使用在一般的代码中不太容易见到,常见的有volatile和synchronized关键字修饰的代码块(后面再展开介绍),还可以通过Unsafe这个类来使用内存屏障。</p><h2 id="4-4-8种操作同步的规则"><a href="#4-4-8种操作同步的规则" class="headerlink" title="4.4 8种操作同步的规则"></a>4.4 8种操作同步的规则</h2><p>JMM在执行前面介绍8种基本操作时,为了保证内存间数据一致性,JMM中规定需要满足以下规则:</p><ul><li>规则1:如果要把一个变量从主内存中复制到工作内存,就需要按顺序的执行 read 和 load 操作,如果把变量从工作内存中同步回主内存中,就要按顺序的执行 store 和 write 操作。但 Java 内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。</li><li>规则2:不允许 read 和 load、store 和 write 操作之一单独出现。</li><li>规则3:不允许一个线程丢弃它的最近 assign 的操作,即变量在工作内存中改变了之后必须同步到主内存中。</li><li>规则4:不允许一个线程无原因的(没有发生过任何 assign 操作)把数据从工作内存同步回主内存中。</li><li>规则5:一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load 或 assign )的变量。即就是对一个变量实施 use 和 store 操作之前,必须先执行过了 load 或 assign 操作。</li><li>规则6:一个变量在同一个时刻只允许一条线程对其进行 lock 操作,但 lock 操作可以被同一条线程重复执行多次,多次执行 lock 后,只有执行相同次数的 unlock 操作,变量才会被解锁。所以 lock 和 unlock 必须成对出现。</li><li>规则7:如果对一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行 load 或 assign 操作初始化变量的值。</li><li>规则8:如果一个变量事先没有被 lock 操作锁定,则不允许对它执行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量。</li><li>规则9:对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作)</li></ul><p>看起来这些规则有些繁琐,其实也不难理解:</p><ul><li>规则1、规则2<br>工作内存中的共享变量作为主内存的副本,主内存变量的值同步到工作内存需要read和load一起使用,工作内存中的变量的值同步回主内存需要store和write一起使用,这2组操作各自都是是一个固定的有序搭配,不允许单独出现。</li><li>规则3、规则4<br>由于工作内存中的共享变量是主内存的副本,为保证数据一致性,当工作内存中的变量被字节码引擎重新赋值,必须同步回主内存。如果工作内存的变量没有被更新,不允许无原因同步回主内存。</li><li>规则5<br>由于工作内存中的共享变量是主内存的副本,必须从主内存诞生。</li><li>规则6、7、8、9<br>为了并发情况下安全使用变量,线程可以基于lock操作独占主内存中的变量,其他线程不允许使用或unlock该变量,直到变量被线程unlock。</li></ul><h2 id="4-5-volatile型变量的特殊规则"><a href="#4-5-volatile型变量的特殊规则" class="headerlink" title="4.5 volatile型变量的特殊规则"></a>4.5 volatile型变量的特殊规则</h2><p>volatile的中文意思是不稳定的,易变的,用volatile修饰变量是为了保证变量的可见性。</p><h3 id="volatile的语义"><a href="#volatile的语义" class="headerlink" title="volatile的语义"></a>volatile的语义</h3><p>volatile主要有下面2种语义</p><h3 id="语义1-保证可见性"><a href="#语义1-保证可见性" class="headerlink" title="语义1 保证可见性"></a>语义1 保证可见性</h3><p>保证了不同线程对该变量操作的内存可见性。</p><p>这里保证可见性是不等同于volatile变量并发操作的安全性,保证可见性具体一点解释:</p><p><strong>线程写volatile变量的过程:</strong></p><ul><li>1 改变线程工作内存中volatile变量副本的值</li><li>2 将改变后的副本的值从工作内存刷新到主内存</li></ul><p><strong>线程读volatile变量的过程:</strong></p><ul><li>1 从主内存中读取volatile变量的最新值到线程的工作内存中</li><li>2 从工作内存中读取volatile变量的副本</li></ul><p>但是如果多个线程同时把更新后的变量值同时刷新回主内存,可能导致得到的值不是预期结果:</p><p>举个例子:<br>定义volatile int count = 0,2个线程同时执行count++操作,每个线程都执行500次,最终结果小于1000,原因是每个线程执行count++需要以下3个步骤:</p><ul><li>步骤1 线程从主内存读取最新的count的值</li><li>步骤2 执行引擎把count值加1,并赋值给线程工作内存</li><li>步骤3 线程工作内存把count值保存到主内存<br>有可能某一时刻2个线程在步骤1读取到的值都是100,执行完步骤2得到的值都是101,最后刷新了2次101保存到主内存。</li></ul><h3 id="语义2-禁止进行指令重排序"><a href="#语义2-禁止进行指令重排序" class="headerlink" title="语义2 禁止进行指令重排序"></a>语义2 禁止进行指令重排序</h3><p>具体一点解释,禁止重排序的规则如下:</p><ul><li>当程序执行到 volatile变量的读操作或者写操作时,在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行;</li><li>在进行指令优化时,不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。</li></ul><p>普通的变量仅仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证赋值操作的顺序与程序代码中的执行顺序一致。</p><p>举个例子:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">volatile</span> <span class="token keyword">boolean</span> initialized <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// 下面代码线程A中执行</span><span class="token comment" spellcheck="true">// 读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用</span><span class="token function">doSomethingReadConfg</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>initialized <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span><span class="token comment" spellcheck="true">// 下面代码线程B中执行</span><span class="token comment" spellcheck="true">// 等待initialized 为true,代表线程A已经把配置信息初始化完成</span><span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token operator">!</span>initialized<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">sleep</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment" spellcheck="true">// 使用线程A初始化好的配置信息</span><span class="token function">doSomethingWithConfig</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>上面代码中如果定义initialized变量时没有使用volatile修饰,就有可能会由于指令重排序的优化,导致线程A中最后一句代码 “initialized = true” 在 “doSomethingReadConfg()” 之前被执行,这样会导致线程B中使用配置信息的代码就可能出现错误,而volatile关键字就禁止重排序的语义可以避免此类情况发生。</p><h3 id="volatile型变量实现原理"><a href="#volatile型变量实现原理" class="headerlink" title="volatile型变量实现原理"></a>volatile型变量实现原理</h3><p>具体实现方式是在编译期生成字节码时,会在指令序列中增加内存屏障来保证,下面是基于保守策略的JMM内存屏障插入策略:</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/8.png" alt="volatile型变量内存屏障插入策略"></p><ul><li><p>在每个volatile写操作的前面插入一个StoreStore屏障。<br>该屏障除了保证了屏障之前的写操作和该屏障之后的写操作不能重排序,还会保证了volatile写操作之前,任何的读写操作都会先于volatile被提交。</p></li><li><p>在每个volatile写操作的后面插入一个StoreLoad屏障。<br>该屏障除了使volatile写操作不会与之后的读操作重排序外,还会刷新处理器缓存,使volatile变量的写更新对其他线程可见。</p></li><li><p>在每个volatile读操作的后面插入一个LoadLoad屏障。<br>该屏障除了使volatile读操作不会与之前的写操作发生重排序外,还会刷新处理器缓存,使volatile变量读取的为最新值。</p></li><li><p>在每个volatile读操作的后面插入一个LoadStore屏障。<br>该屏障除了禁止了volatile读操作与其之后的任何写操作进行重排序,还会刷新处理器缓存,使其他线程volatile变量的写更新对volatile读操作的线程可见。</p></li></ul><h3 id="volatile型变量使用场景"><a href="#volatile型变量使用场景" class="headerlink" title="volatile型变量使用场景"></a>volatile型变量使用场景</h3><p>总结起来,就是“一次写入,到处读取”,某一线程负责更新变量,其他线程只读取变量(不更新变量),并根据变量的新值执行相应逻辑。例如状态标志位更新,观察者模型变量值发布。 </p><h2 id="4-6-final型变量的特殊规则"><a href="#4-6-final型变量的特殊规则" class="headerlink" title="4.6 final型变量的特殊规则"></a>4.6 final型变量的特殊规则</h2><p>我们知道,final成员变量必须在声明的时候初始化或者在构造器中初始化,否则就会报编译错误。<br>final关键字的可见性是指:被final修饰的字段在声明时或者构造器中,一旦初始化完成,那么在其他线程无须同步就能正确看见final字段的值。这是因为一旦初始化完成,final变量的值立刻回写到主内存。</p><h2 id="4-7-synchronized的特殊规则"><a href="#4-7-synchronized的特殊规则" class="headerlink" title="4.7 synchronized的特殊规则"></a>4.7 synchronized的特殊规则</h2><p>通过 synchronized关键字包住的代码区域,对数据的读写进行控制:</p><ul><li>读数据<br>当线程进入到该区域读取变量信息时,对数据的读取也不能从工作内存读取,只能从内存中读取,保证读到的是最新的值。</li><li>写数据<br>在同步区内对变量的写入操作,在离开同步区时就将当前线程内的数据刷新到内存中,保证更新的数据对其他线程的可见性。</li></ul><h2 id="4-8-long和double型变量的特殊规则"><a href="#4-8-long和double型变量的特殊规则" class="headerlink" title="4.8 long和double型变量的特殊规则"></a>4.8 long和double型变量的特殊规则</h2><p>Java内存模型要求lock、unlock、read、load、assign、use、store、write这8种操作都具有原子性,但是对于64位的数据类型(long和double),在模型中特别定义相对宽松的规定:允许虚拟机将没有被volatile修饰的64位数据的读写操作分为2次32位的操作来进行。也就是说虚拟机可选择不保证64位数据类型的load、store、read和write这4个操作的原子性。由于这种非原子性,有可能导致其他线程读到同步未完成的“32位的半个变量”的值。</p><p>不过实际开发中,Java内存模型强烈建议虚拟机把64位数据的读写实现为具有原子性,目前各种平台下的商用虚拟机都选择把64位数据的读写操作作为原子操作来对待,因此我们在编写代码时一般不需要把用到的long和double变量专门声明为volatile。</p><h1 id="5-总结"><a href="#5-总结" class="headerlink" title="5 总结"></a>5 总结</h1><p>由于Java内存模型涉及系列规则,网上的文章大部分就是对这些规则进行解析,但是很多没有解释为什么需要这些规则,这些规则的作用,其实这是不利于初学者学习的,容易绕进去这些繁琐规则不知所以然,下面谈谈我的一点学习知识的个人体会:</p><p><strong>学习知识的过程不是等同于只是理解知识和记忆知识,而是要对知识解决的问题的输入和输出建立连接</strong>,知识的本质是解决问题,所以在学习之前要理解问题,理解这个问题要的输出和输出,而知识就是输入到输出的一个关系映射。知识的学习要结合大量的例子来<strong>理解这个映射关系,然后压缩知识</strong>,华罗庚说过:“把一本书读厚,然后再读薄”,解释的就是这个道理,先结合大量的例子理解知识,然后再压缩知识。</p><p>以学习Java内存模型为例:</p><ul><li>理解问题,明确输入输出<br>首先理解Java内存模型是什么,有什么用,解决什么问题</li><li>理解内存模型系列协议<br>结合大量例子理解这些协议规则</li><li>压缩知识<br>大量规则其实就是通过数据同步协议,保证内存副本之间的数据一致性,同时防止重排序对程序的影响。</li></ul><p>希望对大家有帮助。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p>《深入学习Java虚拟机》</p><p><a href="https://time.geekbang.org/column/intro/108?code=XamkmJYooKBPKe8hT7otClsFDeVHb6rptMYHTdgiRPU%3D" target="_blank" rel="noopener">深入拆解Java虚拟机</a></p><p><a href="https://time.geekbang.org/column/article/13484?code=XamkmJYooKBPKe8hT7otClsFDeVHb6rptMYHTdgiRPU%3D" target="_blank" rel="noopener">Java核心技术36讲</a></p><p><a href="http://gee.cs.oswego.edu/dl/cpj/jmm.html" target="_blank" rel="noopener">Synchronization and the Java Memory Model ——Doug Lea</a></p><p><a href="https://www.infoq.cn/article/java-memory-model-1" target="_blank" rel="noopener">深入理解 Java 内存模型</a></p><p><a href="https://blog.csdn.net/kuangzhanshatian/article/details/47949059" target="_blank" rel="noopener">Java内存屏障和可见性</a></p><p><a href="https://www.jianshu.com/p/43af2cc32f90" target="_blank" rel="noopener">内存屏障与synchronized、volatile的原理</a></p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/9.png" alt></p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>理解Java-GC原理和调优</title>
<link href="/2019/10/13/li-jie-java-gc-yuan-li-he-diao-you/"/>
<url>/2019/10/13/li-jie-java-gc-yuan-li-he-diao-you/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/0.jpg" alt></p><h1 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h1><p>本文介绍GC基础原理和理论,GC调优方法思路和方法,基于Hotspot jdk1.8,学习之后将了解如何对生产系统出现的GC问题进行排查解决</p><p>阅读时长约30分钟,内容主要如下:</p><ul><li>GC基础原理,涉及调优目标,GC事件分类、JVM内存分配策略、GC日志分析等</li><li>CMS原理及调优</li><li>G1原理及调优</li><li>GC问题排查和解决思路</li></ul><h1 id="GC基础原理"><a href="#GC基础原理" class="headerlink" title="GC基础原理"></a>GC基础原理</h1><h2 id="1-GC调优目标"><a href="#1-GC调优目标" class="headerlink" title="1 GC调优目标"></a>1 GC调优目标</h2><p>大多数情况下对 Java 程序进行GC调优, 主要关注两个目标:响应速度、吞吐量</p><ul><li><p><strong>响应速度(Responsiveness)</strong><br>响应速度指程序或系统对一个请求的响应有多迅速。比如,用户订单查询响应时间,对响应速度要求很高的系统,较大的停顿时间是不可接受的。调优的重点是在短的时间内快速响应</p></li><li><p><strong>吞吐量(Throughput)</strong><br>吞吐量关注在一个特定时间段内应用系统的最大工作量,例如每小时批处理系统能完成的任务数量,在吞吐量方面优化的系统,较长的GC停顿时间也是可以接受的,因为高吞吐量应用更关心的是如何尽可能快地完成整个任务,不考虑快速响应用户请求</p></li></ul><p>GC调优中,GC导致的应用暂停时间影响系统响应速度,GC处理线程的CPU使用率影响系统吞吐量</p><h2 id="2-GC分代收集算法"><a href="#2-GC分代收集算法" class="headerlink" title="2 GC分代收集算法"></a>2 GC分代收集算法</h2><p>现代的垃圾收集器基本都是采用分代收集算法,其主要思想:<br>将Java的堆内存逻辑上分成两块:新生代、老年代,针对不同存活周期、不同大小的对象采取不同的垃圾回收策略</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/1.png" alt="分代收集算法"></p><ul><li>新生代(Young Generation)</li></ul><p>新生代又叫年轻代,大多数对象在新生代中被创建,很多对象的生命周期很短。每次新生代的垃圾回收(又称Young GC、Minor GC、YGC)后只有少量对象存活,所以使用复制算法,只需少量的复制操作成本就可以完成回收</p><p>新生代内又分三个区:一个Eden区,两个Survivor区(S0、S1,又称From Survivor、To Survivor),大部分对象在Eden区中生成。当Eden区满时,还存活的对象将被复制到两个Survivor区(中的一个)。当这个Survivor区满时,此区的存活且不满足晋升到老年代条件的对象将被复制到另外一个Survivor区。对象每经历一次复制,年龄加1,达到晋升年龄阈值后,转移到老年代</p><ul><li>老年代(Old Generation)</li></ul><p>在新生代中经历了N次垃圾回收后仍然存活的对象,就会被放到老年代,该区域中对象存活率高。老年代的垃圾回收通常使用“标记-整理”算法</p><h2 id="3-GC事件分类"><a href="#3-GC事件分类" class="headerlink" title="3 GC事件分类"></a>3 GC事件分类</h2><p>根据垃圾收集回收的区域不同,垃圾收集主要通常分为Young GC、Old GC、Full GC、Mixed GC</p><h3 id="1-Young-GC"><a href="#1-Young-GC" class="headerlink" title="(1) Young GC"></a>(1) Young GC</h3><p>新生代内存的垃圾收集事件称为Young GC(又称Minor GC),当JVM无法为新对象分配在新生代内存空间时总会触发 Young GC,比如 Eden 区占满时。新对象分配频率越高, Young GC 的频率就越高</p><p>Young GC 每次都会引起全线停顿(Stop-The-World),暂停所有的应用线程,停顿时间相对老年代GC的造成的停顿,几乎可以忽略不计</p><h3 id="2-Old-GC-、Full-GC、Mixed-GC"><a href="#2-Old-GC-、Full-GC、Mixed-GC" class="headerlink" title="(2) Old GC 、Full GC、Mixed GC"></a>(2) Old GC 、Full GC、Mixed GC</h3><p><strong>Old GC</strong>,只清理老年代空间的GC事件,只有CMS的并发收集是这个模式<br><strong>Full GC</strong>,清理整个堆的GC事件,包括新生代、老年代、元空间等</p><ul><li><strong>Mixed GC</strong>,清理整个新生代以及部分老年代的GC,只有G1有这个模式</li></ul><h2 id="4-GC日志分析"><a href="#4-GC日志分析" class="headerlink" title="4 GC日志分析"></a>4 GC日志分析</h2><p>GC日志是一个很重要的工具,它准确记录了每一次的GC的执行时间和执行结果,通过分析GC日志可以调优堆设置和GC设置,或者改进应用程序的对象分配模式,开启的JVM启动参数如下:</p><pre><code>-verbose:gc -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCTimeStamps</code></pre><p>常见的Young GC、Full GC日志含义如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/2.png" alt="Young GC"><br><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/3.png" alt="Full GC"></p><p>免费的GC日志图形分析工具推荐下面2个:</p><ul><li><a href="[https://github.com/chewiebug/GCViewer](https://github.com/chewiebug/GCViewer)">GCViewer</a>,下载jar包直接运行</li><li><a href="https://gceasy.io/" target="_blank" rel="noopener">gceasy</a>,web工具,上传GC日志在线使用</li></ul><h2 id="5-内存分配策略"><a href="#5-内存分配策略" class="headerlink" title="5 内存分配策略"></a>5 内存分配策略</h2><p>Java提供的自动内存管理,可以归结为解决了对象的内存分配和回收的问题,前面已经介绍了内存回收,下面介绍几条最普遍的内存分配策略</p><ul><li><p><strong>对象优先在Eden区分配</strong><br>大多数情况下,对象在先新生代Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Young GC</p></li><li><p><strong>大对象之间进入老年代</strong><br>JVM提供了一个对象大小阈值参数(-XX:PretenureSizeThreshold,默认值为0,代表不管多大都是先在Eden中分配内存),大于参数设置的阈值值的对象直接在老年代分配,这样可以避免对象在Eden及两个Survivor直接发生大内存复制</p></li><li><p><strong>长期存活的对象将进入老年代</strong><br>对象每经历一次垃圾回收,且没被回收掉,它的年龄就增加1,大于年龄阈值参数(-XX:MaxTenuringThreshold,默认15)的对象,将晋升到老年代中</p></li><li><p><strong>空间分配担保</strong><br>当进行Young GC之前,JVM需要预估:老年代是否能够容纳Young GC后新生代晋升到老年代的存活对象,以确定是否需要提前触发GC回收老年代空间,基于空间分配担保策略来计算:</p></li></ul><p><strong>continueSize:老年代最大可用连续空间</strong></p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/4.png" alt="空间分配担保"></p><p>Young GC之后如果成功(Young GC后晋升对象能放入老年代),则代表担保成功,不用再进行Full GC,提高性能;如果失败,则会出现“promotion failed”错误,代表担保失败,需要进行Full GC</p><ul><li><strong>动态年龄判定</strong><br>新生代对象的年龄可能没达到阈值(MaxTenuringThreshold参数指定)就晋升老年代,如果Young GC之后,新生代存活对象<strong>达到相同年龄所有对象大小</strong>的总和大于任一Survivor空间(S0 或 S1总空间)的一半,此时S0或者S1区即将容纳不了存活的新生代对象,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄</li></ul><p>另外,如果Young GC后S0或S1区不足以容纳:未达到晋升老年代条件的新生代存活对象,会导致这些存活对象直接进入老年代,需要尽量避免</p><h1 id="CMS原理及调优"><a href="#CMS原理及调优" class="headerlink" title="CMS原理及调优"></a>CMS原理及调优</h1><h2 id="1-名词解释"><a href="#1-名词解释" class="headerlink" title="1 名词解释"></a>1 名词解释</h2><p><strong>可达性分析算法</strong>:用于判断对象是否存活,基本思想是通过一系列称为“GC Root”的对象作为起点(常见的GC Root有系统类加载器、栈中的对象、处于激活状态的线程等),基于对象引用关系,从GC Roots开始向下搜索,所走过的路径称为引用链,当一个对象到GC Root没有任何引用链相连,证明对象不再存活</p><p><strong>Stop The World</strong>:GC过程中分析对象引用关系,为了保证分析结果的准确性,需要通过停顿所有Java执行线程,保证引用关系不再动态变化,该停顿事件称为Stop The World(STW)</p><p><strong>Safepoint</strong>:代码执行过程中的一些特殊位置,当线程执行到这些位置的时候,说明虚拟机当前的状态是安全的,如果有需要GC,线程可以在这个位置暂停。HotSpot采用主动中断的方式,让执行线程在运行期轮询是否需要暂停的标志,若需要则中断挂起</p><h2 id="2-CMS简介"><a href="#2-CMS简介" class="headerlink" title="2 CMS简介"></a>2 CMS简介</h2><p>CMS(Concurrent Mark and Swee 并发-标记-清除),是一款基于并发、使用标记清除算法的垃圾回收算法,只针对老年代进行垃圾回收。CMS收集器工作时,尽可能让GC线程和用户线程并发执行,以达到降低STW时间的目的</p><p>通过以下命令行参数,启用CMS垃圾收集器:</p><pre><code>-XX:+UseConcMarkSweepGC</code></pre><p>值得补充的是,下面介绍到的CMS GC是指老年代的GC,而Full GC指的是整个堆的GC事件,包括新生代、老年代、元空间等,两者有所区分</p><h2 id="3-新生代垃圾回收"><a href="#3-新生代垃圾回收" class="headerlink" title="3 新生代垃圾回收"></a>3 新生代垃圾回收</h2><p>能与CMS搭配使用的新生代垃圾收集器有Serial收集器和ParNew收集器。这2个收集器都采用标记复制算法,都会触发STW事件,停止所有的应用线程。不同之处在于,Serial是单线程执行,ParNew是多线程执行</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/5.png" alt="新生代"> </p><h2 id="4-老年代垃圾回收"><a href="#4-老年代垃圾回收" class="headerlink" title="4 老年代垃圾回收"></a>4 老年代垃圾回收</h2><p>CMS GC以获取最小停顿时间为目的,尽可能减少STW时间,可以分为7个阶段</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/6.png" alt="CMS 7个阶段"></p><ul><li><strong>阶段 1: 初始标记(Initial Mark)</strong></li></ul><p>此阶段的目标是标记老年代中所有存活的对象, 包括 GC Root 的直接引用, 以及由新生代中存活对象所引用的对象,触发第一次STW事件</p><p>这个过程是支持多线程的(JDK7之前单线程,JDK8之后并行,可通过参数CMSParallelInitialMarkEnabled调整)</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/7.png" alt="初始标记"></p><ul><li><strong>阶段 2: 并发标记(Concurrent Mark)</strong></li></ul><p>此阶段GC线程和应用线程并发执行,遍历阶段1初始标记出来的存活对象,然后继续递归标记这些对象可达的对象</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/8.png" alt="并发标记"></p><ul><li><strong>阶段 3: 并发预清理(Concurrent Preclean)</strong></li></ul><p>此阶段GC线程和应用线程也是并发执行,因为阶段2是与应用线程并发执行,可能有些引用关系已经发生改变。<br>通过卡片标记(Card Marking),提前把老年代空间逻辑划分为相等大小的区域(Card),如果引用关系发生改变,JVM会将发生改变的区域标记位“脏区”(Dirty Card),然后在本阶段,这些脏区会被找出来,刷新引用关系,清除“脏区”标记</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/9.png" alt="并发预清理"></p><ul><li><strong>阶段 4: 并发可取消的预清理(Concurrent Abortable Preclean)</strong></li></ul><p>此阶段也不停止应用线程. 本阶段尝试在 STW 的 最终标记阶段(Final Remark)之前尽可能地多做一些工作,以减少应用暂停时间<br>在该阶段不断循环处理:标记老年代的可达对象、扫描处理Dirty Card区域中的对象,循环的终止条件有:<br>1 达到循环次数<br>2 达到循环执行时间阈值<br>3 新生代内存使用率达到阈值</p><ul><li><strong>阶段 5: 最终标记(Final Remark)</strong></li></ul><p>这是GC事件中第二次(也是最后一次)STW阶段,目标是完成老年代中所有存活对象的标记。在此阶段执行:<br>1 遍历新生代对象,重新标记<br>2 根据GC Roots,重新标记<br>3 遍历老年代的Dirty Card,重新标记</p><ul><li><strong>阶段 6: 并发清除(Concurrent Sweep)</strong></li></ul><p>此阶段与应用程序并发执行,不需要STW停顿,根据标记结果清除垃圾对象</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/10.png" alt="并发清除"></p><ul><li><strong>阶段 7: 并发重置(Concurrent Reset)</strong></li></ul><p>此阶段与应用程序并发执行,重置CMS算法相关的内部数据, 为下一次GC循环做准备</p><h2 id="5-CMS常见问题"><a href="#5-CMS常见问题" class="headerlink" title="5 CMS常见问题"></a>5 CMS常见问题</h2><h4 id="最终标记阶段停顿时间过长问题"><a href="#最终标记阶段停顿时间过长问题" class="headerlink" title="最终标记阶段停顿时间过长问题"></a>最终标记阶段停顿时间过长问题</h4><p>CMS的GC停顿时间约80%都在最终标记阶段(Final Remark),若该阶段停顿时间过长,常见原因是新生代对老年代的无效引用,在上一阶段的并发可取消预清理阶段中,执行阈值时间内未完成循环,来不及触发Young GC,清理这些无效引用</p><p>通过添加参数:-XX:+CMSScavengeBeforeRemark。在执行最终操作之前先触发Young GC,从而减少新生代对老年代的无效引用,降低最终标记阶段的停顿,但如果在上个阶段(并发可取消的预清理)已触发Young GC,也会重复触发Young GC</p><h4 id="并发模式失败-concurrent-mode-failure-amp-晋升失败-promotion-failed-问题"><a href="#并发模式失败-concurrent-mode-failure-amp-晋升失败-promotion-failed-问题" class="headerlink" title="并发模式失败(concurrent mode failure) & 晋升失败(promotion failed)问题"></a>并发模式失败(concurrent mode failure) & 晋升失败(promotion failed)问题</h4><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/11.png" alt="并发模式失败"><br><strong>并发模式失败</strong>:当CMS在执行回收时,新生代发生垃圾回收,同时老年代又没有足够的空间容纳晋升的对象时,CMS 垃圾回收就会退化成单线程的Full GC。所有的应用线程都会被暂停,老年代中所有的无效对象都被回收</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/12.png" alt="晋升失败"><br><strong>晋升失败</strong>:当新生代发生垃圾回收,老年代有足够的空间可以容纳晋升的对象,但是由于空闲空间的碎片化,导致晋升失败,此时会触发单线程且带压缩动作的Full GC</p><p>并发模式失败和晋升失败都会导致长时间的停顿,常见解决思路如下:</p><ul><li>降低触发CMS GC的阈值,即参数-XX:CMSInitiatingOccupancyFraction的值,让CMS GC尽早执行,以保证有足够的空间</li><li>增加CMS线程数,即参数-XX:ConcGCThreads,</li><li>增大老年代空间</li><li>让对象尽量在新生代回收,避免进入老年代</li></ul><h4 id="内存碎片问题"><a href="#内存碎片问题" class="headerlink" title="内存碎片问题"></a>内存碎片问题</h4><p>通常CMS的GC过程基于标记清除算法,不带压缩动作,导致越来越多的内存碎片需要压缩,常见以下场景会触发内存碎片压缩:</p><ul><li>新生代Young GC出现新生代晋升担保失败(promotion failed)</li><li>程序主动执行System.gc()</li></ul><p>可通过参数CMSFullGCsBeforeCompaction的值,设置多少次Full GC触发一次压缩,默认值为0,代表每次进入Full GC都会触发压缩,带压缩动作的算法为上面提到的单线程Serial Old算法,暂停时间(STW)时间非常长,需要尽可能减少压缩时间</p><h1 id="G1原理及调优"><a href="#G1原理及调优" class="headerlink" title="G1原理及调优"></a>G1原理及调优</h1><h2 id="1-G1简介"><a href="#1-G1简介" class="headerlink" title="1 G1简介"></a>1 G1简介</h2><p>G1(Garbage-First)是一款面向服务器的垃圾收集器,支持新生代和老年代空间的垃圾收集,主要针对配备多核处理器及大容量内存的机器,G1最主要的设计目标是: <strong>实现可预期及可配置的STW停顿时间</strong></p><h2 id="2-G1堆空间划分"><a href="#2-G1堆空间划分" class="headerlink" title="2 G1堆空间划分"></a>2 G1堆空间划分</h2><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/13.png" alt="G1收集器堆空间"></p><ul><li><strong>Region</strong></li></ul><p>为实现大内存空间的低停顿时间的回收,将划分为多个大小相等的Region。每个小堆区都可能是 Eden区,Survivor区或者Old区,但是在同一时刻只能属于某个代</p><p>在逻辑上, 所有的Eden区和Survivor区合起来就是新生代,所有的Old区合起来就是老年代,且新生代和老年代各自的内存Region区域由G1自动控制,不断变动</p><ul><li><strong>巨型对象</strong></li></ul><p>当对象大小超过Region的一半,则认为是巨型对象(Humongous Object),直接被分配到老年代的巨型对象区(Humongous regions),这些巨型区域是一个连续的区域集,每一个Region中最多有一个巨型对象,巨型对象可以占多个Region</p><p>G1把堆内存划分成一个个Region的意义在于:</p><ul><li>每次GC不必都去处理整个堆空间,而是每次只处理一部分Region,实现大容量内存的GC</li><li>通过计算每个Region的回收价值,包括回收所需时间、可回收空间,<strong>在有限时间内尽可能回收更多的垃圾对象</strong>,把垃圾回收造成的停顿时间控制在预期配置的时间范围内,这也是G1名称的由来: <strong>garbage-first</strong></li></ul><h2 id="3-G1工作模式"><a href="#3-G1工作模式" class="headerlink" title="3 G1工作模式"></a>3 G1工作模式</h2><p>针对新生代和老年代,G1提供2种GC模式,Young GC和Mixed GC,两种会导致Stop The World</p><ul><li><p>Young GC<br>当新生代的空间不足时,G1触发Young GC回收新生代空间<br>Young GC主要是对Eden区进行GC,它在Eden空间耗尽时触发,基于分代回收思想和复制算法,每次Young GC都会选定所有新生代的Region,同时计算下次Young GC所需的Eden区和Survivor区的空间,动态调整新生代所占Region个数来控制Young GC开销</p></li><li><p>Mixed GC<br>当老年代空间达到阈值会触发Mixed GC,选定所有新生代里的Region,根据全局并发标记阶段(下面介绍到)统计得出收集收益高的若干老年代 Region。在用户指定的开销目标范围内,尽可能选择收益高的老年代Region进行GC,通过选择哪些老年代Region和选择多少Region来控制Mixed GC开销</p></li></ul><h2 id="4-全局并发标记"><a href="#4-全局并发标记" class="headerlink" title="4 全局并发标记"></a>4 全局并发标记</h2><p><strong>全局并发标记</strong>主要是为Mixed GC计算找出回收收益较高的Region区域,具体分为5个阶段</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/14.png" alt="全局并发标记"></p><ul><li><p><strong>阶段 1: 初始标记(Initial Mark)</strong><br>暂停所有应用线程(STW),并发地进行标记从 GC Root 开始直接可达的对象(原生栈对象、全局对象、JNI 对象),当达到触发条件时,G1 并不会立即发起并发标记周期,而是等待下一次新生代收集,利用新生代收集的 STW 时间段,完成初始标记,这种方式称为借道(Piggybacking)</p></li><li><p><strong>阶段 2: 根区域扫描(Root Region Scan)</strong><br>在初始标记暂停结束后,新生代收集也完成的对象复制到 Survivor 的工作,应用线程开始活跃起来;<br>此时为了保证标记算法的正确性,所有新复制到 Survivor 分区的对象,需要找出哪些对象存在对老年代对象的引用,把这些对象标记成根(Root);<br>这个过程称为根分区扫描(Root Region Scanning),同时扫描的 Suvivor 分区也被称为根分区(Root Region);<br>根分区扫描必须在下一次新生代垃圾收集启动前完成(接下来并发标记的过程中,可能会被若干次新生代垃圾收集打断),因为每次 GC 会产生新的存活对象集合</p></li><li><p><strong>阶段 3: 并发标记(Concurrent Marking)</strong><br>标记线程与应用程序线程并行执行,标记各个堆中Region的存活对象信息,这个步骤可能被新的 Young GC 打断<br>所有的标记任务必须在堆满前就完成扫描,如果并发标记耗时很长,那么有可能在并发标记过程中,又经历了几次新生代收集</p></li><li><p><strong>阶段 4: 再次标记(Remark)</strong><br>和CMS类似暂停所有应用线程(STW),以完成标记过程短暂地停止应用线程, 标记在并发标记阶段发生变化的对象,和所有未被标记的存活对象,同时完成存活数据计算</p></li><li><p><strong>阶段 5: 清理(Cleanup)</strong><br>为即将到来的转移阶段做准备, 此阶段也为下一次标记执行所有必需的整理计算工作:</p><ul><li>整理更新每个Region各自的RSet(remember set,HashMap结构,记录有哪些老年代对象指向本Region,key为指向本Region的对象的引用,value为指向本Region的具体Card区域,通过RSet可以确定Region中对象存活信息,避免全堆扫描)</li><li>回收不包含存活对象的Region</li><li>统计计算回收收益高(基于释放空间和暂停目标)的老年代分区集合</li></ul></li></ul><h2 id="5-G1调优注意点"><a href="#5-G1调优注意点" class="headerlink" title="5 G1调优注意点"></a>5 G1调优注意点</h2><h3 id="Full-GC问题"><a href="#Full-GC问题" class="headerlink" title="Full GC问题"></a>Full GC问题</h3><p>G1的正常处理流程中没有Full GC,只有在垃圾回收处理不过来(或者主动触发)时才会出现, G1的Full GC就是单线程执行的Serial old gc,会导致非常长的STW,是调优的重点,需要尽量避免Full GC,常见原因如下:</p><ul><li>程序主动执行System.gc()</li><li>全局并发标记期间老年代空间被填满(并发模式失败)</li><li>Mixed GC期间老年代空间被填满(晋升失败)</li><li>Young GC时Survivor空间和老年代没有足够空间容纳存活对象</li></ul><p>类似CMS,常见的解决是:</p><ul><li>增大-XX:ConcGCThreads=n 选项增加并发标记线程的数量,或者STW期间并行线程的数量:-XX:ParallelGCThreads=n</li><li>减小-XX:InitiatingHeapOccupancyPercent 提前启动标记周期</li><li>增大预留内存 -XX:G1ReservePercent=n ,默认值是10,代表使用10%的堆内存为预留内存,当Survivor区域没有足够空间容纳新晋升对象时会尝试使用预留内存</li></ul><h3 id="巨型对象分配"><a href="#巨型对象分配" class="headerlink" title="巨型对象分配"></a>巨型对象分配</h3><p>巨型对象区中的每个Region中包含一个巨型对象,剩余空间不再利用,导致空间碎片化,当G1没有合适空间分配巨型对象时,G1会启动串行Full GC来释放空间。可以通过增加 -XX:G1HeapRegionSize来增大Region大小,这样一来,相当一部分的巨型对象就不再是巨型对象了,而是采用普通的分配方式</p><h3 id="不要设置Young区的大小"><a href="#不要设置Young区的大小" class="headerlink" title="不要设置Young区的大小"></a>不要设置Young区的大小</h3><p>原因是为了尽量满足目标停顿时间,逻辑上的Young区会进行动态调整。如果设置了大小,则会覆盖掉并且会禁用掉对停顿时间的控制</p><h3 id="平均响应时间设置"><a href="#平均响应时间设置" class="headerlink" title="平均响应时间设置"></a>平均响应时间设置</h3><p>使用应用的平均响应时间作为参考来设置MaxGCPauseMillis,JVM会尽量去满足该条件,可能是90%的请求或者更多的响应时间在这之内, 但是并不代表是所有的请求都能满足,平均响应时间设置过小会导致频繁GC</p><h1 id="调优方法与思路"><a href="#调优方法与思路" class="headerlink" title="调优方法与思路"></a>调优方法与思路</h1><p>如何分析系统JVM GC运行状况及合理优化?</p><p>GC优化的核心思路在于:<strong>尽可能让对象在新生代中分配和回收,尽量避免过多对象进入老年代,导致对老年代频繁进行垃圾回收,同时给系统足够的内存减少新生代垃圾回收次数</strong>,进行系统分析和优化也是围绕着这个思路展开</p><h2 id="1-分析系统的运行状况"><a href="#1-分析系统的运行状况" class="headerlink" title="1 分析系统的运行状况"></a>1 分析系统的运行状况</h2><ul><li>系统每秒请求数、每个请求创建多少对象,占用多少内存</li><li>Young GC触发频率、对象进入老年代的速率</li><li>老年代占用内存、Full GC触发频率、Full GC触发的原因、长时间Full GC的原因</li></ul><p>常用工具如下:</p><ul><li><strong>jstat</strong><br>jvm自带命令行工具,可用于统计内存分配速率、GC次数,GC耗时,常用命令格式<pre><code>jstat -gc <pid> <统计间隔时间> <统计次数></code></pre></li></ul><p>输出返回值代表含义如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/15.png" alt="jstat"></p><p>例如: jstat -gc 32683 1000 10 ,统计pid=32683的进程,每秒统计1次,统计10次</p><ul><li><strong>jmap</strong><br>jvm自带命令行工具,可用于了解系统运行时的对象分布,常用命令格式如下</li></ul><pre class="line-numbers language-java"><code class="language-java"><span class="token comment" spellcheck="true">// 命令行输出类名、类数量数量,类占用内存大小,</span><span class="token comment" spellcheck="true">// 按照类占用内存大小降序排列</span>jmap <span class="token operator">-</span>histo <span class="token operator"><</span>pid<span class="token operator">></span><span class="token comment" spellcheck="true">// 生成堆内存转储快照,在当前目录下导出dump.hrpof的二进制文件,</span><span class="token comment" spellcheck="true">// 可以用eclipse的MAT图形化工具分析</span>jmap <span class="token operator">-</span>dump<span class="token operator">:</span>live<span class="token punctuation">,</span>format<span class="token operator">=</span>b<span class="token punctuation">,</span>file<span class="token operator">=</span>dump<span class="token punctuation">.</span>hprof <span class="token operator"><</span>pid<span class="token operator">></span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><ul><li><strong>jinfo</strong><br>命令格式<pre class="line-numbers language-java"><code class="language-java">jinfo <span class="token operator"><</span>pid<span class="token operator">></span> <span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre>用来查看正在运行的 Java 应用程序的扩展参数,包括Java System属性和JVM命令行参数</li></ul><p>其他GC工具</p><ul><li>监控告警系统:Zabbix、Prometheus、Open-Falcon</li><li>jdk自动实时内存监控工具:VisualVM</li><li>堆外内存监控: Java VisualVM安装Buffer Pools 插件、google perf工具、Java NMT(Native Memory Tracking)工具</li><li>GC日志分析:GCViewer、gceasy</li><li>GC参数检查和优化:<a href="http://xxfox.perfma.com/" target="_blank" rel="noopener">http://xxfox.perfma.com/</a></li></ul><h2 id="2-GC优化案例"><a href="#2-GC优化案例" class="headerlink" title="2 GC优化案例"></a>2 GC优化案例</h2><ul><li><strong>数据分析平台系统频繁Full GC</strong></li></ul><p>平台主要对用户在APP中行为进行定时分析统计,并支持报表导出,使用CMS GC算法。数据分析师在使用中发现系统页面打开经常卡顿,通过jstat命令发现系统每次Young GC后大约有10%的存活对象进入老年代。</p><p>原来是因为Survivor区空间设置过小,每次Young GC后存活对象在Survivor区域放不下,提前进入老年代,通过调大Survivor区,使得Survivor区可以容纳Young GC后存活对象,对象在Survivor区经历多次Young GC达到年龄阈值才进入老年代,调整之后每次Young GC后进入老年代的存活对象稳定运行时仅几百Kb,Full GC频率大大降低</p><ul><li><strong>业务对接网关OOM</strong></li></ul><p>网关主要消费Kafka数据,进行数据处理计算然后转发到另外的Kafka队列,系统运行几个小时候出现OOM,重启系统几个小时之后又OOM,通过jmap导出堆内存,在eclipse MAT工具分析才找出原因:代码中将某个业务Kafka的topic数据进行日志异步打印,该业务数据量较大,大量对象堆积在内存中等待被打印,导致OOM</p><ul><li><strong>账号权限管理系统频繁长时间Full GC</strong></li></ul><p>系统对外提供各种账号鉴权服务,使用时发现系统经常服务不可用,通过Zabbix的监控平台监控发现系统频繁发生长时间Full GC,且触发时老年代的堆内存通常并没有占满,发现原来是业务代码中调用了System.gc()</p><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>GC问题可以说没有捷径,排查线上的性能问题本身就并不简单,除了将本文介绍到的原理和工具融会贯通,还需要我们不断去积累经验,真正做到性能最优</p><p>篇幅所限,不再展开介绍常见GC参数的使用,我发布在github:<a href="https://github.com/caison/caison-blog-demo" target="_blank" rel="noopener">https://github.com/caison/caison-blog-demo</a></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p>《Java Performance: The Definitive Guide》 Scott Oaks</p><p>《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第二版》 周志华</p><p><a href="http://gk.link/a/108k3" target="_blank" rel="noopener">Java性能调优实战</a></p><p><a href="https://www.oracle.com/technetwork/tutorials/tutorials-1876574.html" target="_blank" rel="noopener">Getting Started with the G1 Garbage Collector</a></p><p><a href="https://github.com/cncounter/gc-handbook" target="_blank" rel="noopener">GC参考手册-Java版</a></p><p><a href="https://link.jianshu.com/?t=http://hllvm.group.iteye.com/group/topic/44381#post-272188" target="_blank" rel="noopener">请教G1算法的原理——RednaxelaFX的回答</a></p><p><a href="https://zhuanlan.zhihu.com/p/22591838" target="_blank" rel="noopener">Java Hotspot G1 GC的一些关键技术——美团技术团队</a></p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/16.png" alt></p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>理解高性能网络模型</title>
<link href="/2019/10/13/li-jie-gao-xing-neng-wang-luo-mo-xing/"/>
<url>/2019/10/13/li-jie-gao-xing-neng-wang-luo-mo-xing/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/0.jpg" alt></p><p>随着互联网的发展,面对海量用户高并发业务,传统的阻塞式的服务端架构模式已经无能为力,由此,本文旨在为大家提供有用的概览以及网络服务模型的比较,以揭开设计和实现高性能网络架构的神秘面纱</p><h1 id="1-服务端处理网络请求"><a href="#1-服务端处理网络请求" class="headerlink" title="1 服务端处理网络请求"></a>1 服务端处理网络请求</h1><p>首先看看服务端处理网络请求的典型过程:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/1.png" alt="服务端处理网络请求流程图"><br>可以看到,主要处理步骤包括:</p><ul><li>1、获取请求数据<br>客户端与服务器建立连接发出请求,服务器接受请求(1-3)</li><li>2、构建响应<br>当服务器接收完请求,并在用户空间处理客户端的请求,直到构建响应完成(4)</li><li>3、返回数据<br>服务器将已构建好的响应再通过内核空间的网络I/O发还给客户端(5-7)</li></ul><p>设计服务端并发模型时,主要有如下两个关键点:</p><ul><li>服务器如何管理连接,获取输入数据</li><li>服务器如何处理请求</li></ul><p>以上两个关键点最终都与操作系统的I/O模型以及线程(进程)模型相关,下面详细介绍这两个模型</p><h1 id="2-I-O模型"><a href="#2-I-O模型" class="headerlink" title="2 I/O模型"></a>2 I/O模型</h1><h2 id="2-1-概念理论"><a href="#2-1-概念理论" class="headerlink" title="2.1 概念理论"></a>2.1 概念理论</h2><p>介绍操作系统的I/O模型之前,先了解一下几个概念:</p><ul><li>阻塞调用与非阻塞调用<ul><li>阻塞调用是指调用结果返回之前,当前线程会被挂起。调用线程只有在得到结果之后才会返回</li><li>非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程</li></ul></li></ul><p>两者的最大区别在于被调用方在收到请求到返回结果之前的这段时间内,调用方是否一直在等待。阻塞是指调用方一直在等待而且别的事情什么都不做。非阻塞是指调用方先去忙别的事情</p><ul><li><p>同步处理与异步处理</p><ul><li>同步处理是指被调用方得到最终结果之后才返回给调用方</li><li>异步处理是指被调用方先返回应答,然后再计算调用结果,计算完最终结果后再通知并返回给调用方</li></ul></li><li><p>阻塞、非阻塞和同步、异步的区别<br>阻塞、非阻塞和同步、异步其实针对的对象是不一样的:</p></li><li><p><em>阻塞、非阻塞的讨论对象是调用者*</em></p></li><li><p><em>同步、异步的讨论对象是被调用者*</em></p></li><li><p>recvfrom函数<br>recvfrom函数(经socket接收数据),这里把它视为系统调用</p></li></ul><p>一个输入操作通常包括两个不同的阶段</p><ul><li>等待数据准备好</li><li>从内核向进程复制数据</li></ul><p>对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达。当所等待分组到达时,它被复制到内核中的某个缓冲区。第二步就是把数据从内核缓冲区复制到应用进程缓冲区</p><p>实际应用程序在系统调用完成上面2步操作时,调用方式的阻塞、非阻塞,操作系统在处理应用程序请求时处理方式的同步、异步处理的不同,参考<strong>《UNIX网络编程卷1》</strong>,可以分为5种I/O模型</p><h2 id="2-2-阻塞式I-O模型-blocking-I-O)"><a href="#2-2-阻塞式I-O模型-blocking-I-O)" class="headerlink" title="2.2 阻塞式I/O模型(blocking I/O)"></a>2.2 阻塞式I/O模型(blocking I/O)</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/2.png" alt="阻塞式I/O模型"><br><strong>简介</strong><br>在阻塞式I/O模型中,应用程序在从调用recvfrom开始到它返回有数据报准备好这段时间是阻塞的,recvfrom返回成功后,应用进程开始处理数据报</p><p><strong>比喻</strong><br>一个人在钓鱼,当没鱼上钩时,就坐在岸边一直等</p><p><strong>优点</strong><br>程序简单,在阻塞等待数据期间进程/线程挂起,基本不会占用CPU资源</p><p><strong>缺点</strong><br>每个连接需要独立的进程/线程单独处理,当并发请求量大时为了维护程序,内存、线程切换开销较大,这种模型在实际生产中很少使用</p><h2 id="2-3-非阻塞式I-O模型-non-blocking-I-O)"><a href="#2-3-非阻塞式I-O模型-non-blocking-I-O)" class="headerlink" title="2.3 非阻塞式I/O模型(non-blocking I/O)"></a>2.3 非阻塞式I/O模型(non-blocking I/O)</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/3.png" alt="非阻塞式I/O模型"><br><strong>简介</strong><br>在非阻塞式I/O模型中,应用程序把一个套接口设置为非阻塞就是告诉内核,当所请求的I/O操作无法完成时,不要将进程睡眠,而是返回一个错误,应用程序基于I/O操作函数将不断的轮询数据是否已经准备好,如果没有准备好,继续轮询,直到数据准备好为止</p><p><strong>比喻</strong><br>边钓鱼边玩手机,隔会再看看有没有鱼上钩,有的话就迅速拉杆</p><p><strong>优点</strong><br>不会阻塞在内核的等待数据过程,每次发起的I/O请求可以立即返回,不用阻塞等待,实时性较好</p><p><strong>缺点</strong>轮询将会不断地询问内核,这将占用大量的CPU时间,系统资源利用率较低,所以一般Web服务器不使用这种I/O模型</p><h2 id="2-4-I-O复用模型-I-O-multiplexing)"><a href="#2-4-I-O复用模型-I-O-multiplexing)" class="headerlink" title="2.4 I/O复用模型(I/O multiplexing)"></a>2.4 I/O复用模型(I/O multiplexing)</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/4.png" alt="I/O复用模型"><br><strong>简介</strong><br>在I/O复用模型中,会用到select或poll函数或epoll函数(Linux2.6以后的内核开始支持),这两个函数也会使进程阻塞,但是和阻塞I/O所不同的的,这两个函数可以同时阻塞多个I/O操作,而且可以同时对多个读操作,多个写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数</p><p><strong>比喻</strong><br>放了一堆鱼竿,在岸边一直守着这堆鱼竿,直到有鱼上钩</p><p><strong>优点</strong><br>可以基于一个阻塞对象,同时在多个描述符上等待就绪,而不是使用多个线程(每个文件描述符一个线程),这样可以大大节省系统资源</p><p><strong>缺点</strong><br>当连接数较少时效率相比多线程+阻塞I/O模型效率较低,可能延迟更大,因为单个连接处理需要2次系统调用,占用时间会有增加</p><h2 id="2-5-信号驱动式I-O模型(signal-driven-I-O"><a href="#2-5-信号驱动式I-O模型(signal-driven-I-O" class="headerlink" title="2.5 信号驱动式I/O模型(signal-driven I/O)"></a>2.5 信号驱动式I/O模型(signal-driven I/O)</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/5.png" alt="信号驱动式I/O模型"><br><strong>简介</strong><br>在信号驱动式I/O模型中,应用程序使用套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据</p><p><strong>比喻</strong><br>鱼竿上系了个铃铛,当铃铛响,就知道鱼上钩,然后可以专心玩手机</p><p><strong>优点</strong><br>线程并没有在等待数据时被阻塞,可以提高资源的利用率</p><p><strong>缺点</strong></p><ul><li>信号I/O在大量IO操作时可能会因为信号队列溢出导致没法通知</li><li>信号驱动I/O尽管对于处理UDP套接字来说有用,即这种信号通知意味着到达一个数据报,或者返回一个异步错误。但是,对于TCP而言,信号驱动的I/O方式近乎无用,因为导致这种通知的条件为数众多,每一个来进行判别会消耗很大资源,与前几种方式相比优势尽失</li></ul><h2 id="2-6-异步I-O模型(asynchronous-I-O)"><a href="#2-6-异步I-O模型(asynchronous-I-O)" class="headerlink" title="2.6 异步I/O模型(asynchronous I/O)"></a>2.6 异步I/O模型(asynchronous I/O)</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/6.png" alt="异步I/O模型"><br><strong>简介</strong><br>由POSIX规范定义,应用程序告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到应用程序的缓冲区)完成后通知应用程序。这种模型与信号驱动模型的主要区别在于:信号驱动I/O是由内核通知应用程序何时启动一个I/O操作,而异步I/O模型是由内核通知应用程序I/O操作何时完成</p><p><strong>优点</strong><br>异步 I/O 能够充分利用 DMA 特性,让 I/O 操作与计算重叠</p><p><strong>缺点</strong><br>要实现真正的异步 I/O,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下,Linux2.6才引入,目前 AIO 并不完善,因此在 Linux 下实现高并发网络编程时都是以 IO复用模型模式为主</p><h2 id="2-5-5种I-O模型总结"><a href="#2-5-5种I-O模型总结" class="headerlink" title="2.5 5种I/O模型总结"></a>2.5 5种I/O模型总结</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/7.png" alt><br>从上图中我们可以看出,可以看出,越往后,阻塞越少,理论上效率也是最优。其五种I/O模型中,前四种属于同步I/O,因为其中真正的I/O操作(recvfrom)将阻塞进程/线程,只有异步I/O模型才于POSIX定义的异步I/O相匹配</p><h1 id="3-线程模型"><a href="#3-线程模型" class="headerlink" title="3 线程模型"></a>3 线程模型</h1><p>介绍完服务器如何基于I/O模型管理连接,获取输入数据,下面介绍基于进程/线程模型,服务器如何处理请求</p><p>值得说明的是,具体选择线程还是进程,更多是与平台及编程语言相关,例如C语言使用线程和进程都可以(例如Nginx使用进程,Memcached使用线程),Java语言一般使用线程(例如Netty),为了描述方便,下面都使用线程来进行描述</p><h2 id="3-1-传统阻塞I-O服务模型"><a href="#3-1-传统阻塞I-O服务模型" class="headerlink" title="3.1 传统阻塞I/O服务模型"></a>3.1 传统阻塞I/O服务模型</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/8.png" alt="传统阻塞I/O服务模型"><br><strong>特点</strong></p><ul><li>采用阻塞式I/O模型获取输入数据</li><li>每个连接都需要独立的线程完成数据输入,业务处理,数据返回的完整操作</li></ul><p><strong>存在问题</strong></p><ul><li>当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大</li><li>连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read操作上,造成线程资源浪费</li></ul><h2 id="3-2-Reactor模式"><a href="#3-2-Reactor模式" class="headerlink" title="3.2 Reactor模式"></a>3.2 Reactor模式</h2><p>针对传统传统阻塞I/O服务模型的2个缺点,比较常见的有如下解决方案:</p><ul><li>基于I/O复用模型,多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象上等待,无需阻塞等待所有连接。当某条连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理</li><li>基于线程池复用线程资源,不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务</li></ul><p>I/O复用结合线程池,这就是Reactor模式基本设计思想</p><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/9.png" alt="Reactor"></p><p><strong>Reactor模式</strong>,是指通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一</p><p>Reactor模式中有2个关键组成:</p><ul><li><p>Reactor<br>Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人</p></li><li><p>Handlers<br>处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作</p></li></ul><p>根据Reactor的数量和处理资源池线程的数量不同,有3种典型的实现:</p><ul><li>单Reactor单线程</li><li>单Reactor多线程</li><li>主从Reactor多线程</li></ul><p>下面详细介绍这3种实现</p><h3 id="3-2-1-单Reactor单线程"><a href="#3-2-1-单Reactor单线程" class="headerlink" title="3.2.1 单Reactor单线程"></a>3.2.1 单Reactor单线程</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/10.png" alt="单Reactor单线程"></p><p>其中,select是前面<strong>I/O复用模型</strong>介绍的标准网络编程API,可以实现应用程序通过一个阻塞对象监听多路连接请求,其他方案示意图类似</p><p><strong>方案说明</strong></p><ul><li>Reactor对象通过select监控客户端请求事件,收到事件后通过dispatch进行分发</li><li>如果是建立连接请求事件,则由Acceptor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后的后续业务处理</li><li>如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应</li><li>Handler会完成read->业务处理->send的完整业务流程</li></ul><p><strong>优点</strong><br>模型简单,没有多线程、进程通信、竞争的问题,全部都在一个线程中完成</p><p><strong>缺点</strong></p><ul><li>性能问题:只有一个线程,无法完全发挥多核CPU的性能<br>Handler在处理某个连接上的业务时,整个进程无法处理其他连接事件,很容易导致性能瓶颈 </li><li>可靠性问题:线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障</li></ul><p><strong>使用场景</strong><br>客户端的数量有限,业务处理非常快速,比如Redis,业务处理的时间复杂度O(1)</p><h3 id="3-2-2-单Reactor多线程"><a href="#3-2-2-单Reactor多线程" class="headerlink" title="3.2.2 单Reactor多线程"></a>3.2.2 单Reactor多线程</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/11.png" alt="单Reactor多线程"><br><strong>方案说明</strong></p><ul><li>Reactor对象通过select监控客户端请求事件,收到事件后通过dispatch进行分发</li><li>如果是建立连接请求事件,则由Acceptor通过accept处理连接请求,然后创建一个Handler对象处理连接完成后的续各种事件</li><li>如果不是建立连接事件,则Reactor会分发调用连接对应的Handler来响应</li><li>Handler只负责响应事件,不做具体业务处理,通过read读取数据后,会分发给后面的Worker线程池进行业务处理 </li><li>Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理</li><li>Handler收到响应结果后通过send将响应结果返回给client</li></ul><p><strong>优点</strong><br>可以充分利用多核CPU的处理能力</p><p><strong>缺点</strong></p><ul><li>多线程数据共享和访问比较复杂</li><li>Reactor承担所有事件的监听和响应,在单线程中运行,高并发场景下容易成为性能瓶颈</li></ul><h3 id="3-2-3-主从Reactor多线程"><a href="#3-2-3-主从Reactor多线程" class="headerlink" title="3.2.3 主从Reactor多线程"></a>3.2.3 主从Reactor多线程</h3><p>针对单Reactor多线程模型中,Reactor在单线程中运行,高并发场景下容易成为性能瓶颈,可以让Reactor在多线程中运行</p><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/12.png" alt="主从Reactor多线程"></p><p><strong>方案说明</strong></p><ul><li>Reactor主线程MainReactor对象通过select监控建立连接事件,收到事件后通过Acceptor接收,处理建立连接事件</li><li>Accepto处理建立连接事件后,MainReactor将连接分配Reactor子线程给SubReactor进行处理</li><li>SubReactor将连接加入连接队列进行监听,并创建一个Handler用于处理各种连接事件</li><li>当有新的事件发生时,SubReactor会调用连接对应的Handler进行响应</li><li>Handler通过read读取数据后,会分发给后面的Worker线程池进行业务处理 </li><li>Worker线程池会分配独立的线程完成真正的业务处理,如何将响应结果发给Handler进行处理</li><li>Handler收到响应结果后通过send将响应结果返回给client</li></ul><p><strong>优点</strong></p><ul><li>父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理</li><li>父线程与子线程的数据交互简单,Reactor主线程只需要把新连接传给子线程,子线程无需返回数据</li></ul><p>这种模型在许多项目中广泛使用,包括Nginx主从Reactor多进程模型,Memcached主从多线程,Netty主从多线程模型的支持</p><h3 id="3-2-4-总结"><a href="#3-2-4-总结" class="headerlink" title="3.2.4 总结"></a>3.2.4 总结</h3><p>3种模式可以用个比喻来理解:<br>餐厅常常雇佣接待员负责迎接顾客,当顾客入坐后,侍应生专门为这张桌子服务</p><ul><li>单Reactor单线程<br>接待员和侍应生是同一个人,全程为顾客服务</li><li>单Reactor多线程<br>1个接待员,多个侍应生,接待员只负责接待</li><li>主从Reactor多线程<br>多个接待员,多个侍应生</li></ul><p>Reactor模式具有如下的优点:</p><ul><li>响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的</li><li>编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;</li><li>可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源</li><li>可复用性,Reactor模型本身与具体事件处理逻辑无关,具有很高的复用性</li></ul><h2 id="3-3-Proactor模型"><a href="#3-3-Proactor模型" class="headerlink" title="3.3 Proactor模型"></a>3.3 Proactor模型</h2><p>在Reactor模式中,Reactor等待某个事件或者可应用或个操作的状态发生(比如文件描述符可读写,或者是socket可读写),然后把这个事件传给事先注册的Handler(事件处理函数或者回调函数),由后者来做实际的读写操作,其中的读写操作都需要应用程序同步操作,所以Reactor是非阻塞同步网络模型。如果把I/O操作改为异步,即交给操作系统来完成就能进一步提升性能,这就是异步网络模型Proactor</p><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/13.png" alt="Proactor"><br>Proactor是和异步I/O相关的,<strong>详细方案</strong>如下:</p><ul><li>ProactorInitiator创建Proactor和Handler对象,并将Proactor和Handler都通过AsyOptProcessor(Asynchronous Operation Processor)注册到内核</li><li>AsyOptProcessor处理注册请求,并处理I/O操作</li><li>AsyOptProcessor完成I/O操作后通知Proactor</li><li>Proactor根据不同的事件类型回调不同的Handler进行业务处理</li><li>Handler完成业务处理</li></ul><p>可以看出Proactor和Reactor的区别:Reactor是在事件发生时就通知事先注册的事件(读写在应用程序线程中处理完成);Proactor是在事件发生时基于异步I/O完成读写操作(由内核完成),待I/O操作完成后才回调应用程序的处理器来处理进行业务处理</p><p>理论上Proactor比Reactor效率更高,异步I/O更加充分发挥DMA(Direct Memory Access,直接内存存取)的优势,但是有如下缺点:</p><ul><li>编程复杂性<br>由于异步操作流程的事件的初始化和事件完成在时间和空间上都是相互分离的,因此开发异步应用程序更加复杂。应用程序还可能因为反向的流控而变得更加难以Debug</li><li>内存使用<br>缓冲区在读或写操作的时间段内必须保持住,可能造成持续的不确定性,并且每个并发操作都要求有独立的缓存,相比Reactor模式,在socket已经准备好读或写前,是不要求开辟缓存的</li><li>操作系统支持<br>Windows 下通过 IOCP 实现了真正的异步 I/O,而在 Linux 系统下,Linux2.6才引入,目前异步I/O还不完善</li></ul><p>因此在Linux下实现高并发网络编程都是以Reactor模型为主</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://time.geekbang.org/column/intro/81?code=OK4eM0TBPTKGPRCzcZdzIeXjPACLfY3KCzATXOSWzXE%3D" target="_blank" rel="noopener">从0开始学架构 —— Alibaba技术专家李运华 </a></p><p><a href="https://juejin.im/book/5b4bc28bf265da0f60130116?referrer=598ff735f265da3e1c0f9643" target="_blank" rel="noopener">Netty入门与实战</a></p><p><a href="http://www.merlinblog.site/posts/a4602dea/" target="_blank" rel="noopener">技术: Linux网络IO模型</a></p><p><a href="http://www.itran.cc/2016/05/08/duo-xian-cheng-wang-luo-fu-wu-mo-xing/" target="_blank" rel="noopener">多线程网络服务模型</a></p><p><a href="https://mp.weixin.qq.com/s?__biz=MzI3NzE0NjcwMg==&mid=2650121924&idx=1&sn=2b34c54391347fc81e11702fa5bfea09&chksm=f36bbbe5c41c32f309aa85aa6c027d644052233864db85655839530d111ca37339ea1b81ef71&mpshare=1&scene=1&srcid=0827POjeNpUaga1gKNqMuUiY#rd" target="_blank" rel="noopener">IO中的阻塞、非阻塞、同步、异步</a></p><p>UNIX网络编程卷1:套接字联网API(第3版)</p><p><a href="https://tech.youzan.com/yi-bu-wang-luo-mo-xing/" target="_blank" rel="noopener">异步网络模型</a></p><p><img src="/images/%E7%90%86%E8%A7%A3%E9%AB%98%E6%80%A7%E8%83%BD%E7%BD%91%E7%BB%9C%E6%A8%A1%E5%9E%8B/14.png" alt></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>理解Netty模型架构</title>
<link href="/2019/10/13/li-jie-netty-mo-xing-jia-gou/"/>
<url>/2019/10/13/li-jie-netty-mo-xing-jia-gou/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/0.jpg" alt></p><p>本文基于Netty4.1展开介绍相关理论模型,使用场景,基本组件、整体架构,<strong>知其然且知其所以然</strong>,希望给读者提供学习实践参考。</p><h1 id="1-Netty简介"><a href="#1-Netty简介" class="headerlink" title="1 Netty简介"></a>1 Netty简介</h1><p>Netty是 一个异步事件驱动的网络应用程序框架,用于快速开发可维护的高性能协议服务器和客户端。</p><h2 id="JDK原生NIO程序的问题"><a href="#JDK原生NIO程序的问题" class="headerlink" title="JDK原生NIO程序的问题"></a>JDK原生NIO程序的问题</h2><p>JDK原生也有一套网络应用程序API,但是存在一系列问题,主要如下:</p><ul><li>NIO的类库和API繁杂,使用麻烦,你需要熟练掌握Selector、ServerSocketChannel、SocketChannel、ByteBuffer等</li><li>需要具备其它的额外技能做铺垫,例如熟悉Java多线程编程,因为NIO编程涉及到Reactor模式,你必须对多线程和网路编程非常熟悉,才能编写出高质量的NIO程序</li><li>可靠性能力补齐,开发工作量和难度都非常大。例如客户端面临断连重连、网络闪断、半包读写、失败缓存、网络拥塞和异常码流的处理等等,NIO编程的特点是功能开发相对容易,但是可靠性能力补齐工作量和难度都非常大</li><li>JDK NIO的BUG,例如臭名昭著的epoll bug,它会导致Selector空轮询,最终导致CPU 100%。官方声称在JDK1.6版本的update18修复了该问题,但是直到JDK1.7版本该问题仍旧存在,只不过该bug发生概率降低了一些而已,它并没有被根本解决</li></ul><h2 id="Netty的特点"><a href="#Netty的特点" class="headerlink" title="Netty的特点"></a>Netty的特点</h2><p>Netty的对JDK自带的NIO的API进行封装,解决上述问题,主要特点有:</p><ul><li>设计优雅<br>适用于各种传输类型的统一API - 阻塞和非阻塞Socket<br>基于灵活且可扩展的事件模型,可以清晰地分离关注点<br>高度可定制的线程模型 - 单线程,一个或多个线程池<br>真正的无连接数据报套接字支持(自3.1起)</li><li>使用方便<br>详细记录的Javadoc,用户指南和示例<br>没有其他依赖项,JDK 5(Netty 3.x)或6(Netty 4.x)就足够了</li><li>高性能<br>吞吐量更高,延迟更低<br>减少资源消耗<br>最小化不必要的内存复制</li><li>安全<br>完整的SSL / TLS和StartTLS支持</li><li>社区活跃,不断更新<br>社区活跃,版本迭代周期短,发现的BUG可以被及时修复,同时,更多的新功能会被加入</li></ul><h2 id="Netty常见使用常见"><a href="#Netty常见使用常见" class="headerlink" title="Netty常见使用常见"></a>Netty常见使用常见</h2><p>Netty常见的使用场景如下:</p><ul><li>互联网行业<br>在分布式系统中,各个节点之间需要远程服务调用,高性能的RPC框架必不可少,Netty作为异步高新能的通信框架,往往作为基础通信组件被这些RPC框架使用。<br>典型的应用有:阿里分布式服务框架Dubbo的RPC框架使用Dubbo协议进行节点间通信,Dubbo协议默认使用Netty作为基础通信组件,用于实现各进程节点之间的内部通信。</li><li>游戏行业<br>无论是手游服务端还是大型的网络游戏,Java语言得到了越来越广泛的应用。Netty作为高性能的基础通信组件,它本身提供了TCP/UDP和HTTP协议栈。<br>非常方便定制和开发私有协议栈,账号登录服务器,地图服务器之间可以方便的通过Netty进行高性能的通信</li><li>大数据领域<br>经典的Hadoop的高性能通信和序列化组件Avro的RPC框架,默认采用Netty进行跨界点通信,它的Netty Service基于Netty框架二次封装实现</li></ul><p>有兴趣的读者可以了解一下目前有哪些开源项目使用了 Netty:<a href="https://netty.io/wiki/related-projects.html" target="_blank" rel="noopener">Related projects</a></p><h1 id="2-Netty高性能设计"><a href="#2-Netty高性能设计" class="headerlink" title="2 Netty高性能设计"></a>2 Netty高性能设计</h1><p>Netty作为异步事件驱动的网络,高性能之处主要来自于其I/O模型和线程处理模型,前者决定如何收发数据,后者决定如何处理数据</p><h2 id="I-O模型"><a href="#I-O模型" class="headerlink" title="I/O模型"></a>I/O模型</h2><p>用什么样的通道将数据发送给对方,BIO、NIO或者AIO,I/O模型在很大程度上决定了框架的性能</p><h3 id="阻塞I-O"><a href="#阻塞I-O" class="headerlink" title="阻塞I/O"></a>阻塞I/O</h3><p>传统阻塞型I/O(BIO)可以用下图表示:<br><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/1.png" alt="Blocking I/O"><br><strong>特点</strong></p><ul><li>每个请求都需要独立的线程完成数据read,业务处理,数据write的完整操作</li></ul><p><strong>问题</strong></p><ul><li>当并发数较大时,需要创建大量线程来处理连接,系统资源占用较大</li><li>连接建立后,如果当前线程暂时没有数据可读,则线程就阻塞在read操作上,造成线程资源浪费</li></ul><h3 id="I-O复用模型"><a href="#I-O复用模型" class="headerlink" title="I/O复用模型"></a>I/O复用模型</h3><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/2.png" alt></p><p>在I/O复用模型中,会用到select,这个函数也会使进程阻塞,与阻塞I/O所不同的,这个函数可以同时阻塞多个I/O操作,可同时对多个读操写操作的I/O函数进行检测,直到有数据可读或可写时,才真正调用I/O操作函数</p><p>Netty的非阻塞I/O的实现关键是基于I/O复用模型,这里用Selector对象表示:</p><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/3.png" alt="Nonblocking I/O"><br>Netty的IO线程NioEventLoop由于聚合了多路复用器Selector,可以同时并发处理成百上千个客户端连接。当线程从某客户端Socket通道进行读写数据时,若没有数据可用时,该线程可以进行其他任务。线程通常将非阻塞 IO 的空闲时间用于在其他通道上执行 IO 操作,所以单独的线程可以管理多个输入和输出通道。</p><p>由于读写操作都是非阻塞的,这就可以充分提升IO线程的运行效率,避免由于频繁I/O阻塞导致的线程挂起,一个I/O线程可以并发处理N个客户端连接和读写操作,这从根本上解决了传统同步阻塞I/O一连接一线程模型,架构的性能、弹性伸缩能力和可靠性都得到了极大的提升。</p><h3 id="基于buffer"><a href="#基于buffer" class="headerlink" title="基于buffer"></a>基于buffer</h3><p>传统的I/O是面向字节流或字符流的,以流式的方式顺序地从一个Stream 中读取一个或多个字节, 因此也就不能随意改变读取指针的位置。</p><p>在NIO中, 抛弃了传统的 I/O流, 而是引入了Channel和Buffer的概念. 在NIO中, 只能从Channel中读取数据到Buffer中或将数据 Buffer 中写入到 Channel。</p><p>基于buffer操作不像传统IO的顺序操作, NIO 中可以随意地读取任意位置的数据</p><h2 id="线程模型"><a href="#线程模型" class="headerlink" title="线程模型"></a>线程模型</h2><p>数据报如何读取?读取之后的编解码在哪个线程进行,编解码后的消息如何派发,线程模型的不同,对性能的影响也非常大。</p><h3 id="事件驱动模型"><a href="#事件驱动模型" class="headerlink" title="事件驱动模型"></a>事件驱动模型</h3><p>通常,我们设计一个事件处理模型的程序有两种思路</p><ul><li>轮询方式<br>线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑。</li><li>事件驱动方式<br>发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是<strong>发布-订阅模式</strong>的思路。</li></ul><p>以GUI的逻辑处理为例,说明两种逻辑的不同:</p><ul><li>轮询方式<br>线程不断轮询是否发生按钮点击事件,如果发生,调用处理逻辑</li><li>事件驱动方式<br>发生点击事件把事件放入事件队列,在另外线程消费的事件列表中的事件,根据事件类型调用相关事件处理逻辑</li></ul><p>这里借用O’Reilly 大神关于<a href="http://www.oreilly.com/programming/free/software-architecture-patterns.csp" target="_blank" rel="noopener">事件驱动模型解释图</a><br><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/4.png" alt="事件驱动模型">主要包括4个基本组件:</p><ul><li>事件队列(event queue):接收事件的入口,存储待处理事件</li><li>分发器(event mediator):将不同的事件分发到不同的业务逻辑单元</li><li>事件通道(event channel):分发器与处理器之间的联系渠道</li><li>事件处理器(event processor):实现业务逻辑,处理完成后会发出事件,触发下一步操作</li></ul><p>可以看出,相对传统轮询模式,事件驱动有如下优点:</p><ul><li>可扩展性好,分布式的异步架构,事件处理器之间高度解耦,可以方便扩展事件处理逻辑</li><li>高性能,基于队列暂存事件,能方便并行异步处理事件</li></ul><h3 id="Reactor线程模型"><a href="#Reactor线程模型" class="headerlink" title="Reactor线程模型"></a>Reactor线程模型</h3><p>Reactor是反应堆的意思,Reactor模型,是指通过一个或多个输入同时传递给服务处理器的服务请求的<strong>事件驱动处理模式</strong>。 服务端程序处理传入多路请求,并将它们同步分派给请求对应的处理线程,Reactor模式也叫Dispatcher模式,即I/O多了复用统一监听事件,收到事件后分发(Dispatch给某进程),是编写高性能网络服务器的必备技术之一。</p><p>Reactor模型中有2个关键组成:</p><ul><li><p>Reactor<br>Reactor在一个单独的线程中运行,负责监听和分发事件,分发给适当的处理程序来对IO事件做出反应。 它就像公司的电话接线员,它接听来自客户的电话并将线路转移到适当的联系人</p></li><li><p>Handlers<br>处理程序执行I/O事件要完成的实际事件,类似于客户想要与之交谈的公司中的实际官员。Reactor通过调度适当的处理程序来响应I/O事件,处理程序执行非阻塞操作</p></li></ul><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/5.png" alt="Reactor模型"><br>取决于Reactor的数量和Handler线程数量的不同,Reactor模型有3个变种</p><ul><li>单Reactor单线程</li><li>单Reactor多线程</li><li>主从Reactor多线程</li></ul><p>可以这样理解,Reactor就是一个执行while (true) { selector.select(); …}循环的线程,会源源不断的产生新的事件,称作反应堆很贴切。</p><p>篇幅关系,这里不再具体展开Reactor特性、优缺点比较,有兴趣的读者可以参考我之前另外一篇文章:<a href="https://www.jianshu.com/p/2965fca6bb8f" target="_blank" rel="noopener">《理解高性能网络模型》</a></p><h3 id="Netty线程模型"><a href="#Netty线程模型" class="headerlink" title="Netty线程模型"></a>Netty线程模型</h3><p>Netty主要<strong>基于主从Reactors多线程模型</strong>(如下图)做了一定的修改,其中主从Reactor多线程模型有多个Reactor:MainReactor和SubReactor:</p><ul><li>MainReactor负责客户端的连接请求,并将请求转交给SubReactor</li><li>SubReactor负责相应通道的IO读写请求</li><li>非IO请求(具体逻辑处理)的任务则会直接写入队列,等待worker threads进行处理</li></ul><p>这里引用Doug Lee大神的Reactor介绍:<a href="http://gee.cs.oswego.edu/dl/cpjslides/nio.pdf" target="_blank" rel="noopener">Scalable IO in Java</a>里面关于主从Reactor多线程模型的图</p><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/6.png" alt="主从Rreactor多线程模型"><br>特别说明的是:<br>虽然Netty的线程模型基于主从Reactor多线程,借用了MainReactor和SubReactor的结构,但是实际实现上,SubReactor和Worker线程在同一个线程池中:</p><pre class="line-numbers language-java"><code class="language-java">EventLoopGroup bossGroup <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">NioEventLoopGroup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>EventLoopGroup workerGroup <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">NioEventLoopGroup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>ServerBootstrap server <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ServerBootstrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>server<span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span>bossGroup<span class="token punctuation">,</span> workerGroup<span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">channel</span><span class="token punctuation">(</span>NioServerSocketChannel<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>上面代码中的bossGroup 和workerGroup是Bootstrap构造方法中传入的两个对象,这两个group均是线程池</p><ul><li>bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor,专门处理端口的accept事件,<strong>每个端口对应一个boss线程</strong></li><li>workerGroup线程池会被各个SubReactor和worker线程充分利用</li></ul><h3 id="异步处理"><a href="#异步处理" class="headerlink" title="异步处理"></a>异步处理</h3><p>异步的概念和同步相对。当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者。</p><p>Netty中的I/O操作是异步的,包括bind、write、connect等操作会简单的返回一个ChannelFuture,调用者并不能立刻获得结果,通过Future-Listener机制,用户可以方便的主动获取或者通过通知机制获得IO操作结果。</p><p>当future对象刚刚创建时,处于非完成状态,调用者可以通过返回的ChannelFuture来获取操作执行的状态,注册监听函数来执行完成后的操作,常见有如下:</p><ul><li>通过isDone方法来判断当前操作是否完成</li><li>通过isSuccess方法来判断已完成的当前操作是否成功</li><li>通过getCause方法来获取已完成的当前操作失败的原因</li><li>通过isCancelled方法来判断已完成的当前操作是否被取消</li><li>通过addListener方法来注册监听器,当操作已完成(isDone方法返回完成),将会通知指定的监听器;如果future对象已完成,则理解通知指定的监听器</li></ul><p>例如下面的代码中绑定端口是异步操作,当绑定操作处理完,将会调用相应的监听器处理逻辑</p><pre class="line-numbers language-java"><code class="language-java">serverBootstrap<span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span>port<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>future <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>future<span class="token punctuation">.</span><span class="token function">isSuccess</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">": 端口["</span> <span class="token operator">+</span> port <span class="token operator">+</span> <span class="token string">"]绑定成功!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> System<span class="token punctuation">.</span>err<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"端口["</span> <span class="token operator">+</span> port <span class="token operator">+</span> <span class="token string">"]绑定失败!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>相比传统阻塞I/O,执行I/O操作后线程会被阻塞住, 直到操作完成;异步处理的好处是不会造成线程阻塞,线程在I/O操作期间可以执行别的程序,在高并发情形下会更稳定和更高的吞吐量。</p><h1 id="3-Netty架构设计"><a href="#3-Netty架构设计" class="headerlink" title="3 Netty架构设计"></a>3 Netty架构设计</h1><p>前面介绍完Netty相关一些理论介绍,下面从功能特性、模块组件、运作过程来介绍Netty的架构设计</p><h2 id="功能特性"><a href="#功能特性" class="headerlink" title="功能特性"></a>功能特性</h2><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/7.png" alt="Netty功能特性图"></p><ul><li>传输服务<br>支持BIO和NIO</li><li>容器集成<br>支持OSGI、JBossMC、Spring、Guice容器</li><li>协议支持<br>HTTP、Protobuf、二进制、文本、WebSocket等一系列常见协议都支持。<br>还支持通过实行编码解码逻辑来实现自定义协议</li><li>Core核心<br>可扩展事件模型、通用通信API、支持零拷贝的ByteBuf缓冲对象</li></ul><h2 id="模块组件"><a href="#模块组件" class="headerlink" title="模块组件"></a>模块组件</h2><h3 id="Bootstrap、ServerBootstrap"><a href="#Bootstrap、ServerBootstrap" class="headerlink" title="Bootstrap、ServerBootstrap"></a>Bootstrap、ServerBootstrap</h3><p>Bootstrap意思是引导,一个Netty应用通常由一个Bootstrap开始,主要作用是配置整个Netty程序,串联各个组件,Netty中Bootstrap类是客户端程序的启动引导类,ServerBootstrap是服务端启动引导类。</p><h3 id="Future、ChannelFuture"><a href="#Future、ChannelFuture" class="headerlink" title="Future、ChannelFuture"></a>Future、ChannelFuture</h3><p>正如前面介绍,在Netty中所有的IO操作都是异步的,不能立刻得知消息是否被正确处理,但是可以过一会等它执行完成或者直接注册一个监听,具体的实现就是通过Future和ChannelFutures,他们可以注册一个监听,当操作执行成功或失败时监听会自动触发注册的监听事件。</p><h3 id="Channel"><a href="#Channel" class="headerlink" title="Channel"></a>Channel</h3><p>Netty网络通信的组件,能够用于执行网络I/O操作。<br>Channel为用户提供:</p><ul><li><p>当前网络连接的通道的状态(例如是否打开?是否已连接?)</p></li><li><p>网络连接的配置参数 (例如接收缓冲区大小)</p></li><li><p>提供异步的网络I/O操作(如建立连接,读写,绑定端口),异步调用意味着任何I/O调用都将立即返回,并且不保证在调用结束时所请求的I/O操作已完成。调用立即返回一个ChannelFuture实例,通过注册监听器到ChannelFuture上,可以I/O操作成功、失败或取消时回调通知调用方。</p></li><li><p>支持关联I/O操作与对应的处理程序</p><p>不同协议、不同的阻塞类型的连接都有不同的 Channel 类型与之对应,下面是一些常用的 Channel 类型</p></li><li><p>NioSocketChannel,异步的客户端 TCP Socket 连接</p></li><li><p>NioServerSocketChannel,异步的服务器端 TCP Socket 连接</p></li><li><p>NioDatagramChannel,异步的 UDP 连接</p></li><li><p>NioSctpChannel,异步的客户端 Sctp 连接</p></li><li><p>NioSctpServerChannel,异步的 Sctp 服务器端连接<br>这些通道涵盖了 UDP 和 TCP网络 IO以及文件 IO.</p></li></ul><h3 id="Selector"><a href="#Selector" class="headerlink" title="Selector"></a>Selector</h3><p>Netty基于Selector对象实现I/O多路复用,通过 Selector, 一个线程可以监听多个连接的Channel事件, 当向一个Selector中注册Channel 后,Selector 内部的机制就可以自动不断地查询(select) 这些注册的Channel是否有已就绪的I/O事件(例如可读, 可写, 网络连接完成等),这样程序就可以很简单地使用一个线程高效地管理多个 Channel 。</p><h3 id="NioEventLoop"><a href="#NioEventLoop" class="headerlink" title="NioEventLoop"></a>NioEventLoop</h3><p>NioEventLoop中维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用NioEventLoop的run方法,执行I/O任务和非I/O任务:</p><ul><li>I/O任务<br>即selectionKey中ready的事件,如accept、connect、read、write等,由processSelectedKeys方法触发。</li><li>非IO任务<br>添加到taskQueue中的任务,如register0、bind0等任务,由runAllTasks方法触发。</li></ul><p>两种任务的执行时间比由变量ioRatio控制,默认为50,则表示允许非IO任务执行的时间与IO任务的执行时间相等。</p><h3 id="NioEventLoopGroup"><a href="#NioEventLoopGroup" class="headerlink" title="NioEventLoopGroup"></a>NioEventLoopGroup</h3><p>NioEventLoopGroup,主要管理eventLoop的生命周期,可以理解为一个线程池,内部维护了一组线程,每个线程(NioEventLoop)负责处理多个Channel上的事件,而一个Channel只对应于一个线程。</p><h3 id="ChannelHandler"><a href="#ChannelHandler" class="headerlink" title="ChannelHandler"></a>ChannelHandler</h3><p>ChannelHandler是一个接口,处理I/O事件或拦截I/O操作,并将其转发到其ChannelPipeline(业务处理链)中的下一个处理程序。</p><p>ChannelHandler本身并没有提供很多方法,因为这个接口有许多的方法需要实现,方便使用期间,可以继承它的子类:</p><ul><li>ChannelInboundHandler用于处理入站I/O事件</li><li>ChannelOutboundHandler用于处理出站I/O操作</li></ul><p>或者使用以下适配器类:</p><ul><li>ChannelInboundHandlerAdapter用于处理入站I/O事件</li><li>ChannelOutboundHandlerAdapter用于处理出站I/O操作</li><li>ChannelDuplexHandler用于处理入站和出站事件</li></ul><h3 id="ChannelHandlerContext"><a href="#ChannelHandlerContext" class="headerlink" title="ChannelHandlerContext"></a>ChannelHandlerContext</h3><p>保存Channel相关的所有上下文信息,同时关联一个ChannelHandler对象</p><h3 id="ChannelPipline"><a href="#ChannelPipline" class="headerlink" title="ChannelPipline"></a>ChannelPipline</h3><p>保存ChannelHandler的List,用于处理或拦截Channel的入站事件和出站操作。 ChannelPipeline实现了一种高级形式的拦截过滤器模式,使用户可以完全控制事件的处理方式,以及Channel中各个的ChannelHandler如何相互交互。</p><p>下图引用Netty的Javadoc4.1中ChannelPipline的说明,描述了ChannelPipeline中ChannelHandler通常如何处理I/O事件。 I/O事件由ChannelInboundHandler或ChannelOutboundHandler处理,并通过调用ChannelHandlerContext中定义的事件传播方法(例如ChannelHandlerContext.fireChannelRead(Object)和ChannelOutboundInvoker.write(Object))转发到其最近的处理程序。</p><pre><code> I/O Request via Channel or ChannelHandlerContext | +---------------------------------------------------+---------------+ | ChannelPipeline | | | \|/ | | +---------------------+ +-----------+----------+ | | | Inbound Handler N | | Outbound Handler 1 | | | +----------+----------+ +-----------+----------+ | | /|\ | | | | \|/ | | +----------+----------+ +-----------+----------+ | | | Inbound Handler N-1 | | Outbound Handler 2 | | | +----------+----------+ +-----------+----------+ | | /|\ . | | . . | | ChannelHandlerContext.fireIN_EVT() ChannelHandlerContext.OUT_EVT()| | [ method call] [method call] | | . . | | . \|/ | | +----------+----------+ +-----------+----------+ | | | Inbound Handler 2 | | Outbound Handler M-1 | | | +----------+----------+ +-----------+----------+ | | /|\ | | | | \|/ | | +----------+----------+ +-----------+----------+ | | | Inbound Handler 1 | | Outbound Handler M | | | +----------+----------+ +-----------+----------+ | | /|\ | | +---------------+-----------------------------------+---------------+ | \|/ +---------------+-----------------------------------+---------------+ | | | | | [ Socket.read() ] [ Socket.write() ] | | | | Netty Internal I/O Threads (Transport Implementation) | +-------------------------------------------------------------------+</code></pre><p>入站事件由自下而上方向的入站处理程序处理,如图左侧所示。 入站Handler处理程序通常处理由图底部的I/O线程生成的入站数据。 通常通过实际输入操作(例如SocketChannel.read(ByteBuffer))从远程读取入站数据。</p><p>出站事件由上下方向处理,如图右侧所示。 出站Handler处理程序通常会生成或转换出站传输,例如write请求。 I/O线程通常执行实际的输出操作,例如SocketChannel.write(ByteBuffer)。</p><p> 在 Netty 中每个 Channel 都有且仅有一个 ChannelPipeline 与之对应, 它们的组成关系如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/8.png" alt><br>一个 Channel 包含了一个 ChannelPipeline, 而 ChannelPipeline 中又维护了一个由 ChannelHandlerContext 组成的双向链表, 并且每个 ChannelHandlerContext 中又关联着一个 ChannelHandler。入站事件和出站事件在一个双向链表中,入站事件会从链表head往后传递到最后一个入站的handler,出站事件会从链表tail往前传递到最前一个出站的handler,两种类型的handler互不干扰。</p><h2 id="工作原理架构"><a href="#工作原理架构" class="headerlink" title="工作原理架构"></a>工作原理架构</h2><p>初始化并启动Netty服务端过程如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 创建mainReactor</span> NioEventLoopGroup boosGroup <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">NioEventLoopGroup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 创建工作线程组</span> NioEventLoopGroup workerGroup <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">NioEventLoopGroup</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">final</span> ServerBootstrap serverBootstrap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ServerBootstrap</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> serverBootstrap <span class="token comment" spellcheck="true">// 组装NioEventLoopGroup </span> <span class="token punctuation">.</span><span class="token function">group</span><span class="token punctuation">(</span>boosGroup<span class="token punctuation">,</span> workerGroup<span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// 设置channel类型为NIO类型</span> <span class="token punctuation">.</span><span class="token function">channel</span><span class="token punctuation">(</span>NioServerSocketChannel<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// 设置连接配置参数</span> <span class="token punctuation">.</span><span class="token function">option</span><span class="token punctuation">(</span>ChannelOption<span class="token punctuation">.</span>SO_BACKLOG<span class="token punctuation">,</span> <span class="token number">1024</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">childOption</span><span class="token punctuation">(</span>ChannelOption<span class="token punctuation">.</span>SO_KEEPALIVE<span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">.</span><span class="token function">childOption</span><span class="token punctuation">(</span>ChannelOption<span class="token punctuation">.</span>TCP_NODELAY<span class="token punctuation">,</span> <span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token comment" spellcheck="true">// 配置入站、出站事件handler</span> <span class="token punctuation">.</span><span class="token function">childHandler</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">ChannelInitializer</span><span class="token operator"><</span>NioSocketChannel<span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">protected</span> <span class="token keyword">void</span> <span class="token function">initChannel</span><span class="token punctuation">(</span>NioSocketChannel ch<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 配置入站、出站事件channel</span> ch<span class="token punctuation">.</span><span class="token function">pipeline</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addLast</span><span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span><span class="token punctuation">;</span> ch<span class="token punctuation">.</span><span class="token function">pipeline</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addLast</span><span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 绑定端口</span> <span class="token keyword">int</span> port <span class="token operator">=</span> <span class="token number">8080</span><span class="token punctuation">;</span> serverBootstrap<span class="token punctuation">.</span><span class="token function">bind</span><span class="token punctuation">(</span>port<span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">addListener</span><span class="token punctuation">(</span>future <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>future<span class="token punctuation">.</span><span class="token function">isSuccess</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Date</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">": 端口["</span> <span class="token operator">+</span> port <span class="token operator">+</span> <span class="token string">"]绑定成功!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> System<span class="token punctuation">.</span>err<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"端口["</span> <span class="token operator">+</span> port <span class="token operator">+</span> <span class="token string">"]绑定失败!"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><ul><li>基本过程如下:</li><li>1 初始化创建2个NioEventLoopGroup,其中boosGroup用于Accetpt连接建立事件并分发请求, workerGroup用于处理I/O读写事件和业务逻辑</li><li>2 基于ServerBootstrap(服务端启动引导类),配置EventLoopGroup、Channel类型,连接参数、配置入站、出站事件handler</li><li>3 绑定端口,开始工作</li></ul><p>结合上面的介绍的Netty Reactor模型,介绍服务端Netty的工作架构图:</p><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/9.png" alt="服务端Netty Reactor工作架构图"><br>server端包含1个Boss NioEventLoopGroup和1个Worker NioEventLoopGroup,NioEventLoopGroup相当于1个事件循环组,这个组里包含多个事件循环NioEventLoop,每个NioEventLoop包含1个selector和1个事件循环线程。</p><p>每个Boss NioEventLoop循环执行的任务包含3步:</p><ul><li>1 轮询accept事件</li><li>2 处理accept I/O事件,与Client建立连接,生成NioSocketChannel,并将NioSocketChannel注册到某个Worker NioEventLoop的Selector上</li><li>3 处理任务队列中的任务,runAllTasks。任务队列中的任务包括用户调用eventloop.execute或schedule执行的任务,或者其它线程提交到该eventloop的任务</li></ul><p>每个Worker NioEventLoop循环执行的任务包含3步:</p><ul><li>1 轮询read、write事件</li><li>2 处理I/O事件,即read、write事件,在NioSocketChannel可读、可写事件发生时进行处理</li><li>3 处理任务队列中的任务,runAllTasks</li></ul><p>其中任务队列中的task有3种典型使用场景</p><ul><li><p>1 用户程序自定义的普通任务</p><pre class="line-numbers language-java"><code class="language-java">ctx<span class="token punctuation">.</span><span class="token function">channel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">eventLoop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">//...</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>2 非当前reactor线程调用channel的各种方法<br>例如在推送系统的业务线程里面,根据用户的标识,找到对应的channel引用,然后调用write类方法向该用户推送消息,就会进入到这种场景。最终的write会提交到任务队列中后被异步消费。</p></li><li><p>3 用户自定义定时任务</p><pre class="line-numbers language-java"><code class="language-java">ctx<span class="token punctuation">.</span><span class="token function">channel</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">eventLoop</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">schedule</span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">Runnable</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">run</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">,</span> <span class="token number">60</span><span class="token punctuation">,</span> TimeUnit<span class="token punctuation">.</span>SECONDS<span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li></ul><h1 id="4-总结"><a href="#4-总结" class="headerlink" title="4 总结"></a>4 总结</h1><p>现在稳定推荐使用的主流版本还是Netty4,Netty5 中使用了 ForkJoinPool,增加了代码的复杂度,但是对性能的改善却不明显,所以这个版本不推荐使用,官网也没有提供下载链接。</p><p>Netty 入门门槛相对较高,其实是因为这方面的资料较少,并不是因为他有多难,大家其实都可以像搞透 Spring 一样搞透 Netty。在学习之前,建议先理解透整个框架原理结构,运行过程,可以少走很多弯路。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://juejin.im/book/5b4bc28bf265da0f60130116?referrer=598ff735f265da3e1c0f9643" target="_blank" rel="noopener">Netty入门与实战:仿写微信 IM 即时通讯系统</a></p><p><a href="https://netty.io/" target="_blank" rel="noopener">Netty官网</a></p><p><a href="http://yihongwei.com/2014/01/netty-4-x-thread-model/" target="_blank" rel="noopener">Netty 4.x学习笔记 - 线程模型</a></p><p><a href="https://juejin.im/book/5b4bc28bf265da0f60130116?referrer=598ff735f265da3e1c0f9643" target="_blank" rel="noopener">Netty入门与实战</a></p><p><a href="https://www.jianshu.com/p/2965fca6bb8f" target="_blank" rel="noopener">理解高性能网络模型</a></p><p><a href="https://wangwei.one/posts/netty-base-theory-intro.html" target="_blank" rel="noopener">Netty基本原理介绍</a></p><p><a href="https://www.oreilly.com/programming/free/files/software-architecture-patterns.pdf" target="_blank" rel="noopener">software-architecture-patterns.pdf</a></p><p><a href="http://www.infoq.com/cn/articles/netty-high-performance" target="_blank" rel="noopener">Netty高性能之道 —— 李林锋</a></p><p>《Netty In Action》</p><p>《Netty权威指南》</p><p><img src="/images/%E7%90%86%E8%A7%A3Netty%E6%A8%A1%E5%9E%8B%E6%9E%B6%E6%9E%84/10.png" alt></p>]]></content>
<categories>
<category> 技术框架 </category>
</categories>
</entry>
<entry>
<title>理解Java的分级引用模型</title>
<link href="/2019/10/13/li-jie-java-de-fen-ji-yin-yong-mo-xing/"/>
<url>/2019/10/13/li-jie-java-de-fen-ji-yin-yong-mo-xing/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3Java%E7%9A%84%E5%88%86%E7%BA%A7%E5%BC%95%E7%94%A8%E6%A8%A1%E5%9E%8B/0.jpg" alt></p><pre><code>作者 陈彩华文章转载交流请联系 [email protected]</code></pre><p>本文通过探析Java中的引用模型,分析比较强引用、软引用、弱引用、虚引用的概念及使用场景,<strong>知其然且知其所以然</strong>,希望给大家在实际开发实践、学习开源项目提供参考。</p><h1 id="1-Java的引用"><a href="#1-Java的引用" class="headerlink" title="1 Java的引用"></a>1 Java的引用</h1><p>对于Java中的垃圾回收机制来说,对象是否被应该回收的取决于该对象是否被引用。因此,引用也是JVM进行内存管理的一个重要概念。Java中是JVM负责内存的分配和回收,这是它的优点(使用方便,程序不用再像使用C语言那样担心内存),但同时也是它的缺点(不够灵活)。由此,Java提供了引用分级模型,可以<strong>定义Java对象重要性和优先级,提高JVM内存回收的执行效率</strong>。</p><p>关于引用的定义,在JDK1.2之前,如果reference类型的数据中存储的数值代表的是另一块内存的起始地址,就称为这块内存代表着一个引用;JDK1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种。</p><p>软引用对象和弱应用对象主要用于:当内存空间还足够,则能保存在内存之中;如果内存空间在垃圾收集之后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的使用场景。</p><p>而虚引用对象用于替代不靠谱的finalize方法,可以获取对象的回收事件,来做资源清理工作。</p><h1 id="2-对象生命周期"><a href="#2-对象生命周期" class="headerlink" title="2 对象生命周期"></a>2 对象生命周期</h1><p>## 2.1 无分级引用对象生命周期<br>前面提到,分层引用的模型是用于内存回收,没有分级引用对象下,一个对象从创建到回收的生命周期可以简单地用下图概括:对象被创建,被使用,有资格被收集,最终被收集,阴影区域表示对象“强可达”时间:<br><img src="/images/%E7%90%86%E8%A7%A3Java%E7%9A%84%E5%88%86%E7%BA%A7%E5%BC%95%E7%94%A8%E6%A8%A1%E5%9E%8B/1.png" alt="对象生命周期(无分级引用)"></p><h2 id="2-2-有分级引用对象生命周期"><a href="#2-2-有分级引用对象生命周期" class="headerlink" title="2.2 有分级引用对象生命周期"></a>2.2 有分级引用对象生命周期</h2><p>JDK1.2引入java.lang.ref程序包之后,对象的生命周期多了3个阶段,软可达,弱可达,虚可达,这些状态仅适用于符合垃圾回收条件的对象,这些对象处于非强引用阶段,而且需要基于java.lang.ref包中的相关的引用对象类来指示标明。</p><ul><li><p>软可达<br>软可达对象用SoftReference来指示标明,并没有强引用,垃圾回收器会尽可能长时间地保留对象,但是会在抛出OutOfMemoryError异常之前收集它。</p></li><li><p>弱可达<br>弱可达对象用WeakReference来指示标明,并没有强引用或软引用,垃圾回收器会随时回收对象,并不会尝试保留它,但是会在抛出OutOfMemoryError异常之前收集它。</p></li></ul><p>假设垃圾收集器在某个时间点确定对象是弱可达的。 那时它将原子地清除该弱可达引用对象关联的对象。</p><ul><li>虚可达<br>虚可达对象用PhantomReference来指示标明,它已经被标记选中进行垃圾回收并且它的finalizer(如果有)已经运行。在这种情况下,术语“可达”实际上是用词不当,因为您无法访问实际对象。</li></ul><p><img src="/images/%E7%90%86%E8%A7%A3Java%E7%9A%84%E5%88%86%E7%BA%A7%E5%BC%95%E7%94%A8%E6%A8%A1%E5%9E%8B/2.png" alt="分级引用作用时间在对象生命周期中的位置"></p><p>对象生命周期图中出现三个新的可选状态会造成一些困惑。逻辑顺序上是从强可达到软,弱和虚,最终到回收,但实际的情况取决于程序创建的参考对象。但如果创建WeakReference但不创建SoftReference,则对象直接从强可达到弱到达最终到收集。</p><h1 id="3-强引用"><a href="#3-强引用" class="headerlink" title="3 强引用"></a>3 强引用</h1><p> 强引用就是指在程序代码之中普遍存在的,比如下面这段代码中的obj和str都是强引用:</p><pre class="line-numbers language-java"><code class="language-java">Object obj <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Object</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>String str <span class="token operator">=</span> <span class="token string">"hello world"</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>只要强引用还存在,垃圾收集器永远不会回收被引用的对象,即使在内存不足的情况下,JVM即使抛出OutOfMemoryError异常也不会回收这种对象。</p><p>实际使用上,可以通过把引用显示赋值为null来中断对象与强引用之前的关联,如果没有任何引用执行对象,垃圾收集器将在合适的时间回收对象。</p><p>例如ArrayList类的remove方法中就是通过将引用赋值为null来实现清理工作的:</p><pre class="line-numbers language-java"><code class="language-java"> <span class="token comment" spellcheck="true">/** * Removes the element at the specified position in this list. * Shifts any subsequent elements to the left (subtracts one from their * indices). * * @param index the index of the element to be removed * @return the element that was removed from the list * @throws IndexOutOfBoundsException {@inheritDoc} */</span> <span class="token keyword">public</span> E <span class="token function">remove</span><span class="token punctuation">(</span><span class="token keyword">int</span> index<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token function">rangeCheck</span><span class="token punctuation">(</span>index<span class="token punctuation">)</span><span class="token punctuation">;</span> modCount<span class="token operator">++</span><span class="token punctuation">;</span> E oldValue <span class="token operator">=</span> <span class="token function">elementData</span><span class="token punctuation">(</span>index<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">int</span> numMoved <span class="token operator">=</span> size <span class="token operator">-</span> index <span class="token operator">-</span> <span class="token number">1</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>numMoved <span class="token operator">></span> <span class="token number">0</span><span class="token punctuation">)</span> System<span class="token punctuation">.</span><span class="token function">arraycopy</span><span class="token punctuation">(</span>elementData<span class="token punctuation">,</span> index<span class="token operator">+</span><span class="token number">1</span><span class="token punctuation">,</span> elementData<span class="token punctuation">,</span> index<span class="token punctuation">,</span> numMoved<span class="token punctuation">)</span><span class="token punctuation">;</span> elementData<span class="token punctuation">[</span><span class="token operator">--</span>size<span class="token punctuation">]</span> <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// clear to let GC do its work</span> <span class="token keyword">return</span> oldValue<span class="token punctuation">;</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h1 id="4-引用对象"><a href="#4-引用对象" class="headerlink" title="4 引用对象"></a>4 引用对象</h1><p>介绍软引用、弱引用和虚引用之前,有必要介绍一下引用对象,<br>引用对象是程序代码和其他对象之间的间接层,称为引用对象。每个引用对象都围绕对象的引用构造,并且不能更改引用值。</p><p><img src="/images/%E7%90%86%E8%A7%A3Java%E7%9A%84%E5%88%86%E7%BA%A7%E5%BC%95%E7%94%A8%E6%A8%A1%E5%9E%8B/3.png" alt><br>引用对象提供get()来获得其引用值的一个强引用,垃圾收集器可能随时回收引用值所指的对象。<br>一旦对象被回收,get()方法将返回null,要正确使用引用对象,下面使用SoftReference(软引用对象)作为参考示例:</p><pre class="line-numbers language-java"><code class="language-java"> <span class="token comment" spellcheck="true">/** * 简单使用demo */</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">simpleUseDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> List<span class="token operator"><</span>String<span class="token operator">></span> myList <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> SoftReference<span class="token operator"><</span>List<span class="token operator"><</span>String<span class="token operator">>></span> refObj <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SoftReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span>myList<span class="token punctuation">)</span><span class="token punctuation">;</span> List<span class="token operator"><</span>String<span class="token operator">></span> list <span class="token operator">=</span> refObj<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>null <span class="token operator">!=</span> list<span class="token punctuation">)</span> <span class="token punctuation">{</span> list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">"hello"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 整个列表已经被垃圾回收了,做其他处理</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>也就是说,使用时:</p><ul><li><p>1、必须经常检查引用值是否为null<br>垃圾收集器可能随时回收引用对象,如果轻率地使用引用值,迟早会得到一个NullPointerException。</p></li><li><p>2、必须使用强引用来指向引用对象返回的值<br>垃圾收集器可能在任何时间回收引用对象,即使在一个表达式中间。</p><pre class="line-numbers language-java"><code class="language-java"> <span class="token comment" spellcheck="true">/** * 正确使用引用对象demo */</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">trueUseRefObjDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> List<span class="token operator"><</span>String<span class="token operator">></span> myList <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> SoftReference<span class="token operator"><</span>List<span class="token operator"><</span>String<span class="token operator">>></span> refObj <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SoftReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span>myList<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 正确的使用,使用强引用指向对象保证获得对象之后不会被回收</span> List<span class="token operator"><</span>String<span class="token operator">></span> list <span class="token operator">=</span> refObj<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>null <span class="token operator">!=</span> list<span class="token punctuation">)</span> <span class="token punctuation">{</span> list<span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">"hello"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 整个列表已经被垃圾回收了,做其他处理</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token comment" spellcheck="true">/** * 错误使用引用对象demo */</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">falseUseRefObjDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> List<span class="token operator"><</span>String<span class="token operator">></span> myList <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ArrayList</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> SoftReference<span class="token operator"><</span>List<span class="token operator"><</span>String<span class="token operator">>></span> refObj <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SoftReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span>myList<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// XXX 错误的使用,在检查对象非空到使用对象期间,对象可能已经被回收</span> <span class="token comment" spellcheck="true">// 可能出现空指针异常</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>null <span class="token operator">!=</span> refObj<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> refObj<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">add</span><span class="token punctuation">(</span><span class="token string">"hello"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>3、必须持有引用对象的强引用<br>如果创建引用对象,没有持有对象的强引用,那么引用对象本身将被垃圾收集器回收。</p></li><li><p>4、当引用值没有被其他强引用指向时,软引用、弱引用和虚引用才会发挥作用,引用对象的存在就是为了方便追踪并高效垃圾回收。</p></li></ul><h1 id="5-软引用、弱引用和虚引用"><a href="#5-软引用、弱引用和虚引用" class="headerlink" title="5 软引用、弱引用和虚引用"></a>5 软引用、弱引用和虚引用</h1><p>引用对象的3个重要实现类位于java.lang.ref包下,分别是软引用SoftReference、弱引用WeakReference和虚引用PhantomReference。</p><h2 id="5-1-软引用"><a href="#5-1-软引用" class="headerlink" title="5.1 软引用"></a>5.1 软引用</h2><p>软引用用来描述一些还有用但非必需的对象。对于软引用关联着的对象,在系统将要发生抛出OutOfMemoryError异常之前,将会把这些对象列入回收范围之内进行第二次回收。如果这次回收还没有足够的内存,才会抛出OutOfMemoryError异常。在JDK1.2之后,提供了SoftReference类来实现软引用。</p><p>下面是一个使用示例:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">import</span> java<span class="token punctuation">.</span>lang<span class="token punctuation">.</span>ref<span class="token punctuation">.</span>SoftReference<span class="token punctuation">;</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SoftRefDemo</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token punctuation">{</span> SoftReference<span class="token operator"><</span>String<span class="token operator">></span> sr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">SoftReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"hello world "</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// hello world</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>sr<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>JDK文档中提到:软引用适用于对内存敏感的缓存:每个缓存对象都是通过访问的 SoftReference,如果JVM决定需要内存空间,那么它将清除回收部分或全部软引用对应的对象。如果它不需要空间,则SoftReference指示对象保留在堆中,并且可以通过程序代码访问。在这种情况下,当它们被积极使用时,它们被强引用,否则会被软引用。如果清除了软引用,则需要刷新缓存。</p><p>实际使用上,要除非缓存的对象非常大,每个数量级为几千字节,才值得考虑使用软引用对象。例如:实现一个文件服务器,它需要定期检索相同的文件,或者需要缓存大型对象图。如果对象很小,必须清除很多对象才能产生影响,那么不建议使用,因为清除软引用对象会增加整个过程的开销。</p><h2 id="5-2-弱引用"><a href="#5-2-弱引用" class="headerlink" title="5.2 弱引用"></a>5.2 弱引用</h2><p>弱引用也是用来描述非必需对象,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发送之前。<strong>当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象</strong>。</p><p>在JDK1.2之后,提供了WeakReference类来实现弱引用。</p><pre class="line-numbers language-java"><code class="language-java"> <span class="token comment" spellcheck="true">/** * 简单使用弱引用demo */</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">simpleUseWeakRefDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> WeakReference<span class="token operator"><</span>String<span class="token operator">></span> sr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"hello world "</span> <span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// before gc -> hello world </span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"before gc -> "</span> <span class="token operator">+</span> sr<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 通知JVM的gc进行垃圾回收</span> System<span class="token punctuation">.</span><span class="token function">gc</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// after gc -> null</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"after gc -> "</span> <span class="token operator">+</span> sr<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>可以看到被弱引用关联的对象,在gc之后被回收掉。<br>有意思的地方是,如果把上面代码中的:</p><pre class="line-numbers language-java"><code class="language-java">WeakReference<span class="token operator"><</span>String<span class="token operator">></span> sr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"hello world "</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>改为</p><pre class="line-numbers language-java"><code class="language-java">WeakReference<span class="token operator"><</span>String<span class="token operator">></span> sr <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token string">"hello world "</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>程序将输出</p><pre><code>before gc -> hello world after gc -> hello world </code></pre><p>这是因为使用Java的String直接赋值和使用new区别在于:</p><ul><li>new 会在堆区创建一个可以被正常回收的对象。</li><li>String直接赋值,例如:String str = String( “Hello”);<br>JVM首先在string池内里面看找不找到字符串 “Hello”,找到,不做任何事情;<br>否则,创建新的String对象,放到String常量池里面(常量池Hotspot1.7之前存于永生代,Hotspot1.7和1.7之后的版本存于堆区,通常不会被gc回收)。同时,由于遇到了new,还会在内存上(不是String常量池里面)创建String对象存储 “Hello”,并将内存上的(不是String池内的)String对象返回给str。</li></ul><p><strong>WeakHashMap</strong><br>为了更方便使用弱引用,Java还提供了WeakHashMap,功能类似HashMap,内部实现是用弱引用对key进行包装,当某个key对象没有任何强引用指向,gc会自动回收key和value对象。</p><pre class="line-numbers language-java"><code class="language-java"> <span class="token comment" spellcheck="true">/** * weakHashMap使用demo */</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">weakHashMapDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> WeakHashMap<span class="token operator"><</span>String<span class="token punctuation">,</span>String<span class="token operator">></span> weakHashMap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakHashMap</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> String key1 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"key1"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> String key2 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"key2"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> String key3 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"key3"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> weakHashMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>key1<span class="token punctuation">,</span> <span class="token string">"value1"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> weakHashMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>key2<span class="token punctuation">,</span> <span class="token string">"value2"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> weakHashMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span>key3<span class="token punctuation">,</span> <span class="token string">"value3"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 使没有任何强引用指向key1</span> key1 <span class="token operator">=</span> null<span class="token punctuation">;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"before gc weakHashMap = "</span> <span class="token operator">+</span> weakHashMap <span class="token operator">+</span> <span class="token string">" , size="</span> <span class="token operator">+</span> weakHashMap<span class="token punctuation">.</span><span class="token function">size</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 通知JVM的gc进行垃圾回收</span> System<span class="token punctuation">.</span><span class="token function">gc</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"after gc weakHashMap = "</span> <span class="token operator">+</span> weakHashMap <span class="token operator">+</span> <span class="token string">" , size="</span><span class="token operator">+</span> weakHashMap<span class="token punctuation">.</span><span class="token function">size</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>程序输出:</p><pre class="line-numbers language-java"><code class="language-java">before<span class="token operator">:</span> gc weakHashMap <span class="token operator">=</span> <span class="token punctuation">{</span>key1<span class="token operator">=</span>value1<span class="token punctuation">,</span> key2<span class="token operator">=</span>value2<span class="token punctuation">,</span> key3<span class="token operator">=</span>value3<span class="token punctuation">}</span> <span class="token punctuation">,</span> size<span class="token operator">=</span><span class="token number">3</span>after<span class="token operator">:</span> gc weakHashMap <span class="token operator">=</span> <span class="token punctuation">{</span>key2<span class="token operator">=</span>value2<span class="token punctuation">,</span> key3<span class="token operator">=</span>value3<span class="token punctuation">}</span> <span class="token punctuation">,</span> size<span class="token operator">=</span><span class="token number">2</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p>WeakHashMap比较适用于缓存的场景,例如Tomcat的缓存就用到。</p><h2 id="5-3-引用队列"><a href="#5-3-引用队列" class="headerlink" title="5.3 引用队列"></a>5.3 引用队列</h2><p>介绍虚引用之前,先介绍引用队列:<br>在使用引用对象时,通过判断get()方法返回的值是否为null来判断对象是否已经被回收,当这样做并不是非常高效,特别是当我们有很多引用对象,如果想找出哪些对象已经被回收,需要遍历所有所有对象。</p><p>更好的方案是使用引用队列,在构造引用对象时与队列关联,当gc(垃圾回收线程)准备回收一个对象时,如果发现它还仅有软引用(或弱引用,或虚引用)指向它,就会在回收该对象之前,把这个软引用(或弱引用,或虚引用)加入到与之关联的引用队列(ReferenceQueue)中。</p><p><strong>如果一个软引用(或弱引用,或虚引用)对象本身在引用队列中,就说明该引用对象所指向的对象被回收了</strong>,所以要找出所有被回收的对象,只需要遍历引用队列。</p><p>当软引用(或弱引用,或虚引用)对象所指向的对象被回收了,那么这个引用对象本身就没有价值了,如果程序中存在大量的这类对象(注意,我们创建的软引用、弱引用、虚引用对象本身是个强引用,不会自动被gc回收),就会浪费内存。因此我们这就可以手动回收位于引用队列中的引用对象本身。</p><pre class="line-numbers language-java"><code class="language-java"> <span class="token comment" spellcheck="true">/** * 引用队列demo */</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">refQueueDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> ReferenceQueue<span class="token operator"><</span>String<span class="token operator">></span> refQueue <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReferenceQueue</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 用于检查引用队列中的引用值被回收</span> Thread checkRefQueueThread <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Thread</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span><span class="token operator">></span> <span class="token punctuation">{</span> <span class="token keyword">while</span> <span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> Reference<span class="token operator"><</span><span class="token operator">?</span> <span class="token keyword">extends</span> <span class="token class-name">String</span><span class="token operator">></span> clearRef <span class="token operator">=</span> refQueue<span class="token punctuation">.</span><span class="token function">poll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>null <span class="token operator">!=</span> clearRef<span class="token punctuation">)</span> <span class="token punctuation">{</span> System<span class="token punctuation">.</span>out <span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"引用对象被回收, ref = "</span> <span class="token operator">+</span> clearRef <span class="token operator">+</span> <span class="token string">", value = "</span> <span class="token operator">+</span> clearRef<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">;</span> checkRefQueueThread<span class="token punctuation">.</span><span class="token function">start</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> WeakReference<span class="token operator"><</span>String<span class="token operator">></span> weakRef1 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"value1"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> refQueue<span class="token punctuation">)</span><span class="token punctuation">;</span> WeakReference<span class="token operator"><</span>String<span class="token operator">></span> weakRef2 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"value2"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> refQueue<span class="token punctuation">)</span><span class="token punctuation">;</span> WeakReference<span class="token operator"><</span>String<span class="token operator">></span> weakRef3 <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">WeakReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token keyword">new</span> <span class="token class-name">String</span><span class="token punctuation">(</span><span class="token string">"value3"</span><span class="token punctuation">)</span><span class="token punctuation">,</span> refQueue<span class="token punctuation">)</span><span class="token punctuation">;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"ref1 value = "</span> <span class="token operator">+</span> weakRef1<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">", ref2 value = "</span> <span class="token operator">+</span> weakRef2<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">", ref3 value = "</span> <span class="token operator">+</span> weakRef3<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"开始通知JVM的gc进行垃圾回收"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 通知JVM的gc进行垃圾回收</span> System<span class="token punctuation">.</span><span class="token function">gc</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>程序输出:</p><pre class="line-numbers language-java"><code class="language-java">ref1 value <span class="token operator">=</span> value1<span class="token punctuation">,</span> ref2 value <span class="token operator">=</span> value2<span class="token punctuation">,</span> ref3 value <span class="token operator">=</span> value3开始通知JVM的gc进行垃圾回收引用对象被回收<span class="token punctuation">,</span> ref <span class="token operator">=</span> java<span class="token punctuation">.</span>lang<span class="token punctuation">.</span>ref<span class="token punctuation">.</span>WeakReference<span class="token annotation punctuation">@48c6cd96</span><span class="token punctuation">,</span> value<span class="token operator">=</span>null引用对象被回收<span class="token punctuation">,</span> ref <span class="token operator">=</span> java<span class="token punctuation">.</span>lang<span class="token punctuation">.</span>ref<span class="token punctuation">.</span>WeakReference<span class="token annotation punctuation">@46013afe</span><span class="token punctuation">,</span> value<span class="token operator">=</span>null引用对象被回收<span class="token punctuation">,</span> ref <span class="token operator">=</span> java<span class="token punctuation">.</span>lang<span class="token punctuation">.</span>ref<span class="token punctuation">.</span>WeakReference<span class="token annotation punctuation">@423ea6e6</span><span class="token punctuation">,</span> value<span class="token operator">=</span>null<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="5-4-虚引用"><a href="#5-4-虚引用" class="headerlink" title="5.4 虚引用"></a>5.4 虚引用</h2><p>虚引用也称为幽灵引用或者幻影引用,不同于软引用和弱引用,虚引用不用于访问引用对象所指示的对象,相反,<strong>通过不断轮询虚引用对象关联的引用队列,可以得到对象回收事件</strong>。一个对象是否有虚引用的存在,完全不会对其生产时间构成影响,也无法通过虚引用来取得一个对象实例。虽然这看起来毫无意义,但它实际上可以用来做对象回收时<strong>资源清理、释放</strong>,它比finalize更灵活,我们可以基于虚引用做更安全可靠的对象关联的资源回收。</p><ul><li>finalize的问题<ul><li>Java语言规范并不保证finalize方法会被及时地执行、而且根本不会保证它们会被执行<br>如果可用内存没有被耗尽,垃圾收集器不会运行,finalize方法也不会被执行。</li><li>性能问题<br>JVM通常在单独的低优先级线程中完成finalize的执行。</li><li>对象再生问题<br>finalize方法中,可将待回收对象赋值给GC Roots可达的对象引用,从而达到对象再生的目的。</li></ul></li></ul><p>针对不靠谱finalize方法,完全可以使用虚引用来实现。在JDK1.2之后,提供了PhantomReference类来实现虚引用。</p><p>下面是简单的使用例子,通过访问引用队列可以得到对象的回收事件:</p><pre class="line-numbers language-java"><code class="language-java"> <span class="token comment" spellcheck="true">/** * 简单使用虚引用demo * 虚引用在实现一个对象被回收之前必须做清理操作是很有用的,比finalize()方法更灵活 */</span> <span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">simpleUsePhantomRefDemo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> InterruptedException <span class="token punctuation">{</span> Object obj <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">Object</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> ReferenceQueue<span class="token operator"><</span>Object<span class="token operator">></span> refQueue <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ReferenceQueue</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> PhantomReference<span class="token operator"><</span>Object<span class="token operator">></span> phantomRef <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">PhantomReference</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span>obj<span class="token punctuation">,</span> refQueue<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// null</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>phantomRef<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// null</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>refQueue<span class="token punctuation">.</span><span class="token function">poll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> obj <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 通知JVM的gc进行垃圾回收</span> System<span class="token punctuation">.</span><span class="token function">gc</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// null, 调用phantomRef.get()不管在什么情况下会一直返回null</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>phantomRef<span class="token punctuation">.</span><span class="token function">get</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 当GC发现了虚引用,GC会将phantomRef插入进我们之前创建时传入的refQueue队列</span> <span class="token comment" spellcheck="true">// 注意,此时phantomRef对象,并没有被GC回收,在我们显式地调用refQueue.poll返回phantomRef之后</span> <span class="token comment" spellcheck="true">// 当GC第二次发现虚引用,而此时JVM将phantomRef插入到refQueue会插入失败,此时GC才会对phantomRef对象进行回收</span> Thread<span class="token punctuation">.</span><span class="token function">sleep</span><span class="token punctuation">(</span><span class="token number">200</span><span class="token punctuation">)</span><span class="token punctuation">;</span> Reference<span class="token operator"><</span><span class="token operator">?</span><span class="token operator">></span> pollObj <span class="token operator">=</span> refQueue<span class="token punctuation">.</span><span class="token function">poll</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// java.lang.ref.PhantomReference@1540e19d</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>pollObj<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>null <span class="token operator">!=</span> pollObj<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">// 进行资源回收的操作</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>比较常见的,可以基于虚引用实现JDBC连接池,锁的释放等场景。<br>以连接池为例,调用方正常情况下使用完连接,需要把连接释放回池中,但是不可避免有可能程序有bug,造成连接没有正常释放回池中。基于虚引用对Connection对象进行包装,并关联引用队列,就可以通过轮询引用队列检查哪些连接对象已经被GC回收,释放相关连接资源。<a href="https://github.com/caison/caison-blog-demo" target="_blank" rel="noopener">具体实现已上传github的caison-blog-demo仓库</a>。</p><h1 id="6-总结"><a href="#6-总结" class="headerlink" title="6 总结"></a>6 总结</h1><p>对比一下几种引用对象的不同:<br>|引用类型 | GC回收时间 |常见用途|生存时间|<br>| :- | :-: | :-:| :-:|<br>|强引用|永不|对象的一般状态|JVM停止运行时|<br>|软引用|内存不足时|对象缓存|内存不足时终止|<br>|弱引用|GC时|对象缓存|GC后终止|</p><p>虚引用,配合引用队列使用,通过不断轮询引用队列获取对象回收事件。</p><p>虽然引用对象是一个非常有用的工具来管理你的内存消耗,但有时它们是不够的,或者是过度设计的 。例如,使用一个Map来缓存从数据库中读取的数据。虽然可以使用弱引用来作为缓存,但最终程序需要运行一定量的内存。如果不能给它足够实际足够的资源完成任何工作,那么错误恢复机制有多强大也没有用。</p><p>当遇到OutOfMemoryError错误,第一反应是要弄清楚它为什么会发生,也许真的是程序有bug,也许是可用内存设置的太低。</p><p>在开发过程中,应该制定程序具体的使用内存大小,而已要关注实际使用中用了多少内存。大多数应用程序在实际运行负载下,程序的内存占用会达到稳定状态,可以用此来作为参考来设置合理的堆大小。如果程序的内存使用量随着时间的推移而上升,很有可能是因为当对象不再使用时仍然拥有对对象的强引用。引用对象在这里可能会有所帮助,但更有可能是把它当做一个bug来进行修复。</p><p>文章所有涉及源码已经上传github,地址:<a href="https://github.com/caison/caison-blog-demo" target="_blank" rel="noopener">https://github.com/caison/caison-blog-demo</a></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://github.com/farmerjohngit/myblog/issues/10" target="_blank" rel="noopener">Java引用类型原理剖析</a></p><p><a href="http://www.kdgregory.com/index.php?page=java.refobj" target="_blank" rel="noopener">Java Reference Objects</a></p><p>《深入理解Java虚拟机——JVM高级特性与最佳实践(第2版)》</p><p><a href="https://time.geekbang.org/column/intro/82?code=w8EZ6RGOQApZJ5tpAzP8dRzeVHxZ4q%2FfOdSbSZzbkhc%3D" target="_blank" rel="noopener">Java核心技术36讲</a></p><p><a href="https://time.geekbang.org/column/intro/108?code=XamkmJYooKBPKe8hT7otClsFDeVHb6rptMYHTdgiRPU%3D" target="_blank" rel="noopener">深入拆解 Java 虚拟机</a></p><p> <a href="https://www.cnblogs.com/dolphin0520/p/3784171.html" target="_blank" rel="noopener">Java 如何有效地避免OOM:善于利用软引用和弱引用</a></p><p><a href="https://blog.csdn.net/imzoer/article/details/8044900" target="_blank" rel="noopener">Java幽灵引用的作用</a></p><p><a href="https://docs.oracle.com/javase/7/docs/api/java/lang/ref/package-summary.html#package_description" target="_blank" rel="noopener">oracle官方文档</a></p><p><img src="/images/%E7%90%86%E8%A7%A3Java-GC%E5%8E%9F%E7%90%86%E5%92%8C%E8%B0%83%E4%BC%98/16.png" alt></p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>理解Java中SPI机制</title>
<link href="/2019/10/13/li-jie-java-zhong-spi-ji-zhi/"/>
<url>/2019/10/13/li-jie-java-zhong-spi-ji-zhi/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3Java%E4%B8%ADSPI%E6%9C%BA%E5%88%B6/0.jpg" alt></p><p>本文通过探析JDK提供的,在开源项目中比较常用的Java SPI机制,希望给大家在实际开发实践、学习开源项目提供参考。</p><h1 id="1-SPI是什么"><a href="#1-SPI是什么" class="headerlink" title="1 SPI是什么"></a>1 SPI是什么</h1><p>SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。</p><p>整体机制图如下:<br><img src="/images/%E7%90%86%E8%A7%A3Java%E4%B8%ADSPI%E6%9C%BA%E5%88%B6/1.png" alt><br>Java SPI 实际上是“<strong>基于接口的编程+策略模式+配置文件</strong>”组合实现的动态加载机制。</p><p>系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。<br>Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是<strong>解耦</strong>。</p><h1 id="2-使用场景"><a href="#2-使用场景" class="headerlink" title="2 使用场景"></a>2 使用场景</h1><p>概括地说,适用于:<strong>调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略</strong></p><p>比较常见的例子:</p><ul><li>数据库驱动加载接口实现类的加载<br>JDBC加载不同类型数据库的驱动</li><li>日志门面接口实现类加载<br>SLF4J加载不同提供商的日志实现类</li><li>Spring<br>Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等</li><li>Dubbo<br>Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口</li></ul><h1 id="3-使用介绍"><a href="#3-使用介绍" class="headerlink" title="3 使用介绍"></a>3 使用介绍</h1><p>要使用Java SPI,需要遵循如下约定:</p><ul><li>1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;</li><li>2、接口实现类所在的jar包放在主程序的classpath中;</li><li>3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;</li><li>4、SPI的实现类必须携带一个不带参数的构造方法;</li></ul><h2 id="示例代码"><a href="#示例代码" class="headerlink" title="示例代码"></a>示例代码</h2><p><strong>步骤1</strong>、定义一组接口 (假设是org.foo.demo.IShout),并写出接口的一个或多个实现,(假设是org.foo.demo.animal.Dog、org.foo.demo.animal.Cat)。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">interface</span> <span class="token class-name">IShout</span> <span class="token punctuation">{</span> <span class="token keyword">void</span> <span class="token function">shout</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Cat</span> <span class="token keyword">implements</span> <span class="token class-name">IShout</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">shout</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"miao miao"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">Dog</span> <span class="token keyword">implements</span> <span class="token class-name">IShout</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">shout</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span><span class="token string">"wang wang"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p><strong>步骤2</strong>、在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (org.foo.demo.IShout文件),内容是要应用的实现类(这里是org.foo.demo.animal.Dog和org.foo.demo.animal.Cat,每行一个类)。</p><p>文件位置</p><pre class="line-numbers language-java"><code class="language-java"><span class="token operator">-</span> src <span class="token operator">-</span>main <span class="token operator">-</span>resources <span class="token operator">-</span> META<span class="token operator">-</span>INF <span class="token operator">-</span> services <span class="token operator">-</span> org<span class="token punctuation">.</span>foo<span class="token punctuation">.</span>demo<span class="token punctuation">.</span>IShout<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>文件内容</p><pre class="line-numbers language-java"><code class="language-java">org<span class="token punctuation">.</span>foo<span class="token punctuation">.</span>demo<span class="token punctuation">.</span>animal<span class="token punctuation">.</span>Dogorg<span class="token punctuation">.</span>foo<span class="token punctuation">.</span>demo<span class="token punctuation">.</span>animal<span class="token punctuation">.</span>Cat<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p><strong>步骤3</strong>、使用 ServiceLoader 来加载配置文件中指定的实现。</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">SPIMain</span> <span class="token punctuation">{</span> <span class="token keyword">public</span> <span class="token keyword">static</span> <span class="token keyword">void</span> <span class="token function">main</span><span class="token punctuation">(</span>String<span class="token punctuation">[</span><span class="token punctuation">]</span> args<span class="token punctuation">)</span> <span class="token punctuation">{</span> ServiceLoader<span class="token operator"><</span>IShout<span class="token operator">></span> shouts <span class="token operator">=</span> ServiceLoader<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span>IShout<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">for</span> <span class="token punctuation">(</span>IShout s <span class="token operator">:</span> shouts<span class="token punctuation">)</span> <span class="token punctuation">{</span> s<span class="token punctuation">.</span><span class="token function">shout</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>代码输出:</p><pre class="line-numbers language-java"><code class="language-java">wang wangmiao miao<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><h1 id="4-原理解析"><a href="#4-原理解析" class="headerlink" title="4 原理解析"></a>4 原理解析</h1><p>首先看ServiceLoader类的签名类的成员变量:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">ServiceLoader</span><span class="token operator"><</span>S<span class="token operator">></span> <span class="token keyword">implements</span> <span class="token class-name">Iterable</span><span class="token operator"><</span>S<span class="token operator">></span><span class="token punctuation">{</span><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> String PREFIX <span class="token operator">=</span> <span class="token string">"META-INF/services/"</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 代表被加载的类或者接口</span> <span class="token keyword">private</span> <span class="token keyword">final</span> Class<span class="token operator"><</span>S<span class="token operator">></span> service<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 用于定位,加载和实例化providers的类加载器</span> <span class="token keyword">private</span> <span class="token keyword">final</span> ClassLoader loader<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 创建ServiceLoader时采用的访问控制上下文</span> <span class="token keyword">private</span> <span class="token keyword">final</span> AccessControlContext acc<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 缓存providers,按实例化的顺序排列</span> <span class="token keyword">private</span> LinkedHashMap<span class="token operator"><</span>String<span class="token punctuation">,</span>S<span class="token operator">></span> providers <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">LinkedHashMap</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// 懒查找迭代器</span> <span class="token keyword">private</span> LazyIterator lookupIterator<span class="token punctuation">;</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>参考具体ServiceLoader具体源码,代码量不多,加上注释一共587行,梳理了一下,实现的流程如下:</p><ul><li><p>1 应用程序调用ServiceLoader.load方法<br>ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括: </p><ul><li>loader(ClassLoader类型,类加载器)</li><li>acc(AccessControlContext类型,访问控制器)</li><li>providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)</li><li>lookupIterator(实现迭代器功能)</li></ul></li><li><p>2 应用程序通过迭代器接口获取对象实例<br>ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。<br>如果没有缓存,执行类的装载,实现如下:</p></li><li><p>(1) 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader<strong>可以跨越jar包获取META-INF下的配置文件</strong>,具体加载配置的实现代码如下: </p><pre><code> try { String fullName = PREFIX + service.getName(); if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); }</code></pre></li><li><p>(2) 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。</p></li><li><p>(3) 把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)<br>然后返回实例对象。</p></li></ul><h1 id="5-总结"><a href="#5-总结" class="headerlink" title="5 总结"></a>5 总结</h1><p><strong>优点</strong>:<br>使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。</p><p>相比使用提供接口jar包,供第三方服务模块实现接口的方式,SPI的方式使得源框架,不必关心接口的实现类的路径,可以不用通过下面的方式获取接口实现类:</p><ul><li>代码硬编码import 导入实现类</li><li>指定类全路径反射获取:例如在JDBC4.0之前,JDBC中获取数据库驱动类需要通过<strong>Class.forName(“com.mysql.jdbc.Driver”)</strong>,类似语句先动态加载数据库相关的驱动,然后再进行获取连接等的操作</li><li>第三方服务模块把接口实现类实例注册到指定地方,源框架从该处访问实例</li></ul><p>通过SPI的方式,第三方服务模块实现接口后,在第三方的项目代码的META-INF/services目录下的配置文件指定实现类的全路径名,源码框架即可找到实现类</p><p><strong>缺点</strong>:</p><ul><li>虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。</li><li>多个并发多线程使用ServiceLoader类的实例是不安全的。</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://time.geekbang.org/column/intro/82?code=w8EZ6RGOQApZJ5tpAzP8dRzeVHxZ4q%2FfOdSbSZzbkhc%3D" target="_blank" rel="noopener">Java核心技术36讲</a><br><a href="https://docs.oracle.com/javase/tutorial/ext/basics/spi.html" target="_blank" rel="noopener">The Java™ Tutorials</a><br><a href="https://docs.oracle.com/javase/8/docs/api/java/util/ServiceLoader.html" target="_blank" rel="noopener">Java Doc</a><br><a href="https://www.developer.com/java/article.php/3848881/Service-Provider-Interface-Creating-Extensible-Java-Applications.htm" target="_blank" rel="noopener">Service Provider Interface: Creating Extensible Java Applications</a><br><a href="https://en.wikipedia.org/wiki/Service_provider_interface" target="_blank" rel="noopener">Service provider interface</a><br><a href="https://www.cnblogs.com/lovesqcc/p/5229353.html" target="_blank" rel="noopener">Java ServiceLoader使用和解析</a><br><a href="https://blog.csdn.net/yangguosb/article/details/78772730" target="_blank" rel="noopener">Java基础之SPI机制</a><br><a href="https://cxis.me/2017/04/17/Java%E4%B8%ADSPI%E6%9C%BA%E5%88%B6%E6%B7%B1%E5%85%A5%E5%8F%8A%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/" target="_blank" rel="noopener">Java中SPI机制深入及源码解析</a><br><a href="http://www.spring4all.com/article/260" target="_blank" rel="noopener">SPI机制简介</a></p><p>都看到这里了,关注个公众号吧,一起交流学习<br><img src="/images/%E7%90%86%E8%A7%A3Java%E4%B8%ADSPI%E6%9C%BA%E5%88%B6/2.jpg" alt="caison_way"></p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>Java诊断利器Arthas</title>
<link href="/2019/10/13/java-zhen-duan-li-qi-arthas/"/>
<url>/2019/10/13/java-zhen-duan-li-qi-arthas/</url>
<content type="html"><![CDATA[<p><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/0.jpg" alt></p><p><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/1.png" alt="Arthas"></p><h1 id="1-简介"><a href="#1-简介" class="headerlink" title="1 简介"></a>1 简介</h1><p>Arthas 是Alibaba开源的Java诊断工具,深受开发者喜爱</p><p>当你遇到以下类似问题而束手无策时,Arthas可以帮助你解决:</p><ul><li>这个类从哪个 jar 包加载的?为什么会报各种类相关的 Exception?</li><li>我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?</li><li>遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?</li><li>线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!</li><li>是否有一个全局视角来查看系统的运行状况?</li><li>有什么办法可以监控到JVM的实时运行状态?</li></ul><p>Arthas支持JDK 6+,支持Linux/Mac/Winodws,采用命令行交互模式,同时提供丰富的 Tab 自动补全功能,进一步方便进行问题的定位和诊断</p><h1 id="2-主要功能"><a href="#2-主要功能" class="headerlink" title="2 主要功能"></a>2 主要功能</h1><p>Arthas提供的功能主要可以分为以下3个方面:</p><ul><li>(1) 信息监控<ul><li>进程运行基本信息:内存、CPU占用、线程信息、线程堆栈、线程数统计、环境变量信息</li><li>对象信息:类对象静态属性、 Mbean 的属性信息、已加载类信息、类加载器、类方法信息</li></ul></li><li>(2) 方法调用<ul><li>方法调用入参、返回值查看</li><li>方法被调用的调用路径、调用耗时、方法调用次数、成功次数、失败次数等统计</li><li>记录和重做方法调用</li></ul></li><li>(3) 类文件处理<ul><li>dump已加载类的字节码、字节码反编译、类编译、类重新热加载</li></ul></li></ul><h1 id="3-安装和使用"><a href="#3-安装和使用" class="headerlink" title="3 安装和使用"></a>3 安装和使用</h1><h2 id="3-1-安装"><a href="#3-1-安装" class="headerlink" title="3.1 安装"></a>3.1 安装</h2><p>下载arthas-boot.jar,然后用java -jar的方式启动:</p><pre><code>wget https://alibaba.github.io/arthas/arthas-boot.jarjava -jar arthas-boot.jar</code></pre><p>然后输入进程对应编号,进入Arthas的命令交互界面即可使用:</p><p><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/2.png" alt></p><p>打印帮助信息:</p><pre><code>java -jar arthas-boot.jar -h</code></pre><h2 id="3-2-使用"><a href="#3-2-使用" class="headerlink" title="3.2 使用"></a>3.2 使用</h2><p>下面介绍Arthas的一些常用的命令和用法和原理,看看是如何解决我们实际中的问题的,命令详情可以参考Arthas的官方文档</p><h3 id="1-整体dashboard数据"><a href="#1-整体dashboard数据" class="headerlink" title="(1) 整体dashboard数据"></a>(1) 整体dashboard数据</h3><p>在arthas的命令行界面,输入dashboard命令,会实时展示当前tomcat的多线程状态、JVM各区域、GC情况等信息<br><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/3.png" alt="dashboard"></p><h3 id="2-查看线程监控"><a href="#2-查看线程监控" class="headerlink" title="(2) 查看线程监控"></a>(2) 查看线程监控</h3><p>输入thread命令,会显示所有线程的状态信息<br>输入thread -n 3会显示当前最忙的3个线程,可以用来排查线程CPU消耗<br>输入thread -b 会显示当前处于BLOCKED状态的线程,可以排查线程锁的问题<br><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/4.png" alt="thread"></p><h3 id="3-JVM监控"><a href="#3-JVM监控" class="headerlink" title="(3) JVM监控"></a>(3) JVM监控</h3><p>输入jvm命令,查看jvm详细的性能数据<br><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/5.png" alt="jvm"></p><h3 id="4-观察方法参数、返回值"><a href="#4-观察方法参数、返回值" class="headerlink" title="(4) 观察方法参数、返回值"></a>(4) 观察方法参数、返回值</h3><p>有时排查问题中我们需要查看参数,返回值,通常的需要加日志打印,比较繁琐,基于watch命令我们可以很方便做到这一切</p><pre><code>$ watch demo.MathGame primeFactors "{params,returnObj}" -x 2Press Ctrl+C to abort.Affect(class-cnt:1 , method-cnt:1) cost in 44 ms.ts=2018-12-03 19:16:51; [cost=1.280502ms] result=@ArrayList[ @Object[][ @Integer[535629513], ], @ArrayList[ @Integer[3], @Integer[19], @Integer[191], @Integer[49199], ],]</code></pre><h3 id="5-观察方法调用路径,耗时详情"><a href="#5-观察方法调用路径,耗时详情" class="headerlink" title="(5) 观察方法调用路径,耗时详情"></a>(5) 观察方法调用路径,耗时详情</h3><p>有时会遇到服务卡顿,想排查到底哪个步骤耗时比较久,通常做法是加日志,使用trace命令可以很方便解决这个问题:</p><pre><code>$ trace demo.MathGame runPress Ctrl+C to abort.Affect(class-cnt:1 , method-cnt:1) cost in 42 ms.`---ts=2018-12-04 00:44:17;thread_name=main;id=1;is_daemon=false;priority=5;TCCL=sun.misc.Launcher$AppClassLoader@3d4eac69 `---[10.611029ms] demo.MathGame:run() +---[0.05638ms] java.util.Random:nextInt() +---[10.036885ms] demo.MathGame:primeFactors() `---[0.170316ms] demo.MathGame:print()</code></pre><h1 id="4-实现原理"><a href="#4-实现原理" class="headerlink" title="4 实现原理"></a>4 实现原理</h1><p>整体宏观模块调用图如下:<br><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/6.png" alt="整体模块调用图"><br>篇幅原因,下面对其其中涉及的比较核心的2个原理进行简单介绍:</p><h2 id="1-信息监控、类文件处理"><a href="#1-信息监控、类文件处理" class="headerlink" title="(1) 信息监控、类文件处理"></a>(1) 信息监控、类文件处理</h2><p>JDK提供的JMX(Java Management Extensions Java管理扩展,是一个为应用程序植入管理功能的框架),JMX管理管理了一系列MBean对象,Arthas正是基于这些MBean对象实现内存、GC、类加载信息、JVM信息监控</p><h2 id="2-方法调用"><a href="#2-方法调用" class="headerlink" title="(2) 方法调用"></a>(2) 方法调用</h2><p>从JDK5之后,引入了java.lang.Instrument,程序员通过修改方法的字节码实现动态修改类代码。在代理类的方法中的参数中,就有Instrumentation inst实例。通过该实例,我们可以调用Instrumentation提供的各种接口。比如调用inst.getAllLoadedClasses()得到所有已经加载过的类。调用inst.addTransformer(new SdlTransformer(), true)新增转换器。调用inst.retransformClasses(Class cls),向JVM发起重转换请求</p><p>Arthas使用ASM生成增强后的类的字节码,增强的功能包括方法调用入参、返回值查看、方法调用统计、方法调用记录和重做,再基于JDK提供的Instrumentation接口对方法进行增加和转换</p><h1 id="5-实战案例"><a href="#5-实战案例" class="headerlink" title="5 实战案例"></a>5 实战案例</h1><p>Arthas官方文档提供了许多用户案例,下面介绍几个比较有意思的案例:</p><h2 id="1-排查应用奇怪日志来源-案例详情"><a href="#1-排查应用奇怪日志来源-案例详情" class="headerlink" title="(1) 排查应用奇怪日志来源 案例详情"></a>(1) 排查应用奇怪日志来源 <a href="https://github.com/alibaba/arthas/issues/263" target="_blank" rel="noopener">案例详情</a></h2><p>服务应用运行中有时会出现一些奇怪日志,排查定位这些日志的来源比较麻烦<br>通过修改StringBuilder的实现代码打印出日志的调用堆栈信息,编译生成StringBuilder.clss,再基于Arthas提供的redefine命令修改应用中使用的StringBuilder的实际使用字节码</p><h2 id="2-排查SpringBoot应用401-404问题-案例详情"><a href="#2-排查SpringBoot应用401-404问题-案例详情" class="headerlink" title="(2) 排查SpringBoot应用401/404问题 案例详情"></a>(2) 排查SpringBoot应用401/404问题 <a href="https://github.com/alibaba/arthas/issues/429" target="_blank" rel="noopener">案例详情</a></h2><p>页面访问返回401/404,碰到这种问题时,通常很头痛,特别是在线上环境时<br>通过Arthas提供的trace命令,打印出页面访问时的完整请求树,定位出具体哪个Servlet返回404</p><pre><code>$ trace javax.servlet.Servlet *Press Ctrl+C to abort.Affect(class-cnt:7 , method-cnt:185) cost in 1018 ms.</code></pre><p>通过trace命令,trace对象是javax.servlet.Filter定位具体哪个Filter拦截请求定位返回401的问题来源</p><pre><code>$ trace javax.servlet.Filter *Press Ctrl+C to abort.Affect(class-cnt:13 , method-cnt:75) cost in 278 ms.</code></pre><h2 id="3-线上代码热更新-案例详情"><a href="#3-线上代码热更新-案例详情" class="headerlink" title="(3) 线上代码热更新 案例详情"></a>(3) 线上代码热更新 <a href="https://github.com/alibaba/arthas/issues/537" target="_blank" rel="noopener">案例详情</a></h2><p>有时为了快速验证线上问题的修复方案,或者为了快速测试,我们需要热更新代码<br>Arthas提供的解决步骤如下</p><ul><li>步骤1 jad命令反编译代码</li><li>步骤2 文本编辑器修改代码</li><li>步骤3 sc命令查找代码所在类的ClassLoader</li><li>步骤4 mc命令指定ClassLoader编译代码</li><li>步骤5 redefine命令热更新代码</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://alibaba.github.io/arthas/" target="_blank" rel="noopener">Arthas官方文档</a></p><p><a href="https://alibaba.github.io/arthas/quick-start.html" target="_blank" rel="noopener">Arthas快速入门</a></p><p><a href="https://blog.csdn.net/zl1zl2zl3/article/details/89333004" target="_blank" rel="noopener">6到飞起的Java诊断工具Arthas</a></p><p><a href="https://mp.weixin.qq.com/s/wG51oUqVPObACqvZA9ItOg" target="_blank" rel="noopener">解密阿里线上问题诊断工具Arthas和jvm-sandbox</a></p><p><img src="/images/Java%E8%AF%8A%E6%96%AD%E5%88%A9%E5%99%A8Arthas/7.png" alt></p>]]></content>
<categories>
<category> 工具使用 </category>
</categories>
</entry>
<entry>
<title>理解分布式事务</title>
<link href="/2019/10/13/li-jie-fen-bu-shi-shi-wu/"/>
<url>/2019/10/13/li-jie-fen-bu-shi-shi-wu/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/0.jpg" alt></p><p>这篇文章将介绍什么是分布式事务,分布式事务解决什么问题,对分布式事务实现的难点,解决思路,不同场景下方案的选择,通过图解的方式进行梳理、总结和比较。</p><p>相信耐心看完这篇文章,谈到分布式事务,不再只是有“2PC”、“3PC”、“MQ的消息事务”、“最终一致性”、“TCC”等这些知识碎片,而是能够将知识连成一片,形成知识体系。</p><h1 id="1-什么是事务"><a href="#1-什么是事务" class="headerlink" title="1 什么是事务"></a>1 什么是事务</h1><p>介绍分布式事务之前,先介绍什么是事务。</p><h2 id="事务的具体定义"><a href="#事务的具体定义" class="headerlink" title="事务的具体定义"></a>事务的具体定义</h2><p>事务提供一种机制将一个活动涉及的所有操作纳入到一个不可分割的执行单元,组成事务的所有操作只有在所有操作均能正常执行的情况下方能提交,只要其中任一操作执行失败,都将导致整个事务的回滚。</p><p>简单地说,事务提供一种“ <strong>要么什么都不做,要么做全套(All or Nothing)</strong>”机制。<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/1.png" alt="事务"></p><h2 id="数据库事务的ACID属性"><a href="#数据库事务的ACID属性" class="headerlink" title="数据库事务的ACID属性"></a>数据库事务的ACID属性</h2><p>事务是基于数据进行操作,需要保证事务的数据通常存储在数据库中,所以介绍到事务,就不得不介绍数据库事务的ACID特性,指数据库事务正确执行的四个基本特性的缩写。包含:</p><ul><li><p><strong>原子性(Atomicity)</strong><br>整个事务中的所有操作,要么全部完成,要么全部不完成,不可能停滞在中间某个环节。事务在执行过程中发生错误,会被 回滚(Rollback)到事务开始前的状态,就像这个事务从来没有执行过一样。<br>例如:银行转账,从A账户转100元至B账户,分为两个步骤:</p><ul><li>(1)从A账户取100元</li><li>(2)存入100元至B账户。<br>这两步要么一起完成,要么一起不完成,如果只完成第一步,第二步失败,钱会莫名其妙少了100元。</li></ul></li><li><p><strong>一致性(Consistency)</strong><br>在事务开始之前和事务结束以后,数据库数据的一致性约束没有被破坏。<br>例如:现有完整性约束A+B=100,如果一个事务改变了A,那么必须得改变B,使得事务结束后依然满足A+B=100,否则事务失败。</p></li><li><p><strong>隔离性(Isolation)</strong><br>数据库允许多个并发事务同时对数据进行读写和修改的能力,如果一个事务要访问的数据正在被另外一个事务修改,只要另外一个事务未提交,它所访问的数据就不受未提交事务的影响。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。<br>例如:现有有个交易是从A账户转100元至B账户,在这个交易事务还未完成的情况下,如果此时B查询自己的账户,是看不到新增加的100元的。</p></li><li><p><strong>持久性(Durability)</strong><br>事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。</p></li></ul><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/2.png" alt="数据库事务的ACID特性"><br>简单而言,ACID是从不同维度描述事务的特性:</p><ul><li>原子性 —— 事务操作的整体性</li><li>一致性 —— 事务操作下数据的正确性</li><li>隔离性 —— 事务并发操作下数据的正确性</li><li>持久性 —— 事务对数据修改的可靠性</li></ul><p>一个支持事务(Transaction)的数据库,需要具有这4种特性,否则在事务过程当中无法保证数据的正确性,处理结果极可能达不到请求方的要求。</p><h2 id="什么时候使用数据库事务"><a href="#什么时候使用数据库事务" class="headerlink" title="什么时候使用数据库事务"></a>什么时候使用数据库事务</h2><p>在介绍完事务基本概念之后,什么时候该使用数据库事务?<br>简单而言,就是业务上有一组数据操作,需要如果其中有任何一个操作执行失败,整组操作全部不执行并恢复到未执行状态,要么全部成功,要么全部失败。</p><p>在使用数据库事务时需要注意,尽可能短的保持事务,修改多个不同表的数据的冗长事务会严重妨碍系统中的所有其他用户,这很有可能导致一些性能问题。</p><h1 id="2-什么是分布式事务"><a href="#2-什么是分布式事务" class="headerlink" title="2 什么是分布式事务"></a>2 什么是分布式事务</h1><p>介绍完事务相关基本概念之后,下面介绍分布式事务。</p><h2 id="分布式产生背景与概念"><a href="#分布式产生背景与概念" class="headerlink" title="分布式产生背景与概念"></a>分布式产生背景与概念</h2><p>随着互联网快速发展,微服务,SOA等服务架构模式正在被大规模的使用,现在分布式系统一般由多个独立的子系统组成,多个子系统通过网络通信互相协作配合完成各个功能。</p><p>有很多用例会跨多个子系统才能完成,比较典型的是电子商务网站的下单支付流程,至少会涉及交易系统和支付系统,而且这个过程中会涉及到事务的概念,即保证交易系统和支付系统的数据一致性,此处我们称这种<strong>跨系统的事务为分布式事务</strong>,具体一点而言,分布式事务是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分别位于不同的分布式系统的不同节点之上。</p><p>举个互联网常用的交易业务为例:<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/3.png" alt><br>上图中包含了库存和订单两个独立的微服务,每个微服务维护了自己的数据库。在交易系统的业务逻辑中,一个商品在下单之前需要先调用库存服务,进行扣除库存,再调用订单服务,创建订单记录。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/4.png" alt><br>可以看到,如果多个数据库之间的数据更新没有保证事务,将会导致出现子系统数据不一致,业务出现问题。</p><h2 id="分布式事务的难点"><a href="#分布式事务的难点" class="headerlink" title="分布式事务的难点"></a>分布式事务的难点</h2><ul><li><strong>事务的原子性</strong><br>事务操作跨不同节点,当多个节点某一节点操作失败时,需要保证多节点操作的<strong>要么什么都不做,要么做全套(All or Nothing)</strong>的原子性。</li><li><strong>事务的一致性</strong><br>当发生网络传输故障或者节点故障,节点间数据复制通道中断,在进行事务操作时需要保证数据一致性,保证事务的任何操作都不会使得数据违反数据库定义的约束、触发器等规则。</li><li><strong>事务的隔离性</strong><br>事务隔离性的本质就是如何正确多个并发事务的处理的读写冲突和写写冲突,因为在分布式事务控制中,可能会出现提交不同步的现象,这个时候就有可能出现“部分已经提交”的事务。此时并发应用访问数据如果没有加以控制,有可能出现“脏读”问题。</li></ul><h1 id="3-分布式系统的一致性"><a href="#3-分布式系统的一致性" class="headerlink" title="3 分布式系统的一致性"></a>3 分布式系统的一致性</h1><p>前面介绍到的分布式事务的难点涉及的问题,最终影响是导致数据出现不一致,下面对分布式系统的一致性问题进行理论分析,后面将基于这些理论进行分布式方案的介绍。</p><h2 id="可用性和一致性的冲突-——-CAP理论"><a href="#可用性和一致性的冲突-——-CAP理论" class="headerlink" title="可用性和一致性的冲突 —— CAP理论"></a>可用性和一致性的冲突 —— CAP理论</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/5.png" alt="CAP"><br>CAP 定理又被称作布鲁尔定理,是加州大学的计算机科学家布鲁尔在 2000 年提出的一个猜想。2002 年,麻省理工学院的赛斯·吉尔伯特和南希·林奇发表了布鲁尔猜想的证明,使之成为分布式计算领域公认的一个定理。</p><p>布鲁尔在提出CAP猜想时并没有具体定义 Consistency、Availability、Partition Tolerance 这3个词的含义,不同资料的具体定义也有差别,为了更好地解释,下面选择<a href="http://robertgreiner.com/about/" target="_blank" rel="noopener">Robert Greiner</a>的文章<a href="http://robertgreiner.com/2014/08/cap-theorem-revisited/" target="_blank" rel="noopener">《CAP Theorem》</a>作为参考基础。</p><ul><li><p><strong>CAP理论的定义</strong><br>在一个分布式系统(指互相连接并共享数据的节点的集合)中,当涉及读写操作时,只能保证一致性(Consistence)、可用性(Availability)、分区容错性(PartitionTolerance)三者中的两个,另外一个必须被牺牲。</p><p>Consistency、Availability、Partition Tolerance具体解释如下:</p></li><li><p><strong>C - Consistency 一致性</strong></p><blockquote><p>A read is guaranteed to return the most recent write for a given client.<br>对某个指定的客户端来说,读操作保证能够返回最新的写操作结果。</p></blockquote></li></ul><p>这里并不是强调同一时刻拥有相同的数据,对于系统执行事务来说,在事务执行过程中,系统其实处于一个不一致的状态,不同的节点的数据并不完全一致。</p><p>一致性强调客户端读操作能够获取最新的写操作结果,是因为事务在执行过程中,客户端是无法读取到未提交的数据的,只有等到事务提交后,客户端才能读取到事务写入的数据,而如果事务失败则会进行回滚,客户端也不会读取到事务中间写入的数据。</p><ul><li><strong>A - Availability 可用性</strong><blockquote><p>A non-failing node will return a reasonable response within a reasonable amount of time (no error or timeout).<br>非故障的节点在合理的时间内返回合理的响应(不是错误和超时的响应)。</p></blockquote></li></ul><p>这里强调的是合理的响应,不能超时,不能出错。注意并没有说“正确”的结果,例如,应该返回 100 但实际上返回了 90,肯定是不正确的结果,但可以是一个合理的结果。</p><ul><li><strong>P - Partition Tolerance 分区容忍性</strong><blockquote><p>The system will continue to function when network partitions occur.<br>当出现<strong>网络分区</strong>后,系统能够继续“履行职责”。</p></blockquote></li></ul><p>这里<strong>网络分区</strong>是指:<br>一个分布式系统里面,节点组成的网络本来应该是连通的。然而可能因为一些故障(节点间网络连接断开、节点宕机),使得有些节点之间不连通了,整个网络就分成了几块区域,数据就散布在了这些不连通的区域中。</p><h3 id="一致性、可用性、分区容忍性的选择"><a href="#一致性、可用性、分区容忍性的选择" class="headerlink" title="一致性、可用性、分区容忍性的选择"></a>一致性、可用性、分区容忍性的选择</h3><p>虽然 CAP 理论定义是三个要素中只能取两个,但放到分布式环境下来思考,我们会发现必须选择 P(分区容忍)要素,因为网络本身无法做到 100% 可靠,有可能出故障,所以分区是一个必然的现象。</p><p>如果我们选择了 CA(一致性 + 可用性) 而放弃了 P(分区容忍性),那么当发生分区现象时,为了保证 C(一致性),系统需要禁止写入,当有写入请求时,系统返回 error(例如,当前系统不允许写入),这又和 A(可用性) 冲突了,因为 A(可用性)要求返回 no error 和 no timeout。</p><p>因此,分布式系统理论上不可能选择 CA (一致性 + 可用性)架构,<strong>只能选择 CP(一致性 + 分区容忍性) 或者 AP (可用性 + 分区容忍性)架构,在一致性和可用性做折中选择</strong>。</p><ul><li><strong>CP - Consistency + Partition Tolerance (一致性 + 分区容忍性)</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/6.png" alt="CP模型"><br>如上图所示,因为Node1节点和Node2节点连接中断导致分区现象,Node1节点的数据已经更新到y,但是Node1 和 Node2 之间的复制通道中断,数据 y 无法同步到 Node2,Node2 节点上的数据还是旧数据x。</li></ul><p>这时客户端C 访问 Node2 时,Node2 需要返回 Error,提示客户端 “系统现在发生了错误”,这种处理方式违<br>背了可用性(Availability)的要求,因此 CAP 三者只能满足 CP。</p><ul><li><strong>AP - Availability + Partition Tolerance (可用性 + 分区容忍性)</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/7.png" alt="AP模型"><br>同样是Node2 节点上的数据还是旧数据x,这时客户端C 访问 Node2 时,Node2 将当前自己拥有的数据 x 返回给客户端 了,而实际上当前最新的数据已经是 y 了,这就不满足一致性(Consistency)的要求了,因此 CAP 三者只能满足 AP。</li></ul><p>注意:这里 Node2 节点返回 x,虽然不是一个“正确”的结果,但是一个“合理”的结果,因为 x 是旧的数据,并不是一个错乱的值,只是不是最新的数据。</p><p>值得补充的是,CAP理论告诉我们<strong>分布式系统只能选择AP或者CP</strong>,但实际上并不是说整个系统只能选择AP或者CP,在 CAP 理论落地实践时,我们需要将系统内的数据按照不同的应用场景和要求进行分类,每类数据选择不同的策略(CP 还是 AP),而不是直接限定整个系统所有数据都是同一策略。</p><p>另外,只能选择CP或者AP是指系统发生分区现象时无法同时保证C(一致性)和A(可用性),但不是意味着什么都不做,当分区故障解决后,系统还是要保持保证CA。</p><h2 id="CAP理论的延伸——BASE理论"><a href="#CAP理论的延伸——BASE理论" class="headerlink" title="CAP理论的延伸——BASE理论"></a>CAP理论的延伸——BASE理论</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/8.png" alt="BASE"><br>BASE 是指基本可用(Basically Available)、软状态( Soft State)、最终一致性( Eventual Consistency),核心思想是即使无法做到强一致性(CAP 的一致性就是强一致性),但应用可以采用适合的方式达到最终一致性。</p><ul><li><strong>BA - Basically Available 基本可用</strong><br>分布式系统在出现故障时,允许损失部分可用性,即保证核心可用。</li></ul><p>这里的关键词是“<strong>部分</strong>”和“<strong>核心</strong>”,实际实践上,哪些是核心需要根据具体业务来权衡。例如登录功能相对注册功能更加核心,注册不了最多影响流失一部分用户,如果用户已经注册但无法登录,那就意味用户无法使用系统,造成的影响范围更大。</p><ul><li><p><strong>S - Soft State 软状态</strong><br>允许系统存在中间状态,而该中间状态不会影响系统整体可用性。这里的中间状态就是 CAP 理论中的数据不一致。</p></li><li><p><strong>E - Eventual Consistency 最终一致性</strong><br>系统中的所有数据副本经过一定时间后,最终能够达到一致的状态。</p></li></ul><p>这里的关键词是“一定时间” 和 “最终”,“<strong>一定时间</strong>”和数据的特性是强关联的,不同业务不同数据能够容忍的不一致时间是不同的。例如支付类业务是要求秒级别内达到一致,因为用户时时关注;用户发的最新微博,可以容忍30分钟内达到一致的状态,因为用户短时间看不到明星发的微博是无感知的。而“<strong>最终</strong>”的含义就是不管多长时间,最终还是要达到一致性的状态。</p><p>BASE 理论本质上是对 CAP 的延伸和补充,更具体地说,是对 CAP 中 AP 方案的一个补充:</p><ul><li><p>CAP 理论是忽略延时的,而实际应用中延时是无法避免的。<br>这一点就意味着完美的 CP 场景是不存在的,即使是几毫秒的数据复制延迟,在这几毫秒时间间隔内,系统是不符合 CP 要求的。因此 CAP 中的 CP 方案,实际上也是实现了最终一致性,只是“一定时间”是指几毫秒而已。</p></li><li><p>AP 方案中牺牲一致性只是指发生分区故障期间,而不是永远放弃一致性。<br>这一点其实就是 BASE 理论延伸的地方,分区期间牺牲一致性,但分区故障恢复后,系统应该达到最终一致性。</p></li></ul><h2 id="数据一致性模型"><a href="#数据一致性模型" class="headerlink" title="数据一致性模型"></a>数据一致性模型</h2><p>前面介绍的BASE模型提过“强一致性”和“最终一致性”,下面对这些一致性模型展开介绍。</p><p> 分布式系统通过复制数据来提高系统的可靠性和容错性,并且将数据的不同的副本存放在不同的机器上,由于维护数据副本的一致性代价很高,因此许多系统采用弱一致性来提高性能,下面介绍常见的一致性模型:</p><ul><li><p><strong>强一致性</strong><br>要求无论更新操作是在哪个数据副本上执行,之后所有的读操作都要能获得最新的数据。对于单副本数据来说,读写操作是在同一数据上执行的,容易保证强一致性。对多副本数据来说,则需要使用分布式事务协议。</p></li><li><p><strong>弱一致性</strong><br>在这种一致性下,用户读到某一操作对系统特定数据的更新需要一段时间,我们将这段时间称为”不一致性窗口”。</p></li><li><p><strong>最终一致性</strong><br>是弱一致性的一种特例,在这种一致性下系统保证用户最终能够读取到某操作对系统特定数据的更新(读取操作之前没有该数据的其他更新操作)。”不一致性窗口”的大小依赖于交互延迟、系统的负载,以及数据的副本数等。</p></li></ul><p>系统选择哪种一致性模型取决于应用对一致性的需求,所选取的一致性模型还会影响到系统如何处理用户的请求以及对副本维护技术的选择等。后面将基于上面介绍的一致性模型分别介绍分布式事务的解决方案。</p><h2 id="柔性事务"><a href="#柔性事务" class="headerlink" title="柔性事务"></a>柔性事务</h2><h3 id="柔性事务的概念"><a href="#柔性事务的概念" class="headerlink" title="柔性事务的概念"></a>柔性事务的概念</h3><p>在电商等互联网场景下,传统的事务在数据库性能和处理能力上都暴露出了瓶颈。在分布式领域基于CAP理论以及BASE理论,有人就提出了<strong>柔性事务</strong>的概念。</p><p>基于BASE理论的设计思想,柔性事务下,在不影响系统整体可用性的情况下(Basically Available 基本可用),允许系统存在数据不一致的中间状态(Soft State 软状态),在经过数据同步的延时之后,最终数据能够达到一致。<strong>并不是完全放弃了ACID,而是通过放宽一致性要求,借助本地事务来实现最终分布式事务一致性的同时也保证系统的吞吐</strong>。</p><h3 id="实现柔性事务的一些特性"><a href="#实现柔性事务的一些特性" class="headerlink" title="实现柔性事务的一些特性"></a>实现柔性事务的一些特性</h3><p>下面介绍的是实现柔性事务的一些常见特性,这些特性在具体的方案中不一定都要满足,因为不同的方案要求不一样。</p><p><strong>可见性(对外可查询)</strong><br>在分布式事务执行过程中,如果某一个步骤执行出错,就需要明确的知道其他几个操作的处理情况,这就需要其他的服务都能够提供查询接口,保证可以通过查询来判断操作的处理情况。</p><p>为了保证操作的可查询,需要对于每一个服务的每一次调用都有一个全局唯一的标识,可以是业务单据号(如订单号)、也可以是系统分配的操作流水号(如支付记录流水号)。除此之外,操作的时间信息也要有完整的记录。</p><p><strong>操作幂等性</strong><br>幂等性,其实是一个数学概念。幂等函数,或幂等方法,是指可以使用相同参数重复执行,并能获得相同结果的函数。幂等操作的特点是其任意多次执行所产生的影响均与一次执行的影响相同。也就是说,同一个方法,使用同样的参数,调用多次产生的业务结果与调用一次产生的业务结果相同。</p><p>之所以需要操作幂等性,是因为为了保证数据的最终一致性,很多事务协议都会有很多重试的操作,如果一个方法不保证幂等,那么将无法被重试。幂等操作的实现方式有多种,如在系统中缓存所有的请求与处理结果、检测到重复操作后,直接返回上一次的处理结果等。</p><h1 id="4-常见分布式事务解决方案"><a href="#4-常见分布式事务解决方案" class="headerlink" title="4 常见分布式事务解决方案"></a>4 常见分布式事务解决方案</h1><p>介绍完分布式系统的一致性相关理论,下面基于不同的一致性模型介绍分布式事务的常见解决方案,后面会再介绍各个方案的使用场景。</p><p>分布式事务的实现有许多种,其中较经典是由Tuxedo提出的XA分布式事务协议,XA协议包含二阶段提交(2PC)和三阶段提交(3PC)两种实现。</p><h2 id="4-1-2PC-二阶段提交-方案-——-强一致性"><a href="#4-1-2PC-二阶段提交-方案-——-强一致性" class="headerlink" title="4.1 2PC(二阶段提交)方案 —— 强一致性"></a>4.1 2PC(二阶段提交)方案 —— 强一致性</h2><h3 id="方案简介"><a href="#方案简介" class="headerlink" title="方案简介"></a>方案简介</h3><p>二阶段提交协议(Two-phase Commit,即2PC)是常用的分布式事务解决方案,即将事务的提交过程分为两个阶段来进行处理:准备阶段和提交阶段。事务的发起者称协调者,事务的执行者称参与者。</p><p>在分布式系统里,每个节点都可以知晓自己操作的成功或者失败,却无法知道其他节点操作的成功或失败。当一个事务跨多个节点时,为了保持事务的原子性与一致性,而引入一个协调者来统一掌控所有参与者的操作结果,并指示它们是否要把操作结果进行真正的提交或者回滚(rollback)。</p><p>二阶段提交的算法思路可以概括为:<strong>参与者将操作成败通知协调者,再由协调者根据所有参与者的反馈情报决定各参与者是否要提交操作还是中止操作</strong>。</p><p>核心思想就是对每一个事务都采用先尝试后提交的处理方式,处理后所有的读操作都要能获得最新的数据,因此也可以将二阶段提交看作是一个强一致性算法。</p><h3 id="处理流程"><a href="#处理流程" class="headerlink" title="处理流程"></a>处理流程</h3><p>简单一点理解,可以把协调者节点比喻为带头大哥,参与者理解比喻为跟班小弟,带头大哥统一协调跟班小弟的任务执行。</p><p><strong>阶段1:准备阶段</strong></p><blockquote><ul><li>1、协调者向所有参与者发送事务内容,询问是否可以提交事务,并等待所有参与者答复。</li><li>2、各参与者执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。</li><li>3、如参与者执行成功,给协调者反馈yes,即可以提交;如执行失败,给协调者反馈no,即不可提交。</li></ul></blockquote><p><strong>阶段2:提交阶段</strong><br>如果协调者收到了参与者的失败消息或者超时,直接给每个参与者发送回滚(rollback)消息;否则,发送提交(commit)消息;参与者根据协调者的指令执行提交或者回滚操作,释放所有事务处理过程中使用的锁资源。(注意:必须在最后阶段释放锁资源)<br>接下来分两种情况分别讨论提交阶段的过程。</p><p><strong>情况1,当所有参与者均反馈yes,提交事务</strong>:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/9.png" alt="事务正常提交"></p><blockquote><ul><li>1、协调者向所有参与者发出正式提交事务的请求(即commit请求)。</li><li>2、参与者执行commit请求,并释放整个事务期间占用的资源。</li><li>3、各参与者向协调者反馈ack(应答)完成的消息。</li><li>4、协调者收到所有参与者反馈的ack消息后,即完成事务提交。</li></ul></blockquote><p><strong>情况2,当任何阶段1一个参与者反馈no,中断事务</strong>:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/10.png" alt="事务中断">> * 1、协调者向所有参与者发出回滚请求(即rollback请求)。</p><blockquote><ul><li>2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。</li><li>3、各参与者向协调者反馈ack完成的消息。</li><li>4、协调者收到所有参与者反馈的ack消息后,即完成事务中断。</li></ul></blockquote><h3 id="方案总结"><a href="#方案总结" class="headerlink" title="方案总结"></a>方案总结</h3><p>2PC方案实现起来简单,实际项目中使用比较少,主要因为以下问题:</p><ul><li>性能问题<br>所有参与者在事务提交阶段处于同步阻塞状态,占用系统资源,容易导致性能瓶颈。</li><li>可靠性问题<br>如果协调者存在单点故障问题,如果协调者出现故障,参与者将一直处于锁定状态。</li><li>数据一致性问题<br>在阶段2中,如果发生局部网络问题,一部分事务参与者收到了提交消息,另一部分事务参与者没收到提交消息,那么就导致了节点之间数据的不一致。</li></ul><h2 id="4-2-3PC-三阶段提交-方案"><a href="#4-2-3PC-三阶段提交-方案" class="headerlink" title="4.2 3PC(三阶段提交)方案"></a>4.2 3PC(三阶段提交)方案</h2><h3 id="方案简介-1"><a href="#方案简介-1" class="headerlink" title="方案简介"></a>方案简介</h3><p>三阶段提交协议,是二阶段提交协议的改进版本,与二阶段提交不同的是,引入超时机制。同时在协调者和参与者中都引入超时机制。</p><p>三阶段提交将二阶段的准备阶段拆分为2个阶段,插入了一个preCommit阶段,使得原先在二阶段提交中,参与者在准备之后,由于协调者发生崩溃或错误,而导致参与者处于无法知晓是否提交或者中止的“不确定状态”所产生的可能相当长的延时的问题得以解决。</p><h3 id="处理流程-1"><a href="#处理流程-1" class="headerlink" title="处理流程"></a>处理流程</h3><p><strong>阶段1:canCommit</strong><br>协调者向参与者发送commit请求,参与者如果可以提交就返回yes响应(参与者不执行事务操作),否则返回no响应:</p><blockquote><ul><li>1、协调者向所有参与者发出包含事务内容的canCommit请求,询问是否可以提交事务,并等待所有参与者答复。</li><li>2、参与者收到canCommit请求后,如果认为可以执行事务操作,则反馈yes并进入预备状态,否则反馈no。</li></ul></blockquote><p><strong>阶段2:preCommit</strong><br>协调者根据阶段1 canCommit参与者的反应情况来决定是否可以基于事务的preCommit操作。根据响应情况,有以下两种可能。</p><p><strong>情况1:阶段1所有参与者均反馈yes,参与者预执行事务:</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/11.png" alt="阶段2预执行事务"></p><blockquote><ul><li>1、协调者向所有参与者发出preCommit请求,进入准备阶段。</li><li>2、参与者收到preCommit请求后,执行事务操作,将undo和redo信息记入事务日志中(但不提交事务)。</li><li>3、各参与者向协调者反馈ack响应或no响应,并等待最终指令。</li></ul></blockquote><p><strong>情况2:阶段1任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/12.png" alt="阶段2中断事务"></p><blockquote><ul><li>1、协调者向所有参与者发出abort请求。</li><li>2、无论收到协调者发出的abort请求,或者在等待协调者请求过程中出现超时,参与者均会中断事务。</li></ul></blockquote><p><strong>阶段3:do Commit</strong><br>该阶段进行真正的事务提交,也可以分为以下两种情况:</p><p><strong>情况1:阶段2所有参与者均反馈ack响应,执行真正的事务提交:</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/13.png" alt="阶段3正式执行事务"></p><ul><li>1、如果协调者处于工作状态,则向所有参与者发出do Commit请求。</li><li>2、参与者收到do Commit请求后,会正式执行事务提交,并释放整个事务期间占用的资源。</li><li>3、各参与者向协调者反馈ack完成的消息。</li><li>4、协调者收到所有参与者反馈的ack消息后,即完成事务提交。</li></ul><p><strong>阶段2任何一个参与者反馈no,或者等待超时后协调者尚无法收到所有参与者的反馈,即中断事务:</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/14.png" alt="中断事务"></p><ul><li>1、如果协调者处于工作状态,向所有参与者发出abort请求。</li><li>2、参与者使用阶段1中的undo信息执行回滚操作,并释放整个事务期间占用的资源。</li><li>3、各参与者向协调者反馈ack完成的消息。</li><li>4、协调者收到所有参与者反馈的ack消息后,即完成事务中断。</li></ul><p>注意:进入阶段3后,无论协调者出现问题,或者协调者与参与者网络出现问题,都会导致参与者无法接收到协调者发出的do Commit请求或abort请求。此时,参与者都会在等待超时之后,继续执行事务提交。</p><h3 id="方案总结-1"><a href="#方案总结-1" class="headerlink" title="方案总结"></a>方案总结</h3><ul><li><p>优点<br>相比二阶段提交,三阶段贴近降低了阻塞范围,在等待超时后协调者或参与者会中断事务。避免了协调者单点问题,阶段3中协调者出现问题时,参与者会继续提交事务。</p></li><li><p>缺点<br>数据不一致问题依然存在,当在参与者收到preCommit请求后等待do commite指令时,此时如果协调者请求中断事务,而协调者无法与参与者正常通信,会导致参与者继续提交事务,造成数据不一致。</p></li></ul><h2 id="4-3-TCC-(Try-Confirm-Cancel)事务-——-最终一致性"><a href="#4-3-TCC-(Try-Confirm-Cancel)事务-——-最终一致性" class="headerlink" title="4.3 TCC (Try-Confirm-Cancel)事务 —— 最终一致性"></a>4.3 TCC (Try-Confirm-Cancel)事务 —— 最终一致性</h2><h3 id="方案简介-2"><a href="#方案简介-2" class="headerlink" title="方案简介"></a>方案简介</h3><p>TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland于2007年发表的一篇名为《Life beyond Distributed Transactions:an Apostate’s Opinion》的论文提出。</p><p>TCC是服务化的二阶段编程模型,其Try、Confirm、Cancel 3个方法均由业务编码实现;</p><ul><li>Try操作作为一阶段,负责资源的检查和预留。</li><li>Confirm操作作为二阶段提交操作,执行真正的业务。</li><li>Cancel是预留资源的取消。</li></ul><p>TCC事务的Try、Confirm、Cancel可以理解为SQL事务中的Lock、Commit、Rollback。</p><h3 id="处理流程-2"><a href="#处理流程-2" class="headerlink" title="处理流程"></a>处理流程</h3><p>为了方便理解,下面以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上。</p><p><strong>1、Try 阶段</strong><br>从执行阶段来看,与传统事务机制中业务逻辑相同。但从业务角度来看,却不一样。TCC机制中的Try仅是一个初步操作,它和后续的确认一起才能真正构成一个完整的业务逻辑,这个阶段主要完成:</p><ul><li>完成所有业务检查( 一致性 )</li><li>预留必须业务资源( 准隔离性 )</li><li>Try 尝试执行业务<br>TCC事务机制以初步操作(Try)为中心的,确认操作(Confirm)和取消操作(Cancel)都是围绕初步操作(Try)而展开。因此,Try阶段中的操作,其保障性是最好的,即使失败,仍然有取消操作(Cancel)可以将其执行结果撤销。</li></ul><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/15.png" alt="Try阶段"><br>假设商品库存为100,购买数量为2,这里检查和更新库存的同时,冻结用户购买数量的库存,同时创建订单,订单状态为待确认。</p><p><strong>2、Confirm / Cancel 阶段</strong><br>根据Try阶段服务是否全部正常执行,继续执行确认操作(Confirm)或取消操作(Cancel)。<br>Confirm和Cancel操作满足幂等性,如果Confirm或Cancel操作执行失败,将会不断重试直到执行完成。</p><p><strong>Confirm:当Try阶段服务全部正常执行, 执行确认业务逻辑操作</strong></p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/16.png" alt="Confirm"><br>这里使用的资源一定是Try阶段预留的业务资源。在TCC事务机制中认为,如果在Try阶段能正常的预留资源,那Confirm一定能完整正确的提交。Confirm阶段也可以看成是对Try阶段的一个补充,Try+Confirm一起组成了一个完整的业务逻辑。</p><p><strong>Cancel:当Try阶段存在服务执行失败, 进入Cancel阶段</strong></p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/17.png" alt="Cancel">Cancel取消执行,释放Try阶段预留的业务资源,上面的例子中,Cancel操作会把冻结的库存释放,并更新订单状态为取消。</p><h3 id="方案总结-2"><a href="#方案总结-2" class="headerlink" title="方案总结"></a>方案总结</h3><p>TCC事务机制相对于传统事务机制(X/Open XA),TCC事务机制相比于上面介绍的XA事务机制,有以下优点: </p><ul><li>性能提升<br>具体业务来实现控制资源锁的粒度变小,不会锁定整个资源。</li><li>数据最终一致性<br>基于Confirm和Cancel的幂等性,保证事务最终完成确认或者取消,保证数据的一致性。</li><li>可靠性<br>解决了XA协议的协调者单点故障问题,由主业务方发起并控制整个业务活动,业务活动管理器也变成多点,引入集群。</li></ul><p>缺点:<br>TCC的Try、Confirm和Cancel操作功能要按具体业务来实现,业务耦合度较高,提高了开发成本。</p><h2 id="4-4-本地消息表-——-最终一致性"><a href="#4-4-本地消息表-——-最终一致性" class="headerlink" title="4.4 本地消息表 —— 最终一致性"></a>4.4 本地消息表 —— 最终一致性</h2><h3 id="方案简介-3"><a href="#方案简介-3" class="headerlink" title="方案简介"></a>方案简介</h3><p>本地消息表的方案最初是由ebay提出,核心思路是将分布式事务拆分成本地事务进行处理。</p><p>方案通过在事务主动发起方额外新建事务消息表,事务发起方处理业务和记录事务消息在本地事务中完成,轮询事务消息表的数据发送事务消息,事务被动方基于消息中间件消费事务消息表中的事务。</p><p>这样设计可以避免”<strong>业务处理成功 + 事务消息发送失败</strong>“,或”<strong>业务处理失败 + 事务消息发送成功</strong>“的棘手情况出现,保证2个系统事务的数据一致性。</p><h3 id="处理流程-3"><a href="#处理流程-3" class="headerlink" title="处理流程"></a>处理流程</h3><p>下面把分布式事务最先开始处理的事务方成为事务主动方,在事务主动方之后处理的业务内的其他事务成为事务被动方。</p><p>为了方便理解,下面继续以电商下单为例进行方案解析,这里把整个过程简单分为扣减库存,订单创建2个步骤,库存服务和订单服务分别在不同的服务器节点上,其中库存服务是事务主动方,订单服务是事务被动方。</p><p>事务的主动方需要额外新建事务消息表,用于记录分布式事务的消息的发生、处理状态。</p><p>整个业务处理流程如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/18.png" alt="本地消息表方案"></p><blockquote><ul><li><strong>步骤1 事务主动方处理本地事务。</strong><br>事务主动发在本地事务中处理业务更新操作和写消息表操作。<br>上面例子中库存服务阶段再本地事务中完成扣减库存和写消息表(图中1、2)。</li><li><strong>步骤2 事务主动方通过消息中间件,通知事务被动方处理事务通知事务待消息</strong>。<br>消息中间件可以基于Kafka、RocketMQ消息队列,事务主动方法主动写消息到消息队列,事务消费方消费并处理消息队列中的消息。<br>上面例子中,库存服务把事务待处理消息写到消息中间件,订单服务消费消息中间件的消息,完成新增订单(图中3 - 5)。</li><li><strong>步骤3 事务被动方通过消息中间件,通知事务主动方事务已处理的消息。</strong><br>上面例子中,订单服务把事务已处理消息写到消息中间件,库存服务消费中间件的消息,并将事务消息的状态更新为已完成(图中6 - 8)</li></ul></blockquote><p>为了数据的一致性,当处理错误需要重试,事务发送方和事务接收方相关业务处理需要支持幂等。具体保存一致性的容错处理如下:</p><blockquote><ul><li>1、当步骤1处理出错,事务回滚,相当于什么都没发生。</li><li>2、当步骤2、步骤3处理出错,由于未处理的事务消息还是保存在事务发送方,事务发送方可以定时轮询为超时消息数据,再次发送的消息中间件进行处理。事务被动方消费事务消息重试处理。</li><li>3、如果是业务上的失败,事务被动方可以发消息给事务主动方进行回滚。</li><li>4、如果多个事务被动方已经消费消息,事务主动方需要回滚事务时需要通知事务被动方回滚。 </li></ul></blockquote><h3 id="方案总结-3"><a href="#方案总结-3" class="headerlink" title="方案总结"></a>方案总结</h3><p>方案的优点如下:</p><ul><li>从应用设计开发的角度实现了消息数据的可靠性,消息数据的可靠性不依赖于消息中间件,弱化了对MQ中间件特性的依赖。</li><li>方案轻量,容易实现。</li></ul><p>缺点如下:</p><ul><li>与具体的业务场景绑定,耦合性强,不可公用。</li><li>消息数据与业务数据同库,占用业务系统资源。</li><li>业务系统在使用关系型数据库的情况下,消息服务性能会受到关系型数据库并发性能的局限。</li></ul><h2 id="4-5-MQ事务-——-最终一致性"><a href="#4-5-MQ事务-——-最终一致性" class="headerlink" title="4.5 MQ事务 —— 最终一致性"></a>4.5 MQ事务 —— 最终一致性</h2><h3 id="方案简介-4"><a href="#方案简介-4" class="headerlink" title="方案简介"></a>方案简介</h3><p>基于MQ的分布式事务方案其实是对本地消息表的封装,将本地消息表基于MQ 内部,其他方面的协议基本与本地消息表一致。</p><h3 id="处理流程-4"><a href="#处理流程-4" class="headerlink" title="处理流程"></a>处理流程</h3><p>下面主要基于RocketMQ4.3之后的版本介绍MQ的分布式事务方案。</p><p>在本地消息表方案中,保证事务主动方发写业务表数据和写消息表数据的一致性是基于数据库事务,RocketMQ的事务消息相对于普通MQ,相对于提供了2PC的提交接口,方案如下:</p><p><strong>正常情况——事务主动方发消息</strong><br>这种情况下,事务主动方服务正常,没有发生故障,发消息流程如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/19.png" alt="正常情况——事务主动方发消息"></p><blockquote><ul><li>图中1、发送方向 MQ服务端(MQ Server)发送half消息。</li><li>图中2、MQ Server 将消息持久化成功之后,向发送方 ACK 确认消息已经发送成功。</li><li>图中3、发送方开始执行本地事务逻辑。</li><li>图中4、发送方根据本地事务执行结果向 MQ Server 提交二次确认(commit 或是 rollback)。</li><li>图中5、MQ Server 收到 commit 状态则将半消息标记为可投递,订阅方最终将收到该消息;MQ Server 收到 rollback 状态则删除半消息,订阅方将不会接受该消息。</li></ul></blockquote><p><strong>异常情况——事务主动方消息恢复</strong><br>在断网或者应用重启等异常情况下,图中4提交的二次确认超时未到达 MQ Server,此时处理逻辑如下:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/20.png" alt="异常情况——事务主动方消息恢复"></p><blockquote><ul><li>图中5、MQ Server 对该消息发起消息回查。</li><li>图中6、发送方收到消息回查后,需要检查对应消息的本地事务执行的最终结果。</li><li>图中7、发送方根据检查得到的本地事务的最终状态再次提交二次确认</li><li>图中8、MQ Server基于commit / rollback 对消息进行投递或者删除</li></ul></blockquote><p>介绍完RocketMQ的事务消息方案后,由于前面已经介绍过本地消息表方案,这里就简单介绍RocketMQ分布式事务:<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/21.png" alt="MQ分布式事务"><br>事务主动方基于MQ通信通知事务被动方处理事务,事务被动方基于MQ返回处理结果。<br>如果事务被动方消费消息异常,需要不断重试,业务处理逻辑需要保证幂等。<br>如果是事务被动方业务上的处理失败,可以通过MQ通知事务主动方进行补偿或者事务回滚。</p><h3 id="方案总结-4"><a href="#方案总结-4" class="headerlink" title="方案总结"></a>方案总结</h3><p>相比本地消息表方案,MQ事务方案优点是:</p><ul><li>消息数据独立存储 ,降低业务系统与消息系统之间的耦合。</li><li>吞吐量由于使用本地消息表方案。</li></ul><p>缺点是:</p><ul><li>一次消息发送需要两次网络请求(half消息 + commit/rollback消息)</li><li>业务处理服务需要实现消息状态回查接口</li></ul><h2 id="4-6-Saga事务-——-最终一致性"><a href="#4-6-Saga事务-——-最终一致性" class="headerlink" title="4.6 Saga事务 —— 最终一致性"></a>4.6 Saga事务 —— 最终一致性</h2><h3 id="方案简介-5"><a href="#方案简介-5" class="headerlink" title="方案简介"></a>方案简介</h3><p>Saga事务源于1987年普林斯顿大学的Hecto和Kenneth发表的如何处理long lived transaction(长活事务)论文,Saga事务核心思想是将长事务拆分为多个本地短事务,由Saga事务协调器协调,如果正常结束那就正常完成,如果某个步骤失败,则根据相反顺序一次调用补偿操作。</p><h3 id="处理流程-5"><a href="#处理流程-5" class="headerlink" title="处理流程"></a>处理流程</h3><p><strong>Saga事务基本协议如下</strong>:</p><ul><li>每个Saga事务由一系列幂等的有序子事务(sub-transaction) Ti 组成。</li><li>每个Ti 都有对应的幂等补偿动作Ci,补偿动作用于撤销Ti造成的结果。</li></ul><p>可以看到,和TCC相比,Saga没有“预留”动作,它的Ti就是直接提交到库。</p><p>下面以下单流程为例,整个操作包括:创建订单、扣减库存、支付、增加积分<br>Saga的执行顺序有两种:<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/22.png" alt="Saga事务执行顺序"></p><ul><li>事务正常执行完成<br>T1, T2, T3, …, Tn,例如:扣减库存(T1),创建订单(T2),支付(T3),依次有序完成整个事务。</li><li>事务回滚<br>T1, T2, …, Tj, Cj,…, C2, C1,其中0 < j < n,例如:扣减库存(T1),创建订单(T2),支付(T3,支付失败),支付回滚(C3),订单回滚(C2),恢复库存(C1)。</li></ul><p>Saga定义了两种恢复策略:</p><ul><li>向前恢复(forward recovery)</li></ul><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/23.png" alt="Saga事务向前恢复"></p><p>对应于上面第一种执行顺序,适用于必须要成功的场景,发生失败进行重试,执行顺序是类似于这样的:T1, T2, …, Tj(失败), Tj(重试),…, Tn,其中j是发生错误的子事务(sub-transaction)。该情况下不需要Ci。</p><ul><li>向后恢复(backward recovery)</li></ul><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/24.png" alt="Saga事务向后恢复"></p><p>对应于上面提到的第二种执行顺序,其中j是发生错误的子事务(sub-transaction),这种做法的效果是撤销掉之前所有成功的子事务,使得整个Saga的执行结果撤销。</p><p>Saga事务常见的有两种不同的实现方式:</p><ul><li>1、<strong>命令协调(Order Orchestrator):中央协调器负责集中处理事件的决策和业务逻辑排序。</strong></li></ul><p>中央协调器(Orchestrator,简称OSO)以命令/回复的方式与每项服务进行通信,全权负责告诉每个参与者该做什么以及什么时候该做什么。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/25.png" alt="命令协调模式"><br>以电商订单的例子为例:</p><blockquote><p>1、事务发起方的主业务逻辑请求OSO服务开启订单事务<br>2、OSO向库存服务请求扣减库存,库存服务回复处理结果。<br>3、OSO向订单服务请求创建订单,订单服务回复创建结果。<br>4、OSO向支付服务请求支付,支付服务回复处理结果。<br>5、主业务逻辑接收并处理OSO事务处理结果回复。</p></blockquote><p>中央协调器必须事先知道执行整个订单事务所需的流程(例如通过读取配置)。如果有任何失败,它还负责通过向每个参与者发送命令来撤销之前的操作来协调分布式的回滚。基于中央协调器协调一切时,回滚要容易得多,因为协调器默认是执行正向流程,回滚时只要执行反向流程即可。</p><ul><li>2、<strong>事件编排 (Event Choreography0:没有中央协调器(没有单点风险)时,每个服务产生并观察其他服务的事件,并决定是否应采取行动</strong>。</li></ul><p>在事件编排方法中,第一个服务执行一个事务,然后发布一个事件。该事件被一个或多个服务进行监听,这些服务再执行本地事务并发布(或不发布)新的事件。</p><p>当最后一个服务执行本地事务并且不发布任何事件时,意味着分布式事务结束,或者它发布的事件没有被任何Saga参与者听到都意味着事务结束。</p><p>以电商订单的例子为例:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/26.png" alt="事件编排模式"></p><blockquote><p>1、事务发起方的主业务逻辑发布开始订单事件<br>2、库存服务监听开始订单事件,扣减库存,并发布库存已扣减事件<br>2、订单服务监听库存已扣减事件,创建订单,并发布订单已创建事件<br>4、支付服务监听订单已创建事件,进行支付,并发布订单已支付事件<br>5、主业务逻辑监听订单已支付事件并处理。</p></blockquote><p>事件/编排是实现Saga模式的自然方式,它很简单,容易理解,不需要太多的代码来构建。如果事务涉及2至4个步骤,则可能是非常合适的。</p><h3 id="方案总结-5"><a href="#方案总结-5" class="headerlink" title="方案总结"></a>方案总结</h3><p><strong>命令协调设计的优点和缺点:</strong><br>优点如下:</p><ul><li>1、服务之间关系简单,避免服务之间的循环依赖关系,因为Saga协调器会调用Saga参与者,但参与者不会调用协调器</li><li>2、程序开发简单,只需要执行命令/回复(其实回复消息也是一种事件消息),降低参与者的复杂性。</li><li>3、易维护扩展,在添加新步骤时,事务复杂性保持线性,回滚更容易管理,更容易实施和测试</li></ul><p>缺点如下:</p><ul><li>1、中央协调器容易处理逻辑容易过于复杂,导致难以维护。</li><li>2、存在协调器单点故障风险。</li></ul><p><strong>事件/编排设计的优点和缺点</strong><br>优点如下:</p><ul><li>1、避免中央协调器单点故障风险。</li><li>2、当涉及的步骤较少服务开发简单,容易实现。</li></ul><p>缺点如下:</p><ul><li>1、服务之间存在循环依赖的风险。</li><li>2、当涉及的步骤较多,服务间关系混乱,难以追踪调测。</li></ul><p>值得补充的是,由于Saga模型中没有Prepare阶段,因此事务间不能保证隔离性,当多个Saga事务操作同一资源时,就会产生更新丢失、脏数据读取等问题,这时需要在业务层控制并发,例如:在应用层面加锁,或者应用层面预先冻结资源。</p><h1 id="5-总结"><a href="#5-总结" class="headerlink" title="5 总结"></a>5 总结</h1><h2 id="各方案使用场景"><a href="#各方案使用场景" class="headerlink" title="各方案使用场景"></a>各方案使用场景</h2><p>介绍完分布式事务相关理论和常见解决方案后,最终的目的在实际项目中运用,因此,总结一下各个方案的常见的使用场景。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/27.png" alt="方案比较"></p><ul><li><p>2PC/3PC<br>依赖于数据库,能够很好的提供强一致性和强事务性,但相对来说延迟比较高,比较适合传统的单体应用,在同一个方法中存在跨库操作的情况,不适合高并发和高性能要求的场景。</p></li><li><p>TCC<br>适用于执行时间确定且较短,实时性要求高,对数据一致性要求高,比如互联网金融企业最核心的三个服务:交易、支付、账务。</p></li><li><p>本地消息表/MQ事务<br>都适用于事务中参与方支持操作幂等,对一致性要求不高,业务上能容忍数据不一致到一个人工检查周期,事务涉及的参与方、参与环节较少,业务上有对账/校验系统兜底。</p></li></ul><ul><li>Saga事务<br>由于Saga事务不能保证隔离性,需要在业务层控制并发,适合于业务场景事务并发操作同一资源较少的情况。<br>Saga相比缺少预提交动作,导致补偿动作的实现比较麻烦,例如业务是发送短信,补偿动作则得再发送一次短信说明撤销,用户体验比较差。Saga事务较适用于补偿动作容易处理的场景。</li></ul><h2 id="分布式事务方案设计"><a href="#分布式事务方案设计" class="headerlink" title="分布式事务方案设计"></a>分布式事务方案设计</h2><p>本文介绍的偏向于原理,业界已经有不少开源的或者收费的解决方案,篇幅所限,就不再展开介绍。</p><p>实际运用理论时进行架构设计时,许多人容易犯“手里有了锤子,看什么都觉得像钉子”的错误,设计方案时考虑的问题场景过多,各种重试,各种补偿机制引入系统,导致设计出来的系统过于复杂,落地遥遥无期。</p><blockquote><p>世界上解决一个计算机问题最简单的方法:“恰好”不需要解决它!—— 阿里中间件技术专家沈询</p></blockquote><p>有些问题,看起来很重要,但实际上我们可以通过<strong>合理的设计</strong>或者将<strong>问题分解</strong>来规避。设计分布式事务系统也不是需要考虑所有异常情况,不必过度设计各种回滚,补偿机制。如果硬要把时间花在解决问题本身,实际上不仅效率低下,而且也是一种浪费。</p><p>如果系统要实现回滚流程的话,有可能系统复杂度将大大提升,且很容易出现Bug,估计出现Bug的概率会比需要事务回滚的概率大很多。在设计系统时,我们需要衡量是否值得花这么大的代价来解决这样一个出现概率非常小的问题,可以考虑当出现这个概率很小的问题,能否采用<strong>人工解决</strong>的方式,这也是大家在解决疑难问题时需要多多思考的地方。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://github.com/aalansehaiyang/technology-talk/blob/master/data-base/transaction.md" target="_blank" rel="noopener">technology-talk —— 事务</a></p><p><a href="https://draveness.me/mysql-transaction" target="_blank" rel="noopener">MySQL 中事务的实现</a></p><p><a href="http://blog.51cto.com/11821908/2058651" target="_blank" rel="noopener">分布式一致性算法2PC和3PC</a></p><p><a href="https://www.jianshu.com/p/453c6e7ff81c" target="_blank" rel="noopener">分布式开放消息系统(RocketMQ)的原理与实践</a></p><p><a href="https://blog.csdn.net/lirenzuo/article/details/81275785" target="_blank" rel="noopener">RocketMQ事务消息入门介绍</a></p><p><a href="https://servicecomb.apache.org/assets/slides/20180422/QConBeijing2018-Saga.pdf" target="_blank" rel="noopener">Saga分布式事务解决方案与实践 —— 姜宁</a></p><p><a href="https://www.jdon.com/49338" target="_blank" rel="noopener">分布式事务Saga模式</a></p><p><a href="https://juejin.im/post/5baa54e1f265da0ac2566fb2" target="_blank" rel="noopener">从一笔金币充值去思考分布式事务</a></p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E4%BA%8B%E5%8A%A1/28.png" alt></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>理解分布式系统中的缓存架构</title>
<link href="/2019/10/13/li-jie-fen-bu-shi-xi-tong-zhong-de-huan-cun-jia-gou/"/>
<url>/2019/10/13/li-jie-fen-bu-shi-xi-tong-zhong-de-huan-cun-jia-gou/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/0.jpg" alt></p><pre><code>作者 陈彩华文章转载交流请联系 [email protected]</code></pre><p>本文主要介绍大型分布式系统中缓存的相关理论,常见的缓存组件以及应用场景。</p><h1 id="1-缓存概述"><a href="#1-缓存概述" class="headerlink" title="1 缓存概述"></a>1 缓存概述</h1><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/1.png" alt="缓存概述"></p><h1 id="2-缓存的分类"><a href="#2-缓存的分类" class="headerlink" title="2 缓存的分类"></a>2 缓存的分类</h1><p>缓存主要分为以下四类<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/2.png" alt="缓存的分类"></p><h2 id="2-1-CDN缓存"><a href="#2-1-CDN缓存" class="headerlink" title="2.1 CDN缓存"></a>2.1 CDN缓存</h2><h3 id="基本介绍"><a href="#基本介绍" class="headerlink" title="基本介绍"></a>基本介绍</h3><p>CDN(Content Delivery Network 内容分发网络)的基本原理是广泛采用各种缓存服务器,将这些缓存服务器分布到用户访问相对集中的地区或网络中,在用户访问网站时,利用全局负载技术将用户的访问指向距离最近的工作正常的缓存服务器上,由缓存服务器直接响应用户请求</p><h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><p>主要缓存静态资源,例如图片,视频</p><h3 id="应用图"><a href="#应用图" class="headerlink" title="应用图"></a>应用图</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/3.jpg" alt="未使用CDN缓存"><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/4.jpg" alt="使用CDN缓存"></p><h3 id="优点"><a href="#优点" class="headerlink" title="优点"></a>优点</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/5.png" alt="优点"></p><h2 id="2-2-反向代理缓存"><a href="#2-2-反向代理缓存" class="headerlink" title="2.2 反向代理缓存"></a>2.2 反向代理缓存</h2><h3 id="基本介绍-1"><a href="#基本介绍-1" class="headerlink" title="基本介绍"></a>基本介绍</h3><p>反向代理位于应用服务器机房,处理所有对WEB服务器的请求。<br>如果用户请求的页面在代理服务器上有缓冲的话,代理服务器直接将缓冲内容发送给用户。如果没有缓冲则先向WEB服务器发出请求,取回数据,本地缓存后再发送给用户。通过降低向WEB服务器的请求数,从而降低了WEB服务器的负载。</p><h3 id="应用场景-1"><a href="#应用场景-1" class="headerlink" title="应用场景"></a>应用场景</h3><p>一般只缓存体积较小静态文件资源,如css、js、图片</p><h3 id="应用图-1"><a href="#应用图-1" class="headerlink" title="应用图"></a>应用图</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/6.jpg" alt="反向代理缓存应用图"></p><h3 id="开源实现"><a href="#开源实现" class="headerlink" title="开源实现"></a>开源实现</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/7.png" alt="开源实现"></p><h2 id="2-3-本地应用缓存"><a href="#2-3-本地应用缓存" class="headerlink" title="2.3 本地应用缓存"></a>2.3 本地应用缓存</h2><h3 id="基本介绍-2"><a href="#基本介绍-2" class="headerlink" title="基本介绍"></a>基本介绍</h3><p>指的是在应用中的缓存组件,其最大的优点是应用和cache是在同一个进程内部,请求缓存非常快速,没有过多的网络开销等,在单应用不需要集群支持或者集群情况下各节点无需互相通知的场景下使用本地缓存较合适;<br>同时,它的缺点也是应为缓存跟应用程序耦合,多个应用程序无法直接的共享缓存,各应用或集群的各节点都需要维护自己的单独缓存,对内存是一种浪费。</p><h3 id="应用场景-2"><a href="#应用场景-2" class="headerlink" title="应用场景"></a>应用场景</h3><p>缓存字典等常用数据</p><h3 id="缓存介质"><a href="#缓存介质" class="headerlink" title="缓存介质"></a>缓存介质</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/8.png" alt="缓存介质"></p><h3 id="实现"><a href="#实现" class="headerlink" title="实现"></a>实现</h3><h4 id="编程直接实现"><a href="#编程直接实现" class="headerlink" title="编程直接实现"></a>编程直接实现</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/9.png" alt="编程直接实现"></p><h4 id="Ehcache"><a href="#Ehcache" class="headerlink" title="Ehcache"></a>Ehcache</h4><h5 id="基本介绍-3"><a href="#基本介绍-3" class="headerlink" title="基本介绍"></a>基本介绍</h5><p>Ehcache是一种基于标准的开源缓存,可提高性能,卸载数据库并简化可伸缩性。<br>它是使用最广泛的基于Java的缓存,因为它功能强大,经过验证,功能齐全,并与其他流行的库和框架集成。Ehcache可以从进程内缓存扩展到使用TB级缓存的混合进程内/进程外部署</p><h5 id="应用场景-3"><a href="#应用场景-3" class="headerlink" title="应用场景"></a>应用场景</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/10.png" alt="Ehcache应用场景"></p><h5 id="Ehcache架构图"><a href="#Ehcache架构图" class="headerlink" title="Ehcache架构图"></a>Ehcache架构图</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/11.png" alt="Ehcache架构图"></p><h5 id="Ehcache主要特征"><a href="#Ehcache主要特征" class="headerlink" title="Ehcache主要特征"></a>Ehcache主要特征</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/12.png" alt="Ehcache主要特征"></p><h5 id="Ehcache缓存数据过期策略"><a href="#Ehcache缓存数据过期策略" class="headerlink" title="Ehcache缓存数据过期策略"></a>Ehcache缓存数据过期策略</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/13.png" alt="缓存数据过期策略.png"></p><h5 id="Ehcache过期数据淘汰机制"><a href="#Ehcache过期数据淘汰机制" class="headerlink" title="Ehcache过期数据淘汰机制"></a>Ehcache过期数据淘汰机制</h5><p><strong>懒淘汰机制</strong>:每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做TTL比较来判断是否过期</p><h4 id="Guava-Cache"><a href="#Guava-Cache" class="headerlink" title="Guava Cache"></a>Guava Cache</h4><h2 id="2-4-分布式缓存"><a href="#2-4-分布式缓存" class="headerlink" title="2.4 分布式缓存"></a>2.4 分布式缓存</h2><h5 id="基本介绍-4"><a href="#基本介绍-4" class="headerlink" title="基本介绍"></a>基本介绍</h5><p>Guava Cache是Google开源的Java重用工具集库Guava里的一款缓存工具</p><h5 id="特点与功能"><a href="#特点与功能" class="headerlink" title="特点与功能"></a>特点与功能</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/14.png" alt="Guava Cache特点与功能.png"></p><h5 id="应用场景-4"><a href="#应用场景-4" class="headerlink" title="应用场景"></a>应用场景</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/15.png" alt="Guava Cache应用场景.png"></p><h5 id="数据结构图"><a href="#数据结构图" class="headerlink" title="数据结构图"></a>数据结构图</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/16.png" alt="Guava Cache数据结构图"></p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/17.png" alt="Guava Cache结构特点.png"></p><h5 id="缓存更新策略"><a href="#缓存更新策略" class="headerlink" title="缓存更新策略"></a>缓存更新策略</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/18.png" alt="Guava Cache 缓存更新策略"></p><h5 id="缓存回收策略"><a href="#缓存回收策略" class="headerlink" title="缓存回收策略"></a>缓存回收策略</h5><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/19.png" alt="Guava Cache缓存回收策略.png"></p><h2 id="2-4-分布式缓存-1"><a href="#2-4-分布式缓存-1" class="headerlink" title="2.4 分布式缓存"></a>2.4 分布式缓存</h2><p>指的是与应用分离的缓存组件或服务,其最大的优点是自身就是一个独立的应用,与本地应用隔离,多个应用可直接的共享缓存。</p><p>主要应用场景<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/20.png" alt="分布式缓存应用场景.png"><br>主要接入方式<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/21.png" alt="分布式缓存接入方式.png"><br>下面介绍分布式缓存常见的2大开源实现Memcached和Redis</p><h3 id="Memcached"><a href="#Memcached" class="headerlink" title="Memcached"></a>Memcached</h3><h4 id="基本介绍-5"><a href="#基本介绍-5" class="headerlink" title="基本介绍"></a>基本介绍</h4><p>Memcached是一个高性能,分布式内存对象缓存系统,通过在内存里维护一个统一的巨大的hash表,它能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。简单的说就是将数据调用到内存中,然后从内存中读取,从而大大提高读取速度。</p><h4 id="特点"><a href="#特点" class="headerlink" title="特点"></a>特点</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/22.png" alt="Memcached特点"></p><h4 id="基本架构"><a href="#基本架构" class="headerlink" title="基本架构"></a>基本架构</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/23.jpg" alt="Memcached基本架构"></p><h4 id="缓存数据过期策略"><a href="#缓存数据过期策略" class="headerlink" title="缓存数据过期策略"></a>缓存数据过期策略</h4><p><strong>LRU(最近最少使用)到期失效策略</strong>,在Memcached内存储数据项时,可以指定它在缓存的失效时间,默认为永久。当Memcached服务器用完分配的内时,失效的数据被首先替换,然后也是最近未使用的数据。</p><h4 id="数据淘汰内部实现"><a href="#数据淘汰内部实现" class="headerlink" title="数据淘汰内部实现"></a>数据淘汰内部实现</h4><p><strong>懒淘汰机制</strong>:每次往缓存放入数据的时候,都会存一个时间,在读取<br>的时候要和设置的时间做TTL比较来判断是否过期</p><h4 id="分布式集群实现"><a href="#分布式集群实现" class="headerlink" title="分布式集群实现"></a>分布式集群实现</h4><p>服务端并没有 “ 分布式 ” 功能。每个服务器都是完全独立和隔离的服务。 Memcached的分布式,是由客户端程序实现的</p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/24.jpg" alt="数据读写流程图"><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/25.png" alt="Memcached分布式集群实现"></p><h3 id="Redis"><a href="#Redis" class="headerlink" title="Redis"></a>Redis</h3><h4 id="基本介绍-6"><a href="#基本介绍-6" class="headerlink" title="基本介绍"></a>基本介绍</h4><p><strong>Redis是一个远程内存数据库(非关系型数据库)</strong>,性能强劲,具有复制特性以及解决问题而生的独一无二的数据模型。它可以存储键值对与5种不同类型的值之间的映射,可以将存储在内存的键值对数据持久化到硬盘,可以使用复制特性来扩展读性能,<br>Redis还可以使用客户端分片来扩展写性能。内置了 复制(replication),LUA脚本(Lua scripting),LRU驱动事件(LRU eviction),事务(transactions) 和不同级别的 磁盘持久化(persistence), 并通过 Redis哨兵(Sentinel)和自动分区(Cluster)提供高可用性(high availability)。</p><h4 id="数据模型"><a href="#数据模型" class="headerlink" title="数据模型"></a>数据模型</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/26.png" alt="Redis数据模型"></p><h4 id="数据淘汰策略"><a href="#数据淘汰策略" class="headerlink" title="数据淘汰策略"></a>数据淘汰策略</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/27.png" alt="Redis数据淘汰策略"></p><h4 id="数据淘汰内部实现-1"><a href="#数据淘汰内部实现-1" class="headerlink" title="数据淘汰内部实现"></a>数据淘汰内部实现</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/28.png" alt="Redis数据淘汰内部实现.png"></p><h4 id="持久化方式"><a href="#持久化方式" class="headerlink" title="持久化方式"></a>持久化方式</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/29.png" alt="Redis持久化方式"></p><h4 id="底层实现部分解析"><a href="#底层实现部分解析" class="headerlink" title="底层实现部分解析"></a>底层实现部分解析</h4><ul><li><p>启动的部分过程图解<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/30.jpg" alt="启动的部分过程"></p></li><li><p>server端持久化的部分操作图解<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/31.jpg" alt="server端持久化的部分操作"></p></li><li><p>底层哈希表实现(渐进式Rehash)</p></li></ul><p><strong>初始化字典</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/32.jpg" alt="初始化字典"><br><strong>新增字典元素图解</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/33.jpg" alt="新增字典元素图解"><br><strong>Rehash执行流程</strong><br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/34.jpg" alt="Rehash执行流程"></p><h4 id="缓存设计原则"><a href="#缓存设计原则" class="headerlink" title="缓存设计原则"></a>缓存设计原则</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/35.png" alt="Redis缓存设计原则.png"></p><h3 id="Redis与Memcached比较"><a href="#Redis与Memcached比较" class="headerlink" title="Redis与Memcached比较"></a>Redis与Memcached比较</h3><table><thead><tr><th align="left"></th><th align="center">Redis</th><th align="center">Memcached</th></tr></thead><tbody><tr><td align="left">支持的数据结构</td><td align="center">哈希、列表、集合、有序集合</td><td align="center">纯kev-value</td></tr><tr><td align="left">持久化支持</td><td align="center">有</td><td align="center">无</td></tr><tr><td align="left">高可用支持</td><td align="center">redis天然支持集群功能,可以实现主动复制,读写分离。官方也提供了sentinel集群管理工具,能够实现主从服务监控,故障自动转移,这一切,对于客户端都是透明的,无需程序改动,也无需人工介入</td><td align="center">需要二次开发</td></tr><tr><td align="left">存储value容量</td><td align="center">最大512M</td><td align="center">最大1M</td></tr><tr><td align="left">内存分配</td><td align="center">临时申请空间,可能导致碎片</td><td align="center">预分配内存池的方式管理内存,能够省去内存分配时间</td></tr><tr><td align="left">虚拟内存使用</td><td align="center">有自己的VM机制,理论上能够存储比物理内存更多的数据,当数据超量时,会引发swap,把冷数据刷到磁盘上</td><td align="center">所有的数据存储在物理内存里</td></tr><tr><td align="left">网络模型</td><td align="center">非阻塞IO复用模型,提供一些非KV存储之外的排序,聚合功能,在执行这些功能时,复杂的CPU计算,会阻塞整个IO调度</td><td align="center">非阻塞IO复用模型</td></tr><tr><td align="left">水平扩展的支持</td><td align="center">暂无</td><td align="center">暂无</td></tr><tr><td align="left">多线程</td><td align="center">Redis支持单线程</td><td align="center">Memcached支持多线程,CPU利用方面Memcache优于Redis</td></tr><tr><td align="left">过期策略</td><td align="center">有专门线程,清除缓存数据</td><td align="center">懒淘汰机制:每次往缓存放入数据的时候,都会存一个时间,在读取的时候要和设置的时间做TTL比较来判断是否过期</td></tr><tr><td align="left">单机QPS</td><td align="center">约10W</td><td align="center">约60W</td></tr><tr><td align="left">源代码可读性</td><td align="center">代码清爽简洁</td><td align="center">能是考虑了太多的扩展性,多系统的兼容性,代码不清爽</td></tr><tr><td align="left"><strong>适用场景</strong></td><td align="center">复杂数据结构、有持久化、高可用需求、value存储内容较大</td><td align="center">纯KV,数据量非常大,并发量非常大的业务</td></tr></tbody></table><h1 id="3-分层缓存架构设计"><a href="#3-分层缓存架构设计" class="headerlink" title="3 分层缓存架构设计"></a>3 分层缓存架构设计</h1><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/36.png" alt></p><h1 id="4-缓存带来的复杂度问题"><a href="#4-缓存带来的复杂度问题" class="headerlink" title="4 缓存带来的复杂度问题"></a>4 缓存带来的复杂度问题</h1><p>常见的问题主要包括</p><ul><li>数据一致性</li><li>缓存穿透</li><li>缓存雪崩</li><li>缓存高可用</li><li>缓存热点<br>下面逐一介绍分析这些问题以及相应的解决方案。</li></ul><h2 id="数据一致性"><a href="#数据一致性" class="headerlink" title="数据一致性"></a>数据一致性</h2><p>因为缓存属于持久化数据的一个副本,因此不可避免的会出现数据不一致问题。导致脏读或读不到数据的情况。数据不一致,一般是因为网络不稳定或节点故障导致</p><p>问题出现的常见3个场景以及解决方案:<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/37.png" alt="数据一致性问题场景及解决"></p><h2 id="缓存穿透"><a href="#缓存穿透" class="headerlink" title="缓存穿透"></a>缓存穿透</h2><p>缓存一般是Key,value方式存在,当某一个Key不存在时会查询数据库,假如这个Key,一直不存在,则会频繁的请求数据库,对数据库造成访问压力。</p><p><strong>主要解决方案</strong>:</p><ul><li>对结果为空的数据也进行缓存,当此key有数据后,清理缓存</li><li>一定不存在的key,采用布隆过滤器,建立一个大的Bitmap中,查询时通过该bitmap过滤</li></ul><h2 id="缓存雪崩"><a href="#缓存雪崩" class="headerlink" title="缓存雪崩"></a>缓存雪崩</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/38.png" alt="缓存雪崩"></p><h2 id="缓存高可用"><a href="#缓存高可用" class="headerlink" title="缓存高可用"></a>缓存高可用</h2><p>缓存是否高可用,需要根据实际的场景而定,并不是所有业务都要求缓存高可用,需要结合具体业务,具体情况进行方案设计,例如临界点是是否对后端的数据库造成影响。</p><p><strong>主要解决方案</strong>:</p><ul><li>分布式:实现数据的海量缓存</li><li>复制:实现缓存数据节点的高可用</li></ul><h2 id="缓存热点"><a href="#缓存热点" class="headerlink" title="缓存热点"></a>缓存热点</h2><p>一些特别热点的数据,高并发访问同一份缓存数据,导致缓存服务器压力过大。</p><p><strong>解决</strong>:复制多份缓存副本,把请求分散到多个缓存服务器上,减轻缓存热点导致的单台缓存服务器压力</p><h1 id="5-业界案例"><a href="#5-业界案例" class="headerlink" title="5 业界案例"></a>5 业界案例</h1><p>案例主要参考新浪微博陈波的<a href="https://mp.weixin.qq.com/s/YxGeisz0L9Ja2dwsiZz01w" target="_blank" rel="noopener">技术分享</a></p><h2 id="技术挑战"><a href="#技术挑战" class="headerlink" title="技术挑战"></a>技术挑战</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/39.jpg" alt=" 技术挑战"></p><h2 id="Feed缓存架构图"><a href="#Feed缓存架构图" class="headerlink" title="Feed缓存架构图"></a>Feed缓存架构图</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/40.jpg" alt="Feed缓存架构"></p><h2 id="架构特点"><a href="#架构特点" class="headerlink" title="架构特点"></a>架构特点</h2><p>新浪微博把SSD应用在分布式缓存场景中,将传统的Redis/MC + Mysql方式,扩展为 Redis/MC + SSD Cache + Mysql方式,SSD Cache作为L2缓存使用,第一降低了MC/Redis成本过高,容量小的问题,也解决了穿透DB带来的数据库访问压力</p><p>主要在数据架构、性能、储存成本、服务化等不同方面进行了优化增强<br><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/41.jpg" alt></p><p><img src="/images/%E7%90%86%E8%A7%A3%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F%E4%B8%AD%E7%9A%84%E7%BC%93%E5%AD%98%E6%9E%B6%E6%9E%84/42.png" alt="架构关注点"></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://time.geekbang.org/column/intro/81?code=OK4eM0TBPTKGPRCzcZdzIeXjPACLfY3KCzATXOSWzXE%3D" target="_blank" rel="noopener">从0开始学架构 —— Alibaba 李运华 </a></p><p><a href="https://time.geekbang.org/column/intro/82?code=w8EZ6RGOQApZJ5tpAzP8dRzeVHxZ4q%2FfOdSbSZzbkhc%3D" target="_blank" rel="noopener">Java核心技术36讲—— Oracle 杨晓峰</a></p><p><a href="https://blog.csdn.net/a600423444/article/details/8944601#commentBox" target="_blank" rel="noopener">分析Redis架构设计 ——上帝禁区</a></p><p><a href="https://github.com/memcached/memcached/wiki/Overview" target="_blank" rel="noopener">Memcached官方文档</a></p><p><a href="https://mp.weixin.qq.com/s/DwAwR1a5GcjdldpMykofDQ" target="_blank" rel="noopener">redis的持久化方式RDB和AOF的区别 —— 58沈剑</a></p><p><a href="https://mp.weixin.qq.com/s/P4zaM8RvV4jehByx51tkaw" target="_blank" rel="noopener">缓存,你真的用对了么? —— 58沈剑</a></p><p><a href="https://mp.weixin.qq.com/s/hOdwK2-7_S7_fi-KVu9_OQ" target="_blank" rel="noopener">选redis还是memcached,源码怎么说? —— 58沈剑</a></p><p><a href="https://tech.meituan.com/cache_about.html" target="_blank" rel="noopener">缓存那些事 —— 美团技术团队</a></p><p><a href="https://www.cnblogs.com/Cwj-XFH/p/8998600.html" target="_blank" rel="noopener">Redis 缓存设计原则—— 雪飞鸿</a></p><p><a href="https://www.cnblogs.com/binyue/p/3726842.html" target="_blank" rel="noopener">Redis的缓存策略和主键失效机制 ——邴越</a></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>NoSQL-还是-SQL-?这一篇讲清楚</title>
<link href="/2019/10/13/nosql-huan-shi-sql-zhe-yi-pian-jiang-qing-chu/"/>
<url>/2019/10/13/nosql-huan-shi-sql-zhe-yi-pian-jiang-qing-chu/</url>
<content type="html"><![CDATA[<p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/0.jpg" alt></p><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/1.png" alt="NoSQL历史"><br>随着大数据时代的到来,越来越多的网站、应用系统需要支撑海量数据存储,高并发请求、高可用、高可扩展性等特性要求,传统的关系型数据库在应付这些调整已经显得力不从心,暴露了许多能以克服的问题。由此,各种各样的NoSQL(Not Only SQL)数据库作为传统关系型数据的一个有力补充得到迅猛发展。</p><p>本文将分析传统数据库的存在的相关问题,以及几大类NoSQL如何解决这些问题,希望给大家提供在不同业务场景下,关于存储方面技术选型提供参考。</p><h1 id="1-传统数据库缺点"><a href="#1-传统数据库缺点" class="headerlink" title="1 传统数据库缺点"></a>1 传统数据库缺点</h1><ul><li><p>大数据场景下I/O较高<br>因为数据是按行存储,即使只针对其中某一列进行运算,关系型数据库也会将整行数据从存储设备中读入内存,导致I/O较高</p></li><li><p>存储的是行记录,无法存储数据结构</p></li><li><p>表结构schema扩展不方便<br>如要需要修改表结构,需要执行执行DDL(data definition language),语句修改,修改期间会导致锁表,部分服务不可用</p></li><li><p>全文搜索功能较弱<br>关系型数据库下只能够进行子字符串的匹配查询,当表的数据逐渐变大的时候,like查询的匹配会非常慢,即使在有索引的情况下。况且关系型数据库也不应该对文本字段进行索引</p></li><li><p>存储和处理复杂关系型数据功能较弱<br>许多应用程序需要了解和导航高度连接数据之间的关系,才能启用社交应用程序、推荐引擎、欺诈检测、知识图谱、生命科学和 IT/网络等用例。然而传统的关系数据库并不善于处理数据点之间的关系。它们的表格数据模型和严格的模式使它们很难添加新的或不同种类的关联信息。</p></li></ul><h1 id="2-NoSQL解决方案"><a href="#2-NoSQL解决方案" class="headerlink" title="2 NoSQL解决方案"></a>2 NoSQL解决方案</h1><p>NoSQL,泛指非关系型的数据库,可以理解为SQL的一个有力补充。</p><p>在NoSQL许多方面性能大大优于非关系型数据库的同时,往往也伴随一些特性的缺失,比较常见的,是事务库事务功能的缺失。<br>数据库事务正确执行的四个基本要素:ACID如下:</p><table><thead><tr><th align="left">要素</th><th align="center">名称</th><th align="center">描述</th></tr></thead><tbody><tr><td align="left">A</td><td align="center">Atomicity<br>(原子性)</td><td align="center">一个事务中的所有操作,要么全部完成,要么全部不完成,不会在中间某个环节结束。事务在执行过程中发生错误,会被回滚到事务开始前的状态,就像这个事务从来没有执行过一样。</td></tr><tr><td align="left">C</td><td align="center">Consistency<br>一致性</td><td align="center">在事务开始之前和事务结束以后,数据库数据的一致性约束没有被破坏。</td></tr><tr><td align="left">I</td><td align="center">Isolation<br>隔离性</td><td align="center">数据库允许多个并发事务同时对数据进行读写和修改的能力。隔离性可以防止多个事务并发执行时由于交叉执行而导致数据的不一致。</td></tr><tr><td align="left">D</td><td align="center">Durability<br>持久性</td><td align="center">事务处理结束后,对数据的修改就是永久的,即便系统故障也不会丢失。</td></tr></tbody></table><p>下面介绍5大类NoSQL数据针对传统关系型数据库的缺点提供的解决方案:</p><h2 id="2-1-列式数据库"><a href="#2-1-列式数据库" class="headerlink" title="2.1 列式数据库"></a>2.1 列式数据库</h2><p>列式数据库是以列相关存储架构进行数据存储的数据库,主要适合于批量数据处理和即时查询。相对应的是行式数据库,数据以行相关的存储体系架构进行空间分配,主要适合于小批量的数据处理,常用于联机事务型数据处理。</p><p>基于列式数据库的列列存储特性,可以<strong>解决某些特定场景下关系型数据库I/O较高的问题</strong></p><h3 id="2-1-1-基本原理"><a href="#2-1-1-基本原理" class="headerlink" title="2.1.1 基本原理"></a>2.1.1 基本原理</h3><p>传统关系型数据库是按照行来存储数据库,称为“行式数据库”,而列式数据库是按照列来存储数据。</p><p>将表放入存储系统中有两种方法,而我们绝大部分是采用行存储的。<br>行存储法是将各行放入连续的物理位置,这很像传统的记录和文件系统。<br>列存储法是将数据按照列存储到数据库中,与行存储类似,下图是两种存储方法的图形化解释:</p><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/2.jpg" alt="按行存储和按列存储模式"></p><h3 id="2-1-2-常见列式数据库"><a href="#2-1-2-常见列式数据库" class="headerlink" title="2.1.2 常见列式数据库"></a>2.1.2 常见列式数据库</h3><ul><li><p>HBase<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/3.png" alt="HBase"><br>HBase是一个开源的非关系型分布式数据库(NoSQL),它参考了谷歌的BigTable建模,实现的编程语言为 Java。它是Apache软件基金会的Hadoop项目的一部分,运行于HDFS文件系统之上,为 Hadoop 提供类似于BigTable 规模的服务。因此,它可以容错地存储海量稀疏的数据。</p></li><li><p>BigTable</p></li></ul><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/4.png" alt><br>BigTable是一种压缩的、高性能的、高可扩展性的,基于Google文件系统(Google File System,GFS)的数据存储系统,用于存储大规模结构化数据,适用于云端计算。</p><h3 id="2-1-3-相关特性"><a href="#2-1-3-相关特性" class="headerlink" title="2.1.3 相关特性"></a>2.1.3 相关特性</h3><p>优点如下:</p><ul><li><strong>高效的储存空间利用率</strong></li></ul><p>列式数据库由于其针对不同列的数据特征而发明的不同算法使其往往有比行式数据库高的多的压缩率,普通的行式数据库一般压缩率在3:1 到5:1 左右,而列式数据库的压缩率一般在8:1到30:1 左右。<br>比较常见的,通过字典表压缩数据:<br>下面中才是那张表本来的样子。经过字典表进行数据压缩后,表中的字符串才都变成数字了。正因为每个字符串在字典表里只出现一次了,所以达到了压缩的目的(有点像规范化和非规范化Normalize和Denomalize)<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/5.jpg" alt="通过字典表压缩数据"></p><ul><li><strong>查询效率高</strong></li></ul><p>读取多条数据的同一列效率高,因为这些列都是存储在一起的,一次磁盘操作可以数据的指定列全部读取到内存中。<br>下图通过一条查询的执行过程说明列式存储(以及数据压缩)的优点</p><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/6.jpg" alt></p><pre><code>执行步骤如下:i. 去字典表里找到字符串对应数字(只进行一次字符串比较)。ii. 用数字去列表里匹配,匹配上的位置设为1。iii. 把不同列的匹配结果进行位运算得到符合所有条件的记录下标。iv. 使用这个下标组装出最终的结果集。</code></pre><ul><li><p><strong>适合做聚合操作</strong></p></li><li><p><strong>适合大量的数据而不是小数据</strong></p></li></ul><p>缺点如下:</p><ul><li>不适合扫描小量数据</li><li>不适合随机的更新</li><li>不适合做含有删除和更新的实时操作</li><li>单行的数据是ACID的,多行的事务时,不支持事务的正常回滚,支持 I(Isolation)隔离性(事务串行提交),D(Durability)持久性,不能保证 A(Atomicity)原子性, C(Consistency)一致性</li></ul><h3 id="2-1-4-使用场景"><a href="#2-1-4-使用场景" class="headerlink" title="2.1.4 使用场景"></a>2.1.4 使用场景</h3><p>以HBase为例说明:</p><ul><li>大数据量 (100s TB级数据) 且有快速随机访问的需求</li><li>写密集型应用,每天写入量巨大,而相对读数量较小的应用<br>比如IM的历史消息,游戏的日志等等</li><li>不需要复杂查询条件来查询数据的应用<br>HBase只支持基于rowkey的查询,对于HBase来说,单条记录或者小范围的查询是可以接受的,大范围的查询由于分布式的原因,可能在性能上有点影响,HBase不适用与有join,多级索引,表关系复杂的数据模型</li><li>对性能和可靠性要求非常高的应用<br>由于HBase本身没有单点故障,可用性非常高。</li><li>数据量较大,而且增长量无法预估的应用,需要进行优雅的数据扩展的<br>HBase支持在线扩展,即使在一段时间内数据量呈井喷式增长,也可以通过HBase横向扩展来满足功能。</li><li>存储结构化和半结构化的数据</li></ul><h2 id="2-2-K-V数据库"><a href="#2-2-K-V数据库" class="headerlink" title="2.2 K-V数据库"></a>2.2 K-V数据库</h2><p>指的是使用键值(key-value)存储的数据库,其数据按照键值对的形式进行组织、索引和存储。</p><p>KV 存储非常适合不涉及过多数据关系业务关系的数据,同时能有效减少读写磁盘的次数,比 SQL 数据库存储拥有更好的读写性能,能够<strong>解决关系型数据库无法存储数据结构的问题</strong>。</p><h3 id="2-2-1-常见-K-V数据库"><a href="#2-2-1-常见-K-V数据库" class="headerlink" title="2.2.1 常见 K-V数据库"></a>2.2.1 常见 K-V数据库</h3><ul><li>Redis</li></ul><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/7.png" alt><br>Redis是一个使用ANSI C编写的开源、支持网络、基于内存、可选持久性的键值对存储数据库。从2015年6月开始,Redis的开发由Redis Labs赞助,而2013年5月至2015年6月期间,其开发由Pivotal赞助。在2013年5月之前,其开发由VMware赞助。根据月度排行网站DB-Engines.com的数据显示,Redis是最流行的键值对存储数据库。</p><ul><li>Cassandra</li></ul><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/8.gif" alt></p><p>Apache Cassandra(社区内一般简称为C*)是一套开源分布式NoSQL数据库系统。它最初由Facebook开发,用于储存收件箱等简单格式数据,集Google BigTable的数据模型与Amazon Dynamo的完全分布式架构于一身。Facebook于2008将 Cassandra 开源,此后,由于Cassandra良好的可扩展性和性能,被 Apple, Comcast,Instagram, Spotify, eBay, Rackspace, Netflix等知名网站所采用,成为了一种流行的分布式结构化数据存储方案。</p><ul><li>LevelDB<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/9.jpg" alt><br>LevelDB是一个由Google公司所研发的键/值对(Key/Value Pair)嵌入式数据库管理系统编程库, 以开源的BSD许可证发布。</li></ul><h3 id="2-2-2-相关特性"><a href="#2-2-2-相关特性" class="headerlink" title="2.2.2 相关特性"></a>2.2.2 相关特性</h3><p>以Redis为例:<br>优点如下:</p><ul><li>性能极高:Redis能支持超过10W的TPS</li><li>丰富的数据类型: Redis支持包括String,Hash,List,Set,Sorted Set,Bitmap和hyperloglog</li><li>丰富的特性:Redis还支持 publish/subscribe, 通知, key 过期等等特性</li></ul><p>缺点如下:<br>针对ACID,Redis事务不能支持原子性和持久性(A和D),只支持隔离性和一致性(I和C)<br>特别说明一下,这里所说的无法保证原子性,是针对Redis的事务操作,因为事务是不支持回滚(roll back),而因为Redis的单线程模型,<strong>Redis的普通操作是原子性的</strong></p><p>大部分业务不需要严格遵循ACID原则,例如游戏实时排行榜,粉丝关注等场景,即使部分数据持久化失败,其实业务影响也非常小。因此在设计方案时,需要根据业务特征和要求来做选择</p><h3 id="2-2-3-使用场景"><a href="#2-2-3-使用场景" class="headerlink" title="2.2.3 使用场景"></a>2.2.3 使用场景</h3><ul><li>适用场景<br>储存用户信息(比如会话)、配置文件、参数、购物车等等。这些信息一般都和ID(键)挂钩</li></ul><ul><li>不适用场景<ul><li>需要通过值来查询,而不是键来查询。Key-Value数据库中根本没有通过值查询的途径。</li><li>需要储存数据之间的关系。在Key-Value数据库中不能通过两个或以上的键来关联数据</li><li>需要事务的支持。在Key-Value数据库中故障产生时不可以进行回滚。</li></ul></li></ul><h2 id="2-3-文档数据库"><a href="#2-3-文档数据库" class="headerlink" title="2.3 文档数据库"></a>2.3 文档数据库</h2><p>文档数据库(也称为文档型数据库)是旨在将半结构化数据存储为文档的一种数据库。文档数据库通常以 JSON 或 XML 格式存储数据。</p><p>由于文档数据库的no-schema特性,可以存储和读取任意数据。</p><p>由于使用的数据格式是JSON或者BSON,因为JSON数据是自描述的,无需在使用前定义字段,读取一个JSON中不存在的字段也不会导致SQL那样的语法错误,<strong>可以解决关系型数据库表结构schema扩展不方便的问题</strong></p><h3 id="2-3-1-常见文档数据库"><a href="#2-3-1-常见文档数据库" class="headerlink" title="2.3.1 常见文档数据库"></a>2.3.1 常见文档数据库</h3><ul><li><p>MongoDB<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/10.jpg" alt></p></li><li><p><em>MongoDB*</em>是一种面向文档的数据库管理系统,由C++撰写而成,以此来解决应用程序开发社区中的大量现实问题。2007年10月,MongoDB由10gen团队所发展。2009年2月首度推出。</p></li><li><p>CouchDB<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/11.jpg" alt><br>Apache CouchDB是一个开源数据库,专注于易用性和成为”<strong>完全拥抱web的数据库</strong>“。它是一个使用JSON作为存储格式,JavaScript作为查询语言,MapReduce和HTTP作为API的NoSQL数据库。其中一个显著的功能就是多主复制。CouchDB的第一个版本发布在2005年,在2008年成为了Apache的项目。</p></li></ul><h3 id="2-3-2-相关特性"><a href="#2-3-2-相关特性" class="headerlink" title="2.3.2 相关特性"></a>2.3.2 相关特性</h3><p>以MongoDB为例进行说明</p><p>优点如下:</p><ul><li>新增字段简单<br>无需像关系型数据库一样先执行DDL语句修改表结构,程序代码直接读写即可</li><li>容易兼容历史数据<br>对于历史数据,即使没有新增的字段,也不会导致错误,只会返回空值,此时代码兼容处理即可</li><li>容易存储复杂数据<br>JSON是一种强大的描述语言,能够描述复杂的数据结构</li></ul><p>相比传统关系型数据库,文档数据库的缺点主要是对多条数据记录的事务支持较弱,具体体现如下:</p><ul><li>Atomicity(原子性)<br>仅支持单行/文档级原子性,不支持多行、多文档、多语句原子性</li><li>Isolation(隔离性)<br>隔离级别仅支持已提交读(Read committed)级别,可能导致不可重复读,幻读的问题</li><li>不支持复杂查询<br>例如join查询,如果需要join查询,需要多次操作数据库</li></ul><p>MongonDB还是支持多文档事务的Consistency(一致性)和Durability(持久性)</p><p>虽然官方宣布MongoDB将在4.0版本中正式推出多文档ACID事务支持,最后落地情况还有待见证。</p><h3 id="2-3-3-使用场景"><a href="#2-3-3-使用场景" class="headerlink" title="2.3.3 使用场景"></a>2.3.3 使用场景</h3><p><strong>适用场景</strong>:</p><ul><li>数据量很大或者未来会变得很大</li><li>表结构不明确,且字段在不断增加,例如内容管理系统,信息管理系统</li></ul><p><strong>不适用场景</strong>:</p><ul><li>在不同的文档上需要添加事务。Document-Oriented数据库并不支持文档间的事务</li><li>多个文档直接需要复杂查询,例如join</li></ul><h3 id="2-4-全文搜索引擎"><a href="#2-4-全文搜索引擎" class="headerlink" title="2.4 全文搜索引擎"></a>2.4 全文搜索引擎</h3><p>传统关系型数据库主要通过索引来达到快速查询的目的,在全文搜索的业务下,索引也无能为力,主要体现在:</p><ul><li>全文搜索的条件可以随意排列组合,如果通过索引来满足,则索引的数量非常多</li><li>全文搜索的模糊匹配方式,索引无法满足,只能用like查询,而like查询是整表扫描,效率非常低</li></ul><p>而全文搜索引擎的出现,正是<strong>解决关系型数据库全文搜索功能较弱的问题</strong></p><h3 id="2-4-1-基本原理"><a href="#2-4-1-基本原理" class="headerlink" title="2.4.1 基本原理"></a>2.4.1 基本原理</h3><p>全文搜索引擎的技术原理称为“倒排索引”(inverted index),是一种索引方法,其基本原理是建立单词到文档的索引。与之相对是,是“正排索引”,其基本原理是建立文档到单词的索引。</p><p>现在有如下文档集合:<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/12.png" alt><br>正排索引得到索引如下:<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/13.png" alt><br>可见,正排索引适用于根据文档名称查询文档内容</p><p>简单的倒排索引如下:<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/14.png" alt="简单的倒排索引"><br>带有单词频率信息的倒排索引如下:<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/15.png" alt="带有单词频率信息的倒排索"><br>可见,倒排索引适用于根据关键词来查询文档内容</p><h3 id="2-4-2-常见全文搜索引擎"><a href="#2-4-2-常见全文搜索引擎" class="headerlink" title="2.4.2 常见全文搜索引擎"></a>2.4.2 常见全文搜索引擎</h3><ul><li><p>Elasticsearch<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/16.jpg" alt>Elasticsearch是一个基于Lucene的搜索引擎。它提供了一个分布式,多租户 -能够全文搜索与发动机HTTP Web界面和无架构JSON文件。Elasticsearch是用Java开发的,并根据Apache License的条款作为开源发布。根据DB-Engines排名,Elasticsearch是最受欢迎的企业搜索引擎,后面是基于Lucene的Apache Solr。</p></li><li><p>Solr<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/17.png" alt>Solr是Apache Lucene项目的开源企业搜索平台。其主要功能包括全文检索、命中标示、分面搜索、动态聚类、数据库集成,以及富文本(如Word、PDF)的处理。Solr是高度可扩展的,并提供了分布式搜索和索引复制</p></li></ul><h3 id="2-4-3-相关特性"><a href="#2-4-3-相关特性" class="headerlink" title="2.4.3 相关特性"></a>2.4.3 相关特性</h3><p>以Elasticsearch为例:<br>优点如下:</p><ul><li>查询效率高 对海量数据进行近实时的处理</li><li>可扩展性<br>基于集群环境可以方便横向扩展,可以承载PB级数据</li><li>高可用<br>Elasticsearch集群弹性-他们将发现新的或失败的节点,重组和重新平衡数据,确保数据是安全的和可访问的</li></ul><p>缺点如下:</p><ul><li>ACID支持不足<br>单一文档的数据是ACID的,包含多个文档的事务时不支持事务的正常回滚,支持 I(Isolation)隔离性(基于乐观锁机制的),D(Durability)持久性,<strong>不支持 A(Atomicity)原子性,C(Consistency)一致性</strong></li><li>对类似数据库中通过外键的复杂的多表关联操作支持较弱</li><li>读写有一定延时,写入的数据,最快1s中能被检索到</li><li>更新性能较低,底层实现是先删数据,再插入新数据</li><li>内存占用大,因为Lucene 将索引部分加载到内存中</li></ul><h3 id="2-4-4-使用场景"><a href="#2-4-4-使用场景" class="headerlink" title="2.4.4 使用场景"></a>2.4.4 使用场景</h3><p>适用场景如下:</p><ul><li>分布式的搜索引擎和数据分析引擎</li><li>全文检索,结构化检索,数据分析</li><li>对海量数据进行近实时的处理<br>可以将海量数据分散到多台服务器上去存储和检索</li></ul><p>不适用场景如下:</p><ul><li>数据需要频繁更新</li><li>需要复杂关联查询</li></ul><h2 id="2-5-图形数据库"><a href="#2-5-图形数据库" class="headerlink" title="2.5 图形数据库"></a>2.5 图形数据库</h2><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/18.png" alt><br><strong>图形数据库应用图形理论存储实体之间的关系信息</strong>。最常见例子就是社会网络中人与人之间的关系。关系型数据库用于存储“关系型”数据的效果并不好,其查询复杂、缓慢、超出预期,而图形数据库的独特设计恰恰弥补了这个缺陷,解决关系型数据库存储和处理复杂关系型数据功能较弱的问题。</p><h3 id="2-5-1-常见图形数据库"><a href="#2-5-1-常见图形数据库" class="headerlink" title="2.5.1 常见图形数据库"></a>2.5.1 常见图形数据库</h3><ul><li>Neo4j</li></ul><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/19.jpg" alt><br>Neo4j是由Neo4j,Inc。开发的图形数据库管理系统。由其开发人员描述为具有原生图存储和处理的符合ACID的事务数据库,根据DB-Engines排名, Neo4j是最流行的图形数据库。</p><ul><li><p>ArangoDB </p><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/20.jpg" alt><br>ArangoDB是由triAGENS GmbH开发的原生多模型数据库系统。数据库系统支持三个重要的数据模型(键/值,文档,图形),其中包含一个数据库核心和统一查询语言AQL(ArangoDB查询语言)。查询语言是声明性的,允许在单个查询中组合不同的数据访问模式。ArangoDB是一个NoSQL数据库系统,但AQL在很多方面与SQL类似。</p></li><li><p>Titan<br><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/21.png" alt><br>Titan是一个可扩展的图形数据库,针对存储和查询包含分布在多机群集中的数百亿个顶点和边缘的图形进行了优化。Titan是一个事务性数据库,可以支持数千个并发用户实时执行复杂的图形遍历。</p></li></ul><h3 id="2-5-2-相关特性"><a href="#2-5-2-相关特性" class="headerlink" title="2.5.2 相关特性"></a>2.5.2 相关特性</h3><p>以Neo4j为例:</p><p>Neo4j 使用数据结构中图(graph)的概念来进行建模。<br>Neo4j 中两个最基本的概念是节点和边。节点表示实体,边则表示实体之间的关系。节点和边都可以有自己的属性。不同实体通过各种不同的关系关联起来,形成复杂的对象图。</p><p>针对关系数据,2种2数据库的存储结构不同:</p><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/22.png" alt="2种存储结构"><br>Neo4j中,存储节点时使用了”index-free adjacency”,即每个节点都有指向其邻居节点的指针,可以让我们在O(1)的时间内找到邻居节点。另外,按照官方的说法,在Neo4j中边是最重要的,是”first-class entities”,所以单独存储,这有利于在图遍历的时候提高速度,也可以很方便地以任何方向进行遍历</p><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/23.png" alt><br>如下优点:</p><ul><li><p>高性能表现<br>图的遍历是图数据结构所具有的独特算法,即从一个节点开始,根据其连接的关系,可以快速和方便地找出它的邻近节点。这种查找数据的方法并不受数据量的大小所影响,因为邻近查询始终查找的是有限的局部数据,不会对整个数据库进行搜索</p></li><li><p>设计的灵活性<br>数据结构的自然伸展特性及其非结构化的数据格式,让图数据库设计可以具有很大的伸缩性和灵活性。因为随着需求的变化而增加的节点、关系及其属性并不会影响到原来数据的正常使用</p></li><li><p>开发的敏捷性<br>直观明了的数据模型,从需求的讨论开始,到程序开发和实现,以及最终保存在数据库中的样子,它的模样似乎没有什么变化,甚至可以说本来就是一模一样的</p></li><li><p>完全支持ACID<br>不像别的NoSQL数据库Neo4j还具有完全事务管理特性,完全支持ACID事务管理</p></li></ul><p>缺点如下:</p><ul><li>具有支持节点,关系和属性的数量的限制</li><li>不支持拆分</li></ul><h3 id="2-5-3-使用场景"><a href="#2-5-3-使用场景" class="headerlink" title="2.5.3 使用场景"></a>2.5.3 使用场景</h3><p>适用场景如下:</p><ul><li>在一些关系性强的数据中,例如社交网络</li><li>推荐引擎。如果我们将数据以图的形式表现,那么将会非常有益于推荐的制定</li></ul><p>不适用场景如下:</p><ul><li>记录大量基于事件的数据(例如日志条目或传感器数据)</li><li>对大规模分布式数据进行处理,类似于Hadoop</li><li>适合于保存在关系型数据库中的结构化数据</li><li>二进制数据存储</li></ul><h1 id="3-总结"><a href="#3-总结" class="headerlink" title="3 总结"></a>3 总结</h1><p>关系型数据库和NoSQL数据库的选型,往往需要考虑几个指标:</p><ul><li>数据量</li><li>并发量</li><li>实时性</li><li>一致性要求</li><li>读写分布和类型</li><li>安全性</li><li>运维成本</li></ul><p>常见软件系统数据库选型参考如下:</p><ul><li>内部使用的管理型系统<br>如运营系统,数据量少,并发量小,首选考虑关系型</li><li>大流量系统<br>如电商单品页,后台考虑选关系型,前台考虑选内存型</li><li>日志型系统<br>原始数据考虑选列式,日志搜索考虑选倒排索引</li><li>搜索型系统<br>例如站内搜索,非通用搜索,如商品搜索,后台考虑选关系型,前台考虑选倒排索引</li><li>事务型系统<br>如库存,交易,记账,考虑选关系型型+缓存+一致性型协议</li><li>离线计算<br>如大量数据分析,考虑选列式或者关系型也可以</li><li>实时计算<br>如实时监控,可以考虑选内存型或者列式数据库</li></ul><p>设计实践中,要基于需求、业务驱动架构,无论选用RDB/NoSQL/DRDB,<strong>一定是以需求为导向,最终数据存储方案必然是各种权衡的综合性设计</strong></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://time.geekbang.org/column/intro/81?code=OK4eM0TBPTKGPRCzcZdzIeXjPACLfY3KCzATXOSWzXE%3D" target="_blank" rel="noopener">从0开始学架构 —— Alibaba 李运华 </a></p><p><a href="http://www.nosqlnotes.com/" target="_blank" rel="noopener">NoSQL漫谈</a></p><p><a href="https://www.ibm.com/developerworks/cn/java/j-lo-neo4j/index.html" target="_blank" rel="noopener">图形数据库 Neo4j 开发实战</a></p><p><a href="http://elf8848.iteye.com/blog/2083165" target="_blank" rel="noopener">大数据时代的 9 大Key-Value存储数据库</a></p><p><a href="http://www.redis.cn/topics/transactions.html" target="_blank" rel="noopener">事务—— Redis官方文档 </a></p><p><a href="http://www.ywnds.com/?p=6386" target="_blank" rel="noopener">MongoDB是如何实现事务的ACID?</a></p><p><a href="https://blog.csdn.net/u013474436/article/details/53437220" target="_blank" rel="noopener"><strong>MySQL脏读、虚读、幻读</strong></a></p><p><a href="https://www.oschina.net/news/71132/nosql-use-case" target="_blank" rel="noopener">全面梳理关系型数据库和 NoSQL 的使用情景</a></p><p><a href="https://www.csdn.net/article/2012-05-31/2806184" target="_blank" rel="noopener">浅析列式数据库的特点</a></p><p><a href="https://blog.csdn.net/qq_35952082/article/details/68490536" target="_blank" rel="noopener">一分钟搞懂列式与行式数据库</a></p><p><a href="https://blog.bcmeng.com/post/hbase-note.html" target="_blank" rel="noopener">HBase 基本概念</a></p><p> <a href="http://pauloortins.com/nosql-databases-why-we-should-use-and-which-one-we-should-choose/" target="_blank" rel="noopener">NoSQL Databases, why we should use, and which one we should choose</a></p><p><a href="https://blog.csdn.net/weisg81/article/details/72810454" target="_blank" rel="noopener">传统关系数据库与分布式数据库知识点</a></p><p><img src="/images/NoSQL-%E8%BF%98%E6%98%AF-SQL-%EF%BC%9F%E8%BF%99%E4%B8%80%E7%AF%87%E8%AE%B2%E6%B8%85%E6%A5%9A/24.png" alt></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>理解推荐系统</title>
<link href="/2019/10/13/li-jie-tui-jian-xi-tong/"/>
<url>/2019/10/13/li-jie-tui-jian-xi-tong/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/0.png" alt></p><p>本文主要介绍什么是推荐系统,为什么需要推荐系统,如何实现推荐系统的方案,包括实现推荐系统的一些常见模型,希望给读者提供学习实践参考。</p><h1 id="1-推荐系统概述"><a href="#1-推荐系统概述" class="headerlink" title="1 推荐系统概述"></a>1 推荐系统概述</h1><h2 id="为什么需要推荐系统"><a href="#为什么需要推荐系统" class="headerlink" title="为什么需要推荐系统"></a>为什么需要推荐系统</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/1.png" alt="信息爆炸"></p><ul><li>对于信息消费者,需要从大量信息中找到自己感兴趣的信息,而在信息过载时代,用户难以从大量信息中获取自己感兴趣、或者对自己有价值的信息。</li><li>对于信息生产者,信息生产者,需要让自己生产的信息脱颖而出,受到广大用户的关注。从物品的角度出发,推荐系统可以更好地发掘物品的长尾(long tail)。<br><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/2.png" alt="长尾效应"><blockquote><p><strong>长尾效应</strong>是美国《连线》杂志主编Chris Anderson在于2006年出版了《长尾理论》一书中指出,传统的80/20原则(80%的销售额来自于20%的热门品牌)在互联网的加入下会受到挑战。互联网条件下,由于货架成本极端低廉,电子商务网站往往能出售比传统零售店更多的商品。这些原来不受到重视的销量小但种类多的产品或服务由于总量巨大,累积起来的总收益超过主流产品的现象。</p></blockquote></li></ul><p>主流商品往往代表了绝大多数用户的需求,而长尾商品往往代表了一小部分用户的个性化需求。</p><p>推荐系统通过发掘用户的行为,找到用户的个性化需求,从而将长尾商品准确地推荐给需要它的用户,帮助用户发现那些他们感兴趣但很难发现的商品。</p><p><strong>推荐系统的任务</strong>在于:一方面帮助用户发现对自己有价值的信息,另一方面让信息能够展现在对它感兴趣的用户面前,从而实现信息消费者和信息生产者的双赢。</p><h2 id="推荐系统的本质"><a href="#推荐系统的本质" class="headerlink" title="推荐系统的本质"></a>推荐系统的本质</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/3.png" alt><br>通过一定的方式将用户和物品联系起来,而不同的推荐系统利用了不同的方式。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/4.png" alt="推荐系统常用的3种联系用户和物品的方式"><br>推荐系统就是自动联系用户和物品的一种工具,它能够在信息过载的环境中帮助用户发现令他们感兴趣的信息,也能将信息推送给对它们感兴趣的用户。</p><h2 id="评价指标"><a href="#评价指标" class="headerlink" title="评价指标"></a>评价指标</h2><p>从产品的角度出发,评价一个推荐系统可以从以下维度出发:</p><ul><li><p>用户满意度<br>用户作为推荐系统的重要参与者,其满意度是评测推荐系统的最重要指标。但是,用户满意度没有办法离线计算,只能通过用户调查或者在线实验获得。</p></li><li><p>预测准确度<br>预测准确度度量一个推荐系统或者推荐算法预测用户行为的能力。这个指标是最重要的推荐系统离线评测指标,从推荐系统诞生的那一天起,几乎99%与推荐相关的论文都在讨论这个指标。<br>在计算该指标时需要有一个离线的数据集,该数据集包含用户的历史行为记录。然后,将该数据集通过时间分成训练集和测试集。最后,通过在训练集上建立用户的行为和兴趣模型预测用户在测试集上的行为,并计算预测行为和测试集上实际行为的重合度作为预测准确度。</p></li><li><p>覆盖率<br>覆盖率( coverage )描述一个推荐系统对物品长尾的发掘能力。覆盖率有不同的定义方法,<br>最简单的定义为推荐系统能够推荐出来的物品占总物品集合的比例。</p></li><li><p>多样性<br>用户的兴趣是广泛的,为了满足用户广泛的兴趣,推荐列表需要能够覆盖用户不同的兴趣领域,即推荐结果需要具有多样性。</p></li><li><p>新颖性<br>新颖的推荐是指给用户推荐那些他们以前没有听说过的物品。在一个网站中实现新颖性的最简单办法是,把那些用户之前在网站中对其有过行为的物品从推荐列表中过滤掉。</p></li><li><p>惊喜度<br>与新颖性不同,如果推荐结果和用户的历史兴趣不相似,但却让用户觉得满意,那么就可以说推荐结果的惊喜度很高,而推荐的新颖性仅仅取决于用户是否听说过这个推荐结果。</p></li><li><p>信任度<br>对于基于机器学习的自动推荐系统,同样存在信任度( trust )的问题,如果用户信任推荐系统,那就会增加用户和推荐系统的交互。同样的推荐结果,以让用户信任的方式推荐给用户就更能让用户产生购买欲,而以类似广告形式的方法推荐给用户就可能很难让用户产生购买的意愿。<br>度量推荐系统的信任度只能通过问卷调查的方式,询问用户是否信任推荐系统的推荐结果。</p></li><li><p>实时性<br>推荐系统需要实时地更新推荐列表来满足用户新的行为变化,推荐系统需要能够将新加入系统的物品推荐给用户。这主要考验了推荐系统处理物品冷启动的能力。</p></li><li><p>健壮性<br>任何一个能带来利益的算法系统都会被人攻击,这方面最典型的例子就是搜索引擎。搜索引擎的作弊和反作弊斗争异常激烈,而健壮性(即 robust, 鲁棒性)指标衡量了一个推荐系统抗击作弊的能力。</p></li></ul><h1 id="2-基于用户行为推荐"><a href="#2-基于用户行为推荐" class="headerlink" title="2 基于用户行为推荐"></a>2 基于用户行为推荐</h1><h2 id="2-1-用户行为"><a href="#2-1-用户行为" class="headerlink" title="2.1 用户行为"></a>2.1 用户行为</h2><p>可以分为<strong>显性反馈行为(explicit feedback)</strong>和<strong>隐性反馈行为(implicit feedback)</strong></p><ul><li>显性反馈行为<br>是指用户明确表示对物品喜好的行为,主要方式就是评分和喜欢/不喜欢,常见的显性反馈行为可以参考如下表格:</li></ul><table><thead><tr><th align="left">用户行为</th><th align="center">特征</th><th align="center">作用</th></tr></thead><tbody><tr><td align="left">评分</td><td align="center">整数量化的偏好,可能得取值是[0,n],n一般的取值是5或者10</td><td align="center">通过用户对物品的评分,可以精确得到用户的偏好</td></tr><tr><td align="left">投票</td><td align="center">布尔值的偏好,取值是0或1</td><td align="center">通过用户对物品的投票,可以精确得到用户的偏好</td></tr><tr><td align="left">转发</td><td align="center">布尔值的偏好,取值是0或1</td><td align="center">通过用户对物品的投票,可以精确得到用户的偏好</td></tr><tr><td align="left">保存书签</td><td align="center">布尔值的偏好,取值是0或1</td><td align="center">通过用户对物品的投票,可以精确得到用户的偏好</td></tr><tr><td align="left">标记标签(tag)</td><td align="center">一些单词,需要对单词进行分析,得到偏好</td><td align="center">通过分析用户的标签,可以得到用户对物品的理解,用户的情感,是喜欢还是厌恶</td></tr><tr><td align="left">评论</td><td align="center">一些文字,需要进行文本分析得到偏好</td><td align="center">通过分析用户的评论,可以得到用户的情感,是喜欢还是厌恶</td></tr></tbody></table><ul><li>隐性反馈行为(implicit feedback)<br>指的是那些不能明确反应用户喜好的行为。最具代表性的隐性反馈行为就是<strong>页面浏览行为</strong>。用户浏览一个物品的页面并不代表用户一定喜欢这个页面展示的物品,比如可能因为这个页面链接显示在首页,用户更容易点击它而已。<br>相比显性反馈,隐性反馈虽然不明确,但数据量更大。在很多网站中,很多用户甚至只有隐性反<br>馈数据,而没有显性反馈数据。</li></ul><p>基于用户行为数据设计的推荐算法一般称为协同过滤算法。学术界对协同过滤算法进行了深入研究,提出了很多方法,比如基于邻域的算法( neighborhood-based )、隐语义模型( latent factor model )、基于图的随机游走算法( random walk on graph )等。</p><p>下面主要展开介绍基于领域的算法和隐语义模型算法。</p><h2 id="2-2-基于领域的算法"><a href="#2-2-基于领域的算法" class="headerlink" title="2.2 基于领域的算法"></a>2.2 基于领域的算法</h2><h3 id="2-2-1-算法概述"><a href="#2-2-1-算法概述" class="headerlink" title="2.2.1 算法概述"></a>2.2.1 算法概述</h3><p>基于邻域的方法是最著名的、在业界得到最广泛应用的推荐算法,而基于邻域的方法主要包含下面两种算法:</p><ul><li>基于用户的协同过滤算法(UserCF)</li><li>基于物品的协同过滤算法(ItemCF)</li></ul><p>算法涉及的基本步骤如下:</p><ul><li>1 收集用户偏好,把用户对物品的偏好转换成可量化的综合评分值</li><li>2 找到相似的用户或物品</li><li>3 计算推荐</li></ul><h3 id="2-2-2-相似度计算"><a href="#2-2-2-相似度计算" class="headerlink" title="2.2.2 相似度计算"></a>2.2.2 相似度计算</h3><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/5.png" alt><br>计算相似度主要有以下3种计算方式:</p><ul><li><p><strong>1 欧氏距离(Euclidean Distance)</strong><br>向量欧式距离:<br>$$ d(x,y) = \sqrt{( \sum(x_i-y_i)^2 ) } $$<br>相似度:<br>$$ sim(x,y) = \frac{1}{1+d(x,y)} $$</p></li><li><p><strong>2 皮尔逊相关系数(Pearson Correlation Coefficient)</strong><br>协方差,用来衡量2个向量的变化趋势是否一致:<br>$$ cov(X,Y)=\frac{\sum_n^{i=1}(X_i-\overline{X})(Y_i-\overline{Y}))}{n-1} $$<br>标准差:<br>$$ \sigma_X=\sqrt{ \frac{1}{N} \sum_{i=1}^{N}(x_i-\overline{x})^{2} } $$<br>皮尔逊相关系数:<br>$$ \rho_{X,Y} = corr(X,Y) = \frac{cov(X,Y)}{\sigma_X\sigma_Y} $$<br>皮尔逊相关系数使用协方差除以2个向量的标准差得到,值的范围[-1,1]</p></li><li><p><strong>3 Cosine 相似度(Cosine Similarity,余弦距离)</strong><br>$$ T(x,y) = \frac{x \cdot y}{||x||^2 \times ||y||^2 } = \frac{\sum x_i y_i}{\sqrt {\sum x_i^2} \sqrt {\sum y_i^2}} $$<br>其实就是求2个向量的夹角</p></li></ul><p>3种计算相关系数的算法中,<strong>皮尔逊相关系数</strong>在生产中最为常用。</p><h3 id="2-2-3-邻居的选择"><a href="#2-2-3-邻居的选择" class="headerlink" title="2.2.3 邻居的选择"></a>2.2.3 邻居的选择</h3><p>通过相似度计算出若干个最相似的邻居后,如何选择邻居?主要有以下方式:</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/6.png" alt="邻居的选择"></p><ul><li><strong>基于固定数量的邻居</strong></li></ul><p>该方式直接选择选择固定数量的邻居,有可能把相似度较小的对象也引入</p><ul><li><strong>基于相似度门槛的邻居</strong></li></ul><p>该方式先用相似度门槛筛选出邻居的一个集合,再从集合里面挑选出相似度较大的邻居。<br>可以避免把相似度较小的对象引入,效果更好</p><h3 id="2-2-4-基于用户的协同过滤算法-UserCF"><a href="#2-2-4-基于用户的协同过滤算法-UserCF" class="headerlink" title="2.2.4 基于用户的协同过滤算法(UserCF)"></a>2.2.4 基于用户的协同过滤算法(UserCF)</h3><h4 id="算法概述"><a href="#算法概述" class="headerlink" title="算法概述"></a>算法概述</h4><p>简单而言,就是<strong>给用户推荐和他兴趣相似的其他用户喜欢的物品</strong></p><p>在一个在线个性化推荐系统中,当一个用户 A 需要个性化推荐时,可以先找到和他有相似兴趣的其他用户,然后把那些用户喜欢的、而用户 A 没有听说过的物品推荐给 A 。这种方法称为基于用户的协同过滤算法。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/7.png" alt="基于用户的协同过滤"><br>用于A与用户C的兴趣比较相似,用户C喜欢了物品4,所以给用户A推荐物品4</p><h4 id="数学实现"><a href="#数学实现" class="headerlink" title="数学实现"></a>数学实现</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/8.png" alt="用户评分矩阵"><br>已知用户评分矩阵Matrix R(一般都是非常稀疏的),推断矩阵中问号处的评分值</p><h4 id="UserCF模型存在问题"><a href="#UserCF模型存在问题" class="headerlink" title="UserCF模型存在问题"></a>UserCF模型存在问题</h4><ul><li>对于一个新用户,很难找到邻居用户</li><li>对于一个新物品,所有最近的邻居都在其上没有多少打分</li></ul><h4 id="基础解决方案"><a href="#基础解决方案" class="headerlink" title="基础解决方案"></a>基础解决方案</h4><ul><li>相似度计算最好使用皮尔逊相似度</li><li>计算用户相似度考虑共同打分物品的数目<br>比如乘上<br>$$ \frac{min(n,N)}{N},n为共同打分的商品数,N为指定阈值 $$<br>这样可以让2个用户的共同打分的商品数越少,相似度越小</li><li>对打分进行归一化处理<br>比如把原来分数值范围是[0,10],归一化后变成[0,1]</li><li>设置一个相似度阈值</li></ul><h4 id="基于用户的协同过滤不流行的原因"><a href="#基于用户的协同过滤不流行的原因" class="headerlink" title="基于用户的协同过滤不流行的原因"></a>基于用户的协同过滤不流行的原因</h4><ul><li>数据稀疏问题,数据存取困难</li><li>数百万用户计算,用户之间两两计算相似度,计算量过大</li><li>人是善变的</li></ul><h3 id="2-2-5-基于物品的协同过滤算法-ItemCF"><a href="#2-2-5-基于物品的协同过滤算法-ItemCF" class="headerlink" title="2.2.5 基于物品的协同过滤算法(ItemCF)"></a>2.2.5 基于物品的协同过滤算法(ItemCF)</h3><h4 id="算法概述-1"><a href="#算法概述-1" class="headerlink" title="算法概述"></a>算法概述</h4><p>简单而言,就是<strong>给用户推荐和他之前喜欢的物品相似的物品</strong>。</p><p>基于物品的协同过滤算法(简称 ItemCF )给用户推荐那些和他们之前喜欢的物品相似的物品。</p><p>比如,该算法会因为你购买过《数据挖掘导论》而给你推荐《机器学习》。不过, <strong>ItemCF 算法并不利用物品的内容属性计算物品之间的相似度,它主要通过分析用户的行为记录计算物品之间的相似度</strong>。该算法认为,物品 A 和物品 B 具有很大的相似度是因为喜欢物品 A 的用户大都也喜欢物品B。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/9.png" alt><br>物品1和物品3都被用户A和用户B喜欢,所以认为是相似物品,所以当用户C喜欢物品1,就给用户C推荐物品3</p><p>算法主要主要步骤如下:</p><ul><li>计算物品之间的相似度</li><li>根据物品的相似度和用户的历史行为给用户生成推荐列表</li></ul><h4 id="数学实现思路"><a href="#数学实现思路" class="headerlink" title="数学实现思路"></a>数学实现思路</h4><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/10.png" alt="预测用户评分"><br>需要用户用户5对物品1的评分r_15,由于物品3、物品6是与物品1最为相似的2个物品,取相似度作为权重,所以r_15可以预测如下:<br>$$ r_15 = (0.41<em>2 + 0.59</em>3)/ (0.41 + 0.59) = 2.6 $$</p><h4 id="模型优势"><a href="#模型优势" class="headerlink" title="模型优势"></a>模型优势</h4><ul><li>计算性能高,通常用户数量远大于物品数量<br>实际计算物品之间的相似度,可以只选择同一个大分类下的类似物品来计算,以此减少计算量</li><li>可预先保留结果,物品并不善变</li></ul><h3 id="2-2-6-UserCF和ItemCF综合比较"><a href="#2-2-6-UserCF和ItemCF综合比较" class="headerlink" title="2.2.6 UserCF和ItemCF综合比较"></a>2.2.6 UserCF和ItemCF综合比较</h3><p> UserCF 给用户推荐那些和他有共同兴趣爱好的用户喜欢的物品,而 ItemCF 给用户推荐那些和他之前喜欢的物品类似的物品。从这个算法的原理可以看到:</p><ul><li>UserCF 的推荐结果着重于反映和用户兴趣相似的小群体的热点</li><li>ItemCF的推荐结果着重于维系用户的历史兴趣。</li></ul><p>换句话说:</p><ul><li>UserCF 的推荐更社会化,反映了用户所在的小型兴趣群体中物品的热门程度</li><li>ItemCF 的推荐更加个性化,反映了用户自己的兴趣传承</li></ul><table><thead><tr><th align="left"></th><th align="center">UserCF</th><th align="center">ItemCF</th></tr></thead><tbody><tr><td align="left">性能</td><td align="center">适用于用户较少的场合,如果用户很多,计算用户相似度矩阵代价很大</td><td align="center">适用于物品数明显小于用户数的场合,如果物品很多(网页),计算物品相似度矩阵代价很大</td></tr><tr><td align="left">领域</td><td align="center">时效性较强,用户个性化兴趣不太明显的领域</td><td align="center">长尾物品丰富,用户个性化需求强烈的领域</td></tr><tr><td align="left">实时性</td><td align="center">用户有新行为,不一定造成推荐结果的立即变化</td><td align="center">用户有新行为,一定会导致推荐结果的实时变化</td></tr><tr><td align="left">冷启动</td><td align="center">在新用户对很少的物品产生行为后,不能立即对他进行个性化推荐,因为用户相似度表是每隔一段时间离线计算的。<br><br>新物品上线后一段时间,一旦有用户对物品产生行为,就可以将新物品推荐给和对它产生行为的用户兴趣相似的其他用户</td><td align="center">新用户只要对一个物品产生行为,就可以给他推荐和该物品相关的其他物品。<br><br>但没有办法在不离线更新物品相似度表的情况下将新物品推荐给用户</td></tr><tr><td align="left">推荐理由</td><td align="center">很难提供令用户信服的推荐解释</td><td align="center">利用用户的历史行为给用户做推荐解释,可以令用户比较信服</td></tr><tr><td align="left">常见场景</td><td align="center">实时新闻<br>突发情况</td><td align="center">图书<br>电子商务<br>电影<br>外卖</td></tr></tbody></table><h1 id="3-基于用户标签推荐"><a href="#3-基于用户标签推荐" class="headerlink" title="3 基于用户标签推荐"></a>3 基于用户标签推荐</h1><h2 id="3-1-标签推荐概述"><a href="#3-1-标签推荐概述" class="headerlink" title="3.1 标签推荐概述"></a>3.1 标签推荐概述</h2><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/11.png" alt="推荐系统联系用户和物品的3种途径"><br>推荐系统的目的是联系用户的兴趣和物品,这种联系需要依赖不同的媒介。目前流行的推荐系统基本上通过3种方式联系用户兴趣和物品:</p><ul><li>基于用户推荐UserCF<br>利用和用户兴趣相似的其他用户,给用户推荐那些和他们兴趣爱好相似的其他用户喜欢的物品。</li><li>基于物品推荐ItemCF<br>给用户推荐与他喜欢过的物品相似的物品</li><li>基于特征<br>这里的特征有不同的表现方式,比如可以表现为物品的属性集合(比如对于图书,属性集合<br>包括作者、出版社、主题和关键词等),也可以表现为隐语义向量(latent factor vector)。</li></ul><h2 id="3-2-标签相关问题"><a href="#3-2-标签相关问题" class="headerlink" title="3.2 标签相关问题"></a>3.2 标签相关问题</h2><h3 id="3-2-1-标签的定义"><a href="#3-2-1-标签的定义" class="headerlink" title="3.2.1 标签的定义"></a>3.2.1 标签的定义</h3><p>根据维基百科的定义,标签是一种无层次化结构的、用来描述信息的关键词,它可以用来<br>描述物品的语义。根据给物品打标签的人的不同,标签应用一般分为两种:</p><ul><li>一种是让作者或者专家给物品打标签</li><li>另一种是让普通用户给物品打标签,也就是UGC(User Generated Content,用户生成的内容)的标签应用UGC的标签系统是一种表示用户兴趣和物品语义的重要方式。<br>当一个用户对一个物品打上一个标签,这个标签一方面描述了用户的兴趣,另一方面则表示了物品的语义,从而将用户和物品联系了起来。因此下面主要讨论UGC的标签应用,研究用户给物品打标签的行为,探讨如何通过分析这种行为给用户进行个性化推荐。</li></ul><h3 id="3-2-2-用户为什么要打标签"><a href="#3-2-2-用户为什么要打标签" class="headerlink" title="3.2.2 用户为什么要打标签"></a>3.2.2 用户为什么要打标签</h3><p>从产品的角度,我们需要理解用户打标签的行为,为什么要打标签,只有深入了解用户的行为,我们才能基于这个行为设计出令他们满意的个性化推荐系统。</p><p>用户这个行为背后的原因主要可以从2个维度进行探讨:</p><ul><li>社会维度,有些用户标注是给内容上传者使用的(便于上传者组织自己的信息),而有些用户标注是给广大用户使用的(便于帮助其他用户找到信息)。</li><li>功能维度,有些标注用于更好地组织内容,方便用户将来的查找,而另一些标注用于传达某种信息,比如照片的拍摄时间和地点等。</li></ul><h3 id="3-2-3-用户打什么样的标签"><a href="#3-2-3-用户打什么样的标签" class="headerlink" title="3.2.3 用户打什么样的标签"></a>3.2.3 用户打什么样的标签</h3><ul><li>表明物品是什么</li><li>表明物品的种类</li><li>表明谁拥有物品<br>比如很多博客的标签中会包括博客的作者等信息</li><li>表达用户的观点<br>比如用户认为网页很有趣,就会打上标签 funny (有趣),认为很无聊,就会打上标签 boring (无聊)</li><li>用户相关的标签<br>比如 my favorite (我最喜欢的)、 my comment (我的评论)等</li><li>用户的任务<br>比如 to read (即将阅读)、 job search (找工作)等</li></ul><h3 id="3-2-4-为什么要给用户推荐标签"><a href="#3-2-4-为什么要给用户推荐标签" class="headerlink" title="3.2.4 为什么要给用户推荐标签"></a>3.2.4 为什么要给用户推荐标签</h3><p>用户浏览某个物品时,标签系统非常希望用户能够给这个物品打上高质量的标签,这样才能促进标签系统的良性循环。因此,很多标签系统都设计了标签推荐模块给用户推荐标签。一般认为,给用户推荐标签有以下好处。</p><ul><li>方便用户输入标签<br>让用户从键盘输入标签无疑会增加用户打标签的难度,这样很多用户不愿意给物品打标签,因此我们需要一个辅助工具来减小用户打标签的难度,从而提高用户打标签的参与度。</li><li>提高标签质量<br>同一个语义不同的用户可能用不同的词语来表示。这些同义词会使标签的词表变得很庞大,而且会使计算相似度不太准确。而使用推荐标签时,我们可以对词表进行选择,首先保证词表不出现太多的同义词,同时保证出现的词都是一些比较热门的、有代表性的词。</li></ul><h3 id="3-2-5-如何给用户推荐标签"><a href="#3-2-5-如何给用户推荐标签" class="headerlink" title="3.2.5 如何给用户推荐标签"></a>3.2.5 如何给用户推荐标签</h3><p>用户 u 给物品 i 打标签时,我们有很多方法可以给用户推荐和物品 i 相关的标签。比较简单的方<br>法有 4 种:</p><ul><li><p>给用户 u 推荐整个系统里最热门的标签(这里将这个算法称为 PopularTags ),这个算法太简单了,甚至于不能称为一种标签推荐算法</p></li><li><p>给用户 u 推荐物品 i 上最热门的标签(这里将这个算法称为 ItemPopularTags )</p></li><li><p>用户 u 推荐他自己经常使用的标签(这里将这个算法称为 UserPopularTags )</p></li><li><p>前面两种的融合(这里记为 HybridPopularTags ),该方法通过一个系数将上面的推荐结果线性加权,然后生成最终的推荐结果</p></li></ul><h2 id="3-3-一个最简单的算法"><a href="#3-3-一个最简单的算法" class="headerlink" title="3.3 一个最简单的算法"></a>3.3 一个最简单的算法</h2><p>基本步骤如下:</p><ul><li>统计每个用户最常用的标签</li><li>对于每个标签,统计被打过这个标签次数最多的物品</li><li>对于一个用户,首先找到他常用的标签,然后找到具有这些标签的最热门物品推荐给这个用户</li></ul><p>对于上面算法,用户u对于物品i的兴趣公式如下:<br>$$ p(u,i)=\sum_bn_{u,b}n_{b,i} $$<br>$ B(u) $ 是用户u打过的标签集合,<br>$ B(i) $是物品i被打过标签的集合,<br>$ n_{u,b}$ 是用户 u 打过标签b的次数,<br>$ n_{b,i} $是物品 i 被打过标签 b 的次数</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/12.png" alt="用户 - 标签 - 物品"></p><p>某用户使用过“幽默”标签10次,“搞笑”标签3次,“讽刺”标签6次。这3个标签被物品A使用的次数分别的4、7、2。<br>由此计算用户对物品的兴趣值为:$$ 10 * 4 + 3* 7 + 6*2 = 73$$</p><p>上面的计算公式会倾向于给热门标签对应的热门物品很大的权重,因此会造成推荐热门的物品给用户,从而降低推荐结果的新颖性,还有数据稀疏性的问题,可以通过计算结果除以惩罚项来进行修正。</p><h1 id="4-系统冷启动问题"><a href="#4-系统冷启动问题" class="headerlink" title="4 系统冷启动问题"></a>4 系统冷启动问题</h1><h2 id="问题简介"><a href="#问题简介" class="headerlink" title="问题简介"></a>问题简介</h2><p>系统冷启动(cold start)问题主要在于如何在一个新开发的网站上(还没有用户,也没有用户行为,只有一些物品的信息)设计个性化推荐系统,从而在网站刚发布时就让用户体验到个性化推荐服务这一问题</p><p>主要可以分为3类:</p><ul><li><p>用户冷启动<br>用户冷启动问题主要在于如何给新用户做个性化推荐。当新用户到来时,我们没有他的行为数据,所以也无法根据他的历史行为预测其兴趣,从而无法借此给他做个性化推荐。</p></li><li><p>物品冷启动<br>物品冷启动问题主要在于如何解决如何将新的物品推荐给可能对它感兴趣的用户。</p></li><li><p>系统冷启动<br>系统刚刚新上线,用户、物品数据较少</p></li></ul><h2 id="解决思路"><a href="#解决思路" class="headerlink" title="解决思路"></a>解决思路</h2><p>针对上述3类冷启动问题,一般来说,可以参考如下解决方案:</p><ul><li><p>提供非个性化的推荐<br>非个性化推荐的最简单例子就是热门排行榜,我们可以给用户推荐热门排行榜,然后等到用户数据收集到一定的时候,再切换为个性化推荐。这也是最常见的解决方案。</p></li><li><p>利用用户注册时提供的年龄、性别等数据做粗粒度的个性化。</p></li><li><p>要求用户在首次登录时提供反馈,比如输入感兴趣的标签,或感兴趣的物品。收集用户对物品的兴趣信息,然后给用户推荐那些和这些物品相似的物品。</p></li><li><p>对于新加入的物品,可以利用内容信息,将它们推荐给喜欢过和它们相似的物品的用户。</p></li><li><p>在系统冷启动时,可以引入专家的知识,通过一定的高效方式迅速建立起物品的相关度表。</p></li></ul><h1 id="5-评估指标"><a href="#5-评估指标" class="headerlink" title="5 评估指标"></a>5 评估指标</h1><p>令 $ R(u) $ 是根据用户在训练集上的行为给用户作出的推荐列表, $T(u) $ 是用户在测试集上的行为列表</p><ul><li>准确率<br>用于度量模型的预测值与真实值之间的误差<br>$$ RMSE = \frac {\sqrt {\sum_{u,i\in T (r_{ui} - \hat r_{ui})^2} }}{|T|} $$</li></ul><ul><li><p>召回率<br>用于度量有多个正例被分为正例,这里是正确推荐的数量占测试集合上用户行为列表的比例。<br>$$ Recall = \frac{\sum_{u \in U}|R(u) \bigcap T(u)|}{\sum_{u \in U}|T(u)|} $$</p></li><li><p>覆盖率<br>用户衡量推荐的物品占全部商品的比例,一般我们推荐的物品希望尽可能覆盖更多类别<br>常见有2种计算方法:<br>通过推荐的商品占总商品的比例<br>$$ Coverage = \frac {U_{u \in U} R(u)}{|I|}$$<br>或者通过推荐物品的熵值得到覆盖率,熵值越大,覆盖率越大<br>$$ H = - \sum_{i=1}^n p(i) \log p(i) $$</p></li><li><p>多样性<br>用于衡量每次推荐里面的推送的物品占所有可能性的比率,多样性越大,每次推荐的物品越丰富<br>$$ Diversity = 1 - \frac{\sum_{i,j \in R(u), i \neq j}s(i,j) }{\frac{1}{2}|R(u)|(|R(u)|-1)} $$</p></li></ul><p>实际上,不同的平台还有不同的衡量标准,例如用户满意度,广告收益,需要结合实际业务情况做策略调整。</p><h1 id="6-系统架构"><a href="#6-系统架构" class="headerlink" title="6 系统架构"></a>6 系统架构</h1><h2 id="6-1-基于特征的推荐系统"><a href="#6-1-基于特征的推荐系统" class="headerlink" title="6.1 基于特征的推荐系统"></a>6.1 基于特征的推荐系统</h2><p>再次回顾一下上面提到的推荐系统联系用户和物品的3种途径</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/13.png" alt="推荐系统联系用户和物品的3种途径"><br>如果将这3种方式都抽象一下就可以发现,如果认为用户喜欢的物品也是一种用户特征,或者和用户兴趣相似的其他用户也是一种用户特征,那么用户就和物品通过特征相联系。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/14.png" alt="基于特征的推荐系统"><br>用户特征种类特别多,主要包括一下几类</p><ul><li><p>用户注册属性<br>年龄、性别、国籍等</p></li><li><p>用户行为特征<br>浏览、点赞、评论、购买等</p></li></ul><h2 id="6-2-系统整体架构"><a href="#6-2-系统整体架构" class="headerlink" title="6.2 系统整体架构"></a>6.2 系统整体架构</h2><p>由于推送策略本身的复杂性,如果要在一个系统中把上面提到的各种特征和任务都统筹考虑,那么系统将会非常复杂,而且很难通过配置文件方便地配置不同特征和任务的权重。<br>因此,推荐系统需要由多个推荐引擎组成,每个推荐引擎负责一类特征和一种任务,而推荐系统的任务只是将推荐引擎的结果按照一定权重或者优先级合并、排序然后返回。</p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/15.png" alt="推荐系统基本架构图"><br>这样做有2个好处:</p><ul><li><p>1 可以方便地增加/删除引擎,控制不同引擎对推荐结果的影响。对于绝大多数需求,只需要通过不同的引擎组合实现。</p></li><li><p>2 可以实现推荐引擎级别的用户反馈。每一个推荐引擎其实代表了一种推荐策略,而不同的用户可能喜欢不同的推荐策略。</p><ul><li>有些用户可能喜欢利用他的年龄性别作出的推荐</li><li>有些用户可能比较喜欢看到新加入的和他兴趣相关的视频</li><li>有些用户喜欢比较新颖的推荐</li><li>有些用户喜欢专注于一个邻域的推荐</li><li>有些用户喜欢多样的推荐。</li></ul></li></ul><p>我们可以将每一种策略都设计成一个推荐引擎,然后通过分析用户对推荐结果的反馈了解用户比较喜欢哪些引擎推荐出来的结果,从而对不同的用户给出不同的引擎组合权重。</p><h2 id="6-3-推荐引擎架构"><a href="#6-3-推荐引擎架构" class="headerlink" title="6.3 推荐引擎架构"></a>6.3 推荐引擎架构</h2><p>推荐引擎使用一种或几种用户特征,按照一种推荐策略生成一种类型物品的推荐列表,基本架构如下图:<br><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/16.png" alt="推荐引擎架构图"><br>如图,推荐引擎架构主要包括3部分</p><ul><li><p>用户行为数据模块<br>图中A部分,该部分负责从数据库或者缓存中拿到用户行为数据,通过分析不同行为,生成当前用户的特征向量。不过如果是使用非行为特征,就不需要使用行为提取和分析模块了。该模块的输出是用户特征向量。</p></li><li><p>物品数据模块<br>图中B部分,该部分负责将用户的特征向量通过特征-物品相关矩阵转化为初始推荐物品列表。</p></li><li><p>最终结果生成模块<br>图中C部分,该部分负责对初始的推荐列表进行过滤、排名等处理,从而生成最终的推荐结果。</p></li></ul><p>其中,有几个模块需要特别介绍一下:</p><ul><li><p>候选物品集合<br>特征 — 物品相关推荐模块还可以接受一个候选物品集合。候选物品集合的目的是保证推荐结果只包含候选物品集合中的物品。它的应用场合一般是产品需求希望将某些类型的电视剧推荐给用户。比如有些产品要求给用户推荐最近一周加入的新物品,那么候选物品集合就包括最近一周新加的物品。</p></li><li><p>过滤模块<br>在得到初步的推荐列表后,还不能把这个列表展现给用户,首先需要按照产品需求对结果进行过滤,过滤掉那些不符合要求的物品。一般来说,过滤模块会过滤掉以下物品:</p><ul><li><p><strong>用户已经产生过行为物品</strong><br>因为推荐系统的目的是帮助用户发现物品,因此没必要给用户推荐他已经知道的物品,这样可以保证推荐结果的新颖性。</p></li><li><p><strong>候选物品以外的物品</strong><br>候选物品集合一般有两个来源,一个是产品需求。比如在首页可能要求将新加入的物品推荐给用户,因此需要在过滤模块中过滤掉不满足这一条件的物品。另一个来源是用户自己的选择,比如用户选择了某一个价格区间,只希望看到这个价格区间内的物品,那么过滤模块需要过滤掉不满足用户需求的物品。</p></li><li><p><strong>某些质量很差的物品</strong><br>为了提高用户的体验,推荐系统需要给用户推荐质量好的物品,那么对于一些绝大多数用户评论都很差的物品,推荐系统需要过滤掉。这种过滤一般以用户的历史评分为依据,比如过滤掉平均分在2分以下的物品。</p></li></ul></li><li><p>排名模块<br>经过过滤后的推荐结果直接展示给用户一般也没有问题,但如果对它们进行一些排名,则可以更好地提升用户满意度。实际进行排名时,可以基于新颖性、多样性、用户反馈进行排名优化。</p></li></ul><h1 id="7-总结"><a href="#7-总结" class="headerlink" title="7 总结"></a>7 总结</h1><p>除了本文介绍的模型算法,基于用户行为推荐还有隐语义模型,基于图的模型比较常见,还有的基于上下文、社交网络推荐。实际有一些常见的算法库可以实际推荐系统运算,包括LibRec,Crab等。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p>《推荐系统实践》</p><p><a href="https://cloud.tencent.com/developer/article/1070529" target="_blank" rel="noopener">一文读懂推荐系统知识体系)</a></p><p><a href="https://www.jianshu.com/p/319e4933c5ba" target="_blank" rel="noopener">个性化推荐系统总结</a></p><p><a href="https://www.cnblogs.com/redbear/p/8594939.html" target="_blank" rel="noopener">推荐系统介绍</a></p><p><img src="/images/%E7%90%86%E8%A7%A3%E6%8E%A8%E8%8D%90%E7%B3%BB%E7%BB%9F/17.png" alt></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>架构设计方法初探</title>
<link href="/2019/10/13/jia-gou-she-ji-fang-fa-chu-tan/"/>
<url>/2019/10/13/jia-gou-she-ji-fang-fa-chu-tan/</url>
<content type="html"><![CDATA[<p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/0.jpg" alt></p><pre><code>作者 陈彩华文章转载交流请联系 [email protected]</code></pre><p>最近学习了阿里资深技术专家李运华的架构设计教程,颇有收获,总结一下。</p><p>本文主要介绍架构设计的相关概念,系统复杂度的来源,架构设计的基本原则和流程。</p><h1 id="1-基本概念和目的"><a href="#1-基本概念和目的" class="headerlink" title="1 基本概念和目的"></a>1 基本概念和目的</h1><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/1.png" alt="架构设计的基本概念和目的"><br>架构设计的目的<br>是为了解决系统复杂度带来的问题,并不是要面面俱到,不需要每个架构都具备高性能、高可用、高扩展等特点,而是要识别出实际业务实际情况的复杂点,然后有有<strong>针对性地解决问题</strong>,即:<strong>有的放矢,而不是贪大求全</strong>,即:</p><ul><li>确定系统边界。确定系统在技术层面上的做与不做。</li><li>确定系统内模块之间的关系。确定模块之间的依赖关系及模块的宏观输入与输出。</li><li>确定指导后续设计与演化的原则。使后续的子系统或模块设计在一个规定的框架内继续演化。</li><li>确定非功能性需求。非功能性需求是指安全性、可用性、可扩展性等。</li></ul><p>在实际情况中,不一定每个系统都要做架构设计,需要结合实际情况。有时候最简单的设计开发效率反而是最高的,架构设计毕竟要投入时间和人力,这部分投入如果用来尽早编码,项目也许会更快。</p><h1 id="2-架构设计复杂度来源"><a href="#2-架构设计复杂度来源" class="headerlink" title="2 架构设计复杂度来源"></a>2 架构设计复杂度来源</h1><h3 id="高性能"><a href="#高性能" class="headerlink" title="高性能"></a>高性能</h3><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/2.png" alt="高性能"></p><h3 id="高可用"><a href="#高可用" class="headerlink" title="高可用"></a>高可用</h3><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/3.png" alt="高可用"></p><h3 id="可扩展性"><a href="#可扩展性" class="headerlink" title="可扩展性"></a>可扩展性</h3><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/4.png" alt="可扩展性"></p><h3 id="低成本、安全、规模"><a href="#低成本、安全、规模" class="headerlink" title="低成本、安全、规模"></a>低成本、安全、规模</h3><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/5.png" alt="低成本、安全、规模"></p><h1 id="3-架构设计三原则"><a href="#3-架构设计三原则" class="headerlink" title="3 架构设计三原则"></a>3 架构设计三原则</h1><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/6.png" alt="架构设计三原则"></p><h3 id="合适原则"><a href="#合适原则" class="headerlink" title="合适原则"></a>合适原则</h3><p>GFS为何在Google诞生,而不是在Microsoft诞生,其中Google有那么庞大的数据是一个主要因素,而不是因为Google的工程师比Microsoft的工程师更加聪明。</p><p>真正优秀的架构都是企业<strong>在当前人力、条件、业务等各方面约束条件下设计出来的</strong>,能够合理地将资源整合一起并发挥出最大功效,并且能迅速落地。这也是很多BAT出来的架构师到了小公司或者创业团队反而做不出成绩的原因,因为没有大公司的平台、资源、积累,只是生搬硬套大公司的做法,失败的效率非常高。</p><h3 id="简单原则"><a href="#简单原则" class="headerlink" title="简单原则"></a>简单原则</h3><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/7.png" alt="软件领域的复杂性"><br>无论是结构的复杂性还是逻辑的复杂性,都会存在各种问题,所以架构设计时如果简单方案和复杂的方案都可以满足需求,最好选择简单的方案。《UNIX编程艺术》总结的KISS(<em>Keep It Simple,Stupid!</em>)原则一样适用于架构设计。</p><h3 id="演化原则"><a href="#演化原则" class="headerlink" title="演化原则"></a>演化原则</h3><p>对于软件系统来说,变化才是主题。软件架构需要根据业务的发展而不断变化。<br>如果没有把握“<strong>软件架构需要根据业务发展不断变化</strong>”这个本质,在做架构设计的时候就很容易陷入一个误区:试图一步到位设计一个软件架构,期望不管业务如何变化,架构都稳如磐石。</p><p>为了实现这样的目标,要么照搬业界大公司公开发表的方案;要么投入庞大的资源和时间来做各种各样的预测、分析、设计。无论哪种做法,后果都很明显:投入巨大,落地遥遥无期。更让人沮丧的是,就算跌跌撞撞拼死拼活终于落地,却发现很多预测和分析都是不靠谱的。</p><p>实践中,架构师要提醒自己不要贪大求全,遵循演化优于一步到位的原则,因为业务的发展和变化总是很快的,<strong>无论多牛的团队,都不可能完美预测所有的业务发展和变化路径。</strong>实践中可以参考如下建议:</p><ul><li><p>首先,设计出来的架构要<strong>满足当时的业务需要</strong>。</p></li><li><p>其次,架构要不断地在实际应用过程中迭代,保留优秀的设计,修复有缺陷的设计,改正错误的设计,去掉无用的设计,使得架构逐渐完善。</p></li><li><p>第三,当业务发生变化时,架构要扩展、重构,甚至重写;代码也许会重写,但有价值的经验、教训、逻辑、设计等却可以在新架构中延续。</p></li></ul><h1 id="4-架构设计的流程"><a href="#4-架构设计的流程" class="headerlink" title="4 架构设计的流程"></a>4 架构设计的流程</h1><p><img src="/images/%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%B3%95%E5%88%9D%E6%8E%A2/8.png" alt="架构设计的流程"><br>参考资料:</p><p><a href="https://time.geekbang.org/column/intro/81?code=OK4eM0TBPTKGPRCzcZdzIeXjPACLfY3KCzATXOSWzXE%3D" target="_blank" rel="noopener">从0开始学架构——李运华 </a></p><p><a href="https://www.ibm.com/developerworks/cn/rational/r-4p1-view/" target="_blank" rel="noopener">架构蓝图–软件架构 “4+1” 视图模型</a></p><p><a href="https://github.com/alibaba/p3c/blob/master/%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%EF%BC%88%E8%AF%A6%E5%B0%BD%E7%89%88%EF%BC%89.pdf" target="_blank" rel="noopener">《阿里巴巴开发手册(详尽版)》</a></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>分布式任务调度平台XXL-JOB</title>
<link href="/2019/10/13/fen-bu-shi-ren-wu-diao-du-ping-tai-xxl-job/"/>
<url>/2019/10/13/fen-bu-shi-ren-wu-diao-du-ping-tai-xxl-job/</url>
<content type="html"><![CDATA[<p><img src="/images/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E5%B9%B3%E5%8F%B0XXL-JOB/0.jpg" alt><br>本文主要介绍分布式任务调度平台XXL-JOB(v2.1.0版本),包括功能特性、实现原理、优缺点、同类框架比较等</p><h1 id="基本介绍"><a href="#基本介绍" class="headerlink" title="基本介绍"></a>基本介绍</h1><p>项目开发中,常常以下场景需要分布式任务调度:</p><ul><li>同一服务多个实例的任务存在互斥时,需要统一协调</li><li>定时任务的执行需要支持高可用、监控运维、故障告警</li><li>需要统一管理和追踪各个服务节点定时任务的运行情况,以及任务属性信息,例如任务所属服务、所属责任人</li></ul><p>因此,XXL-JOB应运而生:<br>XXL-JOB是一个开源的轻量级分布式任务调度平台,其核心设计目标是开发迅速、学习简单、轻量级、易扩展、开箱即用,其中“XXL”是主要作者,大众点评<strong>许雪里</strong>名字的缩写</p><p>自2015年开源以来,已接入数百家公司的线上产品线,接入场景涉及电商业务,O2O业务和大数据作业等</p><h1 id="功能特性"><a href="#功能特性" class="headerlink" title="功能特性"></a>功能特性</h1><p>主要功能特性如下:</p><ul><li><p><strong>简单灵活</strong><br>提供Web页面对任务进行管理,管理系统支持用户管理、权限控制;<br>支持容器部署;<br>支持通过通用HTTP提供跨平台任务调度;</p></li><li><p><strong>丰富的任务管理功能</strong><br>支持页面对任务CRUD操作;<br>支持在页面编写脚本任务、命令行任务、Java代码任务并执行;<br>支持任务级联编排,父任务执行结束后触发子任务执行;<br>支持设置任务优先级;<br>支持设置指定任务执行节点路由策略,包括轮询、随机、广播、故障转移、忙碌转移等;<br>支持Cron方式、任务依赖、调度中心API接口方式触发任务执行</p></li><li><p><strong>高性能</strong><br>调度中心基于线程池多线程触发调度任务,快任务、慢任务基于线程池隔离调度,提供系统性能和稳定性;<br>任务调度流程全异步化设计实现,如异步调度、异步运行、异步回调等,有效对密集调度进行流量削峰;</p></li><li><p><strong>高可用</strong><br>任务调度中心、任务执行节点均 集群部署,支持动态扩展、故障转移<br>支持任务配置路由故障转移策略,执行器节点不可用是自动转移到其他节点执行<br>支持任务超时控制、失败重试配置<br>支持任务处理阻塞策略:调度当任务执行节点忙碌时来不及执行任务的处理策略,包括:串行、抛弃、覆盖策略</p></li><li><p><strong>易于监控运维</strong><br>支持设置任务失败邮件告警,预留接口支持短信、钉钉告警;<br>支持实时查看任务执行运行数据统计图表、任务进度监控数据、任务完整执行日志;</p></li></ul><h1 id="系统设计"><a href="#系统设计" class="headerlink" title="系统设计"></a>系统设计</h1><h2 id="1-设计思路"><a href="#1-设计思路" class="headerlink" title="1 设计思路"></a>1 设计思路</h2><p>将调度行为抽象形成“调度中心”公共平台,而平台自身并不承担业务逻辑,“<strong>调度中心</strong>”负责发起调度请求;<br>将任务抽象成分散的JobHandler,交由“执行器”统一管理,“<strong>执行器</strong>”负责接收调度请求并执行对应的JobHandler中业务逻辑;<br>因此,“调度”和“任务”两部分可以相互解耦,提高系统整体稳定性和扩展性;</p><h2 id="2-系统组成"><a href="#2-系统组成" class="headerlink" title="2 系统组成"></a>2 系统组成</h2><ul><li><p><strong>调度模块(调度中心)</strong>: 负责管理调度信息,按照调度配置发出调度请求,自身不承担业务代码。调度系统与任务解耦,提高了系统可用性和稳定性,同时调度系统性能不再受限于任务模块; 支持可视化、简单且动态的管理调度信息,包括任务新建,更新,删除,任务报警等,所有上述操作都会实时生效,同时支持监控调度结果以及执行日志,支持执行器Failover</p></li><li><p><strong>执行模块(执行器)</strong>: 负责接收调度请求并执行任务逻辑。任务模块专注于任务的执行等操作,开发和维护更加简单和高效; 接收“调度中心”的执行请求、终止请求和日志请求等</p></li></ul><p><img src="/images/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E5%B9%B3%E5%8F%B0XXL-JOB/1.png" alt="功能架构"></p><h2 id="3-工作原理"><a href="#3-工作原理" class="headerlink" title="3 工作原理"></a>3 工作原理</h2><p><img src="/images/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E5%B9%B3%E5%8F%B0XXL-JOB/2.png" alt="XXL-JOB任务执行流程"></p><ul><li>任务执行器根据配置的调度中心的地址,自动注册到调度中心</li><li>达到任务触发条件,调度中心下发任务</li><li>执行器基于线程池执行任务,并把执行结果放入内存队列中、把执行日志写入日志文件中</li><li>执行器的回调线程消费内存队列中的执行结果,主动上报给调度中心</li><li>当用户在调度中心查看任务日志,调度中心请求任务执行器,任务执行器读取任务日志文件并返回日志详情</li></ul><h2 id="4-HA设计"><a href="#4-HA设计" class="headerlink" title="4 HA设计"></a>4 HA设计</h2><h3 id="4-1-调度中心高可用"><a href="#4-1-调度中心高可用" class="headerlink" title="4.1 调度中心高可用"></a>4.1 调度中心高可用</h3><p>调度中心支持多节点部署,基于数据库行锁保证同时只有一个调度中心节点触发任务调度,参考com.xxl.job.admin.core.thread.JobScheduleHelper#start</p><pre class="line-numbers language-java"><code class="language-java">Connection conn <span class="token operator">=</span> XxlJobAdminConfig<span class="token punctuation">.</span><span class="token function">getAdminConfig</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getDataSource</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getConnection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>connAutoCommit <span class="token operator">=</span> conn<span class="token punctuation">.</span><span class="token function">getAutoCommit</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>conn<span class="token punctuation">.</span><span class="token function">setAutoCommit</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">;</span>preparedStatement <span class="token operator">=</span> conn<span class="token punctuation">.</span><span class="token function">prepareStatement</span><span class="token punctuation">(</span> <span class="token string">"select * from xxl_job_lock where lock_name = 'schedule_lock' for update"</span> <span class="token punctuation">)</span><span class="token punctuation">;</span>preparedStatement<span class="token punctuation">.</span><span class="token function">execute</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span># 触发任务调度# 事务提交 conn<span class="token punctuation">.</span><span class="token function">commit</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h3 id="4-2-任务调度高可用"><a href="#4-2-任务调度高可用" class="headerlink" title="4.2 任务调度高可用"></a>4.2 任务调度高可用</h3><ul><li><p>路由策略<br>调度中心基于路由策略路由选择一个执行器节点执行任务,XXL-JOB提供了如下路由策略保证任务调度高可用:</p></li><li><p><em>忙碌转移策略*</em>: 下发任务前向执行器节点发起rpc心跳请求查询是否忙碌,如果执行器节点返回忙碌则转移到其他执行器节点执行(参考 com.xxl.job.admin.core.route.strategy.ExecutorRouteBusyover)</p></li><li><p><em>故障转移策略*</em>: 下发任务前向执行器节点发起rpc心跳请求查询是否在线,如果执行器节点没返回或者返回不可用则转移到其他执行器节点执行 (参考com.xxl.job.admin.core.route.strategy.ExecutorRouteFailover)</p></li><li><p>阻塞处理策略<br>当执行器节点存在多个相同任务id的任务未执行完成,则需要基于阻塞策略对任务进行取舍:</p></li><li><p><em>串行策略*</em>:默认策略,任务进行排队、<strong>丢弃旧任务策略</strong>、<strong>丢弃新任务策略</strong><br>(参考:com.xxl.job.core.biz.impl.ExecutorBizImpl#run)</p></li></ul><h1 id="同类框架比较"><a href="#同类框架比较" class="headerlink" title="同类框架比较"></a>同类框架比较</h1><table><thead><tr><th align="left">特性</th><th align="left">quartz</th><th align="left">elastic-job-lite</th><th align="left">xxl-job</th><th align="left">LTS</th></tr></thead><tbody><tr><td align="left">依赖</td><td align="left">MySQL、jdk</td><td align="left">jdk、zookeeper</td><td align="left">mysql、jdk</td><td align="left">jdk、zookeeper、maven</td></tr><tr><td align="left">高可用</td><td align="left">多节点部署,通过竞争数据库锁来保证只有一个节点执行任务</td><td align="left">通过zookeeper的注册与发现,可以动态的添加服务器</td><td align="left">基于竞争数据库锁保证只有一个节点执行任务,支持水平扩容。可以手动增加定时任务,启动和暂停任务,有监控</td><td align="left">集群部署,可以动态的添加服务器。可以手动增加定时任务,启动和暂停任务。有监控</td></tr><tr><td align="left">任务分片</td><td align="left">×</td><td align="left">√</td><td align="left">√</td><td align="left">√</td></tr><tr><td align="left">管理界面</td><td align="left">×</td><td align="left">√</td><td align="left">√</td><td align="left">√</td></tr><tr><td align="left">难易程度</td><td align="left">简单</td><td align="left">简单</td><td align="left">简单</td><td align="left">略复杂</td></tr><tr><td align="left">高级功能</td><td align="left">-</td><td align="left">弹性扩容,多种作业模式,失效转移,运行状态收集,多线程处理数据,幂等性,容错处理,spring命名空间支持</td><td align="left">弹性扩容,分片广播,故障转移,Rolling实时日志,GLUE(支持在线编辑代码,免发布),任务进度监控,任务依赖,数据加密,邮件报警,运行报表,国际化</td><td align="left">支持spring,spring boot,业务日志记录器,SPI扩展支持,故障转移,节点监控,多样化任务执行结果支持,FailStore容错,动态扩容。</td></tr><tr><td align="left">版本更新</td><td align="left">半年没更新</td><td align="left">2年没更新</td><td align="left">最近有更新</td><td align="left">1年没更新</td></tr></tbody></table><h1 id="使用"><a href="#使用" class="headerlink" title="使用"></a>使用</h1><h2 id="快速上手"><a href="#快速上手" class="headerlink" title="快速上手"></a>快速上手</h2><p>具体如何快速上手使用,官方文档:<a href="http://www.xuxueli.com/xxl-job/" target="_blank" rel="noopener">http://www.xuxueli.com/xxl-job/</a> 已经介绍得比较详细和清楚,不再赘述</p><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ul><li><p><strong>1 时钟同步问题</strong><br>调度中心和任务执行器需要时间同步,同步时间误差需要在3分钟内,否则抛出异常<br>参考:com.xxl.rpc.remoting.provider.XxlRpcProviderFactory#invokeService</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">if</span> <span class="token punctuation">(</span>System<span class="token punctuation">.</span><span class="token function">currentTimeMillis</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> xxlRpcRequest<span class="token punctuation">.</span><span class="token function">getCreateMillisTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">></span> <span class="token number">3</span><span class="token operator">*</span><span class="token number">60</span><span class="token operator">*</span><span class="token number">1000</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> xxlRpcResponse<span class="token punctuation">.</span><span class="token function">setErrorMsg</span><span class="token punctuation">(</span><span class="token string">"The timestamp difference between admin and executor exceeds the limit."</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> xxlRpcResponse<span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></li><li><p><strong>2 时区问题</strong><br>任务由调度中心触发,按照在调度中心设置任务的cron表达式触发时,需要注意部署调度中心的机器所在的时区,按照该时区定制化cron表达式</p></li><li><p><strong>3 任务执行中服务宕掉问题</strong><br>调度中心完成任务下发,执行器在执行任务的过程中,如果执行器突然服务宕掉,会导致任务的执行问题在调度中心是执行中,调度中心并不会发起失败重试。即使任务设置了超时时间,执行器宕掉导致导致任务长时间未执行完成,调度中心界面也不会看到任务超时,因为任务超时是由执行器检测的并上报给调度中心的</p></li></ul><p>因此遇到任务长时间未执行完成,可以关注是否发生了执行器突然服务宕掉</p><ul><li><p><strong>4 优雅停机问题</strong><br>执行器执行任务基于线程池异步执行,当需要重启时需要注意线程池中还有未执行完成任务的问题,需要优雅停机,可以直接基于XxlJobExecutor.destroy()优雅停机,注意该方法在v2.0.2之前的版本存在bug导致无法优雅停机,v2.0.2及之后的版本才修复(参考:<a href="https://github.com/xuxueli/xxl-job/issues/727" target="_blank" rel="noopener">https://github.com/xuxueli/xxl-job/issues/727</a>)</p></li><li><p><strong>5 失败重试问题</strong><br>当执行器节点部分服务不可用,例如节点磁盘损坏,但在调度中心仍然处于在线时,调度中心仍可能基于路由策略(包括故障转移策略)路由到该未下线的节点,并不断重试,不断失败,导致重试次数耗尽。所以路由策略尽量不要采用固定化策略,例如固定第一个、固定最后一个</p></li></ul><h1 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h1><p>XXL-JOB上手还是比较简单,项目源码还是比较整洁,容易读懂,学习之后可以更加深入理解分布式系统设计、网络通信、多线程协同处理等知识点,推荐阅读</p><h3 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h3><p><a href="https://github.com/xuxueli/xxl-job" target="_blank" rel="noopener">XXL-JOB github仓库</a><br><a href="http://www.xuxueli.com/xxl-job/#/" target="_blank" rel="noopener">XXL-JOB 官方文档</a></p><p><img src="/images/%E5%88%86%E5%B8%83%E5%BC%8F%E4%BB%BB%E5%8A%A1%E8%B0%83%E5%BA%A6%E5%B9%B3%E5%8F%B0XXL-JOB/3.png" alt></p>]]></content>
<categories>
<category> 技术框架 </category>
</categories>
</entry>
<entry>
<title>自研IM系统方案设计</title>
<link href="/2019/10/13/zi-yan-im-xi-tong-fang-an-she-ji/"/>
<url>/2019/10/13/zi-yan-im-xi-tong-fang-an-she-ji/</url>
<content type="html"><![CDATA[<p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/0.jpg" alt></p><p>本文主要介绍APP功能中的IM模块的设计方案</p><h1 id="1-设计原则"><a href="#1-设计原则" class="headerlink" title="1 设计原则"></a>1 设计原则</h1><ul><li>合适原则——合适优于业界领先</li><li>简单原则——简单优于复杂</li><li>演化原则——演化优于一步到位</li></ul><p>架构设计的目的在于解决系统复杂度问题,<strong>真正优秀的架构都是企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效、并且能够快速落地。</strong></p><h1 id="2-系统复杂度分析"><a href="#2-系统复杂度分析" class="headerlink" title="2 系统复杂度分析"></a>2 系统复杂度分析</h1><h2 id="2-1-优先重点考虑"><a href="#2-1-优先重点考虑" class="headerlink" title="2.1 优先重点考虑"></a>2.1 优先重点考虑</h2><ul><li>高性能<br>消息尽量低延迟</li><li>成本<br>人力,服务器资源有限,需要做到相比之前使用付费环信更节约成本</li></ul><h2 id="2-2-相对重点考虑"><a href="#2-2-相对重点考虑" class="headerlink" title="2.2 相对重点考虑"></a>2.2 相对重点考虑</h2><ul><li>高可用<br>因为IM不是非常关键功能,只是APP的附属功能,目前阶段可暂时不考虑故障自动恢复,先采用故障手工恢复</li><li>可扩展性<br>功能相对固定,后期功能扩展较少</li><li>规模<br>使用该功能的用户规模短期不会爆炸增长</li></ul><h2 id="2-3-其他"><a href="#2-3-其他" class="headerlink" title="2.3 其他"></a>2.3 其他</h2><ul><li>旧APP版本兼容<br>旧版本基于环信SDK,需要支持有环信和无环信用户互聊</li><li>消息的有序性</li><li>消息已读未读状态更新</li></ul><h1 id="3-设计备选方案"><a href="#3-设计备选方案" class="headerlink" title="3 设计备选方案"></a>3 设计备选方案</h1><h2 id="3-1-通信方案"><a href="#3-1-通信方案" class="headerlink" title="3.1 通信方案"></a>3.1 通信方案</h2><ul><li>方案1:个推+http接口方案<ul><li>方案图</li></ul></li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/1.png" alt="个推+http接口方案"></p><ul><li><p>优点<br>技术成熟,基于现有技术,不用额外引入新技术</p></li><li><p>缺点<br>用户发消息频繁建立连接,服务器压力较大<br>消息时延较大,个推推送消息时延较大,存在丢消息的可能性</p></li></ul><ul><li>方案2:TCP长连接方案<ul><li>方案图</li></ul></li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/2.png" alt></p><ul><li><p>优点<br>消息实时性较好<br>基于单条长连接,消息乱序的可能性较小</p></li><li><p>缺点<br>客户端和服务端需要学习使用新的技术方案维持长连接<br>增加新的机器成本</p></li></ul><h2 id="3-2-存储方案"><a href="#3-2-存储方案" class="headerlink" title="3.2 存储方案"></a>3.2 存储方案</h2><p>方案1: 聊天记录服务端存储</p><ul><li>优点<br>支持聊天记录漫游</li><li>缺点<br>增加服务器成本<br>增加服务端开发成本<br>增加客户端,服务端接口对接</li></ul><p>方案2: 聊天记录客户端存储</p><ul><li>优点<br>节省存储成本</li><li>缺点<br>聊天记录不支持漫游,增加客户端开发成本</li></ul><p>具体存储设计如下:<br><a href="https://www.jianshu.com/p/b3287b1ce3f9" target="_blank" rel="noopener">自研IM系统存储设计</a></p><h1 id="4-方案评估和选择"><a href="#4-方案评估和选择" class="headerlink" title="4 方案评估和选择"></a>4 方案评估和选择</h1><p>基于成本和快速开发的考虑,选择个推+http接口方案,聊天消息保存服务端</p><h1 id="5-关键难点解决方案"><a href="#5-关键难点解决方案" class="headerlink" title="5 关键难点解决方案"></a>5 关键难点解决方案</h1><p>字段名定义<br>clientMsgId 消息的客户端UUID,不重复,由客户端生成<br>serverMsgId 消息的服务端ID,不重复,由服务端递增规则生成,<br>msgIsResend 消息是否是重发消息,1-是,0-否</p><p>hasMsgToReceive 是否有消息待接收,1-是,0-否<br>msgAccept 消息是否成功被服务端接收,1-是,0-否</p><h2 id="5-1-APP新旧版本兼容"><a href="#5-1-APP新旧版本兼容" class="headerlink" title="5.1 APP新旧版本兼容"></a>5.1 APP新旧版本兼容</h2><ul><li><p>问题<br>旧版本APP使用环信通信,新版本使用自研IM通信,需要做到兼容使得新旧版本客户端可以互聊</p></li><li><p>解决<br>新版APP做兼容,可以兼容收环信消息,发环信消息,根据新版本发消息时,根据接收方用户的版本号,判断发环信消息,还是发自研IM消息</p></li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/3.png" alt="新旧版本APP兼容方案"></p><h2 id="5-2-发送消息与接收到的消息乱序问题"><a href="#5-2-发送消息与接收到的消息乱序问题" class="headerlink" title="5.2 发送消息与接收到的消息乱序问题"></a>5.2 发送消息与接收到的消息乱序问题</h2><ul><li><p>问题<br>用户发送消息的前,有可能有其他旧的未读消息未到达,<br>消息发出去后,未读消息才到达,导致聊天界面展示消息乱序。</p></li><li><p>解决<br>采用客户端存储聊天记录的方案,发送消息的同时获取未读消息,再一并展示给用户看</p></li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/4.png" alt></p><h2 id="5-3-消息发送传输失败重发问题"><a href="#5-3-消息发送传输失败重发问题" class="headerlink" title="5.3 消息发送传输失败重发问题"></a>5.3 消息发送传输失败重发问题</h2><ul><li><p>问题<br>消息已经发送给服务端,服务端处理成功并返回,但是因为网络问题返回失败,客户端重发消息,导致服务端消息重发</p></li><li><p>解决<br>服务端基于客户端上报的消息的clientMsgId,msgIsResend,进行消息重复校验,如果消息已发的是重复消息,不再重复发送</p></li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/5.png" alt></p><h2 id="5-4-发消息时拉取消息消息包过大问题"><a href="#5-4-发消息时拉取消息消息包过大问题" class="headerlink" title="5.4 发消息时拉取消息消息包过大问题"></a>5.4 发消息时拉取消息消息包过大问题</h2><ul><li>问题<br>客户端在发消息时服务端一并返回未收消息,假定每次限定最多返回1000条消息,当服务端积压超过1000条消息,消息无法一次返回</li></ul><ul><li>解决</li></ul><p><strong>客户端获取消息的有序性</strong><br>在一个聊天对话中,客户端本地已有消息的时间戳(服务端创建),总是处于服务端的所有待收消息的时间戳的过去时间。</p><p>下图只有A情况满足有序性条件</p><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/6.png" alt><br>实际使用上,积压大量消息未接收发生的概率极小,初步解决如下</p><ul><li>服务端返回消息报文中带一个字段,hasMsgToReceive,代表是否有消息待接收</li><li>当未收过多未能一次返回,需要客户端再请求一次才能全部返回,hasMsgToReceive=1,msgAccept =0</li><li>已经全部返回,hasMsgToReceive=0,msgAccept =1</li><li>为了保证客户端获取消息的有序性,当服务端消息未能正常一次返回,服务端不发送本条消息,需要客户端再次重发消息</li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/7.png" alt></p><h2 id="5-5-拉取未读消息传输失败问题"><a href="#5-5-拉取未读消息传输失败问题" class="headerlink" title="5.5 拉取未读消息传输失败问题"></a>5.5 拉取未读消息传输失败问题</h2><ul><li><p>问题<br>APP拉取未读消息时,服务端成功处理并返回消息,在消息网络传输到达到达APP之前,网络中断,消息传输失败,导致丢消息。</p></li><li><p>解决<br>拉取消息时请求参数待最新已收消息ID(serverMsgId),服务端根据这个ID获取未读消息和删除历史已读消息</p></li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/8.png" alt="image.png"></p><h1 id="6-其他待解决问题"><a href="#6-其他待解决问题" class="headerlink" title="6 其他待解决问题"></a>6 其他待解决问题</h1><h2 id="6-1-敏感内容,信息过滤监控"><a href="#6-1-敏感内容,信息过滤监控" class="headerlink" title="6.1 敏感内容,信息过滤监控"></a>6.1 敏感内容,信息过滤监控</h2><p>目前阶段暂时不做</p><h2 id="6-2-消息“已读”、“未读”的标记展示"><a href="#6-2-消息“已读”、“未读”的标记展示" class="headerlink" title="6.2 消息“已读”、“未读”的标记展示"></a>6.2 消息“已读”、“未读”的标记展示</h2><p>目前阶段暂时不做</p><h2 id="6-3-客户端删除聊天消息"><a href="#6-3-客户端删除聊天消息" class="headerlink" title="6.3 客户端删除聊天消息"></a>6.3 客户端删除聊天消息</h2><p>目前阶段暂时不做</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://github.com/a2888409/face2face" target="_blank" rel="noopener">基于netty的异步非阻塞实时聊天(IM)服务器——github</a></p><p><a href="https://github.com/davideuler/architecture.of.internet-product/blob/master/B.%E5%9F%BA%E7%A1%80%E6%9E%B6%E6%9E%84-%E5%8F%8A%E6%97%B6%E9%80%9A%E8%AE%AF-%E8%AF%AD%E9%9F%B3-%E8%A7%86%E9%A2%91/%E5%8D%B3%E6%97%B6%E9%80%9A%E8%AE%AF%E6%9E%B6%E6%9E%84-20140718-lizhiwei.pdf" target="_blank" rel="noopener">陌陌IM通信架构</a></p><p><a href="https://github.com/davideuler/architecture.of.internet-product/blob/master/B.%E5%9F%BA%E7%A1%80%E6%9E%B6%E6%9E%84-%E5%8F%8A%E6%97%B6%E9%80%9A%E8%AE%AF-%E8%AF%AD%E9%9F%B3-%E8%A7%86%E9%A2%91/QCon%E4%B8%8A%E6%B5%B72015-IM%E9%80%9A%E8%AE%AF%E4%BA%91%E6%8A%80%E6%9C%AF%E8%B7%AF%E7%BA%BF%E7%9A%84%E9%80%89%E6%8B%A9-%E8%AE%B8%E5%BF%97%E5%BC%BA.pdf" target="_blank" rel="noopener">IM通讯云技术路线的选择</a></p><p><a href="https://github.com/davideuler/architecture.of.internet-product/blob/master/7.%E7%BD%91%E6%98%93.%E6%8A%80%E6%9C%AF%E6%9E%B6%E6%9E%84/%E7%BD%91%E6%98%93IM%E4%BA%91%E5%8D%83%E4%B8%87%E7%BA%A7%E5%B9%B6%E5%8F%91%E6%B6%88%E6%81%AF%E5%A4%84%E7%90%86%E8%83%BD%E5%8A%9B%E7%9A%84%E6%9E%B6%E6%9E%84%E8%AE%BE%E8%AE%A1%E4%B8%8E%E5%AE%9E%E8%B7%B5.pdf" target="_blank" rel="noopener">网易IM云千万级并发消息处理能力的架构设计与实践</a></p><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/9.png" alt></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>自研IM系统存储设计</title>
<link href="/2019/10/13/zi-yan-im-xi-tong-cun-chu-she-ji/"/>
<url>/2019/10/13/zi-yan-im-xi-tong-cun-chu-she-ji/</url>
<content type="html"><![CDATA[<p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E5%AD%98%E5%82%A8%E8%AE%BE%E8%AE%A1/0.jpg" alt></p><h1 id="1-数据操作需求"><a href="#1-数据操作需求" class="headerlink" title="1 数据操作需求"></a>1 数据操作需求</h1><h2 id="1-1-发消息"><a href="#1-1-发消息" class="headerlink" title="1.1 发消息"></a>1.1 发消息</h2><ul><li>发送方新增已发消息 (用于消息判重)</li><li>接收方新增待收消息</li><li>根据发送方用户ID查询最近100条已发消息(用于消息判重)</li><li>消息持久化存储</li></ul><h2 id="1-2-收消息"><a href="#1-2-收消息" class="headerlink" title="1.2 收消息"></a>1.2 收消息</h2><ul><li>根据接收方用户ID,最新已收消息ID,查询未收消息,支持分页查询,每次取1000条</li></ul><h2 id="1-3-删除数据"><a href="#1-3-删除数据" class="headerlink" title="1.3 删除数据"></a>1.3 删除数据</h2><ul><li>删除历史持久化存储数据</li><li>删除历史已发消息,接收消息</li></ul><h1 id="2-存储设计"><a href="#2-存储设计" class="headerlink" title="2 存储设计"></a>2 存储设计</h1><h2 id="基本存储结构"><a href="#基本存储结构" class="headerlink" title="基本存储结构"></a>基本存储结构</h2><p>消息缓存存储使用redis,结构设计如下</p><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E5%AD%98%E5%82%A8%E8%AE%BE%E8%AE%A1/1.png" alt></p><p>每个用户关联redis中的一个List和一个Sorted Set(有序Set):</p><ul><li>List中只保留用户最近发送的100条客户端消息ID(UUID,由客户端生成)</li><li>Sorted Set中保留用户接收的消息,其中Set的score是服务端消息ID(long型,由服务端生成,递增);Set的member是消息对象,包括发送方,消息内容,时间戳,消息ID等</li></ul><h2 id="发送消息操作"><a href="#发送消息操作" class="headerlink" title="发送消息操作"></a>发送消息操作</h2><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E5%AD%98%E5%82%A8%E8%AE%BE%E8%AE%A1/2.png" alt><br>主要步骤如下:</p><ul><li>步骤1 接收用户APP发送的消息报文,包括客户端消息ID</li><li>步骤2 保存用户已发客户端消息ID,保存到用户已发消息List中</li><li>步骤3 保存消息,保存到目标接收用户消息的Sorted Set中</li><li>步骤4 消息持久化存储数据库</li><li>步骤5 返回已发消息的服务端消息ID</li></ul><p>实际情况中,有可能消息是重发消息,这时需要在步骤2之前查询用户已发消息List,与客户端消息ID比对判重</p><h2 id="拉取消息操作"><a href="#拉取消息操作" class="headerlink" title="拉取消息操作"></a>拉取消息操作</h2><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E5%AD%98%E5%82%A8%E8%AE%BE%E8%AE%A1/3.png" alt></p><p>在APP发送消息接口,拉取未收消息接口都会拉取消息,主要步骤如下:</p><ul><li>步骤1 用户APP基于本地已有最新服务端消息ID拉取消息</li><li>步骤2 服务器基于用户接收消息的Sorted Set分页查询未收消息</li><li>步骤3 返回消息 </li></ul><h2 id="过期无效数据清理"><a href="#过期无效数据清理" class="headerlink" title="过期无效数据清理"></a>过期无效数据清理</h2><ul><li><p>用户已发消息List<br>限定List大小为100,超过100清除最旧的数据</p></li><li><p>用户接收消息的Sorted Set<br>限定消息保存一个月,使用定时任务定时删除</p></li><li><p>持久化数据库<br>限定消息保存一个月,使用定时任务定时删除</p></li></ul><h1 id="3-后续扩展"><a href="#3-后续扩展" class="headerlink" title="3 后续扩展"></a>3 后续扩展</h1><ul><li>发送消息接口新增过滤器,用于过滤、监控、拒绝敏感消息</li></ul><p><img src="/images/%E8%87%AA%E7%A0%94IM%E7%B3%BB%E7%BB%9F%E5%AD%98%E5%82%A8%E8%AE%BE%E8%AE%A1/4.png" alt></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>Jenkins搭建集成SonarQube最简实践</title>
<link href="/2019/10/13/jenkins-da-jian-ji-cheng-sonarqube-zui-jian-shi-jian/"/>
<url>/2019/10/13/jenkins-da-jian-ji-cheng-sonarqube-zui-jian-shi-jian/</url>
<content type="html"><![CDATA[<p><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/0.jpg" alt></p><p>本文介绍在Linux环境下Jenkins如何整合SonarQube</p><h1 id="环境准备"><a href="#环境准备" class="headerlink" title="环境准备"></a>环境准备</h1><ul><li>JDK环境<br>JDK1.8</li><li>代码托管<br>Gitlab</li><li>审查工具<br>SonarQube</li><li>发布容器<br>Tomcat</li><li>构建工具<br>Maven</li><li>数据库<br>MySQL</li></ul><h1 id="系统配置要求"><a href="#系统配置要求" class="headerlink" title="系统配置要求"></a>系统配置要求</h1><ul><li><p>OS内核需要高于Linux5.3</p></li><li><p>推荐运行内存为8G左右,至少需要大于4G</p></li><li><p>需要分配额外的用户和用户组来运行代码审查工具</p></li><li><p>若需持久化代码审查记录,需要提供一个数据库(MySQL,H2,postgresql等),数据库的安装过程在此跳过</p></li></ul><h1 id="具体步骤"><a href="#具体步骤" class="headerlink" title="具体步骤"></a>具体步骤</h1><ul><li><p>步骤1 安装Jenkins和SonarQube基本环境<br>可以参考<a href="https://blog.csdn.net/u011230736/article/details/79439015" target="_blank" rel="noopener">文章</a></p></li><li><p>步骤2 在系统中安装好JDK环境和Maven环境</p></li><li><p>步骤3 Jenkins配置连接Gitlab</p><ul><li>3.1 Jenkins页面,系统管理->管理插件,安装“GitLab”和“Git client” 2个插件<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/1.png" alt> * 3.2 Jenkins页面,系统管理->系统设置,配置Gitlab<br>其中token在Gitlab中生成<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/2.png" alt></li></ul></li><li><p>步骤4 配置maven</p><ul><li>4.1 配置安装目录下的conf/setting.xml文件配置<br>配置项如下:</li></ul></li></ul><pre><code> <profile> <id>sonar</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <!-- 平台登录的账号的用户名 --> <sonar.login>你的用户名</sonar.login> <!-- SonarQube平台登录的账号的密码 --> <sonar.password>你的密码</sonar.password> <!-- SonarQube访问地址 --> <sonar.host.url>http://sonar.ibeiliao.net:9000</sonar.host.url> <!-- 代码分析包括哪些文件需要分析,英文逗号分隔 --> <sonar.inclusions>**/*.java,**/*.xml</sonar.inclusions> </properties> </profile></code></pre><p>并使用 <activeprofile>sonar</activeprofile> 激活profile</p><p><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/3.png" alt> * 4.2 配置Jenkins集成Maven<br>Jenkins页面,系统管理->全局工具配置,配置好Maven<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/4.png" alt></p><ul><li>步骤5 Jenkins创建每日构建项目<ul><li>新建任务,输入任务名,选择“构建一个自由风格的软件项目”<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/5.png" alt></li><li>选择gitlab<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/6.png" alt> * 填写代码仓库、分支信息<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/7.png" alt> * 配置构建触发器<br>H 16 * * 1,4 代表每日16前构建,每周一,周日构建<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/8.png" alt> * 基于Maven配置代码扫描<br>clean org.jacoco:jacoco-maven-plugin:prepare-agent install -Dmaven.test.failure.ignore=true -Pdev<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/9.png" alt> * 配置构建后构建失败发邮件<br><img src="/images/Jenkins%E6%90%AD%E5%BB%BA%E9%9B%86%E6%88%90SonarQube%E6%9C%80%E7%AE%80%E5%AE%9E%E8%B7%B5/10.png" alt> * 保存</li></ul></li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://blog.csdn.net/u011230736/article/details/79439015" target="_blank" rel="noopener">持续集成平台搭建:Jenkins,SonarQube</a></p>]]></content>
<categories>
<category> 运维监控 </category>
</categories>
</entry>
<entry>
<title>基于Java内存dump文件分析解决内存泄漏问题</title>
<link href="/2019/10/13/ji-yu-java-nei-cun-dump-wen-jian-fen-xi-jie-jue-nei-cun-xie-lou-wen-ti/"/>
<url>/2019/10/13/ji-yu-java-nei-cun-dump-wen-jian-fen-xi-jie-jue-nei-cun-xie-lou-wen-ti/</url>
<content type="html"><![CDATA[<p><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/0.jpg" alt></p><p>* <em>本文介绍一次解决现场*</em>java内存泄漏问题**的经过,希望能提供后续遇到类似情况的读者一点思路。 </p><h1 id="生产环境发现的问题问题"><a href="#生产环境发现的问题问题" class="headerlink" title="生产环境发现的问题问题"></a>生产环境发现的问题问题</h1><p>* *生产环境运维人员反馈,服务器(windows系统)卡死,相关的服务都运行异常,重启之后也没作用。通过运行管理器看到java进程内存占用5G,初步判断是程序内存泄漏。</p><h1 id="基本解决方案"><a href="#基本解决方案" class="headerlink" title="基本解决方案"></a>基本解决方案</h1><p>* *基本解决方案是先收集生产环境的jvm内存使用信息,线程信息,再利用工具进行进一步分析。</p><h1 id="解决过程"><a href="#解决过程" class="headerlink" title="解决过程"></a>解决过程</h1><h4 id="1、收集jvm内存信息,线程信息"><a href="#1、收集jvm内存信息,线程信息" class="headerlink" title="1、收集jvm内存信息,线程信息"></a>1、收集jvm内存信息,线程信息</h4><p>生产环境的操作系统是windows,机器需要先设置好JAVA_HOME环境变量。下面以java进程PID为12140,输出文件路径保存在C:\jvmtest 文件夹中为例。</p><h2 id="1-1、收集内存使用基本情况统计"><a href="#1-1、收集内存使用基本情况统计" class="headerlink" title="1.1、收集内存使用基本情况统计"></a>1.1、收集内存使用基本情况统计</h2><p><strong>使用命令行命令:jmap -heap 12140 > C:\jvmtest\jmapheap</strong><br>直接打开查看C:\jvmtest文件夹下面的jampheap文件,里面包含内存使用情况基本统计,可以确认问题原因不是jvm参数内存分配过小。<br>可以看到java堆内存基本已经用完。<br><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/1.png" alt="内存使用情况基本统计.png"></p><h2 id="1-2、收集所有java线程运行信息"><a href="#1-2、收集所有java线程运行信息" class="headerlink" title="1.2、收集所有java线程运行信息"></a>1.2、收集所有java线程运行信息</h2><p><strong>使用命令行命令:jstack 12140 > C:\jvmtest\jstack</strong></p><p>直接打开查看C:\jvmtest文件夹下面的jstack文件,里面包含所有java线程运行信息:<br><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/2.png" alt="所有java线程运行信息"></p><h2 id="1-3、收集java内存详细使用信息"><a href="#1-3、收集java内存详细使用信息" class="headerlink" title="1.3、收集java内存详细使用信息"></a>1.3、收集java内存详细使用信息</h2><p><strong>使用命令行命令:jmap -dump:format=b,file=C:\jvmtest\jmap_dump_all 12140</strong><br>得到C:\jvmtest文件夹下面的jmap_dump_all文件,该内存dump文件有5G大小,二进制文件,不可直接查看,需要用工具查看。</p><h2 id="2、基于工具分析"><a href="#2、基于工具分析" class="headerlink" title="2、基于工具分析"></a>2、基于工具分析</h2><p>#####2.1、工具选择<br>如果dump文件比较小,推荐直接使用jdk自带的jvisiualvm工具进行打开,但是如果dump文件比较大,亲测jvisiualvm打开失败,这时推荐选择eclipse的内存分析工具:<a href="http://www.eclipse.org/mat/" target="_blank" rel="noopener">eclipse memory analyer(mat)</a></p><h2 id="2-2、eclipse-memory-analye软件配置"><a href="#2-2、eclipse-memory-analye软件配置" class="headerlink" title="2.2、eclipse memory analye软件配置"></a>2.2、eclipse memory analye软件配置</h2><p>这里选择从eclipse官网下载MemoryAnalyzer-1.7.0.20170613-win32.win32.x86_64.zip文件(也可以下载eclipse插件),由于dump文件比较大,打开分析工具前需要修改eclipse memory analyer的内存配置:</p><p><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/3.png" alt="配置.png"><br><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/4.png" alt="配置.png"><br>最后一行修改为需要的内存大小</p><p>如果dump文件比较大的情况下,如果分析工具运行的环境机器内存太小是打不开的,机器可用内存至少要比dump文件大。</p><h2 id="2-2、查看分析情况"><a href="#2-2、查看分析情况" class="headerlink" title="2.2、查看分析情况"></a>2.2、查看分析情况</h2><p>打开eclipse memory analye软件,载入dump文件,看到以下信息:</p><p><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/5.png" alt="overlook.png"><br><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/6.png" alt="线程内存使用情况.png"></p><h2 id="2-3、问题定位"><a href="#2-3、问题定位" class="headerlink" title="2.3、问题定位"></a>2.3、问题定位</h2><p>基于eclipse memory analye软件,可用定位到有1条线程名为pool-4-thread-1的线程,里面有一个ArrayList的对象,这个list对象保存了一系列的HashMap对象,总共有4G。</p><p>搜索 <strong>1.2步骤</strong>介绍的所有java线程信息的文件,可以得到pool-4-thread-1线程运行状态如下:基于此可以定位到有问题的代码。</p><p><img src="/images/%E5%9F%BA%E4%BA%8EJava%E5%86%85%E5%AD%98dump%E6%96%87%E4%BB%B6%E5%88%86%E6%9E%90%E8%A7%A3%E5%86%B3%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F%E9%97%AE%E9%A2%98/7.png" alt="image.png"><br>#####2.4、问题原因定位<br>检查代码的时候发现,程序有一个模块,功能是从<strong>数据库定时查询数据</strong>然后数据做处理,模块中把查出来的数据基于<strong>log4j写到日志</strong>中,实际现场环境有时候<strong>定时查询</strong>得到的数据有几百兆,打印到日志文件中打印不过来。导致数据在内存中不断积压等待被打印,内存得不到释放。</p><p>###总结<br>本次使用了JVM性能调优监控工具jstack、jamp,相关工具还有jstack、jmap、jhat、jstat,这些工具对于内存溢出,CPU飙升,线程死锁、等问题解决非常有帮助。</p>]]></content>
<categories>
<category> 运维监控 </category>
</categories>
</entry>
<entry>
<title>如何高效学习开源项目</title>
<link href="/2019/10/13/ru-he-gao-xiao-xue-xi-kai-yuan-xiang-mu/"/>
<url>/2019/10/13/ru-he-gao-xiao-xue-xi-kai-yuan-xiang-mu/</url>
<content type="html"><![CDATA[<p><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/0.jpg" alt></p><p>随着蓬勃发展的开源时代的到来,为了减少开发成本,提高开发效率,越来越多的公司使用各种开源项目,作为开发者,如果能充分利用好开源项目中的资源,不仅能提高实践能力,专业知识水平,还能从中其中学到的优秀的架构思想。</p><p>本文将提供一些学习开源项目的思路,相信看了这篇文章,小白也可学习读懂开源项目,不必再对着高大上的开源项目望而生畏,浅尝辄止。</p><h1 id="1-学习的价值"><a href="#1-学习的价值" class="headerlink" title="1 学习的价值"></a>1 学习的价值</h1><p>总结起来,学习开源项目的价值主要包括以下几点:</p><ul><li>专业水平的提升<br>很多通用的专业知识,在专业领域内去到哪个公司都能通用,特别是底层方面的知识,可以在开源项目中学到,比如多线程处理、网络通信、操作系统处理等。<br>举个例子,通过学习Redis 的 RDB 持久化模式的“会将当前内存中的数据库快照保存到磁盘文件中”,可以学习到其实就是在操作系统fork一个子进程来实现,再继续深入的话,就涉及到父子进程机制, copy-on-write 技术。</li></ul><p><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/1.jpg" alt></p><p>这些专业知识之间是可以联系起来的并且像一颗大树一样自我生长,但是当没理解透彻,自然没法产生联系,也就不能够自我生长了。当我们对开源项目的关键的点理解清晰,知识也随着自我生长,也就如滚雪球一样可以滚起来了。</p><ul><li>解决问题能力的提升</li></ul><p>通过学习开源项目的实现,出现线上问题时,可以快速定位问题症结所在,通过修改配置或者修改源代码来解决;或者当业务需求没有合适的开源项目能满足时,可以改造现有的开源项目来满足业务。<br>作为要优秀开发,避免陷入“<strong>API操作工</strong>”的被动局面,学习开源项目的一个很重要目的就是知道其功能点是如何实现且优化的,学习其中的知识好比公式的推导过程,掌握基本API使用好比会数学公式可以应付考试,但是理解好的推导过程根据有助于记忆和理解,<strong>知其然也要知其所以然</strong>,当遇那些没法套公式的情况下,我们也知道如何解决。</p><ul><li>思维的提升</li></ul><p>通过学习成熟的开源项目的优秀架构,可以总结和理解一些软件设计常用的架构思路,例如实现高可用,主要是通过集群的数据冗余,例如Kafka集群,HDSF集群;实现可扩展可以考虑把变化层和不变层隔离,把业务实现抽象化,例如Spring的预留的一些可扩展接口。</p><h1 id="2-常见错误观点"><a href="#2-常见错误观点" class="headerlink" title="2 常见错误观点"></a>2 常见错误观点</h1><p>学习开源项目有一些常见的错误观点,导致新手容易望而生畏而轻易放弃,或者浪费大量时间而收获不大:</p><pre><code>学习开源项目是架构师,技术大牛的事,我作为新手根本难以学会,就算学了也用不到。</code></pre><p>学习是一个过程,不是一朝一夕就可以成为大牛的,但是只要踏出第一步,总会有可能实现的大牛梦想的;另一方面,通过不断复盘不断总结,加以合适的方法论指导,相信是可以有所收获的,能力得到提升的。<br>学习之后对于逻辑思维,知识体系的构建有相信会有很大提升,即使项目没用到具体的开源项目,以后遇到相关问题可以触类旁通,举一反三,也是一种进步。</p><pre><code>数据结构和算法很重要,我只要学习这项目中的2方面就可以了</code></pre><p>不要只盯着数据结构和算法,这2点在开源项目中并没有那么重要,例如Netty中的超时队列是基于红黑树来实现的,我们其实只需要知道这一点就够了,除非需要改造这方面的功能。更重要的是理解系统的设计,功能的实现方案。</p><pre><code>一头扎进源码进行学习</code></pre><p>很多新手笃信社区论坛流行的一句话“Talk is cheap, show me the code”,一头扎进源码阅读,却最后陷入源码的泥潭中,在层层代码函数跳转中迷失了方向。</p><p>其实学习开源项目应该是<strong>自顶而下</strong>的,最底层的源码应该是最后才开始学习,在此之前,需要学习项目相关架构设计方面的知识,有了这些知识,就仿佛数据库有了索引,按照知识索引来进行源码针对性突破,如巡航导弹精准爆破,自然比地毯式轰炸更起到事半功倍的作用。</p><p><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/2.jpg" alt></p><h1 id="3-学习的4个层次"><a href="#3-学习的4个层次" class="headerlink" title="3 学习的4个层次"></a>3 学习的4个层次</h1><p>根据学习理解的深入程度不同,可以把学习分为4个层次<br><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/3.png" alt="学习的4个层次"></p><ul><li><p>基础学习<br>对项目有一个<strong>大概性、基础性</strong>的了解,比如<strong>项目是什么,有什么作用,大概怎么用,解决了什么问题</strong>。<br>在面试中,不少初入职场的人的简历写到用到众多的技术框架,实际上往往仅仅只到了这个层次,再深入往下问,便支支吾吾答不上来了。</p></li><li><p>检视学习<br>对项目有一个<strong>系统性</strong>的了解,系统的各方面功能,基本原理,优缺点,使用场景,各配置项、API使用。<br>在实际工作中,如果作为一个团队的普通成员,达到这个级别已经可以满足基本业务开发需求,但是如果想有更高的技术追求,仅仅到此是不够的。</p></li><li><p>分析学习<br>在检视学习的基础上,对开源项目的各项性能参数,各自场景性能调优有比较全面的了解和实践经验。<br>到达这个层次,在项目生产中,已经有独当一面的能力,有一定能力承担核心主力开发的角色。</p></li><li><p>主题学习<br>在分析学习的基础上,对开源项目的关键功能模块的源码有所了解,能够根据实际需要封装、修改源码,或者借鉴项目造出新的轮子。<br>到达这个层次,往往有一定能力承担技术负责人、技术带头人的角色。</p></li></ul><p><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/4.jpg" alt></p><h1 id="4-学习的4个步骤"><a href="#4-学习的4个步骤" class="headerlink" title="4 学习的4个步骤"></a>4 学习的4个步骤</h1><p>针对上面提到的学习的层次,下面介绍如何“自顶而下”学习,来达到这4个层次。</p><h2 id="4-1-基础性了解学习"><a href="#4-1-基础性了解学习" class="headerlink" title="4.1 基础性了解学习"></a>4.1 基础性了解学习</h2><p>目标是达到基础学习的层次,对项目有<strong>大概性</strong>的了解,包括项目背景,解决的问题场景,项目功能,使用场景,基本的API使用。通过查找官方文档、相关博客、视频资料学习即可。</p><p>通过对系统有大概性了解之后,会自然而然有一些疑问,例如实现的原理,优缺点等,后续学习带着这些疑问进行学习会更高效。</p><h2 id="4-2-系统性学习与实践"><a href="#4-2-系统性学习与实践" class="headerlink" title="4.2 系统性学习与实践"></a>4.2 系统性学习与实践</h2><p>目标是达到检视学习的层次,对项目有<strong>系统性、全面性</strong>的了解,包括项目的功能、组成模块、基本原理、使用场景、配置项、API使用、与其他类似项目的优缺点比较等。</p><p>方法步骤如下:</p><ul><li><p><strong>1 安装运行</strong><br>按照相关文档,安装运行项目。在这个过程中,需要关注:</p><ul><li><strong>系统的依赖组件</strong><br>因为依赖组件是系统设计和实现的基础,可以了解系统一下关键信息,例如 Memcached最重要的依赖是高性能的网络库 libevent,我们就能大概推测 Memcached 的网络实现应该是 Reactor 模型的。</li><li><strong>安装目录</strong><br>常见的安装目录是conf存放配置文件,logs存放日志文件,bin存放日志文件,而不同项目有些特殊目录,比如Nginx有html目录,这种目录能促使我们带着相关疑问继续去研究学习,带着问题去学习效率是最高的。</li><li><strong>系统提供的工具</strong><br>需要特别<strong>关注命令行和配置文件</strong>,它们提供2个非常重要的关键信息,系统具备哪些能力和系统将会如何运行。这些信息是我们学习系统内部机制和原理的一个观察窗口。<br>通常情况下,如果对每个命令行参数和配置项的作用和原理基本掌握了解的话,基本上对系统已经很熟悉了。实践中,可以不断尝试去修改配置项,然后观察系统有什么变化。</li></ul></li><li><p><strong>2 系统性研究原理与特性</strong><br>这点相当重要,因为只有清楚掌握技术的原理特性,才能算真正掌握这门技术,才能做架构设计的时候做出合理的选择,在这个过程中,需要重点关注:</p><ul><li><p>关键特性的基本实现原理<br>关键特性是该开源开源项目流行的重要卖点,常见的有高性能、高可用、可扩展等特性,项目是如何做到的,这是我们需要重点关注的地方。</p></li><li><p>优缺点比对分析<br>优缺点主要通过对比来分析,即:我们将两个类似的系统进行对比,看看它们的实现差异,以及不同的实现优缺点都是什么。典型的对比有 Memcached 和 Redis、Kafka和ActiveMQ、RocketMQ的比较。</p></li><li><p>使用场景<br>项目在哪些场景适用,哪些场景不适用,业界适用常见案例等。</p></li></ul></li></ul><p>在此阶段可以通过学习官方技术设计文档文档,架构图,原理图,或者相关技术博客,通常比较热门的开源项目都有很多分析文档,我们可以站在前人的基础上避免重复投入。但需要注意的是,<strong>由于经验、水平、关注点、使用的版本不同等差异,不同的人分析的结论可能有差异,甚至有的是错误的</strong>,因此不能完全参照。一个比较好的方式就是多方对照,也就是说看很多篇分析文档,比较它们的内容共同点和差异点。</p><p>同时,如果有些技术点难以查到资料,自己又不确定,可以通过写Example进行验证,通过日志打印、调试、监测工具观察理解具体的细节。例如可以写一个简单程序使用Netty,通过抓包工具观察网络包来理解其中的实现。</p><h2 id="4-3-系统测试"><a href="#4-3-系统测试" class="headerlink" title="4.3 系统测试"></a>4.3 系统测试</h2><p>如果是只是自己学习和研究,可以参考网上测试和分析的文档,但是<strong>如果要在生产环境投入使用必须进行测试</strong>。因为网上搜的测试结果,不一定与自己的业务场景很契合,如果简单参考别人的测试结果,很可能会得出错误的结论,或者使用的版本不同,测试结果差异也比较大。</p><p>要特别注意的是,测试必须建立在对这个开源项目有<strong>系统性</strong>了解的基础上,不能安装完就立马测试,否则可能会因为配置项不对,使用方法不当,导致没有根据业务的特点搭建正确的环境、没有设计合理的测试用例,从而使得最终的测试结果得出了错误结论,误导了设计决策。</p><p>下面提供测试常见的思路参考,需要根据具体项目具体业务进行测试用例的设计。</p><ul><li>核对每个配置项的作用和影响,识别出关键配置项</li><li>进行多种场景的性能测试</li><li>进行压力测试,连续跑几天,观察 CPU、内存、磁盘 IO等指标波动</li><li>进行故障测试:kill,断电、拔网线、重启 100 次以上、倒换等</li></ul><h2 id="4-4-关键源码学习"><a href="#4-4-关键源码学习" class="headerlink" title="4.4 关键源码学习"></a>4.4 关键源码学习</h2><p>钻研、领悟该项目的各种设计思想与代码实现细节,基本定位是“精通”,精益求精,学无止境。这是大神们追求的境界。如果希望成为团队技术担当、项目社区的重要贡献者,则应当以这个层次作为努力的目标。</p><p>代码不仅是读,还要理和试,有的人连API都没有调用过,上来就看代码,以为省了时间,实际是迈向自我摧残。</p><p>对源码进行理和试的关键如下:</p><ul><li><p>1 在IDE拿到调用栈<br>在IDE里读。IDE里可以方便跳转,查看定义,比起网页上看效率高得多。<br>通过IDE工具,运行example程序进行跟踪调试,通过打断点可以得到程序运行的调用栈。</p></li><li><p><em>尽可能编译调试。能调试的代码,几乎没有看不懂的。*</em></p></li><li><p>2 把调用栈画下来<br>把代码的调用逻辑梳理出来之后,再通过画图工具,把代码的图画出来,可以画:流程图、类图、调用图、时序图,更具实际情况选择最有表现力的图。</p></li></ul><p>此外,平时多了解一些设计模式。这样看到名字里有proxy,builder,factory之类的,就心领神会了。横向分层,纵向分块。代码都是分模块的,有的是core,有的是util,parser之类的,要知道看的是哪一层,那一块。</p><p>有的小项目分层不明显,也不必强求。要看的不只是语法上的技巧,更重要的是设计上的思路和原理。<strong>读没读懂,最简单的标准是,假如给充足的时间,有没有信心写出一个差不多的东西来</strong>。</p><p><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/5.jpg" alt></p><h1 id="5-步骤总结"><a href="#5-步骤总结" class="headerlink" title="5 步骤总结"></a>5 步骤总结</h1><p>实际实践操作中,完整执行上面5个步骤花费时间就长,通常情况下,前面2个步骤,在研究开源项目的时候都必不可少,第3个步骤可以在工作中打算采用开源项目才实施,第4个步骤在有一定的时间和精力下灵活安排时间做。</p><p>与其每个项目走马观花去简单了解,不如集中火力把一个项目研究吃透,即使半年才吃透一个,积累几年之后数量还是很可观的。而且很多项目的思想是共同的,例如高可用方案、分布式协议等,研究透一个,再研究类似项目,会发现学习速度非常快,因为已经把共性的部分掌握了,只需要再研究新项目差异的部分。</p><p>同时,在学习的过程中,需要不断总结,复盘,输出学习笔记,一方面锻炼逻辑思维能力,一方面有利于建立知识索引,过一段时间回顾的时候通过索引可以快速重新掌握知识,不容易遗忘。</p><h1 id="6-面向新手友好的几个开源项目推荐"><a href="#6-面向新手友好的几个开源项目推荐" class="headerlink" title="6 面向新手友好的几个开源项目推荐"></a>6 面向新手友好的几个开源项目推荐</h1><p>介绍理论之后,下面就是需要通过实践来检验了,下面介绍服务端开发常见的几个比对对新手友好,而且资料比较多的开源项目参考:</p><ul><li><p>Spring<br><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/6.png" alt>Spring作为业界最流行的框架,其重要性不言而喻。需要注意的是,由于Spring的生态圈非常庞大,精力有限,建议新手先选最简单的模块进行入门,例如Spring JDBC Template,Spring IOC, Spring AOP, Spring MVC</p></li><li><p>Mybatis<br><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/7.png" alt><br>MyBatis 作为业界流行的优秀持久层框架,支持普通 SQL 查询,存储过程和高级映射,代码量不大,网上相关源码解析的资料也比较多,项目的代码质量也是比较高,值得一读。</p></li></ul><ul><li><p>Elastic-Job<br><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/8.jpg" alt>Elastic-Job作为一个当当网开源的分布式任务调度解决方案,其社区活跃度,受欢迎程度较高,通过学习,可以对分布式一些通信、调度方面的知识有所掌握。</p></li><li><p>Dubbo<br><img src="/images/%E5%A6%82%E4%BD%95%E9%AB%98%E6%95%88%E5%AD%A6%E4%B9%A0%E5%BC%80%E6%BA%90%E9%A1%B9%E7%9B%AE/9.png" alt></p></li></ul><p>Dubbo是阿里巴巴公司开源的一个高性能优秀的服务治理框架,使得应用可通过高性能的RPC 实现服务的输出和输入功能。Dubbo在17年年底重新启动维护,在业务广泛使用,通读了解源码,在服务治理,分布式协议方面的技术实力相信会有质的飞跃。</p><p><a href="https://time.geekbang.org/column/intro/81?code=OK4eM0TBPTKGPRCzcZdzIeXjPACLfY3KCzATXOSWzXE%3D" target="_blank" rel="noopener">从0开始学架构 —— Alibaba 李运华 </a></p><p><a href="http://blog.jobbole.com/91495/" target="_blank" rel="noopener">如何高效的学习掌握新技术</a></p><p><a href="http://www.infoq.com/cn/news/2014/04/learn-open-source" target="_blank" rel="noopener">学习开源项目的若干建议</a></p><p><a href="http://vincestyling.com/posts/2014/how-am-i-read-open-source-projects-code.html" target="_blank" rel="noopener">我是如何阅读开源项目的源代码的</a></p>]]></content>
<categories>
<category> 经验心得 </category>
</categories>
</entry>
<entry>
<title>日志打印规范及技巧学习总结</title>
<link href="/2019/10/13/ri-zhi-da-yin-gui-fan-ji-ji-qiao-xue-xi-zong-jie/"/>
<url>/2019/10/13/ri-zhi-da-yin-gui-fan-ji-ji-qiao-xue-xi-zong-jie/</url>
<content type="html"><![CDATA[<p><img src="/images/%E6%97%A5%E5%BF%97%E6%89%93%E5%8D%B0%E8%A7%84%E8%8C%83%E5%8F%8A%E6%8A%80%E5%B7%A7%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/0.jpg" alt></p><h1 id="一、日志打印级别"><a href="#一、日志打印级别" class="headerlink" title="一、日志打印级别"></a>一、日志打印级别</h1><ul><li><strong>DEBUG(调试)</strong><br><strong>开发调试日志</strong>。一般来说,在系统实际运行过程中,不会输出该级别的日志。因此,开发人员可以打印任何自己觉得有利于了解系统运行状态的东东。不过很多场景下,过多的DEBUG日志,并不是好事,建议是按照业务逻辑的走向打印。</li><li><strong>INFO(通知)</strong><br>INFO日志级别主要用于记录系统运行状态等关联信息。该日志级别,<strong>常用于反馈系统当前状态给最终用户</strong>。所以,在这里输出的信息,应该对最终用户具有实际意义,也就是最终用户要能够看得明白是什么意思才行。</li><li><strong>WARN(警告)</strong><br>WARN日志常用来表示<strong>系统模块发生问题,但并不影响系统运行</strong>。 此时,进行一些修复性的工作,还能把系统恢复到正常的状态。</li><li><strong>ERROR(错误)</strong></li><li><em>此信息输出后,主体系统核心模块正常工作,需要修复才能正常工作*</em>。 就是说可以进行一些修复性的工作,但无法确定系统会正常的工作下去,系统在以后的某个阶段,很可能会因为当前的这个问题,导致一个无法修复的错误(例如宕机),但也可能一直工作到停止也不出现严重问题。</li></ul><h1 id="二、日志打印规范"><a href="#二、日志打印规范" class="headerlink" title="二、日志打印规范"></a>二、日志打印规范</h1><blockquote><p><strong>1. 【强制</strong>】应用中不可直接使用日志系统 (Log 4 j 、 Logback) 中的 API ,而应依赖使用日志框架<br>SLF 4 J 中的 API ,使用门面模式的日志框架,有利于维护和各个类的日志处理方式统一。</p></blockquote><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">import</span> org<span class="token punctuation">.</span>slf4j<span class="token punctuation">.</span>Logger<span class="token punctuation">;</span><span class="token keyword">import</span> org<span class="token punctuation">.</span>slf4j<span class="token punctuation">.</span>LoggerFactory<span class="token punctuation">;</span><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> Logger logger <span class="token operator">=</span> LoggerFactory<span class="token punctuation">.</span><span class="token function">getLogger</span><span class="token punctuation">(</span>Abc<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre><blockquote><ol start="2"><li>【强制】日志文件推荐至少保存 15 天,因为有些异常具备以“周”为频次发生的特点。</li></ol></blockquote><p>可以结合实际业务需求,基于按天,和按照容量配置appender。例如,按天保存接口对接基本关键数值记录日志,按照容量保存接口对接详细日志。</p><blockquote><ol start="3"><li>【强制】应用中的扩展日志 ( 如打点、临时监控、访问日志等 ) </li></ol><ul><li><strong>命名方式:</strong>appName _ logType _ logName . log 。</li><li><strong>日志类型( logType),推荐分类有stats / desc / monitor / visit 等</strong>。</li><li><strong>日志描述(logName)</strong>。<br>这种命名的好处:通过文件名就可知道日志文件属于什么应用,什么类型,什么目的,也有利于归类查找。推荐对日志进行分类,如将错误日志和业务日志分开存放,便于开发人员查看,也便于通过日志对系统进行及时监控。</li><li><em>正例: mppserver 应用中单独监控时区转换异常,如:mppserver _ monitor _ timeZoneConvert . log*</em></li></ul></blockquote><blockquote><ol start="4"><li>【强制】对 trace / debug / info 级别的日志输出,必须使用条件输出形式或者使用占位符的方式。<br>说明: logger . debug( “ Processing trade with id : “ + id + “ and symbol : “ + symbol)。如果日志级别是 warn ,上述日志不会打印,但是会执行字符串拼接操作,如果 symbol 是对象,会执行 toString() 方法,浪费了系统资源,执行了上述操作,最终日志却没有打印。<br>正例: ( 占位符 )<br>logger.debug(“Processing trade with id: {} and symbol : {} “, id, symbol);</li></ol></blockquote><blockquote><ol start="5"><li>【强制】避免重复打印日志,浪费磁盘空间,务必在 log 4 j . xml 中设置 additivity = false 。<br>正例: <logger name="com.taobao.dubbo.config" additivity="false"> </logger></li></ol></blockquote><blockquote><ol start="6"><li>【强制】异常信息应该包括两类信息:案发现场信息和异常堆栈信息。如果不处理,那么通过关键字 throws 往上抛出。<br>正例: logger.error(各类参数或者对象 toString + “_” + e.getMessage(), e);</li></ol></blockquote><blockquote><ol start="7"><li>【推荐】谨慎地记录日志。生产环境禁止输出 debug 日志 ; 有选择地输出 info 日志 ; 如果使用 warn 来记录刚上线时的业务行为信息,一定要注意日志输出量的问题,避免把服务器磁盘撑爆,并记得及时删除这些观察日志。<br>说明:大量地输出无效日志,不利于系统性能提升,也不利于快速定位错误点。记录日志时请思考:这些日志真的有人看吗?看到这条日志你能做什么?能不能给问题排查带来好处?</li></ol></blockquote><blockquote><ol start="8"><li>【参考】可以使用 warn 日志级别来记录用户输入参数错误的情况,避免用户投诉时,无所适从。注意日志输出的级别, error 级别只记录系统逻辑出错、异常等重要的错误信息。如非必要,请不要在此场景打出 error 级别。</li></ol></blockquote><h1 id="三、日志打印技巧"><a href="#三、日志打印技巧" class="headerlink" title="三、日志打印技巧"></a>三、日志打印技巧</h1><h2 id="问题排查的日志"><a href="#问题排查的日志" class="headerlink" title="问题排查的日志"></a>问题排查的日志</h2><ul><li><p><strong>对接外部的调用封装</strong>:<br>程序中对接外部系统与模块的依赖调用前后都记下日志,方便接口调试。出问题时也可以很快理清是哪块的问题 </p><pre class="line-numbers language-java"><code class="language-java">LOG<span class="token punctuation">.</span><span class="token function">debug</span><span class="token punctuation">(</span><span class="token string">"Calling external system:"</span> <span class="token operator">+</span> parameters<span class="token punctuation">)</span><span class="token punctuation">;</span> Object result <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token keyword">try</span> <span class="token punctuation">{</span> result <span class="token operator">=</span> <span class="token function">callRemoteSystem</span><span class="token punctuation">(</span>params<span class="token punctuation">)</span><span class="token punctuation">;</span> LOG<span class="token punctuation">.</span><span class="token function">debug</span><span class="token punctuation">(</span><span class="token string">"Called successfully. result is "</span> <span class="token operator">+</span> result<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"Failed at calling xxx system . exception : "</span> <span class="token operator">+</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>状态变化:<br>程序中重要的状态信息的变化应该记录下来,方便查问题时还原现场,推断程序运行过程 </p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">boolean</span> isRunning <span class="token operator">=</span> <span class="token boolean">true</span><span class="token punctuation">;</span> LOG<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"System is running"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//... </span>isRunning <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">;</span> LOG<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"System was interrupted by "</span> <span class="token operator">+</span> Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>系统入口与出口: </p><pre class="line-numbers language-java"><code class="language-java">这个粒度可以是重要方法级或模块级。记录它的输入与输出,方便定位 <span class="token keyword">void</span> <span class="token function">execute</span><span class="token punctuation">(</span>Object input<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">debug</span><span class="token punctuation">(</span><span class="token string">"Invoke parames : "</span> <span class="token operator">+</span> input<span class="token punctuation">)</span><span class="token punctuation">;</span> Object result <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//business logical </span> LOG<span class="token punctuation">.</span><span class="token function">debug</span><span class="token punctuation">(</span><span class="token string">"Method result : "</span> <span class="token operator">+</span> result<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>业务异常:<br>任何业务异常都应该记下来 </p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">try</span> <span class="token punctuation">{</span> <span class="token comment" spellcheck="true">//business logical </span><span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">IOException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"Description xxx"</span> <span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">BusinessException</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"Let me know anything"</span>,e<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">(</span><span class="token class-name">Exception</span> e<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">error</span><span class="token punctuation">(</span><span class="token string">"Description xxx"</span><span class="token punctuation">,</span> e<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">void</span> <span class="token function">invoke</span><span class="token punctuation">(</span>Object primaryParam<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>primaryParam <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span>原因<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>非预期执行:<br>为程序在“有可能”执行到的地方打印日志。如果我想删除一个文件,结果返回成功。但事实上,那个文件在你想删除之前就不存在了。最终结果是一致的,但程序得让我们知道这种情况,要查清为什么文件在删除之前就已经不存在呢 </p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">int</span> myValue <span class="token operator">=</span> xxxx<span class="token punctuation">;</span> <span class="token keyword">int</span> absResult <span class="token operator">=</span> Math<span class="token punctuation">.</span><span class="token function">abs</span><span class="token punctuation">(</span>myValue<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>absResult <span class="token operator"><</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"Original int "</span> <span class="token operator">+</span> myValue <span class="token operator">+</span> <span class="token string">"has nagetive abs "</span> <span class="token operator">+</span> absResult<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>很少出现的else情况:<br>else可能吞掉你的请求,或是赋予难以理解的最终结果 </p><pre class="line-numbers language-java"><code class="language-java">Object result <span class="token operator">=</span> null<span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>running<span class="token punctuation">)</span> <span class="token punctuation">{</span> result <span class="token operator">=</span> xxx<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> result <span class="token operator">=</span> yyy<span class="token punctuation">;</span> LOG<span class="token punctuation">.</span><span class="token function">debug</span><span class="token punctuation">(</span><span class="token string">"System does not running, we change the final result"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="程序运行状态的日志"><a href="#程序运行状态的日志" class="headerlink" title="程序运行状态的日志"></a>程序运行状态的日志</h2><p>程序在运行时就像一个机器人,我们可以从它的日志看出它正在做什么,是不是按预期的设计在做,所以这些正常的运行状态是要有的。 </p></li><li><p>程序运行时间: </p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">long</span> startTime <span class="token operator">=</span> System<span class="token punctuation">.</span><span class="token function">currentTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> LOG<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"execution cost : "</span> <span class="token operator">+</span> <span class="token punctuation">(</span>System<span class="token punctuation">.</span><span class="token function">currentTime</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">-</span> startTime<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">"ms"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span></span></code></pre></li></ul><p>大批量数据的执行进度: </p><pre class="line-numbers language-java"><code class="language-java">LOG<span class="token punctuation">.</span><span class="token function">debug</span><span class="token punctuation">(</span><span class="token string">"current progress: "</span> <span class="token operator">+</span> <span class="token punctuation">(</span>currentPos <span class="token operator">*</span> <span class="token number">100</span> <span class="token operator">/</span> totalAmount<span class="token punctuation">)</span> <span class="token operator">+</span> <span class="token string">"%"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre><p>关键变量及正在做哪些重要的事情:<br>执行关键的逻辑,做IO操作等等 </p><pre class="line-numbers language-java"><code class="language-java">String <span class="token function">getJVMPid</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> String pid <span class="token operator">=</span> <span class="token string">""</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">// Obtains JVM process ID </span> LOG<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"JVM pid is "</span> <span class="token operator">+</span> pid<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> pid<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">void</span> <span class="token function">invokeRemoteMethod</span><span class="token punctuation">(</span>Object params<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">info</span><span class="token punctuation">(</span><span class="token string">"Calling remote method : "</span> <span class="token operator">+</span> params<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//Calling remote server </span><span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h1 id="四、需要规避的问题"><a href="#四、需要规避的问题" class="headerlink" title="四、需要规避的问题"></a>四、需要规避的问题</h1><ul><li><p>频繁打印大数据量日志:<br>当日志产生的速度大于日志文件写磁盘的速度,会导致日志内容积压在内存中,导致内存泄漏。</p></li><li><p>无意义的Log:<br>日志不包含有意义的信息: 你肯定想知道的是哪个文件不存在吧 </p><pre class="line-numbers language-java"><code class="language-java">File file <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">File</span><span class="token punctuation">(</span><span class="token string">"xxx"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span>file<span class="token punctuation">.</span><span class="token function">isExist</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"File does not exist"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//Useless message </span><span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></li><li><p>混淆信息的Log:<br>日志应该是清晰准确的: 当看到日志的时候,你知道是因为连接池取不到连接导致的问题么?</p><pre class="line-numbers language-java"><code class="language-java">Connection connection <span class="token operator">=</span> ConnectionFactory<span class="token punctuation">.</span><span class="token function">getConnection</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">if</span> <span class="token punctuation">(</span>connection <span class="token operator">==</span> null<span class="token punctuation">)</span> <span class="token punctuation">{</span> LOG<span class="token punctuation">.</span><span class="token function">warn</span><span class="token punctuation">(</span><span class="token string">"System initialized unsuccessfully"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre><p>参考:<br>《阿里巴巴开发手册》<br><a href="http://blog.csdn.net/rogger_chen/article/details/50587920" target="_blank" rel="noopener">Logger日志级别说明及设置方法、说明</a><br><a href="http://langyu.iteye.com/blog/1147992" target="_blank" rel="noopener">闲谈程序中如何打印log</a></p></li></ul>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>基于SonarQube代码质量检查工具总结</title>
<link href="/2019/10/13/ji-yu-sonarqube-dai-ma-zhi-liang-jian-cha-gong-ju-zong-jie/"/>
<url>/2019/10/13/ji-yu-sonarqube-dai-ma-zhi-liang-jian-cha-gong-ju-zong-jie/</url>
<content type="html"><![CDATA[<p><img src="/images/%E5%9F%BA%E4%BA%8ESonarQube%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%9F%A5%E5%B7%A5%E5%85%B7%E6%80%BB%E7%BB%93/0.jpg" alt></p><p># 1 概述<br>SonarQube(sonar)是一个开源平台,用于管理源代码的质量。 SonarQube不只是一个质量数据报告工具,更是代码质量管理平台。 支持java, C#, C/C++, PL/SQL, Cobol, JavaScrip, Groovy 等等二十几种编程语言的代码质量管理与检测。 SonarQube可以从以下七个维度检测代码质量,而作为开发人员至少需要处理前5种代码质量问题。</p><ul><li>(1) <strong>不遵循代码标准</strong><br>SonarQube可以通过PMD,CheckStyle,Findbugs等等代码规则检测工具规范代码编写。</li><li>(2) <strong>潜在的缺陷</strong><br>SonarQube可以通过PMD,CheckStyle,Findbugs等等代码规则检测工具检 测出潜在的缺陷。</li><li>(3) <strong>糟糕的复杂度分布</strong><br>文件、类、方法等,如果复杂度过高将难以改变,这会使得开发人员 难以理解它们, 且如果没有自动化的单元测试,对于程序中的任何组件的改变都将可能导致需要全面的回归测试。</li><li>(4) <strong>重复</strong><br>显然程序中包含大量复制粘贴的代码是质量低下的,SonarQube可以展示 源码中重复严重的地方。</li><li>(5) <strong>注释不足或者过多</strong><br>没有注释将使代码可读性变差,特别是当不可避免地出现人员变动时,程序的可读性将大幅下降 而过多的注释又会使得开发人员将精力过多地花费在阅读注释上,亦违背初衷。</li><li>(6) <strong>缺乏单元测试</strong><br>SonarQube可以很方便地统计并展示单元测试覆盖率。</li><li>(7) <strong>糟糕的设计</strong><br>通过SonarQube可以找出循环,展示包与包、类与类之间的相互依赖关系,可以检测自定义的架构规则 通过SonarQube可以管理第三方的jar包,可以利用LCOM4检测单个任务规则的应用情况, 检测耦合。</li></ul><p>通过以下介绍如何基于Jenkins和SonarQube完成代码质量持续检测。</p><h1 id="2-环境准备"><a href="#2-环境准备" class="headerlink" title="2 环境准备"></a>2 环境准备</h1><p>清单如下,安装方法自行百度谷歌</p><ul><li>Java环境</li><li>Maven环境</li><li>SonarQube平台 </li><li>Jenkins平台</li><li>数据库(例如MySQL)</li></ul><h1 id="3-环境配置"><a href="#3-环境配置" class="headerlink" title="3 环境配置"></a>3 环境配置</h1><h2 id="3-1-Maven"><a href="#3-1-Maven" class="headerlink" title="3.1 Maven"></a>3.1 Maven</h2><p>为了基于Maven方式使用Jenkins,需要再Maven安装目录下的conf/setting.xml文件配置关于sonar的配置的profile</p><pre><code> <profile> <id>sonar</id> <activation> <activeByDefault>true</activeByDefault> </activation> <properties> <!-- 平台登录的账号的用户名 --> <sonar.login>你的用户名</sonar.login> <!-- SonarQube平台登录的账号的密码 --> <sonar.password>你的密码</sonar.password> <!-- SonarQube访问地址 --> <sonar.host.url>http://sonar.ibeiliao.net:9000</sonar.host.url> <!-- 代码分析包括哪些文件需要分析,英文逗号分隔 --> <sonar.inclusions>**/*.java,**/*.xml</sonar.inclusions> </properties> </profile></code></pre><p>并使用 <activeprofile>sonar</activeprofile> 激活profile</p><p><img src="/images/%E5%9F%BA%E4%BA%8ESonarQube%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%9F%A5%E5%B7%A5%E5%85%B7%E6%80%BB%E7%BB%93/1.png" alt></p><h1 id="4-使用说明"><a href="#4-使用说明" class="headerlink" title="4 使用说明"></a>4 使用说明</h1><h2 id="4-1-Jenkins"><a href="#4-1-Jenkins" class="headerlink" title="4.1 Jenkins"></a>4.1 Jenkins</h2><p>创建一个新任务:</p><ul><li>步骤1 创建<br>选择构建一个自由风格的软件项目<br><img src="/images/%E5%9F%BA%E4%BA%8ESonarQube%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%9F%A5%E5%B7%A5%E5%85%B7%E6%80%BB%E7%BB%93/2.png" alt></li><li>步骤2 配置源码管理<br>这里是使用了Git来做源码管理,gitlab作为源码库<br>项目分支填写master<br><img src="/images/%E5%9F%BA%E4%BA%8ESonarQube%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%9F%A5%E5%B7%A5%E5%85%B7%E6%80%BB%E7%BB%93/3.png" alt></li><li>步骤3 配置构建触发器<br>这里配置H 18 * * *,代表每日18点前定时构建<br><img src="/images/%E5%9F%BA%E4%BA%8ESonarQube%E4%BB%A3%E7%A0%81%E8%B4%A8%E9%87%8F%E6%A3%80%E6%9F%A5%E5%B7%A5%E5%85%B7%E6%80%BB%E7%BB%93/4.png" alt></li><li>步骤4 配置构建<br>第一行使用jacoco插件,进行代码覆盖率测试<br>第二行使用sonar插件,进行代码检测并提交检测结果<pre><code>clean org.jacoco:jacoco-maven-plugin:prepare-agent install -Dmaven.test.failure.ignore=true -P dev</code></pre></li></ul><p>sonar:sonar</p><pre><code>![](/images/基于SonarQube代码质量检查工具总结/5.png)* 步骤5 配置构建后步骤,发邮件需要在Jenkins提前配置好邮箱![](/images/基于SonarQube代码质量检查工具总结/6.png)## 4.2 SonarQubeSonarQube基本架构图![SonarQube基本架构图](/images/基于SonarQube代码质量检查工具总结/7.png)SonarQube与项目持续集成架构图![SonarQube与项目持续集成架构图](/images/基于SonarQube代码质量检查工具总结/8.png)### 4.2.1 SonarQube说明### 4.2.2 开发者本地基于Maven使用SonarQubeJenkins的每日构建默认是使用master,在开发过程中,有时需要在开发者的开发中的分支进行代码检测方法:* 步骤1 配置Maven按照 **3.1 Maven**的配置说明,配置本地的Maven环境* 步骤2 触发检测在项目顶层目录,执行命令:mvn sonar:sonar即可### 4.2.3 相关指标说明![](/images/基于SonarQube代码质量检查工具总结/9.png)![指标](/images/基于SonarQube代码质量检查工具总结/10.png)### 4.2.4 代码质量阈![](/images/基于SonarQube代码质量检查工具总结/11.png)代码整体质量的统计,**可以帮助用户理解项目是否已经可以投入生产**默认配置(可以根据项目实际情况重新配置):![默认质量阈配置](/images/基于SonarQube代码质量检查工具总结/12.png)# 参考[SonarQube代码质量检查工具](https://my.oschina.net/zzuqiang/blog/843406)</code></pre>]]></content>
<categories>
<category> 运维监控 </category>
</categories>
</entry>
<entry>
<title>Java代码静态检测工具比较</title>
<link href="/2019/10/13/java-dai-ma-jing-tai-jian-ce-gong-ju-bi-jiao/"/>
<url>/2019/10/13/java-dai-ma-jing-tai-jian-ce-gong-ju-bi-jiao/</url>
<content type="html"><![CDATA[<p><img src="/images/Java%E4%BB%A3%E7%A0%81%E9%9D%99%E6%80%81%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7%E6%AF%94%E8%BE%83/0.jpg" alt><br>最近团队想引进代码静态检测工具,稍微调研一下:</p><h1 id="工具比较"><a href="#工具比较" class="headerlink" title="工具比较"></a>工具比较</h1><h2 id="功能比较"><a href="#功能比较" class="headerlink" title="功能比较"></a>功能比较</h2><table><thead><tr><th align="left"></th><th align="center">Checkstyle</th><th align="center">FindBugs</th><th align="center">PMD</th><th align="center">Jtest</th><th align="center">SonarQube</th></tr></thead><tbody><tr><td align="left">使用方式</td><td align="center">IDE插件</td><td align="center">IDE插件</td><td align="center">IDE插件</td><td align="center">IDE插件</td><td align="center">IDE插件+独立部署的服务</td></tr><tr><td align="left">自定义规则</td><td align="center">√</td><td align="center">√</td><td align="center">√</td><td align="center">√</td><td align="center">√</td></tr><tr><td align="left">集成到Jenkins中</td><td align="center">√</td><td align="center">√</td><td align="center">√</td><td align="center"></td><td align="center">√</td></tr><tr><td align="left">多版本统计</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">√</td></tr><tr><td align="left">缺陷跟踪记录统计</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">√</td></tr><tr><td align="left">代码测试覆盖率</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">√<br>(需要配合jacoco)</td></tr></tbody></table><h2 id="工具查错能力比较"><a href="#工具查错能力比较" class="headerlink" title="工具查错能力比较"></a>工具查错能力比较</h2><table><thead><tr><th align="left">代码缺陷分类</th><th align="center">示例</th><th align="center">Checkstyle</th><th align="center">FindBugs</th><th align="center">PMD</th><th align="center">Jtest</th></tr></thead><tbody><tr><td align="left">引用操作</td><td align="center">空指针引用</td><td align="center">√</td><td align="center">√</td><td align="center">√</td><td align="center">√</td></tr><tr><td align="left">对象操作</td><td align="center">对象比较(使用 == 而不是 equals)</td><td align="center"></td><td align="center">√</td><td align="center">√</td><td align="center">√</td></tr><tr><td align="left">表达式复杂化</td><td align="center">多余的 if 语句</td><td align="center"></td><td align="center"></td><td align="center">√</td><td align="center"></td></tr><tr><td align="left">数组使用</td><td align="center">数组下标越界</td><td align="center"></td><td align="center"></td><td align="center"></td><td align="center">√</td></tr><tr><td align="left">未使用变量或代码段</td><td align="center">未使用变量</td><td align="center"></td><td align="center">√</td><td align="center">√</td><td align="center">√</td></tr><tr><td align="left">资源回收</td><td align="center">I/O 未关闭</td><td align="center"></td><td align="center">√</td><td align="center"></td><td align="center">√</td></tr><tr><td align="left">方法调用</td><td align="center">未使用方法返回值</td><td align="center"></td><td align="center">√</td><td align="center"></td><td align="center"></td></tr><tr><td align="left">代码设计</td><td align="center">空的 try/catch/finally 块</td><td align="center"></td><td align="center"></td><td align="center">√</td><td align="center"></td></tr></tbody></table><h2 id="SonarQube-特征"><a href="#SonarQube-特征" class="headerlink" title="SonarQube 特征"></a>SonarQube 特征</h2><ul><li>支持超过25种编程语言:Java、C/C++、C#、PHP、Flex、Groovy、JavaScript、Python、PL/SQL、COBOL等。(不过有些是商业软件插件)</li><li>可以集成不同的测试工具,代码分析工具,以及持续集成工具,比如<strong>pmd-cpd、checkstyle、findbugs、Jenkins</strong>。通过不同的插件对这些结果进行再加工处理,通过量化的方式度量代码质量的变化,从而可以方便地对不同规模和种类的工程进行代码质量管理</li><li>可以在Android开发中使用</li><li>提供重复代码、编码标准、单元测试、代码覆盖率、代码复杂度、潜在Bug、注释和软件设计报告</li><li>提供了指标历史记录、计划图(“时间机器”)和微分查看<br>提供了完全自动化的分析:与Maven、Ant、Gradle和持续集成工具(Atlassian Bamboo、Jenkins、Hudson等)</li><li>与Eclipse开发环境集成</li><li>与JIRA、Mantis、LDAP、Fortify等外部工具集</li><li>支持扩展插件</li><li>利用SQALE计算技术债务</li><li>支持Tomcat。不过计划从SonarQube 4.1起终止对Tomcat的支持。</li></ul><p><img src="/images/Java%E4%BB%A3%E7%A0%81%E9%9D%99%E6%80%81%E6%A3%80%E6%B5%8B%E5%B7%A5%E5%85%B7%E6%AF%94%E8%BE%83/1.png" alt="Sonarqube-nemo-dashboard"></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://www.cnblogs.com/EasonJim/p/7685724.html" target="_blank" rel="noopener">Java静态检测工具/Java代码规范和质量检查简单介绍(转)</a></p><p><a href="https://www.ibm.com/developerworks/cn/java/j-lo-statictest-tools/" target="_blank" rel="noopener">常用 Java 静态代码分析工具的分析与比较</a></p><p><a href="https://zh.wikipedia.org/wiki/SonarQube" target="_blank" rel="noopener">SonarQube维基百科</a></p>]]></content>
<categories>
<category> 工具使用 </category>
</categories>
</entry>
<entry>
<title>线程的5种状态总结</title>
<link href="/2019/10/13/xian-cheng-de-5-chong-zhuang-tai-zong-jie/"/>
<url>/2019/10/13/xian-cheng-de-5-chong-zhuang-tai-zong-jie/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%BA%BF%E7%A8%8B%E7%9A%845%E7%A7%8D%E7%8A%B6%E6%80%81%E6%80%BB%E7%BB%93/0.jpg" alt></p><h2 id="线程的5种状态"><a href="#线程的5种状态" class="headerlink" title="线程的5种状态"></a>线程的5种状态</h2><p>线程可以有如下5种状态:<br>5种状态的转换图如下</p><p><img src="/images/%E7%BA%BF%E7%A8%8B%E7%9A%845%E7%A7%8D%E7%8A%B6%E6%80%81%E6%80%BB%E7%BB%93/1.png" alt="线程状态转换图"></p><h1 id="New-新创建"><a href="#New-新创建" class="headerlink" title="New (新创建)"></a>New (新创建)</h1><p>* <em>当用*</em>new**操作符创建一个线程时,如new Thread(r),该线程还没有开始运行。这意外这它的状态是new。此时程序还没有开始运行线程中的代码,在线程运行之前还有一些基础工作要做。</p><h1 id="Runnable-可运行-就绪"><a href="#Runnable-可运行-就绪" class="headerlink" title="Runnable (可运行/就绪)"></a>Runnable (可运行/就绪)</h1><p>* <em>一旦处于新状态的线程调用*</em>start**方法(如图中的1所示),线程就处于Runnbale状态。<br>* *处于Runnable状态的线程还未运行run()方法的代码,只有在获得CPU时间片才开始运行。</p><h1 id="Running-运行中"><a href="#Running-运行中" class="headerlink" title="Running (运行中)"></a>Running (运行中)</h1><p>* *当线程获得CPU时间片,线程就进入Running状态(如图中的2所示)。<br>处于Running状态的线程有可能在运行中CPU时间片用完,而run方法没运行完,线程就又进入Runnable状态。<br>* *通常情况下,运行中的线程一直处于Running与Runnable交替转换的过程中。</p><h1 id="Blocked-等待-阻塞-睡眠"><a href="#Blocked-等待-阻塞-睡眠" class="headerlink" title="Blocked (等待/阻塞/睡眠)"></a>Blocked (等待/阻塞/睡眠)</h1><p>* <em>当线程在Running状态中,遇到*</em>阻塞等待锁<strong>、</strong>等待用户输入<strong>、</strong>调用sleep()方法<strong>、</strong>调用join等待其他线程**情况,会导致线程进入阻塞状态(Blocked)。<br>* *处于阻塞状态的线程,在阻塞等待结束之后,会进入Runnable状态,等等获得CPU时间片继续运行程序。</p><h1 id="Dead-死亡"><a href="#Dead-死亡" class="headerlink" title="Dead (死亡)"></a>Dead (死亡)</h1><p>* *当线程运行完run方法,直接进入死亡状态Dead 。</p><h1 id="常用运维"><a href="#常用运维" class="headerlink" title="常用运维"></a>常用运维</h1><h2 id="代码中获得线程信息"><a href="#代码中获得线程信息" class="headerlink" title="代码中获得线程信息"></a>代码中获得线程信息</h2><p>在程序运行中,在代码中打印线程的信息(线程id,hashCode,name,id,priority,state等),方法如下:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">static</span> String <span class="token function">getCurrentThreadInfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">{</span> Thread current <span class="token operator">=</span> Thread<span class="token punctuation">.</span><span class="token function">currentThread</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> Map<span class="token operator"><</span>String<span class="token punctuation">,</span>Object<span class="token operator">></span> threadInfoMap <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">HashMap</span><span class="token operator"><</span><span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span> threadInfoMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span><span class="token string">"hashCode"</span><span class="token punctuation">,</span>current<span class="token punctuation">.</span><span class="token function">hashCode</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> threadInfoMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span><span class="token string">"name"</span><span class="token punctuation">,</span>current<span class="token punctuation">.</span><span class="token function">getName</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> threadInfoMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span><span class="token string">"id"</span><span class="token punctuation">,</span>current<span class="token punctuation">.</span><span class="token function">getId</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> threadInfoMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span><span class="token string">"priority"</span><span class="token punctuation">,</span>current<span class="token punctuation">.</span><span class="token function">getPriority</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> threadInfoMap<span class="token punctuation">.</span><span class="token function">put</span><span class="token punctuation">(</span><span class="token string">"state"</span><span class="token punctuation">,</span>current<span class="token punctuation">.</span><span class="token function">getState</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token keyword">return</span> JSON<span class="token punctuation">.</span><span class="token function">toJSONString</span><span class="token punctuation">(</span>threadInfoMap<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><h2 id="查看线程正在执行的代码"><a href="#查看线程正在执行的代码" class="headerlink" title="查看线程正在执行的代码"></a>查看线程正在执行的代码</h2><p>在程序运行的过程中,可以基于jstack查看线程正在执行的堆栈信息,示例:<br>jstack 10765 | grep ‘0x2a34’ -C5 –color<br>其中,’10765’是进程id,’0x2a34’是16进制的线程id<br><img src="/images/%E7%BA%BF%E7%A8%8B%E7%9A%845%E7%A7%8D%E7%8A%B6%E6%80%81%E6%80%BB%E7%BB%93/2.png" alt="线程堆栈信息"></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p>1、《Java核心技术 卷I》<br>2、<a href="https://mp.weixin.qq.com/s/Xb1im4jG_Cobhas4q4YT1Q" target="_blank" rel="noopener">《线上服务CPU100%问题快速定位实战》</a></p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>Dubbo服务本地调测优雅实践</title>
<link href="/2019/10/13/dubbo-fu-wu-ben-di-diao-ce-you-ya-shi-jian/"/>
<url>/2019/10/13/dubbo-fu-wu-ben-di-diao-ce-you-ya-shi-jian/</url>
<content type="html"><![CDATA[<p><img src="/images/Dubbo%E6%9C%8D%E5%8A%A1%E6%9C%AC%E5%9C%B0%E8%B0%83%E6%B5%8B%E4%BC%98%E9%9B%85%E5%AE%9E%E8%B7%B5/0.jpg" alt></p><p>本文介绍在不污染项目配置文件,不影响服务器服务的前提下,如何优雅地在本地调测Dubbo服务</p><h1 id="1-问题场景"><a href="#1-问题场景" class="headerlink" title="1 问题场景"></a>1 问题场景</h1><p>分布式应用的调试总是比常规项目开发调试起来要麻烦很多,主要如下:</p><ul><li>Dubbo服务开发完服务提供者后需要进行本地测试测试,本来希望请求的服务是本地服务,结果经常调用到服务器的服务</li><li>Dubbo支持修改本地配置文件,使服务消费者调用本地服务,但是该本地配置不小心提交上Git/SVN,会导致服务器上版本服务不正常</li></ul><h1 id="2-解决步骤"><a href="#2-解决步骤" class="headerlink" title="2 解决步骤"></a>2 解决步骤</h1><ul><li><p>1 创建properties文件<br>创建一个properties文件,名字可以随便命名,例如命名为:dubbo-local.properties,这个文件可以放在任何地方。该文件不提交到Git/SVN,<strong>建议不要放在工程目录里以避免自己提交了都不知道</strong>,建议放在用户目录下${user.home}<br>获取用户目录的方法之一:</p><pre class="line-numbers language-java"><code class="language-java">System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>System<span class="token punctuation">.</span><span class="token function">getProperty</span><span class="token punctuation">(</span><span class="token string">"user.home"</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span></span></code></pre></li><li><p>2 修改properties文件</p><pre class="line-numbers language-sql"><code class="language-sql"><span class="token comment" spellcheck="true"># 以下是你们DubboServer.xml中配置的需要Export Service,</span><span class="token comment" spellcheck="true"># 建议你有几个要Export Service都配置在这里,后面是请求本地的地址</span><span class="token comment" spellcheck="true"># 地址格式:dubbo://ip:port,这里需要注意的是,需要修改为自己dubbo服务的端口</span>com<span class="token punctuation">.</span>ibeiliao<span class="token punctuation">.</span>course<span class="token punctuation">.</span>stub<span class="token punctuation">.</span>provider<span class="token punctuation">.</span>AdCourseProvider<span class="token operator">=</span>dubbo:<span class="token comment" spellcheck="true">//localhost:20880</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span></span></code></pre></li></ul><p>为方便开发测试,经常会在线下共用一个所有服务可用的注册中心,这时,如果一个正在开发中的服务提供者注册,可能会影响消费者不能正常运行。<br>可以加入如下配置来禁用注册:</p><pre class="line-numbers language-sql"><code class="language-sql"><span class="token comment" spellcheck="true"># 禁止服务提供者注册到注册中心</span>dubbo<span class="token punctuation">.</span>registry<span class="token punctuation">.</span>register<span class="token operator">=</span><span class="token boolean">false</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><ul><li>3 配置运行的环境变量<pre class="line-numbers language-java"><code class="language-java">参数:<span class="token operator">-</span>Ddubbo<span class="token punctuation">.</span>properties<span class="token punctuation">.</span>file值:dubbo<span class="token operator">-</span>local<span class="token punctuation">.</span>properties文件的本地绝对路径<span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre>这里我基于IDEA运行Junit测试,配置环境变量方法如下<br><img src="/images/Dubbo%E6%9C%8D%E5%8A%A1%E6%9C%AC%E5%9C%B0%E8%B0%83%E6%B5%8B%E4%BC%98%E9%9B%85%E5%AE%9E%E8%B7%B5/1.png" alt></li><li>4 运行并测试<br>我这里基于Junit测试,在测试类头加上2个注解,applicationContext.xml是Spring配置文件<pre class="line-numbers language-java"><code class="language-java"><span class="token annotation punctuation">@RunWith</span><span class="token punctuation">(</span>SpringJUnit4ClassRunner<span class="token punctuation">.</span><span class="token keyword">class</span><span class="token punctuation">)</span><span class="token annotation punctuation">@ContextConfiguration</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token string">"classpath:applicationContext.xml"</span> <span class="token punctuation">}</span><span class="token punctuation">)</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><img src="/images/Dubbo%E6%9C%8D%E5%8A%A1%E6%9C%AC%E5%9C%B0%E8%B0%83%E6%B5%8B%E4%BC%98%E9%9B%85%E5%AE%9E%E8%B7%B5/2.png" alt="image.png"><br>可以看到,在Dubbo服务提供者代码打断点,程序可以正常再断点处停下</li></ul><h1 id="3-总结"><a href="#3-总结" class="headerlink" title="3 总结"></a>3 总结</h1><p><a href="http://dubbo.apache.org/zh-cn/docs/user/configuration/properties.html" target="_blank" rel="noopener">参考Dubbo官方文档</a>,Dubbo获取配置文件时,优先级如下:</p><ul><li>JVM 启动 -D 参数优先,这样可以使用户在部署和启动时进行参数重写,比如在启动时需改变协议的端口</li><li>XML 次之,如果在 XML 中有配置,则 dubbo.properties 中的相应配置项无效</li><li>Properties 最后,相当于缺省值,只有 XML 没有配置时,dubbo.properties 的相应配置项才会生效,通常用于共享公共配置,比如应用名</li></ul><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://ningyu1.github.io/site/post/09-dubbo-debug/" target="_blank" rel="noopener">Dubbo本地调试最优方式,本地Server端调用本地Client端</a><br><a href="http://dubbo.apache.org/zh-cn/docs/user/configuration/properties.html" target="_blank" rel="noopener">Dubbo官方文档</a><br><a href="https://blog.csdn.net/abcde474524573/article/details/53026110" target="_blank" rel="noopener">dubbo 知识总结 dubbo配置参考</a></p>]]></content>
<categories>
<category> 技术框架 </category>
</categories>
</entry>
<entry>
<title>编程常用快捷键</title>
<link href="/2019/10/13/bian-cheng-chang-yong-kuai-jie-jian/"/>
<url>/2019/10/13/bian-cheng-chang-yong-kuai-jie-jian/</url>
<content type="html"><![CDATA[<p><img src="/images/%E7%BC%96%E7%A8%8B%E5%B8%B8%E7%94%A8%E5%BF%AB%E6%8D%B7%E9%94%AE/0.jpg" alt></p><p>Intellij idea快捷键<br>编辑类快捷键<br>Ctrl + Shift + Alt + U 生成UML图(专业版才有的功能)<br>Ctrl + Alt + L 代码自动缩进<br>Ctrl + Alt + T 对选中的代码块进行快速包装<br>Ctrl + Alt + V 自动补全变量名称、返回值<br>Ctrl + Alt + O 优化imports(去除不必要的)<br>Alt + Shift + ↑/↓ 选中代码上移/下移一行</p><p>查看快捷键<br>Ctrl + Alt + ← 光标回到上一次的位置<br>Ctrl + Alt + → 光标回到下一次的位置<br>Ctrl + Alt + S 打开设置<br>Alt + Q 快速查看当前方法的声明<br>Ctrl + Q 查看方法说明</p><p>Ctrl + 左键变量 跳到变量声明的地方<br>Ctrl + Shift + F10 运行文件<br>Ctrl + Shift + F 全局搜索<br>Ctrl + E 打开最近打开的文件<br>Ctrl + Shift + Num 添加数字书签(数字必须是键盘横排上的数字)<br>Ctrl + num 快速跳转到指定的数字书签(数字必须是键盘横排上的数字)<br>Ctrl + R 替换<br>Ctrl + Y 删除行<br>Ctrl + D 复制当前行到下一行<br>Ctrl + Shift + R 全局替换<br>Ctrl + Shift + Alt + S 打开项目属性配置<br>Ctrl + H + 点击名词 查看详细<br>Ctrl + 空格 弹出建议列表<br>Ctrl + Shift + V 打开粘贴的历史列表<br>Ctrl + Shift + N 快速打开文件<br>Ctrl + N 快速搜索类<br>Ctrl + F9 重新编译<br>Ctrl + E 最近浏览历史<br>Ctrl + Q 查看方法声明</p><p>Ctrl + W 选你所想<br>Alt + Insert 快速添加get和set方法<br>Alt + 1 打开项目视图<br>Alt + 6 TODO<br>Shift + F6 重新命名<br>Shift + Shift 万能搜索<br>Shift + Enter 任意位置换行<br>psvm + Tab 创建main方法<br>for + i 快速写for循环<br>sout 快速写System.out.println()</p><p>查看函数列表<br>在Project框中最右边有一个齿轮状的设置按钮,增加“show members”即可<br>━━●●━━━━━━━━━━━━━●●━━━━━━━━━━━━━●●━━━━●●━━━━━━━━━━━━━●●━━<br>Eclipse快捷键<br>Alt + ← 上次浏览的页面<br>Alt + → 下次浏览的页面<br>Alt + Shift + W 定位当前文件的位置<br>Alt + Shift + S 生成get,set函数等<br>Ctrl + H 全局搜索<br>Ctrl + F 当前文件搜索<br>Ctrl + Shift + F 代码进行排版<br>Ctrl + O 打开当前java文件的函数,变量列表<br>Ctrl + / 使用//注释文件<br>Ctrl + shift + / 多行注释文件,支持多格式<br>“sysout” System.out.println()<br>“main” public static void main(String[] args)</p><p>━━●●━━━━━━━━━━━━━●●━━━━━━━━━━━━━●●━━━━●●━━━━━━━━━━━━━●●━━<br>navicat<br>Ctrl + D 查看索引<br>━━●●━━━━━━━━━━━━━●●━━━━━━━━━━━━━●●━━━━●●━━━━━━━━━━━━━●●━━<br>Windows下:<br>Ctrl + W 关闭当前的窗口<br>Ctrl + S 保存<br>Ctrl + F4 关闭当前浏览器页面<br>Ctrl + T 新建浏览器页面<br>Ctrl + Shift + N 创建一个新的文件夹<br>Ctrl + Alt + Del 可以锁定电脑<br>Ctrl + Z 撤销<br>Ctrl + Y 取消撤销<br>Shift + F10<br>/Application键 鼠标右键<br>Shift + F10 + E 解压缩文件</p><p>win + E 打开文件管理器<br>win + D 快速切换到桌面<br>win+ ↑ 最大化窗口<br>win + M 最小化所有窗口<br>win+ ↓ 最小化窗口</p><p>目的快捷键:<br>Ctrl + D 删除<br>Alt + F4 关闭活动项目或者退出活动程序<br>Alt + ↑ 在Windows资源管理器中查看上一级文件夹<br>Alt + ← 文件管理器到历史上次目录<br>Alt + → 文件管理器到历史下次目录<br>F2 重新命名文件<br>━━●●━━━━━━━━━━━━━●●━━━━━━━━━━━━━●●━━━━●●━━━━━━━━━━━━━●●━━<br>chrome快捷键:<br>Alt + ← 后退<br>Alt + → 前进<br>Ctrl + Shift + T 撤销关闭网页<br>Ctrl + Tab 切换标签<br>Ctrl + 数字 切换到指定的标签<br>━━●●━━━━━━━━━━━━━●●━━━━━━━━━━━━━●●━━━━●●━━━━━━━━━━━━━●●━━<br>vscode快捷键<br>Ctrl + P 跳转文件<br>━━●●━━━━━━━━━━━━━●●━━━━━━━━━━━━━●●━━━━●●━━━━━━━━━━━━━●●━━<br>myEclipse快捷键<br>Ctrl + T 查看接口的实现类<br>F3 查看方法声明<br>F4 查看类的继承关系<br>Ctrl + Alt + H 查看方法被调用的堆栈<br>Alt + ← 光标回到历史上次页面<br>Alt + → 光标回到历史下次页面<br>Ctrl + Q 光标回到上一次的位置<br>━━●●━━━━━━━━━━━━━●●━━━━━━━━━━━━━●●━━━━●●━━━━━━━━━━━━━●●━━<br>PotPlayer<br>C 加速播放<br>X 减速播放</p>]]></content>
<categories>
<category> 工具使用 </category>
</categories>
</entry>
<entry>
<title>《阿里巴巴Java开发手册》学习笔记</title>
<link href="/2019/10/13/a-li-ba-ba-java-kai-fa-shou-ce-xue-xi-bi-ji/"/>
<url>/2019/10/13/a-li-ba-ba-java-kai-fa-shou-ce-xue-xi-bi-ji/</url>
<content type="html"><![CDATA[<p><img src="/images/%E3%80%8A%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/0.jpg" alt></p><p>2017年,阿里官方推出一套<strong>Java编程规范</strong>:<a href="https://files.cnblogs.com/files/han-1034683568/阿里巴巴Java开发手册终极版v1.3.0.pdf" target="_blank" rel="noopener">《阿里巴巴Java开发手册(终极版)》</a>,这套Java统一规范标准将有助于提高行业编码规范化水平,帮助行业人员提高开发质量和效率、大大降低代码维护成本。推出之后,在CSDN,InfoQ,知乎等网站引起广泛讨论,口碑收获颇丰。本文旨在抛砖引玉,共同学习这套阿里巴巴近万名开发同学集体智慧的结晶写出来的编程规范。</p><h2 id="整体大纲"><a href="#整体大纲" class="headerlink" title="整体大纲"></a>整体大纲</h2><p><img src="/images/%E3%80%8A%E9%98%BF%E9%87%8C%E5%B7%B4%E5%B7%B4Java%E5%BC%80%E5%8F%91%E6%89%8B%E5%86%8C%E3%80%8B%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/1.png" alt="整体大纲"></p><p>相比起多年前Google的编程规范,阿里巴巴发布的Java开发手册之所以叫做<strong>”开发手册”</strong>,而不是像Google那样叫做“Style Guide(样式风格)”,是因为它不仅仅局限于<strong>样式风格</strong>这一方面,而是以开发者为中心视角,划分为<strong>编程规约、异常日志规约、MySQL规约、工程规约、安全规约</strong>五大块,再根据内容特征,细分成若干目录。根据约束力强弱和故障敏感性,规约依次分为强制、推荐、参考三大类。</p><p>该开发手册每一条都值得学习,这里只列出其中颇有感受的几点来共同学习一下。</p><h2 id="命名规约"><a href="#命名规约" class="headerlink" title="命名规约"></a>命名规约</h2><blockquote><p>【强制】 POJO 类中布尔类型的变量,都不要加 is ,否则部分框架解析会引起序列化错误。<br><strong>反例:</strong>定义为基本数据类型 boolean isSuccess;的属性,它的方法也是 isSuccess() ,RPC框架在反向解析的时候,“以为”对应的属性名称是 success ,导致属性获取不到,进而抛出异常。</p></blockquote><p>对于isSuccess这个布尔变量,IDE在自动生成getter,setter方法时,<strong>生成的方法名称是isSuccess和setSuccess,而不是isIsSuccess和setIsSuccess,</strong></p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">public</span> <span class="token keyword">class</span> <span class="token class-name">DemoPOJO</span><span class="token punctuation">{</span> <span class="token keyword">boolean</span> active<span class="token punctuation">;</span> <span class="token keyword">boolean</span> isSuccess<span class="token punctuation">;</span> <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">isActive</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> active<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setActive</span><span class="token punctuation">(</span><span class="token keyword">boolean</span> active<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">this</span><span class="token punctuation">.</span>active <span class="token operator">=</span> active<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">boolean</span> <span class="token function">isSuccess</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> isSuccess<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token keyword">public</span> <span class="token keyword">void</span> <span class="token function">setSuccess</span><span class="token punctuation">(</span><span class="token keyword">boolean</span> success<span class="token punctuation">)</span> <span class="token punctuation">{</span> isSuccess <span class="token operator">=</span> success<span class="token punctuation">;</span> <span class="token punctuation">}</span> <span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>除了RPC框架反向解析会有问题,类型情况反向解析时也会有问题:<br>比如SpringMVC在接收前端页面传回一个”isSuccess”布尔变量时,解析成为POJO对象时,找不到setIsSuccess方法,导致POJO的属性不能正确获取,而且比较坑的是,这种情况不容易发现异常,最终解析后的属性值是拿到布尔类型默认值false。</p><h2 id="常量定义"><a href="#常量定义" class="headerlink" title="常量定义"></a>常量定义</h2><blockquote><p>【推荐】不要使用一个常量类维护所有常量,应该按常量功能进行归类,分开维护。如:缓存相关的常量放在类: CacheConsts 下 ; 系统配置相关的常量放在类: ConfigConsts 下。<br><strong>说明:</strong>大而全的常量类,非得使用查找功能才能定位到修改的常量,不利于理解和维护。</p></blockquote><p>在写代码的时候,从易用性和可维护性出发,不推荐一个类内容太多,大而全的类,改起来牵一发动全身,一个类只负责一类功能,不要涵盖太多方面。</p><h2 id="OOP规约"><a href="#OOP规约" class="headerlink" title="OOP规约"></a>OOP规约</h2><blockquote><p>【强制】所有的覆写方法,必须加@ Override 注解。<br><strong>反例:</strong> getObject() 与 get 0 bject() 的问题。一个是字母的 O ,一个是数字的 0,加@ Override可以准确判断是否覆盖成功。另外,如果在抽象类中对方法签名进行修改,其实现类会马上编译报错。</p></blockquote><p>尽量用最安全的方式写代码,尽量让问题在<strong>编译期暴露</strong>,而不是运行期暴露</p><hr><blockquote><p>【强制】 Object 的 equals 方法容易抛空指针异常,应使用常量或确定有值的对象来调用equals 。<br><strong>正例:</strong>“ test “ .equals(object);<br><strong>反例:</strong> object.equals( “ test “ );<br><strong>说明:</strong>推荐使用 java . util . Objects # equals (JDK 7 引入的工具类 )</p></blockquote><p>类似的还有使用 “==”符号的时候,写成:<strong>if(100 == sum)</strong>比起写成:<strong>if(sum==100)</strong>,前者更好,因为这样可以避免不小心写成<strong>if(sum=100)</strong>的问题,前者会编译报错</p><hr><blockquote><p>【强制】所有的相同类型的包装类对象之间值的比较,全部使用 equals 方法比较。<br>说明:对于 Integer var =?在-128 至 127 之间的赋值, <strong>Integer 对象是在IntegerCache.cache 产生,会复用已有对象</strong>,这个区间内的 Integer 值可以直接使用==进行判断, 但是这个区间之外的所有数据, 都会在堆上产生, 并不会复用已有对象, 这是一个大坑,推荐使用 <strong>equals</strong> 方法进行判断。</p></blockquote><pre class="line-numbers language-java"><code class="language-java"> Integer integerA1 <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">;</span> Integer integerA2 <span class="token operator">=</span> <span class="token number">100</span><span class="token punctuation">;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>integerA1 <span class="token operator">==</span> integerA2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//true</span> Integer integerB1 <span class="token operator">=</span> <span class="token number">1000</span><span class="token punctuation">;</span> Integer integerB2 <span class="token operator">=</span> <span class="token number">1000</span><span class="token punctuation">;</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>integerB1 <span class="token operator">==</span> integerB2<span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//false</span> System<span class="token punctuation">.</span>out<span class="token punctuation">.</span><span class="token function">println</span><span class="token punctuation">(</span>integerB1<span class="token punctuation">.</span><span class="token function">equals</span><span class="token punctuation">(</span><span class="token number">1000</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token comment" spellcheck="true">//true</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><hr><blockquote><p>【强制】关于基本数据类型与包装数据类型的使用标准如下:<br>1 ) 所有的 POJO 类属性必须使用<strong>包装数据类型</strong>。<br>2 ) RPC 方法的返回值和参数必须使用<strong>包装数据类型</strong>。<br>3 ) 所有的局部变量【推荐】使用<strong>基本数据类型</strong>。<br>说明: POJO 类属性没有初值是提醒使用者在需要使用时,必须自己显式地进行赋值,任何NPE 问题,或者入库检查,都由<strong>使用者来保证</strong>。<br><strong>正例:</strong>数据库的查询结果可能是 null ,因为自动拆箱,用基本数据类型接收有 NPE 风险。<br><strong>反例:</strong>比如显示成交总额涨跌情况,即正负 x %, x 为基本数据类型,调用的 RPC 服务,调用不成功时,返回的是默认值,页面显示:0%,这是不合理的,应该显示成中划线-。所以包装数据类型的 null 值,能够表示额外的信息,如:远程调用失败,异常退出。</p></blockquote><p>NPE问题:空指针异常(Null Pointer Exception)</p><hr><blockquote><p>【强制】定义 DO / DTO / VO 等 POJO 类时,不要设定任何属性<strong>默认值</strong>。<br><strong>反例:</strong> POJO 类的 createTime 默认值为 new Date(); 但是这个属性在数据提取时并没有置入具体值,在更新其它字段时又附带更新了此字段,导致创建时间被修改成当前时间。</p></blockquote><hr><blockquote><p>【强制】 POJO 类必须写 toString 方法。使用 IDE 的中工具: <strong>source > generate</strong> ,toString时,如果继承了另一个 POJO 类,注意在前面加一下 super.toString 。<br><strong>说明:</strong>在方法执行抛出异常时,可以直接调用 POJO 的 toString() 方法打印其属性值,便于排查问题。</p></blockquote><hr><blockquote><p>【推荐】 类内方法定义顺序依次是:公有方法或保护方法 > 私有方法 > getter / setter方法。<br><strong>说明</strong>:公有方法是类的调用者和维护者最关心的方法,首屏展示最好;保护方法虽然只是子类关心,也可能是“模板设计模式”下的核心方法;而私有方法外部一般不需要特别关心,是一个黑盒实现;因为方法信息价值较低,所有 Service 和 DAO 的 getter / setter 方法放在类体最后。</p></blockquote><hr><blockquote><p>【推荐】类成员与方法访问控制从严:<br>1 ) 如果不允许外部直接通过 new 来创建对象,那么构造方法必须是 <strong>private</strong> 。<br>2 ) 工具类不允许有 <strong>public</strong> 或 <strong>default</strong> 构造方法。<br>3 ) 类非 static 成员变量并且与子类共享,必须是 <strong>protected</strong> 。<br>4 ) 类非 static 成员变量并且仅在本类使用,必须是 <strong>private</strong> 。<br>5 ) 类 static 成员变量如果仅在本类使用,必须是 <strong>private</strong> 。<br>6 ) 若是 static 成员变量,必须考虑是否为 <strong>final</strong> 。<br>7 ) 类成员方法只供类内部调用,必须是 <strong>private</strong> 。<br>8 ) 类成员方法只对继承类公开,那么限制为 <strong>protected</strong> 。<br><strong>说明</strong>:任何类、方法、参数、变量,严控访问范围。过宽泛的访问范围,不利于模块解耦。思考:如果是一个 private 的方法,想删除就删除,可是一个 public 的 Service 方法,或者一个 public 的成员变量,删除一下,不得手心冒点汗吗?变量像自己的小孩,尽量在自己的视线内,变量作用域太大,如果无限制的到处跑,那么你会担心的。</p></blockquote><p>1)将构造方法私有化,一般在单例模式下用得比较多,这时使用<strong>getInstance()</strong>方法来获取一个实例对象。<br>2)工具类的话,应该暴露出来的方法是静态方法,使用者静态调用,不必实例化对象。<br>严格控制访问范围,也可以避免属性值被不小心乱改。</p><h2 id="集合处理"><a href="#集合处理" class="headerlink" title="集合处理"></a>集合处理</h2><blockquote><p>【强制】关于 hashCode 和 equals 的处理,遵循如下规则:<br>1) 只要重写 <strong>equals</strong> ,就必须重写 <strong>hashCode</strong> 。<br>2) 因为 Set 存储的是不重复的对象,依据 hashCode 和 equals 进行判断,所以 Set 存储的对象必须重写这两个方法。<br>3) 如果自定义对象做为 Map 的键,那么必须重写 hashCode 和 equals 。<br><strong>正例:</strong> String 重写了 hashCode 和 equals 方法,所以我们可以非常愉快地使用 String 对象作为 key 来使用。</p></blockquote><p>假如类User重写了“equals”,没有重写“hashCode”方法,现在有<strong>userA</strong>和<strong>userB</strong> 2个对象,它们用equals比较时为true,2个对象存入一个Set集合中,Set调用User类默认的hashCode方法,结果在集合中就保存了2个User对象而不是我们想象中的一个 User对象</p><p>关于快速重写hashCode,有许多方法:</p><blockquote><ul><li>Google的Guava项目里有处理hashCode()和equals()的工具类</li><li>com.google.common.base.ObjectsApache Commons也有类似的工具类EqualsBuilder和HashCodeBuilder</li><li>Java 7 也提供了工具类java.util.Objects</li><li>常用IDE都提供hashCode()和equals()的代码生成。<br>(<a href="https://www.zhihu.com/question/28293143/answer/40237535" target="_blank" rel="noopener">来源:知乎</a>)</li></ul></blockquote><hr><blockquote><p>【强制】 ArrayList 的 subList 结果不可强转成 ArrayList , 否则会抛出 ClassCastException异常: java . util . RandomAccessSubList cannot be cast to java . util . ArrayList ;<br><strong>说明:</strong> subList 返回的是 ArrayList 的内部类 SubList ,并不是 ArrayList ,<strong>而是ArrayList 的一个视图</strong>,对于 SubList 子列表的所有操作最终会反映到原列表上。</p></blockquote><blockquote><p>【强制】 在 subList 场景中,高度注意对原集合元素个数的修改,<strong>会导致子列表的遍历、增加、删除</strong>均产生 ConcurrentModificationException 异常。</p></blockquote><blockquote><p>【强制】使用工具类 Arrays . asList() 把数组转换成集合时,不能使用其修改集合相关的方法,它的 add / remove / clear 方法会抛出UnsupportedOperationException 异常。<br><strong>说明:</strong> asList 的返回对象是一个 Arrays 内部类,并没有实现集合的修改方法。 Arrays . asList体现的是<strong>适配器模式</strong>,只是转换接口,后台的数据仍是数组。<br>String[] str = new String[] { “a”, “b” };<br>List list = Arrays.asList(str);<br>第一种情况: list.add(“c”); 运行时<strong>异常</strong>。<br>第二种情况: str[0]= “gujin”; 那么 list.get(0) 也会随之修改</p></blockquote><p>类似问题,可以使用<strong>FindBugs</strong>插件,自动扫描代码中的Bug,这类问题这个插件是可以检测出来的,类似的在对象中<strong>返回属性域的一个引用</strong>时,对该引用的修改会影响原对象的域的。</p><hr><blockquote><p>【推荐】<strong>高度注意</strong> Map 类集合 K / V 能不能存储 null 值的情况,如下表格:</p></blockquote><blockquote><table><thead><tr><th align="left">集合类</th><th align="left">Key</th><th align="left">Value</th><th align="left">Super</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">Hashtable</td><td align="left"><strong>不允许为null</strong></td><td align="left"><strong>不允许 null</strong></td><td align="left">Dictionary</td><td align="left">线程安全</td></tr><tr><td align="left">ConcurrentHashMap</td><td align="left"><strong>不允许为 null</strong></td><td align="left"><strong>不允许为 null</strong></td><td align="left">AbstractMap</td><td align="left">分段锁技术</td></tr><tr><td align="left">TreeMap</td><td align="left"><strong>不允许为null</strong></td><td align="left">允许为 null</td><td align="left">AbstractMap</td><td align="left">线程不安全</td></tr><tr><td align="left">HashMap</td><td align="left">允许为 null</td><td align="left">允许为 null</td><td align="left">AbstractMap</td><td align="left">线程不安全</td></tr><tr><td align="left"><strong>反例:</strong> 由于 HashMap 的干扰,很多人认为 ConcurrentHashMap 是可以置入 null 值,注意存储null 值时会抛出 <strong>NPE 异常</strong>。</td><td align="left"></td><td align="left"></td><td align="left"></td><td align="left"></td></tr></tbody></table></blockquote><hr><blockquote><p>【参考】利用 <strong>Set 元素唯一的特性</strong>,可以快速对一个集合进行去重操作,避免使用 List的contains 方法进行遍历、对比、去重操作。</p></blockquote><h2 id="并发处理"><a href="#并发处理" class="headerlink" title="并发处理"></a>并发处理</h2><blockquote><p>【强制】线程资源必须通过线程池提供,<strong>不允许在应用中自行显式创建线程</strong>。<br><strong>说明:</strong>使用线程池的好处是减少在创建和销毁线程上所花的时间以及系统资源的开销,解决资源不足的问题。如果不使用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。</p></blockquote><blockquote><p>【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。<br>说明: Executors 返回的线程池对象的弊端如下:<br><strong>1) FixedThreadPool 和 SingleThreadPool :</strong><br>允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。<br><strong>2) CachedThreadPool 和 ScheduledThreadPool :</strong><br>允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM 。</p></blockquote><p>OOM - Out of Mana法力耗尽,系统资源耗尽。</p><blockquote><p><strong>《Effective java》第68条:executor和task优先于线程</strong><br>尽量不直接使用线程,现在关键的抽象不在是Thread,它已可是即充当工作单元,又是执行机制。现在工作单元和执行机制是分开的。</p></blockquote><p>也就是说,把任务(task)的定义和任务的通用执行机制分开,任务有2种,Runnable和Callable,执行机制通用的是executor service。这样做的好处就是下次其他地方需要执行任务就可以愉快复用了,在<strong>执行任务策略</strong>方面,也因此可以获得极大的灵活性,比如任务的取消,实现<strong>等待所有任务完成之后才执行下一</strong>步操作等策略。</p><hr><blockquote><p>【强制】 <strong>SimpleDateFormat 是线程不安全的类</strong>,一般不要定义为 static 变量,如果定义为<br>static ,必须加锁,或者使用 DateUtils 工具类。<br>正例:注意线程安全,使用 DateUtils 。亦推荐如下处理:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">private</span> <span class="token keyword">static</span> <span class="token keyword">final</span> ThreadLocal<span class="token operator"><</span>DateFormat<span class="token operator">></span> df <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">ThreadLocal</span><span class="token operator"><</span>DateFormat<span class="token operator">></span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token annotation punctuation">@Override</span> <span class="token keyword">protected</span> DateFormat <span class="token function">initialValue</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token keyword">new</span> <span class="token class-name">SimpleDateFormat</span><span class="token punctuation">(</span><span class="token string">"yyyy-MM-dd"</span><span class="token punctuation">)</span><span class="token punctuation">;</span> <span class="token punctuation">}</span><span class="token punctuation">}</span><span class="token punctuation">;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>说明:如果是 JDK 8 的应用,可以使用 Instant 代替 Date, LocalDateTime 代替 Calendar,<strong>DateTimeFormatter 代替 Simpledateformatter</strong>,官方给出的解释: simple beautiful strong immutable thread - safe </p></blockquote><p>个人推荐的话,需要做日期与字符串的转换,推荐使用joda的<a href="http://joda-time.sourceforge.net/apidocs/org/joda/time/DateTime.html" target="_blank" rel="noopener">DataTime</a>,主要特点是易于使用,功能完整,可以利用它<strong>把JDK Date和Calendar类完全替换掉</strong>,而且仍然能够提供很好的集成。</p><hr><blockquote><p>【强制】对多个资源、数据库表、对象同时加锁时,需要保持一致的<strong>加锁顺序</strong>,否则可能会造成死锁。<br><strong>说明:</strong>线程一需要对表 A 、 B 、 C 依次全部加锁后才可以进行更新操作,那么线程二的加锁顺序也必须是 A 、 B 、 C ,否则可能出现死锁。</p></blockquote><p>死锁问题也可以考虑用<strong>对象锁技术</strong>来减少。</p><hr><blockquote><p>【强制】多线程并行处理定时任务时, Timer 运行多个 TimeTask 时,<strong>只要其中之一没有捕获抛出的异常</strong>,其它任务便会自动终止运行,使用 ScheduledExecutorService 则没有这个问题。</p></blockquote><p>除了要注意捕获异常之外,使用Timer还要注意不要执行需要跑<strong>运行时间过长</strong>的任务,否则在一个定时周期内任务没跑完的会,会导致定时不准,因为一个Timer内部是<strong>单线程在跑所有的TimeTask</strong>。</p><hr><blockquote><p>【参考】 volatile 解决多线程内存不可见问题。对于一写多读,是可以解决变量同步问题,但是如果多写,同样无法解决线程安全问题。如果是 count ++操作,使用如下类实现:</p><pre class="line-numbers language-java"><code class="language-java">AtomicInteger count <span class="token operator">=</span> <span class="token keyword">new</span> <span class="token class-name">AtomicInteger</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>count<span class="token punctuation">.</span><span class="token function">addAndGet</span><span class="token punctuation">(</span> <span class="token number">1</span> <span class="token punctuation">)</span><span class="token punctuation">;</span> <span aria-hidden="true" class="line-numbers-rows"><span></span><span></span></span></code></pre><p> 如果是 JDK 8,推荐使用 LongAdder 对象,比 AtomicLong 性能更好 ( 减少乐观锁的重试次数 ) </p></blockquote><p>多个线程操作同一个对象就会有数据可见性问题,当A线程修改了变量number,修改完成之后,B线程读这个number变量,读到的变量不一定是A线程修改后的变量,什么时候B线程能读到A线程修改后的变量值?<strong>有可能永远都读不到</strong>,这种现象称为<strong>“重排序(Recordering)”</strong><br>要避免数据可见性的问题,最简单的方法是使用内置锁(synchronized)来保护变量,内置锁可以用于确保某个线程以一种<strong>可预测的方法来查看</strong>另一个线程的执行结果。也就是说,刚刚的number变量如果使用内置锁保护的话,可以保证多线程可见性。</p><hr><blockquote><p>【参考】 HashMap 在容量不够进行 resize 时由于高并发可能出现死链,导致 CPU 飙升,在开发过程中注意规避此风险。</p></blockquote><p>HashMap容量不足内部会有一个扩容的操作,比较耗CPU,规避此问题可以在初始化HashMap的时候指定容器大小。</p><h2 id="控制语句"><a href="#控制语句" class="headerlink" title="控制语句"></a>控制语句</h2><blockquote><p>【强制】在一个 switch 块内,每个 case 要么通过 break / return 等来终止,要么注释说明程序将继续执行到哪一个 case 为止 ; 在一个 switch 块内,都必须包含一个 default 语句并且放在最后,即使它什么代码也没有。</p></blockquote><p>同样如果不小心忘记写break,FindBugs插件可以检测出来。</p><hr><blockquote><p>【推荐】推荐尽量少用 else , if - else 的方式可以改写成:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">if</span><span class="token punctuation">(</span>condition<span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">return</span> obj<span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token comment" spellcheck="true">// 接着写 else 的业务逻辑代码;</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>说明:如果非得使用 if()…else if()…else… 方式表达逻辑,<strong>【强制】请勿超过 3 层</strong>,<br>超过请使用状态设计模式。<br><strong>正例:</strong>逻辑上超过 3 层的 if-else 代码可以使用卫语句,或者状态模式来实现。</p></blockquote><p>很多时候,例如做参数校验的时候,尽量避免不要用一堆 else if,那样导致读代码的人要把整个方法的代码都读完才能理解好代码,建议使用:</p><pre class="line-numbers language-java"><code class="language-java"><span class="token keyword">if</span><span class="token punctuation">(</span>conditionA<span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">return</span> <span class="token punctuation">;</span><span class="token punctuation">}</span><span class="token keyword">if</span><span class="token punctuation">(</span>conditionB<span class="token punctuation">)</span><span class="token punctuation">{</span> <span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span> <span class="token keyword">return</span> <span class="token punctuation">;</span><span class="token punctuation">}</span><span aria-hidden="true" class="line-numbers-rows"><span></span><span></span><span></span><span></span><span></span><span></span><span></span><span></span></span></code></pre><p>这样的单独检查就是<strong>卫语句(guard clauses)</strong>.卫语句可以把我们的视线从异常处理中解放出来,<strong>集中精力到正常处理</strong>的代码中。</p><hr><blockquote><p>【推荐】除常用方法(如 getXxx/isXxx)等外,不要在条件判断中执行其它复杂的语句,将复杂逻辑判断的结果赋值给一个<strong>有意义的布尔变量名</strong>,以提高可读性。<br><strong>说明:</strong>很多 if 语句内的逻辑相当复杂,阅读者需要分析条件表达式的最终结果,才能明确什么样的条件执行什么样的语句,那么,如果阅读者分析逻辑表达式错误呢?<br><strong>正例:</strong><br>//伪代码如下<br>boolean existed = (file.open(fileName, “w”) != null) && (…) || (…);<br>if (existed) {<br>…<br>}<br><strong>反例:</strong><br>if ((file.open(fileName, “w”) != null) && (…) || (…)) {<br>…<br>}</p></blockquote><p>用一个有意义的布尔变量名,替代复杂逻辑判断的结果,这个变量名可以起到一个<strong>注释</strong>的作用。如果一个方法过长也是不优雅的,可以考虑重构拆分出几个短一点的私有方法来被调用,<strong>方法的名称就是一个很好的注释</strong>,即时方法只被调用一次这种重构也是有意义的。</p><hr><blockquote><p>【推荐】循环体中的语句要考量性能,以下操作尽量移至循环体外处理,如定义对象、变量、获取数据库连接,进行不必要的 try - catch 操作(这个try - catch是否可以移至循环体外 )。</p></blockquote><p>循环里面不要做耗时过长时间的事情,如果耗时过长,应该扔进去队列里面异步处理。</p><h2 id="异常处理"><a href="#异常处理" class="headerlink" title="异常处理"></a>异常处理</h2><blockquote><p>【强制】不要捕获 Java 类库中定义的继承自 RuntimeException 的运行时异常类,如:<br>IndexOutOfBoundsException / NullPointerException,这类异常由程序员预检查<br>来规避,保证程序健壮性。<br><strong>正例:</strong> if(obj != null) {…}<br><strong>反例:</strong> try { obj.method() } catch(NullPointerException e){…}</p></blockquote><blockquote><p>【推荐】定义时区分 unchecked / checked 异常,避免直接使用 RuntimeException 抛出,<strong>更不允许抛出 Exception 或者 Throwable</strong> ,应使用有业务含义的自定义异常。推荐业界已定义过的自定义异常,如: DAOException / ServiceException 等。</p></blockquote><p>对可恢复的情况使用受检异常,对编程错误使用运行时异常,建议优先使用标准的异常。</p><hr><blockquote><p>【强制】<strong>异常不要用来做流程控制</strong>,条件控制,因为异常的处理效率比条件分支低。</p></blockquote><p><strong>只针对异常的情况才使用异常</strong></p><hr><blockquote><p>【强制】<strong>对大段代码进行 try - catch</strong> ,这是不负责任的表现。 catch 时请分清稳定代码和非稳定代码,稳定代码指的是无论如何不会出错的代码。对于非稳定代码的 catch 尽可能进行区分异常类型,再做对应的异常处理。</p></blockquote><p>把try块的范围限制到最小</p><h2 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h2><p>[图片上传失败…(image-6100e-1522248360126)]<br>还有Mysql规约值得探究,篇幅所限制,这里就不展开了。</p><p>有位架构师曾经在知乎网分享了这样的<a href="https://www.zhihu.com/question/40514188/answer/93405793" target="_blank" rel="noopener">故事:</a><br>2013年stackoverflow第一次公布了部分数据和架构,当时stackoverflow日UV 300W+,PV 2Y+,他们使用了<strong>8台</strong>物理服务器,而这个架构师的公司使用了<strong>近500台</strong>物理服务器,换算到性能上,<strong>硬件资源对比是125:4,**</strong>性能差异是反比,8:250**。</p><p> 公司的架构上没有大问题,各项参数调优,<strong>缓存做了,db分布了,nginx mysql redis的各项参数,也做了性能测试和db test经过n轮调整</strong>,那为什么性能差异还是这么大?最后他总结出来:<br> 主要的业务逻辑开销更多性能是其次,主要的,<strong>是应用层面的程序员造成的:他们处理过许多典型的性能坑:</strong></p><ul><li>db查询没用到索引</li><li>联表查询太复杂性能奇差</li><li>循环里写查询一次连接变几十次</li><li>打开文件句柄 socket连接没有释放</li><li>300k文本直接存到redis里</li><li>curl请求没有加超时</li><li>不同的进程争抢同一个文件资源写日志</li><li>get_image_size()获取图片尺寸(<strong>会把图片文件整个读取到内存里,每个连接都会!</strong>)</li><li>写的扩展内存溢出</li><li>直接读取整个巨大文本文件(<strong>应该逐行方式读取</strong>)</li><li>在代码逻辑循环里直接发短信发邮件(<strong>应该扔到队列异步处理</strong>)</li><li>ip黑名单和关键词过滤直接用文本查找比对(*<em>应该做成hash表 *</em>)</li></ul>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>Java线上服务CPU过载问题快速定位</title>
<link href="/2019/10/13/java-xian-shang-fu-wu-cpu-guo-zai-wen-ti-kuai-su-ding-wei/"/>
<url>/2019/10/13/java-xian-shang-fu-wu-cpu-guo-zai-wen-ti-kuai-su-ding-wei/</url>
<content type="html"><![CDATA[<p><img src="/images/Java%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1CPU%E8%BF%87%E8%BD%BD%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/0.jpg" alt></p><p>* <em>本文介绍一次解决线上服务*</em>Java进程CPU过载问题<strong>的经过,提供了如果定位是</strong>哪个服务进程<strong>导致CPU过载,</strong>哪个线程<strong>导致CPU过载,</strong>哪段代码**导致CPU过载,希望能提供后续遇到类似情况的读者一点思路。 </p><p>#线上环境发现的问题<br>* *测试人员在测试环境发现,机器卡顿,CPU占用率相当高。</p><h1 id="基本解决方案"><a href="#基本解决方案" class="headerlink" title="基本解决方案"></a>基本解决方案</h1><p>* <em>基于工具先定位具体*</em>Java线程<strong>,然后定位</strong>Java线程运行的代码块**。</p><h1 id="解决过程"><a href="#解决过程" class="headerlink" title="解决过程"></a>解决过程</h1><p>* *线上环境使用操作系统是linux,机器需要先设置好JAVA_HOME环境变量。</p><h2 id="1、定位Java线程所属的进程"><a href="#1、定位Java线程所属的进程" class="headerlink" title="1、定位Java线程所属的进程"></a>1、定位Java线程所属的进程</h2><p>命令:<strong>top -c</strong><br>得到显示进程运行信息列表<br>定位得到CPU占用过高的JAVA进程的PID为11721<br><img src="/images/Java%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1CPU%E8%BF%87%E8%BD%BD%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/1.png" alt="定位进程PID"></p><h2 id="2、定位最耗CPU的线程"><a href="#2、定位最耗CPU的线程" class="headerlink" title="2、定位最耗CPU的线程"></a>2、定位最耗CPU的线程</h2><p>命令:<strong>top -Hp 11721</strong><br>11721是进程PID,得到进程的线程运行信息列表<br>定位得到最耗CPU的线程PID有3个,分别是<strong>12687,12678,12555</strong><br><img src="/images/Java%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1CPU%E8%BF%87%E8%BD%BD%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/2.png" alt="定位最耗CPU的线程"></p><h2 id="3、把线程PID转为16进制"><a href="#3、把线程PID转为16进制" class="headerlink" title="3、把线程PID转为16进制"></a>3、把线程PID转为16进制</h2><p>命令:<strong>printf “%x\n” 12687</strong><br>12687是线程id<br>得到CPU占用最高的3个16进制的线程ID:<strong>0x318f、0x3186、0x310b</strong></p><p><img src="/images/Java%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1CPU%E8%BF%87%E8%BD%BD%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/3.png" alt="计算线程ID十六进制"></p><h2 id="4、查看线程运行的堆栈信息"><a href="#4、查看线程运行的堆栈信息" class="headerlink" title="4、查看线程运行的堆栈信息"></a>4、查看线程运行的堆栈信息</h2><p>命令:jstack 11721 | grep ‘0x318f’ -C9 –color<br>11721是进程PID,0x318f是线程的16进制的ID<br>定位得到3个运行堆栈信息,就可以定位问题代码:</p><p><img src="/images/Java%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1CPU%E8%BF%87%E8%BD%BD%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/4.png" alt="0x318f.png"></p><p><img src="/images/Java%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1CPU%E8%BF%87%E8%BD%BD%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/5.png" alt><br><img src="/images/Java%E7%BA%BF%E4%B8%8A%E6%9C%8D%E5%8A%A1CPU%E8%BF%87%E8%BD%BD%E9%97%AE%E9%A2%98%E5%BF%AB%E9%80%9F%E5%AE%9A%E4%BD%8D/6.png" alt="0x310b"></p>]]></content>
<categories>
<category> 运维监控 </category>
</categories>
</entry>
<entry>
<title>自研文章爬取系统方案设计</title>
<link href="/2019/10/13/zi-yan-wen-zhang-pa-qu-xi-tong-fang-an-she-ji/"/>
<url>/2019/10/13/zi-yan-wen-zhang-pa-qu-xi-tong-fang-an-she-ji/</url>
<content type="html"><![CDATA[<p><img src="/images/%E8%87%AA%E7%A0%94%E6%96%87%E7%AB%A0%E7%88%AC%E5%8F%96%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/0.jpg" alt></p><h1 id="1-系统设计分析"><a href="#1-系统设计分析" class="headerlink" title="1 系统设计分析"></a>1 系统设计分析</h1><h2 id="1-1-需求介绍"><a href="#1-1-需求介绍" class="headerlink" title="1.1 需求介绍"></a>1.1 需求介绍</h2><p>目前已有社区内容系统的文章数据较少,文章质量普遍较低,为了丰富文章内容,增加用户粘性,需要想办法从其他地方爬取文章数据,丰富社区内容系统的文章。为此,需要设计一个文章内容爬取系统,负责爬取、清洗、保存文章。</p><h2 id="1-2-系统复杂度分析"><a href="#1-2-系统复杂度分析" class="headerlink" title="1.2 系统复杂度分析"></a>1.2 系统复杂度分析</h2><p>整个系统的复杂度分析如下:</p><h3 id="优先重点考虑"><a href="#优先重点考虑" class="headerlink" title="优先重点考虑"></a>优先重点考虑</h3><ul><li><p>可扩展<br>系统需要适配爬取多个不同的数据平台,有可能是今日头条,微信公众号,等等其他平台,后续还会扩展接入其他平台,需要支持可方便扩展接入。</p></li><li><p>低成本<br>系统要基于现有的有限开发资源开发,整系统使用上尽量减低人力成本,减轻相关内容运营人员的工作量,做到简单实用。</p></li></ul><h3 id="相对重点考虑"><a href="#相对重点考虑" class="headerlink" title="相对重点考虑"></a>相对重点考虑</h3><ul><li><p>高性能<br>因为系统定位是一个内容爬取系统,系统没有直接对接用户,用户感知不到,性能要求不是特别高。</p></li><li><p>高可用<br>同样系统没有直接对接用户,允许系统故障一段时间,待发现后再手工恢复。</p></li></ul><h1 id="2-架构设计"><a href="#2-架构设计" class="headerlink" title="2 架构设计"></a>2 架构设计</h1><h2 id="2-1-整体架构设计"><a href="#2-1-整体架构设计" class="headerlink" title="2.1 整体架构设计"></a>2.1 整体架构设计</h2><p><img src="/images/%E8%87%AA%E7%A0%94%E6%96%87%E7%AB%A0%E7%88%AC%E5%8F%96%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/1.png" alt="系统整体架构设计"></p><p>整体分为数据抽取层、数据中间存储层、数据转换存储层3层,实现数据抽取,转换,存储一系列操作,通过分层结构解耦各层的联系,方便系统后续扩展。</p><p>比如后续想再新增xx网站的爬虫,其他地方不用修改,只需新增爬虫模块即可。<br>或者后续业务库表发生变更,其他模块不用修改,只需要修改文章数据转换审核系统。</p><h3 id="2-2-数据抽取层——爬虫模块设计"><a href="#2-2-数据抽取层——爬虫模块设计" class="headerlink" title="2.2 数据抽取层——爬虫模块设计"></a>2.2 数据抽取层——爬虫模块设计</h3><p>数据抽取层由具体对接各个不同数据源的爬虫组成,这些爬虫负责从数据源爬取定时数据,做简单数据处理后把数据保存到中间存储层。模块基本设计如下:</p><p><img src="/images/%E8%87%AA%E7%A0%94%E6%96%87%E7%AB%A0%E7%88%AC%E5%8F%96%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/2.png" alt="爬虫模块设计"><br>整个模块基于Python3的Scrapy框架,主要由3个基本对象组成:</p><ul><li><p>Downloader 下载器<br>http请求工具,负责发送http request到被爬取的网站,把网站返回的Responses传给Spiders</p></li><li><p>Spiders 数据解析器<br>用于数据解析,主要负责处理所有Responses,从中分析提取数据,获取Item字段需要的数据</p></li><li><p>Item Pipeline 数据存储器<br>用于数据存储,把数据保存到中间存储层,同时负责内容去重。<br>内容去重也需要基于数据存储,因为不同网站的爬虫的去重字段,去重逻辑不一致,这里不归到中间存储,还是属于网站爬虫的部分。</p></li></ul><p>实际开发去重方案可以根据实际情况灵活调整优化,如果知道内容已经爬取过,可以灵活制定策略避免重复爬取。</p><h3 id="2-3-数据中间存储层设计"><a href="#2-3-数据中间存储层设计" class="headerlink" title="2.3 数据中间存储层设计"></a>2.3 数据中间存储层设计</h3><p>负责临时存储文章数据,保存文章最基本数据,包括标题,内容等信息。存储备选方案:</p><ul><li>MySQL</li><li>MongoDB</li><li>MQ(Kafka等)<br>这里考虑简单满足需求,容易运维的话,MySQL 能满足当前业务需求,所以考虑选择MySQL。</li></ul><h3 id="2-4-数据存储转换层设计"><a href="#2-4-数据存储转换层设计" class="headerlink" title="2.4 数据存储转换层设计"></a>2.4 数据存储转换层设计</h3><p>负责从中间存储层读取数据,然后进行数据报文转换,例如加文章封面图,待运营人员审核通过后把文章写入文章业务库表。基本设计如下:</p><p><img src="/images/%E8%87%AA%E7%A0%94%E6%96%87%E7%AB%A0%E7%88%AC%E5%8F%96%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/3.png" alt="数据转换审核系统"></p><ul><li><p>审核、编辑<br>这一步需要运营编辑人员干预,在页面上对文章进行审核,编辑,审核通过才进行后续操作。</p></li><li><p>图片地址转储<br>将文章中的图片下载下来,重新上传到CDN下。</p></li><li><p>字段填充转换<br>文章封面图,文章创建人,文章分类信息等字段填充转换</p></li><li><p>审核文章存储<br>保存待审核,审核通过的文章,后续将写入业务库表。</p></li></ul><h1 id="3-总结"><a href="#3-总结" class="headerlink" title="3 总结"></a>3 总结</h1><p>整个系统架构设计遵循如下原则:</p><ul><li>合适原则——合适优于业界领先</li><li>简单原则——简单优于复杂</li><li>演化原则——演化优于一步到位</li></ul><p>架构设计的目的在于解决系统复杂度问题,真正优秀的架构都是企业当前人力、条件、业务等各种约束下设计出来的,能够合理地将资源整合在一起并发挥出最大功效、并且能够快速落地。</p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://cuiqingcai.com/3472.html" target="_blank" rel="noopener">小白进阶之Scrapy第一篇</a><br><a href="https://scrapy-chs.readthedocs.io/zh_CN/0.24/intro/tutorial.html#scrapy" target="_blank" rel="noopener">scrapy入门教程</a></p><p><img src="/images/%E8%87%AA%E7%A0%94%E6%96%87%E7%AB%A0%E7%88%AC%E5%8F%96%E7%B3%BB%E7%BB%9F%E6%96%B9%E6%A1%88%E8%AE%BE%E8%AE%A1/4.png" alt></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>设计模式-创建型模式</title>
<link href="/2019/10/13/she-ji-mo-shi-chuang-jian-xing-mo-shi/"/>
<url>/2019/10/13/she-ji-mo-shi-chuang-jian-xing-mo-shi/</url>
<content type="html"><![CDATA[<p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/0.jpg" alt></p><h1 id="原型模式-Prototype"><a href="#原型模式-Prototype" class="headerlink" title="原型模式(Prototype )"></a>原型模式(Prototype )</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/1.png" alt="原型模式"><br><strong>意图:</strong>用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。<br><strong>主要解决:</strong>在运行期建立和删除原型。</p><h1 id="建造者模式-Builder"><a href="#建造者模式-Builder" class="headerlink" title="建造者模式(Builder)"></a>建造者模式(Builder)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/2.png" alt="建造者模式"><strong>意图:</strong>将一个复杂的构建与其表示相分离,使得同样的构建过程可以创建不同的表示。<br><strong>主要解决:</strong>主要解决在软件系统中,有时候面临着”一个复杂对象”的创建工作,其通常由各个部分的子对象用一定的算法构成;由于需求的变化,这个复杂对象的各个部分经常面临着剧烈的变化,但是将它们组合在一起的算法却相对稳定。</p><h1 id="单例模式-Sington"><a href="#单例模式-Sington" class="headerlink" title="单例模式(Sington)"></a>单例模式(Sington)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/3.png" alt="单例模式"><br><strong>意图:</strong>保证一个类仅有一个实例,并提供一个访问它的全局访问点。<br><strong>主要解决:</strong>一个全局使用的类频繁地创建与销毁。</p><h1 id="抽象工厂模式-Abstract-Factory"><a href="#抽象工厂模式-Abstract-Factory" class="headerlink" title="抽象工厂模式(Abstract Factory)"></a>抽象工厂模式(Abstract Factory)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/4.png" alt="抽象工厂模式"><strong>意图:</strong>提供一个创建一系列相关或相互依赖对象的接口,而无需指定它们具体的类。<br><strong>主要解决:</strong>主要解决接口选择的问题。</p><h1 id="工厂方法模式-Factory-Method"><a href="#工厂方法模式-Factory-Method" class="headerlink" title="工厂方法模式(Factory Method)"></a>工厂方法模式(Factory Method)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/5.png" alt="工厂方法模式"><strong>意图:</strong>暴露一个创建对象的方法,允许客户端创建不同工厂来生产产品。<br><strong>主要解决:</strong>创建对象的接口,让子类去决定具体实例化的对象,把简单的内部逻辑判断移到了客户端代码。</p><h1 id="简单工厂模式-Static-Factory-Method"><a href="#简单工厂模式-Static-Factory-Method" class="headerlink" title="简单工厂模式(Static Factory Method)"></a>简单工厂模式(Static Factory Method)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E5%88%9B%E5%BB%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/6.png" alt="简单工厂模式">又称为静态工厂<br><strong>意图:</strong>由一个工厂对象决定创建出哪一种产品类的实例。</p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>设计模式-行为型模式</title>
<link href="/2019/10/13/she-ji-mo-shi-xing-wei-xing-mo-shi/"/>
<url>/2019/10/13/she-ji-mo-shi-xing-wei-xing-mo-shi/</url>
<content type="html"><![CDATA[<p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/0.jpg" alt></p><h1 id="中介者模式-Mediator"><a href="#中介者模式-Mediator" class="headerlink" title="中介者模式(Mediator)"></a>中介者模式(Mediator)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/1.png" alt="中介者模式"><strong>意图:</strong>用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。<br><strong>主要解决:</strong>对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。</p><h1 id="命令模式-Command"><a href="#命令模式-Command" class="headerlink" title="命令模式(Command)"></a>命令模式(Command)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/2.png" alt="命令模式"><br><strong>意图:</strong>将一个请求封装成一个对象,从而使得行为请求者和行为实现者解耦合。<br><strong>主要解决:</strong>在软件系统中,行为请求者与行为实现者通常是一种紧耦合的关系,但某些场合,比如需要对行为进行记录、撤销或重做、事务等处理时,这种无法抵御变化的紧耦合的设计就不太合适。</p><h1 id="备忘录模式-Memento"><a href="#备忘录模式-Memento" class="headerlink" title="备忘录模式(Memento)"></a>备忘录模式(Memento)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/3.png" alt="备忘录模式"><br><strong>意图:</strong>在不破坏封装性的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态。<br><strong>主要解决:</strong>所谓备忘录模式就是在不破坏封装的前提下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样可以在以后将对象恢复到原先保存的状态。</p><h1 id="模板方法模式-Template"><a href="#模板方法模式-Template" class="headerlink" title="模板方法模式(Template)"></a>模板方法模式(Template)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/4.png" alt="模板方法模式"><br><strong>意图:</strong>定义一个操作中的算法的骨架,而将一些步骤延迟到子类中。模板方法使得子类可以不改变一个算法的结构即可重定义该算法的某些特定步骤。<br><strong>主要解决:</strong>一些方法通用,却在每一个子类都重新写了这一方法。</p><h1 id="状态模式-State"><a href="#状态模式-State" class="headerlink" title="状态模式(State)"></a>状态模式(State)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/5.png" alt="状态模式"><strong>意图:</strong>将特定状态相关的逻辑分散到一些类的状态类中<br><strong>主要解决:</strong>对象的行为依赖于它的状态(属性),并且可以根据它的状态改变而改变它的相关行为。</p><h1 id="策略模式-Strategy"><a href="#策略模式-Strategy" class="headerlink" title="策略模式(Strategy)"></a>策略模式(Strategy)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/6.png" alt="策略模式"><strong>意图:</strong>定义一系列的算法,把它们一个个封装起来, 并且使它们可相互替换。<br><strong>主要解决:</strong>在有多种算法相似的情况下,使用 if…else 所带来的复杂和难以维护。</p><h1 id="观察者模式-Observer"><a href="#观察者模式-Observer" class="headerlink" title="观察者模式(Observer)"></a>观察者模式(Observer)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/7.png" alt="观察者模式"><br><strong>意图:</strong>定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。<br><strong>主要解决:</strong>一个对象状态改变给其他对象通知的问题,而且要考虑到易用和低耦合,保证高度的协作。</p><h1 id="解释器模式-Interpreter"><a href="#解释器模式-Interpreter" class="headerlink" title="解释器模式(Interpreter )"></a>解释器模式(Interpreter )</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/8.png" alt="解释器模式"><strong>意图:</strong>给定一个语言,定义它的文法表示,并定义一个解释器,这个解释器使用该标识来解释语言中的句子。<br><strong>主要解决:</strong>对于一些固定文法构建一个解释句子的解释器。</p><h1 id="访问者模式-Visitor"><a href="#访问者模式-Visitor" class="headerlink" title="访问者模式(Visitor)"></a>访问者模式(Visitor)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/9.png" alt="访问者模式"><strong>意图:</strong>主要将数据结构与数据访问操作分离。<br><strong>主要解决:</strong>稳定的数据结构和易变的访问操作耦合问题。</p><h1 id="责任链模式-Chain-of-Responsibility"><a href="#责任链模式-Chain-of-Responsibility" class="headerlink" title="责任链模式(Chain of Responsibility)"></a>责任链模式(Chain of Responsibility)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/10.png" alt="责任链模式"><strong>意图:</strong>避免请求发送者与接收者耦合在一起,让多个对象都有可能接收请求,将这些对象连接成一条链,并且沿着这条链传递请求,直到有对象处理它为止。<br><strong>主要解决:</strong>职责链上的处理者负责处理请求,客户只需要将请求发送到职责链上即可,无须关心请求的处理细节和请求的传递,所以职责链将请求的发送者和请求的处理者解耦了。</p><h1 id="迭代器模式-Iterator"><a href="#迭代器模式-Iterator" class="headerlink" title="迭代器模式(Iterator)"></a>迭代器模式(Iterator)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E8%A1%8C%E4%B8%BA%E5%9E%8B%E6%A8%A1%E5%BC%8F/11.png" alt="迭代器模式"><strong>意图:</strong>提供一种方法顺序访问一个聚合对象中各个元素, 而又无须暴露该对象的内部表示。<br><strong>主要解决:</strong>不同的方式来遍历整个整合对象。</p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>设计模式-结构型模式</title>
<link href="/2019/10/13/she-ji-mo-shi-jie-gou-xing-mo-shi/"/>
<url>/2019/10/13/she-ji-mo-shi-jie-gou-xing-mo-shi/</url>
<content type="html"><![CDATA[<p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/0.jpg" alt></p><h1 id="享元模式-Flyweight"><a href="#享元模式-Flyweight" class="headerlink" title="享元模式(Flyweight)"></a>享元模式(Flyweight)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/1.png" alt="享元模式"><br><strong>意图:</strong>运用共享技术有效地支持大量细粒度的对象。<br><strong>主要解决:</strong>在有大量对象时,有可能会造成内存溢出,我们把其中共同的部分抽象出来,如果有相同的业务请求,直接返回在内存中已有的对象,避免重新创建。</p><h1 id="代理模式-Proxy"><a href="#代理模式-Proxy" class="headerlink" title="代理模式(Proxy)"></a>代理模式(Proxy)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/2.png" alt="代理模式"><strong>意图:</strong>为其他对象提供一种代理以控制对这个对象的访问。<br><strong>主要解决:</strong>在直接访问对象时带来的问题,比如说:要访问的对象在远程的机器上。在面向对象系统中,有些对象由于某些原因(比如对象创建开销很大,或者某些操作需要安全控制,或者需要进程外的访问),直接访问会给使用者或者系统结构带来很多麻烦,我们可以在访问此对象时加上一个对此对象的访问层。</p><h1 id="桥接模式-Bridge"><a href="#桥接模式-Bridge" class="headerlink" title="桥接模式(Bridge)"></a>桥接模式(Bridge)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/3.png" alt="桥接模式"><strong>意图:</strong>将抽象部分与实现部分分离,使它们都可以独立的变化。<br><strong>主要解决:</strong>在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活</p><h1 id="组合模式-Composite"><a href="#组合模式-Composite" class="headerlink" title="组合模式(Composite)"></a>组合模式(Composite)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/4.png" alt="组合模式"><strong>意图:</strong>将对象组合成树形结构以表示”部分-整体”的层次结构。组合模式使得用户对单个对象和组合对象的使用具有一致性。<br><strong>主要解决:</strong>它在我们树型结构的问题中,模糊了简单元素和复杂元素的概念,客户程序可以向处理简单元素一样来处理复杂元素,从而使得客户程序与复杂元素的内部结构解耦。</p><h1 id="装饰器模式-Decorator"><a href="#装饰器模式-Decorator" class="headerlink" title="装饰器模式(Decorator)"></a>装饰器模式(Decorator)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/5.png" alt="装饰器模式"><strong>意图:</strong>动态地给一个对象添加一些额外的职责。就增加功能来说,装饰器模式相比生成子类更为灵活。<br><strong>主要解决:</strong>一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。</p><h1 id="适配器模式-Adapter"><a href="#适配器模式-Adapter" class="headerlink" title="适配器模式(Adapter)"></a>适配器模式(Adapter)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/6.png" alt="适配器模式"><strong>意图:</strong>将一个类的接口转换成客户希望的另外一个接口。适配器模式使得原本由于接口不兼容而不能一起工作的那些类可以一起工作。<br><strong>主要解决:</strong>主要解决在软件系统中,常常要将一些”现存的对象”放到新的环境中,而新环境要求的接口是现对象不能满足的。</p><h1 id="门面模式-Facade"><a href="#门面模式-Facade" class="headerlink" title="门面模式(Facade)"></a>门面模式(Facade)</h1><p><img src="/images/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F-%E7%BB%93%E6%9E%84%E5%9E%8B%E6%A8%A1%E5%BC%8F/7.png" alt="门面模式"><br><strong>意图:</strong>为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。<br><strong>主要解决:</strong>降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。</p>]]></content>
<categories>
<category> Java基础 </category>
</categories>
</entry>
<entry>
<title>高性能数据库集群——分库分表</title>
<link href="/2019/10/13/gao-xing-neng-shu-ju-ku-ji-qun-fen-ku-fen-biao/"/>
<url>/2019/10/13/gao-xing-neng-shu-ju-ku-ji-qun-fen-ku-fen-biao/</url>
<content type="html"><![CDATA[<p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/0.jpg" alt></p><pre><code>作者 陈彩华文章转载交流请联系 [email protected]</code></pre><p>最近学习了阿里资深技术专家李运华的架构设计关于分库分表的教程,颇有收获,总结一下。</p><p>本文主要介绍高性能数据库集群分库分表相关理论,基本架构,涉及的复杂度问题以及常见解决方案。</p><h1 id="分库分表概述"><a href="#分库分表概述" class="headerlink" title="分库分表概述"></a>分库分表概述</h1><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/1.png" alt="分库分表概述"><br>读写分离分散数据库读写操作压力,分库分表<strong>分散存储压力</strong></p><h1 id="适用场景"><a href="#适用场景" class="headerlink" title="适用场景"></a>适用场景</h1><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/2.png" alt="适用场景"></p><blockquote><p><strong>类似读写分离,分库分表也是确定没有其他优化空间之后才采取的优化方案</strong>。那如果业务真的发展很快岂不是很快要进行分库分表了?那为何不一开始就设计好呢?</p></blockquote><blockquote><p>按照架构设计的“三原则”(<strong>简单原则,合适原则,演化原则</strong>),简单分析一下:</p></blockquote><blockquote><p>首先,这里的“如果”事实上发生的概率比较低,做10个业务有一个业务能活下去就很不错了,更何况快速发展,和中彩票的概率差不多。<strong>如果我们每个业务上来就按照淘宝、微信的规模去做架构设计,不但会累死自己,还会害死业务</strong>。</p></blockquote><blockquote><p>其次,<strong>如果业务真的发展很快,后面进行分库分表也不迟</strong>。因为业务发展好,相应的资源投入就会加大,可以投入更多的人和更多的钱,那业务分库带来的代码和业务复杂问题就可以通过加人来解决,成本问题也可以通过增加资金来解决。</p></blockquote><h1 id="业务分库"><a href="#业务分库" class="headerlink" title="业务分库"></a>业务分库</h1><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/3.png" alt="业务分库"><br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/4.jpg" alt="示例"></p><h1 id="业务分表"><a href="#业务分表" class="headerlink" title="业务分表"></a>业务分表</h1><h2 id="业务分表概述"><a href="#业务分表概述" class="headerlink" title="业务分表概述"></a>业务分表概述</h2><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/5.png" alt="业务分表"><br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/6.jpg" alt="拆分方式"></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><ul><li>路由问题</li></ul><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/7.png" alt="路由问题"></p><ul><li>数据库操作问题</li></ul><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/8.png" alt="数据库操作问题"></p><h1 id="实现方法"><a href="#实现方法" class="headerlink" title="实现方法"></a>实现方法</h1><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8/9.png" alt="实现方法"><br>类似<a href="https://juejin.im/post/5b3753b66fb9a00e65267a55" target="_blank" rel="noopener">读写分离</a>,具体实现也是“程序代码封装”和“中间件封装”,但具体实现复杂一些,因为还有要判断SQL中具体操作的表,具体操作(例如count、order by、group by等),根据具体操作做不同的处理。</p><p>参考</p><p><a href="https://time.geekbang.org/column/intro/81?code=OK4eM0TBPTKGPRCzcZdzIeXjPACLfY3KCzATXOSWzXE%3D" target="_blank" rel="noopener">从0开始学架构 —— 李运华 </a></p><p><a href="https://juejin.im/post/5b3753b66fb9a00e65267a55" target="_blank" rel="noopener">《浅谈高性能数据库集群——读写分离》—— 陈彩华</a></p><p><a href="https://juejin.im/post/5b2c6669e51d4558c91ba776" target="_blank" rel="noopener">《架构设计方法初探》 —— 陈彩华</a></p><p><a href="http://kuaibao.qq.com/s/20180506G0J38H00?refer=spider" target="_blank" rel="noopener">《分库分表、主从、读写分离》</a></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>高性能数据库集群——读写分离</title>
<link href="/2019/10/13/gao-xing-neng-shu-ju-ku-ji-qun-du-xie-fen-chi/"/>
<url>/2019/10/13/gao-xing-neng-shu-ju-ku-ji-qun-du-xie-fen-chi/</url>
<content type="html"><![CDATA[<p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/0.jpg" alt></p><pre><code>作者 陈彩华文章转载交流请联系 [email protected]</code></pre><p>最近学习了阿里资深技术专家李运华的架构设计关于读写分离的教程,颇有收获,总结一下。</p><p>本文主要介绍高性能数据库集群读写分离相关理论,基本架构,涉及的复杂度问题以及常见解决方案。</p><h1 id="1-读写分离概述"><a href="#1-读写分离概述" class="headerlink" title="1 读写分离概述"></a>1 读写分离概述</h1><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/1.png" alt="读写分离概述"></p><p>基本架构图:<br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/2.jpg" alt="基本架构图.jpg"></p><h1 id="2-适用场景"><a href="#2-适用场景" class="headerlink" title="2 适用场景"></a>2 适用场景</h1><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/3.png" alt="适用场景.png"><br><strong>读写分离不是银弹,并不是一有性能问题就上读写分离,</strong>而是应该先优化,例如优化慢查询,调整不合理的业务逻辑,引入缓存查询等只有确定系统没有优化空间后才考虑读写分离集群</p><h1 id="3-引入的系统复杂度问题"><a href="#3-引入的系统复杂度问题" class="headerlink" title="3 引入的系统复杂度问题"></a>3 引入的系统复杂度问题</h1><h2 id="问题一-主从复制延迟"><a href="#问题一-主从复制延迟" class="headerlink" title="问题一 主从复制延迟"></a>问题一 主从复制延迟</h2><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/4.png" alt="主从复制延迟.png"></p><h2 id="问题二-分配机制"><a href="#问题二-分配机制" class="headerlink" title="问题二 分配机制"></a>问题二 分配机制</h2><p>如何将读写操作区分开来,然后访问不同的数据库服务器?</p><h3 id="解决方案1-客户端程序代码封装实现"><a href="#解决方案1-客户端程序代码封装实现" class="headerlink" title="解决方案1 客户端程序代码封装实现"></a>解决方案1 客户端程序代码封装实现</h3><p><strong>基本架构图</strong><br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/5.jpg" alt="程序代码封装实现分配基本架构图"><br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/6.png" alt="程序代码封装"><br><strong>业界开源实现</strong></p><ul><li>Sharding-JDBC<br>定位为轻量级Java框架,在Java的JDBC层提供的额外服务。 它使用客户端直连数据库,以jar包形式提供服务,无需额外部署和依赖,可理解为增强版的JDBC驱动,完全兼容JDBC和各种ORM框架。</li></ul><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/7.png" alt="Sharding-JDBC基本架构图"></p><ul><li>淘宝TDDL<br>淘宝根据自身业务需求研发了 TDDL ( Taobao Distributed Data Layer )框架,主要用于解决 分库分表场景下的访问路由(持久层与数据访问层的配合)以及异构数据库之间的数据同步 ,它是一个基于集中式配置的 JDBC DataSource 实现,具有分库分表、 Master/Salve 、动态数据源配置等功能。</li></ul><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/8.png" alt="淘宝TDDL基本架构图"></p><h3 id="解决方案2-服务端中间件封装"><a href="#解决方案2-服务端中间件封装" class="headerlink" title="解决方案2 服务端中间件封装"></a>解决方案2 服务端中间件封装</h3><p><strong>基本架构图</strong></p><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/9.jpg" alt="服务端中间件封装实现分配基本架构图"><br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/10.png" alt="服务端中间件封装"><br><strong>业界开源实现</strong></p><ul><li>MySQL官方推荐的MySQL Router</li></ul><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/11.png" alt="MySQL Router架构图"><br>MySQL Router是轻量级的中间件,可在应用程序和任何后端MySQL服务器之间提供透明路由。它可以用于各种各样的用例,例如通过有效地将数据库流量路由到适当的后端MySQL服务器来提供高可用性和可伸缩性。可插拔架构还使开发人员能够扩展MySQL Router以用于自定义用例。</p><p>基于MySQL Router可以实现读写分离,故障自动切换,负载均衡,连接池等功能。</p><ul><li>MySQL官方提供的MySQL Proxy<br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/12.png" alt="MySQL Proxy"></li><li>360开源的Atlas</li></ul><p><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/13.jpg" alt="Atlas架构图形象表示"><br><img src="/images/%E9%AB%98%E6%80%A7%E8%83%BD%E6%95%B0%E6%8D%AE%E5%BA%93%E9%9B%86%E7%BE%A4%E2%80%94%E2%80%94%E8%AF%BB%E5%86%99%E5%88%86%E7%A6%BB/14.jpg" alt="Atlas总体架构"><br>Atlas是由平台部基础架构团队开发维护的一个基于MySQL协议的数据中间层项目。它是在mysql-proxy的基础上,对其进行了优化,增加了一些新的功能特性。</p><h3 id="常见的开源数据库中间件对比"><a href="#常见的开源数据库中间件对比" class="headerlink" title="常见的开源数据库中间件对比"></a>常见的开源数据库中间件对比</h3><table><thead><tr><th align="left">功能</th><th align="center">Sharding-JDBC</th><th align="center">TDDL</th><th align="center">Amoeba</th><th align="center">Cobar</th><th align="center">MyCat</th></tr></thead><tbody><tr><td align="left">基于客户端还是服务端</td><td align="center">客户端</td><td align="center">客户端</td><td align="center">服务端</td><td align="center">服务端</td><td align="center">服务端</td></tr><tr><td align="left">分库分表</td><td align="center">有</td><td align="center">有</td><td align="center">有</td><td align="center">有</td><td align="center">有</td></tr><tr><td align="left">MySQL交互协议</td><td align="center">JDBC Driver</td><td align="center">JDBC Driver</td><td align="center">前端用NIO,后端用JDBC Driver</td><td align="center">前端用NIO,后端用BIO</td><td align="center">前后端均用NIO</td></tr><tr><td align="left">支持的数据库</td><td align="center">任意</td><td align="center">任意</td><td align="center">任意</td><td align="center">MySQL</td><td align="center">任意</td></tr></tbody></table><p>参考</p><p><a href="https://time.geekbang.org/column/intro/81?code=OK4eM0TBPTKGPRCzcZdzIeXjPACLfY3KCzATXOSWzXE%3D" target="_blank" rel="noopener">从0开始学架构——李运华 </a></p><p><a href="https://blog.csdn.net/u011983531/article/details/78948680" target="_blank" rel="noopener">Mycat原理解析-Mycat架构分析</a></p>]]></content>
<categories>
<category> 架构设计 </category>
</categories>
</entry>
<entry>
<title>Intellij-idea-利用断点添加调试代码</title>
<link href="/2019/10/13/intellij-idea-li-yong-duan-dian-tian-jia-diao-shi-dai-ma/"/>
<url>/2019/10/13/intellij-idea-li-yong-duan-dian-tian-jia-diao-shi-dai-ma/</url>
<content type="html"><![CDATA[<p><img src="/images/Intellij-idea-%E5%88%A9%E7%94%A8%E6%96%AD%E7%82%B9%E6%B7%BB%E5%8A%A0%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81/0.jpg" alt></p><h1 id="问题场景"><a href="#问题场景" class="headerlink" title="问题场景"></a>问题场景</h1><p>在调试代码时,有时需要额外打印信息到日志或者控制台,事后又需要再把代码注释掉,操作起来比较繁琐,代码臃肿。</p><h1 id="解决方法"><a href="#解决方法" class="headerlink" title="解决方法"></a>解决方法</h1><p>测试代码:<br><img src="/images/Intellij-idea-%E5%88%A9%E7%94%A8%E6%96%AD%E7%82%B9%E6%B7%BB%E5%8A%A0%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81/1.png" alt="测试代码"><br>右键点击断点,点击“More”<br><img src="/images/Intellij-idea-%E5%88%A9%E7%94%A8%E6%96%AD%E7%82%B9%E6%B7%BB%E5%8A%A0%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81/2.png" alt="断点"><br>勾选“Evaluate and log”<br><img src="/images/Intellij-idea-%E5%88%A9%E7%94%A8%E6%96%AD%E7%82%B9%E6%B7%BB%E5%8A%A0%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81/3.png" alt="详情"><br>编写断点调试逻辑<br><img src="/images/Intellij-idea-%E5%88%A9%E7%94%A8%E6%96%AD%E7%82%B9%E6%B7%BB%E5%8A%A0%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81/4.png" alt="详情"><br>Dubug运行代码,得到结果:<br><img src="/images/Intellij-idea-%E5%88%A9%E7%94%A8%E6%96%AD%E7%82%B9%E6%B7%BB%E5%8A%A0%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81/5.png" alt="结果"><br>可以看到,在断点逻辑执行之前,idea会执行我们刚刚编写的断点调试代码。</p><h1 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h1><p>除此之外,在BreakPoints还有其他功能:</p><p>Condition<br>可以自定义判断条件,如果满足判断条件,断点才会成功停下。<br><img src="/images/Intellij-idea-%E5%88%A9%E7%94%A8%E6%96%AD%E7%82%B9%E6%B7%BB%E5%8A%A0%E8%B0%83%E8%AF%95%E4%BB%A3%E7%A0%81/6.png" alt="判断条件"></p><h1 id="参考"><a href="#参考" class="headerlink" title="参考"></a>参考</h1><p><a href="https://www.linuxidc.com/Linux/2017-09/146772.htm" target="_blank" rel="noopener">Intellij IDEA中使用Debug调试详解</a></p>]]></content>
<categories>
<category> 工具使用 </category>
</categories>
</entry>
</search>