《partner4java 講述Spring入門》之:spring cache支持(spring3.1如何使用cache 緩存)

(以下內容參照自官方文檔;p4jorm下載地址http://blog.csdn.net/partner4java/article/details/8629578;cache demo下載地址http://download.csdn.net/detail/partner4java/5102477)

若您只想儘快簡單的使用,可以直接跳轉到“第三部分:Hello World

1、引言:

       從3.1開始,spring框架提供了非侵入式的緩存支持。類似事務支持(transaction)一樣簡單,且通過定義註解來提供統一進行緩存支持。

2、理解緩存:

        在互聯網行業,一般使用的是MySQL數據庫,不會像oracle一樣包含了強大的緩存策略,所以往往大併發訪問瓶頸在數據庫訪問上。

        減少數據庫訪問,一般信息界面可以採用靜態化,但是緩存無疑會是一個簡單高效的方式(一般會採用OScache -- spring mvc進行了單獨支持)。在統一的頁面緩存背後還有一些數據是很少變化的,所以僅僅依靠頁面緩存也是會照成不必要的“資源”浪費,設置一層數據緩存也是很重要的(藉助ehcache或memcached,memcached互聯網公司採用的比較多)。

需要注意的是:無論何種緩存方案都會存在數據實效性問題。且,spring的cache方案還需要相同參數調用同一個方法在數據一致的情況下返回結果也應該是一致的。

使用spring cache只需要完成兩部分:

·緩存聲明:在方法上加上相應緩存註解和相應策略

·configuration:定義緩存位置和具體保存策略

(spring cache並不是完全由spring提供,和transaction一樣,只是對第三方框架進行上層封裝)


第一分部:緩存聲明

3、基於註解的緩存聲明:

我們只需要學習四個註解:@Cacheable、@CachePut 、 @CacheEvict 和@Caching


@Cacheable annotation:

正如其名字,@Cacheable用於添加在需高速緩存的方法上。這些方法默認會以參數爲主鍵把返回結果存儲到高速緩存中,以便在隨後的調用(使用相同的參數)方法,直接返回高速緩存中的值,不需要實際執行此方法。

最簡單的方式,只需要聲明一個相關緩存策略的名稱:

@Cacheable("books")
public Book findBook(ISBN isbn) {...}

也可以設置多個緩衝塊,其中一個緩衝塊命中即會返回,並會同步其他緩存塊:

@Cacheable({ "books", "isbns" })
public Book findBook(ISBN isbn) {...}

默認緩存主鍵:

緩存是採用鍵值的方式存儲,所以每次調用都要將相應的參數轉化成一個合適的高效緩存主鍵。
默認的主鍵生成策略:
·如果沒有參數,返回0;
·如果存在一個參數,則返回該參數實例;
·如果不止一個參數,返回所有參數的哈希計算值。

也可以同時實現org.springframework.cache.KeyGenerator來定義自己特定的主鍵生成策略。

自定義緩存主鍵:

由於緩存塊是通用的,所以不能簡單的進行緩存主鍵聲明,這樣將導致生成的主鍵與業務不服或者與其他業務重複,如:

@Cacheable("books")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

看上去應該並不是所有參數對於生成緩存主鍵都是有意義的。

像這種情況下,允許通過key屬性來指定主鍵生成策略,且key支持使用SpEL:

@Cacheable(value="books", key="#isbn"
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed);


@Cacheable(value="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed);


@Cacheable(value="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed);

上面的key可以簡單的通過SpEL調用參數的屬性。

緩存條件(何時緩存):

同樣可以對condition參數通過SpEL表達式,當返回true時將被緩存,若返回false會執行方法。

如下,當名字長度小於32時纔會緩存:

@Cacheable(value="book", condition="#name.length < 32")
public Book findBook(String name)

SpEL使用上下文:



@CachePut annotation:

用法類似於@Cacheable,但是用於存放數據聲明(如更新數據),所以每次都會執行,將執行後的結果存入緩存。

所以不建議把@CachePut and @Cacheable放在同一方法上,對於需要更新的數據我們應使用 @CachePut。


@CacheEvict annotation:

此註解對於去除無效的數據是非常重要的。@CacheEvict用於觸發去除緩存中的數據。

除了和上面的註解用於標識緩存策略、主鍵和判斷方式等外,又添加了allEntries屬性,用於標識是否不僅只刪除基於主鍵的數據:

@CacheEvict(value = "books", allEntries=true)
public void loadBooks(InputStream batch);

如上當需要清除一個“區域”的所有數據,而不是隻清除一條數據,所以即使指定主鍵也是無用的。

初次之外還提供了一個beforeInvocation屬性,用於表示是在執行前清除還是之後。這個屬性是很有用的,比如當你在聲明一個更新緩存方法之上(結合@Cacheable的場景)。

If the method does not execute (as it might be cached) or an exception is thrown, the eviction does not occur. 
The latter (beforeInvocation=true) causes the eviction to occur always, before the method is invoked - this is useful in cases where the eviction does not need  to be tied to the method outcome.

但是當無返回值(void)時,結合Cacheable將沒有什麼意義。

It is important to note that void methods can be used with @CacheEvict - as the methods act as triggers, the return values are ignored (as they don't interact with the cache) - this is not the case with @Cacheable which adds/update data into the cache and thus requires a result.


@Caching annotation:

有時我們需要添加多個註解,可以通過此註解嵌套在一起。

@Caching(evict = { @CacheEvict("primary"), @CacheEvict(value = "secondary", key =
"#p0") })
public Book importBooks(String deposit, Date date);


自定義註解:

有時緩存的一些註解配置是常用重複的,爲了避免來回拷貝,你可以自定義自己的註解,如:

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
@Cacheable(value=“books”, key="#isbn")
public @interface SlowService {
}

然後你就可以通過你自定義的@SlowService註解替代下面的做法:

@Cacheable(value="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

替代方式:

@SlowService
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)

(這樣Spring就會自動識別,當然前提是你已經開啓了緩存註解支持)


學習了,註解如何使用,那麼如何通知Spring掃描相關注解呢?

緩存註解開啓開關:

有時,你需要一個統一的開關進行控制緩存的開啓和關閉。

只需要在你標註了@Configuration註解的類上添加@EnableCaching註解即可。

@Configuration
@EnableCaching
public class AppConfig {
}

或者通過XML方式配置使用的緩存:annotation-driven

<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://
www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://
www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/
cache/spring-cache.xsd">
	<cache:annotation-driven />
</beans>

類似於@Transactional這兩種方式通過AOP進行實現。

Cache annotation settings:

XML Attribute        |           Annotation Attribute  |    Default           |Description
cache-manager  N/A (See CachingConfigurer Javadoc)    cacheManager      Name of cache manager to use. Only required if the name of the cache manager is not cacheManager, as in the example above.
mode             mode                    proxy                    The default mode "proxy" processes annotated beans to be proxied using Spring's AOP framework (following proxy semantics, as discussed above, applying to method calls coming in through the proxy only). 
 The alternative mode "aspectj" instead weaves the affected classes with Spring's AspectJ caching aspect, modifying the target class byte code to apply to any kind of method call. 
 AspectJ weaving requires springaspects. jar in the classpath as well as load-time weaving (or compile-time weaving) enabled. 
proxy-targetclass       proxyTargetClass                              false                                   Applies to proxy mode only. 
 Controls what type of caching proxies are created for classes annotated with the @Cacheable or @CacheEvict annotations. 
 If the proxy-targetclass attribute is set to true, then class-based proxies are created.
 If proxy-targetclass is false or if the attribute is omitted, then standard JDK interface-based proxies are created. 
order  order Ordered.LOWEST_PRECEDENCE  Defines the order of the cache advice that is applied to beans annotated with @Cacheable or @CacheEvict. 
 (For more information about the rules related to ordering of AOP advice, see the section called “Advice ordering”.) 
 No specified ordering means that the AOP subsystem determines the order of the advice.

注意<cache:annotation-driven/>僅對上下文程序有效,如你添加到了WebApplicationContext那麼僅對Controller有效,對你的service是無效的。這一點你沒必要太糾結,若對IoC瞭解不是很透徹。

如果你使用的JDK代理方式,你只能在public類型方法上使用,若你想添加在私有方法上,需切換到AspectJ模式。

Spring建議將緩存註解標註在實現類而非接口上,因爲標註在接口上且使用了類代理而非接口代理,當運行其他AOP反射時,將無法得知,因爲註解是不會被繼承的。


<cache:annotation-driven />用於指定了緩存“封裝”策略,那麼具體的“實現”策略如何指定呢?


第二部分:configuration

4、Configuring the cache storage:

使用過Spring的同學應該很清楚,通過IoC,spring讓各個層面的實現易於替換,spring cache也是如此。

添加緩存註解聲明後,我們需要指定一個緩存管理器--“明確”具體的數據保存策略。

下面我們列出了兩種:通過ConcurrentMap和ehcache。

JDK ConcurrentMap-based Cache:

<!-- generic cache manager -->
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
	<property name="caches">
		<set>
			<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" p:name="default"/>
			<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean" 	p:name="books"/>
		</set>
	</property>
</bean>

上面的cache策略,簡單的通過SimpleCacheManager來實現,可以用來測試或簡單緩存(但是會非常高效,這種簡單方式並沒有指定超時等策略),配置中添加了兩個緩存名稱default、books。

Ehcache-based Cache:

ehcache的實現放在包org.springframework.cache.ehcache中,同樣,也只需要非常簡單的配置:

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager" p:cachemanager-
ref="ehcache"/>
<!-- Ehcache library setup -->
<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean" p:configlocation="
ehcache.xml"/>

首先通過factory指定了ehcache配置文件位置並創建CacheManager,然後賦予EhCacheCacheManager。

聲明“空”緩存:

有時你需要測試無緩存的環境,且不想修改註解或其他配置,可參照:

<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
	<property name="cacheManagers">
		<list>
		<ref bean="jdkCache"/>
		<ref bean="gemfireCache"/>
		</list>
	</property>
	<property name="fallbackToNoOpCache" value="true"/>
</bean>
list中存放實際緩存策略,fallbackToNoOpCache聲明無緩存。

除了以上兩種緩存策略,你也可以自定義所需的策略,思路是通過CacheManager、Cache兩個類的封裝,org.springframework.cache.support下的類可以幫助實現。


第一部分擴充:基於XML的緩存

如果項目不允許或者不習慣使用註解,也可以像transaction一樣,使用XML方式進行聲明指定。


cache:advice配置中添加了對bookService的緩存策略,緩存策略名爲books,對方法findBook進行了緩存,主鍵爲#isbn。還包括清除緩存的聲明。

aop:advisor通過AspectJ的方式進行了通知。

方法和transaction非常相似,可參考相關文章。

通過XML方式有一些好處:對已有代碼沒有任何侵入;便於統一指定範圍內的類等


第三部分:Hello World

(首先本demo採用:spring3.1.2.RELEASE + struts2.3.8 + hibernate4.1.10.Final;訪問類似地址http://localhost:8080/cache/;CURD操作採用了本博主的P4jORM框架,請參照相關文章;先跑起來項目添加一些數據熟悉一下工程)


Demo1:在“spring_cache_demo沒添加cache之前原始項目.zip”基礎之上,通過簡單的三步,來實現第一個cache功能 -- 緩存根據id獲取數據方法

第一步:添加緩存註解

	// 添加緩存聲明,demo1爲緩存策略名稱(我們會在XML中聲明此策略)
	@Cacheable("demo1")
	@Override
	public User get(Serializable entityId) {
		return super.get(entityId);
	}
第二步:打開緩存註解(我們採用的XML開啓方式)

<cache:annotation-driven />

第三步:聲明具體緩存策略

	<cache:annotation-driven cache-manager="cacheManager" />

	<!-- generic cache manager -->
	<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
		<property name="caches">
			<set>
				<bean
					class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
					p:name="default" />
				<bean
					class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean"
					p:name="demo1" />
			</set>
		</property>
	</bean>
還對action的更新動作進行了修改(這一步和Demo實現步驟無關,只是爲了更直接的顯示緩存效果):

	/**
	 * 更新動作
	 * 
	 * @return
	 */
	public String edit() {
		User user = userService.get(form.getUserId());

		// 又定義一個新的user是因爲在測試緩存時,避免緩存策略是採用指針直接引用
		User mUser = new User();
		BeanUtils.copyProperties(user, mUser);
		mUser.setUserName(form.getUserName());
		userService.update(mUser);

		return viewList();
	}
現在我們對一條數據進行修改,點擊修改,提交,查看數據庫的數據是已經更改生效的,但是再次點擊該數據更改界面還是顯示更改前的名稱。充分說明了@Cacheable後不再從數據庫中獲取數據,而是從緩存中獲取。

@Cacheable的具體使用規則請參考文章中相關解釋。


Demo2:更新數據時,使緩存失效,下一次從數據庫中獲取

在Demo1基礎上,更改如下即可:

	@CacheEvict(value = "demo1", key = "#entity.userId")
	@Override
	public void update(Object entity) {
		super.update(entity);
	}
@CacheEvict的使用規則參照文章中相關解釋。

試驗步驟:打開列表頁 -- > 修改數據--提交 -- > 刷新列表頁數據已被更改 -- > 繼續點擊此數據修改連接(關閉只打開不修改),顯示的數據爲修改後的,證明@CacheEvict發揮了清除緩存的功能 -- > 然後直接SQL通過工具修改數據庫該字段(一定要在修改後再次點擊修改之後) -- > 再次點擊列表頁中此數據的修改連接,回顯的數據非數據庫實時數據,而是上次修改後的值,證明獲取繼續走了緩存。


Demo3:將更新後的數據進行緩存,且更新前清除緩存

	//不添加@CacheEvict,只添加@Cacheable,更新是無效的
	@CacheEvict(value = "demo1", key = "#entity.userId", beforeInvocation = true)
	@Cacheable(value = "demo1", key = "#entity.userId")
	@Override
	public User updateUser(User entity) {
		// 更改了原有設計,返回了更改後的對象
		super.update(entity);
		return entity;
	}
測試步驟:打開列表頁 --> 修改一條數據 --> 打開數據庫,驗證更新生效,且從數據庫中修改本條數據 --> 繼續修改此條數據,回顯數據爲界面更改後的,不是數據庫中實時數據(因爲更新的主鍵和查詢緩存主鍵相同)

也可以通過一個註解實現:

	@CachePut(value = "demo1", key = "#entity.userId")
	@Override
	public User updateUser(User entity) {
		// 更改了原有設計,返回了更改後的對象
		super.update(entity);
		return entity;
	}

Demo4:自定義緩存主鍵生成策略

	@Cacheable("demo1")
	@Override
	public PageData<User> query(Object formbean, PageIndex pageIndex, LinkedHashMap<String, OrderType> orderby) {
		return super.query(formbean, pageIndex, orderby);
	}
默認緩存方式,會以參數的hash值爲key:

public class DefaultKeyGenerator implements KeyGenerator {

	public static final int NO_PARAM_KEY = 0;
	public static final int NULL_PARAM_KEY = 53;

	public Object generate(Object target, Method method, Object... params) {
		if (params.length == 1) {
			return (params[0] == null ? NULL_PARAM_KEY : params[0]);
		}
		if (params.length == 0) {
			return NO_PARAM_KEY;
		}
		int hashCode = 17;
		for (Object object : params) {
			hashCode = 31 * hashCode + (object == null ? NULL_PARAM_KEY : object.hashCode());
		}
		return Integer.valueOf(hashCode);
	}

}
透過默認提供的key生成方式可以看到,若默認key策略,參數對象需要儘量重寫hashCode(),保證相同數據生成的hashCode值相同,否則會出現問題(相同查詢參數,但生成的緩存key不同,進而導致無法緩存,且使緩存爆滿)。

自定義key生成只需要簡單兩步:

第一步:自定義主鍵策略

public class MyKeyGenerator extends DefaultKeyGenerator {

	@Override
	public Object generate(Object target, Method method, Object... params) {
		// Object keyGenerator = super.generate(target, method, params);

		StringBuffer buffer = new StringBuffer();
		Class entityClass = GenericsHelper.getSuperGenericsClass(target.getClass());
		buffer.append(entityClass.getName());
		if (params != null && params.length > 1) {
			for (Object obj : params) {
				if (obj != null) {
					if (obj instanceof AtomicInteger || obj instanceof AtomicLong || obj instanceof BigDecimal
							|| obj instanceof BigInteger || obj instanceof Byte || obj instanceof Double
							|| obj instanceof Float || obj instanceof Integer || obj instanceof Long
							|| obj instanceof Short) {
						buffer.append(obj);
					} else if (obj instanceof List || obj instanceof Set || obj instanceof Map) {
						buffer.append(obj);
					} else {
						buffer.append(obj.hashCode());
					}
				}
			}
		}
		System.out.println("key-buffer:" + buffer.toString());
		int keyGenerator = buffer.toString().hashCode();
		return keyGenerator;
	}

}
第二步:配置策略

	<cache:annotation-driven cache-manager="cacheManager"
		key-generator="keyGenerator" />

	<!-- 自定義cache主鍵生成策略 -->
	<bean id="keyGenerator" class="com.partner4java.helper.MyKeyGenerator" />

Demo5:ehcache支持

      我們接下來是介紹的Spring如何藉助ehcache來對bean(dao、service、controller...)的調用結果進行緩存。(一般還有另外一種結合方案,如hibernate本身支持對ehcache的結合)

緩存註解無需變更和SimpleCacheManager一致,沒有任何區別,所用其他緩存策略,只需要更改配置即可:

首先配置文件分兩部分,spring指定ehcache和ehcache本身的緩存策略配置:

	<cache:annotation-driven cache-manager="cacheManager"
		key-generator="keyGenerator" />

	<!-- spring-cache:cache相關 -->
	<bean id="cacheManagerFactory"
		class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
		p:configLocation="classpath:META-INF/ehcache.xml" />
	<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"
		p:cacheManager-ref="cacheManagerFactory" />
ehcache.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!-- 
• timeToIdleSeconds – The maximum number of seconds an element can exist in the cache without being accessed. The element expires at this limit and will no longer be returned from the cache. The default value is 0, which means no TTI eviction takes place (infinite lifetime).
• timeToLiveSeconds – The maximum number of seconds an element can exist in the cache regardless of use. The element expires at this limit and will no longer be returned from the cache. The default value is 0, which means no TTL eviction takes place (infinite lifetime).
• maxElementsOnDisk – The maximum sum total number of elements (cache entries) allowed for a distributed cache in all Terracotta clients. If this target is exceeded, eviction occurs to bring the count within the allowed target. The default value is 0, which means no eviction takes place (infinite size is allowed). Note that this value reflects storage allocated on the Terracotta Server Array. A setting of 0 means that no eviction of the cache's entries takes place on Terracotta Server Array, and consequently can cause the servers to run out of disk space.
• eternal – If the cache–s eternal flag is set, it overrides any finite TTI/TTL values that have been set. 
-->
<ehcache>
	<defaultCache maxElementsInMemory="10" eternal="false"
		timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false">
	</defaultCache>
	<cache name="demo1" maxElementsInMemory="20000" eternal="false"
		timeToIdleSeconds="3600" timeToLiveSeconds="1800" overflowToDisk="false" />

	<!-- <terracottaConfig url="localhost:9510"/> -->
</ehcache>  
加入相關的jar:

		<!-- 不能使用2.5:否則會報錯誤“1. Use one of the CacheManager.create() static” -->
		<dependency>
			<groupId>net.sf.ehcache</groupId>
			<artifactId>ehcache-core</artifactId>
			<version>2.4.7</version>
		</dependency>


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章