Guava cache本地緩存簡介、刷新機制簡單示例及封裝

簡介

Guava Cache與ConcurrentMap很相似,但也不完全一樣。最基本的區別是ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地,Guava Cache爲了限制內存佔用,通常都設定爲自動回收元素。在某些場景下,儘管LoadingCache 不回收元素,它也是很有用的,因爲它會自動加載緩存。

通常來說,Guava Cache適用於:

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

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

如同範例代碼展示的一樣,Cache實例通過CacheBuilder生成器模式獲取,但是自定義你的緩存纔是最有趣的部分。

:如果你不需要Cache中的特性,使用ConcurrentHashMap有更好的內存效率——但Cache的大多數特性都很難基於舊有的ConcurrentMap複製,甚至根本不可能做到。

Guava Cache的使用示例

使用緩存時,最常遇到的場景需要就是:

"獲取緩存-如果沒有-則計算"[get-if-absent-compute]的原子語義.

具體含義:

  1. 從緩存中取。
  2. 緩存中存在該數據,直接返回;
  3. 緩存中不存在該數據,從數據源中取。
  4. 數據源中存在該數據,放入緩存,並返回;
  5. 數據源中不存在該數據,返回空。

刷新機制

三種基於時間的清理或刷新緩存數據的方式:

expireAfterAccess: 當緩存項在指定的時間段內沒有被讀或寫就會被回收。

expireAfterWrite:當緩存項在指定的時間段內沒有更新就會被回收。

refreshAfterWrite:當緩存項上一次更新操作之後的多久會被刷新。

一、定時過期

LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .expireAfterWrite(30, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}

如代碼所示新建了名爲caches的一個緩存對象,定義了緩存大小、過期時間及緩存值生成方法。maximumSize定義了緩存的容量大小,當緩存數量即將到達容量上線時,則會進行緩存回收,回收最近沒有使用或總體上很少使用的緩存項。需要注意的是在接近這個容量上限時就會發生,所以在定義這個值的時候需要視情況適量地增大一點。 
另外通過expireAfterWrite這個方法定義了緩存的過期時間,寫入十分鐘之後過期。 
在build方法裏,傳入了一個CacheLoader對象,重寫了其中的load方法。當獲取的緩存值不存在或已過期時,則會調用此load方法,進行緩存值的計算。 

二、定時刷新

LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}

每隔十分鐘緩存值則會被刷新。

有一個需要注意也很難處理的地方,這裏的定時並不是真正意義上的定時。Guava cache的刷新需要依靠用戶請求線程,讓該線程去進行load方法的調用,所以如果一直沒有用戶嘗試獲取該緩存值,則該緩存也並不會刷新。

三、異步刷新

ListeningExecutorService backgroundRefreshPools = 
                MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20));
        LoadingCache<String, Object> caches = CacheBuilder.newBuilder()
                .maximumSize(100)
                .refreshAfterWrite(10, TimeUnit.MINUTES)
                .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) throws Exception {
                        return generateValueByKey(key);
                    }
 
                    @Override
                    public ListenableFuture<Object> reload(String key,
                            Object oldValue) throws Exception {
                        return backgroundRefreshPools.submit(new Callable<Object>() {
 
                            @Override
                            public Object call() throws Exception {
                                return generateValueByKey(key);
                            }
                        });
                    }
                });
try {
    System.out.println(caches.get("key-zorro"));
} catch (ExecutionException e) {
    e.printStackTrace();
}

當緩存的key很多時,高併發條件下大量線程同時獲取不同key對應的緩存,此時依然會造成大量線程阻塞,並且給數據庫帶來很大壓力。這個問題的解決辦法就是將刷新緩存值的任務交給後臺線程,所有的用戶請求線程均返回舊的緩存值,這樣就不會有用戶線程被阻塞了。

{

  • 可以看到防緩存穿透和防用戶線程阻塞都是依靠返回舊值來完成的。所以如果沒有舊值,同樣會全部阻塞,因此應視情況儘量在系統啓動時將緩存內容加載到內存中。

  • 在刷新緩存時,如果generateValueByKey方法出現異常或者返回了null,此時舊值不會更新。

  • 題外話:在使用內存緩存時,切記拿到緩存值之後不要在業務代碼中對緩存直接做修改,因爲此時拿到的對象引用是指向緩存真正的內容的。如果需要直接在該對象上進行修改,則在獲取到緩存值後拷貝一份副本,然後傳遞該副本,進行修改操作

}

封裝

封裝後的的緩存抽象類實現了刷新時間、時間單位、定時刷新及初始化緩存值等方法。實現類只需要設置自己的緩存時間等信息,實現preload()方法加載緩存數據,實現getCacheValue()方法將緩存數據添加到緩存中。

抽象類代碼如下:

package com.lenchy.lms.util.cache;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.ListenableFutureTask;
import com.homolo.datamodel.manager.EntityManager;
import com.homolo.framework.setup.Initializer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.task.TaskExecutor;

import javax.validation.constraints.NotNull;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

/**
 * 數據緩存抽象類
 *
 * @param <T> 緩存值的類型
 */
public abstract class BaseDataCache<T> extends Initializer {
	private static final Logger LOGGER = LoggerFactory.getLogger(BaseDataCache.class);
	private static final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(10);
	@Autowired
	private TaskExecutor taskExecutor;
	@Autowired
	protected EntityManager entityManager;

	private LoadingCache<String, T> cache;

	/**
	 * @return 時間
	 */
	protected abstract long duration();

	/**
	 * @return 時間單位
	 */
	protected abstract TimeUnit unit();

	/**
	 * 根據key生成緩存值的方法
	 *
	 * @param key key
	 * @return 緩存值
	 */
	protected abstract T getCacheValue(String key);

	/**
	 * 緩存到期是否自動刷新
	 */
	protected boolean refresh() {
		return false;
	}

	/**
	 * 預加載緩存數據,系統啓動後執行
	 *
	 * @return Runnable 返回 null 表示不執行預加載
	 */
	protected Runnable preload() {
		return null;
	}

	/**
	 * 獲取緩存值,請在初始化完成之後調用
	 *
	 * @param key key
	 * @return 緩存值,異常時返回 getCacheValue()
	 */
	public T get(String key) {
		try {
			return cache.get(key);
		} catch (ExecutionException e) {
			LOGGER.error("get cache error", e);
			return getCacheValue(key);
		}
	}

	@Override
	public void initialize() {
		long duration = duration();
		TimeUnit unit = unit();
		cache = CacheBuilder.newBuilder().refreshAfterWrite(duration(), unit())
				.build(new CacheLoader<String, T>() {
					@Override
					public T load(@NotNull String key) {
						return getCacheValue(key);
					}

					@Override
					public ListenableFuture<T> reload(final String key, T oldValue) throws Exception {
						ListenableFutureTask<T> task = ListenableFutureTask.create(new Callable<T>() {
							public T call() {
								return getCacheValue(key);
							}
						});
						taskExecutor.execute(task);
						return task;
					}
				});
		Runnable preload = preload();
		if (preload != null) {
			LOGGER.info("preload {}", this.getClass().getSimpleName());
			taskExecutor.execute(preload());
		}
		if (refresh()) {
			executorService.scheduleAtFixedRate(new Runnable() {
				@Override
				public void run() {
					for (String key : cache.asMap().keySet()) {
						cache.refresh(key);
					}
				}
			}, duration, duration, unit);
		}
	}

	@Override
	public int getPhase() {
		return 20000;
	}
}

簡單實現類

@Component
public class Statistic4JusticeCache extends BaseDataCache<Statistic4JusticeCache.JusticeCache> {
	@Autowired
	private JusticeBureauManager justiceBureauManager;
	@Autowired
	protected NationalDataCountUtil nationalDataStatisticsUtil;
	@Autowired
	private JusticeBureauFilter justiceBureauFilter;

	@Override
	protected long duration() {
		return 1;
	}

	@Override
	protected TimeUnit unit() {
		return TimeUnit.DAYS;
	}

	@Override
	protected boolean refresh() {
		return true;
	}

	@Override
	protected Runnable preload() {
		return new Runnable() {
			@Override
			public void run() {
				List<String> cacheKwys = new ArrayList<>();
                cacheKwys.add( "first" ); 
                cacheKwys.add( "second" ); 
                cacheKwys.add( "third" ); 
				for (String key : cacheKwys) {
					get(key);
				}
			}
		};
	}

	@Override
	protected Statistic4JusticeCache.JusticeCache getCacheValue(String key) {
		JusticeCache cache = new JusticeCache();
		cache.setFirstList(managerGetList(key));
		cache.setSecondList(getZoneList(key));
		cache.setThirdMap(getLawyerModifymap(key));
		return cache;
	}

    public static class JusticeCache {
		private List firstList;
		private List secondList;
		private Map thirdMap;

		public List getFirstList() {
			return firstList;
		}

		public void setFirstList(List firstList) {
			this.firstList = firstList;
		}

		public List getSecondlist() {
			return secondList;
		}

		public void setSecondList(List secondList) {
			this.secondList = secondList;
		}

		public Map getThirdMap() {
			return thirdMap;
		}

		public void setThirdMap(Map thirdMap) {
			this.thirdMap = thirdMap;
		}
	}

    public List<String> getFirstList(){
        List first = new ArrayList<>();
        first.add("first1");
        first.add("first2");
        return first;
    }

    public List<String> getSecondlist(){
        List second = new ArrayList<>();
        first.add("second1");
        first.add("second2");
        return first;
    }

    public Map<String,String> getThirdMap(){
        Map<String, String> third = new HashMap<String, String>();
        third.put( "third1" , "third1" ); 
        third.put( "third2" , "third2" );
        return third; 
    }


}

有不足的地方希望大家指出。。。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章