俄遠東堪察加地區發生海水污染 環團憂生態災難

摘錄自2020年10月4日中央社報導

俄羅斯當局今(4日)通知遠東堪察加地區民眾遠離一處原始海灘,因發生不明原因的海水污染。綠色和平組織表示,這是當地發生「生態災難」的證據,並已造成一些衝浪者發燒和嘔吐。

路透社報導,污染地點在堪察加地區(Kamchatka Region)的卡拉克提斯基(Khalaktyrsky)海灘,目前還不清楚原因。俄羅斯當局表示,初步檢測發現海水有高含量的石油產品和化學物質苯酚。綠色和平組織(Greenpeace)要求當局盡速找到污染源。

這片海灘位在太平洋沿岸,布滿黑色火山砂,全長數十公里,是熱門觀光景點。綠色和平組織暗示,這起污染可能數週前已開始。綠色和平組織在聲明中表示,來到卡拉克提斯基海灘的民眾,數週以來就注意到污染情況。接觸過海水的人都出現不良反應,包括喉嚨痠痛、視力惡化、眼睛乾澀、噁心、身體虛弱、嘔吐和發燒。

海洋
污染治理
國際新聞
俄國

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

疫情肆虐油價跌 石油巨擘被迫減少探勘轉向綠能

摘錄自2020年10月4日中央社報導

疫情重創石油需求及油價,迫使能源巨擘縮減探勘規模,儘管尋找新油田對他們的生計依舊至關重要,他們仍將必須加強綠能投資。

根據研究團體Westwood統計,與疫情前計畫相比,能源業在英國北海水域探勘計畫削減7成,在挪威外海削減3成。

美國石油巨擘埃克森美孚公司(ExxonMobil)共砍了三成的探勘計畫,投資額減少100億美元。歐洲對手埃尼(Eni)、英國石油公司(BP)及艾基挪(Equinor)都有類似行動。根據美國軒尼詩布恩律師事務所(Haynes & Boone)統計,美國今年有30多家石油探勘和生產公司聲請破產。

根據能源諮詢機構「雷斯塔德能源」(Rystad Energy)估計,如果油價持續在當前每桶40美元左右徘徊,到了2022年可能還會有150家類似的公司消失。

能源轉型
國際新聞
石油
綠能

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

日本新潟「新世代熊」不怕人9天7人遇襲 半年內出沒逾700次

摘錄自2020年10月4日中央社報導

日本新潟縣近半年「熊出沒」次數創新高,次數多達逾700次,甚至發生近9天內有7人遭熊襲受傷的意外。專家推測,山上食物缺乏及不怕人的「新世代熊」逐年增加,恐是熊出沒次數創新高的原因。

專家提醒,在北海道、東北及北陸等很多熊棲息的地區,「由於熊分布區域愈來愈廣,大家應該要認知這是一種近在身旁的動物」。新潟縣政府表示,「熊出沒」次數主要是統計熊被目擊到或是被發現出沒痕跡的次數;縣府本月1日已發布「熊出沒警戒警報」。

日本新潟放送(BSN)報導,發生在新潟縣關川村的兩人遭熊攻擊事件,由於案發地附近約700公尺有一間小學,目前小學生都在警察等人護送下上下學。縣府表示,作為熊的食物之一的山毛櫸本季結果數量,是近10年來最少的一季,這些熊應該是下山到人居住的地方覓食,全縣各地都有必要警戒熊出沒。

生物多樣性
國際新聞
日本
人與動物衝突事件簿
人熊衝突

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

聯合國:恢復自然環境的資金缺口 高達7000億美元

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

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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

C# 9.0 新特性之參數非空檢查簡化

閱讀本文大概需要 1.5 分鐘。

參數非空檢查是縮寫類庫很常見的操作,在一個方法中要求參數不能為空,否則拋出相應的異常。比如:

public static string HashPassword(string password)
{
    if(password is null)
    {
        throw new ArgumentNullException(nameof(password));
    }
    ...
}

當異常發生時,調用者很容易知道是什麼問題。如果不加這個檢查,可能就會由系統拋出未將對象引用為實例之類的錯誤,這不利於調用者診斷錯誤。

由於這個場景太常見了,於是我經常在我的項目中通過一個輔助類來做此類檢查。這個類用來檢查方法參數,所以命名為 Guard,主要代碼如下:

public static class Guard
{
    public static void NotNull(object param, string paramName)
    {
        if (param is null)
        {
            throw new ArgumentNullException(paramName);
        }
    }

    public static void NotNullOrEmpty(string param, string paramName)
    {
        NotNull(param, paramName);
        if (param == string.Empty)
        {
            throw new ArgumentException($"The string can not be empty.", paramName);
        }
    }

    public static void NotNullOrEmpty<T>(IEnumerable<T> param, string paramName)
    {
        NotNull(param, paramName);
        if (param.Count() == 0)
        {
            throw new ArgumentException("The collection can not be empty.", paramName);
        }
    }
    ...
}

這個類包含了三個常見的非空檢查,包括 null、空字符串、空集合的檢查。使用示例:

public static string HashPassword(string password)
{
    Guard.NotNull(password, nameof(password));
    ...
}

public static IEnumerable<TSource> DistinctBy<TSource, TKey>(
    this IEnumerable<TSource> source,
    Func<TSource, TKey> keySelector)
{
    Guard.NotNullOrEmpty(source, nameof(source));
    ...
}

介於這種非空檢查極其常見,C# 9.0 對此做了簡化,增加了操作符‘!’,放在參數名後面,表示此參數不接受 null 值。使用方式如下:

public static string HashPassword(string password!)
{
    ...
}

簡化了很多有木有。這個提案已經納入 C# 9.0 的特性中,但目前(2020-06-13)還沒有完成開發。

這個特性只支持非 null 檢查,其它參數檢查場景還是不夠用的,我還是會通過輔助類來進行像空字符串、空集合的檢查。

這個特性在寫公共類庫的時候很有用,但我想大多數人在寫業務邏輯代碼的時候可能用不到這個特性,一般會封自己的參數檢查機制。比如,我在項目中,對於上層 API 開發,我通過封裝一個輔助類(ApiGuard)來對對參數進行檢查,如果參數不通過,則拋出相應的業務異常,而不是 ArgumentNullException。比如下面的一段截取自我的 GeekGist 小項目的代碼:

public static class ApiGuard
{
    public static void EnsureNotNull(object param, string paramName)
    {
        if (param == null) throw new BadRequestException($"{paramName} can not be null.");
    }

    public static void EnsureNotEmpty<T>(IEnumerable<T> collection, string paramName)
    {
        if (collection == null || collection.Count() == 0)
            throw new BadRequestException($"{paramName} can not be null or empty.");
    }

    public static void EnsureExist(object value, string message = "Not found")
    {
        if (value == null) throw new BadRequestException(message);
    }

    public static void EnsureNotExist(object value, string message = "Already existed")
    {
        if (value != null) throw new BadRequestException(message);
    }
    ...
}

使用示例:

public async Task UpdateAsync(long id, BookUpdateDto dto)
{
    ApiGuard.EnsureNotNull(dto, nameof(dto));
    ApiGuard.EnsureNotEmpty(dto.TagValues, nameof(dto.TagValues));

    var book = await DbSet
        .Include(x => x.BookTags)
        .FirstOrDefaultAsync(x => x.Id == id);
    ApiGuard.EnsureExist(book);

    Mapper.Map(dto, book);

    ...
}

ApiGuard 的好處是,當 API 接口接到不合要求的參數時,可以自定義響應返回內容。比如,增加一個 Filter 或中間件用來全局捕獲業務代碼異常,根據不同的異常返回給前端不同的狀態碼和消息提示:

private Task HandleExceptionAsync(HttpContext context, Exception exception)
{
    ApiResult result;
    if (exception is BadRequestException)
    {
        result = ApiResult.Error(exception.Message, 400);
    }
    else if (exception is NotFoundException)
    {
        message = string.IsNullOrEmpty(message) ? "Not Found" : message;
        result = ApiResult.Error(message, 404);
    }
    else if (exception is UnauthorizedAccessException)
    {
        message = string.IsNullOrEmpty(message) ? "Unauthorized" : message;
        result = ApiResult.Error(message, 401);
    }
    ...
}

只是一個參數非空檢查,在實際開發中卻有不少的學問,所以學好了理論還要多實踐才能更透徹的理解它。

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

【其他文章推薦】

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

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

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

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

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

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

關於問問題和時間管理的感悟

這應該算是第一次認真的談談自己,第一次聊自己的時候是Java建設者剛出生沒多久,現在看看屆時的文筆,簡直了,不像是寫了一篇讓讀者觀看的文字,更像是自己情感的集散地。

首談自己

其實,讀者是很容易埋單的,只要你為他考慮一些即可。你的文字能否做到通俗易懂,你的文章是否能夠做到風趣幽默,你的文章是否能讓讀者學到什麼東西,亦或者說是你想傳達/表達的意思是否表達到位?

比如我們拿一篇源碼分析的文章舉例子,你是否能做好這幾點

  • 你這篇源碼分析,想要教會讀者什麼?
  • 這篇源碼分析,自己看完有沒有疑惑?怎麼解決這些疑惑?或者說哪段代碼比較難以理解,是否再應該詳細解釋?
  • 論述文章中大量代碼對讀者的直觀感受是怎樣的,如果你文章中出現了大量源碼,幾十行以上,而且還帶着中文註釋的話,是否應該以另外的一種形式來展現出來?這些代碼能否放在 Github 上面,讓意猶未盡的讀者得到釋懷?大部分人看文章還是在手機上的,所以你要為他們考慮啊。
  • 對於源碼的話,有必要都貼出來嗎?有沒有可能簡化一些無關代碼,比如日誌打印?邏輯判斷?各種方法調用?這些能否通過流程圖給出呢?
  • 只講關鍵代碼,比如說某段代碼很關鍵,這段代碼做了什麼事兒,能否簡單列一下?
  • 注意講述的措辭,源碼分析的 title 註定就是枯燥無味的,你能否加一些適量措辭,讓源碼分析不再枯燥?

其實上面你滿足一點或幾點的話,都是會有讀者買賬的。某一個點 get 住,讀者就三連了

如果你不想考慮這些問題的話,那你終究還是為了自己寫作了。這個方向也沒錯,那拜託你就不要想着天天還要硬性指標漲粉了,佛系一些更快樂。

上面這段論述是想告訴大家,我寫文章過程中的一些疑惑和注意事項,下面來真正談一談我自己

我不是大神

請千萬擦亮眼睛來看我,我真的不是大神。一部分人給我留言非常尊重我,把我封為大佬、大神,其實我真不是。我只是一個堅持學習,堅持分享的程序員,想要通過文章獲得更多的關注,擴大自己的影響力罷了。

這裡有一點大家需要注意一下,在了解一個人之前,千萬不要給他人過早的樹立 IP,下面就是我犯的一個錯誤。

只通過三言兩句就覺得別人非常厲害?萬一他只是一個網絡搬運工呢?

所以一些讀者朋友可能看我文章寫的還能看過去,就覺得我是大神,其實真不是。但是我不可能和每個人都講我不是大神,我只能默默的承擔這個稱呼,欲戴皇冠,必承其重,我本是一個普通學校畢業的辣雞,非要給我扣上大神的帽子,讓我的行事、回答樣樣標準,不好意思這個真做不到。

私信問問題

這個我需要說一下自己的看法,關於私信問問題這件事情,我相信大家都有接觸過,而且很多大佬其實都談過了,這裏我也說一下自己的想法,暫時列出來幾類吧

  • 私信問代碼怎麼跑不通,能否幫忙看一下。這種問題我現在一般的回復就是 不好意思,我沒有時間來看這些問題,請發到群里謝謝,一方面是我菜,在一方面是我本理解讀者的意思,着急、摳了好久摳不出來,但是你有沒有想過,解決一個問題的成本有多少?你需要和他溝通,你需要把你表達的意思闡述到位,你需要讓他理解清楚你的困惑,你需要理解他的意思,你需要理解他的意思然後懂得對應的知識點,你需要理解他的意思懂得對應的知識點成功的把問題解決,流程圖如下。

任何一個環節遺漏都會提高溝通成本,大家都非常忙,真的沒有時間給你解決這種代碼問題……

換個角度想,代碼問題其實是提高你自己解決辦法的一個機會,如果你是學生,你要學會自己修改代碼。如果你是職場人士,那就不用多說了,大家都是吃這口飯的。

  • 私信問這錯誤怎麼回事?這種問題問出來我覺得就是對人的不尊重。代碼錯誤的原因有很多種,你要讓其他人都給你分析到位嗎?有的時候貼出來自己的代碼片段,但是你以為你以為的就是你以為的嗎?

我記得有很多人寫過關於如何詢問一個技術問題的回答,stackoverflow 上面好像也有這個回答,我找到了一個回答。

https://princetonuniversity.github.io/PUbootcamp/sessions/technical-questions/HowToAskQuestions2018Bootcamp.pdf

真實情況下我們都不會仔細研究這個 pdf,所以普世的我覺得能接受的問問題方式就是

  • 針對 xxx 情況,有沒有什麼解決辦法或者方向?
  • 針對 xxx ,有沒有可以參考的書籍或者是博客/論文?
  • 大家有沒有了解過 xxx ,針對 xxx 的 xxx 問題,大家有沒有好的建議?

理性提問,拒絕做伸手黨,沒有人有義務的幫你解決問題,提問題前請先想好自己能否把問題描述清楚,需要得到什麼樣的回答。很多人埋怨自己問問題沒人回,總覺得是他人的原因,其實是自己根本沒有描述好問題本身,就是自己不知道自己有問題,這是最可怕的

關於時間管理

很多人問我如何管理時間的,這個我要哭了

真的沒有什麼時間管理辦法……我一般都是硬肝,就算你列周全一個詳細的計劃表,也會被各種各樣的因素所打斷,打斷了之後就要做計劃變更,一兩件事情還好,一旦多了之後,可以想像你這個計劃表還有意義嗎?

或者說是這樣,可以夜深人靜沒有人打擾的時候,做一份學習計劃呢

但是有幾個時間管理的小技巧需要注意一下

問題:每個人都特么只有 24 小時,怎麼能讓自己的時間變得比別人多?

那特么只能睡的少啊!!!

如果能正確理解上面的意思,就可以看看下面這些小技巧

  • 關於出行,公交/地鐵,這是比較理想化的出行方式,因為路上可以學習東西,我一般在公交/地鐵上看指導性、結構化的東西,比如一本書他的章節是怎樣的,值不值得看。不會細摳面試題、源碼之類,別問為什麼,容易頭暈。
  • 上班摸魚時間,這個大家都懂。建議這個時間多搜羅一些好的文章,及時收藏保存,會很有用的。
  • 早上早起的時間是最適合看書的時間,看源碼/文章都可以,這個時間太寶貴了
  • 有朋友問女友太纏着我怎麼辦,這個無解,只能把她哄開心了你才能做自己的事兒,要不只能吵架,吵架更麻煩,還得哄着,需要時間更多。所以遇到一個不可拒絕的條件,只能受着,你讓她開開心心,你才能開開心心。太難了。
  • 關於飯後時間,我相信大部分人都拿着手機和朋友圈在刷,這個時間我一般都會讀一些收藏過的文章,效果也很不錯

關於學習

我之前提過,學習是最簡單的事兒,同時也是你應該始終堅持的一件事情。

關於學習的重要性比如三天不讀書,智商輸給豬這類的,我就不再多說了。

我說一下我學習的順序把

  • 一般早上和晚上,我都會刷計算機基礎之類的書,或者源碼之類
  • 工作時間,一般都會看框架方面的書
  • 累了,才會看小說這類

聯繫我

下面我匯總了一下 Java 高頻面試題 PDF,可以關注公眾號回復 面試題 領取

另外,我最近發起了一個 Github 項目,裏面有我認為比較全的技術棧,祝你學習一臂之力。

https://github.com/crisxuan/bestJavaer

點擊閱讀原文跳轉,歡迎各位 star

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

【其他文章推薦】

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

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

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

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

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

JS之預解釋原理

預解釋的原理

  • 預解釋的不同機制
  • var的預解釋機制
  • function 的預解釋機制
  • 預解釋機制
  • 面試題練習

預解釋的的不同機制

預解釋也叫預聲明,是提前解釋聲明的意思;預解釋是針對變量和函數來說的;但是變量和function的的預解釋是兩套不同的機制;

  • 當瀏覽器加載我們的HTML頁面的時候,首先會提供一個供JS代碼執行的環境->全局作用域global(瀏覽器中的全局作用域,也叫頂級作用域是window
  • JS中的內存空間分為兩種:棧內存、堆內存
    • 棧內存;提供JS代碼執行的環境,存儲基本數據類型的值;->全局作用域或者私有的作用域其實都是棧內存;
    • 堆內存;存儲引用數據類型的值(對象是把屬性名和屬性值儲存進去,函數是把函數體內的代碼當做字符串儲存進去)
  • 在當前的作用域中,JS代碼執行之前,瀏覽器首先會默認的把所有代var和function的進行提前的聲明或者定義,->“預解釋”(也叫變量提升)

var的預解釋機制

var a=1

  • 1、代碼運行之前,先掃描有沒有帶var關鍵字的變量名,有的話,為這個變量名在內存里開一個空間;這時候變量名a是不代表任何值的;用undefined來表示;undefined是一個標識符/記號,表示找不到這個變量名所代表的數據;不存在的意思;這個階段叫變量的聲明

  • 2、當代碼運行的時候,則給數據1開闢一個內存空間;

  • 3、讓數據1和變量名a綁定在一起;變量類型指的就是數據類型;按照js語言的原理來說變量類型有undefined類型;但是數據類型是沒有undefined這種數據類型的;只有”undecided”這種字符串類型(字符串類型是數據類型的一種);同理也沒有unll這個數據類型,但是有”null”這種字符串類型;

    var num;
    //1、聲明(declare):var num; ->告訴瀏覽器在當前作用域中有一個num的變量了,如果一個變量只是聲明了但是沒有賦值,默認的值是undefined
    console.log(num);//->undefined
    num = 12;
    //2、定義(defined):num=12; ->給我們的變量進行賦值
    console.log(num);//->12
    
    //變量提前使用的話,就是undefined
    console.log(testStr);//undefined
    var testStr="22222222"
    

function 關鍵字的預解釋步驟

function fn(){……}

在代碼執行之前,把所有的帶function關鍵字的腳本都掃描一遍,然後定義變量;並且同時給變量賦值;

  • 1、函數的定義只是保存一些字符串;預解釋的時候在內存里保存fn大括號裏面的字符串;

  • 2、代碼運行時候,讀到fn()時候,這個時候就是函數的運行;函數的運行,會先開闢一個堆內存把字符串當做代碼在堆內存中再次運行,函數產生的作用域內還會再進行預解釋和代碼運行;

    函數如果多次執行;會產生多個作用域;但是產生的多個作用域裏面的內容都是相互獨立的;互相沒有關係;(在原型和原型鏈時候再仔細研究原理;)

    fn(100,200);//->可以在上面執行,因為預解釋的時候聲明+定義就已經完成了    
    function fn(num1, num2) {        
    	var total = num1 + num2;  
    	console.log(total);
    }
    

總結

  • 1、varfunction關鍵字的在預解釋的時候操作還是不一樣的
    • var -> 在預解釋的時候只是提前的聲明了這個變量,只有當代碼執行的時候才會完成賦值操作
    • function -> 在預解釋的時候會提前的把聲明加定義都完成了(在代碼執行的時候遇到定義的代碼直接的跳過)
  • 2、預解釋只發生在當前的作用域下,例如:開始只對window下的進行預解釋,只有函數執行的時候才會對函數中的進行預解釋;

[重要]剛開始只對window下的進行預解釋,fn函數中目前存儲的都是字符串,所以var total沒啥實際的意義,所以不進行預解釋 -> “預解釋是發生在當前作用域下的”

  綜合題;

	console.log(obj);//->undefined
	var obj = {name: "xie", age: 25};
    function fn(num1, num2) {//代碼執行到這一行的時候直接的跳過這一塊的代碼,因為在預解釋的時候我們已經完成了聲明加定義
        var total = num1 + num2;
        console.log(total);
    }
    var num1 = 12;
    fn(num1, 100);//執行fn,把全局變量num1的值賦值給形參num1,把100賦值給形參num2
  • 下面是一個預解釋思路

    var a,
        b = 0,
        fn = function () {
            var a = b = 2;
        };
    fn();
    console.log(a, b);
    

把上面解析成下面就好理解了

var a;
window.b = 0;
window.fn = function () {
    //var a = b = 2;
    var a = 2;//a是私有的和全局沒關係
    b = 2;//b是全局的
};
fn();//window.fn()
console.log(a, b);//undefined 2

預解釋機制

  • 1、不管條件是否成立都要進行預解釋

    console.log(a);//->undefined
    if (!!("a" in window)) {//"a" in window -> true
        var a = "xie";
    }
    console.log(a);//->xie
    

    例子中的if是不成立的,預解釋的時候,碰到非functon內的var,都會聲明,無論你寫在if else 還是別的判斷里; 假設if語句起作用的話,那麼第一次log(a)的時候,就會報錯了(沒有聲明的變量,是不能直接用的,除非typeof ),而聲明並且沒有賦值的表現才是undefined;假設不成立; 最開始總結的預解釋步驟:代碼運行之前,先掃描有沒有帶var關鍵字的變量名,有的話,為這個變量名,在內存里開一個空間;預解釋是發生在代碼執行前的,所以if根本阻擋不了預解釋;

  • 2、預解釋只發生在”=“的左邊,只把左邊的進行預解釋,右邊的是值是不進行預解釋的

匿名函數之函數表達式:把函數定義的部分當做值賦值給一個變量或者元素的事件

fn1();//->undefined() Uncaught TypeError: fn is not a function JS中只有函數可以執行 && JS上面的代碼如果報錯了,在不進行任何的特殊處理情況下我們下面的代碼都不在執行了
var fn1 = function () {
  console.log("ok");
};
fn1();
//預解釋的時候:fn=xxxfff000
fn2();//->"ok"
function fn2() {
  console.log("ok");
}
fn2();//->"ok"

預解釋的時候:var fn1 = function()... ->fn的默認值是undefined;這裏即使有function,也是不能進行預解釋的

  • 3、函數體中return下面的代碼都不在執行了,但是下面的代碼需要參加預解釋;而return後面的東西是需要處理的,但是由於它是當做一個值返回的,所以不進行預解釋;

    function fn() {
        console.log(total);
        return function sum() {};//return是把函數中的值返回到函數的外面,這裡是把function對應的內存地址返回的到函數的外面,例如:return xxxfff111;函數體中return下面的代碼都不在執行了
        var total = 10;
        console.log(total);
    }
    
  • 4、匿名函數的function在全局作用域下是不進行預解釋的;

匿名函數之自執行函數:定義和執行一起完成了;函數內的聲明,只是在函數內使用;

 (function(num){
  var testStr="test"+num;
  console.log(num);
})(100);

console.log(testStr);// testStr is not defined
  • 5、在預解釋的時候,如果遇到名字重複了,只聲明一次。 不重複的聲明,但是賦值還是要重複的進行的

  預解釋:

    var fn; 聲明
    fn = xxxfff000; [聲明]不用了+定義
    fn = xxxfff111; [聲明]不用了+定義
    // ->fn=xxxfff111
    var fn = 12;//window.fn=12
    function fn() {//window.fn=function(){}
    }

  JS中作用域只有兩種:

  • window全局作用域;
  • 函數執行形成的私有作用域;
  • {name:“”} if(){} for(){} while(){} switch(){} 這些都不會產生作用域;

ES6可以用let形成塊級作用域;http://www.cnblogs.com/snandy/archive/2015/05/10/4485832.html

面試題

// 涉及this的指向和閉包
var num = 20;
var obj = {
	num: 37,
	fn: (function (num) {
		this.num *= 3; // window.num * 3 = 60
		// num += 15; 
		var num = 45;
		return function () {
			this.num *= 4; 
			num += 20; // 調用父作用域的num (45+20)
			console.log(num);
		};
	})(num), //->把全局變量num的值20賦值給了自執行函數的形參,而不是obj下的30,如果想是obj下的30,我們需要寫obj.num
};
var fn = obj.fn; 
fn(); //->65 , 執行了第1次=> window.num = 240
obj.fn(); //->85 閉包(65+20) // 執行了第2次=>  obj.num = 37*4 = 148
console.log(window.num, obj.num); // 240,148

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

【其他文章推薦】

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

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

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

※超省錢租車方案

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

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

Mybatis源碼手記-從緩存體系看責任鏈派發模式與循環依賴企業級實踐

一、緩存總覽  

  Mybatis在設計上處處都有用到的緩存,而且Mybatis的緩存體系設計上遵循單一職責、開閉原則、高度解耦。及其精巧,充分的將緩存分層,其獨到之處可以套用到很多類似的業務上。這裏將主要的緩存體系做一下簡單的分析筆記。以及藉助Mybatis緩存體系的學習,進一步窺探責任鏈派發模式企業級實踐,以及對象循環依賴場景下如何避免裝載死循環的企業級解決方案

  先來一張之前的執行體系圖:

 

 對照這張執行圖,不難看出,其實對於一次Mybatis查詢調用,即SqlSession -> SimpleExecutor/ReuseExecutor/BatchExecutor -> JDBC,其實緩存就是在SqlSession到Executor*之間做一層截獲請求的邏輯。從宏觀上很好理解。CachingExecutor作為BaseExecutor的一個前置增強裝飾器,其增強的功能就是,判斷是否命中了緩存,如果命中緩存,則不進行BaseExecutor的執行派發。

 1 public class CachingExecutor implements Executor {
 2   // BaseExecutor
 3   private final Executor delegate;
 4   public CachingExecutor(Executor delegate) {
 5     this.delegate = delegate;
 6     delegate.setExecutorWrapper(this);
 7   }
 8   @Override
 9   public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
10       throws SQLException {
11     Cache cache = ms.getCache();
12     if (cache != null) {
13       flushCacheIfRequired(ms);
14       if (ms.isUseCache() && resultHandler == null) {
15         ensureNoOutParams(ms, boundSql);
16         List<E> list = (List<E>) tcm.getObject(cache, key);
17         if (list == null) {
18           list = delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
19           tcm.putObject(cache, key, list); // issue #578 and #116
20         }
21         return list;
22       }
23     }
24     // 如果未命中緩存則向BaseExecutor派發
25     return delegate.query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
26   }
27 }

所以由此來看,mybatis的緩存是先嘗試命中CachingExecutor的二級緩存,如果未命中,則派發個BaseExecutor,下來才會去嘗試命中一級緩存。由於一級緩存比較簡單,我們先來看一級緩存。

二、一級緩存概覽

之前執行器的那一節講過,Mybatis的執行器和SqlSession都是一對一的關係

1 public class DefaultSqlSession implements SqlSession {
2     // ...
3   private final Executor executor;
4     // ...
5 }

而每個執行器裡邊用一個成員變量來做緩存容器

1 public abstract class BaseExecutor implements Executor {
2     // ...
3   protected PerpetualCache localCache;
4     // ...
5 }

那麼也就是說,一旦SqlSession關閉,即對象銷毀,必然BaseExecutor對象銷毀,所以一級緩存容器跟着銷毀。由此可以推到出:一級緩存是SqlSession級別的緩存。也就是要命中一級緩存,必須是同一個SqlSession,而且未關閉。

再來看一下一級緩存是如何設置緩存的:

 1 public abstract class BaseExecutor implements Executor {
 2   protected PerpetualCache localCache;
 3   @Override
 4   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) throws SQLException {
 5     BoundSql boundSql = ms.getBoundSql(parameter);
 6     CacheKey key = createCacheKey(ms, parameter, rowBounds, boundSql);
 7     return query(ms, parameter, rowBounds, resultHandler, key, boundSql);
 8   }
 9   @Override
10   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
11     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
12     if (closed) {
13       throw new ExecutorException("Executor was closed.");
14     }
15     if (queryStack == 0 && ms.isFlushCacheRequired()) {
16       clearLocalCache();
17     }
18     List<E> list;
19     try {
20       queryStack++;
21       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
22       if (list != null) {
23         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
24       } else {
25         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
26       }
27     } finally {
28       queryStack--;
29     }
30     if (queryStack == 0) {
31       for (DeferredLoad deferredLoad : deferredLoads) {
32         deferredLoad.load();
33       }
34       // issue #601
35       deferredLoads.clear();
36       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
37         // issue #482
38         clearLocalCache();
39       }
40     }
41     return list;
42   }
43 }

