爛路山路都能跑,家用也有面子,這款合資SUV還不到17萬?

不存在的,根本不用鎖止后差速鎖,即便是很陡的坡,2。5L發動機足以應付,有些坡度真的非常陡,特別是越到山頂越陡,從拍攝圖中就可以看出來,頭已經昂起來了,還能看到底盤離地面還是比較高的,離地間隙確實很強,總的來說就是爬了一個很陡,很多彎,很泥濘的山,一般的城市SUV不能像途達那樣平穩通過,難度算是中等吧,也能夠體現其的越野性能的了,遺憾的是由於時間和天氣關係,後面有岩石以及坡度高達40°左右的路段就沒能體驗了,“可以說是相當危險”這是教練的原話,只能以後找機會嘗試一下了。

說起硬派越野車,大家首先想到的應該是Jeep牧馬人 或者是豐田普拉多,而如果是真正玩越野的車友,一定知道日產在硬派越野領域也有着豐厚的經驗,從途樂開始,日產便一直深耕專業越野SUV領域,而就在前两天,另一款全新硬派SUV——途達正式上市,上市的價格就非常具有吸引力,17萬不到的起步,對很多同級別的自主車型有不小的威脅。

途達可以看作是小號的途樂,價格非常漂亮,那麼實力怎麼樣?作為硬派越野SUV,四驅性能夠不夠強悍?把它開到終南山體驗一下越野道路為你揭曉。

途達是硬派越野SUV,所以把最重要的駕駛以及越野體驗部分放到最前面。

駕駛&越野

搭載一款代號為QR25的2.5L 自然吸氣發動機,最大馬力184匹,峰值扭矩可達251N·m,匹配6擋手動或者7擋手自一體變速箱。

大,真的很大,這是開這款車第一感覺,寬度1850mm,老實說即使是老司機一開始也有點難適應,很難把車身穩住在車道正中間,潛意識的就會偏離路邊壓着左側路線走,同行前後車的朋友都有這樣的情況;車身高度1835mm,坐上去讓身高1.7米的也能體驗一把俯視眾車的感覺,在收費站也能與收銀員微笑平視。

僅僅是夠用,是覺得這是對這款車動力比較中肯的評價,試駕車為頂配車型,官方車身重量數據就已經達到1926KG,在試駕的過程中,全車四個成年人,再加幾個背包,二噸多的整備重量,整個動力輸出比較平淡。

即便是比較平淡,但加速還是比較線性的,在渦輪增壓浪潮開着這麼大的自吸車型也是種奇特的體驗,有朋友可能會覺得這股動力日常開肯定是很肉的,2000,甚至到3500轉左右都是比較平淡的輸出,峰值扭矩轉速在4000后爆發,事實這個轉速以後給你的推背感並不是很強。

值得肯定的是4000轉以下的隔音是比較好的,怠速時可以說聽不到發動機的聲音,到了峰值扭矩轉速后發動機聲音就來了,動靜較大,但不難聽。

懸挂偏硬,支撐性不錯,但是由於坐墊柔軟度很好,所以即使是中小顛簸有着不錯的舒適性。

然而其實對於硬派越野來說,最重要的是並非動力性能,而是功能性,扭矩夠大就行,鎖止后差速鎖可以把扭矩放大2.7倍,爬坡、走泥路最合適不過了;在低速行駛時,如果是渦輪增壓發動機,動力輸出控制不平順,渦輪介入那一下子或許會措手不及,平時在城市鋪裝路面滿足通勤需求,在野外也能有好的脫困能力,這麼看2.5的自吸其實是更合適的。

油門和剎車前段都很輕,前40%的行程差不多是輸出20%的力度,開起來很佛系;而由於方向盤用的是油壓助力,感覺是略重的,而且轉向不算精確,同車的女生就覺得很好開很順手,在越野的時候其實是不用太精確的轉向,這同樣是出於側重於功能性的,又輕又假的电子轉向在越野過程當中反而會給不了駕駛員紮實的感覺。

以上是基本的駕駛感受,接下來是重頭戲部分,越野性能如何?駕駛路段時秦嶺七十二峪,通過的是其中前小部分以及牛脊樑森林公園再去挑戰一個當地推薦的越野聖地。

進入牛脊樑國家森林公園,整一段十幾公里都是上山連續爬坡路段,連綿多彎,十來二十米就是一個彎道,並且經常有落石,路況有點複雜險峻,而在這時候225mm離地間隙的優勢就有用武之地了,一路開過來,小石頭可以無視,大石頭稍微躲一下就沒事了,如果是開一輛轎車,石子彈到底盤上都會有點小怕。

連綿的爬坡路段其實並沒有感到動力不足,線性的動力輸出,配合沉穩的方向盤,反而能給足夠的信心,有點要注意的是方向盤在轉到後段時,需要轉動更大的角度才能保證不會轉向不足。

來到越野路段,坡度比牛脊樑景區更加大,比較小的坡度都有20°左右,較大難度的30度、40°都有,路面更窄,剛好適合一輛車通過,而且剛下過雨,路面濕滑泥濘。

一上去就是非常陡的坡,然後就是U型彎,這個時候掛空擋,切到低速四驅,可以實現扭矩放大,轉速控制在一千轉出頭,路面有點難度,然而在強大的越野性能下,輕點油門就能緩慢通過。

上了坡就是急轉彎,好在彎還是比較寬的,一把方向就能安全通過,再次感受到液壓方向盤給人足夠的信心,有點難度的是需要判斷兩側的距離,再次強調這車真的大,兩邊的樹枝不斷拍打車身,不過只要跟着車撤走就沒問題。

即便是坡度很大,路面也很泥濘,得意於良好的輪胎性能,打滑?不存在的,根本不用鎖止后差速鎖,即便是很陡的坡,2.5L發動機足以應付,有些坡度真的非常陡,特別是越到山頂越陡,從拍攝圖中就可以看出來,頭已經昂起來了,還能看到底盤離地面還是比較高的,離地間隙確實很強,總的來說就是爬了一個很陡,很多彎,很泥濘的山,一般的城市SUV不能像途達那樣平穩通過,難度算是中等吧,也能夠體現其的越野性能的了,遺憾的是由於時間和天氣關係,後面有岩石以及坡度高達40°左右的路段就沒能體驗了,“可以說是相當危險”這是教練的原話,只能以後找機會嘗試一下了。

很帥氣的硬漢

再看一下的基本情況,雖然途達是定位為中型SUV,但是它的車身尺寸:4882*1850*1835mm,軸距為2850mm,車身長度和軸距都比中大型的普拉多4840和2790還要長,尺寸相當大。

空載情況下,接近角為32°,離去角為27°,最小離地間隙高達22.5厘米,此等通過性,與同為非承載式車身硬派SUV相比,它的數據甚至比普拉多還要長,對比自主車型更是不在話下,途達硬派越野的基本要素毋庸置疑,當然肯定就比不上牧馬人了,牧馬人本身就是專業級的,新款車型更加強悍。

側面非常好看,車屁股很大,也比較方正,再加上很大的一塊擾流板提供了不錯的視覺效果,整一個車身側面也是威風凜凜。

車身很高,整個前臉的造型設計很威武,V-Motion設計前臉設計很年輕,舒展上揚的LED日行燈的形狀很醒目,中間鍍鉻飾條很粗壯,但是用在這個車上則氣勢十足,這張臉可以說是非常帥氣了,相信很多人都會喜歡。

發動機蓋上面的線條很飽滿,滿滿的肌肉感,坐在前排會看到機蓋兩側有兩個比較明顯的凸起。

還有不少亮點,最大的亮點應該是前杠的兩個獠牙了,視覺上極具攻擊力,下方是黑色塑料護板,蹭着不心疼。

戰鬥過後的划痕

車尾也非常很好看,尾部LED視寬燈和前LED日行燈一致,看起來很順眼,在看來,前後左右哪個角度的視覺效果都很棒,果然大的就是耐看。

18寸輪轂,後輪為鼓剎,採訪日本的技術人員表示,廠商認為鼓剎的性能是最適合這款車的設定,當然後期會根據消費者的需求考慮更換成碟剎,在試駕過程中其實並沒有什麼感覺這個剎車有什麼毛病,也沒感覺到有什麼影響,後期使用性能如何有待以後市場反饋。

空間

真皮座椅很鬆軟,日產沙發廠的名號不是蓋的,坐上去很舒服,給人一種很居家的感覺,日常通勤也可以很輕鬆。身高1.7米,調整到合適的駕駛坐姿后(偏高的坐姿),頭部空間很充裕,有一拳。

而到了後排,保持前排座椅不變的情況下,頭部空間就超過兩拳,腿部空間不用比劃了,一目瞭然。

當然1.7米身高參考性有限,所以找來身高1.82米的體驗者,調整到合適的駕駛坐姿后,在前排仍有一拳頭部空間。

而到了後排,頭部空間和腿部空間都是差不多兩拳,空間足夠大的,而且坐墊柔軟度很好,買了這車回去,家裡可以少買一個沙發,而且後排座椅靠背角度比較大,北京癱也沒問題。

後備廂很平整,而且很大,拉貨拉東西什麼的完全沒問題,第二排座椅可以放倒,無法形成一個平面,算是個可以無視的不足吧,這後備廂空間裝什麼都沒問題了。

居家內飾

進入車內,其實跟外形是有點反差的,和硬朗帥氣的外觀想比,內飾的風格是日產以往居家實用走向,內飾基本都是黑的,也可以看到“V”型的布局,副駕側的中控台只有一條曲線,所以設計一點都不花哨,甚至是平淡的,就是一輛普普通通的家用SUV。

方向盤是日產舊款的款式,如果換上新奇駿哪款應該會讓車內看起來年輕很多,方向盤的尺寸和手感都比較平淡,找不到什麼亮點。

7英寸中控屏是傳統的嵌入式,兩側是銀色裝飾,面板有木紋,使得不那麼沉悶,下方是空調控制按鍵,是非常容易上手的實體按鍵,這非常符合越野、實用性定位,有百度CarLife功能。

行車電腦可以显示多種信息

比較奇特的是倒車影像或者全景攝像的显示是位於內后視鏡左側,這是比較少見的,雖然尺寸小,但是不影響觀察判斷,依然很好用。

下面的這個分時四驅操作區就是把它和普通家用SUV區分開來的地方,分時四驅頂配和次頂配配備,並且配備机械牙嵌式后差速鎖,分時四驅可以實現兩輪驅動、四輪高速、以及四輪低速,四輪高/低速均可以實現前後50:50扭矩分配。

重頭戲是這個后差速鎖,鎖止后差速鎖可以讓左右輪作為一個整體旋轉,然後把動力扭矩傳遞到有抓地力的車輪,以實現脫困;好消息是,30萬以內帶机械式後橋差速鎖的硬派越野車,僅此一款,22.58萬帶回家。

總的來說,途達是一款非常具有吸引力的車型,功能性非常強,長得又帥氣,車身又大,二十萬出頭的價格讓很多人都能實現越野夢,是不折不扣的硬派越野車,除了內飾不太新穎以外,並沒有什麼太大缺點了,另外,那鬆軟舒適的座椅,作為日常開也有不錯的舒適性。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

7秒破百的霸氣大7座SUV,賣多少萬能讓你忘了漢蘭達?

0T發動機是同一副,採用時下流行的缸內直噴、雙渦管單渦輪增壓、水中冷、缸蓋集成排氣歧管等方式來提升發動機響應和燃油經濟性。而與之配搭的是大家都很熟悉的ZF 9AT變速箱,帶有4個超比擋以降低巡航轉速提升燃油經濟性。

說到國人對SUV或者越野車最早的記憶,除了以前的BJ212以外,就是當時跑遍中國大江南北的BJ213,也就是第二代切諾基。那可以說是所有越野愛好者的美好回憶,也一定程度上塑造了國人對於SUV這種“全能”的基本印象。甚至很多人說SUV還是會用“吉普車”的話術,說明JEEp品牌一直都在國內深入民心。不過現在國內對於SUV的市場需求日益增大,特別是7座SUV。不過一直以來,JEEp都缺少了一個在中大型7座SUV市場的產品。今天,我們來到了寧波,去JEEp專為中國市場研發的第一台7座中大型SUV,新一代大指揮官。

