溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Java緩存技術(shù)怎么使用

發(fā)布時間:2021-12-21 11:55:12 來源:億速云 閱讀:94 作者:iii 欄目:軟件技術(shù)

這篇文章主要介紹“Java緩存技術(shù)怎么使用”,在日常操作中,相信很多人在Java緩存技術(shù)怎么使用問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”Java緩存技術(shù)怎么使用”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

緩存和它的那些淘汰算法們

為什么我們需要緩存?

很久很久以前,在還沒有緩存的時候……用戶經(jīng)常是去請求一個對象,而這個對象是從數(shù)據(jù)庫去取,然后,這個對象變得越來越大,這個用戶每次的請求時間也越來越長了,這也把數(shù)據(jù)庫弄得很痛苦,他無時不刻不在工作。所以,這個事情就把用戶和數(shù)據(jù)庫弄得很生氣,接著就有可能發(fā)生下面兩件事情:

1.用戶很煩,在抱怨,甚至不去用這個應(yīng)用了(這是大多數(shù)情況下都會發(fā)生的)

2.數(shù)據(jù)庫為打包回家,離開這個應(yīng)用,然后,就出現(xiàn)了麻煩(沒地方去存儲數(shù)據(jù)了)(發(fā)生在極少數(shù)情況下)

上帝派來了緩存

在幾年之后,IBM(60年代)的研究人員引進了一個新概念,它叫“緩存”。

什么是緩存?

正如開篇所講,緩存是“存貯數(shù)據(jù)(使用頻繁的數(shù)據(jù))的臨時地方,因為取原始數(shù)據(jù)的代價太大了,所以我可以取得快一些?!?/p>

緩存可以認為是數(shù)據(jù)的池,這些數(shù)據(jù)是從數(shù)據(jù)庫里的真實數(shù)據(jù)復(fù)制出來的,并且為了能正確取回,被標上了標簽(鍵 ID)。太棒了

programmer one 已經(jīng)知道這點了,但是他還不知道下面的緩存術(shù)語。

命中:

當(dāng)客戶發(fā)起一個請求(我們說他想要查看一個產(chǎn)品信息),我們的應(yīng)用接受這個請求,并且如果是在第一次檢查緩存的時候,需要去數(shù)據(jù)庫讀取產(chǎn)品信息。

如果在緩存中,一個條目通過一個標記被找到了,這個條目就會被使用、我們就叫它緩存命中。所以,命中率也就不難理解了。

Cache Miss:

但是這里需要注意兩點:

1. 如果還有緩存的空間,那么,沒有命中的對象會被存儲到緩存中來。

2. 如果緩存滿了,而又沒有命中緩存,那么就會按照某一種策略,把緩存中的舊對象踢出,而把新的對象加入緩存池。而這些策略統(tǒng)稱為替代策略(緩存算法),這些策略會決定到底應(yīng)該提出哪些對象。

存儲成本:

當(dāng)沒有命中時,我們會從數(shù)據(jù)庫取出數(shù)據(jù),然后放入緩存。而把這個數(shù)據(jù)放入緩存所需要的時間和空間,就是存儲成本。

索引成本:

和存儲成本相仿。

失效:

當(dāng)存在緩存中的數(shù)據(jù)需要更新時,就意味著緩存中的這個數(shù)據(jù)失效了。

替代策略:

當(dāng)緩存沒有命中時,并且緩存容量已經(jīng)滿了,就需要在緩存中踢出一個老的條目,加入一條新的條目,而到底應(yīng)該踢出什么條目,就由替代策略決定。

最優(yōu)替代策略:

最優(yōu)的替代策略就是想把緩存中最沒用的條目給踢出去,但是未來是不能夠被預(yù)知的,所以這種策略是不可能實現(xiàn)的。但是有很多策略,都是朝著這個目前去努力。

緩存算法

沒有人能說清哪種緩存算法優(yōu)于其他的緩存算法

Least Frequently Used(LFU):

大家好,我是 LFU,我會計算為每個緩存對象計算他們被使用的頻率。我會把最不常用的緩存對象踢走。

Least Recently User(LRU):

我是 LRU 緩存算法,我把最近最少使用的緩存對象給踢走。