通過這一段源碼,可以看到,是在第6行去構建緩存key,在第21行嘗試獲取緩存。構建緩存key,取決於四個維度:MappedStatement(同一個statementId)、parameter(同樣的查詢參數)、RowBounds(同樣的行數)、BoundsSql(同樣的SQL),加上上邊SqlSession的條件,一級緩存的命中條件為:相同的SqlSession、statementId、parameter、行數、Sql,才能命中一級緩存

這裡在說一句題外話,就是當mybatis與Spring集成時,SqlSession的管理就交給Spring框架了,每次Mybatis的查詢都會由Spring框架新建一個Sqlsession供mybatis用,看起來一級緩存永遠失效。解決辦法就是給查詢加上事務,當加上事務的時候,Spring框架會保證在一個事務裡邊只提供給mybatis同一個SqlSession對象。

再看下一級緩存何時會被刷新掉,來上源碼:

 1 public abstract class BaseExecutor implements Executor {
 2   protected PerpetualCache localCache;
 3   @Override
 4   public void close(boolean forceRollback) {
 5     try {
 6       try {
 7         rollback(forceRollback);
 8       } finally {
 9         if (transaction != null) {
10           transaction.close();
11         }
12       }
13     } catch (SQLException e) {
14       log.warn("Unexpected exception on closing transaction.  Cause: " + e);
15     } finally {
16       transaction = null;
17       deferredLoads = null;
18       localCache = null;
19       localOutputParameterCache = null;
20       closed = true;
21     }
22   }
23   @Override
24   public int update(MappedStatement ms, Object parameter) throws SQLException {
25     ErrorContext.instance().resource(ms.getResource()).activity("executing an update").object(ms.getId());
26     if (closed) {
27       throw new ExecutorException("Executor was closed.");
28     }
29     clearLocalCache();
30     return doUpdate(ms, parameter);
31   }
32   @Override
33   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
34     if (queryStack == 0 && ms.isFlushCacheRequired()) {
35       clearLocalCache();
36     }
37     if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
38       clearLocalCache();
39     }
40   }
41   @Override
42   public void commit(boolean required) throws SQLException {
43     clearLocalCache();
44   }
45 
46   @Override
47   public void rollback(boolean required) throws SQLException {
48     if (!closed) {
49       try {
50         clearLocalCache();
51         flushStatements(true);
52       } finally {
53         if (required) {
54           transaction.rollback();
55         }
56       }
57     }
58   }
59 
60   @Override
61   public void clearLocalCache() {
62     if (!closed) {
63       localCache.clear();
64       localOutputParameterCache.clear();
65     }
66   }

對於這段源碼,清除緩存的場景,着重關注一下clearLocalCache的調用的地方:

即觸發更新操作(第29行)、配置flushCache=true(第35行)、配置緩存作用於為STATEMENT(第38行)、commit時候(第42行)、rollback時候(第50行)、執行器關閉時候(第7行)都會清除一級緩存

 

三、一級緩存對於嵌套子查詢循環依賴場景的解決方案

循環依賴的情況處處可見,比如:一個班主任,下邊有多個學生,每個學生又有一個對應的班主任。

對於班主任和學生這種場景,在mybatis層面屬於典型的嵌套子查詢。mybatis在處理嵌套查詢的時候,都會查詢,然後在設置屬性的時候,如果發現有子查詢,則發起子查詢。那麼,如果不加特殊干預,這種場景將會陷入設置屬性觸發查詢的死循環中。

 1 <select id="selectHeadmasterById" resultMap="teacherMap">
 2     select * from teacher where id = #{id}
 3 </select>
 4 <resultMap id="teacherMap" type="Teacher" autoMapping="true">
 5     <result column="name" property="name"/>
 6     <collection property="students" column="id" select="selectStudentsByTeacherId" fetchType="eager"/>
 7 </resultMap>
 8 <select id="selectStudentsByTeacherId" resultMap="studentMap">
 9     select * from student where teacher_id = #{teacherId}
