Redis應用學習——緩存的使用與設計

1. 緩存的收益與成本

    1. 收益:java

  • 經過緩存加速讀寫速度。在內存中讀寫比硬盤速度快
  • 下降數據庫服務器的負載。好比業務端的請求的數據大多數都由Redis服務器來處理,大大減輕MySQL服務器的壓力

    2. 成本:redis

  • 數據不一致問題,好比Redis服務器與數據庫服務器之間的某些數據可能會發生不一致問題,這是由兩個服務器的數據更新策略不一樣引發的
  • 代碼維護成本,須要添加數據緩存的邏輯代碼
  • 運維成本,好比須要維護RedisCluster

    3. 使用場景:算法

  • 使用緩存來下降關係型數據庫服務器的負載,好比將某些業務須要讀寫的數據庫服務器中的一些數據存儲到緩存服務器中,而後這部分業務就能夠直接經過緩存服務器進行數據讀寫
  • 加快請求響應時間,Redis的數據是存儲在內存中的,因此能夠大大提升IO響應速度
  • 對於關係型數據庫服務器的大量寫操做,能夠先由Redis服務器進行批量寫操做,而後再將Redis服務器中批量寫操做的結果寫入到數據庫服務器中。好比計數器操做,若是要作1千萬次計數,不可能每次都要對數據庫服務器進行update操做,能夠在Redis服務器中經過incr key命令進行計數,批量執行完成後再將最後的結果寫入數據庫服務器

2. 緩存更新策略

    1. 超時刪除數據:也就是設置key的過時時間,好比經過expire命令設置的超時key,該策略數據一致性較低,但維護成本也低數據庫

    2. LRU/LFU/FIFO算法剔除:主要是針對當Redis的緩存數據達到設置的最大內存如何處理的策略,該策略數據一致性很低,但維護成本也低後端

    3. 主動更新:在開發過程當中經過編寫邏輯代碼,控制數據更新,數據一致性高,但維護成本也高緩存

    4. 使用狀況:數據一致性要求低就使用最大內存時數據淘汰策略,若是數據一致性要求高,就將主動更新和超時刪除結合使用,最大內存時數據淘汰策略保底服務器

3. 緩存粒度控制

    1. 緩存粒度:以用戶信息爲例,在MySQL中用戶表包含多個字段,經過select語句查詢得到用戶信息時,到底是選擇緩存用戶每一個字段的數據,仍是選擇某幾個重要字段的數據進行緩存,緩存全部字段或部分字段就是指緩存粒度,能夠理解爲緩存粒度就是指緩存對象的數據的完整性併發

    2. 緩存粒度控制:運維

  • 從通用性角度來看,確定是使用全量屬性更好
  • 從佔用空間角度來看,部分屬性更好
  • 代碼維護上來看,全量屬性更好,由於若是緩存部分屬性須要增刪屬性時,比較麻煩

4. 緩存穿透問題

    1. 緩存穿透:在實際應用中,業務層會先向緩存層發出數據請求,若是過這些請求沒有命中(緩存層沒有請求的數據),那麼就會向關係型數據庫層發出請求,從關係型數據庫中取出數據回寫到緩存層中,並返回給業務層,在下一次請求時就能夠從緩存層返回數據;但若是數據庫中也沒有請求的數據,那麼就會返回業務層空值,在後續業務層的請求同一個數據時,緩存層始終都沒有數據,那麼每次都會向數據庫層請求數據,這樣就形成了緩存穿透。異步

    2. 產生緩存穿透的緣由:

  • 業務邏輯代碼自身問題
  • 惡意攻擊、爬蟲等

    3. 解決緩存穿透的方法:

  • 緩存空對象,即當緩存層、數據庫層皆沒有業務層請求的數據時,就向緩存層中寫入一個null,問題就是可能會須要更多的key,通常會給這些key設置一個較短的過時時間,另外一個問題就是緩存層和數據庫層出現短期的數據不一致,這個也能夠經過設置過時時間解決。
  • 布隆過濾器:能夠理解爲將key放置在布隆過濾器中,若是請求數據的key存在,則經過請求,不然阻止請求經過。將全部可能存在的數據的key哈希到一個足夠大的bitmap中,一個必定不存在的數據會被這個bitmap攔截掉,從而避免了對底層存儲系統的查詢壓力。

5. 無底洞問題

    1. 什麼是無底洞問題:一般狀況下,能夠經過增長集羣部署的機器數量來提高性能,可是在2010年,FaceBook發如今部署了3000個節點後發現性能反而降低;也就是說,集羣中有更多的機器不表明有更好的性能,但隨着數據量和併發處理量的提高,又必須提高集羣的機器數量,這就是無底洞問題,這個問題沒有好的解決辦法,只能是經過在細節方面的優化處理來儘可能提升性能,好比優化IO操做、優化Redis集羣中的批量命令執行等

