在上一篇文章 萬字長文聊緩存(上)中,咱們主要如何圍繞着Http作緩存優化,在後端服務器的應用層一樣有不少地方能夠作緩存,提升服務的效率;本篇咱們就來繼續聊聊應用級的緩存。html
緩存的命中率是指從緩存中獲取到數據的次數和總讀取次數的比率,命中率越高證實緩存的效果越好。這是一個很重要的指標,應該經過監控這個指標來判斷咱們的緩存是否設置的合理。git
設置緩存的存儲空間,好比:設置緩存的空間是 1G,當達到了1G以後就會按照必定的策略將部分數據移除github
設置緩存的最大條目數,當達到了設置的最大條目數以後按照必定的策略將舊的數據移除web
堆緩存是指把數據緩存在JVM的堆內存中,使用堆緩存的好處是沒有序列化和反序列化的操做,是最快的緩存。若是緩存的數據量很大,爲了不形成OOM一般狀況下使用的時軟引用來存儲緩存對象;堆緩存的缺點是緩存的空間有限,而且垃圾回收器暫停的時間會變長。redis
Cache<string, string> cache = CacheBuilder.newBuilder() .build();
經過CacheBuilder
構建緩存對象算法
Gauva Cache的主要配置和方法spring
put
: 向緩存中設置key-valueV get(K key, Callable<!--? extends V--> loader)
: 獲取一個緩存值,若是緩存中沒有,那麼就調用loader獲取一個而後放入到緩存expireAfterWrite
: 設置緩存的存活期,寫入數據後指定時間以後失效expireAfterAccess
: 設置緩存的空閒期,在給定的時間內沒有被訪問就會被回收maximumSize
: 設置緩存的最大條目數weakKeys/weakValues
: 設置弱引用緩存softValues
: 設置軟引用緩存invalidate/invalidateAll
: 主動失效指定key的緩存數據recordStats
: 啓動記錄統計信息,能夠查看到命中率removalListener
: 當緩存被刪除的時候會調用此監聽器,能夠用於查看爲何緩存會被刪除Caffeine是使用Java8對Guava緩存的重寫版本,高性能Java本地緩存組件,也是Spring推薦的堆緩存的實現,與spring的集成能夠查看文檔https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache-store-configuration-caffeine。數據庫
因爲是對Guava緩存的重寫版本,因此不少的配置參數都是和Guava緩存一致:後端
initialCapacity
: 初始的緩存空間大小maximumSize
: 緩存的最大條數maximumWeight
: 緩存的最大權重expireAfterAccess
: 最後一次寫入或訪問後通過固定時間過時expireAfterWrite
: 最後一次寫入後通過固定時間過時expireAfter
: 自定義過時策略refreshAfterWrite
: 建立緩存或者最近一次更新緩存後通過固定的時間間隔,刷新緩存weakKeys
: 打開key的弱引用weakValues
:打開value的弱引用softValues
:打開value的軟引用recordStats
:開啓統計功能Caffeine的官方文檔:https://github.com/ben-manes/caffeine/wiki緩存
<dependency> <groupid>com.github.ben-manes.caffeine</groupid> <artifactid>caffeine</artifactid> <version>2.8.4</version> </dependency>
public Object manual(String key) { Cache<string, object> cache = Caffeine.newBuilder() .expireAfterAccess(1, TimeUnit.SECONDS) //設置空閒期時長 .maximumSize(10) .build(); return cache.get(key, t -> setValue(key).apply(key)); } public Function<string, object> setValue(String key){ return t -> "https://silently9527.cn"; }
public Object sync(String key){ LoadingCache<string, object> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) //設置存活期時長 .build(k -> setValue(key).apply(key)); return cache.get(key); } public Function<string, object> setValue(String key){ return t -> "https://silently9527.cn"; }
public CompletableFuture async(String key) { AsyncLoadingCache<string, object> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .buildAsync(k -> setAsyncValue().get()); return cache.get(key); } public CompletableFuture<object> setAsyncValue() { return CompletableFuture.supplyAsync(() -> "公衆號:貝塔學JAVA"); }
public void removeListener() { Cache<string, object> cache = Caffeine.newBuilder() .removalListener((String key, Object value, RemovalCause cause) -> { System.out.println("remove lisitener"); System.out.println("remove Key:" + key); System.out.println("remove Value:" + value); }) .build(); cache.put("name", "silently9527"); cache.invalidate("name"); }
public void recordStats() { Cache<string, object> cache = Caffeine.newBuilder() .maximumSize(10000) .recordStats() .build(); cache.put("公衆號", "貝塔學JAVA"); cache.get("公衆號", (t) -> ""); cache.get("name", (t) -> "silently9527"); CacheStats stats = cache.stats(); System.out.println(stats); }
經過 Cache.stats()
獲取到CacheStats
。CacheStats
提供如下統計方法:
hitRate()
: 返回緩存命中率evictionCount()
: 緩存回收數量averageLoadPenalty()
: 加載新值的平均時間EhCache 是老牌Java開源緩存框架,早在2003年就已經出現了,發展到如今已經很是成熟穩定,在Java應用領域應用也很是普遍,並且和主流的Java框架好比Srping能夠很好集成。相比於 Guava Cache,EnCache 支持的功能更豐富,包括堆外緩存、磁盤緩存,固然使用起來要更重一些。使用 Ehcache 的Maven 依賴以下:
<dependency> <groupid>org.ehcache</groupid> <artifactid>ehcache</artifactid> <version>3.6.3</version> </dependency>
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.heap(10); //設置最大緩存條目數 CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10))) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig);
ResourcePoolsBuilder.heap(10)
設置緩存的最大條目數,這是簡寫方式,等價於ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, EntryUnit.ENTRIES);
ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB)
設置緩存最大的空間10MBwithExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofMinutes(10)))
設置緩存空閒時間withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10)))
設置緩存存活時間remove/removeAll
主動失效緩存,與Guava Cache相似,調用方法後不會當即去清除回收,只有在get或者put的時候判斷緩存是否過時withSizeOfMaxObjectSize(10,MemoryUnit.KB)
限制單個緩存對象的大小,超過這兩個限制的對象則不被緩存堆外緩存即緩存數據在堆外內存中,空間大小隻受本機內存大小限制,不受GC管理,使用堆外緩存能夠減小GC暫停時間,可是堆外內存中的對象都須要序列化和反序列化,KEY和VALUE必須實現Serializable接口,所以速度會比堆內緩存慢。在Java中能夠經過 -XX:MaxDirectMemorySize
參數設置堆外內存的上限
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); // 堆外內存不能按照存儲條目限制,只能按照內存大小進行限制,超過限制則回收緩存 ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().offheap(10, MemoryUnit.MB); CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withDispatcherConcurrency(4) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) .withSizeOfMaxObjectSize(10, MemoryUnit.KB) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo2", cacheConfig); cache.put("website", "https://silently9527.cn"); System.out.println(cache.get("website"));
把緩存數據存放到磁盤上,在JVM重啓時緩存的數據不會受到影響,而堆緩存和堆外緩存都會丟失;而且磁盤緩存有更大的存儲空間;可是緩存在磁盤上的數據也須要支持序列化,速度會被比內存更慢,在使用時推薦使用更快的磁盤帶來更大的吞吐率,好比使用閃存代替機械磁盤。
CacheManagerConfiguration<persistentcachemanager> persistentManagerConfig = CacheManagerBuilder .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache")); PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder() .with(persistentManagerConfig).build(true); //disk 第三個參數設置爲 true 表示將數據持久化到磁盤上 ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().disk(100, MemoryUnit.MB, true); CacheConfiguration<string, string> config = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource).build(); Cache<string, string> cache = persistentCacheManager.createCache("userInfo", CacheConfigurationBuilder.newCacheConfigurationBuilder(config)); cache.put("公衆號", "貝塔學JAVA"); System.out.println(cache.get("公衆號")); persistentCacheManager.close();
在JVM中止時,必定要記得調用persistentCacheManager.close()
,保證內存中的數據可以dump到磁盤上。
這是典型 heap + offheap + disk 組合的結構圖,上層比下層速度快,下層比上層存儲空間大,在ehcache中,空間大小設置 heap > offheap > disk
,不然會報錯; ehcache 會將最熱的數據保存在高一級的緩存。這種結構的代碼以下:
CacheManagerConfiguration<persistentcachemanager> persistentManagerConfig = CacheManagerBuilder .persistence(new File("/Users/huaan9527/Desktop", "ehcache-cache")); PersistentCacheManager persistentCacheManager = CacheManagerBuilder.newCacheManagerBuilder() .with(persistentManagerConfig).build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder() .heap(10, MemoryUnit.MB) .offheap(100, MemoryUnit.MB) //第三個參數設置爲true,支持持久化 .disk(500, MemoryUnit.MB, true); CacheConfiguration<string, string> config = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource).build(); Cache<string, string> cache = persistentCacheManager.createCache("userInfo", CacheConfigurationBuilder.newCacheConfigurationBuilder(config)); //寫入緩存 cache.put("name", "silently9527"); // 讀取緩存 System.out.println(cache.get("name")); // 再程序關閉前,須要手動釋放資源 persistentCacheManager.close();
前面提到的堆內緩存和堆外緩存若是在多個JVM實例的狀況下會有兩個問題:1.單機容量畢竟有限;2.多臺JVM實例緩存的數據可能不一致;3.若是緩存數據同一時間都失效了,那麼請求都會打到數據庫上,數據庫壓力增大。這時候咱們就須要引入分佈式緩存來解決,如今使用最多的分佈式緩存是redis
當引入分佈式緩存以後就能夠把應用緩存的架構調整成上面的結構。
緩存使用的模式大概分爲兩類:Cache-Aside、Cache-As-SoR(SoR表示實際存儲數據的系統,也就是數據源)
業務代碼圍繞着緩存來寫,一般都是從緩存中來獲取數據,若是緩存沒有命中,則從數據庫中查找,查詢到以後就把數據放入到緩存;當數據被更新以後,也須要對應的去更新緩存中的數據。這種模式也是咱們一般使用最多的。
value = cache.get(key); //從緩存中讀取數據 if(value == null) { value = loadFromDatabase(key); //從數據庫中查詢 cache.put(key, value); //放入到緩存中 }
wirteToDatabase(key, value); //寫入到數據庫 cache.put(key, value); //放入到緩存中 或者 能夠刪除掉緩存 cache.remove(key) ,再讀取的時候再查一次
Spring的Cache擴展就是使用的Cache-Aside模式,Spring爲了把業務代碼和緩存的讀取更新分離,對Cache-Aside模式使用AOP進行了封裝,提供了多個註解來實現讀寫場景。官方參考文檔:
@Cacheable
: 一般是放在查詢方法上,實現的就是Cache-Aside
讀的場景,先查緩存,若是不存在在查詢數據庫,最後把查詢出來的結果放入到緩存。@CachePut
: 一般用在保存更新方法上面,實現的就是Cache-Aside
寫的場景,更新完成數據庫後把數據放入到緩存中。@CacheEvict
: 從緩存中刪除指定key的緩存> 對於一些容許有一點點更新延遲基礎數據能夠考慮使用canal訂閱binlog日誌來完成緩存的增量更新。 > > Cache-Aside還有個問題,若是某個時刻熱點數據緩存失效,那麼會有不少請求同時打到後端數據庫上,數據庫的壓力會瞬間增大
Cache-As-SoR模式也就會把Cache看作是數據源,全部的操做都是針對緩存,Cache在委託給真正的SoR去實現讀或者寫。業務代碼中只會看到Cache的操做,這種模式又分爲了三種
應用程序始終從緩存中請求數據,若是緩存中沒有數據,則它負責使用提供的數據加載程序從數據庫中檢索數據,檢索數據後,緩存會自行更新並將數據返回給調用的應用程序。Gauva Cache、Caffeine、EhCache都支持這種模式;
LoadingCache<key, graph> cache = Caffeine.newBuilder() .maximumSize(10_000) .expireAfterWrite(10, TimeUnit.MINUTES) .build(key -> createExpensiveGraph(key)); // Lookup and compute an entry if absent, or null if not computable Graph graph = cache.get(key); // Lookup and compute entries that are absent Map<key, graph> graphs = cache.getAll(keys);
在build Cache的時候指定一個CacheLoader
cache.get(key)
CacheLoader
去數據源中查詢數據,以後在放入到緩存,返回給應用程序> CacheLoader
不要直接返回null,建議封裝成本身定義的Null對像,在放入到緩存中,能夠防止緩存擊穿
爲了防止由於某個熱點數據失效致使後端數據庫壓力增大的狀況,我能夠在CacheLoader
中使用鎖限制只容許一個請求去查詢數據庫,其餘的請求都等待第一個請求查詢完成後從緩存中獲取,在上一篇 《萬字長文聊緩存(上)》中咱們聊到了Nginx也有相似的配置參數
value = loadFromCache(key); if(value != null) { return value; } synchronized (lock) { value = loadFromCache(key); if(value != null) { return value; } return loadFromDatabase(key); }
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //設置最大緩存條目數 CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) .withLoaderWriter(new CacheLoaderWriter<string, string>(){ @Override public String load(String key) throws Exception { //load from database return "silently9527"; } @Override public void write(String key, String value) throws Exception { } @Override public void delete(String key) throws Exception { } }) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig); System.out.println(cache.get("name"));
在EhCache中使用的是CacheLoaderWriter
來從數據庫中加載數據;解決由於某個熱點數據失效致使後端數據庫壓力增大的問題和上面的方式同樣,也能夠在load
中實現。
和Read Through模式相似,當數據進行更新時,先去更新SoR,成功以後在更新緩存。
Cache<string, string> cache = Caffeine.newBuilder() .maximumSize(100) .writer(new CacheWriter<string, string>() { @Override public void write(@NonNull String key, @NonNull String value) { //write data to database System.out.println(key); System.out.println(value); } @Override public void delete(@NonNull String key, @Nullable String value, @NonNull RemovalCause removalCause) { //delete from database } }) .build(); cache.put("name", "silently9527");
Caffeine經過使用CacheWriter
來實現Write Through,CacheWriter
能夠同步的監聽到緩存的建立、變動和刪除操做,只有寫成功了纔會去更新緩存
CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder().build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //設置最大緩存條目數 CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withExpiry(ExpiryPolicyBuilder.timeToLiveExpiration(Duration.ofMinutes(10))) .withLoaderWriter(new CacheLoaderWriter<string, string>(){ @Override public String load(String key) throws Exception { return "silently9527"; } @Override public void write(String key, String value) throws Exception { //write data to database System.out.println(key); System.out.println(value); } @Override public void delete(String key) throws Exception { //delete from database } }) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig); System.out.println(cache.get("name")); cache.put("website","https://silently9527.cn");
EhCache仍是經過CacheLoaderWriter
來實現的,當咱們調用cache.put("xxx","xxx")
進行寫緩存的時候,EhCache首先會將寫的操做委託給CacheLoaderWriter
,有CacheLoaderWriter.write
去負責寫數據源
這種模式一般先將數據寫入緩存,再異步地寫入數據庫進行數據同步。這樣的設計既能夠減小對數據庫的直接訪問,下降壓力,同時對數據庫的屢次修改能夠合併操做,極大地提高了系統的承載能力。可是這種模式也存在風險,如當緩存機器出現宕機時,數據有丟失的可能。
CacheLoaderWriter.write
方法中把數據發送到MQ中,實現異步的消費,這樣能夠保證數據的安全,可是要想實現合併操做就須要擴展功能更強大的CacheLoaderWriter
。//1 定義線程池 PooledExecutionServiceConfiguration testWriteBehind = PooledExecutionServiceConfigurationBuilder .newPooledExecutionServiceConfigurationBuilder() .pool("testWriteBehind", 5, 10) .build(); CacheManager cacheManager = CacheManagerBuilder.newCacheManagerBuilder() .using(testWriteBehind) .build(true); ResourcePoolsBuilder resource = ResourcePoolsBuilder.newResourcePoolsBuilder().heap(10, MemoryUnit.MB); //設置最大緩存條目數 //2 設置回寫模式配置 WriteBehindConfiguration testWriteBehindConfig = WriteBehindConfigurationBuilder .newUnBatchedWriteBehindConfiguration() .queueSize(10) .concurrencyLevel(2) .useThreadPool("testWriteBehind") .build(); CacheConfiguration<string, string> cacheConfig = CacheConfigurationBuilder .newCacheConfigurationBuilder(String.class, String.class, resource) .withLoaderWriter(new CacheLoaderWriter<string, string>() { @Override public String load(String key) throws Exception { return "silently9527"; } @Override public void write(String key, String value) throws Exception { //write data to database } @Override public void delete(String key) throws Exception { } }) .add(testWriteBehindConfig) .build(); Cache<string, string> cache = cacheManager.createCache("userInfo", cacheConfig);
首先使用PooledExecutionServiceConfigurationBuilder
定義了線程池配置;而後使用WriteBehindConfigurationBuilder
設置會寫模式配置,其中newUnBatchedWriteBehindConfiguration
表示不進行批量寫操做,由於是異步寫,因此須要把寫操做先放入到隊列中,經過queueSize
設置隊列大小,useThreadPool
指定使用哪一個線程池; concurrencyLevel
設置使用多少個併發線程和隊列進行Write Behind
EhCache實現批量寫的操做也很容易
newUnBatchedWriteBehindConfiguration()
替換成newBatchedWriteBehindConfiguration(10, TimeUnit.SECONDS, 20)
,這裏設置的是數量達到20就進行批處理,若是10秒內沒有達到20個也會進行處理CacheLoaderWriter
中實現wirteAll 和 deleteAll進行批處理> 若是須要把對相同的key的操做合併起來只記錄最後一次數據,能夠經過enableCoalescing()
來啓用合併
文中或許會存在或多或少的不足、錯誤之處,有建議或者意見也很是歡迎你們在評論交流。