中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

緩存那些事

2018-07-20    來源:編程學習網(wǎng)

容器云強勢上線!快速搭建集群,上萬Linux鏡像隨意使用

前言

一般而言,現(xiàn)在互聯(lián)網(wǎng)應用(網(wǎng)站或App)的整體流程,可以概括如圖1所示,用戶請求從界面(瀏覽器或App界面)到網(wǎng)絡轉(zhuǎn)發(fā)、應用服務再到存儲(數(shù)據(jù)庫或文件系統(tǒng)),然后返回到界面呈現(xiàn)內(nèi)容。

隨著互聯(lián)網(wǎng)的普及,內(nèi)容信息越來越復雜,用戶數(shù)和訪問量越來越大,我們的應用需要支撐更多的并發(fā)量,同時我們的應用服務器和數(shù)據(jù)庫服務器所做的計算也越來越多。但是往往我們的應用服務器資源是有限的,且技術變革是緩慢的,數(shù)據(jù)庫每秒能接受的請求次數(shù)也是有限的(或者文件的讀寫也是有限的),如何能夠有效利用有限的資源來提供盡可能大的吞吐量?一個有效的辦法就是引入緩存,打破標準流程,每個環(huán)節(jié)中請求可以從緩存中直接獲取目標數(shù)據(jù)并返回,從而減少計算量,有效提升響應速度,讓有限的資源服務更多的用戶。

如圖1所示,緩存的使用可以出現(xiàn)在1~4的各個環(huán)節(jié)中,每個環(huán)節(jié)的緩存方案與使用各有特點。

圖1 互聯(lián)網(wǎng)應用一般流程

緩存特征

緩存也是一個數(shù)據(jù)模型對象,那么必然有它的一些特征:

命中率

命中率=返回正確結果數(shù)/請求緩存次數(shù),命中率問題是緩存中的一個非常重要的問題,它是衡量緩存有效性的重要指標。命中率越高,表明緩存的使用率越高。

最大元素(或最大空間)

緩存中可以存放的最大元素的數(shù)量,一旦緩存中元素數(shù)量超過這個值(或者緩存數(shù)據(jù)所占空間超過其最大支持空間),那么將會觸發(fā)緩存啟動清空策略根據(jù)不同的場景合理的設置最大元素值往往可以一定程度上提高緩存的命中率,從而更有效的時候緩存。

清空策略

如上描述,緩存的存儲空間有限制,當緩存空間被用滿時,如何保證在穩(wěn)定服務的同時有效提升命中率?這就由緩存清空策略來處理,設計適合自身數(shù)據(jù)特征的清空策略能有效提升命中率。常見的一般策略有:

  • FIFO(first in first out)

    先進先出策略,最先進入緩存的數(shù)據(jù)在緩存空間不夠的情況下(超出最大元素限制)會被優(yōu)先被清除掉,以騰出新的空間接受新的數(shù)據(jù)。策略算法主要比較緩存元素的創(chuàng)建時間。在數(shù)據(jù)實效性要求場景下可選擇該類策略,優(yōu)先保障最新數(shù)據(jù)可用。

  • LFU(less frequently used)

    最少使用策略,無論是否過期,根據(jù)元素的被使用次數(shù)判斷,清除使用次數(shù)較少的元素釋放空間。策略算法主要比較元素的hitCount(命中次數(shù))。在保證高頻數(shù)據(jù)有效性場景下,可選擇這類策略。

  • LRU(least recently used)

    最近最少使用策略,無論是否過期,根據(jù)元素最后一次被使用的時間戳,清除最遠使用時間戳的元素釋放空間。策略算法主要比較元素最近一次被get使用時間。在熱點數(shù)據(jù)場景下較適用,優(yōu)先保證熱點數(shù)據(jù)的有效性。

除此之外,還有一些簡單策略比如:

  • 根據(jù)過期時間判斷,清理過期時間最長的元素;
  • 根據(jù)過期時間判斷,清理最近要過期的元素;
  • 隨機清理;
  • 根據(jù)關鍵字(或元素內(nèi)容)長短清理等。

緩存介質(zhì)

雖然從硬件介質(zhì)上來看,無非就是內(nèi)存和硬盤兩種,但從技術上,可以分成內(nèi)存、硬盤文件、數(shù)據(jù)庫。

  • 內(nèi)存: 將緩存存儲于內(nèi)存中是最快的選擇,無需額外的I/O開銷,但是內(nèi)存的缺點是沒有持久化落地物理磁盤,一旦應用異常break down而重新啟動,數(shù)據(jù)很難或者無法復原。
  • 硬盤: 一般來說,很多緩存框架會結合使用內(nèi)存和硬盤,在內(nèi)存分配空間滿了或是在異常的情況下,可以被動或主動的將內(nèi)存空間數(shù)據(jù)持久化到硬盤中,達到釋放空間或備份數(shù)據(jù)的目的。
  • 數(shù)據(jù)庫: 前面有提到,增加緩存的策略的目的之一就是為了減少數(shù)據(jù)庫的I/O壓力,F(xiàn)在使用數(shù)據(jù)庫做緩存介質(zhì)是不是又回到了老問題上了?其實,數(shù)據(jù)庫也有很多種類型,像那些不支持SQL,只是簡單的key-value存儲結構的特殊數(shù)據(jù)庫(如BerkeleyDB和Redis),響應速度和吞吐量都遠遠高于我們常用的關系型數(shù)據(jù)庫等。

緩存分類和應用場景

緩存有各類特征,而且有不同介質(zhì)的區(qū)別,那么實際工程中我們怎么去對緩存分類呢?在目前的應用服務框架中,比較常見的,時根據(jù)緩存雨應用的藕合度,分為local cache(本地緩存)和remote cache(分布式緩存):

  • 本地緩存:指的是在應用中的緩存組件,其最大的優(yōu)點是應用和cache是在同一個進程內(nèi)部,請求緩存非常快速,沒有過多的網(wǎng)絡開銷等,在單應用不需要集群支持或者集群情況下各節(jié)點無需互相通知的場景下使用本地緩存較合適;同時,它的缺點也是應為緩存跟應用程序耦合,多個應用程序無法直接的共享緩存,各應用或集群的各節(jié)點都需要維護自己的單獨緩存,對內(nèi)存是一種浪費。

  • 分布式緩存:指的是與應用分離的緩存組件或服務,其最大的優(yōu)點是自身就是一個獨立的應用,與本地應用隔離,多個應用可直接的共享緩存。

目前各種類型的緩存都活躍在成千上萬的應用服務中,還沒有一種緩存方案可以解決一切的業(yè)務場景或數(shù)據(jù)類型,我們需要根據(jù)自身的特殊場景和背景,選擇最適合的緩存方案。緩存的使用是程序員、架構師的必備技能,好的程序員能根據(jù)數(shù)據(jù)類型、業(yè)務場景來準確判斷使用何種類型的緩存,如何使用這種緩存,以最小的成本最快的效率達到最優(yōu)的目的。

本地緩存

編程直接實現(xiàn)緩存

