成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

Guava Cache全面解析

開發(fā) 前端
Guava Cache的架構(gòu)設(shè)計靈感ConcurrentHashMap,在簡單場景中可以通過HashMap實現(xiàn)簡單數(shù)據(jù)緩存,但如果要實現(xiàn)緩存隨時間改變、存儲的數(shù)據(jù)空間可控則緩存工具還是很有必要的。

緩存分為本地緩存和遠(yuǎn)端緩存。常見的遠(yuǎn)端緩存有Redis,MongoDB;本地緩存一般使用map的方式保存在本地內(nèi)存中。一般我們在業(yè)務(wù)中操作緩存,都會操作緩存和數(shù)據(jù)源兩部分。如:put數(shù)據(jù)時,先插入DB,再刪除原來的緩存;ge數(shù)據(jù)時,先查緩存,命中則返回,沒有命中時,需要查詢DB,再把查詢結(jié)果放入緩存中 。如果訪問量大,我們還得兼顧本地緩存的線程安全問題。必要的時候也要考慮緩存的回收策略。

今天說的 Guava Cache 是google guava中的一個內(nèi)存緩存模塊,用于將數(shù)據(jù)緩存到JVM內(nèi)存中。他很好的解決了上面提到的幾個問題:

  • 很好的封裝了get、put操作,能夠集成數(shù)據(jù)源 ;
  • 線程安全的緩存,與ConcurrentMap相似,但前者增加了更多的元素失效策略,后者只能顯示的移除元素;
  • Guava Cache提供了三種基本的緩存回收方式:基于容量回收、定時回收和基于引用回收。定時回收有兩種:按照寫入時間,最早寫入的最先回收;按照訪問時間,最早訪問的最早回收;
  • 監(jiān)控緩存加載/命中情況

Guava Cache的架構(gòu)設(shè)計靈感ConcurrentHashMap,在簡單場景中可以通過HashMap實現(xiàn)簡單數(shù)據(jù)緩存,但如果要實現(xiàn)緩存隨時間改變、存儲的數(shù)據(jù)空間可控則緩存工具還是很有必要的。Cache存儲的是鍵值對的集合,不同時是還需要處理緩存過期、動態(tài)加載等算法邏輯,需要額外信息實現(xiàn)這些操作,對此根據(jù)面向?qū)ο蟮乃枷耄€需要做方法與數(shù)據(jù)的關(guān)聯(lián)性封裝,主要實現(xiàn)的緩存功能有:自動將節(jié)點加載至緩存結(jié)構(gòu)中,當(dāng)緩存的數(shù)據(jù)超過最大值時,使用LRU算法替換;它具備根據(jù)節(jié)點上一次被訪問或?qū)懭霑r間計算緩存過期機制,緩存的key被封裝在WeakReference引用中,緩存的value被封裝在WeakReference或SoftReference引用中;還可以統(tǒng)計緩存使用過程中的命中率、異常率和命中率等統(tǒng)計數(shù)據(jù)。

構(gòu)建緩存對象

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;


public class GuavaCacheService {

    public void setCache() {
        LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
                //設(shè)置并發(fā)級別為8,并發(fā)級別是指可以同時寫緩存的線程數(shù)
                .concurrencyLevel(8)
                //設(shè)置緩存容器的初始容量為10
                .initialCapacity(10)
                //設(shè)置緩存最大容量為100,超過100之后就會按照LRU最近雖少使用算法來移除緩存項
                .maximumSize(100)
                //是否需要統(tǒng)計緩存情況,該操作消耗一定的性能,生產(chǎn)環(huán)境應(yīng)該去除
                .recordStats()
                //設(shè)置寫緩存后n秒鐘過期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                //設(shè)置讀寫緩存后n秒鐘過期,實際很少用到,類似于expireAfterWrite
                //.expireAfterAccess(17, TimeUnit.SECONDS)
                //只阻塞當(dāng)前數(shù)據(jù)加載線程,其他線程返回舊值
                //.refreshAfterWrite(13, TimeUnit.SECONDS)
                //設(shè)置緩存的移除通知
                .removalListener(notification -> {
                    System.out.println(notification.getKey() + " " + notification.getValue() + " 被移除,原因:" + notification.getCause());
                })
                //build方法中可以指定CacheLoader,在緩存不存在時通過CacheLoader的實現(xiàn)自動加載緩存
                .build(new DemoCacheLoader());

        //模擬線程并發(fā)
        new Thread(() -> {
            //非線程安全的時間格式化工具
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
            try {
                for (int i = 0; i < 10; i++) {
                    String value = cache.get(1);
                    System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value);
                    TimeUnit.SECONDS.sleep(3);
                }
            } catch (Exception ignored) {
            }
        }).start();

