-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathfeed.xml
1938 lines (1518 loc) · 145 KB
/
feed.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
998
999
1000
<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.9.0">Jekyll</generator><link href="https://naffan.cn/feed.xml" rel="self" type="application/atom+xml" /><link href="https://naffan.cn/" rel="alternate" type="text/html" /><updated>2021-12-07T23:17:55+08:00</updated><id>https://naffan.cn/feed.xml</id><title type="html">拿饭网</title><subtitle>拿饭网是一个私人博客系统,采用jekyll博客系统部署在GitHub-Pages服务上。拿饭网内的文章均出自作者,作者以技术为导向同时兼顾自己的生活与对经济的观点。</subtitle><author><name>张一帆</name></author><entry><title type="html">聊聊我使用ffmpeg时总结的最佳实践</title><link href="https://naffan.cn/tech/2021/11/25/01.html" rel="alternate" type="text/html" title="聊聊我使用ffmpeg时总结的最佳实践" /><published>2021-11-25T00:00:00+08:00</published><updated>2021-11-25T00:00:00+08:00</updated><id>https://naffan.cn/tech/2021/11/25/01</id><content type="html" xml:base="https://naffan.cn/tech/2021/11/25/01.html"><blockquote>
<p>FFmpeg is the leading multimedia framework, able to <strong>decode</strong>, <strong>encode</strong>, <strong>transcode</strong>, <strong>mux</strong>, <strong>demux</strong>, <strong>stream</strong>, <strong>filter</strong> and <strong>play</strong> pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure <a href="http://fate.ffmpeg.org/">FATE</a> across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations.</p>
</blockquote>
<p>部门的项目涉及到视频处理的功能,公司里负责存储服务的同学并没有这方面的能力支持(虽然他们也正在建设此方面的能力,因为公司内部需要视频处理的业务越来越多了。),所以我们只能硬着头皮开搞ffmpeg。曾经在看游戏的时候短暂的接触过半年多ffmpeg,但那个时候解决问题的能力不足,靠别人帮助的情况比较多。这么多年过去了,自己在开发领域的驾轻就熟,让我有了底气接了这摊子事儿。那么接下来我就总结下这几个月对ffmpeg的学习和使用吧。</p>
<h1 id="一框架选择---jave">一、框架选择 - jave</h1>
<p>项目是java的项目,借助github和google,了解到目前处理ffmpeg的框架最好用的是一个叫做<a href="https://github.com/a-schild/jave2">jave</a>的开源项目。jave其实就是一个对ffmpeg包装的java库。目前jave的版本已经发展到了3.2.0,只需要在项目中引用jave-all-deps即可,他包括了jave-core和不同平台的ffmpeg脚本。</p>
<pre><code class="language-java">&lt;dependency&gt;
&lt;groupId&gt;ws.schild&lt;/groupId&gt;
&lt;artifactId&gt;jave-all-deps&lt;/artifactId&gt;
&lt;version&gt;3.2.0&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>由于我们公司的maven库的阿里镜像源超时的原因(那个时候找不到人来解决这个问题。),导致我用不到3.2.0版本的jave,我能获取到最新的版本就是2.7.3。这就导致了我在使用2.7.3时发现了问题去github上提<a href="https://github.com/a-schild/jave2/issues/160">concat mp4 not working properly. version 2.7.3 </a>,开发者明确表示他没有资源去解决老版本的问题。反正,经过各种方面的努力,我也找到了我们公司管理maven库的同学,在他的帮助下我可以获取到3.2.0版本的jave了。但是,jave框架在视频拼接的处理上做的实在够差,我提了很多类似拼接的issue,比如<a href="https://github.com/a-schild/jave2/issues/166">The ffmpeg version included in 2.7.3 is a vry old one, which might cause the issue. </a>。开发者在我的追问下已经不再回答我了。还有一次给他提了一个多个视频拼接时监控代码会报NPE问题的issue,<a href="https://github.com/a-schild/jave2/issues/178">when lots of videos to concat . EncoderProgressListernr get a NPE Exception</a>,感觉开发者实在忙不过来了,转而求助我对此框架的修复。</p>
<p>最终,拼接视频的代码我自己写了。格式转换的用了jave的。总的来说,jave可以帮助门外汉快速上手,因为我们可以通过看他的代码一步一步了解他封装ffmpeg时使用的方法以及他在遇到不同操作情况时是采用的什么方案。但是,当你慢慢了解ffmpeg的时候,jave框架会因为他的封装不够扩展而让开发举步维艰。</p>
<p>当对ffmpeg有了一些最基本的了解后,ffmpeg官方网站(https://ffmpeg.org)就成为了进阶时最信得过的锦囊宝典。</p>
<h1 id="二ffmpeg官网">二、FFmpeg官网</h1>
<p>FFmpeg官网的左边栏,我们可以下载到ffmpeg最新的命令行。通过Documentation我们可以实时查看目前现有的所有功能,因为这个页面每晚都会重新生成。里面最常用的就是“<strong>Command Line Tools Documentation</strong>”和“<strong>Components Documentation</strong>”还有“<strong>General Documentation</strong>”。</p>
<p>官网同时还提供了IRC和MAIL等联系方式,IRC的频道是#ffmpeg 和 #ffmpeg-devel。两个频道分别面向的是使用者和开发者。我曾经在使用ffmpeg发生各种问题时进到IRC的频道里去寻求帮助,虽然里面会有4百多位处在挂机状态,但不时还是会有一些人进行交流的。我总得看下来,感觉里面说的东西都是C方面的问题,别的问题感觉里面的人不太感兴趣。我就因为又一次遇到了问题在里面提问了,确实有人回应我了,人家要我给出命令行以及错误堆栈。在IRC中你不能直接贴出来代码,需要一些专门托管代码片段的网站专门来制作代码片段连接,这一点有点让我觉得麻烦,可是又没别的好办法。等过了一会我把连接贴出来后,就没人再回应了。总体来说,体验很差,浪费了自己很多宝贵时间。我建议后来的人还是不要在IRC上浪费时间了。</p>
<h1 id="三音视频流的概念">三、音视频流的概念</h1>
<p>让我们先看一个视频的详细信息:</p>
<pre><code class="language-bash"># ffprobe 1_1637395707\&amp;88966_1637395335_1637395635.mp4 -hide_banner
[aac @ 0x7f846a808600] Input buffer exhausted before END element found
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from '1_1637395707&amp;88966_1637395335_1637395635.mp4':
Metadata:
major_brand : mp42
minor_version : 0
compatible_brands: isommp42
creation_time : 2021-11-20T08:07:16.000000Z
com.android.version: 9
Duration: 00:04:59.78, start: 0.000000, bitrate: 575 kb/s
Stream #0:0(eng): Video: h264 (Constrained Baseline) (avc1 / 0x31637661), yuv420p, 1280x720, 478 kb/s, SAR 1:1 DAR 16:9, 6.15 fps, 90k tbr, 90k tbn, 180k tbc (default)
Metadata:
creation_time : 2021-11-20T08:07:16.000000Z
handler_name : VideoHandle
vendor_id : [0][0][0][0]
Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 96 kb/s (default)
Metadata:
creation_time : 2021-11-20T08:07:16.000000Z
handler_name : SoundHandle
vendor_id : [0][0][0][0]
</code></pre>
<p>视频一共有三种流:音频流,视频流,字幕流。我建议还是去看看FFmpeg官网对这几种结构的解释,介绍的挺全的,比我写的强多了。</p>
<p>一般来说一个视频会有一个视频流和多个音频流及多个字幕流。他们的顺序安排按照约定俗成的顺序是第一个是视频流,对应着Stream #0:0;第二个及多个是音频流,对应着Stream#0:1,字幕流按此规则依次展开(当然,在我做ffmpeg的时候,经历过audio和video反了的情况,这种反了情况如果不去做处理ffmpeg其实会智能帮助挑选的,但是在某些时候就会报错失败。所以建议还是按照这个顺序来)。所以,我们可以从上面的代码片段中看到1_1637395707\&amp;88966_1637395335_1637395635.mp4这个mp4文件包含以下信息:</p>
<ul>
<li>视频持续时间 00:04:59.78,开始时间是0.000000,码率是575kb/s</li>
<li>#0:0是视频流(Video),h264编码,profile等级为baseline,视频格式是yuv420p,分辨率1280*720,平均码率478kb/s,帧率 6.15fps,时间基线 90k。(我列举出来的这几个是挺重要的参数,其他参数我还没有太明白)</li>
<li>#0:1是音频流(Audio),aac编码,采样率16000Hz,单声道,码率96kb/s</li>
</ul>
<h1 id="四concat">四、concat</h1>
<p>拼接的意思就是,将两段或者多端音视频片段拼接成一个完整的音视频。ffmpeg官网<a href="https://ffmpeg.org/faq.html#How-can-I-concatenate-video-files_003f">指出</a>,根据不同的情况,有多种方案可以进行选择。</p>
<h2 id="1拼接的3种方案">1、拼接的3种方案</h2>
<h3 id="1concat-filter">(1)concat filter</h3>
<p>过滤器法适用于同步视频和音频流的片段,每个视频段都必须具有相同数量的流(包括类型),而且最终输出的视频也会按照这个顺序进行合并。这种方法会对视频进行重新编码,速度慢,耗cpu和内存。</p>
<p>因为有些流不一定会和别的流的时间一致,举例来说一段视频有1个视频流和2个音频流,其中有一个音频流长度很短。那么这个方法就会按照这个视频最长时间的那个流为基准,其他流就会填补上静音。手册里明确说明最后一个视频不能这样。</p>
<p>如果想让合并如预期那样工作,一定要保证所有视频片段的时间戳都是从0开始的。否则会出现,画面和声音不同步的情况。</p>
<p>合并的时候还会遇到不同视频片段中有不同类型,不同采样率,不同通道的音频流。FFmpeg会自动的选择一个值来进行处理。另外,还有一些值是不能被FFmpeg自动填充的。还是需要人为的指定。另外,如果每段视频片段拥有不同的帧率,我们在最终结果视频制作时一定要设定个值,否则制作出来的视频就会按照每段自己的帧率进行合并了,这样的结果就是视频卡顿。</p>
<pre><code class="language-bash"># ffmpeg -i opening.mkv -i episode.mkv -i ending.mkv -filter_complex \
'[0:0] [0:1] [0:2] [1:0] [1:1] [1:2] [2:0] [2:1] [2:2]
concat=n=3:v=1:a=2 [v] [a1] [a2]' \
-map '[v]' -map '[a1]' -map '[a2]' output.mkv
</code></pre>
<p>这个bash脚本的具体意思,怎么使。我看到了一篇CSDN上的文章,直接在这里做个<a href="https://blog.csdn.net/xiaoluer/article/details/81136478">传送门</a>。</p>
<h3 id="2demuxer">(2)demuxer</h3>
<p>当我们的视频文件不支持文件级别的拼接并且我们想要避免重新编译,就应该采用这种方案。</p>
<p>这种方案,通过读取一个文件里面的视频,将他们一个一个进行拼接。这些视频片段的时间都会被重新调整,第一个视频的开始时间调整为0,后面的视频等待前一个文件拼接完毕再进行拼接。如果他们的流时间长短不一,就会在他们之间产生间隙。</p>
<p>如果他们的duration不同,那就会产生伪影(这个伪影,是翻译软件的翻译,我出现过这个问题,形象讲就是还在说话但是画面不动了。)</p>
<pre><code class="language-bash"># 文件名为file,内容为
file '/mnt/share/file-1.mp4'
file '/mnt/share/file 2.mp4'
file '/mnt/share/file 3.mp4'
# ffmpg -y -f concat -i file output.mp4
</code></pre>
<h3 id="3protocol">(3)protocol</h3>
<p>物理上进行拼接的方案,这个方案支持文件级拼接,也是最快的方案。一定要保证拼接的视频文件格式都一样。我最终就是选择用这个方案来进行拼接的,因为这种方案更稳定,更高效。(当然,如果你每个文件都不一样的结构,那这个方案就不行。)</p>
<pre><code class="language-bas"># ffmpeg -y -i "concat:a.mp4|b.mp4" output.mp4
</code></pre>
<h2 id="2拼接时我遇到的各种问题">2、拼接时我遇到的各种问题</h2>
<h4 id="1-channel-element-10-is-not-allocated">(1) channel element 1.0 is not allocated</h4>
<p>在采用demuxer拼接方案时,遇到了这个错误。错误的根本原因是拼接的视频文件中的流顺序和第一个文件的流顺序不一致导致的。</p>
<p>demuxer的方案,是以第一个文件作为基准的。如果第一个视频的流顺序 #0:0是video,#0:1是audio,那么后面的所有视频文件的流顺序也得是 #0:0是video,#0:1是audio。这种情况是很可能发生的,因为你的输入可能是别人给你的,别人在操作视频的时候因为个人的疏忽或者经验缺失,有可能在制作的过程中将这种顺序弄混。如果你真遇到了这种情况,通过下面的命令,就能将顺序调整成第一个流是video,第二个流是audio。</p>
<pre><code class="language-bash"># ffmpeg -i input.mp4 -y output.mp4
</code></pre>
<h4 id="2non-monotonous-dts-in-output-stream-01-previous-3277744-current-3276712-changing-to-3277745-this-may-result-in-incorrect-timestamps-in-the-output-file">(2)Non-monotonous DTS in output stream 0:1; previous: 3277744, current: 3276712; changing to 3277745. This may result in incorrect timestamps in the output file.</h4>
<p>首先需要了解一下DTS和PTS是什么意思。简单说,PTS就是这一帧应该在什么时候显示,DTS就是这一帧应该在什么时候进行解码。所以,这个问题是有关于帧的时间的报错。我们可以通过以下三篇文章了解到更多。</p>
<p><a href="https://blog.csdn.net/fanyun_01/article/details/89608876">《DTS和PTS的解释》</a>和<a href="https://www.cnblogs.com/linyilong3/p/9940230.html">《图解DTS和PTS》</a>和<a href="https://www.cnblogs.com/qingquan/archive/2011/07/27/2118967.html">《I,P,B帧和PTS,DTS的关系》</a>]</p>
<p>想要理解DTS和PTS还要知道I、B、P帧的概念。上面那篇《图解DTS和PTS》中的图片表达的很清楚了。为了解决数据存储问题在I帧和P帧的基础上增加了B帧的概念。有了B帧以后才又有了DTS的概念,因为B帧和P帧的组合可以用更少的存储量表示出同样数量的I帧的画面。我们可以看到图中的B帧只有小人的身体,B帧通过把I帧没有移动的背景和P帧中运动的小人整合起来,通过DTS来控制实际解压出来的画面顺序:I &gt;P&gt;B,这样就达到了同样是看了3个画面但实际存储量远小于3个I帧的数据量。注意,每帧蓝色的背景表示的就是实际节省下来的数据量。</p>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/08c4b0f98c0610a3e11d18f4082092032801300138a168.png" src="/images/default.jpg" alt="IBP" /></p>
<p>然后,我们得再了解下采样率是什么,有什么用途。我百度了很多关于采样率的文章,感觉大家都在抄互相的东西,让人看的一头雾水。</p>
<p>首先要确定一点,采样率的意义是模拟信号转成数字信号时的参考。因为根据Nyquist和Shannon定理指出,当采样率大于被采信号最大频率的两倍时才能无损的重建原始的模拟信号。如果采样率低的话,就无法从采样信号中重建,或者可能产生混叠的现象。采样率实际上表明了采集的频率,单位是KHZ,意思就是每秒做了1000次采集。例如:音频CD的采样率是44.1khz,这意味着模拟信号每秒采样44100次,就能在数字信号中还原模拟信号。更多内容可以参考这个<a href="https://www.sweetwater.com/insync/7-things-about-sample-rate/">《7 Questions About Sample Rate》</a></p>
<p>所以,问题的关键就是让各文件的采样率一致了。就是因为各文件的采样率不一致导致的,拼接视频时解码的时间3277744之后应该是3277745,但突然变小了 3276712就出现了问题。</p>
<h4 id="3多个mp4文件拼接不成功---found-duplicated-moov-atomfound-duplicated-moov-atom">(3)多个mp4文件拼接不成功 - Found duplicated MOOV Atom.Found duplicated MOOV Atom.</h4>
<p>如果想直接拼接的话,采用concat protocol的方案,最终拼接出来的mp4和第一个视频一模一样。以下两个视频均是同样设备和参数采集的视频a.mp4和b.mp4。两个mp4拼接出来的视频为abd.mp4</p>
<pre><code class="language-bash"># ffprobe a.mp4 -hide_banner
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'a.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf59.4.101
Duration: 00:04:59.78, start: 0.000000, bitrate: 680 kb/s
Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 607 kb/s, 6.15 fps, 6.15 tbr, 82935000.00 tbn, 12.31 tbc (default)
Metadata:
handler_name : VideoHandle
vendor_id : [0][0][0][0]
Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 71 kb/s (default)
Metadata:
handler_name : SoundHandle
vendor_id : [0][0][0][0]
# ffprobe b.mp4 -hide_banner
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'b.mp4':
Metadata:
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
encoder : Lavf59.4.101
Duration: 00:04:59.20, start: 0.000000, bitrate: 653 kb/s
Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 582 kb/s, 4.08 fps, 4.08 tbr, 13713750.00 tbn, 8.16 tbc (default)
Metadata:
handler_name : VideoHandle
vendor_id : [0][0][0][0]
Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 70 kb/s (default)
Metadata:
handler_name : SoundHandle
vendor_id : [0][0][0][0]
# ffmpeg -y -i "concat:a.mp4|b.mp4" abd.mp4
[mov,mp4,m4a,3gp,3g2,mj2 @ 0x7f9575c0fa40] Found duplicated MOOV Atom. Skipped it
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'concat:a.mp4|b.mp4':
Metadata:
encoder : Lavf59.4.101
major_brand : isom
minor_version : 512
compatible_brands: isomiso2avc1mp41
Duration: 00:04:59.78, start: 0.000000, bitrate: 1332 kb/s
Stream #0:0(eng): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 1280x720 [SAR 1:1 DAR 16:9], 607 kb/s, 6.15 fps, 6.15 tbr, 82935000.00 tbn (default)
Metadata:
handler_name : VideoHandle
vendor_id : [0][0][0][0]
Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 71 kb/s (default)
Metadata:
handler_name : SoundHandle
vendor_id : [0][0][0][0]
Stream mapping:
Stream #0:0 -&gt; #0:0 (h264 (native) -&gt; h264 (libx264))
Stream #0:1 -&gt; #0:1 (aac (native) -&gt; aac (native))
Press [q] to stop, [?] for help
[libx264 @ 0x7f95768062c0] using SAR=1/1
[libx264 @ 0x7f95768062c0] using cpu capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX FMA3 BMI2 AVX2
[libx264 @ 0x7f95768062c0] profile High, level 3.1, 4:2:0, 8-bit
[libx264 @ 0x7f95768062c0] 264 - core 164 r3065 ae03d92 - H.264/MPEG-4 AVC codec - Copyleft 2003-2021 - http://www.videolan.org/x264.html - options: cabac=1 ref=3 deblock=1:0:0 analyse=0x3:0x113 me=hex subme=7 psy=1 psy_rd=1.00:0.00 mixed_ref=1 me_range=16 chroma_me=1 trellis=1 8x8dct=1 cqm=0 deadzone=21,11 fast_pskip=1 chroma_qp_offset=-2 threads=18 lookahead_threads=3 sliced_threads=0 nr=0 decimate=1 interlaced=0 bluray_compat=0 constrained_intra=0 bframes=3 b_pyramid=2 b_adapt=1 b_bias=0 direct=1 weightb=1 open_gop=0 weightp=2 keyint=250 keyint_min=6 scenecut=40 intra_refresh=0 rc_lookahead=40 rc=crf mbtree=1 crf=23.0 qcomp=0.60 qpmin=0 qpmax=69 qpstep=4 ip_ratio=1.40 aq=1:1.00
Output #0, mp4, to 'abd.mp4':
Metadata:
compatible_brands: isomiso2avc1mp41
major_brand : isom
minor_version : 512
encoder : Lavf59.4.101
Stream #0:0(eng): Video: h264 (avc1 / 0x31637661), yuv420p(progressive), 1280x720 [SAR 1:1 DAR 16:9], q=2-31, 6.15 fps, 82935000.00 tbn (default)
Metadata:
handler_name : VideoHandle
vendor_id : [0][0][0][0]
encoder : Lavc59.3.102 libx264
Side data:
cpb: bitrate max/min/avg: 0/0/0 buffer size: 0 vbv_delay: N/A
Stream #0:1(eng): Audio: aac (LC) (mp4a / 0x6134706D), 16000 Hz, mono, fltp, 69 kb/s (default)
Metadata:
handler_name : SoundHandle
vendor_id : [0][0][0][0]
encoder : Lavc59.3.102 aac
frame= 1843 fps=191 q=-1.0 Lsize= 23688kB time=00:04:59.84 bitrate= 647.2kbits/s speed=31.1x
video:20986kB audio:2629kB subtitle:0kB other streams:0kB global headers:0kB muxing overhead: 0.311686%
[libx264 @ 0x7f95768062c0] frame I:8 Avg QP:11.52 size:147506
[libx264 @ 0x7f95768062c0] frame P:747 Avg QP:16.26 size: 20112
[libx264 @ 0x7f95768062c0] frame B:1088 Avg QP:22.94 size: 4858
[libx264 @ 0x7f95768062c0] consecutive B-frames: 17.1% 9.7% 8.3% 64.9%
[libx264 @ 0x7f95768062c0] mb I I16..4: 16.2% 26.1% 57.7%
[libx264 @ 0x7f95768062c0] mb P I16..4: 1.4% 2.4% 1.6% P16..4: 19.9% 5.1% 3.8% 0.0% 0.0% skip:65.9%
[libx264 @ 0x7f95768062c0] mb B I16..4: 0.1% 0.3% 0.2% B16..8: 19.3% 2.8% 1.0% direct: 0.9% skip:75.3% L0:52.0% L1:40.5% BI: 7.5%
[libx264 @ 0x7f95768062c0] 8x8 transform intra:41.5% inter:26.0%
[libx264 @ 0x7f95768062c0] coded y,uvDC,uvAC intra: 62.9% 46.9% 24.0% inter: 8.0% 4.5% 0.2%
[libx264 @ 0x7f95768062c0] i16 v,h,dc,p: 15% 24% 13% 47%
[libx264 @ 0x7f95768062c0] i8 v,h,dc,ddl,ddr,vr,hd,vl,hu: 28% 25% 19% 4% 4% 5% 5% 5% 6%
[libx264 @ 0x7f95768062c0] i4 v,h,dc,ddl,ddr,vr,hd,vl,hu: 26% 21% 13% 6% 7% 7% 7% 6% 7%
[libx264 @ 0x7f95768062c0] i8c dc,h,v,p: 51% 19% 21% 8%
[libx264 @ 0x7f95768062c0] Weighted P-Frames: Y:0.0% UV:0.0%
[libx264 @ 0x7f95768062c0] ref P L0: 77.2% 8.7% 10.2% 3.9%
[libx264 @ 0x7f95768062c0] ref B L0: 91.6% 6.8% 1.6%
[libx264 @ 0x7f95768062c0] ref B L1: 97.3% 2.7%
[libx264 @ 0x7f95768062c0] kb/s:573.94
[aac @ 0x7f9576807680] Qavg: 20877.602
</code></pre>
<p>我们注意,其中有个报错是<code>Found duplicated MOOV Atom.</code> ,MOOV Atom是个什么东西呢?我们可以参考这篇文章:<a href="https://zhuanlan.zhihu.com/p/88196225">理解 MPEG 的 moov atom</a>,通过这篇文章我们了解到mp4是个封装容器,一个视频文件只能有一个MOOV Atom,现在两个mp4文件要进行拼接,就会有两个MOOV Atom。所以,ffmpeg就会选择第一个文件作为最终文件直接输出出来,而后面的都会选择跳过。</p>
<p>所以,想用concat方法直接去拼接mp4文件的想法应该立刻停止。我们可以选择其他方案继续进行,比如ts类型文件就是少数可以这么做的类型之一。ts文件是在DVD标准上存储的视频流文件。它使用的是标准的MPEG-2(.MPEG)来压缩视频数据。我们只需要记住ts文件主要存储的是流媒体,流媒体可以直接拼。</p>
<p>最后,我的方案就是:先转成ts文件,然后再按照concat protocol拼接起来。完美,perfect。此种方法耗费cpu资源和少量内存资源。可以说,是最佳方案,而且健壮性很强。我为什么说健壮性很强呢,因为下面的问题。</p>
<h4 id="4使用filter方案无缘无故的停止ffmpeg线程">(4)使用filter方案,无缘无故的停止ffmpeg线程</h4>
<p>之前,我拼接时采用的方案是filter,功能一切正常,直到有一天系统拼接失败数量直线上升,我立刻进行排查。我发现了好多源视频都没有声音,后来跟端上的同学询问,可能是端上加载别的sdk导致音频流丢失了,这就导致了我的拼接系统拼不出来了。因为filter方案,是需要自己map每个视频的声音和视频流的,如果音频流没了的话就会造成意想不到的错误。</p>
<h4 id="5could-not-find-codec-parameters-for-stream-0--xxxx-consider-increasing-the-value-for-the-analyzeduration-and-probesize-options">(5)Could not find codec parameters for stream 0 XXXX Consider increasing the value for the ‘analyzeduration’ and ‘probesize’ options</h4>
<p>ffmpeg在avformat_find_stream_info中会读取一部分源文件的音视频数据,来分析文件信息。其中有两个参数来控制:probesize和analyzeduration。该函数的作用是通过读取一定时间内或一定长度内的字节码流数据来分析码流的基本信息。比如编码信息,时长,码率,帧率等等。如果这两个值设置的过小就会增加此函数的耗时,严重的可能会导致读取数据量不足,从而无法解析出来关键信息。这样就会导致播放失败或者有音频没有视频,有视频没有音频的问题。</p>
<p>我就是因为没有设置这两个值,端上采集的视频由于电视的cpu持续飙高,视频中的关键信息被延迟写入到视频内,我的ffmpeg以默认的值去读取视频而没有获取到关键信息,最终拼接出来的视频有声音但是没有图像。</p>
<h1 id="五anullsrc空源">五、ANULLSRC(空源)</h1>
<p>我需要做一个黑屏没有声音的视频放在拼接视频的最前面,通过查询ffmpeg的手册。用以下方法能够制作出:</p>
<pre><code class="language-bash"># ffmpeg -f lavfi -i color=size=1280x720:rate=25:color=black -f lavfi -i anullsrc=channel_layout=mono:sample_rate=16000 \
# -metadata:s:a:0 language=eng -metadata:s:v:0 language=eng \
# -video_track_timescale 12800k \
# -vcodec h264-acodec aac \
# -threads 1 -t 30 \
# -y abc.mp4;
</code></pre>
<p>这里面有几个参数我需要说明下。</p>
<ul>
<li>-f lavfi 代表输入是一个虚拟输入源。-i 后面的就是这个是什么样的输入源</li>
<li>Language = eng 代表的是元数据中视频或者音频的stream是英文。</li>
<li>-video_track_timescale 设置视频tbn,tbn是容器的时间基准。经过测试,他的作用是如果设置的过大视频就会以几倍速的速度快速播放。如果设置的小就会以原来的几分之一的速度播放。最终,会造成和音频流不同步。</li>
</ul>
<p>因为,空源视频是放在所有视频之前的,所以tbn这个值一定要设置好。因为后面的所有视频都以它为基准进行拼接。</p></content><author><name>张一帆</name></author><category term="tech" /><category term="ffmpeg" /><summary type="html">FFmpeg is the leading multimedia framework, able to decode, encode, transcode, mux, demux, stream, filter and play pretty much anything that humans and machines have created. It supports the most obscure ancient formats up to the cutting edge. No matter if they were designed by some standards committee, the community or a corporation. It is also highly portable: FFmpeg compiles, runs, and passes our testing infrastructure FATE across Linux, Mac OS X, Microsoft Windows, the BSDs, Solaris, etc. under a wide variety of build environments, machine architectures, and configurations.</summary></entry><entry><title type="html">借助DDD思想,实践出质量检测系统</title><link href="https://naffan.cn/tech/2021/09/12/01.html" rel="alternate" type="text/html" title="借助DDD思想,实践出质量检测系统" /><published>2021-09-12T00:00:00+08:00</published><updated>2021-09-12T00:00:00+08:00</updated><id>https://naffan.cn/tech/2021/09/12/01</id><content type="html" xml:base="https://naffan.cn/tech/2021/09/12/01.html"><blockquote>
<p><strong>DDD 是 Domain-Driven Design 的缩写。</strong>其主要的思想是,我们在设计软件时,先从业务出发,理解真实的业务含义,将业务中的一些概念吸收到软件建模中来,避免造出“大而无用”软件。也避免软件设计没有内在联系,否则一团散沙,无法继续演进。</p>
</blockquote>
<h1 id="前言">前言</h1>
<p> 大屏项目借助链家门店中的终端设备(比如电视,pad等),构建智慧门店各场景的解决方案,助力门店业务发展。同时,协同交易签约场景打造居住服务中心智慧标杆,提供全新体验并优化签约流程。</p>
<p> 我们希望借助大屏深挖签约过程中更有价值的需求点,比如在签约过程中会录音录像(经过客业双方授权)。我们借助录制的视频可以保存下来作为客业(客户业主)双方斡旋时的证据,也能联合内部AI能力借助ASR转化来检测签约经理在服务环节中的关键讲解进而提升签约经理的服务质量。于是,我们就对此需求进行了立项,立项通过后进入了开发环节。</p>
<h1 id="项目背景">项目背景</h1>
<p> 首先,简单介绍下项目交互和情况。实际发生签约时,客业及经纪人落座签约室进行签约过程。签约过程包括但不限于接待、斡旋、风险视频播放、起草合同等环节。每个环节的切换都是由签约经理通过电视进行操控的,所以每个环节的打点时间是有的,就可以和录音录像连动起来了。环节如图所示:</p>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/08f786d38c0610acf30518e30620a0032801300138eb8802.png" src="/images/default.jpg" alt="架构图" /></p>
<p> 接着简要描述一下项目需求:</p>
<ul>
<li>一个签约场景需要进行的环节有很多,每个环节内需要做的质量检测条目也很多。</li>
<li>需要一个工作流系统,承接上游下发过来的质检时间点和质检规则。根据质检规则发送给AI侧进行ASR。</li>
<li>需要一个工作流系统,转化上游质检环节的数据,下发给实际处理视频的系统进行ffmpeg处理。</li>
<li>需要一个操作ffmpeg脚本的服务,负责制作音视频。</li>
<li>未来可能会有更多的扩展业务需要进行质量检测。</li>
</ul>
<h1 id="架构思想">架构思想</h1>
<p> 然后,说下项目的架构设计思想。成熟的架构设计应该遵循简单,可复用的原则去设计。我们应该将整个项目优先进行切分,划分出不同的领域去设计。在这里我们可以简单的理解领域为专注的一件事情。在我们的这个项目中就划分出了两个领域,一个是工作流,一个是ffmpeg音视频制作系统。工作流系统专注于将上游业务下发的任务通过转化和编排等步骤切分出不同的底层可理解的任务,如拼接,转码等,再发送给视频处理系统去处理。ffmpeg音视频制作系统关注ffmpeg领域,只关心音视频制作等逻辑。ffmepg系统为底层系统,只对接工作流系统。如果将来接入方越来越多的话,因为只有工作流是对接系统,所以底层的ffmpeg可以与业务解耦,能够提供更灵活的支持。如下图所示:</p>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/08bb85ce8c0610fdec0318a40820f3032801300138c4c102.png" src="/images/default.jpg" alt="项目的架构" /></p>
<h1 id="领域设计实践">领域设计实践</h1>
<p> 从以上的需求描述来看,设计的概念较多,也有一定的业务复杂性,而且此次需求是质检系统从0到1的过程,我们决定使用领域驱动设计的方法论来解决业务复杂性问题,以及未来系统的可扩展性、可维护性。我在其中负责的是底层ffmpeg的服务。</p>
<h2 id="构建统一领域语言">构建统一领域语言</h2>
<p> 由于需求涉及到的很多概念,为保证左后程序运行结果符合业务预期,需要对这些概念进行在限定语义下的无歧义的描述,就是构建统一的领域语言。这个步骤需要项目组全体成员达成共识,项目的正确性依赖于此。基于本次需求,我们规定了以下这些概念的具体含义:</p>
<ul>
<li>任务:指制作视频时的各种动作类型的任务;</li>
<li>动作:指一个任务下需要进行的各种动作;</li>
<li>
<ul>
<li>下载:指从S3上进行下载</li>
<li>上传:指上传至S3(批量同理)</li>
<li>拼接:音视频文件拼接动作</li>
<li>转码:音视频文件的转码动作</li>
<li>剪切:通过业务指定的打点时间,剪切出音视频片段</li>
<li>空源:指一个视频为黑屏,声音为无声的视频或者音频</li>
</ul>
</li>
<li>方案:由一个任务和一组动作组成的集合</li>
<li>匹配:一个任务匹配动作的方案集</li>
<li>ffmpeg命令:通过不同的参数运行ffmpeg可以实现对音视频的操作</li>
</ul>
<h2 id="业务分析与建模">业务分析与建模</h2>
<p> 以上的概念统一有助于接下来要进行的业务分析,在业务分析中主要的关注点在业务的相关概念、隐含概念以及这些概念之间的关系上,这些关系包括依赖、关联、聚合、组合等。</p>
<p> 对于业务分析,可以使用多种方式进行,比如用例分析法、UserStory、事件风暴等。这个具体看团队对于哪种方式比较熟悉。我们采用用例分析法,就是对于业务描述、梳理出各种情况下的用户用例。此次需求中有三个模块的用例:</p>
<ul>
<li>任务模块;</li>
<li>动作匹配配置模块;</li>
<li>动作组装命令策略模块;</li>
</ul>
<p> 这里先来进行动作配置模块的用例分析:</p>
<ul>
<li>生成动作列表,校验任务状态;</li>
<li>生成动作策略方案,从配置列表按照任务进行过滤;</li>
<li>生成动作匹配集,每个任务根据传入的参数筛选匹配符合的动作;</li>
<li>……</li>
</ul>
<p> 具体更多的详细用例这里不再展示,业务用例的列举越全面详细,由它构建的模型就会越准确,在列举用例的过程中要依据产品的PRD(如果有),但不能照搬PRD,要对PRD的内容进行归纳总结抽象,发现其中隐含的模型概念,比如这里的动作匹配策略的概念在PRD中没有显示的表现,但是隐藏在业务执行的过程中。</p>
<p> 当用例列举完善后,需要在这些用例中总结抽象出业务模型以及模型之间的关系。对于分析用例,简单来说有以下三个基本方法论:</p>
<ul>
<li>提取实体,识别名词定位出实体;</li>
<li>添加关联,识别动词添加实体和实体质检的关联;</li>
<li>添加属性,识别形容词添加实体属性;</li>
</ul>
<p> 对于上述过程,它不是一蹴而就的,而是一步一步的发现用例,画出相关的模型图,然后再次发现隐含的用例或模型,修改模型图,它是一个迭代而产生的模型图。在迭代的过程中,依然要对相关模型进行统一语言的构建。</p>
<h3 id="匹配配置模型分析">匹配配置模型分析</h3>
<p> 此项目的难点就是对于音视频处理任务的类型较多,而且每种类型的任务的场景不同,ffmpeg需要采用的命令行参数也不同,涉及到的规则很是复杂。如下面的规则示例:</p>
<ul>
<li>拼接 &amp;&amp; A场景 &amp;&amp; 兼容模式 &amp;&amp; 电视摄像头采集 &amp;&amp; ……</li>
<li>拼接 &amp;&amp; B场景 &amp;&amp; 高画质模式 &amp;&amp; 安防摄像头采集 &amp;&amp; ……</li>
</ul>
<p> 类似这样的规则需求里面有很多种,未来会频繁的修改添加,所以如何对其进行建模使其可配置可扩展至关重要。对与这样的规则,我们把它抽象为规则、条件表达式、ffmpeg命令参数这样的统一概念,一个方案对应一个表达式,一个表达式由一个或者多个ffmpeg命令参数组成。模型图如下所示:</p>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/08a3aad48c0610fb9c05188a0a20ee042801300138fac803.png" src="/images/default.jpg" alt="模型图1" /></p>
<p> 这样的模型可以使ffmpeg命令和方案配置解耦,方便未来扩展更多的ffmpeg命令以及进行方案的配置,条件表达式可以使用方案引擎进行解析。</p>
<h3 id="组装ffmpeg命令策略模型分析">组装ffmpeg命令策略模型分析</h3>
<p> 对于ffmpeg命令策略模型的设计不但依赖上面的匹配配置模型,还要考虑到不同命令参数对最终音视频的影响。目前项目涉及的任务类型只涉及到质检场景的视频拼接、剪切及转码。在未来规划中,还会对其他场景及任务类型的支持,所以这里需要对其进行可扩展的设计:抽象出场景的概念。</p>
<p> 基于以上的模型构建,主要分为三个模块:场景、方案、ffmpeg命令参数。想要完成ffmpeg命令策略模型的设计就需要对这三个模块的信息和算法进行编写,我们还需要在设计的同时让他们保持相互解耦,方便未来对算法进行迭代优化。基于此,整个业务模型图如下所示:</p>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/08e9bad48c0610e6960918a80920f20828013001389cd803.png" src="/images/default.jpg" alt="模型图2" /></p>
<h2 id="领域建模">领域建模</h2>
<p> 基于以上的业务分析与建模、接下来要进行相关领域模型的建模。领域模型构建的关注点主要在于实体、值对象、聚合、领域服务和库的定义。</p>
<ol>
<li>
<p>实体、值对象分析</p>
<p>首先要对业务模型中的每个概念进行领域建模、领域模型中最主要的就是实体和值对象,他们的区别有两点:</p>
<ul>
<li>实体是有唯一ID的,而且在业务中是有生命周期的,就是它会有一些状态变化;</li>
<li>值对象也可以有唯一的ID,但是它是无状态的,是在程序运行过程中是不可变的;</li>
</ul>
<p>根据以上的原则,可以对上述概念进行领域建模。</p>
<ul>
<li>对于任务这个概念,他是有唯一ID的,在任务处理过程中它是会改变状态的,所以它应该是实体。</li>
<li>对于动作这个概念,它是由唯一ID的,在处理过程中它不会改变状态,所以他应该是值对象。</li>
<li>对于场景这个概念,他是有唯一ID的,在任务处理过程中它是不会改变状态的,所以它应该是值对象。</li>
<li>对于匹配来说,有唯一ID,在处理过程中一旦创建就不会改变,构建为值对象。</li>
<li>对于方案来说,同上。</li>
<li>对于方案集来说,在处理过程中它需要维护方案的ffmpeg参数,是否为最优方案等相关状态,所以他是实体。</li>
<li>对于匹配策略来说,有唯一ID,在处理过程中不可变,构建为值对象。对于他所包含的参数规则,筛选规则,方案匹配规则都应该是值对象。</li>
</ul>
</li>
<li>
<p>聚合的划分</p>
<p>聚合这个概念是一个抽象的概念,它在代码中没有具体的类模型去承载,但是聚合的划分对于代码逻辑的内聚性至关重要。聚合可以理解为领域模型之间的一种代码约束,比如不同聚合之间的模型不可以直接调用,需要通过聚合根进行聚合间的交互。</p>
<p>对于上述模型来说,可以看出来有任务、场景、ffmpeg命令策略这三个聚合。其他的模型与这三个聚合一起构成这个ffmpeg模块聚合。对于ffmpeg命令策略这个聚合,它的内部有匹配规则这个聚合,规则由于有多种类型,所以它是一个抽象类;规则聚合包括表达式这个小聚合,表达式聚合又包含了ffmpeg命令聚合</p>
</li>
<li>
<p>工厂、库、领域服务分析</p>
</li>
</ol>
<p> 实体和值对象是领域模型中比较重要的两个模型,剩下的工厂、库、领域服务这些对象是为了维护以上两个模型的生命周期而产生的模型。</p>
<ul>
<li>工厂:正如它的名字,主要维护模型的创建工作。有两种形式来创建工厂,一个是通过构造方法充当模型的工厂方法;如果模型构建比较复杂,就需要创建一个工厂类来承载创建模型的工作。对于上述模型,需要为任务、场景、ffmpeg命令这三个聚合创建工厂类,因为他们够在相对来说比较复杂。</li>
<li>库:库的概念主要是用来调用基础设施层来获取持久化数据以及进行数据持久化。这里服务者需要从第三方接口获取数据,需要构建一个库对象;测录需要从数据库获取,构建一个库对象;</li>
<li>领域服务:对于领域服务,它主要承载聚合之间的交互逻辑,如果发现有一个逻辑设计多个聚合,它没有一个主体,不适合放在其中的摸一个聚合中,这时需要构建一个领域服务来承载这部分逻辑。比如上面木星中的方案生成逻辑,它涉及任务、场景、ffmpeg这三个聚合,所以应为其创建一个领域服务来承载方案生成以及最优化方案的成圣逻辑。对于领域服务,不能随便滥用,不然会变成一个业务逻辑的打你团。</li>
</ul>
<ol>
<li>领域模型</li>
</ol>
<p> 基于以上的分析,我们可以构建出如下的领域模型图:</p>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/0895a3dd8c06108de80518d0062093062801300138e75c.png" src="/images/default.jpg" alt="领域模型" /></p>
<p> 以上模型基本和业务分析出的模型类似,我们为其确立了具体模型归属,划分了相关的聚合,添加了相应的辅助领域模型。在构建这个模型时,依然要随时丰富相关的用例case,不断完善领域模型。</p></content><author><name>张一帆</name></author><category term="tech" /><category term="系统" /><summary type="html">DDD 是 Domain-Driven Design 的缩写。其主要的思想是,我们在设计软件时,先从业务出发,理解真实的业务含义,将业务中的一些概念吸收到软件建模中来,避免造出“大而无用”软件。也避免软件设计没有内在联系,否则一团散沙,无法继续演进。</summary></entry><entry><title type="html">我在贝壳的第一次活水</title><link href="https://naffan.cn/life/2021/06/30/01.html" rel="alternate" type="text/html" title="我在贝壳的第一次活水" /><published>2021-06-30T00:00:00+08:00</published><updated>2021-06-30T00:00:00+08:00</updated><id>https://naffan.cn/life/2021/06/30/01</id><content type="html" xml:base="https://naffan.cn/life/2021/06/30/01.html"><blockquote>
<p>得志不得意,失意不失志</p>
</blockquote>
<p>随着4月的晋升失败,给我带来的是又一次打击。本来2020年10月的第一次晋升失败以后,经过2个月在java的项目上的锤炼,我又建立起了很足的信心。因为,在2020年底我自己一人做出的项目平均每天的请求量在80万,qps50。</p></content><author><name>张一帆</name></author><category term="life" /><category term="链家,活水" /><summary type="html">得志不得意,失意不失志</summary></entry><entry><title type="html">拿饭网对java的classLoader分析</title><link href="https://naffan.cn/tech/2021/06/15/01.html" rel="alternate" type="text/html" title="拿饭网对java的classLoader分析" /><published>2021-06-15T00:00:00+08:00</published><updated>2021-06-15T00:00:00+08:00</updated><id>https://naffan.cn/tech/2021/06/15/01</id><content type="html" xml:base="https://naffan.cn/tech/2021/06/15/01.html"><ul>
<li>
<h1 id="类加载机制">类加载机制</h1>
</li>
</ul>
<p> 虚拟机把描述类的数据从class文件加载到内存,并且进行校验、解析、初始化。最终形成可以直接使用的Class对象,这就是类加载机制。</p>
<p> 类加载并不是一次性把所有class文件都加载到JVM中的,而是按照需求来加载的。比如,JVM启动时,会通过不同的类的加载器加载不同的类。当用户在自己代码中,需要额外的类时,再通过加载机制加载到JVM中,并且存放一段时间,便于频繁使用。</p>
<ul>
<li>全盘委托,当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示的使用另外一个类加载器加载。</li>
<li>父类委托,先让父类加载器试图加载该类,只有在父类加载器无法加载该类时才尝试从自己的类路径中加载该类。</li>
<li>缓存机制,缓存机制将会保证所有加载过的Class都会被缓存,当程序中需要使用某个Class时,类加载器先从缓存区寻找该Class,自由缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM的原因。</li>
</ul>
<hr />
<ul>
<li>
<h1 id="类加载过程">类加载过程</h1>
</li>
</ul>
<p> 类从被加载到JVM内存开始,到卸载出内存为止,它的整个生命周期包括:加载、验证、准备、解析、初始化、使用、卸载,共7个阶段。其中验证、准备、解析3个部分统称为连接。这7个阶段的发生顺序如图:</p>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/raw/p/image-distinguish/08dca7e7850610aef604188e0c20fa042801300138b2e403.jpeg" src="/images/default.jpg" alt="类的生命周期" /></p>
<p> 其中<code>类加载的过程包括了加载、验证、准备、解析、初始化五个阶段。</code>在这五个阶段中,加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段则不一定,它在某些情况下可以在初始化阶段之后开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。另外注意这里的几个阶段是按顺序开始,而不是按顺序进行或完成,因为这些阶段通常都是互相交叉地混合进行的,通常在一个阶段执行的过程中调用或激活另一个阶段。</p>
<p> 下面详细介绍每个阶段所做的事情:</p>
<hr />
<ul>
<li>
<h1 id="加载">加载</h1>
</li>
</ul>
<p> 加载时类加载过程的第一个阶段,在加载阶段,JVM需要完成以下三件事情:</p>
<ol>
<li>通过一个类的全限定名来获取其定义的二进制字节流。</li>
<li>将这个字节流所代表的的静态存储结构转化为方法区的运行时数据结构。</li>
<li>在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口</li>
</ol>
<p> 第一条中的二进制字节流并不只是单纯的从class文件中获取,比如它还可以从Jar包中获取、从网络中获取(最典型的应用便是Applet)、由其他文件生成(JSP应用)等。</p>
<p> 相对于类加载的其他阶段而言,加载阶段是可控性最强的阶段,因为开发人员既可以使用系统提供的类加载器来完成加载,也可以自定义自己的类加载器来完成加载。</p>
<p> 加载完成以后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在<code>方法区</code>之中,而且在java堆中也会创建一个java.lang.Class类的对象,这样便可以通过对象访问方法区中的这些数据。</p>
<p> 类加载器分为以下三类:</p>
<ol>
<li>启动类加载器:Bootstrap ClassLoader,它负责加载放在JDK/jre/lib下,或者被-Xbootclasspath参数指定的路径中的,并且被虚拟机是别的类库。启动类加载器是无法被java程序直接启动的。它是C++实现的,是虚拟机的一部分。</li>
<li>扩展类加载器:Extension ClassLoader,该加载器由sun.misc.launCher$ExtClassLoader实现,它负责加载JDK/jre/lib/ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用扩展类加载器。</li>
<li>应用程序类加载器:Application ClassLoader,该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。</li>
</ol>
<p> 应用程序都是由这三种加载器相互配合进行加载的,如果有必要程序员还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做做到:</p>
<ol>
<li>在执行非置信代码之前,自动验证数字签名。</li>
<li>动态地创建符合用户特定需要的定制化构建类。</li>
<li>从特定的场所取得java class,例如数据库中或者网络中。</li>
</ol>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/raw/p/image-distinguish/0888e8e7850610f18c0118c10220c5032801300138ae1c.png" src="/images/default.jpg" alt="双亲委派模型" /></p>
<p> 这种层次关系称为类加载器的<code>双亲委派模型</code>。他们之间的父子关系并不是通过继承关系来实现的,而是使用组合关系来复用父加载器中代码。这不是一个强制性的约模型,而是java设计者们推荐给开发者的一种类的加载器实现方式。</p>
<p> 双亲委派的工作流程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把请求委托给父加载器去完成,依次向上,因此,所有的类加载请求最终都应该被传递到顶层的启动类加载器中,只有当父加载器在它搜索范围中没有找到所需的类时,子加载器才会去尝试自己去完成加载。</p>
<p><code>为什么要使用这种模式呢?</code>因为java中的类随着它的类加载器一起具备了一种带有优先级的层级关系。这样的好处是,避免了循环引用,而可以一直溯源到最父类。例如,java.lang.Object,他存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型顶端的启动类加载类进行加载,因此Object类在程序的各种类加载器环境中都是能够保证是同一个类。同时,也防止了内存中出现同样的字节码。</p>
<hr />
<ul>
<li>
<h1 id="举例说明">举例说明</h1>
</li>
</ul>
<pre><code class="language-java">Dog dog = new Dog();
</code></pre>
<p>由new关键字创建一个类的实例。这个动作会导致常量池的解析,Dog类被隐式装在。如果当前ClassLoader无法找到Dog,则抛出NoClassDefFoundError</p>
<pre><code class="language-java">try{
Class clazz = Class.forName("Dog");
Object dog = clazz.newInstance();
}catch (Exception e){
System.out.println(e.getMessage());
}
</code></pre>
<p>通过反射加载类型并创建对象实例。如果无法找到Dog,则抛出ClassNotFoundException</p>
<pre><code class="language-java">try{
ClassLoader cl = new ClassLoader() {
@Override
public Class&lt;?&gt; loadClass(String name) throws ClassNotFoundException {
return super.loadClass(name);
}
};
Class clazz = cl.loadClass("Dog");
Object dog = clazz.newInstance();
}catch (Exception e){
System.out.println(e.getMessage());
}
</code></pre>
<p>通过反射加载类型并创建对象实例。如果无法找到Dog,则抛出ClassNotFoundException</p>
<p><code>上面三种有什么区别呢?分别用于什么情况呢?</code></p>
<p>1和2使用的类加载器是相同的,都是当前类加载器(this.getClass.getClassLoader)。3是用户指定的类加载器。如果需要在当前类路径以外寻找类,则只能用第3种方式。第3种方式加载的类与当前类分属不同的命名空间。当前类加载器命名空间对其不可见。当然,如果被加载类的超类对于当前类命名空间可见的话,则可以进行强制转型。第1种抛出error,第2,3种抛出Exception。</p>
<hr />
<ul>
<li>
<h1 id="jdk9的双亲委派模式">JDK9的双亲委派模式</h1>
</li>
</ul>
<p>JDK9为了模块化的支持,对双亲委派模式租了一些改动:</p>
<ol>
<li>扩展类加载器被平台类加载器(Platform ClassLoader)取代,原来的rt.jar和tools.jar被拆分成数十个JMOD文件。</li>
<li>
<p>平台类加载器和应用程序类加载器都不再继承自java.netURLClassLoader。现在启动类加载器、平台类加载器、应用程序类加载器全都继承于jdk.internal.loader.BuiltinClassLoader。</p>
</li>
<li>启动类加载器现在是在java虚拟机内部和java类库共同协作实现的类加载器(以前是C++实现)。为了与之前的代码保持兼容,所有在获取启动类加载器的场景中仍然会返回null来代替,而不会得到BootClassLoader的实例。</li>
<li>类加载的委派关系也发生了变动,当平台及应用程序类加载器收到类加载请求,在委派给父加载器加载前,要先判断该类是否能够归属到某一个系统模块中,如果可以找到这样的归属关系,就要优先委派给负责哪个模块的加载器完成加载。</li>
</ol>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/raw/p/image-distinguish/08d3bea0860610a791181880082080062801300138d28f01.jpeg" src="/images/default.jpg" alt="jdk9双亲委派模型" /></p></content><author><name>张一帆</name></author><category term="tech" /><category term="java" /><summary type="html">类加载机制</summary></entry><entry><title type="html">赛格广场晃动,原因竟然是共振</title><link href="https://naffan.cn/thinking/2021/05/22/01.html" rel="alternate" type="text/html" title="赛格广场晃动,原因竟然是共振" /><published>2021-05-22T00:00:00+08:00</published><updated>2021-05-22T00:00:00+08:00</updated><id>https://naffan.cn/thinking/2021/05/22/01</id><content type="html" xml:base="https://naffan.cn/thinking/2021/05/22/01.html"><blockquote>
<p>赛格广场,位于深圳市交通干道深南中路与华强北路交汇处,由深圳赛格集团投资兴建,是深圳市跨世纪的标志性建筑。总高355.8米,总建筑层79层,地上75层,地下4层,总建筑面积达17万平方米。</p>
</blockquote>
<p><img class="lazy post-img" data-original="https://nimg.ws.126.net/?url=http%3A%2F%2Fdingyue.ws.126.net%2F2021%2F0519%2F79a605c9j00qtcuwt00a4c0010400r4m.jpg&amp;thumbnail=650x2147483647&amp;quality=80&amp;type=jpg" src="/images/default.jpg" alt="共振例图" /></p>
<ul>
<li>
<p>2021年5月18日,深圳市福田区华强北街道赛格大厦出现摇晃,大厦已经封闭,大厦内人员已全部撤出</p>
</li>
<li>
<p>2021年5月19日,大楼在下午一点半到两点之间出现过晃动。目前大厦原则上只能让商家等内部工作人员出入。</p>
</li>
<li>
<p>2021年5月20日12时30分左右在35楼、55楼、60楼等多个楼层感受到晃动。</p>
</li>
<li>
<p>2020年5月21日,深圳市赛格集团有限公司的通知显示自21日起暂停所有业主、商户、租户进出赛格大厦写字楼和电子市场,待相关检测工作完成后再有序开放,有关事宜另行通知。</p>
</li>
</ul>
<p> 最近,赛格大厦晃动的事件在民间讨论的沸沸扬扬,一石激起千层浪,媒体的曝光更加让本来疑云密布的事件罩上了更多的阴谋论。其中一家媒体爆料说是20年前就有一篇<a href="https://baijiahao.baidu.com/s?id=1700150908927741800&amp;wfr=spider&amp;for=pc">《深圳赛格广场建设项目评析》</a>的硕士论文中指出,大厦是边设计边施工。这位硕士学生是一名华中科技大学非建筑学科且并未参与过赛格广场建设的学生,她的名字叫金典琦。此文章一出各种小道每天就开始各种添油加醋,疯狂的不分青红皂白的就开始说赛格大厦是豆腐渣工程,是中国体系内的不负责任的典型。那么事情真的是这样么?我在网上翻到了当时这名学生的论文并且通读以后得出了结论:</p>
<p> 金典琦是学工商管理的在职硕士,全程参与了赛格大厦建设的管理工作。对赛格大厦建造过程中选取的技术和解决方案以及施工方的预算和组织架构有充足的了解和深入的分析。对于建设过程中遇到的问题和解决方案有自己独有的理解和路径分析,形成了自己理论体系内的底层逻辑,逻辑能够自洽,对当时的市场分析和市场判断有比较全面的认知和实践。论文中也给出了详细的问题列表和抓手,对待施工时施工单位采用的方案有给出追种方案的比较并得到了学界内权威人士的认可与支持。</p>
<p> 论文中两次提到赛格大厦晃动的经历,一次为大厦顶部的两根天线设计不合理并重新进行了调整;一次为95年因大厦耸入云间,由于云彩移动导致广场上的人们误以为大厦要倾倒的趣事。还有一次提到了共振问题导致晃动。一次边设计边建造的问题根源。</p>
<p> 综上所述,论文给出了客观且符合当事人认知的合理判断与真实建造情况。所以,对于这些媒体给出的偏颇结论我认定为是媒体的误导及看到这些文章后唯恐天下不乱的人们不经过自己调研后就轻易下结论的事实。</p>
<p> 接下来,让我们的目光集中在<code>共振</code>这一物理名词上。首先让我们看看bilibili上科普博主用通俗的语言讲的共振的机制和影响吧。</p>
<div align="middle">
<iframe src="//player.bilibili.com/player.html?aid=714516015&amp;bvid=BV1qX4y1V7cE&amp;cid=308414800&amp;page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true"> </iframe>
</div>
<p><img class="lazy post-img" data-original="https://5b0988e595225.cdn.sohucs.com/images/20180826/2e7857ed9bfb4122aaf35d697dbbf8b3.jpeg" src="/images/default.jpg" alt="共振例图" /></p>
<blockquote>
<p>三百多年前,荷兰科学家惠更斯(Christian Huygens)在房间里的墙上并排放置不同速率的摆钟。当他第二天再回来时,发现这几个摆钟的钟锤都以同速率同步摆动。最终根据这一现象惠更斯写出了《摆钟轮》(Horologium Oscillatorium),摆钟现象被后来许多人相继重复实验,实际上这就是今天说的共振。</p>
</blockquote>
<p><img class="lazy post-img" data-original="https://5b0988e595225.cdn.sohucs.com/images/20180826/dfce68126e974af08a03e71a2b578caa.gif" src="/images/default.jpg" alt="共振动图" /></p>
<p> 让我们来看看数学上是如何解释共振机理的吧,出自《数学指南》,科学出版社出版:</p>
<p><img class="lazy post-img" data-original="https://pic2.zhimg.com/v2-afd3f984658ef92358a21e3245278824_r.jpg?source=1940ef5c" src="/images/default.jpg" alt="共振机理1" /></p>
<hr />
<p><img class="lazy post-img" data-original="https://pic1.zhimg.com/v2-e531f3ecae8171692340dd76189c3fe9_r.jpg?source=1940ef5c" src="/images/default.jpg" alt="共振机理2" /></p>
<hr />
<p><img class="lazy post-img" data-original="https://pic1.zhimg.com/v2-062748cdc1ecca03d291bdcb76c6b5aa_r.jpg?source=1940ef5c" src="/images/default.jpg" alt="共振机理3" /></p>
<hr />
<p> 通过上面的数学方程式,我们了解到。如果我们一直给弹簧施加一个周期性的力,当力的频率等于弹簧固有震频时,力的作用方向和振子运动方向一直保持一致。这就意味着,力对振子做的功永远为正,于是振子的能量越来越大,对物体内部结构的破坏就越来越大,最终会导致物体被摧毁。</p>
<p> 2021年特斯拉上架了ModelY,当发售两个月后车主就曝出了modelY低频共振的问题。据说,很多车主都遇到过这个问题,并且有些车主花钱去做了<a href="https://club.autohome.com.cn/bbs/thread/870af073c080e909/94575641-1.html">后备箱隔音</a>,但仍于事无补。据业内人士分析,ModelY的低频噪音多来自尾部,或许是因为车辆行驶过程中的噪音通过底盘传至车身,由于后尾门存在空腔,且钢板较薄,很容易因为刚性不足而放大噪音,从而产生低频共振的现象。这个就是通过后备箱的声音传过来的声波和车主的耳膜一致造成了共振现象,当振幅越来越大时就会给车主的耳朵造成不可逆的创伤。</p>
<p> 在我弹吉他的时候,每次拿出来吉他都要进行调音测试,为了让每条弦能够弹出准确的声音。调音手段中有一个利用共振原理来进行调弦的方法。就是波动一条弦后,其他弦的空弦如果与这条弦的声调一致就会跟着颤抖起来。</p>
<p> 在航天航空中,宇航员升天时会与宇宙飞船产生共振。我们国家英雄杨利伟就曾说:“2003年10月15日上午9时整,火箭尾部发出巨大的轰鸣声,几百吨高能燃料开始燃烧,8台发动机同时喷出炽热的火焰,高温高速的气体,几秒钟就把发射台下的上千吨水化为蒸汽。火箭和飞船总重达到487吨,当推力让这个庞然大物升起时,大漠颤抖、天空轰鸣。火箭逐步地加速,我感到压力在渐渐增加。因为这种负荷我们训练时承受过,我的身体感受还挺好,觉得没啥问题。但就在火箭上升到三四十公里的高度时,火箭和飞船开始急剧抖动,产生了共振。这让我感到非常痛苦。人体对10赫兹以下的低频振动非常敏感,它会让人的内脏产生共振。而这时不单单是低频振动的问题,还是这个新的振动要叠加在大约6G的一个负荷上。这种叠加太可怕了,我们从来没有进行过这种训练。我担心的意外还是发生了。”</p>
<p> 在现实生活中,我们周边还会有许许多多的共振现象。要解决共振其实很简单,只要找到物体共振频率,在此频率上减小激励力就能够避免共振了。对于高楼大厦来说,一般都是用类似阻尼器的装置来减小共振。</p></content><author><name>张一帆</name></author><category term="thinking" /><category term="共振" /><summary type="html">赛格广场,位于深圳市交通干道深南中路与华强北路交汇处,由深圳赛格集团投资兴建,是深圳市跨世纪的标志性建筑。总高355.8米,总建筑层79层,地上75层,地下4层,总建筑面积达17万平方米。</summary></entry><entry><title type="html">敬给我最尊重的老板,贝壳董事长-左晖</title><link href="https://naffan.cn/life/2021/05/20/01.html" rel="alternate" type="text/html" title="敬给我最尊重的老板,贝壳董事长-左晖" /><published>2021-05-20T00:00:00+08:00</published><updated>2021-05-20T00:00:00+08:00</updated><id>https://naffan.cn/life/2021/05/20/01</id><content type="html" xml:base="https://naffan.cn/life/2021/05/20/01.html"><blockquote>
<p>我们这个时代企业经营者的宿命,就是要去干烟花背后的真正提升基础服务品质的苦活、累活。</p>
</blockquote>
<p><img class="lazy post-img" data-original="https://pic3.zhimg.com/80/v2-929a9e08b764aafea5ab50701fc66f82_720w.jpg" src="/images/default.jpg" alt="左晖" /></p>
<p> 上面是我们公司的老板说的话,他的精神永远激励着我们,我们将矢志不渝的坚持长期主义,团结一心为新居住产业发展做难而正确的事!</p>
<p> 我很早就把我的行业固定在了中国的房地产行业之中,我又是从事互联网的技术人员。所以很自然的就干上了产业互联网之事。而我选择链家就是因为我在见证了链家这20年来的发展,坚定了链家作为中国(将来)最好的企业之一的信念以及我对中国房地产相关产业的看好。</p>
<p> 我是18年8月入职贝壳的,其实在四月份我就来链家面过试。那个时候福道大厦还不是贝壳的logo呢。我对链家是有一种执着的信念的,我愿意追随这样一位好领导,干有意义且难但正确的事情。入职贝壳以来,我专心落地到了为左晖先生提的”做有尊严的服务者“这句话的意义上奉献,长达3年的贝壳工作,我一直服务于链家的经纪人产品。</p>
<p> 经过这几年的工作,从入职php,到改为写go,再到现在的java。感觉这几年对语言的转型让我对语言已经有深刻的理解了。目前我正在做自己负责的业务,并且一直致力于学习。在2020年10月,和21年4月,我得到了部门给我的晋升机会。虽然通过自己的努力,最终没有晋级成功。但是,两次折磨的过程让我重新认识到了我目前缺乏的东西,第一次是技术,但是在第二次评委已经不说我技术问题了,而是产品高度。在接下来的工作和生活中,我将以提高我的这个能力为前提努力的认真工作。左晖先生的精神,会激励着我,一直向前。</p>
<p> 今天下午3点,在得知左晖先生离开后,我心情难以平复,本来未来可期,本来还有一番事业等待着他带领我们一起航行,可是就这样戛然而止,希望左晖先生一路走好,后面还有大S帮您完成您的愿望,我们会一起努力!</p></content><author><name>张一帆</name></author><category term="life" /><category term="链家,癌症" /><summary type="html">我们这个时代企业经营者的宿命,就是要去干烟花背后的真正提升基础服务品质的苦活、累活。</summary></entry><entry><title type="html">redis连接池源码分析</title><link href="https://naffan.cn/tech/2021/05/15/01.html" rel="alternate" type="text/html" title="redis连接池源码分析" /><published>2021-05-15T00:00:00+08:00</published><updated>2021-05-15T00:00:00+08:00</updated><id>https://naffan.cn/tech/2021/05/15/01</id><content type="html" xml:base="https://naffan.cn/tech/2021/05/15/01.html"><p> 我们在开发的时候经常会用到redis。java中首选的redis客户端是jedis。jedis封装了redis的所有功能,并且提供了更多额外好用的功能。</p>
<p> 你在开发时,可以通过jedis连接redis。jedis支持直连模式和连接池模式。</p>
<ul>
<li>直连模式:</li>
</ul>
<pre><code class="language-java"> public static void main(String[] arg){
//1个实例
Jedis jedis = new Jedis("127.0.0.1",6379,100);
jedis.incr("threadSafe");
jedis.close();
}
</code></pre>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/089f91fe840610d38104189b092092042801300138f312.png" src="/images/default.jpg" alt="直连" /></p>
<p> 你不应该将一个redis实例共享给不同线程,时用时消的用法会造成产生很多的socket和tcp连接,即浪费时间又耗费资源,也提高了服务不可用的风险。如果你只是在本机环境或者使用者相对少的环境中使用,这种模式比较适宜。但是,如果你需要大量使用redis时,应该采用连接池模式。</p>
<ul>
<li>连接池模式:</li>
</ul>
<pre><code class="language-java"> public static void main(String[] arg){
JedisPool pool = new JedisPool(new JedisPoolConfig(),"localhost");
try (Jedis jedis = pool.getResource()) {
jedis.set("foo", "bar");
String foobar = jedis.get("foo");
jedis.zadd("sose", 0, "car"); jedis.zadd("sose", 0, "bike");
Set&lt;String&gt; sose = jedis.zrange("sose", 0, -1);
}
pool.close();
}
</code></pre>
<p><img class="lazy post-img" data-original="https://p.ljcdn.com/v1/hdic.t_m/p/image-distinguish/08af91fe84061087a50a189b09208b052801300138b9f703.png" src="/images/default.jpg" alt="连接池" /></p>
<p> 为了避免由单一redis实例引发的未知的服务不可用的风险,我们应该采用这种方案。这种方案背后的原理就是连接池(common-pool2)。当服务到达时服务会从连接池中borrow一个jedis连接,当用完或者发生错误时,连接会归还到连接池中。如果没有borrow到连接,那么服务就会报错且关闭。</p>
<p> 我的上一篇<a href="https://www.naffan.cn/tech/2021/05/12/01.html">池化技术</a>已经提到过连接池的概念,所以这篇文章是我想记录并分享我所学习到的jedis连接池的知识。通过上一段代码我们看到当jedisPool实例化以后,我们从pool中通过getResource来获取一个jedis实例。那么我们的代码研究就从这里开始。</p>
<pre><code class="language-java"> //jedis-3.6.0.jar/redis/clients/jedis/jedisPool.class
public Jedis getResource() {
//调用父集getResource
Jedis jedis = (Jedis)super.getResource();
//将当前实例暴露出dataSource,供后续redis操作使用
jedis.setDataSource(this);
return jedis;
}
</code></pre>
<pre><code class="language-java"> //jedis-3.6.0.jar/redis/clients/jedis/client/util/Pool.class
//genericObjectPool是ObjectPool的实现,ObjectPool是common.pool2中关于池化对象的接口。其中定义了几个标准的对象方法,这些方法就是管理池的核心方法。
protected GenericObjectPool&lt;T&gt; internalPool;
……
public T getResource() {
try {
//通过调用common.pool2中的borrowObject,完成对jedis实例的借取。
return this.internalPool.borrowObject();
//如果出现了异常了,按照异常的分类进行处理。
} catch (NoSuchElementException var2) {
if (null == var2.getCause()) {
throw new JedisExhaustedPoolException("Could not get a resource since the pool is exhausted", var2);
} else {
throw new JedisException("Could not get a resource from the pool", var2);
}
} catch (Exception var3) {
throw new JedisConnectionException("Could not get a resource from the pool", var3);
}
}
</code></pre>
<p> 在common-pool2中,对象池的核心接口叫做ObjectPool,他定义了对象池的实现的行为。</p>
<ol>
<li>addObject方法:往池中添加一个对象。池子里的所有对象都是通过这个方法进来的。</li>
<li>borrowObject方法:从池中借走到一个对象。借走不等于删除。对象一直都属于池子,只是状态的变化。</li>
<li>returnObject方法:把对象归还给对象池。归还不等于添加。对象一直都属于池子,只是状态的变化。</li>
<li>invalidateObject:销毁一个对象。这个方法才会将对象从池子中删除,当然这其中最重要的就是释放对象本身持有的各种资源。</li>
<li>getNumIdle:返回对象池中有多少对象是空闲的,也就是能够被借走的对象的数量。</li>
<li>getNumActive:返回对象池中有对象对象是活跃的,也就是已经被借走的,在使用中的对象的数量。</li>
<li>clear:清理对象池。注意是清理不是清空,改方法要求的是,清理所有空闲对象,释放相关资源。</li>
<li>close:关闭对象池。这个方法可以达到清空的效果,清理所有对象以及相关资源。</li>
</ol>
<p> 在common-pool2中,objectPool的核心实现类就是GenericObjectPool。</p>
<pre><code class="language-java"> //commons-pool2.jar/org/apache/commons/pool2/impl/GenericObjectedPool
@Override
public T borrowObject() throws Exception {
//getMaxWaitMillis()是BaseGenericObjectPool中设定的volatile类型的值,代表最长等待时间(毫秒),配置文件中的"maxWait"
return borrowObject(getMaxWaitMillis());
}
</code></pre>
<p> 接下来我们来分析下borrowObject方法,刚才说过borrowObject是实现了ObjectPool。那么先看一下这个接口中对borrowObject的描述。</p>
<pre><code class="language-java"> /**
* Obtains an instance from this pool.
* &lt;p&gt;
* Instances returned from this method will have been either newly created
* with {@link PooledObjectFactory#makeObject} or will be a previously
* idle object and have been activated with
* {@link PooledObjectFactory#activateObject} and then validated with
* {@link PooledObjectFactory#validateObject}.
* &lt;/p&gt;
* &lt;p&gt;
* By contract, clients &lt;strong&gt;must&lt;/strong&gt; return the borrowed instance
* using {@link #returnObject}, {@link #invalidateObject}, or a related
* method as defined in an implementation or sub-interface.
* &lt;/p&gt;
* &lt;p&gt;
* The behavior of this method when the pool has been exhausted
* is not strictly specified (although it may be specified by
* implementations).
* &lt;/p&gt;
*
* @return an instance from this pool.
*
* @throws IllegalStateException
* after {@link #close close} has been called on this pool.
* @throws Exception
* when {@link PooledObjectFactory#makeObject} throws an
* exception.
* @throws NoSuchElementException
* when the pool is exhausted and cannot or will not return
* another instance.
*/
/*
这个method返回实例,这个实例将是一个被makeObject()创建的对象,或者是一个之前就是idle并且经过activateObject()激活过的并且经过validatedObject()验证过的对象。
根据约定,客户端必须调用过returenObject(),invalidateObject()或者一个在子类中实现了归还逻辑的方法后归还。
这个方法在池子耗尽时的表现没有指明具体如何处理(尽管他有可能被他的实现制定过。)
*/
T borrowObject() throws Exception, NoSuchElementException,
IllegalStateException;
</code></pre>
<p> 接着我们再来看看这个方法的具体实现。</p>
<pre><code class="language-java"> //commons-pool2.jar/org/apache/commons/pool2/impl/GenericObjectPool.class
/**
* Equivalent to &lt;code&gt;{@link #borrowObject(long)
* borrowObject}({@link #getMaxWaitMillis()})&lt;/code&gt;.
* &lt;p&gt;
* {@inheritDoc}
* &lt;/p&gt;
*/
//通过获取配置中MaxWait配置,当做传输调用重载的方法。
@Override
public T borrowObject() throws Exception {
return borrowObject(getMaxWaitMillis());
}
/**
* Borrows an object from the pool using the specific waiting time which only
* applies if {@link #getBlockWhenExhausted()} is true.
* &lt;p&gt;
* If there is one or more idle instance available in the pool, then an
* idle instance will be selected based on the value of {@link #getLifo()},
* activated and returned. If activation fails, or {@link #getTestOnBorrow()
* testOnBorrow} is set to {@code true} and validation fails, the
* instance is destroyed and the next available instance is examined. This
* continues until either a valid instance is returned or there are no more
* idle instances available.
* &lt;/p&gt;
* &lt;p&gt;
* If there are no idle instances available in the pool, behavior depends on
* the {@link #getMaxTotal() maxTotal}, (if applicable)
* {@link #getBlockWhenExhausted()} and the value passed in to the
* {@code borrowMaxWaitMillis} parameter. If the number of instances
* checked out from the pool is less than {@code maxTotal,} a new
* instance is created, activated and (if applicable) validated and returned
* to the caller. If validation fails, a {@code NoSuchElementException}
* is thrown.
* &lt;/p&gt;
* &lt;p&gt;
* If the pool is exhausted (no available idle instances and no capacity to
* create new ones), this method will either block (if
* {@link #getBlockWhenExhausted()} is true) or throw a
* {@code NoSuchElementException} (if
* {@link #getBlockWhenExhausted()} is false). The length of time that this
* method will block when {@link #getBlockWhenExhausted()} is true is
* determined by the value passed in to the {@code borrowMaxWaitMillis}
* parameter.
* &lt;/p&gt;
* &lt;p&gt;
* When the pool is exhausted, multiple calling threads may be
* simultaneously blocked waiting for instances to become available. A
* "fairness" algorithm has been implemented to ensure that threads receive
* available instances in request arrival order.
* &lt;/p&gt;
*
* @param borrowMaxWaitMillis The time to wait in milliseconds for an object
* to become available
*
* @return object instance from the pool
*
* @throws NoSuchElementException if an instance cannot be returned
*
* @throws Exception if an object instance cannot be returned due to an
* error
*/
/*
当getBlockWhenExhausted()返回true时,需要提供等待时间从对象池中借出对象,
如果有1个或多个空闲实例的话,就通过getLifo()方法,也就是先进先出的策略选择由哪个空闲实例承接工作。如果激活失败了,或者testOnBorrow设置为true并且检测失败了,这个实例就会销毁,接下来由下一个可用实例进行承接。这个流程一直持续,直到没有可用的实例可供使用或者没有空闲实例可供使用。
如果在对象池中没有空闲实例了,接下来的走向取决于maxTotal,getBlockWhenExhausted()和传进来的等待时间参数决定。如果当前对象池中的实力数量小于maxtotal,一个新的实例将被创建,激活并且检测最终返回给调用者。如果检测失败,一个NoSuchElementException异常将被抛出。
如果对象池已经占满了(也就是说没有可用的空闲实例并且没有容量可以被创建),这个方法要不堵塞(如果getBlockWhenExhausted设置为true的情况下才会堵塞)要不就抛出一个NoSuchElementException(相反getBlockWhenExhausted为false的情况)异常。具体阻塞的时间跟传入的borrowMaxWaitMillis时间大小有关。(还记得刚才说的maxWait的设置么?)
当对象池已经耗尽,多个调用线程将被同时堵塞,他们一起等待可用的jedis实例接客。接客的规则采用公平算法。其实就是先到先被接~哈哈
*/
public T borrowObject(final long borrowMaxWaitMillis) throws Exception {
//这一步就是检查一下对象池的状态,如果之前已经关闭了,就会抛出IllegalStateException异常。
assertOpen();
//移除已经被抛弃的实例
final AbandonedConfig ac = this.abandonedConfig;
if (ac != null &amp;&amp; ac.getRemoveAbandonedOnBorrow() &amp;&amp;
(getNumIdle() &lt; 2) &amp;&amp;
(getNumActive() &gt; getMaxTotal() - 3) ) {
/*
被抛弃的实例不为null
borrow时去除被抛弃的实例
空闲线程实例&lt;2
当前活动的实例数量大于最大实例数量-3
*/
removeAbandoned(ac);
}
//定义一个PooledObject类型的对象,T是真正的实例对象。如jedis,db等。
PooledObject&lt;T&gt; p = null;
// Get local copy of current config so it is consistent for entire
// method execution
/*
获取一个本地的拷贝,这个拷贝是目前的配置。这样就能够保证整体的一致性。
这个getBlockWhenExhausted()最终是获取的一个volatile类型的值,这个值是从配置中获取的,代表当线程池满时,是否对新来的任务给予堵塞。但是,我们得注意volatile这个类型了,而且设计者在这个地方单独获取出来这个值,肯定是为了什么。接着往下看吧。
*/
final boolean blockWhenExhausted = getBlockWhenExhausted();
//是否为新建,请区别于created。为什么仅在这里定义了变量而没有初始化呢?
boolean create;
//当前的毫秒,用来就算
final long waitTime = System.currentTimeMillis();
//循环开始,第一次p=null开始循环
while (p == null) {
//初始化create=false,代表目前这个线程不是新建的。
create = false;
/*
private final LinkedBlockingDeque&lt;PooledObject&lt;T&gt;&gt; idleObjects;
idleObjects是一个阻塞有序队列,队列的类型是刚才的封装类型,封装的是实际的资源。从名字上可以看出,这里叫做空闲对象组
可以看到,从空闲对象组里拿出第一个元素。pollFirst()是LinkedBlockingDeque.java中的方法。就是通过操作节点来把第一个节点删除并返回。
*/
p = idleObjects.pollFirst();
//如果p为空代表弹出的是null,说明没有空闲。那就创建一个新的对象。
if (p == null) {
p = create();
//p可能会创建失败,创建失败的时候 create=false。 创建成功create=true
if (p != null) {
create = true;
}
}
//如果设置了对象池耗尽后堵塞等待的标示
if (blockWhenExhausted) {
if (p == null) {
//p没有创建成功
if (borrowMaxWaitMillis &lt; 0) {
//如果没有设置为-1,只有这种情况会符合小于0.所以就直接从空闲对象组中获取第一个有效的空闲对象。这个方法有锁的实现,所以这块可能会一直堵塞着直到有一个空闲对象可以使用。
p = idleObjects.takeFirst();
} else {
//如果设置等待时间了,就在这段时间里获取。超过了就返回null
p = idleObjects.pollFirst(borrowMaxWaitMillis,
TimeUnit.MILLISECONDS);
}
}
//到这里如果还是空,就说明没等待到空闲的资源了。所以就要报异常了。
if (p == null) {
throw new NoSuchElementException(
"Timeout waiting for idle object");
}
} else {
//如果没有设置blockWhenExhausted标示且p是null,那真是获取不到资源了,因为池子已经被占满了。
if (p == null) {
throw new NoSuchElementException("Pool exhausted");
}
}
/*
到这里其实p应该不是null了,说明肯定是有资源了。有可能是获得了空闲资源或是创建了一个新的资源。这个时候就要让系统给他分配内存了。跟进allocate()代码中,发现这是一个synchronized方法,保证了线程安全。(这块会有线程安全问题。为什么呢?)方法体判断了当前池状态(空闲状态或者测试中状态)然后更改状态位,置lastBorrowTime,lastUseTime,borrowedCount++等,然后返回boolean。
我们可以推算出,当前的池资源状态可能会被另一个线程改写。有一种情况就是,空闲的连接还需要进行test和validate才能够正确分配。但是如果在test或validate阶段失败了,那线程就不能够被分配资源了,因为前后状态不一致了。所以需要allocate的同步操作来保证当时的空闲资源不被改写。
*/
if (!p.allocate()) {
//没有成功分配资源,当然p就应该置为null,等待重新进行遍历。
p = null;
}
//到这里,分配成功的p应该进行校验阶段了。没有成功分配的p目前还是null,所以跳过下面的判断。
if (p != null) {
//从这开始p是有资源了
try {
/*
private final PooledObjectFactory&lt;T&gt; factory;
通过池工厂(PooledObjectFactory.java)对返回来的实例重新初始化。因为PooledObjectFactory是个接口。所以,我以Jedis为T进行解析(JedisFactory.java),别的实际类型会有不同的行为,但是基本差不多。
这个方法里就是获取了BinaryJedis,判断当前的库是不是库0.如果不是就就简单做个select操作。如果是的话什么都不做。
这块的设计有一个小心思,就是作者用这种代码结构来给开发者留出了很大的实现空间。
*/
factory.activateObject(p);
} catch (final Exception e) {
//激活异常
try {
//摧毁对象,释放资源
destroy(p, DestroyMode.NORMAL);
} catch (final Exception e1) {
// 摧毁的程序都报异常了,我可管不了了。采取鸵鸟算法。
}
//help GC
p = null;
//如果是新建的资源,就说明分配成功,但是激活失败。抛个错吧。
if (create) {
final NoSuchElementException nsee = new NoSuchElementException(
"Unable to activate object");
//用什么错误原因呢?就用上面这个吧
nsee.initCause(e);
throw nsee;
}
}
/*
如果分配成功了, 激活也成功了,并且开启了TestOnBorrow,逻辑进来再让我蹂躏下吧。如果失败了跳过这个逻辑吧。
testOnBorrow的目的是不论资源是否是从池子里借的,都要在返回钱进行一次验证。如果失败了资源会被清除出池子并且毁掉。然后再向池对象尝试借一次。
*/
if (p != null &amp;&amp; getTestOnBorrow()) {
boolean validate = false;
Throwable validationThrowable = null;
try {
/*