個別場景下,我們只需要簡單的緩存數(shù)據(jù)的功能,而無需關注更多存取、清空策略等深入的特性時,直接編程實現(xiàn)緩存則是最便捷和高效的。

a. 成員變量或局部變量實現(xiàn)

簡單代碼示例如下:

public void UseLocalCache(){
     //一個本地的緩存變量
     Map<String, Object> localCacheStoreMap = new HashMap<String, Object>();

    List<Object> infosList = this.getInfoList();
    for(Object item:infosList){
        if(localCacheStoreMap.containsKey(item)){ //緩存命中 使用緩存數(shù)據(jù)
            // todo
        } else { // 緩存未命中  IO獲取數(shù)據(jù),結果存入緩存
            Object valueObject = this.getInfoFromDB();
            localCacheStoreMap.put(valueObject.toString(), valueObject);

        }
    }
}
//示例
private List<Object> getInfoList(){
    return new ArrayList<Object>();
}
//示例數(shù)據(jù)庫IO獲取
private Object getInfoFromDB(){
    return new Object();
}

以局部變量map結構緩存部分業(yè)務數(shù)據(jù),減少頻繁的重復數(shù)據(jù)庫I/O操作。缺點僅限于類的自身作用域內(nèi),類間無法共享緩存。

b. 靜態(tài)變量實現(xiàn)

最常用的單例實現(xiàn)靜態(tài)資源緩存,代碼示例如下:

public class CityUtils {
      private static final HttpClient httpClient = ServerHolder.createClientWithPool(); 
      private static Map<Integer, String> cityIdNameMap = new HashMap<Integer, String>();
      private static Map<Integer, String> districtIdNameMap = new HashMap<Integer, String>();

  static {
    HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/city/all");
    BaseAuthorizationUtils.generateAuthAndDateHeader(get,
            BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
            BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);
    try {
        String resultStr = httpClient.execute(get, new BasicResponseHandler());
        JSONObject resultJo = new JSONObject(resultStr);
        JSONArray dataJa = resultJo.getJSONArray("data");
        for (int i = 0; i < dataJa.length(); i++) {
            JSONObject itemJo = dataJa.getJSONObject(i);
            cityIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
        }
    } catch (Exception e) {
        throw new RuntimeException("Init City List Error!", e);
    }
}
    static {
    HttpGet get = new HttpGet("http://gis-in.sankuai.com/api/location/district/all");
    BaseAuthorizationUtils.generateAuthAndDateHeader(get,
            BaseAuthorizationUtils.CLIENT_TO_REQUEST_MDC,
            BaseAuthorizationUtils.SECRET_TO_REQUEST_MDC);
    try {
        String resultStr = httpClient.execute(get, new BasicResponseHandler());
        JSONObject resultJo = new JSONObject(resultStr);
        JSONArray dataJa = resultJo.getJSONArray("data");
        for (int i = 0; i < dataJa.length(); i++) {
            JSONObject itemJo = dataJa.getJSONObject(i);
            districtIdNameMap.put(itemJo.getInt("id"), itemJo.getString("name"));
        }
    } catch (Exception e) {
        throw new RuntimeException("Init District List Error!", e);
    }
}

    public static String getCityName(int cityId) {
      String name = cityIdNameMap.get(cityId);
      if (name == null) {
        name = "未知";
      }
       return name;
     }

    public static String getDistrictName(int districtId) {
      String name = districtIdNameMap.get(districtId);
       if (name == null) {
         name = "未知";
        }
       return name;
     }
   }

O2O業(yè)務中常用的城市基礎基本信息判斷,通過靜態(tài)變量一次獲取緩存內(nèi)存中,減少頻繁的I/O讀取,靜態(tài)變量實現(xiàn)類間可共享,進程內(nèi)可共享,緩存的實時性稍差。

為了解決本地緩存數(shù)據(jù)的實時性問題,目前大量使用的是結合ZooKeeper的自動發(fā)現(xiàn)機制,實時變更本地靜態(tài)變量緩存:

美團點評內(nèi)部的基礎配置組件MtConfig,采用的就是類似原理,使用靜態(tài)變量緩存,結合ZooKeeper的統(tǒng)一管理,做到自動動態(tài)更新緩存,如圖2所示。

圖2 Mtconfig實現(xiàn)圖

這類緩存實現(xiàn),優(yōu)點是能直接在heap區(qū)內(nèi)讀寫,最快也最方便;缺點同樣是受heap區(qū)域影響,緩存的數(shù)據(jù)量非常有限,同時緩存時間受GC影響。主要滿足單機場景下的小數(shù)據(jù)量緩存需求,同時對緩存數(shù)據(jù)的變更無需太敏感感知,如上一般配置管理、基礎靜態(tài)數(shù)據(jù)等場景。

Ehcache

Ehcache是現(xiàn)在最流行的純Java開源緩存框架,配置簡單、結構清晰、功能強大,是一個非常輕量級的緩存實現(xiàn),我們常用的Hibernate里面就集成了相關緩存功能。

圖3 Ehcache框架圖

從圖3中我們可以了解到,Ehcache的核心定義主要包括:

  • cache manager:緩存管理器,以前是只允許單例的,不過現(xiàn)在也可以多實例了。

  • cache:緩存管理器內(nèi)可以放置若干cache,存放數(shù)據(jù)的實質(zhì),所有cache都實現(xiàn)了Ehcache接口,這是一個真正使用的緩存實例;通過緩存管理器的模式,可以在單個應用中輕松隔離多個緩存實例,獨立服務于不同業(yè)務場景需求,緩存數(shù)據(jù)物理隔離,同時需要時又可共享使用。

  • element:單條緩存數(shù)據(jù)的組成單位。

  • system of record(SOR):可以取到真實數(shù)據(jù)的組件,可以是真正的業(yè)務邏輯、外部接口調(diào)用、存放真實數(shù)據(jù)的數(shù)據(jù)庫等,緩存就是從SOR中讀取或者寫入到SOR中去的。

在上層可以看到,整個Ehcache提供了對JSR、JMX等的標準支持,能夠較好的兼容和移植,同時對各類對象有較完善的監(jiān)控管理機制。它的緩存介質(zhì)涵蓋堆內(nèi)存(heap)、堆外內(nèi)存(BigMemory商用版本支持)和磁盤,各介質(zhì)可獨立設置屬性和策略。Ehcache最初是獨立的本地緩存框架組件,在后期的發(fā)展中,結合Terracotta服務陣列模型,可以支持分布式緩存集群,主要有RMI、JGroups、JMS和Cache Server等傳播方式進行節(jié)點間通信,如圖3的左側(cè)部分描述。

整體數(shù)據(jù)流轉(zhuǎn)包括這樣幾類行為:

  • Flush:緩存條目向低層次移動。
  • Fault:從低層拷貝一個對象到高層。在獲取緩存的過程中,某一層發(fā)現(xiàn)自己的該緩存條目已經(jīng)失效,就觸發(fā)了Fault行為。
  • Eviction:把緩存條目除去。
  • Expiration:失效狀態(tài)。
  • Pinning:強制緩存條目保持在某一層。

