一、Spring Cache
1、基本介紹
基於註釋(annotation)的緩存(cache)技術是在Spring 3.1 引入的,它本質上不是一個具體的緩存實現方案(例如 EHCache),而是一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 annotation,就能夠達到緩存方法的返回對象的效果。
Spring 的緩存技術還具備相當的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的 key 和各種 condition,還提供開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存例如Redis集成。
主要特點如下:
- 通過少量的配置 annotation 註釋即可使得既有代碼支持緩存
- 支持 Spring Express Language,能使用對象的任何屬性或者方法來定義緩存的 key 和 condition
- 支持 AspectJ,並通過其實現任何方法的緩存支持
- 支持自定義 key 和自定義緩存管理者,具有相當的靈活性和擴展性
2、常用緩存註解
spring cache 常用註解: @Cacheable、@CachePut 和 @CacheEvict
2.1、@Cacheable
作用: 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存。
參數介紹:
value:緩存的名稱
每一個緩存名稱代表一個緩存對象。當一個方法填寫多個緩存名稱時將創建多個緩存對象。當多個方法使用同一緩存名稱時相同參數的緩存會被覆蓋。所以通常情況我們使用“包名+類名+方法名”或者使用接口的RequestMapping作爲緩存名稱防止命名重複引起的問題。
單緩存名稱:@Cacheable(value=”mycache”)
多緩存名稱:@Cacheable(value={”cache1”,”cache2”}
key:緩存的 key
key標記了緩存對象下的每一條緩存。如果不指定key則系統自動按照方法的所有入參生成key,也就是說相同的入參值將會返回同樣的緩存結果。
如果指定key則要按照 SpEL 表達式編寫使用的入參列表。如下列無論方法存在多少個入參,只要userName值一致,則會返回相同的緩存結果。
@Cacheable(value=”testcache”,key=”#userName”)
condition:緩存的條件
滿足條件後方法結果纔會被緩存。不填寫則認爲無條件全部緩存。
條件使用 SpEL表達式編寫,返回 true 或者 false,只有爲 true 才進行緩存
如下例,只有用戶名長度大於2時參會進行緩存
@Cacheable(value=”testcache”,condition=”#userName.length()>2”)
2.2、@CachePut
作用: 主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存。和 @Cacheable 不同的是,它每次都會觸發真實方法的調用,此註解常被用於更新緩存使用。
參數介紹:
value:緩存的名稱
@CachePut(value=”mycache”)
@CachePut(value={”cache1”,”cache2”}
key:緩存的 key
@CachePut(value=”testcache”,key=”#userName”)
condition:緩存的條件
@CachePut(value=”testcache”,condition=”#userName.length()>2”)
2.3、@CacheEvict
作用: 主要針對方法配置,能夠根據一定的條件對緩存進行清空
參數介紹:
value 緩存的名稱
刪除指定名稱的緩存對象。必須與下面的其中一個參數配合使用
例如:
@CacheEvict(value=”mycache”) 或者
@CacheEvict(value={”cache1”,”cache2”}
key 緩存的 key
刪除指定key的緩存對象
例如:
@CacheEvict(value=”testcache”,key=”#userName”)
condition 緩存的條件
刪除指定條件的緩存對象
例如:
@CacheEvict(value=”testcache”,condition=”#userName.length()>2”)
allEntries 方法執行後清空所有緩存
缺省爲 false,如果指定爲 true,則方法調用後將立即清空所有緩存。
例如:
@CacheEvict(value=”testcache”,allEntries=true)
beforeInvocation 方法執行前清空所有緩存
缺省爲 false,如果指定爲 true,則在方法還沒有執行的時候就清空緩存,缺省情況下,如果方法執行拋出異常,則不會清空緩存。
例如:
@CacheEvict(value=”testcache”,beforeInvocation=true)
二、Spring Boot使用Spring cache默認緩存
Spring Boot 爲我們提供了多種緩存CacheManager配置方案。默認情況下會使用基於內存map一種緩存方案ConcurrenMapCacheManager。當然我沒也可以通過配置使用 Generic、JCache (JSR-107)、EhCache 2.x、Hazelcast、Infinispan、Redis、Guava、Simple等技術進行緩存實現。
這裏使用默認的基於內存的方案進行舉例
引入依賴
在pom文件中引入緩存包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
啓用緩存
在啓動類增加啓用緩存註解@EnableCaching,該註解主要是用於spring framework中的註解驅動的緩存管理。如果使用了該註解,則就不需要在XML文件中配置cache manager了。
@SpringBootApplication
@EnableCaching //啓用緩存
public class App {
public static void main(String[] args) {
SpringApplication.run(App.class, args);
}
}
緩存測試方法
測試方法做了一個2秒的延時
public class CacheTest {
/**
* 緩存測試方法延時兩秒
* @param i
* @return
*/
@Cacheable(value = "cache_test")
public String cacheFunction(int i){
try {
long time = 2000L;
Thread.sleep(time);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
return "success"+ i;
}
}
調用緩存測試方法
這裏需要注意:不能在同一個類中調用被註解緩存了的方法。也就是說緩存調用方法和緩存註解方法不能在一個類中出現。
public class HelloController {
@Autowired
CacheTest cacheTest;
@GetMapping(value = "/")
public String hello(){
for(int i=0;i<5;i++){
System.out.println(new Date() + " " + cacheTest.cacheFunction(i));
}
return "/hello";
}
}
測試結果
我們可以看出第一次執行時每間隔2秒打印了一次success
而第二次同一時間全部打印完成
Tue Jun 12 15:35:01 CST 2018 success0
Tue Jun 12 15:35:03 CST 2018 success1
Tue Jun 12 15:35:05 CST 2018 success2
Tue Jun 12 15:35:07 CST 2018 success3
Tue Jun 12 15:35:09 CST 2018 success4
Tue Jun 12 15:35:26 CST 2018 success0
Tue Jun 12 15:35:26 CST 2018 success1
Tue Jun 12 15:35:26 CST 2018 success2
Tue Jun 12 15:35:26 CST 2018 success3
Tue Jun 12 15:35:26 CST 2018 success4
三、spring boot中使用Spring cache + redis緩存
Spring Cache集成redis的運行原理
Spring緩存抽象模塊通過CacheManager來創建、管理實際緩存組件,當SpringBoot應用程序引入spring-boot-starter-data-redis依賴後,容器中將註冊的是CacheManager實例RedisCacheManager對象,RedisCacheManager來負責創建RedisCache作爲緩存管理組件,由RedisCache操作redis服務器實現緩存數據操作。
引入redis依賴
在pom文件中引入redis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
redis配置如下
在application.properties配置文件中增加redis配置
#redis配置
#Redis數據庫索引(緩存將使用此索引編號的數據庫)
spring.redis.database=10
#Redis服務器地址
spring.redis.host=127.0.0.1
#Redis服務器連接端口
spring.redis.port=6379
#Redis服務器連接密碼(默認爲空)
spring.redis.password=******
#連接超時時間 毫秒(默認2000)
#請求redis服務的超時時間,這裏注意設置成0時取默認時間2000
spring.redis.timeout=2000
#連接池最大連接數(使用負值表示沒有限制)
#建議爲業務期望QPS/一個連接的QPS,例如50000/1000=50
#一次命令時間(borrow|return resource+Jedis執行命令+網絡延遲)的平均耗時約爲1ms,一個連接的QPS大約是1000
spring.redis.pool.max-active=50
#連接池中的最大空閒連接
#建議和最大連接數一致,這樣做的好處是連接數從不減少,從而避免了連接池伸縮產生的性能開銷。
spring.redis.pool.max-idle=50
#連接池中的最小空閒連接
#建議爲0,在無請求的狀況下從不創建鏈接
spring.redis.pool.min-idle=0
#連接池最大阻塞等待時間 毫秒(-1表示沒有限制)
#建議不要爲-1,連接池佔滿後無法獲取連接時將在該時間內阻塞等待,超時後將拋出異常。
spring.redis.pool.max-wait=2000
設置緩存生存時間
我們可以對redis緩存數據指定生存時間從而達到緩存自動失效的目的。
通過創建緩存配置文件類可以設置緩存各項參數
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisTemplate redisTemplate) {
//獲得redis緩存管理類
RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
// 開啓使用緩存名稱做爲key前綴(這樣所有同名緩存會整理在一起比較容易查找)
redisCacheManager.setUsePrefix(true);
//這裏可以設置一個默認的過期時間 單位是秒
redisCacheManager.setDefaultExpiration(600L);
// 設置緩存的過期時間 單位是秒
Map<String, Long> expires = new HashMap<>();
expires.put("pub.imlichao.CacheTest.cacheFunction", 100L);
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
}
設置過期時間時也可以不採用expires.put(“pub.imlichao.CacheTest.cacheFunction”, 100L)的寫法,而是使用@Cacheable標籤的value值進行聲明,如下
@Configuration
public class RedisCacheConfig {
......
// 設置緩存的過期時間 單位是秒
Map<String, Long> expires = new HashMap<>();
expires.put("cache_test", 100L);
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
}
設置緩存序列化方式
redisTemplate 默認的序列化方式爲 jdkSerializeable,我們也可以使用其他序列化方式來達到不同的需求。比如我們希望緩存的數據具有可讀性就可以將其序列化爲json格式,json序列化可以使用Jackson2JsonRedisSerialize或FastJsonRedisSerializer。如果我們希望擁有更快的速度和佔用更小的存儲空間推薦使用KryoRedisSerializer進行序列化。
由於redis緩存對可讀性沒什麼要求,而存儲空間和速度是比較重要的,所以這裏使用KryoRedisSerializer進行對象序列化。
添加Kryo依賴
<dependency>
<groupId>com.esotericsoftware</groupId>
<artifactId>kryo</artifactId>
<version>4.0.2</version>
</dependency>
實現RedisSerializer接口創建KryoRedisSerializer序列化工具
import com.esotericsoftware.kryo.Kryo;
import com.esotericsoftware.kryo.io.Input;
import com.esotericsoftware.kryo.io.Output;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import java.io.ByteArrayOutputStream;
public class KryoRedisSerializer<T> implements RedisSerializer<T> {
public static final byte[] EMPTY_BYTE_ARRAY = new byte[0];
private static final ThreadLocal<Kryo> kryos = ThreadLocal.withInitial(Kryo::new);
private Class<T> clazz;
public KryoRedisSerializer(Class<T> clazz) {
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException {
if (t == null) {
return EMPTY_BYTE_ARRAY;
}
Kryo kryo = kryos.get();
kryo.setReferences(false);
kryo.register(clazz);
try (ByteArrayOutputStream baos = new ByteArrayOutputStream();
Output output = new Output(baos)) {
kryo.writeClassAndObject(output, t);
output.flush();
return baos.toByteArray();
} catch (Exception e) {
e.printStackTrace();
}
return EMPTY_BYTE_ARRAY;
}
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (bytes == null || bytes.length <= 0) {
return null;
}
Kryo kryo = kryos.get();
kryo.setReferences(false);
kryo.register(clazz);
try (Input input = new Input(bytes)) {
return (T) kryo.readClassAndObject(input);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
修改配置文件替換默認序列化工具
@Configuration
public class RedisCacheConfig {
@Bean("redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory);
redisTemplate.setKeySerializer(new StringRedisSerializer());
// KryoRedisSerializer 替換默認序列化
redisTemplate.setDefaultSerializer(new KryoRedisSerializer());
return redisTemplate;
}
}