【架構師】緩存--如何更優雅的做緩存

    本博文主要講解如何更加優雅的做緩存,緩存服務器與基礎客戶端連接、緩存代碼在博主的博文《MemCached的安裝和JAVA客戶端連接Memcached示例代碼》有介紹。優雅緩存的代碼都是基於那篇博文基礎上完成的。主要業務代碼有如下:

 

/**  
* 緩存客戶端接口類 
*/  
public interface CacheClient {  
   .......
}

 

 

/**  
* memcached緩存客戶端持有者,交於Spring實例化
*/  
@Component  
public class MemcachedClientHolder implements CacheClient{
   //實現緩存接口
}

    有了如上的代碼,就可以在業務代碼中使用緩存功能了。

 

    但是我們可以想想,是否可以更加簡單優雅的做緩存呢,是否可以讓業務方更加簡單的編寫緩存使用的業務代碼? 答案當然是可以的,請看如下分析。

 

業務場景描述

    例如網易新聞客戶端,剛開始打開體育版塊的時候,每個人看到的內容都是一樣(暫不考慮千人千面推薦的情況),這個時候就非常適用緩存,但是業務代碼中使用緩存就顯得有些冗餘,其實完全可以在Web應用RestApi接口級別做緩存。大概思路如下:

1:即自定義編寫一個註解,用於收集緩存的時間、key前綴、排除的參數等信息。

2:在業務接口方法頭中,設置好註解。這樣就可以不用編寫任何使用緩存的業務代碼了。

各種購物網站首頁的 banner、icon都是一樣的道理,只是緩存的時間不同而已。如下詳細講解如上兩點如何去做。

 

 

自定義註解

package com.cloud.practice.demo.cache.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 對Controller接口進行緩存
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cachable {

    /**
     * Expire time in seconds. default 取配置文件中的expire時間
     * @return  expire time 時間爲秒,默認60秒
     */
    int expire() default  60;
    
    /**
     * 緩存Key的前綴,以防不同Controller相同接口相同參數導致Key/Value混亂
     * @return  默認tt
     */
    String prefix() default  "tt";

    /**
     * 排除掉那些參數
     * @return 要排除的參數次序,從0開始
     */
    int[] excludeArgs() default {};

}

 

 

 

 

 

 

在自定義註解上做AOP切面編程

package com.cloud.practice.demo.cache.aop;


/**
 * 對Controller的接口方法進行AOP,實現HTTP 接口的cache。 請求的處理流程:
 * 1.從cache中找,看是否hit,如果hit,直接返回結果
 * 2.如果沒有命中,調用服務,調用完成後寫入cacle中
 */
@Aspect
@Component
public class ControllerCacheAop {

	
	@Value("${cache.servers.memcached.expire:60}")
	private int memcachedServiceExpire ;
	
  @Autowired
  CacheClient memecacheClientHolder;
    
	@Autowired
	private HttpSession session;

    //定義一個切面在Controller上面
    @Pointcut("within(@org.springframework.stereotype.Controller *)")
    public void controllerPointcut() {
    }
    @Pointcut("within(@ org.springframework.web.bind.annotation.RestController *)")
    public void restControllerPointcut() {
    }

		//定義一個切面在@Cachable註解上面
    @Pointcut("@annotation(com.cloud.tasktrack.core.aop.Cachable)")
    public void cacheAnnotationPointCut() {
    }


    /**
     * 先從cache中取數據,找不到則調用controller原來的實現,後在寫入緩存
     */
    @Around("(controllerPointcut() || restControllerPointcut()) &&  cacheAnnotationPointCut() ")
    @Order(1)
    public Object cacheAop(ProceedingJoinPoint joinPoint) throws Throwable {

        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        //返回類型
        final Class<?> returnType = signature.getMethod().getReturnType();

    	//獲得方法名、類名
    	String intefaceName=signature.getMethod().getName();
    	String className=joinPoint.getTarget().getClass().toString();
    	if(className.contains(".")){
				String[] _classNameTmp=className.split("\\.");
				className=_classNameTmp[_classNameTmp.length-1];
			}
    	long start= System.currentTimeMillis();
        
        //獲得@Cachable註解的相關信息
        Cachable cacheAnnotation= signature.getMethod().getAnnotation(Cachable.class);

        final String cacheKey = this.getCacheKey(joinPoint, cacheAnnotation);
        //從緩存中獲取數據
        try {
            Object _obj = this.memecacheClientHolder.get(cacheKey, returnType);
            if (_obj != null) {
            	long usedTimeMs= System.currentTimeMillis()-start;
                
        		User operationUser=(User)session.getAttribute("user");
        		String operationUserWebId=null,operationUserId=null;
        		if(null!=operationUser){
        			operationUserWebId=String.valueOf(operationUser.getWebId());
        			operationUserId=String.valueOf(operationUser.getId());
        		}
        		log.debug("============ Cache log: "+"webId "+ operationUserWebId + " userId "+ operationUserId);
        		
        		log.debug("============ Cache hit for key [" + cacheKey + "]" + " in " + usedTimeMs + "ms. " + " value is [" + _obj +"]");
                           
            return _obj;
            }
        }catch (Exception e){
        	log.warn("============ Cache exception for key[" + cacheKey + "]"+" Exception is : "+e.getMessage());
        }

        //緩存沒有命中: 調用原來的請求,然後將結果緩存到cache中。
        Object intefaceCallResult;
        try {
        	intefaceCallResult = joinPoint.proceed();
            if (intefaceCallResult != null) {
                //一級緩存失效的時間,如果指定了,則使用指定的值。如果沒有指定,則使用配置的值
                int expire = cacheAnnotation.expire();
                if (expire <= 0) {
                    expire = this.memcachedServiceExpire;
                }
                this.memecacheClientHolder.set(cacheKey, expire, intefaceCallResult);
                log.debug("============ Set cache value for key[" + cacheKey + "]" + " value in[" + intefaceCallResult +"]");
            }
        } catch (Exception e) {
        	intefaceCallResult=new ApiResponse.ApiResponseBuilder().code(ApiResponse.SERVICE_ERROR_CODE).message("服務調用異常"+e.getMessage()).build() ;
        }
        return intefaceCallResult;
    }

    /**
     * 根據攔截的方法的參數,生成cache的key.  prefix:METHOD_NAME:METHOD_PARAM
     * @param joinPoint   攔截點
     * @param cacheExpire
     * @return key
     */
    private String getCacheKey(ProceedingJoinPoint joinPoint, Cachable cacheExpire) {

        //獲得要排除的方法參數
        int[] excludeParams = cacheExpire.excludeArgs();
        //獲得要換成Key的前綴
        String prefix=cacheExpire.prefix();
        String cacheKeyPrefix = prefix+":" + joinPoint.getSignature().getName();

        //把參數連接起來
        List<String> methodParams = new LinkedList<>();
        Object arguments[] = joinPoint.getArgs();
        if (ArrayUtils.isNotEmpty(arguments)) {
            for (int i = 0; i < arguments.length; i++) {
                //排除掉某些參數
                if (ArrayUtils.contains(excludeParams, i)) {
                    continue;
                }
                Object arg = arguments[i];
                //fix key contain ' ' || b == '\n' || b == '\r' || b == 0
                String param = (arg == null ? "" : String.valueOf(arg));
                methodParams.add(param);
            }
        }
        return cacheKeyPrefix + ":" + String.join("-", methodParams);
    }
}

 

業務代碼層面如何使用此種優雅緩存

package com.cloud.practice.demo.cache;

@Controller
@RequestMapping("/post")
public class CacheUsedDemoController {

    @Autowired
    private PostService postService;



    /**
     * 根據站點ID,站點ID(可有可無),查詢所有崗位信息,需要附帶返回站點下面的用戶信息
     * @param request
     * @return
     */
    @RequestMapping("/getPostUser")
    @Cachable(expire = 120, prefix = "post", excludeArgs = 2)  //這種就使用了此種緩存
    @Loggerable(interfaceDesc = "查詢崗位與崗位下用戶信息")
    @ResponseBody
    public ApiResponse getPostUser(HttpServletRequest request) {
        ApiResponse apiResponse;
        int webId = CloudRequestPropertyUtils.getWebIdFromRequest(request);
        if (webId<1) {
            apiResponse = new ApiResponse.ApiResponseBuilder().code(ApiResponse.DEFAULT_CODE).message("站點ID不能爲空,請重新輸入").build();
        } else {
            List<Post> postUsers = postService.getPostUser(webId);
            apiResponse = new ApiResponse.ApiResponseBuilder().code(ApiResponse.DEFAULT_CODE).message(ApiResponse.DEFAULT_MSG)
                    .data(postUsers).dataCount(postUsers.size()).build();
        }
     		return apiResponse;
     }
}

 

 

    是不是感覺很棒很方便,大家不妨自己試試,的確很方便。此種思想也可以應用到日誌記錄等功能當中。

 

業務場景描述

表示java虛擬機堆區內存初始內存分配的大小,通常爲操作系統可用內存的1/64大小即可,但仍需按照實際情況進行分配。
表示java虛擬機堆區內存可被分配的最大上限,通常爲操作系統可用內存的1/4大小。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章