學習筆記:cache 和spring cache(3)--本地緩存-分佈式緩存,緩存穿透,雪崩,和熱點key的問題

title: 學習筆記:cache 和spring cache 技術---本地緩存-分佈式緩存,緩存穿透,雪崩,和熱點key的問題 author: Eric liu tags: [] categories:html

  • hexo

JVM緩存(本地緩存)

將數據緩存在JVM中,使用Map或者Guava的Table來保存數據。redis

考慮因素: 使用內存緩存時,需考慮緩存數據消耗多大內存。spring

優點:sql

  • 無網路交互,減小網絡抖動對服務的影響;
  • 查詢速度快;

場景:數據庫

適用數據基本不怎麼變化的數據上

分佈式緩存

將數據緩存在緩存中間件中,例如,redis、memcached。後端

推薦使用redis:緩存

  • redis支持的數據持久化;
  • redis支持多種數據結構存儲;
  • redis有專業團隊維護;

redis 與memcached

(1)memcached:鍵值對 redis 還能夠支持更多形式tomcat

(2)一樣是內存數據庫,redis 能夠持久化,雖然redis是基於內存的存儲系統,可是他自己是支持內存數據的持久化,並且主要提供兩種主要的持久化策略,RDB快照和AOF日誌,memcache不能bash

(3)性能 :redis 單線程io複用,,只有IO操做來講,性能好,也有一些計算,如排序聚合,可是計算的時候影響吞吐量網絡

memcached 多線程,非阻塞io複用有對全局變量加鎖 性能有損耗,

(4)內存管理機制不一樣。

memcached 是提早將 分配的內存切分紅規定大小的塊,而後使用的使用 用多少分配多少,有一個空閒列表進行統計。 不會用內存碎片,可是存在內存浪費

redis,會把剩餘內存大小存在內存塊中,Redis使用現場申請內存的方式來存儲數據,會在必定程度上存在內存碎片。

在redis中,並非全部的數據都一一直存儲在內存中的,這是和memcached相比最大的一個區別
Redis只會緩存全部的key端的信息,若是redis發現內存的使用量超過某一個值,將觸發swap的操做,redis根據相應的表達式計算出那些key對應value須要swap到磁盤,而後再將這些這些key對應的value持久化到磁盤中,同時再內存清除。同時因爲redis將內存中的數據swap到磁盤的時候,提供服務的主線程和進行swap操做的子進程會共享這部份內存,因此若是更新須要swap的數據,redis將阻塞這個操做,直到子線程完成swap操做後才能夠進行修改
https://www.cnblogs.com/hanfei-1005/p/5692455.html
複製代碼

(5)數據一致性 memcached 有cas 保證,redis 提供了事務

參考文檔:http://blog.csdn.net/u013256816/article/details/51146314

本地緩存+分佈式緩存

在jvm以及redis中均緩存數據,服務優先從jvm獲取,miss後從redis中獲取

具體使用:
目前ugc 使用 本地緩存 guava 和 redis 分佈式緩存。

  使用spring cache的註解使用,經過名字區分指定使用哪一個緩存。  g- 開頭爲 使用本地緩存,在guava的建立方法裏判斷 若是非g-開頭 return null, 而後去redis 緩存中建立緩存

  針對失效時間沒有作特殊處理 如失效後加鎖 或失效前預處理等,由於量級不大 且沒有 某時刻的大流量。

  ugc 業務中 查詢固定的 和不太常改變的 使用本地緩存,文章等放在分佈式緩存中。
複製代碼

緩存預熱

在緩存初始化時,緩存中是沒有任何緩存數據的,需先將數據緩存後,緩存服務纔算徹底啓動。預熱方式:

  • miss後,實時查詢,而後更新緩存數據;
    1. 缺點1:多個tomcat實例同時查詢數據並跟新緩存,在一段時間內緩存近似於失效;
    2. 缺點2:在高併發場景下,沒法限制對數據庫訪問速度;

