分佈式專題(十四):Spring Boot使用註解集成Redis緩存

爲了提高性能,減少數據庫的壓力,使用緩存是非常好的手段之一。本文,講解 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 作爲緩存技術。

以下是我們不引入其他緩存依賴情況下,控制檯打印的日誌信息。

  1. Bean 'cacheManager' of type [class org.springframework.cache.concurrent.ConcurrentMapCacheManager]

實戰演練

Maven 依賴

首先,我們先創建一個 POM 文件。

  1. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  2. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  3. <modelVersion>4.0.0</modelVersion>
  4.  
  5. <parent>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-parent</artifactId>
  8. <version>1.3.3.RELEASE</version>
  9. </parent>
  10.  
  11. <groupId>com.lianggzone.demo</groupId>
  12. <artifactId>springboot-action-cache</artifactId>
  13. <version>0.1</version>
  14. <packaging>jar</packaging>
  15. <name>springboot-action-cache</name>
  16.  
  17. <dependencies>
  18. <dependency>
  19. <groupId>org.springframework.boot</groupId>
  20. <artifactId>spring-boot-starter</artifactId>
  21. </dependency>
  22. <dependency>
  23. <groupId>org.springframework.boot</groupId>
  24. <artifactId>spring-boot-starter-web</artifactId>
  25. </dependency>
  26.  
  27. <dependency>
  28. <groupId>org.springframework.boot</groupId>
  29. <artifactId>spring-boot-starter-cache</artifactId>
  30. </dependency>
  31. </dependencies>
  32. <build>
  33. <plugins>
  34. <plugin>
  35. <groupId>org.apache.maven.plugins</groupId>
  36. <artifactId>maven-compiler-plugin</artifactId>
  37. <configuration>
  38. <defaultLibBundleDir>lib</defaultLibBundleDir>
  39. <source>1.7</source>
  40. <target>1.7</target>
  41. <encoding>UTF-8</encoding>
  42. </configuration>
  43. </plugin>
  44. <plugin>
  45. <groupId>org.apache.maven.plugins</groupId>
  46. <artifactId>maven-resources-plugin</artifactId>
  47. <configuration>
  48. <encoding>UTF-8</encoding>
  49. <useDefaultDelimiters>false</useDefaultDelimiters>
  50. <escapeString>\</escapeString>
  51. <delimiters>
  52. <delimiter>${*}</delimiter>
  53. </delimiters>
  54. </configuration>
  55. </plugin>
  56. <plugin>
  57. <groupId>org.springframework.boot</groupId>
  58. <artifactId>spring-boot-maven-plugin</artifactId>
  59. </plugin>
  60. </plugins>
  61. </build>
  62. </project>

其中,最核心的是添加 spring-boot-starter-cache 依賴。

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-cache</artifactId>
  4. </dependency>

開啓緩存支持

在 Spring Boot 中使用 @EnableCaching 開啓緩存支持。

  1. @Configuration
  2. @EnableCaching
  3. public class CacheConfiguration {}

服務層

創建一個服務類

  1. @Service("concurrenmapcache.cacheService")
  2. public class CacheService {
  3.  
  4. }

首先,我們先來講解下 @Cacheable 註解。@Cacheable 在方法執行前 Spring 先查看緩存中是否有數據,如果有數據,則直接返回緩存數據;若沒有數據,調用方法並將方法返回值放進緩存。有兩個重要的值, value,返回的內容將存儲在 value 定義的緩存的名字對象中。key,如果不指定將使用默認的 KeyGenerator 生成。

我們在查詢方法上,添加 @Cacheable 註解,其中緩存名稱爲 concurrenmapcache。

  1. @Cacheable(value = "concurrenmapcache")
  2. public long getByCache() {
  3. try {
  4. Thread.sleep(3 * 1000);
  5. } catch (InterruptedException e) {
  6. e.printStackTrace();
  7. }
  8. return new Timestamp(System.currentTimeMillis()).getTime();
  9. }