我總是需要去了解在什么時候,用了哪個緩存對象。如果有人想要了解我為什么總能把最近最少使用的對象踢掉,是非常困難的。

瀏覽器就是使用了我(LRU)作為緩存算法。新的對象會被放在緩存的頂部,當(dāng)緩存達到了容量極限,我會把底部的對象踢走,而技巧就是:我會把最新被訪問的緩存對象,放到緩存池的頂部。

所以,經(jīng)常被讀取的緩存對象就會一直呆在緩存池中。有兩種方法可以實現(xiàn)我,array 或者是 linked list。

我的速度很快,我也可以被數(shù)據(jù)訪問模式適配。我有一個大家庭,他們都可以完善我,甚至做的比我更好(我確實有時會嫉妒,但是沒關(guān)系)。我家庭的一些成員包括 LRU2 和 2Q,他們就是為了完善 LRU 而存在的。

First in First out(FIFO):

我是先進先出,我是一個低負載的算法,并且對緩存對象的管理要求不高。我通過一個隊列去跟蹤所有的緩存對象,最近最常用的緩存對象放在后面,而更早的緩存對象放在前面,當(dāng)緩存容量滿時,排在前面的緩存對象會被踢走,然后把新的緩存對象加進去。我很快,但是我并不適用。

Second Chance:

大家好,我是 second chance,我是通過 FIFO 修改而來的,被大家叫做 second chance 緩存算法,我比 FIFO 好的地方是我改善了 FIFO 的成本。我是 FIFO 一樣也是在觀察隊列的前端,但是很FIFO的立刻踢出不同,我會檢查即將要被踢出的對象有沒有之前被使用過的標志(1一個 bit 表示),沒有被使用過,我就把他踢出;否則,我會把這個標志位清除,然后把這個緩存對象當(dāng)做新增緩存對象加入隊列。你可以想象就這就像一個環(huán)隊列。當(dāng)我再一次在隊頭碰到這個對象時,由于他已經(jīng)沒有這個標志位了,所以我立刻就把他踢開了。我在速度上比 FIFO 快。

其他的緩存算法還考慮到了下面幾點:

成本:如果緩存對象有不同的成本,應(yīng)該把那些難以獲得的對象保存下來。

容量:如果緩存對象有不同的大小,應(yīng)該把那些大的緩存對象清除,這樣就可以讓更多的小緩存對象進來了。

時間:一些緩存還保存著緩存的過期時間。電腦會失效他們,因為他們已經(jīng)過期了。

根據(jù)緩存對象的大小而不管其他的緩存算法可能是有必要的。

看看緩存元素(緩存實體)

public class CacheElement
 {
     private Object objectValue;
     private Object objectKey;
     private int index;
     private int hitCount; // getters and setters
 }

這個緩存實體擁有緩存的key和value,這個實體的數(shù)據(jù)結(jié)構(gòu)會被以下所有緩存算法用到。

緩存算法的公用代碼

 public final synchronized void addElement(Object key, Object value)
 {
     int index;
     Object obj;
     // get the entry from the table
     obj = table.get(key);
     // If we have the entry already in our table
     // then get it and replace only its value.
     obj = table.get(key);
     if (obj != null)
     {
         CacheElement element;
         element = (CacheElement) obj;
         element.setObjectValue(value);
         element.setObjectKey(key);
         return;
     }
 }

上面的代碼會被所有的緩存算法實現(xiàn)用到。這段代碼是用來檢查緩存元素是否在緩存中了,如果是,我們就替換它,但是如果我們找不到這個 key 對應(yīng)的緩存,我們會怎么做呢?那我們就來深入的看看會發(fā)生什么吧!

現(xiàn)場訪問

今天的專題很特殊,因為我們有特殊的客人,事實上他們是我們想要聽的與會者,但是首先,先介紹一下我們的客人:Random Cache,F(xiàn)IFO Cache。讓我們從 Random Cache開始。