新一代大指揮官是現在JEEp城市SUV裏面的旗艦車型,也是專為中國市場所打造的一款全新的7座SUV,按照官方的話術叫”專業級全場合高端SUV”,設計定位兼顧商務和家用。和大部分7座SUV都以5座車型為基礎的不一樣,大指揮官在設計之初就定位為“真”7座SUV,這從其宣傳“7人VIp商務艙”就可見。按照其定位及價格區間,競爭對手就鎖定為漢蘭達、銳界等不多的幾款車型上。

很“JEEp”的外觀:

從設計上說,大指揮官沿用了雲圖概念車上的一些設計元素。不過從整體的外觀設計來說,相比於指南者的活潑和自由光的時尚,大指揮官更強調沉穩的外觀設計風格,整體的外觀氣氛也偏向商務。雖然說整車造型基本使用JEEp家族的直線條,也保留了7孔前進氣格柵的的家族元素,不過整車看起來在硬派的整體感覺上加入了一些柔性的線條。

加上前格柵、車燈和轉向燈等部分都採用了扁平的造型,讓整個前臉看起來變得更大和更精緻,看起來並沒有美系的粗枝大恭弘=叶 恭弘。

大指揮官的車身尺寸為4873*1892*1738mm,三圍尺寸相比漢蘭達和銳界兩個競爭對手來說並不算特別大,2800mm的軸距也僅僅處於兩者中等的位置。不過由於線條設計的原因,顯得大指揮官較長也較幹練,並不像銳界這種典型的美國味。很多凸顯肌肉感和力量感的稜線和外拋的恭弘=叶 恭弘子板設計也是點到即止,整體風格很沉穩,很符合大部分人對於商務SUV的印象。

照顧周到的乘坐空間

既然能叫做“7人VIp商務艙”,內飾自然不能馬虎。大指揮官的內飾設計雖然並不能說很“精緻”,但很多設計也很考究,整體感覺更像一台歐洲車,而不像粗枝大恭弘=叶 恭弘的美國車。採用淺色和深色的Two Tone配色,加上一些啞光鍍鉻的邊框來提升細節感,透露出一些豪華的屬性。

特別是材質方面大指揮官並不吝嗇。除了大量使用軟性材質作為中控台和門板的包裹材質以外,也大量使用了皮革以提升檔次感。頂配的座椅甚至有麂皮和Nappa皮,這在這個級別的SUV產品中算是非常越級的表現。

除了設計和用料以外,乘坐舒適性也是“7人VIp商務艙”所注重的部分。不過由於其實際車身尺寸在同級並不算很大,因此它的乘坐空間並不算非常寬闊,甚至比漢蘭達等級別標杆會稍窄小一些。不過大指揮官用了其他方法來提升乘坐舒適性。首先座椅的坐墊和靠背都較厚而且較軟。另外全系標配三區自動空調,各種USB接口、230V電源、12V電源、座椅加熱等配置都比較齊全。

第三排的座椅坐墊厚度和軟硬度適中,而且椅背可以小角度微調,也有獨立出風口和獨立的閱讀燈,基本能照顧到第三排乘客的中短途乘坐舒適性要求。

全景天窗的面積較大,整個車內空間的開陽感非常好。副駕駛的靠背邊上更設計了SUV少見的老闆鍵,讓第二排的乘客能夠擁有更好的乘坐空間,加上有隱私玻璃和手動遮陽簾,滿足商務要求。唯一的缺點就是頭枕較硬,這也是不少車型上的通病了。

同級領先的動力性能

動力方面,這次試駕的大指揮官搭載一台2.0T GME T4發動機,最大馬力265匹,最大扭矩400牛米。實際上這是和Giulia上的2.0T發動機是同一副,採用時下流行的缸內直噴、雙渦管單渦輪增壓、水中冷、缸蓋集成排氣歧管等方式來提升發動機響應和燃油經濟性。而與之配搭的是大家都很熟悉的ZF 9AT變速箱,帶有4個超比擋以降低巡航轉速提升燃油經濟性。再加上適時四驅的幫助下,讓這台7座中型SUV獲得低於8秒的百公里加速時間。相比銳界和漢蘭達,百公里加速時間幾乎短了一秒鐘。可以說,動力性能在同級處於領先水平。

實際駕駛時,動力表現還算比較滿意。即使使用很淺的油門深度,引擎的動力反應也比較迅速。而全油門時也有不錯的加速力,以一台這樣的中大型SUV來說,動力表現已經很好,當然不能和那些動輒三四百匹的高性能SUV作比較,畢竟這還是結合家用和商務需求而設計的。而需要注意的是油門和剎車腳感較重行程也較大,新手來說用力過猛或用力過小都會造成頓挫,需要一些習慣的時間。

另外,大指揮官採用了ZF提供的9AT變速箱。據說這副變速箱的TCU標定按照中國的消費者需求所調校,因此整體表現就四個字“超級平順”。任何換擋基本上感受不到頓挫,但隨之而來的代價就是換擋速度較慢。

另外由於為了降低油耗,變速箱非常积極往超比擋上換。雖然巡航轉速較低燃油經濟性較好,但是遇到需要加速的場合,油門一下去變速箱還要思考一下再退擋,動力的反應變得稍弱。還有一個細節,為了能夠在極低速更有效的推動較大的車身重量,因此1擋的齒比設定較大,導致初段加速較“沖”,但換擋時機較晚(約3000rpm),會造成一些低速的頓挫。當然,這副變速箱以舒適性和行駛質感來說還是不錯的。

並不硬派的駕駛感覺

懸挂方面,大指揮官採用了這個級別較常見的前麥弗遜后多連桿設計,調校也是傾向舒適性的設計。由於要照顧一定的非鋪裝路面需求,因此懸挂行程較長。這次我們試駕的均是頂配車型,都配有自適應懸挂,能根據行駛的需求調整避震的阻尼。在行駛過程中會感到舒適性很好,對於顛簸路面的濾震也不錯。整個底盤給人的感覺過濾顛簸很乾脆,較厚實。不過估計照顧到舒適性,因此彈簧腳軟,在較大動力介入的時候,車頭抬起的現象會比較明顯,剎車的時候也會有些點頭。

而操控性方面,大指揮官也採用了可變助力的設計。低速時整體方向手感傾向輕盈,而高速的時候會加重得比較明顯,不過整個變化還算線性不會有很突兀的感覺,方向指向性也都算靈敏了。雖然說懸挂整體偏軟,不過過彎時傾側抑制卻出乎意料並不會很嚴重,整體駕駛還是頗像轎車的。不過由於體重和身高始終在哪裡,因此即使動力表現較好,大指揮官還是不適合去做過分的激烈駕駛。

而隔音方面,大指揮官的表現還是頗不錯的。據悉,為了提升NVH表現,大指揮官採用了同級別車型較主流的“空腔注泡”的工藝,對車身空腔灌注環保發泡材質來降低噪音,整體隔音得水平在同級表現還是不錯的。。只是四氣缸的發動機始終排氣脈衝不夠綿密,因此傳入駕駛室里的引擎聲較單薄,所幸排氣的聲浪還算厚實,彌補了一些質感上的缺失(如果有六缸就好了)。

值得一提的是,大指揮官有一套智能切換的適時四驅系統,我們進行了輕度的非鋪裝路面體驗。在一些較泥濘的道路上四驅能夠讓行駛軌跡更穩定,同時車身剛性很高,在很顛簸的路面行駛也沒有發生各種扭曲和異響,和競品相比有較大的優勢。

總結

總體來說,大指揮官是一台並不很“JEEp”和很“美式”的SUV,這也和它說的“專為中國市場打造”不謀而合。整體而言,較強的動力是大指揮官的優勢,而舒適性也很不錯,整體的駕駛感受符合國人對歐洲車的總體印象。雖然說乘坐空間和外觀尺寸等並不佔優勢,不過整體外觀和內飾設計沉穩大氣,風格兼顧商務和家用,而且國人對JEEp品牌有一個固有的良好印象,在該級別能夠做到差異化競爭,市場潛力還是很不錯的。

本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

小師妹學JVM之:JVM的架構和執行過程

目錄

  • 簡介
  • JVM是一種標準
  • java程序的執行順序
  • JVM的架構
    • 類加載系統
    • 運行時數據區域
    • 執行引擎
  • 總結

簡介

JVM也叫Java Virtual Machine,它是java程序運行的基礎,負責將java bytecode轉換成為適合在各個不同操作系統中運行的機器代碼並運行。今天我們和小師妹一起走進java的核心JVM,領略java在設計上的哲學。

JVM是一種標準

小師妹:F師兄,經常聽到有人說hotspot VM,這個跟JVM是什麼關係?

其實吧,JVM只是一種標準,就像是一種協議,只要是實現和滿足這種協議的都可以稱為JVM。當然,java現在是Oracle公司的,所以這些所謂的JVM標準也是由Oracle來頒布的,如果你去查看Oracle的文檔,就會發現有一個專門的Java SE Specifications欄目,這個欄目中列出了JVM的實現標準,最新的標準就是The Java Virtual Machine Specification, Java SE 14 Edition。

更多精彩內容且看:

  • 區塊鏈從入門到放棄系列教程-涵蓋密碼學,超級賬本,以太坊,Libra,比特幣等持續更新
  • Spring Boot 2.X系列教程:七天從無到有掌握Spring Boot-持續更新
  • Spring 5.X系列教程:滿足你對Spring5的一切想象-持續更新
  • java程序員從小工到專家成神之路(2020版)-持續更新中,附詳細文章教程

更多內容請訪問www.flydean.com

既然JVM是一個標準,就可能有很多種實現。各大公司在滿足JVM標準的基礎上,開發了很多個不同的版本。

下面是我在維基百科中截取到的目前各個JVM的比較:

小師妹:F師兄,大家齊心協力做一個JVM不是更好嗎?為什麼分來分去的,還要重複造輪子?

有聽過Oracle和Google之間的API十年訴訟案嗎?API都不能順便用,更何況是JVM。各大廠商為了各自的利益,最終搞出了這麼多個JVM的版本。

在這些JVM中,最常用的就是HotSpot JVM了,畢竟它是Oracle的親兒子,或者可以說HotSpot JVM就是JVM的標準。

接下來就是Eclipse OpenJ9,這個是由IBM主導的JVM,一般只能跟IBM的產品一起使用的,因為有許可證限制。

java程序的執行順序

為了說明JVM的作用,我們先來回顧一下java程序的執行順序。

  1. 編寫java代碼文件比如Example.java
  2. 使用java編譯器javac將源文件編譯成為Example.class文件
  3. JVM加載生成的字節碼文件,將其轉換成為機器可以識別的native machine code執行

JVM的架構

小師妹:F師兄,Java語言那麼多特性,最後都要在JVM中運行,JVM的架構是不是特別複雜?好怕我聽不懂。

其實吧,JVM可以分為三大部分,五大空間和三大引擎,要講起來也不是特別複雜,先看下面的總體的JVM架構圖。

從上面的圖中,我們可以看到JVM中有三大部分,分別是類加載系統,運行時數據區域和Execution Engine。

類加載系統

類加載系統分為三個階段,分別是加載,鏈接和初始化。

加載大家都很清楚了,java中有個專門的ClassLoader來負責這個事情。除了加載Class之外,ClassLoader還可以用來加載resources。

在JDK9之前,系統默認有三個類加載器,分別是:

  1. Bootstrap ClassLoader

這個類加載器主要是加載 /jre/lib下面的rt.jar,並且這個類加載器是用C/C++來編寫的,並且它是後面Extension ClassLoader的父ClassLoader。
這個類應該在java代碼中找不到的(correct me if I am wrong!)。

  1. Extension ClassLoader

這個類加載器主要加載JDK的擴展類 /jre/lib/ext,它的實現類是 sun.misc.Launcher$ExtClassLoader :

