javascript
基于Spring的Web缓存
緩存的基本思想其實是以空間換時間。我們知道,IO的讀寫速度相對內存來說是非常比較慢的,通常一個web應用的瓶頸就出現在磁盤IO的讀寫上。那么,如果我們在內存中建立一個存儲區,將數據緩存起來,當瀏覽器端由請求到達的時候,直接從內存中獲取相應的數據,這樣一來可以降低服務器的壓力,二來,可以提高請求的響應速度,提升用戶體驗。
緩存的分類
- 數據庫數據緩存
一般來說,web應用業務邏輯業務邏輯比較復雜,數據庫繁多,要獲取某個完整的數據,往往要多次讀取數據庫,或者使用極其復雜效率較低的SQL查詢語句。為了提高查詢的性能,將查詢后的數據放到內存中進行緩存,下次查詢時,直接從內存緩存直接返回,提高響應效率。
- 應用層緩存
應用層緩存主要針對某個業務方法進行緩存,有些業務對象邏輯比較復雜,,可能涉及到多次數據庫讀寫或者其他消耗較高的操作,應用層緩存可以將復雜的業務邏輯解放出來,降低服務器壓力。
- 頁面緩存
除了IO外,web應用的另一大瓶頸就是頁面模板的渲染。每次請求都需要從業務邏輯層獲取相應的model,并將其渲染成對應的HTML。一般來說,web應用讀取數據的需求比更新數據的需求大很多,大多數情況下,某個請求返回的HTML是一樣的,因此直接將HTML緩存起來也是緩存的一個主流做法。
- 代理服務器緩存
代理服務器是瀏覽器和源服務器之間的中間服務器,瀏覽器先向這個中間服務器發起Web請求,經過處理后(比如權限驗證,緩存匹配等),再將請求轉發到源服務器。代理服務器緩存的運作原理跟瀏覽器的運作原理差不多,只是規模更大。可以把它理解為一個共享緩存,不只為一個用戶服務,一般為大量用戶提供服務,因此在減少相應時間和帶寬使用方面很有效,同一個副本會被重用多次。
- CDN緩存
CDN( Content delivery networks )緩存,也叫網關緩存、反向代理緩存。瀏覽器先向CDN網關發起Web請求,網關服務器后面對應著一臺或多臺負載均衡源服務器,會根據它們的負載請求,動態將請求轉發到合適的源服務器上。雖然這種架構負載均衡源服務器之間的緩存沒法共享,但卻擁有更好的處擴展性。
基于spring的緩存
spring作為一個成熟的java web 框架,自身有一套完善的緩存機制,同時,spring還未其他緩存的實現提供了擴展。接下來,讓我們在一個簡單的學生管理系統中嘗試spring的數據庫緩存、應用層緩存、頁面緩存的實現。
基于spring的Web緩存
源程序簡介
本節課我們來看看一個簡單的學生管理系統,改系統使用了Spring+JPA+EhCache的架構對數據庫進行了緩存。大家可以直接下載源碼進行學習。
數據庫準備
測試程序使用了mysql作為數據庫,安裝好mysql后,建立一個空白的 數據庫,例如cache。
建好數據庫后,修改src/main/resources/application.properties的數據庫配置
spring.datasource.url=jdbc:mysql://localhost/cache?useUnicode=true&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=利用maven啟動程序
該系統利用maven作為構建工具,如果對maven沒有了解的同學可以自行了解一下,我們會利用maven進行整個項目的構建以及運行。因此需要大家下載安裝maven。
安裝完成后,打開命令行,進入程序所在目錄,輸入以下命令:
mvn spring-boot:run打開瀏覽器,訪問以下http://localhost:8111/blogs即可看到最初的博客列表頁面
直接運行?com.tmy.App.java
如果你成功的將項目作為一個maven項目導入進eclipse,直接運行com.tmy.App.java也可以將項目啟動起來。
注意,如果希望將項目導入進eclipse,需要為eclipse添加maven插件,否則會出現依賴的類找不到的問題。
頁面列表
以下是程序所提供的所有頁面以及相關說明:
http://localhost:8111/blogs //沒有加緩存的博客列表頁面 http://localhost:8111/blogs/dao //添加了數據層緩存 http://localhost:8111/blogs/service?test=test //添加了服務層緩存 http://localhost:8111/blogs/service/update?test=test //更新服務層緩存 http://localhost:8111/blogs/service/evict?test=test //刪除服務層緩存 http://localhost:8111/blogs/service/test?test=test //刪除服務層緩存的同時更新緩存 http://localhost:8111/blogs/page //添加了頁面緩存 http://localhost:8111/blogs/page/update //清空頁面緩存 http://localhost:8111/blogs/page/delete //清空頁面緩存涉及到的技術
- maven
maven是目前主流java的構建工具之一,如果對maven沒有了解的同學可以自行了解一下,接下來我們會利用maven進行整個項目的構建以及運行。
- spring boot
spring boot是spring的一個子項目,其目的是spring應用的初始搭建以及開發過程,如果你想自己搭建一個基于spring的應用,強烈建議學習一下在《java web 全棧開發》這門課程,教你如何從對spring零基礎到搭建好一個完整的spring web應用。這里,我們只需知道mvn spring-boot:run命令可以將系統run起來即可。
- Spring
Spring作為目前主流的java web框架,大家應該都很了解,這里不做過多介紹。
- JPA
JPA全稱Java Persistence API,JPA通過JDK 5.0注解或XML描述對象-關系表的映射關系,并將運行期的實體對象持久化到數據庫中。本門課程主要講基于spring的數據庫緩存,對于JPA的內容不做過多的涉及。
- EhCache
EhCache 是一個純Java的進程內緩存框架,具有快速、精干等特點。我們的學生管理系統將利用EhCache對數據庫層進行緩存。
配置EhCache
對EhCache的依賴
上一節我們講到很多技術,這里我們主要的依賴是指對EhCache的依賴,需要在Spring項目中引入EhCache,在pom.xml中加入以下代碼即可:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-ehcache</artifactId> </dependency>配置CacheManager
添加ehcache配置文件
在src/main/resources下添加文件ehcache.xml:
<?xml version="1.0" encoding="UTF-8"?> <ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" name="CM1" updateCheck="false" maxBytesLocalHeap="16M"> <diskStore path="/data/app/cache/ehcache"/> <defaultCache eternal="false" overflowToDisk="false" maxElementsInMemory="10000" timeToIdleSeconds="3600" timeToLiveSeconds="36000" /> </ehcache>encache可以對以下參數進行配置:
- name
緩存名稱
- maxElementsInMemory
內存中最大緩存對象數
- maxElementsOnDisk
硬盤中最大緩存對象數,若是0表示無窮大
- eternal
true表示對象永不過期,此時會忽略timeToIdleSeconds和timeToLiveSeconds屬性,默認為false
- overflowToDisk
true表示當內存緩存的對象數目達到了maxElementsInMemory界限后,會把溢出的對象寫到硬盤緩存中。注意:如果緩存的對象要寫入到硬盤中的話,則該對象必須實現了Serializable接口才行。
- diskSpoolBufferSizeMB
磁盤緩存區大小,默認為30MB。每個Cache都應該有自己的一個緩存區。
- diskPersistent
是否緩存虛擬機重啟期數據
- diskExpiryThreadIntervalSeconds
磁盤失效線程運行時間間隔,默認為120秒
- timeToIdleSeconds
設定允許對象處于空閑狀態的最長時間,以秒為單位。當對象自從最近一次被訪問后,如果處于空閑狀態的時間超過了timeToIdleSeconds屬性值,這個對象就會過期,EHCache將把它從緩存中清空。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地處于空閑狀態
- timeToLiveSeconds
設定對象允許存在于緩存中的最長時間,以秒為單位。當對象自從被存放到緩存中后,如果處于緩存中的時間超過了 timeToLiveSeconds屬性值,這個對象就會過期,EHCache將把它從緩存中清除。只有當eternal屬性為false,該屬性才有效。如果該屬性值為0,則表示對象可以無限期地存在于緩存中。timeToLiveSeconds必須大于timeToIdleSeconds屬性,才有意義
- memoryStoreEvictionPolicy
當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。可選策略有:LRU(最近最少使用,默認策略)、FIFO(先進先出)、LFU(最少訪問次數)。
添加cacheManager
首先,我們要通過@EnableCaching標注將Spring通過標注進行緩存管理的功能打開,以方便我們之后通過標注添加數據庫緩存。
然后,為CacheConfiguration添加@Configuration標注,打開CacheConfiguration內@Bean的功能。
生成一個CacheManager的實例。
最后,在web app銷毀的時候銷毀cacheManager。
@Configuration @EnableCaching public class CacheConfiguration { private net.sf.ehcache.CacheManager cacheManager; @PreDestroy public void destroy() { cacheManager.shutdown(); } @Bean public CacheManager cacheManager() { cacheManager = net.sf.ehcache.CacheManager.create(); EhCacheCacheManager ehCacheManager = new EhCacheCacheManager(); ehCacheManager.setCacheManager(cacheManager); return ehCacheManager; } }數據層緩存實現
添加ehcache設置
首先,我們需要在EhCache中設置一塊區域來存放緩存,在src/main/resources/ehcache.xml中添加如下配置:
<cache name="com.tmy.model.User"></cache> <cache name="com.tmy.model.Blog"></cache>Hibernate的一級緩存和二級緩存
Hibernate提供了兩級緩存,第一級是Session的緩存。由于Session對象的生命周期通常對應一個數據庫事務或者一個應用事務,因此它的緩存是事務范圍的緩存。第一級緩存是必需的,hibernate會默認提供好。
第二級緩存是一個可插拔的的緩存插件,它是由SessionFactory負責管理。由于SessionFactory對象的生命周期和應用程序的整個過程對應,因此第二級緩存是進程范圍或者集群范圍的緩存。這個緩存中存放的對象的松散數據第二級緩存是可選的,可以在每個類或每個集合的粒度上配置第二級緩存。
打開二級緩存
我們可以通過為entry對象添加標注的方式打開二級緩存:
@org.hibernate.annotations.Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)二級緩存一共有以下5種策略:
- CacheConcurrencyStrategy.NONE
不使用緩存,默認的緩存策略
- CacheConcurrencyStrategy.READ_ONLY
只讀模式,在此模式下,如果對數據進行更新操作,會有異常
- CacheConcurrencyStrategy.READ_WRITE
讀寫模式在更新緩存的時候會把緩存里面的數據換成一個鎖,其它事務如果去取相應的緩存數據,發現被鎖了,直接就去數據庫查詢
- CacheConcurrencyStrategy.NONSTRICT_READ_WRITE
不嚴格的讀寫模式則不會的緩存數據加鎖
- CacheConcurrencyStrategy.TRANSACTIONAL
事務模式指緩存支持事務,當事務回滾時,緩存也能回滾
指定cache region factory
然后,在src/main/resources/application.properties中為cache指定一個factory:
spring.jpa.properties. =org.hibernate.cache.ehcache.SingletonEhCacheRegionFactory性能對比
第一次訪問
第一次訪問http://localhost:8111/blogs時,waiting也就是服務器響應的時間為2.82秒,耗時較多。
注意:這里消耗2.82秒的原因是:在Blog對象中添加了對成員creator添加了@ManyToOne的標注,因此,當通過JPA獲取blog對象后,JPA還會請求一次SQL查詢,去user表中獲取user信息,將user填充進來,而為了效果更加明顯,系統在添加測試數據時為每個blog都添加了不同的user,導致sql請求大大增加,處理時間也大大增加
多次訪問未緩存頁面
多次訪問http://localhost:8111/blogs后,服務器響應時間大大減少,基本保持在700毫秒左右:
這是因為mysql實際上幫我們做了緩存的工作,因此,多次訪問后,服務器響應時間會大大減少。如果大家有興趣,可以自行搜索mysql緩存相關的內容。
多次訪問已緩存頁面
那么,在多次訪問http://localhost:8111/blogs/dao后,訪問時間基本保持在100多毫秒,比沒有緩存的頁面效率高了5倍左右,比第一次訪問效率高了20倍以上。
服務層緩存實現
Spring緩存的相關標注
Spring 提供了一套標注來保住我們快速的實現緩存系統:
- @Cacheable?觸發添加緩存的方法
- @CacheEvict?觸發刪除緩存的方法
- @CachePut?在不干涉方法執行的情況下更新緩存
- @Caching?組織多個緩存標注的標注
- @CacheConfig?在class的層次共享緩存的設置
接下來我們來看緩存的具體實現。
添加ehcache設置
和數據層緩存一樣,需要在內存中設置一塊區域來存放service的緩存,在src/main/resources/ehcache.xml中添加如下配置:
<cache name="com.tmy.service.allBlogs"></cache>為某個方法添加緩存
首先,在BlogWithCacheService上添加@CacheConfig(cacheNames = "com.tmy.service.allBlogs")標注,表明在BlogWithCacheService中的方法的緩存都是放在com.tmy.service.allBlogs區域中。
在需要緩存的方法上添加@Cacheable標注:
@Cacheable(key = "#justTest") public List<BlogWithoutCache> findAll(String justTest){ return blogRepository.findAll(); }當第一次調用該方法后,其返回值就會添加進緩存當中,當第二次調用時就能直接從緩存中獲取對象了。為了測試緩存功能,我們為findAll方法添加了一個參數,這里我們將這個參數作為緩存的key。除了用參數之外,Spring還提供了其他解析方式來生成key:
- 被調用方法的名稱?#root.methodName
- 被調用的方法?#root.method.name
- 被調用的目標對象?#root.target
- 被調用的對象的類名?#root.targetClass
- 被調用方法的參數?#root.args[0]
- 被調用方法所用的緩存?#root.caches[0].name
- 被調用方法的參數名?#arg
- 調用后的結果(該參數只在unless參數或者@CachePut標注中才能使用)?#result
更新緩存
添加進緩存后,在update方法中添加@CachePut標注可以更新相應的緩存,同樣,我們還是使用傳進來的參數來更新相應的緩存:
@CachePut(key = "#justTest") public List<BlogWithoutCache> updateAll(String justTest){ BlogWithoutCache blog = new BlogWithoutCache(); blog.setContent("這是不存在的博客"); blog.setTitle("謹慎使用這個方法"); return Lists.newArrayList(blog); }刪除緩存
在某些情況下,我們還需要刪除緩存,@CacheEvict可以干這件事情:
@CacheEvict(key = "#justTest") public void evictAll(String justTest){ }組織多種緩存操作
如果你想在一個方法中同時對緩存做多種操作,Spring支持使用@Caching來組織這些操作:
@Caching(evict = @CacheEvict(key="#justTest"), put = @CachePut(key="test")) public List<BlogWithoutCache> testForCaching(String justTest){ BlogWithoutCache blog = new BlogWithoutCache(); blog.setContent("這是不存在的博客"); blog.setTitle("謹慎使用這個方法"); return Lists.newArrayList(blog); }性能對比
多次訪問service層緩存頁面
在多次訪問http://localhost:8111/blogs/service?test=test后,服務器的訪問時間基本保持在100毫秒以下,根據上次實驗可以發現,其效率甚至比加了數據層緩存后還要高。
更新緩存
更新緩存前,訪問http://localhost:8111/blogs/service?test=test頁面,看下以下博客:
訪問http://localhost:8111/blogs/service/update?test=test更新緩存,再次訪問http://localhost:8111/blogs/service?test=test,將發現數據庫沒有變化,但是返回的博客列表發生了變化:
刪除緩存
現在緩存對象已經被玩壞了,讓我們訪問http://localhost:8111/blogs/service/evict?test=test緩存的對象給刪掉,再次訪問http://localhost:8111/blogs/service/update?test=test,我們發現博客列表重新變為正確的列表,同時服務器響應時間變成和沒有做緩存時一致:
頁面緩存的實現
添加緩存空間
同樣,第一件事情讓我們添加一下緩存的空間:
<cache name="com.tmy.mapper.allBlogs"></cache>PageCachingFilter
ehcache為我們提供了幾個緩存頁面的filter,使用這些filter實現緩存:
- SimplePageCachingFilter
最基本的頁面緩存filter實現,其滿足大部分頁面緩存的需求,該filter只緩存頁面,不會修改herder的 ETag、Last-Modified、Expires屬性
- SimplePageCachingFilterWithBlankPageProblem
當response沒有提交時寫入緩存,否則不寫緩存,該緩存可能導致空白頁的錯誤,需要特別注意!
- SimplePageFragmentCachingFilter
專門針對那些不獨立存在,只是被include到其他頁面的頁面緩存
- SimpleCachingHeadersPageCachingFilter
SimplePageCachingFilter的擴展,會填寫herder的 ETag、Last-Modified、Expires屬性,可以進一步減少瀏覽器的訪問次數
自定義的PageCachingFilter
以上filter會在filter初始化的時候通過FilterConfig對緩存進行初始化,為了在SpringBoot中方便的通過注解去實例化這些Filter,我們將CacheName的獲取做一個定制:
public class CustomPageCachingFilter extends SimpleCachingHeadersPageCachingFilter { private final String customCacheName; public CustomPageCachingFilter(String name){ this.customCacheName = name; } @Override protected String getCacheName() { return customCacheName; } }這樣,我們就能很方便的注入cacheName了。
EhCache只提供了添加緩存的Filter,但是并沒有提供刪除緩存的Filter,沒關系,讓我們來自己實現一個:
public class ClearPageCachingFilter implements Filter { private final CacheManager cacheManager; private final String customCacheName; public ClearPageCachingFilter(String name){ this.customCacheName = name; cacheManager = CacheManager.getInstance(); assert cacheManager != null; } @Override public void init(FilterConfig filterConfig) throws ServletException {} @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { Ehcache ehcache = cacheManager.getEhcache(customCacheName); ehcache.removeAll(); } @Override public void destroy() {} }現實情況URL的設計是極其復雜的,我們在這里就簡單粗暴的將所有cache直接刪除,如果緩存設計的比較好,最好可以通過ehcache.remove(key);的方式對cache進行管理。
添加Filter
我們目前使用標注的方式對Filter以及Filter mapping進行管理,目前我們只緩存/blogs/page這一個頁面:
@Configuration @AutoConfigureAfter(CacheConfiguration.class) public class PageCacheConfiguration { @Bean public FilterRegistrationBean registerBlogsPageFilter(){ CustomPageCachingFilter customPageCachingFilter = new CustomPageCachingFilter("com.tmy.mapper.allBlogs"); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(customPageCachingFilter); filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/blogs/page")); return filterRegistrationBean; } @Bean public FilterRegistrationBean registerClearBlogsPageFilter(){ ClearPageCachingFilter clearPageCachingFilter = new ClearPageCachingFilter("com.tmy.mapper.allBlogs"); FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(clearPageCachingFilter); filterRegistrationBean.setUrlPatterns(Lists.newArrayList("/blogs/page/update", "/blogs/page/delete")); return filterRegistrationBean; } }從以上配置可以看出,我們為/blogs/page注冊了一個添加緩存的Filter,/blogs/page請求將被緩存到內存當中。同時,為/blogs/page/update以及/blogs/page/delete注冊了清空緩存的Filter,當訪問這兩個url時,將清空所有的緩存。
性能對比
訪問被緩存的頁面
訪問http://localhost:8111/blogs/page,刷新,我們可以看到,服務器的響應時間只需要4毫秒,是mysql緩存、數據層緩存、服務層緩存當中最好的。
清空緩存后第一次訪問
我們可以將以上幾種緩存結合起來一起使用,http://localhost:8111/blogs/page,該請求已經結合了以上三種緩存的實現。因此,當我們訪問http://localhost:8111/blogs/page/update清空頁面緩存時,再次訪問http://localhost:8111/blogs/page也只需要100多毫秒,此時頁面緩存沒有命中,但是service層緩存命中。
總結
就實踐看來,數據層緩存、服務層緩存、頁面緩存一層比一層更加高效,但是由于其實現越來越復雜,需要考慮的情況也越來越多,因此,其設計也越來越復雜。
從服務層緩存的實現@CachePut實現來看,在這一層需要我們配置的東西越來越多,已經有很大可能出現數據不一致的現象。而頁面緩存的復雜性相對服務層緩存又高了一個層級,因此在針對緩存進行設計的時候,不僅僅考慮緩存所帶來的性能提升,還要考慮到更新緩存所帶來的性能損失。而且在實踐當中,不是數據層緩存、服務層緩存、頁面緩存越多越好,需要根據實際情況做出選擇。
轉載于:https://www.cnblogs.com/duyinqiang/p/5696473.html
總結
以上是生活随笔為你收集整理的基于Spring的Web缓存的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 关于sizeof表达式作为数组元素个数的
- 下一篇: 【Spring】Spring学习笔记-0