1. 程式人生 > >3. java快取-程序內快取guava cache

3. java快取-程序內快取guava cache

guava cache的快取結構

在這裡插入圖片描述

常用的guava cache快取

根據上圖中的快取框架,我們常用的一些快取例項有:LocalManualCache和LocalLoadingCache,兩者唯一的區別就是LocalLoadingCache extends LocalManualCache implements LoadingCache<K,V>介面。
LocalManualCache和LocalLoadingCache兩者都是對LoaclCache的包裝,而LocalCache就是一個快取的儲存器,通過繼承AbstractMap和實現ConcurrentMap介面,實現了支援併發的本地map(可以看成類似的ConcunrrentHashMap),LocalCache不對外暴露,因此只能通過其他方式提供例項,這就是CacheBuilder,以後建議大家也可以通過Builder的形式對外暴露例項。

範例

手動載入快取

手動載入快取:需要提前把資料put,當資料不存在返回null

public class MyCache {
    private static Cache<String,Object> cache;
    static {
	    // removeListener 從快取中移除,呼叫這個方法
	    // initialCapacity 初始化容量
	    // concurrentLevel 併發的執行緒數
	    // expireAfterWrite 寫入多長時間後,失效
        cache = CacheBuilder.newBuilder()
                .removalListener(new RemovalListener<Object, Object>() {
                    @Override
                    public void onRemoval(RemovalNotification<Object, Object> removalNotification) {
                        System.out.println("remove  "+removalNotification.getKey()+":"+removalNotification.getValue());
                    }
                })
                .initialCapacity(30)
                .concurrencyLevel(5)
                .expireAfterWrite(20, TimeUnit.MINUTES)
                .build();
        //手動載入資料
        cache.put("1","name11");
        cache.put("2","name12");
        cache.put("3","name13");
        cache.put("4","name14");
        cache.put("5","name15");
        cache.put("6","name16");
    }
    public static void main(String[] args) throws ExecutionException {
        //獲取一個不存在的key
        System.out.println("------------------");
        System.out.println(cache.getIfPresent("7'"));
        //獲取一個存在的key
        System.out.println(cache.getIfPresent("1"));
        //獲取一個不存在的key,自己定義一個載入方法
        System.out.println(cache.get("7",new Callable(){
            @Override
            public Object call() throws Exception {
	            //返回值,一定不能為null
                return "dadad";
            }
        }));
        System.out.println(cache.getIfPresent("7"));
        cache.invalidate("1");
        System.out.println("------------------");
    }
}

----輸出:
------------------
null
name11
dadad
dadad
remove  1:name11
------------------

上述快取就可以看做是一個手動載入資料的快取,即使用前自己手動的載入完成資料,當然也可以呼叫特殊的方法,當呼叫時,資料不存在後再呼叫載入方法。

自動載入快取

自動載入快取:不需要提前載入資料,當get時,不存在資料,會自動根據CacheLoader載入資料。

public class MyLoadingCache {
    
    public static void main(String[] args) {
        // 自動載入的快取
        // expireAfterWrite 快取載入指定時間後,自動失效
        // maximumSize 快取數量超出指定數量後,載入新的快取,會根據指定策略淘汰老的快取
        // initialCapacity 建立是預設的大小
        // concurrencyLevel 併發執行緒數,類似於concurrentHashMap的segmentShift
        // CacheLoader 當快取沒有命中時,自動呼叫方法載入,返回值不能為空
        LoadingCache<String,User> loadingCache = CacheBuilder.newBuilder()
                .expireAfterWrite(30,TimeUnit.MINUTES)
                .maximumSize(300L)
                .concurrencyLevel(20)
                .initialCapacity(300)
                .build(new CacheLoader<String, User>() {
                    @Override
                    public User load(String key) throws Exception {
                        User user = getUserInfoFromDB(key);
                        return user;
                    }
                });
        System.out.println(loadingCache.getUnchecked("12").toString());

    }

    // 模擬從資料庫獲取資料
    private static User getUserInfoFromDB(String key) {
        if("123".equals(key)){
            //模擬資料庫中不存在的資料
            return null;
        }
        return new User(key);
    }
}

class User{
    String id;

    public User(String id) {
        this.id = id;
    }
    @Override
    public String toString(){
        return "id="+this.id;
    }

} 

---輸出內容
id=12

Cache和LoadingCache的醉醉噠區別,就是LoadingCache建立時,指定快取未命中後的載入來源即可,同時還提供getUnchecked方法,如果你的CacherLoader沒有返回檢查異常,可以呼叫getUnchecked方法。