圖4反映了數(shù)據(jù)在各個層之間的流轉(zhuǎn),同時也體現(xiàn)了各層數(shù)據(jù)的一個生命周期。

圖4 緩存數(shù)據(jù)流轉(zhuǎn)圖(L1:本地內(nèi)存層;L2:Terracotta服務節(jié)點層)

Ehcache的配置使用如下:

<ehcache>
<!-- 指定一個文件目錄,當Ehcache把數(shù)據(jù)寫到硬盤上時,將把數(shù)據(jù)寫到這個文件目錄下 -->
<diskStore path="java.io.tmpdir"/>

<!-- 設定緩存的默認數(shù)據(jù)過期策略 -->
<defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        overflowToDisk="true"
        timeToIdleSeconds="0"
        timeToLiveSeconds="0"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"/>

<!--  
    設定具體的命名緩存的數(shù)據(jù)過期策略

    cache元素的屬性:
        name:緩存名稱

        maxElementsInMemory:內(nèi)存中最大緩存對象數(shù)

        maxElementsOnDisk:硬盤中最大緩存對象數(shù),若是0表示無窮大

        eternal:true表示對象永不過期,此時會忽略timeToIdleSeconds和timeToLiveSeconds屬性,默認為false

        overflowToDisk:true表示當內(nèi)存緩存的對象數(shù)目達到了maxElementsInMemory界限后,會把溢出的對象寫到硬盤緩存中。注意:如果緩存的對象要寫入到硬盤中的話,則該對象必須實現(xiàn)了Serializable接口才行。

        diskSpoolBufferSizeMB:磁盤緩存區(qū)大小,默認為30MB。每個Cache都應該有自己的一個緩存區(qū)。

        diskPersistent:是否緩存虛擬機重啟期數(shù)據(jù)

        diskExpiryThreadIntervalSeconds:磁盤失效線程運行時間間隔,默認為120秒

        timeToIdleSeconds: 設定允許對象處于空閑狀態(tài)的最長時間,以秒為單位。當對象自從最近一次被訪問后,如果處于空閑狀態(tài)的時間超過了timeToIdleSeconds屬性值,這個對象就會過期,EHCache將把它從緩存中清空。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地處于空閑狀態(tài)

        timeToLiveSeconds:設定對象允許存在于緩存中的最長時間,以秒為單位。當對象自從被存放到緩存中后,如果處于緩存中的時間超過了 timeToLiveSeconds屬性值,這個對象就會過期,Ehcache將把它從緩存中清除。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地存在于緩存中。timeToLiveSeconds必須大于timeToIdleSeconds屬性,才有意義

        memoryStoreEvictionPolicy:當達到maxElementsInMemory限制時,Ehcache將會根據(jù)指定的策略去清理內(nèi)存?蛇x策略有:LRU(最近最少使用,默認策略)、FIFO(先進先出)、LFU(最少訪問次數(shù))。
-->
<cache name="CACHE1"
       maxElementsInMemory="1000"
       eternal="true"
       overflowToDisk="true"/>  

<cache name="CACHE2"
    maxElementsInMemory="1000"
    eternal="false"
    timeToIdleSeconds="200"
    timeToLiveSeconds="4000"
    overflowToDisk="true"/>
</ehcache>

整體上看,Ehcache的使用還是相對簡單便捷的,提供了完整的各類API接口。需要注意的是,雖然Ehcache支持磁盤的持久化,但是由于存在兩級緩存介質(zhì),在一級內(nèi)存中的緩存,如果沒有主動的刷入磁盤持久化的話,在應用異常down機等情形下,依然會出現(xiàn)緩存數(shù)據(jù)丟失,為此可以根據(jù)需要將緩存刷到磁盤,將緩存條目刷到磁盤的操作可以通過cache.flush()方法來執(zhí)行,需要注意的是,對于對象的磁盤寫入,前提是要將對象進行序列化。

主要特性:

  • 快速,針對大型高并發(fā)系統(tǒng)場景,Ehcache的多線程機制有相應的優(yōu)化改善。
  • 簡單,很小的jar包,簡單配置就可直接使用,單機場景下無需過多的其他服務依賴。
  • 支持多種的緩存策略,靈活。
  • 緩存數(shù)據(jù)有兩級:內(nèi)存和磁盤,與一般的本地內(nèi)存緩存相比,有了磁盤的存儲空間,將可以支持更大量的數(shù)據(jù)緩存需求。
  • 具有緩存和緩存管理器的偵聽接口,能更簡單方便的進行緩存實例的監(jiān)控管理。
  • 支持多緩存管理器實例,以及一個實例的多個緩存區(qū)域。

注意:Ehcache的超時設置主要是針對整個cache實例設置整體的超時策略,而沒有較好的處理針對單獨的key的個性的超時設置(有策略設置,但是比較復雜,就不描述了),因此,在使用中要注意過期失效的緩存元素無法被GC回收,時間越長緩存越多,內(nèi)存占用也就越大,內(nèi)存泄露的概率也越大。

Guava Cache

Guava Cache是Google開源的Java重用工具集庫Guava里的一款緩存工具,其主要實現(xiàn)的緩存功能有:

  • 自動將entry節(jié)點加載進緩存結構中;
  • 當緩存的數(shù)據(jù)超過設置的最大值時,使用LRU算法移除;
  • 具備根據(jù)entry節(jié)點上次被訪問或者寫入時間計算它的過期機制;
  • 緩存的key被封裝在WeakReference引用內(nèi);
  • 緩存的Value被封裝在WeakReference或SoftReference引用內(nèi);
  • 統(tǒng)計緩存使用過程中命中率、異常率、未命中率等統(tǒng)計數(shù)據(jù)。

Guava Cache的架構設計靈感來源于ConcurrentHashMap,我們前面也提到過,簡單場景下可以自行編碼通過hashmap來做少量數(shù)據(jù)的緩存,但是,如果結果可能隨時間改變或者是希望存儲的數(shù)據(jù)空間可控的話,自己實現(xiàn)這種數(shù)據(jù)結構還是有必要的。

Guava Cache繼承了ConcurrentHashMap的思路,使用多個segments方式的細粒度鎖,在保證線程安全的同時,支持高并發(fā)場景需求。Cache類似于Map,它是存儲鍵值對的集合,不同的是它還需要處理evict、expire、dynamic load等算法邏輯,需要一些額外信息來實現(xiàn)這些操作。對此,根據(jù)面向?qū)ο笏枷,需要做方法與數(shù)據(jù)的關聯(lián)封裝。如圖5所示cache的內(nèi)存數(shù)據(jù)模型,可以看到,使用ReferenceEntry接口來封裝一個鍵值對,而用ValueReference來封裝Value值,之所以用Reference命令,是因為Cache要支持WeakReference Key和SoftReference、WeakReference value。

圖5 Guava Cache數(shù)據(jù)結構圖

ReferenceEntry是對一個鍵值對節(jié)點的抽象,它包含了key和值的ValueReference抽象類,Cache由多個Segment組成,而每個Segment包含一個ReferenceEntry數(shù)組,每個ReferenceEntry數(shù)組項都是一條ReferenceEntry鏈,且一個ReferenceEntry包含key、hash、valueReference、next字段。除了在ReferenceEntry數(shù)組項中組成的鏈,在一個Segment中,所有ReferenceEntry還組成access鏈(accessQueue)和write鏈(writeQueue)(后面會介紹鏈的作用)。ReferenceEntry可以是強引用類型的key,也可以WeakReference類型的key,為了減少內(nèi)存使用量,還可以根據(jù)是否配置了expireAfterWrite、expireAfterAccess、maximumSize來決定是否需要write鏈和access鏈確定要創(chuàng)建的具體Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。

對于ValueReference,因為Cache支持強引用的Value、SoftReference Value以及WeakReference Value,因而它對應三個實現(xiàn)類:StrongValueReference、SoftValueReference、WeakValueReference。為了支持動態(tài)加載機制,它還有一個LoadingValueReference,在需要動態(tài)加載一個key的值時,先把該值封裝在LoadingValueReference中,以表達該key對應的值已經(jīng)在加載了,如果其他線程也要查詢該key對應的值,就能得到該引用,并且等待改值加載完成,從而保證該值只被加載一次,在該值加載完成后,將LoadingValueReference替換成其他ValueReference類型。ValueReference對象中會保留對ReferenceEntry的引用,這是因為在Value因為WeakReference、SoftReference被回收時,需要使用其key將對應的項從Segment的table中移除。

WriteQueue和AccessQueue :為了實現(xiàn)最近最少使用算法,Guava Cache在Segment中添加了兩條鏈:write鏈(writeQueue)和access鏈(accessQueue),這兩條鏈都是一個雙向鏈表,通過ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue鏈接而成,但是以Queue的形式表達。WriteQueue和AccessQueue都是自定義了offer、add(直接調(diào)用offer)、remove、poll等操作的邏輯,對offer(add)操作,如果是新加的節(jié)點,則直接加入到該鏈的結尾,如果是已存在的節(jié)點,則將該節(jié)點鏈接的鏈尾;對remove操作,直接從該鏈中移除該節(jié)點;對poll操作,將頭節(jié)點的下一個節(jié)點移除,并返回。

了解了cache的整體數(shù)據(jù)結構后,再來看下針對緩存的相關操作就簡單多了:

  • Segment中的evict清除策略操作,是在每一次調(diào)用操作的開始和結束時觸發(fā)清理工作,這樣比一般的緩存另起線程監(jiān)控清理相比,可以減少開銷,但如果長時間沒有調(diào)用方法的話,會導致不能及時的清理釋放內(nèi)存空間的問題。evict主要處理四個Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前兩個queue是因為WeakReference、SoftReference被垃圾回收時加入的,清理時只需要遍歷整個queue,將對應的項從LocalCache中移除即可,這里keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference,要從Cache中移除需要有key,因而ValueReference需要有對ReferenceEntry的引用,這個前面也提到過了。而對后面兩個Queue,只需要檢查是否配置了相應的expire時間,然后從頭開始查找已經(jīng)expire的Entry,將它們移除即可。
  • Segment中的put操作:put操作相對比較簡單,首先它需要獲得鎖,然后嘗試做一些清理工作,接下來的邏輯類似ConcurrentHashMap中的rehash,查找位置并注入數(shù)據(jù)。需要說明的是當找到一個已存在的Entry時,需要先判斷當前的ValueRefernece中的值事實上已經(jīng)被回收了,因為它們可以是WeakReference、SoftReference類型,如果已經(jīng)被回收了,則將新值寫入。并且在每次更新時注冊當前操作引起的移除事件,指定相應的原因:COLLECTED、REPLACED等,這些注冊的事件在退出的時候統(tǒng)一調(diào)用Cache注冊的RemovalListener,由于事件處理可能會有很長時間,因而這里將事件處理的邏輯在退出鎖以后才做。最后,在更新已存在的Entry結束后都嘗試著將那些已經(jīng)expire的Entry移除。另外put操作中還需要更新writeQueue和accessQueue的語義正確性。
  • Segment帶CacheLoader的get操作:1. 先查找table中是否已存在沒有被回收、也沒有expire的entry,如果找到,并在CacheBuilder中配置了refreshAfterWrite,并且當前時間間隔已經(jīng)操作這個事件,則重新加載值,否則,直接返回原有的值;2. 如果查找到的ValueReference是LoadingValueReference,則等待該LoadingValueReference加載結束,并返回加載的值;3. 如果沒有找到entry,或者找到的entry的值為null,則加鎖后,繼續(xù)在table中查找已存在key對應的entry,如果找到并且對應的entry.isLoading()為true,則表示有另一個線程正在加載,因而等待那個線程加載完成,如果找到一個非null值,返回該值,否則創(chuàng)建一個LoadingValueReference,并調(diào)用loadSync加載相應的值,在加載完成后,將新加載的值更新到table中,即大部分情況下替換原來的LoadingValueReference。

Guava Cache提供Builder模式的CacheBuilder生成器來創(chuàng)建緩存的方式,十分方便,并且各個緩存參數(shù)的配置設置,類似于函數(shù)式編程的寫法,可自行設置各類參數(shù)選型。它提供三種方式加載到緩存中。分別是:

  1. 在構建緩存的時候,使用build方法內(nèi)部調(diào)用CacheLoader方法加載數(shù)據(jù);
  2. callable 、callback方式加載數(shù)據(jù);
  3. 使用粗暴直接的方式,直接Cache.put 加載數(shù)據(jù),但自動加載是首選的,因為它可以更容易的推斷所有緩存內(nèi)容的一致性。

build生成器的兩種方式都實現(xiàn)了一種邏輯:從緩存中取key的值,如果該值已經(jīng)緩存過了則返回緩存中的值,如果沒有緩存過可以通過某個方法來獲取這個值,不同的地方在于cacheloader的定義比較寬泛,是針對整個cache定義的,可以認為是統(tǒng)一的根據(jù)key值load value的方法,而callable的方式較為靈活,允許你在get的時候指定load方法。使用示例如下:

/**
    * CacheLoader
   */
   public void loadingCache()
   {
     LoadingCache<String, String> graphs =CacheBuilder.newBuilder()
        .maximumSize(1000).build(new CacheLoader<String, String>()
        {
            @Override
            public String load(String key) throws Exception
            {
                System.out.println("key:"+key);
                if("key".equals(key)){
                    return "key return result";
                }else{
                    return "get-if-absent-compute";
                }                   
            }
        });
   String resultVal = null;
   try {
       resultVal = graphs.get("key");
       } catch (ExecutionException e) {
         e.printStackTrace();
      }

    System.out.println(resultVal);
   }

   /**
    *
    * Callable
   */
   public void callablex() throws ExecutionException
    {
      Cache<String, String> cache = CacheBuilder.newBuilder()
        .maximumSize(1000).build();
      String result = cache.get("key", new Callable<String>()
       {
         public String call()
         {
          return "result";
         }
       });
     System.out.println(result);
    }

總體來看,Guava Cache基于ConcurrentHashMap的優(yōu)秀設計借鑒,在高并發(fā)場景支持和線程安全上都有相應的改進策略,使用Reference引用命令,提升高并發(fā)下的數(shù)據(jù)……訪問速度并保持了GC的可回收,有效節(jié)省空間;同時,write鏈和access鏈的設計,能更靈活、高效的實現(xiàn)多種類型的緩存清理策略,包括基于容量的清理、基于時間的清理、基于引用的清理等;編程式的build生成器管理,讓使用者有更多的自由度,能夠根據(jù)不同場景設置合適的模式。

分布式緩存

memcached緩存

memcached是應用較廣的開源分布式緩存產(chǎn)品之一,它本身其實不提供分布式解決方案。在服務端,memcached集群環(huán)境實際就是一個個memcached服務器的堆積,環(huán)境搭建較為簡單;cache的分布式主要是在客戶端實現(xiàn),通過客戶端的路由處理來達到分布式解決方案的目的?蛻舳俗雎酚傻脑矸浅:唵危瑧梅⻊掌髟诿看未嫒∧砶ey的value時,通過某種算法把key映射到某臺memcached服務器nodeA上,因此這個key所有操作都在nodeA上,結構圖如圖6、圖7所示。

圖6 memcached客戶端路由圖

圖7 memcached一致性hash示例圖

memcached客戶端采用一致性hash算法作為路由策略,如圖7,相對于一般hash(如簡單取模)的算法,一致性hash算法除了計算key的hash值外,還會計算每個server對應的hash值,然后將這些hash值映射到一個有限的值域上(比如0~2^32)。通過尋找hash值大于hash(key)的最小server作為存儲該key數(shù)據(jù)的目標server。如果找不到,則直接把具有最小hash值的server作為目標server。同時,一定程度上,解決了擴容問題,增加或刪除單個節(jié)點,對于整個集群來說,不會有大的影響。最近版本,增加了虛擬節(jié)點的設計,進一步提升了可用性。

memcached是一個高效的分布式內(nèi)存cache,了解memcached的內(nèi)存管理機制,才能更好的掌握memcached,讓我們可以針對我們數(shù)據(jù)特點進行調(diào)優(yōu),讓其更好的為我所用。我們知道m(xù)emcached僅支持基礎的key-value鍵值對類型數(shù)據(jù)存儲。在memcached內(nèi)存結構中有兩個非常重要的概念:slab和chunk。如圖8所示。

圖8 memcached內(nèi)存結構圖

slab是一個內(nèi)存塊,它是memcached一次申請內(nèi)存的最小單位。在啟動memcached的時候一般會使用參數(shù)-m指定其可用內(nèi)存,但是并不是在啟動的那一刻所有的內(nèi)存就全部分配出去了,只有在需要的時候才會去申請,而且每次申請一定是一個slab。Slab的大小固定為1M(1048576 Byte),一個slab由若干個大小相等的chunk組成。每個chunk中都保存了一個item結構體、一對key和value。

雖然在同一個slab中chunk的大小相等的,但是在不同的slab中chunk的大小并不一定相等,在memcached中按照chunk的大小不同,可以把slab分為很多種類(class),默認情況下memcached把slab分為40類(class1~class40),在class 1中,chunk的大小為80字節(jié),由于一個slab的大小是固定的1048576字節(jié)(1M),因此在class1中最多可以有13107個chunk(也就是這個slab能存最多13107個小于80字節(jié)的key-value數(shù)據(jù))。

memcached內(nèi)存管理采取預分配、分組管理的方式,分組管理就是我們上面提到的slab class,按照chunk的大小slab被分為很多種類。內(nèi)存預分配過程是怎樣的呢?向memcached添加一個item時候,memcached首先會根據(jù)item的大小,來選擇最合適的slab class:例如item的大小為190字節(jié),默認情況下class 4的chunk大小為160字節(jié)顯然不合適,class 5的chunk大小為200字節(jié),大于190字節(jié),因此該item將放在class 5中(顯然這里會有10字節(jié)的浪費是不可避免的),計算好所要放入的chunk之后,memcached會去檢查該類大小的chunk還有沒有空閑的,如果沒有,將會申請1M(1個slab)的空間并劃分為該種類chunk。例如我們第一次向memcached中放入一個190字節(jié)的item時,memcached會產(chǎn)生一個slab class 2(也叫一個page),并會用去一個chunk,剩余5241個chunk供下次有適合大小item時使用,當我們用完這所有的5242個chunk之后,下次再有一個在160~200字節(jié)之間的item添加進來時,memcached會再次產(chǎn)生一個class 5的slab(這樣就存在了2個pages)。

總結來看,memcached內(nèi)存管理需要注意的幾個方面:

  • chunk是在page里面劃分的,而page固定為1m,所以chunk最大不能超過1m。
  • chunk實際占用內(nèi)存要加48B,因為chunk數(shù)據(jù)結構本身需要占用48B。
  • 如果用戶數(shù)據(jù)大于1m,則memcached會將其切割,放到多個chunk內(nèi)。
  • 已分配出去的page不能回收。

對于key-value信息,最好不要超過1m的大。煌瑫r信息長度最好相對是比較均衡穩(wěn)定的,這樣能夠保障最大限度的使用內(nèi)存;同時,memcached采用的LRU清理策略,合理甚至過期時間,提高命中率。

無特殊場景下,key-value能滿足需求的前提下,使用memcached分布式集群是較好的選擇,搭建與操作使用都比較簡單;分布式集群在單點故障時,只影響小部分數(shù)據(jù)異常,目前還可以通過Magent緩存代理模式,做單點備份,提升高可用;整個緩存都是基于內(nèi)存的,因此響應時間是很快,不需要額外的序列化、反序列化的程序,但同時由于基于內(nèi)存,數(shù)據(jù)沒有持久化,集群故障重啟數(shù)據(jù)無法恢復。高版本的memcached已經(jīng)支持CAS模式的原子操作,可以低成本的解決并發(fā)控制問題。

Redis緩存

Redis是一個遠程內(nèi)存數(shù)據(jù)庫(非關系型數(shù)據(jù)庫),性能強勁,具有復制特性以及解決問題而生的獨一無二的數(shù)據(jù)模型。它可以存儲鍵值對與5種不同類型的值之間的映射,可以將存儲在內(nèi)存的鍵值對數(shù)據(jù)持久化到硬盤,可以使用復制特性來擴展讀性能,還可以使用客戶端分片來擴展寫性能。

圖9 Redis數(shù)據(jù)模型圖

如圖9,Redis內(nèi)部使用一個redisObject對象來標識所有的key和value數(shù)據(jù),redisObject最主要的信息如圖所示:type代表一個value對象具體是何種數(shù)據(jù)類型,encoding是不同數(shù)據(jù)類型在Redis內(nèi)部的存儲方式,比如——type=string代表value存儲的是一個普通字符串,那么對應的encoding可以是raw或是int,如果是int則代表世界Redis內(nèi)部是按數(shù)值類型存儲和表示這個字符串。