static class ExtClassLoader extends URLClassLoader {
        private static volatile Launcher.ExtClassLoader instance;

        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            if (instance == null) {
                Class var0 = Launcher.ExtClassLoader.class;
                synchronized(Launcher.ExtClassLoader.class) {
                    if (instance == null) {
                        instance = createExtClassLoader();
                    }
                }
            }

            return instance;
        }

我們看下它的實現,實際上它創建了一個單例模式,使用的是雙重檢查加鎖,小師妹可以考慮一下怎麼使用延遲初始化佔位類的方式來重新這個類。

  1. System ClassLoader

這個加載器是加載定義在ClassLoader中的類。它的實現類是sun.misc.Launcher$AppClassLoader,這個類的實現很長,這裏就不完整列出來了:

static class AppClassLoader extends URLClassLoader 

在JDK9之後,因為引入了JPMS模塊的概念,所以類加載器變得不一樣了,在JDK9之後還是有三個內置的類加載器,分別是BootClassLoader,PlatformClassLoader和AppClassLoader:

    private static class BootClassLoader extends BuiltinClassLoader {
        BootClassLoader(URLClassPath bcp) {
            super(null, null, bcp);
        }

        @Override
        protected Class<?> loadClassOrNull(String cn, boolean resolve) {
            return JLA.findBootstrapClassOrNull(this, cn);
        }
    };
private static class PlatformClassLoader extends BuiltinClassLoader 
private static class AppClassLoader extends BuiltinClassLoader

Linking階段主要做了三件事情:

  1. Verification – 主要驗證字節碼文件的結構的正確性,如果不正確則會報LinkageError。
  2. Preparation – 負責創建static fields,並且初始化他們的值。
  3. Resolution – 把類型的常量池中引用的類,接口,字段和方法替換為直接引用的過程。

Initialization階段主要是調用class的父類和自身的初始化方法,來設置變量的初始值。

運行時數據區域

類加載好了,也初始化了,接下來就可以準備運行了。

運行的時候要為數據分配運行空間,這就是運行時數據區域的作用。

運行時數據區域又可以分為5個部分:

  1. Method Area

方法區是非Heap的內存空間,主要用來存放class結構,static fields, method, method’s data 和 static fields等。方法區是在JVM啟動的時候創建的,並且在所有的線程中共享。

Run-Time Constant Pool運行時常量池是放在方法區中的,他是class文件中constant_pool的運行時表現。

注意在JDK8之前,HotSpot JVM中對方法區的實現叫做持久代Perm Gen。不過在JDK8之後,Perm Gen已經被取消了,現在叫做Metaspace。Metaspace並不在java虛擬機中,它使用的是本地內存。Metaspace可以通過-XX:MaxMetaspaceSize來控制。

  1. Heap Area

Heap Area主要存儲類對象和數組。垃圾回收器(GC)主要就是用來回收Heap Area中的對象的。

  1. Stack Area

因為是棧的結構,所以這個區域總是LIFO(Last in first out)。我們考慮一個方法的執行,當方法執行的時候,就會在Stack Area中創建一個block,這個block中持有對本地對象和其他對象的引用。一旦方法執行完畢,則這個block就會出棧,供其他方法訪問。

  1. PC Registers

PC Registers主要用來對程序的執行狀態進行跟蹤,比如保存當前的執行地址,和下一步的地址等。

  1. Native Methods

最後一個就是本地方法區了,因為JVM的底層很多都是由C/C++來實現的,這些方法的實現就構成了本地方法區。

執行引擎

執行引擎主要負責將java的字節碼翻譯成機器碼然後執行。

先看一個java字節碼的內在結構,大家可以隨便找一個編譯好的類,使用javap來進行解析:

javap -v BufferUsage

這裏不過多介紹輸出結果的含義,我們會在後面的文章中進行詳解。

這我們可以看到方法中都有一個Code片段,這些Code被稱為OpCode,是JVM可以理解的操作命令。

執行引擎中裏面又有三個部分:

  1. Interpreter

翻譯器用來讀取上面介紹的OpCode,並將其翻譯成為機器語言。因為翻譯器需要一個命令一個命令的翻譯字節碼,所以速度會比較慢。這就是很久很久以前Java被詬病的地方。

  1. JIT (Just-In-Time) compiler

為了解決Interpreter翻譯慢的問題,JDK引入了JIT,對於那些經常使用的代碼,JIT會將這些字節碼翻譯成為機器代碼,並直接復用這些機器代碼,從而提高了執行效率。

  1. Garbage Collector

GC用來回收Heap Area,他是一個Daemon thread。

總結

本文介紹了JVM的總體架構信息。各個部分的細節信息會在後面的系列文章中陸續講解。歡迎大家關注小師妹系列。

本文作者:flydean程序那些事

本文鏈接:http://www.flydean.com/jvm-all-in-one/

本文來源:flydean的博客

歡迎關注我的公眾號:程序那些事,更多精彩等着您!

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

你必須知道的雲計算知識(下)

最近訂閱學習了《深入淺出雲計算》專欄,一口氣學完之後,做了一些總結筆記形成此文,特分享與你,希望對你有所幫助!本文為下半部分,主要總結了PaaS篇的核心要點。

一、PaaS篇

1、正確評估和理解PaaS

  PaaS的核心概念是Platform-as-a-Service,即雲計算提供的平台類服務。它是抽象和可復用的單元,讓用戶將更多精力放在業務邏輯上。

  PaaS的核心優勢在於生產力/效率,尤其體現在搭建和運維層面。

  怎樣學習和研究PaaS呢?可以參照以下幾個維度:

  (1)第一維度:服務是否帶有內生的運行環境;

  (2)第二維度:PaaS服務存在的位置和範圍及給予你的控制粒度;

  (3)第三維度:服務是否是“有狀態的”;

  (4)第四維度:支持PaaS的虛擬機是否對外暴露;

  怎樣衡量和評估PaaS呢?由於PaaS的核心在於封裝,既帶來了效率優勢也帶來了靈活性上的犧牲,因此我們需要檢查PaaS服務的限制,它們有:功能屏蔽、版本選擇、性能極限、更新頻率、版本陷阱;

  此外,當雲上官方的PaaS不滿足需求時也可以考慮第三方PaaS。

2、對象存儲服務

  對象存儲的主要功能:它是雲上創建的一種“網盤”,可以存儲任意結構化的和非結構化的二進制文件,用戶可以隨時上傳下載、修改刪除。對象存儲服務保證了數據的可靠性、可用性和擴展性。

  對象存儲和雲硬盤的區別:

  (1)訪問的接口和形式不同 => 雲硬盤作為虛擬機的塊存儲設備而存在,對象存儲本質是一個網絡化的服務;

  (2)對象存儲內本身不存在一個真正的文件系統,它是一個鍵值形式的存儲服務;

  (3)對象存儲的巨大容量,能夠輕鬆容納PB級的大容量數據;

  對象存儲的高級特性:

  (1)存儲分層 => 設置訪問頻率從高頻到低頻等不同的存儲級別;

  (2)生命周期管理 => 允許設置一定的過期規則,自動執行一些清理操作;

  (3)對象的版本管理 => 啟用此特性會自動記錄版本號以便進行回滾和恢復;

  總的來說,對象存儲的應用場景就是一切需要保存數據的地方!

3、應用託管服務

正確理解應用託管服務

  應用託管服務本質是為應用提供一個隔離的獨立運行環境,讓用戶只專註於業務邏輯而無須關注運維。

  目前主流雲廠商的該項服務的產品有:AWS ElasticBeanstalk、阿里雲Web應用託管服務(Web+)、Azure應用服務(Azure App Service)。

應用託管的增值服務

  (1)監控:針對Web應用的特點而進行的HTTP層面的監控,基於這些監控指標可以指定相應的報警規則;

  (2)擴展:既包含了底層機器配置的垂直擴展,也包含了機器數量層面的水平擴展;

  (3)集成:與其他PaaS服務的集成很方便,與雲上DevOps組件和流程的無縫對接;

4、雲上數據庫服務

理解數據庫服務

  數據庫PaaS服務是將數據庫服務搬到雲上,讓用戶更方便輕鬆地使用、管理和維護數據庫。

  數據庫PaaS服務和傳統關係型數據庫的共同點:外部交互層面,保持了和原版數據庫幾乎一致的編程接口和使用體驗。

  和傳統關係型數據庫的差異點:搭建、運維和管理層面實現了相當程度的智能化和自動化,極大提升用戶友好度,降低使用門檻。

  雲上數據庫最具代表的高級特性:

  (1)支持讀寫分離 => 只需在後台略加操作配置即可完成;

  (2)支持自動調優 => 自帶有性能分析和改進模塊幫助發現性能熱點;

新一代雲原生數據庫

  目前主流雲原生數據庫產品有:AWS Aurora、阿里雲 PolarDB、Azure Cosmos DB;

  目前雲原生數據庫的特點:

  (1)更強的可擴展性 => 存儲規模和算力;

  (2)更高的可用性和可靠性 => 默認多副本高可用;

  (3)支持多種數據模型 => 兼容關係型與NoSQL;

  (4)低成本啟動 => 自然地跟隨業務增長,經濟適用;

數據庫服務為何能不斷佔領市場

  除了上面提到的易用性和豐富功能性外,雲廠商能夠端到端地掌控影響一個數據庫的設計和性能的所有因素,為它配備了最新最好的軟硬件組合。

  藉助雲計算平台,雲數據庫擁有非常好的流量入口,Gartner甚至大膽預測到2023年全球3/4的數據庫都會跑在雲上!

使用雲數據庫的建議

  如果是老的應用遷移/與自建數據庫保持高度兼容性的場合 => 使用經典RDS服務

  如果應用場景中數據量大、性能要求高/沒有歷史負擔 => 直接考慮雲原生數據庫服務

5、雲上大數據服務

理解雲上大數據服務

  大數據是技術手段,而雲是一種資源和能力的載體!如果雲是一艘航母,是一個大型綜合作戰品台,那大數據就好比戰鬥機,在航母上就是艦載機。

雲上大數據的特點

  (1)簡單易用,方便管理:大大降低了學習和應用大數據技術的門檻

  (2)盡可能的保證了兼容性:和熟知的大數據技術別無他樣

  (3)解耦了大數據架構中的計算和存儲:存儲端一般優先選擇對象存儲,計算端集群可以動態地創建和銷毀

  (4)提供了很多增值服務:如性能監控、Jupyter Notebook等;

主流雲廠商的大數據服務

  (1)大數據計算:AWS EMR、阿里雲 MaxCompute / E-MapReduce、Azure HDInsight

  (2)大數據存儲:AWS S3(EMRFS)、阿里雲 OSS / JindoFS、Azure Blob Storage / Data Lake Storage Gen2

 基於阿里雲MaxCompute的日誌大數據分析案例

6、雲上容器服務

容器和雲的關係

  一方面,容器和雲是相輔相成的:雲承載着容器的運行,容器生態也驅動着雲的發展。

  另一方面,容器與雲廠商力推的一些雲服務存在競爭和替代關係:其實部分PaaS服務也可以使用IaaS服務來實現,又如Google积極創立和發展CNCF推動容器應用標準化。

雲上Kubernetes服務的特點

  (1)雲端的多租戶特性,可以免除在Master節點方面的開銷;

  (2)雲廠商做了很多工作,使得K8s可以和IaaS/PaaS組件有很好的的集成;

  (3)易部署多集群,保證不同的集群有更好的隔離性;

 騰訊雲TKE產品架構

雲上容器鏡像服務

  雲廠商都提供了自己的容器鏡像服務,如AWS ECR、Azure ACR等。

 騰訊雲容器鏡像服務助力實現容器持續部署

全託管容器實例服務

  “全託管”思想在容器服務商的體現有以下幾個點:

  (1)不用關心底層基礎設施;

  (2)AWS Fargate、阿里雲彈性容器實例等;

  (3)適合有容器鏡像且想要儘快在雲上跑起來的場景;

 Azure Container Instance容器實例服務創建容器實例

7、無服務器計算服務

理解無服務器計算

  無服務器:“無服務器”是雲計算中資源抽象的極致體現,用戶感覺不到服務器的存在,可以專註於業務邏輯而不再關心基礎設施;

  無服務器計算服務:完全屏蔽計算資源,引導不再關心底層環境,主流的無服務器計算服務有AWS Lambda、阿里雲函數計算 以及 Azure Functions。

無服務器計算的計費機制

  兩個指標:調用次數 和 調用時長。

  適合場景:偶爾觸發、短時間運行的工作。

無服務器計算是多面手

  (1)事件模型是無服務器的核心編程模型和運行邏輯;

  (2)雲上Serverless服務一般都配套提供了多種多樣的觸發器;

  (3)多樣的外部交互方式也讓無服務器計算能夠對外訪問,並向下游輸出;

  (4)以工作流的方式進行雲函數等事件處理單元的組合和編排;

無服務器計算服務的限制

  無服務器計算服務的主要限制還是在於它是一個受限的環境,比如:冷啟動的延時、內存的限制、雲函數的運行時長、併發數上限等等。

8、雲上AI服務

開箱即用的API服務

  這類服務將非結構化數據處理分析的通用需求場景,進行了封裝和開放。非結構化數據包括:圖像、視頻、語音、文本等等。

  我們可以通過雲端標準的API和SDK來進行調用,一般按調用次數進行收費。

  現在各領域的AI服務有:

  (1)計算機視覺:人臉識別、物體檢測、OCR、安全掃描等;

  (2)自然語言處理:機器翻譯、文本分析等;

  (3)語音類智能服務:語音識別、語音合成等;

  (4)視頻類智能服務:視頻分析、用戶內容審查等;

 百度雲圖片識別服務識別到花瓶和水杯

構建自己的AI模型

  實際應用中,很多企業都需要按照特定的需求來構建自己的定製模型,是普遍而常見的場景。

  因此,雲廠商開始提供幫助用戶構建AI模型的基礎設施服務:

  (1)主流的產品有:AWS SageMaker、Azure Machine Learning 以及 阿里雲機器學習平台PAI;

  (2)適合的對象由:數據科學家、算法工程師;

  此外,雲上機器學習服務可以幫助進行貫穿生命周期的模型構建和管理:

  (1)數據準備:集成了數據標註相關的功能簡化操作難度;

  (2)模型訓練:內置常見機器學習和深度學習算法 以及 也兼容開源的機器學習和深度學習框架;

  (3)模型發布和部署:簡化了模型部署和推理調用操作難度;

 百度雲EasyDL定製化圖像識別模型訓練報告

二、結束語

1、再看雲計算

  (1)雲計算是一個載體和平台,承載着從IaaS到PaaS林林總總的能力;

  (2)每項能力中既包含了資源,也體現了技術,並且以產品和服務的形態開放;

  (3)雲的承載性是雲得以包羅萬象,並且與時俱進的根本原因;

2、雲原生的本質

  (1)雲原生的本質是用於構建現代雲端應用的一系列架構理念,以及幫助這些理念落地的技術支撐和最佳實踐;

  (2)雲原生的核心理念包括無狀態、分佈式、服務化、彈性擴展等等;

  (3)雲原生的狹義的定義:特指容器化、容器編排和微服務架構,此外雲廠商在宣傳K8s服務和產品時所說的“雲原生”;

  (4)雲原生的廣義視角:只要是適合在雲上運行,具備和符合雲上架構特點的應用,都可以說是屬於“雲原生”範疇;

3、面向未來的建議

  (1)跟隨雲的發展腳步來不斷提升自己:比如考取雲廠商的認證(比如阿里雲ACP認證)、參加雲計算大會、積极參与雲生態等;

  (2)用雲來構建和開展你的業務:讓雲在手中發揮最大的作用和價值,抓住機會上雲用效率贏取競爭優勢;

推薦考取阿里雲ACP認證(考證不是主要目的)

三、腦圖分享

點此查看完整腦圖

參考資料

何愷鐸,極客時間《深入淺出雲計算》專欄(推薦訂閱學習)

 

 

作者:周旭龍

出處:https://edisonchou.cnblogs.com

本文版權歸作者和博客園共有,歡迎轉載,但未經作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文鏈接。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※想知道最厲害的網頁設計公司"嚨底家"!

※別再煩惱如何寫文案,掌握八大原則!

※產品缺大量曝光嗎?你需要的是一流包裝設計!

基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)

系列文章

  1. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用 abp cli 搭建項目
  2. 基於 abp vNext 和 .NET Core 開發博客項目 – 給項目瘦身,讓它跑起來
  3. 基於 abp vNext 和 .NET Core 開發博客項目 – 完善與美化,Swagger登場
  4. 基於 abp vNext 和 .NET Core 開發博客項目 – 數據訪問和代碼優先
  5. 基於 abp vNext 和 .NET Core 開發博客項目 – 自定義倉儲之增刪改查
  6. 基於 abp vNext 和 .NET Core 開發博客項目 – 統一規範API,包裝返回模型
  7. 基於 abp vNext 和 .NET Core 開發博客項目 – 再說Swagger,分組、描述、小綠鎖
  8. 基於 abp vNext 和 .NET Core 開發博客項目 – 接入GitHub,用JWT保護你的API
  9. 基於 abp vNext 和 .NET Core 開發博客項目 – 異常處理和日誌記錄
  10. 基於 abp vNext 和 .NET Core 開發博客項目 – 使用Redis緩存數據
  11. 基於 abp vNext 和 .NET Core 開發博客項目 – 集成Hangfire實現定時任務處理
  12. 基於 abp vNext 和 .NET Core 開發博客項目 – 用AutoMapper搞定對象映射
  13. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(一)
  14. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(二)
  15. 基於 abp vNext 和 .NET Core 開發博客項目 – 定時任務最佳實戰(三)
  16. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(一)
  17. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(二)
  18. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(三)
  19. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(四)
  20. 基於 abp vNext 和 .NET Core 開發博客項目 – 博客接口實戰篇(五)
  21. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(一)
  22. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(二)
  23. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(三)
  24. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(四)
  25. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(五)
  26. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(六)
  27. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(七)
  28. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(八)
  29. 基於 abp vNext 和 .NET Core 開發博客項目 – Blazor 實戰系列(九)
  30. 基於 abp vNext 和 .NET Core 開發博客項目 – 終結篇之發布項目

上一篇完成了博客文章詳情頁面的數據展示和基於JWT方式的簡單身份驗證,本篇繼續推進,完成後台分類管理的所有增刪改查等功能。

分類管理

在 Admin 文件夾下新建Razor組件,Categories.razor,設置路由,@page "/admin/categories"。將具體的展示內容放在組件AdminLayout中。

@page "/admin/categories"

<AdminLayout>
      <Loading />
</AdminLayout>

在這裏我會將所有分類展示出來,新增、更新、刪除都會放在一個頁面上去完成。

先將列表查出來,添加API的返回參數,private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;,然後再初始化中去獲取數據。

//QueryCategoryForAdminDto.cs
namespace Meowv.Blog.BlazorApp.Response.Blog
{
    public class QueryCategoryForAdminDto : QueryCategoryDto
    {
        /// <summary>
        /// 主鍵
        /// </summary>
        public int Id { get; set; }
    }
}
/// <summary>
/// API返回的分類列表數據
/// </summary>
private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;

/// <summary>
/// 初始化
/// </summary>
/// <returns></returns>
protected override async Task OnInitializedAsync()
{
    var token = await Common.GetStorageAsync("token");
    Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

    categories = await FetchData();
}

/// <summary>
/// 獲取數據
/// </summary>
/// <returns></returns>
private async Task<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>> FetchData()
{
    return await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");
}

初始化的時候,需要將我們存在localStorage中的token讀取出來,因為我們後台的API都需要添加 Authorization Header 請求頭才能成功返回數據。

在Blazor添加請求頭也是比較方便的,直接Http.DefaultRequestHeaders.Add(...)即可,要注意的是 token值前面需要加 Bearer ,跟了一個空格不可以省略。

獲取數據單獨提成了一個方法FetchData(),因為會頻繁用到,現在在頁面上將數據綁定進行展示。

@if (categories == null)
{
    <Loading />
}
else
{
    <div class="post-wrap categories">
        <h2 class="post-title">-&nbsp;Categories&nbsp;-</h2>
        @if (categories.Success && categories.Result.Any())
        {
            <div class="categories-card">
                @foreach (var item in categories.Result)
                {
                    <div class="card-item">
                        <div class="categories">
                            <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(item.Id))"></NavLink>
                            <NavLink title="編輯" @onclick="@(() => ShowBox(item))"></NavLink>
                            <NavLink target="_blank" href="@($"/category/{item.DisplayName}")">
                                <h3>@item.CategoryName</h3>
                                <small>(@item.Count)</small>
                            </NavLink>
                        </div>
                    </div>
                }
                <div class="card-item">
                    <div class="categories">
                        <NavLink><h3 @onclick="@(() => ShowBox())">~~~ 新增分類 ~~~</h3></NavLink>
                    </div>
                </div>
            </div>
        }
        else
        {
            <ErrorTip />
        }
    </div>
}

同樣的當categories還沒成功獲取到數據的時候,我們直接在展示 <Loading />組件。然後就是循環列表數據在foreach中進行綁定數據。

在每條數據最前面,加了刪除和編輯兩個按鈕,刪除的時候調用DeleteAsync方法,將當前分類的Id傳給他即可。新增和編輯的時候調用ShowBox方法,他接受一個參數,當前循環到的分類對象item,即QueryCategoryForAdminDto。

同時這裏考慮到復用性,我寫了一個彈窗組件,Box.Razor,放在Shared文件夾下面,可以先看一下標題為彈窗組件的內容再回來繼續往下看。

刪除分類

接下來看看刪除方法。

/// <summary>
/// 刪除分類
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
private async Task DeleteAsync(int id)
{
    // 彈窗確認
    bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這個該死的分類嗎");

    if (confirmed)
    {
        var response = await Http.DeleteAsync($"/blog/category?id={id}");

        var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

        if (result.Success)
        {
            categories = await FetchData();
        }
    }
}

刪除之前搞個原生的confirm進行提示,避免手殘誤刪。因為API那邊使用的是HttpDelete,所有我們調用API時候要用Http.DeleteAsync,返回的是HttpResponseMessage對象,需要我們手動處理接收返回數據,將其轉換為ServiceResult對象,如果判斷刪除成功后重新調用FetchData()刷新分類數據。

新增/更新分類

新增和更新數據選擇使用彈窗的方式來進行(彈窗組件在下方),首先是需要一個參數判斷彈窗是否打開,因為是將新增和更新放在一起,所以如何判斷是新增還是更新呢?這裏使用Id來進行判斷,當編輯的時候肯定會有Id參數。新增的時候是沒有參數傳遞的。

當我們打開彈窗后裏面需要展示兩個input框,用來供輸入要保存的數據,同樣是添加兩個變量。

添加所需的這幾個參數。

/// <summary>
/// 默認隱藏Box
/// </summary>
private bool Open { get; set; } = false;

/// <summary>
/// 新增或者更新時候的分類字段值
/// </summary>
private string categoryName, displayName;

/// <summary>
/// 更新分類的Id值
/// </summary>
private int id;

現在可以將Box組件添加到頁面上。

<div class="post-wrap categories">
	...
</div>

<Box OnClickCallback="@SubmitAsync" Open="@Open">
    <div class="box-item">
        <b>DisplayName:</b><input type="text" @bind="@displayName" @bind:event="oninput" />
    </div>
    <div class="box-item">
        <b>CategoryName:</b><input type="text" @bind="@categoryName" @bind:event="oninput" />
    </div>
</Box>

確定按鈕回調事件執行SubmitAsync()方法,打開狀態參數為上面添加的Open,按鈕文字ButtonText為默認值不填。

添加了兩個input,將兩個分類字段分別綁定上去,使用@bind和@bind:event。前者等價於設置其value值,後者等價於一個change事件當值改變後會重新賦給綁定的字段參數。

現在可以來看看點擊了新增或者編輯按鈕的方法ShowBox(...),接收一個參數QueryCategoryForAdminDto讓其默認值為null。