        new Thread(() -> {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
            try {
                for (int i = 0; i < 10; i++) {
                    String value = cache.get(1);
                    System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value);
                    TimeUnit.SECONDS.sleep(5);
                }
            } catch (Exception ignored) {
            }
        }).start();
        //緩存狀態(tài)查看
        System.out.println(cache.stats().toString());

    }

    /**
     * 隨機緩存加載,實際使用時應(yīng)實現(xiàn)業(yè)務(wù)的緩存加載邏輯,例如從數(shù)據(jù)庫獲取數(shù)據(jù)
     */
    public static class DemoCacheLoader extends CacheLoader<Integer, String> {
        @Override
        public String load(Integer key) throws Exception {
            System.out.println(Thread.currentThread().getName() + " 加載數(shù)據(jù)開始");
            TimeUnit.SECONDS.sleep(8);
            Random random = new Random();
            System.out.println(Thread.currentThread().getName() + " 加載數(shù)據(jù)結(jié)束");
            return "value:" + random.nextInt(10000);
        }
    }
}

LoadingCache是Cache的子接口,相比較于Cache,當(dāng)從LoadingCache中讀取一個指定key的記錄時,如果該記錄不存在,則LoadingCache可以自動執(zhí)行加載數(shù)據(jù)到緩存的操作。

在調(diào)用CacheBuilder的build方法時,必須傳遞一個CacheLoader類型的參數(shù),CacheLoader的load方法需要我們提供實現(xiàn)。當(dāng)調(diào)用LoadingCache的get方法時,如果緩存不存在對應(yīng)key的記錄,則CacheLoader中的load方法會被自動調(diào)用從外存加載數(shù)據(jù),load方法的返回值會作為key對應(yīng)的value存儲到LoadingCache中,并從get方法返回。

當(dāng)然如果你不想指定重建策略,那么你可以使用無參的build()方法,它將返回Cache類型的構(gòu)建對象。

CacheBuilder 是Guava 提供的一個快速構(gòu)建緩存對象的工具類。CacheBuilder類采用builder設(shè)計模式,它的每個方法都返回CacheBuilder本身,直到build方法被調(diào)用。該類中提供了很多的參數(shù)設(shè)置選項,你可以設(shè)置cache的默認(rèn)大小,并發(fā)數(shù),存活時間,過期策略等等。

可選配置分析

緩存的并發(fā)級別

Guava提供了設(shè)置并發(fā)級別的api,使得緩存支持并發(fā)的寫入和讀取。同 ConcurrentHashMap 類似Guava cache的并發(fā)也是通過分離鎖實現(xiàn)。在一般情況下,將并發(fā)級別設(shè)置為服務(wù)器cpu核心數(shù)是一個比較不錯的選擇。

CacheBuilder.newBuilder()
  // 設(shè)置并發(fā)級別為cpu核心數(shù)
  .concurrencyLevel(Runtime.getRuntime().availableProcessors()) 
  .build();

緩存的初始容量設(shè)置

在構(gòu)建緩存時可以為緩存設(shè)置一個合理大小初始容量,由于Guava的緩存使用了分離鎖的機制,擴(kuò)容的代價非常昂貴。所以合理的初始容量能夠減少緩存容器的擴(kuò)容次數(shù)。

CacheBuilder.newBuilder()
  // 設(shè)置初始容量為100
  .initialCapacity(100)
  .build();

設(shè)置最大存儲