圖9左邊的raw列為對象的編碼方式:字符串可以被編碼為raw(一般字符串)或Rint(為了節(jié)約內(nèi)存,Redis會將字符串表示的64位有符號整數(shù)編碼為整數(shù)來進行儲存);列表可以被編碼為ziplist或linkedlist,ziplist是為節(jié)約大小較小的列表空間而作的特殊表示;集合可以被編碼為intset或者hashtable,intset是只儲存數(shù)字的小集合的特殊表示;hash表可以編碼為zipmap或者hashtable,zipmap是小hash表的特殊表示;有序集合可以被編碼為ziplist或者skiplist格式,ziplist用于表示小的有序集合,而skiplist則用于表示任何大小的有序集合。

從網(wǎng)絡I/O模型上看,Redis使用單線程的I/O復用模型,自己封裝了一個簡單的AeEvent事件處理框架,主要實現(xiàn)了epoll、kqueue和select。對于單純只有I/O操作來說,單線程可以將速度優(yōu)勢發(fā)揮到最大,但是Redis也提供了一些簡單的計算功能,比如排序、聚合等,對于這些操作,單線程模型實際會嚴重影響整體吞吐量,CPU計算過程中,整個I/O調(diào)度都是被阻塞住的,在這些特殊場景的使用中,需要額外的考慮。相較于memcached的預分配內(nèi)存管理,Redis使用現(xiàn)場申請內(nèi)存的方式來存儲數(shù)據(jù),并且很少使用free-list等方式來優(yōu)化內(nèi)存分配,會在一定程度上存在內(nèi)存碎片。Redis跟據(jù)存儲命令參數(shù),會把帶過期時間的數(shù)據(jù)單獨存放在一起,并把它們稱為臨時數(shù)據(jù),非臨時數(shù)據(jù)是永遠不會被剔除的,即便物理內(nèi)存不夠,導致swap也不會剔除任何非臨時數(shù)據(jù)(但會嘗試剔除部分臨時數(shù)據(jù))。

我們描述Redis為內(nèi)存數(shù)據(jù)庫,作為緩存服務,大量使用內(nèi)存間的數(shù)據(jù)快速讀寫,支持高并發(fā)大吞吐;而作為數(shù)據(jù)庫,則是指Redis對緩存的持久化支持。Redis由于支持了非常豐富的內(nèi)存數(shù)據(jù)庫結構類型,如何把這些復雜的內(nèi)存組織方式持久化到磁盤上?Redis的持久化與傳統(tǒng)數(shù)據(jù)庫的方式差異較大,Redis一共支持四種持久化方式,主要使用的兩種:

  1. 定時快照方式(snapshot): 該持久化方式實際是在Redis內(nèi)部一個定時器事件,每隔固定時間去檢查當前數(shù)據(jù)發(fā)生的改變次數(shù)與時間是否滿足配置的持久化觸發(fā)的條件,如果滿足則通過操作系統(tǒng)fork調(diào)用來創(chuàng)建出一個子進程,這個子進程默認會與父進程共享相同的地址空間,這時就可以通過子進程來遍歷整個內(nèi)存來進行存儲操作,而主進程則仍然可以提供服務,當有寫入時由操作系統(tǒng)按照內(nèi)存頁(page)為單位來進行copy-on-write保證父子進程之間不會互相影響。它的缺點是快照只是代表一段時間內(nèi)的內(nèi)存映像,所以系統(tǒng)重啟會丟失上次快照與重啟之間所有的數(shù)據(jù)。
  2. 基于語句追加文件的方式(aof): aof方式實際類似MySQl的基于語句的binlog方式,即每條會使Redis內(nèi)存數(shù)據(jù)發(fā)生改變的命令都會追加到一個log文件中,也就是說這個log文件就是Redis的持久化數(shù)據(jù)。

aof的方式的主要缺點是追加log文件可能導致體積過大,當系統(tǒng)重啟恢復數(shù)據(jù)時如果是aof的方式則加載數(shù)據(jù)會非常慢,幾十G的數(shù)據(jù)可能需要幾小時才能加載完,當然這個耗時并不是因為磁盤文件讀取速度慢,而是由于讀取的所有命令都要在內(nèi)存中執(zhí)行一遍。另外由于每條命令都要寫log,所以使用aof的方式,Redis的讀寫性能也會有所下降。

Redis的持久化使用了Buffer I/O,所謂Buffer I/O是指Redis對持久化文件的寫入和讀取操作都會使用物理內(nèi)存的Page Cache,而大多數(shù)數(shù)據(jù)庫系統(tǒng)會使用Direct I/O來繞過這層Page Cache并自行維護一個數(shù)據(jù)的Cache。而當Redis的持久化文件過大(尤其是快照文件),并對其進行讀寫時,磁盤文件中的數(shù)據(jù)都會被加載到物理內(nèi)存中作為操作系統(tǒng)對該文件的一層Cache,而這層Cache的數(shù)據(jù)與Redis內(nèi)存中管理的數(shù)據(jù)實際是重復存儲的。雖然內(nèi)核在物理內(nèi)存緊張時會做Page Cache的剔除工作,但內(nèi)核很可能認為某塊Page Cache更重要,而讓你的進程開始Swap,這時你的系統(tǒng)就會開始出現(xiàn)不穩(wěn)定或者崩潰了,因此在持久化配置后,針對內(nèi)存使用需要實時監(jiān)控觀察。

與memcached客戶端支持分布式方案不同,Redis更傾向于在服務端構建分布式存儲,如圖10、11。

圖10 Redis分布式集群圖1

圖11 Redis分布式集群圖2

Redis Cluster是一個實現(xiàn)了分布式且允許單點故障的Redis高級版本,它沒有中心節(jié)點,具有線性可伸縮的功能。如圖11,其中節(jié)點與節(jié)點之間通過二進制協(xié)議進行通信,節(jié)點與客戶端之間通過ascii協(xié)議進行通信。在數(shù)據(jù)的放置策略上,Redis Cluster將整個key的數(shù)值域分成4096個hash槽,每個節(jié)點上可以存儲一個或多個hash槽,也就是說當前Redis Cluster支持的最大節(jié)點數(shù)就是4096。Redis Cluster使用的分布式算法也很簡單:crc16( key ) % HASH_SLOTS_NUMBER。整體設計可總結為:

  • 數(shù)據(jù)hash分布在不同的Redis節(jié)點實例上;
  • M/S的切換采用Sentinel;
  • 寫:只會寫master Instance,從sentinel獲取當前的master Instance;
  • 讀:從Redis Node中基于權重選取一個Redis Instance讀取,失敗/超時則輪詢其他Instance;Redis本身就很好的支持讀寫分離,在單進程的I/O場景下,可以有效的避免主庫的阻塞風險;
  • 通過RPC服務訪問,RPC server端封裝了Redis客戶端,客戶端基于Jedis開發(fā)。

