SpringMvc in Action——快取資料
小孩子常常會反覆問我一個問題:“為什麼你長的這麼帥啊?”過了一會,又再問一遍。 很多方面來看,在我們所編寫的應用中,有些元件也是這樣的。無狀態的元件一般來講擴充套件性要好一些,但是他們也更傾向於一遍一遍詢問相同的問題。因為他們是無狀態的,一旦完成當前的任務,就會丟棄掉已經獲取到的所有解答。 為了得到問題的答案,我們可能會使用資料庫,呼叫遠端服務,或者執行復雜的計算。 而如果問題的答案變更沒有那麼頻繁或者根本沒有變化,那麼再去走一遍流程是很浪費的,所以我們還不如將問題的答案記住,這就用到了快取Caching。
啟用對快取的支援
Spring對快取的支援有兩種方式:
- 註解驅動的快取
- XML宣告的快取
在使用Spring的快取抽象時,最為通用的方法是加上Cacheable
和CacheEvict
註解,我們對XML宣告的可以有所瞭解。在往bean上新增快取註解之前,必須啟用Spring對註解驅動快取的支援。我們可以在配置類上新增@EnableCaching
,這樣的話就能啟用註解驅動的快取。
package spittr.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework. cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager(){
return new ConcurrentMapCacheManager ();
}
}
@EnableCaching
啟動快取。
cacheManager()
用以宣告快取管理器。
如果以XML的方式配置,那麼可以用<cache:annotation-driven>
元素來啟動註解驅動的快取。
本質上,@EnableCaching
和<cache:annotation-driven>
的工作方式是相同的。它們都會建立一個切面並出發Spring快取註解的切點,根據所使用的註解以及快取的狀態,這個切面會從快取中獲取資料,將資料新增到快取之中或者從快取之中移除某個值。
不僅啟動了註解驅動的快取,而且還聲明瞭一個快取管理器(cache manager)的bean。快取管理器是Spring快取抽象的核心,它能夠與多個流行的快取實現進行繼承。
本例中宣告的ConcurrentMapCacheManager,這個簡單的快取管理器使用java.util.concurrent.ConcurrentHashMap作為其快取。它非常簡單,因此對於開發,測試或基礎的應用來講,這是一個很不錯的選擇。但對於生產級別的大型企業級程式,這可能不是很好的選擇。
配置快取管理器 Spring3.1內建了五個快取管理器: Spring3.2引入了另外一個快取管理器。除了核心的Spring框架,Spring Data又提供了兩個快取管理器:
- RedisCacheManager
- GemfireCacheManager
可以看到,這麼多快取管理器,我們有很多選擇。儘管做出的選擇會影響到如何快取,但是Spring宣告快取的方式並沒有什麼差別。
ehcache直接在jvm虛擬機器中快取,速度快,效率高;但是快取共享麻煩,叢集分散式應用不方便。 redis是通過socket訪問到快取服務,效率比ecache低,比資料庫要快很多,處理叢集和分散式快取方便,有成熟的方案。 如果是單個應用或者對快取訪問要求很高的應用,用ehcache。 如果是大型系統,存在快取共享、分散式部署、快取內容很大的,建議用redis。
使用Ehcache快取:
package spittr.config;
import net.sf.ehcache.management.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.ehcache.EhCacheCacheManager;
import org.springframework.cache.ehcache.EhCacheManagerFactoryBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public EhCacheCacheManager cacheManager(CacheManager cacheManager){
return new EhCacheCacheManager(cacheManager);
}
@Bean
public EhCacheManagerFactoryBean ehcache(){
EhCacheManagerFactoryBean ehCacheManagerFactoryBean=
new EhCacheManagerFactoryBean();
ehCacheManagerFactoryBean.setConfigLocation(
new ClassPathResource("ehcache.xml"));
return ehCacheManagerFactoryBean;
}
}
這是一個基礎的EhCache配置,其他的配置細節,可以等具體實現的時候再瞭解。
使用Redis快取: 其實快取的條目無非就是一個鍵值對,很自然的想到Redis快取。 Redis可以用來為Spring快取抽象機制儲存快取條目。RedisCacheManager是一個CacheManager的實現。RedisCacheManager會與一個Redis伺服器寫作,並通過RedisTemplate將快取條目儲存到Redis中。 為了使用RedisCacheManager,我們需要RedisTemplate bean以及RedisConnectionFactory實現類的一個bean.
在RedisTemplate就緒之後,配置RedisCacheManager就是非常簡單的一件事了:
package spittr.config;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
@Configuration
@EnableCaching
public class RedisCacheConfig {
@Bean
public CacheManager cacheManager(RedisTemplate redisTemplate){
return new RedisCacheManager(redisTemplate);
}
@Bean
public JedisConnectionFactory jedisConnectionFactory(){//redisFactory
JedisConnectionFactory jedisConnectionFactory=new JedisConnectionFactory();
jedisConnectionFactory.afterPropertiesSet();
return jedisConnectionFactory;
}
@Bean
public RedisTemplate<String,String> redisTemplate(
RedisConnectionFactory redisConnectionFactory
){
RedisTemplate<String,String> redisTemplate=new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
在配置完快取管理器並啟動快取後,就可以在bean方法上應用快取規則了。
為方法添加註解以支援快取
填充快取
我們可以看到@Cacheable
和@CachePut
註解都可以填充快取,但是他們的工作方式略有差異。
@Cacheable
首先在快取中查詢條目,如果找到了匹配的條目,那麼就不會對方法進行呼叫了。如果沒有找到匹配的條目,方法會被呼叫並且返回值放到快取之中。
@CachePut
並不會在快取中檢查匹配的值,目標方法總是會呼叫,並將返回值新增到快取中。
他們倆屬性有一些是共有的:
在最簡單的情況下,只需要使用value屬性指定一個或多個快取即可。例如考慮SpittleRepository中的findOne()方法。在初始儲存之後,Spittle就不會再發生變化了。如果有的Spittle比較熱門並且會被頻繁的請求,我們可以在findOne()上新增@Cacheable
註解,確保將Spittle儲存在快取中,從而避免對資料庫的不必要的訪問。
@Cacheable("spittleCache")
public Spittle findOne(long id) {
return jdbc.queryForObject(
"select id, message, created_at, latitude, longitude" +
" from Spittle" +
" where id = ?",
new SpittleRowMapper(), id);
}
使用@Cacheable(“spittleCache”),當findOne()被呼叫時,快取切面會攔截呼叫並在快取中查詢之前以名"spittleCache"儲存的返回值,快取的Key是傳遞到findOne方法中的id引數。
當然,可以把@Cacheable放在介面的方法上面,而不是放在實現的方法上面。這樣,介面的方法的所有實現都會實現快取。
將值放到快取之中
@CachePut
採用了一種更為直接的流程。帶有這個註解的方法始終會被呼叫,而且它的返回值也會放到快取中。這提供了很便利機制,讓我們在請求之前預先載入快取。
例如,當一個全新的Spittle通過SpittleRepository的save()方法儲存之後,很可能馬上就會請求這條記錄。所以當save()方法呼叫後,立即將Spittle塞到快取中是很有意義的。當其他人通過findOne()查詢時,它已經準備就緒。
@Cacheable("spittleCache")
Spittle save(Spittle spittle);
這裡唯一的問題就是,快取的key。前文說過,快取的key是基於方法的引數決定的。因為save的引數是Spittle,那麼它會做快取的key。然而,詭異的是,Spittle的鍵值對都是spittle,更不幸的是,我們希望用id作為他的key。
讓我們看一下怎麼自定義快取的key。
自定義快取Key
@Cacheable
和@CachePut
都有一個名為key屬性,這個屬效能夠替換預設的key,它是通過一個SpEL表示式計算得到的。
具體到我們現在的場景,我們需要將Key設定為所儲存Spittle的ID,以引數形式傳遞給save()返回的Spittle得到的id屬性。幸好,為快取編寫SpEL表示式的時候,Spring暴露了一些很有用的元資料。
對於Save()方法, 我們需要的鍵是所返回的Spittle物件的id,表示式#result能夠得到方法調動的返回值Spittle物件。我們可以將key屬性設定為#result.id來引用id屬性。
@Cacheable(value="spittleCache",key="#result.id")
Spittle save(Spittle spittle);
條件化快取
通過為方法新增Spring的快取註解,Spring就會圍繞這個方法建立一個快取切面。但是,在有些場景下我們可能希望將快取功能關閉。
@Cacheable
和@CachePut
提供了兩個屬性以實現條件化快取:unless和condition。這兩個屬性都接受一個SpEL表示式。如果unless屬性的SpEL表示式計算結果為true,那麼快取方法返回的資料就不會放到快取中。與之類似,如果condition計算結果為false,那麼這個方法快取就會被禁用掉。
表面上看unless和condition都是做的相同的事情,但是實際上,unless屬性只能組織物件放進快取,但是在這個方法呼叫的時候,依然先去快取中查詢,沒找到才呼叫方法。而condition的表達結果為false時,不會去快取中查詢,直接呼叫方法,同時返回值也不會放進快取中。
作為樣例(儘管有些牽強),假設對於message屬性包含"NoCache"的Spittle物件不進行快取。為了阻止這樣的物件被快取起來,我們使用unless:
@Cacheable(value="spittleCache"
unless="#result.message.contains('NoCache')")
Spittle findOne(long id);
假如我們對於ID小於10的Spittle物件不使用快取,也不希望從快取中獲取資料:
@Cacheable(value="spittleCache"
unless="#result.message.contains('NoCache')"
condition="#id>=10")
Spittle findOne(long id);
移除快取條目
@CacheEvict
並不往快取中新增任何東西,相反,它還會移除一個或更多的在快取中的條目。
在什麼場景需要從快取中移除內容呢?當快取值不再合法時,我們應該確保將其從快取中移除,這樣的話,後續的快取命中就不會返回舊的或者已經不存在的值。
這樣的話,SpittleRepository的remove()方法就是使用@CacheEvict的絕佳選擇:
@CacheEvict("spittleCache")
void remove(long spittleId);