看看隨機緩存的實現(xiàn)

 public final synchronized void addElement(Object key, Object value)
 {
     int index;
     Object obj;
     obj = table.get(key);
     if (obj != null)
     {
         CacheElement element;// Just replace the value.
         element = (CacheElement) obj;
         element.setObjectValue(value);
         element.setObjectKey(key);
         return;
     }// If we haven't filled the cache yet, put it at the end.
     if (!isFull())
     {
         index = numEntries;
         ++numEntries;
     }
     else { // Otherwise, replace a random entry.
         index = (int) (cache.length * random.nextFloat());
         table.remove(cache[index].getObjectKey());
     }
     cache[index].setObjectValue(value);
     cache[index].setObjectKey(key);
     table.put(key, cache[index]);
 }

看看FIFO緩算法的實現(xiàn)

 public final synchronized void addElement(Objectkey, Object value)
 {
     int index;
     Object obj;
     obj = table.get(key);
     if (obj != null)
     {
         CacheElement element; // Just replace the value.
         element = (CacheElement) obj;
         element.setObjectValue(value);
         element.setObjectKey(key);
         return;
     }
     // If we haven't filled the cache yet, put it at the end.
     if (!isFull())
     {
         index = numEntries;
         ++numEntries;
     }
     else { // Otherwise, replace the current pointer,
            // entry with the new one.
         index = current;
         // in order to make Circular FIFO
         if (++current >= cache.length)
             current = 0;
         table.remove(cache[index].getObjectKey());
     }
     cache[index].setObjectValue(value);
     cache[index].setObjectKey(key);
     table.put(key, cache[index]);
 }

看看LFU緩存算法的實現(xiàn)

 public synchronized Object getElement(Object key)
 {
     Object obj;
     obj = table.get(key);
     if (obj != null)
     {
         CacheElement element = (CacheElement) obj;
         element.setHitCount(element.getHitCount() + 1);
         return element.getObjectValue();
     }
     return null;
 }
 public final synchronized void addElement(Object key, Object value)
 {
     Object obj;
     obj = table.get(key);
     if (obj != null)
     {
         CacheElement element; // Just replace the value.
         element = (CacheElement) obj;
         element.setObjectValue(value);
         element.setObjectKey(key);
         return;
     }
     if (!isFull())
     {
         index = numEntries;
         ++numEntries;
     }
     else
     {
         CacheElement element = removeLfuElement();
         index = element.getIndex();
         table.remove(element.getObjectKey());
     }
     cache[index].setObjectValue(value);
     cache[index].setObjectKey(key);
     cache[index].setIndex(index);
     table.put(key, cache[index]);
 }
 public CacheElement removeLfuElement()
 {
     CacheElement[] elements = getElementsFromTable();
     CacheElement leastElement = leastHit(elements);
     return leastElement;
 }
 public static CacheElement leastHit(CacheElement[] elements)
 {
     CacheElement lowestElement = null;
     for (int i = 0; i < elements.length; i++)
     {
         CacheElement element = elements[i];
         if (lowestElement == null)
         {
             lowestElement = element;
         }
         else {
             if (element.getHitCount() < lowestElement.getHitCount())
             {
                 lowestElement = element;
             }
         }
     }
     return lowestElement;
 }

最重點的代碼,就應(yīng)該是 leastHit 這個方法,這段代碼就是把hitCount 最低的元素找出來,然后刪除,給新進的緩存元素留位置

看看LRU緩存算法實現(xiàn)

 private void moveToFront(int index)
 {
     int nextIndex, prevIndex;
     if(head != index)
     {
         nextIndex = next[index];
         prevIndex = prev[index];
         // Only the head has a prev entry that is an invalid index
         // so we don't check.
         next[prevIndex] = nextIndex;
         // Make sure index is valid. If it isn't, we're at the tail
         // and don't set prev[next].
         if(nextIndex >= 0)
             prev[nextIndex] = prevIndex;
         else
             tail = prevIndex;
         prev[index] = -1;
         next[index] = head;
         prev[head] = index;
         head = index;
     }
 }
 public final synchronized void addElement(Object key, Object value)
 {
     int index;Object obj;
     obj = table.get(key);
     if(obj != null)
     {
         CacheElement entry;
         // Just replace the value, but move it to the front.
         entry = (CacheElement)obj;
         entry.setObjectValue(value);
         entry.setObjectKey(key);
         moveToFront(entry.getIndex());
         return;
     }
     // If we haven't filled the cache yet, place in next available
     // spot and move to front.
     if(!isFull())
     {
         if(_numEntries > 0)
         {
             prev[_numEntries] = tail;
             next[_numEntries] = -1;
             moveToFront(numEntries);
         }
         ++numEntries;
     }
     else { // We replace the tail of the list.
         table.remove(cache[tail].getObjectKey());
         moveToFront(tail);
     }
     cache[head].setObjectValue(value);
     cache[head].setObjectKey(key);
     table.put(key, cache[head]);
 }

