標籤: 貨運

  • 無異常日誌,就不能排查問題了???

    無異常日誌,就不能排查問題了???

    小聲逼逼

    眾所周知,日誌是排查問題的重要手段。關於日誌設計,以及怎麼根據從【用戶報障】環節開始到秒級定位問題這個我們下一期說(絕非套路),這一期,主要講一下,在沒有異常日誌的情況下,如何定位問題。沒有日誌當真能排查問題,不會是標題黨吧!

    案例一

    從最大的同性交友網站中拉取【dubbo-spring-boot-project】的代碼。

    然後把demo跑起來。

    本場景是由真實案例改編,因為公司代碼比較複雜也不方便透露,而這個demo在github上大家都能找到,既保證了原汁原味,又能讓大家方便自己體驗排查過程。

    好了,我們先設置owner = "feichao",然後看一下控制台

    一切正常

    那麼,當我設置成owner = "feichaozhenshuai!",再啟動

    看似一切都正常,那麼,我們到控制台一看。

    什麼情況,怎麼就沒owner了?

    這是在哪個環節出問題了?其實肥朝當初在公司遇到這個問題的時候,場景比這個複雜得多。因為公司的業務里沒有owner的話,在運行時會出現一些其他異常,涉及公司業務這裏就不展開了,我們言歸正傳,為毛我設置成feichaozhenshuai!就不行了,那我設置成肥朝大帥比電腦會不會爆炸啊???

    常見的錯誤做法是,把這個問題截圖往群里一丟,問“你們有沒有遇到過dubbo裏面,owner設置不生效的問題?”

    而關注了肥朝公眾號的【真愛粉絲】會這麼問,“dubbo裏面設置owner卻不生效,你們覺得我要從個角度排查問題?”。一看到這麼正確的提問方式,我覺得我不回復你都不好意思。好了,回到主題,這個時候,沒有一點點錯誤日誌,但是卻設置不成功,我們有哪些排查手段?

    套路一

    直接找set方法,看看是不是代碼做了判斷,防止在owner字段裏面set肥朝真帥這種詞語,避免把帥這件事走漏風聲!。這麼一分析似乎挺有道理對吧,那麼,如何快速找到這個set方法呢?如圖

    public void setOwner(String owner) {
        checkMultiName("owner", owner);
        this.owner = owner;
    }
    

    我們跟進checkMultiName代碼后發現

    protected static void checkProperty(String property, String value, int maxlength, Pattern pattern) {
        if (StringUtils.isEmpty(value)) {
            return;
        }
        if (value.length() > maxlength) {
            throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" is longer than " + maxlength);
        }
        if (pattern != null) {
            Matcher matcher = pattern.matcher(value);
            if (!matcher.matches()) {
                throw new IllegalStateException("Invalid " + property + "=\"" + value + "\" contains illegal " +
                        "character, only digit, letter, '-', '_' or '.' is legal.");
            }
        }
    }
    

    從異常描述就很明顯可以看出,原來owner裏面是只支持-_等這類特殊符號,!是不支持的,所以設置成不成功,和肥朝帥不帥是沒關係的,和後面的!是有關係的。擦,原來是肥朝想多了,給自己加戲了!!!

    當然肥朝可以告訴你,在後面的版本,修復了這個bug,日誌會看得到異常了。這個時候你覺得問題就解決了?

    我相信此時很多假粉就會關掉文章,或者說下次肥朝發了一些他們不喜歡看的文章(你懂的)后,他們就從此取關,但是肥朝想說,且慢動手!!!

    你想嘛,萬一你以後又遇到類似的問題呢?而且源碼層次很深,就不是簡單的搜個set方法這麼簡單,這次給你搜到了set方法並解決問題,簡直是偶然成功。因此,我才多次強調,要持續關注肥朝,掌握更多套路。這難道是想騙你關注?我這分明是愛你啊!

    那麼,萬一以後遇到一些吞掉異常,亦或者某些原因導致日誌沒打印,我們到底如何排查?

    套路二

    我們知道idea裏面有很多好用的功能,比如肥朝之前的【看源碼,我為何推薦idea?】中就提到了條件斷點,除此之外,還有一個被大家低估的功能,叫做異常斷點

    肥朝掃了一眼,裏面的單詞都是小學的英語單詞,因此怎麼使用就不做過多解釋。遇到這個問題時,我們可以這樣設置異常斷點。

    運行起來如下:

    這樣,運行起來的時候,就會迅速定位到異常位置。然後一頓分析,應該很容易找出問題。

    是不是有點感覺了?那我們再來一個題型練習一下。

    案例二

    我們先在看之前肥朝粉絲群的提問

    考慮到部分粉絲不在群里,我就簡單描述一下這個粉絲的問題,他代碼有個異常,然後catch打異常日誌,但是日誌卻沒輸出。

    當然你還是不理解也沒關係,我根據該粉絲的問題,給你搭建了一個最簡模型的demo,模型雖然簡單,但是問題是同樣的,原汁原味,熟悉的配方,熟悉的味道。git地址如下:【https://gitee.com/HelloToby/springboot-run-exception】

    我們運行起來看一下

    @Slf4j
    public class HelloSpringApplicationRunListener implements SpringApplicationRunListener {
    
        public HelloSpringApplicationRunListener(SpringApplication application, String[] args) {
        }
    
        @Override
        public void starting() {
    
        }
    
        @Override
        public void environmentPrepared(ConfigurableEnvironment environment) {
    
        }
    
        @Override
        public void contextPrepared(ConfigurableApplicationContext context) {
            throw new RuntimeException("歡迎關注微信公眾號【肥朝】");
        }
    
        @Override
        public void contextLoaded(ConfigurableApplicationContext context) {
    
        }
    
        @Override
        public void finished(ConfigurableApplicationContext context, Throwable exception) {
        }
    }
    

    你會發現,一運行起來進程就停止,一點日誌都沒。絕大部分假粉絲遇到這個情況,都是菊花一緊,一點頭緒都沒,又去群里問”你們有沒有遇到過,Springboot一起來進程就沒了,但是沒有日誌的問題?“。正確提問姿勢肥朝已經強調過,這裏不多說。那麼我們用前面學到的排查套路,再來走一波

    我們根據異常棧順藤摸瓜

    我們從代碼中看出兩個關鍵單詞【reportFailure】、【context.close()】,經過斷點我們發現,確實是會先打印日誌,再關掉容器。但是為啥日誌先執行,再關掉容器,日誌沒輸出,容器就關掉了呢?因為,這個demo中,日誌是全異步日誌,異步日誌還沒執行,容器就關了,導致了日誌沒有輸出。

    該粉絲遇到的問題是類似的,他是單元測試中,代碼中的異步日誌還沒輸出,單元測試執行完進程就停止了。知道了原理解決起來也很簡單,比如最簡單的,跑單元測試的時候末尾先sleep一下等日誌輸出。

    在使用Springboot中,其實經常會遇到這種,啟動期間出現異常,但是日誌是異步的,日誌還沒輸出就容器停止,導致沒有異常日誌。知道了原理之後,要徹底解決這類問題,可以增加一個SpringApplicationRunListener

    /**
     * 負責應用啟動時的異常輸出
     */
    @Slf4j
    public class OutstandingExceptionReporter implements SpringApplicationRunListener {
    
        public OutstandingExceptionReporter(SpringApplication application, String[] args) {
        }
    
        @Override
        public void starting() {
    
        }
    
        @Override
        public void environmentPrepared(ConfigurableEnvironment environment) {
    
        }
    
        @Override
        public void contextPrepared(ConfigurableApplicationContext context) {
    
        }
    
        @Override
        public void contextLoaded(ConfigurableApplicationContext context) {
    
        }
    
        @Override
        public void finished(ConfigurableApplicationContext context, Throwable exception) {
            if (exception != null) {
                log.error("application started failed",exception);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    log.error("application started failed", e);
                }
            }
        }
    }
    

    再啰嗦一句,其實日誌輸出不了,除了這個異步日誌的案例外,還有很多情況的,比如日誌衝突之類的,排查套路還很多,因此,建議持續關注,每一個套路,都想和你分享!

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • 2020科學界聯合報告:武肺未阻氣候變遷 溫室氣體濃度創300萬年新高

    2020科學界聯合報告:武肺未阻氣候變遷 溫室氣體濃度創300萬年新高

    環境資訊中心外電;姜唯 翻譯;林大利 審校;稿源:ENS

    儘管全球為防堵武漢肺炎(COVID-19)而大規模封城,大氣中的溫室氣體濃度卻仍來到300萬年來最高。

    氣候變遷沒有因為武漢肺炎而停下腳步。封城和經濟趨緩雖使碳排放出現暫時性下降,整體趨勢仍朝著肺炎爆發前的水準邁進。

    2020年二氧化碳排放量將因為疫情關係減少4%至7%。確切能減少多少將取決於疫情控制情況和政府的應對措施。

    今年雖然碰上疫情而大規模封城,大氣中的溫室氣體濃度卻仍來到300萬年來最高。照片來源:Tony Webster(CC BY-SA 2.0)

    2016至2020年將是有史以來最熱的五年

    全球最大、最具權威性的多個科學組織合作發表「2020科學界聯合報告(United in Science 2020)」,彙整出全面性的相關資訊。

    這份報告是本系列報告的第二份,由世界氣象組織(World Meteorological Organization﹐WMO)協調,收集來自全球碳計畫(Global Carbon Project)、政府間氣候變遷專門委員會(Intergovernmental Panel on Climate Change)、聯合國教科文組織政府間海洋學委員會(Intergovernmental Oceanographic Commission of UNESCO)、聯合國環境規劃署(UN Environment Programme﹐UNEP)和英國氣象局的專業意見 。

    「溫室氣體濃度已經達到300萬年來的最高水準,並持續上升中。同時在2020年上半年,西伯利亞大片地區出現長時間的異常熱浪,若不是人為的氣候變遷,這幾乎不可能發生。2016至2020年將是有史以來最熱的五年。」WMO秘書長塔拉斯(Petteri Taalas)教授警告,「這份報告說明了,儘管我們的生活在2020年多方被打亂,但氣候變遷的影響力並未減弱。」

    暖化趨勢很可能會持續 使巴黎協定無法達成

    乾旱和熱浪大幅增加了野火風險。有史以來野火造成的三次最大經濟損失都發生在最近四年。2019年和2020年夏季,北極地區發生了前所未有的野火。2019年6月,這些野火向大氣排放了5000萬噸二氧化碳,造成永凍土融化。2019年和2020年,亞馬遜雨林發生了大火,對環境造成了巨大影響。

    2020科學界聯合報告引用的「世界天氣歸因」最近的一項研究結果指出,由於人為氣候變遷,2020年1月至2020年6月的高溫可能性至少高出600倍。

    報告中記載的暖化趨勢很可能會持續下去,使全世界無法實現2015年巴黎協定設定的氣候目標,全球氣溫上升幅度遠低於工業化前水準2°C或僅比工業化高前1.5°C。

    報告提供與氣候變遷相關的最新科學資料和發現,作為全球政策和行動的指引。內容聚焦氣候變遷的影響日益增加且不可逆轉,它影響冰川、海洋、自然、經濟和人類生活條件,人類往往可從乾旱或洪水等與水有關的危害切身感受到。

    報告也記載了武漢肺炎如何破壞我們透過全球觀測系統監測這些變化的能力。

    有史以來野火造成的三次最大經濟損失都發生在最近四年。照片來源: Ulet Ifansasti/Greenpeace(CC BY-NC-ND 2.0)

    2020科學界聯合報告的主要發現

    大氣中的溫室氣體濃度(世界氣象組織)

    大氣中的二氧化碳濃度沒有要封頂的跡象,一直在打破紀錄。

    根據WMO全球大氣監測網水準點的報告,2020年上半年二氧化碳濃度高於410 ppm,夏威夷冒納羅亞和澳洲塔斯馬尼亞格里姆角的觀測值分別為414.38 ppm和410.04 ppm。2020年比2019年7月增加了約3 ppm。

    2020年二氧化碳排放量的減少只會輕微影響大氣中濃度的增加速度,因為今日大氣中二氧化碳濃度是過去和當前排放以及二氧化碳超長壽命所致結果。

    WMO在其報告中表示:「要使氣候變遷穩定下來,必須將排放量持續減少至零淨值。」

    全球化石燃料二氧化碳排放量(全球碳計畫)

    由於武肺封鎖,2020年二氧化碳排放量預計將下降4%至7%。確切的下降百分比將取決於疫情控制狀況和政府因應方式。

    2020年4月上旬的封城高峰期,全球每日化石燃料二氧化碳排放量與2019年相比下降了前所未有的17%。

    但儘管如此,排放量仍與2006年的水準相當,凸顯過去15年來的急劇增長及長期依賴化石能源。

    到2020年6月上旬,全球每日化石燃料排放量只比2019年水準低了5%不到,去年達到了367億公噸的新紀錄,比1990年氣候變遷談判開始時高62%。

    過去10年間,人類活動產生的全球甲烷排放量也在持續增加。報告警告:「目前的二氧化碳和甲烷排放趨勢均無法達到巴黎協定目標。」

    排放差距(聯合國環境規劃署)

    聯合國環境規劃署呼籲,要實現巴黎協定目標,轉型行動不能再延。

    環境署「2019年排放差距報告」顯示,從2020~2030年,要達到巴黎協定的2°C目標,每年要將全球排放量削減3%,要達到1.5°C目標平均每年要削減7%以上。

    根據目前的預估,2030年與2°C目標的排放差距為120~150億公噸二氧化碳當量(CO2e),與1.5°C目標的排放差距為29~32吉噸二氧化碳當量,大約等於六個最大排放國的排放總量。

    環境署說:「仍然有可能縮小這個排放差距,但需要所有國家和所有部門立即協調一致的行動……短期來說可以透過擴大現有的有效的政策來實現,例如再生能源和能源效率、低碳運輸以及逐步淘汰煤炭的政策。」

    全球氣候狀況(WMO和英國氣象局)

    2016~2020年的全球平均溫度將是有記錄以來最高溫,比前工業化時代參考期1850~1900年高出約1.1°C,比2011~2015年的全球平均溫度高出0.24°C。

    在2020年至2024年這五年期間,至少有一年比工業化前水準高出1.5°C以上的機率是24%,五年平均值超過該水準的機會很小(3%)。兩家機構在報告中表示:「未來五年內,有70%的機率有一個或多個月的氣溫可能比工業化前高至少1.5°C。」

    2016年至2020年間的每一年,北極海冰面積都低於平均水準。

    2016至2019年的冰川質量損失均大於1950年以來的每個五年期。

    2011至2015年和2016至2020年這兩個五年相比,全球平均海平面上升速度有所提高。

    氣候變遷下的海洋和冰凍圈(政府間氣候變遷專門委員會)

    人為氣候變遷正在影響從山頂到海洋深處的生命維持系統,導致海平面上升加快,對生態系統和人類安全產生連鎖反應,也對適應和綜合風險管理造成嚴峻挑戰。

    全球的冰蓋和冰川正在消失。1979年至2018年間,一年之中每個月的北極海冰範圍都一直在減少。野火增加、永凍土突然融化以及北極和山區水文的變化,已經改變了生態系統擾動的頻率和強度。

    1970年以來,全球海洋暖化不停息,並吸收了氣候系統90%以上的多餘熱量。自1993年以來,海洋暖化的速度和所吸收的來自氣候系統的熱量增加了一倍以上。

    海洋熱浪的頻率增加了一倍,持續時間更長、強度更大、範圍更廣,導致大規模珊瑚白化。自1980年代以來,海洋吸收了人為二氧化碳總排放量的20%至30%,使海洋進一步酸化。

    自大約1950年以來,海洋暖化、海冰變化和氧氣流失,許多海洋物種的分佈範圍和季節性活動發生了變化。

    由於格陵蘭和南極冰蓋的冰流失率增加、冰川持續流失和海洋熱膨脹,近幾十年來海平面加速上升。2006至2015年全球平均海平面上升速度為每年3.6±0.5公釐,這在上個世紀前所未見。

    全球的冰蓋和冰川正在消失。美國冰河灣國家公園。照片來源:mulf(CC BY-NC-ND 2.0)

    氣候與水資源(WMO)

    氣候變遷最明顯的影響出現在水文條件的變化,包括冰雪動力學的變化。

    到2050年,受洪水威脅的人數將從目前的12億增加到16億。在2010年代初到中期,有19億人(全球人口的27%)生活在可能缺水的地區。到2050年,這個數字將增加到27~32億。

    截至2019年,全世界有12%人口的飲用水來自未經改進和不安全的水源。全世界有30%以上的人口(即24億人)沒有任何形式的衛生設施。

    氣候變遷將使更多地區缺水,已經缺水的地區將更嚴重。

    冰凍圈是山區及其下游地區的重要淡水來源。學界認為,冰川的年徑流最晚將在21世紀末達到全球最高峰。此後全球冰川徑流將減少,影響水的儲存。

    據估計,中歐和高加索地區現已達到最高水位,青藏高原地區將在2030年至2050年達到最高水位。隨著積雪融化形成徑流,該地區的永凍土和冰川佔河流總流量的45%,流量減少將影響17億人的用水。

    武肺期間的地球系統觀測(教科文組織和WMO政府間海洋學委員會)

    武漢肺炎嚴重影響全球觀測工作,進而影響預報以及其他天氣、氣候和海洋相關服務的品質。

    3月和4月的飛行器觀測工作平均減少了75%至80%,降低了天氣模型的預報能力。自6月以來僅略有恢復。人工操作的氣象站觀測工作也受到嚴重干擾,尤其在非洲和南美。

    諸如河流流量之類的水文觀測情況也類似。自動化系統可以繼續傳遞數據,而人工讀取的測量站受到影響。

    2020年3月,幾乎所有海洋學研究船都被召回國籍港口。商用船無法提供重要的海洋和天氣觀測資料,並且無法維護海洋浮標和其他系統。每10年要進行四次的全深度海洋調查,包括碳、溫度、鹽度和水鹼度等變量偵測,都取消了。提供溫室氣體排放資訊的的船舶表面碳測量工作也幾乎停止。

    這對氣候變遷監測的影響是長期的,可能會阻礙或限制融冰期結束時進行的冰川質量變化或永凍土厚度的測量活動。觀測活動中斷將使基本氣候變量的歷史時間序列產生斷層,不利監測氣候變動和變遷以及相關影響。

    Climate Change Intensifies Despite Pandemic Lockdowns GENEVA, Switzerland, September 10, 2020 (ENS)

    Already at their highest levels in three million years, greenhouse gas concentrations in the atmosphere continue to increase, lockdowns around the world to slow the spread of the pandemic coronavirus have forced vehicles to stay parked, making way for clearer skies – temporarily.

    But climate change has not stopped for COVID-19. Emissions are heading in the direction of pre-pandemic levels following a temporary decline caused by the lockdown and economic slowdown.

    In 2020, emissions of the greenhouse gas carbon dioxide (CO2) are projected to fall by an estimated four to seven percent due to COVID-19 confinement policies. The exact drop in atmospheric CO2 will depend on the trajectory of the pandemic and government responses to address it.

    These facts are contained in a new multi-agency report from the world’s largest and most respected scientific organizations, “United in Science 2020.”

    The report, the second in a series, was coordinated by the World Meteorological Organization, WMO, with input from the Global Carbon Project, the Intergovernmental Panel on Climate Change, the Intergovernmental Oceanographic Commission of UNESCO, the UN Environment Programme and the UK Met Office.

    WMO Secretary-General Professor Petteri Taalas warned, “Greenhouse gas concentrations – which are already at their highest levels in three million years – have continued to rise. Meanwhile, large swathes of Siberia have seen a prolonged and remarkable heatwave during the first half of 2020, which would have been very unlikely without anthropogenic climate change. And now 2016–2020 is set to be the warmest five-year period on record.

    “This report shows that whilst many aspects of our lives have been disrupted in 2020, climate change has continued unabated,” Taalas said.

    “Major impacts have been caused by extreme weather and climate events. A clear fingerprint of human-induced climate change has been identified on many of these extreme events,” the WMO and UN Met Office say in the report.

    Drought and heatwaves substantially increased the risk of wildfires. The three largest economic losses on record from wildfires have all occurred in the last four years. Summer 2019 and 2020 saw unprecedented wildfires in the Arctic region. In June 2019, these fires emitted 50 million tonnes of CO2 into the atmosphere and caused the loss of permafrost. In 2019 and 2020 there were also widespread fires in the Amazon rainforest, with dramatic environmental impacts.

    The results of a recent study by World Weather Attribution cited in “United in Science 2020” showed with high confidence that the January to June 2020 heat is at least 600 times more likely as a result of human-induced climate change.

    The warming trend documented in this report is likely to continue, and the world is not on track to meet targets set in the 2015 Paris Agreement on climate to keep the global temperature increase well below 2°C or at 1.5°C above pre-industrial levels.

    “United in Science 2020” presents the latest scientific data and findings related to climate change to inform global policy and action. It highlights the increasing and irreversible impacts of climate change, which affects glaciers, oceans, nature, economies and human living conditions and is often felt through water-related hazards such as drought or flooding.

    It also documents how COVID-19 has impeded our ability to monitor these changes through the global observing system.

    “This has been an unprecedented year for people and planet. The COVID-19 pandemic has disrupted lives worldwide. At the same time, the heating of our planet and climate disruption has continued apace,” said UN Secretary-General António Guterres in a foreword to the report.

    “Never before has it been so clear that we need long-term, inclusive, clean transitions to tackle the climate crisis and achieve sustainable development. We must turn the recovery from the pandemic into a real opportunity to build a better future,” said Guterres, who presented the report to the UN on Wednesday. “We need science, solidarity and solutions.”

    KEY FINDINGS FROM “UNITED IN SCIENCE 2020”

    Greenhouse Gas Concentrations in the Atmosphere (World Meteorological Organization)

    Atmospheric CO2 concentrations showed no signs of peaking and have continued to increase to new records.

    Benchmark stations in the WMO Global Atmosphere Watch network reported CO2 concentrations above 410 parts per million (ppm) during the first half of 2020, with observations from Mauna Loa, Hawaii and Cape Grim, Tasmania at 414.38 ppm and 410.04 ppm, respectively, in July 2020, up about three parts per million from July 2019.

    Reductions in emissions of CO2 in 2020 will only slightly impact the rate of increase in the atmospheric concentrations, which are the result of past and current emissions, as well as the very long lifetime of CO2.

    “Sustained reductions in emissions to net zero are necessary to stabilize climate change,” the WMO states in its report.

    Global Fossil CO2 emissions (Global Carbon Project)

    CO2 emissions in 2020 will fall by an estimated four percent to seven percent due to COVID-19 confinement policies. The exact percent of decline will depend on the trajectory of the pandemic and government responses to address it.

    During peak lockdown in early April 2020, the daily global fossil CO2 emissions dropped by an unprecedented 17 percent compared to 2019.

    But even so, emissions were still equivalent to 2006 levels, highlighting both the steep growth over the past 15 years and the continued dependence on fossil sources for energy.

    By early June 2020, global daily fossil CO2 emissions had mostly returned to within five percent below 2019 levels, which reached a new record of 36.7 gigatonnes last year, 62 percent higher than at the start of climate change negotiations in 1990.

    Global methane emissions from human activities, too, have continued to increase over the past decade. “Current emissions of both CO2 and methane are not compatible with emissions pathways consistent with the targets of the Paris Agreement,” the report warns.

    Emissions Gap (UN Environment Programme)

    “Transformational action can no longer be postponed if the Paris Agreement targets are to be met,” urges the UN Environment Programme.

    The UNEP Emissions Gap Report 2019 showed that the cuts in global emissions required per year from 2020 to 2030 are close to three percent for a 2°C target and more than seven percent per year on average for the 1.5°C goal of the Paris Agreement.

    The Emissions Gap in 2030 is estimated at 12-15 gigatonnes (Gt) of carbon dioxide equivalent (CO2e) to limit global warming to below 2°C. For the 1.5°C goal, the gap is estimated at 29-32 Gt CO2e, roughly equivalent to the combined emissions of the six largest-emitting countries.

    “It is still possible to bridge the emissions gap, but this will require urgent and concerted action by all countries and across all sectors,” UNEP said.

    “A substantial part of the short-term potential can be realized through scaling up existing, well-proven policies, for instance on renewables and energy efficiency, low carbon transportation means and a phase-out of coal,” the UN agency said.

    Technically and economically feasible solutions already exist, said UNEP. Looking beyond the 2030 timeframe, new technological solutions and gradual change in consumption patterns are needed at all levels.

    State of Global Climate (WMO and UK’s Met Office)

    The average global temperature for 2016–2020 is expected to be the warmest on record, about 1.1°C above 1850-1900, a reference period for temperature change since pre-industrial times and 0.24°C warmer than the global average temperature for 2011-2015.

    In the five-year period 2020–2024, the chance of at least one year exceeding 1.5°C above pre-industrial levels is 24 percent, with a very small chance (three percent) of the five-year mean exceeding this level. “It is likely (~70 percent chance) that one or more months during the next five years will be at least 1.5 °C warmer than pre-industrial levels,” the two agencies said in the report.

    In every year between 2016 and 2020, the Arctic sea ice extent has been below average.

    The years 2016–2019 recorded a greater glacier mass loss than all other past five-year periods since 1950.

    The rate of global mean sea-level rise increased between the five-year periods 2011–2015 and 2016–2020.

    The Ocean and Cryosphere in a Changing Climate (Intergovernmental Panel on Climate Change)

    Human-induced climate change is affecting life-sustaining systems, from the top of the mountains to the depths of the oceans, leading to accelerating sea-level rise, with cascading effects for ecosystems and human security.

    This increasingly challenges adaptation and integrated risk management responses.

    Ice sheets and glaciers worldwide have lost mass. Between 1979 and 2018, Arctic sea-ice extent has decreased for all months of the year. Increasing wildfire and abrupt permafrost thaw, as well as changes in Arctic and mountain hydrology, have altered the frequency and intensity of ecosystem disturbances.

    The global ocean has warmed unabated since 1970 and has taken up more than 90 percent of the excess heat in the climate system. Since 1993 the rate of ocean warming, and thus heat uptake has more than doubled.

    Marine heatwaves have doubled in frequency and have become longer-lasting, more intense and more extensive, resulting in large-scale coral bleaching events. The ocean has absorbed between 20 percent to 30 percent of total anthropogenic CO2 emissions since the 1980s causing further ocean acidification.

    Since about 1950 many marine species have undergone shifts in geographical range and seasonal activities in response to ocean warming, sea-ice change and oxygen loss.

    The global mean sea-level is rising, with acceleration in recent decades due to increasing rates of ice loss from the Greenland and Antarctic ice sheets, as well as continued glacier mass loss and ocean thermal expansion. The rate of global mean sea-level rise for 2006–2015 of 3.6 ±0.5 mm/yr is unprecedented over the last century

    Climate and Water Resources (WMO)

    Climate change impacts are most felt through changing hydrological conditions including changes in snow and ice dynamics.

    By 2050, the number of people at risk of floods will increase from its current level of 1.2 billion to 1.6 billion. In the early to mid-2010s, 1.9 billion people, or 27 percent of the global population, lived in potentially water-scarce areas. In 2050, this number will increase to 2.7 to 3.2 billion people.

    As of 2019, 12 percent of the world population drinks water from unimproved and unsafe sources. More than 30 percent of the world population, or 2.4 billion people, live without any form of sanitation.

    Climate change is projected to increase the number of water-stressed regions and exacerbate shortages in already water-stressed regions.

    The cryosphere is an important source of freshwater in mountains and their downstream regions. There is high confidence that annual runoff from glaciers will reach peak globally at the latest by the end of the 21st century. After that, glacier runoff is projected to decline globally with implications for water storage.

    It is estimated that Central Europe and Caucasus have reached peak water now and that the Tibetan Plateau region will reach peak water between 2030 and 2050. As runoff from snow cover, permafrost and glaciers in this region provides up to 45 percent of the total river flow, the flow decrease would affect water availability for 1.7 billion people.

    Earth System Observations during COVID-19 (Intergovernmental Oceanographic Commission of UNESCO and WMO)

    The COVID-19 pandemic has produced significant impacts on the global observing systems, which in turn have affected the quality of forecasts and other weather, climate and ocean-related services.

    The reduction of aircraft-based observations by an average of 75 percent to 80 percent in March and April degraded the forecast skills of weather models. Since June, there has been only a slight recovery. Observations at manually operated weather stations, especially in Africa and South America, have also been badly disrupted.

    For hydrological observations like river discharge, the situation is similar to that of atmospheric in situ measurements. Automated systems continue to deliver data whereas gauging stations that depend on manual reading are affected.

    In March 2020, nearly all oceanographic research vessels were recalled to home ports. Commercial ships have been unable to contribute vital ocean and weather observations, and ocean buoys and other systems could not be maintained. Four full-depth ocean surveys of variables such as carbon, temperature, salinity, and water alkalinity, completed only once per decade, have been canceled. Surface carbon measurements from ships, which tell us about the evolution of greenhouse gases, also effectively ceased.

    The impacts on climate change monitoring are long-term. They are likely to prevent or restrict measurement campaigns for the mass balance of glaciers or the thickness of permafrost, usually conducted at the end of the thawing period. The overall disruption of observations will introduce gaps in the historical time series of Essential Climate Variables needed to monitor climate variability and change and associated impacts.

    ※ 全文及圖片詳見:ENS

    溫室氣體
    冰川崩塌
    熱浪
    極端高溫
    山林野火
    極圈
    疫情看氣候與能源
    深度低碳專題
    國際新聞
    氣候變遷

    作者

    姜唯

    如果有一件事是重要的,如果能為孩子實現一個願望,那就是人類與大自然和諧共存。

    林大利

    於特有生物研究保育中心服務,小鳥和棲地是主要的研究對象。是龜毛的讀者,認為龜毛是探索世界的美德。

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • 英國石油轉型能源公司 11億美元投資離岸風電

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

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

  • 氣候變遷影響更甚疫情 英王儲籲各界迅速行動

    摘錄自2020年9月21日中央社報導

    英國王儲查爾斯今天(21日)警告,氣候變遷危機影響更甚2019冠狀病毒疾病(COVID-19),敦促全球應將這波疫情視為採取行動的契機。

    查爾斯(Prince Charles)說:「若未以史無前例的速度及規模,迅速且立即採取行動,我們將錯失為更永續、更包容的未來『重置』的短暫機會。」

    查爾斯要向各界傳遞的訊息,包括上述言論,將在明天登場的紐約「氣候週」(Climate Week)線上開幕時播放。現年71歲的查爾斯3月時曾染疫,而他長期以來都是永續發展及對抗氣候變遷行動的擁護者。

    氣候變遷
    國際新聞
    英國
    武漢肺炎
    疫情看氣候與能源

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • 比思域還快,車主都說這款20來萬的SUV配置高動力強

    比思域還快,車主都說這款20來萬的SUV配置高動力強

    發動機:1。5T/2。0T最大馬力(pS):181/245最大扭矩(Nm):240/350變速箱:6AT百公里加速(s):9。92/7。1百公里油耗(L):7。2/8。2車主百公里油耗(L):9。61/11。9驅動方式:前置前驅/前置四驅底盤懸挂:前麥弗遜/后多連桿前排腿部空間(mm):870-1085前排高度(mm):990前排寬度(mm):1470後排腿部空間(mm):610-850後排高度(mm):955後排寬度(mm):1485實際體驗(體驗者178cm):前排頭部1拳/後排頭部4指/後排腿部2拳

    如果你手持20萬的購車預算,想買一款緊湊型SUV的的話,有非常多的選擇。而如果你對於操控性和動力有較大需求的話,福特的翼虎是一個不錯的選擇!

    首先,翼虎懸架的調校帶有的韌性,它對於路面細碎振動的過濾徹底,提供了不錯的舒適性,同時在過彎時支撐性比較充足,尾部隨動性也不錯,駕駛起來頗為靈活!

    它搭載的1.5T、2.0T兩台發動機具備強勁功率,與它們匹配的6AT變速箱降擋頗為积極,而且它帶有換擋撥片,可玩性挺高它還帶有S擋,能提升動力響應的靈敏程度。它2.0T四驅車型的破百時間僅為7.1秒,而1.5T兩驅車型百公里加速則在9.92秒內。

    長寬高:4524*1838*1701mm

    軸距:2690mm

    定位:緊湊型SUV

    外觀方面,現款的翼虎車身線條設計很凌厲,中網上粗壯的橫向條裝飾富有力量感。而且尾燈加入了黑色描邊細節,時尚感不低。而尾部的下方則採用了讓雙邊共兩出的排氣,運動感頗為出眾!

    而內飾方面則為比較保守的設計風格,中控台運用軟質搪塑工藝,手感不錯。細節處加入了鋼琴漆黑色亮面裝飾條點綴,質感表現還不錯。

    發動機:1.5T/2.0T

    最大馬力(pS):181/245

    最大扭矩(Nm):240/350

    變速箱:6AT

    百公里加速(s):9.92/7.1

    百公里油耗(L):7.2/8.2

    車主百公里油耗(L):9.61/11.9

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

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

    前排腿部空間(mm):870-1085

    前排高度(mm):990

    前排寬度(mm):1470

    後排腿部空間(mm):610-850

    後排高度(mm):955

    後排寬度(mm):1485

    實際體驗(體驗者178cm):前排頭部1拳/後排頭部4指/後排腿部2拳2指。

    感興趣的朋友可以點擊小程序查看詳細口碑,從口碑中可以看到車主們對翼虎的乘坐空間、操控性、2.0T發動機的強勁動力頗為滿意,但是對較高的油耗有些不滿。

    咱們發現翼虎的優惠幅度中規中矩,在廣州地需搭配店內置換、貸款、上保險、上牌等項目,在北京、武漢地區的一些4S店還需加點裝飾。在成都地區則以現金優惠。

    福特翼虎的電動助力轉向手感比較輕盈,精準度較高。懸架的調校帶有一定的韌性,它更多地側重於對震動的過濾,同時在過彎時支撐性比較充足,底盤質感較高!而且2.0T車型的動力表現強勁,當然油耗也較高!本站聲明:網站內容來源於http://www.auto6s.com/,如有侵權,請聯繫我們,我們將及時處理

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

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

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

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

    昨晚,現代ENCINO上市

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

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

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

    緊湊型SUV?

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

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

    這也太划算了吧

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

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

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

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

    吐槽的同時

    也必須弄清一個概念

    汽車級別怎麼劃分的?

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

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

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

    A00級=微型車

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

    A0級=小型車

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

    A級=緊湊型車

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

    B級=中型車

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

    C級=中大型車

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

    D級=大型車

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

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

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

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

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

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

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

    奔馳S級:再次發明汽車

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

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

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

    君越:圖書館級靜音

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

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

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

    總結:

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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

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

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

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

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

    奧德賽:67.9分

    途安L:64.6分

    銳界:66分

    奇駿:65.4分

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    【其他文章推薦】

    ※超省錢租車方案

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

    ※回頭車貨運收費標準

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

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

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

  • SpringColud Eureka的服務註冊與發現

    SpringColud Eureka的服務註冊與發現

    一、Eureka簡介

    本文中所有代碼都會上傳到git上,請放心瀏覽
    項目git地址:https://github.com/839022478/Spring-Cloud

    在傳統應用中,組件之間的調用,通過有規範的約束的接口來實現,從而實現不同模塊間良好的協作。但是被拆分成微服務后,每個微服務實例的網絡地址都可能動態變化,數量也會變化,使得原來硬編碼的地址失去了作用。需要一个中心化的組件來進行服務的登記和管理,為了解決上面的問題,於是出現了服務治理,就是管理所有的服務信息和狀態,也就是我們所說的註冊中心

    1.1 註冊中心

    比如我們去做火車或者汽車,需要去買票乘車,只看我們有沒有票(有沒有服務),有就去買票(獲取註冊列表),然後乘車(調用),不用關心到底有多少車在運行

    流程圖:

    使用註冊中心,我們不需要關心有多少提供方,只管去調用就可以了,那麼註冊中心有哪些呢?

    註冊中心:Eureka,Nacos,Consul,Zookeeper

    本文中講解的是比較火熱的Spring Cloud微服務下的Eureka,Eureka是Netflix開發的服務發現框架,是一個RESTful風格的服務,是一個用於服務發現和註冊的基礎組件,是搭建Spring Cloud微服務的前提之一,它屏蔽了Server和client的交互細節,使得開發者將精力放到業務上。

    服務註冊與發現主要包括兩個部分:服務端(Eureka Server)和客戶端(Eureka Client)

    • 服務端(Eureka Server): 一個公共服務,為Client提供服務註冊和發現的功能,維護註冊到自身的Client的相關信息,同時提供接口給Client獲取註冊表中其他服務的信息,使得動態變化的Client能夠進行服務間的相互調用。

    • 客戶端(Eureka Client): Client將自己的服務信息通過一定的方式登記到Server上,並在正常範圍內維護自己信息一致性,方便其他服務發現自己,同時可以通過Server獲取到自己依賴的其他服務信息,完成服務調用,還內置了負載均衡器,用來進行基本的負載均衡

    Eureka GIt官網:https://github.com/Netflix/Eureka

    1.3 服務註冊與發現

    服務註冊與發現關係圖:

    1.2 client功能和server功能

    1.2.1 client功能

    1. 註冊:每個微服務啟動時,將自己的網絡地址等信息註冊到註冊中心,註冊中心會存儲(內存中)這些信息。
    2. 獲取服務註冊表:服務消費者從註冊中心,查詢服務提供者的網絡地址,並使用該地址調用服務提供者,為了避免每次都查註冊表信息,所以client會定時去server拉取註冊表信息到緩存到client本地。
    3. 心跳:各個微服務與註冊中心通過某種機制(心跳)通信,若註冊中心長時間和服務間沒有通信,就會註銷該實例。
    4. 調用:實際的服務調用,通過註冊表,解析服務名和具體地址的對應關係,找到具體服務的地址,進行實際調用。

    1.2.2 server註冊中心功能

    1. 服務註冊表:記錄各個微服務信息,例如服務名稱,ip,端口等。
      註冊表提供 查詢API(查詢可用的微服務實例)和管理API(用於服務的註冊和註銷)。
    2. 服務註冊與發現:註冊:將微服務信息註冊到註冊中心。發現:查詢可用微服務列表及其網絡地址。
    3. 服務檢查:定時檢測已註冊的服務,如發現某實例長時間無法訪問,就從註冊表中移除。

    二、Eureka單節點搭建

    2.1 pom.xml

    在有的教程中,會引入spring-boot-starter-web,這個依賴其實不用,因為spring-cloud-starter-netflix-eureka-server的依賴已經包含了它,在pom依賴進去,就可以了

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    

    2.2 application.yml

    server:
      port: 8500
    eureka:
      client:
        #是否將自己註冊到Eureka Server,默認為true,由於當前就是server,故而設置成false,表明該服務不會向eureka註冊自己的信息
        register-with-eureka: false
        #是否從eureka server獲取註冊信息,由於單節點,不需要同步其他節點數據,用false
        fetch-registry: false
        #設置服務註冊中心的URL,用於client和server端交流
        service-url:
          defaultZone: http://localhost:8080/eureka/
    

    2.3 服務端啟動類

    啟動類上添加此註解標識該服務為配置中心
    @EnableEurekaServer

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
    
    @EnableEurekaServer
    @SpringBootApplication
    public class EurekaServerApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EurekaServerApplication.class, args);
        }
    
    }
    
    

    2.4 啟動

    我們啟動EurekaDemoApplication ,然後在瀏覽器中輸入地址 http://localhost:8500/,就可以啟動我們的 Eureka 了,我們來看下效果,出現了這個畫面,就說明我們已經成功啟動~,只是此時我們的服務中是還沒有客戶端進行註冊

    三、服務註冊

    注意:在客戶端pom裏面我們需要加上spring-boot-starter-web,否則服務是無法正常啟動的

    3.1 pom.xml

           <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>     
    

    3.2 application.yml

    #註冊中心
    eureka:
      client:
        #設置服務註冊中心的URL
        service-url:
          defaultZone: http://localhost:8500/eureka/
      #服務名
      instance:
        appname: mxn
    

    3.3 客戶端啟動類

    在客戶端啟動類中我們需要加上 @EnableDiscoveryClient註解

    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    
    @EnableDiscoveryClient
    @SpringBootApplication
    public class EurekaClientApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(EurekaClientApplication.class, args);
        }
    }
    

    3.4 查看效果

    工程啟動后,刷新http://localhost:8500/頁面,我們可以發現服務註冊成功了

    並且我們可以在idea日誌打印中看到DiscoveryClient_MXN/DESKTOP-5BQ3UK8 - registration status: 204,說明就是註冊成功了
    Eureka Server與Eureka Client之間的聯繫主要通過心跳的方式實現。心跳(Heartbeat)即Eureka Client定時向Eureka Server彙報本服務實例當前的狀態,維護本服務實例在註冊表中租約的有效性。

    Eureka Client將定時從Eureka Server中拉取註冊表中的信息,並將這些信息緩存到本地,用於服務發現

    四、Eureka 端點

    官網地址:https://github.com/Netflix/eureka/wiki/Eureka-REST-operations

    Eureka服務器還提供了一個端點(eureka/apps/{applicaitonName})可以查看所註冊的服務詳細信息 。applicaitonName就是微服務的名稱,比如這裏我們訪問 http://localhost:8500/eureka/apps/mxn

    五、Eureka 原理

    5.1 本質

    存儲了每個客戶端的註冊信息。EurekaClient從EurekaServer同步獲取服務註冊列表。通過一定的規則選擇一個服務進行調用

    5.2 Eureka架構圖

    • 服務提供者: 是一個eureka client,向Eureka Server註冊和更新自己的信息,同時能從Eureka Server註冊表中獲取到其他服務的信息。
    • 服務註冊中心: 提供服務註冊和發現的功能。每個Eureka Cient向Eureka Server註冊自己的信息,也可以通過Eureka Server獲取到其他服務的信息達到發現和調用其他服務的目的。
    • 服務消費者: 是一個eureka client,通過Eureka Server獲取註冊到其上其他服務的信息,從而根據信息找到所需的服務發起遠程調用。
    • 同步複製: Eureka Server之間註冊表信息的同步複製,使Eureka Server集群中不同註冊表中服務實例信息保持一致。
    • 遠程調用: 服務客戶端之間的遠程調用。
    • 註冊: Client端向Server端註冊自身的元數據以供服務發現。
    • 續約: 通過發送心跳到Server以維持和更新註冊表中服務實例元數據的有效性。當在一定時長內,Server沒有收到Client的心跳信息,將默認服務下線,會把服務實例的信息從註冊表中刪除。
    • 下線: Client在關閉時主動向Server註銷服務實例元數據,這時Client的服務實例數據將從Server的註冊表中刪除。
    • 獲取註冊表: Client向Server請求註冊表信息,用於服務發現,從而發起服務間遠程調用。

    5.3 Eureka自我保護

    有時候我們會看到這樣的提示信息:EMERGENCY! EUREKA MAY BE INCORRECTLY CLAIMING INSTANCES ARE UP WHEN THEY'RE NOT. RENEWALS ARE LESSER THAN THRESHOLD AND HENCE THE INSTANCES ARE NOT BEING EXPIRED JUST TO BE SAFE.,這是因為默認情況下,Eureka Server在一定時間內,沒有接收到某個微服務心跳,會將某個微服務註銷(90S)。但是當網絡故障時,微服務與Server之間無法正常通信,上述行為就非常危險,因為微服務正常,不應該註銷,它的指導思想就是 寧可保留健康的和不健康的,也不盲目註銷任何健康的服務
    我們也可以通過命令去關閉自我保護的功能:

    eureka:
      server: 
        enable-self-preservation: false
    

    那麼自我保護是如何觸發的呢?
    自我保護機制的觸發條件是,當每分鐘心跳次數( renewsLastMin) 小於 numberOfRenewsPerMinThreshold時,並且開啟自動保護模式開關( eureka.server.enable-self-preservation = true) 時,觸發自我保護機制,不再自動過期租約
    上面我們所有的小於 numberOfRenewsPerMinThreshold,到底是怎麼計算的呢,我們在eureka源碼中可以得知

    numberOfRenewsPerMinThreshold = expectedNumberOfRenewsPerMin * 續租百分比(默認為0.85)
    expectedNumberOfRenewsPerMin = 當前註冊的應用實例數 x 2
    當前註冊的應用實例數 x 2 是因為,在默認情況下,註冊的應用實例每半分鐘續租一次,那麼一分鐘心跳兩次,因此 x 2

    例如:我們有10個服務,期望每分鐘續約數:10 * 2=20,期望閾值:20*0.85=17,當少於17時,就會觸發自我保護機制

    5.4 健康檢查

    由於server和client通過心跳保持 服務狀態,而只有狀態為UP的服務才能被訪問。看eureka界面中的status。

    比如心跳一直正常,服務一直UP,但是此服務DB(數據庫)連不上了,無法正常提供服務。

    此時,我們需要將 微服務的健康狀態也同步到server。只需要啟動eureka的健康檢查就行。這樣微服務就會將自己的健康狀態同步到eureka。配置如下即可。

    在client端配置:將自己的健康狀態傳播到server。

    eureka:
      client:
        healthcheck:
          enabled: true
    

    5.5 Eureka監聽事件

    import com.netflix.appinfo.InstanceInfo;
    import org.springframework.cloud.netflix.eureka.server.event.*;
    import org.springframework.context.event.EventListener;
    import org.springframework.stereotype.Component;
    
    import java.time.LocalDateTime;
    
    @Component
    public class CustomEvent {
    
        @EventListener
        public void listen(EurekaInstanceCanceledEvent event ) {
            System.out.println(LocalDateTime.now()+"服務下線事件:"+event.getAppName()+"---"+event.getServerId());
    //發釘釘
        }
    
        @EventListener
        public void listen(EurekaInstanceRegisteredEvent event) {
            InstanceInfo instanceInfo = event.getInstanceInfo();
            System.out.println(LocalDateTime.now()+"服務上線事件:"+instanceInfo.getAppName()+"---"+instanceInfo.getInstanceId());
        }
    
        @EventListener
        public void listen(EurekaInstanceRenewedEvent event) {
            System.out.println(LocalDateTime.now()+"服務續約/心跳上報事件:"+event.getAppName()+"---"+event.getServerId());
    
        }
    
        @EventListener
        public void listen(EurekaRegistryAvailableEvent event) {
            System.out.println(LocalDateTime.now()+"註冊中心可用事件");
        }
    
        @EventListener
        public void listen(EurekaServerStartedEvent event) {
            System.out.println(LocalDateTime.now()+"註冊中心啟動事件");
    
        }
    }
    

    5.6 Renew: 服務續約

    Eureka Client 會每隔 30 秒發送一次心跳來續約。 通過續約來告知 Eureka Server 該 Eureka Client 運行正常,沒有出現問題。 默認情況下,如果 Eureka Server 在 90 秒內沒有收到 Eureka Client 的續約,Server 端會將實例從其註冊表中刪除,此時間可配置,一般情況不建議更改。

    5.6 服務剔除

    如果Eureka Client在註冊后,既沒有續約,也沒有下線(服務崩潰或者網絡異常等原因),那麼服務的狀態就處於不可知的狀態,不能保證能夠從該服務實例中獲取到回饋,所以需要服務剔除此方法定時清理這些不穩定的服務,該方法會批量將註冊表中所有過期租約剔除,剔除是定時任務,默認60秒執行一次。延時60秒,間隔60秒

    剔除的限制:
    1.自我保護期間不清除。
    2.分批次清除。

    六、Eureka缺陷

    由於集群間的同步複製是通過HTTP的方式進行,基於網絡的不可靠性,集群中的Eureka Server間的註冊表信息難免存在不同步的時間節點,不滿足CAP中的C(數據一致性)

    七、總結

    中間我們講解了eureka的節點搭建,以及原理,對於現在很火熱的微服務,我們對Eureka是非常有必要進行了解的,如果覺得文章對你有幫助,來個點贊支持吧,如果對文章有疑問或建議,歡迎討論留言,謝謝大家~

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

  • 併發系列(一)——線程池源碼(ThreadPoolExecutor類)簡析

    併發系列(一)——線程池源碼(ThreadPoolExecutor類)簡析

    前言

      本文主要是結合源碼去線程池執行任務的過程,基於JDK 11,整個過程基本與JDK 8相同。

      個人水平有限,文中若有表達有誤的,歡迎大夥留言指出,謝謝了!

    一、線程池簡介

      1.1 使用線程池的優點

        1)通過復用已創建的線程,降低資源的消耗(線程的創建/銷毀是要消耗資源的)、提高響應速度;

        2)管理線程的個數,線程的個數在初始化線程池的時候指定;

        3)統一管理線程,比如停止,stop()方法;

      1.2 線程池執行任務過程

        線程池執行任務的過程如下圖所示,主要分為以下4步,其中參數的含義會在後面詳細講解:

        1)判斷工作的線程是否小於核心線程數據(workerCountOf(c) < corePoolSize),若小於則會新建一個線程去執行任務,這一步僅僅的是根據線程個數決定;

        2)若核心線程池滿了,就會判斷線程池的狀態,若是running狀態,則嘗試加入任務隊列,若加入成功后還會做一些事情,後面詳細說;

        3)若任務隊列滿了,則加入失敗,此時會判斷整個線程池線程是否滿,若沒有則創建非核心線程執行任務;

        4)若線程池滿了,則根據拒絕測試處理無法執行的任務;

        整體過程如下圖:

    二、ThreadPoolExecutor類解析

      2.1 ThreadPoolExecutor的構造函數

        ThreadPoolExecutor類一共提供了4個構造函數,涉及5~7個參數,下面就5個必備參數的構造函數進行說明:

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue) {
            this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
                 Executors.defaultThreadFactory(), defaultHandler);
        }

        1)corePoolSize :初始化核心線程池中線程個數的大小;

        2)maxmumPoolSize:線程池中線程大小;

        3)keepAliveTime:非核心線程的超時時長;

          非核心線程空閑時常大於該值就會被終止。

        4)unit :keepAliveTime的單位,類型可以參見TimeUnit類;

        5)BlockingQueue workQueue:阻塞隊列,維護等待執行的任務;

      2.2  私有類Worker

        在ThreadPoolExecutor類中有兩個集合類型比較重要,一個是用於放置等待任務的workQueue,其類型是阻塞對列;一個是用於用於存放工作線程的works,其是Set類型,其中存放的類型是Worker。

        進一步簡化線程池執行過程,可以理解為works中的工作線程不停的去阻塞對列中取任務,執行結束,線程重新加入大works中。

        為此,有必要簡單了解一下Work類型的組成。

    private final class Worker
            extends AbstractQueuedSynchronizer
            implements Runnable
        {
            /** Thread this worker is running in.  Null if factory fails. */
            //工作線程,由線程的工廠類初始化
            final Thread thread;
            /** Initial task to run.  Possibly null. */
            Runnable firstTask;
            /** Per-thread task counter */
            volatile long completedTasks;
            //不可重入的鎖
            protected boolean tryAcquire(int unused) {
                if (compareAndSetState(0, 1)) {
                    setExclusiveOwnerThread(Thread.currentThread());
                    return true;
                }
                return false;
            }
    
            .......
        }

        Worker類繼承於隊列同步器(AbstractQueueSynchronizer),隊列同步器是採取鎖或其他同步組件的基礎框架,其主要結構是自旋獲取鎖的同步隊列和等待喚醒的等待隊列,其方法因此可以分為兩類:對state改變的方法 和 入、出隊列的方法,即獲取獲取鎖的資格的變化(可能描述的不準確)。關於隊列同步器後續博客會詳細分析,此處不展開討論。

        Work類中通過CAS設置狀態失敗后直接返回false,而不是判斷當前線程是否已獲取鎖來實現不可重入的鎖,源碼註釋中解釋這樣做的原因是因為避免work tash重新獲取到控制線程池全局的方法,如setCorePoolSize。

      2.3  拒絕策略類

        ThreadPoolExecutor的拒絕策略類是以私有類的方式實現的,有四種策略:

        1)AbortPolicy:丟棄任務並拋出RejectedExecutionException異常(默認拒絕處理策略)。

          2)DiscardPolicy:拋棄新來的任務,但是不拋出異常。

          3)DiscardOldestPolicy:拋棄等待隊列頭部(最舊的)的任務,然後重新嘗試執行程序(失敗則會重複此過程)。

          4)CallerRunsPolicy:由調用線程處理該任務。

        其代碼相對簡單,可以參考源碼。

    三、任務執行過程分析

      3.1 execute(Runnable)方法

        execute(Runnable)方法的整體過程如上文1.2所述,其實現方式如下:

    public void execute(Runnable command) {
            //執行的任務為空,直接拋出異常
            if (command == null)
                throw new NullPointerException();
            //ctl是ThreadPoolExecutor中很關鍵的一個AtomicInteger,主線程池的控制狀態
            int c = ctl.get();
            //1、判斷是否小於核心線程池的大小,若是則直接嘗試新建一個work線程
            if (workerCountOf(c) < corePoolSize) {
                if (addWorker(command, true))
                    return;
                c = ctl.get();
            }
            //2、大於核心線程池的大小或新建work失敗(如創建thread失敗),會先判斷線程池是否是running狀態,若是則加入阻塞對列
            if (isRunning(c) && workQueue.offer(command)) {
                int recheck = ctl.get();
                //重新驗證線程池是否為running,若否,則嘗試從對列中刪除,成功后執行拒絕策略
                if (! isRunning(recheck) && remove(command))
                    reject(command);
                //若線程池的狀態為shutdown則,嘗試去執行完阻塞對列中的任務
                else if (workerCountOf(recheck) == 0)
                    addWorker(null, false);
            }
            //3、新建非核心線程去執行任務,若失敗,則採取拒絕策略
            else if (!addWorker(command, false))
                reject(command);
        }

      3.2 addWorker(Runnable,boole)方法

        execute(Runnable)方法中,新建(非)核心線程執行任務主要是通過addWorker方法實現的,其執行過程如下:

    private boolean addWorker(Runnable firstTask, boolean core) {
            //此處反覆檢查線程池的狀態以及工作線程是否超過給定的值
            retry:
            for (int c = ctl.get();;) {
                // Check if queue empty only if necessary.
                if (runStateAtLeast(c, SHUTDOWN)
                    && (runStateAtLeast(c, STOP)
                        || firstTask != null
                        || workQueue.isEmpty()))
                    return false;
    
                for (;;) {
                //核心和非核心線程的區別
                    if (workerCountOf(c)
                        >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                        return false;
                    if (compareAndIncrementWorkerCount(c))
                        break retry;
                    c = ctl.get();  // Re-read ctl
                    if (runStateAtLeast(c, SHUTDOWN))
                        continue retry;
                    // else CAS failed due to workerCount change; retry inner loop
                }
            }
    
            boolean workerStarted = false;
            boolean workerAdded = false;
            Worker w = null;
            try {
                w = new Worker(firstTask);
                //通過工廠方法初始化,可能失敗,即可能為null
                final Thread t = w.thread;
                if (t != null) {
                //獲取全局鎖
                    final ReentrantLock mainLock = this.mainLock;
                    mainLock.lock();
                    try {
                        // Recheck while holding lock.
                        // Back out on ThreadFactory failure or if
                        // shut down before lock acquired.
                        int c = ctl.get();
                        //線程池處於running狀態
                        //或shutdown狀態但無需要執行的task,個人理解為用於去阻塞隊列中取任務執行
                        if (isRunning(c) ||
                            (runStateLessThan(c, STOP) && firstTask == null)) {
                            if (t.isAlive()) // precheck that t is startable
                                throw new IllegalThreadStateException();
                            workers.add(w);
                            int s = workers.size();
                            if (s > largestPoolSize)
                                largestPoolSize = s;
                            workerAdded = true;
                        }
                    } finally {
                        mainLock.unlock();
                    }
                    if (workerAdded) {
                        //執行任務,這裡會執行thread的firstTask獲取阻塞對列中取任務
                        t.start();
                        workerStarted = true;
                    }
                }
            } finally {
                if (! workerStarted)
                //開始失敗,則會從workers中刪除新建的work,work數量減1,嘗試關閉線程池,這些過程會獲取全局鎖
                    addWorkerFailed(w);
            }
            return workerStarted;
        }

      3.3  runWorker(this) 方法

         在3.2 中當新建的worker線程加入在workers中成功后,就會啟動對應任務,其調用的是Worker類中的run()方法,即調用runWorker(this)方法,其過程如下:

    final void runWorker(Worker w) {
            Thread wt = Thread.currentThread();
            Runnable task = w.firstTask;
            w.firstTask = null;
            w.unlock(); // allow interrupts
            boolean completedAbruptly = true;
            try {
            //while()循環中,前者是新建線程執行firstTask,對應線程個數小於核心線程和阻塞隊列滿的情況,
            //getTask()則是從阻塞對列中取任務執行
                while (task != null || (task = getTask()) != null) {
                    w.lock();
                    // If pool is stopping, ensure thread is interrupted;
                    // if not, ensure thread is not interrupted.  This
                    // requires a recheck in second case to deal with
                    // shutdownNow race while clearing interrupt
                    //僅線程池狀態為stop時,線程響應中斷,這裏也就解釋了調用shutdown時,正在工作的線程會繼續工作
                    if ((runStateAtLeast(ctl.get(), STOP) ||
                         (Thread.interrupted() &&
                          runStateAtLeast(ctl.get(), STOP))) &&
                        !wt.isInterrupted())
                        wt.interrupt();
                    try {
                        beforeExecute(wt, task);
                        try {
                        //執行任務
                            task.run();
                            afterExecute(task, null);
                        } catch (Throwable ex) {
                            afterExecute(task, ex);
                            throw ex;
                        }
                    } finally {
                        task = null;
                        //完成的個數+1
                        w.completedTasks++;
                        w.unlock();
                    }
                }
                completedAbruptly = false;
            } finally {
                //處理後續工作
                processWorkerExit(w, completedAbruptly);
            }
        }

       3.4 processWorkerExit(Worker,boole)方法

        當任務執行結果后,在滿足一定條件下會新增一個worker線程,代碼如下:

    private void processWorkerExit(Worker w, boolean completedAbruptly) {
            if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
                decrementWorkerCount();
    
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                completedTaskCount += w.completedTasks;
                //對工作線程的增減需要加全局鎖
                workers.remove(w);
            } finally {
                mainLock.unlock();
            }
            //嘗試終止線程池
            tryTerminate();
    
            int c = ctl.get();
            if (runStateLessThan(c, STOP)) {
            //線程不是中斷,會維持最小的個數
                if (!completedAbruptly) {
                    int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
                    if (min == 0 && ! workQueue.isEmpty())
                        min = 1;
                    if (workerCountOf(c) >= min)
                        return; // replacement not needed
                }
                //執行完任務后,線程重新加入workers中
                addWorker(null, false);
            }
        }

      至此,線程池執行任務的過程分析結束,其他方法的實現過程可以參考源碼。

     

    Ref:

    [1]http://concurrent.redspider.group/article/03/12.html

    [2]《Java併發編程的藝術》

     

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

    【其他文章推薦】

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

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

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

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

    ※回頭車貨運收費標準

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

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

    一:背景

    1. 講故事

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

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

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

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

    二:源碼探索

    1. 從IL中尋找答案

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

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

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

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

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

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

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

    2. 面對疾風

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

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

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

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

    • netcore 3.1

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

    • net5

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

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

    四: 總結

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

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

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

    【其他文章推薦】

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

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

    ※回頭車貨運收費標準

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

    ※超省錢租車方案

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