img

  • 經過task或接口預先加載服務,而後開啓緩存服務;
    1. 優點1:在初始化服務時,限制加載數據的速度;
    2. 優點2:批量查詢數據庫,減小與數據庫之間的網絡交互;

img

數據一致性問題

數據庫與緩存同時變動

當用戶發生數據變動時,優先更新數據庫數據。在更新數據庫數據成功後,再更新緩存中數據。儘可能避免緩存數據與數據庫數據不一致的狀況。

img

數據庫先變動、緩存保持最終一致性

當數據庫數據發生變動後,將變動後的key值放入到異步刷新緩存隊列中。後臺線程根據隊列中數據,刷新緩存數據。

img

先刷緩存會形成

緩存失效機制及處理方式

先刷後返

img

缺點:

  • 若是查詢的數據始終不存在,致使每次查詢都請求DB,緩存做用失效。例如,id=0的supplier數據。經過在緩存中,緩存一個默認數據;
  • 在高併發的狀況下,多個應用請求同時更新緩存,對緩存系統存在壓力。經過加鎖的方式來解決;
  • 在進行刷新的時,線程會被阻塞。在高併發的狀況下,會耗盡tomcat的線程資源;

先返後刷

img

在高併發下,異步隊列會對下游系統產生壓力。例如,10K的客戶端同時請求服務端,單個客戶端的請求QPS是200,且每次請求的key不一樣及緩存中不存在數據,則每次都將key寫入到數據庫中,則數據庫扛不住。所以,先將數據寫入本地中,先本地冪等,而後在異步的寫入到數據庫中及緩存中。所以,每次入異步隊列的時候,都查詢redis中是否已經將這個key放入異步刷新隊列中。若是已經放入待刷新隊列中,則再也不再次入隊列。

#緩存穿透的問題

問題:

  • 緩存穿透是指查詢一個根本不存在的數據,緩存層和存儲層都不會命中,
  • 緩存穿透將致使不存在的數據每次請求都要到存儲層去查詢,失去了緩存保護後端存儲的意義。

緣由:代碼問題, 爬蟲,攻擊,大量空命中

場景:查詢某個文章,給了一個錯誤的文章id。一直查詢不到。

方法:

緩存空對象

  • 空值作緩存,即緩存層中存了更多的鍵,這就須要更多的內存空間 ,能夠對其設置一個較短的過時時間,讓其自動清除。
  • 優勢是實時性高,代碼維護簡單。

能夠緩存到本地內存中,空對想用一個靜態變量。這樣不會形成 形成佔用內存。

#緩存雪崩的問題

問題:熱點key問題,這裏指 緩存層直接失效的問題。

方法:集羣,隔離組件 把重要資源隔離。讓每種資源都單獨運行在本身的線程池中。

而Hystrix 是解決依賴隔離的利器

熱點key

問題: 熱點key 緩存過時或者失效 形成段時間大量訪問數據庫

緣由:

​ 通常使用,緩存 + 過時時間的策略,加速接口的訪問速度,減小了後端負載,同時保證功能的更新

可是有兩個問題:

​ (1) 這個key是一個熱點key(例如一個重要的新聞,一個熱門的八卦新聞等等),因此這種key訪問量可能很是大。

​ (2) 緩存的構建是須要必定時間的。(多是一個複雜計算,例如複雜的sql、屢次IO、多個依賴(各類接口)等等)

從而在緩存失效的瞬間,有大量線程來構建緩存

熱點key 問題解決 一:如何解決失效時 大量併發

1.加鎖

(1)單機,synchronized ,spring cache 有sync 關鍵字