這段代碼的邏輯如 LRU算法 的描述一樣,把再次用到的緩存提取到最前面,而每次刪除的都是最后面的元素。

緩存技術(shù)雜談

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

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

緩存特征

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

命中率

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

最大元素(或最大空間)

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

清空策略

如上描述,緩存的存儲空間有限制,當(dāng)緩存空間被用滿時,如何保證在穩(wěn)定服務(wù)的同時有效提升命中率?這就由緩存清空策略來處理,設(shè)計適合自身數(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ù)關(guān)鍵字(或元素內(nèi)容)長短清理等。

緩存介質(zhì)

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

  • 內(nèi)存:將緩存存儲于內(nèi)存中是最快的選擇,無需額外的I/O開銷,但是內(nèi)存的缺點是沒有持久化落地物理磁盤,一旦應(yīng)用異常break down而重新啟動,數(shù)據(jù)很難或者無法復(fù)原。

  • 硬盤:一般來說,很多緩存框架會結(jié)合使用內(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存儲結(jié)構(gòu)的特殊數(shù)據(jù)庫(如BerkeleyDB和Redis),響應(yīng)速度和吞吐量都遠遠高于我們常用的關(guān)系型數(shù)據(jù)庫等。

緩存分類和應(yīng)用場景

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

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

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

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

本地緩存

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

個別場景下,我們只需要簡單的緩存數(shù)據(jù)的功能,而無需關(guān)注更多存取、清空策略等深入的特性時,直接編程實現(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ù),結(jié)果存入緩存
            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結(jié)構(gòu)緩存部分業(yè)務(wù)數(shù)據(jù),減少頻繁的重復(fù)數(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è)務(wù)中常用的城市基礎(chǔ)基本信息判斷,通過靜態(tài)變量一次獲取緩存內(nèi)存中,減少頻繁的I/O讀取,靜態(tài)變量實現(xiàn)類間可共享,進程內(nèi)可共享,緩存的實時性稍差。

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

Ehcache

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

Java緩存技術(shù)怎么使用

主要特性:

  • 快速,針對大型高并發(fā)系統(tǒng)場景,Ehcache的多線程機制有相應(yīng)的優(yōu)化改善。

  • 簡單,很小的jar包,簡單配置就可直接使用,單機場景下無需過多的其他服務(wù)依賴。

  • 支持多種的緩存策略,靈活。

  • 緩存數(shù)據(jù)有兩級:內(nèi)存和磁盤,與一般的本地內(nèi)存緩存相比,有了磁盤的存儲空間,將可以支持更大量的數(shù)據(jù)緩存需求。

  • 具有緩存和緩存管理器的偵聽接口,能更簡單方便的進行緩存實例的監(jiān)控管理。

  • 支持多緩存管理器實例,以及一個實例的多個緩存區(qū)域。

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

分布式緩存

memcached緩存

memcached是應(yīng)用較廣的開源分布式緩存產(chǎn)品之一,它本身其實不提供分布式解決方案。在服務(wù)端,memcached集群環(huán)境實際就是一個個memcached服務(wù)器的堆積,環(huán)境搭建較為簡單;cache的分布式主要是在客戶端實現(xiàn),通過客戶端的路由處理來達到分布式解決方案的目的。客戶端做路由的原理非常簡單,應(yīng)用服務(wù)器在每次存取某key的value時,通過某種算法把key映射到某臺memcached服務(wù)器nodeA上。

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

Redis緩存

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

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

  • 在主頁中顯示最新的項目列表:Redis使用的是常駐內(nèi)存的緩存,速度非???。LPUSH用來插入一個內(nèi)容ID,作為關(guān)鍵字存儲在列表頭部。LTRIM用來限制列表中的項目數(shù)最多為5000。如果用戶需要的檢索的數(shù)據(jù)量超越這個緩存容量,這時才需要把請求發(fā)送到數(shù)據(jù)庫。

  • 刪除和過濾:如果一篇文章被刪除,可以使用LREM從緩存中徹底清除掉。

  • 排行榜及相關(guān)問題:排行榜(leader board)按照得分進行排序。ZADD命令可以直接實現(xiàn)這個功能,而ZREVRANGE命令可以用來按照得分來獲取前100名的用戶,ZRANK可以用來獲取用戶排名,非常直接而且操作容易。

  • 按照用戶投票和時間排序:排行榜,得分會隨著時間變化。LPUSH和LTRIM命令結(jié)合運用,把文章添加到一個列表中。一項后臺任務(wù)用來獲取列表,并重新計算列表的排序,ZADD命令用來按照新的順序填充生成列表。列表可以實現(xiàn)非??焖俚臋z索,即使是負載很重的站點。

  • 過期項目處理:使用Unix時間作為關(guān)鍵字,用來保持列表能夠按時間排序。對current_time和time_to_live進行檢索,完成查找過期項目的艱巨任務(wù)。另一項后臺任務(wù)使用ZRANGE…WITHSCORES進行查詢,刪除過期的條目。

  • 計數(shù):進行各種數(shù)據(jù)統(tǒng)計的用途是非常廣泛的,比如想知道什么時候封鎖一個IP地址。INCRBY命令讓這些變得很容易,通過原子遞增保持計數(shù);GETSET用來重置計數(shù)器;過期屬性用來確認一個關(guān)鍵字什么時候應(yīng)該刪除。

  • 特定時間內(nèi)的特定項目:這是特定訪問者的問題,可以通過給每次頁面瀏覽使用SADD命令來解決。SADD不會將已經(jīng)存在的成員添加到一個集合。

  • Pub/Sub:在更新中保持用戶對數(shù)據(jù)的映射是系統(tǒng)中的一個普遍任務(wù)。Redis的pub/sub功能使用了SUBSCRIBE、UNSUBSCRIBE和PUBLISH命令,讓這個變得更加容易。

  • 隊列:在當(dāng)前的編程中隊列隨處可見。除了push和pop類型的命令之外,Redis還有阻塞隊列的命令,能夠讓一個程序在執(zhí)行時被另一個程序添加到隊列。

緩存常見問題

前面一節(jié)說到了《 為什么說Redis是單線程的以及Redis為什么這么快!》,今天給大家整理一篇關(guān)于Redis經(jīng)常被問到的問題:緩存雪崩、緩存穿透、緩存預(yù)熱、緩存更新、緩存降級等概念的入門及簡單解決方案。

一、緩存雪崩

緩存雪崩我們可以簡單的理解為:由于原有緩存失效,新緩存未到期間(例如:我們設(shè)置緩存時采用了相同的過期時間,在同一時刻出現(xiàn)大面積的緩存過期),所有原本應(yīng)該訪問緩存的請求都去查詢數(shù)據(jù)庫了,而對數(shù)據(jù)庫CPU和內(nèi)存造成巨大壓力,嚴重的會造成數(shù)據(jù)庫宕機。從而形成一系列連鎖反應(yīng),造成整個系統(tǒng)崩潰。

緩存正常從Redis中獲取,示意圖如下:

Java緩存技術(shù)怎么使用

緩存失效瞬間示意圖如下:

Java緩存技術(shù)怎么使用

緩存失效時的雪崩效應(yīng)對底層系統(tǒng)的沖擊非??膳?!大多數(shù)系統(tǒng)設(shè)計者考慮用加鎖或者隊列的方式保證來保證不會有大量的線程對數(shù)據(jù)庫一次性進行讀寫,從而避免失效時大量的并發(fā)請求落到底層存儲系統(tǒng)上。還有一個簡單方案就時講緩存失效時間分散開,比如我們可以在原有的失效時間基礎(chǔ)上增加一個隨機值,比如1-5分鐘隨機,這樣每一個緩存的過期時間的重復(fù)率就會降低,就很難引發(fā)集體失效的事件。

以下簡單介紹兩種實現(xiàn)方式的偽代碼:

(1)碰到這種情況,一般并發(fā)量不是特別多的時候,使用最多的解決方案是加鎖排隊,偽代碼如下:

//偽代碼
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    String lockKey = cacheKey;
    String cacheValue = CacheHelper.get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    } else {
        synchronized(lockKey) {
            cacheValue = CacheHelper.get(cacheKey);
            if (cacheValue != null) {
                return cacheValue;
            } else {
                //這里一般是sql查詢數(shù)據(jù)
                cacheValue = GetProductListFromDB(); 
                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
            }
        }
        return cacheValue;
    }
}

