-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsearch.xml
997 lines (472 loc) · 573 KB
/
search.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
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
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
<?xml version="1.0" encoding="utf-8"?>
<search>
<entry>
<title>缓存重建问题</title>
<link href="/2019/10/07/%E7%BC%93%E5%AD%98%E9%87%8D%E5%BB%BA%E9%97%AE%E9%A2%98/"/>
<url>/2019/10/07/%E7%BC%93%E5%AD%98%E9%87%8D%E5%BB%BA%E9%97%AE%E9%A2%98/</url>
<content type="html"><![CDATA[<p>我们还遇到一个问题,就是说,如果缓存服务在本地的ehcache中都读取不到数据,那就坑爹了</p><p>这个时候就意味着,需要重新到源头的服务中去拉去数据,拉取到数据之后,赶紧先给nginx的请求返回,同时将数据写入ehcache和redis中</p><p>分布式重建缓存的并发冲突问题</p><p>重建缓存:比如我们这里,数据在所有的缓存中都不存在了(LRU算法弄掉了),就需要重新查询数据写入缓存,重建缓存</p><p>分布式的重建缓存,在不同的机器上,不同的服务实例中,去做上面的事情,就会出现多个机器分布式重建去读取相同的数据,然后写入缓存中</p><p>分布式重建缓存的并发冲突问题。。。。。。</p><p>1、流量均匀分布到所有缓存服务实例上</p><p>应用层nginx,是将请求流量均匀地打到各个缓存服务实例中的,可能咱们的eshop-cache那个服务,可能会部署多实例在不同的机器上</p><p>2、应用层nginx的hash,固定商品id,走固定的缓存服务实例</p><p>分发层的nginx的lua脚本,是怎么写的,怎么玩儿的,搞一堆应用层nginx的地址列表,对每个商品id做一个hash,然后对应用nginx数量取模</p><p>将每个商品的请求固定分发到同一个应用层nginx上面去</p><p>在应用层nginx里,发现自己本地lua shared dict缓存中没有数据的时候,就采取一样的方式,对product id取模,然后将请求固定分发到同一个缓存服务实例中去</p><p>这样的话,就不会出现说多个缓存服务实例分布式的去更新那个缓存了</p><p>留个作业,大家去做吧,这个东西,之前已经讲解果了,lua脚本几乎都是一模一样的,我们就不去做了,节省点时间</p><p>3、源信息服务发送的变更消息,需要按照商品id去分区,固定的商品变更走固定的kafka分区,也就是固定的一个缓存服务实例获取到</p><p>缓存服务,是监听kafka topic的,一个缓存服务实例,作为一个kafka consumer,就消费topic中的一个partition</p><p>所以你有多个缓存服务实例的话,每个缓存服务实例就消费一个kafka partition</p><p>所以这里,一般来说,你的源头信息服务,在发送消息到kafka topic的时候,都需要按照product id去分区</p><p>也就时说,同一个product id变更的消息一定是到同一个kafka partition中去的,也就是说同一个product id的变更消息,一定是同一个缓存服务实例消费到的</p><p>我们也不去做了,其实很简单,kafka producer api,里面send message的时候,多加一个参数就可以了,product id传递进去,就可以了</p><p>4、问题是,自己写的简易的hash分发,与kafka的分区,可能并不一致!!!</p><p>我们自己写的简易的hash分发策略,是按照crc32去取hash值,然后再取模的</p><p>关键你又不知道你的kafka producer的hash策略是什么,很可能说跟我们的策略是不一样的</p><p>拿就可能导致说,数据变更的消息所到的缓存服务实例,跟我们的应用层nginx分发到的那个缓存服务实例也许就不在一台机器上了</p><p>这样的话,在高并发,极端的情况下,可能就会出现冲突</p><p>5、分布式的缓存重建并发冲突问题发生了。。。</p><p>6、基于zookeeper分布式锁的解决方案</p><p>分布式锁,如果你有多个机器在访问同一个共享资源,那么这个时候,如果你需要加个锁,让多个分布式的机器在访问共享资源的时候串行起来</p><p>那么这个时候,那个锁,多个不同机器上的服务共享的锁,就是分布式锁</p><p>分布式锁当然有很多种不同的实现方案,redis分布式锁,zookeeper分布式锁</p><p>zk,做分布式协调这一块,还是很流行的,大数据应用里面,hadoop,storm,都是基于zk去做分布式协调</p><p>zk分布式锁的解决并发冲突的方案</p><p>(1)变更缓存重建以及空缓存请求重建,更新redis之前,都需要先获取对应商品id的分布式锁<br>(2)拿到分布式锁之后,需要根据时间版本去比较一下,如果自己的版本新于redis中的版本,那么就更新,否则就不更新<br>(3)如果拿不到分布式锁,那么就等待,不断轮询等待,直到自己获取到分布式的锁</p>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>幂等性保证机制</title>
<link href="/2019/10/06/%E5%B9%82%E7%AD%89%E6%80%A7%E4%BF%9D%E8%AF%81%E6%9C%BA%E5%88%B6/"/>
<url>/2019/10/06/%E5%B9%82%E7%AD%89%E6%80%A7%E4%BF%9D%E8%AF%81%E6%9C%BA%E5%88%B6/</url>
<content type="html"><![CDATA[<h2 id="幂等性概念"><a href="#幂等性概念" class="headerlink" title="幂等性概念"></a><strong>幂等性概念</strong></h2><p>在编程中.一个幂等操作的特点是其<strong>任意多次执行所产生的影响均与一次执行的影响相同。</strong>指可以使用相同参数重复执行,并能获得相同结果,<strong>不用担心重复执行会对系统造成改变</strong>。</p><h2 id="幂等性场景"><a href="#幂等性场景" class="headerlink" title="幂等性场景"></a><strong>幂等性场景</strong></h2><ul><li>查询操作:查询一次和查询多次,在数据不变的情况下,查询结果是一样的。select是天然的幂等操作;</li><li>删除操作:删除操作也是幂等的,删除一次和多次删除都是把数据删除。(注意可能返回结果不一样,删除的数据不存在,返回0,删除的数据多条,返回结果多个) ;</li><li>修改操作:多次修改,因为主键约束,修改一次和修改多次,如果数据没变,操作相同。</li></ul><h2 id="保证幂等性措施"><a href="#保证幂等性措施" class="headerlink" title="保证幂等性措施"></a>保证幂等性措施</h2><h3 id="唯一索引,防重表"><a href="#唯一索引,防重表" class="headerlink" title="唯一索引,防重表"></a>唯一索引,防重表</h3><p>防止<strong>新增</strong>脏数据。比如:支付宝的资金账户,支付宝也有用户账户,每个用户只能有一个资金账户,怎么防止给用户创建资金账户多个,那么给资金账户表中的用户ID加唯一索引,所以一个用户新增成功一个资金账户记录。要点:唯一索引或唯一组合索引来防止新增数据存在脏数据(当表存在唯一索引,并发时新增报错时,再查询一次就可以了,数据应该已经存在了,返回结果即可);需要合适的业务唯一字段</p><h3 id="token机制"><a href="#token机制" class="headerlink" title="token机制"></a>token机制</h3><p>防止页面重复提交。原理上通过session token来实现的(<strong>也可以通过redis来实现</strong>)。当客户端<strong>请求页面时,服务器会生成一个随机数Token,并且将Token放置到session当中</strong>,然后将Token发给客户端(一般通过<strong>构造hidden表单</strong>)。下次客户端提交请求时,Token会<strong>随着表单一起提交到服务器端</strong>。</p><p>服务器端<strong>第一次验证相同过后,会将session中的Token值更新下</strong>,<strong>若用户重复提交,第二次的验证判断将失败,因为用户提交的表单中的Token没变,但服务器端session中Token已经改变了。</strong></p><h3 id="悲观锁"><a href="#悲观锁" class="headerlink" title="悲观锁"></a>悲观锁</h3><p>获取数据的时候加锁获取。select * from table_xxx where id=’xxx’ for update;<br>注意:<strong>id字段一定是主键或者唯一索引,不然是锁表,会死人的</strong>;悲观锁使用时一般伴随事务一起使用,数据锁定时间可能会很长,根据实际情况选用;</p><h3 id="乐观锁"><a href="#乐观锁" class="headerlink" title="乐观锁"></a>乐观锁</h3><p><strong>只是在更新数据那一刻锁表</strong>,其他时间不锁表,所以相对于悲观锁,效率更高。乐观锁的实现方式多种多样可以通过version或者其他状态条件:</p><ol><li>通过版本号实现update table_xxx set name=#{name},version=version+1 where version=#{version};</li><li>通过条件限制 update table_xxx set avai_amount=avai_amount-#subAmount# where<br>avai_amount-#subAmount# >= 0要求:quality-#subQuality# >= 0<br>,这个情景适合不用版本号,只更新是做数据安全校验,适合库存模型,扣份额和回滚份额,性能更高;</li></ol><h3 id="分布式锁"><a href="#分布式锁" class="headerlink" title="分布式锁"></a>分布式锁</h3><p>如果是分布式系统,构建全局唯一索引比较困难,例如唯一性的字段没法确定,这时候可以引入分布式锁,通过第三方的系统(redis或zookeeper),在业务系统插入数据或者更新数据,获取分布式锁,然后做操作,之后释放锁,这样其实是把多线程并发的锁的思路,引入多多个系统,也就是分布式系统中得解决思路。要点:某个长流程处理过程要求不能并发执行,可以<strong>在流程执行之前根据某个标志(用户ID+后缀等)获取分布式锁,其他流程执行时获取锁就会失败,也就是同一时间该流程只能有一个能执行成功,执行完成后,释放分布式锁(分布式锁要第三方系统提供);</strong></p><h3 id="select-insert"><a href="#select-insert" class="headerlink" title="select + insert"></a>select + insert</h3><p>并发不高的后台系统,或者一些任务JOB,为了支持幂等,支持重复执行,简单的处理方法是,<strong>先查询下一些关键数据,判断是否已经执行过,在进行业务处理,就可以了。</strong>注意:<strong>核心高并发流程不要用这种方法;</strong></p><h3 id="状态机幂等"><a href="#状态机幂等" class="headerlink" title="状态机幂等"></a>状态机幂等</h3><p>在设计单据相关的业务,或者是任务相关的业务,肯定会涉及到状态机(状态变更图),就是业务单据上面有个状态,状态在不同的情况下会发生变更,一般情况下存在有限状态机,这时候,如果状态机已经处于下一个状态,这时候来了一个上一个状态的变更,理论上是不能够变更的,这样的话,保证了有限状态机的幂等。注意:订单等单据类业务,存在很长的状态流转,一定要深刻理解状态机,对业务系统设计能力提高有很大帮助</p><h3 id="对外提供接口的api如何保证幂等"><a href="#对外提供接口的api如何保证幂等" class="headerlink" title="对外提供接口的api如何保证幂等"></a>对外提供接口的api如何保证幂等</h3><p>如银联提供的付款接口:需要接入商户提交付款请求时附带:source来源,seq序列号;source+seq在数据库里面做唯一索引,防止多次付款(并发时,只能处理一个请求) 。<br>重点:对外提供接口为了支持幂等调用,接口有两个字段必须传,一个是来源source,一个是来源方序列号seq,这个两个字段在提供方系统里面做联合唯一索引,这样当第三方调用时,先在本方系统里面查询一下,是否已经处理过,返回相应处理结果;没有处理过,进行相应处理,返回结果。注意,为了幂等友好,一定要先查询一下,是否处理过该笔业务,不查询直接插入业务系统,会报错,但实际已经处理了。</p><h2 id="高并发下的幂等"><a href="#高并发下的幂等" class="headerlink" title="高并发下的幂等"></a>高并发下的幂等</h2><p>其中数据库的乐观锁和防重表的使用,都是涉及到数据的参与,在高并发的应用场景中,业务的判断逻辑尽量不要使用数据库参与,特别是RDBMS的参与,因为RDBMS天生具有不易扩展及事务处理属性,吞吐量上都会有相应的瓶颈</p><h3 id="Token令牌-分布式锁的方式"><a href="#Token令牌-分布式锁的方式" class="headerlink" title="Token令牌+分布式锁的方式"></a>Token令牌+分布式锁的方式</h3><p><strong>Token是用于确定交易的唯一属性</strong>,也是服务端用于检验当前交易是否合法交易的依据,但是在分布式的复杂环境中,如果没有分布式锁的控制,同一笔交易就可能会被处理多次,因而为了确认交易的<strong>幂等性</strong>,Token令牌和分布式锁必须要一起使用。</p><p>实现逻辑步骤如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">1、服务端根据交易前请求生成对应的Token,保存于服务端的Token库中,通常是缓存集群中,并将生成好的Token库下发给客户端;隐藏在其表单</span><br><span class="line">2、客户端在每次请求的时候,都带上对应的Token;</span><br><span class="line">3、服务端获取该Token对应的锁,如果获取成功,则继续下面的步骤;</span><br><span class="line">4、判断是否该Token是否合法,如果合法则继续下一步;</span><br><span class="line">5、处理真实的业务逻辑;</span><br><span class="line">6、业务处理成功后,从缓存中删除该Token;</span><br><span class="line">7、删除获取的分布式锁;</span><br></pre></td></tr></table></figure><img src="/2019/10/06/幂等性保证机制/148060-20190615074312859-1847837669.jpg"><h3 id="异步处理"><a href="#异步处理" class="headerlink" title="异步处理"></a>异步处理</h3><p>异步处理,通常的做法是将认为需要消费的交易,提交到消息队列中,并注册监听事件,待交易被处理完后,再由处理交易的应用回调注册的监听事件反馈处理的结果。交易处理的调度应用,需要负责对交易的处理符合幂等性的原则,将重复请求的交易请求做去重处理。</p><img src="/2019/10/06/幂等性保证机制/148060-20190615074331227-1455327638.jpg"><p>从逻辑处理上可以看到,只要交易处理器足够多,处理速度也不一定会受到多少的影响,交易生产者和交易接收者甚至可以同步返回结果,当交易接收者接收处理结果超时后,再提示用户过一会儿查看交易的处理结果。</p>]]></content>
<tags>
<tag> 系统设计 </tag>
</tags>
</entry>
<entry>
<title>mysql日志2</title>
<link href="/2019/10/06/mysql%E6%97%A5%E5%BF%972/"/>
<url>/2019/10/06/mysql%E6%97%A5%E5%BF%972/</url>
<content type="html"><![CDATA[<h2 id="错误日志"><a href="#错误日志" class="headerlink" title="错误日志"></a>错误日志</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql> show variables like 'log_error';</span><br><span class="line">+---------------+---------------------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+---------------+---------------------+</span><br><span class="line">| log_error | /var/log/mysqld.log |</span><br><span class="line">+---------------+---------------------+</span><br><span class="line">1 row in set (0.03 sec)</span><br></pre></td></tr></table></figure><p><strong>错误日志文件对MySQL的启动,运行,关闭过程进行了记录。</strong>可以看到错误日志的路径和文件名,默认情况下错误文件的文件名为服务器的主机名,即:hostname.err。只不过我这里设置的是/var/log/mysqld.log,修改错误日志地址可以在/etc/my.cnf中添加</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># Recommended in standard MySQL setup</span><br><span class="line">sql_mode=NO_ENGINE_SUBSTITUTION,STRICT_TRANS_TABLES</span><br><span class="line"></span><br><span class="line">[mysqld_safe]</span><br><span class="line">log-error=/var/log/mysqld.log</span><br><span class="line">pid-file=/var/run/mysqld/mysqld.pid</span><br></pre></td></tr></table></figure><p><strong>当出现MySQL数据库不能正常启动时,第一个必须查找的文件就是错误日志文件,该文件记录了出错信息,能够帮助我们找到问题。</strong></p><h2 id="慢查询日志"><a href="#慢查询日志" class="headerlink" title="慢查询日志"></a>慢查询日志</h2><p><strong>慢查询日志用来记录响应时间超过阈值的SQL语句</strong>,所以我们可以设置一个阈值,将运行时间超过该值的所有SQL语句都记录到慢查询日志文件中。该阈值可以<strong>通过参数 long_query_time 来设置</strong>,<strong>默认为10秒。</strong></p><h3 id="启动慢查询日志"><a href="#启动慢查询日志" class="headerlink" title="启动慢查询日志"></a>启动慢查询日志</h3><p>默认情况下,MySQL数据库并不启动慢查询日志,需要手动将这个参数设为ON,然后启动</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre></td><td class="code"><pre><span class="line">mysql> show variables like "%slow%";</span><br><span class="line">+---------------------------+-------------------------------------------------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+---------------------------+-------------------------------------------------+</span><br><span class="line">| log_slow_admin_statements | OFF |</span><br><span class="line">| log_slow_slave_statements | OFF |</span><br><span class="line">| slow_launch_time | 2 |</span><br><span class="line">| slow_query_log | OFF |</span><br><span class="line">| slow_query_log_file | /var/lib/mysql/iz2zeaf3cg1099kiidi06mz-slow.log |</span><br><span class="line">+---------------------------+-------------------------------------------------+</span><br><span class="line">5 rows in set (0.00 sec)</span><br><span class="line"></span><br><span class="line">mysql> set global slow_query_log='ON';</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line"></span><br><span class="line"></span><br><span class="line">mysql> show variables like "slow_query_log";</span><br><span class="line">+---------------------------+-------------------------------------------------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+---------------------------+-------------------------------------------------+ |</span><br><span class="line">| slow_query_log | ON |</span><br><span class="line">| slow_query_log_file | /var/lib/mysql/iz2zeaf3cg1099kiidi06mz-slow.log |</span><br><span class="line">+---------------------------+-------------------------------------------------+</span><br><span class="line">2 rows in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>但是使用 set global slow_query_log=’ON’ 开启慢查询日志,只是对当前数据库有效,如果MySQL数据库重启后就会失效。所以如果要永久生效,就要修改配置文件 my.cnf (其他系统变量也是如此)</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[mysqld]</span><br><span class="line">slow_query_log=1</span><br></pre></td></tr></table></figure><h3 id="设置阈值"><a href="#设置阈值" class="headerlink" title="设置阈值"></a>设置阈值</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql> show variables like 'long_query_time';</span><br><span class="line">+-----------------+-----------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+-----------------+-----------+</span><br><span class="line">| long_query_time | 10.000000 |</span><br><span class="line">+-----------------+-----------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>阈值默认为10秒,我们可以修改阈值大小,比如(当然这还是对当前数据库有效)</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mysql> set global long_query_time=0.05;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br></pre></td></tr></table></figure><p><strong>设置long_query_time这个阈值之后,MySQL数据库会记录运行时间超过该值的所有SQL语句,但对于运行时间正好等于 long_query_time 的情况,并不会被记录下。</strong>而设置 long_query_time为0来捕获所有的查询</p><h3 id="参数log-queries-not-using-indexes"><a href="#参数log-queries-not-using-indexes" class="headerlink" title="参数log_queries_not_using_indexes"></a>参数log_queries_not_using_indexes</h3><p><strong>如果运行的SQL语句没有使用索引,则MySQL数据库同样会将这条SQL语句记录到慢查询日志文件。首先确认打开了log_queries_not_using_indexes;</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql> show variables like 'log_queries_not_using_indexes';</span><br><span class="line">+-------------------------------+-------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+-------------------------------+-------+</span><br><span class="line">| log_queries_not_using_indexes | ON |</span><br><span class="line">+-------------------------------+-------+</span><br><span class="line">1 row in set (0.12 sec)</span><br></pre></td></tr></table></figure><h3 id="将日志记录放入表中"><a href="#将日志记录放入表中" class="headerlink" title="将日志记录放入表中"></a>将日志记录放入表中</h3><p>多数情况下这样做没什么必要,这不但<strong>对性能有较大影响</strong>,而且 MySQL 5.1 在将慢查询记录到文件中时已经支持微秒级别的信息,然而将慢查询记录到表中会导致时间粒度退化为只能到秒级,而秒级别的慢查询日志没有太大的意义</p><h3 id="慢查询日志分析工具"><a href="#慢查询日志分析工具" class="headerlink" title="慢查询日志分析工具"></a>慢查询日志分析工具</h3><p>当越来越多的SQL查询被记录到慢查询日志文件中,这时候直接看日志文件就不容易了,MySQL提供了<strong>mysqldumpslow 命令</strong>解决</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">[root@iz2zeaf3cg1099kiidi06mz mysql]# mysqldumpslow iz2zeaf3cg1099kiidi06mz-slow.log</span><br><span class="line"></span><br><span class="line">Reading mysql slow query log from iz2zeaf3cg1099kiidi06mz-slow.log</span><br><span class="line">Count: 1 Time=60.02s (60s) Lock=0.00s (0s) Rows=149272.0 (149272), root[root]@[117.136.86.151]</span><br><span class="line"> select * from vote_record_memory</span><br><span class="line"></span><br><span class="line">Count: 1 Time=14.85s (14s) Lock=0.00s (0s) Rows=0.0 (0), root[root]@[117.136.86.151]</span><br><span class="line"> CALL add_vote_memory(N)</span><br><span class="line"></span><br><span class="line">Count: 1 Time=1.72s (1s) Lock=0.00s (0s) Rows=0.0 (0), root[root]@[117.136.86.151]</span><br><span class="line"> INSERT into vote_record SELECT * from vote_record_memory</span><br><span class="line"></span><br><span class="line">Count: 1 Time=0.02s (0s) Lock=0.00s (0s) Rows=142.0 (142), root[root]@[117.136.86.151]</span><br><span class="line"> select * from vote_record_memory where vote_id = N</span><br></pre></td></tr></table></figure><h3 id="pt-query-digest-工具"><a href="#pt-query-digest-工具" class="headerlink" title="pt-query-digest 工具"></a><strong>pt-query-digest 工具</strong></h3><p>pt-query-digest 是分析MySQL查询日志最有力的工具,该工具功能强大,它可以分析binlog,Generallog,slowlog,也可以通过show processlist或者通过 tcpdump 抓取的MySQL协议数据来进行分析,比 mysqldumpslow 更具体,更完善。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">pt-query-digest slow.log > slow_report.log</span><br></pre></td></tr></table></figure><p>该工具可以将查询的剖析报告打印出来,可以分析结果输出到文件中,分析过程是先对查询语句的条件进行参数化,然后对参数化以后的查询进行分组统计,统计出各查询的执行时间,次数,占比等,可以借助分析结果找出问题进行优化。</p><h2 id="查询日志"><a href="#查询日志" class="headerlink" title="查询日志"></a>查询日志</h2><p>查看日志记录了所有对 MySQL 数据库请求的信息,不论这些请求是否得到了正确的执行。默认为 主机名.log</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">mysql> show variables like "general_log%";</span><br><span class="line">+------------------+--------------------------------------------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+------------------+--------------------------------------------+</span><br><span class="line">| general_log | OFF |</span><br><span class="line">| general_log_file | /var/lib/mysql/iz2zeaf3cg1099kiidi06mz.log |</span><br><span class="line">+------------------+--------------------------------------------+</span><br><span class="line">2 rows in set (0.24 sec)</span><br></pre></td></tr></table></figure><p>默认情况下不启动查询日志,必须要先开启。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">mysql> set global general_log='ON';</span><br><span class="line">Query OK, 0 rows affected (0.05 sec)</span><br><span class="line"></span><br><span class="line">mysql> show variables like "general_log%";</span><br><span class="line">+------------------+--------------------------------------------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+------------------+--------------------------------------------+</span><br><span class="line">| general_log | ON |</span><br><span class="line">| general_log_file | /var/lib/mysql/iz2zeaf3cg1099kiidi06mz.log |</span><br><span class="line">+------------------+--------------------------------------------+</span><br><span class="line">2 rows in set (0.11 sec)</span><br></pre></td></tr></table></figure><h2 id="二进制日志-bin-log"><a href="#二进制日志-bin-log" class="headerlink" title="二进制日志(bin log)"></a>二进制日志(bin log)</h2><p>二进制日志记录了对数据库<strong>执行更改</strong>的所有操作,但是不包括select和show这类操作,因为这类操作对数据本身并没有修改,如果你还想记录select和show操作,那只能使用查询日志了,而不是二进制日志。</p><p>此外,二进制还包括了执行数据库更改操作的时间和执行时间等信息。二进制日志主要有以下几种作用</p><p><strong>恢复(recovery):</strong>某些数据的恢复需要二进制日志,如当一个数据库全备文件恢复后,我们可以通过二进制的日志进行 point-in-time的恢复</p><p><strong>复制(replication) :</strong> 通过复制和执行二进制日志使得一台远程的 MySQL 数据库(一般是slave 或者 standby) 与一台MySQL数据库(一般为master或者primary) 进行实时同步</p><p><strong>审计(audit):</strong>用户可以通过二进制日志中的信息来进行审计,判断是否有对数据库进行注入攻击</p><h3 id="开启二进制日志"><a href="#开启二进制日志" class="headerlink" title="开启二进制日志"></a>开启二进制日志</h3><p>通过配置参数 log-bin[=name] 可以启动二进制日志。如果不指定name,则默认二进制日志文件名为主机名,后缀名为二进制日志的序列号</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[mysqld]</span><br><span class="line">log-bin</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql> show variables like 'datadir';</span><br><span class="line">+---------------+-----------------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+---------------+-----------------+</span><br><span class="line">| datadir | /var/lib/mysql/ |</span><br><span class="line">+---------------+-----------------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>mysqld-bin.000001即为二进制日志文件,而mysqld-bin.index为二进制的索引文件,为了管理所有的binlog文件,MySQL额外创建了一个index文件,它按顺序记录了MySQL使用的所有binlog文件。如果你想自定义index文件的名称,可以设置log_bin_index=file参数。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-rw-rw---- 1 mysql mysql 120 Aug 21 16:42 mysqld-bin.000001</span><br><span class="line">-rw-rw---- 1 mysql mysql 20 Aug 21 16:42 mysqld-bin.index</span><br></pre></td></tr></table></figure><h3 id="查看二进制日志文件"><a href="#查看二进制日志文件" class="headerlink" title="查看二进制日志文件"></a>查看二进制日志文件</h3><p>对于二进制日志文件来说,不像错误日志文件,慢查询日志文件那样用cat,head, tail等命令可以查看,它需要通过 MySQL 提供的工具 mysqlbinlog。如</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">[root@iz2zeaf3cg1099kiidi06mz mysql]# mysqlbinlog mysqld-bin.000001</span><br><span class="line">/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=1*/;</span><br><span class="line">/*!40019 SET @@session.max_insert_delayed_threads=0*/;</span><br><span class="line">/*!50003 SET @OLD_COMPLETION_TYPE=@@COMPLETION_TYPE,COMPLETION_TYPE=0*/;</span><br><span class="line">DELIMITER /*!*/;</span><br><span class="line"># at 4</span><br><span class="line">#180821 16:42:53 server id 1 end_log_pos 120 CRC32 0x3e55be40 Start: binlog v 4, server v 5.6.39-log created 180821 16:42:53 at startup</span><br><span class="line"># Warning: this binlog is either in use or was not closed properly.</span><br><span class="line">ROLLBACK/*!*/;</span><br><span class="line">BINLOG '</span><br><span class="line">jdB7Ww8BAAAAdAAAAHgAAAABAAQANS42LjM5LWxvZwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA</span><br><span class="line">AAAAAAAAAAAAAAAAAACN0HtbEzgNAAgAEgAEBAQEEgAAXAAEGggAAAAICAgCAAAACgoKGRkAAUC+</span><br><span class="line">VT4=</span><br><span class="line">'/*!*/;</span><br><span class="line">DELIMITER ;</span><br><span class="line"># End of log file</span><br><span class="line">ROLLBACK /* added by mysqlbinlog */;</span><br><span class="line">/*!50003 SET COMPLETION_TYPE=@OLD_COMPLETION_TYPE*/;</span><br><span class="line">/*!50530 SET @@SESSION.PSEUDO_SLAVE_MODE=0*/;</span><br></pre></td></tr></table></figure><h3 id="二进制日志文件配置参数"><a href="#二进制日志文件配置参数" class="headerlink" title="二进制日志文件配置参数"></a>二进制日志文件配置参数</h3><h4 id="max-binlog-size"><a href="#max-binlog-size" class="headerlink" title="max_binlog_size"></a><strong>max_binlog_size</strong></h4><p>可以通过max_binlog_size参数来限定单个binlog文件的大小(默认1G)</p><h4 id="binlog-cache-size"><a href="#binlog-cache-size" class="headerlink" title="binlog_cache_size"></a><strong>binlog_cache_size</strong></h4><p>当使用事务的表存储引擎(如InnoDB存储引擎)时,所有未提交(uncommitted)的二进制日志会被记录到一个缓冲中去,等该事务提交(committed)时,直接将缓存中的二进制日志写入二进制日志文件中,而该缓冲的大小由binlog_cache_size决定,默认大小为32K。</p><p>此外,binlog_cache_size 是基于会话(session)的,当每一个线程开启一个事务时,MySQL会自动分配一个大小为 binlog_cache_size 的缓存</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql> show variables like 'binlog_cache_size';</span><br><span class="line">+-------------------+-------+</span><br><span class="line">| Variable_name | Value |</span><br><span class="line">+-------------------+-------+</span><br><span class="line">| binlog_cache_size | 32768 |</span><br><span class="line">+-------------------+-------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><h4 id="sync-binlog"><a href="#sync-binlog" class="headerlink" title="sync_binlog"></a><strong>sync_binlog</strong></h4><p>在默认情况下,二进制日志并不是在每次写的时候同步到磁盘。参数 sync_binlog = [N] 表示每写缓冲多少次就同步到磁盘。如果将N设置为1,即 sync_binlog = 1表示采用同步写磁盘的方式来写二进制日志,这时写操作就不用向上面所说的使用操作系统的缓冲来写二进制日志</p><h4 id="binlog-format"><a href="#binlog-format" class="headerlink" title="binlog_format"></a><strong>binlog_format</strong></h4><p><strong>1、statement :</strong> 记录的是日志的逻辑SQL语句</p><p><strong>2、row:</strong> 记录表的行更改情况</p><p><strong>3、mixed:</strong> 在此格式下,mysql默认采用statement格式进行二进制日志文件的记录,但是有些情况下使用ROW格式</p>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>synchronized原理及对比</title>
<link href="/2019/10/06/synchronized%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AF%B9%E6%AF%94/"/>
<url>/2019/10/06/synchronized%E5%8E%9F%E7%90%86%E5%8F%8A%E5%AF%B9%E6%AF%94/</url>
<content type="html"><![CDATA[<h2 id="synchronized关键字最主要的三种使用方式的总结"><a href="#synchronized关键字最主要的三种使用方式的总结" class="headerlink" title="synchronized关键字最主要的三种使用方式的总结"></a>synchronized关键字最主要的三种使用方式的总结</h2><ul><li><strong>修饰实例方法,作用于当前对象实例加锁,进入同步代码前要获得当前对象实例的锁</strong></li><li><strong>修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁</strong> 。也就是给当前类加锁,会作用于类的所有对象实例,因为静态成员不属于任何一个实例对象,是类成员(static 表明这是该类的一个静态资源,不管new了多少个对象,只有一份,所以对该类的所有对象都加了锁)。所以如果一个线程A调用一个实例对象的非静态 synchronized 方法,而线程B需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,<strong>因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁</strong>。</li><li><strong>修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。</strong> 和 synchronized 方法一样,synchronized(this)代码块也是锁定当前对象的。synchronized 关键字加到 static 静态方法和 synchronized(class)代码块上都是是给 Class 类上锁。这里再提一下:synchronized关键字加到非 static 静态方法上是给对象实例上锁。另外需要注意的是:尽量不要使用 synchronized(String a) 因为JVM中,字符串常量池具有缓冲功能!</li></ul><h2 id="双重校验锁实现对象单例(线程安全)"><a href="#双重校验锁实现对象单例(线程安全)" class="headerlink" title="双重校验锁实现对象单例(线程安全)"></a><strong>双重校验锁实现对象单例(线程安全)</strong></h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">public class Singleton {</span><br><span class="line"></span><br><span class="line"> private volatile static Singleton uniqueInstance;</span><br><span class="line"></span><br><span class="line"> private Singleton() {</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> public static Singleton getUniqueInstance() {</span><br><span class="line"> //先判断对象是否已经实例过,没有实例化过才进入加锁代码</span><br><span class="line"> if (uniqueInstance == null) {</span><br><span class="line"> //类对象加锁</span><br><span class="line"> synchronized (Singleton.class) {</span><br><span class="line"> if (uniqueInstance == null) {</span><br><span class="line"> uniqueInstance = new Singleton();</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> return uniqueInstance;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>uniqueInstance 采用 volatile 关键字修饰也是很有必要的, uniqueInstance = new Singleton(); 这段代码其实是分为三步执行:</p><ol><li>为 uniqueInstance 分配内存空间</li><li>初始化 uniqueInstance</li><li>将 uniqueInstance 指向分配的内存地址</li></ol><p>但是由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。</p><p>使用 <strong>volatile 可以禁止 JVM 的指令重排</strong>,保证在多线程环境下也能正常运行。</p><h2 id="synchronized-关键字底层原理总结"><a href="#synchronized-关键字底层原理总结" class="headerlink" title="synchronized 关键字底层原理总结"></a>synchronized 关键字底层原理总结</h2><p>synchronized 关键字底层原理属于 JVM 层面。</p><h3 id="synchronized-同步语句块的情况"><a href="#synchronized-同步语句块的情况" class="headerlink" title="synchronized 同步语句块的情况"></a>synchronized 同步语句块的情况</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">public class SynchronizedDemo {</span><br><span class="line"> public void method() {</span><br><span class="line"> synchronized (this) {</span><br><span class="line"> System.out.println("synchronized 代码块");</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行<code>javac SynchronizedDemo.java</code> 命令生成编译后的 .class 文件,然后执行<code>javap -c -s -v -l SynchronizedDemo.class</code></p><img src="/2019/10/06/synchronized原理及对比/java5-1543319924.png"><p><strong>synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。</strong> 当执行monitorenter 指令时,线程试图获取锁也就是获取 monitor(monitor对象存在于每个Java对象的对象头中,synchronized 锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因) 的持有权.当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。</p><h3 id="synchronized-修饰方法的的情况"><a href="#synchronized-修饰方法的的情况" class="headerlink" title="synchronized 修饰方法的的情况"></a>synchronized 修饰方法的的情况</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">public class SynchronizedDemo2 {</span><br><span class="line"> public synchronized void method() {</span><br><span class="line"> System.out.println("synchronized 方法");</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><img src="/2019/10/06/synchronized原理及对比/java0-1543319924.png"><p>synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 <strong>ACC_SYNCHRONIZED</strong> 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。</p><h2 id="Synchronized-和-ReenTrantLock-的对比"><a href="#Synchronized-和-ReenTrantLock-的对比" class="headerlink" title="Synchronized 和 ReenTrantLock 的对比"></a>Synchronized 和 ReenTrantLock 的对比</h2><h3 id="两者都是可重入锁"><a href="#两者都是可重入锁" class="headerlink" title="两者都是可重入锁"></a><strong>两者都是可重入锁</strong></h3><p>两者都是可重入锁。“可重入锁”概念是:自己可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当其再次想要获取这个对象的锁的时候还是可以获取的,如果不可锁重入的话,就会造成死锁。同一个线程每次获取锁,锁的计数器都自增1,所以要等到锁的计数器下降为0时才能释放锁。</p><blockquote><p>synchronized是对象头中锁的计数器,ReenTrantLock是volatile state</p></blockquote><h3 id="synchronized-依赖于-JVM-而-ReenTrantLock-依赖于-API"><a href="#synchronized-依赖于-JVM-而-ReenTrantLock-依赖于-API" class="headerlink" title="synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API"></a><strong>synchronized 依赖于 JVM 而 ReenTrantLock 依赖于 API</strong></h3><p>synchronized 是依赖于 JVM 实现的,前面我们也讲到了 虚拟机团队在 JDK1.6 为 synchronized 关键字进行了很多优化,但是这些优化都是在虚拟机层面实现的,并没有直接暴露给我们。ReenTrantLock 是 JDK 层面实现的(也就是 API 层面,需要 lock() 和 unlock 方法配合 try/finally 语句块来完成)</p><h3 id="ReenTrantLock-比-synchronized-增加了一些高级功能"><a href="#ReenTrantLock-比-synchronized-增加了一些高级功能" class="headerlink" title="ReenTrantLock 比 synchronized 增加了一些高级功能"></a><strong>ReenTrantLock 比 synchronized 增加了一些高级功能</strong></h3><p>相比synchronized,ReenTrantLock增加了一些高级功能。主要来说主要有三点:<strong>①等待可中断;②可实现公平锁;③可实现选择性通知(锁可以绑定多个条件)</strong></p><ul><li><strong>ReenTrantLock提供了一种能够中断等待锁的线程的机制</strong>,通过lock.lockInterruptibly()来实现这个机制。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。</li><li><strong>ReenTrantLock可以指定是公平锁还是非公平锁。而synchronized只能是非公平锁。所谓的公平锁就是先等待的线程先获得锁。</strong> ReenTrantLock默认情况是非公平的,可以通过 ReenTrantLock类的<code>ReentrantLock(boolean fair)</code>构造方法来制定是否是公平的。</li><li>synchronized关键字与wait()和notify/notifyAll()方法相结合可以实现等待/通知机制,ReentrantLock类当然也可以实现,但是需要借助于Condition接口与newCondition() 方法。Condition是JDK1.5之后才有的,它具有很好的灵活性,比如可以实现多路通知功能也就是在一个Lock对象中可以创建多个Condition实例(即对象监视器),<strong>线程对象可以注册在指定的Condition中,从而可以有选择性的进行线程通知,在调度线程上更加灵活。 在使用notify/notifyAll()方法进行通知时,被通知的线程是由 JVM 选择的,用ReentrantLock类结合Condition实例可以实现“选择性通知”</strong> ,这个功能非常重要,而且是Condition接口默认提供的。而synchronized关键字就相当于整个Lock对象中只有一个Condition实例,所有的线程都注册在它一个身上。如果执行notifyAll()方法的话就会通知所有处于等待状态的线程这样会造成很大的效率问题,而Condition实例的signalAll()方法 只会唤醒注册在该Condition实例中的所有等待线程。</li></ul><h3 id="性能已不是选择标准"><a href="#性能已不是选择标准" class="headerlink" title="性能已不是选择标准"></a><strong>性能已不是选择标准</strong></h3><p><strong>JDK1.6之后,synchronized 和 ReenTrantLock 的性能基本是持平了。所以网上那些说因为性能才选择 ReenTrantLock 的文章都是错的!JDK1.6之后,性能已经不是选择synchronized和ReenTrantLock的影响因素了!而且虚拟机在未来的性能改进中会更偏向于原生的synchronized,所以还是提倡在synchronized能满足你的需求的情况下,优先考虑使用synchronized关键字来进行同步!优化后的synchronized和ReenTrantLock一样,在很多地方都是用到了CAS操作</strong>。</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>synchronized详解</title>
<link href="/2019/10/06/synchronized%E8%AF%A6%E8%A7%A3/"/>
<url>/2019/10/06/synchronized%E8%AF%A6%E8%A7%A3/</url>
<content type="html"><![CDATA[<h2 id="synchronize的可重入性"><a href="#synchronize的可重入性" class="headerlink" title="synchronize的可重入性"></a>synchronize的可重入性</h2><p>在java 内部,同一个线程在调用自己类中其他synchronize方法/块或调用父类的synchronize方法/块都不会阻碍改线程的执行,就是说同一线程对同一个对象锁是可重入的,而且同一个线程可以获取同一把锁多次,也就是可以多次重入。</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><span class="line"><span class="keyword">public</span> <span class="class"><span class="keyword">class</span> <span class="title">Child</span> <span class="keyword">extends</span> <span class="title">Father</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">static</span> <span class="keyword">void</span> <span class="title">main</span><span class="params">(String[] args)</span> </span>{</span><br><span class="line"> Child child = <span class="keyword">new</span> Child();</span><br><span class="line"> child.doSomething();</span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"child.doSomething()"</span>);</span><br><span class="line"> doAnotherThing(); <span class="comment">// 调用自己类中其他的synchronized方法</span></span><br><span class="line"></span><br><span class="line"> }</span><br><span class="line"></span><br><span class="line"> <span class="function"><span class="keyword">private</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">doAnotherThing</span><span class="params">()</span> </span>{</span><br><span class="line"> <span class="keyword">super</span>.doSomething(); <span class="comment">// 调用父类的synchronized方法</span></span><br><span class="line"> System.out.println(<span class="string">"child.doAnotherThing()"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Father</span> </span>{</span><br><span class="line"> <span class="function"><span class="keyword">public</span> <span class="keyword">synchronized</span> <span class="keyword">void</span> <span class="title">doSomething</span><span class="params">()</span> </span>{</span><br><span class="line"> System.out.println(<span class="string">"father.doSomething()"</span>);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">运行结果:</span><br><span class="line"></span><br><span class="line">child.doSomething() </span><br><span class="line">father.doSomething() </span><br><span class="line">child.doAnotherThing()</span><br></pre></td></tr></table></figure><p>这里的对象锁只有一个,就是child对象的锁,当执行child.doSomething时,该线程获得child对象的锁,在doSomething方法内执行doAnotherThing时再次请求child对象的锁,因为synchronized是重入锁,所以可以得到该锁,继续在doAnotherThing里执行父类的doSomething方法时第三次请求child对象的锁,同理可得到,如果不是重入锁的话,那这后面这两次请求锁将会被一直阻塞,从而导致死锁。</p><h2 id="synchonized可重入锁的实现"><a href="#synchonized可重入锁的实现" class="headerlink" title="synchonized可重入锁的实现"></a>synchonized可重入锁的实现</h2><p>每个锁会关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应方法,当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1,此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronize方法/块时,计数器会递减,如果计数器为0则释放该锁。</p><h2 id="偏向锁、轻量级锁和重量级锁"><a href="#偏向锁、轻量级锁和重量级锁" class="headerlink" title="偏向锁、轻量级锁和重量级锁"></a>偏向锁、轻量级锁和重量级锁</h2><p>synchronized的偏向锁、轻量级锁以及重量级锁是通过Java对象头实现的。博主在Java对象大小内幕浅析中提到了<strong>Java对象的内存布局分为:对象头、实例数据和对齐填充</strong>,而对象头又可以分为”Mark Word”和类型指针klass。”Mark Word”是关键,默认情况下,其存储对象的HashCode、分代年龄和锁标记位。</p><p>这里说的都是以HotSpot虚拟机为基准的。首先来看一下”Mark Word”的内容:</p><table><thead><tr><th>状态</th><th>存储内容</th><th>标志位</th></tr></thead><tbody><tr><td>无锁</td><td>对象的hashCode、对象分代年龄、是否是偏向锁(0)</td><td>01</td></tr><tr><td>轻量级</td><td>指向栈中锁记录的指针</td><td>00</td></tr><tr><td>重量级</td><td>指向互斥量(重量级锁)的指针</td><td>10</td></tr><tr><td>GC标记</td><td>(空)</td><td>11</td></tr><tr><td>偏向锁</td><td>偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)</td><td>01</td></tr></tbody></table><p>偏向锁是JDK6中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。</p><p>偏向锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要同步。大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。</p><p><strong>当锁对象第一次被线程获取的时候,线程使用CAS操作把这个锁的线程ID记录在对象Mark Word之中,同时置偏向标志位1。以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。</strong></p><p>如果线程使用CAS操作时失败则表示该锁对象上存在竞争并且这个时候另外一个线程获得偏向锁的所有权。当到达全局安全点(safepoint,这个时间点上没有正在执行的字节码)时获得偏向锁的线程被挂起,膨胀为轻量级锁(涉及Monitor Record,Lock Record相关操作,这里不展开),同时被撤销偏向锁的线程继续往下执行同步代码。</p><p>则当有另外一个线程去尝试获取这个锁时,偏向模式就宣告结束。</p><p>线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录(Lock Record)的空间,并将对象头中的Mard Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋失败则锁会膨胀成重量级锁。如果自旋成功则依然处于轻量级锁的状态。</p><p>轻量级锁的解锁过程也是通过CAS操作来进行的,如果对象的Mark Word仍然指向线程的锁记录,那就用CAS操作把对象当前的Mark Word和线程中赋值的Displaced Mark Word替换回来,如果替换成功,整个同步过程就完成了,如果替换失败,就说明有其他线程尝试过获取该锁,那就要在释放锁的同时,唤醒被挂起的线程。</p><p>轻量级锁提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。</p><p>整个synchronized锁流程如下:</p><ol><li>检测Mark Word里面是不是当前线程的ID,如果是,表示当前线程处于偏向锁</li><li>如果不是,则使用CAS将当前线程的ID替换Mard Word,如果成功则表示当前线程获得偏向锁,置偏向标志位1</li><li>如果失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁。</li><li>当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁</li><li>如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。</li><li>如果自旋成功则依然处于轻量级状态。</li><li>如果自旋失败,则升级为重量级锁。</li></ol>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>深入理解AQS</title>
<link href="/2019/10/06/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3AQS/"/>
<url>/2019/10/06/%E6%B7%B1%E5%85%A5%E7%90%86%E8%A7%A3AQS/</url>
<content type="html"><![CDATA[<h2 id="同步队列"><a href="#同步队列" class="headerlink" title="同步队列"></a>同步队列</h2><p>当共享资源被某个线程占有,其他请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,队列的实现方式无外乎两者一是通过数组的形式,另外一种则是链表的形式。AQS中的同步队列则是<strong>通过链式方式</strong>进行实现。同步队列是<strong>带头结点的链式存储结构</strong>。</p><p>在AQS有一个静态内部类<strong>Node</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">volatile int waitStatus //节点状态 </span><br><span class="line">volatile Node prev //当前节点/线程的前驱节点 </span><br><span class="line">volatile Node next; //当前节点/线程的后继节点 </span><br><span class="line">volatile Thread thread;//加入同步队列的线程引用 </span><br><span class="line">Node nextWaiter;//等待队列中的下一个节点</span><br></pre></td></tr></table></figure><p><strong>节点的状态</strong>有以下这些</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">int CANCELLED = 1//节点从同步队列中取消 </span><br><span class="line">int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行; </span><br><span class="line">int CONDITION = -2//当前节点进入等待队列中 </span><br><span class="line">int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去 </span><br><span class="line">int INITIAL = 0;//初始状态`</span><br></pre></td></tr></table></figure><p>AQS中有两个重要的成员变量,通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">private transient volatile Node head;</span><br><span class="line">private transient volatile Node tail;</span><br></pre></td></tr></table></figure><img src="/2019/10/06/深入理解AQS/163261637bb25796.png"><h2 id="独占锁"><a href="#独占锁" class="headerlink" title="独占锁"></a>独占锁</h2><p>调用lock()方法是获取独占式锁,获取失败就将当前线程加入同步队列,成功则线程执行。而lock()方法实际上会调用AQS的<strong>acquire()</strong>方法,源码如下</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">//先看同步状态是否获取成功,如果成功则方法结束返回 </span><br><span class="line">//若失败则先调用addWaiter()方法再调用acquireQueued()方法 </span><br><span class="line">public final void acquire(int arg) {</span><br><span class="line"> if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))</span><br><span class="line"> selfInterrupt();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>acquire根据当前获得同步状态成功与否做了两件事情:1. 成功,则方法结束返回,2. 失败,则先调用addWaiter()然后在调用acquireQueued()方法。</p><h3 id="addWaiter-源码"><a href="#addWaiter-源码" class="headerlink" title="addWaiter()源码"></a><strong>addWaiter()</strong>源码</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">private Node addWaiter(Node mode) {</span><br><span class="line">// 1. 将当前线程构建成Node类型</span><br><span class="line"> Node node = new Node(Thread.currentThread(), mode);</span><br><span class="line"> // Try the fast path of enq; backup to full enq on failure</span><br><span class="line"> // 2. 当前尾节点是否为null?</span><br><span class="line">Node pred = tail;</span><br><span class="line"> if (pred != null) {</span><br><span class="line">// 2.2 将当前节点尾插入的方式插入同步队列中</span><br><span class="line"> node.prev = pred;</span><br><span class="line"> if (compareAndSetTail(pred, node)) {</span><br><span class="line"> pred.next = node;</span><br><span class="line"> return node;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">// 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程</span><br><span class="line"> enq(node);</span><br><span class="line"> return node;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>程序的逻辑主要分为两个部分:<strong>1. 当前同步队列的尾节点为null,调用方法enq()插入;2. 当前队列的尾节点不为null,则采用尾插入(compareAndSetTail()方法)的方式入队。</strong>另外还会有另外一个问题:如果 <code>if (compareAndSetTail(pred, node))</code>为false怎么办?会继续执行到enq()方法,同时很明显compareAndSetTail是一个CAS操作,通常来说如果CAS操作失败会继续自旋(死循环,在enq中自旋)进行重试。</p><h3 id="enq-源码"><a href="#enq-源码" class="headerlink" title="enq()源码"></a>enq()源码</h3><p><strong>1. 处理当前同步队列尾节点为null时进行入队操作;2. 如果CAS尾插入节点失败后负责自旋进行尝试。</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line">private Node enq(final Node node) {</span><br><span class="line"> for (;;) {</span><br><span class="line"> Node t = tail;</span><br><span class="line">if (t == null) { // Must initialize</span><br><span class="line">//1. 构造头结点</span><br><span class="line"> if (compareAndSetHead(new Node()))</span><br><span class="line"> tail = head;</span><br><span class="line"> } else {</span><br><span class="line">// 2. 尾插入,CAS操作失败自旋尝试</span><br><span class="line"> node.prev = t; </span><br><span class="line"> if (compareAndSetTail(t, node)) {</span><br><span class="line"> t.next = node;</span><br><span class="line"> return t;</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>获取独占式锁失败的线程包装成Node然后插入同步队列的过程,线程)会做什么事情了来保证自己能够有机会获得独占式锁?</p><h3 id="acquireQueued-源码"><a href="#acquireQueued-源码" class="headerlink" title="acquireQueued()源码"></a><strong>acquireQueued()</strong>源码</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre></td><td class="code"><pre><span class="line">final boolean acquireQueued(final Node node, int arg) {</span><br><span class="line"> boolean failed = true;</span><br><span class="line"> try {</span><br><span class="line"> boolean interrupted = false;</span><br><span class="line"> for (;;) {</span><br><span class="line">// 1. 获得当前节点的先驱节点</span><br><span class="line"> final Node p = node.predecessor();</span><br><span class="line">// 2. 当前节点能否获取独占式锁</span><br><span class="line">// 2.1 如果当前节点的先驱节点是头结点并且成功获取同步状态,即可以获得独占式锁</span><br><span class="line">// 链表是带头节点的表示头节点的下一个结点为第一个结点</span><br><span class="line"> if (p == head && tryAcquire(arg)) {</span><br><span class="line">//队列头指针用指向当前节点</span><br><span class="line"> setHead(node);</span><br><span class="line">//释放前驱节点</span><br><span class="line"> p.next = null; // help GC</span><br><span class="line"> failed = false;</span><br><span class="line"> //没有被中断,返回false,让selfInterrupt();不执行,让线程不被中断</span><br><span class="line"> return interrupted;</span><br><span class="line"> }</span><br><span class="line">// 2.2 获取锁失败,线程进入等待状态等待获取独占式锁</span><br><span class="line"> if (shouldParkAfterFailedAcquire(p, node) &&</span><br><span class="line"> parkAndCheckInterrupt())</span><br><span class="line"> interrupted = true;</span><br><span class="line"> }</span><br><span class="line"> } finally {</span><br><span class="line"> if (failed)</span><br><span class="line"> cancelAcquire(node);</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p><strong>如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前节点所指向的线程能够获取锁</strong>。反之,获取锁失败进入等待状态。</p><p>acquireQueued()在自旋过程中主要完成了两件事情:</p><ol><li><strong>如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁该方法执行结束退出</strong>;</li><li><strong>获取锁失败的话,先将当前结点的前驱结点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞</strong>。</li></ol><h3 id="shouldParkAfterFailedAcquire-源码"><a href="#shouldParkAfterFailedAcquire-源码" class="headerlink" title="shouldParkAfterFailedAcquire()源码"></a>shouldParkAfterFailedAcquire()源码</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line">private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {</span><br><span class="line"> int ws = pred.waitStatus;</span><br><span class="line"> if (ws == Node.SIGNAL)</span><br><span class="line"> // 有个规定,就是当前结点的前驱结点状态一定要是-1,才能将当前结点park,</span><br><span class="line"> // 相当于你告诉你前面的人排到你的时候叫我,我睡个觉</span><br><span class="line"> // 至于为什么前驱结点要是signal,等到release的时候有用</span><br><span class="line"> return true;</span><br><span class="line"> if (ws > 0) {</span><br><span class="line"> //ws大于0只有一个取消状态</span><br><span class="line"> //做个循环将取消结点去掉</span><br><span class="line"> do {</span><br><span class="line"> node.prev = pred = pred.prev;</span><br><span class="line"> } while (pred.waitStatus > 0);</span><br><span class="line"> pred.next = node;</span><br><span class="line"> } else {</span><br><span class="line"> //CAS将前驱结点的ws设置成-1,自旋在aquareQueued里</span><br><span class="line"> compareAndSetWaitStatus(pred, ws, Node.SIGNAL);</span><br><span class="line"> }</span><br><span class="line"> return false;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h3 id="parkAndCheckInterrupt-源码"><a href="#parkAndCheckInterrupt-源码" class="headerlink" title="parkAndCheckInterrupt()源码"></a>parkAndCheckInterrupt()源码</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">private final boolean parkAndCheckInterrupt() {</span><br><span class="line"> //使得该线程阻塞</span><br><span class="line">LockSupport.park(this);</span><br><span class="line"> return Thread.interrupted();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="独占锁的释放"><a href="#独占锁的释放" class="headerlink" title="独占锁的释放"></a>独占锁的释放</h2><h3 id="release-源码"><a href="#release-源码" class="headerlink" title="release()源码"></a>release()源码</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">public final boolean release(int arg) {</span><br><span class="line"> if (tryRelease(arg)) {</span><br><span class="line"> Node h = head;</span><br><span class="line"> if (h != null && h.waitStatus != 0)</span><br><span class="line"> unparkSuccessor(h);</span><br><span class="line"> return true;</span><br><span class="line"> }</span><br><span class="line"> return false;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>如果同步状态释放成功(tryRelease返回true)则会执行if块中的代码,当head指向的头结点不为null,并且该节点的状态值不为0的话才会执行unparkSuccessor()方法。</p><h3 id="unparkSuccessor-源码"><a href="#unparkSuccessor-源码" class="headerlink" title="unparkSuccessor()源码"></a>unparkSuccessor()源码</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br></pre></td><td class="code"><pre><span class="line">private void unparkSuccessor(Node node) {</span><br><span class="line"> /*</span><br><span class="line"> * If status is negative (i.e., possibly needing signal) try</span><br><span class="line"> * to clear in anticipation of signalling. It is OK if this</span><br><span class="line"> * fails or if status is changed by waiting thread.</span><br><span class="line"> */</span><br><span class="line"> // 如果头结点为负数,可能为-1,尝试将其ws设置为0,失败也没关系</span><br><span class="line"> int ws = node.waitStatus;</span><br><span class="line"> if (ws < 0)</span><br><span class="line"> compareAndSetWaitStatus(node, ws, 0);</span><br><span class="line"></span><br><span class="line"> /*</span><br><span class="line"> * Thread to unpark is held in successor, which is normally</span><br><span class="line"> * just the next node. But if cancelled or apparently null,</span><br><span class="line"> * traverse backwards from tail to find the actual</span><br><span class="line"> * non-cancelled successor.</span><br><span class="line"> */</span><br><span class="line"></span><br><span class="line">//向后遍历找到头节点的后继节点,不能为取消结点</span><br><span class="line"> Node s = node.next;</span><br><span class="line"> if (s == null || s.waitStatus > 0) {</span><br><span class="line"> s = null;</span><br><span class="line"> for (Node t = tail; t != null && t != node; t = t.prev)</span><br><span class="line"> if (t.waitStatus <= 0)</span><br><span class="line"> s = t;</span><br><span class="line"> }</span><br><span class="line"> if (s != null)</span><br><span class="line">//后继节点不为null时唤醒该线程</span><br><span class="line"> LockSupport.unpark(s.thread);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>首先获取头节点的后继节点,当后继节点的时候会调用LookSupport.unpark()方法,该方法会唤醒该节点的后继节点所包装的线程。因此,<strong>每一次锁释放后就会唤醒队列中该节点的后继节点所引用的线程,从而进一步可以佐证获得锁的过程是一个FIFO(先进先出)的过程。</strong></p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p><strong>线程获取锁失败,线程被封装成Node进行入队操作,核心方法在于addWaiter()和enq(),同时enq()完成对同步队列的头结点初始化工作以及CAS操作失败的重试</strong>;</p><p><strong>线程获取锁是一个自旋的过程,当且仅当 当前节点的前驱节点是头结点并且成功获得同步状态时,节点出队即该节点引用的线程获得锁,否则,当不满足条件时就会调用LookSupport.park()方法使得线程阻塞</strong>;</p><p><strong>释放锁的时候会唤醒后继节点;</strong></p><p><strong>在获取同步状态时,AQS维护一个同步队列,获取同步状态失败的线程会加入到队列中进行自旋;移除队列(或停止自旋)的条件是前驱节点是头结点并且成功获得了同步状态。在释放同步状态时,同步器会调用unparkSuccessor()方法唤醒后继节点。</strong></p><h2 id="可中断式获取锁"><a href="#可中断式获取锁" class="headerlink" title="可中断式获取锁"></a>可中断式获取锁</h2><p>唯一的区别是当<strong>parkAndCheckInterrupt</strong>返回true时即线程阻塞时该线程被中断,代码抛出被中断异常。而上面只是<code>interrupted = true;</code></p><h2 id="超时等待式获取锁"><a href="#超时等待式获取锁" class="headerlink" title="超时等待式获取锁"></a>超时等待式获取锁</h2><p>和上面类似,每次自旋都会重新计算超时时间</p><h2 id="共享锁"><a href="#共享锁" class="headerlink" title="共享锁"></a>共享锁</h2><p>共享锁的原理和独占锁差不多,区别在于aquire(arg)大于0就能获取锁,每次都是arg–</p><p>释放大概是CAS释放多个线程,保证一定的顺序性</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>三大范式</title>
<link href="/2019/10/04/%E4%B8%89%E5%A4%A7%E8%8C%83%E5%BC%8F/"/>
<url>/2019/10/04/%E4%B8%89%E5%A4%A7%E8%8C%83%E5%BC%8F/</url>
<content type="html"><![CDATA[<h2 id="第一范式(1NF)"><a href="#第一范式(1NF)" class="headerlink" title="第一范式(1NF)"></a>第一范式(1NF)</h2><p>在任何一个关系数据库中,第一范式(1NF)是对关系模式的基本要求,不满足第一范式(1NF)的数据库就不是关系数据库。 </p><p><strong>所谓第一范式(1NF)是指数据库表的每一列都是不可分割的基本数据项</strong>,同一列中不能有多个值,即实体中的某个属性不能有多个值或者不能有重复的属性。如果出现重复的属性,就可能需要定义一个新的实体,新的实体由重复的属性构成,新实体与原实体之间为一对多关系。</p><p>在第一范式(1NF)中表的每一行只包含一个实例的信息。简而言之,第一范式要求数据表中的每一列(每个字段)必须是不可拆分的最小单元。</p><h2 id="第二范式(2NF)"><a href="#第二范式(2NF)" class="headerlink" title="第二范式(2NF)"></a>第二范式(2NF)</h2><p>第二范式(2NF)是在第一范式(1NF)的基础上建立起来的,即满足第二范式(2NF)必须先满足第一范式(1NF)。第二范式(2NF)要求数据库表中的每个实例或行必须可以被惟一地区分。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。</p><p><strong>第二范式(2NF)要求实体的属性完全依赖于主关键字。</strong>所谓完全依赖是指<strong>不能存在仅依赖主关键字一部分的属性</strong>,如果存在,那么这个属性和主关键字的这一部分应该分离出来形成一个新的实体,新实体与原实体之间是一对多的关系。为实现区分通常需要为表加上一个列,以存储各个实例的惟一标识。简而言之,<strong>第二范式要求表中的所有列,都必须依赖于主键,而不能有任何一列与主键没有关系。</strong></p><h2 id="第三范式(3NF)"><a href="#第三范式(3NF)" class="headerlink" title="第三范式(3NF)"></a>第三范式(3NF)</h2><p>满足第三范式(3NF)必须先满足第二范式(2NF)。第三范式(3NF)要求一个数据库表中不包含其它表中已包含的非主关键字信息。简而言之,<strong>第三范式要求表中的每一列只与主键直接相关而不是间接相关,表中的每一列只能依赖于主键。</strong>即不存在依赖传递</p>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>SYN Flood攻击与防御</title>
<link href="/2019/10/04/SYN-Flood%E6%94%BB%E5%87%BB%E4%B8%8E%E9%98%B2%E5%BE%A1/"/>
<url>/2019/10/04/SYN-Flood%E6%94%BB%E5%87%BB%E4%B8%8E%E9%98%B2%E5%BE%A1/</url>
<content type="html"><![CDATA[<h2 id="Syn-Flood攻击"><a href="#Syn-Flood攻击" class="headerlink" title="Syn Flood攻击"></a><strong>Syn Flood攻击</strong></h2><p>当开放了一个TCP端口后,该端口就处于Listening状态,不停地监视发到该端口的SYN报文,一旦接收到Client发来的SYN报文,就需要为该请求分配一个TCB(Transmission Control Block),通常一个TCB至少需要280个字节,在某些操作系统中TCB甚至需要1300个字节,并返回一个SYN+ACK报文,立即转为SYN-RECEIVED即半开连接状态,而某些操作系统在SOCKT的实现上最多可开启512个半开连接(如Linux2.4.20 内核)。</p><img src="/2019/10/04/SYN-Flood攻击与防御/20170803194758556.jpg"><p>如果<strong>恶意的向某个服务器端口发送大量的SYN包,则可以使服务器打开大量的半开连接,分配TCB,从而消耗大量的服务器资源,同时也使得正常的连接请求无法被相应。</strong>而攻击发起方的资源消耗相比较可忽略不计。</p><h2 id="SYN-Flood种类"><a href="#SYN-Flood种类" class="headerlink" title="SYN Flood种类"></a>SYN Flood种类</h2><img src="/2019/10/04/SYN-Flood攻击与防御/20170803194856319.jpg"><p>1.Direct Attack 攻击方<strong>使用固定的源地址发起攻击</strong>,这种方法对攻击方的消耗最小</p><p>2.Spoofing Attack 攻击方使用<strong>变化的源地址发起攻击</strong>,这种方法需要攻击方不停地修改源地址,实际上消耗也不大</p><p>3.Distributed Direct Attack 这种攻击主要是<strong>使用僵尸网络进行固定源地址的攻击</strong></p><h2 id="如何防御"><a href="#如何防御" class="headerlink" title="如何防御"></a>如何防御</h2><p>对于第一种攻击的防范可以使用比较简单的方法,即对SYN包进行监视,如果发现某个IP发起了较多的攻击报文,直接将这个IP列入黑名单即可。</p><p>对于源地址不停变化的攻击使用上述方法则不行,首先从某一个被伪装的IP过来的Syn报文可能不会太多,达不到被拒绝的阈值</p><h3 id="无效连接监视释放"><a href="#无效连接监视释放" class="headerlink" title="无效连接监视释放"></a>无效连接监视释放</h3><p>这种方法<strong>不停监视系统的半开连接和不活动连接,当达到一定阈值时拆除这些连接,从而释放系统资源。</strong>这种方法对于所有的连接一视同仁,而且如果SYN Flood造成的半开连接数量很大,<strong>正常连接请求也被淹没在其中被这种方式误释放掉</strong>,因此这种方法属于入门级的SYN Flood方法。</p><h3 id="延缓TCB分配方法"><a href="#延缓TCB分配方法" class="headerlink" title="延缓TCB分配方法"></a>延缓TCB分配方法</h3><p>从前面SYN Flood原理可以看到,消耗服务器资源主要是因为当SYN数据报文一到达,系统立即分配TCB,从而占用了资源。而SYN Flood由于很难建立起正常连接,因此,<strong>当正常连接建立起来后再分配TCB则可以有效地减轻服务器资源的消耗。常见的方法是使用SYN Cache和SYN Cookie技术。</strong></p><h4 id="SYN-Cache技术"><a href="#SYN-Cache技术" class="headerlink" title="SYN Cache技术"></a>SYN Cache技术</h4><p>这种技术是在收到SYN数据报文时不急于去分配TCB,而是<strong>先回应一个SYN ACK报文,并在一个专用HASH表(Cache)中保存这种半开连接信息</strong>,<strong>直到收到正确的回应ACK报文再分配TCB</strong>。在FreeBSD系统中这种Cache每个半开连接只需使用160字节,远小于TCB所需的736个字节。在发送的SYN ACK中需要使用一个己方的Sequence Number,这个数字不能被对方猜到,否则对于某些稍微智能一点的SYNFlood攻击软件来说,它们在发送SYN报文后会发送一个ACK报文,如果己方的Sequence Number被对方猜测到,则会被其建立起真正的连接。<strong>因此一般采用一些加密算法生成难于预测的Sequence Number。</strong></p><h4 id="SYN-Cookie技术"><a href="#SYN-Cookie技术" class="headerlink" title="SYN Cookie技术"></a>SYN Cookie技术</h4><p>对于SYN攻击,SYN Cache虽然不分配TCB,但是为了判断后续对方发来的ACK报文中的Sequence Number的正确性,还是需要使用一些空间去保存己方生成的Sequence Number等信息,也造成了一些资源的浪费。<strong>Syn Cookie技术则完全不使用任何存储资源</strong>,这种方法比较巧妙,它使用一种<strong>特殊的算法生成Sequence Number,这种算法考虑到了对方的IP、端口、己方IP、端口的固定信息,以及对方无法知道而己方比较固定的一些信息,如MSS、时间等</strong>,在收到对方的ACK报文后,重新计算一遍,看其是否与对方回应报文中的(SequenceNumber-1)相同,从而决定是否分配TCB资源。</p><h3 id="使用SYN-Proxy防火墙"><a href="#使用SYN-Proxy防火墙" class="headerlink" title="使用SYN Proxy防火墙"></a>使用SYN Proxy防火墙</h3><p>SYN Cache技术和SYN Cookie技术总的来说是一种主机保护技术,需要系统的TCP/IP协议栈的支持,而目前并非所有的操作系统支持这些技术。因此很多防火墙中都提供一种SYN代理的功能,其主要原理是对试图穿越的SYN请求进行验证后才放行,下图描述了这种过程</p><img src="/2019/10/04/SYN-Flood攻击与防御/20170803194933743.jpg"><p>从上图(左图)中可以看出,防火墙在确认了连接的有效性后,才向内部的服务器(Listener)发起SYN请求,在右图中,所有的无效连接均无法到达内部的服务器。而防火墙采用的验证连接有效性的方法则可以是SYN Cookie或SYN Cache等其他技术。采用这种方式进行防范需要注意的一点就是<strong>防火墙需要对整个有效连接的过程发生的数据包进行代理</strong></p><p>这种方法在验证了连接之后立即发出一个Safe Reset命令包,从而使得Client重新进行连接,这时出现的Syn报文防火墙就直接放行。在这种方式中,防火墙就不需要对通过防火墙的数据报文进行序列号的修改了。这需要客户端的TCP协议栈支持RFC 793中的相关约定,同时<strong>由于Client需要两次握手过程,连接建立的时间将有所延长</strong>。</p>]]></content>
<tags>
<tag> 计算机网络 </tag>
</tags>
</entry>
<entry>
<title>count</title>
<link href="/2019/10/04/count/"/>
<url>/2019/10/04/count/</url>
<content type="html"><![CDATA[<h2 id="count-1-and-count"><a href="#count-1-and-count" class="headerlink" title="count(1) and count(*)"></a>count(1) and count(*)</h2><p>从执行计划来看,count(1)和count(*)的效果是一样的。</p><p>count(1) 中的 1 是恒真表达式,那么 count(*) 还是 count(1) <strong>都是对所有的结果集进行 count</strong>,所以他们<strong>本质上没有什么区别。</strong></p><p>当然这个地方 InnoDB 本身也做了一些优化,<strong>它会使用最小的二级索引来进行 <code>count</code> 的查询优化。</strong>如果没有二级索引才会选择聚簇索引,这样的设计单从 IO 的角度就节省了很多开销。</p><h2 id="count-、count-1-和-count-列名"><a href="#count-、count-1-和-count-列名" class="headerlink" title="count(*)、count(1) 和 count(列名)"></a>count(*)、count(1) 和 count(列名)</h2><p>count(column) 也是会遍历整张表,但是不同的是它会拿到 column 的值以后判断是否为空,然后再进行累加,那么如果针对主键需要解析内容,如果是<strong>二级索引需要再次根据主键获取内容</strong>,又是一次 IO 操作,所以 count(column) 的性能肯定不如前两者喽,如果按照效率比较的话:<code>count(*)=count(1)>count(primary key)>count(column)</code></p>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>水平触发和边沿触发</title>
<link href="/2019/10/04/%E6%B0%B4%E5%B9%B3%E8%A7%A6%E5%8F%91%E5%92%8C%E8%BE%B9%E6%B2%BF%E8%A7%A6%E5%8F%91/"/>
<url>/2019/10/04/%E6%B0%B4%E5%B9%B3%E8%A7%A6%E5%8F%91%E5%92%8C%E8%BE%B9%E6%B2%BF%E8%A7%A6%E5%8F%91/</url>
<content type="html"><![CDATA[<h2 id="ET-vs-LT-概念"><a href="#ET-vs-LT-概念" class="headerlink" title="ET vs LT - 概念"></a><strong>ET vs LT - 概念</strong></h2><ul><li>Edge Triggered (ET) 边沿触发</li></ul><ol><li>socket的接收缓冲区状态变化时触发读事件,即<strong>空的接收缓冲区刚接收到数据时触发读事件</strong></li><li>socket的发送缓冲区状态变化时触发写事件,即<strong>满的缓冲区刚空出空间时触发读事件</strong></li></ol><p>仅在缓冲区状态变化时触发事件,比如<strong>数据缓冲区从无到有</strong>的时候(不可读-可读)</p><ul><li>Level Triggered (LT) 水平触发</li></ul><ol><li>socket<strong>接收缓冲区不为空,有数据可读,则读事件一直触发</strong></li><li>socket<strong>发送缓冲区不满可以继续写入数据,则写事件一直触发</strong></li></ol><p>符合思维习惯,epoll_wait返回的事件就是socket的状态</p><p><strong>挂在ready_list上的sk什么时候会被移除掉呢?</strong></p><p>对于Edge Triggered (ET) 边沿触发:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">[<span class="number">5</span>] 遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件</span><br></pre></td></tr></table></figure><p>对于Level Triggered (LT) 水平触发:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[<span class="number">5.1</span>] 遍历epoll的ready_list,将sk从ready_list中移除,然后调用该sk的poll逻辑收集发生的事件</span><br><span class="line">[<span class="number">5.2</span>] 如果该sk的poll函数返回了关心的事件(对于可读事件来说,就是POLL_IN事件),那么该sk被重新加入到epoll的ready_list中。</span><br></pre></td></tr></table></figure><p>对于可读事件而言,在ET模式下,如果某个socket有新的数据到达,那么该sk就会被排入epoll的ready_list,从而epoll_wait就一定能收到可读事件的通知(调用sk的poll逻辑一定能收集到可读事件)。于是,我们通常理解的缓冲区状态变化(从无到有)的理解是不准确的,准确的理解应该是是否有新的数据达到缓冲区。</p><p>而在LT模式下,某个sk被探测到有数据可读,那么该sk会被重新加入到ready_list,那么在该sk的数据被全部取走前,下次调用epoll_wait就一定能够收到该sk的可读事件(调用sk的poll逻辑一定能收集到可读事件),从而epoll_wait就能返回。</p><h2 id="ET-vs-LT-性能"><a href="#ET-vs-LT-性能" class="headerlink" title="ET vs LT - 性能"></a><strong>ET vs LT - 性能</strong></h2><p>对于可读事件而言,LT比ET多了两个操作:(1)对ready_list的遍历的时候,对于收集到可读事件的sk会<strong>重新放入ready_list</strong>;(2)下次epoll_wait的时候会<strong>再次遍历上次重新放入</strong>的sk,如果sk本身没有数据可读了,那么这次遍历就变得多余了。</p><p>在服务端有海量活跃socket的时候,LT模式下,epoll_wait返回的时候,会有海量的socket<br>sk重新放入ready_list。如果,用户在第一次epoll_wait返回的时候,将有数据的socket都处理掉了,那么下次epoll_wait的时候,上次epoll_wait重新入ready_list的sk被再次遍历就有点多余,这个时候LT确实会带来一些性能损失。</p><p>先不说第一次epoll_wait返回的时候,用户进程能否都将有数据返回的socket处理掉。在用户处理的过程中,如果该socket有新的数据上来,那么协议栈发现sk已经在ready_list中了,那么就不需要再次放入ready_list,也就是在LT模式下,对该sk的再次遍历不是多余的,是有效的。同时,我们回归epoll高效的场景在于,服务器有海量socket,但是活跃socket较少的情况下才会体现出epoll的高效、高性能。因此,在实际的应用场合,绝大多数情况下,ET模式在性能上并不会比LT模式具有压倒性的优势,至少,<strong>目前还没有实际应用场合的测试表明ET比LT性能更好。</strong></p><h2 id="ET-vs-LT-复杂度"><a href="#ET-vs-LT-复杂度" class="headerlink" title="ET vs LT - 复杂度"></a><strong>ET vs LT - 复杂度</strong></h2><p>我们知道,对于可读事件而言,在阻塞模式下,是无法识别队列空的事件的,并且,<strong>事件通知机制,仅仅是通知有数据,并不会通知有多少数据。</strong>于是,在阻塞模式下,在epoll_wait返回的时候,我们对某个socket_fd调用recv或read读取并返回了一些数据的时候,我们不能再次直接调用recv或read,因为,如果socket_fd已经无数据可读的时候,进程就会阻塞在该socket_fd的recv或read调用上,这样就影响了IO多路复用的逻辑(<strong>我们希望是阻塞在所有被监控socket的epoll_wait调用上,而不是单独某个socket_fd上</strong>),<strong>造成其他socket饿死,即使有数据来了,也无法处理。</strong></p><p>接下来,我们只能再次调用epoll_wait来探测一些socket_fd,看是否还有数据可读。<strong>在LT模式下,如果socket_fd还有数据可读,那么epoll_wait就一定能够返回,接着,我们就可以对该socket_fd调用recv或read读取数据。</strong>然而,<strong>在ET模式下,尽管socket_fd还是数据可读,但是如果没有新的数据上来,那么epoll_wait是不会通知可读事件的。</strong>这个时候,epoll_wait阻塞住了,这下子坑爹了,明明有数据你不处理,非要等新的数据来了在处理,那么我们就死扛咯,看谁先忍不住。</p><p>等等,在阻塞模式下,不是不能用ET的么?是的,正是因为有这样的缺点,<strong>ET强制需要在非阻塞模式下使用</strong>。<strong>在ET模式下,epoll_wait返回socket_fd有数据可读,我们必须要读完所有数据才能离开。因为,如果不读完,epoll不会在通知你了,虽然有新的数据到来的时候,会再次通知,但是我们并不知道新数据会不会来,以及什么时候会来。</strong>由于在阻塞模式下,我们是无法通过recv/read来探测空数据事件,于是,我们必须采用非阻塞模式,一直read直到EAGAIN。因此,ET要求socket_fd非阻塞也就不难理解了。</p><p>另外,epoll_wait原本的语意是:<strong>监控并探测socket是否有数据可读</strong>(对于读事件而言)。<strong>LT模式</strong>保留了其原本的语意,<strong>只要socket还有数据可读,它就能不断反馈</strong>,于是,我们想什么时候读取处理都可以,我们永远有再次poll的机会去探测是否有数据可以处理,这样带来了编程上的很大方便,不容易死锁造成某些socket饿死。相反,<strong>ET模式修改了epoll_wait原本的语意,变成了:监控并探测socket是否有新的数据可读。</strong></p><p>于是,<strong>在epoll_wait返回socket_fd可读的时候,我们需要小心处理,要不然会造成死锁和socket饿死现象。</strong>典型如listen_fd返回可读的时候,我们需要不断的accept直到EAGAIN。假设同时有三个请求到达,epoll_wait返回listen_fd可读,这个时候,如果仅仅accept一次拿走一个请求去处理,那么就会留下两个请求,如果这个时候一直没有新的请求到达,那么再次调用epoll_wait是不会通知listen_fd可读的,于是epoll_wait只能睡眠到超时才返回,遗留下来的两个请求一直得不到处理,处于饿死状态。</p><h2 id="ET-vs-LT-总结"><a href="#ET-vs-LT-总结" class="headerlink" title="ET vs LT - 总结"></a><strong>ET vs LT - 总结</strong></h2><ul><li>ET - 对于读操作</li></ul><p>[1] 当接收缓冲buffer内待读数据增加的时候时候(由空变为不空的时候、或者有新的数据进入缓冲buffer)</p><p>[2] 调用epoll_ctl(EPOLL_CTL_MOD)来改变socket_fd的监控事件,也就是重新mod socket_fd的EPOLLIN事件,并且接收缓冲buffer内还有数据没读取。(这里不能是EPOLL_CTL_ADD的原因是,epoll不允许重复ADD的,除非先DEL了,再ADD) 因为epoll_ctl(ADD或MOD)会调用sk的poll逻辑来检查是否有关心的事件,如果有,就会将该sk加入到epoll的ready_list中,下次调用epoll_wait的时候,就会遍历到该sk,然后会重新收集到关心的事件返回。</p><ul><li>ET - 对于写操作</li></ul><p>[1] 发送缓冲buffer内待发送的数据减少的时候(由满状态变为不满状态的时候、或者有部分数据被发出去的时候) [2] 调用epoll_ctl(EPOLL_CTL_MOD)来改变socket_fd的监控事件,也就是重新mod socket_fd的EPOLLOUT事件,并且发送缓冲buffer还没满的时候。</p><ul><li>LT - 对于读操作 LT就简单多了,唯一的条件就是,接收缓冲buffer内有可读数据的时候</li><li>LT - 对于写操作 LT就简单多了,唯一的条件就是,发送缓冲buffer还没满的时候</li></ul><p>在绝大多少情况下,ET模式并不会比LT模式更为高效,同时,ET模式带来了不好理解的语意,这样容易造成编程上面的复杂逻辑和坑点。因此,建议还是采用LT模式来编程更为舒爽。</p>]]></content>
<tags>
<tag> epoll </tag>
</tags>
</entry>
<entry>
<title>tcp粘包问题</title>
<link href="/2019/10/04/tcp%E7%B2%98%E5%8C%85%E9%97%AE%E9%A2%98/"/>
<url>/2019/10/04/tcp%E7%B2%98%E5%8C%85%E9%97%AE%E9%A2%98/</url>
<content type="html"><![CDATA[<h2 id="粘包怎么出现"><a href="#粘包怎么出现" class="headerlink" title="粘包怎么出现"></a>粘包怎么出现</h2><p><strong>采用TCP协议进行网络数据传送的软件设计中,普遍存在粘包问题。</strong>这主要是由于现代操作系统的网络传输机制所产生的。我们知道,网络通信采用的套接字(socket)技术,其实现实际是由系统内核提供一片连续缓存(流缓冲)来实现应用层程序与网卡接口之间的中转功能。<strong>多个数据包被连续存储于连续的缓存中,在对数据包进行读取时由于无法确定发生方的发送边界,而采用某一估测值大小来进行数据读出,若双方的size不一致时就会使数据包的边界发生错位,导致读出错误的数据分包,进而曲解原始数据含义。</strong></p><h2 id="解决方案"><a href="#解决方案" class="headerlink" title="解决方案"></a>解决方案</h2><h3 id="定长发送"><a href="#定长发送" class="headerlink" title="定长发送"></a>定长发送</h3><p>在进行数据发送时采用固定长度的设计,也就是<strong>无论多大数据发送都分包为固定长度</strong>(为便于描述,此处定长为记为LEN),也就是<strong>发送端在发送数据时都以LEN为长度进行分包。这样接收方都以固定的LEN进行接收</strong>,如此一来发送和接收就能一一对应了。<strong>分包的时候不一定能完整的恰好分成多个完整的LEN的包</strong>,最后一个包一般都会小于LEN,这时候最后一个包可以在<strong>不足的部分填充空白字节</strong>。</p><h4 id="缺陷"><a href="#缺陷" class="headerlink" title="缺陷"></a>缺陷</h4><ol><li><strong>最后一个包的不足长度被填充为空白部分,也即无效字节序。</strong>那么接收方可能难以辨别这无效的部分,它本身就是为了补位的,并无实际含义。这就为<strong>接收端处理其含义带来了麻烦</strong>。当然也有解决办法,<strong>可以通过增添标志位的方法来弥补,即在每一个数据包的最前面增加一个定长的报头,然后将该数据包的末尾标记一并发送。接收方根据这个标记确认无效字节序列</strong>,从而实现数据的完整接收。</li><li><strong>在发送包长度随机分布的情况下,会造成带宽浪费。</strong>比如发送长度可能为<br>1,100,1000,4000字节等等,则都需要按照定长最大值即4000来发送,<strong>数据包小于4000字节的其他包也会被填充至4000,造成网络负载的无效浪费。</strong></li></ol><h3 id="尾部标记序列"><a href="#尾部标记序列" class="headerlink" title="尾部标记序列"></a>尾部标记序列</h3><p>在每个要发送的<strong>数据包的尾部设置一个特殊的字节序列</strong>,此序列带有特殊含义,跟字符串的结束符标识”\0”一样的含义,用来标示这个数据包的末尾,接收方可对接收的数据进行分析,通过尾部序列确认数据包的边界。</p><h4 id="缺陷-1"><a href="#缺陷-1" class="headerlink" title="缺陷"></a>缺陷</h4><ol><li>接收方需要对数据进行分析,甄别尾部序列。</li><li>尾部序列的确定本身是一个问题。什么样的序列可以像”\0”一样来做一个结束符呢?这个序列必须是不具备通常任何人类或者程序可识别的带含义的数据序列,就像“\0”是一个无效字符串内容,因而可以作为字符串的结束标记。</li></ol><h3 id="头部标记分步接收"><a href="#头部标记分步接收" class="headerlink" title="头部标记分步接收"></a>头部标记分步接收</h3><p>既不损失效率,还完美解决了任何大小的数据包的边界问题。</p><p>定义一个用户报头,在报头中注明每次发送的数据包大小。接收方每次接收时先以报头的size进行数据读取,这必然只能读到一个报头的数据,从报头中得到该数据包的数据大小,然后再按照此大小进行再次读取,就能读到数据的内容了。这样一来,<strong>每个数据包发送时都封装一个报头,然后接收方分两次接收一个包,第一次接收报头,根据报头大小第二次才接收数据内容。</strong></p><h4 id="缺陷-2"><a href="#缺陷-2" class="headerlink" title="缺陷"></a>缺陷</h4><ol><li>报头虽小,但每个包都需要多封装sizeof(_data_head)的数据,积累效应也不可完全忽略。</li><li>接收方的接收动作分成了两次,也就是进行<strong>数据读取的操作被增加了一倍</strong>,而数据读取操作的recv或者read都是系统调用,这<strong>对内核而言的开销是一个不能完全忽略的影响</strong>,对程序而言性能影响可忽略(系统调用的速度非常快)。</li></ol>]]></content>
<tags>
<tag> 计算机网络 </tag>
</tags>
</entry>
<entry>
<title>NIO</title>
<link href="/2019/10/04/NIO/"/>
<url>/2019/10/04/NIO/</url>
<content type="html"><![CDATA[<h2 id="select-和-epoll底层实现原理"><a href="#select-和-epoll底层实现原理" class="headerlink" title="select 和 epoll底层实现原理"></a>select 和 epoll底层实现原理</h2><h3 id="内核接受网卡流量的整个流程"><a href="#内核接受网卡流量的整个流程" class="headerlink" title="内核接受网卡流量的整个流程"></a>内核接受网卡流量的整个流程</h3><ul><li>网络编程的核心对象是socket,当创建socket时在底层会创建一个由文件系统管理的socket对象。这个对象包括了发送缓冲区,接收缓冲区,<strong>等待队列</strong>。</li><li>recv函数用于从<strong>某一个socket</strong>中接受流量,但是<strong>这个函数在被调用入进程会一直处于阻塞状态,直到从该socket收到数据为止。</strong></li></ul><p>网卡接收流量的流程:</p><ul><li>步骤一:进程中调用了recv函数请求接收指定socket的流量。</li><li>步骤二:<strong>操作系统将这个进程加入到对应socket的等待队列中,</strong>并从CPU工作队列中移除,经过这一步后,<strong>进程会处于阻塞状态。</strong></li><li>计算机接收到对端传输的数据,网卡把数据写入到内存中。这一步不需要CPU参与(数据不经过CPU直接从IO设备写入内存的技术叫作DMA技术)</li><li>数据写完后网卡会发送一个中断信号,中断CPU,通知CPU有数据到达。</li><li>CPU中断程序响应中断,并把内存中的数据写入了对应socket的缓冲区里</li><li>CPU唤醒进程,把进程从socket的等待队列中移除,然后加入到工作队列等待系统调用。</li></ul><p>上面的流程有一个问题:<br> <strong>revc函数只能监控一个socket,并且会导致进程一直阻塞在这个socket中,直到socket中有数据返回为止。如果有多个socket监控,则需要创建多个进程,非常浪费资源。</strong></p><p>而select和epoll就解决了上面的问题,它们让<strong>一个进程可以监控多个socket</strong>,下面分别说一下两个函数的实现细节。</p><h2 id="select的实现细节"><a href="#select的实现细节" class="headerlink" title="select的实现细节"></a>select的实现细节</h2><p>select一次监控多个socket的原理很简单:</p><ul><li><strong>它会把进程加入到它需要监控的所有socket的等待队列中,然后将进程从CPU 工作队列中移除,进入阻塞状态。</strong></li><li><strong>当这些socket中有一个socket有数据返回时,中断程序会把进程从socket等待队列中移除,并把进程重新加入到CPU工作队列中,让进程进就绪状态。</strong></li><li>进程进入被CPU调用到,只需要<strong>遍历所有socket的状态</strong>,就可以知道哪些socket可以读取数据了。</li><li>操作完这些可读取数据的socket之后,又会重复第一步,把进程加入到所有的 socket中,然后让进程进入阻塞状态。</li></ul><p>select函数实现了在一个进程中监控多个socket的方法。但是这函数的性能并不高,因为它需要重复把进程从所有的socket中加入/移除。因此它监控的socket数量不能太多,底层规定<strong>不能超过1024个。</strong></p><h2 id="epoll函数的实现细节"><a href="#epoll函数的实现细节" class="headerlink" title="epoll函数的实现细节"></a>epoll函数的实现细节</h2><p>当进程调用epoll监控多个socket时,会在底层创建一个eventpoll对象,这个对象中包含一个重要的队列:<strong>就绪队列</strong></p><ul><li>进程调用epoll函数后,epoll会把这个进程加入<strong>eventpoll对象</strong>的等待队列中</li><li>然后把eventpoll对象加入到所有socket的等待队列中,并让CPU阻塞住</li><li>当某一个socket有数据返回时,CPU中断程序会把这个socket加入到eventpoll对象的就绪队列中,并把eventpoll中等待的进程唤醒。</li><li>进程被唤醒后直接从就绪队列中获取socket读取数据</li><li>数据读取完成后,epoll又会把进程加入到eventpoll的等待队列中,然后让CPU阻塞住。</li></ul><p>epoll针对select优化的点:</p><ul><li>除了第一次外,epoll不需操作所有socket对象的等待队列,只需要操作eventpoll的等待队列即可</li><li>进程被唤醒后,不需要遍历即可直接知道哪些socket准备好了。</li></ul><h2 id="Linux的socket-事件wakeup-callback机制"><a href="#Linux的socket-事件wakeup-callback机制" class="headerlink" title="Linux的socket 事件wakeup callback机制"></a>Linux的socket 事件wakeup callback机制</h2><p>在介绍select、poll、epoll前,有必要说说linux(2.6+)内核的事件wakeup callback机制,这是IO多路复用机制存在的本质。<strong>Linux通过socket睡眠队列来管理所有等待socket的某个事件的process,同时通过wakeup机制来异步唤醒整个睡眠队列上等待事件的process,通知process相关事件发生。</strong>通常情况,<strong>socket的事件发生的时候,其会顺序遍历socket睡眠队列上的每个process节点,调用每个process节点挂载的callback函数。</strong>在遍历的过程中,如果遇到某个节点是排他的,那么就终止遍历,总体上会涉及两大逻辑:(1)睡眠等待逻辑;(2)唤醒逻辑。</p><ul><li>睡眠等待逻辑:涉及select、poll、epoll_wait的阻塞等待逻辑</li></ul><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></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>]select、poll、epoll_wait陷入内核,判断监控的socket是否有关心的事件发生了,如果没,则为当前process构建一个wait_entry节点,然后插入到监控socket的sleep_list</span><br><span class="line">[<span class="number">2</span>]进入循环的schedule直到关心的事件发生了</span><br><span class="line">[<span class="number">3</span>]关心的事件发生后,将当前process的wait_entry节点从socket的sleep_list中删除</span><br></pre></td></tr></table></figure><ul><li>唤醒逻辑</li></ul><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></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>]socket的事件发生了,然后socket顺序遍历其睡眠队列,依次调用每个wait_entry节点的callback函数</span><br><span class="line">[<span class="number">2</span>]直到完成队列的遍历或遇到某个wait_entry节点是排他的才停止。</span><br><span class="line">[<span class="number">3</span>]一般情况下callback包含两个逻辑:<span class="number">1.</span>wait_entry自定义的私有逻辑;<span class="number">2.</span>唤醒的公共逻辑,主要用于将该wait_entry的process放入CPU的就绪队列,让CPU随后可以调度其执行。</span><br></pre></td></tr></table></figure><p>下面就上面的两大逻辑,分别阐述select、poll、epoll的异同,为什么epoll能够比select、poll高效。</p><h2 id="Select—1024"><a href="#Select—1024" class="headerlink" title="Select—1024"></a>Select—1024</h2><p>在一个高性能的网络服务上,大多情况下一个服务进程(线程)process需要同时处理多个socket,我们需要公平对待所有socket,对于read而言,那个socket有数据可读,process就去读取该socket的数据来处理。于是对于read,一个朴素的需求就是<strong>关心的N个socket是否有数据”可读”</strong>,也就是我们期待”可读”事件的通知,而不是盲目地对每个socket调用recv/recvfrom来尝试接收数据。我们<strong>应该block在等待事件的发生上,这个事件简单点就是”关心的N个socket中一个或多个socket有数据可读了”,当block解除的时候,就意味着,我们一定可以找到一个或多个socket上有可读的数据。</strong>另一方面,根据上面的socket wakeup callback机制,我们不知道什么时候,哪个socket会有读事件发生,于是,<strong>process需要同时插入到这N个socket的sleep_list上等待任意一个socket可读事件发生而被唤醒,当时process被唤醒的时候,其callback里面应该有个逻辑去检查具体那些socket可读了。</strong></p><p>于是,select的多路复用逻辑就清晰了,select为每个socket引入一个poll逻辑,该poll逻辑用于收集socket发生的事件,对于可读事件来说,简单伪码如下:</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><span class="line">poll()</span><br><span class="line">{</span><br><span class="line"> <span class="comment">//其他逻辑</span></span><br><span class="line"> <span class="keyword">if</span> (recieve queque is not empty)</span><br><span class="line"> {</span><br><span class="line"> sk_event |= POLL_IN;</span><br><span class="line"> }</span><br><span class="line"> <span class="comment">//其他逻辑</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>接下来就到select的逻辑了,下面是select的函数原型:5个参数,后面4个参数都是in/out类型(值可能会被修改返回)</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);</span><br></pre></td></tr></table></figure><p>当用户process调用select的时候,select会将需要监控的readfds集合拷贝到内核空间(假设监控的仅仅是socket可读),然后<strong>遍历自己监控的socket sk,挨个调用sk的poll逻辑以便检查该sk是否有可读事件</strong>,遍历完所有的sk后,<strong>如果没有任何一个sk可读,那么select会调用schedule_timeout进入schedule循环,使得process进入睡眠。</strong>如果在timeout时间内某个sk上有数据可读了,或者等待timeout了,则调用select的process会被唤醒,接下来select就是遍历监控的sk集合,挨个收集可读事件并返回给用户了,相应的伪码如下:</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></pre></td><td class="code"><pre><span class="line"><span class="keyword">for</span> (sk <span class="keyword">in</span> readfds)</span><br><span class="line">{</span><br><span class="line"> sk_event.evt = sk.poll();</span><br><span class="line"> sk_event.sk = sk;</span><br><span class="line"> ret_event_for_process;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>通过上面的select逻辑过程分析,相信大家都意识到,select存在两个问题</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></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>] 被监控的fds需要从用户空间拷贝到内核空间</span><br><span class="line"> 为了减少数据拷贝带来的性能损坏,内核对被监控的fds集合大小做了限制,并且这个是通过宏控制的,大小不可改变(限制为<span class="number">1024</span>)。</span><br><span class="line">[<span class="number">2</span>] 被监控的fds集合中,只要有一个有数据可读,整个socket集合就会被遍历一次调用sk的poll函数收集可读事件</span><br><span class="line"> 由于当初的需求是朴素,仅仅关心是否有数据可读这样一个事件,当事件通知来的时候,由于数据的到来是异步的,我们不知道事件来的时候,有多少个被监控的socket有数据可读了,于是,只能挨个遍历每个socket来收集可读事件。</span><br></pre></td></tr></table></figure><p>到这里,我们有三个问题需要解决:</p><p>(1)被监控的fds集合限制为1024,1024太小了,我们希望能够有个比较大的可监控fds集合 </p><p>(2)fds集合需要从用户空间拷贝到内核空间的问题,我们希望不需要拷贝 </p><p>(3)当被监控的fds中某些有数据可读的时候,我们希望通知更加精细一点,就是我们希望能够从通知中得到有可读事件的fds列表,而不是需要遍历整个fds来收集。</p><blockquote><p>我的理解:进程要监听多个socket,调用select方法,会用自己构建出wait_entry并且排队到socket的等待队列中,最多能监听1024个,并且遍历后阻塞,当socket有事件产生,会唤醒响应等待队列中的wait_entry,也就是进程,进程会被回调遍历监听的所有socket,也可以说是所有文件描述符。并返回数据。</p></blockquote><h2 id="poll—鸡肋"><a href="#poll—鸡肋" class="headerlink" title="poll—鸡肋"></a>poll—鸡肋</h2><p>select遗留的三个问题中,问题(1)是用法限制问题,问题(2)和(3)则是性能问题。poll和select非常相似,poll并没着手解决性能问题,<strong>poll只是解决了select的问题(1)fds集合大小1024限制问题。</strong>下面是poll的函数原型,poll改变了fds集合的描述方式,使用了pollfd结构而不是select的fd_set结构,使得poll支持的fds集合限制远大于select的1024。poll虽然解决了fds集合大小1024的限制问题,但是,它并没改变大量描述符数组被整体复制于用户态和内核态的地址空间之间,以及个别描述符就绪触发整体描述符集合的遍历的低效问题。poll随着监控的socket集合的增加性能线性下降,poll不适合用于大并发场景。</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">int poll(struct pollfd *fds, nfds_t nfds, int timeout);</span><br></pre></td></tr></table></figure><h2 id="epoll—终极"><a href="#epoll—终极" class="headerlink" title="epoll—终极"></a>epoll—终极</h2><h3 id="fds集合拷贝问题的解决"><a href="#fds集合拷贝问题的解决" class="headerlink" title="fds集合拷贝问题的解决"></a>fds集合拷贝问题的解决</h3><p>对于IO多路复用,有两件事是必须要做的(对于监控可读事件而言):<strong>1. 准备好需要监控的fds集合;2. 探测并返回fds集合中哪些fd可读了。</strong>细看select或poll的函数原型,我们会发现,每次调用select或poll都在重复地准备(集中处理)整个需要监控的fds集合。然而对于频繁调用的select或poll而言,fds集合的变化频率要低得多,我们没必要每次都重新准备(集中处理)整个fds集合。</p><p>于是,epoll引入了epoll_ctl系统调用,<strong>将高频调用的epoll_wait和低频的epoll_ctl隔离开。</strong>同时,epoll_ctl通过(EPOLL_CTL_ADD、EPOLL_CTL_MOD、EPOLL_CTL_DEL)三个操作来分散对需要监控的fds集合的修改,做到了有变化才变更,将select或poll高频、大块内存拷贝(集中处理)变成epoll_ctl的低频、小块内存的拷贝(分散处理),避免了大量的内存拷贝。同时,对于高频epoll_wait的可读就绪的fd集合返回的拷贝问题,epoll通过内核与用户空间mmap(内存映射)同一块内存来解决。<strong>mmap将用户空间的一块地址和内核空间的一块地址同时映射到相同的一块物理内存地址</strong>(不管是用户空间还是内核空间都是虚拟地址,最终要通过地址映射映射到物理地址),使得这块物理内存对内核和对用户均可见,<strong>减少用户态和内核态之间的数据交换。</strong></p><p>另外,<strong>epoll通过epoll_ctl来对监控的fds集合来进行增、删、改,那么必须涉及到fd的快速查找问题</strong>,于是,一个低时间复杂度的增、删、改、查的数据结构来组织被监控的fds集合是必不可少的了。在linux 2.6.8之前的内核,epoll使用hash来组织fds集合,于是在创建epoll fd的时候,epoll需要初始化hash的大小。于是epoll_create(int size)有一个参数size,以便内核根据size的大小来分配hash的大小。在linux 2.6.8以后的内核中,<strong>epoll使用红黑树来组织监控的fds集合</strong>,于是epoll_create(int size)的参数size实际上已经没有意义了。</p><h3 id="按需遍历就绪的fds集合"><a href="#按需遍历就绪的fds集合" class="headerlink" title="按需遍历就绪的fds集合"></a>按需遍历就绪的fds集合</h3><p>通过上面的socket的睡眠队列唤醒逻辑我们知道,socket唤醒睡眠在其睡眠队列的wait_entry(process)的时候会调用wait_entry的回调函数callback,并且,我们可以在callback中做任何事情。<strong>为了做到只遍历就绪的fd,我们需要有个地方来组织那些已经就绪的fd。</strong>为此,epoll引入了一个中间层,一个双向链表(ready_list),一个单独的睡眠队列(single_epoll_wait_list),并且,与select或poll不同的是,<strong>epoll的process不需要同时插入到多路复用的socket集合的所有睡眠队列中,相反process只是插入到中间层的epoll的单独睡眠队列中</strong>,process睡眠在epoll的单独队列上,等待事件的发生。同时,<strong>引入一个中间的wait_entry_sk,它与某个socket sk密切相关wait_entry_sk睡眠在sk的睡眠队列上,其callback函数逻辑是将当前sk排入到epoll的ready_list中,并唤醒epoll的single_epoll_wait_list。</strong>而single_epoll_wait_list上睡眠的<strong>process的回调函数</strong>就明朗了:<strong>遍历ready_list上的所有sk,挨个调用sk的poll函数收集事件,然后唤醒process从epoll_wait返回。</strong><br>于是,整个过程可以分为以下几个逻辑:</p><blockquote><p>我的理解:epoll分成epoll_wait和epoll_ctl,进程调用epoll_ctl构建出wait_entry_sk排入当前socket的睡眠队列中,并且这个wait_entry_sk被唤醒时,回调函数会把当前有事件的socket排队到ready_list中,并且会唤醒epoll的单独等待队列,这个单独等待队列有存着进程,进程也相应被唤醒,回调遍历ready_list的socket,这样就可以做到不遍历所有socket,而只针对发生了事件的socket。</p></blockquote><p>(1)epoll_ctl EPOLL_CTL_ADD逻辑</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>] 构建睡眠实体wait_entry_sk,将当前socket sk关联给wait_entry_sk,并设置wait_entry_sk的回调函数为epoll_callback_sk</span><br><span class="line">[<span class="number">2</span>] 将wait_entry_sk排入当前socket sk的睡眠队列上</span><br></pre></td></tr></table></figure><p>回调函数epoll_callback_sk的逻辑如下:</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>] 将之前关联的sk排入epoll的ready_list</span><br><span class="line">[<span class="number">2</span>] 然后唤醒epoll的单独睡眠队列single_epoll_wait_list</span><br></pre></td></tr></table></figure><p>(2)epoll_wait逻辑</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></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>] 构建睡眠实体wait_entry_proc,将当前process关联给wait_entry_proc,并设置回调函数为epoll_callback_proc</span><br><span class="line">[<span class="number">2</span>] 判断epoll的ready_list是否为空,如果为空,则将wait_entry_proc排入epoll的single_epoll_wait_list中,随后进入schedule循环,这会导致调用epoll_wait的process睡眠。</span><br><span class="line">[<span class="number">3</span>] wait_entry_proc被事件唤醒或超时醒来,wait_entry_proc将被从single_epoll_wait_list移除掉,然后wait_entry_proc执行回调函数epoll_callback_proc</span><br></pre></td></tr></table></figure><p>回调函数epoll_callback_proc的逻辑如下</p><figure class="highlight javascript"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>] 遍历epoll的ready_list,挨个调用每个sk的poll逻辑收集发生的事件,对于监控可读事件而已,ready_list上的每个sk都是有数据可读的,这里的遍历必要的(不同于select/poll的遍历,它不管有没数据可读都需要遍历一些来判断,这样就做了很多无用功。)</span><br><span class="line">[<span class="number">2</span>] 将每个sk收集到的事件,通过epoll_wait传入的events数组回传并唤醒相应的process。</span><br></pre></td></tr></table></figure><p>(3)epoll唤醒逻辑</p><p>整个epoll的协议栈唤醒逻辑如下(对于可读事件而言)</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></pre></td><td class="code"><pre><span class="line">[<span class="number">1</span>] 协议数据包到达网卡并被排入socket sk的接收队列</span><br><span class="line">[<span class="number">2</span>] 睡眠在sk的睡眠队列wait_entry被唤醒,wait_entry_sk的回调函数epoll_callback_sk被执行</span><br><span class="line">[<span class="number">3</span>] epoll_callback_sk将当前sk插入epoll的ready_list中</span><br><span class="line">[<span class="number">4</span>] 唤醒睡眠在epoll的单独睡眠队列single_epoll_wait_list的wait_entry,wait_entry_proc被唤醒执行回调函数epoll_callback_proc</span><br><span class="line">[<span class="number">5</span>] 遍历epoll的ready_list,挨个调用每个sk的poll逻辑收集发生的事件</span><br><span class="line">[<span class="number">6</span>] 将每个sk收集到的事件,通过epoll_wait传入的events数组回传并唤醒相应的process。</span><br></pre></td></tr></table></figure><p><strong>epoll巧妙的引入一个中间层解决了大量监控socket的无效遍历问题</strong>。细心的同学会发现,epoll在中间层上为每个监控的socket准备了一个单独的回调函数epoll_callback_sk,而对于select/poll,所有的socket都公用一个相同的回调函数。正是这个单独的回调epoll_callback_sk使得每个socket都能单独处理自身,当自己就绪的时候将自身socket挂入epoll的ready_list。同时,epoll引入了一个睡眠队列single_epoll_wait_list,分割了两类睡眠等待。process不再睡眠在所有的socket的睡眠队列上,而是睡眠在epoll的睡眠队列上,在等待”任意一个socket可读就绪”事件。而中间wait_entry_sk则代替process睡眠在具体的socket上,当socket就绪的时候,它就可以处理自身了。</p>]]></content>
<tags>
<tag> linux </tag>
<tag> 基础 </tag>
</tags>
</entry>
<entry>
<title>mysql日志</title>
<link href="/2019/10/02/mysql%E6%97%A5%E5%BF%97/"/>
<url>/2019/10/02/mysql%E6%97%A5%E5%BF%97/</url>
<content type="html"><![CDATA[<p>MySQL中有以下日志文件,分别是:</p><p> 1:<strong>重做日志(redo log)</strong> *</p><p> 2:<strong>回滚日志(undo log)</strong> *</p><p> 3:<strong>二进制日志(binlog)</strong> *</p><p> 4:<strong>错误日志(errorlog)</strong> </p><p> 5:<strong>慢查询日志(slow query log)</strong> </p><p> 6:<strong>一般查询日志(general log)</strong> </p><p> 7:<strong>中继日志(relay log)。</strong></p><p>其中重做日志和回滚日志与事务操作息息相关,二进制日志也与事务操作有一定的关系,这三种日志,对理解MySQL中的事务操作有着重要的意义。(只对前三进行学习)</p><h2 id="重做日志(redo-log)"><a href="#重做日志(redo-log)" class="headerlink" title="重做日志(redo log)"></a>重做日志(redo log)</h2><h3 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h3><p><strong>确保事务的持久性</strong>。redo日志记录事务执行后的状态,用来恢复未写入data file的已成功事务更新的数据。<strong>防止在发生故障的时间点,尚有脏页未写入磁盘</strong>,<strong>在重启mysql服务的时候,根据redo log进行重做,从而达到事务的持久性这一特性。</strong></p><blockquote><p>脏页:在内存已经修改了数据,还没刷新到磁盘</p></blockquote><h3 id="内容"><a href="#内容" class="headerlink" title="内容"></a>内容</h3><p>物理格式的日志,记录的是物理数据页面的修改的信息,其redo log是顺序写入redo log file的物理文件中去的。</p><h3 id="什么时候产生"><a href="#什么时候产生" class="headerlink" title="什么时候产生"></a><strong>什么时候产生</strong></h3><p>事务开始之后就产生redo log,redo log的落盘并不是随着事务的提交才写入的,而是在事务的执行过程中,便开始写入redo log文件中。</p><h3 id="什么时候释放"><a href="#什么时候释放" class="headerlink" title="什么时候释放"></a><strong>什么时候释放</strong></h3><p>当对应事务的脏页写入到磁盘之后,redo log的使命也就完成了,重做日志占用的空间就可以重用(被覆盖)。</p><h3 id="对应的物理文件"><a href="#对应的物理文件" class="headerlink" title="对应的物理文件"></a>对应的物理文件</h3><p>默认情况下,对应的物理文件位于数据库的data目录下的ib_logfile1&ib_logfile2<br>innodb_log_group_home_dir 指定日志文件组所在的路径,默认./ ,表示在数据库的数据目录下。<br>innodb_log_files_in_group 指定重做日志文件组中文件的数量,默认2</p><h3 id="关于文件的大小和数量,由以下两个参数配置"><a href="#关于文件的大小和数量,由以下两个参数配置" class="headerlink" title="关于文件的大小和数量,由以下两个参数配置"></a>关于文件的大小和数量,由以下两个参数配置</h3><p>innodb_log_file_size 重做日志文件的大小。<br>innodb_mirrored_log_groups 指定了日志镜像文件组的数量,默认1</p><h3 id="redo-log是什么时候写盘的"><a href="#redo-log是什么时候写盘的" class="headerlink" title="redo log是什么时候写盘的"></a>redo log是什么时候写盘的</h3><p>之所以说重做日志是在事务开始之后逐步写入重做日志文件,而不一定是事务提交才写入重做日志缓存,原因就是,<strong>重做日志有一个缓存区Innodb_log_buffer</strong>,Innodb_log_buffer的默认大小为8M(这里设置的16M),Innodb存储引擎<strong>先将重做日志写入innodb_log_buffer中</strong>。</p><p>然后会通过以下三种方式将innodb日志缓冲区的日志刷新到磁盘</p><ul><li>Master Thread 每秒一次执行刷新Innodb_log_buffer到重做日志文件。</li><li>每个事务提交时会将重做日志刷新到重做日志文件。</li><li>当重做日志缓存可用空间少于一半时,重做日志缓存被刷新到重做日志文件</li></ul><p>因此重做日志的写盘,并不一定是随着事务的提交才写入重做日志文件的,而是<strong>随着事务的开始,逐步开始的。</strong></p><p>即使某个<strong>事务还没有提交,Innodb存储引擎仍然每秒会将重做日志缓存刷新到重做日志文件。</strong></p><p>这一点是必须要知道的,因为这可以很好地解释再大的事务的提交(commit)的时间也是很短暂的。</p><h2 id="两阶段提交"><a href="#两阶段提交" class="headerlink" title="两阶段提交"></a>两阶段提交</h2><p>更新数据时,更新到内存后,先记录redo log,redo log进入准备提交状态(1阶段),写入bin log,再提交redo log(2阶段),保证数据一致性(mysql主从,崩溃恢复)</p><ul><li>判断 redo log 是否完整,如果判断是完整的,就立即提交。</li><li>如果 redo log 只是预提交但不是 commit 状态,这个时候就会去判断 binlog 是否完整,如果完整就提交 redo log, 不完整就回滚事务。</li></ul><h2 id="回滚日志(undo-log)"><a href="#回滚日志(undo-log)" class="headerlink" title="回滚日志(undo log)"></a>回滚日志(undo log)</h2><h3 id="作用-1"><a href="#作用-1" class="headerlink" title="作用"></a>作用</h3><p><strong>保证数据的原子性</strong>,<strong>保存了事务发生之前的数据的一个版本,可以用于回滚</strong>,<strong>同时可以提供多版本并发控制下的读(MVCC),也即非锁定读</strong></p><h3 id="内容-1"><a href="#内容-1" class="headerlink" title="内容"></a>内容</h3><p>逻辑格式的日志,<strong>在执行undo的时候,仅仅是将数据从逻辑上恢复至事务之前的状态,而不是从物理页面上操作实现的,这一点是不同于redo log的</strong>。</p><h3 id="什么时候产生-1"><a href="#什么时候产生-1" class="headerlink" title="什么时候产生"></a>什么时候产生</h3><p><strong>事务开始之前,将当前的版本生成undo log</strong>,<strong>undo 也会产生 redo 来保证undo log的可靠性</strong></p><h3 id="什么时候释放-1"><a href="#什么时候释放-1" class="headerlink" title="什么时候释放"></a>什么时候释放</h3><p><strong>当事务提交之后,undo log并不能立马被删除,而是放入待清理的链表</strong>,由purge线程判断是否有其他事务在使用undo段中表的上一个事务之前的版本信息,决定是否可以清理undo log的日志空间。</p><h3 id="对应的物理文件-1"><a href="#对应的物理文件-1" class="headerlink" title="对应的物理文件"></a>对应的物理文件</h3><p>MySQL5.6之前,undo表空间位于共享表空间的回滚段中,共享表空间的默认的名称是ibdata,位于数据文件目录中。<br>MySQL5.6之后,undo表空间可以配置成独立的文件,但是提前需要在配置文件中配置,完成数据库初始化后生效且不可改变undo log文件的个数<br>如果初始化数据库之前没有进行相关配置,那么就无法配置成独立的表空间了。</p><h3 id="关于MySQL5-7之后的独立undo-表空间配置参数如下"><a href="#关于MySQL5-7之后的独立undo-表空间配置参数如下" class="headerlink" title="关于MySQL5.7之后的独立undo 表空间配置参数如下"></a>关于MySQL5.7之后的独立undo 表空间配置参数如下</h3><p>innodb_undo_directory = /data/undospace/ –undo独立表空间的存放目录 innodb_undo_logs = 128 –回滚段为128KB innodb_undo_tablespaces = 4 –指定有4个undo log文件<br>如果undo使用的共享表空间,这个共享表空间中又不仅仅是存储了undo的信息,共享表空间的默认为与MySQL的数据目录下面,其属性由参数innodb_data_file_path配置。</p><h3 id="其他"><a href="#其他" class="headerlink" title="其他"></a>其他</h3><p>undo是在事务开始之前保存的被修改数据的一个版本,产生undo日志的时候,同样会伴随类似于保护事务持久化机制的redolog的产生。<br><strong>默认情况下undo文件是保持在共享表空间的,也即ibdatafile文件中,当数据库中发生一些大的事务性操作的时候,要生成大量的undo信息,全部保存在共享表空间中的。</strong><br>因此共享表空间可能会变的很大,<strong>默认情况下,也就是undo 日志使用共享表空间的时候,被“撑大”的共享表空间是不会也不能自动收缩的。</strong><br>因此,mysql5.7之后的“独立undo 表空间”的配置就显得很有必要了。</p><h2 id="二进制日志(binlog)"><a href="#二进制日志(binlog)" class="headerlink" title="二进制日志(binlog)"></a>二进制日志(binlog)</h2><h3 id="作用-2"><a href="#作用-2" class="headerlink" title="作用"></a>作用</h3><ul><li><strong>用于复制,在主从复制中,从库利用主库上的binlog进行重播,实现主从同步。</strong></li><li><strong>用于数据库的基于时间点的还原。</strong></li></ul><h3 id="内容-2"><a href="#内容-2" class="headerlink" title="内容"></a>内容</h3><p>逻辑格式的日志,可以<strong>简单认为就是执行过的事务中的sql语句。</strong></p><p>但又<strong>不完全是sql语句这么简单,而是包括了执行的sql语句(增删改)反向的信息,也就意味着delete对应着delete本身和其反向的insert;update对应着update执行前后的版本的信息;insert对应着delete和insert本身的信息。</strong></p><p>在使用mysqlbinlog解析binlog之后一些都会真相大白。</p><p>因此可以基于binlog做到类似于oracle的闪回功能,其实都是依赖于binlog中的日志记录。</p><h3 id="什么时候产生-2"><a href="#什么时候产生-2" class="headerlink" title="什么时候产生"></a>什么时候产生</h3><p><strong>事务提交的时候,一次性将事务中的sql语句(一个事物可能对应多个sql语句)按照一定的格式记录到binlog中。</strong><br>这里与redo log很明显的差异就是redo log并不一定是在事务提交的时候刷新到磁盘,redo log是在事务开始之后就开始逐步写入磁盘。<br>因此对于事务的提交,即便是较大的事务,提交(commit)都是很快的,但是<strong>在开启了bin_log的情况下,对于较大事务的提交,可能会变得比较慢一些。</strong><br>这是因为binlog是在事务提交的时候一次性写入的造成的,这些可以通过测试验证。</p><h3 id="什么时候释放-2"><a href="#什么时候释放-2" class="headerlink" title="什么时候释放"></a>什么时候释放</h3><p>binlog的默认是保持时间由参数expire_logs_days配置,也就是说对于非活动的日志文件,在生成时间超过expire_logs_days配置的天数之后,会被自动删除。</p><h3 id="对应的物理文件-2"><a href="#对应的物理文件-2" class="headerlink" title="对应的物理文件"></a><strong>对应的物理文件</strong></h3><p>配置文件的路径为log_bin_basename,binlog日志文件按照指定大小,当日志文件达到指定的最大的大小之后,进行滚动更新,生成新的日志文件。</p><h3 id="其他-1"><a href="#其他-1" class="headerlink" title="其他"></a><strong>其他</strong></h3><p>二进制日志的作用之一是还原数据库的,这与redo log很类似,很多人混淆过,但是两者有本质的不同</p><p><strong>作用不同</strong>:redo log是保证事务的持久性的,是事务层面的,binlog作为还原的功能,是数据库层面的(当然也可以精确到事务层面的),虽然都有还原的意思,但是其保护数据的层次是不一样的。</p><p><strong>内容不同</strong>:redo log是物理日志,是数据页面的修改之后的物理记录,binlog是逻辑日志,可以简单认为记录的就是sql语句</p><p>另外,两者日志产生的时间,可以释放的时间,在可释放的情况下清理机制,都是完全不同的。</p><p><strong>恢复数据时候的效率,基于物理日志的redo log恢复数据的效率要高于语句逻辑日志的binlog</strong></p><p>关于事务提交时,redo log和binlog的写入顺序,为了保证主从复制时候的主从一致(当然也包括使用binlog进行基于时间点还原的情况),是要严格一致的,<strong>MySQL通过两阶段提交过程来完成事务的一致性的,也即redo log和binlog的一致性的,理论上是先写redo log,再写binlog,两个日志都提交成功(刷入磁盘),事务才算真正的完成。</strong></p>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>sql练习</title>
<link href="/2019/10/01/sql%E7%BB%83%E4%B9%A0/"/>
<url>/2019/10/01/sql%E7%BB%83%E4%B9%A0/</url>
<content type="html"><![CDATA[<h2 id="组合两个表"><a href="#组合两个表" class="headerlink" title="组合两个表"></a>组合两个表</h2><h3 id="题目"><a href="#题目" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427141648369.png"><h3 id="解答"><a href="#解答" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select p.FirstName,p.LastName,a.City,a.State from Person p left join Address on p.PersonId = a.PersonId;</span><br></pre></td></tr></table></figure><h2 id="第二高薪水(成绩第二高)"><a href="#第二高薪水(成绩第二高)" class="headerlink" title="第二高薪水(成绩第二高)"></a>第二高薪水(成绩第二高)</h2><h3 id="题目-1"><a href="#题目-1" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427142135808.png"><h3 id="解答-1"><a href="#解答-1" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">// 分成两部分,第一块是最高的薪水,第二个是比最高薪水小的最高的薪水</span><br><span class="line">select Max(Salary) SecondHighestSalary from Employee where (select Max(Salary) from Employee) > Salary;</span><br></pre></td></tr></table></figure><h2 id="第N高的薪水"><a href="#第N高的薪水" class="headerlink" title="第N高的薪水"></a>第N高的薪水</h2><h3 id="题目-2"><a href="#题目-2" class="headerlink" title="题目"></a>题目</h3><p>第二题的扩展</p><h3 id="解答-2"><a href="#解答-2" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">// 自表关联,拿出表e1的每一行数据分别和e2的全数据比较,计算出有多少薪水大于e1的那一行薪水,有多少比它大,即排名第N</span><br><span class="line">Select Max(Salary) from Employee e1 where N = (select count(distinct(e2.Salary)) from Employee e2 where e2.Salary >= e1.Salary);</span><br></pre></td></tr></table></figure><h2 id="分数排名"><a href="#分数排名" class="headerlink" title="分数排名"></a>分数排名</h2><h3 id="题目-3"><a href="#题目-3" class="headerlink" title="题目"></a>题目</h3><p>编写一个 SQL查询来实现分数排名。如果两个分数相同,则两个分数排名(Rank)相同。请注意,平分后的下一个名次应该是下一个连续的整数值。换句话说,名次之间不应该有“间隔”。</p><img src="/2019/10/01/sql练习/20180427144045666.png"><h3 id="解答-3"><a href="#解答-3" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">//对每一行数据,找到比他大的分数个数即可</span><br><span class="line">SELECT Score,(SELECT count(DISTINCT Score) FROM Scores WHERE Score >= s.Score) 'Rank'</span><br><span class="line">FROM Scores s ORDER BY Score DESC;</span><br></pre></td></tr></table></figure><h2 id="连续出现的数字"><a href="#连续出现的数字" class="headerlink" title="连续出现的数字"></a>连续出现的数字</h2><h3 id="题目-4"><a href="#题目-4" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427144502979.png"><h3 id="解答-4"><a href="#解答-4" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">select distinct l1.num from logs l1</span><br><span class="line">left join logs l2 on l1.id = l2.id -1 </span><br><span class="line">left join logs l3 on l1.id = l3.id -2</span><br><span class="line">where l1.num = l2.num and l2.num = l3.num</span><br></pre></td></tr></table></figure><h2 id="超过经理收入的员工"><a href="#超过经理收入的员工" class="headerlink" title="超过经理收入的员工"></a>超过经理收入的员工</h2><h3 id="题目-5"><a href="#题目-5" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427144832555.png"><h3 id="解答-5"><a href="#解答-5" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select name from employee e1 where salary > (select e2.salary from employee e2 where e1.ManagerId = e2.id)</span><br></pre></td></tr></table></figure><h2 id="寻找重复的电子邮箱"><a href="#寻找重复的电子邮箱" class="headerlink" title="寻找重复的电子邮箱"></a>寻找重复的电子邮箱</h2><h3 id="题目-6"><a href="#题目-6" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427145522214.png"><h3 id="解答-6"><a href="#解答-6" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select Email from Person group by Email having counnt(Email)>1;</span><br></pre></td></tr></table></figure><h2 id="从不订购的客户"><a href="#从不订购的客户" class="headerlink" title="从不订购的客户"></a>从不订购的客户</h2><h3 id="题目-7"><a href="#题目-7" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427145544425.png"><h3 id="解答-7"><a href="#解答-7" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">// 直接左连接,取出CustomerId = null的项</span><br><span class="line">select c.name Customers from Customers c</span><br><span class="line">left join Orders o on o.CustomersId = c.id where o.CustomerId is null</span><br></pre></td></tr></table></figure><h2 id="部门最高工资"><a href="#部门最高工资" class="headerlink" title="部门最高工资"></a>部门最高工资</h2><h3 id="题目-8"><a href="#题目-8" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427145945298.png"><h3 id="解答-8"><a href="#解答-8" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//解法1:内联e1和d,找到e1的每一行对应e2departmentid相同的列表的最大值</span><br><span class="line">select d.Name Department,e1.Name Employee,e1.Salary Salary </span><br><span class="line">from Employee e1</span><br><span class="line">inner join Department d on e1.DepartmentId= d.Id</span><br><span class="line">where e1.Salary in (select Max(e2.Salary) from Employee e2 where e2.DepartmentId =e1.DepartmentId)</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">//解法2:直接group by DepartmentId 找到最大值,在对应查其他项就行了</span><br><span class="line">select d.Name Department,e1.Name Employee,e1.Salary Salary</span><br><span class="line">from Employee e1,Department d</span><br><span class="line">where e1.DepartmentId = d.Id and(e1.Salary,e1.DepartmentId)</span><br><span class="line">in (select max(Salary),DepartmentId from Employee group by DepartmentId)</span><br></pre></td></tr></table></figure><h2 id="部门前三收入"><a href="#部门前三收入" class="headerlink" title="部门前三收入"></a>部门前三收入</h2><h3 id="题目-9"><a href="#题目-9" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427150450545.png"><h3 id="解答-9"><a href="#解答-9" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">select d.Name Department,e1.Name Employee,e1.Salary Salary</span><br><span class="line">from Employee e1</span><br><span class="line">inner join Department d on e1.DepartmentId = d.Id</span><br><span class="line">where 3>= (</span><br><span class="line">select count(distinct(e2.salary)) from Employee e2 where e2.Salary >=e1.Salary and e2.departmentId = d.Id</span><br><span class="line">) order by d.name,e1.Salary desc;</span><br></pre></td></tr></table></figure><h2 id="删除重复邮件"><a href="#删除重复邮件" class="headerlink" title="删除重复邮件"></a>删除重复邮件</h2><h3 id="题目-10"><a href="#题目-10" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180427150643550.png"><h3 id="解答-10"><a href="#解答-10" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">DELETE p2 FROM Person p1 JOIN Person p2</span><br><span class="line">ON p2.Email = p1.Email WHERE p2.Id > p1.Id;</span><br></pre></td></tr></table></figure><h2 id="上升的温度"><a href="#上升的温度" class="headerlink" title="上升的温度"></a>上升的温度</h2><h3 id="题目-11"><a href="#题目-11" class="headerlink" title="题目"></a>题目</h3><p>给定一个Weather表,编写一个SQL查询来查找与之前(昨天的)日期相比温度更高的所有日期的id。</p><img src="/2019/10/01/sql练习/20180428090117569.png"><h3 id="解答-11"><a href="#解答-11" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">//mysql的函数DATEDIFF</span><br><span class="line">select w1.id from weather w1</span><br><span class="line">inner join weather w2 on w1.Temperature > w2.Temperature and DATEDIFF(w1.RecordDate, w2.RecordDate) = 1;</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">//mysql的函数TO_DAYS</span><br><span class="line">SELECT w1.Id FROM Weather w1, Weather w2</span><br><span class="line">WHERE w1.Temperature > w2.Temperature AND TO_DAYS(w1.RecordDate)=TO_DAYS(w2.RecordDate) + 1;</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">//mysql的函数SUBDATE</span><br><span class="line">SELECT w1.Id FROM Weather w1, Weather w2</span><br><span class="line">WHERE w1.Temperature > w2.Temperature AND SUBDATE(w1.RecordDate, 1) = w2.RecordDate;</span><br></pre></td></tr></table></figure><h2 id="行程和用户"><a href="#行程和用户" class="headerlink" title="行程和用户"></a>行程和用户</h2><h3 id="题目-12"><a href="#题目-12" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/微信图片_20191001214616.png"><h3 id="解答-12"><a href="#解答-12" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">select t.Request_at Day,</span><br><span class="line">ROUND(sum((case when t.Status like 'cancelled%' then 1 else 0 end))/count(*),2) 'Cancellation'</span><br><span class="line">from Trips t</span><br><span class="line">inner join Users u on u.Users_Id = t.Client_Id and u.Banned = 'No'</span><br><span class="line">where t.Request_at between '2013-10-01' and '2013-10-03' group by t.Request_at;</span><br></pre></td></tr></table></figure><h2 id="大的国家"><a href="#大的国家" class="headerlink" title="大的国家"></a>大的国家</h2><h3 id="题目-13"><a href="#题目-13" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180428091939613.png"><h3 id="解答-13"><a href="#解答-13" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select name,population,area from worldwhere area > 3000000 or population > 25000000;</span><br></pre></td></tr></table></figure><h2 id="超过5名学生的课"><a href="#超过5名学生的课" class="headerlink" title="超过5名学生的课"></a>超过5名学生的课</h2><h3 id="题目-14"><a href="#题目-14" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180428092347946.png"><h3 id="解答-14"><a href="#解答-14" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT class from courses group by class having count(DISTINCT student) >= 5;</span><br></pre></td></tr></table></figure><h2 id="体育馆的人流量"><a href="#体育馆的人流量" class="headerlink" title="体育馆的人流量"></a>体育馆的人流量</h2><h3 id="题目-15"><a href="#题目-15" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180428094049782.png"><h3 id="解答-15"><a href="#解答-15" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><figcaption><span>distinct s1.*</span></figcaption><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">// 困难的题= =</span><br><span class="line">select distinct s1.*</span><br><span class="line">from stadium s1, stadium s2, stadium s3</span><br><span class="line">where s1.people >= 100 and s2.people>= 100 and s3.people >= 100</span><br><span class="line">and</span><br><span class="line">(</span><br><span class="line"> (s1.id - s2.id = 1 and s2.id - s3.id =1)</span><br><span class="line"> or</span><br><span class="line"> (s2.id - s1.id = 1 and s1.id - s3.id =1) </span><br><span class="line"> or</span><br><span class="line"> (s3.id - s2.id = 1 and s2.id - s1.id = 1) </span><br><span class="line">) order by s1.id;</span><br></pre></td></tr></table></figure><h2 id="有趣的电影"><a href="#有趣的电影" class="headerlink" title="有趣的电影"></a>有趣的电影</h2><h3 id="题目-16"><a href="#题目-16" class="headerlink" title="题目"></a>题目</h3><p>找出所有影片描述为<strong>非</strong><code>boring</code>(不无聊)的并且<strong>id 为奇数</strong>的影片,结果请按等级<code>rating</code>排列。</p><img src="/2019/10/01/sql练习/20180428094346964.png"><h3 id="解答-16"><a href="#解答-16" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">select * from cinema where description <>'boring' and mod(id,2)=1 ORDER BY rating desc</span><br></pre></td></tr></table></figure><h2 id="换座位"><a href="#换座位" class="headerlink" title="换座位"></a>换座位</h2><h3 id="题目-17"><a href="#题目-17" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180428104238138.png"><h3 id="解答-17"><a href="#解答-17" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">//奇数+1,偶数-1</span><br><span class="line">select s.id,s.student FROM</span><br><span class="line">(</span><br><span class="line">select id-1 as id,student from seat where mod(id,2) = 0</span><br><span class="line">UNION</span><br><span class="line">select id+1 as id,student from seat where mod(id,2) = 1 and id !=(select count(*) from seat)</span><br><span class="line">UNION</span><br><span class="line">select id,student from seat where mod(id,2) = 1 and id = (select count(*) from seat)</span><br><span class="line">) s order by id;</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">//奇数且不是最后一行,id+1,其余-1</span><br><span class="line">select (case</span><br><span class="line">when mod(id,2)!=0 and id !=counts then id+1</span><br><span class="line">when mod(id,2)!=0 and id = counts then id</span><br><span class="line">else id-1 end) as id,student</span><br><span class="line">from seat,(select count(*) as counts from seat) as seat_counts</span><br><span class="line">order by id;</span><br></pre></td></tr></table></figure><h2 id="交换工资"><a href="#交换工资" class="headerlink" title="交换工资"></a>交换工资</h2><h3 id="题目-18"><a href="#题目-18" class="headerlink" title="题目"></a>题目</h3><img src="/2019/10/01/sql练习/20180428104953585.png"><h3 id="解答-18"><a href="#解答-18" class="headerlink" title="解答"></a>解答</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">update salary set sex = if(sex = 'm', 'f','m');</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE salary SET sex = (CASE WHEN sex = 'm' THEN 'f' ELSE 'm' END)</span><br></pre></td></tr></table></figure>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>zookeeper学习2</title>
<link href="/2019/10/01/zookeeper%E6%A6%82%E5%BF%B5/"/>
<url>/2019/10/01/zookeeper%E6%A6%82%E5%BF%B5/</url>
<content type="html"><![CDATA[<h2 id="什么是-ZooKeeper"><a href="#什么是-ZooKeeper" class="headerlink" title="什么是 ZooKeeper"></a>什么是 ZooKeeper</h2><p>ZooKeeper 是一个开源的分布式协调服务,设计目标是将那些复杂且容易出错的分布式一致性服务封装起来,构成一个高效可靠的原语集,并以一系列简单易用的接口提供给用户使用。</p><blockquote><p>原语: 操作系统或计算机网络用语范畴。是由若干条指令组成的,用于完成一定功能的一个过程。具有不可分割性·即原语的执行必须是连续的,在执行过程中不允许被中断。</p></blockquote><p>ZooKeeper 是一个典型的分布式数据一致性解决方案,分布式应用程序可以基于 ZooKeeper 实现诸如数据发布/订阅、负载均衡、命名服务、分布式协调/通知、集群管理、Master 选举、分布式锁和分布式队列等功能。</p><p>Zookeeper 一个最常用的使用场景就是用于担任服务生产者和服务消费者的注册中心(提供发布订阅服务)。 服务生产者将自己提供的服务注册到Zookeeper中心,服务的消费者在进行服务调用的时候先到Zookeeper中查找服务,获取到服务生产者的详细信息之后,再去调用服务生产者的内容与数据。如下图所示,在 Dubbo架构中 Zookeeper 就担任了注册中心这一角色。</p><img src="/2019/10/01/zookeeper概念/35571782.jpg.png"><h2 id="关于-ZooKeeper-的一些重要概念"><a href="#关于-ZooKeeper-的一些重要概念" class="headerlink" title="关于 ZooKeeper 的一些重要概念"></a>关于 ZooKeeper 的一些重要概念</h2><ul><li><strong>ZooKeeper 本身就是一个分布式程序(只要半数以上节点存活,ZooKeeper 就能正常服务)。</strong></li><li><strong>为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么 ZooKeeper 本身仍然是可用的。</strong></li><li><strong>ZooKeeper 将数据保存在内存中,这也就保证了 高吞吐量和低延迟</strong>(但是内存限制了能够存储的容量不太大,此限制也是保持znode中存储的数据量较小的进一步原因)。</li><li><strong>ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。</strong>(“读”多于“写”是协调服务的典型场景。)</li><li><strong>ZooKeeper有临时节点的概念。 当创建临时节点的客户端会话一直保持活动,瞬时节点就一直存在。而当会话终结时,瞬时节点被删除。持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。</strong></li><li>ZooKeeper 底层其实只提供了两个功能:①管理(存储、读取)用户程序提交的数据;②为用户程序提供数据节点监听服务。</li></ul><h2 id="会话(Session)"><a href="#会话(Session)" class="headerlink" title="会话(Session)"></a>会话(Session)</h2><p>Session 指的是 ZooKeeper 服务器与客户端会话。<strong>在 ZooKeeper 中,一个客户端连接是指客户端和服务器之间的一个 TCP 长连接</strong>。客户端启动的时候,首先会与服务器建立一个 TCP 连接,从第一次连接建立开始,客户端会话的生命周期也开始了。<strong>通过这个连接,客户端能够通过心跳检测与服务器保持有效的会话,也能够向Zookeeper服务器发送请求并接受响应,同时还能够通过该连接接收来自服务器的Watch事件通知。</strong> Session的<code>sessionTimeout</code>值用来设置一个客户端会话的超时时间。当由于服务器压力太大、网络故障或是客户端主动断开连接等各种原因导致客户端连接断开时,<strong>只要在sessionTimeout规定的时间内能够重新连接上集群中任意一台服务器,那么之前创建的会话仍然有效。</strong></p><p><strong>在为客户端创建会话之前,服务端首先会为每个客户端都分配一个sessionID。由于 sessionID 是 Zookeeper 会话的一个重要标识,许多与会话相关的运行机制都是基于这个 sessionID 的,因此,无论是哪台服务器为客户端分配的 sessionID,都务必保证全局唯一。</strong></p><h2 id="Znode"><a href="#Znode" class="headerlink" title="Znode"></a>Znode</h2><p><strong>在谈到分布式的时候,我们通常说的“节点”是指组成集群的每一台机器。然而,在Zookeeper中,“节点”分为两类,第一类同样是指构成集群的机器,我们称之为机器节点;第二类则是指数据模型中的数据单元,我们称之为数据节点一一ZNode。</strong></p><p>Zookeeper将所有数据存储在内存中,数据模型是一棵树(Znode Tree),由斜杠(/)的进行分割的路径,就是一个Znode,例如/foo/path1。每个上都会保存自己的数据内容,同时还会保存一系列属性信息。</p><p><strong>在Zookeeper中,node可以分为持久节点和临时节点两类。所谓持久节点是指一旦这个ZNode被创建了,除非主动进行ZNode的移除操作,否则这个ZNode将一直保存在Zookeeper上。而临时节点就不一样了,它的生命周期和客户端会话绑定,一旦客户端会话失效,那么这个客户端创建的所有临时节点都会被移除。</strong> 另外,ZooKeeper还允许用户为每个节点添加一个特殊的属性:<strong>SEQUENTIAL</strong>.一旦节点被标记上这个属性,那么在这个节点被创建的时候,Zookeeper会自动在其节点名后面追加上一个整型数字,这个整型数字是一个由父节点维护的自增数字。</p><p>zookeeper 的每个 ZNode 上都会存储数据,对应于每个ZNode,Zookeeper 都会为其维护一个叫作 <strong>Stat</strong> 的数据结构,Stat 中记录了这个 ZNode 的三个数据版本,分别是version(当前ZNode的版本)、cversion(当前ZNode子节点的版本)和 aversion(当前ZNode的ACL版本)。</p><h2 id="Watcher"><a href="#Watcher" class="headerlink" title="Watcher"></a>Watcher</h2><p><strong>Watcher(事件监听器),是Zookeeper中的一个很重要的特性。Zookeeper允许用户在指定节点上注册一些Watcher,并且在一些特定事件触发的时候,ZooKeeper服务端会将事件通知到感兴趣的客户端上去,该机制是Zookeeper实现分布式协调服务的重要特性。</strong></p><h2 id="ACL"><a href="#ACL" class="headerlink" title="ACL"></a>ACL</h2><p>Zookeeper采用ACL(AccessControlLists)策略来进行权限控制,类似于 UNIX 文件系统的权限控制。Zookeeper 定义了如下5种权限。其中尤其需要注意的是,CREATE和DELETE这两种权限都是针对子节点的权限控制。</p><img src="/2019/10/01/zookeeper概念/27473480.jpg.png"><h2 id="ZooKeeper-特点"><a href="#ZooKeeper-特点" class="headerlink" title="ZooKeeper 特点"></a>ZooKeeper 特点</h2><ul><li><strong>顺序一致性:</strong> 从同一客户端发起的事务请求,最终将会严格地按照顺序被应用到 ZooKeeper 中去。</li><li><strong>原子性:</strong> 所有事务请求的处理结果在整个集群中所有机器上的应用情况是一致的,也就是说,要么整个集群中所有的机器都成功应用了某一个事务,要么都没有应用。</li><li><strong>单一系统映像 :</strong> 无论客户端连到哪一个 ZooKeeper 服务器上,其看到的服务端数据模型都是一致的。</li><li><strong>可靠性:</strong> 一旦一次更改请求被应用,更改的结果就会被持久化,直到被下一次更改覆盖。</li></ul><h2 id="ZooKeeper-设计目标"><a href="#ZooKeeper-设计目标" class="headerlink" title="ZooKeeper 设计目标"></a>ZooKeeper 设计目标</h2><h3 id="简单的数据模型"><a href="#简单的数据模型" class="headerlink" title="简单的数据模型"></a>简单的数据模型</h3><p>ZooKeeper 允许分布式进程通过共享的层次结构命名空间进行相互协调,这与标准文件系统类似。 名称空间由 ZooKeeper 中的数据寄存器组成 - 称为znode,这些类似于文件和目录。 与为存储设计的典型文件系统不同,ZooKeeper数据保存在内存中,这意味着ZooKeeper可以实现高吞吐量和低延迟。</p><img src="/2019/10/01/zookeeper概念/94251757.jpg"><h3 id="可构建集群"><a href="#可构建集群" class="headerlink" title="可构建集群"></a>可构建集群</h3><p><strong>为了保证高可用,最好是以集群形态来部署 ZooKeeper,这样只要集群中大部分机器是可用的(能够容忍一定的机器故障),那么zookeeper本身仍然是可用的。</strong> 客户端在使用 ZooKeeper 时,需要知道集群机器列表,通过与集群中的某一台机器建立 TCP 连接来使用服务,客户端使用这个TCP链接来发送请求、获取结果、获取监听事件以及发送心跳包。如果这个连接异常断开了,客户端可以连接到另外的机器上。</p><img src="/2019/10/01/zookeeper概念/68900686.jpg.png"><p>上图中每一个Server代表一个安装Zookeeper服务的服务器。组成 ZooKeeper<br>服务的服务器都会在内存中维护当前的服务器状态,并且每台服务器之间都互相保持着通信。集群间通过 Zab 协议(Zookeeper Atomic<br>Broadcast)来保持数据的一致性。</p><h3 id="顺序访问"><a href="#顺序访问" class="headerlink" title="顺序访问"></a>顺序访问</h3><p><strong>对于来自客户端的每个更新请求,ZooKeeper 都会分配一个全局唯一的递增编号,这个编号反应了所有事务操作的先后顺序,应用程序可以使用 ZooKeeper 这个特性来实现更高层次的同步原语。</strong> <strong>这个编号也叫做时间戳——zxid(Zookeeper Transaction Id)</strong></p><h3 id="高性能"><a href="#高性能" class="headerlink" title="高性能"></a>高性能</h3><p><strong>ZooKeeper 是高性能的。 在“读”多于“写”的应用程序中尤其地高性能,因为“写”会导致所有的服务器间同步状态。(“读”多于“写”是协调服务的典型场景。)</strong></p><h2 id="ZooKeeper-集群角色介绍"><a href="#ZooKeeper-集群角色介绍" class="headerlink" title="ZooKeeper 集群角色介绍"></a>ZooKeeper 集群角色介绍</h2><p><strong>最典型集群模式: Master/Slave 模式(主备模式)</strong>。在这种模式中,通常 Master服务器作为主服务器提供写服务,其他的 Slave 服务器从服务器通过异步复制的方式获取 Master 服务器最新的数据提供读服务。</p><p>但是,<strong>在 ZooKeeper 中没有选择传统的 Master/Slave 概念,而是引入了Leader、Follower 和 Observer 三种角色</strong>。</p><ul><li><strong>Leader</strong> 一个ZooKeeper集群同一时间只会有一个实际工作的Leader,它会<strong>发起并维护</strong>与各Follwer及Observer间的心跳。所有的写操作必须要通过Leader完成再由Leader将写操作广播给其它服务器。</li><li><strong>Follower</strong> 一个ZooKeeper集群可能同时存在多个Follower,它会响应Leader的心跳。Follower可直接处理并返回客户端的读请求,同时会将写请求转发给Leader处理,并且负责在Leader处理写请求时对请求进行投票。</li><li><strong>Observer</strong> 角色与Follower类似,但是无投票权。</li></ul><p>如下图所示</p><img src="/2019/10/01/zookeeper概念/89602762.jpg.png"><p><strong>ZooKeeper 集群中的所有机器通过一个 Leader 选举过程来选定一台称为 “Leader” 的机器,Leader<br>既可以为客户端提供写服务又能提供读服务。除了 Leader 外,Follower 和 Observer 都只能提供读服务。Follower 和<br>Observer 唯一的区别在于 Observer 机器不参与 Leader 的选举过程,也不参与写操作的“过半写成功”策略,因此<br>Observer 机器可以在不影响写性能的情况下提升集群的读性能。</strong></p><img src="/2019/10/01/zookeeper概念/91622395.jpg.png"><p><strong>当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进人恢复模式并选举产生新的Leader服务器。这个过程大致是这样的:</strong></p><ol><li>Leader election(选举阶段):节点在一开始都处于选举阶段,只要有一个节点得到超半数节点的票数,它就可以当选准 leader。</li><li>Discovery(发现阶段):在这个阶段,followers 跟准 leader 进行通信,同步 followers 最近接收的事务提议。</li><li>Synchronization(同步阶段):同步阶段主要是利用 leader 前一阶段获得的最新提议历史,同步集群中所有的副本。同步完成之后 准 leader 才会成为真正的 leader。</li><li>Broadcast(广播阶段) 到了这个阶段,Zookeeper 集群才能正式对外提供事务服务,并且 leader 可以进行消息广播。同时如果有新的节点加入,还需要对新节点进行同步。</li></ol><h2 id="ZooKeeper-amp-ZAB-协议-amp-Paxos算法"><a href="#ZooKeeper-amp-ZAB-协议-amp-Paxos算法" class="headerlink" title="ZooKeeper &ZAB 协议&Paxos算法"></a>ZooKeeper &ZAB 协议&Paxos算法</h2><h3 id="ZAB-协议-amp-Paxos算法"><a href="#ZAB-协议-amp-Paxos算法" class="headerlink" title="ZAB 协议&Paxos算法"></a>ZAB 协议&Paxos算法</h3><p>Paxos 算法应该可以说是 ZooKeeper 的灵魂了。但是,ZooKeeper 并没有完全采用 Paxos算法 ,而是使用 ZAB 协议作为其保证数据一致性的核心算法。另外,在ZooKeeper的官方文档中也指出,ZAB协议并不像 Paxos 算法那样,是一种通用的分布式一致性算法,它是一种特别为Zookeeper设计的崩溃可恢复的原子消息广播算法。</p><h3 id="ZAB-协议介绍"><a href="#ZAB-协议介绍" class="headerlink" title="ZAB 协议介绍"></a>ZAB 协议介绍</h3><p>ZAB(ZooKeeper Atomic Broadcast 原子广播) 协议是为分布式协调服务 ZooKeeper 专门设计的一种支持崩溃恢复的原子广播协议。 在 ZooKeeper 中,主要依赖 ZAB 协议来实现分布式数据一致性,基于该协议,ZooKeeper 实现了一种主备模式的系统架构来保持集群中各个副本之间的数据一致性。</p><h3 id="ZAB-协议两种基本的模式:崩溃恢复和消息广播"><a href="#ZAB-协议两种基本的模式:崩溃恢复和消息广播" class="headerlink" title="ZAB 协议两种基本的模式:崩溃恢复和消息广播"></a>ZAB 协议两种基本的模式:崩溃恢复和消息广播</h3><p>ZAB协议包括两种基本的模式,分别是 崩溃恢复和消息广播。当整个服务框架在启动过程中,或是当 Leader 服务器出现网络中断、崩溃退出与重启等异常情况时,ZAB 协议就会进人恢复模式并选举产生新的Leader服务器。当选举产生了新的 Leader 服务器,同时集群中已经有过半的机器与该Leader服务器完成了状态同步之后,ZAB协议就会退出恢复模式。其中,<strong>所谓的状态同步是指数据同步,用来保证集群中存在过半的机器能够和Leader服务器的数据状态保持一致。</strong></p><p><strong>当集群中已经有过半的Follower服务器完成了和Leader服务器的状态同步,那么整个服务框架就可以进人消息广播模式了</strong>。 当一台同样遵守ZAB协议的服务器启动后加人到集群中时,如果此时集群中已经存在一个Leader服务器在负责进行消息广播,那么新加人的服务器就会自觉地进人数据恢复模式:找到Leader所在的服务器,并与其进行数据同步,然后一起参与到消息广播流程中去。正如上文介绍中所说的,ZooKeeper设计成只允许唯一的一个Leader服务器来进行事务请求的处理。Leader服务器在接收到客户端的事务请求后,会生成对应的事务提案并发起一轮广播协议;而如果集群中的其他机器接收到客户端的事务请求,那么这些非Leader服务器会首先将这个事务请求转发给Leader服务器。</p>]]></content>
<tags>
<tag> zookeeper </tag>
</tags>
</entry>
<entry>
<title>一致性哈希</title>
<link href="/2019/10/01/%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C/"/>
<url>/2019/10/01/%E4%B8%80%E8%87%B4%E6%80%A7%E5%93%88%E5%B8%8C/</url>
<content type="html"><![CDATA[<h2 id="主从架构如果不做hash"><a href="#主从架构如果不做hash" class="headerlink" title="主从架构如果不做hash"></a>主从架构如果不做hash</h2><p>假设,我们有一个社交网站,需要使用Redis存储图片资源,存储的格式为键值对,key值为图片名称,value为该图片所在文件服务器的路径,我们需要根据文件名查找该文件所在文件服务器上的路径,数据量大概有2000W左右,按照我们约定的规则进行分库,规则就是<strong>随机分配</strong>,我们可以部署8台缓存服务器,每台服务器大概含有500W条数据,并且进行主从复制,由于规则是随机的,所有我们的一条数据都有可能存储在任何一组Redis中。</p><p>我们需要进行1、2、3、4,4次查询才能够查询到(也就是遍历了所有的Redis服务器),这显然不是我们想要的结果,有了解过的小伙伴可能会想到,随机的规则不行,可以使用类似于数据库中的分库分表规则:按照Hash值、取模、按照类别、按照某一个字段值等等常见的规则就可以出来了!好,按照我们的主题,我们就使用Hash的方式。</p><h2 id="使用hash"><a href="#使用hash" class="headerlink" title="使用hash"></a>使用hash</h2><p>如果我们使用Hash的方式 hash(图片名称) % N ,每一张图片在进行分库的时候都可以定位到特定的服务器</p><p>因为图片的名称是不重复的,所以,当我们对同一个图片名称做相同的哈希计算时,得出的结果应该是不变的,如果我们有4台服务器,使用哈希后的结果对4求余,那么余数一定是0、1、2或3,没错,正好与我们之前的服务器编号相同。我们暂时称上述算法为HASH算法或者取模算法。</p><p>这样的话就不会遍历所有的服务器,大大提升了性能!</p><h3 id="使用hash的问题"><a href="#使用hash的问题" class="headerlink" title="使用hash的问题"></a>使用hash的问题</h3><p>使用上述HASH算法进行缓存时,会出现一些缺陷,主要体现在<strong>服务器数量变动的时候,所有缓存的位置都要发生改变!</strong></p><p>当服务器数量变动时,所有缓存的位置都要发生改变,换句话说,当服务器数量发生改变时,所有缓存在一定时间内是失效的,当应用无法从缓存中获取数据时,则会向后端服务器请求数据</p><p>由于大量缓存在同一时间失效,造成了缓存的雪崩,此时前端缓存已经无法起到承担部分压力的作用,后端服务器将会承受巨大的压力,整个系统很有可能被压垮,所以,我们应该想办法不让这种情况发生,但是由于上述HASH算法本身的缘故,使用取模法进行缓存时,这种情况是无法避免的。</p><h2 id="一致性哈希的基本概念"><a href="#一致性哈希的基本概念" class="headerlink" title="一致性哈希的基本概念"></a>一致性哈希的基本概念</h2><p>一致性Hash算法也是使用取模的方法,只是,刚才描述的取模法是对服务器的数量进行取模,而一致性Hash算法是对2^32取模,什么意思呢?简单来说,一致性Hash算法将整个哈希值空间组织成一个虚拟的圆环,如假设某哈希函数H的值空间为0-2^32-1(即哈希值是一个32位无符号整形),整个哈希环如下:</p><img src="/2019/10/01/一致性哈希/926638-20180323105038179-377052053.png"><blockquote><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">> hash(服务器A的IP地址) % 2^32</span><br><span class="line">></span><br></pre></td></tr></table></figure></blockquote><p>通过上述公式算出的结果一定是一个0到2^32-1之间的一个整数,我们就用算出的这个整数,代表服务器A,既然这个整数肯定处于0到2^32-1之间,那么,上图中的hash环上必定有一个点与这个整数对应,而我们刚才已经说明,使用这个整数代表服务器A,那么,服务器A就可以映射到这个环上。</p><p>以此类推,下一步将各个服务器使用类似的Hash算式进行一个哈希,这样每台机器就能确定其在哈希环上的位置,这里假设将上文中四台服务器使用IP地址哈希后在环空间的位置如下:</p><img src="/2019/10/01/一致性哈希/926638-20180323105141218-1992817322.png"><p>接下来使用如下算法定位数据访问到相应服务器: <strong>将数据key使用相同的函数Hash计算出哈希值,并确定此数据在环上的位置,从此位置沿环顺时针“行走”,第一台遇到的服务器就是其应该定位到的服务器</strong>!</p><h2 id="一致性Hash算法的容错性和可扩展性"><a href="#一致性Hash算法的容错性和可扩展性" class="headerlink" title="一致性Hash算法的容错性和可扩展性"></a>一致性Hash算法的容错性和可扩展性</h2><h3 id="减少机器"><a href="#减少机器" class="headerlink" title="减少机器"></a>减少机器</h3><p>现假设Node C不幸宕机,可以看到此时对象A、B、D不会受到影响,只有C对象被重定位到Node<br>D。一般的,在一致性Hash算法中,如果一台服务器不可用,则<strong>受影响的数据仅仅是此服务器到其环空间中前一台服务器</strong>(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它不会受到影响,如下所示:</p><img src="/2019/10/01/一致性哈希/926638-20180323105553110-1709004647.jpg"><h3 id="增加机器"><a href="#增加机器" class="headerlink" title="增加机器"></a>增加机器</h3><img src="/2019/10/01/一致性哈希/926638-20180323105319554-1536882438.png"><p>此时对象Object A、B、D不受影响,只有对象C需要重定位到新的Node X !一般的,在一致性Hash算法中,如果增加一台服务器,则受影响的数据仅仅是新服务器到其环空间中前一台服务器(即沿着逆时针方向行走遇到的第一台服务器)之间数据,其它数据也不会受到影响。</p><p>综上所述,一致性Hash算法对于节点的增减都只需重定位环空间中的一小部分数据,具有较好的容错性和可扩展性。</p><h2 id="Hash环的数据倾斜问题"><a href="#Hash环的数据倾斜问题" class="headerlink" title="Hash环的数据倾斜问题"></a>Hash环的数据倾斜问题</h2><p>一致性Hash算法在服务节点太少时,容易因为节点分部不均匀而造成数据倾斜(被缓存的对象大部分集中缓存在某一台服务器上)问题,例如系统中只有两台服务器,其环分布如下:</p><img src="/2019/10/01/一致性哈希/926638-20180323105859432-525186842.jpg"><p>此时必然造成大量数据集中到Node A上,而只有极少量会定位到Node B上,从而出现hash环偏斜的情况,当hash环偏斜以后,缓存往往会极度不均衡的分布在各服务器上,如果想要均衡的将缓存分布到2台服务器上,最好能让这2台服务器尽量多的、均匀的出现在hash环上,但是,真实的服务器资源只有2台,我们怎样凭空的让它们多起来呢,没错,就是凭空的让服务器节点多起来,既然没有多余的真正的物理服务器节点,我们就只能<strong>将现有的物理节点通过虚拟的方法复制出来</strong>。</p><p>这些由实际节点虚拟复制而来的节点被称为”虚拟节点”,即对每一个服务节点计算多个哈希,每个计算结果位置都放置一个此服务节点,称为虚拟节点。具体做法可以在服务器IP或主机名的后面增加编号来实现。</p><h2 id="hash-slot"><a href="#hash-slot" class="headerlink" title="hash slot"></a>hash slot</h2><p>redis cluster 有固定的 16384 个 hash slot,对每个 key 计算 CRC16 值,然后对 16384 取模,可以获取 key 对应的 hash slot。</p><p>redis cluster 中每个 master 都会持有部分 slot,比如有 3 个 master,那么可能每个 master 持有 5000 多个 hash slot。hash slot 让 node 的增加和移除很简单,增加一个 master,就将其他 master 的 hash slot 移动部分过去,减少一个 master,就将它的 hash slot 移动到其他 master 上去。移动 hash slot 的成本是非常低的。客户端的 api,可以对指定的数据,让他们走同一个 hash slot,通过 hash tag 来实现。</p><p>任何一台机器宕机,另外两个节点,不影响的。因为 key 找的是 hash slot,不是机器。</p><h2 id="为什么是2的32次方-1"><a href="#为什么是2的32次方-1" class="headerlink" title="为什么是2的32次方-1"></a>为什么是2的32次方-1</h2><p>因为int类型最大值为2的31次方-1,有1位是符号位,不考虑符号位是2的32次方-1个数</p>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>字节码执行引擎</title>
<link href="/2019/10/01/%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E/"/>
<url>/2019/10/01/%E5%AD%97%E8%8A%82%E7%A0%81%E6%89%A7%E8%A1%8C%E5%BC%95%E6%93%8E/</url>
<content type="html"><![CDATA[<p>执行引擎是Java虚拟机最核心的组成部分之一,区别于物理机的执行引擎是直接建立在处理器、硬件、指令集和操作系统层面上的,虚拟机的执行引擎是自己实现的,可以自行制定指令集与执行引擎的结构体系,并且能够执行那些不被硬件直接支持的指令集格式</p><h2 id="运行时栈帧结构"><a href="#运行时栈帧结构" class="headerlink" title="运行时栈帧结构"></a>运行时栈帧结构</h2><p>栈帧是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素;<br><strong>栈帧存储了方法的局部变量表、操作数栈、动态连接和方法返回地址等信息</strong>,<strong>每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机里面从入栈到出栈的过程;</strong><br>栈帧需要分配多少<strong>内存在编译时就完全确定</strong>并写入到方法表的Code属性之中了,不会受到程序运行期变量数据的影响;<br>对于执行引擎来说,在活动线程中只有位于栈顶的栈帧才算有效的,称为当前栈帧,与这个栈帧相关联的方法称为当前方法,<strong>执行引擎运行的所有字节码指令都只针对当前栈帧进行操作。</strong></p><h3 id="局部变量表"><a href="#局部变量表" class="headerlink" title="局部变量表"></a>局部变量表</h3><p>变量值存储空间,用于存放方法参数和方法内部定义的局部变量,Code属性的max_locals确定了该方法所需要分配的局部变量表的最大容量;<br>其容量以变量槽(Variable Slot)为最小单位,虚拟机规范允许Slot的长度随处理器、操作系统或虚拟机的不同而发生变化;<br>一个Slot可以存放一个32位以内的数据类型,包括boolean、byte、char。short、int、float、reference和returnAddress这八种类型;对于64位的数据类型(long和double),虚拟机会以高位对齐的方式为其分配两个连续的Slot空间;</p><h3 id="操作数栈"><a href="#操作数栈" class="headerlink" title="操作数栈"></a>操作数栈</h3><p>常称为操作栈,它是一个后入先出栈;Code属性的max_stacks确定了其最大深度;</p><p>比如整数加法的字节码指令iadd在运行的时候操作数栈中最接近栈顶的两个元素已经存入了两个int型的数值,当执行这个指令时,会将这两个int值出栈并相加,然后将相加的结果入栈;</p><p>操作数栈中元素的类型必须与字节码指令的序列严格匹配;</p><p>Java虚拟机的解释执行引擎称为“基于栈的执行引擎”,其中所指的栈就是操作数栈;</p><h3 id="动态连接"><a href="#动态连接" class="headerlink" title="动态连接"></a>动态连接</h3><p>每个栈帧都包含一个执行运行时常量池中该栈帧所属方法引用,持有这个引用是为了支持方法调用过程中的动态连接(Dynamic Linking);<br>Class文件的常量池的符号引用,有一部分在类加载阶段或者第一次使用时就转换为直接引用,这种称为静态解析,而另外一部分在每一次运行期间转换为直接引用,这部分称为动态连接;</p><h3 id="方法返回地址"><a href="#方法返回地址" class="headerlink" title="方法返回地址"></a>方法返回地址</h3><p>退出方法的方式:正常完成出口和异常完成出口;</p><p>方法退出的过程实际上就等同于把当前栈帧出栈,因此退出时可能只需的操作有:<strong>恢复上层方法的局部变量表和操作数栈,把返回值压入调用者栈帧的操作数中,调整PC计数器的值以只需方法调用指令后面的一套指令等;</strong></p><h2 id="方法调用"><a href="#方法调用" class="headerlink" title="方法调用"></a>方法调用</h2><ul><li>方法调用并不等同于方法执行,方法调用阶段唯一的任务就是确定被调用方法的版本即调用哪一个方法,暂时还不涉及方法内部的具体运行过程;</li><li>Class文件的编译过程中不报警传统编译的连接步骤,一切方法调用在<strong>Class文件里面存储的都只是符号引用</strong>,而不是方法在实际运行时内存布局的入口地址。这个特性给Java带来了更强大的动态扩展能力,但也使得Java方法调用过程变得相对复杂;</li></ul><h3 id="解析"><a href="#解析" class="headerlink" title="解析"></a>解析</h3><p>方法在程序真正运行之前就有一个可确定的调用版本,并且这个方法的调用版本在运行期是不可改变的,这类方法的调用称为解析;<br>在Java语言中符合<strong>编译器可知、运行期不可变</strong>这个要求的方法,主要包括静态方法和私有方法两大类;<br>五条方法调用字节码指令:invokestatic、invokespecial、invokevirtual、invokeinterface、invokedynamic;<br>解析调用是一个静态的过程,在编译期间就完全确定,<strong>在类加载的解析阶段就会把涉及的符号引用全部转变为可确定的直接引用</strong>;而分派调用则可能是静态的也可能是动态的;</p><h3 id="分派"><a href="#分派" class="headerlink" title="分派"></a>分派</h3><p>静态分派:“Human man = new Man();”语句中Human称为变量的静态类型,后面的Man称为变量的实际类型;静态类型和实际类型在程序中都可以发生一些变化,区别是静态类型的变化仅仅在使用时发生,变量本身的静态类型不会被改变,并且最终的<strong>静态类型是在编译器可知的;而实际类型的变化在运行期才确定</strong>,编译器在编译程序的时候并不知道一个对象的实际类型是什么;编译器在重载时是通过参数的静态类型而不是实际类型作为判定依据的;所有根据静态类型来定位方法执行版本的分派动作称为静态分派,其典型应用是<strong>方法重载</strong>;</p><p>动态分派:invokevirtual指令执行的第一步就是在运行期间确定接收者的实际类型,所以<strong>两次调用中invokevirtual指令把常量池中的类方法符号引用解析到了不同的直接引用上</strong>,这个过程就是Java语言中<strong>方法重写</strong>的本质;我们把这种在运行期根据实际类型确定方法执行版本的分派过程称为动态分派;</p><p>单分派与多分派:方法的接收者与方法的参数统称为方法的宗量,根据分派基于多少种宗量,可以将分派分为单分派(根据一个宗量对目标方法进行选择)与多分派(根据多于一个宗量对目标方法进行选择)两种;今天的<strong>Java语言是一门静态多分派、动态单分派的语言;</strong></p><p>虚拟机动态分派的实现:在<strong>方法区中建立一个虚方法表</strong>(Virtual Method Table),使用虚方法表索引来代替元数据查找以提高性能;方法表一般在类加载的连接阶段进行初始化,准备了类的变量初始化值后,虚拟机会把该类的方法表也初始化完毕;</p><h3 id="动态类型语言支持"><a href="#动态类型语言支持" class="headerlink" title="动态类型语言支持"></a>动态类型语言支持</h3><p>JDK 1.7发布增加的invokedynamic指令实现了“动态类型语言”支持,也是为JDK 1.8顺利实现Lambda表达式做技术准备;</p><p><strong>动态类型语言的关键特征是它的类型检查的主体过程是在运行期而不是编译器</strong>,比如JavaScript、Python等;</p><p><strong>Java语言在编译期间就将方法完整的符号引用生成出来,作为方法调用指令的参数存储到Class文件中;这个符号引用包含了此方法定义在哪个具体类型之中、方法的名字以及参数顺序、参数类型和方法返回值等信息;</strong>而在ECMAScript等动态语言中,变量本身是没有类型的,变量的值才具有类型,编译时最多只能确定方法名称、参数、返回值这些信息,而不会去确定方法所在的具体类型;<strong>变量无类型而变量值才有类型,这个特点也是动态类型语言的一个重要特征;</strong></p><p>JDK 1.7实现了JSR-292,新加入的java.lang.invoke包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法外,提供一种新的动态确定目标方法的机制,称为MethodHandle;</p><p>从本质上讲,Reflection(反射)和MethodHandle机制都是在模拟方法调用,但<strong>Reflection是在模拟Java代码层次的方法调用,而MethodHandle是在模拟字节码层次的方法调用</strong>,前者是重量级,而后者是轻量级;另外前者只为Java语言服务,后者可服务于所有Java虚拟机之上的语言;</p><p>每一处含有invokedynamic指令的位置都称为“动态调用点(Dynamic Call Site)”,这条指令的第一个参数不再是代表符号引用的CONSTANT_Methodref_info常量,而是CONSTANT_InvokeDynamic_info常量(可以得到引导方法、方法类型和名称);</p><p>invokedynamic指令与其他invoke指令的最大差别就是它的分派逻辑不是由虚拟机决定的,而是由程序员决定的;</p><h2 id="基于栈的字节码解释执行引擎"><a href="#基于栈的字节码解释执行引擎" class="headerlink" title="基于栈的字节码解释执行引擎"></a>基于栈的字节码解释执行引擎</h2><p>虚拟机是如何执行方法中的字节码指令的。</p><h3 id="解释执行"><a href="#解释执行" class="headerlink" title="解释执行"></a>解释执行</h3><p>Java语言中,javac编译器完成了程序代码经过词法分析、语法分析到抽象语法树,再遍历语法树生成线性的字节码指令流的过程;因为这一部分动作是在Java虚拟机之外进行的,而解释器在虚拟机的内部,所以Java程序的编译就是半独立的实现;</p><h3 id="基于栈的指令集与基于寄存器的指令集"><a href="#基于栈的指令集与基于寄存器的指令集" class="headerlink" title="基于栈的指令集与基于寄存器的指令集"></a>基于栈的指令集与基于寄存器的指令集</h3><ul><li>Java编译器输出的指令集,基本上是一种基于栈的指令集架构,指令流中的指令大部分是零地址指令,它们依赖操作数栈进行工作;</li><li>基于栈的指令集主要的优点是可移植性,寄存器由硬件直接提供,程序直接依赖这些硬件寄存器则不可避免地要受到硬件的约束;主要缺点是执行速度相对来说会稍慢一点;</li></ul>]]></content>
<tags>
<tag> jvm </tag>
</tags>
</entry>
<entry>
<title>对象</title>
<link href="/2019/09/30/%E5%AF%B9%E8%B1%A1/"/>
<url>/2019/09/30/%E5%AF%B9%E8%B1%A1/</url>
<content type="html"><![CDATA[<h2 id="对象的创建"><a href="#对象的创建" class="headerlink" title="对象的创建"></a>对象的创建</h2><p>Java的对象创建大致有如下四种方式:</p><ul><li><strong>new关键字</strong></li><li><strong>使用newInstance()方法</strong><br>这里包括Class类的newInstance()方法和Constructor类的newInstance()方法(前者其实也是调用的后者)。</li><li><strong>使用clone()方法</strong><br>要使用clone()方法我们必须实现实现Cloneable接口,用clone()方法创建对象并不会调用任何构造函数。即我们所说的浅拷贝(多个引用,一个对象)。</li><li><strong>反序列化</strong><br>要实现反序列化我们需要让我们的类实现Serializable接口。当我们序列化和反序列化一个对象,JVM会给我们创建一个单独的对象,在反序列化时,JVM创建对象并不会调用任何构造函数。即我们所说的深拷贝(引用不同对象)。</li></ul><p>上面的四种创建对象的方法除了第一种使用new指令之外,其他三种都是使用<strong>invokespecial(构造函数的直接调用)</strong></p><h2 id="虚拟机遇到new指令的时候对象是如何创建的"><a href="#虚拟机遇到new指令的时候对象是如何创建的" class="headerlink" title="虚拟机遇到new指令的时候对象是如何创建的"></a>虚拟机遇到new指令的时候对象是如何创建的</h2><h3 id="类加载检查"><a href="#类加载检查" class="headerlink" title="类加载检查"></a>类加载检查</h3><p>虚拟机遇到一条new指令时,首先将去检查这个指令的参数是否能<strong>在常量池中定位到一个类的符号引用</strong>,并且检查这个符号引用代表的类是否已被加载、解析和初始化过的,如果没有,则必须先执行相应的<strong>类加载过程</strong></p><blockquote><p>符号引用可以是任何形式的字面量,与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中;而直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,它和虚拟机实现的内存布局相关,引用的目标必定以及在内存中存在;</p></blockquote><h3 id="分配内存"><a href="#分配内存" class="headerlink" title="分配内存"></a>分配内存</h3><p>在类加载检查通过后,虚拟机就将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务具体便等同于<strong>从Java堆中划出一块大小确定的内存空间</strong></p><ul><li>Java堆中内存绝对规整<br>所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。</li><li>Java堆中的内存不规整<br>已被使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“空闲列表”(Free List)。</li></ul><p>选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时(说明一下,CMS收集器可以通过UseCMSCompactAtFullCollection或CMSFullGCsBeforeCompaction来整理内存),就通常采用空闲列表。<br>除如何划分可用空间之外,另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并非线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。解决这个问题有如下两个方案:</p><ul><li><strong>对分配内存空间的动作进行同步</strong><br>实际上虚拟机是采用<strong>CAS</strong>配上<strong>失败重试</strong>的方式保证更新操作的原子性。</li><li><strong>把内存分配的动作按照线程划分在不同的空间之中进行</strong><br>即每个线程在Java堆中预先分配一小块内存,称为<strong>本地线程分配缓冲(TLAB ,Thread Local Allocation Buffer)</strong>,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完,分配新的TLAB时才需要同步锁定。虚拟机是否使用TLAB,可以通过<strong>-XX:+/-UseTLAB</strong>参数来设定。</li></ul><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>内存分配完成之后,虚拟机需要<strong>将分配到的内存空间都初始化为零值(不包括对象头)</strong>,如果使用TLAB的话,这一个工作也可以提前至TLAB分配时进行。这步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用。</p><h3 id="设置对象头"><a href="#设置对象头" class="headerlink" title="设置对象头"></a>设置对象头</h3><p>接下来,虚拟机要设置对象的信息(如这个对象是哪个类的实例、如何才能找到类的元数据信息、对象的哈希码、对象的GC分代年龄等信息)并存放在对象的对象头(Object Header)中。根据虚拟机当前的运行状态的不同,如是否启用偏向锁等,对象头会有不同的设置方式。</p><h3 id="执行-lt-init-gt-方法"><a href="#执行-lt-init-gt-方法" class="headerlink" title="执行<init>方法"></a>执行<code><init></code>方法</h3><p>在虚拟机的视角来看,一个新的对象已经产生了。但是在Java程序的视角看来,对象创建才刚刚开始——<code><init></code>方法还没有执行,所有的字段都还为零值。所以一般来说(由字节码中是否跟随有invokespecial指令所决定),new指令之后会接着执行<code><init></code>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。</p><h2 id="对象的内存布局"><a href="#对象的内存布局" class="headerlink" title="对象的内存布局"></a>对象的内存布局</h2><p>HotSpot虚拟机中,对象在内存中存储的布局可以分为三块区域:<strong>对象头(Header)</strong>、<strong>实例数据(Instance Data)</strong>和<strong>对齐填充(Padding)</strong>。</p><h3 id="对象头"><a href="#对象头" class="headerlink" title="对象头"></a>对象头</h3><ul><li><strong>对象自身的运行时数据 “Mark Word”</strong><br>对象自身的运行时数据 “Mark Word”,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,这部分数据的长度在32位和64位的虚拟机(暂不考虑开启压缩指针的场景)中分别为32个和64个Bits,官方称它为“Mark Word”。Mark Word被设计成一个<strong>非固定的数据结构</strong>以便在极小的空间内存储尽量多的信息,它会<strong>根据对象的状态复用自己的存储空间</strong>。</li></ul><img src="/2019/09/30/对象/8be38542.png" title="对象头"><ul><li><strong>类型指针</strong><br>类型指针即<strong>对象指向它的类元数据的指针</strong>,虚拟机通过这个指针来确定这个对象是哪个类的实例。并不是所有的虚拟机实现都必须在对象数据上保留类型指针,换句话说<strong>查找对象的元数据信息并不一定要经过对象本身</strong>,这点我们在下一节讨论。另外,如果对象是一个Java数组,那在对象头中还必须有一块用于<strong>记录数组长度</strong>的数据,因为虚拟机可以通过普通Java对象的元数据信息确定Java对象的大小,但是从数组的元数据中无法确定数组的大小。</li></ul><h3 id="实例数据"><a href="#实例数据" class="headerlink" title="实例数据"></a>实例数据</h3><p>实例数据是对象真正存储的有效信息,也既是我们在程序代码里面所定义的各种类型的字段内容,无论是从父类继承下来的,还是在子类中定义的都需要记录起来。这部分的存储顺序会受到虚拟机分配策略参数(FieldsAllocationStyle)和字段在Java源码中定义顺序的影响。HotSpot虚拟机默认的分配策略为longs/doubles、ints、shorts/chars、bytes/booleans、oops(Ordinary Object Pointers),从分配策略中可以看出,相同宽度的字段总是被分配到一起。在满足这个前提条件的情况下,在父类中定义的变量会出现在子类之前。如果CompactFields参数值为true(默认为true),那子类之中较窄的变量也可能会插入到父类变量的空隙之中。</p><h3 id="对齐填充"><a href="#对齐填充" class="headerlink" title="对齐填充"></a>对齐填充</h3><p>对齐填充并不是必然存在的,也没有特别的含义,它仅仅起着占位符的作用。由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是对象的大小必须是8字节的整数倍。对象头部分正好似8字节的倍数(1倍或者2倍),因此当对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。</p><h2 id="对象的访问定位"><a href="#对象的访问定位" class="headerlink" title="对象的访问定位"></a>对象的访问定位</h2><p>我们的Java程序需要通过栈上的对象引用(reference)数据(存储在栈上的局部变量表中)来操作堆上的具体对象。主流的访问方式有使用句柄和直接指针两种。</p><h3 id="使用句柄访问"><a href="#使用句柄访问" class="headerlink" title="使用句柄访问"></a>使用句柄访问</h3><p>如果使用句柄访问的话,<strong>Java堆中</strong>将会划分出一块内存来作为<strong>句柄池</strong>,reference中存储的就是对象的句柄地址,而句柄中包含了<strong>对象实例数据</strong>与<strong>类型数据</strong>的各自的<strong>具体地址信息</strong>。</p><p>使用句柄访问的最大好处就是<strong>reference中存储的是稳定的句柄地址</strong>,在对象被移动(垃圾收集时移动对象是非常普遍的行为)时<strong>只会改变句柄中的实例数据指针,而reference本身不需要被修改</strong>。</p><p>如下图所示:</p><img src="/2019/09/30/对象/bfd967c5.png" title="使用句柄访问"><h3 id="使用直接指针访问"><a href="#使用直接指针访问" class="headerlink" title="使用直接指针访问"></a>使用直接指针访问</h3><p>如果使用直接指针访问的话,Java堆对象的布局中就必须考虑如何放置访问类型数据的相关信息,reference中存储的直接就是对象地址,使用直接指针来访问最大的好处就是<strong>速度更快</strong>,它<strong>节省了一次指针定位的时间开销</strong>,由于对象访问的在Java中非常频繁,因此这类开销积小成多也是一项 非常可观的执行成本。<strong>HotSpot是使用直接指针进行对象访问的</strong></p><p>如下图所示:</p><img src="/2019/09/30/对象/f5086a4d.png" title="使用直接指针访问">]]></content>
<tags>
<tag> jvm </tag>
</tags>
</entry>
<entry>
<title>内存分配策略</title>
<link href="/2019/09/30/%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E7%AD%96%E7%95%A5/"/>
<url>/2019/09/30/%E5%86%85%E5%AD%98%E5%88%86%E9%85%8D%E7%AD%96%E7%95%A5/</url>
<content type="html"><![CDATA[<h2 id="给对象分配内存"><a href="#给对象分配内存" class="headerlink" title="给对象分配内存"></a>给对象分配内存</h2><p>对象的内存分配通常是在堆上分配(除此以外还有可能经过JIT编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是固定的,实际取决于垃圾收集器的具体组合以及虚拟机中与内存相关的参数的设置。</p><p>在类加载检查通过后,虚拟机就将为新生对象分配内存。对象所需内存的大小在类加载完成后便可完全确定,为对象分配空间的任务具体便等同于<strong>从Java堆中划出一块大小确定的内存空间</strong></p><ul><li><strong>Java堆中内存绝对规整</strong><br>所有用过的内存都被放在一边,空闲的内存被放在另一边,中间放着一个指针作为分界点的指示器,那所分配内存就仅仅是把那个指针向空闲空间那边挪动一段与对象大小相等的距离,这种分配方式称为“指针碰撞”(Bump The Pointer)。</li><li><strong>Java堆中的内存不规整</strong><br>已被使用的内存和空闲的内存相互交错,那就没有办法简单的进行指针碰撞了,虚拟机就必须维护一个列表,记录哪些内存块是可用的,在分配的时候从列表中找到一块足够大的空间划分给对象实例,并更新列表上的记录,这种分配方式称为“<strong>空闲列表</strong>”(Free List)。</li></ul><p>选择哪种分配方式由Java堆是否规整决定,而Java堆是否规整又由所采用的垃圾收集器是否带有压缩整理功能决定。因此在使用Serial、ParNew等带Compact过程的收集器时,系统采用的分配算法是指针碰撞,而使用CMS这种基于Mark-Sweep算法的收集器时(说明一下,CMS收集器可以通过UseCMSCompactAtFullCollection或CMSFullGCsBeforeCompaction来整理内存),就通常采用空闲列表。<br>除如何划分可用空间之外,另外一个需要考虑的问题是对象创建在虚拟机中是非常频繁的行为,即使是仅仅修改一个指针所指向的位置,在并发情况下也并非线程安全的,可能出现正在给对象A分配内存,指针还没来得及修改,对象B又同时使用了原来的指针来分配内存。解决这个问题有如下两个方案:</p><ul><li><strong>对分配内存空间的动作进行同步</strong><br>实际上虚拟机是采用<strong>CAS</strong>配上<strong>失败重试</strong>的方式保证更新操作的原子性。</li><li><strong>把内存分配的动作按照线程划分在不同的空间之中进行</strong><br>即每个线程在Java堆中预先分配一小块内存,称为<strong>本地线程分配缓冲(TLAB ,Thread Local Allocation Buffer)</strong>,哪个线程要分配内存,就在哪个线程的TLAB上分配,只有TLAB用完,分配新的TLAB时才需要同步锁定。虚拟机是否使用TLAB,可以通过<strong>-XX:+/-UseTLAB</strong>参数来设定。</li></ul><h3 id="对象优先在Eden区分配"><a href="#对象优先在Eden区分配" class="headerlink" title="对象优先在Eden区分配"></a>对象优先在Eden区分配</h3><p>大多数情况下,对象在新生代的Eden区中分配。当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC。</p><h3 id="大对象直接进入老年代"><a href="#大对象直接进入老年代" class="headerlink" title="大对象直接进入老年代"></a>大对象直接进入老年代</h3><p>所谓的大对象是指,需要大量连续内存空间的Java对象,最典型的大对象就是很长的字符串以及数组。大对象对虚拟机的内存分配来说是一个坏消息(尤其是遇到朝生夕灭的“短命大对象”,写程序时应避免),<strong>经常出现大对象容易导致内存还有不少空间时就提前触发GC以获取足够的连续空间来安置它们</strong>。</p><p>虚拟机提供了一个<strong>-XX:PretenureSizeThreshold</strong>参数,令大于这个设置值的对象直接在老年代分配。这样做的目的是<strong>避免在Eden区及两个Survivor区之间发生大量的内存复制</strong>(新生代采用复制算法回收内存)。</p><h3 id="长期存活的对象将进入老年代"><a href="#长期存活的对象将进入老年代" class="headerlink" title="长期存活的对象将进入老年代"></a>长期存活的对象将进入老年代</h3><p>虚拟机给每个对象定义了一个<strong>对象年龄(Age)计数器</strong>。<strong>如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。</strong>对象晋升老年代的年龄阈值,可以通过参数<strong>-XX:MaxTenuringThreshold</strong>设置。</p><h3 id="空间分配担保"><a href="#空间分配担保" class="headerlink" title="空间分配担保"></a>空间分配担保</h3><p><strong>在发生Minor GC之前</strong>,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果这个条件成立,那么Minor GC可以确保是安全的。如果不成立,则虚拟机会查看<strong>HandlePromotionFailure</strong>设置值是否允许担保失败。如果允许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的;如果小于,或者<strong>HandlePromotionFailure</strong>设置不允许冒险,那这时也要改为进行一次<strong>Full GC</strong>。</p><p><strong>当出现大量对象在Minor GC后仍然存活的情况(最极端的情况就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,把Survivor无法容纳的对象直接进入老年代。</strong></p><p>取平均值进行比较其实仍然是一种动态概率的手段,也就是说,如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致<strong>担保失败(Handle Promotion Failure)</strong>。如果出现了<strong>HandlePromotionFailure</strong>失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将<strong>HandlePromotionFailure</strong>开关打开,避免Full GC过于频繁。</p><h2 id="Full-GC的触发条件"><a href="#Full-GC的触发条件" class="headerlink" title="Full GC的触发条件"></a>Full GC的触发条件</h2><p>对于Minor GC,其触发条件非常简单,当Eden区空间满时,就将触发一次Minor GC。而Full GC则相对复杂。</p><h3 id="调用System-gc"><a href="#调用System-gc" class="headerlink" title="调用System.gc()"></a>调用System.gc()</h3><p>此方法的调用是<strong>建议JVM进行Full GC</strong>,虽然只是建议而非一定,<strong>但很多情况下它会触发 Full GC</strong>,从而增加Full GC的频率,也即增加了间歇性停顿的次数。因此强烈建议能不使用此方法就不要使用,让虚拟机自己去管理它的内存,可通过<strong>-XX:+ DisableExplicitGC</strong>来禁止RMI调用System.gc()。</p><h3 id="老年代空间不足"><a href="#老年代空间不足" class="headerlink" title="老年代空间不足"></a>老年代空间不足</h3><p>老年代空间不足的常见场景为前文所讲的<strong>大对象直接进入老年代</strong>、<strong>长期存活的对象进入老年代</strong>等,当执行Full GC后空间仍然不足,则抛出如下错误:<br><code>Java.lang.OutOfMemoryError: Java heap space</code></p><p>为避免以上两种状况引起的Full GC,调优时应<strong>尽量做到让对象在Minor GC阶段被回收、让对象在新生代多存活一段时间及不要创建过大的对象及数组。</strong></p><h3 id="空间分配担保失败"><a href="#空间分配担保失败" class="headerlink" title="空间分配担保失败"></a>空间分配担保失败</h3><p>使用复制算法的Minor GC需要老年代的内存空间作担保,如果出现了<strong>HandlePromotionFailure</strong>担保失败,则会触发Full GC。</p><h3 id="JDK-1-7及以前的永久代空间不足"><a href="#JDK-1-7及以前的永久代空间不足" class="headerlink" title="JDK 1.7及以前的永久代空间不足"></a>JDK 1.7及以前的永久代空间不足</h3><p>在JDK 1.7及以前,HotSpot虚拟机中的方法区是用永久代实现的,永久代中存放的为一些class的信息、常量、静态变量等数据,<strong>当系统中要加载的类、反射的类和调用的方法较多时</strong>,Permanet Generation可能会被占满,在未配置为采用CMS GC的情况下也会执行Full GC。如果经过Full GC仍然回收不了,那么JVM会抛出如下错误信息:<br> <code>java.lang.OutOfMemoryError: PermGen space</code><br> 为避免PermGen占满造成Full GC现象,可采用的方法为增大PermGen空间或转为使用CMS GC。</p><p>在JDK 1.8中用元空间替换了永久代作为方法区的实现,<strong>元空间是本地内存,因此减少了一种Full GC触发的可能性。</strong></p><h3 id="Concurrent-Mode-Failure"><a href="#Concurrent-Mode-Failure" class="headerlink" title="Concurrent Mode Failure"></a>Concurrent Mode Failure</h3><p>执行CMS GC的过程中同时有对象要放入老年代,而此时老年代空间不足(有时候“空间不足”是CMS GC时<strong>当前的浮动垃圾过多导致暂时性的空间</strong>不足触发Full GC),便会报<code>Concurrent Mode Failure</code>错误,并触发Full GC。</p>]]></content>
<tags>
<tag> jvm </tag>
</tags>
</entry>
<entry>
<title>类加载机制</title>
<link href="/2019/09/30/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/"/>
<url>/2019/09/30/%E7%B1%BB%E5%8A%A0%E8%BD%BD%E6%9C%BA%E5%88%B6/</url>
<content type="html"><![CDATA[<h2 id="概述"><a href="#概述" class="headerlink" title="概述"></a>概述</h2><ul><li>虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制。</li><li>在Java语言里面,类型的加载、连接和初始化过程都是在程序运行期间完成,这虽然增量一些性能开销,但是会为Java应用程序提供高度的灵活性。</li></ul><h2 id="类加载的时机"><a href="#类加载的时机" class="headerlink" title="类加载的时机"></a>类加载的时机</h2><p>类的整个生命周期:<strong>加载、验证、准备、解析、初始化</strong>、使用和卸载;其中验证、准备和解析统称为连接;<br>虚拟机规范没有强制约束类加载的时机,但严格规定了有且只有5种情况必须立即对类进行初始化:遇到<strong>new、getstatic、putstatic和invokestatic指令</strong>;对类进行<strong>反射调用</strong>时如果类没有进行过初始化;<strong>初始化时发现父类还没有进行初始化</strong>;<strong>虚拟机启动指定的主类</strong>;动态语言中MethodHandle实例最后解析结果REF_getStatic等的方法句柄对应的类没有初始化时;</p><h3 id="加载"><a href="#加载" class="headerlink" title="加载"></a>加载</h3><ul><li>通过一个类的全限定名来获取定义此类的二进制字节流;</li><li>将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;</li><li>在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;</li></ul><h3 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h3><ul><li>验证是连接阶段的第一步,其目的是确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;</li><li>验证阶段是非常重要的,这个阶段是否严谨决定了Java虚拟机是否能承受恶意代码的攻击;</li><li>校验动作:文件格式验证(基于二进制字节流)、元数据验证(对类的元数据语义分析)、字节码验证(对方法体语义分析)、符号引用验证(对类自身以外的信息进行匹配性校验);</li></ul><h3 id="准备"><a href="#准备" class="headerlink" title="准备"></a>准备</h3><ul><li>正式为变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在这个方法区中进行分配;</li><li>需要强调两点:这时候内存分配的仅包括类变量,而不包括类实例变量;这里所说的初始化通常情况下是数据类型的零值,真正的赋值是在初始化阶段,如果是static final的则是直接赋值;</li></ul><h3 id="解析"><a href="#解析" class="headerlink" title="解析"></a>解析</h3><ul><li>解析阶段是虚拟机将常量池内的符号引用(如CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等7种)替换为直接引用的过程;</li><li>符号引用可以是任何形式的字面量,与虚拟机实现的内存布局无关,引用的目标并不一定已经加载到内存中;而直接引用是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄,它和虚拟机实现的内存布局相关,引用的目标必定以及在内存中存在;</li><li>对同一个符号引用进行多次解析请求是很常见的事情,虚拟机实现可以对第一次解析的结果进行缓存;</li></ul><h3 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h3><p>是类加载过程的最后一步,真正开始执行类中定义的Java程序代码(或者说是字节码);<br>初始化阶段是执行类构造器方法的过程,该方法是由<strong>编译器自动收集类中的所有类变量的赋值动作和静态语句块中的语句</strong>合并产生的;<br>方法与类的构造函数(或者说是实例构造器方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的方法执行之前,父类的方法已执行完毕;<br>执行接口的方法不需要先执行父接口的方法,只有当父接口中定义的变量使用时父接口才会初始化,接口的实现类在初始化时也一样不会执行接口的方法;<br>方法初始化是加锁阻塞等待的,应当避免在方法中有耗时很长的操作;</p><h2 id="类加载器"><a href="#类加载器" class="headerlink" title="类加载器"></a>类加载器</h2><p>虚拟机设计团队把类加载阶段的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到虚拟机外部去实现,实现这个动作的代码模块称为类加载器;</p><h3 id="类与类加载器"><a href="#类与类加载器" class="headerlink" title="类与类加载器"></a>类与类加载器</h3><p>对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机的唯一性,每一个类加载器都拥有一个独立的类名称空间;<br>比较两个类是否相等(如Class对象的equals方法、isAssignableFrom方法、isInstance方法),只有在这两个类是由同一个类加载器加载的前提下才有意义;</p><h3 id="双亲委派模型"><a href="#双亲委派模型" class="headerlink" title="双亲委派模型"></a>双亲委派模型</h3><img src="/2019/09/30/类加载机制/3709321-4e150bb5176ab73d.png" title="双亲委派模型"><ul><li>三种系统提供的类加载器:启动类加载器(Bootstrap ClassLoader)、扩展类加载器(Extension ClassLoader)、应用程序类加载器(Application ClassLoader);</li><li>双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器,这里一般不会以继承的关系来实现,而是使用组合的关系来复用父加载器的代码;</li><li>其工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,只有父类加载器反馈自己无法完成这个加载请求时(它的搜索范围中没有找到所需的类),子加载器才会尝试自己去加载;</li><li>这样的好处是Java类随着它的类加载器具备了一种带有优先级的层次关系,对保证Java程序的稳定运作很重要;</li><li>实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass方法中,逻辑清晰易懂;</li></ul><p>覆写loadClass,不让系统加载器去加载就打破了双亲委托机制,java自带的包还是要让系统加载器去加载。自己编写java.lang.String,然后要加载是不行的,会报安全异常,不允许你自己加载java.lang.String的</p>]]></content>
<tags>
<tag> jvm </tag>
</tags>
</entry>
<entry>
<title>GC算法</title>
<link href="/2019/09/30/GC%E7%AE%97%E6%B3%95/"/>
<url>/2019/09/30/GC%E7%AE%97%E6%B3%95/</url>
<content type="html"><![CDATA[<p>在Java的运行时数据区中,程序计数器、虚拟机栈、本地方法栈三个区域都是线程私有的,随线程而生,随线程而灭,在方法结束或线程结束时,内存自然就跟着回收了,不需要过多考虑回收的问题。而<strong>Java堆</strong>和<strong>方法区</strong>则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在程序处于运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的,垃圾回收器关注的是这部分内存</p><h2 id="对象已死吗"><a href="#对象已死吗" class="headerlink" title="对象已死吗"></a>对象已死吗</h2><h3 id="引用计数算法"><a href="#引用计数算法" class="headerlink" title="引用计数算法"></a>引用计数算法</h3><p>引用计数算法是在JVM中被<strong>摒弃</strong>的一种对象存活判定算法</p><p>用引用计数器判断对象是否存活的过程是这样的:<strong>给对象中添加一个引用计数器,每当有一个地方引用它时,计数器加1;当引用失效时,计数器减1;任何时刻计数器为0的对象就是不可能再被使用的。</strong></p><p>用计数算法的实现简单,判定效率也很高,大部分情况下是一个不错的算法。它没有被JVM采用的原因是<strong>它很难解决对象之间循环引用的问题</strong>。</p><h3 id="可达性分析算法"><a href="#可达性分析算法" class="headerlink" title="可达性分析算法"></a>可达性分析算法</h3><p>可达性分析(tracing GC)来判定对象是否存活的基本思路是:通过一系列的称为“GC Roots”的对象作为起点,从这些节点向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是GC Roots 到这个对象不可达)时,则证明此对象时不可用的。<br>GC Roots其实不是一组对象,而通常是一组特别管理的指向引用类型对象的指针,只有引用类型的变量才被认为是Roots,值类型的变量永远不被认为是Roots。</p><ul><li><strong>虚拟机栈(栈帧中的局部变量表,Local Variable Table)</strong>中引用的对象。</li><li><strong>方法区中类静态属性</strong>引用的对象。</li><li><strong>方法区中常量</strong>引用的对象。</li><li><strong>本地方法栈中JNI(即一般说的Native方法)</strong>引用的对象。</li></ul><p>GC管理的区域是Java堆,<strong>虚拟机栈</strong>、<strong>方法区</strong>和<strong>本地方法栈</strong>不被GC所管理,因此选用这些区域内引用的对象作为GC Roots,是<strong>不会被GC所回收</strong>的。其中虚拟机栈和本地方法栈都是线程私有的内存区域,只要线程没有终止,就能确保它们中引用的对象的存活。而方法区中类静态属性引用的对象是显然存活的。常量引用的对象在当前可能存活,因此,也可能是GC roots的一部分。</p><h2 id="引用"><a href="#引用" class="headerlink" title="引用"></a>引用</h2><h3 id="强引用"><a href="#强引用" class="headerlink" title="强引用"></a>强引用</h3><p>被强引用关联的对象不会被回收。<br>使用 new 一个新对象的方式来创建强引用。</p><h3 id="软引用"><a href="#软引用" class="headerlink" title="软引用"></a>软引用</h3><p>被软引用关联的对象只有在内存不够的情况下才会被回收。<br>使用 SoftReference 类来创建软引用。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">Object obj = new Object();</span><br><span class="line">SoftReference<Object> sf = new SoftReference<Object>(obj);</span><br><span class="line">obj = null; // 使对象只被软引用关联</span><br></pre></td></tr></table></figure><h3 id="弱引用"><a href="#弱引用" class="headerlink" title="弱引用"></a>弱引用</h3><p>被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。</p><h3 id="虚引用"><a href="#虚引用" class="headerlink" title="虚引用"></a>虚引用</h3><p>又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。<br>为一个对象设置虚引用关联的唯一目的就是能在这个对象被回收时收到一个系统通知。</p><h2 id="两次标记与-finalize-方法"><a href="#两次标记与-finalize-方法" class="headerlink" title="两次标记与 finalize()方法"></a>两次标记与 finalize()方法</h2><p>如果对象在进行可达性分析后发现没有与 GC Roots相连接的引用链,那它将会被<strong>第一次标记</strong>并且进行一次筛选,筛选的条件是<strong>此对象是否有必要执行finaliza()方法</strong>。当对象没有覆盖<code>finalize()</code>方法,或者<code>finalize()</code>方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”。</p><p>如果这个对象被判定为有必要执行<code>finalize()</code>方法,那么此对象将会放置在一个叫做 F-Queue 的队列中,并在稍后由一个虚拟机自动建立的、低优先级的Finalizer线程去执行它。这里所谓的“执行”是指虚拟机会触发此方法,但并<strong>不承诺会等待它运行结束</strong>,原因是:如果一个对象在<code>finalize()</code>方法中执行缓慢,或者发生了死循环(更极端的情况),将很可能导致F-Queue 队列中的其它对象永久处于等待,甚至导致整个内存回收系统崩溃。</p><p><code>finalize()</code>方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue 队列中的对象进行<strong>第二次小规模的标记</strong>。如果对象想在<code>finalize()</code>方法中成功拯救自己,<strong>只要重新与引用链上的任何一个对象建立关联即可,例如把自己(this关键字)赋值给某个类变量或者对象的成员变量,这样在第二次标记时它将被移出“即将回收”的集合</strong>;如果对象这时候还没有逃脱,基本上它就真的被回收了。</p><p>任何一个对象的<code>finalize()</code>方法都只会被系统调用一次,如果对象面临下一次回收,它的<code>finalize()</code>方法不会再被执行,因此第二次逃脱行动失败。</p><p><strong>它的运行代价高昂,不确定性大,无法保证各个对象的调用顺序。</strong></p><h2 id="垃圾收集算法"><a href="#垃圾收集算法" class="headerlink" title="垃圾收集算法"></a>垃圾收集算法</h2><h3 id="标记-清除(Mark-Sweep)算法"><a href="#标记-清除(Mark-Sweep)算法" class="headerlink" title="标记-清除(Mark-Sweep)算法"></a>标记-清除(Mark-Sweep)算法</h3><p>顾名思义,算法分成“标记”、“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象<br>标记-清除算法的不足主要有以下两点:</p><ul><li><strong>空间问题</strong>,标记清除之后会产生大量不连续的<strong>内存碎片</strong>,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不触发另一次垃圾收集动作。</li><li><strong>效率问题</strong>,因为内存碎片的存在,操作会变得更加费时,因为查找下一个可用空闲块已不再是一个简单操作。</li></ul><img src="/2019/09/30/GC算法/efc6204a.png" title="标记-清除"><h3 id="复制(Copying)算法"><a href="#复制(Copying)算法" class="headerlink" title="复制(Copying)算法"></a>复制(Copying)算法</h3><p><strong>将可用内存按容量分成大小相等的两块</strong>,每次只使用其中的一块。<strong>当这一块内存用完,就将还存活着的对象复制到另一块上面</strong>,然后再把已使用过的内存空间一次清理掉。</p><p>这样做使得<strong>每次都是对整个半区进行内存回收</strong>,内存分配时也就<strong>不用考虑内存碎片</strong>等复杂情况,只要<strong>移动堆顶指针,按顺序分配内存</strong>即可,实现简单,运行高效。只是这种算法的代价是<strong>将内存缩小为原来的一半</strong>,代价可能过高了。复制算法的执行过程如下图所示:</p><img src="/2019/09/30/GC算法/f1cada8a.png" title="复制(Copying)算法"><h3 id="标记-整理(Mark-Compact)算法"><a href="#标记-整理(Mark-Compact)算法" class="headerlink" title="标记-整理(Mark-Compact)算法"></a>标记-整理(Mark-Compact)算法</h3><p>此算法的标记过程与标记-清除算法一样,但后续步骤不是直接对可回收对象进行清理,而是让<strong>所有存活的对象都向一端移动,然后直接清理掉边界以外的内存</strong></p><img src="/2019/09/30/GC算法/d3d3277f.png" title="标记-整理(Mark-Compact)算法"><h2 id="Minor-GC与复制算法"><a href="#Minor-GC与复制算法" class="headerlink" title="Minor GC与复制算法"></a>Minor GC与复制算法</h2><p>现在的商业虚拟机都使用复制算法来回收新生代。新生代的GC又叫“Minor GC”,IBM公司的专门研究表明:新生代中的对象98%是“朝生夕死”的,所以<strong>Minor GC非常频繁</strong>,一般回收速度也比较快,同时“朝生夕死”的特性也使得Minor GC使用复制算法时不需要按照1:1的比例来划分新生代内存空间。</p><h3 id="Minor-GC过程"><a href="#Minor-GC过程" class="headerlink" title="Minor GC过程"></a>Minor GC过程</h3><p>新生代将内存分为<strong>一块较大的Eden空间</strong>和<strong>两块较小的Survivor空间(From Survivor和To Survivor)</strong>,<strong>每次Minor GC都使用Eden和From Survivor</strong>,当回收时,<strong>将Eden和From Survivor中还存活着的对象都一次性地复制到另外一块To Survivor空间上</strong>,最后清理掉Eden和刚使用的Survivor空间。</p><p>HotSpot虚拟机默认的<strong>Eden : Survivor</strong>的比例是<strong>8 : 1</strong>,由于一共有两块Survivor,所以<strong>每次新生代中可用内存空间为整个新生代容量的90%(80%+10%)</strong>,只有10%的容量会被“浪费”。</p><h3 id="分配担保"><a href="#分配担保" class="headerlink" title="分配担保"></a>分配担保</h3><p>我们没有办法保证每次回收都只有不多于10%的对象存活,<strong>当Survivor空间不够用时</strong>,需要依赖<strong>老年代内存</strong>进行<strong>分配担保(Handle Promotion)</strong>。如果另外一块Survivor上没有足够空间存放上一次新生代收集下来的存活对象,这些对象将直接通过分配担保机制进入老年代。(<strong>可能Full GC</strong>)</p><h2 id="分代收集(Generational-Collection)算法"><a href="#分代收集(Generational-Collection)算法" class="headerlink" title="分代收集(Generational Collection)算法"></a>分代收集(Generational Collection)算法</h2><p>当前商业虚拟机的垃圾收集都采用<strong>分代收集(Generational Collection)算法</strong>,此算法相较于前几种没有什么新的特征,主要思想为:根据对象存活周期的不同将内存划分为几块,一般是把Java堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适合的收集算法:</p><ul><li><strong>新生代</strong><br>在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用<strong>复制算法</strong>,只需要付出少量存活对象的复制成本就可以完成收集。</li><li><strong>老年代</strong><br>在老年代中,因为对象存活率高、没有额外空间对它进行分配担保,就必须使用<strong>“标记-清除”</strong>或<strong>“标记-整理”</strong>算法来进行回收。</li></ul><h2 id="各种GC的区别"><a href="#各种GC的区别" class="headerlink" title="各种GC的区别"></a>各种GC的区别</h2><h3 id="Minor-GC"><a href="#Minor-GC" class="headerlink" title="Minor GC"></a>Minor GC</h3><p>当Eden区被对象填满时,就会执行Minor GC。并把所有存活下来的对象转移到其中一个survivor区。</p><p>Minor GC同样会检查存活下来的对象,并把它们转移到另一个survivor区。这样在一段时间内,总会有一个空的survivor区。</p><p>经过多次GC周期后,仍然存活下来的对象会被转移到年老代内存空间。通常这是在年轻代有资格提升到年老代前通过设定年龄阈值来完成的。</p><h3 id="Major-GC"><a href="#Major-GC" class="headerlink" title="Major GC"></a>Major GC</h3><p>老年代的垃圾收集叫做Major GC,Major GC通常是跟full GC是等价的,收集整个GC堆。</p><p>Minor GC和Major GC其实就是年轻代GC和年老年GC的俗称。而在Hotspot VM具体实现的收集器:Serial GC, Parallel GC, CMS, G1 GC中,大致可以对应到某个Young GC和Old GC算法组合。</p><h3 id="Full-GC"><a href="#Full-GC" class="headerlink" title="Full GC"></a>Full GC</h3><p>Full GC定义是相对明确的,就是针对整个新生代、老生代、元空间(metaspace,java8以上版本取代perm gen)的全局范围的GC。</p><h2 id="HotSpot的算法实现"><a href="#HotSpot的算法实现" class="headerlink" title="HotSpot的算法实现"></a>HotSpot的算法实现</h2><h3 id="枚举根节点"><a href="#枚举根节点" class="headerlink" title="枚举根节点"></a>枚举根节点</h3><p>从可达性分析中<strong>从GC Roots节点找引用链</strong>这个操作为例,可作为GC Roots的节点主要在<strong>全局性的引用</strong>(例如常量或类静态属性)与<strong>执行上下文</strong>(例如栈帧中的局部变量表)中,现在很多应用仅仅方法区就有数百兆,如果要逐个检查这里面的引用,那么必然会消耗很多时间。</p><h3 id="GC停顿(”Stop-The-World”)"><a href="#GC停顿(”Stop-The-World”)" class="headerlink" title="GC停顿(”Stop The World”)"></a>GC停顿(”Stop The World”)</h3><p>可达性分析工作必须在一个<strong>能确保一致性的快照</strong>中进行——这里<strong>“一致性”</strong>的意思是指<strong>在整个分析期间整个执行系统看起来就像被冻结在某个时间点上</strong>,不可以出现分析过程中对象引用关系还在不断变化的情况,这是保证分析结果准确性的基础。这点是导致GC进行时必须<strong>停顿所有Java执行线程</strong></p><blockquote><p>CMS收集器中,枚举根节点时也是必须要停顿的。</p></blockquote><h3 id="准确式GC与OopMap"><a href="#准确式GC与OopMap" class="headerlink" title="准确式GC与OopMap"></a>准确式GC与OopMap</h3><p>主流Java虚拟机使用的都是<strong>准确式GC(即使用准确式内存管理,虚拟机可用知道内存中某个位置的数据具体是什么类型)</strong>,所以当执行系统停顿下来后,并不需要一个不漏地检查完所有执行上下文和全局的引用位置,虚拟机应当是有办法直接得知哪些地方存放着对象引用。在HotSpot的实现中,是使用一组称为<strong>OopMap</strong>的数据结构来达到这个目的的,在类加载完成的时候,HotSpot就把<strong>对象内什么偏移量上是什么类型的数据</strong>计算出来,在JIT编译过程中,也会在特定的位置记录下<strong>栈和寄存器中哪些位置是引用</strong>。这样,GC在扫描时就可以直接得知这些信息了。</p><h4 id="安全点(Safepoint)——进行GC时程序停顿的位置"><a href="#安全点(Safepoint)——进行GC时程序停顿的位置" class="headerlink" title="安全点(Safepoint)——进行GC时程序停顿的位置"></a>安全点(Safepoint)——进行GC时程序停顿的位置</h4><p><strong>方法调用</strong>、<strong>循环跳转</strong>、<strong>异常跳转</strong>等,所以具有这些功能的指令才会产生Safepoint。</p><p><strong>主动式中断</strong>的思想是当GC需要中断线程的时候,不直接对线程操作,仅仅简单地<strong>设置一个标志</strong>,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起。<strong>轮询标志的地方和安全点是重合的</strong>,另外<strong>再加上创建对象需要分配内存的地方</strong>。</p><h4 id="安全区域(Safe-Region)"><a href="#安全区域(Safe-Region)" class="headerlink" title="安全区域(Safe Region)"></a>安全区域(Safe Region)</h4><p>安全区域是指<strong>在一段代码片段之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的。</strong>我们也可以把Safe Region看做是被扩展了的Safepoint。</p><p>在线程执行到Safe Region中的代码时,首先<strong>标识自己已经进入了Safe Region</strong>,那样,当在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了。<strong>在线程要离开Safe Region时,它要检查系统是否已经完成了根节点枚举(或者是整个GC过程)</strong>,如果完成了,那线程就继续执行,否则它就必须等待直到收到可以安全离开Safe Region的信号为止。</p>]]></content>
<tags>
<tag> jvm </tag>
</tags>
</entry>
<entry>
<title>7种垃圾收集器</title>
<link href="/2019/09/30/7%E7%A7%8D%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/"/>
<url>/2019/09/30/7%E7%A7%8D%E5%9E%83%E5%9C%BE%E6%94%B6%E9%9B%86%E5%99%A8/</url>
<content type="html"><![CDATA[<p>HotSpot 虚拟机中的 7 个垃圾收集器,连线表示垃圾收集器可以配合使用。</p><img src="/2019/09/30/7种垃圾收集器/27df7474.jpg" title="7个垃圾收集器"><blockquote><p>单线程与多线程:单线程指的是垃圾收集器只使用一个线程进行收集,而多线程使用多个线程;</p><p>串行与并行:串行指的是垃圾收集器与用户程序交替执行,这意味着在执行垃圾收集的时候需要停顿用户程序;并行指的是垃圾收集器和用户程序同时执行。除了 CMS 和 G1 之外,其它垃圾收集器都是以串行的方式执行。</p><hr><p><strong>并行(Parallel)</strong>:指多条垃圾收集线程并行工作,但此时用户线程仍然处于等待状态。</p><p><strong>并发(Concurrent)</strong>:指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能会交替执行),用户程序在继续运行。而垃圾收集程序运行在另一个CPU上。</p><hr><p>吞吐量就是<strong>CPU用于运行用户代码的时间</strong>与<strong>CPU总消耗时间</strong>的比值,即</p><p><strong>吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。</strong></p><p>假设虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。</p><hr><p><strong>新生代GC(Minor GC)</strong>:指发生在新生代的垃圾收集动作,因为Java对象大多都具备朝生夕灭的特性,所以Minor GC非常频繁,一般回收速度也比较快。具体原理见上一篇文章。</p><p><strong>老年代GC(Major GC / Full GC)</strong>:指发生在老年代的GC,出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在Parallel Scavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。Major GC的速度一般会比Minor GC慢10倍以上。</p></blockquote><table><thead><tr><th>收集器</th><th>串行、并行or并发</th><th>新生代/老年代</th><th>算法</th><th>目标</th><th>适用场景</th></tr></thead><tbody><tr><td><strong>Serial</strong></td><td>串行</td><td>新生代</td><td>复制算法</td><td>响应速度优先</td><td>单CPU环境下的Client模式</td></tr><tr><td><strong>Serial Old</strong></td><td>串行</td><td>老年代</td><td>标记-整理</td><td>响应速度优先</td><td>单CPU环境下的Client模式、CMS的后备预案</td></tr><tr><td><strong>ParNew</strong></td><td>并行</td><td>新生代</td><td>复制算法</td><td>响应速度优先</td><td>多CPU环境时在Server模式下与CMS配合</td></tr><tr><td><strong>Parallel Scavenge</strong></td><td>并行</td><td>新生代</td><td>复制算法</td><td>吞吐量优先</td><td>在后台运算而不需要太多交互的任务</td></tr><tr><td><strong>Parallel Old</strong></td><td>并行</td><td>老年代</td><td>标记-整理</td><td>吞吐量优先</td><td>在后台运算而不需要太多交互的任务</td></tr><tr><td><strong>CMS</strong></td><td>并发</td><td>老年代</td><td>标记-清除</td><td>响应速度优先</td><td>集中在互联网站或B/S系统服务端上的Java应用</td></tr><tr><td><strong>G1</strong></td><td>并发</td><td>both</td><td>标记-整理+复制算法</td><td>响应速度优先</td><td>面向服务端应用,将来替换CMS</td></tr></tbody></table><h2 id="Serial-收集器"><a href="#Serial-收集器" class="headerlink" title="Serial 收集器"></a>Serial 收集器</h2><p>Serial 翻译为串行,也就是说它以串行的方式执行。</p><p>它是<strong>单线程的收集器</strong>,只会使用一个线程进行垃圾收集工作。</p><p>它的优点是简单高效,对于单个 CPU 环境来说,<strong>由于没有线程交互的开销</strong>,因此拥有最高的单线程收集效率。</p><p>它是 Client 模式下的默认新生代收集器,因为在该应用场景下,分配给虚拟机管理的内存一般来说不会很大。Serial 收集器收集几十兆甚至一两百兆的新生代停顿时间可以控制在一百多毫秒以内,只要不是太频繁,这点停顿是可以接受的。</p><img src="/2019/09/30/7种垃圾收集器/b1ac068e.jpg" title="Serial 收集器"><h2 id="ParNew-收集器"><a href="#ParNew-收集器" class="headerlink" title="ParNew 收集器"></a>ParNew 收集器</h2><p>它是 Serial 收集器的<strong>多线程</strong>版本。(GC线程并行,用户线程阻塞)</p><p>是 Server 模式下的虚拟机首选新生代收集器,除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。</p><p>默认开启的线程数量与 CPU 数量相同,可以使用 -XX:ParallelGCThreads 参数来设置线程数。</p><img src="/2019/09/30/7种垃圾收集器/481e1ca1.jpg" title="ParNew 收集器"><h2 id="Parallel-Scavenge-收集器"><a href="#Parallel-Scavenge-收集器" class="headerlink" title="Parallel Scavenge 收集器"></a>Parallel Scavenge 收集器</h2><p>与 ParNew 一样是<strong>多线程</strong>收集器。(吞吐量指充分利用cpu去运行用户代买,</p><p>其它收集器关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而<strong>它的目标是达到一个可控制的吞吐量</strong>,它被称为“<strong>吞吐量优先</strong>”收集器。这里的<strong>吞吐量指 CPU 用于运行用户代码的时间占总时间的比值。</strong></p><p><strong>停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验</strong>。<strong>而高吞吐量则可以高效率地利用 CPU 时间,尽快完成程序的运算任务,适合在后台运算而不需要太多交互的任务。</strong></p><p><strong>缩短停顿时间是以牺牲吞吐量和新生代空间来换取的</strong>:新生代空间变小,垃圾回收变得频繁,导致吞吐量下降。</p><p>可以通过一个开关参数打开 GC 自适应的调节策略(GC Ergonomics),就不需要手工指定新生代的大小(-Xmn)、Eden 和 Survivor 区的比例、晋升老年代对象年龄等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量。</p><p>Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。MaxGCPauseMillis参数允许的值是一个大于0的毫秒数,收集器将尽可能地保证内存回收花费的时间不超过设定值。GCTimeRatio参数的值应当是一个大于0且小于100的整数,也就是垃圾收集时间占总时间的比率,相当于是吞吐量的倒数。</p><h2 id="Serial-Old-收集器"><a href="#Serial-Old-收集器" class="headerlink" title="Serial Old 收集器"></a>Serial Old 收集器</h2><p>是 Serial 收集器的老年代版本,也是给 Client 模式下的虚拟机使用。如果用在 Server 模式下,它有两大用途:</p><ul><li>在 JDK 1.5 以及之前版本(Parallel Old 诞生以前)中与 Parallel Scavenge 收集器搭配使用。</li><li>作为 CMS 收集器的后备预案,在并发收集发生 Concurrent Mode Failure 时使用。</li></ul><img src="/2019/09/30/7种垃圾收集器/untitled.png" title="Serial Old 收集器"><h2 id="Parallel-Old-收集器"><a href="#Parallel-Old-收集器" class="headerlink" title="Parallel Old 收集器"></a>Parallel Old 收集器</h2><p>是 Parallel Scavenge 收集器的老年代版本。</p><p>在注重吞吐量以及 CPU 资源敏感的场合,都可以优先考虑 Parallel Scavenge 加 Parallel Old 收集器。</p><img src="/2019/09/30/7种垃圾收集器/untitled1.png" title="Parallel Old 收集器"><h2 id="CMS-收集器"><a href="#CMS-收集器" class="headerlink" title="CMS 收集器"></a>CMS 收集器</h2><p>CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记 - 清除算法。(低停顿)</p><img src="/2019/09/30/7种垃圾收集器/367020d3.jpg" title="CMS 收集器"><ul><li>初始标记:仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿。</li><li>并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时最长,不需要停顿。</li><li>重新标记:为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿。</li><li>并发清除:不需要停顿。</li></ul><p>在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。</p><p>具有以下缺点:</p><ul><li>吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。</li><li>无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。</li><li>标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。</li></ul><h2 id="G1-收集器"><a href="#G1-收集器" class="headerlink" title="G1 收集器"></a>G1 收集器</h2><p>G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在<strong>多 CPU 和大内存的场景下有很好的性能</strong></p><img src="/2019/09/30/7种垃圾收集器/10c8999f.jpg" title="G1 收集器"><p>G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。</p><p>通过引入 Region 的概念,从而<strong>将原来的一整块内存空间划分成多个的小空间</strong>,使得<strong>每个小空间可以单独进行垃圾回收(复制)</strong>。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个Region垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并<strong>维护一个优先列表</strong>,每次根据允许的收集时间,<strong>优先回收价值最大的 Region</strong>。</p><p>每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。</p><ul><li>初始标记</li><li>并发标记</li><li>最终标记:为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行。</li><li>筛选回收:首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。</li></ul><p>具备如下特点:</p><ul><li>空间整合:<strong>整体来看是基于“标记 - 整理”算法实现的收集器,从局部(两个 Region 之间)上来看是基于“复制”算法实现的,这意味着运行期间不会产生内存空间碎片。</strong></li><li><strong>可预测的停顿</strong>:能让使用者明确指定在一个长度为 M 毫秒的时间片段内,消耗在 GC 上的时间不得超过 N 毫秒。</li></ul>]]></content>
<tags>
<tag> jvm </tag>
</tags>
</entry>
<entry>
<title>CopyOnWriteArrayList</title>
<link href="/2019/09/25/CopyOnWriteArrayList/"/>
<url>/2019/09/25/CopyOnWriteArrayList/</url>
<content type="html"><![CDATA[<p>所有的修改操作(add/set等)都会将底层依赖的数组拷贝一份并在其之上修改,但是我们知道数组的拷贝是一个比较耗时的操作,因此通常用于读多写少的场景下,例如随机访问、遍历等。<br>有个可重入锁用于修改(add/set等)时保证其线程安全型,另外有一个array数组用于存储实际的数据,并用volatile修饰,保证可见性。</p><h2 id="字段"><a href="#字段" class="headerlink" title="字段"></a>字段</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre></td><td class="code"><pre><span class="line">final transient ReentrantLock lock = new ReentrantLock();</span><br><span class="line"> private transient volatile Object[] array;</span><br><span class="line"> </span><br><span class="line"> final Object[] getArray() {</span><br><span class="line"> return array;</span><br><span class="line"> }</span><br><span class="line"> </span><br><span class="line"> final void setArray(Object[] a) {</span><br><span class="line"> array = a;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><h2 id="add-E-e"><a href="#add-E-e" class="headerlink" title="add(E e)"></a>add(E e)</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">public boolean add(E e) {</span><br><span class="line"> final ReentrantLock lock = this.lock;</span><br><span class="line"> lock.lock();</span><br><span class="line"> try {</span><br><span class="line"> Object[] elements = getArray();</span><br><span class="line"> int len = elements.length;</span><br><span class="line"> Object[] newElements = Arrays.copyOf(elements, len + 1);</span><br><span class="line"> newElements[len] = e;</span><br><span class="line"> setArray(newElements);</span><br><span class="line"> return true;</span><br><span class="line"> } finally {</span><br><span class="line"> lock.unlock();</span><br><span class="line"> }</span><br><span class="line"> }</span><br></pre></td></tr></table></figure><p>在add()方法中也没有想ArrayList一样去判断当前数组的容量并去扩容,将当前数组的内容复制到新数组中,新数组的大小是老数组的长度+1,因此每次新增操作都会导致CopyOnWriteArrayList的长度自增。<br>拷贝完成后将元素添加到新数组中。<br>用新数组替换当前数组,用volatile修饰保证后续对其他线程可见性</p><p>读取并没有加锁,因此对于读取操作来说可能会存在延迟,读取不到最新的数据,这里读取通过getArray()方法获取的相当于是一个快照,在修改才做完成前,我们读取的都是这个快照数组的内容,对于遍历也是类似,其内部会利用这个快照数组构造一个新的构造器,因此这里遍历才不需要加锁,但是相对的,之后的add/remove/set等操作不会对迭代器造成任务影响,迭代器也不支持remove操作,也就不会抛出ConcurrentModificationException异常。</p><h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2><p>CopyOnArrayList使用与读多写少的场景,而且存储的对象最好不要太多,加入CopyOnArrayList中存储的数据比较多,那么每一次修改才做都会造成一次大对象拷贝,造成YGC甚至是FULL GC,因此使用前一定要考虑好场景。另外一个是由于读取都是快照读,因此会存在一定的延时造成读取不到最新的数据。</p>]]></content>
<tags>
<tag> 集合&容器 </tag>
</tags>
</entry>
<entry>
<title>LinkedList</title>
<link href="/2019/09/25/LinkedList/"/>
<url>/2019/09/25/LinkedList/</url>
<content type="html"><![CDATA[<p>LinkedList是一种可以在任何位置进行高效地插入和移除操作的有序序列,它是基于双向链表实现的,是线程不安全的,允许元素为null的双向链表。</p><h2 id="字段"><a href="#字段" class="headerlink" title="字段"></a>字段</h2><figure class="highlight plain"><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><span class="line">transient Node<E> first;</span><br><span class="line">transient Node<E> last;</span><br><span class="line">private static class Node<E> {</span><br><span class="line"> // 值</span><br><span class="line"> E item;</span><br><span class="line"> // 后继</span><br><span class="line"> Node<E> next;</span><br><span class="line"> // 前驱</span><br><span class="line"> Node<E> prev;</span><br><span class="line"></span><br><span class="line"> Node(Node<E> prev, E element, Node<E> next) {</span><br><span class="line"> this.item = element;</span><br><span class="line"> this.next = next;</span><br><span class="line"> this.prev = prev;</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="操作都是双向链表的操作"><a href="#操作都是双向链表的操作" class="headerlink" title="操作都是双向链表的操作"></a>操作都是双向链表的操作</h2><p>node(int index) 取出index的元素,这里会根据index是否大小,来决定从前查找还是从链表后查找。</p><h2 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h2><p>优点:<br>不需要扩容和预留空间,空间效率高增删效率高</p><p>缺点:<br>随机访问时间效率低改查效率低</p>]]></content>
<tags>
<tag> 集合&容器 </tag>
</tags>
</entry>
<entry>
<title>ArrayList</title>
<link href="/2019/09/25/ArrayList/"/>
<url>/2019/09/25/ArrayList/</url>
<content type="html"><![CDATA[<h2 id="字段"><a href="#字段" class="headerlink" title="字段"></a>字段</h2><p>int DEFAULT_CAPACITY = 10;<br>Object[] EMPTY_ELEMENTDATA = {};<br>Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};<br>transient 在已经实现序列化的类中,不允许某变量序列化<br>transient Object[] elementData;<br>int size;</p><h2 id="序列化"><a href="#序列化" class="headerlink" title="序列化"></a>序列化</h2><p>将对象转换成以字节序列的形式来表示,以便用于持久化和传输。用的时候拿出来进行反序列化即可又变成Java对象。<br>实现方法:实现Serializable接口。<br>ArrayList只序列化size大小的元素,不是整个数组<br>原因在于elementData是一个缓存数组,它通常会预留一些容量,等容量不足时再扩充容量,那么有些空间可能就没有实际存储元素,采用上诉的方式来实现序列化时,就可以保证只序列化实际存储的那些元素,而不是整个数组,从而节省空间和时间。</p><h2 id="初始化"><a href="#初始化" class="headerlink" title="初始化"></a>初始化</h2><p>根据initialCapacity 初始化一个空数组,如果值为0,则初始化一个空数组</p><h2 id="添加元素时会判断容量并扩容"><a href="#添加元素时会判断容量并扩容" class="headerlink" title="添加元素时会判断容量并扩容"></a>添加元素时会判断容量并扩容</h2><p>参数为size+1,保证最小容量能添加元素<br>elementData[size++] = e;在数组末尾添加元素</p><h2 id="ensureCapacityInternal-int-minCapacity"><a href="#ensureCapacityInternal-int-minCapacity" class="headerlink" title="ensureCapacityInternal(int minCapacity)"></a>ensureCapacityInternal(int minCapacity)</h2><p>ensureExplicitCapacity(calculateCapacity(elementData, minCapacity));<br>计算容量+确保容量<br>如果是空数组,返回默认容量10和size+1的最大值,如果不是,返回size+1</p><h2 id="默认1-5倍扩容"><a href="#默认1-5倍扩容" class="headerlink" title="默认1.5倍扩容"></a>默认1.5倍扩容</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">private void grow(int minCapacity) {</span><br><span class="line"> // overflow-conscious code</span><br><span class="line"> int oldCapacity = elementData.length;</span><br><span class="line"> int newCapacity = oldCapacity + (oldCapacity >> 1); //1.5</span><br><span class="line"> if (newCapacity - minCapacity < 0)</span><br><span class="line"> newCapacity = minCapacity;</span><br><span class="line"> if (newCapacity - MAX_ARRAY_SIZE > 0)</span><br><span class="line"> newCapacity = hugeCapacity(minCapacity);</span><br><span class="line"> // minCapacity is usually close to size, so this is a win:</span><br><span class="line"> elementData = Arrays.copyOf(elementData, newCapacity);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="指定位置添加add-int-index-E-element"><a href="#指定位置添加add-int-index-E-element" class="headerlink" title="指定位置添加add(int index, E element)"></a>指定位置添加add(int index, E element)</h2><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">public void add(int index, E element) {</span><br><span class="line"> rangeCheckForAdd(index);</span><br><span class="line"></span><br><span class="line"> ensureCapacityInternal(size + 1); // Increments modCount!!</span><br><span class="line"> System.arraycopy(elementData, index, elementData, index + 1,</span><br><span class="line"> size - index); //第i个位置后的往后移一位,这个方法是移动多少个元素,也就是size-index个元素都要往后移</span><br><span class="line"> elementData[index] = element;</span><br><span class="line"> size++;</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="删除修改同理"><a href="#删除修改同理" class="headerlink" title="删除修改同理"></a>删除修改同理</h2><p>都要移动元素,所以删除添加性能比链表差。</p><h2 id="线程不安全"><a href="#线程不安全" class="headerlink" title="线程不安全"></a>线程不安全</h2><p>不推荐用vector,读写都加synchronized,性能差<br>推荐CopyOnWriteArrayList,写时复制,读不加锁</p><h2 id="优缺点"><a href="#优缺点" class="headerlink" title="优缺点"></a>优缺点</h2><p>因为其底层是数组,所以修改和查询效率高。<br>可自动扩容(1.5倍)。<br>插入和删除效率不高。<br>线程不安全。</p>]]></content>
<tags>
<tag> 集合&容器 </tag>
</tags>
</entry>
<entry>
<title>HashMap</title>
<link href="/2019/09/25/HashMap/"/>
<url>/2019/09/25/HashMap/</url>
<content type="html"><![CDATA[<h2 id="结构"><a href="#结构" class="headerlink" title="结构"></a>结构</h2><p>HashMap是数组+链表+红黑树,node数组,Node是HashMap的一个内部类,实现了Map.Entry接口,本质是就是一个映射(键值对)。</p><h2 id="解决冲突"><a href="#解决冲突" class="headerlink" title="解决冲突"></a>解决冲突</h2><p>HashMap采用了链地址法解决冲突。</p><h2 id="字段及初始值"><a href="#字段及初始值" class="headerlink" title="字段及初始值"></a>字段及初始值</h2><p>Node[] table的初始化长度length(默认值是16)。<br>Load factor为负载因子(默认值是0.75)。<br>threshold是HashMap所能容纳的最大数据量的Node(键值对)个数。threshold = length * Load factor。超过thresholde就重新resize(扩容),扩容后的HashMap容量是之前容量的两倍。loadFactor越大,利用内存越充分,但是时间效率会降低。<br>size这个字段其实很好理解,就是HashMap中实际存在的键值对数量。注意和table的长度length、容纳最大键值对数量threshold的区别。<br>modCount字段主要用来记录HashMap内部结构发生变化的次数,主要用于迭代的快速失败。</p><h2 id="定位下标-hash算法"><a href="#定位下标-hash算法" class="headerlink" title="定位下标(hash算法)"></a>定位下标(hash算法)</h2><p>这里的Hash算法本质上就是三步:取key的hashCode值、高位运算[(key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16)]、取模运算(h & (length-1))。通过hashCode()的高16位异或低16位实现的:(h = k.hashCode()) ^ (h >>> 16),主要是从速度、功效、质量来考虑的,这么做可以在数组table的length比较小的时候,也能保证考虑到高低Bit都参与到Hash的计算中,同时不会有太大的开销。</p><h2 id="2的n次方"><a href="#2的n次方" class="headerlink" title="2的n次方"></a>2的n次方</h2><p>哈希桶数组table的长度length大小必须为2的n次方(一定是合数)。主要是为了在取模和扩容时做优化,同时为了减少冲突,HashMap定位哈希桶索引位置时,也加入了高位参与运算的过程。通过h & (table.length -1)来得到该对象的下标,而HashMap底层数组的长度总是2的n次方,这是HashMap在速度上的优化。当length总是2的n次方时,h& (length-1)运算等价于对length取模,也就是h%length,但是&比%具有更高的效率。</p><h2 id="扩容"><a href="#扩容" class="headerlink" title="扩容"></a>扩容</h2><p>扩容使用一个新的数组代替已有的容量小的数组。1.7头插,1.8尾插。我们使用的是2次幂的扩展(指长度扩为原来2倍),所以,元素的位置要么是在原位置,要么是在原位置再移动2次幂的位置。不需要像JDK1.7的实现那样重新计算hash,只需要看看原来的hash值新增的那个bit是1还是0就好了,是0的话索引没变,是1的话索引变成“原索引+oldCap”。JDK1.7中rehash的时候,旧链表迁移新链表的时候,如果在新表的数组索引位置相同,则链表元素会倒置,JDK1.8不会倒置。</p><h2 id="一些对比"><a href="#一些对比" class="headerlink" title="一些对比"></a>一些对比</h2><p>根据键的hashCode值存储数据,遍历顺序却是不确定的,最多只允许一条记录的键为null,允许多条记录的值为null,任一时间只有一个线程能写Hashtable(synchronized),并发性不如ConcurrentHashMap,当用Iterator遍历TreeMap时,得到的记录是排过序的。Hashtable初始化桶大小为11。</p><h2 id="线程不安全"><a href="#线程不安全" class="headerlink" title="线程不安全"></a>线程不安全</h2><p>初始化table不安全(ConcurrentHashMap只有一个线程会扩容,利用sizeCtl来进行CAS,和Thread.yield让出cpu),put元素不安全,操作链表,红黑树也不安全(ConcurrentHashMap会锁住对应下标的Node),扩容也不安全(没有可见性)</p><h2 id="put过程"><a href="#put过程" class="headerlink" title="put过程"></a>put过程</h2><p>先判断table是否为空或者长度为0,是的话resize一下,会得到新数组(16)或者旧数组,用hash &(length-1)得到数组下标,判断此位置是否为null,null的话直接放入,否则看一下key是否等于要插入的key,是的话覆盖其值,否则先判断是否是树结点,如果是,用红黑树插入去插入,如果不是,去遍历链表插入,这里会有一个结点的计数,等结点数大于等于8时要对这条链表进行转红黑树操作,如果中途遍历到相同key,就覆盖值,如果没有,连接到尾部。最后判断一下size否大于threshold,是的话resize</p><img src="/2019/09/25/HashMap/HashMap之put方法.jpg" title="HashMap之put方法"><h2 id="get过程"><a href="#get过程" class="headerlink" title="get过程"></a>get过程</h2><p>只有table不为空,length>0,hash &(length-1)下标元素存在,不然返回null,先判断是不是树结点,如果是,用红黑树搜索,如果不是,遍历链表。</p><h2 id="resize过程"><a href="#resize过程" class="headerlink" title="resize过程"></a>resize过程</h2><p>table为空或者为0,创建初始值为16的返回,否则size扩充为2倍,threshold扩充为2倍。根据hash值在oldCap哪一位是1还是0,分成2队,连接在新数组的原位置或原位置+oldCap位置。尾插。</p>]]></content>
<tags>
<tag> 集合&容器 </tag>
</tags>
</entry>
<entry>
<title>协议与设备</title>
<link href="/2019/09/25/%E5%8D%8F%E8%AE%AE%E4%B8%8E%E8%AE%BE%E5%A4%87/"/>
<url>/2019/09/25/%E5%8D%8F%E8%AE%AE%E4%B8%8E%E8%AE%BE%E5%A4%87/</url>
<content type="html"><![CDATA[<h2 id="各种设备"><a href="#各种设备" class="headerlink" title="各种设备"></a>各种设备</h2><blockquote><p>中继器(信号放大与整形):物理层<br>集线器Hub(可以有多个端口,也是信号放大与整形):物理层<br>交换机(在多端口之间同时转发多帧):MAC层<br>网桥(MAC地址过滤与帧转发):数据链路层(MAC层)<br>路由器(连接两个网络,对分组报文进行转发的设备):网络层<br>网关(协议的转换与数据的转发):网络层以上</p></blockquote><h2 id="常见协议和端口"><a href="#常见协议和端口" class="headerlink" title="常见协议和端口"></a>常见协议和端口</h2><blockquote><p>80 http 用于万维网(WWW)服务的超文本传输协议(HTTP)<br>21 ftp 文件传输协议(FTP)端口;有时被文件服务协议(FSP)使用(FTP服务器有两个端口,其中21端口用于控制连接,20端口用于传输数据)<br>22 ssh 安全 Shell(SSH)服务<br>23 telnet Telnet 服务<br>25 smtp 简单邮件传输协议(SMTP)</p></blockquote><p>ICMP 报文作为数据字段封装在 IP 分组中,因此, IP 协议直接为 ICMP 提供服务。 UDP 和 TCP 都是传输层协议,为应用层提供服务。 PPP协议是链路层协议,为网络层提供服务。<br>IP 分片发生在 IP 层 ,不仅 源端主机 会进行分片,中间的 路由器 也有可能分片,因为不同的网络的 MTU 是不一样的,如果传输路径上的某个网络的 MTU 比源端网络的 MTU 要小,路由器就可能对 IP 数据报再次进行分片。而分片数据的 重组 只会发生在 目的端 的 IP 层。<br><strong>PPP(Point-to-Point Protocol点到点协议)</strong>是为在 同等单元之间传输数据包这样的简单链路设计的链路层协议 。这种链路提供全双工操作,并按照顺序传递数据包。 PPP协议支持以下功能:</p><ul><li>IP地址的动态分配和管理</li><li>同步或异步的物理层通信</li><li>链路的配置、质量检测和纠错</li><li>多种配置参数选项的协商</li></ul><p>PPP是目前使用最广泛的数据链路层协议,不管是低速的拨号猫连接还是高速的光纤链路,都适用PPP协议 。<strong>因特网用户通常都要连接到某个ISP 才能接入到因特网。 PPP协议就是用户计算机和ISP进行通信时所使用的数据链路层协议。 ISP使用PPP协议为计算机分配一些网络参数(如IP地址、域名等)。</strong></p><p>简单网络管理协议( SNMP ):属于<strong>应用层</strong><br>RTSP(Real Time Streaming Protocol),实时流传输协议,是TCP/IP协议体系中的一个<strong>应用层</strong>协议<br><strong>RIP</strong>是一种分布式的基于距离向量的路由选择协议,通过广播<strong>UDP</strong>报文来交换路由信息。<br><strong>OSPF</strong>是一个内部网关协议,不使用传输协议,如UDP或TCP,而是直接用<strong>IP</strong>包封装它的数据。<br><strong>BGP</strong>是一个外部网关协议,用<strong>TCP</strong>封装它的数据。<br><strong>HDLC协议</strong>(高级数据链路控制)对比特串进行组帧时,HDLC数据帧以位值0111 1110 标识,每一个帧的开始和结束,因此,在帧数据中凡是出现连续五个1时,就在输出位流末尾加0<br><strong>域名系统DNS</strong>(Domain Name System)是因特网使用的命名系统,用来把便于人们记忆的含有特定含义的主机名转换为便于机器处理的IP地址。DNS系统采用客户\服务器模型,其协议运行在UDP之上,使用53号端口。属于<strong>应用层</strong></p>]]></content>
<tags>
<tag> 计算机网络 </tag>
</tags>
</entry>
<entry>
<title>应用层</title>
<link href="/2019/09/25/%E5%BA%94%E7%94%A8%E5%B1%82/"/>
<url>/2019/09/25/%E5%BA%94%E7%94%A8%E5%B1%82/</url>
<content type="html"><![CDATA[<p>应用层( application-layer )的任务是通过应用进程间的交互来完成特定网络应用。<strong>应用层协议定义的是应用进程(进程:主机中正在运行的程序)间的通信和交互的规则</strong>。对于不同的网络应用需要不同的应用层协议。在互联网中应用层协议很多,如域名系统 <strong>DNS</strong>,支持万维网应用的 <strong>HTTP 协议</strong>,支持电子邮件的 <strong>SMTP 协议</strong>、支持文件传输的<strong>FTP协议</strong>等等。</p><blockquote><p>我们把<strong>应用层交互的数据单元称为报文</strong></p></blockquote><h3 id="域名系统DNS"><a href="#域名系统DNS" class="headerlink" title="域名系统DNS"></a>域名系统DNS</h3><p>DNS 是一个分布式数据库,提供了主机名和 IP 地址之间相互转换的服务。这里的分布式数据库是指,每个站点只保留它自己的那部分数据。域名具有层次结构,从上到下依次为:根域名、顶级域名、二级域名。</p><img src="/2019/09/25/应用层/b54eeb16-0b0e-484c-be62-306f57c40d77.jpg" title="域名系统DNS"><p>DNS 可以使用 UDP 或者 TCP 进行传输,使用的端口号都为 53。大多数情况下 DNS 使用 UDP 进行传输,这就要求域名解析器和域名服务器都必须自己处理超时和重传从而保证可靠性。在两种情况下会使用 TCP 进行传输:</p><ul><li>如果返回的响应超过的 512 字节(UDP 最大只支持 512 字节的数据)。</li><li>区域传送(区域传送是主域名服务器向辅助域名服务器传送变化的那部分数据)。</li></ul><h3 id="文件传送协议FTP"><a href="#文件传送协议FTP" class="headerlink" title="文件传送协议FTP"></a>文件传送协议FTP</h3><p>FTP 使用 TCP 进行连接,它需要两个连接来传送一个文件:</p><ul><li>控制连接:服务器打开端口号 21 等待客户端的连接,客户端主动建立连接后,使用这个连接将客户端的命令传送给服务器,并传回服务器的应答。</li><li>数据连接:20端口,用来传送一个文件数据。</li></ul><h3 id="动态主机配置协议DHCP"><a href="#动态主机配置协议DHCP" class="headerlink" title="动态主机配置协议DHCP"></a>动态主机配置协议DHCP</h3><p>DHCP (Dynamic Host Configuration Protocol) 提供了即插即用的连网方式,用户不再需要手动配置 IP 地址等信息。</p><p>DHCP 配置的内容不仅是 IP 地址,还包括子网掩码、网关 IP 地址。</p><p>DHCP 工作过程如下:</p><ol><li>客户端发送 Discover 报文,该报文的目的地址为 255.255.255.255:67,源地址为 0.0.0.0:68,被放入 UDP 中,该报文被广播到同一个子网的所有主机上。如果客户端和 DHCP 服务器不在同一个子网,就需要使用中继代理。</li><li>DHCP 服务器收到 Discover 报文之后,发送 Offer 报文给客户端,该报文包含了客户端所需要的信息。因为客户端可能收到多个 DHCP 服务器提供的信息,因此客户端需要进行选择。</li><li>如果客户端选择了某个 DHCP 服务器提供的信息,那么就发送 Request 报文给该 DHCP 服务器。</li><li>DHCP 服务器发送 Ack 报文,表示客户端此时可以使用提供给它的信息。</li></ol><h3 id="远程登录协议telnet"><a href="#远程登录协议telnet" class="headerlink" title="远程登录协议telnet"></a>远程登录协议telnet</h3><p>TELNET 用于登录到远程主机上,并且远程主机上的输出也会返回。<br>TELNET 可以适应许多计算机和操作系统的差异,例如不同操作系统系统的换行符定义。</p><h3 id="电子邮件协议"><a href="#电子邮件协议" class="headerlink" title="电子邮件协议"></a>电子邮件协议</h3><p>一个电子邮件系统由三部分组成:用户代理、邮件服务器以及邮件协议。<br>邮件协议包含发送协议和读取协议,发送协议常用 SMTP,读取协议常用 POP3 和 IMAP</p><ul><li><p>SMTP<br>SMTP 只能发送 ASCII 码,而互联网邮件扩充 MIME 可以发送二进制文件。MIME 并没有改动或者取代 SMTP,而是增加邮件主体的结构,定义了非 ASCII 码的编码规则。</p></li><li><p>POP3<br>POP3 的特点是只要用户从服务器上读取了邮件,就把该邮件删除。但最新版本的 POP3 可以不删除邮件。</p></li><li><p>IMAP<br>IMAP 协议中客户端和服务器上的邮件保持同步,如果不手动删除邮件,那么服务器上的邮件也不会被删除。IMAP 这种做法可以让用户随时随地去访问服务器上的邮件。</p><h3 id="常用端口"><a href="#常用端口" class="headerlink" title="常用端口"></a>常用端口</h3></li></ul><table><thead><tr><th>应用</th><th>应用层协议</th><th>端口号</th><th>传输层协议</th><th>备注</th></tr></thead><tbody><tr><td>域名解析</td><td>DNS</td><td>53</td><td>UDP/TCP</td><td>长度超过 512 字节时使用 TCP</td></tr><tr><td>动态主机配置协议</td><td>DHCP</td><td>67/68</td><td>UDP</td><td></td></tr><tr><td>简单网络管理协议</td><td>SNMP</td><td>161/162</td><td>UDP</td><td></td></tr><tr><td>文件传送协议</td><td>FTP</td><td>20/21</td><td>TCP</td><td>控制连接 21,数据连接 20</td></tr><tr><td>远程终端协议</td><td>TELNET</td><td>23</td><td>TCP</td><td></td></tr><tr><td>超文本传送协议</td><td>HTTP</td><td>80</td><td>TCP</td><td></td></tr><tr><td>简单邮件传送协议</td><td>SMTP</td><td>25</td><td>TCP</td><td></td></tr><tr><td>邮件读取协议</td><td>POP3</td><td>110</td><td>TCP</td><td></td></tr><tr><td>网际报文存取协议</td><td>IMAP</td><td>143</td><td>TCP</td></tr></tbody></table>]]></content>
<tags>
<tag> 计算机网络 </tag>
</tags>
</entry>
<entry>
<title>传输层</title>
<link href="/2019/09/25/%E4%BC%A0%E8%BE%93%E5%B1%82/"/>
<url>/2019/09/25/%E4%BC%A0%E8%BE%93%E5%B1%82/</url>
<content type="html"><![CDATA[<h2 id="TCP报文(标准长度20字节)"><a href="#TCP报文(标准长度20字节)" class="headerlink" title="TCP报文(标准长度20字节)"></a>TCP报文(标准长度20字节)</h2><img src="/2019/09/25/传输层/20150611163052331.png" title="TCP报文"><p>TCP数据包每次能够传输的最大长度 = MTU(1500B) - IP头(20B)- TCP头(20B)= 1460Bytes。<br>源端口号与目的端口号:标识了发送方与接收方的地址,IP地址和端口号合称为套接字。</p><h2 id="字段"><a href="#字段" class="headerlink" title="字段"></a>字段</h2><ul><li>序列号和确认号:32位序列号与32位确认序号:序列号与确认号可以理解成两个通信进程在收发数据的时候互相应答的信息。比如说:A进程从序列号1000开始给B进程发送数据,发送五个数据。那么在B收到数据回复的时候,这里A的确认序列号应该是从1006,如果不是1006,比如说是1003,那就意味着1004、1005数据包B没有收到,于是A启动重发机制。这也就保证了数据的可靠性,也是TCP的特点之一。序列号是进程发送消息的号码,而确认号是期望目的进程返回的号码。进行比对,从而验证数据包是否到达。</li><li>4位TCP报头长度:这里的四位TCP报头长度,可以理解成四个比特位表示长度,四位比特位表示的值乘以四就是该TCP头部的长度。由图可知,报头最短长度为20字节,也就是说这里的四位TCP报头长度默认为0101。并且TCP报头长度不可超过15*4=60个字节。</li><li>标志位:共6个,即URG、ACK、PSH、RST、SYN、FIN等,具体含义如下:<br>URG:紧急指针(urgent pointer)有效;<br><strong>ACK</strong>:确认序号有效;<br>PSH:接收方应该尽快将这个报文交给应用层;<br>RST:重置连接;<br><strong>SYN</strong>:发起一个新连接;<br><strong>FIN</strong>:释放一个连接。</li><li>16位窗口大小:<strong>窗口大小标志着TCP缓冲区内部剩余空间的大小,起到一个流量控制的作用。如果窗口满了,那么这个时候是不允许数据接收的</strong>。后面到达的数据会被丢失。</li><li>16位校验和:这里的校验和由发送端填充,CRC校验。接收端校验数据的时候如果校验不通过,那么认为数据有问题。此处的校验和不仅仅校验TCP首部,还校验数据部分。</li><li>16位紧急指针:标识哪部分的数据为紧急数据。</li></ul><h2 id="三次握手"><a href="#三次握手" class="headerlink" title="三次握手"></a>三次握手</h2><img src="/2019/09/25/传输层/e92d0ebc-7d46-413b-aec1-34a39602f787.png" title="三次握手"><p>第一次握手:Client先产生一个初始序列号Seq = x, 作为SYN并将该数据包发送给Server,Client进入SYN_SENT状态,等待Server确认。<br>第二次握手:Server收到数据包后也发送自己的SYN报文作为响应,并初始化序列号Seq = y,为了确认Client的Seq,Server将Client发送的Seq加1(x+1),作为ACK发送给Client,Server进入SYN_RCVD状态。(SYN为synchronize的缩写,ACK为acknowledgment的缩写)。<br>第三次握手:为了确认Server的SYN,Client将Server发送的Seq加1(y+1),作为ACK发送给Server。Client和Server进入ESTABLISHED状态,完成三次握手,随后Client与Server之间可以开始传输数据了。<br>通过这样的三次握手,客户端与服务端建立起可靠的双工的连接,开始传送数据。 三次握手的最主要目的是保证连接是双工的,可靠更多的是通过重传机制来保证的。</p><h2 id="半连接、半打开、半关闭状态"><a href="#半连接、半打开、半关闭状态" class="headerlink" title="半连接、半打开、半关闭状态"></a>半连接、半打开、半关闭状态</h2><p>半连接状态:发生在TCP三次握手过程中,客户端向服务器发起连接,服务器也进行了回应,但是客户端却不进行第3次握手。</p><p>半打开状态:在TCP连接中,如果某一端关闭了连接或者是异常关闭,则该连接处于半打开状态。解决半打开问题:引入心跳机制就可以察觉半打开状态。</p><p>半关闭状态:当TCP链接中客户端向服务器发送 FIN 请求关闭,服务端回应ACK之后,并没有立即发送 FIN 给客户端,客户端就处于半关闭状态,此时客户端可以接收服务器发送的数据,但是客户端已经不能再向服务器发送数据。(应用层面,传输层服务端发送信号,客户端还是得发送响应)</p><h2 id="SYN-flood攻击"><a href="#SYN-flood攻击" class="headerlink" title="SYN flood攻击"></a>SYN flood攻击</h2><p>在三次握手过程中,服务器发送SYN-ACK之后,收到客户端的ACK之前的TCP连接称为半连接(half-open connect)。此时服务器处于SYN_RCVD状态。当收到ACK后,服务器转入ESTABLISHED状态。Syn攻击就是攻击客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,<strong>服务器回复确认包,并等待客户的确认</strong>,由于源地址是不存在的,服务器需要不断的重发直至超时,这些<strong>伪造的SYN包将长时间占用未连接队列</strong>,<strong>正常的SYN请求被丢弃</strong>,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。<br>一般较新的TCP/IP协议栈都对这一过程进行修正来防范Syn攻击,修改tcp协议实现。主要方法有SynAttackProtect保护机制、SYN cookies技术、增加最大半连接和缩短超时时间等。但是不能完全防范syn攻击。</p><h2 id="为什么需要三次握手?"><a href="#为什么需要三次握手?" class="headerlink" title="为什么需要三次握手?"></a>为什么需要三次握手?</h2><p>为了保证服务端能收接受到客户端的信息并能做出正确的应答而进行前两次(第一次和第二次)握手,为了保证客户端能够接收到服务端的信息并能做出正确的应答而进行后两次(第二次和第三次)握手。</p><h2 id="在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?"><a href="#在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?" class="headerlink" title="在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?"></a>在三次握手过程中,如果服务器一直收不到客户端的ack会发生什么?</h2><p>服务端会给每个待完成的半连接都设一个定时器,如果超过时间还没有收到客户端的ACK消息,则重新发送一次SYN-ACK消息给客户端,直到重试超过一定次数时才会放弃。这个时候服务器需要分配内核资源维护半连接。</p><h2 id="初始序列号Seq为什么要随机初始化?"><a href="#初始序列号Seq为什么要随机初始化?" class="headerlink" title="初始序列号Seq为什么要随机初始化?"></a>初始序列号Seq为什么要随机初始化?</h2><p>这样做主要是为了保证网络安全,如果不是随机产生初始序列号,黑客将会以很容易的方式获取到你与其他主机之间通信的初始化序列号,并且伪造序列号进行攻击,这已经成为一种很常见的网络攻击手段。</p><h2 id="四次挥手"><a href="#四次挥手" class="headerlink" title="四次挥手"></a>四次挥手</h2><img src="/2019/09/25/传输层/f87afe72-c2df-4c12-ac03-9b8d581a8af8.jpg" title="四次挥手"><p>第一次挥手:Client发送一个FIN,Seq=K,用来关闭Client到Server的数据传送,Client进入FIN_WAIT_1状态;<br>第二次挥手:Server收到FIN后,发送一个ACK(K+1)给Client,确认序号为收到序号+1(与SYN相同,一个FIN占用一个序号),Server进入CLOSE_WAIT状态;<br>第三次挥手:Server发送一个FIN,Seq=L,用来关闭Server到Client的数据传送,Server进入LAST_ACK状态;<br>第四次挥手:Client收到FIN后,Client进入TIME_WAIT状态(等待2MSL后关闭),接着发送一个ACK(L+1)给Server,确认序号为收到序号+1,Server进入CLOSED状态,完成四次挥手。</p><h2 id="为什么建立连接是三次握手,而关闭连接却是四次挥手呢?"><a href="#为什么建立连接是三次握手,而关闭连接却是四次挥手呢?" class="headerlink" title="为什么建立连接是三次握手,而关闭连接却是四次挥手呢?"></a>为什么建立连接是三次握手,而关闭连接却是四次挥手呢?</h2><p>原因在于:首先FIN信号是由于调用close所以才发送的,而ACK是由内核发送的,所以ACK报文和FIN报文在发送的时间上都是分开的,不一定能同时发送。但是三次握手的时候发送SYN是由内核直接完成的,所以这就可以达到一个同步发送的情况。</p><h2 id="CLOSE-WAIT状态有什么影响?"><a href="#CLOSE-WAIT状态有什么影响?" class="headerlink" title="CLOSE_WAIT状态有什么影响?"></a>CLOSE_WAIT状态有什么影响?</h2><p>如果服务器的代码没有调用close,那么意味着并没有发送FIN结束报文段。那么也就是说,此连接的服务器长期保持在CLOSE_WAIT状态<br>服务器长期保持在CLOSE_WAIT状态,也就是说分配的文件描述符并没有关闭并归还。那么大量的CLOSE_WAIT存在的话,就会导致一种资源的泄漏,可能到最后就没有可分配的文件描述符了,那么就会使一些客户端无法连接,从而造成不可估量的影响。</p><h2 id="TIME-WAIT状态"><a href="#TIME-WAIT状态" class="headerlink" title="TIME_WAIT状态"></a>TIME_WAIT状态</h2><p>TIME_WAIT状态对大并发服务器的影响,应尽可能在服务器避免出现TIME_WAIT状态,如果服务器端主动断开连接,服务端就会进入TIME_WAIT状态(主动发起关闭连接的一方)</p><h2 id="为什么-TIME-WAIT-状态还需要等-2MSL后才能返回到-CLOSED-状态?"><a href="#为什么-TIME-WAIT-状态还需要等-2MSL后才能返回到-CLOSED-状态?" class="headerlink" title="为什么 TIME_WAIT 状态还需要等 2MSL后才能返回到 CLOSED 状态?"></a>为什么 TIME_WAIT 状态还需要等 2MSL后才能返回到 CLOSED 状态?</h2><p>保证可靠(可靠性是TCP最根本的特征)地终止TCP连接:处于TIME_WAIT状态的客户端会向服务端发送ACK,如果此时ACK丢失,目的端会超时重传FIN报文段,目的端收到重传的报文段最少需要2MSL,所以发送端会等待2MSL时间;</p><p>客户端在发送ACK后,再等待2MSL时间,可以使本次连接所产生的数据段从网络中消失,从而保证关闭连接后不会有还在网络中滞留的数据段去骚扰服务端。</p><h2 id="为什么是2MSL?"><a href="#为什么是2MSL?" class="headerlink" title="为什么是2MSL?"></a>为什么是2MSL?</h2><p>我们知道服务端收到ACK,关闭连接。但是客户端无法知道ACK是否已经到达服务端,于是开始等待?等待什么呢?假如ACK没有到达服务端,服务端会为FIN这个消息超时重传 timeout retransmit ,那如果客户端等待时间足够,又收到FIN消息,说明ACK没有到达服务端,于是再发送ACK,直到在足够的时间内没有收到FIN,说明ACK成功到达。这个等待时间至少是:服务端的timeout + FIN的传输时间,为了保证可靠,采用更加保守的等待时间2MSL。<br>客户端发出ACK,等待ACK到达对方的超时时间 MSL(最大报文生存时间),等待FIN的超时重传,也是MSL,所以如果2MSL时间内没有收到FIN,说明对方安全收到ACK。</p><h2 id="滑动窗口(流量控制)"><a href="#滑动窗口(流量控制)" class="headerlink" title="滑动窗口(流量控制)"></a>滑动窗口(流量控制)</h2><p>在确认应答机制中,对每一个发送的数据段,都要给一个ACK确认应答,收到ACK后再发送下一个数据段。这样做有一个比较大的缺点,就是性能较差。尤其是数据往返时间较长的时候。那么我们可不可以一次发送多个数据段呢:滑动窗口。<br>所谓的流量控制就是让发送方的发送速率不要太快,让接收方来得及接收。利用滑动窗口机制可以很方便的在TCP连接上实现对发送方的流量控制。</p><ul><li>接收端窗口rwnd(recv window):接收端缓冲区大小。接收端将此窗口值放在 TCP 报文的首部中的窗口字段,传送给发送端;</li><li>拥塞窗口cwnd(congestion window):发送端缓冲区大小;</li><li>发送窗口swnd(send window):发送窗口的上限值 = Min [rwnd, cwnd]。</li></ul><img src="/2019/09/25/传输层/20180524132418928.png" title="滑动窗口"><p><strong>滑动窗口内部的数据都是已经发送但是没有收到ACK应答的数据</strong>,滑动窗口<strong>左侧都是已经收到了ACK应答的数据</strong>,滑动窗口<strong>右侧是未发送的数据</strong>。</p><p>TCP协议规定,接收到三个重复的ACK响应,就开始重传响应所要求的报文的机制就是快重传速机制。<br>接收端ACK响应丢包其实对发送端发送的影响并不是那么大,后续的ACK响应能够处理好这个问题。</p><h2 id="快速重传为什么是三次冗余-ACK-?"><a href="#快速重传为什么是三次冗余-ACK-?" class="headerlink" title="快速重传为什么是三次冗余 ACK ?"></a>快速重传为什么是三次冗余 ACK ?</h2><p>如果连续收到两个ACK,极有可能是乱序问题,如果收到三个ACK,那么很大概率是丢包了,这个值在实际过程中也不一定就是三,可能远大于3,只是一个经验值。</p><h2 id="拥塞控制"><a href="#拥塞控制" class="headerlink" title="拥塞控制"></a>拥塞控制</h2><p>拥塞控制也就是考虑当前的网络环境,动态调整窗口大小,没有发生拥塞情况,则窗口增大,拥塞了窗口减小,如此往复,最终应该接近与接收端的窗口大小。</p><ol><li>慢启动和拥塞避免<br>在开始发送信息时,由于不知道具体的网络环境,为避免大量信息造成的拥塞现象,此时的拥塞窗口以最小值(即拥塞窗口和接收端窗口中的较小值)进行数据发送,并设定门限值作为慢启动算法和拥塞避免算法的分割点。慢启动是指以最小的拥塞窗口按照指数形式递增,达到门限值后,以拥塞避免算法,即线性递增方式增大拥塞窗口(这里递增时间间隔为一个往返时间RTT)。<br>在上述过程中,无论是窗口大小指数递增或者线性递增,当发生拥塞现象,则门限值更新为当前窗口大小的一半,拥塞窗口大小变为最小值,重复上述递增过程(此时属于网络环境限制,所以在接收端和拥塞窗口两个限制条件中选择拥塞窗口作为限制)。</li></ol><img src="/2019/09/25/传输层/20181028161235619.jpeg" title="拥塞控制"><ol start="2"><li><p>快重传和快恢复<br>当发送端连续收到三个重复的ack时,表示该数据段已经丢失,需要重发。当收到三个表示同一个数据段的ack时,不需要等待计时器超时,立即重新发送数据段(当时这三个ack要在超时之前到达发送端),因为能够收到接收端的ack确认信息,所以数据段只是单纯的丢失,而不是因为网络拥塞导致,所以此时不需要拥塞窗口更新为最小值进行慢启动(如果这样的话,反倒因为拥塞窗口的增长需要时间,可能导致性能降低),此时需要设置拥塞窗口大小为:门限值大小+3,当然此处的门限值已经更新为拥塞窗口值的一半大小,该行为也就是所谓的“乘法减少”,更新之后按照拥塞避免算法继续进行。</p></li><li><p>拥塞窗口大小为什么先以指数增加再以线性增加?<br>窗口大小首先以指数递增去探测一下网络的拥塞程度,执行拥塞避免算法后,拥塞窗口线性缓慢增大,防止网络过早出现拥塞。</p></li></ol><h2 id="粘包问题"><a href="#粘包问题" class="headerlink" title="粘包问题"></a>粘包问题</h2><p>TCP是基于字节流传输的,只维护发送出去多少,确认了多少,没有维护消息与消息之间的边界,因而可能导致粘包问题。<br>粘包问题本质上要在应用层维护消息与消息的边界。解决方案如下:</p><blockquote><p>在接收端接收的时候采用定长的方式接收;<br>在数据包尾添加一些分隔符;<br>在数据包头部加上数据包长度;<br>更复杂的应用层协议。</p></blockquote><h2 id="为什么udp不会粘包?"><a href="#为什么udp不会粘包?" class="headerlink" title="为什么udp不会粘包?"></a>为什么udp不会粘包?</h2><ol><li>TCP协议是面向流的协议,UDP是面向消息的协议<br>UDP段都是一条消息,应用程序必须以消息为单位提取数据,不能一次提取任意字节的数据</li><li>UDP具有保护消息边界,在每个UDP包中就有了消息头(消息来源地址,端口等信息),这样对于接收端来说就容易进行区分处理了。传输协议把数据当作一条独立的消息在网上传输,接收端只能接收独立的消息。接收端一次只能接收发送端发出的一个数据包,如果一次接受数据的大小小于发送端一次发送的数据大小,就会丢失一部分数据,即使丢失,接受端也不会分两次去接收</li></ol><h2 id="UDP-和-TCP-的特点"><a href="#UDP-和-TCP-的特点" class="headerlink" title="UDP 和 TCP 的特点"></a>UDP 和 TCP 的特点</h2><ul><li>用户数据报协议 UDP(User Datagram Protocol)是无连接的,尽最大可能交付,没有拥塞控制,面向报文(对于应用程序传下来的报文不合并也不拆分,只是添加 UDP 首部),支持一对一、一对多、多对一和多对多的交互通信。</li><li>传输控制协议 TCP(Transmission Control Protocol)是面向连接的,提供可靠交付,有流量控制,拥塞控制,提供全双工通信,面向字节流(把应用层传下来的报文看成字节流,把字节流组织成大小不等的数据块),每一条 TCP 连接只能是点对点的(一对一)。</li></ul><h2 id="UDP"><a href="#UDP" class="headerlink" title="UDP"></a>UDP</h2><p>UDP数据报最大长度64K(包含UDP首部),如果数据长度超过64K就需要在应用层手动分包,UDP无法保证包序,需要在应用层进行编号。</p><ul><li>特点</li></ul><ol><li>无连接:知道对端的IP和端口号就直接进行传输, 不需要建立连接。</li><li>不可靠:没有确认机制, 没有重传机制; 如果因为网络故障该段无法发到对方, UDP协议层也不会给应用层返回任何错误信息。</li><li>面向数据报:不能够灵活的控制读写数据的次数和数量,应用层交给UDP多长的报文, UDP原样发送, 既不会拆分, 也不会合并。</li><li>数据收不够灵活,但是能够明确区分两个数据包,避免粘包问题。</li></ol><ul><li>协议:<blockquote><p>NFS: 网络文件系统<br>TFTP: 简单文件传输协议<br>DHCP: 动态主机配置协议<br>BOOTP: 启动协议(用于无盘设备启动)<br>DNS: 域名解析协议</p></blockquote></li></ul><h2 id="基于-UDP-的几个例子"><a href="#基于-UDP-的几个例子" class="headerlink" title="基于 UDP 的几个例子"></a>基于 UDP 的几个例子</h2><ul><li>直播。直播对实时性的要求比较高,宁可丢包,也不要卡顿的,所以很多直播应用都基于 UDP 实现了自己的视频传输协议</li><li>实时游戏。游戏的特点也是实时性比较高,在这种情况下,采用自定义的可靠的 UDP 协议,自定义重传策略,能够把产生的延迟降到最低,减少网络问题对游戏造成的影响</li><li>物联网。一方面,物联网领域中断资源少,很可能知识个很小的嵌入式系统,而维护 TCP 协议的代价太大了;另一方面,物联网对实时性的要求也特别高。比如 Google 旗下的 Nest 简历 Thread Group,推出了物联网通信协议 Thread,就是基于 UDP 协议的</li></ul>]]></content>
<tags>
<tag> 计算机网络 </tag>
</tags>
</entry>
<entry>
<title>http</title>
<link href="/2019/09/25/http/"/>
<url>/2019/09/25/http/</url>
<content type="html"><![CDATA[<h2 id="HTTP-与-HTTPS-的区别"><a href="#HTTP-与-HTTPS-的区别" class="headerlink" title="HTTP 与 HTTPS 的区别"></a>HTTP 与 HTTPS 的区别</h2><table><thead><tr><th style="text-align:center">区别</th><th style="text-align:center">HTTP</th><th style="text-align:center">HTTPS</th></tr></thead><tbody><tr><td style="text-align:center">协议</td><td style="text-align:center">运行在 TCP 之上,明文传输,<strong>客户端与服务器端都无法验证对方的身份</strong></td><td style="text-align:center">身披 SSL( Secure Socket Layer 安全套接层 )外壳的 HTTP,运行于 SSL 上,SSL 运行于 TCP 之上, <strong>是添加了加密和认证机制的 HTTP</strong>。</td></tr><tr><td style="text-align:center">端口</td><td style="text-align:center"><strong>80</strong></td><td style="text-align:center"><strong>443</strong></td></tr><tr><td style="text-align:center">资源消耗</td><td style="text-align:center">较少</td><td style="text-align:center">由于加解密处理,会消耗更多的 CPU 和内存资源</td></tr><tr><td style="text-align:center">开销</td><td style="text-align:center">无需证书</td><td style="text-align:center"><strong>需要证书</strong>,而证书一般需要向认证机构购买</td></tr><tr><td style="text-align:center">加密机制</td><td style="text-align:center">无</td><td style="text-align:center">共享密钥加密和公开密钥加密并用的混合加密机制</td></tr><tr><td style="text-align:center">安全性</td><td style="text-align:center">弱</td><td style="text-align:center">由于加密机制,安全性强</td></tr></tbody></table><h3 id="对称加密与非对称加密"><a href="#对称加密与非对称加密" class="headerlink" title="对称加密与非对称加密"></a>对称加密与非对称加密</h3><p>对称密钥加密是指加密和解密使用同一个密钥的方式,<strong>这种方式存在的最大问题就是密钥发送问题,即如何安全地将密钥发给对方;</strong> 而非对称加密是指使用一对非对称密钥,即公钥和私钥,公钥可以随意发布,但私钥只有自己知道。<strong>发送密文的一方使用对方的公钥进行加密处理,对方接收到加密信息后,使用自己的私钥进行解密。</strong> 由于非对称加密的方式不需要发送用来解密的私钥,所以可以保证安全性;但是和对称加密比起来,非常的慢.</p><blockquote><p>我们还是用对称加密来传送消息,但对称加密所使用的密钥我们可以通过非对称加密的方式发送出去。</p></blockquote><h3 id="HTTP2"><a href="#HTTP2" class="headerlink" title="HTTP2"></a>HTTP2</h3><p>HTTP2 可以提高了网页的性能。</p><p>在 HTTP1 中浏览器<strong>限制了同一个域名下的请求数量</strong>(Chrome 下一般是六个),当在请求很多资源的时候,由于队头阻塞当浏览器达到最大请求数量时,剩余的资源需等待当前的六个请求完成后才能发起请求。</p><p>HTTP2 中引入了<strong>多路复用</strong>的技术,这个技术可以只通过一个 TCP 连接就可以传输所有的请求数据。多路复用可以绕过浏览器限制同一个域名下的请求数量的问题,进而提高了网页的性能。</p><h2 id="HTTP有哪些方法?"><a href="#HTTP有哪些方法?" class="headerlink" title="HTTP有哪些方法?"></a>HTTP有哪些方法?</h2><ul><li>HTTP1.0定义了三种请求方法: GET, POST 和 HEAD方法</li><li>HTTP1.1新增了五种请求方法:OPTIONS, PUT, DELETE, TRACE 和 CONNECT</li></ul><h2 id="这些方法的具体作用是什么?"><a href="#这些方法的具体作用是什么?" class="headerlink" title="这些方法的具体作用是什么?"></a>这些方法的具体作用是什么?</h2><ul><li>GET: 通常用于请求服务器发送某些资源(<strong>幂等</strong>、<strong>缓存</strong>)</li><li>HEAD: 请求资源的头部信息, 并且这些头部与 HTTP GET 方法请求时返回的一致. 该请求方法的一个使用场景是在下载一个大文件前先获取其大小再决定是否要下载, 以此可以节约带宽资源</li><li>OPTIONS: 用于获取目的资源所支持的通信选项</li><li>POST: 发送数据给服务器(<strong>不幂等</strong>)</li><li>PUT: 用于新增资源或者使用请求中的有效负载<strong>替换</strong>目标资源的表现形式(<strong>幂等</strong>)</li><li>DELETE: 用于删除指定的资源(<strong>幂等</strong>)</li><li>PATCH: 用于对资源进行<strong>部分修改</strong></li><li>CONNECT: HTTP/1.1协议中预留给能够将连接改为管道方式的代理服务器</li><li>TRACE: 回显服务器收到的请求,主要用于测试或诊断</li></ul><h2 id="Methods-分类"><a href="#Methods-分类" class="headerlink" title="Methods 分类"></a>Methods 分类</h2><ul><li>Safe Methods 对资源的访问权限是 read-only 的请求 method,被归类为 safe methods,例如 GET, HEAD, OPTIONS, TRACE。</li><li>Idempotent Methods 多次请求与一次请求所得到的结果相同,被归类为 idempotent method,例如 PUT, DELETE 和 safe methods。</li><li>Cacheable Methods 应答允许缓存的methods,例如 GET, HEAD, POST (大多数实现只支持GET 和 HEAD)。</li></ul><h2 id="GET和POST有什么区别?"><a href="#GET和POST有什么区别?" class="headerlink" title="GET和POST有什么区别?"></a>GET和POST有什么区别?</h2><ul><li>数据传输方式不同:GET请求通过URL传输数据,而POST的数据通过请求体传输。</li><li>安全性不同:POST的数据因为在请求主体内,所以有一定的安全性保证,而GET的数据在URL中,通过历史记录,缓存很容易查到数据信息。</li><li>数据类型不同:GET只允许 ASCII 字符,而POST无限制</li><li>GET无害: 刷新、后退等浏览器操作GET请求是无害的,POST可能重复提交表单</li><li>特性不同:GET是安全(这里的安全是指只读特性,就是使用这个方法不会引起服务器状态变化)且幂等(<strong>幂等的概念是指同一个请求方法执行多次和仅执行一次的效果完全相同</strong>),而POST是非安全非幂等</li></ul><h2 id="PUT和POST都是给服务器发送新增资源,有什么区别?"><a href="#PUT和POST都是给服务器发送新增资源,有什么区别?" class="headerlink" title="PUT和POST都是给服务器发送新增资源,有什么区别?"></a>PUT和POST都是给服务器发送新增资源,有什么区别?</h2><p><strong>PUT 和POST方法的区别是,PUT方法是幂等的:连续调用一次或者多次的效果相同(无副作用),而POST方法是非幂等的。</strong></p><p>除此之外还有一个区别,<strong>通常情况下,PUT的URI指向是具体单一资源,而POST可以指向资源集合</strong>。</p><p>举个例子,我们在开发一个博客系统,当我们要创建一篇文章的时候往往用<code>POST https://www.jianshu.com/articles</code>,这个请求的语义是,在articles的资源集合下创建一篇新的文章,如果我们多次提交这个请求会创建多个文章,这是非幂等的。</p><p>而<code>PUT https://www.jianshu.com/articles/820357430</code>的语义是更新对应文章下的资源(比如修改作者名称等),这个URI指向的就是单一资源,而且是幂等的,比如你把『刘德华』修改成『蔡徐坤』,提交多少次都是修改成『蔡徐坤』</p><blockquote><p>ps: 『POST表示创建资源,PUT表示更新资源』这种说法是错误的,两个都能创建资源,根本区别就在于幂等性</p></blockquote><h2 id="PUT和PATCH都是给服务器发送修改资源,有什么区别?"><a href="#PUT和PATCH都是给服务器发送修改资源,有什么区别?" class="headerlink" title="PUT和PATCH都是给服务器发送修改资源,有什么区别?"></a>PUT和PATCH都是给服务器发送修改资源,有什么区别?</h2><p>PUT和PATCH都是更新资源,而PATCH用来对已知资源进行局部更新。</p><p>比如我们有一篇文章的地址<code>https://www.jianshu.com/articles/820357430</code>,这篇文章的可以表示为:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">article = {</span><br><span class="line"> author: <span class="string">'dxy'</span>,</span><br><span class="line"> creationDate: <span class="string">'2019-6-12'</span>,</span><br><span class="line"> content: <span class="string">'我写文章像蔡徐坤'</span>,</span><br><span class="line"> id: <span class="number">820357430</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>当我们要修改文章的作者时,我们可以直接发送<code>PUT https://www.jianshu.com/articles/820357430</code>,这个时候的数据应该是:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> author:<span class="string">'蔡徐坤'</span>,</span><br><span class="line"> creationDate: <span class="string">'2019-6-12'</span>,</span><br><span class="line"> content: <span class="string">'我写文章像蔡徐坤'</span>,</span><br><span class="line"> id: <span class="number">820357430</span></span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>这种直接覆盖资源的修改方式应该用put,但是你觉得每次都带有这么多无用的信息,那么可以发送<code>PATCH https://www.jianshu.com/articles/820357430</code>,这个时候只需要:</p><figure class="highlight js"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">{</span><br><span class="line"> author:<span class="string">'蔡徐坤'</span>,</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="http的请求报文是什么样的?"><a href="#http的请求报文是什么样的?" class="headerlink" title="http的请求报文是什么样的?"></a>http的请求报文是什么样的?</h2><p>请求报文有4部分组成:</p><ul><li>请求行</li><li>请求头部</li><li>空行</li><li>请求体</li></ul><img src="/2019/09/25/http/1567478479466.7fb0babf.png"><ul><li>请求行包括:请求方法字段、URL字段、HTTP协议版本字段。它们用空格分隔。例如,GET /index.html HTTP/1.1。</li><li>请求头部:请求头部由关键字/值对组成,每行一对,关键字和值用英文冒号“:”分隔</li></ul><ol><li>User-Agent:产生请求的浏览器类型。</li><li>Accept:客户端可识别的内容类型列表。</li><li>Host:请求的主机名,允许多个域名同处一个IP地址,即虚拟主机。</li></ol><ul><li>请求体: post put等请求携带的数据</li></ul><img src="/2019/09/25/http/1567478494757.9af403f4.png"><h2 id="http的响应报文是什么样的?"><a href="#http的响应报文是什么样的?" class="headerlink" title="http的响应报文是什么样的?"></a>http的响应报文是什么样的?</h2><p>请求报文有4部分组成:</p><ul><li>响应行</li><li>响应头</li><li>空行</li><li>响应体</li></ul><img src="/2019/09/25/http/1567478507043.7a6c79d8.png"><ul><li>响应行: 由协议版本,状态码和状态码的原因短语组成,例如<code>HTTP/1.1 200 OK</code>。</li><li>响应头:响应部首组成</li><li>响应体:服务器响应的数据</li></ul><h2 id="聊一聊HTTP的部首有哪些?"><a href="#聊一聊HTTP的部首有哪些?" class="headerlink" title="聊一聊HTTP的部首有哪些?"></a>聊一聊HTTP的部首有哪些?</h2><blockquote><p>内容很多,重点看标『✨』内容</p></blockquote><p>通用首部字段(General Header Fields):请求报文和响应报文两方都会使用的首部</p><ul><li>Cache-Control 控制缓存 ✨</li><li>Connection 连接管理、逐条首部 ✨</li><li>Upgrade 升级为其他协议</li><li>via 代理服务器的相关信息</li><li>Wraning 错误和警告通知</li><li>Transfor-Encoding 报文主体的传输编码格式 ✨</li><li>Trailer 报文末端的首部一览</li><li>Pragma 报文指令</li><li>Date 创建报文的日期</li></ul><p>请求首部字段(Reauest Header Fields):客户端向服务器发送请求的报文时使用的首部</p><ul><li>Accept 客户端或者代理能够处理的媒体类型 ✨</li><li>Accept-Encoding 优先可处理的编码格式</li><li>Accept-Language 优先可处理的自然语言</li><li>Accept-Charset 优先可以处理的字符集</li><li>If-Match 比较实体标记(ETage) ✨</li><li>If-None-Match 比较实体标记(ETage)与 If-Match相反 ✨</li><li>If-Modified-Since 比较资源更新时间(Last-Modified)✨</li><li>If-Unmodified-Since比较资源更新时间(Last-Modified),与 If-Modified-Since相反 ✨</li><li>If-Rnages 资源未更新时发送实体byte的范围请求</li><li>Range 实体的字节范围请求 ✨</li><li>Authorization web的认证信息 ✨</li><li>Proxy-Authorization 代理服务器要求web认证信息</li><li>Host 请求资源所在服务器 ✨</li><li>From 用户的邮箱地址</li><li>User-Agent 客户端程序信息 ✨</li><li>Max-Forwrads 最大的逐跳次数</li><li>TE 传输编码的优先级</li><li>Referer 请求原始放的url</li><li>Expect 期待服务器的特定行为</li></ul><p>响应首部字段(Response Header Fields):从服务器向客户端响应时使用的字段</p><ul><li>Accept-Ranges 能接受的字节范围</li><li>Age 推算资源创建经过时间</li><li>Location 令客户端重定向的URI ✨</li><li>vary 代理服务器的缓存信息</li><li>ETag 能够表示资源唯一资源的字符串 ✨</li><li>WWW-Authenticate 服务器要求客户端的验证信息</li><li>Proxy-Authenticate 代理服务器要求客户端的验证信息</li><li>Server 服务器的信息 ✨</li><li>Retry-After 和状态码503 一起使用的首部字段,表示下次请求服务器的时间</li></ul><p>实体首部字段(Entiy Header Fields):针对请求报文和响应报文的实体部分使用首部</p><ul><li>Allow 资源可支持http请求的方法 ✨</li><li>Content-Language 实体的资源语言</li><li>Content-Encoding 实体的编码格式</li><li>Content-Length 实体的大小(字节)</li><li>Content-Type 实体媒体类型</li><li>Content-MD5 实体报文的摘要</li><li>Content-Location 代替资源的yri</li><li>Content-Rnages 实体主体的位置返回</li><li>Last-Modified 资源最后的修改资源 ✨</li><li>Expires 实体主体的过期资源 ✨</li></ul><h2 id="聊一聊HTTP的状态码有哪些?"><a href="#聊一聊HTTP的状态码有哪些?" class="headerlink" title="聊一聊HTTP的状态码有哪些?"></a>聊一聊HTTP的状态码有哪些?</h2><p>2XX 成功</p><ul><li>200 OK,表示从客户端发来的请求在服务器端被正确处理 ✨</li><li>201 Created 请求已经被实现,而且有一个新的资源已经依据请求的需要而建立</li><li>202 Accepted 请求已接受,但是还没执行,不保证完成请求</li><li>204 No content,表示请求成功,但响应报文不含实体的主体部分</li><li>206 Partial Content,进行范围请求 ✨</li></ul><p>3XX 重定向</p><ul><li>301 moved permanently,永久性重定向,表示资源已被分配了新的 URL</li><li>302 found,临时性重定向,表示资源临时被分配了新的 URL ✨</li><li>303 see other,表示资源存在着另一个 URL,应使用 GET 方法定向获取资源</li><li>304 not modified,表示服务器允许访问资源,但因发生请求未满足条件的情况</li><li>307 temporary redirect,临时重定向,和302含义相同</li></ul><p>4XX 客户端错误</p><ul><li>400 bad request,请求报文存在语法错误 ✨</li><li>401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息 ✨</li><li>403 forbidden,表示对请求资源的访问被服务器拒绝 ✨</li><li>404 not found,表示在服务器上没有找到请求的资源 ✨</li><li>408 Request timeout, 客户端请求超时</li><li>409 Confict, 请求的资源可能引起冲突</li></ul><p>5XX 服务器错误</p><ul><li>500 internal sever error,表示服务器端在执行请求时发生了错误 ✨</li><li>501 Not Implemented 请求超出服务器能力范围,例如服务器不支持当前请求所需要的某个功能,或者请求是服务器不支持的某个方法</li><li>503 service unavailable,表明服务器暂时处于超负载或正在停机维护,无法处理请求</li><li>505 http version not supported 服务器不支持,或者拒绝支持在请求中使用的 HTTP 版本</li></ul><h2 id="同样是重定向307,303,302的区别?"><a href="#同样是重定向307,303,302的区别?" class="headerlink" title="同样是重定向307,303,302的区别?"></a>同样是重定向307,303,302的区别?</h2><p>302是http1.0的协议状态码,在http1.1版本的时候为了细化302状态码,出来了303和307。</p><p>303明确表示客户端应当采用get方法获取资源,他会把POST请求变为GET请求进行重定向。 307会遵照浏览器标准,不会从post变为get。</p><h2 id="HTTP的keep-alive是干什么的?"><a href="#HTTP的keep-alive是干什么的?" class="headerlink" title="HTTP的keep-alive是干什么的?"></a>HTTP的keep-alive是干什么的?</h2><p>在早期的HTTP/1.0中,每次http请求都要创建一个连接,而创建连接的过程需要消耗资源和时间,为了减少资源消耗,缩短响应时间,就需要重用连接。在后来的HTTP/1.0中以及HTTP/1.1中,引入了重用连接的机制,就是在http请求头中加入<strong>Connection: keep-alive</strong>来告诉对方这个请求响应完成后不要关闭,下一次咱们还用这个请求继续交流。协议规定HTTP/1.0如果想要保持长连接,需要在请求头中加上<strong>Connection: keep-alive。</strong></p><p>keep-alive的优点:</p><ul><li>较少的CPU和内存的使用(由于同时打开的连接的减少了)</li><li>允许请求和应答的HTTP管线化</li><li>降低拥塞控制 (TCP连接减少了)</li><li>减少了后续请求的延迟(无需再进行握手)</li><li>报告错误无需关闭TCP连接</li></ul><h2 id="为什么有了HTTP为什么还要HTTPS?"><a href="#为什么有了HTTP为什么还要HTTPS?" class="headerlink" title="为什么有了HTTP为什么还要HTTPS?"></a>为什么有了HTTP为什么还要HTTPS?</h2><p>https是安全版的http,因为http协议的数据都是明文进行传输的,所以对于一些敏感信息的传输就很不安全,HTTPS就是为了解决HTTP的不安全而生的。</p><h2 id="HTTPS是如何保证安全的?"><a href="#HTTPS是如何保证安全的?" class="headerlink" title="HTTPS是如何保证安全的?"></a>HTTPS是如何保证安全的?</h2><p>过程比较复杂,我们得先理解两个概念</p><p>对称加密:即通信的双方都使用同一个秘钥进行加解密,比如特务接头的暗号,就属于对称加密</p><p>对称加密虽然很简单性能也好,但是无法解决首次把秘钥发给对方的问题,很容易被黑客拦截秘钥。</p><p>非对称加密:</p><ol><li>私钥 + 公钥= 密钥对</li><li>即用私钥加密的数据,只有对应的公钥才能解密,用公钥加密的数据,只有对应的私钥才能解密</li><li>因为通信双方的手里都有一套自己的密钥对,通信之前双方会先把自己的公钥都先发给对方</li><li>然后对方再拿着这个公钥来加密数据响应给对方,等到到了对方那里,对方再用自己的私钥进行解密</li></ol><p>非对称加密虽然安全性更高,但是带来的问题就是速度很慢,影响性能。</p><p>解决方案:</p><p>那么结合两种加密方式,将对称加密的密钥使用非对称加密的公钥进行加密,然后发送出去,接收方使用私钥进行解密得到对称加密的密钥,然后双方可以使用对称加密来进行沟通。</p><p>此时又带来一个问题,中间人问题:</p><p>如果此时在客户端和服务器之间存在一个中间人,这个中间人只需要把原本双方通信互发的公钥,换成自己的公钥,这样中间人就可以轻松解密通信双方所发送的所有数据。</p><p>所以这个时候需要一个安全的第三方颁发证书(CA),证明身份的身份,防止被中间人攻击。</p><p>证书中包括:签发者、证书用途、使用者公钥、使用者私钥、使用者的HASH算法、证书到期时间等</p><img src="/2019/09/25/http/1567479571116.11594f83.png"><p>但是问题来了,如果中间人篡改了证书,那么身份证明是不是就无效了?这个证明就白买了,这个时候需要一个新的技术,数字签名。</p><p>数字签名就是用CA自带的HASH算法对证书的内容进行HASH得到一个摘要,再用CA的私钥加密,最终组成数字签名。</p><p>当别人把他的证书发过来的时候,我再用同样的Hash算法,再次生成消息摘要,然后用CA的公钥对数字签名解密,得到CA创建的消息摘要,两者一比,就知道中间有没有被人篡改了。</p><p>这个时候就能最大程度保证通信的安全了。</p><h2 id="HTTP2相对于HTTP1-x有什么优势和特点?"><a href="#HTTP2相对于HTTP1-x有什么优势和特点?" class="headerlink" title="HTTP2相对于HTTP1.x有什么优势和特点?"></a>HTTP2相对于HTTP1.x有什么优势和特点?</h2><h3 id="二进制分帧"><a href="#二进制分帧" class="headerlink" title="二进制分帧"></a>二进制分帧</h3><p>帧:HTTP/2 数据通信的最小单位消息:指 HTTP/2 中逻辑上的 HTTP 消息。例如请求和响应等,消息由一个或多个帧组成。</p><p>流:存在于连接中的一个虚拟通道。流可以承载双向消息,每个流都有一个唯一的整数ID</p><p>HTTP/2 采用二进制格式传输数据,而非 HTTP 1.x 的文本格式,二进制协议解析起来更高效。</p><h3 id="头部压缩"><a href="#头部压缩" class="headerlink" title="头部压缩"></a>头部压缩</h3><p>HTTP/1.x会在请求和响应中中重复地携带不常改变的、冗长的头部数据,给网络带来额外的负担。</p><ul><li>HTTP/2在客户端和服务器端使用“首部表”来跟踪和存储之前发送的键-值对,对于相同的数据,不再通过每次请求和响应发送</li><li>首部表在HTTP/2的连接存续期内始终存在,由客户端和服务器共同渐进地更新;</li><li>每个新的首部键-值对要么被追加到当前表的末尾,要么替换表中之前的值。</li></ul><blockquote><p>你可以理解为只发送差异数据,而不是全部发送,从而减少头部的信息量</p></blockquote><img src="/2019/09/25/http/1567493242362.4bbb6a2c.png"><h3 id="服务器推送"><a href="#服务器推送" class="headerlink" title="服务器推送"></a>服务器推送</h3><p>服务端可以在发送页面HTML时主动推送其它资源,而不用等到浏览器解析到相应位置,发起请求再响应。例如服务端可以主动把JS和CSS文件推送给客户端,而不需要客户端解析HTML时再发送这些请求。</p><p>服务端可以主动推送,客户端也有权利选择是否接收。如果服务端推送的资源已经被浏览器缓存过,浏览器可以通过发送RST_STREAM帧来拒收。主动推送也遵守同源策略,服务器不会随便推送第三方资源给客户端。</p><h3 id="多路复用"><a href="#多路复用" class="headerlink" title="多路复用"></a>多路复用</h3><p>HTTP 1.x 中,如果想并发多个请求,必须使用多个 TCP 链接,且浏览器为了控制资源,还会对单个域名有 6-8个的TCP链接请求限制。</p><p>HTTP2中:</p><ul><li>同域名下所有通信都在单个连接上完成。</li><li>单个连接可以承载任意数量的双向数据流。</li><li>数据流以消息的形式发送,而消息又由一个或多个帧组成,多个帧之间可以乱序发送,因为根据帧首部的流标识可以重新组装</li></ul><img src="/2019/09/25/http/1567493352624.923e523a.png">]]></content>
<tags>
<tag> http </tag>
</tags>
</entry>
<entry>
<title>索引</title>
<link href="/2019/09/25/%E7%B4%A2%E5%BC%95/"/>
<url>/2019/09/25/%E7%B4%A2%E5%BC%95/</url>
<content type="html"><![CDATA[<h2 id="什么是索引?"><a href="#什么是索引?" class="headerlink" title="什么是索引?"></a>什么是索引?</h2><p>“索引”是为了能够更快地查询数据。比如一本书的目录,就是这本书的内容的索引,读者可以通过在目录中快速查找自己想要的内容,然后根据页码去找到具体的章节。<br>数据库也是一样,如果查询语句使用到了索引,会先去索引里面查询,取得数据所在行的物理地址,进而访问数据。</p><h2 id="索引的优缺点"><a href="#索引的优缺点" class="headerlink" title="索引的优缺点"></a>索引的优缺点</h2><p>优势:以快速检索,减少I/O次数,加快检索速度;根据索引分组和排序,可以加快分组和排序;</p><p>劣势:索引本身也是表,因此会占用存储空间。索引的维护和创建需要时间成本,这个成本随着数据量增大而增大;构建索引会降低数据表的修改操作(删除,添加,修改)的效率,因为在修改数据表的同时还需要修改索引表。</p><h2 id="索引的分类"><a href="#索引的分类" class="headerlink" title="索引的分类"></a>索引的分类</h2><p>在MySQL中,常见的索引类型有:主键索引、唯一索引、普通索引、全文索引、组合索引。创建语法分别为:<br><img src="/2019/09/25/索引/java6-1567744443.png" title="索引"></p><h2 id="索引的实现原理"><a href="#索引的实现原理" class="headerlink" title="索引的实现原理"></a>索引的实现原理</h2><p>MySQL的索引是由存储引擎来实现的。由于存储引擎不同,所以具有不同的索引类型,如BTree索引,B+Tree索引,哈希索引,全文索引等。这里由于主要介绍BTree索引和B+Tree索引,我们平时使用最多的InnoDB引擎就是基于B+Tree索引的。<br>从二叉搜索树聊起<br>二叉树根据用途不同,衍生了不同的变种,比如堆,比如二叉搜索树。<br>而二叉搜索树中,为了防止极端情况树的高度过大影响查询效率,所以衍生出了一些平衡二叉查找树,最典型的就是AVL和红黑树。<br>但二叉树在数据量较大时,深度过深,不太适合数据库的查询,所以数据库使用了多叉树。<br>BTree(又称为B-Tree)是一个平衡搜索多叉树。BTree的结构如下图:<br>B-树的特性:</p><ol><li>关键字集合分布在整颗树中;</li><li>任何一个关键字出现且只出现在一个结点中;</li><li>搜索有可能在非叶子结点结束;</li><li>其搜索性能等价于在关键字全集内做一次二分查找;</li><li>自动层次控制;</li></ol><img src="/2019/09/25/索引/java8-1567744444.png" title="索引实现原理"><p>B+Tree是BTree的一种变种。B+Tree和BTree的不同主要在于:</p><p>B+Tree中的非叶子结点不存储数据,只存储键值;<br>B+Tree的叶子结点没有指针,所有键值都会出现在叶子结点上,且key存储的键值对应data数据的物理地址;<br>B+Tree的每个非叶子节点由n个键值key和n个指针point组成;<br>叶子节点有顺序结点指针</p><img src="/2019/09/25/索引/5687393-717ab97b31dfa84b.png" title="索引结构"><p>B+的搜索与B-树也基本相同,区别是B+树只有达到叶子结点才命中(B-树可以在非叶子结点命中),其性能也等价于在关键字全集做一次二分查找;<br>B+的特性:</p><ol><li>所有关键字都出现在叶子结点的链表中(稠密索引),且链表中的关键字恰好是有序的;</li><li>不可能在非叶子结点命中;</li><li>非叶子结点相当于是叶子结点的索引(稀疏索引),叶子结点相当于是存储(关键字)数据的数据层;</li><li>更适合文件索引系统;</li></ol><img src="/2019/09/25/索引/5687393-59a8ea11c8555ab0.gif" title="动态图:插入过程"><h2 id="索引的物理存储"><a href="#索引的物理存储" class="headerlink" title="索引的物理存储"></a>索引的物理存储</h2><p>一般来说,索引本身也很大,不可能全部存储在内存中,因此索引往往以索引文件的形式存储的磁盘上。这样的话,索引查找过程中就要产生磁盘I/O消耗,相对于内存存取,I/O存取的消耗要高几个数量级,所以评价一个数据结构作为索引的优劣最重要的指标就是在查找过程中磁盘I/O操作次数的渐进复杂度。换句话说,索引的结构组织要尽量减少查找过程中磁盘I/O的存取次数。</p><h2 id="B-Tree优点"><a href="#B-Tree优点" class="headerlink" title="B+Tree优点"></a>B+Tree优点</h2><p>B+tree的内部结点并没有指向关键字具体信息的指针。因此其内部结点相对B 树更小。如果把所有同一内部结点的关键字存放在同一盘块中,那么盘块所能容纳的关键字数量也越多。一次性读入内存中的需要查找的关键字也就越多。相对来说IO读写次数也就降低了。<br>举个例子,假设磁盘中的一个盘块容纳16bytes,而一个关键字2bytes,一个关键字具体信息指针2bytes。一棵9阶B-tree(一个结点最多8个关键字)的内部结点需要2个盘快。而B+<br>树内部结点只需要1个盘快。当需要把内部结点读入内存中的时候,B 树就比B+树多一次盘块查找时间(在磁盘中就是盘片旋转的时间)。<br>B+tree的查询效率更加稳定<br>由于非终结点并不是最终指向文件内容的结点,而只是叶子结点中关键字的索引。所以任何关键字的查找必须走一条从根结点到叶子结点的路。所有关键字查询的路径长度相同,导致每一个数据的查询效率相当。</p><h2 id="聚簇索引和非聚簇索引"><a href="#聚簇索引和非聚簇索引" class="headerlink" title="聚簇索引和非聚簇索引"></a>聚簇索引和非聚簇索引</h2><p>为什么InnoDB非主键索引普遍比主键索引要慢?InnoDB使用了聚簇索引,主键索引主需要查询一次,而非主键索引需要查询两次。</p><ol><li>非聚簇索引的主索引和辅助索引几乎是一样的,只是主索引不允许重复,不允许空值,他们的叶子结点的key都存储指向键值对应的数据的物理地址。<br>非聚簇索引的数据表和索引表是分开存储的。非聚簇索引中的数据是根据数据的插入顺序保存。因此非聚簇索引更适合单个数据的查询。插入顺序不受键值影响。</li><li>聚簇索引的主索引的叶子结点存储的是键值对应的数据本身,辅助索引的叶子结点存储的是键值对应的数据的主键键值。因此主键的值长度越小越好,类型越简单越好。<br>聚簇索引的数据和主键索引存储在一起。<br>聚簇索引的数据是根据主键的顺序保存。因此适合按主键索引的区间查找,可以有更少的磁盘I/O,加快查询速度。但是也是因为这个原因,聚簇索引的插入顺序最好按照主键单调的顺序插入,否则会频繁的引起页分裂(BTree插入时的一个操作),严重影响性能。<br>在InnoDB中,如果只需要查找索引的列,就尽量不要加入其它的列,这样会提高查询效率。</li></ol><h2 id="什么叫做覆盖索引?"><a href="#什么叫做覆盖索引?" class="headerlink" title="什么叫做覆盖索引?"></a>什么叫做覆盖索引?</h2><p>对于INNODB的辅助索引,它的叶子节点存储的是索引值和指向主键索引的位置,然后需要通过主键在查询表的字段值,所以辅助索引存储了主键的值<br>如果索引包含所有满足查询需要的数据的索引成为覆盖索引(Covering Index),也就是平时所说的不需要回表操作</p><h2 id="索引失效原因"><a href="#索引失效原因" class="headerlink" title="索引失效原因"></a>索引失效原因</h2><p>对索引列运算,运算包括(+、-、<em>、/、!、<>、%、like’%_’(% 放在前面)。<br>类型错误,如字段类型为 varchar,where 条件用 number。<br>对索引应用内部函数,这种情况下应该要建立基于函数的索引。例如 select </em> from template t where ROUND (t.logicdb_id) = 1,此时应该建 ROUND (t.logicdb_id) 为索引。<br>MySQL 8.0 开始支持函数索引,5.7 可以通过虚拟列的方式来支持,之前只能新建一个 ROUND (t.logicdb_id) 列然后去维护。<br>如果条件有 or,即使其中有条件带索引也不会使用(这也是为什么建议少使用 or 的原因),如果想使用 or,又想索引有效,只能将 or 条件中的每个列加上索引。<br>如果列类型是字符串,那一定要在条件中数据使用引号,否则不使用索引。<br>B-tree 索引 is null 不会走,is not null 会走,位图索引 is null,is not null 都会走。<br>组合索引遵循最左原则。</p><h2 id="索引的建立"><a href="#索引的建立" class="headerlink" title="索引的建立"></a>索引的建立</h2><p>最重要的肯定是根据业务经常查询的语句。<br>尽量选择区分度高的列作为索引,区分度的公式是 COUNT(DISTINCT col) / COUNT(*),表示字段不重复的比率,比率越大我们扫描的记录数就越少。<br>如果业务中唯一特性最好建立唯一键,一方面可以保证数据的正确性,另一方面索引的效率能大大提高。</p><h2 id="使用索引进行排序"><a href="#使用索引进行排序" class="headerlink" title="使用索引进行排序"></a>使用索引进行排序</h2><p>有两种方式生成有序结果集:一是使用filesort,二是按索引顺序扫描<br>利用索引进行排序操作是非常快的,而且可以利用同一索引同时进 行查找和排序操作。当索引的顺序与ORDER BY中的列顺序相同且所有的列是同一方向(全部升序或者全部降序)时,可以使用索引来排序,如果查询是连接多个表,仅当ORDER BY中的所有列都是第一个表的列时才会使用索引,其它情况都会使用filesort<br>当MySQL不能使用索引进行排序时,就会利用自己的排序算法(快速排序算法)在内存(sort buffer)中对数据进行排序,如果内存装载不下,它会将磁盘上的数据进行分块,再对各个数据块进行排序,然后将各个块合并成有序的结果集(实际上就是外排序)</p>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>存储引擎</title>
<link href="/2019/09/25/%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E/"/>
<url>/2019/09/25/%E5%AD%98%E5%82%A8%E5%BC%95%E6%93%8E/</url>
<content type="html"><![CDATA[<h2 id="MyISAM和InnoDB区别"><a href="#MyISAM和InnoDB区别" class="headerlink" title="MyISAM和InnoDB区别"></a>MyISAM和InnoDB区别</h2><p>MyISAM是MySQL的默认数据库引擎(5.5版之前)。虽然性能极佳,而且提供了大量的特性,包括全文索引、压缩、空间函数等,但MyISAM不支持事务和行级锁,而且最大的缺陷就是崩溃后无法安全恢复。不过,5.5版本之后,MySQL引入了InnoDB(事务性数据库引擎),MySQL 5.5版本后默认的存储引擎为InnoDB。MyISAM 只有表级锁(table-level locking),而InnoDB 支持行级锁(row-level locking)和表级锁,默认为行级锁。MyISAM 强调的是性能,每次查询具有原子性,其执行数度比InnoDB类型更快,但是不提供事务支持。但是InnoDB 提供事务支持事务,外部键等高级数据库功能。 具有事务(commit)、回滚(rollback)和崩溃修复能力(crash recovery capabilities)的事务安全(transaction-safe (ACID compliant))型表。 MyISAM不支持外键,而InnoDB支持。<br>由于MyISAM缓存有表的meta-data(行数等)。因此在做count(*)时,对于一个结构很好的查询是不需要消耗多少资源的。而对于InnDB来说,没有这种缓存,当你需要行锁定,事务时,使用InnoDB是更好的选择,也具有更高级的安全性,此外,MyISAM为非聚簇索引,InnoDB为聚簇索引。</p><h2 id="MyISAM"><a href="#MyISAM" class="headerlink" title="MyISAM"></a>MyISAM</h2><p>不支持行锁,读取时对需要读到得所有表加锁,写入时对表加排他锁<br>不支持事务,不支持外键,不支持崩溃后的安全恢复,在表有读取查询的同时,支持往表中插入新纪录,支持BLOB和TEXT的前500个字符索引,支持全文索引,支持延迟更新索引,极大地提高了写入性能,对于不会进行修改的表,支持压缩表,极大地减少了磁盘空间的占用。</p><h2 id="InnoDB"><a href="#InnoDB" class="headerlink" title="InnoDB"></a>InnoDB</h2><p>支持行锁,采用MVCC来支持高并发,有可能死锁<br>支持事务,支持外键,支持崩溃后的安全恢复,5.6.24之后支持全文索引。</p>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>Explain</title>
<link href="/2019/09/25/Explain/"/>
<url>/2019/09/25/Explain/</url>
<content type="html"><![CDATA[<h2 id="基本用法"><a href="#基本用法" class="headerlink" title="基本用法"></a>基本用法</h2><p>desc 或者 explain 加上你的 SQL。<br>extended explain 加上你的 SQL,然后通过 show warnings 可以查看实际执行的语句,这一点也是非常有用的,很多时候不同的写法经 SQL 分析后,实际执行的代码是一样的。</p><h2 id="提高性能的特性"><a href="#提高性能的特性" class="headerlink" title="提高性能的特性"></a>提高性能的特性</h2><p>索引覆盖(covering index):需要查询的数据在索引上都可以查到不需要回表 EXTRA 列显示 using index<br>ICP特性(Index Condition Pushdown):本来 index 仅仅是 data access 的一种访问模式,存数引擎通过索引回表获取的数据会传递到 MySQL Server 层进行 where 条件过滤。<br>5.6 版本开始当 ICP 打开时,如果部分 where 条件能使用索引的字段,MySQL Server 会把这部分下推到引擎层,可以利用 index 过滤的 where 条件在存储引擎层进行数据过滤。<br>EXTRA 显示 using index condition。需要了解 MySQL 的架构图分为 Server 和存储引擎层。<br>索引合并(index merge):对多个索引分别进行条件扫描,然后将它们各自的结果进行合并(intersect/union)。<br>一般用 or 会用到,如果是 AND 条件,考虑建立复合索引。EXPLAIN 显示的索引类型会显示 index_merge,EXTRA 会显示具体的合并算法和用到的索引。</p><h2 id="Extra-字段"><a href="#Extra-字段" class="headerlink" title="Extra 字段"></a>Extra 字段</h2><ul><li>using filesort:说明 MySQL 会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取。MySQL 中无法利用索引完成的排序操作称为“文件排序”,其实不一定是文件排序,内部使用的是快排。</li><li>using temporary:使用了临时表保存中间结果,MySQL 在对查询结果排序时使用临时表。常见于排序 order by 和分组查询 group by。</li><li>using index:表示相应的 SELECT 操作中使用了覆盖索引(Covering Index),避免访问了表的数据行,效率不错。</li><li>impossible where:where 子句的值总是 false,不能用来获取任何元组。</li><li>select tables optimized away:在没有 group by 子句的情况下基于索引优化 MIN/MAX 操作或者对于 MyISAM 存储引擎优化 COUNT(*) 操作,不必等到执行阶段再进行计算,查询执行计划生成的阶段即完成优化。</li><li>distinct:优化 distinct 操作,在找到第一匹配的元组后即停止找同样值的操作。<br>using filesort、using temporary 这两项出现时需要注意下,这两项是十分耗费性能的。<br>在使用 group by 的时候,虽然没有使用 order by,如果没有索引,是可能同时出现 using filesort,using temporary 的。<br>因为 group by 就是先排序在分组,如果没有排序的需要,可以加上一个 order by NULL 来避免排序,这样 using filesort 就会去除,能提升一点性能。</li></ul><h2 id="type-字段"><a href="#type-字段" class="headerlink" title="type 字段"></a>type 字段</h2><ul><li>system:表只有一行记录(等于系统表),这是 const 类型的特例,平时不会出现。</li><li>const:如果通过索引依次就找到了,const 用于比较主键索引或者 unique 索引。因为只能匹配一行数据,所以很快。如果将主键置于 where 列表中,MySQL 就能将该查询转换为一个常量。</li><li>eq_ref:唯一性索引扫描,对于每个索引键,表中只有一条记录与之匹配。常见于主键或唯一索引扫描。</li><li>ref:非唯一性索引扫描,返回匹配某个单独值的所有行。本质上也是一种索引访问,它返回所有匹配某个单独值的行,然而它可能会找到多个符合条件的行,所以它应该属于查找和扫描的混合体。</li><li>range:只检索给定范围的行,使用一个索引来选择行。key 列显示使用了哪个索引,一般就是在你的 where 语句中出现 between、<、>、in 等的查询。这种范围扫描索引比全表扫描要好,因为只需要开始于缩印的某一点,而结束于另一点,不用扫描全部索引。</li><li>index:Full Index Scan ,index 与 ALL 的区别为 index 类型只遍历索引树,这通常比 ALL 快,因为索引文件通常比数据文件小。也就是说虽然 ALL 和 index 都是读全表,但 index 是从索引中读取的,而 ALL 是从硬盘读取的。</li><li>all:Full Table Scan,遍历全表获得匹配的行。</li></ul>]]></content>
<tags>
<tag> mysql </tag>
</tags>
</entry>
<entry>
<title>哨兵sentinel</title>
<link href="/2019/09/25/%E5%93%A8%E5%85%B5sentinel/"/>
<url>/2019/09/25/%E5%93%A8%E5%85%B5sentinel/</url>
<content type="html"><![CDATA[<h2 id="哨兵的介绍"><a href="#哨兵的介绍" class="headerlink" title="哨兵的介绍"></a>哨兵的介绍</h2><p>哨兵是redis集群架构中非常重要的一个组件,主要功能如下:</p><ol><li>集群监控,负责监控redis master和slave进程是否正常工作</li><li>消息通知,如果某个redis实例有故障,那么哨兵负责发送消息作为报警通知给管理员</li><li>故障转移,如果master node挂掉了,会自动转移到slave node上</li><li>配置中心,如果故障转移发生了,通知client客户端新的master地址</li></ol><p>哨兵本身也是分布式的,作为一个哨兵集群去运行,互相协同工作</p><ol><li>故障转移时,判断一个master node是宕机了,需要大部分的哨兵都同意才行,涉及到了分布式选举的问题</li><li>即使部分哨兵节点挂掉了,哨兵集群还是能正常工作的,因为如果一个作为高可用机制重要组成部分的故障转移系统本身是单点的,那就很坑爹了</li></ol><h2 id="哨兵的核心知识"><a href="#哨兵的核心知识" class="headerlink" title="哨兵的核心知识"></a>哨兵的核心知识</h2><ul><li>哨兵至少需要3个实例,来保证自己的健壮性</li><li>哨兵 + redis主从的部署架构,是不会保证数据零丢失的,只能保证redis集群的高可用性</li><li>对于哨兵 + redis主从这种复杂的部署架构,尽量在测试环境和生产环境,都进行充足的测试和演练</li></ul><h2 id="为什么redis哨兵集群只有2个节点无法正常工作?"><a href="#为什么redis哨兵集群只有2个节点无法正常工作?" class="headerlink" title="为什么redis哨兵集群只有2个节点无法正常工作?"></a>为什么redis哨兵集群只有2个节点无法正常工作?</h2><p>哨兵集群必须部署2个以上节点<br>如果哨兵集群仅仅部署了个2个哨兵实例,quorum=1<br>master宕机,s1和s2中只要有1个哨兵认为master宕机就可以还行切换,同时s1和s2中会选举出一个哨兵来执行故障转移<br>同时这个时候,需要majority,也就是大多数哨兵都是运行的,2个哨兵的majority就是2(2的majority=2,3的majority=2,5的majority=3,4的majority=2),2个哨兵都运行着,就可以允许执行故障转移<br>但是如果整个M1和S1运行的机器宕机了,那么哨兵只有1个了,此时就没有majority来允许执行故障转移,虽然另外一台机器还有一个R1,但是故障转移不会执行</p><p><strong>要有majority个哨兵同意故障转移,才行</strong>。</p><h2 id="主备切换的过程,可能会导致数据丢失"><a href="#主备切换的过程,可能会导致数据丢失" class="headerlink" title="主备切换的过程,可能会导致数据丢失"></a>主备切换的过程,可能会导致数据丢失</h2><ul><li>异步复制导致的数据丢失<br>因为master -> slave的复制是异步的,所以可能有部分数据还没复制到slave,master就宕机了,此时这些部分数据就丢失了</li><li>脑裂导致的数据丢失<br>脑裂,也就是说,某个master所在机器突然脱离了正常的网络,跟其他slave机器不能连接,但是实际上master还运行着<br>此时哨兵可能就会认为master宕机了,然后开启选举,将其他slave切换成了master<br>这个时候,集群里就会有两个master,也就是所谓的脑裂<br>此时虽然某个slave被切换成了master,但是可能<strong>client还没来得及切换到新的master,还继续写向旧master的数据可能也丢失了</strong><br>因此旧master再次恢复的时候,会被作为一个slave挂到新的master上去,自己的数据会清空,重新从新的master复制数据</li></ul><h2 id="解决异步复制和脑裂导致的数据丢失"><a href="#解决异步复制和脑裂导致的数据丢失" class="headerlink" title="解决异步复制和脑裂导致的数据丢失"></a>解决异步复制和脑裂导致的数据丢失</h2><p>min-slaves-to-write 1<br>min-slaves-max-lag 10<br>要求至少有1个slave,数据复制和同步的延迟不能超过10秒<br>如果说一旦所有的slave,数据复制和同步的延迟都超过了10秒钟,那么这个时候,master就不会再接收任何请求了<br>上面两个配置可以减少异步复制和脑裂导致的数据丢失</p><h2 id="减少异步复制的数据丢失"><a href="#减少异步复制的数据丢失" class="headerlink" title="减少异步复制的数据丢失"></a>减少异步复制的数据丢失</h2><p>有了min-slaves-max-lag这个配置,就可以确保说,一旦slave复制数据和ack延时太长,就认为可能master宕机后损失的数据太多了,那么就拒绝写请求,这样可以把master宕机时由于部分数据未同步到slave导致的数据丢失降低的可控范围内</p><h2 id="减少脑裂的数据丢失"><a href="#减少脑裂的数据丢失" class="headerlink" title="减少脑裂的数据丢失"></a>减少脑裂的数据丢失</h2><p>如果一个master出现了脑裂,跟其他slave丢了连接,那么上面两个配置可以确保说,如果不能继续给指定数量的slave发送数据,而且slave超过10秒没有给自己ack消息,那么就直接拒绝客户端的写请求<br>这样脑裂后的旧master就不会接受client的新数据,也就避免了数据丢失<br>上面的配置就确保了,如果跟任何一个slave丢了连接,在10秒后发现没有slave给自己ack,那么就拒绝新的写请求<br>因此在脑裂场景下,最多就丢失10秒的数据</p>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>redis主从复制</title>
<link href="/2019/09/25/redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6/"/>
<url>/2019/09/25/redis%E4%B8%BB%E4%BB%8E%E5%A4%8D%E5%88%B6/</url>
<content type="html"><![CDATA[<h2 id="复制的完整流程"><a href="#复制的完整流程" class="headerlink" title="复制的完整流程"></a>复制的完整流程</h2><ol><li>slave node启动,仅仅保存master node的信息,包括master node的host和ip,但是复制流程没开始(master host和ip是从哪儿来的,redis.conf里面的slaveof配置的)</li><li>slave node内部有个定时任务,每秒检查是否有新的master node要连接和复制,如果发现,就跟master node建立socket网络连接</li><li>slave node发送ping命令给master node</li><li>口令认证,如果master设置了requirepass,那么salve node必须发送masterauth的口令过去进行认证</li><li>master node第一次执行全量复制,生成RDB文件,发到slave node磁盘,将所有数据发给slave node</li><li>master node后续持续将写命令,异步复制给slave node</li></ol><img src="/2019/09/25/redis主从复制/复制的完整的基本流程.png" title="复制的完整的基本流程"><h2 id="数据同步相关的核心机制"><a href="#数据同步相关的核心机制" class="headerlink" title="数据同步相关的核心机制"></a>数据同步相关的核心机制</h2><ol><li>master和slave都会维护一个offset<br>master会在自身不断累加offset,slave也会在自身不断累加offset<br>slave每秒都会上报自己的offset给master,同时master也会保存每个slave的offset<br>这个倒不是说特定就用在全量复制的,主要是master和slave都要知道各自的数据的offset,才能知道互相之间的数据不一致的情况</li><li>backlog<br>master node有一个backlog,默认是1MB大小<br>master node给slave node复制数据时,也会将数据在backlog中同步写一份<br>backlog主要是用来做全量复制中断候的增量复制的</li><li>master run id<br>info server,可以看到master run id<br>如果根据host+ip定位master node,是不靠谱的,如果master node重启或者数据出现了变化,那么slave node应该根据不同的run id区分,run id不同就做全量复制<br>如果需要不更改run id重启redis,可以使用redis-cli debug reload命令</li><li>psync<br>从节点使用psync从master node进行复制,psync runid offset<br>master node会根据自身的情况返回响应信息,可能是FULLRESYNC runid offset触发全量复制,可能是CONTINUE触发增量复制</li></ol><h2 id="全量复制"><a href="#全量复制" class="headerlink" title="全量复制"></a>全量复制</h2><ol><li>master执行bgsave,在本地生成一份rdb快照文件</li><li>master node将rdb快照文件发送给salve node,如果rdb复制时间超过60秒(repl-timeout),那么slave node就会认为复制失败,可以适当调节大这个参数</li><li>对于千兆网卡的机器,一般每秒传输100MB,6G文件,很可能超过60s</li><li>master node在生成rdb时,会将所有新的写命令缓存在内存中,在salve node保存了rdb之后,再将新的写命令复制给salve node</li><li>client-output-buffer-limit slave 256MB 64MB 60,如果在复制期间,内存缓冲区持续消耗超过64MB,或者一次性超过256MB,那么停止复制,复制失败</li><li>slave node接收到rdb之后,清空自己的旧数据,然后重新加载rdb到自己的内存中,同时基于旧的数据版本对外提供服务</li><li>如果slave node开启了AOF,那么会立即执行BGREWRITEAOF,重写AOF</li></ol><blockquote><p>rdb生成、rdb通过网络拷贝、slave旧数据的清理、slave aof rewrite,很耗费时间</p></blockquote><h2 id="增量复制"><a href="#增量复制" class="headerlink" title="增量复制"></a>增量复制</h2><ol><li>如果全量复制过程中,master-slave网络连接断掉,那么slave重新连接master时,会触发增量复制</li><li>master直接从自己的backlog中获取部分丢失的数据,发送给slave node,默认backlog就是1MB</li><li>msater就是根据slave发送的psync中的offset来从backlog中获取数据的</li></ol><h2 id="异步复制"><a href="#异步复制" class="headerlink" title="异步复制"></a>异步复制</h2><p>master每次接收到写命令之后,现在内部写入数据,然后异步发送给slave node,像客户端发送写命令一样</p><h2 id="heartbeat"><a href="#heartbeat" class="headerlink" title="heartbeat"></a>heartbeat</h2><p>主从节点互相都会发送heartbeat信息<br>master默认每隔10秒发送一次heartbeat,salve node每隔1秒发送一个heartbeat</p>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>redis主从架构</title>
<link href="/2019/09/25/redis%E4%B8%BB%E4%BB%8E%E6%9E%B6%E6%9E%84/"/>
<url>/2019/09/25/redis%E4%B8%BB%E4%BB%8E%E6%9E%B6%E6%9E%84/</url>
<content type="html"><![CDATA[<h2 id="单机redis瓶颈"><a href="#单机redis瓶颈" class="headerlink" title="单机redis瓶颈"></a>单机redis瓶颈</h2><p>单机的redis几乎不太可能说QPS超过10万+,除非一些特殊情况,比如你的机器性能特别好,配置特别高,物理机,维护做的特别好,而且你的整体的操作不是太复杂,单机在几万</p><h2 id="主从架构-读写分离"><a href="#主从架构-读写分离" class="headerlink" title="主从架构+读写分离"></a>主从架构+读写分离</h2><p>一般来说,对缓存,一般都是用来支撑读高并发的,写的请求是比较少的</p><h2 id="主从复制-replication"><a href="#主从复制-replication" class="headerlink" title="主从复制 replication"></a>主从复制 replication</h2><ul><li>redis采用异步方式复制数据到slave节点,不过redis 2.8开始,slave node会周期性地确认自己每次复制的数据量</li><li>一个master node是可以配置多个slave node的</li><li>slave node也可以连接其他的slave node</li><li>slave node做复制的时候,是不会block master node的正常工作的</li><li>slave node在做复制的时候,也不会block对自己的查询操作,它会用旧的数据集来提供服务; 但是复制完成的时候,需要删除旧数据集,加载新数据集,这个时候就会暂停对外服务了</li><li>slave node主要用来进行横向扩容,做读写分离,扩容的slave node可以提高读的吞吐量</li></ul><h2 id="master持久化对于主从架构的安全保障的意义"><a href="#master持久化对于主从架构的安全保障的意义" class="headerlink" title="master持久化对于主从架构的安全保障的意义"></a>master持久化对于主从架构的安全保障的意义</h2><p>如果采用了主从架构,那么建议必须开启master node的持久化!<br>不建议用slave node作为master node的数据热备,因为那样的话,如果你关掉master的持久化,可能在master宕机重启的时候数据是空的,然后可能一经过复制,salve node数据也丢了<br>master -> RDB和AOF都关闭了 -> 全部在内存中<br>master宕机,重启,是没有本地数据可以恢复的,然后就会直接认为自己IDE数据是空的<br>master就会将空的数据集同步到slave上去,所有slave的数据全部清,100%的数据丢失</p><h2 id="主从架构的核心原理"><a href="#主从架构的核心原理" class="headerlink" title="主从架构的核心原理"></a>主从架构的核心原理</h2><p>当启动一个slave node的时候,它会发送一个PSYNC命令给master node,如果这是slave node重新连接master node,那么master node仅仅会复制给slave部分缺少的数据(双方都会维护一个offset); 否则如果是slave node第一次连接master node,那么会触发一次full resynchronization(全量复制)<br>开始full resynchronization的时候,master会启动一个后台线程,开始生成一份RDB快照文件,同时还会将从客户端收到的所有写命令缓存在内存中。RDB文件生成完毕之后,master会将这个RDB发送给slave,slave会先写入本地磁盘,然后再从本地磁盘加载到内存中。然后master会将内存中缓存的写命令发送给slave,slave也会同步这些数据。<br>slave node如果跟master node有网络故障,断开了连接,会自动重连。master如果发现有多个slave node都来重新连接,仅仅会启动一个rdb save操作,用一份数据服务所有slave node。</p><h3 id="主从复制的断点续传"><a href="#主从复制的断点续传" class="headerlink" title="主从复制的断点续传"></a>主从复制的断点续传</h3><p>从redis 2.8开始,就支持主从复制的断点续传,如果主从复制过程中,网络连接断掉了,那么可以接着上次复制的地方,继续复制下去,而不是从头开始复制一份<br>master node会在内存中常见一个backlog,master和slave都会保存一个replica offset还有一个master id,offset就是保存在backlog中的。如果master和slave网络连接断掉了,slave会让master从上次的replica offset开始继续复制<br>但是如果没有找到对应的offset,那么就会执行一次resynchronization</p><h2 id="无磁盘化复制"><a href="#无磁盘化复制" class="headerlink" title="无磁盘化复制"></a>无磁盘化复制</h2><p>master在内存中直接创建rdb,然后发送给slave,不会在自己本地落地磁盘了<br>repl-diskless-sync<br>repl-diskless-sync-delay,等待一定时长再开始复制,因为要等更多slave重新连接过来</p><h2 id="过期key处理"><a href="#过期key处理" class="headerlink" title="过期key处理"></a>过期key处理</h2><p>slave不会过期key,只会等待master过期key。如果master过期了一个key,或者通过LRU淘汰了一个key,那么会模拟一条del命令发送给slave。</p>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>redis持久化</title>
<link href="/2019/09/25/redis%E6%8C%81%E4%B9%85%E5%8C%96/"/>
<url>/2019/09/25/redis%E6%8C%81%E4%B9%85%E5%8C%96/</url>
<content type="html"><![CDATA[<h2 id="redis持久化的意义"><a href="#redis持久化的意义" class="headerlink" title="redis持久化的意义"></a>redis持久化的意义</h2><p>在于故障恢复,如果没有持久化的话,redis遇到灾难性故障的时候,就会丢失所有的数据<br>如果通过持久化将数据搞一份儿在磁盘上去,然后定期比如说同步和备份到一些云存储服务上去,那么就可以<strong>保证数据不丢失全部</strong>,还是可以恢复一部分数据回来的</p><h2 id="AOF和RDB介绍"><a href="#AOF和RDB介绍" class="headerlink" title="AOF和RDB介绍"></a>AOF和RDB介绍</h2><img src="/2019/09/25/redis持久化/RDB和AOF的介绍.png" title="RDB和AOF的介绍"><p>RDB持久化机制,对redis中的数据执行周期性的持久化<br>AOF机制对每条写入命令作为日志,以append-only的模式写入一个日志文件中,在redis重启的时候,可以通过回放AOF日志中的写入指令来重新构建整个数据集<br>如果我们想要redis仅仅作为纯内存的缓存来用,那么可以禁止RDB和AOF所有的持久化机制<br>通过RDB或AOF,都可以将redis内存中的数据给持久化到磁盘上面来,然后可以将这些数据备份到别的地方去,比如说阿里云,云服务<br>如果redis挂了,服务器上的内存和磁盘上的数据都丢了,可以从云服务上拷贝回来之前的数据,放到指定的目录中,然后重新启动redis,redis就会自动根据持久化数据文件中的数据,去恢复内存中的数据,继续对外提供服务<br>如果同时使用RDB和AOF两种持久化机制,那么在redis重启的时候,会使用AOF来重新构建数据,因为AOF中的数据更加完整</p><h2 id="RDB持久化机制的优点"><a href="#RDB持久化机制的优点" class="headerlink" title="RDB持久化机制的优点"></a>RDB持久化机制的优点</h2><ol><li>RDB会生成多个数据文件,每个数据文件都代表了某一个时刻中redis的数据,这种多个数据文件的方式,非常适合做冷备,可以将这种完整的数据文件发送到一些远程的安全存储上去</li><li>RDB对redis对外提供的读写服务,影响非常小,可以让redis保持高性能,因为redis主进程只需要fork一个子进程,让子进程执行磁盘IO操作来进行RDB持久化即可</li><li>相对于AOF持久化机制来说,直接基于RDB数据文件来重启和恢复redis进程,更加快速</li></ol><h2 id="RDB持久化机制的缺点"><a href="#RDB持久化机制的缺点" class="headerlink" title="RDB持久化机制的缺点"></a>RDB持久化机制的缺点</h2><ol><li>如果想要在redis故障时,尽可能少的丢失数据,那么RDB没有AOF好。一般来说,RDB数据快照文件,都是每隔5分钟,或者更长时间生成一次,这个时候就得接受一旦redis进程宕机,那么会丢失最近5分钟的数据</li><li>RDB每次在fork子进程来执行RDB快照数据文件生成的时候,如果数据文件特别大,可能会导致对客户端提供的服务暂停数毫秒,或者甚至数秒</li></ol><h2 id="AOF持久化机制的优点"><a href="#AOF持久化机制的优点" class="headerlink" title="AOF持久化机制的优点"></a>AOF持久化机制的优点</h2><ol><li>AOF可以更好的保护数据不丢失,一般AOF会每隔1秒,通过一个后台线程执行一次fsync操作,最多丢失1秒钟的数据</li><li>AOF日志文件以append-only模式写入,所以没有任何磁盘寻址的开销,写入性能非常高,而且文件不容易破损,即使文件尾部破损,也很容易修复</li><li>AOF日志文件即使过大的时候,出现后台重写操作,也不会影响客户端的读写。因为在rewrite log的时候,会对其中的指导进行压缩,创建出一份需要恢复数据的最小日志出来。再创建新日志文件的时候,老的日志文件还是照常写入。当新的merge后的日志文件ready的时候,再交换新老日志文件即可。</li><li>AOF日志文件的命令通过非常可读的方式进行记录,这个特性非常适合做灾难性的误删除的紧急恢复。比如某人不小心用flushall命令清空了所有数据,只要这个时候后台rewrite还没有发生,那么就可以立即拷贝AOF文件,将最后一条flushall命令给删了,然后再将该AOF文件放回去,就可以通过恢复机制,自动恢复所有数据</li></ol><h2 id="AOF持久化机制的缺点"><a href="#AOF持久化机制的缺点" class="headerlink" title="AOF持久化机制的缺点"></a>AOF持久化机制的缺点</h2><ol><li>对于同一份数据来说,AOF日志文件通常比RDB数据快照文件更大</li><li>AOF开启后,支持的写QPS会比RDB支持的写QPS低,因为AOF一般会配置成每秒fsync一次日志文件,当然,每秒一次fsync,性能也还是很高的</li><li>通过AOF记录的日志,进行数据恢复的时候,没有恢复一模一样的数据出来。所以说,类似AOF这种较为复杂的基于命令日志/merge/回放的方式,比基于RDB每次持久化一份完整的数据快照文件的方式,更加脆弱一些,容易有bug。不过AOF就是为了避免rewrite过程导致的bug,因此每次rewrite并不是基于旧的指令日志进行merge的,而是基于当时内存中的数据进行指令的重新构建,这样健壮性会好很多。</li></ol><h2 id="RDB和AOF到底该如何选择"><a href="#RDB和AOF到底该如何选择" class="headerlink" title="RDB和AOF到底该如何选择"></a>RDB和AOF到底该如何选择</h2><p>综合使用AOF和RDB两种持久化机制,用AOF来保证数据不丢失,作为数据恢复的第一选择; 用RDB来做不同程度的冷备,在AOF文件都丢失或损坏不可用的时候,还可以使用RDB来进行快速的数据恢复<br>如果RDB在执行snapshotting操作,那么redis不会执行AOF rewrite; 如果redis再执行AOF rewrite,那么就不会执行RDB snapshotting<br>如果RDB在执行snapshotting,此时用户执行BGREWRITEAOF命令,那么等RDB快照生成之后,才会去执行AOF rewrite<br>同时有RDB snapshot文件和AOF日志文件,那么redis重启的时候,会优先使用AOF进行数据恢复,因为其中的日志更完整</p><h2 id="如何配置RDB持久化机制"><a href="#如何配置RDB持久化机制" class="headerlink" title="如何配置RDB持久化机制"></a>如何配置RDB持久化机制</h2><p>save 60 1000<br>每隔60s,如果有超过1000个key发生了变更,那么就生成一个新的dump.rdb文件,就是当前redis内存中完整的数据快照,这个操作也被称之为snapshotting,快照<br>也可以手动调用save或者bgsave命令,同步或异步执行rdb快照生成<br>save可以设置多个,就是多个snapshotting检查点,每到一个检查点,就会去check一下,是否有指定的key数量发生了变更,如果有,就生成一个新的dump.rdb文件</p><h2 id="RDB持久化机制的工作流程"><a href="#RDB持久化机制的工作流程" class="headerlink" title="RDB持久化机制的工作流程"></a>RDB持久化机制的工作流程</h2><ol><li>redis根据配置自己尝试去生成rdb快照文件</li><li>fork一个子进程出来</li><li>子进程尝试将数据dump到临时的rdb快照文件中</li><li><strong>完成rdb快照文件的生成之后,就替换之前的旧的快照文件</strong></li></ol><h2 id="AOF持久化的配置"><a href="#AOF持久化的配置" class="headerlink" title="AOF持久化的配置"></a>AOF持久化的配置</h2><p>AOF持久化,默认是关闭的,默认是打开RDB持久化<br>appendonly yes,可以打开AOF持久化机制,在生产环境里面,一般来说AOF都是要打开的,除非你说随便丢个几分钟的数据也无所谓<br>打开AOF持久化机制之后,redis每次接收到一条写命令,就会写入日志文件中,当然是先写入os cache的,然后每隔一定时间再fsync一下<br>而且即使AOF和RDB都开启了,redis重启的时候,也是优先通过AOF进行数据恢复的,因为aof数据比较完整<br>可以配置AOF的fsync策略,有三种策略可以选择,一种是每次写入一条数据就执行一次fsync; 一种是每隔一秒执行一次fsync; 一种是不主动执行fsync</p><ul><li><p>always: 每次写入一条数据,立即将这个数据对应的写日志fsync到磁盘上去,性能非常非常差,吞吐量很低; 确保说redis里的数据一条都不丢,那就只能这样了<br>mysql -> 内存策略,大量磁盘,QPS到多少,一两k。QPS,每秒钟的请求数量<br>redis -> 内存,磁盘持久化,QPS到多少,单机,一般来说,上万QPS没问题</p></li><li><p>everysec: 每秒将os cache中的数据fsync到磁盘,这个最常用的,生产环境一般都这么配置,性能很高,QPS还是可以上万的</p></li><li>no: 仅仅redis负责将数据写入os cache就撒手不管了,然后后面os自己会时不时有自己的策略将数据刷入磁盘,不可控了</li></ul><h2 id="AOF-rewrite"><a href="#AOF-rewrite" class="headerlink" title="AOF rewrite"></a>AOF rewrite</h2><p>edis中的数据其实有限的,很多数据可能会自动过期,可能会被用户删除,可能会被redis用缓存清除的算法清理掉<br>redis中的数据会不断淘汰掉旧的,就一部分常用的数据会被自动保留在redis内存中<br>所以可能很多之前的已经被清理掉的数据,对应的写日志还停留在AOF中,AOF日志文件就一个,会不断的膨胀,到很大很大<br>所以AOF会自动在后台每隔一定时间做rewrite操作,比如日志里已经存放了针对100w数据的写日志了; redis内存只剩下10万; 基于内存中当前的10万数据构建一套最新的日志,到AOF中; 覆盖之前的老日志; 确保AOF日志文件不会过大,保持跟redis内存数据量一致<br>在redis.conf中,可以配置rewrite策略<br>auto-aof-rewrite-percentage 100<br>auto-aof-rewrite-min-size 64mb<br>比如说上一次AOF rewrite之后,是128mb<br>然后就会接着128mb继续写AOF的日志,如果发现增长的比例,超过了之前的100%,256mb,就可能会去触发一次rewrite<br>但是此时还要去跟min-size,64mb去比较,256mb > 64mb,才会去触发rewrite</p><h2 id="AOF-rewrite机制的工作流程"><a href="#AOF-rewrite机制的工作流程" class="headerlink" title="AOF rewrite机制的工作流程"></a>AOF rewrite机制的工作流程</h2><ol><li>redis fork一个子进程</li><li>子进程基于当前内存中的数据,构建日志,开始往一个新的临时的AOF文件中写入日志</li><li>redis主进程,接收到client新的写操作之后,在内存中写入日志,同时新的日志也继续写入旧的AOF文件</li><li>子进程写完新的日志文件之后,redis主进程将内存中的新日志再次追加到新的AOF文件中<br>用新的日志文件替换掉旧的日志文件</li></ol><h2 id="AOF破损文件的修复"><a href="#AOF破损文件的修复" class="headerlink" title="AOF破损文件的修复"></a>AOF破损文件的修复</h2><p>如果redis在append数据到AOF文件时,机器宕机了,可能会导致AOF文件破损<br>用redis-check-aof –fix命令来修复破损的AOF文件,修复方式->删掉命令</p><h2 id="做数据恢复时"><a href="#做数据恢复时" class="headerlink" title="做数据恢复时"></a>做数据恢复时</h2><p>关闭redis,配置关闭AOF,拷贝RDB,重启redis,等到数据加载到内存,热修改启动AOF,这样AOF和RDB才会同步。如果不是热修改,而是关闭重启redis,改AOF配置,数据读不到AOF数据,内存为空,同步到RDB,让RDB也为空</p>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>java基础</title>
<link href="/2019/09/25/java%E5%9F%BA%E7%A1%80/"/>
<url>/2019/09/25/java%E5%9F%BA%E7%A1%80/</url>
<content type="html"><![CDATA[<h2 id="面向对象和面向过程的区别"><a href="#面向对象和面向过程的区别" class="headerlink" title="面向对象和面向过程的区别"></a>面向对象和面向过程的区别</h2><p>面向过程性能比面向对象高。 因为类调用时需要实例化,开销比较大,比较消耗资源,面向过程没有面向对象易维护、易复用、易扩展。</p><h2 id="Java-语言有哪些特点"><a href="#Java-语言有哪些特点" class="headerlink" title="Java 语言有哪些特点?"></a>Java 语言有哪些特点?</h2><p>简单易学;<br>面向对象(封装,继承,多态);<br>平台无关性( Java 虚拟机实现平台无关性);<br>可靠性;<br>安全性;<br>支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);<br>支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);<br>编译与解释并存;</p><h2 id="Java和C-的区别"><a href="#Java和C-的区别" class="headerlink" title="Java和C++的区别?"></a>Java和C++的区别?</h2><p>都是面向对象的语言,都支持封装、继承和多态Java 不提供指针来直接访问内存,程序内存更加安全Java 的类是单继承的,C++ 支持多重继承;虽然 Java 的类不可以多继承,但是接口可以多继承。Java 有自动内存管理机制,不需要程序员手动释放无用内存</p><h2 id="字符型常量和字符串常量的区别"><a href="#字符型常量和字符串常量的区别" class="headerlink" title="字符型常量和字符串常量的区别?"></a>字符型常量和字符串常量的区别?</h2><p>字符常量是单引号引起的一个字符; 字符串常量是双引号引起的若干个字符<br>字符常量相当于一个整型值( ASCII 值)可以参加表达式运算; 字符串常量代表一个地址值(该字符串在内存中存放位置)<br>字符常量只占2个字节; 字符串常量占若干个字节(至少一个字符结束标志) (注意: char在Java中占两个字节)</p><h2 id="Java-面向对象编程三大特性-封装-继承-多态"><a href="#Java-面向对象编程三大特性-封装-继承-多态" class="headerlink" title="Java 面向对象编程三大特性: 封装 继承 多态"></a>Java 面向对象编程三大特性: 封装 继承 多态</h2><ul><li>封装<br>封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。</li><li>继承<br>如果子类是带参构造函数,没调用父类的super,那么父类需要有一个无参构造函数,不然会报错。父类有带参构造函数,子类就得super他。不然就需要父类有无参构造函数</li><li>多态<br>所谓多态就是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。</li></ul><h2 id="String"><a href="#String" class="headerlink" title="String"></a>String</h2><p>value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。</p><ul><li>不可变的好处</li></ul><ol><li>可以缓存 hash 值<br>因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。</li><li>String Pool 的需要<br>如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。</li><li>安全性<br>String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。</li><li>线程安全<br>String 不可变性天生具备线程安全,可以在多个线程中安全地使用。</li></ol><h2 id="String-StringBuffer-和-StringBuilder-的区别是什么-String-为什么是不可变的"><a href="#String-StringBuffer-和-StringBuilder-的区别是什么-String-为什么是不可变的" class="headerlink" title="String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?"></a>String StringBuffer 和 StringBuilder 的区别是什么? String 为什么是不可变的?</h2><p>String 类中使用 final 关键字修饰字符数组来保存字符串,private final char value[],所以 String 对象是不可变的。而StringBuilder 与 StringBuffer 都继承自 AbstractStringBuilder 类,在 AbstractStringBuilder 中也是使用字符数组保存字符串char[]value 但是没有用 final 关键字修饰,所以这两种对象都是可变的。<br>StringBuffer 对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder 并没有对方法进行加同步锁,所以是非线程安全的。 <br>每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。StringBuffer 每次都会对 StringBuffer 对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder 相比使用 StringBuffer 仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。</p><ol><li>操作少量的数据: 适用String</li><li>单线程操作字符串缓冲区下操作大量数据: 适用StringBuilder</li><li>多线程操作字符串缓冲区下操作大量数据: 适用StringBuffer</li></ol><h2 id="String-Pool"><a href="#String-Pool" class="headerlink" title="String Pool"></a>String Pool</h2><p>字符串常量池(String Pool)保存着所有字符串字面量(literal strings),这些字面量在编译时期就确定。不仅如此,还可以使用 String 的 intern() 方法在运行过程中将字符串添加到 String Pool 中。<br>当一个字符串调用 intern() 方法时,如果 String Pool 中已经存在一个字符串和该字符串值相等(使用 equals() 方法进行确定),那么就会返回 String Pool 中字符串的引用;否则,就会在 String Pool 中添加一个新的字符串,并返回这个新字符串的引用。<br>在 Java 7 之前,String Pool 被放在运行时常量池中,它属于永久代。而在 Java 7,String Pool 被移到堆中。这是因为永久代的空间有限,在大量使用字符串的场景下会导致 OutOfMemoryError 错误。</p><h2 id="Java-不能隐式执行向下转型,因为这会使得精度降低。"><a href="#Java-不能隐式执行向下转型,因为这会使得精度降低。" class="headerlink" title="Java 不能隐式执行向下转型,因为这会使得精度降低。"></a>Java 不能隐式执行向下转型,因为这会使得精度降低。</h2><p>可以强转</p><h2 id="自动装箱与拆箱"><a href="#自动装箱与拆箱" class="headerlink" title="自动装箱与拆箱"></a>自动装箱与拆箱</h2><p>装箱:将基本类型用它们对应的引用类型包装起来;<br>拆箱:将包装类型转换为基本数据类型;<br>注意每种包装类型都会有缓存如Integer会缓存 -128~127</p><h2 id="数据类型"><a href="#数据类型" class="headerlink" title="数据类型"></a>数据类型</h2><ul><li>基本类型<br>byte/8<br>char/16<br>short/16<br>int/32<br>float/32<br>long/64<br>double/64<br>boolean/~</li><li>包装类型<br>基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。包装类不能被继承</li><li>缓存池<br>new Integer(123) 与 Integer.valueOf(123) 的区别在于:<br>new Integer(123) 每次都会新建一个对象;<br>Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。<br>valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。<br>编译器会在<strong>自动装箱过程调用 valueOf()</strong> 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。</li></ul><h2 id="接口和抽象类的区别是什么?"><a href="#接口和抽象类的区别是什么?" class="headerlink" title="接口和抽象类的区别是什么?"></a>接口和抽象类的区别是什么?</h2><ol><li>接口的方法默认是 public,所有方法在接口中不能有实现(Java 8 开始接口方法可以有默认实现),而抽象类可以有非抽象的方法。</li><li>接口中除了static、final变量,不能有其他变量,而抽象类中则不一定。</li><li>一个类可以实现多个接口,但只能实现一个抽象类。接口自己本身可以通过extends关键字扩展多个接口。</li><li><strong>接口方法默认修饰符是public</strong>,抽象方法可以有public、protected和default这些修饰符(抽象方法就是为了被重写所以不能使用private关键字修饰!)。</li><li>从设计层面来说,抽象是对类的抽象,是一种模板设计,而接口是对行为的抽象,是一种行为的规范。</li></ol><h2 id="成员变量与局部变量的区别有哪些?"><a href="#成员变量与局部变量的区别有哪些?" class="headerlink" title="成员变量与局部变量的区别有哪些?"></a>成员变量与局部变量的区别有哪些?</h2><ol><li>从语法形式上看:成员变量是属于类的,而局部变量是在方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。</li><li>从变量在内存中的存储方式来看:如果成员变量是使用static修饰的,那么这个成员变量是属于类的,如果没有使用static修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。</li><li>从变量在内存中的生存时间上看:成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。</li><li>成员变量如果没有被赋初值:则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。</li></ol><h2 id="静态方法和实例方法有何不同"><a href="#静态方法和实例方法有何不同" class="headerlink" title="静态方法和实例方法有何不同"></a>静态方法和实例方法有何不同</h2><ol><li>在外部调用静态方法时,可以使用”类名.方法名”的方式,也可以使用”对象名.方法名”的方式。而实例方法只有后面这种方式。也就是说,调用静态方法可以无需创建对象。</li><li>静态方法在访问本类的成员时,只允许访问静态成员(即静态成员变量和静态方法),而不允许访问实例成员变量和实例方法;实例方法则无此限制。</li></ol><h2 id="与-equals-重要"><a href="#与-equals-重要" class="headerlink" title="== 与 equals(重要)"></a>== 与 equals(重要)</h2><p>== : 它的作用是判断两个对象的地址是不是相等。即,判断两个对象是不是同一个对象(基本数据类型==比较的是值,引用数据类型==比较的是内存地址)。</p><p>equals() : 它的作用也是判断两个对象是否相等。但它一般有两种使用情况:<br>情况1:类没有覆盖 equals() 方法。则通过 equals() 比较该类的两个对象时,等价于通过“==”比较这两个对象。<br>情况2:类覆盖了 equals() 方法。一般,我们都覆盖 equals() 方法来比较两个对象的内容是否相等;若它们的内容相等,则返回 true (即,认为这两个对象相等)。</p><h2 id="hashCode-与-equals-重要"><a href="#hashCode-与-equals-重要" class="headerlink" title="hashCode 与 equals (重要)"></a>hashCode 与 equals (重要)</h2><p>hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode() 函数。</p><p>散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)</p><ol><li>如果两个对象相等,则hashcode一定也是相同的</li><li>两个对象相等,对两个对象分别调用equals方法都返回true</li><li>两个对象有相同的hashcode值,它们也不一定是相等的</li><li>因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖</li><li>hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)</li></ol><h2 id="Java中只有值传递"><a href="#Java中只有值传递" class="headerlink" title="Java中只有值传递"></a>Java中只有值传递</h2><h2 id="浅拷贝-深拷贝"><a href="#浅拷贝-深拷贝" class="headerlink" title="浅拷贝 深拷贝"></a>浅拷贝 深拷贝</h2><p>拷贝对象和原始对象的引用类型引用同一个对象。<br>拷贝对象和原始对象的引用类型引用不同对象。<br>根据覆写clone方法不同而不同</p><p>使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。</p><h2 id="静态内部类"><a href="#静态内部类" class="headerlink" title="静态内部类"></a>静态内部类</h2><p>非静态内部类依赖于外部类的实例,而静态内部类不需要。</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">public class OuterClass {</span><br><span class="line"> class InnerClass {</span><br><span class="line"> }</span><br><span class="line"> static class StaticInnerClass {</span><br><span class="line"> }</span><br><span class="line"> public static void main(String[] args) {</span><br><span class="line"> OuterClass outerClass = new OuterClass();</span><br><span class="line"> InnerClass innerClass = outerClass.new InnerClass();</span><br><span class="line"> StaticInnerClass staticInnerClass = new StaticInnerClass();</span><br><span class="line"> }</span><br><span class="line">}</span><br></pre></td></tr></table></figure><h2 id="初始化顺序"><a href="#初始化顺序" class="headerlink" title="初始化顺序"></a>初始化顺序</h2><p>父类(静态变量、静态语句块)<br>子类(静态变量、静态语句块)<br>父类(实例变量、普通语句块)<br>父类(构造函数)<br>子类(实例变量、普通语句块)<br>子类(构造函数)</p><h2 id="反射"><a href="#反射" class="headerlink" title="反射"></a>反射</h2><p>每个类都有一个 Class 对象,包含了与类有关的信息。当编译一个新类时,会产生一个同名的 .class 文件,该文件内容保存着 Class 对象。<br>类加载相当于 Class 对象的加载,类在第一次使用时才动态加载到 JVM 中。也可以使用 Class.forName(“com.mysql.jdbc.Driver”) 这种方式来控制类的加载,该方法会返回一个 Class 对象。<br>反射可以提供运行时的类信息,并且这个类可以在运行时才加载进来,甚至在编译时期该类的 .class 不存在也可以加载进来。<br>Class 和 java.lang.reflect 一起对反射提供了支持,java.lang.reflect 类库主要包含了以下三个类:</p><ol><li>Field :可以使用 get() 和 set() 方法读取和修改 Field 对象关联的字段;</li><li>Method :可以使用 invoke() 方法调用与 Method 对象关联的方法;</li><li>Constructor :可以用 Constructor 的 newInstance() 创建新的对象。</li></ol><ul><li>反射的优点:</li></ul><ol><li>可扩展性 :应用程序可以利用全限定名创建可扩展对象的实例,来使用来自外部的用户自定义类。</li><li>类浏览器和可视化开发环境 :一个类浏览器需要可以枚举类的成员。可视化开发环境(如 IDE)可以从利用反射中可用的类型信息中受益,以帮助程序员编写正确的代码。</li><li>调试器和测试工具 : 调试器需要能够检查一个类里的私有成员。测试工具可以利用反射来自动地调用类里定义的可被发现的 API 定义,以确保一组测试中有较高的代码覆盖率。</li></ol><ul><li>反射的缺点:<br>尽管反射非常强大,但也不能滥用。如果一个功能可以不用反射完成,那么最好就不用。在我们使用反射技术时,下面几条内容应该牢记于心。</li></ul><ol><li>性能开销 :反射涉及了动态类型的解析,所以 JVM 无法对这些代码进行优化。因此,反射操作的效率要比那些非反射操作低得多。我们应该避免在经常被执行的代码或对性能要求很高的程序中使用反射。</li><li>安全限制 :使用反射技术要求程序必须在一个没有安全限制的环境中运行。如果一个程序必须在有安全限制的环境中运行,如 Applet,那么这就是个问题了。</li><li>内部暴露 :由于反射允许代码执行一些在正常情况下不被允许的操作(比如访问私有的属性和方法),所以使用反射可能会导致意料之外的副作用,这可能导致代码功能失调并破坏可移植性。反射代码破坏了抽象性,因此当平台发生改变的时候,代码的行为就有可能也随着变化。</li></ol><h2 id="关于-final-关键字的一些总结"><a href="#关于-final-关键字的一些总结" class="headerlink" title="关于 final 关键字的一些总结"></a>关于 final 关键字的一些总结</h2><p>final关键字主要用在三个地方:变量、方法、类。</p><ol><li>对于一个final变量,如果是基本数据类型的变量,则其数值一旦在初始化之后便不能更改;如果是引用类型的变量,则在对其初始化之后便不能再让其指向另一个对象。</li><li>当用final修饰一个类时,表明这个类不能被继承。final类中的所有成员方法都会被隐式地指定为final方法。</li><li>使用final方法的原因有两个。第一个原因是把方法锁定,以防任何继承类修改它的含义;第二个原因是效率。在早期的Java实现版本中,会将final方法转为内嵌调用。但是如果方法过于庞大,可能看不到内嵌调用带来的任何性能提升(现在的Java版本已经不需要使用final方法进行这些优化了)。类中所有的private方法都隐式地指定为final。</li></ol>]]></content>
<tags>
<tag> 基础 </tag>
</tags>
</entry>
<entry>
<title>线程</title>
<link href="/2019/09/25/%E7%BA%BF%E7%A8%8B/"/>
<url>/2019/09/25/%E7%BA%BF%E7%A8%8B/</url>
<content type="html"><![CDATA[<h2 id="线程的生命周期"><a href="#线程的生命周期" class="headerlink" title="线程的生命周期"></a>线程的生命周期</h2><p>线程创建之后调用start()方法开始运行</p><p>当调用wait(),join(),LockSupport.lock()方法线程会进入到WAITING状态,而同样的wait(long timeout),sleep(long),join(long),LockSupport.parkNanos(),LockSupport.parkUtil()增加了超时等待的功能,也就是调用这些方法后线程会进入TIMED_WAITING状态,当超时等待时间到达后,线程会切换到Runable的状态</p><p>另外当WAITING和TIMED _WAITING状态时可以通过Object.notify(),Object.notifyAll()方法使线程转换到Runable状态。当线程出现资源竞争时,即等待获取锁的时候,线程会进入到BLOCKED阻塞状态,当线程获取锁时,线程进入到Runable状态。线程运行结束后,线程进入到TERMINATED状态,状态转换可以说是线程的生命周期。</p><p>当线程进入到synchronized方法或者synchronized代码块时,线程切换到的是BLOCKED状态,而使用java.util.concurrent.locks下lock进行加锁的时候线程切换的是WAITING或者TIMED_WAITING状态,因为lock会调用LockSupport的方法。</p><p>就绪和运行中统称为Runnable,原因:start之后,线程不一定运行,分配cpu时间片之前为就绪状态</p><img src="/2019/09/25/线程/adfb427d-3b21-40d7-a142-757f4ed73079.png" title="线程的生命周期"><h2 id="中断"><a href="#中断" class="headerlink" title="中断"></a>中断</h2><p>interrupted() boolean,测试当前线程是否被中断。中断标志会被清除<br>isinterrupted() boolean 测试线程对象是否被中断,中断标志不会被清除<br>interrupt() 中断该线程对象,中断标志位会清除,抛出InterruptedException</p><h2 id="join"><a href="#join" class="headerlink" title="join"></a>join</h2><p>while(isAlive()){<br>wait(0);<br>}<br>当前线程会阻塞等待在当前线程调用了join的线程</p><h2 id="sleep-vs-wait"><a href="#sleep-vs-wait" class="headerlink" title="sleep vs wait"></a>sleep vs wait</h2><ol><li>sleep()方法是Thread的静态方法,而wait是Object实例方法</li><li>wait()方法必须要在同步方法或者同步块中调用,也就是必须已经获得对象锁。而sleep()方法没有这个限制可以在任何地方种使用。另外,wait()方法会释放占有的对象锁,使得该线程进入等待池中,等待下一次获取资源。而sleep()方法只是会让出CPU并不会释放掉对象锁;</li><li>sleep()方法在休眠时间达到后如果再次获得CPU时间片就会继续执行,而wait()方法必须等待Object.notift/Object.notifyAll通知后,才会离开等待池,并且再次获得CPU时间片才会继续执行。</li></ol><h2 id="守护线程"><a href="#守护线程" class="headerlink" title="守护线程"></a>守护线程</h2><p>守护线程是一种特殊的线程,就和它的名字一样,它是系统的守护者,在后台默默地守护一些系统服务,比如垃圾回收线程,JIT线程就可以理解守护线程。与之对应的就是用户线程,用户线程就可以认为是系统的工作线程,它会完成整个系统的业务操作。用户线程完全结束后就意味着整个系统的业务任务全部结束了,因此系统就没有对象需要守护的了,守护线程自然而然就会退。<br>这里需要注意的是守护线程在退出的时候并不会执行finnaly块中的代码,所以将释放资源等操作不要放在finnaly块中执行,这种操作是不安全的,设置守护线程要先于start()方法</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>三大性质</title>
<link href="/2019/09/25/%E4%B8%89%E5%A4%A7%E6%80%A7%E8%B4%A8/"/>
<url>/2019/09/25/%E4%B8%89%E5%A4%A7%E6%80%A7%E8%B4%A8/</url>
<content type="html"><![CDATA[<h2 id="原子性"><a href="#原子性" class="headerlink" title="原子性"></a>原子性</h2><p>原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。<br>java内存模型中定义了8中操作都是原子的,不可再分的。<br>lock(锁定):作用于主内存中的变量,它把一个变量标识为一个线程独占的状态;<br>unlock(解锁):作用于主内存中的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定<br>read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便后面的load动作使用;<br>load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量值放入工作内存中的变量副本<br>use(使用):作用于工作内存中的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作;<br>assign(赋值):作用于工作内存中的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作;<br>store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送给主内存中以便随后的write操作使用;<br>write(操作):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。<br>volatile并不能保证原子性,synchronized满足原子性。<br>如果让volatile保证原子性,必须符合以下两条规则:<br>运算结果并不依赖于变量的当前值,或者能够确保只有一个线程修改变量的值;<br>变量不需要与其他的状态变量共同参与不变约束</p><h2 id="有序性"><a href="#有序性" class="headerlink" title="有序性"></a>有序性</h2><p>synchronized语义表示锁在同一时刻只能由一个线程进行获取,当锁被占用后,其他线程只能等待。因此,synchronized语义就要求线程在访问读写共享变量时只能“串行”执行,因此synchronized具有有序性。<br>instance = new Singleton();这条语句实际上包含了三个操作:1.分配对象的内存空间;2.初始化对象;3.设置instance指向刚分配的内存地址。存在重排序。volatile包含禁止指令重排序的语义,其具有有序性。</p><h2 id="可见性"><a href="#可见性" class="headerlink" title="可见性"></a>可见性</h2><p>可见性是指当一个线程修改了共享变量后,其他线程能够立即得知这个修改。通过之前对synchronzed内存语义进行了分析,当线程获取锁时会从主内存中获取共享变量的最新值,释放锁的时候会将共享变量同步到主内存中。从而,synchronized具有可见性。同样的在volatile分析中,会通过在指令中添加lock指令,以实现内存可见性。因此, volatile具有可见性</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>初识AbstractQueuedSynchronizer</title>
<link href="/2019/09/25/%E5%88%9D%E8%AF%86AbstractQueuedSynchronizer/"/>
<url>/2019/09/25/%E5%88%9D%E8%AF%86AbstractQueuedSynchronizer/</url>
<content type="html"><![CDATA[<h2 id="lock简介"><a href="#lock简介" class="headerlink" title="lock简介"></a>lock简介</h2><p>synchronized同步块执行完成或者遇到异常是锁会自动释放,而lock必须调用unlock()方法释放锁,因此在finally块中释放锁。失去了像synchronize关键字隐式加锁解锁的便捷性,但是却拥有了锁获取和释放的可操作性,可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。</p><h2 id="初识AQS"><a href="#初识AQS" class="headerlink" title="初识AQS"></a>初识AQS</h2><p>AbstractQueuedSynchronizer(简称同步器)</p><p>同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的<strong>子类必须重写AQS的几个protected修饰的用来改变同步状态的方法</strong>,其他方法主要是实现了排队和阻塞机制。<strong>状态的更新使用getState,setState以及compareAndSetState这三个方法</strong>。</p><p>子类被<strong>推荐定义为自定义同步组件的静态内部类</strong>,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,这样就可以方便的实现不同类型的同步组件。</p><p>同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者的关系:<strong>锁是面向使用者,它定义了使用者与锁交互的接口,隐藏了实现细节;同步器是面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态的管理,线程的排队,等待和唤醒等底层操作</strong>。锁和同步器很好的隔离了使用者和实现者所需关注的领域。</p><h2 id="AQS的模板方法设计模式"><a href="#AQS的模板方法设计模式" class="headerlink" title="AQS的模板方法设计模式"></a>AQS的模板方法设计模式</h2><p>AQS的设计是使用模板方法设计模式,它将一些方法开放给子类进行重写,而同步器给同步组件所提供模板方法又会重新调用被子类所重写的方法。举个例子,AQS中需要重写的方法tryAcquire:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">protected boolean tryAcquire(int arg) {</span><br><span class="line"> throw new UnsupportedOperationException();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>ReentrantLock中NonfairSync(继承AQS)会重写该方法为:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">protected final boolean tryAcquire(int acquires) {</span><br><span class="line"> return nonfairTryAcquire(acquires);</span><br><span class="line">}</span><br></pre></td></tr></table></figure><p>而AQS中的模板方法acquire():</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">public final void acquire(int arg) {</span><br><span class="line"> if (!tryAcquire(arg) &&</span><br><span class="line"> acquireQueued(addWaiter(Node.EXCLUSIVE), arg))</span><br><span class="line"> selfInterrupt();</span><br><span class="line">}</span><br></pre></td></tr></table></figure><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ReentrantLock.lock() -> sync.lock() -> NonfairSync.lock() -> AQS.acquire(1) -> NonfairSync.tryAcquire(1) -> Sync.nonfairTryAcquire(1)</span><br></pre></td></tr></table></figure><p>在同步组件的实现上主要是利用了AQS,而AQS“屏蔽”了同步状态的修改,线程排队等底层实现,通过AQS的模板方法可以很方便的给同步组件的实现者进行调用。而针对用户来说,只需要调用同步组件提供的方法来实现并发编程即可。同时在新建一个同步组件时需要把握的两个关键点是:</p><ol><li>实现同步组件时推荐定义继承AQS的静态内存类,并重写需要的protected修饰的方法;</li><li>同步组件语义的实现依赖于AQS的模板方法,而AQS模板方法又依赖于被AQS的子类所重写的方法。</li></ol><p><strong>同步组件(如ReentrantLock)实现者的角度:</strong></p><p>通过可重写的方法:<strong>独占式</strong>: tryAcquire()(独占式获取同步状态),tryRelease()(独占式释放同步状态);<strong>共享式</strong> :tryAcquireShared()(共享式获取同步状态),tryReleaseShared()(共享式释放同步状态);<strong>告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放</strong>。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。这句话比较抽象,举例来说,上面的Mutex例子中通过tryAcquire方法实现自己的同步语义,在该方法中如果当前同步状态为0(即该同步组件没被任何线程获取),当前线程可以获取同时将状态更改为1返回true,否则,该组件已经被线程占用返回false。很显然,该同步组件只能在同一时刻被线程占用,Mutex专注于获取释放的逻辑来实现自己想要表达的同步语义。</p><p><strong>AQS的角度</strong></p><p>而对AQS来说,只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>volatile</title>
<link href="/2019/09/25/volatile/"/>
<url>/2019/09/25/volatile/</url>
<content type="html"><![CDATA[<p>被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。</p><h2 id="lock前缀指令"><a href="#lock前缀指令" class="headerlink" title="lock前缀指令"></a>lock前缀指令</h2><p>在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令。</p><ol><li>将当前处理器缓存行的数据写回系统内存;</li><li>这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效<br>在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。</li></ol><h2 id="volatile的happens-before关系"><a href="#volatile的happens-before关系" class="headerlink" title="volatile的happens-before关系"></a>volatile的happens-before关系</h2><p>volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。</p><h2 id="volatile的内存语义"><a href="#volatile的内存语义" class="headerlink" title="volatile的内存语义"></a>volatile的内存语义</h2><p>当volatile变量写后,线程中本地内存中共享变量就会置为失效的状态,因此线程B再需要读取从主内存中去读取该变量的最新值。<br>我们都知道,为了性能优化,JMM在不改变正确语义的前提下,会允许编译器和处理器对指令序列进行重排序,那如果想阻止重排序要怎么办了?答案是可以添加内存屏障。</p><ol><li>在每个volatile写操作的前面插入一个StoreStore屏障;</li><li>在每个volatile写操作的后面插入一个StoreLoad屏障;</li><li>在每个volatile读操作的后面插入一个LoadLoad屏障;</li><li>在每个volatile读操作的后面插入一个LoadStore屏障。</li></ol><p>StoreStore屏障:禁止上面的普通写和下面的volatile写重排序;<br>StoreLoad屏障:防止上面的volatile写与下面可能有的volatile读/写重排序<br>LoadLoad屏障:禁止下面所有的普通读操作和上面的volatile读重排序<br>LoadStore屏障:禁止下面所有的普通写操作和上面的volatile读重排序</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>synchronized</title>
<link href="/2019/09/25/synchronized/"/>
<url>/2019/09/25/synchronized/</url>
<content type="html"><![CDATA[<h2 id="多线程-i"><a href="#多线程-i" class="headerlink" title="多线程 i++"></a>多线程 i++</h2><p>读i,给i+1,写i</p><h2 id="synchronized作用"><a href="#synchronized作用" class="headerlink" title="synchronized作用"></a>synchronized作用</h2><p>synchronized可以用在方法上也可以使用在代码块中,其中方法是实例方法和静态方法分别锁的是该类的实例对象和该类的对象。而使用在代码块中也可以分为三种,锁对象,锁this,锁类。这里的需要注意的是:如果锁的是类对象的话,尽管new多个实例对象,但他们仍然是属于同一个类依然会被锁住,即线程之间保证同步关系。</p><h2 id="对象锁(monitor)机制"><a href="#对象锁(monitor)机制" class="headerlink" title="对象锁(monitor)机制"></a>对象锁(monitor)机制</h2><p>javap -v XXX.class查看字节码文件<br>执行同步代码块后首先要先执行monitorenter指令,退出的时候monitorexit指令。通过分析之后可以看出,使用Synchronized进行同步,其关键就是必须要对对象的监视器monitor进行获取,当线程获取monitor后才能继续往下执行,否则就只能等待。而这个获取的过程是互斥的,即同一时刻只有一个线程能够获取到monitor。<br>synchronized先天具有重入性。每个对象拥有一个计数器,当线程获取该对象锁后,计数器就会加一,释放锁后就会将计数器减一。<br>如果没有获取到监视器的线程将会被阻塞在同步块和同步方法的入口处,进入到BLOCKED状态</p><h2 id="锁获取和锁释放的内存语义"><a href="#锁获取和锁释放的内存语义" class="headerlink" title="锁获取和锁释放的内存语义"></a>锁获取和锁释放的内存语义</h2><p>释放锁的时候会将值刷新到主内存中,其他线程获取锁时会强制从主内存中获取最新的值。</p><h2 id="synchronized优化"><a href="#synchronized优化" class="headerlink" title="synchronized优化"></a>synchronized优化</h2><p>在 Java 早期版本中,synchronized 属于重量级锁,效率低下,<strong>因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。如果要挂起或者唤醒一个线程,都需要操作系统帮忙完成,而操作系统实现线程之间的切换时需要从用户态转换到内核态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized 效率低的原因。</strong>庆幸的是在 Java 6 之后 Java 官方对从 JVM 层面对synchronized<br>较大优化,所以现在的 synchronized 锁效率也优化得很不错了。JDK1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。</p><p>使用锁时,线程获取锁是一种悲观锁策略,即假设每一次执行临界区代码都会产生冲突,所以当前线程获取到锁的时候同时也会阻塞其他线程获取该锁。而CAS操作(又称为无锁操作)是一种乐观锁策略,它假设所有线程访问共享资源的时候不会出现冲突,既然不会出现冲突自然而然就不会阻塞其他线程的操作。因此,线程就不会出现阻塞停顿的状态。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)又叫做比较交换来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。</p><h2 id="CAS"><a href="#CAS" class="headerlink" title="CAS"></a>CAS</h2><p>CAS比较交换的过程可以通俗的理解为CAS(V,O,N),包含三个值分别为:V 内存地址存放的实际值;O 预期的值(旧值);N 更新的新值。当V和O相同时,也就是说旧值和内存中实际的值相同表明该值没有被其他线程更改过,即该旧值O就是目前来说最新的值了,自然而然可以将新值N赋值给V。反之,V和O不相同,表明该值已经被其他线程改过了则该旧值O不是最新版本的值了,所以不能将新值N赋给V,返回V即可。当多个线程使用CAS操作一个变量是,只有一个线程会成功,并成功更新,其余会失败。失败的线程会重新尝试,当然也可以选择挂起线程<br>CAS的实现需要硬件指令集的支撑,在JDK1.5后虚拟机才可以使用处理器提供的CMPXCHG指令实现。<br>在J.U.C包中利用CAS实现类有很多,可以说是支撑起整个concurrency包的实现,在Lock实现中会有CAS改变state变量,在atomic包中的实现类也几乎都是用CAS实现</p><h2 id="CAS问题"><a href="#CAS问题" class="headerlink" title="CAS问题"></a>CAS问题</h2><p>ABA问题(加版本号解决)<br>自旋时间过长,使用CAS时非阻塞同步,也就是说不会将线程挂起,会自旋(无非就是一个死循环)进行下一次尝试,如果这里自旋时间过长对性能是很大的消耗。如果JVM能支持处理器提供的pause指令,那么在效率上会有一定的提升。<br>只能保证一个共享变量的原子操作,AtomicReference</p><h2 id="Java对象头"><a href="#Java对象头" class="headerlink" title="Java对象头"></a>Java对象头</h2><p>对象的锁怎么理解?无非就是类似对对象的一个标志,那么这个标志就是<strong>存放在Java对象的对象头。Java对象头里的Mark Word里默认的存放的对象的Hashcode,分代年龄和锁标记位。</strong>锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。</p><table><thead><tr><th>状态</th><th>存储内容</th><th>标志位</th></tr></thead><tbody><tr><td>无锁</td><td>对象的hashCode、对象分代年龄、是否是偏向锁(0)</td><td>01</td></tr><tr><td>轻量级</td><td>指向栈中锁记录的指针</td><td>00</td></tr><tr><td>重量级</td><td>指向互斥量(重量级锁)的指针</td><td>10</td></tr><tr><td>GC标记</td><td>(空)</td><td>11</td></tr><tr><td>偏向锁</td><td>偏向线程ID、偏向时间戳、对象分代年龄、是否是偏向锁(1)</td><td>01</td></tr></tbody></table><h2 id="偏向锁"><a href="#偏向锁" class="headerlink" title="偏向锁"></a>偏向锁</h2><p>当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程<br>偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。</p><p><strong>引入偏向锁的目的和引入轻量级锁的目的很像,他们都是为了没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。但是不同是:轻量级锁在无竞争的情况下使用 CAS 操作去代替使用互斥量。而偏向锁在无竞争的情况下会把整个同步都消除掉</strong>。</p><p>但是<strong>对于锁竞争比较激烈的场合,偏向锁就失效了</strong>,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。</p><h2 id="轻量级锁"><a href="#轻量级锁" class="headerlink" title="轻量级锁"></a>轻量级锁</h2><p>线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。<br>轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。<br>一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。</p><p><strong>轻量级锁不是为了代替重量级锁,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗,因为使用轻量级锁时,不需要申请互斥量。另外,轻量级锁的加锁和解锁都用到了CAS操作。</strong></p><p><strong>轻量级锁能够提升程序同步性能的依据是“对于绝大部分锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。如果没有竞争,轻量级锁使用CAS 操作避免了使用互斥操作的开销。但如果存在锁竞争,除了互斥量开销外,还会额外发生CAS操作,因此在有锁竞争的情况下,轻量级锁比传统的重量级锁更慢!如果锁竞争激烈,那么轻量级将很快膨胀为重量级锁!</strong></p><p>轻量级锁提升程序同步性能的依据是:对于绝大部分的锁,在整个同步周期内都是不存在竞争的(区别于偏向锁)。这是一个经验数据。如果没有竞争,轻量级锁使用CAS操作避免了使用互斥量的开销,但如果存在锁竞争,除了互斥量的开销外,还额外发生了CAS操作,因此在有竞争的情况下,轻量级锁比传统的重量级锁更慢。</p><h2 id="重量级锁"><a href="#重量级锁" class="headerlink" title="重量级锁"></a>重量级锁</h2><p>对象头锁记录存储操作系统互斥信号量指针 </p><h2 id="自旋锁和自适应自旋"><a href="#自旋锁和自适应自旋" class="headerlink" title="自旋锁和自适应自旋"></a>自旋锁和自适应自旋</h2><p>轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。</p><p><strong>互斥同步对性能最大的影响就是阻塞的实现</strong>,<strong>因为挂起线程/恢复线程的操作都需要转入内核态中完成(用户态转换到内核态会耗费时间)。</strong></p><p><strong>一般线程持有锁的时间都不是太长,所以仅仅为了这一点时间去挂起线程/恢复线程是得不偿失的。</strong> 所以,虚拟机的开发团队就这样去考虑:“我们能不能让后面来的请求获取锁的线程等待一会而不被挂起呢?看看持有锁的线程是否很快就会释放锁”。<strong>为了让一个线程等待,我们只需要让线程执行一个忙循环(自旋),这项技术就叫做自旋</strong>。</p><p>自旋锁在 JDK1.6 之前其实就已经引入了,不过是默认关闭的,需要通过<code>--XX:+UseSpinning</code>参数来开启。JDK1.6及1.6之后,就改为默认开启的了。需要注意的是:自旋等待不能完全替代阻塞,因为它还是要占用处理器时间。如果锁被占用的时间短,那么效果当然就很好了!反之,相反!自旋等待的时间必须要有限度。如果自旋超过了限定次数任然没有获得锁,就应该挂起线程。<strong>自旋次数的默认值是10次,用户可以修改–XX:PreBlockSpin来更改</strong>。</p><h2 id="锁消除"><a href="#锁消除" class="headerlink" title="锁消除"></a><strong>锁消除</strong></h2><p>锁消除理解起来很简单,它指的就是虚拟机即使编译器在运行时,如果检测到那些共享数据不可能存在竞争,那么就执行锁消除。锁消除可以节省毫无意义的请求锁的时间。</p><h2 id="锁粗化"><a href="#锁粗化" class="headerlink" title="锁粗化"></a><strong>锁粗化</strong></h2><p>原则上,我们再编写代码的时候,总是推荐将同步快的作用范围限制得尽量小——只在共享数据的实际作用域才进行同步,这样是为了使得需要同步的操作数量尽可能变小,如果存在锁竞争,那等待线程也能尽快拿到锁。</p><p>大部分情况下,上面的原则都是没有问题的,但是如果一系列的连续操作都对同一个对象反复加锁和解锁,那么会带来很多不必要的性能消耗。</p><h2 id="synchonized可重入锁的实现"><a href="#synchonized可重入锁的实现" class="headerlink" title="synchonized可重入锁的实现"></a>synchonized可重入锁的实现</h2><p>每个锁会关联一个线程持有者和一个计数器。当计数器为0时表示该锁没有被任何线程持有,那么任何线程都可能获得该锁而调用相应方法,当一个线程请求成功后,JVM会记下持有锁的线程,并将计数器计为1,此时其他线程请求该锁,则必须等待。而该持有锁的线程如果再次请求这个锁,就可以再次拿到这个锁,同时计数器会递增。当线程退出一个synchronize方法/块时,计数器会递减,如果计数器为0则释放该锁。</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>java内存模型</title>
<link href="/2019/09/25/java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/"/>
<url>/2019/09/25/java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B/</url>
<content type="html"><![CDATA[<h2 id="什么是线程安全问题?"><a href="#什么是线程安全问题?" class="headerlink" title="什么是线程安全问题?"></a>什么是线程安全问题?</h2><p>当多个线程访问同一个对象时,如果不用考虑这些线程在运行时环境下的调度和交替运行,也不需要进行额外的同步,或者在调用方进行任何其他的协调操作,调用这个对象的行为都可以获取正确的结果,那这个对象是线程安全的。</p><p>出现线程安全的问题一般是因为主内存和工作内存数据不一致性和重排序导致的</p><h2 id="内存模型抽象结构"><a href="#内存模型抽象结构" class="headerlink" title="内存模型抽象结构"></a>内存模型抽象结构</h2><p>java内存模型是共享内存的并发模型,线程之间主要通过读-写共享变量来完成隐式通信。所有实例域,静态域和数组元素都是放在堆内存中(所有线程均可访问到,是可以共享的),而局部变量,方法定义参数和异常处理器参数不会在线程间共享。共享数据会出现线程安全的问题,而非共享数据不会出现线程安全的问题。<br>CPU的处理速度和主存的读写速度不是一个量级的,为了平衡这种巨大的差距,每个CPU都会有缓存。因此,共享变量会先放在主存中,每个线程都有属于自己的工作内存,并且会把位于主存中的共享变量拷贝到自己的工作内存,之后的读写操作均使用位于工作内存的变量副本,并在某个时刻将工作内存的变量副本写回到主存中去。JMM就从抽象层次定义了这种方式,并且JMM决定了一个线程对共享变量的写入何时对其他线程是可见的。<br>线程A和线程B之间要完成通信的话,要经历如下两步:</p><ol><li>线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中;</li><li>线程B从主存中读取最新的共享变量<br>如果线程A更新后数据并没有及时写回到主存,而此时线程B读到的是过期的数据,这就出现了“脏读”现象。可以通过同步机制(控制不同线程间操作发生的相对顺序)来解决或者通过volatile关键字使得每次volatile变量都能够强制刷新到主存,从而对每个线程都是可见的。</li></ol><img src="/2019/09/25/java内存模型/942ca0d2-9d5c-45a4-89cb-5fd89b61913f.png" title="内存模型抽象结构"><h2 id="重排序"><a href="#重排序" class="headerlink" title="重排序"></a>重排序</h2><p>为了提高性能,编译器和处理器常常会对指令进行重排序。一般重排序可以分为如下三种:</p><ol><li>编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序;</li><li>指令级并行的重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序;</li><li>内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。<br>针对编译器重排序,JMM的编译器重排序规则会禁止一些特定类型的编译器重排序;针对处理器重排序,编译器在生成指令序列的时候会通过插入内存屏障指令来禁止某些特殊的处理器重排序。</li></ol><h2 id="as-if-serial"><a href="#as-if-serial" class="headerlink" title="as-if-serial"></a>as-if-serial</h2><p>as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提供并行度),(单线程)程序的执行结果不能被改变。编译器,runtime和处理器都必须遵守as-if-serial语义。</p><h2 id="happens-before规则"><a href="#happens-before规则" class="headerlink" title="happens-before规则"></a>happens-before规则</h2><p>JMM为程序员在上层提供了六条规则,这样我们就可以根据规则去推论跨线程的内存可见性问题,而不用再去理解底层重排序的规则。</p><ol><li>程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。</li><li>监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。</li><li>volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。</li><li>传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。</li><li>start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。</li><li>join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。</li><li>程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。</li><li>对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。</li></ol><h2 id="作用"><a href="#作用" class="headerlink" title="作用"></a>作用</h2><p>一个happens-before规则对应于一个或多个编译器和处理器重排序规则。对于Java程序员来说,happens-before规则简单易懂,它避免Java程序员为了理解JMM提供的内存可见性保证而去学习复杂的重排序规则以及这些规则的具体实现方法<br>JMM对编译器和处理器的束缚已经尽可能少。从上面的分析可以看出,JMM其实是在遵循一个基本原则:只要不改变程序的执行结果(指的是单线程程序和正确同步的多线程程序),编译器和处理器怎么优化都行。</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>ConcurrentHashMap</title>
<link href="/2019/09/25/ConcurrentHashMap/"/>
<url>/2019/09/25/ConcurrentHashMap/</url>
<content type="html"><![CDATA[<h2 id="对比:"><a href="#对比:" class="headerlink" title="对比:"></a>对比:</h2><p>因为hashmap并不是线程安全的,通常我们可以使用在java体系中古老的hashtable类,该类基本上所有的方法都采用synchronized进行线程安全的控制,可想而知,在高并发的情况下,每次只有一个线程能够获取对象监视器锁,这样的并发性能的确不令人满意。另外一种方式通过Collections的Map<K,V> synchronizedMap(Map<K,V> m)将hashmap包装成一个线程安全的map。实际上SynchronizedMap实现依然是采用synchronized独占式锁进行线程安全的并发控制的。</p><p>ConcurrentHashMap就是线程安全的map,其中利用了锁分段的思想提高了并发度。</p><p>1.6版本:segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。<br>1.8版本:舍弃了segment,并且大量使用了synchronized,以及CAS无锁操作以保证ConcurrentHashMap操作的线程安全性。</p><h2 id="字段:"><a href="#字段:" class="headerlink" title="字段:"></a>字段:</h2><ol><li>volatile Node<K,V>[] table,采用懒加载的方式,直到第一次插入数据的时候才会进行初始化操作,数组的大小总是为2的幂次方。Node结点val和next都是volatile修饰,保证内存可见性。</li><li>volatile Node<K,V>[] nextTable,扩容时使用,平时为null,只有在扩容的时候才为非null</li><li>volatile int sizeCtl,如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作;<br>当值为正数时:如果当前数组为null的话表示table在初始化过程中,sizeCtl表示为需要新建数组的长度;若已经初始化了,表示当前数据容器(table数组)可用容量也可以理解成临界值(插入节点数超过了该临界值就需要扩容),具体指为数组的长度n 乘以 加载因子loadFactor;<br>当值为0时,即数组长度为默认初始值。</li><li>sun.misc.Unsafe U,CAS操作依赖于现代处理器指令集,通过底层CMPXCHG指令实现。CAS(V,O,N)核心思想为:若当前变量实际值V与期望的旧值O相同,则表明该变量没被其他线程进行修改,因此可以安全的将新值N赋值给变量;若当前变量实际值V与期望的旧值O不相同,则表明该变量已经被其他线程做了处理,此时将新值N赋给变量操作就是不安全的,在进行重试。而在大量的同步组件和并发容器的实现中使用CAS是通过sun.misc.Unsafe类实现的,该类提供了一些可以直接操控内存和线程的底层操作,可以理解为java中的“指针”。(可以取设对应内存偏移量的值,保证可见性)</li><li>ForwardingNode 在扩容时才会出现的特殊节点,其key,value,hash全部为null。并拥有nextTable指针引用新的table数组。</li></ol><h2 id="CAS方法,都是Unsafe调用的,操作内存,底层是CMPXCHG指令"><a href="#CAS方法,都是Unsafe调用的,操作内存,底层是CMPXCHG指令" class="headerlink" title="CAS方法,都是Unsafe调用的,操作内存,底层是CMPXCHG指令"></a>CAS方法,都是Unsafe调用的,操作内存,底层是CMPXCHG指令</h2><h2 id="putVal:"><a href="#putVal:" class="headerlink" title="putVal:"></a>putVal:</h2><p>首先对于每一个放入的值,首先利用spread方法对key的hashcode进行一次hash计算,由此来确定这个值在 table中的位置;<br>如果当前table数组还未初始化,先将table数组进行初始化操作;<br>如果这个位置是null的,那么使用CAS操作直接放入;<br>如果这个位置存在结点,说明发生了hash碰撞,首先判断这个节点的类型。如果该节点fh==MOVED(代表forwardingNode,数组正在进行扩容)的话,说明正在进行扩容;<br>如果是链表节点(fh>0),则得到的结点就是hash值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到key相同的节点,则只需要覆盖该结点的value值即可。否则依次向后遍历,直到链表尾插入这个结点;<br>如果这个节点的类型是TreeBin的话,直接调用红黑树的插入方法进行插入新的节点;<br>插入完节点之后再次检查链表长度,如果长度大于8,就把这个链表转换成红黑树;<br>对当前容量大小进行检查,如果超过了临界值(实际大小*加载因子)就需要扩容。</p><h2 id="get:"><a href="#get:" class="headerlink" title="get:"></a>get:</h2><p>首先先看当前的hash桶数组节点即table[i]是否为查找的节点,若是则直接返回;若不是,则继续再看当前是不是树节点?通过看节点的hash值是否为小于0,如果小于0则为树节点或者forwardingNode。如果是树节点在红黑树中查找节点;如果不是树节点,forwardingNode找不到,那就只剩下为链表的形式的一种可能性了,就向后遍历查找节点,若查找到则返回节点的value即可,若没有找到就返回null。</p><h2 id="initTable:"><a href="#initTable:" class="headerlink" title="initTable:"></a>initTable:</h2><p>他是用sizeCtl来控制的,CAS将sizeCtl设置程-1,成功则初始化数组,否则自旋。并且会判断sizeCtl是否<0,是的话Thread.yield让出cpu。</p><h2 id="transfer方法:"><a href="#transfer方法:" class="headerlink" title="transfer方法:"></a>transfer方法:</h2><p>当ConcurrentHashMap容量不足的时候,需要对table进行扩容。这个方法的基本思想跟HashMap是很像的,但是由于它是支持并发扩容的,所以要复杂的多。原因是它支持多线程进行扩容操作,而并没有加锁。我想这样做的目的不仅仅是为了满足concurrent的要求,而是希望利用并发处理去减少扩容带来的时间影响。</p>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>运行时数据区域</title>
<link href="/2019/09/25/%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9F/"/>
<url>/2019/09/25/%E8%BF%90%E8%A1%8C%E6%97%B6%E6%95%B0%E6%8D%AE%E5%8C%BA%E5%9F%9F/</url>
<content type="html"><![CDATA[<h2 id="运行时数据区域"><a href="#运行时数据区域" class="headerlink" title="运行时数据区域"></a>运行时数据区域</h2><p>JVM载执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。其中程序计数器、Java虚拟机栈、本地方法栈为线程私有;Java堆、方法区位线程共享的内存区域。</p><img src="/2019/09/25/运行时数据区域/776c8d55.png" title="运行时数据区域"><h2 id="程序计数器(Program-Counter-Register)"><a href="#程序计数器(Program-Counter-Register)" class="headerlink" title="程序计数器(Program Counter Register)"></a>程序计数器(Program Counter Register)</h2><p>程序计数器(Program Counter Register)是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器。字节码解释器工作时就是通过改变计数器的值来选取下一条需要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。每条线程都有一个独立的程序计数器,各条线程之间的计数器互不影响,独立存储。这样设计使得在多线程环境下,线程切换后能恢复到正确的执行位置。程序计数器也是<strong>唯一</strong>一个在Java虚拟机规范中<strong>没有</strong>规定任何<strong>OutOfMemoryError</strong>情况的内存区域。</p><h2 id="Java虚拟机栈(Java-Virtual-Machine-Stacks)"><a href="#Java虚拟机栈(Java-Virtual-Machine-Stacks)" class="headerlink" title="Java虚拟机栈(Java Virtual Machine Stacks)"></a>Java虚拟机栈(Java Virtual Machine Stacks)</h2><p>Java虚拟机栈(Java Virtual Machine Stacks)描述的是Java<strong>方法执行</strong>的内存模型:每个方法在执行的同时都会创建一个栈帧(Stack Frame),栈帧中存储着局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法从调用直至执行完成的过程,会对应一个栈帧在虚拟机栈中入栈到出栈的过程。与程序计数器一样,Java虚拟机栈也是<strong>线程私有</strong>的。</p><blockquote><p>StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度,将会抛出此异常。<br>OutOfMemoryError:当可动态扩展的虚拟机栈在扩展时无法申请到足够的内存,就会抛出该异常。</p></blockquote><h3 id="局部变量表"><a href="#局部变量表" class="headerlink" title="局部变量表"></a>局部变量表</h3><p>存放了<strong>编译期可知</strong>的各种:</p><ul><li>基本数据类型(boolen、byte、char、short、int、 float、 long、double)</li><li>对象引用(reference类型,它不等于对象本身,可能是一个指向对象起始地址的指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)</li><li>returnAddress类型(指向了一条字节码指令的地址)</li></ul><p>其中64位长度的long和double类型的数据会占用2个局部变量空间(Slot),其余数据类型只占用1个。局部变量表所需的内存空间在<strong>编译期间完成分配</strong>,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。</p><h2 id="本地方法栈(Native-Method-Stack)"><a href="#本地方法栈(Native-Method-Stack)" class="headerlink" title="本地方法栈(Native Method Stack)"></a>本地方法栈(Native Method Stack)</h2><p>本地方法栈(Native Method Stack)与Java虚拟机栈作用很相似,它们的区别在于虚拟机栈为虚拟机执行Java方法(即字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务。</p><blockquote><p>本地方法栈会抛出StackOverflowError和OutOfMemoryError异常。</p></blockquote><h2 id="Java堆(Heap)"><a href="#Java堆(Heap)" class="headerlink" title="Java堆(Heap)"></a>Java堆(Heap)</h2><p>Java堆(Heap)是Java虚拟机所管理的内存中最大的一块,它被所有<strong>线程共享的</strong>,在虚拟机启动时创建。此内存区域唯一的目的是存放对象实例,<strong>几乎所有的对象实例都在这里分配内存</strong>,且每次分配的空间是<strong>不定长的</strong>。在Heap 中分配一定的内存来保存对象实例,<strong>实际上只是保存对象实例的属性值,属性的类型和对象本身的类型标记等</strong>,并不保存对象的方法(方法是指令,保存在Stack中),在Heap 中分配一定的内存保存对象实例和对象的序列化比较类似。<strong>对象实例在Heap 中分配好以后,需要在Stack中保存一个4字节的Heap 内存地址,用来定位该对象实例在Heap 中的位置,便于找到该对象实例。</strong></p><blockquote><p>Java虚拟机规范中描述道:所有的对象实例以及数组都要在堆上分配,但是随着<strong>JIT编译器的发展和逃逸分析技术逐渐成熟,栈上分配、标量替换优化技术</strong>将会导致一些微妙的变化发生,所有的对象都在堆上分配的定论也并不“绝对”了。</p></blockquote><img src="/2019/09/25/运行时数据区域/fe5079d3.png" title="Java堆"><ul><li>新生代(Young): 新生成的对象优先存放在新生代中,新生代对象朝生夕死,存活率很低。在新生代中,常规应用进行一次垃圾收集一般可以回收70% ~ 95% 的空间,回收效率很高。新生代又可细分为Eden空间、From Survivor空间、To Survivor空间,默认比例为8:1:1。</li><li>老年代(Tenured/Old):在新生代中经历了多次(具体看虚拟机配置的阀值)GC后仍然存活下来的对象会进入老年代中。老年代中的对象生命周期较长,存活率比较高,在老年代中进行GC的频率相对而言较低,而且回收的速度也比较慢。</li><li>永久代(Perm):永久代存储类信息、常量、静态变量、即时编译器编译后的代码等数据,对这一区域而言,Java虚拟机规范指出可以不进行垃圾收集,一般而言不会进行垃圾回收。</li></ul><p>其中<strong>新生代和老年代组成了Java堆的全部内存区域</strong>,而<strong>永久代不属于堆空间,它在JDK 1.8以前被Sun HotSpot虚拟机用作方法区的实现</strong></p><blockquote><p>-Xms:1G , 就是说初始堆大小为1G<br>-Xmx:2G , 就是说最大堆大小为2G<br>-Xmn:500M ,就是说新生代大小是500M(包括一个Eden和两个Survivor)<br>-XX:MaxPermSize:64M , 就是说设置持久代最大值为64M<br>-XX:+UseConcMarkSweepGC , 就是说使用使用CMS内存收集算法<br>-XX:SurvivorRatio=3 , 就是说Eden区与Survivor区的大小比值为3:1:1<br>Eden区的大小是指年轻代的大小,直接根据-Xmn:500M和-XX:SurvivorRatio=3可以直接计算得出:500M*(3/(3+1+1)) = 300M</p></blockquote><h2 id="方法区(Method-Area)"><a href="#方法区(Method-Area)" class="headerlink" title="方法区(Method Area)"></a>方法区(Method Area)</h2><p>方法区(Method Area)与Java堆一样,是各个线程共享的内存区域。Object Class Data(<strong>类定义数据</strong>)是存储在方法区的,此外,<strong>常量、静态变量、JIT编译后的代码</strong>也存储在方法区</p><blockquote><p>类加载过程第一步:加载<br>通过一个类的全限定名来获取定义此类的二进制字节流;<br>将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;方法区类的元数据<br>在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;</p></blockquote><h3 id="JDK-1-8以前的永久代(PermGen)"><a href="#JDK-1-8以前的永久代(PermGen)" class="headerlink" title="JDK 1.8以前的永久代(PermGen)"></a>JDK 1.8以前的永久代(PermGen)</h3><p><strong>对于JDK 1.8之前的版本,HotSpot虚拟机设计团队选择把GC分代收集扩展至方法区,即用永久代来实现方法区</strong>,这样HotSpot的垃圾收集器可以像管理Java堆一样管理这部分内存,能够省去专门为方法区编写内存管理代码的工作。对于其他的虚拟机(如Oracle JRockit、IBM J9等)来说是不存在永久代的概念的。<br><strong>如果运行时有大量的类产生,可能会导致方法区被填满,直至溢出。</strong>,常见的应用场景如:</p><ul><li>Spring和ORM框架使用CGLib操纵字节码对类进行增强,增强的类越多,就需要越大的方法区来保证动态生成的Class可以加载入内存。</li><li>大量JSP或动态产生JSP文件的应用(JSP第一次运行时需要编译为Java类)。</li><li>基于OSGi的应用(即使是同一个类文件,被不同的类加载器加载也会视为不同的类)。</li></ul><p>这些都会导致方法区溢出,报出<code>java.lang.OutOfMemoryError: PermGen space</code></p><h3 id="JDK-1-8的元空间(Metaspace)"><a href="#JDK-1-8的元空间(Metaspace)" class="headerlink" title="JDK 1.8的元空间(Metaspace)"></a>JDK 1.8的元空间(Metaspace)</h3><p>在JDK 1.8中,修改了方法区的实现,移除了永久代,选择使用本地化的内存空间(而不是JVM的内存空间)存放类的元数据,这个空间叫做元空间(Metaspace)。</p><p>做了这个改动以后,<code>java.lang.OutOfMemoryError: PermGen</code>的空间问题将不复存在,并且不再需要调整和监控这个内存空间。且虚拟机需要为方法区设计额外的GC策略:如果类元数据的空间占用达到参数<strong>“MaxMetaspaceSize”</strong>设置的值,将会触发对死亡对象和类加载器的垃圾回收。 为了限制垃圾回收的频率和延迟,适当的监控和调优<strong>元空间</strong>是非常有必要的。元空间过多的垃圾收集可能表示类、类加载器内存泄漏或对你的应用程序来说空间太小了。</p><p>元空间的内存管理由<strong>元空间虚拟机</strong>来完成。先前,对于类的元数据我们需要不同的垃圾回收器进行处理,现在只需要执行元空间虚拟机的C++代码即可完成。<strong>在元空间中,类和其元数据的生命周期</strong>和<strong>其对应的类加载器</strong>是相同的。话句话说,<strong>只要类加载器存活,其加载的类的元数据也是存活的</strong>,因而不会被回收掉。</p><p><strong>每一个类加载器的存储区域都称作一个元空间,所有的元空间合在一起就是我们一直说的元空间。</strong></p><p><strong>元空间虚拟机</strong>负责元空间的分配,其采用的形式为<strong>组块分配</strong>。组块的大小因类加载器的类型而异。在元空间虚拟机中存在一个<strong>全局的空闲组块列表</strong>。当一个类加载器需要组块时,它就会从这个全局的组块列表中获取并维持一个自己的组块列表。当一个类加载器不再存活,那么其持有的组块将会被释放,并返回给全局组块列表。类加载器持有的组块又会被分成多个块,每一个块存储一个单元的元信息。组块中的块<strong>是线性分配(指针碰撞分配形式)</strong>。组块分配自内存映射区域。这些全局的虚拟内存映射区域以链表形式连接,一旦某个虚拟内存映射区域清空,这部分内存就会返回给操作系统。</p><h3 id="运行时常量池(Runtime-Constant-Pool)"><a href="#运行时常量池(Runtime-Constant-Pool)" class="headerlink" title="运行时常量池(Runtime Constant Pool)"></a>运行时常量池(Runtime Constant Pool)</h3><p><strong>运行时常量池(Runtime Constant Pool)</strong>是方法区的一部分。<strong>Class文件</strong>中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是<strong>常量池(Constant Pool Table)</strong>,用于存放编译期生成的各种字面量和符号引用,<strong>这部分内容将在类加载后进入方法区的运行时常量池存放</strong>。除了保存<strong>Class文件中的描述符号引用</strong>外,还会把<strong>翻译出的直接引用</strong>也存储在运行时常量池中。运行时常量池相对于Class文件常量池的另外一个重要特征是具备<strong>动态性</strong>,Java语言并不要求常量一定只有编译器才能产生,也就是<strong>并非置入Class文件中的常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中</strong>,此特性被开发人员利用得比较多的便是String类的<code>intern()</code>方法。<strong>jdk1.8移入堆中</strong></p><h2 id="直接内存"><a href="#直接内存" class="headerlink" title="直接内存"></a>直接内存</h2><p><strong>直接内存(Direct Memory)</strong>并不是虚拟机<strong>运行时数据区</strong>的一部分,也不是Java虚拟机规范中定义的内存区域。但这部分内存也被频繁运用,而却可能导致<strong>OutOfMemoryError</strong>异常出现。</p><p>以<strong>NIO(New Input/Output)</strong>类为例,NIO引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,它可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。这样能避免在Java堆和Native堆中来回复制数据,在一些场景里显著提高性能。</p><p>既然是内存,还是会受到本机总内存(包括RAM以及SWAP区或分页文件)大小以及处理器寻址空间的限制。动态扩展时出现<strong>OutOfMemoryError</strong>异常。</p>]]></content>
<tags>
<tag> jvm </tag>
</tags>
</entry>
<entry>
<title>缓存和数据库双写不一致</title>
<link href="/2019/09/06/%E7%BC%93%E5%AD%98%E5%92%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8F%8C%E5%86%99%E4%B8%8D%E4%B8%80%E8%87%B4/"/>
<url>/2019/09/06/%E7%BC%93%E5%AD%98%E5%92%8C%E6%95%B0%E6%8D%AE%E5%BA%93%E5%8F%8C%E5%86%99%E4%B8%8D%E4%B8%80%E8%87%B4/</url>
<content type="html"><![CDATA[<h2 id="最初级的缓存不一致问题以及解决方案"><a href="#最初级的缓存不一致问题以及解决方案" class="headerlink" title="最初级的缓存不一致问题以及解决方案"></a>最初级的缓存不一致问题以及解决方案</h2><p>问题:先修改数据库,再删除缓存,如果删除缓存失败了,那么会导致数据库中是新数据,缓存中是旧数据,数据出现不一致<br>解决思路<br>先删除缓存,再修改数据库,如果删除缓存成功了,如果修改数据库失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致<br>因为读的时候缓存没有,则读数据库中旧数据,然后更新到缓存中</p><h2 id="比较复杂的数据不一致问题分析"><a href="#比较复杂的数据不一致问题分析" class="headerlink" title="比较复杂的数据不一致问题分析"></a>比较复杂的数据不一致问题分析</h2><p>数据发生了变更,先删除了缓存,然后要去修改数据库,此时还没修改<br>一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中<br>数据变更的程序完成了数据库的修改<br>完了,数据库和缓存中的数据不一样了。。。。<br>只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题<br>其实如果说你的并发量很低的话,特别是读并发很低,每天访问量就1万次,那么很少的情况下,会出现刚才描述的那种不一致的场景</p><h2 id="数据库与缓存更新与读取操作进行异步串行化"><a href="#数据库与缓存更新与读取操作进行异步串行化" class="headerlink" title="数据库与缓存更新与读取操作进行异步串行化"></a>数据库与缓存更新与读取操作进行异步串行化</h2><p>更新数据的时候,根据数据的唯一标识,将操作路由之后,发送到一个jvm内部的队列中<br>读取数据的时候,如果发现数据不在缓存中,那么将重新读取数据+更新缓存的操作,根据唯一标识路由之后,也发送同一个jvm内部的队列中<br>一个队列对应一个工作线程<br>每个工作线程串行拿到对应的操作,然后一条一条的执行<br>这样的话,一个数据变更的操作,先执行,删除缓存,然后再去更新数据库,但是还没完成更新<br>此时如果一个读请求过来,读到了空的缓存,那么可以先将缓存更新的请求发送到队列中,此时会在队列中积压,然后同步等待缓存更新完成<br>这里有一个优化点,一个队列中,其实多个更新缓存请求串在一起是没意义的,因此可以做过滤,如果发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操作进去了,直接等待前面的更新操作请求完成即可<br>待那个队列对应的工作线程完成了上一个操作的数据库的修改之后,才会去执行下一个操作,也就是缓存更新的操作,此时会从数据库中读取最新的值,然后写入缓存中<br>如果请求还在等待时间范围内,不断轮询发现可以取到值了,那么就直接返回; 如果请求等待的时间超过一定时长,那么这一次直接从数据库中读取当前的旧值</p><h2 id="该解决方案要注意的问题"><a href="#该解决方案要注意的问题" class="headerlink" title="该解决方案要注意的问题"></a>该解决方案要注意的问题</h2><ol><li>读请求长时阻塞<br>由于读请求进行了非常轻度的异步化,所以一定要注意读超时的问题,每个读请求必须在超时时间范围内返回<br>该解决方案,最大的风险点在于说,可能数据更新很频繁,导致队列中积压了大量更新操作在里面,然后读请求会发生大量的超时,最后导致大量的请求直接走数据库<br>务必通过一些模拟真实的测试,看看更新数据的频繁是怎样的<br>另外一点,因为一个队列中,可能会积压针对多个数据项的更新操作,因此需要根据自己的业务情况进行测试,可能需要部署多个服务,每个服务分摊一些数据的更新操作<br>如果一个内存队列里居然会挤压100个商品的库存修改操作,每隔库存修改操作要耗费10ms区完成,那么最后一个商品的读请求,可能等待10 * 100 = 1000ms = 1s后,才能得到数据<br>这个时候就导致读请求的长时阻塞<br>一定要做根据实际业务系统的运行情况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操作,可能会导致最后一个更新操作对应的读请求,会hang多少时间,如果读请求在200ms返回,如果你计算过后,哪怕是最繁忙的时候,积压10个更新操作,最多等待200ms,那还可以的<br>如果一个内存队列可能积压的更新操作特别多,那么你就要加机器,让每个机器上部署的服务实例处理更少的数据,那么每个内存队列中积压的更新操作就会越少<br>其实根据之前的项目经验,一般来说数据的写频率是很低的,因此实际上正常来说,在队列中积压的更新操作应该是很少的<br>针对读高并发,读缓存架构的项目,一般写请求相对读来说,是非常非常少的,每秒的QPS能到几百就不错了<br>一秒,500的写操作,5份,每200ms,就100个写操作<br>单机器,20个内存队列,每个内存队列,可能就积压5个写操作,每个写操作性能测试后,一般在20ms左右就完成<br>那么针对每个内存队列中的数据的读请求,也就最多hang一会儿,200ms以内肯定能返回了<br>写QPS扩大10倍,但是经过刚才的测算,就知道,单机支撑写QPS几百没问题,那么就扩容机器,扩容10倍的机器,10台机器,每个机器20个队列,200个队列<br>大部分的情况下,应该是这样的,大量的读请求过来,都是直接走缓存取到数据的<br>少量情况下,可能遇到读跟数据更新冲突的情况,如上所述,那么此时更新操作如果先入队列,之后可能会瞬间来了对这个数据大量的读请求,但是因为做了去重的优化,所以也就一个更新缓存的操作跟在它后面<br>等数据更新完了,读请求触发的缓存更新操作也完成,然后临时等待的读请求全部可以读到缓存中的数据</li><li>读请求并发量过高<br>这里还必须做好压力测试,确保恰巧碰上上述情况的时候,还有一个风险,就是突然间大量读请求会在几十毫秒的延时hang在服务上,看服务能不能抗的住,需要多少机器才能抗住最大的极限情况的峰值<br>但是因为并不是所有的数据都在同一时间更新,缓存也不会同一时间失效,所以每次可能也就是少数数据的缓存失效了,然后那些数据对应的读请求过来,并发量应该也不会特别大<br>按1:99的比例计算读和写的请求,每秒5万的读QPS,可能只有500次更新操作<br>如果一秒有500的写QPS,那么要测算好,可能写操作影响的数据有500条,这500条数据在缓存中失效后,可能导致多少读请求,发送读请求到库存服务来,要求更新缓存<br>一般来说,1:1,1:2,1:3,每秒钟有1000个读请求,会hang在库存服务上,每个读请求最多hang多少时间,200ms就会返回<br>在同一时间最多hang住的可能也就是单机200个读请求,同时hang住<br>单机hang200个读请求,还是ok的<br>1:20,每秒更新500条数据,这500秒数据对应的读请求,会有20 * 500 = 1万<br>1万个读请求全部hang在库存服务上,就死定了</li><li>多服务实例部署的请求路由<br>可能这个服务部署了多个实例,那么必须保证说,执行数据更新操作,以及执行缓存更新操作的请求,都通过nginx服务器路由到相同的服务实例上</li><li>热点商品的路由问题,导致请求的倾斜<br>万一某个商品的读写请求特别高,全部打到相同的机器的相同的队列里面去了,可能造成某台机器的压力过大<br>就是说,因为只有在商品数据更新的时候才会清空缓存,然后才会导致读写并发,所以更新频率不是太高的话,这个问题的影响并不是特别大<br>但是的确可能某些机器的负载会高一些</li></ol>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>多线程</title>
<link href="/2019/07/09/%E5%A4%9A%E7%BA%BF%E7%A8%8B/"/>
<url>/2019/07/09/%E5%A4%9A%E7%BA%BF%E7%A8%8B/</url>
<content type="html"><![CDATA[<h2 id="volatile定义和实现原理"><a href="#volatile定义和实现原理" class="headerlink" title="volatile定义和实现原理"></a>volatile定义和实现原理</h2><p>有volatile变量修饰的共享变量进行写操作时会多出一行汇编代码<br>以lock前缀,将当前处理器缓存行的数据写回到系统内存,这个写回内存的操作会使其他cpu缓存了该内存地址的数据无效。<br>每个处理器遵循缓存一致性协议,自动嗅探总线上传播的数据来检查自己缓存的数据是否过期</p><h2 id="synchronized的实现原理"><a href="#synchronized的实现原理" class="headerlink" title="synchronized的实现原理"></a>synchronized的实现原理</h2><p>jdk1.6为了减少获得锁和释放锁带来的性能消耗而引入偏向锁和轻量级锁<br>java每个对象都可以作为锁。表现为3种形式</p><ol><li>普通同步方法,锁是当前实例对象</li><li>静态同步方法,锁是当前类</li><li>同步方法块,锁是synchonized括号里的对象<br>jvm基于进入和退出Monitor对象来实现方法同步和代码块同步,方法块表现为monitorenter和monitorexit指令。方法是ACC_SYNCHRONIZED标志</li></ol><h2 id="java对象头"><a href="#java对象头" class="headerlink" title="java对象头"></a>java对象头</h2><p>对象头主要存储HashCode,锁标志位,分代年龄,对象类型的指针。<br>偏向锁位0+锁标志位01=无锁<br>偏向锁位1+锁标志位01=偏向锁<br>所标志位00=轻量级锁<br>锁标志位10=重量级锁</p><h2 id="偏向锁"><a href="#偏向锁" class="headerlink" title="偏向锁"></a>偏向锁</h2><p>why?大多数情况下锁不仅不存在多线程竞争,而且总是由相同的线程多次获得,为了使线程获取锁代价更低。<br>how?当线程访问同步块并获取锁时,在对象头和栈帧中的锁记录里存储锁偏向的线程id<br>what?线程访问同步块时只需简单测试一下Mark Word里是否偏向当前线程,是的话直接进入。如果测试失败,那么先判断偏向锁位是否为1(1表示是偏向锁),如果为0,则使用CAS竞争锁。<br>如果为1,尝试用CAS将对象头偏向锁指向当前线程。<br>当其他线程尝试竞争锁的时候,持有偏向锁的线程才会撤销。<br>偏向锁撤销后, 对象可能处于两种状态。</p><ol><li>不可偏向的无锁状态,之所以不允许偏向, 是因为已经检测到了多于一个线程的竞争, 升级到了轻量级锁的机制。</li><li>不可偏向的已锁 ( 轻量级锁) 状态<br>之所以会出现上述两种状态, 是因为偏向锁不存在解锁的操作, 只有撤销操作。 触发撤销操作时:原来已经获取了偏向锁的线程可能已经执行完了同步代码块, 使得对象处于 “闲置状态”,相当于原有的偏向锁已经过期无效了。此时该对象就应该被直接转换为不可偏向的无锁状态。<br>原来已经获取了偏向锁的线程也可能尚未执行完同步代码块, 偏向锁依旧有效, 此时对象就应该被转换为被轻量级加锁的状态。</li></ol><h2 id="轻量级锁"><a href="#轻量级锁" class="headerlink" title="轻量级锁"></a>轻量级锁</h2><p>线程在执行同步块前,jvm会现在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头的Mark Word复制到锁记录中,官方称为Displaced Mark Word,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。</p><h2 id="重量级锁"><a href="#重量级锁" class="headerlink" title="重量级锁"></a>重量级锁</h2><p>轻量级锁膨胀时, 被锁对象的 markword 会被通过 CAS 操作尝试更新为一个数据结构的指针, 这个数据结构中进一步包含了指向操作系统互斥量(mutex) 和 条件变量(condition variable) 的指针<br>依赖于操作系统的互斥量(mutex) 实现, 其具体的详细机制此处暂不展开, 日后可能补充。 此处暂时只需要了解该操作会导致进程从用户态与内核态之间的切换, 是一个开销较大的操作。</p><h2 id="CAS"><a href="#CAS" class="headerlink" title="CAS"></a>CAS</h2><p>CAS (Compare And Swap) 指令是一个CPU层级的原子性操作指令。 在 Intel 处理器中, 其汇编指令为 cmpxchg。<br>该指令概念上存在 3 个参数, 第一个参数【目标地址】, 第二个参数【值1】, 第三个参数【值2】, 指令会比较【目标地址存储的内容】和 【值1】 是否一致, 如果一致, 则将【值 2】 填写到【目标地址】<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">function cas(p , old , new ) returns bool {</span><br><span class="line"> if *p ≠ old { // *p 表示指针p所指向的内存地址</span><br><span class="line"> return false</span><br><span class="line"> }</span><br><span class="line"> *p ← new</span><br><span class="line"> return true</span><br><span class="line">}</span><br></pre></td></tr></table></figure></p><p>在 CPU 层面, CAS 指令的执行是有原子性语义保证的, 如果 CAS 操作放在应用层面来实现, 则需要我们自行保证其原子性。<br>CAS三大问题:</p><ol><li>ABA问题<br>why?A->B->A,其实第二次的A值虽然没变,但是他的语义却变了<br>解决:AtomicStampedReference,判断时附带版本号</li><li>循环时间长开销大<br>jvm如果支持处理器pause指令,那么效率会有一定提升。延迟流水线指令,避免循环时内存顺序冲突。</li><li>只能保证一个共享变量的原子操作<br>AtomicReference,多个变量放在一个对象里进行CAS操作</li></ol><h2 id="java内存模型"><a href="#java内存模型" class="headerlink" title="java内存模型"></a>java内存模型</h2><p>java并发采用的是共享内存模型,线程之间共享程序的公共状态,通过读写内存中的公共状态来进行隐式通信。同步是显示进行的,程序员必须显示指定某个方法或某段代码需要在线程间互斥执行。<br>所有实例域、静态域、数组元素都存储在堆内存中。线程共享。<br>局部变量、方法定义参数、异常处理器参数不在线程之间共享。<br>结构:<br>线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存存储了该线程以读写共享变量的副本。本地内存是一个抽象概念,涵盖缓存,写缓冲区,寄存器及其他的硬件和编译器优化。<br>jmm确保在不同的编译器和不同的处理器平台之上,禁止特定类型的编译器重排序(不是所有)和通过内存屏障来禁止特定的处理器重排序。提供一致的内存可见性保证。</p><h2 id="重排序"><a href="#重排序" class="headerlink" title="重排序"></a>重排序</h2><p>为了提高性能,编译器和处理器通常会对指令做重排序,分成3种类型</p><ol><li>编译器优化的重排序</li><li>指令级并行的重排序</li><li>内存系统的重排序<br>as-if-serial语义<br>不管怎么重排序(编译器和处理器为了提高并行度)(单线程)程序的执行结果不能被改变。处理器和编译器不会对存在数据依赖关系的存在做重排序。在多线程中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。</li></ol><h2 id="内存屏障类型"><a href="#内存屏障类型" class="headerlink" title="内存屏障类型"></a>内存屏障类型</h2><ol><li>LoadLoad:确保Load1数据的装载先于Load2及所有后续的装载</li><li>StoreStore:确保Store1数据对其他处理器可见(刷新到内存)先于Store2及后续的存储</li><li>LoadStore:确保Load1数据的装载先于Store2及后续存储指令刷新到内存</li><li>StoreLoad:确保Store1数据对其他处理器可见(刷新到内存)先于Load2及所有后续装载指令的装载。会使该屏障之前的内存访问指令完成后,才执行该屏障之后的指令。</li></ol><h2 id="happens-before"><a href="#happens-before" class="headerlink" title="happens-before"></a>happens-before</h2><p>JSR-133使用happens-before的概念来阐述操作之间的内存可见性,如果一个操作的执行的结果需要对另一个操作可见,那么这两个操作之间必须存在hb关系。hb仅仅要求前一个操作(执行的结果)对后一个操作可见,并不意味着前一个操作必须在后一个操作之前执行。</p><ol><li>程序顺序规则:一个线程中的每个操作,hb于该线程中的任意后续操作。</li><li>监视器锁规则:对一个锁的解锁,hb于随后对这个锁的加锁</li><li>volatile变量规则:对一个volatile域的写,hb于任意后续对这个volatile域的读</li><li>传递性:A hb B,B hb C,那么A hb C</li></ol>]]></content>
<tags>
<tag> 并发 </tag>
</tags>
</entry>
<entry>
<title>从零开始打造CentOs环境</title>
<link href="/2019/06/30/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E6%89%93%E9%80%A0CentOs%E7%8E%AF%E5%A2%83/"/>
<url>/2019/06/30/%E4%BB%8E%E9%9B%B6%E5%BC%80%E5%A7%8B%E6%89%93%E9%80%A0CentOs%E7%8E%AF%E5%A2%83/</url>
<content type="html"><![CDATA[<h2 id="目的"><a href="#目的" class="headerlink" title="目的"></a>目的</h2><p>为redis和java应用搭建一个可以运行的环境</p><h2 id="centos7"><a href="#centos7" class="headerlink" title="centos7"></a>centos7</h2><p>centos7的iso映像文件,下mini版本即可<br>根据VmWare默认安装就行。</p><h2 id="配置网卡"><a href="#配置网卡" class="headerlink" title="配置网卡"></a>配置网卡</h2><p>安装完后进入终端,输入<strong>ip addr</strong> 不同系统不同版本可能命令不一样,就是主要看他的HADDR,一般都是ens头的网卡。<br><img src="/2019/06/30/从零开始打造CentOs环境/网卡HWADDR.jpg" title="网卡HWADDR"><br><strong>cd /etc/sysconfig/network-script</strong> 进到这个目录找到ifcfg开头的与之前网卡名称相同的文件<br>注意,此时只有vi,还没装vim<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br></pre></td><td class="code"><pre><span class="line">HWADDR=00:0c:29:0b:8c:e6</span><br><span class="line">TYPE=Ethernet</span><br><span class="line">PROXY_METHOD=none</span><br><span class="line">BROWSER_ONLY=no</span><br><span class="line">IPADDR=192.168.229.7</span><br><span class="line">PREFIX=24</span><br><span class="line">GATEWAY=192.168.229.2 //在VmWare虚拟机网络编辑器NAT设置可以看到</span><br><span class="line">BOOTPROTO=static</span><br><span class="line">DEFROUTE=yes</span><br><span class="line">IPV4_FAILURE_FATAL=no</span><br><span class="line">IPV6INIT=yes</span><br><span class="line">IPV6_AUTOCONF=yes</span><br><span class="line">IPV6_DEFROUTE=yes</span><br><span class="line">IPV6_FAILURE_FATAL=no</span><br><span class="line">IPV6_ADDR_GEN_MODE=stable-privacy</span><br><span class="line">NAME=ens33</span><br><span class="line">DEVICE=ens33</span><br><span class="line">ONBOOT=yes</span><br><span class="line">NM_CONTROLLED=no</span><br></pre></td></tr></table></figure><br>根据此内容依次修改,wq保存后<strong>systemctl restart network</strong>重启一下网络,用secureCRT测试连接能不能连上,能连上表示设置成功<br>发现ping得通内网,ping不通外网,<strong>vi /etc/resolv.conf</strong><br>添加如下内容,就ping得通了<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">nameserver 114.114.114.114</span><br><span class="line">nameserver 8.8.8.8</span><br></pre></td></tr></table></figure><br>顺便把防火墙关了<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">systemctl status firewalld.service</span><br><span class="line">systemctl stop firewalld.service</span><br><span class="line">systemctl disable firewalld.service</span><br></pre></td></tr></table></figure><br>安装vim <strong>yum -y install vim</strong><br>安装gcc <strong>yum -y install gcc</strong><br>安装wget <strong>yum - y install wget</strong></p><h2 id="配置4台CentOS为ssh免密码互相通信"><a href="#配置4台CentOS为ssh免密码互相通信" class="headerlink" title="配置4台CentOS为ssh免密码互相通信"></a>配置4台CentOS为ssh免密码互相通信</h2><p>(1)首先在三台机器上配置对本机的ssh免密码登录<br>ssh-keygen -t rsa<br>生成本机的公钥,过程中不断敲回车即可,ssh-keygen命令默认会将公钥放在/root/.ssh目录下<br>cd /root/.ssh<br>cp id_rsa.pub authorized_keys<br>将公钥复制为authorized_keys文件,此时使用ssh连接本机就不需要输入密码了<br>(2)接着配置三台机器互相之间的ssh免密码登录<br>使用ssh-copy-id -i hostname命令将本机的公钥拷贝到指定机器的authorized_keys文件中</p><h2 id="安装jdk"><a href="#安装jdk" class="headerlink" title="安装jdk"></a>安装jdk</h2><p>解压再配环境<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">vi /etc/profile</span><br><span class="line">添加如下</span><br><span class="line">export JAVA_HOME=/data/program/software/java8</span><br><span class="line">export JRE_HOME=/data/program/software/java8/jre</span><br><span class="line">export CLASSPATH=.:$CLASSPATH:$JAVA_HOME/lib:$JRE_HOME/lib</span><br><span class="line">export PATH=$PATH:$JAVA_HOME/bin:$JRE_HOME/bin</span><br></pre></td></tr></table></figure></p><h2 id="安装perl(给java-nginx-lua)提供环境"><a href="#安装perl(给java-nginx-lua)提供环境" class="headerlink" title="安装perl(给java+nginx+lua)提供环境"></a>安装perl(给java+nginx+lua)提供环境</h2><p>编译过程比较久<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">wget http://www.cpan.org/src/5.0/perl-5.16.1.tar.gz</span><br><span class="line">tar -xzf perl-5.16.1.tar.gz</span><br><span class="line">cd perl-5.16.1</span><br><span class="line">./Configure -des -Dprefix=/usr/local/perl</span><br><span class="line">make && make test && make install</span><br><span class="line">perl -v</span><br></pre></td></tr></table></figure></p><h2 id="安装redis单机"><a href="#安装redis单机" class="headerlink" title="安装redis单机"></a>安装redis单机</h2><p>安装包和程序都安装在根目录/data下<br>官网下载redis<br>下载tcl wget <a href="http://downloads.sourceforge.net/tcl/tcl8.6.9-src.tar.gz" target="_blank" rel="noopener">http://downloads.sourceforge.net/tcl/tcl8.6.9-src.tar.gz</a> 用来给redis make test</p><ol><li>安装tcl<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">tar -xzvf tcl8.6.1-src.tar.gz</span><br><span class="line">cd /data/tcl8.6.1/unix/</span><br><span class="line">./configure </span><br><span class="line">make && make install</span><br></pre></td></tr></table></figure></li><li>安装redis<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">tar -xzvf redis.tar.gz</span><br><span class="line">cd redis</span><br><span class="line">make && make test && make install</span><br></pre></td></tr></table></figure><img src="/2019/06/30/从零开始打造CentOs环境/redis安装.jpg" title="redis安装"></li></ol><h2 id="redis的生产环境启动方案"><a href="#redis的生产环境启动方案" class="headerlink" title="redis的生产环境启动方案"></a>redis的生产环境启动方案</h2><p>如果一般的学习课程,你就随便用redis-server启动一下redis,做一些实验,这样的话,没什么意义<br>要把redis作为一个系统的daemon进程去运行的,每次系统启动,redis进程一起启动<br>(1)redis utils目录下,有个redis_init_script脚本<br>(2)将redis_init_script脚本拷贝到linux的/etc/init.d目录中,将redis_init_script重命名为redis_6379,6379是我们希望这个redis实例监听的端口号<br>(3)修改redis_6379脚本的第6行的REDISPORT,设置为相同的端口号(默认就是6379)<br>(4)创建两个目录:/etc/redis(存放redis的配置文件),/var/redis/6379(存放redis的持久化文件)<br>(5)修改redis配置文件(默认在根目录下,redis.conf),拷贝到/etc/redis目录中,修改名称为6379.conf<br>(6)修改redis.conf中的部分配置为生产环境<br>daemonize yes 让redis以daemon进程运行<br>pidfile /var/run/redis_6379.pid 设置redis的pid文件位置<br>port 6379 设置redis的监听端口号<br>dir /var/redis/6379 设置持久化文件的存储位置<br>(7)启动redis,执行cd /etc/init.d, chmod 777 redis_6379,./redis_6379 start<br>(8)确认redis进程是否启动,ps -ef | grep redis<br>(9)让redis跟随系统启动自动启动<br>在redis_6379脚本中,最上面,加入两行注释<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># chkconfig: 2345 90 10</span><br><span class="line"># description: Redis is a persistent key-value database</span><br></pre></td></tr></table></figure><br>chkconfig redis_6379 on</p><h2 id="springboot-jpa-redis-注意事项"><a href="#springboot-jpa-redis-注意事项" class="headerlink" title="springboot jpa-redis 注意事项"></a>springboot jpa-redis 注意事项</h2><ol><li>把reids.conf 里bind 127.0.0.1注释掉,不然无法连接</li><li>把redis保护模式protected-mode yes改成no,不然无法连接</li><li>改一下redisTemplate的序列化,默认jdk序列化会带一串二进制<figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">@Bean</span><br><span class="line"> public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {</span><br><span class="line"> StringRedisTemplate template = new StringRedisTemplate(factory);</span><br><span class="line"> //定义key序列化方式</span><br><span class="line"> //RedisSerializer<String> redisSerializer = new StringRedisSerializer();//Long类型会出现异常信息;需要我们上面的自定义key生成策略,一般没必要</span><br><span class="line"> //定义value的序列化方式</span><br><span class="line"> Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);</span><br><span class="line"> ObjectMapper om = new ObjectMapper();</span><br><span class="line"> om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);</span><br><span class="line"> om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);</span><br><span class="line"> jackson2JsonRedisSerializer.setObjectMapper(om);</span><br><span class="line"></span><br><span class="line"> // template.setKeySerializer(redisSerializer);</span><br><span class="line"> template.setValueSerializer(jackson2JsonRedisSerializer);</span><br><span class="line"> template.setHashValueSerializer(jackson2JsonRedisSerializer);</span><br><span class="line"> template.afterPropertiesSet();</span><br><span class="line"> return template;</span><br><span class="line"> }</span><br></pre></td></tr></table></figure></li></ol>]]></content>
<tags>
<tag> linux </tag>
</tags>
</entry>
<entry>
<title>FastDFS搭建</title>
<link href="/2019/05/20/FastDFS%E6%90%AD%E5%BB%BA/"/>
<url>/2019/05/20/FastDFS%E6%90%AD%E5%BB%BA/</url>
<content type="html"><![CDATA[<h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><p>FastDFS 是一个开源的高性能分布式文件系统(DFS)。 它的主要功能包括:文件存储,文件同步和文件访问,以及高容量和负载平衡。主要解决了海量数据存储问题,特别适合以中小文件(建议范围:4KB < file_size <500MB)为载体的在线服务。</p><ul><li>Tracker Server:跟踪服务器,主要做调度工作,起到均衡的作用;负责管理所有的 storage server和 group,每个 storage 在启动后会连接 Tracker,告知自己所属 group 等信息,并保持周期性心跳。</li><li>Storage Server:存储服务器,主要提供容量和备份服务;以 group 为单位,每个 group 内可以有多台 storage server,数据互为备份。</li><li>Client:客户端,上传下载数据的服务器,也就是我们自己的项目所部署在的服务器。</li></ul><img src="/2019/05/20/FastDFS搭建/FastDFS结构.png" title="FastDFS结构"><h2 id="开始安装"><a href="#开始安装" class="headerlink" title="开始安装"></a>开始安装</h2><p>操作环境:CentOS7 X64,以下操作都是单机环境。<br>我把所有的安装包下载到/data/program/下,解压到当前目录。<br>先做一件事,修改hosts,将文件服务器的ip与域名映射(单机TrackerServer环境),因为后面很多配置里面都需要去配置服务器地址,ip变了,就只需要修改hosts即可。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># vim /etc/hosts</span><br><span class="line">增加如下一行,这是我的IP</span><br><span class="line">192.168.229.100 file.jie.com</span><br><span class="line">如果要本机访问虚拟机,在C:\Windows\System32\drivers\etc\hosts中同样增加一行</span><br></pre></td></tr></table></figure></p><h3 id="下载安装-libfastcommon"><a href="#下载安装-libfastcommon" class="headerlink" title="下载安装 libfastcommon"></a>下载安装 libfastcommon</h3><p>libfastcommon是从 FastDFS 和 FastDHT 中提取出来的公共 C 函数库,基础环境,安装即可 。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line">下载</span><br><span class="line"># wget https://github.com/happyfish100/libfastcommon/archive/V1.0.7.tar.gz</span><br><span class="line">解压</span><br><span class="line"># tar -zxvf V1.0.7.tar.gz</span><br><span class="line"># cd libfastcommon-1.0.7 </span><br><span class="line">安装,记得装gcc yum install gcc</span><br><span class="line"># ./make.sh</span><br><span class="line"># ./make.sh install</span><br><span class="line">libfastcommon.so 安装到了/usr/lib64/libfastcommon.so,但是FastDFS主程序设置的lib目录是/usr/local/lib,所以需要创建软链接。</span><br><span class="line"># ln -s /usr/lib64/libfastcommon.so /usr/local/lib/libfastcommon.so</span><br><span class="line"># ln -s /usr/lib64/libfastcommon.so /usr/lib/libfastcommon.so</span><br><span class="line"># ln -s /usr/lib64/libfdfsclient.so /usr/local/lib/libfdfsclient.so</span><br><span class="line"># ln -s /usr/lib64/libfdfsclient.so /usr/lib/libfdfsclient.so </span><br></pre></td></tr></table></figure></p><h3 id="下载安装FastDFS"><a href="#下载安装FastDFS" class="headerlink" title="下载安装FastDFS"></a>下载安装FastDFS</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br></pre></td><td class="code"><pre><span class="line">下载FastDFS</span><br><span class="line"># wget https://github.com/happyfish100/fastdfs/archive/V5.05.tar.gz</span><br><span class="line">解压</span><br><span class="line"># tar -zxvf V5.05.tar.gz</span><br><span class="line"># cd fastdfs-5.05</span><br><span class="line">编译、安装</span><br><span class="line"># ./make.sh</span><br><span class="line"># ./make.sh install</span><br><span class="line">默认安装方式安装后的相应文件与目录</span><br><span class="line">A、服务脚本:</span><br><span class="line">/etc/init.d/fdfs_storaged</span><br><span class="line">/etc/init.d/fdfs_tracker</span><br><span class="line">B、配置文件(这三个是作者给的样例配置文件):</span><br><span class="line">/etc/fdfs/client.conf.sample</span><br><span class="line">/etc/fdfs/storage.conf.sample</span><br><span class="line">/etc/fdfs/tracker.conf.sample</span><br><span class="line">C、命令工具在 /usr/bin/ 目录下:</span><br><span class="line">fdfs_appender_test</span><br><span class="line">fdfs_appender_test1</span><br><span class="line">fdfs_append_file</span><br><span class="line">fdfs_crc32</span><br><span class="line">fdfs_delete_file</span><br><span class="line">fdfs_download_file</span><br><span class="line">fdfs_file_info</span><br><span class="line">fdfs_monitor</span><br><span class="line">fdfs_storaged</span><br><span class="line">fdfs_test</span><br><span class="line">fdfs_test1</span><br><span class="line">fdfs_trackerd</span><br><span class="line">fdfs_upload_appender</span><br><span class="line">fdfs_upload_file</span><br><span class="line">stop.sh</span><br><span class="line">restart.sh </span><br><span class="line">FastDFS 服务脚本设置的 bin 目录是 /usr/local/bin, 但实际命令安装在 /usr/bin/下</span><br><span class="line">建立 /usr/bin 到 /usr/local/bin 的软链接</span><br><span class="line"># ln -s /usr/bin/fdfs_trackerd /usr/local/bin</span><br><span class="line"># ln -s /usr/bin/fdfs_storaged /usr/local/bin</span><br><span class="line"># ln -s /usr/bin/stop.sh /usr/local/bin</span><br><span class="line"># ln -s /usr/bin/restart.sh /usr/local/bin</span><br></pre></td></tr></table></figure><h3 id="配置FastDFS跟踪器-Tracker"><a href="#配置FastDFS跟踪器-Tracker" class="headerlink" title="配置FastDFS跟踪器(Tracker)"></a>配置FastDFS跟踪器(Tracker)</h3><p>进入 /etc/fdfs,复制 FastDFS 跟踪器样例配置文件 tracker.conf.sample,并重命名为 tracker.conf。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"># cd /etc/fdfs</span><br><span class="line"># cp tracker.conf.sample tracker.conf</span><br><span class="line"># vim tracker.conf</span><br><span class="line">修改内容如下</span><br><span class="line"># Tracker 数据和日志目录地址(根目录必须存在,子目录会自动创建)</span><br><span class="line">base_path=/data/fastdfs/tracker</span><br><span class="line"></span><br><span class="line"># HTTP 服务端口</span><br><span class="line">http.server_port=80</span><br><span class="line"></span><br><span class="line">另外创建tracker基础数据目录,即base_path对应的目录</span><br><span class="line"># mkdir -p /data/fastdfs/tracker</span><br></pre></td></tr></table></figure><br> 防火墙中打开跟踪端口(默认的22122) CentOS7把iptables替换为firewall,而我又把防火墙直接关了,此步骤应该可以跳过<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"># vim /etc/sysconfig/iptables</span><br><span class="line"></span><br><span class="line">添加如下端口行:</span><br><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 22122 -j ACCEPT</span><br><span class="line"></span><br><span class="line">重启防火墙:</span><br><span class="line"># service iptables restart</span><br></pre></td></tr></table></figure><br>centos7.0 没有netstat 和 ifconfig命令问题<br>yum install net-tools 就OK了<br>启动Tracker<br>初次成功启动,会在 /data/fdfsdfs/tracker/ (配置的base_path)下创建 data、logs 两个目录。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">可以用这种方式启动</span><br><span class="line"># /etc/init.d/fdfs_trackerd start</span><br><span class="line">也可以用这种方式启动,前提是上面创建了软链接,后面都用这种方式</span><br><span class="line"># service fdfs_trackerd start</span><br><span class="line">查看 FastDFS Tracker 是否已成功启动 ,22122端口正在被监听,则算是Tracker服务安装成功。</span><br><span class="line"># netstat -unltp|grep fdfs</span><br></pre></td></tr></table></figure><br>关闭Tracker命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># service fdfs_trackerd stop</span><br></pre></td></tr></table></figure><br>设置Tracker开机启动<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line"># chkconfig fdfs_trackerd on</span><br><span class="line"></span><br><span class="line">或者:</span><br><span class="line"># vim /etc/rc.d/rc.local</span><br><span class="line">加入配置:</span><br><span class="line">/etc/init.d/fdfs_trackerd start </span><br></pre></td></tr></table></figure></p><h3 id="配置-FastDFS-存储-Storage"><a href="#配置-FastDFS-存储-Storage" class="headerlink" title="配置 FastDFS 存储 (Storage)"></a>配置 FastDFS 存储 (Storage)</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">进入 /etc/fdfs 目录,复制 FastDFS 存储器样例配置文件 storage.conf.sample,并重命名为 storage.conf</span><br><span class="line"># cd /etc/fdfs</span><br><span class="line"># cp storage.conf.sample storage.conf</span><br><span class="line"># vim storage.conf</span><br></pre></td></tr></table></figure><p>编辑storage.conf,标红的需要修改,其它的默认即可。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"></span><br><span class="line"># Storage 数据和日志目录地址(根目录必须存在,子目录会自动生成)</span><br><span class="line">base_path=/data/fastdfs/storage</span><br><span class="line"></span><br><span class="line"># 逐一配置 store_path_count 个路径,索引号基于 0。</span><br><span class="line"># 如果不配置 store_path0,那它就和 base_path 对应的路径一样。</span><br><span class="line">store_path0=/data/fastdfs/file</span><br><span class="line"></span><br><span class="line"># tracker_server 的列表 ,会主动连接 tracker_server</span><br><span class="line"># 有多个 tracker server 时,每个 tracker server 写一行</span><br><span class="line">tracker_server=file.jie.com:22122</span><br><span class="line"></span><br><span class="line"># 访问端口</span><br><span class="line">http.server_port=80</span><br></pre></td></tr></table></figure><br>防火墙中打开存储器端口(默认的 23000)CentOS7把iptables替换为firewall,而我又把防火墙直接关了,此步骤应该可以跳过<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line"># vim /etc/sysconfig/iptables</span><br><span class="line"></span><br><span class="line">添加如下端口行:</span><br><span class="line">-A INPUT -m state --state NEW -m tcp -p tcp --dport 23000 -j ACCEPT</span><br><span class="line"></span><br><span class="line">重启防火墙:</span><br><span class="line"># service iptables restart</span><br></pre></td></tr></table></figure><br>启动 Storage,启动Storage前确保Tracker是启动的。初次启动成功,会在 /data/fastdfs/storage 目录下创建 data、 logs 两个目录。<br>可以用这种方式启动<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line"># /etc/init.d/fdfs_storaged start</span><br><span class="line"></span><br><span class="line">也可以用这种方式,后面都用这种</span><br><span class="line"># service fdfs_storaged start</span><br></pre></td></tr></table></figure></p><p>关闭Storage命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"># service fdfs_storaged stop</span><br></pre></td></tr></table></figure><br>查看Storage和Tracker是否在通信:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">/usr/bin/fdfs_monitor /etc/fdfs/storage.conf</span><br></pre></td></tr></table></figure><br><img src="/2019/05/20/FastDFS搭建/Storage和Tracker是否通信.jpg" title="Storage和Tracker是否通信"><br>设置 Storage 开机启动<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># chkconfig fdfs_storaged on</span><br><span class="line">或者:</span><br><span class="line"># vim /etc/rc.d/rc.local</span><br><span class="line">加入配置:</span><br><span class="line">/etc/init.d/fdfs_storaged start</span><br></pre></td></tr></table></figure></p><h3 id="文件上传测试"><a href="#文件上传测试" class="headerlink" title="文件上传测试"></a>文件上传测试</h3><p>修改 Tracker 服务器中的客户端配置文件<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line"># cd /etc/fdfs</span><br><span class="line"># cp client.conf.sample client.conf</span><br><span class="line"># vim client.conf</span><br></pre></td></tr></table></figure><br>修改如下配置即可,其它默认。<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line"># Client 的数据和日志目录</span><br><span class="line">base_path=/data/fastdfs/client</span><br><span class="line"></span><br><span class="line"># Tracker端口</span><br><span class="line">tracker_server=file.jie.com:22122</span><br></pre></td></tr></table></figure><br>上传测试<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line"># /usr/bin/fdfs_upload_file /etc/fdfs/client.conf 111.png</span><br><span class="line">group1/M00/00/00/wKjlZFziAFuASdbVAAWocxCYfsk289.png</span><br></pre></td></tr></table></figure><br><img src="/2019/05/20/FastDFS搭建/文件id.png" title="文件id"></p>]]></content>
<tags>
<tag> linux </tag>
</tags>
</entry>
<entry>
<title>redis数据结构与对象</title>
<link href="/2019/05/18/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E5%AF%B9%E8%B1%A1/"/>
<url>/2019/05/18/redis%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%B8%8E%E5%AF%B9%E8%B1%A1/</url>
<content type="html"><![CDATA[<h2 id="简单动态字符串"><a href="#简单动态字符串" class="headerlink" title="简单动态字符串"></a>简单动态字符串</h2><p>Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。<br>比如在Redis的数据库里面,包含字符串值的键值对在底层都是由SDS实现的。<br>举个例子,如果客户端执行命令:<br><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">redis> SET msg "hello world"</span><br><span class="line">OK</span><br></pre></td></tr></table></figure></p><ul><li>键值对的<strong>键</strong>是一个字符串对象,对象的底层实现是一个保存着字符串“msg”的SDS。</li><li>键值对的<strong>值</strong>也是一个字符串对象,对象的底层实现是一个保存着字符串“hello world”的SDS。<br>除了用来保存数据库中的字符串值之外,SDS还被用作缓冲区(buffer):AOF模块中的AOF缓冲区,以及客户端状态中的输入缓冲区,都是由SDS实现的。</li></ul><h3 id="SDS的定义"><a href="#SDS的定义" class="headerlink" title="SDS的定义"></a>SDS的定义</h3><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">struct sdshdr {</span><br><span class="line"> //记录buf数组中已使用字节的数量</span><br><span class="line"> //等于SDS所保存字符串的长度</span><br><span class="line"> int len;</span><br><span class="line"> //记录buf数组中未使用字节的数量 最后字节空字符'\0'不计算在len里</span><br><span class="line"> int free;</span><br><span class="line"> //字节数组,用于保存字符串</span><br><span class="line"> char buf[];</span><br><span class="line">};</span><br></pre></td></tr></table></figure><img src="/2019/05/18/redis数据结构与对象/sds示例.png" title="SDS示例"><h3 id="SDS与C字符串的区别"><a href="#SDS与C字符串的区别" class="headerlink" title="SDS与C字符串的区别"></a>SDS与C字符串的区别</h3><ol><li>SDS记录了字符串长度信息,不用遍历整个字符串,O(1),设置和更新SDS长度的工作由SDS的API自动完成</li><li>杜绝缓冲区溢出,sdscat会先检查给定SDS空间是否足够,如果不够,会扩展SDS空间,在进行拼接操作<img src="/2019/05/18/redis数据结构与对象/杜绝溢出1.png" title="在内存中紧邻的两个C字符串"> <string.h>/strcat函数可以将src字符串中的内容拼接到dest字符串的末尾<br>假设要使用strcat函数将redis,修改为redis Cluster,没有为s1分配足够的内存空间,将把s1的数据溢出到s2的空间,修改了s2<img src="/2019/05/18/redis数据结构与对象/杜绝溢出2.png" title="s1的内容溢出到了s2位置"> </li></ol><h3 id="SDS空间分配策略"><a href="#SDS空间分配策略" class="headerlink" title="SDS空间分配策略"></a>SDS空间分配策略</h3><p>由于C字符串长度和底层数组的长度之间存在着这种关联性(N+1),所以每次增长或缩短一个C字符串,程序都总要对保存这个C字符串的数组进行一次内存重分配操作</p><ul><li>如果程序执行的是增长字符串的操作,比如拼接操作(append),那么在执行这个操作之前,程序需要先通过内存重分配来扩展底层数组的空间大小——如果忘了这一步就会产生缓冲区溢出。</li><li>如果程序执行的是缩短字符串的操作,比如截断操作(trim),那么在执行这个操作之后,程序需要通过内存重分配来释放字符串不再使用的那部分空间——如果忘了这一步就会产生内存泄漏。<blockquote><p>内存溢出:是指在申请内存时,没有足够的内存供其使用<br>内存泄漏:是指申请内存后,无法释放已申请的内存</p></blockquote>为了避免C字符串的这种缺陷,SDS通过未使用空间解除了字符串长度和底层数组长度之间的关联:在SDS中,buf数组的长度不一定就是字符数量加一,数组里面可以包含未使用的字节,而这些字节的数量就由SDS的free属性记录。<h4 id="空间预分配"><a href="#空间预分配" class="headerlink" title="空间预分配"></a>空间预分配</h4>空间预分配用于优化SDS的字符串增长操作:当SDS的API对一个SDS进行修改,并且需要对SDS进行空间扩展的时候,程序不仅会为SDS分配修改所必须要的空间,还会为SDS分配额外的未使用空间。在扩展SDS空间之前,SDS API会先检查未使用空间是否足够,如果足够的话,API就会直接使用未使用空间,而无须执行内存重分配。<br>额外分配的未使用空间数量由以下公式决定:<blockquote><p>如果对SDS进行修改之后,SDS的长度(也即是len属性的值)将小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS len属性的值将和free属性的值相同。举个例子,如果进行修改之后,SDS的len将变成13字节,那么程序也会分配13字节的未使用空间,SDS的buf数组的实际长度将变成13+13+1=27字节(额外的一字节用于保存空字符)。<br>如果对SDS进行修改之后,SDS的长度将大于等于1MB,那么程序会分配1MB的未使用空间。举个例子,如果进行修改之后,SDS的len将变成30MB,那么程序会分配1MB的未使用空间,SDS的buf数组的实际长度将为30MB+1MB+1byte。</p></blockquote><h4 id="惰性空间释放"><a href="#惰性空间释放" class="headerlink" title="惰性空间释放"></a>惰性空间释放</h4>惰性空间释放用于优化SDS的字符串缩短操作:当SDS的API需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来回收缩短后多出来的字节,而是<strong>使用free属性</strong>将这些字节的数量记录起来,并等待将来使用。</li></ul><h3 id="二进制安全"><a href="#二进制安全" class="headerlink" title="二进制安全"></a>二进制安全</h3><p>C字符串只能保存文本数据,而不能保存像图片、音频、视频、压缩文件这样的二进制数据。<br>将SDS的buf属性称为字节数组的原因——Redis不是用这个数组来保存字符,而是用它来保存一系列二进制数据。通过使用二进制安全的SDS,而不是C字符串,使得Redis不仅可以保存文本数据,还可以保存任意格式的二进制数据。</p><h3 id="SDS总结"><a href="#SDS总结" class="headerlink" title="SDS总结"></a>SDS总结</h3><img src="/2019/05/18/redis数据结构与对象/SDS总结.png" title="SDS总结"> <h2 id="链表"><a href="#链表" class="headerlink" title="链表"></a>链表</h2><p>链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。</p><p>链表在Redis中的应用非常广泛,比如列表键的底层实现之一就是链表。<strong>当一个列表键包含了数量比较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。</strong></p><p>除了链表键之外,<strong>发布与订阅、慢查询、监视器等功能也用到了链表</strong>,Redis服务器本身还使用链表来保存多个客户端的状态信息,以及使用链表来构建客户端输出缓冲区(output buffer)</p><h3 id="链表的实现"><a href="#链表的实现" class="headerlink" title="链表的实现"></a>链表的实现</h3><p>每个链表节点使用一个adlist.h/listNode结构来表示</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">typedef struct listNode {</span><br><span class="line"> // 前置节点</span><br><span class="line"> struct listNode * prev;</span><br><span class="line"> // 后置节点</span><br><span class="line"> struct listNode * next;</span><br><span class="line"> // 节点的值</span><br><span class="line"> void * value;</span><br><span class="line">}listNode;</span><br></pre></td></tr></table></figure><img src="/2019/05/18/redis数据结构与对象/由多个listNode组成的双端链表.png"><p>虽然仅仅使用多个listNode结构就可以组成链表,但使用adlist.h/list来持有链表的话,操作起来会更方便</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">typedef struct list {</span><br><span class="line"> // 表头节点</span><br><span class="line"> listNode * head;</span><br><span class="line"> // 表尾节点</span><br><span class="line"> listNode * tail;</span><br><span class="line"> // 链表所包含的节点数量</span><br><span class="line"> unsigned long len;</span><br><span class="line"> // 节点值复制函数</span><br><span class="line"> void *(*dup)(void *ptr);</span><br><span class="line"> // 节点值释放函数</span><br><span class="line"> void (*free)(void *ptr);</span><br><span class="line"> // 节点值对比函数</span><br><span class="line"> int (*match)(void *ptr,void *key);</span><br><span class="line">} list;</span><br></pre></td></tr></table></figure><img src="/2019/05/18/redis数据结构与对象/由list结构和listNode结构组成的链表.png"><h3 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h3><p>Redis的链表实现的特性可以总结如下:</p><ul><li><p>双端:链表节点带有prev和next指针,获取某个节点的前置节点和后置节点的复杂度都是O(1)。</p></li><li><p>无环:表头节点的prev指针和表尾节点的next指针都指向NULL,对链表的访问以NULL为终点。</p></li><li><p>带表头指针和表尾指针:通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度为O(1)。</p></li><li><p>带链表长度计数器:程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)。</p></li><li><p>多态:链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。</p></li></ul><h2 id="字典"><a href="#字典" class="headerlink" title="字典"></a>字典</h2><p>字典,又称为符号表(symbol table)、关联数组(associative array)或映射(map),是一种<strong>用于保存键值对</strong>(key-value pair)的抽象数据结构。</p><p><strong>字典中的每个键都是独一无二的</strong>,程序可以在字典中根据键查找与之关联的值,或者通过键来更新值,又或者根据键来删除整个键值对,等等。</p><p>字典在Redis中的应用相当广泛,比如<strong>Redis的数据库就是使用字典来作为底层实现的</strong>,对数据库的增、删、查、改操作也是构建在对字典的操作之上的。</p><p>除了用来表示数据库之外,<strong>字典还是哈希键的底层实现之一,当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。</strong></p><h3 id="字典的实现"><a href="#字典的实现" class="headerlink" title="字典的实现"></a>字典的实现</h3><p>Redis的<strong>字典使用哈希表作为底层实现</strong>,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。</p><h3 id="字典-1"><a href="#字典-1" class="headerlink" title="字典"></a>字典</h3><p>Redis中的字典由dict.h/dict结构表示:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">typedef struct dict {</span><br><span class="line"> // 类型特定函数</span><br><span class="line"> dictType *type;</span><br><span class="line"> // 私有数据</span><br><span class="line"> void *privdata;</span><br><span class="line"> // 哈希表</span><br><span class="line"> dictht ht[2];</span><br><span class="line"> // rehash索引</span><br><span class="line"> //当rehash不在进行时,值为-1</span><br><span class="line"> int rehashidx; /* rehashing not in progress if rehashidx == -1 */</span><br><span class="line">} dict;</span><br></pre></td></tr></table></figure><p>ht属性是一个包含两个项的数组,数组中的每个项都是一个dictht哈希表,一般情况下,字典只使用ht[0]哈希表,ht[1]哈希表只会在对ht[0]哈希表进行rehash时使用。</p><p>除了ht[1]之外,另一个和rehash有关的属性就是rehashidx,它记录了rehash目前的进度,如果目前没有在进行rehash,那么它的值为-1。</p><img src="/2019/05/18/redis数据结构与对象/普通状态下的字典.png"><h3 id="哈希表"><a href="#哈希表" class="headerlink" title="哈希表"></a>哈希表</h3><p>Redis字典所使用的哈希表由dict.h/dictht结构定义:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line">typedef struct dictht {</span><br><span class="line"> // 哈希表数组</span><br><span class="line"> dictEntry **table;</span><br><span class="line"> // 哈希表大小</span><br><span class="line"> unsigned long size;</span><br><span class="line"> //哈希表大小掩码,用于计算索引值</span><br><span class="line"> //总是等于size-1</span><br><span class="line"> unsigned long sizemask;</span><br><span class="line"> // 该哈希表已有节点的数量</span><br><span class="line"> unsigned long used;</span><br><span class="line">} dictht;</span><br></pre></td></tr></table></figure><p>table属性是一个数组,数组中的每个元素都是一个指向dict.h/dictEntry结构的指针,每个dictEntry结构保存着一个键值对。size属性记录了哈希表的大小,也即是table数组的大小,而used属性则记录了哈希表目前已有节点(键值对)的数量。sizemask属性的值总是等于size-1,这个属性和哈希值一起决定一个键应该被放到table数组的哪个索引上面。</p><img src="/2019/05/18/redis数据结构与对象/一个空的哈希表.png"><h3 id="哈希表节点"><a href="#哈希表节点" class="headerlink" title="哈希表节点"></a>哈希表节点</h3><p>哈希表节点使用dictEntry结构表示,每个dictEntry结构都保存着一个键值对:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">typedef struct dictEntry {</span><br><span class="line"> // 键</span><br><span class="line"> void *key;</span><br><span class="line"> // 值</span><br><span class="line"> union{</span><br><span class="line"> void *val;</span><br><span class="line"> uint64_tu64;</span><br><span class="line"> int64_ts64;</span><br><span class="line"> } v;</span><br><span class="line"> // 指向下个哈希表节点,形成链表</span><br><span class="line"> struct dictEntry *next;</span><br><span class="line">} dictEntry;</span><br></pre></td></tr></table></figure><p>next属性是指向另一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对连接在一次,以此来解决键冲突(collision)的问题。</p><h3 id="哈希算法"><a href="#哈希算法" class="headerlink" title="哈希算法"></a>哈希算法</h3><p>当要将一个新的键值对添加到字典里面时,程序需要先<strong>根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。</strong></p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">#使用字典设置的哈希函数,计算键key的哈希值</span><br><span class="line">hash = dict->type->hashFunction(key);</span><br><span class="line">#使用哈希表的sizemask属性和哈希值,计算出索引值</span><br><span class="line">#根据情况不同,ht[x]可以是ht[0]或者ht[1]</span><br><span class="line">index = hash & dict->ht[x].sizemask;</span><br></pre></td></tr></table></figure><h3 id="rehash"><a href="#rehash" class="headerlink" title="rehash"></a>rehash</h3><p>随着操作的不断执行,哈希表保存的键值对会逐渐地增多或者减少,为了让哈希表的负载因子(load factor)维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。</p><p><strong>扩展和收缩哈希表的工作可以通过执行rehash(重新散列)操作来完成</strong>,Redis对字典的哈希表执行rehash的步骤如下:</p><p>1)为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性的值):</p><ul><li><p>如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2 n(2的n次方幂);</p></li><li><p>如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2 n。</p></li></ul><p>2)将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。</p><p>3)当ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]变为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。</p><h3 id="哈希表的扩展与收缩"><a href="#哈希表的扩展与收缩" class="headerlink" title="哈希表的扩展与收缩"></a>哈希表的扩展与收缩</h3><p>当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:</p><p>1)服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于1。</p><p>2)服务器目前正在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5。</p><p>根据BGSAVE命令或BGREWRITEAOF命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为<strong>在执行BGSAVE命令或BGREWRITEAOF命令的过程中,Redis需要创建当前服务器进程的子进程</strong>,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行哈希表扩展操作,这可以避免不必要的内存写入操作,最大限度地节约内存。</p><p>另一方面,当哈希表的负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。</p><h3 id="渐进式rehash"><a href="#渐进式rehash" class="headerlink" title="渐进式rehash"></a>渐进式rehash</h3><p>扩展或收缩哈希表需要将ht[0]里面的所有键值对rehash到ht[1]里面,但是,这个rehash动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。</p><p>这样做的原因在于,如果ht[0]里只保存着四个键值对,那么服务器可以在瞬间就将这些键值对全部rehash到ht[1];但是,如果哈希表里保存的键值对数量不是四个,而是四百万、四千万甚至四亿个键值对,那么要一次性将这些键值对全部rehash到ht[1]的话,<strong>庞大的计算量可能会导致服务器在一段时间内停止服务。</strong></p><p>因此,为了避免rehash对服务器性能造成影响,服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。</p><p>在字典中维持一个索引计数器变量rehashidx,并将它的值设置为0,表示rehash工作正式开始。</p><p>在rehash进行期间,<strong>每次对字典执行添加、删除、查找或者更新操作时,程序除了执行指定的操作以外,还会顺带将ht[0]哈希表在rehashidx索引上的所有键值对rehash到ht[1],当rehash工作完成之后,程序将rehashidx属性的值增一。</strong>随着字典操作的不断执行,最终在某个时间点上,ht[0]的所有键值对都会被rehash至ht[1],这时程序将rehashidx属性的值设为-1,表示rehash操作已完成。</p><p>渐进式rehash的好处在于它采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。</p><h3 id="渐进式rehash执行期间的哈希表操作"><a href="#渐进式rehash执行期间的哈希表操作" class="headerlink" title="渐进式rehash执行期间的哈希表操作"></a>渐进式rehash执行期间的哈希表操作</h3><p>因为在进行渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的<strong>删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行</strong>。例如,要在字典里面查找一个键的话,程序会<strong>先在ht[0]里面进行查找,如果没找到的话,就会继续到ht[1]里面进行查找,诸如此类。</strong></p><p>另外,在渐进式rehash执行期间,<strong>新添加到字典的键值对一律会被保存到ht[1]里面</strong>,而ht[0]则不再进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增,并随着rehash操作的执行而最终变成空表。</p><h2 id="跳跃表"><a href="#跳跃表" class="headerlink" title="跳跃表"></a>跳跃表</h2><p>跳跃表(skiplist)是一种<strong>有序数据结构</strong>,它通过在<strong>每个节点中维持多个指向其他节点的指针</strong>,从而达到快速访问节点的目的。</p><p>跳跃表支持平均O(logN)、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。</p><p>Redis使用跳跃表作为<strong>有序集合键</strong>的底层实现之一,如果一个<strong>有序集合</strong>包含的<strong>元素数量比较多</strong>,又或者有序集合中元素的成员(member)是比较长的字符串时,Redis就会使用跳跃表来作为有序集合键的底层实现。</p><p>Redis<strong>只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构</strong>,除此之外,跳跃表在Redis里面没有其他用途。</p><h3 id="跳跃表的实现"><a href="#跳跃表的实现" class="headerlink" title="跳跃表的实现"></a>跳跃表的实现</h3><p>Redis的跳跃表由redis.h/zskiplistNode和redis.h/zskiplist两个结构定义,其中zskiplistNode结构用于表示跳跃表节点,而zskiplist结构则用于保存跳跃表节点的相关信息,比如节点的数量,以及指向表头节点和表尾节点的指针等等。</p><p>zskiplist结构的定义如下:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">typedef struct zskiplist {</span><br><span class="line"> // 表头节点和表尾节点</span><br><span class="line"> structz skiplistNode *header, *tail;</span><br><span class="line"> // 表中节点的数量</span><br><span class="line"> unsigned long length;</span><br><span class="line"> // 表中层数最大的节点的层数</span><br><span class="line"> int level;</span><br><span class="line">} zskiplist;</span><br></pre></td></tr></table></figure><img src="/2019/05/18/redis数据结构与对象/一个跳跃表.png"><img src="/2019/05/18/redis数据结构与对象/2019021502154415.jpg"><p>zskiplist结构,该结构包含以下属性:</p><ul><li><p>header:指向跳跃表的表头节点。</p></li><li><p>tail:指向跳跃表的表尾节点。</p></li><li><p>level:记录目前跳跃表内,层数最大的那个节点的层数(表头节点的层数不计算在内)。</p></li><li><p>length:记录跳跃表的长度,也即是,跳跃表目前包含节点的数量(表头节点不计算在内)。</p></li></ul><p>位于zskiplist结构右方的是四个zskiplistNode结构,该结构包含以下属性:</p><ul><li><p>层(level):节点中用L1、L2、L3等字样标记节点的各个层,L1代表第一层,L2代表第二层,以此类推。每个层都带有两个属性:前进指针和跨度。前进指针用于访问位于表尾方向的其他节点,而跨度则记录了前进指针所指向节点和当前节点的距离。在上面的图片中,连线上带有数字的箭头就代表前进指针,而那个数字就是跨度。当程序从表头向表尾进行遍历时,访问会沿着层的前进指针进行。</p></li><li><p>后退(backward)指针:节点中用BW字样标记节点的后退指针,它指向位于当前节点的前一个节点。后退指针在程序从表尾向表头遍历时使用。</p></li><li><p>分值(score):各个节点中的1.0、2.0和3.0是节点所保存的分值。在跳跃表中,节点按各自所保存的分值从小到大排列。</p></li><li><p>成员对象(obj):各个节点中的o1、o2和o3是节点所保存的成员对象。</p></li></ul><h3 id="跳跃表节点"><a href="#跳跃表节点" class="headerlink" title="跳跃表节点"></a>跳跃表节点</h3><p>跳跃表节点的实现由redis.h/zskiplistNode结构定义:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">typedef struct zskiplistNode {</span><br><span class="line"> // 层</span><br><span class="line"> struct zskiplistLevel {</span><br><span class="line"> // 前进指针</span><br><span class="line"> struct zskiplistNode *forward;</span><br><span class="line"> // 跨度</span><br><span class="line"> unsigned int span;</span><br><span class="line"> } level[];</span><br><span class="line"> // 后退指针</span><br><span class="line"> struct zskiplistNode *backward;</span><br><span class="line"> // 分值</span><br><span class="line"> double score;</span><br><span class="line"> // 成员对象</span><br><span class="line"> robj *obj;</span><br><span class="line">} zskiplistNode;</span><br></pre></td></tr></table></figure><h3 id="层"><a href="#层" class="headerlink" title="层"></a>层</h3><p>跳跃表节点的level数组可以包含多个元素,每个元素都包含一个指向其他节点的指针,程序可以通过这些层来加快访问其他节点的速度,一般来说,<strong>层的数量越多,访问其他节点的速度就越快。</strong></p><p>每次创建一个新跳跃表节点的时候,程序都根据<strong>幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的“高度”。</strong></p><h3 id="前进指针"><a href="#前进指针" class="headerlink" title="前进指针"></a>前进指针</h3><p>每个层都有一个指向表尾方向的前进指针(level[i].forward属性),用于从表头向表尾方向访问节点。</p><h3 id="跨度"><a href="#跨度" class="headerlink" title="跨度"></a>跨度</h3><p>跨度实际上是用来计算排位(rank)的:在查找某个节点的过程中,将沿途访问过的所有层的跨度累计起来,得到的结果就是目标节点在跳跃表中的排位。</p><h3 id="后退指针"><a href="#后退指针" class="headerlink" title="后退指针"></a>后退指针</h3><p>节点的后退指针(backward属性)用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。</p><h3 id="分值和成员"><a href="#分值和成员" class="headerlink" title="分值和成员"></a>分值和成员</h3><p>节点的分值(score属性)是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。分值相同的节点将按照成员对象在字典序中的大小来进行排序,成员对象较小的节点会排在前面(靠近表头的方向),而成员对象较大的节点则会排在后面(靠近表尾的方向)。</p><h2 id="整数集合"><a href="#整数集合" class="headerlink" title="整数集合"></a>整数集合</h2><p>整数集合(intset)是<strong>集合键</strong>的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。</p><h3 id="整数集合的实现"><a href="#整数集合的实现" class="headerlink" title="整数集合的实现"></a>整数集合的实现</h3><p>整数集合(intset)是Redis用于保存整数值的集合抽象数据结构,它可以保存类型为int16_t、int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。</p><p>每个intset.h/intset结构表示一个整数集合:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">typedef struct intset {</span><br><span class="line"> // 编码方式</span><br><span class="line"> uint32_t encoding;</span><br><span class="line"> // 集合包含的元素数量</span><br><span class="line"> uint32_t length;</span><br><span class="line"> // 保存元素的数组</span><br><span class="line"> int8_t contents[];</span><br><span class="line">} intset;</span><br></pre></td></tr></table></figure><p>contents数组是整数集合的底层实现:整数集合的每个元素都是contents数组的一个数组项(item),各个项在数组中按值的大小<strong>从小到大有序地排列,并且数组中不包含任何重复项。</strong></p><p>length属性记录了整数集合包含的元素数量,也即是contents数组的长度。</p><p>虽然intset结构将contents属性声明为int8_t类型的数组,但实际上contents数组并不保存任何int8_t类型的值,<strong>contents数组的真正类型取决于encoding属性的值</strong>:</p><ul><li><p>如果encoding属性的值为INTSET_ENC_INT16,那么contents就是一个int16_t类型的数组,数组里的每个项都是一个int16_t类型的整数值(最小值为-32768,最大值为32767)。</p></li><li><p>如果encoding属性的值为INTSET_ENC_INT32,那么contents就是一个int32_t类型的数组,数组里的每个项都是一个int32_t类型的整数值(最小值为-2147483648,最大值为2147483647)。</p></li><li><p>如果encoding属性的值为INTSET_ENC_INT64,那么contents就是一个int64_t类型的数组,数组里的每个项都是一个int64_t类型的整数值(最小值为-9223372036854775808,最大值为9223372036854775807)。</p></li></ul><img src="/2019/05/18/redis数据结构与对象/一个包含五个int16_t类型整数值的整数集合.png"><p>不过根据整数集合的升级规则,当向一个底层为int16_t数组的整数集合添加一个int64_t类型的整数值时,整数集合已有的所有元素都会被转换成int64_t类型,所以contents数组保存的四个整数值都是int64_t类型的</p><h3 id="升级"><a href="#升级" class="headerlink" title="升级"></a>升级</h3><p>每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级(upgrade),然后才能将新元素添加到整数集合里面。</p><p>升级整数集合并添加新元素共分为三步进行:</p><p>1)<strong>根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。</strong></p><p>2)<strong>将底层数组现有的所有元素都转换成与新元素相同的类型</strong>,并将类型转换后的元素放置到正确的位上,而且在放置元素的过程中,需要继续维持底层数组的有序性质不变。</p><p>3)将新元素添加到底层数组里面。</p><p>因为每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中已有的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为O(N)。</p><h3 id="升级的好处"><a href="#升级的好处" class="headerlink" title="升级的好处"></a>升级的好处</h3><p>整数集合的升级策略有两个好处,一个是提升整数集合的灵活性,另一个是尽可能地节约内存。</p><h3 id="降级"><a href="#降级" class="headerlink" title="降级"></a>降级</h3><p><strong>整数集合不支持降级操作</strong>,一旦对数组进行了升级,编码就会一直保持升级后的状态。</p><h2 id="压缩列表"><a href="#压缩列表" class="headerlink" title="压缩列表"></a>压缩列表</h2><p><strong>压缩列表(ziplist)是列表键和哈希键的底层实现之一。</strong>当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。</p><p><strong>压缩列表是Redis为了节约内存而开发的</strong>,<strong>是由一系列特殊编码的连续内存块组成的顺序型(sequential)数据结构</strong>。一个压缩列表可以包含任意多个节点(entry),每个节点可以保存一个字节数组或者一个整数值。</p><img src="/2019/05/18/redis数据结构与对象/压缩列表的各个组成部分.png"><img src="/2019/05/18/redis数据结构与对象/压缩列表各个组成部分的详细说明.png"><img src="/2019/05/18/redis数据结构与对象/包含三个节点的压缩列表.png"><ul><li><p>列表zlbytes属性的值为0x50(十进制80),表示压缩列表的总长为80字节。</p></li><li><p>列表zltail属性的值为0x3c(十进制60),这表示如果我们有一个指向压缩列表起始地址的指针p,那么只要用指针p加上偏移量60,就可以计算出表尾节点entry3的地址。</p></li><li><p>列表zllen属性的值为0x3(十进制3),表示压缩列表包含三个节点。</p></li></ul><h3 id="压缩列表节点的构成"><a href="#压缩列表节点的构成" class="headerlink" title="压缩列表节点的构成"></a>压缩列表节点的构成</h3><p>每个压缩列表节点都由previous_entry_length、encoding、content三个部分组成</p><img src="/2019/05/18/redis数据结构与对象/压缩列表节点的各个组成部分.png"><h4 id="previous-entry-length"><a href="#previous-entry-length" class="headerlink" title="previous_entry_length"></a>previous_entry_length</h4><p>节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。</p><p>previous_entry_length属性的长度可以是1字节或者5字节:</p><ul><li><p>如果前一节点的长度小于254字节,那么previous_entry_length属性的长度为1字节:前一节点的长度就保存在这一个字节里面。</p></li><li><p>如果前一节点的长度大于等于254字节,那么previous_entry_length属性的长度为5字节:其中属性的第一字节会被设置为0xFE(十进制值254),而之后的四个字节则用于保存前一节点的长度。</p></li></ul><p>因为节点的previous_entry_length属性记录了前一个节点的长度,所以程序可以通过指针运算,<strong>根据当前节点的起始地址来计算出前一个节点的起始地址。</strong>程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。</p><h3 id="连锁更新"><a href="#连锁更新" class="headerlink" title="连锁更新"></a>连锁更新</h3><p>每个节点的previous_entry_length属性都记录了前一个节点的长度:</p><ul><li><p>如果前一节点的长度小于254字节,那么previous_entry_length属性需要用1字节长的空间来保存这个长度值。</p></li><li><p>如果前一节点的长度大于等于254字节,那么previous_entry_length属性需要用5字节长的空间来保存这个长度值。</p></li></ul><p>现在,考虑这样一种情况:在一个压缩列表中,有多个连续的、长度介于250字节到253字节之间的节点e1至eN,因为e1至eN的所有节点的长度都小于254字节,所以记录这些节点的长度只需要1字节长的previous_entry_length属性,换句话说,e1至eN的所有节点的previous_entry_length属性都是1字节长的。</p><p>这时,如果我们将一个长度大于等于254字节的新节点new设置为压缩列表的表头节点,那么new将成为e1的前置节点</p><img src="/2019/05/18/redis数据结构与对象/添加新节点到压缩列表.png"><p>因为e1的previous_entry_length属性仅长1字节,它没办法保存新节点new的长度,所以程序将对压缩列表执行空间重分配操作,并将e1节点的previous_entry_length属性从原来的1字节长扩展为5字节长。正如扩展e1引发了对e2的扩展一样,扩展e2也会引发对e3的扩展,而扩展e3又会引发对e4的扩展……为了让每个节点的previous_entry_length属性都符合压缩列表对节点的要求,程序需要不断地对压缩列表执行空间重分配操作,直到eN为止。</p><p>删除结点同理</p><h2 id="不同类型和编码的对象"><a href="#不同类型和编码的对象" class="headerlink" title="不同类型和编码的对象"></a>不同类型和编码的对象</h2><img src="/2019/05/18/redis数据结构与对象/不同类型和编码的对象.png"><h3 id="string"><a href="#string" class="headerlink" title="string"></a>string</h3><ul><li><strong>使用整数值实现的字符串对象</strong></li></ul><ul><li><p><strong>使用embstr编码的简单动态字符串实现的字符串对象</strong></p></li><li><p><strong>使用简单动态字符串实现的字符串对象</strong></p></li></ul><p>字符串对象的编码可以是int、raw或者embstr。<br>如果一个字符串对象保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成long),并将字符串对象的编码设置为int。</p><p>如果字符串对象保存的是一个字符串值,并且<strong>这个字符串值的长度大于32字节</strong>,那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串值,并将对象的编码设置为raw。</p><p>如果字符串对象保存的是一个字符串值,并且这个字符串值的长度小于等于32字节,那么字符串对象将使用embstr编码的方式来保存这个字符串值。embstr编码是专门用于保存短字符串的一种优化编码方式,这种编码和raw编码一样,都使用redisObject结构和sdshdr结构来表示字符串对象,但raw编码会调用两次内存分配函数来分别创建redisObject结构和sdshdr结构,而embstr编码则通过调用一次内存分配函数来分配一块连续的空间,空间中依次包含redisObject和sdshdr两个结构。</p><blockquote><p>embstr编码将创建字符串对象所需的内存分配次数从raw编码的两次降低为一次。</p><p>释放embstr编码的字符串对象只需要调用一次内存释放函数,而释放raw编码的字符串对象需要调用两次内存释放函数。</p><p>因为embstr编码的字符串对象的所有数据都保存在一块连续的内存里面,所以这种编码的字符串对象比起raw编码的字符串对象能够更好地利用缓存带来的优势。</p></blockquote><h3 id="list"><a href="#list" class="headerlink" title="list"></a>list</h3><ul><li><strong>使用压缩列表实现的列表对象</strong></li><li><strong>使用双端链表实现的列表对象</strong></li></ul><p>列表对象的编码可以是ziplist或者linkedlist。</p><p>ziplist编码的列表对象使用压缩列表作为底层实现,每个压缩列表节点(entry)保存了一个列表元素。</p><p>linkedlist编码的列表对象使用双端链表作为底层实现,每个双端链表节点(node)都保存了一个字符串对象,而每个字符串对象都保存了一个列表元素。</p><p>当列表对象可以同时满足以下两个条件时,列表对象使用ziplist编码:</p><ul><li>列表对象保存的所有字符串元素的长度都小于64字节;</li><li>列表对象保存的元素数量小于512个;</li></ul><p>不能满足这两个条件的列表对象需要使用linkedlist编码。</p><h3 id="hash"><a href="#hash" class="headerlink" title="hash"></a>hash</h3><ul><li><strong>使用压缩列表实现的哈希对象</strong></li><li><strong>使用字典实现的哈希对象</strong></li></ul><p>哈希对象的编码可以是ziplist或者hashtable。</p><p>ziplist编码的哈希对象使用压缩列表作为底层实现,每当有新的键值对要加入到哈希对象时,程序会先将保存了键的压缩列表节点推入到压缩列表表尾,然后再将保存了值的压缩列表节点推入到压缩列表表尾,因此:</p><ul><li><p>保存了同一键值对的两个节点总是紧挨在一起,保存键的节点在前,保存值的节点在后;</p></li><li><p>先添加到哈希对象中的键值对会被放在压缩列表的表头方向,而后来添加到哈希对象中的键值对会被放在压缩列表的表尾方向。</p></li></ul><p>另一方面,hashtable编码的哈希对象使用字典作为底层实现,哈希对象中的每个键值对都使用一个字典键值对来保存:</p><ul><li><p>字典的每个键都是一个字符串对象,对象中保存了键值对的键;</p></li><li><p>字典的每个值都是一个字符串对象,对象中保存了键值对的值。</p></li></ul><p>当哈希对象可以同时满足以下两个条件时,哈希对象使用ziplist编码:</p><ul><li><p>哈希对象保存的所有键值对的键和值的字符串长度都小于64字节;</p></li><li><p>哈希对象保存的键值对数量小于512个;</p></li></ul><p>不能满足这两个条件的哈希对象需要使用hashtable编码。</p><h3 id="set"><a href="#set" class="headerlink" title="set"></a>set</h3><ul><li><strong>使用整数集合实现的集合对象</strong></li><li><strong>使用字典实现的集合对象</strong></li></ul><p>集合对象的编码可以是intset或者hashtable。</p><p>intset编码的集合对象使用整数集合作为底层实现,集合对象包含的所有元素都被保存在整数集合里面。</p><p>另一方面,hashtable编码的集合对象使用字典作为底层实现,字典的每个键都是一个字符串对象,每个字符串对象包含了一个集合元素,而字典的值则全部被设置为NULL。</p><p>当集合对象可以同时满足以下两个条件时,对象使用intset编码:</p><ul><li><p>集合对象保存的所有元素都是整数值;</p></li><li><p>集合对象保存的元素数量不超过512个。</p></li></ul><p>不能满足这两个条件的集合对象需要使用hashtable编码。</p><h3 id="zset"><a href="#zset" class="headerlink" title="zset"></a>zset</h3><ul><li><p><strong>使用压缩列表实现的有序集合对象</strong></p></li><li><p><strong>使用跳跃表和字典实现的有序集合对象</strong></p></li></ul><p>有序集合的编码可以是ziplist或者skiplist。</p><p>ziplist编码的压缩列表对象使用压缩列表作为底层实现,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员(member),而第二个元素则保存元素的分值(score)。</p><p>压缩列表内的集合元素按分值从小到大进行排序,分值较小的元素被放置在靠近表头的方向,而分值较大的元素则被放置在靠近表尾的方向。</p><p>skiplist编码的有序集合对象使用zset结构作为底层实现,一个zset结构同时包含一个字典和一个跳跃表:</p><figure class="highlight plain"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">typedef struct zset {</span><br><span class="line"> zskiplist *zsl;</span><br><span class="line"> dict *dict;</span><br><span class="line">} zset;</span><br></pre></td></tr></table></figure><p>zset结构中的zsl跳跃表按分值从小到大保存了所有集合元素,每个跳跃表节点都保存了一个集合元素:跳跃表节点的object属性保存了元素的成员,而跳跃表节点的score属性则保存了元素的分值。通过这个跳跃表,程序可以对有序集合进行范围型操作,比如ZRANK、ZRANGE等命令就是基于跳跃表API来实现的。</p><p>在理论上,有序集合可以单独使用字典或者跳跃表的其中一种数据结构来实现,但无论单独使用字典还是跳跃表,在性能上对比起同时使用字典和跳跃表都会有所降低。举个例子,如果我们只使用字典来实现有序集合,那么虽然以O(1)复杂度查找成员的分值这一特性会被保留,但是,因为字典以无序的方式来保存集合元素,所以每次在执行范围型操作——比如ZRANK、ZRANGE等命令时,程序都需要对字典保存的所有元素进行排序,完成这种排序需要至少O(NlogN)时间复杂度,以及额外的O(N)内存空间(因为要创建一个数组来保存排序后的元素)。</p><p>另一方面,如果我们只使用跳跃表来实现有序集合,那么跳跃表执行范围型操作的所有优点都会被保留,但因为没有了字典,所以根据成员查找分值这一操作的复杂度将从O(1)上升为O(logN)。因为以上原因,为了让有序集合的查找和范围型操作都尽可能快地执行,Redis选择了同时使用字典和跳跃表两种数据结构来实现有序集合。</p><p>当有序集合对象可以同时满足以下两个条件时,对象使用ziplist编码:</p><ul><li><p>有序集合保存的元素数量小于128个;</p></li><li><p>有序集合保存的所有元素成员的长度都小于64字节;</p></li></ul><p>不能满足以上两个条件的有序集合对象将使用skiplist编码。</p>]]></content>
<tags>
<tag> redis </tag>
</tags>
</entry>
<entry>
<title>zookeeper学习1</title>
<link href="/2019/05/12/zookeeper/"/>
<url>/2019/05/12/zookeeper/</url>
<content type="html"><![CDATA[<h2 id="zookeeper是什么"><a href="#zookeeper是什么" class="headerlink" title="zookeeper是什么"></a>zookeeper是什么</h2><p>ZooKeeper是一个分布式的,开放源码的分布式应用程序协调服务<br>具体介绍可以看官方网站:<a href="https://zookeeper.apache.org/" target="_blank" rel="noopener">https://zookeeper.apache.org/</a></p><h2 id="zookeeper能做什么"><a href="#zookeeper能做什么" class="headerlink" title="zookeeper能做什么"></a>zookeeper能做什么</h2><ol><li>配置维护:在分布式系统中,一般会把服务部署到n台机器上,服务配置文件都是相同的,如果配置文件的配置选项发生了改变,那我们就得一台一台的去改动。这时候zookeeper就起作用了,可以把zk当成一个高可用的配置存储器,把这样配置的事情交给zk去进行管理,将集群的配置文件拷贝到zookeeper的文件系统的某个节点上,然后用zk监控所有分布式系统里的配置文件状态,一旦发现有配置文件发生了变化,那么每台服务器同步zk的配置文件,zk同时保证同步操作的原子性,确保每个服务器的配置文件都能被更新。</li><li>命名服务:在分布式应用中,通常需要一个完整的命名规则,既能够产生唯一的名称又便于人识别和记住。Zk就提供了这种服务,类似于域名和ip之间对应关系,域名容易记住,通过名称来获取资源和服务的地址,提供者等信息。</li><li>分布式锁:分布式程序分布在不同主机上的进程对互斥资源进行访问的时候需要加锁。这样理解:很多分布式系统有多个服务窗口,但是某个时刻只让一个服务去干活,当这台服务器出问题的时候锁释放,里脊fail over到另外的服务。举例子,比如去某个地方办理证件的时候,只能有一个窗口对你服务,如果这个窗口的柜员有急事走了,那么系统或者经理给你指定另外一个窗口继续服务。</li><li>集群管理:分布式集群中,经常会由于各种原因,比如硬件故障,网络问题,有些节点挂掉、有些节点加进来。这个时候机器需要感知到变化,然后根据变化做出对应的决策,那么zk就实现了类似这种集群的管理。</li><li>队列管理 :类似一些mq实现队列的功能,这个不常用,不适合高性能的应用。</li></ol><h2 id="zookeeper的角色管理"><a href="#zookeeper的角色管理" class="headerlink" title="zookeeper的角色管理"></a>zookeeper的角色管理</h2><p>领导者(Leader):领导者负责进行投票的发起和决议,更新系统状态。<br>学习者(Learner):跟随者(Follower):用于接受客户请求并向客户端返回结果,在选主过程中参与投票。<br> 观察者(ObServer):ObServer可以接受客户端连接,将写请求转发给leader节点,但<br>ObServer不参加投票过程,只同步leader状态。ObServer的目的是为了扩展系统,提高读取<br>速度。<br>客户端(Client):请求发起方。</p><h2 id="zookeeper设计原则"><a href="#zookeeper设计原则" class="headerlink" title="zookeeper设计原则"></a>zookeeper设计原则</h2><ol><li>最终一致性:客户端(Client)无论连接到哪个zk的节点,展示给他的视图都是一样的。</li><li>可靠性:消息message被到一台服务器接受,那么它到任何服务器都被接受。</li><li>实时性:zk保证在一个时间间隔范围内获得服务器的更新信息,或者服务器失效信息。但是由于网络延时等一些其他原因,zk不能保证两个客户端同事得到跟新或者失效信息。</li><li>等待无关:慢的或者失效的客户端(Client)不得干预快速的client的请求,使得每个client都能有效的等待。</li><li>原子性:更新只能成功或者失败,没有其他中间信息。</li><li>顺序性:包括全局有序和偏序两种:全局有序是指如果再一台服务器上消息a在消息b前发布,则在所有Server上消息a都将在消息b前被发布;偏序是指如果一个消息b在消息a后被同一个发送者发布,a必将排在b前面。</li></ol><h2 id="zookeeper工作原理"><a href="#zookeeper工作原理" class="headerlink" title="zookeeper工作原理"></a>zookeeper工作原理</h2><p>用这个命令登录服务器看一下目录结构以及zid:./zkCli.sh -server 10.15.0.97:2181<br>基本常用的两个命令:ls /目录 get /目录<br>zk的核心是原子广播,这个机制保证了各个Server之间的同步,实现这个机制的协议叫做Zab协议。Zab协议有两种模式,分别是恢复模式(选主)和广播模式(同步)。当服务启动或者领导者崩溃后,Zab进入恢复模式,当leader被选举出来,然后进行同步模式,同步完成以后,恢复模式结束。<br>为了保证事务的顺序一致性。实现中zxid是一个64位的数字,它高32位是用epoch用来标志leader关系是否改变,每次一个新的leader选举出来,都会拥有一个新的epoch。低32位用来递增计数。<br>(1)Serverid:在配置server时,给定的服务器的标示id。<br>(2)Zxid:服务器在运行时产生的数据id,zxid越大,表示数据越新。<br>(3)Epoch:选举的轮数,即逻辑时钟。随着选举的轮数++</p><h3 id="选主流程"><a href="#选主流程" class="headerlink" title="选主流程"></a>选主流程</h3><p>当leader崩溃或者leader失去大多数的follower,这时候zk进入恢复模式,然后需要重新选举出一个leader。让所有的Server都恢复到一个正确的状态。Zk选举算法有两种,一种是基于basic paxos实现,一种是基于fast paxos算法实现。系统默认的是fast paxos。<br>每个Server在工作过程中有三种状态:<br>LOOKING:当前Server不知道Leader是谁,正在搜寻。<br>LEADING:当前Server即为选举出来的leader。<br>FOLLOWING:leader已经选举出来,当前Server与之同步。<br>首先介绍basic paxos流程(简单介绍):</p><ol><li>选举线程由当前Server发起选举的线程担任,其主要功能是对投票结果进行统计,并选出推荐的Server。</li><li>选举线程首先向所有Server发起一次询问(包括自己)。</li><li>选举线程收到回复后,验证是否是自己发起的询问(验证zxid是否一致),然后获取对方的id(myid),并存储到当前询问对象列表中,最后获取对方提议的leader相关信息(myid,zxid),并将这些信息存储到当次选举的投票记录表中。</li><li>收到所有Server回复以后,就计算出zxid最大的那个Server,并将这个Server相关信息设置成下一次投票的Server。</li><li>线程将当前zxid最大的Server设置成为当前Server要推荐的Leader,若果此时获胜的Server获得n/2+1的Server票数,设置当前推荐的leader为获胜的Server,将根据获胜的Server相关信息设置成自己的状态,否则,继续这个过程,直到leader被选举出来。<br>备注:要使Leader获得多数的Server支持,则Server总数必须是奇数2n+1,且存活的Server的数据不得少于n+1。<img src="/2019/05/12/zookeeper/xuanzhu.jpg" title="选主流程"></li></ol><p>其次介绍fast paxos:</p><ol><li>server启动、恢复准备加入集群,此时都会读取本身的zxid等信息。</li><li>所有server加入集群时都会推荐自己成为leader,然后将(leader id,zxid,epoch)作为广播信息到集群中所有的server,等待集群中的server返回信息。</li><li>收到集群中其他服务器返回的信息,分为两类,服务器处于looking状态,或者其他状态。<br>(1)服务器处于looking状态<br>说先判断逻辑时钟Epoch:<br>(a)如果接受到Epoch大于自己目前的逻辑时钟,那么更新本机的Epoch,同时clear其他服务器发送来的选举数据。然后判断是否需要更新当前自己的选举情况(开始选择的leader id是自己)。<br>判断规则:保存的zxid最大值和leader id来进行判断。先看数据zxid,zxid大的胜出;其次判断leader id,leader id大的胜出;然后再将自身最新的选举结果广播给其他server。<br>(b)如果接受到的Epoch小于目前的逻辑时钟,说明对方处于一个比较低一轮的选举轮数,这时需要将自己的选举情况发送给它即可。<br>(c)如果接收到的Epoch等于目前的逻辑时钟,再根据(a)中的判断规则,将自身的最新选举结果广播给其他server。<br>同时server还要处理两种情况:<br>(a)如果server接收到了其他所有服务器的选举信息,那么则根据这些选举信息确定自己的状态(Following,Leading),结束Looking,退出选举。<br>(b)即时没有收到所有服务器的选举信息,也可以判断一下根据以上过程之后最新的选举leader是不是得到了超过半数以上服务器的支持,如果是则尝试接受最新数据,如果没有最新数据,说明都接受了这个结果,同样也退出选举过程。<br>(2)服务器处于其他状态(Following,Leading)<br>(a)若果逻辑时钟Epoch相同,将该数据保存到recvset,若果所接受服务器宣称自己是leader,那么将判断是不是有半数以上的服务器选举他,若果是则设置选举状态退出选举过程。<br>(b)若果Epoch不相同,那么说明另一个选举过程中已经有了选举结果,于是将选举结果加入到outofelection集合中,再根据outofelection来判断是否可以结束选举,保存逻辑时钟,设置选举状态,并退出选举过程。<img src="/2019/05/12/zookeeper/xuanzhu1.jpg" title="选主流程"><h3 id="同步流程"><a href="#同步流程" class="headerlink" title="同步流程"></a>同步流程</h3></li><li>leader等待server连接。</li><li>follower连接到leader,将最大的zxid发送给leader。</li><li>leader根据zxid确定同步点。</li><li>同步完成之后,通知follower成为uptodat状态。</li><li>follower收到uptodate消息后,开始接受client请求服务。<h3 id="主要功能"><a href="#主要功能" class="headerlink" title="主要功能"></a>主要功能</h3></li><li>Leader主要功能<br>(a)恢复数据。<br>(b)维持与Learner的心跳,接受Learner请求并判断Learner的请求消息类型。<br>备注:Learner的消息类型主要是ping、request、ack、revalidate。<br>ping消息:是指Learner的心跳信息。<br>request消息:follower发送的提议信息,包括写请求和同步请求。<br>ack消息:是follower对提议的回复,超过半数follower通过,则commit提议。<br>revalidate消息:用来延长session有效时间。</li><li>Follower主要功能<br>(a)向Leader发送请求。<br>(b)接受Leaser消息并进行处理。<br>(c)接受Client的请求,如果是写请求,发送给Leader进行投票。<br>(d)返回结果给Client。<br>备注:follower处理Leader的如下几个消息:<br>ping:心跳信息。<br>proposal消息:leader发起提案,要求follower投票。<br>commit消息:服务器端最新一次提案的消息。<br>uptodate消息:表明同步完成。<br>revalidate消息:根据Leader的REVALIDATE结果,关闭待revalidate的session还是允许其接受消息;sync消息:返回sync信息到client客户端。</li></ol>]]></content>
<tags>
<tag> zookeeper </tag>
</tags>
</entry>
</search>