/// <summary>
/// 显示box,綁定字段
/// </summary>
/// <param name="dto"></param>
private void ShowBox(QueryCategoryForAdminDto dto = null)
{
    Open = true;
    id = 0;

    // 新增
    if (dto == null)
    {
        displayName = null;
        categoryName = null;
    }
    else // 更新
    {
        id = dto.Id;
        displayName = dto.DisplayName;
        categoryName = dto.CategoryName;
    }
}

執行ShowBox()方法,將彈窗打開,設置Open = true;和初始化id的值id = 0;。

通過參數是否null進行判斷是新增還是更新,這樣打開彈窗就搞定了,剩下的就交給彈窗來處理了。

因為新增和更新API需要還對應的輸入參數EditCategoryInput,去添加它不要忘了。

那麼現在就只差按鈕回調事件SubmitAsync()了,主要是給輸入參數進行賦值調用API,執行新增或者更新即可。

/// <summary>
/// 確認按鈕點擊事件
/// </summary>
/// <returns></returns>
private async Task SubmitAsync()
{
    var input = new EditCategoryInput()
    {
        DisplayName = displayName.Trim(),
        CategoryName = categoryName.Trim()
    };

    if (string.IsNullOrEmpty(input.DisplayName) || string.IsNullOrEmpty(input.CategoryName))
    {
        return;
    }

    var responseMessage = new HttpResponseMessage();

    if (id > 0)
        responseMessage = await Http.PutAsJsonAsync($"/blog/category?id={id}", input);
    else
        responseMessage = await Http.PostAsJsonAsync("/blog/category", input);

    var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
    if (result.Success)
    {
        categories = await FetchData();
        Open = false;
    }
}

當參數為空時,直接return什麼都不執行。通過當前Id判斷是新增還是更新操作,調用不同的方法PutAsJsonAsync和PostAsJsonAsync去請求API,同樣返回到是HttpResponseMessage對象,最後如果操作成功,重新請求一個數據,刷新分類列表,將彈窗關閉掉。

分類管理頁面的全部代碼如下:

點擊查看代碼

@page "/admin/categories"

<AdminLayout>
    @if (categories == null)
    {
        <Loading />
    }
    else
    {
        <div class="post-wrap categories">
            <h2 class="post-title">-&nbsp;Categories&nbsp;-</h2>
            @if (categories.Success && categories.Result.Any())
            {
                <div class="categories-card">
                    @foreach (var item in categories.Result)
                    {
                        <div class="card-item">
                            <div class="categories">
                                <NavLink title="刪除" @onclick="@(async () => await DeleteAsync(item.Id))"></NavLink>
                                <NavLink title="編輯" @onclick="@(() => ShowBox(item))"></NavLink>
                                <NavLink target="_blank" href="@($"/category/{item.DisplayName}")">
                                    <h3>@item.CategoryName</h3>
                                    <small>(@item.Count)</small>
                                </NavLink>
                            </div>
                        </div>
                    }
                    <div class="card-item">
                        <div class="categories">
                            <NavLink><h3 @onclick="@(() => ShowBox())">~~~ 新增分類 ~~~</h3></NavLink>
                        </div>
                    </div>
                </div>
            }
            else
            {
                <ErrorTip />
            }
        </div>

        <Box OnClickCallback="@SubmitAsync" Open="@Open">
            <div class="box-item">
                <b>DisplayName:</b><input type="text" @bind="@displayName" @bind:event="oninput" />
            </div>
            <div class="box-item">
                <b>CategoryName:</b><input type="text" @bind="@categoryName" @bind:event="oninput" />
            </div>
        </Box>
    }
</AdminLayout>

@code {
    /// <summary>
    /// 默認隱藏Box
    /// </summary>
    private bool Open { get; set; } = false;

    /// <summary>
    /// 新增或者更新時候的分類字段值
    /// </summary>
    private string categoryName, displayName;

    /// <summary>
    /// 更新分類的Id值
    /// </summary>
    private int id;

    /// <summary>
    /// API返回的分類列表數據
    /// </summary>
    private ServiceResult<IEnumerable<QueryCategoryForAdminDto>> categories;

    /// <summary>
    /// 初始化
    /// </summary>
    /// <returns></returns>
    protected override async Task OnInitializedAsync()
    {
        var token = await Common.GetStorageAsync("token");
        Http.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");

        categories = await FetchData();
    }

    /// <summary>
    /// 獲取數據
    /// </summary>
    /// <returns></returns>
    private async Task<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>> FetchData()
    {
        return await Http.GetFromJsonAsync<ServiceResult<IEnumerable<QueryCategoryForAdminDto>>>("/blog/admin/categories");
    }

    /// <summary>
    /// 刪除分類
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    private async Task DeleteAsync(int id)
    {
        Open = false;

        // 彈窗確認
        bool confirmed = await Common.InvokeAsync<bool>("confirm", "\n真的要幹掉這個該死的分類嗎");

        if (confirmed)
        {
            var response = await Http.DeleteAsync($"/blog/category?id={id}");

            var result = await response.Content.ReadFromJsonAsync<ServiceResult>();

            if (result.Success)
            {
                categories = await FetchData();
            }
        }
    }

    /// <summary>
    /// 显示box,綁定字段
    /// </summary>
    /// <param name="dto"></param>
    private void ShowBox(QueryCategoryForAdminDto dto = null)
    {
        Open = true;
        id = 0;

        // 新增
        if (dto == null)
        {
            displayName = null;
            categoryName = null;
        }
        else // 更新
        {
            id = dto.Id;
            displayName = dto.DisplayName;
            categoryName = dto.CategoryName;
        }
    }

    /// <summary>
    /// 確認按鈕點擊事件
    /// </summary>
    /// <returns></returns>
    private async Task SubmitAsync()
    {
        var input = new EditCategoryInput()
        {
            DisplayName = displayName.Trim(),
            CategoryName = categoryName.Trim()
        };

        if (string.IsNullOrEmpty(input.DisplayName) || string.IsNullOrEmpty(input.CategoryName))
        {
            return;
        }

        var responseMessage = new HttpResponseMessage();

        if (id > 0)
            responseMessage = await Http.PutAsJsonAsync($"/blog/category?id={id}", input);
        else
            responseMessage = await Http.PostAsJsonAsync("/blog/category", input);

        var result = await responseMessage.Content.ReadFromJsonAsync<ServiceResult>();
        if (result.Success)
        {
            categories = await FetchData();
            Open = false;
        }
    }
}

彈窗組件

考慮到新增和更新數據的時候需要彈窗,這裏就簡單演示一下寫一個小組件。

在 Shared 文件夾下新建一個Box.razor。

在開始之前分析一下彈窗組件所需的元素,彈窗肯定有一個確認和取消按鈕,右上角需要有一個關閉按鈕,關閉按鈕和取消按鈕一個意思。他還需要一個打開或者關閉的狀態,判斷是否打開彈窗,還有就是彈窗內需要自定義展示內容。

確定按鈕的文字可以自定義,所以差不多就需要3個參數,組件內容RenderFragment ChildContent,是否打開彈窗bool Open默認隱藏,按鈕文字string ButtonText默認值給”確定”。然後最重要的是確定按鈕需要一個回調事件,EventCallback<MouseEventArgs> OnClickCallback 用於執行不同的事件。

/// <summary>
/// 組件內容
/// </summary>
[Parameter]
public RenderFragment ChildContent { get; set; }

/// <summary>
/// 是否隱藏
/// </summary>
[Parameter]
public bool Open { get; set; } = true;

/// <summary>
/// 按鈕文字
/// </summary>
[Parameter]
public string ButtonText { get; set; } = "確定";

/// <summary>
/// 確認按鈕點擊事件回調
/// </summary>
[Parameter]
public EventCallback<MouseEventArgs> OnClickCallback { get; set; }

/// <summary>
/// 關閉Box
/// </summary>
private void Close() => Open = false;

右上角關閉和取消按鈕直接在內部進行處理,執行Close()方法,將參數Open值設置為false即可。

對應的html如下。

@if (Open)
{
    <div class="shadow"></div>
    <div class="box">
        <div class="close" @onclick="Close"></div>
        <div class="box-content">
            @ChildContent
            <div class="box-item box-item-btn">
                <button class="box-btn" @onclick="OnClickCallback">@ButtonText</button>
                <button class="box-btn btn-primary" @onclick="Close">取消</button>
            </div>
        </div>
    </div>
}

關於樣式

下面是彈窗組件所需的樣式代碼,大家需要的自取,也可以直接去GitHub實時獲取最新的樣式文件。

.box {
    width: 600px;
    height: 300px;
    border-radius: 5px;
    background-color: #fff;
    position: fixed;
    top: 50%;
    left: 50%;
    margin-top: -150px;
    margin-left: -300px;
    z-index: 997;
}
.close {
    position: absolute;
    right: 3px;
    top: 2px;
    cursor: pointer;
}
.shadow {
    width: 100%;
    height: 100%;
    position: fixed;
    left: 0;
    top: 0;
    z-index: 996;
    background-color: #000;
    opacity: 0.3;
}
.box-content {
    width: 90%;
    margin: 20px auto;
}
.box-item {
    margin-top: 10px;
    height: 30px;
}
.box-item b {
    width: 130px;
    display: inline-block;
}
.box-item input[type=text] {
    padding-left: 5px;
    width: 300px;
    height: 30px;
}
.box-item label {
    width: 100px;
    white-space: nowrap;
}
.box-item input[type=radio] {
    width: auto;
    height: auto;
    visibility: initial;
    display: initial;
    margin-right: 2px;
}
.box-item button {
    height: 30px;
    width: 100px;
}
.box-item-btn {
    position: absolute;
    right: 20px;
    bottom: 20px;
}
.box-btn {
    display: inline-block;
    height: 30px;
    line-height: 30px;
    padding: 0 18px;
    background-color: #5A9600;
    color: #fff;
    white-space: nowrap;
    text-align: center;
    font-size: 14px;
    border: none;
    border-radius: 2px;
    cursor: pointer;
}
button:focus {
    outline: 0;
}
.box-btn:hover {
    opacity: .8;
    filter: alpha(opacity=80);
    color: #fff;
}
.btn-primary {
    border: 1px solid #C9C9C9;
    background-color: #fff;
    color: #555;
}
.btn-primary:hover {
    border-color: #5A9600;
    color: #333;
}
.post-box {
    width: 98%;
    margin: 27px auto 0;
}
.post-box-item {
    width: 100%;
    height: 30px;
    margin-bottom: 5px;
}
.post-box-item input {
    width: 49.5%;
    height: 30px;
    padding-left: 5px;
    border: 1px solid #ddd;
}
.post-box-item input:nth-child(1) {
    float: left;
    margin-right: 1px;
}
.post-box-item input:nth-child(2) {
    float: right;
    margin-left: 1px;
}
.post-box .box-item b {
    width: auto;
}
.post-box .box-item input[type=text] {
    width: 90%;
}

好了,分類模塊的功能都完成了,標籤和友情鏈接的管理界面還會遠嗎?這兩個模塊的做法和分類是一樣的,有興趣的可以自己動手完成,今天到這吧,未完待續…

開源地址:https://github.com/Meowv/Blog/tree/blog_tutorial

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※教你寫出一流的銷售文案?

※超省錢租車方案

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※產品缺大量曝光嗎?你需要的是一流包裝設計!

上周熱點回顧(6.8-6.14)

熱點隨筆:

· 真慘!連各大編程語言都擺起地攤了! (軒轅之風)
· .NET開發者省份分佈排名 (張善友)
· C#9.0 終於來了,您還學的動嗎? 帶上VS一起解讀吧!(應該是全網第一篇) (一線碼農)
· WinUI 3 試玩報告 (dino.c)
· [C#.NET 拾遺補漏]04:你必須知道的反射 (LiamWang)
· 120行代碼打造.netcore生產力工具-小而美的後台異步組件 (福祿網絡技術團隊)
· .Net Core實戰之基於角色的訪問控制的設計 (陳珙)
· 面試了 6 輪 Google 中國 之後,還是掛了 (程序猿石頭)
· 手動造輪子——基於.NetCore的RPC框架DotNetCoreRpc (yi念之間)
· .Net Core微服務入門全紀錄(一)——項目搭建 (xhznl)
· 搭上末班車去了京東,終於可以做東哥兄弟… (龍躍十二)
· 8000字長文讓你徹底了解 Java 8 的 Lambda、函數式接口、Stream 用法和原理 (風的姿態)

熱點新聞:

· 中國最慘創業者:3年前我被投資人趕出公司,3年後讓我賠3800萬!
· 馬化騰每天刷Leetcode?代碼你打算寫到幾歲?
· Linux之父親手拒補丁、怒懟亞馬遜程序員 網友:我的快樂又回來了
· 一場屬於少數人的復蘇
· 打破Oracle垄斷!支付寶OceanBase獨立:年內將發布重大版本升級
· 京東真正上岸了
· 關閉微信朋友圈個性化廣告有多難?13 個步驟點擊 16 次,只能關半年
· 27歲程序員轉職賞金獵人:一個漏洞10萬美元,比工資香多了
· .NET 5.0 Preview 5 發布
· 好評率84%,微軟工作室翻身大作《盜賊之海》登陸Steam!
· 阿里雲年度戰略首次公開!中台做厚,硬件擴張,還要再招5000人
· 分析了上千張照片發現:R語言程序員最快樂,Java開發者最年輕

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※超省錢租車方案

※教你寫出一流的銷售文案?

※網頁設計最專業,超強功能平台可客製化

※產品缺大量曝光嗎?你需要的是一流包裝設計!

重學 Java 設計模式:實戰享元模式「基於Redis秒殺,提供活動與庫存信息查詢場景」

作者:小傅哥
博客:https://bugstack.cn

沉澱、分享、成長,讓自己和他人都能有所收穫!

一、前言

程序員‍‍的上下文是什麼?

很多時候一大部分編程開發的人員都只是關注於功能的實現,只要自己把這部分需求寫完就可以了,有點像被動的交作業。這樣的問題一方面是由於很多新人還不了解程序員的職業發展,還有一部分是對於編程開發只是工作並非興趣。但在程序員的發展來看,如果不能很好的處理上文(產品),下文(測試),在這樣不能很好的了解業務和產品發展,也不能編寫出很有體繫結構的代碼,日久天長,1到3年、3到5年,就很難跨越一個個技術成長的分水嶺。

擁有接受和學習新知識的能力

你是否有感受過小時候在什麼都還不會的時候接受知識的能力很強,但隨着我們開始長大后,慢慢學習能力、處事方式、性格品行,往往會固定。一方面是形成了各自的性格特徵,一方面是圈子已經固定。但也正因為這樣的故步,而很少願意聽取別人的意見,就像即使看到了一整片內容,在視覺盲區下也會過掉到80%,就在眼前也看不見,也因此導致了能力不再有較大的提升。

編程能力怎樣會成長的最快

工作內容往往有些像在工廠擰螺絲,大部分內容是重複的,也可以想象過去的一年你有過多少創新和學習了新的技能。那麼這時候一般為了多學些內容會買一些技術書籍,但!技術類書籍和其他書籍不同,只要不去用看了也就只是輕描淡寫,很難接納和理解。就像設計模式,雖然可能看了幾遍,但是在實際編碼中仍然很少會用,大部分原因還是沒有認認真真的跟着實操。事必躬親才是學習編程的最好是方式。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號:bugstack蟲洞棧,回復源碼下載獲取(打開獲取的鏈接,找到序號18)
工程 描述
itstack-demo-design-11-01 使用一坨代碼實現業務需求
itstack-demo-design-11-02 通過設計模式優化代碼結構,減少內存使用和查詢耗時

三、享元模式介紹

享元模式,主要在於共享通用對象,減少內存的使用,提升系統的訪問效率。而這部分共享對象通常比較耗費內存或者需要查詢大量接口或者使用數據庫資源,因此統一抽離作為共享對象使用。

另外享元模式可以分為在服務端和客戶端,一般互聯網H5和Web場景下大部分數據都需要服務端進行處理,比如數據庫連接池的使用、多線程線程池的使用,除了這些功能外,還有些需要服務端進行包裝后的處理下發給客戶端,因為服務端需要做享元處理。但在一些遊戲場景下,很多都是客戶端需要進行渲染地圖效果,比如;樹木、花草、魚蟲,通過設置不同元素描述使用享元公用對象,減少內存的佔用,讓客戶端的遊戲更加流暢。

在享元模型的實現中需要使用到享元工廠來進行管理這部分獨立的對象和共享的對象,避免出現線程安全的問題。

四、案例場景模擬

在這個案例中我們模擬在商品秒殺場景下使用享元模式查詢優化

你是否經歷過一個商品下單的項目從最初的日均十幾單到一個月後每個時段秒殺量破十萬的項目。一般在最初如果沒有經驗的情況下可能會使用數據庫行級鎖的方式下保證商品庫存的扣減操作,但是隨着業務的快速發展秒殺的用戶越來越多,這個時候數據庫已經扛不住了,一般都會使用redis的分佈式鎖來控制商品庫存。

同時在查詢的時候也不需要每一次對不同的活動查詢都從庫中獲取,因為這裏除了庫存以外其他的活動商品信息都是固定不變的,以此這裏一般大家會緩存到內存中。

這裏我們模擬使用享元模式工廠結構,提供活動商品的查詢。活動商品相當於不變的信息,而庫存部分屬於變化的信息。

五、用一坨坨代碼實現

邏輯很簡單,就怕你寫亂。一片片的固定內容和變化內容的查詢組合,CV的哪裡都是!

其實這部分邏輯的查詢在一般情況很多程序員都是先查詢固定信息,在使用過濾的或者添加if判斷的方式補充變化的信息,也就是庫存。這樣寫最開始並不會看出來有什麼問題,但隨着方法邏輯的增加,後面就越來越多重複的代碼。

1. 工程結構

itstack-demo-design-11-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── ActivityController.java
  • 以上工程結構比較簡單,之後一個控制類用於查詢活動信息。

2. 代碼實現

/**
 * 博客:https://bugstack.cn - 沉澱、分享、成長,讓自己和他人都能有所收穫!
 * 公眾號:bugstack蟲洞棧
 * Create by 小傅哥(fustack) @2020
 */
public class ActivityController {

    public Activity queryActivityInfo(Long id) {
        // 模擬從實際業務應用從接口中獲取活動信息
        Activity activity = new Activity();
        activity.setId(10001L);
        activity.setName("圖書嗨樂");
        activity.setDesc("圖書優惠券分享激勵分享活動第二期");
        activity.setStartTime(new Date());
        activity.setStopTime(new Date());
        activity.setStock(new Stock(1000,1));
        return activity;
    }

}
  • 這裏模擬的是從接口中查詢活動信息,基本也就是從數據庫中獲取所有的商品信息和庫存。有點像最開始寫的商品銷售系統,數據庫就可以抗住購物量。
  • 當後續因為業務的發展需要擴展代碼將庫存部分交給redis處理,那麼久需要從redis中獲取活動的庫存,而不是從庫中,否則將造成數據不統一的問題。

六、享元模式重構代碼

接下來使用享元模式來進行代碼優化,也算是一次很小的重構。

享元模式一般情況下使用此結構在平時的開發中並不太多,除了一些線程池、數據庫連接池外,再就是遊戲場景下的場景渲染。另外這個設計的模式思想是減少內存的使用提升效率,與我們之前使用的原型模式通過克隆對象的方式生成複雜對象,減少rpc的調用,都是此類思想。

1. 工程結構

itstack-demo-design-11-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── util
    │           │	└── RedisUtils.java	
    │           ├── Activity.java
    │           ├── ActivityController.java
    │           ├── ActivityFactory.java
    │           └── Stock.java
    └── test
        └── java
            └── org.itstack.demo.test
                └── ApiTest.java

享元模式模型結構

  • 以上是我們模擬查詢活動場景的類圖結構,左側構建的是享元工廠,提供固定活動數據的查詢,右側是Redis存放的庫存數據。
  • 最終交給活動控制類來處理查詢操作,並提供活動的所有信息和庫存。因為庫存是變化的,所以我們模擬的RedisUtils中設置了定時任務使用庫存。

2. 代碼實現

2.1 活動信息

public class Activity {

    private Long id;        // 活動ID
    private String name;    // 活動名稱
    private String desc;    // 活動描述
    private Date startTime; // 開始時間
    private Date stopTime;  // 結束時間
    private Stock stock;    // 活動庫存
    
    // ...get/set
}
  • 這裏的對象類比較簡單,只是一個活動的基礎信息;id、名稱、描述、時間和庫存。

2.2 庫存信息

public class Stock {

    private int total; // 庫存總量
    private int used;  // 庫存已用
    
    // ...get/set
}
  • 這裡是庫存數據我們單獨提供了一個類進行保存數據。

2.3 享元工廠

public class ActivityFactory {

    static Map<Long, Activity> activityMap = new HashMap<Long, Activity>();

    public static Activity getActivity(Long id) {
        Activity activity = activityMap.get(id);
        if (null == activity) {
            // 模擬從實際業務應用從接口中獲取活動信息
            activity = new Activity();
            activity.setId(10001L);
            activity.setName("圖書嗨樂");
            activity.setDesc("圖書優惠券分享激勵分享活動第二期");
            activity.setStartTime(new Date());
            activity.setStopTime(new Date());
            activityMap.put(id, activity);
        }
        return activity;
    }

}
  • 這裏提供的是一個享元工廠,通過map結構存放已經從庫表或者接口中查詢到的數據,存放到內存中,用於下次可以直接獲取。
  • 這樣的結構一般在我們的編程開發中還是比較常見的,當然也有些時候為了分佈式的獲取,會把數據存放到redis中,可以按需選擇。

2.4 模擬Redis類

public class RedisUtils {

    private ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);

    private AtomicInteger stock = new AtomicInteger(0);

    public RedisUtils() {
        scheduledExecutorService.scheduleAtFixedRate(() -> {
            // 模擬庫存消耗
            stock.addAndGet(1);
        }, 0, 100000, TimeUnit.MICROSECONDS);

    }

    public int getStockUsed() {
        return stock.get();
    }

}
  • 這裏處理模擬redis的操作工具類外,還提供了一個定時任務用於模擬庫存的使用,這樣方面我們在測試的時候可以觀察到庫存的變化。

2.4 活動控制類

public class ActivityController {

    private RedisUtils redisUtils = new RedisUtils();

    public Activity queryActivityInfo(Long id) {
        Activity activity = ActivityFactory.getActivity(id);
        // 模擬從Redis中獲取庫存變化信息
        Stock stock = new Stock(1000, redisUtils.getStockUsed());
        activity.setStock(stock);
        return activity;
    }

}
  • 在活動控制類中使用了享元工廠獲取活動信息,查詢后將庫存信息在補充上。因為庫存信息是變化的,而活動信息是固定不變的。
  • 最終通過統一的控制類就可以把完整包裝后的活動信息返回給調用方。

3. 測試驗證

3.1 編寫測試類

public class ApiTest {

    private Logger logger = LoggerFactory.getLogger(ApiTest.class);

    private ActivityController activityController = new ActivityController();

    @Test
    public void test_queryActivityInfo() throws InterruptedException {
        for (int idx = 0; idx < 10; idx++) {
            Long req = 10001L;
            Activity activity = activityController.queryActivityInfo(req);
            logger.info("測試結果:{} {}", req, JSON.toJSONString(activity));
            Thread.sleep(1200);
        }
    }

}
  • 這裏我們通過活動查詢控制類,在for循環的操作下查詢了十次活動信息,同時為了保證庫存定時任務的變化,加了睡眠操作,實際的開發中不會有這樣的睡眠。

3.2 測試結果