10 </select>
11 <resultMap id="studentMap" type="comment">
12     <association property="teacher" column="teacher_id" select="selectHeadmasterById" fetchType="eager"/>
13 </resultMap>

mybatis在處理這種情況的時候,巧妙的用了一個臨時一級緩存佔位符與延遲裝載(不同於懶加載),解決了查詢死循環的問題。這裏我們直接上源碼:

每次查詢,如果沒有命中有效緩存(即非佔位符緩存)mybatis都會事先給一級緩存寫入一個佔位符,待數據庫查詢完畢后,再將真正的數據覆蓋掉佔位符緩存。

 1 public abstract class BaseExecutor implements Executor {
 2   protected ConcurrentLinkedQueue<DeferredLoad> deferredLoads;
 3   protected PerpetualCache localCache;
 4   protected int queryStack;
 5   @Override
 6   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 7     ErrorContext.instance().resource(ms.getResource()).activity("executing a query").object(ms.getId());
 8     if (closed) {
 9       throw new ExecutorException("Executor was closed.");
10     }
11     if (queryStack == 0 && ms.isFlushCacheRequired()) {
12       clearLocalCache();
13     }
14     List<E> list;
15     try {
16       queryStack++;
17       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
18       if (list != null) {
19         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
20       } else {
21         // 如果未獲取到緩存則查庫
22         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
23       }
24     } finally {
25       queryStack--;
26     }
27     if (queryStack == 0) {
28       for (DeferredLoad deferredLoad : deferredLoads) {
29         deferredLoad.load();
30       }
31       deferredLoads.clear();
32       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
33         clearLocalCache();
34       }
35     }
36     return list;
37   }
38 }

如上Query方法的第22行進去:

 1 private <E> List<E> queryFromDatabase(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 2     List<E> list;
 3     // 查庫之前先設置佔位符緩存
 4     localCache.putObject(key, EXECUTION_PLACEHOLDER);
 5     try {
 6       list = doQuery(ms, parameter, rowBounds, resultHandler, boundSql);
 7     } finally {
 8       localCache.removeObject(key);
 9     }
10     localCache.putObject(key, list);
11     if (ms.getStatementType() == StatementType.CALLABLE) {
12       localOutputParameterCache.putObject(key, parameter);
13     }
14     return list;
15 }

BaseExecutor.queryFromDataBase方法的第6行,會觸發數據庫查詢,緊接着會進入結果值設定的邏輯。那麼首先會探測有無嵌套的子查詢,如果有,則前一步主查詢暫時等待,立即發起子查詢。

 1   private Object getNestedQueryMappingValue(ResultSet rs, MetaObject metaResultObject, ResultMapping propertyMapping, ResultLoaderMap lazyLoader, String columnPrefix)
 2       throws SQLException {
 3     final String nestedQueryId = propertyMapping.getNestedQueryId();
 4     final String property = propertyMapping.getProperty();
 5     final MappedStatement nestedQuery = configuration.getMappedStatement(nestedQueryId);
 6     final Class<?> nestedQueryParameterType = nestedQuery.getParameterMap().getType();
 7     final Object nestedQueryParameterObject = prepareParameterForNestedQuery(rs, propertyMapping, nestedQueryParameterType, columnPrefix);
 8     Object value = null;
 9     if (nestedQueryParameterObject != null) {
10       final BoundSql nestedBoundSql = nestedQuery.getBoundSql(nestedQueryParameterObject);
11       final CacheKey key = executor.createCacheKey(nestedQuery, nestedQueryParameterObject, RowBounds.DEFAULT, nestedBoundSql);
12       final Class<?> targetType = propertyMapping.getJavaType();
13       // 判斷當前的子查詢是否和之前的某一步主查詢相同
14       if (executor.isCached(nestedQuery, key)) {
15         executor.deferLoad(nestedQuery, metaResultObject, property, key, targetType);
16         value = DEFERRED;
17       } else {
18         final ResultLoader resultLoader = new ResultLoader(configuration, executor, nestedQuery, nestedQueryParameterObject, targetType, key, nestedBoundSql);
19         if (propertyMapping.isLazy()) {
20           lazyLoader.addLoader(property, metaResultObject, resultLoader);
21           value = DEFERRED;
22         } else {
23         // 立即發起子查詢
24           value = resultLoader.loadResult();
25         }
26       }
27     }
28     return value;
29  }

這塊重點關注第13行和第23行。其中第23行又會遞歸到上邊BaseExecutor.query代碼片段的第22行。如果getNestedQueryMappingValue代碼段走的是滴15行邏輯,那麼,會對應BaseExecutor.query代碼片段的第28行。這塊遞歸比較繞。下來做下通俗的解釋:

首先查詢班主任的主查詢給一級緩存寫入一個佔位符緩存,然後去查庫,然後設定屬性,如果沒有嵌套子查詢,那麼到這裏就把設置好屬性的值寫入覆蓋剛才一級佔位符緩存。流暢完畢。

但是恰好有嵌套子查詢,所以查詢班主任的主查詢就停在設置屬性這一步,然後又發起一次查詢,查詢學生,然後又進入查詢學生設定屬性的方法。

設定學生屬性方法又發現又有嵌套子查詢,所以有發起一次學生查詢班主任的查詢操作,又進入到設定屬性這塊,但是發現一級緩存裡邊有前邊住查詢的站位緩存。所以沒有在查庫,而是將本次子查詢放入延遲裝載的容器裡邊。本次子查詢結束。緊接着前一步子查詢(老師查學生)結束。

緊接着查詢老師的住查詢設定屬性完畢,並將自己的結果覆蓋之前寫入的站位緩存。同時啟動了延時裝載的邏輯,延時裝載就是從一級緩存取出剛才查詢老師的一級緩存數據(老師),給第二步子查詢(學生)做一下MetaObject屬性設置。

說的通俗一點:主查詢(查班主任)執行時先寫入站位緩存,緊接着掛起,發起第一個嵌套子查詢(用老師查學生),緊接着該子查詢再掛起,發起學生查老師,但是發現第一步主查詢有一級緩存(站位緩存),那麼本次子查詢自動加入延遲裝載隊列,然後終結改子查詢,等待主查詢真正查完,然後延遲裝載器再從緩存取出數據給第一個子查詢(老師查學生)進行屬性設定。

說了這麼多,肯定暈車了,這裏給出一個時序圖:

總結一下:
1、佔位符緩存作用在於標識與當前查詢相同的前邊的嵌套查詢。比如:查詢學生所屬班主任,發現前邊的主查詢就是查詢班主任,所以就不在執行班主任查詢。等待真正的班主任查詢完畢,我們只需去緩存裡邊取即可。所以我們不執行查詢,只是將本次屬性設置放入延遲裝載隊列即可。
2、queryStack用來記錄當前查詢處於嵌套的第幾層。當queryStack == 0時,證明整個查詢已經回歸到最初的主查詢上,此時,所有過程中需要延遲裝載的對象,都能啟動真實裝載了。
3、一級緩存在解決嵌套子查詢屬性設置循環依賴上啟至關作用。所以以及緩存是不能完全關閉的。但是我們可以設置:LocalCacheScope.STATEMENT,來讓一級緩存及時清空。參見源碼

 

 1   public <E> List<E> query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql) throws SQLException {
 2     // ...
 3     try {
 4       queryStack++;
 5       list = resultHandler == null ? (List<E>) localCache.getObject(key) : null;
 6       if (list != null) {
 7         handleLocallyCachedOutputParameters(ms, key, parameter, boundSql);
 8       } else {
 9         list = queryFromDatabase(ms, parameter, rowBounds, resultHandler, key, boundSql);
10       }
11     } finally {
12       queryStack--;
13     }
14     if (queryStack == 0) {
15       for (DeferredLoad deferredLoad : deferredLoads) {
16         deferredLoad.load();
17       }
18       deferredLoads.clear();
19       // 設置LocalCacheScope.STATEMENT來及時清空緩存
20       if (configuration.getLocalCacheScope() == LocalCacheScope.STATEMENT) {
21         clearLocalCache();
22       }
23     }
24     return list;
25   }

 四、二級緩存

來先上一個二級緩存的執行流程:

 

二級緩存是BaseExecutor的前置增強包裝類CachingExecutor裡邊實現的,即如果從CachingExecutor裡邊命中緩存,則不進行BaseExecutor的派發(如下第14行)。

 1   public <E> List<E> query(MappedStatement ms, Object parameterObject, RowBounds rowBounds, ResultHandler resultHandler, CacheKey key, BoundSql boundSql)
 2       throws SQLException {
 3     Cache cache = ms.getCache();
 4     if (cache != null) {
 5       flushCacheIfRequired(ms);
 6       if (ms.isUseCache() && resultHandler == null) {
 7         ensureNoOutParams(ms, parameterObject, boundSql);
 8         @SuppressWarnings("unchecked")
 9         List<E> list = (List<E>) tcm.getObject(cache, key);
10         if (list == null) {
11           list = delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
12           tcm.putObject(cache, key, list); // issue #578 and #116
13         }
14         return list;
15       }
16     }
17     return delegate.<E> query(ms, parameterObject, rowBounds, resultHandler, key, boundSql);
18   }

  11行與17行的區別在於,是否啟動二級緩存,如果啟動了,則將派發給BaseExecutor的查詢結果寫入暫存區(第12行,TransactionCacheManager),等事務提交在真正刷入二級緩存。下來我們重點來關注一下緩存的讀寫(第9行、第12行),這裏邊真正的執行對象是一系列Cache接口的實現,按職責有:線程安全、日誌記錄、過期清理、溢出淘汰、序列化、執行存儲等等環節。而二級緩存的設計精巧之處就在於此處,完美的按職責進行責任派發,完全解耦。

  接下來我們來看下,默認情況下,緩存責任鏈的初始化過程:

 1   public Cache useNewCache(Class<? extends Cache> typeClass,
 2       Class<? extends Cache> evictionClass,
 3       Long flushInterval,
 4       Integer size,
 5       boolean readWrite,
 6       boolean blocking,
 7       Properties props) {
 8     Cache cache = new CacheBuilder(currentNamespace)
 9         // 這裏設置默認的存儲為內存
10         .implementation(valueOrDefault(typeClass, PerpetualCache.class))
11         // 這裏設置默認的溢出淘汰為LRU
12         .addDecorator(valueOrDefault(evictionClass, LruCache.class))
13         .clearInterval(flushInterval)
14         .size(size)
15         .readWrite(readWrite)
16         .blocking(blocking)
17         .properties(props)
18         .build();
19     configuration.addCache(cache);
20     currentCache = cache;
21     return cache;
22   }

  然後是初始化過程:

 1   public Cache build() {
 2     setDefaultImplementations();
 3     Cache cache = newBaseCacheInstance(implementation, id);
 4     setCacheProperties(cache);
 5     // issue #352, do not apply decorators to custom caches
 6     if (PerpetualCache.class.equals(cache.getClass())) {
 7       for (Class<? extends Cache> decorator : decorators) {
 8         cache = newCacheDecoratorInstance(decorator, cache);
 9         setCacheProperties(cache);
10       }
11       cache = setStandardDecorators(cache);
12     } else if (!LoggingCache.class.isAssignableFrom(cache.getClass())) {
13       cache = new LoggingCache(cache);
14     }
15     return cache;
16   }
17   private Cache setStandardDecorators(Cache cache) {
18     try {
19       MetaObject metaCache = SystemMetaObject.forObject(cache);
20       if (size != null && metaCache.hasSetter("size")) {
21         metaCache.setValue("size", size);
22       }
23       if (clearInterval != null) {
24         cache = new ScheduledCache(cache);
25         ((ScheduledCache) cache).setClearInterval(clearInterval);
26       }
27       if (readWrite) {
28         cache = new SerializedCache(cache);
29       }
30       cache = new LoggingCache(cache);
31       cache = new SynchronizedCache(cache);
32       if (blocking) {
33         cache = new BlockingCache(cache);
34       }
35       return cache;
36     } catch (Exception e) {
37       throw new CacheException("Error building standard cache decorators.  Cause: " + e, e);
38     }
39   }

這裏從第3、8、24、28、30、31、33行分別進行了責任裝飾初始化。這種依據職責分別拆分然後嵌套的解耦方式,其實是一種很成熟的企業級責任派發設計模式。而且形如第8行的循環裝飾嵌套,在很多開源框架中都能見到,比如Dubbo的AOP機制就是這樣初始化的。

下邊直接列一下Mybatis的二級緩存在設計上所覆蓋的功能,以及各功能責任鏈派發的結構圖:

 

 從上邊的代碼可以看出,如果設置了blocking的話,那麼最外層將會包裹BlockingCache、下來是SynchronizedCache,這兩個均是進行線程安全,防止緩存穿透的處理。

 1 public class BlockingCache implements Cache {
 2   private final Cache delegate;
 3   private final ConcurrentHashMap<Object, ReentrantLock> locks;
 4   public BlockingCache(Cache delegate) {
 5     this.delegate = delegate;
 6     this.locks = new ConcurrentHashMap<Object, ReentrantLock>();
 7   }
 8   @Override
 9   public void putObject(Object key, Object value) {
10     try {
11       delegate.putObject(key, value);
12     } finally {
13       releaseLock(key);
14     }
15   }
16   @Override
17   public Object getObject(Object key) {
18     acquireLock(key);
19     Object value = delegate.getObject(key);
20     if (value != null) {
21       releaseLock(key);
22     }        
23     return value;
24   }
25 }
 1 public class SynchronizedCache implements Cache {
 2   private Cache delegate;
 3   @Override
 4   public synchronized void putObject(Object key, Object object) {
 5     delegate.putObject(key, object);
 6   }
 7   @Override
 8   public synchronized Object getObject(Object key) {
 9     return delegate.getObject(key);
10   }
11 }

再看一下負責溢出淘汰的LruCache:

 1 public class LruCache implements Cache {
 2   private final Cache delegate;
 3   private Map<Object, Object> keyMap;
 4   // 記錄當溢出時,需要淘汰的Key
 5   private Object eldestKey;
 6   public void setSize(final int size) {
 7       // LinkedHashMap.accessOrder設置為true,即,每個被訪問的元素會一次放到隊列末尾。當溢出的時候就能從首部來移除了
 8     keyMap = new LinkedHashMap<Object, Object>(size, .75F, true) {
 9       @Override
10       protected boolean removeEldestEntry(Map.Entry<Object, Object> eldest) {
11         boolean tooBig = size() > size;
12         if (tooBig) {
13           eldestKey = eldest.getKey();
14         }
15         return tooBig;
16       }
17     };
18   }
19   @Override
20   public void putObject(Object key, Object value) {
21     delegate.putObject(key, value);
22     cycleKeyList(key);
23   }
24   private void cycleKeyList(Object key) {
25     keyMap.put(key, key);
26     if (eldestKey != null) {
27       delegate.removeObject(eldestKey);
28       eldestKey = null;
29     }
30   }
31 }

二級緩存就講到這裏,總結一下二級緩存件:

1、默認開啟,cachEnable開關。作用於提交后。

2、相同的StatementId。

3、相同的SQL、參數、行數。

4、跨Mapper調用。

 

五、總結

  雖然在目前各種分佈式應用的場景下,一級緩存和二級緩存都有很大概率的臟讀現象,而被禁止,但是Mybatis對這種局部場景的設計是及其精巧的。比如,解決對象循環嵌套查詢的場景設計,其實這種成熟的解決方案也被Spring(也存在對象循環注入的情景)所應用。以及責任裝飾的設計,Dubbo同樣在使用。其實我們能從得到很多啟發,比如,對於既定的業務場景,要加入現成安全的考量,那在不侵入業務代碼的前提下,我們是否也能增加一層責任裝飾,進行派發來完成呢?

 

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

【其他文章推薦】

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

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

※超省錢租車方案

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

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

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

大話性能測試系列(3)- 常用的性能指標

如果你對性能測試感興趣,但是又不熟悉理論知識,可以看下面的系列文章

https://www.cnblogs.com/poloyy/category/1620792.html

 

