一.簡述
Spring從3.1開始定義了
org.springframework.cache.Cache 和
org.springframework.cache.CacheManager接口來統一不同的緩存技術
自然SpringBoot 也提供了支持
二.環境搭建
- 創建一個SpringBoot 項目,引入下面這些依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 在啓動類上加上@EnableCaching註解,表示開啓基於註解的緩存
- 編寫配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_cache?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
#mybatis:
# configuration:
# # 打印sql日誌
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 打印sql日誌
logging:
level:
com.xx.mapper: debug
- 創建測試用的表
CREATE TABLE `tb_user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(10) DEFAULT NULL,
`password` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
- 編寫Mapper層接口代碼,這裏不做解釋,直接拿去用就好
package com.xx.mapper;
import com.xx.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
/**
* @author aqi
* DateTime: 2020/6/30 1:40 下午
* Description: No Description
*/
@Mapper
public interface UserMapper {
@Select("SELECT * FROM tb_user WHERE id = #{id}")
User getUserById(int id);
@Insert("INSERT INTO tb_user (username, password) VALUES (#{username}, #{password})")
void addUser(User user);
@Update("UPDATE tb_user SET username = #{username}, password = #{password} where id = #{id}")
void modUser(User user);
}
三.緩存相關的註解
- SpringBoot 提供的有關緩存的註解,這些註解既可以作用在方法上也可以作用在類上
註解名稱 | 註解作用 | 說明 |
---|---|---|
@Cacheable | 添加緩存 | 將方法的返回值存到緩存中,方法執行之前先去查詢是否存在緩存,若存在則不執行方法,反之執行方法 |
@CacheEvict | 清除緩存 | 根據指定的key去清除緩存,也可以清除所有的緩存 |
@CachePut | 更新緩存 | 每次執行都會執行方法,並且修改緩存中的數據 |
@CacheConfig | 緩存的全局配置,抽取公共的配置信息 | 將一些相同的配置信息寫在類上 |
@Caching | 複雜緩存 | 可以配置多個緩存信息 |
- 幾個核心註解的屬性
- 核心屬性詳解
屬性名稱 | 屬性作用 | 用法 |
---|---|---|
value | 緩存的名稱,相當於命名空間,必須指定 | @Cacheable(value = “user”) @Cacheable(value = {“user”, “people”}) |
cacheNames | 和value一樣,二選一 | |
key | 緩存的key,如果不指定則按照方法的所有參數進行組合,可以使用SpEL進行指定 | @Cacheable(value = “user”, key = “#id”) |
keyGenerator | 自定義緩存key生成器,和key二選一 | @Cacheable(value = “user”, keyGenerator = “myKeyGenerator”) |
cacheManager | 緩存管理器,默認採用的是SimpleCacheConfiguration | 只要引入相應地配置,SpringBoot就會自動的切換成對應的緩存管理器 |
cacheResolver | 緩存解析器,自定義緩存解析器 | |
condition | 緩存的條件,使用SpEL編寫,只有條件爲true時才進行緩存操作,在方法的調用之後之後都可以進行判斷 | @Cacheable(value = “user”, key = “#id”, condition = “#id > 0 and #result != null”) |
unless | 與condition相反,條件爲false時纔會緩存,並且只在方法執行之後判斷 | 用法和condition一樣 |
sync | 異步 @Cacheable 特有的 | |
allEntries | 是否在方法執行之後清空緩存,默認爲false @CacheEvict特有的 |
@CacheEvict(value = “user”, allEntries = true) |
beforeInvocation | 是否在方法執行之前清空緩存,默認爲false @CacheEvict 特有 |
@CacheEvict(value = “user”, beforeInvocation = true) |
自定義緩存key生成器
package com.xx.config;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* @author aqi
* DateTime: 2020/6/29 5:00 下午
* Description: 自定義緩存key生成器
*/
@Configuration
public class MyCacheConfig {
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator(){
@Override
public Object generate(Object o, Method method, Object... objects) {
// 自定義緩存key的樣式
return method.getName() + "[" + Arrays.asList(objects).toString() + "]";
}
};
}
}
SpringBoot默認提供的緩存管理器,如果要使用Redis只需要引入Redis的POM和配置文件,就會默認切換到RedisCacheManager
四.編寫測試接口
- 測試@Cacheable註解
/**
* 這裏如果不寫key,則使用id作爲key,也就是id = result
*/
@Cacheable(value = "user", key = "#id", condition = "#id == 1")
@GetMapping("/getUser/{id}")
public User getUser(@PathVariable Integer id) {
System.out.println("請求的id:" + id);
return userMapper.getUserById(id);
}
- 當請求的id爲1時,第一次請求去訪問數據庫,後續則不再訪問數據庫
- 當請求的id爲2時,每一次請求都會去訪問數據庫
- 測試@CachePut註解
/**
* 使用返回結果對象的id值作爲key值
*/
@CachePut(value = "user", key = "#result.id")
@GetMapping("/modUser")
public User modUser(User user) {
userMapper.modUser(user);
return user;
}
- 每次執行更新操作都會訪問一次數據庫,因爲@CachePut每次執行都會訪問數據庫,並且修改緩存,執行更新操作之後調用查詢接口不再訪問數據庫
- 測試@CacheEvict註解
@CacheEvict(value = "user", key = "#id")
@GetMapping("/delUser/{id}")
public void delUser(@PathVariable int id) {
System.out.println("刪除用戶緩存");
}
- 執行查詢操作後,執行刪除操作,由於緩存中的數據被清除,所以再次執行查詢操作將會訪問數據庫
五.Spring Cache 總結
- Spring Boot 緩存的結構圖
名稱 | 說明 |
---|---|
CacheingProvider | 緩存提供者:用於控制、管理、創建、配置、獲取多個CacheManager |
CacheManager | 緩存管理者:用於控制、管理、創建、配置、獲取多個唯一命名的Cache |
Cache | 類似於一個命名空間,用於區分不同的緩存 |
Entry | 存儲在Cache中的數據,以key-value的形式存儲 |
Expiry | 緩存有效期 |
- 緩存部分源碼流程(這裏學習了尚硅谷的SpringBoot Cache教程,這裏附上鍊接)
①Spring Cache 的自動配置類是:CacheAutoConfiguration
②定義了多個緩存組件的配置類
③系統如何選擇使用哪個配置類
- 通過類頭做的判斷來決定使用哪個配置類
- 在配置文件中加上debug:
true這個配置,在控制檯查看SpringBoot自動配置了哪些服務,可以看一下默認情況下,SpringBoot
到底使用了哪個緩存配置類,可以發現 SimpleCacheConfiguration匹配上了
- SimpleCacheConfiguration配置往容器中注入了一個ConcurrentMapCacheManager緩存管理器
- ConcurrentMapCacheManager實現了CacheManager接口,通過名稱獲取到一個緩存組件,如果沒有獲取到就自己創建一個ConcurrentMapCache緩存組件,並將數據存儲在ConcurrentMap中
- 最後再完整的梳理一下緩存的執行流程
- 第一步:在方法執行之前,先進入到ConcurrentMapCacheManager中的getCache這個方法,獲取到名稱爲user的Cache緩存組件,第一次進來的時候沒有名稱叫user的Cache緩存組件,這時候會走到createConcurrentMapCache這裏去創建一個名叫user的Cache緩存組件
- 第二步:去剛纔創建的叫user的Cache緩存組件中,查找內容,查找的key值就是在@Cahceable中設置的key值,這裏是1,由於是第一次進來所以自然是查不到數據的
- 第三步:沒有查到緩存結果,就會執行目標方法,並將結果放進緩存中
五.整合Redis
- Cache緩存接口,提供了8種緩存實現,只需要配置對應的緩存組件,Spring在自動裝配的時候的時候就會自動匹配,並注入容器
- 配置redis
①引入redis依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
②修改配置文件
六.Redis簡單介紹
- Redis是一個高性能的key-value數據庫,可以存儲一下這些數據類型
String(字符串),List(列表),Set(集合),Hash(散列),ZSet(有序集合)
SpringBoot Rerdis 提供了兩種模板去操作redis
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
- 一些模板方法,redis提供的命令api中都有,具體可以查看Redis官網
方法名稱 | 作用 |
---|---|
opsForValue | 用於操作字符串 |
opsForList | 用於操作列表 |
opsForSet | 用於操作集合 |
opsForHash | 用於操作散列 |
ZSet | 用於操作有序集合 |
- 安裝redis desktop manager,Redis可視化工具
- 編寫測試類操作redis
@Test
void addMsg() {
redisTemplate.opsForValue().set("msg", "Hello");
}
@Test
void appendMsg() {
redisTemplate.opsForValue().append("msg", "Java");
}
- 這裏可以看到存進去的數據是一些奇怪的字符和亂碼,這是由於我使用的是redisTemplate需要進行序列化配置,如果僅僅使用StringRedisTemplate操作字符串是不會出現這種問題的,但是操作其他數據類型則會報錯
七.最後再聊一聊Redis序列化
- 什麼是序列化和反序列化
- 序列化:將對象寫到IO流中
- 反序列化:從IO流中恢復對象
- 序列化的意義:序列化機制允許將實現序列化的Java對象轉換位字節序列,這些字節序列可以保存在磁盤上,或通過網絡傳輸,以達到以後恢復成原來的對象。序列化機制使得對象可以脫離程序的運行而獨立存在。
- Redis提供了多種序列化的手段,當然也可以使用一些外部的序列化工具
- 只需要配置一下,就可以解決剛纔出現的問題,但是這麼多序列化的手段如何挑選呢,我比較好奇,所以我又稍微深挖了一下
package com.xx.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author aqi
* DateTime: 2020/6/30 10:56 上午
* Description: Redis配置
*/
@Configuration
public class MyRedisConfig {
/**
* redisTemplate配置
* 序列化的幾種方式:
* OxmSerializer
* ByteArrayRedisSerializer
* GenericJackson2JsonRedisSerializer
* GenericToStringSerializer
* StringRedisSerializer
* JdkSerializationRedisSerializer
* Jackson2JsonRedisSerializer
* @param redisConnectionFactory redis連接工廠
* @return RedisTemplate
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 配置連接工廠
template.setConnectionFactory(redisConnectionFactory);
// 設置key的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 設置value的序列化方式
template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
return template;
}
}
名稱 | 說明 |
---|---|
ByteArrayRedisSerializer | 數組序列化 |
GenericJackson2JsonRedisSerializer | 使用Jackson進行序列化 |
GenericToStringSerializer | 將對象泛化成字符串並序列化,和StringRedisSerializer差不多 |
Jackson2JsonRedisSerializer | 使用Jackson序列化對象爲json |
JdkSerializationRedisSerializer | jdk自帶的序列化方式,需要實現Serializable接口 |
OxmSerializer | 用xml格式存儲 |
StringRedisSerializer | 簡單的字符串序列化 |
- 比較幾種常見序列化手段的差異
測試代碼
package com.xx;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import com.xx.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.serializer.*;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class CacheApplicationTests {
/**
* 測試幾種序列化手段的效率
*/
@Test
void test() {
User user = new User();
user.setId(1);
user.setUsername("張三");
user.setPassword("123");
List<Object> list = new ArrayList<>();
for (int i = 0; i < 2000; i++) {
list.add(user);
}
// 使用GenericJackson2JsonRedisSerializer做序列化(效率太低,不推薦使用)
GenericJackson2JsonRedisSerializer g2 = new GenericJackson2JsonRedisSerializer();
long g2s = System.currentTimeMillis();
byte[] byteG2 = g2.serialize(list);
long g2l = System.currentTimeMillis();
System.out.println("GenericJackson2JsonRedisSerializer序列化消耗的時間:" + (g2l - g2s) + "ms,序列化之後的長度:" + byteG2.length);
g2.deserialize(byteG2);
System.out.println("GenericJackson2JsonRedisSerializer反序列化的時間:" + (System.currentTimeMillis() - g2l) + "ms");
// 使用GenericToStringSerializer做序列化(和StringRedisSerializer差不多,效率沒有StringRedisSerializer高,不推薦使用)
GenericToStringSerializer g = new GenericToStringSerializer(Object.class);
long gs = System.currentTimeMillis();
byte[] byteG = g.serialize(list.toString());
long gl = System.currentTimeMillis();
System.out.println("GenericToStringSerializer序列化消耗的時間:" + (gl - gs) + "ms,序列化之後的長度:" + byteG.length);
g.deserialize(byteG);
System.out.println("GenericToStringSerializer反序列化的時間:" + (System.currentTimeMillis() - gl) + "ms");
// 使用Jackson2JsonRedisSerializer做序列化(效率高,適合value值的序列化)
Jackson2JsonRedisSerializer j2 = new Jackson2JsonRedisSerializer(Object.class);
long j2s = System.currentTimeMillis();
byte[] byteJ2 = j2.serialize(list);
long j2l = System.currentTimeMillis();
System.out.println("Jackson2JsonRedisSerializer序列化消耗的時間:" + (j2l - j2s) + "ms,序列化之後的長度:" + byteJ2.length);
j2.deserialize(byteJ2);
System.out.println("Jackson2JsonRedisSerializer反序列化的時間:" + (System.currentTimeMillis() - j2l) + "ms");
// 使用JdkSerializationRedisSerializer,實體類必須實現序列化接口(不推薦使用)
JdkSerializationRedisSerializer j = new JdkSerializationRedisSerializer();
long js = System.currentTimeMillis();
byte[] byteJ = j.serialize(list);
long jl = System.currentTimeMillis();
System.out.println("JdkSerializationRedisSerializer序列化消耗的時間:" + (jl - js) + "ms,序列化之後的長度:" + byteJ.length);
j.deserialize(byteJ);
System.out.println("JdkSerializationRedisSerializer反序列化的時間:" + (System.currentTimeMillis() - jl) + "ms");
// 使用StringRedisSerializer做序列化(效率非常的高,但是比較佔空間,只能對字符串序列化,適合key值的序列化)
StringRedisSerializer s = new StringRedisSerializer();
long ss = System.currentTimeMillis();
byte[] byteS = s.serialize(list.toString());
long sl = System.currentTimeMillis();
System.out.println("StringRedisSerializer序列化消耗的時間:" + (sl - ss) + "ms,序列化之後的長度:" + byteS.length);
s.deserialize(byteS);
System.out.println("StringRedisSerializer反序列化的時間:" + (System.currentTimeMillis() - sl) + "ms");
// 使用FastJson做序列化,這個表現爲什麼這麼差我也不是很明白
FastJsonRedisSerializer<Object> f = new FastJsonRedisSerializer<>(Object.class);
long fs = System.currentTimeMillis();
byte[] byteF = f.serialize(list);
long fl = System.currentTimeMillis();
System.out.println("FastJsonRedisSerializer序列化消耗的時間:" + (fl - fs) + "ms,序列化之後的長度:" + byteF.length);
f.deserialize(byteF);
System.out.println("FastJsonRedisSerializer反序列化的時間:" + (System.currentTimeMillis() - fl) + "ms");
// 使用FastJson(效率高,序列化後佔空間也很小,推薦使用)
GenericFastJsonRedisSerializer gf = new GenericFastJsonRedisSerializer();
long gfs = System.currentTimeMillis();
byte[] byteGf = gf.serialize(list);
long gfl = System.currentTimeMillis();
System.out.println("GenericFastJsonRedisSerializer序列化消耗的時間:" + (gfl - gfs) + "ms,序列化之後的長度:" + byteGf.length);
gf.deserialize(byteGf);
System.out.println("GenericFastJsonRedisSerializer反序列化的時間:" + (System.currentTimeMillis() - gfl) + "ms");
}
}
測試結果
- 總結
名稱 | 序列化效率 | 反序列化效率 | 佔用空間 | 是否推薦使用 |
---|---|---|---|---|
StringRedisSerializer | 很高 | 很高 | 高 | 推薦給kye進行序列化 |
Jackson2JsonRedisSerializer | 高 | 較高 | 偏高 | 推薦給value進行序列化 |
GenericFastJsonRedisSerializer | 高 | 較低 | 較低 | 推薦給value進行序列化 |
- 附上Redis序列化配置文件
package com.xx.config;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author aqi
* DateTime: 2020/6/30 10:56 上午
* Description: Redis配置
*/
@Configuration
public class MyRedisConfig {
/**
* redisTemplate配置
* @param redisConnectionFactory redis連接工廠
* @return RedisTemplate
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 配置連接工廠
template.setConnectionFactory(redisConnectionFactory);
// 配置key的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 使用Jackson2JsonRedisSerializer配置value的序列化方式
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
// 使用FastJson配置value的序列化方式
// template.setValueSerializer(new GenericFastJsonRedisSerializer());
return template;
}
}
使用Jackson2JsonRedisSerializer序列化的結果
使用FastJson序列化的結果
八.最後
才疏學淺,可能有些地方說的不準確,如果有些的不對的地方感謝各位老哥指正。