javascript
Spring Cache-缓存概述及使用
- 概述
- 緩存的概念
- 緩存命中率
- 過期策略
- Spring Cache的好處
- Spring Cache的缺點
- 自定義緩存管理器粗略實現
- 使用Spring Cache
- 示例源碼
概述
伴隨信息量的爆炸式增長以及構建的應用系統越來越多樣化、復雜化,特別是企業級應用互聯網化的趨勢,緩存(Cache)對應用程序性能的優化變的越來越重要。 將所需服務請求的數據放在緩存中,既可以提高應用程序的訪問效率,又可以減少數據庫服務器的壓力,從而讓用戶獲得更好的用戶體驗。
Spring從3.1開始,以一貫的優雅風格提供了一種透明的緩存解決方案,這使得Spring可以在后臺使用不同的緩存框架(如EhCache,GemFire、HazelCast和Guava)時保持編程的一致。
Spring從4.0開始則全面支持JSR-107 annotations和自定義的緩存標簽。
緩存的概念
我們可以將緩存定義為一種存儲機制,它將數據保存在某個地方,并以一種更快的方式提供服務。
要理解緩存,我們先了解下基本概念
緩存命中率
即從緩存中讀取數據的次數與總讀取次數的比率。 一般來講,命中率越高也好。
命中率 = 從緩存中讀取的次數 / (總讀取次數[從緩存中讀取的次數+從慢速設備上讀取的次數]) Miss率 = 沒從緩存中讀取的次數/ (總讀取次數[從緩存中讀取的次數+從慢速設備上讀取的次數])這是一個非常重要的監控指標,如果要做緩存,就一定要監控這個指標,來看緩存是否工作良好。
過期策略
即如果緩存滿了,從緩存中移除數據的策略,常見的有 LFU 、LRU、FIFO
FIFO (First in First Out) 先進先出策略,即先放入緩存的數據先被移除
LRU (Least Recently Used) 最久未使用策略, 即使用時間距離現在最久的那個數據被移除
LFU (Leaset Frequently Used) 最近最少使用策略,即一定時間內使用次數(頻率)最少的那個數據被移除
TTL(Time To Live)存活期,即從緩存中創建時間點開始至到期的一個時間段(不管在這個時間段內有沒被訪問過都將過期)
TTI (Time To Idle)空閑期,即一個數據多久沒有被訪問就從緩存中移除的時間。
至此,我們基本了解了緩存的一些基本知識。 在Java中一般會對調用方法進行緩存控制,比如 findUserById(Sting id),先從緩存中查找有沒有符合查詢條件的數據,如果沒有,則執行改方法從數據庫中查找該用戶,然后添加到緩存中,下次調用時將從緩存中獲取。
從Spring3.1開始,提供了緩存注解,并且提供了Cache層的抽象。 此外,JSR-107也從Spring4.0開始得到全面支持。
Spring提供可一種可以在方法級別進行緩存的緩存抽象。 通過使用AOP對方法機型織入,如果已經為特定方法入參執行過該方法,那么不必執行實際方法就可以返回被緩存的結果。
為了啟用AOP緩存功能,需要使用緩存注解對類中的相關方法進行標記,以便Spring為其生成具備緩存功能的代理類。 需要注意的是,Spring Cache僅提供了一種抽象而未提供具體的實現。 我們以便會自己使用AIP來做一定程度的封裝實現。
Spring Cache的好處
支持開箱即用(Out Of The Box),并提供基本的Cache抽象,方便切換各種底層Cache
通過Cache注解即可實現緩存邏輯透明化,讓開發者關注業務邏輯
當事務回滾時,緩存也會自動回滾
支持比較復雜的緩存邏輯
提供緩存編程的一致性抽象,方便代碼維護。
Spring Cache的缺點
Spring Cache并不針對多進程的應用環境進行專門的處理。
另外Spring Cache抽象的操作中沒有鎖的概念,當多線程并發操作(更新或者刪除)同一個緩存項時,有可能讀取到過期的數據。
自定義緩存管理器(粗略實現)
我們首先自定義一個緩存的實現,即不通過任何第三方組件來實現的對象內存緩存, 然后我們再通過Spring Cache來實現緩存操作,對比體會下SpringCache的優雅和便捷。
假設:我們根據artisanName查詢artisan信息是一個非常頻繁的動作,自然會想到對一個artisan的查詢方法做緩存,以避免頻繁的數據庫訪問操作,提高頁面的相應速度。
通常的做法是:以artisanName作為Key,以返回的用戶信息對象作為Value值存儲。 而當以相同的artisanName查詢用戶時,程序將直接從緩存中獲取結果并返回,否則更新緩存。
首先定義一個實體列 LittleArtisan
package com.xgj.cache.selfCacheManagerDemo.domain;import java.io.Serializable;/*** * * @ClassName: LittleArtisan* * @Description: Java中的緩存和序列化是息息相關的,注意實現Serializable接口* * @author: Mr.Yang* * @date: 2017年10月2日 下午1:40:53*/public class LittleArtisan implements Serializable {private static final long serialVersionUID = 1L;private String artisanId;private String artisanName;private String artisanDesc;public String getArtisanId() {return artisanId;}public void setArtisanId(String artisanId) {this.artisanId = artisanId;}public String getArtisanName() {return artisanName;}public void setArtisanName(String artisanName) {this.artisanName = artisanName;}public String getArtisanDesc() {return artisanDesc;}public void setArtisanDesc(String artisanDesc) {this.artisanDesc = artisanDesc;}public static long getSerialversionuid() {return serialVersionUID;}}java對象的緩存和序列化是息息相關的,一般情況下,需要被緩存的實體類需要實現Serializable接口 。 只有實現了Serializable接口的類,JVM才可以對其對象進行序列化。
實體類始終實現Serializable接口是一個良好的變成習慣。 實現Serializable接口的實體類,一般需要聲明一個serialVersionUID成員變量,以表明該實體類的版本,如果實體類的接口繁盛變化,則可以修改serialVersionUID的值以支持反序列化工作。
接下來定義一個緩存管理器,該管理器負責實現緩存邏輯,支持對象的增加、修改和刪除,并且支持值對象的泛型。
package com.xgj.cache.selfCacheManagerDemo;import java.util.Map; import java.util.concurrent.ConcurrentHashMap;/*** * * @ClassName: CacheManager* * @Description: 泛型類-自定義緩存管理器的粗略實現* * @author: Mr.Yang* * @date: 2017年10月2日 下午1:10:13*/public class CacheManager<T> {/*** ConcurrentHashMap - 線程安全的集合容器*/Map<Object, T> cacheMap = new ConcurrentHashMap<Object, T>();/*** * * @Title: getValue* * @Description: 根據Key獲取緩存數據* * @param key* @return* * @return: T*/public T getValue(Object key) {return cacheMap.get(key);}/*** * * @Title: addOrUpdateCache* * @Description: 添加或者更新緩存* * @param key* @param value* * @return: void*/public void addOrUpdateCache(Object key, T value) {cacheMap.put(key, value);}/*** * * @Title: evictCache* * @Description: 根據key, 從緩存中清除特定的key記錄* * @param key* * @return: void*/public void evictCache(Object key) {if (cacheMap.containsKey(key)) {cacheMap.remove(key);}}/*** * * @Title: evictCache* * @Description: 清空緩存中的數據* * * @return: void*/public void evictCache() {cacheMap.clear();} }有了實體類和緩存管理器,還需要一個查詢Artisan的服務類,此服務使用緩存管理器來支持用戶查詢。
package com.xgj.cache.selfCacheManagerDemo.service;import java.sql.ResultSet; import java.sql.SQLException;import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.stereotype.Service;import com.xgj.cache.selfCacheManagerDemo.CacheManager; import com.xgj.cache.selfCacheManagerDemo.domain.LittleArtisan;/*** * * @ClassName: LittleArtisanService* * @Description: @Service標注的Service層,受Spring管理* * @author: Mr.Yang* * @date: 2017年10月2日 下午1:55:56*/@Service("littleArtisanService") public class LittleArtisanService {private Logger logger = Logger.getLogger(LittleArtisanService.class);// 緩存管理器private CacheManager<LittleArtisan> cacheManager;// JdbcTemplateprivate JdbcTemplate jdbcTemplate;private static final String selectArtisanSQL = "select artisan_id ,artisan_name ,artisan_desc from little_artisan where artisan_name = ?";/*** * * @Title: setJdbcTemplate* * @Description: 通過@Autowired注入JdbcTemplate* * @param jdbcTemplate* * @return: void*/@Autowiredpublic void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}/*** * * @Title:LittleArtisanService* * @Description:構造函數*/public LittleArtisanService() {// 初始化LittleArtisanService的時候,實例化CacheManagercacheManager = new CacheManager<LittleArtisan>();}public LittleArtisan getLittleArtisan(String artisanName) {// 首先從緩存中查找LittleArtisanLittleArtisan littleArtisan = cacheManager.getValue(artisanName);// 緩存中不存在則從數據庫中獲取if (littleArtisan != null) {logger.info("get littleArtisan from Cache...");return littleArtisan;}// 從數據庫中獲取littleArtisan = getFromDB(artisanName);logger.info("get littleArtisan from DB...");// 如果數據庫中存在記錄,則將獲取的新數據放在緩存中if (littleArtisan != null) {cacheManager.addOrUpdateCache(artisanName, littleArtisan);logger.info("put cache successfully");}return littleArtisan;}/*** * * @Title: reload* * @Description: 清除緩存數據,重新加載* * * @return: void*/public void reload() {cacheManager.evictCache();}/*** * * @Title: getFromDB* * @Description: 從數據庫中獲取LittleArtisan* * @param artisanName* @return* * @return: LittleArtisan*/private LittleArtisan getFromDB(String artisanName) {final LittleArtisan littleArtisan = new LittleArtisan();jdbcTemplate.query(selectArtisanSQL, new Object[] { artisanName },new RowCallbackHandler() {@Overridepublic void processRow(ResultSet rs) throws SQLException {littleArtisan.setArtisanId(rs.getString("artisan_id"));littleArtisan.setArtisanName(rs.getString("artisan_name"));littleArtisan.setArtisanDesc(rs.getString("artisan_desc"));}});return littleArtisan;} }Spring配置文件如下
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"><!-- 掃描類包,將標注Spring注解的類自動轉化Bean,同時完成Bean的注入 --><context:component-scan base-package="com.xgj.cache.selfCacheManagerDemo" /><!-- 使用context命名空間,加載數據庫的properties文件 --><context:property-placeholder location="classpath:spring/jdbc.properties" /><!-- 數據庫 --><bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close" p:driverClassName="${jdbc.driverClassName}"p:url="${jdbc.url}" p:username="${jdbc.username}" p:password="${jdbc.password}" /><!-- 配置Jdbc模板 --><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"p:dataSource-ref="dataSource" /></beans>下面來編寫單元測試
package com.xgj.cache.selfCacheManagerDemo.service;import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.context.support.ClassPathXmlApplicationContext;import com.xgj.cache.selfCacheManagerDemo.domain.LittleArtisan;public class LittleArtisanCacheTest {ClassPathXmlApplicationContext ctx = null;LittleArtisanService littleArtisanService = null;@Beforepublic void initContext() {// 啟動Spring 容器ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/cache/selfCacheManagerDemo/conf.xml");littleArtisanService = ctx.getBean("littleArtisanService",LittleArtisanService.class);System.out.println("initContext successfully");}@Testpublic void testLoadArtisanFromDBAndCache() {LittleArtisan littleArtisan = new LittleArtisan();// 第一次 從數據庫中獲取littleArtisan = littleArtisanService.getLittleArtisan("artisan");System.out.println("artisanDesc:" + littleArtisan.getArtisanDesc());// 再此調用,會從Cache中獲取littleArtisan = littleArtisanService.getLittleArtisan("artisan");System.out.println("artisanDesc:" + littleArtisan.getArtisanDesc());// 清空緩存,再此讀取,會再此從數據庫中加載littleArtisanService.reload();littleArtisan = littleArtisanService.getLittleArtisan("artisan");System.out.println("artisanDesc:" + littleArtisan.getArtisanDesc());}@Afterpublic void closeContext() {if (ctx != null) {ctx.close();}System.out.println("close context successfully");} }運行結果
2017-10-02 14:39:27,876 INFO [main] (AbstractApplicationContext.java:583) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@611de50: startup date [Mon Oct 02 14:39:27 BOT 2017]; root of context hierarchy 2017-10-02 14:39:27,989 INFO [main] (XmlBeanDefinitionReader.java:317) - Loading XML bean definitions from class path resource [com/xgj/cache/selfCacheManagerDemo/conf.xml] initContext successfully 2017-10-02 14:39:29,621 INFO [main] (LittleArtisanService.java:78) - get littleArtisan from DB... 2017-10-02 14:39:29,622 INFO [main] (LittleArtisanService.java:82) - put cache successfully artisanDesc:Spring Cache 2017-10-02 14:39:29,622 INFO [main] (LittleArtisanService.java:73) - get littleArtisan from Cache... artisanDesc:Spring Cache 2017-10-02 14:39:29,624 INFO [main] (LittleArtisanService.java:78) - get littleArtisan from DB... 2017-10-02 14:39:29,624 INFO [main] (LittleArtisanService.java:82) - put cache successfully artisanDesc:Spring Cache 2017-10-02 14:39:29,625 INFO [main] (AbstractApplicationContext.java:984) - Closing org.springframework.context.support.ClassPathXmlApplicationContext@611de50: startup date [Mon Oct 02 14:39:27 BOT 2017]; root of context hierarchy close context successfully首先第一次從數據庫中加載數據,然后放入緩存中,第二次讀取從緩存中加載, 然后我們清空了緩存,再此運行,又從DB加載數據
缺點:
雖然,這種自定義的緩存可以正常工作,但是這種實現方式并不優雅,緩存代碼和業務代碼高度耦合, 業務代碼穿插這大量的緩存控制邏輯,并且代碼顯式依賴緩存的具體實現。
并且我們的這個版本目前也不支持按照條件緩存,比如只緩存某些特定條件的Artisan等等。
使用Spring Cache
下面我們使用Spring Cache來重構上面的實現。
package com.xgj.cache.springCacheManagerDemo.service;import java.sql.ResultSet; import java.sql.SQLException;import org.apache.log4j.Logger; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.annotation.Cacheable; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowCallbackHandler; import org.springframework.stereotype.Service;import com.xgj.cache.springCacheManagerDemo.domain.LittleArtisan;/*** * * @ClassName: LittleArtisanSpringCacheService* * @Description: @Service標注的服務層,受Spring管理* * @author: Mr.Yang* * @date: 2017年10月2日 下午5:34:29*/@Service public class LittleArtisanSpringCacheService {private Logger logger = Logger.getLogger(LittleArtisanSpringCacheService.class);private static final String selectArtisanSQL = "select artisan_id ,artisan_name ,artisan_desc from little_artisan where artisan_name = ?";private JdbcTemplate jdbcTemplate;@Autowiredpublic void setJdbcTemplate(JdbcTemplate jdbcTemplate) {this.jdbcTemplate = jdbcTemplate;}/*** * * @Title: getArtisan* * @Description: @Cacheable(cacheNames = "littleArtisan")* 使用名為littleArtisan的緩存* * * @return* * @return: LittleArtisan*/@Cacheable(cacheNames = "littleArtisan")public LittleArtisan getArtisan(String artisanName) {// 方法內部實現不考慮緩存邏輯,直接實現業務System.out.println("查找Artisan:" + artisanName);return getFromDB(artisanName);}/*** * * @Title: getFromDB* * @Description: 從數據庫中獲取LittleArtisan* * @param artisanName* @return* * @return: LittleArtisan*/private LittleArtisan getFromDB(String artisanName) {System.out.println("getFromDB");final LittleArtisan littleArtisan = new LittleArtisan();jdbcTemplate.query(selectArtisanSQL, new Object[] { artisanName },new RowCallbackHandler() {@Overridepublic void processRow(ResultSet rs) throws SQLException {littleArtisan.setArtisanId(rs.getString("artisan_id"));littleArtisan.setArtisanName(rs.getString("artisan_name"));littleArtisan.setArtisanDesc(rs.getString("artisan_desc"));}});return littleArtisan;} }@Cacheable(value=littleArtisan”),這個注釋的意思是,當調用這個方法的時候,會從一個名叫 littleArtisan的緩存中查詢,如果沒有,則執行實際的方法(即查詢數據庫),并將執行的結果存入緩存中,否則返回緩存中的對象。這里的緩存中的 key 就是參數 artisanName,value 就是 Artisan對象。“littleArtisan”緩存是在 spring*.xml 中定義的名稱。
我們使用spring,所以肯定還需要一個 spring 的配置文件來支持基于注釋的緩存 。
配置文件如下:
<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"xmlns:context="http://www.springframework.org/schema/context"xmlns:util="http://www.springframework.org/schema/util"xmlns:cache="http://www.springframework.org/schema/cache"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsdhttp://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd"><!-- 掃描類包,將標注Spring注解的類自動轉化Bean,同時完成Bean的注入 --><context:component-scan base-package="com.xgj.cache.springCacheManagerDemo" /><!-- 使用context命名空間,加載數據庫的properties文件 --><context:property-placeholder location="classpath:spring/jdbc.properties" /><!-- 數據庫 --><bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"destroy-method="close" p:driverClassName="${jdbc.driverClassName}"p:url="${jdbc.url}" p:username="${jdbc.username}" p:password="${jdbc.password}" /><!-- 配置Jdbc模板 --><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"p:dataSource-ref="dataSource" /><!-- (1)添加cache命名空間和schema文件 --> <!-- (2)開啟支持緩存的配置項 --><cache:annotation-driven cache-manager="cacheManager" proxy-target-class="true"/><!-- (3)配置cacheManger --><bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager"p:caches-ref="cacheObjects"></bean><!-- (4)caches集合 --><util:set id="cacheObjects"><bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"p:name="default"/><!-- @Cacheable(cacheNames = "littleArtisan")標注的cache名稱 --><bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"p:name="littleArtisan"/></util:set></beans>spring 配置文件有一個關鍵的支持緩存的配置項:<cache:annotation-driven />,這個配置項缺省使用了一個名字叫 cacheManager 的緩存管理器,這個緩存管理器有一個 spring 的缺省實現,即 org.springframework.cache.support.SimpleCacheManager,這個緩存管理器實現了我們剛剛自定義的緩存管理器的邏輯,它需要配置一個屬性 caches,即此緩存管理器管理的緩存集合。
除了缺省的名字叫 default 的緩存,我們還自定義了一個名字叫 littleArtisan的緩存,
使用了缺省的內存存儲方案 ConcurrentMapCacheFactoryBean,它是基于 java.util.concurrent.ConcurrentHashMap 的一個內存緩存實現方案。
單元測試
package com.xgj.cache.springCacheManagerDemo.service;import org.junit.After; import org.junit.Before; import org.junit.Test; import org.springframework.context.support.ClassPathXmlApplicationContext;import com.xgj.cache.springCacheManagerDemo.domain.LittleArtisan;public class LittleArtisanSCServiceTest {ClassPathXmlApplicationContext ctx = null;LittleArtisanSpringCacheService littleArtisanSpringCacheService = null;@Beforepublic void initContext() {// 啟動Spring 容器ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/cache/springCacheManagerDemo/conf_spring.xml");littleArtisanSpringCacheService = ctx.getBean("littleArtisanSpringCacheService",LittleArtisanSpringCacheService.class);System.out.println("initContext successfully");}@Testpublic void testLoadArtisanFromDBAndCache() {LittleArtisan artisan = new LittleArtisan();// 第一次查詢,從數據庫獲取數據artisan = littleArtisanSpringCacheService.getArtisan("artisan");System.out.println("========load from db===========");System.out.println("artisanDesc:" + artisan.getArtisanDesc());// 第二次查詢,直接返回緩存的值artisan = littleArtisanSpringCacheService.getArtisan("artisan");System.out.println("========hit cache========");System.out.println("artisanDesc:" + artisan.getArtisanDesc());}@Afterpublic void closeContext() {if (ctx != null) {ctx.close();}System.out.println("close context successfully");} }輸出結果
2017-10-02 20:06:31,637 INFO [main] (AbstractApplicationContext.java:583) - Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@77c55a79: startup date [Mon Oct 02 20:06:31 BOT 2017]; root of context hierarchy 2017-10-02 20:06:31,751 INFO [main] (XmlBeanDefinitionReader.java:317) - Loading XML bean definitions from class path resource [com/xgj/cache/springCacheManagerDemo/conf_spring.xml] initContext successfully 查找Artisan:artisan getFromDB ========load from db=========== artisanDesc:Spring Cache ========hit cache======== artisanDesc:Spring Cache 2017-10-02 20:06:33,711 INFO [main] (AbstractApplicationContext.java:984) - Closing org.springframework.context.support.ClassPathXmlApplicationContext@77c55a79: startup date [Mon Oct 02 20:06:31 BOT 2017]; root of context hierarchy close context successfully可以看到第二次加載并沒有打印getFromDB,說明沒有從數據庫加載,而是從緩存中加載。
示例源碼
代碼已托管到Github—> https://github.com/yangshangwei/SpringMaster
總結
以上是生活随笔為你收集整理的Spring Cache-缓存概述及使用的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring-Spring整合MyBat
- 下一篇: Spring Cache抽象-缓存注解