分類: 3C資訊

  • 一個生物專業學生的內心獨白:我為什麼能去互聯網大廠?

    這回,考慮到近期關注了許多新朋友,並且大多都是學生黨,可能對我還不是特別熟悉。因此我決定重新把我從非科班如何通過自學(狗屎運)進入大廠的經歷分享出來,希望能夠給予一些將要面臨秋招,或者將要準備進入互聯網行業的同學一丟丟的幫助。

    早期關注我的讀者可能都約莫着記得,我是在華科讀的本碩,專業是生物醫學工程。雖說專業的的確確是帶了生物二字,但是我老實交代,實際上並不是純粹的生物技術方向上的專業。簡單科普一下:

    生物醫學工程是屬於交叉程度非常高,主要面向生物醫學領域的工程學科。我們在學校的主要課程包含硬件設計、軟件設計、生化基礎以及儀器科學等。

    所以呢,原諒本文的些許標題黨。但是說實話,由於學科高度交叉,我們實際上所習得的技能就是一團漿糊。在計算機科學方面的系統知識積累,與純生物專業相比,也不過是五十步笑百步。

    一 啟智階段

    雖說大學期間學的都不是計算機,但是我對計算機的興趣萌芽卻是十分的早。那估計都得有二十年前了,嗯誇張了點。不過應該是在小學時候了。

    我是在老家鄉下上的小學,你們可能想象不到,那個時候的鄉下小學竟然都有電腦課。不過那時候不叫電腦課,叫做“微機課”。

    微機課是幹嘛的呢?就是一群小朋友排排隊,拖鞋進到一個乾淨的房間里,然後幾個人圍着一台那種屁股又大又方的電腦,玩紙牌。沒錯,就是windows上流行多年的紙牌遊戲。

    怎麼說呢。我玩紙牌賊溜。 大概天生對這一類東西的接受能力比較強,所以上手很快,並且除了紙牌,還有空當接龍,掃雷,3D彈球。這麼說可能有點暴露年齡了。

    最早windows系統普及的這一批遊戲真的讓我對“微機”這麼個東西產生了懵懂的概念和持續的嚮往。

    二 本科階段

    之後的成長道路中,一直對計算機耍的挺溜。但是有點尷尬的是,沒人領進門。所以一直都是在娛樂,並沒有真正意義上接觸到計算機內部或者代碼編程的世界。

    初識代碼

    所以在上大學之前,基本上對編程一無所知,但是卻有一種強烈的對計算機的熱愛。不過那時對於計算機的理解僅限於【裝系統】,【裝軟件】和【拆洗主機風扇】。不要問我為什麼沒有去學計算機,要怪就只能怪考試時坐在我旁邊的那位,大概天生就得了瘋狂抖腿病的二貨。氣!

    一不小心,還進了生科院。但是緣分這種東西,真的妙不可言。這一切的開始源於一場面試。

    所在的大學是一所以工科著稱的高校,其中創業氣氛十分濃厚。學校因此有許多小有名氣的科創團隊,基本上是由老師主導,各專業學生組成的小團體。可不要小看這些小團隊,世界級程序設計大賽的獎牌獲得者經常就出於此類團隊。剛上大學的我們單純稚嫩,自然會被被這些團隊的大幅宣傳報和滿目的獎牌稱號所吸引膜拜。我也不例外。

    當時我便懷着澎湃的心情申請了一家曾多次在微軟創新杯奪得金獎的團隊。一個從鄉下來的小伙,第一次參加面試,第一次單獨和碼農小姐姐夜晚座談,第一次參加所謂的通宵測試。

    也就是在那一個晚上,開啟了我新世界的大門。

    那天晚上給我一群編程小白的任務,是模仿百度首頁,實現整個網頁的設計、布局和基本鏈接。提前給出的提示是w3c的教程網址。

    從來沒有接觸過編程的我在此之前,連編程的流程都不清楚,更不必說編譯環境、編譯語言甚至是源代碼閱讀(當然這個任務也涉及不到這些)。

    但是也就是那一個晚上,讓我真正意義上的接觸了敲代碼這個事兒。

    沒錯,我的編程起點是HTML+CSS。可惜的是,我並沒有通過那個團隊最終的面試。不過從那以後,我就開始了網站開發的自學之路。

    個人自學的堅持很大程度上基於興趣,源於在室友面前一頓裝逼后的成就感。但這就是一個生科院的學生在課後的最大樂趣所在。

    小試牛刀

    在接觸到網站開發之後,從最初的HTML+CSS到後來的HTML5+CSS3+JS+ASP,從靜態頁面的布局到動態網頁的請求。

    雖然感覺技術的成長也就是從博客的複製粘貼走向了文檔的複製粘貼。但是終於迎來了小試牛刀的機會。

    學校某大型學校組織需要做一個展示網站,朋友拉上了我和幾個人承擔了這個事情。這算是第一次真正意義上的項目開發,不過整個網站的功能不多,主要還是展示為主,後台也直接通過學校網絡平台整合就行了。所以整體下來做的事情並不多。

    迷茫困惑

    一直以來,都是自己通過博客自學,東拼西湊的建立起知識架構。但是其實技術基礎十分不牢固,不懂計算機基礎、計算機網絡,更別說編譯原理、操作系統等基本的知識。因此在很長一段時間內,覺得好像也沒有會什麼,一直對自己的能力保持懷疑。

    在這段時間內,有過跑去搞嵌入式,硬件開發,甚至是產品設計、界面設計,視頻製作。但都不一而終。

    一段迷茫,一段頹廢。一不小心就成為校園裡的學長,於是開始緊緊茫茫的尋找實驗室。

    新的方向

    好在我們專業與生科院其它專業相比,還是帶有工科氣質的。醫學影像學本身就是這個專業最為重要的一個方向。因此懷着對編程的興趣,加入了一個專門做醫學影像的實驗室。

    進入實驗室,不再是之前的漫無目的,隨處拾荒,但卻也並不會接觸到前沿新興的技術。畢竟在實驗室里,工程技術更多的只是工具。

    不過值得一提的是,在實驗室里,我接觸到了算法思想。醫學圖像本來就是圖像處理領域的一大分支,因此圖像處理算法的了解同樣也是至關重要。

    從這個時候開始,我已經不再搞網站開發那一套東西,專心使用C++寫我的算法。那時候深度學習還沒那麼熱門,大多數的圖像處理方式還是基於傳統的算法進行。雖然算法能力薄弱,但是倒也是我所求。

    同時,為了獲得數據,還接觸到Linux系統以及一些腳本,在做研究的過程中也大大增加自己的技術面。

    與此同時,為了鞏固自己的一些技術基礎,也為了督促自己的學習,我報考了計算機等級考試和國家軟考。可能對於計算機專業的學生來說,沒有什麼含金量,輕而易舉。但是對於一個非科班的學生而言,這樣的考試可以很大程度上幫助自己去重新組織零碎的知識。畢竟不是所有人都有足夠的精力去跟着計算機系的人上課。

    本科階段的經歷讓我知道自己與計算機系的差距,雖然接觸了許多東西,但是也都不夠精,甚至沒有底氣去獨立承擔一個小的開發任務。

    因此最終選擇了讀研。希望讓自己的技術都夠在某一個點上精進,能夠獨立的做出點東西來。當然最終的結果可能並不是自己當初所想,但是說到底還是得感謝在研究生期間所做出的努力。

    三 碩士階段

    我研究生的生活是從大四就開始的。因為是本校保研生,所以在大三下確定好實驗室后,基本就直接開始搬磚的苦逼生活了。當然舍友和同學在為畢業而慶祝歡快的時候,我就開始在實驗室早出晚歸。

    讀研的開始

    進入實驗室之後,剛開始一段時間也是沒有人帶。這能靠自己去多多實驗室專業方向的文章,然後學一點基本技能。更重要的其實是為了在實驗室混個臉熟。

    實驗室的研究方向主要是核醫學成像系統,整個系統的搭建涉及到核科學研究、物理數學模型的建立、电子信息的數據獲取、自動化控制設計、机械結構設計、軟件和算法處理以及醫學實驗驗證等一系列的過程。

    但對我們個人來說,主要是做某一個方向即可。當時我還是自認有一些編程基礎,於是計劃去做軟件開發或者數據處理的方向。但是最初期是沒有人帶的,所以就花三天時間自學了python3,把廖雪峰的python教程從頭到尾走了一遍。

    可惜的是,學完之後也並沒有派上用場。

    之後實驗室新項目確立,我被導師安排到了一個新項目組裡。在項目組中,主要負責的是圖像重建的部分。圖像重建是醫學影像領域非常重要的一個研究方向,目的是將影像系統採集到的多維多尺度的信息根據成像原理,依照不同算法還原成二維或三維的斷層圖像。所以本質上來說,圖像重建的重點也是在於算法設計和優化。

    方向的多變

    開始所在的項目組內,主要的工作還是基於軟件編程和算法開發,涉及到的技術棧主要還是以C++為主,然後加上一些圖型庫,如QtopenGLVTK等等。不過都是處於調用函數調包的階段,也沒有很深入的研究算法原理和實現機制。實話說其實個人提升不大。

    過了一段時間后,實驗室師兄拉我去幫他做事。師兄本身能力非常出眾,科研水平不容置疑,所以當時就過去了。之後在他手下做的事就很多變了,不只是軟件開發,還涉及到数字電路編程,PCB制板,系統仿真等。

    那個時候還來實驗室沒多久,沒有仔細的想過自己之後的就業方向,只想着能夠在研究生期間多做些成果,多發文章。因此,跟着師兄搬磚的時候也沒有過多考慮對之後的求職是否有幫助,只是想把事情做好,做出有意義的成果。

    不過幸運的是,在大四暑假前,也就是研究生入學之前。根據師兄的指導,將一個軟件開發的工作給擴充,投了一篇領域內的頂會。雖然覺得所作的工作沒什麼特別的,但是最終文章被錄還是非常開心的。也正因為這個事,之後更想着能夠多發文章多出成果。同時,師兄也比較會熬雞湯,各種說辭讓我放佛感覺到即將成為科研巨人,登上學校官網,走上人生巔峰。

    綜合能力鍛煉

    在實驗室,除了完成自己的科研任務,還有很重要的一部分,就是寫本子。也就是申請項目基金等等文字工作。

    我估計有參与過的項目申請或結題工作不下十餘項,每次都會涉及到大量文字的整理、表達、排版等工作。一般的學生對於這樣的工作都是極度排斥的,覺得很煩又沒什麼用。當然我也是這樣覺得的。

    只不過在寫了大量的本子後會發現,其實這也是綜合能力的鍛煉。我們導師常說,博士生一定要會寫項目,碩士生的話盡量寫。說明他把這也是當作一種能力的培養。在師兄手下,我也經常性的寫本子,甚至於超過了敲的代碼。那時候也沒有很明確的技術培養規劃,所以倒也覺得還行。

    獨自前行

    只不過好景不長。帶我的師兄由於過於優秀,將要離開實驗室去其它高校就職。這對當時的我來說,處境有些許尷尬。因為我的研究方向是與師兄的工作有交叉的,同時一些設備和材料也需要師兄的支持。

    實驗室本身的氛圍是以博士帶頭,碩士輔助進行項目開展的,而我的研究方向是我自己獨立出來的一個題目。說白了就是實驗室就我一個人在搞這個,但是這個方向不是導師關注的方向。

    因此,在這之後,我又回歸到了一個人做事的階段。大概是研一下開始,我就只能自己一個人去想自己的研究課題,偶爾會跟老師討論,但是也沒有很確定的工作路線。具體的工作內容也只能靠自己去思索。

    在這段期間,我寫過verilog,調過FPGA,畫過板子,畫過工程圖,當然也寫過軟件做過算法。一個人做事的話,可以說是很自由,沒有老師管,也沒有報告的壓力。只是很多時候發現有一些想法沒辦法實現,也無法對前沿領域有很深的洞察。空有一腔發文章報效實驗室的熱血,卻發現像是站在水中的浮木上,搖搖欲墜。當然這其中,個人的問題佔有很大比重。

    摸魚的日子

    不敢說自己的有多麼自律,多麼出色。可能世上大多人都是芸芸眾生,我也不過一個普通人。在自己做研究的同時,每次到臨近頂會收集文章摘要之時,我都會很积極的熬個一兩個月,將自己的想法和結果寫成一到兩篇的會議文章。我自己也大概知道其實工作沒有太大的突破點,但也是希望能夠賭一把。

    只不過兩年來,一共整理出數篇會議摘要,都沒有被同意投稿。每一次還是會有些灰心,也有些不服。會時刻回想起導師的反饋,而後想到一堆反駁的理由。只不過都沒用。

    每一次之後,都會有一段時間的消沉。不太想去實驗室,不太想看論文,在實驗室可能也就是摸摸魚罷了。很多時候覺得鬥志滿滿,又會瞬間像泄了氣的皮球,沒有精力去完成任何事。

    職業方向

    到如今,算是大四的時光,在實驗室已經呆了快四年。平心而論,沒有做過什麼突出的成果,也沒有練就什麼出色的技術。研究生活高開低走,一度以為自己是什麼科研巨星,後來僅剩的一絲熱情也在導師的「也沒看出你有什麼天分」中黯然消逝。不過還好,在這之前,我找到了自己的職業方向。

    我本身就是一個比較後知后覺的人,一方面是懶,另一方面又對自己有一種迷之自信。我真正意義上開始準備校招還是在去年六月底七月初的時候,也就是研二下學期快結束的時候吧。我們實驗室的碩士生出去基本就是兩個方向,軟件或者硬件。搞電路的基本都選擇去做硬件開發,其它的大部分都會選擇做軟件。我也沒啥可想的,雖然做過硬件,但是技術水平根本不敢出手。所以求職的方向直接就定在了軟件開發相關。

    當時還不知道應聘互聯網還要刷題,還要複習,以為上去介紹下自己,講講自己做的跟互聯網沒有半毛錢關係的項目就可以顯得很有想法的樣子。後來真正開始準備的時候才發現,自己原來還差得有點遠。

    老實說在研究生期間,主要使用C++Matlab,主要的項目就是用Qt寫了一個客戶端,裏面有網絡通信的模塊可以對數據做一些處理,並且能夠显示圖譜。用Matlab主要就是做了一些比較基礎的圖像處理和機器學習的算法。光這項目再加上非科班帶「生物」二字的專業,的確讓不清楚的人會十動然拒。

    不過其實也沒得選擇。生醫專業對口的公司都是做醫療器械,比如聯影、邁瑞等。但是其實去這些公司也是做軟件開發工程師。相對來說,互聯網公司技術好,待遇好,發展好,自然就成為大部分的選擇去向。

    說是跨行,但是其實也沒有別的更好的選擇。我們老師常說希望我們學生以後能夠在我們這個領域發光發熱。這倒是真的,國家的發展還是離不開這些能夠在某個領域深耕的人,而不是為了一昧追求熱點和高薪而忘記了初心。

    四 求職經歷

    講講我的秋招經歷吧。我的秋招雖然準備的晚,但是其實還是挺順利的。從19年8月開始接到面試,2個月內已經拿到了15+的offer,基本平均薪資都在30w+。包括抖音美團華為小米等等。那段時間真的是狀態上來了,就是佛擋殺佛,神擋殺神。

    不過剛開始的時候,由於實驗室的原因,沒有辦法出去實習。甚至由於一些原因,一直拖到暑期前才開始準備複習。

    那時已經快七月初了。急匆匆的登上各種學習網站,發現這也太多要看了的吧,還得刷題。關鍵一看面經,這都啥呀。還要手撕代碼的嗎?

    這一看當時不要緊,關鍵晚上就焦慮的快睡不着了。每天都在想應該怎樣複習,怎麼寫簡歷,沒有項目該怎麼辦。

    剛開始的時候連簡歷都不敢投,因為老覺得簡歷一過去就會安排面試。後來發現這完全就是多慮了。大概從七月初我就開始投簡歷,因為七八月是一些公司提前批招聘的階段。

    許多非科班的學生,在投遞簡歷的時候才能發現自己的無助。我在簡歷投遞初期,基本沒有任何反饋。提前批階段,許多公司都會去爭奪更優秀的簡歷候選者,對於生物專業的學生真的沒有什麼優勢。

    但是沒有關係,既然選擇了這條路,那麼就要堅持下去。投一家無人應答,那麼就投十家,投五十家。我在整個秋招階段,總共投遞過近一百家公司。許多在提前批沒有給予反饋的公司,後期大部分都有電話聯繫重新開啟面試流程。所以,就算認為自己的簡歷再不夠出色,也要相信總會有瞎了眼的HR(誤)。

    我當時一直在堅持投簡歷,只要看到的招聘信息都會去投遞。還記得第一次做測評題的時候都非常緊張,以為這就是筆試題。非常認真的拿紙筆在計算,慌的不行。

    後來直到八月初才收到第一份面試邀請,多益網絡。當時約了面試之後簡直怕死了,雖然說複習了一個月大部分的 C++ 基礎知識都看得差不多了,但是肯定是不夠的。面試的時候面試小哥全程就低頭照着題庫念,也不看我。我這邊的音頻信號也不太行,他那邊說話都聽不清楚。兩個人就在無數次的重複和確認。關鍵是面試完之後我自我感覺還非常良好。

    最終結果還是掛了,說實話打擊挺大的,感覺因此對面試產生了恐懼。不過後來試着自己跟自己講解,慢慢的也習慣了面試的感覺。

    隨後在八月底的時候,終於收穫了第一家 ihandy 的 offer。並且在進行總管面的時候,跟面試官進行了深入的交談。面完之後讓我有一種都不好意思拒絕這家公司的念頭。這也是第一次感受到了面試官的認可。

    隨後,便一發不可收拾。這個時候,已經準備了大概兩個月,基本的技術知識我都看完了,劍指 offer 上的66題以及 leetcode 上基本的題也大概刷了兩次。同時獲得了一個 offer 之後,對自己的認可度非常高,使得面試的狀態非常好。

    後來給了面試機會的公司基本都拿到了 offer。像字節跳動、美團、虎牙等等公司,面完的感覺就是基本穩了。不過可惜的是阿里簡歷面后,內推人開始說通過了,後來不知道什麼情況流程就拖到結束了。

    騰訊也一直沒有撈過我面試。感覺如果在狀態頂峰的情況下能有面試機會的話,還是很有希望的。不過也說明簡歷依然不夠出色,非科班沒有實習經歷,項目也比較水,導致AT大廠連面試機會都沒給。這也是秋招比較遺憾的地方。

    另外,記住面試過程,跟投遞簡歷一樣,一定要多面多總結。

    如果你的表達能力不好,沒有別的好的辦法,只能多練。自己在面試前問自己問題,然後用自己的話陳述出來。甚至是錄音自己聽,感覺一下面試官聽到你的回答是做何感想。

    心態要好。面試官也是人,不可能所有人都能夠絕對公平的跟你面試,所以遇到人品不好的面試官,做好自己就行了。

    要善於總結。每一次的面試都可以做好記錄,錄音或者筆記都可以。面試完之後需要多回顧,發現自己的錯誤,感受面試官對你的引導,然後下次面試注意。我一般喜歡用印象筆記記錄東西,每一次的面試記錄我都記錄在印象筆記上。電腦手機都可以看,即使是出門現場面試也不怕。

    最後就是一定要堅持下去。金九銀十,金三銀四。把握好機會,要善於規劃自己的成功。

    五 複習準備

    想進互聯網的技術崗,基本都是要提前準備的。當然某些大佬及大大佬除外。無論是校招還是社招,都需要針對自己的求職崗位進行必要的理論知識複習、項目經歷反思和算法能力訓練。只不過校招會偏向於基礎和算法能力,社招可能都會重點考察。

    除項目經歷外,複習的階段主要分為語言基礎、數據結構和算法、計算機網絡、操作系統、數據庫以及算法刷題。

    語言基礎:

    以 C++ 為主。我不喜歡看又厚又重的語言書,因此複習全程是以博客、開發文檔和實踐相結合的方式進行技術點複習。C++ 的技術點相對於其它語言來說,不算多,也不算難。技術重點的篩選可以從面經中提取而來。當然每一屆都差不多,所以找找別人總結好的資料看就行。(想要我複習資料的,可關注公眾號後台回復秋招領取)

    數據結構和算法:

    這應該是編程的基礎,重要需要了解的數據結構不出10種。花點時間弄清楚它們的原理、結構和使用方法,常用的操作也需要掌握。最難不過紅黑樹。

    這裏的算法指的是常用的算法,比如排序、遍歷,與數據結構相結合的數據操作方式。需要保證手寫才行。

    計算機網絡:

    網絡部分的內容其實可以算是最重要的,無論是前端後端都需要掌握網絡通信過程中的操作和機制。技術點可參考網絡服務器的請求和響應過程。將其中所有涉及到的協議、機制了解清楚,就可以掌握大部分了。

    操作系統:

    如果有Linux使用經歷和腳本編程基礎在面試中會很加分。對於操作系統的理解建議按照Linuxwindos系統的區分進行。

    數據庫:

    SQL基本操作必須要掌握,還包括一些關係型數據庫的基本原理和機制,內容不多,多看看就可以掌握。

    Redis同樣也是加分項,有能力的可去研究下源碼。

    算法刷題

    這一部分不多說,普通人只能勤能補拙。無論是劍指offer66題還是leetcode都可以,劍指刷兩遍,leetcode兩百題,基本沒有問題了。刷題時不要死刷,可以根據類型刷,比如鏈表操作、二叉樹操作、動態規劃等。相同類型重複做,能夠更好的培養算法思想。

    六 感想體會

    說起求職的過程,其實真要我來評價的話,估計運氣是佔了一大部分,連我這樣都行,你們也可以的。不過最終能夠獲得一些互聯網公司認可的原因,我認為主要有以下幾點原因吧:

    1.本科階段接觸互聯網行業比較早,對於這個行業有自己的見解。

    2.本科階段有考過一些計算機水平的證書,大概系統的學了一下計算機相關的基礎知識。

    3.用C++比較多,對語言基礎的理解比較好。

    4.面試狀態比較好,比較會表達自己的想法。

    5.學習能力還行,能夠在面試官的引導下找到他想問的技術點。

    6.準備過程比較有規劃,能夠快速的掌握面試的重點。

    7.人長得老實,比較容易獲得信任感。(這個你們可能學不了^_^)

    8.比較幸運。

    前天剛碩士答辯完,這两天也把學位申請的各種材料提交上去了。一不小心,七年的大學校園時光真的要結束了,兜兜轉轉感覺好像依舊是一無是處,一事無成。但是依舊希望以後能夠:

    二龍騰飛、三羊開泰、四季平安、 五福臨門、六六大順、七星高照、八方來財、九九同心、十全十美、百事亨通、千事吉祥、萬事如意

    人生無常,活在當下,且行且珍惜!

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

    【其他文章推薦】

    ※為什麼 USB CONNECTOR 是電子產業重要的元件?

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

    ※台北網頁設計公司全省服務真心推薦

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

    ※推薦評價好的iphone維修中心

  • 機器學習——手把手教你用Python實現回歸樹模型

    機器學習——手把手教你用Python實現回歸樹模型

    本文始發於個人公眾號:TechFlow,原創不易,求個關注

    今天這篇是機器學習專題的第24篇文章,我們來聊聊回歸樹模型。

    所謂的回歸樹模型其實就是用樹形模型來解決回歸問題,樹模型當中最經典的自然還是決策樹模型,它也是幾乎所有樹模型的基礎。雖然基本結構都是使用決策樹,但是根據預測方法的不同也可以分為兩種。第一種,樹上的恭弘=叶 恭弘子節點就對應一個預測值和分類樹對應,這一種方法稱為回歸樹。第二種,樹上的恭弘=叶 恭弘子節點對應一個線性模型,最後的結果由線性模型給出。這一種方法稱為模型樹。

    今天我們先來看看其中的回歸樹。

    回歸樹模型

    回歸樹模型的核心算法,也就是構建決策樹的算法,就是我們上篇文章所講的CART算法。如果有生疏或者是遺漏的同學,可以通過下方傳送門回顧一下:

    機器學習——十大數據挖掘之一的決策樹CART算法

    CART算法的核心精髓就是我們每次選擇特徵對數據進行拆分的時候,永遠對數據集進行二分。無論是離散特徵還是連續性特徵,一視同仁。CART還有一個特點是使用GINI指數而不是信息增益或者是信息增益比來選擇拆分的特徵,但是在回歸問題當中用不到這個。因為回歸問題的損失函數是均方差,而不是交叉熵,很難用熵來衡量連續值的準確度。

    在分類樹當中,我們一個恭弘=叶 恭弘子節點代表一個類別的預測值,這個類別的值是落到這個恭弘=叶 恭弘子節點當中訓練樣本的類別的眾數,也就是出現頻率最高的類別。在回歸樹當中,恭弘=叶 恭弘子節點對應的自然就是一個連續值。這個連續值是落到這個節點的訓練樣本的均值,它的誤差就是這些樣本的均方差。

    另外,之前我們在選擇特徵的劃分閾值的時候,對閾值的選擇進行了優化,只選擇了那些會引起預測類別變化的閾值。但是在回歸問題當中,由於預測值是一個浮點數,所以這個優化也不存在了。整體上來說,其實回歸樹的實現難度比分類樹是更低的。

    實戰

    我們首先來加載數據,我們這次使用的是scikit-learn庫當中經典的波士頓房價預測的數據。關於房價預測,kaggle當中也有一個類似的比賽,叫做:house-prices-advanced-regression-techniques。不過給出的特徵更多,並且存在缺失等情況,需要我們進行大量的特徵工程。感興趣的同學可以自行研究一下。

    首先,我們來獲取數據,由於sklearn庫當中已經有數據了,我們可以直接調用api獲取,非常簡單:

    import numpy as np
    import pandas as pd
    from sklearn.datasets import load_boston
    boston = load_boston()
    
    X, y = boston.data, boston.target
    

    我們輸出前幾條數據查看一下:

    這個數據質量很高,sklearn庫已經替我們做完了數據篩選與特徵工程,直接拿來用就可以了。為了方便我們傳遞數據,我們將X和y合併在一起。由於y是一維的數組形式是不能和二維的X合併的,所以我們需要先對y進行reshape之後再進行合併。

    y = y.reshape(-1, 1)
    X = np.hstack((X, y))
    

    hstack函數可以將兩個np的array橫向拼接,與之對應的是vstack,是將兩個array縱向拼接,這個也是常規操作。合併之後,y作為新的一列添加在了X的後面。數據搞定了,接下來就要輪到實現模型了。

    在實現決策樹的主體部分之前,我們先來實現兩個輔助函數。第一個輔助函數是計算一批樣本的方差和,第二個輔助函數是獲取樣本的均值,也就是子節點的預測值。

    def node_mean(X):
        return np.mean(X[:, -1])
    
    
    def node_variance(X):
        return np.var(X[:, -1]) * X.shape[0]
    

    這個搞定了之後,我們繼續實現根據閾值拆分數據的函數。這個也可以復用之前的代碼:

    from collections import defaultdict
    def split_dataset(X, idx, thred):
        split_data = defaultdict(list)
        for x in X:
            split_data[x[idx] < thred].append(x)
        return list(split_data.values()), list(split_data.keys())
    

    接下來是兩個很重要的函數,分別是get_thresholds和split_variance。顧名思義,第一個函數用來獲取閾值,前面說了由於我們做的是回歸模型,所以理論上來說特徵的每一個取值都可以作為切分的依據。但是也不排除可能會存在多條數據的特徵值相同的情況,所以我們對它進行去重。第二個函數是根據閾值對數據進行拆分,返回拆分之後的方差和。

    def get_thresholds(X, i):
        return set(X[:, i].tolist())
    
    # 每次迭代方差優化的底線
    MINIMUM_IMPROVE = 2.0
    # 每個恭弘=叶 恭弘子節點最少樣本數
    MINIMUM_SAMPLES = 10
    
    def split_variance(dataset, idx, threshold):
        left, right = [], []
        n = dataset.shape[0]
        for data in dataset:
            if data[idx] < threshold:
                left.append(data)
            else:
                right.append(data)
        left, right = np.array(left), np.array(right)
        # 預剪枝
        # 如果拆分結果有一邊過少,則返回None,防止過擬合
        if len(left) < MINIMUM_SAMPLES or len(right) < MINIMUM_SAMPLES:
            return None
        # 拆分之後的方差和等於左子樹的方差和加上右子樹的方差和
        # 因為是方差和而不是均方差,所以可以累加
        return node_variance(left) + node_variance(right)
    

    這裏我們用到了MINIMUM_SAMPLES這個參數,它是用來預剪枝用的。由於我們是回歸模型,如果不對決策樹的生長加以限制,那麼很有可能得到的決策樹的恭弘=叶 恭弘子節點和訓練樣本的數量一樣多。這顯然就陷入了過擬合了,對於模型的效果是有害無益的。所以我們要限制每個節點的樣本數量,這個是一個參數,我們可以根據需要自行調整。

    接下來,就是特徵和閾值篩選的函數了。我們需要開發一個函數來遍歷所有可以拆分的特徵和閾值,對數據進行拆分,從所有特徵當中找到最佳的拆分可能。

    def choose_feature_to_split(dataset):
        n = len(dataset[0])-1
        m = len(dataset)
        # 記錄最佳方差,特徵和閾值
        var_ = node_variance(dataset)
        bestVar = float('inf')
        feature = -1
        thred = None
        for i in range(n):
            threds = get_thresholds(dataset, i)
            for t in threds:
                # 遍歷所有的閾值,計算每個閾值的variance
                v = split_variance(dataset, i, t)
                # 如果v等於None,說明拆分過擬合了,跳過
                if v is None:
                    continue
                if v  < bestVar:
                    bestVar, feature, thred = v, i, t
        # 如果最好的拆分效果達不到要求,那麼就不拆分,控制子樹的數量
        if var_ - bestVar < MINIMUM_IMPROVE:
            return None, None
        return feature, thred
    
    

    和上面一樣,這個函數當中也用到了一個預剪枝的參數MINIMUM_IMPROVE,它衡量的是每一次生成子樹帶來的收益。當某一次生成子樹帶來的收益小於某個值的時候,說明收益很小,並不划算,所以我們就放棄這次子樹的生成。這也是預剪枝的一種。

    這些都搞定了之後,就可以來建樹了。建樹的過程和之前類似,只是我們這一次的數據當中沒有特徵的name,所以我們去掉特徵名稱的相關邏輯。

    def create_decision_tree(dataset):
        dataset = np.array(dataset)
        
        # 如果當前數量小於10,那麼就不再繼續劃分了
        if dataset.shape[0] < MINIMUM_SAMPLES:
            return node_mean(dataset)
        
        # 記錄最佳拆分的特徵和閾值
        fidx, th = choose_feature_to_split(dataset)
        if fidx is None:
            return th
        
        node = {}
        node['feature'] = fidx
        node['threshold'] = th
        
        # 遞歸建樹
        split_data, vals = split_dataset(dataset, fidx, th)
        for data, val in zip(split_data, vals):
            node[val] = create_decision_tree(data)
        return node
    

    我們來完整測試一下建樹,首先我們需要對原始數據進行拆分。將原始數據拆分成訓練數據和測試數據,由於我們的場景比較簡單,就不設置驗證數據了。

    拆分數據不用我們自己實現,sklearn當中提供了相應的工具,我們直接調用即可:

    from sklearn.model_selection import train_test_split
    
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=23)
    

    我們一般用到的參數就兩個,一個是test_size,它可以是一個整數也可以是一個浮點數。如果是整數,代表的是測試集的樣本數量。如果是一個0-1.0的浮點數,則代表測試集的佔比。random_state是生成隨機數的時候用到的隨機種子。

    我們輸出一下生成的樹,由於數據量比較大,可以看到一顆龐大的樹結構。建樹的部分實現了之後,最後剩下的就是預測的部分了。

    預測部分的代碼和之前分類樹相差不大,整體的邏輯完全一樣,只是去掉了feature_names的相關邏輯。

    def classify(node, data):
        key = node['feature']
        pred = None
        thred = node['threshold']
    
        if isinstance(node[data[key] < thred], dict):
            pred = classify(node[data[key] < thred], data)
        else:
            pred = node[data[key] < thred]
                
        # 放置pred為空,挑選一個恭弘=叶 恭弘子節點作為替補
        if pred is None:
            for key in node:
                if not isinstance(node[key], dict):
                    pred = node[key]
                    break
        return pred
    

    由於這個函數一次只能接受一條數據,如果我們想要批量預測的話還不行,所以最好的話再實現一個批量預測的predict函數比較好。

    def predict(node, X):
        y_pred = []
        for x in X:
            y = classify(node, x)
            y_pred.append(y)
        return np.array(y_pred)
    

    后剪枝

    后剪枝的英文原文是post-prune,但是翻譯成事後剪枝也有點奇怪。anyway,我們就用后剪枝這個詞好了。

    在回歸樹當中,我們利用的思想非常樸素,在建樹的時候建立一棵盡量複雜龐大的樹。然後在通過測試集對這棵樹進行修剪,修剪的邏輯也非常簡單,我們判斷一棵子樹存在分叉和沒有分叉單獨成為恭弘=叶 恭弘子節點時的誤差,如果修剪之後誤差更小,那麼我們就減去這棵子樹。

    整個剪枝的過程和建樹的過程一樣,從上到下,遞歸執行。

    整個邏輯很好理解,我們直接來看代碼:

    def is_dict(node):
        return isinstance(node, dict)
    
    
    def prune(node, testData):
        testData = np.array(testData)
        if testData.shape[0] == 0:
            return node
     
        # 拆分數據
        split_data, _ = split_dataset(testData, node['feature'], node['threshold'])
        # 對左右子樹遞歸修剪
        if is_dict(node[0]):
            node[0] = prune(node[0], split_data[0])
        if is_dict(node[1]) and len(split_data) > 1:
            node[1] = prune(node[1], split_data[1])
    
        # 如果左右都是恭弘=叶 恭弘子節點,那麼判斷當前子樹是否需要修剪
        if len(split_data) > 1 and not is_dict(node[0]) and not is_dict(node[1]):
            # 計算修剪前的方差和
            baseError = np.sum(np.power(np.array(split_data[0])[:, -1] - node[0], 2)) + np.sum(np.power(np.array(split_data[1])[:, -1] - node[1], 2))
            # 計算修剪后的方差和
            meanVal = (node[0] + node[1]) / 2
            mergeError = np.sum(np.power(meanVal - testData[:, -1], 2))
            if mergeError < baseError:
                return meanVal
            else:
                return node
        return node
    

    最後,我們對修剪之後的效果做一下驗證:

    從圖中可以看到,修剪之前我們在測試數據上的均方差是19.65,而修剪之後降低到了19.48。從數值上來看是有效果的,只是由於我們的訓練數據比較少,同時進行了預剪枝,影響了后剪枝的效果。但是對於實際的機器學習工程來說,一個方法只要是有明確效果的,在代價可以承受的範圍內,它就是有價值的,千萬不能覺得提升不明顯,而隨便否定一個方法。

    這裏計算均方差的時候用到了sklearn當中的一個庫函數mean_square_error,從名字當中我們也可以看得出來它的用途,它可以對兩個Numpy的array計算均方差

    總結

    關於回歸樹模型的相關內容到這裏就結束了,我們不僅親手實現了模型,而且還在真實的數據集上做了實驗。如果你是親手實現的模型的代碼,相信你一定會有很多收穫。

    雖然從實際運用來說我們幾乎不會使用樹模型來做回歸任務,但是回歸樹模型本身是非常有意義的。因為在它的基礎上我們發展出了很多效果更好的模型,比如大名鼎鼎的GBDT。因此理解回歸樹對於我們後續進階的學習是非常重要的。在深度學習普及之前,其實大多數高效果的模型都是以樹模型為基礎的,比如隨機森林、GBDT、Adaboost等等。可以說樹模型撐起了機器學習的半個時代,這麼說相信大家應該都能理解它的重要性了吧。

    今天的文章就到這裏,如果喜歡本文,可以的話,請點個關注,給我一點鼓勵,也方便獲取更多文章。

    本文使用 mdnice 排版

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

    【其他文章推薦】

    USB CONNECTOR掌控什麼技術要點? 帶您認識其相關發展及效能

    台北網頁設計公司這麼多該如何選擇?

    ※智慧手機時代的來臨,RWD網頁設計為架站首選

    ※評比南投搬家公司費用收費行情懶人包大公開

    ※回頭車貨運收費標準

  • 【C#】AutoMapper 使用手冊

    目錄

    • 1 入門例子
    • 2 註冊
      • 2.1 Profile
    • 3 配置
      • 3.1 命名約定
      • 3.2 配置可見性
      • 3.3 全局屬性/字段過濾
      • 3.4 識別前綴和後綴
      • 3.5 替換字符
    • 4 調用構造函數
    • 5 數組和列表映射
      • 5.1 處理空集合
      • 5.2 集合中的多態
    • 6 方法到屬性映射
    • 7 自定義映射
    • 8 扁平化映射
      • 8.1 IncludeMembers
    • 9 嵌套映射

    本文基於 AutoMapper 9.0.0

    AutoMapper 是一個對象-對象映射器,可以將一個對象映射到另一個對象。

    官網地址:http://automapper.org/

    官方文檔:https://docs.automapper.org/en/latest/

    1 入門例子

    public class Foo
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class FooDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public void Map()
    {
        var config = new MapperConfiguration(cfg => cfg.CreateMap<Foo, FooDto>());
    
        var mapper = config.CreateMapper();
    
        Foo foo = new Foo { ID = 1, Name = "Tom" };
    
        FooDto dto = mapper.Map<FooDto>(foo);
    }
    

    2 註冊

    在使用 Map 方法之前,首先要告訴 AutoMapper 什麼類可以映射到什麼類。

    var config = new MapperConfiguration(cfg => cfg.CreateMap<Foo, FooDto>());
    

    每個 AppDomain 只能進行一次配置。這意味着放置配置代碼的最佳位置是在應用程序啟動中,例如 ASP.NET 應用程序的 Global.asax 文件。

    從 9.0 開始 Mapper.Initialize 方法就不可用了。

    2.1 Profile

    Profile 是組織映射的另一種方式。新建一個類,繼承 Profile,並在構造函數中配置映射。

    public class EmployeeProfile : Profile
    {
        public EmployeeProfile()
        {
            CreateMap<Employee, EmployeeDto>();
        }
    }
    
    var config = new MapperConfiguration(cfg =>
    {
        cfg.AddProfile<EmployeeProfile>();
    });
    

    Profile 內部的配置僅適用於 Profile 內部的映射。應用於根配置的配置適用於所有創建的映射。

    AutoMapper 也可以在指定的程序集中掃描從 Profile 繼承的類,並將其添加到配置中。

    var config = new MapperConfiguration(cfg =>
    {
        // 掃描當前程序集
        cfg.AddMaps(System.AppDomain.CurrentDomain.GetAssemblies());
        
        // 也可以傳程序集名稱(dll 名稱)
        cfg.AddMaps("LibCoreTest");
    });
    

    3 配置

    3.1 命名約定

    默認情況下,AutoMapper 基於相同的字段名映射,並且是 不區分大小寫 的。

    但有時,我們需要處理一些特殊的情況。

    • SourceMemberNamingConvention 表示源類型命名規則
    • DestinationMemberNamingConvention 表示目標類型命名規則

    LowerUnderscoreNamingConventionPascalCaseNamingConvention 是 AutoMapper 提供的兩個命名規則。前者命名是小寫並包含下劃線,後者就是帕斯卡命名規則(每個單詞的首字母大寫)。

    我的理解,如果源類型和目標類型分別採用了 蛇形命名法駝峰命名法,那麼就需要指定命名規則,使其能正確映射。

    public class Foo
    {
        public int Id { get; set; }
    
        public string MyName { get; set; }
    }
    
    public class FooDto
    {
        public int ID { get; set; }
    
        public string My_Name { get; set; }
    }
    
    public void Map()
    {
        var config = new MapperConfiguration(cfg =>
        {
            cfg.CreateMap<Foo, FooDto>();
    
            cfg.SourceMemberNamingConvention = new PascalCaseNamingConvention();
            cfg.DestinationMemberNamingConvention = new LowerUnderscoreNamingConvention();
        });
    
        var mapper = config.CreateMapper();
    
        Foo foo = new Foo { Id = 2, MyName = "Tom" };
    
        FooDto dto = mapper.Map<FooDto>(foo);
    }
    

    3.2 配置可見性

    默認情況下,AutoMapper 僅映射 public 成員,但其實它是可以映射到 private 屬性的。

    var config = new MapperConfiguration(cfg =>
    {
        cfg.ShouldMapProperty = p => p.GetMethod.IsPublic || p.SetMethod.IsPrivate;
        cfg.CreateMap<Source, Destination>();
    });
    

    需要注意的是,這裏屬性必須添加 private set,省略 set 是不行的。

    3.3 全局屬性/字段過濾

    默認情況下,AutoMapper 嘗試映射每個公共屬性/字段。以下配置將忽略字段映射。

    var config = new MapperConfiguration(cfg =>
    {
    	cfg.ShouldMapField = fi => false;
    });
    

    3.4 識別前綴和後綴

    var config = new MapperConfiguration(cfg =>
    {
        cfg.RecognizePrefixes("My");
        cfg.RecognizePostfixes("My");
    }
    

    3.5 替換字符

    var config = new MapperConfiguration(cfg =>
    {
        cfg.ReplaceMemberName("Ä", "A");
    });
    

    這功能我們基本上用不上。

    4 調用構造函數

    有些類,屬性的 set 方法是私有的。

    public class Commodity
    {
        public string Name { get; set; }
    
        public int Price { get; set; }
    }
    
    public class CommodityDto
    {
        public string Name { get; }
    
        public int Price { get; }
    
        public CommodityDto(string name, int price)
        {
            Name = name;
            Price = price * 2;
        }
    }
    

    AutoMapper 會自動找到相應的構造函數調用。如果在構造函數中對參數做一些改變的話,其改變會反應在映射結果中。如上例,映射后 Price 會乘 2。

    禁用構造函數映射:

    var config = new MapperConfiguration(cfg => cfg.DisableConstructorMapping());
    

    禁用構造函數映射的話,目標類要有一個無參構造函數。

    5 數組和列表映射

    數組和列表的映射比較簡單,僅需配置元素類型,定義簡單類型如下:

    public class Source
    {
        public int Value { get; set; }
    }
    
    public class Destination
    {
        public int Value { get; set; }
    }
    

    映射:

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Source, Destination>();
    });
    IMapper mapper = config.CreateMapper();
    
    var sources = new[]
    {
        new Source { Value = 5 },
        new Source { Value = 6 },
        new Source { Value = 7 }
    };
    
    IEnumerable<Destination> ienumerableDest = mapper.Map<Source[], IEnumerable<Destination>>(sources);
    ICollection<Destination> icollectionDest = mapper.Map<Source[], ICollection<Destination>>(sources);
    IList<Destination> ilistDest = mapper.Map<Source[], IList<Destination>>(sources);
    List<Destination> listDest = mapper.Map<Source[], List<Destination>>(sources);
    Destination[] arrayDest = mapper.Map<Source[], Destination[]>(sources);
    

    具體來說,支持的源集合類型包括:

    • IEnumerable
    • IEnumerable
    • ICollection
    • ICollection
    • IList
    • IList
    • List
    • Arrays

    映射到現有集合時,將首先清除目標集合。如果這不是你想要的,請查看AutoMapper.Collection。

    5.1 處理空集合

    映射集合屬性時,如果源值為 null,則 AutoMapper 會將目標字段映射為空集合,而不是 null。這與 Entity Framework 和 Framework Design Guidelines 的行為一致,認為 C# 引用,數組,List,Collection,Dictionary 和 IEnumerables 永遠不應該為 null

    5.2 集合中的多態

    這個官方的文檔不是很好理解。我重新舉個例子。實體類如下:

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class Employee2 : Employee
    {
        public string DeptName { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class EmployeeDto2 : EmployeeDto
    {
        public string DeptName { get; set; }
    }
    

    數組映射代碼如下:

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>().Include<Employee2, EmployeeDto2>();
        cfg.CreateMap<Employee2, EmployeeDto2>();
    });
    IMapper mapper = config.CreateMapper();
    
    var employees = new[]
    {
        new Employee { ID = 1, Name = "Tom" },
        new Employee2 { ID = 2, Name = "Jerry", DeptName = "R & D" }
    };
    
    var dto = mapper.Map<Employee[], EmployeeDto[]>(employees);
    

    可以看到,映射后,dto 中兩個元素的類型,一個是 EmployeeDto,一個是 EmployeeDto2,即實現了父類映射到父類,子類映射到子類。

    如果去掉 Include 方法,則映射后 dto 中兩個元素的類型均為 EmployeeDto

    6 方法到屬性映射

    AutoMapper 不僅能實現屬性到屬性映射,還可以實現方法到屬性的映射,並且不需要任何配置,方法名可以和屬性名一致,也可以帶有 Get 前綴。

    例如下例的 Employee.GetFullName() 方法,可以映射到 EmployeeDto.FullName 屬性。

    public class Employee
    {
        public int ID { get; set; }
    
        public string FirstName { get; set; }
    
        public string LastName { get; set; }
    
        public string GetFullName()
        {
            return $"{FirstName} {LastName}";
        }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string FirstName { get; set; }
    
        public string LastName { get; set; }
    
        public string FullName { get; set; }
    }
    

    7 自定義映射

    當源類型與目標類型名稱不一致時,或者需要對源數據做一些轉換時,可以用自定義映射。

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public DateTime JoinTime { get; set; }
    }
    
    public class EmployeeDto
    {
        public int EmployeeID { get; set; }
    
        public string EmployeeName { get; set; }
    
        public int JoinYear { get; set; }
    }
    

    如上例,IDEmployeeID 屬性名不同,JoinTimeJoinYear 不僅屬性名不同,屬性類型也不同。

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>()
            .ForMember("EmployeeID", opt => opt.MapFrom(src => src.ID))
            .ForMember(dest => dest.EmployeeName, opt => opt.MapFrom(src => src.Name))
            .ForMember(dest => dest.JoinYear, opt => opt.MapFrom(src => src.JoinTime.Year));
    });
    

    8 扁平化映射

    對象-對象映射的常見用法之一是將複雜的對象模型並將其展平為更簡單的模型。

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public Department Department { get; set; }
    }
    
    public class Department
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public int DepartmentID { get; set; }
    
        public string DepartmentName { get; set; }
    }
    

    如果目標類型上的屬性,與源類型的屬性、方法都對應不上,則 AutoMapper 會將目標成員名按駝峰法拆解成單個單詞,再進行匹配。例如上例中,EmployeeDto.DepartmentID 就對應到了 Employee.Department.ID

    8.1 IncludeMembers

    如果屬性命名不符合上述的規則,而是像下面這樣:

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public Department Department { get; set; }
    }
    
    public class Department
    {
        public int DepartmentID { get; set; }
    
        public string DepartmentName { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public int DepartmentID { get; set; }
    
        public string DepartmentName { get; set; }
    }
    

    Department 類中的屬性名,直接跟 EmployeeDto 類中的屬性名一致,則可以使用 IncludeMembers 方法指定。

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>().IncludeMembers(e => e.Department);
        cfg.CreateMap<Department, EmployeeDto>();
    });
    

    9 嵌套映射

    有時,我們可能不需要展平。看如下例子:

    public class Employee
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public int Age { get; set; }
    
        public Department Department { get; set; }
    }
    
    public class Department
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public string Heads { get; set; }
    }
    
    public class EmployeeDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    
        public DepartmentDto Department { get; set; }
    }
    
    public class DepartmentDto
    {
        public int ID { get; set; }
    
        public string Name { get; set; }
    }
    

    我們要將 Employee 映射到 EmployeeDto,並且將 Department 映射到 DepartmentDto

    var config = new MapperConfiguration(cfg =>
    {
        cfg.CreateMap<Employee, EmployeeDto>();
        cfg.CreateMap<Department, DepartmentDto>();
    });
    

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

    【其他文章推薦】

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

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

    ※想知道購買電動車哪裡補助最多?台中電動車補助資訊懶人包彙整

    南投搬家公司費用,距離,噸數怎麼算?達人教你簡易估價知識!

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

    ※超省錢租車方案

  • 環境篇:嘔心瀝血@CDH線上調優

    環境篇:嘔心瀝血@CDH線上調優

    環境篇:嘔心瀝血@CDH線上調優

    • 為什麼出這篇文章?

    近期有很多公司開始引入大數據,由於各方資源有限,並不能合理分配服務器資源,和服務器選型,小恭弘=叶 恭弘這裏將工作中的總結出來,給新入行的小夥伴帶個方向,不敢說一定對,但是本人親自測試,發現集群使用率穩定提高了3分之1,最高可達到2分之1,有不對的地方歡迎留言指出。

    注:可能有些服務沒有設計,使用到的小夥伴可以參照這種方式去規劃。

    0 資源:集群服務安排

    服務名稱 子服務 CM-64G ZK-Kafka(3台)-12G DataNode(3台)-64G NameNode1-64G NameNode2-64G Resourcemanager1-32G Resourcemanager2-32G hive-hbase-16G hive-hbase-16G
    MySQL MySQL
    CM Activity Monitor
    Alert Publisher
    Event Server
    Host Monitor
    Service Monitor




    HDFS NameNode
    DataNode
    Failover Controller
    JournalNode
    X

    X
    X

    X


    X

    X
    X
    X
    Yarn NodeManager
    Resourcemanager
    JobHisoryServer

    X
    X
    X

    X

    Zookeeper Zookeeper Server
    Kafka Kafka Broker
    Hive Hive Metastore Server
    HiveServer2
    Gateway(安裝對應應用服務器)
    X


    X

    X
    Hbase HMaster
    HRegionServer
    Thrift Server
    X


    X

    X
    Oozie Oozie Server
    Hue Hue Server
    Load Balancer
    X

    X
    Spark History Server
    Gateway(安裝對應應用服務器)

    X
    Flume Flume Agent (安裝對應應用服務器)
    Sqoop Sqoop(安裝對應應用服務器)

    1 優化:Cloudera Management

    1.1 Cloudera Management Service

    這些服務主要是提供監控功能,目前的調整主要集中在內存放,以便有足夠的資源 完成集群管理。

    服務 選項 配置值
    Activity Monitor Java Heap Size 2G
    Alert Publisher Java Heap Size 2G
    Event Server Java Heap Size 2G
    Host Monitor Java Heap Size 4G
    Service Monitor Java Heap Size 4G
    Reports Manager Java Heap Size 2G
    Navigator Metadata Server Java Heap Size 8G

    2 優化:Zookeeper

    服務 選項 配置值
    Zookeeper Java Heap Size (堆棧大小) 4G
    Zookeeper maxClientCnxns (最大客戶端連接數) 1024
    Zookeeper dataDir (數據文件目錄+數據持久化路徑) /hadoop/zookeeper (建議獨立目錄)
    Zookeeper dataLogDir (事務日誌目錄) /hadoop/zookeeper_log (建議獨立目錄)

    3 優化:HDFS

    3.1 磁盤測試

    3.1.1 讀測試

    hdparm 用於查看硬盤的相關信息或對硬盤進行測速、優化、修改硬盤相關參數設定

    #安裝hdparm
    yum install hdparm
    #獲取硬盤符
    fdisk -l
    #讀測試(讀取上一步獲取硬盤符)
    hdparm -t /dev/vda
    

    三次測試結果:

    Timing buffered disk reads: 500 MB in 0.84 seconds = 593.64 MB/sec

    Timing buffered disk reads: 500 MB in 0.93 seconds = 538.80 MB/sec

    Timing buffered disk reads: 500 MB in 0.74 seconds = 672.95 MB/sec

    說明:接近1s秒讀取了500MB磁盤,讀速度約 500 MB/秒

    3.1.2 寫測試

    dd 這裏使用 time + dd 簡單測試寫速度,不要求很精確

    查看內存緩存情況
    free -m
     
    清除緩存
    sync; echo 3 > /proc/sys/vm/drop_caches
     
    查block size
    blockdev --getbsz /dev/vda
     
    寫測試
    echo 3 > /proc/sys/vm/drop_caches; time dd if=/dev/zero of=/testdd bs=4k count=100000
    

    三次測試結果:

    記錄了100000+0 的讀入
    記錄了100000+0 的寫出

    409600000 bytes (410 MB) copied, 0.574066 s, 714 MB/s –410MB複製,用時0.57秒,評估714M/s

    409600000 bytes (410 MB) copied, 1.84421 s, 222 MB/s –410MB複製,用時1.84秒,評估222 M/s

    409600000 bytes (410 MB) copied, 1.06969 s, 383 MB/s –410MB複製,用時1.06秒,評估383M/s

    3.1.3 網絡帶寬

    iperf3測量一個網絡最大帶寬

    #安裝iperf3
    yum -y install iperf3
     
    #服務端
    iperf3 -s
     
    #客戶端
    iperf3 -c 上調命令執行的服務機器IP
    

    測試結果:

    [ ID]–>線程id Interva–>傳輸時間 Transfer–>接收數據大小 Bandwidth–>帶寬每秒大小 Retr 角色
    [ 4] 0.00-10.00 sec 17.0 GBytes 14.6 Gbits/sec 0 sender–>發送
    [ 4] 0.00-10.00 sec 17.0 GBytes 14.6 Gbits/sec receiver–>接收

    3.2 官方壓測

    3.2.1 用戶準備

    由於只能使用yarn配置了允許用戶,故這裏選擇hive用戶,如果su hive不能進入,則需要配置該步驟

    usermod -s /bin/bash  hive
    su hive
    

    3.2.2 HDFS 寫性能測試

    • 測試內容:HDFS集群寫入10個128M文件(-D指定文件存儲目錄)
    hadoop  jar /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/jars/hadoop-mapreduce-client-jobclient-3.0.0-cdh6.2.0-tests.jar  TestDFSIO  -D test.build.data=/test/benchmark -write -nrFiles 10 -fileSize 128
    

    INFO fs.TestDFSIO: —– TestDFSIO —– : write
    INFO fs.TestDFSIO: Date & time: Thu Jun 11 10:30:36 CST 2020
    INFO fs.TestDFSIO: Number of files: 10 –十個文件
    INFO fs.TestDFSIO: Total MBytes processed: 1280 –總大小1280M
    INFO fs.TestDFSIO: Throughput mb/sec: 16.96 –吞吐量 每秒16.96M
    INFO fs.TestDFSIO: Average IO rate mb/sec: 17.89 –平均IO情況17.89M
    INFO fs.TestDFSIO: IO rate std deviation: 4.74 –IO速率標準偏差
    INFO fs.TestDFSIO: Test exec time sec: 46.33 –總運行時間

    3.2.3 HDFS 讀性能測試

    • 測試內容:HDFS集群讀取10個128M文件
    hadoop jar /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/jars/hadoop-mapreduce-client-jobclient-3.0.0-cdh6.2.0-tests.jar TestDFSIO  -D test.build.data=/test/benchmark -read -nrFiles 10 -fileSize 128
    

    INFO fs.TestDFSIO: —– TestDFSIO —– : read
    INFO fs.TestDFSIO: Date & time: Thu Jun 11 10:41:19 CST 2020
    INFO fs.TestDFSIO: Number of files: 10 –文件數
    INFO fs.TestDFSIO: Total MBytes processed: 1280 –總大小
    INFO fs.TestDFSIO: Throughput mb/sec: 321.53 –吞吐量 每秒321.53M
    INFO fs.TestDFSIO: Average IO rate mb/sec: 385.43 –平均IO情況385.43M
    INFO fs.TestDFSIO: IO rate std deviation: 107.67 –IO速率標準偏差
    INFO fs.TestDFSIO: Test exec time sec: 20.81 –總運行時間

    3.2.4 刪除測試數據

    hadoop jar /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/jars/hadoop-mapreduce-client-jobclient-3.0.0-cdh6.2.0-tests.jar TestDFSIO  -D test.build.data=/test/benchmark -clean
    

    3.3 參數調優

    服務 選項 配置值
    NameNode Java Heap Size (堆棧大小) 56G
    NameNode dfs.namenode.handler.count (詳見3.3.2) 80
    NameNode dfs.namenode.service.handler.count (詳見3.3.2) 80
    NameNode fs.permissions.umask-mode (使用默認值022) 027(使用默認值022)
    DataNode Java Heap Size (堆棧大小) 8G
    DataNode dfs.datanode.failed.volumes.tolerated (詳見3.3.3) 1
    DataNode dfs.datanode.balance.bandwidthPerSec (DataNode 平衡帶寬) 100M
    DataNode dfs.datanode.handler.count (服務器線程數) 64
    DataNode dfs.datanode.max.transfer.threads (最大傳輸線程數) 20480
    JournalNode Java Heap Size (堆棧大小) 1G

    3.3.1 數據塊優化

    dfs.blocksize = 128M

    • 文件以塊為單位進行切分存儲,塊通常設置的比較大(最小6M,默認128M),根據網絡帶寬計算最佳值。
    • 塊越大,尋址越快,讀取效率越高,但同時由於MapReduce任務也是以塊為最小單位來處理,所以太大的塊不利於於對數據的并行處理。
    • 一個文件至少佔用一個塊(如果一個1KB文件,佔用一個塊,但是佔用空間還是1KB)
    • 我們在讀取HDFS上文件的時候,NameNode會去尋找block地址,尋址時間為傳輸時間的1%時,則為最佳狀態。
      • 目前磁盤的傳輸速度普遍為100MB/S
      • 如果尋址時間約為10ms,則傳輸時間=10ms/0.01=1000ms=1s
      • 如果傳輸時間為1S,傳輸速度為100MB/S,那麼一秒鐘我們就可以向HDFS傳送100MB文件,設置塊大小128M比較合適。
      • 如果帶寬為200MB/S,那麼可以將block塊大小設置為256M比較合適。

    3.3.2 NameNode 的服務器線程的數量

    • dfs.namenode.handler.count=20*log2(Cluster Size),比如集群規模為16 ,8以2為底的對數是4,故此參數設置為80
    • dfs.namenode.service.handler.count=20*log2(Cluster Size),比如集群規模為16 ,8以2為底的對數是4,故此參數設置為80

    NameNode有一個工作線程池,用來處理不同DataNode的併發心跳以及客戶端併發的元數據操作。該值需要設置為集群大小的自然對數乘以20,。

    3.3.3 DataNode 停止提供服務前允許失敗的卷的數量

    DN多少塊盤損壞后停止服務,默認為0,即一旦任何磁盤故障DN即關閉。 對盤較多的集群(例如DN有超過2塊盤),磁盤故障是常態,通常可以將該值設置為1或2,避免頻繁有DN下線。

    4 優化:YARN + MapReduce

    服務 選項 配置值 參數說明
    ResourceManager Java Heap Size (堆棧大小) 4G
    ResourceManager yarn.scheduler.minimum-allocation-mb (最小容器內存) 2G 給應用程序 Container 分配的最小內存
    ResourceManager yarn.scheduler.increment-allocation-mb (容器內存增量) 512M 如果使用 Fair Scheduler,容器內存允許增量
    ResourceManager yarn.scheduler.maximum-allocation-mb (最大容器內存) 32G 給應用程序 Container 分配的最大內存
    ResourceManager yarn.scheduler.minimum-allocation-vcores (最小容器虛擬 CPU 內核數量) 1 每個 Container 申請的最小 CPU 核數
    ResourceManager yarn.scheduler.increment-allocation-vcores (容器虛擬 CPU 內核增量) 1 如果使用 Fair Scheduler,虛擬 CPU 內核允許增量
    ResourceManager yarn.scheduler.maximum-allocation-vcores (最大容器虛擬 CPU 內核數量) 16 每個 Container 申請的最大 CPU 核數
    ResourceManager yarn.resourcemanager.recovery.enabled true 啟用后,ResourceManager 中止時在群集上運行的任何應用程序將在 ResourceManager 下次啟動時恢復,備註:如果啟用 RM-HA,則始終啟用該配置。
    NodeManager Java Heap Size (堆棧大小) 4G
    NodeManager yarn.nodemanager.resource.memory-mb 40G 可分配給容器的物理內存數量,參照資源池內存90%左右
    NodeManager yarn.nodemanager.resource.cpu-vcores 32 可以為容器分配的虛擬 CPU 內核的數量,參照資源池內存90%左右
    ApplicationMaster yarn.app.mapreduce.am.command-opts 右紅 傳遞到 MapReduce ApplicationMaster 的 Java 命令行參數 “-Djava.net.preferIPv4Stack=true
    ApplicationMaster yarn.app.mapreduce.am.resource.mb (ApplicationMaster 內存) 4G
    JobHistory Java Heap Size (堆棧大小) 2G
    MapReduce mapreduce.map.memory.mb (Map 任務內存) 4G 一個MapTask可使用的資源上限。如果MapTask實際使用的資源量超過該值,則會被強制殺死。
    MapReduce mapreduce.reduce.memory.mb (Reduce 任務內存) 8G 一個 ReduceTask 可使用的資源上限。如果 ReduceTask 實際使用的資源量超過該值,則會被強制殺死
    MapReduce mapreduce.map.cpu.vcores 2 每個 MapTask 可使用的最多 cpu core 數目
    MapReduce mapreduce.reduce.cpu.vcores 4 每個 ReduceTask 可使用的最多 cpu core 數目
    MapReduce mapreduce.reduce.shuffle.parallelcopies 20 每個 Reduce 去 Map 中取數據的并行數。
    MapReduce mapreduce.task.io.sort.mb(Shuffle 的環形緩衝區大小) 512M 當排序文件時要使用的內存緩衝總量。注意:此內存由 JVM 堆棧大小產生(也就是:總用戶 JVM 堆棧 – 這些內存 = 總用戶可用堆棧空間)
    MapReduce mapreduce.map.sort.spill.percent 80% 環形緩衝區溢出的閾值
    MapReduce mapreduce.task.timeout 10分鐘 Task 超時時間,經常需要設置的一個參數,該參數表 達的意思為:如果一個 Task 在一定時間內沒有任何進 入,即不會讀取新的數據,也沒有輸出數據,則認為 該 Task 處於 Block 狀態,可能是卡住了,也許永遠會 卡住,為了防止因為用戶程序永遠 Block 住不退出, 則強制設置了一個該超時時間。如果你的程序對每條輸入數據的處理時間過長(比如會訪問數據庫,通過網絡拉取數據等),建議將該參數調大,該參數過小常出現的錯誤提示是 :AttemptID:attempt_12267239451721_123456_m_00 0335_0 Timed out after 600 secsContainer killed by the ApplicationMaster。

    5 優化:Impala

    服務 選項 配置值 參數說明
    Impala Daemon mem_limit (內存限制) 50G 由守護程序本身強制執行的 Impala Daemon 的內存限制。
    如果達到該限制,Impalad Daemon 上運行的查詢可能會被停止
    Impala Daemon Impala Daemon JVM Heap 512M 守護進程堆棧大小
    Impala Daemon scratch_dirs 節點上多塊獨立磁盤(目錄) Impala Daemon 將溢出信息等數據寫入磁盤以釋放內存所在的目錄。這可能是大量數據
    Impala Catalog Server Java Heap Size 8G 堆棧大小

    6 優化:Kafka

    6.1 官方壓測

    6.1.1 Kafka Producer 壓力測試

    • record-size 是一條信息有多大,單位是字節。
    • num-records 是總共發送多少條信息。
    • throughput 是每秒多少條信息,設成-1,表示不限流,可測出生產者最大吞吐量。
    bash /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/lib/kafka//bin/kafka-producer-perf-test.sh --topic test --record-size 100 --num-records 100000 --throughput -1 --producer-props bootstrap.servers=cdh01.cm:9092,cdh02.cm:9092,cdh03.cm:9092
    

    100000 records sent, 225733.634312 records/sec (21.53 MB/sec),

    8.20 ms avg latency, 66.00 ms max latency,

    3 ms 50th, 28 ms 95th, 30 ms 99th, 30 ms 99.9th.

    參數解析:一共寫入 10w 條消息,吞吐量為 21.53 MB/sec,每次寫入的平均延遲

    為 8.20 毫秒,最大的延遲為 66.00 毫秒。

    6.1.2 Kafka Consumer 壓力測試

    • zookeeper 指定 zookeeper 的鏈接信息
    • topic 指定 topic 的名稱
    • fetch-size 指定每次 fetch 的數據的大小
    • messages 總共要消費的消息個數
    bash /opt/cloudera/parcels/CDH-6.2.0-1.cdh6.2.0.p0.967373/lib/kafka//bin/kafka-consumer-perf-test.sh --broker-list cdh01.cm:9092,cdh02.cm:9092,cdh03.cm:9092 --topic test --fetch-size 10000 --messages 10000000 --threads 1
    

    start.time, end.time, data.consumed.in.MB, MB.sec, data.consumed.in.nMsg, nMsg.sec, rebalance.time.ms, fetch.time.ms, fetch.MB.sec, fetch.nMsg.sec

    2020-06-11 17:53:48:179, 2020-06-11 17:54:04:525, 57.2205, 3.5006, 600000, 36706.2278, 3051, 13295, 4.3039, 45129.7480

    start.time:2020-06-11 17:53:48:179 開始時間

    end.time:2020-06-11 17:54:04:525 結束時間(用時16秒)

    data.consumed.in.MB:57.2205 消費57M數據

    MB.sec:3.5006 3.5M/S

    data.consumed.in.nMsg:600000 消費60萬消息

    nMsg.sec:36706.2278 36706條消息/S

    rebalance.time.ms:3051 平衡時間3S

    fetch.time.ms:13295 抓取時間13S

    fetch.MB.sec:4.3039 一秒抓取4.3M

    fetch.nMsg.sec:45129.7480 一秒抓取45129條消息

    開始測試時間,測試結束數據,共消費數據57.2205MB,吞吐量 3.5M/S,共消費600000條,平均每秒消費36706.2278條。

    6.1.3 Kafka 機器數量計算

    Kafka 機器數量(經驗公式)= 2 X(峰值生產速度 X 副本數 /100)+ 1

    先拿到峰值生產速度,再根據設定的副本數,就能預估出需要部署 Kafka 的數量。

    比如我們的峰值生產速度是 50M/s。副本數為 2。

    Kafka 機器數量 = 2 X( 50 X 2 / 100 )+ 1 = 3 台

    6.2 參數調優

    服務 選項 配置值 參數說明
    Kafka Broker Java Heap Size of Broker 2G Broker堆棧大小
    Kafka Broker Data Directories 多塊獨立磁盤
    Kafka 服務 Maximum Message Size 10M 服務器可以接收的消息的最大大小。此屬性必須與使用者使用的最大提取大小同步。否則,不守規矩的生產者可能會發布太大而無法消費的消息
    Kafka 服務 Replica Maximum Fetch Size 20M 副本發送給leader的獲取請求中每個分區要獲取的最大字節數。此值應大於message.max.bytes。
    Kafka 服務 Number of Replica Fetchers 6 用於複製來自領導者的消息的線程數。增大此值將增加跟隨者代理中I / O并行度。

    7 優化:HBase

    服務 選項 配置值 參數說明
    HBase Java Heap Size 18G 客戶端 Java 堆大小(字節)主要作用來緩存Table數據,但是flush時會GC,不要太大,根據集群資源,一般分配整個Hbase集群內存的70%,16->48G就可以了
    HBase hbase.client.write.buffer 512M 寫入緩衝區大小,調高該值,可以減少RPC調用次數,單數會消耗更多內存,較大緩衝區需要客戶端和服務器中有較大內存,因為服務器將實例化已通過的寫入緩衝區並進行處理,這會降低遠程過程調用 (RPC) 的數量。
    HBase Master Java Heap Size 8G HBase Master 的 Java 堆棧大小
    HBase Master hbase.master.handler.count 300 HBase Master 中啟動的 RPC 服務器實例數量。
    HBase RegionServer Java Heap Size 31G HBase RegionServer 的 Java 堆棧大小
    HBase RegionServer hbase.regionserver.handler.count 100 RegionServer 中啟動的 RPC 服務器實例數量,根據集群情況,可以適當增加該值,主要決定是客戶端的請求數
    HBase RegionServer hbase.regionserver.metahandler.count 60 用於處理 RegionServer 中的優先級請求的處理程序的數量
    HBase RegionServer zookeeper.session.timeout 180000ms ZooKeeper 會話延遲(以毫秒為單位)。HBase 將此作為建議的最長會話時間傳遞給 ZooKeeper 仲裁
    HBase RegionServer hbase.hregion.memstore.flush.size 1G 如 memstore 大小超過此值,Memstore 將刷新到磁盤。通過運行由 hbase.server.thread.wakefrequency 指定的頻率的線程檢查此值。
    HBase RegionServer hbase.hregion.majorcompaction 0 合併周期,在合格節點下,Region下所有的HFile會進行合併,非常消耗資源,在空閑時手動觸發
    HBase RegionServer hbase.hregion.majorcompaction.jitter 0 抖動比率,根據上面的合併周期,有一個抖動比率,也不靠譜,還是手動好
    HBase RegionServer hbase.hstore.compactionThreshold 6 如在任意一個 HStore 中有超過此數量的 HStoreFiles,則將運行壓縮以將所有 HStoreFiles 文件作為一個 HStoreFile 重新寫入。(每次 memstore 刷新寫入一個 HStoreFile)您可通過指定更大數量延長壓縮,但壓縮將運行更長時間。在壓縮期間,更新無法刷新到磁盤。長時間壓縮需要足夠的內存,以在壓縮的持續時間內記錄所有更新。如太大,壓縮期間客戶端會超時。
    HBase RegionServer hbase.client.scanner.caching 1000 內存未提供數據的情況下掃描儀下次調用時所提取的行數。較高緩存值需啟用較快速度的掃描儀,但這需要更多的內存且當緩存為空時某些下一次調用會運行較長時間
    HBase RegionServer hbase.hregion.max.filesize 50G HStoreFile 最大大小。如果列組的任意一個 HStoreFile 超過此值,則託管 HRegion 將分割成兩個
    HBase Master hbase.master.logcleaner.plugins 日誌清除器插件 org.apache.hadoop.hbase.master.cleaner.TimeToLiveLogCleaner
    HBase hbase.replication false 禁用複製
    HBase hbase.master.logcleaner.ttl 10min 保留 HLogs 的最長時間,加上如上兩條解決oldWALs增長問題

    8 優化:Hive

    服務 選項 配置值 參數說明
    HiveServer2 Java Heap Size 4G
    Hive MetaStore Java Heap Size 8G
    Hive Gateway Java Heap Size 2G
    Hive hive.execution.engine Spark 執行引擎切換
    Hive hive.fetch.task.conversion more Fetch抓取修改為more,可以使全局查找,字段查找,limit查找等都不走計算引擎,而是直接讀取表對應儲存目錄下的文件,大大普通查詢速度
    Hive hive.exec.mode.local.auto(hive-site.xml 服務高級配置,客戶端高級配置) true 開啟本地模式,在單台機器上處理所有的任務,對於小的數據集,執行時間可以明顯被縮短
    Hive hive.exec.mode.local.auto.inputbytes.max(hive-site.xml 服務高級配置,客戶端高級配置) 50000000 文件不超過50M
    Hive hive.exec.mode.local.auto.input.files.max(hive-site.xml 服務高級配置,客戶端高級配置) 10 個數不超過10個
    Hive hive.auto.convert.join 開啟 在join問題上,讓小表放在左邊 去左鏈接(left join)大表,這樣可以有效的減少內存溢出錯誤發生的幾率
    Hive hive.mapjoin.smalltable.filesize(hive-site.xml 服務高級配置,客戶端高級配置) 50000000 50M以下認為是小表
    Hive hive.map.aggr 開啟 默認情況下map階段同一個key發送給一個reduce,當一個key數據過大時就發生數據傾斜。
    Hive hive.groupby.mapaggr.checkinterval(hive-site.xml 服務高級配置,客戶端高級配置) 200000 在map端進行聚合操作的條目數目
    Hive hive.groupby.skewindata(hive-site.xml 服務高級配置,客戶端高級配置) true 有數據傾斜時進行負載均衡,生成的查詢計劃會有兩個MR Job,第一個MR Job會將key加隨機數均勻的分佈到Reduce中,做部分聚合操作(預處理),第二個MR Job在根據預處理結果還原原始key,按照Group By Key分佈到Reduce中進行聚合運算,完成最終操作
    Hive hive.exec.parallel(hive-site.xml 服務高級配置,客戶端高級配置) true 開啟并行計算
    Hive hive.exec.parallel.thread.number(hive-site.xml 服務高級配置,客戶端高級配置) 16G 同一個sql允許的最大并行度,針對集群資源適當增加

    9 優化:Oozie、Hue

    服務 選項 配置值 參數說明
    Oozie Java Heap Size 1G 堆棧大小
    Hue Java Heap Size 4G 堆棧大小

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

    【其他文章推薦】

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

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

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

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

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

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

  • Android官方新推的DI庫 Hilt

    Android官方新推的DI庫 Hilt

    Hilt是Google Android官方新推薦的依賴注入工具.
    已加入到官方文檔: Dependency injection with Hilt. 目前是alpha release階段.

    Hilt是在Dagger之上, Hilt單詞的意思是: 刀把, 柄.
    代碼庫還是這個google/dagger.

    Hilt的出現, 讓我想起了曾經曇花一現的dagger.android, 不知道hilt能不能經得住時間的考驗.

    本文介紹Hilt的基本使用. 熟悉dagger的朋友可能會發現很容易.

    Hilt是花里胡哨的小打小鬧? 還是下一個主流工具? 讓我們拭目以待.

    Codelab練習例子

    • Codelab: Using Hilt in your Android app.

    我的Fork: https://github.com/mengdd/android-hilt.

    這個項目最開始是一個用ServiceLocator手動注入的例子, 功能是記錄用戶點擊button操作, 显示log. 有兩個Fragment和Activity.

    通過Codelab中一系列步驟的改造, 最終改成用Hilt做依賴注入.

    本文舉例就用其中的代碼片段了.

    原repo還貼心地附上了改造前後的對比diff: Full codelab comparison.

    Hilt依賴添加

    project root: build.gradle:

    buildscript {
        ext.hilt_version = '2.28-alpha'
        ...
        dependencies {
            ...
            classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
        }
    }
    

    需要Android 4.0以上.

    app/build.gradle:

    apply plugin: 'kotlin-kapt'
    apply plugin: 'dagger.hilt.android.plugin'
    
    android {
        ...
    }
    
    dependencies {
        // Hilt dependencies
        implementation "com.google.dagger:hilt-android:$hilt_version"
        kapt "com.google.dagger:hilt-android-compiler:$hilt_version"
    
        // Hilt testing dependency
        androidTestImplementation "com.google.dagger:hilt-android-testing:$hilt_version"
        // Make Hilt generate code in the androidTest folder
        kaptAndroidTest "com.google.dagger:hilt-android-compiler:$hilt_version"
    }
    

    後面兩個是測試的依賴.

    概念, 用詞解釋

    • Container, 對應Component.
    • Bindings, 對應依賴.
    The information Hilt has about how to provide instances of different types are also called bindings.
    
    • Component hierarchy是指依賴在當前component中可以用, 也可以在它包含的child component中用. 舉例: application容器中的依賴activity可以用, 但是反過來不行.

    Hilt基本使用

    • @HiltAndroidApp: 標記在Application類上, 觸發代碼生成, application container是整個app的parent container.
    @HiltAndroidApp
    class LogApplication : Application()
    
    • @AndroidEntryPoint: 標記在Activity和Fragment上. 創建了一個和當前Activity/fragment生命周期相關的container. 目前支持的類型是: Activity, Fragment, View, Service, BroadcastReceiver.
    @AndroidEntryPoint
    class ExampleActivity : AppCompatActivity() { ... }
    

    每次Fragment(或Activity等)創建都會有它對應的container實例.

    • 字段注入: 用@Inject標記字段, 注意字段不能是private的.
    @AndroidEntryPoint
    class LogsFragment : Fragment() {
    
        @Inject
        lateinit var dateFormatter: DateFormatter
        ...
    }
    


    Component lifetimes可以看到每種類型的component創建和注入的時間.

    Activity是onCreate(), Fragment是onAttach().

    對Fragment來說, 在onAttach()的時候完成了對象的注入, 之後訪問對象都沒有問題.

    • 提供依賴: 用@Inject標記構造器.
    class DateFormatter @Inject constructor() {
        ...
    }
    

    它的依賴可以在構造參數中標明.

    Hilt的Scope支持

    默認所有的依賴都是沒有scope的, 每次注入依賴都創建新的實例.
    Hilt自動在對應的生命周期創建/銷毀對象: Component lifetimes.

    也可以把依賴scope到某個component, 這樣在這個component內, 依賴就是單例.

    • scope: @Singleton: application container的scope, 說明是application範圍內的單例. @ActivityScoped對應activity component.

    所有可用的scope: Component scopes.

    module

    module的使用基本和dagger一樣, 用來提供一些無法用構造@Inject的依賴, 比如接口, 第三方庫類型, Builder模式構造的對象等.

    • @Module: 標記這是一個module. 在Kotlin代碼中, module可以是一個object.
    • @Provides: 標記方法, 提供返回值類型的依賴.
    • @Binds: 標記抽象方法, 返回接口類型, 實現是方法的唯一參數.

    Hilt多了一個註解:

    • @InstallIn: 指明module對應哪個container, 也即Component.
      Generated components for Android classes
    @InstallIn(ApplicationComponent::class)
    @Module
    object DatabaseModule {
    
        @Provides
        @Singleton
        fun provideDatabase(@ApplicationContext appContext: Context): AppDatabase {
            return Room.databaseBuilder(
                appContext,
                AppDatabase::class.java,
                "logging.db"
            ).build()
        }
    
        @Provides
        fun provideLogDao(database: AppDatabase): LogDao {
            return database.logDao()
        }
    }
    

    module的拆分

    module的名字最好能傳達它提供了什麼類型的依賴, 所以多種依賴拆分多個modules寫比較好.

    @InstallIn(ActivityComponent::class)
    @Module
    abstract class NavigationModule {
        @Binds
        abstract fun bindNavigator(impl: AppNavigatorImpl): AppNavigator
    }
    

    @Binds@Provides不能放在同一個module里.
    因為@Binds需要module是一個abstract class, 而@Provides需要module是一個object.
    放一起會報錯: error: A @Module may not contain both non-static and abstract binding methods.

    默認依賴

    有一些默認依賴是已經有的, 比如:

    • @ApplicationContext.
    • @ActivityContext.

    可以直接作為@Provides方法或@Inject構造的參數.

    默認依賴是和component對應的.

    Component default bindings

    Qualifier

    要提供同一個接口的不同實現, 可以用不同的註解來標記. (dagger之前用的是@Named).

    A qualifier is an annotation used to identify a binding.

    舉例: LoggerDataSource接口提供了內存和數據庫兩種實現.

    定義兩個註解:

    @Qualifier
    annotation class InMemoryLogger
    
    @Qualifier
    annotation class DatabaseLogger
    

    module中提供的時候用來標記相應的依賴:

    @InstallIn(ApplicationComponent::class)
    @Module
    abstract class LoggingDatabaseModule {
    
        @DatabaseLogger
        @Singleton
        @Binds
        abstract fun bindDatabaseLogger(impl: LoggerLocalDataSource): LoggerDataSource
    }
    
    @InstallIn(ActivityComponent::class)
    @Module
    abstract class LoggingInMemoryModule {
    
        @InMemoryLogger
        @ActivityScoped
        @Binds
        abstract fun bindInMemoryLogger(impl: LoggerInMemoryDataSource): LoggerDataSource
    }
    

    這裏用了兩個module因為它們對應兩個不同的component, 一個是application一個是activity, 依賴也是相應的scope.

    注入的時候也對應加上:

    @InMemoryLogger
    @Inject
    lateinit var logger: LoggerDataSource
    

    @EntryPoint

    Hilt支持最常用的Android組件, 對於默認不支持的類型, 如果還想做字段注入, 需要用@EntryPoint.

    注意這裏只是限制了字段注入的情況, 自定義類型一般用構造注入比較方便(如果能用的話).

    @EntryPoint的意思就是一個邊界點, 這裏可以通往Hilt的世界, 得到容器提供的依賴對象們.

    Codelab中的例子是一個ContentProvider.

    關鍵的部分是這段代碼:

    @InstallIn(ApplicationComponent::class)
    @EntryPoint
    interface LogsContentProviderEntryPoint {
        fun logDao(): LogDao
    }
    
    private fun getLogDao(appContext: Context): LogDao {
        val hiltEntryPoint = EntryPointAccessors.fromApplication(
            appContext,
            LogsContentProviderEntryPoint::class.java
        )
        return hiltEntryPoint.logDao()
    }
    

    Hilt and Dagger

    當初的dagger.android已被放棄, 它是為了簡化dagger在Android上的使用而單獨推出的. (根據Activity和Fragment的生命周期, 開發者不用手動調用inject方法, 但是確實最開始的setup code比較多.)

    Hilt相對於Dagger來說, 簡化了幾個點:

    • 不用自己創建Component.
    • 不用手動調用inject()方法來完成字段注入.
    • 不用自己在Application中保存component.
    • 提供了一些Scope, 不用自己定義和寫.
    • 提供了一些默認依賴, 比如Context.

    總體來說Hilt就是針對Android做的定製, 讓依賴注入框架用起來更方便. 畢竟dagger是一個java注入庫, 它的應用範圍不限於Android.

    因為Hilt和Dagger可以共存, 可以逐步遷移. 既然官方推薦了, 可以在項目內小範圍地先試試.

    最後推薦這個cheat sheet

    Reference

    • Dependency Injection on Android with Hilt
    • Dependency injection with Hilt
    • Dagger Hilt

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

    【其他文章推薦】

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

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

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

    南投搬家公司費用需注意的眉眉角角,別等搬了再說!

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

  • .net core3.1 abp動態菜單和動態權限(思路) (二)

    .net core3.1 abp動態菜單和動態權限(思路) (二)

    ps:本文需要先把abp的源碼下載一份來下,跟着一起找實現,更容易懂

    在abp中,對於權限和菜單使用靜態來管理,菜單的加載是在登陸頁面的地方(具體是怎麼知道的,瀏覽器按F12,然後去sources中去找)

    這個/AbpScripts/GetScripts是獲取需要初始化的script,源自AbpScriptsController,GetScripts方法包括

    頁面加載時的鏈接是:http://localhost:62114/AbpScripts/GetScripts?v=637274153555501055

    _multiTenancyScriptManager //當前租戶初始化 對應報文的 abp.multiTenancy

    _sessionScriptManager //當前session初始化 對應報文的 abp.session
    _localizationScriptManager  //本地化的初始化 對應報文的 abp.localization
    _featuresScriptManager  //對應報文的 abp.features
    _authorizationScriptManager  //權限初始化  對應報文的 abp.auth
    _navigationScriptManager  //導航菜單初始化  對應報文的 abp.nav
    _settingScriptManager  //設置初始化  對應報文的 abp.setting
    _timingScriptManager  //對應報文的 abp.clock
    _customConfigScriptManager  //對應報文的 abp.custom

     

     

     

     

     好了,現在基本算是找到菜單和權限js獲取的地方了,一般系統裏面,權限是依賴於菜單和菜單按鈕的,所以我們先不管權限,先把菜單做成動態加載的

    從await _navigationScriptManager.GetScriptAsync()開始,一路F12,大概流程是

    (接口)INavigationScriptManager=>(接口實現)NavigationScriptManager=>(方法)GetScriptAsync=>(調用)await _userNavigationManager.GetMenusAsync=>
    (接口)IUserNavigationManager=>(接口實現)UserNavigationManager=>(方法)GetMenuAsync=>(調用)navigationManager.Menus=>
    (接口)INavigationManager=>(接口實現)NavigationManager=>(非靜態構造函數為Menus屬性賦值)NavigationManager

     到這裏之後基本就到底了,我們看看NavigationManager的內容

        internal class NavigationManager : INavigationManager, ISingletonDependency
        {
            public IDictionary<string, MenuDefinition> Menus { get; private set; }  //屬性
    
            public MenuDefinition MainMenu //屬性
            {
                get { return Menus["MainMenu"]; }
            }
    
            private readonly IIocResolver _iocResolver;  
            private readonly INavigationConfiguration _configuration;
    
            public NavigationManager(IIocResolver iocResolver, INavigationConfiguration configuration) //非靜態構造函數
            {
                _iocResolver = iocResolver;
                _configuration = configuration;
    
                Menus = new Dictionary<string, MenuDefinition>
                        {
                            {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))}
                        };
            }
    
            public void Initialize()  //初始化方法
            {
                var context = new NavigationProviderContext(this);
    
                foreach (var providerType in _configuration.Providers)
                {
                    using (var provider = _iocResolver.ResolveAsDisposable<NavigationProvider>(providerType))
                    {
                        provider.Object.SetNavigation(context);  //中式英語翻譯一下,應該是設置導航
                    }
                }
            }
        }

    這個類裏面就只有屬性、需要注入的接口聲明、非靜態構造函數、初始化方法,我們到這裏需要關注的是Menus這個屬性,這個屬性似乎將會包含我們需要生成的菜單內容

    Menus = new Dictionary<string, MenuDefinition>
                        {
                            {"MainMenu", new MenuDefinition("MainMenu", new LocalizableString("MainMenu", AbpConsts.LocalizationSourceName))}
                        };

    這裡是對Menus的賦值,實例化了一個Dictionary,前面的不用看,主要是看標紅的這句話,從new LocalizableString(“MainMenu”, AbpConsts.LocalizationSourceName)裏面獲取到值

    好了現在基本找到地方了,我們不知道LocalizableString是什麼意思,但是我們可以百度一波

    ILocalizableString/LocalizableString:封裝需要被本地化的string的信息,並提供Localize方法(調用ILocalizationManager的GetString方法)返回本地化的string. SourceName指定其從那個本地化資源讀取本地化文本。

      LocalizableString(“Questions”, “”) 如果本地找不到資源,會報300

    大概的意思是通過new LocalizableString,我們可以在本地化來源為AbpConsts.LocalizationSourceName的string裏面尋找到Key為MainMenu的value(理解不對請噴)

     

    現在需要去找到那個地方對MainMenu進行了本地化操作,一般來說這個事情都是在程序加載的時候進行的,先對MainMenu進行讀取,保存到本地,然後在_navigationScriptManager讀取,傳輸給前台

    似乎不好找了,但是我們發現有一個類型MenuDefinition,F12一下,可以發現寶藏

    namespace Abp.Application.Navigation
    {
        /// <summary>
        /// Represents a navigation menu for an application.  //表示應用程序的導航菜單
    /// </summary>
        public class MenuDefinition : IHasMenuItemDefinitions
        {
            /// <summary>
            /// Unique name of the menu in the application. Required.  //應用程序中菜單的唯一名稱。 必須
            /// </summary>
            public string Name { get; private set; }
    
            /// <summary>
            /// Display name of the menu. Required.  //菜單显示名稱 必須
    /// </summary>
            public ILocalizableString DisplayName { get; set; }
    
            /// <summary>
            /// Can be used to store a custom object related to this menu. Optional.  //可用於存儲與此菜單相關的自定義對象
    /// </summary>
            public object CustomData { get; set; }
    
            /// <summary>
            /// Menu items (first level).   //菜單項(第一級)
    /// </summary>
            public List<MenuItemDefinition> Items { get; set; }
    
            /// <summary>
            /// Creates a new <see cref="MenuDefinition"/> object.
            /// </summary>
            /// <param name="name">Unique name of the menu</param>
            /// <param name="displayName">Display name of the menu</param>
            /// <param name="customData">Can be used to store a custom object related to this menu.</param>
            public MenuDefinition(string name, ILocalizableString displayName, object customData = null)
            {
                if (string.IsNullOrEmpty(name))
                {
                    throw new ArgumentNullException("name", "Menu name can not be empty or null.");
                }
    
                if (displayName == null)
                {
                    throw new ArgumentNullException("displayName", "Display name of the menu can not be null.");
                }
    
                Name = name;
                DisplayName = displayName;
                CustomData = customData;
    
                Items = new List<MenuItemDefinition>();
            }
    
            /// <summary>
            /// Adds a <see cref="MenuItemDefinition"/> to <see cref="Items"/>.
            /// </summary>
            /// <param name="menuItem"><see cref="MenuItemDefinition"/> to be added</param>
            /// <returns>This <see cref="MenuDefinition"/> object</returns>
            public MenuDefinition AddItem(MenuItemDefinition menuItem)
            {
                Items.Add(menuItem);
                return this;
            }
    
            /// <summary>
            /// Remove menu item with given name
            /// </summary>
            /// <param name="name"></param>
            public void RemoveItem(string name)
            {
                Items.RemoveAll(m => m.Name == name);
            }
        }
    }

    找到了菜單的類型了,那麼我們去找保存的地方就好找了,我們其實可以根據AddItem這個方法去找,去查看哪個地方引用了

    AddItem方法添加的是MenuItemDefinition類型的變量,那我們現在退出abp源碼,去我們的AbpLearn項目中去全局搜索一下

     

     

    看來是同一個AbpLearnNavigationProvider類裏面,雙擊過去看一下

     

        /// <summary>
        /// This class defines menus for the application.
        /// </summary>
        public class AbpLearnNavigationProvider : NavigationProvider
        {
            public override void SetNavigation(INavigationProviderContext context)
            {
                context.Manager.MainMenu
                    .AddItem(
                        new MenuItemDefinition(
                            PageNames.Home,
                            L("HomePage"),
                            url: "",
                            icon: "fas fa-home",
                            requiresAuthentication: true
                        )
                    ).AddItem(
                        new MenuItemDefinition(
                            PageNames.Tenants,
                            L("Tenants"),
                            url: "Tenants",
                            icon: "fas fa-building",
                            permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Tenants)
                        )
                    ).AddItem(
                        new MenuItemDefinition(
                            PageNames.Users,
                            L("Users"),
                            url: "Users",
                            icon: "fas fa-users",
                            permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Users)
                        )
                    ).AddItem(
                        new MenuItemDefinition(
                            PageNames.Roles,
                            L("Roles"),
                            url: "Roles",
                            icon: "fas fa-theater-masks",
                            permissionDependency: new SimplePermissionDependency(PermissionNames.Pages_Roles)
                                )
                    )
                    .AddItem(
                        new MenuItemDefinition(
                            PageNames.About,
                            L("About"),
                            url: "About",
                            icon: "fas fa-info-circle"
                        )
                    ).AddItem( // Menu items below is just for demonstration!
                        new MenuItemDefinition(
                            "MultiLevelMenu",
                            L("MultiLevelMenu"),
                            icon: "fas fa-circle"
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetBoilerplate",
                                new FixedLocalizableString("ASP.NET Boilerplate"),
                                icon: "far fa-circle"
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetBoilerplateHome",
                                    new FixedLocalizableString("Home"),
                                    url: "https://aspnetboilerplate.com?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetBoilerplateTemplates",
                                    new FixedLocalizableString("Templates"),
                                    url: "https://aspnetboilerplate.com/Templates?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetBoilerplateSamples",
                                    new FixedLocalizableString("Samples"),
                                    url: "https://aspnetboilerplate.com/Samples?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetBoilerplateDocuments",
                                    new FixedLocalizableString("Documents"),
                                    url: "https://aspnetboilerplate.com/Pages/Documents?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            )
                        ).AddItem(
                            new MenuItemDefinition(
                                "AspNetZero",
                                new FixedLocalizableString("ASP.NET Zero"),
                                icon: "far fa-circle"
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetZeroHome",
                                    new FixedLocalizableString("Home"),
                                    url: "https://aspnetzero.com?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetZeroFeatures",
                                    new FixedLocalizableString("Features"),
                                    url: "https://aspnetzero.com/Features?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetZeroPricing",
                                    new FixedLocalizableString("Pricing"),
                                    url: "https://aspnetzero.com/Pricing?ref=abptmpl#pricing",
                                    icon: "far fa-dot-circle"
                                )
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetZeroFaq",
                                    new FixedLocalizableString("Faq"),
                                    url: "https://aspnetzero.com/Faq?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            ).AddItem(
                                new MenuItemDefinition(
                                    "AspNetZeroDocuments",
                                    new FixedLocalizableString("Documents"),
                                    url: "https://aspnetzero.com/Documents?ref=abptmpl",
                                    icon: "far fa-dot-circle"
                                )
                            )
                        )
                    );
            }
    
            private static ILocalizableString L(string name)
            {
                return new LocalizableString(name, AbpLearnConsts.LocalizationSourceName);
            }
        }

    好了,現在我們找到菜單定義的地方了,那麼我們如何去做動態菜單哪?

     

    首先我們想一下需要什麼樣的動態菜單?

    1.從數據庫加載,不從數據庫加載怎麼叫動態

    2.可以根據不同Host(管理者)和Tenant(租戶)加載不同的菜單,不可能管理者和租戶看到的菜單全是一個樣子的吧!

    3.可以根據不同的角色或者用戶加載不同的菜單(這個就牽扯到權限了,比如誰可以看到什麼,不可以看到什麼)

    4.權限、按鈕最好和菜單相綁定,這樣便於控制

    ……

     

    根據以上幾點,我們可以確定

    1.必須要在用戶登錄之後加載出來的菜單才能符合條件

    2.菜單需要建一個表(因為abp默認沒有單獨的菜單表),來進行存放

    3.字段需要包含:菜單名,菜單與權限對應的名稱(用於動態權限),菜單對應的Url,Icon,級聯父Id,是否啟用,排序,租戶Id

    4.需要對菜單進行編輯時,因為牽扯到多租戶,我們需要對多租戶定義一個標準的菜單,在添加租戶時,自動將標準菜單複製保存一份到新租戶中,所以我們需要對於菜單的進行區分,一般來說Host對應的數據行TenantId(int)都為null,我們可以將標準菜單的TenantId標為-1,已經分配保存的菜單TenantId為當前租戶Id,這樣便於區分和查詢

     

    好了,讓我們開始寫動態菜單吧

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

    【其他文章推薦】

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

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

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

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

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

  • Redis的內存和實現機制

    Redis的內存和實現機制

    1. Reids內存的劃分

    1. 數據 內存統計在used_memory中
    2. 進程本身運行需要內存 Redis主進程本身運行需要的內存佔用,代碼、常量池等
    3. 緩衝內存,客戶端緩衝區、複製積壓緩衝區、AOF緩衝區。有jemalloc分配內存,會統計在used_memory中
    4. 內存碎片 Redis在分配、回收物理內存過程中產生的。內存碎片不會統計在used_memory中。如果Redis服務器中的內存碎片已經很大,可以通過安全重啟的方式減小內存碎片:因為重啟之後,Redis重新從備份文件中讀取數據,在內存中進行重排,為每個數據重新選擇合適的內存單元,減小內存碎片。

    2. Redis的數據存儲的細節

    涉及到內存分配器jemalloc, 簡單動態字符串(SDS),5種值類型對象的內部編碼,redisObject,

    1. DictEntry: Redis 是key-value數據庫,因此對每個鍵值對都會有一個dictEntry,裏面存儲了指向Key和Value的指針;next指向下一個dictEntry,與本Key-Value無關
    2. Key: 並不是以字符串存儲,而是存儲在SDS結構中
    3. RedisObject: 5種值對象不是直接以對應的類型存儲的,而是被封裝為redisObject來存儲
    4. jemalloc: 無論是DictEntry對象,還是redisObject, SDS對象,都需要內存分配器

    2.1 Jemalloc

    redis 在編譯時便會指定內存分配器, 內存分配器可以是libc、jemalloc、tcmalloc

    jemalloc作為Redis的默認內存分配器,在減小內存碎片方面做的相對比較好。jemalloc在64位系統中,將內存空間劃分為小、大、巨大三個範圍;每個範圍內又劃分了許多小的內存塊單位;當Redis存儲數據時,會選擇大小最合適的內存塊進行存儲。

    2.2 RedisObject

    redis對象的類型,內部編碼,內存回收,共享對象等功能都需要RedisObject的支持

    typedef struct redisObject{
        unsigned type: 4;
        unsigned encoding: 4;
        unsigned lru: REDIS_LRU_BITS; /*lru time*/
        int refcount;
        void *ptr;
    } robj;
    
    • type 字段 佔4bit 目前有5中類型, REDIS_STRING, REDIS_LIST, REDIS_HASH, REDIS_SET, REDIS_ZSET。 當執行type命令時,便是通過讀取redisObject對象的type字段獲取對象類型

    • encoding 佔4bit (表示對象的內部編碼),對於redis支持的每種類型,都有至少兩種內部編碼。通過object encoding命令,可以查看對象採用的編碼方式

      • 對於字符串,有int, embstr, raw 三種編碼。
      • 對於列表, 有壓縮列表和雙端列表兩種編碼方式,如果列表中元素較少,redis傾向於使用壓縮列表進行存儲,因為壓縮列表內存佔用少,而且比雙端鏈表可以更快載入;當列表對象元素較多時,壓縮列表就會轉化為更適合存儲大量元素的雙端鏈表。
    • lru 不同版本佔用內存大小不一樣,4.0版本佔用24bit,2.6版本佔用22bit

      • 記錄的是對象最後一次被命令程序訪問的時間,通過對比lru時間和當前時間,可以計算某個對象的空轉時間,object idletime命令可以显示該空轉時間 秒級別,改命令並不會改變對象的lru值,lru值除了通過object idletime命令打印之外,還與Redis的內存回收有關係:如果Redis打開了maxmemory選項,且內存回收算法選擇的是volatile-lru或allkeys—lru,那麼當Redis內存佔用超過maxmemory指定的值時,Redis會優先選擇空轉時間最長的對象進行釋放。
    • refcount 共享對象 記錄對象的引用計數,協助內存回收,引用計數可以通過 object refcount命令查看

      • ​ 共享對象的具體實現
      • Redis共享對象目前只支持整數值的對象。實際上是對內存和CPU時間的衡量。共享對象雖然會降低內存消耗,但是判斷兩個對象是否相等時需要消耗時間的。,對於整數值,判斷操作複雜度為O(1);對於普通字符串,判斷複雜度為O(n);而對於哈希、列表、集合和有序集合,判斷的複雜度為O(n^2)。
      • 雖然共享對象只能是整數值的字符串對象,但是5種類型都可能使用共享對象(如哈希、列表等的元素可以使用)。reids服務器在初始化時,會創建10000個字符串對象,值分別是0-9999的整數值。10000這個数字可以通過調整參數REDIS_SHARED_INTEGERS(4.0中是OBJ_SHARED_INTEGERS)的值進行改變
    • ptr 指針指向具體的數據 如 set hello world ptr指向包含字符串world的SDS

    • RedisObject對象大小16字節 4bit+4bit+24bit+4Byte+8Byte=16Byte

    3. Redis內部數據結

    3.1 SDS 簡單動態字符串

    結構

    struct sdshdr {
    	int len;  // 記錄buf數組中已使用字節的數量 等於SDS所保存字符串的長度
        int free;  // 記錄buf數組中未使用的字節數量
        char buf[];
    };
    
    1. SDS結構 佔據的空間:free+len+buf(表示字符串結尾的空字符串), 其中buf=free+len+1. 則總長度為4+4+free+len+1=free+len+9

    2. 與C字符串的比較

      在C字符串的基礎上加入了free和len字段,優勢

      • 獲取字符串長度: SDS O(1), C字符串是O(n)
      • 緩衝區溢出:使用C字符串的API時,如果字符串長度增加(如strcat操作)而忘記重新分配內存,很容易造成緩衝區的溢出;而SDS由於記錄了長度,相應的API在可能造成緩衝區溢出時會自動重新分配內存,杜絕了緩衝區溢出。
      • 修改字符串內存的重分配:對於C字符串,如果要修改字符串,必須要重新分配內存(先釋放再申請),因為如果沒有重新分配,字符串長度增大時會造成內存溢出,字符串長度減小時會造成內存泄漏。對於SDS, 由於記錄了len和free,因此解除了字符串長度和空間數組長度之間的關聯,可以在此基礎上進行優化:空間預分配(分配內存時比實際需要的多)使得字符串長度增大時重新分配內存的概率減小。惰性空間釋放策略 惰性空間釋放用於優化 SDS 的字符串縮短操作: 當 SDS 的 API 需要縮短 SDS 保存的字符串時, 程序並不立即使用內存重分配來回收縮短后多出來的字節, 而是使用 free 屬性將這些字節的數量記錄起來, 並等待將來使用。
      • 二進制安全 C 字符串中的字符必須符合某種編碼(比如 ASCII), 並且除了字符串的末尾之外, 字符串裏面不能包含空字符, 否則最先被程序讀入的空字符將被誤認為是字符串結尾 —— 這些限制使得 C 字符串只能保存文本數據, 而不能保存像圖片、音頻、視頻、壓縮文件這樣的二進制數據。
        SDS 的 API 都是二進制安全的(binary-safe): 所有 SDS API 都會以處理二進制的方式來處理 SDS 存放在 buf 數組裡的數據, 程序不會對其中的數據做任何限制、過濾、或者假設 —— 數據在寫入時是什麼樣的, 它被讀取時就是什麼樣。

      總結:

      • Redis 的字符串表示為 sds ,而不是 C 字符串(以 \0 結尾的 char*)。

      • 對比 C 字符串,sds 有以下特性:
        – 可以高效地執行長度計算(strlen);
        – 可以高效地執行追加操作(append);
        – 二進制安全;

      • sds 會為追加 操作進行優化:加快追加操作的速度,並降低內存分配的次數,代價是多佔用了一些內存,而且這些內存不會被主動釋放。

    3.3 字典

    在Redis中的應用:

    1. 實現數據庫鍵空間(key space) Redis 是一個鍵值對數據庫,數據庫中的鍵值對就由典保存:每個數據庫都有一個與之相對應的字典,這個字典被稱之為鍵空間(key space。
    2. 用作Hash類型鍵的其中一種底層實現

    Redis 的 Hash 類型鍵使用以下兩種數據結構作為底層實現:

    1. 字典;
    2. 壓縮列表

    3.3.1 字典的底層實現

    實現字典的方法有很多種:

    • 最簡單的就是使用鏈表和數組,方式只適用於元素個數不多的情況
    • 兼顧高效和簡單性,使用哈希表
    • 追求更穩定的性能特徵,並且希望高效的實現排序操作,可以是用更為複雜的平衡樹

    Reids選擇了高效且實現簡單的哈希表作為字典的底層實現。

    /* dict.h/dict
    * 字典
    *
    * 每個字典使用兩個哈希表,用於實現漸進式 rehash
    */
    
    typedef struct dict {
        dictType *type;  // 特定於類型的處理函數
        void *privdata;  // 類型處理函數的私有數據
        dictht ht[2];   // 2個哈希表
        
        int rehashidx;  // 記錄rehash 進度的標誌, 值為-1  表示rehash未進行
        
        int iterators;   // 當前正在運作的安全迭代器數量
    } dict;
    

    注: dict類型使用了兩個指針分別指向兩個哈希表

    其中,0號哈希表(ht[0])是字典主要使用的哈希表,而 1號哈希表(ht[1])則只有對0號哈希表進行rehash時才使用。

    3.3.2 哈希表的實現

    /*哈希表*/
    typedef struct dictht {
        dictEntry **table;   // 哈希表節點指針數組(俗稱桶, bucket)
        unsigned long size;  //指針數組的大小
        unsigned long sizemask;   //指針數組的長度掩碼
        unsigned long used;   // 哈希表現有的節點數量
    }dictht;
    
    /*哈希表節點*/
    typedef struct dictEntry {
        void *key;
        union {
            void *val;
            uint64_t u64;
            int64_t s64;
        } v;
        
        // 鏈接後繼系節點
        struct dictEntry *next;
    } dictEntry;
    

    next 屬性指向另一個dictEntry結構, 多個dictEntry 可以通過next指針串連成鏈表dictht使用鏈地址法來處理鍵碰撞;當多個不同鍵擁有相同的哈希值時,哈希表用一個鏈表將這些鍵連接起來。

    3.3.3 哈希碰撞

    在哈希表實現中,當兩個不同的鍵擁有相同的哈希值時,我們稱這兩個鍵發生碰撞(collision),而哈希表實現必須想辦法對碰撞進行處理。字典哈希表所使用的碰撞解決方法被稱之為鏈地址法:這種方法使用鏈表將多個哈希值相同的節點串連在一起,從而解決衝突問題。

    假設現在有一個帶有三個節點的哈希表:

    對於一個新的鍵值對 key4 和 value4 ,如果 key4 的哈希值和 key1 的哈希值相同,那麼它們將在哈希表的 0 號索引上發生碰撞。

    3.2.4 添加新鍵值對時觸發rehash操作?

    對於使用鏈地址法來解決碰撞問題的哈希表 dictht 來說,哈希表的性能依賴於它的大小(size屬性)和它所保存的節點的數量(used 屬性)之間的比率:比率最好在1:1。

    4. 跳躍表

    跳躍表是一種隨機化數據結果,查找、添加、刪除操作都可以在對數期望時間下完成

    跳躍表目前在Redis的唯一作用就是作為有序集類型的底層數據結構之一

    Redis對跳躍表進行了修改包括:

    • score值可重複
    • 對比一個元素需要同時檢查它的score和member
    • 每個節點帶有高度為1層的後退指針,用於從表尾方向向表頭方向迭代

    Redis 為什麼用跳錶而不用平衡樹?

    4.1 skiplist與平衡樹、哈希表的比較

    • skiplist和各種平衡樹(如AVL、紅黑樹等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做單個key的查找,不適宜做範圍查找。所謂範圍查找,指的是查找那些大小在指定的兩個值之間的所有節點。
    • 在做範圍查找的時候,平衡樹比skiplist操作要複雜。在平衡樹上,我們找到指定範圍的小值之後,還需要以中序遍歷的順序繼續尋找其它不超過大值的節點。如果不對平衡樹進行一定的改造,這裏的中序遍歷並不容易實現。而在skiplist上進行範圍查找就非常簡單,只需要在找到小值之後,對第1層鏈表進行若干步的遍歷就可以實現。
    • 平衡樹的插入和刪除操作可能引發子樹的調整,邏輯複雜,而skiplist的插入和刪除只需要修改相鄰節點的指針,操作簡單又快速。
    • 從內存佔用上來說,skiplist比平衡樹更靈活一些。一般來說,平衡樹每個節點包含2個指針(分別指向左右子樹),而skiplist每個節點包含的指針數目平均為1/(1-p),具體取決於參數p的大小。如果像Redis里的實現一樣,取p=1/4,那麼平均每個節點包含1.33個指針,比平衡樹更有優勢。
    • 查找單個key,skiplist和平衡樹的時間複雜度都為O(log n),大體相當;而哈希表在保持較低的哈希值衝突概率的前提下,查找時間複雜度接近O(1),性能更高一些。所以我們平常使用的各種Map或dictionary結構,大都是基於哈希表實現的。
    • 從算法實現難度上來比較,skiplist比平衡樹要簡單得多。

    Redis的對象類型和內部編碼

    1. 字符串

    1.1 內部編碼

    • int 8個字節的長整型。字符串值是整型時,這個值使用long整型表示
    • embstr <=39字節的字符串。embstr與raw都使用redisObject和sds保存數據,區別在於,embstr的使用只分配一次內存空間(因此redisObject和sds是連續的),而raw需要分配兩次內存空間(分別為redisObject和sds分配空間)。因此與raw相比,embstr的好處在於創建時少分配一次空間,刪除時少釋放一次空間,以及對象的所有數據連在一起,尋找方便。而embstr的壞處也很明顯,如果字符串的長度增加需要重新分配內存時,整個redisObject和sds都需要重新分配空間,因此redis中的embstr實現為只讀。
    • raw: 大於39個字節的字符串

    1.2 編碼轉換

    新創建的字符串默認使用 REDIS_ENCODING_RAW 編碼,在將字符串作為鍵或者值保存進數據庫時,程序會嘗試將字符串轉為 REDIS_ENCODING_INT 編碼, 字符串的長度不超過512MB

    2. 列表

    創建新列表時Redis默認使用REDIS_ENCODING_ZIPLIST編碼,當一下任意一個條件滿足時,列表會被轉換成REDIS_ENCODING_LINKEDLIST編碼:

    • 試圖往列表新添加一個字符串值,且這個字符串的長度超過sever.list_max_ziplist_value(默認值是64)
    • ziplist 包含的節點超過server.list_max_ziplist_entries(默認的值為512)

    且編碼只可能由壓縮列錶轉化為雙端鏈表,一個列表可以存儲2^32-1個元素

    2.1 壓縮列表

    壓縮列表是Redis為了節約內存而開發的,由一系列特殊編碼的連續內存塊(而不是像雙端鏈表每個節點都是指針) 順序型數據結構;與雙端鏈表相比,壓縮列表可以節省內存空間,但是進行修改或增刪操作時,複雜度較高;因此當節點數量較少時,可以使用壓縮列表;但是節點數量多時,還是使用雙端鏈表划算。因為 ziplist 節約內存的性質,它被哈希鍵、列表鍵和有序集合鍵作為初始化的底層實現來使

    2.2 雙端鏈表

    typedef struct listNode {
        struct listNode *prev;  //前驅節點
        struct listNode *next;  // 後繼節點
        void *value;
    } listNode;
    
    typedef struct list {
        //表頭指針
        listNode *head;
        //表尾指針
        listNode *tail;
        unsigned long len; // 節點長度
        void *(*dup) (void *ptr);
        void (*freee)(void *ptr);
        int (*match) (void *ptr, void *key);
    }list;
    

    小結:

    作為Reids列表的底層實現之一; 作為通用數據結構,被其他功能模塊使用。

    • 節點帶有前驅和後繼指針,訪問前驅節點和後繼節點的複雜度為 O(1) ,並且對鏈表
      的迭代可以在從表頭到表尾和從表尾到表頭兩個方向進行;
    • 鏈錶帶有指向表頭和表尾的指針,因此對錶頭和表尾進行處理的複雜度為 O(1) ;
    • 鏈錶帶有記錄節點數量的屬性,所以可以在 O(1) 複雜度內返回鏈表的節點數量(長
      度);

    3. 哈希表

    • 當哈希表使用字典編碼時,程序將哈希表的鍵(key)保存為字典的鍵,將哈希表的值(value)保存為字典的值, 字典的鍵和值都是字符串對象

    • 壓縮列表編碼的哈希表

    • 編碼轉換

      默認使用ziplist編碼,當滿足以下條件時,自動切換為字典編碼

      • 哈希表中某個鍵或某個值的長度大於sever.hash_max_ziplist_value(默認值是64)
      • ziplist 包含的節點超過server.list_max_ziplist_entries(默認的值為512)

    4. 集合

    第一個添加到集合的元素,決定了創建集合時所使用的編碼:

    • 如果第一個元素可以表示為 long long 類型值(也即是,它是一個整數),那麼集合的初始編碼為 REDIS_ENCODING_INTSET 。
    • 否則,集合的初始編碼為 REDIS_ENCODING_HT 。

    4.1 內部編碼

    當使用 REDIS_ENCODING_HT 編碼時,集合將元素保存到字典的鍵裏面,而字典的值則統一設為 NULL

    如果一個集合使用 REDIS_ENCODING_INTSET 編碼, 當滿足以下條件的時候會轉成字典編碼

    • intset保存的整數值個數超過server.set_max_intset_entries 默認值為512
    • 試圖往集合中添加一個新的元素,這個元素不能被表示為long, long類型,類型不一樣的時候使用字典

    整數集合適用於集合所有元素都是整數且集合元素數量較小的時候,與哈希表相比,整數集合的優勢在於集中存儲,節省空間;同時,雖然對於元素的操作複雜度也由O(1)變為了O(n),但由於集合數量較少,因此操作的時間並沒有明顯劣勢。

    5 .有序集合

    有序集合與集合一樣,元素都不能重複;但與集合不同的是,有序集合中的元素是有順序的。與列表使用索引下標作為排序依據不同,有序集合為每個元素設置一個分數(score)作為排序依據

    5.1 內部編碼

    • 壓縮列表

    • 跳躍表(skiplist)

      跳躍表是一種有序數據結構,通過在每個節點中維持多個指向其他節點的指針,從而達到快速訪問節點的目的。除了跳躍表,實現有序數據結構的另一種典型實現是平衡樹;大多數情況下,跳躍表的效率可以和平衡樹媲美,且跳躍表實現比平衡樹簡單很多,因此redis中選用跳躍表代替平衡樹。跳躍表支持平均O(logN)、最壞O(N)的複雜點進行節點查找,並支持順序操作。Redis的跳躍表實現由zskiplist和zskiplistNode兩個結構組成:前者用於保存跳躍表信息(如頭結點、尾節點、長度等),後者用於表示跳躍表節點

    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;
    

    5.2 編碼轉換

    對於一個 REDIS_ENCODING_ZIPLIST 編碼的有序集,只要滿足以下任一條件,就將它轉換為REDIS_ENCODING_SKIPLIST 編碼

    • ziplist所保存的元素數量超過服務器屬性server.zset_max_ziplist_entries值 默認值是128
    • 新添加元素的member的長度大於服務器屬性server.zset_max_ziplist_value 默認值是64

    優化Redis 內存佔用

    1. 利用共享對象,可以減少對象的創建(同時減少了redisObject的創建),節省內存空間。目前redis中的共享對象只包括10000個整數(0-9999);可以通過調整REDIS_SHARED_INTEGERS參數提高共享對象的個數;例如將REDIS_SHARED_INTEGERS調整到20000,則0-19999之間的對象都可以共享。

      考慮這樣一種場景:論壇網站在redis中存儲了每個帖子的瀏覽數,而這些瀏覽數絕大多數分佈在0-20000之間,這時候通過適當增大REDIS_SHARED_INTEGERS參數,便可以利用共享對象節省內存空間

    內存碎片率

    mem_fragmentation_ratio=used_memory_rss (Redis進程佔據操作系統的內存(單位是字節))/ used_memory(Redis分配器分配的內存總量(單位是字節)).

    如果內存碎片率過高(jemalloc在1.03左右比較正常),說明內存碎片多,內存浪費嚴重;這時便可以考慮重啟redis服務,在內存中對數據進行重排,減少內存碎片。

    參考博文與書籍:

    1. 《redis設計與實現》
    2. Redis內存模型
    3. Redis 基礎操作 – 時間複雜度

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

    【其他文章推薦】

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

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

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

    ※超省錢租車方案

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

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

  • 手動造輪子——基於.NetCore的RPC框架DotNetCoreRpc

    手動造輪子——基於.NetCore的RPC框架DotNetCoreRpc

    前言

        一直以來對內部服務間使用RPC的方式調用都比較贊同,因為內部間沒有這麼多限制,最簡單明了的方式就是最合適的方式。個人比較喜歡類似Dubbo的那種使用方式,採用和本地方法相同的方式,把接口層獨立出來作為服務契約,為服務端提供服務,客戶端也通過此契約調用服務。.Net平台上類似Dubbo這種相對比較完善的RPC框架還是比較少的,GRPC確實是一款非常優秀的RPC框架,能跨語言調用,但是每次還得編寫proto文件,個人感覺還是比較麻煩的。如今服務拆分,微服務架構比較盛行的潮流下,一個簡單實用的RPC框架確實可以提升很多開發效率。

    簡介

        隨着.Net Core逐漸成熟穩定,為我一直以來想實現的這個目標提供了便利的方式。於是利用閑暇時間本人手寫了一套基於Asp.Net Core的RPC框架,算是實現了一個自己的小目標。大致的實現方式,Server端依賴Asp.Net Core,採用的是中間件的方式攔截處理請求比較方便。Client端可以是任何可承載.Net Core的宿主程序。通信方式是HTTP協議,使用的是HttpClientFactory。至於為什麼使用HttpClientFactory,因為HttpClientFactory可以更輕鬆的實現服務發現,而且可以很好的集成Polly,很方便的實現,超時重試,熔斷降級這些,給開發過程中提供了很多便利。由於本人能力有限,基於這些便利,站在巨人的肩膀上,簡單的實現了一個RPC框架,項目託管在GitHub上https://github.com/softlgl/DotNetCoreRpc有興趣的可以自行查閱。

    開發環境

    • Visual Studio 2019
    • .Net Standard 2.1
    • Asp.Net Core 3.1.x

    使用方式

        打開Visual Studio先新建一個RPC契約接口層,這裏我起的名字叫IRpcService。然後新建一個Client層(可以是任何可承載.Net Core的宿主程序)叫ClientDemo,然後建立一個Server層(必須是Asp.Net Core項目)叫WebDemo,文末附本文Demo連接,建完這些之後項目結構如下:

    Client端配置

    Client端引入DotNetCoreRpc.Client包,並引入自定義的契約接口層

    <PackageReference Include="DotNetCoreRpc.Client" Version="1.0.2" />
    

    然後可以愉快的編碼了,大致編碼如下

    class Program
    {
        static void Main(string[] args)
        {
            IServiceCollection services = new ServiceCollection();
            //*註冊DotNetCoreRpcClient核心服務
            services.AddDotNetCoreRpcClient()
            //*通信是基於HTTP的,內部使用的HttpClientFactory,自行註冊即可
            .AddHttpClient("WebDemo", client => { client.BaseAddress = new Uri("http://localhost:13285/"); });
    
            IServiceProvider serviceProvider = services.BuildServiceProvider();
            //*獲取RpcClient使用這個類創建具體服務代理對象
            RpcClient rpcClient = serviceProvider.GetRequiredService<RpcClient>();
    
            //IPersonService是我引入的服務包interface,需要提供ServiceName,即AddHttpClient的名稱
            IPersonService personService = rpcClient.CreateClient<IPersonService>("WebDemo");
    
            PersonDto personDto = new PersonDto
            {
                Id = 1,
                Name = "yi念之間",
                Address = "中國",
                BirthDay = new DateTime(2000,12,12),
                IsMarried = true,
                Tel = 88888888888
            };
    
            bool addFlag = personService.Add(personDto);
            Console.WriteLine($"添加結果=[{addFlag}]");
    
            var person = personService.Get(personDto.Id);
            Console.WriteLine($"獲取person結果=[{person.ToJson()}]");
    
            var persons = personService.GetAll();
            Console.WriteLine($"獲取persons結果=[{persons.ToList().ToJson()}]");
    
            personService.Delete(person.Id);
            Console.WriteLine($"刪除完成");
    
            Console.ReadLine();
        }
    }
    

    到這裏Client端的代碼就編寫完成了

    Server端配置

    Client端引入DotNetCoreRpc.Client包,並引入自定義的契約接口層

    <PackageReference Include="DotNetCoreRpc.Server" Version="1.0.2" />
    

    然後編寫契約接口實現類,比如我的叫PersonService

    //實現契約接口IPersonService
    public class PersonService:IPersonService
    {
        private readonly ConcurrentDictionary<int, PersonDto> persons = new ConcurrentDictionary<int, PersonDto>();
        public bool Add(PersonDto person)
        {
            return persons.TryAdd(person.Id, person);
        }
    
        public void Delete(int id)
        {
            persons.Remove(id,out PersonDto person);
        }
    
        //自定義Filter
        [CacheFilter(CacheTime = 500)]
        public PersonDto Get(int id)
        {
            return persons.GetValueOrDefault(id);
        }
    
        //自定義Filter
        [CacheFilter(CacheTime = 300)]
        public IEnumerable<PersonDto> GetAll()
        {
            foreach (var item in persons)
            {
                yield return item.Value;
            }
        }
    }
    

    通過上面的代碼可以看出,我自定義了Filter,這裏的Filter並非Asp.Net Core框架定義的Filter,而是DotNetCoreRpc框架定義的Filter,自定義Filter的方式如下

    //*要繼承自抽象類RpcFilterAttribute
    public class CacheFilterAttribute: RpcFilterAttribute
    {
        public int CacheTime { get; set; }
    
        //*支持屬性注入,可以是public或者private
        //*這裏的FromServices並非Asp.Net Core命名空間下的,而是來自DotNetCoreRpc.Core命名空間
        [FromServices]
        private RedisConfigOptions RedisConfig { get; set; }
    
        [FromServices]
        public ILogger<CacheFilterAttribute> Logger { get; set; }
    
        public override async Task InvokeAsync(RpcContext context, RpcRequestDelegate next)
        {
            Logger.LogInformation($"CacheFilterAttribute Begin,CacheTime=[{CacheTime}],Class=[{context.TargetType.FullName}],Method=[{context.Method.Name}],Params=[{JsonConvert.SerializeObject(context.Parameters)}]");
            await next(context);
            Logger.LogInformation($"CacheFilterAttribute End,ReturnValue=[{JsonConvert.SerializeObject(context.ReturnValue)}]");
        }
    }
    

    以上代碼基本上完成了對服務端業務代碼的操作,接下來我們來看如何在Asp.Net Core中配置使用DotNetCoreRpc。打開Startup,配置如下代碼既可

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSingleton<IPersonService, PersonService>()
            .AddSingleton(new RedisConfigOptions { Address = "127.0.0.1:6379", Db = 10 })
            //*註冊DotNetCoreRpcServer
            .AddDotNetCoreRpcServer(options => {
                //*確保添加的契約服務接口事先已經被註冊到DI容器中
    
                //添加契約接口
                //options.AddService<IPersonService>();
    
                //或添加契約接口名稱以xxx為結尾的
                //options.AddService("*Service");
    
                //或添加具體名稱為xxx的契約接口
                //options.AddService("IPersonService");
    
                //或掃描具體命名空間下的契約接口
                options.AddNameSpace("IRpcService");
    
                //可以添加全局過濾器,實現方式和CacheFilterAttribute一致
                options.AddFilter<LoggerFilterAttribute>();
            });
        }
    
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
        {
            //這一堆可以不要+1
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
    
            //添加DotNetCoreRpc中間件既可
            app.UseDotNetCoreRpc();
    
            //這一堆可以不要+2
            app.UseRouting();
    
            //這一堆可以不要+3
            app.UseEndpoints(endpoints =>
            {
                endpoints.MapGet("/", async context =>
                {
                    await context.Response.WriteAsync("Server Start!");
                });
            });
        }
    }
    

    以上就是Server端簡單的使用和配置,是不是感覺非常的Easy。附上可運行的Demo地址,具體編碼可查看Demo.

    總結

        能自己實現一套RPC框架是我近期以來的一個願望,現在可以說實現了。雖然看起來沒這麼高大上,但是整體還是符合RPC的思想。主要還是想自身實地的實踐一下,順便也希望能給大家提供一些簡單的思路。不是說我說得一定是對的,我講得可能很多是不對的,但是我說的東西都是我自身的體驗和思考,也許能給你帶來一秒鐘、半秒鐘的思考,亦或是哪怕你覺得我哪一句話說的有點道理,能引發你內心的感觸,這就是我做這件事的意義。最後,歡迎大家評論區或本項目GitHub下批評指導。

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

    【其他文章推薦】

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

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

    ※超省錢租車方案

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

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

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

  • 沒有國產主機,怎麼開發:交叉編譯和QEMU虛擬機

    沒有國產主機,怎麼開發:交叉編譯和QEMU虛擬機

    1. 背景

    近期國產化的趨勢越來越濃,包括國產操作系統、國產CPU等。時隔十多年,QQ for Linux也更新了。做為軟件開發人員,“有幸”也需要適配國產化。至於國產化的意義等就不在此討論。

    本文提到的國產主機主要是指使用國產CPU和操作系統的計算機,比如:操作系統是銀河麒麟,CPU是飛騰FT2000。如果需要做適配開發,起碼需要一台對應的主機吧。據說在國產化早期,有錢都難買到機器,需要特殊渠道申請購買。不過,現在購買還是比較方便的。

    通過客戶提供的正規正統的廠家詢價,着實嚇一跳,一台居然要一萬多!!而同等性能配置的windows-x86普通台式主機,才兩三千塊左右,相差有點大呀。本着能省就省的原則,上萬能的某寶看能不能淘一個。真得感謝馬爸爸和深圳華強北,5千多塊,突然感覺肉沒那麼痛了。

    其實完全可以理解,國產的批量肯定很小很小,價格必然是高的。對於不專門開發“國產軟件”的公司來說,買一台使用率比較低的機器不太值得。後面將介紹在沒有國產主機情況下,進行軟件開發的兩種替代方法:交叉編譯和QEMU虛擬機。

    2. 銀河麒麟是什麼

    銀河麒麟操作系統有服務器版本和桌面版本,本文使用的是桌面版本。具體細節看官方的介紹即可,就不做搬運工了。官方說的自主研發、安全可控都不是我們所關心的,我們只需要關心它的內核是什麼,會不會如網上所說根本就是個Ubutun,改個皮膚而已?!。

    先用VMware安裝個虛擬機試試吧,網上找了一個只有X86架構的鏡像包Kylin-4.0.2-desktop-sp2_Community-20171127-x86_64.iso,安裝過程略過,使用命令“uname -a”查一下。

    Linux wrgz-Lenovo 4.11.0-14-generic #20~16.04.1kord0k1-Ubuntu SMP Wed Oct 18 00:56:13 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
    

    看到Ubuntu就放心了,就當它是個Ubuntu Linux就行了。

    3. 飛騰FT2000又是什麼

    通俗點講它就是個CPU,再看看飛騰的官網上的描述。FT-2000系列芯片是基於飛騰片上并行系統(PSoC)體繫結構設計的通用微處理器,兼容ARMv8指令集,兼容支持ARM64和ARM32兩種執行模式。哦嚯,划個重點,簡單點看它就是一個ARMv8的64位CPU。

    划個不考試的重點:對於應用軟件開發者,簡單理解為是在ARMv8架構上的Ubuntu Linux上進行開發軟件;對於普通辦公者,則理解為是仿Windows的Linux系統。

    4. 交叉編譯

    本文提到的軟件開發,是使用C/C++開發無界面的應用軟件,實際上開發和測試都有是可以在Ubuntu上進行。但發布軟件則需要真機編譯或者交叉編譯才能運行。

    很幸運,在上飛騰官網時,發現了飛騰FT2000的技術文檔FT-2000+64Sv1.1.pdf,裏面有介紹到交叉編譯環境。

    • 安裝Ubuntu16.04(可安裝在虛擬機上或 X86電腦裸機上)
    • 安裝成功后,虛擬機 apt 源修改 修改/etc/apt/source.list 內容為如下:
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial main restricted
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates main restricted
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial universe
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates universe
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial multiverse
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-updates multiverse
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-backports main restricted universe > multiverse
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security main restricted
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security universe
    deb http://mirrors.tuna.tsinghua.edu.cn/ubuntu/ xenial-security multiverse
    
    • 運行 apt-get update,再運行apt-get install gcc-aarch64-linux-gnu安裝
    • 使用命令aarch64-linux-gnu-gcc –v可以看到gcc版本號為gcc version 5.4.0 20160609

    有了交叉編譯器,編譯是很輕鬆的事。經後續測試,交叉編譯出來的程序,可以在國產真機上運行。

    5. QEMU虛擬機是什麼

    我們經常使用的虛擬機軟件是VMware,擺着這麼好的不用,為什麼選擇QEMU呢。這得從他們的區別說起。

    VMware重點於在一個硬件平台下運行多個操作系統,虛擬硬件平台與宿主硬件架構一致,也就是說虛擬機程序中的指令一般就是宿主CPU指令集,可以直接執行,因此一般速度上也就比較快。

    QEMU的特點是可以虛擬不同的硬件平台架構,比如在X86機器上虛擬出ARM架構的機器。許多基於ARM指令集的Android手機模擬器是基於Qemu的,很適合無真機情況下進行Android開發。當然執行ARM指令,需要轉換成X86指令才能在宿主機器上運行,這樣速度一般會慢點。

    由於本文提到的國產主機就是ARM架構的,VMware並不適用,而QEMU則符合要求。還有一個原因是QEMU支持Windows,只需要一個安裝包,安裝過程簡單,太香了。

    6. QEMU安裝銀河麒麟操作系統

    無獨有偶,鯤鵬處理器也是ARMv8指令集,在華為官網看到詳細的安裝過程,安裝細節可參考https://www.huaweicloud.com/kunpeng/software/qemu.html。

    下面只針對一些重點關注點做些說明。

    • 需要下載一個Arm64架構的麒麟桌面操作系統鏡像包,名字類似Kylin-4.0.2-desktop-sp3-xxxxxxx-arm64.iso。之所以重點提這點,是因為這種鏡像包在網上很難找。有想到用Arm64架構的Ubuntu鏡像包代替,才發現原來官方並沒有提供ARM桌面版的鏡像包(有ARM服務器版)。
    • 原來華為提供的安裝參數有些問題,包括網絡、鼠標、鍵盤參數。這些參數配置不對,會直接影響使用。

    QEMU有一個不太人性化的特點,就是沒有提供類似VMware的界面操作,只能通過命令操作,參數還特別多,網上的資料不多,官方文檔都有是英文的。下面給出三個重要的QEMU命令:創建、安裝、啟動。

    創建
    這個步驟就是創建一個預分配一個大文件,做為虛擬機的磁盤,我比較任性地分配了40G。

    c:\qemu\qemu-img.exe create D:\qemu\vm\kylin\hdd01.img 40G
    

    安裝

    c:\qemu\qemu-system-aarch64.exe -m 4096 -cpu cortex-a72 -smp 2,cores=2,threads=1,sockets=1 -M virt -bios D:\qemu\bios\QEMU_EFI.fd -net nic,model=pcnet -device nec-usb-xhci -device usb-kbd -device usb-mouse -device VGA -drive if=none,file=D:\software\kylin\Kylin-4.0.2-desktop-sp3-19122616.Z1-arm64.iso,id=cdrom,media=cdrom -device virtio-scsi-device -device scsi-cd,drive=cdrom -drive if=none,file=D:\qemu\vm\kylin\hdd01.img,id=hd0 -device virtio-blk-device,drive=hd0
    

    啟動

    c:\qemu\qemu-system-aarch64.exe -m 4096 -cpu cortex-a72 -smp 2,cores=2,threads=1,sockets=1 -M virt -bios D:\qemu\bios\QEMU_EFI.fd -net nic -net tap,ifname=tap0 -device nec-usb-xhci -device usb-kbd -device usb-mouse -device VGA -device virtio-scsi-device -drive if=none,file=D:\qemu\vm\kylin\hdd01.img,id=hd0 -device virtio-blk-device,drive=hd0
    

    安裝和啟動的命令參數差不多,統一說明它們的含義:

    參數 說明
    qemu-system-aarch64.exe 二進制文件,提供模擬aarch64架構的虛擬機進程
    -m 2048 分配2048MB內存
    -M virt 模擬成什麼服務器,我們一般選擇virt就可以了,他會自動選擇最高版本的virt
    -cpu cortex-a72 模擬成什麼CPU,其中cortex-a53\a57\a72都是ARMv8指令集的
    -smp 2,cores=2,threads=1,sockets=1 2個vCPU,這2個vCPU由qemu模擬出的一個插槽(socket)中的2個核心,每個核心支持一個超線程構成
    -bios xxx 指定bios bin所在的路徑
    -device xxx 添加一個設備,參數可重複
    -drive 添加一個驅動器,參數可重複
    -net 添加網絡設備

    QEMU虛擬機怎麼連網
    在Windows上使用qemu虛擬機,使虛擬機能連網,配置方法如下:

    • 在Windows主機上安裝TAP網卡驅動:可下載openvpn客戶端軟件,只安裝其中的TAP驅動;在網絡連接中,會看到一個新的虛擬網卡,屬性類似於TAP-Windows Adapter V9,將其名稱修改為tap0
    • 將虛擬網卡和Windows上真實網卡橋接:選中這兩塊網卡,右鍵,橋接。此時,Windows主機將不能連接互聯網,需要在網橋上配置IP地址和域名等信息,才能使Windows主機連接互聯網。
    • QEMU參數配置:在虛擬機啟動命令行添加以下參數–net nic -net tap,ifname=tap0;tap0為的虛擬網卡名。

    7. 總結

    國產操作系統的使用體驗已經好了很多,輕度辦公室還是可行的,但想替換Windows,太難了。
    QEMU可以虛擬不同的硬件平台架構,是個不錯的虛擬機軟件,而且開源,但在使用體驗方面還是差了一些。

    歡迎關注我的公眾號【林哥哥的編程札記】,謝謝!

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 寫了個全局變量的bug,被同事們打臉!!!

    寫了個全局變量的bug,被同事們打臉!!!

    話說棧長前陣子寫了一個功能,測試 0 bug 就上線了,上線后也運行好好的,好多天都沒有人反饋bug,超爽。。

    不出問題還好,出問題就是大問題。。

    最近有個客戶反饋某些數據混亂問題,看代碼死活看不出什麼問題,很詭異,再仔細看代碼,原來是一個全局變量的問題,導致在併發情況下出現了線程不安全的問題,事後被同事們打臉!!!

    慎用全局變量,我在公司一直在強調,沒想到這麼低級的問題居然發生在自己身上,說起來真的慚愧啊。。

    最開始使用的是 Spring 注入對象的方式:

    @Autowired
    private Object object;
    

    因為 Spring 默認是單例,所以這樣寫是沒有問題的,後來隨着業務的發展,需要多個不同的業務實例,我改成了這種方式:

    @Setter
    private Object object;
    

    這個 @Setter 是 Lombok 的註解,用來生成 setters 方法,現在想起來,真是低級啊,同時操作的情況下,這個對象肯定會出現覆蓋的情況,從而導致上面說的問題。

    寫了一個這麼低級bug,我也不怕不好意思發出來,大家都謹記一下吧。

    另外,我再總結幾個慎用全局變量的場景:

    1、SimpleDateFormat

    SimpleDateFormat 禁止定義成 static 變量或者全局共享變量,因為它是線程不安全的,都被寫進阿里巴巴的《Java開發手冊》里了:

    為什麼說 SimpleDateFormat 不是線程安全的呢?

    來看下它的 format 方法源碼:

    可以看到 calendar 變量居然也是全局變量,多線程情況下就會存在設置臟變量的情況。

    所以,如果要用 SimpleDateFormat,就在每次用的時候都創建一個 SimpleDateFormat 對象,做到線程間隔離。

    2、資源連接

    資源連接包括數據庫連接、FTP連接、Redis連接等,這種也要慎用全局變量,一量使用全局變量,就會遇到以下問題:

    1)關閉連接的時候,就可能把別人正在操作的連接給關了,導致其他線程的業務中斷;

    2)因為是全局變量,創建的時候可能會創建多個實例,在關閉連接的時候,就可能只關閉了一個對象的連接,造成其他連接沒有被關閉,最後導致連接耗光系統不可用;

    3、数字運算

    這也是個很經典的問題了,如果要用多線程對一個数字進行累加等其他運算處理,千萬不要用全局基礎類型的變量,如下所示:

    private long count;
    

    多線程情況下,某個線程獲取到的值可能已經被其他線程修改了,最後得到的值就不準確了。

    當然,上面的示例可以通過加鎖的方式來解決,也可以使用全局的原子類(java.util.concurrent.atomic.Atom*)進行處理,比如:

    private AtomicInteger count = new AtomicInteger();
    

    注意,這種原子類使用全局變量就沒有線程安全的問題,它使用了 CAS 算法保證了數據一致性。

    不過,阿里推薦使用LongAdder,因為性能更好:

    java.util.concurrent.atomic.LongAdder

    4、全局session

    來看下面的例子:

    @Autowired
    protected HttpSession session;
    

    全局注入一個 Session 對象,在 Spring 中,這樣全局注入使用上面是默認沒問題的,包括 request, response 對象,都可以通過全局注入來獲取。

    這樣會存在線程安全性嗎?

    不會!

    使用這種方式,當 Bean 初始化時,Spring 並沒有注入真實對象,而是注入了一個代理對象,真正使用的時候通過該代理對象獲取真正的對象。

    並且,在注入此類對象時,Spring使用了線程局部變量(ThreadLocal),這就保證了 request/response/session 對象的線程安全性了。

    具體就不展開了,詳細的介紹及測試大家可以點擊這個鏈接查看這篇文章。

    既然是線程安全,但也得小心,如果我在方法中主動使 session 對象失效並重建了:

    session.invalidate();
    session = request.getSession();
    

    這樣,session對象就變成了真實對象了,不再是代理對象,就變成了文章最開始的時候我說的那種多線程安全問題了,如果線上出現 session 會話混亂,用戶 A 就可能看到用戶 B 的數據,你想想可不可怕?

    所以,即使可以這樣使用,也得千萬小心謹慎,最好是在方法級別使用這些對象。

    總結

    今天,棧長總結了一下我是怎麼寫出這個全局變量的低級 bug,也總結了下慎用全局變量的 4 種情況,相信大家多多少都遇到過類似的問題,希望能幫助大家少踩坑。

    全局變量雖好,但我們也得謹慎使用啊,一定要考慮是否引起多線程安全問題,不然會引起重大問題。

    你還遇到過哪些全局變量的問題,歡迎留言分享哦!

    推薦去我的博客閱讀更多:

    1.Java JVM、集合、多線程、新特性系列教程

    2.Spring MVC、Spring Boot、Spring Cloud 系列教程

    3.Maven、Git、Eclipse、Intellij IDEA 系列工具教程

    4.Java、後端、架構、阿里巴巴等大廠最新面試題

    覺得不錯,別忘了點贊+轉發哦!

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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