[Java 緩存] Java Cache之 Guava Cache的簡單應用.


前言

今天第一次使用MarkDown的形式發博客. 准備記錄一下自己對Guava Cache的認識及項目中的實際使用經驗.

一: 什么是Guava

Guava工程包含了若干被Google的 Java項目廣泛依賴 的核心庫,例如:集合 [collections] 、緩存 [caching] 、原生類型支持 [primitives support] 、並發庫 [concurrency libraries] 、通用注解 [common annotations] 、字符串處理 [string processing] 、I/O 等等。 所有這些工具每天都在被Google的工程師應用在產品服務中。

//Guava Cache的使用
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
        .expireAfterAccess(10, TimeUnit.MINUTES)
        .build(
            new CacheLoader<Key, Graph>() {
                public Graph load(Key key) { // no checked exception
                    return createExpensiveGraph(key);
                }
            });

...
return graphs.getUnchecked(key);

二: 使用場景

當我們使用一種新工具的時候 我們總要先弄清楚它到底適用於什么樣的場景.

  • 你願意消耗一些內存空間來提升速度。
  • 你預料到某些鍵會被查詢一次以上。
  • 緩存中存放的數據總量不會超出內存容量。(Guava Cache是單個應用運行時的本地緩存。它不把數據存放到文件或外部服務器。如果這不符合你的需求,請嘗試Memcached這類工具)

如果你的場景符合上述的每一條,Guava Cache就適合你。

三: 核心類圖

file-list

四: 使用實例

前面說了這么多, 都不如如何使用來的實在. 現在直接貼出來使用的實例, 具體實現的邏輯大家可以看下源碼, 這里也會有一些實際的講解.

在pom文件中引入Guava Cache的坐標:

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
</dependency>

下面拿我們實際項目中使用的一個GuavaCache來舉例:

public abstract class BaseCacheService<K,V> {
    private LoadingCache<K,V> cache;

    public BaseCacheService(){
        cache = CacheBuilder.newBuilder()
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .build(new CacheLoader<K, V>() {
                    @Override
                    public V load(K k) throws Exception {
                        return loadData(k);
                    }
                });
    }

    public BaseCacheService(long duration){
        cache = CacheBuilder.newBuilder()
                .expireAfterWrite(duration, TimeUnit.MINUTES)
                .build(new CacheLoader<K, V>() {
                    @Override
                    public V load(K k) throws Exception {
                        return loadData(k);
                    }
                });
    }

    protected abstract V loadData(K k);

    public V getCache(K param){
        return cache.getUnchecked(param);
    }

    //更新緩存中數據
    public void refresh(K k){
        cache.refresh(k);
    }
}

這里我是抽象出來了一個BaseCacheService, 當我們使用時則可以繼承這個抽象類:
如果我們第一次請求, 那么這會執行這里面的load方法去數據庫中查詢相應的值, 當第二次請求時這會從緩存中直接返回了.

@Service
public class MaterialInfoCacheService extends BaseCacheService<Long, List<MaterialInfoDto>> {

    @Override
    protected List<MaterialInfoDto> loadData(Long key) {
        //具體的查詢數據庫得到數據的邏輯.

        return materialInfoDtos;
    }
}

這里面有關於緩存的回收(expireAfterWrite), 有關於緩存的刷新(refresh)等, 這些東西會一一來介紹.

緩存的回收:

1, 基於容量的回收(size-based eviction)
如果要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或總體上很少使用的緩存項。——警告:在緩存項的數目達到限定值之前,緩存就可能進行回收操作——通常來說,這種情況發生在緩存項的數目逼近限定值時。

另外,不同的緩存項有不同的“權重”(weights)——例如,如果你的緩存值,占據完全不同的內存空間,你可以使用CacheBuilder.weigher(Weigher)指定一個權重函數,並且用CacheBuilder.maximumWeight(long)指定最大總重。在權重限定場景中,除了要注意回收也是在重量逼近限定值時就進行了,還要知道重量是在緩存創建時計算的,因此要考慮重量計算的復雜度。

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
        .maximumWeight(100000)
        .weigher(new Weigher<Key, Graph>() {
            public int weigh(Key k, Graph g) {
                return g.vertices().size();
            }
        })
        .build(
            new CacheLoader<Key, Graph>() {
                public Graph load(Key key) { // no checked exception
                    return createExpensiveGraph(key);
                }
            });

2, 定時回收(Timed Eviction)
CacheBuilder提供兩種定時回收的方法:

  • expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣。
  • expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。

3, 基於引用的回收(Reference-based Eviction)
通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:

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

其實這里使用最多的還是基於時間的定時回收, 其他的兩種回收方式大家可以根據自己的項目而定.