可以看到,通過集群+主從結合的設計,Redis在擴展和穩(wěn)定高可用性能方面都是比較成熟的。但是,在數(shù)據(jù)一致性問題上,Redis沒有提供CAS操作命令來保障高并發(fā)場景下的數(shù)據(jù)一致性問題,不過它卻提供了事務的功能,Redis的Transactions提供的并不是嚴格的ACID的事務(比如一串用EXEC提交執(zhí)行的命令,在執(zhí)行中服務器宕機,那么會有一部分命令執(zhí)行了,剩下的沒執(zhí)行)。但是這個Transactions還是提供了基本的命令打包執(zhí)行的功能(在服務器不出問題的情況下,可以保證一連串的命令是順序在一起執(zhí)行的,中間有會有其它客戶端命令插進來執(zhí)行)。Redis還提供了一個Watch功能,你可以對一個key進行Watch,然后再執(zhí)行Transactions,在這過程中,如果這個Watched的值進行了修改,那么這個Transactions會發(fā)現(xiàn)并拒絕執(zhí)行。在失效策略上,Redis支持多大6種的數(shù)據(jù)淘汰策略:

  1. volatile-lru:從已設置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選最近最少使用的數(shù)據(jù)淘汰;
  2. volatile-ttl:從已設置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選將要過期的數(shù)據(jù)淘汰;
  3. volatile-random:從已設置過期時間的數(shù)據(jù)集(server.db[i].expires)中任意選擇數(shù)據(jù)淘汰 ;
  4. allkeys-lru:從數(shù)據(jù)集(server.db[i].dict)中挑選最近最少使用的數(shù)據(jù)淘汰;
  5. allkeys-random:從數(shù)據(jù)集(server.db[i].dict)中任意選擇數(shù)據(jù)淘汰;
  6. no-enviction(驅(qū)逐):禁止驅(qū)逐數(shù)據(jù)。

個人總結了以下多種Web應用場景,在這些場景下可以充分的利用Redis的特性,大大提高效率。

  • 在主頁中顯示最新的項目列表:Redis使用的是常駐內(nèi)存的緩存,速度非常快。LPUSH用來插入一個內(nèi)容ID,作為關鍵字存儲在列表頭部。LTRIM用來限制列表中的項目數(shù)最多為5000。如果用戶需要的檢索的數(shù)據(jù)量超越這個緩存容量,這時才需要把請求發(fā)送到數(shù)據(jù)庫。
  • 刪除和過濾:如果一篇文章被刪除,可以使用LREM從緩存中徹底清除掉。
  • 排行榜及相關問題:排行榜(leader board)按照得分進行排序。ZADD命令可以直接實現(xiàn)這個功能,而ZREVRANGE命令可以用來按照得分來獲取前100名的用戶,ZRANK可以用來獲取用戶排名,非常直接而且操作容易。
  • 按照用戶投票和時間排序:排行榜,得分會隨著時間變化。LPUSH和LTRIM命令結合運用,把文章添加到一個列表中。一項后臺任務用來獲取列表,并重新計算列表的排序,ZADD命令用來按照新的順序填充生成列表。列表可以實現(xiàn)非?焖俚臋z索,即使是負載很重的站點。
  • 過期項目處理:使用Unix時間作為關鍵字,用來保持列表能夠按時間排序。對current_time和time_to_live進行檢索,完成查找過期項目的艱巨任務。另一項后臺任務使用ZRANGE…WITHSCORES進行查詢,刪除過期的條目。
  • 計數(shù):進行各種數(shù)據(jù)統(tǒng)計的用途是非常廣泛的,比如想知道什么時候封鎖一個IP地址。INCRBY命令讓這些變得很容易,通過原子遞增保持計數(shù);GETSET用來重置計數(shù)器;過期屬性用來確認一個關鍵字什么時候應該刪除。
  • 特定時間內(nèi)的特定項目:這是特定訪問者的問題,可以通過給每次頁面瀏覽使用SADD命令來解決。SADD不會將已經(jīng)存在的成員添加到一個集合。
  • Pub/Sub:在更新中保持用戶對數(shù)據(jù)的映射是系統(tǒng)中的一個普遍任務。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,讓這個變得更加容易。
  • 隊列:在當前的編程中隊列隨處可見。除了push和pop類型的命令之外,Redis還有阻塞隊列的命令,能夠讓一個程序在執(zhí)行時被另一個程序添加到隊列。

緩存實戰(zhàn)

實際工程中,對于緩存的應用可以有多種的實戰(zhàn)方式,包括侵入式硬編碼,抽象服務化應用,以及輕量的注解式使用等。本文將主要介紹下注解式方式。

Spring注解緩存

Spring 3.1之后,引入了注解緩存技術,其本質(zhì)上不是一個具體的緩存實現(xiàn)方案,而是一個對緩存使用的抽象,通過在既有代碼中添加少量自定義的各種annotation,即能夠達到使用緩存對象和緩存方法的返回對象的效果。Spring的緩存技術具備相當?shù)撵`活性,不僅能夠使用SpEL(Spring Expression Language)來定義緩存的key和各種condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業(yè)緩存集成。其特點總結如下:

  • 少量的配置annotation注釋即可使得既有代碼支持緩存;
  • 支持開箱即用,不用安裝和部署額外的第三方組件即可使用緩存;
  • 支持Spring Express Language(SpEL),能使用對象的任何屬性或者方法來定義緩存的key和使用規(guī)則條件;
  • 支持自定義key和自定義緩存管理者,具有相當?shù)撵`活性和可擴展性。

和Spring的事務管理類似,Spring Cache的關鍵原理就是Spring AOP,通過Spring AOP實現(xiàn)了在方法調(diào)用前、調(diào)用后獲取方法的入?yún)⒑头祷刂,進而實現(xiàn)了緩存的邏輯。而Spring Cache利用了Spring AOP的動態(tài)代理技術,即當客戶端嘗試調(diào)用pojo的foo()方法的時候,給它的不是pojo自身的引用,而是一個動態(tài)生成的代理類。

圖12 Spring動態(tài)代理調(diào)用圖

如圖12所示,實際客戶端獲取的是一個代理的引用,在調(diào)用foo()方法的時候,會首先調(diào)用proxy的foo()方法,這個時候proxy可以整體控制實際的pojo.foo()方法的入?yún)⒑头祷刂,比如緩存結果,比如直接略過執(zhí)行實際的foo()方法等,都是可以輕松做到的。Spring Cache主要使用三個注釋標簽,即@Cacheable、@CachePut和@CacheEvict,主要針對方法上注解使用,部分場景也可以直接類上注解使用,當在類上使用時,該類所有方法都將受影響。我們總結一下其作用和配置方法,如表1所示。

表1