22:35:20.285 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":1},"stopTime":1592130919931}
22:35:21.634 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":18},"stopTime":1592130919931}
22:35:22.838 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":30},"stopTime":1592130919931}
22:35:24.042 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":42},"stopTime":1592130919931}
22:35:25.246 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":54},"stopTime":1592130919931}
22:35:26.452 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":66},"stopTime":1592130919931}
22:35:27.655 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":78},"stopTime":1592130919931}
22:35:28.859 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":90},"stopTime":1592130919931}
22:35:30.063 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":102},"stopTime":1592130919931}
22:35:31.268 [main] INFO  org.i..t.ApiTest - 測試結果:10001 {"desc":"圖書優惠券分享激勵分享活動第二期","id":10001,"name":"圖書嗨樂","startTime":1592130919931,"stock":{"total":1000,"used":114},"stopTime":1592130919931}

Process finished with exit code 0
  • 可以仔細看下stock部分的庫存是一直在變化的,其他部分是活動信息,是固定的,所以我們使用享元模式來將這樣的結構進行拆分。

七、總結

  • 關於享元模式的設計可以着重學習享元工廠的設計,在一些有大量重複對象可復用的場景下,使用此場景在服務端減少接口的調用,在客戶端減少內存的佔用。是這個設計模式的主要應用方式。
  • 另外通過map結構的使用方式也可以看到,使用一個固定id來存放和獲取對象,是非常關鍵的點。而且不只是在享元模式中使用,一些其他工廠模式、適配器模式、組合模式中都可以通過map結構存放服務供外部獲取,減少ifelse的判斷使用。
  • 當然除了這種設計的減少內存的使用優點外,也有它帶來的缺點,在一些複雜的業務處理場景,很不容易區分出內部和外部狀態,就像我們活動信息部分與庫存變化部分。如果不能很好的拆分,就會把享元工廠設計的非常混亂,難以維護。

八、推薦閱讀

  • 1. 重學 Java 設計模式:實戰工廠方法模式(多種類型商品發獎場景)
  • 2. 重學 Java 設計模式:實戰抽象工廠模式(替換Redis雙集群升級場景)
  • 3. 重學 Java 設計模式:實戰建造者模式(裝修物料組合套餐選配場景)
  • 4. 重學 Java 設計模式:實戰原型模式(多套試每人題目和答案亂序場景)
  • 5. 重學 Java 設計模式:實戰橋接模式(多支付渠道「微信、支付寶」與多支付模式「刷臉、指紋」場景)
  • 6. 重學 Java 設計模式:實戰組合模式(營銷差異化人群發券,決策樹引擎搭建場景)

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

使用Apache Spark和Apache Hudi構建分析數據湖

1. 引入

大多數現代數據湖都是基於某種分佈式文件系統(DFS),如HDFS或基於雲的存儲,如AWS S3構建的。遵循的基本原則之一是文件的“一次寫入多次讀取”訪問模型。這對於處理海量數據非常有用,如數百GB到TB的數據。

但是在構建分析數據湖時,更新數據並不罕見。根據不同場景,這些更新頻率可能是每小時一次,甚至可能是每天或每周一次。另外可能還需要在最新視圖、包含所有更新的歷史視圖甚至僅是最新增量視圖上運行分析。

通常這會導致使用用於流和批處理的多個系統,前者處理增量數據,而後者處理歷史數據。

處理存儲在HDFS上的數據時,維護增量更新的常見工作流程是這裏所述的Ingest-Reconcile-Compact-Purge策略。

Apache Hudi之類的框架在這裏便可發揮作用。它在後台為我們管理此工作流程,從而使我們的核心應用程序代碼更加簡潔,Hudi支持對最新數據視圖的查詢以及查詢在某個時間點的增量更改。

這篇文章將介紹Hudi的核心概念以及如何在Copy-On-Write模式下進行操作。

本篇文章項目源代碼放在github。

2. 大綱

  • 先決條件和框架版本
  • Hudi核心概念
  • 初始設置和依賴項
  • 使用CoW表

2.1 先決條件和框架版本

如果你事先了解如何使用scala編寫spark作業以及讀取和寫入parquet文件,那麼本篇文章理解起來將非常容易。

框架版本如下

  • JDK: openjdk 1.8.0_242
  • Scala: 2.12.8
  • Spark: 2.4.4
  • Hudi Spark bundle: 0.5.2-incubating

注意:在撰寫本文時,AWS EMR與Hudi v0.5.0-incubating集成在一起,該軟件包具有一個bug會導致upsert操作卡死或花費很長時間才能完成,可查看相關issue了解更多,該問題已在當前版本的Hudi(0.5.2-incubating及之後版本)中修復。如果計劃在AWS EMR上運行代碼,則可能要考慮用最新版本覆蓋默認的集成版本。

2.2 Hudi核心概念

先從一些需要理解的核心概念開始。

1. 表類型

Hudi支持兩種表類型

  • 寫時複製(CoW):寫入CoW表時,將運行Ingest-Reconcile-Compact-Purge周期。每次寫操作后,CoW表中的數據始終是最新記錄,對於需要儘快讀取最新數據的場景,可首選此模式。數據僅以列文件格式(parquet)存儲在CoW表中,由於每個寫操作都涉及壓縮和覆蓋,因此此模式產生的文件最少。

  • 讀時合併(MoR):MoR表專註於快速寫操作。寫入這些表將創建增量文件,隨後將其壓縮以生成讀取時的最新數據,壓縮操作可以同步或異步完成,數據以列文件格式(parquet)和基於行的文件格式(avro)組合存儲。

這是Hudi文檔中提到的兩種表格格式之間的權衡取捨。

Trade-off CoW MoR
數據延遲 Higher Lower
更新開銷 (I/O) Higher (重寫整個parquet文件) Lower (追加到delta log文件)
Parquet文件大小 Smaller (高update(I/0) 開銷) Larger (低更新開銷)
Write Amplification Higher Lower (由compaction策略決定)

2. 查詢類型

Hudi支持兩種主要類型的查詢:“快照查詢”和“增量查詢”。除兩種主要查詢類型外,MoR表還支持“讀優化查詢”。

  • 快照查詢:對於CoW表,快照查詢返回數據的最新視圖,而對於MoR表,則返回接近實時的視圖。 對於MoR表,快照查詢將即時合併基本文件和增量文件,因此可能會有一些讀取延遲。使用CoW,由於寫入負責合併,因此讀取很快,只需要讀取基本文件。

  • 增量查詢:增量查詢使您可以通過指定“開始”時間或在特定時間點通過指定“開始”和“結束”時間來查看特定提交時間之後的數據。

  • 讀優化查詢:對於MoR表,讀取優化查詢返回一個視圖,該視圖僅包含基本文件中的數據,而不合併增量文件。

3. 以Hudi格式寫入時的關鍵屬性

  • hoodie.datasource.write.table.type,定義表的類型-默認值為COPY_ON_WRITE。對於MoR表,將此值設置為MERGE_ON_READ。

  • hoodie.table.name,這是必填字段,每個表都應具有唯一的名稱。

  • hoodie.datasource.write.recordkey.field,將此視為表的主鍵。此屬性的值是DataFrame中列的名稱,該列是主鍵。

  • hoodie.datasource.write.precombine.field,更新數據時,如果存在兩個具有相同主鍵的記錄,則此列中的值將決定更新哪個記錄。選擇諸如時間戳記的列將確保選擇具有最新時間戳記的記錄。

  • hoodie.datasource.write.operation,定義寫操作的類型。值可以為upsert,insert,bulk_insert和delete,默認值為upsert。

2.3 初始設置和依賴項

1. 依賴說明

為了在Spark作業中使用Hudi,需要使用spark-sql,hudi-spark-bundle和spark-avro依賴項,此外還需要將Spark配置為使用KryoSerializer。

pom.xml大致內容如下

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <encoding>UTF-8</encoding>
    <scala.version>2.12.8</scala.version>
    <scala.compat.version>2.12</scala.compat.version>
    <spec2.version>4.2.0</spec2.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.scala-lang</groupId>
        <artifactId>scala-library</artifactId>
        <version>${scala.version}</version>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-sql_${scala.compat.version}</artifactId>
        <version>2.4.4</version>
    </dependency>
    <dependency>
        <groupId>org.apache.hudi</groupId>
        <artifactId>hudi-spark-bundle_${scala.compat.version}</artifactId>
        <version>0.5.2-incubating</version>
    </dependency>
    <dependency>
        <groupId>org.apache.spark</groupId>
        <artifactId>spark-avro_${scala.compat.version}</artifactId>
        <version>2.4.4</version>
    </dependency>
</dependencies>

2. 設置Schema

我們使用下面的Album類來表示表的schema。

case class Album(albumId: Long, title: String, tracks: Array[String], updateDate: Long)   

3. 生成測試數據

創建一些用於upsert操作的數據。

  • INITIAL_ALBUM_DATA有兩個記錄,鍵為801。
  • UPSERT_ALBUM_DATA包含一個更新的記錄和兩個新的記錄。
def dateToLong(dateString: String): Long = LocalDate.parse(dateString, formatter).toEpochDay

private val INITIAL_ALBUM_DATA = Seq(
    Album(800, "6 String Theory", Array("Lay it down", "Am I Wrong", "68"), dateToLong("2019-12-01")),
    Album(801, "Hail to the Thief", Array("2+2=5", "Backdrifts"), dateToLong("2019-12-01")),
    Album(801, "Hail to the Thief", Array("2+2=5", "Backdrifts", "Go to sleep"), dateToLong("2019-12-03"))
  )

  private val UPSERT_ALBUM_DATA = Seq(
    Album(800, "6 String Theory - Special", Array("Jumpin' the blues", "Bluesnote", "Birth of blues"), dateToLong("2020-01-03")),
    Album(802, "Best Of Jazz Blues", Array("Jumpin' the blues", "Bluesnote", "Birth of blues"), dateToLong("2020-01-04")),
    Album(803, "Birth of Cool", Array("Move", "Jeru", "Moon Dreams"), dateToLong("2020-02-03"))
  )

4. 初始化SparkContext

最後初始化Spark上下文。這裏要注意的重要一點是KryoSerializer的使用。

val spark: SparkSession = SparkSession.builder()
    .appName("hudi-datalake")
    .master("local[*]")
    .config("spark.serializer", "org.apache.spark.serializer.KryoSerializer")
    .config("spark.sql.hive.convertMetastoreParquet", "false") // Uses Hive SerDe, this is mandatory for MoR tables
    .getOrCreate()

2.4 使用CoW表

本節將處理CoW表的記錄,如讀取和刪除記錄。

1. basePath(基本路徑)和Upsert方法

定義一個basePath,upsert方法會將表數據寫入該路徑,該方法將以org.apache.hudi格式寫入Dataframe,請確保上面討論的所有Hudi屬性均已設置。

val basePath = "/tmp/store"

private def upsert(albumDf: DataFrame, tableName: String, key: String, combineKey: String) = {
    albumDf.write
      .format("hudi")
      .option(DataSourceWriteOptions.TABLE_TYPE_OPT_KEY, DataSourceWriteOptions.COW_TABLE_TYPE_OPT_VAL)
      .option(DataSourceWriteOptions.RECORDKEY_FIELD_OPT_KEY, key)
      .option(DataSourceWriteOptions.PRECOMBINE_FIELD_OPT_KEY, combineKey)
      .option(HoodieWriteConfig.TABLE_NAME, tableName)
      .option(DataSourceWriteOptions.OPERATION_OPT_KEY, DataSourceWriteOptions.UPSERT_OPERATION_OPT_VAL)
      // Ignore this property for now, the default is too high when experimenting on your local machine
      // Set this to a lower value to improve performance.
      // I'll probably cover Hudi tuning in a separate post.
      .option("hoodie.upsert.shuffle.parallelism", "2")
      .mode(SaveMode.Append)
      .save(s"$basePath/$tableName/")
  }

2. 初始化upsert

插入INITIAL_ALBUM_DATA,我們應該創建2條記錄,對於801,該記錄的日期為2019-12-03。

val tableName = "Album"
upsert(INITIAL_ALBUM_DATA.toDF(), tableName, "albumId", "updateDate")
spark.read.format("hudi").load(s"$basePath/$tableName/*").show()

讀取CoW表就像使用格式(“hudl”)的常規spark.read一樣簡單。