加鎖排隊只是為了減輕數(shù)據(jù)庫的壓力,并沒有提高系統(tǒng)吞吐量。假設(shè)在高并發(fā)下,緩存重建期間key是鎖著的,這是過來1000個請求999個都在阻塞的。同樣會導(dǎo)致用戶等待超時,這是個治標不治本的方法!

注意:加鎖排隊的解決方式分布式環(huán)境的并發(fā)問題,有可能還要解決分布式鎖的問題;線程還會被阻塞,用戶體驗很差!因此,在真正的高并發(fā)場景下很少使用!

(2)還有一個解決辦法解決方案是:給每一個緩存數(shù)據(jù)增加相應(yīng)的緩存標記,記錄緩存的是否失效,如果緩存標記失效,則更新數(shù)據(jù)緩存,實例偽代碼如下:

//偽代碼
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    //緩存標記
    String cacheSign = cacheKey + "_sign";
    String sign = CacheHelper.Get(cacheSign);
    //獲取緩存值
    String cacheValue = CacheHelper.Get(cacheKey);
    if (sign != null) {
        return cacheValue; //未過期,直接返回
    } else {
        CacheHelper.Add(cacheSign, "1", cacheTime);
        ThreadPool.QueueUserWorkItem((arg) -> {
            //這里一般是 sql查詢數(shù)據(jù)
            cacheValue = GetProductListFromDB(); 
            //日期設(shè)緩存時間的2倍,用于臟讀
            CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
        });
        return cacheValue;
    }
}

解釋說明:

1、緩存標記:記錄緩存數(shù)據(jù)是否過期,如果過期會觸發(fā)通知另外的線程在后臺去更新實際key的緩存;

2、緩存數(shù)據(jù):它的過期時間比緩存標記的時間延長1倍,例:標記緩存時間30分鐘,數(shù)據(jù)緩存設(shè)置為60分鐘。 這樣,當(dāng)緩存標記key過期后,實際緩存還能把舊數(shù)據(jù)返回給調(diào)用端,直到另外的線程在后臺更新完成后,才會返回新緩存。

關(guān)于緩存崩潰的解決方法,這里提出了三種方案:使用鎖或隊列、設(shè)置過期標志更新緩存、為key設(shè)置不同的緩存失效時間,還有一各被稱為“二級緩存”的解決方法,有興趣的讀者可以自行研究。

二、緩存穿透

緩存穿透是指用戶查詢數(shù)據(jù),在數(shù)據(jù)庫沒有,自然在緩存中也不會有。這樣就導(dǎo)致用戶查詢的時候,在緩存中找不到,每次都要去數(shù)據(jù)庫再查詢一遍,然后返回空(相當(dāng)于進行了兩次無用的查詢)。這樣請求就繞過緩存直接查數(shù)據(jù)庫,這也是經(jīng)常提的緩存命中率問題。

有很多種方法可以有效地解決緩存穿透問題,最常見的則是采用布隆過濾器,將所有可能存在的數(shù)據(jù)哈希到一個足夠大的bitmap中,一個一定不存在的數(shù)據(jù)會被這個bitmap攔截掉,從而避免了對底層存儲系統(tǒng)的查詢壓力。

另外也有一個更為簡單粗暴的方法,如果一個查詢返回的數(shù)據(jù)為空(不管是數(shù)據(jù)不存在,還是系統(tǒng)故障),我們?nèi)匀话堰@個空結(jié)果進行緩存,但它的過期時間會很短,最長不超過五分鐘。通過這個直接設(shè)置的默認值存放到緩存,這樣第二次到緩沖中獲取就有值了,而不會繼續(xù)訪問數(shù)據(jù)庫,這種辦法最簡單粗暴!

