前言:
本文只是介紹怎么使用,關於一些源碼的解析,請看另一篇: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 。關於緩存注解的使用,請參考:
6. 完