當我們在上述LoadingCache中執行:

System.out.println(loadingCache.getUnchecked("12").toString());
---輸出:
Exception in thread "main" com.google.common.cache.CacheLoader$InvalidCacheLoadException: CacheLoader returned null for key 123.
	at com.google.common.cache.LocalCache$Segment.getAndRecordStats(LocalCache.java:2353)
	at com.google.common.cache.LocalCache$Segment.loadSync(LocalCache.java:2323)
	at com.google.common.cache.LocalCache$Segment.lockedGetOrLoad(LocalCache.java:2285)
	at com.google.common.cache.LocalCache$Segment.get(LocalCache.java:2200)
	at com.google.common.cache.LocalCache.get(LocalCache.java:3947)
	at com.google.common.cache.LocalCache.getOrLoad(LocalCache.java:3951)
	at com.google.common.cache.LocalCache$LocalLoadingCache.get(LocalCache.java:4874)
	at com.google.common.cache.LocalCache$LocalLoadingCache.getUnchecked(LocalCache.java:4880)

這就是LoadingCache的load方法,不能返回null。因此在load的時候,我們返回物件前必須處理null的問題。

Cache為什麼不能返回null

先不說為什麼不可以返回null,先假設cache可以接收null值。null的存在主要是影響判斷載入的時機,下面列出兩種情況下,判斷載入的時機

允許null

get時,key不存在,返回的是null。
get時,key存在,快取的value是null,返回的是null。
不能通過get的valuenull,來判斷載入的時機,只能通過containsKey來判斷,如果通過valuenull作為是否載入快取的標準,就會產生一個問題,如果快取的是null,那麼即使載入過一次,但是每次get時,同樣會再載入一次,這樣就沒有起到快取的作用。

不允許null
get時,key不存在返回null,返回null。
get時,key存在,快取的value不為null,返回非null值。
可以通過get的value==null,來判斷載入時機。
根據上述兩種方式,我們現在看看LocalCache中,如何處理get得到的null。
通過定位,找到LocalCache.Segment的如下程式碼

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      checkNotNull(key);
      checkNotNull(loader);
      try {
        if (count != 0) { // read-volatile
          // don't call getLiveEntry, which would ignore loading values
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            long now = map.ticker.read();
            V value = getLiveValue(e, now);
            if (value != null) {
              recordRead(e, now);
              statsCounter.recordHits(1);
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }
		// 看到這段註釋,我們就發現,LocalCache是通過value == null,就進行load
        // at this point e is either null or expired;
        return lockedGetOrLoad(key, hash, loader);
      } catch (ExecutionException ee) {
        Throwable cause = ee.getCause();
        if (cause instanceof Error) {
          throw new ExecutionError((Error) cause);
        } else if (cause instanceof RuntimeException) {
          throw new UncheckedExecutionException(cause);
        }
        throw ee;
      } finally {
        postReadCleanup();
      }
    }

通過分析LocalCache程式碼,LocalCache是根據get的value==null來判斷載入時機,因此通過load載入時,不允許返回null值,因此需要特殊判斷load的返回值,建議使用Optional進行包裝。

示例程式碼:

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

import java.util.concurrent.TimeUnit;

public class UserLocalCache {
    
    private final LoadingCache<Long,Optional<User>> loadingCache = CacheBuilder.newBuilder()
            .expireAfterWrite(60*24, TimeUnit.MINUTES)
            .maximumSize(500)
            .build(new CacheLoader<Long, Optional<User>>() {
                @Override
                public Optional<User> load(Long key) throws Exception {
                    // 模擬訪問資料庫
                    User userFromDb = getUserFromDb(key);
                    // 使用Optional進行包裝,雖然Optional裡面為null,但是對cache來說,該物件不為空
                    return Optional.fromNullable(userFromDb);
                }
            });
    private UserLocalCache(){}

    public User getUser(Long userId){
        if(userId != null) {
            Optional<User> userOpt = loadingCache.getUnchecked(2L);
            if (userOpt.isPresent()) {
                return userOpt.get();
            }
        }
        return null;
    }
    

    private static User getUserFromDb(Long id) {
        Long temp = 1L;
        if(temp.equals(id)){
            return null;
        }else{
            return new User(id,"name"+id);
        }
    }
}

class User{
    private Long id;
    private String name;
    public User(Long id, String name){
        this.id = id;
        this.name = name;
    }
    public String getName(){
        return this.name;
    }
}

更多的guava cache的分析,請等待。