Spring Cache擴展:註解失效時間+主動刷新緩存

Spring Cache 兩個需求

  • 緩存失效時間支持在方法的註解上指定
    Spring Cache默認是不支持在@Cacheable上添加過時時間的,能夠在配置緩存容器時統一指定:
@Bean
public CacheManager cacheManager(
        @SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
    CustomizedRedisCacheManager cacheManager= new CustomizedRedisCacheManager(redisTemplate);
    cacheManager.setDefaultExpiration(60);
    Map<String,Long> expiresMap=new HashMap<>();
    expiresMap.put("Product",5L);
    cacheManager.setExpires(expiresMap);
    return cacheManager;
}

想這樣配置過時時間,焦點在value的格式上Product#5#2,詳情下面會詳細說明。前端

    @Cacheable(value = {"Product#5#2"},key ="#id")
    

上面兩種各有利弊,並非說哪種必定要比另一種強,根據本身項目的實際狀況選擇。java

  • 在緩存即將過時時主動刷新緩存

通常緩存失效後,會有一些請求會打到後端的數據庫上,這段時間的訪問性能確定是比有緩存的狀況要差不少。因此指望在緩存即將過時的某一時間點後臺主動去更新緩存以確保前端請求的緩存命中率,示意圖以下:git

Srping 4.3提供了一個sync參數。是當緩存失效後,爲了不多個請求打到數據庫,系統作了一個併發控制優化,同時只有一個線程會去數據庫取數據其它線程會被阻塞。github

背景

我以Spring Cache +Redis爲前提來實現上面兩個需求,其它類型的緩存原理應該是相同的。redis

本文內容未在生產環境驗證過,也許有不妥的地方,請多多指出。spring

擴展RedisCacheManager

CustomizedRedisCacheManager

繼承自RedisCacheManager,定義兩個輔助性的屬性:數據庫

/**
     * 緩存參數的分隔符
     * 數組元素0=緩存的名稱
     * 數組元素1=緩存過時時間TTL
     * 數組元素2=緩存在多少秒開始主動失效來強制刷新
     */
    private String separator = "#";

    /**
     * 緩存主動在失效前強制刷新緩存的時間
     * 單位:秒
     */
    private long preloadSecondTime=0;

註解配置失效時間簡單的方法就是在容器名稱上動動手腳,經過解析特定格式的名稱來變向實現失效時間的獲取。好比第一個#後面的5能夠定義爲失效時間,第二個#後面的2是刷新緩存的時間,只須要重寫getCache:後端

  • 解析配置的value值,分別計算出真正的緩存名稱,失效時間以及緩存刷新的時間
  • 調用構造函數返回緩存對象
@Override
public Cache getCache(String name) {

    String[] cacheParams=name.split(this.getSeparator());
    String cacheName = cacheParams[0];

    if(StringUtils.isBlank(cacheName)){
        return null;
    }

    Long expirationSecondTime = this.computeExpiration(cacheName);

    if(cacheParams.length>1) {
        expirationSecondTime=Long.parseLong(cacheParams[1]);
        this.setDefaultExpiration(expirationSecondTime);
    }
    if(cacheParams.length>2) {
        this.setPreloadSecondTime(Long.parseLong(cacheParams[2]));
    }

    Cache cache = super.getCache(cacheName);
    if(null==cache){
        return cache;
    }
    logger.info("expirationSecondTime:"+expirationSecondTime);
    CustomizedRedisCache redisCache= new CustomizedRedisCache(
            cacheName,
            (this.isUsePrefix() ? this.getCachePrefix().prefix(cacheName) : null),
            this.getRedisOperations(),
            expirationSecondTime,
            preloadSecondTime);
    return redisCache;

}

CustomizedRedisCache

主要是實現緩存即將過時時可以主動觸發緩存更新,核心是下面這個get方法。在獲取到緩存後再次取緩存剩餘的時間,若是時間小余咱們配置的刷新時間就手動刷新緩存。爲了避免影響get的性能,啓用後臺線程去完成緩存的刷新。數組

public ValueWrapper get(Object key) {

    ValueWrapper valueWrapper= super.get(key);
    if(null!=valueWrapper){
        Long ttl= this.redisOperations.getExpire(key);
        if(null!=ttl&& ttl<=this.preloadSecondTime){
            logger.info("key:{} ttl:{} preloadSecondTime:{}",key,ttl,preloadSecondTime);
            ThreadTaskHelper.run(new Runnable() {
                @Override
                public void run() {
                    //從新加載數據
                    logger.info("refresh key:{}",key);

                    CustomizedRedisCache.this.getCacheSupport().refreshCacheByKey(CustomizedRedisCache.super.getName(),key.toString());
                }
            });

        }
    }
    return valueWrapper;
}

ThreadTaskHelper是個幫助類,但須要考慮重複請求問題,及相同的數據在併發過程當中只容許刷新一次,這塊尚未完善就不貼代碼了。緩存

攔截@Cacheable,並記錄執行方法信息

上面提到的緩存獲取時,會根據配置的刷新時間來判斷是否須要刷新數據,當符合條件時會觸發數據刷新。但它須要知道執行什麼方法以及更新哪些數據,因此就有了下面這些類。

CacheSupport

刷新緩存接口,可刷新整個容器的緩存也能夠只刷新指定鍵的緩存。

public interface CacheSupport {

	/**
	 * 刷新容器中全部值
	 * @param cacheName
     */
	void refreshCache(String cacheName);

	/**
	 * 按容器以及指定鍵更新緩存
	 * @param cacheName
	 * @param cacheKey
     */
	void refreshCacheByKey(String cacheName,String cacheKey);

}

InvocationRegistry

執行方法註冊接口,可以在適當的地方主動調用方法執行來完成緩存的更新。

public interface InvocationRegistry {

	void registerInvocation(Object invokedBean, Method invokedMethod, Object[] invocationArguments, Set<String> cacheNames);

}

CachedInvocation

執行方法信息類,這個比較簡單,就是知足方法執行的全部信息便可。

public final class CachedInvocation {

    private Object key;
    private final Object targetBean;
    private final Method targetMethod;
    private Object[] arguments;

    public CachedInvocation(Object key, Object targetBean, Method targetMethod, Object[] arguments) {
        this.key = key;
        this.targetBean = targetBean;
        this.targetMethod = targetMethod;
        if (arguments != null && arguments.length != 0) {
            this.arguments = Arrays.copyOf(arguments, arguments.length);
        }
    }

}

CacheSupportImpl

這個類主要實現上面定義的緩存刷新接口以及執行方法註冊接口

  • 刷新緩存
    獲取cacheManager用來操做緩存:
@Autowired
private CacheManager cacheManager;

實現緩存刷新接口方法:

@Override
public void refreshCache(String cacheName) {
	this.refreshCacheByKey(cacheName,null);
}

@Override
public void refreshCacheByKey(String cacheName, String cacheKey) {
	if (cacheToInvocationsMap.get(cacheName) != null) {
		for (final CachedInvocation invocation : cacheToInvocationsMap.get(cacheName)) {
			if(!StringUtils.isBlank(cacheKey)&&invocation.getKey().toString().equals(cacheKey)) {
				refreshCache(invocation, cacheName);
			}
		}
	}
}

反射來調用方法:

private Object invoke(CachedInvocation invocation)
			throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
	final MethodInvoker invoker = new MethodInvoker();
	invoker.setTargetObject(invocation.getTargetBean());
	invoker.setArguments(invocation.getArguments());
	invoker.setTargetMethod(invocation.getTargetMethod().getName());
	invoker.prepare();
	return invoker.invoke();
}

緩存刷新最後實際執行是這個方法,經過invoke函數獲取到最新的數據,而後經過cacheManager來完成緩存的更新操做。

private void refreshCache(CachedInvocation invocation, String cacheName) {

	boolean invocationSuccess;
	Object computed = null;
	try {
		computed = invoke(invocation);
		invocationSuccess = true;
	} catch (Exception ex) {
		invocationSuccess = false;
	}
	if (invocationSuccess) {
		if (cacheToInvocationsMap.get(cacheName) != null) {
			cacheManager.getCache(cacheName).put(invocation.getKey(), computed);
		}
	}
}
  • 執行方法信息註冊

定義一個Map用來存儲執行方法的信息:

private Map<String, Set<CachedInvocation>> cacheToInvocationsMap;

實現執行方法信息接口,構造執行方法對象而後存儲到Map中。

@Override
public void registerInvocation(Object targetBean, Method targetMethod, Object[] arguments, Set<String> annotatedCacheNames) {

	StringBuilder sb = new StringBuilder();
	for (Object obj : arguments) {
		sb.append(obj.toString());
	}

	Object key = sb.toString();

	final CachedInvocation invocation = new CachedInvocation(key, targetBean, targetMethod, arguments);
	for (final String cacheName : annotatedCacheNames) {
		String[] cacheParams=cacheName.split("#");
		String realCacheName = cacheParams[0];
		if(!cacheToInvocationsMap.containsKey(realCacheName)) {
			this.initialize();
		}
		cacheToInvocationsMap.get(realCacheName).add(invocation);
	}
}

CachingAnnotationsAspect

攔截@Cacheable方法信息並完成註冊,將使用了緩存的方法的執行信息存儲到Map中,key是緩存容器的名稱,value是不一樣參數的方法執行實例,核心方法就是registerInvocation。

@Around("pointcut()")
public Object registerInvocation(ProceedingJoinPoint joinPoint) throws Throwable{

	Method method = this.getSpecificmethod(joinPoint);

	List<Cacheable> annotations=this.getMethodAnnotations(method,Cacheable.class);

	Set<String> cacheSet = new HashSet<String>();
	for (Cacheable cacheables : annotations) {
		cacheSet.addAll(Arrays.asList(cacheables.value()));
	}
	cacheRefreshSupport.registerInvocation(joinPoint.getTarget(), method, joinPoint.getArgs(), cacheSet);
	return joinPoint.proceed();
}

客戶端調用

指定5秒後過時,而且在緩存存活3秒後若是請求命中,會在後臺啓動線程從新從數據庫中獲取數據來完成緩存的更新。理論上前端不會存在緩存不命中的狀況,固然若是正好最後兩秒沒有請求那也會出現緩存失效的狀況。

@Cacheable(value = {"Product#5#2"},key ="#id")
public Product getById(Long id) {
    //...
}

代碼

能夠從個人我的項目中下載。spring cache code

引用

刷新緩存的思路取自於這個開源項目。https://github.com/yantrashala/spring-cache-self-refresh

相關文章
相關標籤/搜索