(2)分佈式,分佈式加鎖(redis,添加一個key_mutex , "1" , 若是添加上了 至關於獲取鎖,若是這個存在說明其餘人在用鎖,獲取失敗

String get(String key) {  
       String value = redis.get(key);  
       if (value  == null) {  
        if (redis.setnx(key_mutex, "1")) {  
            // 3 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 3 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
        } else {  
            //其餘線程休息50毫秒後重試  
            Thread.sleep(50);  
            get(key);  
        }  
      }  
    }  
複製代碼

缺點:擠滿線程池

2.不過時

redis 設置物理不過時,

異步-邏輯過時:存值中設置timeout,若是發現timeout 過時,後臺異步線程構建緩存

缺點:於性能很是友好,惟一不足的就是構建緩存時候,其他線程(非構建緩存的線程)可能訪問的是老數據,

String get(final String key) {  
            V v = redis.get(key);  
            String value = v.getValue();  
            long timeout = v.getTimeout();  
            if (v.timeout <= System.currentTimeMillis()) {  
                // 異步更新後臺異常執行  
                threadPool.execute(new Runnable() {  
                    public void run() {  
                        String keyMutex = "mutex:" + key;  
                        if (redis.setnx(keyMutex, "1")) {  
                            // 3 min timeout to avoid mutex holder crash  
                            redis.expire(keyMutex, 3 * 60);  
                            String dbValue = db.get(key);  
                            redis.set(key, dbValue);  
                            redis.delete(keyMutex);  
                        }  
                    }  
                });  
            }  
            return value;  
        }  
複製代碼

3.過時前 刷緩存

(1)

在value內部設置1個超時值(proTimeout), proTimeout比實際的redis timeout小。當從cache讀取到proTimeout發現它已通過期時候. 而後 加分佈式鎖,設置一個短暫的過時時間。保證有一個線程在刷緩存,其餘的正常使用。

若是這個線程刷緩存出了問題沒成功,短暫的過時時間 事後 鎖解開,下一個線程會機型刷緩存。

原創 僞代碼

String get(String key) {  
       String v = redis.get(key);  
       if (v  == null) {  
        if (redis.setnx(key_mutex, "1")) {  
            // 3 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 1 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
        } else {  
            //其餘線程休息50毫秒後重試  
            Thread.sleep(50);  
            get(key);  
        }  
      }  
     else{
       if (v.get(timeout) <= now()) {
         if (  redis.setnx(key_mutex, "1")  ) {
           // 1 min timeout to avoid mutex holder crash  
            redis.expire(key_mutex, 1 * 60)  
            value = db.get(key);  
            redis.set(key, value);  
            redis.delete(key_mutex);  
         	}   
         }
         //若是沒到提早的時間 或者有線程在刷,則繼續取
         return v.get(value);
       }  
     }
    
複製代碼

緩存數據的設計 也能夠是多值,

(不推薦,內存佔用多)兩個key,一個key用來存放數據,另外一個用來標記失效時間

好比key是aaa,設置失效時間爲30s,則另外一個key爲expire_aaa,失效時間爲25s。

好比一個key是aaa,失效時間是30s。查詢DB在1s內。

  • put數據時,設置aaa過時時間30s,設置expire_aaa過時時間25s;
  • get數據時,multiget  aaa 和 expire_aaa,若是expired_aaa對應的value != null,則直接返回aaa對應的數據給用戶。若是expire_aaa返回value == null,則後臺啓動一個任務,嘗試add expire_aaa,並設置超時過間爲3s。這裏設置爲3s是爲了防止後臺任務失敗或者阻塞,若是這個任務執行失敗,那麼3秒後,若是有另外的用戶訪問,那麼能夠再次嘗試查詢DB。若是add執行成功,則查詢DB,再更新aaa的緩存,並設置expire_aaa的超時時間爲25s。

若是是冷數據,30秒都沒有人訪問,那麼數據會過時。

若是是熱門數據,一直有大流量訪問,那麼數據就是一直熱的,並且數據一直不會過時。

(2)其餘失效前 刷緩存的方式

a.按期從DB裏查詢數據,再刷到redis 裏 有點扯,不適用 常變化的 緩存

b.緩存失效 加鎖查

相關文章
相關標籤/搜索