您好,登錄后才能下訂單哦!
怎樣搭建SpringBoot緩存系統(tǒng),很多新手對此不是很清楚,為了幫助大家解決這個難題,下面小編將為大家詳細(xì)講解,有這方面需求的人可以來學(xué)習(xí)下,希望你能有所收獲。
緩存是最直接有效提升系統(tǒng)性能的手段之一。個人認(rèn)為用好用對緩存是優(yōu)秀程序員的必備基本素質(zhì)。
(1)、FIFO(First In First Out),先進(jìn)先出,和OS里的FIFO思路相同,如果一個數(shù)據(jù)最先進(jìn)入緩存中,當(dāng)緩存滿的時候,應(yīng)當(dāng)把最先進(jìn)入緩存的數(shù)據(jù)給移除掉。(2)、LFU(Least Frequently Used),最不經(jīng)常使用,如果一個數(shù)據(jù)在最近一段時間內(nèi)使用次數(shù)很少,那么在將來一段時間內(nèi)被使用的可能性也很小。(3)、LRU(Least Recently Used),最近最少使用,如果一個數(shù)據(jù)在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當(dāng)限定的空間已存滿數(shù)據(jù)時,應(yīng)當(dāng)把最久沒有被訪問到的數(shù)據(jù)移除。
簡單定義緩存接口,大致可以抽象如下:
package com.power.demo.cache.contract;
import java.util.function.Function;
/**
* 緩存提供者接口
**/
public interface CacheProviderService {
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
**/
<T extends Object> T get(String key);
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
**/
<T extends Object> T get(String key, Function<String, T> function);
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
**/
<T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm);
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
<T extends Object> T get(String key, Function<String, T> function, Long expireTime);
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
<T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime);
/**
* 設(shè)置緩存鍵值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
**/
<T extends Object> void set(String key, T obj);
/**
* 設(shè)置緩存鍵值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
<T extends Object> void set(String key, T obj, Long expireTime);
/**
* 移除緩存
*
* @param key 緩存鍵 不可為空
**/
void remove(String key);
/**
* 是否存在緩存
*
* @param key 緩存鍵 不可為空
**/
boolean contains(String key);
}
注意,這里列出的只是常見緩存功能接口,一些在特殊場景下用到的統(tǒng)計類的接口、分布式鎖、自增(減)等功能不在討論范圍之內(nèi)。
Get相關(guān)方法,注意多個參數(shù)的情況,緩存接口里面?zhèn)魅说腇unction,這是Java8提供的函數(shù)式接口,雖然支持的入?yún)€數(shù)有限(這里你會非常懷念.NET下的Func委托),但是僅對Java這個語言來說,這真是一個重大的進(jìn)步^_^。
接口定義好了,下面就要實現(xiàn)緩存提供者程序了。按照存儲類型的不同,本文簡單實現(xiàn)最常用的兩種緩存提供者:本地緩存和分布式緩存。
本地緩存,也就是JVM級別的緩存(本地緩存可以認(rèn)為是直接在進(jìn)程內(nèi)通信調(diào)用,而分布式緩存則需要通過網(wǎng)絡(luò)進(jìn)行跨進(jìn)程通信調(diào)用),一般有很多種實現(xiàn)方式,比如直接使用Hashtable、ConcurrentHashMap等天生線程安全的集合作為緩存容器,或者使用一些成熟的開源組件,如EhCache、Guava Cache等。本文選擇上手簡單的Guava緩存。
Guava,簡單來說就是一個開發(fā)類庫,且是一個非常豐富強(qiáng)大的開發(fā)工具包,號稱可以讓使用Java語言更令人愉悅,主要包括基本工具類庫和接口、緩存、發(fā)布訂閱風(fēng)格的事件總線等。在實際開發(fā)中,我用的最多的是集合、緩存和常用類型幫助類,很多人都對這個類庫稱贊有加。
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
</dependency>
package com.power.demo.cache.impl;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import com.google.common.collect.Maps;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.Map;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/*
* 本地緩存提供者服務(wù) (Guava Cache)
* */
@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
@Qualifier("localCacheService")
public class LocalCacheProviderImpl implements CacheProviderService {
private static Map<String, Cache<String, Object>> _cacheMap = Maps.newConcurrentMap();
static {
Cache<String, Object> cacheContainer = CacheBuilder.newBuilder()
.maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
.expireAfterWrite(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS)//最后一次寫入后的一段時間移出
//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次訪問后的一段時間移出
.recordStats()//開啟統(tǒng)計功能
.build();
_cacheMap.put(String.valueOf(AppConst.CACHE_MINUTE), cacheContainer);
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
**/
public <T extends Object> T get(String key) {
T obj = get(key, null, null, AppConst.CACHE_MINUTE);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
**/
public <T extends Object> T get(String key, Function<String, T> function) {
T obj = get(key, function, key, AppConst.CACHE_MINUTE);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
**/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
T obj = get(key, function, key, expireTime);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
T obj = null;
if (StringUtils.isEmpty(key) == true) {
return obj;
}
expireTime = getExpireTime(expireTime);
Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
try {
if (function == null) {
obj = (T) cacheContainer.getIfPresent(key);
} else {
final Long cachedTime = expireTime;
obj = (T) cacheContainer.get(key, () -> {
T retObj = function.apply(funcParm);
return retObj;
});
}
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
/**
* 設(shè)置緩存鍵值 直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
**/
public <T extends Object> void set(String key, T obj) {
set(key, obj, AppConst.CACHE_MINUTE);
}
/**
* 設(shè)置緩存鍵值 直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object> void set(String key, T obj, Long expireTime) {
if (StringUtils.isEmpty(key) == true) {
return;
}
if (obj == null) {
return;
}
expireTime = getExpireTime(expireTime);
Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
cacheContainer.put(key, obj);
}
/**
* 移除緩存
*
* @param key 緩存鍵 不可為空
**/
public void remove(String key) {
if (StringUtils.isEmpty(key) == true) {
return;
}
long expireTime = getExpireTime(AppConst.CACHE_MINUTE);
Cache<String, Object> cacheContainer = getCacheContainer(expireTime);
cacheContainer.invalidate(key);
}
/**
* 是否存在緩存
*
* @param key 緩存鍵 不可為空
**/
public boolean contains(String key) {
boolean exists = false;
if (StringUtils.isEmpty(key) == true) {
return exists;
}
Object obj = get(key);
if (obj != null) {
exists = true;
}
return exists;
}
private static Lock lock = new ReentrantLock();
private Cache<String, Object> getCacheContainer(Long expireTime) {
Cache<String, Object> cacheContainer = null;
if (expireTime == null) {
return cacheContainer;
}
String mapKey = String.valueOf(expireTime);
if (_cacheMap.containsKey(mapKey) == true) {
cacheContainer = _cacheMap.get(mapKey);
return cacheContainer;
}
try {
lock.lock();
cacheContainer = CacheBuilder.newBuilder()
.maximumSize(AppConst.CACHE_MAXIMUM_SIZE)
.expireAfterWrite(expireTime, TimeUnit.MILLISECONDS)//最后一次寫入后的一段時間移出
//.expireAfterAccess(AppConst.CACHE_MINUTE, TimeUnit.MILLISECONDS) //最后一次訪問后的一段時間移出
.recordStats()//開啟統(tǒng)計功能
.build();
_cacheMap.put(mapKey, cacheContainer);
} finally {
lock.unlock();
}
return cacheContainer;
}
/**
* 獲取過期時間 單位:毫秒
*
* @param expireTime 傳人的過期時間 單位毫秒 如小于1分鐘,默認(rèn)為10分鐘
**/
private Long getExpireTime(Long expireTime) {
Long result = expireTime;
if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
result = AppConst.CACHE_MINUTE;
}
return result;
}
}
Guava Cache初始化容器時,支持緩存過期策略,類似FIFO、LRU和LFU等算法。
expireAfterWrite:最后一次寫入后的一段時間移出。
expireAfterAccess:最后一次訪問后的一段時間移出。
Guava Cache對緩存過期時間的設(shè)置實在不夠友好。常見的應(yīng)用場景,比如,有些幾乎不變的基礎(chǔ)數(shù)據(jù)緩存1天,有些熱點數(shù)據(jù)緩存2小時,有些會話數(shù)據(jù)緩存5分鐘等等。
通常我們認(rèn)為設(shè)置緩存的時候帶上緩存的過期時間是非常容易的,而且只要一個緩存容器實例即可,比如.NET下的ObjectCache、System.Runtime.Cache等等。
但是Guava Cache不是這個實現(xiàn)思路,如果緩存的過期時間不同,Guava的CacheBuilder要初始化多份Cache實例。
好在我在實現(xiàn)的時候注意到了這個問題,并且提供了解決方案,可以看到getCacheContainer這個函數(shù),根據(jù)過期時長做緩存實例判斷,就算不同過期時間的多實例緩存也是完全沒有問題的。
分布式緩存產(chǎn)品非常多,本文使用應(yīng)用普遍的Redis,在Spring Boot應(yīng)用中使用Redis非常簡單。
Redis是一款開源(BSD許可)的、用C語言寫成的高性能的鍵-值存儲(key-value store)。它常被稱作是一款數(shù)據(jù)結(jié)構(gòu)服務(wù)器(data structure server)。它可以被用作緩存、消息中間件和數(shù)據(jù)庫,在很多應(yīng)用中,經(jīng)??吹接腥诉x擇使用Redis做緩存,實現(xiàn)分布式鎖和分布式Session等。作為緩存系統(tǒng)時,和經(jīng)典的KV結(jié)構(gòu)的Memcached非常相似,但又有很多不同。Redis支持豐富的數(shù)據(jù)類型。Redis的鍵值可以包括字符串(strings)類型,同時它還包括哈希(hashes)、列表(lists)、集合(sets)和有序集合(sorted sets)等數(shù)據(jù)類型。對于這些數(shù)據(jù)類型,你可以執(zhí)行原子操作。例如:對字符串進(jìn)行附加操作(append);遞增哈希中的值;向列表中增加元素;計算集合的交集、并集與差集等。
Redis的數(shù)據(jù)類型:Keys:非二進(jìn)制安全的字符類型( not binary-safe strings ),由于key不是binary safe的字符串,所以像“my key”和“mykey\n”這樣包含空格和換行的key是不允許的。Values:Strings、Hash、Lists、 Sets、 Sorted sets??紤]到Redis單線程操作模式,Value的粒度不應(yīng)該過大,緩存的值越大,越容易造成阻塞和排隊。
為了獲得優(yōu)異的性能,Redis采用了內(nèi)存中(in-memory)數(shù)據(jù)集(dataset)的方式。同時,Redis支持?jǐn)?shù)據(jù)的持久化,你可以每隔一段時間將數(shù)據(jù)集轉(zhuǎn)存到磁盤上(snapshot),或者在日志尾部追加每一條操作命令(append only file,aof)。Redis同樣支持主從復(fù)制(master-slave replication),并且具有非??焖俚姆亲枞状瓮剑?non-blocking first synchronization)、網(wǎng)絡(luò)斷開自動重連等功能。同時Redis還具有其它一些特性,其中包括簡單的事物支持、發(fā)布訂閱 ( pub/sub)、管道(pipeline)和虛擬內(nèi)存(vm)等 。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
在application.properties配置文件中,配置Redis常用參數(shù):
# Redis緩存相關(guān)配置
#Redis數(shù)據(jù)庫索引(默認(rèn)為0)
spring.redis.database=0
#Redis服務(wù)器地址
spring.redis.host=127.0.0.1
#Redis服務(wù)器端口
spring.redis.port=6379
#Redis服務(wù)器密碼(默認(rèn)為空)
spring.redis.password=123321
#Redis連接超時時間 默認(rèn):5分鐘(單位:毫秒)
spring.redis.timeout=300000ms
#Redis連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
spring.redis.jedis.pool.max-active=512
#Redis連接池中的最小空閑連接
spring.redis.jedis.pool.min-idle=0
#Redis連接池中的最大空閑連接
spring.redis.jedis.pool.max-idle=8
#Redis連接池最大阻塞等待時間(使用負(fù)值表示沒有限制)
spring.redis.jedis.pool.max-wait=-1ms
常見的需要注意的是最大連接數(shù)(spring.redis.jedis.pool.max-active )和超時時間(spring.redis.jedis.pool.max-wait)。Redis在生產(chǎn)環(huán)境中出現(xiàn)故障的頻率經(jīng)常和這兩個參數(shù)息息相關(guān)。
接著定義一個繼承自CachingConfigurerSupport(請注意cacheManager和keyGenerator這兩個方法在子類的實現(xiàn))的RedisConfig類:
package com.power.demo.cache.config;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* Redis緩存配置類
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
return RedisCacheManager.create(connectionFactory);
}
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
//Jedis的Key和Value的序列化器默認(rèn)值是JdkSerializationRedisSerializer
//經(jīng)實驗,JdkSerializationRedisSerializer通過RedisDesktopManager看到的鍵值對不能正常解析
//設(shè)置key的序列化器
template.setKeySerializer(new StringRedisSerializer());
////設(shè)置value的序列化器 默認(rèn)值是JdkSerializationRedisSerializer
//使用Jackson序列化器的問題是,復(fù)雜對象可能序列化失敗,比如JodaTime的DateTime類型
// //使用Jackson2,將對象序列化為JSON
// Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// //json轉(zhuǎn)對象類,不設(shè)置默認(rèn)的會將json轉(zhuǎn)成hashmap
// ObjectMapper om = new ObjectMapper();
// om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// jackson2JsonRedisSerializer.setObjectMapper(om);
// template.setValueSerializer(jackson2JsonRedisSerializer);
//將redis連接工廠設(shè)置到模板類中
template.setConnectionFactory(factory);
return template;
}
// //自定義緩存key生成策略
// @Bean
// public KeyGenerator keyGenerator() {
// return new KeyGenerator() {
// @Override
// public Object generate(Object target, java.lang.reflect.Method method, Object... params) {
// StringBuffer sb = new StringBuffer();
// sb.append(target.getClass().getName());
// sb.append(method.getName());
// for (Object obj : params) {
// if (obj == null) {
// continue;
// }
// sb.append(obj.toString());
// }
// return sb.toString();
// }
// };
// }
}
在RedisConfig這個類上加上@EnableCaching這個注解,這個注解會被Spring發(fā)現(xiàn),并且會創(chuàng)建一個切面(aspect) 并觸發(fā)Spring緩存注解的切點(pointcut)。據(jù)所使用的注解以及緩存的狀態(tài),這個切面會從緩存中獲取數(shù)據(jù),將數(shù)據(jù)添加到緩存之中或者從緩存中移除某個值。cacheManager方法,申明一個緩存管理器(CacheManager)的bean,作用就是@EnableCaching這個切面在新增緩存或者刪除緩存的時候會調(diào)用這個緩存管理器的方法。keyGenerator方法,可以根據(jù)需求自定義緩存key生成策略。
而redisTemplate方法,則主要是設(shè)置Redis模板類,比如鍵和值的序列化器(從這里可以看出,Redis的鍵值對必須可序列化)、redis連接工廠等。
RedisTemplate支持的序列化器主要有如下幾種:
JdkSerializationRedisSerializer:使用Java序列化;StringRedisSerializer:序列化String類型的key和value;GenericToStringSerializer:使用Spring轉(zhuǎn)換服務(wù)進(jìn)行序列化;JacksonJsonRedisSerializer:使用Jackson 1,將對象序列化為JSON;Jackson2JsonRedisSerializer:使用Jackson 2,將對象序列化為JSON;OxmSerializer:使用Spring O/X映射的編排器和解排器(marshaler和unmarshaler)實現(xiàn)序列化,用于XML序列化;
注意:RedisTemplate的鍵和值序列化器,默認(rèn)情況下都是JdkSerializationRedisSerializer,它們都可以自定義設(shè)置序列化器。推薦將字符串鍵使用StringRedisSerializer序列化器,因為運維的時候好排查問題,JDK序列化器的也能識別,但是可讀性稍差(是因為緩存服務(wù)器沒有JRE嗎?),見如下效果:
而值序列化器則要復(fù)雜的多,很多人推薦使用Jackson2JsonRedisSerializer序列化器,但是實際開發(fā)過程中,經(jīng)常有人碰到反序列化錯誤,經(jīng)過排查多數(shù)都和Jackson2JsonRedisSerializer這個序列化器有關(guān)。
使用RedisTemplate,在Spring Boot中調(diào)用Redis接口比直接調(diào)用Jedis簡單多了。
package com.power.demo.cache.impl;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.util.StringUtils;
import javax.annotation.Resource;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.function.Function;
@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
@Qualifier("redisCacheService")
public class RedisCacheProviderImpl implements CacheProviderService {
@Resource
private RedisTemplate<Serializable, Object> redisTemplate;
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
**/
public <T extends Object> T get(String key) {
T obj = get(key, null, null, AppConst.CACHE_MINUTE);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
**/
public <T extends Object> T get(String key, Function<String, T> function) {
T obj = get(key, function, key, AppConst.CACHE_MINUTE);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
**/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
T obj = get(key, function, funcParm, AppConst.CACHE_MINUTE);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object> T get(String key, Function<String, T> function, Long expireTime) {
T obj = get(key, function, key, expireTime);
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, Long expireTime) {
T obj = null;
if (StringUtils.isEmpty(key) == true) {
return obj;
}
expireTime = getExpireTime(expireTime);
try {
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
obj = (T) operations.get(key);
if (function != null && obj == null) {
obj = function.apply(funcParm);
if (obj != null) {
set(key, obj, expireTime);//設(shè)置緩存信息
}
}
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}
/**
* 設(shè)置緩存鍵值 直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
**/
public <T extends Object> void set(String key, T obj) {
set(key, obj, AppConst.CACHE_MINUTE);
}
/**
* 設(shè)置緩存鍵值 直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object> void set(String key, T obj, Long expireTime) {
if (StringUtils.isEmpty(key) == true) {
return;
}
if (obj == null) {
return;
}
expireTime = getExpireTime(expireTime);
ValueOperations<Serializable, Object> operations = redisTemplate.opsForValue();
operations.set(key, obj);
redisTemplate.expire(key, expireTime, TimeUnit.MILLISECONDS);
}
/**
* 移除緩存
*
* @param key 緩存鍵 不可為空
**/
public void remove(String key) {
if (StringUtils.isEmpty(key) == true) {
return;
}
redisTemplate.delete(key);
}
/**
* 是否存在緩存
*
* @param key 緩存鍵 不可為空
**/
public boolean contains(String key) {
boolean exists = false;
if (StringUtils.isEmpty(key) == true) {
return exists;
}
Object obj = get(key);
if (obj != null) {
exists = true;
}
return exists;
}
/**
* 獲取過期時間 單位:毫秒
*
* @param expireTime 傳人的過期時間 單位毫秒 如小于1分鐘,默認(rèn)為10分鐘
**/
private Long getExpireTime(Long expireTime) {
Long result = expireTime;
if (expireTime == null || expireTime < AppConst.CACHE_MINUTE / 10) {
result = AppConst.CACHE_MINUTE;
}
return result;
}
}
注意:很多教程里都講到通過注解的方式(@Cacheable,@CachePut、@CacheEvict和@Caching)實現(xiàn)數(shù)據(jù)緩存,根據(jù)實踐,我個人是不推崇這種使用方式的。
這個也是開發(fā)和運維過程中非常經(jīng)典的問題。
有些公司寫緩存客戶端的時候,會給每個團(tuán)隊分別定義一個Area,但是這個只能做到緩存鍵的分布區(qū)分,不能保證緩存“實時”有效的過期。
多年以前我寫過一篇結(jié)合實際情況的文章,也就是加上緩存版本,請猛擊這里 ,算是提供了一種相對有效的方案,不過高并發(fā)站點要慎重,防止發(fā)生雪崩效應(yīng)。
Redis還有一些其他常見問題,比如:Redis的字符串類型Key和Value都有限制,且都是不能超過512M,請猛擊這里。還有最大連接數(shù)和超時時間設(shè)置等問題,本文就不再一一列舉了。
在配置文件中,加上緩存提供者開關(guān):
##是否啟用本地緩存
spring.power.isuselocalcache=1
##是否啟用Redis緩存
spring.power.isuserediscache=1
緩存提供者程序都實現(xiàn)好了,我們會再包裝一個調(diào)用外觀類PowerCacheBuilder,加上緩存版本控制,可以輕松自如地控制和切換緩存,code talks:
package com.power.demo.cache;
import com.google.common.collect.Lists;
import com.power.demo.cache.contract.CacheProviderService;
import com.power.demo.common.AppConst;
import com.power.demo.common.AppField;
import com.power.demo.util.ConfigUtil;
import com.power.demo.util.PowerLogger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Function;
/*
* 支持多緩存提供程序多級緩存的緩存幫助類
* */
@Configuration
@ComponentScan(basePackages = AppConst.BASE_PACKAGE_NAME)
public class PowerCacheBuilder {
@Autowired
@Qualifier("localCacheService")
private CacheProviderService localCacheService;
@Autowired
@Qualifier("redisCacheService")
private CacheProviderService redisCacheService;
private static List<CacheProviderService> _listCacheProvider = Lists.newArrayList();
private static final Lock providerLock = new ReentrantLock();
/**
* 初始化緩存提供者 默認(rèn)優(yōu)先級:先本地緩存,后分布式緩存
**/
private List<CacheProviderService> getCacheProviders() {
if (_listCacheProvider.size() > 0) {
return _listCacheProvider;
}
//線程安全
try {
providerLock.tryLock(1000, TimeUnit.MILLISECONDS);
if (_listCacheProvider.size() > 0) {
return _listCacheProvider;
}
String isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_LOCAL_CACHE);
CacheProviderService cacheProviderService = null;
//啟用本地緩存
if ("1".equalsIgnoreCase(isUseCache)) {
_listCacheProvider.add(localCacheService);
}
isUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
//啟用Redis緩存
if ("1".equalsIgnoreCase(isUseCache)) {
_listCacheProvider.add(redisCacheService);
resetCacheVersion();//設(shè)置分布式緩存版本號
}
PowerLogger.info("初始化緩存提供者成功,共有" + _listCacheProvider.size() + "個");
} catch (Exception e) {
e.printStackTrace();
_listCacheProvider = Lists.newArrayList();
PowerLogger.error("初始化緩存提供者發(fā)生異常:{}", e);
} finally {
providerLock.unlock();
}
return _listCacheProvider;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
**/
public <T extends Object> T get(String key) {
T obj = null;
//key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
for (CacheProviderService provider : getCacheProviders()) {
obj = provider.get(key);
if (obj != null) {
return obj;
}
}
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
**/
public <T extends Object> T get(String key, Function<String, T> function) {
T obj = null;
for (CacheProviderService provider : getCacheProviders()) {
if (obj == null) {
obj = provider.get(key, function);
} else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
provider.get(key, function);
}
//如果callable函數(shù)為空 而緩存對象不為空 及時跳出循環(huán)并返回
if (function == null && obj != null) {
return obj;
}
}
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
**/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm) {
T obj = null;
for (CacheProviderService provider : getCacheProviders()) {
if (obj == null) {
obj = provider.get(key, function, funcParm);
} else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
provider.get(key, function, funcParm);
}
//如果callable函數(shù)為空 而緩存對象不為空 及時跳出循環(huán)并返回
if (function == null && obj != null) {
return obj;
}
}
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object> T get(String key, Function<String, T> function, long expireTime) {
T obj = null;
for (CacheProviderService provider : getCacheProviders()) {
if (obj == null) {
obj = provider.get(key, function, expireTime);
} else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
provider.get(key, function, expireTime);
}
//如果callable函數(shù)為空 而緩存對象不為空 及時跳出循環(huán)并返回
if (function == null && obj != null) {
return obj;
}
}
return obj;
}
/**
* 查詢緩存
*
* @param key 緩存鍵 不可為空
* @param function 如沒有緩存,調(diào)用該callable函數(shù)返回對象 可為空
* @param funcParm function函數(shù)的調(diào)用參數(shù)
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object, M extends Object> T get(String key, Function<M, T> function, M funcParm, long expireTime) {
T obj = null;
for (CacheProviderService provider : getCacheProviders()) {
if (obj == null) {
obj = provider.get(key, function, funcParm, expireTime);
} else if (function != null && obj != null) {//查詢并設(shè)置其他緩存提供者程序緩存
provider.get(key, function, funcParm, expireTime);
}
//如果callable函數(shù)為空 而緩存對象不為空 及時跳出循環(huán)并返回
if (function == null && obj != null) {
return obj;
}
}
return obj;
}
/**
* 設(shè)置緩存鍵值 直接向緩存中插入或覆蓋值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
**/
public <T extends Object> void set(String key, T obj) {
//key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
for (CacheProviderService provider : getCacheProviders()) {
provider.set(key, obj);
}
}
/**
* 設(shè)置緩存鍵值 直接向緩存中插入或覆蓋值
*
* @param key 緩存鍵 不可為空
* @param obj 緩存值 不可為空
* @param expireTime 過期時間(單位:毫秒) 可為空
**/
public <T extends Object> void set(String key, T obj, Long expireTime) {
//key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
for (CacheProviderService provider : getCacheProviders()) {
provider.set(key, obj, expireTime);
}
}
/**
* 移除緩存
*
* @param key 緩存鍵 不可為空
**/
public void remove(String key) {
//key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
if (StringUtils.isEmpty(key) == true) {
return;
}
for (CacheProviderService provider : getCacheProviders()) {
provider.remove(key);
}
}
/**
* 是否存在緩存
*
* @param key 緩存鍵 不可為空
**/
public boolean contains(String key) {
boolean exists = false;
//key = generateVerKey(key);//構(gòu)造帶版本的緩存鍵
if (StringUtils.isEmpty(key) == true) {
return exists;
}
Object obj = get(key);
if (obj != null) {
exists = true;
}
return exists;
}
/**
* 獲取分布式緩存版本號
**/
public String getCacheVersion() {
String version = "";
boolean isUseCache = checkUseRedisCache();
//未啟用Redis緩存
if (isUseCache == false) {
return version;
}
version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);
return version;
}
/**
* 重置分布式緩存版本 如果啟用分布式緩存,設(shè)置緩存版本
**/
public String resetCacheVersion() {
String version = "";
boolean isUseCache = checkUseRedisCache();
//未啟用Redis緩存
if (isUseCache == false) {
return version;
}
//設(shè)置緩存版本
version = String.valueOf(Math.abs(UUID.randomUUID().hashCode()));
redisCacheService.set(AppConst.CACHE_VERSION_KEY, version);
return version;
}
/**
* 如果啟用分布式緩存,獲取緩存版本,重置查詢的緩存key,可以實現(xiàn)相對實時的緩存過期控制
* <p>
* 如沒有啟用分布式緩存,緩存key不做修改,直接返回
**/
public String generateVerKey(String key) {
String result = key;
if (StringUtils.isEmpty(key) == true) {
return result;
}
boolean isUseCache = checkUseRedisCache();
//沒有啟用分布式緩存,緩存key不做修改,直接返回
if (isUseCache == false) {
return result;
}
String version = redisCacheService.get(AppConst.CACHE_VERSION_KEY);
if (StringUtils.isEmpty(version) == true) {
return result;
}
result = String.format("%s_%s", result, version);
return result;
}
/**
* 驗證是否啟用分布式緩存
**/
private boolean checkUseRedisCache() {
boolean isUseCache = false;
String strIsUseCache = ConfigUtil.getConfigVal(AppField.IS_USE_REDIS_CACHE);
isUseCache = "1".equalsIgnoreCase(strIsUseCache);
return isUseCache;
}
}
單元測試如下:
@Test
public void testCacheVerson() throws Exception {
String version = cacheBuilder.getCacheVersion();
System.out.println(String.format("當(dāng)前緩存版本:%s", version));
String cacheKey = cacheBuilder.generateVerKey("goods778899");
GoodsVO goodsVO = new GoodsVO();
goodsVO.setGoodsId(UUID.randomUUID().toString());
goodsVO.setCreateTime(new Date());
goodsVO.setCreateDate(new DateTime(new Date()));
goodsVO.setGoodsType(1024);
goodsVO.setGoodsCode("123456789");
goodsVO.setGoodsName("我的測試商品");
cacheBuilder.set(cacheKey, goodsVO);
GoodsVO goodsVO1 = cacheBuilder.get(cacheKey);
Assert.assertNotNull(goodsVO1);
version = cacheBuilder.resetCacheVersion();
System.out.println(String.format("重置后的緩存版本:%s", version));
cacheKey = cacheBuilder.generateVerKey("goods112233");
cacheBuilder.set(cacheKey, goodsVO);
GoodsVO goodsVO2 = cacheBuilder.get(cacheKey);
Assert.assertNotNull(goodsVO2);
Assert.assertTrue("兩個緩存對象的主鍵相同", goodsVO1.getGoodsId().equals(goodsVO2.getGoodsId()));
}
一個滿足基本功能的多級緩存系統(tǒng)就好了。
在Spring Boot應(yīng)用中使用緩存則非常簡潔,選擇調(diào)用上面包裝好的緩存接口即可。
String cacheKey = _cacheBuilder.generateVerKey("com.power.demo.apiservice.impl.getgoodsbyid." + request.getGoodsId());
GoodsVO goodsVO = _cacheBuilder.get(cacheKey, _goodsService::getGoodsByGoodsId, request.getGoodsId());
到這里Spring Boot業(yè)務(wù)系統(tǒng)開發(fā)中最常用到的ORM,緩存和隊列三板斧就介紹完了。
在開發(fā)的過程中你會發(fā)現(xiàn),Java真的是非常非常中規(guī)中矩的語言,你需要不斷折騰并熟悉常見的開源中間件和工具,開源的輪子實在是太豐富,多嘗試幾個,實踐出真知。
看完上述內(nèi)容是否對您有幫助呢?如果還想對相關(guān)知識有進(jìn)一步的了解或閱讀更多相關(guān)文章,請關(guān)注億速云行業(yè)資訊頻道,感謝您對億速云的支持。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。