SpringCache整合Redis

  之前一篇文章 SpringBoot整合Redis 已經介紹了在SpringBoot中使用redisTemplate手動

操作redis數據庫的方法了。其實這個時候我們就已經可以拿redis來做項目了,需要緩存服務的地方

就引入redisTemplate就行啦。

  但是這裏有個問題,緩存歸根結底不是業務的核心,只是作爲功能和效率上的輔助,如果我現在在某個

項目中用到緩存的地方特別多,那豈不是意味着我需要大量的寫redisTempate的增刪改的方法。而且還需要很多判斷,

比如查詢的時候判斷緩存是否已經存在,如果存在則直接返回,如果不存在則先去查詢關係型數據庫,再將返回的值存入緩存,

在返回給控制器,類似這樣的邏輯要是多了還是比較麻煩,比如下面這段僞代碼。

1 if(cache.has(userId))
2     return cache.get(userId);
3 else
4 {
5   User user = db.get(userId);
6   cache.put(userId, user);
7   return user;              
8 }

  這個時候怎麼辦呢,我們可以很自然的想到Spring的核心之一AOP,它可以幫助我們實現橫切關注點分離。就是把分散在

各段代碼裏面的非業務邏輯給抽取成一個切面,然後在需要切面的方法上加上註解。我們再看上面的場景,不難發現,針對redis

增刪改的操作都可以抽取出來,我們自己只需要編寫必需的業務邏輯,比如我從mysql查詢某個用戶,這段代碼自己實現,然後加上註解

之後,會通過動態代理在我的方法前後加上對應的緩存邏輯。

  說了這麼多,就是讓看官知道此處切面的必要性。那麼可能看官又要問了,我們自己需要去實現這些切面,一個切面還好,要是針對不同的方法

有不同的切面,那也很麻煩啊。不用慌,Spring已經爲我們考慮到了。Spring3.0後提供了Cache和CacheManager等接口,其他緩存服務

可以去實現Spring的接口,然後按Spring的語法來使用,我這裏使用是Redis來和SpringCache做集成。

  關於SpringCache的詳細知識和代碼需要看官自行研究,本文只是淺顯的介紹使用,其原理的實現我也還沒太搞清楚。

 

  下面請看代碼實現:

  開啓Spring對緩存的支持

  @EnableCaching註解

1 @SpringBootApplication
2 @EnableCaching
3 public class RedisApplication {
4 
5     public static void main(String[] args) {
6         SpringApplication.run(RedisApplication.class, args);
7     }
8 
9 }

  

  編寫業務邏輯

  User

 1 package com.example.redis.domain;
 2 
 3 import java.io.Serializable;
 4 
 5 public class User implements Serializable {
 6 
 7     private static final long serialVersionUID = 10000000L;
 8 
 9     private Long id;
10 
11     private String name;
12 
13     private Integer age;
14 
15     public User() {
16 
17     }
18 
19     public User(Long id, String name, Integer age) {
20         this.id = id;
21         this.name = name;
22         this.age = age;
23     }
24 
25     public Long getId() {
26         return id;
27     }
28 
29     public void setId(Long id) {
30         this.id = id;
31     }
32 
33     public String getName() {
34         return name;
35     }
36 
37     public void setName(String name) {
38         this.name = name;
39     }
40 
41     public Integer getAge() {
42         return age;
43     }
44 
45     public void setAge(Integer age) {
46         this.age = age;
47     }
48 
49     @Override
50     public String toString() {
51         return "User{" +
52                 "id=" + id +
53                 ", username='" + name + '\'' +
54                 ", age=" + age +
55                 '}';
56     }
57 
58 }

  

  UserService

 1 package com.example.redis.service;
 2 
 3 import com.example.redis.domain.User;
 4 
 5 public interface UserService {
 6 
 7     /**
 8      * 刪除
 9      *
10      * @param user 用戶對象
11      * @return 操作結果
12      */
13     User saveOrUpdate(User user);
14 
15     /**
16      * 添加
17      *
18      * @param id key值
19      * @return 返回結果
20      */
21     User get(Long id);
22 
23     /**
24      * 刪除
25      *
26      * @param id key值
27      */
28     void delete(Long id);
29 
30 }

 

  UserServiceImpl

 1 package com.example.redis.service;
 2 
 3 import com.example.redis.domain.User;
 4 import org.slf4j.Logger;
 5 import org.slf4j.LoggerFactory;
 6 import org.springframework.cache.annotation.CacheEvict;
 7 import org.springframework.cache.annotation.CachePut;
 8 import org.springframework.cache.annotation.Cacheable;
 9 import org.springframework.stereotype.Service;
