-
Notifications
You must be signed in to change notification settings - Fork 0
/
local-search.xml
761 lines (365 loc) · 304 KB
/
local-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
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>Nginx keepalive长连接</title>
<link href="/nginx-keepalive/"/>
<url>/nginx-keepalive/</url>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>当使用nginx作为反向代理时,为了支持长连接,需要做到两点:</p><p>从client到nginx的连接是长连接<br>从nginx到server的连接是长连接<br>从HTTP协议的角度看,nginx在这个过程中,对于客户端它扮演着HTTP服务器端的角色。而对于真正的服务器端(在nginx的术语中称为upstream)nginx又扮演着HTTP客户端的角色。</p><h3 id="保持和client的长连接"><a href="#保持和client的长连接" class="headerlink" title="保持和client的长连接"></a>保持和client的长连接</h3><p>为了在client和nginx之间保持上连接,有两个要求:</p><ul><li>client发送的HTTP请求要求keep alive</li><li>nginx设置上支持keep alive</li></ul><h3 id="HTTP-配置"><a href="#HTTP-配置" class="headerlink" title="HTTP 配置"></a>HTTP 配置</h3><p>默认情况下,nginx已经自动开启了对client连接的keep alive支持。一般场景可以直接使用,但是对于一些比较特殊的场景,还是有必要调整个别参数。</p><p>需要修改nginx的配置文件:</p><figure class="highlight abnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs abnf">http {<br> keepalive_timeout <span class="hljs-number">120</span>s <span class="hljs-number">120</span>s<span class="hljs-comment">;</span><br> keepalive_requests <span class="hljs-number">10000</span><span class="hljs-comment">;</span><br>}<br></code></pre></td></tr></table></figure><h4 id="keepalive-timeout指令"><a href="#keepalive-timeout指令" class="headerlink" title="keepalive_timeout指令"></a>keepalive_timeout指令</h4><p>语法:</p><figure class="highlight avrasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs avrasm"><span class="hljs-symbol">Syntax:</span> keepalive_timeout timeout [header_timeout]<span class="hljs-comment">;</span><br><span class="hljs-symbol">Default:</span> keepalive_timeout <span class="hljs-number">75</span>s<span class="hljs-comment">;</span><br><span class="hljs-symbol">Context:</span> http, server, location<br></code></pre></td></tr></table></figure><p>第一个参数设置 keep-alive 客户端连接在服务器端保持开启的超时值。值为0会禁用 keep-alive 客户端连接。可选的第二个参数在响应的 header 域中设置一个值 “Keep-Alive: timeout=time”。这两个参数可以不一样。</p><blockquote><p>注:默认75s一般情况下也够用,对于一些请求比较大的内部服务器通讯的场景,适当加大为120s或者300s。第二个参数通常可以不用设置。</p></blockquote><h4 id="keepalive-requests指令"><a href="#keepalive-requests指令" class="headerlink" title="keepalive_requests指令"></a>keepalive_requests指令</h4><p>keepalive_requests指令用于设置一个keep-alive连接上可以服务的请求的最大数量。当最大请求数量达到时,连接被关闭。默认是100。</p><blockquote><p>这个参数的真实含义,是指一个keep alive建立之后,nginx就会为这个连接设置一个计数器,记录这个keep alive的长连接上已经接收并处理的客户端请求的数量。如果达到这个参数设置的最大值时,则nginx会强行关闭这个长连接,逼迫客户端不得不重新建立新的长连接。<br/><br>这个参数往往被大多数人忽略,因为大多数情况下当QPS(每秒请求数)不是很高时,默认值100凑合够用。但是,对于一些QPS比较高(比如超过10000QPS,甚至达到30000,50000甚至更高) 的场景,默认的100就显得太低。<br /><br>简单计算一下,QPS=10000时,客户端每秒发送10000个请求(通常建立有多个长连接),每个连接只能最多跑100次请求,意味着平均每秒钟就会有100个长连接因此被nginx关闭。同样意味着为了保持QPS,客户端不得不每秒中重新新建100个连接。因此,如果用netstat命令看客户端机器,就会发现有大量的TIME_WAIT的socket连接(即使此时keep alive已经在client和nginx之间生效)。</p></blockquote><p>因此对于QPS较高的场景,非常有必要加大这个参数,以避免出现大量连接被生成再抛弃的情况,减少TIME_WAIT。</p><h3 id="保持和server的长连接"><a href="#保持和server的长连接" class="headerlink" title="保持和server的长连接"></a>保持和server的长连接</h3><p>为了让nginx和server(nginx称为upstream)之间保持长连接,典型设置如下:</p><figure class="highlight awk"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><code class="hljs awk">http {<br> upstream BACKEND {<br> server <span class="hljs-number">192.168</span>.<span class="hljs-number">0.1</span>:<span class="hljs-number">8080</span> weight=<span class="hljs-number">1</span> max_fails=<span class="hljs-number">2</span> fail_timeout=<span class="hljs-number">30</span>s;<br> server <span class="hljs-number">192.168</span>.<span class="hljs-number">0.2</span>:<span class="hljs-number">8080</span> weight=<span class="hljs-number">1</span> max_fails=<span class="hljs-number">2</span> fail_timeout=<span class="hljs-number">30</span>s;<br><br> keepalive <span class="hljs-number">300</span>; <span class="hljs-regexp">//</span> 这个很重要!<br> }<br><br> server {<br> listen <span class="hljs-number">8080</span> default_server;<br> server_name <span class="hljs-string">""</span>;<br><br> location / {<br> proxy_pass http:<span class="hljs-regexp">//</span>BACKEND;<br> proxy_set_header Host <span class="hljs-variable">$Host</span>;<br> proxy_set_header x-forwarded-<span class="hljs-keyword">for</span> <span class="hljs-variable">$remote_addr</span>;<br> proxy_set_header X-Real-IP <span class="hljs-variable">$remote_addr</span>;<br> add_header Cache-Control no-store;<br> add_header Pragma no-cache;<br><br> proxy_http_version <span class="hljs-number">1.1</span>; <span class="hljs-regexp">//</span> 这两个最好也设置<br> proxy_set_header Connection <span class="hljs-string">""</span>;<br><br> client_max_body_size <span class="hljs-number">3072</span>k;<br> client_body_buffer_size <span class="hljs-number">128</span>k;<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><h4 id="upstream设置"><a href="#upstream设置" class="headerlink" title="upstream设置"></a>upstream设置</h4><p>upstream设置中,有个参数要特别的小心,就是这个keepalive。</p><p>大多数未仔细研读过nginx的同学通常都会误解这个参数,有些人理解为这里的keepalive是设置是否打开长连接,以为应该设置为on/off。有些人会被前面的keepalive_timeout误导,以为这里也是设置keepalive的timeout。</p><p>但是实际上这个keepalive参数的含义非常的奇特,请小心看 nginx 文档中的说明:</p><figure class="highlight avrasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs avrasm"><span class="hljs-symbol">Syntax:</span> keepalive connections<span class="hljs-comment">;</span><br><span class="hljs-symbol">Default:</span> —<br><span class="hljs-symbol">Context:</span> upstream<br></code></pre></td></tr></table></figure><blockquote><p>Activates the cache for connections to upstream servers.<br>激活到upstream服务器的连接缓存。<br /><br>The connections parameter sets the maximum number of idle keepalive connections to upstream servers that are preserved in the cache of each worker process. When this number is exceeded, the least recently used connections are closed.<br><code>connections</code> 参数设置每个worker进程在缓冲中保持的到upstream服务器的空闲keepalive连接的最大数量.当这个数量被突破时,最近使用最少的连接将被关闭。<br /><br>It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open. The connections parameter should be set to a number small enough to let upstream servers process new incoming connections as well.<br>特别提醒:keepalive指令不会限制一个nginx worker进程到upstream服务器连接的总数量。connections参数应该设置为一个足够小的数字来让upstream服务器来处理新进来的连接。</p></blockquote><p>在这里可以看到,前面的几种猜测可以确认是错误的了:</p><ul><li>keepalive不是on/off之类的开关</li><li>keepalive不是timeout,不是用来设置超时值</li></ul><p>很多人读到这里的文档之后,会产生另外一个误解:认为这个参数是设置到upstream服务器的长连接的数量,分歧在于是最大连接数还是最小连接数,不得不说这也是一个挺逗的分歧……</p><p>回到nginx的文档,请特别注意这句话,至关重要:</p><blockquote><p>The connections parameter sets the maximum number of idle keepalive connections to upstream servers.<br><code>connections</code> 参数设置到upstream服务器的空闲keepalive连接的最大数量</p></blockquote><p>请仔细体会这个”idle”的概念,何为idle。大多数人之所以误解为是到upstream服务器的最大长连接数,一般都是因为看到了文档中的这句话,而漏看了这个”idle”一词。</p><p>然后继续看文档后面另外一句话:</p><blockquote><p>When this number is exceeded, the least recently used connections are closed.<br>当这个数量被突破时,最近使用最少的连接将被关闭。</p></blockquote><p>这句话更是大大强化了前面关于keepalive设置的是最大长连接数的误解:如果连接数超过keepalive的限制,就关闭连接。这不是赤裸裸的最大连接数么?</p><p>但是nginx的文档立马给出了指示,否定了最大连接数的可能:</p><blockquote><p>It should be particularly noted that the keepalive directive does not limit the total number of connections to upstream servers that an nginx worker process can open.<br>特别提醒:keepalive指令不会限制一个nginx worker进程到upstream服务器连接的总数量。</p></blockquote><h3 id="keepalive参数的理解"><a href="#keepalive参数的理解" class="headerlink" title="keepalive参数的理解"></a>keepalive参数的理解</h3><p>要真正理解keepalive参数的含义,请回到文档中的这句:</p><blockquote><p>The connections parameter sets the maximum number of idle keepalive connections to upstream servers.<br><code>connections</code> 参数设置到upstream服务器的空闲keepalive连接的最大数量</p></blockquote><p>请注意空闲keepalive连接的最大数量中空闲这个关键字。</p><p>为了能让大家理解这个概念,我们先假设一个场景: 有一个HTTP服务,作为upstream服务器接收请求,响应时间为100毫秒。如果要达到10000 QPS的性能,就需要在nginx和upstream服务器之间建立大约1000条HTTP连接。nginx为此建立连接池,然后请求过来时为每个请求分配一个连接,请求结束时回收连接放入连接池中,连接的状态也就更改为idle。</p><p>我们再假设这个upstream服务器的keepalive参数设置比较小,比如常见的10.</p><p>假设请求和响应是均匀而平稳的,那么这1000条连接应该都是一放回连接池就立即被后续请求申请使用,线程池中的idle线程会非常的少,趋进于零。我们以10毫秒为一个单位,来看连接的情况(注意场景是1000个线程+100毫秒响应时间,每秒有10000个请求完成):</p><ul><li>每10毫秒有100个新请求,需要100个连接</li><li>每10毫秒有100个请求结束,可以释放100个连接</li><li>如果请求和应答都均匀,则10毫秒内释放的连接刚好够用,不需要新建连接,连接池空闲连接为零</li></ul><p>然后再回到现实世界,请求通常不是足够的均匀和平稳,为了简化问题,我们假设应答始终都是平稳的,只是请求不平稳,第一个10毫秒只有50,第二个10毫秒有150:</p><ul><li>下一个10毫秒,有100个连接结束请求回收连接到连接池,但是假设此时请求不均匀10毫秒内没有预计的100个请求进来,而是只有50个请求。注意此时连接池回收了100个连接又分配出去50个连接,因此连接池内有50个空闲连接。</li><li>然后注意看keepalive=10的设置,这意味着连接池中最多容许保留有10个空闲连接。因此nginx不得不将这50个空闲连接中的40个关闭,只留下10个。</li><li>再下一个10个毫秒,有150个请求进来,有100个请求结束任务释放连接。150 - 100 = 50,空缺了50个连接,减掉前面连接池保留的10个空闲连接,nginx不得不新建40个新连接来满足要求。</li></ul><p>我们可以看到,在短短的20毫秒内,仅仅因为请求不够均匀,就导致nginx在前10毫秒判断空闲连接过多关闭了40个连接,而后10毫秒又不得不新建40个连接来弥补连接的不足。</p><p>再来一次类似的场景,假设请求是均匀的,而应答不再均匀,前10毫秒只有50个请求结束,后10毫秒有150个:</p><ul><li>前10毫秒,进来100个请求,结束50个请求,导致连接不够用,nginx为此新建50个连接</li><li>后10毫秒,进来100个请求,结束150个请求,导致空闲连接过多,ngixn为此关闭了150-100-10=40个空闲连接</li></ul><p>第二个应答不均匀的场景实际上是对应第一个请求不均匀的场景:正是因为请求不均匀,所以导致100毫秒之后这些请求的应答必然不均匀。</p><p>现实世界中的请求往往和理想状态有巨大差异,请求不均匀,服务器处理请求的时间也不平稳,这理论上的大概1000个连接在反复的回收和再分配的过程中,必然出现两种非常矛盾场景在短时间内反复: 1. 连接不够用,造成新建连接 2. 连接空闲,造成关闭连接。从而使得总连接数出现反复震荡,不断的创建新连接和关闭连接,使得长连接的效果被大大削弱。</p><p>造成连接数量反复震荡的一个推手,就是这个keepalive 这个最大空闲连接数。毕竟连接池中的1000个连接在频繁利用时,出现短时间内多余10个空闲连接的概率实在太高。因此为了避免出现上面的连接震荡,必须考虑加大这个参数,比如上面的场景如果将keepalive设置为100或者200,就可以非常有效的缓冲请求和应答不均匀。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>keepalive 这个参数一定要小心设置,尤其对于QPS比较高的场景,推荐先做一下估算,根据QPS和平均响应时间大体能计算出需要的长连接的数量。比如前面10000 QPS和100毫秒响应时间就可以推算出需要的长连接数量大概是1000. 然后将keepalive设置为这个长连接数量的10%到30%。</p><p>比较懒的同学,可以直接设置为keepalive=1024之类的,一般都OK的了。</p><h4 id="location设置"><a href="#location设置" class="headerlink" title="location设置"></a>location设置</h4><figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs crmsh">http {<br> server {<br> <span class="hljs-keyword">location</span> <span class="hljs-title">/ {</span><br><span class="hljs-title"> proxy_http_version</span> <span class="hljs-number">1.1</span>; // 这两个最好也设置<br> proxy_set_header Connection <span class="hljs-string">""</span>;<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>HTTP协议中对长连接的支持是从1.1版本之后才有的,因此最好通过proxy_http_version指令设置为”1.1”,而”Connection” header应该被清理。清理的意思,我的理解,是清理从client过来的http header,因为即使是client和nginx之间是短连接,nginx和upstream之间也是可以开启长连接的。这种情况下必须清理来自client请求中的”Connection” header。</p>]]></content>
<categories>
<category>Nginx</category>
</categories>
<tags>
<tag>Nginx</tag>
</tags>
</entry>
<entry>
<title>Nginx 轮询算法</title>
<link href="/nginx-poll/"/>
<url>/nginx-poll/</url>
<content type="html"><![CDATA[<h4 id="负载均衡"><a href="#负载均衡" class="headerlink" title="负载均衡"></a>负载均衡</h4><p>一般的反向代理服务器,都具备负载均衡的功能。负载均衡功能可以由硬件来提供,比如以前的F5设备。<br>也可以由软件来提供,LVS可以提供四层的负载均衡(利用IP和端口),Haproxy和Nginx可以提供七层的负载均衡(利用应用层信息)。<br>像nginx可以使用负载均衡分配流量,ribbon为客户端提供负载均衡,dubbo服务调用里的负载均衡等等,很多地方都使用到了负载均衡。</p><p>负载均衡有好几种实现策略,常见的有:</p><ul><li>随机 (Random) </li><li>轮询 (RoundRobin) </li><li>一致性哈希 (ConsistentHash) </li><li>哈希 (Hash) </li><li>加权(Weighted)</li></ul><p>Nginx目前提供的负载均衡模块:<br>ngx_http_upstream_round_robin,加权轮询,可均分请求,是默认的HTTP负载均衡算法,集成在框架中。<br>ngx_http_upstream_ip_hash_module,IP哈希,可保持会话。<br>ngx_http_upstream_least_conn_module,最少连接数,可均分连接。<br>ngx_http_upstream_hash_module,一致性哈希,可减少缓存数据的失效。 </p><h3 id="简单轮询算法"><a href="#简单轮询算法" class="headerlink" title="简单轮询算法"></a>简单轮询算法</h3><table><thead><tr><th align="left">描述</th><th align="left">IP</th></tr></thead><tbody><tr><td align="left">第一台服务器</td><td align="left">192.168.1.1</td></tr><tr><td align="left">第二台服务器</td><td align="left">192.168.1.2</td></tr><tr><td align="left">第三台服务器</td><td align="left">192.168.1.3</td></tr></tbody></table><p>第一个请求过来之后默认访问第一台,第二个请求过来访问第二台,第三次请求过来访问第三台,第四次请求过来访问第一台,以此类推。<br>此时如果我有一台服务器性能比较好(比如192.168.1.1),我想让这台服务器处理多一点请求,此时就涉及到了权重得概率,这种算法就不能实现。</p><h3 id="加权轮询算法"><a href="#加权轮询算法" class="headerlink" title="加权轮询算法"></a>加权轮询算法</h3><p>来看一个简单的Nginx负载均衡配置</p><figure class="highlight nginx"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><code class="hljs nginx"><span class="hljs-section">http</span> {<br> <span class="hljs-section">upstream</span> cluster {<br> <span class="hljs-attribute">server</span> a weight=<span class="hljs-number">5</span>;<br> <span class="hljs-attribute">server</span> b weight=<span class="hljs-number">1</span>;<br> <span class="hljs-attribute">server</span> c weight=<span class="hljs-number">1</span>;<br> }<br> <br> <span class="hljs-section">server</span> {<br> <span class="hljs-attribute">listen</span> <span class="hljs-number">80</span>;<br> <br> <span class="hljs-section">location</span> / {<br> <span class="hljs-attribute">proxy_pass</span> http://cluster;<br> }<br> }<br>}<br></code></pre></td></tr></table></figure><p>此时前5个请求都会访问到第一台服务器,第六个请求会访问到第二台服务器,第七个请求会访问到第三台服务器。</p><p>其实这种算法有一个缺点,后端序列是这样的:{ c, b, a, a, a, a, a },会有5个连续的请求落在后端a上,分布不太均匀。如果我第一台服务器设置权重过大可能我很多次请求都执行到第一台服务器上去,会造成某一台服务器压力过大导致崩溃。</p><h3 id="平滑加权轮询算法"><a href="#平滑加权轮询算法" class="headerlink" title="平滑加权轮询算法"></a>平滑加权轮询算法</h3><p><img src="/images/20220520154627.jpg"></p><p>由上图可以看出第一台服务器虽然权重设置的是5,但并不是第五次请求过来都是第一台服务器执行,而是分散执行,调度序列是非常均匀的,且第 7 次调度时选中后当前权重又回到 {0, 0, 0},实例的状态同初始状态一致,所以后续可以一直重复调度操作。</p><p>这里大概描述一下:</p><p>1.首先总权重不会变,默认就是当前设置的权重之和</p><p>2.在第一次请求进来的时候我默认初始化当前权重选中值是{0,0,0},所以当前权重的值就是{5+0,1+0,1+0},这里的5,1,1就是我们前面每台服务器设置的权重。</p><p>3.这里我们可以得出第一次请求过来的最大权重是5。然后返回第一台服务器ip</p><p>4.然后我们设置选中后当前权重,这里就是当前最大权重减去总权重(5-7),没有选中的权重不变,这时候得到当前权重选中权重的值{5-7,1,1}</p><p>5.在第二次请求过来的时候我们延续上面的2,3,4步骤执行.</p>]]></content>
<categories>
<category>Nginx</category>
</categories>
<tags>
<tag>Nginx</tag>
</tags>
</entry>
<entry>
<title>APISIX 与现有网关以及 SpringCloudGateWay 压测报告对比</title>
<link href="/apisix-preftest/"/>
<url>/apisix-preftest/</url>
<content type="html"><![CDATA[<h4 id="压测环境"><a href="#压测环境" class="headerlink" title="压测环境"></a>压测环境</h4><ul><li><p>测试主机:阿里云 8 vCPU 16 GiB 分别1台网关,1台后端</p></li><li><p>压测工具:jemter</p></li><li><p>压测说明:</p></li></ul><p>针对接口分别执行线程总数 400(jemter1台)、800(jemter2台) 进行压力测试,并对产生的每秒TPS,响应时间(min,ave,max)及错误率进行统计。后端有一个/hello接口,请求方式GET。网关反向代理到后端.</p><p>APISIX模型:</p><p><img src="/images/apisix-1.png"></p><p>zuul-server公司现有网关模型:</p><p><img src="/images/apisix-2.png"></p><p>全新的springCloudGateway(没有开发任何功能):</p><p><img src="/images/apisix-3.png"></p><h4 id="8000QPS的指标对比"><a href="#8000QPS的指标对比" class="headerlink" title="8000QPS的指标对比"></a>8000QPS的指标对比</h4><h5 id="APISIX未开启证书"><a href="#APISIX未开启证书" class="headerlink" title="APISIX未开启证书"></a>APISIX未开启证书</h5><p><img src="/images/apisix-4.png"></p><h5 id="ZUUL-SERVER"><a href="#ZUUL-SERVER" class="headerlink" title="ZUUL-SERVER"></a>ZUUL-SERVER</h5><p><img src="/images/apisix-5.png"></p><h5 id="SpringCloudGateway"><a href="#SpringCloudGateway" class="headerlink" title="SpringCloudGateway"></a>SpringCloudGateway</h5><p><img src="/images/apisix-6.png"></p><h5 id="APISIX开启证书"><a href="#APISIX开启证书" class="headerlink" title="APISIX开启证书"></a>APISIX开启证书</h5><p>平均RT压了多次都是0,可能被四舍五入了</p><p><img src="/images/apisix-7.png"></p><h4 id="16000QPS的指标对比"><a href="#16000QPS的指标对比" class="headerlink" title="16000QPS的指标对比"></a>16000QPS的指标对比</h4><h5 id="APISIX未开启https"><a href="#APISIX未开启https" class="headerlink" title="APISIX未开启https"></a>APISIX未开启https</h5><p><img src="/images/apisix-8.png"></p><h5 id="APISIX开启HTTPS"><a href="#APISIX开启HTTPS" class="headerlink" title="APISIX开启HTTPS"></a>APISIX开启HTTPS</h5><p><img src="/images/apisix-9.png"></p><h5 id="SpringCloudGateway-1"><a href="#SpringCloudGateway-1" class="headerlink" title="SpringCloudGateway"></a>SpringCloudGateway</h5><p><img src="/images/apisix-10.png"></p><h4 id="30000QPS的指标对比"><a href="#30000QPS的指标对比" class="headerlink" title="30000QPS的指标对比"></a>30000QPS的指标对比</h4><h5 id="APISIX未开启HTTPS"><a href="#APISIX未开启HTTPS" class="headerlink" title="APISIX未开启HTTPS"></a>APISIX未开启HTTPS</h5><p><img src="/images/apisix-11.png"></p><h5 id="APISIX开启HTTPS-1"><a href="#APISIX开启HTTPS-1" class="headerlink" title="APISIX开启HTTPS"></a>APISIX开启HTTPS</h5><p><img src="/images/apisix-12.png"></p><h4 id="压测结果"><a href="#压测结果" class="headerlink" title="压测结果"></a>压测结果</h4><p>APISIX: 开启插件和不开启插件的性能差不了多少,开启HTTPS的功能之后,16000QPS的RT无差,3万QPS之后RT会增加一倍。</p><p>APISIX的3万QPS的情况下,cpu使用率在30%~40%;</p><p>zuul-server的8000QPS已经是极限;SpringCloudGateWay只能支撑到16000QPS。</p>]]></content>
<categories>
<category>网关</category>
</categories>
<tags>
<tag>APISIX</tag>
</tags>
</entry>
<entry>
<title>OpenResty 使用body_filter_by_lua* 修改返回内容</title>
<link href="/openresty-body-filter/"/>
<url>/openresty-body-filter/</url>
<content type="html"><![CDATA[<h4 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h4><p>测试开发环境所有前端页面需要嵌入一段前端脚本,方便开发者在手机浏览器里调试,类似小程序调试的功能。</p><h4 id="谈谈-body-filter-by-lua-多次调用的问题"><a href="#谈谈-body-filter-by-lua-多次调用的问题" class="headerlink" title="谈谈 body_filter_by_lua* 多次调用的问题"></a>谈谈 <code>body_filter_by_lua*</code> 多次调用的问题</h4><p>正如 OpenResty 文档中指出,body_filter_by_lua* 可能会在一次请求中多次调用。</p><blockquote><p>Nginx output filters may be called multiple times for a single request because response body may be delivered in chunks.<br>Thus, the Lua code specified by in this directive may also run multiple times in the lifetime of a single HTTP request.</p></blockquote><p>文档中举了个例子:</p><figure class="highlight bash"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs bash">location /t {<br> <span class="hljs-built_in">echo</span> hello world;<br> <span class="hljs-built_in">echo</span> hiya globe;<br></code></pre></td></tr></table></figure><p><code>body_filter_by_lua*</code> 首次调用时,<code>ngx.arg[1]</code> 的值只是 <code>hello world</code>,不包括下面的 <code>hiya globe</code>。</p><p>不过初看下来,很难将 <code>echo hello world</code>; 和 <code>delivered in chunks</code> 联系起来。这个 <code>chunk</code> 的大小是怎么确定的?看例子,应该跟 <code>echo/ngx.say</code> 这一类输出方式有关。但是会不会跟输出的大小也有关?如果我一次性 <code>ngx.say</code> 了很多内容,是否会分成多个 <code>chunks</code>发送?如果响应来自上游服务器,<code>chunks</code> 的数目又怎么定?</p><p>要回答这个问题,需要看看 <code>Nginx</code> 内响应内容的组织方式。<code>Nginx</code> 上游产生的内容,存储为 <code>ngx_chain_t</code> 类型的数据。这其实是一条 <code>ngx_buf_t</code>链表。很容易可以想像到,这个链表就代表着数据流。上游产生的内容,像流水线上的包裹一样,不停地向下游传递。<code>output filter</code> 阶段像流水线上的机器,处理这些“包裹”。跟流水线上的机器不同的是,<code>Nginx</code> 中的 <code>output filter</code> 并非逐个处理这些“包裹”,而是一批一批地处理。上游成批成批地生产出这些包裹,每批包裹构成 <code>ngx_chain_t</code> 的子串,而 <code>output fiter</code>则遍历这一子串,把其中的每个包裹打开处理。</p><p>想到 <code>body_filter_by_lua*</code> 其实属于 <code>output filter</code> 的一种,我们就回到了一开始讨论的问题。既然 <code>body_filter_by_lua*</code>是一批一批处理上游的响应,那么它的调用次数就取决于上游的响应次数。上游的一次响应,如一次 <code>ngx.say</code>,会产生一个 <code>ngx_chain_t</code> 的子串(就 <code>ngx.say</code> 而言,这个子串仅包含单个 <code>ngx_buf_t</code>)。至于响应的大小,最多只会影响到子串的长短,具体情况则取决于具体实现。</p><p>以我们常用的 ngx.say 为例:</p><figure class="highlight sqf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sqf">ngx.<span class="hljs-built_in">say</span>(<span class="hljs-string">'This '</span>, <span class="hljs-string">'will '</span>, <span class="hljs-string">'be '</span>, <span class="hljs-string">'in '</span>, <span class="hljs-string">'a '</span>, <span class="hljs-string">'buffer'</span>)<br></code></pre></td></tr></table></figure><p>以上几个字符串会通过栈从 <code>lua</code> 域传递给 C 域。接着 <code>OpenResty</code> 计算它们的总长度,从 <code>buffer chain</code> 中找出一个空闲的大小合适的 ngx_buf_t,把它们拷贝进来。 之后就走 <code>http_output_filter</code> 把这个 <code>ngx_buf_t</code> (准确来说,是它所在的链表)发送出去。</p><p>那么,上游什么时候会把数据发完了?<code>Nginx</code> 采用了一个 <code>last_buf</code> 的标志位,如果某个 <code>ngx_buf_t</code> 是链表中的最后一个,跟上游交互的模块会设置这一个标志位为1. 映射回 <code>OpenResty</code> 的 <code>lua</code> 域,则是 <code>body_filter_by_lua*</code> 中的 <code>ngx.arg[2]</code>。你可能会注意到,<code>last_buf</code> 是一个设置在 <code>ngx_buf_t</code> 上的标志位,而传递给 <code>output filter</code> 的是 <code>ngx_chain_t</code>。<code>OpenResty</code> 把这一差别隐藏在实现之下——它会遍历当前输入的子串,如果某个 <code>ngx_buf_t</code> 存在 <code>last_buf</code>,那么就返回 true。</p><h4 id="自行尝试"><a href="#自行尝试" class="headerlink" title="自行尝试"></a>自行尝试</h4><figure class="highlight lua"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs lua"><span class="hljs-comment">-- access_by_lua_file:</span><br>ngx.say(<span class="hljs-string">"123"</span>)<br>ngx.say(<span class="hljs-string">"456"</span>)<br><br><span class="hljs-comment">-- body_filter_by_lua_file:</span><br><span class="hljs-keyword">local</span> chunk, eof = ngx.<span class="hljs-built_in">arg</span>[<span class="hljs-number">1</span>], ngx.<span class="hljs-built_in">arg</span>[<span class="hljs-number">2</span>]<br><span class="hljs-built_in">print</span>(chunk, eof)<br><br><span class="hljs-comment">-- 结果</span><br><span class="hljs-comment">-- body_filter_by_lua*首次调用时:123 false</span><br><span class="hljs-comment">-- body_filter_by_lua*第二次调用时:456 false</span><br><span class="hljs-comment">-- body_filter_by_lua*第三次调用时: 空 true</span><br></code></pre></td></tr></table></figure><p>尽管这里只有两个 <code>echo</code>,但是 <code>body_filter_by_lua*</code> 会被调用三次!第三次调用的时候,<code>ngx.arg[1]</code> 为空字符串,而 <code>ngx.arg[2]</code>为 <code>true</code>。这是因为 <code>Nginx</code> 的 <code>upstream</code> 相关模块,以及 <code>OpenResty</code> 的 <code>content_by_lua</code>,会单独发送一个设置了 <code>last_buf</code> 的空 <code>buffer</code> ,来表示流的结束。所以我们需要在运行相关逻辑之前,检查 <code>ngx.arg[1]</code> 是否为空,但是需要注意的是 <code>ngx.arg[2]=true</code> 并不代表 <code>ngx.arg[1]</code>一定为空。</p><p>也许你已经发现了,子请求也会走到 <code>body_filter_by_lua*</code> 的流程。严格意义上,如果只希望 <code>body_filter_by_lua*</code> 修改响应给客户端的内容,需要额外用 <code>ngx.is_subrequest</code> 判断下:</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-keyword">if</span> ngx<span class="hljs-selector-class">.arg</span><span class="hljs-selector-attr">[1]</span> and not ngx<span class="hljs-selector-class">.is_subrequest</span> then<br></code></pre></td></tr></table></figure><p>官方文档也有一段说明,源码链接地址:<a href="https://github.com/openresty/lua-nginx-module#body_filter_by_lua">body_filter_by_lua </a></p><blockquote><p>The input data chunk is passed via ngx.arg[1] (as a Lua string value) and the “eof” flag indicating the end of the response body data stream is passed via ngx.arg[2] (as a Lua boolean value).</p></blockquote><p>流每次的内容输出在 <code>ngx.arg[1]</code> 中; <code>eof</code> 最后的标记在 <code>ngx.arg[2]</code> 中, 所以你要改输出内容那么就把 <code>ngx.arg[1]</code>改掉,如果不想要以后的内容了那么 <code>ngx.arg[2]=true</code> 就行.</p><h4 id="需要注意的地方"><a href="#需要注意的地方" class="headerlink" title="需要注意的地方"></a>需要注意的地方</h4><p>还有一个特别需要注意地方是,当代码运行到 <code>body_filter_by_lua*</code> 时,<code>HTTP</code>报头(header)已经发送出去了。如果在之前设置了跟响应体相关的报头,而又在 <code>body_filter_by_lua*</code>中修改了响应体,会导致响应报头和实际响应的不一致。举个简单的例子:假设上游的服务器返回了 <code>Content-Length</code> 报头,而 <code>body_filter_by_lua*</code> 又修改了响应体的实际大小。客户端收到这个报头后,按其中的 <code>Content-Length</code> 去处理,顺着一头栽进坑里。由于 <code>Nginx</code> 的流式响应,发出去的报头就像泼出去的水,要想修改只能提前进行。<code>OpenResty</code> 提供了跟 <code>body_filter_by_lua*</code> 相对应的 <code>header_filter_by_lua*</code>。<code>header_filter</code> 会在 <code>Nginx</code> 发送报头之前调用,所以可以在这里置空 <code>Content-Length</code> 报头:</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs stylus">header_filter_by_lua_block {<br> ngx<span class="hljs-selector-class">.header</span><span class="hljs-selector-class">.content_length</span> = nil<br>}<br></code></pre></td></tr></table></figure><p>现在 <code>Nginx</code> 会代以 <code>Transfer-Encoding: chunked</code>,再也不会误导客户端了。同样可能需要处理的还有 <code>accept-range</code> 和 <code>etag</code> 等跟响应体相关的报头。HTTP1.1 之后基于流式处理的方式,<code>body_filter_by_lua</code> 基本在一个请求中会调用多次。 简单直白的理解就是流式输出,每次拿到了如果你要处理那么就处理,不处理就输出!!</p><h4 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h4><ul><li><code>body_filter_by_lua*</code> 可能在一次请求中调用多次,跟响应数据量无关,取决于响应次数</li><li><code>body_filter_by_lua*</code> 的最后一次调用时,<code>ngx.arg[1]</code> 一般为空字符串</li><li><code>body_filter_by_lua*</code> 也会在 <code>subrequest</code> 之中调用</li><li><code>body_filter_by_lua*</code> 有些时候离不开有 <code>header_filter_by_lua*</code> 辅佐</li></ul>]]></content>
<categories>
<category>OpenResty</category>
</categories>
<tags>
<tag>OpenResty</tag>
</tags>
</entry>
<entry>
<title>Android 破解APP抓包限制(绕开https的SSL Pinning)</title>
<link href="/android-catchhttp/"/>
<url>/android-catchhttp/</url>
<content type="html"><![CDATA[<h3 id="介绍"><a href="#介绍" class="headerlink" title="介绍"></a>介绍</h3><blockquote><p>这里写下来是为了防止自己遗忘的,毕竟去刷机也不是经常有的事</p></blockquote><p>这篇文章主要想解决的问题是,在对安卓手机APP抓包时,出现的HTTPS报文通过MITM代理后证书不被信任的问题。如果有些https,在之前设置了好各种证书和配置后,看到的:</p><ul><li>要么是unknown</li><li>要么是:加密的乱码</li><li>要么是:报错无法抓包</li></ul><p>而无法看到我们希望的明文数据,则:</p><p>最大可能是,对方用了https的 <code>SSL pinning</code></p><h4 id="什么是SSL-Pinning"><a href="#什么是SSL-Pinning" class="headerlink" title="什么是SSL Pinning"></a>什么是SSL Pinning</h4><blockquote><p>SSL pinning = 证书绑定 = SSL证书绑定</p></blockquote><p>对方的app内部,只允许,承认其自己的,特定的证书</p><p>导致此处MITM的证书不识别,不允许</p><p>导致MITM无法解密看到https的明文数据</p><h4 id="Android-7-0之后系统如何破解https的ssl-pinning"><a href="#Android-7-0之后系统如何破解https的ssl-pinning" class="headerlink" title="Android 7.0之后系统如何破解https的ssl pinning"></a>Android 7.0之后系统如何破解https的ssl pinning</h4><p>对于Android 7.0 (API 24) 之后,做了些改动,使得系统安全性增加了,导致:</p><ul><li>APP 默认不信任用户域的证书。之前把MITM的ssl证书,安装到 受信任的凭据 -> 用户 就没用了,因为不受信任了。只信任(安装到)系统域的证书</li></ul><p>导致无法抓包https,抓出来的https的请求,都是加了密的,无法看到原文了。</p><p>对此,总结出相关解决思路和方案:</p><ul><li><p>(努力想办法)让系统信任Charles的ssl证书</p><ul><li>作为app的开发者自己:改自己的app的配置,允许https抓包。前提是得到或本身有app的源码</li><li>把证书放到受系统信任的系统证书中去。前提是手机已root</li></ul></li><li><p>绕开https不去校验</p><ul><li>借助于其他(JustTrustMe等)工具绕开https的校验。需要借助其他XPosed等框架配合才可以 (这里只介绍这一种方案)</li></ul></li></ul><h3 id="刷机(ROOT)"><a href="#刷机(ROOT)" class="headerlink" title="刷机(ROOT)"></a>刷机(ROOT)</h3><p>网上有很多刷机教程,我试过小米、oppo、华为、一加,除了一加其它root都需要很多门槛,oppo需要深度测试申请,审核估计都要15天,华为需要解锁码,小米也要申请,而一加什么门槛都没有,而且官方论坛一堆小白刷机工具,一键就能搞定。所以极力推荐一加手机。</p><p>这里推荐 @千古风流帝 的 <a href="https://www.oneplusbbs.com/thread-4701007-1.html">工具箱</a>,该工具箱里包含了解锁、TWRP、Root 等一系列无脑操作。按照软件的教程,先解锁在ROOT,它会自动刷入 <a href="https://github.com/topjohnwu/Magisk">Magisk</a>,刷完之后多一个MagiskManager的APP。至此ROOT已经成完成</p><h4 id="安装太极·Magisk"><a href="#安装太极·Magisk" class="headerlink" title="安装太极·Magisk"></a>安装太极·Magisk</h4><p>按照官方教程刷入,这里不在赘述,<a href="https://github.com/taichi-framework/TaiChi/wiki/%E5%A6%82%E4%BD%95%E4%BD%BF%E7%94%A8">教程地址</a></p><h4 id="安装JustTrustMe模块"><a href="#安装JustTrustMe模块" class="headerlink" title="安装JustTrustMe模块"></a>安装JustTrustMe模块</h4><p> <a href="https://github.com/taichi-framework/TaiChi/issues/538">JustTrustMe下载地址</a></p><p> ok,这样就可以正常抓包了,测试手机是一加5T,测试APP是抖音。</p>]]></content>
<categories>
<category>Android</category>
</categories>
<tags>
<tag>爬虫</tag>
</tags>
</entry>
<entry>
<title>springboot2 整合lettuce启动卡住的问题</title>
<link href="/lettuce-blocked/"/>
<url>/lettuce-blocked/</url>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>EasyCache升级兼容 <code>Springboot2</code>,有个业务系统启动总是会卡住,最后抛出超时异常,如下:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs java">java.util.concurrent.TimeoutException: <span class="hljs-literal">null</span><br>at java.util.concurrent.FutureTask.get(FutureTask.java:<span class="hljs-number">205</span>)<br>.....<br></code></pre></td></tr></table></figure><p>springboot 版本是 2.2.x,springCloudVersion 版本是 2.2.x, lettuce版本是5.2.x,如果使用jedis客户端没有,所以问题一定是出在lettuce。</p><h3 id="分析原因"><a href="#分析原因" class="headerlink" title="分析原因"></a>分析原因</h3><p>如果是线上发生这个问题会使用 jstack 查看线程的情况,在本地idea调试就更加方便了,查看线程发现lettuce的线程被Blocked,dump出的部分信息如下:</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-string">"lettuce-kqueueEventLoop-7-1@14257"</span> daemon prio=<span class="hljs-number">5</span> tid=<span class="hljs-number">0</span>x4c nid=NA waiting <span class="hljs-keyword">for</span> monitor entry<br> java<span class="hljs-selector-class">.lang</span><span class="hljs-selector-class">.Thread</span><span class="hljs-selector-class">.State</span>: BLOCKED<br> waiting <span class="hljs-keyword">for</span> main@<span class="hljs-number">1</span> to release lock on <<span class="hljs-number">0</span>x38a5> (<span class="hljs-selector-tag">a</span> java<span class="hljs-selector-class">.util</span><span class="hljs-selector-class">.concurrent</span>.ConcurrentHashMap)<br> at org<span class="hljs-selector-class">.springframework</span><span class="hljs-selector-class">.beans</span><span class="hljs-selector-class">.factory</span><span class="hljs-selector-class">.support</span><span class="hljs-selector-class">.DefaultSingletonBeanRegistry</span><span class="hljs-selector-class">.getSingleton</span>(DefaultSingletonBeanRegistry<span class="hljs-selector-class">.java</span>:<span class="hljs-number">208</span>)<br> at org<span class="hljs-selector-class">.springframework</span><span class="hljs-selector-class">.beans</span><span class="hljs-selector-class">.factory</span><span class="hljs-selector-class">.support</span><span class="hljs-selector-class">.AbstractBeanFactory</span><span class="hljs-selector-class">.doGetBean</span>(AbstractBeanFactory<span class="hljs-selector-class">.java</span>:<span class="hljs-number">321</span>)<br> ....<br></code></pre></td></tr></table></figure><p>看第一行的报错是在获取Bean的时候阻塞了,说明有地方获取Bean的时候没有释放锁。在这地方打断点发现是 spring-cloud-sleuth 的 <code>SamplerAutoConfiguration</code>获取bean的时候有锁没有释放。源代码如下</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Configuration(proxyBeanMethods = false)</span><br><span class="hljs-meta">@ConditionalOnBean(type = "org.springframework.cloud.context.scope.refresh.RefreshScope")</span><br><span class="hljs-keyword">protected</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">RefreshScopedSamplerConfiguration</span> {<br><br><span class="hljs-meta">@Bean</span><br><span class="hljs-meta">@RefreshScope</span><br><span class="hljs-meta">@ConditionalOnMissingBean</span><br><span class="hljs-keyword">public</span> Sampler <span class="hljs-title function_">defaultTraceSampler</span><span class="hljs-params">(SamplerProperties config)</span> {<br><span class="hljs-keyword">return</span> samplerFromProps(config);<br>}<br><br>}<br></code></pre></td></tr></table></figure><p>@RefreshScope 获取代理类的时候如果是@PostConstruct的方法,bean是加载不到,所以导致一直没有释放锁。所以猜想,容器还没有启动完成的时候,有地方调用了lettuce的Bean,导致循环依赖。</p><h3 id="坑的复现及解决办法"><a href="#坑的复现及解决办法" class="headerlink" title="坑的复现及解决办法"></a>坑的复现及解决办法</h3><p>运行下面这段代码,错误就出现了,和业务系统出现的问题一模一样,也验证了上面的猜想。解决办法是在容器启动之后在调用init方法。(实测使用InitializingBean时也会出现该问题)</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Service</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SpringDataTestService</span> {<br><br> <span class="hljs-meta">@Autowired</span><br> <span class="hljs-keyword">private</span> StringRedisTemplate stringRedisTemplate;<br><br> <span class="hljs-comment">//@EventListener(MainContextRefreshedEvent.class)</span><br> <span class="hljs-meta">@PostConstruct</span><br> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">init</span><span class="hljs-params">()</span> {<br> <span class="hljs-type">String</span> <span class="hljs-variable">s</span> <span class="hljs-operator">=</span> stringRedisTemplate.opsForValue().get(<span class="hljs-string">"gateway:ab-test:config"</span>);<br> System.out.println(s);<br> }<br>}<br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Springboot</tag>
<tag>Lettuce</tag>
</tags>
</entry>
<entry>
<title>python 模块的构建与发布 setup.py</title>
<link href="/py-setup/"/>
<url>/py-setup/</url>
<content type="html"><![CDATA[<h3 id="应用场景"><a href="#应用场景" class="headerlink" title="应用场景"></a>应用场景</h3><p>随着爬虫需求越来越多,python系统也越来越多,有很多重复模块每个系统都要copy一遍,比如:apollo配置中心、注册eureka以及一些工具类。</p><h3 id="setuptools-介绍"><a href="#setuptools-介绍" class="headerlink" title="setuptools 介绍"></a>setuptools 介绍</h3><p><code>setuptools</code> 是 distutils 增强版,不包括在标准库中。其扩展了很多功能,能够帮助开发者更好的创建和分发 Python 包。大部分 Python 用户都会使用更先进的 <a href="https://setuptools.readthedocs.io/en/latest/">setuptools</a> 模块。</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre></td><td class="code"><pre><code class="hljs routeros"><span class="hljs-comment"># coding=utf-8</span><br><span class="hljs-keyword">from</span> setuptools import setup,find_packages<br>import io<br>import os<br><br>def read(*parts):<br> filename = os.path.join(os.path.abspath(os.path.dirname(__file__)), *parts)<br><br> with io.open(filename, <span class="hljs-attribute">encoding</span>=<span class="hljs-string">'utf-8'</span>, <span class="hljs-attribute">mode</span>=<span class="hljs-string">'rt'</span>) as fp:<br> return fp.read()<br><br><br>setup(<br> <span class="hljs-attribute">name</span>=<span class="hljs-string">'duiba-ext'</span>, # 应用名<br> <span class="hljs-attribute">version</span>=<span class="hljs-string">'0.0.2'</span>, # 版本号<br> <span class="hljs-attribute">author</span>=<span class="hljs-string">"xxx"</span>, # 程序的作者<br> <span class="hljs-attribute">author_email</span>=<span class="hljs-string">"xxx"</span>,<br> <span class="hljs-attribute">description</span>=<span class="hljs-string">"兑吧扩展"</span>,<br> <span class="hljs-attribute">long_description</span>=read('README.md'),<br> <span class="hljs-attribute">license</span>=<span class="hljs-string">"MIT"</span>,<br> <span class="hljs-attribute">url</span>=<span class="hljs-string">"xxxx"</span>,<br> <span class="hljs-attribute">packages</span>=find_packages(),<br> data_files=[<span class="hljs-string">'duiba_ext/common/fake_useragent.json'</span>],<br> include_package_data = <span class="hljs-literal">True</span>,<br> install_requires=[<br> <span class="hljs-string">"requests>=2.25.0"</span>,<br> <span class="hljs-string">"redis>=3.5.3"</span>,<br> <span class="hljs-string">"py_eureka_client>=0.7.6"</span>,<br> <span class="hljs-string">"fake-useragent>=0.1.11"</span>,<br> ],<br> classifiers=[<br> # 发展时期,常见的如下<br> # 3 - Alpha<br> # 4 - Beta<br> # 5 - Production/Stable<br> <span class="hljs-string">'Development Status :: 3 - Alpha'</span>,<br> # 开发的目标用户<br> <span class="hljs-string">'Intended Audience :: Developers'</span>,<br> <span class="hljs-string">"Topic :: Software Development :: Libraries :: Python Modules"</span>,<br> <span class="hljs-string">"Programming Language :: Python :: 3.6"</span>,<br> <span class="hljs-string">"Programming Language :: Python :: 3.7"</span>,<br> <span class="hljs-string">"Programming Language :: Python :: 3.8"</span>,<br> <span class="hljs-string">"Programming Language :: Python :: 3.9"</span>,<br> ],<br>)<br></code></pre></td></tr></table></figure><p>各个参数的含义</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-attr">--name</span> 库名称,▲需要注意的是不要大写,不然会有坑<br><span class="hljs-attr">--version</span> (-V) 包版本<br><span class="hljs-attr">--author</span> 程序的作者<br><span class="hljs-attr">--author_email</span> 程序的作者的邮箱地址<br><span class="hljs-attr">--maintainer</span> 维护者<br><span class="hljs-attr">--maintainer_email</span> 维护者的邮箱地址<br><span class="hljs-attr">--url</span> 程序的官网地址<br><span class="hljs-attr">--license</span> 程序的授权信息<br><span class="hljs-attr">--description</span> 程序的简单描述<br><span class="hljs-attr">--long_description</span> 程序的详细描述<br><span class="hljs-attr">--platforms</span> 程序适用的软件平台列表<br><span class="hljs-attr">--classifiers</span> 程序的所属分类列表<br><span class="hljs-attr">--keywords</span> 程序的关键字列表<br><span class="hljs-attr">--packages</span> 需要处理的包目录(包含__init__.py的文件夹) <br><span class="hljs-attr">--py_modules</span> 需要打包的python文件列表<br><span class="hljs-attr">--download_url</span> 程序的下载地址<br><span class="hljs-attr">--cmdclass</span> <br><span class="hljs-attr">--data_files</span> 打包时需要打包的数据文件,如图片,配置文件等<br><span class="hljs-attr">--scripts</span> 安装时需要执行的脚步列表<br><span class="hljs-attr">--package_dir</span> 告诉setuptools哪些目录下的文件被映射到哪个源码包。一个例子:package_dir = {<span class="hljs-string">''</span>: <span class="hljs-string">'lib'</span>},表示“root package”中的模块都在lib 目录中。<br><span class="hljs-attr">--requires</span> 定义依赖哪些模块 <br><span class="hljs-attr">--provides</span>定义可以为哪些模块提供依赖 <br><span class="hljs-attr">--find_packages</span>() 对于简单工程来说,手动增加packages参数很容易,刚刚我们用到了这个函数,它默认在和setup.py同一目录下搜索各个含有 __init__.py的包。<br><span class="hljs-attr">--install_requires</span> = <span class="hljs-selector-attr">[<span class="hljs-string">"requests"</span>]</span> 需要安装的依赖包<br><span class="hljs-attr">--entry_points</span> 动态发现服务和插件<br></code></pre></td></tr></table></figure><h4 id="setup-py-需要的模块"><a href="#setup-py-需要的模块" class="headerlink" title="setup.py 需要的模块"></a>setup.py 需要的模块</h4><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">setuptools</span>>=<span class="hljs-number">50</span>.<span class="hljs-number">3</span>.<span class="hljs-number">2</span><br><span class="hljs-attribute">wheel</span>>=<span class="hljs-number">0</span>.<span class="hljs-number">35</span>.<span class="hljs-number">1</span><br><span class="hljs-attribute">twine</span>>=<span class="hljs-number">3</span>.<span class="hljs-number">2</span>.<span class="hljs-number">0</span><br></code></pre></td></tr></table></figure><h4 id="setup-py-打包命令"><a href="#setup-py-打包命令" class="headerlink" title="setup.py 打包命令"></a>setup.py 打包命令</h4><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs vim"><span class="hljs-keyword">python3</span> setup.<span class="hljs-keyword">py</span> sdist bdist_wheel<br></code></pre></td></tr></table></figure><p>此处列举一些常用命令:</p><ul><li>build:</li></ul><p>构建安装时所需的所有内容</p><ul><li>build_ext:</li></ul><p>构建扩展,如用 C/C++, Cython 等编写的扩展,在调试时通常加 –inplace 参数,表示原地编译,即生成的扩展与源文件在同样的位置。</p><ul><li>sdist:</li></ul><p>构建源码分发包,在 Windows 下为 zip 格式,Linux 下为 tag.gz 格式 。执行 sdist 命令时,默认会被打包的文件:</p><figure class="highlight arduino"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs arduino">所有 py_modules 或 packages 指定的源码文件<br>所有 ext_modules 指定的文件<br>所有 package_data 或 data_files 指定的文件<br>所有 scripts 指定的脚本文件<br>README、README.txt、setup.py 和 setup.cfg文件<br>该命令构建的包主要用于发布,例如上传到 pypi 上。<br></code></pre></td></tr></table></figure><ul><li>bdist:</li></ul><p>构建一个二进制的分发包。</p><ul><li>bdist_egg:</li></ul><p>构建一个 egg 分发包,经常用来替代基于 bdist 生成的模式</p><ul><li>bdist_wheel:</li></ul><p>构建一个 wheel 分发包,egg 包是过时的,whl 包是新的标准</p><ul><li>install:</li></ul><p>安装包到系统环境中。</p><ul><li>develop:</li></ul><p>以开发方式安装包,该命名不会真正的安装包,而是在系统环境中创建一个软链接指向包实际所在目录。这边在修改包之后不用再安装就能生效,便于调试。</p><h4 id="发布到私服仓库"><a href="#发布到私服仓库" class="headerlink" title="发布到私服仓库"></a>发布到私服仓库</h4><p>需要在当前用户根目录下添加 .pypirc 文件,内容如下:</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs routeros">[distutils]<br>index-servers =<br> nexus<br><br>[nexus]<br><span class="hljs-attribute">repository</span>=https://nexus3.dui88.com/repository/hosted/<br><span class="hljs-attribute">username</span>=pythoner<br><span class="hljs-attribute">password</span>=xxx<br></code></pre></td></tr></table></figure><p>执行命令:</p><figure class="highlight mipsasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs mipsasm">twine upload -r nexus <span class="hljs-keyword">dist/* </span><span class="hljs-comment"># 这里是上传所有文件,每次构建的时候旧的版本也会保留在这个目录下面,上传的时候需要确认下。</span><br></code></pre></td></tr></table></figure><p>这样其它系统就可以直接安装该模块了。因为我们使用了nexus私服,如果每次 <code>pip install</code> 指定源仓库很麻烦,可以全局设置为私服。方法如下:</p><p>修改 ~/.pip/pip.conf (没有就自己创建), 增加或者修改 index-url 至源</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs ini"><span class="hljs-section">[global]</span><br><span class="hljs-attr">timeout</span> = <span class="hljs-number">60</span><br><span class="hljs-attr">index-url</span> = https://nexus3.dui88.com/repository/pypi-group/simple<br></code></pre></td></tr></table></figure><h3 id="坑点记录"><a href="#坑点记录" class="headerlink" title="坑点记录"></a>坑点记录</h3><p>当程序里有.json或者.txt一些非py文件的时候不会自动打包。这时候就要靠<code>include_package_data</code> 和 <code>data_files</code> 来指定了。data_files一般写成 {‘your_package_name’: [“files”]} 或者直接路径。还需要一个 <code>MANIFEST.in</code> 文件来明确指定哪些文件需要打到包中。</p><p><code>MANIFEST.in</code> 文件内容</p><figure class="highlight autoit"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs autoit"><span class="hljs-meta"># <span class="hljs-keyword">Include</span> the README</span><br>include *.md<br><br><span class="hljs-meta"># <span class="hljs-keyword">Include</span> the txt file</span><br><span class="hljs-meta"># <span class="hljs-keyword">include</span> */*.txt</span><br><br><span class="hljs-meta"># <span class="hljs-keyword">Include</span> the data files</span><br>recursive-include duiba_ext/common/fake_useragent.json<br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>python</category>
</categories>
<tags>
<tag>setup.py</tag>
</tags>
</entry>
<entry>
<title>Spring 关于getBeansOfType获取不到实例的问题</title>
<link href="/spring-bean/"/>
<url>/spring-bean/</url>
<content type="html"><![CDATA[<h4 id="问题描述"><a href="#问题描述" class="headerlink" title="问题描述"></a>问题描述</h4><p>ElasticJob官方只有xml注册job,有很多job的时候文件非常臃肿而且不好维护,还有官方的控制台也不好用,系统多之后非常卡,页面加载的时候要一次性加载所有任务。所以我们开发出使用注解来创建job和自研的控制台。问题出在在手动触发job,通过job的class获取实例时获取不到,而且只有一个系统有问题。</p><p>Job的bean注册到容器的代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-type">DefaultListableBeanFactory</span> <span class="hljs-variable">defaultListableBeanFactory</span> <span class="hljs-operator">=</span> (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();<br>defaultListableBeanFactory.registerBeanDefinition(beanId, beanDefinition);<br></code></pre></td></tr></table></figure><p>获取job实例的代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">Map<String, ?> beanMap = applicationContext.getBeansOfType(jobClass);<br></code></pre></td></tr></table></figure><p>上面的代码获取不到job的bean,但是这个bean确确实实的在spring容器里。</p><h4 id="进一步debug"><a href="#进一步debug" class="headerlink" title="进一步debug"></a>进一步debug</h4><p>getBeansOfType的源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-meta">@Override</span><br><span class="hljs-keyword">public</span> String[] getBeanNamesForType(<span class="hljs-meta">@Nullable</span> Class<?> type, <span class="hljs-type">boolean</span> includeNonSingletons, <span class="hljs-type">boolean</span> allowEagerInit) {<br><span class="hljs-keyword">if</span> (!isConfigurationFrozen() || type == <span class="hljs-literal">null</span> || !allowEagerInit) {<br><span class="hljs-keyword">return</span> doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, allowEagerInit);<br>}<br>Map<Class<?>, String[]> cache =<br>(includeNonSingletons ? <span class="hljs-built_in">this</span>.allBeanNamesByType : <span class="hljs-built_in">this</span>.singletonBeanNamesByType);<br>String[] resolvedBeanNames = cache.get(type);<br><span class="hljs-keyword">if</span> (resolvedBeanNames != <span class="hljs-literal">null</span>) {<br><span class="hljs-keyword">return</span> resolvedBeanNames;<br>}<br><br><span class="hljs-comment">// 上面内容是去缓存中获取,如果获取不到走下面的方法,同时会更新的bean的缓存</span><br>resolvedBeanNames = doGetBeanNamesForType(ResolvableType.forRawClass(type), includeNonSingletons, <span class="hljs-literal">true</span>);<br><span class="hljs-keyword">if</span> (ClassUtils.isCacheSafe(type, getBeanClassLoader())) {<br>cache.put(type, resolvedBeanNames);<br>}<br><span class="hljs-keyword">return</span> resolvedBeanNames;<br>}<br></code></pre></td></tr></table></figure><p>注释的下面一行获取的对象总是空对象,所以问题出在doGetBeanNamesForType这个方法</p><p>doGetBeanNamesForType的部分源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs java">...<br><br><span class="hljs-keyword">if</span> (!isFactoryBean) {<br><span class="hljs-keyword">if</span> (includeNonSingletons || isSingleton(beanName, mbd, dbd)) {<br><span class="hljs-comment">// 这里是false就是没有匹配到bean</span><br>matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);<br>}<br>}<br><br>...<br><br></code></pre></td></tr></table></figure><p>isTypeMatch的部分源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs java"><br>...<br><br><span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (!isFactoryDereference) {<br><span class="hljs-comment">// 这一步返回的是false,所以继续往里面debug</span><br><span class="hljs-keyword">if</span> (typeToMatch.isInstance(beanInstance)) {<br><span class="hljs-comment">// Direct match for exposed instance?</span><br><span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>}<br>...<br><br></code></pre></td></tr></table></figure><p>最终返回false的地方,ClassUtils源码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-type">boolean</span> <span class="hljs-title function_">isAssignable</span><span class="hljs-params">(Class<?> lhsType, Class<?> rhsType)</span> {<br>Assert.notNull(lhsType, <span class="hljs-string">"Left-hand side type must not be null"</span>);<br>Assert.notNull(rhsType, <span class="hljs-string">"Right-hand side type must not be null"</span>);<br><span class="hljs-comment">// 这一步返回的false,lhsType是LaunchClassLoader,rhsType是RestartClassLoader</span><br><span class="hljs-keyword">if</span> (lhsType.isAssignableFrom(rhsType)) {<br><span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;<br>}<br><span class="hljs-keyword">if</span> (lhsType.isPrimitive()) {<br>Class<?> resolvedPrimitive = primitiveWrapperTypeMap.get(rhsType);<br><span class="hljs-keyword">return</span> (lhsType == resolvedPrimitive);<br>}<br><span class="hljs-keyword">else</span> {<br>Class<?> resolvedWrapper = primitiveTypeToWrapperMap.get(rhsType);<br><span class="hljs-keyword">return</span> (resolvedWrapper != <span class="hljs-literal">null</span> && lhsType.isAssignableFrom(resolvedWrapper));<br>}<br>}<br></code></pre></td></tr></table></figure><p>两个Class对象的类加载器不是同一个。全局搜索RestartClassLoader 发现其属于依赖 Spring-boot-devtools 引入的包,继承自URLClassLoader,是Spring热部署功能代码。</p><h6 id="延伸阅读"><a href="#延伸阅读" class="headerlink" title="延伸阅读"></a>延伸阅读</h6><p><a href="https://www.dazhuanlan.com/2019/08/30/5d6859a908b16/">由RestartClassLoader探索Springboot热部署</a></p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Spring</tag>
<tag>ElasticJob</tag>
</tags>
</entry>
<entry>
<title>pyppeteer 内存泄漏排查</title>
<link href="/pyppeteer-process/"/>
<url>/pyppeteer-process/</url>
<content type="html"><![CDATA[<blockquote><p>在工作中很少能够碰到内存泄漏的问题,但是一旦遇到了,就是一个比较难解的问题,<br> 本文旨在记录这次在问题排查的过程中,一些思路和排查方向</p></blockquote><p> 收到告警后,笔者先登录到告警机器中,<br> top 命令查看此时此刻的各个应用程序占用的内存大小,发现没有占用很大内存的进程。<br> 执行 <code>ps -ef</code> 发现有很多chromium,查了资料都说是僵尸进程,但是僵尸进程应该不占用内存和cpu的。</p><blockquote><p>僵尸子进程已经放弃了几乎所有的内存空间,没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置,记载该进程的退出状态信息供其他进程收集,除此之外,僵尸进程不再占有任何存储空间。他需要他的父进程来为他收尸,如果他的父进程没有安装SIGCHLD信号处理函数调用wait 或 waitpid() 等待子进程结束,也没有显式忽略该信号,那么它就一直保持僵尸状态,如果这时候父进程结束了,那么init进程会自动接手这个子进程,为他收尸,他还是能被清除掉的。但是如果父进程是一个循环,不会结束,那么子进程就会一直保持僵尸状态,这就是系统中为什么有时候会有很多的僵尸进程。</p></blockquote><h5 id="第一步先内存分析"><a href="#第一步先内存分析" class="headerlink" title="第一步先内存分析"></a>第一步先内存分析</h5><p>网上有很多方式,没几个好用的。后来发现一款工具 <code>pyrasite</code>, 可以直接连上一个正在运行的python程序,打开一个类似python的交互终端来运行命令、检查程序状态。很像阿里开源的的Arthas,这种真的超级方便。</p><p>首先安装</p><figure class="highlight 1c"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs 1c"><span class="hljs-meta"># pip install pyrasite</span><br></code></pre></td></tr></table></figure><p>连接到有问题的python程序,开始收集信息</p><figure class="highlight python-repl"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs python-repl">pyrasite-shell <pid><br><span class="hljs-meta prompt_">>>></span><br></code></pre></td></tr></table></figure><p>接下来就可以在<pid>进程里调用任意python代码,查看进程状态。<br>所以就可以使用 guppy 获取内存使用的各种对象占用情况,可以打印各种对象所占空间大小,如果python进程中有未释放的对象,造成内存占用升高,可通过guppy查看。</p><figure class="highlight capnproto"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs capnproto"><span class="hljs-comment"># pip install guppy</span><br><span class="hljs-keyword">from</span> guppy <span class="hljs-keyword">import</span> hpy<br>h = hpy()<br>h.heap()<br></code></pre></td></tr></table></figure><p>结果运行如下:</p><figure class="highlight dns"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><code class="hljs dns"># Partition of a set of <span class="hljs-number">48477</span> objects. Total size = <span class="hljs-number">3265516</span> bytes.<br># Index Count % Size % Cumulative % Kind (class / dict of class)<br># <span class="hljs-number">0 25773 53</span> <span class="hljs-number">1612820 49</span> <span class="hljs-number">1612820 49</span> str<br># <span class="hljs-number">1 11699 24</span> <span class="hljs-number">483960 15</span> <span class="hljs-number">2096780 64</span> tuple<br># <span class="hljs-number">2 174 0</span> <span class="hljs-number">241584 7</span> <span class="hljs-number">2338364 72</span> dict of module<br># <span class="hljs-number">3 3478 7</span> <span class="hljs-number">222592 7</span> <span class="hljs-number">2560956 78</span> types.CodeType<br># <span class="hljs-number">4 3296 7</span> <span class="hljs-number">184576 6</span> <span class="hljs-number">2745532 84</span> function<br># <span class="hljs-number">5 401 1</span> <span class="hljs-number">175112 5</span> <span class="hljs-number">2920644 89</span> dict of class<br># <span class="hljs-number">6 108 0</span> <span class="hljs-number">81888 3</span> <span class="hljs-number">3002532 92</span> dict (no owner)<br># <span class="hljs-number">7 114 0</span> <span class="hljs-number">79632 2</span> <span class="hljs-number">3082164 94</span> dict of type<br># <span class="hljs-number">8 117 0</span> <span class="hljs-number">51336 2</span> <span class="hljs-number">3133500 96</span> type<br># <span class="hljs-number">9 667 1</span> <span class="hljs-number">24012 1</span> <span class="hljs-number">3157512 97</span> __builtin__.wrapper_descriptor<br># <<span class="hljs-number">76</span> more rows. Type e.g. '_.more' to view.><br></code></pre></td></tr></table></figure><p>发现占用最大的是str,占比很小,所以确定就是僵尸进程的问题了。</p><h5 id="解决僵尸进程"><a href="#解决僵尸进程" class="headerlink" title="解决僵尸进程"></a>解决僵尸进程</h5><p>使用专门的 init 进程 <a href="https://juejin.im/post/6844904029248552973">查看文章</a><br>原来的启动方式:</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-selector-id">#ENTRYPOINT</span> gunicorn -w <span class="hljs-number">1</span> <span class="hljs-attr">--threads</span> <span class="hljs-number">60</span> -<span class="hljs-selector-tag">b</span> <span class="hljs-number">0.0</span>.<span class="hljs-number">0.0</span>:<span class="hljs-number">5000</span> server:app<br></code></pre></td></tr></table></figure><p>变更后:</p><figure class="highlight css"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs css">ENTRYPOINT <span class="hljs-selector-attr">[<span class="hljs-string">"dumb-init"</span>, <span class="hljs-string">"--"</span>]</span><br>CMD <span class="hljs-selector-attr">[<span class="hljs-string">"bash"</span>, <span class="hljs-string">"-c"</span>, <span class="hljs-string">"gunicorn -w 1 --threads 60 -b 0.0.0.0:5000 server:app"</span>]</span><br></code></pre></td></tr></table></figure><p>运行一段时间进程照样还是多,问题没有解决</p><h5 id="手动关闭chrome进程"><a href="#手动关闭chrome进程" class="headerlink" title="手动关闭chrome进程"></a>手动关闭chrome进程</h5><p><a href="https://stackoverflow.com/questions/53939503/puppeteer-doesnt-close-browser">原文地址</a></p><p>修改之后的代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">if</span> browser <span class="hljs-keyword">is</span> <span class="hljs-keyword">not</span> <span class="hljs-literal">None</span>:<br> <span class="hljs-keyword">await</span> browser.close()<br>os.system(<span class="hljs-string">'pkill chrome'</span>)<br></code></pre></td></tr></table></figure><p>问题还是没有解决</p><h5 id="从代码方面解决"><a href="#从代码方面解决" class="headerlink" title="从代码方面解决"></a>从代码方面解决</h5><p>通过调试程序,发现程序一直卡在这里,当没有这个节点的时候一直在等待</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs ini"><span class="hljs-attr">title_elements</span> = await page.xpath(<span class="hljs-string">'//*[@class="cc-button cc-button-default ad-login-comp-btn "]/div'</span>)<br></code></pre></td></tr></table></figure><p>修改为问题解决</p><figure class="highlight reasonml"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs reasonml">await page.wait<span class="hljs-constructor">ForXPath('<span class="hljs-operator">/</span><span class="hljs-operator">/</span><span class="hljs-operator">*</span>[@<span class="hljs-params">class</span>=<span class="hljs-string">"cc-button cc-button-default ad-login-comp-btn "</span>]<span class="hljs-operator">/</span><span class="hljs-params">div</span>', {'<span class="hljs-params">timeout</span>': 2000})</span><br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>爬虫</category>
</categories>
<tags>
<tag>pyppeteer</tag>
</tags>
</entry>
<entry>
<title>使用Airtest爬虫总结和注意事项</title>
<link href="/airtest-sum/"/>
<url>/airtest-sum/</url>
<content type="html"><![CDATA[<h5 id="为什么不使用Appium"><a href="#为什么不使用Appium" class="headerlink" title="为什么不使用Appium"></a>为什么不使用Appium</h5><p>一开始调研的时候使用的就是Appium,功能全,文档也很多,而且元素定位方式比Airtest靠谱很多。我们要爬的是美团的活动广告页,开始使用appinum都很顺利,但是遇到webview的时候,它的页面是canvas的结构,根本没办法使用元素定位,所以这个时候Airtest的图像定位就起作用了。</p><h5 id="webview如何开启debug模式"><a href="#webview如何开启debug模式" class="headerlink" title="webview如何开启debug模式"></a>webview如何开启debug模式</h5><p>网上的大部分教程都是手动修改app的源码(<a href="https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews?hl=zh-cn">官网文档</a>),但是第三方总不会给你个debug的包吧。</p><p>Android有个一个很牛逼的软件<a href="https://taichi.cool/zh/download.html">太极</a>,手机不需要Root就可以运行Xposed框架。里面有很多模块都很不错,比如钉钉助手、抢红包,有兴趣的同学可以试试,一定会屡试不爽的。<br>所以,webview开启debug也有模块,<a href="https://github.com/taichi-framework/TaiChi/issues/805">查看地址</a></p><h5 id="巧用keyevent-“BACK”-替代返回的截图脚本"><a href="#巧用keyevent-“BACK”-替代返回的截图脚本" class="headerlink" title="巧用keyevent(“BACK”)替代返回的截图脚本"></a>巧用keyevent(“BACK”)替代返回的截图脚本</h5><p>很多时候,我们需要从APP的某个页面,回到APP首页,一些同学可能会使用一堆的返回图标的截图语句,来实现这个需求: </p><p><img src="/images/125b98339a0425e.jpeg"><br>实际上,如果同学们测的是安卓设备,完全可以用 keyevent(“BACK”) 来替代这个返回的截图语句,更加稳定高效: </p><p><img src="/images/965e175d5b414762.image"></p><h5 id="局部截图保存到指定位置"><a href="#局部截图保存到指定位置" class="headerlink" title="局部截图保存到指定位置"></a>局部截图保存到指定位置</h5><p>局部截图或者说按坐标截图是大家经常会问到的问题,Airtest提供了 crop_image(img, rect) 方法可以帮助我们实现局部截图,但是<a href="https://airtest.doc.io.netease.com/IDEdocs/faq/4_common%20problems/#8">官方demo</a>并没有给出保存到指定位置:</p><p>举个例子,我们想要截取手机屏幕中被红框圈中位置的截图:<br><img src="/images/aHR0cHM6Ly9ub3Rl.png"></p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-comment"># -*- encoding=utf8 -*-</span><br>__author__ = <span class="hljs-string">"AirtestProject"</span><br><br><span class="hljs-keyword">from</span> airtest.core.api <span class="hljs-keyword">import</span> *<br><span class="hljs-comment"># crop_image()方法在airtest.aircv中,需要引入</span><br><span class="hljs-keyword">from</span> airtest.aircv <span class="hljs-keyword">import</span> *<br><br>auto_setup(__file__)<br><br><span class="hljs-comment"># 局部截图</span><br>screen = G.DEVICE.snapshot() <span class="hljs-comment">#这一步之后不能打开其它页面,因为是截取当前页面</span><br>screen = aircv.crop_image(screen,(<span class="hljs-number">0</span>,<span class="hljs-number">160</span>,<span class="hljs-number">1067</span>,<span class="hljs-number">551</span>))<br><span class="hljs-comment"># 保存局部截图到log文件夹中</span><br>filename = <span class="hljs-string">"%(time)d.jpg"</span> % {<span class="hljs-string">'time'</span>: time.time() * <span class="hljs-number">1000</span>}<br>filepath = os.path.join(<span class="hljs-string">'需要保存的路径'</span>, filename)<br>aircv.imwrite(filepath, screen, quality, max_size=max_size)<br></code></pre></td></tr></table></figure><h5 id="如何在flaskweb项目中运行"><a href="#如何在flaskweb项目中运行" class="headerlink" title="如何在flaskweb项目中运行"></a>如何在flaskweb项目中运行</h5><p>首先要知道如何脱离IDEA在python环境下直接运行 <a href="https://airtest.doc.io.netease.com/IDEdocs/run_script/1_useCommand_runScript/#python">官网教程</a></p><p>比如使用apscheduler定时运行airtest脚本</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-keyword">import</span> os<br><br>devices = [{<span class="hljs-string">'name'</span>: <span class="hljs-string">'34235ed0'</span>, <span class="hljs-string">'location'</span>: <span class="hljs-string">'北京'</span>}, {<span class="hljs-string">'name'</span>: <span class="hljs-string">'2a5b398e0005'</span>, <span class="hljs-string">'location'</span>: <span class="hljs-string">'上海'</span>}]<br>comand = <span class="hljs-string">"airtest run {} --device android://127.0.0.1:5037/{} --log ~/logs/berserker/meituan"</span><br>air_path = <span class="hljs-string">'{}/airtest.shell/meituan.air'</span>.<span class="hljs-built_in">format</span>(os.getcwd())<br><br><span class="hljs-keyword">def</span> <span class="hljs-title function_">run</span>():<br> <span class="hljs-keyword">for</span> device <span class="hljs-keyword">in</span> devices:<br> os.system(comand.<span class="hljs-built_in">format</span>(air_path, device.get(<span class="hljs-string">'name'</span>)))<br></code></pre></td></tr></table></figure><h5 id="搭建手机爬虫集群"><a href="#搭建手机爬虫集群" class="headerlink" title="搭建手机爬虫集群"></a>搭建手机爬虫集群</h5><p>一台电脑可以连接三十台手机,那么如果有很多电脑和很多手机,就可以实现手机爬虫集群,其运行效果如下图所示。<br>!()[/images/ccdf080c7af7e8a1.png]</p><p>可以使用手机的无线调试模式,首先想到的是搭建手机管理平台 <a href="https://github.com/openstf/stf">STF</a><br>获取adb的url代码如下:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-comment"># 获取手机的adb地址</span><br><span class="hljs-keyword">async</span> <span class="hljs-keyword">def</span> <span class="hljs-title function_">take_up_device</span>(<span class="hljs-params">device=<span class="hljs-string">''</span></span>):<br> <span class="hljs-keyword">async</span> <span class="hljs-keyword">with</span> aiohttp.ClientSession(connector=TCPConnector(verify_ssl=<span class="hljs-literal">False</span>), timeout=timeout) <span class="hljs-keyword">as</span> session:<br> headers = {<span class="hljs-string">'Authorization'</span>: <span class="hljs-string">'Bearer {}'</span>.<span class="hljs-built_in">format</span>(cgf.get(<span class="hljs-string">'yunce'</span>, <span class="hljs-string">'auth_token'</span>))}<br> result = <span class="hljs-keyword">await</span> async_utils.form_post(session, take_url, is_re_json=<span class="hljs-literal">True</span>, headers=headers,<br> json_data={<span class="hljs-string">'serial'</span>: device, <span class="hljs-string">'timeout'</span>: <span class="hljs-number">600000</span>})<br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> result.get(<span class="hljs-string">'success'</span>, <span class="hljs-literal">False</span>):<br> app_logger.error(<span class="hljs-string">'获取设备异常,错误信息:%s'</span>, result.get(<span class="hljs-string">'description'</span>))<br> <span class="hljs-keyword">return</span><br><br> result = <span class="hljs-keyword">await</span> async_utils.fetch(session, adb_url.<span class="hljs-built_in">format</span>(serial=device), is_re_json=<span class="hljs-literal">True</span>,<br> headers=headers)<br><br> <span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> result.get(<span class="hljs-string">'success'</span>, <span class="hljs-literal">False</span>):<br> app_logger.error(<span class="hljs-string">'获取设备adb链接错误,错误信息:%s'</span>, result.get(<span class="hljs-string">'description'</span>))<br> <span class="hljs-keyword">return</span><br><br> <span class="hljs-keyword">return</span> result.get(<span class="hljs-string">'device'</span>, {}).get(<span class="hljs-string">'remoteConnectUrl'</span>)<br></code></pre></td></tr></table></figure><p>有了这些,是不是就可以搞个刷抖音平台?</p><h5 id="结尾"><a href="#结尾" class="headerlink" title="结尾"></a>结尾</h5><p>附上爬取美团活动的代码:</p><figure class="highlight python"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br><span class="line">63</span><br><span class="line">64</span><br><span class="line">65</span><br><span class="line">66</span><br><span class="line">67</span><br><span class="line">68</span><br><span class="line">69</span><br><span class="line">70</span><br><span class="line">71</span><br><span class="line">72</span><br><span class="line">73</span><br><span class="line">74</span><br><span class="line">75</span><br><span class="line">76</span><br><span class="line">77</span><br><span class="line">78</span><br><span class="line">79</span><br><span class="line">80</span><br><span class="line">81</span><br></pre></td><td class="code"><pre><code class="hljs python"><span class="hljs-comment"># -*- encoding=utf8 -*-</span><br>__author__ = <span class="hljs-string">"panaihua"</span><br><br><span class="hljs-keyword">from</span> airtest.core.api <span class="hljs-keyword">import</span> *<br><span class="hljs-keyword">from</span> poco.drivers.android.uiautomation <span class="hljs-keyword">import</span> AndroidUiautomationPoco<br><span class="hljs-keyword">import</span> logging<br><span class="hljs-keyword">import</span> os<br><span class="hljs-keyword">import</span> time<br><br>logger = logging.getLogger(<span class="hljs-string">"airtest"</span>)<br>logger.setLevel(logging.ERROR)<br><br>auto_setup(__file__)<br><br><span class="hljs-comment"># connect_device("android://127.0.0.1:5037/172.16.140.68:17593?cap_method=MINICAP_STREAM&&ori_method=MINICAPORI&&touch_method=MINITOUCH")</span><br><br>poco = AndroidUiautomationPoco(use_airtest_input=<span class="hljs-literal">True</span>, screenshot_each_action=<span class="hljs-literal">False</span>)<br><br>wake()<br><br>stop_app(<span class="hljs-string">'com.sankuai.meituan'</span>)<br><br>home()<br><br>start_app(<span class="hljs-string">'com.sankuai.meituan'</span>)<br><br>sleep(<span class="hljs-number">10</span>)<br>poco(<span class="hljs-string">"免费领水果"</span>).click()<br><br>sleep(<span class="hljs-number">10</span>)<br><br><span class="hljs-keyword">if</span> exists(Template(<span class="hljs-string">r"tpl1604041772025.png"</span>, record_pos=(<span class="hljs-number">0.017</span>, <span class="hljs-number">0.381</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>))):<br> touch(Template(<span class="hljs-string">r"tpl1604041788888.png"</span>, record_pos=(<span class="hljs-number">0.381</span>, -<span class="hljs-number">0.484</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)))<br><br>touch(Template(<span class="hljs-string">r"tpl1603789796067.png"</span>, record_pos=(-<span class="hljs-number">0.388</span>, <span class="hljs-number">0.666</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)))<br><br>sleep(<span class="hljs-number">5</span>)<br>swipe((<span class="hljs-number">527</span>, <span class="hljs-number">2044</span>), (<span class="hljs-number">527</span>, <span class="hljs-number">280</span>), duration=<span class="hljs-number">3</span>)<br><br>sleep(<span class="hljs-number">15</span>)<br><br><span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> exists(Template(<span class="hljs-string">r"tpl1603854492318.png"</span>, record_pos=(<span class="hljs-number">0.007</span>, <span class="hljs-number">0.362</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>))):<br> swipe((<span class="hljs-number">527</span>, <span class="hljs-number">2044</span>), (<span class="hljs-number">527</span>, <span class="hljs-number">280</span>), duration=<span class="hljs-number">3</span>)<br><br>assert_exists(Template(<span class="hljs-string">r"tpl1603854492318.png"</span>, record_pos=(<span class="hljs-number">0.007</span>, <span class="hljs-number">0.362</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)), <span class="hljs-string">'玩游戏的按钮不存在'</span>)<br><br><span class="hljs-keyword">if</span> exists(Template(<span class="hljs-string">r"tpl1603873295969.png"</span>, record_pos=(-<span class="hljs-number">0.006</span>, <span class="hljs-number">0.51</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>))):<br> touch(Template(<span class="hljs-string">r"tpl1603873313059.png"</span>, record_pos=(<span class="hljs-number">0.009</span>, <span class="hljs-number">0.509</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)))<br> sleep(<span class="hljs-number">5</span>)<br><br>touch(Template(<span class="hljs-string">r"tpl1603854492318.png"</span>, record_pos=(<span class="hljs-number">0.007</span>, <span class="hljs-number">0.362</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)))<br><br>sleep(<span class="hljs-number">15.0</span>)<br><br>assert_not_exists(Template(<span class="hljs-string">r"tpl1603798232722.png"</span>, record_pos=(<span class="hljs-number">0.017</span>, -<span class="hljs-number">0.393</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)),<br> <span class="hljs-string">"今日游戏次数已经用完,退出"</span>)<br><br>touch(Template(<span class="hljs-string">r"tpl1603791478271.png"</span>, record_pos=(-<span class="hljs-number">0.001</span>, <span class="hljs-number">0.165</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)))<br><br>sleep(<span class="hljs-number">15.0</span>)<br><br>now = time.localtime()<br>path = os.environ.get(<span class="hljs-string">'BERSERKER_ENV'</span>) + <span class="hljs-string">'/meituan'</span><br><span class="hljs-keyword">if</span> <span class="hljs-keyword">not</span> os.path.exists(path):<br> os.makedirs(path, exist_ok=<span class="hljs-literal">True</span>)<br><br>file_name = <span class="hljs-string">'{}/activity-{}.png'</span>.<span class="hljs-built_in">format</span>(path, time.strftime(<span class="hljs-string">"%Y%m%d-%H"</span>, now))<br><br>snapshot(file_name)<br><br><span class="hljs-keyword">if</span> exists(Template(<span class="hljs-string">r"tpl1603792766280.png"</span>, record_pos=(-<span class="hljs-number">0.014</span>, <span class="hljs-number">0.256</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>))):<br> touch(Template(<span class="hljs-string">r"tpl1603792766280.png"</span>, record_pos=(-<span class="hljs-number">0.014</span>, <span class="hljs-number">0.256</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)))<br><span class="hljs-keyword">else</span>:<br> touch(Template(<span class="hljs-string">r"tpl1603855598743.png"</span>, record_pos=(-<span class="hljs-number">0.007</span>, <span class="hljs-number">0.439</span>), resolution=(<span class="hljs-number">1080</span>, <span class="hljs-number">2160</span>)))<br><br>sleep(<span class="hljs-number">15.0</span>)<br>file_name = <span class="hljs-string">'{}/advert-{}.png'</span>.<span class="hljs-built_in">format</span>(path, time.strftime(<span class="hljs-string">"%Y%m%d-%H"</span>, now))<br><br>snapshot(file_name)<br><br>stop_app(<span class="hljs-string">'com.sankuai.meituan'</span>)<br></code></pre></td></tr></table></figure><p><a href="http://airtest.netease.com/">Airtest官网</a>:airtest.netease.com<br><a href="https://airtest.doc.io.netease.com/">Airtest教程官网</a>:airtest.doc.io.netease.com</p>]]></content>
<categories>
<category>爬虫</category>
</categories>
<tags>
<tag>Airtest</tag>
</tags>
</entry>
<entry>
<title>自适应限流 netflix-concurrency-limits</title>
<link href="/concurrency-limits/"/>
<url>/concurrency-limits/</url>
<content type="html"><![CDATA[<p>作为应对高并发的手段之一,限流并不是一个新鲜的话题了。从Guava的Ratelimiter到Hystrix,以及Sentinel都可作为限流的工具。</p><h3 id="自适应限流"><a href="#自适应限流" class="headerlink" title="自适应限流"></a>自适应限流</h3><p>一般的限流常常需要指定一个固定值(qps)作为限流开关的阈值,这个值一是靠经验判断,二是靠通过大量的测试数据得出。但这个阈值,在流量激增、系统自动伸缩或者某某commit了一段有毒代码后就有可能变得不那么合适了。并且一般业务方也不太能够正确评估自己的容量,去设置一个合适的限流阈值。</p><p>而此时自适应限流就是解决这样的问题的,限流阈值不需要手动指定,也不需要去预估系统的容量,并且阈值能够随着系统相关指标变化而变化。</p><p>自适应限流算法借鉴了TCP拥塞算法,根据各种指标预估限流的阈值,并且不断调整。大致获得的效果如下:</p><p><img src="/images/20191104083803.jpg"></p><p>从图上可以看到,首先以一个降低的初始并发值发送请求,同时通过增大限流窗口来探测系统更高的并发性。而一旦延迟增加到一定程度了,又会退回到较小的限流窗口。循环往复持续探测并发极限,从而产生类似锯齿状的时间关系函数。</p><h3 id="TCP-Vegas"><a href="#TCP-Vegas" class="headerlink" title="TCP Vegas"></a>TCP Vegas</h3><p>vegas是一种主动调整cwnd的拥塞控制算法,主要是设置两个阈值alpha 和 beta,然后通过计算目标速率和实际速率的差diff,再比较差diff与alpha和beta的关系,对cwnd进行调节。伪代码如下:</p><figure class="highlight powershell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><code class="hljs powershell"><span class="hljs-built_in">diff</span> = cwnd*(<span class="hljs-number">1</span><span class="hljs-literal">-baseRTT</span>/RTT)<br><span class="hljs-keyword">if</span> (<span class="hljs-built_in">diff</span> < alpha)<br><span class="hljs-built_in">set</span>: cwnd = cwnd + <span class="hljs-number">1</span> <br><span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (<span class="hljs-built_in">diff</span> >= beta)<br><span class="hljs-built_in">set</span>: cwnd = cwnd - <span class="hljs-number">1</span><br><span class="hljs-keyword">else</span><br><span class="hljs-built_in">set</span>: cwnd = cwnd<br></code></pre></td></tr></table></figure><p>其中baseRTT指的是测量的最小往返时间,RTT指的是当前测量的往返时间,cwnd指的是当前的TCP窗口大小。通常在tcp中alpha会被设置成2-3,beta会被设置成4-6。这样子,cwnd就保持在了一个平衡的状态。</p><h5 id="netflix-concuurency-limits"><a href="#netflix-concuurency-limits" class="headerlink" title="netflix-concuurency-limits"></a>netflix-concuurency-limits</h5><p>concuurency-limits是netflix推出的自适应限流组件,借鉴了TCP相关拥塞控制算法,主要是根据请求延时,及其直接影响到的排队长度来进行限流窗口的动态调整。</p><h5 id="alpha-beta-amp-threshold"><a href="#alpha-beta-amp-threshold" class="headerlink" title="alpha , beta & threshold"></a>alpha , beta & threshold</h5><p>vegas算法实现在了VegasLimit类中。先看一下初始化相关代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">initialLimit</span> <span class="hljs-operator">=</span> <span class="hljs-number">20</span>;<br> <span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-variable">maxConcurrency</span> <span class="hljs-operator">=</span> <span class="hljs-number">1000</span>;<br> <span class="hljs-keyword">private</span> <span class="hljs-type">MetricRegistry</span> <span class="hljs-variable">registry</span> <span class="hljs-operator">=</span> EmptyMetricRegistry.INSTANCE;<br> <span class="hljs-keyword">private</span> <span class="hljs-type">double</span> <span class="hljs-variable">smoothing</span> <span class="hljs-operator">=</span> <span class="hljs-number">1.0</span>;<br> <br> <span class="hljs-keyword">private</span> Function<Integer, Integer> alphaFunc = (limit) -> <span class="hljs-number">3</span> * LOG10.apply(limit.intValue());<br> <span class="hljs-keyword">private</span> Function<Integer, Integer> betaFunc = (limit) -> <span class="hljs-number">6</span> * LOG10.apply(limit.intValue());<br> <span class="hljs-keyword">private</span> Function<Integer, Integer> thresholdFunc = (limit) -> LOG10.apply(limit.intValue());<br> <span class="hljs-keyword">private</span> Function<Double, Double> increaseFunc = (limit) -> limit + LOG10.apply(limit.intValue());<br> <span class="hljs-keyword">private</span> Function<Double, Double> decreaseFunc = (limit) -> limit - LOG10.apply(limit.intValue());<br></code></pre></td></tr></table></figure><p>这里首先定义了一个初始化值initialLimit为20,以及极大值maxConcurrency1000。其次是三个<br>阈值函数alphaFunc,betaFunc以及thresholdFunc。最后是两个增减函数increaseFunc和decreaseFunc。<br>函数都是基于当前的并发值limit做运算的。</p><ul><li>alphaFunc可类比vegas算法中的alpha,此处的实现是3*log limit。limit值从初始20增加到极大1000时候,相应的alpha从3.9增加到了9。</li><li>betaFunc则可类比为vegas算法中的beta,此处的实现是6*log limit。limit值从初始20增加到极大1000时候,相应的alpha从7.8增加到了18。</li><li>thresholdFunc算是新增的一个函数,表示一个较为初始的阈值,小于这个值的时候limit会采取激进一些的增量算法。这里的实现是1倍的log limit。mit值从初始20增加到极大1000时候,相应的alpha从1.3增加到了3。</li></ul><p>这三个函数值可以认为确定了动态调整函数的四个区间范围。当变量queueSize = limit × (1 − RTTnoLoad/RTTactual)落到这四个区间的时候应用不同的调整函数。</p><h5 id="变量queueSize"><a href="#变量queueSize" class="headerlink" title="变量queueSize"></a>变量queueSize</h5><p>其中变量为queueSize,计算方法即为limit × (1 − RTTnoLoad/RTTactual),为什么这么计算其实稍加领悟一下即可。</p><p><img src="/images/2019110483804.jpg"></p><p>我们把系统处理请求的过程想象为一个水管,到来的请求是往这个水管灌水,当系统处理顺畅的时候,请求不需要排队,直接从水管中穿过,这个请求的RT是最短的,即RTTnoLoad;反之,当请求堆积的时候,那么处理请求的时间则会变为:排队时间+最短处理时间,即RTTactual = inQueueTime + RTTnoLoad。而显然排队的队列长度为<br>总排队时间/每个请求的处理时间及queueSize = (limit * inQueueTime) / (inQueueTime + RTTnoLoad) = limit × (1 − RTTnoLoad/RTTactual)。<br>再举个栗子,因为假设当前延时即为最佳延时,那么自然是不用排队的,即queueSize=0。而假设当前延时为最佳延时的一倍的时候,可以认为处理能力折半,100个流量进来会有一半即50个请求在排队,及queueSize= 100 * (1 − 1/2)=50。</p><h5 id="动态调整函数"><a href="#动态调整函数" class="headerlink" title="动态调整函数"></a>动态调整函数</h5><p>调整函数中最重要的即增函数与减函数。从初始化的代码中得知,增函数increaseFunc实现为limit+log limit,减函数decreaseFunc实现为limit-log limit,相对来说增减都是比较保守的。</p><p>看一下应用动态调整函数的相关代码:</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">private</span> <span class="hljs-type">int</span> <span class="hljs-title function_">updateEstimatedLimit</span><span class="hljs-params">(<span class="hljs-type">long</span> rtt, <span class="hljs-type">int</span> inflight, <span class="hljs-type">boolean</span> didDrop)</span> {<br> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">queueSize</span> <span class="hljs-operator">=</span> (<span class="hljs-type">int</span>) Math.ceil(estimatedLimit * (<span class="hljs-number">1</span> - (<span class="hljs-type">double</span>)rtt_noload / rtt));<br><br> <span class="hljs-type">double</span> newLimit;<br> <span class="hljs-comment">// Treat any drop (i.e timeout) as needing to reduce the limit</span><br> <span class="hljs-comment">// 发现错误直接应用减函数decreaseFunc</span><br> <span class="hljs-keyword">if</span> (didDrop) {<br> newLimit = decreaseFunc.apply(estimatedLimit);<br> <span class="hljs-comment">// Prevent upward drift if not close to the limit</span><br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (inflight * <span class="hljs-number">2</span> < estimatedLimit) {<br> <span class="hljs-keyword">return</span> (<span class="hljs-type">int</span>)estimatedLimit;<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-type">int</span> <span class="hljs-variable">alpha</span> <span class="hljs-operator">=</span> alphaFunc.apply((<span class="hljs-type">int</span>)estimatedLimit);<br> <span class="hljs-type">int</span> <span class="hljs-variable">beta</span> <span class="hljs-operator">=</span> betaFunc.apply((<span class="hljs-type">int</span>)estimatedLimit);<br> <span class="hljs-type">int</span> <span class="hljs-variable">threshold</span> <span class="hljs-operator">=</span> <span class="hljs-built_in">this</span>.thresholdFunc.apply((<span class="hljs-type">int</span>)estimatedLimit);<br><br> <span class="hljs-comment">// Aggressive increase when no queuing</span><br> <span class="hljs-keyword">if</span> (queueSize <= threshold) {<br> newLimit = estimatedLimit + beta;<br> <span class="hljs-comment">// Increase the limit if queue is still manageable</span><br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (queueSize < alpha) {<br> newLimit = increaseFunc.apply(estimatedLimit);<br> <span class="hljs-comment">// Detecting latency so decrease</span><br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (queueSize > beta) {<br> newLimit = decreaseFunc.apply(estimatedLimit);<br> <span class="hljs-comment">// We're within he sweet spot so nothing to do</span><br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">return</span> (<span class="hljs-type">int</span>)estimatedLimit;<br> }<br> }<br><br> newLimit = Math.max(<span class="hljs-number">1</span>, Math.min(maxLimit, newLimit));<br> newLimit = (<span class="hljs-number">1</span> - smoothing) * estimatedLimit + smoothing * newLimit;<br> <span class="hljs-keyword">if</span> ((<span class="hljs-type">int</span>)newLimit != (<span class="hljs-type">int</span>)estimatedLimit && LOG.isDebugEnabled()) {<br> LOG.debug(<span class="hljs-string">"New limit={} minRtt={} ms winRtt={} ms queueSize={}"</span>,<br> (<span class="hljs-type">int</span>)newLimit,<br> TimeUnit.NANOSECONDS.toMicros(rtt_noload) / <span class="hljs-number">1000.0</span>,<br> TimeUnit.NANOSECONDS.toMicros(rtt) / <span class="hljs-number">1000.0</span>,<br> queueSize);<br> }<br> estimatedLimit = newLimit;<br> <span class="hljs-keyword">return</span> (<span class="hljs-type">int</span>)estimatedLimit;<br>}<br></code></pre></td></tr></table></figure><p>动态调整函数规则如下:</p><ul><li>当变量queueSize < threshold时,选取较激进的增量函数,newLimit = limit+beta</li><li>当变量queueSize < alpha时,需要增大限流窗口,选择增函数increaseFunc,即newLimit = limit + log limit</li><li>当变量queueSize处于alpha,beta之间时候,limit不变</li><li>当变量queueSize大于beta时候,需要收拢限流窗口,选择减函数decreaseFunc,即newLimit = limit - log limit</li></ul><h5 id="平滑递减-smoothingDecrease"><a href="#平滑递减-smoothingDecrease" class="headerlink" title="平滑递减 smoothingDecrease"></a>平滑递减 smoothingDecrease</h5><p>注意到可以设置变量smoothing,这里初始值为1,表示平滑递减不起作用。如果有需要的话可以按需设置,比如设置smoothing为0.5时候,那么效果就是采用减函数decreaseFunc时候效果减半,实现方式为newLimitAfterSmoothing = 0.5 newLimit + 0.5 limit。</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>限流</tag>
</tags>
</entry>
<entry>
<title>J.U.C之AQS原理-CLH队列</title>
<link href="/aqs-clh/"/>
<url>/aqs-clh/</url>
<content type="html"><![CDATA[<h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>CLH 同步队列是一个 FIFO 双向队列,AQS 依赖它来完成同步状态的管理:</p><ul><li>当前线程如果获取同步状态失败时,AQS则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程。</li><li>当同步状态释放时,会将某个节点唤醒(是否首节点取决于公平锁/非公平锁),使其再次尝试获取同步状态。</li></ul><h3 id="Node"><a href="#Node" class="headerlink" title="Node"></a>Node</h3><blockquote><p>CLH队列由Node对象组成,Node是AQS中的内部类。</p></blockquote><p><img src="/images/5cbd6a30badb8.jpeg"></p><p>上图直观的向我们展示了节点的组织状态,我们可以看看node节点的源代码。</p><p>node节点作为CLH队列的一个节点,有着5条属性,分别是waitStatus、prev、next、thread、nextWater。</p><h5 id="waitStatus"><a href="#waitStatus" class="headerlink" title="waitStatus"></a>waitStatus</h5><blockquote><p>waitStatus是当前节点的一个等待状态标志位,该标志位决定了该节点在当前情况下处于何种状态。</p></blockquote><p>变量waitStatus则表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。不用再说了,直接看注释吧。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">/** waitStatus value to indicate thread has cancelled */</span> <br><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">CANCELLED</span> <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;<span class="hljs-comment">//表示等待锁的线程,被取消</span><br><span class="hljs-comment">/** waitStatus value to indicate successor's thread needs unparking */</span><br><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">SIGNAL</span> <span class="hljs-operator">=</span> -<span class="hljs-number">1</span>;<span class="hljs-comment">//表示后继线程需要被唤醒</span><br><span class="hljs-comment">/** waitStatus value to indicate thread is waiting on condition */</span> <br><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">CONDITION</span> <span class="hljs-operator">=</span> -<span class="hljs-number">2</span>;<span class="hljs-comment">//表示在等待条件 </span><br><span class="hljs-comment">/** </span><br><span class="hljs-comment"> * waitStatus value to indicate the next acquireShared should </span><br><span class="hljs-comment"> * unconditionally propagate </span><br><span class="hljs-comment"> */</span> <br><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">int</span> <span class="hljs-variable">PROPAGATE</span> <span class="hljs-operator">=</span> -<span class="hljs-number">3</span>;<span class="hljs-comment">//表示下一个获取共享锁的线程,无条件传递获取 </span><br><span class="hljs-comment">// 小于0时是有效状态,说明线程处于可以被唤醒的状态,大于0时取消状态,说明该线程中断或者等待超时,需要移除该线程。</span><br></code></pre></td></tr></table></figure><ul><li>CANCELLED(1):表示当前节点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的节点将不会再变化</li><li>SIGNAL(-1):表示后继节点在等待当前节点唤醒。后继节点入队后进入休眠状态之前,会将前驱节点的状态更新为SIGNAL</li><li>CONDITION(-2):表示节点等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的节点将从等待队列转移到同步队列中,等待获取同步锁</li><li>PROPAGATE(-3):共享模式下,前驱节点不仅会唤醒其后继节点,同时也可能会唤醒后继的后继节点</li></ul><h5 id="nextWaiter"><a href="#nextWaiter" class="headerlink" title="nextWaiter"></a>nextWaiter</h5><p>AQS中阻塞队列采用的是用双向链表保存,用prve和next相互链接。而AQS中条件队列是使用单向列表保存的,用nextWaiter来连接。阻塞队列和条件队列并不是使用的相同的数据结构。<br>在Node节点的源码中有两个常量属性</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">// 共享模式</span><br><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Node</span> <span class="hljs-variable">SHARED</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Node</span>();<br><span class="hljs-comment">// 独占模式</span><br><span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">Node</span> <span class="hljs-variable">EXCLUSIVE</span> <span class="hljs-operator">=</span> <span class="hljs-literal">null</span>;<br><span class="hljs-comment">// 其他模式</span><br><span class="hljs-comment">// 其他非空值:条件等待节点(调用Condition的await方法的时候</span><br></code></pre></td></tr></table></figure><p>nextWaiter实际上标记的就是在该节点唤醒后依据该节点的状态判断是否依据条件唤醒下一个节点。</p><table><thead><tr><th>nextWaiter状态标志</th><th>说明</th></tr></thead><tbody><tr><td>SHARED(共享模式)</td><td>直接唤醒下一个节点</td></tr><tr><td>EXCLUSIVE(独占模式)</td><td>等待当前线程执行完成后再唤醒</td></tr></tbody></table><h3 id="入队"><a href="#入队" class="headerlink" title="入队"></a>入队</h3><p>上面了解了同步队列的结构, 在分析其入列操作在简单不过。无非就是将tail(使用CAS保证原子操作)指向新节点,新节点的prev指向队列中最后一节点(旧的tail节点),原队列中最后一节点的next节点指向新节点以此来建立联系。</p><p><img src="/images/5cbd6a7a93674.jpeg"></p><p>具体实现查看<code>addWaiter()</code>源码。</p><h3 id="出队"><a href="#出队" class="headerlink" title="出队"></a>出队</h3><p>同步队列(CLH)遵循FIFO,首节点是获取同步状态的节点,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单。如下图</p><p><img src="/images/5cbd6b431be55.jpeg"></p><p>设置首节点是通过获取同步状态成功的线程来完成的(获取同步状态是通过CAS来完成),只能有一个线程能够获取到同步状态,因此设置头节点的操作并不需要CAS来保证,只需要将首节点设置为其原首节点的后继节点并断开原首节点的next(等待GC回收)应用即可。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>CLH阻塞队列采用的是双向链表队列,头部节点默认获取资源获得执行权限。后续节点不断自旋方式查询前置节点是否执行完成,直到头部节点执行完成将自己的waitStatus状态修改以通知后续节点可以获取资源执行。CLH锁是一个有序的无饥饿的公平锁。</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>AQS</tag>
</tags>
</entry>
<entry>
<title>如何判断SSL证书的私钥和证书匹配</title>
<link href="/same-key/"/>
<url>/same-key/</url>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>防止有人篡改证书或者私钥,导致网关无法解析,所以需要验证私钥和证书是否匹配。开始以为Java会有这种方法直接校验,网上找了一圈都没找到。翻了 <code>X509Certificate</code> 的源码,发现可以获取到公钥,那么也可以通过私钥获取到公钥,所以判断两个公钥是否相等就可以了。</p><h3 id="通过证书获取公钥"><a href="#通过证书获取公钥" class="headerlink" title="通过证书获取公钥"></a>通过证书获取公钥</h3><blockquote><p>网上资料很多,一搜就有了</p></blockquote><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">getPublicValByCert</span><span class="hljs-params">(String cert)</span> {<br><br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-type">CertificateFactory</span> <span class="hljs-variable">certificateFactory</span> <span class="hljs-operator">=</span> CertificateFactory<br> .getInstance(<span class="hljs-string">"X.509"</span>);<br> <span class="hljs-type">X509Certificate</span> <span class="hljs-variable">x509certificate</span> <span class="hljs-operator">=</span> (X509Certificate) certificateFactory<br> .generateCertificate(<span class="hljs-keyword">new</span> <span class="hljs-title class_">ByteArrayInputStream</span>(cert.getBytes()));<br> <span class="hljs-keyword">return</span> getValue(x509certificate.getPublicKey());<br> } <span class="hljs-keyword">catch</span> (Exception e) {<br> log.error(<span class="hljs-string">"解析公钥异常"</span>, e);<br> }<br><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;<br>}<br><br><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">getValue</span><span class="hljs-params">(PublicKey publicKey)</span> {<br> <span class="hljs-type">BASE64Encoder</span> <span class="hljs-variable">bse</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">BASE64Encoder</span>();<br> <span class="hljs-keyword">return</span> bse.encode(publicKey.getEncoded());<br>}<br>``` <br><br><br>### 通过私钥获取公钥<br><br>按照网上的教程,大部分都是将PKCS#<span class="hljs-number">1</span>转换成PKCS#<span class="hljs-number">8</span>,然后通过 `PKCS8EncodedKeySpec` 获取到公钥。但是根本不管用。<br>> 参考的网址:[https:<span class="hljs-comment">//stackoverflow.com/questions/8434428/get-public-key-from-private-in-java](https://stackoverflow.com/questions/8434428/get-public-key-from-private-in-java) </span><br><br>按照上面的方式总是报错,大概就是第一行不是合法的格式。如果不去掉第一行和最后一行,也还是不能解析。<br><br>尝试了几百次了吧,后来看见一篇获取 `PEM` 的文章,才柳暗花明。 <br>贴上代码,如果以后遇到同样的坑,又不用乱找了。<br><br>```java<br><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> String <span class="hljs-title function_">getPublicValByKey</span><span class="hljs-params">(String key)</span> {<br> Security.addProvider(<span class="hljs-keyword">new</span> <span class="hljs-title class_">BouncyCastleProvider</span>());<br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-type">StringReader</span> <span class="hljs-variable">stringReader</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">StringReader</span>(key);<br> <span class="hljs-type">PEMParser</span> <span class="hljs-variable">pemParser</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">PEMParser</span>(stringReader);<br> <span class="hljs-type">JcaPEMKeyConverter</span> <span class="hljs-variable">converter</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">JcaPEMKeyConverter</span>().setProvider(<span class="hljs-string">"BC"</span>);<br> <span class="hljs-type">Object</span> <span class="hljs-variable">object</span> <span class="hljs-operator">=</span> pemParser.readObject();<br> <span class="hljs-type">KeyPair</span> <span class="hljs-variable">kp</span> <span class="hljs-operator">=</span> converter.getKeyPair((PEMKeyPair) object);<br> <span class="hljs-keyword">return</span> getValue(kp.getPublic());<br> } <span class="hljs-keyword">catch</span> (Exception e) {<br> log.error(<span class="hljs-string">"解析私钥异常"</span>, e);<br> }<br><br> <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;<br>}<br></code></pre></td></tr></table></figure><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><h5 id="PKCS1"><a href="#PKCS1" class="headerlink" title="PKCS1"></a>PKCS1</h5><p>PKCS#1结构仅为RSA设计</p><figure class="highlight vbnet"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs vbnet">-----BEGIN RSA <span class="hljs-keyword">PUBLIC</span> <span class="hljs-keyword">KEY</span>-----<br>BASE64 ENCODED DATA<br>-----<span class="hljs-keyword">END</span> RSA <span class="hljs-keyword">PUBLIC</span> <span class="hljs-keyword">KEY</span>-----<br></code></pre></td></tr></table></figure><h5 id="PKCS8"><a href="#PKCS8" class="headerlink" title="PKCS8"></a>PKCS8</h5><p>X509,SSL支持的算法不仅仅是RSA,因此产生了更具有通用性的PKCS#8</p><figure class="highlight vbnet"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs vbnet">-----BEGIN <span class="hljs-keyword">PUBLIC</span> <span class="hljs-keyword">KEY</span>-----<br>BASE64 ENCODED DATA<br>-----<span class="hljs-keyword">END</span> <span class="hljs-keyword">PUBLIC</span> <span class="hljs-keyword">KEY</span>-----<br></code></pre></td></tr></table></figure><p>与PKCS#1相比将文件包含的加密算法和Key分开存储,因此可以存储其他加密算法的Key</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>SSL</tag>
</tags>
</entry>
<entry>
<title>记一次 Binlog 的应用</title>
<link href="/mysql-binlog/"/>
<url>/mysql-binlog/</url>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><p>我们现在遇到一个问题,每次在发券系统里面增加一个表操作的时候,都会实现缓存,实现了缓存就要实现通知刷新,所以程序里只要有更新的操作都要硬编码发送一次消息。这个过程维护成本太大,就想到是否有什么方法可以实现自动通知刷新。</p><p>所以我就在这里小小去了解一下 Binlog。</p><h3 id="什么是Binlog"><a href="#什么是Binlog" class="headerlink" title="什么是Binlog"></a>什么是Binlog</h3><p><code>MySQL</code> 有四种类型的日志——Error Log、General Query Log、Binary Log 和 Slow Query Log。</p><p>第一个是错误日志,记录 <code>MySQL</code> 的一些错误。第二个是一般查询日志,记录 <code>MySQL</code> 正在做的事情,比如客户端的连接和断开、来自客户端每条 <code>Sql Statement</code> 记录信息;如果你想准确知道客户端到底传了什么瞎 [哔哔] 玩意儿给服务端,这个日志就非常管用了,不过它非常影响性能。第四个是慢查询日志,记录一些查询比较慢的 SQL 语句——这种日志非常常用,主要是给开发者调优用的。</p><p>剩下的第三种就是<code>Binlog</code>了,包含了一些事件,这些事件描述了数据库的改动,如建表、数据改动等,也包括一些潜在改动,比如</p><figure class="highlight sql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs sql"><span class="hljs-keyword">DELETE</span> <span class="hljs-keyword">FROM</span> xxx <span class="hljs-keyword">WHERE</span> bing <span class="hljs-operator">=</span> xxx<br></code></pre></td></tr></table></figure><p>然而一条数据都没被删掉的这种情况。除非使用 Row-based logging,否则会包含所有改动数据的<code>SQL Statement</code>。</p><p>显然,我们执行 <code>SELECT</code> 等不设计数据变更的语句是不会记录 <code>Binlog</code> 的,而涉及到数据更新则会记录。要注意的是,对支持事务的引擎如InnoDB而言,必须要提交了事务才会记录 <code>Binlog</code>。<code>Binlog</code> 是在事务最终commit前写入的,<code>Binlog</code> 什么时候刷新到磁盘跟参数sync_binlog相关。如果设置为0,则表示<code>MySQL</code>不控制 <code>Binlog</code> 的刷新,由文件系统去控制它缓存的刷新,而如果设置为不为0的值则表示每sync_binlog次事务,MySQL调用文件系统的刷新操作刷新 <code>Binlog</code> 到磁盘中。设为1是最安全的,在系统故障时最多丢失一个事务的更新,但是会对性能有所影响,一般情况下会设置为100或者0,牺牲一定的一致性来获取更好的性能.</p><h3 id="流程设计"><a href="#流程设计" class="headerlink" title="流程设计"></a>流程设计</h3><p><img src="/images/binlog-1.png"></p><p>mysql-binlog-connector-server实现监听binlog,收到之后放入队列中,另起一个线程消耗队列。<br>利用钩子在宕机之前保存消费到的position。</p><blockquote><p>保存position的信息本来想是放在mysql,但是后期会增加HA,需要使用Zookeeper保持心跳,为了方便还是选择保存在Zookeeper</p></blockquote><p><img src="/images/binlog-2.png"></p><ul><li><p>EventListener: 在向mysql发送dump命令之前会先从Zookeeper中获取上次解析成功的 <code>Position</code> (如果是第一次启动,则获取初始指定位置或者当前数据段binlog位点)。mysql接受到dump命令后,由EventListener从mysql上pull binlog数据进行解析并传递给 <code>parse</code> (传递给parse 模块进行数据存储,是一个阻塞操作,直到存储成功),传送成功之后更新 <code>Position</code>。</p></li><li><p>MessagContaner: 保存解析成功的数据,交给 <code>DataSchdule</code> 处理。设计这个模块是为了缓冲,防止重复发送消息。</p></li></ul><p>下面是模拟mysql的slave时收到的消息</p><blockquote><p>{before=[797, 3528, zjy-客户1-广告1, 100004, 2017-03-24, 2017-04-09, 1488857138210, 1488857138210, 0, 0, 2, 4, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, Mon Mar 06 23:00:36 CST 2017, Thu Oct 12 03:23:18 CST 2017, 0, 0, 0, 26526708857909, null, 1, 0, 1.000], after=[797, 3528, zjy-客户1-广告1, 100004, 2017-10-11, 2017-04-09, 1488857138210, 1488857138210, 0, 0, 2, 4, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, Mon Mar 06 23:00:36 CST 2017, Thu Oct 12 22:01:15 CST 2017, 0, 0, 0, 26526708857909, null, 1, 0, 1.000]},<br> {before=[885, 3528, zjy-客户1-广告1, 333001, 2017-03-08, 2017-04-27, 1489993531824, 1489993531824, 0, 0, 2, 4, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, Mon Mar 20 23:05:31 CST 2017, Thu Oct 12 01:45:16 CST 2017, 0, 0, 0, 27795375556109, null, 0, 0, 1.000], after=[885, 3528, zjy-客户1-广告1, 333001, 2017-10-11, 2017-04-27, 1489993531824, 1489993531824, 0, 0, 2, 4, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, Mon Mar 20 23:05:31 CST 2017, Thu Oct 12 22:01:15 CST 2017, 0, 0, 0, 27795375556109, null, 0, 0, 1.000]},<br> {before=[4882, 3531, zjy-客户2-广告1, 12200, 2017-04-14, 2017-04-14, 1492148740122, 1492148740122, 1, 0, 0, 3, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, Fri Apr 14 21:44:11 CST 2017, Sat Sep 02 00:17:55 CST 2017, 0, 0, 0, 30055174218659, null, 0, 0, 1.000], after=[4882, 3531, zjy-客户2-广告1, 12200, 2017-10-11, 2017-04-14, 1492148740122, 1492148740122, 1, 0, 0, 3, null, null, null, null, null, null, null, null, 1, null, null, null, null, null, null, Fri Apr 14 21:44:11 CST 2017, Thu Oct 12 22:01:15 CST 2017, 0, 0, 0, 30055174218659, null, 0, 0, 1.000]}<br>]}}Event{header=EventHeaderV4{timestamp=1507788075000, eventType=XID, serverId=1, headerLength=19, dataLength=12, nextPosition=81793595, flags=0}, data=XidEventData{xid=3329634}}</p></blockquote><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>这里相当于一个迷你的 <code>Canal</code> ,还有很多场景没有实现,比如一致性,HA。如果有机会在参考 <code>Canal</code> 的设计。</p><p>参考资料:</p><p><a href="https://blog.csdn.net/u012758088/article/details/78788523">canal系列—Canal 的介绍</a></p>]]></content>
<categories>
<category>Mysql</category>
</categories>
<tags>
<tag>Binlog</tag>
</tags>
</entry>
<entry>
<title>React 入门 (基础概念)</title>
<link href="/react-base/"/>
<url>/react-base/</url>
<content type="html"><![CDATA[<h3 id="前言"><a href="#前言" class="headerlink" title="前言"></a>前言</h3><blockquote><p>准备写这篇文章时,才发现一年都没有总结了。之前还想一个月总结一次,对比现在简直太夸张了。其实今年的收获还挺多的,比如将学习的python、react应用到工作中,个人爬虫网站上线。</p></blockquote><p>开始的时候我是很抵触的,因为觉得后端写前端最后肯定会不三不四,之前也写过前后端全包的项目,但是对后面的工作都没什么帮助,所以觉得对个人发展也不是很好,而且全栈工程师在杭州需求也不多。没办法,公司就是不给配前端,只能一个人整,想想不能影响项目进度啊,最后倒霉还是自己,所以硬着头皮跳下去了。</p><p>项目大概做了4个月,对前端其实还是皮毛,但是完成项目需求还是搓搓有余。能撑到现在是因为有个念头:往全栈工程师发展也不一定是件坏事,说不定就有奇迹了呢。</p><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>React.js 是一个帮助你构建页面 UI 的库。如果你熟悉 MVC 概念的话,那么 React 的组件就相当于 MVC 里面的 View。说白点就是帮助我们将界面分成各个独立的小块,每一个块就是组件,这些组件之间可以组合、嵌套,就成了我们的页面。</p><p>一个组件的显示形态和行为有可能是由某些数据决定的。而数据是可能发生改变的,这时候组件的显示形态就会发生相应的改变。而 React.js 也提供了一种非常高效的方式帮助我们做到了数据和组件显示形态之间的同步。</p><p>React.js 不是一个框架,它只是一个库。它只提供 UI (view)层面的解决方案。在实际的项目当中,它并不能解决我们所有的问题,需要结合其它的库,例如 Redux、React-router 等来协助提供完整的解决方法。</p><h3 id="JSX"><a href="#JSX" class="headerlink" title="JSX"></a>JSX</h3><p>React的核心机制就是实现了一个虚拟DOM,利用虚拟DOM来减少对实际DOM的操作从而提升性能,组件DOM结构就是映射到这个虚拟的DOM上,React在这个虚拟DOM上实现了一个diff算法,当要更新组件的时候,会通过diff寻找要变更的DOM节点,再把这个修改更新到浏览器实际的DOM节点上,所以实际上不是真的渲染整个DOM树,这个虚拟的DOM是一个纯粹的JS数据结构,所以性能比原生DOM会提高很多; </p><p>虚拟DOM(virtual-dom)实际上是对实际DOM的一个抽象,是一个js对象。react所有的表层操作实际上是在操作虚拟DOM。经过diff算法计算出虚拟DOM的差异,然后将这些差异进行实际的DOM操作更新页面</p><p>从 JSX 到页面经历的过程:</p><p><img src="/images/jsx-banyi.png"></p><blockquote><ul><li>JSX 是 JavaScript 语言的一种语法扩展,长得像 HTML,但并不是 HTML。</li></ul></blockquote><ul><li>React.js 可以用 JSX 来描述你的组件长什么样的。</li><li>JSX 在编译的时候会变成相应的 JavaScript 对象描述。</li><li>react-dom 负责把这个用来描述 UI 信息的 JavaScript 对象变成 DOM 元素,并且渲染到页面上。</li></ul><h3 id="组件化"><a href="#组件化" class="headerlink" title="组件化"></a>组件化</h3><p>虚拟DOM(virtual-dom)不仅带来了简单的UI开发逻辑,同时也带来了组件化开发的思想,所谓组件,即封装起来的具有独立功能的UI部件。React推荐以组件的方式去重新思考UI构成,将UI上每一个功能相对独立的模块定义成组件,然后将小的组件通过组合或者嵌套的方式构成大的组件,最终完成整体UI的构建。</p><p>可以按照下面的规则进行划分组件:</p><ul><li><p>容器组件:容器型组件是一个页面容器,用来放置当前页面的所有展示型组件和业务组件组合成一个页面,通过数据的驱动进行控制展示组件和业务组件。</p></li><li><p>展示组件:展示型组件是具体到某一个小的组件模块,比如一个按钮,一个卡片,一个进度条等,我们在用react做组件化开发的时候,先定义好一个个小的展示型组件,然后把这些组件都导入容器型组件,最终组合成一个完整的页面。</p></li><li><p>业务组件:页面中某个业务模块的拆分,涉及到数据交互,有自己独立的业务逻辑</p></li></ul><p>React 认为一个组件应该具有如下特征:</p><blockquote><ul><li>可组合(Composeable):一个组件易于和其它组件一起使用,或者嵌套在另一个组件内部。如果一个组件内部创建了另一个组件,那么说父组件拥有(own)它创建的子组件,通过这个特性,一个复杂的UI可以拆分成多个简单的UI组件。</li></ul></blockquote><ul><li>可重用(Reusable):每个组件都是具有独立功能的,它可以被使用在多个UI场景。</li><li>可维护(Maintainable):每个小的组件仅仅包含自身的逻辑,更容易被理解和维护。</li></ul><h3 id="组件种类"><a href="#组件种类" class="headerlink" title="组件种类"></a>组件种类</h3><p>**function-based(函数组件)**:函数组件也称为无状态组件,使用纯函数创建,创建后不会产生组件实例,也就是说无法使用ref获取,主要用于展示组件的开发,性能高,没有生命周期,没有state,但是可以接收props,相当于一个只有render生命周期的组件</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">function</span> <span class="hljs-title function_">Component</span> (props) {<br> <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>{props.children}<span class="hljs-tag"></<span class="hljs-name">div</span>></span></span><br>}<br><span class="hljs-comment">//使用</span><br><<span class="hljs-title class_">Component</span>>组件</<span class="hljs-title class_">Component</span>><br></code></pre></td></tr></table></figure><p>**class-based(类组件)**:使用es6 class的方式创建,通过继承React.component实现,可以有自己独立的生命周期,state状态,必须有render生命周期,state定义在constructor构造函数中,所用的容器组件都是通过class创建</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><code class="hljs javascript"><span class="hljs-keyword">class</span> <span class="hljs-title class_">Component</span> <span class="hljs-keyword">extends</span> <span class="hljs-title class_ inherited__">React.component</span> {<br> <span class="hljs-title function_">constructor</span> (props) {<br> <span class="hljs-variable language_">super</span>(props)<br> <span class="hljs-variable language_">this</span>.<span class="hljs-property">state</span> = {<br> <span class="hljs-attr">age</span>: <span class="hljs-number">100</span><br> }<br> }<br> render () {<br> <span class="hljs-keyword">const</span> {age} = <span class="hljs-variable language_">this</span>.<span class="hljs-property">state</span><br> <span class="hljs-keyword">return</span> <span class="language-xml"><span class="hljs-tag"><<span class="hljs-name">div</span>></span>{age}<span class="hljs-tag"></<span class="hljs-name">div</span>></span></span><br> }<br>}<br><span class="hljs-comment">//使用</span><br><<span class="hljs-title class_">Component</span> /><br></code></pre></td></tr></table></figure><p><strong>createClass组件</strong>:不常用</p><blockquote><p>在react中最小的单位是元素,元素分为dom元素,组件元素,区分方法是dom元素小写,组件元素首字母大写</p></blockquote><h3 id="组件状态"><a href="#组件状态" class="headerlink" title="组件状态"></a>组件状态</h3><p>每个组件都有一个状态,下面几点仅适用于class-based组件。<br>一个组件的显示形态是可以由它数据状态和配置参数决定的。在react中组件的状态使用state定义,使用setState修改,使用this.state读取。</p><p><em><strong>setState</strong></em></p><p><code>setState</code> 方法由父类 Component 所提供。当我们调用这个函数的时候,React.js 会更新组件的状态 state ,并且重新调用 render 方法,然后再把 render 方法所渲染的最新的内容显示到页面上。</p><p><code>setState</code>是“异步”的,调用<code>setState</code>只会提交一次state修改到队列中,不会直接修改<code>this.state</code>。等到满足一定条件时,react会合并队列中的所有修改,触发一次update流程,更新<code>this.state</code>。因此setState机制减少了update流程的触发次数,从而提高了性能。</p><p><code>setState</code> 的更新是一个浅合并(Shallow Merge)的过程</p><p><em>特殊情况:</em></p><blockquote><p>在实际开发中,setState的表现有时会不同于理想情况。主要是以下两种: </p></blockquote><ol><li>在mount流程中调用setState。 </li><li>在setTimeout/Promise回调中调用setState。<br>在第一种情况下,不会进入update流程,队列在mount时合并修改并render。<br>在第二种情况下,setState将不会进行队列的批更新,而是直接触发一次update流程。这是由于setState的两种更新机制导致的,只有在批量更新模式中,才会是“异步”的。</li></ol><p><em><strong>State 与 Props 区别</strong></em></p><p><code>state</code> 的主要作用是用于组件保存、控制、修改自己的可变状态。state 在组件内部初始化,可以被组件自身修改,而外部不能访问也不能修改。你可以认为 state 是一个局部的、只能被组件自身控制的数据源。state 中状态可以通过 this.setState 方法进行更新,setState 会导致组件的重新渲染。</p><p><code>props</code> 的主要作用是让使用该组件的父组件可以传入参数来配置该组件。它是外部传进来的配置参数,组件内部无法控制也无法修改。除非外部组件主动传入新的 props,否则组件的 props 永远保持不变。</p><blockquote><p>state 是让组件控制自己的状态,props 是让外部对组件自己进行配置</p></blockquote><h3 id="组件的生命周期"><a href="#组件的生命周期" class="headerlink" title="组件的生命周期"></a>组件的生命周期</h3><p>对于 React 组件来说,生命周期主要包含三个阶段:创建(挂载)过程、销毁(卸载)过程和存在期。</p><p>React.js 将组件渲染,并且构造 DOM 元素然后塞入页面的过程称为组件的挂载。过程如下:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs javascript">-> <span class="hljs-title function_">constructor</span>(<span class="hljs-params"></span>)<br>-> <span class="hljs-title function_">componentWillMount</span>()<br>-> <span class="hljs-title function_">render</span>()<br><span class="hljs-comment">// 然后构造 DOM 元素插入页面</span><br>-> <span class="hljs-title function_">componentDidMount</span>()<br></code></pre></td></tr></table></figure><p><code>componentWillMount</code> 和 <code>componentDidMount</code> 都是可以像 render 方法一样自定义在组件的内部。挂载的时候,React.js 会在组件的 render 之前调用 <code>componentWillMount</code>,在 DOM 元素塞入页面以后调用 <code>componentDidMount</code>。</p><p>一个组件可以插入页面,当然也可以从页面中删除</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs javascript">-> <span class="hljs-title function_">constructor</span>(<span class="hljs-params"></span>)<br>-> <span class="hljs-title function_">componentWillMount</span>()<br>-> <span class="hljs-title function_">render</span>()<br><span class="hljs-comment">// 然后构造 DOM 元素插入页面</span><br>-> <span class="hljs-title function_">componentDidMount</span>()<br><span class="hljs-comment">// ...</span><br><span class="hljs-comment">// 即将从页面中删除</span><br>-> <span class="hljs-title function_">componentWillUnmount</span>()<br><span class="hljs-comment">// 从页面中删除</span><br></code></pre></td></tr></table></figure><p>setState更新机制大致流程:</p><p><img src="/images/state-update.png"></p><blockquote><ul><li>当一个组件的state或者其父组件传递的props发生改变的时候组件就会重新渲染.</li></ul></blockquote><ul><li>如果是后者即props发生改变时 React 会调用另外一个周期函数<code>componentWillReceiveProps</code></li><li>如果两者都发生改变,React会做一个重要决策,该组件是否需要在浏览器里被更新?这也是为什么会调用另外一个周期函数<code>shouldComponentUpdate</code>的原因. 这个方法实际上也是在问一个问题,所以如果你想自定义或者优化你的渲染过程,你就需要通过返回一个true或者false来回答这个问题。</li><li>如果没有手动指定<code>shouldComponentUpdate</code>, React 会默认作出聪明的决策,多数情况下也是足够良好的.</li><li>首先, 这时候React会调用<code>componentWillUpdate</code>方法. 然后计算新的渲染产出把它和上一次的渲染产出进行比较.</li><li>如果没什么改变,那么就什么也不做.</li><li>如果有改变则把差异反应到浏览器上.</li><li>无论什么情况,尽管更新会发生在任何地方(甚至计算出来的产出是相同的),React 最终都会调用另一个周期方法<code>componentDidUpdate</code>.</li></ul><p>可以将<code>React</code>视为我们聘请的与浏览器通信的代理。我们不是去手动的用DOM API来操作DOM,而是更组件状态的属性值,然后让<code>React</code>代表我们去和浏览器沟通. 我相信着就是react为什么这么受欢迎的原因. 我们讨厌和浏览器先生(还说着各种带有口音的DOM方言)打交道,React志愿为我们做这些事情,还是免费的~</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><ul><li>React速度很快</li></ul><p>与其它框架相比,React采取了一种特立独行的操作DOM的方式。<br>它并不直接对DOM进行操作。<br>它引入了一个叫做虚拟DOM的概念,安插在JavaScript逻辑和实际的DOM之间。<br>这一概念提高了Web性能。在UI渲染过程中,React通过在虚拟DOM中的微操作来实对现实际DOM的局部更新。</p><ul><li>跨浏览器兼容</li></ul><p>虚拟DOM帮助我们解决了跨浏览器问题,它为我们提供了标准化的API,甚至在IE8中都是没问题的。</p><ul><li>模块化</li></ul><p>为你程序编写独立的模块化UI组件,这样当某个或某些组件出现问题是,可以方便地进行隔离。<br>每个组件都可以进行独立的开发和测试,并且它们可以引入其它组件。这等同于提高了代码的可维护性。</p><ul><li>React与其它框架/库兼容性好</li></ul><p>比如使用RequireJS来加载和打包,而Browserify和Webpack适用于构建大型应用。它们使得那些艰难的任务不再让人望而生畏。</p><p><strong>缺点:</strong></p><p>React本身只是一个V而已,所以如果是大型项目想要一套完整的框架的话,还需要引入Flux和routing相关的东西</p>]]></content>
<categories>
<category>前端</category>
</categories>
<tags>
<tag>React</tag>
</tags>
</entry>
<entry>
<title>elastic-job 在广告系统中的实践总结</title>
<link href="/elastic-job/"/>
<url>/elastic-job/</url>
<content type="html"><![CDATA[<h3 id="为什么要使用elastic-job"><a href="#为什么要使用elastic-job" class="headerlink" title="为什么要使用elastic-job"></a>为什么要使用elastic-job</h3><h5 id="为什么要使用定时任务"><a href="#为什么要使用定时任务" class="headerlink" title="为什么要使用定时任务"></a>为什么要使用定时任务</h5><p>一般来说,系统可使用消息传递代替部分使用作业的场景。两者确有相似之处。可互相替换的场景,如队列表。将待处理的数据放入队列表,然后使用频率极短的定时任务拉取队列表的数据并处理。这种情况使用消息中间件的推送模式可更好的处理实时性数据。而且基于数据库的消息存储吞吐量远远小于基于文件的顺序追加消息存储,但在某些场景下则不能互换:</p><ul><li><p>时间驱动 OR 事件驱动:内部系统一般可以通过事件来驱动,但涉及到外部系统,则只能使用时间驱动。如:抓取外部系统价格。每小时抓取,由于是外部系统,不能像内部系统一样发送事件触发事件。</p></li><li><p>批量处理 OR 逐条处理:批量处理堆积的数据更加高效,在不需要实时性的情况下比消息中间件更有优势。而且有的业务逻辑只能批量处理,如:电商公司与快递公司结算,一个月结算一次,并且根据送货的数量有提成。比如,当月送货超过1000则额外给快递公司多1%的快递费。</p></li><li><p>非实时性 OR 实时性:虽然消息中间件可以做到实时处理数据,但有的情况并不需要。如:VIP用户降级,如果超过1年无购买行为,则自动降级。这类需求没有强烈的时间要求,不需要按照时间精确的降级VIP用户。</p></li><li><p>系统内部 OR 系统解耦:作业一般封装在系统内部,而消息中间件可用于系统间解耦。</p></li></ul><h5 id="为什么要选择elastic-job"><a href="#为什么要选择elastic-job" class="headerlink" title="为什么要选择elastic-job"></a>为什么要选择elastic-job</h5><p>先比较下常见作业系统的差异:</p><ul><li><p>Quartz:Java事实上的定时任务标准。但Quartz关注点在于定时任务而非数据,并无一套根据数据处理而定制化的流程。虽然Quartz可以基于数据库实现作业的高可用,但缺少分布式并行执行作业的功能。</p></li><li><p>TBSchedule:阿里早期开源的分布式任务调度系统。代码略陈旧,使用timer而非线程池执行任务调度。众所周知,timer在处理异常状况时是有缺陷的。而且TBSchedule作业类型较为单一,只能是获取/处理数据一种模式。还有就是文档缺失比较严重。</p></li><li><p>Crontab:Linux系统级的定时任务执行器。缺乏分布式和集中管理功能。</p></li></ul><blockquote><p>之前广告系统使用的是Crontab,虽然也可以很好的支撑我们的需求,但是后来跑任务的这台机器有问题需要下线,然而运维迁移的时候任务没有迁移,导致所有的定时任务在那一晚全部失效。这是为什么我们需要迁移到elastic-job的主要原因</p></blockquote><p>综上所述,当前存在的作业系统缺少分布式、并行调度、弹性扩容缩容、集中管理、定制化流程型任务等功能,所以需要一个新的作业系统完善这些功能。</p><h3 id="elastic-job介绍"><a href="#elastic-job介绍" class="headerlink" title="elastic-job介绍"></a>elastic-job介绍</h3><p>elastic-job主要的设计理念是无中心化的分布式定时调度框架,思路来源于Quartz的基于数据库的高可用方案。但数据库没有分布式协调功能,所以在高可用方案的基础上增加了弹性扩容和数据分片的思路,以便于更大限度的利用分布式服务器的资源。</p><p><img src="/images/struct.png"></p><h5 id="主要功能"><a href="#主要功能" class="headerlink" title="主要功能"></a>主要功能</h5><ul><li>分布式<ul><li>重写Quartz基于数据库的分布式功能,改用Zookeeper实现注册中心</li></ul></li><li>并行调度<ul><li>采用任务分片方式实现。将一个任务拆分为n个独立的任务项,由分布式的服务器并行执行各自分配到的分片项</li></ul></li><li>弹性扩容缩容<ul><li>将任务拆分为n个任务项后,各个服务器分别执行各自分配到的任务项。一旦有新的服务器加入集群,或现有服务器下线,elastic-job将在保留本次任务执行不变的情况下,下次任务开始前触发任务重分片</li></ul></li><li>集中管理<ul><li>采用基于Zookeeper的注册中心,集中管理和协调分布式作业的状态,分配和监听。外部系统可直接根据Zookeeper的数据管理和- 监控elastic-job</li></ul></li><li>定制化流程型任务<ul><li>作业可分为简单和数据流处理两种模式,数据流又分为高吞吐处理模式和顺序性处理模式,其中高吞吐处理模式可以开启足够多的线程快速的处理数据,而顺序性处理模式将每个分片项分配到一个独立线程,用于保证同一分片的顺序性</li></ul></li><li>失效转移<ul><li>弹性扩容缩容在下次作业运行前重分片,但本次作业执行的过程中,下线的服务器所分配的作业将不会重新被分配。失效转移功能可以在本次作业运行中用空闲服务器抓取孤儿作业分片执行。同样失效转移功能也会牺牲部分性能</li></ul></li><li>运行时状态收集<ul><li>监控作业运行时状态,统计最近一段时间处理的数据成功和失败数量,记录作业上次运行开始时间,结束时间和下次运行时间</li></ul></li><li>作业停止,恢复和禁用<ul><li>用于操作作业启停,并可以禁止某作业运行(上线时常用)</li></ul></li><li>Spring命名空间支持<ul><li>elastic-job可以不依赖于spring直接运行,但是也提供了自定义的命名空间方便与spring集成</li></ul></li><li>运维平台<ul><li>提供web控制台用于管理作业和注册中心</li></ul></li><li>稳定性<ul><li>在服务器无波动的情况下,并不会重新分片;即使服务器有波动,下次分片的结果也会根据服务器IP和作业名称哈希值算出稳定的分片顺序,尽量不做大的变动</li></ul></li><li>高性能<ul><li>同一服务器的批量数据处理采用自动切割并多线程并行处理</li></ul></li><li>灵活性<ul><li>所有在功能和性能之间的权衡,都可通过配置开启/关闭。如:elastic-job会将作业运行状态的必要信息更新到注册中心。如果作业执行频度很高,会造成大量Zookeeper写操作,而分布式Zookeeper同步数据可能引起网络风暴。因此为了考虑性能问题,可以牺牲一些功能,而换取性能的提升</li></ul></li><li>幂等性<ul><li>elastic-job可牺牲部分性能用以保证同一分片项不会同时在两个服务器上运行</li></ul></li><li>容错性<ul><li>作业服务器与Zookeeper服务器通信失败则立即停止作业运行,防止作业注册中心将失效的分片分项配给其他作业服务器,而当前作业服务器仍在执行任务,导致重复执行</li></ul></li></ul><h5 id="流程图"><a href="#流程图" class="headerlink" title="流程图"></a>流程图</h5><p>作业启动流程图</p><p><img src="/images/start.jpg"></p><ul><li>(1)第一台服务器上线触发主服务器选举。主服务器一旦下线,则重新触发选举,选举过程中阻塞,只有主服务器选举完成,才会执行其他任务。</li><li>(2)某作业服务器上线时会自动将服务器信息注册到注册中心,下线时会自动更新服务器状态。</li><li>(3)主节点选举,服务器上下线,分片总数变更均更新重新分片标记。</li><li>(4)定时任务触发时,如需重新分片,则通过主服务器分片,分片过程中阻塞,分片结束后才可执行任务。如分片过程中主服务器下线,则先选举主服务器,再分片。</li><li>(5)通过(4)可知,为了维持作业运行时的稳定性,运行过程中只会标记分片状态,不会重新分片。分片仅可能发生在下次任务触发前。</li><li>(6)每次分片都会按服务器IP排序,保证分片结果不会产生较大波动。</li><li>(7)实现失效转移功能,在某台服务器执行完毕后主动抓取未分配的分片,并且在某台服务器下线后主动寻找可用的服务器执行任务。</li></ul><p></p><h3 id="使用中遇到的坑"><a href="#使用中遇到的坑" class="headerlink" title="使用中遇到的坑"></a>使用中遇到的坑</h3><p>首先看下最初的配置</p><figure class="highlight xml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs xml"><span class="hljs-tag"><<span class="hljs-name">job:simple</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"UpdateAdvertMatchTagJob"</span> <span class="hljs-attr">class</span>=<span class="hljs-string">"cn.com.duiba.tuia.core.biz.job.UpdateAdvertMatchTagJob"</span> <span class="hljs-attr">description</span>=<span class="hljs-string">"更新标签广告redis缓存数据"</span> <span class="hljs-attr">registry-center-ref</span>=<span class="hljs-string">"regCenter"</span> <span class="hljs-attr">cron</span>=<span class="hljs-string">"0 0/5 * * * ?"</span> <span class="hljs-attr">sharding-total-count</span>=<span class="hljs-string">"1"</span> <span class="hljs-attr">event-trace-rdb-data-source</span>=<span class="hljs-string">"dataSource"</span> /></span><br>``` <br>上面配置使用中遇到一个问题,就是第一次启动项目之后再修改这里的参数怎么都不生效。翻阅[官方文档](http://elasticjob.io/docs/elastic-job-lite/02-guide/config-manual/)发现有个overwrite的参数,它的意思是:<br>> 本地配置是否可覆盖注册中心配置,如果可覆盖,每次启动作业都以本地配置为准<br><br>通过这个参数可以知道elastic-job,如果没有配置这个参数,配置第一次初始化之后不会在更改了。<br>翻阅 `#JobScheduler#init` 作业初始化的源码<br><br>```java<br>/**<br> * 初始化作业.<br> */<br>public void init() {<br> LiteJobConfiguration liteJobConfigFromRegCenter = schedulerFacade.updateJobConfiguration(liteJobConfig);<br> JobRegistry.getInstance().setCurrentShardingTotalCount(liteJobConfigFromRegCenter.getJobName(), liteJobConfigFromRegCenter.getTypeConfig().getCoreConfig().getShardingTotalCount());<br> JobScheduleController jobScheduleController = new JobScheduleController(<br> createScheduler(), createJobDetail(liteJobConfigFromRegCenter.getTypeConfig().getJobClass()), liteJobConfigFromRegCenter.getJobName());<br> JobRegistry.getInstance().registerJob(liteJobConfigFromRegCenter.getJobName(), jobScheduleController, regCenter);<br> schedulerFacade.registerStartUpInfo(!liteJobConfigFromRegCenter.isDisabled());<br> jobScheduleController.scheduleJob(liteJobConfigFromRegCenter.getTypeConfig().getCoreConfig().getCron());<br>}<br></code></pre></td></tr></table></figure><p>首先是调用的更新配置方法,查看 <code>#ConfigurationService#persist </code>方法的实现 </p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-comment">/**</span><br><span class="hljs-comment"> * 持久化分布式作业配置信息.</span><br><span class="hljs-comment"> * </span><br><span class="hljs-comment"> * <span class="hljs-doctag">@param</span> liteJobConfig 作业配置</span><br><span class="hljs-comment"> */</span><br><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">persist</span><span class="hljs-params">(<span class="hljs-keyword">final</span> LiteJobConfiguration liteJobConfig)</span> {<br> checkConflictJob(liteJobConfig);<br> <span class="hljs-keyword">if</span> (!jobNodeStorage.isJobNodeExisted(ConfigurationNode.ROOT) || liteJobConfig.isOverwrite()) {<br> jobNodeStorage.replaceJobNode(ConfigurationNode.ROOT, LiteJobConfigurationGsonFactory.toJson(liteJobConfig));<br> }<br>}<br></code></pre></td></tr></table></figure><p>这里的if条件先判断注册中心的节点是否存在,如果不存在并且 <code>overwrite</code> 的配置为true才会更新注册中心里的配置。被坑的就是这里,不管怎么修改配置甚至加上了 <code>overwrite</code> 就是不生效。所以如果第一次初始化的时候没有加 <code>overwrite</code> 参数,之后你的配置不会再被修改了,除非自己手动修改注册中心的值,或者删除注册中心的值重新初始化。所以建议这个参数最好加上,这样也可以避免修改了注册中心里的值但是和代码里的配置又对不上的问题。</p><hr><p>第二个问题是追踪数据源的问题,通过程序自己创建的表名是大写的。那么问题来了,DBA那关通过不了,说大写的表名不符合规范。当时心里一万个草泥马,都快上线了,难道要卡在这里。<br>和DBA撕逼之后还是妥协修改表名称为小写,谁让人家是大腿呢。<br>查看数据源监听器 <code>#JobEventRdbListener#JobEventRdbStorage</code> </p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> <span class="hljs-variable">TABLE_JOB_EXECUTION_LOG</span> <span class="hljs-operator">=</span> <span class="hljs-string">"job_execution_log"</span>;<br> <br><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> <span class="hljs-variable">TABLE_JOB_STATUS_TRACE_LOG</span> <span class="hljs-operator">=</span> <span class="hljs-string">"job_status_trace_log"</span>;<br> <br><span class="hljs-keyword">private</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">final</span> <span class="hljs-type">String</span> <span class="hljs-variable">TASK_ID_STATE_INDEX</span> <span class="hljs-operator">=</span> <span class="hljs-string">"idx_task_state"</span>;<br> <br><span class="hljs-keyword">private</span> <span class="hljs-keyword">final</span> DataSource dataSource;<br> <br><span class="hljs-keyword">private</span> DatabaseType databaseType;<br>``` <br>它已经把表名放在了常量里,修改还是比较方便的,ok,这个问题也解决了。<br><br>---<br><br>第三个问题是在监控平台查看job配置列表总是报<span class="hljs-number">500</span>的错误,查看作业状态展示的实现类 `#JobStatisticsAPIImpl#getJobBriefInfo`<br><br>```java<br><span class="hljs-keyword">public</span> JobBriefInfo <span class="hljs-title function_">getJobBriefInfo</span><span class="hljs-params">(<span class="hljs-keyword">final</span> String jobName)</span> {<br> <span class="hljs-type">JobNodePath</span> <span class="hljs-variable">jobNodePath</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">JobNodePath</span>(jobName);<br> <span class="hljs-type">JobBriefInfo</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-title class_">JobBriefInfo</span>();<br> result.setJobName(jobName);<br> <span class="hljs-type">String</span> <span class="hljs-variable">liteJobConfigJson</span> <span class="hljs-operator">=</span> regCenter.get(jobNodePath.getConfigNodePath());<br> <span class="hljs-keyword">if</span> (liteJobConfigJson.isEmpty()) {<br> <span class="hljs-keyword">return</span> <span class="hljs-literal">null</span>;<br> }<br> <span class="hljs-type">LiteJobConfiguration</span> <span class="hljs-variable">liteJobConfig</span> <span class="hljs-operator">=</span> LiteJobConfigurationGsonFactory.fromJson(liteJobConfigJson);<br> result.setDescription(liteJobConfig.getTypeConfig().getCoreConfig().getDescription());<br> result.setCron(liteJobConfig.getTypeConfig().getCoreConfig().getCron());<br> result.setInstanceCount(getJobInstanceCount(jobName));<br> result.setShardingTotalCount(liteJobConfig.getTypeConfig().getCoreConfig().getShardingTotalCount());<br> result.setStatus(getJobStatus(jobName));<br> <span class="hljs-keyword">return</span> result;<br>}<br>``` <br>这里的 `liteJobConfigJson` 配置信息有可能为空,应该加个空判断<br><br>```java<br><span class="hljs-title function_">if</span> <span class="hljs-params">(liteJobConfigJson == <span class="hljs-literal">null</span> || liteJobConfigJson.isEmpty()</span>) <br></code></pre></td></tr></table></figure><p>但是问题是什么情况下配置的信息会为空值呢?这个问题修复后界面上只会展示elastic-job 2.0的任务节点,于是我对比了下1.0和2.0在注册中心的目录结构,发现servers的结构不一样.</p><p><img src="/images/deffied.jpg"></p><p>所以如果是1.0的 job,获取子节点会是空值。所以job的控制台只能兼容到2.0。</p><blockquote><p>ps: 之前已经接入过elastic-job的系统都还停留在1.0版本</p></blockquote><hr><p>第四个问题是在控制台禁用 job 之后,如果服务重启任务又被开启了。</p><p><img src="/images/console.png"></p><p>这里的原理和第一个问题一样,在任务初始化的时候,都会将任务为可用状态开启</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs java">schedulerFacade.registerStartUpInfo(!liteJobConfigFromRegCenter.isDisabled());<br></code></pre></td></tr></table></figure><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>elastic-job的设计理念我觉都很巧妙,比如选举,如果是我可能会设计成同步锁,还有去中心化、分片策略、失效转移的思路也很值得去探究。 </p><p>目前的elastic-job定位是一个基于java的定时任务调度框架,仍然有很多不足的地方:</p><ol><li>异构语言不支持<ul><li>目前采用的无中心设计,难于支持多语言,后面需要考虑调度中心的可行性。</li></ul></li><li>监控体系有待提高,目前只能通过注册中心做简单的存活和数据积压监控,未来需要做的监控部分有:<ul><li>增加可监控维度,如作业运行时间等。</li><li>基于JMX的内部状态监控。</li><li>基于历史的全量数据监控,将所有监控数据通过flume等形式发到外部监控中心,提供实时分析功能。</li></ul></li><li>不能支持多种注册中心。</li><li>需要增加任务工作流,如任务依赖,初始化任务,清理任务等。</li><li>失效转移功能的实时性有待提升。</li><li>缺少更多作业类型支持,如文件,MQ等类型作业的支持。</li><li>缺少更多分片策略支持。</li></ol>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>elastic-job</tag>
</tags>
</entry>
<entry>
<title>Guava Cache 在广告系统中的优化</title>
<link href="/guava-cache/"/>
<url>/guava-cache/</url>
<content type="html"><![CDATA[<h3 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h3><blockquote><p>我们现在做的事情简单的说就是对流量、用户行为、特征等多维度分析,从而给用户推荐最精准的广告。这个过程就是我们现在做的事情也是我们广告组最核心的任务。本文主要从 Guava Cache 知识点的角度讲述它在我们系统中的优化过程,并不会嗸述太多知识点。</p></blockquote><h3 id="面临的问题"><a href="#面临的问题" class="headerlink" title="面临的问题"></a>面临的问题</h3><p>我们将广告的信息存放在 Redis,在线上跑了一段时间并没有问题。就在一天 Redis 出现超时的告警,而且频率越来越高。我们开始对流量进行分析,发现是业务快速增长已达到 Redis 的极限。所以燃眉之急的任务就是减少 Redis 的压力。</p><h3 id="关于缓存"><a href="#关于缓存" class="headerlink" title="关于缓存"></a>关于缓存</h3><p>缓存,在我们日常开发中是必不可少的一种解决性能问题的方法。 </p><p>其主要作用是暂时在内存中保存业务系统的数据处理结果,并且等待下次访问使用。在日常开发的很多场合,由于受限于硬盘IO的性能或者我们自身业务系统的数据处理和获取可能非常费时,当我们发现我们的系统这个数据请求量很大的时候,频繁的IO和频繁的逻辑处理会导致硬盘和CPU资源的瓶颈出现。缓存的作用就是将这些来自不易的数据保存在内存中,当有其他线程或者客户端需要查询相同的数据资源时,直接从缓存的内存块中返回数据,这样不但可以提高系统的响应时间,同时也可以节省对这些数据的处理流程的资源消耗,整体上来说,系统性能会有大大的提升。</p><h3 id="为什么要使用Guava-Cache"><a href="#为什么要使用Guava-Cache" class="headerlink" title="为什么要使用Guava Cache"></a>为什么要使用Guava Cache</h3><p>缓存的解决方案有很多种,第一个想到的肯定是第三方缓存服务器。刚开始我们使用的是 Memcache ,但是它只支持简单的key-value存储,并不能满足我们现有的场景所以改成了 Redis 。但是 Redis 是单线程的,如果请求数很多,会出现瓶颈导致线程阻塞,也就是我们现在面临的问题。这个时候就需要引入本地缓存了。</p><p>Guava Cache 说简单点就是一个支持LRU的<code>ConCurrentHashMap</code>,它没有 Ehcache 那么多的各种特性,只是提供了增、删、改、查、刷新规则和时效规则设定等最基本的元素,并提供了线程安全的实现机制。做一个 jar 包中的一个功能之一,Guava Cache 的极度简洁无非是不二之选,简单易用,性能好。</p><p>对于我个人而言,Guava 的开发活跃度和良好的质量保证是我更愿意转而使用 Guava 的的原因之一。Guava 几年发展下来各界发表的各类文章和其自身良好的文档风格也极大的帮助了该库的传播使用。 </p><h3 id="初次使用"><a href="#初次使用" class="headerlink" title="初次使用"></a>初次使用</h3><p>为了快速适应从 redis 迁移到 Guava Cache,以及业务优先的原则,简单的使用了它的 API ,大致如下:</p><figure class="highlight aspectj"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><code class="hljs aspectj">LoadingCache<Long, AdvertVO> advertCache = CacheBuilder.newBuilder().concurrencyLevel(<span class="hljs-number">200</span>).expireAfterWrite(<span class="hljs-number">60</span>,<br> TimeUnit.SECONDS).initialCapacity(<span class="hljs-number">500</span>).maximumSize(<span class="hljs-number">1000</span>).recordStats().build(<span class="hljs-keyword">new</span> CacheLoader<Long, AdvertVO>() {<br><br> <span class="hljs-meta">@Override</span><br> <span class="hljs-keyword">public</span> <span class="hljs-function">AdvertVO <span class="hljs-title">load</span><span class="hljs-params">(Long advertId)</span></span><br><span class="hljs-function"> <span class="hljs-keyword">throws</span> Exception </span>{<br> <span class="hljs-function"><span class="hljs-keyword">return</span> <span class="hljs-title">getAdvertByCache</span><span class="hljs-params">(advertId)</span></span>;<br> }<br> });<br></code></pre></td></tr></table></figure><p>上面的代码使用了两种方式清除数据。 </p><ul><li><p><code>maximumSize</code> 从字面意思看是按照缓存的大小来移除,如果到达指定的条目数,就会把不常用的键值对从cache中移除。但是要注意的是并不是完全到了指定的size系统才开始移除不常用的数据,而是接近这个size的时候系统就会开始做移除的动作。 </p></li><li><p><code>expireAfterWrite</code> 根据某个键值对被创建或值被替换后多少时间移除,即当缓存项在指定的时间段内没有更新就会被回收。需要注意的是它在回源的load方法上加了控制,对于同一个key,只会让一个请求回源load,其他线程阻塞等待结果。这样会很好地防止缓存失效的瞬间大量请求穿透到后端引起雪崩效应,它的请求回源过程如下:<br><img src="/images/guava-cache-expired.png"></p></li></ul><blockquote><p>这里提一下<code>expireAfterAccess</code>的清除方式,之前活动中心的团队有人使用它出现了问题。场景是这样的:<br>db中没有数据时接口调用缓存数据;在新增db记录后,接口返回的数据一直是错误数据。造成这个问题怀疑是代码笔误,因为后来我们对所有系统进行排查都没有发现用它的地方。那么什么场景使用它才合适呢?<code>expireAfterAccess</code>:当缓存项在指定的时间段内没有被访问就会被回收。反过来说就是如果在设置的超时时间内不断访问这个缓存,就永远不会执行load操作,所以在访问频次很低的时候或者有手动触发更新操作的场景才可以使用。</p></blockquote><p>就这样广告的redis缓存安全地迁移到了Guava Cache,虽然看似很简单,但是我们在背后还做了很多其它的事情,比如保证数据的一致性。</p><h3 id="第二次优化"><a href="#第二次优化" class="headerlink" title="第二次优化"></a>第二次优化</h3><p>刚才说的对于同一个key,只让一个请求回源,所以频繁的过期和加载,锁等待等过程会让性能有较大的损耗。而且其他线程都在等待状态,虽然对后端服务不会造成压力,但请求blocked了,整个请求还是会被堵一下。用户抽券的时候,从点击到展示的时间是3秒,如果超过这个时间,将会流失流量,最终导致的就是损失。因此我们考虑使用<code>refreshAfterWrite</code>。</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs gradle">LoadingCache<<span class="hljs-keyword">Long</span>, <span class="hljs-keyword">Long</span>> ADVERT_GROUP_CACHE = CacheBuilder.newBuilder().initialCapacity(<span class="hljs-number">1000</span>).<br> recordStats().refreshAfterWrite(<span class="hljs-number">15</span>, TimeUnit.MINUTES).build(<span class="hljs-keyword">new</span> CacheLoader<<span class="hljs-keyword">Long</span>, <span class="hljs-keyword">Long</span>>() {<br> @Override<br> <span class="hljs-keyword">public</span> <span class="hljs-keyword">Long</span> load(<span class="hljs-keyword">Long</span> advertId) <span class="hljs-keyword">throws</span> Exception {<br><br> GroupMemberDO groupMemberDO = getgroupByAdvertid(advertId);<br> <span class="hljs-keyword">return</span> groupMemberDO == <span class="hljs-keyword">null</span> || groupMemberDO.getGroupId() == <span class="hljs-keyword">null</span> ? <span class="hljs-number">0</span>L : groupMemberDO.getGroupId();<br> }<br><br> @Override<br> <span class="hljs-keyword">public</span> ListenableFuture<<span class="hljs-keyword">Long</span>> reload(<span class="hljs-keyword">final</span> <span class="hljs-keyword">Long</span> key, <span class="hljs-keyword">Long</span> oldValue) <span class="hljs-keyword">throws</span> Exception {<br><br> ListenableFutureTask<<span class="hljs-keyword">Long</span>> <span class="hljs-keyword">task</span> = ListenableFutureTask.create(<span class="hljs-keyword">new</span> Callable<<span class="hljs-keyword">Long</span>>() {<br> <span class="hljs-keyword">public</span> <span class="hljs-keyword">Long</span> <span class="hljs-keyword">call</span>() <span class="hljs-keyword">throws</span> Exception {<br> <span class="hljs-keyword">return</span> load(key);<br> }<br> });<br> executorService.submit(<span class="hljs-keyword">task</span>);<br> <span class="hljs-keyword">return</span> <span class="hljs-keyword">task</span>;<br> }<br> });<br></code></pre></td></tr></table></figure><p><code>refreshAfterWrite</code>的特点是,刷新时仍只有一个线程回源取数据,但其他线程只会稍微等一会,没等到就返回旧值,整个请求看起来就比较平滑了。这样就有效地可以减少等待和锁争用,所以它会比<code>expireAfterWrite</code>性能好。它的回源过程如下图:</p><p><img src="/images/guava-cache-refresh.png"></p><h3 id="第三次优化"><a href="#第三次优化" class="headerlink" title="第三次优化"></a>第三次优化</h3><p>上面代码的也有一个缺点,因为到达指定时间后,它不能严格保证所有的查询都获取到新值。了解过guava cache的定时失效(或刷新)的同学都知道,Guava Cache 并没使用额外的线程去做定时清理和加载的功能,而是依赖于查询请求。在查询的时候去比对上次更新的时间,如超过指定时间则进行回源。所以,如果使用<code>refreshAfterWrite</code>,在吞吐量很低的情况下,如很长一段时间内没有查询之后,发生的查询有可能会得到一个旧值(这个旧值可能来自于很长时间之前),这将会引发问题。是否有一个折中的办法在第二次优化的基础之上增加超时刷新呢?</p><p>源码分析Guava Cache的get方法:</p><figure class="highlight reasonml"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br></pre></td><td class="code"><pre><code class="hljs reasonml">V get(K key, <span class="hljs-built_in">int</span> hash, CacheLoader<? super K, V> loader) throws ExecutionException {<br> check<span class="hljs-constructor">NotNull(<span class="hljs-params">key</span>)</span>;<br> check<span class="hljs-constructor">NotNull(<span class="hljs-params">loader</span>)</span>;<br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-keyword">if</span> (count != <span class="hljs-number">0</span>) { <span class="hljs-comment">// read-volatile</span><br> <span class="hljs-comment">// don't call getLiveEntry, which would ignore loading values</span><br> ReferenceEntry<K, V> e = get<span class="hljs-constructor">Entry(<span class="hljs-params">key</span>, <span class="hljs-params">hash</span>)</span>;<br> <span class="hljs-keyword">if</span> (e != null) {<br> long now = map.ticker.read<span class="hljs-literal">()</span>;<br> V value = get<span class="hljs-constructor">LiveValue(<span class="hljs-params">e</span>, <span class="hljs-params">now</span>)</span>;<br> <span class="hljs-keyword">if</span> (value != null) {<br> record<span class="hljs-constructor">Read(<span class="hljs-params">e</span>, <span class="hljs-params">now</span>)</span>;<br> statsCounter.record<span class="hljs-constructor">Hits(1)</span>;<br> return schedule<span class="hljs-constructor">Refresh(<span class="hljs-params">e</span>, <span class="hljs-params">key</span>, <span class="hljs-params">hash</span>, <span class="hljs-params">value</span>, <span class="hljs-params">now</span>, <span class="hljs-params">loader</span>)</span>;<br> }<br> ValueReference<K, V> valueReference = e.get<span class="hljs-constructor">ValueReference()</span>;<br> <span class="hljs-keyword">if</span> (valueReference.is<span class="hljs-constructor">Loading()</span>) {<br> return wait<span class="hljs-constructor">ForLoadingValue(<span class="hljs-params">e</span>, <span class="hljs-params">key</span>, <span class="hljs-params">valueReference</span>)</span>;<br> }<br> }<br> }<br><br> <span class="hljs-comment">// at this point e is either null or expired;</span><br> return locked<span class="hljs-constructor">GetOrLoad(<span class="hljs-params">key</span>, <span class="hljs-params">hash</span>, <span class="hljs-params">loader</span>)</span>;<br> } catch (ExecutionException ee) {<br> Throwable cause = ee.get<span class="hljs-constructor">Cause()</span>;<br> <span class="hljs-keyword">if</span> (cause instanceof Error) {<br> throw <span class="hljs-keyword">new</span> <span class="hljs-constructor">ExecutionError((Error)</span> cause);<br> } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (cause instanceof RuntimeException) {<br> throw <span class="hljs-keyword">new</span> <span class="hljs-constructor">UncheckedExecutionException(<span class="hljs-params">cause</span>)</span>;<br> }<br> throw ee;<br> } finally {<br> post<span class="hljs-constructor">ReadCleanup()</span>;<br> }<br> }<br></code></pre></td></tr></table></figure><p>如果不存在缓存e值的时候会直接执行load方法,如果存在会继续判断value是否过期,如果过期了,会执行load方法而非reload方法,如果没有过期,会继续判断是否执行reload方法。</p><p>从代码来看,在get的时候,是先判断过期,再判断refresh,所以我们可以通过设置<code>refreshAfterWrite</code>为1s,将<code>expireAfterWrite</code>设为2s,当访问频繁的时候,会在每秒都进行refresh,而当超过2s没有访问,下一次访问必须load新值。</p><figure class="highlight stylus"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs stylus"><span class="hljs-function"><span class="hljs-title">expireAfterWrite</span><span class="hljs-params">(<span class="hljs-number">30</span>, TimeUnit.MINUTES)</span></span><br></code></pre></td></tr></table></figure><p>所以最终的代码在第二部的基础上又增加了超时的设置,保证了在过去很久之后也不会读取到旧值了。</p><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>我们现在做的事情都是围绕如何提高广告的ROI展开的,随着业务的不断增长,技术覆盖面也越来越广。这就对我们技术的要求更高,不能只实现需求就完事了,还要不断的琢磨技术,真正的掌握技术,让技术在系统中发挥出最大的作用,从而保证系统稳定以及为业务发展奠下扎实的基础。</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Guava</tag>
</tags>
</entry>
<entry>
<title>Git 如何撤销分支的merge</title>
<link href="/git-revert/"/>
<url>/git-revert/</url>
<content type="html"><![CDATA[<h3 id="首先回滚两种方式的区别"><a href="#首先回滚两种方式的区别" class="headerlink" title="首先回滚两种方式的区别"></a>首先回滚两种方式的区别</h3><p><b>revert</b>:是用一次新的commit回滚之前的commit,所以它的版本号不会变,这样就会导致在日后继续merge以前的老版本的时候,这些差异并不会显示,我们这次也有这个问题,master和develop对比差异的时候,并没有把回滚之后的差异显示出来。 </p><p><b>reset</b>:是直接删除指定的commit,提交及之前的commit都会被保留,但是此次之后的修改都会被退回到暂存区。</p><p>举个例子:假设有三个commit, git log: </p><figure class="highlight avrasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs avrasm"><span class="hljs-symbol">commit3:</span> <span class="hljs-keyword">add</span> test3.c <br><span class="hljs-symbol">commit2:</span> <span class="hljs-keyword">add</span> test2.c <br><span class="hljs-symbol">commit1:</span> <span class="hljs-keyword">add</span> test1.c <br></code></pre></td></tr></table></figure><p>当执行git revert HEAD~1时, commit2被撤销了,git log可以看到:</p><figure class="highlight avrasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs avrasm">revert <span class="hljs-string">"commit2"</span>:this reverts commit <span class="hljs-number">5</span>fe21s2... <br><span class="hljs-symbol">commit3:</span> <span class="hljs-keyword">add</span> test3.c <br><span class="hljs-symbol">commit2:</span> <span class="hljs-keyword">add</span> test2.c <br><span class="hljs-symbol">commit1:</span> <span class="hljs-keyword">add</span> test1.c<br></code></pre></td></tr></table></figure><p>git status 没有任何变化,如果换做执行git reset –soft(默认) HEAD~1后,运行git log </p><figure class="highlight avrasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs avrasm"><span class="hljs-symbol">commit2:</span> <span class="hljs-keyword">add</span> test2.c <br><span class="hljs-symbol">commit1:</span> <span class="hljs-keyword">add</span> test1.c<br></code></pre></td></tr></table></figure><p>运行git status, 则test3.c处于暂存区,准备提交。<br>如果换做执行git reset –hard HEAD~1后,显示:HEAD is now at commit2,运行git log</p><figure class="highlight avrasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs avrasm"><span class="hljs-symbol">commit2:</span> <span class="hljs-keyword">add</span> test2.c <br><span class="hljs-symbol">commit1:</span> <span class="hljs-keyword">add</span> test1.c<br></code></pre></td></tr></table></figure><p>运行git log, 没有任何变化</p><h3 id="怎么撤销megre呢?"><a href="#怎么撤销megre呢?" class="headerlink" title="怎么撤销megre呢?"></a>怎么撤销megre呢?</h3><p><b>方法一</b> 如果没有新的提交可以用reset 到 merge 前的版本,然后再重做接下来的操作,要求每个合作者都晓得怎么将本地的 HEAD 都回滚回去:</p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs pgsql">$ git <span class="hljs-keyword">reset</span> <span class="hljs-comment">--hard 【merge前的版本号】</span><br></code></pre></td></tr></table></figure><p><b>方法二</b> 当 merge 以后还有别的操作和改动时,git 正好也有办法能撤销 merge,用 git revert:</p><figure class="highlight cos"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs cos">$ git revert -m 【要撤销的那条<span class="hljs-keyword">merge</span>线的编号,从<span class="hljs-number">1</span>开始计算】 【<span class="hljs-keyword">merge</span>前的版本号】<br></code></pre></td></tr></table></figure><p>这样会创建新的 commit 来抵消对应的 merge 操作,而且以后 git merge 【那个编号所代表的分支】 会提示:</p><figure class="highlight applescript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs applescript">Already up-<span class="hljs-keyword">to</span>-<span class="hljs-built_in">date</span>.<br></code></pre></td></tr></table></figure><p>因为使用方法二会让 git 误以为这个分支的东西都是咱们不想要的。</p><p><b>方法三</b> 怎么撤销方法二</p><figure class="highlight n1ql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs n1ql">$ git revert 【方法二撤销<span class="hljs-keyword">merge</span>时提交的<span class="hljs-keyword">commit</span>的版本号,这里是<span class="hljs-number">88</span>edd6d】<br></code></pre></td></tr></table></figure><p>方法二里的megre线的编号怎么算,1:合并目标的分支 2:合并来源分支 </p><blockquote><p>一般来说,如果你在master上merge zhc_branch,那么parent 1就是master,parent 2就是zhc_branch.</p></blockquote>]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Git</tag>
</tags>
</entry>
<entry>
<title>如何定位消耗CPU最多的线程</title>
<link href="/jvm-thread/"/>
<url>/jvm-thread/</url>
<content type="html"><![CDATA[<p>话不多说了,先来看代码吧</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">Test</span>{<br> <span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">main</span><span class="hljs-params">(String args[])</span>{<br> <span class="hljs-keyword">for</span>(<span class="hljs-type">int</span> i=<span class="hljs-number">0</span>;i<<span class="hljs-number">10</span>;i++){<br> <span class="hljs-keyword">new</span> <span class="hljs-title class_">Thread</span>(){<br> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">run</span><span class="hljs-params">()</span>{<br> <span class="hljs-keyword">try</span>{<br> Thread.sleep(<span class="hljs-number">100000</span>);<br> }<span class="hljs-keyword">catch</span>(Exception e){}<br> }<br> }.start();<br> }<br> Thread t=<span class="hljs-keyword">new</span> <span class="hljs-title class_">Thread</span>(){<br> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">run</span><span class="hljs-params">()</span>{<br> <span class="hljs-type">int</span> i=<span class="hljs-number">0</span>;<br> <span class="hljs-keyword">while</span>(<span class="hljs-literal">true</span>){<br> i=(i++)/<span class="hljs-number">100</span>;<br> }<br> }<br> };<br> t.setName(<span class="hljs-string">"Busiest Thread"</span>);<br> t.start();<br> }<br>}<br></code></pre></td></tr></table></figure><p>这个例子里新创建了11个线程,其中10个线程没干什么事,主要是sleep,另外有一个线程在循环里一直跑着,可以想象这个线程是这个进程里最耗cpu的线程了,那怎么把这个线程给抓出来呢?</p><p>首先我们可以通过top -Hp <pid>来看这个进程里所有线程的cpu消耗情况,得到类似下面的数据</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><code class="hljs linux">$ top -Hp 18207<br>top - 19:11:43 up 573 days, 2:43, 2 users, load average: 3.03, 3.03, 3.02<br>Tasks: 44 total, 1 running, 43 sleeping, 0 stopped, 0 zombie<br>Cpu(s): 18.8%us, 0.0%sy, 0.0%ni, 81.1%id, 0.0%wa, 0.0%hi, 0.0%si, 0.0%st<br>Mem: 99191752k total, 98683576k used, 508176k free, 128248k buffers<br>Swap: 1999864k total, 191064k used, 1808800k free, 17413760k cached<br><br> PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND<br>18250 admin 20 0 26.1g 28m 10m R 99.9 0.0 0:19.50 java Test<br>18207 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18208 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.09 java Test<br>18209 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18210 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18211 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18212 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18213 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18214 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18215 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18216 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18217 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18218 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18219 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18220 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18221 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18222 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18223 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18224 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18225 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18226 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br>18227 admin 20 0 26.1g 28m 10m S 0.0 0.0 0:00.00 java Test<br></code></pre></td></tr></table></figure><p>拿到这个结果之后,我们可以看到cpu最高的线程是pid为18250的线程,占了99.8%:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs linux">PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND<br>18250 admin 20 0 26.1g 28m 10m R 99.9 0.0 0:19.50 java Test<br></code></pre></td></tr></table></figure><p>接着我们可以通过jstack <pid>的输出来看各个线程栈:</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs linux">$ jstack 18207<br>2016-03-30 19:12:23<br>Full thread dump OpenJDK 64-Bit Server VM (25.66-b60 mixed mode):<br><br>"Attach Listener" #30 daemon prio=9 os_prio=0 tid=0x00007fb90be13000 nid=0x47d7 waiting on condition [0x0000000000000000]<br> java.lang.Thread.State: RUNNABLE<br><br>"DestroyJavaVM" #29 prio=5 os_prio=0 tid=0x00007fb96245b800 nid=0x4720 waiting on condition [0x0000000000000000]<br> java.lang.Thread.State: RUNNABLE<br><br>"Busiest Thread" #28 prio=5 os_prio=0 tid=0x00007fb91498d000 nid=0x474a runnable [0x00007fb9065fe000]<br> java.lang.Thread.State: RUNNABLE<br> at Test$2.run(Test.java:18)<br><br>"Thread-9" #27 prio=5 os_prio=0 tid=0x00007fb91498c800 nid=0x4749 waiting on condition [0x00007fb906bfe000]<br> java.lang.Thread.State: TIMED_WAITING (sleeping)<br> at java.lang.Thread.sleep(Native Method)<br> at Test$1.run(Test.java:9)<br><br>"Thread-8" #26 prio=5 os_prio=0 tid=0x00007fb91498b800 nid=0x4748 waiting on condition [0x00007fb906ffe000]<br> java.lang.Thread.State: TIMED_WAITING (sleeping)<br> at java.lang.Thread.sleep(Native Method)<br> at Test$1.run(Test.java:9)<br><br>"Thread-7" #25 prio=5 os_prio=0 tid=0x00007fb91498b000 nid=0x4747 waiting on condition [0x00007fb9073fe000]<br> java.lang.Thread.State: TIMED_WAITING (sleeping)<br> at java.lang.Thread.sleep(Native Method)<br> at Test$1.run(Test.java:9)<br><br>"Thread-6" #24 prio=5 os_prio=0 tid=0x00007fb91498a000 nid=0x4746 waiting on condition [0x00007fb9077fe000]<br> java.lang.Thread.State: TIMED_WAITING (sleeping)<br> at java.lang.Thread.sleep(Native Method)<br> at Test$1.run(Test.java:9)<br>...<br></code></pre></td></tr></table></figure><p>上面的线程栈我们注意到nid的值其实就是线程ID,它是十六进制的,我们将消耗cpu最高的线程18250,转成十六进制0X47A,然后从上面的线程栈里找到nid=0X47A的线程,其栈为:</p><pre><code class="linux">"Busiest Thread" #28 prio=5 os_prio=0 tid=0x00007fb91498d000 nid=0x474a runnable [0x00007fb9065fe000] java.lang.Thread.State: RUNNABLE at Test$2.run(Test.java:18)</code></pre><p>即将最耗cpu的线程找出来了,是Businest Thread</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Thread</tag>
</tags>
</entry>
<entry>
<title>mybaits 关于游标状态closed的问题</title>
<link href="/mybaits-cursor/"/>
<url>/mybaits-cursor/</url>
<content type="html"><![CDATA[<p>由于数据不断增加,查询的时候如果一次性加载出来,会导致mybaits的mapper对象非常大,可以选择分页但是觉得太麻烦了所以选择了游标。之前使用游标是jdbc自带的,没有遇到读取数据游标状态会closed的情况。后来不断debug发现是mybaits封装SqlSessionTemplate的问题。现把解决的过程记录下来。</p><h4 id="下面是SqlSessionTemplate执行语句的主要内容"><a href="#下面是SqlSessionTemplate执行语句的主要内容" class="headerlink" title="下面是SqlSessionTemplate执行语句的主要内容"></a>下面是SqlSessionTemplate执行语句的主要内容</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">private</span> <span class="hljs-keyword">class</span> <span class="hljs-title class_">SqlSessionInterceptor</span> <span class="hljs-keyword">implements</span> <span class="hljs-title class_">InvocationHandler</span> {<br> <span class="hljs-meta">@Override</span><br> <span class="hljs-keyword">public</span> Object <span class="hljs-title function_">invoke</span><span class="hljs-params">(Object proxy, Method method, Object[] args)</span> <span class="hljs-keyword">throws</span> Throwable {<br> <span class="hljs-type">SqlSession</span> <span class="hljs-variable">sqlSession</span> <span class="hljs-operator">=</span> getSqlSession(<br> SqlSessionTemplate.<span class="hljs-built_in">this</span>.sqlSessionFactory,<br> SqlSessionTemplate.<span class="hljs-built_in">this</span>.executorType,<br> SqlSessionTemplate.<span class="hljs-built_in">this</span>.exceptionTranslator);<br> <span class="hljs-keyword">try</span> {<br> <span class="hljs-type">Object</span> <span class="hljs-variable">result</span> <span class="hljs-operator">=</span> method.invoke(sqlSession, args);<br> <span class="hljs-keyword">if</span> (!isSqlSessionTransactional(sqlSession, SqlSessionTemplate.<span class="hljs-built_in">this</span>.sqlSessionFactory)) {<br> <span class="hljs-comment">// force commit even on non-dirty sessions because some databases require</span><br> <span class="hljs-comment">// a commit/rollback before calling close()</span><br> sqlSession.commit(<span class="hljs-literal">true</span>);<br> }<br> <span class="hljs-keyword">return</span> result;<br> } <span class="hljs-keyword">catch</span> (Throwable t) {<br> <span class="hljs-type">Throwable</span> <span class="hljs-variable">unwrapped</span> <span class="hljs-operator">=</span> unwrapThrowable(t);<br> <span class="hljs-keyword">if</span> (SqlSessionTemplate.<span class="hljs-built_in">this</span>.exceptionTranslator != <span class="hljs-literal">null</span> && unwrapped <span class="hljs-keyword">instanceof</span> PersistenceException) {<br> <span class="hljs-comment">// release the connection to avoid a deadlock if the translator is no loaded. See issue #22</span><br> closeSqlSession(sqlSession, SqlSessionTemplate.<span class="hljs-built_in">this</span>.sqlSessionFactory);<br> sqlSession = <span class="hljs-literal">null</span>;<br> <span class="hljs-type">Throwable</span> <span class="hljs-variable">translated</span> <span class="hljs-operator">=</span> SqlSessionTemplate.<span class="hljs-built_in">this</span>.exceptionTranslator.translateExceptionIfPossible((PersistenceException) unwrapped);<br> <span class="hljs-keyword">if</span> (translated != <span class="hljs-literal">null</span>) {<br> unwrapped = translated;<br> }<br> }<br> <span class="hljs-keyword">throw</span> unwrapped;<br> } <span class="hljs-keyword">finally</span> {<br> <span class="hljs-keyword">if</span> (sqlSession != <span class="hljs-literal">null</span>) {<br> closeSqlSession(sqlSession, SqlSessionTemplate.<span class="hljs-built_in">this</span>.sqlSessionFactory);<br> }<br> }<br> }<br> }<br></code></pre></td></tr></table></figure><p>这个方法的最后会关闭session,问题就出在这里,当游标读完的时候也会被关闭。但不是我们想要的结果。再看看SqlSessionUtils.closeSqlSession里的方法。</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><code class="hljs java"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title function_">closeSqlSession</span><span class="hljs-params">(SqlSession session, SqlSessionFactory sessionFactory)</span> {<br> notNull(session, NO_SQL_SESSION_SPECIFIED);<br> notNull(sessionFactory, NO_SQL_SESSION_FACTORY_SPECIFIED);<br><br> <span class="hljs-type">SqlSessionHolder</span> <span class="hljs-variable">holder</span> <span class="hljs-operator">=</span> (SqlSessionHolder) TransactionSynchronizationManager.getResource(sessionFactory);<br> <span class="hljs-keyword">if</span> ((holder != <span class="hljs-literal">null</span>) && (holder.getSqlSession() == session)) {<br> <span class="hljs-keyword">if</span> (LOGGER.isDebugEnabled()) {<br> LOGGER.debug(<span class="hljs-string">"Releasing transactional SqlSession ["</span> + session + <span class="hljs-string">"]"</span>);<br> }<br> holder.released();<br> } <span class="hljs-keyword">else</span> {<br> <span class="hljs-keyword">if</span> (LOGGER.isDebugEnabled()) {<br> LOGGER.debug(<span class="hljs-string">"Closing non transactional SqlSession ["</span> + session + <span class="hljs-string">"]"</span>);<br> }<br> session.close();<br> }<br> }<br></code></pre></td></tr></table></figure><p>在第5行的判断,大概意思是如果是同一事务里的session会继续执行下去。<br>所以解决这个问题就是在读取游标的外面加一层事务控制。感觉没有jdbc读取游标的方法好用,还多此一举了,先这么办吧。之后如果有时间改造一个读取游标的方法。</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Mybaits</tag>
</tags>
</entry>
<entry>
<title>Gradle SNAPSHOT版本不能更新的问题</title>
<link href="/gradle-refresh/"/>
<url>/gradle-refresh/</url>
<content type="html"><![CDATA[<p>团队里使用SNAPSHOT版本每次修改都要重新发布个版本,本身就是快照版,为什么都要升级呢,还不如用release版本呢。之前一直使用maven没有发现这个问题,是因为gradle为了加快构建的速度,对jar包默认会缓存24小时,缓存之后就不在请求远程仓库了。 </p><h4 id="在Gradle里设置本地缓存的更新策略"><a href="#在Gradle里设置本地缓存的更新策略" class="headerlink" title="在Gradle里设置本地缓存的更新策略"></a>在Gradle里设置本地缓存的更新策略</h4><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><code class="hljs java">configurations.all { <br><span class="hljs-comment">// check for updates every build </span><br>resolutionStrategy.cacheChangingModulesFor <span class="hljs-number">0</span>,<span class="hljs-string">'seconds'</span> <br>}<br></code></pre></td></tr></table></figure><p>上面的方法别人可能会有效果,但是我的就是怎么都没效果。google了半天,发现这个插件效果没有应用到子项目里,参考地址:<a href="https://github.com/spring-gradle-plugins/dependency-management-plugin/issues/38">https://github.com/spring-gradle-plugins/dependency-management-plugin/issues/38</a></p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs java">dependencyManagement {<br> resolutionStrategy {<br> cacheChangingModulesFor <span class="hljs-number">0</span>, <span class="hljs-string">'seconds'</span><br> }<br>}<br></code></pre></td></tr></table></figure><p>好了,看下gradle的构建日志,去远程仓库获取了。</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Gradle</tag>
</tags>
</entry>
<entry>
<title>Nginx 配置location 匹配规则总结</title>
<link href="/nginx-location/"/>
<url>/nginx-location/</url>
<content type="html"><![CDATA[<p>项目拆分的时候,需要将老的请求转发到新的项目,配置location总是不生效,查资料发现是执行优先级的原因,现在对它的用法做个记录.</p><h2 id="location正则写法"><a href="#location正则写法" class="headerlink" title="location正则写法"></a>location正则写法</h2><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br></pre></td><td class="code"><pre><code class="hljs vim">location = / {<br> # 精确匹配 / ,主机名后面不能带任何字符串<br> [ configuration A ]<br>}<br>location / {<br> # 因为所有的地址都以 / 开头,所以这条规则将匹配到所有请求<br> # 但是正则和最长字符串会优先匹配<br> [ configuration B ]<br>}<br>location /documents/ {<br> # 匹配任何以 /documents/ 开头的地址,匹配符合以后,还要继续往下搜索<br> # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条<br> [ configuration C ]<br>}<br>location ~ /documents/Abc {<br> # 匹配任何以 /documents/Abc 开头的地址,匹配符合以后,还要继续往下搜索<br> # 只有后面的正则表达式没有匹配到时,这一条才会采用这一条<br> [ configuration CC ]<br>}<br>location ^~ /images/ {<br> # 匹配任何以 /images/ 开头的地址,匹配符合以后,停止往下搜索正则,采用这一条。<br> [ configuration D ]<br>}<br>location ~* \.(gif|jpg|jpeg)$ {<br> # 匹配所有以 gif,jpg或jpeg 结尾的请求<br> # 然而,所有请求 /images/ 下的图片会被 config D 处理,因为 ^~ 到达不了这一条正则<br> [ configuration E ]<br>}<br>location /images/ {<br> # 字符匹配到 /images/,继续往下,会发现 ^~ 存在<br> [ configuration F ]<br>}<br>location /images/<span class="hljs-keyword">abc</span> {<br> # 最长字符匹配到 /images/<span class="hljs-keyword">abc</span>,继续往下,会发现 ^~ 存在<br> # F与G的放置顺序是没有关系的<br> [ configuration G ]<br>}<br>location ~ /images/<span class="hljs-keyword">abc</span>/ {<br> # 只有去掉 config D 才有效:先最长匹配 config G 开头的地址,继续往下搜索,匹配到这一条正则,采用<br> [ configuration H ]<br>}<br>location ~* /js/.*/\.js<br></code></pre></td></tr></table></figure><ul><li>开头表示精确匹配</li><li>如 A 中只匹配根目录结尾的请求,后面不能带任何字符串。</li><li>^~ 开头表示uri以某个常规字符串开头,不是正则匹配</li><li>~ 开头表示区分大小写的正则匹配;</li><li>~* 开头表示不区分大小写的正则匹配</li><li>/ 通用匹配, 如果没有其它匹配,任何请求都会匹配到</li></ul><h2 id="顺序优先级"><a href="#顺序优先级" class="headerlink" title="顺序优先级"></a>顺序优先级</h2><blockquote><p>(location =) > (location 完整路径) > (location ^~ 路径) > (location <del>,</del>* 正则顺序) >(location 部分起始路径) > (/)</p></blockquote><p><strong>上面的匹配结果,按照上面的location写法,以下的匹配示例成立</strong><br>/ -> config A<br>精确完全匹配,即使/index.html也匹配不了<br>/downloads/download.html -> config B<br>匹配B以后,往下没有任何匹配,采用B<br>/images/1.gif -> configuration D<br>匹配到F,往下匹配到D,停止往下<br>/images/abc/def -> config D<br>最长匹配到G,往下匹配D,停止往下<br>你可以看到 任何以/images/开头的都会匹配到D并停止,FG写在这里是没有任何意义的,H是永远轮不到 的,这里只是为了说明匹配顺序<br>/documents/document.html -> config C<br>匹配到C,往下没有任何匹配,采用C<br>/documents/1.jpg -> configuration E<br>匹配到C,往下正则匹配到E<br>/documents/Abc.jpg -> config CC<br>最长匹配到C,往下正则顺序匹配到CC,不会往下到E </p><h2 id="实际使用建议"><a href="#实际使用建议" class="headerlink" title="实际使用建议"></a>实际使用建议</h2><figure class="highlight crmsh"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre></td><td class="code"><pre><code class="hljs crmsh">所以实际使用中,个人觉得至少有三个匹配规则定义,如下:<br><span class="hljs-comment">#直接匹配网站根,通过域名访问网站首页比较频繁,使用这个会加速处理,官网如是说。</span><br><span class="hljs-comment">#这里是直接转发给后端应用服务器了,也可以是一个静态首页</span><br><span class="hljs-comment"># 第一个必选规则</span><br><span class="hljs-keyword">location</span> <span class="hljs-title">= / {</span><br><span class="hljs-title"> proxy_pass</span> http://tomcat:<span class="hljs-number">8080</span>/index<br>}<br><span class="hljs-comment"># 第二个必选规则是处理静态文件请求,这是nginx作为http服务器的强项</span><br><span class="hljs-comment"># 有两种配置模式,目录匹配或后缀匹配,任选其一或搭配使用</span><br><span class="hljs-keyword">location</span> <span class="hljs-title">^~ /static</span>/ {<br> root /webroot/static/;<br>}<br><span class="hljs-keyword">location</span> <span class="hljs-title">~* \.(gif</span>|jpg|jpeg|png|css|js|ico)$ {<br> root /webroot/res/;<br>}<br><span class="hljs-comment">#第三个规则就是通用规则,用来转发动态请求到后端应用服务器</span><br><span class="hljs-comment">#非静态文件请求就默认是动态请求,自己根据实际把握</span><br><span class="hljs-comment">#毕竟目前的一些框架的流行,带.php,.jsp后缀的情况很少了</span><br><span class="hljs-keyword">location</span> <span class="hljs-title">/ {</span><br><span class="hljs-title"> proxy_pass</span> http://tomcat:<span class="hljs-number">8080</span>/<br>}<br></code></pre></td></tr></table></figure><p>附上两个地址:<br><em><a href="http://tengine.taobao.org/book/chapter_02.html">http://tengine.taobao.org/book/chapter_02.html</a></em><br><em><a href="http://nginx.org/en/docs/http/ngx_http_rewrite_module.html">http://nginx.org/en/docs/http/ngx_http_rewrite_module.html</a></em></p>]]></content>
<categories>
<category>Nginx</category>
</categories>
<tags>
<tag>Nginx</tag>
</tags>
</entry>
<entry>
<title>用Redis实现分布式锁</title>
<link href="/redis-lock/"/>
<url>/redis-lock/</url>
<content type="html"><![CDATA[<p>Redis有一系列的命令,特点是以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists。这系列的命令非常有用,这里讲使用SETNX来实现分布式锁。</p><h2 id="用SETNX实现分布式锁"><a href="#用SETNX实现分布式锁" class="headerlink" title="用SETNX实现分布式锁"></a>用SETNX实现分布式锁</h2><h4 id="1-利用SETNX非常简单地实现分布式锁。"><a href="#1-利用SETNX非常简单地实现分布式锁。" class="headerlink" title="1. 利用SETNX非常简单地实现分布式锁。"></a>1. 利用SETNX非常简单地实现分布式锁。</h4><p>例如:某客户端要获得一个名字foo的锁,客户端使用下面的命令进行获取: </p><figure class="highlight armasm"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br></pre></td><td class="code"><pre><code class="hljs armasm"><span class="hljs-symbol">SETNX</span> lock.foo<br>``` <br>如返回<span class="hljs-number">1</span>,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定 <br>该客户端最后可以通过 ```DEL lock.foo```来释放该锁。 <br>如返回<span class="hljs-number">0</span>,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。<br><span class="hljs-comment">#### 2. 解决死锁</span><br>上面的锁定逻辑有一个问题:如果一个持有锁的客户端失败或崩溃了不能释放锁,该怎么解决?我们可以通过锁的键对应的时间戳来判断这种情况是否发生了,如果当前的时间已经大于lock.foo的值,说明该锁已失效,可以被重新使用。 <br>发生这种情况时,可不能简单的通过DEL来删除锁,然后再SETNX一次,当多个客户端检测到锁超时后都会尝试去释放它,这里就可能出现一个竞态条件,让我们模拟一下这个场景:<br>><span class="hljs-built_in">C0</span>操作超时了,但它还持有着锁,<span class="hljs-built_in">C1</span>和<span class="hljs-built_in">C2</span>读取lock.foo检查时间戳,先后发现超时了。 <br><span class="hljs-symbol">C1</span> 发送DEL lock.foo <br><span class="hljs-symbol">C1</span> 发送SETNX lock.foo 并且成功了。 <br><span class="hljs-symbol">C2</span> 发送DEL lock.foo <br><span class="hljs-symbol">C2</span> 发送SETNX lock.foo 并且成功了。 <br>这样一来,<span class="hljs-built_in">C1</span>,<span class="hljs-built_in">C2</span>都拿到了锁!问题大了! <br><br>幸好这种问题是可以避免D,让我们来看看<span class="hljs-built_in">C3</span>这个客户端是怎样做的:<br><br>><span class="hljs-built_in">C3</span>发送SETNX lock.foo 想要获得锁,由于<span class="hljs-built_in">C0</span>还持有锁,所以Redis返回给<span class="hljs-built_in">C3</span>一个<span class="hljs-number">0</span> <br><span class="hljs-symbol">C3</span>发送<span class="hljs-meta">GET</span> lock.foo 以检查锁是否超时了,如果没超时,则等待或重试。 <br>反之,如果已超时,<span class="hljs-built_in">C3</span>通过下面的操作来尝试获得锁: <br><span class="hljs-symbol">GETSET</span> lock.foo <br>通过GETSET,<span class="hljs-built_in">C3</span>拿到的时间戳如果仍然是超时的,那就说明,<span class="hljs-built_in">C3</span>如愿以偿拿到锁了。 <br>如果在<span class="hljs-built_in">C3</span>之前,有个叫<span class="hljs-built_in">C4</span>的客户端比<span class="hljs-built_in">C3</span>快一步执行了上面的操作,那么<span class="hljs-built_in">C3</span>拿到的时间戳是个未超时的值,这时,<span class="hljs-built_in">C3</span>没有如期获得锁,需要再次等待或重试。留意一下,尽管<span class="hljs-built_in">C3</span>没拿到锁,但它改写了<span class="hljs-built_in">C4</span>设置的锁的超时值,不过这一点非常微小的误差带来的影响可以忽略不计。 <br><br>注意:为了让分布式锁的算法更稳键些,持有锁的客户端在解锁之前应该再检查一次自己的锁是否已经超时,再去做DEL操作,因为可能客户端因为某个耗时的操作而挂起,操作完的时候锁因为超时已经被别人获得,这时就不必解锁了。<br><br><span class="hljs-comment">## 示例伪代码</span><br><br>根据上面的代码,我写了一小段Fake代码来描述使用分布式锁的全过程:<br><br>```python<br><span class="hljs-comment"># get lock</span><br><span class="hljs-symbol">lock</span> = <span class="hljs-number">0</span><br><span class="hljs-symbol">while</span> lock != <span class="hljs-number">1</span>:<br> timestamp = current Unix time + lock timeout + <span class="hljs-number">1</span><br> lock = SETNX lock.foo timestamp<br> <span class="hljs-comment"># 可以使用lua脚本保本原子性</span><br> <span class="hljs-meta">if</span> lock == <span class="hljs-number">1</span> or (now() > (<span class="hljs-meta">GET</span> lock.foo) <span class="hljs-keyword">and</span> now() > (GETSET lock.foo timestamp)):<br> break<span class="hljs-comment">;</span><br><span class="hljs-symbol"> else:</span><br> sleep(<span class="hljs-number">10</span>ms)<br><br><span class="hljs-comment"># do your job</span><br><span class="hljs-symbol">do_job</span>()<br><br><span class="hljs-comment"># release</span><br><span class="hljs-symbol">if</span> now() < <span class="hljs-meta">GET</span> lock.foo:<br> DEL lock.foo<br></code></pre></td></tr></table></figure><p>是的,要想这段逻辑可以重用,使用python的你马上就想到了Decorator,而用Java的你是不是也想到了那谁?AOP + annotation?行,怎样舒服怎样用吧,别重复代码就行。 </p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Redis</tag>
</tags>
</entry>
<entry>
<title>MySQL Explain总结</title>
<link href="/mysql-explain/"/>
<url>/mysql-explain/</url>
<content type="html"><![CDATA[<h2 id="MySQL-查询优化器是如何工作的"><a href="#MySQL-查询优化器是如何工作的" class="headerlink" title="MySQL 查询优化器是如何工作的"></a>MySQL 查询优化器是如何工作的</h2><p>MySQL 查询优化器有几个目标,但是其中最主要的目标是尽可能地使用索引,并且使用最严格的索引来消除尽可能多的数据行。最终目标是提交 SELECT 语句查找数据行,而不是排除数据行。优化器试图排除数据行的原因在于它排除数据行的速度越快,那么找到与条件匹配的数据行也就越快。如果能够首先进行最严格的测试,查询就可以执行地更快。</p><h2 id="Explain-的每个输出列的含义"><a href="#Explain-的每个输出列的含义" class="headerlink" title="Explain 的每个输出列的含义"></a>Explain 的每个输出列的含义</h2><h4 id="id"><a href="#id" class="headerlink" title="id"></a>id</h4><p>MySQL Query Optimizer 选定的执行计划中查询的序列号。表示查询中执行 select 子句或操作表的顺序,id 值越大优先级越高,越先被执行。id 相同,执行顺序由上至下。</p><h4 id="select-type"><a href="#select-type" class="headerlink" title="select_type"></a>select_type</h4><p>查询类型<br><em>SIMPLE</em> : 简单的 select 查询,不使用 union 及子查询<br><em>PRIMARY</em> : 最外层的 select 查询<br><em>UNION</em> : UNION 中的第二个或随后的 select 查询,不 依赖于外部查询的结果集<br><em>DEPENDENT UNION</em> : UNION 中的第二个或随后的 select 查询,依 赖于外部查询的结果集<br><em>SUBQUERY</em> : 子查询中的第一个 select 查询,不依赖于外 部查询的结果集<br><em>DEPENDENT SUBQUERY</em> : 子查询中的第一个 select 查询,依赖于外部 查询的结果集<br><em>DERIVED</em> : 用于 from 子句里有子查询的情况。 MySQL 会 递归执行这些子查询, 把结果放在临时表里。<br><em>UNCACHEABLE SUBQUERY</em> : 结果集不能被缓存的子查询,必须重新为外 层查询的每一行进行评估。<br><em>UNCACHEABLE UNION</em> : UNION 中的第二个或随后的 select 查询,属 于不可缓存的子查询 </p><h4 id="table"><a href="#table" class="headerlink" title="table"></a>table</h4><p>顾名思义,就是查询的表</p><h4 id="type"><a href="#type" class="headerlink" title="type"></a>type</h4><p>输出行所引用的表(重要的项,显示连接使用的类型,按最优到最差的类型排序)<br><em>system</em> : 表仅有一行(=系统表)。这是 const 连接类型的一个特例。<br><em>const</em> : 用于用常数值比较 PRIMARY KEY 时。当 查询的表仅有一行时,使用 System。<br><em>eq_ref</em> : 用于用常数值比较 PRIMARY KEY 时。当 查询的表仅有一行时,使用 System。<br><em>ref</em> : 连接不能基于关键字选择单个行,可能查找到多个符合条件的行。叫做 ref 是因为索引要 跟某个参考值相比较。这个参考值或者是一 个常数,或者是来自一个表里的多表查询的 结果值。<br><em>ref_or_null</em> : 如同 ref, 但是 MySQL 必须在初次查找的结果 里找出 null 条目,然后进行二次查找。<br><em>index_merge</em> : 说明索引合并优化被使用了。<br><em>unique_subquery</em> : 在某些 IN 查询中使用此种类型,而不是常规的。<br><em>index_subquery</em> : 在某些IN查询中使用此种类型,与unique_subquery类似,但是查询的是非唯一 性索<br><em>range</em> : 只检索给定范围的行,使用一个索引来选择 行。key 列显示使用了哪个索引。当使用=、 <>、>、>=、<、<=、IS NULL、<=>、BETWEEN 或者 IN 操作符,用常量比较关键字列时,可以使用 range。<br><em>index</em> : 全表扫描,只是扫描表的时候按照索引次序 进行而不是行。主要优点就是避免了排序, 但是开销仍然非常大。<br><em>all</em> : 最坏的情况,从头到尾全表扫描。<br>MySQL一次查询只能使用一个索引(多个表查询时就是表的数量),所以MySQL在选择索引的时候会按照上面的顺序使用。<br>比如:(每个字段都有索引) </p><figure class="highlight pgsql"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs pgsql"><span class="hljs-keyword">select</span> * <span class="hljs-keyword">from</span> tbl_order <span class="hljs-keyword">where</span> id >= <span class="hljs-number">1000</span> <span class="hljs-keyword">and</span> <span class="hljs-type">name</span> = "zhangsan" <span class="hljs-keyword">limit</span> <span class="hljs-number">0</span>,<span class="hljs-number">10</span> <br></code></pre></td></tr></table></figure><blockquote><p>看似会使用主键的索引,其实不然。”=“是”ref“的类型, ”>=“是”range“类型,优先级最低,所以MySQL会使用name的索引。</p></blockquote><h4 id="possible-keys"><a href="#possible-keys" class="headerlink" title="possible_keys"></a>possible_keys</h4><p>指出 MySQL 能在该表中使用哪些索引有助于 查询。如果为空,说明没有可用的索引。</p><h4 id="key"><a href="#key" class="headerlink" title="key"></a>key</h4><p>MySQL 实际从 possible_key 选择使用的索引。 如果为 NULL,则没有使用索引。很少的情况 下,MYSQL 会选择优化不足的索引。这种情况下,可以在 SELECT 语句中使用 USE INDEX (indexname)来强制使用一个索引或者用 IGNORE INDEX(indexname)来强制 MYSQL 忽略索引</p><h4 id="key-len"><a href="#key-len" class="headerlink" title="key_len"></a>key_len</h4><p>使用的索引的长度。在不损失精确性的情况 下,长度越短越好。</p><h4 id="ref"><a href="#ref" class="headerlink" title="ref"></a>ref</h4><p>显示索引的哪一列被使用了</p><h4 id="rows"><a href="#rows" class="headerlink" title="rows"></a>rows</h4><p>MySQL 认为必须检查的用来返回请求数据的行数</p><h4 id="extra"><a href="#extra" class="headerlink" title="extra"></a>extra</h4><p>出现以下两项意味着 MYSQL 根本不能使用索引,效率会受到重大影响。应尽可能对此进行优化。<br><em>Using filesort</em> : 表示 MySQL 会对结果使用一个外部索引排序,而不是从表里按索引次序读到相关内容。可能在内存或者磁盘上进行排序。MySQL 中无法利用索引完成的排序操作称为“文件排序”<br><em>Using temporary</em> : 表示 MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。</p><p>MySql 中的 explain 语法可以帮助我们改写查询,优化表的结构和索引的设置,从而最大地提高查询效率。当然,在大规模数据量时,索引的建立和维护的代价也是很高的,往往需要较长的时间和较大的空间,如果在不同的列组合上建立索引,空间的开销会更大。因此索引最好设置在需要经常查询的字段中。</p>]]></content>
<categories>
<category>Mysql</category>
</categories>
<tags>
<tag>Mysql</tag>
</tags>
</entry>
<entry>
<title>Tomcat Cannot Allocate Memory</title>
<link href="/tomcat-allocate-memory/"/>
<url>/tomcat-allocate-memory/</url>
<content type="html"><![CDATA[<p>今天测试环境的服务总是挂掉,上面部署了6个服务,也不怎么吃内存的。但是我这个服务总是启动之后过不了多久又挂了。tomcat日志如下:</p><figure class="highlight vim"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><code class="hljs vim">Java HotSpot(TM) <span class="hljs-number">64</span>-Bit Server VM warning: INFO: os::commit_memory(<span class="hljs-number">0</span>x00000007ca580000, <span class="hljs-number">211288064</span>, <span class="hljs-number">0</span>) failed; error=<span class="hljs-string">'Cannot allocate memory'</span> (errno=<span class="hljs-number">12</span>)<br>#<br># There <span class="hljs-keyword">is</span> insufficient memory <span class="hljs-keyword">for</span> the Java Runtime Environment <span class="hljs-keyword">to</span> <span class="hljs-keyword">continue</span>.<br># Native memory allocation (malloc) failed <span class="hljs-keyword">to</span> allocate <span class="hljs-number">211288064</span> bytes <span class="hljs-keyword">for</span> committing reserved memory.<br># An error report <span class="hljs-keyword">file</span> with more information <span class="hljs-keyword">is</span> saved <span class="hljs-keyword">as</span>:<br># /root/hs_err_pid5095.<span class="hljs-built_in">log</span><br></code></pre></td></tr></table></figure><p>解决如下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">test01:~ root$ grep -i commit /proc/meminfo<br>CommitLimit: 20389524 kB<br>Committed_AS: 18541832 kB<br></code></pre></td></tr></table></figure><p>说明:</p><blockquote><p><em>CommitLimit</em>:最大能分配的内存(个人理解仅仅在vm.overcommit_memory=2时候生效),具体的值是SWAP内存大小 + 物理内存 * overcommit_ratio / 100<br><em>Committed_AS</em>:当前已经分配的内存大小<br><em>overcommit_memory</em>:规定决定是否接受超大内存请求的条件。 </p><blockquote><p>这个参数有三个可能的值:<br>0 — 默认设置。个人理解:当应用进程尝试申请内存时,内核会做一个检测。内核将检查是否有足够的可用内存供应用进程使用;如果有足够的可用内存,内存申请允许;否则,内存申请失败,并把错误返回给应用进程。举个例子,比如1G的机器,A进程已经使用了500M,当有另外进程尝试malloc 500M的内存时,内核就会进行check,发现超出剩余可用内存,就会提示失败。<br>1 — 对于内存的申请请求,内核不会做任何check,直到物理内存用完,触发OOM杀用户态进程。同样是上面的例子,1G的机器,A进程500M,B进程尝试malloc 500M,会成功,但是一旦kernel发现内存使用率接近1个G(内核有策略),就触发OOM,杀掉一些用户态的进程(有策略的杀)。<br>2 — 当 请求申请的内存 >= SWAP内存大小 + 物理内存 * N,则拒绝此次内存申请。解释下这个N:N是一个百分比,根据overcommit_ratio/100来确定,比如overcommit_ratio=50,那么N就是50%。注意:只为swap区域大于其物理内存的系统推荐这个设置overcommit_ratio将 overcommit_memory 设定为 2 时,指定所考虑的物理 RAM 比例。默认为 50。 </p></blockquote></blockquote><p>尝试修改内核参数如下:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs shell">test01:~ root$ vim /etc/sysctl.conf<br>vm.overcommit_memory = 1<br></code></pre></td></tr></table></figure><p>运行使之生效:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs shell">test01:~ root$ sysctl -p<br>test01:~ root$ sysctl -n vm.overcommit_memory<br>1<br></code></pre></td></tr></table></figure><p>上面的方法尝试过后还是没有用,内存还是不够。<br>最后找了一个释放内存的命令:</p><figure class="highlight shell"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs shell">test01:~ root$ echo 1 > /proc/sys/vm/drop_caches<br></code></pre></td></tr></table></figure><p>用free -m 查看内存压缩了一个g,待看看是否有效果吧~</p>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>Memory</tag>
</tags>
</entry>
<entry>
<title>JVM 常用垃圾回收机制和垃圾收集器</title>
<link href="/jvm-memory-gc/"/>
<url>/jvm-memory-gc/</url>
<content type="html"><![CDATA[<h2 id="常用垃圾回收机制"><a href="#常用垃圾回收机制" class="headerlink" title="常用垃圾回收机制"></a>常用垃圾回收机制</h2><h4 id="标记-清除收集器"><a href="#标记-清除收集器" class="headerlink" title="标记-清除收集器"></a>标记-清除收集器</h4><p>这种收集器首先遍历对象图并标记可到达的对象,然后扫描堆栈以寻找未标记对象并释放它们的内存。这种收集器一般使用单线程工作并停止其他操作。</p><h4 id="标记-压缩收集器"><a href="#标记-压缩收集器" class="headerlink" title="标记-压缩收集器"></a>标记-压缩收集器</h4><p>有时也叫标记-清除-压缩收集器,与标记-清除收集器有相同的标记阶段。在第二阶段,则把标记对象复制到堆栈的新域中以便压缩堆栈。这种收集器也停止其他操作。</p><h4 id="复制收集器"><a href="#复制收集器" class="headerlink" title="复制收集器"></a>复制收集器</h4><p>这种收集器将堆栈分为两个域,常称为半空间。每次仅使用一半的空间,虚拟机生成的新对象则放在另一半空间中。垃圾回收器运行时,它把可到达对象复制到另一半空间,没有被复制的的对象都是不可达对象,可以被回收。这种方法适用于短生存期的对象,持续复制长生存期的对象由于多次拷贝,导致效率降低。缺点是只有一半的虚拟机空间得到使用。</p><h4 id="增量收集器"><a href="#增量收集器" class="headerlink" title="增量收集器"></a>增量收集器</h4><p>增量收集器把堆栈分为多个域,每次仅从一个域收集垃圾。这会造成较小的应用程序中断。</p><h4 id="分代收集器"><a href="#分代收集器" class="headerlink" title="分代收集器"></a>分代收集器</h4><p>这种收集器把堆栈分为两个或多个域,用以存放不同寿命的对象。虚拟机生成的新对象一般放在其中的某个域中。过一段时间,继续存在的对象将获得使用期并转入更长寿命的域中。分代收集器对不同的域使用不同的算法以优化性能。这样可以减少复制对象的时间。</p><h4 id="并发收集器"><a href="#并发收集器" class="headerlink" title="并发收集器"></a>并发收集器</h4><p>并发收集器与应用程序同时运行。这些收集器在某点上(比如压缩时)一般都不得不停止其他操作以完成特定的任务,但是因为其他应用程序可进行其他的后台操作,所以中断其他处理的实际时间大大降低。</p><h4 id="并行收集器"><a href="#并行收集器" class="headerlink" title="并行收集器"></a>并行收集器</h4><p>并行收集器使用某种传统的算法并使用多线程并行的执行它们的工作。在多CPU机器上使用多线程技术可以显著的提高java应用程序的可扩展性。</p><h4 id="自适应收集器"><a href="#自适应收集器" class="headerlink" title="自适应收集器"></a>自适应收集器</h4><p>根据程序运行状况以及堆的使用状况,自动选一种合适的垃圾回收算法。这样可以不局限与一种垃圾回收算法。</p><h2 id="几种垃圾收集器"><a href="#几种垃圾收集器" class="headerlink" title="几种垃圾收集器"></a>几种垃圾收集器</h2><p>常见的垃圾收集器有:serial收集器、Parallel收集器、Parallel Old 垃圾收集器、CMS(Concurrent Mark-Sweep)收集器、G1收集器.其中Serial收集器为串行收集器,其他均为并行收集器。</p><blockquote><p>串行垃圾回收器(Serial Garbage Collector)<br>并行垃圾回收器(Parallel Garbage Collector)<br>并发标记扫描垃圾回收器(CMS Garbage Collector)<br>G1垃圾回收器(G1 Garbage Collector) </p></blockquote><h4 id="Serial收集器-gt-串行收集器-XX-UseSerialGC"><a href="#Serial收集器-gt-串行收集器-XX-UseSerialGC" class="headerlink" title="Serial收集器->串行收集器 (-XX:+UseSerialGC)"></a>Serial收集器->串行收集器 (-XX:+UseSerialGC)</h4><p>最古老,最稳定,简单而高效,可能会产生较长的停顿。<br>Serial是一个单线程的收集器,它不仅仅只会使用一个CPU或一条线程去完成垃圾收集工作,并且在进行垃圾收集的同时,必须暂停其他所有的工作线程,直到垃圾收集结束。<br>Serial垃圾收集器虽然在收集垃圾过程中需要暂停所有其他的工作线程,但是它简单高效,对于限定单个CPU环境来说,没有线程交互的开销,可以获得最高的单线程垃圾收集效率,因此Serial垃圾收集器依然是java虚拟机运行在Client模式下默认的新生代垃圾收集器。</p><figure> <a href="/assets/img/java-gc-serial.jpg"><img src="/assets/img/java-gc-serial.jpg"></a></figure><h4 id="Serial-Old收集器"><a href="#Serial-Old收集器" class="headerlink" title="Serial Old收集器"></a>Serial Old收集器</h4><p>Serial Old是Serial垃圾收集的老年代版本。它同样是个单线程的收集器,使用标记-整理算法,这个收集器也主要是运行在Client默认的java虚拟机默认的年老代垃圾收集器。</p><h4 id="ParNew收集器-XX-UseParallelGC"><a href="#ParNew收集器-XX-UseParallelGC" class="headerlink" title="ParNew收集器 (-XX:+UseParallelGC)"></a>ParNew收集器 (-XX:+UseParallelGC)</h4><p>ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Serial收集器可用的所有控制参数、收集算法、Stop The World、对象分配规则、回收策略等都与Serial收集器完全一样,在实现上,这两种收集器也共用了相当多的代码。ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器。</p><h4 id="Parallel-Scavenge"><a href="#Parallel-Scavenge" class="headerlink" title="Parallel Scavenge"></a>Parallel Scavenge</h4><p>Parallel Scavenge是一个新生代收集器,使用多线程和复制算法。相比其他收集器,只有这个收集器是针对系统吞吐量进行改进,适用于后台运算并且交互不多的程序。其他收集器则更关注改善收集时的停顿时间,适用于用户交互的程序。</p><h4 id="Parallel-Old-垃圾收集器-XX-UseParallelOldGC"><a href="#Parallel-Old-垃圾收集器-XX-UseParallelOldGC" class="headerlink" title="Parallel Old 垃圾收集器(-XX:+UseParallelOldGC)"></a>Parallel Old 垃圾收集器(-XX:+UseParallelOldGC)</h4><p>Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。<br>在注重吞吐量以及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。</p><h4 id="CMS-收集器"><a href="#CMS-收集器" class="headerlink" title="CMS 收集器"></a>CMS 收集器</h4><p>CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。<br>CMS收集器是基于“标记—清除”算法实现的。整个过程需要下面四个步骤。 </p><ul><li>初始标记(CMS initial mark)<br>初始标记仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。</li><li>并发标记(CMS concurrent mark)<br>并发标记阶段就是进行GC Roots Tracing的过程。</li><li>重新标记(CMS remark)<br>重新标记阶段是为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短,仍然需要“Stop The World”。</li><li>并发清除(CMS concurrent sweep)<br>并发清除阶段会清除对象。<br>优点: 并发收集、低停顿。<br>缺点: CMS收集器对CPU资源非常敏感,以为在并发阶段占用一部分线程(CPU资源),导致应用程序变慢,总吞吐量变低。CMS默认启动的回收线程数是(CPU数量+3)/4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不少于25%的CPU资源,并且随着CPU数量的增加而下降。<br>CMS收集器无法处理浮动垃圾,可能出现“Concurrent Mode Failure”失败而导致另一次Full GC的产生。也是由于在垃圾收集阶段用户线程还需要运行,那也就还需要预留有足够的内存空间给用户线程使用,因此CMS收集器不能像其他收集器那样等到老年代几乎完全被填满了再进行收集,需要预留一部分空间提供并发收集时的程序运作使用。要是CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就很长了。<br>CMS是一款基于“标记—清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,往往会出现老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。</li></ul><blockquote><p><em>浮动垃圾</em>: 由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉,本地无法清理的垃圾则称为浮动垃圾</p></blockquote><h4 id="G1-收集器"><a href="#G1-收集器" class="headerlink" title="G1 收集器"></a>G1 收集器</h4><p>G1收集器是当前收集器技术发展最前沿的成果,一款面向服务端应用的垃圾收集器。基于标记-整理算法,也就是说不会产生内存碎片,可以精确控制停顿。基本不牺牲吞吐量的前提下完成低停顿的内存回收。这是由于它将新生代、老年代划分为多个区域,并维护一个每个区域收集的优先列表,保证了在有限的时间内可以获得最高的收集效率。收集的范围是整个JAVA堆。而不是在区分新生代,老年代。 </p><p><em>执行过程:</em> </p><ul><li>初始标记(Initial Marking)<br>初始标记阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象,这阶段需要停顿线程,但耗时很短。</li><li>并发标记(Concurrent Marking)<br>并发标记阶段是从GC Root开始对堆中对象进行可达性分析,找出存活的对象,这阶段耗时较长,但可与用户程序并发执行。</li><li>最终标记(Final Marking)<br>最终标记阶段是为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。</li><li>筛选回收(Live Data Counting and Evacuation)<br>筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。</li></ul><p><em>特点:</em></p><ul><li>并行与并发<br>可使用多个CPU来缩短Stop-The-World停顿的时间,部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。</li><li>分代收集<br>与其他收集器一样,分代概念在G1中依然得以保留。虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。</li><li>空间整合<br>与CMS的“标记—清理”算法不同,G1从整体来看是基于“标记—整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片,收集后能提供规整的可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。</li><li>可预测的停顿<br>这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒。<br>在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样。使用G1收集器时,Java堆的内存布局就与其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。<br>G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。<br>附上tomcat经过压测和调试的设置:</li></ul><blockquote><p>JAVA_OPTS=”-server -Xms6000M -Xmx6000M -Xmn2g -Xss512k -XX:+AggressiveOpts -XX:+UseBiasedLocking -XX:PermSize=128M -XX:MaxPermSize=256M -XX:+DisableExplicitGC -XX:MaxTenuringThreshold=31 -XX:+UseConcMarkSweepGC -XX:+UseParNewGC -XX:+CMSParallelRemarkEnabled -XX:+UseCMSCompactAtFullCollection -XX:LargePageSizeInBytes=128m -XX:+UseFastAccessorMethods -XX:+UseCMSInitiatingOccupancyOnly -Djava.awt.headless=true”</p></blockquote>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>GC</tag>
</tags>
</entry>
<entry>
<title>JVM GC日志收集</title>
<link href="/jvm-gc-log/"/>
<url>/jvm-gc-log/</url>
<content type="html"><![CDATA[<p>GC日志是一个很重要的工具,它准确记录了每一次的GC的执行时间和执行结果,通过分析GC日志可以优化堆设置和GC设置,或者改进应用程序的对象分配模式。</p><h2 id="XX-PrintGC"><a href="#XX-PrintGC" class="headerlink" title="-XX:+PrintGC"></a>-XX:+PrintGC</h2><p>参数-XX:+PrintGC(或者-verbose:gc)开启了简单GC日志模式,为每一次新生代(young generation)的GC和每一次的Full GC打印一行信息。下面举例说明:</p><figure class="highlight mathematica"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mathematica"><span class="hljs-punctuation">[</span><span class="hljs-variable">GC</span> <span class="hljs-number">246656</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">243120</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">376320</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-operator">,</span> <span class="hljs-number">0.0929090</span> <span class="hljs-variable">secs</span><span class="hljs-punctuation">]</span><br><span class="hljs-punctuation">[</span><span class="hljs-built_in">Full</span> <span class="hljs-variable">GC</span> <span class="hljs-number">243120</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">241951</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">629760</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-operator">,</span> <span class="hljs-number">1.5589690</span> <span class="hljs-variable">secs</span><span class="hljs-punctuation">]</span><br></code></pre></td></tr></table></figure><p>每行开始首先是GC的类型(可以是“GC”或者“Full GC”),然后是在GC之前和GC之后已使用的堆空间,再然后是当前的堆容量,最后是GC持续的时间(以秒计)。<br>第一行的意思就是GC将已使用的堆空间从246656K减少到243120K,当前的堆容量(译者注:GC发生时)是376320K,GC持续的时间是0.0929090秒。<br>简单模式的GC日志格式是与GC算法无关的,日志也没有提供太多的信息。在上面的例子中,我们甚至无法从日志中判断是否GC将一些对象从young generation移到了old generation。所以详细模式的GC日志更有用一些。</p><h2 id="XX-PrintGCDetails"><a href="#XX-PrintGCDetails" class="headerlink" title="-XX:PrintGCDetails"></a>-XX:PrintGCDetails</h2><p>如果不是使用-XX:+PrintGC,而是-XX:PrintGCDetails,就开启了详细GC日志模式。在这种模式下,日志格式和所使用的GC算法有关。我们首先看一下使用Throughput垃圾收集器在young generation中生成的日志。为了便于阅读这里将一行日志分为多行并使用缩进。</p><figure class="highlight mathematica"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs mathematica"><span class="hljs-punctuation">[</span><span class="hljs-variable">GC</span> <span class="hljs-punctuation">[</span><span class="hljs-variable">PSYoungGen</span><span class="hljs-operator">:</span> <span class="hljs-number">142816</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">10752</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">142848</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-punctuation">]</span> <span class="hljs-number">246648</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">243136</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">375296</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-operator">,</span> <span class="hljs-number">0.0935090</span><span class="hljs-variable">secs</span><span class="hljs-punctuation">]</span><br><span class="hljs-punctuation">[</span><span class="hljs-built_in">Times</span><span class="hljs-operator">:</span> <span class="hljs-variable">user</span><span class="hljs-operator">=</span><span class="hljs-number">0.55</span> <span class="hljs-variable">sys</span><span class="hljs-operator">=</span><span class="hljs-number">0.10</span><span class="hljs-operator">,</span> <span class="hljs-variable">real</span><span class="hljs-operator">=</span><span class="hljs-number">0.09</span> <span class="hljs-variable">secs</span><span class="hljs-punctuation">]</span><br></code></pre></td></tr></table></figure><p>我们可以很容易发现:这是一次在young generation中的GC,它将已使用的堆空间从246648K减少到了243136K,用时0.0935090秒。此外我们还可以得到更多的信息:所使用的垃圾收集器(即PSYoungGen)、young generation的大小和使用情况(在这个例子中“PSYoungGen”垃圾收集器将young generation所使用的堆空间从142816K减少到10752K)。<br>既然我们已经知道了young generation的大小,所以很容易判定发生了GC,因为young generation无法分配更多的对象空间:已经使用了142848K中的142816K。我们可以进一步得出结论,多数从young generation移除的对象仍然在堆空间中,只是被移到了old generation:通过对比绿色的和蓝色的部分可以发现即使young generation几乎被完全清空(从142816K减少到10752K),但是所占用的堆空间仍然基本相同(从246648K到243136K)。<br>详细日志的“Times”部分包含了GC所使用的CPU时间信息,分别为操作系统的用户空间和系统空间所使用的时间。同时,它显示了GC运行的“真实”时间(0.09秒是0.0929090秒的近似值)。如果CPU时间(译者注:0.55秒+0.10秒)明显多于”真实“时间(译者注:0.09秒),我们可以得出结论:GC使用了多线程运行。这样的话CPU时间就是所有GC线程所花费的CPU时间的总和。实际上我们的例子中的垃圾收集器使用了8个线程。 </p><p>接下来看一下Full GC的输出日志</p><figure class="highlight mathematica"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs mathematica"><span class="hljs-punctuation">[</span><span class="hljs-built_in">Full</span> <span class="hljs-variable">GC</span><br> <span class="hljs-punctuation">[</span><span class="hljs-variable">PSYoungGen</span><span class="hljs-operator">:</span> <span class="hljs-number">10752</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">9707</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">142848</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-punctuation">]</span><br> <span class="hljs-punctuation">[</span><span class="hljs-variable">ParOldGen</span><span class="hljs-operator">:</span> <span class="hljs-number">232384</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">232244</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">485888</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-punctuation">]</span> <span class="hljs-number">243136</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">241951</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">628736</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><br> <span class="hljs-punctuation">[</span><span class="hljs-variable">PSPermGen</span><span class="hljs-operator">:</span> <span class="hljs-number">3162</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">3161</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">21504</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-punctuation">]</span><span class="hljs-operator">,</span> <span class="hljs-number">1.5265450</span> <span class="hljs-variable">secs</span><br><span class="hljs-punctuation">]</span><br></code></pre></td></tr></table></figure><p>除了关于young generation的详细信息,日志也提供了old generation和permanent generation的详细信息。对于这三个generations,一样也可以看到所使用的垃圾收集器、堆空间的大小、GC前后的堆使用情况。需要注意的是显示堆空间的大小等于young generation和old generation各自堆空间的和。以上面为例,堆空间总共占用了241951K,其中9707K在young generation,232244K在old generation。Full GC持续了大约1.53秒,用户空间的CPU执行时间为10.96秒,说明GC使用了多线程(和之前一样8个线程)。<br>对不同generation详细的日志可以让我们分析GC的原因,如果某个generation的日志显示在GC之前,堆空间几乎被占满,那么很有可能就是这个generation触发了GC。但是在上面的例子中,三个generation中的任何一个都不是这样的,在这种情况下是什么原因触发了GC呢。对于Throughput垃圾收集器,在某一个generation被过度使用之前,GC ergonomics(参考本系列第6节)决定要启动GC。<br>Full GC也可以通过显式的请求而触发,可以是通过应用程序,或者是一个外部的JVM接口。这样触发的GC可以很容易在日志里分辨出来,因为输出的日志是以“Full GC(System)”开头的,而不是“Full GC”。<br>对于Serial垃圾收集器,详细的GC日志和Throughput垃圾收集器是非常相似的。唯一的区别是不同的generation日志可能使用了不同的GC算法(例如:old generation的日志可能以Tenured开头,而不是ParOldGen)。使用垃圾收集器作为一行日志的开头可以方便我们从日志就判断出JVM的GC设置。<br>对于CMS垃圾收集器,young generation的详细日志也和Throughput垃圾收集器非常相似,但是old generation的日志却不是这样。对于CMS垃圾收集器,在old generation中的GC是在不同的时间片内与应用程序同时运行的。GC日志自然也和Full GC的日志不同。而且在不同时间片的日志夹杂着在此期间young generation的GC日志。但是了解了上面介绍的GC日志的基本元素,也不难理解在不同时间片内的日志。只是在解释GC运行时间时要特别注意,由于大多数时间片内的GC都是和应用程序同时运行的,所以和那种独占式的GC相比,GC的持续时间更长一些并不说明一定有问题。<br>正如我们在第7节中所了解的,即使CMS垃圾收集器没有完成一个CMS周期,Full GC也可能会发生。如果发生了GC,在日志中会包含触发Full GC的原因,例如众所周知的”concurrent mode failure“。<br>为了避免过于冗长,我这里就不详细说明CMS垃圾收集器的日志了。另外,CMS垃圾收集器的作者做了详细的说明(在这里),强烈建议阅读。 </p><h2 id="XX-PrintGCTimeStamps和-XX-PrintGCDateStamps"><a href="#XX-PrintGCTimeStamps和-XX-PrintGCDateStamps" class="headerlink" title="-XX:+PrintGCTimeStamps和-XX:+PrintGCDateStamps"></a>-XX:+PrintGCTimeStamps和-XX:+PrintGCDateStamps</h2><p>使用-XX:+PrintGCTimeStamps可以将时间和日期也加到GC日志中。表示自JVM启动至今的时间戳会被添加到每一行中。例子如下:</p><figure class="highlight mathematica"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs mathematica"><span class="hljs-number">0.185</span><span class="hljs-operator">:</span> <span class="hljs-punctuation">[</span><span class="hljs-variable">GC</span> <span class="hljs-number">66048</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">53077</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">251392</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-operator">,</span> <span class="hljs-number">0.0977580</span> <span class="hljs-variable">secs</span><span class="hljs-punctuation">]</span><br><span class="hljs-number">0.323</span><span class="hljs-operator">:</span> <span class="hljs-punctuation">[</span><span class="hljs-variable">GC</span> <span class="hljs-number">119125</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">114661</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">317440</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-operator">,</span> <span class="hljs-number">0.1448850</span> <span class="hljs-variable">secs</span><span class="hljs-punctuation">]</span><br><span class="hljs-number">0.603</span><span class="hljs-operator">:</span> <span class="hljs-punctuation">[</span><span class="hljs-variable">GC</span> <span class="hljs-number">246757</span><span class="hljs-built_in">K</span><span class="hljs-operator">-></span><span class="hljs-number">243133</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">(</span><span class="hljs-number">375296</span><span class="hljs-built_in">K</span><span class="hljs-punctuation">)</span><span class="hljs-operator">,</span> <span class="hljs-number">0.2860800</span> <span class="hljs-variable">secs</span><span class="hljs-punctuation">]</span><br></code></pre></td></tr></table></figure><p>如果指定了-XX:+PrintGCDateStamps,每一行就添加上了绝对的日期和时间。 </p><figure class="highlight apache"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><code class="hljs apache"><span class="hljs-attribute">2014</span>-<span class="hljs-number">01</span>-<span class="hljs-number">03</span>T12:<span class="hljs-number">08</span>:<span class="hljs-number">38</span>.<span class="hljs-number">102</span>-<span class="hljs-number">0100</span>:<span class="hljs-meta"> [GC 66048K->53077K(251392K), 0.0959470 secs]</span><br><span class="hljs-attribute">2014</span>-<span class="hljs-number">01</span>-<span class="hljs-number">03</span>T12:<span class="hljs-number">08</span>:<span class="hljs-number">38</span>.<span class="hljs-number">239</span>-<span class="hljs-number">0100</span>:<span class="hljs-meta"> [GC 119125K->114661K(317440K), 0.1421720 secs]</span><br><span class="hljs-attribute">2014</span>-<span class="hljs-number">01</span>-<span class="hljs-number">03</span>T12:<span class="hljs-number">08</span>:<span class="hljs-number">38</span>.<span class="hljs-number">513</span>-<span class="hljs-number">0100</span>:<span class="hljs-meta"> [GC 246757K->243133K(375296K), 0.2761000 secs]</span><br></code></pre></td></tr></table></figure><p>如果需要也可以同时使用两个参数。推荐同时使用这两个参数,因为这样在关联不同来源的GC日志时很有帮助。 </p><h2 id="Xloggc"><a href="#Xloggc" class="headerlink" title="-Xloggc"></a>-Xloggc</h2><p>缺省的GC日志时输出到终端的,使用-Xloggc:也可以输出到指定的文件。需要注意这个参数隐式的设置了参数-XX:+PrintGC和-XX:+PrintGCTimeStamps,但为了以防在新版本的JVM中有任何变化,我仍建议显示的设置这些参数。 </p><h2 id="可管理的JVM参数"><a href="#可管理的JVM参数" class="headerlink" title="可管理的JVM参数"></a>可管理的JVM参数</h2><p>一个常常被讨论的问题是在生产环境中GC日志是否应该开启。因为它所产生的开销通常都非常有限,因此我的答案是需要开启。但并不一定在启动JVM时就必须指定GC日志参数。<br>针对高延迟问题调优HotSpot VM时,下面两个命令行选项特别有用,通过它们可以获得应用程序由于执行VM安全操作而阻塞的时间以及两个安全点操作之间应用程序运行的时间。 </p><figure class="highlight ruby"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><code class="hljs ruby">-<span class="hljs-variable constant_">XX</span><span class="hljs-symbol">:+PrintGCApplicationStoppedTime</span><br>-<span class="hljs-variable constant_">XX</span><span class="hljs-symbol">:+PrintGCApplicationConcurrentTime</span><br></code></pre></td></tr></table></figure><p>何谓安全操作:安全操作使JVM进入到一种状态:所有的java应用线程都被阻塞、执行本地代码的线程都被禁止返回VM执行Java代码。安全操作常用于虚拟机需要进行内部操作时,此时所有的Java线程都被显式地置于阻塞状态且不能修改Java堆的情况。<br>HotSpot JVM有一类特别的参数叫做可管理的参数。对于这些参数,可以在运行时修改他们的值。我们这里所讨论的所有参数以及以“PrintGC”开头的参数都是可管理的参数。这样在任何时候我们都可以开启或是关闭GC日志。比如我们可以使用JDK自带的jinfo工具来设置这些参数,或者是通过JMX客户端调用HotSpotDiagnostic MXBean的setVMOption方法来设置这些参数。<br>附上tomcat输出gc日志的配置:</p><figure class="highlight routeros"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><code class="hljs routeros"><span class="hljs-built_in">export</span> <span class="hljs-attribute">CATALINA_OPTS</span>=<span class="hljs-string">"<span class="hljs-variable">$CATALINA_OPTS</span> -XX:+PrintGCDetails"</span><br><span class="hljs-built_in">export</span> <span class="hljs-attribute">CATALINA_OPTS</span>=<span class="hljs-string">"<span class="hljs-variable">$CATALINA_OPTS</span> -XX:+PrintGCTimeStamps"</span><br><span class="hljs-built_in">export</span> <span class="hljs-attribute">CATALINA_OPTS</span>=<span class="hljs-string">"<span class="hljs-variable">$CATALINA_OPTS</span> -XX:+PrintGCDateStamps"</span><br><span class="hljs-built_in">export</span> <span class="hljs-attribute">CATALINA_OPTS</span>=<span class="hljs-string">"<span class="hljs-variable">$CATALINA_OPTS</span> -Xloggc:/data/logs/ordercenter_cms/tomcat.gc.log"</span><br><span class="hljs-built_in">export</span> <span class="hljs-attribute">JAVA_OPTS</span>=<span class="hljs-string">"-server -Xms1024m -Xmx1536m"</span><br></code></pre></td></tr></table></figure>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>GC</tag>
</tags>
</entry>
<entry>
<title>JVM GC 策略&内存申请、对象衰老</title>
<link href="/jvm-gc-tactics/"/>
<url>/jvm-gc-tactics/</url>
<content type="html"><![CDATA[<p>JVM里的GC(Garbage Collection)的算法有很多种,如标记清除收集器,压缩收集器,分代收集器等等,详见HotSpot VM GC 的种类。 </p><blockquote><p>现在比较常用的是分代收集(generational collection,也是SUN VM使用的,J2SE1.2之后引入),即将内存分为几个区域,将不同生命周期的对象放在不同区域里:young generation,tenured generation和permanet generation。绝大部分的objec被分配在young generation(生命周期短),并且大部分的object在这里die。当young generation满了之后,将引发minor collection(YGC)。在minor collection后存活的object会被移动到tenured generation(生命周期比较长)。最后,tenured generation满之后触发major collection。major collection(Full gc)会触发整个heap的回收,包括回收young generation。permanet generation区域比较稳定,主要存放classloader信息。<br>young generation有eden、2个survivor 区域组成。其中一个survivor区域一直是空的,是eden区域和另一个survivor区域在下一次copy collection后活着的objecy的目的地。object在survivo区域被复制直到转移到tenured区。<br>我们要尽量减少 Full gc 的次数(tenured generation 一般比较大,收集的时间较长,频繁的Full gc会导致应用的性能收到严重的影响)。 </p></blockquote><h2 id="堆内存GC"><a href="#堆内存GC" class="headerlink" title="堆内存GC"></a>堆内存GC</h2><p>JVM(采用分代回收的策略),用较高的频率对年轻的对象(young generation)进行YGC,而对老对象(tenured generation)较少(tenured generation 满了后才进行)进行Full GC。这样就不需要每次GC都将内存中所有对象都检查一遍。</p><h2 id="非堆内存不GC"><a href="#非堆内存不GC" class="headerlink" title="非堆内存不GC"></a>非堆内存不GC</h2><p>GC不会在主程序运行期对PermGen Space进行清理,所以如果你的应用中有很多CLASS(特别是动态生成类,当然permgen space存放的内容不仅限于类)的话,就很可能出现PermGen Space错误。</p><h3 id="内存申请过程"><a href="#内存申请过程" class="headerlink" title="内存申请过程"></a>内存申请过程</h3><ol><li>JVM会试图为相关Java对象在Eden中初始化一块内存区域;</li><li>当Eden空间足够时,内存申请结束。否则到下一步;</li><li>JVM试图释放在Eden中所有不活跃的对象(minor collection),释放后若Eden空间仍然不足以放入新对象,则试图将部分Eden中活跃对象放入Survivor区;</li><li>Survivor区被用来作为Eden及old的中间交换区域,当OLD区空间足够时,Survivor区的对象会被移到Old区,否则会被保留在Survivor区;</li><li>当old区空间不够时,JVM会在old区进行major collection;</li><li>完全垃圾收集后,若Survivor及old区仍然无法存放从Eden复制过来的部分对象,导致JVM无法在Eden区为新对象创建内存区域,则出现”Out of memory错误”;</li></ol><h3 id="对象衰老过程"><a href="#对象衰老过程" class="headerlink" title="对象衰老过程"></a>对象衰老过程</h3><ol><li><p>新创建的对象的内存都分配自eden。Minor collection的过程就是将eden和在用survivor space中的活对象copy到空闲survivor space中。对象在young generation里经历了一定次数(可以通过参数配置)的minor collection后,就会被移到old generation中,称为tenuring。</p></li><li><p>GC触发条件</p><table border="1" cellspacing="0"><tbody><tr><td valign="top"><strong>GC类型</strong></td><td valign="top"><strong>触发条件</strong></td><td valign="top"><strong>触发时发生了什么</strong></td><td valign="top"><strong>注意</strong></td><td valign="top"><strong>查看方式</strong></td></tr><tr><td valign="top">YGC</td><td valign="top">eden空间不足</td><td valign="top">清空Eden+from survivor中所有no ref的对象占用的内存<br>将eden+from sur中所有存活的对象copy到to sur中<br>一些对象将晋升到old中:<br>to sur放不下的<br>存活次数超过turning threshold中的<br>重新计算tenuring threshold(serial parallel GC会触发此项)<p></p><p>重新调整Eden 和from的大小(parallel GC会触发此项)</p></td><td valign="top">全过程暂停应用<br>是否为多线程处理由具体的GC决定</td><td valign="top">jstat –gcutil<br>gc log</td></tr><tr><td valign="top">FGC</td><td valign="top">old空间不足<br>perm空间不足<br>显示调用System.GC, RMI等的定时触发<br>YGC时的悲观策略<br>dump live的内存信息时(jmap –dump:live)</td><td valign="top">清空heap中no ref的对象<br>permgen中已经被卸载的classloader中加载的class信息<p></p><p>如配置了CollectGenOFirst,则先触发YGC(针对serial GC)<br>如配置了ScavengeBeforeFullGC,则先触发YGC(针对serial GC)</p></td><td valign="top" width="155">全过程暂停应用<br>是否为多线程处理由具体的GC决定<p></p><p>是否压缩需要看配置的具体GC</p></td><td valign="top">jstat –gcutil<br>gc log</td></tr></tbody></table>permanent generation空间不足会引发Full GC,仍然不够会引发PermGen Space错误。</li></ol>]]></content>
<categories>
<category>Java</category>
</categories>
<tags>
<tag>GC</tag>
</tags>
</entry>
<entry>
<title>CentOs 设置静态IP方法</title>
<link href="/centos-static-ip/"/>
<url>/centos-static-ip/</url>
<content type="html"><![CDATA[<p>在做项目时由于公司局域网采用自动获取IP的方式,导到每次服务器重启主机IP都会变化。为了解决这个问题,我参考了</p><h2 id="修改网卡配置"><a href="#修改网卡配置" class="headerlink" title="修改网卡配置"></a>修改网卡配置</h2><p>编辑:vi /etc/sysconfig/network-scripts/ifcfg-eth0</p><figure class="highlight ini"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><code class="hljs ini"><span class="hljs-attr">DEVICE</span>=eth0 <br><span class="hljs-attr">ONBOOT</span>=<span class="hljs-literal">yes</span> <br><span class="hljs-attr">HWADDR</span>=<span class="hljs-number">22</span>:<span class="hljs-number">78</span>:e3:<span class="hljs-number">3</span>e:<span class="hljs-number">11</span><br><span class="hljs-attr">BOOTPROTO</span>=static <br><span class="hljs-attr">IPADDR</span>=<span class="hljs-number">192.168</span>.<span class="hljs-number">56.103</span> <br><span class="hljs-attr">NETMASK</span>=<span class="hljs-number">255.255</span>.<span class="hljs-number">255.0</span> <br><span class="hljs-attr">BROADCAST</span>=<span class="hljs-number">192.168</span>.<span class="hljs-number">56.255</span><br><span class="hljs-attr">GATEWAY</span>=<span class="hljs-number">192.168</span>.<span class="hljs-number">56.0</span><br></code></pre></td></tr></table></figure><p>DEVICE=eth0 #描述网卡对应的设备别名,例如ifcfg-eth0的文件中它为eth0<br>BOOTPROTO=static #设置网卡获得ip地址的方式,可能的选项为static,dhcp或bootp,分别对应静态指定的 ip地址,通过dhcp协议获得的ip地址,通过bootp协议获得的ip地址<br>BROADCAST=192.168.56.255 #对应的子网广播地址<br>HWADDR=00:07:E9:05:E8:B4 #对应的网卡物理地址<br>IPADDR=192.168.56.103 #如果设置网卡获得 ip地址的方式为静态指定,此字段就指定了网卡对应的ip地址<br>NETMASK=255.255.255.0 #网卡对应的网络掩码 </p><h2 id="修改网关配置"><a href="#修改网关配置" class="headerlink" title="修改网关配置"></a>修改网关配置</h2><p>编辑:vi /etc/sysconfig/network 增加 GATEWAY 配置<br>NETWORKING=yes(表示系统是否使用网络,一般设置为yes。如果设为no,则不能使用网络,而且很多系统服务程序将无法启动)<br>HOSTNAME=centos(设置本机的主机名,这里设置的主机名要和/etc/hosts中设置的主机名对应)<br>GATEWAY=192.168.56.103(设置本机连接的网关的IP地址。)<br>我在修改这里打开编辑时前三项已经默认有了所以只增加了GATEWAY<br>重启网络服务<br>执行命令: </p><figure class="highlight ebnf"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><code class="hljs ebnf"><span class="hljs-attribute">service network restart</span><br></code></pre></td></tr></table></figure><h2 id="遇到的问题"><a href="#遇到的问题" class="headerlink" title="遇到的问题"></a>遇到的问题</h2><blockquote><p>错误:弹出界面eth0: 错误:没有找到合适的设备:没有找到可用于链接System eth0 的<br>清除/etc/udev/rules.d/70-persistent-net.rules 里的内容,重启机器,会生成新的内容<br>文件下记录着网卡对应mac地址信息,将 mac 地址拷贝到ifcfg-eth0的HWADDR,重新启动网卡,成功。</p></blockquote><blockquote><p>错误:设备 eth0 似乎不存在, 初始化操作将被延迟<br>与上面的问题一样,重新操作一遍即可</p></blockquote>]]></content>
<categories>
<category>Linux</category>
</categories>
<tags>
<tag>Linux</tag>
</tags>
</entry>
<entry>
<title>MySQL 索引提高优化Order By</title>
<link href="/mysql-order-optimize/"/>
<url>/mysql-order-optimize/</url>
<content type="html"><![CDATA[<p>在数据库中我们一般都会对一些字段进行索引操作,这样可以提升数据的查询速度,同时提高数据库的性能像order by ,group by前都需要索引哦。 </p><blockquote><p>首先我们要注意一下 </p><ol><li>mysql一次查询只能使用一个索引。如果要对多个字段使用索引,建立复合索引。 </li><li>在ORDER BY操作中,MySQL只有在排序条件不是一个查询条件表达式的情况下才使用索引。</li></ol></blockquote><h2 id="关于索引一些说法"><a href="#关于索引一些说法" class="headerlink" title="关于索引一些说法"></a>关于索引一些说法</h2><p>MySQL索引通常是被用于提高WHERE条件的数据行匹配或者执行联结操作时匹配其它表的数据行的搜索速度。<br>MySQL也能利用索引来快速地执行ORDER BY和GROUP BY语句的排序和分组操作。 </p><h3 id="通过索引优化来实现MySQL的ORDER-BY语句优化"><a href="#通过索引优化来实现MySQL的ORDER-BY语句优化" class="headerlink" title="通过索引优化来实现MySQL的ORDER BY语句优化"></a>通过索引优化来实现MySQL的ORDER BY语句优化</h3><ul><li><p>ORDER BY的索引优化<br>如果一个SQL语句形如:SELECT [column1],[column2],…. FROM [TABLE] ORDER BY [sort];<br>在[sort]这个栏位上建立索引就可以实现利用索引进行order by 优化。 </p></li><li><p>WHERE + ORDER BY的索引优化<br>例如:SELECT [column1],[column2],…. FROM [TABLE] WHERE [columnX] = [value] ORDER BY [sort];建立一个联合索引(columnX,sort)来实现order by 优化。<br>注意:如果columnX对应多个值,如下面语句就无法利用索引来实现order by的优化<br>SELECT [column1],[column2],…. FROM [TABLE] WHERE [columnX] IN ([value1],[value2],…) ORDER BY[sort]; </p></li><li><p>WHERE+ 多个字段ORDER BY<br>SELECT * FROM [table] WHERE uid=1 ORDER x,y LIMIT 0,10;<br>建立索引(uid,x,y)实现order by的优化,比建立(x,y,uid)索引效果要好得多。 </p><h3 id="MySQL-Order-By不能使用索引来优化排序的情况"><a href="#MySQL-Order-By不能使用索引来优化排序的情况" class="headerlink" title="MySQL Order By不能使用索引来优化排序的情况"></a>MySQL Order By不能使用索引来优化排序的情况</h3></li><li><p>对不同的索引键做 ORDER BY :(key1,key2分别建立索引)<br>SELECT * FROM t1 ORDER BY key1, key2;</p></li><li><p>在非连续的索引键部分上做 ORDER BY:(key_part1,key_part2建立联合索引;key2建立索引)<br>SELECT * FROM t1 WHERE key2=constant ORDER BY key_part2;</p></li><li><p>同时使用了 ASC 和 DESC:(key_part1,key_part2建立联合索引)<br>SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC;</p></li><li><p>用于搜索记录的索引键和做 ORDER BY 的不是同一个:(key1,key2分别建立索引)<br>SELECT * FROM t1 WHERE key2=constant ORDER BY key1;</p></li><li><p>如果在WHERE和ORDER BY的栏位上应用表达式(函数)时,则无法利用索引来实现order by的优化<br>SELECT * FROM t1 ORDER BY YEAR(logindate) LIMIT 0,10;</p></li></ul><h3 id="MySQL支持很多数据类型,选择合适的数据类型存储数据对性能有很大的影响。"><a href="#MySQL支持很多数据类型,选择合适的数据类型存储数据对性能有很大的影响。" class="headerlink" title="MySQL支持很多数据类型,选择合适的数据类型存储数据对性能有很大的影响。"></a>MySQL支持很多数据类型,选择合适的数据类型存储数据对性能有很大的影响。</h3><p>通常来说,可以遵循以下一些指导原则:</p><ul><li>越小的数据类型通常更好:越小的数据类型通常在磁盘、内存和CPU缓存中都需要更少的空间,处理起来更快。</li><li>简单的数据类型更好:整型数据比起字符,处理开销更小,因为字符串的比较更复杂。在MySQL中,应该用内置的日期和时间数据类型,而不是用字符串来存储时间;以及用整型数据类型存储IP地址。</li><li>尽量避免NULL:应该指定列为NOT NULL,除非你想存储NULL。在MySQL中,含有空值的列很难进行查询优化,因为它们使得索引、索引的统计信息以及比较运算更加复杂。你应该用0、一个特殊的值或者一个空串代替空值。</li></ul>]]></content>
<categories>
<category>Mysql</category>
</categories>
<tags>
<tag>Mysql</tag>
</tags>
</entry>
</search>