//偽代碼
public object GetProductListNew() {
    int cacheTime = 30;
    String cacheKey = "product_list";
    String cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    }
    cacheValue = CacheHelper.Get(cacheKey);
    if (cacheValue != null) {
        return cacheValue;
    } else {
        //數(shù)據(jù)庫查詢不到,為空
        cacheValue = GetProductListFromDB();
        if (cacheValue == null) {
            //如果發(fā)現(xiàn)為空,設(shè)置個默認值,也緩存起來
            cacheValue = string.Empty;
        }
        CacheHelper.Add(cacheKey, cacheValue, cacheTime);
        return cacheValue;
    }
}

把空結(jié)果,也給緩存起來,這樣下次同樣的請求就可以直接返回空了,即可以避免當(dāng)查詢的值為空時引起的緩存穿透。同時也可以單獨設(shè)置個緩存區(qū)域存儲空值,對要查詢的key進行預(yù)先校驗,然后再放行給后面的正常緩存處理邏輯。

三、緩存預(yù)熱

緩存預(yù)熱這個應(yīng)該是一個比較常見的概念,相信很多小伙伴都應(yīng)該可以很容易的理解,緩存預(yù)熱就是系統(tǒng)上線后,將相關(guān)的緩存數(shù)據(jù)直接加載到緩存系統(tǒng)。這樣就可以避免在用戶請求的時候,先查詢數(shù)據(jù)庫,然后再將數(shù)據(jù)緩存的問題!用戶直接查詢事先被預(yù)熱的緩存數(shù)據(jù)!

解決思路:

1、直接寫個緩存刷新頁面,上線時手工操作下;

2、數(shù)據(jù)量不大,可以在項目啟動的時候自動進行加載;

3、定時刷新緩存;

四、緩存更新

除了緩存服務(wù)器自帶的緩存失效策略之外(Redis默認的有6中策略可供選擇),我們還可以根據(jù)具體的業(yè)務(wù)需求進行自定義的緩存淘汰,常見的策略有兩種:

(1)定時去清理過期的緩存;

(2)當(dāng)有用戶請求過來時,再判斷這個請求所用到的緩存是否過期,過期的話就去底層系統(tǒng)得到新數(shù)據(jù)并更新緩存。

兩者各有優(yōu)劣,第一種的缺點是維護大量緩存的key是比較麻煩的,第二種的缺點就是每次用戶請求過來都要判斷緩存失效,邏輯相對比較復(fù)雜!具體用哪種方案,大家可以根據(jù)自己的應(yīng)用場景來權(quán)衡。

五、緩存降級

當(dāng)訪問量劇增、服務(wù)出現(xiàn)問題(如響應(yīng)時間慢或不響應(yīng))或非核心服務(wù)影響到核心流程的性能時,仍然需要保證服務(wù)還是可用的,即使是有損服務(wù)。系統(tǒng)可以根據(jù)一些關(guān)鍵數(shù)據(jù)進行自動降級,也可以配置開關(guān)實現(xiàn)人工降級。

降級的最終目的是保證核心服務(wù)可用,即使是有損的。而且有些服務(wù)是無法降級的(如加入購物車、結(jié)算)。

在進行降級之前要對系統(tǒng)進行梳理,看看系統(tǒng)是不是可以丟卒保帥;從而梳理出哪些必須誓死保護,哪些可降級;比如可以參考日志級別設(shè)置預(yù)案:

(1)一般:比如有些服務(wù)偶爾因為網(wǎng)絡(luò)抖動或者服務(wù)正在上線而超時,可以自動降級;

(2)警告:有些服務(wù)在一段時間內(nèi)成功率有波動(如在95~100%之間),可以自動降級或人工降級,并發(fā)送告警;

(3)錯誤:比如可用率低于90%,或者數(shù)據(jù)庫連接池被打爆了,或者訪問量突然猛增到系統(tǒng)能承受的最大閥值,此時可以根據(jù)情況自動降級或者人工降級;

(4)嚴重錯誤:比如因為特殊原因數(shù)據(jù)錯誤了,此時需要緊急人工降級。

到此,關(guān)于“Java緩存技術(shù)怎么使用”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬嵱玫奈恼拢?/p>

向AI問一下細節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI