上一篇Spring Boot Cache + redis 設置有效時間和自動刷新緩存,時間支持在配置文件中配置,說了一種時間方式,直接擴展註解的Value值,如:java
@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() + "數據作了緩存"); System.out.println(redisTemplate); return p; }
可是這種方式有一個弊端就是破壞了原有Spring Cache架構,致使若是後期想換緩存就會去改不少代碼。git
RedisCacheManager能夠在配置CacheManager的Bean的時候指定過時時間,如:github
@Bean public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) { RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate); // 開啓使用緩存名稱最爲key前綴 redisCacheManager.setUsePrefix(true); //這裏能夠設置一個默認的過時時間 單位是秒 redisCacheManager.setDefaultExpiration(redisDefaultExpiration); // 設置緩存的過時時間 Map<String, Long> expires = new HashMap<>(); expires.put("people", 1000); redisCacheManager.setExpires(expires); return redisCacheManager; }
咱們借鑑一下redisCacheManager.setExpires(expires)思路,進行擴展。直接新建一個CacheTime類,來存過時時間和自動刷新時間。redis
在RedisCacheManager調用getCache(name)獲取緩存的時候,當沒有找到緩存的時候會調用getMissingCache(String cacheName)來新建緩存。在新建緩存的時候咱們能夠在擴展的Map<String, CacheTime> cacheTimes裏面根據key獲取CacheTime進而拿到有效時間和自動刷新時間。spring
CacheTime:緩存
/** * @author yuhao.wang */ public class CacheTime { public CacheTime(long preloadSecondTime, long expirationSecondTime) { this.preloadSecondTime = preloadSecondTime; this.expirationSecondTime = expirationSecondTime; } /** * 緩存主動在失效前強制刷新緩存的時間 * 單位:秒 */ private long preloadSecondTime = 0; /** * 緩存有效時間 */ private long expirationSecondTime; public long getPreloadSecondTime() { return preloadSecondTime; } public long getExpirationSecondTime() { return expirationSecondTime; } }
和上一篇的CustomizedRedisCache類同樣,主要解決:架構
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; /** * 緩存主動在失效前強制刷新緩存的時間 * 單位:秒 */ private long preloadSecondTime = 0; /** * 緩存有效時間 */ 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; // 指定有效時間 this.expirationSecondTime = expiration; // 指定自動刷新時間 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; // 指定有效時間 this.expirationSecondTime = expiration; // 指定自動刷新時間 this.preloadSecondTime = preloadSecondTime; this.prefix = prefix; } /** * 重寫get方法,獲取到緩存後再次取緩存剩餘的時間,若是時間小余咱們配置的刷新時間就手動刷新緩存。 * 爲了避免影響get的性能,啓用後臺線程去完成緩存的刷。 * 而且只放一個線程去刷新數據。 * * @param key * @return */ @Override public ValueWrapper get(final Object key) { RedisCacheKey cacheKey = getRedisCacheKey(key); String cacheKeyStr = getCacheKey(key); // 調用重寫後的get方法 ValueWrapper valueWrapper = this.get(cacheKey); if (null != valueWrapper) { // 刷新緩存數據 refreshCache(key, cacheKeyStr); } return valueWrapper; } /** * 重寫父類的get函數。 * 父類的get方法,是先使用exists判斷key是否存在,不存在返回null,存在再到redis緩存中去取值。這樣會致使併發問題, * 假若有一個請求調用了exists函數判斷key存在,可是在下一時刻這個緩存過時了,或者被刪掉了。 * 這時候再去緩存中獲取值的時候返回的就是null了。 * 能夠先獲取緩存的值,再去判斷key是否存在。 * * @param cacheKey * @return */ @Override public RedisCacheElement get(final RedisCacheKey cacheKey) { Assert.notNull(cacheKey, "CacheKey must not be null!"); // 根據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; } /** * 刷新緩存數據 */ private void refreshCache(Object key, String cacheKeyStr) { Long ttl = this.redisOperations.getExpire(cacheKeyStr); if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) { // 儘可能少的去開啓線程,由於線程池是有限的 ThreadTaskUtils.run(new Runnable() { @Override public void run() { // 加一個分佈式鎖,只放一個請求去刷新緩存 RedisLock redisLock = new RedisLock((RedisTemplate) redisOperations, cacheKeyStr + "_lock"); try { if (redisLock.lock()) { // 獲取鎖以後再判斷一下過時時間,看是否須要加載數據 Long ttl = CustomizedRedisCache.this.redisOperations.getExpire(cacheKeyStr); if (null != ttl && ttl <= CustomizedRedisCache.this.preloadSecondTime) { // 經過獲取代理方法信息從新加載緩存數據 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()); } }
主要擴展經過getCache(String name)方法獲取緩存的時候,當沒有找到緩存回去調用getMissingCache(String cacheName)來新建緩存。app
CustomizedRedisCacheManager:框架
/** * 自定義的redis緩存管理器 * 支持方法上配置過時時間 * 支持熱加載緩存:緩存即將過時時主動刷新緩存 * * @author yuhao.wang */ public class CustomizedRedisCacheManager extends RedisCacheManager { private static final Logger logger = LoggerFactory.getLogger(CustomizedRedisCacheManager.class); /** * 父類dynamic字段 */ private static final String SUPER_FIELD_DYNAMIC = "dynamic"; /** * 父類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; } /** * 獲取過時時間 * * @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; } /** * 獲取自動刷新時間 * * @return */ private long getPreloadSecondTime(String name) { // 自動刷新時間,默認是0 CacheTime cacheTime = null; if (!CollectionUtils.isEmpty(cacheTimes)) { cacheTime = cacheTimes.get(name); } Long preloadSecondTime = cacheTime != null ? cacheTime.getPreloadSecondTime() : 0; return preloadSecondTime < 0 ? 0 : preloadSecondTime; } /** * 建立緩存 * * @param cacheName 緩存名稱 * @return */ public CustomizedRedisCache getMissingCache(String cacheName) { // 有效時間,初始化獲取默認的有效時間 Long expirationSecondTime = getExpirationSecondTime(cacheName); // 自動刷新時間,默認是0 Long preloadSecondTime = getPreloadSecondTime(cacheName); logger.info("緩存 cacheName:{},過時時間:{}, 自動刷新時間:{}", cacheName, expirationSecondTime, preloadSecondTime); // 是否在運行時建立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; } /** * 根據緩存名稱設置緩存的有效時間和刷新時間,單位秒 * * @param cacheTimes */ public void setCacheTimess(Map<String, CacheTime> cacheTimes) { this.cacheTimes = (cacheTimes != null ? new ConcurrentHashMap<String, CacheTime>(cacheTimes) : null); } /** * 設置默認的過去時間, 單位:秒 * * @param defaultExpireTime */ @Override public void setDefaultExpiration(long defaultExpireTime) { super.setDefaultExpiration(defaultExpireTime); this.defaultExpiration = defaultExpireTime; } @Deprecated @Override public void setExpires(Map<String, Long> expires) { } }
@Bean public RedisCacheManager cacheManager(RedisTemplate<String, Object> redisTemplate) { CustomizedRedisCacheManager redisCacheManager = new CustomizedRedisCacheManager(redisTemplate); // 開啓使用緩存名稱最爲key前綴 redisCacheManager.setUsePrefix(true); //這裏能夠設置一個默認的過時時間 單位是秒 redisCacheManager.setDefaultExpiration(redisDefaultExpiration); // 設置緩存的過時時間和自動刷新時間 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; }
剩餘的建立切面來緩存方法信息請看上篇
源碼地址: https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases
spring-boot-student-cache-redis-2 工程
爲監控而生的多級緩存框架 layering-cache這是我開源的一個多級緩存框架的實現,若是有興趣能夠看一下