// Output
+-------------------+--------------------+------------------+----------------------+--------------------+-------+-----------------+--------------------+----------+
|_hoodie_commit_time|_hoodie_commit_seqno|_hoodie_record_key|_hoodie_partition_path|   _hoodie_file_name|albumId|            title|              tracks|updateDate|
+-------------------+--------------------+------------------+----------------------+--------------------+-------+-----------------+--------------------+----------+
|     20200412182343|  20200412182343_0_1|               801|               default|65841d0a-0083-447...|    801|Hail to the Thief|[2+2=5, Backdrift...|     18233|
|     20200412182343|  20200412182343_0_2|               800|               default|65841d0a-0083-447...|    800|  6 String Theory|[Lay it down, Am ...|     18231|
+-------------------+--------------------+------------------+----------------------+--------------------+-------+-----------------+--------------------+----------+

另一種確定的方法是查看Workload profile的日誌輸出,內容大致如下

Workload profile :WorkloadProfile {globalStat=WorkloadStat {numInserts=2, numUpdates=0}, partitionStat={default=WorkloadStat {numInserts=2, numUpdates=0}}}

3. 更新記錄

upsert(UPSERT_ALBUM_DATA.toDF(), tableName, "albumId", "updateDate")

查看Workload profile的日誌輸出,並驗證它是否符合預期

Workload profile :WorkloadProfile {globalStat=WorkloadStat {numInserts=2, numUpdates=1}, partitionStat={default=WorkloadStat {numInserts=2, numUpdates=1}}}

查詢輸出如下

spark.read.format("hudi").load(s"$basePath/$tableName/*").show()

//Output
+-------------------+--------------------+------------------+----------------------+--------------------+-------+--------------------+--------------------+----------+
|_hoodie_commit_time|_hoodie_commit_seqno|_hoodie_record_key|_hoodie_partition_path|   _hoodie_file_name|albumId|               title|              tracks|updateDate|
+-------------------+--------------------+------------------+----------------------+--------------------+-------+--------------------+--------------------+----------+
|     20200412183510|  20200412183510_0_1|               801|               default|65841d0a-0083-447...|    801|   Hail to the Thief|[2+2=5, Backdrift...|     18233|
|     20200412184040|  20200412184040_0_1|               800|               default|65841d0a-0083-447...|    800|6 String Theory -...|[Jumpin' the blue...|     18264|
|     20200412184040|  20200412184040_0_2|               802|               default|65841d0a-0083-447...|    802|  Best Of Jazz Blues|[Jumpin' the blue...|     18265|
|     20200412184040|  20200412184040_0_3|               803|               default|65841d0a-0083-447...|    803|       Birth of Cool|[Move, Jeru, Moon...|     18295|
+-------------------+--------------------+------------------+----------------------+--------------------+-------+--------------------+--------------------+----------+

4. 查詢記錄

我們在上面查看數據的方式稱為“快照查詢”,這是默認設置,另外還支持“增量查詢”。

4.1 增量查詢

要執行增量查詢,我們需要在讀取時將hoodie.datasource.query.type屬性設置為incremental,並指定hoodie.datasource.read.begin.instanttime屬性。 這將在指定的即時時間之後讀取所有記錄,對於本示例,我們將instantTime指定為20200412183510。

spark.read
    .format("hudi")
    .option(DataSourceReadOptions.QUERY_TYPE_OPT_KEY, DataSourceReadOptions.QUERY_TYPE_INCREMENTAL_OPT_VAL)
    .option(DataSourceReadOptions.BEGIN_INSTANTTIME_OPT_KEY, "20200412183510")
    .load(s"$basePath/$tableName")
    .show()

這將在提交時間20200412183510之後返回所有記錄。

+-------------------+--------------------+------------------+----------------------+--------------------+-------+--------------------+--------------------+----------+
|_hoodie_commit_time|_hoodie_commit_seqno|_hoodie_record_key|_hoodie_partition_path|   _hoodie_file_name|albumId|               title|              tracks|updateDate|
+-------------------+--------------------+------------------+----------------------+--------------------+-------+--------------------+--------------------+----------+
|     20200412184040|  20200412184040_0_1|               800|               default|65841d0a-0083-447...|    800|6 String Theory -...|[Jumpin' the blue...|     18264|
|     20200412184040|  20200412184040_0_2|               802|               default|65841d0a-0083-447...|    802|  Best Of Jazz Blues|[Jumpin' the blue...|     18265|
|     20200412184040|  20200412184040_0_3|               803|               default|65841d0a-0083-447...|    803|       Birth of Cool|[Move, Jeru, Moon...|     18295|
+-------------------+--------------------+------------------+----------------------+--------------------+-------+--------------------+--------------------+----------+

5. 刪除記錄

我們要查看的最後一個操作是刪除,刪除類似於upsert,需要一個待刪除記錄的DataFrame,如下面的示例代碼所示,不需要整行,只需要主鍵即可。

val deleteKeys = Seq(
    Album(803, "", null, 0l),
    Album(802, "", null, 0l)
)

import spark.implicits._

val df = deleteKeys.toDF()

df.write.format("hudi")
    .option(DataSourceWriteOptions.TABLE_TYPE_OPT_KEY, DataSourceWriteOptions.COW_TABLE_TYPE_OPT_VAL)
    .option(DataSourceWriteOptions.RECORDKEY_FIELD_OPT_KEY, "albumId")
    .option(HoodieWriteConfig.TABLE_NAME, tableName)
    // Set the option "hoodie.datasource.write.operation" to "delete"
    .option(DataSourceWriteOptions.OPERATION_OPT_KEY, DataSourceWriteOptions.DELETE_OPERATION_OPT_VAL)
    .mode(SaveMode.Append) // Only Append Mode is supported for Delete.
    .save(s"$basePath/$tableName/")

spark.read.format("hudi").load(s"$basePath/$tableName/*").show()

這是本部分介紹的全部內容。後面我們將探討在MERGE-ON-READ表進行操作。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※超省錢租車方案

※別再煩惱如何寫文案,掌握八大原則!

※回頭車貨運收費標準

※教你寫出一流的銷售文案?

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

20萬出頭,這款最便宜的奧迪SUV,車主說這個缺點比較明顯

1/6。7百公里油耗(L):6。3、7、7。4車主百公里油耗(L):8。3、10。2驅動方式:前置前驅/前置四驅底盤懸挂:前麥弗遜/后四多連桿空間實際體驗(體驗者172cm):前排頭部1拳/後排頭部4指/後排腿部2拳2指,後排乘坐空間表現良好。

奧迪Q3的定價不高,而且在銷售終端也有着較大的市場優惠,這也是我們在街上經常能看見它的原因之一。

它的車內乘坐空間不錯,動力充足,操控性頗為靈活!造型也時尚、富有質感,對於年輕的帥哥、美女來說是比較具有吸引力的。而今天虎哥就和大家聊聊它的各方面表現,還有車主們對它的評價!

長寬高:4398*1841*1591mm

軸距:2603mm

定位:緊湊型SUV

奧迪Q3的外觀設計圓潤、飽滿,車身側面的線條相當簡約,而尾部的造型收穫的讚譽最多,豐滿的車尾造型,辨識度頗高,整體的造型頗為時尚、精緻,十分適合年輕群體。

奧迪Q3的內飾設計顯得有些保守,但是它用料不錯,內飾採用大面積軟性材料覆蓋,做工細緻度高,而且各個按鍵布局清晰,操作比較方便!

發動機:1.4T/2.0T

最大馬力(pS):150/180

最大扭矩(Nm):250/320/350

變速箱:6擋雙離合/7擋雙離合

百公里加速(s):9.2/8.1/6.7

百公里油耗(L):6.3、7、7.4

車主百公里油耗(L):8.3、10.2

驅動方式:前置前驅/前置四驅

底盤懸挂:前麥弗遜/后四多連桿

空間實際體驗(體驗者172cm):前排頭部1拳/後排頭部4指/後排腿部2拳2指,後排乘坐空間表現良好。

感興趣的朋友可以點擊小程序查看詳細口碑,從口碑中可以看到車主們對它的動力、油耗表現頗為滿意,對於配置方面也有挺高的評價。而對於後排空間頭部空間則有一些投訴。

咱們發現奧迪Q3的市場優惠幅度還是很不錯的,在廣州、武漢等地區需要搭配店內上保險、店內貸款等項目,在北京、上海、成都還需要搭配加裝飾、店內上保險、店內上牌等項目。

奧迪Q3的乘坐空間是夠用的,而且變速箱換擋速度快、傳動效率高,在動力性能方面它表現挺好!日常駕駛時它轉向輕鬆,最重要是實惠的終端價格讓不少年輕消費者也願意為它掏腰包。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化

空間同級最大,30來萬的SUV能優惠6萬,車主卻說……

5T/2。0T最大馬力(pS):136/192/231最大扭矩(Nm):220/280/350變速箱:6AT/8AT百公里加速(s):9。46/7。67/7。06百公里油耗(L):暫無/8。6車主百公里油耗(L):9。08/9。4驅動方式:前置前驅/前置四驅底盤懸挂:前麥弗遜/后多連桿前排腿部空間(mm):860-1070前排高度(mm):980前排寬度(mm):1480後排腿部空間(mm):710-950後排高度(mm):970後排寬度(mm):1470實際體驗(體驗者184cm):前排頭部1拳2指/后

寶馬X1擁有BBA緊湊型SUV車型中最大的車身尺寸,因此在乘坐空間與儲物空間方面也是這些車型中表現最突出的!這對於已經成家立室的用戶來說尤為重要。

而它的舒適性表現不錯,造型設計也極具寶馬的家族特色,富有力量感!那麼這款車型的各方面表現如何?下面咱們一起來看看。

長寬高:4565*1821*1624mm

軸距:2780mm

定位:緊湊型SUV

在外觀方面,它肌肉線條比較突顯,車身比例也是標準的SUV風格,車身厚重感不錯。而且車身尺寸較大,軸距也針對中國市場加長了110mm,車身更為修長,運動感也相當不錯!它的尾部造型也簡約、幹練,層次感很不錯。

而它的中控檯面設計簡約,在行駛過程中操作、查看都很方便,這也是UKL平台車型慣用的設計風格,缺乏一些新意。它的用料不錯,採用了較多的軟質材料覆蓋,而配置也頗為豐富。

發動機:1.5T/2.0T

最大馬力(pS):136/192/231

最大扭矩(Nm):220/280/350

變速箱:6AT/8AT

百公里加速(s):9.46/7.67/7.06

百公里油耗(L):暫無/8.6

車主百公里油耗(L):9.08/9.4

驅動方式:前置前驅/前置四驅

底盤懸挂:前麥弗遜/后多連桿

前排腿部空間(mm):860-1070

前排高度(mm):980

前排寬度(mm):1480

後排腿部空間(mm):710-950

後排高度(mm):970

後排寬度(mm):1470

實際體驗(體驗者184cm):前排頭部1拳2指/後排頭部4指/後排腿部2拳2指,乘坐空間是相當充裕的!

感興趣的朋友可以點擊小程序查看詳細口碑,從口碑中可以看到車主們對寶馬X1陽剛的外觀設計、充沛的乘坐空間、駕駛感受是很滿意的,同時車主們也提出它在座椅舒適度、內飾用料方面有再進步的空間。

咱們發現寶馬X1的優惠幅度還是相當可觀的,但是在廣州、武漢、上海等地需要搭配加裝飾、店內上保險、貸款、上牌等項目,所以實際的優惠幅度還需要諮詢當地的4S店!

寶馬X1的乘坐空間相當不錯,它具備了陽剛、有力的造型設計,駕駛感受也可圈可點。而它的定價較高,但在一些地區有着可觀的優惠幅度,想要購買這款車型的朋友不妨先去試車感受一下!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※網頁設計一頭霧水該從何著手呢? 台北網頁設計公司幫您輕鬆架站!

※網頁設計公司推薦不同的風格,搶佔消費者視覺第一線

※Google地圖已可更新顯示潭子電動車充電站設置地點!!

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※別再煩惱如何寫文案,掌握八大原則!

※網頁設計最專業,超強功能平台可客製化