兩種性能指標

  • 業務指標
  • 技術指標

通常我們會從兩個層面定義性能場景的需求指標,它們有映射關係,技術指標不能脫離業務指標

 

併發

狹義

指同一個時間點執行相同的操作(如:秒殺)

 

廣義

  • 同一時間點,向服務器發起的請求(可能是不同的請求)
  • 只要向服務器發起請求,那麼服務器在這一時間點內都會收到請求(不管是不是同一個請求)

 

場景類比

高速公路上,同時有多少輛車經過同一個關卡,但不一定是同一個牌子的汽車

 

併發用戶數(重點)

同一時間點,發出請求的用戶數,一個用戶可以發出多個請求

 

和併發的關係

假設有 10 個用戶數,每個用戶同一時間點內發起 2 個請求,那麼服務器收到的請求併發數就是 20

 

相關概念

  • 系統用戶數:系統累計註冊用戶數,不一定在線
  • 在線用戶數:在線用戶可能是正常發起請求,也可能只是掛機啥操作都沒有【在線用戶數併發用戶數】
  • 線程數:在 jmeter 中,線程數和併發用戶數等價

 

事務

  • 客戶端向服務器發送請求,然後服務器做出響應的過程
  • 登錄、註冊、下單等功能都屬於一個事務
  • 一個事務可能會發起多個請求

 

jmeter 相關

jmerter 中,默認一個接口請求,就是一個事務;但也支持多個接口整合成一個事務

 

注意點

若一個業務或事務有多個接口,那麼多個單接口的性能指標值相加 業務或事務的性能指標值

 

再來看看有哪些常見的性能指標值

 

響應時間(Respose Time)

概念:從發起請求到收到請求響應的時間

包含:Request Time 和 Response Time

等價:發起請求網絡傳輸時間 + 服務器處理時間 + 返迴響應網絡傳輸時間

 

重點

在做性能測試時,要盡可能的降低網絡傳輸時間,這樣最終得出的 RT 會無限接近服務器處理時間,所以我們要把網絡環境搞好

 

事務請求響應時間

完成單個事務所用的時間,可能包含了多個請求

 

TPS(Transaction Per Second,最主要的指標)

服務器每秒處理事務數,衡量服務器處理能力的最主要指標

 

知道 T 是如何定義的

  • 在不同的行業、業務中,TPS 定義的顆粒度可能是不同的
  • 所以不管什麼情況下,需要做性能測試的業務的相關方都要知道你的 T 是如何定義的 

 

定義 TPS 的粒度

  • 一般會根據場景的目的來定義 TPS 的粒度
  • 接口層性能測試:T 可以定義為接口級
  • 業務級性能測試:T 可以定義為每個業務步驟和完整的業務流

 

栗子

如果要單獨測試接口 1、2、3,那麼 T 就是接口級

如果從用戶角度下訂單,那 1、2、3 都在一個 T 中,就是業務級

結合實際業務設計,庫存服務一定是同步,而積分服務可以是異步,所以這個下單業務,可以只看作由 1、2 這兩個接口組成,但是 3 接口還是要監控分析的

 

所以,性能中 TPS 中 T 的定義取決於場景的目標和 T 的作用

 

拿上圖做個例子

接口級腳本

——事務 start(接口 1)

接口 1 腳本

——事務 end(接口 1)

——事務 start(接口 2)

接口 2 腳本

——事務 end(接口 2)

——事務 start(接口 3)

接口 3 腳本

——事務 end(接口 3)

 

業務級接口層腳本(就是用接口拼接出一個完整的業務流)

——事務 start(業務 A)

接口 1 腳本 – 接口 2(同步調用)

接口 1 腳本 – 接口 3(異步調用)

——事務 end(業務 A)

 

用戶級腳本

——事務 start(業務 A)

點擊 0 – 接口 1 腳本 – 接口 2(同步調用)

點擊 0 – 接口 1 腳本 – 接口 3(異步調用)

——事務 end(業務 A)

 

總結

一般情況下,我們會按從上到下的順序一一來測試,這樣路徑清晰地執行,容易定位問題

 

QPS(Queries per Second)

  • 每秒查詢率,在數據庫中每秒執行 SQL 數量
  • 一個請求可能會執行多條 SQL
  • 某些企業可能會用QPS代替TPS
  • 也是衡量服務端處理能力的一個指標,但不建議使用

 

RPS(Request per Second)

簡單理解

每秒請求數,用戶從客戶端發起的請求數

 

深入挖掘

對於請求數來說,也要看是哪個層面的請求,把上面的圖做一點點變化來描述請求數

如果一個用戶點擊了一次,發出來 3 個 HTTP Request,調用了 2 次訂單服務,調用了 2 次庫存服務,調用了 1 次積分服務

問:Request 數量如何計算

答:3+2+2+1 = 8?不, 應該是 3,因為發出了 3 個 Request,而調用服務會有單獨的描述,以便做性能統計

 

 

HPS(Hit per Second)

  • 點擊率,每秒點擊數
  • 有直接理解為用戶在界面上的點擊次數
  • 一般在性能測試中,都用來描述 HTTP Request,那它代表每秒發送 HTTP 請求的數量,和 RPS 概念完全一樣
  • HPS 越大對 Server 的壓力越大

 

CPS/CPM(Calls Per Second/ Calls Per Minutes)

  • 每秒/每分鐘調用次數
  • 通常用來描述 Service 層的單位時間內被其他服務調用的次數

 

栗子

上圖的訂單服務、庫存服務、積分服務,各調用了2、2、1次,還是比較好理解的

 

TPS、QPS、RPS、HPS、CPS 的總結

有很多維度可以衡量一個系統的性能能力,但是如果把五個指標同時都拿來描述系統性能能力的話,未必太混亂了

 

為此我們可以這樣做

  • TPS 來統一形容系統的性能能力,其他的都在各層面加上限制條件來描述,比如說:接口調用 1000 Calls/s
  • 在團隊中要定義清楚術語的使用場景,還有含義

 

吞吐量(Throughput)

單位時間內,網絡處理的請求數量(事務/s)

網絡沒有瓶頸時,吞吐量≈TPS

 

吞吐率

單位時間內,在網絡傳輸的數據量的平均速率(kB/s)

 

資源利用率

  • 服務器資源的使用程度,比如服務器(應用、服務器)的CPU利用率,內存利用率,磁盤利用率,網絡帶寬利用率
  • 一般不超過80%

 

結尾

本篇博文,部分參考了高老師的《性能測試實戰30講》,因為指標那一塊講的特別好哦~

 

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

【其他文章推薦】

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

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

※回頭車貨運收費標準

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

※超省錢租車方案

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

.NETCore微服務探尋(一) – 網關

前言

一直以來對於.NETCore微服務相關的技術棧都處於一個淺嘗輒止的了解階段,在現實工作中也對於微服務也一直沒有使用的業務環境,所以一直也沒有整合過一個完整的基於.NETCore技術棧的微服務項目。正好由於最近剛好辭職,有了時間可以寫寫自己感興趣的東西,所以在此想把自己了解的微服務相關的概念和技術框架使用實現記錄在一個完整的工程中,由於本人技術有限,所以錯誤的地方希望大家指出。\

項目地址:https://github.com/yingpanwang/fordotnet/tree/dev

什麼是Api網關

  由於微服務把具體的業務分割成單獨的服務,所以如果直接將每個服務都與調用者直接,那麼維護起來將相當麻煩與頭疼,Api網關擔任的角色就是整合請求並按照路由規則轉發至服務的實例,並且由於所有所有請求都經過網關,那麼網關還可以承擔一系列宏觀的攔截功能,例如安全認證,日誌,熔斷

為什麼需要Api網關

 因為Api網關可以提供安全認證,日誌,熔斷相關的宏觀攔截的功能,也可以屏蔽多個下游服務的內部細節

有哪些有名的Api網關項目

  • Zuul Spring Cloud 集成
  • Kong 一款lua輕量級網關項目
  • Ocelot .NETCore網關項目

Ocelot使用

1.通過Nuget安裝Ocelot

2.準備並編輯Ocelot配置信息

Ocelot.json