緩存的顯示刷新和清除:

(任何時候,你都可以顯式地清除緩存項,而不是等到它被回收)
這里需要說明下刷新(refresh)和清除(invalidate)的區別:
刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個過程可以是異步的。在刷新操作進行時,
緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。
如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日志后被丟棄 .

  • 刷新: Cache.refresh(K k)
  • 個別清除:Cache.invalidate(key)
  • 批量清除:Cache.invalidateAll(keys)
  • 清除所有緩存項:Cache.invalidateAll()

三: 使用實例

這里更新下我在項目中常用的guava cache的實例. 更新於2016年12月14日.

LoadingCache<String, Map<Long, CarAttentionDTO>> cache = CacheBuilder.newBuilder()
            .expireAfterAccess(30, TimeUnit.MINUTES)
            .build(new CacheLoader<String, Map<Long, CarAttentionDTO>>() {
                public Map<Long, CarAttentionDTO> load(String key) { // no checked exception
                    LOGGER.info("loading car week attention data......");
                    long startTime = System.currentTimeMillis();
                    List<String> groupBy = Lists.newArrayList();
                    groupBy.add("key2");

                    Map<String, String> where = Maps.newHashMap();
                    where.put("group_name", String.valueOf(CommonConstants.CounterGroup.ATTENTION));
                    where.put("key1", String.valueOf(CommonConstants.DataType.CAR));

                    Calendar cal = Calendar.getInstance();
                    Date dateTo = DateUtils.addDays(cal.getTime(), -1);
                    Date dateFrom = DateUtils.addDays(cal.getTime(), -8);

                    int dayTo = Integer.valueOf(DateFormatUtils.format(dateTo, "yyyyMMdd"));
                    int dayFrom = Integer.valueOf(DateFormatUtils.format(dateFrom, "yyyyMMdd"));
                    List<CountDayUvEntity> list = uvEntityDao.countByParams(groupBy, where, dayFrom, dayTo);

                    int multiple = configReader.getInt(CommonConstants.SystemConfigKey.ATTENTION_MULTIPLE, 53);
                    Map<Long, CarAttentionDTO> tempMap = Maps.newHashMap();
                    for (CountDayUvEntity uvEntity : list) {
                        CarAttentionDTO attentionDTO = new CarAttentionDTO();
                        attentionDTO.setCarId(Long.valueOf(uvEntity.getKey2()));
                        attentionDTO.setAttention(uvEntity.getCount() * multiple + RandomUtils.nextInt(0, 10));
                        tempMap.put(attentionDTO.getCarId(), attentionDTO);
                    }

                    LOGGER.info("load car week attention finished. useTime=" + (System.currentTimeMillis() - startTime));
                    return tempMap;
                }
            });

private Cache<String, Object> carIndexCache = CacheBuilder.newBuilder().expireAfterAccess(20, TimeUnit.MINUTES).build();

public Map<Long, Long> getCarAttentions() throws ExecutionException {
        String key = "getCarAttentions";
        return (Map<Long, Long>) carIndexCache.get(key, new Callable<Map<Long, Long>>() {
            @Override
            public Map<Long, Long> call() throws Exception {
                List<CarIndexEntity> carIndexs = carIndexEntityDao.findAll(
                        CarIndexEntity.Fields.type.eq(CommonConstants.CarIndexStatus.ATTENTION));
                Map<Long, Long> data = Maps.newHashMapWithExpectedSize(carIndexs.size());
                for (CarIndexEntity carIndex : carIndexs) {
                    data.put(carIndex.getCarId(), carIndex.getCount());
                }
                return data;
            }
        });
    }

public Map<Long, Long> getCarSales() throws ExecutionException {
        String key = "getCarSales";
        return (Map<Long, Long>) carIndexCache.get(key, new Callable<Map<Long, Long>>() {
            @Override
            public Map<Long, Long> call() throws Exception {
                List<CarIndexEntity> carIndexs = carIndexEntityDao.findAll(
                        CarIndexEntity.Fields.type.eq(CommonConstants.CarIndexStatus.SALES));
                Map<Long, Long> data = Maps.newHashMapWithExpectedSize(carIndexs.size());
                for (CarIndexEntity carIndex : carIndexs) {
                    data.put(carIndex.getCarId(), carIndex.getCount());
                }

                return data;
            }
        });
    }

其實兩種情況都是一樣的, 第二個是使用場景是一個service有多個方法都需要用到guava cache.

好了 知道了這些就可以在項目中直接使用了, 更多的內容請看Guava Cache官方文檔(翻譯版):http://ifeve.com/google-guava-cachesexplained/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM