基於Spring的Web緩存

緩存的基本思想實際上是以空間換時間。咱們知道,IO的讀寫速度相對內存來講是很是比較慢的,一般一個web應用的瓶頸就出如今磁盤IO的讀寫上。那麼,若是咱們在內存中創建一個存儲區,將數據緩存起來,當瀏覽器端由請求到達的時候,直接從內存中獲取相應的數據,這樣一來能夠下降服務器的壓力,二來,能夠提升請求的響應速度,提高用戶體驗。java

緩存的分類

  • 數據庫數據緩存

通常來講,web應用業務邏輯業務邏輯比較複雜,數據庫繁多,要獲取某個完整的數據,每每要屢次讀取數據庫,或者使用極其複雜效率較低的SQL查詢語句。爲了提升查詢的性能,將查詢後的數據放到內存中進行緩存,下次查詢時,直接從內存緩存直接返回,提升響應效率。mysql

  • 應用層緩存

應用層緩存主要針對某個業務方法進行緩存,有些業務對象邏輯比較複雜,,可能涉及到屢次數據庫讀寫或者其餘消耗較高的操做,應用層緩存能夠將複雜的業務邏輯解放出來,下降服務器壓力。web

  • 頁面緩存

除了IO外,web應用的另外一大瓶頸就是頁面模板的渲染。每次請求都須要從業務邏輯層獲取相應的model,並將其渲染成對應的HTML。通常來講,web應用讀取數據的需求比更新數據的需求大不少,大多數狀況下,某個請求返回的HTML是同樣的,所以直接將HTML緩存起來也是緩存的一個主流作法。spring

  • 代理服務器緩存

代理服務器是瀏覽器和源服務器之間的中間服務器,瀏覽器先向這個中間服務器發起Web請求,通過處理後(好比權限驗證,緩存匹配等),再將請求轉發到源服務器。代理服務器緩存的運做原理跟瀏覽器的運做原理差很少,只是規模更大。能夠把它理解爲一個共享緩存,不僅爲一個用戶服務,通常爲大量用戶提供服務,所以在減小相應時間和帶寬使用方面頗有效,同一個副本會被重用屢次。sql

  • 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的數據庫配置java-web

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實現來看,在這一層須要咱們配置的東西愈來愈多,已經有很大可能出現數據不一致的現象。而頁面緩存的複雜性相對服務層緩存又高了一個層級,所以在針對緩存進行設計的時候,不只僅考慮緩存所帶來的性能提高,還要考慮到更新緩存所帶來的性能損失。並且在實踐當中,不是數據層緩存、服務層緩存、頁面緩存越多越好,須要根據實際狀況作出選擇。

相關文章
相關標籤/搜索