標籤: 包裝設計

  • 【實拍】能賣得過凱美瑞邁騰?20萬級最熱門中型車到了!

    【實拍】能賣得過凱美瑞邁騰?20萬級最熱門中型車到了!

    而車尾造型簡潔,還帶有小鴨尾式的隆起。比較讓人費解的是它那雙C字型尾燈,雖然個性,但放在一款大尺寸的B級車身上顯得小氣,與簡潔明了的頭燈也沒太大呼應。內裝部分,它的設計沒有我們想象中那麼動感,造型四平八穩,反倒是具有幾分商務氣息。

    近日,虎哥收到一則重磅消息,那就是在廣州地區的一家廣汽本田4S店已經有全新一代雅閣到店!聽到這一則消息的虎哥馬上放開手中的糯米雞,帶上小夥伴飛奔到這家廣汽本田4S店!

    視頻看完還沒過癮?咱們繼續往下看。

    其實雅閣對於國內朋友來說是一款相當熟悉的車型,而對於廣東地區的朋友來說,它更加是屬於具有特殊情懷的車型!早在1999年3月份,第6代雅閣就正式在廣州實現國產,作為廣汽本田成立后推出的第一款車型它被寄予厚望。

    而截止到2002年第6代車型停產,它銷量一直很火爆,耐用、駕駛舒適成為它的最大標籤!在1999年-2002年間它銷量累計達到了13.8萬

    輛!

    緊接着推出的7代雅閣也一度造成一車難求的現象,也是經過兩代車型的努力,雅閣車型在家用中型車市場確立了標杆地位。

    而8代雅閣以前衛、富有動感的外觀,實用的大空間亦獲得優秀的市場表現。

    然而,從9代雅閣開始,“鍍鉻狂魔式”的前臉造型,和略顯臃腫的車身線條讓雅閣車型的油膩指數直線飆升!也是在這一代開始人們更多的認為它其實是一輛大叔座駕。

    當然,廣汽本田也意識到這代車型設計有些用力過猛,所以在後來改款的9.5代設計向運動感回歸,銷量得以上升!

    時至今日,第10代雅閣也即將來到我們身邊,“油膩”這個標籤也徹底與它脫離了關係!而關於它的具體細節我們就來細細分析。

    10代雅閣的前臉營造出強烈的視覺衝擊力,看實車的時候這種感覺尤為明顯。中網上粗壯的一條鍍鉻飾條立體感強,而頭燈內部一字排開的LED燈組,十分不低調!

    (注:本文圖片拍攝場地為廣汽本田第一店)

    而設計師為側面造型畫下了濃墨重彩的一筆,Coupe式的車身設計,具有溜背式的車尾造型,車頂弧線平滑。而A柱的位置比上一代車型延後了100mm,發動機艙更修長、造型也被壓得更低,運動感已相當明顯。

    輪轂的造型比較動感,但從目前的信息來看,雅閣的1.5T頂配車型也只配備17英寸的輪圈,它放在10代雅閣身上只是顯得剛剛夠用。想營造出讓人熱血噴張的運動感,它遠遠不行。或者廠家是出於對減低油耗的考慮,才限制了輪圈規格。畢竟它採用的是米其林primacy 3st浩悅系列 225/50 R17 規格的輪胎,它是一款注重降低油耗同時具備一定的操控性的輪胎型號。

    尾部帶有真雙出排氣,這點是比較厚道的。而車尾造型簡潔,還帶有小鴨尾式的隆起。比較讓人費解的是它那雙C字型尾燈,雖然個性,但放在一款大尺寸的B級車身上顯得小氣,與簡潔明了的頭燈也沒太大呼應。

    內裝部分,它的設計沒有我們想象中那麼動感,造型四平八穩,反倒是具有幾分商務氣息。

    而新車型的內飾在細節處理方面有了較明顯進步。它加入了深色的木紋飾板和金屬拉絲材質進行點綴,比以前採用的鋼琴烤漆更顯檔次感。而且做工水準也更符合B級車定位。

    儀錶盤左側設置了液晶显示屏,它能显示的多種信息,實用性不錯。值得一提的是,中控台上獨立式的8英寸多媒體觸控屏操作流暢,系統反應很快,而且显示界面也簡潔而細膩!比上一代車型好用不少,而兩側設置有實體按鍵,在開車過程中盲操作也便利,這提高了駕駛安全性。

    而中控屏支撐Honda CONNECT功能,這是一套本田開發的智能互聯繫統。頂配車型帶有前排座椅通風、加熱功能,雙區自動空調還帶有空氣凈化功能,而詳細的配置信息會在後期公布。

    這個內飾,並不能給人驚艷感,但它實用性很不錯,我想用一個“功能產生美”來形容它最貼切!

    本田的乘坐空間最大化理念在新車型上依然有很好的體現,它溜背式的尾部設計沒有對後排頭部空間帶來過多影響。而對於身高177cm的虎哥來說,它營造出接近3拳的腿部空間,翹個二郎腿很合適。

    而座椅柔軟程度和日產天籟有得一拼,坐墊長度也足夠,對於大腿的承托到位,中央扶手的長度和高度也到位,舒適性高!

    只是稍有遺憾的是後排乘客能享受到的配置基本上只有後排出風口和中央扶手上帶有的兩個杯架。

    關於新車預定:

    銷售人員表示,第10代雅閣現在已經接受預定,喜歡它的朋友可以多加留意,因為車型還沒上市,所以提車時間現在還不能確定。

    來到4S店看全新雅閣的人真的很多,可見它對於消費者吸引力還是比較大,而與這些潛在客戶交談時,他們也表示新車型內飾實用性強、做工有了進步!

    寫在最後:第10代雅閣即將到來,它的造型無疑有了脫胎換骨般的變化,其造型確實富有運動感、衝擊力!而內飾的質感得到進步,但是整體水準仍然只屬於B級車應該有的水平,乘坐空間表現出色,座椅很舒適!1.5T+CVT的動力總成雖值得期待,但2.0T+10AT車型的缺失確實有些遺憾,不過作為主力銷售的1.5T車型到底具備多大的競爭力,那就要等待後期它的配置和價格信息公布!

    特別鳴謝以下經銷商提供拍攝車輛:廣汽本田第一店;

    電話:020-36312608;地址:廣東省廣州市白雲區黃石東路448號。本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

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

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

  • 12萬買1.6T合資緊湊型SUV?看完恍然大悟!

    12萬買1.6T合資緊湊型SUV?看完恍然大悟!

    7-2。9米C級=中大型車長度4。8米-5米,軸距2。8-3米,排量超過2。4LD級=大型車長度超過5米,軸距超過3米,排量超過3L呃,根據這個分類方法,不同的車型基本都可以找到各自對應的分類,而且通過看車型的分類級別,就能一目瞭然找到自己要的大小的車子。

    昨晚,現代ENCINO上市

    除了分體式大燈以及標配的1.6T發動機十分好看之外

    還注意到一個有意思的地方

    有媒體竟然叫它緊湊型SUV!

    緊湊型SUV?

    就是和CR-V、RAV4榮放一個級別?

    只要12萬而且還標配1.6T?

    這也太划算了吧

    然而,事情沒那麼簡單,這貨的尺寸為4195*1800*1575mm,軸距為2600mm

    長度和軸距甚至還不如本田XR-V這種小型SUV

    比起中國車企的東南DX3、傳祺GS3等小型SUV也差了不少

    這樣的尺寸也敢叫緊湊型SUV,是梁靜茹給它的勇氣嗎?

    吐槽的同時

    也必須弄清一個概念

    汽車級別怎麼劃分的?

    如今多數媒體車企給車型劃分分類的標準多是歐洲標準

    也就是大眾汽車的分級辦法,綜合排量、車型大小等因素分為

    A00級、A0級、A級、B級、C級、D級。

    A00級=微型車

    長度4米內,軸距2-2.3米

    A0級=小型車

    長度4-4.3米,軸距2.3-2.5米

    A級=緊湊型車

    長度4.2-4.6米,軸距2.5-2.7米

    B級=中型車

    長度4.5-4.9米,軸距2.7-2.9米

    C級=中大型車

    長度4.8米-5米,軸距2.8-3米,排量超過2.4L

    D級=大型車

    長度超過5米,軸距超過3米,排量超過3L

    呃,根據這個分類方法,不同的車型基本都可以找到各自對應的分類,而且通過看車型的分類級別,就能一目瞭然找到自己要的大小的車子。因此這一套分類方法十分流行。

    然而,這套方法的分級卻經常被車企混用,比如這一次,尺寸4195*1800*1575mm,軸距為2600mm的ENCINO也敢叫自己緊湊型SUV。

    除了ENCINO之外,還有不少這類型的例子:

    咱們熟悉的緊湊型轎車科魯茲,在官網pDF上把自己叫做“新銳性能中級車”,這,莫非科魯茲是中型轎車?

    非也,中型車與中級車一字之差,但是差距可不是一星半點,前文中說到中型車是歐洲分類標準,而中級車則是咱們中國的標準了,依照中國汽車分類標準(GB9417-89)的分級方法,中級車屬於排量1.6-2.5L的車型,因此依照這個標準來看,科魯茲還確實是中級車,而且大多數緊湊型車也確實可以叫自己中級車,不過在咱們大多數人的理解中,中級車=中型車啊!因此科魯茲也確實有鑽這個空子的嫌疑。

    為了產品賣得好一點,吹出一點牛皮也是合情合理的,不過相比上面兩款車型的手法,下面這些才是真大佬!

    奔馳S級:再次發明汽車

    奔馳S級在上市之初打出了許多十分誇張的口號,比如:“汽車發明者,再次發明汽車”“再見愛迪生”等等,雖然S級從設計的角度來說確實達到了一個新高度,但是再次發明汽車的口號也有些太狂了。

    昂科威:百萬級最好的隔音

    昂科威是別克旗下的中型SUV,售價21.99-31.99萬,這個價位的SUV老老實實賣車才是王道,然而昂科威並不安分,在上市之初昂科威便把百萬內最好的隔音作為賣點,要知道不同價位車型之間的差別可是十分大的,昂科威這口號也是夠大膽的,不過經實測,昂科威隔音確實比百萬級的卡宴更好。

    君越:圖書館級靜音

    同樣宣傳隔音的還有君越,這一次君越使用了圖書館級靜音水準這個詞彙,而根據《圖書館、博物館、美術館、展覽館衛生標準》(GB9669-1996)規定,圖書館的噪聲標準為≤50dB(A),這樣的噪音數值恐怕君越只有怠速工況下能夠達到吧~

    攬勝:越野車中的勞斯萊斯

    嚴格來說,這個稱號是廣大粉絲送的,不過也是非常霸氣的一個稱號了,除了越野車中的勞斯萊斯之外,路虎還有英國皇室狩獵專用車等頭銜,不過勞斯萊斯的越野車馬上就要上市了…

    總結:

    汽車廣告與宣傳中往往用到許多誇張的詞彙,越級、澎湃、奢華等詞語的出鏡率十分高,這樣的宣傳往往能讓人印象深刻,不過如果真的太相信這些宣傳詞彙,到頭來往往會讓人失望,汽車說到底也只是普通商品,既然是商品那麼一分錢一分貨這個道理還是適用的,用10萬元買到20萬的品質這種事情往往不會存在的,作為消費者,在看車企宣傳的同時一定要自己辨別,這樣才能避免被騙哦~本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 空間大做工好!這是6萬元區間最好的7座車?

    空間大做工好!這是6萬元區間最好的7座車?

    相比較7座SUV來說,歐尚A800的後備箱也小有優勢,主要體現在A800的後備箱高度和進深上,在這兩個參數上A800十分有優勢,超過1米的後備箱高度十分誇張。A800搭載了一台1。5T渦輪增壓發動機,型號為JL476ZQCD,這台全鋁發動機帶有DVVT技術,最大功率156馬力,最大扭矩225牛米,參數並不是很高。

    看過了非常適合家用的SUV奇駿、夠大夠霸氣的銳界、大氣實用的奧德賽、精緻好用的途安L之後,你是覺得SUV好還是MpV好呢?有興趣的朋友可以點擊鏈接查看往期文章:

    奧德賽:67.9分

    途安L:64.6分

    銳界:66分

    奇駿:65.4分

    說起8萬左右的MpV車型,就不得不提歐尚A800了!它最大的特點當然是空間、動力以及配置,這些方面它絲毫不遜於對手寶駿,而且由於這台車還是我們的工作車的原因,長期使用下來我們對它也是非常熟悉,歐尚A800外表雖然不算出色,但是論及內在絕對是一名出色的选手!

    在測試中歐尚A800也表現出了強大的實力,無論是在外觀品質、動力表現以及車內空間上都可圈可點。

    相比較長安以往的車型,歐尚A800在設計上盡量營造出時尚感與精緻感,從外觀很多細節上都能看到它的設計思路,這樣的造型設計顯然是成功的,A800雖然尺寸龐大,但是看上去卻並不顯臃腫,而且較大的車窗也能夠提供非常不錯的採光。

    內飾也是如此,我們這台高配車型中控台非常簡潔,碩大的屏幕與空調操作區的按鈕擺放都很有檔次感,全液晶儀錶盤在這個價格區間的車型里也十分少見,加上內飾的材質比較考究,整體營造的氛圍還是不錯的。

    A800的外觀工藝相比較更高價位的車型也毫不遜色,無論是外觀的鈑金縫隙,還是車漆的噴漆均勻度都很不錯,不過車漆的厚度平均不足100微米則有點太薄了。

    雖然內飾看上去不錯,但是受限於價格,A800在內飾材質上大面積使用了硬塑料,如果真的談及觸感的話還是顯得有一些廉價,不過好在內飾的拼裝工藝還是不錯的,塑料件也沒有毛刺。

    有了龐大的尺寸以及方正的設計,A800的內部空間可以說十分寬裕,無論是前排後排還是第三排空間都可以用寬敞來形容,而且A800的第二排還是採用獨立座椅設計,相比較大多數轎車來說都要更加舒適,不過受限於第三排地板以及空間,第三排的座椅規格比前兩排要小一些,硬度上也更硬一點。

    相比較7座SUV來說,歐尚A800的後備箱也小有優勢,主要體現在A800的後備箱高度和進深上,在這兩個參數上A800十分有優勢,超過1米的後備箱高度十分誇張。

    A800搭載了一台1.5T渦輪增壓發動機,型號為JL476ZQCD,這台全鋁發動機帶有DVVT技術,最大功率156馬力,最大扭矩225牛米,參數並不是很高。

    與之匹配的是6擋手動變速箱,這台變速箱齒比比較綿密,尤其是前兩個擋位可以說是為拉貨設計的,非常大的齒比對於載重來說是一件好事。

    不過由於齒比比較綿密,因此在加速上A800就有些吃虧了,2擋僅能跑到70km/h的速度來,再升上3擋之後才能破百,而3擋的加速度就遠不如1/2擋了,因此最終A800的破百成績為12.5秒,這樣的成績對於這台大傢伙來說倒也還算可以。

    作為一台MpV車型,A800顯然和運動扯不上關係,對於這類車型來說我們的要求也就是好開,從這個角度考慮A800確實算得上不錯,首先A800的離合點十分清晰,變速箱的換擋手感也不錯!加上發動機的低扭還算不錯,開起來比較得心應手。

    不過由於尺寸龐大且車身較高,懸挂也偏軟,因此A800在高速行駛的穩定性上和轎車以及多數SUV比還是不佔優勢,尤其是面對橫風的時候需要更加集中精力駕駛。

    雖然加速成績是橫評車型里最慢的,不過在實際動力感受上還是不錯,尤其是低速駕駛的時候會感覺車子很有力,再加上不錯的變速箱,A800是一台很能輕鬆駕馭的手動擋車型。

    對於這類型的MpV,其實最讓人擔心的就是隔音了,由於車內空間比較大,車子的迎風面積也大,所以容易在第二/三排產生較大的共鳴聲和風聲,不過在實際體驗中A800這個問題倒也不算嚴重,當然相比較轎車那肯定是差一些了。

    在售價上歐尚A800的指導價算是自主入門MpV中比較低的了,性價比還是不錯的。

    A800在諸多方面的表現都堪稱出色,優異的配置、不錯的駕駛感受和寬敞的空間都是它的優勢所在,對於這個價位買車的消費者來說這恰恰也是它們最關心的,再加上較低的售價使得這款車有了不錯的性價比,所以在6-9萬的MpV市場中A800確實算得上一個稱心的好選擇!

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • 老大吩咐的可重入分佈式鎖,終於完美的實現了!!!

    老大吩咐的可重入分佈式鎖,終於完美的實現了!!!

    重做永遠比改造簡單

    最近在做一個項目,將一個其他公司的實現系統(下文稱作舊系統),完整的整合到自己公司的系統(下文稱作新系統)中,這其中需要將對方實現的功能完整在自己系統也實現一遍。

    舊系統還有一批存量商戶,為了不影響存量商戶的體驗,新系統提供的對外接口,還必須得跟以前一致。最後系統完整切換之後,功能只運行在新系統中,這就要求舊系統的數據還需要完整的遷移到新系統中。

    當然這些在做這個項目之前就有預期,想過這個過程很難,但是沒想到有那麼難。原本感覺排期大半年,時間還是挺寬裕,現在感覺就是大坑,還不得不在坑裡一點點去填。

    哎,說多都是淚,不吐槽了,等到下次做完再給大家復盤下真正心得體會。

    回到正文,上篇文章Redis 分佈式鎖,咱們基於 Redis 實現一個分佈式鎖。這個分佈式鎖基本功能沒什麼問題,但是缺少可重入的特性,所以這篇文章小黑哥就帶大家來實現一下可重入的分佈式鎖。

    本篇文章將會涉及以下內容:

    • 可重入
    • 基於 ThreadLocal 實現方案
    • 基於 Redis Hash 實現方案

    先贊后看,養成習慣。微信搜索「程序通事」,關注就完事了~

    可重入

    說到可重入鎖,首先我們來看看一段來自 wiki 上可重入的解釋:

    若一個程序或子程序可以“在任意時刻被中斷然後操作系統調度執行另外一段代碼,這段代碼又調用了該子程序不會出錯”,則稱其為可重入(reentrant或re-entrant)的。即當該子程序正在運行時,執行線程可以再次進入並執行它,仍然獲得符合設計時預期的結果。與多線程併發執行的線程安全不同,可重入強調對單個線程執行時重新進入同一個子程序仍然是安全的。

    當一個線程執行一段代碼成功獲取鎖之後,繼續執行時,又遇到加鎖的代碼,可重入性就就保證線程能繼續執行,而不可重入就是需要等待鎖釋放之後,再次獲取鎖成功,才能繼續往下執行。

    用一段 Java 代碼解釋可重入:

    public synchronized void a() {
        b();
    }
    
    public synchronized void b() {
        // pass
    }
    

    假設 X 線程在 a 方法獲取鎖之後,繼續執行 b 方法,如果此時不可重入,線程就必須等待鎖釋放,再次爭搶鎖。

    鎖明明是被 X 線程擁有,卻還需要等待自己釋放鎖,然後再去搶鎖,這看起來就很奇怪,我釋放我自己~

    可重入性就可以解決這個尷尬的問題,當線程擁有鎖之後,往後再遇到加鎖方法,直接將加鎖次數加 1,然後再執行方法邏輯。退出加鎖方法之後,加鎖次數再減 1,當加鎖次數為 0 時,鎖才被真正的釋放。

    可以看到可重入鎖最大特性就是計數,計算加鎖的次數。所以當可重入鎖需要在分佈式環境實現時,我們也就需要統計加鎖次數。

    分佈式可重入鎖實現方式有兩種:

    • 基於 ThreadLocal 實現方案
    • 基於 Redis Hash 實現方案

    首先我們看下基於 ThreadLocal 實現方案。

    基於 ThreadLocal 實現方案

    實現方式

    Java 中 ThreadLocal可以使每個線程擁有自己的實例副本,我們可以利用這個特性對線程重入次數進行技術。

    下面我們定義一個ThreadLocal的全局變量 LOCKS,內存存儲 Map 實例變量。

    private static ThreadLocal<Map<String, Integer>> LOCKS = ThreadLocal.withInitial(HashMap::new);
    

    每個線程都可以通過 ThreadLocal獲取自己的 Map實例,Mapkey 存儲鎖的名稱,而 value存儲鎖的重入次數。

    加鎖的代碼如下:

    /**
     * 可重入鎖
     *
     * @param lockName  鎖名字,代表需要爭臨界資源
     * @param request   唯一標識,可以使用 uuid,根據該值判斷是否可以重入
     * @param leaseTime 鎖釋放時間
     * @param unit      鎖釋放時間單位
     * @return
     */
    public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
        Map<String, Integer> counts = LOCKS.get();
        if (counts.containsKey(lockName)) {
            counts.put(lockName, counts.get(lockName) + 1);
            return true;
        } else {
            if (redisLock.tryLock(lockName, request, leaseTime, unit)) {
                counts.put(lockName, 1);
                return true;
            }
        }
        return false;
    }
    

    ps: redisLock#tryLock 為上一篇文章實現的分佈鎖。

    由於公號外鏈無法直接跳轉,關注『程序通事』,回復分佈式鎖獲取源代碼。

    加鎖方法首先判斷當前線程是否已經已經擁有該鎖,若已經擁有,直接對鎖的重入次數加 1。

    若還沒擁有該鎖,則嘗試去 Redis 加鎖,加鎖成功之後,再對重入次數加 1 。

    釋放鎖的代碼如下:

    /**
     * 解鎖需要判斷不同線程池
     *
     * @param lockName
     * @param request
     */
    public void unlock(String lockName, String request) {
        Map<String, Integer> counts = LOCKS.get();
        if (counts.getOrDefault(lockName, 0) <= 1) {
            counts.remove(lockName);
            Boolean result = redisLock.unlock(lockName, request);
            if (!result) {
                throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "
                        + request);
            }
    
        } else {
            counts.put(lockName, counts.get(lockName) - 1);
        }
    }
    

    釋放鎖的時首先判斷重入次數,若大於 1,則代表該鎖是被該線程擁有,所以直接將鎖重入次數減 1 即可。

    若當前可重入次數小於等於 1,首先移除 Map中鎖對應的 key,然後再到 Redis 釋放鎖。

    這裏需要注意的是,當鎖未被該線程擁有,直接解鎖,可重入次數也是小於等於 1 ,這次可能無法直接解鎖成功。

    ThreadLocal 使用過程要記得及時清理內部存儲實例變量,防止發生內存泄漏,上下文數據串用等問題。

    下次咱來聊聊最近使用 ThreadLocal 寫的 Bug。

    相關問題

    使用 ThreadLocal 這種本地記錄重入次數,雖然真的簡單高效,但是也存在一些問題。

    過期時間問題

    上述加鎖的代碼可以看到,重入加鎖時,僅僅對本地計數加 1 而已。這樣可能就會導致一種情況,由於業務執行過長,Redis 已經過期釋放鎖。

    而再次重入加鎖時,由於本地還存在數據,認為鎖還在被持有,這就不符合實際情況。

    如果要在本地增加過期時間,還需要考慮本地與 Redis 過期時間一致性的,代碼就會變得很複雜。

    不同線程/進程可重入問題

    狹義上可重入性應該只是對於同一線程的可重入,但是實際業務可能需要不同的應用線程之間可以重入同把鎖。

    ThreadLocal的方案僅僅只能滿足同一線程重入,無法解決不同線程/進程之間重入問題。

    不同線程/進程重入問題就需要使用下述方案 Redis Hash 方案解決。

    基於 Redis Hash 可重入鎖

    實現方式

    ThreadLocal 的方案中我們使用了 Map 記載鎖的可重入次數,而 Redis 也同樣提供了 Hash (哈希表)這種可以存儲鍵值對數據結構。所以我們可以使用 Redis Hash 存儲的鎖的重入次數,然後利用 lua 腳本判斷邏輯。

    加鎖的 lua 腳本如下:

    ---- 1 代表 true
    ---- 0 代表 false
    
    if (redis.call('exists', KEYS[1]) == 0) then
        redis.call('hincrby', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return 1;
    end ;
    if (redis.call('hexists', KEYS[1], ARGV[2]) == 1) then
        redis.call('hincrby', KEYS[1], ARGV[2], 1);
        redis.call('pexpire', KEYS[1], ARGV[1]);
        return 1;
    end ;
    return 0;
    

    如果 KEYS:[lock],ARGV[1000,uuid]

    不熟悉 lua 語言同學也不要怕,上述邏輯還是比較簡單的。

    加鎖代碼首先使用 Redis exists 命令判斷當前 lock 這個鎖是否存在。

    如果鎖不存在的話,直接使用 hincrby創建一個鍵為 lock hash 表,並且為 Hash 表中鍵為 uuid 初始化為 0,然後再次加 1,最後再設置過期時間。

    如果當前鎖存在,則使用 hexists判斷當前 lock 對應的 hash 表中是否存在 uuid 這個鍵,如果存在,再次使用 hincrby 加 1,最後再次設置過期時間。

    最後如果上述兩個邏輯都不符合,直接返回。

    加鎖代碼如下:

    // 初始化代碼
    
    String lockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:lock.lua").openStream(), Charsets.UTF_8);
    lockScript = new DefaultRedisScript<>(lockLuaScript, Boolean.class);
    
    /**
     * 可重入鎖
     *
     * @param lockName  鎖名字,代表需要爭臨界資源
     * @param request   唯一標識,可以使用 uuid,根據該值判斷是否可以重入
     * @param leaseTime 鎖釋放時間
     * @param unit      鎖釋放時間單位
     * @return
     */
    public Boolean tryLock(String lockName, String request, long leaseTime, TimeUnit unit) {
        long internalLockLeaseTime = unit.toMillis(leaseTime);
        return stringRedisTemplate.execute(lockScript, Lists.newArrayList(lockName), String.valueOf(internalLockLeaseTime), request);
    }
    

    Spring-Boot 2.2.7.RELEASE

    只要搞懂 Lua 腳本加鎖邏輯,Java 代碼實現還是挺簡單的,直接使用 SpringBoot 提供的 StringRedisTemplate 即可。

    解鎖的 Lua 腳本如下:

    -- 判斷 hash set 可重入 key 的值是否等於 0
    -- 如果為 0 代表 該可重入 key 不存在
    if (redis.call('hexists', KEYS[1], ARGV[1]) == 0) then
        return nil;
    end ;
    -- 計算當前可重入次數
    local counter = redis.call('hincrby', KEYS[1], ARGV[1], -1);
    -- 小於等於 0 代表可以解鎖
    if (counter > 0) then
        return 0;
    else
        redis.call('del', KEYS[1]);
        return 1;
    end ;
    return nil;
    

    首先使用 hexists 判斷 Redis Hash 表是否存給定的域。

    如果 lock 對應 Hash 表不存在,或者 Hash 表不存在 uuid 這個 key,直接返回 nil

    若存在的情況下,代表當前鎖被其持有,首先使用 hincrby使可重入次數減 1 ,然後判斷計算之後可重入次數,若小於等於 0,則使用 del 刪除這把鎖。

    解鎖的 Java 代碼如下:

    // 初始化代碼:
    
    
    String unlockLuaScript = IOUtils.toString(ResourceUtils.getURL("classpath:unlock.lua").openStream(), Charsets.UTF_8);
    unlockScript = new DefaultRedisScript<>(unlockLuaScript, Long.class);
    
    /**
     * 解鎖
     * 若可重入 key 次數大於 1,將可重入 key 次數減 1 <br>
     * 解鎖 lua 腳本返回含義:<br>
     * 1:代表解鎖成功 <br>
     * 0:代表鎖未釋放,可重入次數減 1 <br>
     * nil:代表其他線程嘗試解鎖 <br>
     * <p>
     * 如果使用 DefaultRedisScript<Boolean>,由於 Spring-data-redis eval 類型轉化,<br>
     * 當 Redis 返回  Nil bulk, 默認將會轉化為 false,將會影響解鎖語義,所以下述使用:<br>
     * DefaultRedisScript<Long>
     * <p>
     * 具體轉化代碼請查看:<br>
     * JedisScriptReturnConverter<br>
     *
     * @param lockName 鎖名稱
     * @param request  唯一標識,可以使用 uuid
     * @throws IllegalMonitorStateException 解鎖之前,請先加鎖。若為加鎖,解鎖將會拋出該錯誤
     */
    public void unlock(String lockName, String request) {
        Long result = stringRedisTemplate.execute(unlockScript, Lists.newArrayList(lockName), request);
        // 如果未返回值,代表其他線程嘗試解鎖
        if (result == null) {
            throw new IllegalMonitorStateException("attempt to unlock lock, not locked by lockName:+" + lockName + " with request: "
                    + request);
        }
    }
    

    解鎖代碼執行方式與加鎖類似,只不過解鎖的執行結果返回類型使用 Long。這裏之所以沒有跟加鎖一樣使用 Boolean ,這是因為解鎖 lua 腳本中,三個返回值含義如下:

    • 1 代表解鎖成功,鎖被釋放
    • 0 代表可重入次數被減 1
    • null 代表其他線程嘗試解鎖,解鎖失敗

    如果返回值使用 BooleanSpring-data-redis 進行類型轉換時將會把 null 轉為 false,這就會影響我們邏輯判斷,所以返回類型只好使用 Long

    以下代碼來自 JedisScriptReturnConverter

    相關問題

    spring-data-redis 低版本問題

    如果 Spring-Boot 使用 Jedis 作為連接客戶端,並且使用Redis Cluster 集群模式,需要使用 2.1.9 以上版本的spring-boot-starter-data-redis,不然執行過程中將會拋出:

    org.springframework.dao.InvalidDataAccessApiUsageException: EvalSha is not supported in cluster environment.
    

    如果當前應用無法升級 spring-data-redis也沒關係,可以使用如下方式,直接使用原生 Jedis 連接執行 lua 腳本。

    以加鎖代碼為例:

    public boolean tryLock(String lockName, String reentrantKey, long leaseTime, TimeUnit unit) {
        long internalLockLeaseTime = unit.toMillis(leaseTime);
        Boolean result = stringRedisTemplate.execute((RedisCallback<Boolean>) connection -> {
            Object innerResult = eval(connection.getNativeConnection(), lockScript, Lists.newArrayList(lockName), Lists.newArrayList(String.valueOf(internalLockLeaseTime), reentrantKey));
            return convert(innerResult);
        });
        return result;
    }
    
    private Object eval(Object nativeConnection, RedisScript redisScript, final List<String> keys, final List<String> args) {
    
        Object innerResult = null;
        // 集群模式和單點模式雖然執行腳本的方法一樣,但是沒有共同的接口,所以只能分開執行
        // 集群
        if (nativeConnection instanceof JedisCluster) {
            innerResult = evalByCluster((JedisCluster) nativeConnection, redisScript, keys, args);
        }
        // 單點
        else if (nativeConnection instanceof Jedis) {
            innerResult = evalBySingle((Jedis) nativeConnection, redisScript, keys, args);
        }
        return innerResult;
    }
    

    數據類型轉化問題

    如果使用 Jedis 原生連接執行 Lua 腳本,那麼可能又會碰到數據類型的轉換坑。

    可以看到 Jedis#eval返回 Object,我們需要具體根據 Lua 腳本的返回值的,再進行相關轉化。這其中就涉及到 Lua 數據類型轉化為 Redis 數據類型。

    下面主要我們來講下 Lua 數據轉化 Redis 的規則中幾條比較容易踩坑:

    1、Lua number 與 Redis 數據類型轉換

    Lua 中 number 類型是一個雙精度的浮點數,但是 Redis 只支持整數類型,所以這個轉化過程將會丟棄小數位。

    2、Lua boolean 與 Redis 類型轉換

    這個轉化比較容易踩坑,Redis 中是不存在 boolean 類型,所以當Lua 中 true 將會轉為 Redis 整數 1。而 Lua 中 false 並不是轉化整數,而是轉化 null 返回給客戶端。

    3、Lua nil 與 Redis 類型轉換

    Lua nil 可以當做是一個空值,可以等同於 Java 中的 null。在 Lua 中如果 nil 出現在條件表達式,將會當做 false 處理。

    所以 Lua nil 也將會 null 返回給客戶端。

    其他轉化規則比較簡單,詳情參考:

    http://doc.redisfans.com/script/eval.html

    總結

    可重入分佈式鎖關鍵在於對於鎖重入的計數,這篇文章主要給出兩種解決方案,一種基於 ThreadLocal 實現方案,這種方案實現簡單,運行也比較高效。但是若要處理鎖過期的問題,代碼實現就比較複雜。

    另外一種採用 Redis Hash 數據結構實現方案,解決了 ThreadLocal 的缺陷,但是代碼實現難度稍大,需要熟悉 Lua 腳本,以及Redis 一些命令。另外使用 spring-data-redis 等操作 Redis 時不經意間就會遇到各種問題。

    幫助

    https://www.sofastack.tech/blog/sofa-jraft-rheakv-distributedlock/

    https://tech.meituan.com/2016/09/29/distributed-system-mutually-exclusive-idempotence-cerberus-gtis.html

    最後說兩句(求關注)

    看完文章,哥哥姐姐們點個吧,周更真的超累,不知覺又寫了两天,拒絕白嫖,來點正反饋唄~。

    最後感謝各位的閱讀,才疏學淺,難免存在紕漏,如果你發現錯誤的地方,可以留言指出。如果看完文章還有其他不懂的地方,歡迎加我,互相學習,一起成長~

    最後謝謝大家支持~

    最最後,重要的事再說一篇~

    快來關注我呀~
    快來關注我呀~
    快來關注我呀~

    歡迎關注我的公眾號:程序通事,獲得日常乾貨推送。如果您對我的專題內容感興趣,也可以關注我的博客:studyidea.cn

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

    【其他文章推薦】

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

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

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

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

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

  • 面試官:線程池如何按照core、max、queue的執行循序去執行?(內附詳細解析)

    面試官:線程池如何按照core、max、queue的執行循序去執行?(內附詳細解析)

    前言

    這是一個真實的面試題。

    前幾天一個朋友在群里分享了他剛剛面試候選者時問的問題:“線程池如何按照core、max、queue的執行循序去執行?”

    我們都知道線程池中代碼執行順序是:corePool->workQueue->maxPool,源碼我都看過,你現在問題讓我改源碼??

    一時間群里炸開了鍋,小夥伴們紛紛打聽他所在的公司,然後拉黑避坑。(手動狗頭,大家一起調侃٩(๑ᴗ๑)۶)

    關於線程池他一共問了這麼幾個問題:

    • 線程池如何按照core、max、queue的順序去執行?
    • 子線程拋出的異常,主線程能感知到么?
    • 線程池發生了異常改怎樣處理?

    全是一些有意思的問題,我之前也寫過一篇很詳細的圖文教程:【萬字圖文-原創】 | 學會Java中的線程池,這一篇也許就夠了! ,不了解的小夥伴可以再回顧下~

    但是針對這幾個問題,可能大家一時間也有點懵。今天的文章我們以源碼為基礎來分析下該如何回答這三個問題。(之前沒閱讀過源碼也沒關係,所有的分析都會貼出源碼及圖解)

    線程池如何按照core、max、queue的順序執行?

    問題思考

    對於這個問題,很多小夥伴肯定會疑惑:“別人源碼中寫好的執行流程你為啥要改?這面試官腦子有病吧……”

    這裏來思考一下現實工作場景中是否有這種需求?之前也看到過一份簡歷也寫到過這個問題:

    一個線程池執行的任務屬於IO密集型,CPU大多屬於閑置狀態,系統資源未充分利用。如果一瞬間來了大量請求,如果線程池數量大於coreSize時,多餘的請求都會放入到等待隊列中。等待着corePool中的線程執行完成后再來執行等待隊列中的任務。

    試想一下,這種場景我們該如何優化?

    我們可以修改線程池的執行順序為corePool->maxPool->workQueue。 這樣就能夠充分利用CPU資源,提交的任務會被優先執行。當線程池中線程數量大於maxSize時才會將任務放入等待隊列中。

    你就說巧不巧?面試官的這個問題顯然是經過認真思考來提問的,這是一個很有意思的溫恩提,下面就一起看看如何解決吧。

    線程池運行流程

    我們都知道線程池執行流程是先corePoolworkQueue,最後才是maxPool的一個執行流程。

    線程池核心參數

    在回顧下ThreadPoolExecutor.execute()源碼前我們先回顧下線程池中的幾個重要參數:

    我們來看下這幾個參數的定義:
    corePoolSize: 線程池中核心線程數量
    maximumPoolSize: 線程池中最大線程數量
    keepAliveTime: 非核心的空閑線程等待新任務的時間
    unit: 時間單位。配合allowCoreThreadTimeOut也會清理核心線程池中的線程。
    workQueue: 基於Blocking的任務隊列,最好選用有界隊列,指定隊列長度
    threadFactory: 線程工廠,最好自定義線程工廠,可以自定義每個線程的名稱
    handler: 拒絕策略,默認是AbortPolicy

    ThreadPoolExecutor.execute()源碼分析

    我們可以看下execute()如下:

    接着來分析下執行過程:

    1. 第一步:workerCountOf(c)時間計算當前線程池中線程的個數,當線程個數小於核心線程數
    2. 第二步:線程池線程數量大於核心線程數,此時提交的任務會放入workQueue中,使用offer()進行操作
    3. 第三步:workQueue.offer()執行失敗,新提交的任務會直接執行,addWorker()會判斷如果當前線程池數量大於最大線程數,則執行拒絕策略

    好了,到了這裏我們都已經很清楚了,關鍵在於第二步和第三步如何交換順序執行呢?

    解決思路

    仔細想一想,如果修改workQueue.offer()的實現不就可以達到目的了?我們先來畫圖來看一下:

    現在的問題就在於,如果當前線程池中coreSize < workCount < maxSize時,一定會先執行offer()操作。

    我們如果修改offer的實現是否可以完成執行順序的更換呢?這裏也是畫圖來展示一下:

    Dubbo中EagerThreadPool解決方案

    湊巧Dubbo中也有類似的實現,在DubboEagerThreadPool自定義了一個BlockingQueue,在offer()方法中,如果當前線程池數量小於最大線程池時,直接返回false,這裏就達到了調節線程池執行順序的目的。

    源碼直達:https://github.com/apache/dubbo/blob/master/dubbo-common/src/main/java/org/apache/dubbo/common/threadpool/support/eager/TaskQueue.java

    看到這裏一切都真相大白了,解決思路以及方案都很簡單,學會了沒有?

    這個問題背後還隱藏了一些場景的優化、源碼的擴展等等知識,果然是一個值得思考的好問題。

    子線程拋出的異常,主線程能感知到么?

    問題思考

    這個問題其實也很容易回答,也僅僅是一個面試題而已,實際工作中子線程的異常不應該由主線程來捕獲。

    針對這個問題,希望大家清楚的是: 我們要明確線程代碼的邊界,異步化過程中,子線程拋出的異常應該由子線程自己去處理,而不是需要主線程感知來協助處理。

    解決方案

    解決方案很簡單,在虛擬機中,當一個線程如果沒有顯式處理異常而拋出時會將該異常事件報告給該線程對象的 java.lang.Thread.UncaughtExceptionHandler 進行處理,如果線程沒有設置 UncaughtExceptionHandler,則默認會把異常棧信息輸出到終端而使程序直接崩潰。

    所以如果我們想在線程意外崩潰時做一些處理就可以通過實現 UncaughtExceptionHandler 來滿足需求。

    我們使用線程池設置ThreadFactory時可以指定UncaughtExceptionHandler,這樣就可以捕獲到子線程拋出的異常了。

    代碼示例

    具體代碼如下:

    /**
     * 測試子線程異常問題
     *
     * @author wangmeng
     * @date 2020/6/13 18:08
     */
    public class ThreadPoolExceptionTest {
    
        public static void main(String[] args) throws InterruptedException {
            MyHandler myHandler = new MyHandler();
            ExecutorService execute = new ThreadPoolExecutor(10, 10,
                    0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(myHandler).build());
    
            TimeUnit.SECONDS.sleep(5);
            for (int i = 0; i < 10; i++) {
                execute.execute(new MyRunner());
            }
        }
    
    
        private static class MyRunner implements Runnable {
            @Override
            public void run() {
                int count = 0;
                while (true) {
                    count++;
                    System.out.println("我要開始生產Bug了============");
                    if (count == 10) {
                        System.out.println(1 / 0);
                    }
    
                    if (count == 20) {
                        System.out.println("這裡是不會執行到的==========");
                        break;
                    }
                }
            }
        }
    }
    
    class MyHandler implements Thread.UncaughtExceptionHandler {
        private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
        }
    }
    

    執行結果:

    UncaughtExceptionHandler 解析

    我們來看下Thread中的內部接口UncaughtExceptionHandler

    public class Thread {
        ......
        /**
         * 當一個線程因未捕獲的異常而即將終止時虛擬機將使用 Thread.getUncaughtExceptionHandler()
         * 獲取已經設置的 UncaughtExceptionHandler 實例,並通過調用其 uncaughtException(...) 方
         * 法而傳遞相關異常信息。
         * 如果一個線程沒有明確設置其 UncaughtExceptionHandler,則將其 ThreadGroup 對象作為其
         * handler,如果 ThreadGroup 對象對異常沒有什麼特殊的要求,則 ThreadGroup 會將調用轉發給
         * 默認的未捕獲異常處理器(即 Thread 類中定義的靜態未捕獲異常處理器對象)。
         *
         * @see #setDefaultUncaughtExceptionHandler
         * @see #setUncaughtExceptionHandler
         * @see ThreadGroup#uncaughtException
         */
        @FunctionalInterface
        public interface UncaughtExceptionHandler {
            /**
             * 未捕獲異常崩潰時回調此方法
             */
            void uncaughtException(Thread t, Throwable e);
        }
    
        /**
         * 靜態方法,用於設置一個默認的全局異常處理器。
         */
        public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
             defaultUncaughtExceptionHandler = eh;
         }
    
        /**
         * 針對某個 Thread 對象的方法,用於對特定的線程進行未捕獲的異常處理。
         */
        public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
            checkAccess();
            uncaughtExceptionHandler = eh;
        }
    
        /**
         * 當 Thread 崩潰時會調用該方法獲取當前線程的 handler,獲取不到就會調用 group(handler 類型)。
         * group 是 Thread 類的 ThreadGroup 類型屬性,在 Thread 構造中實例化。
         */
        public UncaughtExceptionHandler getUncaughtExceptionHandler() {
            return uncaughtExceptionHandler != null ?
                uncaughtExceptionHandler : group;
        }
    
        /**
         * 線程全局默認 handler。
         */
        public static UncaughtExceptionHandler getDefaultUncaughtExceptionHandler() {
            return defaultUncaughtExceptionHandler;
        }
        ......
    }
    

    部分內容參考自:https://mp.weixin.qq.com/s/ghnNQnpou6-NemhFjpl4Jg

    線程池發生了異常改怎樣處理?

    線程池中線程運行過程中出現了異常該怎樣處理呢?線程池提交任務有兩種方式,分別是execute()submit(),這裡會依次說明。

    ThreadPoolExecutor.runWorker()實現

    不管是使用execute()還是submit()提交任務,最終都會執行到ThreadPoolExecutor.runWorker(),我們來看下源碼(源碼基於JDK1.8):

    我們看到在執行task.run()時,出現異常會直接向上拋出,這裏處理的最好的方式就是在我們業務代碼中使用try...catch()來捕獲異常。

    FutureTask.run()實現

    如果我們使用submit()來提交任務,在ThreadPoolExecutor.runWorker()方法執行時最終會調用到FutureTask.run()方法裏面去,不清楚的小夥伴也可以看下我之前的文章:

    線程池續:你必須要知道的線程池submit()實現原理之FutureTask!

    這裏可以看到,如果業務代碼拋出異常后,會被catch捕獲到,然後調用setExeception()方法:

    可以看到其實類似於直接吞掉了,當我們調用get()方法的時候異常信息會包裝到FutureTask內部的變量outcome中,我們也會獲取到對應的異常信息。

    ThreadPoolExecutor.runWorker()最後finally中有一個afterExecute()鈎子方法,如果我們重寫了afterExecute()方法,就可以獲取到子線程拋出的具體異常信息Throwable了。

    結論

    對於線程池、包括線程的異常處理推薦以下方式:

    1. 直接使用try/catch,這個也是最推薦的方式
    2. 在我們構造線程池的時候,重寫uncaughtException()方法,上面示例代碼也有提到:
    public class ThreadPoolExceptionTest {
    
        public static void main(String[] args) throws InterruptedException {
            MyHandler myHandler = new MyHandler();
            ExecutorService execute = new ThreadPoolExecutor(10, 10,
                    0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), new ThreadFactoryBuilder().setUncaughtExceptionHandler(myHandler).build());
    
            TimeUnit.SECONDS.sleep(5);
            for (int i = 0; i < 10; i++) {
                execute.execute(new MyRunner());
            }
        }
    }
    
    class MyHandler implements Thread.UncaughtExceptionHandler {
        private final static Logger LOGGER = LoggerFactory.getLogger(MyHandler.class);
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            LOGGER.error("threadId = {}, threadName = {}, ex = {}", t.getId(), t.getName(), e.getMessage());
        }
    }
    

    3 直接重寫afterExecute()方法,感知異常細節

    總結

    這篇文章到這裏就結束了,不知道小夥伴們有沒有一些感悟或收穫?

    通過這幾個面試問題,我也深刻的感受到學習知識要多思考,看源碼的過程中要多設置一些場景,這樣才會收穫更多。

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

  • [.NET 開源] 高性能的 Swifter.MessagePack 已發布,併發布新版本的 Swifter.Json 和 Swifter.Data。

    [.NET 開源] 高性能的 Swifter.MessagePack 已發布,併發布新版本的 Swifter.Json 和 Swifter.Data。

    抱歉各位朋友,由於各種私事公事,本應該在 19 年底發布的 Swifter.MessagePack 庫延遲了這麼久才發布,我深感抱歉。

    MsgPack 簡介

    MsgPack 一種非常輕巧的二進制數據交換格式,巧妙的設計讓它相比其他二進制數據格式更可讀,並且有着不錯的壓縮率和邏輯性能,是目前相當火熱的數據交換格式。

    Swifter.MessagePack 遵循 MsgPack 新的規範實現;相比 .NET 其他 MsgPack 序列化庫,Swifter.MessagePack 有着更好的性能,生成的內容更緊湊合理且更簡單易用。

    Nuget:Swifter.MessagePackSwifter.JsonSwifter.Data

    GitHub:Swifter.MessagePackSwifter.Json

    如果您想使用 Swifter 庫,請在 Nuget 上安裝/下載最新版本,如需單文件版本,請自行生成/合併。

     

    簡單使用 Swifter.MessagePack

    MessagePackFormatter 類內部還有十個方法重載,包括靜態和實例方法,總有一些適合您;這些方法都是線程安全的。

    更多使用方法請參考早期關於 Swifter.Json 的文章,GitHub 或 Wiki;學習交流進 Swifter 的 QQ 群:133630914(新群,歡迎加入)。

     

    Swifter 框架的特性

    (1) Swifter 可以運行在 .NET Framework 2.0+, .NET Core 2.0+, .NET Standard 2.0+, MONO JIT, MONO AOT, Xamarin.Android, Xamarin.iOS, Unity JIT 等平台/運行時上,Unity IL2CPP 運行時由於沒有我們測試環境,不知可否正常運行,更多信息請看下面的 AOT 說明

    (2) Swifter 有着深層的抽象封裝,這雖然帶來了一些性能和內存的損耗,但也獲得了更高的擴展性;Swifter.Json/Swifter.MessagePack/Swifter.Data 的可公用的代碼非常多,這使得在 Swifter 上實現一個新的序列化庫只需要編寫少量代碼即可實現,這是其他框架難實現的。

    (3) 雖然 Swifter 有很多接口和抽象編程,但是 Swifter 並沒有因此比其他的框架慢或內存佔用大,反比它們更快和更小內存佔用;這是因為 Swifter 從來都是使用更好算法和邏輯來獲取性能,而不是使用更直接的代碼獲取直接的性能。

    (4) 作為類庫開發者,我們深知每個人開發和測試的側重點都與他人不一樣,自己找出自己的問題太難,所以 Swifter.Json 和 Swifter.MessagePack 除了我們自己的測試單元之外, 還 “偷” 了 Newtonsoft, Neuecc 和 Spanjson 的 5000+ 個測試單元( 去除了 Newtonsoft 的部分測試單元);現已測試通過 4200+ 個,不通過 800+ 個是我們認為可以允許或是更加合理的行為。(不勞而獲的測試單元確實用着很爽,但事實是我們”搬”這些測試單元用了 3 天,無腦替換改到手指抽筋)

     

    Swifter.Json 和 Swifter.MessagePack

    (1) Swifter.MessagePack 和 Swifter.Json 一樣,都有着非常優異的性能和極小的額外內存分配。

    (2) Swifter.MessagePack 和 Swifter.Json 的 API 大致相同,如果使用者同時使用它們,那麼可以極小成本在它們之間切換。

    (3) 得益於 Swifter.Core 的強大數據映射,Swifter.MessagePack 和 Swifter.Json 都同時支持 .NET 上大多數常用的數據結構和類型。

    (4) Swifter.MessagePack 和 Swifter.Json 對重複引用的對象的表示方式不一樣,在開啟 MultiReferencingReference 配置項后,Swifter.Json 將使用 { “$ref”: “#/obj/1/target” } 來表示重複引用的對象,而 Swifter.MessagePack 使用對象在 MsgPack 內容的偏移量表示重複引用的對象;相比之下 Swifter.MessagePack 的方案更簡單性能更快,但是可讀性較差,不過說來 MsgPack 本來就是要專門的工具才能閱讀。

    (5) Swifter.MessagePack 在序列化基礎類型時,在保證精度不丟失的前提下,將大數據類型轉換為更小數據類型,以得到更緊湊的 MsgPack 內容(如將 double 123 轉換為 int 123,int 123 只需要 1 個字節即可表示,如果不做轉換則需要 9 個字節表示)。

    (6) Swifter.MessagePack 在序列化未知長度的集合時(如 Enumerable<T>),會將長度定義為四字節 (FixArray32),然後在寫入完成后把實際長度賦予這四字節長度;這樣雖然在較短的未知長度集合時,將產生 1-3 個 0;但是這避免了將未知長度的集合轉換為 List<T> 或 T[], 這提高了性能也減少了內存分配,這是不虧的(因為未知長度的集合很常用,如 Linq,DbDataReader 等)。

     

    新版本做了啥?

    (1) 主要是解決了已知 BUG,包括了 Issues 上提到的幾個。

    (2) 允許將 “” 值解析為 DateTime, int?, double 等基礎類型的默認值,但是需要啟用 EmptyStringAsDefault 配置項,默認未開啟。

    (3) 解決了 Swifter.Json 浮點數: float, double 失真的問題,並增加了 UseSystemFloatingPointsMethods 配置項使用系統的浮點數方法,此配置項的更多說明請看該配置項的註釋。

    (4) 增加了序列化的事件:ObjectFiltering 和 ArrayFiltering,這兩個事件可以對正在序列化中的 鍵/值 做處理和篩選,包括駝峰命名法,忽略一些值等。它們被放在 JsonFormatter 和 MessagePackFormatter 的實例裏面。

    (5) 增加了 .NET 對象的持久序列化和反序列化功能,這個功能將對象序列化為包含類型信息和字段值的內容,不包含邏輯信息;使用 SerializationBox<T> 盒子使用此功能。圖示:

    更多新增的功能請繼續看以下內容。

     

     

    AOT

    在 Swifter 新版本里,AOT 的 JIT 的界限更加明顯,由 VersionDifferences.IsSupportEmit 字段標識;當這個字段為 true 表示當前平台是 JIT 運行時,Swifter 將在一些類中使用 Emit 技術提高性能;當此字段為 false 時,Swifter 會完全不使用 Emit 技術。

    因我們設備有限,無法提供大規模的平台測試,但我們非常希望可以 Swifter 可以支持更多的平台,所以希望朋友們加 Swifter 交流 QQ 群:(133630914),在這裏我們可以更快的提供反饋。

     

    直接文檔讀取/寫入的 API

    通常情況下,將小型對象序列化為 Json/MsgPack 和將小型 Json/MsgPack 反序列化為對象是 .NET 程序中常見的操作,Swifter 也正以此為常用場景做優化,所以 Swifter 在對小型數據操作時性能最佳,且相比其他 Json/MsgPack 解析庫優勢明顯。

    但在大型數據下優勢減少,這主要原因是大型數據的存儲需要實體類或字典/集合存儲,創建/填充/遍歷這些對象消耗了大量資源(接口編程的損耗);所以 Swifter 提供了直接讀取/寫入的 API 來繞開了對存儲介質的操作,以更快更小損耗的讀寫大型數據。

    使用 JsonFormatter.CreateJsonReader/MessagePackFormatter.CreateMessagePackReader 函數來創建文檔讀取器,使用 JsonFormatter.CreateJsonWriter/MessagePackFormatter.CreateMessagePackWriter 函數來創建文檔寫入器。

    使用文檔讀取器完整的讀取一個 Json/MsgPack 文檔將比反序列化為對象快 4-8 倍!使用文檔寫入器生成文檔的性能與將實體類序列化為 Json/MsgPack 相差較小,前提是您已構建好了這些對象。

    讀取器演示:

    寫入器演示:

     

    擁有簡單預測數組的長度的能力

    Swifter 在對小型數組,部分集合寫入時,會根據數組的類型,來源(Data,Json,MsgPack 等),名稱等信息並結合之前的一些長度記錄,簡單的預測出新的數組的長度;在寫入完成后,如果預測長度與實際長度不符,則擴展或壓縮為實際長度;如果與實際長度相符,則不需要重新創建新數組。此能力有效提高反序列化小型數組和部分集合性能,並且減少額外內存分配。

    在其他高性能的 Json 解析庫,它們使用 ArrayPool<T> 同樣可以提高性能和減少內存分配;但是由於 Swifter 對兼容性的要求,使得我們不能使用 ArrayPool<T> 方案;在數組的長度比較穩定的情況下,我們的方案更好;但在數組長度非常不穩定的情況下,我們的方案可能仍需要 1-3 次的擴容/壓縮。

     

    假定有序的對象反序列化

    Swifter.Json 和 Swifter.MessagePack 都支持了假定有序的對象反序列化,當一個 Json/MsgPack 的對象與當前的實體類對象的字段順序一致時,將有效提升反序列化性能。

    此操作默認不開啟,可以使用 AsOrderedObjectDeserialize 配置項開啟。

     

    高性能的反射封裝

    Swifter.Core 里提供了一些對反射封裝的類,它們放在 Swifter.Reflection 命名空間下;這些類型主要功能就是提高了系統反射的性能;XObjectRW 正是使用它們實現不依賴 Emit 的高性能對象讀寫器。

    雖然放棄一些安全性檢查可以提高更多的性能,但是我們並沒有這麼做;我們仍然有類型安全檢查和防溢出檢查(事實上讀寫字段和屬性大多數的損害都在這裏,如果去掉這些檢查將得到上百倍的性能;事實上這些檢查只起到了提示程序員不能這麼做的作用,程序實際運行時這些檢查無意義)。

     

    高效的数字 ToString 和 Parse 方法

    Swifter.Core 提供了一些高性能数字算法,包括 Int64, UInt64, Double, Single, Decimal 的 Parse 和 ToString 算法,它們被放在 Swifter.Tools.NumberHelper 里,這些算法被應用與 Swifter.Json 和一些其他地方,這些算法支持 2-64 進制。

     

    XConvert 萬能類型轉換器

    Swifter.Tools.XConvert.Convert<TSource, TDestination> 是一個功能強大的萬能類型轉換函數,它在初始化時嘗試以下方式獲取合適的轉換函數:

    (1) 包含在 System.Convert 里的基礎轉換函數;

    (2) 類型兼容的隱式轉換(如:從子類轉換為父類,從 Int32 轉換為 Int64,從 Int64 轉換為 Double)。

    (3) 原類型和目標類型中的 static implicit operator (隱式轉換) 函數。

    (4) 原類型中的 ToXXX 實例函數。

    (5) 目標類型中的 Parse 和 ValueOf 靜態函數。

    (7) 目標類型的構造函數。

    (8) 原類型和目標類型中的 static explicit operator (顯式轉換) 函數。

    (9) 當以上方法都沒有找到合適函數時,將使用 (TDestination)(object)value 進行強制轉換。

    簡單示例:

     

    性能測試

    ServiceStack.Json, Jil, LitJson, NetJson 等庫因為出錯太多未展示出來;如果有需要,您可以到 GitHub 上自行克隆/修改/運行,已收錄了 .NET 的大多數 Json 序列化庫。

     

    更多實用功能等你發現…

    Swifter.Core 還提供了許許多多的工具類,包括反射,委託,類型轉換,字符串,加密,哈希,数字,日期,數組和集合等工具,它們被放在 Swifter.Tools 命名空間下,您可以使用它們來提高開發效率和運行效率。

    Swifter.RW 命名空間是整個 Swifter 框架的核心,它主要邏輯是:從讀取器中讀取值,寫入到寫入器中;如:從 JsonReader 讀取值到 ObjectWriter 或 DictionaryWriter 中;熟悉它們就等於精通了 Swifter 框架。

    Swifter.Json/Swifter.MessagePack 有一個非常重要的配置項 JsonFormatterOptions/MessagePackFormatterOptions;使用前建議先閱讀它們,以配置更適合您系統的序列化和反序列化方案。

     

    最後附上 Swifter.Data 的簡介

    Swifter.Data 是一個小型的 ORM 工具,它相比 Dapper 性能要快一些,功能要強大一些。

     

    感謝閱讀

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

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

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

  • foreach 集合又拋經典異常了,這次一定要刨根問底

    foreach 集合又拋經典異常了,這次一定要刨根問底

    一:背景

    1. 講故事

    最近同事在寫一段業務邏輯的時候,程序跑起來總是報:集合已修改;可能無法執行枚舉操作,硬是沒有找到什麼情況下會導致這個異常產生,就讓我來找一下bug,其實這個異常在座的每個程序員幾乎都遇到過,誰也不是一生下就是大牛,簡單看了下代碼,確實是多線程操作foreach,但並沒有對foreach進行Add,Remove操作,掃完代碼其實我也是有點懵,沒撤只能調試了,在foreach里套一層trycatch,查看異常的線程堆棧從而找出了問題代碼,代碼簡化如下:

    
            static void Main(string[] args)
            {
                var dict = new Dictionary<int, int>()
                {
                    [1001] = 1,
                    [1002] = 10,
                    [1003] = 20
                };
    
                foreach (var userid in dict.Keys)
                {
                    dict[userid] = dict[userid] + 1;
                }
            }
    

    先尋找點安慰,說實話,憑肉眼你覺得這段代碼會拋出異常嗎? 反正我是被騙過了,大寫的尷尬,結論如下,運行一下便知。

    從圖中看確實是異常,說明在foreach的過程中連迭代集合的 value 都不可以修改,這讓我激起了強烈的探索欲,看看FCL中到底是怎麼限制的。

    二:源碼探索

    1. 從IL中尋找答案

    C#已發展到 9.0 了,到處都充斥着語法糖,有時候不看一下底層的IL都不知道到底是轉化成了什麼,所以這個是必須的。

    
    	IL_000d: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
    	IL_001b: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
    	IL_0029: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
    	IL_0037: callvirt instance valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<!0, !1> class [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection<int32, int32>::GetEnumerator()
    
    	.try
    	{
    		IL_003d: br.s IL_005a
    		// loop start (head: IL_005a)
    			IL_003f: ldloca.s 1
    			IL_0041: call instance !0 valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<int32, int32>::get_Current()
    			IL_004c: callvirt instance !1 class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::get_Item(!0)
    			IL_0053: callvirt instance void class [System.Collections]System.Collections.Generic.Dictionary`2<int32, int32>::set_Item(!0, !1)
    			IL_005a: ldloca.s 1
    			IL_005c: call instance bool valuetype [System.Collections]System.Collections.Generic.Dictionary`2/KeyCollection/Enumerator<int32, int32>::MoveNext()
    			IL_0061: brtrue.s IL_003f
    		// end loop
    
    		IL_0063: leave.s IL_0074
    	} // end .try
    	finally
    	{
    
    	} // end handler    
    
    

    從IL代碼中可以看到,先執行了三次字典的索引器操作,然後調用了 Dictionary.GetEnumerator 來生成字典的迭代類,這思路就非常清晰了,然後我們看一下類索引器都做了些什麼。

    從圖中可以看到,每一次的索引器操作,這裏都執行了version++,所以字典初始化完成之後,這裏的 version=3,沒有問題吧,然後繼續看代碼,尋找 Dictionary.GetEnumerator 方法啟動迭代類。

    上面代碼的 _version = dictionary._version; 一定要看仔細了,在啟動迭代類的時候記錄了當時字典的版本號,也就是_version=3,然後繼續探索moveNext方法幹了什麼,如下圖:

    從圖中可以看到,當每次執行moveNext的過程中,都會判斷一下字典的 version 和 當初初始化迭代類中的version 版本號是否一致,如果不一致就拋出異常,所以這行代碼就是點睛之筆了,當在foreach體中執行了 dict[userid] = dict[userid] + 1; 語句,相當於又執行了一次類索引器操作,這時候字典的version就變成 4 了,而當初初始化迭代類的時候還是3,自然下一次執行 moveNext 就是 3 != 4 拋出異常了。

    如果你非要讓我證明給你看,這裏可以使用dnspy直接調試源碼,在異常那裡下一個斷點再查看兩個version版本號不就知道啦。。。

    2. 面對疾風

    有些朋友可能要說,碼農今天分享的這篇一點水準都沒有,我18年前就知道字典是不能動態修改的,還分析的頭頭是勁。

    但是我有話要說,這個還確實是我的一個盲區,平時在迭代字典的時候value一般都是引用類型,動態修改引用類型的值自然是沒有問題的,這是因為你不管怎麼修改都不會改變 _version 版本號,但質疑我的也不要把話說的太滿,因為這種操作是非常語義化非常大眾的需求,你能保證後面net版本不支持這個嗎??? 如果你說不可能,那恭喜你,被我帶到坑裡面去啦。

    下面我用原封不動的代碼在 .net 5 下跑一次,睜大眼睛好好看哦~~~

    驚訝吧, 居然在 .Net 5 中可以的,接下來用ILSpy去查查底層源碼,.netcore 3.1 和 net5 中分別對 類索引器 都做了啥修改。

    • netcore 3.1

    Path: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\3.1.2\System.Private.CoreLib.dll

    • net5

    Path: C:\Program Files\dotnet\shared\Microsoft.NETCore.App\5.0.0-preview.5.20278.1\System.Private.CoreLib.dll

    對比兩張圖你會發現 .Net5 中並沒有做 _version++ 操作,這就了,如果你再細讀代碼,你還發現 .Net5 對字典進行了較大幅度的優化,哈哈,當初在 .Net5 之前產生的錯誤,在 .Net5 中居然沒有啦!

    四: 總結

    源碼面前,不談隱私,沒事多翻翻源碼,有可能還有意外收穫,比如在 .Net 5下的這點新發現,可能還是全網第一個哦,這要是兩個大牛爭吵,讓小白去相信誰呢,嘿嘿,源碼才是真正的專家~

    如您有更多問題與我互動,掃描下方進來吧~

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • .Net Core微服務入門全紀錄(三)——Consul-服務註冊與發現(下)

    .Net Core微服務入門全紀錄(三)——Consul-服務註冊與發現(下)

    前言

    上一篇【.Net Core微服務入門全紀錄(二)——Consul-服務註冊與發現(上)】已經成功將我們的服務註冊到Consul中,接下來就該客戶端通過Consul去做服務發現了。

    服務發現

    • 同樣Nuget安裝一下Consul:

    • 改造一下業務系統的代碼:

    ServiceHelper.cs:

        public class ServiceHelper : IServiceHelper
        {
            private readonly IConfiguration _configuration;
    
            public ServiceHelper(IConfiguration configuration)
            {
                _configuration = configuration;
            }
    
            public async Task<string> GetOrder()
            {
                //string[] serviceUrls = { "http://localhost:9060", "http://localhost:9061", "http://localhost:9062" };//訂單服務的地址,可以放在配置文件或者數據庫等等...
    
                var consulClient = new ConsulClient(c =>
                {
                    //consul地址
                    c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]);
                });
    
                //consulClient.Catalog.Services().Result.Response;
                //consulClient.Agent.Services().Result.Response;
                var services = consulClient.Health.Service("OrderService", null, true, null).Result.Response;//健康的服務
    
                string[] serviceUrls = services.Select(p => $"http://{p.Service.Address + ":" + p.Service.Port}").ToArray();//訂單服務地址列表
    
                if (!serviceUrls.Any())
                {
                    return await Task.FromResult("【訂單服務】服務列表為空");
                }
    
                //每次隨機訪問一個服務實例
                var Client = new RestClient(serviceUrls[new Random().Next(0, serviceUrls.Length)]);
                var request = new RestRequest("/orders", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
    
            public async Task<string> GetProduct()
            {
                //string[] serviceUrls = { "http://localhost:9050", "http://localhost:9051", "http://localhost:9052" };//產品服務的地址,可以放在配置文件或者數據庫等等...
    
                var consulClient = new ConsulClient(c =>
                {
                    //consul地址
                    c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]);
                });
    
                //consulClient.Catalog.Services().Result.Response;
                //consulClient.Agent.Services().Result.Response;
                var services = consulClient.Health.Service("ProductService", null, true, null).Result.Response;//健康的服務
    
                string[] serviceUrls = services.Select(p => $"http://{p.Service.Address + ":" + p.Service.Port}").ToArray();//產品服務地址列表
    
                if (!serviceUrls.Any())
                {
                    return await Task.FromResult("【產品服務】服務列表為空");
                }
    
                //每次隨機訪問一個服務實例
                var Client = new RestClient(serviceUrls[new Random().Next(0, serviceUrls.Length)]);
                var request = new RestRequest("/products", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
        }
    

    appsettings.json:

    {
      "Logging": {
        "LogLevel": {
          "Default": "Information",
          "Microsoft": "Warning",
          "Microsoft.Hosting.Lifetime": "Information"
        }
      },
      "AllowedHosts": "*",
      "ConsulSetting": {
        "ConsulAddress": "http://localhost:8500"
      }
    }
    

    OK,以上代碼就完成了服務列表的獲取。

    瀏覽器測試一下:

    隨便停止2個服務:

    繼續訪問:

    這時候停止的服務地址就獲取不到了,客戶端依然正常運行。

    這時候解決了服務的發現,新的問題又來了…

    • 客戶端每次要調用服務,都先去Consul獲取一下地址,這不僅浪費資源,還增加了請求的響應時間,這顯然讓人無法接受。

    那麼怎麼保證不要每次請求都去Consul獲取地址,同時又要拿到可用的地址列表呢?
    Consul提供的解決方案:——Blocking Queries (阻塞的請求)。詳情請見官網:https://www.consul.io/api-docs/features/blocking

    Blocking Queries

    這是什麼意思呢,簡單來說就是當客戶端請求Consul獲取地址列表時,需要攜帶一個版本號信息,Consul會比較這個客戶端版本號是否和Consul服務端的版本號一致,如果一致,則Consul會阻塞這個請求,直到Consul中的服務列表發生變化,或者到達阻塞時間上限;如果版本號不一致,則立即返回。這個阻塞時間默認是5分鐘,支持自定義。
    那麼我們另外啟動一個線程去干這件事情,就不會影響每次的用戶請求了。這樣既保證了客戶端服務列表的準確性,又節約了客戶端請求服務列表的次數。

    • 繼續改造代碼:
      IServiceHelper增加一個獲取服務列表的接口方法:
        public interface IServiceHelper
        {
            /// <summary>
            /// 獲取產品數據
            /// </summary>
            /// <returns></returns>
            Task<string> GetProduct();
    
            /// <summary>
            /// 獲取訂單數據
            /// </summary>
            /// <returns></returns>
            Task<string> GetOrder();
    
            /// <summary>
            /// 獲取服務列表
            /// </summary>
            void GetServices();
        }
    

    ServiceHelper實現接口:

        public class ServiceHelper : IServiceHelper
        {
            private readonly IConfiguration _configuration;
            private readonly ConsulClient _consulClient;
            private ConcurrentBag<string> _orderServiceUrls;
            private ConcurrentBag<string> _productServiceUrls;
    
            public ServiceHelper(IConfiguration configuration)
            {
                _configuration = configuration;
                _consulClient = new ConsulClient(c =>
                {
                    //consul地址
                    c.Address = new Uri(_configuration["ConsulSetting:ConsulAddress"]);
                });
            }
    
            public async Task<string> GetOrder()
            {
                if (_productServiceUrls == null)
                    return await Task.FromResult("【訂單服務】正在初始化服務列表...");
    
                //每次隨機訪問一個服務實例
                var Client = new RestClient(_orderServiceUrls.ElementAt(new Random().Next(0, _orderServiceUrls.Count())));
                var request = new RestRequest("/orders", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
    
            public async Task<string> GetProduct()
            {
                if(_productServiceUrls == null)
                    return await Task.FromResult("【產品服務】正在初始化服務列表...");
    
                //每次隨機訪問一個服務實例
                var Client = new RestClient(_productServiceUrls.ElementAt(new Random().Next(0, _productServiceUrls.Count())));
                var request = new RestRequest("/products", Method.GET);
    
                var response = await Client.ExecuteAsync(request);
                return response.Content;
            }
    
            public void GetServices()
            {
                var serviceNames = new string[] { "OrderService", "ProductService" };
                Array.ForEach(serviceNames, p =>
                {
                    Task.Run(() =>
                    {
                        //WaitTime默認為5分鐘
                        var queryOptions = new QueryOptions { WaitTime = TimeSpan.FromMinutes(10) };
                        while (true)
                        {
                            GetServices(queryOptions, p);
                        }
                    });
                });
            }
            private void GetServices(QueryOptions queryOptions, string serviceName)
            {
                var res = _consulClient.Health.Service(serviceName, null, true, queryOptions).Result;
                
                //控制台打印一下獲取服務列表的響應時間等信息
                Console.WriteLine($"{DateTime.Now}獲取{serviceName}:queryOptions.WaitIndex:{queryOptions.WaitIndex}  LastIndex:{res.LastIndex}");
    
                //版本號不一致 說明服務列表發生了變化
                if (queryOptions.WaitIndex != res.LastIndex)
                {
                    queryOptions.WaitIndex = res.LastIndex;
    
                    //服務地址列表
                    var serviceUrls = res.Response.Select(p => $"http://{p.Service.Address + ":" + p.Service.Port}").ToArray();
    
                    if (serviceName == "OrderService")
                        _orderServiceUrls = new ConcurrentBag<string>(serviceUrls);
                    else if (serviceName == "ProductService")
                        _productServiceUrls = new ConcurrentBag<string>(serviceUrls);
                }
            }
        }
    

    Startup的Configure方法中調用一下獲取服務列表:

            public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceHelper serviceHelper)
            {
                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }
                else
                {
                    app.UseExceptionHandler("/Home/Error");
                }
                app.UseStaticFiles();
    
                app.UseRouting();
    
                app.UseAuthorization();
    
                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllerRoute(
                        name: "default",
                        pattern: "{controller=Home}/{action=Index}/{id?}");
                });
    
                //程序啟動時 獲取服務列表
                serviceHelper.GetServices();
            }
    

    代碼完成,運行測試:

    現在不用每次先請求服務列表了,是不是流暢多了?

    看一下控制台打印:

    這時候如果服務列表沒有發生變化的話,獲取服務列表的請求會一直阻塞到我們設置的10分鐘。

    隨便停止2個服務:

    這時候可以看到,數據被立馬返回了。

    繼續訪問客戶端網站,同樣流暢。
    (gif圖傳的有點問題。。。)

    至此,我們就通過Consul完成了服務的註冊與發現。
    接下來又引發新的思考。。。

    1. 每個客戶端系統都去維護這一堆服務地址,合理嗎?
    2. 服務的ip端口直接暴露給所有客戶端,安全嗎?
    3. 這種模式下怎麼做到客戶端的統一管理呢?

    代碼放在:https://github.com/xiajingren/NetCoreMicroserviceDemo

    未完待續…

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • 人類壓力步步進逼 全球13年間荒野損失面積相當於墨西哥

    環境資訊中心綜合外電;姜唯 編譯;林大利 審校

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

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

  • 印度8歲氣候人士 為氣候變遷法案請命

    摘錄自2020年9月29日公視報導

    印度一位年僅8歲的氣候人士「坎古嘉姆」,為氣候變遷相關法案請命:「我今年8歲,我是印度氣候人士,也是兒童運動的創辦人,今天我在議會前,要告訴我們最尊敬的總理莫迪,還有我們的議員,盡快通過氣候變遷法案。」

    坎古嘉姆舉著看板持續朝議會前進,遭警方攔阻並驅離。她出生於印度東北方的曼尼普爾邦,自小享受山上清淨的空氣,對擁有1900萬人口、世界上空污最嚴重的城市「德里」無法忍受。

    坎古嘉姆強調:「我希望每個國家及國際媒體,要寫故事就以我們的真名去寫,如果你說我是印度的童貝里,那你不是在寫故事,你是在刪故事。」

    氣候變遷
    國際新聞
    印度
    兒童

    本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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