問(wèn)題
上一篇Spring Boot Cache + redis 設(shè)置有效時(shí)間和自動(dòng)刷新緩存,時(shí)間支持在配置文件中配置晨雳,說(shuō)了一種時(shí)間方式锋谐,直接擴(kuò)展注解的Value值,如:
@Override
@Cacheable(value = "people#${select.cache.timeout:1800}#${select.cache.refresh:600}", key = "#person.id", sync = true)
public Person findOne(Person person, String a, String[] b, List<Long> c) {
Person p = personRepository.findOne(person.getId());
System.out.println("為id质蕉、key為:" + p.getId() + "數(shù)據(jù)做了緩存");
System.out.println(redisTemplate);
return p;
}
但是這種方式有一個(gè)弊端就是破壞了原有Spring Cache架構(gòu),導(dǎo)致如果后期想換緩存就會(huì)去改很多代碼翩肌。
解決思路
RedisCacheManager可以在配置CacheManager的Bean的時(shí)候指定過(guò)期時(shí)間模暗,如:
@Bean
public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate);
// 開(kāi)啟使用緩存名稱(chēng)最為key前綴
redisCacheManager.setUsePrefix(true);
//這里可以設(shè)置一個(gè)默認(rèn)的過(guò)期時(shí)間 單位是秒
redisCacheManager.setDefaultExpiration(redisDefaultExpiration);
// 設(shè)置緩存的過(guò)期時(shí)間
Map<String, Long> expires = new HashMap<>();
expires.put("people", 1000);
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
我們借鑒一下redisCacheManager.setExpires(expires)思路,進(jìn)行擴(kuò)展念祭。直接新建一個(gè)CacheTime類(lèi)兑宇,來(lái)存過(guò)期時(shí)間和自動(dòng)刷新時(shí)間。
在RedisCacheManager調(diào)用getCache(name)獲取緩存的時(shí)候粱坤,當(dāng)沒(méi)有找到緩存的時(shí)候會(huì)調(diào)用getMissingCache(String cacheName)來(lái)新建緩存隶糕。在新建緩存的時(shí)候我們可以在擴(kuò)展的Map<String, CacheTime> cacheTimes里面根據(jù)key獲取CacheTime進(jìn)而拿到有效時(shí)間和自動(dòng)刷新時(shí)間瓷产。
具體實(shí)現(xiàn)
我們先新建CacheTime類(lèi)
CacheTime:
/**
* @author yuhao.wang
*/
public class CacheTime {
public CacheTime(long preloadSecondTime, long expirationSecondTime) {
this.preloadSecondTime = preloadSecondTime;
this.expirationSecondTime = expirationSecondTime;
}
/**
* 緩存主動(dòng)在失效前強(qiáng)制刷新緩存的時(shí)間
* 單位:秒
*/
private long preloadSecondTime = 0;
/**
* 緩存有效時(shí)間
*/
private long expirationSecondTime;
public long getPreloadSecondTime() {
return preloadSecondTime;
}
public long getExpirationSecondTime() {
return expirationSecondTime;
}
}
擴(kuò)展一下RedisCache類(lèi)
和上一篇的CustomizedRedisCache類(lèi)一樣,主要解決:
- 獲取緩存的在大并發(fā)下的一個(gè)bug枚驻,詳情
- 在獲取緩存的時(shí)候判斷一下緩存的過(guò)期時(shí)間和自動(dòng)刷新時(shí)間濒旦,根據(jù)這個(gè)值去刷新緩存
CustomizedRedisCache:
/**
* 自定義的redis緩存
*
* @author yuhao.wang
*/
public class CustomizedRedisCache extends RedisCache {
private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCache.class);
private CacheSupport getCacheSupport() {
return SpringContextUtils.getBean(CacheSupport.class);
}
private final RedisOperations redisOperations;
private final byte[] prefix;
/**
* 緩存主動(dòng)在失效前強(qiáng)制刷新緩存的時(shí)間
* 單位:秒
*/
private long preloadSecondTime = 0;
/**
* 緩存有效時(shí)間
*/
private long expirationSecondTime;
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, long preloadSecondTime) {
super(name, prefix, redisOperations, expiration);
this.redisOperations = redisOperations;
// 指定有效時(shí)間
this.expirationSecondTime = expiration;
// 指定自動(dòng)刷新時(shí)間
this.preloadSecondTime = preloadSecondTime;
this.prefix = prefix;
}
public CustomizedRedisCache(String name, byte[] prefix, RedisOperations<? extends Object, ? extends Object> redisOperations, long expiration, long preloadSecondTime, boolean allowNullValues) {
super(name, prefix, redisOperations, expiration, allowNullValues);
this.redisOperations = redisOperations;
// 指定有效時(shí)間
this.expirationSecondTime = expiration;
// 指定自動(dòng)刷新時(shí)間
this.preloadSecondTime = preloadSecondTime;
this.prefix = prefix;
}
/**
* 重寫(xiě)get方法,獲取到緩存后再次取緩存剩余的時(shí)間再登,如果時(shí)間小余我們配置的刷新時(shí)間就手動(dòng)刷新緩存尔邓。
* 為了不影響get的性能,啟用后臺(tái)線(xiàn)程去完成緩存的刷锉矢。
* 并且只放一個(gè)線(xiàn)程去刷新數(shù)據(jù)铃拇。
*
* @param key
* @return
*/
@Override
public ValueWrapper get(final Object key) {
RedisCacheKey cacheKey = getRedisCacheKey(key);
String cacheKeyStr = getCacheKey(key);
// 調(diào)用重寫(xiě)后的get方法
ValueWrapper valueWrapper = this.get(cacheKey);
if (null != valueWrapper) {
// 刷新緩存數(shù)據(jù)
refreshCache(key, cacheKeyStr);
}
return valueWrapper;
}
/**
* 重寫(xiě)父類(lèi)的get函數(shù)。
* 父類(lèi)的get方法沈撞,是先使用exists判斷key是否存在,不存在返回null雕什,存在再到redis緩存中去取值缠俺。這樣會(huì)導(dǎo)致并發(fā)問(wèn)題,
* 假如有一個(gè)請(qǐng)求調(diào)用了exists函數(shù)判斷key存在贷岸,但是在下一時(shí)刻這個(gè)緩存過(guò)期了壹士,或者被刪掉了。
* 這時(shí)候再去緩存中獲取值的時(shí)候返回的就是null了偿警。
* 可以先獲取緩存的值躏救,再去判斷key是否存在。
*
* @param cacheKey
* @return
*/
@Override
public RedisCacheElement get(final RedisCacheKey cacheKey) {
Assert.notNull(cacheKey, "CacheKey must not be null!");
// 根據(jù)key獲取緩存值
RedisCacheElement redisCacheElement = new RedisCacheElement(cacheKey, fromStoreValue(lookup(cacheKey)));
// 判斷key是否存在
Boolean exists = (Boolean) redisOperations.execute(new RedisCallback<Boolean>() {
@Override
public Boolean doInRedis(RedisConnection connection) throws DataAccessException {
return connection.exists(cacheKey.getKeyBytes());
}
});
if (!exists.booleanValue()) {
return null;
}
return redisCacheElement;
}
/**
* 刷新緩存數(shù)據(jù)
*/
private void refreshCache(Object key, String cacheKeyStr) {
Long ttl = this.redisOperations.getExpire(cacheKeyStr);
if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
// 盡量少的去開(kāi)啟線(xiàn)程螟蒸,因?yàn)榫€(xiàn)程池是有限的
ThreadTaskUtils.run(new Runnable() {
@Override
public void run() {
// 加一個(gè)分布式鎖盒使,只放一個(gè)請(qǐng)求去刷新緩存
RedisLock redisLock = new RedisLock((RedisTemplate) redisOperations, cacheKeyStr + "_lock");
try {
if (redisLock.lock()) {
// 獲取鎖之后再判斷一下過(guò)期時(shí)間,看是否需要加載數(shù)據(jù)
Long ttl = CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr);
if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) {
// 通過(guò)獲取代理方法信息重新加載緩存數(shù)據(jù)
CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(), cacheKeyStr);
}
}
} catch (Exception e) {
logger.info(e.getMessage(), e);
} finally {
redisLock.unlock();
}
}
});
}
}
public long getExpirationSecondTime() {
return expirationSecondTime;
}
/**
* 獲取RedisCacheKey
*
* @param key
* @return
*/
public RedisCacheKey getRedisCacheKey(Object key) {
return new RedisCacheKey(key).usePrefix(this.prefix)
.withKeySerializer(redisOperations.getKeySerializer());
}
/**
* 獲取RedisCacheKey
*
* @param key
* @return
*/
public String getCacheKey(Object key) {
return new String(getRedisCacheKey(key).getKeyBytes());
}
}
擴(kuò)展RedisCacheManager
主要擴(kuò)展通過(guò)getCache(String name)方法獲取緩存的時(shí)候七嫌,當(dāng)沒(méi)有找到緩存回去調(diào)用getMissingCache(String cacheName)來(lái)新建緩存少办。
CustomizedRedisCacheManager:
/**
* 自定義的redis緩存管理器
* 支持方法上配置過(guò)期時(shí)間
* 支持熱加載緩存:緩存即將過(guò)期時(shí)主動(dòng)刷新緩存
*
* @author yuhao.wang
*/
public class CustomizedRedisCacheManager extends RedisCacheManager {
private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class);
/**
* 父類(lèi)dynamic字段
*/
private static final String SUPER_FIELD_DYNAMIC = "dynamic";
/**
* 父類(lèi)cacheNullValues字段
*/
private static final String SUPER_FIELD_CACHENULLVALUES = "cacheNullValues";
RedisCacheManager redisCacheManager = null;
// 0 - never expire
private long defaultExpiration = 0;
private Map<String, CacheTime> cacheTimes = null;
public CustomizedRedisCacheManager(RedisOperations redisOperations) {
super(redisOperations);
}
public CustomizedRedisCacheManager(RedisOperations redisOperations, Collection<String> cacheNames) {
super(redisOperations, cacheNames);
}
public RedisCacheManager getInstance() {
if (redisCacheManager == null) {
redisCacheManager = SpringContextUtils.getBean(RedisCacheManager.class);
}
return redisCacheManager;
}
/**
* 獲取過(guò)期時(shí)間
*
* @return
*/
public long getExpirationSecondTime(String name) {
if (StringUtils.isEmpty(name)) {
return 0;
}
CacheTime cacheTime = null;
if (!CollectionUtils.isEmpty(cacheTimes)) {
cacheTime = cacheTimes.get(name);
}
Long expiration = cacheTime != null ? cacheTime.getExpirationSecondTime() : defaultExpiration;
return expiration < 0 ? 0 : expiration;
}
/**
* 獲取自動(dòng)刷新時(shí)間
*
* @return
*/
private long getPreloadSecondTime(String name) {
// 自動(dòng)刷新時(shí)間,默認(rèn)是0
CacheTime cacheTime = null;
if (!CollectionUtils.isEmpty(cacheTimes)) {
cacheTime = cacheTimes.get(name);
}
Long preloadSecondTime = cacheTime != null ? cacheTime.getPreloadSecondTime() : 0;
return preloadSecondTime < 0 ? 0 : preloadSecondTime;
}
/**
* 創(chuàng)建緩存
*
* @param cacheName 緩存名稱(chēng)
* @return
*/
public CustomizedRedisCache getMissingCache(String cacheName) {
// 有效時(shí)間诵原,初始化獲取默認(rèn)的有效時(shí)間
Long expirationSecondTime = getExpirationSecondTime(cacheName);
// 自動(dòng)刷新時(shí)間英妓,默認(rèn)是0
Long preloadSecondTime = getPreloadSecondTime(cacheName);
logger.info("緩存 cacheName:{},過(guò)期時(shí)間:{}, 自動(dòng)刷新時(shí)間:{}", cacheName, expirationSecondTime, preloadSecondTime);
// 是否在運(yùn)行時(shí)創(chuàng)建Cache
Boolean dynamic = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_DYNAMIC);
// 是否允許存放NULL
Boolean cacheNullValues = (Boolean) ReflectionUtils.getFieldValue(getInstance(), SUPER_FIELD_CACHENULLVALUES);
return dynamic ? new CustomizedRedisCache(cacheName, (this.isUsePrefix() ? this.getCachePrefix().prefix(cacheName) : null),
this.getRedisOperations(), expirationSecondTime, preloadSecondTime, cacheNullValues) : null;
}
/**
* 根據(jù)緩存名稱(chēng)設(shè)置緩存的有效時(shí)間和刷新時(shí)間绍赛,單位秒
*
* @param cacheTimes
*/
public void setCacheTimess(Map<String, CacheTime> cacheTimes) {
this.cacheTimes = (cacheTimes != null ? new ConcurrentHashMap<String, CacheTime>(cacheTimes) : null);
}
/**
* 設(shè)置默認(rèn)的過(guò)去時(shí)間蔓纠, 單位:秒
*
* @param defaultExpireTime
*/
@Override
public void setDefaultExpiration(long defaultExpireTime) {
super.setDefaultExpiration(defaultExpireTime);
this.defaultExpiration = defaultExpireTime;
}
@Deprecated
@Override
public void setExpires(Map<String, Long> expires) {
}
}
在Config中配置RedisCacheManager
@Bean
public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) {
CustomizedRedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate);
// 開(kāi)啟使用緩存名稱(chēng)最為key前綴
redisCacheManager.setUsePrefix(true);
//這里可以設(shè)置一個(gè)默認(rèn)的過(guò)期時(shí)間 單位是秒
redisCacheManager.setDefaultExpiration(redisDefaultExpiration);
// 設(shè)置緩存的過(guò)期時(shí)間和自動(dòng)刷新時(shí)間
Map<String, CacheTime> cacheTimes = new HashMap<>();
cacheTimes.put("people", new CacheTime(selectCacheTimeout, selectCacheRefresh));
cacheTimes.put("people1", new CacheTime(120, 115));
cacheTimes.put("people2", new CacheTime(120, 115));
redisCacheManager.setCacheTimess(cacheTimes);
return redisCacheManager;
}
剩余的創(chuàng)建切面來(lái)緩存方法信息請(qǐng)看上篇
源碼地址:
https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-cache-redis-2 工程
為監(jiān)控而生的多級(jí)緩存框架 layering-cache這是我開(kāi)源的一個(gè)多級(jí)緩存框架的實(shí)現(xiàn),如果有興趣可以看一下