Guava Cache可以在構(gòu)建緩存對象時指定緩存所能夠存儲的最大記錄數(shù)量。當(dāng)Cache中的記錄數(shù)量達(dá)到最大值后再調(diào)用put方法向其中添加對象,Guava會先從當(dāng)前緩存的對象記錄中選擇一條刪除掉,騰出空間后再將新的對象存儲到Cache中。

  • 基于容量的清除: 通過CacheBuilder.maximumSize(long)方法可以設(shè)置Cache的最大容量數(shù),當(dāng)緩存數(shù)量達(dá)到或接近該最大值時,Cache將清除掉那些最近最少使用的緩存;
  • 基于權(quán)重的清除: 使用CacheBuilder.weigher(Weigher)指定一個權(quán)重函數(shù),并且用CacheBuilder.maximumWeight(long)指定最大總重。比如每一項緩存所占據(jù)的內(nèi)存空間大小都不一樣,可以看作它們有不同的“權(quán)重”(weights)。

緩存清除策略

  • 基于存活時間的清除

expireAfterWrite 寫緩存后多久過期

expireAfterAccess 讀寫緩存后多久過期

refreshAfterWrite 寫入數(shù)據(jù)后多久過期,只阻塞當(dāng)前數(shù)據(jù)加載線程,其他線程返回舊值

這幾個策略時間可以單獨設(shè)置,也可以組合配置。

  • 上面提到的基于容量的清除
  • 顯式清除

個別清除:Cache.invalidate(key)

批量清除:Cache.invalidateAll(keys)

清除所有緩存項:Cache.invalidateAll()

  • 基于引用的清除

