緩存的基本思想實際上是以空間換時間。咱們知道,IO的讀寫速度相對內存來講是很是比較慢的,一般一個web應用的瓶頸就出如今磁盤IO的讀寫上。那麼,若是咱們在內存中創建一個存儲區,將數據緩存起來,當瀏覽器端由請求到達的時候,直接從內存中獲取相應的數據,這樣一來能夠下降服務器的壓力,二來,能夠提升請求的響應速度,提高用戶體驗。java
通常來講,web應用業務邏輯業務邏輯比較複雜,數據庫繁多,要獲取某個完整的數據,每每要屢次讀取數據庫,或者使用極其複雜效率較低的SQL查詢語句。爲了提升查詢的性能,將查詢後的數據放到內存中進行緩存,下次查詢時,直接從內存緩存直接返回,提升響應效率。mysql
應用層緩存主要針對某個業務方法進行緩存,有些業務對象邏輯比較複雜,,可能涉及到屢次數據庫讀寫或者其餘消耗較高的操做,應用層緩存能夠將複雜的業務邏輯解放出來,下降服務器壓力。web
除了IO外,web應用的另外一大瓶頸就是頁面模板的渲染。每次請求都須要從業務邏輯層獲取相應的model,並將其渲染成對應的HTML。通常來講,web應用讀取數據的需求比更新數據的需求大不少,大多數狀況下,某個請求返回的HTML是同樣的,所以直接將HTML緩存起來也是緩存的一個主流作法。spring
代理服務器是瀏覽器和源服務器之間的中間服務器,瀏覽器先向這個中間服務器發起Web請求,通過處理後(好比權限驗證,緩存匹配等),再將請求轉發到源服務器。代理服務器緩存的運做原理跟瀏覽器的運做原理差很少,只是規模更大。能夠把它理解爲一個共享緩存,不僅爲一個用戶服務,通常爲大量用戶提供服務,所以在減小相應時間和帶寬使用方面頗有效,同一個副本會被重用屢次。sql
CDN( Content delivery networks )緩存,也叫網關緩存、反向代理緩存。瀏覽器先向CDN網關發起Web請求,網關服務器後面對應着一臺或多臺負載均衡源服務器,會根據它們的負載請求,動態將請求轉發到合適的源服務器上。雖然這種架構負載均衡源服務器之間的緩存無法共享,但卻擁有更好的處擴展性。數據庫
spring做爲一個成熟的java web 框架,自身有一套完善的緩存機制,同時,spring還未其餘緩存的實現提供了擴展。接下來,讓咱們在一個簡單的學生管理系統中嘗試spring的數據庫緩存、應用層緩存、頁面緩存的實現。瀏覽器
本節課咱們來看看一個簡單的學生管理系統,改系統使用了Spring+JPA+EhCache的架構對數據庫進行了緩存。你們能夠直接下載源碼進行學習。緩存
測試程序使用了mysql做爲數據庫,安裝好mysql後,創建一個空白的 數據庫,例如cache
。服務器
建好數據庫後,修改src/main/resources/application.properties
的數據庫配置java-web
spring.datasource.url=jdbc:mysql://localhost/cache?useUnicode=true&characterEncoding=utf8 spring.datasource.username=root spring.datasource.password=
該系統利用maven做爲構建工具,若是對maven沒有了解的同窗能夠自行了解一下,咱們會利用maven進行整個項目的構建以及運行。所以須要你們下載安裝maven。
安裝完成後,打開命令行,進入程序所在目錄,輸入如下命令:
打開瀏覽器,訪問如下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是目前主流java的構建工具之一,若是對maven沒有了解的同窗能夠自行了解一下,接下來咱們會利用maven進行整個項目的構建以及運行。
spring boot是spring的一個子項目,其目的是spring應用的初始搭建以及開發過程,若是你想本身搭建一個基於spring的應用,強烈建議學習一下在《java web 全棧開發》這門課程,教你如何從對spring零基礎到搭建好一個完整的spring web應用。這裏,咱們只需知道mvn spring-boot:run
命令能夠將系統run起來便可。
Spring做爲目前主流的java web框架,你們應該都很瞭解,這裏不作過多介紹。
JPA全稱Java Persistence API,JPA經過JDK 5.0註解或XML描述對象-關係表的映射關係,並將運行期的實體對象持久化到數據庫中。本門課程主要講基於spring的數據庫緩存,對於JPA的內容不作過多的涉及。
EhCache 是一個純Java的進程內緩存框架,具備快速、精幹等特色。咱們的學生管理系統將利用EhCache對數據庫層進行緩存。
上一節咱們講到不少技術,這裏咱們主要的依賴是指對EhCache的依賴,須要在Spring項目中引入EhCache,在pom.xml
中加入如下代碼便可:
<dependency> <groupId>org.hibernate</groupId> <artifactId>hibernate-ehcache</artifactId> </dependency>
在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能夠對如下參數進行配置:
緩存名稱
內存中最大緩存對象數
硬盤中最大緩存對象數,如果0表示無窮大
true表示對象永不過時,此時會忽略timeToIdleSeconds和timeToLiveSeconds屬性,默認爲false
true表示當內存緩存的對象數目達到了maxElementsInMemory界限後,會把溢出的對象寫到硬盤緩存中。注意:若是緩存的對象要寫入到硬盤中的話,則該對象必須實現了Serializable接口才行。
磁盤緩存區大小,默認爲30MB。每一個Cache都應該有本身的一個緩存區。
是否緩存虛擬機重啓期數據
磁盤失效線程運行時間間隔,默認爲120秒
設定容許對象處於空閒狀態的最長時間,以秒爲單位。當對象自從最近一次被訪問後,若是處於空閒狀態的時間超過了timeToIdleSeconds屬性值,這個對象就會過時,EHCache將把它從緩存中清空。只有當eternal屬性爲false,該屬性纔有效。若是該屬性值爲0,則表示對象能夠無限期地處於空閒狀態
設定對象容許存在於緩存中的最長時間,以秒爲單位。當對象自從被存放到緩存中後,若是處於緩存中的時間超過了 timeToLiveSeconds屬性值,這個對象就會過時,EHCache將把它從緩存中清除。只有當eternal屬性爲false,該屬性纔有效。若是該屬性值爲0,則表示對象能夠無限期地存在於緩存中。timeToLiveSeconds必須大於timeToIdleSeconds屬性,纔有意義
當達到maxElementsInMemory限制時,Ehcache將會根據指定的策略去清理內存。可選策略有:LRU(最近最少使用,默認策略)、FIFO(先進先出)、LFU(最少訪問次數)。
首先,咱們要經過@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中設置一塊區域來存放緩存,在src/main/resources/ehcache.xml
中添加以下配置:
Hibernate提供了兩級緩存,第一級是Session的緩存。因爲Session對象的生命週期一般對應一個數據庫事務或者一個應用事務,所以它的緩存是事務範圍的緩存。第一級緩存是必需的,hibernate會默認提供好。
第二級緩存是一個可插拔的的緩存插件,它是由SessionFactory負責管理。因爲SessionFactory對象的生命週期和應用程序的整個過程對應,所以第二級緩存是進程範圍或者集羣範圍的緩存。這個緩存中存放的對象的鬆散數據第二級緩存是可選的,能夠在每一個類或每一個集合的粒度上配置第二級緩存。
咱們能夠經過爲entry對象添加標註的方式打開二級緩存:
二級緩存一共有如下5種策略:
不使用緩存,默認的緩存策略
只讀模式,在此模式下,若是對數據進行更新操做,會有異常
讀寫模式在更新緩存的時候會把緩存裏面的數據換成一個鎖,其它事務若是去取相應的緩存數據,發現被鎖了,直接就去數據庫查詢
不嚴格的讀寫模式則不會的緩存數據加鎖
事務模式指緩存支持事務,當事務回滾時,緩存也能回滾
而後,在src/main/resources/application.properties
中爲cache指定一個factory:
第一次訪問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 提供了一套標註來保住咱們快速的實現緩存系統:
@Cacheable
觸發添加緩存的方法@CacheEvict
觸發刪除緩存的方法@CachePut
在不干涉方法執行的狀況下更新緩存@Caching
組織多個緩存標註的標註@CacheConfig
在class的層次共享緩存的設置接下來咱們來看緩存的具體實現。
和數據層緩存同樣,須要在內存中設置一塊區域來存放service的緩存,在src/main/resources/ehcache.xml
中添加以下配置:
首先,在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
能夠幹這件事情:
若是你想在一個方法中同時對緩存作多種操做,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); }
在屢次訪問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
,咱們發現博客列表從新變爲正確的列表,同時服務器響應時間變成和沒有作緩存時一致:
一樣,第一件事情讓咱們添加一下緩存的空間:
ehcache爲咱們提供了幾個緩存頁面的filter,使用這些filter實現緩存:
最基本的頁面緩存filter實現,其知足大部分頁面緩存的需求,該filter只緩存頁面,不會修改herder的 ETag、Last-Modified、Expires屬性
當response沒有提交時寫入緩存,不然不寫緩存,該緩存可能致使空白頁的錯誤,須要特別注意!
專門針對那些不獨立存在,只是被include到其餘頁面的頁面緩存
SimplePageCachingFilter的擴展,會填寫herder的 ETag、Last-Modified、Expires屬性,能夠進一步減小瀏覽器的訪問次數
以上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 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
實現來看,在這一層須要咱們配置的東西愈來愈多,已經有很大可能出現數據不一致的現象。而頁面緩存的複雜性相對服務層緩存又高了一個層級,所以在針對緩存進行設計的時候,不只僅考慮緩存所帶來的性能提高,還要考慮到更新緩存所帶來的性能損失。並且在實踐當中,不是數據層緩存、服務層緩存、頁面緩存越多越好,須要根據實際狀況作出選擇。