1. 程式人生 > >SpringMvc in Action——快取資料

SpringMvc in Action——快取資料

小孩子常常會反覆問我一個問題:“為什麼你長的這麼帥啊?”過了一會,又再問一遍。 很多方面來看,在我們所編寫的應用中,有些元件也是這樣的。無狀態的元件一般來講擴充套件性要好一些,但是他們也更傾向於一遍一遍詢問相同的問題。因為他們是無狀態的,一旦完成當前的任務,就會丟棄掉已經獲取到的所有解答。 為了得到問題的答案,我們可能會使用資料庫,呼叫遠端服務,或者執行復雜的計算。 而如果問題的答案變更沒有那麼頻繁或者根本沒有變化,那麼再去走一遍流程是很浪費的,所以我們還不如將問題的答案記住,這就用到了快取Caching。

啟用對快取的支援

Spring對快取的支援有兩種方式:

  • 註解驅動的快取
  • XML宣告的快取

在使用Spring的快取抽象時,最為通用的方法是加上CacheableCacheEvict註解,我們對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);

在這裡插入圖片描述

使用XML宣告快取

在這裡插入圖片描述