{
  "ReRoutes": [
    // Auth
    {
      "UpstreamPathTemplate": "/auth/{action}", // 上游請求路徑模板
      "UpstreamHttpMethod": [ "GET", "POST", "PUT", "DELETE" ], // 上游請求方法
      "ServiceName": "Auth", // 服務名稱
      "UseServiceDiscovery": true, // 是否使用服務發現
      "DownstreamPathTemplate": "/connect/{action}", // 下游匹配路徑模板
      "DownstreamScheme": "http", // 下游請求
      "LoadBalancerOptions": { // 負載均衡配置
        "Type": "RoundRobin"
      }
      //,
      // 如果不採用服務發現需要指定下游host
      //"DownstreamHostAndPorts": [
      //  {
      //    "Host": "10.0.1.10",
      //    "Port": 5000
      //  },
      //  {
      //    "Host": "10.0.1.11",
      //    "Port": 5000
      //  }
      //]
    }
  ],
  "GlobalConfiguration": { // 全局配置信息
    "BaseUrl": "http://localhost:5000", // 請求 baseurl 
    "ServiceDiscoveryProvider": { //服務發現提供者
      "Host": "106.53.199.185",
      "Port": 8500,
      "Type": "Consul" // 使用Consul
    }
  }
}

3.添加Ocelot json文件到項目中

將Config目錄下的ocelot.json添加到項目中

4.在網關項目中 StartUp ConfigService中添加Ocelot的服務,在Configure中添加Ocelot的中間件(由於我這裏使用了Consul作為服務發現,所以需要添加Consul的依賴的服務AddConsul,如果不需要服務發現的話可以不用添加)

5.將需要發現的服務通過代碼在啟動時註冊到Consul中

我這裏自己封裝了一個註冊服務的擴展(寫的比較隨意沒有在意細節)

appsettings.json 中添加註冊服務配置信息

"ServiceOptions": {
    "ServiceIP": "localhost",
    "ServiceName": "Auth",
    "Port": 5800,
    "HealthCheckUrl": "/api/health",
    "ConsulOptions": {
      "Scheme": "http",
      "ConsulIP": "localhost",
      "Port": 8500
    }
  }

擴展代碼 ConsulExtensions(注意:3.1中 IApplicationLifetime已廢棄 所以使用的是IHostApplicationLifetime 作為程序生命周期注入的方式)


using Consul;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System;

namespace ForDotNet.Common.Consul.Extensions
{
    /// <summary>
    /// 服務配置信息
    /// </summary>
    public class ServiceOptions
    {
        /// <summary>
        /// 服務ip
        /// </summary>
        public string ServiceIP { get; set; }

        /// <summary>
        /// 服務名稱
        /// </summary>
        public string ServiceName { get; set; }

        /// <summary>
        /// 協議類型http or https
        /// </summary>
        public string Scheme { get; set; } = "http";

        /// <summary>
        /// 端口
        /// </summary>
        public int Port { get; set; }

        /// <summary>
        /// 健康檢查接口
        /// </summary>
        public string HealthCheckUrl { get; set; } = "/api/values";

        /// <summary>
        /// 健康檢查間隔時間
        /// </summary>
        public int HealthCheckIntervalSecond { get; set; } = 10;

        /// <summary>
        /// consul配置信息
        /// </summary>
        public ConsulOptions ConsulOptions { get; set; }
    }

    /// <summary>
    /// consul配置信息
    /// </summary>
    public class ConsulOptions
    {
        /// <summary>
        /// consul ip
        /// </summary>
        public string ConsulIP { get; set; }

        /// <summary>
        /// consul 端口
        /// </summary>
        public int Port { get; set; }

        /// <summary>
        /// 協議類型http or https
        /// </summary>
        public string Scheme { get; set; } = "http";
    }

    /// <summary>
    /// consul註冊客戶端信息
    /// </summary>
    public class ConsulClientInfo
    {
        /// <summary>
        /// 註冊信息
        /// </summary>
        public AgentServiceRegistration RegisterInfo { get; set; }

        /// <summary>
        /// consul客戶端
        /// </summary>
        public ConsulClient Client { get; set; }
    }

    /// <summary>
    /// consul擴展(通過配置文件配置)
    /// </summary>
    public static class ConsulExtensions
    {
        private static readonly ServiceOptions serviceOptions = new ServiceOptions();

        /// <summary>
        /// 添加consul
        /// </summary>
        public static void AddConsulServiceDiscovery(this IServiceCollection services)
        {
            var config = services.BuildServiceProvider().GetService<IConfiguration>();
            config.GetSection("ServiceOptions").Bind(serviceOptions);
            //config.Bind(serviceOptions);

            if (serviceOptions == null)
            {
                throw new Exception("獲取服務註冊信息失敗!請檢查配置信息是否正確!");
            }
            Register(services);
        }

        /// <summary>
        /// 添加consul(通過配置opt對象配置)
        /// </summary>
        /// <param name="app"></param>
        /// <param name="life">引用生命周期</param>
        /// <param name="options">配置參數</param>
        public static void AddConsulServiceDiscovery(this IServiceCollection services, Action<ServiceOptions> options)
        {
            options.Invoke(serviceOptions);
            Register(services);
        }

        /// <summary>
        /// 註冊consul服務發現
        /// </summary>
        /// <param name="app"></param>
        /// <param name="life"></param>
        public static void UseConsulServiceDiscovery(this IApplicationBuilder app, IHostApplicationLifetime life)
        {
            var consulClientInfo = app.ApplicationServices.GetRequiredService<ConsulClientInfo>();
            if (consulClientInfo != null)
            {
                life.ApplicationStarted.Register( () =>
                {
                     consulClientInfo.Client.Agent.ServiceRegister(consulClientInfo.RegisterInfo).Wait();
                });

                life.ApplicationStopping.Register( () =>
                {
                     consulClientInfo.Client.Agent.ServiceDeregister(consulClientInfo.RegisterInfo.ID).Wait();
                });
            }
            else
            {
                throw new NullReferenceException("未找到相關consul客戶端信息!");
            }
        }

        private static void Register(this IServiceCollection services)
        {
            if (serviceOptions == null)
            {
                throw new Exception("獲取服務註冊信息失敗!請檢查配置信息是否正確!");
            }
            if (serviceOptions.ConsulOptions == null)
            {
                throw new ArgumentNullException("請檢查是否配置Consul信息!");
            }

            string consulAddress = $"{serviceOptions.ConsulOptions.Scheme}://{serviceOptions.ConsulOptions.ConsulIP}:{serviceOptions.ConsulOptions.Port}";

            var consulClient = new ConsulClient(opt =>
            {
                opt.Address = new Uri(consulAddress);
            });

            var httpCheck = new AgentServiceCheck()
            {
                DeregisterCriticalServiceAfter = TimeSpan.FromSeconds(10), // 服務啟動多久后註冊
                Interval = TimeSpan.FromSeconds(serviceOptions.HealthCheckIntervalSecond), // 間隔
                HTTP = $"{serviceOptions.Scheme}://{serviceOptions.ServiceIP}:{serviceOptions.Port}{serviceOptions.HealthCheckUrl}",
                Timeout = TimeSpan.FromSeconds(10)
            };

            var registration = new AgentServiceRegistration()
            {
                Checks = new[] { httpCheck },
                ID = Guid.NewGuid().ToString(),
                Name = serviceOptions.ServiceName,
                Address = serviceOptions.ServiceIP,
                Port = serviceOptions.Port,
            };

            services.AddSingleton(new ConsulClientInfo()
            {
                Client = consulClient,
                RegisterInfo = registration
            });
        }
    }
}

6.啟動運行

  • 啟動consul
  • 啟動 Auth,Gateway項目
  • 通過網關項目訪問Auth

啟動Consul

為了方便演示這裡是以開發者啟動的consul
在consul.exe的目錄下執行
consul agent -dev -ui // 開發者模式運行帶ui

啟動 Auth,Gateway項目

啟動項目和可以發現我的們Auth服務已經註冊進來了

通過網關訪問Auth

我們這裏訪問 http://localhost:5000/auth/token 獲取token

我們可以看到網關項目接收到了請求並在控制台中打印出以下信息

然後在Auth項目中的控制台中可以看到已經成功接收到了請求並響應

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

【其他文章推薦】

※超省錢租車方案

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

※回頭車貨運收費標準

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

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

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