6. 緩存雪崩

    1. 問題描述:若是緩存中部分key集中在一段時間內失效,發生大量的緩存穿透,全部的查詢都落在數據庫上,形成了緩存雪崩。形成這個問題的緣由除了是key失效之外,還多是緩存集羣宕機

    2. 解決方案:

  • 設置緩存永遠不過時:

    • 從緩存上看,確實沒有設置過時時間,這就保證了,不會出現熱點key過時問題,也就是「物理」不過時。

    • 從功能上看,把過時時間存在key對應的value裏,若是發現要過時了,經過一個後臺的異步線程進行緩存的構建,也就是「邏輯」過時

    •  從實戰看,這種方法對於性能很是友好,惟一不足的就是構建緩存時候,其他線程(非構建緩存的線程)可能訪問的是老數據

    • 示例僞代碼

      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")) {  
                              redis.expire(keyMutex, 3 * 60);  
                              String dbValue = db.get(key);  
                              redis.set(key, dbValue);  
                              redis.delete(keyMutex);  
                          }  
                      }  
                  });  
              }  
              return value;  
          }
  • 能夠在原有的失效時間基礎上增長一個隨機值,好比1-5分鐘隨機,這樣每個緩存的過時時間的重複率就會下降,就很難引起集體失效的事件。
  • 使用互斥鎖(mutex key): 這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其餘線程等待構建緩存的線程執行完,從新從緩存獲取數據就能夠了, 若是是單機,能夠用synchronized或者lock來處理,若是是分佈式環境能夠用分佈式鎖就能夠了(分佈式鎖,能夠用memcache的add, redis的setnx, zookeeper的添加節點操做),能保證數據一致性,可是可能會引發死鎖。

    對應的示例僞代碼

    String get(String key) {
    //首先嚐試從redis(或redis集羣)中獲取key對應的數據  
       String value = redis.get(key); 
    //若是爲null,則使用redis中的分佈式鎖
       if (value  == null) {  
    //經過setnx方法建立分佈式鎖
        if (redis.setnx(key_mutex, "1")) {  
            // 設置分佈式鎖的過時時間,能夠避免死鎖 
            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);  
        }  
      }  
    }
  • "提早"使用互斥鎖(mutex key):在value內部設置1個超時值(timeout1),。當從cache讀取到timeout1發現它已通過期時候,立刻延長timeout1並從新設置到cache。而後再從數據庫加載數據並設置到cache中。

7. 緩存擊穿

    1. 對於一些設置了過時時間的key,若是這些key可能會在某些時間點被超高併發地訪問,是一種很是「熱點」的數據。這個時候,須要考慮一個問題:緩存被「擊穿」的問題,這個和緩存雪崩的區別在於這裏針對某一key緩存,前者則是不少key。緩存在某個時間點過時的時候,剛好在這個時間點對這個Key有大量的併發請求過來,這些請求發現緩存過時通常都會從後端DB加載數據並回設到緩存,這個時候大併發的請求可能會瞬間把後端DB壓垮。

    2. 解決方案:

  • 使用互斥鎖(mutex key): 這種解決方案思路比較簡單,就是隻讓一個線程構建緩存,其餘線程等待構建緩存的線程執行完,從新從緩存獲取數據就能夠了, 若是是單機,能夠用synchronized或者lock來處理,若是是分佈式環境能夠用分佈式鎖就能夠了(分佈式鎖,能夠用memcache的add, redis的setnx, zookeeper的添加節點操做),可是可能會引發死鎖。分佈式鎖以及Java代碼實現後面詳細介紹

    對應的示例僞代碼

    String get(String key) {
    //首先嚐試從redis(或redis集羣)中獲取key對應的數據  
       String value = redis.get(key); 
    //若是爲null,則使用redis中的分佈式鎖
       if (value  == null) {  
    //經過setnx方法建立分佈式鎖
        if (redis.setnx(key_mutex, "1")) {  
            // 設置分佈式鎖的過時時間,能夠避免死鎖 
            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);  
        }  
      }  
    }
  • "提早"使用互斥鎖(mutex key):在value內部設置1個超時值(timeout1),。當從cache讀取到timeout1發現它已通過期時候,立刻延長timeout1並從新設置到cache。而後再從數據庫加載數據並設置到cache中。

  • 設置緩存永遠不過時:

    • 從緩存上看,確實沒有設置過時時間,這就保證了,不會出現熱點key過時問題,也就是「物理」不過時。

    • 從功能上看,把過時時間存在key對應的value裏,若是發現要過時了,經過一個後臺的異步線程進行緩存的構建,也就是「邏輯」過時

    •  從實戰看,這種方法對於性能很是友好,惟一不足的就是構建緩存時候,其他線程(非構建緩存的線程)可能訪問的是老數據

    • 示例僞代碼

      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")) {  
                              redis.expire(keyMutex, 3 * 60);  
                              String dbValue = db.get(key);  
                              redis.set(key, dbValue);  
                              redis.delete(keyMutex);  
                          }  
                      }  
                  });  
              }  
              return value;  
          }
相關文章
相關標籤/搜索