10 
11 import java.util.HashMap;
12 import java.util.Map;
13 
14 @Service
15 public class UserServiceImpl implements UserService{
16 
17     private static final Map<Long, User> DATABASES = new HashMap<>();
18 
19     static {
20         DATABASES.put(1L, new User(1L, "張三", 18));
21         DATABASES.put(2L, new User(2L, "李三", 19));
22         DATABASES.put(3L, new User(3L, "王三", 20));
23     }
24 
25     private static final Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
26 
27     @Cacheable(value = "user", unless = "#result == null")
28     @Override
29     public User get(Long id) {
30         // TODO 我們就假設它是從數據庫讀取出來的
31         log.info("進入 get 方法");
32         return DATABASES.get(id);
33     }
34 
35     @CachePut(value = "user")
36     @Override
37     public User saveOrUpdate(User user) {
38         DATABASES.put(user.getId(), user);
39         log.info("進入 saveOrUpdate 方法");
40         return user;
41     }
42 
43     @CacheEvict(value = "user")
44     @Override
45     public void delete(Long id) {
46         DATABASES.remove(id);
47         log.info("進入 delete 方法");
48     }
49 
50 }

  UserServcieImpl這裏面就是重頭戲了,可以看到其中的每個方法上面都有@Cache...註解,我先介紹一下

這些註解是幹嘛的。

  

  我簡單的歸納一下,@Cacheable註解適用於查詢,@CachePut適用於修改和新增,@CacheEvict則適用於刪除。

@Caching呢我還沒用使用,就不說了,也簡單。

  對應到UserServiceImpl中的get,saveOrUpdate,delete三個方法則很好理解了,我描述一下增刪改查的邏輯哈:

  查詢:如果緩存中由則直接返回,如果無則從數據源中拿到,放入緩存中。

  新增和修改:如果緩存中沒有則新增,如果有則修改。

  刪除:如果由則刪除。

  注意我這裏的數據源使用的是Map,因爲電腦上沒有安裝Mysql所以就簡單直接使用map了。

  

  就把map當成一個簡單的mysql數據庫吧。

 

  業務邏輯寫完了還需要配置一下,因爲SpringCache最終操作redis數據庫也要用到redisTemplate,而redisTemplate

默認使用的序列化器是JdkSerializationRedisSerializer,對中文和對象的支持不太友好,所以需要配置下redisTemplate的

序列化器:

 1 package com.example.redis.config;
 2 
 3 import org.springframework.boot.autoconfigure.AutoConfigureAfter;
 4 import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
 5 import org.springframework.context.annotation.Bean;
 6 import org.springframework.context.annotation.Configuration;
 7 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
 8 import org.springframework.data.redis.core.RedisTemplate;
 9 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
10 import org.springframework.data.redis.serializer.StringRedisSerializer;
11 
12 import java.io.Serializable;
13 
14 @Configuration
15 @AutoConfigureAfter(RedisAutoConfiguration.class)
16 public class RedisConfig {
17 
18     /**
19      * 配置自定義redisTemplate
20      */
21     @Bean
22     public RedisTemplate<String, Serializable> redisTemplate(LettuceConnectionFactory redisConnectionFactory) {
23         RedisTemplate<String, Serializable> template = new RedisTemplate<>();
24         template.setKeySerializer(new StringRedisSerializer());
25         template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
26         template.setConnectionFactory(redisConnectionFactory);
27         return template;
28     }
29 
30 }

  然後還需要配置一下CacheManager,主要配置自定義的key的超時時間,是否使用前綴來拼接key,是否允許空值入庫

等等。另外還定義了key的生成器,因爲像@Cacheable(value = "user", unless = "#result == null")這種沒有定義key的,

默認的keyGenerator會使用方法的參數來拼湊一個key出來。拼湊的規則就像下面這樣:

  這樣的話是有問題的,因爲很多方法可能具有相同的參數名,它們生成的key是一樣的,這樣就會取到同一個鍵對象。

所以需要自定義一個keyGenerator。

 1 package com.example.redis.config;
 2 
 3 import com.example.redis.utils.BaseUtil;
 4 import org.slf4j.Logger;
 5 import org.slf4j.LoggerFactory;
 6 import org.springframework.beans.factory.annotation.Autowired;
 7 import org.springframework.cache.CacheManager;
 8 import org.springframework.cache.annotation.CachingConfigurerSupport;
 9 import org.springframework.cache.interceptor.KeyGenerator;