標簽類型 作用 主要配置參數(shù)說明
@Cacheable 主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結果進行緩存 value: 緩存的名稱,在 Spring 配置文件中定義,必須指定至少一個; key: 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則默認按照方法的所有參數(shù)進行組合; condition: 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩存
@CachePut 主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結果進行緩存,和 @Cacheable 不同的是,它每次都會觸發(fā)真實方法的調(diào)用 value: 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個; key: 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則默認按照方法的所有參數(shù)進行組合; condition: 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩存
@CacheEvict 主要針對方法配置,能夠根據(jù)一定的條件對緩存進行清空 value: 緩存的名稱,在 Spring 配置文件中定義,必須指定至少一個; key: 緩存的 key,可以為空,如果指定要按照 SpEL 表達式編寫,如果不指定,則默認按照方法的所有參數(shù)進行組合; condition: 緩存的條件,可以為空,使用 SpEL 編寫,返回 true 或者 false,只有為 true 才進行緩存; allEntries: 是否清空所有緩存內(nèi)容,默認為 false,如果指定為 true,則方法調(diào)用后將立即清空所有緩存; beforeInvocation: 是否在方法執(zhí)行前就清空,默認為 false,如果指定為 true,則在方法還沒有執(zhí)行的時候就清空緩存,默認情況下,如果方法執(zhí)行拋出異常,則不會清空緩存

可擴展支持:Spring注解cache能夠滿足一般應用對緩存的需求,但隨著應用服務的復雜化,大并發(fā)高可用性能要求下,需要進行一定的擴展,這時對其自身集成的緩存方案可能不太適用,該怎么辦?Spring預先有考慮到這點,那么怎樣利用Spring提供的擴展點實現(xiàn)我們自己的緩存,且在不改變原來已有代碼的情況下進行擴展?是否在方法執(zhí)行前就清空,默認為false,如果指定為true,則在方法還沒有執(zhí)行的時候就清空緩存,默認情況下,如果方法執(zhí)行拋出異常,則不會清空緩存。

這基本能夠滿足一般應用對緩存的需求,但現(xiàn)實總是很復雜,當你的用戶量上去或者性能跟不上,總需要進行擴展,這個時候你或許對其提供的內(nèi)存緩存不滿意了,因為其不支持高可用性,也不具備持久化數(shù)據(jù)能力,這個時候,你就需要自定義你的緩存方案了,還好,Spring也想到了這一點。

我們先不考慮如何持久化緩存,畢竟這種第三方的實現(xiàn)方案很多,我們要考慮的是,怎么利用Spring提供的擴展點實現(xiàn)我們自己的緩存,且在不改原來已有代碼的情況下進行擴展。這需要簡單的三步驟,首先需要提供一個CacheManager接口的實現(xiàn)(繼承至AbstractCacheManager),管理自身的cache實例;其次,實現(xiàn)自己的cache實例MyCache(繼承至Cache),在這里面引入我們需要的第三方cache或自定義cache;最后就是對配置項進行聲明,將MyCache實例注入CacheManager進行統(tǒng)一管理。

酒店商家端自定義注解緩存

注解緩存的使用,可以有效增強應用代碼的可讀性,同時統(tǒng)一管理緩存,提供較好的可擴展性,為此,酒店商家端在Spring注解緩存基礎上,自定義了適合自身業(yè)務特性的注解緩存。

主要使用兩個標簽,即@HotelCacheable、@HotelCacheEvict,其作用和配置方法見表2。

表2

標簽類型 作用 主要配置參數(shù)說明
@HotelCacheable 主要針對方法配置,能夠根據(jù)方法的請求參數(shù)對其結果進行緩存 domain: 作用域,針對集合場景,解決批量更新問題; domainKey: 作用域?qū)木彺鎘ey; key: 緩存對象key 前綴; fieldKey: 緩存對象key,與前綴合并生成對象key; condition: 緩存獲取前置條件,支持spel語法; cacheCondition: 緩存刷入前置條件,支持spel語法; expireTime: 超時時間設置
@HotelCacheEvict 主要針對方法配置,能夠根據(jù)一定的條件對緩存進行清空 同上

增加作用域的概念,解決商家信息變更下,多重重要信息實時更新的問題。

圖13 域緩存處理圖

如圖13,按舊的方案,當cache0發(fā)送變化時,為了保持信息的實時更新,需要手動刪除cache1、cache2、cache3等相關處的緩存數(shù)據(jù)。增加域緩存概念,cache0、cache1、cache2、cache3是以賬號ID為基礎,相互存在影響約束的集合體,我們作為一個域集合,增加域緩存處理,當cache0發(fā)送變化時,整體的賬號ID domain域已發(fā)生更新,自動影響cache1、cache2、cache3等處的緩存數(shù)據(jù)。將相關聯(lián)邏輯緩存統(tǒng)一化,有效提升代碼可讀性,同時更好服務業(yè)務,賬號重點信息能夠?qū)崟r變更刷新,相關服務響應速度提升。

另外,增加了cacheCondition緩存刷入前置判斷,有效解決商家業(yè)務多重外部依賴場景下,業(yè)務降級有損服務下,業(yè)務數(shù)據(jù)一致性保證,不因為緩存的增加影響業(yè)務的準確性;自定義CacheManager緩存管理器,可以有效兼容公共基礎組件Medis、Cellar相關服務,在對應用程序不做改動的情況下,有效切換緩存方式;同時,統(tǒng)一的緩存服務AOP入口,結合接入Mtconfig統(tǒng)一配置管理,對應用內(nèi)緩存做好降級準備,一鍵關閉緩存。幾點建議:

  • 上面介紹過Spring Cache的原理是基于動態(tài)生成的proxy代理機制來進行切面處理,關鍵點是對象的引用問題,如果對象的方法是類里面的內(nèi)部調(diào)用(this引用)而不是外部引用的場景下,會導致proxy失敗,那么我們所做的緩存切面處理也就失效了。因此,應避免已注解緩存的方法在類里面的內(nèi)部調(diào)用。
  • 使用的key約束,緩存的key應盡量使用簡單的可區(qū)別的元素,如ID、名稱等,不能使用list等容器的值,或者使用整體model對象的值。非public方法無法使用注解緩存實現(xiàn)。

總之,注釋驅(qū)動的Spring Cache能夠極大的減少我們編寫常見緩存的代碼量,通過少量的注釋標簽和配置文件,即可達到使代碼具備緩存的能力,且具備很好的靈活性和擴展性。但是我們也應該看到,Spring Cache由于基于Spring AOP技術,尤其是動態(tài)的proxy技術,導致其不能很好的支持方法的內(nèi)部調(diào)用或者非public方法的緩存設置,當然這些都是可以解決的問題。

作者簡介

明輝,美團點評酒旅事業(yè)群酒店住宿研發(fā)團隊B端商家業(yè)務平臺負責人,主導構建商家業(yè)務平臺系統(tǒng),支撐美團點評酒店住宿業(yè)務的飛速發(fā)展需求。曾任職于聯(lián)想集團、百度。

 

來自:http://tech.meituan.com/cache_about.html

 

標簽: Google Mysql O2O swap 安全 代碼 服務器 互聯(lián)網(wǎng) 排名 數(shù)據(jù)庫 通信 網(wǎng)絡 應用服務器

版權申明:本站文章部分自網(wǎng)絡,如有侵權,請聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點!
本站所提供的圖片等素材,版權歸原作者所有,如需使用,請與原作者聯(lián)系。

上一篇:JAVA 常用集合內(nèi)部機制原理

下一篇:Android性能優(yōu)化之被忽視的優(yōu)化點