@CachePut 與 @Cacheable 類似,但是它無論什麼情況,都會將方法的返回值放到緩存中, 主要用於數據新增和修改方法。

  1. @CachePut(value = "concurrenmapcache")
  2. public long save() {
  3. long timestamp = new Timestamp(System.currentTimeMillis()).getTime();
  4. System.out.println("進行緩存:" + timestamp);
  5. return timestamp;
  6. }

@CacheEvict 將一條或多條數據從緩存中刪除, 主要用於刪除方法,用來從緩存中移除相應數據。

  1. @CacheEvict(value = "concurrenmapcache")
  2. public void delete() {
  3. System.out.println("刪除緩存");
  4. }

控制層

爲了展現效果,我們先定義一組簡單的 RESTful API 接口進行測試。

  1. @RestController("concurrenmapcache.cacheController")
  2. @RequestMapping(value = "/concurrenmapcache/cache")
  3. public class CacheController {
  4. @Autowired
  5. private CacheService cacheService;
  6.  
  7. /**
  8. * 查詢方法
  9. */
  10. @RequestMapping(value = "", method = RequestMethod.GET)
  11. public String getByCache() {
  12. Long startTime = System.currentTimeMillis();
  13. long timestamp = this.cacheService.getByCache();
  14. Long endTime = System.currentTimeMillis();
  15. System.out.println("耗時: " + (endTime - startTime));
  16. return timestamp+"";
  17. }
  18.  
  19. /**
  20. * 保存方法
  21. */
  22. @RequestMapping(value = "", method = RequestMethod.POST)
  23. public void save() {
  24. this.cacheService.save();
  25. }
  26.  
  27. /**
  28. * 刪除方法
  29. */
  30. @RequestMapping(value = "", method = RequestMethod.DELETE)
  31. public void delete() {
  32. this.cacheService.delete();
  33. }
  34. }

運行

  1. @RestController
  2. @EnableAutoConfiguration
  3. @ComponentScan(basePackages = { "com.lianggzone.springboot" })
  4. public class WebMain {
  5.  
  6. public static void main(String[] args) throws Exception {
  7. SpringApplication.run(WebMain.class, args);
  8. }
  9. }

課後作業

我們分爲幾個場景進行測試。

  • 多次調用查詢接口,查看緩存信息是否變化,控制檯日誌是否如下?你得到的結論是什麼?
  • 調用保存接口,再調用查詢接口,查看緩存信息是否變化?你得到的結論是什麼?
  • 調用刪除接口,再調用查詢接口,接口響應是否變慢了?你再看看控制檯日誌,你得到的結論是什麼?

擴展閱讀

如果想更深入理解 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),

  • 需要引入的starterspring-boot-starter-data-redis,spring-boot-starter-cache;
  • 自動配置生成的Beans: RedisConnectionFactoryStringRedisTemplate , RedisTemplateRedisCacheManager,自動配置的Bean可以直接注入我們的代碼中使用;

I. 配置

application.properties

  1. # REDIS (RedisProperties)
  2. spring.redis.host=localhost # Redis server host.
  3. spring.redis.port=6379 # Redis server port.
  4. spring.redis.password= # Login password of the redis server.

具體對Redis cluster或者Sentinel的配置可以參考這裏

開啓緩存支持

  1. @SpringBootApplication
  2. @EnableCaching//開啓caching
  3. public class NewsWebServer {
  4. //省略內容
  5. }

定製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:

  1. //通用的RedisTemplate
  2. @Bean
  3. public RedisTemplate<String, Object> redisTemplate(JedisConnectionFactory jedisConnectionFactory) {
  4. RedisTemplate<String, Object> template = new RedisTemplate<>();
  5. template.setConnectionFactory(jedisConnectionFactory);
  6. template.setKeySerializer(new StringRedisSerializer());
  7. template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
  8. //template.setHashKeySerializer(template.getKeySerializer());
  9. //template.setHashValueSerializer(template.getValueSerializer());
  10. return template;
  11. }

這裏我們使用GenericJackson2JsonRedisSerializer而不是Jackson2JsonRedisSerializer,後者的問題是你需要爲每一個需要序列化進Redis的類指定一個Jackson2JsonRedisSerializer因爲其構造函數中需要指定一個類型來做反序列化:

redis.setValueSerializer(new Jackson2JsonRedisSerializer<Product>(Product.class));

如果我們應用中有大量對象需要緩存,這顯然是不合適的,而前者直接把類型信息序列化到了JSON格式中,讓一個實例可以操作多個對象的反序列化。

定製RedisCacheManager

有時候Spring Boot自動給我們配置的RedisCacheManager也不能滿足我們應用的需求,我看到很多用法都直接聲明瞭一個自己的RedisCacheManager,其實使用CacheManagerCustomizer可以對自動配置的RedisCacheManager進行定製化:

  1. @Bean
  2. public CacheManagerCustomizer<RedisCacheManager> cacheManagerCustomizer() {
  3. return new CacheManagerCustomizer<RedisCacheManager>() {
  4. @Override
  5. public void customize(RedisCacheManager cacheManager) {
  6. cacheManager.setUsePrefix(true); //事實上這是Spring Boot的默認設置,爲了避免key衝突
  7. Map<String, Long> expires = new HashMap<>();
  8. expires.put("myLittleCache", 12L*60*60); // 設置過期時間 key is cache-name
  9. expires.put("myBiggerCache", 24L*60*60);
  10. cacheManager.setExpires(expires); // expire per cache
  11. cacheManager.setDefaultExpiration(24*60*60);// 默認過期時間:24 hours
  12. }
  13. };
  14. }

II. 使用

緩存Key的生成

我們都知道Redis是一個key-value的存儲系統,無論我們想要緩存什麼值,都需要制定一個key。

  1. @Cacheable(cacheNames = "user")
  2. public User findById(long id) {
  3. return userMapper.findById(id);
  4. }

上面的代碼中,findById方法返回的對象會被緩存起來,key由默認的org.springframework.cache.interceptor.SimpleKeyGenerator生成,生成策略是根據被標註方法的參數生成一個SimpleKey對象,然後由RedisTemplate中定義的KeySerializer序列化後作爲key(注意StringRedisSerializer只能序列化String類型,對SimpleKey對象無能爲力,你只能定義其他Serializer)。

不過大多數情況下我們都會採用自己的key生成方案,方式有兩種:

1.實現自己的KeyGenerator;

  1. @Configuration
  2. @EnableCaching
  3. public class CacheConfig extends CachingConfigurerSupport {
  4. @Bean
  5. public KeyGenerator customKeyGenerator() {
  6. return new KeyGenerator() {
  7. @Override
  8. public Object generate(Object o, Method method, Object... objects) {
  9. StringBuilder sb = new StringBuilder();
  10. sb.append(o.getClass().getName());
  11. sb.append(method.getName());
  12. for (Object obj : objects) {
  13. sb.append(obj.toString());
  14. }
  15. return sb.toString();
  16. }
  17. };
  18. }
  19. }

2.在@Cacheable標註中直接聲明key:

  1. @Cacheable(cacheNames = "user", key="#id.toString()") ❶
  2. public User findById(long id) {
  3. return userMapper.findById(id);
  4. }
  5. @Cacheable(cacheNames = "user", key="'admin'") ❷
  6. public User findAdmin() {
  7. return userMapper.findAdminUser();
  8. }
  9. @Cacheable(cacheNames = "user", key="#userId + ':address'") ❸
  10. public List<Address> findUserAddress(long userId) {
  11. return userMapper.findUserAddress(userId);
  12. }

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

  1. @Service
  2. public class FeedService {
  3. @Resource(name="redisTemplate") ❶
  4. private ZSetOperations<String, Feed> feedOp;
  5. public List<Feed> getFeed(int count, long maxId) {
  6. return new ArrayList<>(feedOp.reverseRangeByScore(FEED_CACHE_KEY, 0, maxId, offset, count));
  7. }
  8. //省略
  9. }

❶ 我們可以直接把RedisTemplate的實例注入爲ZSetOperationsListOperationsValueOperations等類型(Spring IoC Container幫我們做了轉化工作,可以參考org.springframework.data.redis.core.ZSetOperationsEditor)。

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