10 import org.springframework.context.annotation.Configuration;
11 import org.springframework.data.redis.cache.RedisCacheConfiguration;
12 import org.springframework.data.redis.cache.RedisCacheManager;
13 import org.springframework.data.redis.cache.RedisCacheWriter;
14 import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
15 import org.springframework.data.redis.core.script.DigestUtils;
16 import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
17 import org.springframework.data.redis.serializer.RedisSerializationContext;
18 import org.springframework.data.redis.serializer.StringRedisSerializer;
19 
20 import java.lang.reflect.Field;
21 import java.lang.reflect.Method;
22 import java.time.Duration;
23 
24 @Configuration
25 //@AutoConfigureAfter(RedisCacheConfiguration.class)
26 public class RedisCacheConfig extends CachingConfigurerSupport {
27 
28     private Logger logger = LoggerFactory.getLogger(RedisCacheConfig.class);
29 
30     @Autowired
31     private LettuceConnectionFactory redisConnectionFactory;
32 
33     @Override
34     public CacheManager cacheManager() {
35         // 重新配置緩存
36         RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
37 
38         //設置緩存的默認超時時間:30分鐘
39         redisCacheConfiguration = redisCacheConfiguration.entryTtl(Duration.ofMinutes(30L))
40                 .disableCachingNullValues()
41                 .disableKeyPrefix()
42                 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
43                 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer((new GenericJackson2JsonRedisSerializer())));
44 
45         return RedisCacheManager.builder(RedisCacheWriter
46                 .nonLockingRedisCacheWriter(redisConnectionFactory))
47                 .cacheDefaults(redisCacheConfiguration).build();
48     }
49 
50     @Override
51     public KeyGenerator keyGenerator(){
52         return new KeyGenerator() {
53             @Override
54             public Object generate(Object target, Method method, Object... params) {
55                 StringBuilder sb = new StringBuilder();
56                 sb.append(target.getClass().getName());
57                 sb.append("&");
58                 for (Object obj : params) {
59                     if (obj != null){
60                         if(!BaseUtil.isBaseType(obj)) {
61                             try {
62                                 Field id = obj.getClass().getDeclaredField("id");
63                                 id.setAccessible(true);
64                                 sb.append(id.get(obj));
65                             } catch (NoSuchFieldException | IllegalAccessException e) {
66                                 logger.error(e.getMessage());
67                             }
68                         } else{
69                             sb.append(obj);
70                         }
71                     }
72                 }
73 
74                 logger.info("redis cache key str: " + sb.toString());
75                 logger.info("redis cache key sha256Hex: " + DigestUtils.sha1DigestAsHex(sb.toString()));
76                 return DigestUtils.sha1DigestAsHex(sb.toString());
77             }
78         };
79     }
80 }

  需要說明的是,這裏的keyGenerator和cacheManager需要各位看官根據自己的業務場景來自行定義,

切勿模仿,我都是亂寫的。

  另外還有配置文件怎麼配置的,很簡單,我直接貼出來:

  application.yml

server:
  port: 80
spring:
  cache:
    type:
      redis
  profile: dev

  application-dev.yml

spring:
  redis:
    host: localhost
    port: 6379
    # 如果使用的jedis 則將lettuce改成jedis即可
    lettuce:
      pool:
        # 最大活躍鏈接數 默認8
        max-active: 8
        # 最大空閒連接數 默認8
        max-idle: 8
        # 最小空閒連接數 默認0
        min-idle: 0

  我本地做測試把配置文件分成了兩個,看官合成一個就可以了。

  好,到此一步,邏輯、Java配置和配置文件都編寫好了,接下來測試程序:

 1 package com.example.redis;
 2 
 3 import com.example.redis.domain.User;
 4 import com.example.redis.service.UserService;
 5 import org.junit.runner.RunWith;
 6 import org.slf4j.Logger;
 7 import org.slf4j.LoggerFactory;
 8 import org.springframework.beans.factory.annotation.Autowired;
 9 import org.springframework.boot.test.context.SpringBootTest;
10 import org.springframework.test.context.junit4.SpringRunner;
11 
12 @RunWith(SpringRunner.class)
13 @SpringBootTest
14 public class Test {
15 
16     private static final Logger log = LoggerFactory.getLogger(Test.class);
17 
18     @Autowired
19     private UserService userService;
20 
21     @org.junit.Test
22     public void contextLoads() {
23         User user = userService.saveOrUpdate(new User(1L, "張三", 21));
24         log.info("[saveOrUpdate] - [{}]", user);
25         final User user1 = userService.get(1L);
26         log.info("[get] - [{}]", user1);
27     }
28 }

  再來看一下數據源裏面有哪些數據:

  

  現在的張三是{id:1, name:'張三',age:18},我對id爲1這條數據修改一下,把age改成了21,按照我們之前的

邏輯此時應該修改完成之後會把修改的結果放入緩存中,那麼下面查詢的時候應該不會走真實的查詢邏輯,而是直接從

緩存裏面取數據,此時查詢不應該輸出log.info("進入 get 方法");這段日誌。

  運行程序看下結果:

  

  從日誌上看是沒啥問題的,再看看redis數據庫:

  

  對的,存的確實是修改過後的值21,所以我們就算是基本成功了。

  需要說明的是,關於SpringCache這塊還有很多需要學習的地方,比如如何配置多個緩存服務,如何配置二級緩存等等,

我也還在學習中。

  本文到此結束,謝謝各位看官。

 

  參考資料

  《Spring實踐》第四版,也就是《Spring In Action》

  https://redis.io/commands

  Spring Data Redis

  https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/integration.html#cache-annotations-cacheable-default-key

  https://docs.spring.io/spring/docs/5.0.5.RELEASE/spring-framework-reference/integration.html#cache-store-configuration

  使用Spring Cache集成Redis

  https://www.jianshu.com/p/6ba2d2dbf36e

  https://www.iteye.com/blog/412887952-qq-com-2397144

  https://www.iteye.com/blog/anhongyang125-2354554

  https://www.cnblogs.com/wsjun/p/9777575.html

  https://www.jianshu.com/p/2a584aaafad3

  

  

 

 

  

  

 

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