spring cache 學習——整合 redis 實現聲明式緩存配置


前言:

  本文只是介紹怎么使用,關於一些源碼的解析,請看另一篇:https://www.cnblogs.com/coding-one/p/12373522.html

1. 添加依賴(版本自選)

<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-cache -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-cache</artifactId>
                <version>2.2.4.RELEASE</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis -->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
                <version>2.2.4.RELEASE</version>
            </dependency>

依賴

2. 直接使用

  spring 默認為我們提供了兩個操作 redis 的 bean,分別是:

@Autowired
    RedisTemplate<Object, Object> redisTemplate;
    @Autowired
    StringRedisTemplate stringRedisTemplate;

  所以,我們只需要在需要用到的地方注入即可使用,例子:

@GetMapping("/getById/{id}")
    public Menu getById(@PathVariable("id")String id){
        String menuName = stringRedisTemplate.opsForValue().get("menuName_" + id);
        Menu menu = null;
        if (menuName == null){
            menu = menuService.getById(id);
            menuName = menu.getName();
            stringRedisTemplate.opsForValue().set("menuName_" + id, menuName);
        }
        menu = (Menu)redisTemplate.opsForValue().get("menu_" + id);
        if (menu == null){
            menu = menuService.getById(id);
            redisTemplate.opsForValue().set("menu_" + id, menu);
        }
        return menu;
    }

  2.1. 在測試之前,我們先來看看當前現有的 key:

    

   2.2. 接下來我們請求上面代碼中的接口,結果如下:

    

   2.3. 按我們代碼的邏輯,這個時候 redis 中應該多了兩條記錄,我們來看看:

    name 字段的緩存記錄:

      

 

 

     整個 menu 的緩存記錄:

      

  2.4. 接下來,我們手動把 mysql 中的數據字段改一下,再重新請求一遍,按照代碼邏輯,期望的返回結果應該跟第一次請求完全一樣。

    使用 sqlyog 修改 mysql 中的數據:

      

    重新請求一遍,結果:

      

    可以看到,結果與期望一致。到目前為止,我們證明了“若 redis 緩存中有數據,就不會去數據庫查詢”。接下來我們來證明“若 redis 緩存中沒有數據,則去查詢數據庫”。我們手動刪掉 redis 中上述兩個 key,重新請求一遍,結果:

      

  至此,緩存的功能已經實現了。

 

3. 自定義序列化配置

  從上面例子中 RDM 工具的截圖可以看到,name 字段的鍵值存儲是正常的,但是 menu 對象的鍵值都是亂碼的(准確的說不叫亂碼,而是二進制數據的十六進制表示),這樣的內容可讀性非常差,不友好。我們知道,redis 只支持 string 和基於 string 的幾種集合數據類型,並不能存儲 java 對象。所以我們將 Menu 對象從 redis 中進行存取的操作肯定經過了對象的序列化和反序列化過程。所以十六進制存儲肯定是序列化的結果,所以我們如果想要存儲的內容可讀性強,只需要指定序列化的方式(比如指定為 json 序列化)即可。spring 當然為我們考慮到了,spring 允許我們自定義 RedisTemplate 的 bean,來覆蓋默認的。

  3.1. 我們新建一個配置類,然后使用   @Bean  來創建一個 RedisTemplate 對象,交給容器管理。我們在創建 bean 的時候,可以指定序列化器

@Configuration
public class OnezaiRedisConfig {
    @Bean
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory){
        RedisTemplate<Object, Object> redisTemplate  = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        GenericJackson2JsonRedisSerializer jackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        return redisTemplate;
    }

    @Bean
    public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory){
        StringRedisTemplate redisTemplate  = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        redisTemplate.setKeySerializer(stringRedisSerializer);
        redisTemplate.setValueSerializer(stringRedisSerializer);
        return redisTemplate;
    }

  在這里,我們將 value 序列化器指定為   GenericJackson2JsonRedisSerializer  。這是 spring-data-redis 自帶的一個 json 序列化器。現在我們再來看看新的序列化結果。

  3.2. 刪掉剛剛寫進 redis 的 key,重啟,請求接口,然后查看 redis 中的內容:

    

 

     可以看到,存儲的 key 變為正常的 string ,value 變成了 json。

  這里我們使用的是自帶的序列化器,序列化之后的結果可讀性強了很多。但是加入了 class 信息,而且時間類型使用了時間戳,可讀性還是不夠。如果你還是不滿足,那么可以自定義序列化器,重寫序列化方法(比如使用 fastjson)這里不做例子了。

 

4. 做一個小封裝

  首先我們回頭看一下前面的代碼,我們在使用的時候調用了一個 opsForValue 方法。其實 RedisTemplate 是一個統一的 dao 類,它自己本身只提供 key 的操作方法,但是它內部引用了不同的操作器類(xxxOperations)來分別操作不同的 redis 數據類型,具體的有:

    

  這樣一來我們在調用的時候都需要先調用一個 opsForXxx 方法,未免有些麻煩。

  另外,還需要進行一個強制轉換:Object -> Menu。說實話,我個人非常討厭強制類型轉換的代碼寫法(不知道有沒有同感的。。。)

  所以我們可以寫一個統一的分裝類,提供一些常用的方法,將這些 opsForXxx 和 強轉代碼放在這些方法里面完成,一勞永逸。如:

public class RedisService {
    private StringRedisTemplate stringRedisTemplate;
    private RedisTemplate redisTemplate;

    public RedisService(StringRedisTemplate stringRedisTemplate, RedisTemplate redisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
        this.redisTemplate = redisTemplate;
    }
    // -------- key 相關命令 ------------------
    Long del(String... keys){
        return redisTemplate.delete(Arrays.asList(keys));
    }

    public Boolean exists(String key){
        return redisTemplate.hasKey(key);
    }

    public Boolean expire(String key, Long seconds){
        return redisTemplate.expire(key, seconds, TimeUnit.SECONDS);
    }

    public Boolean expire(String key, Date date){
        return redisTemplate.expireAt(key, date);
    }

    public Set<String> keys(String pattern){
        return redisTemplate.keys(pattern);
    }

    public String randomKey(){
        return (String)redisTemplate.randomKey();
    }

    public void rename(String oldKey, String newKey){
        redisTemplate.rename(oldKey, newKey);
    }

    public DataType type(String key){
        return redisTemplate.type(key);
    }

    // -------- string 相關命令 -----------
    public void set(String key, Object object){
        if (object instanceof String){
            stringRedisTemplate.opsForValue().set(key, (String) object);
        }else {
            redisTemplate.opsForValue().set(key, object);
        }
    }

    public String get(String key){
        return stringRedisTemplate.opsForValue().get(key);
    }

    public <T> T get(String key, Class<T> clazz){
        return (T)redisTemplate.opsForValue().get(key);
    }

    public <T> List<T> getList(String key, Class<T> clazz){
        return (List<T>)redisTemplate.opsForValue().get(key);
    }

    public <T> T getAndSet(String key, Object object, Class<T> clazz){
        return (T) redisTemplate.opsForValue().getAndSet(key, object);
    }

    public List<Object> multiGet(String... keys){
        return redisTemplate.opsForValue().multiGet(Arrays.asList(keys));
    }

    public void multiSet(Map<String, Object> map){
        redisTemplate.opsForValue().multiSet(map);
    }

    // ---------- list 相關命令 ------------------
}

  我這里只寫了一部分,准備以后用到的時候再補充,畢竟這里只是一個例子。但是思路就是這么個思路。

 

5. 聲明式緩存調用

  我們都知道,所有的緩存代碼,都有一個統一的格式,用偽碼可以這么表示:

data = getDataFromCache;
if (data is empty){
    data = getDataFromDB;
    if(data isNot empty){
        saveDataToCache;
    }
}

  在這個格式中,具體的業務相關的就只有   data = getDataFromDB;  這一行代碼。其它的都是重復的。既然如此,為什么不將那些重復的代碼封裝起來,然后提供一個注解聲明,當調用這行代碼時,就自動執行緩存代碼的邏輯呢?spring 已經幫我們做好了這件事。

  spring 提供了一套聲明式緩存的接口,讓我們只需要添加相關的依賴,配置緩存的第三方工具,然后在方法上添加注解,就可以使用注解。具體使用方法:

  5.1. 添加依賴

    文章開頭已經給出了依賴的代碼。

  5.2. 啟用緩存

    在啟動類添加   @EnableCaching  ,以啟用緩存。

  5.3. 編寫需要使用緩存的方法

@CacheConfig(cacheNames = "onezai-cloud-menuCache")
@Service
public class MenuService extends BaseService<MenuMapper, Menu> implements MenuIService {

    @Cacheable(key = "'getAll'")
    @Override
    public List<Menu> getAll() {
        return this.list();
    }
}

    在這里,我們在 MenuService 中編寫一個 getAll 方法,該方法調用 list() 方法,無條件查詢所有菜單(list() 是 mybatis-plus-generator 自動生成的,用過的朋友應該知道)。這里使用了兩個注解:

     @CacheConfig :類級別的注解。指定該類所有方法上的緩存注解默認緩存名都是 cacheName (本例中即“onezai-cloud-menuCache”),緩存名可以理解為對一類緩存的一個集合(比如講菜單相關數據的緩存放到一起);

     @Cacheable :方法級別注解。key 指定緩存的 key ,redis 是鍵值存儲的,這里特就是指定鍵。不過該注解同樣可以指定 cacheName,如果 CacheConfig 和 Cacheable 都指定了,則以顆粒度小的為准,即以 Cacheable 為准。需要注意的是,key 需要使用 spEL 表達式,所以也支持根據參數來組合生成 key;

    另外,還有一些其它注解: @CacheEvict   @CachePut   @Caching   。關於緩存注解的使用,請參考:

      spring cache 學習——緩存注解使用

 

6. 完

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM