標籤: 南投搬家公司費用

  • Vue —— 精講 VueRouter(1)

    最近被Boos調去給新人做培訓去了,目前把自己整理的一些東西分享出來,希望對大家有所幫助

    demo源代碼地址:https://github.com/BM-laoli/BMlaoli-learn-VueRouter

    本章節為VueRouter前端 路由的章節部分

    大綱

    一、基本概念

    路由就是通過網絡把訊息從源地址傳輸到目的地的活動
    需要一些映射表

    1. 做路由
    2. 做信息的轉發(核心就是:轉發)

    後端路由還有前端路由,後端渲染和前端渲染

    前端渲染(前後端分離API生態),後端渲染(view嵌套一起)

    前端路由的核心概念
    地址變化的時候改變url的時候,不進行整體頁面刷新

    改變url但是不刷新頁面,的解決方式

    我們有這樣的一個需求,改變url跳轉地址,我們獲取新的頁面,但是不希望頁面發生刷新

    解決方案1:locaion.hash = ‘/’

    這個是vueRouter的底層實現

    監聽hash的變化,從而改變網頁數據的獲取機制,渲染對應的組件,

    解決方案2:H5的histroray模式

    1. pushState
      history.pushState({},”,’home’),第三個參數就是url

    這裏的push實際上就是一個棧結構(先進后出),

    假設我們這裏需要回去,使用back()彈棧

    history.pushState({},'','home'),
    history.pushState({},'','about'),
    history.pushState({},'','user'),
    
    //執行這個之後就能進行back()出棧了
    history.back(),
    // 此時的url是 /about
    
    
    1. repalceState

    這裡有一個方法和push方法很像,但是不會back()不能點擊後腿按鈕

    history.repalceState({},'','user'),
    
    1. go

    這裏的go是對棧的一個操作,
    go(-1)彈出一個
    go(-2)彈出二個

    go(1)壓入一個
    go(2)壓入二個

    go(-1)
    

    以上就是我們的基本的前端路由原理

    二、v-router基本使用

    前端三大框架都有自己的router,可以用來構建SPA應用

    使用小提示,還是非常非常的簡單的:

    1. 如果你沒有安裝就需要 npm install vue-router去安裝
      • 導入路由對象,並且調用Vue.use(VueRouter)安裝這個路由插件
      • 創建路由實例,傳入映射配置wxain
      • 在vue實例中掛載創建好了的路由

    1.導入路由對象,並且配置optionn給路由

    /router/index.js

    
    /**
     * 配置路由相關的信息
     */
    // 1. 導入
     import Router from 'vue-router'
     
     // 2.1 導入vue實例
    import Vue from 'vue'
    
    // 導入組件
    import Home from '../components/Home.vue'
    import About from '../components/About.vue'
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = []
    
    const router = new Router({routes})
    
    //4. 導出
    export default router 
     
    
    

    2.配置路由映射

    /router/index.js

    const routes = [
     
     {path:'/home',component:Home},
     {path:'/about',component:About},
    
    ] 
    

    3.在實例中使用路由

    /main.js

    import Vue from 'vue'
    import App from './App'
    import router from './router'//注意啊模塊查找規則index.js
    
    Vue.config.productionTip = false
    
    /* eslint-disable no-new */
    new Vue({
      el: '#app',
      router,// 主要是在這裏掛載進去就好了
      render: h => h(App)
    }) 
    

    4.小心,我們的路由入口還有連接link

    /App.vue

    
    <template>
      <div id="app">
        <!-- //這兩個是一個全局祖冊過着個組件,這個就是一個a標籤 -->
        <router-link to='/home'>首頁</router-link>
        <router-link to='/about'>關於</router-link>
        <!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
        <router-view></router-view>
      </div>
    </template>
    
    

    以下是我們的兩個組件

    /Home.vue

    <template>
        <div>
            <h2>我是首頁</h2>
            <p>我是首頁內容哈哈哈</p>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style scoped>
    
    </style>
    

    /About.vue

    <template>
        <div>
            <h2>我是關於頁面</h2>
            <p>我是首關於內容哈哈哈</p>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style>
    
    </style>
    

    以上就是我們非常簡單的使用

    三、其它的知識點補充

    路由的默認值,並且修改成mode=>hisary模式

    我們希望默認显示的就是一個首頁
    解決方式,映射一個’/’,然後進行重定向
    /index.js

      {
        path:'/',
        redirect:'/home'
      },
    

    我們為什麼要去做這調整成一個history,因為我們希望去掉#這個標識

    只需要在new 的時候指定一下就好了
    /index,js

    const router = new Router({
      routes,
      mode:"history"//就是這裏的這個更改路由方式
    })
    

    router-link的屬性

    1. tage
      to是一個屬性 ,默認是渲染成一個a鏈接,假設我現在需要默認渲染成一個buttmm怎麼辦呢?
      加一個tag就好了
        <router-link to='/home' tag='button'  >首頁</router-link>
    
    1. 更改模式replceStats 不允許瀏覽器回退
      replace加上去就好了
    <router-link to='/about' tag='button' replace >關於</router-link>
    
    1. 我們可以利用一些默認的東西去非常方便的做到想要的效果
    <style>
    .router-link-active{
      color: blue;
    }
    </style>
    

    替換值:我們希望不要怎麼長,我們希望.active就能改樣式怎麼搞?
    加一個active-calss就好了,這個直接就是acitve做為類就好了

     <router-link to='/home' tag='button'  active-class  >首頁</router-link>
     <style>
        .active{
            bgc:red
        }
     </style>
    

    代碼路由跳轉,意思就是重定向

    注意啊!route != router
    在我們學習路由的時候,this.$router是一個非常重要的對象

    這個東西在開中經常的使用

    // this.$router.push('重定向的值就好了')。
    // this.$router.push('/home')
    // 如果你不想有出現回退按鈕,這樣來做就好了
    this.$router.replace('/home')
    

    四、動態路由參數

    這裏只是簡單的介紹了理由傳參的地址欄拼接模式,但是還有更多更奇奇怪怪的傳值方式,詳見官方Router文檔,

    this.$router.parmas
    // 這個parmas裏面就是我們的路由參數存放點
    

    這裏我們有這樣的一個需求,我們希望點擊user頁面的時候可以,得到任意的路由參數

    比如我們現在/user/zhnsang,的時候可以獲取zhangshang,/user/lishi的時候可以獲取lishi>

    1. 首先我們需要在路由裏面加:
      /router/index.js
       {
            path: "/user/:usermsg",
            component: User
        }
    ]
    
    1. 頁面傳遞數據
      /App.vue
    router-link :to="'/user/'+username">用戶相關</router-link>
    <!-- 路由出口,既:渲染的出口,這個就是一個佔位符號 -->
    <router-view></router-view>
      </div>
    </template>
    
    <script>
    export default {
      name: 'App',
      data() {
        return {
          username: 'lisi'
        }
      },
    
    
    1. 頁面獲取數據

    一定要注意了,一定是rouer裏面定義的才能從另一路由拿出來

    /User.vue

    
    <template>
        <div>
            <h2>我是用戶相關專業</h2>
            <p>我是用戶訊息相關頁面,嘿嘿嘿嘿嘿</p>
            <h1>{{ $route.params.usermsg }}44</h1>
            <hr>
            <h2>{{username}}</h2>
        </div>
    </template>
    
    <script>
        export default {    
            computed: {
                username() {
                    return this.$route.params.usermsg
                }
            },
        }
    </script>
    
    <style scpoe>
    
    </style>
    

    五、細節詳解

    注意啊!再說一遍route != router

    注意啊,這裏的$route實際上是我們在main裏面new的一個Router得到的,
    並且 這個route對象是隨着請求的地方不一樣,而改變的。也就是說,這個的route是當前頁面中的route對象,而且在vue只能只有一個route實例存在

    六、 Vue的webpack打包詳解 + 路由懶加載

    一個vue項目的簡單打包目錄結構分析

    我們來看看,在一個vue項目中,簡單的三個文件是怎麼打包的

    假設目前有這樣的三個文件 ,我們需要對他們進行打包,mian是入口,有一個add業務,有一個math依賴模塊。那麼我們webpack打包成的三個文件到底是如何運行的呢?

    在vue中 使用webpack打包的時候,會把一些東西給分模塊的打包出來,它打包的東西的目錄結構如下
    裏面我們實際打包的時候會把css,js都給分開,各自有各自的作用

    | dist
    | ---static
    | ---css
    | ---js
    | -----app.XXXX.js         (這個是項目的業務邏輯所在)
    | -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
    | -----vendor.xxxx.js      (這個是依賴所在)
    | idnex.html
    

    路由懶加載

    1. 概念的理解

    目前呢,我們打包的情況是這樣的:我們所有的代碼都是集中放在了以一個app.xxx.js文件中,這樣其實不利於後期的維護和開發,因為如果我們有很多很多的大量的代碼的時候,我們的這個文件就會變得非常非常的大,於是呢,我們就需要路由懶加載,所謂懶加載就是:‘在需要的時候,才去加載某個資源文件’,路由懶加載,就是把每一個路由對應的業務邏輯代碼,在打包的時候分割到不同的js文件中,如何在需要用的時候再去請求它

    經過這樣的打包的懶加載之後,我們的目錄會變成這個樣子

    | dist
    | ---static
    | ---css
    | ---js
    | -----0.xxx.js            (假設是路由home的業務邏輯代碼)
    | -----1.xxx.js             (假設是路由about的業務邏輯代碼)
    | -----app.XXXX.js         (這個是項目的業務邏輯所在)
    | -----manifest.xxxx.js    (這個是底層打包的依賴文件所在)
    | -----vendor.xxxx.js      (這個是依賴所在)
    | idnex.html
    
    1. 如何使用

    使用非常的簡單,主要有如下的三種方式去使用,但是我最喜歡的還是最後一種方式
    /rouetr/index.js

    - 使用vue的異步組價和webpack的寫法,早期的時候
    const Home = resolve =>{ require.ensure(['../compenet/Home.vue'],()=>{
       resolve (require('../compenet/Home.vue'))
    })}
    
    - AMD規範的寫法
    const About = resolve =>{ require(['../compenent/About.vue'],resolve) }
    
    
    - ES6的結合異步組件的方式(最常用)
    const Home = () => import('../compenet/Home.vue')
    

    實際的使用
    /router/index.js

    /**
     * 配置路由相關的信息
     */
    // 1. 導入
    import Router from 'vue-router'
    
    // 2.1 導入vue實例
    import Vue from 'vue'
    
    // 導入組件
    // import Home from '../components/Home.vue'
    // import About from '../components/About.vue'
    // import User from '../components/User'
    const Home = () =>
        import ('../components/Home.vue')
    const About = () =>
        import ('../components/About.vue')
    const User = () =>
        import ('../components/User')
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = [{
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: Home
        },
        {
            path: '/about',
            component: About
        },
        {
            path: "/user/:usermsg",
            component: User
        }
    ]
    
    const router = new Router({
        routes,
        mode: "history"
    })
    
    //4. 導出
    export default router
    
    //6. 去main裏面掛載
    

    七、 路由嵌套

    我們目前有這樣的一個需求:我們希望我們在hone下,可以/home/new去到home下的一個子組件,/home/message去到另一個子組件

    1. 首先 我們需要有組件
      /components/HomeMessage.vue
    <template>
        <div>
          <ul>
              <li1>我是消息1</li1>
              <li2>我是消息2</li2>
              <li3>我是消息3</li3>
              <li4>我是消息4</li4>
          </ul>
        </div>
    </template>
    
    <script>
        export default {
            name:"HomeMessage"
        }   
    </script>
    
    <style>
    
    </style>
    
    

    /components/HomeNews

    <template>
        <div>
        <ul>
            <li1>新1</li1>
            <li2>新2</li2>
            <li3>新3</li3>
            <li4>新4</li4>
            <li5>新5</li5>
        </ul>
        </div>
    </template>
    
    <script>
        export default {
            name:"HomeNews"
        }
    </script>
    
    <style>
    
    </style>
    
    1. 在路由裏面去配置
    const HomeNews = () =>
        import ('../components/HomeNews')
    const HomeMessage = () =>
        import ('../components/HomeNews')
    
    
    // 2.2使用路由(插件),安裝插件,vue的插件,都是這樣安裝,Vue.use
    Vue.use(Router)
    
    // 3. 創建路路由對象,這個就是在Router裏面配置映射和對象等東西
    
    // 4. 抽離配置項出來
    const routes = [{
            path: '/',
            redirect: '/home'
        },
        {
            path: '/home',
            component: Home,
            children: [{
                    path: '',
                    redirect: 'news'
                },
                {
                    path: 'news',// 這裏寫路由實際上應該是/home/news,這裏只是一個相對路由地址,
                    component: HomeNews
                },
                {
                    path: 'message',
                    component: HomeMessage
                },
    
            ]
        },
        {
    
    1. 打入口router-view(瞎起的名字實際上就是路由的佔位符)
      /Home.vue
    <template>
        <div>
            <h2>我是首頁</h2>
            <p>我是首頁內容哈哈哈</p>
         <router-link to="/home/news">news</router-link>
         <router-link to="/home/message">message</router-link>
        <router-view></router-view>
        </div>
    </template>
    
    <script>
        export default {
            
        }
    </script>
    
    <style scoped>
    
    </style>
    

    這裏如果是有關狀態的保持,我們需要使用key-alive,後面我們再做詳細的講解

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

    【其他文章推薦】

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

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

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

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

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

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

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

    小聲逼逼

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

    案例一

    從最大的同性交友網站中拉取【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網頁設計為架站首選

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

    ※回頭車貨運收費標準

  • 德東又發現非洲豬瘟 布蘭登堡邦5野豬染疫

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

    德國政府今天表示,15日確認東部布蘭登堡邦又有5隻野豬感染非洲豬瘟(ASF)。繼上週發現首例野豬感染非洲豬瘟後,德國目前為止已累計6例。

    布蘭登堡邦(Brandenburg)衛生廳表示,疫病是在死去野豬身上發現,並非農場畜養的豬隻;發現地點靠近上週發現的確認首例。

    聯邦農業部表示,在布蘭登堡邦實驗室初步檢驗結果呈陽性反應後,德國聯邦動物防疫研究院(Friedrich-Loeffler-Institut) 確認那些野豬感染非洲豬瘟。

    過去數月在靠近德國邊界的波蘭,一再傳出並確認有野豬感染非洲豬瘟;根據德國聯邦動物防疫研究院網站資料,波蘭今年以來大約有3100隻野豬感染非洲豬瘟。

    國際新聞
    德國
    非洲豬瘟

    本站聲明:網站內容來源環境資訊中心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網頁設計為架站首選

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

    ※回頭車貨運收費標準

  • 50年不怎麼變,它卻始終是王者

    50年不怎麼變,它卻始終是王者

    2011年,廣州車展上新一代的911車型正式發布。新一代保時捷911車內多處添加鍍鉻裝飾,檔次再度升級。新一代保時捷911 Carrera搭載的是3。4L水平對置發動機,最大功率達到345馬力,911 Carrera S搭載一台3。8L水平對置發動機,最大功率達到400馬力,而911 Turbo最大功率達到542馬力,此外,保時捷為新一代911全系標配了7速保時捷雙離合變速箱。

    說起跑車,我相信這款有着將近50年不變的後置后驅結構和萌萌噠”青蛙眼睛”的跑車,一定讓每位車迷都魂牽夢繞,從它誕生至今,因為其獨特的風格和超強的耐用性而享譽世界,沒錯,它就是世界經典—保時捷911。

    但是它成為一代經典的傳奇生涯你是否了解呢?不知道也沒關係,今天就為你再現它的成名之路以及競技生涯。

    自誕生日起,保時捷至今歷經七代車型,911於1963年以跑車的身份亮相法蘭克福車展。“蛙眼大燈”的前臉設計、COUpE車身造型、後置六缸水平對置發動機、良好的操控性為保時捷911的成功奠定了基礎。在不斷的升級換代中,911又加入了很多的創新系元素,但新一代的911整體設計思路都會傳承911車型的經典元素,所以911每一款成功的車型都會有屬於自己的氣質,它所體現出來的“車魂”是唯一的。

    第一代誕生於1963年,它的出現奠定了日後整個911系列車型的幾個重要元素,”蛙眼大燈“、後置六缸水平對置發動機以及空氣冷卻系統。底盤技術多沿用356的技術,包括四輪碟式制動器和后懸挂,前懸挂則為全新開發的。

    第二代是911車系裡面時間最長的一代,歷時15年。在這期間,911Turbo等技術不斷創新的全新車型為911贏得了許多國際大獎,其中四輪驅動的959曾是1984和1986年的達喀爾拉力賽的冠軍。

    第三代911開始,在引擎變速箱方面進行了技術革新,之後又相繼推出了新開發的四輪驅動911Carrera4 、Carrera以及第二代911Turbo車型,其中Carrera採用了當時比較新穎的“Tiptronic”自動變速器。另外由第三代開始,底盤編號開始正式記錄入911的車型歷史中。

    到了第四代911,Targa獨成一派,作為911新的版本車型。1992年,為了舉辦Carrera杯比賽,911 Carrera再度推出其賽道版本RS,該車擁有300匹的馬力,最高時速269KM/h。四代後期全球推出新的廢氣排放標準,為此911放棄了風冷發動機繼而推出新款水冷發動機,此款水冷發動機也是第五代911車型996的引擎雛形。

    第五代911正式開始採用水冷發動機,1999年保時捷穩定管理系統首次搭載在新問世的Carrera 4車型上,在五代時還發生件非常有趣的事,在勒芒大賽中獲獎的911GT1因為規則前進氣口必須修改,於是966式的新車誕生了,正式命名為911 GT1 Evolution。

    第六代911車身外觀依然是以流暢簡潔為主,經典的圓形大燈,鬼臉大燈下增加了兩個小燈,911也相繼推出敞篷型和四驅車型。第六代保時捷911(997)除了缸內直噴、主動懸挂系統和7速保時捷雙離合變速箱外,911 Turbo又增加了可變幾何渦輪和膨脹進氣歧管等技術。

    2011年,廣州車展上新一代的911車型正式發布。新一代保時捷911車內多處添加鍍鉻裝飾,檔次再度升級。新一代保時捷911 Carrera搭載的是3.4L水平對置發動機,最大功率達到345馬力,911 Carrera S搭載一台3.8L水平對置發動機,最大功率達到400馬力,而911 Turbo最大功率達到542馬力,此外,保時捷為新一代911全系標配了7速保時捷雙離合變速箱。

    911系列分為為Carrera系列、 Targa系列、Turbo系列、Turbo S系列、GT系列。

    其中的Carrera系列作為911車系當中的主打產品,款式是最多的,它也是整個911車系的靈魂。

    Targa的整體風格造型都顯得獨具特色、精美絕倫。這款極富典雅氣息的911跑車有自己的一套美學標準,透明的車頂從擋風玻璃一直延伸到引擎艙蓋,這樣設計讓車內的環境更親近大自然,寬大的天窗展開面積可達到0.45平方米,並且能夠在尾門的下方平穩滑行。

    911Turbo系列是整個系列中歷史最悠久的,可以追溯到1974年,適逢經濟衰退和石油缺乏的時期,於是這款名為911Turbo以”追求高效率“的新概念跑車就孕育而生。這也是保時捷的首款搭載渦輪增壓的911車型。

    TurboS 系列是Turbo的強化版本,在性能上有了實質性的突破,最大的輸出功率是390KW,比Turbo系列搞出了22KW,峰值扭矩更是達到了700牛.米,這是在民用版的車型中動力最為強勁的民用版車型。

    保時捷GT系列因為是為賽道而打造的車型,所以在性能配置和底盤調校上都更有賽道的風格。911GT系列的運動感比其他系列的會更強,不管是GT2、GT3還是GT3 RS都匹配了6MT的變速箱,操控性會更好,而且在車身的輕量化方面也做得很好,同時在韌性碰撞及安全性上有更大的提升。

    結語:911之所以是經典,離不開其經典的設計元素,也離不開它出色的性能表現以及對自身不斷完善,追求新技術的理念;當然了,更離不開它極其出色的可靠性以及實用性。曾經有個名人說:保時捷既可以穿梭於非洲沙漠甚至是沙漠,又可以開着它去廣場閑庭散步,穿梭於擁堵的城市街道看盡都市繁華。本站聲明:網站內容來源於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網頁設計為架站首選

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

    ※回頭車貨運收費標準

  • 曹工說Redis源碼(8)–面試時,redis 內存淘汰總被問,但是總答不好

    曹工說Redis源碼(8)–面試時,redis 內存淘汰總被問,但是總答不好

    文章導航

    Redis源碼系列的初衷,是幫助我們更好地理解Redis,更懂Redis,而怎麼才能懂,光看是不夠的,建議跟着下面的這一篇,把環境搭建起來,後續可以自己閱讀源碼,或者跟着我這邊一起閱讀。由於我用c也是好幾年以前了,些許錯誤在所難免,希望讀者能不吝指出。

    曹工說Redis源碼(1)– redis debug環境搭建,使用clion,達到和調試java一樣的效果

    曹工說Redis源碼(2)– redis server 啟動過程解析及簡單c語言基礎知識補充

    曹工說Redis源碼(3)– redis server 啟動過程完整解析(中)

    曹工說Redis源碼(4)– 通過redis server源碼來理解 listen 函數中的 backlog 參數

    曹工說Redis源碼(5)– redis server 啟動過程解析,以及EventLoop每次處理事件前的前置工作解析(下)

    曹工說Redis源碼(6)– redis server 主循環大體流程解析

    曹工說Redis源碼(7)– redis server 的周期執行任務,到底要做些啥

    什麼是內存淘汰

    內存淘汰,和平時我們設置redis key的過期時間,不是一回事;內存淘汰是說,假設我們限定redis只能使用8g內存,現在已經使用了這麼多了(包括設置了過期時間的key和沒設過期時間的key),那,後續的set操作,還怎麼辦呢?

    是不是只能報錯了?

    那不行啊,不科學吧,因為有的key,可能已經很久沒人用了,可能以後也不會再用到了,那我們是不是可以把這類key給幹掉呢?

    幹掉key的過程,就是內存淘汰。

    內存淘汰什麼時候啟用

    當我們在配置文件里設置了如下屬性時:

    # maxmemory <bytes>
    

    默認,該屬性是被註釋掉的。

    其實,這個配置項的註釋,相當有價值,我們來看看:

    # Don't use more memory than the specified amount of bytes.
    # When the memory limit is reached Redis will try to remove keys
    # according to the eviction policy selected (see maxmemory-policy).
    #
    # If Redis can't remove keys according to the policy, or if the policy is
    # set to 'noeviction', Redis will start to reply with errors to commands
    # that would use more memory, like SET, LPUSH, and so on, and will continue
    # to reply to read-only commands like GET.
    #
    # This option is usually useful when using Redis as an LRU cache, or to set
    # a hard memory limit for an instance (using the 'noeviction' policy).
    #
    # WARNING: If you have slaves attached to an instance with maxmemory on,
    # the size of the output buffers needed to feed the slaves are subtracted
    # from the used memory count, so that network problems / resyncs will
    # not trigger a loop where keys are evicted, and in turn the output
    # buffer of slaves is full with DELs of keys evicted triggering the deletion
    # of more keys, and so forth until the database is completely emptied.
    #
    # In short... if you have slaves attached it is suggested that you set a lower
    # limit for maxmemory so that there is some free RAM on the system for slave
    # output buffers (but this is not needed if the policy is 'noeviction').
    #
    # maxmemory <bytes>
    

    渣翻譯如下:

    不能使用超過指定數量bytes的內存。當該內存限制被達到時,redis會根據過期策略(eviction policy,通過參數 maxmemory-policy來指定)來驅逐key。

    如果redis根據指定的策略,或者策略被設置為“noeviction”,redis會開始針對如下這種命令,回復錯誤。什麼命令呢?會使用更多內存的那類命令,比如set、lpush;只讀命令還是不受影響,可以正常響應。

    該選項通常在redis使用LRU緩存時有用,或者在使用noeviction策略時,設置一個進程級別的內存limit。

    內存淘汰策略

    所謂策略,意思是,當我們要刪除部分key的時候,刪哪些,不刪哪些?是不是需要一個策略?比如是隨機刪,就像滅霸一樣?還是按照lru時間來刪,lru的策略意思就是,最近最少使用的key,將被優先刪除。

    總之,我們需要定一個規則。

    redis默認支持以下策略:

    # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
    # is reached. You can select among five behaviors:
    # 
    # volatile-lru -> remove the key with an expire set using an LRU algorithm
    # allkeys-lru -> remove any key accordingly to the LRU algorithm
    # volatile-random -> remove a random key with an expire set
    # allkeys-random -> remove a random key, any key
    # volatile-ttl -> remove the key with the nearest expire time (minor TTL)
    # noeviction -> don't expire at all, just return an error on write operations
    # 
    # Note: with any of the above policies, Redis will return an error on write
    #       operations, when there are not suitable keys for eviction.
    #
    #       At the date of writing this commands are: set setnx setex append
    #       incr decr rpush lpush rpushx lpushx linsert lset rpoplpush sadd
    #       sinter sinterstore sunion sunionstore sdiff sdiffstore zadd zincrby
    #       zunionstore zinterstore hset hsetnx hmset hincrby incrby decrby
    #       getset mset msetnx exec sort
    #
    # The default is:
    #
    # maxmemory-policy noeviction
    maxmemory-policy allkeys-lru
    
    針對設置了過期時間的,使用lru算法
    # volatile-lru -> remove the key with an expire set using an LRU algorithm
    
    針對全部key,使用lru算法
    # allkeys-lru -> remove any key accordingly to the LRU algorithm
    
    針對設置了過期時間的,隨機刪
    # volatile-random -> remove a random key with an expire set
    
    針對全部key,隨機刪
    # allkeys-random -> remove a random key, any key
    
    針對設置了過期時間的,馬上要過期的,刪掉
    # volatile-ttl -> remove the key with the nearest expire time (minor TTL)
    
    不過期,不能寫了,就報錯
    # noeviction -> don't expire at all, just return an error on write operations
    

    一般呢,我們會設置為:

    allkeys-lru,即,針對全部key,進行lru。

    源碼實現

    配置讀取

    在如下結構體中,定義了如下字段:

    struct redisServer {
    	...
    	unsigned long long maxmemory;   /* Max number of memory bytes to use */
        int maxmemory_policy;           /* Policy for key eviction */
        int maxmemory_samples;          /* Pricision of random sampling */
        ...
    }
    

    當我們在配置文件中,進入如下配置時,該結構體中幾個字段的值如下:

    maxmemory 3mb
    maxmemory-policy allkeys-lru
    # maxmemory-samples 5  這個取了默認值
    

    maxmemory_policy為3,是因為枚舉值為3:

    #define REDIS_MAXMEMORY_VOLATILE_LRU 0
    #define REDIS_MAXMEMORY_VOLATILE_TTL 1
    #define REDIS_MAXMEMORY_VOLATILE_RANDOM 2
    #define REDIS_MAXMEMORY_ALLKEYS_LRU 3
    #define REDIS_MAXMEMORY_ALLKEYS_RANDOM 4
    #define REDIS_MAXMEMORY_NO_EVICTION 5
    #define REDIS_DEFAULT_MAXMEMORY_POLICY REDIS_MAXMEMORY_NO_EVICTION
    

    處理命令時,判斷是否進行內存淘汰

    在處理命令的時候,會調用中的

    redis.c  processCommand
        
    int processCommand(redisClient *c) {
        /* The QUIT command is handled separately. Normal command procs will
         * go through checking for replication and QUIT will cause trouble
         * when FORCE_REPLICATION is enabled and would be implemented in
         * a regular command proc. */
        // 特別處理 quit 命令
        void *commandName = c->argv[0]->ptr;
        redisLog(REDIS_NOTICE, "The server is now processing %s", commandName);
    
        if (!strcasecmp(c->argv[0]->ptr, "quit")) {
            addReply(c, shared.ok);
            c->flags |= REDIS_CLOSE_AFTER_REPLY;
            return REDIS_ERR;
        }
    
        /* Now lookup the command and check ASAP about trivial error conditions
         * such as wrong arity, bad command name and so forth. */
        // 1 查找命令,並進行命令合法性檢查,以及命令參數個數檢查
        c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
        if (!c->cmd) {
            // 沒找到指定的命令
            flagTransaction(c);
            addReplyErrorFormat(c, "unknown command '%s'",
                                (char *) c->argv[0]->ptr);
            return REDIS_OK;
        }
    
        /* Check if the user is authenticated */
        //2 檢查認證信息
        if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand) {
            flagTransaction(c);
            addReply(c, shared.noautherr);
            return REDIS_OK;
        }
    
        /* If cluster is enabled perform the cluster redirection here.
         *
         * 3 如果開啟了集群模式,那麼在這裏進行轉向操作。
         *
         * However we don't perform the redirection if:
         *
         * 不過,如果有以下情況出現,那麼節點不進行轉向:
         *
         * 1) The sender of this command is our master.
         *    命令的發送者是本節點的主節點
         *
         * 2) The command has no key arguments. 
         *    命令沒有 key 參數
         */
        if (server.cluster_enabled &&
            !(c->flags & REDIS_MASTER) &&
            !(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0)) {
            int hashslot;
    
            // 集群已下線
            if (server.cluster->state != REDIS_CLUSTER_OK) {
                flagTransaction(c);
                addReplySds(c, sdsnew("-CLUSTERDOWN The cluster is down. Use CLUSTER INFO for more information\r\n"));
                return REDIS_OK;
    
                // 集群運作正常
            } else {
                int error_code;
                clusterNode *n = getNodeByQuery(c, c->cmd, c->argv, c->argc, &hashslot, &error_code);
                // 不能執行多鍵處理命令
                if (n == NULL) {
                    flagTransaction(c);
                    if (error_code == REDIS_CLUSTER_REDIR_CROSS_SLOT) {
                        addReplySds(c, sdsnew("-CROSSSLOT Keys in request don't hash to the same slot\r\n"));
                    } else if (error_code == REDIS_CLUSTER_REDIR_UNSTABLE) {
                        /* The request spawns mutliple keys in the same slot,
                         * but the slot is not "stable" currently as there is
                         * a migration or import in progress. */
                        addReplySds(c, sdsnew("-TRYAGAIN Multiple keys request during rehashing of slot\r\n"));
                    } else {
                        redisPanic("getNodeByQuery() unknown error.");
                    }
                    return REDIS_OK;
    
                    //3.1 命令針對的槽和鍵不是本節點處理的,進行轉向
                } else if (n != server.cluster->myself) {
                    flagTransaction(c);
                    // -<ASK or MOVED> <slot> <ip>:<port>
                    // 例如 -ASK 10086 127.0.0.1:12345
                    addReplySds(c, sdscatprintf(sdsempty(),
                                                "-%s %d %s:%d\r\n",
                                                (error_code == REDIS_CLUSTER_REDIR_ASK) ? "ASK" : "MOVED",
                                                hashslot, n->ip, n->port));
    
                    return REDIS_OK;
                }
    
                // 如果執行到這裏,說明鍵 key 所在的槽由本節點處理
                // 或者客戶端執行的是無參數命令
            }
        }
    
        /* Handle the maxmemory directive.
         *
         * First we try to free some memory if possible (if there are volatile
         * keys in the dataset). If there are not the only thing we can do
         * is returning an error. */
        //4 如果設置了最大內存,那麼檢查內存是否超過限制,並做相應的操作
        if (server.maxmemory) {
            //4.1 如果內存已超過限制,那麼嘗試通過刪除過期鍵來釋放內存
            int retval = freeMemoryIfNeeded();
            // 如果即將要執行的命令可能佔用大量內存(REDIS_CMD_DENYOOM)
            // 並且前面的內存釋放失敗的話
            // 那麼向客戶端返回內存錯誤
            if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
                flagTransaction(c);
                addReply(c, shared.oomerr);
                return REDIS_OK;
            }
        }    
        ....
    
    • 1處,查找命令,對應的函數指針(類似於java里的策略模式,根據命令,找對應的策略)
    • 2處,檢查,是否密碼正確
    • 3處,集群相關操作;
    • 3.1處,不是本節點處理,直接返回ask,指示客戶端轉向
    • 4處,判斷是否設置了maxMemory,這裏就是本文重點:設置了maxMemory時,內存淘汰策略
    • 4.1處,調用了下方的 freeMemoryIfNeeded

    接下來,深入4.1處:

    
    int freeMemoryIfNeeded(void) {
        size_t mem_used, mem_tofree, mem_freed;
        int slaves = listLength(server.slaves);
    
        /* Remove the size of slaves output buffers and AOF buffer from the
         * count of used memory. */
        // 計算出 Redis 目前佔用的內存總數,但有兩個方面的內存不會計算在內:
        // 1)從服務器的輸出緩衝區的內存
        // 2)AOF 緩衝區的內存
        mem_used = zmalloc_used_memory();
        if (slaves) {
    		...
        }
        if (server.aof_state != REDIS_AOF_OFF) {
            mem_used -= sdslen(server.aof_buf);
            mem_used -= aofRewriteBufferSize();
        }
    
        /* Check if we are over the memory limit. */
        //1 如果目前使用的內存大小比設置的 maxmemory 要小,那麼無須執行進一步操作
        if (mem_used <= server.maxmemory) return REDIS_OK;
    
        //2 如果佔用內存比 maxmemory 要大,但是 maxmemory 策略為不淘汰,那麼直接返回
        if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
            return REDIS_ERR; /* We need to free memory, but policy forbids. */
    
        /* Compute how much memory we need to free. */
        // 3 計算需要釋放多少字節的內存
        mem_tofree = mem_used - server.maxmemory;
    
        // 初始化已釋放內存的字節數為 0
        mem_freed = 0;
    
        // 根據 maxmemory 策略,
        //4 遍歷字典,釋放內存並記錄被釋放內存的字節數
        while (mem_freed < mem_tofree) {
            int j, k, keys_freed = 0;
    
            // 遍歷所有字典
            for (j = 0; j < server.dbnum; j++) {
                long bestval = 0; /* just to prevent warning */
                sds bestkey = NULL;
                dictEntry *de;
                redisDb *db = server.db + j;
                dict *dict;
    
                if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                    server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM) {
                    // 如果策略是 allkeys-lru 或者 allkeys-random 
                    //5 那麼淘汰的目標為所有數據庫鍵
                    dict = server.db[j].dict;
                } else {
                    // 如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl 
                    //6 那麼淘汰的目標為帶過期時間的數據庫鍵
                    dict = server.db[j].expires;
                }
    
    
                /* volatile-random and allkeys-random policy */
                // 如果使用的是隨機策略,那麼從目標字典中隨機選出鍵
                if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                    server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM) {
                    de = dictGetRandomKey(dict);
                    bestkey = dictGetKey(de);
                }
                /* volatile-lru and allkeys-lru policy */
                //7 
                else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                         server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU) {
                    struct evictionPoolEntry *pool = db->eviction_pool;
    
                    while (bestkey == NULL) {
                        // 8 
                        evictionPoolPopulate(dict, db->dict, db->eviction_pool);
                        /* Go backward from best to worst element to evict. */
                        for (k = REDIS_EVICTION_POOL_SIZE - 1; k >= 0; k--) {
                            if (pool[k].key == NULL) continue;
                            // 8.1
                            de = dictFind(dict, pool[k].key);
    
                            /* 8.2 Remove the entry from the pool. */
                            sdsfree(pool[k].key);
                            /* Shift all elements on its right to left. */
                            memmove(pool + k, pool + k + 1,
                                    sizeof(pool[0]) * (REDIS_EVICTION_POOL_SIZE - k - 1));
                            /* Clear the element on the right which is empty
                             * since we shifted one position to the left.  */
                            pool[REDIS_EVICTION_POOL_SIZE - 1].key = NULL;
                            pool[REDIS_EVICTION_POOL_SIZE - 1].idle = 0;
    
                            /* If the key exists, is our pick. Otherwise it is
                             * a ghost and we need to try the next element. */
                            // 8.3
                            if (de) {
                                bestkey = dictGetKey(de);
                                break;
                            } else {
                                /* Ghost... */
                                continue;
                            }
                        }
                    }
                }
    
                    /* volatile-ttl */
                    // 策略為 volatile-ttl ,從一集 sample 鍵中選出過期時間距離當前時間最接近的鍵
                else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                    ...
                }
    
                /* Finally remove the selected key. */
                // 8.4 刪除被選中的鍵
                if (bestkey) {
                    long long delta;
    
                    robj *keyobj = createStringObject(bestkey, sdslen(bestkey));
                    propagateExpire(db, keyobj);
                    /* We compute the amount of memory freed by dbDelete() alone.
                     * It is possible that actually the memory needed to propagate
                     * the DEL in AOF and replication link is greater than the one
                     * we are freeing removing the key, but we can't account for
                     * that otherwise we would never exit the loop.
                     *
                     * AOF and Output buffer memory will be freed eventually so
                     * we only care about memory used by the key space. */
                    // 計算刪除鍵所釋放的內存數量
                    delta = (long long) zmalloc_used_memory();
                    dbDelete(db, keyobj);
                    delta -= (long long) zmalloc_used_memory();
                    mem_freed += delta;
    
                    // 對淘汰鍵的計數器增一
                    server.stat_evictedkeys++;
    
                    notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted",
                                        keyobj, db->id);
                    decrRefCount(keyobj);
                    keys_freed++;
    				...
                }
            }
    
            if (!keys_freed) return REDIS_ERR; /* nothing to free... */
        }
    
        return REDIS_OK;
    }
    
    • 1處,如果目前使用的內存大小比設置的 maxmemory 要小,那麼無須執行進一步操作

    • 2處,如果佔用內存比 maxmemory 要大,但是 maxmemory 策略為不淘汰,那麼直接返回

    • 3處,計算需要釋放多少字節的內存

    • 4處,遍歷字典,釋放內存並記錄被釋放內存的字節數

    • 5處,如果策略是 allkeys-lru 或者 allkeys-random 那麼淘汰的目標為所有數據庫鍵

    • 6處,如果策略是 volatile-lru 、 volatile-random 或者 volatile-ttl ,那麼淘汰的目標為帶過期時間的數據庫鍵

    • 7處,如果使用的是 LRU 策略, 那麼從 sample 鍵中選出 IDLE 時間最長的那個鍵

    • 8處,調用evictionPoolPopulate,該函數在下面講解,該函數的功能是,傳入一個鏈表,即這裏的db->eviction_pool,然後在函數內部,隨機找出n個key,放入傳入的鏈表中,並按照空閑時間排序,空閑最久的,放到最後。

      當該函數,返回后,db->eviction_pool這個鏈表裡就存放了我們要淘汰的key。

    • 8.1處,找到這個key,這個key,在後邊會被刪除

    • 8.2處,下面這一段,從db->eviction_pool將這個已經處理了的key刪掉

    • 8.3處,如果這個key,是存在的,則跳出循環,在後面8.4處,會被刪除

    • 8.4處,刪除這個key

    選擇哪些key作為被淘汰的key

    前面我們看到,在7處,如果為lru策略,則會進入8處的函數:

    evictionPoolPopulate。

    該函數的名稱為:填充(populate)驅逐(eviction)對象池(pool)。驅逐的意思,就是現在達到了maxmemory,沒辦法,只能開始刪除掉一部分元素,來騰空間了,不然新的put類型的命令,根本沒辦法執行。

    該方法的大概思路是,使用lru的時候,隨機找n個key,類似於抽樣,然後放到一個鏈表,根據空閑時間排序。

    具體看看該方法的實現:

    void evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    

    其中,傳入的第三個參數,是要被填充的對象,在c語言中,習慣傳入一個入參,然後在函數內部填充或者修改入參對象的屬性。

    該屬性,就是前面說的那個鏈表,用來存放收集的隨機的元素,該鏈表中節點的結構如下:

    struct evictionPoolEntry {
        unsigned long long idle;    /* Object idle time. */
        sds key;                    /* Key name. */
    };
    

    該結構共2個字段,一個存儲key,一個存儲空閑時間。

    該鏈表中,共maxmemory-samples個元素,會按照idle時間長短排序,idle時間長的在鏈表尾部,(假設頭在左,尾在右)。

    void evictionPoolPopulate(dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
        int j, k, count;
        dictEntry *_samples[EVICTION_SAMPLES_ARRAY_SIZE];
        dictEntry **samples;
    
        /* Try to use a static buffer: this function is a big hit...
         * Note: it was actually measured that this helps. */
        if (server.maxmemory_samples <= EVICTION_SAMPLES_ARRAY_SIZE) {
            samples = _samples;
        } else {
            samples = zmalloc(sizeof(samples[0]) * server.maxmemory_samples);
        }
    
        /* 1 Use bulk get by default. */
        count = dictGetRandomKeys(sampledict, samples, server.maxmemory_samples);
    
    	// 2
        for (j = 0; j < count; j++) {
            unsigned long long idle;
            sds key;
            robj *o;
            dictEntry *de;
    
            de = samples[j];
            key = dictGetKey(de);
            /* If the dictionary we are sampling from is not the main
             * dictionary (but the expires one) we need to lookup the key
             * again in the key dictionary to obtain the value object. */
            if (sampledict != keydict) de = dictFind(keydict, key);
            // 3
            o = dictGetVal(de);
            // 4
            idle = estimateObjectIdleTime(o);
    
            /* 5  Insert the element inside the pool.
             * First, find the first empty bucket or the first populated
             * bucket that has an idle time smaller than our idle time. */
            k = 0;
            while (k < REDIS_EVICTION_POOL_SIZE &&
                   pool[k].key &&
                   pool[k].idle < idle)
                k++;
            
    		...
                
            // 6
            pool[k].key = sdsdup(key);
            pool[k].idle = idle;
        }
        if (samples != _samples) zfree(samples);
    }
    
    • 1處,獲取 server.maxmemory_samples個key,這裡是隨機獲取的,(dictGetRandomKeys),這個值,默認值為5,放到samples中

    • 2處,遍歷返回來的samples

    • 3處,調用如下宏,獲取val

      he的類型為dictEntry:

      /*
       * 哈希表節點
       */
      typedef struct dictEntry {
          
          // 鍵
          void *key;
      
          // 值
          union {
              // 1
              void *val;
              uint64_t u64;
              int64_t s64;
          } v;
      
          // 指向下個哈希表節點,形成鏈表
          struct dictEntry *next;
      
      } dictEntry;
      

      所以,這裏去

      robj *o; 
      
      o = dictGetVal(de);
      

      實際就是獲取其v屬性中的val,(1處):

      #define dictGetVal(he) ((he)->v.val)
      
    • 4處,準備計算該val的空閑時間

      我們上面3處,看到,獲取的o的類型為robj。我們現在看看怎麼計算對象的空閑時長:

      /* Given an object returns the min number of milliseconds the object was never
       * requested, using an approximated LRU algorithm. */
      unsigned long long estimateObjectIdleTime(robj *o) {
          //4.1 獲取系統的當前時間
          unsigned long long lruclock = LRU_CLOCK();
          // 4.2
          if (lruclock >= o->lru) {
              // 4.3
              return (lruclock - o->lru) * REDIS_LRU_CLOCK_RESOLUTION;
          } else {
              return (lruclock + (REDIS_LRU_CLOCK_MAX - o->lru)) *
                          REDIS_LRU_CLOCK_RESOLUTION;
          }
      }
      

      這裏,4.1處,獲取系統的當前時間;

      4.2處,如果系統時間,大於對象的lru時間

      4.3處,則用系統時間減去對象的lru時間,再乘以單位,換算為毫秒,最終返回的單位,為毫秒(可以看註釋。)

      #define REDIS_LRU_CLOCK_RESOLUTION 1000 /* LRU clock resolution in ms */
      
    • 5處,這裏拿當前元素,和pool中已經放進去的元素,從第0個開始比較,如果當前元素的idle時長,大於pool中指針0指向的元素,則和pool中索引1的元素比較;直到條件不滿足為止。

      這句話意思就是,類似於冒泡,把當前元素一直往後冒,直到idle時長小於被比較的元素為止。

    • 6處,把當前元素放進pool中。

    經過上面的處理后,鏈表中存放了全部的抽樣元素,且ide時間最長的,在最右邊。

    對象還有字段存儲空閑時間?

    前面4處,說到,用系統的當前時間,減去對象的lru時間。

    大家看看對象的結構體

    typedef struct redisObject {
    
        // 類型
        unsigned type:4;
    
        // 編碼
        unsigned encoding:4;
    
        //1 對象最後一次被訪問的時間
        unsigned lru:REDIS_LRU_BITS; /* lru time (relative to server.lruclock) */
    
        // 引用計數
        int refcount;
    
        // 指向實際值的指針
        void *ptr;
    
    } robj;
    

    上面1處,lru屬性,就是用來存儲這個。

    創建對象時,直接使用當前系統時間創建

    robj *createObject(int type, void *ptr) {
    
        robj *o = zmalloc(sizeof(*o));
    
        o->type = type;
        o->encoding = REDIS_ENCODING_RAW;
        o->ptr = ptr;
        o->refcount = 1;
    
        /*1 Set the LRU to the current lruclock (minutes resolution). */
        o->lru = LRU_CLOCK();
        return o;
    }
    

    1處即是。

    robj *createEmbeddedStringObject(char *ptr, size_t len) {
        robj *o = zmalloc(sizeof(robj)+sizeof(struct sdshdr)+len+1);
        struct sdshdr *sh = (void*)(o+1);
    
        o->type = REDIS_STRING;
        o->encoding = REDIS_ENCODING_EMBSTR;
        o->ptr = sh+1;
        o->refcount = 1;
        // 1
        o->lru = LRU_CLOCK();
    
        sh->len = len;
        sh->free = 0;
        if (ptr) {
            memcpy(sh->buf,ptr,len);
            sh->buf[len] = '\0';
        } else {
            memset(sh->buf,0,len+1);
        }
        return o;
    }
    

    1處即是。

    每次查找該key時,刷新時間

    robj *lookupKey(redisDb *db, robj *key) {
    
        // 查找鍵空間
        dictEntry *de = dictFind(db->dict,key->ptr);
    
        // 節點存在
        if (de) {
            
    
            // 取出值
            robj *val = dictGetVal(de);
    
            /* Update the access time for the ageing algorithm.
             * Don't do it if we have a saving child, as this will trigger
             * a copy on write madness. */
            // 更新時間信息(只在不存在子進程時執行,防止破壞 copy-on-write 機制)
            if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
                // 1
                val->lru = LRU_CLOCK();
    
            // 返回值
            return val;
        } else {
    
            // 節點不存在
    
            return NULL;
        }
    }
    

    1處即是,包括get、set等各種操作,都會刷新該時間。

    仔細看下面的堆棧,set的,get同理:

    總結

    大家有沒有更清楚一些呢?

    總的來說,就是,設置了max-memory后,達到該內存限制后,會在處理命令時,檢查是否要進行內存淘汰;如果要淘汰,則根據maxmemory-policy的策略來。

    隨機選擇maxmemory-sample個元素,按照空閑時間排序,拉鏈表;挨個挨個清除。

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

    【其他文章推薦】

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

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

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

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

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

  • 加州野火每5秒燒1英畝 吞噬酒莊數萬人撤離

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

    美國加州野火在強風助長下,每5秒鐘延燒約1英畝的土地,蔓延到世界知名的葡萄酒之鄉,納帕(Napa)與索諾馬(Sonoma)山谷今天有數以萬計的民眾被迫逃離家園。

    根據美國國家海洋暨大氣總署(NOAA)衛星影像,昨天清晨約4時從納帕山谷爆發的「玻璃之火」(Glass Fire),昨晚延燒了2500英畝的土地,到了今早擴大到1萬1000英畝,相當於每5秒鐘燒掉約1英畝(約0.4公頃)。

    法新社報導,加州森林防火廳(Cal Fire)說,加州野火把天空染成橘紅色,在悶熱的熱浪侵襲之下,火勢以「危險的速度」蔓延,且沒有一處獲得控制,沿途燒毀數座葡萄園與建築物。

    官員說,當局已下令近3萬4000名居民疏散,並要求約1萬4000人準備立即撤離,因為「迅速蔓延的火勢」延燒到乾燥的植被以及難以進入的山區。

    氣候變遷
    國際新聞
    美國
    加州
    野火
    森林野火

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

    【其他文章推薦】

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

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

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

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

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