在構(gòu)建Cache實例過程中,通過設(shè)置使用弱引用的鍵、或弱引用的值、或軟引用的值,從而使JVM在GC時順帶實現(xiàn)緩存的清除,不過一般不輕易使用這個特性。

  • CacheBuilder.weakKeys():使用弱引用存儲鍵。當(dāng)鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恒等式,使用弱引用鍵的緩存用而不是equals比較鍵。
  • CacheBuilder.weakValues():使用弱引用存儲值。當(dāng)值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恒等式,使用弱引用值的緩存用而不是equals比較值。
  • CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應(yīng)內(nèi)存需要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,我們通常建議使用更有性能預(yù)測性的緩存大小限定(見上文,基于容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值。

清理什么時候發(fā)生

也許這個問題有點奇怪,如果設(shè)置的存活時間為一分鐘,難道不是一分鐘后這個key就會立即清除掉嗎?我們來分析一下如果要實現(xiàn)這個功能,那Cache中就必須存在線程來進(jìn)行周期性地檢查、清除等工作,很多cache如redis、ehcache都是這樣實現(xiàn)的。

使用CacheBuilder構(gòu)建的緩存不會”自動”執(zhí)行清理和回收工作,也不會在某個緩存項過期后馬上清理,也沒有諸如此類的清理機制。相反,它會在寫操作時順帶做少量的維護(hù)工作,或者偶爾在讀操作時做——如果寫操作實在太少的話。

這樣做的原因在于:如果要自動地持續(xù)清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環(huán)境下線程創(chuàng)建可能受限制,這樣CacheBuilder就不可用了。參考如下示例:

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;

public class GuavaCacheService {


    static Cache<Integer, String> cache = CacheBuilder.newBuilder()
            .expireAfterWrite(5, TimeUnit.SECONDS)
            .build();

    public static void main(String[] args) throws Exception {
        new Thread(() -> {
            while (true) {
                SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
                System.out.println(sdf.format(new Date()) + " size: " + cache.size());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {

                }
            }
        }).start();
        SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
        cache.put(1, "a");
        System.out.println("寫入 key:1 ,value:" + cache.getIfPresent(1));
        Thread.sleep(10000);
        cache.put(2, "b");
        System.out.println("寫入 key:2 ,value:" + cache.getIfPresent(2));
        Thread.sleep(10000);
        System.out.println(sdf.format(new Date())
                + " sleep 10s , key:1 ,value:" + cache.getIfPresent(1));
        System.out.println(sdf.format(new Date())
                + " sleep 10s, key:2 ,value:" + cache.getIfPresent(2));
    }
}

部分輸出結(jié)果:
23:57:36 size: 0
寫入 key:1 ,value:a
23:57:38 size: 1
23:57:40 size: 1
23:57:42 size: 1
23:57:44 size: 1
23:57:46 size: 1
寫入 key:2 ,value:b
23:57:48 size: 1
23:57:50 size: 1
23:57:52 size: 1
23:57:54 size: 1
23:57:56 size: 1
23:57:56 sleep 10s , key:1 ,value:null
23:57:56 sleep 10s, key:2 ,value:null
23:57:58 size: 0
23:58:00 size: 0
23:58:02 size: 0
    ...
    ...

上面程序設(shè)置了緩存過期時間為5S,每打印一次當(dāng)前的size需要2S,打印了5次size之后寫入key 2,此時的size為1,說明在這個時候才把第一次應(yīng)該過期的key 1給刪除。

給移除操作添加一個監(jiān)聽器:

可以為Cache對象添加一個移除監(jiān)聽器,這樣當(dāng)有記錄被刪除時可以感知到這個事件:

RemovalListener<String, String> listener = notification -> System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(5)
                .removalListener(listener)
                .build();

但是要注意的是:默認(rèn)情況下,監(jiān)聽器方法是在移除緩存時同步調(diào)用的。因為緩存的維護(hù)和請求響應(yīng)通常是同時進(jìn)行的,代價高昂的監(jiān)聽器方法在同步模式下會拖慢正常的緩存請求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監(jiān)聽器裝飾為異步操作。

自動加載

上面我們說過使用get方法的時候如果key不存在你可以使用指定方法去加載這個key。在Cache構(gòu)建的時候通過指定CacheLoder的方式。如果你沒有指定,你也可以在get的時候顯式的調(diào)用call方法來設(shè)置key不存在的補救策略。

Cache的get方法有兩個參數(shù),第一個參數(shù)是要從Cache中獲取記錄的key,第二個記錄是一個Callable對象。

當(dāng)緩存中已經(jīng)存在key對應(yīng)的記錄時,get方法直接返回key對應(yīng)的記錄。如果緩存中不包含key對應(yīng)的記錄,Guava會啟動一個線程執(zhí)行Callable對象中的call方法,call方法的返回值會作為key對應(yīng)的值被存儲到緩存中,并且被get方法返回。

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

public class GuavaCacheService {


    private static Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(3)
            .build();

    public static void main(String[] args) {

        new Thread(() -> {
            System.out.println("thread1");
            try {
                String value = cache.get("key", new Callable<String>() {
                    public String call() throws Exception {
                        System.out.println("thread1"); //加載數(shù)據(jù)線程執(zhí)行標(biāo)志
                        Thread.sleep(1000); //模擬加載時間
                        return "thread1";
                    }
                });
                System.out.println("thread1 " + value);
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            System.out.println("thread2");
            try {
                String value = cache.get("key", new Callable<String>() {
                    public String call() throws Exception {
                        System.out.println("thread2"); //加載數(shù)據(jù)線程執(zhí)行標(biāo)志
                        Thread.sleep(1000); //模擬加載時間
                        return "thread2";
                    }
                });
                System.out.println("thread2 " + value);
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }).start();
    }

}

輸出結(jié)果為:
thread1
thread2
thread2
thread1 thread2
thread2 thread2

可以看到輸出結(jié)果:兩個線程都啟動,輸出thread1,thread2,接著又輸出了thread2,說明進(jìn)入了thread2的call方法了,此時thread1正在阻塞,等待key被設(shè)置。然后thread1 得到了value是thread2,thread2的結(jié)果自然也是thread2。

這段代碼中有兩個線程共享同一個Cache對象,兩個線程同時調(diào)用get方法獲取同一個key對應(yīng)的記錄。由于key對應(yīng)的記錄不存在,所以兩個線程都在get方法處阻塞。此處在call方法中調(diào)用Thread.sleep(1000)模擬程序從外存加載數(shù)據(jù)的時間消耗。

從結(jié)果中可以看出,雖然是兩個線程同時調(diào)用get方法,但只有一個get方法中的Callable會被執(zhí)行(沒有打印出load2)。Guava可以保證當(dāng)有多個線程同時訪問Cache中的一個key時,如果key對應(yīng)的記錄不存在,Guava只會啟動一個線程執(zhí)行g(shù)et方法中Callable參數(shù)對應(yīng)的任務(wù)加載數(shù)據(jù)存到緩存。當(dāng)加載完數(shù)據(jù)后,任何線程中的get方法都會獲取到key對應(yīng)的值。

統(tǒng)計信息

可以對Cache的命中率、加載數(shù)據(jù)時間等信息進(jìn)行統(tǒng)計。在構(gòu)建Cache對象時,可以通過CacheBuilder的recordStats方法開啟統(tǒng)計信息的開關(guān)。開關(guān)開啟后Cache會自動對緩存的各種操作進(jìn)行統(tǒng)計,調(diào)用Cache的stats方法可以查看統(tǒng)計后的信息。

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;


public class GuavaCacheService {


    public static void main(String[] args) {
        Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .recordStats() //開啟統(tǒng)計信息開關(guān)
                .build();
        cache.put("1", "v1");
        cache.put("2", "v2");
        cache.put("3", "v3");
        cache.put("4", "v4");

        cache.getIfPresent("1");
        cache.getIfPresent("2");
        cache.getIfPresent("3");
        cache.getIfPresent("4");
        cache.getIfPresent("5");
        cache.getIfPresent("6");

        System.out.println(cache.stats()); //獲取統(tǒng)計信息
    }

}

輸出:
CacheStats{hitCount=3, missCount=3, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=1}


責(zé)任編輯:武曉燕 來源: 一安未來
相關(guān)推薦

2022-09-21 08:16:18

緩存框架

2023-09-11 07:46:03

Cache2k緩存

2024-01-04 08:33:11

異步JDK數(shù)據(jù)結(jié)構(gòu)

2025-05-16 08:53:06

2025-06-27 07:19:48

2017-04-10 18:34:16

AndroidNotificatio

2009-09-22 10:50:04

Hibernate c

2018-07-14 21:59:57

緩存數(shù)據(jù)庫數(shù)據(jù)

2010-07-22 09:25:09

telnet命令

2010-03-09 17:19:01

Linux時鐘

2010-06-24 15:35:04

IPx協(xié)議

2020-12-23 13:14:00

LinuxLinux內(nèi)存Swap

2021-03-30 10:50:18

Linux內(nèi)存命令

2010-01-06 17:12:57

Linux主要構(gòu)成

2009-07-06 09:17:51

2010-06-28 18:52:49

UML關(guān)系符號

2009-07-17 17:02:54

JRuby是什么

2021-11-19 17:26:11

AppApplication方法

2021-11-23 09:09:27

Applicationandroid系統(tǒng)開發(fā)

2011-04-12 15:00:48

Oracle碎片
點贊
收藏

51CTO技術(shù)棧公眾號

主站蜘蛛池模板: 欧美久久久久 | 黄 色 毛片免费 | 日韩精品一区二区在线 | 日韩人体视频 | 中文字幕国产精品 | 国产精品视频导航 | 国产高清在线 | 一级a性色生活片久久毛片波多野 | 毛片黄片| 午夜精品久久久久久久99黑人 | 麻豆视频在线免费看 | 国产日韩欧美在线一区 | 狠狠做深爱婷婷综合一区 | 日本午夜一区二区三区 | 亚洲久久一区 | 久久久久久久久国产 | 久久伊人精品 | 欧美激情区 | 日韩在线免费观看视频 | 久久久久国产精品一区二区 | 福利网址| 精品亚洲一区二区三区 | 97国产一区二区精品久久呦 | 亚州成人 | 久久国产精品一区 | 一区二区精品在线 | 日韩欧美在线观看一区 | 日本一区二区不卡 | 一区二区三区 在线 | 国产一区二区欧美 | 国产一区二区在线免费播放 | 久久精品视频播放 | 日韩视频三区 | 欧美激情精品久久久久久变态 | 天天射美女 | 中文字幕 欧美 日韩 | 色视频免费 | 亚洲国产精品久久久久秋霞不卡 | 成人在线视频免费观看 | 看一级毛片视频 | 日韩精品在线一区 |