• 
    

      <address id="upfr9"><pre id="upfr9"><strike id="upfr9"></strike></pre></address>
      1. <address id="upfr9"><tr id="upfr9"></tr></address><dl id="upfr9"></dl>

        為什么大多數(shù)IP地址通常以192.168開頭?

        2021-03-06    分類: 網站建設

        我們使用緩存的主要目是提升查詢速度和保護數(shù)據庫等稀缺資源不被占滿。而緩存最常見的問題是緩存穿透、擊穿和雪崩,在高并發(fā)下這三種情況都會有大量請求落到數(shù)據庫,導致數(shù)據庫資源占滿,引起數(shù)據庫故障。今天我主要分享一下layering-cache緩存框架在這個三個問題上的實踐方案。

        概念

        緩存穿透

        在高并發(fā)下,查詢一個不存在的值時,緩存不會被命中,導致大量請求直接落到數(shù)據庫上,如活動系統(tǒng)里面查詢一個不存在的活動。

        緩存擊穿

        在高并發(fā)下,對一個特定的值進行查詢,但是這個時候緩存正好過期了,緩存沒有命中,導致大量請求直接落到數(shù)據庫上,如活動系統(tǒng)里面查詢活動信息,但是在活動進行過程中活動緩存突然過期了。

        緩存雪崩

        在高并發(fā)下,大量的緩存key在同一時間失效,導致大量的請求落到數(shù)據庫上,如活動系統(tǒng)里面同時進行著非常多的活動,但是在某個時間點所有的活動緩存全部過期。

        常見解決方案

        • 直接緩存NULL值
        • 限流
        • 緩存預熱
        • 分級緩存
        • 緩存永遠不過期

        layering-cache實踐

        在layering-cache里面結合了緩存NULL值,緩存預熱,限流、分級緩存和間接的實現(xiàn)"永不過期"等幾種方案來應對緩存穿透、擊穿和雪崩問題。

        直接緩存NULL值

        應對緩存穿透最有效的方法是直接緩存NULL值,但是緩存NULL的時間不能太長,否則NULL數(shù)據長時間得不到更新,也不能太短,否則達不到防止緩存擊穿的效果。

        我在layering-cache對NULL值進行了特殊處理,一級緩存不允許存NULL值,二級緩存可以配置緩存是否允許存NULL值,如果配置可以允許存NULL值,框架還支持配置緩存非空值和NULL值之間的過期時間倍率,這使得我們能精準的控制每一個緩存的NULL值過期時間,控制粒度非常細。當NULL緩存過期我還可以使用限流,緩存預熱等手段來防止穿透。

        示例:

        1. @Cacheable(value = "people", key = "#person.id", depict = "用戶信息緩存", 
        2.         firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES), 
        3.         secondaryCache = @SecondaryCache(expireTime = 10, timeUnit = TimeUnit.HOURS, 
        4.                 isAllowNullValue = true, magnification = 10)) 
        5. public Person findOne(Person person) { 
        6.     Person p = personRepository.findOne(Example.of(person)); 
        7.     logger.info("為id、key為:" + p.getId() + "數(shù)據做了緩存"); 
        8.     return p; 

        在這個例子里面isAllowNullValue = true表示允許換存NULL值,magnification = 10表示NULL值和非NULL值之間的時間倍率是10,也就是說當緩存值為NULL是,二級緩存的有效時間將是1個小時。

        限流

        應對緩存穿透的常用方法之一是限流,常見的限流算法有滑動窗口,令牌桶算法和漏桶算法,或者直接使用隊列、加鎖等,在layering-cache里面我主要使用分布式鎖來做限流。

        layering-cache數(shù)據讀取流程:

        數(shù)據讀取流程.jpg

        下面是讀取數(shù)據的核心代碼:

        1. private <T> T executeCacheMethod(RedisCacheKey redisCacheKey, Callable<T> valueLoader) { 
        2.     Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_sync_lock"); 
        3.     // 同一個線程循環(huán)20次查詢緩存,每次等待20毫秒,如果還是沒有數(shù)據直接去執(zhí)行被緩存的方法 
        4.     for (int i = 0; i < RETRY_COUNT; i++) { 
        5.         try { 
        6.             // 先取緩存,如果有直接返回,沒有再去做拿鎖操作 
        7.             Object result = redisTemplate.opsForValue().get(redisCacheKey.getKey()); 
        8.             if (result != null) { 
        9.                 logger.debug("redis緩存 key= {} 獲取到鎖后查詢查詢緩存命中,不需要執(zhí)行被緩存的方法", redisCacheKey.getKey()); 
        10.                 return (T) fromStoreValue(result); 
        11.             } 
        12.  
        13.  
        14.             // 獲取分布式鎖去后臺查詢數(shù)據 
        15.             if (redisLock.lock()) { 
        16.                 T t = loaderAndPutValue(redisCacheKey, valueLoader, true); 
        17.                 logger.debug("redis緩存 key= {} 從數(shù)據庫獲取數(shù)據完畢,喚醒所有等待線程", redisCacheKey.getKey()); 
        18.                 // 喚醒線程 
        19.                 container.signalAll(redisCacheKey.getKey()); 
        20.                 return t; 
        21.             } 
        22.             // 線程等待 
        23.             logger.debug("redis緩存 key= {} 從數(shù)據庫獲取數(shù)據未獲取到鎖,進入等待狀態(tài),等待{}毫秒", redisCacheKey.getKey(), WAIT_TIME); 
        24.             container.await(redisCacheKey.getKey(), WAIT_TIME); 
        25.         } catch (Exception e) { 
        26.             container.signalAll(redisCacheKey.getKey()); 
        27.             throw new LoaderCacheValueException(redisCacheKey.getKey(), e); 
        28.         } finally { 
        29.             redisLock.unlock(); 
        30.         } 
        31.     } 
        32.     logger.debug("redis緩存 key={} 等待{}次,共{}毫秒,任未獲取到緩存,直接去執(zhí)行被緩存的方法", redisCacheKey.getKey(), RETRY_COUNT, RETRY_COUNT * WAIT_TIME, WAIT_TIME); 
        33.     return loaderAndPutValue(redisCacheKey, valueLoader, true); 

        當需要加載緩存的時候,需要獲取到鎖才有權限到后臺去加載緩存數(shù)據,否則就會等待(同一個線程循環(huán)20次查詢緩存,每次等待20毫秒,如果還是沒有數(shù)據直接去執(zhí)行被緩存的方法,這個主要是為了防止獲取到鎖并且去加載緩存的線程出問題,沒有返回而導致死鎖)。當獲取到鎖的線程執(zhí)行完成會將獲取到的數(shù)據放到緩存中,并且喚醒所有等待線程。

        這里需要注意一下讓線程等待一定不能用Thread.sleep(),我在使用Spring Redis Cache的時候,我發(fā)現(xiàn)當并發(fā)達到300左右,緩存一旦過期就會引起死鎖,原因是使用的是sleep方法來讓沒有獲取到鎖的線程等待,當?shù)却木€程很多的時候會產生大量上下文切換,導致獲取到鎖的線程一直獲取不到cpu的執(zhí)行權,導致死鎖。在layering-cache里面,我們使用的是LockSupport.parkNanos方法,它會釋放cpu資源, 因為我們使用的是redis分布式鎖,所以也不能使用wait-notify機制。

        緩存預熱

        有效應對緩存的擊穿和雪崩的方式之一是緩存預加載。

        1. @Cacheable(value = "people", key = "#person.id", depict = "用戶信息緩存", 
        2.         firstCache = @FirstCache(expireTime = 10, timeUnit = TimeUnit.MINUTES), 
        3.         secondaryCache = @SecondaryCache(expireTime = 10, preloadTime = 2,timeUnit = TimeUnit.HOURS,)) 
        4. public Person findOne(Person person) { 
        5.     Person p = personRepository.findOne(Example.of(person)); 
        6.     logger.info("為id、key為:" + p.getId() + "數(shù)據做了緩存"); 
        7.     return p; 

        在 layering-cache里面二級緩存會配置兩個時間,expireTime是緩存的過期時間,preloadTime 是緩存的刷新時間(預加載時間)。每次二級緩存被命中都會去檢查緩存的過去時間是否小于刷新時間,如果小于就會開啟一個異步線程預先去更新緩存,并將新的值放到緩存中,有效的保證了熱點數(shù)據**"永不過期"**。這里預先更新緩存也是需要加鎖的,并不是所有的線程都會落到庫上刷新緩存,如果沒有獲取到鎖就直接結束當前線程。

        1. /** 
        2.  * 刷新緩存數(shù)據 
        3.  */ 
        4. private <T> void refreshCache(RedisCacheKey redisCacheKey, Callable<T> valueLoader, Object result) { 
        5.     Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 
        6.     Long preload = preloadTime; 
        7.     // 允許緩存NULL值,則自動刷新時間也要除以倍數(shù) 
        8.     boolean flag = isAllowNullValues() && (result instanceof NullValue || result == null); 
        9.     if (flag) { 
        10.         preload = preload / getMagnification(); 
        11.     } 
        12.     if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preload) { 
        13.         // 判斷是否需要強制刷新在開啟刷新線程 
        14.         if (!getForceRefresh()) { 
        15.             logger.debug("redis緩存 key={} 軟刷新緩存模式", redisCacheKey.getKey()); 
        16.             softRefresh(redisCacheKey); 
        17.         } else { 
        18.             logger.debug("redis緩存 key={} 強刷新緩存模式", redisCacheKey.getKey()); 
        19.             forceRefresh(redisCacheKey, valueLoader); 
        20.         } 
        21.     } 
          1. /** 
          2.  * 硬刷新(執(zhí)行被緩存的方法) 
          3.  * 
          4.  * @param redisCacheKey {@link RedisCacheKey} 
          5.  * @param valueLoader   數(shù)據加載器 
          6.  */ 
          7. private <T> void forceRefresh(RedisCacheKey redisCacheKey, Callable<T> valueLoader) { 
          8.     // 盡量少的去開啟線程,因為線程池是有限的 
          9.     ThreadTaskUtils.run(() -> { 
          10.         // 加一個分布式鎖,只放一個請求去刷新緩存 
          11.         Lock redisLock = new Lock(redisTemplate, redisCacheKey.getKey() + "_lock"); 
          12.         try { 
          13.             if (redisLock.lock()) { 
          14.                 // 獲取鎖之后再判斷一下過期時間,看是否需要加載數(shù)據 
          15.                 Long ttl = redisTemplate.getExpire(redisCacheKey.getKey()); 
          16.                 if (null != ttl && ttl > 0 && TimeUnit.SECONDS.toMillis(ttl) <= preloadTime) { 
          17.                     // 加載數(shù)據并放到緩存 
          18.                     loaderAndPutValue(redisCacheKey, valueLoader, false); 
          19.                 } 
          20.             } 
          21.         } catch (Exception e) { 
          22.             logger.error(e.getMessage(), e); 
          23.         } finally { 
          24.             redisLock.unlock(); 
          25.         } 
          26.     }); 

          在緩存總量和并發(fā)量都很大的時候,這個時候緩存如果同時失效,緩存預熱將是一個非常慢長的過程,就比如說服務重啟或新上線一個新的緩存。這個時候我們可以采用切流的方式,讓緩存慢慢預熱,如開始切10%流量,觀察沒有異常后,再切30%流量,觀察沒有異常后,再切60%流量,然后全量。這種方式雖然有點繁瑣,但是一旦遇到異常我們可以快速的切回流量,讓風險可控。

          當前名稱:為什么大多數(shù)IP地址通常以192.168開頭?
          URL地址:http://www.jbt999.com/news36/104486.html

          成都網站建設公司_創(chuàng)新互聯(lián),為您提供移動網站建設、企業(yè)建站、手機網站建設、網站營銷App設計、外貿網站建設

          廣告

          聲明:本網站發(fā)布的內容(圖片、視頻和文字)以用戶投稿、用戶轉載內容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:[email protected]。內容未經允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)

          成都seo排名網站優(yōu)化

        22. 
          

            <address id="upfr9"><pre id="upfr9"><strike id="upfr9"></strike></pre></address>
            1. <address id="upfr9"><tr id="upfr9"></tr></address><dl id="upfr9"></dl>
              国产视频1 | 久久久亚洲成人 | 色噜噜人妻av中文字幕 | 在线天堂资源19 | 这里只有精品在线观看 | 大香蕉大香蕉 | 在线观看无码视频 | 久久大胆人体免费视频 | 欧美日韩一级免费看 | 国模一区二区三区 |