爲了提高性能,減少數據庫的壓力,使用緩存是非常好的手段之一。本文,講解 Spring Boot 如何集成緩存管理。
Spring註解緩存
Spring 3.1之後,引入了註解緩存技術,其本質上不是一個具體的緩存實現方案,而是一個對緩存使用的抽象,通過在既有代碼中添加少量自定義的各種annotation,即能夠達到使用緩存對象和緩存方法的返回對象的效果。Spring的緩存技術具備相當的靈活性,不僅能夠使用SpEL(Spring Expression Language)來定義緩存的key和各種condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存集成。其特點總結如下:
- 少量的配置annotation註釋即可使得既有代碼支持緩存;
- 支持開箱即用,不用安裝和部署額外的第三方組件即可使用緩存;
- 支持Spring Express Language(SpEL),能使用對象的任何屬性或者方法來定義緩存的key和使用規則條件;
- 支持自定義key和自定義緩存管理者,具有相當的靈活性和可擴展性。
和Spring的事務管理類似,Spring Cache的關鍵原理就是Spring AOP,通過Spring AOP實現了在方法調用前、調用後獲取方法的入參和返回值,進而實現了緩存的邏輯。而Spring Cache利用了Spring AOP的動態代理技術,即當客戶端嘗試調用pojo的foo()方法的時候,給它的不是pojo自身的引用,而是一個動態生成的代理類。
圖12 Spring動態代理調用圖
如圖12所示,實際客戶端獲取的是一個代理的引用,在調用foo()方法的時候,會首先調用proxy的foo()方法,這個時候proxy可以整體控制實際的pojo.foo()方法的入參和返回值,比如緩存結果,比如直接略過執行實際的foo()方法等,都是可以輕鬆做到的。Spring Cache主要使用三個註釋標籤,即@Cacheable、@CachePut和@CacheEvict,主要針對方法上註解使用,部分場景也可以直接類上註解使用,當在類上使用時,該類所有方法都將受影響。我們總結一下其作用和配置方法,如表1所示。
表1
標籤類型 | 作用 | 主要配置參數說明 |
---|---|---|
@Cacheable | 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存 | value:緩存的名稱,在 Spring 配置文件中定義,必須指定至少一個; key:緩存的 key,可以爲空,如果指定要按照 SpEL 表達式編寫,如果不指定,則默認按照方法的所有參數進行組合; condition:緩存的條件,可以爲空,使用 SpEL 編寫,返回 true 或者 false,只有爲 true 才進行緩存 |
@CachePut | 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用 | value:緩存的名稱,在 spring 配置文件中定義,必須指定至少一個; key:緩存的 key,可以爲空,如果指定要按照 SpEL 表達式編寫,如果不指定,則默認按照方法的所有參數進行組合; condition:緩存的條件,可以爲空,使用 SpEL 編寫,返回 true 或者 false,只有爲 true 才進行緩存 |
@CacheEvict | 主要針對方法配置,能夠根據一定的條件對緩存進行清空 | value:緩存的名稱,在 Spring 配置文件中定義,必須指定至少一個; key:緩存的 key,可以爲空,如果指定要按照 SpEL 表達式編寫,如果不指定,則默認按照方法的所有參數進行組合; condition:緩存的條件,可以爲空,使用 SpEL 編寫,返回 true 或者 false,只有爲 true 才進行緩存; allEntries:是否清空所有緩存內容,默認爲 false,如果指定爲 true,則方法調用後將立即清空所有緩存; beforeInvocation:是否在方法執行前就清空,默認爲 false,如果指定爲 true,則在方法還沒有執行的時候就清空緩存,默認情況下,如果方法執行拋出異常,則不會清空緩存 |
可擴展支持:Spring註解cache能夠滿足一般應用對緩存的需求,但隨着應用服務的複雜化,大併發高可用性能要求下,需要進行一定的擴展,這時對其自身集成的緩存方案可能不太適用,該怎麼辦?Spring預先有考慮到這點,那麼怎樣利用Spring提供的擴展點實現我們自己的緩存,且在不改變原來已有代碼的情況下進行擴展?是否在方法執行前就清空,默認爲false,如果指定爲true,則在方法還沒有執行的時候就清空緩存,默認情況下,如果方法執行拋出異常,則不會清空緩存。
這基本能夠滿足一般應用對緩存的需求,但現實總是很複雜,當你的用戶量上去或者性能跟不上,總需要進行擴展,這個時候你或許對其提供的內存緩存不滿意了,因爲其不支持高可用性,也不具備持久化數據能力,這個時候,你就需要自定義你的緩存方案了,還好,Spring也想到了這一點。
我們先不考慮如何持久化緩存,畢竟這種第三方的實現方案很多,我們要考慮的是,怎麼利用Spring提供的擴展點實現我們自己的緩存,且在不改原來已有代碼的情況下進行擴展。這需要簡單的三步驟,首先需要提供一個CacheManager接口的實現(繼承至AbstractCacheManager),管理自身的cache實例;其次,實現自己的cache實例MyCache(繼承至Cache),在這裏面引入我們需要的第三方cache或自定義cache;最後就是對配置項進行聲明,將MyCache實例注入CacheManager進行統一管理。
聲明式緩存
Spring 定義 CacheManager 和 Cache 接口用來統一不同的緩存技術。例如 JCache、 EhCache、 Hazelcast、 Guava、 Redis 等。在使用 Spring 集成 Cache 的時候,我們需要註冊實現的 CacheManager 的 Bean。
Spring Boot默認集成CacheManager
Spring Boot 爲我們自動配置了多個 CacheManager 的實現。
Spring Boot 爲我們自動配置了 JcacheCacheConfiguration、 EhCacheCacheConfiguration、HazelcastCacheConfiguration、GuavaCacheConfiguration、RedisCacheConfiguration、SimpleCacheConfiguration 等。
默認的 ConcurrenMapCacheManager
Spring 從 Spring3.1 開始基於 java.util.concurrent.ConcurrentHashMap 實現的緩存管理器。所以, Spring Boot 默認使用 ConcurrentMapCacheManager 作爲緩存技術。
以下是我們不引入其他緩存依賴情況下,控制檯打印的日誌信息。
- Bean 'cacheManager' of type [class org.springframework.cache.concurrent.ConcurrentMapCacheManager]
實戰演練
Maven 依賴
首先,我們先創建一個 POM 文件。
- <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
- <modelVersion>4.0.0</modelVersion>
- <parent>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-parent</artifactId>
- <version>1.3.3.RELEASE</version>
- </parent>
- <groupId>com.lianggzone.demo</groupId>
- <artifactId>springboot-action-cache</artifactId>
- <version>0.1</version>
- <packaging>jar</packaging>
- <name>springboot-action-cache</name>
- <dependencies>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-web</artifactId>
- </dependency>
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
- </dependencies>
- <build>
- <plugins>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-compiler-plugin</artifactId>
- <configuration>
- <defaultLibBundleDir>lib</defaultLibBundleDir>
- <source>1.7</source>
- <target>1.7</target>
- <encoding>UTF-8</encoding>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.apache.maven.plugins</groupId>
- <artifactId>maven-resources-plugin</artifactId>
- <configuration>
- <encoding>UTF-8</encoding>
- <useDefaultDelimiters>false</useDefaultDelimiters>
- <escapeString>\</escapeString>
- <delimiters>
- <delimiter>${*}</delimiter>
- </delimiters>
- </configuration>
- </plugin>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
- </project>
其中,最核心的是添加 spring-boot-starter-cache 依賴。
- <dependency>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-starter-cache</artifactId>
- </dependency>
開啓緩存支持
在 Spring Boot 中使用 @EnableCaching 開啓緩存支持。
- @Configuration
- @EnableCaching
- public class CacheConfiguration {}
服務層
創建一個服務類
- @Service("concurrenmapcache.cacheService")
- public class CacheService {
- }
首先,我們先來講解下 @Cacheable 註解。@Cacheable 在方法執行前 Spring 先查看緩存中是否有數據,如果有數據,則直接返回緩存數據;若沒有數據,調用方法並將方法返回值放進緩存。有兩個重要的值, value,返回的內容將存儲在 value 定義的緩存的名字對象中。key,如果不指定將使用默認的 KeyGenerator 生成。
我們在查詢方法上,添加 @Cacheable 註解,其中緩存名稱爲 concurrenmapcache。
- @Cacheable(value = "concurrenmapcache")
- public long getByCache() {
- try {
- Thread.sleep(3 * 1000);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- return new Timestamp(System.currentTimeMillis()).getTime();
- }
@CachePut 與 @Cacheable 類似,但是它無論什麼情況,都會將方法的返回值放到緩存中, 主要用於數據新增和修改方法。
- @CachePut(value = "concurrenmapcache")
- public long save() {
- long timestamp = new Timestamp(System.currentTimeMillis()).getTime();
- System.out.println("進行緩存:" + timestamp);
- return timestamp;
- }
@CacheEvict 將一條或多條數據從緩存中刪除, 主要用於刪除方法,用來從緩存中移除相應數據。
- @CacheEvict(value = "concurrenmapcache")
- public void delete() {
- System.out.println("刪除緩存");
- }
控制層
爲了展現效果,我們先定義一組簡單的 RESTful API 接口進行測試。
- @RestController("concurrenmapcache.cacheController")
- @RequestMapping(value = "/concurrenmapcache/cache")
- public class CacheController {
- @Autowired
- private CacheService cacheService;
- /**
- * 查詢方法
- */
- @RequestMapping(value = "", method = RequestMethod.GET)
- public String getByCache() {
- Long startTime = System.currentTimeMillis();
- long timestamp = this.cacheService.getByCache();
- Long endTime = System.currentTimeMillis();
- System.out.println("耗時: " + (endTime - startTime));
- return timestamp+"";
- }
- /**
- * 保存方法
- */
- @RequestMapping(value = "", method = RequestMethod.POST)
- public void save() {
- this.cacheService.save();
- }
- /**
- * 刪除方法
- */
- @RequestMapping(value = "", method = RequestMethod.DELETE)
- public void delete() {
- this.cacheService.delete();
- }
- }
運行
- @RestController
- @EnableAutoConfiguration
- @ComponentScan(basePackages = { "com.lianggzone.springboot" })
- public class WebMain {
- public static void main(String[] args) throws Exception {
- SpringApplication.run(WebMain.class, args);
- }
- }
課後作業
我們分爲幾個場景進行測試。
- 多次調用查詢接口,查看緩存信息是否變化,控制檯日誌是否如下?你得到的結論是什麼?
- 調用保存接口,再調用查詢接口,查看緩存信息是否變化?你得到的結論是什麼?
- 調用刪除接口,再調用查詢接口,接口響應是否變慢了?你再看看控制檯日誌,你得到的結論是什麼?
擴展閱讀
如果想更深入理解 Spring 的 Cache 機制,這邊推薦兩篇不錯的文章。
源代碼
Spring Boot In Practice:Redis緩存實戰
閱讀本文需要對Spring和Redis比較熟悉。
Spring Framework 提供了Cache Abstraction對緩存層進行了抽象封裝,通過幾個annotation可以透明給您的應用增加緩存支持,而不用去關心底層緩存具體由誰實現。目前支持的緩存有java.util.concurrent.ConcurrentMap
,Ehcache 2.x,Redis等。
一般我們使用最常用的Redis做爲緩存實現(Spring Data Redis),
- 需要引入的starter:
spring-boot-starter-data-redis
,spring-boot-starter-cache
; - 自動配置生成的Beans:
RedisConnectionFactory
,StringRedisTemplate
,RedisTemplate
,RedisCacheManager
,自動配置的Bean可以直接注入我們的代碼中使用;
I. 配置
application.properties
- # REDIS (RedisProperties)
- spring.redis.host=localhost # Redis server host.
- spring.redis.port=6379 # Redis server port.
- spring.redis.password= # Login password of the redis server.
具體對Redis cluster或者Sentinel的配置可以參考這裏
開啓緩存支持
- @SpringBootApplication
- @EnableCaching//開啓caching
- public class NewsWebServer {
- //省略內容
- }
定製RedisTemplate
自動配置的RedisTemplate
並不能滿足大部分項目的需求,比如我們基本都需要設置特定的Serializer
(RedisTemplate默認會使用JdkSerializationRedisSerializer
)。
Redis底層中存儲的數據只是字節。雖然Redis本身支持各種類型(List, Hash等),但在大多數情況下,這些指的是數據的存儲方式,而不是它所代表的內容(內容都是byte)。用戶自己來決定數據如何被轉換成String或任何其他對象。用戶(自定義)類型和原始數據類型之間的互相轉換通過RedisSerializer接口(包org.springframework.data.redis.serializer)來處理,顧名思義,它負責處理序列化/反序列化過程。多個實現可以開箱即用,如:StringRedisSerializer和JdkSerializationRedisSerialize。Jackson2JsonRedisSerializer或GenericJackson2JsonRedisSerializer來處理JSON格式的數據。請注意,存儲格式不僅限於value 它可以用於key,Hash的key和value。
聲明自己的RedisTemplate
覆蓋掉自動配置的Bean:
- //通用的RedisTemplate
- @Bean
- public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
- RedisTemplate<String, Object> template = new RedisTemplate<>();
- template.setConnectionFactory(jedisConnectionFactory);
- template.setKeySerializer(new StringRedisSerializer());
- template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
- //template.setHashKeySerializer(template.getKeySerializer());
- //template.setHashValueSerializer(template.getValueSerializer());
- return template;
- }
這裏我們使用GenericJackson2JsonRedisSerializer
而不是Jackson2JsonRedisSerializer
,後者的問題是你需要爲每一個需要序列化進Redis的類指定一個Jackson2JsonRedisSerializer
因爲其構造函數中需要指定一個類型來做反序列化:
redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));
如果我們應用中有大量對象需要緩存,這顯然是不合適的,而前者直接把類型信息序列化到了JSON格式中,讓一個實例可以操作多個對象的反序列化。
定製RedisCacheManager
有時候Spring Boot自動給我們配置的RedisCacheManager
也不能滿足我們應用的需求,我看到很多用法都直接聲明瞭一個自己的RedisCacheManager,其實使用CacheManagerCustomizer
可以對自動配置的RedisCacheManager進行定製化:
- @Bean
- public CacheManagerCustomizer<RedisCacheManager> cacheManagerCustomizer() {
- return new CacheManagerCustomizer<RedisCacheManager>() {
- @Override
- public void customize(RedisCacheManager cacheManager) {
- cacheManager.setUsePrefix(true); //事實上這是Spring Boot的默認設置,爲了避免key衝突
-
- Map<String, Long> expires = new HashMap<>();
- expires.put("myLittleCache", 12L*60*60); // 設置過期時間 key is cache-name
- expires.put("myBiggerCache", 24L*60*60);
- cacheManager.setExpires(expires); // expire per cache
-
- cacheManager.setDefaultExpiration(24*60*60);// 默認過期時間:24 hours
- }
- };
- }
II. 使用
緩存Key的生成
我們都知道Redis是一個key-value的存儲系統,無論我們想要緩存什麼值,都需要制定一個key。
- @Cacheable(cacheNames = "user")
- public User findById(long id) {
- return userMapper.findById(id);
- }
上面的代碼中,findById
方法返回的對象會被緩存起來,key由默認的org.springframework.cache.interceptor.SimpleKeyGenerator
生成,生成策略是根據被標註方法的參數生成一個SimpleKey
對象,然後由RedisTemplate
中定義的KeySerializer序列化後作爲key(注意StringRedisSerializer
只能序列化String類型,對SimpleKey
對象無能爲力,你只能定義其他Serializer)。
不過大多數情況下我們都會採用自己的key生成方案,方式有兩種:
1.實現自己的KeyGenerator;
- @Configuration
- @EnableCaching
- public class CacheConfig extends CachingConfigurerSupport {
- @Bean
- public KeyGenerator customKeyGenerator() {
- return new KeyGenerator() {
- @Override
- public Object generate(Object o, Method method, Object... objects) {
- StringBuilder sb = new StringBuilder();
- sb.append(o.getClass().getName());
- sb.append(method.getName());
- for (Object obj : objects) {
- sb.append(obj.toString());
- }
- return sb.toString();
- }
- };
- }
- }
2.在@Cacheable
標註中直接聲明key:
- @Cacheable(cacheNames = "user", key="#id.toString()") ❶
- public User findById(long id) {
- return userMapper.findById(id);
- }
-
- @Cacheable(cacheNames = "user", key="'admin'") ❷
- public User findAdmin() {
- return userMapper.findAdminUser();
- }
-
- @Cacheable(cacheNames = "user", key="#userId + ':address'") ❸
- public List<Address> findUserAddress(long userId) {
- return userMapper.findUserAddress(userId);
- }
key的聲明形式支持SpEL。
❶ 最終生成的Redis key爲:user:100234
,user部分是因爲cacheManager.setUsePrefix(true)
,cacheName會被添加到key作爲前綴避免引起key的衝突。之所以#id.toString()
要long型轉爲String是因爲我們設置的KeySerializer爲StringRedisSerializer
只能用來序列化String。
❷ 如果被標註方法沒有參數,我們可以用一個靜態的key值,最終生成的key爲user:admin
。
❸ 最終生成的key爲user:100234:address
。
這種方式更符合我們以前使用Redis的習慣,所以推薦。
直接使用RedisTemplate
有時候標註不能滿足我們的使用場景,我們想要直接使用更底層的RedisTemplate
。
- @Service
- public class FeedService {
-
- @Resource(name="redisTemplate") ❶
- private ZSetOperations<String, Feed> feedOp;
-
- public List<Feed> getFeed(int count, long maxId) {
- return new ArrayList<>(feedOp.reverseRangeByScore(FEED_CACHE_KEY, 0, maxId, offset, count));
- }
- //省略
- }
❶ 我們可以直接把RedisTemplate的實例注入爲ZSetOperations
、ListOperations
、ValueOperations
等類型(Spring IoC Container幫我們做了轉化工作,可以參考org.springframework.data.redis.core.ZSetOperationsEditor
)。